@danielsimonjr/mathts-autograd 0.2.0 → 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.
package/dist/index.d.ts CHANGED
@@ -391,14 +391,21 @@ declare class TapedTensor {
391
391
  */
392
392
  square(): TapedTensor;
393
393
  /**
394
- * Elementwise fixed-exponent power: x^k.
394
+ * Elementwise power: x^k.
395
395
  *
396
- * Only fixed (non-TapedTensor) exponents are supported. Variable-exponent
397
- * 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).
398
398
  *
399
- * 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.
400
407
  */
401
- pow(k: number): TapedTensor;
408
+ pow(k: number | TapedTensor): TapedTensor;
402
409
  /**
403
410
  * Elementwise reciprocal: 1 / x.
404
411
  *
@@ -594,4 +601,51 @@ declare function reverseGrad(fn: (x: Tensor) => Tensor, x: Tensor, cotangent?: T
594
601
  gradient: Tensor;
595
602
  };
596
603
 
597
- 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
@@ -658,8 +658,8 @@ var TapedTensor = class _TapedTensor {
658
658
  const thisGradSlot = this.tape.getInputGrad(this.id);
659
659
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
660
660
  const outShape = [...outT.shape];
661
- const outStrides = _rowMajorStrides(outShape);
662
- const inStrides = _rowMajorStrides(inputShape);
661
+ const outStrides = Tensor3.rowMajorStrides(outShape);
662
+ const inStrides = Tensor3.rowMajorStrides(inputShape);
663
663
  const inCoords = new Array(inputShape.length).fill(0);
664
664
  for (let n = 0; n < inputSize; n++) {
665
665
  let outFlat = 0;
@@ -702,7 +702,7 @@ var TapedTensor = class _TapedTensor {
702
702
  const thisGradSlot = this.tape.getInputGrad(this.id);
703
703
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
704
704
  const outShape = [...outT.shape];
705
- const outStrides = _rowMajorStrides(outShape);
705
+ const outStrides = Tensor3.rowMajorStrides(outShape);
706
706
  const inCoords = new Array(inputShape.length).fill(0);
707
707
  for (let n = 0; n < inputSize; n++) {
708
708
  let outFlat = 0;
@@ -750,8 +750,8 @@ var TapedTensor = class _TapedTensor {
750
750
  const thisGradSlot = this.tape.getInputGrad(this.id);
751
751
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
752
752
  const outShape = [...outT.shape];
753
- const outStrides = _rowMajorStrides(outShape);
754
- const inStrides = _rowMajorStrides(inputShape);
753
+ const outStrides = Tensor3.rowMajorStrides(outShape);
754
+ const inStrides = Tensor3.rowMajorStrides(inputShape);
755
755
  const groups = /* @__PURE__ */ new Map();
756
756
  const inCoords = new Array(inputShape.length).fill(0);
757
757
  for (let n = 0; n < primalData.length; n++) {
@@ -815,7 +815,7 @@ var TapedTensor = class _TapedTensor {
815
815
  const thisGradSlot = this.tape.getInputGrad(this.id);
816
816
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
817
817
  const outShape = [...outT.shape];
818
- const outStrides = _rowMajorStrides(outShape);
818
+ const outStrides = Tensor3.rowMajorStrides(outShape);
819
819
  const argmaxSlot = new Int32Array(outT.data.length).fill(-1);
820
820
  const inCoords = new Array(inputShape.length).fill(0);
821
821
  for (let n = 0; n < primalData.length; n++) {
@@ -864,7 +864,7 @@ var TapedTensor = class _TapedTensor {
864
864
  const thisGradSlot = this.tape.getInputGrad(this.id);
865
865
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
866
866
  const outShape = [...outT.shape];
867
- const outStrides = _rowMajorStrides(outShape);
867
+ const outStrides = Tensor3.rowMajorStrides(outShape);
868
868
  const argminSlot = new Int32Array(outT.data.length).fill(-1);
869
869
  const inCoords = new Array(inputShape.length).fill(0);
870
870
  for (let n = 0; n < primalData.length; n++) {
@@ -922,7 +922,7 @@ var TapedTensor = class _TapedTensor {
922
922
  const normData = new Float64Array(outT.data);
923
923
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
924
924
  const outShape = [...outT.shape];
925
- const outStrides = _rowMajorStrides(outShape);
925
+ const outStrides = Tensor3.rowMajorStrides(outShape);
926
926
  const inCoords = new Array(inputShape.length).fill(0);
927
927
  for (let n = 0; n < primalData.length; n++) {
928
928
  let outFlat = 0;
@@ -950,7 +950,7 @@ var TapedTensor = class _TapedTensor {
950
950
  if (p === 1) {
951
951
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
952
952
  const outShape = [...outT.shape];
953
- const outStrides = _rowMajorStrides(outShape);
953
+ const outStrides = Tensor3.rowMajorStrides(outShape);
954
954
  const inCoords = new Array(inputShape.length).fill(0);
955
955
  for (let n = 0; n < primalData.length; n++) {
956
956
  let outFlat = 0;
@@ -977,7 +977,7 @@ var TapedTensor = class _TapedTensor {
977
977
  {
978
978
  const { id } = this.tape.record([this.id], outT.data.length, (outputGrad) => {
979
979
  const outShape = [...outT.shape];
980
- const outStrides = _rowMajorStrides(outShape);
980
+ const outStrides = Tensor3.rowMajorStrides(outShape);
981
981
  const argmaxAbsSlot = new Int32Array(outT.data.length).fill(-1);
982
982
  const inCoords = new Array(inputShape.length).fill(0);
983
983
  for (let n = 0; n < primalData.length; n++) {
@@ -1145,21 +1145,55 @@ var TapedTensor = class _TapedTensor {
1145
1145
  return new _TapedTensor(this.shape, out, this.tape, id);
1146
1146
  }
1147
1147
  /**
1148
- * Elementwise fixed-exponent power: x^k.
1148
+ * Elementwise power: x^k.
1149
1149
  *
1150
- * Only fixed (non-TapedTensor) exponents are supported. Variable-exponent
1151
- * 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).
1152
1152
  *
1153
- * 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.
1154
1161
  */
1155
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");
1156
1177
  const out = new Float64Array(this.primal.length);
1157
- for (let i = 0; i < out.length; i++) out[i] = Math.pow(this.primal[i], k);
1158
- 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);
1159
1182
  const thisGradSlot = this.tape.getInputGrad(this.id);
1160
- 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) => {
1161
1186
  for (let i = 0; i < outputGrad.length; i++) {
1162
- 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
+ }
1163
1197
  }
1164
1198
  });
1165
1199
  return new _TapedTensor(this.shape, out, this.tape, id);
@@ -1939,15 +1973,6 @@ function _resolveAxes(axis, rank) {
1939
1973
  }
1940
1974
  return [...axis].sort((a, b) => a - b);
1941
1975
  }
1942
- function _rowMajorStrides(shape) {
1943
- const strides = new Array(shape.length);
1944
- let acc = 1;
1945
- for (let k = shape.length - 1; k >= 0; k--) {
1946
- strides[k] = acc;
1947
- acc *= shape[k];
1948
- }
1949
- return strides;
1950
- }
1951
1976
  function _scatterToOriginalAxes(tensor, targetLabels) {
1952
1977
  const currentLabels = tensor.axisLabels;
1953
1978
  const rank = targetLabels.length;
@@ -2076,10 +2101,58 @@ function reverseGrad(fn, x, cotangent) {
2076
2101
  gradient: new Tensor4(x.shape, new Float64Array(xGradSlot))
2077
2102
  };
2078
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
+ }
2079
2148
  export {
2080
2149
  DualTensor,
2081
2150
  Tape,
2082
2151
  TapedTensor,
2152
+ derivative,
2083
2153
  forwardGrad,
2084
- reverseGrad
2154
+ grad,
2155
+ jacobian,
2156
+ reverseGrad,
2157
+ valueAndGrad
2085
2158
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielsimonjr/mathts-autograd",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Forward + reverse-mode automatic differentiation for MathTS rank-N Tensor",
5
5
  "author": "Daniel Simon Jr.",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@danielsimonjr/mathts-core": "^0.1.4",
35
- "@danielsimonjr/mathts-tensor": "^0.1.3"
35
+ "@danielsimonjr/mathts-tensor": "^0.2.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.5.2",