@johnhenry/math 0.0.2 → 0.1.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/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/Structure.d.ts +20 -3
- package/dist/Structure.d.ts.map +1 -1
- package/dist/Structure.js +80 -5
- package/dist/Structure.js.map +1 -1
- package/dist/Symbolic.d.ts +40 -4
- package/dist/Symbolic.d.ts.map +1 -1
- package/dist/Symbolic.js +624 -104
- package/dist/Symbolic.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- 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/Structure.ts +77 -5
- package/src/Symbolic.ts +652 -103
- package/src/index.ts +1 -0
package/src/Symbolic.ts
CHANGED
|
@@ -443,15 +443,38 @@ export interface OdeClosedFormResult {
|
|
|
443
443
|
implicitRelation?: Expr;
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
/** Options for {@link Symbolic.parse} and {@link Symbolic.fromLatex}. */
|
|
447
|
+
export interface ParseOptions {
|
|
448
|
+
/**
|
|
449
|
+
* Read juxtaposition as multiplication, the way people type maths: `2x`, `x y`, `xy`, `2(x+1)`,
|
|
450
|
+
* `(x+1)(x-1)`, `x(x+1)`, `3sin(x)`. Default `false`, which keeps the strict grammar (`2x` is an
|
|
451
|
+
* error and `xy` is one variable named `xy`).
|
|
452
|
+
*
|
|
453
|
+
* With this on, a run of letters that is not a function call (`name(`) or `pi` is split into
|
|
454
|
+
* single-letter variables, so `xy` is `x*y` and multi-letter variable names such as `theta` are
|
|
455
|
+
* unavailable (use a subscript: `x_1` or `x_a`, which stays one variable). A letter followed by
|
|
456
|
+
* digits is a product too (`x3` is `x*3`). A function name must be followed by `(`. Space
|
|
457
|
+
* between two numbers multiplies (`2 3` is `6`). Juxtaposition has the same precedence as `*`
|
|
458
|
+
* and `/` and binds left to right, so `1/2x` is `(1/2)*x`; `^` binds tighter (`2x^2` is `2*(x^2)`).
|
|
459
|
+
* For `fromLatex` it also lets a function with no brackets take a whole product as its argument
|
|
460
|
+
* (`\sin 2x` is `sin(2*x)`, `\sin x\cos x` is `sin(x)*cos(x)`), and `^` take one token (`x^23` is
|
|
461
|
+
* `x^2*3`, as TeX reads it; `x^{23}` is `x^23`).
|
|
462
|
+
*/
|
|
463
|
+
implicitMultiplication?: boolean;
|
|
464
|
+
}
|
|
465
|
+
|
|
446
466
|
export class Symbolic {
|
|
447
|
-
/**
|
|
448
|
-
|
|
449
|
-
|
|
467
|
+
/**
|
|
468
|
+
* Parse an expression string such as `"sin(x^2) + 3*x"` into an AST.
|
|
469
|
+
* Pass `{ implicitMultiplication: true }` to also read `2x`, `x y` and `2(x+1)`; see {@link ParseOptions}.
|
|
470
|
+
*/
|
|
471
|
+
static parse(input: string, options: ParseOptions = {}): Expr {
|
|
472
|
+
return new Parser(input, options ?? {}).parse();
|
|
450
473
|
}
|
|
451
474
|
|
|
452
475
|
static differentiate(expr: Expr | string, variable = "x"): Expr {
|
|
453
476
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
454
|
-
return
|
|
477
|
+
return simplifyCore(diff(e, variable));
|
|
455
478
|
}
|
|
456
479
|
|
|
457
480
|
/**
|
|
@@ -465,17 +488,21 @@ export class Symbolic {
|
|
|
465
488
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
466
489
|
const steps: DifferentiationStep[] = [];
|
|
467
490
|
const raw = diffTraced(e, variable, steps);
|
|
468
|
-
return { steps, result:
|
|
491
|
+
return { steps, result: simplifyCore(raw) };
|
|
469
492
|
}
|
|
470
493
|
|
|
494
|
+
/**
|
|
495
|
+
* Algebraic simplification: constant folding, like-term collection, and, as a last
|
|
496
|
+
* step, expanding the expression as a rational function and cancelling common
|
|
497
|
+
* factors -- `(x+1)^2 - x^2 - 2*x` gives `1` and `(x^2-1)/(x-1)` gives `x+1`.
|
|
498
|
+
* The expanded/cancelled form is used only when it is strictly smaller than the
|
|
499
|
+
* collected form, so a product such as `(x+1)*2` keeps its shape. Non-polynomial
|
|
500
|
+
* subexpressions (`sin(x)`, `x^y`) are treated as opaque symbols. Like any CAS
|
|
501
|
+
* cancellation, `(x^2-1)/(x-1)` is `x+1` as an expression, ignoring that the
|
|
502
|
+
* original is undefined at `x = 1`.
|
|
503
|
+
*/
|
|
471
504
|
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;
|
|
505
|
+
return normalizeRational(simplifyCore(expr));
|
|
479
506
|
}
|
|
480
507
|
|
|
481
508
|
/**
|
|
@@ -495,15 +522,15 @@ export class Symbolic {
|
|
|
495
522
|
* assumptions (e.g. "x+y is positive" isn't expressible).
|
|
496
523
|
*/
|
|
497
524
|
static simplifyAssuming(expr: Expr | string, assumptions: Record<string, Assumption>): Expr {
|
|
498
|
-
const simplified =
|
|
525
|
+
const simplified = simplifyCore(expr);
|
|
499
526
|
const rewritten = applyAssumptions(simplified, assumptions);
|
|
500
|
-
return
|
|
527
|
+
return simplifyCore(rewritten);
|
|
501
528
|
}
|
|
502
529
|
|
|
503
530
|
/** Symbolic anti-derivative (elementary rules; throws {@link NotIntegrableError} otherwise). */
|
|
504
531
|
static integrate(expr: Expr | string, variable = "x"): Expr {
|
|
505
532
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
506
|
-
return
|
|
533
|
+
return simplifyCore(integ(e, variable));
|
|
507
534
|
}
|
|
508
535
|
|
|
509
536
|
/**
|
|
@@ -542,13 +569,13 @@ export class Symbolic {
|
|
|
542
569
|
variable = "x",
|
|
543
570
|
env: Record<string, number> = {},
|
|
544
571
|
): number {
|
|
545
|
-
const e =
|
|
572
|
+
const e = simplifyCore(typeof expr === "string" ? Symbolic.parse(expr) : expr);
|
|
546
573
|
const probe = compileExpr(e);
|
|
547
574
|
if (hasInteriorSingularity((val: number) => probe({ ...env, [variable]: val }), lower, upper)) {
|
|
548
575
|
throw new IntegrationSingularityError();
|
|
549
576
|
}
|
|
550
577
|
try {
|
|
551
|
-
const F =
|
|
578
|
+
const F = simplifyCore(integ(e, variable));
|
|
552
579
|
const at = (val: number) => evalExpr(F, { ...env, [variable]: val });
|
|
553
580
|
return at(upper) - at(lower);
|
|
554
581
|
} catch (err) {
|
|
@@ -608,7 +635,7 @@ export class Symbolic {
|
|
|
608
635
|
xVar = "x",
|
|
609
636
|
yVar = "y",
|
|
610
637
|
): OdeClosedFormResult {
|
|
611
|
-
const e =
|
|
638
|
+
const e = simplifyCore(typeof dyDxExpr === "string" ? Symbolic.parse(dyDxExpr) : dyDxExpr);
|
|
612
639
|
const sep = matchSeparable(e, xVar, yVar);
|
|
613
640
|
if (sep) return solveSeparableOde(sep, xVar, yVar, x0, y0);
|
|
614
641
|
const lin = matchLinearOde(e, xVar, yVar);
|
|
@@ -724,14 +751,14 @@ export class Symbolic {
|
|
|
724
751
|
}
|
|
725
752
|
term = diff(term, variable);
|
|
726
753
|
}
|
|
727
|
-
return
|
|
754
|
+
return simplifyCore(result);
|
|
728
755
|
}
|
|
729
756
|
|
|
730
757
|
/** Replace every occurrence of a variable with a sub-expression (e.g. for changing variables or plugging in a solved value). */
|
|
731
758
|
static substitute(expr: Expr | string, variable: string, replacement: Expr | string): Expr {
|
|
732
759
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
733
760
|
const r = typeof replacement === "string" ? Symbolic.parse(replacement) : replacement;
|
|
734
|
-
return
|
|
761
|
+
return simplifyCore(subst(e, variable, r));
|
|
735
762
|
}
|
|
736
763
|
|
|
737
764
|
/** Distribute multiplication over addition/subtraction and expand small integer powers of sums, e.g. `(x+1)^2 -> x^2+2*x+1`. */
|
|
@@ -739,7 +766,7 @@ export class Symbolic {
|
|
|
739
766
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
740
767
|
let cur = e;
|
|
741
768
|
for (let i = 0; i < 30; i++) {
|
|
742
|
-
const next =
|
|
769
|
+
const next = simplifyCore(expandOnce(cur));
|
|
743
770
|
if (equal(next, cur)) return next;
|
|
744
771
|
cur = next;
|
|
745
772
|
}
|
|
@@ -852,12 +879,16 @@ export class Symbolic {
|
|
|
852
879
|
* commands (`\sin`, `\arcsin`, `\sinh`, `\operatorname{sech}`, ...), and the
|
|
853
880
|
* two-argument `BinaryFuncName` commands (`\min`, `\max`, `\gcd`,
|
|
854
881
|
* `\operatorname{atan2}`, `\operatorname{hypot}`, `\operatorname{lcm}`).
|
|
855
|
-
* Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored.
|
|
882
|
+
* Spacing commands (`\,`, `\;`, `\!`, `\quad`) are ignored. Single-token arguments as TeX
|
|
883
|
+
* reads them (`\frac12`, `\frac1x`, `\sqrt2`) and a power on a function command
|
|
884
|
+
* (`\sin^{2}(x)` is `sin(x)^2`) are accepted. An exponent of `-1` on a function command (`\sin^{-1}`) is
|
|
885
|
+
* refused as ambiguous (inverse function or reciprocal): write `\arcsin(x)` or `(\sin x)^{-1}`. For what people type into a math field
|
|
886
|
+
* (`2x`, `\sin 2x`, `(x+1)(x-1)`, `x^23`) pass `{ implicitMultiplication: true }`; see {@link ParseOptions}.
|
|
856
887
|
*
|
|
857
888
|
* @throws on LaTeX constructs with no `Expr` equivalent, e.g. `\int` or `\sum`.
|
|
858
889
|
*/
|
|
859
|
-
static fromLatex(latex: string): Expr {
|
|
860
|
-
return Symbolic.parse(latexToInfix(latex));
|
|
890
|
+
static fromLatex(latex: string, options: ParseOptions = {}): Expr {
|
|
891
|
+
return Symbolic.parse(latexToInfix(latex, options), options);
|
|
861
892
|
}
|
|
862
893
|
|
|
863
894
|
/**
|
|
@@ -878,11 +909,11 @@ export class Symbolic {
|
|
|
878
909
|
*/
|
|
879
910
|
static solve(expr: Expr | string, variable = "x"): Expr[] {
|
|
880
911
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
881
|
-
const coeffs = polynomialCoeffs(
|
|
912
|
+
const coeffs = polynomialCoeffs(simplifyCore(e), variable, 6);
|
|
882
913
|
if (!coeffs) {
|
|
883
914
|
throw new Error(`solve: expression is not a polynomial in "${variable}" of degree ≤ 6.`);
|
|
884
915
|
}
|
|
885
|
-
return solvePolynomial(coeffs).map((r) =>
|
|
916
|
+
return solvePolynomial(coeffs).map((r) => simplifyCore(r));
|
|
886
917
|
}
|
|
887
918
|
|
|
888
919
|
/**
|
|
@@ -900,7 +931,7 @@ export class Symbolic {
|
|
|
900
931
|
* @throws {SingularSystemError} if the coefficient matrix is singular/rank-deficient.
|
|
901
932
|
*/
|
|
902
933
|
static solveSystem(equations: (Expr | string)[], variables: string[]): Record<string, number> {
|
|
903
|
-
const exprs = equations.map((eq) =>
|
|
934
|
+
const exprs = equations.map((eq) => simplifyCore(typeof eq === "string" ? Symbolic.parse(eq) : eq));
|
|
904
935
|
if (exprs.length !== variables.length) {
|
|
905
936
|
throw new Error(
|
|
906
937
|
`solveSystem: expected ${variables.length} equations for ${variables.length} variables, got ${exprs.length}.`,
|
|
@@ -1149,7 +1180,7 @@ export class Symbolic {
|
|
|
1149
1180
|
*/
|
|
1150
1181
|
static factor(expr: Expr | string, variable = "x"): Expr {
|
|
1151
1182
|
const e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
1152
|
-
const simplified =
|
|
1183
|
+
const simplified = simplifyCore(e);
|
|
1153
1184
|
const coeffs = polynomialCoeffs(simplified, variable, 6);
|
|
1154
1185
|
if (!coeffs) return simplified;
|
|
1155
1186
|
return factorPolynomial(coeffs, variable);
|
|
@@ -1368,6 +1399,17 @@ function diff(e: Expr, x: string): Expr {
|
|
|
1368
1399
|
}
|
|
1369
1400
|
|
|
1370
1401
|
// -- simplification ---------------------------------------------------------
|
|
1402
|
+
/** Constant folding and like-term collection to a fixed point -- no expansion or cancellation. */
|
|
1403
|
+
function simplifyCore(expr: Expr | string): Expr {
|
|
1404
|
+
let e = typeof expr === "string" ? Symbolic.parse(expr) : expr;
|
|
1405
|
+
for (let i = 0; i < 30; i++) {
|
|
1406
|
+
const next = collectLikeTerms(simplifyOnce(e));
|
|
1407
|
+
if (equal(next, e)) return next;
|
|
1408
|
+
e = next;
|
|
1409
|
+
}
|
|
1410
|
+
return e;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1371
1413
|
function simplifyOnce(e: Expr): Expr {
|
|
1372
1414
|
if (e.type === "const" || e.type === "var") return e;
|
|
1373
1415
|
if (e.type === "neg") {
|
|
@@ -2115,7 +2157,7 @@ function freshVariableName(e: Expr, x: string): string {
|
|
|
2115
2157
|
* legitimately hit a domain error/NaN on either side).
|
|
2116
2158
|
*/
|
|
2117
2159
|
function verifyByDifferentiation(candidate: Expr, e: Expr, x: string): boolean {
|
|
2118
|
-
const check =
|
|
2160
|
+
const check = simplifyCore(diff(candidate, x));
|
|
2119
2161
|
let agreements = 0;
|
|
2120
2162
|
for (const p of [0.7, 1.3, -0.9]) {
|
|
2121
2163
|
const expected = evalExpr(e, { [x]: p });
|
|
@@ -2140,16 +2182,16 @@ function verifyByDifferentiation(candidate: Expr, e: Expr, x: string): boolean {
|
|
|
2140
2182
|
*/
|
|
2141
2183
|
function tryUSubstitution(e: Expr, x: string): Expr | null {
|
|
2142
2184
|
for (const g of collectSubstitutionCandidates(e, x)) {
|
|
2143
|
-
const gPrime =
|
|
2185
|
+
const gPrime = simplifyCore(diff(g, x));
|
|
2144
2186
|
if (isConst(gPrime, 0)) continue;
|
|
2145
|
-
const quotient =
|
|
2187
|
+
const quotient = simplifyCore(divideOutFactors(e, gPrime) ?? div(e, gPrime));
|
|
2146
2188
|
if (!containsExpr(quotient, g)) continue;
|
|
2147
2189
|
const uName = freshVariableName(quotient, x);
|
|
2148
2190
|
const substituted = substExpr(quotient, g, v(uName));
|
|
2149
2191
|
if (containsVar(substituted, x)) continue;
|
|
2150
2192
|
try {
|
|
2151
2193
|
const antiderivativeInU = integRules(substituted, uName);
|
|
2152
|
-
const candidate =
|
|
2194
|
+
const candidate = simplifyCore(substExpr(antiderivativeInU, v(uName), g));
|
|
2153
2195
|
if (verifyByDifferentiation(candidate, e, x)) return candidate;
|
|
2154
2196
|
} catch (err) {
|
|
2155
2197
|
if (!(err instanceof NotIntegrableError)) throw err;
|
|
@@ -2270,11 +2312,11 @@ function matchSeparable(e: Expr, xVar: string, yVar: string): { g: Expr; h: Expr
|
|
|
2270
2312
|
*/
|
|
2271
2313
|
function matchLinearOde(e: Expr, xVar: string, yVar: string): { p: Expr; q: Expr } | null {
|
|
2272
2314
|
if (!containsVar(e, yVar)) return null; // handled by matchSeparable's h(y)=1 case instead
|
|
2273
|
-
const negP =
|
|
2315
|
+
const negP = simplifyCore(diff(e, yVar));
|
|
2274
2316
|
if (containsVar(negP, yVar)) return null; // not affine in y
|
|
2275
|
-
const q =
|
|
2317
|
+
const q = simplifyCore(sub(e, mul(negP, v(yVar))));
|
|
2276
2318
|
if (containsVar(q, yVar)) return null; // sanity check
|
|
2277
|
-
const p =
|
|
2319
|
+
const p = simplifyCore(neg(negP));
|
|
2278
2320
|
// Conservative, matching matchSeparable's philosophy: p/q must depend on
|
|
2279
2321
|
// no free variable other than xVar (an extra symbolic parameter, or a
|
|
2280
2322
|
// stray reference to yVar that survived simplification, both bail out).
|
|
@@ -2337,21 +2379,21 @@ function solveSeparableOde(
|
|
|
2337
2379
|
x0: number,
|
|
2338
2380
|
y0: number,
|
|
2339
2381
|
): OdeClosedFormResult {
|
|
2340
|
-
const invH =
|
|
2382
|
+
const invH = simplifyCore(div(num(1), sep.h));
|
|
2341
2383
|
let H: Expr;
|
|
2342
2384
|
let G: Expr;
|
|
2343
2385
|
try {
|
|
2344
|
-
H =
|
|
2345
|
-
G =
|
|
2386
|
+
H = simplifyCore(integ(invH, yVar));
|
|
2387
|
+
G = simplifyCore(integ(sep.g, xVar));
|
|
2346
2388
|
} catch (err) {
|
|
2347
2389
|
if (err instanceof NotIntegrableError) throw new NoClosedFormError();
|
|
2348
2390
|
throw err;
|
|
2349
2391
|
}
|
|
2350
2392
|
const C = evalExpr(H, { [yVar]: y0 }) - evalExpr(G, { [xVar]: x0 });
|
|
2351
|
-
const rhs =
|
|
2393
|
+
const rhs = simplifyCore(add(G, num(C)));
|
|
2352
2394
|
const explicitY = tryInvertForY(H, yVar, rhs, y0);
|
|
2353
|
-
if (explicitY) return { explicit: true, y:
|
|
2354
|
-
return { explicit: false, implicitRelation:
|
|
2395
|
+
if (explicitY) return { explicit: true, y: simplifyCore(explicitY) };
|
|
2396
|
+
return { explicit: false, implicitRelation: simplifyCore(sub(H, rhs)) };
|
|
2355
2397
|
}
|
|
2356
2398
|
|
|
2357
2399
|
function solveLinearOde(
|
|
@@ -2365,9 +2407,9 @@ function solveLinearOde(
|
|
|
2365
2407
|
let mu: Expr;
|
|
2366
2408
|
let muQIntegral: Expr;
|
|
2367
2409
|
try {
|
|
2368
|
-
integralP =
|
|
2369
|
-
mu =
|
|
2370
|
-
muQIntegral =
|
|
2410
|
+
integralP = simplifyCore(integ(lin.p, xVar));
|
|
2411
|
+
mu = simplifyCore(fn("exp", integralP));
|
|
2412
|
+
muQIntegral = simplifyCore(integ(simplifyCore(mul(mu, lin.q)), xVar));
|
|
2371
2413
|
} catch (err) {
|
|
2372
2414
|
if (err instanceof NotIntegrableError) throw new NoClosedFormError();
|
|
2373
2415
|
throw err;
|
|
@@ -2376,7 +2418,7 @@ function solveLinearOde(
|
|
|
2376
2418
|
const muAtX0 = evalExpr(mu, { [xVar]: x0 });
|
|
2377
2419
|
const muQIntegralAtX0 = evalExpr(muQIntegral, { [xVar]: x0 });
|
|
2378
2420
|
const C = y0 * muAtX0 - muQIntegralAtX0;
|
|
2379
|
-
const y =
|
|
2421
|
+
const y = simplifyCore(div(add(muQIntegral, num(C)), mu));
|
|
2380
2422
|
return { explicit: true, y };
|
|
2381
2423
|
}
|
|
2382
2424
|
|
|
@@ -2429,7 +2471,7 @@ function matchHomogeneous(e: Expr, xVar: string, yVar: string): { vName: string;
|
|
|
2429
2471
|
}
|
|
2430
2472
|
}
|
|
2431
2473
|
if (attempts === 0 || agreements < 4) return null; // not enough usable probes to trust the match
|
|
2432
|
-
const F =
|
|
2474
|
+
const F = simplifyCore(subst(substituted, xVar, num(1)));
|
|
2433
2475
|
return { vName, F };
|
|
2434
2476
|
}
|
|
2435
2477
|
|
|
@@ -2461,15 +2503,15 @@ function solveHomogeneousOde(
|
|
|
2461
2503
|
): OdeClosedFormResult {
|
|
2462
2504
|
const { vName, F } = hom;
|
|
2463
2505
|
const v0 = y0 / x0;
|
|
2464
|
-
const FMinusV =
|
|
2506
|
+
const FMinusV = simplifyCore(sub(F, v(vName)));
|
|
2465
2507
|
if (FMinusV.type === "const" && FMinusV.value === 0) {
|
|
2466
|
-
return { explicit: true, y:
|
|
2508
|
+
return { explicit: true, y: simplifyCore(mul(num(v0), v(xVar))) };
|
|
2467
2509
|
}
|
|
2468
2510
|
const sepResult = solveSeparableOde({ g: div(num(1), v(xVar)), h: FMinusV }, xVar, vName, x0, v0);
|
|
2469
2511
|
if (sepResult.explicit && sepResult.y) {
|
|
2470
|
-
return { explicit: true, y:
|
|
2512
|
+
return { explicit: true, y: simplifyCore(mul(sepResult.y, v(xVar))) };
|
|
2471
2513
|
}
|
|
2472
|
-
const implicitRelation =
|
|
2514
|
+
const implicitRelation = simplifyCore(subst(sepResult.implicitRelation as Expr, vName, div(v(yVar), v(xVar))));
|
|
2473
2515
|
return { explicit: false, implicitRelation };
|
|
2474
2516
|
}
|
|
2475
2517
|
|
|
@@ -2554,8 +2596,8 @@ function matchBernoulli(e: Expr, xVar: string, yVar: string): { p: Expr; q: Expr
|
|
|
2554
2596
|
return null;
|
|
2555
2597
|
}
|
|
2556
2598
|
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 ?
|
|
2599
|
+
const q = powSign === 1 ? powMatch.coef : simplifyCore(neg(powMatch.coef));
|
|
2600
|
+
const p = linSign === 1 ? simplifyCore(neg(linMatch.coef)) : linMatch.coef;
|
|
2559
2601
|
return { p, q, n: powMatch.n };
|
|
2560
2602
|
}
|
|
2561
2603
|
|
|
@@ -2582,10 +2624,10 @@ function solveBernoulliOde(
|
|
|
2582
2624
|
): OdeClosedFormResult {
|
|
2583
2625
|
const oneMinusN = 1 - bern.n;
|
|
2584
2626
|
const w0 = y0 ** oneMinusN;
|
|
2585
|
-
const transformedP =
|
|
2586
|
-
const transformedQ =
|
|
2627
|
+
const transformedP = simplifyCore(mul(num(oneMinusN), bern.p));
|
|
2628
|
+
const transformedQ = simplifyCore(mul(num(oneMinusN), bern.q));
|
|
2587
2629
|
const wResult = solveLinearOde({ p: transformedP, q: transformedQ }, xVar, yVar, x0, w0);
|
|
2588
|
-
const y =
|
|
2630
|
+
const y = simplifyCore(pow(wResult.y as Expr, num(1 / oneMinusN)));
|
|
2589
2631
|
return { explicit: true, y };
|
|
2590
2632
|
}
|
|
2591
2633
|
|
|
@@ -2607,7 +2649,7 @@ function matchExact(e: Expr, xVar: string, yVar: string): { M: Expr; N: Expr } |
|
|
|
2607
2649
|
const vars = collectFreeVars(e);
|
|
2608
2650
|
if (![...vars].every((n) => n === xVar || n === yVar)) return null;
|
|
2609
2651
|
const M = e.left;
|
|
2610
|
-
const N =
|
|
2652
|
+
const N = simplifyCore(neg(e.right));
|
|
2611
2653
|
const dMdy = diff(M, yVar);
|
|
2612
2654
|
const dNdx = diff(N, xVar);
|
|
2613
2655
|
let agreements = 0;
|
|
@@ -2662,8 +2704,8 @@ function solveExactOde(
|
|
|
2662
2704
|
let g: Expr;
|
|
2663
2705
|
let gPrime: Expr;
|
|
2664
2706
|
try {
|
|
2665
|
-
F0 =
|
|
2666
|
-
gPrime =
|
|
2707
|
+
F0 = simplifyCore(integ(M, xVar));
|
|
2708
|
+
gPrime = simplifyCore(sub(N, diff(F0, yVar)));
|
|
2667
2709
|
if (containsVar(gPrime, xVar)) {
|
|
2668
2710
|
// Symbolic cancellation fell short -- verify x-independence
|
|
2669
2711
|
// numerically, then pin x to a probe value that evaluates finitely.
|
|
@@ -2675,18 +2717,18 @@ function solveExactOde(
|
|
|
2675
2717
|
if (Math.abs(atProbe - atOther) > 1e-6 * Math.max(1, Math.abs(atProbe))) {
|
|
2676
2718
|
throw new NoClosedFormError();
|
|
2677
2719
|
}
|
|
2678
|
-
pinned =
|
|
2720
|
+
pinned = simplifyCore(subst(gPrime, xVar, num(xp)));
|
|
2679
2721
|
break;
|
|
2680
2722
|
}
|
|
2681
2723
|
if (!pinned) throw new NoClosedFormError();
|
|
2682
2724
|
gPrime = pinned;
|
|
2683
2725
|
}
|
|
2684
|
-
g =
|
|
2726
|
+
g = simplifyCore(integ(gPrime, yVar));
|
|
2685
2727
|
} catch (err) {
|
|
2686
2728
|
if (err instanceof NotIntegrableError) throw new NoClosedFormError();
|
|
2687
2729
|
throw err;
|
|
2688
2730
|
}
|
|
2689
|
-
const F =
|
|
2731
|
+
const F = simplifyCore(add(F0, g));
|
|
2690
2732
|
const C = evalExpr(F, { [xVar]: x0, [yVar]: y0 });
|
|
2691
2733
|
if (!Number.isFinite(C)) throw new NoClosedFormError();
|
|
2692
2734
|
// Cheap explicit attempt: F = H(y) + G(x) (either order) inverts via the
|
|
@@ -2701,12 +2743,12 @@ function solveExactOde(
|
|
|
2701
2743
|
] as const) {
|
|
2702
2744
|
if (!isPureFunctionOf(yPart, yOnly) || !isPureFunctionOf(xPartSigned, xOnly)) continue;
|
|
2703
2745
|
if (F.type === "sub" && yPart === b) continue; // F = G(x) - H(y): inverting -H is out of tryInvertForY's shapes
|
|
2704
|
-
const rhs =
|
|
2746
|
+
const rhs = simplifyCore(sub(num(C), xPartSigned));
|
|
2705
2747
|
const explicitY = tryInvertForY(yPart, yVar, rhs, y0);
|
|
2706
|
-
if (explicitY) return { explicit: true, y:
|
|
2748
|
+
if (explicitY) return { explicit: true, y: simplifyCore(explicitY) };
|
|
2707
2749
|
}
|
|
2708
2750
|
}
|
|
2709
|
-
return { explicit: false, implicitRelation:
|
|
2751
|
+
return { explicit: false, implicitRelation: simplifyCore(sub(F, num(C))) };
|
|
2710
2752
|
}
|
|
2711
2753
|
|
|
2712
2754
|
/** If `e` is `a·x + b` (a, b constant), return `{ a, b }`, else `null`. */
|
|
@@ -3604,12 +3646,47 @@ const CMP_LATEX_CMDS: Record<string, string> = {
|
|
|
3604
3646
|
};
|
|
3605
3647
|
|
|
3606
3648
|
/** 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);
|
|
3649
|
+
function latexToInfix(latex: string, opts: ParseOptions = {}): string {
|
|
3650
|
+
const cleaned = latex.replace(/\\left|\\right/g, "").replace(/(?<!\\)\\(?:,|;|!|quad|qquad| )/g, "");
|
|
3651
|
+
return transformLatex(cleaned, opts);
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
/**
|
|
3655
|
+
* Where the unbracketed argument of a function command ends under implicit multiplication: the longest
|
|
3656
|
+
* product of numbers, letters, `\\pi`, powers, subscripts and bracket groups starting at `start`
|
|
3657
|
+
* (`\\sin 2x` takes `2x`; `\\sin x\\cos x` takes just `x`). Returns `start` when there is none.
|
|
3658
|
+
*/
|
|
3659
|
+
function bareArgumentEnd(s: string, start: number): number {
|
|
3660
|
+
let j = start;
|
|
3661
|
+
const group = (at: number): number => {
|
|
3662
|
+
const open = s[at];
|
|
3663
|
+
return findGroupEnd(s, at, open, open === "{" ? "}" : ")") + 1;
|
|
3664
|
+
};
|
|
3665
|
+
while (j < s.length) {
|
|
3666
|
+
const c = s[j];
|
|
3667
|
+
if (/[A-Za-z0-9.]/.test(c)) {
|
|
3668
|
+
j++;
|
|
3669
|
+
} else if (c === "(" || c === "{") {
|
|
3670
|
+
j = group(j);
|
|
3671
|
+
} else if (c === "_" && s[j + 1] === "{") {
|
|
3672
|
+
j = group(j + 1);
|
|
3673
|
+
} else if (c === "^") {
|
|
3674
|
+
if (s[j + 1] === "{") j = group(j + 1);
|
|
3675
|
+
else if (s[j + 1] === "\\") j += 1 + (/^\\[a-zA-Z]+/.exec(s.slice(j + 1))?.[0].length ?? 1);
|
|
3676
|
+
else j += 2;
|
|
3677
|
+
} else if (/^\\pi(?![a-zA-Z])/.test(s.slice(j))) {
|
|
3678
|
+
j += 3;
|
|
3679
|
+
} else if (c === " " && /^ +(?:[A-Za-z0-9.({]|\\pi(?![a-zA-Z]))/.test(s.slice(j))) {
|
|
3680
|
+
j += s.slice(j).length - s.slice(j).trimStart().length;
|
|
3681
|
+
} else {
|
|
3682
|
+
break;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
return j;
|
|
3610
3686
|
}
|
|
3611
3687
|
|
|
3612
|
-
function transformLatex(s: string): string {
|
|
3688
|
+
function transformLatex(s: string, opts: ParseOptions = {}): string {
|
|
3689
|
+
const implicit = opts.implicitMultiplication === true;
|
|
3613
3690
|
let out = "";
|
|
3614
3691
|
let i = 0;
|
|
3615
3692
|
|
|
@@ -3621,11 +3698,20 @@ function transformLatex(s: string): string {
|
|
|
3621
3698
|
};
|
|
3622
3699
|
|
|
3623
3700
|
const readGroupOrParenOrToken = (): string => {
|
|
3701
|
+
while (s[i] === " ") i++;
|
|
3624
3702
|
if (s[i] === "{" || s[i] === "(") {
|
|
3625
3703
|
const { content, next } = extractGroup(s, i);
|
|
3626
3704
|
i = next;
|
|
3627
3705
|
return content;
|
|
3628
3706
|
}
|
|
3707
|
+
if (implicit) {
|
|
3708
|
+
const end = bareArgumentEnd(s, i);
|
|
3709
|
+
if (end > i) {
|
|
3710
|
+
const run = s.slice(i, end).trim();
|
|
3711
|
+
i = end;
|
|
3712
|
+
return run;
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3629
3715
|
const m = /^[a-zA-Z_]\w*|^\d+\.?\d*(?:[eE][+-]?\d+)?/.exec(s.slice(i));
|
|
3630
3716
|
if (m) {
|
|
3631
3717
|
i += m[0].length;
|
|
@@ -3634,6 +3720,32 @@ function transformLatex(s: string): string {
|
|
|
3634
3720
|
throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
|
|
3635
3721
|
};
|
|
3636
3722
|
|
|
3723
|
+
/** A `{...}` group, or the one token TeX takes without braces: a digit, a letter or a `\\command`. */
|
|
3724
|
+
const readGroupOrToken = (): string => {
|
|
3725
|
+
while (s[i] === " ") i++;
|
|
3726
|
+
if (s[i] === "{") return readGroup();
|
|
3727
|
+
const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i));
|
|
3728
|
+
if (!m) throw new Error(`Expected an argument at position ${i} in LaTeX source: ${s}`);
|
|
3729
|
+
i += m[0].length;
|
|
3730
|
+
return m[0];
|
|
3731
|
+
};
|
|
3732
|
+
|
|
3733
|
+
/** A power written on a function command (`\\sin^{2}`, `\\sin^2`), or null. */
|
|
3734
|
+
const readFunctionPower = (): string | null => {
|
|
3735
|
+
if (s[i] !== "^") return null;
|
|
3736
|
+
i++;
|
|
3737
|
+
const power = readGroupOrToken();
|
|
3738
|
+
// `\sin^{-1}` is arcsin in most textbooks and calculators but 1/sin in strict TeX/notation; guessing
|
|
3739
|
+
// wrong would silently change the maths, so it is refused rather than interpreted.
|
|
3740
|
+
if (/^\s*-\s*1\s*$/.test(power)) {
|
|
3741
|
+
throw new Error(
|
|
3742
|
+
"Ambiguous LaTeX: a function with exponent -1 (e.g. \\sin^{-1}) may mean the inverse function or the reciprocal; " +
|
|
3743
|
+
"write \\arcsin(x) for the inverse, or (\\sin x)^{-1} / 1/\\sin x for the reciprocal",
|
|
3744
|
+
);
|
|
3745
|
+
}
|
|
3746
|
+
return power;
|
|
3747
|
+
};
|
|
3748
|
+
|
|
3637
3749
|
/** Read a `(...)`/`{...}` group holding exactly 2 comma-separated arguments, for `call2` functions. */
|
|
3638
3750
|
const readArgPair = (): [string, string] => {
|
|
3639
3751
|
if (s[i] !== "(" && s[i] !== "{") throw new Error(`Expected '(' at position ${i} in LaTeX source: ${s}`);
|
|
@@ -3683,9 +3795,10 @@ function transformLatex(s: string): string {
|
|
|
3683
3795
|
const [exprPart, condPart] = cols;
|
|
3684
3796
|
if (condPart.trim() === "\\text{otherwise}") {
|
|
3685
3797
|
if (otherwise !== null) throw new Error("\\begin{cases} may have only one \\text{otherwise} row");
|
|
3686
|
-
otherwise = transformLatex(exprPart);
|
|
3798
|
+
otherwise = transformLatex(exprPart, opts);
|
|
3687
3799
|
} else {
|
|
3688
|
-
|
|
3800
|
+
const condition = condPart.replace(/^\s*\\text\{\s*(?:if|when)\s*\}/, ""); // "x & \text{if } x<0" as MathLive writes it
|
|
3801
|
+
parts.push(`${transformLatex(condition, opts)}, ${transformLatex(exprPart, opts)}`);
|
|
3689
3802
|
}
|
|
3690
3803
|
}
|
|
3691
3804
|
if (otherwise === null) {
|
|
@@ -3694,28 +3807,28 @@ function transformLatex(s: string): string {
|
|
|
3694
3807
|
out += `piecewise(${parts.concat(otherwise).join(", ")})`;
|
|
3695
3808
|
continue;
|
|
3696
3809
|
}
|
|
3697
|
-
if (cmd === "\\frac") {
|
|
3698
|
-
const numerator =
|
|
3699
|
-
const denominator =
|
|
3700
|
-
out += `((${transformLatex(numerator)})/(${transformLatex(denominator)}))`;
|
|
3810
|
+
if (cmd === "\\frac" || cmd === "\\dfrac" || cmd === "\\tfrac") {
|
|
3811
|
+
const numerator = readGroupOrToken();
|
|
3812
|
+
const denominator = readGroupOrToken();
|
|
3813
|
+
out += `((${transformLatex(numerator, opts)})/(${transformLatex(denominator, opts)}))`;
|
|
3701
3814
|
continue;
|
|
3702
3815
|
}
|
|
3703
3816
|
if (cmd === "\\sqrt") {
|
|
3704
3817
|
if (s[i] === "[") {
|
|
3705
3818
|
const { content: root, next } = extractGroup(s, i);
|
|
3706
3819
|
i = next;
|
|
3707
|
-
const arg =
|
|
3708
|
-
const transformedRoot = transformLatex(root).trim();
|
|
3820
|
+
const arg = readGroupOrToken();
|
|
3821
|
+
const transformedRoot = transformLatex(root, opts).trim();
|
|
3709
3822
|
// \sqrt[3]{...} maps to cbrt(...) rather than a fractional power so
|
|
3710
3823
|
// negative arguments evaluate correctly (Math.cbrt(-8) = -2, but
|
|
3711
3824
|
// (-8)^(1/3) is NaN under JS's `**`).
|
|
3712
3825
|
out +=
|
|
3713
3826
|
transformedRoot === "3"
|
|
3714
|
-
? `cbrt(${transformLatex(arg)})`
|
|
3715
|
-
: `((${transformLatex(arg)})^(1/(${transformedRoot})))`;
|
|
3827
|
+
? `cbrt(${transformLatex(arg, opts)})`
|
|
3828
|
+
: `((${transformLatex(arg, opts)})^(1/(${transformedRoot})))`;
|
|
3716
3829
|
} else {
|
|
3717
|
-
const arg =
|
|
3718
|
-
out += `sqrt(${transformLatex(arg)})`;
|
|
3830
|
+
const arg = readGroupOrToken();
|
|
3831
|
+
out += `sqrt(${transformLatex(arg, opts)})`;
|
|
3719
3832
|
}
|
|
3720
3833
|
continue;
|
|
3721
3834
|
}
|
|
@@ -3729,8 +3842,10 @@ function transformLatex(s: string): string {
|
|
|
3729
3842
|
if (base !== "10" && base !== "2") {
|
|
3730
3843
|
throw new Error(`Unsupported LaTeX construct: \\log base ${base} (only base 10 and base 2 are supported)`);
|
|
3731
3844
|
}
|
|
3845
|
+
const power = readFunctionPower();
|
|
3732
3846
|
const arg = readGroupOrParenOrToken();
|
|
3733
|
-
|
|
3847
|
+
const call = `${base === "2" ? "log2" : "log10"}(${transformLatex(arg, opts)})`;
|
|
3848
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3734
3849
|
continue;
|
|
3735
3850
|
}
|
|
3736
3851
|
if (cmd === "\\lfloor" || cmd === "\\lceil") {
|
|
@@ -3739,7 +3854,7 @@ function transformLatex(s: string): string {
|
|
|
3739
3854
|
if (closeIdx === -1) throw new Error(`Unmatched '${cmd}' in LaTeX source`);
|
|
3740
3855
|
const inner = s.slice(i, closeIdx);
|
|
3741
3856
|
i = closeIdx + closeCmd.length;
|
|
3742
|
-
out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner)})`;
|
|
3857
|
+
out += `${cmd === "\\lfloor" ? "floor" : "ceil"}(${transformLatex(inner, opts)})`;
|
|
3743
3858
|
continue;
|
|
3744
3859
|
}
|
|
3745
3860
|
if (cmd === "\\operatorname") {
|
|
@@ -3747,25 +3862,29 @@ function transformLatex(s: string): string {
|
|
|
3747
3862
|
const binaryName = OPERATORNAME_BINARY_FUNC_NAMES[opName];
|
|
3748
3863
|
if (binaryName) {
|
|
3749
3864
|
const [left, right] = readArgPair();
|
|
3750
|
-
out += `${binaryName}(${transformLatex(left)}, ${transformLatex(right)})`;
|
|
3865
|
+
out += `${binaryName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
|
|
3751
3866
|
continue;
|
|
3752
3867
|
}
|
|
3753
3868
|
const funcName = OPERATORNAME_FUNC_NAMES[opName];
|
|
3754
3869
|
if (!funcName) throw new Error(`Unsupported LaTeX construct: \\operatorname{${opName}}`);
|
|
3870
|
+
const power = readFunctionPower();
|
|
3755
3871
|
const arg = readGroupOrParenOrToken();
|
|
3756
|
-
|
|
3872
|
+
const call = `${funcName}(${transformLatex(arg, opts)})`;
|
|
3873
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3757
3874
|
continue;
|
|
3758
3875
|
}
|
|
3759
3876
|
const binaryFuncName = LATEX_BINARY_FUNC_NAMES[cmd];
|
|
3760
3877
|
if (binaryFuncName) {
|
|
3761
3878
|
const [left, right] = readArgPair();
|
|
3762
|
-
out += `${binaryFuncName}(${transformLatex(left)}, ${transformLatex(right)})`;
|
|
3879
|
+
out += `${binaryFuncName}(${transformLatex(left, opts)}, ${transformLatex(right, opts)})`;
|
|
3763
3880
|
continue;
|
|
3764
3881
|
}
|
|
3765
3882
|
const funcName = LATEX_FUNC_NAMES[cmd];
|
|
3766
3883
|
if (funcName) {
|
|
3884
|
+
const power = readFunctionPower();
|
|
3767
3885
|
const arg = readGroupOrParenOrToken();
|
|
3768
|
-
|
|
3886
|
+
const call = `${funcName}(${transformLatex(arg, opts)})`;
|
|
3887
|
+
out += power === null ? call : `${call}^(${transformLatex(power, opts)})`;
|
|
3769
3888
|
continue;
|
|
3770
3889
|
}
|
|
3771
3890
|
throw new Error(`Unsupported LaTeX construct: ${cmd}`);
|
|
@@ -3775,13 +3894,20 @@ function transformLatex(s: string): string {
|
|
|
3775
3894
|
if (closeIdx === -1) throw new Error("Unmatched '|' in LaTeX source");
|
|
3776
3895
|
const inner = s.slice(i + 1, closeIdx);
|
|
3777
3896
|
i = closeIdx + 1;
|
|
3778
|
-
out += `abs(${transformLatex(inner)})`;
|
|
3897
|
+
out += `abs(${transformLatex(inner, opts)})`;
|
|
3898
|
+
continue;
|
|
3899
|
+
}
|
|
3900
|
+
if (implicit && ch === "^" && /^[a-zA-Z0-9]|^\\[a-zA-Z]/.test(s.slice(i + 1))) {
|
|
3901
|
+
// TeX reads `x^23` as `x^2` times `3`: the exponent is one token
|
|
3902
|
+
const m = /^(?:\\[a-zA-Z]+|[a-zA-Z0-9])/.exec(s.slice(i + 1)) as RegExpExecArray;
|
|
3903
|
+
i += 1 + m[0].length;
|
|
3904
|
+
out += `^(${transformLatex(m[0], opts)})`;
|
|
3779
3905
|
continue;
|
|
3780
3906
|
}
|
|
3781
3907
|
if (ch === "^" && s[i + 1] === "{") {
|
|
3782
3908
|
const { content, next } = extractGroup(s, i + 1);
|
|
3783
3909
|
i = next;
|
|
3784
|
-
out += `^(${transformLatex(content)})`;
|
|
3910
|
+
out += `^(${transformLatex(content, opts)})`;
|
|
3785
3911
|
continue;
|
|
3786
3912
|
}
|
|
3787
3913
|
if (ch === "_" && s[i + 1] === "{") {
|
|
@@ -3793,7 +3919,7 @@ function transformLatex(s: string): string {
|
|
|
3793
3919
|
if (ch === "{") {
|
|
3794
3920
|
const { content, next } = extractGroup(s, i);
|
|
3795
3921
|
i = next;
|
|
3796
|
-
out += `(${transformLatex(content)})`;
|
|
3922
|
+
out += `(${transformLatex(content, opts)})`;
|
|
3797
3923
|
continue;
|
|
3798
3924
|
}
|
|
3799
3925
|
if (ch === "=") {
|
|
@@ -3810,6 +3936,398 @@ function transformLatex(s: string): string {
|
|
|
3810
3936
|
return out;
|
|
3811
3937
|
}
|
|
3812
3938
|
|
|
3939
|
+
// -- rational-function normalization (expand + cancel) ----------------------------
|
|
3940
|
+
// `simplify` finishes by trying to read the expression as a quotient of two
|
|
3941
|
+
// polynomials (over the rationals, in "atoms": variables and any subexpression that
|
|
3942
|
+
// is not itself polynomial, such as `sin(x)`), expanding both, cancelling their
|
|
3943
|
+
// greatest common divisor, and printing the result back out. The result is used only
|
|
3944
|
+
// when it is strictly smaller than what `simplify` had already reached, so products
|
|
3945
|
+
// like `(x+1)*2` that are no smaller expanded keep their form.
|
|
3946
|
+
|
|
3947
|
+
/** A monomial's exponent per atom, and its coefficient. */
|
|
3948
|
+
interface PolyTerm {
|
|
3949
|
+
exps: number[];
|
|
3950
|
+
c: Rational;
|
|
3951
|
+
}
|
|
3952
|
+
type Poly = PolyTerm[]; // sorted by lex order descending, no zero coefficients, no duplicate monomials
|
|
3953
|
+
|
|
3954
|
+
const MAX_POLY_TERMS = 400;
|
|
3955
|
+
const MAX_RATIONAL_POWER = 8;
|
|
3956
|
+
|
|
3957
|
+
class RatBail extends Error {}
|
|
3958
|
+
|
|
3959
|
+
interface RatCtx {
|
|
3960
|
+
atoms: Expr[];
|
|
3961
|
+
keys: string[];
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
function atomIndex(ctx: RatCtx, e: Expr): number {
|
|
3965
|
+
const key = render(e, 0);
|
|
3966
|
+
let i = ctx.keys.indexOf(key);
|
|
3967
|
+
if (i < 0) {
|
|
3968
|
+
i = ctx.keys.length;
|
|
3969
|
+
ctx.keys.push(key);
|
|
3970
|
+
ctx.atoms.push(e);
|
|
3971
|
+
}
|
|
3972
|
+
return i;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
function expsCmp(a: number[], b: number[]): number {
|
|
3976
|
+
const n = Math.max(a.length, b.length);
|
|
3977
|
+
for (let i = 0; i < n; i++) {
|
|
3978
|
+
const d = (a[i] ?? 0) - (b[i] ?? 0);
|
|
3979
|
+
if (d !== 0) return d;
|
|
3980
|
+
}
|
|
3981
|
+
return 0;
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
function expsKey(a: number[]): string {
|
|
3985
|
+
let end = a.length;
|
|
3986
|
+
while (end > 0 && !a[end - 1]) end--;
|
|
3987
|
+
return a.slice(0, end).join(",");
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3990
|
+
function polyNormalize(terms: PolyTerm[]): Poly {
|
|
3991
|
+
const byKey = new Map<string, PolyTerm>();
|
|
3992
|
+
for (const t of terms) {
|
|
3993
|
+
const k = expsKey(t.exps);
|
|
3994
|
+
const prev = byKey.get(k);
|
|
3995
|
+
if (prev) byKey.set(k, { exps: prev.exps, c: prev.c.add(t.c) });
|
|
3996
|
+
else byKey.set(k, { exps: t.exps, c: t.c });
|
|
3997
|
+
}
|
|
3998
|
+
const out = [...byKey.values()].filter((t) => !t.c.isZero());
|
|
3999
|
+
out.sort((a, b) => expsCmp(b.exps, a.exps));
|
|
4000
|
+
if (out.length > MAX_POLY_TERMS) throw new RatBail();
|
|
4001
|
+
return out;
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
const polyConst = (c: Rational): Poly => (c.isZero() ? [] : [{ exps: [], c }]);
|
|
4005
|
+
const polyAdd = (a: Poly, b: Poly): Poly => polyNormalize([...a, ...b]);
|
|
4006
|
+
const polyNeg = (a: Poly): Poly => a.map((t) => ({ exps: t.exps, c: t.c.negate() }));
|
|
4007
|
+
const polySub = (a: Poly, b: Poly): Poly => polyAdd(a, polyNeg(b));
|
|
4008
|
+
|
|
4009
|
+
function polyMul(a: Poly, b: Poly): Poly {
|
|
4010
|
+
if (a.length * b.length > MAX_POLY_TERMS * 20) throw new RatBail();
|
|
4011
|
+
const out: PolyTerm[] = [];
|
|
4012
|
+
for (const x of a) {
|
|
4013
|
+
for (const y of b) {
|
|
4014
|
+
const n = Math.max(x.exps.length, y.exps.length);
|
|
4015
|
+
const exps: number[] = [];
|
|
4016
|
+
for (let i = 0; i < n; i++) exps.push((x.exps[i] ?? 0) + (y.exps[i] ?? 0));
|
|
4017
|
+
out.push({ exps, c: x.c.multiply(y.c) });
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
return polyNormalize(out);
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
function polyEqual(a: Poly, b: Poly): boolean {
|
|
4024
|
+
return a.length === b.length && a.every((t, i) => expsKey(t.exps) === expsKey(b[i].exps) && t.c.equals(b[i].c));
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
const polyIsConst = (a: Poly): boolean => a.length === 0 || (a.length === 1 && expsKey(a[0].exps) === "");
|
|
4028
|
+
|
|
4029
|
+
/** Division in lex order: `a = q*b + r` (`b` must be nonzero). */
|
|
4030
|
+
function polyDivMod(a: Poly, b: Poly): { q: Poly; r: Poly } {
|
|
4031
|
+
const lead = b[0];
|
|
4032
|
+
let rem = a;
|
|
4033
|
+
const q: PolyTerm[] = [];
|
|
4034
|
+
const r: PolyTerm[] = [];
|
|
4035
|
+
let guard = 0;
|
|
4036
|
+
while (rem.length > 0) {
|
|
4037
|
+
if (++guard > 20_000) throw new RatBail();
|
|
4038
|
+
const t = rem[0];
|
|
4039
|
+
const n = Math.max(t.exps.length, lead.exps.length);
|
|
4040
|
+
const exps: number[] = [];
|
|
4041
|
+
let divisible = true;
|
|
4042
|
+
for (let i = 0; i < n; i++) {
|
|
4043
|
+
const d = (t.exps[i] ?? 0) - (lead.exps[i] ?? 0);
|
|
4044
|
+
if (d < 0) divisible = false;
|
|
4045
|
+
exps.push(d);
|
|
4046
|
+
}
|
|
4047
|
+
if (!divisible) {
|
|
4048
|
+
r.push(t);
|
|
4049
|
+
rem = rem.slice(1);
|
|
4050
|
+
continue;
|
|
4051
|
+
}
|
|
4052
|
+
const qt: PolyTerm = { exps, c: t.c.divide(lead.c) };
|
|
4053
|
+
q.push(qt);
|
|
4054
|
+
rem = polySub(rem, polyMul([qt], b));
|
|
4055
|
+
}
|
|
4056
|
+
return { q: polyNormalize(q), r: polyNormalize(r) };
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
function polyAtoms(...ps: Poly[]): Set<number> {
|
|
4060
|
+
const s = new Set<number>();
|
|
4061
|
+
for (const p of ps) {
|
|
4062
|
+
for (const t of p) {
|
|
4063
|
+
for (let i = 0; i < t.exps.length; i++) if (t.exps[i] > 0) s.add(i);
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
return s;
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
/** Monic GCD of two polynomials in one atom (Euclid); 1 when several atoms are involved. */
|
|
4070
|
+
function polyGcd(a: Poly, b: Poly): Poly {
|
|
4071
|
+
if (polyAtoms(a, b).size > 1) return polyConst(Rational.One);
|
|
4072
|
+
let x = a;
|
|
4073
|
+
let y = b;
|
|
4074
|
+
while (y.length > 0) {
|
|
4075
|
+
const { r } = polyDivMod(x, y);
|
|
4076
|
+
x = y;
|
|
4077
|
+
y = r;
|
|
4078
|
+
}
|
|
4079
|
+
if (x.length === 0) return polyConst(Rational.One);
|
|
4080
|
+
const lc = x[0].c;
|
|
4081
|
+
return x.map((t) => ({ exps: t.exps, c: t.c.divide(lc) }));
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
/** The largest monomial dividing every term (exponent-wise minimum). */
|
|
4085
|
+
function monomialContent(p: Poly): number[] {
|
|
4086
|
+
if (p.length === 0) return [];
|
|
4087
|
+
const n = Math.max(...p.map((t) => t.exps.length));
|
|
4088
|
+
const out: number[] = [];
|
|
4089
|
+
for (let i = 0; i < n; i++) out.push(Math.min(...p.map((t) => t.exps[i] ?? 0)));
|
|
4090
|
+
return out;
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
function polyDivMonomial(p: Poly, m: number[]): Poly {
|
|
4094
|
+
return p.map((t) => ({ exps: t.exps.map((e, i) => e - (m[i] ?? 0)), c: t.c }));
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
interface Ratio {
|
|
4098
|
+
n: Poly;
|
|
4099
|
+
d: Poly;
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
const ratioFromPoly = (n: Poly): Ratio => ({ n, d: polyConst(Rational.One) });
|
|
4103
|
+
|
|
4104
|
+
function ratioAdd(a: Ratio, b: Ratio, subtract: boolean): Ratio {
|
|
4105
|
+
const bn = subtract ? polyNeg(b.n) : b.n;
|
|
4106
|
+
if (polyEqual(a.d, b.d)) return { n: polyAdd(a.n, bn), d: a.d };
|
|
4107
|
+
return { n: polyAdd(polyMul(a.n, b.d), polyMul(bn, a.d)), d: polyMul(a.d, b.d) };
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
const ratioMul = (a: Ratio, b: Ratio): Ratio => ({ n: polyMul(a.n, b.n), d: polyMul(a.d, b.d) });
|
|
4111
|
+
|
|
4112
|
+
function ratioDiv(a: Ratio, b: Ratio): Ratio {
|
|
4113
|
+
if (b.n.length === 0) throw new RatBail(); // division by zero: leave it to evaluation
|
|
4114
|
+
return { n: polyMul(a.n, b.d), d: polyMul(a.d, b.n) };
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
function ratioPow(a: Ratio, n: number): Ratio {
|
|
4118
|
+
const k = Math.abs(n);
|
|
4119
|
+
if (k > 1000) throw new RatBail();
|
|
4120
|
+
const singleTerm = a.n.length === 1 && a.d.length === 1;
|
|
4121
|
+
if (!singleTerm && k > MAX_RATIONAL_POWER) throw new RatBail();
|
|
4122
|
+
let result: Ratio;
|
|
4123
|
+
if (singleTerm) {
|
|
4124
|
+
const mono = (t: PolyTerm): PolyTerm => ({ exps: t.exps.map((e) => e * k), c: t.c.pow(k) });
|
|
4125
|
+
result = { n: [mono(a.n[0])], d: [mono(a.d[0])] };
|
|
4126
|
+
} else {
|
|
4127
|
+
result = ratioFromPoly(polyConst(Rational.One));
|
|
4128
|
+
for (let i = 0; i < k; i++) result = ratioMul(result, a);
|
|
4129
|
+
}
|
|
4130
|
+
return n < 0 ? ratioDiv(ratioFromPoly(polyConst(Rational.One)), result) : result;
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
function exactRational(value: number): Rational | null {
|
|
4134
|
+
if (!Number.isFinite(value)) return null;
|
|
4135
|
+
if (Number.isInteger(value)) return new Rational(BigInt(value));
|
|
4136
|
+
const r = Rational.fromNumber(value, 10_000);
|
|
4137
|
+
return r.toNumber() === value ? r : null;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
function toRatio(e: Expr, ctx: RatCtx): Ratio {
|
|
4141
|
+
const atom = (x: Expr): Ratio => {
|
|
4142
|
+
const i = atomIndex(ctx, x);
|
|
4143
|
+
const exps: number[] = new Array(i + 1).fill(0);
|
|
4144
|
+
exps[i] = 1;
|
|
4145
|
+
return ratioFromPoly([{ exps, c: Rational.One }]);
|
|
4146
|
+
};
|
|
4147
|
+
switch (e.type) {
|
|
4148
|
+
case "const": {
|
|
4149
|
+
const r = exactRational(e.value);
|
|
4150
|
+
return r ? ratioFromPoly(polyConst(r)) : atom(e);
|
|
4151
|
+
}
|
|
4152
|
+
case "var":
|
|
4153
|
+
return atom(e);
|
|
4154
|
+
case "neg": {
|
|
4155
|
+
const a = toRatio(e.arg, ctx);
|
|
4156
|
+
return { n: polyNeg(a.n), d: a.d };
|
|
4157
|
+
}
|
|
4158
|
+
case "add":
|
|
4159
|
+
return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), false);
|
|
4160
|
+
case "sub":
|
|
4161
|
+
return ratioAdd(toRatio(e.left, ctx), toRatio(e.right, ctx), true);
|
|
4162
|
+
case "mul":
|
|
4163
|
+
return ratioMul(toRatio(e.left, ctx), toRatio(e.right, ctx));
|
|
4164
|
+
case "div":
|
|
4165
|
+
return ratioDiv(toRatio(e.left, ctx), toRatio(e.right, ctx));
|
|
4166
|
+
case "pow": {
|
|
4167
|
+
if (e.exp.type === "const" && Number.isInteger(e.exp.value)) {
|
|
4168
|
+
return ratioPow(toRatio(e.base, ctx), e.exp.value);
|
|
4169
|
+
}
|
|
4170
|
+
return atom(pow(normalizeRational(e.base), normalizeRational(e.exp)));
|
|
4171
|
+
}
|
|
4172
|
+
case "func":
|
|
4173
|
+
return atom(fn(e.name, normalizeRational(e.arg)));
|
|
4174
|
+
case "call2":
|
|
4175
|
+
return atom(call2(e.name, normalizeRational(e.left), normalizeRational(e.right)));
|
|
4176
|
+
case "cmp":
|
|
4177
|
+
return atom(cmp(e.op, normalizeRational(e.left), normalizeRational(e.right)));
|
|
4178
|
+
case "piecewise":
|
|
4179
|
+
return atom(
|
|
4180
|
+
piecewise(
|
|
4181
|
+
e.branches.map((br) => ({ cond: normalizeRational(br.cond), expr: normalizeRational(br.expr) })),
|
|
4182
|
+
normalizeRational(e.otherwise),
|
|
4183
|
+
),
|
|
4184
|
+
);
|
|
4185
|
+
case "sum":
|
|
4186
|
+
return atom(sumExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)));
|
|
4187
|
+
case "product":
|
|
4188
|
+
return atom(
|
|
4189
|
+
productExpr(e.variable, normalizeRational(e.from), normalizeRational(e.to), normalizeRational(e.body)),
|
|
4190
|
+
);
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
function bigGcd(a: bigint, b: bigint): bigint {
|
|
4195
|
+
let x = a < 0n ? -a : a;
|
|
4196
|
+
let y = b < 0n ? -b : b;
|
|
4197
|
+
while (y) [x, y] = [y, x % y];
|
|
4198
|
+
return x;
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
/** Cancel common factors, then scale to coprime integer coefficients with a positive denominator lead. */
|
|
4202
|
+
function ratioReduce(r: Ratio): Ratio {
|
|
4203
|
+
if (r.n.length === 0) return ratioFromPoly([]);
|
|
4204
|
+
let { n, d } = r;
|
|
4205
|
+
const m = monomialContent([...n, ...d]);
|
|
4206
|
+
if (m.some((e) => e > 0)) {
|
|
4207
|
+
n = polyDivMonomial(n, m);
|
|
4208
|
+
d = polyDivMonomial(d, m);
|
|
4209
|
+
}
|
|
4210
|
+
if (!polyIsConst(d)) {
|
|
4211
|
+
const g = polyGcd(n, d);
|
|
4212
|
+
if (!polyIsConst(g)) {
|
|
4213
|
+
n = polyDivMod(n, g).q;
|
|
4214
|
+
d = polyDivMod(d, g).q;
|
|
4215
|
+
} else {
|
|
4216
|
+
// Several atoms: still cancel when one side divides the other exactly.
|
|
4217
|
+
const dn = polyDivMod(n, d);
|
|
4218
|
+
if (dn.r.length === 0) {
|
|
4219
|
+
n = dn.q;
|
|
4220
|
+
d = polyConst(Rational.One);
|
|
4221
|
+
} else {
|
|
4222
|
+
const nd = polyDivMod(d, n);
|
|
4223
|
+
if (nd.r.length === 0) {
|
|
4224
|
+
d = nd.q;
|
|
4225
|
+
n = polyConst(Rational.One);
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
if (polyIsConst(d)) {
|
|
4231
|
+
const c = d[0].c;
|
|
4232
|
+
return { n: n.map((t) => ({ exps: t.exps, c: t.c.divide(c) })), d: polyConst(Rational.One) };
|
|
4233
|
+
}
|
|
4234
|
+
// integer coefficients: multiply through by the lcm of the denominators, divide by the content
|
|
4235
|
+
let lcm = 1n;
|
|
4236
|
+
for (const t of [...n, ...d]) {
|
|
4237
|
+
const den = t.c.denominator;
|
|
4238
|
+
lcm = (lcm / bigGcd(lcm, den)) * den;
|
|
4239
|
+
}
|
|
4240
|
+
const scale = new Rational(lcm);
|
|
4241
|
+
n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
|
|
4242
|
+
d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(scale) }));
|
|
4243
|
+
let content = 0n;
|
|
4244
|
+
for (const t of [...n, ...d]) content = bigGcd(content, t.c.numerator);
|
|
4245
|
+
if (content > 1n) {
|
|
4246
|
+
const inv = new Rational(1n, content);
|
|
4247
|
+
n = n.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
|
|
4248
|
+
d = d.map((t) => ({ exps: t.exps, c: t.c.multiply(inv) }));
|
|
4249
|
+
}
|
|
4250
|
+
if (d[0].c.sign < 0) {
|
|
4251
|
+
n = polyNeg(n);
|
|
4252
|
+
d = polyNeg(d);
|
|
4253
|
+
}
|
|
4254
|
+
return { n, d };
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
function monomialExpr(exps: number[], ctx: RatCtx): Expr | null {
|
|
4258
|
+
let out: Expr | null = null;
|
|
4259
|
+
for (let i = 0; i < exps.length; i++) {
|
|
4260
|
+
const e = exps[i] ?? 0;
|
|
4261
|
+
if (e === 0) continue;
|
|
4262
|
+
const factor = e === 1 ? ctx.atoms[i] : pow(ctx.atoms[i], num(e));
|
|
4263
|
+
out = out ? mul(out, factor) : factor;
|
|
4264
|
+
}
|
|
4265
|
+
return out;
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
/** A polynomial as a descending-degree sum; fractional coefficients print as `p*m/q`. */
|
|
4269
|
+
function polyToTree(p: Poly, ctx: RatCtx): Expr {
|
|
4270
|
+
if (p.length === 0) return num(0);
|
|
4271
|
+
const ordered = [...p].sort((a, b) => {
|
|
4272
|
+
const da = a.exps.reduce((s, x) => s + x, 0);
|
|
4273
|
+
const db = b.exps.reduce((s, x) => s + x, 0);
|
|
4274
|
+
return db - da || expsCmp(b.exps, a.exps);
|
|
4275
|
+
});
|
|
4276
|
+
let out: Expr | null = null;
|
|
4277
|
+
for (const t of ordered) {
|
|
4278
|
+
const negative = t.c.sign < 0;
|
|
4279
|
+
const abs = t.c.abs();
|
|
4280
|
+
const mono = monomialExpr(t.exps, ctx);
|
|
4281
|
+
const numer = abs.numerator;
|
|
4282
|
+
let body: Expr;
|
|
4283
|
+
if (!mono) body = num(abs.toNumber());
|
|
4284
|
+
else if (numer === 1n && abs.denominator === 1n) body = mono;
|
|
4285
|
+
else if (abs.denominator === 1n) body = mul(num(Number(numer)), mono);
|
|
4286
|
+
else body = div(numer === 1n ? mono : mul(num(Number(numer)), mono), num(Number(abs.denominator)));
|
|
4287
|
+
if (!out) out = negative ? neg(body) : body;
|
|
4288
|
+
else out = negative ? sub(out, body) : add(out, body);
|
|
4289
|
+
}
|
|
4290
|
+
return out as Expr;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
function nodeCount(e: Expr): number {
|
|
4294
|
+
switch (e.type) {
|
|
4295
|
+
case "const":
|
|
4296
|
+
case "var":
|
|
4297
|
+
return 1;
|
|
4298
|
+
case "neg":
|
|
4299
|
+
case "func":
|
|
4300
|
+
return 1 + nodeCount(e.arg);
|
|
4301
|
+
case "pow":
|
|
4302
|
+
return 1 + nodeCount(e.base) + nodeCount(e.exp);
|
|
4303
|
+
case "piecewise":
|
|
4304
|
+
return 1 + nodeCount(e.otherwise) + e.branches.reduce((s, br) => s + nodeCount(br.cond) + nodeCount(br.expr), 0);
|
|
4305
|
+
case "sum":
|
|
4306
|
+
case "product":
|
|
4307
|
+
return 1 + nodeCount(e.from) + nodeCount(e.to) + nodeCount(e.body);
|
|
4308
|
+
default:
|
|
4309
|
+
return 1 + nodeCount(e.left) + nodeCount(e.right);
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
/**
|
|
4314
|
+
* Expand `e` as a rational function and cancel common factors, returning the result only
|
|
4315
|
+
* when it has strictly fewer nodes than `e`; otherwise returns `e` itself.
|
|
4316
|
+
*/
|
|
4317
|
+
function normalizeRational(e: Expr): Expr {
|
|
4318
|
+
if (e.type === "const" || e.type === "var") return e;
|
|
4319
|
+
try {
|
|
4320
|
+
const ctx: RatCtx = { atoms: [], keys: [] };
|
|
4321
|
+
const reduced = ratioReduce(toRatio(e, ctx));
|
|
4322
|
+
const numer = polyToTree(reduced.n, ctx);
|
|
4323
|
+
const candidate = polyIsConst(reduced.d) ? numer : div(numer, polyToTree(reduced.d, ctx));
|
|
4324
|
+
return nodeCount(candidate) < nodeCount(e) ? candidate : e;
|
|
4325
|
+
} catch (err) {
|
|
4326
|
+
if (err instanceof RatBail) return e;
|
|
4327
|
+
throw err;
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
|
|
3813
4331
|
// -- equation solving & factoring ----------------------------------------------
|
|
3814
4332
|
/** Best-effort exact `Expr` for a numeric value: an integer, a reduced fraction, or (as a last resort) the raw float. */
|
|
3815
4333
|
function toExactExpr(value: number): Expr {
|
|
@@ -3841,7 +4359,7 @@ function linearCoeffsForSystem(eq: Expr, variables: string[]): number[] | null {
|
|
|
3841
4359
|
if (!Number.isFinite(constant)) return null;
|
|
3842
4360
|
const coeffs: number[] = [];
|
|
3843
4361
|
for (const name of variables) {
|
|
3844
|
-
const derivative =
|
|
4362
|
+
const derivative = simplifyCore(diff(eq, name));
|
|
3845
4363
|
if (variables.some((other) => containsVar(derivative, other))) return null;
|
|
3846
4364
|
const coeff = evalExpr(derivative, zeroEnv);
|
|
3847
4365
|
if (!Number.isFinite(coeff)) return null;
|
|
@@ -3870,7 +4388,7 @@ function polynomialCoeffs(expr: Expr, variable: string, maxDegree: number): numb
|
|
|
3870
4388
|
// derivative of x^0 (`0·x^-1`) evaluates to `0·Infinity = NaN` at x=0
|
|
3871
4389
|
// even though it is symbolically zero — simplifying collapses `0·anything`
|
|
3872
4390
|
// to `0` before that indeterminate form can arise.
|
|
3873
|
-
term =
|
|
4391
|
+
term = simplifyCore(diff(term, variable));
|
|
3874
4392
|
}
|
|
3875
4393
|
while (coeffs.length > 1 && Math.abs(coeffs[coeffs.length - 1]) < 1e-9) coeffs.pop();
|
|
3876
4394
|
for (const p of [1.3, -2.1, 3.7]) {
|
|
@@ -4058,7 +4576,7 @@ function factorPolynomial(coeffsIn: number[], variable: string): Expr {
|
|
|
4058
4576
|
} else if (Math.abs(remaining[0] - 1) > 1e-9) {
|
|
4059
4577
|
result = mul(result, toExactExpr(remaining[0]));
|
|
4060
4578
|
}
|
|
4061
|
-
return
|
|
4579
|
+
return simplifyCore(result);
|
|
4062
4580
|
}
|
|
4063
4581
|
|
|
4064
4582
|
// -- limits ---------------------------------------------------------------------
|
|
@@ -4083,8 +4601,8 @@ function limitAt(e: Expr, x: string, point: number, direction: "left" | "right"
|
|
|
4083
4601
|
// operand verbatim into the result) and a limit needing more than a
|
|
4084
4602
|
// couple of rounds (e.g. most limits at infinity) blows up memory
|
|
4085
4603
|
// long before the depth<12 cap is reached.
|
|
4086
|
-
const df =
|
|
4087
|
-
const dg =
|
|
4604
|
+
const df = simplifyCore(diff(e.left, x));
|
|
4605
|
+
const dg = simplifyCore(diff(e.right, x));
|
|
4088
4606
|
return limitAt(div(df, dg), x, point, direction, depth + 1);
|
|
4089
4607
|
}
|
|
4090
4608
|
}
|
|
@@ -4092,12 +4610,19 @@ function limitAt(e: Expr, x: string, point: number, direction: "left" | "right"
|
|
|
4092
4610
|
}
|
|
4093
4611
|
|
|
4094
4612
|
// -- recursive-descent parser ----------------------------------------------
|
|
4613
|
+
/** Every name the parser treats as a function, longest first, so `asinh(` is not read as `a*sinh(`. */
|
|
4614
|
+
const FUNCTION_NAMES_LONGEST_FIRST: string[] = [...FUNCTION_NAMES].sort((a, b) => b.length - a.length);
|
|
4615
|
+
|
|
4095
4616
|
class Parser {
|
|
4096
4617
|
private readonly s: string;
|
|
4618
|
+
private readonly implicit: boolean;
|
|
4097
4619
|
private pos = 0;
|
|
4098
4620
|
|
|
4099
|
-
constructor(input: string) {
|
|
4100
|
-
this.
|
|
4621
|
+
constructor(input: string, options: ParseOptions = {}) {
|
|
4622
|
+
this.implicit = options.implicitMultiplication === true;
|
|
4623
|
+
// With implicit multiplication, white space between two numbers is a product (`2 3`), not one number.
|
|
4624
|
+
const spaced = this.implicit ? input.replace(/(\d)\s+(?=[\d.])/g, "$1*") : input;
|
|
4625
|
+
this.s = spaced.replace(/\s+/g, "");
|
|
4101
4626
|
}
|
|
4102
4627
|
|
|
4103
4628
|
parse(): Expr {
|
|
@@ -4218,12 +4743,34 @@ class Parser {
|
|
|
4218
4743
|
|
|
4219
4744
|
private term(): Expr {
|
|
4220
4745
|
let left = this.factor();
|
|
4221
|
-
|
|
4222
|
-
const
|
|
4223
|
-
|
|
4224
|
-
|
|
4746
|
+
for (;;) {
|
|
4747
|
+
const ch = this.peek();
|
|
4748
|
+
if (ch === "*" || ch === "/") {
|
|
4749
|
+
this.pos++;
|
|
4750
|
+
const right = this.factor();
|
|
4751
|
+
left = ch === "*" ? mul(left, right) : div(left, right);
|
|
4752
|
+
} else if (this.implicit && /[a-zA-Z0-9.(]/.test(ch)) {
|
|
4753
|
+
left = mul(left, this.factor()); // juxtaposition: `2x`, `x y`, `2(x+1)`
|
|
4754
|
+
} else {
|
|
4755
|
+
return left;
|
|
4756
|
+
}
|
|
4225
4757
|
}
|
|
4226
|
-
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
/**
|
|
4761
|
+
* The identifier at `this.pos` under implicit multiplication: a function name followed by `(`, `pi`, or a
|
|
4762
|
+
* single letter with an optional `_subscript`. A letter run like `xy` is therefore read one letter at a time.
|
|
4763
|
+
*/
|
|
4764
|
+
private implicitIdentifier(): string {
|
|
4765
|
+
const rest = this.s.slice(this.pos);
|
|
4766
|
+
const run = /^[a-zA-Z]+/.exec(rest)?.[0] ?? "";
|
|
4767
|
+
const call = FUNCTION_NAMES_LONGEST_FIRST.find((f) => rest.startsWith(f) && rest[f.length] === "(");
|
|
4768
|
+
if (call) return call;
|
|
4769
|
+
const bare = FUNCTION_NAMES_LONGEST_FIRST.find((f) => f.length > 1 && run.startsWith(f));
|
|
4770
|
+
if (bare) throw new Error(`Function ${bare} needs parentheses: ${bare}(...)`);
|
|
4771
|
+
if (rest.startsWith("pi")) return "pi";
|
|
4772
|
+
const sub = /^[a-zA-Z]_(?:\d+|[a-zA-Z])/.exec(rest);
|
|
4773
|
+
return sub ? sub[0] : rest[0];
|
|
4227
4774
|
}
|
|
4228
4775
|
|
|
4229
4776
|
private factor(): Expr {
|
|
@@ -4261,9 +4808,11 @@ class Parser {
|
|
|
4261
4808
|
return num(Number(numMatch[0]));
|
|
4262
4809
|
}
|
|
4263
4810
|
// identifier (function or variable or constant)
|
|
4264
|
-
const idMatch =
|
|
4811
|
+
const idMatch = this.implicit
|
|
4812
|
+
? /^[a-zA-Z_]/.exec(this.s.slice(this.pos))
|
|
4813
|
+
: /^[a-zA-Z_]\w*/.exec(this.s.slice(this.pos));
|
|
4265
4814
|
if (idMatch) {
|
|
4266
|
-
const name = idMatch[0];
|
|
4815
|
+
const name = this.implicit ? this.implicitIdentifier() : idMatch[0];
|
|
4267
4816
|
this.pos += name.length;
|
|
4268
4817
|
if ((name === "log" || name === "clamp") && this.peek() === "(") {
|
|
4269
4818
|
this.pos++;
|