@aleph-ai/tinyaleph 1.0.2 → 1.2.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,712 @@
1
+ /**
2
+ * Prime Entanglement Graph
3
+ *
4
+ * Explicit representation of prime relationships that emerge from:
5
+ * - Co-occurrence in semantic contexts
6
+ * - Phase resonance in Kuramoto dynamics
7
+ * - Coherence in hypercomplex states
8
+ *
9
+ * Structure:
10
+ * - Nodes: Prime numbers
11
+ * - Edges: Weighted by resonance strength, phase alignment, co-occurrence
12
+ *
13
+ * Applications:
14
+ * - Semantic relationship tracking
15
+ * - Adaptive coupling for synchronization
16
+ * - Memory consolidation patterns
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const { isPrime, firstNPrimes } = require('./prime');
22
+
23
+ /**
24
+ * Edge data for entanglement between two primes
25
+ */
26
+ class EntanglementEdge {
27
+ constructor(source, target) {
28
+ this.source = source;
29
+ this.target = target;
30
+ this.weight = 0;
31
+ this.phaseAlignment = 0;
32
+ this.cooccurrenceCount = 0;
33
+ this.resonanceStrength = 0;
34
+ this.lastUpdated = Date.now();
35
+ this.createdAt = Date.now();
36
+ }
37
+
38
+ /**
39
+ * Update edge from observation
40
+ * @param {number} strength - Observation strength (0-1)
41
+ * @param {number} phaseCorrelation - Phase correlation (-1 to 1)
42
+ */
43
+ observe(strength, phaseCorrelation = 0) {
44
+ this.cooccurrenceCount++;
45
+
46
+ // Exponential moving average for weight
47
+ const alpha = 0.1;
48
+ this.weight = (1 - alpha) * this.weight + alpha * strength;
49
+ this.phaseAlignment = (1 - alpha) * this.phaseAlignment + alpha * phaseCorrelation;
50
+ this.resonanceStrength = Math.sqrt(this.weight * Math.abs(this.phaseAlignment));
51
+
52
+ this.lastUpdated = Date.now();
53
+ }
54
+
55
+ /**
56
+ * Apply decay to edge weight
57
+ * @param {number} rate - Decay rate (0-1)
58
+ */
59
+ decay(rate) {
60
+ this.weight *= (1 - rate);
61
+ this.resonanceStrength *= (1 - rate);
62
+ }
63
+
64
+ /**
65
+ * Get age in milliseconds
66
+ */
67
+ age() {
68
+ return Date.now() - this.createdAt;
69
+ }
70
+
71
+ /**
72
+ * Get time since last update in milliseconds
73
+ */
74
+ staleness() {
75
+ return Date.now() - this.lastUpdated;
76
+ }
77
+
78
+ toJSON() {
79
+ return {
80
+ source: this.source,
81
+ target: this.target,
82
+ weight: this.weight,
83
+ phaseAlignment: this.phaseAlignment,
84
+ cooccurrenceCount: this.cooccurrenceCount,
85
+ resonanceStrength: this.resonanceStrength,
86
+ age: this.age(),
87
+ staleness: this.staleness()
88
+ };
89
+ }
90
+ }
91
+
92
+ /**
93
+ * PrimeEntanglementGraph - Main graph class
94
+ *
95
+ * Tracks relationships between primes using adjacency list representation.
96
+ */
97
+ class PrimeEntanglementGraph {
98
+ /**
99
+ * @param {number[]} primes - Initial set of primes (or number for first N primes)
100
+ * @param {object} options - Configuration
101
+ * @param {number} [options.decayRate=0.01] - Edge weight decay per decay() call
102
+ * @param {number} [options.pruneThreshold=0.01] - Minimum weight to keep edges
103
+ * @param {number} [options.maxEdgesPerNode=50] - Maximum neighbors per prime
104
+ */
105
+ constructor(primes = 100, options = {}) {
106
+ // Handle number input as "first N primes"
107
+ if (typeof primes === 'number') {
108
+ this.primes = new Set(firstNPrimes(primes));
109
+ } else {
110
+ this.primes = new Set(primes.filter(p => isPrime(p)));
111
+ }
112
+
113
+ this.decayRate = options.decayRate ?? 0.01;
114
+ this.pruneThreshold = options.pruneThreshold ?? 0.01;
115
+ this.maxEdgesPerNode = options.maxEdgesPerNode ?? 50;
116
+
117
+ // Adjacency list: Map<prime, Map<prime, EntanglementEdge>>
118
+ this.adjacency = new Map();
119
+
120
+ // Initialize empty adjacency lists
121
+ for (const p of this.primes) {
122
+ this.adjacency.set(p, new Map());
123
+ }
124
+
125
+ // Statistics
126
+ this.totalObservations = 0;
127
+ this.edgeCount = 0;
128
+ }
129
+
130
+ /**
131
+ * Add a prime to the graph
132
+ * @param {number} p - Prime to add
133
+ */
134
+ addPrime(p) {
135
+ if (!isPrime(p)) return false;
136
+ if (this.primes.has(p)) return false;
137
+
138
+ this.primes.add(p);
139
+ this.adjacency.set(p, new Map());
140
+ return true;
141
+ }
142
+
143
+ /**
144
+ * Get edge between two primes (creates if doesn't exist)
145
+ * @param {number} p1 - First prime
146
+ * @param {number} p2 - Second prime
147
+ * @returns {EntanglementEdge|null} Edge or null if primes not in graph
148
+ */
149
+ getEdge(p1, p2) {
150
+ if (!this.primes.has(p1) || !this.primes.has(p2)) return null;
151
+ if (p1 === p2) return null;
152
+
153
+ // Ensure consistent ordering
154
+ const [source, target] = p1 < p2 ? [p1, p2] : [p2, p1];
155
+
156
+ const neighbors = this.adjacency.get(source);
157
+ if (!neighbors.has(target)) {
158
+ const edge = new EntanglementEdge(source, target);
159
+ neighbors.set(target, edge);
160
+ this.adjacency.get(target).set(source, edge); // Bidirectional reference
161
+ this.edgeCount++;
162
+ }
163
+
164
+ return neighbors.get(target);
165
+ }
166
+
167
+ /**
168
+ * Check if edge exists
169
+ * @param {number} p1 - First prime
170
+ * @param {number} p2 - Second prime
171
+ */
172
+ hasEdge(p1, p2) {
173
+ if (!this.primes.has(p1) || !this.primes.has(p2)) return false;
174
+ const [source, target] = p1 < p2 ? [p1, p2] : [p2, p1];
175
+ return this.adjacency.get(source)?.has(target) ?? false;
176
+ }
177
+
178
+ /**
179
+ * Observe co-occurrence of primes
180
+ *
181
+ * @param {number[]} primes1 - First set of primes
182
+ * @param {number[]} primes2 - Second set of primes (or same as first)
183
+ * @param {number} strength - Observation strength (0-1)
184
+ * @param {Map<number, number>} phases - Optional phase map for correlation
185
+ */
186
+ observe(primes1, primes2 = null, strength = 1.0, phases = null) {
187
+ primes2 = primes2 || primes1;
188
+
189
+ // Filter to known primes
190
+ const set1 = primes1.filter(p => this.primes.has(p));
191
+ const set2 = primes2.filter(p => this.primes.has(p));
192
+
193
+ // Create/update edges for all pairs
194
+ for (const p1 of set1) {
195
+ for (const p2 of set2) {
196
+ if (p1 === p2) continue;
197
+
198
+ const edge = this.getEdge(p1, p2);
199
+ if (edge) {
200
+ // Compute phase correlation if phases provided
201
+ let phaseCorr = 0;
202
+ if (phases && phases.has(p1) && phases.has(p2)) {
203
+ const phase1 = phases.get(p1);
204
+ const phase2 = phases.get(p2);
205
+ phaseCorr = Math.cos(phase1 - phase2);
206
+ }
207
+
208
+ edge.observe(strength, phaseCorr);
209
+ }
210
+ }
211
+ }
212
+
213
+ this.totalObservations++;
214
+ }
215
+
216
+ /**
217
+ * Get neighbors of a prime within k hops
218
+ * @param {number} prime - Source prime
219
+ * @param {number} depth - Maximum hop distance (default 1)
220
+ * @param {number} minWeight - Minimum edge weight to follow
221
+ * @returns {Map<number, {distance: number, pathWeight: number}>}
222
+ */
223
+ neighbors(prime, depth = 1, minWeight = 0) {
224
+ if (!this.primes.has(prime)) return new Map();
225
+
226
+ const result = new Map();
227
+ const visited = new Set([prime]);
228
+ const queue = [{ node: prime, distance: 0, pathWeight: 1 }];
229
+
230
+ while (queue.length > 0) {
231
+ const { node, distance, pathWeight } = queue.shift();
232
+
233
+ if (distance >= depth) continue;
234
+
235
+ const neighbors = this.adjacency.get(node);
236
+ if (!neighbors) continue;
237
+
238
+ for (const [neighbor, edge] of neighbors) {
239
+ if (visited.has(neighbor)) continue;
240
+ if (edge.weight < minWeight) continue;
241
+
242
+ visited.add(neighbor);
243
+ const newPathWeight = pathWeight * edge.weight;
244
+
245
+ result.set(neighbor, {
246
+ distance: distance + 1,
247
+ pathWeight: newPathWeight,
248
+ edge: edge.toJSON()
249
+ });
250
+
251
+ queue.push({
252
+ node: neighbor,
253
+ distance: distance + 1,
254
+ pathWeight: newPathWeight
255
+ });
256
+ }
257
+ }
258
+
259
+ return result;
260
+ }
261
+
262
+ /**
263
+ * Find shortest path between two primes (Dijkstra with inverse weights)
264
+ * @param {number} source - Start prime
265
+ * @param {number} target - End prime
266
+ * @returns {object|null} Path info or null if no path
267
+ */
268
+ shortestPath(source, target) {
269
+ if (!this.primes.has(source) || !this.primes.has(target)) return null;
270
+ if (source === target) return { path: [source], totalWeight: 1, hops: 0 };
271
+
272
+ const distances = new Map();
273
+ const previous = new Map();
274
+ const unvisited = new Set(this.primes);
275
+
276
+ for (const p of this.primes) {
277
+ distances.set(p, Infinity);
278
+ }
279
+ distances.set(source, 0);
280
+
281
+ while (unvisited.size > 0) {
282
+ // Find minimum distance node
283
+ let current = null;
284
+ let minDist = Infinity;
285
+ for (const p of unvisited) {
286
+ const d = distances.get(p);
287
+ if (d < minDist) {
288
+ minDist = d;
289
+ current = p;
290
+ }
291
+ }
292
+
293
+ if (current === null || minDist === Infinity) break;
294
+ if (current === target) break;
295
+
296
+ unvisited.delete(current);
297
+
298
+ // Update neighbors
299
+ const neighbors = this.adjacency.get(current);
300
+ if (!neighbors) continue;
301
+
302
+ for (const [neighbor, edge] of neighbors) {
303
+ if (!unvisited.has(neighbor)) continue;
304
+
305
+ // Distance = inverse of weight (stronger = shorter)
306
+ const edgeDist = edge.weight > 0.001 ? 1 / edge.weight : 1000;
307
+ const alt = distances.get(current) + edgeDist;
308
+
309
+ if (alt < distances.get(neighbor)) {
310
+ distances.set(neighbor, alt);
311
+ previous.set(neighbor, current);
312
+ }
313
+ }
314
+ }
315
+
316
+ // Reconstruct path
317
+ if (!previous.has(target) && source !== target) return null;
318
+
319
+ const path = [target];
320
+ let current = target;
321
+ let totalWeight = 1;
322
+
323
+ while (previous.has(current)) {
324
+ const prev = previous.get(current);
325
+ const edge = this.getEdge(prev, current);
326
+ totalWeight *= edge.weight;
327
+ path.unshift(prev);
328
+ current = prev;
329
+ }
330
+
331
+ return {
332
+ path,
333
+ totalWeight,
334
+ hops: path.length - 1,
335
+ distance: distances.get(target)
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Compute clustering coefficient for a prime
341
+ * Ratio of edges between neighbors to possible edges
342
+ *
343
+ * @param {number} prime - Prime to analyze
344
+ */
345
+ clusteringCoefficient(prime) {
346
+ if (!this.primes.has(prime)) return 0;
347
+
348
+ const neighbors = Array.from(this.adjacency.get(prime)?.keys() || []);
349
+ const k = neighbors.length;
350
+
351
+ if (k < 2) return 0;
352
+
353
+ let triangles = 0;
354
+ for (let i = 0; i < k; i++) {
355
+ for (let j = i + 1; j < k; j++) {
356
+ if (this.hasEdge(neighbors[i], neighbors[j])) {
357
+ triangles++;
358
+ }
359
+ }
360
+ }
361
+
362
+ const possibleTriangles = k * (k - 1) / 2;
363
+ return triangles / possibleTriangles;
364
+ }
365
+
366
+ /**
367
+ * Compute average clustering coefficient
368
+ */
369
+ averageClusteringCoefficient() {
370
+ let sum = 0;
371
+ let count = 0;
372
+
373
+ for (const p of this.primes) {
374
+ const cc = this.clusteringCoefficient(p);
375
+ if (!isNaN(cc)) {
376
+ sum += cc;
377
+ count++;
378
+ }
379
+ }
380
+
381
+ return count > 0 ? sum / count : 0;
382
+ }
383
+
384
+ /**
385
+ * Compute degree centrality for a prime
386
+ */
387
+ degreeCentrality(prime) {
388
+ if (!this.primes.has(prime)) return 0;
389
+ const degree = this.adjacency.get(prime)?.size || 0;
390
+ return degree / (this.primes.size - 1);
391
+ }
392
+
393
+ /**
394
+ * Get top primes by degree centrality
395
+ * @param {number} k - Number of top primes
396
+ */
397
+ topByDegree(k = 10) {
398
+ const centralities = [];
399
+
400
+ for (const p of this.primes) {
401
+ centralities.push({
402
+ prime: p,
403
+ centrality: this.degreeCentrality(p),
404
+ degree: this.adjacency.get(p)?.size || 0
405
+ });
406
+ }
407
+
408
+ return centralities
409
+ .sort((a, b) => b.centrality - a.centrality)
410
+ .slice(0, k);
411
+ }
412
+
413
+ /**
414
+ * Get weighted degree (sum of edge weights) for a prime
415
+ */
416
+ weightedDegree(prime) {
417
+ if (!this.primes.has(prime)) return 0;
418
+
419
+ let sum = 0;
420
+ const neighbors = this.adjacency.get(prime);
421
+ if (!neighbors) return 0;
422
+
423
+ for (const edge of neighbors.values()) {
424
+ sum += edge.weight;
425
+ }
426
+
427
+ return sum;
428
+ }
429
+
430
+ /**
431
+ * Apply decay to all edges
432
+ * @param {number} rate - Decay rate (uses this.decayRate if not specified)
433
+ */
434
+ decay(rate = null) {
435
+ const decayRate = rate ?? this.decayRate;
436
+
437
+ for (const neighbors of this.adjacency.values()) {
438
+ for (const edge of neighbors.values()) {
439
+ edge.decay(decayRate);
440
+ }
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Prune edges below threshold
446
+ * @param {number} threshold - Minimum weight (uses this.pruneThreshold if not specified)
447
+ * @returns {number} Number of edges pruned
448
+ */
449
+ prune(threshold = null) {
450
+ const pruneThreshold = threshold ?? this.pruneThreshold;
451
+ let pruned = 0;
452
+
453
+ for (const [prime, neighbors] of this.adjacency) {
454
+ const toRemove = [];
455
+
456
+ for (const [neighbor, edge] of neighbors) {
457
+ if (edge.weight < pruneThreshold) {
458
+ toRemove.push(neighbor);
459
+ }
460
+ }
461
+
462
+ for (const neighbor of toRemove) {
463
+ neighbors.delete(neighbor);
464
+ this.adjacency.get(neighbor)?.delete(prime);
465
+ pruned++;
466
+ }
467
+ }
468
+
469
+ // Each edge counted twice (bidirectional)
470
+ this.edgeCount -= pruned / 2;
471
+ return pruned / 2;
472
+ }
473
+
474
+ /**
475
+ * Convert to adjacency matrix for NetworkKuramoto
476
+ * @param {number[]} primeOrder - Ordering of primes (indices)
477
+ * @returns {number[][]} Adjacency matrix
478
+ */
479
+ toAdjacencyMatrix(primeOrder = null) {
480
+ const order = primeOrder || Array.from(this.primes).sort((a, b) => a - b);
481
+ const n = order.length;
482
+ const matrix = Array(n).fill(null).map(() => Array(n).fill(0));
483
+
484
+ const primeToIdx = new Map(order.map((p, i) => [p, i]));
485
+
486
+ for (const [source, neighbors] of this.adjacency) {
487
+ const i = primeToIdx.get(source);
488
+ if (i === undefined) continue;
489
+
490
+ for (const [target, edge] of neighbors) {
491
+ const j = primeToIdx.get(target);
492
+ if (j === undefined) continue;
493
+
494
+ matrix[i][j] = edge.weight;
495
+ }
496
+ }
497
+
498
+ return matrix;
499
+ }
500
+
501
+ /**
502
+ * Create NetworkKuramoto model from this graph
503
+ * @param {number[]} frequencies - Oscillator frequencies
504
+ * @param {number} coupling - Global coupling multiplier
505
+ * @returns {object} NetworkKuramoto-compatible object
506
+ */
507
+ toNetworkKuramoto(frequencies, coupling = 0.3) {
508
+ // Import dynamically to avoid circular dependency
509
+ const { NetworkKuramoto } = require('../physics/sync-models');
510
+
511
+ const primeOrder = Array.from(this.primes).sort((a, b) => a - b);
512
+ const adjacency = this.toAdjacencyMatrix(primeOrder);
513
+
514
+ // Ensure frequencies match prime count
515
+ const freqs = frequencies.slice(0, primeOrder.length);
516
+ while (freqs.length < primeOrder.length) {
517
+ freqs.push(1.0);
518
+ }
519
+
520
+ return new NetworkKuramoto(freqs, adjacency, coupling);
521
+ }
522
+
523
+ /**
524
+ * Export to edge list format
525
+ * @param {number} minWeight - Minimum weight to include
526
+ */
527
+ toEdgeList(minWeight = 0) {
528
+ const edges = [];
529
+ const seen = new Set();
530
+
531
+ for (const [source, neighbors] of this.adjacency) {
532
+ for (const [target, edge] of neighbors) {
533
+ const key = `${Math.min(source, target)}-${Math.max(source, target)}`;
534
+ if (seen.has(key)) continue;
535
+
536
+ if (edge.weight >= minWeight) {
537
+ edges.push(edge.toJSON());
538
+ seen.add(key);
539
+ }
540
+ }
541
+ }
542
+
543
+ return edges;
544
+ }
545
+
546
+ /**
547
+ * Import from edge list
548
+ * @param {object[]} edges - Array of edge objects
549
+ */
550
+ fromEdgeList(edges) {
551
+ for (const e of edges) {
552
+ this.addPrime(e.source);
553
+ this.addPrime(e.target);
554
+
555
+ const edge = this.getEdge(e.source, e.target);
556
+ if (edge) {
557
+ edge.weight = e.weight || 0;
558
+ edge.phaseAlignment = e.phaseAlignment || 0;
559
+ edge.cooccurrenceCount = e.cooccurrenceCount || 0;
560
+ edge.resonanceStrength = e.resonanceStrength || 0;
561
+ }
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Get graph statistics
567
+ */
568
+ stats() {
569
+ let totalWeight = 0;
570
+ let maxWeight = 0;
571
+ let totalDegree = 0;
572
+
573
+ const seen = new Set();
574
+
575
+ for (const [prime, neighbors] of this.adjacency) {
576
+ totalDegree += neighbors.size;
577
+
578
+ for (const [neighbor, edge] of neighbors) {
579
+ const key = `${Math.min(prime, neighbor)}-${Math.max(prime, neighbor)}`;
580
+ if (seen.has(key)) continue;
581
+ seen.add(key);
582
+
583
+ totalWeight += edge.weight;
584
+ maxWeight = Math.max(maxWeight, edge.weight);
585
+ }
586
+ }
587
+
588
+ return {
589
+ nodeCount: this.primes.size,
590
+ edgeCount: seen.size,
591
+ totalObservations: this.totalObservations,
592
+ averageDegree: totalDegree / this.primes.size,
593
+ averageWeight: seen.size > 0 ? totalWeight / seen.size : 0,
594
+ maxWeight,
595
+ averageClustering: this.averageClusteringCoefficient(),
596
+ density: seen.size / (this.primes.size * (this.primes.size - 1) / 2)
597
+ };
598
+ }
599
+
600
+ /**
601
+ * Clone the graph
602
+ */
603
+ clone() {
604
+ const copy = new PrimeEntanglementGraph(Array.from(this.primes), {
605
+ decayRate: this.decayRate,
606
+ pruneThreshold: this.pruneThreshold,
607
+ maxEdgesPerNode: this.maxEdgesPerNode
608
+ });
609
+
610
+ copy.fromEdgeList(this.toEdgeList());
611
+ copy.totalObservations = this.totalObservations;
612
+
613
+ return copy;
614
+ }
615
+
616
+ /**
617
+ * Merge another graph into this one
618
+ * @param {PrimeEntanglementGraph} other - Graph to merge
619
+ * @param {number} weight - Weight for other graph's edges
620
+ */
621
+ merge(other, weight = 0.5) {
622
+ for (const p of other.primes) {
623
+ this.addPrime(p);
624
+ }
625
+
626
+ for (const e of other.toEdgeList()) {
627
+ const edge = this.getEdge(e.source, e.target);
628
+ if (edge) {
629
+ edge.weight = (1 - weight) * edge.weight + weight * e.weight;
630
+ edge.phaseAlignment = (1 - weight) * edge.phaseAlignment + weight * e.phaseAlignment;
631
+ edge.cooccurrenceCount += e.cooccurrenceCount;
632
+ }
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Find strongly connected components
638
+ * Uses Tarjan's algorithm
639
+ */
640
+ findComponents(minWeight = 0.1) {
641
+ const index = new Map();
642
+ const lowlink = new Map();
643
+ const onStack = new Set();
644
+ const stack = [];
645
+ const components = [];
646
+ let idx = 0;
647
+
648
+ const strongConnect = (v) => {
649
+ index.set(v, idx);
650
+ lowlink.set(v, idx);
651
+ idx++;
652
+ stack.push(v);
653
+ onStack.add(v);
654
+
655
+ const neighbors = this.adjacency.get(v);
656
+ if (neighbors) {
657
+ for (const [w, edge] of neighbors) {
658
+ if (edge.weight < minWeight) continue;
659
+
660
+ if (!index.has(w)) {
661
+ strongConnect(w);
662
+ lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
663
+ } else if (onStack.has(w)) {
664
+ lowlink.set(v, Math.min(lowlink.get(v), index.get(w)));
665
+ }
666
+ }
667
+ }
668
+
669
+ if (lowlink.get(v) === index.get(v)) {
670
+ const component = [];
671
+ let w;
672
+ do {
673
+ w = stack.pop();
674
+ onStack.delete(w);
675
+ component.push(w);
676
+ } while (w !== v);
677
+ components.push(component);
678
+ }
679
+ };
680
+
681
+ for (const v of this.primes) {
682
+ if (!index.has(v)) {
683
+ strongConnect(v);
684
+ }
685
+ }
686
+
687
+ return components.filter(c => c.length > 1);
688
+ }
689
+ }
690
+
691
+ /**
692
+ * Factory function for common graph configurations
693
+ */
694
+ function createEntanglementGraph(config = {}) {
695
+ const {
696
+ numPrimes = 100,
697
+ primes = null,
698
+ decayRate = 0.01,
699
+ pruneThreshold = 0.01
700
+ } = config;
701
+
702
+ return new PrimeEntanglementGraph(primes || numPrimes, {
703
+ decayRate,
704
+ pruneThreshold
705
+ });
706
+ }
707
+
708
+ module.exports = {
709
+ EntanglementEdge,
710
+ PrimeEntanglementGraph,
711
+ createEntanglementGraph
712
+ };