@johnhenry/math 0.2.0 → 0.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.
Files changed (52) hide show
  1. package/README.md +4 -3
  2. package/dist/Distributions.d.ts +86 -16
  3. package/dist/Distributions.d.ts.map +1 -1
  4. package/dist/Distributions.js +340 -89
  5. package/dist/Distributions.js.map +1 -1
  6. package/dist/EigenGeneral.d.ts +22 -0
  7. package/dist/EigenGeneral.d.ts.map +1 -0
  8. package/dist/EigenGeneral.js +327 -0
  9. package/dist/EigenGeneral.js.map +1 -0
  10. package/dist/Frobenius.d.ts +60 -0
  11. package/dist/Frobenius.d.ts.map +1 -0
  12. package/dist/Frobenius.js +162 -0
  13. package/dist/Frobenius.js.map +1 -0
  14. package/dist/Graph.d.ts +26 -0
  15. package/dist/Graph.d.ts.map +1 -1
  16. package/dist/Graph.js +75 -0
  17. package/dist/Graph.js.map +1 -1
  18. package/dist/MatrixMath.d.ts +10 -0
  19. package/dist/MatrixMath.d.ts.map +1 -1
  20. package/dist/MatrixMath.js +11 -0
  21. package/dist/MatrixMath.js.map +1 -1
  22. package/dist/MonteCarlo.d.ts +106 -0
  23. package/dist/MonteCarlo.d.ts.map +1 -0
  24. package/dist/MonteCarlo.js +183 -0
  25. package/dist/MonteCarlo.js.map +1 -0
  26. package/dist/Random.d.ts +31 -0
  27. package/dist/Random.d.ts.map +1 -0
  28. package/dist/Random.js +95 -0
  29. package/dist/Random.js.map +1 -0
  30. package/dist/Regression.d.ts +114 -0
  31. package/dist/Regression.d.ts.map +1 -0
  32. package/dist/Regression.js +338 -0
  33. package/dist/Regression.js.map +1 -0
  34. package/dist/SpecialFunctions.d.ts +3 -3
  35. package/dist/SpecialFunctions.d.ts.map +1 -1
  36. package/dist/SpecialFunctions.js +61 -51
  37. package/dist/SpecialFunctions.js.map +1 -1
  38. package/dist/index.d.ts +6 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -0
  41. package/dist/index.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/Distributions.ts +391 -94
  44. package/src/EigenGeneral.ts +342 -0
  45. package/src/Frobenius.ts +203 -0
  46. package/src/Graph.ts +68 -0
  47. package/src/MatrixMath.ts +13 -0
  48. package/src/MonteCarlo.ts +257 -0
  49. package/src/Random.ts +104 -0
  50. package/src/Regression.ts +423 -0
  51. package/src/SpecialFunctions.ts +52 -48
  52. package/src/index.ts +33 -0
@@ -0,0 +1,342 @@
1
+ import { ComplexNumber } from "./ComplexNumber.ts";
2
+
3
+ /**
4
+ * General (non-symmetric) eigen solver: Householder reduction to upper
5
+ * Hessenberg form, then single-shift QR iteration (Wilkinson shifts, Givens
6
+ * rotations, deflation) in complex arithmetic until the matrix is the complex
7
+ * Schur form `A = Z·T·Zᴴ`; eigenvectors come from back-substitution on the
8
+ * triangular `T`. Real input with conjugate eigenvalue pairs works without a
9
+ * separate real-Schur code path, at the cost of doing the arithmetic in ℂ.
10
+ *
11
+ * Mirrors `scipy.linalg.eig` / `numpy.linalg.eig` (right eigenvectors,
12
+ * columns of `vectors`, unit 2-norm). Eigenvalues are sorted by descending
13
+ * real part, then descending imaginary part; each eigenvector's largest
14
+ * component is real and positive (LAPACK leaves that sign arbitrary).
15
+ */
16
+
17
+ export interface GeneralEigenResult {
18
+ /** Eigenvalues, `values[k]` pairs with column `k` of `vectors`. */
19
+ values: ComplexNumber[];
20
+ /** `n×n` matrix whose column `k` is the unit-norm eigenvector for `values[k]`. */
21
+ vectors: ComplexNumber[][];
22
+ }
23
+
24
+ type CMat = { re: number[][]; im: number[][] };
25
+
26
+ const zeros = (n: number): number[][] => Array.from({ length: n }, () => new Array<number>(n).fill(0));
27
+
28
+ export function eigenGeneral(
29
+ input: ReadonlyArray<ReadonlyArray<number | ComplexNumber>>,
30
+ maxIterations = 60,
31
+ ): GeneralEigenResult {
32
+ const n = input.length;
33
+ if (n === 0) return { values: [], vectors: [] };
34
+ const H: CMat = { re: zeros(n), im: zeros(n) };
35
+ for (let i = 0; i < n; i++) {
36
+ if (input[i].length !== n) throw new Error("eigen requires a square matrix.");
37
+ for (let j = 0; j < n; j++) {
38
+ const v = input[i][j];
39
+ if (typeof v === "number") H.re[i][j] = v;
40
+ else {
41
+ H.re[i][j] = v.re;
42
+ H.im[i][j] = v.im;
43
+ }
44
+ if (!Number.isFinite(H.re[i][j]) || !Number.isFinite(H.im[i][j]))
45
+ throw new Error("eigen requires finite matrix entries.");
46
+ }
47
+ }
48
+ const Z: CMat = { re: zeros(n), im: zeros(n) };
49
+ for (let i = 0; i < n; i++) Z.re[i][i] = 1;
50
+
51
+ let norm = 0;
52
+ for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) norm = Math.max(norm, Math.hypot(H.re[i][j], H.im[i][j]));
53
+ if (norm === 0) norm = 1;
54
+
55
+ hessenberg(H, Z, n);
56
+ schur(H, Z, n, norm, maxIterations);
57
+
58
+ // eigenvalues & vectors
59
+ const tNorm = norm * n;
60
+ const lam: Array<[number, number]> = [];
61
+ for (let k = 0; k < n; k++) lam.push([H.re[k][k], H.im[k][k]]);
62
+ const realTol = 1e-12 * norm;
63
+ const realInput = input.every((row) => row.every((v) => typeof v === "number" || v.im === 0));
64
+
65
+ const cols: Array<{ lre: number; lim: number; vre: number[]; vim: number[] }> = [];
66
+ for (let k = 0; k < n; k++) {
67
+ // back-substitute (T - λ_k I) y = 0 with y_k = 1
68
+ const yr = new Array<number>(n).fill(0);
69
+ const yi = new Array<number>(n).fill(0);
70
+ yr[k] = 1;
71
+ for (let i = k - 1; i >= 0; i--) {
72
+ let sr = 0;
73
+ let si = 0;
74
+ for (let j = i + 1; j <= k; j++) {
75
+ sr += H.re[i][j] * yr[j] - H.im[i][j] * yi[j];
76
+ si += H.re[i][j] * yi[j] + H.im[i][j] * yr[j];
77
+ }
78
+ let dr = H.re[i][i] - lam[k][0];
79
+ let di = H.im[i][i] - lam[k][1];
80
+ if (Math.hypot(dr, di) < Number.EPSILON * tNorm) {
81
+ dr = Number.EPSILON * tNorm;
82
+ di = 0;
83
+ }
84
+ const d2 = dr * dr + di * di;
85
+ yr[i] = -(sr * dr + si * di) / d2;
86
+ yi[i] = -(si * dr - sr * di) / d2;
87
+ }
88
+ // v = Z y
89
+ const vre = new Array<number>(n).fill(0);
90
+ const vim = new Array<number>(n).fill(0);
91
+ for (let i = 0; i < n; i++) {
92
+ for (let j = 0; j <= k; j++) {
93
+ vre[i] += Z.re[i][j] * yr[j] - Z.im[i][j] * yi[j];
94
+ vim[i] += Z.re[i][j] * yi[j] + Z.im[i][j] * yr[j];
95
+ }
96
+ }
97
+ // unit norm, largest component real positive
98
+ let nrm = 0;
99
+ let big = 0;
100
+ let bigIdx = 0;
101
+ for (let i = 0; i < n; i++) {
102
+ const m = Math.hypot(vre[i], vim[i]);
103
+ nrm += m * m;
104
+ if (m > big * (1 + 1e-12)) {
105
+ big = m;
106
+ bigIdx = i;
107
+ }
108
+ }
109
+ nrm = Math.sqrt(nrm);
110
+ const pr = vre[bigIdx] / big;
111
+ const pi = -vim[bigIdx] / big; // conj(phase)
112
+ for (let i = 0; i < n; i++) {
113
+ const r = (vre[i] * pr - vim[i] * pi) / nrm;
114
+ const m = (vre[i] * pi + vim[i] * pr) / nrm;
115
+ vre[i] = r;
116
+ vim[i] = m;
117
+ }
118
+ let lre = lam[k][0];
119
+ let lim = lam[k][1];
120
+ if (Math.abs(lim) <= realTol && realInput) {
121
+ lim = 0;
122
+ for (let i = 0; i < n; i++) if (Math.abs(vim[i]) <= 1e-10) vim[i] = 0;
123
+ }
124
+ if (lre === 0) lre = 0; // normalise -0
125
+ cols.push({ lre, lim, vre, vim });
126
+ }
127
+
128
+ cols.sort((a, b) => (Math.abs(a.lre - b.lre) <= realTol ? b.lim - a.lim : b.lre - a.lre));
129
+ return {
130
+ values: cols.map((c) => new ComplexNumber(c.lre, c.lim)),
131
+ vectors: Array.from({ length: n }, (_, i) => cols.map((c) => new ComplexNumber(c.vre[i], c.vim[i]))),
132
+ };
133
+ }
134
+
135
+ /** Householder reduction to upper Hessenberg form, accumulating the unitary Z (`A = Z H Zᴴ`). */
136
+ function hessenberg(H: CMat, Z: CMat, n: number): void {
137
+ for (let k = 0; k < n - 2; k++) {
138
+ let alpha = 0;
139
+ for (let i = k + 1; i < n; i++) alpha += H.re[i][k] ** 2 + H.im[i][k] ** 2;
140
+ alpha = Math.sqrt(alpha);
141
+ if (alpha === 0) continue;
142
+ const x0 = Math.hypot(H.re[k + 1][k], H.im[k + 1][k]);
143
+ // v = x + e^{i arg(x0)} alpha e1
144
+ const pr = x0 === 0 ? 1 : H.re[k + 1][k] / x0;
145
+ const pi = x0 === 0 ? 0 : H.im[k + 1][k] / x0;
146
+ const vr = new Array<number>(n).fill(0);
147
+ const vi = new Array<number>(n).fill(0);
148
+ for (let i = k + 1; i < n; i++) {
149
+ vr[i] = H.re[i][k];
150
+ vi[i] = H.im[i][k];
151
+ }
152
+ vr[k + 1] += pr * alpha;
153
+ vi[k + 1] += pi * alpha;
154
+ let vv = 0;
155
+ for (let i = k + 1; i < n; i++) vv += vr[i] ** 2 + vi[i] ** 2;
156
+ if (vv === 0) continue;
157
+ // H <- P H, P = I - 2 v vᴴ / vᴴv (rows k+1.., all columns)
158
+ for (let j = 0; j < n; j++) {
159
+ let sr = 0;
160
+ let si = 0;
161
+ for (let i = k + 1; i < n; i++) {
162
+ sr += vr[i] * H.re[i][j] + vi[i] * H.im[i][j];
163
+ si += vr[i] * H.im[i][j] - vi[i] * H.re[i][j];
164
+ }
165
+ sr = (2 * sr) / vv;
166
+ si = (2 * si) / vv;
167
+ for (let i = k + 1; i < n; i++) {
168
+ H.re[i][j] -= vr[i] * sr - vi[i] * si;
169
+ H.im[i][j] -= vr[i] * si + vi[i] * sr;
170
+ }
171
+ }
172
+ // H <- H P and Z <- Z P (columns k+1..)
173
+ for (const M of [H, Z]) {
174
+ for (let i = 0; i < n; i++) {
175
+ let sr = 0;
176
+ let si = 0;
177
+ for (let j = k + 1; j < n; j++) {
178
+ sr += M.re[i][j] * vr[j] - M.im[i][j] * vi[j];
179
+ si += M.re[i][j] * vi[j] + M.im[i][j] * vr[j];
180
+ }
181
+ sr = (2 * sr) / vv;
182
+ si = (2 * si) / vv;
183
+ for (let j = k + 1; j < n; j++) {
184
+ M.re[i][j] -= sr * vr[j] + si * vi[j];
185
+ M.im[i][j] -= si * vr[j] - sr * vi[j];
186
+ }
187
+ }
188
+ }
189
+ for (let i = k + 2; i < n; i++) {
190
+ H.re[i][k] = 0;
191
+ H.im[i][k] = 0;
192
+ }
193
+ }
194
+ }
195
+
196
+ /** Shifted QR iteration on a Hessenberg matrix until upper triangular (complex Schur form). */
197
+ function schur(H: CMat, Z: CMat, n: number, norm: number, maxIterations: number): void {
198
+ let hi = n - 1;
199
+ let iter = 0;
200
+ let total = 0;
201
+ while (hi > 0) {
202
+ // find the start of the active unreduced block
203
+ let l = hi;
204
+ while (l > 0) {
205
+ const s = Math.hypot(H.re[l - 1][l - 1], H.im[l - 1][l - 1]) + Math.hypot(H.re[l][l], H.im[l][l]);
206
+ const sub = Math.hypot(H.re[l][l - 1], H.im[l][l - 1]);
207
+ if (sub <= Number.EPSILON * (s === 0 ? norm : s)) {
208
+ H.re[l][l - 1] = 0;
209
+ H.im[l][l - 1] = 0;
210
+ break;
211
+ }
212
+ l--;
213
+ }
214
+ if (l === hi) {
215
+ hi--;
216
+ iter = 0;
217
+ continue;
218
+ }
219
+ if (++iter > maxIterations) throw new Error("eigen: QR iteration failed to converge.");
220
+ if (++total > 1000 * n) throw new Error("eigen: QR iteration failed to converge.");
221
+
222
+ // Wilkinson shift: eigenvalue of the trailing 2x2 closer to H[hi][hi]
223
+ let mur: number;
224
+ let mui: number;
225
+ if (iter % 10 === 0) {
226
+ // exceptional shift
227
+ mur = H.re[hi][hi] + Math.hypot(H.re[hi][hi - 1], H.im[hi][hi - 1]);
228
+ mui = H.im[hi][hi];
229
+ } else {
230
+ const ar = H.re[hi - 1][hi - 1];
231
+ const ai = H.im[hi - 1][hi - 1];
232
+ const br = H.re[hi - 1][hi];
233
+ const bi = H.im[hi - 1][hi];
234
+ const cr = H.re[hi][hi - 1];
235
+ const ci = H.im[hi][hi - 1];
236
+ const dr = H.re[hi][hi];
237
+ const di = H.im[hi][hi];
238
+ // tr/2, disc = sqrt(((a-d)/2)^2 + bc)
239
+ const hr = (ar - dr) / 2;
240
+ const hiI = (ai - di) / 2;
241
+ const qr = hr * hr - hiI * hiI + (br * cr - bi * ci);
242
+ const qi = 2 * hr * hiI + (br * ci + bi * cr);
243
+ const qm = Math.hypot(qr, qi);
244
+ const sr = Math.sqrt((qm + qr) / 2);
245
+ const si = (qi < 0 ? -1 : 1) * Math.sqrt(Math.max(0, (qm - qr) / 2));
246
+ const mr = (ar + dr) / 2;
247
+ const mi = (ai + di) / 2;
248
+ const e1r = mr + sr;
249
+ const e1i = mi + si;
250
+ const e2r = mr - sr;
251
+ const e2i = mi - si;
252
+ if (Math.hypot(e1r - dr, e1i - di) <= Math.hypot(e2r - dr, e2i - di)) {
253
+ mur = e1r;
254
+ mui = e1i;
255
+ } else {
256
+ mur = e2r;
257
+ mui = e2i;
258
+ }
259
+ }
260
+
261
+ for (let i = l; i <= hi; i++) {
262
+ H.re[i][i] -= mur;
263
+ H.im[i][i] -= mui;
264
+ }
265
+ const rots: Array<[number, number, number]> = []; // [c, sr, si]
266
+ for (let k = l; k < hi; k++) {
267
+ const ar = H.re[k][k];
268
+ const ai = H.im[k][k];
269
+ const br = H.re[k + 1][k];
270
+ const bi = H.im[k + 1][k];
271
+ const am = Math.hypot(ar, ai);
272
+ const r = Math.hypot(am, Math.hypot(br, bi));
273
+ let c: number;
274
+ let sr: number;
275
+ let si: number;
276
+ if (r === 0) {
277
+ c = 1;
278
+ sr = 0;
279
+ si = 0;
280
+ } else if (am === 0) {
281
+ c = 0;
282
+ // s = conj(b)/|b|
283
+ const bm = Math.hypot(br, bi);
284
+ sr = br / bm;
285
+ si = -bi / bm;
286
+ } else {
287
+ c = am / r;
288
+ // s = (a/|a|) conj(b) / r
289
+ const pr = ar / am;
290
+ const pi = ai / am;
291
+ sr = (pr * br + pi * bi) / r;
292
+ si = (pi * br - pr * bi) / r;
293
+ }
294
+ rots.push([c, sr, si]);
295
+ // rows k, k+1 over columns k..n-1: [c s; -conj(s) c]
296
+ for (let j = k; j < n; j++) {
297
+ const xr = H.re[k][j];
298
+ const xi = H.im[k][j];
299
+ const yr = H.re[k + 1][j];
300
+ const yi = H.im[k + 1][j];
301
+ H.re[k][j] = c * xr + (sr * yr - si * yi);
302
+ H.im[k][j] = c * xi + (sr * yi + si * yr);
303
+ H.re[k + 1][j] = -(sr * xr + si * xi) + c * yr;
304
+ H.im[k + 1][j] = -(sr * xi - si * xr) + c * yi;
305
+ }
306
+ H.re[k + 1][k] = 0;
307
+ H.im[k + 1][k] = 0;
308
+ }
309
+ for (let idx = 0; idx < rots.length; idx++) {
310
+ const k = l + idx;
311
+ const [c, sr, si] = rots[idx];
312
+ const top = Math.min(hi, k + 1);
313
+ // columns k, k+1 over rows 0..top (H) and all rows (Z): multiply by Gᴴ = [c -s; conj(s) c]
314
+ for (const [M, rowEnd] of [
315
+ [H, top],
316
+ [Z, n - 1],
317
+ ] as Array<[CMat, number]>) {
318
+ for (let i = 0; i <= rowEnd; i++) {
319
+ const xr = M.re[i][k];
320
+ const xi = M.im[i][k];
321
+ const yr = M.re[i][k + 1];
322
+ const yi = M.im[i][k + 1];
323
+ M.re[i][k] = c * xr + (yr * sr + yi * si);
324
+ M.im[i][k] = c * xi + (yi * sr - yr * si);
325
+ M.re[i][k + 1] = -(xr * sr - xi * si) + c * yr;
326
+ M.im[i][k + 1] = -(xr * si + xi * sr) + c * yi;
327
+ }
328
+ }
329
+ }
330
+ for (let i = l; i <= hi; i++) {
331
+ H.re[i][i] += mur;
332
+ H.im[i][i] += mui;
333
+ }
334
+ }
335
+ // clear numerical dust below the diagonal
336
+ for (let i = 1; i < n; i++) {
337
+ for (let j = 0; j < i; j++) {
338
+ H.re[i][j] = 0;
339
+ H.im[i][j] = 0;
340
+ }
341
+ }
342
+ }
@@ -0,0 +1,203 @@
1
+ import { Graph } from "./Graph.ts";
2
+ import { Rational } from "./Rational.ts";
3
+
4
+ /**
5
+ * Two "Frobenius" normal forms.
6
+ *
7
+ * - {@link frobeniusNormalForm}: the *permutation* normal form of a square
8
+ * matrix (Frobenius / Perron-Frobenius block-triangular form). Treat the
9
+ * matrix as a weighted digraph (row `i` = node `i`'s out-edges, a non-zero
10
+ * entry = an edge); its strongly connected components, put in
11
+ * source-to-sink topological order, give a relabelling `P` for which
12
+ * `Pᵀ·A·P` is block upper triangular with an irreducible diagonal block per
13
+ * component. This is what mallory's matrix panel shows (`MAL-LIN-10`).
14
+ * - {@link rationalCanonicalForm}: the *similarity* normal form (Frobenius
15
+ * normal form in the algebraic sense): `A ~ diag(C(f₁), …, C(f_k))`,
16
+ * companion blocks of the invariant factors `f₁ | f₂ | … | f_k`, computed
17
+ * exactly over ℚ from the Smith normal form of `xI − A`.
18
+ */
19
+
20
+ export type NumericMatrix = ReadonlyArray<ReadonlyArray<number>>;
21
+
22
+ export interface FrobeniusBlock {
23
+ /** The component's vertices as ORIGINAL matrix indices. */
24
+ component: number[];
25
+ /** Row/column range in `permuted` (inclusive start, exclusive end). */
26
+ start: number;
27
+ end: number;
28
+ }
29
+
30
+ export interface FrobeniusResult {
31
+ /** `order[i]` = the original index now at permuted position `i`. */
32
+ order: number[];
33
+ /** Permutation matrix with `Pᵀ·matrix·P === permuted`. */
34
+ P: number[][];
35
+ /** `matrix` reindexed by `order` on both axes: block upper triangular. */
36
+ permuted: number[][];
37
+ /** One entry per strongly connected component, source to sink. */
38
+ blocks: FrobeniusBlock[];
39
+ /** True when the whole matrix is a single strongly connected component. */
40
+ irreducible: boolean;
41
+ }
42
+
43
+ /**
44
+ * The block-triangular (permutation) Frobenius normal form of a square matrix.
45
+ * Diagonal entries are self-loops and never affect the component structure.
46
+ * Within a component, original index order is the (deterministic) tiebreak.
47
+ */
48
+ export function frobeniusNormalForm(matrix: NumericMatrix): FrobeniusResult {
49
+ const n = matrix.length;
50
+ for (const row of matrix) {
51
+ if (row.length !== n)
52
+ throw new Error(
53
+ `frobeniusNormalForm requires a square matrix (got a ${n}-row matrix with a ${row.length}-entry row).`,
54
+ );
55
+ }
56
+ const graph = new Graph<number>(true);
57
+ for (let i = 0; i < n; i++) graph.addVertex(i);
58
+ for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) if (matrix[i][j] !== 0) graph.addEdge(i, j, matrix[i][j]);
59
+
60
+ const { graph: dag, components } = graph.condensation();
61
+ const topo = dag.topologicalSort();
62
+ if (!topo) throw new Error("frobeniusNormalForm: condensation unexpectedly has a cycle.");
63
+
64
+ const order: number[] = [];
65
+ const blocks: FrobeniusBlock[] = [];
66
+ for (const c of topo) {
67
+ const component = [...components[c]].sort((a, b) => a - b);
68
+ const start = order.length;
69
+ order.push(...component);
70
+ blocks.push({ component, start, end: order.length });
71
+ }
72
+ const permuted = order.map((oi) => order.map((oj) => matrix[oi][oj]));
73
+ const P = Array.from({ length: n }, () => new Array<number>(n).fill(0));
74
+ for (let i = 0; i < n; i++) P[order[i]][i] = 1;
75
+ return { order, P, permuted, blocks, irreducible: components.length === 1 };
76
+ }
77
+
78
+ // ---- rational canonical form ------------------------------------------------
79
+
80
+ /** A polynomial over ℚ, coefficients lowest degree first, no trailing zeros ([] is the zero polynomial). */
81
+ type Poly = Rational[];
82
+
83
+ const trim = (p: Poly): Poly => {
84
+ let n = p.length;
85
+ while (n > 0 && p[n - 1].isZero()) n--;
86
+ return p.slice(0, n);
87
+ };
88
+ const deg = (p: Poly) => p.length - 1;
89
+ const padd = (a: Poly, b: Poly): Poly =>
90
+ trim(
91
+ Array.from({ length: Math.max(a.length, b.length) }, (_, i) => (a[i] ?? Rational.Zero).add(b[i] ?? Rational.Zero)),
92
+ );
93
+ const pneg = (a: Poly): Poly => a.map((c) => c.negate());
94
+ const pmul = (a: Poly, b: Poly): Poly => {
95
+ if (!a.length || !b.length) return [];
96
+ const out: Rational[] = Array.from({ length: a.length + b.length - 1 }, () => Rational.Zero);
97
+ for (let i = 0; i < a.length; i++)
98
+ for (let j = 0; j < b.length; j++) out[i + j] = out[i + j].add(a[i].multiply(b[j]));
99
+ return trim(out);
100
+ };
101
+ /** Polynomial division with remainder (`b` non-zero). */
102
+ const pdivmod = (a: Poly, b: Poly): { q: Poly; r: Poly } => {
103
+ let r = [...a];
104
+ const q: Rational[] = Array.from({ length: Math.max(0, a.length - b.length + 1) }, () => Rational.Zero);
105
+ const lead = b[b.length - 1];
106
+ while (r.length >= b.length) {
107
+ const c = r[r.length - 1].divide(lead);
108
+ const d = r.length - b.length;
109
+ q[d] = c;
110
+ r = trim(r.map((x, i) => (i >= d ? x.subtract(c.multiply(b[i - d])) : x)));
111
+ }
112
+ return { q: trim(q), r };
113
+ };
114
+ const monic = (p: Poly): Poly => (p.length ? p.map((c) => c.divide(p[p.length - 1])) : p);
115
+
116
+ /** Smith normal form diagonal of a polynomial matrix, as monic polynomials (zero entries stay zero). */
117
+ function smithDiagonal(M: Poly[][]): Poly[] {
118
+ const n = M.length;
119
+ for (let t = 0; t < n; t++) {
120
+ for (;;) {
121
+ // pivot: non-zero entry of least degree in the trailing submatrix
122
+ let pi = -1;
123
+ let pj = -1;
124
+ for (let i = t; i < n; i++) {
125
+ for (let j = t; j < n; j++) {
126
+ if (M[i][j].length && (pi < 0 || deg(M[i][j]) < deg(M[pi][pj]))) {
127
+ pi = i;
128
+ pj = j;
129
+ }
130
+ }
131
+ }
132
+ if (pi < 0) return M.map((row, i) => (i < t ? monic(row[i]) : [])).concat([]);
133
+ [M[t], M[pi]] = [M[pi], M[t]];
134
+ for (const row of M) [row[t], row[pj]] = [row[pj], row[t]];
135
+ const p = M[t][t];
136
+ let dirty = false;
137
+ for (let i = t + 1; i < n; i++) {
138
+ if (!M[i][t].length) continue;
139
+ const { q } = pdivmod(M[i][t], p);
140
+ for (let j = t; j < n; j++) M[i][j] = padd(M[i][j], pneg(pmul(q, M[t][j])));
141
+ if (M[i][t].length) dirty = true;
142
+ }
143
+ for (let j = t + 1; j < n; j++) {
144
+ if (!M[t][j].length) continue;
145
+ const { q } = pdivmod(M[t][j], p);
146
+ for (let i = t; i < n; i++) M[i][j] = padd(M[i][j], pneg(pmul(q, M[i][t])));
147
+ if (M[t][j].length) dirty = true;
148
+ }
149
+ if (dirty) continue; // a smaller-degree remainder appeared: re-pivot
150
+ // pivot must divide everything left; if not, fold the offending row in
151
+ let bad = -1;
152
+ for (let i = t + 1; i < n && bad < 0; i++) {
153
+ for (let j = t + 1; j < n; j++) {
154
+ if (M[i][j].length && pdivmod(M[i][j], p).r.length) {
155
+ bad = i;
156
+ break;
157
+ }
158
+ }
159
+ }
160
+ if (bad < 0) break;
161
+ for (let j = t; j < n; j++) M[t][j] = padd(M[t][j], M[bad][j]);
162
+ }
163
+ }
164
+ return M.map((row, i) => monic(row[i]));
165
+ }
166
+
167
+ export interface RationalCanonicalResult {
168
+ /** Monic invariant factors `f₁ | f₂ | … | f_k` (non-constant only), each as coefficients lowest degree first. */
169
+ invariantFactors: Rational[][];
170
+ /** Block diagonal of the companion matrices of `invariantFactors` (n×n, exact). */
171
+ form: Rational[][];
172
+ /** The blocks' sizes (the degrees of the invariant factors). */
173
+ blockSizes: number[];
174
+ }
175
+
176
+ /**
177
+ * Rational canonical form (Frobenius normal form in the similarity sense) of a
178
+ * square matrix with integer or {@link Rational} entries, exact over ℚ.
179
+ * `invariantFactors` multiply to the characteristic polynomial and the last is
180
+ * the minimal polynomial. Each companion block `C(f)` for
181
+ * `f = xᵏ + c_{k-1}x^{k-1} + … + c₀` has ones on the subdiagonal and last
182
+ * column `(-c₀, …, -c_{k-1})ᵀ`. The similarity transform itself is not returned.
183
+ */
184
+ export function rationalCanonicalForm(
185
+ matrix: ReadonlyArray<ReadonlyArray<number | bigint | Rational>>,
186
+ ): RationalCanonicalResult {
187
+ const n = matrix.length;
188
+ for (const row of matrix) if (row.length !== n) throw new Error("rationalCanonicalForm requires a square matrix.");
189
+ const A = matrix.map((row) => row.map((v) => Rational.from(v)));
190
+ // xI - A as a polynomial matrix
191
+ const M: Poly[][] = A.map((row, i) => row.map((a, j) => trim(i === j ? [a.negate(), Rational.One] : [a.negate()])));
192
+ const diag = smithDiagonal(M);
193
+ const factors = diag.filter((p) => p.length > 1);
194
+ const form: Rational[][] = Array.from({ length: n }, () => Array.from({ length: n }, () => Rational.Zero));
195
+ let off = 0;
196
+ for (const f of factors) {
197
+ const k = deg(f);
198
+ for (let i = 1; i < k; i++) form[off + i][off + i - 1] = Rational.One;
199
+ for (let i = 0; i < k; i++) form[off + i][off + k - 1] = f[i].negate();
200
+ off += k;
201
+ }
202
+ return { invariantFactors: factors, form, blockSizes: factors.map(deg) };
203
+ }
package/src/Graph.ts CHANGED
@@ -260,6 +260,74 @@ export class Graph<T = string | number> {
260
260
  return mst;
261
261
  }
262
262
 
263
+ /**
264
+ * Minimum spanning tree (or forest, when the graph is disconnected) with
265
+ * its total weight and the number of connected components. Edge direction
266
+ * is ignored. `algorithm` picks Kruskal (default, sort + union-find) or
267
+ * Prim (heap-based, grown from each unvisited vertex in turn); both give
268
+ * the same total weight, though tie-broken edge choices may differ.
269
+ */
270
+ mst(algorithm: "kruskal" | "prim" = "kruskal"): { edges: Array<Edge<T>>; totalWeight: number; components: number } {
271
+ const n = this.vertices().length;
272
+ let edges: Array<Edge<T>>;
273
+ if (algorithm === "kruskal") edges = this.minimumSpanningTree();
274
+ else {
275
+ edges = [];
276
+ const nbrs = new Map<T, Array<Edge<T>>>();
277
+ for (const v of this.vertices()) nbrs.set(v, []);
278
+ for (const e of this.edges()) {
279
+ (nbrs.get(e.from) as Array<Edge<T>>).push(e);
280
+ (nbrs.get(e.to) as Array<Edge<T>>).push(e);
281
+ }
282
+ const seen = new Set<T>();
283
+ for (const root of this.vertices()) {
284
+ if (seen.has(root)) continue;
285
+ seen.add(root);
286
+ const heap = new MinHeap<Edge<T>>();
287
+ for (const e of nbrs.get(root) as Array<Edge<T>>) heap.push(e, e.weight);
288
+ for (let top = heap.pop(); top; top = heap.pop()) {
289
+ const e = top.value;
290
+ const next = seen.has(e.from) ? e.to : e.from;
291
+ if (seen.has(next)) continue;
292
+ seen.add(next);
293
+ edges.push(e);
294
+ for (const f of nbrs.get(next) as Array<Edge<T>>)
295
+ if (!seen.has(f.from) || !seen.has(f.to)) heap.push(f, f.weight);
296
+ }
297
+ }
298
+ }
299
+ return { edges, totalWeight: edges.reduce((s, e) => s + e.weight, 0), components: n - edges.length };
300
+ }
301
+
302
+ /** Alias of {@link stronglyConnectedComponents} (Tarjan; components in reverse topological order). */
303
+ scc(): T[][] {
304
+ return this.stronglyConnectedComponents();
305
+ }
306
+
307
+ /**
308
+ * The condensation: one vertex per strongly connected component (numbered
309
+ * by position in {@link scc}'s output), with one weight-1 edge `i -> j`
310
+ * wherever the original graph has any edge from a member of `i` to a
311
+ * member of `j`. Always a DAG. Also returns the `components` and the
312
+ * `componentOf` vertex-to-index map.
313
+ */
314
+ condensation(): { graph: Graph<number>; components: T[][]; componentOf: Map<T, number> } {
315
+ const components = this.scc();
316
+ const componentOf = new Map<T, number>();
317
+ components.forEach((c, i) => {
318
+ for (const v of c) componentOf.set(v, i);
319
+ });
320
+ const graph = new Graph<number>(true);
321
+ for (let i = 0; i < components.length; i++) graph.addVertex(i);
322
+ for (const e of this.edges()) {
323
+ const a = componentOf.get(e.from) as number;
324
+ const b = componentOf.get(e.to) as number;
325
+ if (a !== b) graph.addEdge(a, b, 1);
326
+ if (!this.directed && a !== b) graph.addEdge(b, a, 1);
327
+ }
328
+ return { graph, components, componentOf };
329
+ }
330
+
263
331
  /** Adjacency matrix (rows/cols follow the returned `order`; `Infinity` = no edge). */
264
332
  toAdjacencyMatrix(): { matrix: number[][]; order: T[] } {
265
333
  const order = this.vertices();
package/src/MatrixMath.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { ComplexNumber } from "./ComplexNumber.ts";
2
+ import { eigenGeneral, type GeneralEigenResult } from "./EigenGeneral.ts";
1
3
  import { Vector } from "./Vector.ts";
2
4
  import type { Matrix } from "./VectorUtils.ts";
3
5
 
@@ -259,6 +261,17 @@ export class MatrixMath {
259
261
  };
260
262
  }
261
263
 
264
+ /**
265
+ * Eigenvalues and right eigenvectors of any square matrix (real or complex
266
+ * entries), as `scipy.linalg.eig`: Hessenberg reduction plus shifted QR.
267
+ * Eigenvalues may be complex; `vectors[i][k]` is component `i` of the unit
268
+ * eigenvector for `values[k]`. For symmetric input prefer
269
+ * {@link MatrixMath.eigenSymmetric} (real, orthonormal, faster).
270
+ */
271
+ static eigen(A: MatrixInput | ComplexNumber[][]): GeneralEigenResult {
272
+ return eigenGeneral([...(A as Iterable<Iterable<number | ComplexNumber>>)].map((r) => [...r]));
273
+ }
274
+
262
275
  /**
263
276
  * Singular value decomposition `A = U·Σ·Vᵀ` (via the eigendecomposition of
264
277
  * `AᵀA`). Returns `U`, the singular values `S` (descending), and `V`.