@tangent.to/lina 0.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/src/svd.js ADDED
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Singular value decomposition by one-sided Jacobi (Hestenes), plus the
3
+ * SVD-derived utilities pinv, rank and cond.
4
+ *
5
+ * One-sided Jacobi orthogonalizes pairs of columns of A directly; it is
6
+ * simple, unconditionally convergent, and computes even tiny singular
7
+ * values to high relative accuracy — a good fit for the suite's target
8
+ * sizes. For m < n the problem is transposed internally.
9
+ */
10
+
11
+ import { fromNested, toNested, vecFrom } from './_mat.js';
12
+
13
+ /**
14
+ * Thin SVD: A = U diag(s) V^T with U m×k, s length k, V n×k, k = min(m, n).
15
+ * Singular values are non-negative and descending.
16
+ *
17
+ * @param {Array<Array<number>>} A - Matrix (any shape)
18
+ * @param {Object} [options]
19
+ * @param {number} [options.maxSweeps=60] - Maximum Jacobi sweeps
20
+ * @param {number} [options.tol=1e-15] - Column-pair orthogonality tolerance
21
+ * @returns {{U: Array<Array<number>>, s: Array<number>, V: Array<Array<number>>}}
22
+ */
23
+ export function svd(A, options = {}) {
24
+ const M = fromNested(A);
25
+ const transposed = M.m < M.n;
26
+ const m = transposed ? M.n : M.m;
27
+ const n = transposed ? M.m : M.n;
28
+
29
+ // Column-major working copy W (m×n, m >= n) for cache-friendly column ops
30
+ const W = new Float64Array(m * n);
31
+ if (transposed) {
32
+ // W[:, j] = A^T[:, j] = A[j, :]
33
+ for (let j = 0; j < n; j++) {
34
+ for (let i = 0; i < m; i++) {
35
+ W[j * m + i] = M.data[j * M.n + i];
36
+ }
37
+ }
38
+ } else {
39
+ for (let j = 0; j < n; j++) {
40
+ for (let i = 0; i < m; i++) {
41
+ W[j * m + i] = M.data[i * M.n + j];
42
+ }
43
+ }
44
+ }
45
+
46
+ // V accumulates the right rotations (n×n, column-major)
47
+ const V = new Float64Array(n * n);
48
+ for (let i = 0; i < n; i++) V[i * n + i] = 1;
49
+
50
+ const maxSweeps = options.maxSweeps || 60;
51
+ const tol = options.tol || 1e-15;
52
+
53
+ for (let sweep = 0; sweep < maxSweeps; sweep++) {
54
+ let rotated = false;
55
+
56
+ for (let p = 0; p < n - 1; p++) {
57
+ for (let q = p + 1; q < n; q++) {
58
+ let alpha = 0;
59
+ let beta = 0;
60
+ let gam = 0;
61
+ const cp = p * m;
62
+ const cq = q * m;
63
+ for (let i = 0; i < m; i++) {
64
+ const wp = W[cp + i];
65
+ const wq = W[cq + i];
66
+ alpha += wp * wp;
67
+ beta += wq * wq;
68
+ gam += wp * wq;
69
+ }
70
+
71
+ if (Math.abs(gam) <= tol * Math.sqrt(alpha * beta) || alpha === 0 || beta === 0) {
72
+ continue;
73
+ }
74
+ rotated = true;
75
+
76
+ // Jacobi rotation that zeroes the (p, q) inner product.
77
+ // sign(0) must be +1: zeta = 0 (equal column norms) needs a 45°
78
+ // rotation, not a no-op.
79
+ const zeta = (beta - alpha) / (2 * gam);
80
+ const sgn = zeta >= 0 ? 1 : -1;
81
+ const t = sgn / (Math.abs(zeta) + Math.sqrt(1 + zeta * zeta));
82
+ const c = 1 / Math.sqrt(1 + t * t);
83
+ const s = c * t;
84
+
85
+ for (let i = 0; i < m; i++) {
86
+ const wp = W[cp + i];
87
+ const wq = W[cq + i];
88
+ W[cp + i] = c * wp - s * wq;
89
+ W[cq + i] = s * wp + c * wq;
90
+ }
91
+ for (let i = 0; i < n; i++) {
92
+ const vp = V[p * n + i];
93
+ const vq = V[q * n + i];
94
+ V[p * n + i] = c * vp - s * vq;
95
+ V[q * n + i] = s * vp + c * vq;
96
+ }
97
+ }
98
+ }
99
+
100
+ if (!rotated) break;
101
+ }
102
+
103
+ // Singular values = column norms; U columns = normalized W columns
104
+ const sv = new Array(n);
105
+ for (let j = 0; j < n; j++) {
106
+ let norm = 0;
107
+ for (let i = 0; i < m; i++) norm += W[j * m + i] * W[j * m + i];
108
+ sv[j] = Math.sqrt(norm);
109
+ }
110
+
111
+ // Sort descending
112
+ const order = Array.from({ length: n }, (_, i) => i).sort((i, j) => sv[j] - sv[i]);
113
+ const s = order.map((j) => sv[j]);
114
+
115
+ const Umat = new Float64Array(m * n); // row-major m×n
116
+ const Vmat = new Float64Array(n * n); // row-major n×n
117
+ for (let col = 0; col < n; col++) {
118
+ const src = order[col];
119
+ const norm = sv[src];
120
+ if (norm > 0) {
121
+ for (let i = 0; i < m; i++) Umat[i * n + col] = W[src * m + i] / norm;
122
+ } // zero singular value: leave U column zero (rank-deficient case)
123
+ for (let i = 0; i < n; i++) Vmat[i * n + col] = V[src * n + i];
124
+ }
125
+
126
+ const Uout = toNested(Umat, m, n);
127
+ const Vout = toNested(Vmat, n, n);
128
+
129
+ // Undo the internal transpose: A^T = U s V^T => A = V s U^T
130
+ return transposed ? { U: Vout, s, V: Uout } : { U: Uout, s, V: Vout };
131
+ }
132
+
133
+ /**
134
+ * Numerical rank via SVD.
135
+ *
136
+ * @param {Array<Array<number>>} A - Matrix
137
+ * @param {number} [tol] - Threshold; default max(m,n) * eps * s[0] (numpy convention)
138
+ * @returns {number}
139
+ */
140
+ export function rank(A, tol) {
141
+ const { s } = svd(A);
142
+ const m = A.length;
143
+ const n = A[0].length;
144
+ const threshold = tol !== undefined ? tol : Math.max(m, n) * Number.EPSILON * (s[0] || 0);
145
+ return s.filter((x) => x > threshold).length;
146
+ }
147
+
148
+ /**
149
+ * Condition number (2-norm): s_max / s_min. Infinity when singular.
150
+ *
151
+ * @param {Array<Array<number>>} A - Matrix
152
+ * @returns {number}
153
+ */
154
+ export function cond(A) {
155
+ const { s } = svd(A);
156
+ const smin = s[s.length - 1];
157
+ return smin > 0 ? s[0] / smin : Infinity;
158
+ }
159
+
160
+ /**
161
+ * Moore-Penrose pseudoinverse via SVD, with numpy's default cutoff.
162
+ * Solves rank-deficient least squares: x = pinv(A) b is the minimum-norm
163
+ * solution.
164
+ *
165
+ * @param {Array<Array<number>>} A - Matrix (any shape)
166
+ * @param {number} [rcond] - Relative cutoff; default max(m,n) * eps
167
+ * @returns {Array<Array<number>>} n×m pseudoinverse
168
+ */
169
+ export function pinv(A, rcond) {
170
+ const { U, s, V } = svd(A);
171
+ const m = U.length;
172
+ const n = V.length;
173
+ const k = s.length;
174
+ const cutoff = (rcond !== undefined ? rcond : Math.max(m, n) * Number.EPSILON) * (s[0] || 0);
175
+
176
+ // pinv = V diag(1/s) U^T, dropping singular values below the cutoff
177
+ const out = new Array(n);
178
+ for (let i = 0; i < n; i++) {
179
+ out[i] = new Array(m).fill(0);
180
+ }
181
+ for (let j = 0; j < k; j++) {
182
+ if (s[j] <= cutoff) continue;
183
+ const invS = 1 / s[j];
184
+ for (let i = 0; i < n; i++) {
185
+ const vij = V[i][j] * invS;
186
+ if (vij === 0) continue;
187
+ const row = out[i];
188
+ for (let l = 0; l < m; l++) {
189
+ row[l] += vij * U[l][j];
190
+ }
191
+ }
192
+ }
193
+ return out;
194
+ }
195
+
196
+ /**
197
+ * Minimum-norm least squares via the pseudoinverse (works for any rank).
198
+ *
199
+ * @param {Array<Array<number>>} A - m×n matrix
200
+ * @param {Array<number>} b - Vector of length m
201
+ * @returns {Array<number>} x of length n
202
+ */
203
+ export function pinvSolve(A, b) {
204
+ const m = A.length;
205
+ vecFrom(b, m, 'b');
206
+ const P = pinv(A);
207
+ const n = P.length;
208
+ const x = new Array(n).fill(0);
209
+ for (let i = 0; i < n; i++) {
210
+ let sum = 0;
211
+ for (let j = 0; j < m; j++) sum += P[i][j] * b[j];
212
+ x[i] = sum;
213
+ }
214
+ return x;
215
+ }