From 36881c903eb8b32181718ec22b9b46ecfbef7000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Tue, 10 Nov 2026 02:28:02 +0100 Subject: [PATCH 1/3] Accurate derivative of tanh for large |x| MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DiffRules' `1 - tanh(x)^2` returns 0 once `tanh(x)` rounds to ±1 (JuliaDiff/DiffRules.jl#102). Define `tanh(::Dual)` by hand: reuse the primal value for `|x| < 1` and use `sech(x)^2` otherwise, as in JuliaDiff/ChainRules.jl#864. Co-Authored-By: Claude Opus 5.5 --- src/dual.jl | 14 +++++++++++++- test/DualTest.jl | 22 +++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/dual.jl b/src/dual.jl index 48d5d0e3..8d4f0bb3 100644 --- a/src/dual.jl +++ b/src/dual.jl @@ -474,7 +474,7 @@ Base.float(d::Dual) = convert(float(typeof(d)), d) ################################### for (M, f, arity) in DiffRules.diffrules(filter_modules = nothing) - if (M, f) in ((:Base, :^), (:NaNMath, :pow), (:Base, :/), (:Base, :+), (:Base, :-), (:Base, :sin), (:Base, :cos)) + if (M, f) in ((:Base, :^), (:NaNMath, :pow), (:Base, :/), (:Base, :+), (:Base, :-), (:Base, :sin), (:Base, :cos), (:Base, :tanh)) continue # Skip methods which we define elsewhere. elseif !(isdefined(@__MODULE__, M) && isdefined(getfield(@__MODULE__, M), f)) continue # Skip rules for methods not defined in the current scope @@ -721,6 +721,18 @@ end return (Dual{T}(sd, cd * partials(d)), Dual{T}(cd, -sd * partials(d))) end +# tanh # +#------# + +# `1 - tanh(x)^2` reuses the primal value but suffers from catastrophic cancellation for +# large `|x|` (JuliaDiff/DiffRules.jl#102), so use `sech(x)^2` there +@inline function Base.tanh(d::Dual{T}) where T + x = value(d) + val = tanh(x) + deriv = abs(x) < 1 ? 1 - val^2 : sech(x)^2 + return Dual{T}(val, deriv * partials(d)) +end + # sincospi # #----------# diff --git a/test/DualTest.jl b/test/DualTest.jl index 094e009b..5ebb27ae 100644 --- a/test/DualTest.jl +++ b/test/DualTest.jl @@ -559,7 +559,12 @@ ForwardDiff.:≺(::Type{OuterTestTag}, ::Type{TestTag}) = false if actualval isa Real @test dx isa Dual{TestTag} @test value(dx) == actualval - @test partials(dx, 1) == $deriv + if $(M === :Base && f === :tanh) + # defined by hand with an equivalent formula that is faster for |x| < 1 + @test partials(dx, 1) ≈ $deriv + else + @test partials(dx, 1) == $deriv + end else @test dx isa Complex{<:Dual{TestTag}} @test value(real(dx)) == real(actualval) @@ -775,4 +780,19 @@ end end end +@testset "tanh: no catastrophic cancellation for large |x|" begin # JuliaDiff/DiffRules.jl#102 + for T in (Float32, Float64), x in (0.5, -0.99, 1.0, -5.0, 10.0, -20.0, 30.0, 300.0) + x = T(x) + expected = T(setprecision(() -> sech(big(x))^2, BigFloat, 256)) + d = tanh(Dual{TestTag}(x, one(x))) + @test value(d) === tanh(x) + @test partials(d, 1) isa T + @test partials(d, 1) ≈ expected rtol = 4 * eps(T) + # nested duals take the same branch + dd = tanh(Dual{TestTag}(Dual{TestTag}(x, one(x)), one(x))) + @test value(value(dd)) === tanh(x) + @test value(partials(dd, 1)) ≈ expected rtol = 4 * eps(T) + end +end + end # module From 3919d0cc81eeec39b9431f868ca244fe1acb5f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 13 Nov 2026 10:15:35 +0100 Subject: [PATCH 2/3] Use fields --- src/dual.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dual.jl b/src/dual.jl index 8d4f0bb3..fe95ca39 100644 --- a/src/dual.jl +++ b/src/dual.jl @@ -727,10 +727,10 @@ end # `1 - tanh(x)^2` reuses the primal value but suffers from catastrophic cancellation for # large `|x|` (JuliaDiff/DiffRules.jl#102), so use `sech(x)^2` there @inline function Base.tanh(d::Dual{T}) where T - x = value(d) + x = d.value val = tanh(x) deriv = abs(x) < 1 ? 1 - val^2 : sech(x)^2 - return Dual{T}(val, deriv * partials(d)) + return Dual{T}(val, deriv * d.partials) end # sincospi # From 284de1c7ffe7593a1e9101b30bcafcdf73b68487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Fri, 13 Nov 2026 12:49:02 +0100 Subject: [PATCH 3/3] Bump version from 1.4.8 to 1.4.9 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index de0ce4da..97dd92b0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ForwardDiff" uuid = "f6369f11-7733-5829-9624-2563aa707210" -version = "1.4.8" +version = "1.4.9" [deps] CommonSubexpressions = "bbf7d656-a473-5ed7-a52c-81e309532950"