Skip to content

Add derivative rules for norm and fix hypot at zero - #872

Draft
devmotion wants to merge 1 commit into
masterfrom
dmw/norm
Draft

devmotion wants to merge 1 commit into
masterfrom
dmw/norm

Conversation

@devmotion

Copy link
Copy Markdown
Member

Fixes #243, fixes #794.

Problem

The generic implementations of norm (LinearAlgebra.generic_norm2/generic_normp and StaticArrays' _norm/_norm_scaled) rescale by the maximum absolute value to avoid overflow and underflow. Their branches (iszero, isfinite, <) take partials into account for Duals (< since #791), so Duals end up in the wrong branch:

input before after
gradient(norm, [0.0, 0.0]) [NaN, NaN] [0.0, 0.0]
gradient(x -> norm(x)^2, [0.0, 0.0]) [NaN, NaN] [0.0, 0.0]
gradient(x -> norm(x, 3), [0.0, 0.0]) [NaN, NaN] [0.0, 0.0]
norm([Dual(1e-200, 1e-100), Dual(0.0, 0.0)]) Dual(0.0, Inf) (wrong primal) Dual(1e-200, 1e-100)
gradient(norm, [1e-200, 1e-200]) [Inf, Inf] [0.707, 0.707]
norm(SA[Dual(0, 1), Dual(0, 1), Dual(0, 1)]) Dual(NaN, NaN) Dual(0.0, 0.0)
derivative(p -> norm([3.0, 4.0], p), 3.0) MethodError -0.304
hypot(Dual(0.0, 1.0), Dual(0.0, 1.0)) Dual(0.0, NaN) Dual(0.0, 0.0)

All of these fail with NaN-safe mode enabled as well.

Approach

Similar to the rules in ChainRules, the norm of the primal values is computed with the existing (scale-robust) implementations and the chain rule is applied with weights that are bounded by 1 in absolute value (no overflow/underflow):

  • with respect to x: w_i = |u_i|^(p-1) * sign(u_i) with u_i = x_i / norm(x, p) (u_i for p = 2; only the first maximum for p = Inf); p ∈ {0, 1, -Inf} keep the generic implementation, which is not affected by the scaling issues
  • with respect to p: norm(x, p) / p * sum(|u_i|^p * log(|u_i|)), which is bounded and avoids the cancellation of sum(|x_i|^p * log(|x_i|)) - norm(x, p) * log(norm(x, p))
  • at norm(x, p) == 0, the subgradient zero is used (ChainRules uses the same convention via pinv)

Complex arrays, nested differentiation (with all tag orderings of x and p), StaticArrays, Adjoint/Transpose, and sparse arrays are supported. Method ambiguities with LinearAlgebra, StaticArrays, and SparseArrays (new weak dependency/extension) are resolved explicitly.

hypot uses the same convention at zero. Additionally, the three-argument hypot no longer drops the partials of outer tags in nested differentiation (it used the tag-agnostic value/partials).

Limitations

Second-order derivatives through norm at zero remain incorrect (e.g., hessian(x -> norm(x)^2, zeros(2)) returns zeros instead of 2I): Since norm is not differentiable at zero, no choice of partials of norm(x) can recover the correct result. This is documented in the limitations section of the docs; sum(abs2, x) should be used instead.

🤖 Generated with Claude Code

The generic implementations of `norm` in LinearAlgebra and StaticArrays
rescale by the maximum absolute value to avoid overflow and underflow.
Their branches (`iszero`, `isfinite`, `<`) take partials into account for
`Dual`s, resulting in NaNs, Infs, and even incorrect primal values
(e.g., at zero or for very small or large inputs).

Instead, the norm of the primal values is computed and the chain rule is
applied with weights that are bounded by 1 in absolute value. At zero, the
subgradient zero is used (as in ChainRules). Derivatives with respect to
`p` are supported as well.

Additionally, `hypot` uses the same convention at zero, and the
three-argument `hypot` no longer drops the partials of outer tags in
nested differentiation.

Fixes #243, fixes #794.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.25000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.78%. Comparing base (70ef06c) to head (50ba6b3).

Files with missing lines Patch % Lines
ext/SparseArraysExt.jl 66.66% 2 Missing ⚠️
ext/ForwardDiffStaticArraysExt.jl 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #872      +/-   ##
==========================================
+ Coverage   91.24%   91.78%   +0.54%     
==========================================
  Files          11       12       +1     
  Lines        1085     1157      +72     
==========================================
+ Hits          990     1062      +72     
  Misses         95       95              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
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.

norm() on a zero vector with non-zero partials gives nan value for v1.3.1+ derivative of norm at 0

1 participant