@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
package/dist/Symbolic.js
ADDED
|
@@ -0,0 +1,4077 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbolic — a small computer-algebra engine: an expression AST with parsing,
|
|
3
|
+
* symbolic differentiation, algebraic simplification, basic symbolic
|
|
4
|
+
* integration, Taylor expansion, polynomial equation solving/factoring,
|
|
5
|
+
* limits, LaTeX rendering, and numeric evaluation.
|
|
6
|
+
*/
|
|
7
|
+
import { FUNC_DERIVATIVE_RULES } from "./differentiation-rules.js";
|
|
8
|
+
import { MatrixMath } from "./MatrixMath.js";
|
|
9
|
+
import { Numerical } from "./Numerical.js";
|
|
10
|
+
import { Rational } from "./Rational.js";
|
|
11
|
+
import { SpecialFunctions } from "./SpecialFunctions.js";
|
|
12
|
+
const FUNCS = [
|
|
13
|
+
"sin",
|
|
14
|
+
"cos",
|
|
15
|
+
"tan",
|
|
16
|
+
"exp",
|
|
17
|
+
"ln",
|
|
18
|
+
"sqrt",
|
|
19
|
+
"asin",
|
|
20
|
+
"acos",
|
|
21
|
+
"atan",
|
|
22
|
+
"sinh",
|
|
23
|
+
"cosh",
|
|
24
|
+
"tanh",
|
|
25
|
+
"cot",
|
|
26
|
+
"sec",
|
|
27
|
+
"csc",
|
|
28
|
+
"asinh",
|
|
29
|
+
"acosh",
|
|
30
|
+
"atanh",
|
|
31
|
+
"coth",
|
|
32
|
+
"sech",
|
|
33
|
+
"csch",
|
|
34
|
+
"acot",
|
|
35
|
+
"asec",
|
|
36
|
+
"acsc",
|
|
37
|
+
"acoth",
|
|
38
|
+
"asech",
|
|
39
|
+
"acsch",
|
|
40
|
+
"abs",
|
|
41
|
+
"log10",
|
|
42
|
+
"log2",
|
|
43
|
+
"cbrt",
|
|
44
|
+
"floor",
|
|
45
|
+
"ceil",
|
|
46
|
+
"round",
|
|
47
|
+
"sign",
|
|
48
|
+
"trunc",
|
|
49
|
+
"expm1",
|
|
50
|
+
"log1p",
|
|
51
|
+
"sigmoid",
|
|
52
|
+
"erf",
|
|
53
|
+
"relu",
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* `BinaryFuncName`s recognized by {@link Symbolic.parse}. `hypot`/`min`/`max`/
|
|
57
|
+
* `gcd`/`lcm` accept N ≥ 2 user-facing arguments, pairwise-folded into nested
|
|
58
|
+
* `call2` nodes at parse time; `atan2` takes exactly 2. `log(base, x)` and
|
|
59
|
+
* `clamp(x, lo, hi)` are also multi-arg but desugar entirely into existing
|
|
60
|
+
* constructs at parse time rather than becoming `call2` nodes.
|
|
61
|
+
*/
|
|
62
|
+
const BINARY_FUNCS = ["atan2", "hypot", "min", "max", "gcd", "lcm"];
|
|
63
|
+
/**
|
|
64
|
+
* Alternate plain-text spellings accepted by {@link Symbolic.parse}, mapped to
|
|
65
|
+
* their canonical `FuncName` — e.g. `arcsin(x)` parses identically to
|
|
66
|
+
* `asin(x)`.
|
|
67
|
+
*/
|
|
68
|
+
const FUNC_ALIASES = {
|
|
69
|
+
arcsin: "asin",
|
|
70
|
+
arccos: "acos",
|
|
71
|
+
arctan: "atan",
|
|
72
|
+
arcsinh: "asinh",
|
|
73
|
+
arccosh: "acosh",
|
|
74
|
+
arctanh: "atanh",
|
|
75
|
+
arccot: "acot",
|
|
76
|
+
arcsec: "asec",
|
|
77
|
+
arccsc: "acsc",
|
|
78
|
+
arccoth: "acoth",
|
|
79
|
+
arcsech: "asech",
|
|
80
|
+
arccsch: "acsch",
|
|
81
|
+
sgn: "sign",
|
|
82
|
+
logistic: "sigmoid",
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Every function name {@link Symbolic.parse} recognizes — canonical spellings
|
|
86
|
+
* plus alternate ones like `arcsin` — for consumers (e.g. a UI's implicit-
|
|
87
|
+
* multiplication preprocessor) that need to know what counts as a known
|
|
88
|
+
* identifier without hand-duplicating this list.
|
|
89
|
+
*/
|
|
90
|
+
export const FUNCTION_NAMES = [
|
|
91
|
+
...FUNCS,
|
|
92
|
+
...Object.keys(FUNC_ALIASES),
|
|
93
|
+
...BINARY_FUNCS,
|
|
94
|
+
"log",
|
|
95
|
+
"clamp",
|
|
96
|
+
"piecewise",
|
|
97
|
+
"sum",
|
|
98
|
+
"product",
|
|
99
|
+
];
|
|
100
|
+
// -- constructors -----------------------------------------------------------
|
|
101
|
+
const num = (value) => ({ type: "const", value });
|
|
102
|
+
const v = (name) => ({ type: "var", name });
|
|
103
|
+
const add = (left, right) => ({ type: "add", left, right });
|
|
104
|
+
const sub = (left, right) => ({ type: "sub", left, right });
|
|
105
|
+
const mul = (left, right) => ({ type: "mul", left, right });
|
|
106
|
+
const div = (left, right) => ({ type: "div", left, right });
|
|
107
|
+
const pow = (base, exp) => ({ type: "pow", base, exp });
|
|
108
|
+
const neg = (arg) => ({ type: "neg", arg });
|
|
109
|
+
const fn = (name, arg) => ({ type: "func", name, arg });
|
|
110
|
+
const call2 = (name, left, right) => ({ type: "call2", name, left, right });
|
|
111
|
+
const cmp = (op, left, right) => ({ type: "cmp", op, left, right });
|
|
112
|
+
const piecewise = (branches, otherwise) => ({
|
|
113
|
+
type: "piecewise",
|
|
114
|
+
branches,
|
|
115
|
+
otherwise,
|
|
116
|
+
});
|
|
117
|
+
const sumExpr = (variable, from, to, body) => ({
|
|
118
|
+
type: "sum",
|
|
119
|
+
variable,
|
|
120
|
+
from,
|
|
121
|
+
to,
|
|
122
|
+
body,
|
|
123
|
+
});
|
|
124
|
+
const productExpr = (variable, from, to, body) => ({
|
|
125
|
+
type: "product",
|
|
126
|
+
variable,
|
|
127
|
+
from,
|
|
128
|
+
to,
|
|
129
|
+
body,
|
|
130
|
+
});
|
|
131
|
+
const isConst = (e, value) => e.type === "const" && (value === undefined || e.value === value);
|
|
132
|
+
const containsVar = (e, name) => {
|
|
133
|
+
switch (e.type) {
|
|
134
|
+
case "const":
|
|
135
|
+
return false;
|
|
136
|
+
case "var":
|
|
137
|
+
return e.name === name;
|
|
138
|
+
case "neg":
|
|
139
|
+
return containsVar(e.arg, name);
|
|
140
|
+
case "func":
|
|
141
|
+
return containsVar(e.arg, name);
|
|
142
|
+
case "pow":
|
|
143
|
+
return containsVar(e.base, name) || containsVar(e.exp, name);
|
|
144
|
+
case "piecewise":
|
|
145
|
+
return (e.branches.some((br) => containsVar(br.cond, name) || containsVar(br.expr, name)) ||
|
|
146
|
+
containsVar(e.otherwise, name));
|
|
147
|
+
case "sum":
|
|
148
|
+
case "product":
|
|
149
|
+
return containsVar(e.from, name) || containsVar(e.to, name) || (e.variable !== name && containsVar(e.body, name));
|
|
150
|
+
default:
|
|
151
|
+
return containsVar(e.left, name) || containsVar(e.right, name);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
/** Collects every distinct `var` node name referenced anywhere in `e`. */
|
|
155
|
+
const collectFreeVars = (e, found = new Set()) => {
|
|
156
|
+
switch (e.type) {
|
|
157
|
+
case "const":
|
|
158
|
+
return found;
|
|
159
|
+
case "var":
|
|
160
|
+
found.add(e.name);
|
|
161
|
+
return found;
|
|
162
|
+
case "neg":
|
|
163
|
+
case "func":
|
|
164
|
+
collectFreeVars(e.arg, found);
|
|
165
|
+
return found;
|
|
166
|
+
case "pow":
|
|
167
|
+
collectFreeVars(e.base, found);
|
|
168
|
+
collectFreeVars(e.exp, found);
|
|
169
|
+
return found;
|
|
170
|
+
case "piecewise":
|
|
171
|
+
for (const br of e.branches) {
|
|
172
|
+
collectFreeVars(br.cond, found);
|
|
173
|
+
collectFreeVars(br.expr, found);
|
|
174
|
+
}
|
|
175
|
+
collectFreeVars(e.otherwise, found);
|
|
176
|
+
return found;
|
|
177
|
+
case "sum":
|
|
178
|
+
case "product": {
|
|
179
|
+
collectFreeVars(e.from, found);
|
|
180
|
+
collectFreeVars(e.to, found);
|
|
181
|
+
// e.variable is bound within e.body -- collect its free vars into a
|
|
182
|
+
// scratch set and merge everything except the bound name, so a
|
|
183
|
+
// same-named free variable elsewhere in `e` isn't shadowed out.
|
|
184
|
+
const bodyFound = collectFreeVars(e.body, new Set());
|
|
185
|
+
for (const n of bodyFound)
|
|
186
|
+
if (n !== e.variable)
|
|
187
|
+
found.add(n);
|
|
188
|
+
return found;
|
|
189
|
+
}
|
|
190
|
+
default:
|
|
191
|
+
collectFreeVars(e.left, found);
|
|
192
|
+
collectFreeVars(e.right, found);
|
|
193
|
+
return found;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
/** See {@link Symbolic.simplifyAssuming} -- the tree-walk applying each assumption's specific rewrite patterns. */
|
|
197
|
+
function applyAssumptions(e, assumptions) {
|
|
198
|
+
const isNonneg = (name) => assumptions[name] === "positive" || assumptions[name] === "nonnegative";
|
|
199
|
+
const isNonpos = (name) => assumptions[name] === "negative" || assumptions[name] === "nonpositive";
|
|
200
|
+
function walk(node) {
|
|
201
|
+
switch (node.type) {
|
|
202
|
+
case "const":
|
|
203
|
+
case "var":
|
|
204
|
+
return node;
|
|
205
|
+
case "neg":
|
|
206
|
+
return { type: "neg", arg: walk(node.arg) };
|
|
207
|
+
case "func": {
|
|
208
|
+
const arg = walk(node.arg);
|
|
209
|
+
if (node.name === "sqrt" && arg.type === "pow" && isConst(arg.exp, 2) && arg.base.type === "var") {
|
|
210
|
+
if (isNonneg(arg.base.name))
|
|
211
|
+
return arg.base;
|
|
212
|
+
if (isNonpos(arg.base.name))
|
|
213
|
+
return { type: "neg", arg: arg.base };
|
|
214
|
+
}
|
|
215
|
+
if (node.name === "abs" && arg.type === "var") {
|
|
216
|
+
if (isNonneg(arg.name))
|
|
217
|
+
return arg;
|
|
218
|
+
if (isNonpos(arg.name))
|
|
219
|
+
return { type: "neg", arg };
|
|
220
|
+
}
|
|
221
|
+
return { type: "func", name: node.name, arg };
|
|
222
|
+
}
|
|
223
|
+
case "pow":
|
|
224
|
+
return { type: "pow", base: walk(node.base), exp: walk(node.exp) };
|
|
225
|
+
case "piecewise":
|
|
226
|
+
return {
|
|
227
|
+
type: "piecewise",
|
|
228
|
+
branches: node.branches.map((br) => ({ cond: walk(br.cond), expr: walk(br.expr) })),
|
|
229
|
+
otherwise: walk(node.otherwise),
|
|
230
|
+
};
|
|
231
|
+
case "sum":
|
|
232
|
+
case "product":
|
|
233
|
+
return { ...node, from: walk(node.from), to: walk(node.to), body: walk(node.body) };
|
|
234
|
+
default:
|
|
235
|
+
return { ...node, left: walk(node.left), right: walk(node.right) };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return walk(e);
|
|
239
|
+
}
|
|
240
|
+
export class NotIntegrableError extends Error {
|
|
241
|
+
constructor(message = "This expression cannot be integrated by the elementary rules implemented.") {
|
|
242
|
+
super(message);
|
|
243
|
+
this.name = "NotIntegrableError";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
export class NonLinearSystemError extends Error {
|
|
247
|
+
constructor(message = "solveSystem only supports systems that are linear in the given variables.") {
|
|
248
|
+
super(message);
|
|
249
|
+
this.name = "NonLinearSystemError";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export class SingularSystemError extends Error {
|
|
253
|
+
constructor(message = "solveSystem: the system is singular (rank < number of variables) and has no unique solution.") {
|
|
254
|
+
super(message);
|
|
255
|
+
this.name = "SingularSystemError";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export class SeriesDivergesError extends Error {
|
|
259
|
+
constructor(message = "sumSeries: the series does not appear to converge (or converges too slowly to sum numerically).") {
|
|
260
|
+
super(message);
|
|
261
|
+
this.name = "SeriesDivergesError";
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export class UndeclaredVariableError extends Error {
|
|
265
|
+
names;
|
|
266
|
+
constructor(names) {
|
|
267
|
+
super(`Expression references variable${names.length === 1 ? "" : "s"} not in the declared set: ${names.join(", ")}`);
|
|
268
|
+
this.name = "UndeclaredVariableError";
|
|
269
|
+
this.names = names;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
export class SystemDidNotConvergeError extends Error {
|
|
273
|
+
constructor(message = "solveSystemNumeric: Newton's method did not converge from the given initial guess.") {
|
|
274
|
+
super(message);
|
|
275
|
+
this.name = "SystemDidNotConvergeError";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
export class IntegrationSingularityError extends Error {
|
|
279
|
+
constructor(message = "integrateDefinite: the integrand appears to have a singularity strictly inside the integration bounds.") {
|
|
280
|
+
super(message);
|
|
281
|
+
this.name = "IntegrationSingularityError";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* No longer thrown internally -- `differentiate` now handles `product` via
|
|
286
|
+
* the logarithmic-derivative identity (see `diff`/`diffTraced`'s `case
|
|
287
|
+
* "product":`). Kept exported for backward compatibility with any code
|
|
288
|
+
* that imports/catches it.
|
|
289
|
+
*/
|
|
290
|
+
export class ProductNotDifferentiableError extends Error {
|
|
291
|
+
constructor(message = 'differentiate: a "product" (Π) node has no mechanical differentiation rule -- it needs the general Leibniz/logarithmic-derivative rule, which isn\'t implemented.') {
|
|
292
|
+
super(message);
|
|
293
|
+
this.name = "ProductNotDifferentiableError";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
export class NotSeparableError extends Error {
|
|
297
|
+
constructor(message = "solveOdeClosedForm: dy/dx matches none of the five recognized closed-form methods (separable, linear first-order, homogeneous, Bernoulli, exact).") {
|
|
298
|
+
super(message);
|
|
299
|
+
this.name = "NotSeparableError";
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export class NoClosedFormError extends Error {
|
|
303
|
+
constructor(message = "solveOdeClosedForm: the equation matches a recognized closed-form method, but the antiderivative(s) it requires aren't in this engine's elementary-rule coverage.") {
|
|
304
|
+
super(message);
|
|
305
|
+
this.name = "NoClosedFormError";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
export class DegenerateOdeError extends Error {
|
|
309
|
+
constructor(message = "solveOde2ndOrderConstCoeff: a === 0 -- this isn't actually a second-order equation (it's first-order, or not an ODE at all in the caller's intended sense).") {
|
|
310
|
+
super(message);
|
|
311
|
+
this.name = "DegenerateOdeError";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Thrown by {@link Symbolic.solve} when `expr` is identically zero (e.g.
|
|
316
|
+
* `solve("0")`) -- every real number is a solution. Distinct from `[]`,
|
|
317
|
+
* which means "verified no real solution" (e.g. `x^2 + 1`).
|
|
318
|
+
*/
|
|
319
|
+
export class InfiniteSolutionsError extends Error {
|
|
320
|
+
constructor(message = "solve: the expression is identically zero -- every real number is a solution (infinitely many), not none.") {
|
|
321
|
+
super(message);
|
|
322
|
+
this.name = "InfiniteSolutionsError";
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
export class Symbolic {
|
|
326
|
+
/** Parse an expression string such as `"sin(x^2) + 3*x"` into an AST. */
|
|
327
|
+
static parse(input) {
|
|
328
|
+
return new Parser(input).parse();
|
|
329
|
+
}
|
|
330
|
+
static differentiate(expr, variable = "x") {
|
|
331
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
332
|
+
return Symbolic.simplify(diff(e, variable));
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Like {@link differentiate}, but also returns a bottom-up trace of every
|
|
336
|
+
* differentiation rule applied — one step per subexpression, innermost
|
|
337
|
+
* first — for a "show your work" UI. Each step's `input`/`output` are
|
|
338
|
+
* unsimplified, matching the mechanical rule application; `result` is the
|
|
339
|
+
* final simplified derivative (same as `differentiate` would return).
|
|
340
|
+
*/
|
|
341
|
+
static differentiateSteps(expr, variable = "x") {
|
|
342
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
343
|
+
const steps = [];
|
|
344
|
+
const raw = diffTraced(e, variable, steps);
|
|
345
|
+
return { steps, result: Symbolic.simplify(raw) };
|
|
346
|
+
}
|
|
347
|
+
static simplify(expr) {
|
|
348
|
+
let e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
349
|
+
for (let i = 0; i < 30; i++) {
|
|
350
|
+
const next = collectLikeTerms(simplifyOnce(e));
|
|
351
|
+
if (equal(next, e))
|
|
352
|
+
return next;
|
|
353
|
+
e = next;
|
|
354
|
+
}
|
|
355
|
+
return e;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Simplifies `expr` with additional domain knowledge about specific
|
|
359
|
+
* variables (e.g. "x is positive"), unlocking simplifications that aren't
|
|
360
|
+
* sound in general -- `sqrt(x^2) -> x` requires x >= 0 (otherwise the
|
|
361
|
+
* correct simplification is |x|), and `abs(x) -> x`/`-x` requires knowing
|
|
362
|
+
* x's sign. Runs ordinary `simplify` first, then a second tree-walk pass
|
|
363
|
+
* applying the specific patterns each assumption unlocks, then
|
|
364
|
+
* `simplify`s once more in case a rewrite exposes a further reduction
|
|
365
|
+
* (e.g. `sqrt(x^2)*2` -> `x*2` -> could combine with a like term
|
|
366
|
+
* elsewhere).
|
|
367
|
+
*
|
|
368
|
+
* NON-GOALS: a small, explicit pattern set (`sqrt(x^2)`, `abs(x)`), not a
|
|
369
|
+
* general constraint-propagation/theorem-proving simplifier; assumptions
|
|
370
|
+
* are a flat per-variable fact (`Assumption`), not compound/derived
|
|
371
|
+
* assumptions (e.g. "x+y is positive" isn't expressible).
|
|
372
|
+
*/
|
|
373
|
+
static simplifyAssuming(expr, assumptions) {
|
|
374
|
+
const simplified = Symbolic.simplify(expr);
|
|
375
|
+
const rewritten = applyAssumptions(simplified, assumptions);
|
|
376
|
+
return Symbolic.simplify(rewritten);
|
|
377
|
+
}
|
|
378
|
+
/** Symbolic anti-derivative (elementary rules; throws {@link NotIntegrableError} otherwise). */
|
|
379
|
+
static integrate(expr, variable = "x") {
|
|
380
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
381
|
+
return Symbolic.simplify(integ(e, variable));
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Definite integral of `expr` over `[lower, upper]` with respect to
|
|
385
|
+
* `variable`. Tries the elementary symbolic antiderivative first
|
|
386
|
+
* (evaluating `F(upper) - F(lower)`); falls back to adaptive-Simpson
|
|
387
|
+
* numeric quadrature when {@link integrate} would throw
|
|
388
|
+
* {@link NotIntegrableError}. `env` binds any other free variables/
|
|
389
|
+
* constants appearing in `expr` (same convention as {@link evaluate}/
|
|
390
|
+
* {@link compile}); if `env` also happens to bind `variable`, the
|
|
391
|
+
* integration bound always wins.
|
|
392
|
+
*
|
|
393
|
+
* `lower > upper` needs no special-casing -- both the closed-form
|
|
394
|
+
* `F(upper) - F(lower)` and {@link Numerical.adaptiveSimpson} are
|
|
395
|
+
* antisymmetric in their two bounds (verified directly against
|
|
396
|
+
* `adaptiveSimpson`'s implementation), so reversed bounds just negate
|
|
397
|
+
* the result, matching the standard ∫[a,b] = -∫[b,a] convention.
|
|
398
|
+
*
|
|
399
|
+
* Detects (best-effort) a singularity strictly inside `(lower, upper)` --
|
|
400
|
+
* e.g. `1/x` from -1 to 1 -- via {@link hasInteriorSingularity} and throws
|
|
401
|
+
* {@link IntegrationSingularityError} rather than silently returning a
|
|
402
|
+
* finite-looking but meaningless value, for both the closed-form path (a
|
|
403
|
+
* naive `F(upper) - F(lower)` is wrong across a pole even when `F` itself
|
|
404
|
+
* evaluates to finite numbers at both ends) and the numeric fallback.
|
|
405
|
+
*
|
|
406
|
+
* NON-GOAL: the detector is a bounded-effort numeric heuristic (see its
|
|
407
|
+
* own doc comment), not an analytic proof -- a sufficiently narrow
|
|
408
|
+
* singularity can in principle still slip through uniform sampling.
|
|
409
|
+
*
|
|
410
|
+
* @throws {IntegrationSingularityError} if an interior singularity is detected.
|
|
411
|
+
*/
|
|
412
|
+
static integrateDefinite(expr, lower, upper, variable = "x", env = {}) {
|
|
413
|
+
const e = Symbolic.simplify(typeof expr === "string" ? Symbolic.parse(expr) : expr);
|
|
414
|
+
const probe = compileExpr(e);
|
|
415
|
+
if (hasInteriorSingularity((val) => probe({ ...env, [variable]: val }), lower, upper)) {
|
|
416
|
+
throw new IntegrationSingularityError();
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const F = Symbolic.simplify(integ(e, variable));
|
|
420
|
+
const at = (val) => evalExpr(F, { ...env, [variable]: val });
|
|
421
|
+
return at(upper) - at(lower);
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
if (!(err instanceof NotIntegrableError))
|
|
425
|
+
throw err;
|
|
426
|
+
// `e` hasn't changed since `probe` was compiled above for the
|
|
427
|
+
// singularity check, so reuse it instead of recompiling.
|
|
428
|
+
return Numerical.adaptiveSimpson((val) => probe({ ...env, [variable]: val }), lower, upper);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Attempts a closed-form (symbolic) solution to the first-order initial
|
|
433
|
+
* value problem `dy/dx = dyDxExpr(x, y)`, `y(x0) = y0`, trying five
|
|
434
|
+
* elementary methods in order -- all built entirely on {@link integrate}'s
|
|
435
|
+
* existing elementary-rule coverage, not new integration machinery:
|
|
436
|
+
*
|
|
437
|
+
* 1. **Separable**: if `dyDxExpr` factors as `g(x)*h(y)` (including the
|
|
438
|
+
* degenerate cases `h(y)=1` or `g(x)=1`), integrates
|
|
439
|
+
* `∫(1/h(y))dy = ∫g(x)dx + C`, solves for `C` numerically from
|
|
440
|
+
* `(x0,y0)`, and tries to isolate `y`.
|
|
441
|
+
* 2. **Linear first-order**: if `dyDxExpr` is affine in `y` (i.e.
|
|
442
|
+
* `dy/dx + p(x)*y = q(x)` for some `p`,`q` depending only on `x`),
|
|
443
|
+
* solves via the integrating factor `μ(x)=exp(∫p(x)dx)`.
|
|
444
|
+
* 3. **Homogeneous**: if `dyDxExpr` depends only on the ratio `y/x`
|
|
445
|
+
* (tested by substituting `y=v·x` and checking whether `x` cancels
|
|
446
|
+
* out), the substitution reduces it to a separable equation in `v`
|
|
447
|
+
* and `x`, reusing method 1.
|
|
448
|
+
* 4. **Bernoulli**: if `dyDxExpr` has the shape `q(x)·y^n - p(x)·y`
|
|
449
|
+
* (`n != 0, 1`), the substitution `w=y^(1-n)` reduces it to a linear
|
|
450
|
+
* equation in `w` and `x`, reusing method 2.
|
|
451
|
+
* 5. **Exact**: if `dyDxExpr` is a quotient `P/Q`, cross-multiplying
|
|
452
|
+
* gives `M dx + N dy = 0` with `M=P`, `N=-Q`; when `∂M/∂y ≡ ∂N/∂x`
|
|
453
|
+
* (verified numerically at probe points), a potential function
|
|
454
|
+
* `F(x,y)` with `∂F/∂x=M`, `∂F/∂y=N` is constructed by integration,
|
|
455
|
+
* giving the (usually implicit) solution `F(x,y) = F(x0,y0)`.
|
|
456
|
+
* Deliberately tried LAST: the four methods above produce cleaner,
|
|
457
|
+
* usually-explicit results where they apply, and many equations match
|
|
458
|
+
* both (any separable quotient is also exact, for example) -- exact
|
|
459
|
+
* only picks up quotients none of the others could handle.
|
|
460
|
+
*
|
|
461
|
+
* All five paths legitimately fail on realistic inputs whose required
|
|
462
|
+
* antiderivative isn't in {@link integrate}'s elementary coverage -- that
|
|
463
|
+
* surfaces as {@link NoClosedFormError}. An equation matching none of the
|
|
464
|
+
* five structural patterns (e.g. a genuinely nonlinear/non-homogeneous
|
|
465
|
+
* case like a Riccati equation) surfaces as {@link NotSeparableError}.
|
|
466
|
+
* Second-order ODEs remain out of scope here (but see
|
|
467
|
+
* {@link solveOde2ndOrderConstCoeff} for the constant-coefficient
|
|
468
|
+
* homogeneous case).
|
|
469
|
+
*
|
|
470
|
+
* @throws {NotSeparableError} if no method's structural pattern matches.
|
|
471
|
+
* @throws {NoClosedFormError} if a method matches but its required integral(s) aren't elementary.
|
|
472
|
+
*/
|
|
473
|
+
static solveOdeClosedForm(dyDxExpr, x0, y0, xVar = "x", yVar = "y") {
|
|
474
|
+
const e = Symbolic.simplify(typeof dyDxExpr === "string" ? Symbolic.parse(dyDxExpr) : dyDxExpr);
|
|
475
|
+
const sep = matchSeparable(e, xVar, yVar);
|
|
476
|
+
if (sep)
|
|
477
|
+
return solveSeparableOde(sep, xVar, yVar, x0, y0);
|
|
478
|
+
const lin = matchLinearOde(e, xVar, yVar);
|
|
479
|
+
if (lin)
|
|
480
|
+
return solveLinearOde(lin, xVar, yVar, x0, y0);
|
|
481
|
+
const hom = matchHomogeneous(e, xVar, yVar);
|
|
482
|
+
if (hom)
|
|
483
|
+
return solveHomogeneousOde(hom, xVar, yVar, x0, y0);
|
|
484
|
+
const bern = matchBernoulli(e, xVar, yVar);
|
|
485
|
+
if (bern)
|
|
486
|
+
return solveBernoulliOde(bern, xVar, yVar, x0, y0);
|
|
487
|
+
const exact = matchExact(e, xVar, yVar);
|
|
488
|
+
if (exact)
|
|
489
|
+
return solveExactOde(exact, xVar, yVar, x0, y0);
|
|
490
|
+
throw new NotSeparableError();
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Closed-form solution to the second-order, constant-coefficient,
|
|
494
|
+
* *homogeneous* linear ODE `a*y'' + b*y' + c*y = 0`, given
|
|
495
|
+
* `y(x0)=y0`, `y'(x0)=yPrime0` -- a separate method from
|
|
496
|
+
* {@link solveOdeClosedForm} (not an extension of it): that method
|
|
497
|
+
* represents a first-order equation as a single `dy/dx=f(x,y)` `Expr`,
|
|
498
|
+
* but this file has no AST node for a second derivative, so a
|
|
499
|
+
* second-order equation is instead specified directly by its three
|
|
500
|
+
* numeric coefficients. Built directly from the characteristic
|
|
501
|
+
* equation's roots (`a*r^2+b*r+c=0`) via basic algebra -- no calls to
|
|
502
|
+
* {@link integrate}, unlike every first-order method above.
|
|
503
|
+
*
|
|
504
|
+
* Three cases by discriminant `disc = b^2 - 4ac`:
|
|
505
|
+
* 1. `disc > 0`: two distinct real roots `r1,r2`; general solution
|
|
506
|
+
* `y = C1*e^(r1*x) + C2*e^(r2*x)`, `C1`/`C2` from the 2x2 linear
|
|
507
|
+
* system the two initial conditions impose.
|
|
508
|
+
* 2. `disc ≈ 0` (tolerance `1e-9`): repeated root `r=-b/(2a)`; general
|
|
509
|
+
* solution `y = (C1+C2*x)*e^(r*x)`.
|
|
510
|
+
* 3. `disc < 0`: complex conjugate roots `α±iβ`; general solution
|
|
511
|
+
* `y = e^(α*x)*(C1*cos(β*x)+C2*sin(β*x))`.
|
|
512
|
+
*
|
|
513
|
+
* NON-GOALS: homogeneous only -- no forcing term `f(x)` on the
|
|
514
|
+
* right-hand side (a nonhomogeneous `a*y''+b*y'+c*y=f(x)` would need
|
|
515
|
+
* undetermined coefficients or variation of parameters, out of scope
|
|
516
|
+
* for this pass); no third-order-or-higher or non-constant-coefficient
|
|
517
|
+
* support.
|
|
518
|
+
*
|
|
519
|
+
* @throws {DegenerateOdeError} if `a === 0` (not actually second-order).
|
|
520
|
+
*/
|
|
521
|
+
static solveOde2ndOrderConstCoeff(a, b, c, x0, y0, yPrime0) {
|
|
522
|
+
if (a === 0)
|
|
523
|
+
throw new DegenerateOdeError();
|
|
524
|
+
const disc = b * b - 4 * a * c;
|
|
525
|
+
const x = v("x");
|
|
526
|
+
if (Math.abs(disc) < 1e-9) {
|
|
527
|
+
// Repeated root r = -b/(2a): y = (C1+C2*x)*e^(rx).
|
|
528
|
+
// C2 = (yPrime0 - r*y0)*e^(-r*x0), C1 = y0*e^(-r*x0) - C2*x0
|
|
529
|
+
// (verified: differentiating y and matching y(x0)=y0, y'(x0)=yPrime0
|
|
530
|
+
// with these C1/C2 holds numerically at several probe points).
|
|
531
|
+
const r = -b / (2 * a);
|
|
532
|
+
const eNegRX0 = Math.exp(-r * x0);
|
|
533
|
+
const C2 = (yPrime0 - r * y0) * eNegRX0;
|
|
534
|
+
const C1 = y0 * eNegRX0 - C2 * x0;
|
|
535
|
+
return mul(add(num(C1), mul(num(C2), x)), fn("exp", mul(num(r), x)));
|
|
536
|
+
}
|
|
537
|
+
if (disc > 0) {
|
|
538
|
+
// Two distinct real roots r1,r2: y = C1*e^(r1x) + C2*e^(r2x).
|
|
539
|
+
// C1,C2 solved from the 2x2 linear system the initial conditions
|
|
540
|
+
// impose (y(x0)=y0, y'(x0)=yPrime0), via the same MatrixMath.solve
|
|
541
|
+
// this file's own solveSystem already uses for small linear solves.
|
|
542
|
+
const sqrtDisc = Math.sqrt(disc);
|
|
543
|
+
const r1 = (-b + sqrtDisc) / (2 * a);
|
|
544
|
+
const r2 = (-b - sqrtDisc) / (2 * a);
|
|
545
|
+
const e1 = Math.exp(r1 * x0);
|
|
546
|
+
const e2 = Math.exp(r2 * x0);
|
|
547
|
+
const [C1, C2] = MatrixMath.solve([
|
|
548
|
+
[e1, e2],
|
|
549
|
+
[r1 * e1, r2 * e2],
|
|
550
|
+
], [y0, yPrime0]);
|
|
551
|
+
return add(mul(num(C1), fn("exp", mul(num(r1), x))), mul(num(C2), fn("exp", mul(num(r2), x))));
|
|
552
|
+
}
|
|
553
|
+
// Complex conjugate roots α±iβ: y = e^(αx)*(C1*cos(βx)+C2*sin(βx)).
|
|
554
|
+
// C1,C2 solved the same way from y(x0)=y0, y'(x0)=yPrime0 (the second
|
|
555
|
+
// equation's own α*y0 term comes from differentiating the e^(αx)
|
|
556
|
+
// factor via the product rule).
|
|
557
|
+
const alpha = -b / (2 * a);
|
|
558
|
+
const beta = Math.sqrt(4 * a * c - b * b) / (2 * a);
|
|
559
|
+
const eAlphaX0 = Math.exp(alpha * x0);
|
|
560
|
+
const cosB = Math.cos(beta * x0);
|
|
561
|
+
const sinB = Math.sin(beta * x0);
|
|
562
|
+
const [C1, C2] = MatrixMath.solve([
|
|
563
|
+
[eAlphaX0 * cosB, eAlphaX0 * sinB],
|
|
564
|
+
[-eAlphaX0 * beta * sinB, eAlphaX0 * beta * cosB],
|
|
565
|
+
], [y0, yPrime0 - alpha * y0]);
|
|
566
|
+
return mul(fn("exp", mul(num(alpha), x)), add(mul(num(C1), fn("cos", mul(num(beta), x))), mul(num(C2), fn("sin", mul(num(beta), x)))));
|
|
567
|
+
}
|
|
568
|
+
/** Taylor expansion of `expr` about `center` up to `order`, as an expression. */
|
|
569
|
+
static taylor(expr, variable = "x", center = 0, order = 4) {
|
|
570
|
+
let term = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
571
|
+
let result = num(0);
|
|
572
|
+
let factorial = 1;
|
|
573
|
+
for (let n = 0; n <= order; n++) {
|
|
574
|
+
if (n > 0)
|
|
575
|
+
factorial *= n;
|
|
576
|
+
const coeff = Symbolic.evaluate(term, { [variable]: center });
|
|
577
|
+
if (coeff !== 0) {
|
|
578
|
+
const power = pow(sub(v(variable), num(center)), num(n));
|
|
579
|
+
result = add(result, mul(num(coeff / factorial), power));
|
|
580
|
+
}
|
|
581
|
+
term = diff(term, variable);
|
|
582
|
+
}
|
|
583
|
+
return Symbolic.simplify(result);
|
|
584
|
+
}
|
|
585
|
+
/** Replace every occurrence of a variable with a sub-expression (e.g. for changing variables or plugging in a solved value). */
|
|
586
|
+
static substitute(expr, variable, replacement) {
|
|
587
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
588
|
+
const r = typeof replacement === "string" ? Symbolic.parse(replacement) : replacement;
|
|
589
|
+
return Symbolic.simplify(subst(e, variable, r));
|
|
590
|
+
}
|
|
591
|
+
/** Distribute multiplication over addition/subtraction and expand small integer powers of sums, e.g. `(x+1)^2 -> x^2+2*x+1`. */
|
|
592
|
+
static expand(expr) {
|
|
593
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
594
|
+
let cur = e;
|
|
595
|
+
for (let i = 0; i < 30; i++) {
|
|
596
|
+
const next = Symbolic.simplify(expandOnce(cur));
|
|
597
|
+
if (equal(next, cur))
|
|
598
|
+
return next;
|
|
599
|
+
cur = next;
|
|
600
|
+
}
|
|
601
|
+
return cur;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Every distinct variable name referenced anywhere in `expr`, sorted. This
|
|
605
|
+
* is auto-detection, not validation -- see {@link assertVariables} for a
|
|
606
|
+
* strict-mode check against a declared list.
|
|
607
|
+
*/
|
|
608
|
+
static freeVariables(expr) {
|
|
609
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
610
|
+
return [...collectFreeVars(e)].sort();
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Strict-mode companion to {@link freeVariables}: throws
|
|
614
|
+
* {@link UndeclaredVariableError} if `expr` references any variable
|
|
615
|
+
* outside `declared`. Useful to catch typos or ambiguous-parse fallout
|
|
616
|
+
* before they silently become spurious sliders/parameters or silently
|
|
617
|
+
* evaluate to `NaN`.
|
|
618
|
+
*/
|
|
619
|
+
static assertVariables(expr, declared) {
|
|
620
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
621
|
+
const declaredSet = new Set(declared);
|
|
622
|
+
const undeclared = Symbolic.freeVariables(e).filter((name) => !declaredSet.has(name));
|
|
623
|
+
if (undeclared.length > 0)
|
|
624
|
+
throw new UndeclaredVariableError(undeclared);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* @param options.declaredVariables When provided, throws
|
|
628
|
+
* {@link UndeclaredVariableError} if `expr` references any variable
|
|
629
|
+
* outside this list, instead of the default behavior of silently
|
|
630
|
+
* resolving an undeclared variable to `NaN` via a missing `env` entry.
|
|
631
|
+
*/
|
|
632
|
+
static evaluate(expr, env = {}, options) {
|
|
633
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
634
|
+
if (options?.declaredVariables)
|
|
635
|
+
Symbolic.assertVariables(e, options.declaredVariables);
|
|
636
|
+
return evalExpr(e, env);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Compile `expr` into a closure tree, walking the AST once instead of on
|
|
640
|
+
* every call. Prefer this over {@link evaluate} when the same expression is
|
|
641
|
+
* evaluated many times (e.g. sampling a curve across hundreds of x values).
|
|
642
|
+
*
|
|
643
|
+
* @param options.declaredVariables Same strict-mode check as
|
|
644
|
+
* {@link evaluate}, checked once up front rather than per call.
|
|
645
|
+
*/
|
|
646
|
+
static compile(expr, options) {
|
|
647
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
648
|
+
if (options?.declaredVariables)
|
|
649
|
+
Symbolic.assertVariables(e, options.declaredVariables);
|
|
650
|
+
return compileExpr(e);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Evaluates `expr` exactly over {@link Rational} arithmetic instead of
|
|
654
|
+
* floats (e.g. `1/3` stays an exact fraction, not `0.333...`). Throws
|
|
655
|
+
* whenever the expression isn't exactly representable -- any `func`/
|
|
656
|
+
* `call2` node (sin/cos/sqrt/atan2/etc. are generally irrational) or a
|
|
657
|
+
* `pow` whose exponent isn't an integer -- so callers should fall back to
|
|
658
|
+
* a plain float evaluation on catch. `cmp` evaluates to exactly `1`/`0`
|
|
659
|
+
* via `Rational.compare`; `piecewise` selects the branch exactly (first
|
|
660
|
+
* `cond` whose Rational is nonzero).
|
|
661
|
+
*/
|
|
662
|
+
static evaluateExact(expr, env = {}) {
|
|
663
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
664
|
+
return evalExprExact(e, env);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Evaluates `expr` by folding it through an arbitrary {@link Structure}'s
|
|
668
|
+
* own algebra instead of native JS math, so e.g. plotting can work over
|
|
669
|
+
* Z/7Z instead of the reals. Throws on any `func`/`call2` node (no
|
|
670
|
+
* general meaning over an abstract structure), a `pow` whose exponent
|
|
671
|
+
* isn't a literal integer constant, or an ordering comparison (`lt`/`le`/
|
|
672
|
+
* `gt`/`ge` -- several structures, e.g. quaternions, have no order
|
|
673
|
+
* compatible with their ring operations). `eq`/`ne` and `piecewise` work
|
|
674
|
+
* over any structure via its own `.equality()`.
|
|
675
|
+
*/
|
|
676
|
+
static evaluateOverStructure(expr, structure, env = {}) {
|
|
677
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
678
|
+
return evalExprOverStructure(e, structure, env);
|
|
679
|
+
}
|
|
680
|
+
static toString(expr) {
|
|
681
|
+
return render(typeof expr === "string" ? Symbolic.parse(expr) : expr, 0);
|
|
682
|
+
}
|
|
683
|
+
/** Render `expr` as a LaTeX string, e.g. `"x/2"` -> `"\\frac{x}{2}"`. */
|
|
684
|
+
static toLatex(expr) {
|
|
685
|
+
return toLatexRec(typeof expr === "string" ? Symbolic.parse(expr) : expr, 0);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Parse a LaTeX math string into an `Expr` — the reverse of {@link toLatex}.
|
|
689
|
+
* Round-trips everything `toLatex` produces: `\frac{a}{b}`, `\sqrt{a}` /
|
|
690
|
+
* `\sqrt[3]{a}` (as `cbrt`) / `\sqrt[n]{a}`, `\left(...\right)`,
|
|
691
|
+
* `\left|...\right|` (absolute value), `\left\lfloor...\right\rfloor` /
|
|
692
|
+
* `\left\lceil...\right\rceil`, `\log_{10}`/`\log_{2}`, `\cdot`/`\times`,
|
|
693
|
+
* `\pi`, `^{...}` exponents, `_{...}` subscripts, the named unary function
|
|
694
|
+
* commands (`\sin`, `\arcsin`, `\sinh`, `\operatorname{sech}`, ...), and the
|
|
695
|
+
* two-argument `BinaryFuncName` commands (`\min`, `\max`, `\gcd`,
|
|
696
|
+
* `\operatorname{atan2}`, `\operatorname{hypot}`, `\operatorname{lcm}`).
|
|
697
|
+
* Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored.
|
|
698
|
+
*
|
|
699
|
+
* @throws on LaTeX constructs with no `Expr` equivalent, e.g. `\int` or `\sum`.
|
|
700
|
+
*/
|
|
701
|
+
static fromLatex(latex) {
|
|
702
|
+
return Symbolic.parse(latexToInfix(latex));
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Solve `expr = 0` for `variable`, returning every *real* root @johnhenry/math
|
|
706
|
+
* can find as an exact `Expr` (rationals and `sqrt`-radicals where
|
|
707
|
+
* possible) — complex roots are not returned (e.g. `x^2 + 1` yields `[]`).
|
|
708
|
+
* Supports polynomials up to degree 6: linear and quadratic are solved in
|
|
709
|
+
* closed form; degree ≥ 3 is solved by repeatedly finding a rational root
|
|
710
|
+
* (rational root theorem) and deflating via synthetic division, so a cubic
|
|
711
|
+
* or quartic with only irrational/complex roots left over after rational
|
|
712
|
+
* deflation cannot be fully solved this way.
|
|
713
|
+
*
|
|
714
|
+
* @throws if `expr` isn't a polynomial in `variable` of degree ≤ 6, or if a
|
|
715
|
+
* degree ≥ 3 factor has no rational root to deflate on.
|
|
716
|
+
* @throws {InfiniteSolutionsError} if `expr` is identically zero (e.g.
|
|
717
|
+
* `solve("0")`) -- every real number is a solution, a different result
|
|
718
|
+
* shape from `[]` (which means "verified no real solution").
|
|
719
|
+
*/
|
|
720
|
+
static solve(expr, variable = "x") {
|
|
721
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
722
|
+
const coeffs = polynomialCoeffs(Symbolic.simplify(e), variable, 6);
|
|
723
|
+
if (!coeffs) {
|
|
724
|
+
throw new Error(`solve: expression is not a polynomial in "${variable}" of degree ≤ 6.`);
|
|
725
|
+
}
|
|
726
|
+
return solvePolynomial(coeffs).map((r) => Symbolic.simplify(r));
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Solve a system of linear equations, each written in the same "expr
|
|
730
|
+
* implicitly equals zero" convention as {@link solve} (no `=` operator
|
|
731
|
+
* needed -- e.g. `"2*x + y - 5"` means `2x+y=5`). Requires exactly one
|
|
732
|
+
* equation per variable (a square system) with a unique solution.
|
|
733
|
+
*
|
|
734
|
+
* NON-GOAL (v1): nonlinear systems are not supported -- throws
|
|
735
|
+
* {@link NonLinearSystemError}. A future multivariable-Newton extension
|
|
736
|
+
* (seeded from an initial guess, iterating via the Jacobian) would slot in
|
|
737
|
+
* here as a fallback when linearity detection fails, but is out of scope now.
|
|
738
|
+
*
|
|
739
|
+
* @throws {NonLinearSystemError} if any equation isn't linear in `variables`.
|
|
740
|
+
* @throws {SingularSystemError} if the coefficient matrix is singular/rank-deficient.
|
|
741
|
+
*/
|
|
742
|
+
static solveSystem(equations, variables) {
|
|
743
|
+
const exprs = equations.map((eq) => Symbolic.simplify(typeof eq === "string" ? Symbolic.parse(eq) : eq));
|
|
744
|
+
if (exprs.length !== variables.length) {
|
|
745
|
+
throw new Error(`solveSystem: expected ${variables.length} equations for ${variables.length} variables, got ${exprs.length}.`);
|
|
746
|
+
}
|
|
747
|
+
const A = [];
|
|
748
|
+
const b = [];
|
|
749
|
+
exprs.forEach((eq, i) => {
|
|
750
|
+
const row = linearCoeffsForSystem(eq, variables);
|
|
751
|
+
if (!row) {
|
|
752
|
+
throw new NonLinearSystemError(`solveSystem: equation ${i} ("${Symbolic.toString(eq)}") is not linear in [${variables.join(", ")}].`);
|
|
753
|
+
}
|
|
754
|
+
A.push(row.slice(0, variables.length));
|
|
755
|
+
b.push(-row[variables.length]); // eq = A·v + c = 0 => A·v = -c
|
|
756
|
+
});
|
|
757
|
+
const rank = MatrixMath.rank(A);
|
|
758
|
+
if (rank < variables.length) {
|
|
759
|
+
throw new SingularSystemError(`solveSystem: the system is singular (rank ${rank} < ${variables.length} variables) and has no unique solution.`);
|
|
760
|
+
}
|
|
761
|
+
const x = MatrixMath.solve(A, b);
|
|
762
|
+
const result = {};
|
|
763
|
+
variables.forEach((name, i) => {
|
|
764
|
+
result[name] = x[i];
|
|
765
|
+
});
|
|
766
|
+
return result;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Numerically solves a square system of (possibly nonlinear) equations
|
|
770
|
+
* via multivariable Newton's method with a finite-difference Jacobian,
|
|
771
|
+
* starting from `initialGuess` (default: all 1s). Unlike `solveSystem`,
|
|
772
|
+
* this never throws `NonLinearSystemError` -- a genuinely linear system
|
|
773
|
+
* just converges in one Newton step, since Newton's method reduces to a
|
|
774
|
+
* single linear solve whenever the Jacobian is already constant. Added as
|
|
775
|
+
* a separate opt-in method (not `solveSystem`'s new default behavior) so
|
|
776
|
+
* `solveSystem`'s existing "throw on nonlinear" contract, and the tests
|
|
777
|
+
* that rely on it, stay unchanged -- callers who want a nonlinear-capable
|
|
778
|
+
* solve opt in explicitly.
|
|
779
|
+
*
|
|
780
|
+
* Each Newton step is damped by a backtracking line search: the full step
|
|
781
|
+
* is tried first, halved up to 20 times until it actually reduces the
|
|
782
|
+
* residual norm (or leaves the function's domain), rather than being
|
|
783
|
+
* taken unconditionally -- this is what keeps a merely-nearby (not
|
|
784
|
+
* exact) initial guess from overshooting past a real root, the concrete
|
|
785
|
+
* failure mode plain undamped Newton has no defense against.
|
|
786
|
+
*
|
|
787
|
+
* NON-GOALS: still no trust-region step (backtracking damps the step
|
|
788
|
+
* length but never changes its *direction* the way a trust region
|
|
789
|
+
* would), so a genuinely bad initial guess -- one on the wrong side of a
|
|
790
|
+
* local max/min of the residual, not just far from the root -- can still
|
|
791
|
+
* fail to converge; only ever finds one root near the initial guess, not
|
|
792
|
+
* every root of a system with multiple solutions.
|
|
793
|
+
*
|
|
794
|
+
* @throws {SystemDidNotConvergeError} if the residual doesn't shrink
|
|
795
|
+
* below tolerance within the iteration budget, if evaluation produces a
|
|
796
|
+
* non-finite value (e.g. leaving the function's domain) at some step,
|
|
797
|
+
* or if no damped step size improves the residual at some iterate.
|
|
798
|
+
*/
|
|
799
|
+
static solveSystemNumeric(equations, variables, initialGuess) {
|
|
800
|
+
const exprs = equations.map((eq) => (typeof eq === "string" ? Symbolic.parse(eq) : eq));
|
|
801
|
+
if (exprs.length !== variables.length) {
|
|
802
|
+
throw new Error(`solveSystemNumeric: expected ${variables.length} equations for ${variables.length} variables, got ${exprs.length}.`);
|
|
803
|
+
}
|
|
804
|
+
const n = variables.length;
|
|
805
|
+
const compiled = exprs.map((e) => Symbolic.compile(e));
|
|
806
|
+
const evalF = (point) => {
|
|
807
|
+
const env = {};
|
|
808
|
+
variables.forEach((v, i) => {
|
|
809
|
+
env[v] = point[i];
|
|
810
|
+
});
|
|
811
|
+
return compiled.map((f) => f(env));
|
|
812
|
+
};
|
|
813
|
+
let x = initialGuess ? [...initialGuess] : new Array(n).fill(1);
|
|
814
|
+
const h = 1e-6;
|
|
815
|
+
const maxIterations = 100;
|
|
816
|
+
const tolerance = 1e-10;
|
|
817
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
818
|
+
const fx = evalF(x);
|
|
819
|
+
if (!fx.every(Number.isFinite)) {
|
|
820
|
+
throw new SystemDidNotConvergeError("solveSystemNumeric: evaluation produced a non-finite value.");
|
|
821
|
+
}
|
|
822
|
+
const residualNorm = Math.sqrt(fx.reduce((sum, v) => sum + v * v, 0));
|
|
823
|
+
if (residualNorm < tolerance) {
|
|
824
|
+
const result = {};
|
|
825
|
+
variables.forEach((name, i) => {
|
|
826
|
+
result[name] = x[i];
|
|
827
|
+
});
|
|
828
|
+
return result;
|
|
829
|
+
}
|
|
830
|
+
const jacobian = Array.from({ length: n }, () => new Array(n).fill(0));
|
|
831
|
+
for (let col = 0; col < n; col++) {
|
|
832
|
+
const perturbed = [...x];
|
|
833
|
+
perturbed[col] = perturbed[col] + h;
|
|
834
|
+
const fPerturbed = evalF(perturbed);
|
|
835
|
+
for (let row = 0; row < n; row++) {
|
|
836
|
+
jacobian[row][col] = (fPerturbed[row] - fx[row]) / h;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
let delta;
|
|
840
|
+
try {
|
|
841
|
+
delta = MatrixMath.solve(jacobian, fx.map((v) => -v));
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
throw new SystemDidNotConvergeError("solveSystemNumeric: the Jacobian became singular during iteration.");
|
|
845
|
+
}
|
|
846
|
+
let stepScale = 1;
|
|
847
|
+
let accepted = false;
|
|
848
|
+
for (let backtrack = 0; backtrack < 20 && !accepted; backtrack++) {
|
|
849
|
+
const candidate = x.map((xi, i) => xi + stepScale * delta[i]);
|
|
850
|
+
const fCandidate = evalF(candidate);
|
|
851
|
+
if (fCandidate.every(Number.isFinite)) {
|
|
852
|
+
const candidateNorm = Math.sqrt(fCandidate.reduce((sum, v) => sum + v * v, 0));
|
|
853
|
+
if (candidateNorm < residualNorm) {
|
|
854
|
+
x = candidate;
|
|
855
|
+
accepted = true;
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
stepScale /= 2;
|
|
860
|
+
}
|
|
861
|
+
if (!accepted) {
|
|
862
|
+
throw new SystemDidNotConvergeError("solveSystemNumeric: no damped step improved the residual from the current iterate.");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
throw new SystemDidNotConvergeError(`solveSystemNumeric: did not converge within ${maxIterations} iterations from the given initial guess.`);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Numerically spot-checks that `candidate` is (approximately) a root of
|
|
869
|
+
* `equation` (the same "implicitly equals zero" convention `solve` uses):
|
|
870
|
+
* substitutes `candidate` for `variable` and checks the result is near
|
|
871
|
+
* zero. A lightweight reviewer/verification pass over a CAS result, not a
|
|
872
|
+
* proof of exactness -- catches an outright wrong candidate (e.g. from a
|
|
873
|
+
* heuristic search or a downstream bug) before it's presented, using the
|
|
874
|
+
* same probe-and-compare technique this codebase's own test suite already
|
|
875
|
+
* relies on for anything heuristic.
|
|
876
|
+
*/
|
|
877
|
+
static verifySolution(equation, variable, candidate, env = {}, tolerance = 1e-6) {
|
|
878
|
+
const value = Symbolic.evaluate(equation, { ...env, [variable]: candidate });
|
|
879
|
+
return Number.isFinite(value) && Math.abs(value) < tolerance;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The `solveSystem`/`solveSystemNumeric` analog of {@link verifySolution}:
|
|
883
|
+
* substitutes `solution` into every equation and requires all of them to
|
|
884
|
+
* land near zero.
|
|
885
|
+
*/
|
|
886
|
+
static verifySystemSolution(equations, solution, tolerance = 1e-6) {
|
|
887
|
+
return equations.every((eq) => {
|
|
888
|
+
const value = Symbolic.evaluate(eq, solution);
|
|
889
|
+
return Number.isFinite(value) && Math.abs(value) < tolerance;
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Sum `expr` over integer values of `variable` from `from` to `to`
|
|
894
|
+
* (inclusive); `to` may be `Infinity` for an infinite series. A finite
|
|
895
|
+
* range is a plain partial sum. An infinite range first tries to recognize
|
|
896
|
+
* `expr` as a geometric series (`c·r^(a·variable+b)`, closed form
|
|
897
|
+
* `c·r^(a·from+b)/(1-r^a)`), falling back to numeric partial summation with
|
|
898
|
+
* a convergence/divergence check otherwise.
|
|
899
|
+
*
|
|
900
|
+
* `variable` defaults to `"n"` (not `"x"`), matching conventional
|
|
901
|
+
* summation-index notation.
|
|
902
|
+
*
|
|
903
|
+
* NON-GOALS: no general convergence-radius/ratio-test analysis beyond a
|
|
904
|
+
* simple term-growth divergence heuristic; no summation acceleration
|
|
905
|
+
* (Euler transform etc.) for slowly-converging or conditionally-convergent
|
|
906
|
+
* alternating series; closed-form recognition covers geometric series
|
|
907
|
+
* only, not arbitrary known series (no telescoping, no p-series/zeta
|
|
908
|
+
* recognition).
|
|
909
|
+
*
|
|
910
|
+
* If the term-magnitude stopping rule alone can't confirm convergence
|
|
911
|
+
* within the term budget (e.g. `Σ 1/n^2`, a slowly-decaying polynomial
|
|
912
|
+
* tail the per-term check isn't a valid proxy for), a second pass tries
|
|
913
|
+
* an integral-test tail bound instead of immediately declaring divergence:
|
|
914
|
+
* see {@link estimateConvergentTail}'s own doc comment for how. This
|
|
915
|
+
* recovers the correct value for a genuinely convergent slow-decay
|
|
916
|
+
* series without weakening the divergence check for a genuinely
|
|
917
|
+
* divergent one (e.g. `Σ 1/n`) -- the tail estimator itself confirms
|
|
918
|
+
* whether the integral is actually shrinking before trusting it.
|
|
919
|
+
*
|
|
920
|
+
* @throws {SeriesDivergesError} if the numeric fallback (including the
|
|
921
|
+
* integral-test tail estimate) still can't confirm convergence within
|
|
922
|
+
* its budget, or detects the terms growing.
|
|
923
|
+
*/
|
|
924
|
+
static sumSeries(expr, from, to, variable = "n", env = {}) {
|
|
925
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
926
|
+
if (Number.isFinite(to)) {
|
|
927
|
+
if (from > to)
|
|
928
|
+
return 0; // empty sum
|
|
929
|
+
const f = compileExpr(e);
|
|
930
|
+
let total = 0;
|
|
931
|
+
for (let k = from; k <= to; k++)
|
|
932
|
+
total += f({ ...env, [variable]: k });
|
|
933
|
+
return total;
|
|
934
|
+
}
|
|
935
|
+
const closedForm = tryGeometricSeriesClosedForm(e, variable, from, env);
|
|
936
|
+
if (closedForm !== null)
|
|
937
|
+
return closedForm;
|
|
938
|
+
return sumSeriesNumeric(e, variable, from, env);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Factor a polynomial in `variable`: extracts a common numeric GCD and
|
|
942
|
+
* common power of `variable`, then repeatedly pulls out rational linear
|
|
943
|
+
* factors `(variable - r)` via the rational root theorem, falling back to
|
|
944
|
+
* the quadratic formula for a final degree-2 remainder. Any remaining
|
|
945
|
+
* factor @johnhenry/math can't reduce further (e.g. an irreducible cubic) is
|
|
946
|
+
* left as-is. Returns `expr` unchanged (simplified) if it isn't a
|
|
947
|
+
* polynomial in `variable`.
|
|
948
|
+
*/
|
|
949
|
+
static factor(expr, variable = "x") {
|
|
950
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
951
|
+
const simplified = Symbolic.simplify(e);
|
|
952
|
+
const coeffs = polynomialCoeffs(simplified, variable, 6);
|
|
953
|
+
if (!coeffs)
|
|
954
|
+
return simplified;
|
|
955
|
+
return factorPolynomial(coeffs, variable);
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Numeric limit of `expr` as `variable` approaches `point`. Falls back to
|
|
959
|
+
* L'Hopital's rule (repeated symbolic differentiation of numerator and
|
|
960
|
+
* denominator) whenever direct evaluation of a `div` node hits a `0/0` or
|
|
961
|
+
* `∞/∞` indeterminate form.
|
|
962
|
+
*/
|
|
963
|
+
static limit(expr, variable = "x", point = 0, direction = "both") {
|
|
964
|
+
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
965
|
+
return limitAt(e, variable, point, direction, 0);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function diffTraced(e, x, steps) {
|
|
969
|
+
let rule;
|
|
970
|
+
let result;
|
|
971
|
+
switch (e.type) {
|
|
972
|
+
case "const":
|
|
973
|
+
rule = "Constant Rule";
|
|
974
|
+
result = num(0);
|
|
975
|
+
break;
|
|
976
|
+
case "var":
|
|
977
|
+
rule = e.name === x ? "Variable Rule" : "Constant Rule";
|
|
978
|
+
result = num(e.name === x ? 1 : 0);
|
|
979
|
+
break;
|
|
980
|
+
case "add":
|
|
981
|
+
rule = "Sum Rule";
|
|
982
|
+
result = add(diffTraced(e.left, x, steps), diffTraced(e.right, x, steps));
|
|
983
|
+
break;
|
|
984
|
+
case "sub":
|
|
985
|
+
rule = "Difference Rule";
|
|
986
|
+
result = sub(diffTraced(e.left, x, steps), diffTraced(e.right, x, steps));
|
|
987
|
+
break;
|
|
988
|
+
case "mul":
|
|
989
|
+
rule = "Product Rule";
|
|
990
|
+
result = add(mul(diffTraced(e.left, x, steps), e.right), mul(e.left, diffTraced(e.right, x, steps)));
|
|
991
|
+
break;
|
|
992
|
+
case "div":
|
|
993
|
+
rule = "Quotient Rule";
|
|
994
|
+
result = div(sub(mul(diffTraced(e.left, x, steps), e.right), mul(e.left, diffTraced(e.right, x, steps))), pow(e.right, num(2)));
|
|
995
|
+
break;
|
|
996
|
+
case "neg":
|
|
997
|
+
rule = "Negation Rule";
|
|
998
|
+
result = neg(diffTraced(e.arg, x, steps));
|
|
999
|
+
break;
|
|
1000
|
+
case "pow":
|
|
1001
|
+
if (e.exp.type === "const") {
|
|
1002
|
+
rule = "Power Rule";
|
|
1003
|
+
result = mul(mul(e.exp, pow(e.base, num(e.exp.value - 1))), diffTraced(e.base, x, steps));
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
rule = "Generalized Power Rule";
|
|
1007
|
+
result = mul(e, add(mul(diffTraced(e.exp, x, steps), fn("ln", e.base)), mul(e.exp, div(diffTraced(e.base, x, steps), e.base))));
|
|
1008
|
+
}
|
|
1009
|
+
break;
|
|
1010
|
+
case "func": {
|
|
1011
|
+
rule = `Chain Rule (${e.name})`;
|
|
1012
|
+
const u = e.arg;
|
|
1013
|
+
const du = diffTraced(u, x, steps);
|
|
1014
|
+
result = FUNC_DERIVATIVE_RULES[e.name](u, du);
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
case "call2": {
|
|
1018
|
+
rule = `Multivariable Chain Rule (${e.name})`;
|
|
1019
|
+
const l = e.left;
|
|
1020
|
+
const r = e.right;
|
|
1021
|
+
const dl = diffTraced(l, x, steps);
|
|
1022
|
+
const dr = diffTraced(r, x, steps);
|
|
1023
|
+
switch (e.name) {
|
|
1024
|
+
case "atan2":
|
|
1025
|
+
result = div(sub(mul(r, dl), mul(l, dr)), add(pow(l, num(2)), pow(r, num(2))));
|
|
1026
|
+
break;
|
|
1027
|
+
case "hypot":
|
|
1028
|
+
result = div(add(mul(l, dl), mul(r, dr)), call2("hypot", l, r));
|
|
1029
|
+
break;
|
|
1030
|
+
case "min":
|
|
1031
|
+
result = sub(div(add(dl, dr), num(2)), mul(fn("sign", sub(l, r)), div(sub(dl, dr), num(2))));
|
|
1032
|
+
break;
|
|
1033
|
+
case "max":
|
|
1034
|
+
result = add(div(add(dl, dr), num(2)), mul(fn("sign", sub(l, r)), div(sub(dl, dr), num(2))));
|
|
1035
|
+
break;
|
|
1036
|
+
case "gcd":
|
|
1037
|
+
case "lcm":
|
|
1038
|
+
result = num(0);
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
case "cmp":
|
|
1044
|
+
rule = "Comparison Rule (locally constant)";
|
|
1045
|
+
result = num(0);
|
|
1046
|
+
break;
|
|
1047
|
+
case "piecewise":
|
|
1048
|
+
rule = "Piecewise Rule (branch-wise)";
|
|
1049
|
+
result = piecewise(e.branches.map((br) => ({ cond: br.cond, expr: diffTraced(br.expr, x, steps) })), diffTraced(e.otherwise, x, steps));
|
|
1050
|
+
break;
|
|
1051
|
+
case "sum":
|
|
1052
|
+
// Mechanical by linearity: differentiate the body, keep the bound
|
|
1053
|
+
// variable/bounds unchanged. NON-GOAL: a differentiation variable
|
|
1054
|
+
// that happens to share a name with e.variable (a bound-variable
|
|
1055
|
+
// shadowing collision) isn't specially detected -- same class of
|
|
1056
|
+
// edge case as any other variable shadowing in this file.
|
|
1057
|
+
rule = "Sum Rule (Σ, term-wise by linearity)";
|
|
1058
|
+
result = sumExpr(e.variable, e.from, e.to, diffTraced(e.body, x, steps));
|
|
1059
|
+
break;
|
|
1060
|
+
case "product":
|
|
1061
|
+
// Logarithmic-derivative identity: ln(P) = sum(i,from,to, ln(body)),
|
|
1062
|
+
// so d(ln P)/dx = sum(i,from,to, dbody/dx / body), and
|
|
1063
|
+
// dP/dx = P * d(ln P)/dx. Well-defined whenever no body value is
|
|
1064
|
+
// zero over the range -- a legitimate restriction of the technique
|
|
1065
|
+
// itself (division by zero otherwise), not special-cased here, same
|
|
1066
|
+
// as every other division in this file.
|
|
1067
|
+
rule = "Product Rule (Π, via the logarithmic-derivative identity: d(ln P)/dx = Σ (dbody/dx)/body)";
|
|
1068
|
+
result = mul(productExpr(e.variable, e.from, e.to, e.body), sumExpr(e.variable, e.from, e.to, div(diffTraced(e.body, x, steps), e.body)));
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
steps.push({ rule, input: e, output: result });
|
|
1072
|
+
return result;
|
|
1073
|
+
}
|
|
1074
|
+
function diff(e, x) {
|
|
1075
|
+
switch (e.type) {
|
|
1076
|
+
case "const":
|
|
1077
|
+
return num(0);
|
|
1078
|
+
case "var":
|
|
1079
|
+
return num(e.name === x ? 1 : 0);
|
|
1080
|
+
case "add":
|
|
1081
|
+
return add(diff(e.left, x), diff(e.right, x));
|
|
1082
|
+
case "sub":
|
|
1083
|
+
return sub(diff(e.left, x), diff(e.right, x));
|
|
1084
|
+
case "mul":
|
|
1085
|
+
return add(mul(diff(e.left, x), e.right), mul(e.left, diff(e.right, x)));
|
|
1086
|
+
case "div":
|
|
1087
|
+
return div(sub(mul(diff(e.left, x), e.right), mul(e.left, diff(e.right, x))), pow(e.right, num(2)));
|
|
1088
|
+
case "neg":
|
|
1089
|
+
return neg(diff(e.arg, x));
|
|
1090
|
+
case "pow": {
|
|
1091
|
+
// constant exponent: c·u^(c-1)·u'
|
|
1092
|
+
if (e.exp.type === "const") {
|
|
1093
|
+
return mul(mul(e.exp, pow(e.base, num(e.exp.value - 1))), diff(e.base, x));
|
|
1094
|
+
}
|
|
1095
|
+
// general: u^v·(v'·ln u + v·u'/u)
|
|
1096
|
+
return mul(e, add(mul(diff(e.exp, x), fn("ln", e.base)), mul(e.exp, div(diff(e.base, x), e.base))));
|
|
1097
|
+
}
|
|
1098
|
+
case "func": {
|
|
1099
|
+
const u = e.arg;
|
|
1100
|
+
const du = diff(u, x);
|
|
1101
|
+
return FUNC_DERIVATIVE_RULES[e.name](u, du);
|
|
1102
|
+
}
|
|
1103
|
+
case "call2": {
|
|
1104
|
+
const l = e.left;
|
|
1105
|
+
const r = e.right;
|
|
1106
|
+
const dl = diff(l, x);
|
|
1107
|
+
const dr = diff(r, x);
|
|
1108
|
+
const binaryName = e.name;
|
|
1109
|
+
switch (binaryName) {
|
|
1110
|
+
case "atan2":
|
|
1111
|
+
return div(sub(mul(r, dl), mul(l, dr)), add(pow(l, num(2)), pow(r, num(2))));
|
|
1112
|
+
case "hypot":
|
|
1113
|
+
return div(add(mul(l, dl), mul(r, dr)), call2("hypot", l, r));
|
|
1114
|
+
case "min":
|
|
1115
|
+
return sub(div(add(dl, dr), num(2)), mul(fn("sign", sub(l, r)), div(sub(dl, dr), num(2))));
|
|
1116
|
+
case "max":
|
|
1117
|
+
return add(div(add(dl, dr), num(2)), mul(fn("sign", sub(l, r)), div(sub(dl, dr), num(2))));
|
|
1118
|
+
case "gcd":
|
|
1119
|
+
case "lcm":
|
|
1120
|
+
return num(0);
|
|
1121
|
+
}
|
|
1122
|
+
throw new Error(`Unhandled binary function: ${binaryName}`);
|
|
1123
|
+
}
|
|
1124
|
+
case "cmp":
|
|
1125
|
+
return num(0);
|
|
1126
|
+
case "piecewise":
|
|
1127
|
+
return piecewise(e.branches.map((br) => ({ cond: br.cond, expr: diff(br.expr, x) })), diff(e.otherwise, x));
|
|
1128
|
+
case "sum":
|
|
1129
|
+
return sumExpr(e.variable, e.from, e.to, diff(e.body, x));
|
|
1130
|
+
case "product":
|
|
1131
|
+
return mul(productExpr(e.variable, e.from, e.to, e.body), sumExpr(e.variable, e.from, e.to, div(diff(e.body, x), e.body)));
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
// -- simplification ---------------------------------------------------------
|
|
1135
|
+
function simplifyOnce(e) {
|
|
1136
|
+
if (e.type === "const" || e.type === "var")
|
|
1137
|
+
return e;
|
|
1138
|
+
if (e.type === "neg") {
|
|
1139
|
+
const a = simplifyOnce(e.arg);
|
|
1140
|
+
if (a.type === "const")
|
|
1141
|
+
return num(-a.value);
|
|
1142
|
+
if (a.type === "neg")
|
|
1143
|
+
return a.arg;
|
|
1144
|
+
return neg(a);
|
|
1145
|
+
}
|
|
1146
|
+
if (e.type === "func")
|
|
1147
|
+
return fn(e.name, simplifyOnce(e.arg));
|
|
1148
|
+
if (e.type === "call2") {
|
|
1149
|
+
const l = simplifyOnce(e.left);
|
|
1150
|
+
const r = simplifyOnce(e.right);
|
|
1151
|
+
if (l.type === "const" && r.type === "const")
|
|
1152
|
+
return num(BINARY_FUNC_IMPLS[e.name](l.value, r.value));
|
|
1153
|
+
return call2(e.name, l, r);
|
|
1154
|
+
}
|
|
1155
|
+
if (e.type === "pow") {
|
|
1156
|
+
const b = simplifyOnce(e.base);
|
|
1157
|
+
const p = simplifyOnce(e.exp);
|
|
1158
|
+
if (isConst(p, 0))
|
|
1159
|
+
return num(1);
|
|
1160
|
+
if (isConst(p, 1))
|
|
1161
|
+
return b;
|
|
1162
|
+
if (isConst(b, 0))
|
|
1163
|
+
return num(0);
|
|
1164
|
+
if (isConst(b, 1))
|
|
1165
|
+
return num(1);
|
|
1166
|
+
if (b.type === "const" && p.type === "const")
|
|
1167
|
+
return num(b.value ** p.value);
|
|
1168
|
+
return pow(b, p);
|
|
1169
|
+
}
|
|
1170
|
+
// Early returns (not switch cases below) for "cmp"/"piecewise" -- the
|
|
1171
|
+
// generic `(e as {left:Expr}).left` extraction a few lines down silently
|
|
1172
|
+
// no-ops for "cmp" (falls through to `return e`, discarding the simplified
|
|
1173
|
+
// children) and would crash at runtime for "piecewise" (no `.left` at
|
|
1174
|
+
// all), since neither is caught by the trailing `switch (e.type)` either.
|
|
1175
|
+
if (e.type === "cmp") {
|
|
1176
|
+
const l = simplifyOnce(e.left);
|
|
1177
|
+
const r = simplifyOnce(e.right);
|
|
1178
|
+
if (l.type === "const" && r.type === "const")
|
|
1179
|
+
return num(CMP_IMPLS[e.op](l.value, r.value) ? 1 : 0);
|
|
1180
|
+
return cmp(e.op, l, r);
|
|
1181
|
+
}
|
|
1182
|
+
if (e.type === "piecewise") {
|
|
1183
|
+
return piecewise(e.branches.map((br) => ({ cond: simplifyOnce(br.cond), expr: simplifyOnce(br.expr) })), simplifyOnce(e.otherwise));
|
|
1184
|
+
}
|
|
1185
|
+
if (e.type === "sum")
|
|
1186
|
+
return sumExpr(e.variable, simplifyOnce(e.from), simplifyOnce(e.to), simplifyOnce(e.body));
|
|
1187
|
+
if (e.type === "product") {
|
|
1188
|
+
return productExpr(e.variable, simplifyOnce(e.from), simplifyOnce(e.to), simplifyOnce(e.body));
|
|
1189
|
+
}
|
|
1190
|
+
const l = simplifyOnce(e.left);
|
|
1191
|
+
const r = simplifyOnce(e.right);
|
|
1192
|
+
if (l.type === "const" && r.type === "const") {
|
|
1193
|
+
if (e.type === "add")
|
|
1194
|
+
return num(l.value + r.value);
|
|
1195
|
+
if (e.type === "sub")
|
|
1196
|
+
return num(l.value - r.value);
|
|
1197
|
+
if (e.type === "mul")
|
|
1198
|
+
return num(l.value * r.value);
|
|
1199
|
+
if (e.type === "div" && r.value !== 0)
|
|
1200
|
+
return num(l.value / r.value);
|
|
1201
|
+
}
|
|
1202
|
+
switch (e.type) {
|
|
1203
|
+
case "add":
|
|
1204
|
+
if (isConst(l, 0))
|
|
1205
|
+
return r;
|
|
1206
|
+
if (isConst(r, 0))
|
|
1207
|
+
return l;
|
|
1208
|
+
return add(l, r);
|
|
1209
|
+
case "sub":
|
|
1210
|
+
if (isConst(r, 0))
|
|
1211
|
+
return l;
|
|
1212
|
+
if (isConst(l, 0))
|
|
1213
|
+
return neg(r);
|
|
1214
|
+
if (equal(l, r))
|
|
1215
|
+
return num(0);
|
|
1216
|
+
return sub(l, r);
|
|
1217
|
+
case "mul":
|
|
1218
|
+
if (isConst(l, 0) || isConst(r, 0))
|
|
1219
|
+
return num(0);
|
|
1220
|
+
if (isConst(l, 1))
|
|
1221
|
+
return r;
|
|
1222
|
+
if (isConst(r, 1))
|
|
1223
|
+
return l;
|
|
1224
|
+
return mul(l, r);
|
|
1225
|
+
case "div":
|
|
1226
|
+
if (isConst(l, 0))
|
|
1227
|
+
return num(0);
|
|
1228
|
+
if (isConst(r, 1))
|
|
1229
|
+
return l;
|
|
1230
|
+
return div(l, r);
|
|
1231
|
+
}
|
|
1232
|
+
return e;
|
|
1233
|
+
}
|
|
1234
|
+
function equal(a, b) {
|
|
1235
|
+
if (a.type !== b.type)
|
|
1236
|
+
return false;
|
|
1237
|
+
switch (a.type) {
|
|
1238
|
+
case "const":
|
|
1239
|
+
return a.value === b.value;
|
|
1240
|
+
case "var":
|
|
1241
|
+
return a.name === b.name;
|
|
1242
|
+
case "neg":
|
|
1243
|
+
return equal(a.arg, b.arg);
|
|
1244
|
+
case "func":
|
|
1245
|
+
return a.name === b.name && equal(a.arg, b.arg);
|
|
1246
|
+
case "call2":
|
|
1247
|
+
return (a.name === b.name && equal(a.left, b.left) && equal(a.right, b.right));
|
|
1248
|
+
case "pow":
|
|
1249
|
+
return equal(a.base, b.base) && equal(a.exp, b.exp);
|
|
1250
|
+
case "cmp": {
|
|
1251
|
+
const bb = b;
|
|
1252
|
+
return a.op === bb.op && equal(a.left, bb.left) && equal(a.right, bb.right);
|
|
1253
|
+
}
|
|
1254
|
+
case "piecewise": {
|
|
1255
|
+
const bb = b;
|
|
1256
|
+
return (a.branches.length === bb.branches.length &&
|
|
1257
|
+
a.branches.every((br, i) => equal(br.cond, bb.branches[i].cond) && equal(br.expr, bb.branches[i].expr)) &&
|
|
1258
|
+
equal(a.otherwise, bb.otherwise));
|
|
1259
|
+
}
|
|
1260
|
+
case "sum":
|
|
1261
|
+
case "product": {
|
|
1262
|
+
const bb = b;
|
|
1263
|
+
return a.variable === bb.variable && equal(a.from, bb.from) && equal(a.to, bb.to) && equal(a.body, bb.body);
|
|
1264
|
+
}
|
|
1265
|
+
default:
|
|
1266
|
+
return equal(a.left, b.left) && equal(a.right, b.right);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
// -- substitution -------------------------------------------------------------
|
|
1270
|
+
function subst(e, name, r) {
|
|
1271
|
+
switch (e.type) {
|
|
1272
|
+
case "const":
|
|
1273
|
+
return e;
|
|
1274
|
+
case "var":
|
|
1275
|
+
return e.name === name ? r : e;
|
|
1276
|
+
case "neg":
|
|
1277
|
+
return neg(subst(e.arg, name, r));
|
|
1278
|
+
case "func":
|
|
1279
|
+
return fn(e.name, subst(e.arg, name, r));
|
|
1280
|
+
case "call2":
|
|
1281
|
+
return call2(e.name, subst(e.left, name, r), subst(e.right, name, r));
|
|
1282
|
+
case "pow":
|
|
1283
|
+
return pow(subst(e.base, name, r), subst(e.exp, name, r));
|
|
1284
|
+
case "add":
|
|
1285
|
+
return add(subst(e.left, name, r), subst(e.right, name, r));
|
|
1286
|
+
case "sub":
|
|
1287
|
+
return sub(subst(e.left, name, r), subst(e.right, name, r));
|
|
1288
|
+
case "mul":
|
|
1289
|
+
return mul(subst(e.left, name, r), subst(e.right, name, r));
|
|
1290
|
+
case "div":
|
|
1291
|
+
return div(subst(e.left, name, r), subst(e.right, name, r));
|
|
1292
|
+
case "cmp":
|
|
1293
|
+
return cmp(e.op, subst(e.left, name, r), subst(e.right, name, r));
|
|
1294
|
+
case "piecewise":
|
|
1295
|
+
return piecewise(e.branches.map((br) => ({ cond: subst(br.cond, name, r), expr: subst(br.expr, name, r) })), subst(e.otherwise, name, r));
|
|
1296
|
+
case "sum":
|
|
1297
|
+
case "product": {
|
|
1298
|
+
// Capture-avoidance: e.variable is bound within e.body, so a
|
|
1299
|
+
// substitution targeting that same name must not reach inside it --
|
|
1300
|
+
// only from/to (and body, when name differs from the bound variable)
|
|
1301
|
+
// are substituted.
|
|
1302
|
+
const from = subst(e.from, name, r);
|
|
1303
|
+
const to = subst(e.to, name, r);
|
|
1304
|
+
const body = e.variable === name ? e.body : subst(e.body, name, r);
|
|
1305
|
+
return e.type === "sum" ? sumExpr(e.variable, from, to, body) : productExpr(e.variable, from, to, body);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
// -- expansion ----------------------------------------------------------------
|
|
1310
|
+
const MAX_EXPAND_POWER = 8;
|
|
1311
|
+
function expandOnce(e) {
|
|
1312
|
+
switch (e.type) {
|
|
1313
|
+
case "const":
|
|
1314
|
+
case "var":
|
|
1315
|
+
return e;
|
|
1316
|
+
case "neg":
|
|
1317
|
+
return neg(expandOnce(e.arg));
|
|
1318
|
+
case "func":
|
|
1319
|
+
return fn(e.name, expandOnce(e.arg));
|
|
1320
|
+
case "call2":
|
|
1321
|
+
return call2(e.name, expandOnce(e.left), expandOnce(e.right));
|
|
1322
|
+
case "add":
|
|
1323
|
+
return add(expandOnce(e.left), expandOnce(e.right));
|
|
1324
|
+
case "sub":
|
|
1325
|
+
return sub(expandOnce(e.left), expandOnce(e.right));
|
|
1326
|
+
case "div":
|
|
1327
|
+
return div(expandOnce(e.left), expandOnce(e.right));
|
|
1328
|
+
case "pow": {
|
|
1329
|
+
const base = expandOnce(e.base);
|
|
1330
|
+
if (e.exp.type === "const" &&
|
|
1331
|
+
Number.isInteger(e.exp.value) &&
|
|
1332
|
+
e.exp.value >= 2 &&
|
|
1333
|
+
e.exp.value <= MAX_EXPAND_POWER &&
|
|
1334
|
+
(base.type === "add" || base.type === "sub")) {
|
|
1335
|
+
let result = base;
|
|
1336
|
+
for (let i = 1; i < e.exp.value; i++)
|
|
1337
|
+
result = distribute(result, base);
|
|
1338
|
+
return result;
|
|
1339
|
+
}
|
|
1340
|
+
return pow(base, expandOnce(e.exp));
|
|
1341
|
+
}
|
|
1342
|
+
case "mul":
|
|
1343
|
+
return distribute(expandOnce(e.left), expandOnce(e.right));
|
|
1344
|
+
case "cmp":
|
|
1345
|
+
return cmp(e.op, expandOnce(e.left), expandOnce(e.right));
|
|
1346
|
+
case "piecewise":
|
|
1347
|
+
return piecewise(e.branches.map((br) => ({ cond: expandOnce(br.cond), expr: expandOnce(br.expr) })), expandOnce(e.otherwise));
|
|
1348
|
+
case "sum":
|
|
1349
|
+
return sumExpr(e.variable, expandOnce(e.from), expandOnce(e.to), expandOnce(e.body));
|
|
1350
|
+
case "product":
|
|
1351
|
+
return productExpr(e.variable, expandOnce(e.from), expandOnce(e.to), expandOnce(e.body));
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
/** Distribute a product over any top-level +/- structure on either side. */
|
|
1355
|
+
function distribute(l, r) {
|
|
1356
|
+
if (l.type === "add")
|
|
1357
|
+
return add(distribute(l.left, r), distribute(l.right, r));
|
|
1358
|
+
if (l.type === "sub")
|
|
1359
|
+
return sub(distribute(l.left, r), distribute(l.right, r));
|
|
1360
|
+
if (r.type === "add")
|
|
1361
|
+
return add(distribute(l, r.left), distribute(l, r.right));
|
|
1362
|
+
if (r.type === "sub")
|
|
1363
|
+
return sub(distribute(l, r.left), distribute(l, r.right));
|
|
1364
|
+
return mul(l, r);
|
|
1365
|
+
}
|
|
1366
|
+
function collectLikeTerms(e) {
|
|
1367
|
+
switch (e.type) {
|
|
1368
|
+
case "const":
|
|
1369
|
+
case "var":
|
|
1370
|
+
return e;
|
|
1371
|
+
case "func":
|
|
1372
|
+
return fn(e.name, collectLikeTerms(e.arg));
|
|
1373
|
+
case "call2":
|
|
1374
|
+
return call2(e.name, collectLikeTerms(e.left), collectLikeTerms(e.right));
|
|
1375
|
+
case "div": {
|
|
1376
|
+
const l = collectLikeTerms(e.left);
|
|
1377
|
+
const r = collectLikeTerms(e.right);
|
|
1378
|
+
// A monomial numerator over a nonzero-constant denominator goes
|
|
1379
|
+
// through the same coefficient-extraction path as a mul chain (see
|
|
1380
|
+
// flattenMonomial's div rule), so `(2*x^2)/2` folds to `x^2`. A
|
|
1381
|
+
// sum-shaped numerator stays a plain div -- distributing a constant
|
|
1382
|
+
// across a sum is expand's job, not simplify's. `rebuildAdditive`'s
|
|
1383
|
+
// reciprocal rendering keeps the no-op cases (e.g. `x^2/2`)
|
|
1384
|
+
// rendering back as the identical division they started as.
|
|
1385
|
+
if (r.type === "const" && r.value !== 0 && l.type !== "add" && l.type !== "sub") {
|
|
1386
|
+
return rebuildAdditive(groupTerms([flattenMonomial(div(l, r))]));
|
|
1387
|
+
}
|
|
1388
|
+
return div(l, r);
|
|
1389
|
+
}
|
|
1390
|
+
case "pow":
|
|
1391
|
+
return pow(collectLikeTerms(e.base), collectLikeTerms(e.exp));
|
|
1392
|
+
case "neg":
|
|
1393
|
+
case "add":
|
|
1394
|
+
case "sub":
|
|
1395
|
+
case "mul": {
|
|
1396
|
+
const terms = [];
|
|
1397
|
+
flattenAdditive(e, 1, terms);
|
|
1398
|
+
return rebuildAdditive(groupTerms(terms));
|
|
1399
|
+
}
|
|
1400
|
+
case "cmp":
|
|
1401
|
+
return cmp(e.op, collectLikeTerms(e.left), collectLikeTerms(e.right));
|
|
1402
|
+
case "piecewise":
|
|
1403
|
+
return piecewise(e.branches.map((br) => ({ cond: collectLikeTerms(br.cond), expr: collectLikeTerms(br.expr) })), collectLikeTerms(e.otherwise));
|
|
1404
|
+
case "sum":
|
|
1405
|
+
return sumExpr(e.variable, collectLikeTerms(e.from), collectLikeTerms(e.to), collectLikeTerms(e.body));
|
|
1406
|
+
case "product":
|
|
1407
|
+
return productExpr(e.variable, collectLikeTerms(e.from), collectLikeTerms(e.to), collectLikeTerms(e.body));
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
function flattenAdditive(e, sign, out) {
|
|
1411
|
+
if (e.type === "add") {
|
|
1412
|
+
flattenAdditive(e.left, sign, out);
|
|
1413
|
+
flattenAdditive(e.right, sign, out);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
if (e.type === "sub") {
|
|
1417
|
+
flattenAdditive(e.left, sign, out);
|
|
1418
|
+
flattenAdditive(e.right, -sign, out);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (e.type === "neg") {
|
|
1422
|
+
flattenAdditive(e.arg, -sign, out);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
const term = flattenMonomial(e);
|
|
1426
|
+
out.push({ coeff: term.coeff * sign, factors: term.factors });
|
|
1427
|
+
}
|
|
1428
|
+
function flattenMonomial(e) {
|
|
1429
|
+
if (e.type === "const")
|
|
1430
|
+
return { coeff: e.value, factors: [] };
|
|
1431
|
+
if (e.type === "neg") {
|
|
1432
|
+
const inner = flattenMonomial(e.arg);
|
|
1433
|
+
return { coeff: -inner.coeff, factors: inner.factors };
|
|
1434
|
+
}
|
|
1435
|
+
if (e.type === "mul") {
|
|
1436
|
+
const l = flattenMonomial(e.left);
|
|
1437
|
+
const r = flattenMonomial(e.right);
|
|
1438
|
+
return mergeFactors(l.coeff * r.coeff, [...l.factors, ...r.factors]);
|
|
1439
|
+
}
|
|
1440
|
+
// A division by a nonzero constant is just a fractional coefficient --
|
|
1441
|
+
// extracting it here (rather than treating the whole div as an opaque
|
|
1442
|
+
// atom, the old behavior) is what lets the surrounding term's constants
|
|
1443
|
+
// cancel against it: `2*(x^2/2)` -> `x^2`, `(2*x^2)/2` -> `x^2`,
|
|
1444
|
+
// `x/2 + x/2` -> `x`. A div by a non-constant stays opaque below.
|
|
1445
|
+
if (e.type === "div" && e.right.type === "const" && e.right.value !== 0) {
|
|
1446
|
+
const inner = flattenMonomial(e.left);
|
|
1447
|
+
return { coeff: inner.coeff / e.right.value, factors: inner.factors };
|
|
1448
|
+
}
|
|
1449
|
+
// a power with a constant exponent contributes a single (base, exp) factor;
|
|
1450
|
+
// the base itself is collected so nested sums inside it are also handled.
|
|
1451
|
+
if (e.type === "pow" && e.exp.type === "const") {
|
|
1452
|
+
return mergeFactors(1, [{ base: collectLikeTerms(e.base), exp: e.exp.value }]);
|
|
1453
|
+
}
|
|
1454
|
+
// opaque atom (var, func, div with a non-constant denominator, or pow with a non-constant exponent)
|
|
1455
|
+
return mergeFactors(1, [{ base: collectLikeTerms(e), exp: 1 }]);
|
|
1456
|
+
}
|
|
1457
|
+
function mergeFactors(coeff, factors) {
|
|
1458
|
+
const merged = [];
|
|
1459
|
+
for (const f of factors) {
|
|
1460
|
+
const existing = merged.find((m) => equal(m.base, f.base));
|
|
1461
|
+
if (existing)
|
|
1462
|
+
existing.exp += f.exp;
|
|
1463
|
+
else
|
|
1464
|
+
merged.push({ ...f });
|
|
1465
|
+
}
|
|
1466
|
+
return { coeff, factors: merged.filter((f) => f.exp !== 0) };
|
|
1467
|
+
}
|
|
1468
|
+
function groupTerms(terms) {
|
|
1469
|
+
const groups = new Map();
|
|
1470
|
+
const order = [];
|
|
1471
|
+
for (const t of terms) {
|
|
1472
|
+
const sorted = [...t.factors].sort((a, b) => render(a.base, 0).localeCompare(render(b.base, 0)));
|
|
1473
|
+
const key = sorted.map((f) => `${render(f.base, 0)}^${f.exp}`).join("*");
|
|
1474
|
+
const existing = groups.get(key);
|
|
1475
|
+
if (existing)
|
|
1476
|
+
existing.coeff += t.coeff;
|
|
1477
|
+
else {
|
|
1478
|
+
groups.set(key, { coeff: t.coeff, factors: sorted });
|
|
1479
|
+
order.push(key);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return order.map((k) => groups.get(k)).filter((t) => t.coeff !== 0);
|
|
1483
|
+
}
|
|
1484
|
+
function buildMonomial(factors) {
|
|
1485
|
+
let result = null;
|
|
1486
|
+
for (const f of factors) {
|
|
1487
|
+
const factorExpr = f.exp === 1 ? f.base : pow(f.base, num(f.exp));
|
|
1488
|
+
result = result === null ? factorExpr : mul(result, factorExpr);
|
|
1489
|
+
}
|
|
1490
|
+
return result;
|
|
1491
|
+
}
|
|
1492
|
+
function rebuildAdditive(terms) {
|
|
1493
|
+
let result = null;
|
|
1494
|
+
for (const t of terms) {
|
|
1495
|
+
const monomial = buildMonomial(t.factors);
|
|
1496
|
+
const magnitude = Math.abs(t.coeff);
|
|
1497
|
+
// A fractional coefficient whose reciprocal is an integer renders as a
|
|
1498
|
+
// division (`x/2`, not `0.5*x`) -- this is what keeps expressions that
|
|
1499
|
+
// entered the flattener already division-shaped (`-(x/2)`, `x^2/2`)
|
|
1500
|
+
// rendering back out in their original, conventional form now that
|
|
1501
|
+
// flattenMonomial extracts constant denominators into the coefficient.
|
|
1502
|
+
const reciprocal = magnitude !== 0 ? 1 / magnitude : 0;
|
|
1503
|
+
const asDivision = magnitude < 1 && Math.abs(Math.round(reciprocal) - reciprocal) < 1e-12;
|
|
1504
|
+
const positivePart = monomial === null
|
|
1505
|
+
? num(magnitude)
|
|
1506
|
+
: magnitude === 1
|
|
1507
|
+
? monomial
|
|
1508
|
+
: asDivision
|
|
1509
|
+
? div(monomial, num(Math.round(reciprocal)))
|
|
1510
|
+
: mul(num(magnitude), monomial);
|
|
1511
|
+
if (result === null) {
|
|
1512
|
+
result = t.coeff < 0 ? neg(positivePart) : positivePart;
|
|
1513
|
+
}
|
|
1514
|
+
else {
|
|
1515
|
+
result = t.coeff < 0 ? sub(result, positivePart) : add(result, positivePart);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return result ?? num(0);
|
|
1519
|
+
}
|
|
1520
|
+
// -- integration (elementary) ----------------------------------------------
|
|
1521
|
+
function integRules(e, x) {
|
|
1522
|
+
if (!containsVar(e, x))
|
|
1523
|
+
return mul(e, v(x)); // ∫c dx = c·x
|
|
1524
|
+
switch (e.type) {
|
|
1525
|
+
case "var":
|
|
1526
|
+
return div(pow(v(x), num(2)), num(2));
|
|
1527
|
+
case "add":
|
|
1528
|
+
return add(integ(e.left, x), integ(e.right, x));
|
|
1529
|
+
case "sub":
|
|
1530
|
+
return sub(integ(e.left, x), integ(e.right, x));
|
|
1531
|
+
case "neg":
|
|
1532
|
+
return neg(integ(e.arg, x));
|
|
1533
|
+
case "mul": {
|
|
1534
|
+
// pull out a constant factor
|
|
1535
|
+
if (!containsVar(e.left, x))
|
|
1536
|
+
return mul(e.left, integ(e.right, x));
|
|
1537
|
+
if (!containsVar(e.right, x))
|
|
1538
|
+
return mul(e.right, integ(e.left, x));
|
|
1539
|
+
// integration by parts: ∫ x^n · f(a·x + b) dx, f ∈ {sin, cos, exp}
|
|
1540
|
+
for (const [poly, other] of [
|
|
1541
|
+
[e.left, e.right],
|
|
1542
|
+
[e.right, e.left],
|
|
1543
|
+
]) {
|
|
1544
|
+
const n = xPowerDegree(poly, x);
|
|
1545
|
+
if (n !== null && other.type === "func" && isByPartsFunc(other.name) && linearCoeffs(other.arg, x)) {
|
|
1546
|
+
return ibpPolyTimesFunc(n, other, x);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
throw new NotIntegrableError();
|
|
1550
|
+
}
|
|
1551
|
+
case "div": {
|
|
1552
|
+
if (!containsVar(e.right, x))
|
|
1553
|
+
return div(integ(e.left, x), e.right); // ∫ u/c
|
|
1554
|
+
// c/x -> c*ln|x|, any constant c (not just literally 1). abs() because
|
|
1555
|
+
// the integrand 1/x is perfectly real for x < 0 where a naked ln(x)
|
|
1556
|
+
// would evaluate to NaN -- the textbook antiderivative is ln|x| + C.
|
|
1557
|
+
// The same reasoning drives every abs() below: wrap exactly where the
|
|
1558
|
+
// integrand is real but the ln argument can go negative; leave rules
|
|
1559
|
+
// whose ln argument is nonnegative whenever the integrand itself is
|
|
1560
|
+
// defined (∫ln(u)du, tanh's ln(cosh), atan's ln(1+u^2)) unwrapped.
|
|
1561
|
+
if (!containsVar(e.left, x) && e.right.type === "var" && e.right.name === x) {
|
|
1562
|
+
return mul(e.left, fn("ln", fn("abs", v(x))));
|
|
1563
|
+
}
|
|
1564
|
+
// numerator constant wrt x — check rational/radical forms with an x^2 denominator
|
|
1565
|
+
if (!containsVar(e.left, x)) {
|
|
1566
|
+
const c = evalExpr(e.left, {});
|
|
1567
|
+
// ∫ c / (x^2 + k) dx = (c/√k)·atan(x/√k) (k > 0, no linear term)
|
|
1568
|
+
const quad = quadraticCoeffs(e.right, x);
|
|
1569
|
+
if (quad && Math.abs(quad.a - 1) < 1e-9 && Math.abs(quad.b) < 1e-9 && quad.c > 0) {
|
|
1570
|
+
const s = Math.sqrt(quad.c);
|
|
1571
|
+
return mul(num(c / s), fn("atan", div(v(x), num(s))));
|
|
1572
|
+
}
|
|
1573
|
+
// ∫ c / √(k - x^2) dx = c·asin(x/√k) (k > 0)
|
|
1574
|
+
if (e.right.type === "func" && e.right.name === "sqrt") {
|
|
1575
|
+
const under = quadraticCoeffs(e.right.arg, x);
|
|
1576
|
+
if (under && Math.abs(under.a + 1) < 1e-9 && Math.abs(under.b) < 1e-9 && under.c > 0) {
|
|
1577
|
+
const s = Math.sqrt(under.c);
|
|
1578
|
+
return mul(num(c), fn("asin", div(v(x), num(s))));
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
// ∫ c / (a·x^2 + b·x + c0) dx, two distinct real roots (disc > 0):
|
|
1582
|
+
// partial fractions c/(a(x-r1)(x-r2)) = A/(x-r1) + B/(x-r2), giving
|
|
1583
|
+
// A·ln|x-r1| + B·ln|x-r2|. A = c/(a(r1-r2)), B = -A (re-derived and
|
|
1584
|
+
// numerically verified against d/dx of the result before trusting it).
|
|
1585
|
+
// abs() matters here even more than for c/x: evaluating anywhere
|
|
1586
|
+
// *between* the two roots makes exactly one factor negative, so a
|
|
1587
|
+
// naked ln silently returned NaN over that whole interval.
|
|
1588
|
+
const quad2 = quadraticCoeffs(e.right, x);
|
|
1589
|
+
if (quad2) {
|
|
1590
|
+
const disc = quad2.b * quad2.b - 4 * quad2.a * quad2.c;
|
|
1591
|
+
if (disc > 1e-9) {
|
|
1592
|
+
const sqrtDisc = Math.sqrt(disc);
|
|
1593
|
+
const r1 = (-quad2.b + sqrtDisc) / (2 * quad2.a);
|
|
1594
|
+
const r2 = (-quad2.b - sqrtDisc) / (2 * quad2.a);
|
|
1595
|
+
const A = c / (quad2.a * (r1 - r2));
|
|
1596
|
+
const B = -A;
|
|
1597
|
+
return add(mul(num(A), fn("ln", fn("abs", sub(v(x), num(r1))))), mul(num(B), fn("ln", fn("abs", sub(v(x), num(r2))))));
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
throw new NotIntegrableError();
|
|
1602
|
+
}
|
|
1603
|
+
case "pow": {
|
|
1604
|
+
// x^n (constant n)
|
|
1605
|
+
if (e.base.type === "var" && e.base.name === x && e.exp.type === "const") {
|
|
1606
|
+
if (e.exp.value === -1)
|
|
1607
|
+
return fn("ln", fn("abs", v(x))); // ∫x^-1 = ln|x|, same reasoning as the c/x rule
|
|
1608
|
+
return div(pow(v(x), num(e.exp.value + 1)), num(e.exp.value + 1));
|
|
1609
|
+
}
|
|
1610
|
+
throw new NotIntegrableError();
|
|
1611
|
+
}
|
|
1612
|
+
case "func": {
|
|
1613
|
+
// only elementary functions of a linear argument a·x + b
|
|
1614
|
+
const lin = linearCoeffs(e.arg, x);
|
|
1615
|
+
if (!lin)
|
|
1616
|
+
throw new NotIntegrableError();
|
|
1617
|
+
const inner = e.arg;
|
|
1618
|
+
const scale = (F) => div(F, num(lin.a));
|
|
1619
|
+
switch (e.name) {
|
|
1620
|
+
case "sin":
|
|
1621
|
+
return scale(neg(fn("cos", inner)));
|
|
1622
|
+
case "cos":
|
|
1623
|
+
return scale(fn("sin", inner));
|
|
1624
|
+
case "exp":
|
|
1625
|
+
return scale(fn("exp", inner));
|
|
1626
|
+
case "ln":
|
|
1627
|
+
// NO abs() here, unlike tan/cot/sec/csc below: the integrand ln(u)
|
|
1628
|
+
// is itself undefined for u <= 0, so the antiderivative's ln shares
|
|
1629
|
+
// the integrand's own domain -- wrapping it would silently turn
|
|
1630
|
+
// this into an antiderivative of ln|u|, a different integrand.
|
|
1631
|
+
return scale(sub(mul(inner, fn("ln", inner)), inner));
|
|
1632
|
+
case "sqrt":
|
|
1633
|
+
return scale(mul(num(2 / 3), pow(inner, num(1.5))));
|
|
1634
|
+
// tan/cot/sec/csc: the integrand is real wherever the ln argument
|
|
1635
|
+
// below is merely *negative* (e.g. tan(2) is real but cos(2) < 0),
|
|
1636
|
+
// so these need the textbook ln|...| form -- a naked ln returned NaN
|
|
1637
|
+
// over every such interval.
|
|
1638
|
+
case "tan":
|
|
1639
|
+
return scale(neg(fn("ln", fn("abs", fn("cos", inner)))));
|
|
1640
|
+
case "cot":
|
|
1641
|
+
return scale(fn("ln", fn("abs", fn("sin", inner))));
|
|
1642
|
+
case "sec":
|
|
1643
|
+
return scale(fn("ln", fn("abs", add(fn("sec", inner), fn("tan", inner)))));
|
|
1644
|
+
case "csc":
|
|
1645
|
+
return scale(neg(fn("ln", fn("abs", add(fn("csc", inner), fn("cot", inner))))));
|
|
1646
|
+
case "sinh":
|
|
1647
|
+
return scale(fn("cosh", inner));
|
|
1648
|
+
case "cosh":
|
|
1649
|
+
return scale(fn("sinh", inner));
|
|
1650
|
+
case "tanh":
|
|
1651
|
+
// No abs(): cosh(u) >= 1 always, the argument can never go negative.
|
|
1652
|
+
return scale(fn("ln", fn("cosh", inner)));
|
|
1653
|
+
case "asin":
|
|
1654
|
+
return scale(add(mul(inner, fn("asin", inner)), fn("sqrt", sub(num(1), pow(inner, num(2))))));
|
|
1655
|
+
case "acos":
|
|
1656
|
+
return scale(sub(mul(inner, fn("acos", inner)), fn("sqrt", sub(num(1), pow(inner, num(2))))));
|
|
1657
|
+
case "atan":
|
|
1658
|
+
return scale(sub(mul(inner, fn("atan", inner)), mul(num(0.5), fn("ln", add(num(1), pow(inner, num(2)))))));
|
|
1659
|
+
default:
|
|
1660
|
+
throw new NotIntegrableError();
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
default:
|
|
1664
|
+
throw new NotIntegrableError();
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
// -- u-substitution (fallback when integRules can't find a direct rule) ----
|
|
1668
|
+
/** Structural "does e contain target as an exact subtree" check, via {@link equal}. */
|
|
1669
|
+
function containsExpr(e, target) {
|
|
1670
|
+
if (equal(e, target))
|
|
1671
|
+
return true;
|
|
1672
|
+
switch (e.type) {
|
|
1673
|
+
case "const":
|
|
1674
|
+
case "var":
|
|
1675
|
+
return false;
|
|
1676
|
+
case "neg":
|
|
1677
|
+
return containsExpr(e.arg, target);
|
|
1678
|
+
case "func":
|
|
1679
|
+
return containsExpr(e.arg, target);
|
|
1680
|
+
case "pow":
|
|
1681
|
+
return containsExpr(e.base, target) || containsExpr(e.exp, target);
|
|
1682
|
+
case "piecewise":
|
|
1683
|
+
return (e.branches.some((br) => containsExpr(br.cond, target) || containsExpr(br.expr, target)) ||
|
|
1684
|
+
containsExpr(e.otherwise, target));
|
|
1685
|
+
// sum/product aren't u-substitution candidates or targets (integration
|
|
1686
|
+
// over them isn't supported -- see integRules's default case), so no
|
|
1687
|
+
// structural descent is needed beyond the equal() check above.
|
|
1688
|
+
case "sum":
|
|
1689
|
+
case "product":
|
|
1690
|
+
return false;
|
|
1691
|
+
default:
|
|
1692
|
+
return containsExpr(e.left, target) || containsExpr(e.right, target);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Structural subexpression replacement -- distinct from {@link subst}, which
|
|
1697
|
+
* only replaces a *named variable*, not an arbitrary subtree.
|
|
1698
|
+
*/
|
|
1699
|
+
function substExpr(e, target, replacement) {
|
|
1700
|
+
if (equal(e, target))
|
|
1701
|
+
return replacement;
|
|
1702
|
+
switch (e.type) {
|
|
1703
|
+
case "const":
|
|
1704
|
+
case "var":
|
|
1705
|
+
return e;
|
|
1706
|
+
case "neg":
|
|
1707
|
+
return neg(substExpr(e.arg, target, replacement));
|
|
1708
|
+
case "func":
|
|
1709
|
+
return fn(e.name, substExpr(e.arg, target, replacement));
|
|
1710
|
+
case "pow":
|
|
1711
|
+
return pow(substExpr(e.base, target, replacement), substExpr(e.exp, target, replacement));
|
|
1712
|
+
case "piecewise":
|
|
1713
|
+
return piecewise(e.branches.map((br) => ({
|
|
1714
|
+
cond: substExpr(br.cond, target, replacement),
|
|
1715
|
+
expr: substExpr(br.expr, target, replacement),
|
|
1716
|
+
})), substExpr(e.otherwise, target, replacement));
|
|
1717
|
+
// Not a substitution target (see containsExpr above) -- returned as-is.
|
|
1718
|
+
case "sum":
|
|
1719
|
+
case "product":
|
|
1720
|
+
return e;
|
|
1721
|
+
default:
|
|
1722
|
+
return { ...e, left: substExpr(e.left, target, replacement), right: substExpr(e.right, target, replacement) };
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
/** Flatten a chain of `mul` nodes into its (unordered) list of factors. */
|
|
1726
|
+
function flattenFactors(e) {
|
|
1727
|
+
if (e.type === "mul")
|
|
1728
|
+
return [...flattenFactors(e.left), ...flattenFactors(e.right)];
|
|
1729
|
+
return [e];
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Attempts `e / divisor` by canceling matching multiplicative factors
|
|
1733
|
+
* directly (structural match via {@link equal}, plus numeric-coefficient
|
|
1734
|
+
* division) -- {@link simplifyOnce}'s `div` case has no general polynomial-
|
|
1735
|
+
* factor cancellation for products, so `simplify(div(e, divisor))` alone
|
|
1736
|
+
* would never collapse e.g. `(2·x·sin(x²))/(2·x)` down to `sin(x²)`. Returns
|
|
1737
|
+
* `null` if `divisor`'s symbolic factors aren't all found in `e`'s.
|
|
1738
|
+
*
|
|
1739
|
+
* When `e` is itself a `div` node (e.g. `x/(x²+1)`), cancellation is
|
|
1740
|
+
* attempted only against `e`'s own *numerator* factors -- `e`'s denominator
|
|
1741
|
+
* is deliberately left untouched (not flattened/merged into the same
|
|
1742
|
+
* cancellation pool) and just carried through unchanged into the result's
|
|
1743
|
+
* denominator. This matters for u-substitution: `e`'s denominator is
|
|
1744
|
+
* typically the substitution candidate `g` itself, which needs to survive
|
|
1745
|
+
* intact in the quotient so the caller's later `substExpr(quotient, g, u)`
|
|
1746
|
+
* can still find and replace it -- if it got tangled up in cancellation
|
|
1747
|
+
* here, it might come out simplified into a form that no longer matches
|
|
1748
|
+
* `g` structurally.
|
|
1749
|
+
*/
|
|
1750
|
+
function divideOutFactors(e, divisor) {
|
|
1751
|
+
const eDen = e.type === "div" ? e.right : null;
|
|
1752
|
+
const eNumeratorSource = e.type === "div" ? e.left : e;
|
|
1753
|
+
let eNumeric = 1;
|
|
1754
|
+
const eSymbolic = [];
|
|
1755
|
+
for (const f of flattenFactors(eNumeratorSource)) {
|
|
1756
|
+
if (f.type === "const")
|
|
1757
|
+
eNumeric *= f.value;
|
|
1758
|
+
else
|
|
1759
|
+
eSymbolic.push(f);
|
|
1760
|
+
}
|
|
1761
|
+
let dNumeric = 1;
|
|
1762
|
+
const dSymbolic = [];
|
|
1763
|
+
for (const f of flattenFactors(divisor)) {
|
|
1764
|
+
if (f.type === "const")
|
|
1765
|
+
dNumeric *= f.value;
|
|
1766
|
+
else
|
|
1767
|
+
dSymbolic.push(f);
|
|
1768
|
+
}
|
|
1769
|
+
if (dNumeric === 0)
|
|
1770
|
+
return null;
|
|
1771
|
+
for (const df of dSymbolic) {
|
|
1772
|
+
const idx = eSymbolic.findIndex((ef) => equal(ef, df));
|
|
1773
|
+
if (idx === -1)
|
|
1774
|
+
return null;
|
|
1775
|
+
eSymbolic.splice(idx, 1);
|
|
1776
|
+
}
|
|
1777
|
+
const coeff = eNumeric / dNumeric;
|
|
1778
|
+
const factors = coeff === 1 ? eSymbolic : [num(coeff), ...eSymbolic];
|
|
1779
|
+
const numerator = factors.length === 0 ? num(coeff) : factors.reduce((acc, f) => (acc ? mul(acc, f) : f));
|
|
1780
|
+
return eDen ? div(numerator, eDen) : numerator;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Heuristic search for candidate "inner functions" g(x) for u-substitution:
|
|
1784
|
+
* function-call arguments, call2 operands, and pow base/exponent -- the
|
|
1785
|
+
* classic "spot the inner function" spots -- rather than every subexpression,
|
|
1786
|
+
* to keep this fast and avoid pathological false-positive matches. Dedupes
|
|
1787
|
+
* structurally-equal candidates and drops the trivial "g(x) = x" candidate.
|
|
1788
|
+
*/
|
|
1789
|
+
function collectSubstitutionCandidates(e, x) {
|
|
1790
|
+
const found = [];
|
|
1791
|
+
const visit = (node) => {
|
|
1792
|
+
switch (node.type) {
|
|
1793
|
+
case "const":
|
|
1794
|
+
case "var":
|
|
1795
|
+
return;
|
|
1796
|
+
case "neg":
|
|
1797
|
+
visit(node.arg);
|
|
1798
|
+
return;
|
|
1799
|
+
case "func":
|
|
1800
|
+
if (containsVar(node.arg, x))
|
|
1801
|
+
found.push(node.arg);
|
|
1802
|
+
visit(node.arg);
|
|
1803
|
+
return;
|
|
1804
|
+
case "pow":
|
|
1805
|
+
if (containsVar(node.base, x))
|
|
1806
|
+
found.push(node.base);
|
|
1807
|
+
if (containsVar(node.exp, x))
|
|
1808
|
+
found.push(node.exp);
|
|
1809
|
+
visit(node.base);
|
|
1810
|
+
visit(node.exp);
|
|
1811
|
+
return;
|
|
1812
|
+
case "call2":
|
|
1813
|
+
if (containsVar(node.left, x))
|
|
1814
|
+
found.push(node.left);
|
|
1815
|
+
if (containsVar(node.right, x))
|
|
1816
|
+
found.push(node.right);
|
|
1817
|
+
visit(node.left);
|
|
1818
|
+
visit(node.right);
|
|
1819
|
+
return;
|
|
1820
|
+
case "piecewise":
|
|
1821
|
+
for (const br of node.branches) {
|
|
1822
|
+
visit(br.cond);
|
|
1823
|
+
visit(br.expr);
|
|
1824
|
+
}
|
|
1825
|
+
visit(node.otherwise);
|
|
1826
|
+
return;
|
|
1827
|
+
// Not a u-substitution candidate site (see containsExpr above).
|
|
1828
|
+
case "sum":
|
|
1829
|
+
case "product":
|
|
1830
|
+
return;
|
|
1831
|
+
case "div":
|
|
1832
|
+
// The entire denominator is itself a classic u-substitution
|
|
1833
|
+
// candidate (e.g. u = x^2+1 for x/(x^2+1)) -- not just whatever's
|
|
1834
|
+
// nested inside it, which is all the generic default: branch below
|
|
1835
|
+
// would ever find via node.left/node.right.
|
|
1836
|
+
if (containsVar(node.right, x))
|
|
1837
|
+
found.push(node.right);
|
|
1838
|
+
visit(node.left);
|
|
1839
|
+
visit(node.right);
|
|
1840
|
+
return;
|
|
1841
|
+
default:
|
|
1842
|
+
visit(node.left);
|
|
1843
|
+
visit(node.right);
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
visit(e);
|
|
1848
|
+
const unique = [];
|
|
1849
|
+
for (const c of found) {
|
|
1850
|
+
if (c.type === "var" && c.name === x)
|
|
1851
|
+
continue;
|
|
1852
|
+
if (!unique.some((u) => equal(u, c)))
|
|
1853
|
+
unique.push(c);
|
|
1854
|
+
}
|
|
1855
|
+
return unique;
|
|
1856
|
+
}
|
|
1857
|
+
/** A variable name that appears nowhere in `e` and isn't `x` itself, for a temporary substitution variable. */
|
|
1858
|
+
function freshVariableName(e, x) {
|
|
1859
|
+
let name = "u";
|
|
1860
|
+
let i = 2;
|
|
1861
|
+
while (name === x || containsVar(e, name))
|
|
1862
|
+
name = `u${i++}`;
|
|
1863
|
+
return name;
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Defensive safety net for the heuristic u-substitution search below: confirm
|
|
1867
|
+
* that `d/dx candidate` actually matches the original integrand `e` at a
|
|
1868
|
+
* handful of probe points (same probe-point convention as
|
|
1869
|
+
* {@link polynomialCoeffs}), rather than ever trusting the substitution
|
|
1870
|
+
* blindly. Requires agreement at at least 2 of the 3 probe points (some may
|
|
1871
|
+
* legitimately hit a domain error/NaN on either side).
|
|
1872
|
+
*/
|
|
1873
|
+
function verifyByDifferentiation(candidate, e, x) {
|
|
1874
|
+
const check = Symbolic.simplify(diff(candidate, x));
|
|
1875
|
+
let agreements = 0;
|
|
1876
|
+
for (const p of [0.7, 1.3, -0.9]) {
|
|
1877
|
+
const expected = evalExpr(e, { [x]: p });
|
|
1878
|
+
const actual = evalExpr(check, { [x]: p });
|
|
1879
|
+
if (!Number.isFinite(expected) || !Number.isFinite(actual))
|
|
1880
|
+
continue;
|
|
1881
|
+
if (Math.abs(actual - expected) > 1e-6 * Math.max(1, Math.abs(expected)))
|
|
1882
|
+
return false;
|
|
1883
|
+
agreements++;
|
|
1884
|
+
}
|
|
1885
|
+
return agreements >= 2;
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Attempts to integrate `e` w.r.t. `x` via u-substitution: searches for a
|
|
1889
|
+
* candidate inner function g(x) such that `e = h(g(x))·g'(x)` for some `h`,
|
|
1890
|
+
* and if found, integrates `h(u)` and substitutes `g(x)` back in. Only a
|
|
1891
|
+
* single level of substitution is attempted for the transformed `h(u)`
|
|
1892
|
+
* problem (calls {@link integRules} directly, not {@link integ}, so this
|
|
1893
|
+
* doesn't recurse into another substitution attempt on the same
|
|
1894
|
+
* sub-problem); if multiple candidates verify successfully, the first one
|
|
1895
|
+
* found wins. Returns `null` (never throws) if no candidate works out, so
|
|
1896
|
+
* the caller can fall back to the original `NotIntegrableError`.
|
|
1897
|
+
*/
|
|
1898
|
+
function tryUSubstitution(e, x) {
|
|
1899
|
+
for (const g of collectSubstitutionCandidates(e, x)) {
|
|
1900
|
+
const gPrime = Symbolic.simplify(diff(g, x));
|
|
1901
|
+
if (isConst(gPrime, 0))
|
|
1902
|
+
continue;
|
|
1903
|
+
const quotient = Symbolic.simplify(divideOutFactors(e, gPrime) ?? div(e, gPrime));
|
|
1904
|
+
if (!containsExpr(quotient, g))
|
|
1905
|
+
continue;
|
|
1906
|
+
const uName = freshVariableName(quotient, x);
|
|
1907
|
+
const substituted = substExpr(quotient, g, v(uName));
|
|
1908
|
+
if (containsVar(substituted, x))
|
|
1909
|
+
continue;
|
|
1910
|
+
try {
|
|
1911
|
+
const antiderivativeInU = integRules(substituted, uName);
|
|
1912
|
+
const candidate = Symbolic.simplify(substExpr(antiderivativeInU, v(uName), g));
|
|
1913
|
+
if (verifyByDifferentiation(candidate, e, x))
|
|
1914
|
+
return candidate;
|
|
1915
|
+
}
|
|
1916
|
+
catch (err) {
|
|
1917
|
+
if (!(err instanceof NotIntegrableError))
|
|
1918
|
+
throw err;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Best-effort numeric detector for a pole strictly inside `(lower, upper)`
|
|
1925
|
+
* (used by {@link Symbolic.integrateDefinite}). Distinguishes a genuine
|
|
1926
|
+
* pole from a legitimate-but-steep bounded peak (e.g. a narrow Gaussian) by
|
|
1927
|
+
* repeatedly zooming a sampling window in on the current sample-max and
|
|
1928
|
+
* checking whether the observed maximum keeps growing as the window
|
|
1929
|
+
* narrows: near a true `1/(x-c)`-style singularity the maximum keeps
|
|
1930
|
+
* roughly multiplying each zoom-in pass (it grows as 1/distance-to-pole,
|
|
1931
|
+
* and each pass shrinks that distance by a consistent factor), whereas a
|
|
1932
|
+
* bounded peak's maximum converges and stops growing once the window is
|
|
1933
|
+
* smaller than the peak's own width. A single fixed-resolution sampling
|
|
1934
|
+
* pass can't tell these apart on its own -- a steep peak and a nearby-but-
|
|
1935
|
+
* finite sample near a pole can look identical at one resolution.
|
|
1936
|
+
*
|
|
1937
|
+
* Deliberately excludes a small margin (1% of the interval width) at each
|
|
1938
|
+
* end from every pass's sampling and zoom range -- an *endpoint* singularity
|
|
1939
|
+
* (e.g. `1/sqrt(x)` from 0 to 1) is the classic convergent improper
|
|
1940
|
+
* integral of the first kind, a legitimate and common case the
|
|
1941
|
+
* "`lower`/`upper` themselves" NON-GOAL wording deliberately excludes, and
|
|
1942
|
+
* without this margin the zoom would immediately walk right up to the
|
|
1943
|
+
* endpoint and flag it as if it were interior.
|
|
1944
|
+
*
|
|
1945
|
+
* NON-GOAL: this is a bounded-effort heuristic (6 zoom passes, 64 samples
|
|
1946
|
+
* each), not an analytic proof -- a sufficiently narrow singularity that
|
|
1947
|
+
* the first pass's coarse grid steps entirely over (landing nowhere near
|
|
1948
|
+
* it) can still be missed, since each pass zooms toward the *current*
|
|
1949
|
+
* sample-max rather than searching the whole interval again.
|
|
1950
|
+
*/
|
|
1951
|
+
function hasInteriorSingularity(f, lower, upper) {
|
|
1952
|
+
const lo = Math.min(lower, upper);
|
|
1953
|
+
const hi = Math.max(lower, upper);
|
|
1954
|
+
const margin = (hi - lo) * 0.01;
|
|
1955
|
+
let a = lo + margin;
|
|
1956
|
+
let b = hi - margin;
|
|
1957
|
+
const SAMPLES = 64;
|
|
1958
|
+
let prevMax = -Infinity;
|
|
1959
|
+
for (let pass = 0; pass < 6; pass++) {
|
|
1960
|
+
let maxAbs = -Infinity;
|
|
1961
|
+
let argmax = (a + b) / 2;
|
|
1962
|
+
for (let i = 1; i < SAMPLES; i++) {
|
|
1963
|
+
const x = a + (i / SAMPLES) * (b - a);
|
|
1964
|
+
const y = f(x);
|
|
1965
|
+
if (!Number.isFinite(y))
|
|
1966
|
+
return true;
|
|
1967
|
+
const m = Math.abs(y);
|
|
1968
|
+
if (m > maxAbs) {
|
|
1969
|
+
maxAbs = m;
|
|
1970
|
+
argmax = x;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (pass > 0 && maxAbs > prevMax * 4)
|
|
1974
|
+
return true;
|
|
1975
|
+
prevMax = maxAbs;
|
|
1976
|
+
const width = (b - a) / SAMPLES;
|
|
1977
|
+
a = Math.max(lo + margin, argmax - width * 2);
|
|
1978
|
+
b = Math.min(hi - margin, argmax + width * 2);
|
|
1979
|
+
if (b - a < 1e-9)
|
|
1980
|
+
return true;
|
|
1981
|
+
}
|
|
1982
|
+
return false;
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Symbolic anti-derivative dispatcher: tries the elementary rules in
|
|
1986
|
+
* {@link integRules} first, falling back to one attempt at
|
|
1987
|
+
* {@link tryUSubstitution} when those rules throw {@link NotIntegrableError}.
|
|
1988
|
+
*/
|
|
1989
|
+
function integ(e, x) {
|
|
1990
|
+
try {
|
|
1991
|
+
return integRules(e, x);
|
|
1992
|
+
}
|
|
1993
|
+
catch (err) {
|
|
1994
|
+
if (!(err instanceof NotIntegrableError))
|
|
1995
|
+
throw err;
|
|
1996
|
+
const bySubstitution = tryUSubstitution(e, x);
|
|
1997
|
+
if (bySubstitution)
|
|
1998
|
+
return bySubstitution;
|
|
1999
|
+
throw err;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
// -- closed-form ODE solving (Symbolic.solveOdeClosedForm) -----------------
|
|
2003
|
+
const isPureFunctionOf = (e, allowed) => [...collectFreeVars(e)].every((n) => allowed.has(n));
|
|
2004
|
+
/**
|
|
2005
|
+
* `dyDxExpr = g(x)*h(y)`? Includes the degenerate `h(y)=1` (dy/dx purely a
|
|
2006
|
+
* function of x) and `g(x)=1` (dy/dx purely a function of y) cases as the
|
|
2007
|
+
* same path, rather than special-casing them. Conservative: any free
|
|
2008
|
+
* variable other than `xVar`/`yVar` (an extra symbolic parameter) fails the
|
|
2009
|
+
* match rather than attempting to carry it through -- out of v1 scope.
|
|
2010
|
+
*/
|
|
2011
|
+
function matchSeparable(e, xVar, yVar) {
|
|
2012
|
+
const vars = collectFreeVars(e);
|
|
2013
|
+
if (![...vars].every((n) => n === xVar || n === yVar))
|
|
2014
|
+
return null;
|
|
2015
|
+
const hasX = vars.has(xVar);
|
|
2016
|
+
const hasY = vars.has(yVar);
|
|
2017
|
+
if (!hasY)
|
|
2018
|
+
return { g: e, h: num(1) };
|
|
2019
|
+
if (!hasX)
|
|
2020
|
+
return { g: num(1), h: e };
|
|
2021
|
+
if (e.type !== "mul")
|
|
2022
|
+
return null;
|
|
2023
|
+
const xOnly = new Set([xVar]);
|
|
2024
|
+
const yOnly = new Set([yVar]);
|
|
2025
|
+
if (isPureFunctionOf(e.left, xOnly) && isPureFunctionOf(e.right, yOnly))
|
|
2026
|
+
return { g: e.left, h: e.right };
|
|
2027
|
+
if (isPureFunctionOf(e.left, yOnly) && isPureFunctionOf(e.right, xOnly))
|
|
2028
|
+
return { g: e.right, h: e.left };
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* `dyDxExpr = q(x) - p(x)*y` (affine in y)? Detected via differentiation
|
|
2033
|
+
* rather than structural pattern-matching: if `e` is truly affine in `y`,
|
|
2034
|
+
* `∂e/∂y` is exactly `-p(x)` (a constant w.r.t. `y`) regardless of how `e`
|
|
2035
|
+
* is literally shaped (add/sub/mul combinations all collapse the same
|
|
2036
|
+
* way), which is more robust than trying to enumerate every equivalent
|
|
2037
|
+
* surface form of "affine in y".
|
|
2038
|
+
*/
|
|
2039
|
+
function matchLinearOde(e, xVar, yVar) {
|
|
2040
|
+
if (!containsVar(e, yVar))
|
|
2041
|
+
return null; // handled by matchSeparable's h(y)=1 case instead
|
|
2042
|
+
const negP = Symbolic.simplify(diff(e, yVar));
|
|
2043
|
+
if (containsVar(negP, yVar))
|
|
2044
|
+
return null; // not affine in y
|
|
2045
|
+
const q = Symbolic.simplify(sub(e, mul(negP, v(yVar))));
|
|
2046
|
+
if (containsVar(q, yVar))
|
|
2047
|
+
return null; // sanity check
|
|
2048
|
+
const p = Symbolic.simplify(neg(negP));
|
|
2049
|
+
// Conservative, matching matchSeparable's philosophy: p/q must depend on
|
|
2050
|
+
// no free variable other than xVar (an extra symbolic parameter, or a
|
|
2051
|
+
// stray reference to yVar that survived simplification, both bail out).
|
|
2052
|
+
const xOnly = new Set([xVar]);
|
|
2053
|
+
if (!isPureFunctionOf(p, xOnly) || !isPureFunctionOf(q, xOnly))
|
|
2054
|
+
return null;
|
|
2055
|
+
return { p, q };
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Tries to isolate `y` from `H(y) = rhs`, for the bounded set of shapes
|
|
2059
|
+
* {@link integ} can actually produce when integrating a pure function of a
|
|
2060
|
+
* single variable: `y` itself (h(y)=1 case), `ln|y|` (the `1/y -> ln|y|`
|
|
2061
|
+
* rule), or `y^p/p` (the power rule). NOT a general symbolic equation
|
|
2062
|
+
* solver -- returns `null` (surfacing as an implicit result) for anything
|
|
2063
|
+
* else, which is an explicitly allowed outcome per solveOdeClosedFrom's
|
|
2064
|
+
* contract.
|
|
2065
|
+
*
|
|
2066
|
+
* The `ln(abs(y))` case needs `y0`: |y| = e^rhs has two branches, y = ±e^rhs.
|
|
2067
|
+
* A separable solution can't cross y = 0 (that's where 1/h(y) blew up), so
|
|
2068
|
+
* the branch is pinned by the initial condition's own sign for the entire
|
|
2069
|
+
* solution: y = sign(y0)·e^rhs.
|
|
2070
|
+
*/
|
|
2071
|
+
function tryInvertForY(H, yVar, rhs, y0) {
|
|
2072
|
+
if (H.type === "var" && H.name === yVar)
|
|
2073
|
+
return rhs;
|
|
2074
|
+
if (H.type === "func" && H.name === "ln" && H.arg.type === "var" && H.arg.name === yVar) {
|
|
2075
|
+
return fn("exp", rhs);
|
|
2076
|
+
}
|
|
2077
|
+
if (H.type === "func" &&
|
|
2078
|
+
H.name === "ln" &&
|
|
2079
|
+
H.arg.type === "func" &&
|
|
2080
|
+
H.arg.name === "abs" &&
|
|
2081
|
+
H.arg.arg.type === "var" &&
|
|
2082
|
+
H.arg.arg.name === yVar) {
|
|
2083
|
+
const branch = Math.sign(y0);
|
|
2084
|
+
if (branch === 0)
|
|
2085
|
+
return null; // y0 = 0 is a degenerate initial condition for a 1/h(y) ~ 1/y separable form
|
|
2086
|
+
return branch === 1 ? fn("exp", rhs) : neg(fn("exp", rhs));
|
|
2087
|
+
}
|
|
2088
|
+
if (H.type === "div" &&
|
|
2089
|
+
H.right.type === "const" &&
|
|
2090
|
+
H.left.type === "pow" &&
|
|
2091
|
+
H.left.base.type === "var" &&
|
|
2092
|
+
H.left.base.name === yVar &&
|
|
2093
|
+
H.left.exp.type === "const" &&
|
|
2094
|
+
H.left.exp.value === H.right.value) {
|
|
2095
|
+
const p = H.right.value;
|
|
2096
|
+
if (p === 0)
|
|
2097
|
+
return null;
|
|
2098
|
+
return pow(mul(num(p), rhs), num(1 / p));
|
|
2099
|
+
}
|
|
2100
|
+
return null;
|
|
2101
|
+
}
|
|
2102
|
+
function solveSeparableOde(sep, xVar, yVar, x0, y0) {
|
|
2103
|
+
const invH = Symbolic.simplify(div(num(1), sep.h));
|
|
2104
|
+
let H;
|
|
2105
|
+
let G;
|
|
2106
|
+
try {
|
|
2107
|
+
H = Symbolic.simplify(integ(invH, yVar));
|
|
2108
|
+
G = Symbolic.simplify(integ(sep.g, xVar));
|
|
2109
|
+
}
|
|
2110
|
+
catch (err) {
|
|
2111
|
+
if (err instanceof NotIntegrableError)
|
|
2112
|
+
throw new NoClosedFormError();
|
|
2113
|
+
throw err;
|
|
2114
|
+
}
|
|
2115
|
+
const C = evalExpr(H, { [yVar]: y0 }) - evalExpr(G, { [xVar]: x0 });
|
|
2116
|
+
const rhs = Symbolic.simplify(add(G, num(C)));
|
|
2117
|
+
const explicitY = tryInvertForY(H, yVar, rhs, y0);
|
|
2118
|
+
if (explicitY)
|
|
2119
|
+
return { explicit: true, y: Symbolic.simplify(explicitY) };
|
|
2120
|
+
return { explicit: false, implicitRelation: Symbolic.simplify(sub(H, rhs)) };
|
|
2121
|
+
}
|
|
2122
|
+
function solveLinearOde(lin, xVar, _yVar, x0, y0) {
|
|
2123
|
+
let integralP;
|
|
2124
|
+
let mu;
|
|
2125
|
+
let muQIntegral;
|
|
2126
|
+
try {
|
|
2127
|
+
integralP = Symbolic.simplify(integ(lin.p, xVar));
|
|
2128
|
+
mu = Symbolic.simplify(fn("exp", integralP));
|
|
2129
|
+
muQIntegral = Symbolic.simplify(integ(Symbolic.simplify(mul(mu, lin.q)), xVar));
|
|
2130
|
+
}
|
|
2131
|
+
catch (err) {
|
|
2132
|
+
if (err instanceof NotIntegrableError)
|
|
2133
|
+
throw new NoClosedFormError();
|
|
2134
|
+
throw err;
|
|
2135
|
+
}
|
|
2136
|
+
// y = (∫μ(x)q(x)dx + C) / μ(x); solve for C from (x0, y0): C = y0*μ(x0) - ∫μq dx |_{x0}
|
|
2137
|
+
const muAtX0 = evalExpr(mu, { [xVar]: x0 });
|
|
2138
|
+
const muQIntegralAtX0 = evalExpr(muQIntegral, { [xVar]: x0 });
|
|
2139
|
+
const C = y0 * muAtX0 - muQIntegralAtX0;
|
|
2140
|
+
const y = Symbolic.simplify(div(add(muQIntegral, num(C)), mu));
|
|
2141
|
+
return { explicit: true, y };
|
|
2142
|
+
}
|
|
2143
|
+
/** A variable name distinct from every name in `avoid` and not free in `e`, for a temporary substitution variable (like {@link freshVariableName}, but avoiding a whole list of names rather than just one). */
|
|
2144
|
+
function freshVarAvoiding(e, avoid) {
|
|
2145
|
+
let name = "v";
|
|
2146
|
+
let i = 2;
|
|
2147
|
+
while (avoid.includes(name) || containsVar(e, name))
|
|
2148
|
+
name = `v${i++}`;
|
|
2149
|
+
return name;
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* `dyDxExpr = F(y/x)` (depends only on the ratio y/x)? Substitute `y -> v·x`
|
|
2153
|
+
* for a fresh `v`, then check whether the result is actually independent of
|
|
2154
|
+
* `x` -- NOT via `Symbolic.simplify` (which doesn't perform the
|
|
2155
|
+
* cross-term factor/cancellation this needs: e.g. `(x+v·x)/x` doesn't
|
|
2156
|
+
* auto-simplify to `1+v`), but numerically, the same probe-point
|
|
2157
|
+
* convention {@link verifyByDifferentiation}/`polynomialCoeffs` already use
|
|
2158
|
+
* elsewhere in this file: for each of a few `v` probe values, evaluate at
|
|
2159
|
+
* several different `x` probe values and require they all agree (within
|
|
2160
|
+
* tolerance; a probe pair that hits a domain error/NaN is skipped, not
|
|
2161
|
+
* treated as disagreement). If confirmed independent of `x`, `F(v)` is
|
|
2162
|
+
* obtained by literally substituting the concrete value `x=1` into the
|
|
2163
|
+
* expression tree -- a plain substitution, not something that depends on
|
|
2164
|
+
* `simplify` proving anything algebraically. By the time this runs, `e`
|
|
2165
|
+
* already failed both `matchSeparable` and `matchLinearOde`, and
|
|
2166
|
+
* `matchSeparable`'s own `hasX`/`hasY` degenerate branches mean `e` is
|
|
2167
|
+
* guaranteed to depend on both `xVar` and `yVar` already -- no separate
|
|
2168
|
+
* guard needed here.
|
|
2169
|
+
*/
|
|
2170
|
+
function matchHomogeneous(e, xVar, yVar) {
|
|
2171
|
+
const vName = freshVarAvoiding(e, [xVar, yVar]);
|
|
2172
|
+
const substituted = subst(e, yVar, mul(v(vName), v(xVar)));
|
|
2173
|
+
let agreements = 0;
|
|
2174
|
+
let attempts = 0;
|
|
2175
|
+
for (const vp of [0.6, 1.4, -0.8]) {
|
|
2176
|
+
let baseline = null;
|
|
2177
|
+
for (const xp of [1, 2.3, -1.7]) {
|
|
2178
|
+
attempts++;
|
|
2179
|
+
const val = evalExpr(substituted, { [xVar]: xp, [vName]: vp });
|
|
2180
|
+
if (!Number.isFinite(val))
|
|
2181
|
+
continue;
|
|
2182
|
+
if (baseline === null) {
|
|
2183
|
+
baseline = val;
|
|
2184
|
+
agreements++;
|
|
2185
|
+
}
|
|
2186
|
+
else if (Math.abs(val - baseline) > 1e-6 * Math.max(1, Math.abs(baseline))) {
|
|
2187
|
+
return null; // depends on x -- not homogeneous
|
|
2188
|
+
}
|
|
2189
|
+
else {
|
|
2190
|
+
agreements++;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
if (attempts === 0 || agreements < 4)
|
|
2195
|
+
return null; // not enough usable probes to trust the match
|
|
2196
|
+
const F = Symbolic.simplify(subst(substituted, xVar, num(1)));
|
|
2197
|
+
return { vName, F };
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* `y = v·x` transforms `dy/dx = F(v)` into `v + x·dv/dx = F(v)`, i.e.
|
|
2201
|
+
* `dv/dx = (1/x)·(F(v) - v)` -- separable in `x`/`v` with `g(x) = 1/x` and
|
|
2202
|
+
* `h(v) = F(v) - v` (NOT its reciprocal: {@link solveSeparableOde} itself
|
|
2203
|
+
* inverts `h` before integrating, since the separable ODE `dv/dx=g·h`
|
|
2204
|
+
* rearranges to `dv/h(v) = g(x)dx`). Reuses `solveSeparableOde` directly by
|
|
2205
|
+
* passing `vName` in place of the dependent-variable name it expects.
|
|
2206
|
+
*
|
|
2207
|
+
* `F(v) - v` simplifying to the exact constant 0 is a degenerate special
|
|
2208
|
+
* case (`dv/dx = 0` identically, i.e. `y = v0·x` for the constant
|
|
2209
|
+
* `v0 = y0/x0`) handled directly, since `solveSeparableOde` would otherwise
|
|
2210
|
+
* divide by zero when it inverts `h`.
|
|
2211
|
+
*
|
|
2212
|
+
* Back-substitution is NOT "replace `v` with `y/x` inside the result" --
|
|
2213
|
+
* `solveSeparableOde`'s explicit result is `v` already solved as a function
|
|
2214
|
+
* of `x` alone (no `vName` symbol remains in it), so the original `y` is
|
|
2215
|
+
* `v(x)·x` directly. Only the *implicit*-relation branch still literally
|
|
2216
|
+
* contains `vName` and needs that substitution.
|
|
2217
|
+
*/
|
|
2218
|
+
function solveHomogeneousOde(hom, xVar, yVar, x0, y0) {
|
|
2219
|
+
const { vName, F } = hom;
|
|
2220
|
+
const v0 = y0 / x0;
|
|
2221
|
+
const FMinusV = Symbolic.simplify(sub(F, v(vName)));
|
|
2222
|
+
if (FMinusV.type === "const" && FMinusV.value === 0) {
|
|
2223
|
+
return { explicit: true, y: Symbolic.simplify(mul(num(v0), v(xVar))) };
|
|
2224
|
+
}
|
|
2225
|
+
const sepResult = solveSeparableOde({ g: div(num(1), v(xVar)), h: FMinusV }, xVar, vName, x0, v0);
|
|
2226
|
+
if (sepResult.explicit && sepResult.y) {
|
|
2227
|
+
return { explicit: true, y: Symbolic.simplify(mul(sepResult.y, v(xVar))) };
|
|
2228
|
+
}
|
|
2229
|
+
const implicitRelation = Symbolic.simplify(subst(sepResult.implicitRelation, vName, div(v(yVar), v(xVar))));
|
|
2230
|
+
return { explicit: false, implicitRelation };
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Recognizes a single additive term as `coef(x)·y^n` (or `coef(x)·y` when
|
|
2234
|
+
* `n` is omitted, i.e. `n=1`, or bare `y`/`y^n` when `coef` is omitted,
|
|
2235
|
+
* i.e. `coef=1`) -- the building block {@link matchBernoulli} needs for
|
|
2236
|
+
* both the `y^n` term (n != 0, 1) and the plain `y^1` term of
|
|
2237
|
+
* `dy/dx = q(x)·y^n - p(x)·y`. `coef` must be a pure function of `xVar`
|
|
2238
|
+
* only (no `yVar`, no other free variables) -- same conservatism
|
|
2239
|
+
* `matchSeparable`/`matchLinearOde` already use.
|
|
2240
|
+
*/
|
|
2241
|
+
function matchPureXTimesYPow(term, xVar, yVar) {
|
|
2242
|
+
const isYVar = (t) => t.type === "var" && t.name === yVar;
|
|
2243
|
+
const asYPow = (t) => {
|
|
2244
|
+
if (isYVar(t))
|
|
2245
|
+
return 1;
|
|
2246
|
+
if (t.type === "pow" && isYVar(t.base) && t.exp.type === "const")
|
|
2247
|
+
return t.exp.value;
|
|
2248
|
+
return null;
|
|
2249
|
+
};
|
|
2250
|
+
const xOnly = new Set([xVar]);
|
|
2251
|
+
if (term.type !== "mul") {
|
|
2252
|
+
const n = asYPow(term);
|
|
2253
|
+
return n === null ? null : { coef: num(1), n };
|
|
2254
|
+
}
|
|
2255
|
+
for (const [coef, yPart] of [
|
|
2256
|
+
[term.left, term.right],
|
|
2257
|
+
[term.right, term.left],
|
|
2258
|
+
]) {
|
|
2259
|
+
if (!isPureFunctionOf(coef, xOnly))
|
|
2260
|
+
continue;
|
|
2261
|
+
const n = asYPow(yPart);
|
|
2262
|
+
if (n !== null)
|
|
2263
|
+
return { coef, n };
|
|
2264
|
+
}
|
|
2265
|
+
return null;
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* `dyDxExpr = q(x)·y^n - p(x)·y` (n != 0, 1 -- those degenerate to
|
|
2269
|
+
* separable/linear, already handled earlier)? `e` must be `add`/`sub` of
|
|
2270
|
+
* exactly two terms, one matching the `y^n` shape and the other the plain
|
|
2271
|
+
* `y^1` shape (either order). Converts both to a canonical signed-term
|
|
2272
|
+
* form first (`e = termA + signA·termB`... really `e = signA·termA +
|
|
2273
|
+
* signB·termB` with `signA=+1` always and `signB` = -1 for `sub`, +1 for
|
|
2274
|
+
* `add`) so the sign bookkeeping for `q`/`p` is one formula rather than
|
|
2275
|
+
* four separate cases: whichever term matches `y^n` (n != 1) contributes
|
|
2276
|
+
* `q(x) = sign·coef`; whichever matches plain `y` contributes
|
|
2277
|
+
* `p(x) = -sign·coef`. Verified against all four sub/add x left/right
|
|
2278
|
+
* orderings by hand before relying on it.
|
|
2279
|
+
*/
|
|
2280
|
+
function matchBernoulli(e, xVar, yVar) {
|
|
2281
|
+
let termA;
|
|
2282
|
+
let termB;
|
|
2283
|
+
let signB;
|
|
2284
|
+
if (e.type === "sub") {
|
|
2285
|
+
termA = e.left;
|
|
2286
|
+
termB = e.right;
|
|
2287
|
+
signB = -1;
|
|
2288
|
+
}
|
|
2289
|
+
else if (e.type === "add") {
|
|
2290
|
+
termA = e.left;
|
|
2291
|
+
termB = e.right;
|
|
2292
|
+
signB = 1;
|
|
2293
|
+
}
|
|
2294
|
+
else {
|
|
2295
|
+
return null;
|
|
2296
|
+
}
|
|
2297
|
+
const matchA = matchPureXTimesYPow(termA, xVar, yVar);
|
|
2298
|
+
const matchB = matchPureXTimesYPow(termB, xVar, yVar);
|
|
2299
|
+
if (!matchA || !matchB)
|
|
2300
|
+
return null;
|
|
2301
|
+
let powMatch;
|
|
2302
|
+
let powSign;
|
|
2303
|
+
let linMatch;
|
|
2304
|
+
let linSign;
|
|
2305
|
+
if (matchA.n !== 1 && matchB.n === 1) {
|
|
2306
|
+
powMatch = matchA;
|
|
2307
|
+
powSign = 1;
|
|
2308
|
+
linMatch = matchB;
|
|
2309
|
+
linSign = signB;
|
|
2310
|
+
}
|
|
2311
|
+
else if (matchB.n !== 1 && matchA.n === 1) {
|
|
2312
|
+
powMatch = matchB;
|
|
2313
|
+
powSign = signB;
|
|
2314
|
+
linMatch = matchA;
|
|
2315
|
+
linSign = 1;
|
|
2316
|
+
}
|
|
2317
|
+
else {
|
|
2318
|
+
return null;
|
|
2319
|
+
}
|
|
2320
|
+
if (powMatch.n === 0)
|
|
2321
|
+
return null; // y^0=1: really just linear, matchLinearOde's job
|
|
2322
|
+
const q = powSign === 1 ? powMatch.coef : Symbolic.simplify(neg(powMatch.coef));
|
|
2323
|
+
const p = linSign === 1 ? Symbolic.simplify(neg(linMatch.coef)) : linMatch.coef;
|
|
2324
|
+
return { p, q, n: powMatch.n };
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Bernoulli's substitution `w = y^(1-n)` transforms
|
|
2328
|
+
* `dy/dx + p(x)·y = q(x)·y^n` into the LINEAR first-order equation
|
|
2329
|
+
* `dw/dx + (1-n)·p(x)·w = (1-n)·q(x)` -- reuses {@link solveLinearOde}
|
|
2330
|
+
* directly for that transformed equation (its third parameter, the
|
|
2331
|
+
* dependent-variable name, is never actually read inside
|
|
2332
|
+
* `solveLinearOde`'s own body, so no fresh-name bookkeeping is needed
|
|
2333
|
+
* here). `solveLinearOde` never returns an implicit result (only throws or
|
|
2334
|
+
* returns explicit), so `y = w^(1/(1-n))` is always immediately explicit
|
|
2335
|
+
* too -- no further inversion step is possible or needed. A negative `y0`
|
|
2336
|
+
* with a non-integer `1/(1-n)` produces a non-finite `w0`/result, a
|
|
2337
|
+
* genuine restriction of raising a negative number to a fractional power,
|
|
2338
|
+
* not a bug to special-case around.
|
|
2339
|
+
*/
|
|
2340
|
+
function solveBernoulliOde(bern, xVar, yVar, x0, y0) {
|
|
2341
|
+
const oneMinusN = 1 - bern.n;
|
|
2342
|
+
const w0 = y0 ** oneMinusN;
|
|
2343
|
+
const transformedP = Symbolic.simplify(mul(num(oneMinusN), bern.p));
|
|
2344
|
+
const transformedQ = Symbolic.simplify(mul(num(oneMinusN), bern.q));
|
|
2345
|
+
const wResult = solveLinearOde({ p: transformedP, q: transformedQ }, xVar, yVar, x0, w0);
|
|
2346
|
+
const y = Symbolic.simplify(pow(wResult.y, num(1 / oneMinusN)));
|
|
2347
|
+
return { explicit: true, y };
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* `dyDxExpr = P/Q` where `M dx + N dy = 0` (with `M=P`, `N=-Q`) is an
|
|
2351
|
+
* exact differential -- i.e. `∂M/∂y ≡ ∂N/∂x`? Only attempted for a
|
|
2352
|
+
* top-level `div` node, since that's the one shape where the M/N
|
|
2353
|
+
* decomposition is unambiguous. The exactness identity is verified
|
|
2354
|
+
* numerically over a small (x, y) probe grid rather than symbolically
|
|
2355
|
+
* (the same conservative probe-point convention `matchHomogeneous`/
|
|
2356
|
+
* `verifyByDifferentiation` use elsewhere in this file: `simplify` can't
|
|
2357
|
+
* always prove two derivative expressions equal even when they are), with
|
|
2358
|
+
* non-finite probe pairs skipped and a minimum number of successful
|
|
2359
|
+
* agreements required before the match is trusted. Any free variable
|
|
2360
|
+
* besides `xVar`/`yVar` bails, matching every other matcher's philosophy.
|
|
2361
|
+
*/
|
|
2362
|
+
function matchExact(e, xVar, yVar) {
|
|
2363
|
+
if (e.type !== "div")
|
|
2364
|
+
return null;
|
|
2365
|
+
const vars = collectFreeVars(e);
|
|
2366
|
+
if (![...vars].every((n) => n === xVar || n === yVar))
|
|
2367
|
+
return null;
|
|
2368
|
+
const M = e.left;
|
|
2369
|
+
const N = Symbolic.simplify(neg(e.right));
|
|
2370
|
+
const dMdy = diff(M, yVar);
|
|
2371
|
+
const dNdx = diff(N, xVar);
|
|
2372
|
+
let agreements = 0;
|
|
2373
|
+
for (const xp of [0.7, 1.3, -0.9]) {
|
|
2374
|
+
for (const yp of [0.8, -1.2, 1.7]) {
|
|
2375
|
+
const a = evalExpr(dMdy, { [xVar]: xp, [yVar]: yp });
|
|
2376
|
+
const b = evalExpr(dNdx, { [xVar]: xp, [yVar]: yp });
|
|
2377
|
+
if (!Number.isFinite(a) || !Number.isFinite(b))
|
|
2378
|
+
continue;
|
|
2379
|
+
if (Math.abs(a - b) > 1e-6 * Math.max(1, Math.abs(a), Math.abs(b))) {
|
|
2380
|
+
return null; // ∂M/∂y != ∂N/∂x -- not exact
|
|
2381
|
+
}
|
|
2382
|
+
agreements++;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (agreements < 4)
|
|
2386
|
+
return null; // not enough usable probes to trust the match
|
|
2387
|
+
return { M, N };
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Constructs the potential function `F(x,y)` with `∂F/∂x = M`,
|
|
2391
|
+
* `∂F/∂y = N`: `F₀ = ∫M dx` (y held as a symbolic constant -- `integ`'s
|
|
2392
|
+
* `containsVar` machinery already treats any non-integration variable
|
|
2393
|
+
* that way), then `g'(y) = N - ∂F₀/∂y`, `g = ∫g' dy`, `F = F₀ + g`.
|
|
2394
|
+
* The solution is the level set through the initial condition:
|
|
2395
|
+
* `F(x,y) = F(x0,y0)`.
|
|
2396
|
+
*
|
|
2397
|
+
* `g'` is mathematically a pure function of `y` whenever the equation is
|
|
2398
|
+
* genuinely exact (which {@link matchExact} just verified numerically), but
|
|
2399
|
+
* `simplify` may fail to cancel the `x` terms *symbolically* -- in that
|
|
2400
|
+
* case `x`-independence is re-verified numerically and a concrete probe
|
|
2401
|
+
* value is substituted for `x`, mirroring how `matchHomogeneous` extracts
|
|
2402
|
+
* `F(v)` by substituting `x=1` rather than trusting algebraic
|
|
2403
|
+
* cancellation. If no finite probe substitution exists, the closed form
|
|
2404
|
+
* can't be constructed reliably -- {@link NoClosedFormError}, same as an
|
|
2405
|
+
* integral outside {@link integ}'s coverage.
|
|
2406
|
+
*
|
|
2407
|
+
* The result is usually implicit (`F` genuinely mixes `x` and `y`; a
|
|
2408
|
+
* cleanly-splitting `F` would almost always have been caught by the
|
|
2409
|
+
* separable path first). The one cheap explicit attempt made: when `F`
|
|
2410
|
+
* splits as `add`/`sub` of a pure-`y` part and a pure-`x` part,
|
|
2411
|
+
* {@link tryInvertForY} is tried on the pure-`y` side.
|
|
2412
|
+
*/
|
|
2413
|
+
function solveExactOde(exact, xVar, yVar, x0, y0) {
|
|
2414
|
+
const { M, N } = exact;
|
|
2415
|
+
let F0;
|
|
2416
|
+
let g;
|
|
2417
|
+
let gPrime;
|
|
2418
|
+
try {
|
|
2419
|
+
F0 = Symbolic.simplify(integ(M, xVar));
|
|
2420
|
+
gPrime = Symbolic.simplify(sub(N, diff(F0, yVar)));
|
|
2421
|
+
if (containsVar(gPrime, xVar)) {
|
|
2422
|
+
// Symbolic cancellation fell short -- verify x-independence
|
|
2423
|
+
// numerically, then pin x to a probe value that evaluates finitely.
|
|
2424
|
+
let pinned = null;
|
|
2425
|
+
for (const xp of [1, 2.3, -1.7]) {
|
|
2426
|
+
const atProbe = evalExpr(gPrime, { [xVar]: xp, [yVar]: 0.8 });
|
|
2427
|
+
const atOther = evalExpr(gPrime, { [xVar]: xp === 1 ? 2.3 : 1, [yVar]: 0.8 });
|
|
2428
|
+
if (!Number.isFinite(atProbe) || !Number.isFinite(atOther))
|
|
2429
|
+
continue;
|
|
2430
|
+
if (Math.abs(atProbe - atOther) > 1e-6 * Math.max(1, Math.abs(atProbe))) {
|
|
2431
|
+
throw new NoClosedFormError();
|
|
2432
|
+
}
|
|
2433
|
+
pinned = Symbolic.simplify(subst(gPrime, xVar, num(xp)));
|
|
2434
|
+
break;
|
|
2435
|
+
}
|
|
2436
|
+
if (!pinned)
|
|
2437
|
+
throw new NoClosedFormError();
|
|
2438
|
+
gPrime = pinned;
|
|
2439
|
+
}
|
|
2440
|
+
g = Symbolic.simplify(integ(gPrime, yVar));
|
|
2441
|
+
}
|
|
2442
|
+
catch (err) {
|
|
2443
|
+
if (err instanceof NotIntegrableError)
|
|
2444
|
+
throw new NoClosedFormError();
|
|
2445
|
+
throw err;
|
|
2446
|
+
}
|
|
2447
|
+
const F = Symbolic.simplify(add(F0, g));
|
|
2448
|
+
const C = evalExpr(F, { [xVar]: x0, [yVar]: y0 });
|
|
2449
|
+
if (!Number.isFinite(C))
|
|
2450
|
+
throw new NoClosedFormError();
|
|
2451
|
+
// Cheap explicit attempt: F = H(y) + G(x) (either order) inverts via the
|
|
2452
|
+
// same bounded shapes the separable path uses.
|
|
2453
|
+
if (F.type === "add" || F.type === "sub") {
|
|
2454
|
+
const xOnly = new Set([xVar]);
|
|
2455
|
+
const yOnly = new Set([yVar]);
|
|
2456
|
+
const [a, b] = [F.left, F.right];
|
|
2457
|
+
for (const [yPart, xPartSigned] of [
|
|
2458
|
+
[a, F.type === "sub" ? neg(b) : b],
|
|
2459
|
+
[b, F.type === "sub" ? neg(a) : a],
|
|
2460
|
+
]) {
|
|
2461
|
+
if (!isPureFunctionOf(yPart, yOnly) || !isPureFunctionOf(xPartSigned, xOnly))
|
|
2462
|
+
continue;
|
|
2463
|
+
if (F.type === "sub" && yPart === b)
|
|
2464
|
+
continue; // F = G(x) - H(y): inverting -H is out of tryInvertForY's shapes
|
|
2465
|
+
const rhs = Symbolic.simplify(sub(num(C), xPartSigned));
|
|
2466
|
+
const explicitY = tryInvertForY(yPart, yVar, rhs, y0);
|
|
2467
|
+
if (explicitY)
|
|
2468
|
+
return { explicit: true, y: Symbolic.simplify(explicitY) };
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
return { explicit: false, implicitRelation: Symbolic.simplify(sub(F, num(C))) };
|
|
2472
|
+
}
|
|
2473
|
+
/** If `e` is `a·x + b` (a, b constant), return `{ a, b }`, else `null`. */
|
|
2474
|
+
function linearCoeffs(e, x) {
|
|
2475
|
+
try {
|
|
2476
|
+
const at0 = evalExpr(e, { [x]: 0 });
|
|
2477
|
+
const at1 = evalExpr(e, { [x]: 1 });
|
|
2478
|
+
const a = at1 - at0;
|
|
2479
|
+
const b = at0;
|
|
2480
|
+
// verify linearity at a third point
|
|
2481
|
+
const at2 = evalExpr(e, { [x]: 2 });
|
|
2482
|
+
if (Math.abs(at2 - (2 * a + b)) > 1e-9)
|
|
2483
|
+
return null;
|
|
2484
|
+
if (!Number.isFinite(a) || !Number.isFinite(b))
|
|
2485
|
+
return null;
|
|
2486
|
+
return { a, b };
|
|
2487
|
+
}
|
|
2488
|
+
catch {
|
|
2489
|
+
return null;
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
// -- series/summation --------------------------------------------------------
|
|
2493
|
+
/**
|
|
2494
|
+
* Recognizes `e` as a geometric series term `coeff · base^(a·variable + b)`
|
|
2495
|
+
* (`coeff` and `base` constant w.r.t. `variable`; `coeff` defaults to 1 if
|
|
2496
|
+
* `e` is a bare `pow` node). Reuses {@link linearCoeffs} on the exponent, so
|
|
2497
|
+
* shifted/scaled exponents are handled too (e.g. `r^(2n+1)` is still
|
|
2498
|
+
* geometric, with ratio `r^2`). Returns the closed-form sum
|
|
2499
|
+
* `Σ_{k=from}^∞ coeff·base^(a·k+b)` if it converges (`|base^a| < 1`), else
|
|
2500
|
+
* `null` (not recognized as geometric, or doesn't converge).
|
|
2501
|
+
*/
|
|
2502
|
+
function tryGeometricSeriesClosedForm(e, variable, from, env) {
|
|
2503
|
+
let coeff = 1;
|
|
2504
|
+
let powNode = e;
|
|
2505
|
+
if (e.type === "mul") {
|
|
2506
|
+
if (!containsVar(e.left, variable) && e.right.type === "pow") {
|
|
2507
|
+
coeff = evalExpr(e.left, env);
|
|
2508
|
+
powNode = e.right;
|
|
2509
|
+
}
|
|
2510
|
+
else if (!containsVar(e.right, variable) && e.left.type === "pow") {
|
|
2511
|
+
coeff = evalExpr(e.right, env);
|
|
2512
|
+
powNode = e.left;
|
|
2513
|
+
}
|
|
2514
|
+
else {
|
|
2515
|
+
return null;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
else if (e.type !== "pow") {
|
|
2519
|
+
return null;
|
|
2520
|
+
}
|
|
2521
|
+
if (powNode.type !== "pow" || containsVar(powNode.base, variable))
|
|
2522
|
+
return null;
|
|
2523
|
+
const lin = linearCoeffs(powNode.exp, variable);
|
|
2524
|
+
if (!lin)
|
|
2525
|
+
return null;
|
|
2526
|
+
const base = evalExpr(powNode.base, env);
|
|
2527
|
+
const ratio = base ** lin.a;
|
|
2528
|
+
if (!Number.isFinite(ratio) || Math.abs(ratio) >= 1)
|
|
2529
|
+
return null;
|
|
2530
|
+
const firstExponent = lin.a * from + lin.b;
|
|
2531
|
+
const firstTerm = coeff * base ** firstExponent;
|
|
2532
|
+
return firstTerm / (1 - ratio);
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Numeric partial-sum fallback for {@link Symbolic.sumSeries} when no closed
|
|
2536
|
+
* form is recognized: sums terms until several consecutive terms fall below
|
|
2537
|
+
* a tolerance relative to the running total. If that per-term check alone
|
|
2538
|
+
* doesn't confirm convergence within the term budget, tries
|
|
2539
|
+
* {@link estimateConvergentTail} on the remainder before throwing
|
|
2540
|
+
* {@link SeriesDivergesError} -- which still throws if the terms are
|
|
2541
|
+
* visibly growing instead of shrinking, or if neither check confirms
|
|
2542
|
+
* convergence.
|
|
2543
|
+
*/
|
|
2544
|
+
function sumSeriesNumeric(e, variable, from, env) {
|
|
2545
|
+
const f = compileExpr(e);
|
|
2546
|
+
const tolerance = 1e-12;
|
|
2547
|
+
const maxTerms = 200_000;
|
|
2548
|
+
let total = 0;
|
|
2549
|
+
let consecutiveSmall = 0;
|
|
2550
|
+
let firstTermMag = 0;
|
|
2551
|
+
for (let i = 0; i < maxTerms; i++) {
|
|
2552
|
+
const k = from + i;
|
|
2553
|
+
const term = f({ ...env, [variable]: k });
|
|
2554
|
+
if (!Number.isFinite(term))
|
|
2555
|
+
throw new SeriesDivergesError(`sumSeries: term at index ${k} is not finite.`);
|
|
2556
|
+
if (i === 0)
|
|
2557
|
+
firstTermMag = Math.abs(term) || 1;
|
|
2558
|
+
total += term;
|
|
2559
|
+
if (Math.abs(term) < tolerance * Math.max(1, Math.abs(total))) {
|
|
2560
|
+
consecutiveSmall++;
|
|
2561
|
+
if (consecutiveSmall >= 5)
|
|
2562
|
+
return total;
|
|
2563
|
+
}
|
|
2564
|
+
else {
|
|
2565
|
+
consecutiveSmall = 0;
|
|
2566
|
+
}
|
|
2567
|
+
if (i > 100 && Math.abs(term) > firstTermMag * 10) {
|
|
2568
|
+
throw new SeriesDivergesError("sumSeries: terms are growing (not shrinking toward 0) -- the series appears to diverge.");
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
const tail = estimateConvergentTail((x) => f({ ...env, [variable]: x }), from + maxTerms);
|
|
2572
|
+
if (tail !== null)
|
|
2573
|
+
return total + tail;
|
|
2574
|
+
throw new SeriesDivergesError(`sumSeries: did not converge to within tolerance after ${maxTerms} terms -- the series may converge too slowly to sum numerically, or may not converge at all.`);
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Integral-test tail estimate for `Σ_{k=start}^∞ f(k)`, used by
|
|
2578
|
+
* {@link sumSeriesNumeric} once the per-term stopping rule alone can't
|
|
2579
|
+
* confirm convergence within its term budget (the case a per-term check
|
|
2580
|
+
* structurally can't handle: a polynomially-decaying tail like `1/n^2`,
|
|
2581
|
+
* where individual terms shrink far slower than any fixed tolerance
|
|
2582
|
+
* requires, even though the tail itself is provably small).
|
|
2583
|
+
*
|
|
2584
|
+
* Estimates the tail by numerically integrating `f` over successively
|
|
2585
|
+
* doubling intervals `[start, 2·start], [2·start, 4·start], ...` (each via
|
|
2586
|
+
* {@link Numerical.adaptiveSimpson}) and accumulating them, rather than
|
|
2587
|
+
* attempting one improper integral to infinity. This doubling scheme is
|
|
2588
|
+
* itself the confirmation of convergence, not just a computation of the
|
|
2589
|
+
* answer: for `f(x) ~ C/x^p` with `p > 1`, each doubling's integral
|
|
2590
|
+
* contribution shrinks geometrically (by a factor of `2^(p-1)`) as the
|
|
2591
|
+
* interval moves outward, so the increments visibly shrinking toward zero
|
|
2592
|
+
* *is* the integral test passing; for a genuinely divergent tail (e.g.
|
|
2593
|
+
* `f(x) = 1/x`, harmonic), each doubling contributes very close to the same
|
|
2594
|
+
* amount (`∫ 1/x dx` over `[N, 2N]` is `ln 2` regardless of `N`) and never
|
|
2595
|
+
* shrinks, so the loop exhausts its budget and returns `null` -- the
|
|
2596
|
+
* existing divergence behavior for that case is unchanged.
|
|
2597
|
+
*
|
|
2598
|
+
* Returns `null` (never a wrong number) whenever the tail can't be
|
|
2599
|
+
* confirmed convergent, so the caller falls through to its own
|
|
2600
|
+
* `SeriesDivergesError`.
|
|
2601
|
+
*/
|
|
2602
|
+
function estimateConvergentTail(f, start) {
|
|
2603
|
+
let lo = start;
|
|
2604
|
+
let width = Math.max(1, start);
|
|
2605
|
+
let tail = 0;
|
|
2606
|
+
let prevIncrement = Number.POSITIVE_INFINITY;
|
|
2607
|
+
const MAX_DOUBLINGS = 40;
|
|
2608
|
+
for (let d = 0; d < MAX_DOUBLINGS; d++) {
|
|
2609
|
+
const hi = lo + width;
|
|
2610
|
+
const increment = Numerical.adaptiveSimpson(f, lo, hi);
|
|
2611
|
+
if (!Number.isFinite(increment))
|
|
2612
|
+
return null;
|
|
2613
|
+
tail += increment;
|
|
2614
|
+
if (d > 5 && Math.abs(increment) > Math.abs(prevIncrement) * 1.5)
|
|
2615
|
+
return null; // growing, not shrinking -- doesn't confirm convergence
|
|
2616
|
+
if (Math.abs(increment) < 1e-13 * Math.max(1, Math.abs(tail)) && d > 2)
|
|
2617
|
+
return tail;
|
|
2618
|
+
prevIncrement = increment;
|
|
2619
|
+
lo = hi;
|
|
2620
|
+
width *= 2;
|
|
2621
|
+
}
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2624
|
+
/** If `e` is `a·x^2 + b·x + c` (a, b, c constant), return `{ a, b, c }`, else `null`. */
|
|
2625
|
+
function quadraticCoeffs(e, x) {
|
|
2626
|
+
try {
|
|
2627
|
+
const at0 = evalExpr(e, { [x]: 0 });
|
|
2628
|
+
const at1 = evalExpr(e, { [x]: 1 });
|
|
2629
|
+
const atm1 = evalExpr(e, { [x]: -1 });
|
|
2630
|
+
const c = at0;
|
|
2631
|
+
const a = (at1 + atm1) / 2 - c;
|
|
2632
|
+
const b = (at1 - atm1) / 2;
|
|
2633
|
+
// verify quadratic-ness at a fourth point
|
|
2634
|
+
const at2 = evalExpr(e, { [x]: 2 });
|
|
2635
|
+
if (Math.abs(at2 - (4 * a + 2 * b + c)) > 1e-9)
|
|
2636
|
+
return null;
|
|
2637
|
+
if (![a, b, c].every(Number.isFinite))
|
|
2638
|
+
return null;
|
|
2639
|
+
return { a, b, c };
|
|
2640
|
+
}
|
|
2641
|
+
catch {
|
|
2642
|
+
return null;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
/** If `e` is `x` or `x^n` (positive integer n), return `n`, else `null`. */
|
|
2646
|
+
function xPowerDegree(e, x) {
|
|
2647
|
+
if (e.type === "var" && e.name === x)
|
|
2648
|
+
return 1;
|
|
2649
|
+
if (e.type === "pow" &&
|
|
2650
|
+
e.base.type === "var" &&
|
|
2651
|
+
e.base.name === x &&
|
|
2652
|
+
e.exp.type === "const" &&
|
|
2653
|
+
Number.isInteger(e.exp.value) &&
|
|
2654
|
+
e.exp.value >= 1) {
|
|
2655
|
+
return e.exp.value;
|
|
2656
|
+
}
|
|
2657
|
+
return null;
|
|
2658
|
+
}
|
|
2659
|
+
const BY_PARTS_FUNCS = new Set(["sin", "cos", "exp"]);
|
|
2660
|
+
function isByPartsFunc(name) {
|
|
2661
|
+
return BY_PARTS_FUNCS.has(name);
|
|
2662
|
+
}
|
|
2663
|
+
/** ∫ x^n · f dx via repeated integration by parts, reducing the polynomial degree by 1 each step. */
|
|
2664
|
+
function ibpPolyTimesFunc(n, f, x) {
|
|
2665
|
+
if (n === 0)
|
|
2666
|
+
return integ(f, x);
|
|
2667
|
+
const antideriv = integ(f, x);
|
|
2668
|
+
const xn = n === 1 ? v(x) : pow(v(x), num(n));
|
|
2669
|
+
const term = mul(xn, antideriv);
|
|
2670
|
+
const rest = ibpPolyTimesFunc(n - 1, antideriv, x);
|
|
2671
|
+
return sub(term, mul(num(n), rest));
|
|
2672
|
+
}
|
|
2673
|
+
// -- evaluation -------------------------------------------------------------
|
|
2674
|
+
function evalExpr(e, env) {
|
|
2675
|
+
switch (e.type) {
|
|
2676
|
+
case "const":
|
|
2677
|
+
return e.value;
|
|
2678
|
+
case "var":
|
|
2679
|
+
return env[e.name] ?? Number.NaN;
|
|
2680
|
+
case "add":
|
|
2681
|
+
return evalExpr(e.left, env) + evalExpr(e.right, env);
|
|
2682
|
+
case "sub":
|
|
2683
|
+
return evalExpr(e.left, env) - evalExpr(e.right, env);
|
|
2684
|
+
case "mul":
|
|
2685
|
+
return evalExpr(e.left, env) * evalExpr(e.right, env);
|
|
2686
|
+
case "div":
|
|
2687
|
+
return evalExpr(e.left, env) / evalExpr(e.right, env);
|
|
2688
|
+
case "pow":
|
|
2689
|
+
return evalExpr(e.base, env) ** evalExpr(e.exp, env);
|
|
2690
|
+
case "neg":
|
|
2691
|
+
return -evalExpr(e.arg, env);
|
|
2692
|
+
case "func": {
|
|
2693
|
+
const a = evalExpr(e.arg, env);
|
|
2694
|
+
const name = e.name;
|
|
2695
|
+
switch (name) {
|
|
2696
|
+
case "sin":
|
|
2697
|
+
return Math.sin(a);
|
|
2698
|
+
case "cos":
|
|
2699
|
+
return Math.cos(a);
|
|
2700
|
+
case "tan":
|
|
2701
|
+
return Math.tan(a);
|
|
2702
|
+
case "exp":
|
|
2703
|
+
return Math.exp(a);
|
|
2704
|
+
case "ln":
|
|
2705
|
+
return Math.log(a);
|
|
2706
|
+
case "sqrt":
|
|
2707
|
+
return Math.sqrt(a);
|
|
2708
|
+
case "asin":
|
|
2709
|
+
return Math.asin(a);
|
|
2710
|
+
case "acos":
|
|
2711
|
+
return Math.acos(a);
|
|
2712
|
+
case "atan":
|
|
2713
|
+
return Math.atan(a);
|
|
2714
|
+
case "sinh":
|
|
2715
|
+
return Math.sinh(a);
|
|
2716
|
+
case "cosh":
|
|
2717
|
+
return Math.cosh(a);
|
|
2718
|
+
case "tanh":
|
|
2719
|
+
return Math.tanh(a);
|
|
2720
|
+
case "cot":
|
|
2721
|
+
return cotImpl(a);
|
|
2722
|
+
case "sec":
|
|
2723
|
+
return secImpl(a);
|
|
2724
|
+
case "csc":
|
|
2725
|
+
return cscImpl(a);
|
|
2726
|
+
case "asinh":
|
|
2727
|
+
return Math.asinh(a);
|
|
2728
|
+
case "acosh":
|
|
2729
|
+
return Math.acosh(a);
|
|
2730
|
+
case "atanh":
|
|
2731
|
+
return Math.atanh(a);
|
|
2732
|
+
case "coth":
|
|
2733
|
+
return cothImpl(a);
|
|
2734
|
+
case "sech":
|
|
2735
|
+
return sechImpl(a);
|
|
2736
|
+
case "csch":
|
|
2737
|
+
return cschImpl(a);
|
|
2738
|
+
case "acot":
|
|
2739
|
+
return acotImpl(a);
|
|
2740
|
+
case "asec":
|
|
2741
|
+
return asecImpl(a);
|
|
2742
|
+
case "acsc":
|
|
2743
|
+
return acscImpl(a);
|
|
2744
|
+
case "acoth":
|
|
2745
|
+
return acothImpl(a);
|
|
2746
|
+
case "asech":
|
|
2747
|
+
return asechImpl(a);
|
|
2748
|
+
case "acsch":
|
|
2749
|
+
return acschImpl(a);
|
|
2750
|
+
case "abs":
|
|
2751
|
+
return Math.abs(a);
|
|
2752
|
+
case "log10":
|
|
2753
|
+
return Math.log10(a);
|
|
2754
|
+
case "log2":
|
|
2755
|
+
return Math.log2(a);
|
|
2756
|
+
case "cbrt":
|
|
2757
|
+
return Math.cbrt(a);
|
|
2758
|
+
case "floor":
|
|
2759
|
+
return Math.floor(a);
|
|
2760
|
+
case "ceil":
|
|
2761
|
+
return Math.ceil(a);
|
|
2762
|
+
case "round":
|
|
2763
|
+
return Math.round(a);
|
|
2764
|
+
case "sign":
|
|
2765
|
+
return Math.sign(a);
|
|
2766
|
+
case "trunc":
|
|
2767
|
+
return Math.trunc(a);
|
|
2768
|
+
case "expm1":
|
|
2769
|
+
return Math.expm1(a);
|
|
2770
|
+
case "log1p":
|
|
2771
|
+
return Math.log1p(a);
|
|
2772
|
+
case "sigmoid":
|
|
2773
|
+
return sigmoidImpl(a);
|
|
2774
|
+
case "erf":
|
|
2775
|
+
return SpecialFunctions.erf(a);
|
|
2776
|
+
case "relu":
|
|
2777
|
+
return Math.max(a, 0);
|
|
2778
|
+
}
|
|
2779
|
+
throw new Error(`Unhandled function: ${name}`);
|
|
2780
|
+
}
|
|
2781
|
+
case "call2":
|
|
2782
|
+
return BINARY_FUNC_IMPLS[e.name](evalExpr(e.left, env), evalExpr(e.right, env));
|
|
2783
|
+
case "cmp":
|
|
2784
|
+
return CMP_IMPLS[e.op](evalExpr(e.left, env), evalExpr(e.right, env)) ? 1 : 0;
|
|
2785
|
+
case "piecewise": {
|
|
2786
|
+
for (const br of e.branches) {
|
|
2787
|
+
if (evalExpr(br.cond, env) !== 0)
|
|
2788
|
+
return evalExpr(br.expr, env);
|
|
2789
|
+
}
|
|
2790
|
+
return evalExpr(e.otherwise, env);
|
|
2791
|
+
}
|
|
2792
|
+
case "sum":
|
|
2793
|
+
return evalSum(e, env);
|
|
2794
|
+
case "product":
|
|
2795
|
+
return evalProduct(e, env);
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
/**
|
|
2799
|
+
* Delegates entirely to {@link Symbolic.sumSeries}'s existing finite-loop /
|
|
2800
|
+
* geometric-closed-form / numeric-tail-convergence logic (`to` may be
|
|
2801
|
+
* `Infinity`, same convention `sumSeries` itself uses for an infinite
|
|
2802
|
+
* series) -- shared by {@link evalExpr} and {@link compileExpr} so neither
|
|
2803
|
+
* reimplements it.
|
|
2804
|
+
*/
|
|
2805
|
+
function evalSum(e, env) {
|
|
2806
|
+
const from = Math.round(evalExpr(e.from, env));
|
|
2807
|
+
const to = evalExpr(e.to, env);
|
|
2808
|
+
return Symbolic.sumSeries(e.body, from, Number.isFinite(to) ? Math.round(to) : to, e.variable, env);
|
|
2809
|
+
}
|
|
2810
|
+
/**
|
|
2811
|
+
* Unlike {@link evalSum}, an infinite product's convergence isn't handled --
|
|
2812
|
+
* v1 only supports finite bounds (mirrors {@link ProductNotDifferentiableError}'s
|
|
2813
|
+
* scope note on `differentiate`). Shared by {@link evalExpr} and {@link compileExpr}.
|
|
2814
|
+
*/
|
|
2815
|
+
function evalProduct(e, env) {
|
|
2816
|
+
const from = Math.round(evalExpr(e.from, env));
|
|
2817
|
+
const to = evalExpr(e.to, env);
|
|
2818
|
+
if (!Number.isFinite(to))
|
|
2819
|
+
throw new Error("product (Π): only finite upper bounds are supported.");
|
|
2820
|
+
const roundedTo = Math.round(to);
|
|
2821
|
+
if (from > roundedTo)
|
|
2822
|
+
return 1; // empty product
|
|
2823
|
+
const f = compileExpr(e.body);
|
|
2824
|
+
let total = 1;
|
|
2825
|
+
for (let k = from; k <= roundedTo; k++)
|
|
2826
|
+
total *= f({ ...env, [e.variable]: k });
|
|
2827
|
+
return total;
|
|
2828
|
+
}
|
|
2829
|
+
// -- compilation --------------------------------------------------------------
|
|
2830
|
+
const cotImpl = (x) => 1 / Math.tan(x);
|
|
2831
|
+
const secImpl = (x) => 1 / Math.cos(x);
|
|
2832
|
+
const cscImpl = (x) => 1 / Math.sin(x);
|
|
2833
|
+
const cothImpl = (x) => 1 / Math.tanh(x);
|
|
2834
|
+
const sechImpl = (x) => 1 / Math.cosh(x);
|
|
2835
|
+
const cschImpl = (x) => 1 / Math.sinh(x);
|
|
2836
|
+
// Inverse reciprocal-trig/hyperbolic functions expressed via their reciprocal-argument
|
|
2837
|
+
// counterparts (e.g. arcsec(x) = arccos(1/x)) — JS's Math object has no direct equivalents.
|
|
2838
|
+
const acotImpl = (x) => Math.PI / 2 - Math.atan(x);
|
|
2839
|
+
const asecImpl = (x) => Math.acos(1 / x);
|
|
2840
|
+
const acscImpl = (x) => Math.asin(1 / x);
|
|
2841
|
+
const acothImpl = (x) => Math.atanh(1 / x);
|
|
2842
|
+
const asechImpl = (x) => Math.acosh(1 / x);
|
|
2843
|
+
const acschImpl = (x) => Math.asinh(1 / x);
|
|
2844
|
+
const sigmoidImpl = (x) => 1 / (1 + Math.exp(-x));
|
|
2845
|
+
const FUNC_IMPLS = {
|
|
2846
|
+
sin: Math.sin,
|
|
2847
|
+
cos: Math.cos,
|
|
2848
|
+
tan: Math.tan,
|
|
2849
|
+
exp: Math.exp,
|
|
2850
|
+
ln: Math.log,
|
|
2851
|
+
sqrt: Math.sqrt,
|
|
2852
|
+
asin: Math.asin,
|
|
2853
|
+
acos: Math.acos,
|
|
2854
|
+
atan: Math.atan,
|
|
2855
|
+
sinh: Math.sinh,
|
|
2856
|
+
cosh: Math.cosh,
|
|
2857
|
+
tanh: Math.tanh,
|
|
2858
|
+
cot: cotImpl,
|
|
2859
|
+
sec: secImpl,
|
|
2860
|
+
csc: cscImpl,
|
|
2861
|
+
asinh: Math.asinh,
|
|
2862
|
+
acosh: Math.acosh,
|
|
2863
|
+
atanh: Math.atanh,
|
|
2864
|
+
coth: cothImpl,
|
|
2865
|
+
sech: sechImpl,
|
|
2866
|
+
csch: cschImpl,
|
|
2867
|
+
acot: acotImpl,
|
|
2868
|
+
asec: asecImpl,
|
|
2869
|
+
acsc: acscImpl,
|
|
2870
|
+
acoth: acothImpl,
|
|
2871
|
+
asech: asechImpl,
|
|
2872
|
+
acsch: acschImpl,
|
|
2873
|
+
abs: Math.abs,
|
|
2874
|
+
log10: Math.log10,
|
|
2875
|
+
log2: Math.log2,
|
|
2876
|
+
cbrt: Math.cbrt,
|
|
2877
|
+
floor: Math.floor,
|
|
2878
|
+
ceil: Math.ceil,
|
|
2879
|
+
round: Math.round,
|
|
2880
|
+
sign: Math.sign,
|
|
2881
|
+
trunc: Math.trunc,
|
|
2882
|
+
expm1: Math.expm1,
|
|
2883
|
+
log1p: Math.log1p,
|
|
2884
|
+
sigmoid: sigmoidImpl,
|
|
2885
|
+
erf: SpecialFunctions.erf,
|
|
2886
|
+
relu: (x) => Math.max(x, 0),
|
|
2887
|
+
};
|
|
2888
|
+
const BINARY_FUNC_IMPLS = {
|
|
2889
|
+
atan2: Math.atan2,
|
|
2890
|
+
hypot: Math.hypot,
|
|
2891
|
+
min: Math.min,
|
|
2892
|
+
max: Math.max,
|
|
2893
|
+
gcd: intGcd,
|
|
2894
|
+
lcm: (a, b) => Math.abs(a * b) / intGcd(a, b),
|
|
2895
|
+
};
|
|
2896
|
+
const CMP_IMPLS = {
|
|
2897
|
+
lt: (a, b) => a < b,
|
|
2898
|
+
le: (a, b) => a <= b,
|
|
2899
|
+
gt: (a, b) => a > b,
|
|
2900
|
+
ge: (a, b) => a >= b,
|
|
2901
|
+
eq: (a, b) => a === b,
|
|
2902
|
+
ne: (a, b) => a !== b,
|
|
2903
|
+
};
|
|
2904
|
+
function compileExpr(e) {
|
|
2905
|
+
switch (e.type) {
|
|
2906
|
+
case "const": {
|
|
2907
|
+
const value = e.value;
|
|
2908
|
+
return () => value;
|
|
2909
|
+
}
|
|
2910
|
+
case "var": {
|
|
2911
|
+
const name = e.name;
|
|
2912
|
+
return (env) => env[name] ?? Number.NaN;
|
|
2913
|
+
}
|
|
2914
|
+
case "add": {
|
|
2915
|
+
const l = compileExpr(e.left);
|
|
2916
|
+
const r = compileExpr(e.right);
|
|
2917
|
+
return (env) => l(env) + r(env);
|
|
2918
|
+
}
|
|
2919
|
+
case "sub": {
|
|
2920
|
+
const l = compileExpr(e.left);
|
|
2921
|
+
const r = compileExpr(e.right);
|
|
2922
|
+
return (env) => l(env) - r(env);
|
|
2923
|
+
}
|
|
2924
|
+
case "mul": {
|
|
2925
|
+
const l = compileExpr(e.left);
|
|
2926
|
+
const r = compileExpr(e.right);
|
|
2927
|
+
return (env) => l(env) * r(env);
|
|
2928
|
+
}
|
|
2929
|
+
case "div": {
|
|
2930
|
+
const l = compileExpr(e.left);
|
|
2931
|
+
const r = compileExpr(e.right);
|
|
2932
|
+
return (env) => l(env) / r(env);
|
|
2933
|
+
}
|
|
2934
|
+
case "pow": {
|
|
2935
|
+
const b = compileExpr(e.base);
|
|
2936
|
+
const p = compileExpr(e.exp);
|
|
2937
|
+
return (env) => b(env) ** p(env);
|
|
2938
|
+
}
|
|
2939
|
+
case "neg": {
|
|
2940
|
+
const a = compileExpr(e.arg);
|
|
2941
|
+
return (env) => -a(env);
|
|
2942
|
+
}
|
|
2943
|
+
case "func": {
|
|
2944
|
+
const a = compileExpr(e.arg);
|
|
2945
|
+
const impl = FUNC_IMPLS[e.name];
|
|
2946
|
+
return (env) => impl(a(env));
|
|
2947
|
+
}
|
|
2948
|
+
case "call2": {
|
|
2949
|
+
const l = compileExpr(e.left);
|
|
2950
|
+
const r = compileExpr(e.right);
|
|
2951
|
+
const impl = BINARY_FUNC_IMPLS[e.name];
|
|
2952
|
+
return (env) => impl(l(env), r(env));
|
|
2953
|
+
}
|
|
2954
|
+
case "cmp": {
|
|
2955
|
+
const l = compileExpr(e.left);
|
|
2956
|
+
const r = compileExpr(e.right);
|
|
2957
|
+
const impl = CMP_IMPLS[e.op];
|
|
2958
|
+
return (env) => (impl(l(env), r(env)) ? 1 : 0);
|
|
2959
|
+
}
|
|
2960
|
+
case "piecewise": {
|
|
2961
|
+
const branches = e.branches.map((br) => ({ cond: compileExpr(br.cond), expr: compileExpr(br.expr) }));
|
|
2962
|
+
const otherwise = compileExpr(e.otherwise);
|
|
2963
|
+
return (env) => {
|
|
2964
|
+
for (const br of branches) {
|
|
2965
|
+
if (br.cond(env) !== 0)
|
|
2966
|
+
return br.expr(env);
|
|
2967
|
+
}
|
|
2968
|
+
return otherwise(env);
|
|
2969
|
+
};
|
|
2970
|
+
}
|
|
2971
|
+
// Not pre-compiled further -- evalSum/evalProduct each recompile the body
|
|
2972
|
+
// per call via Symbolic.sumSeries/compileExpr(e.body), matching evalExpr's
|
|
2973
|
+
// own approach (this is a rare node type, not the hot per-sample path
|
|
2974
|
+
// compile() is meant to speed up).
|
|
2975
|
+
case "sum":
|
|
2976
|
+
return (env) => evalSum(e, env);
|
|
2977
|
+
case "product":
|
|
2978
|
+
return (env) => evalProduct(e, env);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
// -- exact (Rational) and structure-aware evaluation -------------------------
|
|
2982
|
+
// Moved here from mallory's app-level rational-eval.ts/structure-eval.ts:
|
|
2983
|
+
// these are generic "evaluate an Expr over an alternate algebra" utilities
|
|
2984
|
+
// with no graphing-calculator-specific logic, so they belong alongside
|
|
2985
|
+
// evalExpr/compileExpr where this file's own exhaustiveness checking keeps
|
|
2986
|
+
// them in sync with new Expr variants automatically.
|
|
2987
|
+
/** Maps a {@link Rational.compare} result (-1/0/1) to a truth value per {@link CmpOp}. */
|
|
2988
|
+
const CMP_FROM_COMPARE = {
|
|
2989
|
+
lt: (c) => c < 0,
|
|
2990
|
+
le: (c) => c <= 0,
|
|
2991
|
+
gt: (c) => c > 0,
|
|
2992
|
+
ge: (c) => c >= 0,
|
|
2993
|
+
eq: (c) => c === 0,
|
|
2994
|
+
ne: (c) => c !== 0,
|
|
2995
|
+
};
|
|
2996
|
+
function evalExprExact(e, env) {
|
|
2997
|
+
switch (e.type) {
|
|
2998
|
+
case "const":
|
|
2999
|
+
return Rational.fromNumber(e.value);
|
|
3000
|
+
case "var": {
|
|
3001
|
+
const value = env[e.name];
|
|
3002
|
+
if (!value)
|
|
3003
|
+
throw new Error(`No exact value bound for "${e.name}"`);
|
|
3004
|
+
return value;
|
|
3005
|
+
}
|
|
3006
|
+
case "add":
|
|
3007
|
+
return evalExprExact(e.left, env).add(evalExprExact(e.right, env));
|
|
3008
|
+
case "sub":
|
|
3009
|
+
return evalExprExact(e.left, env).subtract(evalExprExact(e.right, env));
|
|
3010
|
+
case "mul":
|
|
3011
|
+
return evalExprExact(e.left, env).multiply(evalExprExact(e.right, env));
|
|
3012
|
+
case "div":
|
|
3013
|
+
return evalExprExact(e.left, env).divide(evalExprExact(e.right, env));
|
|
3014
|
+
case "pow": {
|
|
3015
|
+
const exponent = evalExprExact(e.exp, env);
|
|
3016
|
+
if (exponent.denominator !== 1n)
|
|
3017
|
+
throw new Error("Exact pow requires an integer exponent");
|
|
3018
|
+
return evalExprExact(e.base, env).pow(Number(exponent.numerator));
|
|
3019
|
+
}
|
|
3020
|
+
case "neg":
|
|
3021
|
+
return evalExprExact(e.arg, env).negate();
|
|
3022
|
+
case "func":
|
|
3023
|
+
throw new Error(`"${e.name}" is not exactly representable as a Rational`);
|
|
3024
|
+
case "call2":
|
|
3025
|
+
throw new Error(`"${e.name}" is not exactly representable as a Rational`);
|
|
3026
|
+
case "cmp": {
|
|
3027
|
+
const l = evalExprExact(e.left, env);
|
|
3028
|
+
const r = evalExprExact(e.right, env);
|
|
3029
|
+
const truth = CMP_FROM_COMPARE[e.op](l.compare(r));
|
|
3030
|
+
return truth ? Rational.One : Rational.Zero;
|
|
3031
|
+
}
|
|
3032
|
+
case "piecewise": {
|
|
3033
|
+
for (const branch of e.branches) {
|
|
3034
|
+
if (!evalExprExact(branch.cond, env).isZero())
|
|
3035
|
+
return evalExprExact(branch.expr, env);
|
|
3036
|
+
}
|
|
3037
|
+
return evalExprExact(e.otherwise, env);
|
|
3038
|
+
}
|
|
3039
|
+
case "sum":
|
|
3040
|
+
throw new Error("sum (Σ) is not exactly representable as a Rational");
|
|
3041
|
+
case "product":
|
|
3042
|
+
throw new Error("product (Π) is not exactly representable as a Rational");
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
const ORDERING_CMP_OPS = new Set(["lt", "le", "gt", "ge"]);
|
|
3046
|
+
function evalExprOverStructure(e, structure, env) {
|
|
3047
|
+
switch (e.type) {
|
|
3048
|
+
case "const":
|
|
3049
|
+
return structure.wrap(e.value);
|
|
3050
|
+
case "var": {
|
|
3051
|
+
const value = env[e.name];
|
|
3052
|
+
if (value === undefined)
|
|
3053
|
+
throw new Error(`No value bound for "${e.name}"`);
|
|
3054
|
+
return structure.wrap(value);
|
|
3055
|
+
}
|
|
3056
|
+
case "add":
|
|
3057
|
+
return structure.add(evalExprOverStructure(e.left, structure, env), evalExprOverStructure(e.right, structure, env));
|
|
3058
|
+
case "sub":
|
|
3059
|
+
return structure.subtract(evalExprOverStructure(e.left, structure, env), evalExprOverStructure(e.right, structure, env));
|
|
3060
|
+
case "mul":
|
|
3061
|
+
return structure.multiply(evalExprOverStructure(e.left, structure, env), evalExprOverStructure(e.right, structure, env));
|
|
3062
|
+
case "div":
|
|
3063
|
+
return structure.divide(evalExprOverStructure(e.left, structure, env), evalExprOverStructure(e.right, structure, env));
|
|
3064
|
+
case "pow": {
|
|
3065
|
+
if (e.exp.type !== "const" || !Number.isInteger(e.exp.value)) {
|
|
3066
|
+
throw new Error("Structure-aware pow requires a literal integer exponent");
|
|
3067
|
+
}
|
|
3068
|
+
return structure.multiplyPower(evalExprOverStructure(e.base, structure, env), e.exp.value);
|
|
3069
|
+
}
|
|
3070
|
+
case "neg":
|
|
3071
|
+
return structure.negative(evalExprOverStructure(e.arg, structure, env));
|
|
3072
|
+
case "func":
|
|
3073
|
+
throw new Error(`"${e.name}" has no meaning over this structure`);
|
|
3074
|
+
case "call2":
|
|
3075
|
+
throw new Error(`"${e.name}" has no meaning over this structure`);
|
|
3076
|
+
case "cmp": {
|
|
3077
|
+
if (ORDERING_CMP_OPS.has(e.op)) {
|
|
3078
|
+
throw new Error(`Ordering comparisons ("${e.op}") have no general meaning over this structure`);
|
|
3079
|
+
}
|
|
3080
|
+
const l = evalExprOverStructure(e.left, structure, env);
|
|
3081
|
+
const r = evalExprOverStructure(e.right, structure, env);
|
|
3082
|
+
const equal = structure.equality(l, r);
|
|
3083
|
+
const truth = e.op === "eq" ? equal : !equal;
|
|
3084
|
+
return truth ? structure.one : structure.zero;
|
|
3085
|
+
}
|
|
3086
|
+
case "piecewise": {
|
|
3087
|
+
for (const branch of e.branches) {
|
|
3088
|
+
const condValue = evalExprOverStructure(branch.cond, structure, env);
|
|
3089
|
+
if (!structure.equality(condValue, structure.zero)) {
|
|
3090
|
+
return evalExprOverStructure(branch.expr, structure, env);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
return evalExprOverStructure(e.otherwise, structure, env);
|
|
3094
|
+
}
|
|
3095
|
+
case "sum":
|
|
3096
|
+
throw new Error("sum (Σ) has no general meaning over this structure");
|
|
3097
|
+
case "product":
|
|
3098
|
+
throw new Error("product (Π) has no general meaning over this structure");
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
// -- rendering --------------------------------------------------------------
|
|
3102
|
+
const PREC = { add: 1, sub: 1, mul: 2, div: 2, neg: 3, pow: 4, cmp: 0 };
|
|
3103
|
+
function render(e, parentPrec) {
|
|
3104
|
+
switch (e.type) {
|
|
3105
|
+
case "const":
|
|
3106
|
+
return `${e.value}`;
|
|
3107
|
+
case "var":
|
|
3108
|
+
return e.name;
|
|
3109
|
+
case "func":
|
|
3110
|
+
return `${e.name}(${render(e.arg, 0)})`;
|
|
3111
|
+
case "call2":
|
|
3112
|
+
return `${e.name}(${render(e.left, 0)}, ${render(e.right, 0)})`;
|
|
3113
|
+
case "neg":
|
|
3114
|
+
return wrap(`-${render(e.arg, PREC.neg)}`, PREC.neg, parentPrec);
|
|
3115
|
+
case "pow":
|
|
3116
|
+
return wrap(`${render(e.base, PREC.pow + 1)}^${render(e.exp, PREC.pow)}`, PREC.pow, parentPrec);
|
|
3117
|
+
case "add":
|
|
3118
|
+
return wrap(`${render(e.left, PREC.add)} + ${render(e.right, PREC.add)}`, PREC.add, parentPrec);
|
|
3119
|
+
case "sub":
|
|
3120
|
+
return wrap(`${render(e.left, PREC.sub)} - ${render(e.right, PREC.sub + 1)}`, PREC.sub, parentPrec);
|
|
3121
|
+
case "mul":
|
|
3122
|
+
return wrap(`${render(e.left, PREC.mul)}*${render(e.right, PREC.mul + 1)}`, PREC.mul, parentPrec);
|
|
3123
|
+
case "div":
|
|
3124
|
+
return wrap(`${render(e.left, PREC.div)}/${render(e.right, PREC.div + 1)}`, PREC.div, parentPrec);
|
|
3125
|
+
case "cmp": {
|
|
3126
|
+
const opStr = CMP_OP_TEXT[e.op];
|
|
3127
|
+
return wrap(`${render(e.left, PREC.cmp + 1)}${opStr}${render(e.right, PREC.cmp + 1)}`, PREC.cmp, parentPrec);
|
|
3128
|
+
}
|
|
3129
|
+
case "piecewise": {
|
|
3130
|
+
const parts = e.branches.map((br) => `${render(br.cond, 0)}, ${render(br.expr, 0)}`);
|
|
3131
|
+
return `piecewise(${parts.join(", ")}, ${render(e.otherwise, 0)})`;
|
|
3132
|
+
}
|
|
3133
|
+
case "sum":
|
|
3134
|
+
return `sum(${e.variable}, ${render(e.from, 0)}, ${render(e.to, 0)}, ${render(e.body, 0)})`;
|
|
3135
|
+
case "product":
|
|
3136
|
+
return `product(${e.variable}, ${render(e.from, 0)}, ${render(e.to, 0)}, ${render(e.body, 0)})`;
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
const wrap = (s, prec, parentPrec) => (prec < parentPrec ? `(${s})` : s);
|
|
3140
|
+
/** Plain-text infix spelling for each `CmpOp`, matching what the parser's `comparison()` tier accepts. */
|
|
3141
|
+
const CMP_OP_TEXT = { lt: "<", le: "<=", gt: ">", ge: ">=", eq: "==", ne: "!=" };
|
|
3142
|
+
// -- LaTeX rendering ----------------------------------------------------------
|
|
3143
|
+
const LATEX_FUNCS = {
|
|
3144
|
+
sin: "\\sin",
|
|
3145
|
+
cos: "\\cos",
|
|
3146
|
+
tan: "\\tan",
|
|
3147
|
+
exp: "\\exp",
|
|
3148
|
+
ln: "\\ln",
|
|
3149
|
+
asin: "\\arcsin",
|
|
3150
|
+
acos: "\\arccos",
|
|
3151
|
+
atan: "\\arctan",
|
|
3152
|
+
sinh: "\\sinh",
|
|
3153
|
+
cosh: "\\cosh",
|
|
3154
|
+
tanh: "\\tanh",
|
|
3155
|
+
cot: "\\cot",
|
|
3156
|
+
sec: "\\sec",
|
|
3157
|
+
csc: "\\csc",
|
|
3158
|
+
coth: "\\coth",
|
|
3159
|
+
// \sech/\csch and arc-hyperbolic names aren't standard LaTeX commands —
|
|
3160
|
+
// render via \operatorname, same convention KaTeX/MathJax use for them.
|
|
3161
|
+
sech: "\\operatorname{sech}",
|
|
3162
|
+
csch: "\\operatorname{csch}",
|
|
3163
|
+
asinh: "\\operatorname{arcsinh}",
|
|
3164
|
+
acosh: "\\operatorname{arccosh}",
|
|
3165
|
+
atanh: "\\operatorname{arctanh}",
|
|
3166
|
+
acot: "\\operatorname{arccot}",
|
|
3167
|
+
asec: "\\operatorname{arcsec}",
|
|
3168
|
+
acsc: "\\operatorname{arccsc}",
|
|
3169
|
+
acoth: "\\operatorname{arccoth}",
|
|
3170
|
+
asech: "\\operatorname{arcsech}",
|
|
3171
|
+
acsch: "\\operatorname{arccsch}",
|
|
3172
|
+
round: "\\operatorname{round}",
|
|
3173
|
+
sign: "\\operatorname{sgn}",
|
|
3174
|
+
trunc: "\\operatorname{trunc}",
|
|
3175
|
+
expm1: "\\operatorname{expm1}",
|
|
3176
|
+
log1p: "\\operatorname{log1p}",
|
|
3177
|
+
sigmoid: "\\operatorname{sigmoid}",
|
|
3178
|
+
erf: "\\operatorname{erf}",
|
|
3179
|
+
relu: "\\operatorname{relu}",
|
|
3180
|
+
};
|
|
3181
|
+
/** LaTeX commands for `BinaryFuncName`s — `\min`/`\max`/`\gcd` are standard LaTeX; the rest use `\operatorname`. */
|
|
3182
|
+
const LATEX_BINARY_FUNCS = {
|
|
3183
|
+
atan2: "\\operatorname{atan2}",
|
|
3184
|
+
hypot: "\\operatorname{hypot}",
|
|
3185
|
+
min: "\\min",
|
|
3186
|
+
max: "\\max",
|
|
3187
|
+
gcd: "\\gcd",
|
|
3188
|
+
lcm: "\\operatorname{lcm}",
|
|
3189
|
+
};
|
|
3190
|
+
function toLatexRec(e, parentPrec) {
|
|
3191
|
+
switch (e.type) {
|
|
3192
|
+
case "const":
|
|
3193
|
+
if (Math.abs(e.value - Math.PI) < 1e-12)
|
|
3194
|
+
return "\\pi";
|
|
3195
|
+
if (Math.abs(e.value - Math.E) < 1e-12)
|
|
3196
|
+
return "e";
|
|
3197
|
+
return `${e.value}`;
|
|
3198
|
+
case "var":
|
|
3199
|
+
return e.name;
|
|
3200
|
+
case "func":
|
|
3201
|
+
if (e.name === "sqrt")
|
|
3202
|
+
return `\\sqrt{${toLatexRec(e.arg, 0)}}`;
|
|
3203
|
+
if (e.name === "cbrt")
|
|
3204
|
+
return `\\sqrt[3]{${toLatexRec(e.arg, 0)}}`;
|
|
3205
|
+
if (e.name === "abs")
|
|
3206
|
+
return `\\left|${toLatexRec(e.arg, 0)}\\right|`;
|
|
3207
|
+
if (e.name === "floor")
|
|
3208
|
+
return `\\left\\lfloor ${toLatexRec(e.arg, 0)}\\right\\rfloor`;
|
|
3209
|
+
if (e.name === "ceil")
|
|
3210
|
+
return `\\left\\lceil ${toLatexRec(e.arg, 0)}\\right\\rceil`;
|
|
3211
|
+
if (e.name === "log10")
|
|
3212
|
+
return `\\log_{10}\\left(${toLatexRec(e.arg, 0)}\\right)`;
|
|
3213
|
+
if (e.name === "log2")
|
|
3214
|
+
return `\\log_{2}\\left(${toLatexRec(e.arg, 0)}\\right)`;
|
|
3215
|
+
return `${LATEX_FUNCS[e.name]}\\left(${toLatexRec(e.arg, 0)}\\right)`;
|
|
3216
|
+
case "call2":
|
|
3217
|
+
return `${LATEX_BINARY_FUNCS[e.name]}\\left(${toLatexRec(e.left, 0)}, ${toLatexRec(e.right, 0)}\\right)`;
|
|
3218
|
+
case "neg":
|
|
3219
|
+
return wrap(`-${toLatexRec(e.arg, PREC.neg)}`, PREC.neg, parentPrec);
|
|
3220
|
+
case "pow":
|
|
3221
|
+
return wrap(`${toLatexRec(e.base, PREC.pow + 1)}^{${toLatexRec(e.exp, 0)}}`, PREC.pow, parentPrec);
|
|
3222
|
+
case "add":
|
|
3223
|
+
return wrap(`${toLatexRec(e.left, PREC.add)} + ${toLatexRec(e.right, PREC.add)}`, PREC.add, parentPrec);
|
|
3224
|
+
case "sub":
|
|
3225
|
+
return wrap(`${toLatexRec(e.left, PREC.sub)} - ${toLatexRec(e.right, PREC.sub + 1)}`, PREC.sub, parentPrec);
|
|
3226
|
+
case "mul":
|
|
3227
|
+
return wrap(`${toLatexRec(e.left, PREC.mul)} \\cdot ${toLatexRec(e.right, PREC.mul + 1)}`, PREC.mul, parentPrec);
|
|
3228
|
+
case "div":
|
|
3229
|
+
// \frac is self-delimiting — never needs outer parens.
|
|
3230
|
+
return `\\frac{${toLatexRec(e.left, 0)}}{${toLatexRec(e.right, 0)}}`;
|
|
3231
|
+
case "cmp": {
|
|
3232
|
+
const latexOp = CMP_OP_LATEX[e.op];
|
|
3233
|
+
return wrap(`${toLatexRec(e.left, PREC.cmp + 1)} ${latexOp} ${toLatexRec(e.right, PREC.cmp + 1)}`, PREC.cmp, parentPrec);
|
|
3234
|
+
}
|
|
3235
|
+
case "piecewise": {
|
|
3236
|
+
// \begin{cases} is self-delimiting -- never needs outer parens, same as \frac.
|
|
3237
|
+
// No space directly after the "\\" row separator: latexToInfix's
|
|
3238
|
+
// cleaning regex (which strips LaTeX spacing commands like `\ `) would
|
|
3239
|
+
// otherwise eat the second backslash, seeing "\<space>" and mangling
|
|
3240
|
+
// the separator down to a single backslash -- verified empirically.
|
|
3241
|
+
const rows = e.branches.map((br) => `${toLatexRec(br.expr, 0)} & ${toLatexRec(br.cond, 0)}`);
|
|
3242
|
+
rows.push(`${toLatexRec(e.otherwise, 0)} & \\text{otherwise}`);
|
|
3243
|
+
return `\\begin{cases}${rows.join("\\\\")}\\end{cases}`;
|
|
3244
|
+
}
|
|
3245
|
+
// \sum_{}^{}/\prod_{}^{} are self-delimiting via their sub/superscript,
|
|
3246
|
+
// same as \frac/\begin{cases} above -- never need outer parens.
|
|
3247
|
+
case "sum":
|
|
3248
|
+
return `\\sum_{${e.variable}=${toLatexRec(e.from, 0)}}^{${toLatexRec(e.to, 0)}} ${toLatexRec(e.body, 0)}`;
|
|
3249
|
+
case "product":
|
|
3250
|
+
return `\\prod_{${e.variable}=${toLatexRec(e.from, 0)}}^{${toLatexRec(e.to, 0)}} ${toLatexRec(e.body, 0)}`;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
/** LaTeX symbol for each `CmpOp`. */
|
|
3254
|
+
const CMP_OP_LATEX = { lt: "<", le: "\\leq", gt: ">", ge: "\\geq", eq: "=", ne: "\\neq" };
|
|
3255
|
+
// -- LaTeX parsing (fromLatex) ------------------------------------------------
|
|
3256
|
+
const LATEX_FUNC_NAMES = Object.fromEntries(Object.entries(LATEX_FUNCS)
|
|
3257
|
+
.filter(([, latex]) => !latex.startsWith("\\operatorname"))
|
|
3258
|
+
.map(([name, latex]) => [latex, name]));
|
|
3259
|
+
/** Reverse lookup for the `\operatorname{name}` functions in {@link LATEX_FUNCS} (e.g. `sech` -> `"sech"`). */
|
|
3260
|
+
const OPERATORNAME_FUNC_NAMES = Object.fromEntries(Object.entries(LATEX_FUNCS)
|
|
3261
|
+
.filter((entry) => entry[1].startsWith("\\operatorname"))
|
|
3262
|
+
.map(([name, latex]) => [/\\operatorname\{(\w+)\}/.exec(latex)?.[1], name]));
|
|
3263
|
+
/** Reverse lookup for the standard-LaTeX-command `BinaryFuncName`s (`\min`, `\max`, `\gcd`). */
|
|
3264
|
+
const LATEX_BINARY_FUNC_NAMES = Object.fromEntries(Object.entries(LATEX_BINARY_FUNCS)
|
|
3265
|
+
.filter((entry) => !entry[1].startsWith("\\operatorname"))
|
|
3266
|
+
.map(([name, latex]) => [latex, name]));
|
|
3267
|
+
/** Reverse lookup for the `\operatorname{name}` `BinaryFuncName`s (`atan2`, `hypot`, `lcm`). */
|
|
3268
|
+
const OPERATORNAME_BINARY_FUNC_NAMES = Object.fromEntries(Object.entries(LATEX_BINARY_FUNCS)
|
|
3269
|
+
.filter((entry) => entry[1].startsWith("\\operatorname"))
|
|
3270
|
+
.map(([name, latex]) => [/\\operatorname\{(\w+)\}/.exec(latex)?.[1], name]));
|
|
3271
|
+
/** Find the index of the delimiter matching `open` at `s[start]`, honoring nesting. */
|
|
3272
|
+
function findGroupEnd(s, start, open, close) {
|
|
3273
|
+
let depth = 0;
|
|
3274
|
+
for (let i = start; i < s.length; i++) {
|
|
3275
|
+
if (s[i] === open)
|
|
3276
|
+
depth++;
|
|
3277
|
+
else if (s[i] === close) {
|
|
3278
|
+
depth--;
|
|
3279
|
+
if (depth === 0)
|
|
3280
|
+
return i;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
throw new Error(`Unmatched '${open}' in LaTeX source`);
|
|
3284
|
+
}
|
|
3285
|
+
/** Extract the `{...}`/`(...)`/`[...]` group starting at `s[pos]` (the opening delimiter itself). */
|
|
3286
|
+
function extractGroup(s, pos) {
|
|
3287
|
+
const open = s[pos];
|
|
3288
|
+
const close = open === "{" ? "}" : open === "(" ? ")" : open === "[" ? "]" : undefined;
|
|
3289
|
+
if (!close)
|
|
3290
|
+
throw new Error(`Expected a group starting with '{', '(' or '[' at position ${pos}`);
|
|
3291
|
+
const end = findGroupEnd(s, pos, open, close);
|
|
3292
|
+
return { content: s.slice(pos + 1, end), next: end + 1 };
|
|
3293
|
+
}
|
|
3294
|
+
/** Split `s` on top-level commas, ignoring commas nested inside `{}`/`()`/`[]`. */
|
|
3295
|
+
function splitTopLevelCommas(s) {
|
|
3296
|
+
const parts = [];
|
|
3297
|
+
let depth = 0;
|
|
3298
|
+
let start = 0;
|
|
3299
|
+
for (let i = 0; i < s.length; i++) {
|
|
3300
|
+
const ch = s[i];
|
|
3301
|
+
if (ch === "{" || ch === "(" || ch === "[")
|
|
3302
|
+
depth++;
|
|
3303
|
+
else if (ch === "}" || ch === ")" || ch === "]")
|
|
3304
|
+
depth--;
|
|
3305
|
+
else if (ch === "," && depth === 0) {
|
|
3306
|
+
parts.push(s.slice(start, i));
|
|
3307
|
+
start = i + 1;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
parts.push(s.slice(start));
|
|
3311
|
+
return parts;
|
|
3312
|
+
}
|
|
3313
|
+
/** Split `s` on top-level occurrences of a (possibly multi-char) separator, ignoring ones nested inside `{}`/`()`/`[]`. */
|
|
3314
|
+
function splitTopLevelOn(s, sep) {
|
|
3315
|
+
const parts = [];
|
|
3316
|
+
let depth = 0;
|
|
3317
|
+
let start = 0;
|
|
3318
|
+
for (let i = 0; i < s.length; i++) {
|
|
3319
|
+
const ch = s[i];
|
|
3320
|
+
if (ch === "{" || ch === "(" || ch === "[")
|
|
3321
|
+
depth++;
|
|
3322
|
+
else if (ch === "}" || ch === ")" || ch === "]")
|
|
3323
|
+
depth--;
|
|
3324
|
+
else if (depth === 0 && s.startsWith(sep, i)) {
|
|
3325
|
+
parts.push(s.slice(start, i));
|
|
3326
|
+
start = i + sep.length;
|
|
3327
|
+
i += sep.length - 1;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
parts.push(s.slice(start));
|
|
3331
|
+
return parts;
|
|
3332
|
+
}
|
|
3333
|
+
/** LaTeX command spellings for comparison operators, mapped to the plain-infix text {@link Parser.matchCmpOp} understands. */
|
|
3334
|
+
const CMP_LATEX_CMDS = {
|
|
3335
|
+
"\\leq": "<=",
|
|
3336
|
+
"\\le": "<=",
|
|
3337
|
+
"\\geq": ">=",
|
|
3338
|
+
"\\ge": ">=",
|
|
3339
|
+
"\\neq": "!=",
|
|
3340
|
+
"\\ne": "!=",
|
|
3341
|
+
};
|
|
3342
|
+
/** Convert a LaTeX math string into the plain-infix syntax `Symbolic.parse`'s `Parser` understands. */
|
|
3343
|
+
function latexToInfix(latex) {
|
|
3344
|
+
const cleaned = latex.replace(/\\left|\\right/g, "").replace(/\\(?:,|;|!|quad|qquad| )/g, "");
|
|
3345
|
+
return transformLatex(cleaned);
|
|
3346
|
+
}
|
|
3347
|
+
function transformLatex(s) {
|
|
3348
|
+
let out = "";
|
|
3349
|
+
let i = 0;
|
|
3350
|
+
const readGroup = () => {
|
|
3351
|
+
if (s[i] !== "{")
|
|
3352
|
+
throw new Error(`Expected '{' at position ${i} in LaTeX source: ${s}`);
|
|
3353
|
+
const { content, next } = extractGroup(s, i);
|
|
3354
|
+
i = next;
|
|
3355
|
+
return content;
|
|
3356
|
+
};
|
|
3357
|
+
const readGroupOrParenOrToken = () => {
|
|
3358
|
+
if (s[i] === "{" || s[i] === "(") {
|
|
3359
|
+
const { content, next } = extractGroup(s, i);
|
|
3360
|
+
i = next;
|
|
3361
|
+
return content;
|
|
3362
|
+
}
|
|
3363
|
+
const m = /^[a-zA-Z_]\w*|^\d+\.?\d*(?:[eE][+-]?\d+)?/.exec(s.slice(i));
|
|
3364
|
+
if (m) {
|
|
3365
|
+
i += m[0].length;
|
|
3366
|
+
return m[0];
|
|
3367
|
+
}
|
|
3368
|
+
throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
|
|
3369
|
+
};
|
|
3370
|
+
/** Read a `(...)`/`{...}` group holding exactly 2 comma-separated arguments, for `call2` functions. */
|
|
3371
|
+
const readArgPair = () => {
|
|
3372
|
+
if (s[i] !== "(" && s[i] !== "{")
|
|
3373
|
+
throw new Error(`Expected '(' at position ${i} in LaTeX source: ${s}`);
|
|
3374
|
+
const { content, next } = extractGroup(s, i);
|
|
3375
|
+
i = next;
|
|
3376
|
+
const parts = splitTopLevelCommas(content);
|
|
3377
|
+
if (parts.length !== 2)
|
|
3378
|
+
throw new Error(`Expected 2 comma-separated arguments at position ${i} in LaTeX source: ${s}`);
|
|
3379
|
+
return [parts[0], parts[1]];
|
|
3380
|
+
};
|
|
3381
|
+
while (i < s.length) {
|
|
3382
|
+
const ch = s[i];
|
|
3383
|
+
if (ch === "\\") {
|
|
3384
|
+
const cmdMatch = /^\\[a-zA-Z]+/.exec(s.slice(i));
|
|
3385
|
+
if (!cmdMatch)
|
|
3386
|
+
throw new Error(`Unsupported LaTeX construct at position ${i}: ${s.slice(i, i + 10)}`);
|
|
3387
|
+
const cmd = cmdMatch[0];
|
|
3388
|
+
i += cmd.length;
|
|
3389
|
+
if (cmd === "\\cdot" || cmd === "\\times") {
|
|
3390
|
+
out += "*";
|
|
3391
|
+
continue;
|
|
3392
|
+
}
|
|
3393
|
+
if (cmd === "\\pi") {
|
|
3394
|
+
out += "pi";
|
|
3395
|
+
continue;
|
|
3396
|
+
}
|
|
3397
|
+
const cmpText = CMP_LATEX_CMDS[cmd];
|
|
3398
|
+
if (cmpText) {
|
|
3399
|
+
out += cmpText;
|
|
3400
|
+
continue;
|
|
3401
|
+
}
|
|
3402
|
+
if (cmd === "\\begin") {
|
|
3403
|
+
const envName = readGroup();
|
|
3404
|
+
if (envName !== "cases")
|
|
3405
|
+
throw new Error(`Unsupported LaTeX environment: \\begin{${envName}}`);
|
|
3406
|
+
const endMarker = "\\end{cases}";
|
|
3407
|
+
const endIdx = s.indexOf(endMarker, i);
|
|
3408
|
+
if (endIdx === -1)
|
|
3409
|
+
throw new Error("Unmatched \\begin{cases} in LaTeX source");
|
|
3410
|
+
const body = s.slice(i, endIdx);
|
|
3411
|
+
i = endIdx + endMarker.length;
|
|
3412
|
+
const rows = splitTopLevelOn(body, "\\\\");
|
|
3413
|
+
const parts = [];
|
|
3414
|
+
let otherwise = null;
|
|
3415
|
+
for (const row of rows) {
|
|
3416
|
+
if (row.trim() === "")
|
|
3417
|
+
continue;
|
|
3418
|
+
const cols = splitTopLevelOn(row, "&");
|
|
3419
|
+
if (cols.length !== 2)
|
|
3420
|
+
throw new Error(`Expected 'expr & cond' in \\begin{cases} row: ${row}`);
|
|
3421
|
+
const [exprPart, condPart] = cols;
|
|
3422
|
+
if (condPart.trim() === "\\text{otherwise}") {
|
|
3423
|
+
if (otherwise !== null)
|
|
3424
|
+
throw new Error("\\begin{cases} may have only one \\text{otherwise} row");
|
|
3425
|
+
otherwise = transformLatex(exprPart);
|
|
3426
|
+
}
|
|
3427
|
+
else {
|
|
3428
|
+
parts.push(`${transformLatex(condPart)}, ${transformLatex(exprPart)}`);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
if (otherwise === null) {
|
|
3432
|
+
throw new Error("\\begin{cases} must have exactly one \\text{otherwise} row (as its last row)");
|
|
3433
|
+
}
|
|
3434
|
+
out += `piecewise(${parts.concat(otherwise).join(", ")})`;
|
|
3435
|
+
continue;
|
|
3436
|
+
}
|
|
3437
|
+
if (cmd === "\\frac") {
|
|
3438
|
+
const numerator = readGroup();
|
|
3439
|
+
const denominator = readGroup();
|
|
3440
|
+
out += `((${transformLatex(numerator)})/(${transformLatex(denominator)}))`;
|
|
3441
|
+
continue;
|
|
3442
|
+
}
|
|
3443
|
+
if (cmd === "\\sqrt") {
|
|
3444
|
+
if (s[i] === "[") {
|
|
3445
|
+
const { content: root, next } = extractGroup(s, i);
|
|
3446
|
+
i = next;
|
|
3447
|
+
const arg = readGroup();
|
|
3448
|
+
const transformedRoot = transformLatex(root).trim();
|
|
3449
|
+
// \sqrt[3]{...} maps to cbrt(...) rather than a fractional power so
|
|
3450
|
+
// negative arguments evaluate correctly (Math.cbrt(-8) = -2, but
|
|
3451
|
+
// (-8)^(1/3) is NaN under JS's `**`).
|
|
3452
|
+
out +=
|
|
3453
|
+
transformedRoot === "3"
|
|
3454
|
+
? `cbrt(${transformLatex(arg)})`
|
|
3455
|
+
: `((${transformLatex(arg)})^(1/(${transformedRoot})))`;
|
|
3456
|
+
}
|
|
3457
|
+
else {
|
|
3458
|
+
const arg = readGroup();
|
|
3459
|
+
out += `sqrt(${transformLatex(arg)})`;
|
|
3460
|
+
}
|
|
3461
|
+
continue;
|
|
3462
|
+
}
|
|
3463
|
+
if (cmd === "\\log") {
|
|
3464
|
+
let base = "10";
|
|
3465
|
+
if (s[i] === "_" && s[i + 1] === "{") {
|
|
3466
|
+
const { content, next } = extractGroup(s, i + 1);
|
|
3467
|
+
i = next;
|
|
3468
|
+
base = content.trim();
|
|
3469
|
+
}
|
|
3470
|
+
if (base !== "10" && base !== "2") {
|
|
3471
|
+
throw new Error(`Unsupported LaTeX construct: \\log base ${base} (only base 10 and base 2 are supported)`);
|
|
3472
|
+
}
|
|
3473
|
+
const arg = readGroupOrParenOrToken();
|
|
3474
|
+
out += `${base === "2" ? "log2" : "log10"}(${transformLatex(arg)})`;
|
|
3475
|
+
continue;
|
|
3476
|
+
}
|
|
3477
|
+
if (cmd === "\\lfloor" || cmd === "\\lceil") {
|
|
3478
|
+
const closeCmd = cmd === "\\lfloor" ? "\\rfloor" : "\\rceil";
|
|
3479
|
+
const closeIdx = s.indexOf(closeCmd, i);
|
|
3480
|
+
if (closeIdx === -1)
|
|
3481
|
+
throw new Error(`Unmatched '${cmd}' in LaTeX source`);
|
|
3482
|
+
const inner = s.slice(i, closeIdx);
|
|
3483
|
+
i = closeIdx + closeCmd.length;
|
|
3484
|
+
out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner)})`;
|
|
3485
|
+
continue;
|
|
3486
|
+
}
|
|
3487
|
+
if (cmd === "\\operatorname") {
|
|
3488
|
+
const opName = readGroup();
|
|
3489
|
+
const binaryName = OPERATORNAME_BINARY_FUNC_NAMES[opName];
|
|
3490
|
+
if (binaryName) {
|
|
3491
|
+
const [left, right] = readArgPair();
|
|
3492
|
+
out += `${binaryName}(${transformLatex(left)}, ${transformLatex(right)})`;
|
|
3493
|
+
continue;
|
|
3494
|
+
}
|
|
3495
|
+
const funcName = OPERATORNAME_FUNC_NAMES[opName];
|
|
3496
|
+
if (!funcName)
|
|
3497
|
+
throw new Error(`Unsupported LaTeX construct: \\operatorname{${opName}}`);
|
|
3498
|
+
const arg = readGroupOrParenOrToken();
|
|
3499
|
+
out += `${funcName}(${transformLatex(arg)})`;
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
const binaryFuncName = LATEX_BINARY_FUNC_NAMES[cmd];
|
|
3503
|
+
if (binaryFuncName) {
|
|
3504
|
+
const [left, right] = readArgPair();
|
|
3505
|
+
out += `${binaryFuncName}(${transformLatex(left)}, ${transformLatex(right)})`;
|
|
3506
|
+
continue;
|
|
3507
|
+
}
|
|
3508
|
+
const funcName = LATEX_FUNC_NAMES[cmd];
|
|
3509
|
+
if (funcName) {
|
|
3510
|
+
const arg = readGroupOrParenOrToken();
|
|
3511
|
+
out += `${funcName}(${transformLatex(arg)})`;
|
|
3512
|
+
continue;
|
|
3513
|
+
}
|
|
3514
|
+
throw new Error(`Unsupported LaTeX construct: ${cmd}`);
|
|
3515
|
+
}
|
|
3516
|
+
if (ch === "|") {
|
|
3517
|
+
const closeIdx = s.indexOf("|", i + 1);
|
|
3518
|
+
if (closeIdx === -1)
|
|
3519
|
+
throw new Error("Unmatched '|' in LaTeX source");
|
|
3520
|
+
const inner = s.slice(i + 1, closeIdx);
|
|
3521
|
+
i = closeIdx + 1;
|
|
3522
|
+
out += `abs(${transformLatex(inner)})`;
|
|
3523
|
+
continue;
|
|
3524
|
+
}
|
|
3525
|
+
if (ch === "^" && s[i + 1] === "{") {
|
|
3526
|
+
const { content, next } = extractGroup(s, i + 1);
|
|
3527
|
+
i = next;
|
|
3528
|
+
out += `^(${transformLatex(content)})`;
|
|
3529
|
+
continue;
|
|
3530
|
+
}
|
|
3531
|
+
if (ch === "_" && s[i + 1] === "{") {
|
|
3532
|
+
const { content, next } = extractGroup(s, i + 1);
|
|
3533
|
+
i = next;
|
|
3534
|
+
out += `_${content.replace(/\W/g, "")}`;
|
|
3535
|
+
continue;
|
|
3536
|
+
}
|
|
3537
|
+
if (ch === "{") {
|
|
3538
|
+
const { content, next } = extractGroup(s, i);
|
|
3539
|
+
i = next;
|
|
3540
|
+
out += `(${transformLatex(content)})`;
|
|
3541
|
+
continue;
|
|
3542
|
+
}
|
|
3543
|
+
if (ch === "=") {
|
|
3544
|
+
// toLatexRec's "eq" case emits a bare "=" (standard LaTeX), but the
|
|
3545
|
+
// parser's comparison() requires 2 chars ("==") -- translate here so
|
|
3546
|
+
// toLatex/fromLatex round-trips for "eq".
|
|
3547
|
+
out += "==";
|
|
3548
|
+
i++;
|
|
3549
|
+
continue;
|
|
3550
|
+
}
|
|
3551
|
+
out += ch;
|
|
3552
|
+
i++;
|
|
3553
|
+
}
|
|
3554
|
+
return out;
|
|
3555
|
+
}
|
|
3556
|
+
// -- equation solving & factoring ----------------------------------------------
|
|
3557
|
+
/** Best-effort exact `Expr` for a numeric value: an integer, a reduced fraction, or (as a last resort) the raw float. */
|
|
3558
|
+
function toExactExpr(value) {
|
|
3559
|
+
if (Number.isInteger(value))
|
|
3560
|
+
return num(value);
|
|
3561
|
+
const r = Rational.fromNumber(value, 10_000);
|
|
3562
|
+
if (r.denominator === 1n)
|
|
3563
|
+
return num(Number(r.numerator));
|
|
3564
|
+
return div(num(Number(r.numerator)), num(Number(r.denominator)));
|
|
3565
|
+
}
|
|
3566
|
+
function evalPoly(coeffsLowToHigh, p) {
|
|
3567
|
+
let result = 0;
|
|
3568
|
+
for (let i = coeffsLowToHigh.length - 1; i >= 0; i--)
|
|
3569
|
+
result = result * p + coeffsLowToHigh[i];
|
|
3570
|
+
return result;
|
|
3571
|
+
}
|
|
3572
|
+
// -- systems of linear equations --------------------------------------------
|
|
3573
|
+
/**
|
|
3574
|
+
* If `eq` is linear in every name in `variables` (i.e. `eq = c1*v1 + ... +
|
|
3575
|
+
* cn*vn + constant`), returns `[c1, ..., cn, constant]`; otherwise `null`.
|
|
3576
|
+
* Linearity is checked by differentiating `eq` w.r.t. each variable and
|
|
3577
|
+
* confirming the derivative contains none of the system's variables (so it's
|
|
3578
|
+
* a true constant, not just constant-looking at the probe point).
|
|
3579
|
+
*/
|
|
3580
|
+
function linearCoeffsForSystem(eq, variables) {
|
|
3581
|
+
const zeroEnv = {};
|
|
3582
|
+
for (const name of variables)
|
|
3583
|
+
zeroEnv[name] = 0;
|
|
3584
|
+
const constant = evalExpr(eq, zeroEnv);
|
|
3585
|
+
if (!Number.isFinite(constant))
|
|
3586
|
+
return null;
|
|
3587
|
+
const coeffs = [];
|
|
3588
|
+
for (const name of variables) {
|
|
3589
|
+
const derivative = Symbolic.simplify(diff(eq, name));
|
|
3590
|
+
if (variables.some((other) => containsVar(derivative, other)))
|
|
3591
|
+
return null;
|
|
3592
|
+
const coeff = evalExpr(derivative, zeroEnv);
|
|
3593
|
+
if (!Number.isFinite(coeff))
|
|
3594
|
+
return null;
|
|
3595
|
+
coeffs.push(coeff);
|
|
3596
|
+
}
|
|
3597
|
+
return [...coeffs, constant];
|
|
3598
|
+
}
|
|
3599
|
+
/**
|
|
3600
|
+
* Extract `[c0, c1, ..., cn]` such that `expr = c0 + c1·x + ... + cn·x^n`, via
|
|
3601
|
+
* Taylor coefficients at 0 (`f^(n)(0)/n!`), verified against `expr` at a few
|
|
3602
|
+
* probe points so non-polynomial expressions (e.g. `sin(x)`) are rejected
|
|
3603
|
+
* rather than silently truncated. Returns `null` if `expr` isn't a polynomial
|
|
3604
|
+
* of degree ≤ `maxDegree` in `variable`.
|
|
3605
|
+
*/
|
|
3606
|
+
function polynomialCoeffs(expr, variable, maxDegree) {
|
|
3607
|
+
const coeffs = [];
|
|
3608
|
+
let term = expr;
|
|
3609
|
+
let factorial = 1;
|
|
3610
|
+
for (let n = 0; n <= maxDegree; n++) {
|
|
3611
|
+
if (n > 0)
|
|
3612
|
+
factorial *= n;
|
|
3613
|
+
const c0 = evalExpr(term, { [variable]: 0 });
|
|
3614
|
+
if (!Number.isFinite(c0))
|
|
3615
|
+
return null;
|
|
3616
|
+
coeffs.push(c0 / factorial);
|
|
3617
|
+
// Simplify between differentiation steps: an unsimplified term like the
|
|
3618
|
+
// derivative of x^0 (`0·x^-1`) evaluates to `0·Infinity = NaN` at x=0
|
|
3619
|
+
// even though it is symbolically zero — simplifying collapses `0·anything`
|
|
3620
|
+
// to `0` before that indeterminate form can arise.
|
|
3621
|
+
term = Symbolic.simplify(diff(term, variable));
|
|
3622
|
+
}
|
|
3623
|
+
while (coeffs.length > 1 && Math.abs(coeffs[coeffs.length - 1]) < 1e-9)
|
|
3624
|
+
coeffs.pop();
|
|
3625
|
+
for (const p of [1.3, -2.1, 3.7]) {
|
|
3626
|
+
const actual = evalExpr(expr, { [variable]: p });
|
|
3627
|
+
if (!Number.isFinite(actual))
|
|
3628
|
+
return null;
|
|
3629
|
+
if (Math.abs(evalPoly(coeffs, p) - actual) > 1e-6 * Math.max(1, Math.abs(actual)))
|
|
3630
|
+
return null;
|
|
3631
|
+
}
|
|
3632
|
+
return coeffs;
|
|
3633
|
+
}
|
|
3634
|
+
function intGcd(a, b) {
|
|
3635
|
+
let x = Math.abs(a);
|
|
3636
|
+
let y = Math.abs(b);
|
|
3637
|
+
while (y)
|
|
3638
|
+
[x, y] = [y, x % y];
|
|
3639
|
+
return x || 1;
|
|
3640
|
+
}
|
|
3641
|
+
function divisorsOf(n) {
|
|
3642
|
+
const a = Math.round(Math.abs(n));
|
|
3643
|
+
if (a === 0)
|
|
3644
|
+
return [1];
|
|
3645
|
+
const result = [];
|
|
3646
|
+
for (let i = 1; i <= a; i++)
|
|
3647
|
+
if (a % i === 0)
|
|
3648
|
+
result.push(i);
|
|
3649
|
+
return result;
|
|
3650
|
+
}
|
|
3651
|
+
/** Rational-root-theorem search for one root of an integer-coefficient polynomial (low-to-high `coeffs`). */
|
|
3652
|
+
function findRationalRoot(coeffs) {
|
|
3653
|
+
const n = coeffs.length - 1;
|
|
3654
|
+
const rounded = coeffs.map((c) => Math.round(c));
|
|
3655
|
+
const isIntegerPoly = coeffs.every((c, i) => Math.abs(c - rounded[i]) < 1e-6);
|
|
3656
|
+
if (!isIntegerPoly)
|
|
3657
|
+
return null;
|
|
3658
|
+
const pCands = divisorsOf(rounded[0]);
|
|
3659
|
+
const qCands = divisorsOf(rounded[n]);
|
|
3660
|
+
const tried = new Set();
|
|
3661
|
+
for (const p of pCands) {
|
|
3662
|
+
for (const q of qCands) {
|
|
3663
|
+
for (const sign of [1, -1]) {
|
|
3664
|
+
const candidate = (sign * p) / q;
|
|
3665
|
+
if (tried.has(candidate))
|
|
3666
|
+
continue;
|
|
3667
|
+
tried.add(candidate);
|
|
3668
|
+
if (Math.abs(evalPoly(rounded, candidate)) < 1e-6)
|
|
3669
|
+
return candidate;
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
return null;
|
|
3674
|
+
}
|
|
3675
|
+
/** Synthetic division of a polynomial (low-to-high `coeffs`) by `(x - root)`; assumes `root` is an exact root. */
|
|
3676
|
+
function syntheticDivide(coeffsLowToHigh, root) {
|
|
3677
|
+
const desc = [...coeffsLowToHigh].reverse();
|
|
3678
|
+
const quotientDesc = [desc[0]];
|
|
3679
|
+
for (let i = 1; i < desc.length - 1; i++) {
|
|
3680
|
+
quotientDesc.push(desc[i] + root * quotientDesc[i - 1]);
|
|
3681
|
+
}
|
|
3682
|
+
return quotientDesc.reverse();
|
|
3683
|
+
}
|
|
3684
|
+
function solvePolynomial(coeffsIn) {
|
|
3685
|
+
const coeffs = [...coeffsIn];
|
|
3686
|
+
while (coeffs.length > 1 && Math.abs(coeffs[coeffs.length - 1]) < 1e-9)
|
|
3687
|
+
coeffs.pop();
|
|
3688
|
+
const n = coeffs.length - 1;
|
|
3689
|
+
if (n <= 0) {
|
|
3690
|
+
// Degree collapsed to a constant (or the input was empty). If that
|
|
3691
|
+
// remaining constant is genuinely nonzero, "constant = 0" has no real
|
|
3692
|
+
// solution -- [] is correct. But if it's ~0 too, every coefficient of
|
|
3693
|
+
// the original polynomial was ~0 (e.g. solve("0")): the equation is
|
|
3694
|
+
// identically true, so every real number is a solution -- [] can't
|
|
3695
|
+
// represent that, so signal it distinctly.
|
|
3696
|
+
if (coeffs.length === 0 || Math.abs(coeffs[0]) < 1e-9) {
|
|
3697
|
+
throw new InfiniteSolutionsError();
|
|
3698
|
+
}
|
|
3699
|
+
return [];
|
|
3700
|
+
}
|
|
3701
|
+
if (n === 1) {
|
|
3702
|
+
const [c0, c1] = coeffs;
|
|
3703
|
+
return [toExactExpr(-c0 / c1)];
|
|
3704
|
+
}
|
|
3705
|
+
if (n === 2) {
|
|
3706
|
+
const [c0, c1, c2] = coeffs;
|
|
3707
|
+
const disc = c1 * c1 - 4 * c2 * c0;
|
|
3708
|
+
if (disc < -1e-9)
|
|
3709
|
+
return [];
|
|
3710
|
+
const negB = -c1;
|
|
3711
|
+
const twoA = 2 * c2;
|
|
3712
|
+
if (Math.abs(disc) < 1e-9)
|
|
3713
|
+
return [toExactExpr(negB / twoA)];
|
|
3714
|
+
const sqrtDisc = Math.sqrt(disc);
|
|
3715
|
+
if (Math.abs(sqrtDisc - Math.round(sqrtDisc)) < 1e-9) {
|
|
3716
|
+
return [toExactExpr((negB + sqrtDisc) / twoA), toExactExpr((negB - sqrtDisc) / twoA)];
|
|
3717
|
+
}
|
|
3718
|
+
const sqrtExpr = fn("sqrt", toExactExpr(disc));
|
|
3719
|
+
return [
|
|
3720
|
+
div(add(toExactExpr(negB), sqrtExpr), toExactExpr(twoA)),
|
|
3721
|
+
div(sub(toExactExpr(negB), sqrtExpr), toExactExpr(twoA)),
|
|
3722
|
+
];
|
|
3723
|
+
}
|
|
3724
|
+
// The rational-root search below enumerates divisors of the constant
|
|
3725
|
+
// term as numerator candidates ("p" in the rational root theorem's p/q).
|
|
3726
|
+
// A zero constant term is the degenerate case for that search --
|
|
3727
|
+
// `divisorsOf(0)` special-cases to `[1]` (to avoid an infinite/undefined
|
|
3728
|
+
// "divisors of 0" enumeration), but `[1]` can never produce the candidate
|
|
3729
|
+
// `0` itself, so `x = 0` -- which trivially *is* a root whenever the
|
|
3730
|
+
// constant term is zero -- is silently never tried and the search fails
|
|
3731
|
+
// outright (issue #52). Handle it up front instead: pull out `(x - 0)` as
|
|
3732
|
+
// a factor (same as `Symbolic.factor` already documents doing for its own
|
|
3733
|
+
// "common power of `variable`" step), record the `0` root, and recurse on
|
|
3734
|
+
// the deflated polynomial, which now has one lower degree and either a
|
|
3735
|
+
// nonzero constant term (so the rational-root search below can proceed
|
|
3736
|
+
// normally) or a low-enough degree for the n<=2 closed-form branches
|
|
3737
|
+
// above to handle it directly.
|
|
3738
|
+
if (Math.abs(coeffs[0]) < 1e-9) {
|
|
3739
|
+
return [toExactExpr(0), ...solvePolynomial(coeffs.slice(1))];
|
|
3740
|
+
}
|
|
3741
|
+
const root = findRationalRoot(coeffs);
|
|
3742
|
+
if (root === null) {
|
|
3743
|
+
throw new Error(`solve: no closed-form root found for a degree-${n} polynomial (only rational roots are searched for degree ≥ 3).`);
|
|
3744
|
+
}
|
|
3745
|
+
const deflated = syntheticDivide(coeffs, root);
|
|
3746
|
+
return [toExactExpr(root), ...solvePolynomial(deflated)];
|
|
3747
|
+
}
|
|
3748
|
+
/** Build `c0 + c1·x + c2·x^2 + ...` from low-to-high coefficients. */
|
|
3749
|
+
function polyToExpr(coeffs, variable) {
|
|
3750
|
+
let result = null;
|
|
3751
|
+
for (let i = 0; i < coeffs.length; i++) {
|
|
3752
|
+
if (coeffs[i] === 0)
|
|
3753
|
+
continue;
|
|
3754
|
+
const monomial = i === 0 ? toExactExpr(coeffs[i]) : mul(toExactExpr(coeffs[i]), i === 1 ? v(variable) : pow(v(variable), num(i)));
|
|
3755
|
+
result = result === null ? monomial : add(result, monomial);
|
|
3756
|
+
}
|
|
3757
|
+
return result ?? num(0);
|
|
3758
|
+
}
|
|
3759
|
+
function factorPolynomial(coeffsIn, variable) {
|
|
3760
|
+
let coeffs = [...coeffsIn];
|
|
3761
|
+
while (coeffs.length > 1 && Math.abs(coeffs[coeffs.length - 1]) < 1e-9)
|
|
3762
|
+
coeffs.pop();
|
|
3763
|
+
if (coeffs.every((c) => Math.abs(c) < 1e-9))
|
|
3764
|
+
return num(0);
|
|
3765
|
+
let xPower = 0;
|
|
3766
|
+
while (coeffs.length > 1 && Math.abs(coeffs[0]) < 1e-9) {
|
|
3767
|
+
coeffs.shift();
|
|
3768
|
+
xPower++;
|
|
3769
|
+
}
|
|
3770
|
+
const rounded = coeffs.map((c) => Math.round(c));
|
|
3771
|
+
const isIntegerPoly = coeffs.every((c, i) => Math.abs(c - rounded[i]) < 1e-6);
|
|
3772
|
+
let gcd = 1;
|
|
3773
|
+
if (isIntegerPoly) {
|
|
3774
|
+
gcd = rounded.reduce((g, c) => (c === 0 ? g : intGcd(g, Math.abs(c))), 0) || 1;
|
|
3775
|
+
coeffs = rounded.map((c) => c / gcd);
|
|
3776
|
+
}
|
|
3777
|
+
const linearRoots = [];
|
|
3778
|
+
let remaining = coeffs;
|
|
3779
|
+
while (remaining.length - 1 >= 3) {
|
|
3780
|
+
const root = findRationalRoot(remaining);
|
|
3781
|
+
if (root === null)
|
|
3782
|
+
break;
|
|
3783
|
+
linearRoots.push(root);
|
|
3784
|
+
remaining = syntheticDivide(remaining, root);
|
|
3785
|
+
}
|
|
3786
|
+
if (remaining.length - 1 === 2) {
|
|
3787
|
+
const [c0, c1, c2] = remaining;
|
|
3788
|
+
const disc = c1 * c1 - 4 * c2 * c0;
|
|
3789
|
+
if (disc >= 0) {
|
|
3790
|
+
const sqrtDisc = Math.sqrt(disc);
|
|
3791
|
+
if (Math.abs(sqrtDisc - Math.round(sqrtDisc)) < 1e-9) {
|
|
3792
|
+
linearRoots.push((-c1 + sqrtDisc) / (2 * c2), (-c1 - sqrtDisc) / (2 * c2));
|
|
3793
|
+
remaining = [c2];
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
else if (remaining.length - 1 === 1) {
|
|
3798
|
+
const [c0, c1] = remaining;
|
|
3799
|
+
linearRoots.push(-c0 / c1);
|
|
3800
|
+
remaining = [c1];
|
|
3801
|
+
}
|
|
3802
|
+
let result = toExactExpr(gcd);
|
|
3803
|
+
if (xPower === 1)
|
|
3804
|
+
result = mul(result, v(variable));
|
|
3805
|
+
else if (xPower > 1)
|
|
3806
|
+
result = mul(result, pow(v(variable), num(xPower)));
|
|
3807
|
+
for (const r of linearRoots) {
|
|
3808
|
+
result = mul(result, Math.abs(r) < 1e-9 ? v(variable) : sub(v(variable), toExactExpr(r)));
|
|
3809
|
+
}
|
|
3810
|
+
if (remaining.length > 1) {
|
|
3811
|
+
result = mul(result, polyToExpr(remaining, variable));
|
|
3812
|
+
}
|
|
3813
|
+
else if (Math.abs(remaining[0] - 1) > 1e-9) {
|
|
3814
|
+
result = mul(result, toExactExpr(remaining[0]));
|
|
3815
|
+
}
|
|
3816
|
+
return Symbolic.simplify(result);
|
|
3817
|
+
}
|
|
3818
|
+
// -- limits ---------------------------------------------------------------------
|
|
3819
|
+
function evalNear(e, x, point, direction) {
|
|
3820
|
+
if (direction === "both")
|
|
3821
|
+
return evalExpr(e, { [x]: point });
|
|
3822
|
+
const eps = direction === "right" ? 1e-6 : -1e-6;
|
|
3823
|
+
return evalExpr(e, { [x]: point + eps });
|
|
3824
|
+
}
|
|
3825
|
+
function limitAt(e, x, point, direction, depth) {
|
|
3826
|
+
const direct = evalNear(e, x, point, direction);
|
|
3827
|
+
if (Number.isFinite(direct))
|
|
3828
|
+
return direct;
|
|
3829
|
+
if (e.type === "div" && depth < 12) {
|
|
3830
|
+
const fAt = evalNear(e.left, x, point, direction);
|
|
3831
|
+
const gAt = evalNear(e.right, x, point, direction);
|
|
3832
|
+
const indeterminate = (Math.abs(fAt) < 1e-6 && Math.abs(gAt) < 1e-6) || (!Number.isFinite(fAt) && !Number.isFinite(gAt));
|
|
3833
|
+
if (indeterminate) {
|
|
3834
|
+
// Simplify between successive L'Hopital differentiations -- otherwise
|
|
3835
|
+
// the expression tree grows without bound across iterations (each
|
|
3836
|
+
// product/quotient-rule application duplicates an undifferentiated
|
|
3837
|
+
// operand verbatim into the result) and a limit needing more than a
|
|
3838
|
+
// couple of rounds (e.g. most limits at infinity) blows up memory
|
|
3839
|
+
// long before the depth<12 cap is reached.
|
|
3840
|
+
const df = Symbolic.simplify(diff(e.left, x));
|
|
3841
|
+
const dg = Symbolic.simplify(diff(e.right, x));
|
|
3842
|
+
return limitAt(div(df, dg), x, point, direction, depth + 1);
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
return direct;
|
|
3846
|
+
}
|
|
3847
|
+
// -- recursive-descent parser ----------------------------------------------
|
|
3848
|
+
class Parser {
|
|
3849
|
+
s;
|
|
3850
|
+
pos = 0;
|
|
3851
|
+
constructor(input) {
|
|
3852
|
+
this.s = input.replace(/\s+/g, "");
|
|
3853
|
+
}
|
|
3854
|
+
parse() {
|
|
3855
|
+
const e = this.comparison();
|
|
3856
|
+
if (this.pos < this.s.length)
|
|
3857
|
+
throw new Error(`Unexpected token at ${this.pos}: ${this.s.slice(this.pos)}`);
|
|
3858
|
+
return e;
|
|
3859
|
+
}
|
|
3860
|
+
peek() {
|
|
3861
|
+
return this.s[this.pos] ?? "";
|
|
3862
|
+
}
|
|
3863
|
+
/** Parse a comma-separated list of expressions, for multi-arg function calls. */
|
|
3864
|
+
argList() {
|
|
3865
|
+
const args = [this.expr()];
|
|
3866
|
+
while (this.peek() === ",") {
|
|
3867
|
+
this.pos++;
|
|
3868
|
+
args.push(this.expr());
|
|
3869
|
+
}
|
|
3870
|
+
return args;
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Comparisons bind loosest of all (below `+`/`-`), and are non-chaining --
|
|
3874
|
+
* `a < b < c` is not supported, only one comparison per level, matching
|
|
3875
|
+
* typical CAS grammars. 2-char operators (`<=`,`>=`,`==`,`!=`) need
|
|
3876
|
+
* lookahead before falling back to the 1-char `<`/`>` (no other multi-char
|
|
3877
|
+
* operator exists elsewhere in this grammar).
|
|
3878
|
+
*/
|
|
3879
|
+
comparison() {
|
|
3880
|
+
const left = this.expr();
|
|
3881
|
+
const op = this.matchCmpOp();
|
|
3882
|
+
if (!op)
|
|
3883
|
+
return left;
|
|
3884
|
+
return cmp(op, left, this.expr());
|
|
3885
|
+
}
|
|
3886
|
+
matchCmpOp() {
|
|
3887
|
+
const CMP_TWO_CHAR = { "<=": "le", ">=": "ge", "==": "eq", "!=": "ne" };
|
|
3888
|
+
const two = this.s.slice(this.pos, this.pos + 2);
|
|
3889
|
+
const twoOp = CMP_TWO_CHAR[two];
|
|
3890
|
+
if (twoOp) {
|
|
3891
|
+
this.pos += 2;
|
|
3892
|
+
return twoOp;
|
|
3893
|
+
}
|
|
3894
|
+
if (this.peek() === "<") {
|
|
3895
|
+
this.pos++;
|
|
3896
|
+
return "lt";
|
|
3897
|
+
}
|
|
3898
|
+
if (this.peek() === ">") {
|
|
3899
|
+
this.pos++;
|
|
3900
|
+
return "gt";
|
|
3901
|
+
}
|
|
3902
|
+
return null;
|
|
3903
|
+
}
|
|
3904
|
+
/**
|
|
3905
|
+
* `piecewise(cond1, expr1, cond2, expr2, ..., otherwise)` -- alternating
|
|
3906
|
+
* (cond, expr) pairs plus a trailing `otherwise`, so an odd arg count >= 3
|
|
3907
|
+
* is required. Both cond and expr slots parse at `comparison()` level
|
|
3908
|
+
* (not the generic `argList()`/`expr()`) since cond slots need comparison
|
|
3909
|
+
* operators; `comparison()` gracefully degrades to plain `expr()` when no
|
|
3910
|
+
* `<`/`>`/etc. is found, so this is a strict superset with no downside for
|
|
3911
|
+
* the expr slots.
|
|
3912
|
+
*/
|
|
3913
|
+
piecewiseArgs() {
|
|
3914
|
+
const args = [this.comparison()];
|
|
3915
|
+
while (this.peek() === ",") {
|
|
3916
|
+
this.pos++;
|
|
3917
|
+
args.push(this.comparison());
|
|
3918
|
+
}
|
|
3919
|
+
if (this.peek() !== ")")
|
|
3920
|
+
throw new Error("Expected ')'");
|
|
3921
|
+
this.pos++;
|
|
3922
|
+
if (args.length < 3 || args.length % 2 === 0) {
|
|
3923
|
+
throw new Error(`piecewise() expects an odd number of arguments >= 3 (cond, expr, ..., otherwise), got ${args.length}`);
|
|
3924
|
+
}
|
|
3925
|
+
const branches = [];
|
|
3926
|
+
for (let i = 0; i + 1 < args.length - 1; i += 2)
|
|
3927
|
+
branches.push({ cond: args[i], expr: args[i + 1] });
|
|
3928
|
+
return piecewise(branches, args[args.length - 1]);
|
|
3929
|
+
}
|
|
3930
|
+
/**
|
|
3931
|
+
* `sum(i, from, to, body)` / `product(i, from, to, body)` -- the bound
|
|
3932
|
+
* variable is a bare identifier (not a general expression, unlike the
|
|
3933
|
+
* other three slots), parsed directly off `this.s` rather than through
|
|
3934
|
+
* `comparison()`/`expr()`. `from`/`to`/`body` parse at `comparison()`
|
|
3935
|
+
* level, same rationale as `piecewiseArgs`.
|
|
3936
|
+
*/
|
|
3937
|
+
sumOrProductArgs(kind) {
|
|
3938
|
+
const idMatch = /^[a-zA-Z_]\w*/.exec(this.s.slice(this.pos));
|
|
3939
|
+
if (!idMatch)
|
|
3940
|
+
throw new Error(`${kind}() expects a bound variable name as its first argument`);
|
|
3941
|
+
const variable = idMatch[0];
|
|
3942
|
+
this.pos += variable.length;
|
|
3943
|
+
if (this.peek() !== ",")
|
|
3944
|
+
throw new Error("Expected ','");
|
|
3945
|
+
this.pos++;
|
|
3946
|
+
const from = this.comparison();
|
|
3947
|
+
if (this.peek() !== ",")
|
|
3948
|
+
throw new Error("Expected ','");
|
|
3949
|
+
this.pos++;
|
|
3950
|
+
const to = this.comparison();
|
|
3951
|
+
if (this.peek() !== ",")
|
|
3952
|
+
throw new Error("Expected ','");
|
|
3953
|
+
this.pos++;
|
|
3954
|
+
const body = this.comparison();
|
|
3955
|
+
if (this.peek() !== ")")
|
|
3956
|
+
throw new Error("Expected ')'");
|
|
3957
|
+
this.pos++;
|
|
3958
|
+
return kind === "sum" ? sumExpr(variable, from, to, body) : productExpr(variable, from, to, body);
|
|
3959
|
+
}
|
|
3960
|
+
expr() {
|
|
3961
|
+
let left = this.term();
|
|
3962
|
+
while (this.peek() === "+" || this.peek() === "-") {
|
|
3963
|
+
const op = this.s[this.pos++];
|
|
3964
|
+
const right = this.term();
|
|
3965
|
+
left = op === "+" ? add(left, right) : sub(left, right);
|
|
3966
|
+
}
|
|
3967
|
+
return left;
|
|
3968
|
+
}
|
|
3969
|
+
term() {
|
|
3970
|
+
let left = this.factor();
|
|
3971
|
+
while (this.peek() === "*" || this.peek() === "/") {
|
|
3972
|
+
const op = this.s[this.pos++];
|
|
3973
|
+
const right = this.factor();
|
|
3974
|
+
left = op === "*" ? mul(left, right) : div(left, right);
|
|
3975
|
+
}
|
|
3976
|
+
return left;
|
|
3977
|
+
}
|
|
3978
|
+
factor() {
|
|
3979
|
+
if (this.peek() === "-") {
|
|
3980
|
+
this.pos++;
|
|
3981
|
+
return neg(this.factor());
|
|
3982
|
+
}
|
|
3983
|
+
const base = this.base();
|
|
3984
|
+
if (this.peek() === "^") {
|
|
3985
|
+
this.pos++;
|
|
3986
|
+
return pow(base, this.factor()); // right-associative
|
|
3987
|
+
}
|
|
3988
|
+
return base;
|
|
3989
|
+
}
|
|
3990
|
+
base() {
|
|
3991
|
+
if (this.peek() === "(") {
|
|
3992
|
+
this.pos++;
|
|
3993
|
+
const e = this.comparison(); // comparison() (not expr()) so `(x<3)*5` parses
|
|
3994
|
+
if (this.peek() !== ")")
|
|
3995
|
+
throw new Error("Expected ')'");
|
|
3996
|
+
this.pos++;
|
|
3997
|
+
return e;
|
|
3998
|
+
}
|
|
3999
|
+
if (this.peek() === "|") {
|
|
4000
|
+
this.pos++;
|
|
4001
|
+
const e = this.expr();
|
|
4002
|
+
if (this.peek() !== "|")
|
|
4003
|
+
throw new Error("Expected '|'");
|
|
4004
|
+
this.pos++;
|
|
4005
|
+
return fn("abs", e);
|
|
4006
|
+
}
|
|
4007
|
+
// number
|
|
4008
|
+
const numMatch = /^\d+\.?\d*(?:[eE][+-]?\d+)?/.exec(this.s.slice(this.pos));
|
|
4009
|
+
if (numMatch) {
|
|
4010
|
+
this.pos += numMatch[0].length;
|
|
4011
|
+
return num(Number(numMatch[0]));
|
|
4012
|
+
}
|
|
4013
|
+
// identifier (function or variable or constant)
|
|
4014
|
+
const idMatch = /^[a-zA-Z_]\w*/.exec(this.s.slice(this.pos));
|
|
4015
|
+
if (idMatch) {
|
|
4016
|
+
const name = idMatch[0];
|
|
4017
|
+
this.pos += name.length;
|
|
4018
|
+
if ((name === "log" || name === "clamp") && this.peek() === "(") {
|
|
4019
|
+
this.pos++;
|
|
4020
|
+
const args = this.argList();
|
|
4021
|
+
if (this.peek() !== ")")
|
|
4022
|
+
throw new Error("Expected ')'");
|
|
4023
|
+
this.pos++;
|
|
4024
|
+
if (name === "log") {
|
|
4025
|
+
if (args.length !== 2)
|
|
4026
|
+
throw new Error(`log() expects 2 arguments (base, x), got ${args.length}`);
|
|
4027
|
+
return div(fn("ln", args[1]), fn("ln", args[0]));
|
|
4028
|
+
}
|
|
4029
|
+
if (args.length !== 3)
|
|
4030
|
+
throw new Error(`clamp() expects 3 arguments (x, lo, hi), got ${args.length}`);
|
|
4031
|
+
return call2("min", call2("max", args[0], args[1]), args[2]);
|
|
4032
|
+
}
|
|
4033
|
+
if (name === "piecewise" && this.peek() === "(") {
|
|
4034
|
+
this.pos++;
|
|
4035
|
+
return this.piecewiseArgs();
|
|
4036
|
+
}
|
|
4037
|
+
if ((name === "sum" || name === "product") && this.peek() === "(") {
|
|
4038
|
+
this.pos++;
|
|
4039
|
+
return this.sumOrProductArgs(name);
|
|
4040
|
+
}
|
|
4041
|
+
if (BINARY_FUNCS.includes(name) && this.peek() === "(") {
|
|
4042
|
+
const binaryName = name;
|
|
4043
|
+
this.pos++;
|
|
4044
|
+
const args = this.argList();
|
|
4045
|
+
if (this.peek() !== ")")
|
|
4046
|
+
throw new Error("Expected ')'");
|
|
4047
|
+
this.pos++;
|
|
4048
|
+
if (binaryName === "atan2") {
|
|
4049
|
+
if (args.length !== 2)
|
|
4050
|
+
throw new Error(`atan2() expects exactly 2 arguments, got ${args.length}`);
|
|
4051
|
+
return call2("atan2", args[0], args[1]);
|
|
4052
|
+
}
|
|
4053
|
+
if (args.length < 2)
|
|
4054
|
+
throw new Error(`${binaryName}() expects at least 2 arguments, got ${args.length}`);
|
|
4055
|
+
return args.reduce((l, r) => call2(binaryName, l, r));
|
|
4056
|
+
}
|
|
4057
|
+
const canonical = FUNCS.includes(name)
|
|
4058
|
+
? name
|
|
4059
|
+
: FUNC_ALIASES[name];
|
|
4060
|
+
if (canonical && this.peek() === "(") {
|
|
4061
|
+
this.pos++;
|
|
4062
|
+
const arg = this.expr();
|
|
4063
|
+
if (this.peek() !== ")")
|
|
4064
|
+
throw new Error("Expected ')'");
|
|
4065
|
+
this.pos++;
|
|
4066
|
+
return fn(canonical, arg);
|
|
4067
|
+
}
|
|
4068
|
+
if (name === "pi")
|
|
4069
|
+
return num(Math.PI);
|
|
4070
|
+
if (name === "e")
|
|
4071
|
+
return num(Math.E);
|
|
4072
|
+
return v(name);
|
|
4073
|
+
}
|
|
4074
|
+
throw new Error(`Unexpected token at ${this.pos}: ${this.s.slice(this.pos)}`);
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
//# sourceMappingURL=Symbolic.js.map
|