@johnhenry/math 0.1.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.
- package/README.md +5 -4
- package/dist/Distributions.d.ts +86 -16
- package/dist/Distributions.d.ts.map +1 -1
- package/dist/Distributions.js +340 -89
- package/dist/Distributions.js.map +1 -1
- package/dist/EigenGeneral.d.ts +22 -0
- package/dist/EigenGeneral.d.ts.map +1 -0
- package/dist/EigenGeneral.js +327 -0
- package/dist/EigenGeneral.js.map +1 -0
- package/dist/Frobenius.d.ts +60 -0
- package/dist/Frobenius.d.ts.map +1 -0
- package/dist/Frobenius.js +162 -0
- package/dist/Frobenius.js.map +1 -0
- package/dist/Graph.d.ts +26 -0
- package/dist/Graph.d.ts.map +1 -1
- package/dist/Graph.js +75 -0
- package/dist/Graph.js.map +1 -1
- package/dist/MatrixMath.d.ts +10 -0
- package/dist/MatrixMath.d.ts.map +1 -1
- package/dist/MatrixMath.js +11 -0
- package/dist/MatrixMath.js.map +1 -1
- package/dist/MonteCarlo.d.ts +106 -0
- package/dist/MonteCarlo.d.ts.map +1 -0
- package/dist/MonteCarlo.js +183 -0
- package/dist/MonteCarlo.js.map +1 -0
- package/dist/Random.d.ts +31 -0
- package/dist/Random.d.ts.map +1 -0
- package/dist/Random.js +95 -0
- package/dist/Random.js.map +1 -0
- package/dist/Regression.d.ts +114 -0
- package/dist/Regression.d.ts.map +1 -0
- package/dist/Regression.js +338 -0
- package/dist/Regression.js.map +1 -0
- package/dist/Shader.d.ts +41 -0
- package/dist/Shader.d.ts.map +1 -0
- package/dist/Shader.js +758 -0
- package/dist/Shader.js.map +1 -0
- package/dist/SpecialFunctions.d.ts +3 -3
- package/dist/SpecialFunctions.d.ts.map +1 -1
- package/dist/SpecialFunctions.js +61 -51
- package/dist/SpecialFunctions.js.map +1 -1
- package/dist/Symbolic.d.ts +12 -0
- package/dist/Symbolic.d.ts.map +1 -1
- package/dist/Symbolic.js +16 -0
- package/dist/Symbolic.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Distributions.ts +391 -94
- package/src/EigenGeneral.ts +342 -0
- package/src/Frobenius.ts +203 -0
- package/src/Graph.ts +68 -0
- package/src/MatrixMath.ts +13 -0
- package/src/MonteCarlo.ts +257 -0
- package/src/Random.ts +104 -0
- package/src/Regression.ts +423 -0
- package/src/Shader.ts +837 -0
- package/src/SpecialFunctions.ts +52 -48
- package/src/Symbolic.ts +18 -0
- package/src/index.ts +34 -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
|
+
}
|
package/src/Frobenius.ts
ADDED
|
@@ -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`.
|