scalar-autograd 0.1.7 → 0.1.9

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 (86) hide show
  1. package/README.md +127 -2
  2. package/dist/CompiledFunctions.d.ts +111 -0
  3. package/dist/CompiledFunctions.js +268 -0
  4. package/dist/CompiledResiduals.d.ts +74 -0
  5. package/dist/CompiledResiduals.js +94 -0
  6. package/dist/EigenvalueHelpers.d.ts +14 -0
  7. package/dist/EigenvalueHelpers.js +93 -0
  8. package/dist/Geometry.d.ts +131 -0
  9. package/dist/Geometry.js +213 -0
  10. package/dist/GraphBuilder.d.ts +64 -0
  11. package/dist/GraphBuilder.js +237 -0
  12. package/dist/GraphCanonicalizerNoSort.d.ts +20 -0
  13. package/dist/GraphCanonicalizerNoSort.js +190 -0
  14. package/dist/GraphHashCanonicalizer.d.ts +46 -0
  15. package/dist/GraphHashCanonicalizer.js +220 -0
  16. package/dist/GraphSignature.d.ts +7 -0
  17. package/dist/GraphSignature.js +7 -0
  18. package/dist/KernelPool.d.ts +55 -0
  19. package/dist/KernelPool.js +124 -0
  20. package/dist/LBFGS.d.ts +84 -0
  21. package/dist/LBFGS.js +313 -0
  22. package/dist/LinearSolver.d.ts +69 -0
  23. package/dist/LinearSolver.js +213 -0
  24. package/dist/Losses.d.ts +9 -0
  25. package/dist/Losses.js +42 -37
  26. package/dist/Matrix3x3.d.ts +50 -0
  27. package/dist/Matrix3x3.js +146 -0
  28. package/dist/NonlinearLeastSquares.d.ts +33 -0
  29. package/dist/NonlinearLeastSquares.js +252 -0
  30. package/dist/Optimizers.d.ts +70 -14
  31. package/dist/Optimizers.js +42 -19
  32. package/dist/V.d.ts +0 -0
  33. package/dist/V.js +0 -0
  34. package/dist/Value.d.ts +84 -2
  35. package/dist/Value.js +296 -58
  36. package/dist/ValueActivation.js +10 -14
  37. package/dist/ValueArithmetic.d.ts +1 -0
  38. package/dist/ValueArithmetic.js +58 -50
  39. package/dist/ValueComparison.js +9 -13
  40. package/dist/ValueRegistry.d.ts +38 -0
  41. package/dist/ValueRegistry.js +88 -0
  42. package/dist/ValueTrig.js +14 -18
  43. package/dist/Vec2.d.ts +45 -0
  44. package/dist/Vec2.js +93 -0
  45. package/dist/Vec3.d.ts +78 -0
  46. package/dist/Vec3.js +169 -0
  47. package/dist/Vec4.d.ts +45 -0
  48. package/dist/Vec4.js +126 -0
  49. package/dist/__tests__/duplicate-inputs.test.js +33 -0
  50. package/dist/cli/gradient-gen.d.ts +19 -0
  51. package/dist/cli/gradient-gen.js +264 -0
  52. package/dist/compileIndirectKernel.d.ts +24 -0
  53. package/dist/compileIndirectKernel.js +148 -0
  54. package/dist/index.d.ts +20 -0
  55. package/dist/index.js +20 -0
  56. package/dist/scalar-autograd.d.ts +1157 -0
  57. package/dist/symbolic/AST.d.ts +113 -0
  58. package/dist/symbolic/AST.js +128 -0
  59. package/dist/symbolic/CodeGen.d.ts +35 -0
  60. package/dist/symbolic/CodeGen.js +280 -0
  61. package/dist/symbolic/Parser.d.ts +64 -0
  62. package/dist/symbolic/Parser.js +329 -0
  63. package/dist/symbolic/Simplify.d.ts +10 -0
  64. package/dist/symbolic/Simplify.js +244 -0
  65. package/dist/symbolic/SymbolicDiff.d.ts +35 -0
  66. package/dist/symbolic/SymbolicDiff.js +339 -0
  67. package/dist/tsdoc-metadata.json +11 -0
  68. package/package.json +29 -5
  69. package/dist/Losses.spec.js +0 -54
  70. package/dist/Optimizers.edge-cases.spec.d.ts +0 -1
  71. package/dist/Optimizers.edge-cases.spec.js +0 -29
  72. package/dist/Optimizers.spec.d.ts +0 -1
  73. package/dist/Optimizers.spec.js +0 -56
  74. package/dist/Value.edge-cases.spec.d.ts +0 -1
  75. package/dist/Value.edge-cases.spec.js +0 -54
  76. package/dist/Value.grad-flow.spec.d.ts +0 -1
  77. package/dist/Value.grad-flow.spec.js +0 -24
  78. package/dist/Value.losses-edge-cases.spec.d.ts +0 -1
  79. package/dist/Value.losses-edge-cases.spec.js +0 -30
  80. package/dist/Value.memory.spec.d.ts +0 -1
  81. package/dist/Value.memory.spec.js +0 -23
  82. package/dist/Value.nn.spec.d.ts +0 -1
  83. package/dist/Value.nn.spec.js +0 -111
  84. package/dist/Value.spec.d.ts +0 -1
  85. package/dist/Value.spec.js +0 -245
  86. /package/dist/{Losses.spec.d.ts → __tests__/duplicate-inputs.test.d.ts} +0 -0
@@ -0,0 +1,237 @@
1
+ import { Value } from './Value';
2
+ // 32-bit primes for mixing
3
+ const PRIMES_32 = [
4
+ 2654435761, // Golden ratio prime
5
+ 805459861,
6
+ 3266489917,
7
+ 4101842887,
8
+ 2484345967,
9
+ 3369493747,
10
+ 1505335857,
11
+ 1267890027,
12
+ ];
13
+ // Operation hash constants (same as GraphHashCanonicalizer)
14
+ const OP_HASHES = {
15
+ '+': { h1: 0x9e3779b9, h2: 0x85ebca6b },
16
+ '-': { h1: 0xc2b2ae35, h2: 0x27d4eb2f },
17
+ '*': { h1: 0x165667b1, h2: 0xd3a2646c },
18
+ '/': { h1: 0xfd7046c5, h2: 0xb55a4f09 },
19
+ 'pow': { h1: 0x5f356495, h2: 0x3c6ef372 },
20
+ 'powValue': { h1: 0x5f356495, h2: 0x3c6ef372 },
21
+ 'sqrt': { h1: 0x8f1bbcdc, h2: 0x42a5c977 },
22
+ 'exp': { h1: 0x1e3779b9, h2: 0x95ebca6b },
23
+ 'log': { h1: 0x2e3779b9, h2: 0xa5ebca6b },
24
+ 'abs': { h1: 0x3e3779b9, h2: 0xb5ebca6b },
25
+ 'square': { h1: 0x4e3779b9, h2: 0xc5ebca6b },
26
+ 'sin': { h1: 0x6e3779b9, h2: 0xe5ebca6b },
27
+ 'cos': { h1: 0x7e3779b9, h2: 0xf5ebca6b },
28
+ 'tan': { h1: 0x8e3779b9, h2: 0x15ebca6b },
29
+ 'asin': { h1: 0x9e3779b9, h2: 0x25ebca6b },
30
+ 'acos': { h1: 0xae3779b9, h2: 0x35ebca6b },
31
+ 'atan': { h1: 0xbe3779b9, h2: 0x45ebca6b },
32
+ 'atan2': { h1: 0xce3779b9, h2: 0x55ebca6b },
33
+ 'min': { h1: 0xde3779b9, h2: 0x65ebca6b },
34
+ 'max': { h1: 0xee3779b9, h2: 0x75ebca6b },
35
+ 'sigmoid': { h1: 0xfe3779b9, h2: 0x85ebca6b },
36
+ 'tanh': { h1: 0x1f3779b9, h2: 0x95ebca6b },
37
+ 'relu': { h1: 0x2f3779b9, h2: 0xa5ebca6b },
38
+ 'softplus': { h1: 0x3f3779b9, h2: 0xb5ebca6b },
39
+ 'sum': { h1: 0x9e3779b9, h2: 0x85ebca6b },
40
+ 'eigenvalue_custom': { h1: 0x4f3779b9, h2: 0xc5ebca6b },
41
+ };
42
+ function getOpHash(op) {
43
+ if (OP_HASHES[op]) {
44
+ return OP_HASHES[op];
45
+ }
46
+ // Fallback for unknown ops
47
+ let h1 = 2166136261;
48
+ let h2 = 5381;
49
+ for (let i = 0; i < op.length; i++) {
50
+ const c = op.charCodeAt(i);
51
+ h1 = (h1 ^ c) >>> 0;
52
+ h1 = Math.imul(h1, 16777619);
53
+ h2 = ((h2 << 5) + h2 + c) >>> 0;
54
+ }
55
+ return { h1: h1 >>> 0, h2: h2 >>> 0 };
56
+ }
57
+ function combineHashes(h1, h2, position = 0) {
58
+ const p1 = PRIMES_32[position % PRIMES_32.length];
59
+ const p2 = PRIMES_32[(position + 1) % PRIMES_32.length];
60
+ return {
61
+ h1: (h1.h1 ^ Math.imul(h2.h1, p1)) >>> 0,
62
+ h2: (h1.h2 ^ Math.imul(h2.h2, p2)) >>> 0
63
+ };
64
+ }
65
+ function hashLeaf(leafIndex, hasGrad) {
66
+ const gradMask = hasGrad ? 0x12345678 : 0;
67
+ return {
68
+ h1: (leafIndex * PRIMES_32[0] ^ gradMask) >>> 0,
69
+ h2: (leafIndex * PRIMES_32[1] ^ (gradMask << 8)) >>> 0
70
+ };
71
+ }
72
+ /**
73
+ * Incremental graph builder that tracks structure during construction.
74
+ *
75
+ * Usage:
76
+ * ```typescript
77
+ * const builder = new GraphBuilder(knownParams);
78
+ * const result = builder.build(() => {
79
+ * const a = V.add(params[0], params[1]);
80
+ * const b = V.mul(a, V.C(2));
81
+ * return b;
82
+ * });
83
+ * // result.signature.hash - for kernel lookup
84
+ * // result.signature.leaves - for index mapping at execution time
85
+ * ```
86
+ */
87
+ export class GraphBuilder {
88
+ leaves = [];
89
+ leafIndexMap = new Map();
90
+ knownParams;
91
+ operations = [];
92
+ /**
93
+ * Create a new graph builder.
94
+ * @param knownParams - Parameters that are known upfront (will be indexed first)
95
+ */
96
+ constructor(knownParams = []) {
97
+ this.knownParams = new Set(knownParams);
98
+ // Pre-register known parameters
99
+ for (const param of knownParams) {
100
+ this.registerLeaf(param);
101
+ }
102
+ }
103
+ /**
104
+ * Register a leaf Value and return its index.
105
+ * If already registered, returns existing index.
106
+ * New parameters are added dynamically.
107
+ */
108
+ registerLeaf(value) {
109
+ let index = this.leafIndexMap.get(value);
110
+ if (index !== undefined) {
111
+ return index;
112
+ }
113
+ // New leaf - add to end
114
+ index = this.leaves.length;
115
+ this.leaves.push(value);
116
+ this.leafIndexMap.set(value, index);
117
+ return index;
118
+ }
119
+ /**
120
+ * Record an operation during graph building.
121
+ * Called automatically by Value.make() when in tracked context.
122
+ * @internal
123
+ */
124
+ recordOp(value) {
125
+ const prev = value.prev;
126
+ // If this is a leaf, register it
127
+ if (prev.length === 0) {
128
+ this.registerLeaf(value);
129
+ return;
130
+ }
131
+ // For intermediate nodes, register all leaf children
132
+ for (const child of prev) {
133
+ if (child.prev.length === 0) {
134
+ this.registerLeaf(child);
135
+ }
136
+ }
137
+ // Record this operation for later hash computation
138
+ this.operations.push(value);
139
+ }
140
+ /**
141
+ * Build a graph in tracked context and return output + signature.
142
+ *
143
+ * @param fn - Function that builds and returns the output Value
144
+ * @returns Output value and graph signature
145
+ */
146
+ build(fn) {
147
+ const prevBuilder = Value.currentBuilder;
148
+ Value.currentBuilder = this;
149
+ try {
150
+ const output = fn();
151
+ // Finalize signature
152
+ // Stable sort: params (with grad) first, then params (no grad), then constants
153
+ const sortedLeaves = [...this.leaves].sort((a, b) => {
154
+ const aIsParam = this.knownParams.has(a);
155
+ const bIsParam = this.knownParams.has(b);
156
+ if (aIsParam !== bIsParam)
157
+ return aIsParam ? -1 : 1;
158
+ if (a.requiresGrad !== b.requiresGrad)
159
+ return b.requiresGrad ? 1 : -1;
160
+ return a._id - b._id;
161
+ });
162
+ // Rebuild index map with sorted order
163
+ const finalLeafIndexMap = new Map();
164
+ for (let i = 0; i < sortedLeaves.length; i++) {
165
+ finalLeafIndexMap.set(sortedLeaves[i], i);
166
+ }
167
+ // Compute hash based on graph structure (NOW that we know all leaf assignments)
168
+ // Create stable node ID map: leaves get their indices, operations get sequential IDs
169
+ const nodeIdMap = new Map();
170
+ for (let i = 0; i < sortedLeaves.length; i++) {
171
+ nodeIdMap.set(sortedLeaves[i], i);
172
+ }
173
+ let nextOpId = sortedLeaves.length;
174
+ let hash = { h1: 2166136261, h2: 5381 };
175
+ // Hash leaf configuration first
176
+ for (let i = 0; i < sortedLeaves.length; i++) {
177
+ const leafHash = hashLeaf(i, sortedLeaves[i].requiresGrad);
178
+ hash = combineHashes(hash, leafHash, i);
179
+ }
180
+ // Hash operations in order they were created
181
+ for (const op of this.operations) {
182
+ const prev = op.prev;
183
+ // Assign ID to this operation
184
+ if (!nodeIdMap.has(op)) {
185
+ nodeIdMap.set(op, nextOpId++);
186
+ }
187
+ let opName = op._op || 'unknown';
188
+ // Normalize operations
189
+ if (opName === 'sum')
190
+ opName = '+';
191
+ if (opName === 'powValue' && prev.length === 2) {
192
+ const exponent = prev[1];
193
+ if (exponent.prev.length === 0 && exponent.data === 2 && !exponent.requiresGrad) {
194
+ opName = 'square';
195
+ }
196
+ }
197
+ const opHash = getOpHash(opName);
198
+ hash = combineHashes(hash, opHash, 0);
199
+ // Hash children by their stable IDs
200
+ for (let i = 0; i < prev.length; i++) {
201
+ const child = prev[i];
202
+ if (!nodeIdMap.has(child)) {
203
+ nodeIdMap.set(child, nextOpId++);
204
+ }
205
+ const childId = nodeIdMap.get(child);
206
+ const childHash = {
207
+ h1: childId * PRIMES_32[2],
208
+ h2: childId * PRIMES_32[3]
209
+ };
210
+ hash = combineHashes(hash, childHash, i + 1);
211
+ }
212
+ }
213
+ const hashString = hash.h1.toString(16).padStart(8, '0') +
214
+ hash.h2.toString(16).padStart(8, '0');
215
+ return {
216
+ output,
217
+ signature: {
218
+ hash: hashString,
219
+ leaves: sortedLeaves,
220
+ leafIndexMap: finalLeafIndexMap
221
+ }
222
+ };
223
+ }
224
+ finally {
225
+ Value.currentBuilder = prevBuilder;
226
+ }
227
+ }
228
+ /**
229
+ * Get current signature without finalizing (for debugging)
230
+ */
231
+ getCurrentSignature() {
232
+ return {
233
+ hash: 'incomplete',
234
+ leaves: [...this.leaves]
235
+ };
236
+ }
237
+ }
@@ -0,0 +1,20 @@
1
+ import { Value } from './Value';
2
+ export interface CanonicalResult {
3
+ /** Canonical string uniquely identifying graph structure */
4
+ canon: string;
5
+ /** Maps Value nodes to their parameter indices */
6
+ paramMap: Map<Value, number>;
7
+ }
8
+ /**
9
+ * Canonicalize a computation graph WITHOUT sorting (fast, fewer matches).
10
+ *
11
+ * This is faster than the sorted version but produces different signatures
12
+ * for reordered commutative operations.
13
+ *
14
+ * @param output - The output Value of the computation graph
15
+ * @param params - Array of parameter Values for this residual/objective
16
+ * @returns Canonical string and parameter mapping
17
+ *
18
+ * @internal
19
+ */
20
+ export declare function canonicalizeGraphNoSort(output: Value, params: Value[]): CanonicalResult;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * CANONICAL STRING REPRESENTATION (NO SORTING)
3
+ *
4
+ * Fast variant that doesn't sort children of commutative operations.
5
+ * This means fewer kernel matches (add(a,b) != add(b,a)), but much faster.
6
+ *
7
+ * Uses traverse counter optimization: instead of Map<Value, number> lookups,
8
+ * stamps each Value with a traverse ID for O(1) property access.
9
+ *
10
+ * @internal
11
+ */
12
+ // Global traverse counter for memoization
13
+ let globalTraverseId = 0;
14
+ /**
15
+ * Canonicalize a computation graph WITHOUT sorting (fast, fewer matches).
16
+ *
17
+ * This is faster than the sorted version but produces different signatures
18
+ * for reordered commutative operations.
19
+ *
20
+ * @param output - The output Value of the computation graph
21
+ * @param params - Array of parameter Values for this residual/objective
22
+ * @returns Canonical string and parameter mapping
23
+ *
24
+ * @internal
25
+ */
26
+ export function canonicalizeGraphNoSort(output, params) {
27
+ const t0 = performance.now();
28
+ const leafToId = new Map();
29
+ const allLeaves = new Set();
30
+ // Increment global traverse ID for this canonicalization
31
+ const traverseId = ++globalTraverseId;
32
+ // Phase 1: Discover all leaves in the graph (iterative)
33
+ const discoverStack = [output];
34
+ const discoverVisited = new Set();
35
+ while (discoverStack.length > 0) {
36
+ const node = discoverStack.pop();
37
+ if (discoverVisited.has(node))
38
+ continue;
39
+ discoverVisited.add(node);
40
+ const prev = node.prev;
41
+ if (prev.length === 0) {
42
+ allLeaves.add(node);
43
+ }
44
+ else {
45
+ for (const child of prev) {
46
+ discoverStack.push(child);
47
+ }
48
+ }
49
+ }
50
+ const t1 = performance.now();
51
+ // Phase 2: Assign IDs in stable, deterministic order
52
+ let nextId = 0;
53
+ const paramSet = new Set(params);
54
+ for (const param of params) {
55
+ if (allLeaves.has(param)) {
56
+ leafToId.set(param, nextId++);
57
+ }
58
+ }
59
+ const remainingLeaves = Array.from(allLeaves)
60
+ .filter(leaf => !paramSet.has(leaf));
61
+ for (const leaf of remainingLeaves) {
62
+ leafToId.set(leaf, nextId++);
63
+ }
64
+ const t2 = performance.now();
65
+ // Phase 3: Build canonical expression (NO SORTING, NO RECURSION!)
66
+ let nextNodeId = nextId; // Start after leaf IDs
67
+ // Build a compact representation: array of [nodeId, op, childIds...]
68
+ const nodeExprs = [];
69
+ // Topological sort to process nodes bottom-up (iterative)
70
+ const topoOrder = [];
71
+ const topoVisited = new Set();
72
+ const topoStack = [output];
73
+ while (topoStack.length > 0) {
74
+ const node = topoStack[topoStack.length - 1];
75
+ if (topoVisited.has(node)) {
76
+ topoStack.pop();
77
+ continue;
78
+ }
79
+ const prev = node.prev;
80
+ // If leaf, mark visited and add to order
81
+ if (prev.length === 0 || leafToId.has(node)) {
82
+ topoVisited.add(node);
83
+ topoOrder.push(node);
84
+ topoStack.pop();
85
+ continue;
86
+ }
87
+ // Check if all children processed
88
+ let allChildrenProcessed = true;
89
+ for (let i = prev.length - 1; i >= 0; i--) {
90
+ if (!topoVisited.has(prev[i])) {
91
+ allChildrenProcessed = false;
92
+ topoStack.push(prev[i]);
93
+ }
94
+ }
95
+ if (allChildrenProcessed) {
96
+ topoVisited.add(node);
97
+ topoOrder.push(node);
98
+ topoStack.pop();
99
+ }
100
+ }
101
+ const tTraversal = performance.now();
102
+ // Process nodes in topological order (leaves first)
103
+ // Use traverse counter stamping instead of Map lookups
104
+ for (const node of topoOrder) {
105
+ const nodeAny = node;
106
+ // Skip if already processed in this traverse
107
+ if (nodeAny._canonTraverseId === traverseId)
108
+ continue;
109
+ // Leaf node
110
+ const leafId = leafToId.get(node);
111
+ if (leafId !== undefined) {
112
+ nodeAny._canonTraverseId = traverseId;
113
+ nodeAny._canonNodeId = leafId;
114
+ continue;
115
+ }
116
+ const prev = node.prev;
117
+ if (prev.length === 0)
118
+ continue; // Skip empty prev
119
+ // Assign new ID and stamp it
120
+ const myId = nextNodeId++;
121
+ nodeAny._canonTraverseId = traverseId;
122
+ nodeAny._canonNodeId = myId;
123
+ let op = node._op || 'unknown';
124
+ // Normalize pow(x, const_2) -> square(x)
125
+ if (op === 'powValue' && prev.length === 2) {
126
+ const exponent = prev[1];
127
+ const exponentId = leafToId.get(exponent);
128
+ if (exponentId !== undefined && exponent.data === 2 && !exponent.requiresGrad) {
129
+ op = 'square';
130
+ const childId = prev[0]._canonNodeId;
131
+ nodeExprs.push([myId, op, [childId]]);
132
+ continue;
133
+ }
134
+ }
135
+ if (op === 'sum')
136
+ op = '+';
137
+ // All operations: just use children as-is (no flattening)
138
+ const childIds = prev.map(p => p._canonNodeId);
139
+ nodeExprs.push([myId, op, childIds]);
140
+ }
141
+ const outputId = output._canonNodeId;
142
+ const tTraversalEnd = performance.now();
143
+ // Build compact canonical signature from node expressions
144
+ const tStringStart = performance.now();
145
+ // Build leaf lookup for gradient flags (id -> 'g' suffix or '')
146
+ const leafGradFlags = new Map();
147
+ for (const [leaf, id] of leafToId) {
148
+ leafGradFlags.set(id, leaf.requiresGrad ? 'g' : '');
149
+ }
150
+ // Build expression parts (compact format)
151
+ const exprParts = [];
152
+ for (const [nodeId, op, childIds] of nodeExprs) {
153
+ const childRefs = childIds.map(id => {
154
+ const gradFlag = leafGradFlags.get(id);
155
+ if (gradFlag !== undefined) {
156
+ return `${id}${gradFlag}`;
157
+ }
158
+ return `n${id}`;
159
+ }).join(',');
160
+ exprParts.push(`n${nodeId}:(${op},${childRefs})`);
161
+ }
162
+ const expr = exprParts.join(';');
163
+ // Phase 4: Build param list
164
+ const paramList = [];
165
+ for (let i = 0; i < params.length; i++) {
166
+ const param = params[i];
167
+ if (!allLeaves.has(param))
168
+ continue;
169
+ const gradFlag = param.requiresGrad ? 'g' : '';
170
+ paramList.push(`${i}${gradFlag}`);
171
+ }
172
+ // For single-node leaf graphs, include the leaf ID
173
+ const finalExpr = expr.length > 0 ? expr : `${outputId}${leafGradFlags.get(outputId) || ''}`;
174
+ const canon = `${paramList.join(',')}|${finalExpr}`;
175
+ const tStringEnd = performance.now();
176
+ const t3 = performance.now();
177
+ // Build reverse map
178
+ const paramMap = new Map();
179
+ for (const [leaf, id] of leafToId) {
180
+ paramMap.set(leaf, id);
181
+ }
182
+ const t4 = performance.now();
183
+ const total = t4 - t0;
184
+ if (total > 10) {
185
+ const traversalTime = tTraversalEnd - tTraversal;
186
+ const stringBuildTime = tStringEnd - tStringStart;
187
+ console.log(`[GraphCanonicalizerNoSort] Total: ${total.toFixed(0)}ms | Phase1(discover): ${(t1 - t0).toFixed(0)}ms | Phase2(assign): ${(t2 - t1).toFixed(0)}ms | Phase3(expr): ${(t3 - t2).toFixed(0)}ms [traversal: ${traversalTime.toFixed(0)}ms, stringBuild: ${stringBuildTime.toFixed(0)}ms] | Phase4(build): ${(t4 - t3).toFixed(0)}ms | Nodes: ${discoverVisited.size} | NodeExprs: ${nodeExprs.length} | keyLength: ${canon.length}`);
188
+ }
189
+ return { canon, paramMap };
190
+ }
@@ -0,0 +1,46 @@
1
+ import { Value } from './Value';
2
+ /**
3
+ * HASH-BASED CANONICAL REPRESENTATION
4
+ *
5
+ * Fast canonicalization using dual 32-bit integer hashing.
6
+ *
7
+ * Design:
8
+ * - Each node gets a hash based on its operation and children
9
+ * - Position-dependent hashing (preserves order and multiplicity)
10
+ * - Direct integer operations (NO STRING HASHING!)
11
+ * - Parameters maintain topological order in signature
12
+ *
13
+ * Format: "0g,1g,2g,...|<hex_hash>"
14
+ *
15
+ * Benefits:
16
+ * - O(n) complexity (no sorting!)
17
+ * - Dual 32-bit hash (64-bit space, no BigInt overhead)
18
+ * - Preserves multiplicity: add(a,a,b) != add(a,b)
19
+ * - Recursion-free (stack-based iteration)
20
+ * - Pure integer operations (no string conversion!)
21
+ *
22
+ * Note: add(a,b) != add(b,a) - not order-independent
23
+ *
24
+ * @internal
25
+ */
26
+ export interface HashCanonicalResult {
27
+ /** Canonical string with parameter list and expression hash */
28
+ canon: string;
29
+ /** Maps Value nodes to their parameter indices */
30
+ paramMap: Map<Value, number>;
31
+ /** Debug: full string expression (if enabled) */
32
+ debugExpr?: string;
33
+ }
34
+ /**
35
+ * Canonicalize a computation graph using hash-based signatures.
36
+ *
37
+ * Much faster than string-based canonicalization - O(n) with no sorting.
38
+ *
39
+ * @param output - The output Value of the computation graph
40
+ * @param params - Array of parameter Values for this residual/objective
41
+ * @param debug - If true, also generate string expression for debugging
42
+ * @returns Canonical string with hash and parameter mapping
43
+ *
44
+ * @internal
45
+ */
46
+ export declare function canonicalizeGraphHash(output: Value, params: Value[], debug?: boolean): HashCanonicalResult;