Skip to content

fix: Correctness fixes in interval_operations - #777

Merged
OlivierHnt merged 15 commits into
JuliaIntervals:masterfrom
OlivierHnt:fix/interval-operations
Sep 5, 2026
Merged

OlivierHnt merged 15 commits into
JuliaIntervals:masterfrom
OlivierHnt:fix/interval-operations

Conversation

@OlivierHnt

Copy link
Copy Markdown
Member

This PR fixes a set of bugs in src/intervals/interval_operations/. The regression tests for every item are contributed separately, as part of the test-suite rework #775.

x = interval(-3.514641998989496e-149, 2.2210542297708545e247)
issubset_interval(x, interval(mid(x), radius(x); format = :midpoint))
# before: false — mid(x) ± radius(x) lost the negative part of x
# after:  true

(As a consequence, the midpoint display of e.g. interval(0, 1.3) is now the honest 0.65 ± 0.650001 instead of 0.65 ± 0.65.)

  • cancelminus and cancelplus threw a MethodError for every non-degenerate Rational interval, and did not propagate NaI when dec was given explicitly.
cancelminus(interval(1//1, 2//1), interval(1//2, 1//1))
# before: MethodError: no method matching prevfloat(::Rational{Int64})
# after:  [1//2, 1//1]_trv

isnai(cancelminus(nai(), interval(1, 2); dec = com))
# before: false, plus a spurious "interval part of NaI" warning
# after:  true, no warning
  • interiordiff returned the wrong piece when x ⊆ y and they share one endpoint, and the scalar and vector paths disagreed on identical inputs. Both now return exactly the pieces of x \ interior(y).
interiordiff(interval(2, 3), interval(1, 3))          # x \ int(y) = {3}
# before: [[2.0, 2.0]_trv] — but 2 is in the interior of y
# after:  [[3.0, 3.0]_trv]

only.(interiordiff([interval(1, 2)], [interval(1, 3)]))  # x \ int(y) = {1}
# before: [[2.0, 2.0]_trv] — and the scalar call returned {1}
# after:  [[1.0, 1.0]_trv]
  • interiordiff produced invalid intervals for unbounded operands, fabricated NaN bounds for NaI inputs, and ignored the dec keyword in its disjoint branch.
interiordiff(interval(-Inf, 3), interval(-Inf, 2))
# before: [[-Inf, -Inf]_trv, [2.0, 3.0]_trv] — the first piece is not a valid interval
# after:  [[2.0, 3.0]_trv]

only(interiordiff(nai(), interval(1, 2)))
# before: an interval with NaN bounds
# after:  NaI

interiordiff(interval(1, 2), interval(3, 4); dec = trv)
# before: [[1.0, 2.0]_com] — keyword ignored
# after:  [[1.0, 2.0]_trv]
  • mince produced invalid intervals (inf > sup, decorated com) because LinRange does not interpolate monotonically, and mince! wrote out of bounds under @inbounds when given a short vector.
v = mince(interval(1.0, nextfloat(1.0)), 13)
all(z -> inf(z) ≤ sup(z), v)
# before: false — one piece had inf > sup, decoration com, diam == -2.2e-16
# after:  true

mince!(Vector{BareInterval{Float64}}(undef, 2), bareinterval(0.0, 1.0), 8)
# before: 6 out-of-bounds writes (silent memory corruption; segfault for large n)
# after:  the vector is resized to length 8, like the vector-of-intervals method
  • dist returned NaN (floats) or threw (rationals) for intervals sharing an infinite endpoint. It is now documented and implemented as an upper bound of the Hausdorff distance, with the bound differences rounded upward.
dist(entireinterval(), entireinterval())
# before: NaN
# after:  0.0

dist(bareinterval(Rational{Int}, -1//0, 1//0), bareinterval(Rational{Int}, -1//0, 1//0))
# before: DivideError
# after:  0//1

dist(bareinterval(-1.0, 4.0), bareinterval(-1.0, 3e-16))
# before: 3.9999999999999996 — underestimates the exact distance 4 - 3e-16
# after:  4.0
  • intersect_interval with dec = :auto could attach a decoration the result cannot carry. The :auto path now goes through the clamping setdecoration, and the four redundant dec === :default fast-paths in intersect_interval/hull were removed (provably equivalent to _set_decoration).
intersect_interval(interval(1, 2), interval(3, 4); dec = :auto)
# before: ∅_com — an empty interval can only be trv or ill (Section 11.5.2)
# after:  ∅_trv
  • mid(x, α) ignored α for half-unbounded intervals. α now acts as a threshold: the finite bound is returned when α selects its side, the Section 12.12.8 clamp otherwise; mid(x) and mid(x, 0.5) are unchanged.
mid(interval(-Inf, 0), 1)
# before: -1.7976931348623157e308 — the docstring promises the upper bound
# after:  0.0
  • mid, radius and midradius threw a raw MethodError for unbounded Rational{BigInt} intervals. Rational bounds mirror the float behaviour; where that is impossible an informative error is thrown.
mid(bareinterval(Rational{BigInt}, -1//0, 3//1))
# before: MethodError: no method matching typemin(::Type{BigInt})
# after:  ArgumentError: cannot compute the midpoint of unbounded intervals with
#         `Rational{BigInt}` bounds; there is no smallest finite `Rational{BigInt}`

mid(bareinterval(Rational{BigInt}, -1//0, 3//1), 1//1)
# before: MethodError
# after:  3//1 — no clamp is needed when α selects the finite bound
  • extended_div was the only binary operation without a promoting fallback for mixed bound types.
extended_div(interval(Float32, 1, 2), interval(-4.0, 4.0))
# before: MethodError: no method matching extended_div(::BareInterval{Float32}, ::BareInterval{Float64})
# after:  ((-∞, -0.25]_trv, [0.25, ∞)_trv) — same as the all-Float64 call
  • isnai on a complex interval required both components to be NaI, contradicting isnan. Any NaI component now invalidates the pair.
z = complex(nai(), interval(1))
isnai(z)
# before: false — while isnan(z) == true on the same object
# after:  true
  • isstrictsubset of a real interval in a complex one missed strict growth in the imaginary part. All isstrictsubset methods now use the same definitional idiom issubset_interval(x, y) & !isequal_interval(x, y).
isstrictsubset(interval(0, 1), complex(interval(0, 1), interval(-1, 1)))
# before: false — but [0,1] × {0} is strictly contained in [0,1] × [-1,1]
# after:  true
  • isdisjoint_interval with a mixed Complex/Interval argument asserted disjointness from an NaI component. Any NaI input now yields false (Section 12.12.9), like the Interval/Interval method.
isdisjoint_interval(complex(interval(1, 2), nai()), interval(0, 3))
# before: true
# after:  false
  • mag and mig of a complex interval with an empty component returned -Inf/+Inf. They now follow the real-interval convention for the empty set.
mag(complex(emptyinterval(), emptyinterval())), mig(complex(emptyinterval(), emptyinterval()))
# before: (-Inf, Inf) — a negative magnitude, with mag < mig
# after:  (NaN, NaN)
  • Docstring corrections (no behaviour change): cancelplus stated the wrong defining equation (y - z == x instead of z - y == x); emptyinterval stated the bound-ordering invariant backwards; precedes/strictprecedes said "any element" for a universally quantified test; isweakless carried a copy-pasted clause from isstrictless; the interiordiff! signature omitted the destination vector; extended_div now notes it corresponds to mulRevToPair(y, x) of the standard; plus a few typos.
x, y = interval(5, 7), interval(1, 2)
z = cancelplus(x, y)                      # [7.0, 8.0]
isequal_interval(z - y, x), isequal_interval(y - z, x)
# (true, false) — the docstring claimed the second identity; it now states the first

The full test suite (all interval_tests, all ITF1788 conformance files, Aqua) passes with these changes; the values pinned by ITF1788 (e.g. mid(bareinterval(-Inf, 1.2)), the cancelminus corner cases) are unchanged.

🤖 Generated with Claude Code

OlivierHnt and others added 15 commits August 30, 2026 13:38
Several docstrings in src/intervals/interval_operations/ contained wrong
or leftover text; this commit fixes the text only, with zero behavior
change.

- cancellative.jl (cancelplus): the docstring claimed the result z
  satisfies y - z == x, but the implementation (cancelminus(x, -y),
  i.e. z = [inf(x)+sup(y), sup(x)+inf(y)]) satisfies z - y == x. For
  example, with x = [5, 7] and y = [1, 2], cancelplus(x, y) gives
  z = [7, 8], for which z - y == x holds while y - z == -x. Reworded to
  z - y == x.
- constants.jl (emptyinterval): the docstring stated the interval
  invariant backwards ("lower bound is larger than the upper one"); the
  rule the empty interval is an exception to is lo <= hi.
- boolean.jl (precedes, strictprecedes): the tests are universal over x
  (sup(x) <= inf(y) resp. sup(x) < inf(y)), so "any element of x" is
  replaced by "every element of x"; also fixed "every elements".
- boolean.jl (isweakless): removed the clause "where < is replaced by
  <= for infinite values", a copy-paste remnant from isstrictless; the
  stated definition of isweakless contains no <. isstrictless is left
  unchanged.
- set_operations.jl (interiordiff!): the signature line omitted the
  destination vector; it now reads interiordiff!(v, x, y; dec = :default).
- extended_div.jl (extended_div): noted that extended_div(x, y)
  corresponds to mulRevToPair(y, x) of the standard (the numerator
  comes first).
- Typo sweep in the same docstrings: "keywork" -> "keyword" and
  "ouptut" -> "output" in cancellative.jl and set_operations.jl.

Tested by loading the package (JULIA_LOAD_PATH="@:@stdlib" julia
--project=. -e 'using IntervalArithmetic') and by numerically checking
the cancelplus identity above; no code paths were modified.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
isnai(x::Complex{<:Interval}) was defined with &, so a complex interval
was only reported as NaI when both the real and imaginary parts were
NaI. A half-NaI value such as complex(nai(), interval(1)) therefore
returned isnai == false, while Base.isnan on the same object returned
true (Base's isnan(::Complex) combines the components with |, and this
package defines Base.isnan(x::Interval) = isnai(x)). Any NaI component
invalidates the whole complex interval, so the two predicates were
inconsistent and isnai understated invalidity.

The fix changes the & to |, matching the isnan semantics: a complex
interval is NaI as soon as either component is NaI. A grep over src/
and test/ found no other code relying on the old & behavior.

Tested with a new "isnai" testset in test/interval_tests/complex.jl
covering NaI in the real part, the imaginary part, both, neither, and
the isnan/isnai consistency; test/interval_tests/complex.jl (128 pass)
and test/ITF1788_tests/libieeep1788_bool.jl (392 pass) both succeed.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`extended_div` only had a bare method requiring identical bound types,
and the decorated method forwards to it, so calls with mixed bound
types threw a `MethodError`:

    extended_div(bareinterval(Float32, 1, 2), bareinterval(-4.0, 4.0))
    extended_div(interval(Float32, 1, 2), interval(-4.0, 4.0))

Every sibling binary operation already has a promoting fallback (e.g.
`cancelminus`, `intersect_interval`, `hull`), so this adds the same
one-line pattern to `extended_div`:

    extended_div(x::BareInterval, y::BareInterval) = extended_div(promote(x, y)...)

Tested by adding mixed-type bare and decorated cases to
test/interval_tests/numeric.jl, checking they match the both-Float64
results; the file passes (278/278). No ITF1788 conformance file covers
mulRevToPair.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dist computed max(abs(inf(x) - inf(y)), abs(sup(x) - sup(y))), so any
pair of intervals sharing an infinite endpoint produced Inf - Inf = NaN:
dist(entireinterval(), entireinterval()) returned NaN instead of 0, and
dist(interval(-Inf, 0), interval(-Inf, 1)) returned NaN instead of 1.
For Rational bounds it was worse: (-1//0) - (-1//0) throws DivideError,
so dist on same-side-unbounded rational intervals errored instead of
returning 0//1.

This adds an internal helper `_dist(a, b)` that returns zero when the
two bounds are equal (equal infinite bounds contribute 0, matching the
Hausdorff distance between identical unbounded sets), typemax for
opposite infinities (which also throw DivideError for Rational on older
Julia versions), and the upward-rounded difference otherwise. Rounding
the subtraction upward (with the same `_fround` idiom as `diam`) makes
`dist` a guaranteed upper bound of the Hausdorff distance, which the
docstring now states: round-to-nearest could underestimate the distance
by half an ulp, accepting a stopping criterion `dist(x, y) ≤ ε` slightly
too early. Both bound pairs of the AbstractFloat and Rational `dist`
methods go through the helper; the empty-interval guards are unchanged.

Tested with test/interval_tests/numeric.jl and
test/ITF1788_tests/libieeep1788_num.jl.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scalar mince! methods (bare and decorated) had two bugs:

1. LinRange interpolation is not monotone in floating-point arithmetic,
   so a node could be smaller than its predecessor and produce an
   invalid interval. For example,
   mince(bareinterval(1.0, nextfloat(1.0)), 13) returned a piece with
   lo = 1.0000000000000002 > hi = 1.0, and the decorated version handed
   it out decorated com with diam == -2.2e-16.

2. The loops wrote v[i] for i in 1:n under @inbounds without checking
   length(v), so mince!(Vector{BareInterval{Float64}}(undef, 2),
   bareinterval(0, 1), 8) silently wrote out of bounds (memory
   corruption; large n segfaults).

Both methods now resize!(v, n) upfront, matching the contract of the
vector mince! method, and build each piece from the previous upper
bound with the next node clamped to [lo, sup(x)] (the last piece ends
exactly at sup(x)). This guarantees lo <= hi for every piece, that the
first and last pieces hit the exact bounds of x, and that adjacent
pieces share endpoints, while keeping the exact node values where they
are already fine.

Tested with new tests in test/interval_tests/bisect.jl covering the
non-monotone examples, the resizing behavior (including the empty/NaI
early returns), and the existing quartering behavior; the existing
mince tests in test/interval_tests/numeric.jl and multidim.jl still
pass.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With `dec = :auto`, the set operations returned the minimum of the input
decorations without reconciling it with what the result interval can
carry. In particular,

    intersect_interval(interval(1, 2), interval(3, 4); dec = :auto)

returned `∅_com`, an empty interval decorated `com`, which is invalid
per the IEEE Standard 1788-2015 (an empty interval can only be `trv` or
`ill`). The explicit-decoration path was unaffected since it goes
through `setdecoration`, which clamps such combinations (Section
11.5.2): `dec = com` on the same call already yielded `∅_trv`.

The fix routes the `:auto` branch of `_set_decoration` through the same
clamping setter, `setdecoration(x, decoration(x))`, so the above call
now returns `∅_trv` while valid combinations are unchanged: the hull of
two `com` intervals stays `com`, and an unbounded hull stays `dac`.
This covers all users of `_set_decoration`: `intersect_interval`,
`hull`, `interiordiff` and the cancellative operations.

The four `dec === :default && return _unsafe_interval(r, trv, t)`
fast-paths in `intersect_interval` and `hull` are also removed: they
were provably equivalent to falling through to `_set_decoration` (after
the `ill` early return, `min(trv, d) == trv`, and `setdecoration` with
`trv` hits none of its clamping branches, so both paths produce
`_unsafe_interval(r, trv, t)` — checked exhaustively over empty,
unbounded, thin and common results with both guarantee flags), and the
other callsites (`cancelminus`, `interiordiff!`) already relied on
`_set_decoration` alone. Every callsite now uses the same single idiom.

Tested with test/interval_tests/set_operations.jl and
test/ITF1788_tests/libieeep1788_set.jl and libieeep1788_cancel.jl.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generic `midradius(x::BareInterval)` computed the radius as
`max(m - inf(x), sup(x) - m)` with round-to-nearest subtractions, so the
enclosure promised by the `radius` docstring,
`issubset_interval(x, mid(x) ± radius(x))`, could fail. For instance,
`x = interval(-3.514641998989496e-149, 2.2210542297708545e247)` gave
`mid(x) ± radius(x) == [0.0, 2.2210542297708545e247]`, losing the
negative part of `x`; a random sweep over finite `Float64` bounds showed
the containment failing for roughly half of wide intervals. IEEE
1788-2015 Section 12.12.8 requires the radius to be rounded upward.

The fix computes both differences with `_fround(-, ..., RoundUp)`, the
same helper `diam` already uses. `mid` is unchanged, the `Rational`
method is exact arithmetic and stays as is, and the empty/entire cases
still return `(NaN, NaN)` and `(0.0, Inf)` respectively.

Three midpoint-format display tests pinned the buggy value: for
`interval(-floatmin(Float64), 1.3)` the old radius `0.65` gave
`[0.0, 1.3]`, which does not contain `-floatmin(Float64)`; the correct
upward-rounded radius now displays as `0.650001` (`0.6504` in
`Float16`), matching the documented display guarantee that
`m_d ± r_d` encloses the interval. These tests were updated accordingly.

Tested with test/interval_tests/numeric.jl (including a new seeded
random sweep of 10^4 intervals asserting the containment property), the
full test/ITF1788_tests suite (7654 passing), and the complex,
consistency, display and rounding test files. `Random` was added to
test/Project.toml for the seeded sweep.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
isstrictsubset(x::Interval, y::Complex{<:Interval}) required x to be a
STRICT subset of real(y), missing the case where x == real(y) and
imag(y) strictly contains [0, 0]:
isstrictsubset(interval(0, 1), complex(interval(0, 1), interval(-1, 1)))
returned false, while the mathematically identical
complex(interval(0, 1), interval(0, 0)) as first argument returned true.
By the docstring's own definition ("subset of, but not equal to"), and
consistently with the mixed issubset_interval and isequal_interval
methods, the correct result is true.

All the Interval/Complex methods of isstrictsubset now use the same
definitional idiom as the two real methods:
`issubset_interval(x, y) & !isequal_interval(x, y)`. For the
Complex/Complex and (Complex, Interval) methods this is a pure
refactoring: the old componentwise formulas are Boolean-equivalent
(exhaustively checked over all combinations of regular, thin, empty,
entire and NaI components); only the (Interval, Complex) method changes
behavior, fixing the bug above. Delegating to issubset_interval and
isequal_interval keeps the three predicates mutually consistent by
construction instead of re-deriving edge cases per method.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mixed methods

    isdisjoint_interval(x::Complex{<:Interval}, y::Interval) = isdisjoint_interval(real(x), y) | !in_interval(0, imag(x))

(and its mirror) inverted the NaI-safe `false` of `in_interval` into an
unsafe assertion of disjointness:
isdisjoint_interval(complex(interval(1, 2), nai()), interval(0, 3))
returned true, claiming certain disjointness from invalid data. The
convention for decorated comparisons (Section 12.12.9 of the IEEE
Standard 1788-2015, cf. the Interval/Interval method) is that any NaI
input yields false.

Both methods now start with the same `isnai(x) | isnai(y) && return false`
guard as the Interval/Interval method; for the complex argument this
relies on `isnai(::Complex)` flagging a single NaI component, fixed in
an earlier commit. The Complex/Complex method is already NaI-safe (both
components route through the NaI-guarded Interval method) and is
unchanged. Non-NaI behavior is unchanged.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
For a complex interval with an empty component, `abs(interval(x))` is
the empty interval, so `mag(x::Complex)` returned `sup(∅) == -Inf` and
`mig(x::Complex)` returned `inf(∅) == Inf` — a negative magnitude, with
`mag(x) < mig(x)`. For instance:

    julia> mag(complex(emptyinterval(), interval(1, 2)))
    -Inf

    julia> mig(complex(emptyinterval(), interval(1, 2)))
    Inf

The real-interval methods instead return `NaN` for empty intervals (or
throw the documented `ArgumentError` for `Rational`).

`mag(x::Complex)` and `mig(x::Complex)` now guard for emptiness and
delegate to the real empty-interval methods via
`emptyinterval(real(z))`, preserving the `AbstractFloat` NaN and
`Rational` `ArgumentError` conventions. Non-empty inputs are unchanged.

Tested with new cases in test/interval_tests/complex.jl covering empty
and mixed-empty inputs for `Float64` and `Rational{Int}`, plus unchanged
non-empty behavior; the complex.jl and numeric.jl test files pass.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ate NaI

Two bugs in `cancelminus` (and hence `cancelplus`):

1. The corner case 1 guard in the bare method eagerly evaluated
`prevfloat(inf(x))` and `nextfloat(sup(x))` (non-short-circuit `&` and
`|`), which have no `Rational` methods. As a result,
`cancelminus(interval(1//1, 2//1), interval(1//2, 1//1))` threw a
`MethodError`, making the operation unusable for every non-degenerate
`Rational` input. Since rational interval arithmetic is exact, corner
case 1 (a finite-precision artifact) cannot occur, so the guard now only
runs for `AbstractFloat` bounds. The Rational example above now returns
the exact result `[1//2, 1//1]`.

2. The decorated method had no `ill` check, so an explicit `dec` keyword
overwrote the `ill` decoration via `_set_decoration`:
`cancelminus(nai(Float64), interval(1, 2); dec = com)` returned a
non-NaI interval, and every NaI call also emitted the spurious
"interval part of NaI" warning from `bareinterval`. The method now
follows the `intersect_interval` pattern: it computes the minimal
decoration of the inputs first, early-returns an NaI when it is `ill`,
and uses the internal `_bareinterval` accessor. `cancelplus` flows
through automatically.

Tested with new cases in test/interval_tests/consistency.jl (exact
Rational results for `cancelminus`/`cancelplus`, the Rational vs empty
edge, and NaI propagation for both argument orders and `dec` options,
checking with `@test_logs` that no warning is emitted). The
test/ITF1788_tests/libieeep1788_cancel.jl conformance suite passes
unchanged (242/242), so floating-point behavior is unaffected.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scalar, vector and decorated code paths of `interiordiff` each picked
the surviving pieces of `x \ int(y)` with ad hoc branching, and several of
those branches selected the wrong piece or fabricated invalid intervals:

- `interiordiff(interval(2, 3), interval(1, 3))` returned `{2}`, a point
  inside the interior of `y`, instead of `{3}`: the `isweakless` branch
  picked the left piece whenever the intersection equals `x`.
- `interiordiff([interval(1, 2)], [interval(1, 3)])` returned `{2}` instead
  of `{1}`: in `_interiordiff` the `inf(x) == inf(inter)` branch fired
  before checking the supremum, so the scalar and vector paths disagreed on
  identical inputs.
- `interiordiff([interval(1, 3)], [interval(1, 2)])` lost the boundary
  point `{1}`: the shared-endpoint branches of `_interiordiff` returned
  only one piece.
- `interiordiff(interval(-Inf, 3), interval(-Inf, 2))` returned the invalid
  piece `[-Inf, -Inf]` (likewise `_interiordiff` for unbounded `x == y`):
  `±Inf` is never a member of an interval in the set-based flavor.

All paths now share a single corrected `_interiordiff` core based on the
set identity: for non-disjoint, non-interior inputs, the left piece
`[inf(x), inf(y)]` survives iff `inf(x) <= inf(y)` and `inf(y)` is finite
(an infinite bound of `y` means its interior is unbounded on that side),
and symmetrically for the right piece `[sup(y), sup(x)]`.

Further fixes included:

- The multi-dimensional disjoint early return produced a box containing an
  empty component (e.g. `interiordiff([emptyinterval(Interval{Float64}),
  interval(1, 2)], [interval(1, 2), interval(1, 2)])` returned one box
  containing the empty interval); it now mirrors the filter of the general
  case and produces no boxes when a component of `x` is empty or NaI.
- The decorated scalar disjoint branch stored `x` verbatim, ignoring the
  `dec` keyword and the guarantee flag of `y`. It now decorates the result
  like every other branch, so `dec = :default` yields `trv` (as the
  docstring already stated) and `isguaranteed` is the conjunction of the
  inputs. This deliberately changes the decoration of disjoint results
  from the input decoration to the documented one.
- The decorated scalar path had no NaI guard: `interiordiff(nai(),
  interval(1, 2))` fabricated an interval with NaN bounds through
  `_unsafe_bareinterval`. An NaI input now yields the single element
  `nai(T)` (NaI in, NaI out), matching the convention of
  `intersect_interval` and `hull`.

No existing test pinned the buggy behavior; all previous assertions in
test/interval_tests/set_operations.jl and test/interval_tests/multidim.jl
pass unchanged. New tests cover the regression cases above in scalar and
1-element vector forms, a scalar/vector agreement sweep over all interval
pairs with endpoints in -1:0.5:3, a pointwise membership oracle for
`x \ int(y)`, and the decorated `dec`/`isguaranteed`/NaI behavior.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The internal interval_diff rebuilt its pieces with the checked
interval(a, b) constructor, which raises the decoration and always marks
the result as guaranteed. Consequently,
interval_diff(interval(1, 10, def), interval(2, 5)) returned pieces
decorated com (raised from def) with isguaranteed == true even when an
input was not guaranteed, silently upgrading an unguaranteed enclosure
to a guaranteed one. There was also no NaI guard: an NaI operand fell
through to bound arithmetic on NaN, triggering the checked
constructor's ill-formed interval warning.

The fix mirrors the conventions already used in this file (e.g.
intersect_interval, interiordiff!): compute the minimal decoration of
the inputs and return [nai(T)] if it is ill, propagate
isguaranteed(x) & isguaranteed(y), and build the pieces with
_unsafe_interval(_unsafe_bareinterval(T, lo, hi), trv, t), so the
result decoration is trv at best, matching the Section 11.7.1 default
used across this file. The disjoint branch, which previously returned x
verbatim, now follows the same construction convention: it returns x's
bare interval decorated trv with the propagated guarantee, for
consistency with the other branches. The branch structure and the
computed bounds are unchanged.

Tested by extending the interval_diff testset in
test/interval_tests/set_operations.jl with decoration, guarantee, and
NaI assertions; all 243 tests in the file pass, and the pre-existing
value assertions are untouched.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
For a half-unbounded interval, `mid(x, α)` returned the Section 12.12.8
clamp regardless of `α`: `mid(interval(-Inf, 0), 1)` gave `-floatmax`
instead of the upper bound 0 promised by the docstring, and the finite
bound was unreachable for every `α`.

`α` now acts as a threshold, mirroring the existing entire-interval
branch: the finite bound is returned when `α` selects its side
(`α > 0.5` for a finite upper bound, `α < 0.5` for a finite lower
bound), and the infinite bound is replaced by the largest finite value
of the bound type otherwise, so `mid(x)` and `mid(x, 0.5)` keep the
values required by Section 12.12.8 of the IEEE Standard 1788-2015 (as
pinned by the ITF1788 test suite). The entire-interval branch and
bounded intervals are unchanged, and the docstring states the new
behavior.

As a side effect, `mid` of an unbounded `BareInterval{Rational{BigInt}}`
no longer throws a `MethodError` when `α` selects the finite bound (the
other paths still hit the undefined `typemin(BigInt)`; that remains a
known issue).

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt}` intervals

The `Rational` method of `mid` clamps infinite bounds with
`convert(Rational{T}, typemin(T))` / `typemax(T)`, mirroring the float
clamps `nextfloat(typemin(T))` / `prevfloat(typemax(T))` of Section
12.12.8. For `T = BigInt` these do not exist, so `mid` (and `radius` and
`midradius`, which call it) threw a raw
`MethodError: no method matching typemin(::Type{BigInt})` for unbounded
`Rational{BigInt}` intervals — a supported bound type, with such
intervals reachable from ordinary arithmetic.

The principle is that `Rational` bounds mirror the behavior of float
bounds, and whenever that is impossible an informative error is thrown
(as the existing "cannot return a `Rational` NaN" refusals do). The
clamps now go through `_finitemin` / `_finitemax` helpers: fixed-width
integer types keep the current `±typemax(T)//1` values, while
`Rational{BigInt}` throws a descriptive `ArgumentError` explaining that
`BigInt` has no smallest or largest finite value. Paths that never need
the clamp are unaffected: `α` selecting the finite bound, `mid` of the
entire interval at `α = 1/2`, and `midradius` of the entire interval
all keep working for `Rational{BigInt}`.

[The regression tests written for this fix are contributed separately,
as part of the test-suite rework.]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master 29b77b9... master / 29b77b9...
BigFloat MPFI/basics/* 25.4 ± 3.5 μs 25.7 ± 3.5 μs 0.99 ± 0.19
BigFloat MPFI/basics/+ 12.4 ± 3.7 μs 12.5 ± 3.6 μs 0.991 ± 0.41
BigFloat MPFI/basics/- 18 ± 3.5 μs 17.5 ± 3.4 μs 1.03 ± 0.28
BigFloat MPFI/basics// 19.7 ± 3.6 μs 19.2 ± 3.6 μs 1.03 ± 0.27
BigFloat MPFI/basics/abs 10.2 ± 3.5 μs 10.3 ± 3.5 μs 0.989 ± 0.48
BigFloat MPFI/basics/acos 0.678 ± 0.0088 ms 0.685 ± 0.0091 ms 0.99 ± 0.018
BigFloat MPFI/basics/asin 0.661 ± 0.0093 ms 0.661 ± 0.011 ms 1 ± 0.022
BigFloat MPFI/basics/atan 2.1 ± 0.0072 ms 2.11 ± 0.011 ms 0.996 ± 0.0061
BigFloat MPFI/basics/cos 0.357 ± 0.0096 ms 0.359 ± 0.01 ms 0.993 ± 0.039
BigFloat MPFI/basics/cosh 0.43 ± 0.012 ms 0.431 ± 0.012 ms 0.998 ± 0.04
BigFloat MPFI/basics/exp 0.502 ± 0.01 ms 0.505 ± 0.01 ms 0.994 ± 0.029
BigFloat MPFI/basics/inv 19 ± 3.5 μs 18.7 ± 3.5 μs 1.01 ± 0.27
BigFloat MPFI/basics/log 0.467 ± 0.01 ms 0.471 ± 0.011 ms 0.991 ± 0.031
BigFloat MPFI/basics/sin 0.459 ± 0.011 ms 0.462 ± 0.011 ms 0.995 ± 0.034
BigFloat MPFI/basics/sinh 0.574 ± 0.0099 ms 0.573 ± 0.01 ms 1 ± 0.025
BigFloat MPFI/basics/sqrt 27.4 ± 7 μs 27.5 ± 6.8 μs 0.996 ± 0.35
BigFloat MPFI/basics/tan 0.382 ± 0.01 ms 0.383 ± 0.01 ms 0.998 ± 0.037
BigFloat MPFI/basics/tanh 0.585 ± 0.01 ms 0.585 ± 0.01 ms 0.999 ± 0.025
BigFloat bareinterval/basics/* 0.0423 ± 0.0027 ms 0.0409 ± 0.0027 ms 1.03 ± 0.095
BigFloat bareinterval/basics/+ 21.7 ± 3.4 μs 22.5 ± 3.3 μs 0.965 ± 0.21
BigFloat bareinterval/basics/- 22.2 ± 3.4 μs 21.9 ± 3.2 μs 1.01 ± 0.21
BigFloat bareinterval/basics// 19.6 ± 5.1 μs 19.4 ± 5 μs 1.01 ± 0.37
BigFloat bareinterval/basics/abs 15.7 ± 3.2 μs 15.2 ± 3.2 μs 1.03 ± 0.3
BigFloat bareinterval/basics/acos 0.708 ± 0.025 ms 0.711 ± 0.024 ms 0.995 ± 0.048
BigFloat bareinterval/basics/asin 0.692 ± 0.021 ms 0.692 ± 0.021 ms 1 ± 0.043
BigFloat bareinterval/basics/atan 2.11 ± 0.0073 ms 2.12 ± 0.0079 ms 0.998 ± 0.0051
BigFloat bareinterval/basics/cos 0.343 ± 0.088 ms 0.344 ± 0.086 ms 0.998 ± 0.36
BigFloat bareinterval/basics/cosh 0.474 ± 0.023 ms 0.474 ± 0.023 ms 1 ± 0.069
BigFloat bareinterval/basics/exp 0.514 ± 0.011 ms 0.513 ± 0.01 ms 1 ± 0.029
BigFloat bareinterval/basics/inv 26.4 ± 3.5 μs 26 ± 3.4 μs 1.02 ± 0.19
BigFloat bareinterval/basics/log 0.501 ± 0.012 ms 0.505 ± 0.012 ms 0.992 ± 0.034
BigFloat bareinterval/basics/sin 0.521 ± 0.097 ms 0.481 ± 0.095 ms 1.08 ± 0.29
BigFloat bareinterval/basics/sinh 0.582 ± 0.011 ms 0.581 ± 0.011 ms 1 ± 0.027
BigFloat bareinterval/basics/sqrt 0.0324 ± 0.0025 ms 0.032 ± 0.0025 ms 1.01 ± 0.11
BigFloat bareinterval/basics/tan 0.326 ± 0.057 ms 0.324 ± 0.056 ms 1.01 ± 0.25
BigFloat bareinterval/basics/tanh 0.595 ± 0.011 ms 0.595 ± 0.011 ms 1 ± 0.026
BigFloat interval/basics/* 0.0435 ± 0.0018 ms 0.0441 ± 0.0018 ms 0.987 ± 0.059
BigFloat interval/basics/+ 24.7 ± 8.3 μs 24.2 ± 8.4 μs 1.02 ± 0.49
BigFloat interval/basics/- 24.6 ± 8.2 μs 24.6 ± 8.2 μs 0.999 ± 0.47
BigFloat interval/basics// 24.3 ± 5 μs 24.4 ± 4.9 μs 0.999 ± 0.29
BigFloat interval/basics/abs 14.7 ± 7.7 μs 14.3 ± 7.7 μs 1.03 ± 0.77
BigFloat interval/basics/acos 0.704 ± 0.041 ms 0.706 ± 0.042 ms 0.997 ± 0.083
BigFloat interval/basics/asin 0.689 ± 0.035 ms 0.697 ± 0.034 ms 0.988 ± 0.07
BigFloat interval/basics/atan 2.12 ± 0.0079 ms 2.12 ± 0.0086 ms 0.999 ± 0.0055
BigFloat interval/basics/cos 0.336 ± 0.015 ms 0.337 ± 0.015 ms 0.996 ± 0.064
BigFloat interval/basics/cosh 0.47 ± 0.023 ms 0.47 ± 0.023 ms 1 ± 0.07
BigFloat interval/basics/exp 0.522 ± 0.01 ms 0.522 ± 0.011 ms 0.998 ± 0.029
BigFloat interval/basics/inv 30.5 ± 5.4 μs 30.4 ± 3 μs 1 ± 0.2
BigFloat interval/basics/log 0.5 ± 0.037 ms 0.507 ± 0.037 ms 0.986 ± 0.1
BigFloat interval/basics/sin 0.485 ± 0.03 ms 0.485 ± 0.024 ms 0.998 ± 0.078
BigFloat interval/basics/sinh 0.591 ± 0.01 ms 0.59 ± 0.011 ms 1 ± 0.026
BigFloat interval/basics/sqrt 0.0408 ± 0.029 ms 0.04 ± 0.029 ms 1.02 ± 1
BigFloat interval/basics/tan 0.282 ± 0.014 ms 0.283 ± 0.016 ms 0.996 ± 0.075
BigFloat interval/basics/tanh 0.605 ± 0.012 ms 0.604 ± 0.01 ms 1 ± 0.026
bareinterval/basics/* 1.74 ± 0.05 μs 1.73 ± 0.04 μs 1.01 ± 0.037
bareinterval/basics/+ 0.501 ± 0.04 μs 0.541 ± 0.02 μs 0.926 ± 0.081
bareinterval/basics/- 0.561 ± 0.02 μs 0.501 ± 0.01 μs 1.12 ± 0.046
bareinterval/basics// 1.22 ± 0.031 μs 1.21 ± 0.031 μs 1.01 ± 0.036
bareinterval/basics/abs 0.191 ± 0.03 μs 0.201 ± 0.031 μs 0.95 ± 0.21
bareinterval/basics/acos 2.71 ± 0.021 μs 2.71 ± 0.021 μs 1 ± 0.011
bareinterval/basics/asin 3 ± 0.021 μs 3.05 ± 0.04 μs 0.987 ± 0.015
bareinterval/basics/atan 13.1 ± 0.18 μs 13.1 ± 0.18 μs 1 ± 0.02
bareinterval/basics/cos 0.0506 ± 0.00098 ms 0.0506 ± 0.011 ms 1 ± 0.22
bareinterval/basics/cosh 7.24 ± 0.04 μs 7.18 ± 0.031 μs 1.01 ± 0.0071
bareinterval/basics/exp 4.26 ± 0.041 μs 4.29 ± 0.04 μs 0.993 ± 0.013
bareinterval/basics/inv 1 ± 0.021 μs 1 ± 0.02 μs 1 ± 0.029
bareinterval/basics/log 1.93 ± 0.049 μs 1.93 ± 0.059 μs 1 ± 0.04
bareinterval/basics/sin 0.0496 ± 0.018 ms 0.0496 ± 0.018 ms 1 ± 0.51
bareinterval/basics/sinh 10 ± 0.059 μs 10 ± 0.069 μs 1 ± 0.0091
bareinterval/basics/sqrt 1.12 ± 0.01 μs 1.12 ± 0.02 μs 1 ± 0.02
bareinterval/basics/tan 29 ± 0.33 μs 28.8 ± 0.34 μs 1.01 ± 0.017
bareinterval/basics/tanh 0.297 ± 0.0093 ms 0.295 ± 0.0094 ms 1.01 ± 0.045
interval/basics/* 3.1 ± 0.061 μs 2.85 ± 0.07 μs 1.08 ± 0.034
interval/basics/+ 1.53 ± 0.05 μs 1.33 ± 0.04 μs 1.15 ± 0.051
interval/basics/- 1.55 ± 0.031 μs 1.37 ± 0.039 μs 1.13 ± 0.039
interval/basics// 2.02 ± 0.05 μs 1.91 ± 0.061 μs 1.06 ± 0.043
interval/basics/abs 0.361 ± 0.061 μs 0.371 ± 0.059 μs 0.973 ± 0.23
interval/basics/acos 3.3 ± 0.06 μs 3.3 ± 0.05 μs 1 ± 0.024
interval/basics/asin 3.51 ± 0.041 μs 3.51 ± 0.041 μs 1 ± 0.017
interval/basics/atan 12.1 ± 0.16 μs 13.1 ± 0.24 μs 0.92 ± 0.021
interval/basics/cos 0.0684 ± 0.0033 ms 0.0684 ± 0.0034 ms 1 ± 0.069
interval/basics/cosh 9.02 ± 0.061 μs 8.98 ± 0.06 μs 1 ± 0.0096
interval/basics/exp 4.88 ± 0.06 μs 4.89 ± 0.08 μs 0.998 ± 0.02
interval/basics/inv 1.48 ± 0.051 μs 1.52 ± 0.06 μs 0.974 ± 0.051
interval/basics/log 6.93 ± 0.06 μs 2.65 ± 0.07 μs 2.62 ± 0.073
interval/basics/sin 0.0682 ± 0.0035 ms 0.0676 ± 0.0035 ms 1.01 ± 0.074
interval/basics/sinh 10.6 ± 0.072 μs 10.6 ± 0.1 μs 0.998 ± 0.012
interval/basics/sqrt 1.66 ± 0.1 μs 1.58 ± 0.031 μs 1.05 ± 0.066
interval/basics/tan 0.039 ± 0.0018 ms 0.0387 ± 0.0018 ms 1.01 ± 0.067
interval/basics/tanh 0.312 ± 0.0079 ms 0.307 ± 0.0059 ms 1.02 ± 0.032
time_to_load 0.116 ± 0.0012 s 0.117 ± 0.00087 s 0.988 ± 0.013
Memory benchmarks
master 29b77b9... master / 29b77b9...
BigFloat MPFI/basics/* 0.424 k allocs: 21.3 kB 0.424 k allocs: 21.3 kB 1
BigFloat MPFI/basics/+ 0.302 k allocs: 16.5 kB 0.302 k allocs: 16.5 kB 1
BigFloat MPFI/basics/- 0.402 k allocs: 20.4 kB 0.402 k allocs: 20.4 kB 1
BigFloat MPFI/basics// 0.345 k allocs: 18.2 kB 0.345 k allocs: 18.2 kB 1
BigFloat MPFI/basics/abs 0.324 k allocs: 17.4 kB 0.324 k allocs: 17.4 kB 1
BigFloat MPFI/basics/acos 1.24 k allocs: 0.0445 MB 1.24 k allocs: 0.0445 MB 1
BigFloat MPFI/basics/asin 1.08 k allocs: 0.0355 MB 1.08 k allocs: 0.0355 MB 1
BigFloat MPFI/basics/atan 2.14 k allocs: 0.0543 MB 2.14 k allocs: 0.0543 MB 1
BigFloat MPFI/basics/cos 2.9 k allocs: 0.0975 MB 2.9 k allocs: 0.0975 MB 1
BigFloat MPFI/basics/cosh 0.781 k allocs: 0.0322 MB 0.781 k allocs: 0.0322 MB 1
BigFloat MPFI/basics/exp 0.902 k allocs: 0.036 MB 0.902 k allocs: 0.036 MB 1
BigFloat MPFI/basics/inv 0.351 k allocs: 18.4 kB 0.351 k allocs: 18.4 kB 1
BigFloat MPFI/basics/log 0.407 k allocs: 20.6 kB 0.407 k allocs: 20.6 kB 1
BigFloat MPFI/basics/sin 3.18 k allocs: 0.106 MB 3.18 k allocs: 0.106 MB 1
BigFloat MPFI/basics/sinh 0.915 k allocs: 0.0369 MB 0.915 k allocs: 0.0369 MB 1
BigFloat MPFI/basics/sqrt 0.302 k allocs: 16.5 kB 0.302 k allocs: 16.5 kB 1
BigFloat MPFI/basics/tan 2.8 k allocs: 0.098 MB 2.8 k allocs: 0.098 MB 1
BigFloat MPFI/basics/tanh 0.908 k allocs: 0.0362 MB 0.908 k allocs: 0.0362 MB 1
BigFloat bareinterval/basics/* 0.262 k allocs: 26 kB 0.262 k allocs: 26 kB 1
BigFloat bareinterval/basics/+ 0.202 k allocs: 20.4 kB 0.202 k allocs: 20.4 kB 1
BigFloat bareinterval/basics/- 0.202 k allocs: 20.4 kB 0.202 k allocs: 20.4 kB 1
BigFloat bareinterval/basics// 0.1 k allocs: 10.8 kB 0.1 k allocs: 10.8 kB 1
BigFloat bareinterval/basics/abs 0.199 k allocs: 20.1 kB 0.199 k allocs: 20.1 kB 1
BigFloat bareinterval/basics/acos 1.27 k allocs: 0.0692 MB 1.27 k allocs: 0.0692 MB 1
BigFloat bareinterval/basics/asin 1.17 k allocs: 0.0597 MB 1.17 k allocs: 0.0597 MB 1
BigFloat bareinterval/basics/atan 2.04 k allocs: 0.08 MB 2.04 k allocs: 0.08 MB 1
BigFloat bareinterval/basics/cos 2.45 k allocs: 0.185 MB 2.45 k allocs: 0.185 MB 1
BigFloat bareinterval/basics/cosh 1.01 k allocs: 0.0671 MB 1.01 k allocs: 0.0671 MB 1
BigFloat bareinterval/basics/exp 0.802 k allocs: 0.0397 MB 0.802 k allocs: 0.0397 MB 1
BigFloat bareinterval/basics/inv 0.198 k allocs: 20 kB 0.198 k allocs: 20 kB 1
BigFloat bareinterval/basics/log 0.616 k allocs: 0.0522 MB 0.616 k allocs: 0.0522 MB 1
BigFloat bareinterval/basics/sin 2.9 k allocs: 0.197 MB 2.9 k allocs: 0.197 MB 1
BigFloat bareinterval/basics/sinh 0.815 k allocs: 0.0407 MB 0.815 k allocs: 0.0407 MB 1
BigFloat bareinterval/basics/sqrt 0.26 k allocs: 25.8 kB 0.26 k allocs: 25.8 kB 1
BigFloat bareinterval/basics/tan 1.67 k allocs: 0.124 MB 1.67 k allocs: 0.124 MB 1
BigFloat bareinterval/basics/tanh 0.808 k allocs: 0.0399 MB 0.808 k allocs: 0.0399 MB 1
BigFloat interval/basics/* 0.263 k allocs: 26.8 kB 0.263 k allocs: 26.8 kB 1
BigFloat interval/basics/+ 0.203 k allocs: 21.2 kB 0.203 k allocs: 21.2 kB 1
BigFloat interval/basics/- 0.203 k allocs: 21.2 kB 0.203 k allocs: 21.2 kB 1
BigFloat interval/basics// 0.101 k allocs: 11.6 kB 0.101 k allocs: 11.6 kB 1
BigFloat interval/basics/abs 0.2 k allocs: 20.9 kB 0.2 k allocs: 20.9 kB 1
BigFloat interval/basics/acos 1.71 k allocs: 0.11 MB 1.71 k allocs: 0.11 MB 1
BigFloat interval/basics/asin 1.47 k allocs: 0.088 MB 1.47 k allocs: 0.0882 MB 0.998
BigFloat interval/basics/atan 2.04 k allocs: 0.0813 MB 2.04 k allocs: 0.0814 MB 0.999
BigFloat interval/basics/cos 2.45 k allocs: 0.186 MB 2.45 k allocs: 0.186 MB 1
BigFloat interval/basics/cosh 1.01 k allocs: 0.068 MB 1.01 k allocs: 0.0683 MB 0.996
BigFloat interval/basics/exp 0.803 k allocs: 0.0409 MB 0.803 k allocs: 0.0409 MB 0.999
BigFloat interval/basics/inv 0.199 k allocs: 20.8 kB 0.199 k allocs: 20.8 kB 1
BigFloat interval/basics/log 0.917 k allocs: 0.0805 MB 0.917 k allocs: 0.0805 MB 1
BigFloat interval/basics/sin 2.9 k allocs: 0.198 MB 2.9 k allocs: 0.198 MB 1
BigFloat interval/basics/sinh 0.816 k allocs: 0.0418 MB 0.816 k allocs: 0.0417 MB 1
BigFloat interval/basics/sqrt 0.714 k allocs: 0.0675 MB 0.714 k allocs: 0.0675 MB 1
BigFloat interval/basics/tan 1.67 k allocs: 0.125 MB 1.67 k allocs: 0.125 MB 1
BigFloat interval/basics/tanh 0.809 k allocs: 0.0412 MB 0.809 k allocs: 0.0412 MB 1
bareinterval/basics/* 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/+ 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/- 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics// 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/abs 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/acos 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/asin 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/atan 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/cos 0.56 k allocs: 0.0423 MB 0.56 k allocs: 0.0423 MB 1
bareinterval/basics/cosh 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/exp 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/inv 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/log 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/sin 0.56 k allocs: 0.0423 MB 0.56 k allocs: 0.0423 MB 1
bareinterval/basics/sinh 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/sqrt 2 allocs: 1.62 kB 2 allocs: 1.62 kB 1
bareinterval/basics/tan 0.29 k allocs: 23 kB 0.29 k allocs: 23 kB 1
bareinterval/basics/tanh 1.03 k allocs: 0.0359 MB 1.03 k allocs: 0.0359 MB 1
interval/basics/* 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/+ 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/- 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics// 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/abs 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/acos 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/asin 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/atan 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/cos 0.561 k allocs: 0.0431 MB 0.561 k allocs: 0.0431 MB 1
interval/basics/cosh 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/exp 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/inv 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/log 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/sin 0.561 k allocs: 0.0431 MB 0.561 k allocs: 0.0431 MB 1
interval/basics/sinh 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/sqrt 3 allocs: 2.45 kB 3 allocs: 2.45 kB 1
interval/basics/tan 0.291 k allocs: 24.4 kB 0.291 k allocs: 24.4 kB 1
interval/basics/tanh 1.03 k allocs: 0.0367 MB 1.03 k allocs: 0.0367 MB 1
time_to_load 0.149 k allocs: 11.2 kB 0.149 k allocs: 11.2 kB 1

@OlivierHnt
OlivierHnt merged commit dca22f8 into JuliaIntervals:master Sep 5, 2026
17 of 22 checks passed
OlivierHnt added a commit to OlivierHnt/IntervalArithmetic.jl that referenced this pull request Sep 10, 2026
Resolves the two modify/delete conflicts: `test/interval_tests/display.jl`
and `test/interval_tests/forwarddiff.jl` are deleted by this branch, and the
assertions they gained in JuliaIntervals#777 and JuliaIntervals#769 are carried over into the new test
layout by the following commits.
OlivierHnt added a commit to OlivierHnt/IntervalArithmetic.jl that referenced this pull request Sep 10, 2026
…s#777 and JuliaIntervals#769

This branch was written against the tree before JuliaIntervals#777, so several assertions
pinned the behavior that JuliaIntervals#777 fixed, and the regression tests its commit
messages defer to "the test-suite rework" were partly missing. The ones that
pinned the old behavior are updated:

- `display.jl`: the midpoint format of `interval(-floatmin(Float64), 1.3)`
  and its `Float32`/`Float16` counterparts, now that the radius is rounded
  upward (`0.65 ± 0.650001`, `Float16(0.6504)`);
- `boolean.jl`: `isnai` on a complex interval with a single NaI component is
  now `true`, consistent with `isnan`;
- `cancellative.jl`: `cancelminus` works for `Rational` bounds (corner case 1
  is a finite-precision artifact and is skipped there) and no longer emits the
  spurious "interval part of NaI" warning;
- `extended_div.jl`: mixed bound types promote instead of throwing;
- `numeric.jl`: `mid(x, α)` of a half-unbounded interval takes `α` into
  account, so the finite bound is reachable;
- `set_operations.jl`: `_interiordiff` keeps the boundary point of a shared
  endpoint, and `interval_diff` builds its pieces with the internal
  constructor (`trv` at best, guarantee propagated rather than upgraded).

The regression tests that were missing are added:

- `radius`/`midradius` enclosure: the previous check used three intervals that
  all pass with the round-to-nearest radius, so it could not catch the bug; it
  is replaced by the documented `mid(x) ± radius(x)` containment on the
  counterexample of JuliaIntervals#777 plus a seeded sweep of 10^4 intervals (which fails on
  roughly half of them without the fix);
- `dist` for intervals sharing an infinite endpoint, including the `Rational`
  case that used to throw a `DivideError`;
- `mince`/`mince!` monotonicity, shared endpoints and output resizing;
- `intersect_interval` with `dec = :auto` on an empty result, which must clamp
  to `trv` rather than return an invalid `∅_com`;
- `mag`/`mig` of complex intervals with an empty component;
- `isstrictsubset` of a real interval in a complex one with equal real parts;
- `isdisjoint_interval` NaI guards on the mixed Complex/Interval methods;
- `mid` of unbounded `Rational{BigInt}` intervals, which throws an informative
  `ArgumentError` where the float clamps have no counterpart.

The interval partials tests of JuliaIntervals#769 are ported from the deleted
`test/interval_tests/forwarddiff.jl` into the extension test file, and the
tuple-vs-tuple `rightof`/`leftof` of `Piecewise` gain direct tests, since only
their use through `intersect_domain` was covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@OlivierHnt
OlivierHnt deleted the fix/interval-operations branch November 1, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

radius is not rounded outward, and can be too small to enclose the interval

1 participant