@johnhenry/math 0.0.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 (242) hide show
  1. package/README.md +165 -0
  2. package/dist/Bivector4.d.ts +52 -0
  3. package/dist/Bivector4.d.ts.map +1 -0
  4. package/dist/Bivector4.js +105 -0
  5. package/dist/Bivector4.js.map +1 -0
  6. package/dist/CellGraph.d.ts +259 -0
  7. package/dist/CellGraph.d.ts.map +1 -0
  8. package/dist/CellGraph.js +637 -0
  9. package/dist/CellGraph.js.map +1 -0
  10. package/dist/Clifford4Internal.d.ts +35 -0
  11. package/dist/Clifford4Internal.d.ts.map +1 -0
  12. package/dist/Clifford4Internal.js +86 -0
  13. package/dist/Clifford4Internal.js.map +1 -0
  14. package/dist/Combinatorics.d.ts +61 -0
  15. package/dist/Combinatorics.d.ts.map +1 -0
  16. package/dist/Combinatorics.js +186 -0
  17. package/dist/Combinatorics.js.map +1 -0
  18. package/dist/CombinatoricsGenerators.d.ts +44 -0
  19. package/dist/CombinatoricsGenerators.d.ts.map +1 -0
  20. package/dist/CombinatoricsGenerators.js +148 -0
  21. package/dist/CombinatoricsGenerators.js.map +1 -0
  22. package/dist/ComplexMath.d.ts +46 -0
  23. package/dist/ComplexMath.d.ts.map +1 -0
  24. package/dist/ComplexMath.js +98 -0
  25. package/dist/ComplexMath.js.map +1 -0
  26. package/dist/ComplexNumber.d.ts +174 -0
  27. package/dist/ComplexNumber.d.ts.map +1 -0
  28. package/dist/ComplexNumber.js +443 -0
  29. package/dist/ComplexNumber.js.map +1 -0
  30. package/dist/Cycle.d.ts +42 -0
  31. package/dist/Cycle.d.ts.map +1 -0
  32. package/dist/Cycle.js +120 -0
  33. package/dist/Cycle.js.map +1 -0
  34. package/dist/Decimal.d.ts +45 -0
  35. package/dist/Decimal.d.ts.map +1 -0
  36. package/dist/Decimal.js +135 -0
  37. package/dist/Decimal.js.map +1 -0
  38. package/dist/Distributions.d.ts +55 -0
  39. package/dist/Distributions.d.ts.map +1 -0
  40. package/dist/Distributions.js +222 -0
  41. package/dist/Distributions.js.map +1 -0
  42. package/dist/DualNumber.d.ts +42 -0
  43. package/dist/DualNumber.d.ts.map +1 -0
  44. package/dist/DualNumber.js +91 -0
  45. package/dist/DualNumber.js.map +1 -0
  46. package/dist/Environment.d.ts +44 -0
  47. package/dist/Environment.d.ts.map +1 -0
  48. package/dist/Environment.js +129 -0
  49. package/dist/Environment.js.map +1 -0
  50. package/dist/Expression.d.ts +19 -0
  51. package/dist/Expression.d.ts.map +1 -0
  52. package/dist/Expression.js +43 -0
  53. package/dist/Expression.js.map +1 -0
  54. package/dist/FFT.d.ts +21 -0
  55. package/dist/FFT.d.ts.map +1 -0
  56. package/dist/FFT.js +136 -0
  57. package/dist/FFT.js.map +1 -0
  58. package/dist/GeometricAlgebra4.d.ts +73 -0
  59. package/dist/GeometricAlgebra4.d.ts.map +1 -0
  60. package/dist/GeometricAlgebra4.js +115 -0
  61. package/dist/GeometricAlgebra4.js.map +1 -0
  62. package/dist/Geometry.d.ts +41 -0
  63. package/dist/Geometry.d.ts.map +1 -0
  64. package/dist/Geometry.js +167 -0
  65. package/dist/Geometry.js.map +1 -0
  66. package/dist/Graph.d.ts +72 -0
  67. package/dist/Graph.d.ts.map +1 -0
  68. package/dist/Graph.js +337 -0
  69. package/dist/Graph.js.map +1 -0
  70. package/dist/Graph3DUtils.d.ts +80 -0
  71. package/dist/Graph3DUtils.d.ts.map +1 -0
  72. package/dist/Graph3DUtils.js +156 -0
  73. package/dist/Graph3DUtils.js.map +1 -0
  74. package/dist/GraphUtils.d.ts +67 -0
  75. package/dist/GraphUtils.d.ts.map +1 -0
  76. package/dist/GraphUtils.js +72 -0
  77. package/dist/GraphUtils.js.map +1 -0
  78. package/dist/GroupTheory.d.ts +73 -0
  79. package/dist/GroupTheory.d.ts.map +1 -0
  80. package/dist/GroupTheory.js +194 -0
  81. package/dist/GroupTheory.js.map +1 -0
  82. package/dist/IntUtils.d.ts +33 -0
  83. package/dist/IntUtils.d.ts.map +1 -0
  84. package/dist/IntUtils.js +185 -0
  85. package/dist/IntUtils.js.map +1 -0
  86. package/dist/Interval.d.ts +44 -0
  87. package/dist/Interval.d.ts.map +1 -0
  88. package/dist/Interval.js +165 -0
  89. package/dist/Interval.js.map +1 -0
  90. package/dist/Lattice.d.ts +48 -0
  91. package/dist/Lattice.d.ts.map +1 -0
  92. package/dist/Lattice.js +65 -0
  93. package/dist/Lattice.js.map +1 -0
  94. package/dist/Logic.d.ts +22 -0
  95. package/dist/Logic.d.ts.map +1 -0
  96. package/dist/Logic.js +34 -0
  97. package/dist/Logic.js.map +1 -0
  98. package/dist/MatrixMath.d.ts +71 -0
  99. package/dist/MatrixMath.d.ts.map +1 -0
  100. package/dist/MatrixMath.js +302 -0
  101. package/dist/MatrixMath.js.map +1 -0
  102. package/dist/NumberTheory.d.ts +49 -0
  103. package/dist/NumberTheory.d.ts.map +1 -0
  104. package/dist/NumberTheory.js +230 -0
  105. package/dist/NumberTheory.js.map +1 -0
  106. package/dist/Numerical.d.ts +85 -0
  107. package/dist/Numerical.d.ts.map +1 -0
  108. package/dist/Numerical.js +343 -0
  109. package/dist/Numerical.js.map +1 -0
  110. package/dist/Permutation.d.ts +43 -0
  111. package/dist/Permutation.d.ts.map +1 -0
  112. package/dist/Permutation.js +142 -0
  113. package/dist/Permutation.js.map +1 -0
  114. package/dist/Polygon.d.ts +64 -0
  115. package/dist/Polygon.d.ts.map +1 -0
  116. package/dist/Polygon.js +221 -0
  117. package/dist/Polygon.js.map +1 -0
  118. package/dist/PolynomialRing.d.ts +74 -0
  119. package/dist/PolynomialRing.d.ts.map +1 -0
  120. package/dist/PolynomialRing.js +231 -0
  121. package/dist/PolynomialRing.js.map +1 -0
  122. package/dist/Quaternion.d.ts +37 -0
  123. package/dist/Quaternion.d.ts.map +1 -0
  124. package/dist/Quaternion.js +106 -0
  125. package/dist/Quaternion.js.map +1 -0
  126. package/dist/Rational.d.ts +34 -0
  127. package/dist/Rational.d.ts.map +1 -0
  128. package/dist/Rational.js +136 -0
  129. package/dist/Rational.js.map +1 -0
  130. package/dist/RealMath.d.ts +112 -0
  131. package/dist/RealMath.d.ts.map +1 -0
  132. package/dist/RealMath.js +262 -0
  133. package/dist/RealMath.js.map +1 -0
  134. package/dist/Rotor4.d.ts +180 -0
  135. package/dist/Rotor4.d.ts.map +1 -0
  136. package/dist/Rotor4.js +388 -0
  137. package/dist/Rotor4.js.map +1 -0
  138. package/dist/SpecialFunctions.d.ts +32 -0
  139. package/dist/SpecialFunctions.d.ts.map +1 -0
  140. package/dist/SpecialFunctions.js +161 -0
  141. package/dist/SpecialFunctions.js.map +1 -0
  142. package/dist/SpecialOperator.d.ts +21 -0
  143. package/dist/SpecialOperator.d.ts.map +1 -0
  144. package/dist/SpecialOperator.js +26 -0
  145. package/dist/SpecialOperator.js.map +1 -0
  146. package/dist/Statistics.d.ts +50 -0
  147. package/dist/Statistics.d.ts.map +1 -0
  148. package/dist/Statistics.js +131 -0
  149. package/dist/Statistics.js.map +1 -0
  150. package/dist/StringEvaluator.d.ts +48 -0
  151. package/dist/StringEvaluator.d.ts.map +1 -0
  152. package/dist/StringEvaluator.js +190 -0
  153. package/dist/StringEvaluator.js.map +1 -0
  154. package/dist/Structure.d.ts +130 -0
  155. package/dist/Structure.d.ts.map +1 -0
  156. package/dist/Structure.js +394 -0
  157. package/dist/Structure.js.map +1 -0
  158. package/dist/Symbolic.d.ts +501 -0
  159. package/dist/Symbolic.d.ts.map +1 -0
  160. package/dist/Symbolic.js +4077 -0
  161. package/dist/Symbolic.js.map +1 -0
  162. package/dist/Type.d.ts +31 -0
  163. package/dist/Type.d.ts.map +1 -0
  164. package/dist/Type.js +47 -0
  165. package/dist/Type.js.map +1 -0
  166. package/dist/Utilities.d.ts +27 -0
  167. package/dist/Utilities.d.ts.map +1 -0
  168. package/dist/Utilities.js +47 -0
  169. package/dist/Utilities.js.map +1 -0
  170. package/dist/Vec4.d.ts +39 -0
  171. package/dist/Vec4.d.ts.map +1 -0
  172. package/dist/Vec4.js +82 -0
  173. package/dist/Vec4.js.map +1 -0
  174. package/dist/Vector.d.ts +83 -0
  175. package/dist/Vector.d.ts.map +1 -0
  176. package/dist/Vector.js +173 -0
  177. package/dist/Vector.js.map +1 -0
  178. package/dist/VectorCalculus.d.ts +42 -0
  179. package/dist/VectorCalculus.d.ts.map +1 -0
  180. package/dist/VectorCalculus.js +82 -0
  181. package/dist/VectorCalculus.js.map +1 -0
  182. package/dist/VectorUtils.d.ts +172 -0
  183. package/dist/VectorUtils.d.ts.map +1 -0
  184. package/dist/VectorUtils.js +706 -0
  185. package/dist/VectorUtils.js.map +1 -0
  186. package/dist/differentiation-rules.d.ts +21 -0
  187. package/dist/differentiation-rules.d.ts.map +1 -0
  188. package/dist/differentiation-rules.js +52 -0
  189. package/dist/differentiation-rules.js.map +1 -0
  190. package/dist/index.d.ts +52 -0
  191. package/dist/index.d.ts.map +1 -0
  192. package/dist/index.js +66 -0
  193. package/dist/index.js.map +1 -0
  194. package/package.json +68 -0
  195. package/src/Bivector4.ts +136 -0
  196. package/src/CellGraph.ts +669 -0
  197. package/src/Clifford4Internal.ts +90 -0
  198. package/src/Combinatorics.ts +183 -0
  199. package/src/CombinatoricsGenerators.ts +144 -0
  200. package/src/ComplexMath.ts +162 -0
  201. package/src/ComplexNumber.ts +491 -0
  202. package/src/Cycle.ts +126 -0
  203. package/src/Decimal.ts +145 -0
  204. package/src/Distributions.ts +269 -0
  205. package/src/DualNumber.ts +105 -0
  206. package/src/Environment.ts +127 -0
  207. package/src/Expression.ts +48 -0
  208. package/src/FFT.ts +146 -0
  209. package/src/GeometricAlgebra4.ts +157 -0
  210. package/src/Geometry.ts +173 -0
  211. package/src/Graph.ts +337 -0
  212. package/src/Graph3DUtils.ts +269 -0
  213. package/src/GraphUtils.ts +159 -0
  214. package/src/GroupTheory.ts +214 -0
  215. package/src/IntUtils.ts +200 -0
  216. package/src/Interval.ts +171 -0
  217. package/src/Lattice.ts +87 -0
  218. package/src/Logic.ts +38 -0
  219. package/src/MatrixMath.ts +326 -0
  220. package/src/NumberTheory.ts +228 -0
  221. package/src/Numerical.ts +390 -0
  222. package/src/Permutation.ts +148 -0
  223. package/src/Polygon.ts +229 -0
  224. package/src/PolynomialRing.ts +242 -0
  225. package/src/Quaternion.ts +125 -0
  226. package/src/Rational.ts +148 -0
  227. package/src/RealMath.ts +346 -0
  228. package/src/Rotor4.ts +440 -0
  229. package/src/SpecialFunctions.ts +152 -0
  230. package/src/SpecialOperator.ts +28 -0
  231. package/src/Statistics.ts +170 -0
  232. package/src/StringEvaluator.ts +208 -0
  233. package/src/Structure.ts +481 -0
  234. package/src/Symbolic.ts +4313 -0
  235. package/src/Type.ts +48 -0
  236. package/src/Utilities.ts +49 -0
  237. package/src/Vec4.ts +98 -0
  238. package/src/Vector.ts +189 -0
  239. package/src/VectorCalculus.ts +87 -0
  240. package/src/VectorUtils.ts +797 -0
  241. package/src/differentiation-rules.ts +73 -0
  242. package/src/index.ts +140 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * NumberTheory — fast, big-integer number theory: modular exponentiation and
3
+ * inverse, the Chinese Remainder Theorem, Miller-Rabin primality, Pollard-rho
4
+ * factorisation, GCD/LCM, and the Legendre/Jacobi symbols. Everything is
5
+ * `bigint`-based, so it does not overflow for large inputs.
6
+ */
7
+
8
+ const big = (x: bigint | number): bigint => (typeof x === "bigint" ? x : BigInt(x));
9
+ const babs = (x: bigint): bigint => (x < 0n ? -x : x);
10
+
11
+ function bgcd(a: bigint, b: bigint): bigint {
12
+ let x = babs(a);
13
+ let y = babs(b);
14
+ while (y) [x, y] = [y, x % y];
15
+ return x;
16
+ }
17
+
18
+ // Deterministic Miller-Rabin witnesses (valid for all n < 3.3·10^24).
19
+ const MR_WITNESSES = [2n, 3n, 5n, 7n, 11n, 13n, 17n, 19n, 23n, 29n, 31n, 37n];
20
+
21
+ export class NumberTheory {
22
+ /** `base^exp mod modulus`, computed by square-and-multiply. */
23
+ static modPow(base: bigint | number, exp: bigint | number, modulus: bigint | number): bigint {
24
+ const m = big(modulus);
25
+ if (m === 1n) return 0n;
26
+ let b = ((big(base) % m) + m) % m;
27
+ let e = big(exp);
28
+ let result = 1n;
29
+ while (e > 0n) {
30
+ if (e & 1n) result = (result * b) % m;
31
+ b = (b * b) % m;
32
+ e >>= 1n;
33
+ }
34
+ return result;
35
+ }
36
+
37
+ /** Extended Euclid: returns `{ g, x, y }` with `a·x + b·y = g = gcd(a, b)`. */
38
+ static extendedGcd(a: bigint | number, b: bigint | number): { g: bigint; x: bigint; y: bigint } {
39
+ let [oldR, r] = [big(a), big(b)];
40
+ let [oldS, s] = [1n, 0n];
41
+ let [oldT, t] = [0n, 1n];
42
+ while (r !== 0n) {
43
+ const q = oldR / r;
44
+ [oldR, r] = [r, oldR - q * r];
45
+ [oldS, s] = [s, oldS - q * s];
46
+ [oldT, t] = [t, oldT - q * t];
47
+ }
48
+ // Negative inputs can leave oldR negative even though a*x + b*y = oldR still
49
+ // holds; flip all three signs so g matches gcd()'s non-negative convention.
50
+ if (oldR < 0n) return { g: -oldR, x: -oldS, y: -oldT };
51
+ return { g: oldR, x: oldS, y: oldT };
52
+ }
53
+
54
+ /** Modular inverse of `a` mod `m`, or `null` when `gcd(a, m) ≠ 1`. */
55
+ static modInverse(a: bigint | number, m: bigint | number): bigint | null {
56
+ const mod = big(m);
57
+ const { g, x } = NumberTheory.extendedGcd(((big(a) % mod) + mod) % mod, mod);
58
+ if (g !== 1n) return null;
59
+ return ((x % mod) + mod) % mod;
60
+ }
61
+
62
+ /** Euclidean remainder, always in `[0, m)` for `m > 0`. Throws for `m === 0`. */
63
+ static mod(a: bigint | number, m: bigint | number): bigint {
64
+ const modulus = big(m);
65
+ if (modulus === 0n) throw new RangeError("NumberTheory.mod: modulus must be non-zero");
66
+ return ((big(a) % modulus) + modulus) % modulus;
67
+ }
68
+
69
+ /** `gcd(a, b)` (non-negative, `gcd(0, 0) === 0`). */
70
+ static gcd(a: bigint | number, b: bigint | number): bigint {
71
+ return bgcd(big(a), big(b));
72
+ }
73
+
74
+ /** `lcm(a, b) = |a·b| / gcd(a, b)`, or `0` if either operand is `0`. */
75
+ static lcm(a: bigint | number, b: bigint | number): bigint {
76
+ const A = big(a);
77
+ const B = big(b);
78
+ if (A === 0n || B === 0n) return 0n;
79
+ return babs(A * B) / bgcd(A, B);
80
+ }
81
+
82
+ /** LCM of a list of values (`1` for an empty list). */
83
+ static lcmList(values: Array<bigint | number>): bigint {
84
+ return values.reduce((acc: bigint, v) => NumberTheory.lcm(acc, big(v)), 1n);
85
+ }
86
+
87
+ /**
88
+ * Solve a system of congruences `x ≡ remainders[i] (mod moduli[i])` for
89
+ * pairwise-coprime moduli. Returns `{ x, modulus }` or `null` if inconsistent.
90
+ */
91
+ static crt(
92
+ remainders: Array<bigint | number>,
93
+ moduli: Array<bigint | number>,
94
+ ): { x: bigint; modulus: bigint } | null {
95
+ let x = 0n;
96
+ let mod = 1n;
97
+ for (let i = 0; i < remainders.length; i++) {
98
+ const ri = ((big(remainders[i]) % big(moduli[i])) + big(moduli[i])) % big(moduli[i]);
99
+ const mi = big(moduli[i]);
100
+ const inv = NumberTheory.modInverse(mod, mi);
101
+ if (inv === null) return null;
102
+ const diff = ((((ri - x) * inv) % mi) + mi) % mi;
103
+ x += mod * diff;
104
+ mod *= mi;
105
+ x = ((x % mod) + mod) % mod;
106
+ }
107
+ return { x, modulus: mod };
108
+ }
109
+
110
+ /** Miller-Rabin primality test (deterministic for n < 3.3·10²⁴). */
111
+ static isProbablePrime(n: bigint | number): boolean {
112
+ const num = big(n);
113
+ if (num < 2n) return false;
114
+ for (const p of [2n, 3n, 5n, 7n, 11n, 13n, 17n, 19n, 23n, 29n, 31n, 37n]) {
115
+ if (num === p) return true;
116
+ if (num % p === 0n) return false;
117
+ }
118
+ let d = num - 1n;
119
+ let r = 0n;
120
+ while ((d & 1n) === 0n) {
121
+ d >>= 1n;
122
+ r++;
123
+ }
124
+ witness: for (const a of MR_WITNESSES) {
125
+ if (a >= num) continue;
126
+ let x = NumberTheory.modPow(a, d, num);
127
+ if (x === 1n || x === num - 1n) continue;
128
+ for (let i = 0n; i < r - 1n; i++) {
129
+ x = (x * x) % num;
130
+ if (x === num - 1n) continue witness;
131
+ }
132
+ return false;
133
+ }
134
+ return true;
135
+ }
136
+
137
+ /** The next prime strictly greater than `n`. */
138
+ static nextPrime(n: bigint | number): bigint {
139
+ let candidate = big(n) + 1n;
140
+ while (!NumberTheory.isProbablePrime(candidate)) candidate++;
141
+ return candidate;
142
+ }
143
+
144
+ /** A single non-trivial factor of composite `n` via Pollard's rho (Brent). */
145
+ static pollardRho(n: bigint | number): bigint {
146
+ const num = big(n);
147
+ // n <= 1 has no non-trivial factor to find: `f(v) = (v*v+c) % 1n` collapses
148
+ // to 0 for every c, so `d = gcd(0, 1) = 1n` forever and the loop below never
149
+ // terminates. Reject degenerate inputs up front instead of hanging.
150
+ if (num <= 1n) throw new RangeError("NumberTheory.pollardRho: n must be greater than 1");
151
+ if (num % 2n === 0n) return 2n;
152
+ for (let c = 1n; ; c++) {
153
+ let x = 2n;
154
+ let y = 2n;
155
+ let d = 1n;
156
+ const f = (v: bigint) => (v * v + c) % num;
157
+ while (d === 1n) {
158
+ x = f(x);
159
+ y = f(f(y));
160
+ d = bgcd(babs(x - y), num);
161
+ }
162
+ if (d !== num) return d;
163
+ }
164
+ }
165
+
166
+ /** Full prime factorisation as sorted `[prime, exponent]` pairs. */
167
+ static factorize(n: bigint | number): Array<[bigint, number]> {
168
+ let num = babs(big(n));
169
+ // 0 has no well-defined prime factorization (every prime divides it, with
170
+ // no finite exponent) -- unlike 1, whose factorization is the empty
171
+ // product. Without this guard `0n % p === 0n` and `0n / p === 0n` forever,
172
+ // so the trial-division loop below never terminates.
173
+ if (num === 0n) throw new RangeError("NumberTheory.factorize: n must be non-zero");
174
+ const counts = new Map<bigint, number>();
175
+ const add = (p: bigint) => counts.set(p, (counts.get(p) ?? 0) + 1);
176
+
177
+ for (const p of [2n, 3n, 5n, 7n, 11n, 13n]) {
178
+ while (num % p === 0n) {
179
+ add(p);
180
+ num /= p;
181
+ }
182
+ }
183
+ const stack = num > 1n ? [num] : [];
184
+ while (stack.length) {
185
+ const m = stack.pop() as bigint;
186
+ if (m === 1n) continue;
187
+ if (NumberTheory.isProbablePrime(m)) {
188
+ add(m);
189
+ continue;
190
+ }
191
+ const factor = NumberTheory.pollardRho(m);
192
+ stack.push(factor, m / factor);
193
+ }
194
+ return [...counts.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
195
+ }
196
+
197
+ /** Euler's totient φ(n) via factorisation (big-integer safe). */
198
+ static eulerPhi(n: bigint | number): bigint {
199
+ let result = babs(big(n));
200
+ for (const [p] of NumberTheory.factorize(n)) result = (result / p) * (p - 1n);
201
+ return result;
202
+ }
203
+
204
+ /** The Legendre symbol `(a/p)` for an odd prime `p` (−1, 0, or 1). */
205
+ static legendreSymbol(a: bigint | number, p: bigint | number): number {
206
+ const P = big(p);
207
+ const ls = NumberTheory.modPow(a, (P - 1n) / 2n, P);
208
+ return ls === P - 1n ? -1 : Number(ls);
209
+ }
210
+
211
+ /** The Jacobi symbol `(a/n)` for odd `n > 0` (−1, 0, or 1). */
212
+ static jacobiSymbol(a: bigint | number, n: bigint | number): number {
213
+ let A = ((big(a) % big(n)) + big(n)) % big(n);
214
+ let N = big(n);
215
+ let result = 1;
216
+ while (A !== 0n) {
217
+ while (A % 2n === 0n) {
218
+ A /= 2n;
219
+ const r = N % 8n;
220
+ if (r === 3n || r === 5n) result = -result;
221
+ }
222
+ [A, N] = [N, A];
223
+ if (A % 4n === 3n && N % 4n === 3n) result = -result;
224
+ A %= N;
225
+ }
226
+ return N === 1n ? result : 0;
227
+ }
228
+ }
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Numerical — numerical-methods toolkit: root finding (bisection, secant,
3
+ * Newton, Brent), quadrature (composite trapezoid/Simpson, adaptive Simpson,
4
+ * Gauss-Legendre), ODE integration (Euler, RK4 for systems), and nonlinear
5
+ * curve fitting (Levenberg-Marquardt). Complements the simpler
6
+ * `integrateN`/`differentiateN`/`solveN` in {@link RealMath}.
7
+ */
8
+
9
+ import { MatrixMath } from "./MatrixMath.ts";
10
+
11
+ type Fn = (x: number) => number;
12
+ /** A first-order ODE system `dy/dt = f(t, y)`. */
13
+ type ODE = (t: number, y: number[]) => number[];
14
+ /** A parametric model `y = model(x, params)`, e.g. `(x, [a, b]) => a * Math.exp(b * x)`. */
15
+ type Model = (x: number, params: number[]) => number;
16
+
17
+ // 5-point Gauss-Legendre nodes/weights on [-1, 1].
18
+ const GL5_NODES = [0, -0.5384693101056831, 0.5384693101056831, -0.906179845938664, 0.906179845938664];
19
+ const GL5_WEIGHTS = [
20
+ 0.5688888888888889, 0.4786286704993665, 0.4786286704993665, 0.2369268850561891, 0.2369268850561891,
21
+ ];
22
+
23
+ export interface ODEStep {
24
+ t: number;
25
+ y: number[];
26
+ }
27
+
28
+ export interface LevenbergMarquardtResult {
29
+ params: number[];
30
+ /** sqrt of the sum of squared residuals at `params`. */
31
+ residualNorm: number;
32
+ iterations: number;
33
+ converged: boolean;
34
+ }
35
+
36
+ export class Numerical {
37
+ // -- root finding --------------------------------------------------------
38
+
39
+ /** Find a root of `f` in `[a, b]` by bisection (requires a sign change). */
40
+ static bisection(f: Fn, a: number, b: number, tolerance = 1e-12, maxIterations = 200): number {
41
+ let lo = a;
42
+ let hi = b;
43
+ let flo = f(lo);
44
+ if (flo * f(hi) > 0) throw new Error("bisection requires f(a) and f(b) to have opposite signs.");
45
+ for (let i = 0; i < maxIterations; i++) {
46
+ const mid = (lo + hi) / 2;
47
+ const fmid = f(mid);
48
+ if (Math.abs(fmid) < tolerance || (hi - lo) / 2 < tolerance) return mid;
49
+ if (flo * fmid < 0) hi = mid;
50
+ else {
51
+ lo = mid;
52
+ flo = fmid;
53
+ }
54
+ }
55
+ return (lo + hi) / 2;
56
+ }
57
+
58
+ /** Find a root of `f` by the secant method from two starting guesses. */
59
+ static secant(f: Fn, x0: number, x1: number, tolerance = 1e-12, maxIterations = 200): number {
60
+ let a = x0;
61
+ let b = x1;
62
+ let fa = f(a);
63
+ let fb = f(b);
64
+ for (let i = 0; i < maxIterations; i++) {
65
+ if (Math.abs(fb) < tolerance) return b;
66
+ const denom = fb - fa;
67
+ if (denom === 0) break;
68
+ const c = b - (fb * (b - a)) / denom;
69
+ a = b;
70
+ fa = fb;
71
+ b = c;
72
+ fb = f(b);
73
+ }
74
+ return b;
75
+ }
76
+
77
+ /** Newton's method; if `df` is omitted, a numeric derivative is used. */
78
+ static newton(f: Fn, x0: number, df?: Fn, tolerance = 1e-12, maxIterations = 200): number {
79
+ let x = x0;
80
+ const derivative = df ?? ((t: number) => (f(t + 1e-7) - f(t - 1e-7)) / 2e-7);
81
+ for (let i = 0; i < maxIterations; i++) {
82
+ const fx = f(x);
83
+ if (Math.abs(fx) < tolerance) return x;
84
+ const d = derivative(x);
85
+ if (d === 0) break;
86
+ x -= fx / d;
87
+ }
88
+ return x;
89
+ }
90
+
91
+ /** Brent's method: robust bracketing root finder combining bisection and interpolation. */
92
+ static brent(f: Fn, a: number, b: number, tolerance = 1e-12, maxIterations = 200): number {
93
+ let fa = f(a);
94
+ let fb = f(b);
95
+ if (fa * fb > 0) throw new Error("brent requires f(a) and f(b) to have opposite signs.");
96
+ if (Math.abs(fa) < Math.abs(fb)) {
97
+ [a, b] = [b, a];
98
+ [fa, fb] = [fb, fa];
99
+ }
100
+ let c = a;
101
+ let fc = fa;
102
+ let d = b - a;
103
+ let mflag = true;
104
+ for (let i = 0; i < maxIterations; i++) {
105
+ if (Math.abs(fb) < tolerance || Math.abs(b - a) < tolerance) return b;
106
+ let s: number;
107
+ if (fa !== fc && fb !== fc) {
108
+ s =
109
+ (a * fb * fc) / ((fa - fb) * (fa - fc)) +
110
+ (b * fa * fc) / ((fb - fa) * (fb - fc)) +
111
+ (c * fa * fb) / ((fc - fa) * (fc - fb));
112
+ } else {
113
+ s = b - (fb * (b - a)) / (fb - fa);
114
+ }
115
+ const cond =
116
+ s < (3 * a + b) / 4 ||
117
+ s > b ||
118
+ (mflag && Math.abs(s - b) >= Math.abs(b - c) / 2) ||
119
+ (!mflag && Math.abs(s - b) >= Math.abs(c - d) / 2);
120
+ if (cond) {
121
+ s = (a + b) / 2;
122
+ mflag = true;
123
+ } else {
124
+ mflag = false;
125
+ }
126
+ const fs = f(s);
127
+ d = c;
128
+ c = b;
129
+ fc = fb;
130
+ if (fa * fs < 0) {
131
+ b = s;
132
+ fb = fs;
133
+ } else {
134
+ a = s;
135
+ fa = fs;
136
+ }
137
+ if (Math.abs(fa) < Math.abs(fb)) {
138
+ [a, b] = [b, a];
139
+ [fa, fb] = [fb, fa];
140
+ }
141
+ }
142
+ return b;
143
+ }
144
+
145
+ // -- quadrature ----------------------------------------------------------
146
+
147
+ /** Composite trapezoidal rule with `n` subintervals. */
148
+ static trapezoid(f: Fn, a: number, b: number, n = 1000): number {
149
+ const h = (b - a) / n;
150
+ let sum = (f(a) + f(b)) / 2;
151
+ for (let i = 1; i < n; i++) sum += f(a + i * h);
152
+ return sum * h;
153
+ }
154
+
155
+ /** Composite Simpson's rule with `n` (rounded up to even) subintervals. */
156
+ static simpson(f: Fn, a: number, b: number, n = 1000): number {
157
+ const m = n % 2 === 0 ? n : n + 1;
158
+ const h = (b - a) / m;
159
+ let sum = f(a) + f(b);
160
+ for (let i = 1; i < m; i++) sum += (i % 2 === 0 ? 2 : 4) * f(a + i * h);
161
+ return (sum * h) / 3;
162
+ }
163
+
164
+ /** Adaptive Simpson quadrature to a target error tolerance. */
165
+ static adaptiveSimpson(f: Fn, a: number, b: number, tolerance = 1e-10, maxDepth = 50): number {
166
+ const simpson = (lo: number, hi: number, flo: number, fmid: number, fhi: number) =>
167
+ ((hi - lo) / 6) * (flo + 4 * fmid + fhi);
168
+ const recurse = (
169
+ lo: number,
170
+ hi: number,
171
+ flo: number,
172
+ fmid: number,
173
+ fhi: number,
174
+ whole: number,
175
+ tol: number,
176
+ depth: number,
177
+ ): number => {
178
+ const mid = (lo + hi) / 2;
179
+ const lmid = (lo + mid) / 2;
180
+ const rmid = (mid + hi) / 2;
181
+ const flmid = f(lmid);
182
+ const frmid = f(rmid);
183
+ const left = simpson(lo, mid, flo, flmid, fmid);
184
+ const right = simpson(mid, hi, fmid, frmid, fhi);
185
+ if (depth <= 0 || Math.abs(left + right - whole) <= 15 * tol) return left + right + (left + right - whole) / 15;
186
+ return (
187
+ recurse(lo, mid, flo, flmid, fmid, left, tol / 2, depth - 1) +
188
+ recurse(mid, hi, fmid, frmid, fhi, right, tol / 2, depth - 1)
189
+ );
190
+ };
191
+ const mid = (a + b) / 2;
192
+ const fa = f(a);
193
+ const fmid = f(mid);
194
+ const fb = f(b);
195
+ return recurse(a, b, fa, fmid, fb, simpson(a, b, fa, fmid, fb), tolerance, maxDepth);
196
+ }
197
+
198
+ /** 5-point Gauss-Legendre quadrature on `[a, b]`. */
199
+ static gaussLegendre(f: Fn, a: number, b: number): number {
200
+ const half = (b - a) / 2;
201
+ const mid = (a + b) / 2;
202
+ let sum = 0;
203
+ for (let i = 0; i < GL5_NODES.length; i++) sum += GL5_WEIGHTS[i] * f(mid + half * GL5_NODES[i]);
204
+ return sum * half;
205
+ }
206
+
207
+ // -- ODE integration -----------------------------------------------------
208
+
209
+ /** Explicit Euler integration of a first-order system from `t0` to `t1`. */
210
+ static euler(f: ODE, y0: number[], t0: number, t1: number, h = 0.01): ODEStep[] {
211
+ const steps: ODEStep[] = [{ t: t0, y: [...y0] }];
212
+ let t = t0;
213
+ let y = [...y0];
214
+ while (t < t1 - 1e-12) {
215
+ const step = Math.min(h, t1 - t);
216
+ const dy = f(t, y);
217
+ y = y.map((yi, i) => yi + step * dy[i]);
218
+ t += step;
219
+ steps.push({ t, y: [...y] });
220
+ }
221
+ return steps;
222
+ }
223
+
224
+ /** Classical fourth-order Runge-Kutta integration of a first-order system. */
225
+ static rk4(f: ODE, y0: number[], t0: number, t1: number, h = 0.01): ODEStep[] {
226
+ const steps: ODEStep[] = [{ t: t0, y: [...y0] }];
227
+ let t = t0;
228
+ let y = [...y0];
229
+ while (t < t1 - 1e-12) {
230
+ const step = Math.min(h, t1 - t);
231
+ const k1 = f(t, y);
232
+ const k2 = f(
233
+ t + step / 2,
234
+ y.map((yi, i) => yi + (step / 2) * k1[i]),
235
+ );
236
+ const k3 = f(
237
+ t + step / 2,
238
+ y.map((yi, i) => yi + (step / 2) * k2[i]),
239
+ );
240
+ const k4 = f(
241
+ t + step,
242
+ y.map((yi, i) => yi + step * k3[i]),
243
+ );
244
+ y = y.map((yi, i) => yi + (step / 6) * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]));
245
+ t += step;
246
+ steps.push({ t, y: [...y] });
247
+ }
248
+ return steps;
249
+ }
250
+
251
+ // -- curve fitting ---------------------------------------------------------
252
+
253
+ /**
254
+ * Nonlinear least-squares curve fitting via Levenberg-Marquardt: finds
255
+ * `params` minimizing `sum((model(xs[i], params) - ys[i])^2)`, starting
256
+ * from `params0`. The Jacobian of `model` with respect to `params` is
257
+ * estimated by central finite differences (generalizes `newton`'s
258
+ * optional-df-else-numeric-derivative convention to a vector of
259
+ * parameters). `lambda` is the Marquardt damping factor -- scaled against
260
+ * each parameter's own row of `JᵀJ` (not a plain identity, the original
261
+ * Levenberg damping) so differently-scaled parameters damp comparably --
262
+ * grown 10x on a rejected step (safer, more like gradient descent) and
263
+ * shrunk 10x on an accepted one (faster, more like Gauss-Newton near the
264
+ * optimum).
265
+ *
266
+ * `converged` is true once *either* the sum of squared residuals itself
267
+ * drops below `tolerance` (a near-exact fit) *or* at least one step has
268
+ * improved it and further steps stop improving it meaningfully (no
269
+ * damped step improves it at all, or the relative improvement has
270
+ * dropped below 1e-12) -- deliberately not "the residual itself is near
271
+ * zero" as the only success criterion, since real (noisy) data's true
272
+ * best fit essentially never drives the residual anywhere near
273
+ * `tolerance`, even when the fit has genuinely converged to the best
274
+ * achievable parameters; requiring near-zero residual would wrongly
275
+ * report `converged: false` for every realistic (as opposed to
276
+ * synthetic/noiseless) dataset.
277
+ *
278
+ * NON-GOALS: no bounds/constraints on `params`; only ever finds one local
279
+ * minimum near `params0`, the same "no guarantee of the global optimum"
280
+ * caveat as `newton`. Returns `converged: false` (with the best `params`
281
+ * found, not a throw) when no step ever improved on `params0` at all --
282
+ * a model/data pairing that can't be fit usefully isn't a numerical error
283
+ * the way a divergent root-find is, but is still worth flagging distinctly
284
+ * from "converged to some local optimum, however good or mediocre."
285
+ */
286
+ static levenbergMarquardt(
287
+ model: Model,
288
+ params0: number[],
289
+ xs: number[],
290
+ ys: number[],
291
+ options: { lambda0?: number; tolerance?: number; maxIterations?: number } = {},
292
+ ): LevenbergMarquardtResult {
293
+ const { lambda0 = 1e-2, tolerance = 1e-10, maxIterations = 200 } = options;
294
+ const n = params0.length;
295
+ const m = xs.length;
296
+ let params = [...params0];
297
+ let lambda = lambda0;
298
+
299
+ function residuals(p: number[]): number[] {
300
+ return xs.map((x, i) => model(x, p) - ys[i]);
301
+ }
302
+ function sumSq(r: number[]): number {
303
+ return r.reduce((s, v) => s + v * v, 0);
304
+ }
305
+ function jacobian(p: number[]): number[][] {
306
+ const J: number[][] = Array.from({ length: m }, () => new Array(n).fill(0));
307
+ for (let j = 0; j < n; j++) {
308
+ const step = 1e-6 * (Math.abs(p[j]) + 1e-6);
309
+ const pPlus = [...p];
310
+ const pMinus = [...p];
311
+ pPlus[j] += step;
312
+ pMinus[j] -= step;
313
+ const rPlus = residuals(pPlus);
314
+ const rMinus = residuals(pMinus);
315
+ for (let i = 0; i < m; i++) J[i][j] = (rPlus[i] - rMinus[i]) / (2 * step);
316
+ }
317
+ return J;
318
+ }
319
+
320
+ let r = residuals(params);
321
+ let currentSumSq = sumSq(r);
322
+ let iterations = 0;
323
+ let everImproved = false;
324
+
325
+ for (; iterations < maxIterations; iterations++) {
326
+ if (currentSumSq < tolerance) break; // a near-exact fit -- stop early regardless of real data's usual nonzero floor below
327
+ const J = jacobian(params);
328
+ const JtJ: number[][] = Array.from({ length: n }, () => new Array(n).fill(0));
329
+ const Jtr: number[] = new Array(n).fill(0);
330
+ for (let a = 0; a < n; a++) {
331
+ for (let b = 0; b < n; b++) {
332
+ let s = 0;
333
+ for (let i = 0; i < m; i++) s += (J[i] as number[])[a] * (J[i] as number[])[b];
334
+ (JtJ[a] as number[])[b] = s;
335
+ }
336
+ let s2 = 0;
337
+ for (let i = 0; i < m; i++) s2 += (J[i] as number[])[a] * r[i];
338
+ Jtr[a] = s2;
339
+ }
340
+
341
+ const prevSumSq = currentSumSq;
342
+ let accepted = false;
343
+ for (let tries = 0; tries < 30 && !accepted; tries++) {
344
+ const A = JtJ.map((row, a) => row.map((v, b) => v + (a === b ? lambda * (JtJ[a] as number[])[a] : 0)));
345
+ let delta: number[];
346
+ try {
347
+ delta = [
348
+ ...MatrixMath.solve(
349
+ A,
350
+ Jtr.map((v) => -v),
351
+ ),
352
+ ];
353
+ } catch {
354
+ lambda *= 10;
355
+ continue;
356
+ }
357
+ const candidate = params.map((p, i) => p + (delta[i] as number));
358
+ const rCandidate = residuals(candidate);
359
+ const candidateSumSq = sumSq(rCandidate);
360
+ if (Number.isFinite(candidateSumSq) && candidateSumSq < currentSumSq) {
361
+ params = candidate;
362
+ r = rCandidate;
363
+ currentSumSq = candidateSumSq;
364
+ lambda = Math.max(lambda / 10, 1e-12);
365
+ accepted = true;
366
+ everImproved = true;
367
+ } else {
368
+ lambda *= 10;
369
+ }
370
+ }
371
+ // Either terminating condition below is a real local minimum, not a
372
+ // failure: no damped step improves further (`!accepted`), or the ones
373
+ // that do are only shaving off numerically-insignificant amounts
374
+ // (`relativeImprovement` near zero) -- real (noisy) data's best fit
375
+ // essentially never drives `currentSumSq` itself near zero the way
376
+ // `tolerance` alone would require, so treating only *that* as success
377
+ // would wrongly call every noisy-data fit unconverged.
378
+ if (!accepted) break;
379
+ const relativeImprovement = (prevSumSq - currentSumSq) / Math.max(1, prevSumSq);
380
+ if (relativeImprovement < 1e-12) break;
381
+ }
382
+
383
+ return {
384
+ params,
385
+ residualNorm: Math.sqrt(currentSumSq),
386
+ iterations,
387
+ converged: everImproved || currentSumSq < tolerance,
388
+ };
389
+ }
390
+ }