@aleph-ai/tinyaleph 1.1.0 → 1.2.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,566 @@
1
+ /**
2
+ * Stochastic Kuramoto Models
3
+ *
4
+ * Kuramoto oscillators with Langevin noise for robust synchronization:
5
+ * dθᵢ/dt = ωᵢ + (K/N) Σⱼ sin(θⱼ - θᵢ) + σ·ξᵢ(t)
6
+ *
7
+ * Features:
8
+ * - White noise (Wiener process)
9
+ * - Colored noise (Ornstein-Uhlenbeck process)
10
+ * - Temperature-dependent coupling
11
+ * - Noise-induced synchronization detection
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const { KuramotoModel } = require('./kuramoto');
17
+
18
+ /**
19
+ * Box-Muller transform for Gaussian random numbers
20
+ * @returns {number} Standard normal random variable
21
+ */
22
+ function gaussianRandom() {
23
+ const u1 = Math.random();
24
+ const u2 = Math.random();
25
+ return Math.sqrt(-2 * Math.log(u1 || 1e-10)) * Math.cos(2 * Math.PI * u2);
26
+ }
27
+
28
+ /**
29
+ * StochasticKuramoto - Kuramoto model with Langevin noise
30
+ *
31
+ * Adds thermal fluctuations to oscillator dynamics:
32
+ * dθᵢ = [ωᵢ + K·coupling(i)]dt + σ·dWᵢ
33
+ *
34
+ * where dWᵢ is a Wiener increment with variance dt.
35
+ */
36
+ class StochasticKuramoto extends KuramotoModel {
37
+ /**
38
+ * @param {number[]} frequencies - Natural frequencies ωᵢ
39
+ * @param {object} options - Configuration options
40
+ * @param {number} [options.coupling=0.3] - Coupling strength K
41
+ * @param {number} [options.noiseIntensity=0.1] - Noise amplitude σ
42
+ * @param {string} [options.noiseType='white'] - 'white' or 'colored'
43
+ * @param {number} [options.correlationTime=1.0] - τ for colored noise
44
+ * @param {number} [options.temperature=1.0] - Temperature for T-dependent coupling
45
+ */
46
+ constructor(frequencies, options = {}) {
47
+ super(frequencies, options.coupling || 0.3);
48
+
49
+ this.sigma = options.noiseIntensity ?? 0.1;
50
+ this.noiseType = options.noiseType || 'white';
51
+ this.tau = options.correlationTime ?? 1.0;
52
+ this.temperature = options.temperature ?? 1.0;
53
+ this.useTemperatureCoupling = options.temperatureCoupling ?? false;
54
+
55
+ // For colored noise (Ornstein-Uhlenbeck): dη = -η/τ dt + σ/√τ dW
56
+ // Each oscillator has its own OU process
57
+ this.coloredNoiseState = new Float64Array(frequencies.length);
58
+
59
+ // Track noise history for analysis
60
+ this.noiseHistory = [];
61
+ this.maxHistoryLength = 1000;
62
+
63
+ // Statistics
64
+ this.noiseStats = {
65
+ mean: 0,
66
+ variance: 0,
67
+ sampleCount: 0
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Set noise intensity dynamically
73
+ * @param {number} sigma - New noise intensity
74
+ */
75
+ setNoiseIntensity(sigma) {
76
+ this.sigma = sigma;
77
+ }
78
+
79
+ /**
80
+ * Set temperature (affects coupling if temperatureCoupling is enabled)
81
+ * @param {number} T - Temperature
82
+ */
83
+ setTemperature(T) {
84
+ this.temperature = Math.max(0.01, T);
85
+ }
86
+
87
+ /**
88
+ * Get effective coupling (temperature-dependent)
89
+ * K_eff = K / T (Arrhenius-like)
90
+ */
91
+ getEffectiveCoupling() {
92
+ if (this.useTemperatureCoupling) {
93
+ return this.K / this.temperature;
94
+ }
95
+ return this.K;
96
+ }
97
+
98
+ /**
99
+ * Generate white noise increment
100
+ * @param {number} dt - Time step
101
+ * @returns {number} Noise increment σ·√dt·N(0,1)
102
+ */
103
+ whiteNoiseIncrement(dt) {
104
+ return this.sigma * Math.sqrt(dt) * gaussianRandom();
105
+ }
106
+
107
+ /**
108
+ * Update Ornstein-Uhlenbeck process for colored noise
109
+ * dη = -η/τ dt + (σ/√τ)·dW
110
+ *
111
+ * @param {number} idx - Oscillator index
112
+ * @param {number} dt - Time step
113
+ * @returns {number} Colored noise value η
114
+ */
115
+ updateColoredNoise(idx, dt) {
116
+ const eta = this.coloredNoiseState[idx];
117
+ const decay = Math.exp(-dt / this.tau);
118
+ const diffusion = this.sigma * Math.sqrt((1 - decay * decay) / 2);
119
+
120
+ // Exact update for OU process
121
+ this.coloredNoiseState[idx] = eta * decay + diffusion * gaussianRandom();
122
+
123
+ return this.coloredNoiseState[idx];
124
+ }
125
+
126
+ /**
127
+ * Get noise increment based on noise type
128
+ * @param {number} idx - Oscillator index
129
+ * @param {number} dt - Time step
130
+ * @returns {number} Noise increment
131
+ */
132
+ getNoiseIncrement(idx, dt) {
133
+ if (this.noiseType === 'colored') {
134
+ return this.updateColoredNoise(idx, dt) * dt;
135
+ }
136
+ return this.whiteNoiseIncrement(dt);
137
+ }
138
+
139
+ /**
140
+ * Stochastic Kuramoto coupling with noise
141
+ * @param {object} osc - Oscillator
142
+ * @param {number} idx - Oscillator index
143
+ * @param {number} dt - Time step
144
+ * @returns {number} Phase increment (deterministic + stochastic)
145
+ */
146
+ stochasticCoupling(osc, idx, dt) {
147
+ // Deterministic Kuramoto coupling
148
+ let coupling = 0;
149
+ const Keff = this.getEffectiveCoupling();
150
+
151
+ for (const other of this.oscillators) {
152
+ if (other !== osc) {
153
+ coupling += Math.sin(other.phase - osc.phase);
154
+ }
155
+ }
156
+
157
+ const deterministicPart = Keff * coupling / this.oscillators.length * dt;
158
+
159
+ // Stochastic part
160
+ const stochasticPart = this.getNoiseIncrement(idx, dt);
161
+
162
+ // Update statistics
163
+ this._updateNoiseStats(stochasticPart);
164
+
165
+ return deterministicPart + stochasticPart;
166
+ }
167
+
168
+ /**
169
+ * Update running noise statistics
170
+ * @private
171
+ */
172
+ _updateNoiseStats(noiseValue) {
173
+ const n = ++this.noiseStats.sampleCount;
174
+ const delta = noiseValue - this.noiseStats.mean;
175
+ this.noiseStats.mean += delta / n;
176
+ const delta2 = noiseValue - this.noiseStats.mean;
177
+ this.noiseStats.variance += (delta * delta2 - this.noiseStats.variance) / n;
178
+ }
179
+
180
+ /**
181
+ * Advance system by one time step with stochastic dynamics
182
+ * @param {number} dt - Time step size
183
+ */
184
+ tick(dt) {
185
+ // Store noise values for this step
186
+ const stepNoise = [];
187
+
188
+ for (let i = 0; i < this.oscillators.length; i++) {
189
+ const osc = this.oscillators[i];
190
+ const phaseIncrement = this.stochasticCoupling(osc, i, dt);
191
+
192
+ stepNoise.push(phaseIncrement);
193
+
194
+ // Update phase
195
+ osc.phase += osc.frequency * dt + phaseIncrement;
196
+ osc.phase = ((osc.phase % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
197
+
198
+ // Amplitude decay
199
+ osc.decay(0.02, dt);
200
+ }
201
+
202
+ // Record history
203
+ if (this.noiseHistory.length < this.maxHistoryLength) {
204
+ this.noiseHistory.push({
205
+ t: Date.now(),
206
+ noise: stepNoise,
207
+ orderParameter: this.orderParameter()
208
+ });
209
+ } else {
210
+ this.noiseHistory.shift();
211
+ this.noiseHistory.push({
212
+ t: Date.now(),
213
+ noise: stepNoise,
214
+ orderParameter: this.orderParameter()
215
+ });
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Run multiple steps
221
+ * @param {number} steps - Number of steps
222
+ * @param {number} dt - Time step size
223
+ * @returns {object[]} Evolution history
224
+ */
225
+ evolve(steps, dt = 0.01) {
226
+ const trajectory = [];
227
+
228
+ for (let i = 0; i < steps; i++) {
229
+ this.tick(dt);
230
+ trajectory.push({
231
+ step: i,
232
+ orderParameter: this.orderParameter(),
233
+ meanPhase: this.meanPhase(),
234
+ noiseStats: { ...this.noiseStats }
235
+ });
236
+ }
237
+
238
+ return trajectory;
239
+ }
240
+
241
+ /**
242
+ * Detect noise-induced synchronization
243
+ *
244
+ * Phenomenon where noise can actually enhance synchronization
245
+ * by helping oscillators escape from metastable states.
246
+ *
247
+ * @param {number} baselineSteps - Steps to establish baseline
248
+ * @param {number} noisySteps - Steps with noise
249
+ * @param {number} dt - Time step
250
+ * @returns {object} Detection result
251
+ */
252
+ detectNoiseInducedSync(baselineSteps = 100, noisySteps = 200, dt = 0.01) {
253
+ // Save current state
254
+ const originalSigma = this.sigma;
255
+
256
+ // Baseline (no noise)
257
+ this.sigma = 0;
258
+ const baselineTrajectory = this.evolve(baselineSteps, dt);
259
+ const baselineOrder = baselineTrajectory.slice(-20)
260
+ .reduce((sum, t) => sum + t.orderParameter, 0) / 20;
261
+
262
+ // With noise
263
+ this.sigma = originalSigma;
264
+ const noisyTrajectory = this.evolve(noisySteps, dt);
265
+ const noisyOrder = noisyTrajectory.slice(-20)
266
+ .reduce((sum, t) => sum + t.orderParameter, 0) / 20;
267
+
268
+ const enhancement = noisyOrder - baselineOrder;
269
+ const isNoiseInduced = enhancement > 0.1;
270
+
271
+ return {
272
+ baselineOrderParameter: baselineOrder,
273
+ noisyOrderParameter: noisyOrder,
274
+ enhancement,
275
+ isNoiseInduced,
276
+ noiseIntensity: originalSigma,
277
+ baselineTrajectory,
278
+ noisyTrajectory
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Compute stochastic order parameter with error bars
284
+ * @param {number} samples - Number of samples for averaging
285
+ * @param {number} dt - Time step between samples
286
+ * @returns {object} Order parameter with uncertainty
287
+ */
288
+ orderParameterWithUncertainty(samples = 100, dt = 0.01) {
289
+ const values = [];
290
+
291
+ for (let i = 0; i < samples; i++) {
292
+ this.tick(dt);
293
+ values.push(this.orderParameter());
294
+ }
295
+
296
+ const mean = values.reduce((a, b) => a + b, 0) / samples;
297
+ const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / samples;
298
+ const stdError = Math.sqrt(variance / samples);
299
+
300
+ return {
301
+ mean,
302
+ stdDev: Math.sqrt(variance),
303
+ stdError,
304
+ confidence95: [mean - 1.96 * stdError, mean + 1.96 * stdError],
305
+ samples: values
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Compute autocorrelation of order parameter
311
+ * @param {number} maxLag - Maximum lag to compute
312
+ * @returns {number[]} Autocorrelation values
313
+ */
314
+ orderParameterAutocorrelation(maxLag = 50) {
315
+ if (this.noiseHistory.length < maxLag + 10) {
316
+ return new Array(maxLag).fill(0);
317
+ }
318
+
319
+ const orderParams = this.noiseHistory.map(h => h.orderParameter);
320
+ const n = orderParams.length;
321
+ const mean = orderParams.reduce((a, b) => a + b, 0) / n;
322
+ const centered = orderParams.map(v => v - mean);
323
+
324
+ const autocorr = [];
325
+ const variance = centered.reduce((sum, v) => sum + v * v, 0);
326
+
327
+ for (let lag = 0; lag < maxLag; lag++) {
328
+ let sum = 0;
329
+ for (let i = 0; i < n - lag; i++) {
330
+ sum += centered[i] * centered[i + lag];
331
+ }
332
+ autocorr.push(variance > 0 ? sum / variance : 0);
333
+ }
334
+
335
+ return autocorr;
336
+ }
337
+
338
+ /**
339
+ * Get correlation time from autocorrelation decay
340
+ * @returns {number} Estimated correlation time
341
+ */
342
+ estimateCorrelationTime() {
343
+ const autocorr = this.orderParameterAutocorrelation(100);
344
+
345
+ // Find where autocorrelation drops to 1/e
346
+ const threshold = 1 / Math.E;
347
+ for (let i = 0; i < autocorr.length; i++) {
348
+ if (autocorr[i] < threshold) {
349
+ return i;
350
+ }
351
+ }
352
+
353
+ return autocorr.length; // Didn't decay fast enough
354
+ }
355
+
356
+ /**
357
+ * Reset noise state
358
+ */
359
+ resetNoise() {
360
+ this.coloredNoiseState.fill(0);
361
+ this.noiseHistory = [];
362
+ this.noiseStats = { mean: 0, variance: 0, sampleCount: 0 };
363
+ }
364
+
365
+ /**
366
+ * Get current state snapshot
367
+ */
368
+ getState() {
369
+ return {
370
+ ...super.getState(),
371
+ noiseIntensity: this.sigma,
372
+ noiseType: this.noiseType,
373
+ correlationTime: this.tau,
374
+ temperature: this.temperature,
375
+ effectiveCoupling: this.getEffectiveCoupling(),
376
+ noiseStats: { ...this.noiseStats },
377
+ coloredNoiseState: [...this.coloredNoiseState]
378
+ };
379
+ }
380
+ }
381
+
382
+ /**
383
+ * ColoredNoiseKuramoto - Specialized class for Ornstein-Uhlenbeck noise
384
+ *
385
+ * Provides more control over colored noise parameters and analysis.
386
+ */
387
+ class ColoredNoiseKuramoto extends StochasticKuramoto {
388
+ /**
389
+ * @param {number[]} frequencies - Natural frequencies
390
+ * @param {object} options - Configuration
391
+ * @param {number} [options.coupling=0.3] - Coupling strength
392
+ * @param {number} [options.noiseIntensity=0.1] - Noise amplitude
393
+ * @param {number} [options.correlationTime=1.0] - OU correlation time
394
+ */
395
+ constructor(frequencies, options = {}) {
396
+ super(frequencies, {
397
+ ...options,
398
+ noiseType: 'colored'
399
+ });
400
+ }
401
+
402
+ /**
403
+ * Set correlation time dynamically
404
+ * @param {number} tau - New correlation time
405
+ */
406
+ setCorrelationTime(tau) {
407
+ this.tau = Math.max(0.01, tau);
408
+ }
409
+
410
+ /**
411
+ * Get OU process stationary distribution parameters
412
+ * For OU: variance = σ²/(2/τ) = σ²τ/2
413
+ */
414
+ getStationaryVariance() {
415
+ return (this.sigma ** 2) * this.tau / 2;
416
+ }
417
+
418
+ /**
419
+ * Check if OU processes have equilibrated
420
+ * @param {number} threshold - Tolerance for equilibration
421
+ */
422
+ isEquilibrated(threshold = 0.1) {
423
+ const expectedVar = this.getStationaryVariance();
424
+ let actualVar = 0;
425
+
426
+ for (const eta of this.coloredNoiseState) {
427
+ actualVar += eta ** 2;
428
+ }
429
+ actualVar /= this.coloredNoiseState.length;
430
+
431
+ return Math.abs(actualVar - expectedVar) / expectedVar < threshold;
432
+ }
433
+
434
+ /**
435
+ * Get power spectrum estimate of noise
436
+ * @param {number} maxFreq - Maximum frequency
437
+ * @param {number} resolution - Frequency resolution
438
+ */
439
+ noisePowerSpectrum(maxFreq = 10, resolution = 0.1) {
440
+ const spectrum = [];
441
+
442
+ // Theoretical OU spectrum: S(ω) = 2σ²τ / (1 + (ωτ)²)
443
+ for (let omega = 0; omega <= maxFreq; omega += resolution) {
444
+ const theoretical = 2 * this.sigma ** 2 * this.tau / (1 + (omega * this.tau) ** 2);
445
+ spectrum.push({ omega, power: theoretical });
446
+ }
447
+
448
+ return spectrum;
449
+ }
450
+ }
451
+
452
+ /**
453
+ * ThermalKuramoto - Temperature-controlled synchronization
454
+ *
455
+ * Models thermal effects on oscillator synchronization:
456
+ * - High temperature: Strong fluctuations, weak effective coupling
457
+ * - Low temperature: Weak fluctuations, strong effective coupling
458
+ *
459
+ * Critical temperature T_c ≈ K (coupling strength)
460
+ */
461
+ class ThermalKuramoto extends StochasticKuramoto {
462
+ /**
463
+ * @param {number[]} frequencies - Natural frequencies
464
+ * @param {object} options - Configuration
465
+ * @param {number} [options.coupling=0.3] - Coupling strength
466
+ * @param {number} [options.temperature=1.0] - Initial temperature
467
+ */
468
+ constructor(frequencies, options = {}) {
469
+ super(frequencies, {
470
+ ...options,
471
+ noiseType: 'white',
472
+ temperatureCoupling: true
473
+ });
474
+
475
+ // Noise intensity proportional to √T (fluctuation-dissipation)
476
+ this._updateNoiseFromTemperature();
477
+ }
478
+
479
+ /**
480
+ * Update noise intensity from temperature
481
+ * @private
482
+ */
483
+ _updateNoiseFromTemperature() {
484
+ // Fluctuation-dissipation: σ² ∝ T
485
+ this.sigma = Math.sqrt(this.temperature) * 0.1;
486
+ }
487
+
488
+ /**
489
+ * Set temperature and update noise
490
+ * @param {number} T - Temperature
491
+ */
492
+ setTemperature(T) {
493
+ super.setTemperature(T);
494
+ this._updateNoiseFromTemperature();
495
+ }
496
+
497
+ /**
498
+ * Estimate critical temperature from current state
499
+ * T_c ≈ K for all-to-all coupling with uniform frequencies
500
+ */
501
+ estimateCriticalTemperature() {
502
+ // Approximate T_c = K * (frequency spread factor)
503
+ const freqs = this.oscillators.map(o => o.frequency);
504
+ const meanFreq = freqs.reduce((a, b) => a + b, 0) / freqs.length;
505
+ const freqSpread = Math.sqrt(
506
+ freqs.reduce((sum, f) => sum + (f - meanFreq) ** 2, 0) / freqs.length
507
+ );
508
+
509
+ return this.K * (1 + freqSpread);
510
+ }
511
+
512
+ /**
513
+ * Perform temperature sweep to find transition
514
+ * @param {number} Tmin - Minimum temperature
515
+ * @param {number} Tmax - Maximum temperature
516
+ * @param {number} steps - Number of temperature steps
517
+ * @param {number} equilibrationSteps - Steps to equilibrate at each T
518
+ */
519
+ temperatureSweep(Tmin = 0.1, Tmax = 2.0, steps = 20, equilibrationSteps = 100) {
520
+ const results = [];
521
+
522
+ for (let i = 0; i < steps; i++) {
523
+ const T = Tmin + (Tmax - Tmin) * i / (steps - 1);
524
+ this.setTemperature(T);
525
+
526
+ // Equilibrate
527
+ this.evolve(equilibrationSteps, 0.01);
528
+
529
+ // Measure
530
+ const stats = this.orderParameterWithUncertainty(50, 0.01);
531
+
532
+ results.push({
533
+ temperature: T,
534
+ orderParameter: stats.mean,
535
+ stdDev: stats.stdDev,
536
+ confidence95: stats.confidence95
537
+ });
538
+ }
539
+
540
+ return results;
541
+ }
542
+
543
+ /**
544
+ * Check if system is in ordered (synchronized) phase
545
+ * @param {number} threshold - Order parameter threshold
546
+ */
547
+ isOrdered(threshold = 0.5) {
548
+ return this.orderParameter() > threshold;
549
+ }
550
+
551
+ /**
552
+ * Check if system is at or near critical temperature
553
+ * @param {number} tolerance - Tolerance factor
554
+ */
555
+ isNearCritical(tolerance = 0.2) {
556
+ const Tc = this.estimateCriticalTemperature();
557
+ return Math.abs(this.temperature - Tc) / Tc < tolerance;
558
+ }
559
+ }
560
+
561
+ module.exports = {
562
+ StochasticKuramoto,
563
+ ColoredNoiseKuramoto,
564
+ ThermalKuramoto,
565
+ gaussianRandom
566
+ };