@johnhenry/math 0.0.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/DualNumber.d.ts +7 -0
- package/dist/DualNumber.d.ts.map +1 -1
- package/dist/DualNumber.js +12 -3
- package/dist/DualNumber.js.map +1 -1
- package/dist/Graph3DUtils.d.ts +2 -0
- package/dist/Graph3DUtils.d.ts.map +1 -1
- package/dist/Graph3DUtils.js +14 -10
- package/dist/Graph3DUtils.js.map +1 -1
- package/dist/GraphUtils.d.ts +4 -1
- package/dist/GraphUtils.d.ts.map +1 -1
- package/dist/GraphUtils.js +15 -7
- package/dist/GraphUtils.js.map +1 -1
- package/dist/Shader.d.ts +41 -0
- package/dist/Shader.d.ts.map +1 -0
- package/dist/Shader.js +758 -0
- package/dist/Shader.js.map +1 -0
- package/dist/Symbolic.d.ts +52 -4
- package/dist/Symbolic.d.ts.map +1 -1
- package/dist/Symbolic.js +640 -104
- package/dist/Symbolic.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/DualNumber.ts +13 -3
- package/src/Graph3DUtils.ts +20 -10
- package/src/GraphUtils.ts +15 -7
- package/src/Shader.ts +837 -0
- package/src/Symbolic.ts +670 -103
- package/src/index.ts +2 -0
package/dist/Symbolic.js
CHANGED
|
@@ -8,6 +8,7 @@ import { FUNC_DERIVATIVE_RULES } from "./differentiation-rules.js";
|
|
|
8
8
|
import { MatrixMath } from "./MatrixMath.js";
|
|
9
9
|
import { Numerical } from "./Numerical.js";
|
|
10
10
|
import { Rational } from "./Rational.js";
|
|
11
|
+
import { compileShader } from "./Shader.js";
|
|
11
12
|
import { SpecialFunctions } from "./SpecialFunctions.js";
|
|
12
13
|
const FUNCS = [
|
|
13
14
|
"sin",
|
|
@@ -323,13 +324,16 @@ export class InfiniteSolutionsError extends Error {
|
|
|
323
324
|
}
|
|
324
325
|
}
|
|
325
326
|
export class Symbolic {
|
|
326
|
-
/**
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Parse an expression string such as `"sin(x^2) + 3*x"` into an AST.
|
|
329
|
+
* Pass `{ implicitMultiplication: true }` to also read `2x`, `x y` and `2(x+1)`; see {@link ParseOptions}.
|
|
330
|
+
*/
|
|
331
|
+
static parse(input, options = {}) {
|
|
332
|
+
return new Parser(input, options ?? {}).parse();
|
|
329
333
|
}
|
|
330
334
|
static differentiate(expr, variable = "x") {
|
|
331
335
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
332
|
-
return
|
|
336
|
+
return simplifyCore(diff(e, variable));
|
|
333
337
|
}
|
|
334
338
|
/**
|
|
335
339
|
* Like {@link differentiate}, but also returns a bottom-up trace of every
|
|
@@ -342,17 +346,20 @@ export class Symbolic {
|
|
|
342
346
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
343
347
|
const steps = [];
|
|
344
348
|
const raw = diffTraced(e, variable, steps);
|
|
345
|
-
return { steps, result:
|
|
349
|
+
return { steps, result: simplifyCore(raw) };
|
|
346
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Algebraic simplification: constant folding, like-term collection, and, as a last
|
|
353
|
+
* step, expanding the expression as a rational function and cancelling common
|
|
354
|
+
* factors -- `(x+1)^2 - x^2 - 2*x` gives `1` and `(x^2-1)/(x-1)` gives `x+1`.
|
|
355
|
+
* The expanded/cancelled form is used only when it is strictly smaller than the
|
|
356
|
+
* collected form, so a product such as `(x+1)*2` keeps its shape. Non-polynomial
|
|
357
|
+
* subexpressions (`sin(x)`, `x^y`) are treated as opaque symbols. Like any CAS
|
|
358
|
+
* cancellation, `(x^2-1)/(x-1)` is `x+1` as an expression, ignoring that the
|
|
359
|
+
* original is undefined at `x = 1`.
|
|
360
|
+
*/
|
|
347
361
|
static simplify(expr) {
|
|
348
|
-
|
|
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;
|
|
362
|
+
return normalizeRational(simplifyCore(expr));
|
|
356
363
|
}
|
|
357
364
|
/**
|
|
358
365
|
* Simplifies `expr` with additional domain knowledge about specific
|
|
@@ -371,14 +378,14 @@ export class Symbolic {
|
|
|
371
378
|
* assumptions (e.g. "x+y is positive" isn't expressible).
|
|
372
379
|
*/
|
|
373
380
|
static simplifyAssuming(expr, assumptions) {
|
|
374
|
-
const simplified =
|
|
381
|
+
const simplified = simplifyCore(expr);
|
|
375
382
|
const rewritten = applyAssumptions(simplified, assumptions);
|
|
376
|
-
return
|
|
383
|
+
return simplifyCore(rewritten);
|
|
377
384
|
}
|
|
378
385
|
/** Symbolic anti-derivative (elementary rules; throws {@link NotIntegrableError} otherwise). */
|
|
379
386
|
static integrate(expr, variable = "x") {
|
|
380
387
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
381
|
-
return
|
|
388
|
+
return simplifyCore(integ(e, variable));
|
|
382
389
|
}
|
|
383
390
|
/**
|
|
384
391
|
* Definite integral of `expr` over `[lower, upper]` with respect to
|
|
@@ -410,13 +417,13 @@ export class Symbolic {
|
|
|
410
417
|
* @throws {IntegrationSingularityError} if an interior singularity is detected.
|
|
411
418
|
*/
|
|
412
419
|
static integrateDefinite(expr, lower, upper, variable = "x", env = {}) {
|
|
413
|
-
const e =
|
|
420
|
+
const e = simplifyCore(typeof expr === "string" ? Symbolic.parse(expr) : expr);
|
|
414
421
|
const probe = compileExpr(e);
|
|
415
422
|
if (hasInteriorSingularity((val) => probe({ ...env, [variable]: val }), lower, upper)) {
|
|
416
423
|
throw new IntegrationSingularityError();
|
|
417
424
|
}
|
|
418
425
|
try {
|
|
419
|
-
const F =
|
|
426
|
+
const F = simplifyCore(integ(e, variable));
|
|
420
427
|
const at = (val) => evalExpr(F, { ...env, [variable]: val });
|
|
421
428
|
return at(upper) - at(lower);
|
|
422
429
|
}
|
|
@@ -471,7 +478,7 @@ export class Symbolic {
|
|
|
471
478
|
* @throws {NoClosedFormError} if a method matches but its required integral(s) aren't elementary.
|
|
472
479
|
*/
|
|
473
480
|
static solveOdeClosedForm(dyDxExpr, x0, y0, xVar = "x", yVar = "y") {
|
|
474
|
-
const e =
|
|
481
|
+
const e = simplifyCore(typeof dyDxExpr === "string" ? Symbolic.parse(dyDxExpr) : dyDxExpr);
|
|
475
482
|
const sep = matchSeparable(e, xVar, yVar);
|
|
476
483
|
if (sep)
|
|
477
484
|
return solveSeparableOde(sep, xVar, yVar, x0, y0);
|
|
@@ -580,20 +587,20 @@ export class Symbolic {
|
|
|
580
587
|
}
|
|
581
588
|
term = diff(term, variable);
|
|
582
589
|
}
|
|
583
|
-
return
|
|
590
|
+
return simplifyCore(result);
|
|
584
591
|
}
|
|
585
592
|
/** Replace every occurrence of a variable with a sub-expression (e.g. for changing variables or plugging in a solved value). */
|
|
586
593
|
static substitute(expr, variable, replacement) {
|
|
587
594
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
588
595
|
const r = typeof replacement === "string" ? Symbolic.parse(replacement) : replacement;
|
|
589
|
-
return
|
|
596
|
+
return simplifyCore(subst(e, variable, r));
|
|
590
597
|
}
|
|
591
598
|
/** Distribute multiplication over addition/subtraction and expand small integer powers of sums, e.g. `(x+1)^2 -> x^2+2*x+1`. */
|
|
592
599
|
static expand(expr) {
|
|
593
600
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
594
601
|
let cur = e;
|
|
595
602
|
for (let i = 0; i < 30; i++) {
|
|
596
|
-
const next =
|
|
603
|
+
const next = simplifyCore(expandOnce(cur));
|
|
597
604
|
if (equal(next, cur))
|
|
598
605
|
return next;
|
|
599
606
|
cur = next;
|
|
@@ -680,6 +687,21 @@ export class Symbolic {
|
|
|
680
687
|
static toString(expr) {
|
|
681
688
|
return render(typeof expr === "string" ? Symbolic.parse(expr) : expr, 0);
|
|
682
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* Compile `expr` to a GLSL ES 3.0 expression over `float` (default) or, with
|
|
692
|
+
* `{ complex: true }`, over `vec2` complex numbers (the helpers it needs, such as
|
|
693
|
+
* `mx_cmul`, are returned in `helpers` and emitted once). Output is deterministic.
|
|
694
|
+
* Refuses nodes with no shader form (`sum`, `product`, `erf`, `gcd`, `lcm`, and anything
|
|
695
|
+
* not closed under the complex numbers in complex mode) with a `ShaderUnsupportedError`.
|
|
696
|
+
* Shader `pow` of a negative base with a non-integer exponent is undefined, unlike JS `NaN`.
|
|
697
|
+
*/
|
|
698
|
+
static toGLSL(expr, options = {}) {
|
|
699
|
+
return compileShader(typeof expr === "string" ? Symbolic.parse(expr) : expr, "glsl", options);
|
|
700
|
+
}
|
|
701
|
+
/** As {@link toGLSL}, for WGSL (`f32` / `vec2<f32>`; `select` replaces the ternary). */
|
|
702
|
+
static toWGSL(expr, options = {}) {
|
|
703
|
+
return compileShader(typeof expr === "string" ? Symbolic.parse(expr) : expr, "wgsl", options);
|
|
704
|
+
}
|
|
683
705
|
/** Render `expr` as a LaTeX string, e.g. `"x/2"` -> `"\\frac{x}{2}"`. */
|
|
684
706
|
static toLatex(expr) {
|
|
685
707
|
return toLatexRec(typeof expr === "string" ? Symbolic.parse(expr) : expr, 0);
|
|
@@ -694,12 +716,16 @@ export class Symbolic {
|
|
|
694
716
|
* commands (`\sin`, `\arcsin`, `\sinh`, `\operatorname{sech}`, ...), and the
|
|
695
717
|
* two-argument `BinaryFuncName` commands (`\min`, `\max`, `\gcd`,
|
|
696
718
|
* `\operatorname{atan2}`, `\operatorname{hypot}`, `\operatorname{lcm}`).
|
|
697
|
-
* Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored.
|
|
719
|
+
* Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored. Single-token arguments as TeX
|
|
720
|
+
* reads them (`\frac12`, `\frac1x`, `\sqrt2`) and a power on a function command
|
|
721
|
+
* (`\sin^{2}(x)` is `sin(x)^2`) are accepted. An exponent of `-1` on a function command (`\sin^{-1}`) is
|
|
722
|
+
* refused as ambiguous (inverse function or reciprocal): write `\arcsin(x)` or `(\sin x)^{-1}`. For what people type into a math field
|
|
723
|
+
* (`2x`, `\sin 2x`, `(x+1)(x-1)`, `x^23`) pass `{ implicitMultiplication: true }`; see {@link ParseOptions}.
|
|
698
724
|
*
|
|
699
725
|
* @throws on LaTeX constructs with no `Expr` equivalent, e.g. `\int` or `\sum`.
|
|
700
726
|
*/
|
|
701
|
-
static fromLatex(latex) {
|
|
702
|
-
return Symbolic.parse(latexToInfix(latex));
|
|
727
|
+
static fromLatex(latex, options = {}) {
|
|
728
|
+
return Symbolic.parse(latexToInfix(latex, options), options);
|
|
703
729
|
}
|
|
704
730
|
/**
|
|
705
731
|
* Solve `expr = 0` for `variable`, returning every *real* root @johnhenry/math
|
|
@@ -719,11 +745,11 @@ export class Symbolic {
|
|
|
719
745
|
*/
|
|
720
746
|
static solve(expr, variable = "x") {
|
|
721
747
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
722
|
-
const coeffs = polynomialCoeffs(
|
|
748
|
+
const coeffs = polynomialCoeffs(simplifyCore(e), variable, 6);
|
|
723
749
|
if (!coeffs) {
|
|
724
750
|
throw new Error(`solve: expression is not a polynomial in "${variable}" of degree ≤ 6.`);
|
|
725
751
|
}
|
|
726
|
-
return solvePolynomial(coeffs).map((r) =>
|
|
752
|
+
return solvePolynomial(coeffs).map((r) => simplifyCore(r));
|
|
727
753
|
}
|
|
728
754
|
/**
|
|
729
755
|
* Solve a system of linear equations, each written in the same "expr
|
|
@@ -740,7 +766,7 @@ export class Symbolic {
|
|
|
740
766
|
* @throws {SingularSystemError} if the coefficient matrix is singular/rank-deficient.
|
|
741
767
|
*/
|
|
742
768
|
static solveSystem(equations, variables) {
|
|
743
|
-
const exprs = equations.map((eq) =>
|
|
769
|
+
const exprs = equations.map((eq) => simplifyCore(typeof eq === "string" ? Symbolic.parse(eq) : eq));
|
|
744
770
|
if (exprs.length !== variables.length) {
|
|
745
771
|
throw new Error(`solveSystem: expected ${variables.length} equations for ${variables.length} variables, got ${exprs.length}.`);
|
|
746
772
|
}
|
|
@@ -948,7 +974,7 @@ export class Symbolic {
|
|
|
948
974
|
*/
|
|
949
975
|
static factor(expr, variable = "x") {
|
|
950
976
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
951
|
-
const simplified =
|
|
977
|
+
const simplified = simplifyCore(e);
|
|
952
978
|
const coeffs = polynomialCoeffs(simplified, variable, 6);
|
|
953
979
|
if (!coeffs)
|
|
954
980
|
return simplified;
|
|
@@ -1132,6 +1158,17 @@ function diff(e, x) {
|
|
|
1132
1158
|
}
|
|
1133
1159
|
}
|
|
1134
1160
|
// -- simplification ---------------------------------------------------------
|
|
1161
|
+
/** Constant folding and like-term collection to a fixed point -- no expansion or cancellation. */
|
|
1162
|
+
function simplifyCore(expr) {
|
|
1163
|
+
let e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
1164
|
+
for (let i = 0; i < 30; i++) {
|
|
1165
|
+
const next = collectLikeTerms(simplifyOnce(e));
|
|
1166
|
+
if (equal(next, e))
|
|
1167
|
+
return next;
|
|
1168
|
+
e = next;
|
|
1169
|
+
}
|
|
1170
|
+
return e;
|
|
1171
|
+
}
|
|
1135
1172
|
function simplifyOnce(e) {
|
|
1136
1173
|
if (e.type === "const" || e.type === "var")
|
|
1137
1174
|
return e;
|
|
@@ -1876,7 +1913,7 @@ function freshVariableName(e, x) {
|
|
|
1876
1913
|
* legitimately hit a domain error/NaN on either side).
|
|
1877
1914
|
*/
|
|
1878
1915
|
function verifyByDifferentiation(candidate, e, x) {
|
|
1879
|
-
const check =
|
|
1916
|
+
const check = simplifyCore(diff(candidate, x));
|
|
1880
1917
|
let agreements = 0;
|
|
1881
1918
|
for (const p of [0.7, 1.3, -0.9]) {
|
|
1882
1919
|
const expected = evalExpr(e, { [x]: p });
|
|
@@ -1902,10 +1939,10 @@ function verifyByDifferentiation(candidate, e, x) {
|
|
|
1902
1939
|
*/
|
|
1903
1940
|
function tryUSubstitution(e, x) {
|
|
1904
1941
|
for (const g of collectSubstitutionCandidates(e, x)) {
|
|
1905
|
-
const gPrime =
|
|
1942
|
+
const gPrime = simplifyCore(diff(g, x));
|
|
1906
1943
|
if (isConst(gPrime, 0))
|
|
1907
1944
|
continue;
|
|
1908
|
-
const quotient =
|
|
1945
|
+
const quotient = simplifyCore(divideOutFactors(e, gPrime) ?? div(e, gPrime));
|
|
1909
1946
|
if (!containsExpr(quotient, g))
|
|
1910
1947
|
continue;
|
|
1911
1948
|
const uName = freshVariableName(quotient, x);
|
|
@@ -1914,7 +1951,7 @@ function tryUSubstitution(e, x) {
|
|
|
1914
1951
|
continue;
|
|
1915
1952
|
try {
|
|
1916
1953
|
const antiderivativeInU = integRules(substituted, uName);
|
|
1917
|
-
const candidate =
|
|
1954
|
+
const candidate = simplifyCore(substExpr(antiderivativeInU, v(uName), g));
|
|
1918
1955
|
if (verifyByDifferentiation(candidate, e, x))
|
|
1919
1956
|
return candidate;
|
|
1920
1957
|
}
|
|
@@ -2044,13 +2081,13 @@ function matchSeparable(e, xVar, yVar) {
|
|
|
2044
2081
|
function matchLinearOde(e, xVar, yVar) {
|
|
2045
2082
|
if (!containsVar(e, yVar))
|
|
2046
2083
|
return null; // handled by matchSeparable's h(y)=1 case instead
|
|
2047
|
-
const negP =
|
|
2084
|
+
const negP = simplifyCore(diff(e, yVar));
|
|
2048
2085
|
if (containsVar(negP, yVar))
|
|
2049
2086
|
return null; // not affine in y
|
|
2050
|
-
const q =
|
|
2087
|
+
const q = simplifyCore(sub(e, mul(negP, v(yVar))));
|
|
2051
2088
|
if (containsVar(q, yVar))
|
|
2052
2089
|
return null; // sanity check
|
|
2053
|
-
const p =
|
|
2090
|
+
const p = simplifyCore(neg(negP));
|
|
2054
2091
|
// Conservative, matching matchSeparable's philosophy: p/q must depend on
|
|
2055
2092
|
// no free variable other than xVar (an extra symbolic parameter, or a
|
|
2056
2093
|
// stray reference to yVar that survived simplification, both bail out).
|
|
@@ -2105,12 +2142,12 @@ function tryInvertForY(H, yVar, rhs, y0) {
|
|
|
2105
2142
|
return null;
|
|
2106
2143
|
}
|
|
2107
2144
|
function solveSeparableOde(sep, xVar, yVar, x0, y0) {
|
|
2108
|
-
const invH =
|
|
2145
|
+
const invH = simplifyCore(div(num(1), sep.h));
|
|
2109
2146
|
let H;
|
|
2110
2147
|
let G;
|
|
2111
2148
|
try {
|
|
2112
|
-
H =
|
|
2113
|
-
G =
|
|
2149
|
+
H = simplifyCore(integ(invH, yVar));
|
|
2150
|
+
G = simplifyCore(integ(sep.g, xVar));
|
|
2114
2151
|
}
|
|
2115
2152
|
catch (err) {
|
|
2116
2153
|
if (err instanceof NotIntegrableError)
|
|
@@ -2118,20 +2155,20 @@ function solveSeparableOde(sep, xVar, yVar, x0, y0) {
|
|
|
2118
2155
|
throw err;
|
|
2119
2156
|
}
|
|
2120
2157
|
const C = evalExpr(H, { [yVar]: y0 }) - evalExpr(G, { [xVar]: x0 });
|
|
2121
|
-
const rhs =
|
|
2158
|
+
const rhs = simplifyCore(add(G, num(C)));
|
|
2122
2159
|
const explicitY = tryInvertForY(H, yVar, rhs, y0);
|
|
2123
2160
|
if (explicitY)
|
|
2124
|
-
return { explicit: true, y:
|
|
2125
|
-
return { explicit: false, implicitRelation:
|
|
2161
|
+
return { explicit: true, y: simplifyCore(explicitY) };
|
|
2162
|
+
return { explicit: false, implicitRelation: simplifyCore(sub(H, rhs)) };
|
|
2126
2163
|
}
|
|
2127
2164
|
function solveLinearOde(lin, xVar, _yVar, x0, y0) {
|
|
2128
2165
|
let integralP;
|
|
2129
2166
|
let mu;
|
|
2130
2167
|
let muQIntegral;
|
|
2131
2168
|
try {
|
|
2132
|
-
integralP =
|
|
2133
|
-
mu =
|
|
2134
|
-
muQIntegral =
|
|
2169
|
+
integralP = simplifyCore(integ(lin.p, xVar));
|
|
2170
|
+
mu = simplifyCore(fn("exp", integralP));
|
|
2171
|
+
muQIntegral = simplifyCore(integ(simplifyCore(mul(mu, lin.q)), xVar));
|
|
2135
2172
|
}
|
|
2136
2173
|
catch (err) {
|
|
2137
2174
|
if (err instanceof NotIntegrableError)
|
|
@@ -2142,7 +2179,7 @@ function solveLinearOde(lin, xVar, _yVar, x0, y0) {
|
|
|
2142
2179
|
const muAtX0 = evalExpr(mu, { [xVar]: x0 });
|
|
2143
2180
|
const muQIntegralAtX0 = evalExpr(muQIntegral, { [xVar]: x0 });
|
|
2144
2181
|
const C = y0 * muAtX0 - muQIntegralAtX0;
|
|
2145
|
-
const y =
|
|
2182
|
+
const y = simplifyCore(div(add(muQIntegral, num(C)), mu));
|
|
2146
2183
|
return { explicit: true, y };
|
|
2147
2184
|
}
|
|
2148
2185
|
/** 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). */
|
|
@@ -2198,7 +2235,7 @@ function matchHomogeneous(e, xVar, yVar) {
|
|
|
2198
2235
|
}
|
|
2199
2236
|
if (attempts === 0 || agreements < 4)
|
|
2200
2237
|
return null; // not enough usable probes to trust the match
|
|
2201
|
-
const F =
|
|
2238
|
+
const F = simplifyCore(subst(substituted, xVar, num(1)));
|
|
2202
2239
|
return { vName, F };
|
|
2203
2240
|
}
|
|
2204
2241
|
/**
|
|
@@ -2223,15 +2260,15 @@ function matchHomogeneous(e, xVar, yVar) {
|
|
|
2223
2260
|
function solveHomogeneousOde(hom, xVar, yVar, x0, y0) {
|
|
2224
2261
|
const { vName, F } = hom;
|
|
2225
2262
|
const v0 = y0 / x0;
|
|
2226
|
-
const FMinusV =
|
|
2263
|
+
const FMinusV = simplifyCore(sub(F, v(vName)));
|
|
2227
2264
|
if (FMinusV.type === "const" && FMinusV.value === 0) {
|
|
2228
|
-
return { explicit: true, y:
|
|
2265
|
+
return { explicit: true, y: simplifyCore(mul(num(v0), v(xVar))) };
|
|
2229
2266
|
}
|
|
2230
2267
|
const sepResult = solveSeparableOde({ g: div(num(1), v(xVar)), h: FMinusV }, xVar, vName, x0, v0);
|
|
2231
2268
|
if (sepResult.explicit && sepResult.y) {
|
|
2232
|
-
return { explicit: true, y:
|
|
2269
|
+
return { explicit: true, y: simplifyCore(mul(sepResult.y, v(xVar))) };
|
|
2233
2270
|
}
|
|
2234
|
-
const implicitRelation =
|
|
2271
|
+
const implicitRelation = simplifyCore(subst(sepResult.implicitRelation, vName, div(v(yVar), v(xVar))));
|
|
2235
2272
|
return { explicit: false, implicitRelation };
|
|
2236
2273
|
}
|
|
2237
2274
|
/**
|
|
@@ -2324,8 +2361,8 @@ function matchBernoulli(e, xVar, yVar) {
|
|
|
2324
2361
|
}
|
|
2325
2362
|
if (powMatch.n === 0)
|
|
2326
2363
|
return null; // y^0=1: really just linear, matchLinearOde's job
|
|
2327
|
-
const q = powSign === 1 ? powMatch.coef :
|
|
2328
|
-
const p = linSign === 1 ?
|
|
2364
|
+
const q = powSign === 1 ? powMatch.coef : simplifyCore(neg(powMatch.coef));
|
|
2365
|
+
const p = linSign === 1 ? simplifyCore(neg(linMatch.coef)) : linMatch.coef;
|
|
2329
2366
|
return { p, q, n: powMatch.n };
|
|
2330
2367
|
}
|
|
2331
2368
|
/**
|
|
@@ -2345,10 +2382,10 @@ function matchBernoulli(e, xVar, yVar) {
|
|
|
2345
2382
|
function solveBernoulliOde(bern, xVar, yVar, x0, y0) {
|
|
2346
2383
|
const oneMinusN = 1 - bern.n;
|
|
2347
2384
|
const w0 = y0 ** oneMinusN;
|
|
2348
|
-
const transformedP =
|
|
2349
|
-
const transformedQ =
|
|
2385
|
+
const transformedP = simplifyCore(mul(num(oneMinusN), bern.p));
|
|
2386
|
+
const transformedQ = simplifyCore(mul(num(oneMinusN), bern.q));
|
|
2350
2387
|
const wResult = solveLinearOde({ p: transformedP, q: transformedQ }, xVar, yVar, x0, w0);
|
|
2351
|
-
const y =
|
|
2388
|
+
const y = simplifyCore(pow(wResult.y, num(1 / oneMinusN)));
|
|
2352
2389
|
return { explicit: true, y };
|
|
2353
2390
|
}
|
|
2354
2391
|
/**
|
|
@@ -2371,7 +2408,7 @@ function matchExact(e, xVar, yVar) {
|
|
|
2371
2408
|
if (![...vars].every((n) => n === xVar || n === yVar))
|
|
2372
2409
|
return null;
|
|
2373
2410
|
const M = e.left;
|
|
2374
|
-
const N =
|
|
2411
|
+
const N = simplifyCore(neg(e.right));
|
|
2375
2412
|
const dMdy = diff(M, yVar);
|
|
2376
2413
|
const dNdx = diff(N, xVar);
|
|
2377
2414
|
let agreements = 0;
|
|
@@ -2421,8 +2458,8 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
|
|
|
2421
2458
|
let g;
|
|
2422
2459
|
let gPrime;
|
|
2423
2460
|
try {
|
|
2424
|
-
F0 =
|
|
2425
|
-
gPrime =
|
|
2461
|
+
F0 = simplifyCore(integ(M, xVar));
|
|
2462
|
+
gPrime = simplifyCore(sub(N, diff(F0, yVar)));
|
|
2426
2463
|
if (containsVar(gPrime, xVar)) {
|
|
2427
2464
|
// Symbolic cancellation fell short -- verify x-independence
|
|
2428
2465
|
// numerically, then pin x to a probe value that evaluates finitely.
|
|
@@ -2435,21 +2472,21 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
|
|
|
2435
2472
|
if (Math.abs(atProbe - atOther) > 1e-6 * Math.max(1, Math.abs(atProbe))) {
|
|
2436
2473
|
throw new NoClosedFormError();
|
|
2437
2474
|
}
|
|
2438
|
-
pinned =
|
|
2475
|
+
pinned = simplifyCore(subst(gPrime, xVar, num(xp)));
|
|
2439
2476
|
break;
|
|
2440
2477
|
}
|
|
2441
2478
|
if (!pinned)
|
|
2442
2479
|
throw new NoClosedFormError();
|
|
2443
2480
|
gPrime = pinned;
|
|
2444
2481
|
}
|
|
2445
|
-
g =
|
|
2482
|
+
g = simplifyCore(integ(gPrime, yVar));
|
|
2446
2483
|
}
|
|
2447
2484
|
catch (err) {
|
|
2448
2485
|
if (err instanceof NotIntegrableError)
|
|
2449
2486
|
throw new NoClosedFormError();
|
|
2450
2487
|
throw err;
|
|
2451
2488
|
}
|
|
2452
|
-
const F =
|
|
2489
|
+
const F = simplifyCore(add(F0, g));
|
|
2453
2490
|
const C = evalExpr(F, { [xVar]: x0, [yVar]: y0 });
|
|
2454
2491
|
if (!Number.isFinite(C))
|
|
2455
2492
|
throw new NoClosedFormError();
|
|
@@ -2467,13 +2504,13 @@ function solveExactOde(exact, xVar, yVar, x0, y0) {
|
|
|
2467
2504
|
continue;
|
|
2468
2505
|
if (F.type === "sub" && yPart === b)
|
|
2469
2506
|
continue; // F = G(x) - H(y): inverting -H is out of tryInvertForY's shapes
|
|
2470
|
-
const rhs =
|
|
2507
|
+
const rhs = simplifyCore(sub(num(C), xPartSigned));
|
|
2471
2508
|
const explicitY = tryInvertForY(yPart, yVar, rhs, y0);
|
|
2472
2509
|
if (explicitY)
|
|
2473
|
-
return { explicit: true, y:
|
|
2510
|
+
return { explicit: true, y: simplifyCore(explicitY) };
|
|
2474
2511
|
}
|
|
2475
2512
|
}
|
|
2476
|
-
return { explicit: false, implicitRelation:
|
|
2513
|
+
return { explicit: false, implicitRelation: simplifyCore(sub(F, num(C))) };
|
|
2477
2514
|
}
|
|
2478
2515
|
/** If `e` is `a·x + b` (a, b constant), return `{ a, b }`, else `null`. */
|
|
2479
2516
|
function linearCoeffs(e, x) {
|
|
@@ -3345,11 +3382,54 @@ const CMP_LATEX_CMDS = {
|
|
|
3345
3382
|
"\\ne": "!=",
|
|
3346
3383
|
};
|
|
3347
3384
|
/** Convert a LaTeX math string into the plain-infix syntax `Symbolic.parse`'s `Parser` understands. */
|
|
3348
|
-
function latexToInfix(latex) {
|
|
3349
|
-
const cleaned = latex.replace(/\\left|\\right/g, "").replace(
|
|
3350
|
-
return transformLatex(cleaned);
|
|
3385
|
+
function latexToInfix(latex, opts = {}) {
|
|
3386
|
+
const cleaned = latex.replace(/\\left|\\right/g, "").replace(/(?<!\\)\\(?:,|;|!|quad|qquad| )/g, "");
|
|
3387
|
+
return transformLatex(cleaned, opts);
|
|
3351
3388
|
}
|
|
3352
|
-
|
|
3389
|
+
/**
|
|
3390
|
+
* Where the unbracketed argument of a function command ends under implicit multiplication: the longest
|
|
3391
|
+
* product of numbers, letters, `\\pi`, powers, subscripts and bracket groups starting at `start`
|
|
3392
|
+
* (`\\sin 2x` takes `2x`; `\\sin x\\cos x` takes just `x`). Returns `start` when there is none.
|
|
3393
|
+
*/
|
|
3394
|
+
function bareArgumentEnd(s, start) {
|
|
3395
|
+
let j = start;
|
|
3396
|
+
const group = (at) => {
|
|
3397
|
+
const open = s[at];
|
|
3398
|
+
return findGroupEnd(s, at, open, open === "{" ? "}" : ")") + 1;
|
|
3399
|
+
};
|
|
3400
|
+
while (j < s.length) {
|
|
3401
|
+
const c = s[j];
|
|
3402
|
+
if (/[A-Za-z0-9.]/.test(c)) {
|
|
3403
|
+
j++;
|
|
3404
|
+
}
|
|
3405
|
+
else if (c === "(" || c === "{") {
|
|
3406
|
+
j = group(j);
|
|
3407
|
+
}
|
|
3408
|
+
else if (c === "_" && s[j + 1] === "{") {
|
|
3409
|
+
j = group(j + 1);
|
|
3410
|
+
}
|
|
3411
|
+
else if (c === "^") {
|
|
3412
|
+
if (s[j + 1] === "{")
|
|
3413
|
+
j = group(j + 1);
|
|
3414
|
+
else if (s[j + 1] === "\\")
|
|
3415
|
+
j += 1 + (/^\\[a-zA-Z]+/.exec(s.slice(j + 1))?.[0].length ?? 1);
|
|
3416
|
+
else
|
|
3417
|
+
j += 2;
|
|
3418
|
+
}
|
|
3419
|
+
else if (/^\\pi(?![a-zA-Z])/.test(s.slice(j))) {
|
|
3420
|
+
j += 3;
|
|
3421
|
+
}
|
|
3422
|
+
else if (c === " " && /^ +(?:[A-Za-z0-9.({]|\\pi(?![a-zA-Z]))/.test(s.slice(j))) {
|
|
3423
|
+
j += s.slice(j).length - s.slice(j).trimStart().length;
|
|
3424
|
+
}
|
|
3425
|
+
else {
|
|
3426
|
+
break;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
return j;
|
|
3430
|
+
}
|
|
3431
|
+
function transformLatex(s, opts = {}) {
|
|
3432
|
+
const implicit = opts.implicitMultiplication === true;
|
|
3353
3433
|
let out = "";
|
|
3354
3434
|
let i = 0;
|
|
3355
3435
|
const readGroup = () => {
|
|
@@ -3360,11 +3440,21 @@ function transformLatex(s) {
|
|
|
3360
3440
|
return content;
|
|
3361
3441
|
};
|
|
3362
3442
|
const readGroupOrParenOrToken = () => {
|
|
3443
|
+
while (s[i] === " ")
|
|
3444
|
+
i++;
|
|
3363
3445
|
if (s[i] === "{" || s[i] === "(") {
|
|
3364
3446
|
const { content, next } = extractGroup(s, i);
|
|
3365
3447
|
i = next;
|
|
3366
3448
|
return content;
|
|
3367
3449
|
}
|
|
3450
|
+
if (implicit) {
|
|
3451
|
+
const end = bareArgumentEnd(s, i);
|
|
3452
|
+
if (end > i) {
|
|
3453
|
+
const run = s.slice(i, end).trim();
|
|
3454
|
+
i = end;
|
|
3455
|
+
return run;
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3368
3458
|
const m = /^[a-zA-Z_]\w*|^\d+\.?\d*(?:[eE][+-]?\d+)?/.exec(s.slice(i));
|
|
3369
3459
|
if (m) {
|
|
3370
3460
|
i += m[0].length;
|
|
@@ -3372,6 +3462,32 @@ function transformLatex(s) {
|
|
|
3372
3462
|
}
|
|
3373
3463
|
throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
|
|
3374
3464
|
};
|
|
3465
|
+
/** A `{...}` group, or the one token TeX takes without braces: a digit, a letter or a `\\command`. */
|
|
3466
|
+
const readGroupOrToken = () => {
|
|
3467
|
+
while (s[i] === " ")
|
|
3468
|
+
i++;
|
|
3469
|
+
if (s[i] === "{")
|
|
3470
|
+
return readGroup();
|
|
3471
|
+
const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i));
|
|
3472
|
+
if (!m)
|
|
3473
|
+
throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
|
|
3474
|
+
i += m[0].length;
|
|
3475
|
+
return m[0];
|
|
3476
|
+
};
|
|
3477
|
+
/** A power written on a function command (`\\sin^{2}`, `\\sin^2`), or null. */
|
|
3478
|
+
const readFunctionPower = () => {
|
|
3479
|
+
if (s[i] !== "^")
|
|
3480
|
+
return null;
|
|
3481
|
+
i++;
|
|
3482
|
+
const power = readGroupOrToken();
|
|
3483
|
+
// `\sin^{-1}` is arcsin in most textbooks and calculators but 1/sin in strict TeX/notation; guessing
|
|
3484
|
+
// wrong would silently change the maths, so it is refused rather than interpreted.
|
|
3485
|
+
if (/^\s*-\s*1\s*$/.test(power)) {
|
|
3486
|
+
throw new Error("Ambiguous LaTeX: a function with exponent -1 (e.g. \\sin^{-1}) may mean the inverse function or the reciprocal; " +
|
|
3487
|
+
"write \\arcsin(x) for the inverse, or (\\sin x)^{-1} / 1/\\sin x for the reciprocal");
|
|
3488
|
+
}
|
|
3489
|
+
return power;
|
|
3490
|
+
};
|
|
3375
3491
|
/** Read a `(...)`/`{...}` group holding exactly 2 comma-separated arguments, for `call2` functions. */
|
|
3376
3492
|
const readArgPair = () => {
|
|
3377
3493
|
if (s[i] !== "(" && s[i] !== "{")
|
|
@@ -3427,10 +3543,11 @@ function transformLatex(s) {
|
|
|
3427
3543
|
if (condPart.trim() === "\\text{otherwise}") {
|
|
3428
3544
|
if (otherwise !== null)
|
|
3429
3545
|
throw new Error("\\begin{cases} may have only one \\text{otherwise} row");
|
|
3430
|
-
otherwise = transformLatex(exprPart);
|
|
3546
|
+
otherwise = transformLatex(exprPart, opts);
|
|
3431
3547
|
}
|
|
3432
3548
|
else {
|
|
3433
|
-
|
|
3549
|
+
const condition = condPart.replace(/^\s*\\text\{\s*(?:if|when)\s*\}/, ""); // "x & \text{if } x<0" as MathLive writes it
|
|
3550
|
+
parts.push(`${transformLatex(condition, opts)}, ${transformLatex(exprPart, opts)}`);
|
|
3434
3551
|
}
|
|
3435
3552
|
}
|
|
3436
3553
|
if (otherwise === null) {
|
|
@@ -3439,29 +3556,29 @@ function transformLatex(s) {
|
|
|
3439
3556
|
out += `piecewise(${parts.concat(otherwise).join(", ")})`;
|
|
3440
3557
|
continue;
|
|
3441
3558
|
}
|
|
3442
|
-
if (cmd === "\\frac") {
|
|
3443
|
-
const numerator =
|
|
3444
|
-
const denominator =
|
|
3445
|
-
out += `((${transformLatex(numerator)})/(${transformLatex(denominator)}))`;
|
|
3559
|
+
if (cmd === "\\frac" || cmd === "\\dfrac" || cmd === "\\tfrac") {
|
|
3560
|
+
const numerator = readGroupOrToken();
|
|
3561
|
+
const denominator = readGroupOrToken();
|
|
3562
|
+
out += `((${transformLatex(numerator, opts)})/(${transformLatex(denominator, opts)}))`;
|
|
3446
3563
|
continue;
|
|
3447
3564
|
}
|
|
3448
3565
|
if (cmd === "\\sqrt") {
|
|
3449
3566
|
if (s[i] === "[") {
|
|
3450
3567
|
const { content: root, next } = extractGroup(s, i);
|
|
3451
3568
|
i = next;
|
|
3452
|
-
const arg =
|
|
3453
|
-
const transformedRoot = transformLatex(root).trim();
|
|
3569
|
+
const arg = readGroupOrToken();
|
|
3570
|
+
const transformedRoot = transformLatex(root, opts).trim();
|
|
3454
3571
|
// \sqrt[3]{...} maps to cbrt(...) rather than a fractional power so
|
|
3455
3572
|
// negative arguments evaluate correctly (Math.cbrt(-8) = -2, but
|
|
3456
3573
|
// (-8)^(1/3) is NaN under JS's `**`).
|
|
3457
3574
|
out +=
|
|
3458
3575
|
transformedRoot === "3"
|
|
3459
|
-
? `cbrt(${transformLatex(arg)})`
|
|
3460
|
-
: `((${transformLatex(arg)})^(1/(${transformedRoot})))`;
|
|
3576
|
+
? `cbrt(${transformLatex(arg, opts)})`
|
|
3577
|
+
: `((${transformLatex(arg, opts)})^(1/(${transformedRoot})))`;
|
|
3461
3578
|
}
|
|
3462
3579
|
else {
|
|
3463
|
-
const arg =
|
|
3464
|
-
out += `sqrt(${transformLatex(arg)})`;
|
|
3580
|
+
const arg = readGroupOrToken();
|
|
3581
|
+
out += `sqrt(${transformLatex(arg, opts)})`;
|
|
3465
3582
|
}
|
|
3466
3583
|
continue;
|
|
3467
3584
|
}
|
|
@@ -3475,8 +3592,10 @@ function transformLatex(s) {
|
|
|
3475
3592
|
if (base !== "10" && base !== "2") {
|
|
3476
3593
|
throw new Error(`Unsupported LaTeX construct: \\log base ${base} (only base 10 and base 2 are supported)`);
|
|
3477
3594
|
}
|
|
3595
|
+
const power = readFunctionPower();
|
|
3478
3596
|
const arg = readGroupOrParenOrToken();
|
|
3479
|
-
|
|
3597
|
+
const call = `${base === "2" ? "log2" : "log10"}(${transformLatex(arg, opts)})`;
|
|
3598
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3480
3599
|
continue;
|
|
3481
3600
|
}
|
|
3482
3601
|
if (cmd === "\\lfloor" || cmd === "\\lceil") {
|
|
@@ -3486,7 +3605,7 @@ function transformLatex(s) {
|
|
|
3486
3605
|
throw new Error(`Unmatched '${cmd}' in LaTeX source`);
|
|
3487
3606
|
const inner = s.slice(i, closeIdx);
|
|
3488
3607
|
i = closeIdx + closeCmd.length;
|
|
3489
|
-
out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner)})`;
|
|
3608
|
+
out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner, opts)})`;
|
|
3490
3609
|
continue;
|
|
3491
3610
|
}
|
|
3492
3611
|
if (cmd === "\\operatorname") {
|
|
@@ -3494,26 +3613,30 @@ function transformLatex(s) {
|
|
|
3494
3613
|
const binaryName = OPERATORNAME_BINARY_FUNC_NAMES[opName];
|
|
3495
3614
|
if (binaryName) {
|
|
3496
3615
|
const [left, right] = readArgPair();
|
|
3497
|
-
out += `${binaryName}(${transformLatex(left)}, ${transformLatex(right)})`;
|
|
3616
|
+
out += `${binaryName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
|
|
3498
3617
|
continue;
|
|
3499
3618
|
}
|
|
3500
3619
|
const funcName = OPERATORNAME_FUNC_NAMES[opName];
|
|
3501
3620
|
if (!funcName)
|
|
3502
3621
|
throw new Error(`Unsupported LaTeX construct: \\operatorname{${opName}}`);
|
|
3622
|
+
const power = readFunctionPower();
|
|
3503
3623
|
const arg = readGroupOrParenOrToken();
|
|
3504
|
-
|
|
3624
|
+
const call = `${funcName}(${transformLatex(arg, opts)})`;
|
|
3625
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3505
3626
|
continue;
|
|
3506
3627
|
}
|
|
3507
3628
|
const binaryFuncName = LATEX_BINARY_FUNC_NAMES[cmd];
|
|
3508
3629
|
if (binaryFuncName) {
|
|
3509
3630
|
const [left, right] = readArgPair();
|
|
3510
|
-
out += `${binaryFuncName}(${transformLatex(left)}, ${transformLatex(right)})`;
|
|
3631
|
+
out += `${binaryFuncName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
|
|
3511
3632
|
continue;
|
|
3512
3633
|
}
|
|
3513
3634
|
const funcName = LATEX_FUNC_NAMES[cmd];
|
|
3514
3635
|
if (funcName) {
|
|
3636
|
+
const power = readFunctionPower();
|
|
3515
3637
|
const arg = readGroupOrParenOrToken();
|
|
3516
|
-
|
|
3638
|
+
const call = `${funcName}(${transformLatex(arg, opts)})`;
|
|
3639
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3517
3640
|
continue;
|
|
3518
3641
|
}
|
|
3519
3642
|
throw new Error(`Unsupported LaTeX construct: ${cmd}`);
|
|
@@ -3524,13 +3647,20 @@ function transformLatex(s) {
|
|
|
3524
3647
|
throw new Error("Unmatched '|' in LaTeX source");
|
|
3525
3648
|
const inner = s.slice(i + 1, closeIdx);
|
|
3526
3649
|
i = closeIdx + 1;
|
|
3527
|
-
out += `abs(${transformLatex(inner)})`;
|
|
3650
|
+
out += `abs(${transformLatex(inner, opts)})`;
|
|
3651
|
+
continue;
|
|
3652
|
+
}
|
|
3653
|
+
if (implicit && ch === "^" && /^[a-zA-Z0-9]|^\\[a-zA-Z]/.test(s.slice(i + 1))) {
|
|
3654
|
+
// TeX reads `x^23` as `x^2` times `3`: the exponent is one token
|
|
3655
|
+
const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i + 1));
|
|
3656
|
+
i += 1 + m[0].length;
|
|
3657
|
+
out += `^(${transformLatex(m[0], opts)})`;
|
|
3528
3658
|
continue;
|
|
3529
3659
|
}
|
|
3530
3660
|
if (ch === "^" && s[i + 1] === "{") {
|
|
3531
3661
|
const { content, next } = extractGroup(s, i + 1);
|
|
3532
3662
|
i = next;
|
|
3533
|
-
out += `^(${transformLatex(content)})`;
|
|
3663
|
+
out += `^(${transformLatex(content, opts)})`;
|
|
3534
3664
|
continue;
|
|
3535
3665
|
}
|
|
3536
3666
|
if (ch === "_" && s[i + 1] === "{") {
|
|
@@ -3542,7 +3672,7 @@ function transformLatex(s) {
|
|
|
3542
3672
|
if (ch === "{") {
|
|
3543
3673
|
const { content, next } = extractGroup(s, i);
|
|
3544
3674
|
i = next;
|
|
3545
|
-
out += `(${transformLatex(content)})`;
|
|
3675
|
+
out += `(${transformLatex(content, opts)})`;
|
|
3546
3676
|
continue;
|
|
3547
3677
|
}
|
|
3548
3678
|
if (ch === "=") {
|
|
@@ -3558,6 +3688,378 @@ function transformLatex(s) {
|
|
|
3558
3688
|
}
|
|
3559
3689
|
return out;
|
|
3560
3690
|
}
|
|
3691
|
+
const MAX_POLY_TERMS = 400;
|
|
3692
|
+
const MAX_RATIONAL_POWER = 8;
|
|
3693
|
+
class RatBail extends Error {
|
|
3694
|
+
}
|
|
3695
|
+
function atomIndex(ctx, e) {
|
|
3696
|
+
const key = render(e, 0);
|
|
3697
|
+
let i = ctx.keys.indexOf(key);
|
|
3698
|
+
if (i < 0) {
|
|
3699
|
+
i = ctx.keys.length;
|
|
3700
|
+
ctx.keys.push(key);
|
|
3701
|
+
ctx.atoms.push(e);
|
|
3702
|
+
}
|
|
3703
|
+
return i;
|
|
3704
|
+
}
|
|
3705
|
+
function expsCmp(a, b) {
|
|
3706
|
+
const n = Math.max(a.length, b.length);
|
|
3707
|
+
for (let i = 0; i < n; i++) {
|
|
3708
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
3709
|
+
if (d !== 0)
|
|
3710
|
+
return d;
|
|
3711
|
+
}
|
|
3712
|
+
return 0;
|
|
3713
|
+
}
|
|
3714
|
+
function expsKey(a) {
|
|
3715
|
+
let end = a.length;
|
|
3716
|
+
while (end > 0 && !a[end - 1])
|
|
3717
|
+
end--;
|
|
3718
|
+
return a.slice(0, end).join(",");
|
|
3719
|
+
}
|
|
3720
|
+
function polyNormalize(terms) {
|
|
3721
|
+
const byKey = new Map();
|
|
3722
|
+
for (const t of terms) {
|
|
3723
|
+
const k = expsKey(t.exps);
|
|
3724
|
+
const prev = byKey.get(k);
|
|
3725
|
+
if (prev)
|
|
3726
|
+
byKey.set(k, { exps: prev.exps, c: prev.c.add(t.c) });
|
|
3727
|
+
else
|
|
3728
|
+
byKey.set(k, { exps: t.exps, c: t.c });
|
|
3729
|
+
}
|
|
3730
|
+
const out = [...byKey.values()].filter((t) => !t.c.isZero());
|
|
3731
|
+
out.sort((a, b) => expsCmp(b.exps, a.exps));
|
|
3732
|
+
if (out.length > MAX_POLY_TERMS)
|
|
3733
|
+
throw new RatBail();
|
|
3734
|
+
return out;
|
|
3735
|
+
}
|
|
3736
|
+
const polyConst = (c) => (c.isZero() ? [] : [{ exps: [], c }]);
|
|
3737
|
+
const polyAdd = (a, b) => polyNormalize([...a, ...b]);
|
|
3738
|
+
const polyNeg = (a) => a.map((t) => ({ exps: t.exps, c: t.c.negate() }));
|
|
3739
|
+
const polySub = (a, b) => polyAdd(a, polyNeg(b));
|
|
3740
|
+
function polyMul(a, b) {
|
|
3741
|
+
if (a.length * b.length > MAX_POLY_TERMS * 20)
|
|
3742
|
+
throw new RatBail();
|
|
3743
|
+
const out = [];
|
|
3744
|
+
for (const x of a) {
|
|
3745
|
+
for (const y of b) {
|
|
3746
|
+
const n = Math.max(x.exps.length, y.exps.length);
|
|
3747
|
+
const exps = [];
|
|
3748
|
+
for (let i = 0; i < n; i++)
|
|
3749
|
+
exps.push((x.exps[i] ?? 0) + (y.exps[i] ?? 0));
|
|
3750
|
+
out.push({ exps, c: x.c.multiply(y.c) });
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
return polyNormalize(out);
|
|
3754
|
+
}
|
|
3755
|
+
function polyEqual(a, b) {
|
|
3756
|
+
return a.length === b.length && a.every((t, i) => expsKey(t.exps) === expsKey(b[i].exps) && t.c.equals(b[i].c));
|
|
3757
|
+
}
|
|
3758
|
+
const polyIsConst = (a) => a.length === 0 || (a.length === 1 && expsKey(a[0].exps) === "");
|
|
3759
|
+
/** Division in lex order: `a = q*b + r` (`b` must be nonzero). */
|
|
3760
|
+
function polyDivMod(a, b) {
|
|
3761
|
+
const lead = b[0];
|
|
3762
|
+
let rem = a;
|
|
3763
|
+
const q = [];
|
|
3764
|
+
const r = [];
|
|
3765
|
+
let guard = 0;
|
|
3766
|
+
while (rem.length > 0) {
|
|
3767
|
+
if (++guard > 20_000)
|
|
3768
|
+
throw new RatBail();
|
|
3769
|
+
const t = rem[0];
|
|
3770
|
+
const n = Math.max(t.exps.length, lead.exps.length);
|
|
3771
|
+
const exps = [];
|
|
3772
|
+
let divisible = true;
|
|
3773
|
+
for (let i = 0; i < n; i++) {
|
|
3774
|
+
const d = (t.exps[i] ?? 0) - (lead.exps[i] ?? 0);
|
|
3775
|
+
if (d < 0)
|
|
3776
|
+
divisible = false;
|
|
3777
|
+
exps.push(d);
|
|
3778
|
+
}
|
|
3779
|
+
if (!divisible) {
|
|
3780
|
+
r.push(t);
|
|
3781
|
+
rem = rem.slice(1);
|
|
3782
|
+
continue;
|
|
3783
|
+
}
|
|
3784
|
+
const qt = { exps, c: t.c.divide(lead.c) };
|
|
3785
|
+
q.push(qt);
|
|
3786
|
+
rem = polySub(rem, polyMul([qt], b));
|
|
3787
|
+
}
|
|
3788
|
+
return { q: polyNormalize(q), r: polyNormalize(r) };
|
|
3789
|
+
}
|
|
3790
|
+
function polyAtoms(...ps) {
|
|
3791
|
+
const s = new Set();
|
|
3792
|
+
for (const p of ps) {
|
|
3793
|
+
for (const t of p) {
|
|
3794
|
+
for (let i = 0; i < t.exps.length; i++)
|
|
3795
|
+
if (t.exps[i] > 0)
|
|
3796
|
+
s.add(i);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
return s;
|
|
3800
|
+
}
|
|
3801
|
+
/** Monic GCD of two polynomials in one atom (Euclid); 1 when several atoms are involved. */
|
|
3802
|
+
function polyGcd(a, b) {
|
|
3803
|
+
if (polyAtoms(a, b).size > 1)
|
|
3804
|
+
return polyConst(Rational.One);
|
|
3805
|
+
let x = a;
|
|
3806
|
+
let y = b;
|
|
3807
|
+
while (y.length > 0) {
|
|
3808
|
+
const { r } = polyDivMod(x, y);
|
|
3809
|
+
x = y;
|
|
3810
|
+
y = r;
|
|
3811
|
+
}
|
|
3812
|
+
if (x.length === 0)
|
|
3813
|
+
return polyConst(Rational.One);
|
|
3814
|
+
const lc = x[0].c;
|
|
3815
|
+
return x.map((t) => ({ exps: t.exps, c: t.c.divide(lc) }));
|
|
3816
|
+
}
|
|
3817
|
+
/** The largest monomial dividing every term (exponent-wise minimum). */
|
|
3818
|
+
function monomialContent(p) {
|
|
3819
|
+
if (p.length === 0)
|
|
3820
|
+
return [];
|
|
3821
|
+
const n = Math.max(...p.map((t) => t.exps.length));
|
|
3822
|
+
const out = [];
|
|
3823
|
+
for (let i = 0; i < n; i++)
|
|
3824
|
+
out.push(Math.min(...p.map((t) => t.exps[i] ?? 0)));
|
|
3825
|
+
return out;
|
|
3826
|
+
}
|
|
3827
|
+
function polyDivMonomial(p, m) {
|
|
3828
|
+
return p.map((t) => ({ exps: t.exps.map((e, i) => e - (m[i] ?? 0)), c: t.c }));
|
|
3829
|
+
}
|
|
3830
|
+
const ratioFromPoly = (n) => ({ n, d: polyConst(Rational.One) });
|
|
3831
|
+
function ratioAdd(a, b, subtract) {
|
|
3832
|
+
const bn = subtract ? polyNeg(b.n) : b.n;
|
|
3833
|
+
if (polyEqual(a.d, b.d))
|
|
3834
|
+
return { n: polyAdd(a.n, bn), d: a.d };
|
|
3835
|
+
return { n: polyAdd(polyMul(a.n, b.d), polyMul(bn, a.d)), d: polyMul(a.d, b.d) };
|
|
3836
|
+
}
|
|
3837
|
+
const ratioMul = (a, b) => ({ n: polyMul(a.n, b.n), d: polyMul(a.d, b.d) });
|
|
3838
|
+
function ratioDiv(a, b) {
|
|
3839
|
+
if (b.n.length === 0)
|
|
3840
|
+
throw new RatBail(); // division by zero: leave it to evaluation
|
|
3841
|
+
return { n: polyMul(a.n, b.d), d: polyMul(a.d, b.n) };
|
|
3842
|
+
}
|
|
3843
|
+
function ratioPow(a, n) {
|
|
3844
|
+
const k = Math.abs(n);
|
|
3845
|
+
if (k > 1000)
|
|
3846
|
+
throw new RatBail();
|
|
3847
|
+
const singleTerm = a.n.length === 1 && a.d.length === 1;
|
|
3848
|
+
if (!singleTerm && k > MAX_RATIONAL_POWER)
|
|
3849
|
+
throw new RatBail();
|
|
3850
|
+
let result;
|
|
3851
|
+
if (singleTerm) {
|
|
3852
|
+
const mono = (t) => ({ exps: t.exps.map((e) => e * k), c: t.c.pow(k) });
|
|
3853
|
+
result = { n: [mono(a.n[0])], d: [mono(a.d[0])] };
|
|
3854
|
+
}
|
|
3855
|
+
else {
|
|
3856
|
+
result = ratioFromPoly(polyConst(Rational.One));
|
|
3857
|
+
for (let i = 0; i < k; i++)
|
|
3858
|
+
result = ratioMul(result, a);
|
|
3859
|
+
}
|
|
3860
|
+
return n < 0 ? ratioDiv(ratioFromPoly(polyConst(Rational.One)), result) : result;
|
|
3861
|
+
}
|
|
3862
|
+
function exactRational(value) {
|
|
3863
|
+
if (!Number.isFinite(value))
|
|
3864
|
+
return null;
|
|
3865
|
+
if (Number.isInteger(value))
|
|
3866
|
+
return new Rational(BigInt(value));
|
|
3867
|
+
const r = Rational.fromNumber(value, 10_000);
|
|
3868
|
+
return r.toNumber() === value ? r : null;
|
|
3869
|
+
}
|
|
3870
|
+
function toRatio(e, ctx) {
|
|
3871
|
+
const atom = (x) => {
|
|
3872
|
+
const i = atomIndex(ctx, x);
|
|
3873
|
+
const exps = new Array(i + 1).fill(0);
|
|
3874
|
+
exps[i] = 1;
|
|
3875
|
+
return ratioFromPoly([{ exps, c: Rational.One }]);
|
|
3876
|
+
};
|
|
3877
|
+
switch (e.type) {
|
|
3878
|
+
case "const": {
|
|
3879
|
+
const r = exactRational(e.value);
|
|
3880
|
+
return r ? ratioFromPoly(polyConst(r)) : atom(e);
|
|
3881
|
+
}
|
|
3882
|
+
case "var":
|
|
3883
|
+
return atom(e);
|
|
3884
|
+
case "neg": {
|
|
3885
|
+
const a = toRatio(e.arg, ctx);
|
|
3886
|
+
return { n: polyNeg(a.n), d: a.d };
|
|
3887
|
+
}
|
|
3888
|
+
case "add":
|
|
3889
|
+
return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), false);
|
|
3890
|
+
case "sub":
|
|
3891
|
+
return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), true);
|
|
3892
|
+
case "mul":
|
|
3893
|
+
return ratioMul(toRatio(e.left, ctx), toRatio(e.right, ctx));
|
|
3894
|
+
case "div":
|
|
3895
|
+
return ratioDiv(toRatio(e.left, ctx), toRatio(e.right, ctx));
|
|
3896
|
+
case "pow": {
|
|
3897
|
+
if (e.exp.type === "const" && Number.isInteger(e.exp.value)) {
|
|
3898
|
+
return ratioPow(toRatio(e.base, ctx), e.exp.value);
|
|
3899
|
+
}
|
|
3900
|
+
return atom(pow(normalizeRational(e.base), normalizeRational(e.exp)));
|
|
3901
|
+
}
|
|
3902
|
+
case "func":
|
|
3903
|
+
return atom(fn(e.name, normalizeRational(e.arg)));
|
|
3904
|
+
case "call2":
|
|
3905
|
+
return atom(call2(e.name, normalizeRational(e.left), normalizeRational(e.right)));
|
|
3906
|
+
case "cmp":
|
|
3907
|
+
return atom(cmp(e.op, normalizeRational(e.left), normalizeRational(e.right)));
|
|
3908
|
+
case "piecewise":
|
|
3909
|
+
return atom(piecewise(e.branches.map((br) => ({ cond: normalizeRational(br.cond), expr: normalizeRational(br.expr) })), normalizeRational(e.otherwise)));
|
|
3910
|
+
case "sum":
|
|
3911
|
+
return atom(sumExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)));
|
|
3912
|
+
case "product":
|
|
3913
|
+
return atom(productExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)));
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
function bigGcd(a, b) {
|
|
3917
|
+
let x = a < 0n ? -a : a;
|
|
3918
|
+
let y = b < 0n ? -b : b;
|
|
3919
|
+
while (y)
|
|
3920
|
+
[x, y] = [y, x % y];
|
|
3921
|
+
return x;
|
|
3922
|
+
}
|
|
3923
|
+
/** Cancel common factors, then scale to coprime integer coefficients with a positive denominator lead. */
|
|
3924
|
+
function ratioReduce(r) {
|
|
3925
|
+
if (r.n.length === 0)
|
|
3926
|
+
return ratioFromPoly([]);
|
|
3927
|
+
let { n, d } = r;
|
|
3928
|
+
const m = monomialContent([...n, ...d]);
|
|
3929
|
+
if (m.some((e) => e > 0)) {
|
|
3930
|
+
n = polyDivMonomial(n, m);
|
|
3931
|
+
d = polyDivMonomial(d, m);
|
|
3932
|
+
}
|
|
3933
|
+
if (!polyIsConst(d)) {
|
|
3934
|
+
const g = polyGcd(n, d);
|
|
3935
|
+
if (!polyIsConst(g)) {
|
|
3936
|
+
n = polyDivMod(n, g).q;
|
|
3937
|
+
d = polyDivMod(d, g).q;
|
|
3938
|
+
}
|
|
3939
|
+
else {
|
|
3940
|
+
// Several atoms: still cancel when one side divides the other exactly.
|
|
3941
|
+
const dn = polyDivMod(n, d);
|
|
3942
|
+
if (dn.r.length === 0) {
|
|
3943
|
+
n = dn.q;
|
|
3944
|
+
d = polyConst(Rational.One);
|
|
3945
|
+
}
|
|
3946
|
+
else {
|
|
3947
|
+
const nd = polyDivMod(d, n);
|
|
3948
|
+
if (nd.r.length === 0) {
|
|
3949
|
+
d = nd.q;
|
|
3950
|
+
n = polyConst(Rational.One);
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
if (polyIsConst(d)) {
|
|
3956
|
+
const c = d[0].c;
|
|
3957
|
+
return { n: n.map((t) => ({ exps: t.exps, c: t.c.divide(c) })), d: polyConst(Rational.One) };
|
|
3958
|
+
}
|
|
3959
|
+
// integer coefficients: multiply through by the lcm of the denominators, divide by the content
|
|
3960
|
+
let lcm = 1n;
|
|
3961
|
+
for (const t of [...n, ...d]) {
|
|
3962
|
+
const den = t.c.denominator;
|
|
3963
|
+
lcm = (lcm / bigGcd(lcm, den)) * den;
|
|
3964
|
+
}
|
|
3965
|
+
const scale = new Rational(lcm);
|
|
3966
|
+
n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
|
|
3967
|
+
d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
|
|
3968
|
+
let content = 0n;
|
|
3969
|
+
for (const t of [...n, ...d])
|
|
3970
|
+
content = bigGcd(content, t.c.numerator);
|
|
3971
|
+
if (content > 1n) {
|
|
3972
|
+
const inv = new Rational(1n, content);
|
|
3973
|
+
n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
|
|
3974
|
+
d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
|
|
3975
|
+
}
|
|
3976
|
+
if (d[0].c.sign < 0) {
|
|
3977
|
+
n = polyNeg(n);
|
|
3978
|
+
d = polyNeg(d);
|
|
3979
|
+
}
|
|
3980
|
+
return { n, d };
|
|
3981
|
+
}
|
|
3982
|
+
function monomialExpr(exps, ctx) {
|
|
3983
|
+
let out = null;
|
|
3984
|
+
for (let i = 0; i < exps.length; i++) {
|
|
3985
|
+
const e = exps[i] ?? 0;
|
|
3986
|
+
if (e === 0)
|
|
3987
|
+
continue;
|
|
3988
|
+
const factor = e === 1 ? ctx.atoms[i] : pow(ctx.atoms[i], num(e));
|
|
3989
|
+
out = out ? mul(out, factor) : factor;
|
|
3990
|
+
}
|
|
3991
|
+
return out;
|
|
3992
|
+
}
|
|
3993
|
+
/** A polynomial as a descending-degree sum; fractional coefficients print as `p*m/q`. */
|
|
3994
|
+
function polyToTree(p, ctx) {
|
|
3995
|
+
if (p.length === 0)
|
|
3996
|
+
return num(0);
|
|
3997
|
+
const ordered = [...p].sort((a, b) => {
|
|
3998
|
+
const da = a.exps.reduce((s, x) => s + x, 0);
|
|
3999
|
+
const db = b.exps.reduce((s, x) => s + x, 0);
|
|
4000
|
+
return db - da || expsCmp(b.exps, a.exps);
|
|
4001
|
+
});
|
|
4002
|
+
let out = null;
|
|
4003
|
+
for (const t of ordered) {
|
|
4004
|
+
const negative = t.c.sign < 0;
|
|
4005
|
+
const abs = t.c.abs();
|
|
4006
|
+
const mono = monomialExpr(t.exps, ctx);
|
|
4007
|
+
const numer = abs.numerator;
|
|
4008
|
+
let body;
|
|
4009
|
+
if (!mono)
|
|
4010
|
+
body = num(abs.toNumber());
|
|
4011
|
+
else if (numer === 1n && abs.denominator === 1n)
|
|
4012
|
+
body = mono;
|
|
4013
|
+
else if (abs.denominator === 1n)
|
|
4014
|
+
body = mul(num(Number(numer)), mono);
|
|
4015
|
+
else
|
|
4016
|
+
body = div(numer === 1n ? mono : mul(num(Number(numer)), mono), num(Number(abs.denominator)));
|
|
4017
|
+
if (!out)
|
|
4018
|
+
out = negative ? neg(body) : body;
|
|
4019
|
+
else
|
|
4020
|
+
out = negative ? sub(out, body) : add(out, body);
|
|
4021
|
+
}
|
|
4022
|
+
return out;
|
|
4023
|
+
}
|
|
4024
|
+
function nodeCount(e) {
|
|
4025
|
+
switch (e.type) {
|
|
4026
|
+
case "const":
|
|
4027
|
+
case "var":
|
|
4028
|
+
return 1;
|
|
4029
|
+
case "neg":
|
|
4030
|
+
case "func":
|
|
4031
|
+
return 1 + nodeCount(e.arg);
|
|
4032
|
+
case "pow":
|
|
4033
|
+
return 1 + nodeCount(e.base) + nodeCount(e.exp);
|
|
4034
|
+
case "piecewise":
|
|
4035
|
+
return 1 + nodeCount(e.otherwise) + e.branches.reduce((s, br) => s + nodeCount(br.cond) + nodeCount(br.expr), 0);
|
|
4036
|
+
case "sum":
|
|
4037
|
+
case "product":
|
|
4038
|
+
return 1 + nodeCount(e.from) + nodeCount(e.to) + nodeCount(e.body);
|
|
4039
|
+
default:
|
|
4040
|
+
return 1 + nodeCount(e.left) + nodeCount(e.right);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
/**
|
|
4044
|
+
* Expand `e` as a rational function and cancel common factors, returning the result only
|
|
4045
|
+
* when it has strictly fewer nodes than `e`; otherwise returns `e` itself.
|
|
4046
|
+
*/
|
|
4047
|
+
function normalizeRational(e) {
|
|
4048
|
+
if (e.type === "const" || e.type === "var")
|
|
4049
|
+
return e;
|
|
4050
|
+
try {
|
|
4051
|
+
const ctx = { atoms: [], keys: [] };
|
|
4052
|
+
const reduced = ratioReduce(toRatio(e, ctx));
|
|
4053
|
+
const numer = polyToTree(reduced.n, ctx);
|
|
4054
|
+
const candidate = polyIsConst(reduced.d) ? numer : div(numer, polyToTree(reduced.d, ctx));
|
|
4055
|
+
return nodeCount(candidate) < nodeCount(e) ? candidate : e;
|
|
4056
|
+
}
|
|
4057
|
+
catch (err) {
|
|
4058
|
+
if (err instanceof RatBail)
|
|
4059
|
+
return e;
|
|
4060
|
+
throw err;
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
3561
4063
|
// -- equation solving & factoring ----------------------------------------------
|
|
3562
4064
|
/** Best-effort exact `Expr` for a numeric value: an integer, a reduced fraction, or (as a last resort) the raw float. */
|
|
3563
4065
|
function toExactExpr(value) {
|
|
@@ -3591,7 +4093,7 @@ function linearCoeffsForSystem(eq, variables) {
|
|
|
3591
4093
|
return null;
|
|
3592
4094
|
const coeffs = [];
|
|
3593
4095
|
for (const name of variables) {
|
|
3594
|
-
const derivative =
|
|
4096
|
+
const derivative = simplifyCore(diff(eq, name));
|
|
3595
4097
|
if (variables.some((other) => containsVar(derivative, other)))
|
|
3596
4098
|
return null;
|
|
3597
4099
|
const coeff = evalExpr(derivative, zeroEnv);
|
|
@@ -3623,7 +4125,7 @@ function polynomialCoeffs(expr, variable, maxDegree) {
|
|
|
3623
4125
|
// derivative of x^0 (`0·x^-1`) evaluates to `0·Infinity = NaN` at x=0
|
|
3624
4126
|
// even though it is symbolically zero — simplifying collapses `0·anything`
|
|
3625
4127
|
// to `0` before that indeterminate form can arise.
|
|
3626
|
-
term =
|
|
4128
|
+
term = simplifyCore(diff(term, variable));
|
|
3627
4129
|
}
|
|
3628
4130
|
while (coeffs.length > 1 && Math.abs(coeffs[coeffs.length - 1]) < 1e-9)
|
|
3629
4131
|
coeffs.pop();
|
|
@@ -3818,7 +4320,7 @@ function factorPolynomial(coeffsIn, variable) {
|
|
|
3818
4320
|
else if (Math.abs(remaining[0] - 1) > 1e-9) {
|
|
3819
4321
|
result = mul(result, toExactExpr(remaining[0]));
|
|
3820
4322
|
}
|
|
3821
|
-
return
|
|
4323
|
+
return simplifyCore(result);
|
|
3822
4324
|
}
|
|
3823
4325
|
// -- limits ---------------------------------------------------------------------
|
|
3824
4326
|
function evalNear(e, x, point, direction) {
|
|
@@ -3842,19 +4344,25 @@ function limitAt(e, x, point, direction, depth) {
|
|
|
3842
4344
|
// operand verbatim into the result) and a limit needing more than a
|
|
3843
4345
|
// couple of rounds (e.g. most limits at infinity) blows up memory
|
|
3844
4346
|
// long before the depth<12 cap is reached.
|
|
3845
|
-
const df =
|
|
3846
|
-
const dg =
|
|
4347
|
+
const df = simplifyCore(diff(e.left, x));
|
|
4348
|
+
const dg = simplifyCore(diff(e.right, x));
|
|
3847
4349
|
return limitAt(div(df, dg), x, point, direction, depth + 1);
|
|
3848
4350
|
}
|
|
3849
4351
|
}
|
|
3850
4352
|
return direct;
|
|
3851
4353
|
}
|
|
3852
4354
|
// -- recursive-descent parser ----------------------------------------------
|
|
4355
|
+
/** Every name the parser treats as a function, longest first, so `asinh(` is not read as `a*sinh(`. */
|
|
4356
|
+
const FUNCTION_NAMES_LONGEST_FIRST = [...FUNCTION_NAMES].sort((a, b) => b.length - a.length);
|
|
3853
4357
|
class Parser {
|
|
3854
4358
|
s;
|
|
4359
|
+
implicit;
|
|
3855
4360
|
pos = 0;
|
|
3856
|
-
constructor(input) {
|
|
3857
|
-
this.
|
|
4361
|
+
constructor(input, options = {}) {
|
|
4362
|
+
this.implicit = options.implicitMultiplication === true;
|
|
4363
|
+
// With implicit multiplication, white space between two numbers is a product (`2 3`), not one number.
|
|
4364
|
+
const spaced = this.implicit ? input.replace(/(\d)\s+(?=[\d.])/g, "$1*") : input;
|
|
4365
|
+
this.s = spaced.replace(/\s+/g, "");
|
|
3858
4366
|
}
|
|
3859
4367
|
parse() {
|
|
3860
4368
|
const e = this.comparison();
|
|
@@ -3973,12 +4481,38 @@ class Parser {
|
|
|
3973
4481
|
}
|
|
3974
4482
|
term() {
|
|
3975
4483
|
let left = this.factor();
|
|
3976
|
-
|
|
3977
|
-
const
|
|
3978
|
-
|
|
3979
|
-
|
|
4484
|
+
for (;;) {
|
|
4485
|
+
const ch = this.peek();
|
|
4486
|
+
if (ch === "*" || ch === "/") {
|
|
4487
|
+
this.pos++;
|
|
4488
|
+
const right = this.factor();
|
|
4489
|
+
left = ch === "*" ? mul(left, right) : div(left, right);
|
|
4490
|
+
}
|
|
4491
|
+
else if (this.implicit && /[a-zA-Z0-9.(]/.test(ch)) {
|
|
4492
|
+
left = mul(left, this.factor()); // juxtaposition: `2x`, `x y`, `2(x+1)`
|
|
4493
|
+
}
|
|
4494
|
+
else {
|
|
4495
|
+
return left;
|
|
4496
|
+
}
|
|
3980
4497
|
}
|
|
3981
|
-
|
|
4498
|
+
}
|
|
4499
|
+
/**
|
|
4500
|
+
* The identifier at `this.pos` under implicit multiplication: a function name followed by `(`, `pi`, or a
|
|
4501
|
+
* single letter with an optional `_subscript`. A letter run like `xy` is therefore read one letter at a time.
|
|
4502
|
+
*/
|
|
4503
|
+
implicitIdentifier() {
|
|
4504
|
+
const rest = this.s.slice(this.pos);
|
|
4505
|
+
const run = /^[a-zA-Z]+/.exec(rest)?.[0] ?? "";
|
|
4506
|
+
const call = FUNCTION_NAMES_LONGEST_FIRST.find((f) => rest.startsWith(f) && rest[f.length] === "(");
|
|
4507
|
+
if (call)
|
|
4508
|
+
return call;
|
|
4509
|
+
const bare = FUNCTION_NAMES_LONGEST_FIRST.find((f) => f.length > 1 && run.startsWith(f));
|
|
4510
|
+
if (bare)
|
|
4511
|
+
throw new Error(`Function ${bare} needs parentheses: ${bare}(...)`);
|
|
4512
|
+
if (rest.startsWith("pi"))
|
|
4513
|
+
return "pi";
|
|
4514
|
+
const sub = /^[a-zA-Z]_(?:\d+|[a-zA-Z])/.exec(rest);
|
|
4515
|
+
return sub ? sub[0] : rest[0];
|
|
3982
4516
|
}
|
|
3983
4517
|
factor() {
|
|
3984
4518
|
if (this.peek() === "-") {
|
|
@@ -4016,9 +4550,11 @@ class Parser {
|
|
|
4016
4550
|
return num(Number(numMatch[0]));
|
|
4017
4551
|
}
|
|
4018
4552
|
// identifier (function or variable or constant)
|
|
4019
|
-
const idMatch =
|
|
4553
|
+
const idMatch = this.implicit
|
|
4554
|
+
? /^[a-zA-Z_]/.exec(this.s.slice(this.pos))
|
|
4555
|
+
: /^[a-zA-Z_]\w*/.exec(this.s.slice(this.pos));
|
|
4020
4556
|
if (idMatch) {
|
|
4021
|
-
const name = idMatch[0];
|
|
4557
|
+
const name = this.implicit ? this.implicitIdentifier() : idMatch[0];
|
|
4022
4558
|
this.pos += name.length;
|
|
4023
4559
|
if ((name === "log" || name === "clamp") && this.peek() === "(") {
|
|
4024
4560
|
this.pos++;
|