Repository navigation
fix: Correctness fixes in interval_operations - #777
Merged
OlivierHnt merged 15 commits intoSep 5, 2026
Merged
Conversation
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>
Contributor
Benchmark Results (Julia v1)Time benchmarks
Memory benchmarks
|
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.midradiuscomputed the radius with round-to-nearest subtractions, so the enclosure promised by theradiusdocstring could fail. The radius is now rounded upward (Section 12.12.8). Closesradiusis not rounded outward, and can be too small to enclose the interval #776.(As a consequence, the midpoint display of e.g.
interval(0, 1.3)is now the honest0.65 ± 0.650001instead of0.65 ± 0.65.)cancelminusandcancelplusthrew aMethodErrorfor every non-degenerateRationalinterval, and did not propagate NaI whendecwas given explicitly.interiordiffreturned the wrong piece whenx ⊆ yand they share one endpoint, and the scalar and vector paths disagreed on identical inputs. Both now return exactly the pieces ofx \ interior(y).interiordiffproduced invalid intervals for unbounded operands, fabricated NaN bounds for NaI inputs, and ignored thedeckeyword in its disjoint branch.minceproduced invalid intervals (inf > sup, decoratedcom) becauseLinRangedoes not interpolate monotonically, andmince!wrote out of bounds under@inboundswhen given a short vector.distreturned 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.intersect_intervalwithdec = :autocould attach a decoration the result cannot carry. The:autopath now goes through the clampingsetdecoration, and the four redundantdec === :defaultfast-paths inintersect_interval/hullwere removed (provably equivalent to_set_decoration).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)andmid(x, 0.5)are unchanged.mid,radiusandmidradiusthrew a rawMethodErrorfor unboundedRational{BigInt}intervals.Rationalbounds mirror the float behaviour; where that is impossible an informative error is thrown.extended_divwas the only binary operation without a promoting fallback for mixed bound types.isnaion a complex interval required both components to be NaI, contradictingisnan. Any NaI component now invalidates the pair.isstrictsubsetof a real interval in a complex one missed strict growth in the imaginary part. Allisstrictsubsetmethods now use the same definitional idiomissubset_interval(x, y) & !isequal_interval(x, y).isdisjoint_intervalwith a mixed Complex/Interval argument asserted disjointness from an NaI component. Any NaI input now yieldsfalse(Section 12.12.9), like the Interval/Interval method.magandmigof a complex interval with an empty component returned-Inf/+Inf. They now follow the real-interval convention for the empty set.cancelplusstated the wrong defining equation (y - z == xinstead ofz - y == x);emptyintervalstated the bound-ordering invariant backwards;precedes/strictprecedessaid "any element" for a universally quantified test;isweaklesscarried a copy-pasted clause fromisstrictless; theinteriordiff!signature omitted the destination vector;extended_divnow notes it corresponds tomulRevToPair(y, x)of the standard; plus a few typos.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)), thecancelminuscorner cases) are unchanged.🤖 Generated with Claude Code