From 8fc20dfacc62f83d6088555a0cb43e9edc7aa24f Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Fri, 10 Jul 2026 16:19:25 -0500 Subject: [PATCH 1/2] Expand function support - `mag`/`mig` for `Real` - `iszero` for `ThickNumber` - loosen types for ForwardDiff extension --- Project.toml | 2 +- ext/ThickNumbersForwardDiffExt.jl | 13 +++++++++++-- src/ThickNumbers.jl | 11 +++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index c6ce9b7..d7e0276 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ThickNumbers" uuid = "b57aa878-5b76-4266-befc-f8e007760995" authors = ["Tim Holy and contributors"] -version = "1.1.0" +version = "1.1.1" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/ext/ThickNumbersForwardDiffExt.jl b/ext/ThickNumbersForwardDiffExt.jl index e92902f..aa713a4 100644 --- a/ext/ThickNumbersForwardDiffExt.jl +++ b/ext/ThickNumbersForwardDiffExt.jl @@ -17,10 +17,14 @@ end ForwardDiff.can_dual(::Type{<:ThickNumber}) = true -function ForwardDiff.dual_definition_retval(::Val{T}, val::ThickNumber, deriv::ThickNumber, partial::Partials) where {T} +# The value is a ThickNumber (the function evaluated over a set), but a diffrule +# derivative may be an ordinary `Real` (e.g. the integer coefficient from a power +# rule), so `deriv` is left as `Number`. `val::ThickNumber` keeps these from +# overlapping ForwardDiff's all-`Real` methods. +function ForwardDiff.dual_definition_retval(::Val{T}, val::ThickNumber, deriv::Number, partial::Partials) where {T} return Dual{T}(val, deriv * partial) end -function ForwardDiff.dual_definition_retval(::Val{T}, val::ThickNumber, deriv1::ThickNumber, partial1::Partials, deriv2::ThickNumber, partial2::Partials) where {T} +function ForwardDiff.dual_definition_retval(::Val{T}, val::ThickNumber, deriv1::Number, partial1::Partials, deriv2::Number, partial2::Partials) where {T} return Dual{T}(val, ForwardDiff._mul_partials(partial1, partial2, deriv1, deriv2)) end @@ -41,6 +45,11 @@ ForwardDiff._div_partial(partial::Real, x::ThickNumber) = partial / x ForwardDiff._div_partial(partial::ThickNumber, x::Real) = partial / x ForwardDiff._div_partial(partial::ThickNumber, x::ThickNumber) = partial / x +# ForwardDiff's `iszero_tuple` tests each partial with `==`, which ThickNumber +# disables. Test exact-zeroness with `iszero` instead (defined for ThickNumber and, +# recursively, for nested Duals over ThickNumbers). +ForwardDiff.iszero_tuple(tup::NTuple{N,V}) where {N,V<:ThickLike} = all(iszero, tup) + Base.promote_rule(::Type{TN}, ::Type{Dual{T,V,N}}) where {TN<:ThickNumber,T,V<:Number,N} = Dual{T, promote_dual(TN, V),N} promote_dual(::Type{TN}, ::Type{V}) where {TN<:ThickNumber,V} = promote_type(TN, V) diff --git a/src/ThickNumbers.jl b/src/ThickNumbers.jl index 24e7e2b..025761d 100644 --- a/src/ThickNumbers.jl +++ b/src/ThickNumbers.jl @@ -294,6 +294,17 @@ so returns zero. Otherwise, it returns the minimum absolute value of the endpoin """ mig(x::ThickNumber{T}) where T = zero(T) ∈ x ? zero(T) : min(abs(loval(x)), abs(hival(x))) +# A `Real` is a degenerate thick number `[x, x]`, so both its maximum and minimum +# absolute value are `abs(x)`. Provides `mag`/`mig` for code that mixes thick and +# point values. +mag(x::Real) = abs(x) +mig(x::Real) = abs(x) + +# A thick number is zero exactly when it is the degenerate set `[0, 0]`. Unlike +# `==`, this is unambiguous, so it is defined (and needed by ForwardDiff, which +# tests partials for exact zero). +Base.iszero(x::ThickNumber) = iszero(loval(x)) & iszero(hival(x)) + Base.promote_rule(::Type{ThickNumber{T}}, ::Type{ThickNumber{S}}) where {T<:Number,S<:Number} = ThickNumber{promote_type(T,S)} ## Trait functions and constants From 39b6e3cc49c4e431527fc8cb0e45c87072c4e842 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Fri, 10 Jul 2026 16:40:37 -0500 Subject: [PATCH 2/2] Test mag/mig on Reals, iszero, and integer-power AD paths Covers the newly added mag(::Real)/mig(::Real) and iszero(::ThickNumber) methods, and exercises the ForwardDiff extension's iszero_tuple and real-valued diffrule coefficients via derivatives of an integer power. Co-Authored-By: Claude Opus 4.8 --- test/extensions/forwarddiff.jl | 10 ++++++++++ test/runtests.jl | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/test/extensions/forwarddiff.jl b/test/extensions/forwarddiff.jl index 6419109..b669d8c 100644 --- a/test/extensions/forwarddiff.jl +++ b/test/extensions/forwarddiff.jl @@ -21,6 +21,16 @@ using IntervalArith ddf2(x) = ForwardDiff.derivative(df2, x) @test ddf2(b) ≐ 1 + # Integer powers: `^(::Dual, ::Integer)` tests its partials for zero via + # `iszero_tuple`, and the power-rule coefficients reach `dual_definition_retval` + # as ordinary `Real`s rather than ThickNumbers. + g(x) = x^4 + dg(x) = ForwardDiff.derivative(g, x) + ddg(x) = ForwardDiff.derivative(dg, x) + xi = Interval(1.0, 2.0) + @test dg(xi) ≐ 4*xi^3 + @test ddg(xi) ≐ 12*xi^2 + # abs dabs(x) = ForwardDiff.derivative(abs, x) ddabs(x) = ForwardDiff.derivative(dabs, x) diff --git a/test/runtests.jl b/test/runtests.jl index 872e4ae..2af9826 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,6 +21,16 @@ using IntervalArith @test mag(x) === 3.0 @test mag(y) === 3.0 @test mag(Interval(-5, 1)) === 5 + # A Real is a degenerate thick number, so mag and mig are its absolute value + @test mag(-3.0) === 3.0 + @test mig(-3.0) === 3.0 + @test mag(2) === 2 + @test mig(2) === 2 + # iszero: true only for the degenerate set [0, 0] + @test iszero(Interval(0, 0)) + @test !iszero(Interval(0, 1)) + @test !iszero(Interval(-1, 0)) + @test !iszero(Interval(-1, 1)) @test lohi(Interval{Float64}, 1, 3) === x @test midrad(Interval{Float64}, 2, 1) === x @test basetype(Interval{Float64}) === Interval