1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
//! Per-opcode math kernels (single source of truth for formulas).
//!
//! Each opcode's numerical formula is easy to duplicate across the CPU AD
//! types (`Dual`, `DualVec`, `Reverse`, `BReverse`, `Laurent`) and the
//! bytecode-tape opcode dispatcher (`opcode.rs`), with the GPU side — WGSL
//! shaders and the CUDA NVRTC kernel — carrying yet another copy in their
//! respective source-level languages. A change to one copy can drift silently
//! from the others; such drift has produced real bugs (atan large-|a|, div
//! small-|b|, hypot Inf handling, out-of-domain log/atanh partials).
//!
//! Any numerical formula shared between the CPU AD types **and** the opcode
//! dispatcher should live here as a single generic function over
//! `num_traits::Float`; each AD type delegates to the helper so CPU drift
//! becomes impossible, and GPU drift is caught by `tests/gpu_cpu_parity.rs`
//! (and `tests/domain_nan_convention.rs` for the out-of-domain convention).
//! Currently extracted: `hypot_partials`, `atan2_partials`, `atan_deriv`,
//! `asinh_deriv`, `acosh_deriv`, and the domain-guarded log / `atanh`
//! derivatives below. Every new extraction must come with a call-site refactor
//! so we don't add abstraction for its own sake.
//!
//! # The structural-zero convention
//!
//! Zero tangents, zero partials, and singular points are handled by ONE
//! convention, applied symmetrically in both AD directions:
//!
//! 1. **Forward mode — zero tangents.** A structurally zero tangent
//! ([`IsAllZero`](crate::float::IsAllZero), every component exactly
//! zero) through any unary elemental stays exactly zero, even where the
//! derivative is unbounded (`sqrt`/`ln` at 0, `atanh` at ±1),
//! overflowed (`exp`/`sinh`/`cosh` at large arguments), or NaN
//! (out-of-domain primals). A constant is a constant; IEEE `0 × Inf =
//! NaN` must not leak into lanes that carry no derivative. Implemented
//! in `Dual`/`DualVec::chain` and, on the GPU, as blanket unary guards
//! in the tangent kernels.
//!
//! 2. **Reverse mode — zero multipliers.** An exactly-zero partial
//! (multiplier) absorbs ANY adjoint, including Inf/NaN adjoints from a
//! chained singularity downstream: a zero partial means this input does
//! not move the output locally, so the adjoint's magnitude is
//! irrelevant. Implemented as accumulation guards in every reverse
//! sweep (bytecode, eager, tangent-carrying, WGSL, CUDA). Note the
//! guard necessarily fires for every exact-zero partial — singular-point
//! zeros (hypot/atan2 at the origin), kink losing branches (max/min),
//! and regular zeros (mul by a zero operand, signum away from 0) are
//! indistinguishable at sweep time, and the convention is correct for
//! all of them. This completes the long-documented zero-ADJOINT skip
//! (the JAX-style `0 × NaN → 0` trade-off noted in `tape.rs`). A
//! corollary shared by both skips: reverse mode does not preserve a
//! derivative's NEGATIVE zero — adjoint accumulation starts at `+0.0`
//! and IEEE `+0.0 + (-0.0) = +0.0` — while forward mode's direct
//! products do. Signed zeros of derivatives are a forward-mode-only
//! guarantee.
//!
//! 3. **Singular-primal partials.** Kernels return a defined convention at
//! non-differentiable points rather than raw IEEE fallout: `hypot` and
//! `atan2` return exact `(0, 0)` at the origin; `ln`/`log2`/`log10`
//! derivatives follow the IEEE reciprocal sign at ±0 (`1/-0 = -Inf`);
//! `sqrt`/`cbrt` derivatives are `+Inf` at 0; out-of-domain points give
//! NaN partials (which are NOT zero, so rule 2 does not absorb them —
//! NaN still propagates to signal the domain error).
//!
//! 4. **Two mechanisms for origin short-circuits.** The eager single-use
//! tape (`Reverse`) may short-circuit a singular-point op to a tape-free
//! constant (`hypot`/`atan2` at the origin) because that tape is never
//! re-evaluated. The re-evaluable bytecode tape must NOT do this — a
//! recording-point constant would be stale on replay at other inputs —
//! so it keeps the node and relies on rule 2 at sweep time. Both
//! mechanisms produce the same gradients; do not "unify" them, the
//! asymmetry is load-bearing.
//!
//! A live (nonzero) tangent or adjoint through a singular point keeps the
//! non-finite result — the convention protects constants, never masks
//! singularities.
use Float;
/// Partial derivatives of `hypot(a, b) = sqrt(a² + b²)`.
///
/// `r` is the primal value `hypot(a, b)`. Returns `(∂r/∂a, ∂r/∂b)`.
///
/// At the origin (`r == 0`) the gradient is mathematically undefined
/// but we return `(0, 0)` to match the JAX / PyTorch convention and
/// to avoid emitting NaN into downstream adjoint chains.
/// Partial derivatives of `atan2(a, b)`.
///
/// Formula: `∂/∂a = b/(a²+b²)`, `∂/∂b = -a/(a²+b²)`. Factored as
/// `(b/h)/h` and `-(a/h)/h` where `h = hypot(a, b)` so the
/// intermediate never forms `a²+b²` directly — that would overflow
/// for `|a|, |b| > sqrt(MAX)` and underflow for values below
/// `sqrt(MIN_POSITIVE)`.
///
/// At the origin (`h == 0`) the gradient is mathematically undefined
/// and we return `(0, 0)`.
/// Derivative of `atan(a)` with an overflow-safe large-|a| path.
///
/// For `|a| ≤ 1e8`, returns `1/(1+a²)`. For `|a| > 1e8`, reformulates
/// via `u = 1/a` so `1/(1+a²) = u²/(1+u²)`, keeping every intermediate
/// in-range even at `|a| ≈ 1e19` where `a² overflows in f32.
/// Derivative of `asinh(a) = ln(a + sqrt(1+a²))` with a large-|a|
/// overflow-safe path.
///
/// For `|a| ≤ 1e8`, returns `1/sqrt(1+a²)`. For `|a| > 1e8`, uses
/// `u = 1/a` and `|u|/sqrt(1+u²)` so `1+a²` can't overflow.
/// Derivative of `acosh(a) = ln(a + sqrt(a²-1))`, guarded to the domain `a >= 1`
/// with a large-`a` overflow-safe path.
///
/// Strictly outside the domain (`a < 1`) returns NaN, matching [`ln_deriv`] /
/// [`atanh_deriv`]: the factored `(a-1)·(a+1)` self-guards only for `-1 < a < 1`
/// (sqrt of a negative), but `a <= -1` leaves it `>= 0` → a finite but meaningless
/// value (and `-Inf` at `a = -1` via `1/sqrt(-0.0)`), as does the overflow branch,
/// so the guard is explicit. The boundary `a = 1` keeps the IEEE `1/sqrt(0) = +Inf`
/// one-sided limit.
///
/// For `1 <= a <= 1e8`, returns `1/sqrt((a-1)·(a+1))`. The factored form (vs naive
/// `a*a - 1`) avoids catastrophic cancellation near `a = 1`: at `a = 1 + ε`, `a*a`
/// rounds to `1 + 2ε` and `a*a - 1 = 2ε` loses the `ε²` contribution, while
/// `(a-1)·(a+1) = ε·(2 + ε)` retains it. For `a > 1e8`, uses `u = 1/a` and
/// `|u|/sqrt(1-u²)`.
///
/// Every CPU AD mode (`Dual`, `DualVec`, `Reverse`) and the bytecode `OpCode`
/// dispatcher delegate here, so the factored form and the domain guard share a
/// single source of truth. The GPU kernels carry the same factored form and guard
/// in their own languages: the wgpu shaders (`reverse.wgsl`, `tangent_forward.wgsl`,
/// `tangent_reverse.wgsl` — both HVP phases) and the CUDA `tape_eval.cu` at all four
/// derivative sweeps (reverse, tangent-forward, and both HVP phases). Pinned by
/// `acosh_deriv_factored_form_keeps_precision_near_one` (factoring) and
/// `tests/domain_nan_convention.rs` (the guard, cross-mode + GPU).
/// Derivative of `ln(a) = 1/a`, guarded to the real domain.
///
/// Domain-restricted logs emit a NaN partial *strictly* outside their valid
/// interval (`a < 0`) so a caller that supplied an out-of-domain input sees NaN
/// rather than a finite-but-meaningless value (`1/-2 = -0.5`). The boundary
/// `a = 0` is left to IEEE arithmetic, sign of zero included: `1/(+0) = +Inf`
/// (the one-sided derivative limit) and `1/(-0) = -Inf`. Unlike `abs_deriv`,
/// ±0 is deliberately NOT collapsed to one value here — every path (all CPU AD
/// modes and both GPU backends) computes the same `1/a` formula, with the ±0
/// result following each backend's IEEE division, so special-casing `-0` would
/// require touching every one of them for an input whose primal `ln(-0) = -Inf`
/// already signals the degenerate limit. Every AD mode and the bytecode
/// `OpCode` dispatcher delegate here so the convention has a single source of
/// truth; the wgpu and CUDA kernels carry the same guard in their own languages
/// (reverse, forward-tangent, and HVP sweeps), pinned by
/// `tests/domain_nan_convention.rs`.
/// Derivative of `log2(a) = 1/(a·ln 2)`, guarded to `a >= 0` (see [`ln_deriv`]).
/// Derivative of `log10(a) = 1/(a·ln 10)`, guarded to `a >= 0` (see [`ln_deriv`]).
/// Derivative of `ln(1+a) = 1/(1+a)`, guarded to `a >= -1`.
///
/// The boundary `a = -1` yields IEEE `1/0 = +Inf`; `a < -1` yields NaN.
/// Derivative of `atanh(a) = 1/((1-a)(1+a))`, guarded to `|a| <= 1`.
///
/// Unlike `asin`/`acos` (whose `sqrt(neg)` self-guards to NaN), `atanh`'s
/// derivative is a bare reciprocal that stays finite for `|a| > 1`
/// (`1/((1-1.5)(1+1.5)) = -0.8`), so it needs an explicit domain guard. The
/// boundary `|a| = 1` yields IEEE `1/0 = +Inf`.
/// Derivative (subgradient) of `|a|`, unified across every AD mode and both GPU
/// backends: `0` at the kink `a = 0`, `sign(a)` elsewhere, `NaN` at `NaN`.
///
/// `0` is the minimal-norm element of the Clarke subdifferential `[-1, 1]` at the
/// kink — the convention PyTorch / JAX / TensorFlow use. Crucially it is *value*-
/// based: `a == 0` catches both `+0.0` and `-0.0`, so the result never depends on
/// the sign bit of a zero (which is reachable via `0*-1`, `x - x`, underflow…).
/// Relying on `a.signum()` alone would leak that sign bit — `signum(+0.0) = +1`,
/// `signum(-0.0) = -1` in Rust — making algebraically equivalent points report
/// different subgradients. `NaN` flows through via `signum(NaN) = NaN`.
///
/// Every eager AD type (`Dual`, `DualVec`, `Reverse`) and the bytecode `OpCode`
/// dispatcher delegate here; the wgpu and CUDA kernels carry the same
/// `a == 0 -> 0` guard. Sharp/limiting subgradients (Clarke, `jacobian_limiting`)
/// still force `±1` explicitly via `forced_reverse_partials`, independent of this
/// smooth default.