@aleph-ai/tinyaleph 1.2.1 → 1.3.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,454 @@
1
+ /**
2
+ * Protein Folding via Kuramoto Oscillator Dynamics
3
+ *
4
+ * Models protein folding as entropy minimization through coupled oscillator synchronization.
5
+ * Each amino acid residue is an oscillator with:
6
+ * - Natural frequency derived from its prime value
7
+ * - Coupling strength from contact propensity (hydrophobic, electrostatic, resonance)
8
+ *
9
+ * Folded state = synchronized oscillators = low entropy configuration
10
+ */
11
+
12
+ const { AMINO_ACID_PRIMES, AMINO_ACID_PROPERTIES, getChargeFromPrime, getHydrophobicityFromPrime } = require('./encoding');
13
+
14
+ // Import physics modules - with fallback for standalone testing
15
+ let KuramotoModel, NetworkKuramoto, AdaptiveKuramoto, shannonEntropy, primeToFrequency, calculateResonance;
16
+
17
+ try {
18
+ const physics = require('../../physics');
19
+ const core = require('../../core');
20
+
21
+ KuramotoModel = physics.KuramotoModel;
22
+ NetworkKuramoto = physics.NetworkKuramoto || physics.KuramotoModel;
23
+ AdaptiveKuramoto = physics.AdaptiveKuramoto || physics.KuramotoModel;
24
+ shannonEntropy = physics.shannonEntropy;
25
+ primeToFrequency = core.primeToFrequency;
26
+ calculateResonance = core.calculateResonance;
27
+ } catch (e) {
28
+ // Fallback implementations for standalone testing
29
+ primeToFrequency = (p, base = 1, logScale = 10) => base + Math.log(p) / logScale;
30
+ calculateResonance = (p1, p2) => {
31
+ const ratio = Math.max(p1, p2) / Math.min(p1, p2);
32
+ const PHI = 1.618033988749895;
33
+ return 1 / (1 + Math.abs(ratio - PHI));
34
+ };
35
+ shannonEntropy = (probs) => {
36
+ let H = 0;
37
+ for (const p of probs) {
38
+ if (p > 1e-10) H -= p * Math.log2(p);
39
+ }
40
+ return H;
41
+ };
42
+ }
43
+
44
+ /**
45
+ * FoldingTransform
46
+ *
47
+ * Protein folding modeled as entropy minimization via Kuramoto synchronization.
48
+ */
49
+ class FoldingTransform {
50
+ constructor(options = {}) {
51
+ this.options = {
52
+ coupling: options.coupling || 0.3,
53
+ temperature: options.temperature || 1.0,
54
+ maxSteps: options.maxSteps || 1000,
55
+ dt: options.dt || 0.01,
56
+ convergenceThreshold: options.convergenceThreshold || 0.95,
57
+ minSequenceSeparation: options.minSequenceSeparation || 4,
58
+ hydrophobicWeight: options.hydrophobicWeight || 0.4,
59
+ electrostaticWeight: options.electrostaticWeight || 0.3,
60
+ resonanceWeight: options.resonanceWeight || 0.3,
61
+ ...options
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Fold protein using Kuramoto oscillator dynamics
67
+ * @param {number[]} proteinPrimes - Protein sequence as amino acid primes
68
+ * @returns {object} Folding result with structure information
69
+ */
70
+ fold(proteinPrimes) {
71
+ const n = proteinPrimes.length;
72
+
73
+ if (n < 4) {
74
+ return {
75
+ success: false,
76
+ error: 'Protein too short for folding simulation',
77
+ length: n
78
+ };
79
+ }
80
+
81
+ // Initialize oscillators with frequencies from primes
82
+ const frequencies = proteinPrimes.map(p => primeToFrequency(p, 1, 10));
83
+
84
+ // Build contact propensity matrix
85
+ const contactMatrix = this.computeContactPropensity(proteinPrimes);
86
+
87
+ // Initialize phases randomly
88
+ const phases = new Array(n).fill(0).map(() => Math.random() * 2 * Math.PI);
89
+
90
+ // Evolution tracking
91
+ const history = {
92
+ orderParameter: [],
93
+ entropy: [],
94
+ energyLandscape: []
95
+ };
96
+
97
+ // Kuramoto evolution
98
+ let order = 0;
99
+ let step = 0;
100
+
101
+ for (step = 0; step < this.options.maxSteps; step++) {
102
+ // Compute phase updates (Kuramoto equation)
103
+ const dPhases = this.kuramotoStep(phases, frequencies, contactMatrix);
104
+
105
+ // Apply thermal noise
106
+ for (let i = 0; i < n; i++) {
107
+ phases[i] += dPhases[i] * this.options.dt;
108
+ phases[i] += this.thermalNoise() * Math.sqrt(this.options.dt);
109
+ // Keep phases in [0, 2π]
110
+ phases[i] = ((phases[i] % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
111
+ }
112
+
113
+ // Calculate order parameter
114
+ order = this.calculateOrderParameter(phases);
115
+ const entropy = this.calculatePhasesEntropy(phases);
116
+ const energy = this.calculateFoldingEnergy(phases, proteinPrimes, contactMatrix);
117
+
118
+ history.orderParameter.push(order);
119
+ history.entropy.push(entropy);
120
+ history.energyLandscape.push(energy);
121
+
122
+ // Check convergence
123
+ if (order > this.options.convergenceThreshold) {
124
+ break;
125
+ }
126
+ }
127
+
128
+ // Extract structure from final phases
129
+ const structure = this.phasesToStructure(phases, proteinPrimes);
130
+ const contacts = this.extractContacts(phases, this.options.minSequenceSeparation);
131
+
132
+ return {
133
+ success: true,
134
+ phases: [...phases],
135
+ structure,
136
+ contacts,
137
+ orderParameter: order,
138
+ finalEntropy: history.entropy[history.entropy.length - 1],
139
+ foldingFreeEnergy: this.estimateFreeEnergy(order, proteinPrimes),
140
+ stepsToConverge: step,
141
+ history,
142
+ secondaryStructure: this.assignSecondaryStructure(structure),
143
+ compactness: this.calculateCompactness(contacts, n)
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Compute Kuramoto phase derivatives
149
+ */
150
+ kuramotoStep(phases, frequencies, coupling) {
151
+ const n = phases.length;
152
+ const dPhases = new Array(n).fill(0);
153
+
154
+ for (let i = 0; i < n; i++) {
155
+ // Natural frequency term
156
+ dPhases[i] = frequencies[i];
157
+
158
+ // Coupling term (sum over all connected oscillators)
159
+ for (let j = 0; j < n; j++) {
160
+ if (i !== j && coupling[i][j] > 0) {
161
+ dPhases[i] += coupling[i][j] * Math.sin(phases[j] - phases[i]);
162
+ }
163
+ }
164
+ }
165
+
166
+ return dPhases;
167
+ }
168
+
169
+ /**
170
+ * Compute contact propensity matrix from amino acid properties
171
+ */
172
+ computeContactPropensity(proteinPrimes) {
173
+ const n = proteinPrimes.length;
174
+ const matrix = Array(n).fill(null).map(() => Array(n).fill(0));
175
+
176
+ for (let i = 0; i < n; i++) {
177
+ for (let j = i + this.options.minSequenceSeparation; j < n; j++) {
178
+ const pi = proteinPrimes[i];
179
+ const pj = proteinPrimes[j];
180
+
181
+ // Resonance component (golden ratio proximity)
182
+ const resonance = calculateResonance(pi, pj);
183
+
184
+ // Hydrophobic interaction (smaller primes are more hydrophobic)
185
+ const hi = getHydrophobicityFromPrime(pi);
186
+ const hj = getHydrophobicityFromPrime(pj);
187
+ const hydrophobic = this.hydrophobicPotential(hi, hj);
188
+
189
+ // Electrostatic interaction
190
+ const qi = getChargeFromPrime(pi);
191
+ const qj = getChargeFromPrime(pj);
192
+ const electrostatic = this.electrostaticPotential(qi, qj);
193
+
194
+ // Combined contact propensity
195
+ const propensity =
196
+ this.options.resonanceWeight * resonance +
197
+ this.options.hydrophobicWeight * hydrophobic +
198
+ this.options.electrostaticWeight * electrostatic;
199
+
200
+ matrix[i][j] = matrix[j][i] = Math.max(0, propensity) * this.options.coupling;
201
+ }
202
+ }
203
+
204
+ return matrix;
205
+ }
206
+
207
+ /**
208
+ * Hydrophobic potential: like attracts like
209
+ */
210
+ hydrophobicPotential(h1, h2) {
211
+ // Both hydrophobic (positive h): attract
212
+ // Both hydrophilic (negative h): weak attract
213
+ // Mixed: repel
214
+ if (h1 > 0 && h2 > 0) {
215
+ return Math.sqrt(h1 * h2) / 4;
216
+ } else if (h1 < 0 && h2 < 0) {
217
+ return Math.sqrt(-h1 * -h2) / 8;
218
+ } else {
219
+ return -0.1;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Electrostatic potential: opposites attract
225
+ */
226
+ electrostaticPotential(q1, q2) {
227
+ if (q1 * q2 < 0) {
228
+ return 0.5; // Opposite charges attract
229
+ } else if (q1 * q2 > 0) {
230
+ return -0.3; // Same charges repel
231
+ }
232
+ return 0;
233
+ }
234
+
235
+ /**
236
+ * Generate thermal noise
237
+ */
238
+ thermalNoise() {
239
+ // Box-Muller transform for Gaussian noise
240
+ const u1 = Math.random();
241
+ const u2 = Math.random();
242
+ const gaussian = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
243
+ return gaussian * Math.sqrt(this.options.temperature) * 0.1;
244
+ }
245
+
246
+ /**
247
+ * Calculate Kuramoto order parameter
248
+ */
249
+ calculateOrderParameter(phases) {
250
+ const n = phases.length;
251
+ let sumCos = 0, sumSin = 0;
252
+
253
+ for (const phase of phases) {
254
+ sumCos += Math.cos(phase);
255
+ sumSin += Math.sin(phase);
256
+ }
257
+
258
+ return Math.sqrt(sumCos * sumCos + sumSin * sumSin) / n;
259
+ }
260
+
261
+ /**
262
+ * Calculate entropy of phase distribution
263
+ */
264
+ calculatePhasesEntropy(phases) {
265
+ const bins = 36; // 10-degree bins
266
+ const histogram = Array(bins).fill(0);
267
+
268
+ for (const phase of phases) {
269
+ const bin = Math.floor(phase / (2 * Math.PI) * bins);
270
+ histogram[Math.min(bin, bins - 1)]++;
271
+ }
272
+
273
+ const probs = histogram.map(h => h / phases.length);
274
+ return shannonEntropy(probs);
275
+ }
276
+
277
+ /**
278
+ * Calculate folding energy from phases and contacts
279
+ */
280
+ calculateFoldingEnergy(phases, primes, contactMatrix) {
281
+ let energy = 0;
282
+ const n = phases.length;
283
+
284
+ for (let i = 0; i < n; i++) {
285
+ for (let j = i + this.options.minSequenceSeparation; j < n; j++) {
286
+ const phaseDiff = Math.abs(phases[i] - phases[j]);
287
+ const inContact = phaseDiff < 0.5 || phaseDiff > 2 * Math.PI - 0.5;
288
+
289
+ if (inContact && contactMatrix[i][j] > 0) {
290
+ // Favorable contact
291
+ energy -= contactMatrix[i][j];
292
+ }
293
+ }
294
+ }
295
+
296
+ return energy;
297
+ }
298
+
299
+ /**
300
+ * Convert phases to structural information
301
+ */
302
+ phasesToStructure(phases, primes) {
303
+ const structure = [];
304
+
305
+ for (let i = 0; i < phases.length; i++) {
306
+ // Phase determines backbone angles (phi, psi approximation)
307
+ const phi = (phases[i] - Math.PI) * 180 / Math.PI;
308
+ const nextPhase = phases[(i + 1) % phases.length];
309
+ const psi = (nextPhase - phases[i]) * 180 / Math.PI;
310
+
311
+ structure.push({
312
+ residue: i,
313
+ prime: primes[i],
314
+ phase: phases[i],
315
+ phi,
316
+ psi,
317
+ secondaryStructure: this.classifySecondaryStructure(phi, psi)
318
+ });
319
+ }
320
+
321
+ return structure;
322
+ }
323
+
324
+ /**
325
+ * Classify secondary structure from backbone angles
326
+ */
327
+ classifySecondaryStructure(phi, psi) {
328
+ // Alpha helix: phi ≈ -60, psi ≈ -45
329
+ if (phi > -100 && phi < -30 && psi > -70 && psi < 0) {
330
+ return 'H'; // Helix
331
+ }
332
+
333
+ // Beta sheet: phi ≈ -120, psi ≈ +120
334
+ if (phi > -180 && phi < -60 && (psi > 90 || psi < -90)) {
335
+ return 'E'; // Extended (sheet)
336
+ }
337
+
338
+ // 3-10 helix: phi ≈ -49, psi ≈ -26
339
+ if (phi > -70 && phi < -30 && psi > -50 && psi < 0) {
340
+ return 'G'; // 3-10 helix
341
+ }
342
+
343
+ return 'C'; // Coil
344
+ }
345
+
346
+ /**
347
+ * Extract residue-residue contacts from phases
348
+ */
349
+ extractContacts(phases, minSeparation = 4, threshold = 0.5) {
350
+ const contacts = [];
351
+ const n = phases.length;
352
+
353
+ for (let i = 0; i < n; i++) {
354
+ for (let j = i + minSeparation; j < n; j++) {
355
+ const phaseDiff = Math.abs(phases[i] - phases[j]);
356
+ const normalized = Math.min(phaseDiff, 2 * Math.PI - phaseDiff);
357
+
358
+ if (normalized < threshold) {
359
+ contacts.push({
360
+ i,
361
+ j,
362
+ distance: normalized,
363
+ strength: 1 - normalized / threshold
364
+ });
365
+ }
366
+ }
367
+ }
368
+
369
+ return contacts;
370
+ }
371
+
372
+ /**
373
+ * Assign secondary structure elements
374
+ */
375
+ assignSecondaryStructure(structure) {
376
+ const ssSequence = structure.map(s => s.secondaryStructure).join('');
377
+
378
+ // Find helices (runs of H)
379
+ const helices = [];
380
+ const helixPattern = /H{4,}/g;
381
+ let match;
382
+ while ((match = helixPattern.exec(ssSequence)) !== null) {
383
+ helices.push({ start: match.index, end: match.index + match[0].length - 1, type: 'helix' });
384
+ }
385
+
386
+ // Find sheets (runs of E)
387
+ const sheets = [];
388
+ const sheetPattern = /E{3,}/g;
389
+ while ((match = sheetPattern.exec(ssSequence)) !== null) {
390
+ sheets.push({ start: match.index, end: match.index + match[0].length - 1, type: 'sheet' });
391
+ }
392
+
393
+ return {
394
+ sequence: ssSequence,
395
+ helices,
396
+ sheets,
397
+ helixContent: (ssSequence.match(/H/g) || []).length / structure.length,
398
+ sheetContent: (ssSequence.match(/E/g) || []).length / structure.length
399
+ };
400
+ }
401
+
402
+ /**
403
+ * Calculate compactness (contact order)
404
+ */
405
+ calculateCompactness(contacts, length) {
406
+ if (contacts.length === 0) return 0;
407
+
408
+ const totalSeparation = contacts.reduce((sum, c) => sum + (c.j - c.i), 0);
409
+ return totalSeparation / (contacts.length * length);
410
+ }
411
+
412
+ /**
413
+ * Estimate folding free energy
414
+ */
415
+ estimateFreeEnergy(orderParameter, primes) {
416
+ // ΔG ∝ -kT * ln(order parameter) - hydrophobic contribution
417
+ const kT = this.options.temperature;
418
+ const entropyTerm = -kT * Math.log(Math.max(orderParameter, 0.01));
419
+
420
+ // Hydrophobic burial contribution
421
+ const hydrophobicPrimes = primes.filter(p => p <= 43);
422
+ const hydrophobicFraction = hydrophobicPrimes.length / primes.length;
423
+ const hydrophobicTerm = -hydrophobicFraction * 2.0; // Favorable
424
+
425
+ return entropyTerm + hydrophobicTerm;
426
+ }
427
+
428
+ /**
429
+ * Simulated annealing for global minimum
430
+ */
431
+ anneal(proteinPrimes, options = {}) {
432
+ const startTemp = options.startTemp || 10.0;
433
+ const endTemp = options.endTemp || 0.1;
434
+ const coolingRate = options.coolingRate || 0.99;
435
+
436
+ let temp = startTemp;
437
+ let bestResult = null;
438
+
439
+ while (temp > endTemp) {
440
+ this.options.temperature = temp;
441
+ const result = this.fold(proteinPrimes);
442
+
443
+ if (!bestResult || result.foldingFreeEnergy < bestResult.foldingFreeEnergy) {
444
+ bestResult = result;
445
+ }
446
+
447
+ temp *= coolingRate;
448
+ }
449
+
450
+ return bestResult;
451
+ }
452
+ }
453
+
454
+ module.exports = { FoldingTransform };