slangmath 1.0.6 → 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,954 @@
1
+ /**
2
+ * SLaNg Complex Numbers Module
3
+ *
4
+ * Full complex arithmetic, complex calculus, and complex analysis
5
+ * integrated with the SLaNg expression format.
6
+ *
7
+ * Features:
8
+ * - Complex number type: { re, im }
9
+ * - Full arithmetic: add, sub, mul, div, pow, sqrt, exp, log
10
+ * - Trigonometric functions in complex plane
11
+ * - Polar form, modulus, argument, conjugate
12
+ * - Complex roots of unity and polynomials (companion matrix method)
13
+ * - Numerical complex differentiation (Cauchy-Riemann check)
14
+ * - Contour integration (numerical)
15
+ * - Laurent series coefficient estimation
16
+ * - Complex fast Fourier transform (Cooley-Tukey FFT)
17
+ */
18
+
19
+ // ============================================================================
20
+ // COMPLEX NUMBER CONSTRUCTOR
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Create a complex number.
25
+ * @param {number} re - Real part
26
+ * @param {number} im - Imaginary part (default 0)
27
+ * @returns {{ re: number, im: number }}
28
+ */
29
+ export function C(re, im = 0) {
30
+ return { re, im };
31
+ }
32
+
33
+ export const ZERO = C(0, 0);
34
+ export const ONE = C(1, 0);
35
+ export const I = C(0, 1);
36
+ export const NEG1 = C(-1, 0);
37
+
38
+ // ============================================================================
39
+ // BASIC COMPLEX ARITHMETIC
40
+ // ============================================================================
41
+
42
+ export function cAdd(a, b) { return C(a.re + b.re, a.im + b.im); }
43
+ export function cSub(a, b) { return C(a.re - b.re, a.im - b.im); }
44
+ export function cScale(a, s) { return C(a.re * s, a.im * s); }
45
+
46
+ export function cMul(a, b) {
47
+ return C(a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re);
48
+ }
49
+
50
+ export function cDiv(a, b) {
51
+ const denom = b.re * b.re + b.im * b.im;
52
+ if (denom < 1e-300) throw new Error('Division by zero in complex division');
53
+ return C(
54
+ (a.re * b.re + a.im * b.im) / denom,
55
+ (a.im * b.re - a.re * b.im) / denom
56
+ );
57
+ }
58
+
59
+ /** Complex conjugate */
60
+ export function cConj(z) { return C(z.re, -z.im); }
61
+
62
+ /** Modulus |z| */
63
+ export function cAbs(z) { return Math.sqrt(z.re * z.re + z.im * z.im); }
64
+
65
+ /** Argument arg(z) ∈ (-π, π] */
66
+ export function cArg(z) { return Math.atan2(z.im, z.re); }
67
+
68
+ /** Convert to polar form [r, θ] */
69
+ export function toPolar(z) { return [cAbs(z), cArg(z)]; }
70
+
71
+ /** Create from polar form [r, θ] */
72
+ export function fromPolar(r, theta) { return C(r * Math.cos(theta), r * Math.sin(theta)); }
73
+
74
+ /** Square of modulus (avoids sqrt) */
75
+ export function cAbsSq(z) { return z.re * z.re + z.im * z.im; }
76
+
77
+ // ============================================================================
78
+ // COMPLEX POWERS AND ROOTS
79
+ // ============================================================================
80
+
81
+ /**
82
+ * Complex exponential: e^z = e^(a+bi) = eᵃ(cos b + i sin b)
83
+ */
84
+ export function cExp(z) {
85
+ const ea = Math.exp(z.re);
86
+ return C(ea * Math.cos(z.im), ea * Math.sin(z.im));
87
+ }
88
+
89
+ /**
90
+ * Principal complex logarithm: Ln(z) = ln|z| + i·arg(z)
91
+ */
92
+ export function cLog(z) {
93
+ if (cAbs(z) < 1e-300) throw new Error('log(0) is undefined');
94
+ return C(Math.log(cAbs(z)), cArg(z));
95
+ }
96
+
97
+ /**
98
+ * Complex power: z^w = exp(w * ln(z)) (principal value)
99
+ */
100
+ export function cPow(z, w) {
101
+ if (cAbs(z) < 1e-300) return ZERO;
102
+ return cExp(cMul(w, cLog(z)));
103
+ }
104
+
105
+ /**
106
+ * Principal complex square root.
107
+ */
108
+ export function cSqrt(z) {
109
+ const r = cAbs(z);
110
+ const theta = cArg(z);
111
+ return fromPolar(Math.sqrt(r), theta / 2);
112
+ }
113
+
114
+ /**
115
+ * All n-th roots of a complex number.
116
+ * @returns {Array<{re, im}>} n roots
117
+ */
118
+ export function cNthRoots(z, n) {
119
+ const [r, theta] = toPolar(z);
120
+ const rn = Math.pow(r, 1 / n);
121
+ return Array.from({ length: n }, (_, k) =>
122
+ fromPolar(rn, (theta + 2 * Math.PI * k) / n));
123
+ }
124
+
125
+ // ============================================================================
126
+ // COMPLEX TRIGONOMETRIC & HYPERBOLIC FUNCTIONS
127
+ // ============================================================================
128
+
129
+ export function cSin(z) { return C(Math.sin(z.re) * Math.cosh(z.im), Math.cos(z.re) * Math.sinh(z.im)); }
130
+ export function cCos(z) { return C(Math.cos(z.re) * Math.cosh(z.im), -Math.sin(z.re) * Math.sinh(z.im)); }
131
+ export function cTan(z) { return cDiv(cSin(z), cCos(z)); }
132
+ export function cSinh(z) { return C(Math.sinh(z.re) * Math.cos(z.im), Math.cosh(z.re) * Math.sin(z.im)); }
133
+ export function cCosh(z) { return C(Math.cosh(z.re) * Math.cos(z.im), Math.sinh(z.re) * Math.sin(z.im)); }
134
+ export function cTanh(z) { return cDiv(cSinh(z), cCosh(z)); }
135
+
136
+ /** Inverse sine: arcsin(z) = -i·ln(iz + sqrt(1-z²)) */
137
+ export function cAsin(z) {
138
+ const iz = cMul(I, z);
139
+ const one = ONE;
140
+ const z2 = cMul(z, z);
141
+ const sqrtPart = cSqrt(cSub(one, z2));
142
+ return cMul(C(0, -1), cLog(cAdd(iz, sqrtPart)));
143
+ }
144
+
145
+ /** Inverse cosine */
146
+ export function cAcos(z) {
147
+ return cSub(C(Math.PI / 2), cAsin(z));
148
+ }
149
+
150
+ /** Inverse tangent: atan(z) = (i/2)·ln((i+z)/(i-z)) */
151
+ export function cAtan(z) {
152
+ const half_i = C(0, 0.5);
153
+ const num = cAdd(I, z);
154
+ const den = cSub(I, z);
155
+ return cMul(half_i, cLog(cDiv(num, den)));
156
+ }
157
+
158
+ // ============================================================================
159
+ // COMPLEX POLYNOMIAL OPERATIONS
160
+ // ============================================================================
161
+
162
+ /**
163
+ * Evaluate a polynomial at a complex point.
164
+ * @param {Array<{re,im}>} coeffs - Coefficients [a0, a1, ..., an] for a0 + a1*z + ... + an*z^n
165
+ * @param {{ re, im }} z
166
+ * @returns {{ re, im }}
167
+ */
168
+ export function cPolyEval(coeffs, z) {
169
+ // Horner's method
170
+ let result = coeffs[coeffs.length - 1];
171
+ for (let i = coeffs.length - 2; i >= 0; i--) {
172
+ result = cAdd(cMul(result, z), coeffs[i]);
173
+ }
174
+ return result;
175
+ }
176
+
177
+ /**
178
+ * Find all roots of a polynomial with real or complex coefficients
179
+ * using Aberth-Ehrlich method (simultaneous root finding).
180
+ * @param {number[]} coeffs - Real coefficients [a0, a1, ..., an] (highest degree last)
181
+ * @param {number} [tol=1e-10]
182
+ * @param {number} [maxIter=1000]
183
+ * @returns {Array<{re,im}>} Array of roots
184
+ */
185
+ export function polyRoots(coeffs, tol = 1e-10, maxIter = 1000) {
186
+ const n = coeffs.length - 1; // degree
187
+ if (n <= 0) return [];
188
+ if (n === 1) return [C(-coeffs[0] / coeffs[1])];
189
+
190
+ // Monic polynomial coefficients
191
+ const lead = coeffs[n];
192
+ const monicCoeffs = coeffs.map(c => C(c / lead));
193
+
194
+ // Initial guesses evenly spaced on a circle
195
+ const radius = 1 + Math.max(...coeffs.slice(0, n).map(c => Math.abs(c / lead)));
196
+ let z = Array.from({ length: n }, (_, k) =>
197
+ fromPolar(radius, 2 * Math.PI * k / n + 0.1));
198
+
199
+ // Polynomial derivative coefficients
200
+ const dCoeffs = monicCoeffs.slice(1).map((c, k) => cScale(c, k + 1));
201
+
202
+ for (let iter = 0; iter < maxIter; iter++) {
203
+ let maxUpdate = 0;
204
+
205
+ const newZ = z.map((zi, i) => {
206
+ // f(zi) / f'(zi)
207
+ const fzi = cPolyEval(monicCoeffs, zi);
208
+ const dfzi = cPolyEval(dCoeffs, zi);
209
+ if (cAbs(dfzi) < 1e-300) return zi;
210
+
211
+ const ratio = cDiv(fzi, dfzi);
212
+
213
+ // Aberth correction
214
+ const correction = z.reduce((sum, zj, j) => {
215
+ if (j === i) return sum;
216
+ const diff = cSub(zi, zj);
217
+ return cAbs(diff) < 1e-300 ? sum : cAdd(sum, cDiv(ONE, diff));
218
+ }, ZERO);
219
+
220
+ const update = cDiv(ratio, cSub(ONE, cMul(ratio, correction)));
221
+ maxUpdate = Math.max(maxUpdate, cAbs(update));
222
+ return cSub(zi, update);
223
+ });
224
+
225
+ z = newZ;
226
+ if (maxUpdate < tol) break;
227
+ }
228
+
229
+ // Polish roots using Newton's method
230
+ return z.map(zi => {
231
+ for (let k = 0; k < 10; k++) {
232
+ const fz = cPolyEval(monicCoeffs, zi);
233
+ const dfz = cPolyEval(dCoeffs, zi);
234
+ if (cAbs(dfz) < 1e-300) break;
235
+ const step = cDiv(fz, dfz);
236
+ zi = cSub(zi, step);
237
+ if (cAbs(step) < 1e-14) break;
238
+ }
239
+ return zi;
240
+ });
241
+ }
242
+
243
+ // ============================================================================
244
+ // COMPLEX CALCULUS
245
+ // ============================================================================
246
+
247
+ /**
248
+ * Numerical complex derivative using Cauchy-Riemann equations.
249
+ * f'(z₀) = lim(h→0) [f(z₀+h) - f(z₀)] / h
250
+ * @param {Function} f - f({re,im}) → {re,im}
251
+ * @param {{ re, im }} z0
252
+ * @param {number} [h=1e-6]
253
+ * @returns {{ re, im }}
254
+ */
255
+ export function cDerivative(f, z0, h = 1e-6) {
256
+ const fz = f(z0);
257
+ // Use complex step for improved accuracy
258
+ const zh = cAdd(z0, C(h));
259
+ const fzh = f(zh);
260
+ return cDiv(cSub(fzh, fz), C(h));
261
+ }
262
+
263
+ /**
264
+ * Check if a function satisfies the Cauchy-Riemann equations at z₀.
265
+ * Returns true if the function appears holomorphic at z₀.
266
+ * @param {Function} f
267
+ * @param {{ re, im }} z0
268
+ * @param {number} [h=1e-5]
269
+ * @param {number} [tol=1e-4]
270
+ */
271
+ export function isCauchyRiemann(f, z0, h = 1e-5, tol = 1e-4) {
272
+ const x = z0.re, y = z0.im;
273
+ const fxy = f(C(x, y));
274
+ const fxhy = f(C(x + h, y));
275
+ const fxyh = f(C(x, y + h));
276
+
277
+ // ∂u/∂x ≈ (Re[f(x+h,y)] - Re[f(x,y)]) / h
278
+ const dudx = (fxhy.re - fxy.re) / h;
279
+ const dvdx = (fxhy.im - fxy.im) / h;
280
+ // ∂u/∂y ≈ (Re[f(x,y+h)] - Re[f(x,y)]) / h
281
+ const dudy = (fxyh.re - fxy.re) / h;
282
+ const dvdy = (fxyh.im - fxy.im) / h;
283
+
284
+ // CR: ∂u/∂x = ∂v/∂y and ∂u/∂y = -∂v/∂x
285
+ return Math.abs(dudx - dvdy) < tol && Math.abs(dudy + dvdx) < tol;
286
+ }
287
+
288
+ /**
289
+ * Numerical contour integration ∮_C f(z) dz
290
+ * along a parametric curve z(t), t ∈ [a, b].
291
+ * @param {Function} f - f(z) → {re,im}
292
+ * @param {Function} z - z(t) → {re,im} (curve parametrization)
293
+ * @param {Function} dz - dz/dt → {re,im} (derivative of z)
294
+ * @param {number} a - Start parameter
295
+ * @param {number} b - End parameter
296
+ * @param {number} [n=1000]
297
+ * @returns {{ re, im }}
298
+ */
299
+ export function contourIntegral(f, z, dz, a, b, n = 1000) {
300
+ const h = (b - a) / n;
301
+ let result = ZERO;
302
+
303
+ for (let k = 0; k <= n; k++) {
304
+ const t = a + k * h;
305
+ const zt = z(t);
306
+ const dzt = dz(t);
307
+ const fzt = f(zt);
308
+ const integrand = cMul(fzt, dzt);
309
+ const weight = (k === 0 || k === n) ? 0.5 : 1;
310
+ result = cAdd(result, cScale(integrand, weight * h));
311
+ }
312
+
313
+ return result;
314
+ }
315
+
316
+ /**
317
+ * Contour integration along a circle of radius r centered at z0.
318
+ * ∮_{|z-z0|=r} f(z) dz
319
+ * @param {Function} f
320
+ * @param {{ re, im }} z0 - Center
321
+ * @param {number} r - Radius
322
+ * @param {number} [n=1000]
323
+ * @returns {{ re, im }}
324
+ */
325
+ export function circleIntegral(f, z0, r, n = 1000) {
326
+ const z = t => cAdd(z0, fromPolar(r, t));
327
+ const dzdt = t => cMul(C(0, r), fromPolar(1, t)); // r·i·e^{it}
328
+ return contourIntegral(f, z, dzdt, 0, 2 * Math.PI, n);
329
+ }
330
+
331
+ /**
332
+ * Estimate residue of f at z0 using the contour integral formula:
333
+ * Res(f, z0) ≈ (1/2πi) ∮ f(z) dz (integral over small circle)
334
+ * @param {Function} f
335
+ * @param {{ re, im }} z0
336
+ * @param {number} [r=0.01]
337
+ * @returns {{ re, im }}
338
+ */
339
+ export function residue(f, z0, r = 0.01) {
340
+ const integral = circleIntegral(f, z0, r);
341
+ return cScale(integral, 1 / (2 * Math.PI));
342
+ }
343
+
344
+ // ============================================================================
345
+ // FAST FOURIER TRANSFORM (Cooley-Tukey)
346
+ // ============================================================================
347
+
348
+ /**
349
+ * Compute the Discrete Fourier Transform (DFT) of a sequence.
350
+ * @param {Array<{re,im}>} x - Input complex sequence (length must be power of 2)
351
+ * @returns {Array<{re,im}>} DFT output
352
+ */
353
+ export function fft(x) {
354
+ const n = x.length;
355
+ if (n === 1) return [x[0]];
356
+
357
+ if ((n & (n - 1)) !== 0) {
358
+ // Fall back to O(n²) DFT for non-power-of-2 lengths
359
+ return dft(x);
360
+ }
361
+
362
+ // Bit-reversal permutation
363
+ const X = x.slice();
364
+ let j = 0;
365
+ for (let i = 1; i < n; i++) {
366
+ let bit = n >> 1;
367
+ while (j & bit) { j ^= bit; bit >>= 1; }
368
+ j ^= bit;
369
+ if (i < j) { [X[i], X[j]] = [X[j], X[i]]; }
370
+ }
371
+
372
+ // Cooley-Tukey butterfly
373
+ for (let len = 2; len <= n; len <<= 1) {
374
+ const ang = -2 * Math.PI / len;
375
+ const wlen = C(Math.cos(ang), Math.sin(ang));
376
+
377
+ for (let i = 0; i < n; i += len) {
378
+ let w = ONE;
379
+ for (let k = 0; k < len / 2; k++) {
380
+ const u = X[i + k];
381
+ const v = cMul(X[i + k + len / 2], w);
382
+ X[i + k] = cAdd(u, v);
383
+ X[i + k + len / 2] = cSub(u, v);
384
+ w = cMul(w, wlen);
385
+ }
386
+ }
387
+ }
388
+
389
+ return X;
390
+ }
391
+
392
+ /**
393
+ * Inverse FFT.
394
+ * @param {Array<{re,im}>} X - Frequency domain
395
+ * @returns {Array<{re,im}>} Time domain
396
+ */
397
+ export function ifft(X) {
398
+ const n = X.length;
399
+ // Conjugate, FFT, conjugate, scale
400
+ const conj = X.map(cConj);
401
+ const result = fft(conj);
402
+ return result.map(z => cScale(cConj(z), 1 / n));
403
+ }
404
+
405
+ /**
406
+ * Real-valued FFT — input is array of numbers.
407
+ * @param {number[]} signal
408
+ * @returns {{ frequencies: number[], magnitudes: number[], phases: number[] }}
409
+ */
410
+ export function realFFT(signal) {
411
+ const X = fft(signal.map(v => C(v)));
412
+ const n = X.length;
413
+ const half = Math.floor(n / 2) + 1;
414
+
415
+ const frequencies = Array.from({ length: half }, (_, k) => k);
416
+ const magnitudes = X.slice(0, half).map(z => cAbs(z) * 2 / n);
417
+ const phases = X.slice(0, half).map(z => cArg(z));
418
+
419
+ if (half > 0) magnitudes[0] /= 2; // DC component
420
+
421
+ return { frequencies, magnitudes, phases, raw: X.slice(0, half) };
422
+ }
423
+
424
+ /**
425
+ * O(n²) DFT — fallback for non-power-of-2 lengths.
426
+ */
427
+ export function dft(x) {
428
+ const n = x.length;
429
+ return Array.from({ length: n }, (_, k) => {
430
+ return x.reduce((sum, xn, nn) => {
431
+ const angle = -2 * Math.PI * k * nn / n;
432
+ return cAdd(sum, cMul(xn, C(Math.cos(angle), Math.sin(angle))));
433
+ }, ZERO);
434
+ });
435
+ }
436
+
437
+ // ============================================================================
438
+ // DISPLAY UTILITIES
439
+ // ============================================================================
440
+
441
+ /**
442
+ * Format a complex number as a string.
443
+ * @param {{ re, im }} z
444
+ * @param {number} [decimals=4]
445
+ * @returns {string}
446
+ */
447
+ export function cToString(z, decimals = 4) {
448
+ const re = z.re.toFixed(decimals);
449
+ const im = Math.abs(z.im).toFixed(decimals);
450
+ if (Math.abs(z.im) < 1e-12) return re;
451
+ if (Math.abs(z.re) < 1e-12) return (z.im < 0 ? '-' : '') + im + 'i';
452
+ const sign = z.im < 0 ? ' - ' : ' + ';
453
+ return `${re}${sign}${im}i`;
454
+ }
455
+
456
+ /**
457
+ * Format a complex number in polar form: r∠θ°
458
+ */
459
+ export function cToPolarString(z, decimals = 4) {
460
+ const r = cAbs(z).toFixed(decimals);
461
+ const theta = (cArg(z) * 180 / Math.PI).toFixed(2);
462
+ return `${r}∠${theta}°`;
463
+ }
464
+
465
+ /**
466
+ * Convert a complex number to LaTeX.
467
+ */
468
+ export function cToLatex(z, decimals = 4) {
469
+ const re = z.re.toFixed(decimals);
470
+ const im = Math.abs(z.im).toFixed(decimals);
471
+ if (Math.abs(z.im) < 1e-12) return re;
472
+ if (Math.abs(z.re) < 1e-12) return (z.im < 0 ? '-' : '') + im + 'i';
473
+ const sign = z.im < 0 ? ' - ' : ' + ';
474
+ return `${re}${sign}${im}i`;
475
+ }
476
+
477
+
478
+ // ============================================================================
479
+ // MÖBIUS TRANSFORMATIONS
480
+ // ============================================================================
481
+
482
+ /**
483
+ * Möbius (linear fractional) transformation: w = (az + b) / (cz + d)
484
+ * @param {{ re, im }} z
485
+ * @param {{ re, im }} a
486
+ * @param {{ re, im }} b
487
+ * @param {{ re, im }} c
488
+ * @param {{ re, im }} d
489
+ * @returns {{ re, im }}
490
+ */
491
+ export function mobiusTransform(z, a, b, c, d) {
492
+ const num = cAdd(cMul(a, z), b);
493
+ const den = cAdd(cMul(c, z), d);
494
+ return cDiv(num, den);
495
+ }
496
+
497
+ /**
498
+ * Fixed points of a Möbius transformation: w = z → cz² + (d-a)z - b = 0.
499
+ * @returns {{ re, im }[]}
500
+ */
501
+ export function mobiusFixedPoints(a, b, c, d) {
502
+ if (Math.abs(c.re) + Math.abs(c.im) < 1e-14) {
503
+ // Degenerate (translation/scaling): z = b/(a-d)
504
+ return [cDiv(b, cSub(a, d))];
505
+ }
506
+ // cz² + (d-a)z - b = 0
507
+ const A = c, B = cSub(d, a), Cneg = cScale(b, -1);
508
+ // z = [-B ± sqrt(B²+4AC)] / 2A
509
+ const disc = cAdd(cMul(B, B), cScale(cMul(A, Cneg), 4));
510
+ const sqD = cSqrt(disc);
511
+ const z1 = cDiv(cAdd(cScale(B, -1), sqD), cScale(A, 2));
512
+ const z2 = cDiv(cSub(cScale(B, -1), sqD), cScale(A, 2));
513
+ return [z1, z2];
514
+ }
515
+
516
+ // ============================================================================
517
+ // POWER SERIES
518
+ // ============================================================================
519
+
520
+ /**
521
+ * Evaluate a complex power series Σ cₙ (z - z0)^n.
522
+ * @param {number[]} coeffs real coefficients (index = power)
523
+ * @param {{ re, im }} z
524
+ * @param {{ re, im }} [z0] center (default 0)
525
+ * @returns {{ re, im }}
526
+ */
527
+ export function powerSeries(coeffs, z, z0 = { re: 0, im: 0 }) {
528
+ const w = cSub(z, z0);
529
+ return coeffs.reduce((sum, c, n) => {
530
+ const term = cScale(cPow(w, { re: n, im: 0 }), c);
531
+ return cAdd(sum, term);
532
+ }, { re: 0, im: 0 });
533
+ }
534
+
535
+ /**
536
+ * Radius of convergence using the ratio test (successive coefficients).
537
+ * R = lim |cₙ/cₙ₊₁| as n → ∞
538
+ */
539
+ export function radiusOfConvergence(coeffs) {
540
+ const nonZero = coeffs.filter(c => Math.abs(c) > 1e-15);
541
+ if (nonZero.length < 2) return Infinity;
542
+ const last = nonZero[nonZero.length - 1];
543
+ const secondLast = nonZero[nonZero.length - 2];
544
+ return Math.abs(secondLast / last);
545
+ }
546
+
547
+ // ============================================================================
548
+ // CONFORMAL MAPPING HELPERS
549
+ // ============================================================================
550
+
551
+ /** Map z → z² */
552
+ export const conformalSquare = z => cMul(z, z);
553
+
554
+ /** Map z → e^z */
555
+ export const conformalExp = z => cExp(z);
556
+
557
+ /** Map z → ln(z) */
558
+ export const conformalLog = z => cLog(z);
559
+
560
+ /** Joukowski transform z → z + 1/z (used in airfoil analysis) */
561
+ export function joukowski(z) {
562
+ return cAdd(z, cDiv({ re: 1, im: 0 }, z));
563
+ }
564
+
565
+ /**
566
+ * Map a polygon of complex vertices through a conformal map f.
567
+ * @param {{ re, im }[]} vertices
568
+ * @param {Function} f complex function
569
+ * @returns {{ re, im }[]}
570
+ */
571
+ export function mapPolygon(vertices, f) {
572
+ return vertices.map(f);
573
+ }
574
+
575
+ // ============================================================================
576
+ // COMPLEX INTEGRATION — EXTENDED
577
+ // ============================================================================
578
+
579
+ /**
580
+ * Compute residue at a pole z0 using the limit formula for simple poles:
581
+ * Res(f, z0) = lim_{z→z0} (z - z0)·f(z)
582
+ * @param {Function} f complex function z → { re, im }
583
+ * @param {{ re, im }} z0 pole location
584
+ * @param {number} [eps=1e-7]
585
+ */
586
+ export function residueAtPole(f, z0, eps = 1e-7) {
587
+ const z = { re: z0.re + eps, im: z0.im };
588
+ const fz = f(z);
589
+ const zMinusZ0 = cSub(z, z0);
590
+ return cMul(zMinusZ0, fz);
591
+ }
592
+
593
+ /**
594
+ * Cauchy's Integral Formula: f(z0) = (1/2πi) ∮_C f(z)/(z-z0) dz
595
+ * Numerically computes the contour integral and recovers f(z0).
596
+ * @param {Function} f
597
+ * @param {{ re, im }} z0 interior point
598
+ * @param {number} [r=1] radius of circular contour
599
+ * @param {number} [n=1000]
600
+ */
601
+ export function cauchyIntegralFormula(f, z0, r = 1, n = 1000) {
602
+ const g = z => cDiv(f(z), cSub(z, z0));
603
+ const integral = circleIntegral(g, z0, r, n);
604
+ return cScale(integral, 1 / (2 * Math.PI));
605
+ }
606
+
607
+ // ============================================================================
608
+ // NUMERICAL DIFFERENTIATION IN ℂ
609
+ // ============================================================================
610
+
611
+ /**
612
+ * Second-order complex derivative using central differences.
613
+ * @param {Function} f z → { re, im }
614
+ * @param {{ re, im }} z0
615
+ * @param {number} [h=1e-5]
616
+ */
617
+ export function cSecondDerivative(f, z0, h = 1e-5) {
618
+ const zp = { re: z0.re + h, im: z0.im };
619
+ const zm = { re: z0.re - h, im: z0.im };
620
+ const fzp = f(zp), fz = f(z0), fzm = f(zm);
621
+ return cScale(cAdd(cSub(fzp, cScale(fz, 2)), fzm), 1 / (h * h));
622
+ }
623
+
624
+ /**
625
+ * Check if f is analytic at z0 (both C-R equations satisfied).
626
+ * @returns {{ analytic: boolean, error: number }}
627
+ */
628
+ export function isAnalytic(f, z0, h = 1e-5) {
629
+ const { uX, uY, vX, vY } = _partials(f, z0, h);
630
+ const crError = Math.max(Math.abs(uX - vY), Math.abs(uY + vX));
631
+ return { analytic: crError < 1e-4, error: crError };
632
+ }
633
+
634
+ function _partials(f, z0, h) {
635
+ const fR = z => f(z).re, fI = z => f(z).im;
636
+ const zxp = { re: z0.re + h, im: z0.im }, zxm = { re: z0.re - h, im: z0.im };
637
+ const zyp = { re: z0.re, im: z0.im + h }, zym = { re: z0.re, im: z0.im - h };
638
+ return {
639
+ uX: (fR(zxp) - fR(zxm)) / (2 * h),
640
+ uY: (fR(zyp) - fR(zym)) / (2 * h),
641
+ vX: (fI(zxp) - fI(zxm)) / (2 * h),
642
+ vY: (fI(zyp) - fI(zym)) / (2 * h),
643
+ };
644
+ }
645
+
646
+ // ============================================================================
647
+ // QUATERNIONS (ℍ — 4D hypercomplex)
648
+ // ============================================================================
649
+
650
+ /**
651
+ * Quaternion: { w, x, y, z } (real part + 3 imaginary components)
652
+ * Useful for 3D rotation and orientation mathematics.
653
+ */
654
+
655
+ export const Q = (w, x, y, z) => ({ w, x, y, z });
656
+ export const qAdd = (a, b) => Q(a.w+b.w, a.x+b.x, a.y+b.y, a.z+b.z);
657
+ export const qSub = (a, b) => Q(a.w-b.w, a.x-b.x, a.y-b.y, a.z-b.z);
658
+ export const qScale = (q, s) => Q(q.w*s, q.x*s, q.y*s, q.z*s);
659
+ export const qConj = q => Q(q.w, -q.x, -q.y, -q.z);
660
+ export const qNormSq = q => q.w**2 + q.x**2 + q.y**2 + q.z**2;
661
+ export const qNorm = q => Math.sqrt(qNormSq(q));
662
+ export const qNormalize = q => qScale(q, 1 / qNorm(q));
663
+
664
+ /** Hamilton product: p·q */
665
+ export function qMul(p, q) {
666
+ return Q(
667
+ p.w*q.w - p.x*q.x - p.y*q.y - p.z*q.z,
668
+ p.w*q.x + p.x*q.w + p.y*q.z - p.z*q.y,
669
+ p.w*q.y - p.x*q.z + p.y*q.w + p.z*q.x,
670
+ p.w*q.z + p.x*q.y - p.y*q.x + p.z*q.w
671
+ );
672
+ }
673
+
674
+ /** Inverse of a quaternion. */
675
+ export function qInv(q) {
676
+ const ns = qNormSq(q);
677
+ return qScale(qConj(q), 1 / ns);
678
+ }
679
+
680
+ /**
681
+ * Rotate a 3D vector v = [x, y, z] by a unit quaternion q.
682
+ * Uses: v' = q·p·q⁻¹ where p = Q(0, vx, vy, vz)
683
+ */
684
+ export function qRotateVec(q, v) {
685
+ const p = Q(0, v[0], v[1], v[2]);
686
+ const r = qMul(qMul(q, p), qConj(q));
687
+ return [r.x, r.y, r.z];
688
+ }
689
+
690
+ /**
691
+ * Convert axis-angle representation to quaternion.
692
+ * @param {number[]} axis unit vector [x,y,z]
693
+ * @param {number} angle radians
694
+ */
695
+ export function axisAngleToQuat(axis, angle) {
696
+ const s = Math.sin(angle / 2);
697
+ return qNormalize(Q(Math.cos(angle / 2), axis[0]*s, axis[1]*s, axis[2]*s));
698
+ }
699
+
700
+ /**
701
+ * Spherical linear interpolation (SLERP) between two unit quaternions.
702
+ * @param {Object} q0 start quaternion
703
+ * @param {Object} q1 end quaternion
704
+ * @param {number} t parameter in [0, 1]
705
+ */
706
+ export function qSlerp(q0, q1, t) {
707
+ let dot = q0.w*q1.w + q0.x*q1.x + q0.y*q1.y + q0.z*q1.z;
708
+ if (dot < 0) { q1 = qScale(q1, -1); dot = -dot; }
709
+ if (dot > 0.9995) {
710
+ return qNormalize(qAdd(q0, qScale(qSub(q1, q0), t)));
711
+ }
712
+ const theta0 = Math.acos(dot);
713
+ const theta = theta0 * t;
714
+ const sinT0 = Math.sin(theta0);
715
+ const s0 = Math.cos(theta) - dot * Math.sin(theta) / sinT0;
716
+ const s1 = Math.sin(theta) / sinT0;
717
+ return qNormalize(Q(
718
+ s0*q0.w + s1*q1.w, s0*q0.x + s1*q1.x,
719
+ s0*q0.y + s1*q1.y, s0*q0.z + s1*q1.z
720
+ ));
721
+ }
722
+
723
+
724
+ // ============================================================================
725
+ // DISCRETE FOURIER TRANSFORM — Extended
726
+ // ============================================================================
727
+
728
+ /**
729
+ * Short-Time Fourier Transform (STFT).
730
+ * Splits signal into overlapping windows and computes FFT on each.
731
+ *
732
+ * @param {number[]} signal
733
+ * @param {number} windowSize must be power of 2
734
+ * @param {number} hopSize samples between windows
735
+ * @param {string} [window='hann'] windowing function: 'hann','hamming','rect'
736
+ * @returns {{ timeFrames: number[], frequencies: number[], magnitudes: number[][] }}
737
+ */
738
+ export function stft(signal, windowSize, hopSize, window = 'hann') {
739
+ const W = _makeWindow(windowSize, window);
740
+ const frames = [];
741
+ for (let start = 0; start + windowSize <= signal.length; start += hopSize) {
742
+ const frame = signal.slice(start, start + windowSize).map((v, i) => v * W[i]);
743
+ const complexFrame = frame.map(re => ({ re, im: 0 }));
744
+ frames.push(fft(complexFrame));
745
+ }
746
+ const nFrames = frames.length;
747
+ const timeFrames = Array.from({ length: nFrames }, (_, i) => i * hopSize);
748
+ const frequencies = Array.from({ length: windowSize / 2 }, (_, k) => k);
749
+ const magnitudes = frames.map(frame =>
750
+ frame.slice(0, windowSize / 2).map(c => cAbs(c))
751
+ );
752
+ return { timeFrames, frequencies, magnitudes, frames };
753
+ }
754
+
755
+ function _makeWindow(n, type) {
756
+ return Array.from({ length: n }, (_, i) => {
757
+ switch (type) {
758
+ case 'hann': return 0.5 * (1 - Math.cos(2 * Math.PI * i / (n - 1)));
759
+ case 'hamming': return 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (n - 1));
760
+ default: return 1; // rect
761
+ }
762
+ });
763
+ }
764
+
765
+ /**
766
+ * Compute power spectral density (PSD) via Welch's method.
767
+ * Averages squared magnitudes across overlapping STFT windows.
768
+ *
769
+ * @param {number[]} signal
770
+ * @param {number} windowSize
771
+ * @param {number} [overlap=0.5] fraction overlap
772
+ * @returns {{ frequencies: number[], psd: number[] }}
773
+ */
774
+ export function welchPSD(signal, windowSize, overlap = 0.5) {
775
+ const hopSize = Math.floor(windowSize * (1 - overlap));
776
+ const { magnitudes, frequencies } = stft(signal, windowSize, hopSize);
777
+ const n = magnitudes.length;
778
+ const psd = frequencies.map((_, k) =>
779
+ magnitudes.reduce((s, frame) => s + frame[k] ** 2, 0) / n
780
+ );
781
+ return { frequencies, psd };
782
+ }
783
+
784
+ // ============================================================================
785
+ // SIGNAL PROCESSING
786
+ // ============================================================================
787
+
788
+ /**
789
+ * Convolve two real sequences (linear convolution).
790
+ * O(n·m) naive implementation; use FFT-based for large arrays.
791
+ * @param {number[]} x
792
+ * @param {number[]} h
793
+ * @returns {number[]} length n+m-1
794
+ */
795
+ export function convolve(x, h) {
796
+ const n = x.length, m = h.length;
797
+ const out = Array(n + m - 1).fill(0);
798
+ for (let i = 0; i < n; i++)
799
+ for (let j = 0; j < m; j++)
800
+ out[i + j] += x[i] * h[j];
801
+ return out;
802
+ }
803
+
804
+ /**
805
+ * Cross-correlation of two real sequences x and y.
806
+ * @returns {number[]} lags from -(m-1) to (n-1)
807
+ */
808
+ export function crossCorrelation(x, y) {
809
+ return convolve(x, y.slice().reverse());
810
+ }
811
+
812
+ /**
813
+ * Design a simple FIR low-pass filter using windowed-sinc method.
814
+ * @param {number} cutoff normalised cutoff frequency ∈ (0, 1) [1 = Nyquist]
815
+ * @param {number} order filter order (should be even)
816
+ * @param {string} [window='hann']
817
+ * @returns {number[]} filter coefficients (impulse response)
818
+ */
819
+ export function firLowPass(cutoff, order, window = 'hann') {
820
+ const M = order % 2 === 0 ? order : order + 1;
821
+ const W = _makeWindow(M + 1, window);
822
+ return Array.from({ length: M + 1 }, (_, n) => {
823
+ const i = n - M / 2;
824
+ const sinc = i === 0 ? cutoff : Math.sin(Math.PI * cutoff * i) / (Math.PI * i);
825
+ return sinc * W[n];
826
+ });
827
+ }
828
+
829
+ /**
830
+ * Apply an FIR filter to a signal.
831
+ * @param {number[]} signal
832
+ * @param {number[]} coeffs filter coefficients
833
+ */
834
+ export function applyFIR(signal, coeffs) {
835
+ const M = coeffs.length;
836
+ return signal.map((_, n) =>
837
+ coeffs.reduce((s, c, k) => s + c * (signal[n - k] || 0), 0)
838
+ );
839
+ }
840
+
841
+ // ============================================================================
842
+ // COMPLEX MATRIX OPERATIONS
843
+ // ============================================================================
844
+
845
+ /**
846
+ * Multiply two complex matrices.
847
+ * Each matrix is a 2D array of { re, im }.
848
+ */
849
+ export function cMatMul(A, B) {
850
+ const m = A.length, n = B[0].length, p = B.length;
851
+ return Array.from({ length: m }, (_, i) =>
852
+ Array.from({ length: n }, (_, j) =>
853
+ A[i].reduce((s, aik, k) => cAdd(s, cMul(aik, B[k][j])), { re: 0, im: 0 })
854
+ )
855
+ );
856
+ }
857
+
858
+ /**
859
+ * Conjugate transpose (Hermitian transpose) of a complex matrix.
860
+ */
861
+ export function cMatH(A) {
862
+ return A[0].map((_, j) => A.map((row, i) => cConj(A[i][j])));
863
+ }
864
+
865
+ /**
866
+ * Check if a complex matrix is unitary: A†·A = I
867
+ */
868
+ export function isUnitary(A, tol = 1e-8) {
869
+ const AH = cMatH(A);
870
+ const prod = cMatMul(AH, A);
871
+ const n = prod.length;
872
+ for (let i = 0; i < n; i++) {
873
+ for (let j = 0; j < n; j++) {
874
+ const expected = i === j ? 1 : 0;
875
+ if (Math.abs(prod[i][j].re - expected) > tol) return false;
876
+ if (Math.abs(prod[i][j].im) > tol) return false;
877
+ }
878
+ }
879
+ return true;
880
+ }
881
+
882
+ // ============================================================================
883
+ // SPECIAL COMPLEX SEQUENCES
884
+ // ============================================================================
885
+
886
+ /**
887
+ * Generate N evenly spaced points on the unit circle in ℂ.
888
+ * @param {number} N
889
+ * @returns {{ re, im }[]}
890
+ */
891
+ export function unitCirclePoints(N) {
892
+ return Array.from({ length: N }, (_, k) => ({
893
+ re: Math.cos(2 * Math.PI * k / N),
894
+ im: Math.sin(2 * Math.PI * k / N),
895
+ }));
896
+ }
897
+
898
+ /**
899
+ * DFT matrix W of order N: W_{jk} = e^{-2πijk/N}
900
+ */
901
+ export function dftMatrix(N) {
902
+ return Array.from({ length: N }, (_, j) =>
903
+ Array.from({ length: N }, (_, k) => ({
904
+ re: Math.cos(2 * Math.PI * j * k / N),
905
+ im: -Math.sin(2 * Math.PI * j * k / N),
906
+ }))
907
+ );
908
+ }
909
+
910
+ /**
911
+ * Chebyshev nodes in the complex plane on [-1, 1] (useful for interpolation).
912
+ * @param {number} N
913
+ */
914
+ export function chebyshevNodes(N) {
915
+ return Array.from({ length: N }, (_, k) => ({
916
+ re: Math.cos((2 * k + 1) * Math.PI / (2 * N)),
917
+ im: 0,
918
+ }));
919
+ }
920
+
921
+ // ============================================================================
922
+ // NUMBER THEORY HELPERS (useful with complex arithmetic)
923
+ // ============================================================================
924
+
925
+ /**
926
+ * Gaussian integers: check if a complex number is a Gaussian integer.
927
+ */
928
+ export function isGaussianInteger(z, tol = 1e-9) {
929
+ return Math.abs(z.re - Math.round(z.re)) < tol
930
+ && Math.abs(z.im - Math.round(z.im)) < tol;
931
+ }
932
+
933
+ /**
934
+ * Gaussian integer norm: N(a+bi) = a²+b²
935
+ */
936
+ export function gaussianNorm(z) { return z.re * z.re + z.im * z.im; }
937
+
938
+ /**
939
+ * Riemann zeta function ζ(s) for complex s with Re(s) > 1.
940
+ * Uses the Euler-Maclaurin formula (100-term approximation).
941
+ * @param {{ re, im }} s
942
+ * @returns {{ re, im }}
943
+ */
944
+ export function riemannZeta(s, terms = 100) {
945
+ let sum = { re: 0, im: 0 };
946
+ for (let n = 1; n <= terms; n++) {
947
+ // n^{-s} = e^{-s·ln(n)} = e^{-(σ·ln(n))} · e^{-iτ·ln(n)}
948
+ const lnN = Math.log(n);
949
+ const mag = Math.exp(-s.re * lnN);
950
+ const phase = -s.im * lnN;
951
+ sum = cAdd(sum, { re: mag * Math.cos(phase), im: mag * Math.sin(phase) });
952
+ }
953
+ return sum;
954
+ }