@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/index.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @tangent.to/lina - Linear algebra for JavaScript (ESM)
3
+ *
4
+ * LU, QR, Cholesky, SVD, symmetric eigendecomposition and least squares
5
+ * on plain nested arrays, computed on flat Float64Array storage
6
+ * internally. numpy/scipy.linalg-validated. MIT infrastructure of the
7
+ * tangent suite.
8
+ */
9
+
10
+ export { det, inv, lu, solve } from './lu.js';
11
+ export { cholesky, choleskySolve, isPositiveDefinite } from './cholesky.js';
12
+ export { lstsq, qr } from './qr.js';
13
+ export { cond, pinv, pinvSolve, rank, svd } from './svd.js';
14
+ export { eigSym } from './eigsym.js';
15
+ export { diag, identity, matmul, norm, trace, transpose } from './ops.js';
16
+
17
+ import { det, inv, lu, solve } from './lu.js';
18
+ import { cholesky, choleskySolve, isPositiveDefinite } from './cholesky.js';
19
+ import { lstsq, qr } from './qr.js';
20
+ import { cond, pinv, pinvSolve, rank, svd } from './svd.js';
21
+ import { eigSym } from './eigsym.js';
22
+ import { diag, identity, matmul, norm, trace, transpose } from './ops.js';
23
+
24
+ export default {
25
+ lu,
26
+ solve,
27
+ det,
28
+ inv,
29
+ cholesky,
30
+ choleskySolve,
31
+ isPositiveDefinite,
32
+ qr,
33
+ lstsq,
34
+ svd,
35
+ pinv,
36
+ pinvSolve,
37
+ rank,
38
+ cond,
39
+ eigSym,
40
+ matmul,
41
+ transpose,
42
+ identity,
43
+ diag,
44
+ norm,
45
+ trace,
46
+ };
package/src/lu.js ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * LU factorization with scaled partial pivoting, and LU-based solve/det/inv.
3
+ *
4
+ * Public APIs accept and return nested row-major matrices; computation runs
5
+ * on flat Float64Array storage (see _mat.js).
6
+ */
7
+
8
+ import { fromNested, toNested, vecFrom } from './_mat.js';
9
+
10
+ /**
11
+ * Factor a flat square matrix in place-of-copy with scaled partial pivoting.
12
+ *
13
+ * Returns the combined LU storage (unit-diagonal L strictly below the
14
+ * diagonal, U on and above), the row permutation `perm` (row k of the
15
+ * factored matrix is row perm[k] of the input), the permutation sign, and a
16
+ * singularity flag (any pivot with |pivot| <= 1e-13 * maxAbs of the input).
17
+ *
18
+ * @param {Float64Array} src - Flat row-major n*n values (not modified)
19
+ * @param {number} n - Dimension
20
+ * @returns {{a: Float64Array, perm: Int32Array, sign: number, singular: boolean}}
21
+ */
22
+ function luFactor(src, n) {
23
+ const a = Float64Array.from(src);
24
+ const perm = new Int32Array(n);
25
+ const scale = new Float64Array(n);
26
+ let maxAbs = 0;
27
+ for (let i = 0; i < a.length; i++) maxAbs = Math.max(maxAbs, Math.abs(a[i]));
28
+ const tol = 1e-13 * maxAbs;
29
+ let sign = 1;
30
+ let singular = maxAbs === 0;
31
+
32
+ for (let i = 0; i < n; i++) {
33
+ perm[i] = i;
34
+ let s = 0;
35
+ for (let j = 0; j < n; j++) s = Math.max(s, Math.abs(a[i * n + j]));
36
+ scale[i] = s;
37
+ }
38
+
39
+ for (let k = 0; k < n; k++) {
40
+ // Pick the row maximizing |a[i][k]| / scale[i] among rows k..n-1.
41
+ let p = k;
42
+ let best = -1;
43
+ for (let i = k; i < n; i++) {
44
+ const v = scale[i] > 0 ? Math.abs(a[i * n + k]) / scale[i] : 0;
45
+ if (v > best) {
46
+ best = v;
47
+ p = i;
48
+ }
49
+ }
50
+ if (p !== k) {
51
+ for (let j = 0; j < n; j++) {
52
+ const t = a[k * n + j];
53
+ a[k * n + j] = a[p * n + j];
54
+ a[p * n + j] = t;
55
+ }
56
+ const ts = scale[k];
57
+ scale[k] = scale[p];
58
+ scale[p] = ts;
59
+ const tp = perm[k];
60
+ perm[k] = perm[p];
61
+ perm[p] = tp;
62
+ sign = -sign;
63
+ }
64
+ const pivot = a[k * n + k];
65
+ if (Math.abs(pivot) <= tol) {
66
+ singular = true;
67
+ continue;
68
+ }
69
+ for (let i = k + 1; i < n; i++) {
70
+ const f = a[i * n + k] / pivot;
71
+ a[i * n + k] = f;
72
+ for (let j = k + 1; j < n; j++) {
73
+ a[i * n + j] -= f * a[k * n + j];
74
+ }
75
+ }
76
+ }
77
+
78
+ return { a, perm, sign, singular };
79
+ }
80
+
81
+ /**
82
+ * Validate a square nested matrix and return its flat form.
83
+ *
84
+ * @param {Array<Array<number>>} A - Nested matrix
85
+ * @param {string} caller - Function name for error messages
86
+ * @returns {{data: Float64Array, m: number, n: number}}
87
+ */
88
+ function squareFromNested(A, caller) {
89
+ const M = fromNested(A, 'A');
90
+ if (M.m !== M.n) {
91
+ throw new Error(`${caller}: matrix must be square (got ${M.m}x${M.n})`);
92
+ }
93
+ return M;
94
+ }
95
+
96
+ /**
97
+ * LU factorization with partial pivoting: P A = L U.
98
+ *
99
+ * @param {Array<Array<number>>} A - Square nested matrix
100
+ * @returns {{L: Array<Array<number>>, U: Array<Array<number>>, P: Array<Array<number>>}}
101
+ * L unit lower triangular, U upper triangular, P a permutation matrix.
102
+ */
103
+ export function lu(A) {
104
+ const { data, n } = squareFromNested(A, 'lu');
105
+ const { a, perm } = luFactor(data, n);
106
+ const L = new Float64Array(n * n);
107
+ const U = new Float64Array(n * n);
108
+ const P = new Float64Array(n * n);
109
+ for (let i = 0; i < n; i++) {
110
+ L[i * n + i] = 1;
111
+ P[i * n + perm[i]] = 1;
112
+ for (let j = 0; j < i; j++) L[i * n + j] = a[i * n + j];
113
+ for (let j = i; j < n; j++) U[i * n + j] = a[i * n + j];
114
+ }
115
+ return { L: toNested(L, n, n), U: toNested(U, n, n), P: toNested(P, n, n) };
116
+ }
117
+
118
+ /**
119
+ * Solve L U x = P b for one right-hand side, in place in `x`.
120
+ *
121
+ * @param {Float64Array} a - Combined LU storage
122
+ * @param {Int32Array} perm - Row permutation
123
+ * @param {number} n - Dimension
124
+ * @param {Float64Array} x - Permuted right-hand side; overwritten with the solution
125
+ */
126
+ function luSolveInPlace(a, perm, n, x) {
127
+ // Forward substitution with unit lower triangular L.
128
+ for (let i = 1; i < n; i++) {
129
+ let s = x[i];
130
+ for (let j = 0; j < i; j++) s -= a[i * n + j] * x[j];
131
+ x[i] = s;
132
+ }
133
+ // Back substitution with U.
134
+ for (let i = n - 1; i >= 0; i--) {
135
+ let s = x[i];
136
+ for (let j = i + 1; j < n; j++) s -= a[i * n + j] * x[j];
137
+ x[i] = s / a[i * n + i];
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Solve A x = b via LU factorization with partial pivoting.
143
+ *
144
+ * @param {Array<Array<number>>} A - Square nested matrix
145
+ * @param {Array<number>|Array<Array<number>>} b - Right-hand side vector (length n)
146
+ * or nested matrix of right-hand sides (n x k, solved column by column)
147
+ * @returns {Array<number>|Array<Array<number>>} Solution vector, or nested n x k matrix
148
+ */
149
+ export function solve(A, b) {
150
+ const { data, n } = squareFromNested(A, 'solve');
151
+ const { a, perm, singular } = luFactor(data, n);
152
+ if (singular) {
153
+ throw new Error('solve: matrix is singular (pivot below tolerance)');
154
+ }
155
+
156
+ const matrixRhs = Array.isArray(b) && Array.isArray(b[0]);
157
+ if (!matrixRhs) {
158
+ const bv = vecFrom(b, n, 'b');
159
+ const x = new Float64Array(n);
160
+ for (let i = 0; i < n; i++) x[i] = bv[perm[i]];
161
+ luSolveInPlace(a, perm, n, x);
162
+ return Array.from(x);
163
+ }
164
+
165
+ const B = fromNested(b, 'b');
166
+ if (B.m !== n) {
167
+ throw new Error(`solve: b must have ${n} rows (got ${B.m})`);
168
+ }
169
+ const k = B.n;
170
+ const X = new Float64Array(n * k);
171
+ const x = new Float64Array(n);
172
+ for (let c = 0; c < k; c++) {
173
+ for (let i = 0; i < n; i++) x[i] = B.data[perm[i] * k + c];
174
+ luSolveInPlace(a, perm, n, x);
175
+ for (let i = 0; i < n; i++) X[i * k + c] = x[i];
176
+ }
177
+ return toNested(X, n, k);
178
+ }
179
+
180
+ /**
181
+ * Determinant via LU factorization (permutation sign times product of pivots).
182
+ * Returns 0 for singular matrices instead of throwing.
183
+ *
184
+ * @param {Array<Array<number>>} A - Square nested matrix
185
+ * @returns {number}
186
+ */
187
+ export function det(A) {
188
+ const { data, n } = squareFromNested(A, 'det');
189
+ const { a, sign, singular } = luFactor(data, n);
190
+ if (singular) return 0;
191
+ let d = sign;
192
+ for (let i = 0; i < n; i++) d *= a[i * n + i];
193
+ return d;
194
+ }
195
+
196
+ /**
197
+ * Matrix inverse via solve(A, I).
198
+ *
199
+ * @param {Array<Array<number>>} A - Square nested matrix
200
+ * @returns {Array<Array<number>>}
201
+ */
202
+ export function inv(A) {
203
+ const M = squareFromNested(A, 'inv');
204
+ const n = M.n;
205
+ const I = new Array(n);
206
+ for (let i = 0; i < n; i++) {
207
+ const row = new Array(n).fill(0);
208
+ row[i] = 1;
209
+ I[i] = row;
210
+ }
211
+ return solve(A, I);
212
+ }
package/src/ops.js ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Basic dense matrix operations: multiply, transpose, identity, diagonal,
3
+ * norms, and trace.
4
+ *
5
+ * Public APIs accept and return nested row-major matrices; computation runs
6
+ * on flat Float64Array storage (see _mat.js).
7
+ */
8
+
9
+ import { fromNested, toNested, vecFrom } from './_mat.js';
10
+
11
+ /**
12
+ * Matrix product A B, or matrix-vector product A b.
13
+ *
14
+ * @param {Array<Array<number>>} A - m x n nested matrix
15
+ * @param {Array<Array<number>>|Array<number>} B - n x p nested matrix, or
16
+ * vector of length n
17
+ * @returns {Array<Array<number>>|Array<number>} m x p nested matrix, or
18
+ * vector of length m
19
+ */
20
+ export function matmul(A, B) {
21
+ const Ma = fromNested(A, 'A');
22
+ const { data: a, m, n } = Ma;
23
+
24
+ if (Array.isArray(B) && !Array.isArray(B[0])) {
25
+ const x = vecFrom(B, n, 'B');
26
+ const y = new Float64Array(m);
27
+ for (let i = 0; i < m; i++) {
28
+ let s = 0;
29
+ for (let j = 0; j < n; j++) s += a[i * n + j] * x[j];
30
+ y[i] = s;
31
+ }
32
+ return Array.from(y);
33
+ }
34
+
35
+ const Mb = fromNested(B, 'B');
36
+ if (Mb.m !== n) {
37
+ throw new Error(`matmul: dimension mismatch (A is ${m}x${n}, B is ${Mb.m}x${Mb.n})`);
38
+ }
39
+ const b = Mb.data;
40
+ const p = Mb.n;
41
+ const c = new Float64Array(m * p);
42
+ for (let i = 0; i < m; i++) {
43
+ for (let k = 0; k < n; k++) {
44
+ const aik = a[i * n + k];
45
+ if (aik === 0) continue;
46
+ for (let j = 0; j < p; j++) {
47
+ c[i * p + j] += aik * b[k * p + j];
48
+ }
49
+ }
50
+ }
51
+ return toNested(c, m, p);
52
+ }
53
+
54
+ /**
55
+ * Matrix transpose.
56
+ *
57
+ * @param {Array<Array<number>>} A - m x n nested matrix
58
+ * @returns {Array<Array<number>>} n x m nested matrix
59
+ */
60
+ export function transpose(A) {
61
+ const { data, m, n } = fromNested(A, 'A');
62
+ const t = new Float64Array(n * m);
63
+ for (let i = 0; i < m; i++) {
64
+ for (let j = 0; j < n; j++) {
65
+ t[j * m + i] = data[i * n + j];
66
+ }
67
+ }
68
+ return toNested(t, n, m);
69
+ }
70
+
71
+ /**
72
+ * Identity matrix of size n.
73
+ *
74
+ * @param {number} n - Dimension (positive integer)
75
+ * @returns {Array<Array<number>>}
76
+ */
77
+ export function identity(n) {
78
+ if (!Number.isInteger(n) || n <= 0) {
79
+ throw new Error(`identity: n must be a positive integer (got ${n})`);
80
+ }
81
+ const I = new Array(n);
82
+ for (let i = 0; i < n; i++) {
83
+ const row = new Array(n).fill(0);
84
+ row[i] = 1;
85
+ I[i] = row;
86
+ }
87
+ return I;
88
+ }
89
+
90
+ /**
91
+ * Build a diagonal matrix from a vector, or extract the diagonal of a matrix.
92
+ *
93
+ * @param {Array<number>|Array<Array<number>>} x - Vector (returns an n x n
94
+ * nested matrix) or nested matrix (returns its diagonal, length min(m, n))
95
+ * @returns {Array<Array<number>>|Array<number>}
96
+ */
97
+ export function diag(x) {
98
+ if (!Array.isArray(x) || x.length === 0) {
99
+ throw new Error('diag: argument must be a non-empty vector or nested matrix');
100
+ }
101
+ if (Array.isArray(x[0])) {
102
+ const { data, m, n } = fromNested(x, 'A');
103
+ const len = Math.min(m, n);
104
+ const d = new Array(len);
105
+ for (let i = 0; i < len; i++) d[i] = data[i * n + i];
106
+ return d;
107
+ }
108
+ const n = x.length;
109
+ const D = new Array(n);
110
+ for (let i = 0; i < n; i++) {
111
+ const row = new Array(n).fill(0);
112
+ row[i] = x[i];
113
+ D[i] = row;
114
+ }
115
+ return D;
116
+ }
117
+
118
+ /**
119
+ * Matrix or vector norm.
120
+ *
121
+ * For a nested matrix: 'fro' (Frobenius), 1 (max column abs sum), or
122
+ * Infinity (max row abs sum). For a vector: 'fro' or 2 (euclidean),
123
+ * 1 (abs sum), or Infinity (max abs).
124
+ *
125
+ * @param {Array<Array<number>>|Array<number>} A - Nested matrix or vector
126
+ * @param {'fro'|1|2|Infinity} [kind='fro'] - Norm kind
127
+ * @returns {number}
128
+ */
129
+ export function norm(A, kind = 'fro') {
130
+ if (!Array.isArray(A) || A.length === 0) {
131
+ throw new Error('norm: argument must be a non-empty vector or nested matrix');
132
+ }
133
+
134
+ if (!Array.isArray(A[0])) {
135
+ if (kind === 'fro' || kind === 2) {
136
+ let s = 0;
137
+ for (let i = 0; i < A.length; i++) s += A[i] * A[i];
138
+ return Math.sqrt(s);
139
+ }
140
+ if (kind === 1) {
141
+ let s = 0;
142
+ for (let i = 0; i < A.length; i++) s += Math.abs(A[i]);
143
+ return s;
144
+ }
145
+ if (kind === Infinity) {
146
+ let s = 0;
147
+ for (let i = 0; i < A.length; i++) s = Math.max(s, Math.abs(A[i]));
148
+ return s;
149
+ }
150
+ throw new Error(`norm: unsupported vector norm kind ${String(kind)}`);
151
+ }
152
+
153
+ const { data, m, n } = fromNested(A, 'A');
154
+ if (kind === 'fro') {
155
+ let s = 0;
156
+ for (let i = 0; i < data.length; i++) s += data[i] * data[i];
157
+ return Math.sqrt(s);
158
+ }
159
+ if (kind === 1) {
160
+ let best = 0;
161
+ for (let j = 0; j < n; j++) {
162
+ let s = 0;
163
+ for (let i = 0; i < m; i++) s += Math.abs(data[i * n + j]);
164
+ best = Math.max(best, s);
165
+ }
166
+ return best;
167
+ }
168
+ if (kind === Infinity) {
169
+ let best = 0;
170
+ for (let i = 0; i < m; i++) {
171
+ let s = 0;
172
+ for (let j = 0; j < n; j++) s += Math.abs(data[i * n + j]);
173
+ best = Math.max(best, s);
174
+ }
175
+ return best;
176
+ }
177
+ throw new Error(`norm: unsupported matrix norm kind ${String(kind)} (use 'fro', 1, or Infinity)`);
178
+ }
179
+
180
+ /**
181
+ * Sum of the diagonal of a square matrix.
182
+ *
183
+ * @param {Array<Array<number>>} A - Square nested matrix
184
+ * @returns {number}
185
+ */
186
+ export function trace(A) {
187
+ const { data, m, n } = fromNested(A, 'A');
188
+ if (m !== n) {
189
+ throw new Error(`trace: matrix must be square (got ${m}x${n})`);
190
+ }
191
+ let s = 0;
192
+ for (let i = 0; i < n; i++) s += data[i * n + i];
193
+ return s;
194
+ }
package/src/qr.js ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * QR decomposition via Householder reflections, and least squares on top of it.
3
+ *
4
+ * Public APIs take and return nested row-major matrices (Array<Array<number>>)
5
+ * and plain vectors (Array<number>); computation runs on flat Float64Array
6
+ * storage via the helpers in _mat.js.
7
+ */
8
+
9
+ import { fromNested, toNested, vecFrom } from './_mat.js';
10
+
11
+ /**
12
+ * Factor a flat matrix in place with Householder reflectors.
13
+ *
14
+ * After the call, `data` holds R in its upper triangle (subdiagonal entries of
15
+ * the processed columns are set to exact zeros). The reflectors H_j =
16
+ * I - beta_j v_j v_j^T (with v_j supported on rows j..m-1) satisfy
17
+ * H_{k-1} ... H_0 A = R, i.e. A = H_0 ... H_{k-1} R.
18
+ *
19
+ * Uses the standard stable sign choice v = x + sign(x0) ||x|| e1.
20
+ *
21
+ * @param {Float64Array} data - Row-major m*n storage, overwritten with R
22
+ * @param {number} m - Rows
23
+ * @param {number} n - Columns
24
+ * @returns {{vs: Array<Float64Array>, betas: Float64Array, k: number}}
25
+ */
26
+ function householderFactor(data, m, n) {
27
+ const k = Math.min(m, n);
28
+ const vs = new Array(k);
29
+ const betas = new Float64Array(k);
30
+ for (let j = 0; j < k; j++) {
31
+ let norm = 0;
32
+ for (let i = j; i < m; i++) {
33
+ const t = data[i * n + j];
34
+ norm += t * t;
35
+ }
36
+ norm = Math.sqrt(norm);
37
+ const v = new Float64Array(m - j);
38
+ vs[j] = v;
39
+ if (norm === 0) {
40
+ betas[j] = 0;
41
+ continue;
42
+ }
43
+ const x0 = data[j * n + j];
44
+ const sign = x0 >= 0 ? 1 : -1;
45
+ v[0] = x0 + sign * norm;
46
+ for (let i = j + 1; i < m; i++) v[i - j] = data[i * n + j];
47
+ let vv = 0;
48
+ for (let t = 0; t < v.length; t++) vv += v[t] * v[t];
49
+ const beta = 2 / vv;
50
+ betas[j] = beta;
51
+ // Apply H_j to the trailing columns.
52
+ for (let c = j + 1; c < n; c++) {
53
+ let s = 0;
54
+ for (let i = j; i < m; i++) s += v[i - j] * data[i * n + c];
55
+ s *= beta;
56
+ for (let i = j; i < m; i++) data[i * n + c] -= s * v[i - j];
57
+ }
58
+ // Column j maps exactly to -sign * norm * e1.
59
+ data[j * n + j] = -sign * norm;
60
+ for (let i = j + 1; i < m; i++) data[i * n + j] = 0;
61
+ }
62
+ return { vs, betas, k };
63
+ }
64
+
65
+ /**
66
+ * Accumulate Q = H_0 H_1 ... H_{k-1} applied to the first qCols columns of I.
67
+ *
68
+ * @param {Array<Float64Array>} vs - Reflector vectors
69
+ * @param {Float64Array} betas - Reflector scalings
70
+ * @param {number} m - Rows of Q
71
+ * @param {number} qCols - Columns of Q (min(m, n) for reduced, m for full)
72
+ * @returns {Float64Array} Row-major m*qCols storage of Q
73
+ */
74
+ function accumulateQ(vs, betas, m, qCols) {
75
+ const Q = new Float64Array(m * qCols);
76
+ for (let i = 0; i < qCols; i++) Q[i * qCols + i] = 1;
77
+ for (let j = vs.length - 1; j >= 0; j--) {
78
+ const v = vs[j];
79
+ const beta = betas[j];
80
+ if (beta === 0) continue;
81
+ for (let c = 0; c < qCols; c++) {
82
+ let s = 0;
83
+ for (let i = j; i < m; i++) s += v[i - j] * Q[i * qCols + c];
84
+ s *= beta;
85
+ for (let i = j; i < m; i++) Q[i * qCols + c] -= s * v[i - j];
86
+ }
87
+ }
88
+ return Q;
89
+ }
90
+
91
+ /**
92
+ * QR decomposition A = Q R via Householder reflections.
93
+ *
94
+ * Works for any shape: m >= n and m < n alike.
95
+ *
96
+ * @param {Array<Array<number>>} A - m x n matrix
97
+ * @param {Object} [options]
98
+ * @param {string} [options.mode='reduced'] - 'reduced' (Q is m x min(m,n),
99
+ * R is min(m,n) x n) or 'full' (Q is m x m, R is m x n)
100
+ * @returns {{Q: Array<Array<number>>, R: Array<Array<number>>}}
101
+ */
102
+ export function qr(A, options = {}) {
103
+ const { data, m, n } = fromNested(A, 'A');
104
+ const mode = options.mode !== undefined ? options.mode : 'reduced';
105
+ if (mode !== 'reduced' && mode !== 'full') {
106
+ throw new Error(`qr: options.mode must be 'reduced' or 'full' (got '${mode}')`);
107
+ }
108
+ const { vs, betas, k } = householderFactor(data, m, n);
109
+ const qCols = mode === 'full' ? m : k;
110
+ const Qflat = accumulateQ(vs, betas, m, qCols);
111
+ const rRows = mode === 'full' ? m : k;
112
+ return {
113
+ Q: toNested(Qflat, m, qCols),
114
+ R: toNested(data.subarray(0, rRows * n), rRows, n),
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Least-squares solution of A x ≈ b via reduced QR.
120
+ *
121
+ * Requires m >= n and full column rank: solves R x = Q^T b by back
122
+ * substitution. Throws for rank-deficient R.
123
+ *
124
+ * @param {Array<Array<number>>} A - m x n matrix with m >= n
125
+ * @param {Array<number>} b - Right-hand side of length m
126
+ * @returns {{x: Array<number>, residualNorm: number}} residualNorm = ||A x - b||_2
127
+ */
128
+ export function lstsq(A, b) {
129
+ const { data, m, n } = fromNested(A, 'A');
130
+ if (m < n) {
131
+ throw new Error(`lstsq: A must have at least as many rows as columns (got ${m}x${n})`);
132
+ }
133
+ const bv = vecFrom(b, m, 'b');
134
+ const original = data.slice();
135
+ const { vs, betas, k } = householderFactor(data, m, n);
136
+
137
+ // Rank check on the diagonal of R against its largest entry.
138
+ let maxR = 0;
139
+ for (let i = 0; i < k; i++) {
140
+ for (let j = i; j < n; j++) maxR = Math.max(maxR, Math.abs(data[i * n + j]));
141
+ }
142
+ for (let i = 0; i < n; i++) {
143
+ if (Math.abs(data[i * n + i]) < 1e-12 * maxR || maxR === 0) {
144
+ throw new Error(
145
+ `lstsq: A is rank deficient (R[${i}][${i}] is negligible); ` +
146
+ 'use pinv for a minimum-norm solution'
147
+ );
148
+ }
149
+ }
150
+
151
+ // Compute Q^T b = H_{k-1} ... H_0 b in place.
152
+ for (let j = 0; j < k; j++) {
153
+ const v = vs[j];
154
+ const beta = betas[j];
155
+ if (beta === 0) continue;
156
+ let s = 0;
157
+ for (let i = j; i < m; i++) s += v[i - j] * bv[i];
158
+ s *= beta;
159
+ for (let i = j; i < m; i++) bv[i] -= s * v[i - j];
160
+ }
161
+
162
+ // Back substitution: R x = (Q^T b)[0..n-1].
163
+ const x = new Array(n).fill(0);
164
+ for (let i = n - 1; i >= 0; i--) {
165
+ let s = bv[i];
166
+ for (let j = i + 1; j < n; j++) s -= data[i * n + j] * x[j];
167
+ x[i] = s / data[i * n + i];
168
+ }
169
+
170
+ // Residual norm from the original A.
171
+ let rr = 0;
172
+ for (let i = 0; i < m; i++) {
173
+ let s = -b[i];
174
+ for (let j = 0; j < n; j++) s += original[i * n + j] * x[j];
175
+ rr += s * s;
176
+ }
177
+ return { x, residualNorm: Math.sqrt(rr) };
178
+ }