@aleph-ai/tinyaleph 1.0.2 → 1.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.
@@ -0,0 +1,770 @@
1
+ /**
2
+ * Extended Synchronization Models
3
+ *
4
+ * Advanced Kuramoto-family models extending the base KuramotoModel:
5
+ * - NetworkKuramoto: Topology-aware coupling via adjacency matrix
6
+ * - AdaptiveKuramoto: Hebbian plasticity with evolving coupling
7
+ * - SakaguchiKuramoto: Phase frustration with lag parameter α
8
+ * - SmallWorldKuramoto: Watts-Strogatz small-world topology
9
+ * - MultiSystemCoupling: Cross-system synchronization
10
+ */
11
+
12
+ const { KuramotoModel } = require('./kuramoto');
13
+ const { OscillatorBank } = require('./oscillator');
14
+
15
+ /**
16
+ * NetworkKuramoto - Topology-Aware Coupling
17
+ *
18
+ * Uses adjacency matrix A for coupling:
19
+ * dθᵢ/dt = ωᵢ + K Σⱼ Aᵢⱼ sin(θⱼ - θᵢ)
20
+ *
21
+ * Enables modular synchronization respecting semantic neighborhoods.
22
+ */
23
+ class NetworkKuramoto extends KuramotoModel {
24
+ /**
25
+ * @param {number[]} frequencies - Natural frequencies
26
+ * @param {number[][]} adjacency - NxN adjacency matrix (weights or 0/1)
27
+ * @param {number} couplingStrength - Global coupling multiplier
28
+ */
29
+ constructor(frequencies, adjacency = null, couplingStrength = 0.3) {
30
+ super(frequencies, couplingStrength);
31
+
32
+ const N = frequencies.length;
33
+
34
+ // Default to all-to-all if no adjacency provided
35
+ if (adjacency) {
36
+ this.adjacency = adjacency;
37
+ } else {
38
+ this.adjacency = Array(N).fill(null).map(() => Array(N).fill(1));
39
+ // Remove self-connections
40
+ for (let i = 0; i < N; i++) {
41
+ this.adjacency[i][i] = 0;
42
+ }
43
+ }
44
+
45
+ // Precompute degree for normalization
46
+ this.degree = this.adjacency.map(row => row.reduce((a, b) => a + b, 0));
47
+ }
48
+
49
+ /**
50
+ * Set adjacency from entanglement graph
51
+ * @param {Map} entanglementGraph - Map<prime, Map<prime, {strength}>>
52
+ * @param {number[]} primeList - Ordered list of primes for indexing
53
+ */
54
+ setFromEntanglementGraph(entanglementGraph, primeList) {
55
+ const N = this.oscillators.length;
56
+ const primeToIdx = new Map(primeList.map((p, i) => [p, i]));
57
+
58
+ // Reset adjacency
59
+ this.adjacency = Array(N).fill(null).map(() => Array(N).fill(0));
60
+
61
+ for (const [prime, neighbors] of entanglementGraph) {
62
+ const i = primeToIdx.get(prime);
63
+ if (i === undefined || i >= N) continue;
64
+
65
+ for (const [otherPrime, pair] of neighbors) {
66
+ const j = primeToIdx.get(otherPrime);
67
+ if (j === undefined || j >= N) continue;
68
+
69
+ this.adjacency[i][j] = pair.strength;
70
+ }
71
+ }
72
+
73
+ // Recompute degrees
74
+ this.degree = this.adjacency.map(row => row.reduce((a, b) => a + b, 0));
75
+ }
76
+
77
+ /**
78
+ * Build adjacency from a distance function
79
+ * @param {Function} distFn - (i, j) => distance
80
+ * @param {number} threshold - Connect if distance < threshold
81
+ * @param {boolean} weighted - Use 1/distance as weight
82
+ */
83
+ buildFromDistance(distFn, threshold = Infinity, weighted = false) {
84
+ const N = this.oscillators.length;
85
+ this.adjacency = Array(N).fill(null).map(() => Array(N).fill(0));
86
+
87
+ for (let i = 0; i < N; i++) {
88
+ for (let j = i + 1; j < N; j++) {
89
+ const d = distFn(i, j);
90
+ if (d < threshold) {
91
+ const weight = weighted ? 1 / (1 + d) : 1;
92
+ this.adjacency[i][j] = weight;
93
+ this.adjacency[j][i] = weight;
94
+ }
95
+ }
96
+ }
97
+
98
+ this.degree = this.adjacency.map(row => row.reduce((a, b) => a + b, 0));
99
+ }
100
+
101
+ /**
102
+ * Network-aware coupling term
103
+ */
104
+ kuramotoCoupling(osc) {
105
+ const idx = this.oscillators.indexOf(osc);
106
+ if (idx < 0) return 0;
107
+
108
+ let coupling = 0;
109
+ for (let j = 0; j < this.oscillators.length; j++) {
110
+ if (j !== idx && this.adjacency[idx][j] > 0) {
111
+ const other = this.oscillators[j];
112
+ coupling += this.adjacency[idx][j] * Math.sin(other.phase - osc.phase);
113
+ }
114
+ }
115
+
116
+ // Normalize by degree (if nonzero)
117
+ const norm = this.degree[idx] > 0 ? this.degree[idx] : 1;
118
+ return this.K * coupling / norm;
119
+ }
120
+
121
+ /**
122
+ * Get clustering coefficient for a node
123
+ */
124
+ clusteringCoefficient(idx) {
125
+ const neighbors = [];
126
+ for (let j = 0; j < this.oscillators.length; j++) {
127
+ if (this.adjacency[idx][j] > 0) neighbors.push(j);
128
+ }
129
+
130
+ if (neighbors.length < 2) return 0;
131
+
132
+ let triangles = 0;
133
+ for (let i = 0; i < neighbors.length; i++) {
134
+ for (let j = i + 1; j < neighbors.length; j++) {
135
+ if (this.adjacency[neighbors[i]][neighbors[j]] > 0) {
136
+ triangles++;
137
+ }
138
+ }
139
+ }
140
+
141
+ const possible = neighbors.length * (neighbors.length - 1) / 2;
142
+ return triangles / possible;
143
+ }
144
+
145
+ /**
146
+ * Average clustering coefficient
147
+ */
148
+ averageClustering() {
149
+ let sum = 0;
150
+ for (let i = 0; i < this.oscillators.length; i++) {
151
+ sum += this.clusteringCoefficient(i);
152
+ }
153
+ return sum / this.oscillators.length;
154
+ }
155
+
156
+ /**
157
+ * Network modularity based on synchronization
158
+ * Returns array of synchronized clusters
159
+ */
160
+ findClusters(phaseThreshold = 0.3) {
161
+ const N = this.oscillators.length;
162
+ const visited = new Array(N).fill(false);
163
+ const clusters = [];
164
+
165
+ for (let i = 0; i < N; i++) {
166
+ if (visited[i]) continue;
167
+
168
+ const cluster = [i];
169
+ visited[i] = true;
170
+ const queue = [i];
171
+
172
+ while (queue.length > 0) {
173
+ const current = queue.shift();
174
+
175
+ for (let j = 0; j < N; j++) {
176
+ if (!visited[j] && this.adjacency[current][j] > 0) {
177
+ const phaseDiff = Math.abs(this.oscillators[current].phase - this.oscillators[j].phase);
178
+ const wrapped = Math.min(phaseDiff, 2 * Math.PI - phaseDiff);
179
+
180
+ if (wrapped < phaseThreshold) {
181
+ visited[j] = true;
182
+ cluster.push(j);
183
+ queue.push(j);
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ clusters.push(cluster);
190
+ }
191
+
192
+ return clusters;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * AdaptiveKuramoto - Hebbian Plasticity
198
+ *
199
+ * Coupling strengths evolve based on synchronization:
200
+ * dθᵢ/dt = ωᵢ + (1/N) Σⱼ Kᵢⱼ sin(θⱼ - θᵢ)
201
+ * dKᵢⱼ/dt = ε(cos(θⱼ - θᵢ) - Kᵢⱼ)
202
+ *
203
+ * "Concepts that sync together link together"
204
+ */
205
+ class AdaptiveKuramoto extends NetworkKuramoto {
206
+ /**
207
+ * @param {number[]} frequencies - Natural frequencies
208
+ * @param {number} couplingStrength - Initial global coupling
209
+ * @param {number} learningRate - Plasticity rate ε
210
+ */
211
+ constructor(frequencies, couplingStrength = 0.3, learningRate = 0.01) {
212
+ // Start with all-to-all small initial coupling
213
+ const N = frequencies.length;
214
+ const initialAdjacency = Array(N).fill(null).map((_, i) =>
215
+ Array(N).fill(null).map((_, j) => i === j ? 0 : couplingStrength)
216
+ );
217
+
218
+ super(frequencies, initialAdjacency, 1.0); // K=1 since coupling is in adjacency
219
+
220
+ this.epsilon = learningRate;
221
+ this.minCoupling = 0.01;
222
+ this.maxCoupling = 2.0;
223
+
224
+ // Track coupling history for analysis
225
+ this.couplingHistory = [];
226
+ }
227
+
228
+ /**
229
+ * Update coupling strengths based on phase alignment
230
+ */
231
+ adaptCoupling(dt) {
232
+ const N = this.oscillators.length;
233
+
234
+ for (let i = 0; i < N; i++) {
235
+ for (let j = i + 1; j < N; j++) {
236
+ const phaseDiff = this.oscillators[j].phase - this.oscillators[i].phase;
237
+ const correlation = Math.cos(phaseDiff);
238
+
239
+ // Hebbian update: dK/dt = ε(correlation - K)
240
+ const delta = this.epsilon * (correlation - this.adjacency[i][j]) * dt;
241
+
242
+ // Apply with bounds
243
+ const newK = Math.max(this.minCoupling,
244
+ Math.min(this.maxCoupling, this.adjacency[i][j] + delta));
245
+
246
+ this.adjacency[i][j] = newK;
247
+ this.adjacency[j][i] = newK;
248
+ }
249
+ }
250
+
251
+ // Recompute degrees
252
+ this.degree = this.adjacency.map(row => row.reduce((a, b) => a + b, 0));
253
+ }
254
+
255
+ tick(dt) {
256
+ // First evolve phases
257
+ super.tick(dt);
258
+
259
+ // Then adapt coupling
260
+ this.adaptCoupling(dt);
261
+ }
262
+
263
+ /**
264
+ * Get total coupling strength (sum of all edge weights)
265
+ */
266
+ totalCoupling() {
267
+ let sum = 0;
268
+ const N = this.oscillators.length;
269
+ for (let i = 0; i < N; i++) {
270
+ for (let j = i + 1; j < N; j++) {
271
+ sum += this.adjacency[i][j];
272
+ }
273
+ }
274
+ return sum;
275
+ }
276
+
277
+ /**
278
+ * Get coupling matrix snapshot
279
+ */
280
+ getCouplingSnapshot() {
281
+ return this.adjacency.map(row => [...row]);
282
+ }
283
+
284
+ /**
285
+ * Record coupling state for analysis
286
+ */
287
+ recordCouplingHistory() {
288
+ this.couplingHistory.push({
289
+ time: Date.now(),
290
+ totalCoupling: this.totalCoupling(),
291
+ orderParameter: this.orderParameter(),
292
+ snapshot: this.getCouplingSnapshot()
293
+ });
294
+
295
+ // Limit history size
296
+ if (this.couplingHistory.length > 1000) {
297
+ this.couplingHistory.shift();
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Reset coupling to uniform initial state
303
+ */
304
+ resetCoupling(value = 0.3) {
305
+ const N = this.oscillators.length;
306
+ for (let i = 0; i < N; i++) {
307
+ for (let j = 0; j < N; j++) {
308
+ this.adjacency[i][j] = i === j ? 0 : value;
309
+ }
310
+ }
311
+ this.degree = this.adjacency.map(row => row.reduce((a, b) => a + b, 0));
312
+ this.couplingHistory = [];
313
+ }
314
+ }
315
+
316
+ /**
317
+ * SakaguchiKuramoto - Phase Frustration
318
+ *
319
+ * Adds phase lag parameter α:
320
+ * dθᵢ/dt = ωᵢ + (K/N) Σⱼ sin(θⱼ - θᵢ - α)
321
+ *
322
+ * Creates chimera states where some oscillators sync while others don't.
323
+ * Models cognitive dissonance, competing interpretations.
324
+ */
325
+ class SakaguchiKuramoto extends KuramotoModel {
326
+ /**
327
+ * @param {number[]} frequencies - Natural frequencies
328
+ * @param {number} couplingStrength - Coupling K
329
+ * @param {number} phaseLag - Frustration parameter α (radians)
330
+ */
331
+ constructor(frequencies, couplingStrength = 0.3, phaseLag = 0) {
332
+ super(frequencies, couplingStrength);
333
+ this.alpha = phaseLag;
334
+ }
335
+
336
+ /**
337
+ * Set phase lag dynamically
338
+ */
339
+ setPhaseLag(alpha) {
340
+ this.alpha = alpha;
341
+ }
342
+
343
+ /**
344
+ * Frustrated coupling term
345
+ */
346
+ kuramotoCoupling(osc) {
347
+ let coupling = 0;
348
+ for (const other of this.oscillators) {
349
+ if (other !== osc) {
350
+ // sin(θⱼ - θᵢ - α) = sin(θⱼ - θᵢ)cos(α) - cos(θⱼ - θᵢ)sin(α)
351
+ const diff = other.phase - osc.phase;
352
+ coupling += Math.sin(diff - this.alpha);
353
+ }
354
+ }
355
+ return this.K * coupling / this.oscillators.length;
356
+ }
357
+
358
+ /**
359
+ * Check for chimera state (partial synchronization)
360
+ * Returns ratio of synchronized oscillators
361
+ */
362
+ chimeraRatio(syncThreshold = 0.3) {
363
+ const meanPh = this.meanPhase();
364
+ let syncCount = 0;
365
+
366
+ for (const osc of this.oscillators) {
367
+ const diff = Math.abs(osc.phase - meanPh);
368
+ const wrapped = Math.min(diff, 2 * Math.PI - diff);
369
+ if (wrapped < syncThreshold) {
370
+ syncCount++;
371
+ }
372
+ }
373
+
374
+ return syncCount / this.oscillators.length;
375
+ }
376
+
377
+ /**
378
+ * Classify the system state
379
+ */
380
+ classifyState() {
381
+ const r = this.orderParameter();
382
+ const chimera = this.chimeraRatio();
383
+
384
+ if (r > 0.8) return 'synchronized';
385
+ if (r < 0.2) return 'incoherent';
386
+ if (chimera > 0.3 && chimera < 0.7) return 'chimera';
387
+ return 'partial';
388
+ }
389
+
390
+ /**
391
+ * Find critical phase lag for chimera formation
392
+ * (Approximate - depends on frequency distribution)
393
+ */
394
+ static criticalPhaseLag(couplingStrength) {
395
+ // For Lorentzian distribution, chimera appears around α ≈ π/2 - arctan(1/K)
396
+ return Math.PI / 2 - Math.atan(1 / couplingStrength);
397
+ }
398
+ }
399
+
400
+ /**
401
+ * SmallWorldKuramoto - Watts-Strogatz Topology
402
+ *
403
+ * Creates small-world network with:
404
+ * - High clustering (local neighborhoods)
405
+ * - Short average path length (long-range shortcuts)
406
+ *
407
+ * Ideal for modular semantic synchronization.
408
+ */
409
+ class SmallWorldKuramoto extends NetworkKuramoto {
410
+ /**
411
+ * @param {number[]} frequencies - Natural frequencies
412
+ * @param {number} k - Each node connected to k nearest neighbors
413
+ * @param {number} p - Rewiring probability (0 = ring, 1 = random)
414
+ * @param {number} couplingStrength - Coupling K
415
+ */
416
+ constructor(frequencies, k = 4, p = 0.1, couplingStrength = 0.3) {
417
+ const N = frequencies.length;
418
+ const adjacency = SmallWorldKuramoto.wattsStrogatz(N, k, p);
419
+
420
+ super(frequencies, adjacency, couplingStrength);
421
+
422
+ this.k = k;
423
+ this.p = p;
424
+ }
425
+
426
+ /**
427
+ * Generate Watts-Strogatz small-world graph
428
+ */
429
+ static wattsStrogatz(N, k, p) {
430
+ // Start with ring lattice
431
+ const adj = Array(N).fill(null).map(() => Array(N).fill(0));
432
+
433
+ // Connect each node to k/2 neighbors on each side
434
+ const halfK = Math.floor(k / 2);
435
+ for (let i = 0; i < N; i++) {
436
+ for (let j = 1; j <= halfK; j++) {
437
+ const neighbor = (i + j) % N;
438
+ adj[i][neighbor] = 1;
439
+ adj[neighbor][i] = 1;
440
+ }
441
+ }
442
+
443
+ // Rewire edges with probability p
444
+ for (let i = 0; i < N; i++) {
445
+ for (let j = 1; j <= halfK; j++) {
446
+ if (Math.random() < p) {
447
+ const oldNeighbor = (i + j) % N;
448
+
449
+ // Remove old edge
450
+ adj[i][oldNeighbor] = 0;
451
+ adj[oldNeighbor][i] = 0;
452
+
453
+ // Add new random edge (avoiding self and duplicates)
454
+ let newNeighbor;
455
+ do {
456
+ newNeighbor = Math.floor(Math.random() * N);
457
+ } while (newNeighbor === i || adj[i][newNeighbor] > 0);
458
+
459
+ adj[i][newNeighbor] = 1;
460
+ adj[newNeighbor][i] = 1;
461
+ }
462
+ }
463
+ }
464
+
465
+ return adj;
466
+ }
467
+
468
+ /**
469
+ * Regenerate the network with new parameters
470
+ */
471
+ regenerate(k = this.k, p = this.p) {
472
+ this.k = k;
473
+ this.p = p;
474
+ this.adjacency = SmallWorldKuramoto.wattsStrogatz(this.oscillators.length, k, p);
475
+ this.degree = this.adjacency.map(row => row.reduce((a, b) => a + b, 0));
476
+ }
477
+
478
+ /**
479
+ * Estimate average path length (BFS from each node)
480
+ */
481
+ averagePathLength() {
482
+ const N = this.oscillators.length;
483
+ let totalPath = 0;
484
+ let pairs = 0;
485
+
486
+ for (let source = 0; source < N; source++) {
487
+ const dist = this._bfs(source);
488
+ for (let target = 0; target < N; target++) {
489
+ if (source !== target && dist[target] < Infinity) {
490
+ totalPath += dist[target];
491
+ pairs++;
492
+ }
493
+ }
494
+ }
495
+
496
+ return pairs > 0 ? totalPath / pairs : Infinity;
497
+ }
498
+
499
+ _bfs(source) {
500
+ const N = this.oscillators.length;
501
+ const dist = Array(N).fill(Infinity);
502
+ dist[source] = 0;
503
+ const queue = [source];
504
+
505
+ while (queue.length > 0) {
506
+ const current = queue.shift();
507
+ for (let j = 0; j < N; j++) {
508
+ if (this.adjacency[current][j] > 0 && dist[j] === Infinity) {
509
+ dist[j] = dist[current] + 1;
510
+ queue.push(j);
511
+ }
512
+ }
513
+ }
514
+
515
+ return dist;
516
+ }
517
+
518
+ /**
519
+ * Small-world coefficient σ = (C/C_random) / (L/L_random)
520
+ * σ > 1 indicates small-world properties
521
+ */
522
+ smallWorldCoefficient() {
523
+ const C = this.averageClustering();
524
+ const L = this.averagePathLength();
525
+
526
+ // Random graph approximations
527
+ const N = this.oscillators.length;
528
+ const avgDegree = this.degree.reduce((a, b) => a + b, 0) / N;
529
+ const C_random = avgDegree / N;
530
+ const L_random = Math.log(N) / Math.log(avgDegree);
531
+
532
+ if (C_random === 0 || L_random === 0) return 0;
533
+
534
+ return (C / C_random) / (L / L_random);
535
+ }
536
+ }
537
+
538
+ /**
539
+ * MultiSystemCoupling - Cross-System Synchronization
540
+ *
541
+ * Couples multiple KuramotoModel systems together:
542
+ * dθᵢ^(a)/dt = ... + Σ_b G_ab (r^(b) sin(ψ^(b) - θᵢ^(a)))
543
+ *
544
+ * Models:
545
+ * - Multi-agent semantic alignment
546
+ * - Hierarchical concept organization
547
+ * - Cross-domain knowledge transfer
548
+ */
549
+ class MultiSystemCoupling {
550
+ /**
551
+ * @param {KuramotoModel[]} systems - Array of Kuramoto systems
552
+ * @param {number[][]} coupling - System coupling matrix G
553
+ */
554
+ constructor(systems, coupling = null) {
555
+ this.systems = systems;
556
+
557
+ // Default: weak uniform inter-system coupling
558
+ if (coupling) {
559
+ this.G = coupling;
560
+ } else {
561
+ const M = systems.length;
562
+ this.G = Array(M).fill(null).map(() => Array(M).fill(0.1));
563
+ for (let i = 0; i < M; i++) {
564
+ this.G[i][i] = 0; // No self-coupling
565
+ }
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Set inter-system coupling strength
571
+ */
572
+ setInterCoupling(i, j, strength) {
573
+ this.G[i][j] = strength;
574
+ this.G[j][i] = strength;
575
+ }
576
+
577
+ /**
578
+ * Get global order parameters for each system
579
+ */
580
+ orderParameters() {
581
+ return this.systems.map(sys => ({
582
+ r: sys.orderParameter(),
583
+ psi: sys.meanPhase()
584
+ }));
585
+ }
586
+
587
+ /**
588
+ * Inter-system coupling force on oscillator
589
+ */
590
+ interSystemCoupling(systemIdx, osc) {
591
+ let coupling = 0;
592
+
593
+ for (let b = 0; b < this.systems.length; b++) {
594
+ if (b === systemIdx || this.G[systemIdx][b] === 0) continue;
595
+
596
+ const other = this.systems[b];
597
+ const r_b = other.orderParameter();
598
+ const psi_b = other.meanPhase();
599
+
600
+ // Mean-field coupling from system b
601
+ coupling += this.G[systemIdx][b] * r_b * Math.sin(psi_b - osc.phase);
602
+ }
603
+
604
+ return coupling;
605
+ }
606
+
607
+ /**
608
+ * Tick all systems with inter-system coupling
609
+ */
610
+ tick(dt) {
611
+ // Store inter-system coupling terms for each oscillator
612
+ const interCouplings = this.systems.map((sys, sIdx) =>
613
+ sys.oscillators.map(osc => this.interSystemCoupling(sIdx, osc) * dt)
614
+ );
615
+
616
+ // Tick each system with combined coupling
617
+ for (let s = 0; s < this.systems.length; s++) {
618
+ const sys = this.systems[s];
619
+
620
+ // Custom tick that includes inter-system coupling
621
+ for (let i = 0; i < sys.oscillators.length; i++) {
622
+ const osc = sys.oscillators[i];
623
+ const intra = sys.kuramotoCoupling(osc) * dt;
624
+ const inter = interCouplings[s][i];
625
+
626
+ osc.tick(dt, intra + inter);
627
+ osc.decay(0.02, dt);
628
+ }
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Global synchronization across all systems
634
+ */
635
+ globalOrderParameter() {
636
+ let sx = 0, sy = 0, total = 0;
637
+
638
+ for (const sys of this.systems) {
639
+ for (const osc of sys.oscillators) {
640
+ sx += osc.amplitude * Math.cos(osc.phase);
641
+ sy += osc.amplitude * Math.sin(osc.phase);
642
+ total++;
643
+ }
644
+ }
645
+
646
+ return total > 0 ? Math.sqrt((sx/total)**2 + (sy/total)**2) : 0;
647
+ }
648
+
649
+ /**
650
+ * Inter-system synchronization matrix
651
+ * Returns phase coherence between each pair of systems
652
+ */
653
+ interSystemCoherence() {
654
+ const M = this.systems.length;
655
+ const coherence = Array(M).fill(null).map(() => Array(M).fill(0));
656
+
657
+ for (let a = 0; a < M; a++) {
658
+ const psi_a = this.systems[a].meanPhase();
659
+ for (let b = a + 1; b < M; b++) {
660
+ const psi_b = this.systems[b].meanPhase();
661
+ const c = Math.cos(psi_a - psi_b);
662
+ coherence[a][b] = c;
663
+ coherence[b][a] = c;
664
+ }
665
+ coherence[a][a] = 1;
666
+ }
667
+
668
+ return coherence;
669
+ }
670
+
671
+ /**
672
+ * Excite oscillators in a specific system
673
+ */
674
+ exciteSystem(systemIdx, primes, primeList, amount = 0.5) {
675
+ if (systemIdx >= 0 && systemIdx < this.systems.length) {
676
+ this.systems[systemIdx].exciteByPrimes(primes, primeList, amount);
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Excite corresponding oscillators across all systems
682
+ */
683
+ exciteAll(primes, primeList, amount = 0.5) {
684
+ for (const sys of this.systems) {
685
+ sys.exciteByPrimes(primes, primeList, amount);
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Reset all systems
691
+ */
692
+ reset() {
693
+ for (const sys of this.systems) {
694
+ sys.reset();
695
+ }
696
+ }
697
+
698
+ /**
699
+ * Get state summary
700
+ */
701
+ getState() {
702
+ return {
703
+ systemCount: this.systems.length,
704
+ orderParameters: this.orderParameters(),
705
+ globalOrder: this.globalOrderParameter(),
706
+ interSystemCoherence: this.interSystemCoherence()
707
+ };
708
+ }
709
+ }
710
+
711
+ /**
712
+ * Create a hierarchical multi-system coupling
713
+ * Lower systems feed into higher ones (bottom-up)
714
+ */
715
+ function createHierarchicalCoupling(frequencies, levels = 3, oscPerLevel = 16) {
716
+ const systems = [];
717
+
718
+ for (let level = 0; level < levels; level++) {
719
+ const levelFreqs = frequencies.slice(0, oscPerLevel).map(f => f * (1 + level * 0.1));
720
+ systems.push(new KuramotoModel(levelFreqs, 0.3 + level * 0.1));
721
+ }
722
+
723
+ // Asymmetric coupling: lower -> higher is stronger
724
+ const G = Array(levels).fill(null).map(() => Array(levels).fill(0));
725
+ for (let i = 0; i < levels; i++) {
726
+ for (let j = 0; j < levels; j++) {
727
+ if (i < j) {
728
+ G[i][j] = 0.2; // Bottom-up
729
+ } else if (i > j) {
730
+ G[i][j] = 0.05; // Top-down (weaker)
731
+ }
732
+ }
733
+ }
734
+
735
+ return new MultiSystemCoupling(systems, G);
736
+ }
737
+
738
+ /**
739
+ * Create a peer-to-peer multi-system coupling
740
+ * All systems have equal symmetric coupling
741
+ */
742
+ function createPeerCoupling(frequencies, numPeers = 3, strength = 0.15) {
743
+ const systems = [];
744
+
745
+ for (let i = 0; i < numPeers; i++) {
746
+ // Each peer has slightly different frequencies (individuality)
747
+ const peerFreqs = frequencies.map(f => f * (1 + (Math.random() - 0.5) * 0.1));
748
+ systems.push(new KuramotoModel(peerFreqs, 0.3));
749
+ }
750
+
751
+ const G = Array(numPeers).fill(null).map(() => Array(numPeers).fill(strength));
752
+ for (let i = 0; i < numPeers; i++) {
753
+ G[i][i] = 0;
754
+ }
755
+
756
+ return new MultiSystemCoupling(systems, G);
757
+ }
758
+
759
+ module.exports = {
760
+ // Core extended models
761
+ NetworkKuramoto,
762
+ AdaptiveKuramoto,
763
+ SakaguchiKuramoto,
764
+ SmallWorldKuramoto,
765
+ MultiSystemCoupling,
766
+
767
+ // Factory functions
768
+ createHierarchicalCoupling,
769
+ createPeerCoupling
770
+ };