A developer-level walkthrough of the engine: the data model, every module's algorithm, and — in full — the ordered routine the integrator follows. Companion to the narrative History pages. Method names, constants, and orderings below are taken from the source in symbolic/.
integral(…) marker. Completed candidate antiderivatives carry a Monte-Carlo derivative check (d/dx of the answer vs the integrand). This numerical spot-check can detect errors; it is not an identity proof, and a sample agreement percentage is not a probability of correctness. A Sinatra server (or, for GitHub Pages, an in-browser ruby.wasm VM) drives a zero-dependency vanilla-JS frontend.
Every request follows one pipeline. The expression is parsed once and stored server-side (or VM-side) by id; operations transform the stored tree, and integrals pass through the verifier before serialization.
The core class MathematicalExpression includes every capability as a mix-in module, so one object answers .derivative, .integrate, .series_expand, .try_residue_integration, etc. Most phases are gated by respond_to?(:method, true) — a module that is required but not included is silently skipped (a bug we hit twice). Instance flags @use_experimental_integration and @use_experimental_simplification select the aggressive paths.
The whole engine rests on six plain Ruby types with no inheritance — structure is by composition and mix-in, never subclassing. The AST node is self-referential (its left/right are themselves ExpressionNodes); the parser builds those trees; the facade owns one root node and folds in every capability module.
The 20 modules MathematicalExpression mixes in are grouped below by concern. Because they are included, every one becomes an instance method on the single facade object, and phases call their siblings through respond_to?. The seven POST modules are a different animal: standalone module_function services the transport layer calls directly — they are not mixed into the facade.
The same engine code runs two ways. Natively, a Sinatra server answers /api/*. On GitHub Pages there is no server: wasm_runtime.js shims fetch so the identical paths route into an in-browser ruby.wasm VM, where Engine.solve (wasm_dispatch.rb) mirrors every Sinatra route. Both paths construct the same MathematicalExpression and produce identical results.
Most operations in the right-hand panel act on the formula in the input bar. Six do not: they open a dialog carrying their own inputs, and the panel separates them under self-contained · own inputs. They are the least self-explanatory part of the engine, so here is what each actually computes and how. Vector calculus joined this group when it stopped being able to work from the bar alone — divergence and curl consume a whole vector field, which has to be typed (§14).
What it does: carries bounds over a whole region. The enclosure guarantee for transcendental functions is conditional on the platform math-library error bound. Three modes: the range of f over a box, its global minimum, and a bracket around a definite integral.
How: a third evaluator, evaluate_interval, walks the same AST as ordinary evaluation but carries [lo, hi] bounds through every operation with outward rounding — basic-operation results widen by one ULP; transcendental results widen by two, assuming that covers the platform library error. Subdivision can tighten dependency overestimation. The global minimum uses branch & bound: if a box's interval lower bound exceeds the best value found so far, that box provably cannot contain the minimum and is discarded. This can retain narrow dips missed by point sampling, though finite search budgets can leave wide bounds.
Why trust it: 4 000–20 000 sample comparisons and an independent Simpson estimate check for regressions. Passing these checks does not establish the transcendental library assumption. Full detail in §18.
What it does: computes with no rounding error at all, or refuses. Four modes: compare float vs interval vs exact on one expression; float-only (what the engine did before); exact-only; a polynomial lab (GCD, squarefree decomposition, resultants); and ℚ(α), arithmetic in an algebraic extension.
How: evaluate_exact returns a Ruby Rational — 0.1 + 0.2 is exactly 3/10 — and returns nothing at all when no rational answer exists (√2, sin 1, e). For ℚ(α), an element is a polynomial in α reduced modulo a minimal polynomial, so √2 is stored as the symbol α plus the rule α² = 2; squaring it gives exactly 2 where a float gives 2.0000000000000004. Inverses come from the extended Euclidean algorithm in ℚ[x].
Why it matters: "is this coefficient zero?" is a guess at 1e−10 in floating point and a fact in ℚ. Every decision procedure is built out of exact zero-tests. The demo worth trying: gcd((x−1/3)⁴, f′) is degree 3 exactly, and the float path returns degree 0 — "no common factor" — which is structurally wrong, not merely imprecise. See §20.
What it does: runs the engine backwards. Given only a trajectory — a table of numbers — it recovers the differential equation that produced it.
How: build a library Θ(x) of candidate terms (monomials, optionally sin/cos), estimate ẋ from the data, then solve Ẋ = Θ(X)·Ξ for sparse Ξ by sequential thresholded least squares: solve, zero every coefficient below λ, re-solve on the survivors, repeat. Sparsity is the prior that turns a curve fit into a candidate physical law.
Why trust it: the discovered model is judged by oracles the regression never saw — agreement with the true vector field at every sample (the verdict), plus RK4 re-simulation (corroboration). Lorenz comes back exactly, −8/3 included. Remove sin from the library and ask for the pendulum, and it reports FAILED with the fabricated terms flagged rather than presenting a plausible polynomial. See §17.
What it does: takes a spacetime metric and derives its curvature — Christoffel symbols, Riemann and Ricci tensors — then flies a test particle along a geodesic.
How: every quantity comes from the engine's own symbolic derivatives, composed through parseable infix, so parameters like mass M stay symbolic in the output (Γttr = M/(r(r−2M))). The orbit is RK4 integration of ẍ^μ = −Γ^μαβ ẋ^α ẋ^β on those computed symbols.
Why trust it: Schwarzschild must be a vacuum (all ten Ricci components vanish) and its Kretschmann scalar must equal 48M²/r⁶ — a nonzero oracle, so "everything came out zero" cannot fake a pass. De Sitter must satisfy Rμν = 3H²gμν, whose sign pins the curvature convention. Along the orbit, energy, angular momentum and the 4-velocity norm must stay conserved. Mercury's numbers in the verified formula give 42.98″/century. See §19.
What it does: a launcher for seven numeric tools — line integrals (work/flux/arc-length), surface integrals, Green's, Stokes' and Gauss's theorems, the Leibniz rule in 1D/2D/3D, Reynolds transport, and the Lebesgue integral.
How: tangents, curls and divergences are symbolic; the domain integrals are Monte Carlo or quadrature. The transport tools use a flow-map identity — a fixed unit ball mapped by φt(u) = c(t) + A(t)u — so the boundary-motion term comes from symbolic det(A)′ and ∇f rather than surface quadrature.
What kind of evidence stands behind it: this is the one group not checked against closed forms. Each instead computes its quantity two independent ways and shows they agree (e.g. a line integral directly versus Green's theorem over the enclosed region), which is strong evidence but weaker than an oracle. They also carry Monte-Carlo noise, so results wobble in the last digits. Laurent sits in the same category: its pole order is detected numerically and unstable coefficients truncate the series rather than being trusted.
Twenty-three operations are reachable from the panel: sixteen buttons, and seven more inside the Field Theorems launcher. The sections that follow go deep on the subsystems; this one is the index — what each mode eats, what it hands back, which module does the work, and above all what kind of evidence supports the answer. That last column is the one to read first, because the modes differ far more in how well they are checked than in how hard they are.
| Mode | Consumes | Produces | Algorithm (module) | Evidence |
|---|---|---|---|---|
| ∫ Integrate | the formula bar | antiderivative + C, steps, trace logs | ordered heuristic cascade, ~17 phases (§6; experimental_risch.rb and friends) | HEURISTIC search, but the ANSWER is checked: verify_integral differentiates the result and compares it to the integrand at 15 random points, and the verdict now rides on the result as a badge beside it. The search is heuristic; the answer is verified. (Until recently the verdict was computed and then dropped — the debug log was snapshotted one line before the verify entry was appended — so it was reachable only through Strategy View.) |
| ∫ab Definite | bar + bounds dialog | a value, a method badge, region plot | four rungs: residue → FTC → known special forms → Simpson (definite_integral.rb) | Read the badge — it is the honesty signal. Exact on ftc/residue, numeric otherwise. A continuity check now refuses divergent integrals outright (§6). |
| ∑Res Residue | bar + bounds | value, poles with multiplicity, contour | two closable contours only: semicircle at ∞, and the unit circle for a full period (§13) | ASYMMETRIC, and worth knowing. The trigonometric route is checked against periodic-trapezoid quadrature — spectrally accurate here — and refuses above 1e−6 relative error. The improper route has no numeric oracle at all; it only warns if the imaginary part comes out large. |
| d/dx Derivative | bar + variable | derivative, rule-by-rule steps, tree | exact recursive rules (§5, derivative_operations.rb) | EXACT, UNCHECKED — differentiation is a total decision procedure, so nothing can go structurally missing. Nothing is checked either. |
| df Differential | bar + variables | Σ (∂f/∂xᵢ)dxᵢ with per-term breakdown | one exact partial per variable (vector_calculus.rb) | EXACT, UNCHECKED |
| Σ Taylor | bar + centre, order | terms as exact rationals, plot, remainder | successive symbolic derivatives at the centre (§12) | Coefficients are exact. The Lagrange remainder is a bound, not a check — and its M is estimated by sampling, so the bound itself is heuristic. |
| P/Q Padé | bar text + L, M, centre | P/Q, poles flagged genuine or spurious, plot | Toeplitz solve then Durand–Kerner (§12) | VERIFIED against f itself: defining-equation residuals, near-centre error, and median error versus the Taylor polynomial over the window. |
| Σ± Laurent | bar + centre, order | series with a principal part, truncation flag | numeric pole-order detection, symbolic cancellation, limit sampling (§12) | HEURISTIC, and the weakest thing in the panel. On 1/(x(x−1)) the x² coefficient prints as −6145/6144 where the truth is −1 — wrong in the fourth significant figure — before the honest truncation notice appears. |
| ⚡ Simplify | the current expression | before/after, named rewrite steps | rewrite to a fixed point, ≤30 passes (§10) | HEURISTIC. A rewrite system, not a canonicaliser: no normal form and no zero test. It will collect 2x+3x into 5x and then rewrite sin²+cos² into half-angle form and stop, never reaching the 1. |
| 🔢 Evaluate | bar + variable values | one float | direct recursive float evaluation (§4) | This is the float lane that §20 exists to contrast against. |
| ∇ Grad · Div · Curl · Laplacian | its own dialog: dimension slider, axis names, then a scalar or one component per axis | the intermediate partials, then a vector or scalar, with the arity stated | exact symbolic partials assembled per operator (§14, vector_field_ops.rb) | EXACT, UNCHECKED — but the signatures are enforced: curl is offered only where it exists, and refuses elsewhere with the component count it would have had. |
| [f] Interval bounds | its own dialog only | naive vs certified enclosure, minimiser boxes, integral bracket | interval arithmetic with outward rounding, branch & bound, Darboux sums (§18) | CONDITIONAL INTERVAL BOUND — subject to the stated transcendental library assumption. Sampling is a regression check. |
| Q Exact ℚ, ℚ(α) | its own dialog only | three lanes side by side, or a polynomial lab, or a field report | exact rationals, primitive-PRS GCD, Yun squarefree, ℚ[x]/(m) (§20) | EXACT — or an honest refusal, which is the actual product. |
| ẋ=Θξ Discovery | its own dialog only | the recovered equations, per-term coefficients, spurious flags | candidate library + sequentially thresholded least squares (§17) | VERIFIED against an independent oracle — the true vector field, which the regression never sees. The re-simulation is corroboration only. |
| gμν Relativity | its own dialog only | Γ, Ricci, Kretschmann, a precessing orbit | symbolic differentiation of a diagonal metric, then RK4 on the computed connection (§19) | VERIFIED against closed forms for the presets. A custom metric gets only the structural identities, and the verdict says so. |
| ∮∬∭ Field Theorems | — | a launcher for the seven below | — | — |
All numeric, all labelled EXPERIMENTAL, and all sharing one doctrine: compute the quantity two independent ways and show they agree. That doctrine is worth stating precisely, because it is weaker than it sounds — both ways are computed by this engine from the same parsed tree, so agreement demonstrates that the implementation is coherent, not that the answer is right. The tolerances are listed because they vary by two orders of magnitude and the loosest would pass almost anything.
| Tool | What it computes | Cross-check, and its tolerance |
|---|---|---|
| ∫μ Lebesgue | partitions the RANGE — ∫f dμ = ∫μ({f>t})dt over 64 level sets | Simpson on the same tree; UI turns green under |Δ| < 0.05. On x² over [0,3] the layer cake gives 8.99666 against 9 — off by 3.3e−3 on a polynomial. |
| ∮F·dr Line integral | work / flux / arc length with symbolic tangents | Green's theorem over the enclosed region by Monte Carlo; passes at 3σ + 1e−3. On the unit circle with F = (−y, x) the two sides differ by 0.35%. |
| ∬·dS Surface | flux on a parametric surface, symbolic ru×rv and curl | Stokes and Gauss both ways; 0.5% relative. Hemisphere with F = (−y, x, 0): both sides land within 2.3e−4 of 2π. |
| d/dt∫ Leibniz 1D | moving interval [a(t), b(t)], boundary + bulk split | an independent numeric d/dt of the whole integral; 0.1% relative. |
| Leibniz 2D / 3D | deforming ellipse / ellipsoid via the flow map φt(u) = c(t) + A(t)u | same shape; 2.5% relative — the loosest band in the app. The measured runs agree to 1e−13, but the band would have accepted far worse. |
| Reynolds | the physics framing: d/dt∭f dV = ∭∂f/∂t + ∬f(vb·n) | common-random-number Monte-Carlo finite difference; 2% relative. Measured 0.05% apart. |
| File | What it does (algorithm in one line) |
|---|---|
mathematical_expression.rb | Core class; mixes in all modules; holds the root node + mode flags. |
parser.rb | Tokenize → normalize → reverse shunting-yard → prefix → tree. Owns OPERATORS precedence + FUNCTION_NAMES. |
expression_node.rb | ExpressionNode (binary tree); float evaluate; special-function quadrature. |
normalizer.rb | Non-mutating 9-step canonicalization pipeline (fold, identities, radicals, power-products, polynomial collection, GCD cancel). |
polynomial.rb | Sparse {degree=>coeff} polynomial: arithmetic, divmod, gcd, rational-root factoring, discriminant. |
tree_visualizer.rb | to_readable_string, to_latex (special fns as \operatorname), ASCII tree. |
derivative_operations.rb | Exact rule-based differentiation; one ordered case over node type. |
experimental_risch.rb | The integration cascade orchestrator (risch_integrate_inner, ~17 phases). Misnomer — see §22. |
integration_operations.rb | integrate entry + dispatch; compute_integral standard integrator; finalize_integration. |
integral_table.rb | Ordered TABLE_ENTRIES pattern table (arctan, ln-linear, arcsin, sec²/csc², …). |
general_u_substitution.rb | u = g(x) candidate collection, ranking, factor-match against du/dx, back-substitution. |
trig_identities.rb | Ordered rewrite pre-pass (Pythagorean, power-split, half-angle, product-to-sum…). |
rational_integration.rb | Long division → linear/irreducible-quad → completing-square → partial fractions. |
special_products.rb | Closed forms: poly·exp·trig reduction recursion, mixed odd trig powers, poly·trig². |
special_functions.rb | Non-elementary integrals → erf/Si/Ci/Ei/li/Fresnel nodes. |
simplification.rb | Fixed-point pass rewriter; constant folding; track_steps. |
verification.rb | Monte-Carlo dF/dx==f check (15 pts, 1% tol, 90% threshold). |
series_expansion.rb | Taylor (symbolic derivatives) / Laurent (flow-map cancel) + Lagrange remainder. |
residue_theorem.rb | Improper rational integrals via contour residues; real-axis guard. |
definite_integral.rb | FTC → known special forms → Simpson; Monte-Carlo mode; plot samples. |
lebesgue.rb | Layer-cake (range partition) integral; Riemann cross-check. |
vector_integrals.rb | 2D line integrals (work/flux/arc) + Green/divergence cross-check + parameter sweep. |
surface_integrals.rb | Parametric surfaces; scalar dS / flux / Stokes / Gauss (two-way). |
leibniz_rule.rb, leibniz_higher.rb, reynolds_transport.rb | Differentiation under the integral over moving 1D/2D/3D domains (flow-map). |
complex_operations.rb | Trig → complex-exponential (Euler) rewrite. |
integration_tracer.rb, strategy_reporter.rb | Thread-local decision-tree / flat-log instrumentation for the live view. |
exact_arithmetic.rb · algebraic_field.rb · exact_lab.rb | Exact rationals over the same AST; ExactPolynomial (GCD / squarefree / resultant / ext-Euclid); ℚ(α) algebraic extensions; the float-vs-interval-vs-exact comparison (§20). |
sindy.rb | Discovers the governing ODE from trajectory data: candidate library + sequential thresholded least squares (§17). |
pade.rb | Rational [L/M] approximant from Taylor coefficients; captures poles, flags Froissart artifacts (§12). |
interval_arithmetic.rb | Outward-rounded evaluate_interval + conditional range / global-min / integral bounds (§18). |
general_relativity.rb | Diagonal metric → Christoffel → Riemann → Ricci, with vacuum/Kretschmann/de Sitter oracles + geodesics (§19). |
web_server.rb · wasm_dispatch.rb | Sinatra routes · the in-browser ruby.wasm dispatch mirroring them. |
All class methods on ExpressionParser; entry is parse(input) = tokenize → infix_to_prefix → build_tree.
pi/e are textually replaced with Math::PI/Math::E before scanning; numbers become Float. Precedence map OPERATORS: + - = 1, * / = 2, ^ = 3, functions = 4.insert_unary_minus rewrites a leading/post-operator - into the pair -1.0, * (the raw form caused a SystemStackError) — except after ^, where the negated operand is parenthesised instead (see the card below). insert_implicit_multiplication inserts * between operand-enders and operand-starters (2x, 2(x+1), 2sin(x), (x+1)(x-2)).).should_pop_for_postfix? uses flipped comparisons: left-associative operators pop on strict stack_prec > current_prec, while the right-associative ^ pops on stack_prec ≥ current_prec. The pre-fix code had these inverted, so 5-2+1, 8/2/2, and 2^3^2 evaluated wrongly. Guarded by test_associativity.rb (17 cases).
x^-1 hid it. The (-1) * rewrite is sound only where the minus sits at a precedence below multiplication. ^ binds tighter than *, so in an exponent the multiplication escaped: x^-3 became (x^-1) * 3, a different function — 1.5 at x=2 instead of 0.125. Nothing errored; the answer was simply wrong. x^-1 is the form that appears most often, and it is the one case the bug cannot touch (multiplying by 1 changes nothing), so the engine looked healthy while x^-2, x^-3 and x^-0.5 were all quietly incorrect.
to_infix prints that form — d²/dx²(1/x) renders as 2 * (x ^ -3) — so every module that builds expressions by re-parsing printed infix (vector_field_ops, general_relativity) got back a different function than it put in. That is how it surfaced: a divergence card showed two correct partials, 2·x⁻³ and 0, and then reported their sum as 6·x⁻&sup9;¹. Now the exponent keeps its own parentheses. Guarded by probe_parser_exponent.rb (29 checks, scored against Ruby’s own float arithmetic rather than against the engine).
tokenize splits with String#scan, which silently skips any character its pattern misses — and the pattern never matched × or ÷, the very glyphs to_readable_string prints. So 6 ÷ 2 lost the operator and parsed as the implicit product 6 × 2 = 12, and 2 × -3 lost it and parsed as the subtraction 2 - 3 = -1. Copying a displayed answer back into the input bar quietly changed the function. Display glyphs (× · ÷ − and friends) are now folded to their ASCII operators before scanning, and a character the scanner still cannot classify raises instead of vanishing — a typo should be an error, not a different question.
Constants are not symbolic: pi/e are float literals at lex time and every number is a Float. This one decision shapes — and bounds — everything downstream (§20, §22).
ExpressionNode fields: isleaf, data (operator/function/variable string, or Numeric), left, right, parent, type. Unary functions hang their argument on right. connect(child, "L"|"R") wires children + back-pointer; deep_clone copies the subtree.
evaluate(var_values) is the numeric oracle the whole verification philosophy rests on. Recursion: number leaves → to_f; variable leaves → var_values[data] (missing → 0.0); operators dispatch via a large case:
+ - * / ^ (division by zero → Float::INFINITY).Math; reciprocal trig is computed as 1/tan, 1/cos, 1/sin with zero-guards; ln/sqrt return NaN out of domain; arcsin/arccos require |arg| ≤ 1.Si, Ci, Ei, li, FresnelS, FresnelC are evaluated by composite Simpson quadrature (sf_quad, n = 600) over their defining integral; Ci/Ei/li add the SF_EULER_GAMMA + ln(…) offset. erf uses Math.erf. (All match published tables to ~1e-10.)to_readable_string/to_latex (in tree_visualizer.rb) render the three output forms; to_latex emits special functions as \operatorname{…} so li(x) is not mistaken for a variable product.
Exact and total — there is no "can't differentiate." compute_derivative(node, variable, steps, aggressive_simplify) is one case node.data evaluated top to bottom; subtrees are reused via deep_clone to avoid aliasing. With track_steps, each rule appends {rule, from, to}.
x → 1, constant → 0.+ sum, - difference.* product u'v + uv'; / quotient (u'v − uv')/v².^ three branches in order: (a) power rule xⁿ → n·xⁿ⁻¹ (base = var, const exponent); (b) generalized power + chain fⁿ → n·fⁿ⁻¹·f' (const exponent); (c) general f^g via logarithmic differentiation f^g·(g'·ln f + g·f'/f).sin cos tan cot sec csc; hyperbolic sinh cosh tanh sech csch coth; inverse hyperbolic arcsinh arccosh arctanh.ln, exp; inverse trig arctan arcsin arccos; sqrt → 1/(2√u)·u'.erf' = (2/√π)e^(−u²), Si' = sin u/u, Ci' = cos u/u, Ei' = e^u/u, li' = 1/ln u, FresnelS' = sin(u²), FresnelC' = cos(u²) — each ·u'.abs → (u/|u|)·u'. Fallback: unknown operator → 0.This is the heart of the engine. integrate(variable) dispatches on @use_experimental_integration: when true it runs the cascade (integrate_experimental → risch_integrate); otherwise it calls the standard integrator compute_integral directly (§7). Both results pass through finalize_integration.
risch_integrate wraps risch_integrate_inner with RISCH_MAX_DEPTH = 30 and RISCH_MAX_CALLS = 4000. On exceed it returns an integral(…) marker instead of recursing — the anti-hang stop (e.g. sin(x)/x returns a marker in ~0.04 s).
risch_integrate_inner runs these phases in this order; each is gated by a respond_to? capability check plus a structural predicate, and the first to yield a marker-free result returns immediately. Phases marked (rewrite) mutate the working node and fall through; phases marked (recurse) transform and re-enter risch_integrate.
node.data == '/' and not a rational function — attempt_u_sub_division. Catches ln(x)/x, sin(ln x)/x before normalization rewrites a/b → a·b⁻¹.node.data == '*' — attempt_general_u_sub; logs the candidate list. Runs before trig identities.node.data == '*' — attempt_special_product: poly·exp·trig, mixed odd trig powers, poly·trig² (§9).* over +/- — expand and re-integrate.is_inverse_trig? — integrate_inverse_trig (with an ln^n reduction).is_rational_function? — attempt_rational_integration → partial fractions (§8). Highest-priority "type" phase.is_trig_power? (sin/cos/…ⁿ, n>1) — attempt_trig_power_reduction.has_quadratic_radical? — attempt_radical_integration.can_apply_smart_substitution? — attempt_smart_substitution. Deliberately before by-parts.can_apply_integration_by_parts? — attempt_integration_by_parts: LIATE choice, special-cases exp×trig, computes du, recursively gets v and ∫v du via risch_integrate.finalize_integration runs once on the top-level result: if the tree still contains an integral marker but is not itself a lone marker, it collapses the whole tree to one marker over the original integrand. This stops a later simplify pass from cancelling a partial marker and presenting a clean-but-wrong antiderivative (the historical x²·eˣ·sin x bug). Combined with §11, this is why the engine never reports a confidently-wrong integral.
F(b) − F(a) was evaluated straight across a pole. ∫₋₁¹ dx/x² is divergent and came back as −2 — a negative area under a strictly positive integrand, so not merely inaccurate but impossible. ∫₋₁¹ dx/x came back as 0 with no mention that this is a principal value, and ∫₋∞∞ dx/(x³+1) came back as 1.79 from Simpson, which skips non-finite samples and so integrates straight through a singularity.
∫₋₁¹ dx/√|x| = 4 converges. The guard locates the blow-up by ternary search on |f| (a refined grid fails: its points sit at fixed offsets, so on tan near π/2 it never lands closer than the original scan and a real pole reads as a plateau), then asks the one-sided limits by watching the sliver ∫ over [x₀−εₖ, x₀−εₖ₊₁] as the cutoff shrinks:
∫₋₁¹ dx/x, whose halves both diverge and whose symmetric combination is 0. The Cauchy principal value is reported as a labelled extra, never as the answer. Endpoint singularities are deliberately out of scope: ∫₀¹ dx/√x and ∫₀¹ ln x dx are improper only at a bound and both converge, so flagging them would be a false refusal. Guarded by probe_improper.rb (19 checks), and the regression output is byte-identical.
compute_integral(node, variable, steps) is the non-experimental engine and the universal fallback. Order: normalize → table lookup → simple_algebraic_simplify (division-focused) → leaf case (∫x = x²/2, ∫c = c·x) → operator case:
| Node | Handling (in order) |
|---|---|
+ / - | sum / difference rule — recurse both sides. |
* | constant factor → constant-multiple rule; else sin·cos → sin²/2; else attempt_general_u_sub; else integral marker. |
/ | constant denominator → (1/c)∫f; c/x → c·ln|x|; then pattern checks: sec², csc², ln(x)/x → (ln x)²/2, arctan; then ∫f'/f = ln|f| (u-sub); then attempt_u_sub_division; then attempt_rational_integration; else marker. |
^ | power rule ∫xⁿ: n=−1 → ln|x|, else xⁿ⁺¹/(n+1) (rational coefficient); other powers → marker. |
| trig / transcendental | whole-node constant → c·x; argument = variable → the known antiderivative; linear argument ax+b → (1/a)-scaled (sin/cos/exp); else marker. |
else | unknown operator → integral marker, "cannot integrate." |
It never raises — failure is always an ExpressionNode("integral") wrapping the integrand, detected everywhere by contains_integral_marker?.
table_lookup_integral walks an ordered TABLE_ENTRIES list, matching each via match_pattern (a case on :type) and checking an optional condition lambda. Trial order:
| # | Form | Result |
|---|---|---|
| 1 | c/(x²+a²) | (c/a)·arctan(x/a) |
| 2 | c/(ax+b) | (c/a)·ln|ax+b| |
| 3 | c/√(a²−x²) | c·arcsin(x/a) |
| 4 | c/(x²−a²) | (c/2a)·ln|(x−a)/(x+a)| |
| 5–6 | tan·sec, cot·csc | sec, −csc |
| 7 | sec²·tan | ½sec² |
| 8–9 | sec² / csc² (and cos⁻², 1/sin² encodings) | tan / −cot |
collect_substitution_candidates gathers inner functions, power bases, sum subtrees, and denominators, then ranks them deepest-first, then shortest infix (sort_by { [-depth, infix.length] }). For each candidate, try_u_substitution computes du/dx and runs try_factor_match (three cases: left ∝ du/dx, right ∝ du/dx, nested const·factor), using compute_proportionality (float ratio) and can_express_in_u? (recursively verifies no residual x remains). On success it integrates f(u) via compute_integral in the placeholder variable __u__, back-substitutes, and scales by the constant factor. attempt_u_sub_division first rewrites division shapes (f/√g → f·g⁻ᐟ², c/(a·b) splits, f/gⁿ → f·g⁻ⁿ) then retries.
apply_trig_identities deep-clones, then applies identities top-down (order is load-bearing): Pythagorean sin²+cos² → 1 → 1+tan² → sec² → mixed-power split (odd sinᵐcosⁿ → factor one for u-sub; returns early, does not recurse, to preserve the pattern) → sin²cos² → ⅛(1−cos 4x) → 1/(sin·cos) → tan+cot → recurse into children → tan² → sec²−1 → double-angle 2 sin cos → sin 2x → half-angle sin²/cos² → (1∓cos 2x)/2.
attempt_rational_integration builds numerator/denominator Polynomials and tries, in order: (1) long division if improper (divmod, integrate quotient + remainder fraction); (2) linear / irreducible quadratic (split Ax+B into a ln|den| part + a ∫1/(quad) part); (3) completing the square → arctan; (4) general partial fractions via factor_linear_roots → distinct-linear (cover-up Aᵢ = P(rᵢ)/∏(rᵢ−rⱼ)), repeated-linear (solve a linear system), or linear + irreducible-quadratic (numerically solve n+2 unknowns). The linear systems use Gaussian elimination with partial pivoting at deterministic non-root sample points.
Three closed-form families the generic cascade can't finish; each formula was verified numerically before implementation, then every output is re-verified by §11.
Plus mixed trig powers sinᵐ cosⁿ with one odd power (binomial u-sub closed form), and xⁿ·sin²/cos²(cx) via half-angle + family 1.
erf/Si/Ci/Ei/li/Fresnel are first-class nodes (§4 numerics, §5 derivatives), so non-elementary integrals become genuine, verifiable closed forms: ∫e^(−ax²) → erf, ∫sin(ax)/x → Si, ∫cos/x → Ci, ∫e^(ax)/x → Ei, ∫1/ln x → li, ∫sin(x²) → FresnelS, ∫eˣ ln x → eˣ ln x − Ei(x).
normalize_expression deep-clones, normalizes children, then applies a fixed pipeline: fold constants → canonical operand order (constant left) → negation (0−x → −1·x) → identity laws (x+0, x·1, x/x→1, x^0→1, …) → radicals (√x → x^½) → reciprocal powers (f/xⁿ → f·x⁻ⁿ) → combine power products (xᵃ·xᵇ → x^(a+b)) → polynomial term collection (for +/- roots, via Polynomial) → common-factor cancellation (for /, GCD, only on exact division).
Sparse @coeffs = {degree => coefficient} (zero polynomial = empty hash, degree == -1). Provides arithmetic, divmod (long division), Euclidean gcd (monic), derivative/integral, and root finding: rational_roots (rational-root theorem, root test at |eval| < 1e-10), factor_linear_roots (divide out (x−r) with multiplicity), discriminant/quadratic_roots. from_expression returns nil for anything non-polynomial (transcendental, non-integer/negative power, non-constant denominator).
A fixed-point pass rewriter: identity laws, constant folding, like-term collection, sign normalization, repeated until stable. track_steps records each rewrite for the Solution Steps panel and the live "polish" phase.
+ constant-folding branch set node.left = node.right = nil and then referenced node.left.data in the step message — a use-after-null that only fired when steps were tracked (which the server always does), crashing every ∫xⁿ. The lesson: the harness must exercise the same path the server uses.
+ and * arms fold constants inside their if/elsif chain, so a fold runs only when no other rule fired. The - arm’s fold sat outside the chain, as a trailing if. The rule 0 - x = -x rewrites the node in place into (-1) * x, so for numeric x the trailing fold then saw two fresh numeric leaves and folded them with a minus that no longer matched the node’s operator: 0 - 1 became -2.
c / v derivative runs the quotient rule, whose numerator is 0·v - c·v′. Whenever v′ collapsed to a constant the answer came out short by exactly one, so d/dx(1/x) returned -2·x⁻² instead of -1·x⁻². The raw derivative was always right; the simplifier broke it afterwards. The fold is now guarded on node.data == '-'. Guarded by probe_simplify_zero.rb (41 checks, derivatives scored against a central difference).
normalizer.rb’s extract_var_power returns a [constant, exponent] pair for a power of the variable. Both its recursive branches threw the inner constant away into an unused _: 1/(2·x) came back as 1·x⁻¹ instead of 0.5·x⁻¹, and -1·(1/(2·x0.5)) as -1·x⁻⁰·⁵ instead of -0.5·x⁻⁰·⁵. That is a factor-of-k error in every c/(k·xⁿ) — precisely the shape the quotient rule emits. 1/(2x)·x simplified to 1 rather than 0.5, and d/dx(√x/x) simplified to 0. It fired only under the experimental simplifier, which is the one the derivative, integral and vector-calculus endpoints all switch on.
tree_visualizer.rb printed the right operand of a minus without its parentheses, in both to_readable_string and to_latex: a - (b + c) rendered as a - b + c. The tree was correct and every numeric check passed, because the checks read the tree — but the formula on screen was a different function. Seven integration-by-parts results inside headless_test.rb had been displayed wrong this way while reporting “verified 12/12”; ∫x²eₓ showed a string that evaluates to −10.68 where the answer is 3.9995. Fixing the printer changed those seven lines of expected output and nothing else in the suite.
probe_display_roundtrip.rb (47 checks): simplification must not change a value, the engine must read back the glyphs it writes, and a printed derivative retyped into the parser must evaluate to the number it came from.
Polynomial.from_expression, which returns nil for anything transcendental — and the two general rules that did fire (x + x = 2x, x − x = 0) demand an exact node match, so any explicit coefficient defeated them. eₓ·cos y + (−1)·eₓ·cos y stayed on screen as a sum whose terms cancel, which is how the Laplacian of a harmonic function came out correct but visibly unfinished.
collect_like_terms_general takes a whole +/− chain at once: it flattens the chain (the right operand of a minus flipping sign), splits each term into a numeric coefficient and a multiset of factors, keys the term on the sorted factor list so a·b and b·a agree, sums the coefficients and rebuilds. ∇²(eₓcos y), ∇²(e²ₓcos 2y) and ∇²(x²−y²) now all print 0.
Polynomial#divmod is Euclidean long division: each step is meant to cancel the remainder’s leading term exactly. Polynomial#initialize prunes a coefficient only when it is exactly 0.0, and in floating point that cancellation routinely leaves a residue around 1e−16 instead. The remainder’s degree therefore never dropped, the loop never exited, and Polynomial#gcd — reached from normalizer.rb’s cancel_common_factors — spun forever. Simplifying 12.25 / (y − 2) hung the engine outright; in the browser build that is a frozen tab rather than an error message. Neighbouring inputs like 12 / (y − 2) and 3.5 / (y − 2) were fine, which is why no example-based test ever met it.
divmod now compares the degree before and after each step. A residue that is negligible against the rest of the polynomial is rounding and is discarded so the degree strictly decreases; a residue large enough to be real means the step did not divide as intended, and the loop stops rather than spinning. Termination no longer depends on exact float cancellation.
probe_simplify_fuzz.rb generates random expressions from a fixed seed, simplifies each under a per-expression timeout, and demands that the value at three sample points is unchanged. It currently clears 5 585 expressions in about four seconds with no value change, no exception and no timeout. Three separate value-changing rewrites and this hang had all survived the example-based suites; a property that must hold for every expression finds the next one without anyone having to guess where it will be.
After integration, verify_integral(integrand, result, variable, n_points: 15) checks the result against independent numerics, never against the engine's own reasoning:
Tolerance was tightened 5%→1% and threshold 80%→90% after the looser bounds accepted ~3%-wrong answers. The same dF/dx==f principle verifies special-function results; an analogous "compute it two ways and compare" verifies every field theorem and transport identity (§14). Principle: a proof the engine generates is trusted only once an oracle it did not write agrees.
integrate_experimental snapshotted the debug log one line before the verify block appended its entry, so [Verify] Verified at 15/15 points never reached the result object. The only way to see it was to switch on Strategy View, which reaches the same conclusion by a different route through integration_tracer.rb. The engine was marking its own homework and then filing the mark in a bin.
probe_integrate_verdict.rb (14 checks), which asserts both the presence of the verdict on solved integrals and its absence on x^x.
Taylor: coefficients f⁽ⁿ⁾(c)/n! from successive symbolic derivatives at the center, rendered as exact rationals when one matches to 1e-9. Laurent EXPERIMENTAL: a pole of order m is detected numerically, the principal part recovered by symbolically simplifying (x−c)ᵐ·f so the pole cancels exactly (evaluating the uncancelled form near a pole is catastrophically ill-conditioned); unstable coefficients truncate the series honestly. Inaccuracy bounded by the Lagrange remainder |R| ≤ M·|x−c|ⁿ⁺¹/(n+1)!, M sampled on [c−½, c+½]. (The old taylor_series.rb is demo scaffolding; the real engine is series_expansion.rb.)
pade.rb)A Taylor polynomial is blind to poles — it diverges outside the circle through the nearest singularity. The [L/M] Padé approximant P(u)/Q(u) matches the Taylor series through order L+M but its denominator puts the poles back: the M×M Toeplitz system Σ c_{k−j} q_j = −c_k (Gaussian elimination, partial pivoting) fixes Q, and the numerator follows by convolution. [3/3] of tan x is (x − x³/15)/(1 − 2x²/5) with a Q-root at √(5/2) = 1.5811 — within 0.7% of π/2 — and keeps tracking far beyond the Taylor radius (ln(1+x) at x=3: Padé 1.3636 vs truth 1.3863, while Taylor-4 gives −12.75). For rational f the table closes: the approximant is exact and reported as such.
((n−1)−1)… exponent subtrees until 0·x⁻¹ = 0·∞ = NaN; exponents are now folded at derivative time (§5).
∫₀¹ x² dx admits no such loop — [0,1] is not a cycle and nothing closes it — so this is a structural limit, not a missing feature. Two shapes do close: an infinite interval (a semicircle at infinity, whose contribution vanishes when deg Q ≥ deg P + 2) and a full period [0, 2π], which closes into the unit circle. The remaining classical families — Fourier-type via Jordan's lemma, and keyhole contours for branch cuts — are not implemented.
Improper integrals of rational P/Q with deg Q ≥ deg P + 2 over (−∞,∞) (and even integrands over (0,∞)) by contour integration: ∫ = 2πi · Σ Res (upper half-plane). Procedure: extract Polynomials → find roots (quadratic formula → rational-root factoring → Durand-Kerner for any remaining degree ≥ 3) → real-axis guard → select upper-half poles → compute residues → sum → 2πi·Σ.
1/(x³+1) returned a bogus finite number).Res = P(z₀)/Q'(z₀); order-m poles use (1/2πi)∮f dz on a small circle (trapezoid is spectrally accurate). The radius was a tiny 1e-5 that bled precision (1/(x²+1)² was 0.5% off); it now adapts to 0.3 × the nearest other pole — exact.1/(x⁶+1), 1/(x⁸+1) work.A finite interval that works, because it closes. Substituting z = e^{iθ} walks the unit circle exactly once, turning a rational function of sin and cos into a rational function of z:
The integrand is converted to a numerator/denominator pair of complex polynomials by walking the AST (polynomial.rb is real-only, so this uses a small local complex-coefficient helper), roots come from a complex Durand-Kerner, and residues use N/Q′ for simple poles or a small numerical contour for higher order — 1/(2+cos θ)² exercises the double-pole path. Verified examples: ∫₀^2π dθ/(a + cos θ) = 2π/√(a²−1), ∫₀^2π dθ/(5 − 4cos θ) = 2π/3, ∫₀^2π dθ/(1 + sin²θ) = π√2, ∫₀^2π cos⁴θ dθ = 3π/4.
1/(1 + cos θ) is declined, mirroring the real-axis guard of Type 1. Integrands that are not rational in sin/cos (exp(cos θ), a bare θ, or cos(2θ) whose argument is not the bare variable) are refused rather than approximated. Every accepted result is then checked against periodic trapezoid quadrature, which converges exponentially for smooth periodic integrands and so is an unusually sharp oracle; a disagreement above 1e−6 relative causes the contour result to be withdrawn. 23/23 probe cases.
String#to_f, so "2*pi" silently became 2.0 and "pi" became 0.0 — the engine integrated over the wrong interval and reported the answer as if it were the requested one. Harmless-looking until [0, 2π] became a meaningful interval. Bounds now go through the engine's own parser (which substitutes π at lex time), so 2*pi, pi/2 and 2*pi/3 all work; ∫₀^π sin x now returns 2 rather than 0.
Not numeric and not experimental: every component is an exact symbolic derivative from the same code path as the Derivative button (§5). One module, vector_field_ops.rb, serves both transports — the Sinatra route and the in-browser wasm dispatcher call it identically, so the two cannot drift apart.
The design is the signatures. Two operators consume a scalar and two consume a whole vector field, and the dialog asks for whichever the operator actually takes:
| Operator | Takes | Returns | Dimensions |
|---|---|---|---|
∇f gradient | scalar | vector, n components | any n |
∇·F divergence | vector, n components | scalar | any n |
∇×F curl | vector, n components | vector in 3D; scalar in 2D | n = 2 or 3 only |
∇²f Laplacian | scalar | scalar | any n |
F to ∇f, taken from the input bar. That is not a shortcut, it is a different operator — ∇·(∇f) is the Laplacian. Asking for a divergence and being handed a Laplacian is a correct answer to a question nobody posed, which is only marginally better than a wrong one. The Laplacian is now its own mode, and F is always exactly the field that is on screen. The convenience survives as an explicit fill from ∇f button, which writes the gradient's components into the boxes where they can be seen and edited — so the identities are still one click away, but nothing is substituted behind the user's back:
Dimension is a slider, and it gates the menu. Grad, div and the Laplacian are defined for every n; at n = 1 they degenerate to f′, F′ and f″, which the engine computes and labels rather than forbidding — watching the general formula collapse to the familiar one is the point. Curl is the sole exception, and for a structural reason rather than an implementation limit. The curl is the antisymmetric part of the derivative, which has n(n−1)/2 independent components:
Only at n = 3 does the component count coincide with the dimension, which is the entire reason the cross product and the curl exist in their familiar form. So the dialog offers curl at n = 2 and n = 3 and disables it everywhere else, naming the count it would have had; if the dimension changes out from under a selected curl, the menu falls back rather than computing something that is not one. Fabricating a 4-component "curl" would be exactly the failure mode the rest of the engine exists to avoid.
What it refuses, and why. Each of these would otherwise produce a confident wrong number. An axis named pi, e, or any function name is refused: the parser substitutes those textually before a variable lookup can happen (§3), so ∂/∂e would come back as 0 and the echoed input would read 2.718^2. A repeated axis name is refused: two slots called x are not two dimensions, and ∇·F over [x, x] would add ∂F/∂x to itself and report twice the true divergence. A vector mode with a missing or mis-counted component is refused rather than padded. And the dialog caps at six axes — stated up front, never silently truncated.
The rest of §14 sits under the "Field Theorems & Moving Domains" launcher, is numeric, and shares one doctrine: compute the quantity two independent ways and show they agree. Tangents, curls and divergences inside them are the same symbolic partials as above (§5); region/domain integrals use Monte-Carlo with point-in-polygon / inside tests; the parameter integral uses Simpson.
| Tool | Direct | Theorem cross-check |
|---|---|---|
| Line integral (2D) | work ∮ P dx + Q dy / flux ∮ P dy − Q dx / arc-length | Green & divergence over the enclosed region (MC). |
| Surface (3D) | scalar ∬ f dS / flux ∬ F·dS, symbolic r_u × r_v | Stokes: boundary ∮ F·dr vs flux of curl. Gauss: surface flux vs MC volume integral of div. |
| Leibniz 1D/2D/3D, Reynolds | numeric d/dt ∫_Ω f over a deforming interval / ellipse / ellipsoid / sphere | bulk ∫_Ω ∂f/∂t + boundary motion (flow-map). |
The transport tools use the flow-map identity: a fixed unit ball B is mapped by φ_t(u) = c(t) + A(t)u, so the boundary-motion term comes from symbolic det(A)', ∇f, φ' over B — no surface quadrature. The LHS is an independent central difference of I(t) using common-random-number Monte Carlo (the same samples rescaled at t±h).
Lebesgue EXPERIMENTAL (lebesgue.rb): the genuine layer-cake construction (range partition), ∫f dμ = ∫₀^∞ μ({f > t}) dt with f = f⁺ − f⁻, level-set measures on a grid, cross-checked against Simpson. Complex/Euler (complex_operations.rb): to_complex_form rewrites trig/hyperbolic into e^(ix) form (imaginary units as string leaves). Quaternions: i, j, k are opaque symbolic constants worked componentwise through differentiation/integration; full non-commutative algebra is intentionally unsupported (the simplifier reorders products, which would corrupt signs).
integration_tracer.rb records a thread-local decision tree of TraceNodes (trace_technique wraps each attempt; all top-level attempts hang off one synthetic ∫ solve root) and renders it as an ASCII tree, Mermaid flowchart, or complexity×depth scatter; integrate_verbose drives the live "strategy view." strategy_reporter.rb is a lighter flat logger with a suggest_technique heuristic recommender.
Web: a Sinatra server (web_server.rb, port 4567) stores parsed expressions by id and exposes one endpoint per operation; wasm_dispatch.rb mirrors all 16 routes for the in-browser ruby.wasm build. Frontend: dependency-free vanilla JS — mathHtml syntax-highlights math (functions, numbers, the variable, i/j/k, depth-cycled parentheses), and 3D is software-projected on a 2D canvas (depth-sorted Lambert-shaded quads, rAF-batched, drag to rotate). A result still containing integral turns the panel red with a FAILED badge.
Every other operation here runs forwards: given an expression, produce a derivative, an integral, a series. sindy.rb runs the engine backwards — given only a trajectory, recover the differential equation that produced it. The method is Sparse Identification of Nonlinear Dynamics (Brunton, Proctor & Kutz, PNAS 2016).
Θ(X) holds every candidate evaluated along the trajectory — monomials up to a chosen total degree, optionally sin/cos of each variable. Ξ is found by STLSQ: solve the least-squares problem, zero every coefficient below λ, re-solve on the survivors, repeat to convergence. Sparsity is the prior that turns a mere fit into a candidate physical law — a dense Ξ would interpolate the data equally well and mean nothing.
MathematicalExpression, integrated with RK4. A user-supplied ODE therefore works exactly like a preset — you can type any autonomous system and watch it get rediscovered.Ẋ is the accuracy bottleneck. On clean data the 4th-order 5-point stencil (O(dt⁴)) is used, which is what lets coefficients come back at full precision; with noise the code switches to a Savitzky-Golay window (quadratic fit, widened with the noise level), which smooths and differentiates in one pass.The discovered model is judged only by oracles the regression never saw:
‖f_disc(X) − f_true(X)‖ / ‖f_true(X)‖ at every sampled state. This carries the verdict.| System | Recovered | Vector-field error |
|---|---|---|
| Lorenz (chaotic) | −10x + 10y, 28x − y − xz, −2.6667z + xy | 8.8e−07 — exact, incl. −8/3 |
| Van der Pol | y, −x + 2y − 2x²y | 2.9e−08 |
| Damped pendulum | y, −0.2y − sin(x) | 3.5e−10 |
| Linear / cubic oscillator | both recovered | 6.7e−09 / 4.8e−06 |
Custom −9.8·sin(x) − 0.3y | recovered exactly | 3.6e−08 |
| Pendulum, 1% noise | structure still exact | 2.3e−04 |
sin from the library and ask it to find the pendulum. The truth is now outside the span of Θ, and the regression happily returns a dense polynomial fit — so the verifier must catch it. It does: vector-field error 0.107, verdict FAILED, and the three fabricated terms are flagged spurious in the coefficient table. The engine reports that it could not find the law rather than presenting a plausible-looking equation. This case is in the probe suite (11/11) precisely to keep that behaviour honest.
Cost: ~0.3 s natively; ~8 s under ruby.wasm on the Pages build, dominated by tree-walking the right-hand side through ~30 000 RK4 evaluations. Resolution was kept (rather than traded for speed) because lowering it costs the exact-recovery result.
structure_match flips to false.
ẏ = −sin(x) − 0.2y, and sin(x) is now outside the span of the candidates, so no correct answer exists. The regression does not refuse — it cheerfully returns a dense, plausible-looking polynomial: ẏ = −0.0537 − 0.7875x − 0.1876y + 0.1283x². What the tool does is mark each term against the truth: three of them flagged spurious, the verdict FAILED. That is the difference between a regression and a discovery tool — the regression always answers, and the oracle is what makes the answer mean something.
evaluate_interval walks the same expression tree as ordinary evaluation, carrying [lo, hi] through each operation. Basic floating-point operations widen outward using adjacent representable values. Library transcendental results widen by two ULPs. That last step assumes a bound on the platform math library’s error; it is not independently certified for every input or platform. The UI therefore calls these conditional interval bounds. Exact rational and algebraic checks are a separate capability.
x − x on [0,1] gives [−1,1] when the two occurrences are treated independently. Subdivision narrows that excess. Division by an interval containing zero returns an unbounded interval.x² − x on [0,1], the one-box interval is about [−1,1]. Thirty-two subdivisions narrow it to about [−0.28125,0.03125], containing the true range [−0.25,0]. Then try the narrow Gaussian dip: compare the plotted samples with the region retained by interval subdivision. Wide bounds and unsupported cases are useful diagnostics, too.general_relativity.rb: give a diagonal metric with symbolic parameters and the engine derives Christoffel symbols Γ^λ_{μν} = (∂_μ g_λν + ∂_ν g_λμ − ∂_λ g_μν)/(2g_λλ), the Riemann tensor, and Ricci by contraction — every derivative the engine's own symbolic derivative, composed through to_infix round-trips so M and H stay symbolic in the displayed symbols (Γttr = M/(r(r−2M)), …). Signature (−+++), MTW conventions.
The oracles (all numeric, all independent of the construction):
| Check | What it proves | Measured |
|---|---|---|
| Schwarzschild Ricci ≡ 0 | vacuum — one wrong sign or index anywhere and it fails | max |Rμν| < 1e−7 over random points |
| Kretschmann = 48M²/r⁶ | a NONZERO closed form: "everything is zero" cannot fake it | rel. dev < 1e−6 |
| de Sitter Rμν = 3H²gμν | solves the Λ-vacuum equation; the sign pins the convention | dev < 1e−7·3H² |
| Riemann identities + first Bianchi | antisymmetries, pair symmetry, cyclicity — numeric, scaled to the summed terms | < 1e−9 |
| Geodesic conservation | E, L, 4-velocity norm along an orbit run on the COMPUTED Γ | drift ~1e−8 |
The rosette. A bound orbit (default rp=40M, ra=60M) is integrated by RK4 directly on the computed connection: ẍ^μ = −Γ^μ_{αβ} ẋ^α ẋ^β. The ellipse's axis visibly swings each pass; the measured perihelion advance (24.86°/orbit) is compared against 6πM/ℓ (22.5°) — first-order PN, so the ~10% gap at this field strength is expected and labelled corroboration, not the verdict. Plugging Mercury's real numbers into the same verified formula gives 42.98″/century — the famous 43.
Scope: diagonal metrics (Schwarzschild, de Sitter, FLRW-family, custom). Kerr's t–φ cross term needs a 2×2 block inverse and much heavier simplification — out of scope this pass. Custom metrics get the structural identities only, and the verdict says so. 36/36 probe cases; an adversarial review pass added the honesty regressions (tiny-M Christoffels not pruned, H=0 de Sitter, unbound-symbol refusal, PN mismatch as corroboration rather than verdict).
Sections 18 and 19 pushed on verification. This one pushes on the constraint underneath everything: the float-only numeric core (§3). exact_arithmetic.rb adds a third evaluator beside the two we had, on the same AST. They are not competing implementations — they answer three different questions, and the UI colour-codes them from data the engine itself tags:
| Evaluator | Colour | Answers | Cost |
|---|---|---|---|
evaluate (float) | amber | always, never refuses | silently approximate: 0.1+0.2 → 0.30000000000000004 |
evaluate_interval (interval bounds, §18) | green | conditional enclosure, or refusal | a range, not a number: [0.3, 0.3000000000000001] |
evaluate_exact (ℚ) NEW | purple | exactly, or refuses | only rational answers exist: 0.1+0.2 → 3/10 |
The refusal is the feature. sqrt(2), sin(1), exp(1), ln(5) return no value and say why, rather than degrading to a float. sin(0), cos(0), ln(1), sqrt(9/4) come back exactly. Recovering a rational from a parsed float distinguishes two genuinely different cases and reports which: exact (2.0 is 2; binary fractions are exact) versus recovered (0.1 is not 1/10 — it is 3602879701896397/36028797018963968 — but 1/10 is the only simple rational whose double this is). A double with no small-denominator reading is refused rather than silently rationalised, and a runaway numerator hits a bit-length budget and refuses instead of hanging.
"Is this coefficient zero?" is a guess at 1e−10 tolerance in float and a fact in ℚ. Every decision procedure — Risch, Rothstein–Trager, Gosper — is assembled out of exact zero-tests, which is precisely why none of them could be built on the existing core (§22). ExactPolynomial is the parallel to polynomial.rb, which cannot be retrofitted: its constructor runs coeff.to_f on every coefficient, so a Rational cannot survive being stored, and its GCD normalises by float division.
f = (x − 1/3)⁴ and ask for gcd(f, f′). The true answer is (3x − 1)³, degree 3. The exact path returns exactly that. The float path returns degree 0 — "no common factor at all." That is not a rounding discrepancy but a structurally wrong answer: 1/3 is not a representable double, so a remainder that should be exactly zero never lands on 0.0, and the Euclidean algorithm runs on to a nonzero constant. (x − 1/7)³ fails the same way (exact degree 2, float degree 0). This is the operation Rothstein–Trager is built on.
Res(f, f′) = 0 exactly when f has a repeated root — an exact test where the float path could only compare against a tolerance.The step above made a point of refusing sqrt(2), because √2 is not rational and inventing a rational for it would be a lie. algebraic_field.rb is the field where it no longer has to. An element of ℚ(α) is a polynomial in α of degree < deg(m) with rational coefficients, reduced modulo the minimal polynomial. So √2 is not stored as 1.4142135623730951 (which squares to 2.0000000000000004) — it is stored as the symbol α together with the rule α² = 2, and squaring it gives exactly 2.
ExactPolynomial: solve u·p + v·m = 1, then p⁻¹ ≡ u (mod m). This is the step that makes ℚ[x]/(m) a field rather than a ring, and it is only possible exactly — a float Euclid never terminates cleanly. (1+√2)⁻¹ = √2−1, φ⁻¹ = φ−1, (∛2)⁻¹ = (∛2)²/2, all exact.gcd(p, m) has positive degree, that failure is reported as what it actually is: a proof that m factors. Probed with (a²+1)(a²+2), which passes both cheap checks and is then caught by the inverse.
Scope: strictly additive and switchable — a mode on the existing ℚ tool, not a new one. No existing operation is routed through the exact path, the float default is untouched, and "float only" reproduces precisely what the engine did before any of this existed. 44/44 + 40/40 probe cases. What is still not here: polynomials over ℚ(α) (the immediate next step — RT's gcd(f − c·g′, g) has coefficients in this field), factorisation over ℚ, and the differential-field tower.
0.1 + 0.2 − 0.3 in compare mode. This is the zero-test in its sharpest form, and the three lanes take three genuinely different epistemic positions on the same question. Float answers 5.551115123125783e−17 — it says the expression is not zero, and it is wrong. The interval evaluator answers [−5e−324, 1.1102230246251568e−16] — an interval straddling zero, which is the honest statement that double precision cannot decide this. Exact answers 0. Not "approximately zero": zero. Plain 0.1 + 0.2 shows the same shape — float 0.30000000000000004, exact 3/10, delta 5.55e−17.
sqrt(2). The exact lane returns no value, its colour drops to grey, and it says: "sqrt(2/1) is irrational. Refusing beats inventing a rational that is not there." The refusal is the product. In ℚ(α) the same √2 becomes representable — α with the rule α² = 2, where squaring gives exactly 2 rather than 2.0000000000000004, the inverse comes from the extended Euclid as ½α with a product of exactly 1, and the norm is computed two independent ways that must agree. Note what the field report claims and what it does not: for α² − 2 it says PROVEN irreducible — squarefree with no rational roots, and for degree 2 that is sufficient. Above degree 3 irreducibility is assumed, and the report says assumed.
headless_test.rb — runs the UI's example set through the engine, verifying each by independent numerics (current: ok = 159, xfail = 1).comprehensive_test.rb + compare_results.rb — a ~174-case, version-agnostic battery (takes an engine directory argument) for A/B comparison between revisions.test_associativity.rb — the 17-case parser-precedence guard.(4/3)π(abc)', series coefficients). The doctrine: verify against an oracle you didn't write.Despite the filename experimental_risch.rb, integration is the deterministic heuristic cascade of §6 — a fixed strategy order. The true Risch algorithm is a decision procedure: it returns an elementary antiderivative or proves none exists (Liouville's theorem). That needs a differential-field tower, Hermite reduction, Rothstein–Trager (resultants), and the Risch differential equation — none present. The only probabilistic component here is the verifier.
sin(pi) ≈ 1.2e−16) those tests are meaningless — so Risch is gated on the exact-rational/algebraic-number rewrite (the same one motivating a C++/WASM-native port).1/(x⁴+1) and the general P/Q log cases — today's biggest coverage gap.
gcd(f − c·g′, g) can be computed where c is algebraic), and factorisation over ℚ for the general case. Step 2's differential-field tower is untouched. The gate is open; RT itself is not implemented and is not claimed to be.
1/(x⁴+1) — elementary but unsolved; needs irrational quadratic factoring.A^x, e^(At)) — need a matrix value type + non-commutative products; assessed HARD, not shipped.Every section above says what the engine does. This one says how that compares to the tools a professional would actually reach for, area by area, without flattering the project. The reference points are Mathematica and Maple (commercial), SymPy and SageMath/PARI (open source), Arb and INTLAB (rigorous numerics), xAct and SageManifolds (differential geometry), and PySINDy (equation discovery).
| Area | State of the art | This engine | Verdict |
|---|---|---|---|
| Differentiation | A solved problem everywhere. Rule-based, exact, fast. | The same: rule-based and exact, over a float-valued AST (§5). | On par. Nothing here is behind; differentiation is the easy direction. |
| Indefinite integration | The Risch algorithm — a decision procedure that returns an elementary antiderivative or proves none exists. Implemented substantially in Mathematica and Maple, partially in SymPy. | A deterministic heuristic cascade of ~17 ordered phases (§6), explicitly not Risch, with every result checked numerically (§11). | Behind, structurally. A cascade cannot prove non-existence; it can only fail to find. And Risch is not merely unimplemented here — it is unavailable, because it presupposes exact arithmetic the float core cannot provide (§22). |
| Simplification | Nobody has solved it. Deciding whether an expression is zero is undecidable for a rich enough class (Richardson's theorem), so every CAS ships heuristics and every CAS occasionally embarrasses itself. | A fixed-point rewriter (§10), weaker than Mathematica's, roughly comparable to a basic sympy.simplify on polynomial and rational forms. |
Behind on coverage, ahead on one habit. The like-term collector refuses to apply itself unless it can show the value is unchanged. Mainstream CASes do not numerically self-check their rewrites — and this engine shipped three value-changing rewrites before adopting the habit, so the caution is earned rather than decorative. |
| Conditional interval bounds | Arb (arbitrary-precision ball arithmetic, the engine under SageMath's rigorous numerics), INTLAB, MPFI, Julia's IntervalArithmetic.jl with branch-and-bound global optimisers. | Outward-rounded interval evaluation over the same AST, branch-and-bound global minimisation, Darboux integral brackets (§18). | Right in kind, small in scale. Transcendental enclosures assume a two-ULP library error bound. This is double-precision only where Arb is arbitrary-precision, handles 1D/2D searches rather than general dimensions, and has much less special-function coverage. The ideas are current; the implementation is a demonstration of them. |
| Exact arithmetic & algebraic numbers | Complete and routine: ℚ, ℚ(α), factorisation over ℚ (Zassenhaus, van Hoeij), full ideal machinery. PARI/GP and Sage do this at research scale. | A deliberately partial gate bolted onto a float core (§20): exact ℚ evaluation that refuses when no rational answer exists, exact polynomial GCD/squarefree/resultants, and ℚ(α) as polynomials modulo a minimal polynomial. | Far behind, and honest about it. The sharpest gap: irreducibility of the minimal polynomial is proven only up to degree 3 and assumed above, which the tool states rather than hides. A real system factors over ℚ properly and never has to assume. |
| Padé approximants | Classical (1892) and standard everywhere. The modern refinement is SVD-based robust Padé (Gonnet–Güttel–Trefethen) for suppressing spurious Froissart doublets. | Toeplitz solve for the denominator, Durand–Kerner for its roots, plus an amplitude-relative test that flags spurious poles (§12). | Comparable for the textbook case. The spurious-pole detection is simpler than the SVD approach but does the job it claims: it labels Froissart artifacts instead of presenting them as structure. |
| Contour integration | General residue machinery with automatic contour selection over wide classes of integrand. | Two contours: a semicircle at infinity for improper rational integrals, and the unit circle for full-period trigonometric ones (§13). | Narrow. Correct and verified within those two families, and it declines outside them rather than guessing. |
| Symbolic differential geometry | xAct/xTensor (Mathematica), SageManifolds, EinsteinPy, GRTensor — arbitrary metrics, index gymnastics, full tensor algebra. | Diagonal metrics only; Christoffel → Riemann → Ricci → Kretschmann, symbolic in the parameters, checked against known oracles (§19). | Far behind in scope, unusually rigorous in verification. Kerr is out of reach (the t–φ cross term needs a block inverse). But checking against a nonzero closed form — Kretschmann = 48M²/r⁶ — is a stronger self-test than most tutorial implementations attempt, because collapsing everything to zero cannot fake it. |
| Equation discovery | SINDy (Brunton, Proctor & Kutz, PNAS 2016) with PySINDy as the reference implementation; the field has since added weak-form, implicit (SINDy-PI) and control variants. | The core method faithfully: candidate library, sequential thresholded least squares, verification against the true vector field (§17). | The most current thing here. The algorithm is genuinely modern research and the implementation is honest — including reporting FAILED when the truth lies outside the library's span. It has none of the later variants, and the library is small. |
e is rejected rather than silently read as 2.718. That is a design stance, not an algorithmic achievement — but it is the stance that makes a small engine trustworthy.
∫₋₁¹ dx/x² is divergent and the engine reported −2 — a negative area under a strictly positive integrand, which no correct answer could be. ∫₋₁¹ dx/x reported 0 without calling it a principal value, and ∫₋∞∞ dx/(x³+1) reported 1.79 by Simpson after the residue route had correctly declined the very same integral. A continuity check now runs first (§6), and all three refuse. The lesson is the one this whole document keeps arriving at: a doctrine is only as good as the check that enforces it, and the claim was worth less than the ten minutes spent testing it.
HEAD, which is how seven silently mis-printed antiderivatives were caught — they had been passing their numeric checks for months because the checks read the tree while the bug was in the printer.
The fair summary: as a computer algebra system this is a competent, unusually well-verified undergraduate-to-graduate scale engine, decisively behind the established tools in coverage and depth, and behind them structurally in integration until exact arithmetic replaces the float core. As a demonstration of how to know whether a symbolic result is right, it is doing something the big systems largely do not.
Symbolic Math Engine — Ruby + Sinatra + vanilla JS (+ ruby.wasm) — Back to Engine · History Part 3