@danielsimonjr/mathts-autograd 0.1.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 +168 -0
- package/dist/index.js +305 -0
- package/package.json +58 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Tensor } from '@danielsimonjr/mathts-tensor';
|
|
2
|
+
|
|
3
|
+
/**
|
|
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.
|
|
12
|
+
*
|
|
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.
|
|
15
|
+
*
|
|
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
|
+
private checkSameShape;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Forward-mode AD via dual numbers. Returns the value and the full Jacobian
|
|
59
|
+
* of fn at x, shape [...value.shape, ...x.shape] (row-major flatten).
|
|
60
|
+
*
|
|
61
|
+
* Implementation: for each flat-index k in x, build a DualTensor with a unit
|
|
62
|
+
* tangent at position k, run fn (which operates on Tensors — see "interop"
|
|
63
|
+
* below), extract the tangent of the result. The tangent is the k-th column
|
|
64
|
+
* of the Jacobian (in row-major-of-output-flattened form).
|
|
65
|
+
*
|
|
66
|
+
* Interop with the Tensor type: fn is typed `(x: Tensor) => Tensor`, so the
|
|
67
|
+
* tracing requires fn's body to be implementable on either Tensor OR
|
|
68
|
+
* DualTensor. v0.1.0 ships a documented "trace mode" — if fn uses only the
|
|
69
|
+
* core ops that DualTensor implements (add/sub/mul/scale), the
|
|
70
|
+
* implementation auto-traces by detecting DualTensor inputs via a thin
|
|
71
|
+
* wrapper. The actual mechanism in v0.1.0: caller writes fn as if Tensor's
|
|
72
|
+
* methods accept either Tensor or DualTensor (TypeScript's structural typing
|
|
73
|
+
* makes this work for the implemented ops). v0.2.0 may broaden via decorator
|
|
74
|
+
* instrumentation.
|
|
75
|
+
*
|
|
76
|
+
* For UPT v0.4.0's call-sites (metric functions in christoffel lowering),
|
|
77
|
+
* fn uses only add/sub/mul/scale → safe.
|
|
78
|
+
*
|
|
79
|
+
* @packageDocumentation
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compute the value and full Jacobian of `fn` at point `x` using forward-mode
|
|
84
|
+
* automatic differentiation (dual numbers).
|
|
85
|
+
*
|
|
86
|
+
* @param fn - Pure function operating on Tensors via add/sub/mul/scale.
|
|
87
|
+
* Must be traceable: at runtime it receives a DualTensor (structurally
|
|
88
|
+
* compatible with Tensor) and must return a DualTensor.
|
|
89
|
+
* @param x - Input tensor at which to differentiate.
|
|
90
|
+
* @returns `{ value, jacobian }` where:
|
|
91
|
+
* - `value` has the output shape of fn applied to x
|
|
92
|
+
* - `jacobian` has shape `[...value.shape, ...x.shape]` row-major.
|
|
93
|
+
* `jacobian.data[kY * xSize + kX]` = ∂y[kY] / ∂x[kX].
|
|
94
|
+
* Verified by Adam+Eve review 2026-05-15 (E13): kY outer, kX inner,
|
|
95
|
+
* stride xSize between successive y-rows IS row-major [...y.shape, ...x.shape].
|
|
96
|
+
*/
|
|
97
|
+
declare function forwardGrad(fn: (x: Tensor) => Tensor, x: Tensor): {
|
|
98
|
+
value: Tensor;
|
|
99
|
+
jacobian: Tensor;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Tape — records the sequence of ops during a forward pass so we can
|
|
104
|
+
* replay them in reverse to compute the vector-Jacobian product.
|
|
105
|
+
*
|
|
106
|
+
* Each TapedTensor wraps a primal Tensor + a tape-node id. Ops on
|
|
107
|
+
* TapedTensors record their backward closures into a shared Tape.
|
|
108
|
+
* After the forward pass, calling Tape.backward(outputGrad) walks the
|
|
109
|
+
* tape in reverse, accumulating gradients into each input slot.
|
|
110
|
+
*
|
|
111
|
+
* v0.1.0 supports the same ops as DualTensor: add, sub, mul, scale.
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
type BackwardFn = (outputGrad: Float64Array) => void;
|
|
115
|
+
declare class Tape {
|
|
116
|
+
private nodes;
|
|
117
|
+
private inputGradSlots;
|
|
118
|
+
private nextOpId;
|
|
119
|
+
private nextInputId;
|
|
120
|
+
/** Allocate a fresh id for an input or intermediate. */
|
|
121
|
+
allocate(size: number): {
|
|
122
|
+
id: number;
|
|
123
|
+
gradSlot: Float64Array;
|
|
124
|
+
};
|
|
125
|
+
record(inputIds: ReadonlyArray<number>, outputSize: number, backward: BackwardFn): {
|
|
126
|
+
id: number;
|
|
127
|
+
gradSlot: Float64Array;
|
|
128
|
+
};
|
|
129
|
+
/** Seed the final output's gradient slot, then replay in reverse. */
|
|
130
|
+
backward(outputId: number, outputGrad: Float64Array): void;
|
|
131
|
+
getInputGrad(id: number): Float64Array | undefined;
|
|
132
|
+
}
|
|
133
|
+
declare class TapedTensor {
|
|
134
|
+
readonly shape: ReadonlyArray<number>;
|
|
135
|
+
readonly primal: Float64Array;
|
|
136
|
+
readonly tape: Tape;
|
|
137
|
+
readonly id: number;
|
|
138
|
+
constructor(shape: ReadonlyArray<number>, primal: Float64Array, tape: Tape, id: number);
|
|
139
|
+
/**
|
|
140
|
+
* S5 fix: existing engine ops (e.g. lower, pderiv, contract) reach into
|
|
141
|
+
* `.data`. The getter returns the primal so those ops still work when a
|
|
142
|
+
* TapedTensor flows through them as a structurally-compatible Tensor.
|
|
143
|
+
* (AD-aware ops branch on `'tape' in arg` before reaching here.)
|
|
144
|
+
*/
|
|
145
|
+
get data(): Float64Array;
|
|
146
|
+
static fromTensorAsInput(t: Tensor, tape: Tape): TapedTensor;
|
|
147
|
+
toPrimalTensor(): Tensor;
|
|
148
|
+
add(other: TapedTensor): TapedTensor;
|
|
149
|
+
sub(other: TapedTensor): TapedTensor;
|
|
150
|
+
mul(other: TapedTensor): TapedTensor;
|
|
151
|
+
scale(k: number): TapedTensor;
|
|
152
|
+
private checkSameShape;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Reverse-mode AD via tape. Returns the value and the vector-Jacobian product
|
|
157
|
+
* (gradient = ∂(cotangent · value) / ∂x). gradient.shape = x.shape.
|
|
158
|
+
*
|
|
159
|
+
* Default cotangent: ones-like(value) — required for scalar outputs, useful
|
|
160
|
+
* for VJP defaults. For non-scalar value, cotangent's shape must match value.shape.
|
|
161
|
+
*/
|
|
162
|
+
|
|
163
|
+
declare function reverseGrad(fn: (x: Tensor) => Tensor, x: Tensor, cotangent?: Tensor): {
|
|
164
|
+
value: Tensor;
|
|
165
|
+
gradient: Tensor;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export { DualTensor, Tape, TapedTensor, forwardGrad, reverseGrad };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// src/dual-tensor.ts
|
|
2
|
+
import { Tensor } from "@danielsimonjr/mathts-tensor";
|
|
3
|
+
var DualTensor = class _DualTensor {
|
|
4
|
+
shape;
|
|
5
|
+
primal;
|
|
6
|
+
tangent;
|
|
7
|
+
constructor(shape, primal, tangent) {
|
|
8
|
+
if (primal.length !== tangent.length) {
|
|
9
|
+
throw new Error(`DualTensor: primal length ${primal.length} != tangent length ${tangent.length}`);
|
|
10
|
+
}
|
|
11
|
+
this.shape = shape;
|
|
12
|
+
this.primal = primal;
|
|
13
|
+
this.tangent = tangent;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* S5 fix: existing engine ops (e.g. lower, pderiv, contract) reach into
|
|
17
|
+
* `.data`. The getter returns the primal so those ops still work when a
|
|
18
|
+
* DualTensor flows through them as a structurally-compatible Tensor.
|
|
19
|
+
* (AD-aware ops branch on `'tangent' in arg` before reaching here.)
|
|
20
|
+
*/
|
|
21
|
+
get data() {
|
|
22
|
+
return this.primal;
|
|
23
|
+
}
|
|
24
|
+
/** Lift a Tensor to a DualTensor with zero tangent. */
|
|
25
|
+
static fromTensor(t) {
|
|
26
|
+
return new _DualTensor(t.shape, new Float64Array(t.data), new Float64Array(t.data.length));
|
|
27
|
+
}
|
|
28
|
+
/** Lift a Tensor to a DualTensor with a unit tangent at flat-index `i`. */
|
|
29
|
+
static unitAt(t, i) {
|
|
30
|
+
const tan = new Float64Array(t.data.length);
|
|
31
|
+
tan[i] = 1;
|
|
32
|
+
return new _DualTensor(t.shape, new Float64Array(t.data), tan);
|
|
33
|
+
}
|
|
34
|
+
/** Extract the primal as a plain Tensor. */
|
|
35
|
+
toPrimalTensor() {
|
|
36
|
+
return new Tensor(this.shape, new Float64Array(this.primal));
|
|
37
|
+
}
|
|
38
|
+
/** Extract the tangent as a plain Tensor. */
|
|
39
|
+
toTangentTensor() {
|
|
40
|
+
return new Tensor(this.shape, new Float64Array(this.tangent));
|
|
41
|
+
}
|
|
42
|
+
/** Elementwise addition following dual-number rule: (a+b, a'+b'). */
|
|
43
|
+
add(other) {
|
|
44
|
+
this.checkSameShape(other, "add");
|
|
45
|
+
const p = new Float64Array(this.primal.length);
|
|
46
|
+
const t = new Float64Array(this.tangent.length);
|
|
47
|
+
for (let i = 0; i < p.length; i++) {
|
|
48
|
+
p[i] = this.primal[i] + other.primal[i];
|
|
49
|
+
t[i] = this.tangent[i] + other.tangent[i];
|
|
50
|
+
}
|
|
51
|
+
return new _DualTensor(this.shape, p, t);
|
|
52
|
+
}
|
|
53
|
+
/** Elementwise subtraction following dual-number rule: (a-b, a'-b'). */
|
|
54
|
+
sub(other) {
|
|
55
|
+
this.checkSameShape(other, "sub");
|
|
56
|
+
const p = new Float64Array(this.primal.length);
|
|
57
|
+
const t = new Float64Array(this.tangent.length);
|
|
58
|
+
for (let i = 0; i < p.length; i++) {
|
|
59
|
+
p[i] = this.primal[i] - other.primal[i];
|
|
60
|
+
t[i] = this.tangent[i] - other.tangent[i];
|
|
61
|
+
}
|
|
62
|
+
return new _DualTensor(this.shape, p, t);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Elementwise multiplication following dual-number rule: (a·b, a·b' + a'·b).
|
|
66
|
+
*
|
|
67
|
+
* I3 fix: explicit alias check. When `this === other` (self-multiplication),
|
|
68
|
+
* use the specialised rule (a·a)' = 2·a·a' directly. The general rule also
|
|
69
|
+
* degenerates correctly, but the explicit branch is more readable and aligns
|
|
70
|
+
* the forward-mode implementation with the reverse-mode alias fix.
|
|
71
|
+
*/
|
|
72
|
+
mul(other) {
|
|
73
|
+
this.checkSameShape(other, "mul");
|
|
74
|
+
const p = new Float64Array(this.primal.length);
|
|
75
|
+
const t = new Float64Array(this.tangent.length);
|
|
76
|
+
if (this === other) {
|
|
77
|
+
for (let i = 0; i < p.length; i++) {
|
|
78
|
+
p[i] = this.primal[i] * this.primal[i];
|
|
79
|
+
t[i] = 2 * this.primal[i] * this.tangent[i];
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
for (let i = 0; i < p.length; i++) {
|
|
83
|
+
p[i] = this.primal[i] * other.primal[i];
|
|
84
|
+
t[i] = this.tangent[i] * other.primal[i] + this.primal[i] * other.tangent[i];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return new _DualTensor(this.shape, p, t);
|
|
88
|
+
}
|
|
89
|
+
/** Scalar multiplication following dual-number rule: (k·a, k·a'). */
|
|
90
|
+
scale(k) {
|
|
91
|
+
const p = new Float64Array(this.primal.length);
|
|
92
|
+
const t = new Float64Array(this.tangent.length);
|
|
93
|
+
for (let i = 0; i < p.length; i++) {
|
|
94
|
+
p[i] = this.primal[i] * k;
|
|
95
|
+
t[i] = this.tangent[i] * k;
|
|
96
|
+
}
|
|
97
|
+
return new _DualTensor(this.shape, p, t);
|
|
98
|
+
}
|
|
99
|
+
checkSameShape(other, op) {
|
|
100
|
+
if (this.shape.length !== other.shape.length || !this.shape.every((v, i) => v === other.shape[i])) {
|
|
101
|
+
throw new Error(`DualTensor.${op}: shape mismatch [${this.shape}] vs [${other.shape}]`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/forward-grad.ts
|
|
107
|
+
import { Tensor as Tensor2 } from "@danielsimonjr/mathts-tensor";
|
|
108
|
+
function forwardGrad(fn, x) {
|
|
109
|
+
const xDualZero = DualTensor.fromTensor(x);
|
|
110
|
+
const yProbeRaw = fn(xDualZero);
|
|
111
|
+
if (!(yProbeRaw instanceof DualTensor)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"forwardGrad: fn must be AD-traceable \u2014 its return must propagate through DualTensor arithmetic (use add/sub/mul/scale on the argument). A fresh Tensor return loses the tangent and silently corrupts the Jacobian."
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const yPrimal = yProbeRaw.toPrimalTensor();
|
|
117
|
+
const jacobianShape = [...yPrimal.shape, ...x.shape];
|
|
118
|
+
const jacobianSize = jacobianShape.reduce((a, b) => a * b, 1);
|
|
119
|
+
const jacobianData = new Float64Array(jacobianSize);
|
|
120
|
+
const xSize = x.data.length;
|
|
121
|
+
const ySize = yPrimal.data.length;
|
|
122
|
+
for (let kX = 0; kX < xSize; kX++) {
|
|
123
|
+
const xDualUnit = DualTensor.unitAt(x, kX);
|
|
124
|
+
const yDualRaw = fn(xDualUnit);
|
|
125
|
+
if (!(yDualRaw instanceof DualTensor)) {
|
|
126
|
+
throw new Error("forwardGrad: fn lost AD trace mid-sweep (returned non-DualTensor)");
|
|
127
|
+
}
|
|
128
|
+
const yDual = yDualRaw;
|
|
129
|
+
for (let kY = 0; kY < ySize; kY++) {
|
|
130
|
+
jacobianData[kY * xSize + kX] = yDual.tangent[kY];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
value: yPrimal,
|
|
135
|
+
jacobian: new Tensor2(jacobianShape, jacobianData)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/tape.ts
|
|
140
|
+
import { Tensor as Tensor3 } from "@danielsimonjr/mathts-tensor";
|
|
141
|
+
var Tape = class {
|
|
142
|
+
nodes = [];
|
|
143
|
+
inputGradSlots = /* @__PURE__ */ new Map();
|
|
144
|
+
// S3 fix: disjoint ID namespaces. Inputs use negative IDs, ops use
|
|
145
|
+
// non-negative IDs. Eliminates the v0.4.0-review collision where
|
|
146
|
+
// id = nodes.length + 1000 would collide with op ids past 1000 entries.
|
|
147
|
+
nextOpId = 0;
|
|
148
|
+
nextInputId = -1;
|
|
149
|
+
// negatives = inputs; non-negatives = ops
|
|
150
|
+
/** Allocate a fresh id for an input or intermediate. */
|
|
151
|
+
allocate(size) {
|
|
152
|
+
const id = this.nextInputId--;
|
|
153
|
+
const gradSlot = new Float64Array(size);
|
|
154
|
+
this.inputGradSlots.set(id, gradSlot);
|
|
155
|
+
return { id, gradSlot };
|
|
156
|
+
}
|
|
157
|
+
record(inputIds, outputSize, backward) {
|
|
158
|
+
const outputGradSlot = new Float64Array(outputSize);
|
|
159
|
+
const id = this.nextOpId++;
|
|
160
|
+
this.nodes.push({ inputIds, backward, outputGradSlot });
|
|
161
|
+
this.inputGradSlots.set(id, outputGradSlot);
|
|
162
|
+
return { id, gradSlot: outputGradSlot };
|
|
163
|
+
}
|
|
164
|
+
/** Seed the final output's gradient slot, then replay in reverse. */
|
|
165
|
+
backward(outputId, outputGrad) {
|
|
166
|
+
const slot = this.inputGradSlots.get(outputId);
|
|
167
|
+
if (!slot) throw new Error(`Tape.backward: unknown outputId ${outputId}`);
|
|
168
|
+
slot.set(outputGrad);
|
|
169
|
+
for (let n = this.nodes.length - 1; n >= 0; n--) {
|
|
170
|
+
this.nodes[n].backward(this.nodes[n].outputGradSlot);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
getInputGrad(id) {
|
|
174
|
+
return this.inputGradSlots.get(id);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
var TapedTensor = class _TapedTensor {
|
|
178
|
+
constructor(shape, primal, tape, id) {
|
|
179
|
+
this.shape = shape;
|
|
180
|
+
this.primal = primal;
|
|
181
|
+
this.tape = tape;
|
|
182
|
+
this.id = id;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* S5 fix: existing engine ops (e.g. lower, pderiv, contract) reach into
|
|
186
|
+
* `.data`. The getter returns the primal so those ops still work when a
|
|
187
|
+
* TapedTensor flows through them as a structurally-compatible Tensor.
|
|
188
|
+
* (AD-aware ops branch on `'tape' in arg` before reaching here.)
|
|
189
|
+
*/
|
|
190
|
+
get data() {
|
|
191
|
+
return this.primal;
|
|
192
|
+
}
|
|
193
|
+
static fromTensorAsInput(t, tape) {
|
|
194
|
+
const { id } = tape.allocate(t.data.length);
|
|
195
|
+
return new _TapedTensor(t.shape, new Float64Array(t.data), tape, id);
|
|
196
|
+
}
|
|
197
|
+
toPrimalTensor() {
|
|
198
|
+
return new Tensor3(this.shape, new Float64Array(this.primal));
|
|
199
|
+
}
|
|
200
|
+
add(other) {
|
|
201
|
+
this.checkSameShape(other, "add");
|
|
202
|
+
const out = new Float64Array(this.primal.length);
|
|
203
|
+
for (let i = 0; i < out.length; i++) out[i] = this.primal[i] + other.primal[i];
|
|
204
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
205
|
+
const otherGradSlot = this.tape.getInputGrad(other.id);
|
|
206
|
+
const { id } = this.tape.record([this.id, other.id], out.length, (outputGrad) => {
|
|
207
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
208
|
+
thisGradSlot[i] += outputGrad[i];
|
|
209
|
+
otherGradSlot[i] += outputGrad[i];
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
213
|
+
}
|
|
214
|
+
sub(other) {
|
|
215
|
+
this.checkSameShape(other, "sub");
|
|
216
|
+
const out = new Float64Array(this.primal.length);
|
|
217
|
+
for (let i = 0; i < out.length; i++) out[i] = this.primal[i] - other.primal[i];
|
|
218
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
219
|
+
const otherGradSlot = this.tape.getInputGrad(other.id);
|
|
220
|
+
const { id } = this.tape.record([this.id, other.id], out.length, (outputGrad) => {
|
|
221
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
222
|
+
thisGradSlot[i] += outputGrad[i];
|
|
223
|
+
otherGradSlot[i] -= outputGrad[i];
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
227
|
+
}
|
|
228
|
+
mul(other) {
|
|
229
|
+
this.checkSameShape(other, "mul");
|
|
230
|
+
const out = new Float64Array(this.primal.length);
|
|
231
|
+
for (let i = 0; i < out.length; i++) out[i] = this.primal[i] * other.primal[i];
|
|
232
|
+
const thisPrimal = this.primal;
|
|
233
|
+
const otherPrimal = other.primal;
|
|
234
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
235
|
+
const otherGradSlot = this.tape.getInputGrad(other.id);
|
|
236
|
+
const isAliased = this === other;
|
|
237
|
+
const { id } = this.tape.record([this.id, other.id], out.length, (outputGrad) => {
|
|
238
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
239
|
+
if (isAliased) {
|
|
240
|
+
thisGradSlot[i] += 2 * outputGrad[i] * thisPrimal[i];
|
|
241
|
+
} else {
|
|
242
|
+
thisGradSlot[i] += outputGrad[i] * otherPrimal[i];
|
|
243
|
+
otherGradSlot[i] += outputGrad[i] * thisPrimal[i];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
248
|
+
}
|
|
249
|
+
scale(k) {
|
|
250
|
+
const out = new Float64Array(this.primal.length);
|
|
251
|
+
for (let i = 0; i < out.length; i++) out[i] = this.primal[i] * k;
|
|
252
|
+
const thisGradSlot = this.tape.getInputGrad(this.id);
|
|
253
|
+
const { id } = this.tape.record([this.id], out.length, (outputGrad) => {
|
|
254
|
+
for (let i = 0; i < outputGrad.length; i++) {
|
|
255
|
+
thisGradSlot[i] += outputGrad[i] * k;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return new _TapedTensor(this.shape, out, this.tape, id);
|
|
259
|
+
}
|
|
260
|
+
checkSameShape(other, op) {
|
|
261
|
+
if (this.shape.length !== other.shape.length || !this.shape.every((v, i) => v === other.shape[i])) {
|
|
262
|
+
throw new Error(`TapedTensor.${op}: shape mismatch [${this.shape}] vs [${other.shape}]`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// src/reverse-grad.ts
|
|
268
|
+
import { Tensor as Tensor4 } from "@danielsimonjr/mathts-tensor";
|
|
269
|
+
function reverseGrad(fn, x, cotangent) {
|
|
270
|
+
const tape = new Tape();
|
|
271
|
+
const xTaped = TapedTensor.fromTensorAsInput(x, tape);
|
|
272
|
+
const yRaw = fn(xTaped);
|
|
273
|
+
if (!(yRaw instanceof TapedTensor)) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
"reverseGrad: fn must be AD-traceable \u2014 its return must propagate through TapedTensor arithmetic (use add/sub/mul/scale on the argument). A fresh Tensor return loses the tape and silently corrupts the gradient."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const yTaped = yRaw;
|
|
279
|
+
const value = yTaped.toPrimalTensor();
|
|
280
|
+
let ct;
|
|
281
|
+
if (cotangent === void 0) {
|
|
282
|
+
const data = new Float64Array(value.data.length).fill(1);
|
|
283
|
+
ct = new Tensor4(value.shape, data);
|
|
284
|
+
} else {
|
|
285
|
+
if (cotangent.shape.length !== value.shape.length || !cotangent.shape.every((v, i) => v === value.shape[i])) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`reverseGrad: cotangent shape [${cotangent.shape}] != value shape [${value.shape}]`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
ct = cotangent;
|
|
291
|
+
}
|
|
292
|
+
tape.backward(yTaped.id, new Float64Array(ct.data));
|
|
293
|
+
const xGradSlot = tape.getInputGrad(xTaped.id);
|
|
294
|
+
return {
|
|
295
|
+
value,
|
|
296
|
+
gradient: new Tensor4(x.shape, new Float64Array(xGradSlot))
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
export {
|
|
300
|
+
DualTensor,
|
|
301
|
+
Tape,
|
|
302
|
+
TapedTensor,
|
|
303
|
+
forwardGrad,
|
|
304
|
+
reverseGrad
|
|
305
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@danielsimonjr/mathts-autograd",
|
|
3
|
+
"version": "0.1.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": "*",
|
|
35
|
+
"@danielsimonjr/mathts-tensor": "*"
|
|
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
|
+
}
|