From d01e00adf712b31491c63121d9441a38014024dd Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Wed, 27 Sep 2023 21:16:14 -0500 Subject: [PATCH 1/2] Add flexibility in dispatch for `iszero_tuple` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IntervalArithmetic.jl may abandon support for `==` among intervals (https://github.com/JuliaIntervals/IntervalArithmetic.jl/pull/571). To support specialization for specific Number subtypes, this makes `iszero_tuple` into a "trait"-dispatched function, first unwrapping all the way down to the elementary numeric type and then jointly dispatching on that type and the actual tuple. This makes it possible to create an extension in IntervalArithmetic that specializes the implementation to use the new comparison operator `≛`. The use of recursive unwrapping enables support for higher-order derivatives. --- src/dual.jl | 3 +++ src/partials.jl | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/dual.jl b/src/dual.jl index a91444b6..034b64c2 100644 --- a/src/dual.jl +++ b/src/dual.jl @@ -25,6 +25,9 @@ end ########## Base.ArithmeticStyle(::Type{<:Dual{T,V}}) where {T,V} = Base.ArithmeticStyle(V) +unwrap_dual(::Type{Dual{T,V,N}}) where {T,V,N} = unwrap_dual(V) +unwrap_dual(::Type{V}) where V = V + ############## # Exceptions # ############## diff --git a/src/partials.jl b/src/partials.jl index 49f8364b..e69e90e3 100644 --- a/src/partials.jl +++ b/src/partials.jl @@ -166,7 +166,11 @@ end @inline rand_tuple(::AbstractRNG, ::Type{Tuple{}}) = tuple() @inline rand_tuple(::Type{Tuple{}}) = tuple() -@generated function iszero_tuple(tup::NTuple{N,V}) where {N,V} +iszero_tuple(tup::NTuple{N,V}) where {N,V} = _iszero_tuple(unwrap_dual(V), tup) + +# default implementation; specific number types (e.g., Interval from IntervalArithmetic) +# can add specializations. +@generated function _iszero_tuple(::Type{V0}, tup::NTuple{N,V}) where {V0,N,V} ex = Expr(:&&, [:(z == tup[$i]) for i=1:N]...) return quote z = zero(V) From 63a008b883cd066551c7cb0bf95f54eee47e1cbc Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Wed, 26 Aug 2026 06:50:49 -0500 Subject: [PATCH 2/2] Test that `iszero_tuple` can be specialized The test defines a scalar type whose `==` throws, so the tests pass only if the `_iszero_tuple` specialization is reached, both directly and through nested `Dual`s. Assisted-by: Claude Opus 5 --- test/PartialsTest.jl | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/PartialsTest.jl b/test/PartialsTest.jl index 0e73160b..a5c44898 100644 --- a/test/PartialsTest.jl +++ b/test/PartialsTest.jl @@ -164,4 +164,40 @@ show(io, MIME("text/plain"), Partials((1, 2, 3))) str = String(take!(io)) @test str == "3-element $(ForwardDiff.Partials{3,Int}):\n 1\n 2\n 3" +################################### +# Specialization of iszero_tuple # +################################### + +# A scalar type that does not support `==`, standing in for types (e.g., intervals) +# that reserve `==` for a different meaning and supply their own `iszero_tuple` +# implementation. +struct Fuzzy{T<:Real} <: Real + val::T +end +Base.:(==)(::Fuzzy, ::Fuzzy) = throw(ArgumentError("`==` is not supported for Fuzzy")) +Base.zero(::Type{Fuzzy{T}}) where {T} = Fuzzy(zero(T)) + +fuzzy_iszero(x::Fuzzy) = iszero(x.val) +fuzzy_iszero(d::ForwardDiff.Dual) = + fuzzy_iszero(ForwardDiff.value(d)) && all(fuzzy_iszero, ForwardDiff.partials(d).values) + +ForwardDiff._iszero_tuple(::Type{<:Fuzzy}, tup::NTuple{N,V}) where {N,V} = all(fuzzy_iszero, tup) + +@testset "iszero_tuple specialization" begin + @test ForwardDiff.unwrap_dual(Fuzzy{Float64}) === Fuzzy{Float64} + @test ForwardDiff.unwrap_dual(ForwardDiff.Dual{:t1,Fuzzy{Float64},1}) === Fuzzy{Float64} + @test ForwardDiff.unwrap_dual(ForwardDiff.Dual{:t2,ForwardDiff.Dual{:t1,Fuzzy{Float64},1},1}) === Fuzzy{Float64} + + # Without the specialization these would throw, since the default implementation uses `==` + z, o = Fuzzy(0.0), Fuzzy(1.0) + @test iszero(Partials((z, z))) + @test !iszero(Partials((z, o))) + + # Higher-order derivatives: dispatch is on the type wrapped by the `Dual`s + dz = ForwardDiff.Dual{:t1}(z, Partials((z,))) + dnz = ForwardDiff.Dual{:t1}(z, Partials((o,))) + @test iszero(Partials((dz, dz))) + @test !iszero(Partials((dz, dnz))) +end + end # module