Repository navigation
Avoid catastrophic cancellation in tanh and inverse trig/hyperbolic derivatives - #864
Merged
Merged
Conversation
…erivatives The `tanh` rule `1 - Ω ^ 2` loses all accuracy for large `|x|` since `Ω = tanh(x)` rounds to ±1 (see JuliaDiff/DiffRules.jl#102); use `sech(x) ^ 2` for `|real(x)| ≥ 1`. Rules involving `1 - x ^ 2` or `x ^ 2 - 1` (asin, acos, atanh, acoth, asech, asind, acosd, and the real variants of asec, acsc, asecd, acscd) lose accuracy near `|x| = 1`; use the factorized forms `(1 - x) * (1 + x)` and `(x - 1) * (x + 1)` instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #864 +/- ##
=======================================
Coverage 96.64% 96.65%
=======================================
Files 28 28
Lines 3609 3614 +5
=======================================
+ Hits 3488 3493 +5
Misses 121 121 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…acotd - Complex `1 + x ^ 2` is factorized as `(1 + im * x) * (1 - im * x)` - Real `asinh` uses `hypot(x, 1)`, `atan(y, x)` uses `hypot(x, y)` Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
`hypot` prevents SIMD and made the derivatives ~10x slower. `asinh` now falls back to `1 / |x|` only if `x ^ 2` overflows, and `atan(y, x)` scales by `max(|x|, |y|)`, which also fixes `atan_fast`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This was referenced Nov 6, 2026
Draft
This was referenced Nov 10, 2026
devmotion
added a commit
to JuliaDiff/DiffRules.jl
that referenced
this pull request
Nov 10, 2026
…rse trigonometric/hyperbolic functions (#122) * Avoid catastrophic cancellation in tanh and inverse trig/hyperbolic derivatives `1 - tanh(x)^2` returns 0 once `tanh(x)` rounds to ±1 (#102); use the branch-free `sech(x)^2`. Rules involving `1 - x^2` or `x^2 - 1` lose accuracy near `|x| = 1`; use the factorized forms `(1 - x) * (1 + x)` and `(x - 1) * (x + 1)` instead, as in JuliaDiff/ChainRules.jl#864. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Avoid overflow in the derivative of NaNMath.acosh Use `sqrt(x - 1) * sqrt(x + 1)` as for `Base.acosh`: `(x - 1) * (x + 1)` overflows for large `x`, and the derivative is now NaN for `x < 1`, like the primal. Test both rules at a point where `x^2` overflows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bump version to 1.17.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
devmotion
added a commit
to JuliaDiff/ForwardDiff.jl
that referenced
this pull request
Nov 13, 2026
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 <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.
Ports the fix for JuliaDiff/DiffRules.jl#102 to ChainRules, and fixes related cancellations.
tanh1 - Ω ^ 2withΩ = tanh(x)cancels catastrophically for large|x|. Fromx ≈ 19.07,Ωrounds to 1 and the derivative is0. Rearranging Ω, e.g.(1 - Ω) * (1 + Ω), cannot fix this, so the derivative is computed fromx:sech(x) ^ 2for|real(x)| ≥ 1. The cheap1 - Ω ^ 2is kept for|real(x)| < 1, where|sech(x)|^2 ≥ 0.42, so it doesn't suffer from cancellation.Relative error vs. BigFloat:
The Hermitian/Symmetric matrix
tanhrule calls the scalarfrule, so it picks up the fix too.1 - x ^ 2/x ^ 2 - 1near|x| = 1asin,acos,atanh,acoth,asech,asind,acosdand theRealmethods ofasec,acsc,asecd,acscdnow use(1 - x) * (1 + x)resp.(x - 1) * (x + 1). Near|x| = 1, the factor1 - x(resp.1 + x) is computed exactly, which avoids the cancellation in1 - x ^ 2. Example:atanhatx = 1 - 1e-8goes from 6e-10 to 1e-16 relative error. On the complex branch cuts (signed-zero imaginary parts), the signs match the previous rules.The generic (complex)
1 - x ^ -2forms ofasec/acsc/asecd/acscdare not changed. The obvious accurate rewrite,(x - 1) * (x + 1) / x ^ 2, puts signed-zero inputs on(-1, 1)on the wrong side of the branch cut.References: this is the standard rewrite of
x² - y²as(x - y)(x + y), which replaces catastrophic cancellation with a benign one (D. Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic, 1991, section "Cancellation"). For1/2 ≤ x ≤ 2,1 - xis computed exactly (ibid., Theorem 11, the Sterbenz lemma). Julia Base uses the same factorization inatanh(::Complex), andsqrt(1-z)*sqrt(1+z)inasin(::Complex). ChainRules' existingacoshrule,inv(sqrt(x - 1) * sqrt(x + 1)), is similar.Complex
1 + x ^ 2nearx = ±iasinh,atan,acot,atandandacotdhave newComplexmethods that use(1 + ix) * (1 - ix), which is the same rewrite as above: nearx = ±i, one factor is computed exactly. For example, atx = 1e-9 + (1 - 1e-8)im, the relative error drops from 3e-10 (asinh) and 6e-10 (the others) to beloweps(). Inasinh,ixis constructed asComplex(-imag(x), real(x)), becauseim * xloses the sign of a zero real part, which matters on the branch cuts. On a grid of signed zeros,±1,±2,±1e300,±InfandNaNin both components, the new rules agree with the old ones up to rounding. Complexacsch(1 + x ^ -2) is not changed, for the same branch-cut reason as complexasec/acsc.Overflow and underflow of
x ^ 2asinh(x::Real)now returnsinv(abs(x))ifx ^ 2overflows. This is exact up to rounding, since1 / |x|matches1 / sqrt(1 + x ^ 2)to withineps / 2already for|x| > 1 / sqrt(eps) = 2 ^ 26, long beforex ^ 2overflows at|x| ≈ 1.3e154. Before,asinhat1e200gave0.0instead of1e-200.atan(y, x)now dividesxandybys = max(|x|, |y|)first: withxs = x / s,ys = y / sandv = xs ^ 2 + ys ^ 2(which is in[1, 2]), the partials are(xs / v / s, -ys / v / s). Before,(1e-200, 1e-200)gaveNaNinstead of±5e199, and(1e200, 3e200)gave0.0;Intinputs could also overflow silently inx ^ 2 + y ^ 2. The maximum relative error over the full range is about 1.6eps. For ordinary inputs, results can differ from before in the last bits. This also works foratan_fast.hypotwould also avoid the overflow, but it is ~10× slower than the old formulas, since its branches and correction step prevent SIMD.Possible regressions
atan(y, x)with infinite arguments (next item). Overflow/limit cases (±800,±Inf,NaN,800 + 1im,Inf + 1im, near the poles atiπ/2) give the same results as before. Element types (Float16,Float32,Int,ComplexF64) are preserved.Float32improves, e.g.tanhat30f0:0before,3.5e-26now.atan(y, x)when exactly one argument is infinite: both partials are nowNaN. Before, one of them was±0.0(the correct limit) and the otherNaN, e.g.(Inf, 1.0)gave(0.0, NaN). All other special values (±0,±1,±Inf,NaNin both arguments) give the same results as before, including(NaN, NaN)at(0, 0).|x| ≥ 1, thetanhderivative now evaluatessech(x)instead of reusingΩ. A broadcast over 10⁶ values with|x|up to about 5 took ~25% longer (5.8 ms → 7.1 ms).asinhis unchanged (0.45 ms → 0.47 ms for 10⁶ values).atan(y, x)takes three divisions instead of two (0.37 ms → 0.96 ms for 10⁶ values), which is small compared to the primalatan(y, x)(~17 ms).tanhrule now branches onabs(real(x)) < 1, and the realasinhrule onisfinite(x ^ 2), so they requireBool-valued comparisons. Number types whose comparisons return symbolic or traced values would fail here.(1 - x) * (1 + x)into1 - x * x, soasin_fastetc. may not gain accuracy. The new tests pass for them.Tests
FiniteDifferences can't detect these errors, so the new testsets compare
fruleagainst the analytic derivative evaluated in BigFloat, withrtol = 4eps().🤖 Generated with Claude Code