@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/src/Symbolic.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { FUNC_DERIVATIVE_RULES } from "./differentiation-rules.ts";
|
|
|
8
8
|
import { MatrixMath } from "./MatrixMath.ts";
|
|
9
9
|
import { Numerical } from "./Numerical.ts";
|
|
10
10
|
import { Rational } from "./Rational.ts";
|
|
11
|
+
import { compileShader, type ShaderOptions, type ShaderResult } from "./Shader.ts";
|
|
11
12
|
import { SpecialFunctions } from "./SpecialFunctions.ts";
|
|
12
13
|
import type { Structure } from "./Structure.ts";
|
|
13
14
|
|
|
@@ -443,15 +444,38 @@ export interface OdeClosedFormResult {
|
|
|
443
444
|
implicitRelation?: Expr;
|
|
444
445
|
}
|
|
445
446
|
|
|
447
|
+
/** Options for {@link Symbolic.parse} and {@link Symbolic.fromLatex}. */
|
|
448
|
+
export interface ParseOptions {
|
|
449
|
+
/**
|
|
450
|
+
* Read juxtaposition as multiplication, the way people type maths: `2x`, `x y`, `xy`, `2(x+1)`,
|
|
451
|
+
* `(x+1)(x-1)`, `x(x+1)`, `3sin(x)`. Default `false`, which keeps the strict grammar (`2x` is an
|
|
452
|
+
* error and `xy` is one variable named `xy`).
|
|
453
|
+
*
|
|
454
|
+
* With this on, a run of letters that is not a function call (`name(`) or `pi` is split into
|
|
455
|
+
* single-letter variables, so `xy` is `x*y` and multi-letter variable names such as `theta` are
|
|
456
|
+
* unavailable (use a subscript: `x_1` or `x_a`, which stays one variable). A letter followed by
|
|
457
|
+
* digits is a product too (`x3` is `x*3`). A function name must be followed by `(`. Space
|
|
458
|
+
* between two numbers multiplies (`2 3` is `6`). Juxtaposition has the same precedence as `*`
|
|
459
|
+
* and `/` and binds left to right, so `1/2x` is `(1/2)*x`; `^` binds tighter (`2x^2` is `2*(x^2)`).
|
|
460
|
+
* For `fromLatex` it also lets a function with no brackets take a whole product as its argument
|
|
461
|
+
* (`\sin 2x` is `sin(2*x)`, `\sin x\cos x` is `sin(x)*cos(x)`), and `^` take one token (`x^23` is
|
|
462
|
+
* `x^2*3`, as TeX reads it; `x^{23}` is `x^23`).
|
|
463
|
+
*/
|
|
464
|
+
implicitMultiplication?: boolean;
|
|
465
|
+
}
|
|
466
|
+
|
|
446
467
|
export class Symbolic {
|
|
447
|
-
/**
|
|
448
|
-
|
|
449
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Parse an expression string such as `"sin(x^2) + 3*x"` into an AST.
|
|
470
|
+
* Pass `{ implicitMultiplication: true }` to also read `2x`, `x y` and `2(x+1)`; see {@link ParseOptions}.
|
|
471
|
+
*/
|
|
472
|
+
static parse(input: string, options: ParseOptions = {}): Expr {
|
|
473
|
+
return new Parser(input, options ?? {}).parse();
|
|
450
474
|
}
|
|
451
475
|
|
|
452
476
|
static differentiate(expr: Expr | string, variable = "x"): Expr {
|
|
453
477
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
454
|
-
return
|
|
478
|
+
return simplifyCore(diff(e, variable));
|
|
455
479
|
}
|
|
456
480
|
|
|
457
481
|
/**
|
|
@@ -465,17 +489,21 @@ export class Symbolic {
|
|
|
465
489
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
466
490
|
const steps: DifferentiationStep[] = [];
|
|
467
491
|
const raw = diffTraced(e, variable, steps);
|
|
468
|
-
return { steps, result:
|
|
492
|
+
return { steps, result: simplifyCore(raw) };
|
|
469
493
|
}
|
|
470
494
|
|
|
495
|
+
/**
|
|
496
|
+
* Algebraic simplification: constant folding, like-term collection, and, as a last
|
|
497
|
+
* step, expanding the expression as a rational function and cancelling common
|
|
498
|
+
* factors -- `(x+1)^2 - x^2 - 2*x` gives `1` and `(x^2-1)/(x-1)` gives `x+1`.
|
|
499
|
+
* The expanded/cancelled form is used only when it is strictly smaller than the
|
|
500
|
+
* collected form, so a product such as `(x+1)*2` keeps its shape. Non-polynomial
|
|
501
|
+
* subexpressions (`sin(x)`, `x^y`) are treated as opaque symbols. Like any CAS
|
|
502
|
+
* cancellation, `(x^2-1)/(x-1)` is `x+1` as an expression, ignoring that the
|
|
503
|
+
* original is undefined at `x = 1`.
|
|
504
|
+
*/
|
|
471
505
|
static simplify(expr: Expr | string): Expr {
|
|
472
|
-
|
|
473
|
-
for (let i = 0; i < 30; i++) {
|
|
474
|
-
const next = collectLikeTerms(simplifyOnce(e));
|
|
475
|
-
if (equal(next, e)) return next;
|
|
476
|
-
e = next;
|
|
477
|
-
}
|
|
478
|
-
return e;
|
|
506
|
+
return normalizeRational(simplifyCore(expr));
|
|
479
507
|
}
|
|
480
508
|
|
|
481
509
|
/**
|
|
@@ -495,15 +523,15 @@ export class Symbolic {
|
|
|
495
523
|
* assumptions (e.g. "x+y is positive" isn't expressible).
|
|
496
524
|
*/
|
|
497
525
|
static simplifyAssuming(expr: Expr | string, assumptions: Record<string, Assumption>): Expr {
|
|
498
|
-
const simplified =
|
|
526
|
+
const simplified = simplifyCore(expr);
|
|
499
527
|
const rewritten = applyAssumptions(simplified, assumptions);
|
|
500
|
-
return
|
|
528
|
+
return simplifyCore(rewritten);
|
|
501
529
|
}
|
|
502
530
|
|
|
503
531
|
/** Symbolic anti-derivative (elementary rules; throws {@link NotIntegrableError} otherwise). */
|
|
504
532
|
static integrate(expr: Expr | string, variable = "x"): Expr {
|
|
505
533
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
506
|
-
return
|
|
534
|
+
return simplifyCore(integ(e, variable));
|
|
507
535
|
}
|
|
508
536
|
|
|
509
537
|
/**
|
|
@@ -542,13 +570,13 @@ export class Symbolic {
|
|
|
542
570
|
variable = "x",
|
|
543
571
|
env: Record<string, number> = {},
|
|
544
572
|
): number {
|
|
545
|
-
const e =
|
|
573
|
+
const e = simplifyCore(typeof expr === "string" ? Symbolic.parse(expr) : expr);
|
|
546
574
|
const probe = compileExpr(e);
|
|
547
575
|
if (hasInteriorSingularity((val: number) => probe({ ...env, [variable]: val }), lower, upper)) {
|
|
548
576
|
throw new IntegrationSingularityError();
|
|
549
577
|
}
|
|
550
578
|
try {
|
|
551
|
-
const F =
|
|
579
|
+
const F = simplifyCore(integ(e, variable));
|
|
552
580
|
const at = (val: number) => evalExpr(F, { ...env, [variable]: val });
|
|
553
581
|
return at(upper) - at(lower);
|
|
554
582
|
} catch (err) {
|
|
@@ -608,7 +636,7 @@ export class Symbolic {
|
|
|
608
636
|
xVar = "x",
|
|
609
637
|
yVar = "y",
|
|
610
638
|
): OdeClosedFormResult {
|
|
611
|
-
const e =
|
|
639
|
+
const e = simplifyCore(typeof dyDxExpr === "string" ? Symbolic.parse(dyDxExpr) : dyDxExpr);
|
|
612
640
|
const sep = matchSeparable(e, xVar, yVar);
|
|
613
641
|
if (sep) return solveSeparableOde(sep, xVar, yVar, x0, y0);
|
|
614
642
|
const lin = matchLinearOde(e, xVar, yVar);
|
|
@@ -724,14 +752,14 @@ export class Symbolic {
|
|
|
724
752
|
}
|
|
725
753
|
term = diff(term, variable);
|
|
726
754
|
}
|
|
727
|
-
return
|
|
755
|
+
return simplifyCore(result);
|
|
728
756
|
}
|
|
729
757
|
|
|
730
758
|
/** Replace every occurrence of a variable with a sub-expression (e.g. for changing variables or plugging in a solved value). */
|
|
731
759
|
static substitute(expr: Expr | string, variable: string, replacement: Expr | string): Expr {
|
|
732
760
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
733
761
|
const r = typeof replacement === "string" ? Symbolic.parse(replacement) : replacement;
|
|
734
|
-
return
|
|
762
|
+
return simplifyCore(subst(e, variable, r));
|
|
735
763
|
}
|
|
736
764
|
|
|
737
765
|
/** Distribute multiplication over addition/subtraction and expand small integer powers of sums, e.g. `(x+1)^2 -> x^2+2*x+1`. */
|
|
@@ -739,7 +767,7 @@ export class Symbolic {
|
|
|
739
767
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
740
768
|
let cur = e;
|
|
741
769
|
for (let i = 0; i < 30; i++) {
|
|
742
|
-
const next =
|
|
770
|
+
const next = simplifyCore(expandOnce(cur));
|
|
743
771
|
if (equal(next, cur)) return next;
|
|
744
772
|
cur = next;
|
|
745
773
|
}
|
|
@@ -837,6 +865,23 @@ export class Symbolic {
|
|
|
837
865
|
return render(typeof expr === "string" ? Symbolic.parse(expr) : expr, 0);
|
|
838
866
|
}
|
|
839
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Compile `expr` to a GLSL ES 3.0 expression over `float` (default) or, with
|
|
870
|
+
* `{ complex: true }`, over `vec2` complex numbers (the helpers it needs, such as
|
|
871
|
+
* `mx_cmul`, are returned in `helpers` and emitted once). Output is deterministic.
|
|
872
|
+
* Refuses nodes with no shader form (`sum`, `product`, `erf`, `gcd`, `lcm`, and anything
|
|
873
|
+
* not closed under the complex numbers in complex mode) with a `ShaderUnsupportedError`.
|
|
874
|
+
* Shader `pow` of a negative base with a non-integer exponent is undefined, unlike JS `NaN`.
|
|
875
|
+
*/
|
|
876
|
+
static toGLSL(expr: Expr | string, options: ShaderOptions = {}): ShaderResult {
|
|
877
|
+
return compileShader(typeof expr === "string" ? Symbolic.parse(expr) : expr, "glsl", options);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** As {@link toGLSL}, for WGSL (`f32` / `vec2<f32>`; `select` replaces the ternary). */
|
|
881
|
+
static toWGSL(expr: Expr | string, options: ShaderOptions = {}): ShaderResult {
|
|
882
|
+
return compileShader(typeof expr === "string" ? Symbolic.parse(expr) : expr, "wgsl", options);
|
|
883
|
+
}
|
|
884
|
+
|
|
840
885
|
/** Render `expr` as a LaTeX string, e.g. `"x/2"` -> `"\\frac{x}{2}"`. */
|
|
841
886
|
static toLatex(expr: Expr | string): string {
|
|
842
887
|
return toLatexRec(typeof expr === "string" ? Symbolic.parse(expr) : expr, 0);
|
|
@@ -852,12 +897,16 @@ export class Symbolic {
|
|
|
852
897
|
* commands (`\sin`, `\arcsin`, `\sinh`, `\operatorname{sech}`, ...), and the
|
|
853
898
|
* two-argument `BinaryFuncName` commands (`\min`, `\max`, `\gcd`,
|
|
854
899
|
* `\operatorname{atan2}`, `\operatorname{hypot}`, `\operatorname{lcm}`).
|
|
855
|
-
* Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored.
|
|
900
|
+
* Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored. Single-token arguments as TeX
|
|
901
|
+
* reads them (`\frac12`, `\frac1x`, `\sqrt2`) and a power on a function command
|
|
902
|
+
* (`\sin^{2}(x)` is `sin(x)^2`) are accepted. An exponent of `-1` on a function command (`\sin^{-1}`) is
|
|
903
|
+
* refused as ambiguous (inverse function or reciprocal): write `\arcsin(x)` or `(\sin x)^{-1}`. For what people type into a math field
|
|
904
|
+
* (`2x`, `\sin 2x`, `(x+1)(x-1)`, `x^23`) pass `{ implicitMultiplication: true }`; see {@link ParseOptions}.
|
|
856
905
|
*
|
|
857
906
|
* @throws on LaTeX constructs with no `Expr` equivalent, e.g. `\int` or `\sum`.
|
|
858
907
|
*/
|
|
859
|
-
static fromLatex(latex: string): Expr {
|
|
860
|
-
return Symbolic.parse(latexToInfix(latex));
|
|
908
|
+
static fromLatex(latex: string, options: ParseOptions = {}): Expr {
|
|
909
|
+
return Symbolic.parse(latexToInfix(latex, options), options);
|
|
861
910
|
}
|
|
862
911
|
|
|
863
912
|
/**
|
|
@@ -878,11 +927,11 @@ export class Symbolic {
|
|
|
878
927
|
*/
|
|
879
928
|
static solve(expr: Expr | string, variable = "x"): Expr[] {
|
|
880
929
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
881
|
-
const coeffs = polynomialCoeffs(
|
|
930
|
+
const coeffs = polynomialCoeffs(simplifyCore(e), variable, 6);
|
|
882
931
|
if (!coeffs) {
|
|
883
932
|
throw new Error(`solve: expression is not a polynomial in "${variable}" of degree ≤ 6.`);
|
|
884
933
|
}
|
|
885
|
-
return solvePolynomial(coeffs).map((r) =>
|
|
934
|
+
return solvePolynomial(coeffs).map((r) => simplifyCore(r));
|
|
886
935
|
}
|
|
887
936
|
|
|
888
937
|
/**
|
|
@@ -900,7 +949,7 @@ export class Symbolic {
|
|
|
900
949
|
* @throws {SingularSystemError} if the coefficient matrix is singular/rank-deficient.
|
|
901
950
|
*/
|
|
902
951
|
static solveSystem(equations: (Expr | string)[], variables: string[]): Record<string, number> {
|
|
903
|
-
const exprs = equations.map((eq) =>
|
|
952
|
+
const exprs = equations.map((eq) => simplifyCore(typeof eq === "string" ? Symbolic.parse(eq) : eq));
|
|
904
953
|
if (exprs.length !== variables.length) {
|
|
905
954
|
throw new Error(
|
|
906
955
|
`solveSystem: expected ${variables.length} equations for ${variables.length} variables, got ${exprs.length}.`,
|
|
@@ -1149,7 +1198,7 @@ export class Symbolic {
|
|
|
1149
1198
|
*/
|
|
1150
1199
|
static factor(expr: Expr | string, variable = "x"): Expr {
|
|
1151
1200
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
1152
|
-
const simplified =
|
|
1201
|
+
const simplified = simplifyCore(e);
|
|
1153
1202
|
const coeffs = polynomialCoeffs(simplified, variable, 6);
|
|
1154
1203
|
if (!coeffs) return simplified;
|
|
1155
1204
|
return factorPolynomial(coeffs, variable);
|
|
@@ -1368,6 +1417,17 @@ function diff(e: Expr, x: string): Expr {
|
|
|
1368
1417
|
}
|
|
1369
1418
|
|
|
1370
1419
|
// -- simplification ---------------------------------------------------------
|
|
1420
|
+
/** Constant folding and like-term collection to a fixed point -- no expansion or cancellation. */
|
|
1421
|
+
function simplifyCore(expr: Expr | string): Expr {
|
|
1422
|
+
let e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
1423
|
+
for (let i = 0; i < 30; i++) {
|
|
1424
|
+
const next = collectLikeTerms(simplifyOnce(e));
|
|
1425
|
+
if (equal(next, e)) return next;
|
|
1426
|
+
e = next;
|
|
1427
|
+
}
|
|
1428
|
+
return e;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1371
1431
|
function simplifyOnce(e: Expr): Expr {
|
|
1372
1432
|
if (e.type === "const" || e.type === "var") return e;
|
|
1373
1433
|
if (e.type === "neg") {
|
|
@@ -2115,7 +2175,7 @@ function freshVariableName(e: Expr, x: string): string {
|
|
|
2115
2175
|
* legitimately hit a domain error/NaN on either side).
|
|
2116
2176
|
*/
|
|
2117
2177
|
function verifyByDifferentiation(candidate: Expr, e: Expr, x: string): boolean {
|
|
2118
|
-
const check =
|
|
2178
|
+
const check = simplifyCore(diff(candidate, x));
|
|
2119
2179
|
let agreements = 0;
|
|
2120
2180
|
for (const p of [0.7, 1.3, -0.9]) {
|
|
2121
2181
|
const expected = evalExpr(e, { [x]: p });
|
|
@@ -2140,16 +2200,16 @@ function verifyByDifferentiation(candidate: Expr, e: Expr, x: string): boolean {
|
|
|
2140
2200
|
*/
|
|
2141
2201
|
function tryUSubstitution(e: Expr, x: string): Expr | null {
|
|
2142
2202
|
for (const g of collectSubstitutionCandidates(e, x)) {
|
|
2143
|
-
const gPrime =
|
|
2203
|
+
const gPrime = simplifyCore(diff(g, x));
|
|
2144
2204
|
if (isConst(gPrime, 0)) continue;
|
|
2145
|
-
const quotient =
|
|
2205
|
+
const quotient = simplifyCore(divideOutFactors(e, gPrime) ?? div(e, gPrime));
|
|
2146
2206
|
if (!containsExpr(quotient, g)) continue;
|
|
2147
2207
|
const uName = freshVariableName(quotient, x);
|
|
2148
2208
|
const substituted = substExpr(quotient, g, v(uName));
|
|
2149
2209
|
if (containsVar(substituted, x)) continue;
|
|
2150
2210
|
try {
|
|
2151
2211
|
const antiderivativeInU = integRules(substituted, uName);
|
|
2152
|
-
const candidate =
|
|
2212
|
+
const candidate = simplifyCore(substExpr(antiderivativeInU, v(uName), g));
|
|
2153
2213
|
if (verifyByDifferentiation(candidate, e, x)) return candidate;
|
|
2154
2214
|
} catch (err) {
|
|
2155
2215
|
if (!(err instanceof NotIntegrableError)) throw err;
|
|
@@ -2270,11 +2330,11 @@ function matchSeparable(e: Expr, xVar: string, yVar: string): { g: Expr; h: Expr
|
|
|
2270
2330
|
*/
|
|
2271
2331
|
function matchLinearOde(e: Expr, xVar: string, yVar: string): { p: Expr; q: Expr } | null {
|
|
2272
2332
|
if (!containsVar(e, yVar)) return null; // handled by matchSeparable's h(y)=1 case instead
|
|
2273
|
-
const negP =
|
|
2333
|
+
const negP = simplifyCore(diff(e, yVar));
|
|
2274
2334
|
if (containsVar(negP, yVar)) return null; // not affine in y
|
|
2275
|
-
const q =
|
|
2335
|
+
const q = simplifyCore(sub(e, mul(negP, v(yVar))));
|
|
2276
2336
|
if (containsVar(q, yVar)) return null; // sanity check
|
|
2277
|
-
const p =
|
|
2337
|
+
const p = simplifyCore(neg(negP));
|
|
2278
2338
|
// Conservative, matching matchSeparable's philosophy: p/q must depend on
|
|
2279
2339
|
// no free variable other than xVar (an extra symbolic parameter, or a
|
|
2280
2340
|
// stray reference to yVar that survived simplification, both bail out).
|
|
@@ -2337,21 +2397,21 @@ function solveSeparableOde(
|
|
|
2337
2397
|
x0: number,
|
|
2338
2398
|
y0: number,
|
|
2339
2399
|
): OdeClosedFormResult {
|
|
2340
|
-
const invH =
|
|
2400
|
+
const invH = simplifyCore(div(num(1), sep.h));
|
|
2341
2401
|
let H: Expr;
|
|
2342
2402
|
let G: Expr;
|
|
2343
2403
|
try {
|
|
2344
|
-
H =
|
|
2345
|
-
G =
|
|
2404
|
+
H = simplifyCore(integ(invH, yVar));
|
|
2405
|
+
G = simplifyCore(integ(sep.g, xVar));
|
|
2346
2406
|
} catch (err) {
|
|
2347
2407
|
if (err instanceof NotIntegrableError) throw new NoClosedFormError();
|
|
2348
2408
|
throw err;
|
|
2349
2409
|
}
|
|
2350
2410
|
const C = evalExpr(H, { [yVar]: y0 }) - evalExpr(G, { [xVar]: x0 });
|
|
2351
|
-
const rhs =
|
|
2411
|
+
const rhs = simplifyCore(add(G, num(C)));
|
|
2352
2412
|
const explicitY = tryInvertForY(H, yVar, rhs, y0);
|
|
2353
|
-
if (explicitY) return { explicit: true, y:
|
|
2354
|
-
return { explicit: false, implicitRelation:
|
|
2413
|
+
if (explicitY) return { explicit: true, y: simplifyCore(explicitY) };
|
|
2414
|
+
return { explicit: false, implicitRelation: simplifyCore(sub(H, rhs)) };
|
|
2355
2415
|
}
|
|
2356
2416
|
|
|
2357
2417
|
function solveLinearOde(
|
|
@@ -2365,9 +2425,9 @@ function solveLinearOde(
|
|
|
2365
2425
|
let mu: Expr;
|
|
2366
2426
|
let muQIntegral: Expr;
|
|
2367
2427
|
try {
|
|
2368
|
-
integralP =
|
|
2369
|
-
mu =
|
|
2370
|
-
muQIntegral =
|
|
2428
|
+
integralP = simplifyCore(integ(lin.p, xVar));
|
|
2429
|
+
mu = simplifyCore(fn("exp", integralP));
|
|
2430
|
+
muQIntegral = simplifyCore(integ(simplifyCore(mul(mu, lin.q)), xVar));
|
|
2371
2431
|
} catch (err) {
|
|
2372
2432
|
if (err instanceof NotIntegrableError) throw new NoClosedFormError();
|
|
2373
2433
|
throw err;
|
|
@@ -2376,7 +2436,7 @@ function solveLinearOde(
|
|
|
2376
2436
|
const muAtX0 = evalExpr(mu, { [xVar]: x0 });
|
|
2377
2437
|
const muQIntegralAtX0 = evalExpr(muQIntegral, { [xVar]: x0 });
|
|
2378
2438
|
const C = y0 * muAtX0 - muQIntegralAtX0;
|
|
2379
|
-
const y =
|
|
2439
|
+
const y = simplifyCore(div(add(muQIntegral, num(C)), mu));
|
|
2380
2440
|
return { explicit: true, y };
|
|
2381
2441
|
}
|
|
2382
2442
|
|
|
@@ -2429,7 +2489,7 @@ function matchHomogeneous(e: Expr, xVar: string, yVar: string): { vName: string;
|
|
|
2429
2489
|
}
|
|
2430
2490
|
}
|
|
2431
2491
|
if (attempts === 0 || agreements < 4) return null; // not enough usable probes to trust the match
|
|
2432
|
-
const F =
|
|
2492
|
+
const F = simplifyCore(subst(substituted, xVar, num(1)));
|
|
2433
2493
|
return { vName, F };
|
|
2434
2494
|
}
|
|
2435
2495
|
|
|
@@ -2461,15 +2521,15 @@ function solveHomogeneousOde(
|
|
|
2461
2521
|
): OdeClosedFormResult {
|
|
2462
2522
|
const { vName, F } = hom;
|
|
2463
2523
|
const v0 = y0 / x0;
|
|
2464
|
-
const FMinusV =
|
|
2524
|
+
const FMinusV = simplifyCore(sub(F, v(vName)));
|
|
2465
2525
|
if (FMinusV.type === "const" && FMinusV.value === 0) {
|
|
2466
|
-
return { explicit: true, y:
|
|
2526
|
+
return { explicit: true, y: simplifyCore(mul(num(v0), v(xVar))) };
|
|
2467
2527
|
}
|
|
2468
2528
|
const sepResult = solveSeparableOde({ g: div(num(1), v(xVar)), h: FMinusV }, xVar, vName, x0, v0);
|
|
2469
2529
|
if (sepResult.explicit && sepResult.y) {
|
|
2470
|
-
return { explicit: true, y:
|
|
2530
|
+
return { explicit: true, y: simplifyCore(mul(sepResult.y, v(xVar))) };
|
|
2471
2531
|
}
|
|
2472
|
-
const implicitRelation =
|
|
2532
|
+
const implicitRelation = simplifyCore(subst(sepResult.implicitRelation as Expr, vName, div(v(yVar), v(xVar))));
|
|
2473
2533
|
return { explicit: false, implicitRelation };
|
|
2474
2534
|
}
|
|
2475
2535
|
|
|
@@ -2554,8 +2614,8 @@ function matchBernoulli(e: Expr, xVar: string, yVar: string): { p: Expr; q: Expr
|
|
|
2554
2614
|
return null;
|
|
2555
2615
|
}
|
|
2556
2616
|
if (powMatch.n === 0) return null; // y^0=1: really just linear, matchLinearOde's job
|
|
2557
|
-
const q = powSign === 1 ? powMatch.coef :
|
|
2558
|
-
const p = linSign === 1 ?
|
|
2617
|
+
const q = powSign === 1 ? powMatch.coef : simplifyCore(neg(powMatch.coef));
|
|
2618
|
+
const p = linSign === 1 ? simplifyCore(neg(linMatch.coef)) : linMatch.coef;
|
|
2559
2619
|
return { p, q, n: powMatch.n };
|
|
2560
2620
|
}
|
|
2561
2621
|
|
|
@@ -2582,10 +2642,10 @@ function solveBernoulliOde(
|
|
|
2582
2642
|
): OdeClosedFormResult {
|
|
2583
2643
|
const oneMinusN = 1 - bern.n;
|
|
2584
2644
|
const w0 = y0 ** oneMinusN;
|
|
2585
|
-
const transformedP =
|
|
2586
|
-
const transformedQ =
|
|
2645
|
+
const transformedP = simplifyCore(mul(num(oneMinusN), bern.p));
|
|
2646
|
+
const transformedQ = simplifyCore(mul(num(oneMinusN), bern.q));
|
|
2587
2647
|
const wResult = solveLinearOde({ p: transformedP, q: transformedQ }, xVar, yVar, x0, w0);
|
|
2588
|
-
const y =
|
|
2648
|
+
const y = simplifyCore(pow(wResult.y as Expr, num(1 / oneMinusN)));
|
|
2589
2649
|
return { explicit: true, y };
|
|
2590
2650
|
}
|
|
2591
2651
|
|
|
@@ -2607,7 +2667,7 @@ function matchExact(e: Expr, xVar: string, yVar: string): { M: Expr; N: Expr } |
|
|
|
2607
2667
|
const vars = collectFreeVars(e);
|
|
2608
2668
|
if (![...vars].every((n) => n === xVar || n === yVar)) return null;
|
|
2609
2669
|
const M = e.left;
|
|
2610
|
-
const N =
|
|
2670
|
+
const N = simplifyCore(neg(e.right));
|
|
2611
2671
|
const dMdy = diff(M, yVar);
|
|
2612
2672
|
const dNdx = diff(N, xVar);
|
|
2613
2673
|
let agreements = 0;
|
|
@@ -2662,8 +2722,8 @@ function solveExactOde(
|
|
|
2662
2722
|
let g: Expr;
|
|
2663
2723
|
let gPrime: Expr;
|
|
2664
2724
|
try {
|
|
2665
|
-
F0 =
|
|
2666
|
-
gPrime =
|
|
2725
|
+
F0 = simplifyCore(integ(M, xVar));
|
|
2726
|
+
gPrime = simplifyCore(sub(N, diff(F0, yVar)));
|
|
2667
2727
|
if (containsVar(gPrime, xVar)) {
|
|
2668
2728
|
// Symbolic cancellation fell short -- verify x-independence
|
|
2669
2729
|
// numerically, then pin x to a probe value that evaluates finitely.
|
|
@@ -2675,18 +2735,18 @@ function solveExactOde(
|
|
|
2675
2735
|
if (Math.abs(atProbe - atOther) > 1e-6 * Math.max(1, Math.abs(atProbe))) {
|
|
2676
2736
|
throw new NoClosedFormError();
|
|
2677
2737
|
}
|
|
2678
|
-
pinned =
|
|
2738
|
+
pinned = simplifyCore(subst(gPrime, xVar, num(xp)));
|
|
2679
2739
|
break;
|
|
2680
2740
|
}
|
|
2681
2741
|
if (!pinned) throw new NoClosedFormError();
|
|
2682
2742
|
gPrime = pinned;
|
|
2683
2743
|
}
|
|
2684
|
-
g =
|
|
2744
|
+
g = simplifyCore(integ(gPrime, yVar));
|
|
2685
2745
|
} catch (err) {
|
|
2686
2746
|
if (err instanceof NotIntegrableError) throw new NoClosedFormError();
|
|
2687
2747
|
throw err;
|
|
2688
2748
|
}
|
|
2689
|
-
const F =
|
|
2749
|
+
const F = simplifyCore(add(F0, g));
|
|
2690
2750
|
const C = evalExpr(F, { [xVar]: x0, [yVar]: y0 });
|
|
2691
2751
|
if (!Number.isFinite(C)) throw new NoClosedFormError();
|
|
2692
2752
|
// Cheap explicit attempt: F = H(y) + G(x) (either order) inverts via the
|
|
@@ -2701,12 +2761,12 @@ function solveExactOde(
|
|
|
2701
2761
|
] as const) {
|
|
2702
2762
|
if (!isPureFunctionOf(yPart, yOnly) || !isPureFunctionOf(xPartSigned, xOnly)) continue;
|
|
2703
2763
|
if (F.type === "sub" && yPart === b) continue; // F = G(x) - H(y): inverting -H is out of tryInvertForY's shapes
|
|
2704
|
-
const rhs =
|
|
2764
|
+
const rhs = simplifyCore(sub(num(C), xPartSigned));
|
|
2705
2765
|
const explicitY = tryInvertForY(yPart, yVar, rhs, y0);
|
|
2706
|
-
if (explicitY) return { explicit: true, y:
|
|
2766
|
+
if (explicitY) return { explicit: true, y: simplifyCore(explicitY) };
|
|
2707
2767
|
}
|
|
2708
2768
|
}
|
|
2709
|
-
return { explicit: false, implicitRelation:
|
|
2769
|
+
return { explicit: false, implicitRelation: simplifyCore(sub(F, num(C))) };
|
|
2710
2770
|
}
|
|
2711
2771
|
|
|
2712
2772
|
/** If `e` is `a·x + b` (a, b constant), return `{ a, b }`, else `null`. */
|
|
@@ -3604,12 +3664,47 @@ const CMP_LATEX_CMDS: Record<string, string> = {
|
|
|
3604
3664
|
};
|
|
3605
3665
|
|
|
3606
3666
|
/** Convert a LaTeX math string into the plain-infix syntax `Symbolic.parse`'s `Parser` understands. */
|
|
3607
|
-
function latexToInfix(latex: string): string {
|
|
3608
|
-
const cleaned = latex.replace(/\\left|\\right/g, "").replace(
|
|
3609
|
-
return transformLatex(cleaned);
|
|
3667
|
+
function latexToInfix(latex: string, opts: ParseOptions = {}): string {
|
|
3668
|
+
const cleaned = latex.replace(/\\left|\\right/g, "").replace(/(?<!\\)\\(?:,|;|!|quad|qquad| )/g, "");
|
|
3669
|
+
return transformLatex(cleaned, opts);
|
|
3610
3670
|
}
|
|
3611
3671
|
|
|
3612
|
-
|
|
3672
|
+
/**
|
|
3673
|
+
* Where the unbracketed argument of a function command ends under implicit multiplication: the longest
|
|
3674
|
+
* product of numbers, letters, `\\pi`, powers, subscripts and bracket groups starting at `start`
|
|
3675
|
+
* (`\\sin 2x` takes `2x`; `\\sin x\\cos x` takes just `x`). Returns `start` when there is none.
|
|
3676
|
+
*/
|
|
3677
|
+
function bareArgumentEnd(s: string, start: number): number {
|
|
3678
|
+
let j = start;
|
|
3679
|
+
const group = (at: number): number => {
|
|
3680
|
+
const open = s[at];
|
|
3681
|
+
return findGroupEnd(s, at, open, open === "{" ? "}" : ")") + 1;
|
|
3682
|
+
};
|
|
3683
|
+
while (j < s.length) {
|
|
3684
|
+
const c = s[j];
|
|
3685
|
+
if (/[A-Za-z0-9.]/.test(c)) {
|
|
3686
|
+
j++;
|
|
3687
|
+
} else if (c === "(" || c === "{") {
|
|
3688
|
+
j = group(j);
|
|
3689
|
+
} else if (c === "_" && s[j + 1] === "{") {
|
|
3690
|
+
j = group(j + 1);
|
|
3691
|
+
} else if (c === "^") {
|
|
3692
|
+
if (s[j + 1] === "{") j = group(j + 1);
|
|
3693
|
+
else if (s[j + 1] === "\\") j += 1 + (/^\\[a-zA-Z]+/.exec(s.slice(j + 1))?.[0].length ?? 1);
|
|
3694
|
+
else j += 2;
|
|
3695
|
+
} else if (/^\\pi(?![a-zA-Z])/.test(s.slice(j))) {
|
|
3696
|
+
j += 3;
|
|
3697
|
+
} else if (c === " " && /^ +(?:[A-Za-z0-9.({]|\\pi(?![a-zA-Z]))/.test(s.slice(j))) {
|
|
3698
|
+
j += s.slice(j).length - s.slice(j).trimStart().length;
|
|
3699
|
+
} else {
|
|
3700
|
+
break;
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
return j;
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
function transformLatex(s: string, opts: ParseOptions = {}): string {
|
|
3707
|
+
const implicit = opts.implicitMultiplication === true;
|
|
3613
3708
|
let out = "";
|
|
3614
3709
|
let i = 0;
|
|
3615
3710
|
|
|
@@ -3621,11 +3716,20 @@ function transformLatex(s: string): string {
|
|
|
3621
3716
|
};
|
|
3622
3717
|
|
|
3623
3718
|
const readGroupOrParenOrToken = (): string => {
|
|
3719
|
+
while (s[i] === " ") i++;
|
|
3624
3720
|
if (s[i] === "{" || s[i] === "(") {
|
|
3625
3721
|
const { content, next } = extractGroup(s, i);
|
|
3626
3722
|
i = next;
|
|
3627
3723
|
return content;
|
|
3628
3724
|
}
|
|
3725
|
+
if (implicit) {
|
|
3726
|
+
const end = bareArgumentEnd(s, i);
|
|
3727
|
+
if (end > i) {
|
|
3728
|
+
const run = s.slice(i, end).trim();
|
|
3729
|
+
i = end;
|
|
3730
|
+
return run;
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3629
3733
|
const m = /^[a-zA-Z_]\w*|^\d+\.?\d*(?:[eE][+-]?\d+)?/.exec(s.slice(i));
|
|
3630
3734
|
if (m) {
|
|
3631
3735
|
i += m[0].length;
|
|
@@ -3634,6 +3738,32 @@ function transformLatex(s: string): string {
|
|
|
3634
3738
|
throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
|
|
3635
3739
|
};
|
|
3636
3740
|
|
|
3741
|
+
/** A `{...}` group, or the one token TeX takes without braces: a digit, a letter or a `\\command`. */
|
|
3742
|
+
const readGroupOrToken = (): string => {
|
|
3743
|
+
while (s[i] === " ") i++;
|
|
3744
|
+
if (s[i] === "{") return readGroup();
|
|
3745
|
+
const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i));
|
|
3746
|
+
if (!m) throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
|
|
3747
|
+
i += m[0].length;
|
|
3748
|
+
return m[0];
|
|
3749
|
+
};
|
|
3750
|
+
|
|
3751
|
+
/** A power written on a function command (`\\sin^{2}`, `\\sin^2`), or null. */
|
|
3752
|
+
const readFunctionPower = (): string | null => {
|
|
3753
|
+
if (s[i] !== "^") return null;
|
|
3754
|
+
i++;
|
|
3755
|
+
const power = readGroupOrToken();
|
|
3756
|
+
// `\sin^{-1}` is arcsin in most textbooks and calculators but 1/sin in strict TeX/notation; guessing
|
|
3757
|
+
// wrong would silently change the maths, so it is refused rather than interpreted.
|
|
3758
|
+
if (/^\s*-\s*1\s*$/.test(power)) {
|
|
3759
|
+
throw new Error(
|
|
3760
|
+
"Ambiguous LaTeX: a function with exponent -1 (e.g. \\sin^{-1}) may mean the inverse function or the reciprocal; " +
|
|
3761
|
+
"write \\arcsin(x) for the inverse, or (\\sin x)^{-1} / 1/\\sin x for the reciprocal",
|
|
3762
|
+
);
|
|
3763
|
+
}
|
|
3764
|
+
return power;
|
|
3765
|
+
};
|
|
3766
|
+
|
|
3637
3767
|
/** Read a `(...)`/`{...}` group holding exactly 2 comma-separated arguments, for `call2` functions. */
|
|
3638
3768
|
const readArgPair = (): [string, string] => {
|
|
3639
3769
|
if (s[i] !== "(" && s[i] !== "{") throw new Error(`Expected '(' at position ${i} in LaTeX source: ${s}`);
|
|
@@ -3683,9 +3813,10 @@ function transformLatex(s: string): string {
|
|
|
3683
3813
|
const [exprPart, condPart] = cols;
|
|
3684
3814
|
if (condPart.trim() === "\\text{otherwise}") {
|
|
3685
3815
|
if (otherwise !== null) throw new Error("\\begin{cases} may have only one \\text{otherwise} row");
|
|
3686
|
-
otherwise = transformLatex(exprPart);
|
|
3816
|
+
otherwise = transformLatex(exprPart, opts);
|
|
3687
3817
|
} else {
|
|
3688
|
-
|
|
3818
|
+
const condition = condPart.replace(/^\s*\\text\{\s*(?:if|when)\s*\}/, ""); // "x & \text{if } x<0" as MathLive writes it
|
|
3819
|
+
parts.push(`${transformLatex(condition, opts)}, ${transformLatex(exprPart, opts)}`);
|
|
3689
3820
|
}
|
|
3690
3821
|
}
|
|
3691
3822
|
if (otherwise === null) {
|
|
@@ -3694,28 +3825,28 @@ function transformLatex(s: string): string {
|
|
|
3694
3825
|
out += `piecewise(${parts.concat(otherwise).join(", ")})`;
|
|
3695
3826
|
continue;
|
|
3696
3827
|
}
|
|
3697
|
-
if (cmd === "\\frac") {
|
|
3698
|
-
const numerator =
|
|
3699
|
-
const denominator =
|
|
3700
|
-
out += `((${transformLatex(numerator)})/(${transformLatex(denominator)}))`;
|
|
3828
|
+
if (cmd === "\\frac" || cmd === "\\dfrac" || cmd === "\\tfrac") {
|
|
3829
|
+
const numerator = readGroupOrToken();
|
|
3830
|
+
const denominator = readGroupOrToken();
|
|
3831
|
+
out += `((${transformLatex(numerator, opts)})/(${transformLatex(denominator, opts)}))`;
|
|
3701
3832
|
continue;
|
|
3702
3833
|
}
|
|
3703
3834
|
if (cmd === "\\sqrt") {
|
|
3704
3835
|
if (s[i] === "[") {
|
|
3705
3836
|
const { content: root, next } = extractGroup(s, i);
|
|
3706
3837
|
i = next;
|
|
3707
|
-
const arg =
|
|
3708
|
-
const transformedRoot = transformLatex(root).trim();
|
|
3838
|
+
const arg = readGroupOrToken();
|
|
3839
|
+
const transformedRoot = transformLatex(root, opts).trim();
|
|
3709
3840
|
// \sqrt[3]{...} maps to cbrt(...) rather than a fractional power so
|
|
3710
3841
|
// negative arguments evaluate correctly (Math.cbrt(-8) = -2, but
|
|
3711
3842
|
// (-8)^(1/3) is NaN under JS's `**`).
|
|
3712
3843
|
out +=
|
|
3713
3844
|
transformedRoot === "3"
|
|
3714
|
-
? `cbrt(${transformLatex(arg)})`
|
|
3715
|
-
: `((${transformLatex(arg)})^(1/(${transformedRoot})))`;
|
|
3845
|
+
? `cbrt(${transformLatex(arg, opts)})`
|
|
3846
|
+
: `((${transformLatex(arg, opts)})^(1/(${transformedRoot})))`;
|
|
3716
3847
|
} else {
|
|
3717
|
-
const arg =
|
|
3718
|
-
out += `sqrt(${transformLatex(arg)})`;
|
|
3848
|
+
const arg = readGroupOrToken();
|
|
3849
|
+
out += `sqrt(${transformLatex(arg, opts)})`;
|
|
3719
3850
|
}
|
|
3720
3851
|
continue;
|
|
3721
3852
|
}
|
|
@@ -3729,8 +3860,10 @@ function transformLatex(s: string): string {
|
|
|
3729
3860
|
if (base !== "10" && base !== "2") {
|
|
3730
3861
|
throw new Error(`Unsupported LaTeX construct: \\log base ${base} (only base 10 and base 2 are supported)`);
|
|
3731
3862
|
}
|
|
3863
|
+
const power = readFunctionPower();
|
|
3732
3864
|
const arg = readGroupOrParenOrToken();
|
|
3733
|
-
|
|
3865
|
+
const call = `${base === "2" ? "log2" : "log10"}(${transformLatex(arg, opts)})`;
|
|
3866
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3734
3867
|
continue;
|
|
3735
3868
|
}
|
|
3736
3869
|
if (cmd === "\\lfloor" || cmd === "\\lceil") {
|
|
@@ -3739,7 +3872,7 @@ function transformLatex(s: string): string {
|
|
|
3739
3872
|
if (closeIdx === -1) throw new Error(`Unmatched '${cmd}' in LaTeX source`);
|
|
3740
3873
|
const inner = s.slice(i, closeIdx);
|
|
3741
3874
|
i = closeIdx + closeCmd.length;
|
|
3742
|
-
out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner)})`;
|
|
3875
|
+
out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner, opts)})`;
|
|
3743
3876
|
continue;
|
|
3744
3877
|
}
|
|
3745
3878
|
if (cmd === "\\operatorname") {
|
|
@@ -3747,25 +3880,29 @@ function transformLatex(s: string): string {
|
|
|
3747
3880
|
const binaryName = OPERATORNAME_BINARY_FUNC_NAMES[opName];
|
|
3748
3881
|
if (binaryName) {
|
|
3749
3882
|
const [left, right] = readArgPair();
|
|
3750
|
-
out += `${binaryName}(${transformLatex(left)}, ${transformLatex(right)})`;
|
|
3883
|
+
out += `${binaryName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
|
|
3751
3884
|
continue;
|
|
3752
3885
|
}
|
|
3753
3886
|
const funcName = OPERATORNAME_FUNC_NAMES[opName];
|
|
3754
3887
|
if (!funcName) throw new Error(`Unsupported LaTeX construct: \\operatorname{${opName}}`);
|
|
3888
|
+
const power = readFunctionPower();
|
|
3755
3889
|
const arg = readGroupOrParenOrToken();
|
|
3756
|
-
|
|
3890
|
+
const call = `${funcName}(${transformLatex(arg, opts)})`;
|
|
3891
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3757
3892
|
continue;
|
|
3758
3893
|
}
|
|
3759
3894
|
const binaryFuncName = LATEX_BINARY_FUNC_NAMES[cmd];
|
|
3760
3895
|
if (binaryFuncName) {
|
|
3761
3896
|
const [left, right] = readArgPair();
|
|
3762
|
-
out += `${binaryFuncName}(${transformLatex(left)}, ${transformLatex(right)})`;
|
|
3897
|
+
out += `${binaryFuncName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
|
|
3763
3898
|
continue;
|
|
3764
3899
|
}
|
|
3765
3900
|
const funcName = LATEX_FUNC_NAMES[cmd];
|
|
3766
3901
|
if (funcName) {
|
|
3902
|
+
const power = readFunctionPower();
|
|
3767
3903
|
const arg = readGroupOrParenOrToken();
|
|
3768
|
-
|
|
3904
|
+
const call = `${funcName}(${transformLatex(arg, opts)})`;
|
|
3905
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3769
3906
|
continue;
|
|
3770
3907
|
}
|
|
3771
3908
|
throw new Error(`Unsupported LaTeX construct: ${cmd}`);
|
|
@@ -3775,13 +3912,20 @@ function transformLatex(s: string): string {
|
|
|
3775
3912
|
if (closeIdx === -1) throw new Error("Unmatched '|' in LaTeX source");
|
|
3776
3913
|
const inner = s.slice(i + 1, closeIdx);
|
|
3777
3914
|
i = closeIdx + 1;
|
|
3778
|
-
out += `abs(${transformLatex(inner)})`;
|
|
3915
|
+
out += `abs(${transformLatex(inner, opts)})`;
|
|
3916
|
+
continue;
|
|
3917
|
+
}
|
|
3918
|
+
if (implicit && ch === "^" && /^[a-zA-Z0-9]|^\\[a-zA-Z]/.test(s.slice(i + 1))) {
|
|
3919
|
+
// TeX reads `x^23` as `x^2` times `3`: the exponent is one token
|
|
3920
|
+
const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i + 1)) as RegExpExecArray;
|
|
3921
|
+
i += 1 + m[0].length;
|
|
3922
|
+
out += `^(${transformLatex(m[0], opts)})`;
|
|
3779
3923
|
continue;
|
|
3780
3924
|
}
|
|
3781
3925
|
if (ch === "^" && s[i + 1] === "{") {
|
|
3782
3926
|
const { content, next } = extractGroup(s, i + 1);
|
|
3783
3927
|
i = next;
|
|
3784
|
-
out += `^(${transformLatex(content)})`;
|
|
3928
|
+
out += `^(${transformLatex(content, opts)})`;
|
|
3785
3929
|
continue;
|
|
3786
3930
|
}
|
|
3787
3931
|
if (ch === "_" && s[i + 1] === "{") {
|
|
@@ -3793,7 +3937,7 @@ function transformLatex(s: string): string {
|
|
|
3793
3937
|
if (ch === "{") {
|
|
3794
3938
|
const { content, next } = extractGroup(s, i);
|
|
3795
3939
|
i = next;
|
|
3796
|
-
out += `(${transformLatex(content)})`;
|
|
3940
|
+
out += `(${transformLatex(content, opts)})`;
|
|
3797
3941
|
continue;
|
|
3798
3942
|
}
|
|
3799
3943
|
if (ch === "=") {
|
|
@@ -3810,6 +3954,398 @@ function transformLatex(s: string): string {
|
|
|
3810
3954
|
return out;
|
|
3811
3955
|
}
|
|
3812
3956
|
|
|
3957
|
+
// -- rational-function normalization (expand + cancel) ----------------------------
|
|
3958
|
+
// `simplify` finishes by trying to read the expression as a quotient of two
|
|
3959
|
+
// polynomials (over the rationals, in "atoms": variables and any subexpression that
|
|
3960
|
+
// is not itself polynomial, such as `sin(x)`), expanding both, cancelling their
|
|
3961
|
+
// greatest common divisor, and printing the result back out. The result is used only
|
|
3962
|
+
// when it is strictly smaller than what `simplify` had already reached, so products
|
|
3963
|
+
// like `(x+1)*2` that are no smaller expanded keep their form.
|
|
3964
|
+
|
|
3965
|
+
/** A monomial's exponent per atom, and its coefficient. */
|
|
3966
|
+
interface PolyTerm {
|
|
3967
|
+
exps: number[];
|
|
3968
|
+
c: Rational;
|
|
3969
|
+
}
|
|
3970
|
+
type Poly = PolyTerm[]; // sorted by lex order descending, no zero coefficients, no duplicate monomials
|
|
3971
|
+
|
|
3972
|
+
const MAX_POLY_TERMS = 400;
|
|
3973
|
+
const MAX_RATIONAL_POWER = 8;
|
|
3974
|
+
|
|
3975
|
+
class RatBail extends Error {}
|
|
3976
|
+
|
|
3977
|
+
interface RatCtx {
|
|
3978
|
+
atoms: Expr[];
|
|
3979
|
+
keys: string[];
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
function atomIndex(ctx: RatCtx, e: Expr): number {
|
|
3983
|
+
const key = render(e, 0);
|
|
3984
|
+
let i = ctx.keys.indexOf(key);
|
|
3985
|
+
if (i < 0) {
|
|
3986
|
+
i = ctx.keys.length;
|
|
3987
|
+
ctx.keys.push(key);
|
|
3988
|
+
ctx.atoms.push(e);
|
|
3989
|
+
}
|
|
3990
|
+
return i;
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
function expsCmp(a: number[], b: number[]): number {
|
|
3994
|
+
const n = Math.max(a.length, b.length);
|
|
3995
|
+
for (let i = 0; i < n; i++) {
|
|
3996
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
3997
|
+
if (d !== 0) return d;
|
|
3998
|
+
}
|
|
3999
|
+
return 0;
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
function expsKey(a: number[]): string {
|
|
4003
|
+
let end = a.length;
|
|
4004
|
+
while (end > 0 && !a[end - 1]) end--;
|
|
4005
|
+
return a.slice(0, end).join(",");
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
function polyNormalize(terms: PolyTerm[]): Poly {
|
|
4009
|
+
const byKey = new Map<string, PolyTerm>();
|
|
4010
|
+
for (const t of terms) {
|
|
4011
|
+
const k = expsKey(t.exps);
|
|
4012
|
+
const prev = byKey.get(k);
|
|
4013
|
+
if (prev) byKey.set(k, { exps: prev.exps, c: prev.c.add(t.c) });
|
|
4014
|
+
else byKey.set(k, { exps: t.exps, c: t.c });
|
|
4015
|
+
}
|
|
4016
|
+
const out = [...byKey.values()].filter((t) => !t.c.isZero());
|
|
4017
|
+
out.sort((a, b) => expsCmp(b.exps, a.exps));
|
|
4018
|
+
if (out.length > MAX_POLY_TERMS) throw new RatBail();
|
|
4019
|
+
return out;
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
const polyConst = (c: Rational): Poly => (c.isZero() ? [] : [{ exps: [], c }]);
|
|
4023
|
+
const polyAdd = (a: Poly, b: Poly): Poly => polyNormalize([...a, ...b]);
|
|
4024
|
+
const polyNeg = (a: Poly): Poly => a.map((t) => ({ exps: t.exps, c: t.c.negate() }));
|
|
4025
|
+
const polySub = (a: Poly, b: Poly): Poly => polyAdd(a, polyNeg(b));
|
|
4026
|
+
|
|
4027
|
+
function polyMul(a: Poly, b: Poly): Poly {
|
|
4028
|
+
if (a.length * b.length > MAX_POLY_TERMS * 20) throw new RatBail();
|
|
4029
|
+
const out: PolyTerm[] = [];
|
|
4030
|
+
for (const x of a) {
|
|
4031
|
+
for (const y of b) {
|
|
4032
|
+
const n = Math.max(x.exps.length, y.exps.length);
|
|
4033
|
+
const exps: number[] = [];
|
|
4034
|
+
for (let i = 0; i < n; i++) exps.push((x.exps[i] ?? 0) + (y.exps[i] ?? 0));
|
|
4035
|
+
out.push({ exps, c: x.c.multiply(y.c) });
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
return polyNormalize(out);
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
function polyEqual(a: Poly, b: Poly): boolean {
|
|
4042
|
+
return a.length === b.length && a.every((t, i) => expsKey(t.exps) === expsKey(b[i].exps) && t.c.equals(b[i].c));
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
const polyIsConst = (a: Poly): boolean => a.length === 0 || (a.length === 1 && expsKey(a[0].exps) === "");
|
|
4046
|
+
|
|
4047
|
+
/** Division in lex order: `a = q*b + r` (`b` must be nonzero). */
|
|
4048
|
+
function polyDivMod(a: Poly, b: Poly): { q: Poly; r: Poly } {
|
|
4049
|
+
const lead = b[0];
|
|
4050
|
+
let rem = a;
|
|
4051
|
+
const q: PolyTerm[] = [];
|
|
4052
|
+
const r: PolyTerm[] = [];
|
|
4053
|
+
let guard = 0;
|
|
4054
|
+
while (rem.length > 0) {
|
|
4055
|
+
if (++guard > 20_000) throw new RatBail();
|
|
4056
|
+
const t = rem[0];
|
|
4057
|
+
const n = Math.max(t.exps.length, lead.exps.length);
|
|
4058
|
+
const exps: number[] = [];
|
|
4059
|
+
let divisible = true;
|
|
4060
|
+
for (let i = 0; i < n; i++) {
|
|
4061
|
+
const d = (t.exps[i] ?? 0) - (lead.exps[i] ?? 0);
|
|
4062
|
+
if (d < 0) divisible = false;
|
|
4063
|
+
exps.push(d);
|
|
4064
|
+
}
|
|
4065
|
+
if (!divisible) {
|
|
4066
|
+
r.push(t);
|
|
4067
|
+
rem = rem.slice(1);
|
|
4068
|
+
continue;
|
|
4069
|
+
}
|
|
4070
|
+
const qt: PolyTerm = { exps, c: t.c.divide(lead.c) };
|
|
4071
|
+
q.push(qt);
|
|
4072
|
+
rem = polySub(rem, polyMul([qt], b));
|
|
4073
|
+
}
|
|
4074
|
+
return { q: polyNormalize(q), r: polyNormalize(r) };
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
function polyAtoms(...ps: Poly[]): Set<number> {
|
|
4078
|
+
const s = new Set<number>();
|
|
4079
|
+
for (const p of ps) {
|
|
4080
|
+
for (const t of p) {
|
|
4081
|
+
for (let i = 0; i < t.exps.length; i++) if (t.exps[i] > 0) s.add(i);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
return s;
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
/** Monic GCD of two polynomials in one atom (Euclid); 1 when several atoms are involved. */
|
|
4088
|
+
function polyGcd(a: Poly, b: Poly): Poly {
|
|
4089
|
+
if (polyAtoms(a, b).size > 1) return polyConst(Rational.One);
|
|
4090
|
+
let x = a;
|
|
4091
|
+
let y = b;
|
|
4092
|
+
while (y.length > 0) {
|
|
4093
|
+
const { r } = polyDivMod(x, y);
|
|
4094
|
+
x = y;
|
|
4095
|
+
y = r;
|
|
4096
|
+
}
|
|
4097
|
+
if (x.length === 0) return polyConst(Rational.One);
|
|
4098
|
+
const lc = x[0].c;
|
|
4099
|
+
return x.map((t) => ({ exps: t.exps, c: t.c.divide(lc) }));
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
/** The largest monomial dividing every term (exponent-wise minimum). */
|
|
4103
|
+
function monomialContent(p: Poly): number[] {
|
|
4104
|
+
if (p.length === 0) return [];
|
|
4105
|
+
const n = Math.max(...p.map((t) => t.exps.length));
|
|
4106
|
+
const out: number[] = [];
|
|
4107
|
+
for (let i = 0; i < n; i++) out.push(Math.min(...p.map((t) => t.exps[i] ?? 0)));
|
|
4108
|
+
return out;
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
function polyDivMonomial(p: Poly, m: number[]): Poly {
|
|
4112
|
+
return p.map((t) => ({ exps: t.exps.map((e, i) => e - (m[i] ?? 0)), c: t.c }));
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
interface Ratio {
|
|
4116
|
+
n: Poly;
|
|
4117
|
+
d: Poly;
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
const ratioFromPoly = (n: Poly): Ratio => ({ n, d: polyConst(Rational.One) });
|
|
4121
|
+
|
|
4122
|
+
function ratioAdd(a: Ratio, b: Ratio, subtract: boolean): Ratio {
|
|
4123
|
+
const bn = subtract ? polyNeg(b.n) : b.n;
|
|
4124
|
+
if (polyEqual(a.d, b.d)) return { n: polyAdd(a.n, bn), d: a.d };
|
|
4125
|
+
return { n: polyAdd(polyMul(a.n, b.d), polyMul(bn, a.d)), d: polyMul(a.d, b.d) };
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
const ratioMul = (a: Ratio, b: Ratio): Ratio => ({ n: polyMul(a.n, b.n), d: polyMul(a.d, b.d) });
|
|
4129
|
+
|
|
4130
|
+
function ratioDiv(a: Ratio, b: Ratio): Ratio {
|
|
4131
|
+
if (b.n.length === 0) throw new RatBail(); // division by zero: leave it to evaluation
|
|
4132
|
+
return { n: polyMul(a.n, b.d), d: polyMul(a.d, b.n) };
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
function ratioPow(a: Ratio, n: number): Ratio {
|
|
4136
|
+
const k = Math.abs(n);
|
|
4137
|
+
if (k > 1000) throw new RatBail();
|
|
4138
|
+
const singleTerm = a.n.length === 1 && a.d.length === 1;
|
|
4139
|
+
if (!singleTerm && k > MAX_RATIONAL_POWER) throw new RatBail();
|
|
4140
|
+
let result: Ratio;
|
|
4141
|
+
if (singleTerm) {
|
|
4142
|
+
const mono = (t: PolyTerm): PolyTerm => ({ exps: t.exps.map((e) => e * k), c: t.c.pow(k) });
|
|
4143
|
+
result = { n: [mono(a.n[0])], d: [mono(a.d[0])] };
|
|
4144
|
+
} else {
|
|
4145
|
+
result = ratioFromPoly(polyConst(Rational.One));
|
|
4146
|
+
for (let i = 0; i < k; i++) result = ratioMul(result, a);
|
|
4147
|
+
}
|
|
4148
|
+
return n < 0 ? ratioDiv(ratioFromPoly(polyConst(Rational.One)), result) : result;
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
function exactRational(value: number): Rational | null {
|
|
4152
|
+
if (!Number.isFinite(value)) return null;
|
|
4153
|
+
if (Number.isInteger(value)) return new Rational(BigInt(value));
|
|
4154
|
+
const r = Rational.fromNumber(value, 10_000);
|
|
4155
|
+
return r.toNumber() === value ? r : null;
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
function toRatio(e: Expr, ctx: RatCtx): Ratio {
|
|
4159
|
+
const atom = (x: Expr): Ratio => {
|
|
4160
|
+
const i = atomIndex(ctx, x);
|
|
4161
|
+
const exps: number[] = new Array(i + 1).fill(0);
|
|
4162
|
+
exps[i] = 1;
|
|
4163
|
+
return ratioFromPoly([{ exps, c: Rational.One }]);
|
|
4164
|
+
};
|
|
4165
|
+
switch (e.type) {
|
|
4166
|
+
case "const": {
|
|
4167
|
+
const r = exactRational(e.value);
|
|
4168
|
+
return r ? ratioFromPoly(polyConst(r)) : atom(e);
|
|
4169
|
+
}
|
|
4170
|
+
case "var":
|
|
4171
|
+
return atom(e);
|
|
4172
|
+
case "neg": {
|
|
4173
|
+
const a = toRatio(e.arg, ctx);
|
|
4174
|
+
return { n: polyNeg(a.n), d: a.d };
|
|
4175
|
+
}
|
|
4176
|
+
case "add":
|
|
4177
|
+
return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), false);
|
|
4178
|
+
case "sub":
|
|
4179
|
+
return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), true);
|
|
4180
|
+
case "mul":
|
|
4181
|
+
return ratioMul(toRatio(e.left, ctx), toRatio(e.right, ctx));
|
|
4182
|
+
case "div":
|
|
4183
|
+
return ratioDiv(toRatio(e.left, ctx), toRatio(e.right, ctx));
|
|
4184
|
+
case "pow": {
|
|
4185
|
+
if (e.exp.type === "const" && Number.isInteger(e.exp.value)) {
|
|
4186
|
+
return ratioPow(toRatio(e.base, ctx), e.exp.value);
|
|
4187
|
+
}
|
|
4188
|
+
return atom(pow(normalizeRational(e.base), normalizeRational(e.exp)));
|
|
4189
|
+
}
|
|
4190
|
+
case "func":
|
|
4191
|
+
return atom(fn(e.name, normalizeRational(e.arg)));
|
|
4192
|
+
case "call2":
|
|
4193
|
+
return atom(call2(e.name, normalizeRational(e.left), normalizeRational(e.right)));
|
|
4194
|
+
case "cmp":
|
|
4195
|
+
return atom(cmp(e.op, normalizeRational(e.left), normalizeRational(e.right)));
|
|
4196
|
+
case "piecewise":
|
|
4197
|
+
return atom(
|
|
4198
|
+
piecewise(
|
|
4199
|
+
e.branches.map((br) => ({ cond: normalizeRational(br.cond), expr: normalizeRational(br.expr) })),
|
|
4200
|
+
normalizeRational(e.otherwise),
|
|
4201
|
+
),
|
|
4202
|
+
);
|
|
4203
|
+
case "sum":
|
|
4204
|
+
return atom(sumExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)));
|
|
4205
|
+
case "product":
|
|
4206
|
+
return atom(
|
|
4207
|
+
productExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)),
|
|
4208
|
+
);
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
function bigGcd(a: bigint, b: bigint): bigint {
|
|
4213
|
+
let x = a < 0n ? -a : a;
|
|
4214
|
+
let y = b < 0n ? -b : b;
|
|
4215
|
+
while (y) [x, y] = [y, x % y];
|
|
4216
|
+
return x;
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
/** Cancel common factors, then scale to coprime integer coefficients with a positive denominator lead. */
|
|
4220
|
+
function ratioReduce(r: Ratio): Ratio {
|
|
4221
|
+
if (r.n.length === 0) return ratioFromPoly([]);
|
|
4222
|
+
let { n, d } = r;
|
|
4223
|
+
const m = monomialContent([...n, ...d]);
|
|
4224
|
+
if (m.some((e) => e > 0)) {
|
|
4225
|
+
n = polyDivMonomial(n, m);
|
|
4226
|
+
d = polyDivMonomial(d, m);
|
|
4227
|
+
}
|
|
4228
|
+
if (!polyIsConst(d)) {
|
|
4229
|
+
const g = polyGcd(n, d);
|
|
4230
|
+
if (!polyIsConst(g)) {
|
|
4231
|
+
n = polyDivMod(n, g).q;
|
|
4232
|
+
d = polyDivMod(d, g).q;
|
|
4233
|
+
} else {
|
|
4234
|
+
// Several atoms: still cancel when one side divides the other exactly.
|
|
4235
|
+
const dn = polyDivMod(n, d);
|
|
4236
|
+
if (dn.r.length === 0) {
|
|
4237
|
+
n = dn.q;
|
|
4238
|
+
d = polyConst(Rational.One);
|
|
4239
|
+
} else {
|
|
4240
|
+
const nd = polyDivMod(d, n);
|
|
4241
|
+
if (nd.r.length === 0) {
|
|
4242
|
+
d = nd.q;
|
|
4243
|
+
n = polyConst(Rational.One);
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
if (polyIsConst(d)) {
|
|
4249
|
+
const c = d[0].c;
|
|
4250
|
+
return { n: n.map((t) => ({ exps: t.exps, c: t.c.divide(c) })), d: polyConst(Rational.One) };
|
|
4251
|
+
}
|
|
4252
|
+
// integer coefficients: multiply through by the lcm of the denominators, divide by the content
|
|
4253
|
+
let lcm = 1n;
|
|
4254
|
+
for (const t of [...n, ...d]) {
|
|
4255
|
+
const den = t.c.denominator;
|
|
4256
|
+
lcm = (lcm / bigGcd(lcm, den)) * den;
|
|
4257
|
+
}
|
|
4258
|
+
const scale = new Rational(lcm);
|
|
4259
|
+
n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
|
|
4260
|
+
d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
|
|
4261
|
+
let content = 0n;
|
|
4262
|
+
for (const t of [...n, ...d]) content = bigGcd(content, t.c.numerator);
|
|
4263
|
+
if (content > 1n) {
|
|
4264
|
+
const inv = new Rational(1n, content);
|
|
4265
|
+
n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
|
|
4266
|
+
d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
|
|
4267
|
+
}
|
|
4268
|
+
if (d[0].c.sign < 0) {
|
|
4269
|
+
n = polyNeg(n);
|
|
4270
|
+
d = polyNeg(d);
|
|
4271
|
+
}
|
|
4272
|
+
return { n, d };
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
function monomialExpr(exps: number[], ctx: RatCtx): Expr | null {
|
|
4276
|
+
let out: Expr | null = null;
|
|
4277
|
+
for (let i = 0; i < exps.length; i++) {
|
|
4278
|
+
const e = exps[i] ?? 0;
|
|
4279
|
+
if (e === 0) continue;
|
|
4280
|
+
const factor = e === 1 ? ctx.atoms[i] : pow(ctx.atoms[i], num(e));
|
|
4281
|
+
out = out ? mul(out, factor) : factor;
|
|
4282
|
+
}
|
|
4283
|
+
return out;
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4286
|
+
/** A polynomial as a descending-degree sum; fractional coefficients print as `p*m/q`. */
|
|
4287
|
+
function polyToTree(p: Poly, ctx: RatCtx): Expr {
|
|
4288
|
+
if (p.length === 0) return num(0);
|
|
4289
|
+
const ordered = [...p].sort((a, b) => {
|
|
4290
|
+
const da = a.exps.reduce((s, x) => s + x, 0);
|
|
4291
|
+
const db = b.exps.reduce((s, x) => s + x, 0);
|
|
4292
|
+
return db - da || expsCmp(b.exps, a.exps);
|
|
4293
|
+
});
|
|
4294
|
+
let out: Expr | null = null;
|
|
4295
|
+
for (const t of ordered) {
|
|
4296
|
+
const negative = t.c.sign < 0;
|
|
4297
|
+
const abs = t.c.abs();
|
|
4298
|
+
const mono = monomialExpr(t.exps, ctx);
|
|
4299
|
+
const numer = abs.numerator;
|
|
4300
|
+
let body: Expr;
|
|
4301
|
+
if (!mono) body = num(abs.toNumber());
|
|
4302
|
+
else if (numer === 1n && abs.denominator === 1n) body = mono;
|
|
4303
|
+
else if (abs.denominator === 1n) body = mul(num(Number(numer)), mono);
|
|
4304
|
+
else body = div(numer === 1n ? mono : mul(num(Number(numer)), mono), num(Number(abs.denominator)));
|
|
4305
|
+
if (!out) out = negative ? neg(body) : body;
|
|
4306
|
+
else out = negative ? sub(out, body) : add(out, body);
|
|
4307
|
+
}
|
|
4308
|
+
return out as Expr;
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
function nodeCount(e: Expr): number {
|
|
4312
|
+
switch (e.type) {
|
|
4313
|
+
case "const":
|
|
4314
|
+
case "var":
|
|
4315
|
+
return 1;
|
|
4316
|
+
case "neg":
|
|
4317
|
+
case "func":
|
|
4318
|
+
return 1 + nodeCount(e.arg);
|
|
4319
|
+
case "pow":
|
|
4320
|
+
return 1 + nodeCount(e.base) + nodeCount(e.exp);
|
|
4321
|
+
case "piecewise":
|
|
4322
|
+
return 1 + nodeCount(e.otherwise) + e.branches.reduce((s, br) => s + nodeCount(br.cond) + nodeCount(br.expr), 0);
|
|
4323
|
+
case "sum":
|
|
4324
|
+
case "product":
|
|
4325
|
+
return 1 + nodeCount(e.from) + nodeCount(e.to) + nodeCount(e.body);
|
|
4326
|
+
default:
|
|
4327
|
+
return 1 + nodeCount(e.left) + nodeCount(e.right);
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
/**
|
|
4332
|
+
* Expand `e` as a rational function and cancel common factors, returning the result only
|
|
4333
|
+
* when it has strictly fewer nodes than `e`; otherwise returns `e` itself.
|
|
4334
|
+
*/
|
|
4335
|
+
function normalizeRational(e: Expr): Expr {
|
|
4336
|
+
if (e.type === "const" || e.type === "var") return e;
|
|
4337
|
+
try {
|
|
4338
|
+
const ctx: RatCtx = { atoms: [], keys: [] };
|
|
4339
|
+
const reduced = ratioReduce(toRatio(e, ctx));
|
|
4340
|
+
const numer = polyToTree(reduced.n, ctx);
|
|
4341
|
+
const candidate = polyIsConst(reduced.d) ? numer : div(numer, polyToTree(reduced.d, ctx));
|
|
4342
|
+
return nodeCount(candidate) < nodeCount(e) ? candidate : e;
|
|
4343
|
+
} catch (err) {
|
|
4344
|
+
if (err instanceof RatBail) return e;
|
|
4345
|
+
throw err;
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
|
|
3813
4349
|
// -- equation solving & factoring ----------------------------------------------
|
|
3814
4350
|
/** Best-effort exact `Expr` for a numeric value: an integer, a reduced fraction, or (as a last resort) the raw float. */
|
|
3815
4351
|
function toExactExpr(value: number): Expr {
|
|
@@ -3841,7 +4377,7 @@ function linearCoeffsForSystem(eq: Expr, variables: string[]): number[] | null {
|
|
|
3841
4377
|
if (!Number.isFinite(constant)) return null;
|
|
3842
4378
|
const coeffs: number[] = [];
|
|
3843
4379
|
for (const name of variables) {
|
|
3844
|
-
const derivative =
|
|
4380
|
+
const derivative = simplifyCore(diff(eq, name));
|
|
3845
4381
|
if (variables.some((other) => containsVar(derivative, other))) return null;
|
|
3846
4382
|
const coeff = evalExpr(derivative, zeroEnv);
|
|
3847
4383
|
if (!Number.isFinite(coeff)) return null;
|
|
@@ -3870,7 +4406,7 @@ function polynomialCoeffs(expr: Expr, variable: string, maxDegree: number): numb
|
|
|
3870
4406
|
// derivative of x^0 (`0·x^-1`) evaluates to `0·Infinity = NaN` at x=0
|
|
3871
4407
|
// even though it is symbolically zero — simplifying collapses `0·anything`
|
|
3872
4408
|
// to `0` before that indeterminate form can arise.
|
|
3873
|
-
term =
|
|
4409
|
+
term = simplifyCore(diff(term, variable));
|
|
3874
4410
|
}
|
|
3875
4411
|
while (coeffs.length > 1 && Math.abs(coeffs[coeffs.length - 1]) < 1e-9) coeffs.pop();
|
|
3876
4412
|
for (const p of [1.3, -2.1, 3.7]) {
|
|
@@ -4058,7 +4594,7 @@ function factorPolynomial(coeffsIn: number[], variable: string): Expr {
|
|
|
4058
4594
|
} else if (Math.abs(remaining[0] - 1) > 1e-9) {
|
|
4059
4595
|
result = mul(result, toExactExpr(remaining[0]));
|
|
4060
4596
|
}
|
|
4061
|
-
return
|
|
4597
|
+
return simplifyCore(result);
|
|
4062
4598
|
}
|
|
4063
4599
|
|
|
4064
4600
|
// -- limits ---------------------------------------------------------------------
|
|
@@ -4083,8 +4619,8 @@ function limitAt(e: Expr, x: string, point: number, direction: "left" | "right"
|
|
|
4083
4619
|
// operand verbatim into the result) and a limit needing more than a
|
|
4084
4620
|
// couple of rounds (e.g. most limits at infinity) blows up memory
|
|
4085
4621
|
// long before the depth<12 cap is reached.
|
|
4086
|
-
const df =
|
|
4087
|
-
const dg =
|
|
4622
|
+
const df = simplifyCore(diff(e.left, x));
|
|
4623
|
+
const dg = simplifyCore(diff(e.right, x));
|
|
4088
4624
|
return limitAt(div(df, dg), x, point, direction, depth + 1);
|
|
4089
4625
|
}
|
|
4090
4626
|
}
|
|
@@ -4092,12 +4628,19 @@ function limitAt(e: Expr, x: string, point: number, direction: "left" | "right"
|
|
|
4092
4628
|
}
|
|
4093
4629
|
|
|
4094
4630
|
// -- recursive-descent parser ----------------------------------------------
|
|
4631
|
+
/** Every name the parser treats as a function, longest first, so `asinh(` is not read as `a*sinh(`. */
|
|
4632
|
+
const FUNCTION_NAMES_LONGEST_FIRST: string[] = [...FUNCTION_NAMES].sort((a, b) => b.length - a.length);
|
|
4633
|
+
|
|
4095
4634
|
class Parser {
|
|
4096
4635
|
private readonly s: string;
|
|
4636
|
+
private readonly implicit: boolean;
|
|
4097
4637
|
private pos = 0;
|
|
4098
4638
|
|
|
4099
|
-
constructor(input: string) {
|
|
4100
|
-
this.
|
|
4639
|
+
constructor(input: string, options: ParseOptions = {}) {
|
|
4640
|
+
this.implicit = options.implicitMultiplication === true;
|
|
4641
|
+
// With implicit multiplication, white space between two numbers is a product (`2 3`), not one number.
|
|
4642
|
+
const spaced = this.implicit ? input.replace(/(\d)\s+(?=[\d.])/g, "$1*") : input;
|
|
4643
|
+
this.s = spaced.replace(/\s+/g, "");
|
|
4101
4644
|
}
|
|
4102
4645
|
|
|
4103
4646
|
parse(): Expr {
|
|
@@ -4218,12 +4761,34 @@ class Parser {
|
|
|
4218
4761
|
|
|
4219
4762
|
private term(): Expr {
|
|
4220
4763
|
let left = this.factor();
|
|
4221
|
-
|
|
4222
|
-
const
|
|
4223
|
-
|
|
4224
|
-
|
|
4764
|
+
for (;;) {
|
|
4765
|
+
const ch = this.peek();
|
|
4766
|
+
if (ch === "*" || ch === "/") {
|
|
4767
|
+
this.pos++;
|
|
4768
|
+
const right = this.factor();
|
|
4769
|
+
left = ch === "*" ? mul(left, right) : div(left, right);
|
|
4770
|
+
} else if (this.implicit && /[a-zA-Z0-9.(]/.test(ch)) {
|
|
4771
|
+
left = mul(left, this.factor()); // juxtaposition: `2x`, `x y`, `2(x+1)`
|
|
4772
|
+
} else {
|
|
4773
|
+
return left;
|
|
4774
|
+
}
|
|
4225
4775
|
}
|
|
4226
|
-
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
/**
|
|
4779
|
+
* The identifier at `this.pos` under implicit multiplication: a function name followed by `(`, `pi`, or a
|
|
4780
|
+
* single letter with an optional `_subscript`. A letter run like `xy` is therefore read one letter at a time.
|
|
4781
|
+
*/
|
|
4782
|
+
private implicitIdentifier(): string {
|
|
4783
|
+
const rest = this.s.slice(this.pos);
|
|
4784
|
+
const run = /^[a-zA-Z]+/.exec(rest)?.[0] ?? "";
|
|
4785
|
+
const call = FUNCTION_NAMES_LONGEST_FIRST.find((f) => rest.startsWith(f) && rest[f.length] === "(");
|
|
4786
|
+
if (call) return call;
|
|
4787
|
+
const bare = FUNCTION_NAMES_LONGEST_FIRST.find((f) => f.length > 1 && run.startsWith(f));
|
|
4788
|
+
if (bare) throw new Error(`Function ${bare} needs parentheses: ${bare}(...)`);
|
|
4789
|
+
if (rest.startsWith("pi")) return "pi";
|
|
4790
|
+
const sub = /^[a-zA-Z]_(?:\d+|[a-zA-Z])/.exec(rest);
|
|
4791
|
+
return sub ? sub[0] : rest[0];
|
|
4227
4792
|
}
|
|
4228
4793
|
|
|
4229
4794
|
private factor(): Expr {
|
|
@@ -4261,9 +4826,11 @@ class Parser {
|
|
|
4261
4826
|
return num(Number(numMatch[0]));
|
|
4262
4827
|
}
|
|
4263
4828
|
// identifier (function or variable or constant)
|
|
4264
|
-
const idMatch =
|
|
4829
|
+
const idMatch = this.implicit
|
|
4830
|
+
? /^[a-zA-Z_]/.exec(this.s.slice(this.pos))
|
|
4831
|
+
: /^[a-zA-Z_]\w*/.exec(this.s.slice(this.pos));
|
|
4265
4832
|
if (idMatch) {
|
|
4266
|
-
const name = idMatch[0];
|
|
4833
|
+
const name = this.implicit ? this.implicitIdentifier() : idMatch[0];
|
|
4267
4834
|
this.pos += name.length;
|
|
4268
4835
|
if ((name === "log" || name === "clamp") && this.peek() === "(") {
|
|
4269
4836
|
this.pos++;
|