@danielsimonjr/mathts-autograd 0.1.3 → 0.3.0

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +170 -6
  2. package/dist/index.js +382 -28
  3. package/package.json +58 -58
package/dist/index.d.ts CHANGED
@@ -51,6 +51,68 @@ declare class DualTensor {
51
51
  mul(other: DualTensor): DualTensor;
52
52
  /** Scalar multiplication following dual-number rule: (k·a, k·a'). */
53
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
+ /** Exponential. f'(x) = eˣ = y. */
62
+ exp(): DualTensor;
63
+ /** Natural logarithm. f'(x) = 1/x. */
64
+ log(): DualTensor;
65
+ /** Sine. f'(x) = cos(x). */
66
+ sin(): DualTensor;
67
+ /** Cosine. f'(x) = −sin(x). */
68
+ cos(): DualTensor;
69
+ /** Tangent. f'(x) = 1/cos²(x). */
70
+ tan(): DualTensor;
71
+ /** Square root. f'(x) = 1/(2·y) where y = √x. */
72
+ sqrt(): DualTensor;
73
+ /** Square (x²). f'(x) = 2x. */
74
+ square(): DualTensor;
75
+ /** Reciprocal (1/x). f'(x) = −1/x². */
76
+ reciprocal(): DualTensor;
77
+ /** Absolute value. f'(x) = sign(x) (subgradient 0 at exact zero). */
78
+ abs(): DualTensor;
79
+ /** Fixed-exponent power x^k. f'(x) = k·x^(k−1). */
80
+ pow(k: number): DualTensor;
81
+ /** Hyperbolic sine. f'(x) = cosh(x). */
82
+ sinh(): DualTensor;
83
+ /** Hyperbolic cosine. f'(x) = sinh(x). */
84
+ cosh(): DualTensor;
85
+ /** Hyperbolic tangent. f'(x) = 1 − tanh²(x) = 1 − y². */
86
+ tanh(): DualTensor;
87
+ /** Arcsine. f'(x) = 1/√(1 − x²). */
88
+ asin(): DualTensor;
89
+ /** Arccosine. f'(x) = −1/√(1 − x²). */
90
+ acos(): DualTensor;
91
+ /** Arctangent. f'(x) = 1/(1 + x²). */
92
+ atan(): DualTensor;
93
+ /** Inverse hyperbolic sine. f'(x) = 1/√(x² + 1). */
94
+ asinh(): DualTensor;
95
+ /** Inverse hyperbolic cosine (x ≥ 1). f'(x) = 1/√(x² − 1). */
96
+ acosh(): DualTensor;
97
+ /** Inverse hyperbolic tangent (|x| < 1). f'(x) = 1/(1 − x²). */
98
+ atanh(): DualTensor;
99
+ /** Base-2 logarithm. f'(x) = 1/(x·ln2). */
100
+ log2(): DualTensor;
101
+ /** Base-10 logarithm. f'(x) = 1/(x·ln10). */
102
+ log10(): DualTensor;
103
+ /** log(1 + x). f'(x) = 1/(1 + x). */
104
+ log1p(): DualTensor;
105
+ /** exp(x) − 1. f'(x) = eˣ = y + 1. */
106
+ expm1(): DualTensor;
107
+ /** Cube root. f'(x) = 1/(3·y²) where y = x^(1/3) (→∞ at x = 0). */
108
+ cbrt(): DualTensor;
109
+ /** Sign (−1/0/+1). f'(x) = 0 almost everywhere. */
110
+ sign(): DualTensor;
111
+ /**
112
+ * Two-argument arctangent: atan2(this, other), this = y, other = x.
113
+ * Tangent: (b·a' − a·b') / (a² + b²), with a = this, b = other.
114
+ */
115
+ atan2(other: DualTensor): DualTensor;
54
116
  private checkSameShape;
55
117
  }
56
118
 
@@ -329,14 +391,21 @@ declare class TapedTensor {
329
391
  */
330
392
  square(): TapedTensor;
331
393
  /**
332
- * Elementwise fixed-exponent power: x^k.
394
+ * Elementwise power: x^k.
333
395
  *
334
- * Only fixed (non-TapedTensor) exponents are supported. Variable-exponent
335
- * pow(taped, taped) is a follow-up slice.
396
+ * `k` may be a fixed `number` (constant exponent) or a `TapedTensor`
397
+ * (variable exponent, both inputs on the tape).
336
398
  *
337
- * Adjoint: dX = dY · k · x^(k−1)
399
+ * Adjoints, for y = a^b:
400
+ * - fixed exponent k: dA = dY · k · a^(k−1)
401
+ * - variable exponent: dA = dY · b · a^(b−1), dB = dY · a^b · ln(a)
402
+ * - aliased a^a: dA = dY · a^a · (ln a + 1)
403
+ *
404
+ * The dB term needs ln(a); for a ≤ 0 it is mathematically undefined and
405
+ * yields NaN/−Inf, honestly signalling non-differentiability w.r.t. the
406
+ * exponent rather than masking it.
338
407
  */
339
- pow(k: number): TapedTensor;
408
+ pow(k: number | TapedTensor): TapedTensor;
340
409
  /**
341
410
  * Elementwise reciprocal: 1 / x.
342
411
  *
@@ -350,6 +419,54 @@ declare class TapedTensor {
350
419
  * Subgradient at exact zero is defined as 0 (rather than undefined).
351
420
  */
352
421
  abs(): TapedTensor;
422
+ /**
423
+ * Build a reverse-mode node for an elementwise unary op.
424
+ * @param primalFn forward value y = f(x)
425
+ * @param localGrad local derivative dy/dx, given x and the computed y
426
+ */
427
+ private _unaryElementwise;
428
+ /** Hyperbolic sine. Adjoint: dX = dY · cosh(x). */
429
+ sinh(): TapedTensor;
430
+ /** Hyperbolic cosine. Adjoint: dX = dY · sinh(x). */
431
+ cosh(): TapedTensor;
432
+ /** Hyperbolic tangent. Adjoint: dX = dY · (1 − tanh²(x)) = dY · (1 − y²). */
433
+ tanh(): TapedTensor;
434
+ /** Arcsine. Adjoint: dX = dY / √(1 − x²). */
435
+ asin(): TapedTensor;
436
+ /** Arccosine. Adjoint: dX = −dY / √(1 − x²). */
437
+ acos(): TapedTensor;
438
+ /** Arctangent. Adjoint: dX = dY / (1 + x²). */
439
+ atan(): TapedTensor;
440
+ /** Inverse hyperbolic sine. Adjoint: dX = dY / √(x² + 1). */
441
+ asinh(): TapedTensor;
442
+ /** Inverse hyperbolic cosine (x ≥ 1). Adjoint: dX = dY / √(x² − 1). */
443
+ acosh(): TapedTensor;
444
+ /** Inverse hyperbolic tangent (|x| < 1). Adjoint: dX = dY / (1 − x²). */
445
+ atanh(): TapedTensor;
446
+ /** Base-2 logarithm. Adjoint: dX = dY / (x · ln2). */
447
+ log2(): TapedTensor;
448
+ /** Base-10 logarithm. Adjoint: dX = dY / (x · ln10). */
449
+ log10(): TapedTensor;
450
+ /** log(1 + x) (accurate near 0). Adjoint: dX = dY / (1 + x). */
451
+ log1p(): TapedTensor;
452
+ /** exp(x) − 1 (accurate near 0). Adjoint: dX = dY · eˣ = dY · (y + 1). */
453
+ expm1(): TapedTensor;
454
+ /** Cube root. Adjoint: dX = dY / (3 · y²) where y = x^(1/3) (→∞ at x = 0). */
455
+ cbrt(): TapedTensor;
456
+ /** Sign of x (−1/0/+1). Adjoint: 0 almost everywhere. */
457
+ sign(): TapedTensor;
458
+ /**
459
+ * Two-argument arctangent: atan2(this, other), with this = y, other = x.
460
+ *
461
+ * Adjoints (a = this, b = other, d = a² + b²):
462
+ * dA = dY · b / d
463
+ * dB = −dY · a / d
464
+ *
465
+ * The aliased case atan2(a, a) is locally constant (±π/4 or ±3π/4), so its
466
+ * gradient is 0 — produced naturally by contributing nothing when aliased
467
+ * (dA + dB = dY · (b − a)/d = 0 at a = b).
468
+ */
469
+ atan2(other: TapedTensor): TapedTensor;
353
470
  /**
354
471
  * Reverse-mode AD over `Tensor.tensordot`.
355
472
  *
@@ -484,4 +601,51 @@ declare function reverseGrad(fn: (x: Tensor) => Tensor, x: Tensor, cotangent?: T
484
601
  gradient: Tensor;
485
602
  };
486
603
 
487
- export { DualTensor, Tape, TapedTensor, forwardGrad, reverseGrad };
604
+ /**
605
+ * GC15 — ergonomic, number-friendly autodiff bridge.
606
+ *
607
+ * The low-level building blocks (`Tape`, `TapedTensor`, `reverseGrad`) require
608
+ * manual tape management and `Tensor` marshalling. This is the JAX-style
609
+ * convenience layer: write a function of a single `TapedTensor` (the AD-aware
610
+ * surface — `.sin()`, `.mul()`, `.sum()`, `.matmul()`, … all record adjoints)
611
+ * and differentiate it against plain JS numbers / arrays.
612
+ *
613
+ * grad((x) => x.sin())(1) // [cos(1)]
614
+ * valueAndGrad((x) => x.pow(2).sum())([1, 2, 3]) // { value: 14, grad: [2,4,6] }
615
+ * derivative((x) => x.sin().mul(x), 2) // sin(2) + 2cos(2)
616
+ *
617
+ * `f` must use TapedTensor operations (not the plain `functions/` ops, which are
618
+ * not AD-aware). Differentiating arbitrary `functions/` ops would require
619
+ * overloading them onto dual numbers / the tape — a separate, larger effort.
620
+ */
621
+
622
+ type ScalarFn = (x: TapedTensor) => TapedTensor;
623
+ type NumericInput = number | readonly number[] | Float64Array;
624
+ /**
625
+ * Forward value and reverse-mode gradient of a scalar-valued function.
626
+ *
627
+ * @param fn maps a TapedTensor (shape `[n]`) to a scalar TapedTensor (length 1).
628
+ * @param x the point — a number, number[], or Float64Array.
629
+ * @returns `{ value, grad }` where `grad` has the same length as `x`.
630
+ * @throws if `fn` does not return a length-1 (scalar) result.
631
+ */
632
+ declare function valueAndGrad(fn: ScalarFn, x: NumericInput): {
633
+ value: number;
634
+ grad: Float64Array;
635
+ };
636
+ /**
637
+ * Gradient function of a scalar-valued function: `grad(f)(x)` returns ∇f(x).
638
+ */
639
+ declare function grad(fn: ScalarFn): (x: NumericInput) => Float64Array;
640
+ /**
641
+ * Scalar convenience: derivative of `f: ℝ → ℝ` at `x0`.
642
+ */
643
+ declare function derivative(fn: ScalarFn, x0: number): number;
644
+ /**
645
+ * Jacobian of a vector-valued function `f: ℝⁿ → ℝᵐ`, one reverse pass per output
646
+ * row. Returns an `m × n` array. (For tall outputs forward-mode is cheaper; this
647
+ * favours wide-input / few-output cases, the common ML gradient shape.)
648
+ */
649
+ declare function jacobian(fn: ScalarFn, x: NumericInput): number[][];
650
+
651
+ export { DualTensor, type NumericInput, type ScalarFn, Tape, TapedTensor, derivative, forwardGrad, grad, jacobian, reverseGrad, valueAndGrad };
package/dist/index.js CHANGED
@@ -98,6 +98,172 @@ var DualTensor = class _DualTensor {
98
98
  }
99
99
  return new _DualTensor(this.shape, p, t);
100
100
  }
101
+ /**
102
+ * Elementwise division following the dual-number quotient rule:
103
+ * (a, a') / (b, b') = (a/b, (a'·b − a·b') / b²).
104
+ * The aliased case a.divide(a) is (1, 0).
105
+ */
106
+ divide(other) {
107
+ this.checkSameShape(other, "divide");
108
+ const p = new Float64Array(this.primal.length);
109
+ const t = new Float64Array(this.tangent.length);
110
+ if (this === other) {
111
+ for (let i = 0; i < p.length; i++) {
112
+ p[i] = 1;
113
+ t[i] = 0;
114
+ }
115
+ } else {
116
+ for (let i = 0; i < p.length; i++) {
117
+ const a = this.primal[i];
118
+ const b = other.primal[i];
119
+ p[i] = a / b;
120
+ t[i] = (this.tangent[i] * b - a * other.tangent[i]) / (b * b);
121
+ }
122
+ }
123
+ return new _DualTensor(this.shape, p, t);
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Elementwise unary ops (parity with reverse-mode TapedTensor). Each follows
127
+ // the dual-number rule (a, a') ↦ (f(a), f'(a)·a'), built via the
128
+ // `_unaryElementwise` helper. `deriv(x, y)` is f'(x) given input x and the
129
+ // computed output y (so adjoints needing y — exp, tanh, sqrt — stay uniform).
130
+ // ---------------------------------------------------------------------------
131
+ _unaryElementwise(primalFn, deriv) {
132
+ const p = new Float64Array(this.primal.length);
133
+ const t = new Float64Array(this.tangent.length);
134
+ for (let i = 0; i < p.length; i++) {
135
+ const y = primalFn(this.primal[i]);
136
+ p[i] = y;
137
+ t[i] = deriv(this.primal[i], y) * this.tangent[i];
138
+ }
139
+ return new _DualTensor(this.shape, p, t);
140
+ }
141
+ /** Exponential. f'(x) = eˣ = y. */
142
+ exp() {
143
+ return this._unaryElementwise(Math.exp, (_x, y) => y);
144
+ }
145
+ /** Natural logarithm. f'(x) = 1/x. */
146
+ log() {
147
+ return this._unaryElementwise(Math.log, (x) => 1 / x);
148
+ }
149
+ /** Sine. f'(x) = cos(x). */
150
+ sin() {
151
+ return this._unaryElementwise(Math.sin, (x) => Math.cos(x));
152
+ }
153
+ /** Cosine. f'(x) = −sin(x). */
154
+ cos() {
155
+ return this._unaryElementwise(Math.cos, (x) => -Math.sin(x));
156
+ }
157
+ /** Tangent. f'(x) = 1/cos²(x). */
158
+ tan() {
159
+ return this._unaryElementwise(Math.tan, (x) => 1 / (Math.cos(x) * Math.cos(x)));
160
+ }
161
+ /** Square root. f'(x) = 1/(2·y) where y = √x. */
162
+ sqrt() {
163
+ return this._unaryElementwise(Math.sqrt, (_x, y) => 1 / (2 * y));
164
+ }
165
+ /** Square (x²). f'(x) = 2x. */
166
+ square() {
167
+ return this._unaryElementwise(
168
+ (x) => x * x,
169
+ (x) => 2 * x
170
+ );
171
+ }
172
+ /** Reciprocal (1/x). f'(x) = −1/x². */
173
+ reciprocal() {
174
+ return this._unaryElementwise(
175
+ (x) => 1 / x,
176
+ (x) => -1 / (x * x)
177
+ );
178
+ }
179
+ /** Absolute value. f'(x) = sign(x) (subgradient 0 at exact zero). */
180
+ abs() {
181
+ return this._unaryElementwise(Math.abs, (x) => Math.sign(x));
182
+ }
183
+ /** Fixed-exponent power x^k. f'(x) = k·x^(k−1). */
184
+ pow(k) {
185
+ return this._unaryElementwise(
186
+ (x) => Math.pow(x, k),
187
+ (x) => k * Math.pow(x, k - 1)
188
+ );
189
+ }
190
+ /** Hyperbolic sine. f'(x) = cosh(x). */
191
+ sinh() {
192
+ return this._unaryElementwise(Math.sinh, (x) => Math.cosh(x));
193
+ }
194
+ /** Hyperbolic cosine. f'(x) = sinh(x). */
195
+ cosh() {
196
+ return this._unaryElementwise(Math.cosh, (x) => Math.sinh(x));
197
+ }
198
+ /** Hyperbolic tangent. f'(x) = 1 − tanh²(x) = 1 − y². */
199
+ tanh() {
200
+ return this._unaryElementwise(Math.tanh, (_x, y) => 1 - y * y);
201
+ }
202
+ /** Arcsine. f'(x) = 1/√(1 − x²). */
203
+ asin() {
204
+ return this._unaryElementwise(Math.asin, (x) => 1 / Math.sqrt(1 - x * x));
205
+ }
206
+ /** Arccosine. f'(x) = −1/√(1 − x²). */
207
+ acos() {
208
+ return this._unaryElementwise(Math.acos, (x) => -1 / Math.sqrt(1 - x * x));
209
+ }
210
+ /** Arctangent. f'(x) = 1/(1 + x²). */
211
+ atan() {
212
+ return this._unaryElementwise(Math.atan, (x) => 1 / (1 + x * x));
213
+ }
214
+ /** Inverse hyperbolic sine. f'(x) = 1/√(x² + 1). */
215
+ asinh() {
216
+ return this._unaryElementwise(Math.asinh, (x) => 1 / Math.sqrt(x * x + 1));
217
+ }
218
+ /** Inverse hyperbolic cosine (x ≥ 1). f'(x) = 1/√(x² − 1). */
219
+ acosh() {
220
+ return this._unaryElementwise(Math.acosh, (x) => 1 / Math.sqrt(x * x - 1));
221
+ }
222
+ /** Inverse hyperbolic tangent (|x| < 1). f'(x) = 1/(1 − x²). */
223
+ atanh() {
224
+ return this._unaryElementwise(Math.atanh, (x) => 1 / (1 - x * x));
225
+ }
226
+ /** Base-2 logarithm. f'(x) = 1/(x·ln2). */
227
+ log2() {
228
+ return this._unaryElementwise(Math.log2, (x) => 1 / (x * Math.LN2));
229
+ }
230
+ /** Base-10 logarithm. f'(x) = 1/(x·ln10). */
231
+ log10() {
232
+ return this._unaryElementwise(Math.log10, (x) => 1 / (x * Math.LN10));
233
+ }
234
+ /** log(1 + x). f'(x) = 1/(1 + x). */
235
+ log1p() {
236
+ return this._unaryElementwise(Math.log1p, (x) => 1 / (1 + x));
237
+ }
238
+ /** exp(x) − 1. f'(x) = eˣ = y + 1. */
239
+ expm1() {
240
+ return this._unaryElementwise(Math.expm1, (_x, y) => y + 1);
241
+ }
242
+ /** Cube root. f'(x) = 1/(3·y²) where y = x^(1/3) (→∞ at x = 0). */
243
+ cbrt() {
244
+ return this._unaryElementwise(Math.cbrt, (_x, y) => 1 / (3 * y * y));
245
+ }
246
+ /** Sign (−1/0/+1). f'(x) = 0 almost everywhere. */
247
+ sign() {
248
+ return this._unaryElementwise(Math.sign, () => 0);
249
+ }
250
+ /**
251
+ * Two-argument arctangent: atan2(this, other), this = y, other = x.
252
+ * Tangent: (b·a' − a·b') / (a² + b²), with a = this, b = other.
253
+ */
254
+ atan2(other) {
255
+ this.checkSameShape(other, "atan2");
256
+ const p = new Float64Array(this.primal.length);
257
+ const t = new Float64Array(this.tangent.length);
258
+ for (let i = 0; i < p.length; i++) {
259
+ const a = this.primal[i];
260
+ const b = other.primal[i];
261
+ p[i] = Math.atan2(a, b);
262
+ const d = a * a + b * b;
263
+ t[i] = (b * this.tangent[i] - a * other.tangent[i]) / d;
264
+ }
265
+ return new _DualTensor(this.shape, p, t);
266
+ }
101
267
  checkSameShape(other, op) {
102
268
  if (this.shape.length !== other.shape.length || !this.shape.every((v, i) => v === other.shape[i])) {
103
269
  throw new Error(`DualTensor.${op}: shape mismatch [${this.shape}] vs [${other.shape}]`);
@@ -492,8 +658,8 @@ var TapedTensor = class _TapedTensor {
492
658
  const thisGradSlot = this.tape.getInputGrad(this.id);
493
659
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
494
660
  const outShape = [...outT.shape];
495
- const outStrides = _rowMajorStrides(outShape);
496
- const inStrides = _rowMajorStrides(inputShape);
661
+ const outStrides = Tensor3.rowMajorStrides(outShape);
662
+ const inStrides = Tensor3.rowMajorStrides(inputShape);
497
663
  const inCoords = new Array(inputShape.length).fill(0);
498
664
  for (let n = 0; n < inputSize; n++) {
499
665
  let outFlat = 0;
@@ -536,7 +702,7 @@ var TapedTensor = class _TapedTensor {
536
702
  const thisGradSlot = this.tape.getInputGrad(this.id);
537
703
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
538
704
  const outShape = [...outT.shape];
539
- const outStrides = _rowMajorStrides(outShape);
705
+ const outStrides = Tensor3.rowMajorStrides(outShape);
540
706
  const inCoords = new Array(inputShape.length).fill(0);
541
707
  for (let n = 0; n < inputSize; n++) {
542
708
  let outFlat = 0;
@@ -584,8 +750,8 @@ var TapedTensor = class _TapedTensor {
584
750
  const thisGradSlot = this.tape.getInputGrad(this.id);
585
751
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
586
752
  const outShape = [...outT.shape];
587
- const outStrides = _rowMajorStrides(outShape);
588
- const inStrides = _rowMajorStrides(inputShape);
753
+ const outStrides = Tensor3.rowMajorStrides(outShape);
754
+ const inStrides = Tensor3.rowMajorStrides(inputShape);
589
755
  const groups = /* @__PURE__ */ new Map();
590
756
  const inCoords = new Array(inputShape.length).fill(0);
591
757
  for (let n = 0; n < primalData.length; n++) {
@@ -649,7 +815,7 @@ var TapedTensor = class _TapedTensor {
649
815
  const thisGradSlot = this.tape.getInputGrad(this.id);
650
816
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
651
817
  const outShape = [...outT.shape];
652
- const outStrides = _rowMajorStrides(outShape);
818
+ const outStrides = Tensor3.rowMajorStrides(outShape);
653
819
  const argmaxSlot = new Int32Array(outT.data.length).fill(-1);
654
820
  const inCoords = new Array(inputShape.length).fill(0);
655
821
  for (let n = 0; n < primalData.length; n++) {
@@ -698,7 +864,7 @@ var TapedTensor = class _TapedTensor {
698
864
  const thisGradSlot = this.tape.getInputGrad(this.id);
699
865
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
700
866
  const outShape = [...outT.shape];
701
- const outStrides = _rowMajorStrides(outShape);
867
+ const outStrides = Tensor3.rowMajorStrides(outShape);
702
868
  const argminSlot = new Int32Array(outT.data.length).fill(-1);
703
869
  const inCoords = new Array(inputShape.length).fill(0);
704
870
  for (let n = 0; n < primalData.length; n++) {
@@ -756,7 +922,7 @@ var TapedTensor = class _TapedTensor {
756
922
  const normData = new Float64Array(outT.data);
757
923
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
758
924
  const outShape = [...outT.shape];
759
- const outStrides = _rowMajorStrides(outShape);
925
+ const outStrides = Tensor3.rowMajorStrides(outShape);
760
926
  const inCoords = new Array(inputShape.length).fill(0);
761
927
  for (let n = 0; n < primalData.length; n++) {
762
928
  let outFlat = 0;
@@ -784,7 +950,7 @@ var TapedTensor = class _TapedTensor {
784
950
  if (p === 1) {
785
951
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
786
952
  const outShape = [...outT.shape];
787
- const outStrides = _rowMajorStrides(outShape);
953
+ const outStrides = Tensor3.rowMajorStrides(outShape);
788
954
  const inCoords = new Array(inputShape.length).fill(0);
789
955
  for (let n = 0; n < primalData.length; n++) {
790
956
  let outFlat = 0;
@@ -811,7 +977,7 @@ var TapedTensor = class _TapedTensor {
811
977
  {
812
978
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
813
979
  const outShape = [...outT.shape];
814
- const outStrides = _rowMajorStrides(outShape);
980
+ const outStrides = Tensor3.rowMajorStrides(outShape);
815
981
  const argmaxAbsSlot = new Int32Array(outT.data.length).fill(-1);
816
982
  const inCoords = new Array(inputShape.length).fill(0);
817
983
  for (let n = 0; n < primalData.length; n++) {
@@ -979,21 +1145,55 @@ var TapedTensor = class _TapedTensor {
979
1145
  return new _TapedTensor(this.shape, out, this.tape, id);
980
1146
  }
981
1147
  /**
982
- * Elementwise fixed-exponent power: x^k.
1148
+ * Elementwise power: x^k.
983
1149
  *
984
- * Only fixed (non-TapedTensor) exponents are supported. Variable-exponent
985
- * pow(taped, taped) is a follow-up slice.
1150
+ * `k` may be a fixed `number` (constant exponent) or a `TapedTensor`
1151
+ * (variable exponent, both inputs on the tape).
986
1152
  *
987
- * Adjoint: dX = dY · k · x^(k−1)
1153
+ * Adjoints, for y = a^b:
1154
+ * - fixed exponent k: dA = dY · k · a^(k−1)
1155
+ * - variable exponent: dA = dY · b · a^(b−1), dB = dY · a^b · ln(a)
1156
+ * - aliased a^a: dA = dY · a^a · (ln a + 1)
1157
+ *
1158
+ * The dB term needs ln(a); for a ≤ 0 it is mathematically undefined and
1159
+ * yields NaN/−Inf, honestly signalling non-differentiability w.r.t. the
1160
+ * exponent rather than masking it.
988
1161
  */
989
1162
  pow(k) {
1163
+ if (typeof k === "number") {
1164
+ const out2 = new Float64Array(this.primal.length);
1165
+ for (let i = 0; i < out2.length; i++) out2[i] = Math.pow(this.primal[i], k);
1166
+ const primalData = new Float64Array(this.primal);
1167
+ const thisGradSlot2 = this.tape.getInputGrad(this.id);
1168
+ const { id: id2 } = this.tape.record([this.id], out2.length, (outputGrad) => {
1169
+ for (let i = 0; i < outputGrad.length; i++) {
1170
+ thisGradSlot2[i] += outputGrad[i] * k * Math.pow(primalData[i], k - 1);
1171
+ }
1172
+ });
1173
+ return new _TapedTensor(this.shape, out2, this.tape, id2);
1174
+ }
1175
+ const other = k;
1176
+ this.checkSameShape(other, "pow");
990
1177
  const out = new Float64Array(this.primal.length);
991
- for (let i = 0; i < out.length; i++) out[i] = Math.pow(this.primal[i], k);
992
- const primalData = new Float64Array(this.primal);
1178
+ for (let i = 0; i < out.length; i++) out[i] = Math.pow(this.primal[i], other.primal[i]);
1179
+ const aPrimal = new Float64Array(this.primal);
1180
+ const bPrimal = new Float64Array(other.primal);
1181
+ const yPrimal = new Float64Array(out);
993
1182
  const thisGradSlot = this.tape.getInputGrad(this.id);
994
- const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
1183
+ const otherGradSlot = this.tape.getInputGrad(other.id);
1184
+ const isAliased = this === other;
1185
+ const { id } = this.tape.record([this.id, other.id], out.length, (outputGrad) => {
995
1186
  for (let i = 0; i < outputGrad.length; i++) {
996
- thisGradSlot[i] += outputGrad[i] * k * Math.pow(primalData[i], k - 1);
1187
+ const a = aPrimal[i];
1188
+ const b = bPrimal[i];
1189
+ const y = yPrimal[i];
1190
+ const g = outputGrad[i];
1191
+ if (isAliased) {
1192
+ thisGradSlot[i] += g * y * (Math.log(a) + 1);
1193
+ } else {
1194
+ thisGradSlot[i] += g * b * Math.pow(a, b - 1);
1195
+ otherGradSlot[i] += g * y * Math.log(a);
1196
+ }
997
1197
  }
998
1198
  });
999
1199
  return new _TapedTensor(this.shape, out, this.tape, id);
@@ -1037,6 +1237,121 @@ var TapedTensor = class _TapedTensor {
1037
1237
  return new _TapedTensor(this.shape, out, this.tape, id);
1038
1238
  }
1039
1239
  // ---------------------------------------------------------------------------
1240
+ // Extended elementwise transcendentals (hyperbolic, inverse-trig, log/exp
1241
+ // variants, sign, cbrt, atan2). Each builds a reverse-mode node via the
1242
+ // `_unaryElementwise` helper; `localGrad(x, y)` is dy/dx given input x and
1243
+ // output y. The helper caches both x and y so adjoints that need either
1244
+ // (e.g. tanh needs y, log needs x) are uniform.
1245
+ // ---------------------------------------------------------------------------
1246
+ /**
1247
+ * Build a reverse-mode node for an elementwise unary op.
1248
+ * @param primalFn forward value y = f(x)
1249
+ * @param localGrad local derivative dy/dx, given x and the computed y
1250
+ */
1251
+ _unaryElementwise(primalFn, localGrad) {
1252
+ const xData = new Float64Array(this.primal);
1253
+ const out = new Float64Array(xData.length);
1254
+ for (let i = 0; i < out.length; i++) out[i] = primalFn(xData[i]);
1255
+ const yData = new Float64Array(out);
1256
+ const thisGradSlot = this.tape.getInputGrad(this.id);
1257
+ const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
1258
+ for (let i = 0; i < outputGrad.length; i++) {
1259
+ thisGradSlot[i] += outputGrad[i] * localGrad(xData[i], yData[i]);
1260
+ }
1261
+ });
1262
+ return new _TapedTensor(this.shape, out, this.tape, id);
1263
+ }
1264
+ /** Hyperbolic sine. Adjoint: dX = dY · cosh(x). */
1265
+ sinh() {
1266
+ return this._unaryElementwise(Math.sinh, (x) => Math.cosh(x));
1267
+ }
1268
+ /** Hyperbolic cosine. Adjoint: dX = dY · sinh(x). */
1269
+ cosh() {
1270
+ return this._unaryElementwise(Math.cosh, (x) => Math.sinh(x));
1271
+ }
1272
+ /** Hyperbolic tangent. Adjoint: dX = dY · (1 − tanh²(x)) = dY · (1 − y²). */
1273
+ tanh() {
1274
+ return this._unaryElementwise(Math.tanh, (_x, y) => 1 - y * y);
1275
+ }
1276
+ /** Arcsine. Adjoint: dX = dY / √(1 − x²). */
1277
+ asin() {
1278
+ return this._unaryElementwise(Math.asin, (x) => 1 / Math.sqrt(1 - x * x));
1279
+ }
1280
+ /** Arccosine. Adjoint: dX = −dY / √(1 − x²). */
1281
+ acos() {
1282
+ return this._unaryElementwise(Math.acos, (x) => -1 / Math.sqrt(1 - x * x));
1283
+ }
1284
+ /** Arctangent. Adjoint: dX = dY / (1 + x²). */
1285
+ atan() {
1286
+ return this._unaryElementwise(Math.atan, (x) => 1 / (1 + x * x));
1287
+ }
1288
+ /** Inverse hyperbolic sine. Adjoint: dX = dY / √(x² + 1). */
1289
+ asinh() {
1290
+ return this._unaryElementwise(Math.asinh, (x) => 1 / Math.sqrt(x * x + 1));
1291
+ }
1292
+ /** Inverse hyperbolic cosine (x ≥ 1). Adjoint: dX = dY / √(x² − 1). */
1293
+ acosh() {
1294
+ return this._unaryElementwise(Math.acosh, (x) => 1 / Math.sqrt(x * x - 1));
1295
+ }
1296
+ /** Inverse hyperbolic tangent (|x| < 1). Adjoint: dX = dY / (1 − x²). */
1297
+ atanh() {
1298
+ return this._unaryElementwise(Math.atanh, (x) => 1 / (1 - x * x));
1299
+ }
1300
+ /** Base-2 logarithm. Adjoint: dX = dY / (x · ln2). */
1301
+ log2() {
1302
+ return this._unaryElementwise(Math.log2, (x) => 1 / (x * Math.LN2));
1303
+ }
1304
+ /** Base-10 logarithm. Adjoint: dX = dY / (x · ln10). */
1305
+ log10() {
1306
+ return this._unaryElementwise(Math.log10, (x) => 1 / (x * Math.LN10));
1307
+ }
1308
+ /** log(1 + x) (accurate near 0). Adjoint: dX = dY / (1 + x). */
1309
+ log1p() {
1310
+ return this._unaryElementwise(Math.log1p, (x) => 1 / (1 + x));
1311
+ }
1312
+ /** exp(x) − 1 (accurate near 0). Adjoint: dX = dY · eˣ = dY · (y + 1). */
1313
+ expm1() {
1314
+ return this._unaryElementwise(Math.expm1, (_x, y) => y + 1);
1315
+ }
1316
+ /** Cube root. Adjoint: dX = dY / (3 · y²) where y = x^(1/3) (→∞ at x = 0). */
1317
+ cbrt() {
1318
+ return this._unaryElementwise(Math.cbrt, (_x, y) => 1 / (3 * y * y));
1319
+ }
1320
+ /** Sign of x (−1/0/+1). Adjoint: 0 almost everywhere. */
1321
+ sign() {
1322
+ return this._unaryElementwise(Math.sign, () => 0);
1323
+ }
1324
+ /**
1325
+ * Two-argument arctangent: atan2(this, other), with this = y, other = x.
1326
+ *
1327
+ * Adjoints (a = this, b = other, d = a² + b²):
1328
+ * dA = dY · b / d
1329
+ * dB = −dY · a / d
1330
+ *
1331
+ * The aliased case atan2(a, a) is locally constant (±π/4 or ±3π/4), so its
1332
+ * gradient is 0 — produced naturally by contributing nothing when aliased
1333
+ * (dA + dB = dY · (b − a)/d = 0 at a = b).
1334
+ */
1335
+ atan2(other) {
1336
+ this.checkSameShape(other, "atan2");
1337
+ const a = new Float64Array(this.primal);
1338
+ const b = new Float64Array(other.primal);
1339
+ const out = new Float64Array(a.length);
1340
+ for (let i = 0; i < out.length; i++) out[i] = Math.atan2(a[i], b[i]);
1341
+ const thisGradSlot = this.tape.getInputGrad(this.id);
1342
+ const otherGradSlot = this.tape.getInputGrad(other.id);
1343
+ const isAliased = this === other;
1344
+ const { id } = this.tape.record([this.id, other.id], out.length, (outputGrad) => {
1345
+ if (isAliased) return;
1346
+ for (let i = 0; i < outputGrad.length; i++) {
1347
+ const d = a[i] * a[i] + b[i] * b[i];
1348
+ thisGradSlot[i] += outputGrad[i] * b[i] / d;
1349
+ otherGradSlot[i] += -outputGrad[i] * a[i] / d;
1350
+ }
1351
+ });
1352
+ return new _TapedTensor(this.shape, out, this.tape, id);
1353
+ }
1354
+ // ---------------------------------------------------------------------------
1040
1355
  // tensordot — generalised dot product over explicit axis pairs.
1041
1356
  // ---------------------------------------------------------------------------
1042
1357
  /**
@@ -1658,15 +1973,6 @@ function _resolveAxes(axis, rank) {
1658
1973
  }
1659
1974
  return [...axis].sort((a, b) => a - b);
1660
1975
  }
1661
- function _rowMajorStrides(shape) {
1662
- const strides = new Array(shape.length);
1663
- let acc = 1;
1664
- for (let k = shape.length - 1; k >= 0; k--) {
1665
- strides[k] = acc;
1666
- acc *= shape[k];
1667
- }
1668
- return strides;
1669
- }
1670
1976
  function _scatterToOriginalAxes(tensor, targetLabels) {
1671
1977
  const currentLabels = tensor.axisLabels;
1672
1978
  const rank = targetLabels.length;
@@ -1795,10 +2101,58 @@ function reverseGrad(fn, x, cotangent) {
1795
2101
  gradient: new Tensor4(x.shape, new Float64Array(xGradSlot))
1796
2102
  };
1797
2103
  }
2104
+
2105
+ // src/grad.ts
2106
+ function toFloat64(x) {
2107
+ if (typeof x === "number") return Float64Array.of(x);
2108
+ return x instanceof Float64Array ? new Float64Array(x) : Float64Array.from(x);
2109
+ }
2110
+ function valueAndGrad(fn, x) {
2111
+ const xs = toFloat64(x);
2112
+ const tape = new Tape();
2113
+ const { id } = tape.allocate(xs.length);
2114
+ const leaf = new TapedTensor([xs.length], xs, tape, id);
2115
+ const out = fn(leaf);
2116
+ if (out.primal.length !== 1) {
2117
+ throw new Error(
2118
+ `valueAndGrad: fn must return a scalar (length 1); got length ${out.primal.length}. Reduce with .sum()/.mean(), or use jacobian() for vector outputs.`
2119
+ );
2120
+ }
2121
+ tape.backward(out.id, Float64Array.of(1));
2122
+ return { value: out.primal[0], grad: new Float64Array(tape.getInputGrad(id)) };
2123
+ }
2124
+ function grad(fn) {
2125
+ return (x) => valueAndGrad(fn, x).grad;
2126
+ }
2127
+ function derivative(fn, x0) {
2128
+ return valueAndGrad(fn, x0).grad[0];
2129
+ }
2130
+ function jacobian(fn, x) {
2131
+ const xs = toFloat64(x);
2132
+ const probeTape = new Tape();
2133
+ const { id: probeId } = probeTape.allocate(xs.length);
2134
+ const probeOut = fn(new TapedTensor([xs.length], new Float64Array(xs), probeTape, probeId));
2135
+ const m = probeOut.primal.length;
2136
+ const rows = [];
2137
+ for (let r = 0; r < m; r++) {
2138
+ const tape = new Tape();
2139
+ const { id } = tape.allocate(xs.length);
2140
+ const out = fn(new TapedTensor([xs.length], new Float64Array(xs), tape, id));
2141
+ const seed = new Float64Array(m);
2142
+ seed[r] = 1;
2143
+ tape.backward(out.id, seed);
2144
+ rows.push(Array.from(tape.getInputGrad(id)));
2145
+ }
2146
+ return rows;
2147
+ }
1798
2148
  export {
1799
2149
  DualTensor,
1800
2150
  Tape,
1801
2151
  TapedTensor,
2152
+ derivative,
1802
2153
  forwardGrad,
1803
- reverseGrad
2154
+ grad,
2155
+ jacobian,
2156
+ reverseGrad,
2157
+ valueAndGrad
1804
2158
  };
package/package.json CHANGED
@@ -1,58 +1,58 @@
1
- {
2
- "name": "@danielsimonjr/mathts-autograd",
3
- "version": "0.1.3",
4
- "description": "Forward + reverse-mode automatic differentiation for MathTS rank-N Tensor",
5
- "author": "Daniel Simon Jr.",
6
- "license": "MIT",
7
- "type": "module",
8
- "main": "./dist/index.js",
9
- "module": "./dist/index.js",
10
- "types": "./dist/index.d.ts",
11
- "exports": {
12
- ".": {
13
- "import": "./dist/index.js",
14
- "types": "./dist/index.d.ts"
15
- }
16
- },
17
- "files": [
18
- "dist",
19
- "README.md"
20
- ],
21
- "scripts": {
22
- "build": "tsup src/index.ts --format esm --dts --clean",
23
- "dev": "tsup src/index.ts --format esm --dts --watch",
24
- "test": "vitest run",
25
- "test:watch": "vitest",
26
- "test:coverage": "vitest run --coverage",
27
- "typecheck": "tsc --noEmit",
28
- "lint": "eslint src --ext .ts",
29
- "lint:fix": "eslint src --ext .ts --fix",
30
- "clean": "rm -rf dist",
31
- "build:prod": "tsup src/index.ts --format esm --dts --clean --minify --treeshake"
32
- },
33
- "dependencies": {
34
- "@danielsimonjr/mathts-core": "^0.1.4",
35
- "@danielsimonjr/mathts-tensor": "^0.1.3"
36
- },
37
- "devDependencies": {
38
- "@types/node": "^25.5.2",
39
- "tsup": "^8.0.0",
40
- "typescript": "^5.3.0",
41
- "vitest": "^4.1.5"
42
- },
43
- "publishConfig": {
44
- "access": "public"
45
- },
46
- "repository": {
47
- "type": "git",
48
- "url": "https://github.com/danielsimonjr/mathts",
49
- "directory": "autograd"
50
- },
51
- "keywords": [
52
- "math",
53
- "autograd",
54
- "automatic-differentiation",
55
- "tensor",
56
- "typescript"
57
- ]
58
- }
1
+ {
2
+ "name": "@danielsimonjr/mathts-autograd",
3
+ "version": "0.3.0",
4
+ "description": "Forward + reverse-mode automatic differentiation for MathTS rank-N Tensor",
5
+ "author": "Daniel Simon Jr.",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup src/index.ts --format esm --dts --clean",
23
+ "dev": "tsup src/index.ts --format esm --dts --watch",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "eslint src --ext .ts",
29
+ "lint:fix": "eslint src --ext .ts --fix",
30
+ "clean": "rm -rf dist",
31
+ "build:prod": "tsup src/index.ts --format esm --dts --clean --minify --treeshake"
32
+ },
33
+ "dependencies": {
34
+ "@danielsimonjr/mathts-core": "^0.1.4",
35
+ "@danielsimonjr/mathts-tensor": "^0.2.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^25.5.2",
39
+ "tsup": "^8.0.0",
40
+ "typescript": "^5.3.0",
41
+ "vitest": "^4.1.5"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/danielsimonjr/mathts",
49
+ "directory": "autograd"
50
+ },
51
+ "keywords": [
52
+ "math",
53
+ "autograd",
54
+ "automatic-differentiation",
55
+ "tensor",
56
+ "typescript"
57
+ ]
58
+ }