@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,257 @@
1
+ import type { Rng } from "./Random.ts";
2
+ import { Symbolic } from "./Symbolic.ts";
3
+
4
+ /**
5
+ * MonteCarlo — seeded Monte Carlo helpers: dart-throwing π, crude Monte Carlo
6
+ * integration with running error bands, histogramming of distribution samples,
7
+ * the central limit theorem demo, random walks and the bootstrap.
8
+ *
9
+ * Every function takes an explicit {@link Rng} (see `Random.ts`), so the same
10
+ * seed always reproduces the same run. They mirror mallory's Monte Carlo panel
11
+ * (`lib/monte-carlo.ts`) and keep its result shapes.
12
+ */
13
+
14
+ export interface DartPoint {
15
+ x: number;
16
+ y: number;
17
+ inside: boolean;
18
+ }
19
+
20
+ export interface ConvergencePoint {
21
+ n: number;
22
+ estimate: number;
23
+ }
24
+
25
+ export interface DartPiResult {
26
+ piEstimate: number;
27
+ n: number;
28
+ /** Capped at ~3000 points for rendering; the estimate itself uses all `n` throws. */
29
+ points: DartPoint[];
30
+ /** Running estimate at ~100 checkpoints. */
31
+ convergence: ConvergencePoint[];
32
+ }
33
+
34
+ export interface IntegrationConvergencePoint extends ConvergencePoint {
35
+ /** 95% CI half-width around `estimate` at this checkpoint. */
36
+ errorBand: number;
37
+ }
38
+
39
+ export interface IntegrationResult {
40
+ estimate: number;
41
+ /** 95% CI half-width of the final estimate (`1.96 * (b-a) * stdErr`). */
42
+ errorBand: number;
43
+ n: number;
44
+ convergence: IntegrationConvergencePoint[];
45
+ /** Present only when an exact value was supplied or derivable (string integrands). */
46
+ trueValue?: number;
47
+ absoluteError?: number;
48
+ }
49
+
50
+ export interface HistogramBin {
51
+ x0: number;
52
+ x1: number;
53
+ count: number;
54
+ }
55
+
56
+ export interface BootstrapResult {
57
+ /** The statistic on the original data. */
58
+ estimate: number;
59
+ /** The statistic on each resample. */
60
+ replicates: number[];
61
+ /** Standard deviation of the replicates. */
62
+ standardError: number;
63
+ /** Percentile confidence interval at `level`. */
64
+ confidenceInterval: [number, number];
65
+ }
66
+
67
+ const MAX_RENDERED_DARTS = 3000;
68
+ const CHECKPOINTS = 100;
69
+
70
+ function requirePositiveInt(n: number, what: string): void {
71
+ if (!Number.isInteger(n) || n <= 0) throw new Error(`${what} must be a positive integer.`);
72
+ }
73
+
74
+ export class MonteCarlo {
75
+ /**
76
+ * Dart-throwing estimate of π: darts land uniformly in the 2×2 square, and
77
+ * the fraction inside the unit circle tends to π/4.
78
+ */
79
+ static estimatePi(n: number, rng: Rng): DartPiResult {
80
+ requirePositiveInt(n, "Sample count");
81
+ let inside = 0;
82
+ const points: DartPoint[] = [];
83
+ const renderEvery = Math.max(1, Math.floor(n / MAX_RENDERED_DARTS));
84
+ const checkEvery = Math.max(1, Math.floor(n / CHECKPOINTS));
85
+ const convergence: ConvergencePoint[] = [];
86
+ for (let i = 0; i < n; i++) {
87
+ const x = rng.nextFloat() * 2 - 1;
88
+ const y = rng.nextFloat() * 2 - 1;
89
+ const hit = x * x + y * y <= 1;
90
+ if (hit) inside++;
91
+ if (i % renderEvery === 0) points.push({ x, y, inside: hit });
92
+ if ((i + 1) % checkEvery === 0) convergence.push({ n: i + 1, estimate: (inside / (i + 1)) * 4 });
93
+ }
94
+ return { piEstimate: (inside / n) * 4, n, points, convergence };
95
+ }
96
+
97
+ /**
98
+ * Crude Monte Carlo integral of `f` over `[a, b]`: `(b-a) * mean(f(x_i))`,
99
+ * `x_i ~ U(a, b)`, with Welford running variance for the error band. `f` is a
100
+ * function, or an expression string in `variable` (default `"x"`), in which
101
+ * case the exact value from `Symbolic.integrateDefinite` is reported too
102
+ * (when it can be computed). Pass `trueValue` to supply your own reference.
103
+ */
104
+ static integrate(
105
+ f: ((x: number) => number) | string,
106
+ a: number,
107
+ b: number,
108
+ n: number,
109
+ rng: Rng,
110
+ options: { variable?: string; trueValue?: number } = {},
111
+ ): IntegrationResult {
112
+ if (!(b > a)) throw new Error(`Upper bound must be greater than lower bound -- got [${a}, ${b}].`);
113
+ requirePositiveInt(n, "Sample count");
114
+ const variable = options.variable ?? "x";
115
+ let fn: (x: number) => number;
116
+ let trueValue = options.trueValue;
117
+ if (typeof f === "string") {
118
+ const compiled = Symbolic.compile(f);
119
+ fn = (x) => compiled({ [variable]: x });
120
+ if (trueValue === undefined) {
121
+ try {
122
+ trueValue = Symbolic.integrateDefinite(Symbolic.parse(f), a, b, variable);
123
+ } catch {
124
+ // no closed form: report the estimate without a reference value
125
+ }
126
+ }
127
+ } else fn = f;
128
+
129
+ const checkEvery = Math.max(1, Math.floor(n / CHECKPOINTS));
130
+ const span = b - a;
131
+ const convergence: IntegrationConvergencePoint[] = [];
132
+ let mean = 0;
133
+ let m2 = 0;
134
+ let band = 0;
135
+ for (let i = 0; i < n; i++) {
136
+ const y = fn(a + rng.nextFloat() * span);
137
+ const count = i + 1;
138
+ const delta = y - mean;
139
+ mean += delta / count;
140
+ m2 += delta * (y - mean);
141
+ if (count % checkEvery === 0 || count === n) {
142
+ band = 1.96 * span * Math.sqrt((count > 1 ? m2 / (count - 1) : 0) / count);
143
+ if (count % checkEvery === 0) convergence.push({ n: count, estimate: span * mean, errorBand: band });
144
+ }
145
+ }
146
+ const estimate = span * mean;
147
+ const result: IntegrationResult = { estimate, errorBand: band, n, convergence };
148
+ if (trueValue !== undefined) {
149
+ result.trueValue = trueValue;
150
+ result.absoluteError = Math.abs(estimate - trueValue);
151
+ }
152
+ return result;
153
+ }
154
+
155
+ /**
156
+ * Partition `values` into `binCount` equal-width bins over `[min, max]`; each
157
+ * bin is half-open `[x0, x1)` except the last, which also takes the maximum.
158
+ */
159
+ static histogram(values: readonly number[], binCount = 20): HistogramBin[] {
160
+ requirePositiveInt(binCount, "Bin count");
161
+ let min = Number.POSITIVE_INFINITY;
162
+ let max = Number.NEGATIVE_INFINITY;
163
+ for (const v of values) {
164
+ if (v < min) min = v;
165
+ if (v > max) max = v;
166
+ }
167
+ const width = (max - min) / binCount || 1;
168
+ const bins: HistogramBin[] = Array.from({ length: binCount }, (_, i) => ({
169
+ x0: min + i * width,
170
+ x1: min + (i + 1) * width,
171
+ count: 0,
172
+ }));
173
+ for (const v of values) bins[Math.min(binCount - 1, Math.max(0, Math.floor((v - min) / width)))].count++;
174
+ return bins;
175
+ }
176
+
177
+ /**
178
+ * Central limit theorem demo: the means of `trials` batches of `sampleSize`
179
+ * draws from `sampler`, plus a histogram of those means. The means have
180
+ * standard deviation about `sd / sqrt(sampleSize)` whatever `sampler`'s shape.
181
+ */
182
+ static centralLimit(
183
+ sampler: () => number,
184
+ sampleSize: number,
185
+ trials: number,
186
+ binCount = 20,
187
+ ): { means: number[]; mean: number; standardDeviation: number; bins: HistogramBin[] } {
188
+ requirePositiveInt(sampleSize, "Sample size");
189
+ requirePositiveInt(trials, "Trial count");
190
+ const means: number[] = [];
191
+ for (let t = 0; t < trials; t++) {
192
+ let s = 0;
193
+ for (let i = 0; i < sampleSize; i++) s += sampler();
194
+ means.push(s / sampleSize);
195
+ }
196
+ const mean = means.reduce((x, y) => x + y, 0) / trials;
197
+ const variance = trials > 1 ? means.reduce((x, y) => x + (y - mean) ** 2, 0) / (trials - 1) : 0;
198
+ return { means, mean, standardDeviation: Math.sqrt(variance), bins: MonteCarlo.histogram(means, binCount) };
199
+ }
200
+
201
+ /**
202
+ * Simple symmetric random walk on the integer lattice in `dimensions` (1-3)
203
+ * dimensions: each step moves ±1 along one uniformly chosen axis. Returns
204
+ * `steps + 1` positions starting at the origin.
205
+ */
206
+ static randomWalk(steps: number, rng: Rng, dimensions = 1): number[][] {
207
+ requirePositiveInt(steps, "Step count");
208
+ if (![1, 2, 3].includes(dimensions)) throw new Error("dimensions must be 1, 2 or 3.");
209
+ const pos = new Array<number>(dimensions).fill(0);
210
+ const path: number[][] = [[...pos]];
211
+ for (let i = 0; i < steps; i++) {
212
+ pos[rng.nextBelow(dimensions)] += rng.nextBelow(2) === 0 ? -1 : 1;
213
+ path.push([...pos]);
214
+ }
215
+ return path;
216
+ }
217
+
218
+ /**
219
+ * Nonparametric bootstrap of `statistic` over `data`: `resamples` draws with
220
+ * replacement, giving the replicate distribution, its standard error and a
221
+ * percentile confidence interval at `level` (default 0.95).
222
+ */
223
+ static bootstrap(
224
+ data: readonly number[],
225
+ statistic: (sample: number[]) => number,
226
+ rng: Rng,
227
+ options: { resamples?: number; level?: number } = {},
228
+ ): BootstrapResult {
229
+ const { resamples = 1000, level = 0.95 } = options;
230
+ if (data.length === 0) throw new Error("bootstrap needs a non-empty sample.");
231
+ requirePositiveInt(resamples, "Resample count");
232
+ if (!(level > 0 && level < 1)) throw new Error("level must be strictly between 0 and 1.");
233
+ const n = data.length;
234
+ const replicates: number[] = [];
235
+ for (let r = 0; r < resamples; r++) {
236
+ const sample = new Array<number>(n);
237
+ for (let i = 0; i < n; i++) sample[i] = data[rng.nextBelow(n)];
238
+ replicates.push(statistic(sample));
239
+ }
240
+ const mean = replicates.reduce((x, y) => x + y, 0) / resamples;
241
+ const standardError = Math.sqrt(
242
+ resamples > 1 ? replicates.reduce((x, y) => x + (y - mean) ** 2, 0) / (resamples - 1) : 0,
243
+ );
244
+ const sorted = [...replicates].sort((x, y) => x - y);
245
+ const q = (p: number) => {
246
+ const h = (sorted.length - 1) * p;
247
+ const lo = Math.floor(h);
248
+ return sorted[lo] + (h - lo) * ((sorted[Math.min(lo + 1, sorted.length - 1)] ?? sorted[lo]) - sorted[lo]);
249
+ };
250
+ return {
251
+ estimate: statistic([...data]),
252
+ replicates,
253
+ standardError,
254
+ confidenceInterval: [q((1 - level) / 2), q(1 - (1 - level) / 2)],
255
+ };
256
+ }
257
+ }
package/src/Random.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Rng — a seeded, reproducible pseudo-random generator (PCG32, XSH-RR variant;
3
+ * see https://www.pcg-random.org/). The same seed always yields the same
4
+ * stream, which is the point: reproducible Monte Carlo runs and deterministic
5
+ * tests. There is no shared global state — every consumer takes an explicit
6
+ * generator, so nothing interferes across tests.
7
+ *
8
+ * The stream is bit-identical to `Rng` in `@johnhenry/math-plus-tensor-core`
9
+ * (same constants, same seeding), so existing seeds keep their results.
10
+ *
11
+ * `rng.next` is a bound `() => number` returning floats in `[0, 1)`, which is
12
+ * the `rng` hook {@link Distributions} factories and the Monte Carlo helpers
13
+ * accept.
14
+ */
15
+
16
+ // 64-bit LCG state held as two uint32 limbs (hi, lo) so no BigInt is needed on the hot path.
17
+ const MULT_HI = 0x5851f42d;
18
+ const MULT_LO = 0x4c957f2d;
19
+ const TWO32 = 4294967296;
20
+
21
+ /** Full 32x32 -> 64-bit unsigned product as [hi, lo], exact in doubles via 16-bit limbs. */
22
+ function mul32(a: number, b: number): [number, number] {
23
+ const a0 = a & 0xffff;
24
+ const a1 = a >>> 16;
25
+ const b0 = b & 0xffff;
26
+ const b1 = b >>> 16;
27
+ const mid = a0 * b1 + a1 * b0;
28
+ const lo64 = a0 * b0 + (mid % 65536) * 65536;
29
+ const carry = Math.floor(lo64 / TWO32);
30
+ return [a1 * b1 + Math.floor(mid / 65536) + carry, lo64 % TWO32];
31
+ }
32
+
33
+ export class Rng {
34
+ #hi = 0;
35
+ #lo = 0;
36
+ readonly #incHi: number;
37
+ readonly #incLo: number;
38
+
39
+ constructor(seedValue: number | bigint = 0n, sequence: number | bigint = 1n) {
40
+ const inc = ((BigInt(sequence) << 1n) | 1n) & 0xffffffffffffffffn;
41
+ this.#incHi = Number(inc >> 32n);
42
+ this.#incLo = Number(inc & 0xffffffffn);
43
+ this.#step();
44
+ const s = BigInt(seedValue) & 0xffffffffffffffffn;
45
+ const lo = this.#lo + Number(s & 0xffffffffn);
46
+ this.#lo = lo % TWO32;
47
+ this.#hi = (this.#hi + Number(s >> 32n) + Math.floor(lo / TWO32)) % TWO32;
48
+ this.#step();
49
+ }
50
+
51
+ /** Advance the LCG; returns the previous state as [hi, lo]. */
52
+ #step(): void {
53
+ const [ph, pl] = mul32(this.#lo, MULT_LO);
54
+ const cross = (Math.imul(this.#hi, MULT_LO) + Math.imul(this.#lo, MULT_HI)) >>> 0;
55
+ const lo = pl + this.#incLo;
56
+ this.#lo = lo % TWO32;
57
+ this.#hi = (ph + cross + this.#incHi + Math.floor(lo / TWO32)) % TWO32;
58
+ }
59
+
60
+ /** Next integer in `[0, 2^32)`. */
61
+ nextUint32(): number {
62
+ const hi = this.#hi;
63
+ const lo = this.#lo;
64
+ this.#step();
65
+ const xh = (hi ^ (hi >>> 18)) >>> 0;
66
+ const xl = (lo ^ (((hi << 14) | (lo >>> 18)) >>> 0)) >>> 0;
67
+ const v = ((xl >>> 27) | (xh << 5)) >>> 0;
68
+ const rot = hi >>> 27;
69
+ return ((v >>> rot) | (v << (-rot & 31))) >>> 0;
70
+ }
71
+
72
+ /** Uniform float in `[0, 1)`. */
73
+ nextFloat(): number {
74
+ return this.nextUint32() / 4294967296;
75
+ }
76
+
77
+ /** Bound alias of {@link nextFloat}, usable directly as a `() => number` rng hook. */
78
+ readonly next = (): number => this.nextFloat();
79
+
80
+ /** Uniform integer in `[0, bound)` without modulo bias. */
81
+ nextBelow(bound: number): number {
82
+ if (!Number.isInteger(bound) || bound <= 0)
83
+ throw new RangeError(`nextBelow: bound must be a positive integer, got ${bound}`);
84
+ const limit = Math.floor(4294967296 / bound) * bound;
85
+ let x: number;
86
+ do {
87
+ x = this.nextUint32();
88
+ } while (x >= limit);
89
+ return x % bound;
90
+ }
91
+
92
+ /** Standard-normal draw (Box-Muller, two uniforms per call). */
93
+ nextNormal(mean = 0, sd = 1): number {
94
+ let u1 = this.nextFloat();
95
+ if (u1 === 0) u1 = Number.MIN_VALUE;
96
+ const u2 = this.nextFloat();
97
+ return mean + sd * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
98
+ }
99
+ }
100
+
101
+ /** A fresh seeded generator. */
102
+ export function seed(value: number | bigint): Rng {
103
+ return new Rng(value);
104
+ }