@classytic/stage 0.1.0 → 0.3.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/LICENSE +1 -1
- package/README.md +7 -3
- package/dist/assets/index.mjs +0 -1
- package/dist/assets/kit/glyphs.d.mts +5 -5
- package/dist/assets/kit/glyphs.mjs +4 -4
- package/dist/builder/Palette.mjs +50 -89
- package/dist/builder/SceneBuilder.mjs +18 -76
- package/dist/builder/editor.d.mts +2 -1
- package/dist/builder/editor.mjs +14 -4
- package/dist/builder/tools.d.mts +1 -1
- package/dist/builder/tools.mjs +1 -1
- package/dist/chem/index.d.mts +5 -5
- package/dist/chem/index.mjs +4 -4
- package/dist/circuit/index.d.mts +92 -0
- package/dist/circuit/index.mjs +333 -0
- package/dist/core/clock.d.mts +1 -1
- package/dist/core/clock.mjs +3 -3
- package/dist/core/control.d.mts +1 -1
- package/dist/core/control.mjs +2 -2
- package/dist/core/coords.d.mts +4 -4
- package/dist/core/coords.mjs +1 -1
- package/dist/core/index.d.mts +2 -1
- package/dist/core/index.mjs +2 -1
- package/dist/core/learner.d.mts +1 -1
- package/dist/core/learner.mjs +1 -1
- package/dist/core/math.d.mts +26 -0
- package/dist/core/math.mjs +37 -0
- package/dist/core/motion.d.mts +6 -6
- package/dist/core/motion.mjs +6 -6
- package/dist/core/richText.d.mts +2 -2
- package/dist/core/vec.d.mts +1 -1
- package/dist/field/index.d.mts +11 -3
- package/dist/field/index.mjs +20 -3
- package/dist/finance/bizsim.d.mts +93 -0
- package/dist/finance/bizsim.mjs +117 -0
- package/dist/finance/index.d.mts +118 -0
- package/dist/finance/index.mjs +203 -0
- package/dist/index.d.mts +6 -3
- package/dist/index.mjs +6 -4
- package/dist/interaction/MovableDot.d.mts +17 -1
- package/dist/interaction/MovableDot.mjs +160 -43
- package/dist/interaction/index.d.mts +2 -1
- package/dist/interaction/index.mjs +2 -1
- package/dist/interaction/useDraggable.d.mts +1 -1
- package/dist/interaction/useDraggable.mjs +55 -5
- package/dist/interaction/usePressSpring.d.mts +24 -0
- package/dist/interaction/usePressSpring.mjs +67 -0
- package/dist/logic/ast.d.mts +1 -1
- package/dist/logic/index.mjs +8 -3
- package/dist/logic/minimize.d.mts +2 -2
- package/dist/logic/minimize.mjs +56 -18
- package/dist/logic/table.mjs +1 -1
- package/dist/math/ast.d.mts +1 -1
- package/dist/math/ast.mjs +1 -1
- package/dist/math/calculus.d.mts +22 -1
- package/dist/math/calculus.mjs +224 -11
- package/dist/math/compile.d.mts +8 -0
- package/dist/math/compile.mjs +40 -0
- package/dist/math/defs.mjs +1 -1
- package/dist/math/index.d.mts +5 -4
- package/dist/math/index.mjs +5 -4
- package/dist/math/latex.mjs +8 -0
- package/dist/math/parse.mjs +2 -2
- package/dist/math/tokenize.mjs +1 -1
- package/dist/primitives/CanvasLayer.mjs +3 -3
- package/dist/primitives/Dot.d.mts +2 -15
- package/dist/primitives/Dot.mjs +6 -4
- package/dist/primitives/Grid.d.mts +41 -14
- package/dist/primitives/Grid.mjs +100 -22
- package/dist/primitives/Label.d.mts +2 -15
- package/dist/primitives/Label.mjs +4 -3
- package/dist/primitives/Lines.d.mts +4 -32
- package/dist/primitives/Lines.mjs +10 -8
- package/dist/primitives/Shapes.d.mts +5 -43
- package/dist/primitives/Shapes.mjs +12 -10
- package/dist/primitives/Tex.mjs +2 -2
- package/dist/primitives/index.d.mts +2 -2
- package/dist/primitives/index.mjs +2 -2
- package/dist/primitives/props.mjs +31 -0
- package/dist/scene/Scene.d.mts +6 -1
- package/dist/scene/Scene.mjs +16 -41
- package/dist/scene/assets.d.mts +2 -2
- package/dist/scene/assets.mjs +2 -2
- package/dist/scene/commands.d.mts +10 -1
- package/dist/scene/commands.mjs +42 -2
- package/dist/scene/evaluators.mjs +2 -2
- package/dist/scene/index.d.mts +2 -2
- package/dist/scene/index.mjs +2 -2
- package/dist/scene/migrate.mjs +1 -1
- package/dist/scene/sims.mjs +1 -1
- package/dist/scene/types.d.mts +4 -4
- package/dist/sim/particles.d.mts +2 -2
- package/dist/sim/rate.d.mts +4 -4
- package/dist/sim/rate.mjs +1 -1
- package/dist/sim/registry.d.mts +1 -1
- package/dist/sim/sampler.d.mts +5 -5
- package/dist/sim/thermal.d.mts +3 -3
- package/dist/sim/thermal.mjs +2 -2
- package/dist/sim/types.d.mts +2 -2
- package/dist/sim/wave.d.mts +2 -2
- package/dist/sim/wave.mjs +2 -2
- package/dist/steps/index.d.mts +1 -1
- package/dist/steps/index.mjs +3 -3
- package/dist/thermo/index.d.mts +1 -1
- package/dist/thermo/index.mjs +1 -1
- package/dist/view/Stage.mjs +6 -13
- package/dist/view/useInView.mjs +1 -1
- package/package.json +37 -25
- package/styles.css +141 -1
- package/dist/assets/kit/index.mjs +0 -4
package/dist/logic/minimize.mjs
CHANGED
|
@@ -3,9 +3,9 @@ import { truthTable } from "./table.mjs";
|
|
|
3
3
|
|
|
4
4
|
//#region src/logic/minimize.ts
|
|
5
5
|
/**
|
|
6
|
-
* Boolean minimisation
|
|
6
|
+
* Boolean minimisation, Quine–McCluskey: the algorithm a Karnaugh map performs
|
|
7
7
|
* by eye. Given a function's minterms (the rows where it is 1) over n variables,
|
|
8
|
-
* it finds the PRIME IMPLICANTS (maximal "don't-care cubes"
|
|
8
|
+
* it finds the PRIME IMPLICANTS (maximal "don't-care cubes", groups on the
|
|
9
9
|
* K-map) and a minimal SUM-OF-PRODUCTS cover (essential PIs + a greedy fill).
|
|
10
10
|
* Pure + dependency-free, the boolean sibling of `simplify` in the expr engine;
|
|
11
11
|
* the K-map lab renders these cubes as the coloured groups, and the same routine
|
|
@@ -31,7 +31,7 @@ function cubeCovers(c, n) {
|
|
|
31
31
|
for (let m = 0; m < total; m++) if ((m & c.mask) === c.value) out.push(m);
|
|
32
32
|
return out;
|
|
33
33
|
}
|
|
34
|
-
/** The product term for a cube
|
|
34
|
+
/** The product term for a cube, a literal per FIXED bit (MSB = first var). */
|
|
35
35
|
function cubeTerm(c, vars, neg = "¬", join = " ∧ ") {
|
|
36
36
|
const n = vars.length;
|
|
37
37
|
const lits = [];
|
|
@@ -86,7 +86,56 @@ function coverageOf(pis, required) {
|
|
|
86
86
|
});
|
|
87
87
|
return map;
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
89
|
+
/** Keep only the minimal PI-sets (drop any that is a strict superset of another). */
|
|
90
|
+
function absorb(products) {
|
|
91
|
+
const uniq = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const p of products) {
|
|
93
|
+
const s = new Set(p);
|
|
94
|
+
uniq.set([...s].sort((a, b) => a - b).join(","), s);
|
|
95
|
+
}
|
|
96
|
+
const sets = [...uniq.values()];
|
|
97
|
+
return sets.filter((s) => !sets.some((o) => o !== s && o.size < s.size && [...o].every((x) => s.has(x)))).map((s) => [...s]);
|
|
98
|
+
}
|
|
99
|
+
/** Greedy fallback: take the PI covering the most still-uncovered minterms, repeat. */
|
|
100
|
+
function greedyCover(uncovered, cov, candidates) {
|
|
101
|
+
const left = new Set(uncovered);
|
|
102
|
+
const picked = [];
|
|
103
|
+
while (left.size) {
|
|
104
|
+
let best = -1, bestCount = 0;
|
|
105
|
+
for (const i of candidates) {
|
|
106
|
+
if (picked.includes(i)) continue;
|
|
107
|
+
const c = cov.get(i).filter((m) => left.has(m)).length;
|
|
108
|
+
if (c > bestCount) {
|
|
109
|
+
bestCount = c;
|
|
110
|
+
best = i;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (best < 0) break;
|
|
114
|
+
picked.push(best);
|
|
115
|
+
for (const m of cov.get(best)) left.delete(m);
|
|
116
|
+
}
|
|
117
|
+
return picked;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Petrick's method: the EXACT minimum-size cover of the remaining minterms. Each minterm
|
|
121
|
+
* contributes a clause (the PIs covering it); multiplying the clauses out (with absorption)
|
|
122
|
+
* enumerates every irredundant cover, and we keep the one with the fewest PIs. Falls back to
|
|
123
|
+
* greedy if the residual is large enough that the expansion could blow up (K-map labs are small).
|
|
124
|
+
*/
|
|
125
|
+
function petrickCover(uncovered, cov, candidates) {
|
|
126
|
+
if (candidates.length > 18) return greedyCover(uncovered, cov, candidates);
|
|
127
|
+
let products = [[]];
|
|
128
|
+
for (const m of uncovered) {
|
|
129
|
+
const clause = candidates.filter((i) => cov.get(i).includes(m));
|
|
130
|
+
if (clause.length === 0) return greedyCover(uncovered, cov, candidates);
|
|
131
|
+
const next = [];
|
|
132
|
+
for (const p of products) for (const pi of clause) next.push(p.includes(pi) ? p : [...p, pi]);
|
|
133
|
+
products = absorb(next);
|
|
134
|
+
if (products.length > 4e3) return greedyCover(uncovered, cov, candidates);
|
|
135
|
+
}
|
|
136
|
+
return products.reduce((best, p) => p.length < best.length ? p : best, products[0] ?? []);
|
|
137
|
+
}
|
|
138
|
+
/** A minimum SOP cover: essential prime implicants + Petrick's exact cover of the rest. */
|
|
90
139
|
function minimalCover(minterms, n, dontCares = []) {
|
|
91
140
|
const required = new Set(minterms);
|
|
92
141
|
if (required.size === 0) return [];
|
|
@@ -100,20 +149,9 @@ function minimalCover(minterms, n, dontCares = []) {
|
|
|
100
149
|
if (owners.length === 1) chosen.add(owners[0]);
|
|
101
150
|
}
|
|
102
151
|
for (const i of chosen) for (const m of cov.get(i)) uncovered.delete(m);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (chosen.has(i)) return;
|
|
107
|
-
const c = cov.get(i).filter((m) => uncovered.has(m)).length;
|
|
108
|
-
if (c > bestCount) {
|
|
109
|
-
bestCount = c;
|
|
110
|
-
best = i;
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
if (best < 0 || bestCount <= 0) break;
|
|
114
|
-
chosen.add(best);
|
|
115
|
-
for (const m of cov.get(best)) uncovered.delete(m);
|
|
116
|
-
}
|
|
152
|
+
if (uncovered.size === 0) return [...chosen].map((i) => pis[i]);
|
|
153
|
+
const candidates = pis.map((_, i) => i).filter((i) => !chosen.has(i));
|
|
154
|
+
for (const i of petrickCover([...uncovered], cov, candidates)) chosen.add(i);
|
|
117
155
|
return [...chosen].map((i) => pis[i]);
|
|
118
156
|
}
|
|
119
157
|
/** Full minimisation of a formula (or explicit minterms) → cover + SOP string. */
|
package/dist/logic/table.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { evalBool, freeVars } from "./ast.mjs";
|
|
|
4
4
|
/**
|
|
5
5
|
* Truth tables + the things derived from them: classification (tautology /
|
|
6
6
|
* contradiction / contingency), logical equivalence, and DNF/CNF. Everything is
|
|
7
|
-
* a fold over the 2ⁿ assignments
|
|
7
|
+
* a fold over the 2ⁿ assignments, the truth table is the source of truth, so
|
|
8
8
|
* equivalence is just "same output column" (no algebraic law engine needed).
|
|
9
9
|
*/
|
|
10
10
|
/** Enumerate all 2^vars assignments (MSB = first var), evaluating `n` on each. */
|
package/dist/math/ast.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/math/ast.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* expr/ast
|
|
3
|
+
* expr/ast, the Abstract Syntax Tree: node types, evaluation, free-variable
|
|
4
4
|
* collection, and small builder helpers. The AST is the shared representation
|
|
5
5
|
* that evaluation, differentiation, simplification, and LaTeX printing all
|
|
6
6
|
* operate on.
|
package/dist/math/ast.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { CONSTANTS, FN1, FN2 } from "./defs.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/math/ast.ts
|
|
4
4
|
/**
|
|
5
|
-
* expr/ast
|
|
5
|
+
* expr/ast, the Abstract Syntax Tree: node types, evaluation, free-variable
|
|
6
6
|
* collection, and small builder helpers. The AST is the shared representation
|
|
7
7
|
* that evaluation, differentiation, simplification, and LaTeX printing all
|
|
8
8
|
* operate on.
|
package/dist/math/calculus.d.mts
CHANGED
|
@@ -3,7 +3,28 @@ import { Node } from "./ast.mjs";
|
|
|
3
3
|
//#region src/math/calculus.d.ts
|
|
4
4
|
/** Exact symbolic derivative, or `null` if a non-differentiable node is present. */
|
|
5
5
|
declare function differentiate(node: Node, x: string): Node | null;
|
|
6
|
+
/**
|
|
7
|
+
* Exact symbolic antiderivative with respect to `x`, or `null` when this engine cannot do it.
|
|
8
|
+
*
|
|
9
|
+
* The constant of integration is NOT included. That is deliberate: `+ c` is the single most
|
|
10
|
+
* commonly dropped mark in the topic, so it belongs in the lesson's own working where a learner
|
|
11
|
+
* has to write it, not silently bolted on by the engine.
|
|
12
|
+
*
|
|
13
|
+
* Covers the Cambridge P1/P3 toolkit: linearity, the power rule including the `1/x → ln|x|`
|
|
14
|
+
* exception, and any of the above composed with a LINEAR inner function. Returns null for
|
|
15
|
+
* integration by parts, by non-linear substitution, and partial fractions, so a caller can fall
|
|
16
|
+
* back to the numerical `integrate` in `core/numeric` and show area rather than a wrong formula.
|
|
17
|
+
*/
|
|
18
|
+
declare function integrate(node: Node, x: string): Node | null;
|
|
19
|
+
/**
|
|
20
|
+
* A definite integral evaluated exactly: `F(b) - F(a)`.
|
|
21
|
+
*
|
|
22
|
+
* Returned as a NUMBER rather than a node, because that is what a lesson compares a learner's
|
|
23
|
+
* answer against. Null when the antiderivative is not elementary, or when either limit lands
|
|
24
|
+
* somewhere the expression is not defined.
|
|
25
|
+
*/
|
|
26
|
+
declare function definiteIntegral(node: Node, x: string, lower: number, upper: number): number | null;
|
|
6
27
|
/** Fold constants and trivial identities so derivative output is readable. */
|
|
7
28
|
declare function simplify(node: Node): Node;
|
|
8
29
|
//#endregion
|
|
9
|
-
export { differentiate, simplify };
|
|
30
|
+
export { definiteIntegral, differentiate, integrate, simplify };
|
package/dist/math/calculus.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { FN1, FN2 } from "./defs.mjs";
|
|
2
|
-
import { add, call, div, mul, neg, num, pow, sub } from "./ast.mjs";
|
|
2
|
+
import { add, call, div, evaluate, mul, neg, num, pow, sub, variable } from "./ast.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/math/calculus.ts
|
|
5
5
|
/**
|
|
6
|
-
* expr/calculus
|
|
6
|
+
* expr/calculus, symbolic differentiation + algebraic simplification on the AST.
|
|
7
7
|
*
|
|
8
8
|
* `differentiate(node, x)` returns the exact derivative as a new AST, or `null`
|
|
9
9
|
* when the expression contains something not symbolically differentiable
|
|
@@ -70,7 +70,201 @@ function differentiate(node, x) {
|
|
|
70
70
|
default: return null;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Outer antiderivative of a 1-arg function, given its argument node `u`.
|
|
75
|
+
*
|
|
76
|
+
* Mirrors DERIV above. Only the functions whose antiderivative is itself elementary appear:
|
|
77
|
+
* `ln` and the inverse trig functions integrate by parts into forms this table cannot express,
|
|
78
|
+
* so they are absent and `integrate` returns null for them rather than guessing.
|
|
79
|
+
*/
|
|
80
|
+
const INTEG = {
|
|
81
|
+
sin: (u) => neg(call("cos", u)),
|
|
82
|
+
cos: (u) => call("sin", u),
|
|
83
|
+
exp: (u) => call("exp", u),
|
|
84
|
+
sinh: (u) => call("cosh", u),
|
|
85
|
+
cosh: (u) => call("sinh", u)
|
|
86
|
+
};
|
|
87
|
+
/** `true` when `node` holds no occurrence of `x`, so it is a constant for this integral. */
|
|
88
|
+
function isConstantIn(node, x) {
|
|
89
|
+
return !freeVarsHas(node, x);
|
|
90
|
+
}
|
|
91
|
+
function freeVarsHas(node, x) {
|
|
92
|
+
switch (node.type) {
|
|
93
|
+
case "num": return false;
|
|
94
|
+
case "var": return node.name === x;
|
|
95
|
+
case "neg": return freeVarsHas(node.arg, x);
|
|
96
|
+
case "binary": return freeVarsHas(node.left, x) || freeVarsHas(node.right, x);
|
|
97
|
+
case "call": return node.args.some((a) => freeVarsHas(a, x));
|
|
98
|
+
default: return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The constant `a` when `u` is linear in `x` (that is, `u = a·x + b`), else null.
|
|
103
|
+
*
|
|
104
|
+
* This is what makes the whole linear-substitution family work with one rule instead of a pattern
|
|
105
|
+
* per case: if `du/dx` simplifies to a non-zero NUMBER, then `∫f(u) dx = F(u)/a`. Deriving it from
|
|
106
|
+
* `differentiate` rather than matching `a*x+b` by shape means `(3-2x)^5` and `(-2x+3)^5` and
|
|
107
|
+
* `3-2*x` all work without three separate matchers.
|
|
108
|
+
*/
|
|
109
|
+
function linearFactor(u, x) {
|
|
110
|
+
const d = differentiate(u, x);
|
|
111
|
+
if (!d) return null;
|
|
112
|
+
const s = simplify(d);
|
|
113
|
+
return s.type === "num" && s.value !== 0 ? s.value : null;
|
|
114
|
+
}
|
|
115
|
+
/** Divide by a constant, without emitting the noise of `expr / 1`. */
|
|
116
|
+
const overConst = (node, a) => a === 1 ? node : div(node, num(a));
|
|
117
|
+
/**
|
|
118
|
+
* Exact symbolic antiderivative with respect to `x`, or `null` when this engine cannot do it.
|
|
119
|
+
*
|
|
120
|
+
* The constant of integration is NOT included. That is deliberate: `+ c` is the single most
|
|
121
|
+
* commonly dropped mark in the topic, so it belongs in the lesson's own working where a learner
|
|
122
|
+
* has to write it, not silently bolted on by the engine.
|
|
123
|
+
*
|
|
124
|
+
* Covers the Cambridge P1/P3 toolkit: linearity, the power rule including the `1/x → ln|x|`
|
|
125
|
+
* exception, and any of the above composed with a LINEAR inner function. Returns null for
|
|
126
|
+
* integration by parts, by non-linear substitution, and partial fractions, so a caller can fall
|
|
127
|
+
* back to the numerical `integrate` in `core/numeric` and show area rather than a wrong formula.
|
|
128
|
+
*/
|
|
129
|
+
function integrate(node, x) {
|
|
130
|
+
if (isConstantIn(node, x)) return mul(node, variable(x));
|
|
131
|
+
switch (node.type) {
|
|
132
|
+
case "var": return div(pow(variable(x), num(2)), num(2));
|
|
133
|
+
case "neg": {
|
|
134
|
+
const inner = integrate(node.arg, x);
|
|
135
|
+
return inner && neg(inner);
|
|
136
|
+
}
|
|
137
|
+
case "binary": {
|
|
138
|
+
const { op, left, right } = node;
|
|
139
|
+
if (op === "+" || op === "-") {
|
|
140
|
+
const l = integrate(left, x);
|
|
141
|
+
const r = integrate(right, x);
|
|
142
|
+
return l && r && (op === "+" ? add(l, r) : sub(l, r));
|
|
143
|
+
}
|
|
144
|
+
if (op === "*") {
|
|
145
|
+
if (isConstantIn(left, x)) {
|
|
146
|
+
const r = integrate(right, x);
|
|
147
|
+
return r && mul(left, r);
|
|
148
|
+
}
|
|
149
|
+
if (isConstantIn(right, x)) {
|
|
150
|
+
const l = integrate(left, x);
|
|
151
|
+
return l && mul(right, l);
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (op === "/") {
|
|
156
|
+
if (isConstantIn(right, x)) {
|
|
157
|
+
const l = integrate(left, x);
|
|
158
|
+
return l && div(l, right);
|
|
159
|
+
}
|
|
160
|
+
if (isConstantIn(left, x)) {
|
|
161
|
+
const a = linearFactor(right, x);
|
|
162
|
+
return a === null ? null : overConst(mul(left, call("ln", call("abs", right))), a);
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
if (op === "^") {
|
|
167
|
+
const exponent = simplify(right);
|
|
168
|
+
if (exponent.type !== "num") return null;
|
|
169
|
+
const n = exponent.value;
|
|
170
|
+
const a = linearFactor(left, x);
|
|
171
|
+
if (a === null) return null;
|
|
172
|
+
if (n === -1) return overConst(call("ln", call("abs", left)), a);
|
|
173
|
+
return overConst(div(pow(left, num(n + 1)), num(n + 1)), a);
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
case "call": {
|
|
178
|
+
const u = node.args[0];
|
|
179
|
+
const rule = INTEG[node.fn];
|
|
180
|
+
if (!rule || !u || node.args.length !== 1) return null;
|
|
181
|
+
const a = linearFactor(u, x);
|
|
182
|
+
return a === null ? null : overConst(rule(u), a);
|
|
183
|
+
}
|
|
184
|
+
default: return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A definite integral evaluated exactly: `F(b) - F(a)`.
|
|
189
|
+
*
|
|
190
|
+
* Returned as a NUMBER rather than a node, because that is what a lesson compares a learner's
|
|
191
|
+
* answer against. Null when the antiderivative is not elementary, or when either limit lands
|
|
192
|
+
* somewhere the expression is not defined.
|
|
193
|
+
*/
|
|
194
|
+
function definiteIntegral(node, x, lower, upper) {
|
|
195
|
+
const F = integrate(node, x);
|
|
196
|
+
if (!F) return null;
|
|
197
|
+
const at = (v) => evaluate(F, { [x]: v });
|
|
198
|
+
const result = at(upper) - at(lower);
|
|
199
|
+
return Number.isFinite(result) ? result : null;
|
|
200
|
+
}
|
|
73
201
|
const isNum = (n, v) => n.type === "num" && (v === void 0 || n.value === v);
|
|
202
|
+
/** A stable structural key, so identical cores group (x and x; x² and x²). */
|
|
203
|
+
function nodeKey(n) {
|
|
204
|
+
switch (n.type) {
|
|
205
|
+
case "num": return `#${n.value}`;
|
|
206
|
+
case "var": return `$${n.name}`;
|
|
207
|
+
case "neg": return `-${nodeKey(n.arg)}`;
|
|
208
|
+
case "binary": return `(${nodeKey(n.left)}${n.op}${nodeKey(n.right)})`;
|
|
209
|
+
case "call": return `${n.fn}(${n.args.map(nodeKey).join(",")})`;
|
|
210
|
+
default: return "?";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Split a term into (numeric coefficient, the rest); core null = a pure number. */
|
|
214
|
+
function splitCoeff(n) {
|
|
215
|
+
if (n.type === "num") return [n.value, null];
|
|
216
|
+
if (n.type === "neg") {
|
|
217
|
+
const [c, core] = splitCoeff(n.arg);
|
|
218
|
+
return [-c, core];
|
|
219
|
+
}
|
|
220
|
+
if (n.type === "binary" && n.op === "*") {
|
|
221
|
+
if (n.left.type === "num") return [n.left.value, n.right];
|
|
222
|
+
if (n.right.type === "num") return [n.right.value, n.left];
|
|
223
|
+
}
|
|
224
|
+
return [1, n];
|
|
225
|
+
}
|
|
226
|
+
/** Flatten a +/- chain into signed (coeff, core) terms. */
|
|
227
|
+
function flattenTerms(node, s, out) {
|
|
228
|
+
if (node.type === "binary" && (node.op === "+" || node.op === "-")) {
|
|
229
|
+
flattenTerms(node.left, s, out);
|
|
230
|
+
flattenTerms(node.right, node.op === "-" ? -s : s, out);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (node.type === "neg") {
|
|
234
|
+
flattenTerms(node.arg, -s, out);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const [coeff, core] = splitCoeff(node);
|
|
238
|
+
out.push({
|
|
239
|
+
coeff: coeff * s,
|
|
240
|
+
core
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/** Combine like terms across `l op r`: 2x + 3x → 5x, x − x → 0, constants merge. */
|
|
244
|
+
function combineLikeTerms(l, r, op) {
|
|
245
|
+
const list = [];
|
|
246
|
+
flattenTerms(l, 1, list);
|
|
247
|
+
flattenTerms(r, op === "-" ? -1 : 1, list);
|
|
248
|
+
const groups = /* @__PURE__ */ new Map();
|
|
249
|
+
for (const t of list) {
|
|
250
|
+
const k = t.core ? nodeKey(t.core) : "";
|
|
251
|
+
const g = groups.get(k);
|
|
252
|
+
if (g) g.coeff += t.coeff;
|
|
253
|
+
else groups.set(k, {
|
|
254
|
+
coeff: t.coeff,
|
|
255
|
+
core: t.core
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
let result = null;
|
|
259
|
+
for (const { coeff, core } of groups.values()) {
|
|
260
|
+
if (Math.abs(coeff) < 1e-12) continue;
|
|
261
|
+
const mag = Math.abs(coeff);
|
|
262
|
+
const body = core === null ? num(mag) : mag === 1 ? core : mul(num(mag), core);
|
|
263
|
+
if (result === null) result = coeff < 0 ? neg(body) : body;
|
|
264
|
+
else result = coeff < 0 ? sub(result, body) : add(result, body);
|
|
265
|
+
}
|
|
266
|
+
return result ?? num(0);
|
|
267
|
+
}
|
|
74
268
|
/** Fold constants and trivial identities so derivative output is readable. */
|
|
75
269
|
function simplify(node) {
|
|
76
270
|
switch (node.type) {
|
|
@@ -90,15 +284,8 @@ function simplify(node) {
|
|
|
90
284
|
const v = fold(op, l.value, r.value);
|
|
91
285
|
if (v !== null && Number.isFinite(v)) return num(v);
|
|
92
286
|
}
|
|
287
|
+
if (op === "+" || op === "-") return combineLikeTerms(l, r, op);
|
|
93
288
|
switch (op) {
|
|
94
|
-
case "+":
|
|
95
|
-
if (isNum(l, 0)) return r;
|
|
96
|
-
if (isNum(r, 0)) return l;
|
|
97
|
-
break;
|
|
98
|
-
case "-":
|
|
99
|
-
if (isNum(r, 0)) return l;
|
|
100
|
-
if (isNum(l, 0)) return simplify(neg(r));
|
|
101
|
-
break;
|
|
102
289
|
case "*":
|
|
103
290
|
if (isNum(l, 0) || isNum(r, 0)) return num(0);
|
|
104
291
|
if (isNum(l, 1)) return r;
|
|
@@ -133,10 +320,36 @@ function simplify(node) {
|
|
|
133
320
|
right: l.left
|
|
134
321
|
};
|
|
135
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Cancel a constant against a fraction's denominator: c·(a/b) → (c/b)·a.
|
|
325
|
+
*
|
|
326
|
+
* `∫3x² dx` is the plainest case. The constant comes out of the integral and the power
|
|
327
|
+
* rule divides by 3, so the honest answer is `3·(x³/3)`. A learner expects `x³`, and
|
|
328
|
+
* only the fully cancelled form is worth showing as the ANSWER.
|
|
329
|
+
*/
|
|
330
|
+
if (l.type === "num" && r.type === "binary" && r.op === "/" && r.right.type === "num") {
|
|
331
|
+
const k = l.value / r.right.value;
|
|
332
|
+
if (Number.isInteger(k)) return k === 1 ? r.left : simplify(mul(num(k), r.left));
|
|
333
|
+
}
|
|
334
|
+
if (r.type === "num" && l.type === "binary" && l.op === "/" && l.right.type === "num") {
|
|
335
|
+
const k = r.value / l.right.value;
|
|
336
|
+
if (Number.isInteger(k)) return k === 1 ? l.left : simplify(mul(num(k), l.left));
|
|
337
|
+
}
|
|
136
338
|
break;
|
|
137
339
|
case "/":
|
|
138
340
|
if (isNum(r, 1)) return l;
|
|
139
341
|
if (isNum(l, 0)) return num(0);
|
|
342
|
+
/**
|
|
343
|
+
* Collapse a nested fraction: (a/b)/c → a/(bc).
|
|
344
|
+
*
|
|
345
|
+
* Integration produces these constantly, because the power rule divides by n+1 and the
|
|
346
|
+
* linear-substitution rule then divides by a. Left alone, ∫(2x+1)³dx renders as a
|
|
347
|
+
* fraction inside a fraction, which is correct and unreadable, and unreadable is the
|
|
348
|
+
* complaint this engine exists to answer.
|
|
349
|
+
*/
|
|
350
|
+
if (r.type === "num" && l.type === "binary" && l.op === "/" && l.right.type === "num") return simplify(div(l.left, num(l.right.value * r.value)));
|
|
351
|
+
if (r.type === "num" && r.value < 0) return simplify(neg(div(l, num(-r.value))));
|
|
352
|
+
if (l.type === "neg") return neg(simplify(div(l.arg, r)));
|
|
140
353
|
break;
|
|
141
354
|
case "^":
|
|
142
355
|
if (isNum(r, 1)) return l;
|
|
@@ -185,4 +398,4 @@ function fold(op, a, b) {
|
|
|
185
398
|
}
|
|
186
399
|
|
|
187
400
|
//#endregion
|
|
188
|
-
export { differentiate, simplify };
|
|
401
|
+
export { definiteIntegral, differentiate, integrate, simplify };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Node } from "./ast.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/math/compile.d.ts
|
|
4
|
+
type CompiledFn = (scope?: Record<string, number>) => number;
|
|
5
|
+
/** Compile an AST to a fast `(scope) => number`. */
|
|
6
|
+
declare function compile(node: Node): CompiledFn;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { CompiledFn, compile };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { CONSTANTS, FN1, FN2 } from "./defs.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/math/compile.ts
|
|
4
|
+
const JS_OP = {
|
|
5
|
+
"+": "+",
|
|
6
|
+
"-": "-",
|
|
7
|
+
"*": "*",
|
|
8
|
+
"/": "/",
|
|
9
|
+
"%": "%",
|
|
10
|
+
"^": "**"
|
|
11
|
+
};
|
|
12
|
+
/** Emit a JS expression string (every node fully parenthesised, so `**` and unary
|
|
13
|
+
* minus never collide — `(-x)**2` is valid, `-x**2` is a JS SyntaxError). */
|
|
14
|
+
function gen(node) {
|
|
15
|
+
switch (node.type) {
|
|
16
|
+
case "num": return `(${node.value})`;
|
|
17
|
+
case "var": {
|
|
18
|
+
const lc = node.name.toLowerCase();
|
|
19
|
+
if (lc in CONSTANTS) return `(${CONSTANTS[lc]})`;
|
|
20
|
+
return `(s[${JSON.stringify(node.name)}]??NaN)`;
|
|
21
|
+
}
|
|
22
|
+
case "neg": return `(-${gen(node.arg)})`;
|
|
23
|
+
case "binary": return `(${gen(node.left)}${JS_OP[node.op] ?? "+"}${gen(node.right)})`;
|
|
24
|
+
case "call": {
|
|
25
|
+
const a = node.args.map(gen);
|
|
26
|
+
if (node.fn in FN1 && a[0]) return `F1[${JSON.stringify(node.fn)}](${a[0]})`;
|
|
27
|
+
if (node.fn in FN2 && a[0] && a[1]) return `F2[${JSON.stringify(node.fn)}](${a[0]},${a[1]})`;
|
|
28
|
+
return "NaN";
|
|
29
|
+
}
|
|
30
|
+
default: return "NaN";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Compile an AST to a fast `(scope) => number`. */
|
|
34
|
+
function compile(node) {
|
|
35
|
+
const f = new Function("s", "F1", "F2", `return ${gen(node)};`);
|
|
36
|
+
return (scope = {}) => f(scope, FN1, FN2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { compile };
|
package/dist/math/defs.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/math/defs.ts
|
|
2
2
|
/**
|
|
3
|
-
* expr/defs
|
|
3
|
+
* expr/defs, the function & constant tables shared by the tokenizer, evaluator,
|
|
4
4
|
* differentiator, and LaTeX printer. Pure data; no AST knowledge here.
|
|
5
5
|
*/
|
|
6
6
|
const CONSTANTS = {
|
package/dist/math/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BinOp, Node, compileNode, evaluate, freeVars } from "./ast.mjs";
|
|
2
2
|
import { parse } from "./parse.mjs";
|
|
3
|
-
import { differentiate, simplify } from "./calculus.mjs";
|
|
3
|
+
import { definiteIntegral, differentiate, integrate, simplify } from "./calculus.mjs";
|
|
4
|
+
import { CompiledFn, compile } from "./compile.mjs";
|
|
4
5
|
import { toLatex } from "./latex.mjs";
|
|
5
6
|
|
|
6
7
|
//#region src/math/index.d.ts
|
|
@@ -9,7 +10,7 @@ interface CompiledExpr {
|
|
|
9
10
|
readonly fn: (scope: Record<string, number>) => number;
|
|
10
11
|
/** Free variables referenced (excludes constants & functions), e.g. `['x','a']`. */
|
|
11
12
|
readonly vars: string[];
|
|
12
|
-
/** The parsed syntax tree
|
|
13
|
+
/** The parsed syntax tree, for differentiation, LaTeX, or analysis. */
|
|
13
14
|
readonly ast: Node;
|
|
14
15
|
readonly error?: undefined;
|
|
15
16
|
}
|
|
@@ -20,7 +21,7 @@ interface ExprError {
|
|
|
20
21
|
readonly ast?: undefined;
|
|
21
22
|
}
|
|
22
23
|
type ExprResult = CompiledExpr | ExprError;
|
|
23
|
-
/** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }
|
|
24
|
+
/** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }`, never throws. */
|
|
24
25
|
declare function compileExpr(src: string): ExprResult;
|
|
25
26
|
//#endregion
|
|
26
|
-
export { type BinOp, CompiledExpr, ExprError, ExprResult, type Node, compileExpr, compileNode, differentiate, evaluate, freeVars, parse, simplify, toLatex };
|
|
27
|
+
export { type BinOp, CompiledExpr, type CompiledFn, ExprError, ExprResult, type Node, compile, compileExpr, compileNode, definiteIntegral, differentiate, evaluate, freeVars, integrate, parse, simplify, toLatex };
|
package/dist/math/index.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { compileNode, evaluate, freeVars } from "./ast.mjs";
|
|
2
2
|
import { parse } from "./parse.mjs";
|
|
3
|
-
import { differentiate, simplify } from "./calculus.mjs";
|
|
3
|
+
import { definiteIntegral, differentiate, integrate, simplify } from "./calculus.mjs";
|
|
4
|
+
import { compile } from "./compile.mjs";
|
|
4
5
|
import { toLatex } from "./latex.mjs";
|
|
5
6
|
|
|
6
7
|
//#region src/math/index.ts
|
|
7
8
|
/**
|
|
8
|
-
* expr
|
|
9
|
+
* expr, a tiny, safe, dependency-free symbolic math engine.
|
|
9
10
|
*
|
|
10
11
|
* Pipeline: `tokenize → parse (AST) → evaluate`. On top of the AST it offers
|
|
11
12
|
* exact `differentiate`, `simplify`, and `toLatex`, so the same typed formula
|
|
@@ -22,7 +23,7 @@ import { toLatex } from "./latex.mjs";
|
|
|
22
23
|
* csc asin acos atan sinh cosh tanh sqrt cbrt abs exp ln log log2 log10 floor
|
|
23
24
|
* ceil round sign` (+ 2-arg `pow atan2 min max mod hypot`).
|
|
24
25
|
*/
|
|
25
|
-
/** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }
|
|
26
|
+
/** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }`, never throws. */
|
|
26
27
|
function compileExpr(src) {
|
|
27
28
|
if (!src || !src.trim()) return { error: "Empty expression" };
|
|
28
29
|
let ast;
|
|
@@ -39,4 +40,4 @@ function compileExpr(src) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
//#endregion
|
|
42
|
-
export { compileExpr, compileNode, differentiate, evaluate, freeVars, parse, simplify, toLatex };
|
|
43
|
+
export { compile, compileExpr, compileNode, definiteIntegral, differentiate, evaluate, freeVars, integrate, parse, simplify, toLatex };
|
package/dist/math/latex.mjs
CHANGED
|
@@ -103,6 +103,14 @@ function texCall(node) {
|
|
|
103
103
|
if (node.fn === "cbrt") return `\\sqrt[3]{${wrap(a0, 0)}}`;
|
|
104
104
|
if (node.fn === "abs") return `\\left|${wrap(a0, 0)}\\right|`;
|
|
105
105
|
if (node.fn === "exp") return `e^{${wrap(a0, 0)}}`;
|
|
106
|
+
/**
|
|
107
|
+
* `ln|x|`, not `ln(|x|)`.
|
|
108
|
+
*
|
|
109
|
+
* The modulus bars already delimit the argument, so the usual parentheses double up. Every
|
|
110
|
+
* textbook and mark scheme writes the integral of 1/x as ln|x|, and that is the form a learner
|
|
111
|
+
* has to reproduce, so it is the form to show.
|
|
112
|
+
*/
|
|
113
|
+
if ((node.fn === "ln" || node.fn === "log") && a0.type === "call" && a0.fn === "abs" && a0.args[0]) return `\\${node.fn}\\left|${wrap(a0.args[0], 0)}\\right|`;
|
|
106
114
|
const args = node.args.map((a) => wrap(a, 0)).join(", ");
|
|
107
115
|
return `${NAMED_FN.has(node.fn) ? `\\${node.fn}` : `\\operatorname{${node.fn}}`}\\left(${args}\\right)`;
|
|
108
116
|
}
|
package/dist/math/parse.mjs
CHANGED
|
@@ -4,11 +4,11 @@ import { bin, call, neg, num, variable } from "./ast.mjs";
|
|
|
4
4
|
|
|
5
5
|
//#region src/math/parse.ts
|
|
6
6
|
/**
|
|
7
|
-
* expr/parse
|
|
7
|
+
* expr/parse, token stream → AST, via precedence-climbing (Pratt).
|
|
8
8
|
*
|
|
9
9
|
* Binary precedence: `+ -` < `* / %` < `^` (right-assoc). Unary minus binds
|
|
10
10
|
* looser than `^` (so `-2^2 = -(2^2) = -4`) but tighter than `*`. Throws a
|
|
11
|
-
* descriptive Error on malformed input
|
|
11
|
+
* descriptive Error on malformed input, the parser is the single source of
|
|
12
12
|
* truth for "is this expression well-formed?".
|
|
13
13
|
*/
|
|
14
14
|
const BIN_PREC = {
|
package/dist/math/tokenize.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { isFunction } from "./defs.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/math/tokenize.ts
|
|
4
4
|
/**
|
|
5
|
-
* expr/tokenize
|
|
5
|
+
* expr/tokenize, source string → token stream.
|
|
6
6
|
*
|
|
7
7
|
* Handles numbers (incl. `1.5e3`, rejecting malformed `2..3`), identifiers
|
|
8
8
|
* (classified as function vs variable), operators, parens, and commas. Inserts
|
|
@@ -7,16 +7,16 @@ import { jsx } from "react/jsx-runtime";
|
|
|
7
7
|
|
|
8
8
|
//#region src/primitives/CanvasLayer.tsx
|
|
9
9
|
/**
|
|
10
|
-
* <CanvasLayer
|
|
10
|
+
* <CanvasLayer>, the engine's HIGH-ELEMENT-COUNT escape hatch: a zero-dependency
|
|
11
11
|
* HiDPI <canvas> that shares the engine's coordinate system + clock. Use it when
|
|
12
12
|
* a lab needs a heatmap, a particle/gas sim, or thousands of moving dots at 60fps
|
|
13
|
-
|
|
13
|
+
*, cases where one retained SVG node per element would drop frames. Declarative,
|
|
14
14
|
* low-count, accessible figures should stay on <Stage> (SVG); this is the
|
|
15
15
|
* deliberate, bundle-free alternative to a WebGL engine (no Pixi/three dependency)
|
|
16
16
|
* until a lab MEASURABLY needs shaders/tens-of-thousands of textured sprites.
|
|
17
17
|
*
|
|
18
18
|
* `draw(ctx, coords)` runs in CSS-pixel space (the context is pre-scaled for dpr)
|
|
19
|
-
* on mount, on resize, and whenever `draw`'s identity changes
|
|
19
|
+
* on mount, on resize, and whenever `draw`'s identity changes, so memoize it on
|
|
20
20
|
* your state (or bump it from a useFrameLoop tick) to drive animation. Overlay a
|
|
21
21
|
* sibling <Stage> with the SAME view for accessible SVG handles/labels on top.
|
|
22
22
|
*/
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { StyleProps } from "./props.mjs";
|
|
2
|
-
import { ReactNode } from "react";
|
|
3
2
|
|
|
4
3
|
//#region src/primitives/Dot.d.ts
|
|
5
4
|
interface DotProps extends StyleProps {
|
|
@@ -8,20 +7,8 @@ interface DotProps extends StyleProps {
|
|
|
8
7
|
/** Pixel radius (never scaled). */
|
|
9
8
|
r?: number;
|
|
10
9
|
}
|
|
11
|
-
declare
|
|
12
|
-
x,
|
|
13
|
-
y,
|
|
14
|
-
r,
|
|
15
|
-
color,
|
|
16
|
-
opacity
|
|
17
|
-
}: DotProps): ReactNode;
|
|
10
|
+
declare const Dot: import("react").NamedExoticComponent<DotProps>;
|
|
18
11
|
/** A point with a contrast ring (reads clearly over any background). */
|
|
19
|
-
declare
|
|
20
|
-
x,
|
|
21
|
-
y,
|
|
22
|
-
r,
|
|
23
|
-
color,
|
|
24
|
-
opacity
|
|
25
|
-
}: DotProps): ReactNode;
|
|
12
|
+
declare const Point: import("react").NamedExoticComponent<DotProps>;
|
|
26
13
|
//#endregion
|
|
27
14
|
export { Dot, DotProps, Point };
|