@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.
Files changed (62) hide show
  1. package/README.md +5 -4
  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/Shader.d.ts +41 -0
  35. package/dist/Shader.d.ts.map +1 -0
  36. package/dist/Shader.js +758 -0
  37. package/dist/Shader.js.map +1 -0
  38. package/dist/SpecialFunctions.d.ts +3 -3
  39. package/dist/SpecialFunctions.d.ts.map +1 -1
  40. package/dist/SpecialFunctions.js +61 -51
  41. package/dist/SpecialFunctions.js.map +1 -1
  42. package/dist/Symbolic.d.ts +12 -0
  43. package/dist/Symbolic.d.ts.map +1 -1
  44. package/dist/Symbolic.js +16 -0
  45. package/dist/Symbolic.js.map +1 -1
  46. package/dist/index.d.ts +7 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +5 -0
  49. package/dist/index.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/Distributions.ts +391 -94
  52. package/src/EigenGeneral.ts +342 -0
  53. package/src/Frobenius.ts +203 -0
  54. package/src/Graph.ts +68 -0
  55. package/src/MatrixMath.ts +13 -0
  56. package/src/MonteCarlo.ts +257 -0
  57. package/src/Random.ts +104 -0
  58. package/src/Regression.ts +423 -0
  59. package/src/Shader.ts +837 -0
  60. package/src/SpecialFunctions.ts +52 -48
  61. package/src/Symbolic.ts +18 -0
  62. package/src/index.ts +34 -0
@@ -0,0 +1,423 @@
1
+ import { MatrixMath } from "./MatrixMath.ts";
2
+
3
+ /**
4
+ * Regression — nonlinear least squares (Levenberg-Marquardt) and robust
5
+ * (Huber) fitting, modelled on `scipy.optimize.least_squares` and
6
+ * `scipy.optimize.curve_fit`.
7
+ *
8
+ * - {@link Regression.leastSquares} minimises `0.5 * Σ ρ((r_i / f_scale)²) * f_scale²`
9
+ * over residuals `r(x)`, with SciPy's robust `loss` options and SciPy's
10
+ * robust-loss Jacobian rescaling. The solver is a damped Gauss-Newton
11
+ * (Levenberg-Marquardt) iteration with Marquardt scaling, so it reaches the
12
+ * same minimiser as SciPy's `method="lm"` / `"trf"` to the requested
13
+ * tolerances (iteration counts differ).
14
+ * - {@link Regression.curveFit} fits `y ≈ model(x, params)` and returns the
15
+ * parameter covariance exactly as `curve_fit` defines it.
16
+ * - {@link Regression.huberLinear} is the robust line fit mallory's regression
17
+ * panel offers (Huber loss), solved to convergence instead of by SGD.
18
+ */
19
+
20
+ export type RobustLoss = "linear" | "huber" | "soft_l1" | "cauchy" | "arctan";
21
+
22
+ export interface LeastSquaresOptions {
23
+ /** Analytic Jacobian `d residual_i / d x_j` (m×n). Default: central differences. */
24
+ jac?: (x: number[]) => number[][];
25
+ /** Loss function ρ applied to scaled squared residuals. Default `"linear"` (plain least squares). */
26
+ loss?: RobustLoss;
27
+ /** Residual scale at which a robust loss turns over (SciPy's `f_scale`). Default 1. */
28
+ fScale?: number;
29
+ /** Relative cost-reduction tolerance (default 1e-8, as SciPy). */
30
+ ftol?: number;
31
+ /** Step-size tolerance (default 1e-8). */
32
+ xtol?: number;
33
+ /** Gradient infinity-norm tolerance (default 1e-8). */
34
+ gtol?: number;
35
+ /** Maximum residual-function evaluations (default `200 * n`, min 1000). */
36
+ maxNfev?: number;
37
+ }
38
+
39
+ export interface LeastSquaresResult {
40
+ x: number[];
41
+ /** `0.5 * Σ ρ` at the solution (SciPy's `cost`; equals `0.5 Σ r²` for the linear loss). */
42
+ cost: number;
43
+ /** Residuals at the solution. */
44
+ fun: number[];
45
+ jac: number[][];
46
+ /** Gradient `Jᵀ ρ'(r) r` at the solution. */
47
+ grad: number[];
48
+ /** Gradient infinity-norm. */
49
+ optimality: number;
50
+ nfev: number;
51
+ njev: number;
52
+ /** 0: max evaluations, 1: gtol, 2: ftol, 3: xtol, 4: ftol and xtol (SciPy's codes). */
53
+ status: 0 | 1 | 2 | 3 | 4;
54
+ success: boolean;
55
+ message: string;
56
+ }
57
+
58
+ export interface CurveFitOptions extends Omit<LeastSquaresOptions, "jac"> {
59
+ /** Analytic `d model / d params` for one point: `(x, params) => number[]`. */
60
+ jac?: (x: number, params: number[]) => number[];
61
+ /** Per-point standard deviations (weights `1/sigma`). */
62
+ sigma?: number[];
63
+ /** Treat `sigma` as absolute (don't rescale the covariance by reduced chi-square). Default false. */
64
+ absoluteSigma?: boolean;
65
+ }
66
+
67
+ export interface CurveFitResult {
68
+ params: number[];
69
+ /** Parameter covariance, as `scipy.optimize.curve_fit`'s `pcov` (`Infinity` entries when singular). */
70
+ covariance: number[][];
71
+ /** Square roots of the covariance diagonal. */
72
+ standardErrors: number[];
73
+ /** `sqrt(Σ r²)`. */
74
+ residualNorm: number;
75
+ /** Coefficient of determination `1 - SS_res / SS_tot`. */
76
+ rSquared: number;
77
+ iterations: number;
78
+ converged: boolean;
79
+ fit: LeastSquaresResult;
80
+ }
81
+
82
+ export interface HuberLinearResult {
83
+ slope: number;
84
+ intercept: number;
85
+ iterations: number;
86
+ converged: boolean;
87
+ }
88
+
89
+ const EPS = Number.EPSILON;
90
+
91
+ /** ρ(z), ρ'(z), ρ''(z) for the SciPy robust losses, `z` = squared scaled residual. */
92
+ function rho(loss: RobustLoss, z: number): [number, number, number] {
93
+ switch (loss) {
94
+ case "linear":
95
+ return [z, 1, 0];
96
+ case "huber":
97
+ if (z <= 1) return [z, 1, 0];
98
+ return [2 * Math.sqrt(z) - 1, 1 / Math.sqrt(z), -0.5 * z ** -1.5];
99
+ case "soft_l1": {
100
+ const t = 1 + z;
101
+ return [2 * (Math.sqrt(t) - 1), 1 / Math.sqrt(t), -0.5 * t ** -1.5];
102
+ }
103
+ case "cauchy": {
104
+ const t = 1 + z;
105
+ return [Math.log1p(z), 1 / t, -1 / (t * t)];
106
+ }
107
+ case "arctan":
108
+ return [Math.atan(z), 1 / (1 + z * z), (-2 * z) / (1 + z * z) ** 2];
109
+ }
110
+ }
111
+
112
+ function robustCost(loss: RobustLoss, f: number[], fScale: number): number {
113
+ if (loss === "linear") return 0.5 * f.reduce((s, v) => s + v * v, 0);
114
+ const c2 = fScale * fScale;
115
+ return 0.5 * c2 * f.reduce((s, v) => s + rho(loss, (v / fScale) ** 2)[0], 0);
116
+ }
117
+
118
+ /** Invert a small symmetric matrix by Gauss-Jordan with partial pivoting; `null` when singular. */
119
+ function invert(a: number[][]): number[][] | null {
120
+ const n = a.length;
121
+ const m = a.map((row, i) => [...row, ...Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))]);
122
+ for (let c = 0; c < n; c++) {
123
+ let p = c;
124
+ for (let r = c + 1; r < n; r++) if (Math.abs(m[r][c]) > Math.abs(m[p][c])) p = r;
125
+ if (Math.abs(m[p][c]) < 1e-300) return null;
126
+ [m[c], m[p]] = [m[p], m[c]];
127
+ const d = m[c][c];
128
+ for (let j = 0; j < 2 * n; j++) m[c][j] /= d;
129
+ for (let r = 0; r < n; r++) {
130
+ if (r === c) continue;
131
+ const f = m[r][c];
132
+ if (f !== 0) for (let j = 0; j < 2 * n; j++) m[r][j] -= f * m[c][j];
133
+ }
134
+ }
135
+ return m.map((row) => row.slice(n));
136
+ }
137
+
138
+ export class Regression {
139
+ /**
140
+ * Nonlinear least squares by Levenberg-Marquardt -- `scipy.optimize.least_squares`
141
+ * (`method="lm"` for `loss: "linear"`; robust losses as `method="trf"` would
142
+ * minimise them). `fun(x)` returns the residual vector.
143
+ */
144
+ static leastSquares(
145
+ fun: (x: number[]) => number[],
146
+ x0: number[],
147
+ options: LeastSquaresOptions = {},
148
+ ): LeastSquaresResult {
149
+ const { loss = "linear", fScale = 1, ftol = 1e-8, xtol = 1e-8, gtol = 1e-8 } = options;
150
+ const n = x0.length;
151
+ const maxNfev = options.maxNfev ?? Math.max(1000, 200 * n);
152
+ let nfev = 0;
153
+ let njev = 0;
154
+ const evalF = (x: number[]) => {
155
+ nfev++;
156
+ return fun(x);
157
+ };
158
+ const jacobian = (x: number[], f: number[]): number[][] => {
159
+ njev++;
160
+ if (options.jac) return options.jac(x);
161
+ const m = f.length;
162
+ const J = Array.from({ length: m }, () => new Array<number>(n).fill(0));
163
+ for (let j = 0; j < n; j++) {
164
+ const h = Math.cbrt(EPS) * Math.max(Math.abs(x[j]), 1);
165
+ const xp = [...x];
166
+ const xm = [...x];
167
+ xp[j] += h;
168
+ xm[j] -= h;
169
+ const fp = fun(xp);
170
+ const fm = fun(xm);
171
+ for (let i = 0; i < m; i++) J[i][j] = (fp[i] - fm[i]) / (2 * h);
172
+ }
173
+ return J;
174
+ };
175
+
176
+ let x = [...x0];
177
+ let f = evalF(x);
178
+ let cost = robustCost(loss, f, fScale);
179
+ let J = jacobian(x, f);
180
+ let lambda = 1e-3;
181
+ let status: LeastSquaresResult["status"] = 0;
182
+ let message = "The maximum number of function evaluations is exceeded.";
183
+
184
+ /** Robust-loss scaled residual & Jacobian (SciPy's `scale_for_robust_loss`) and true gradient. */
185
+ const scaled = (fv: number[], Jv: number[][]) => {
186
+ if (loss === "linear") return { fs: fv, Js: Jv };
187
+ const m = fv.length;
188
+ const fs = new Array<number>(m);
189
+ const Js = new Array<number[]>(m);
190
+ for (let i = 0; i < m; i++) {
191
+ const [, r1, r2] = rho(loss, (fv[i] / fScale) ** 2);
192
+ let js = r1 + 2 * r2 * (fv[i] / fScale) ** 2;
193
+ if (js < EPS) js = EPS;
194
+ js = Math.sqrt(js);
195
+ const fsc = r1 / js;
196
+ fs[i] = fv[i] * fsc;
197
+ Js[i] = Jv[i].map((v) => v * js);
198
+ }
199
+ return { fs, Js };
200
+ };
201
+ const gradientOf = (fv: number[], Jv: number[][]): number[] => {
202
+ const g = new Array<number>(n).fill(0);
203
+ for (let i = 0; i < fv.length; i++) {
204
+ const w = loss === "linear" ? 1 : rho(loss, (fv[i] / fScale) ** 2)[1];
205
+ for (let j = 0; j < n; j++) g[j] += Jv[i][j] * w * fv[i];
206
+ }
207
+ return g;
208
+ };
209
+
210
+ let grad = gradientOf(f, J);
211
+ let optimality = Math.max(0, ...grad.map(Math.abs));
212
+ if (optimality <= gtol) {
213
+ status = 1;
214
+ message = "`gtol` termination condition is satisfied.";
215
+ }
216
+
217
+ while (status === 0 && nfev < maxNfev) {
218
+ const { fs, Js } = scaled(f, J);
219
+ const JtJ = Array.from({ length: n }, () => new Array<number>(n).fill(0));
220
+ const Jtf = new Array<number>(n).fill(0);
221
+ for (let i = 0; i < fs.length; i++) {
222
+ for (let a = 0; a < n; a++) {
223
+ Jtf[a] += Js[i][a] * fs[i];
224
+ for (let b = a; b < n; b++) JtJ[a][b] += Js[i][a] * Js[i][b];
225
+ }
226
+ }
227
+ for (let a = 0; a < n; a++) for (let b = 0; b < a; b++) JtJ[a][b] = JtJ[b][a];
228
+
229
+ let accepted = false;
230
+ let dxNorm = 0;
231
+ let reduction = 0;
232
+ for (let tries = 0; tries < 40 && !accepted && nfev < maxNfev; tries++) {
233
+ const A = JtJ.map((row, a) => row.map((v, b) => (a === b ? v + lambda * Math.max(JtJ[a][a], 1e-12) : v)));
234
+ let dx: number[];
235
+ try {
236
+ dx = [
237
+ ...MatrixMath.solve(
238
+ A,
239
+ Jtf.map((v) => -v),
240
+ ),
241
+ ];
242
+ } catch {
243
+ lambda *= 10;
244
+ continue;
245
+ }
246
+ if (dx.some((v) => !Number.isFinite(v))) {
247
+ lambda *= 10;
248
+ continue;
249
+ }
250
+ const xn = x.map((v, i) => v + dx[i]);
251
+ const fn = evalF(xn);
252
+ const cn = robustCost(loss, fn, fScale);
253
+ if (Number.isFinite(cn) && cn < cost) {
254
+ reduction = cost - cn;
255
+ dxNorm = Math.hypot(...dx);
256
+ const xNorm = Math.hypot(...x);
257
+ const prevCost = cost;
258
+ x = xn;
259
+ f = fn;
260
+ cost = cn;
261
+ lambda = Math.max(lambda / 10, 1e-14);
262
+ accepted = true;
263
+ J = jacobian(x, f);
264
+ grad = gradientOf(f, J);
265
+ optimality = Math.max(0, ...grad.map(Math.abs));
266
+ const fHit = reduction < ftol * prevCost;
267
+ const xHit = dxNorm < xtol * (xtol + xNorm);
268
+ if (optimality <= gtol) {
269
+ status = 1;
270
+ message = "`gtol` termination condition is satisfied.";
271
+ } else if (fHit && xHit) {
272
+ status = 4;
273
+ message = "Both `ftol` and `xtol` termination conditions are satisfied.";
274
+ } else if (fHit) {
275
+ status = 2;
276
+ message = "`ftol` termination condition is satisfied.";
277
+ } else if (xHit) {
278
+ status = 3;
279
+ message = "`xtol` termination condition is satisfied.";
280
+ }
281
+ } else lambda *= 10;
282
+ }
283
+ if (!accepted) {
284
+ // no damped step lowers the cost: a (numerical) stationary point
285
+ status = optimality <= Math.sqrt(gtol) || cost === 0 ? 1 : 2;
286
+ message = "No further improvement is possible at this precision.";
287
+ break;
288
+ }
289
+ }
290
+
291
+ return { x, cost, fun: f, jac: J, grad, optimality, nfev, njev, status, success: status > 0, message };
292
+ }
293
+
294
+ /**
295
+ * Fit `y ≈ model(x, params)` by least squares -- `scipy.optimize.curve_fit`
296
+ * (default `method="lm"`). `covariance` follows `curve_fit`: `(JᵀJ)⁻¹`
297
+ * scaled by the reduced chi-square unless `absoluteSigma` is set.
298
+ */
299
+ static curveFit(
300
+ model: (x: number, params: number[]) => number,
301
+ xs: number[],
302
+ ys: number[],
303
+ p0: number[],
304
+ options: CurveFitOptions = {},
305
+ ): CurveFitResult {
306
+ if (xs.length !== ys.length) throw new Error("xs and ys must have the same length.");
307
+ const m = xs.length;
308
+ const n = p0.length;
309
+ if (m < n) throw new Error(`curveFit needs at least as many points (${m}) as parameters (${n}).`);
310
+ const sigma = options.sigma;
311
+ const w = (i: number) => (sigma ? 1 / sigma[i] : 1);
312
+ const { jac: pointJac, sigma: _s, absoluteSigma, ...rest } = options;
313
+ const fit = Regression.leastSquares((p) => xs.map((x, i) => (model(x, p) - ys[i]) * w(i)), p0, {
314
+ ...rest,
315
+ jac: pointJac ? (p) => xs.map((x, i) => pointJac(x, p).map((d) => d * w(i))) : undefined,
316
+ });
317
+ const JtJ = Array.from({ length: n }, (_, a) =>
318
+ Array.from({ length: n }, (_, b) => fit.jac.reduce((s, row) => s + row[a] * row[b], 0)),
319
+ );
320
+ const inv = invert(JtJ);
321
+ let covariance: number[][];
322
+ if (!inv) covariance = Array.from({ length: n }, () => new Array<number>(n).fill(Number.POSITIVE_INFINITY));
323
+ else {
324
+ const sq = 2 * fit.cost; // Σ (r/σ)²
325
+ const scale = absoluteSigma ? 1 : m > n ? sq / (m - n) : Number.POSITIVE_INFINITY;
326
+ covariance = inv.map((row) => row.map((v) => v * scale));
327
+ }
328
+ const meanY = ys.reduce((a, b) => a + b, 0) / m;
329
+ const ssTot = ys.reduce((s, y) => s + (y - meanY) ** 2, 0);
330
+ const ssRes = xs.reduce((s, x, i) => s + (ys[i] - model(x, fit.x)) ** 2, 0);
331
+ return {
332
+ params: fit.x,
333
+ covariance,
334
+ standardErrors: covariance.map((row, i) => Math.sqrt(row[i])),
335
+ residualNorm: Math.sqrt(ssRes),
336
+ rSquared: ssTot === 0 ? (ssRes === 0 ? 1 : 0) : 1 - ssRes / ssTot,
337
+ iterations: fit.nfev,
338
+ converged: fit.success,
339
+ fit,
340
+ };
341
+ }
342
+
343
+ /**
344
+ * Robust straight-line fit `y = slope·x + intercept` minimising the Huber
345
+ * loss of the residuals scaled by `delta` -- the same objective as
346
+ * `scipy.optimize.least_squares(..., loss="huber", f_scale=delta)` on a
347
+ * linear model. Quadratic for residuals within `delta`, linear beyond, so an
348
+ * outlier pulls the line far less than ordinary least squares. Solved by
349
+ * iteratively reweighted least squares to convergence (the objective is convex).
350
+ */
351
+ static huberLinear(
352
+ xs: number[],
353
+ ys: number[],
354
+ options: { delta?: number; maxIterations?: number; tolerance?: number } = {},
355
+ ): HuberLinearResult {
356
+ const { delta = 1, maxIterations = 200, tolerance = 1e-12 } = options;
357
+ if (xs.length !== ys.length) throw new Error("xs and ys must have the same length.");
358
+ if (xs.length < 2) throw new Error("Enter at least two (x, y) rows.");
359
+ if (!(delta > 0)) throw new Error("delta must be positive.");
360
+ const m = xs.length;
361
+ const wls = (weights: number[]): [number, number] => {
362
+ let sw = 0;
363
+ let sx = 0;
364
+ let sy = 0;
365
+ let sxx = 0;
366
+ let sxy = 0;
367
+ for (let i = 0; i < m; i++) {
368
+ const w = weights[i];
369
+ sw += w;
370
+ sx += w * xs[i];
371
+ sy += w * ys[i];
372
+ sxx += w * xs[i] * xs[i];
373
+ sxy += w * xs[i] * ys[i];
374
+ }
375
+ const det = sw * sxx - sx * sx;
376
+ if (Math.abs(det) < 1e-300) return [0, sy / sw];
377
+ return [(sw * sxy - sx * sy) / det, (sxx * sy - sx * sxy) / det];
378
+ };
379
+ let [slope, intercept] = wls(new Array<number>(m).fill(1));
380
+ let iterations = 0;
381
+ let converged = false;
382
+ for (; iterations < maxIterations; iterations++) {
383
+ const weights = xs.map((x, i) => {
384
+ const r = Math.abs(ys[i] - (slope * x + intercept));
385
+ return r <= delta ? 1 : delta / r;
386
+ });
387
+ const [s, b] = wls(weights);
388
+ const change = Math.max(Math.abs(s - slope), Math.abs(b - intercept));
389
+ slope = s;
390
+ intercept = b;
391
+ if (change <= tolerance * (1 + Math.abs(slope) + Math.abs(intercept))) {
392
+ converged = true;
393
+ iterations++;
394
+ break;
395
+ }
396
+ }
397
+ return { slope, intercept, iterations, converged };
398
+ }
399
+
400
+ /**
401
+ * Indices of points whose residual from `y = slope·x + intercept` is more
402
+ * than `thresholdMads` scaled median absolute deviations from the median
403
+ * residual (MAD × 1.4826 estimates a Gaussian σ without being skewed by the
404
+ * outliers themselves). Empty when the MAD is zero.
405
+ */
406
+ static outlierIndices(xs: number[], ys: number[], slope: number, intercept: number, thresholdMads = 2.5): number[] {
407
+ if (xs.length === 0) return [];
408
+ const median = (v: number[]) => {
409
+ const s = [...v].sort((a, b) => a - b);
410
+ return s.length % 2 ? s[(s.length - 1) / 2] : (s[s.length / 2 - 1] + s[s.length / 2]) / 2;
411
+ };
412
+ const residuals = ys.map((y, i) => y - (slope * xs[i] + intercept));
413
+ const med = median(residuals);
414
+ const mad = median(residuals.map((r) => Math.abs(r - med)));
415
+ if (mad === 0) return [];
416
+ const scaled = mad * 1.4826;
417
+ const out: number[] = [];
418
+ residuals.forEach((r, i) => {
419
+ if (Math.abs(r - med) / scaled > thresholdMads) out.push(i);
420
+ });
421
+ return out;
422
+ }
423
+ }