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.
- package/package.json +54 -23
- package/slang-complex.js +954 -0
- package/slang-linalg.js +1156 -0
- package/slang-math.js +83 -8
- package/slang-ode.js +1082 -0
- package/slang-stats.js +1206 -0
- package/slang-symbolic.js +1616 -0
package/slang-complex.js
ADDED
|
@@ -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
|
+
}
|