@danielsimonjr/mathts-autograd 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,659 +1,20 @@
1
- import { Tensor, Index } from '@danielsimonjr/mathts-tensor';
2
-
3
1
  /**
4
- * DualTensor — a Tensor + per-element tangent component for forward-mode AD.
5
- * Storage: two Float64Arrays of equal length (primal + tangent), shape from
6
- * the wrapped Tensor. Arithmetic follows the dual-number rules:
7
- * (a, a') + (b, b') = (a+b, a'+b')
8
- * (a, a') * (b, b') = (a·b, a·b' + a'·b)
9
- * (a, a') / (b, b') = (a/b, (a'·b − a·b')/b²)
10
- * scale((a, a'), k) = (k·a, k·a')
11
- * Elementwise unless noted. Reductions and contractions add their own rules.
2
+ * @danielsimonjr/mathts-autograd — forward + reverse-mode automatic
3
+ * differentiation for the MathTS rank-N Tensor type.
12
4
  *
13
- * The dual-number framework: tracking ε such that ε²=0; (a + a'·ε)(b + b'·ε)
14
- * = ab + (ab'+a'b)ε, so the tangent is the linearized first-order response.
5
+ * v0.1.0 ships:
6
+ * - forwardGrad(fn, x): dual-number AD; returns { value, jacobian }.
7
+ * - reverseGrad(fn, x, cotangent?): tape-based reverse-mode AD;
8
+ * returns { value, gradient }.
15
9
  *
16
- * @packageDocumentation
17
- */
18
-
19
- declare class DualTensor {
20
- readonly shape: ReadonlyArray<number>;
21
- readonly primal: Float64Array;
22
- readonly tangent: Float64Array;
23
- constructor(shape: ReadonlyArray<number>, primal: Float64Array, tangent: Float64Array);
24
- /**
25
- * S5 fix: existing engine ops (e.g. lower, pderiv, contract) reach into
26
- * `.data`. The getter returns the primal so those ops still work when a
27
- * DualTensor flows through them as a structurally-compatible Tensor.
28
- * (AD-aware ops branch on `'tangent' in arg` before reaching here.)
29
- */
30
- get data(): Float64Array;
31
- /** Lift a Tensor to a DualTensor with zero tangent. */
32
- static fromTensor(t: Tensor): DualTensor;
33
- /** Lift a Tensor to a DualTensor with a unit tangent at flat-index `i`. */
34
- static unitAt(t: Tensor, i: number): DualTensor;
35
- /** Extract the primal as a plain Tensor. */
36
- toPrimalTensor(): Tensor;
37
- /** Extract the tangent as a plain Tensor. */
38
- toTangentTensor(): Tensor;
39
- /** Elementwise addition following dual-number rule: (a+b, a'+b'). */
40
- add(other: DualTensor): DualTensor;
41
- /** Elementwise subtraction following dual-number rule: (a-b, a'-b'). */
42
- sub(other: DualTensor): DualTensor;
43
- /**
44
- * Elementwise multiplication following dual-number rule: (a·b, a·b' + a'·b).
45
- *
46
- * I3 fix: explicit alias check. When `this === other` (self-multiplication),
47
- * use the specialised rule (a·a)' = 2·a·a' directly. The general rule also
48
- * degenerates correctly, but the explicit branch is more readable and aligns
49
- * the forward-mode implementation with the reverse-mode alias fix.
50
- */
51
- mul(other: DualTensor): DualTensor;
52
- /** Scalar multiplication following dual-number rule: (k·a, k·a'). */
53
- scale(k: number): DualTensor;
54
- /**
55
- * Elementwise division following the dual-number quotient rule:
56
- * (a, a') / (b, b') = (a/b, (a'·b − a·b') / b²).
57
- * The aliased case a.divide(a) is (1, 0).
58
- */
59
- divide(other: DualTensor): DualTensor;
60
- private _unaryElementwise;
61
- /**
62
- * Apply a shared elementary rule from core's `DUAL_UNARY_RULES` element-wise.
63
- * Sourcing `primal`/`deriv` from the canonical table (rather than inline
64
- * `Math.*`) keeps this AD path and the scalar `Dual` in lock-step and lets a
65
- * future accelerated `primal` in the standard layer propagate here for free —
66
- * the loop still calls plain scalar functions, so it stays allocation-free.
67
- */
68
- private _rule;
69
- /** Exponential. f'(x) = eˣ = y. */
70
- exp(): DualTensor;
71
- /** Natural logarithm. f'(x) = 1/x. */
72
- log(): DualTensor;
73
- /** Sine. f'(x) = cos(x). */
74
- sin(): DualTensor;
75
- /** Cosine. f'(x) = −sin(x). */
76
- cos(): DualTensor;
77
- /** Tangent. f'(x) = 1/cos²(x). */
78
- tan(): DualTensor;
79
- /** Square root. f'(x) = 1/(2·y) where y = √x. */
80
- sqrt(): DualTensor;
81
- /** Square (x²). f'(x) = 2x. */
82
- square(): DualTensor;
83
- /** Reciprocal (1/x). f'(x) = −1/x². */
84
- reciprocal(): DualTensor;
85
- /** Absolute value. f'(x) = sign(x) (subgradient 0 at exact zero). */
86
- abs(): DualTensor;
87
- /** Fixed-exponent power x^k. f'(x) = k·x^(k−1). */
88
- pow(k: number): DualTensor;
89
- /** Hyperbolic sine. f'(x) = cosh(x). */
90
- sinh(): DualTensor;
91
- /** Hyperbolic cosine. f'(x) = sinh(x). */
92
- cosh(): DualTensor;
93
- /** Hyperbolic tangent. f'(x) = 1 − tanh²(x) = 1 − y². */
94
- tanh(): DualTensor;
95
- /** Arcsine. f'(x) = 1/√(1 − x²). */
96
- asin(): DualTensor;
97
- /** Arccosine. f'(x) = −1/√(1 − x²). */
98
- acos(): DualTensor;
99
- /** Arctangent. f'(x) = 1/(1 + x²). */
100
- atan(): DualTensor;
101
- /** Inverse hyperbolic sine. f'(x) = 1/√(x² + 1). */
102
- asinh(): DualTensor;
103
- /** Inverse hyperbolic cosine (x ≥ 1). f'(x) = 1/√(x² − 1). */
104
- acosh(): DualTensor;
105
- /** Inverse hyperbolic tangent (|x| < 1). f'(x) = 1/(1 − x²). */
106
- atanh(): DualTensor;
107
- /** Base-2 logarithm. f'(x) = 1/(x·ln2). */
108
- log2(): DualTensor;
109
- /** Base-10 logarithm. f'(x) = 1/(x·ln10). */
110
- log10(): DualTensor;
111
- /** log(1 + x). f'(x) = 1/(1 + x). */
112
- log1p(): DualTensor;
113
- /** exp(x) − 1. f'(x) = eˣ = y + 1. */
114
- expm1(): DualTensor;
115
- /** Cube root. f'(x) = 1/(3·y²) where y = x^(1/3) (→∞ at x = 0). */
116
- cbrt(): DualTensor;
117
- /** Sign (−1/0/+1). f'(x) = 0 almost everywhere. */
118
- sign(): DualTensor;
119
- /**
120
- * Two-argument arctangent: atan2(this, other), this = y, other = x.
121
- * Tangent: (b·a' − a·b') / (a² + b²), with a = this, b = other.
122
- */
123
- atan2(other: DualTensor): DualTensor;
124
- private checkSameShape;
125
- }
126
-
127
- /**
128
- * Forward-mode AD via dual numbers. Returns the value and the full Jacobian
129
- * of fn at x, shape [...value.shape, ...x.shape] (row-major flatten).
130
- *
131
- * Implementation: for each flat-index k in x, build a DualTensor with a unit
132
- * tangent at position k, run fn (which operates on Tensors — see "interop"
133
- * below), extract the tangent of the result. The tangent is the k-th column
134
- * of the Jacobian (in row-major-of-output-flattened form).
135
- *
136
- * Interop with the Tensor type: fn is typed `(x: Tensor) => Tensor`, so the
137
- * tracing requires fn's body to be implementable on either Tensor OR
138
- * DualTensor. v0.1.0 ships a documented "trace mode" — if fn uses only the
139
- * core ops that DualTensor implements (add/sub/mul/scale), the
140
- * implementation auto-traces by detecting DualTensor inputs via a thin
141
- * wrapper. The actual mechanism in v0.1.0: caller writes fn as if Tensor's
142
- * methods accept either Tensor or DualTensor (TypeScript's structural typing
143
- * makes this work for the implemented ops). v0.2.0 may broaden via decorator
144
- * instrumentation.
145
- *
146
- * For UPT v0.4.0's call-sites (metric functions in christoffel lowering),
147
- * fn uses only add/sub/mul/scale → safe.
10
+ * Tasks 6/7 populate this barrel. Task 5 scaffolds the package.
148
11
  *
149
12
  * @packageDocumentation
150
13
  */
151
-
152
- /**
153
- * Compute the value and full Jacobian of `fn` at point `x` using forward-mode
154
- * automatic differentiation (dual numbers).
155
- *
156
- * @param fn - Pure function operating on Tensors via add/sub/mul/scale.
157
- * Must be traceable: at runtime it receives a DualTensor (structurally
158
- * compatible with Tensor) and must return a DualTensor.
159
- * @param x - Input tensor at which to differentiate.
160
- * @returns `{ value, jacobian }` where:
161
- * - `value` has the output shape of fn applied to x
162
- * - `jacobian` has shape `[...value.shape, ...x.shape]` row-major.
163
- * `jacobian.data[kY * xSize + kX]` = ∂y[kY] / ∂x[kX].
164
- * Verified by Adam+Eve review 2026-05-15 (E13): kY outer, kX inner,
165
- * stride xSize between successive y-rows IS row-major [...y.shape, ...x.shape].
166
- */
167
- declare function forwardGrad(fn: (x: Tensor) => Tensor, x: Tensor): {
168
- value: Tensor;
169
- jacobian: Tensor;
170
- };
171
-
172
- /**
173
- * Tape — records the sequence of ops during a forward pass so we can
174
- * replay them in reverse to compute the vector-Jacobian product.
175
- *
176
- * Each TapedTensor wraps a primal Tensor + a tape-node id. Ops on
177
- * TapedTensors record their backward closures into a shared Tape.
178
- * After the forward pass, calling Tape.backward(outputGrad) walks the
179
- * tape in reverse, accumulating gradients into each input slot.
180
- *
181
- * v0.1.0 supports the same ops as DualTensor: add, sub, mul, scale.
182
- * v0.2.0 adds: contract (named-index), matmul (batched rank-N).
183
- */
184
-
185
- type BackwardFn = (outputGrad: Float64Array) => void;
186
- declare class Tape {
187
- private nodes;
188
- private inputGradSlots;
189
- private nextOpId;
190
- private nextInputId;
191
- /** Allocate a fresh id for an input or intermediate. */
192
- allocate(size: number): {
193
- id: number;
194
- gradSlot: Float64Array;
195
- };
196
- record(inputIds: ReadonlyArray<number>, outputSize: number, backward: BackwardFn): {
197
- id: number;
198
- gradSlot: Float64Array;
199
- };
200
- /** Seed the final output's gradient slot, then replay in reverse. */
201
- backward(outputId: number, outputGrad: Float64Array): void;
202
- getInputGrad(id: number): Float64Array | undefined;
203
- }
204
- declare class TapedTensor {
205
- readonly shape: ReadonlyArray<number>;
206
- readonly primal: Float64Array;
207
- readonly tape: Tape;
208
- readonly id: number;
209
- /**
210
- * Optional per-axis Index labels. When set, enables `contract`.
211
- * Must have the same length as `shape` when present.
212
- */
213
- readonly axisLabels?: ReadonlyArray<Index>;
214
- constructor(shape: ReadonlyArray<number>, primal: Float64Array, tape: Tape, id: number, axisLabels?: ReadonlyArray<Index>);
215
- /**
216
- * S5 fix: existing engine ops (e.g. lower, pderiv, contract) reach into
217
- * `.data`. The getter returns the primal so those ops still work when a
218
- * TapedTensor flows through them as a structurally-compatible Tensor.
219
- * (AD-aware ops branch on `'tape' in arg` before reaching here.)
220
- */
221
- get data(): Float64Array;
222
- static fromTensorAsInput(t: Tensor, tape: Tape): TapedTensor;
223
- toPrimalTensor(): Tensor;
224
- /**
225
- * Reconstruct the Tensor primal with the given axisLabels (used internally
226
- * when the primal was computed from an op that produces labelled output).
227
- */
228
- private toPrimalTensorWith;
229
- add(other: TapedTensor): TapedTensor;
230
- sub(other: TapedTensor): TapedTensor;
231
- /**
232
- * Elementwise division: this / other.
233
- *
234
- * Adjoints (quotient rule):
235
- * dA = dY / b
236
- * dB = −dY · a / b²
237
- *
238
- * The alias case (a.divide(a)) is handled explicitly: the combined gradient
239
- * is dA + dB = dY/a − dY·a/a² = dY/a − dY/a = 0. This is correct since
240
- * a/a = 1 everywhere and d(1)/da = 0.
241
- */
242
- divide(other: TapedTensor): TapedTensor;
243
- mul(other: TapedTensor): TapedTensor;
244
- scale(k: number): TapedTensor;
245
- /**
246
- * Reverse-mode AD over `Tensor.contract`.
247
- *
248
- * Both operands must carry `axisLabels`; the resulting TapedTensor inherits
249
- * the contracted-output axisLabels (non-shared axis concatenation).
250
- *
251
- * Adjoint derivation (T-notation):
252
- * Let Y = A.contract(B) (contraction over shared indices S).
253
- * dA = dY.contract(B') where B' = B with its free axes re-labelled to
254
- * match the shared positions in A. Equivalently: for each element of A,
255
- * dA[...a_free, ...s] = Σ_{...b_free} dY[...a_free, ...b_free] · B[...s, ...b_free]
256
- * dB = A'.contract(dY) symmetrically.
257
- *
258
- * Implementation: because `Tensor.contract` matches by Index identity, we
259
- * build the backward contraction by re-labelling the free axes of dY with
260
- * the Index objects from the other operand — so `Tensor.contract` automatically
261
- * finds the right shared axes.
262
- */
263
- contract(other: TapedTensor): TapedTensor;
264
- /**
265
- * Reverse-mode AD over batched matmul.
266
- *
267
- * Accepts operands of rank ≥ 2. Convention: trailing 2 axes are the matrix
268
- * dims (rows × cols); all leading axes are batch dims (must be broadcast-
269
- * compatible). For rank-2 inputs this is classical matrix multiplication.
270
- *
271
- * Adjoint derivation (classical matrix calc, extended to batched):
272
- * Y = A @ B (A: ...×m×k, B: ...×k×n → Y: ...×m×n)
273
- * dA = dY @ Bᵀ (dA: ...×m×k)
274
- * dB = Aᵀ @ dY (dB: ...×k×n)
275
- *
276
- * Implemented via `Tensor.einsum` with a dynamically-built spec:
277
- * forward: '...ik,...kj->...ij'
278
- * dA: '...ij,...kj->...ik' (contract dY with B on j)
279
- * dB: '...ki,...kj->...ij' (contract A on k with dY on k)
280
- */
281
- matmul(other: TapedTensor): TapedTensor;
282
- private checkSameShape;
283
- /**
284
- * Sum elements along the given axis/axes (or all axes if omitted).
285
- *
286
- * Adjoint: dX[...] = dY[reduced(idx)] broadcast back to input shape.
287
- * Each input element receives the output-gradient entry from its reduced
288
- * counterpart (the non-reduced coordinates select the dY element; the
289
- * reduced coordinates are collapsed to 0 in the keepDims=false case).
290
- */
291
- sum(axis?: number | ReadonlyArray<number>, opts?: {
292
- keepDims?: boolean;
293
- }): TapedTensor;
294
- /**
295
- * Arithmetic mean along the given axis/axes (or all axes if omitted).
296
- *
297
- * Adjoint: dX[...] = dY[reduced(idx)] / N, broadcast back to input shape.
298
- * N = product of reduced-axis dimensions.
299
- */
300
- mean(axis?: number | ReadonlyArray<number>, opts?: {
301
- keepDims?: boolean;
302
- }): TapedTensor;
303
- /**
304
- * Product of elements along the given axis/axes (or all axes if omitted).
305
- *
306
- * Adjoint: dX_i = dY * (prod_over_axes(x) / x_i) per element.
307
- *
308
- * Zero-element corners:
309
- * - Exactly one x_i = 0 in the reduced group: d/dx_i = product of all others
310
- * (which is the full product / x_i evaluated via alternate product),
311
- * and d/dx_j = 0 for all j ≠ i where x_j ≠ 0.
312
- * - Two or more zeros in the reduced group: gradient is 0 everywhere for that
313
- * group (because changing any single zero cannot change a product that is
314
- * zero due to another zero).
315
- *
316
- * Implementation: uses prefix/suffix products to handle zeros robustly.
317
- */
318
- prod(axis?: number | ReadonlyArray<number>, opts?: {
319
- keepDims?: boolean;
320
- }): TapedTensor;
321
- /**
322
- * Maximum value along the given axis/axes (or all axes if omitted).
323
- *
324
- * Adjoint: dY is scattered to the argmax position(s).
325
- * Tie-breaking: "first-wins" — the gradient flows to the first (smallest
326
- * flat-index) element among those that attain the maximum.
327
- */
328
- max(axis?: number | ReadonlyArray<number>, opts?: {
329
- keepDims?: boolean;
330
- }): TapedTensor;
331
- /**
332
- * Minimum value along the given axis/axes (or all axes if omitted).
333
- *
334
- * Adjoint: dY is scattered to the argmin position(s).
335
- * Tie-breaking: "first-wins" — gradient flows to the first (smallest
336
- * flat-index) element that attains the minimum.
337
- */
338
- min(axis?: number | ReadonlyArray<number>, opts?: {
339
- keepDims?: boolean;
340
- }): TapedTensor;
341
- /**
342
- * p-norm of the tensor.
343
- *
344
- * Supported p values: 1, 2, 'fro', 'inf'. Default p = 2.
345
- * When `opts.axis` is given, reduces along that axis; otherwise reduces all axes.
346
- *
347
- * Adjoints:
348
- * - p=2 / p='fro': dX = dY · x / ‖x‖₂ (Frobenius is the 2-norm of the flattened tensor)
349
- * - p=1: dX = dY · sign(x) (subgradient = 0 at exact zero)
350
- * - p='inf': dX scattered to the element(s) of max absolute value;
351
- * tie-breaking: first-wins. Sign of the scattered gradient
352
- * matches sign(x_max).
353
- */
354
- norm(opts?: {
355
- p?: 1 | 2 | 'fro' | 'inf';
356
- axis?: number;
357
- keepDims?: boolean;
358
- }): TapedTensor;
359
- /**
360
- * Elementwise natural logarithm.
361
- *
362
- * Adjoint: dX = dY / x
363
- */
364
- log(): TapedTensor;
365
- /**
366
- * Elementwise exponential.
367
- *
368
- * Adjoint: dX = dY · y where y = exp(x). Primal output is cached.
369
- */
370
- exp(): TapedTensor;
371
- /**
372
- * Elementwise sine.
373
- *
374
- * Adjoint: dX = dY · cos(x)
375
- */
376
- sin(): TapedTensor;
377
- /**
378
- * Elementwise cosine.
379
- *
380
- * Adjoint: dX = −dY · sin(x)
381
- */
382
- cos(): TapedTensor;
383
- /**
384
- * Elementwise tangent.
385
- *
386
- * Adjoint: dX = dY / cos²(x) (= dY · sec²(x))
387
- */
388
- tan(): TapedTensor;
389
- /**
390
- * Elementwise square root.
391
- *
392
- * Adjoint: dX = dY / (2·y) where y = sqrt(x). Primal output is cached.
393
- */
394
- sqrt(): TapedTensor;
395
- /**
396
- * Elementwise square (x²).
397
- *
398
- * Adjoint: dX = dY · 2x
399
- */
400
- square(): TapedTensor;
401
- /**
402
- * Elementwise power: x^k.
403
- *
404
- * `k` may be a fixed `number` (constant exponent) or a `TapedTensor`
405
- * (variable exponent, both inputs on the tape).
406
- *
407
- * Adjoints, for y = a^b:
408
- * - fixed exponent k: dA = dY · k · a^(k−1)
409
- * - variable exponent: dA = dY · b · a^(b−1), dB = dY · a^b · ln(a)
410
- * - aliased a^a: dA = dY · a^a · (ln a + 1)
411
- *
412
- * The dB term needs ln(a); for a ≤ 0 it is mathematically undefined and
413
- * yields NaN/−Inf, honestly signalling non-differentiability w.r.t. the
414
- * exponent rather than masking it.
415
- */
416
- pow(k: number | TapedTensor): TapedTensor;
417
- /**
418
- * Elementwise reciprocal: 1 / x.
419
- *
420
- * Adjoint: dX = −dY / x²
421
- */
422
- reciprocal(): TapedTensor;
423
- /**
424
- * Elementwise absolute value: |x|.
425
- *
426
- * Adjoint: dX = dY · sign(x)
427
- * Subgradient at exact zero is defined as 0 (rather than undefined).
428
- */
429
- abs(): TapedTensor;
430
- /**
431
- * Build a reverse-mode node for an elementwise unary op.
432
- * @param primalFn forward value y = f(x)
433
- * @param localGrad local derivative dy/dx, given x and the computed y
434
- */
435
- private _unaryElementwise;
436
- /** Hyperbolic sine. Adjoint: dX = dY · cosh(x). */
437
- sinh(): TapedTensor;
438
- /** Hyperbolic cosine. Adjoint: dX = dY · sinh(x). */
439
- cosh(): TapedTensor;
440
- /** Hyperbolic tangent. Adjoint: dX = dY · (1 − tanh²(x)) = dY · (1 − y²). */
441
- tanh(): TapedTensor;
442
- /** Arcsine. Adjoint: dX = dY / √(1 − x²). */
443
- asin(): TapedTensor;
444
- /** Arccosine. Adjoint: dX = −dY / √(1 − x²). */
445
- acos(): TapedTensor;
446
- /** Arctangent. Adjoint: dX = dY / (1 + x²). */
447
- atan(): TapedTensor;
448
- /** Inverse hyperbolic sine. Adjoint: dX = dY / √(x² + 1). */
449
- asinh(): TapedTensor;
450
- /** Inverse hyperbolic cosine (x ≥ 1). Adjoint: dX = dY / √(x² − 1). */
451
- acosh(): TapedTensor;
452
- /** Inverse hyperbolic tangent (|x| < 1). Adjoint: dX = dY / (1 − x²). */
453
- atanh(): TapedTensor;
454
- /** Base-2 logarithm. Adjoint: dX = dY / (x · ln2). */
455
- log2(): TapedTensor;
456
- /** Base-10 logarithm. Adjoint: dX = dY / (x · ln10). */
457
- log10(): TapedTensor;
458
- /** log(1 + x) (accurate near 0). Adjoint: dX = dY / (1 + x). */
459
- log1p(): TapedTensor;
460
- /** exp(x) − 1 (accurate near 0). Adjoint: dX = dY · eˣ = dY · (y + 1). */
461
- expm1(): TapedTensor;
462
- /** Cube root. Adjoint: dX = dY / (3 · y²) where y = x^(1/3) (→∞ at x = 0). */
463
- cbrt(): TapedTensor;
464
- /** Sign of x (−1/0/+1). Adjoint: 0 almost everywhere. */
465
- sign(): TapedTensor;
466
- /**
467
- * Two-argument arctangent: atan2(this, other), with this = y, other = x.
468
- *
469
- * Adjoints (a = this, b = other, d = a² + b²):
470
- * dA = dY · b / d
471
- * dB = −dY · a / d
472
- *
473
- * The aliased case atan2(a, a) is locally constant (±π/4 or ±3π/4), so its
474
- * gradient is 0 — produced naturally by contributing nothing when aliased
475
- * (dA + dB = dY · (b − a)/d = 0 at a = b).
476
- */
477
- atan2(other: TapedTensor): TapedTensor;
478
- /**
479
- * Reverse-mode AD over `Tensor.tensordot`.
480
- *
481
- * `axes[i] = [a, b]` contracts axis `a` of `this` with axis `b` of `other`.
482
- * The result shape is `this`'s non-contracted axes (in original order)
483
- * followed by `other`'s non-contracted axes (in original order).
484
- *
485
- * Adjoint derivation (NumPy/PyTorch tensordot backward, see Townsend 2016 §6,
486
- * and the canonical PyTorch implementation `TensorDotBackward0` in
487
- * `torch/csrc/autograd/generated/Functions.cpp`):
488
- *
489
- * Z = tensordot(A, B, axes)
490
- * dA = tensordot(dZ, B, [axes_of_dZ_corresponding_to_B's_free, B's_free])
491
- * then permute back into A's original axis order.
492
- * dB = tensordot(A, dZ, [A's_free, axes_of_dZ_corresponding_to_A's_free])
493
- * then permute back into B's original axis order.
494
- *
495
- * The axis-permutation bookkeeping is the trickiest part: Tensor.tensordot
496
- * produces output axes in the order [A's free axes (original A order),
497
- * B's free axes (original B order)] — and after the backward contractions
498
- * the survivors come out in B's original (resp. A's original) axis order
499
- * for the contracted side, which then needs to be permuted into pair order
500
- * (so axis k of the contracted block matches the kth pair) and finally
501
- * scattered back into A's (resp. B's) full original axis order.
502
- *
503
- * For the rank-2 × rank-2 single-axis case (i.e. ordinary matmul A·B with
504
- * axes = [[1, 0]]), this reduces to dA = dZ · Bᵀ, dB = Aᵀ · dZ — the same
505
- * adjoint as `TapedTensor.matmul`.
506
- */
507
- tensordot(other: TapedTensor, axes: ReadonlyArray<readonly [number, number]>): TapedTensor;
508
- /**
509
- * Reverse-mode AD over the full SVD of a rank-2 matrix.
510
- *
511
- * Forward: `A = U · diag(S) · Vt`, where for input shape [m, n], k = min(m, n):
512
- * - U has shape [m, k]
513
- * - S has shape [k]
514
- * - Vt has shape [k, n] (Vt is V^T in the standard A = U Σ V^T convention,
515
- * i.e. its rows are right-singular-vector components)
516
- *
517
- * Returned TapedTensors share a single backward closure. When backward()
518
- * runs, it pulls dU, dS, dV from each output's gradient slot, assembles
519
- * dA, and writes to the input's gradient slot.
520
- *
521
- * Adjoint (real, distinct nonzero singular values, m = n square case;
522
- * extended to rectangular below). Derived directly from the forward
523
- * Jacobian; equivalent to PyTorch's `svd_backward`
524
- * (`aten/src/ATen/native/BatchLinearAlgebra.cpp`) and Townsend (2016)
525
- * "Differentiating the Singular Value Decomposition" §3:
526
- *
527
- * Let α = skew(U^T · dU), β = skew(V^T · dV) (k×k, antisymmetric)
528
- * Build C (k×k):
529
- * C[i,i] = dS[i]
530
- * C[i,j] = (α[i,j] + β[i,j]) / (s_j − s_i)
531
- * + (α[i,j] − β[i,j]) / (s_j + s_i) for i ≠ j
532
- * dA_in = U · C · V^T (m×n in-subspace part)
533
- *
534
- * Rectangular correction (when m > k, i.e. m > n):
535
- * dA += (I − U U^T) · dU · diag(1/s) · V^T
536
- * Rectangular correction (when n > k, i.e. n > m):
537
- * dA += U · diag(1/s) · dV^T · (I − V V^T)
538
- *
539
- * Regularisation at repeated/near-zero singular values (PyTorch-equivalent
540
- * subgradient choice): the (i,j) entry of C is masked to 0 whenever
541
- * `|s_j − s_i| < REL_TOL · max(|s|)` (the "difference" denominator) or
542
- * `|s_j + s_i| < REL_TOL · max(|s|)` (the "sum" denominator, only relevant
543
- * when both are ~0). REL_TOL = 1e-10. This makes the gradient a subgradient
544
- * at exact degeneracy — finite, but not the unique true derivative (which
545
- * does not exist at degeneracies). The rectangular correction also masks
546
- * 1/s_i when |s_i| < REL_TOL · max(|s|).
547
- *
548
- * Throws if input is not rank-2. For rank > 2 inputs the user should
549
- * reshape first.
550
- */
551
- svd(): {
552
- U: TapedTensor;
553
- S: TapedTensor;
554
- V: TapedTensor;
555
- };
556
- /**
557
- * Reverse-mode AD over the eigendecomposition of a rank-2 matrix.
558
- *
559
- * Symmetric path (`symmetric: true`):
560
- * Forward: `A = U · diag(Λ) · U^T` for symmetric A (n×n).
561
- * Adjoint (Magnus & Neudecker 1999 §10.6.6; PyTorch `linalg_eigh_backward`):
562
- * F[i,j] = 1 / (λ_j − λ_i) for i ≠ j, 0 otherwise (with degeneracy mask)
563
- * dA_raw = U · (diag(dΛ) + F ∘ (U^T · dU)) · U^T
564
- * dA = (dA_raw + dA_raw^T) / 2 (symmetrise)
565
- *
566
- * Non-symmetric path (`symmetric: false`):
567
- * Forward: `A = V · diag(λ) · V^{-1}` (V columns are right eigenvectors).
568
- * Adjoint (Magnus & Neudecker 1999 §10.6 / Giles 2008 §3.2 / Townsend 2016 §4;
569
- * cross-check: PyTorch `linalg_eig_backward`):
570
- * E[i,j] = 1 / (λ_j − λ_i) for i ≠ j, 0 otherwise (with degeneracy mask)
571
- * dA = V^{-T} · ( E ∘ (V^T · dV) + diag(dλ) ) · V^T
572
- *
573
- * Restrictions (all enforced — throw a clear error otherwise):
574
- * 1. Eigenvalues must be real. The underlying matrix-eig primitive returns
575
- * placeholder eigenvectors (not actual complex vectors) when complex
576
- * eigenvalues arise, so the adjoint formula cannot be evaluated. Real-
577
- * Schur differentiation would require complex arithmetic infrastructure
578
- * throughout the Tape/TapedTensor stack, which is out of scope.
579
- * 2. A must be diagonalisable (non-defective). The adjoint assumes V is
580
- * invertible; defective inputs have algebraic > geometric multiplicity
581
- * so V is rank-deficient. Detected by cond_∞(V) > 1e14.
582
- *
583
- * Regularisation at repeated eigenvalues (subgradient choice): mask
584
- * F/E[i,j] = 0 when `|λ_i − λ_j| < REL_TOL · max(|λ|)`. REL_TOL = 1e-10.
585
- *
586
- * Throws if input is not rank-2 or square, if `symmetric` is missing, or
587
- * (non-symmetric path) on complex eigenvalues / defective input.
588
- */
589
- eig(opts: {
590
- symmetric: boolean;
591
- }): {
592
- eigvals: TapedTensor;
593
- eigvecs: TapedTensor;
594
- };
595
- private _eigSymmetric;
596
- private _eigGeneral;
597
- }
598
-
599
- /**
600
- * Reverse-mode AD via tape. Returns the value and the vector-Jacobian product
601
- * (gradient = ∂(cotangent · value) / ∂x). gradient.shape = x.shape.
602
- *
603
- * Default cotangent: ones-like(value) — required for scalar outputs, useful
604
- * for VJP defaults. For non-scalar value, cotangent's shape must match value.shape.
605
- */
606
-
607
- declare function reverseGrad(fn: (x: Tensor) => Tensor, x: Tensor, cotangent?: Tensor): {
608
- value: Tensor;
609
- gradient: Tensor;
610
- };
611
-
612
- /**
613
- * GC15 — ergonomic, number-friendly autodiff bridge.
614
- *
615
- * The low-level building blocks (`Tape`, `TapedTensor`, `reverseGrad`) require
616
- * manual tape management and `Tensor` marshalling. This is the JAX-style
617
- * convenience layer: write a function of a single `TapedTensor` (the AD-aware
618
- * surface — `.sin()`, `.mul()`, `.sum()`, `.matmul()`, … all record adjoints)
619
- * and differentiate it against plain JS numbers / arrays.
620
- *
621
- * grad((x) => x.sin())(1) // [cos(1)]
622
- * valueAndGrad((x) => x.pow(2).sum())([1, 2, 3]) // { value: 14, grad: [2,4,6] }
623
- * derivative((x) => x.sin().mul(x), 2) // sin(2) + 2cos(2)
624
- *
625
- * `f` must use TapedTensor operations (not the plain `functions/` ops, which are
626
- * not AD-aware). Differentiating arbitrary `functions/` ops would require
627
- * overloading them onto dual numbers / the tape — a separate, larger effort.
628
- */
629
-
630
- type ScalarFn = (x: TapedTensor) => TapedTensor;
631
- type NumericInput = number | readonly number[] | Float64Array;
632
- /**
633
- * Forward value and reverse-mode gradient of a scalar-valued function.
634
- *
635
- * @param fn maps a TapedTensor (shape `[n]`) to a scalar TapedTensor (length 1).
636
- * @param x the point — a number, number[], or Float64Array.
637
- * @returns `{ value, grad }` where `grad` has the same length as `x`.
638
- * @throws if `fn` does not return a length-1 (scalar) result.
639
- */
640
- declare function valueAndGrad(fn: ScalarFn, x: NumericInput): {
641
- value: number;
642
- grad: Float64Array;
643
- };
644
- /**
645
- * Gradient function of a scalar-valued function: `grad(f)(x)` returns ∇f(x).
646
- */
647
- declare function grad(fn: ScalarFn): (x: NumericInput) => Float64Array;
648
- /**
649
- * Scalar convenience: derivative of `f: ℝ → ℝ` at `x0`.
650
- */
651
- declare function derivative(fn: ScalarFn, x0: number): number;
652
- /**
653
- * Jacobian of a vector-valued function `f: ℝⁿ → ℝᵐ`, one reverse pass per output
654
- * row. Returns an `m × n` array. (For tall outputs forward-mode is cheaper; this
655
- * favours wide-input / few-output cases, the common ML gradient shape.)
656
- */
657
- declare function jacobian(fn: ScalarFn, x: NumericInput): number[][];
658
-
659
- export { DualTensor, type NumericInput, type ScalarFn, Tape, TapedTensor, derivative, forwardGrad, grad, jacobian, reverseGrad, valueAndGrad };
14
+ export { DualTensor } from './dual-tensor.js';
15
+ export { forwardGrad } from './forward-grad.js';
16
+ export { Tape, TapedTensor } from './tape.js';
17
+ export { reverseGrad } from './reverse-grad.js';
18
+ export { grad, valueAndGrad, derivative, jacobian } from './grad.js';
19
+ export type { ScalarFn, NumericInput } from './grad.js';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC"}