@aleph-ai/tinyaleph 1.3.0 → 1.4.1

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.
@@ -0,0 +1,1004 @@
1
+ /**
2
+ * CRT-Homology Module for tinyaleph
3
+ *
4
+ * Implements:
5
+ * - Differentiable Chinese Remainder Theorem (CRT) reconstruction
6
+ * - Birkhoff polytope projection (doubly-stochastic attention)
7
+ * - Residue encoding over coprime moduli
8
+ * - Homology loss for detecting consistency failures (holes)
9
+ *
10
+ * Key Insight: "Holes are not degrees of freedom. Holes are consistency
11
+ * failures that persist under perturbation."
12
+ *
13
+ * Mathematical Framework:
14
+ * - r_k = softmax(W_k h + b_k) ∈ Δ(ℤ/p_k) (residue distribution)
15
+ * - L̂ = Σ_k E[r_k] * (P/p_k) * (P/p_k)^{-1} mod p_k (CRT reconstruction)
16
+ * - Ker(ℛ) = { r | ℛ(r) undefined } (obstruction cycles)
17
+ * - ℒ_homology = Σ_{cycles ∈ Ker(ℛ)} f(cycle) (homology loss)
18
+ *
19
+ * @module core/crt-homology
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const { firstNPrimes, isPrime, factorize } = require('./prime');
25
+ const { Complex, PrimeState } = require('./hilbert');
26
+ const { SparsePrimeState, resonanceScore, Quaternion } = require('./rformer');
27
+
28
+ // ============================================================================
29
+ // MODULAR ARITHMETIC UTILITIES
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Extended Euclidean Algorithm
34
+ * Returns { gcd, x, y } such that gcd = a*x + b*y
35
+ * @param {number} a - First number
36
+ * @param {number} b - Second number
37
+ * @returns {Object} { gcd, x, y }
38
+ */
39
+ function extendedGCD(a, b) {
40
+ if (b === 0) {
41
+ return { gcd: a, x: 1, y: 0 };
42
+ }
43
+
44
+ const { gcd, x: x1, y: y1 } = extendedGCD(b, a % b);
45
+ const x = y1;
46
+ const y = x1 - Math.floor(a / b) * y1;
47
+
48
+ return { gcd, x, y };
49
+ }
50
+
51
+ /**
52
+ * Modular multiplicative inverse
53
+ * Returns a^{-1} mod m such that a * a^{-1} ≡ 1 (mod m)
54
+ * @param {number} a - Number to invert
55
+ * @param {number} m - Modulus
56
+ * @returns {number|null} Inverse if exists, null otherwise
57
+ */
58
+ function modInverse(a, m) {
59
+ a = ((a % m) + m) % m; // Normalize to positive
60
+ const { gcd, x } = extendedGCD(a, m);
61
+
62
+ if (gcd !== 1) {
63
+ return null; // No inverse exists (not coprime)
64
+ }
65
+
66
+ return ((x % m) + m) % m;
67
+ }
68
+
69
+ /**
70
+ * Check if two numbers are coprime
71
+ * @param {number} a - First number
72
+ * @param {number} b - Second number
73
+ * @returns {boolean} True if gcd(a, b) = 1
74
+ */
75
+ function areCoprime(a, b) {
76
+ return extendedGCD(a, b).gcd === 1;
77
+ }
78
+
79
+ /**
80
+ * Softmax function
81
+ * @param {Array<number>} logits - Input logits
82
+ * @returns {Array<number>} Probability distribution
83
+ */
84
+ function softmax(logits) {
85
+ const maxLogit = Math.max(...logits);
86
+ const expLogits = logits.map(l => Math.exp(l - maxLogit));
87
+ const sumExp = expLogits.reduce((a, b) => a + b, 0);
88
+ return expLogits.map(e => e / (sumExp + 1e-10));
89
+ }
90
+
91
+ // ============================================================================
92
+ // RESIDUE ENCODER: r_k = softmax(W_k h + b_k) ∈ Δ(ℤ/p_k)
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Residue Encoder
97
+ *
98
+ * Encodes a hidden vector h into K residue distributions, one per prime modulus.
99
+ * Each residue r_k is a probability distribution over ℤ/p_k.
100
+ */
101
+ class ResidueEncoder {
102
+ /**
103
+ * Create a residue encoder
104
+ * @param {Array<number>} primes - Array of coprime moduli [p_1, p_2, ..., p_K]
105
+ * @param {number} hiddenDim - Dimension of input hidden vector
106
+ * @param {Object} options - Configuration options
107
+ */
108
+ constructor(primes, hiddenDim, options = {}) {
109
+ // Validate primes are coprime
110
+ for (let i = 0; i < primes.length; i++) {
111
+ for (let j = i + 1; j < primes.length; j++) {
112
+ if (!areCoprime(primes[i], primes[j])) {
113
+ throw new Error(`Primes ${primes[i]} and ${primes[j]} are not coprime`);
114
+ }
115
+ }
116
+ }
117
+
118
+ this.primes = primes;
119
+ this.K = primes.length;
120
+ this.hiddenDim = hiddenDim;
121
+
122
+ // Compute P = ∏ p_k
123
+ this.P = primes.reduce((a, b) => a * b, 1);
124
+
125
+ // Initialize weight matrices W_k (hiddenDim x p_k) and biases b_k
126
+ // In a real differentiable setting, these would be trainable parameters
127
+ this.weights = primes.map(p => this._initMatrix(hiddenDim, p, options.initScale || 0.1));
128
+ this.biases = primes.map(p => new Float64Array(p));
129
+
130
+ // Optional: learnable prime parameters
131
+ this.learnablePrimes = options.learnablePrimes || false;
132
+ }
133
+
134
+ /**
135
+ * Initialize a weight matrix with small random values
136
+ * @private
137
+ */
138
+ _initMatrix(rows, cols, scale = 0.1) {
139
+ const matrix = [];
140
+ for (let i = 0; i < rows; i++) {
141
+ const row = new Float64Array(cols);
142
+ for (let j = 0; j < cols; j++) {
143
+ row[j] = (Math.random() - 0.5) * 2 * scale;
144
+ }
145
+ matrix.push(row);
146
+ }
147
+ return matrix;
148
+ }
149
+
150
+ /**
151
+ * Matrix-vector multiplication
152
+ * @private
153
+ */
154
+ _matVec(matrix, vec) {
155
+ const result = new Float64Array(matrix[0].length);
156
+ for (let j = 0; j < matrix[0].length; j++) {
157
+ for (let i = 0; i < matrix.length; i++) {
158
+ result[j] += matrix[i][j] * (vec[i] || 0);
159
+ }
160
+ }
161
+ return result;
162
+ }
163
+
164
+ /**
165
+ * Encode hidden vector to K residue distributions
166
+ * r_k = softmax(W_k^T h + b_k) ∈ Δ(ℤ/p_k)
167
+ *
168
+ * @param {Array<number>|Float64Array} h - Hidden vector of dimension hiddenDim
169
+ * @returns {Array<Float64Array>} Array of K probability distributions
170
+ */
171
+ encode(h) {
172
+ return this.primes.map((p, k) => {
173
+ // Compute logits: W_k^T h + b_k
174
+ const logits = this._matVec(this.weights[k], h);
175
+ for (let i = 0; i < p; i++) {
176
+ logits[i] += this.biases[k][i];
177
+ }
178
+ // Apply softmax to get probability distribution over ℤ/p_k
179
+ return Float64Array.from(softmax(Array.from(logits)));
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Compute expected residue E[r_k] = Σ_{i=0}^{p_k-1} i * r_k[i]
185
+ * @param {Float64Array} r_k - Residue distribution for modulus k
186
+ * @returns {number} Expected value
187
+ */
188
+ expectedResidue(r_k) {
189
+ let sum = 0;
190
+ for (let i = 0; i < r_k.length; i++) {
191
+ sum += i * r_k[i];
192
+ }
193
+ return sum;
194
+ }
195
+
196
+ /**
197
+ * Get all expected residues from encoded distributions
198
+ * @param {Array<Float64Array>} residues - Encoded residue distributions
199
+ * @returns {Array<number>} Expected residues [E[r_1], ..., E[r_K]]
200
+ */
201
+ expectedResidues(residues) {
202
+ return residues.map(r => this.expectedResidue(r));
203
+ }
204
+
205
+ /**
206
+ * Encode from SparsePrimeState (integration with existing tinyaleph types)
207
+ * @param {SparsePrimeState} state - Sparse prime state
208
+ * @returns {Array<Float64Array>} Residue distributions
209
+ */
210
+ encodeFromPrimeState(state) {
211
+ // Convert sparse state to dense hidden vector
212
+ const h = new Float64Array(this.hiddenDim);
213
+ const activePrimes = state.getActivePrimes();
214
+
215
+ for (let i = 0; i < Math.min(activePrimes.length, this.hiddenDim); i++) {
216
+ const p = activePrimes[i];
217
+ const act = state.get(p);
218
+ h[i] = act.amplitude.norm();
219
+ }
220
+
221
+ return this.encode(h);
222
+ }
223
+ }
224
+
225
+ // ============================================================================
226
+ // CRT RECONSTRUCTOR: ℛ(r) = Σ_k E[r_k] * (P/p_k) * (P/p_k)^{-1} mod P
227
+ // ============================================================================
228
+
229
+ /**
230
+ * Chinese Remainder Theorem Reconstructor
231
+ *
232
+ * Reconstructs a unique value L̂ ∈ [0, P) from residues mod each p_k.
233
+ */
234
+ class CRTReconstructor {
235
+ /**
236
+ * Create a CRT reconstructor
237
+ * @param {Array<number>} primes - Array of coprime moduli
238
+ */
239
+ constructor(primes) {
240
+ this.primes = primes;
241
+ this.K = primes.length;
242
+ this.P = primes.reduce((a, b) => a * b, 1);
243
+
244
+ // Precompute CRT coefficients: M_k = P/p_k and M_k^{-1} mod p_k
245
+ this.coefficients = primes.map(p => {
246
+ const Mk = this.P / p;
247
+ const MkInv = modInverse(Mk, p);
248
+ if (MkInv === null) {
249
+ throw new Error(`Cannot compute modular inverse of ${Mk} mod ${p}`);
250
+ }
251
+ return { Mk, MkInv };
252
+ });
253
+ }
254
+
255
+ /**
256
+ * Reconstruct value from expected residues (differentiable)
257
+ * L̂ = Σ_k E[r_k] * M_k * M_k^{-1} mod P
258
+ *
259
+ * @param {Array<number>} residues - Expected residues [E[r_1], ..., E[r_K]]
260
+ * @returns {number} Reconstructed value L̂
261
+ */
262
+ reconstruct(residues) {
263
+ let L = 0;
264
+ for (let k = 0; k < this.K; k++) {
265
+ const { Mk, MkInv } = this.coefficients[k];
266
+ L += residues[k] * Mk * MkInv;
267
+ }
268
+ // Modular reduction (soft for differentiability in practice)
269
+ return ((L % this.P) + this.P) % this.P;
270
+ }
271
+
272
+ /**
273
+ * Compute reconstruction error (distance to nearest valid CRT value)
274
+ * ε(r) = |ℛ(r) - nearest_valid|
275
+ *
276
+ * @param {Array<number>} residues - Expected residues
277
+ * @returns {number} Reconstruction error
278
+ */
279
+ reconstructionError(residues) {
280
+ const L = this.reconstruct(residues);
281
+ const nearestValid = Math.round(L);
282
+ return Math.abs(L - nearestValid);
283
+ }
284
+
285
+ /**
286
+ * Detect if residue tuple is in kernel (obstruction)
287
+ * Ker(ℛ) ≈ { r | ε(r) > τ }
288
+ *
289
+ * @param {Array<number>} residues - Expected residues
290
+ * @param {number} tau - Error threshold
291
+ * @returns {boolean} True if in kernel (obstruction detected)
292
+ */
293
+ detectKernel(residues, tau = 0.1) {
294
+ const epsilon = this.reconstructionError(residues);
295
+ return epsilon > tau;
296
+ }
297
+
298
+ /**
299
+ * Validate that residues are consistent (not in kernel)
300
+ * @param {Array<number>} residues - Expected residues
301
+ * @param {number} tau - Threshold
302
+ * @returns {Object} { valid, error, inKernel }
303
+ */
304
+ validate(residues, tau = 0.1) {
305
+ const error = this.reconstructionError(residues);
306
+ const inKernel = error > tau;
307
+ return {
308
+ valid: !inKernel,
309
+ error,
310
+ inKernel,
311
+ reconstructed: this.reconstruct(residues)
312
+ };
313
+ }
314
+ }
315
+
316
+ // ============================================================================
317
+ // BIRKHOFF PROJECTION: A = Birkhoff(QK^T/√d) ⊙ V
318
+ // ============================================================================
319
+
320
+ /**
321
+ * Birkhoff Polytope Projector
322
+ *
323
+ * Projects attention matrices onto the Birkhoff polytope (set of doubly-stochastic
324
+ * matrices) using the Sinkhorn-Knopp algorithm.
325
+ *
326
+ * A doubly-stochastic matrix has all row sums = 1 and all column sums = 1.
327
+ * This constrains attention to "physical" transitions in the modular algebra.
328
+ */
329
+ class BirkhoffProjector {
330
+ /**
331
+ * Create a Birkhoff projector
332
+ * @param {number} iterations - Number of Sinkhorn iterations
333
+ * @param {number} epsilon - Small constant for numerical stability
334
+ */
335
+ constructor(iterations = 10, epsilon = 1e-10) {
336
+ this.iterations = iterations;
337
+ this.epsilon = epsilon;
338
+ }
339
+
340
+ /**
341
+ * Sinkhorn-Knopp algorithm for doubly-stochastic projection
342
+ * @param {Array<Array<number>>} matrix - Input matrix (non-negative)
343
+ * @returns {Array<Array<number>>} Doubly-stochastic matrix
344
+ */
345
+ project(matrix) {
346
+ const rows = matrix.length;
347
+ const cols = matrix[0].length;
348
+
349
+ // Exponentiate (soft threshold) and ensure positivity
350
+ let P = matrix.map(row => row.map(x => Math.exp(x) + this.epsilon));
351
+
352
+ for (let iter = 0; iter < this.iterations; iter++) {
353
+ // Row normalization: each row sums to 1
354
+ P = P.map(row => {
355
+ const sum = row.reduce((a, b) => a + b, 0);
356
+ return row.map(x => x / (sum + this.epsilon));
357
+ });
358
+
359
+ // Column normalization: each column sums to 1
360
+ const colSums = new Float64Array(cols);
361
+ for (let j = 0; j < cols; j++) {
362
+ for (let i = 0; i < rows; i++) {
363
+ colSums[j] += P[i][j];
364
+ }
365
+ }
366
+
367
+ for (let i = 0; i < rows; i++) {
368
+ for (let j = 0; j < cols; j++) {
369
+ P[i][j] /= (colSums[j] + this.epsilon);
370
+ }
371
+ }
372
+ }
373
+
374
+ return P;
375
+ }
376
+
377
+ /**
378
+ * Birkhoff attention: A = Birkhoff(QK^T/√d) ⊙ V
379
+ * @param {Array<Array<number>>} Q - Query matrix (n x d)
380
+ * @param {Array<Array<number>>} K - Key matrix (m x d)
381
+ * @param {Array<Array<number>>} V - Value matrix (m x d_v)
382
+ * @returns {Array<Array<number>>} Attention output
383
+ */
384
+ attention(Q, K, V) {
385
+ const n = Q.length;
386
+ const m = K.length;
387
+ const d = Q[0].length;
388
+ const scale = 1 / Math.sqrt(d);
389
+
390
+ // Compute QK^T / √d
391
+ const scores = [];
392
+ for (let i = 0; i < n; i++) {
393
+ const row = [];
394
+ for (let j = 0; j < m; j++) {
395
+ let dot = 0;
396
+ for (let k = 0; k < d; k++) {
397
+ dot += Q[i][k] * K[j][k];
398
+ }
399
+ row.push(dot * scale);
400
+ }
401
+ scores.push(row);
402
+ }
403
+
404
+ // Project to Birkhoff polytope
405
+ const A = this.project(scores);
406
+
407
+ // Apply attention to values: A ⊙ V
408
+ const dv = V[0].length;
409
+ const output = [];
410
+ for (let i = 0; i < n; i++) {
411
+ const row = new Float64Array(dv);
412
+ for (let j = 0; j < m; j++) {
413
+ for (let k = 0; k < dv; k++) {
414
+ row[k] += A[i][j] * V[j][k];
415
+ }
416
+ }
417
+ output.push(Array.from(row));
418
+ }
419
+
420
+ return output;
421
+ }
422
+
423
+ /**
424
+ * Check if matrix is doubly-stochastic (within tolerance)
425
+ * @param {Array<Array<number>>} matrix - Matrix to check
426
+ * @param {number} tolerance - Tolerance for sum deviation
427
+ * @returns {Object} { isDoublyStochastic, rowErrors, colErrors }
428
+ */
429
+ validate(matrix, tolerance = 0.01) {
430
+ const rows = matrix.length;
431
+ const cols = matrix[0].length;
432
+
433
+ const rowSums = matrix.map(row => row.reduce((a, b) => a + b, 0));
434
+ const colSums = new Float64Array(cols);
435
+ for (let j = 0; j < cols; j++) {
436
+ for (let i = 0; i < rows; i++) {
437
+ colSums[j] += matrix[i][j];
438
+ }
439
+ }
440
+
441
+ const rowErrors = rowSums.map(s => Math.abs(s - 1));
442
+ const colErrors = Array.from(colSums).map(s => Math.abs(s - 1));
443
+
444
+ const maxRowError = Math.max(...rowErrors);
445
+ const maxColError = Math.max(...colErrors);
446
+
447
+ return {
448
+ isDoublyStochastic: maxRowError < tolerance && maxColError < tolerance,
449
+ rowErrors,
450
+ colErrors,
451
+ maxRowError,
452
+ maxColError
453
+ };
454
+ }
455
+ }
456
+
457
+ // ============================================================================
458
+ // HOMOLOGY LOSS: ℒ_homology = Σ_{cycles ∈ Ker(ℛ)} f(cycle)
459
+ // ============================================================================
460
+
461
+ /**
462
+ * Homology Loss Calculator
463
+ *
464
+ * Computes loss terms for obstruction cycles in the residue constraint graph.
465
+ * A cycle represents a consistency failure that persists under perturbation.
466
+ *
467
+ * f(cycle) = Σ_{r ∈ cycle} σ(ε(r) - τ) * |cycle|^α * β^γ
468
+ */
469
+ class HomologyLoss {
470
+ /**
471
+ * Create a homology loss calculator
472
+ * @param {Object} options - Configuration
473
+ * @param {number} options.tau - Kernel detection threshold
474
+ * @param {number} options.alpha - Cycle length exponent
475
+ * @param {number} options.beta - Residue contribution weight
476
+ * @param {number} options.gamma - Residue exponent
477
+ * @param {number} options.lambda - Overall homology loss weight
478
+ */
479
+ constructor(options = {}) {
480
+ this.tau = options.tau ?? 0.1;
481
+ this.alpha = options.alpha ?? 1.0;
482
+ this.beta = options.beta ?? 1.0;
483
+ this.gamma = options.gamma ?? 0.5;
484
+ this.lambda = options.lambda ?? 1.0;
485
+ }
486
+
487
+ /**
488
+ * Sigmoid activation
489
+ * @param {number} x - Input
490
+ * @returns {number} σ(x)
491
+ */
492
+ sigmoid(x) {
493
+ return 1 / (1 + Math.exp(-x));
494
+ }
495
+
496
+ /**
497
+ * Detect cycles in residue space from a batch of residue tuples
498
+ * Groups consecutive kernel points as cycles
499
+ *
500
+ * @param {Array<Array<number>>} residueBatch - Batch of residue tuples
501
+ * @param {CRTReconstructor} crt - CRT reconstructor
502
+ * @returns {Array<Array<Object>>} Detected cycles
503
+ */
504
+ detectCycles(residueBatch, crt) {
505
+ const cycles = [];
506
+ let currentCycle = [];
507
+
508
+ for (let i = 0; i < residueBatch.length; i++) {
509
+ const residues = residueBatch[i];
510
+ const inKernel = crt.detectKernel(residues, this.tau);
511
+
512
+ if (inKernel) {
513
+ currentCycle.push({
514
+ index: i,
515
+ residues,
516
+ error: crt.reconstructionError(residues)
517
+ });
518
+ } else if (currentCycle.length > 0) {
519
+ cycles.push(currentCycle);
520
+ currentCycle = [];
521
+ }
522
+ }
523
+
524
+ // Don't forget the last cycle
525
+ if (currentCycle.length > 0) {
526
+ cycles.push(currentCycle);
527
+ }
528
+
529
+ return cycles;
530
+ }
531
+
532
+ /**
533
+ * Compute loss for a single cycle
534
+ * f(cycle) = Σ_{r ∈ cycle} σ(ε(r) - τ) * |cycle|^α * β^γ
535
+ *
536
+ * @param {Array<Object>} cycle - Points in the cycle
537
+ * @returns {number} Cycle loss
538
+ */
539
+ cycleLoss(cycle) {
540
+ const cycleLength = cycle.length;
541
+ let loss = 0;
542
+
543
+ for (const point of cycle) {
544
+ const sigmaEpsilon = this.sigmoid(point.error - this.tau);
545
+ loss += sigmaEpsilon * Math.pow(cycleLength, this.alpha) *
546
+ Math.pow(this.beta, this.gamma);
547
+ }
548
+
549
+ return loss;
550
+ }
551
+
552
+ /**
553
+ * Compute total homology loss over a batch
554
+ * ℒ_homology = Σ_{cycles} f(cycle)
555
+ *
556
+ * @param {Array<Array<number>>} residueBatch - Batch of residue tuples
557
+ * @param {CRTReconstructor} crt - CRT reconstructor
558
+ * @returns {Object} { loss, cycles, details }
559
+ */
560
+ compute(residueBatch, crt) {
561
+ const cycles = this.detectCycles(residueBatch, crt);
562
+
563
+ let totalLoss = 0;
564
+ const details = [];
565
+
566
+ for (const cycle of cycles) {
567
+ const loss = this.cycleLoss(cycle);
568
+ totalLoss += loss;
569
+ details.push({
570
+ length: cycle.length,
571
+ loss,
572
+ points: cycle.map(p => p.index),
573
+ meanError: cycle.reduce((s, p) => s + p.error, 0) / cycle.length
574
+ });
575
+ }
576
+
577
+ return {
578
+ loss: this.lambda * totalLoss,
579
+ cycles: cycles.length,
580
+ details,
581
+ totalPoints: cycles.reduce((s, c) => s + c.length, 0)
582
+ };
583
+ }
584
+
585
+ /**
586
+ * Compute cycle persistence: max_error - min_error across cycle
587
+ * @param {Array<Object>} cycle - Points in the cycle
588
+ * @returns {number} Persistence value
589
+ */
590
+ cyclePersistence(cycle) {
591
+ if (cycle.length === 0) return 0;
592
+ const errors = cycle.map(p => p.error);
593
+ return Math.max(...errors) - Math.min(...errors);
594
+ }
595
+
596
+ /**
597
+ * Compute Betti numbers (topological invariants)
598
+ * β_0 = number of connected components in kernel
599
+ * β_1 = number of holes (cycles that don't bound)
600
+ *
601
+ * @param {Array<Array<number>>} residueBatch - Batch of residue tuples
602
+ * @param {CRTReconstructor} crt - CRT reconstructor
603
+ * @returns {Object} { beta0, beta1 }
604
+ */
605
+ computeBettiNumbers(residueBatch, crt) {
606
+ const cycles = this.detectCycles(residueBatch, crt);
607
+
608
+ // β_0: number of connected components (cycles)
609
+ const beta0 = cycles.length;
610
+
611
+ // β_1: estimate from persistent cycles (those with high persistence)
612
+ const persistentThreshold = 0.5;
613
+ let beta1 = 0;
614
+ for (const cycle of cycles) {
615
+ if (this.cyclePersistence(cycle) > persistentThreshold) {
616
+ beta1++;
617
+ }
618
+ }
619
+
620
+ return { beta0, beta1, cycles: cycles.length };
621
+ }
622
+ }
623
+
624
+ // ============================================================================
625
+ // CRT MODULAR LAYER: Integrated CRT + Birkhoff Attention
626
+ // ============================================================================
627
+
628
+ /**
629
+ * CRT Modular Layer
630
+ *
631
+ * Combines residue encoding, CRT reconstruction, Birkhoff attention,
632
+ * and homology loss into a single differentiable layer.
633
+ */
634
+ class CRTModularLayer {
635
+ /**
636
+ * Create a CRT modular layer
637
+ * @param {Array<number>} primes - Coprime moduli
638
+ * @param {number} hiddenDim - Hidden dimension
639
+ * @param {Object} options - Configuration
640
+ */
641
+ constructor(primes, hiddenDim, options = {}) {
642
+ this.encoder = new ResidueEncoder(primes, hiddenDim, options);
643
+ this.crt = new CRTReconstructor(primes);
644
+ this.birkhoff = new BirkhoffProjector(options.sinkhornIterations || 10);
645
+ this.homology = new HomologyLoss(options.homology || {});
646
+
647
+ this.primes = primes;
648
+ this.hiddenDim = hiddenDim;
649
+ }
650
+
651
+ /**
652
+ * Forward pass through the CRT modular layer
653
+ *
654
+ * @param {Array<number>|Float64Array} h - Hidden vector
655
+ * @param {Array<Array<number>>} Q - Query matrix (optional)
656
+ * @param {Array<Array<number>>} K - Key matrix (optional)
657
+ * @param {Array<Array<number>>} V - Value matrix (optional)
658
+ * @returns {Object} Forward pass result
659
+ */
660
+ forward(h, Q = null, K = null, V = null) {
661
+ // 1. Encode to residue distributions
662
+ const residueDistributions = this.encoder.encode(h);
663
+ const expectedResidues = this.encoder.expectedResidues(residueDistributions);
664
+
665
+ // 2. CRT reconstruction
666
+ const L = this.crt.reconstruct(expectedResidues);
667
+ const inKernel = this.crt.detectKernel(expectedResidues);
668
+ const reconstructionError = this.crt.reconstructionError(expectedResidues);
669
+
670
+ // 3. Birkhoff attention (if Q, K, V provided)
671
+ let attention = null;
672
+ if (Q && K && V) {
673
+ attention = this.birkhoff.attention(Q, K, V);
674
+ }
675
+
676
+ // 4. Compute coherence (inverse of being in kernel)
677
+ const coherence = 1 - Math.min(1, reconstructionError / this.homology.tau);
678
+
679
+ return {
680
+ residues: residueDistributions,
681
+ expectedResidues,
682
+ latent: L,
683
+ inKernel,
684
+ reconstructionError,
685
+ coherence,
686
+ attention,
687
+ P: this.crt.P
688
+ };
689
+ }
690
+
691
+ /**
692
+ * Forward pass for a batch, computing homology loss
693
+ *
694
+ * @param {Array<Array<number>>} hBatch - Batch of hidden vectors
695
+ * @param {Array<number>|null} targets - Target values (for MSE loss)
696
+ * @returns {Object} Batch forward result with losses
697
+ */
698
+ forwardBatch(hBatch, targets = null) {
699
+ const results = hBatch.map(h => this.forward(h));
700
+ const residueBatch = results.map(r => r.expectedResidues);
701
+
702
+ // Compute homology loss
703
+ const homologyResult = this.homology.compute(residueBatch, this.crt);
704
+
705
+ // Compute MSE loss if targets provided
706
+ let mseLoss = 0;
707
+ if (targets) {
708
+ for (let i = 0; i < results.length; i++) {
709
+ const diff = results[i].latent - targets[i];
710
+ mseLoss += diff * diff;
711
+ }
712
+ mseLoss /= results.length;
713
+ }
714
+
715
+ // Total loss
716
+ const totalLoss = mseLoss + homologyResult.loss;
717
+
718
+ return {
719
+ results,
720
+ homologyLoss: homologyResult.loss,
721
+ mseLoss,
722
+ totalLoss,
723
+ homologyDetails: homologyResult,
724
+ bettiNumbers: this.homology.computeBettiNumbers(residueBatch, this.crt)
725
+ };
726
+ }
727
+
728
+ /**
729
+ * Integration with tinyaleph SparsePrimeState
730
+ * @param {SparsePrimeState} state - Sparse prime state
731
+ * @returns {Object} Forward pass result
732
+ */
733
+ forwardFromPrimeState(state) {
734
+ const residues = this.encoder.encodeFromPrimeState(state);
735
+ const expectedResidues = this.encoder.expectedResidues(residues);
736
+ const L = this.crt.reconstruct(expectedResidues);
737
+
738
+ return {
739
+ residues,
740
+ expectedResidues,
741
+ latent: L,
742
+ inKernel: this.crt.detectKernel(expectedResidues),
743
+ reconstructionError: this.crt.reconstructionError(expectedResidues),
744
+ coherence: 1 - Math.min(1, this.crt.reconstructionError(expectedResidues) / this.homology.tau)
745
+ };
746
+ }
747
+ }
748
+
749
+ // ============================================================================
750
+ // CRT-FUSED ATTENTION: L̂' = CRT_Fuse({A_k, r_k})
751
+ // ============================================================================
752
+
753
+ /**
754
+ * CRT-Fused Modular Attention
755
+ *
756
+ * Combines per-modulus Birkhoff attention with CRT reconstruction.
757
+ * Each prime modulus gets its own attention head, and results are fused via CRT.
758
+ */
759
+ class CRTFusedAttention {
760
+ /**
761
+ * Create CRT-fused attention
762
+ * @param {Array<number>} primes - Coprime moduli (one per attention head)
763
+ * @param {number} hiddenDim - Hidden dimension
764
+ * @param {number} headDim - Per-head dimension
765
+ * @param {Object} options - Configuration
766
+ */
767
+ constructor(primes, hiddenDim, headDim, options = {}) {
768
+ this.primes = primes;
769
+ this.K = primes.length;
770
+ this.hiddenDim = hiddenDim;
771
+ this.headDim = headDim;
772
+
773
+ this.crt = new CRTReconstructor(primes);
774
+ this.birkhoff = new BirkhoffProjector(options.sinkhornIterations || 10);
775
+ this.homology = new HomologyLoss(options.homology || {});
776
+
777
+ // Per-modulus projection matrices (Q_k, K_k, V_k)
778
+ this.projections = primes.map(p => ({
779
+ Q: this._initMatrix(hiddenDim, headDim),
780
+ K: this._initMatrix(hiddenDim, headDim),
781
+ V: this._initMatrix(hiddenDim, headDim)
782
+ }));
783
+ }
784
+
785
+ /**
786
+ * Initialize a weight matrix
787
+ * @private
788
+ */
789
+ _initMatrix(rows, cols, scale = 0.1) {
790
+ const matrix = [];
791
+ for (let i = 0; i < rows; i++) {
792
+ const row = new Float64Array(cols);
793
+ for (let j = 0; j < cols; j++) {
794
+ row[j] = (Math.random() - 0.5) * 2 * scale;
795
+ }
796
+ matrix.push(Array.from(row));
797
+ }
798
+ return matrix;
799
+ }
800
+
801
+ /**
802
+ * Matrix multiplication
803
+ * @private
804
+ */
805
+ _matMul(A, B) {
806
+ const m = A.length;
807
+ const n = B[0].length;
808
+ const k = A[0].length;
809
+
810
+ const result = [];
811
+ for (let i = 0; i < m; i++) {
812
+ const row = new Float64Array(n);
813
+ for (let j = 0; j < n; j++) {
814
+ for (let l = 0; l < k; l++) {
815
+ row[j] += A[i][l] * B[l][j];
816
+ }
817
+ }
818
+ result.push(Array.from(row));
819
+ }
820
+ return result;
821
+ }
822
+
823
+ /**
824
+ * Project input through per-modulus heads
825
+ * @param {Array<Array<number>>} X - Input sequence (seq_len x hiddenDim)
826
+ * @param {number} k - Head index
827
+ * @returns {Object} { Q, K, V } projections
828
+ */
829
+ projectHead(X, k) {
830
+ const proj = this.projections[k];
831
+ return {
832
+ Q: this._matMul(X, proj.Q),
833
+ K: this._matMul(X, proj.K),
834
+ V: this._matMul(X, proj.V)
835
+ };
836
+ }
837
+
838
+ /**
839
+ * Forward pass: Per-modulus Birkhoff attention fused via CRT
840
+ *
841
+ * For each modulus k:
842
+ * A_k = Birkhoff(Q_k K_k^T / √d) ⊙ V_k
843
+ * r_k = residue encoding of A_k
844
+ *
845
+ * Fused output: L̂' = CRT_Fuse({A_k, r_k})
846
+ *
847
+ * @param {Array<Array<number>>} X - Input sequence (seq_len x hiddenDim)
848
+ * @returns {Object} Forward result with fused attention
849
+ */
850
+ forward(X) {
851
+ const seqLen = X.length;
852
+ const perHeadOutputs = [];
853
+ const residueTuples = [];
854
+
855
+ // Process each modular head
856
+ for (let k = 0; k < this.K; k++) {
857
+ const { Q, K, V } = this.projectHead(X, k);
858
+
859
+ // Birkhoff attention for this head
860
+ const A_k = this.birkhoff.attention(Q, K, V);
861
+ perHeadOutputs.push(A_k);
862
+
863
+ // Compute residue from attention output (sum modulo p_k)
864
+ const residue = A_k.reduce((sum, row) => {
865
+ const rowSum = row.reduce((a, b) => a + b, 0);
866
+ return (sum + rowSum) % this.primes[k];
867
+ }, 0);
868
+ residueTuples.push(residue);
869
+ }
870
+
871
+ // CRT reconstruction
872
+ const L = this.crt.reconstruct(residueTuples);
873
+ const inKernel = this.crt.detectKernel(residueTuples);
874
+ const coherence = 1 - Math.min(1, this.crt.reconstructionError(residueTuples) / 0.1);
875
+
876
+ // Fused output: weighted average of per-head outputs
877
+ const fusedOutput = [];
878
+ for (let i = 0; i < seqLen; i++) {
879
+ const row = new Float64Array(this.headDim);
880
+ for (let k = 0; k < this.K; k++) {
881
+ const weight = 1 / this.K; // Uniform weighting (can be learned)
882
+ for (let j = 0; j < this.headDim; j++) {
883
+ row[j] += weight * (perHeadOutputs[k][i]?.[j] || 0);
884
+ }
885
+ }
886
+ fusedOutput.push(Array.from(row));
887
+ }
888
+
889
+ return {
890
+ output: fusedOutput,
891
+ perHeadOutputs,
892
+ residues: residueTuples,
893
+ latent: L,
894
+ inKernel,
895
+ coherence,
896
+ P: this.crt.P
897
+ };
898
+ }
899
+ }
900
+
901
+ // ============================================================================
902
+ // COPRIME SELECTOR: Choose optimal coprime moduli for CRT
903
+ // ============================================================================
904
+
905
+ /**
906
+ * Select K coprime moduli for CRT with desired product P
907
+ *
908
+ * Strategy: Use first K primes for maximum P with minimum K
909
+ */
910
+ class CoprimeSelector {
911
+ /**
912
+ * Select K coprime moduli from available primes
913
+ * @param {number} K - Number of moduli
914
+ * @param {Object} options - Configuration
915
+ */
916
+ constructor(K = 4, options = {}) {
917
+ this.K = K;
918
+ this.minPrime = options.minPrime || 2;
919
+ this.maxPrime = options.maxPrime || 100;
920
+ }
921
+
922
+ /**
923
+ * Get K smallest primes as moduli
924
+ * @returns {Array<number>} Array of K primes
925
+ */
926
+ selectMinimal() {
927
+ return firstNPrimes(this.K);
928
+ }
929
+
930
+ /**
931
+ * Select primes to achieve target product P
932
+ * @param {number} targetP - Target product
933
+ * @returns {Array<number>|null} Primes achieving closest to targetP
934
+ */
935
+ selectForProduct(targetP) {
936
+ const primes = firstNPrimes(20);
937
+ let bestPrimes = null;
938
+ let bestProduct = 0;
939
+
940
+ // Greedy selection
941
+ const selected = [];
942
+ let product = 1;
943
+
944
+ for (const p of primes) {
945
+ if (product * p <= targetP) {
946
+ selected.push(p);
947
+ product *= p;
948
+ }
949
+ if (selected.length >= this.K) break;
950
+ }
951
+
952
+ return selected.length > 0 ? selected : null;
953
+ }
954
+
955
+ /**
956
+ * Select primes for specific domain (e.g., cryptographic sizes)
957
+ * @param {string} domain - Domain name
958
+ * @returns {Array<number>} Recommended primes
959
+ */
960
+ selectForDomain(domain) {
961
+ const domains = {
962
+ 'small': [2, 3, 5, 7], // P = 210
963
+ 'medium': [5, 7, 11, 13], // P = 5005
964
+ 'large': [11, 13, 17, 19], // P = 46189
965
+ 'semantic': [2, 3, 5, 7, 11], // P = 2310 (good for semantic)
966
+ 'temporal': [3, 5, 7, 11, 13] // P = 15015 (good for sequences)
967
+ };
968
+
969
+ return domains[domain] || domains['small'];
970
+ }
971
+ }
972
+
973
+ // ============================================================================
974
+ // EXPORTS
975
+ // ============================================================================
976
+
977
+ module.exports = {
978
+ // Modular arithmetic utilities
979
+ extendedGCD,
980
+ modInverse,
981
+ areCoprime,
982
+ softmax,
983
+
984
+ // Core classes
985
+ ResidueEncoder,
986
+ CRTReconstructor,
987
+ BirkhoffProjector,
988
+ HomologyLoss,
989
+ CRTModularLayer,
990
+ CRTFusedAttention,
991
+ CoprimeSelector,
992
+
993
+ // Factory functions
994
+ createCRTLayer: (primes, hiddenDim, options = {}) =>
995
+ new CRTModularLayer(primes, hiddenDim, options),
996
+
997
+ createFusedAttention: (primes, hiddenDim, headDim, options = {}) =>
998
+ new CRTFusedAttention(primes, hiddenDim, headDim, options),
999
+
1000
+ // Default configurations
1001
+ DEFAULT_PRIMES_SMALL: [2, 3, 5, 7],
1002
+ DEFAULT_PRIMES_MEDIUM: [5, 7, 11, 13],
1003
+ DEFAULT_PRIMES_SEMANTIC: [2, 3, 5, 7, 11]
1004
+ };