Skip to content

Avoid catastrophic cancellation in the derivatives of tanh and inverse trigonometric/hyperbolic functions - #122

Merged
devmotion merged 3 commits into
masterfrom
dmw/accurate-inverse-derivatives
Nov 10, 2026
Merged

devmotion merged 3 commits into
masterfrom
dmw/accurate-inverse-derivatives

Conversation

@devmotion

@devmotion devmotion commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Closes #102. This ports the changes of JuliaDiff/ChainRules.jl#864 to DiffRules, as far as possible without branches.

Changes

  • tanh: 1 - tanh(x)^2 → sech(x)^2. Once tanh(x) rounds to ±1, the old form returns 0. In Float64 that happens for |x| ≥ 20, where the relative error is 100%, and it is already 1e-8 at x = 10. sech(x)^2 is accurate everywhere, with a relative error ≤ 4e-16 in the tested range up to |x| = 300.
  • 1 - x^2 → (1 - x) * (1 + x) in asin, acos, asind, acosd, atanh, acoth, asech, NaNMath.asin, NaNMath.acos and NaNMath.atanh.
  • x^2 - 1 → (x - 1) * (x + 1) in asec, acsc, asecd and acscd. acosh and NaNMath.acosh use sqrt(x - 1) * sqrt(x + 1), as in ChainRules. This also avoids the overflow of x^2 for large x (e.g., the derivative of acosh at 1e200 was 0), and makes the derivative of NaNMath.acosh NaN for x < 1, like the function itself.
  • log10/log2: inv(x) / logten → inv(logten * x), the same form as the NaNMath rules.

The factored forms are exact for x near ±1 (Sterbenz) and cost the same number of operations.

Why no branches

A rule like ChainRules' abs(x) < 1 ? 1 - Ω^2 : sech(x)^2 would be faster in some consumers. But DiffRules' expressions are also evaluated on symbolic arguments: Symbolics builds its derivative rules from them. A value branch would make Symbolics.derivative(tanh(x), x) throw for every released Symbolics version. sech(x)^2 has no branch and equals the derivative as a function. That also makes it valid for Taylor-mode AD, which evaluates the expression on truncated series (JuliaDiff/TaylorDiff.jl#113).

Performance

The new forms give the same derivatives up to rounding. Measured with ForwardDiff (ns per ForwardDiff.derivative call, broadcast over 10⁴ points):

before after
asin / acos / atanh (|x| < 0.9) 6.32 / 7.51 / 8.48 6.43 / 7.73 / 8.29
acosh / asec / acoth (1.1 < x < 6.1) 8.30 / 5.58 / 7.98 8.42 / 5.35 / 7.74

sech(x)^2 costs one cosh more than 1 - tanh(x)^2 in consumers that reuse the primal value, as ForwardDiff does through CSE. ForwardDiff keeps its fast path for |x| < 1 by defining tanh by hand (JuliaDiff/ForwardDiff.jl#874).

Downstream note

Symbolics' derivative expressions change shape, e.g. Symbolics.derivative(asin(x), x) becomes 1 / sqrt((1 - x) * (1 + x)). Downstream tests that compare printed expressions may need to be updated.

Tests

New accuracy tests compare every changed rule, in Float32 and Float64, against the rule evaluated in 256-bit BigFloat (rtol = 8 eps), near |x| = 1 and for large |x| (tanh, and acosh at a point where x^2 overflows). Every one of these test sets fails with the previous rules. All tests pass (1214 in total). ForwardDiff's test suite (v1.4.7) also passes with this branch.

🤖 Generated with Claude Code

…erivatives

`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>
devmotion and others added 2 commits November 10, 2026 09:04
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devmotion
devmotion merged commit ad0834c into master Nov 10, 2026
8 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rule for tanh has catastrophic cancellation for |x| > 20

1 participant