@johnhenry/math 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/README.md +165 -0
  2. package/dist/Bivector4.d.ts +52 -0
  3. package/dist/Bivector4.d.ts.map +1 -0
  4. package/dist/Bivector4.js +105 -0
  5. package/dist/Bivector4.js.map +1 -0
  6. package/dist/CellGraph.d.ts +259 -0
  7. package/dist/CellGraph.d.ts.map +1 -0
  8. package/dist/CellGraph.js +637 -0
  9. package/dist/CellGraph.js.map +1 -0
  10. package/dist/Clifford4Internal.d.ts +35 -0
  11. package/dist/Clifford4Internal.d.ts.map +1 -0
  12. package/dist/Clifford4Internal.js +86 -0
  13. package/dist/Clifford4Internal.js.map +1 -0
  14. package/dist/Combinatorics.d.ts +61 -0
  15. package/dist/Combinatorics.d.ts.map +1 -0
  16. package/dist/Combinatorics.js +186 -0
  17. package/dist/Combinatorics.js.map +1 -0
  18. package/dist/CombinatoricsGenerators.d.ts +44 -0
  19. package/dist/CombinatoricsGenerators.d.ts.map +1 -0
  20. package/dist/CombinatoricsGenerators.js +148 -0
  21. package/dist/CombinatoricsGenerators.js.map +1 -0
  22. package/dist/ComplexMath.d.ts +46 -0
  23. package/dist/ComplexMath.d.ts.map +1 -0
  24. package/dist/ComplexMath.js +98 -0
  25. package/dist/ComplexMath.js.map +1 -0
  26. package/dist/ComplexNumber.d.ts +174 -0
  27. package/dist/ComplexNumber.d.ts.map +1 -0
  28. package/dist/ComplexNumber.js +443 -0
  29. package/dist/ComplexNumber.js.map +1 -0
  30. package/dist/Cycle.d.ts +42 -0
  31. package/dist/Cycle.d.ts.map +1 -0
  32. package/dist/Cycle.js +120 -0
  33. package/dist/Cycle.js.map +1 -0
  34. package/dist/Decimal.d.ts +45 -0
  35. package/dist/Decimal.d.ts.map +1 -0
  36. package/dist/Decimal.js +135 -0
  37. package/dist/Decimal.js.map +1 -0
  38. package/dist/Distributions.d.ts +55 -0
  39. package/dist/Distributions.d.ts.map +1 -0
  40. package/dist/Distributions.js +222 -0
  41. package/dist/Distributions.js.map +1 -0
  42. package/dist/DualNumber.d.ts +42 -0
  43. package/dist/DualNumber.d.ts.map +1 -0
  44. package/dist/DualNumber.js +91 -0
  45. package/dist/DualNumber.js.map +1 -0
  46. package/dist/Environment.d.ts +44 -0
  47. package/dist/Environment.d.ts.map +1 -0
  48. package/dist/Environment.js +129 -0
  49. package/dist/Environment.js.map +1 -0
  50. package/dist/Expression.d.ts +19 -0
  51. package/dist/Expression.d.ts.map +1 -0
  52. package/dist/Expression.js +43 -0
  53. package/dist/Expression.js.map +1 -0
  54. package/dist/FFT.d.ts +21 -0
  55. package/dist/FFT.d.ts.map +1 -0
  56. package/dist/FFT.js +136 -0
  57. package/dist/FFT.js.map +1 -0
  58. package/dist/GeometricAlgebra4.d.ts +73 -0
  59. package/dist/GeometricAlgebra4.d.ts.map +1 -0
  60. package/dist/GeometricAlgebra4.js +115 -0
  61. package/dist/GeometricAlgebra4.js.map +1 -0
  62. package/dist/Geometry.d.ts +41 -0
  63. package/dist/Geometry.d.ts.map +1 -0
  64. package/dist/Geometry.js +167 -0
  65. package/dist/Geometry.js.map +1 -0
  66. package/dist/Graph.d.ts +72 -0
  67. package/dist/Graph.d.ts.map +1 -0
  68. package/dist/Graph.js +337 -0
  69. package/dist/Graph.js.map +1 -0
  70. package/dist/Graph3DUtils.d.ts +80 -0
  71. package/dist/Graph3DUtils.d.ts.map +1 -0
  72. package/dist/Graph3DUtils.js +156 -0
  73. package/dist/Graph3DUtils.js.map +1 -0
  74. package/dist/GraphUtils.d.ts +67 -0
  75. package/dist/GraphUtils.d.ts.map +1 -0
  76. package/dist/GraphUtils.js +72 -0
  77. package/dist/GraphUtils.js.map +1 -0
  78. package/dist/GroupTheory.d.ts +73 -0
  79. package/dist/GroupTheory.d.ts.map +1 -0
  80. package/dist/GroupTheory.js +194 -0
  81. package/dist/GroupTheory.js.map +1 -0
  82. package/dist/IntUtils.d.ts +33 -0
  83. package/dist/IntUtils.d.ts.map +1 -0
  84. package/dist/IntUtils.js +185 -0
  85. package/dist/IntUtils.js.map +1 -0
  86. package/dist/Interval.d.ts +44 -0
  87. package/dist/Interval.d.ts.map +1 -0
  88. package/dist/Interval.js +165 -0
  89. package/dist/Interval.js.map +1 -0
  90. package/dist/Lattice.d.ts +48 -0
  91. package/dist/Lattice.d.ts.map +1 -0
  92. package/dist/Lattice.js +65 -0
  93. package/dist/Lattice.js.map +1 -0
  94. package/dist/Logic.d.ts +22 -0
  95. package/dist/Logic.d.ts.map +1 -0
  96. package/dist/Logic.js +34 -0
  97. package/dist/Logic.js.map +1 -0
  98. package/dist/MatrixMath.d.ts +71 -0
  99. package/dist/MatrixMath.d.ts.map +1 -0
  100. package/dist/MatrixMath.js +302 -0
  101. package/dist/MatrixMath.js.map +1 -0
  102. package/dist/NumberTheory.d.ts +49 -0
  103. package/dist/NumberTheory.d.ts.map +1 -0
  104. package/dist/NumberTheory.js +230 -0
  105. package/dist/NumberTheory.js.map +1 -0
  106. package/dist/Numerical.d.ts +85 -0
  107. package/dist/Numerical.d.ts.map +1 -0
  108. package/dist/Numerical.js +343 -0
  109. package/dist/Numerical.js.map +1 -0
  110. package/dist/Permutation.d.ts +43 -0
  111. package/dist/Permutation.d.ts.map +1 -0
  112. package/dist/Permutation.js +142 -0
  113. package/dist/Permutation.js.map +1 -0
  114. package/dist/Polygon.d.ts +64 -0
  115. package/dist/Polygon.d.ts.map +1 -0
  116. package/dist/Polygon.js +221 -0
  117. package/dist/Polygon.js.map +1 -0
  118. package/dist/PolynomialRing.d.ts +74 -0
  119. package/dist/PolynomialRing.d.ts.map +1 -0
  120. package/dist/PolynomialRing.js +231 -0
  121. package/dist/PolynomialRing.js.map +1 -0
  122. package/dist/Quaternion.d.ts +37 -0
  123. package/dist/Quaternion.d.ts.map +1 -0
  124. package/dist/Quaternion.js +106 -0
  125. package/dist/Quaternion.js.map +1 -0
  126. package/dist/Rational.d.ts +34 -0
  127. package/dist/Rational.d.ts.map +1 -0
  128. package/dist/Rational.js +136 -0
  129. package/dist/Rational.js.map +1 -0
  130. package/dist/RealMath.d.ts +112 -0
  131. package/dist/RealMath.d.ts.map +1 -0
  132. package/dist/RealMath.js +262 -0
  133. package/dist/RealMath.js.map +1 -0
  134. package/dist/Rotor4.d.ts +180 -0
  135. package/dist/Rotor4.d.ts.map +1 -0
  136. package/dist/Rotor4.js +388 -0
  137. package/dist/Rotor4.js.map +1 -0
  138. package/dist/SpecialFunctions.d.ts +32 -0
  139. package/dist/SpecialFunctions.d.ts.map +1 -0
  140. package/dist/SpecialFunctions.js +161 -0
  141. package/dist/SpecialFunctions.js.map +1 -0
  142. package/dist/SpecialOperator.d.ts +21 -0
  143. package/dist/SpecialOperator.d.ts.map +1 -0
  144. package/dist/SpecialOperator.js +26 -0
  145. package/dist/SpecialOperator.js.map +1 -0
  146. package/dist/Statistics.d.ts +50 -0
  147. package/dist/Statistics.d.ts.map +1 -0
  148. package/dist/Statistics.js +131 -0
  149. package/dist/Statistics.js.map +1 -0
  150. package/dist/StringEvaluator.d.ts +48 -0
  151. package/dist/StringEvaluator.d.ts.map +1 -0
  152. package/dist/StringEvaluator.js +190 -0
  153. package/dist/StringEvaluator.js.map +1 -0
  154. package/dist/Structure.d.ts +130 -0
  155. package/dist/Structure.d.ts.map +1 -0
  156. package/dist/Structure.js +394 -0
  157. package/dist/Structure.js.map +1 -0
  158. package/dist/Symbolic.d.ts +501 -0
  159. package/dist/Symbolic.d.ts.map +1 -0
  160. package/dist/Symbolic.js +4077 -0
  161. package/dist/Symbolic.js.map +1 -0
  162. package/dist/Type.d.ts +31 -0
  163. package/dist/Type.d.ts.map +1 -0
  164. package/dist/Type.js +47 -0
  165. package/dist/Type.js.map +1 -0
  166. package/dist/Utilities.d.ts +27 -0
  167. package/dist/Utilities.d.ts.map +1 -0
  168. package/dist/Utilities.js +47 -0
  169. package/dist/Utilities.js.map +1 -0
  170. package/dist/Vec4.d.ts +39 -0
  171. package/dist/Vec4.d.ts.map +1 -0
  172. package/dist/Vec4.js +82 -0
  173. package/dist/Vec4.js.map +1 -0
  174. package/dist/Vector.d.ts +83 -0
  175. package/dist/Vector.d.ts.map +1 -0
  176. package/dist/Vector.js +173 -0
  177. package/dist/Vector.js.map +1 -0
  178. package/dist/VectorCalculus.d.ts +42 -0
  179. package/dist/VectorCalculus.d.ts.map +1 -0
  180. package/dist/VectorCalculus.js +82 -0
  181. package/dist/VectorCalculus.js.map +1 -0
  182. package/dist/VectorUtils.d.ts +172 -0
  183. package/dist/VectorUtils.d.ts.map +1 -0
  184. package/dist/VectorUtils.js +706 -0
  185. package/dist/VectorUtils.js.map +1 -0
  186. package/dist/differentiation-rules.d.ts +21 -0
  187. package/dist/differentiation-rules.d.ts.map +1 -0
  188. package/dist/differentiation-rules.js +52 -0
  189. package/dist/differentiation-rules.js.map +1 -0
  190. package/dist/index.d.ts +52 -0
  191. package/dist/index.d.ts.map +1 -0
  192. package/dist/index.js +66 -0
  193. package/dist/index.js.map +1 -0
  194. package/package.json +68 -0
  195. package/src/Bivector4.ts +136 -0
  196. package/src/CellGraph.ts +669 -0
  197. package/src/Clifford4Internal.ts +90 -0
  198. package/src/Combinatorics.ts +183 -0
  199. package/src/CombinatoricsGenerators.ts +144 -0
  200. package/src/ComplexMath.ts +162 -0
  201. package/src/ComplexNumber.ts +491 -0
  202. package/src/Cycle.ts +126 -0
  203. package/src/Decimal.ts +145 -0
  204. package/src/Distributions.ts +269 -0
  205. package/src/DualNumber.ts +105 -0
  206. package/src/Environment.ts +127 -0
  207. package/src/Expression.ts +48 -0
  208. package/src/FFT.ts +146 -0
  209. package/src/GeometricAlgebra4.ts +157 -0
  210. package/src/Geometry.ts +173 -0
  211. package/src/Graph.ts +337 -0
  212. package/src/Graph3DUtils.ts +269 -0
  213. package/src/GraphUtils.ts +159 -0
  214. package/src/GroupTheory.ts +214 -0
  215. package/src/IntUtils.ts +200 -0
  216. package/src/Interval.ts +171 -0
  217. package/src/Lattice.ts +87 -0
  218. package/src/Logic.ts +38 -0
  219. package/src/MatrixMath.ts +326 -0
  220. package/src/NumberTheory.ts +228 -0
  221. package/src/Numerical.ts +390 -0
  222. package/src/Permutation.ts +148 -0
  223. package/src/Polygon.ts +229 -0
  224. package/src/PolynomialRing.ts +242 -0
  225. package/src/Quaternion.ts +125 -0
  226. package/src/Rational.ts +148 -0
  227. package/src/RealMath.ts +346 -0
  228. package/src/Rotor4.ts +440 -0
  229. package/src/SpecialFunctions.ts +152 -0
  230. package/src/SpecialOperator.ts +28 -0
  231. package/src/Statistics.ts +170 -0
  232. package/src/StringEvaluator.ts +208 -0
  233. package/src/Structure.ts +481 -0
  234. package/src/Symbolic.ts +4313 -0
  235. package/src/Type.ts +48 -0
  236. package/src/Utilities.ts +49 -0
  237. package/src/Vec4.ts +98 -0
  238. package/src/Vector.ts +189 -0
  239. package/src/VectorCalculus.ts +87 -0
  240. package/src/VectorUtils.ts +797 -0
  241. package/src/differentiation-rules.ts +73 -0
  242. package/src/index.ts +140 -0
@@ -0,0 +1,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