@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.
- package/README.md +165 -0
- package/dist/Bivector4.d.ts +52 -0
- package/dist/Bivector4.d.ts.map +1 -0
- package/dist/Bivector4.js +105 -0
- package/dist/Bivector4.js.map +1 -0
- package/dist/CellGraph.d.ts +259 -0
- package/dist/CellGraph.d.ts.map +1 -0
- package/dist/CellGraph.js +637 -0
- package/dist/CellGraph.js.map +1 -0
- package/dist/Clifford4Internal.d.ts +35 -0
- package/dist/Clifford4Internal.d.ts.map +1 -0
- package/dist/Clifford4Internal.js +86 -0
- package/dist/Clifford4Internal.js.map +1 -0
- package/dist/Combinatorics.d.ts +61 -0
- package/dist/Combinatorics.d.ts.map +1 -0
- package/dist/Combinatorics.js +186 -0
- package/dist/Combinatorics.js.map +1 -0
- package/dist/CombinatoricsGenerators.d.ts +44 -0
- package/dist/CombinatoricsGenerators.d.ts.map +1 -0
- package/dist/CombinatoricsGenerators.js +148 -0
- package/dist/CombinatoricsGenerators.js.map +1 -0
- package/dist/ComplexMath.d.ts +46 -0
- package/dist/ComplexMath.d.ts.map +1 -0
- package/dist/ComplexMath.js +98 -0
- package/dist/ComplexMath.js.map +1 -0
- package/dist/ComplexNumber.d.ts +174 -0
- package/dist/ComplexNumber.d.ts.map +1 -0
- package/dist/ComplexNumber.js +443 -0
- package/dist/ComplexNumber.js.map +1 -0
- package/dist/Cycle.d.ts +42 -0
- package/dist/Cycle.d.ts.map +1 -0
- package/dist/Cycle.js +120 -0
- package/dist/Cycle.js.map +1 -0
- package/dist/Decimal.d.ts +45 -0
- package/dist/Decimal.d.ts.map +1 -0
- package/dist/Decimal.js +135 -0
- package/dist/Decimal.js.map +1 -0
- package/dist/Distributions.d.ts +55 -0
- package/dist/Distributions.d.ts.map +1 -0
- package/dist/Distributions.js +222 -0
- package/dist/Distributions.js.map +1 -0
- package/dist/DualNumber.d.ts +42 -0
- package/dist/DualNumber.d.ts.map +1 -0
- package/dist/DualNumber.js +91 -0
- package/dist/DualNumber.js.map +1 -0
- package/dist/Environment.d.ts +44 -0
- package/dist/Environment.d.ts.map +1 -0
- package/dist/Environment.js +129 -0
- package/dist/Environment.js.map +1 -0
- package/dist/Expression.d.ts +19 -0
- package/dist/Expression.d.ts.map +1 -0
- package/dist/Expression.js +43 -0
- package/dist/Expression.js.map +1 -0
- package/dist/FFT.d.ts +21 -0
- package/dist/FFT.d.ts.map +1 -0
- package/dist/FFT.js +136 -0
- package/dist/FFT.js.map +1 -0
- package/dist/GeometricAlgebra4.d.ts +73 -0
- package/dist/GeometricAlgebra4.d.ts.map +1 -0
- package/dist/GeometricAlgebra4.js +115 -0
- package/dist/GeometricAlgebra4.js.map +1 -0
- package/dist/Geometry.d.ts +41 -0
- package/dist/Geometry.d.ts.map +1 -0
- package/dist/Geometry.js +167 -0
- package/dist/Geometry.js.map +1 -0
- package/dist/Graph.d.ts +72 -0
- package/dist/Graph.d.ts.map +1 -0
- package/dist/Graph.js +337 -0
- package/dist/Graph.js.map +1 -0
- package/dist/Graph3DUtils.d.ts +80 -0
- package/dist/Graph3DUtils.d.ts.map +1 -0
- package/dist/Graph3DUtils.js +156 -0
- package/dist/Graph3DUtils.js.map +1 -0
- package/dist/GraphUtils.d.ts +67 -0
- package/dist/GraphUtils.d.ts.map +1 -0
- package/dist/GraphUtils.js +72 -0
- package/dist/GraphUtils.js.map +1 -0
- package/dist/GroupTheory.d.ts +73 -0
- package/dist/GroupTheory.d.ts.map +1 -0
- package/dist/GroupTheory.js +194 -0
- package/dist/GroupTheory.js.map +1 -0
- package/dist/IntUtils.d.ts +33 -0
- package/dist/IntUtils.d.ts.map +1 -0
- package/dist/IntUtils.js +185 -0
- package/dist/IntUtils.js.map +1 -0
- package/dist/Interval.d.ts +44 -0
- package/dist/Interval.d.ts.map +1 -0
- package/dist/Interval.js +165 -0
- package/dist/Interval.js.map +1 -0
- package/dist/Lattice.d.ts +48 -0
- package/dist/Lattice.d.ts.map +1 -0
- package/dist/Lattice.js +65 -0
- package/dist/Lattice.js.map +1 -0
- package/dist/Logic.d.ts +22 -0
- package/dist/Logic.d.ts.map +1 -0
- package/dist/Logic.js +34 -0
- package/dist/Logic.js.map +1 -0
- package/dist/MatrixMath.d.ts +71 -0
- package/dist/MatrixMath.d.ts.map +1 -0
- package/dist/MatrixMath.js +302 -0
- package/dist/MatrixMath.js.map +1 -0
- package/dist/NumberTheory.d.ts +49 -0
- package/dist/NumberTheory.d.ts.map +1 -0
- package/dist/NumberTheory.js +230 -0
- package/dist/NumberTheory.js.map +1 -0
- package/dist/Numerical.d.ts +85 -0
- package/dist/Numerical.d.ts.map +1 -0
- package/dist/Numerical.js +343 -0
- package/dist/Numerical.js.map +1 -0
- package/dist/Permutation.d.ts +43 -0
- package/dist/Permutation.d.ts.map +1 -0
- package/dist/Permutation.js +142 -0
- package/dist/Permutation.js.map +1 -0
- package/dist/Polygon.d.ts +64 -0
- package/dist/Polygon.d.ts.map +1 -0
- package/dist/Polygon.js +221 -0
- package/dist/Polygon.js.map +1 -0
- package/dist/PolynomialRing.d.ts +74 -0
- package/dist/PolynomialRing.d.ts.map +1 -0
- package/dist/PolynomialRing.js +231 -0
- package/dist/PolynomialRing.js.map +1 -0
- package/dist/Quaternion.d.ts +37 -0
- package/dist/Quaternion.d.ts.map +1 -0
- package/dist/Quaternion.js +106 -0
- package/dist/Quaternion.js.map +1 -0
- package/dist/Rational.d.ts +34 -0
- package/dist/Rational.d.ts.map +1 -0
- package/dist/Rational.js +136 -0
- package/dist/Rational.js.map +1 -0
- package/dist/RealMath.d.ts +112 -0
- package/dist/RealMath.d.ts.map +1 -0
- package/dist/RealMath.js +262 -0
- package/dist/RealMath.js.map +1 -0
- package/dist/Rotor4.d.ts +180 -0
- package/dist/Rotor4.d.ts.map +1 -0
- package/dist/Rotor4.js +388 -0
- package/dist/Rotor4.js.map +1 -0
- package/dist/SpecialFunctions.d.ts +32 -0
- package/dist/SpecialFunctions.d.ts.map +1 -0
- package/dist/SpecialFunctions.js +161 -0
- package/dist/SpecialFunctions.js.map +1 -0
- package/dist/SpecialOperator.d.ts +21 -0
- package/dist/SpecialOperator.d.ts.map +1 -0
- package/dist/SpecialOperator.js +26 -0
- package/dist/SpecialOperator.js.map +1 -0
- package/dist/Statistics.d.ts +50 -0
- package/dist/Statistics.d.ts.map +1 -0
- package/dist/Statistics.js +131 -0
- package/dist/Statistics.js.map +1 -0
- package/dist/StringEvaluator.d.ts +48 -0
- package/dist/StringEvaluator.d.ts.map +1 -0
- package/dist/StringEvaluator.js +190 -0
- package/dist/StringEvaluator.js.map +1 -0
- package/dist/Structure.d.ts +130 -0
- package/dist/Structure.d.ts.map +1 -0
- package/dist/Structure.js +394 -0
- package/dist/Structure.js.map +1 -0
- package/dist/Symbolic.d.ts +501 -0
- package/dist/Symbolic.d.ts.map +1 -0
- package/dist/Symbolic.js +4077 -0
- package/dist/Symbolic.js.map +1 -0
- package/dist/Type.d.ts +31 -0
- package/dist/Type.d.ts.map +1 -0
- package/dist/Type.js +47 -0
- package/dist/Type.js.map +1 -0
- package/dist/Utilities.d.ts +27 -0
- package/dist/Utilities.d.ts.map +1 -0
- package/dist/Utilities.js +47 -0
- package/dist/Utilities.js.map +1 -0
- package/dist/Vec4.d.ts +39 -0
- package/dist/Vec4.d.ts.map +1 -0
- package/dist/Vec4.js +82 -0
- package/dist/Vec4.js.map +1 -0
- package/dist/Vector.d.ts +83 -0
- package/dist/Vector.d.ts.map +1 -0
- package/dist/Vector.js +173 -0
- package/dist/Vector.js.map +1 -0
- package/dist/VectorCalculus.d.ts +42 -0
- package/dist/VectorCalculus.d.ts.map +1 -0
- package/dist/VectorCalculus.js +82 -0
- package/dist/VectorCalculus.js.map +1 -0
- package/dist/VectorUtils.d.ts +172 -0
- package/dist/VectorUtils.d.ts.map +1 -0
- package/dist/VectorUtils.js +706 -0
- package/dist/VectorUtils.js.map +1 -0
- package/dist/differentiation-rules.d.ts +21 -0
- package/dist/differentiation-rules.d.ts.map +1 -0
- package/dist/differentiation-rules.js +52 -0
- package/dist/differentiation-rules.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
- package/src/Bivector4.ts +136 -0
- package/src/CellGraph.ts +669 -0
- package/src/Clifford4Internal.ts +90 -0
- package/src/Combinatorics.ts +183 -0
- package/src/CombinatoricsGenerators.ts +144 -0
- package/src/ComplexMath.ts +162 -0
- package/src/ComplexNumber.ts +491 -0
- package/src/Cycle.ts +126 -0
- package/src/Decimal.ts +145 -0
- package/src/Distributions.ts +269 -0
- package/src/DualNumber.ts +105 -0
- package/src/Environment.ts +127 -0
- package/src/Expression.ts +48 -0
- package/src/FFT.ts +146 -0
- package/src/GeometricAlgebra4.ts +157 -0
- package/src/Geometry.ts +173 -0
- package/src/Graph.ts +337 -0
- package/src/Graph3DUtils.ts +269 -0
- package/src/GraphUtils.ts +159 -0
- package/src/GroupTheory.ts +214 -0
- package/src/IntUtils.ts +200 -0
- package/src/Interval.ts +171 -0
- package/src/Lattice.ts +87 -0
- package/src/Logic.ts +38 -0
- package/src/MatrixMath.ts +326 -0
- package/src/NumberTheory.ts +228 -0
- package/src/Numerical.ts +390 -0
- package/src/Permutation.ts +148 -0
- package/src/Polygon.ts +229 -0
- package/src/PolynomialRing.ts +242 -0
- package/src/Quaternion.ts +125 -0
- package/src/Rational.ts +148 -0
- package/src/RealMath.ts +346 -0
- package/src/Rotor4.ts +440 -0
- package/src/SpecialFunctions.ts +152 -0
- package/src/SpecialOperator.ts +28 -0
- package/src/Statistics.ts +170 -0
- package/src/StringEvaluator.ts +208 -0
- package/src/Structure.ts +481 -0
- package/src/Symbolic.ts +4313 -0
- package/src/Type.ts +48 -0
- package/src/Utilities.ts +49 -0
- package/src/Vec4.ts +98 -0
- package/src/Vector.ts +189 -0
- package/src/VectorCalculus.ts +87 -0
- package/src/VectorUtils.ts +797 -0
- package/src/differentiation-rules.ts +73 -0
- 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
|
+
}
|
package/src/Numerical.ts
ADDED
|
@@ -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
|
+
}
|