Technical Reference

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/.

One-paragraph summary. A pure-Ruby CAS on a binary-tree AST. Differentiation is exact rule-based recursion. Integration is a deterministic heuristic cascade of ~17 ordered phases (not Risch), each gated by a structural predicate; the first to return a marker-free result wins, and anything unsolved becomes an honest 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.

Contents

1 · Architecture, object model & data flow

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.

text→ Parser→ AST→ operation module→ Normalize / Simplify→ Verify (MC)→ JSON→ render

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.

Object model

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.

ExpressionParser «class methods» parse(s) → ExpressionNode tokenize · reverse shunting-yard ExpressionNode binary AST node isleaf · data · type left · right · parent evaluate(vars) → Float deep_clone · connect left / right (recursive) Polynomial canonical form @coeffs {deg→coeff} divmod · gcd · roots MathematicalExpression «facade» — one object, every operation root : ExpressionNode variables · steps · @use_experimental_* includes 20 capability modules ▷ derivative · integrate · series_expand · … MathExpression «factory» parse() · from_file() builds to / from root ◆ composition parses via uses creates
Object model. Solid diamond ◆ = composition (the facade owns its root node); dashed arrows = uses / creates. No class inherits from another — every relationship is composition or mix-in.

Capability modules

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.

MathematicalExpression — the facade .derivative · .integrate · .series_expand · .try_residue_integration · … ▼ includes 20 capability modules — dispatched by respond_to? 20 mix-in modules · grouped by concern parse / normalize Normalizer Simplification TrigIdentities integration cascade IntegrationOperations ExperimentalRisch GeneralUSubstitution IntegralTable RationalIntegration special / series SpecialProducts SpecialFunctions SeriesExpansion definite / residue DefiniteIntegral ResidueTheorem Lebesgue differentiation DerivativeOperations exact rule-based recursion verify / trace / viz Verification StrategyReporter IntegrationTracer TreeVisualizer vector calculus VectorCalculus gradient · laplacian · directional · differential (instance) divergence / curl also exposed as VectorCalculus.* (standalone) POST · standalone services — invoked directly by web_server.rb / Engine.dispatch, NOT mixed into the facade ComplexOperations TaylorSeries (legacy) VectorIntegrals SurfaceIntegrals LeibnizRule LeibnizHigher ReynoldsTransport Engine — wasm_dispatch each returns a closed-form-or-cross-check result; several build their own MathematicalExpression instances internally
The facade is a thin shell: 20 included modules supply every operation. The dashed lower band holds the POST services, which live outside the facade and are called straight from the transport layer.

The two transports — one engine

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.

Two transports, one engine Browser vanilla-JS UI Sinatra route web_server.rb · /api/* · :4567 server-side Ruby process ruby.wasm fetch shim wasm_runtime.js → Engine.solve in-browser · no server (Pages) MathematicalExpression facade + 20 modules Normalize · Simplify Verify (MC) dF/dx ≟ f → JSON native Pages wasm_dispatch mirrors every Sinatra route → identical results JSON response → render
Native (Sinatra) and GitHub Pages (ruby.wasm) are interchangeable front doors onto one engine; the verifier gate and JSON shape are shared.

1.5 · The six self-contained tools — what each one does

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).

⟦f⟧ Interval bounds conditional

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.

Q Exact ℚ, ℚ(α) — exact arithmetic proven

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.

ẋ=Θξ Discovery — SINDy verified

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.

gμν Relativity — symbolic differential geometry verified

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.

∮∬∭ Field Theorems & Moving Domains EXPERIMENTAL

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.

Why there is no E badge on the buttons. An earlier build tagged the numeric tools with a small E. It was dropped, because it labelled the wrong thing: for these tools the closed forms are standard and not in doubt — what varies is the quality of the evaluation (Monte-Carlo noise, numerically detected pole orders, sampled derivative stencils). A single badge on the button could not say which, and it competed with the per-result verdict, which states the actual evidence far more precisely: conditional interval bounds, verified against an oracle, corroborated two ways, or FAILED. Read the verdict on the result, not a badge on the button.

1.6 · Every mode, and what stands behind it

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.

Five kinds of evidence. CONDITIONAL INTERVAL BOUND — an outward-rounded enclosure; transcendental guarantees assume the platform library error fits within two ULPs. VERIFIED — checked against an oracle independent of the computation: a closed form, a finite difference, a conservation law, the true vector field. SELF-CONSISTENT — computed two ways by the same engine from the same parsed tree and shown to agree. Strong evidence that the implementation is coherent; no evidence that the model is right. EXACT, UNCHECKED — symbolic and correct by construction (differentiation), with no check run because none is thought necessary. HEURISTIC — a search or a rewrite with no normal form, no completeness claim, and no verdict.

The sixteen panel buttons

ModeConsumesProducesAlgorithm (module)Evidence
∫ Integratethe formula barantiderivative + C, steps, trace logsordered 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 Definitebar + bounds dialoga value, a method badge, region plotfour 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 Residuebar + boundsvalue, poles with multiplicity, contourtwo 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 Derivativebar + variablederivative, rule-by-rule steps, treeexact 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 Differentialbar + variablesΣ (∂f/∂xᵢ)dxᵢ with per-term breakdownone exact partial per variable (vector_calculus.rb)EXACT, UNCHECKED
Σ Taylorbar + centre, orderterms as exact rationals, plot, remaindersuccessive 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, centreP/Q, poles flagged genuine or spurious, plotToeplitz 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.
Σ± Laurentbar + centre, orderseries with a principal part, truncation flagnumeric 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.
⚡ Simplifythe current expressionbefore/after, named rewrite stepsrewrite 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.
🔢 Evaluatebar + variable valuesone floatdirect recursive float evaluation (§4)This is the float lane that §20 exists to contrast against.
∇ Grad · Div · Curl · Laplacianits own dialog: dimension slider, axis names, then a scalar or one component per axisthe intermediate partials, then a vector or scalar, with the arity statedexact 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 boundsits own dialog onlynaive vs certified enclosure, minimiser boxes, integral bracketinterval 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 onlythree lanes side by side, or a polynomial lab, or a field reportexact rationals, primitive-PRS GCD, Yun squarefree, ℚ[x]/(m) (§20)EXACT — or an honest refusal, which is the actual product.
ẋ=Θξ Discoveryits own dialog onlythe recovered equations, per-term coefficients, spurious flagscandidate 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μν Relativityits own dialog onlyΓ, Ricci, Kretschmann, a precessing orbitsymbolic 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——

The seven inside the launcher

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.

ToolWhat it computesCross-check, and its tolerance
∫μ Lebesguepartitions the RANGE — ∫f dμ = ∫μ({f>t})dt over 64 level setsSimpson 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 integralwork / flux / arc length with symbolic tangentsGreen'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 Surfaceflux on a parametric surface, symbolic ru×rv and curlStokes 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 1Dmoving interval [a(t), b(t)], boundary + bulk splitan independent numeric d/dt of the whole integral; 0.1% relative.
Leibniz 2D / 3Ddeforming ellipse / ellipsoid via the flow map φt(u) = c(t) + A(t)usame 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.
Reynoldsthe physics framing: d/dt∭f dV = ∭∂f/∂t + ∬f(vb·n)common-random-number Monte-Carlo finite difference; 2% relative. Measured 0.05% apart.

2 · Full module map

FileWhat it does (algorithm in one line)
mathematical_expression.rbCore class; mixes in all modules; holds the root node + mode flags.
parser.rbTokenize → normalize → reverse shunting-yard → prefix → tree. Owns OPERATORS precedence + FUNCTION_NAMES.
expression_node.rbExpressionNode (binary tree); float evaluate; special-function quadrature.
normalizer.rbNon-mutating 9-step canonicalization pipeline (fold, identities, radicals, power-products, polynomial collection, GCD cancel).
polynomial.rbSparse {degree=>coeff} polynomial: arithmetic, divmod, gcd, rational-root factoring, discriminant.
tree_visualizer.rbto_readable_string, to_latex (special fns as \operatorname), ASCII tree.
derivative_operations.rbExact rule-based differentiation; one ordered case over node type.
experimental_risch.rbThe integration cascade orchestrator (risch_integrate_inner, ~17 phases). Misnomer — see §22.
integration_operations.rbintegrate entry + dispatch; compute_integral standard integrator; finalize_integration.
integral_table.rbOrdered TABLE_ENTRIES pattern table (arctan, ln-linear, arcsin, sec²/csc², …).
general_u_substitution.rbu = g(x) candidate collection, ranking, factor-match against du/dx, back-substitution.
trig_identities.rbOrdered rewrite pre-pass (Pythagorean, power-split, half-angle, product-to-sum…).
rational_integration.rbLong division → linear/irreducible-quad → completing-square → partial fractions.
special_products.rbClosed forms: poly·exp·trig reduction recursion, mixed odd trig powers, poly·trig².
special_functions.rbNon-elementary integrals → erf/Si/Ci/Ei/li/Fresnel nodes.
simplification.rbFixed-point pass rewriter; constant folding; track_steps.
verification.rbMonte-Carlo dF/dx==f check (15 pts, 1% tol, 90% threshold).
series_expansion.rbTaylor (symbolic derivatives) / Laurent (flow-map cancel) + Lagrange remainder.
residue_theorem.rbImproper rational integrals via contour residues; real-axis guard.
definite_integral.rbFTC → known special forms → Simpson; Monte-Carlo mode; plot samples.
lebesgue.rbLayer-cake (range partition) integral; Riemann cross-check.
vector_integrals.rb2D line integrals (work/flux/arc) + Green/divergence cross-check + parameter sweep.
surface_integrals.rbParametric surfaces; scalar dS / flux / Stokes / Gauss (two-way).
leibniz_rule.rb, leibniz_higher.rb, reynolds_transport.rbDifferentiation under the integral over moving 1D/2D/3D domains (flow-map).
complex_operations.rbTrig → complex-exponential (Euler) rewrite.
integration_tracer.rb, strategy_reporter.rbThread-local decision-tree / flat-log instrumentation for the live view.
exact_arithmetic.rb · algebraic_field.rb · exact_lab.rbExact rationals over the same AST; ExactPolynomial (GCD / squarefree / resultant / ext-Euclid); ℚ(α) algebraic extensions; the float-vs-interval-vs-exact comparison (§20).
sindy.rbDiscovers the governing ODE from trajectory data: candidate library + sequential thresholded least squares (§17).
pade.rbRational [L/M] approximant from Taylor coefficients; captures poles, flags Froissart artifacts (§12).
interval_arithmetic.rbOutward-rounded evaluate_interval + conditional range / global-min / integral bounds (§18).
general_relativity.rbDiagonal metric → Christoffel → Riemann → Ricci, with vacuum/Kretschmann/de Sitter oracles + geodesics (§19).
web_server.rb · wasm_dispatch.rbSinatra routes · the in-browser ruby.wasm dispatch mirroring them.

3 · Parser

All class methods on ExpressionParser; entry is parse(input) = tokenize → infix_to_prefix → build_tree.

The associativity fix. Because the list is reversed, 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).
A silent wrong answer, and why 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.

It also poisoned composition. 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).
The scanner dropped what it could not read. 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).

4 · AST & evaluation

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:

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.

5 · Differentiation (rule order)

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}.

  1. Leaf base cases: x → 1, constant → 0.
  2. + sum, - difference.
  3. * product u'v + uv'; / quotient (u'v − uv')/v².
  4. ^ 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).
  5. Trig (chain ·u'): sin cos tan cot sec csc; hyperbolic sinh cosh tanh sech csch coth; inverse hyperbolic arcsinh arccosh arctanh.
  6. ln, exp; inverse trig arctan arcsin arccos; sqrt → 1/(2√u)·u'.
  7. Special functions (one shared branch): 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'.
  8. abs → (u/|u|)·u'. Fallback: unknown operator → 0.

6 · The integration routine — exact order

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.

Depth / budget guard. 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.

  1. Division u-sub on the raw form. if 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⁻¹.
  2. Special-function forms. on the raw shape — special_function_integral → erf, Si, Ci, Ei, li, Fresnel (§9).
  3. Normalize (rewrite) — normalize_expression (§10), then algebraic_simplify.
  4. Table lookup. table_lookup_integral — the fastest closed-form path (§8).
  5. General u-substitution. if node.data == '*' — attempt_general_u_sub; logs the candidate list. Runs before trig identities.
  6. Special product families. if node.data == '*' — attempt_special_product: poly·exp·trig, mixed odd trig powers, poly·trig² (§9).
  7. Trig identity rewrite (rewrite) — apply_trig_identities (§8).
  8. Distribute product over sum (recurse). if * over +/- — expand and re-integrate.
  9. Product-to-sum trig (recurse) — apply_product_to_sum (sin·cos, sin·sin, cos·cos).
  10. Inverse trig / lnⁿ. if is_inverse_trig? — integrate_inverse_trig (with an ln^n reduction).
  11. Rational function. if is_rational_function? — attempt_rational_integration → partial fractions (§8). Highest-priority "type" phase.
  12. Trig power reduction. if is_trig_power? (sin/cos/…ⁿ, n>1) — attempt_trig_power_reduction.
  13. Quadratic radical / trig sub. if has_quadratic_radical? — attempt_radical_integration.
  14. Smart substitution. if can_apply_smart_substitution? — attempt_smart_substitution. Deliberately before by-parts.
  15. Integration by parts. if 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.
  16. Fallback. compute_integral — the standard integrator (§7); returns a marker if it too cannot solve.
∫ f(x) dx [0.0–0.05] raw-form catches — ÷ u-sub · special functions [0] normalize → algebraic simplify [0.1–0.22] table lookup · general u-sub · special products [0.25–0.5] trig identities · distribute · product→sum · inv-trig [1–5] type handlers: rational · trig^n · radical · sub · by-parts ✓ marker-free → return now first match wins — no later phase runs match? → ∞ compute_integral — standard integrator (fallback) finalize · Verify (MC: d/dx ≟ f, ≥ 90% of 15 pts) ✓ verified → closed form shown · ✗ unsolved → honest integral( ) marker · FAILED
The cascade as a funnel: each phase is tried in order; the first marker-free result returns immediately (green lane). Anything unsolved falls to the standard integrator, and every result must pass the independent Monte-Carlo verifier — unsolved parts collapse to an honest marker rather than a wrong answer.
Honest-marker finalize. 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.
The Fundamental Theorem needs a continuity check, and did not have one. 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.

A continuity check now runs before any of the three routes. Finding a singularity is only half of it, because an interior one does not imply divergence — ∫₋₁¹ 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:
convergent slivers collapse 1/√|x| → 0.137, 0.043, 0.014 log slivers hold steady 1/x → ln 10 each time power slivers grow 1/x² → 900, 9 000, 90 000
A side that diverges makes the integral divergent even when the two sides cancel — that is ∫₋₁¹ 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.

7 · The standard integrator

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:

NodeHandling (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 / transcendentalwhole-node constant → c·x; argument = variable → the known antiderivative; linear argument ax+b → (1/a)-scaled (sin/cos/exp); else marker.
elseunknown operator → integral marker, "cannot integrate."

It never raises — failure is always an ExpressionNode("integral") wrapping the integrand, detected everywhere by contains_integral_marker?.

8 · Integration technique modules

integral_table.rb — the lookup table

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:

#FormResult
1c/(x²+a²)(c/a)·arctan(x/a)
2c/(ax+b)(c/a)·ln|ax+b|
3c/√(a²−x²)c·arcsin(x/a)
4c/(x²−a²)(c/2a)·ln|(x−a)/(x+a)|
5–6tan·sec, cot·cscsec, −csc
7sec²·tan½sec²
8–9sec² / csc² (and cos⁻², 1/sin² encodings)tan / −cot

general_u_substitution.rb

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.

trig_identities.rb — ordered rewrite pre-pass

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.

rational_integration.rb

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.

9 · Special products & functions

special_products.rb

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.

∫ xⁿ e^(ax) sin(bx) dx via I(n,sin) = xⁿ·Eₛ − n[a·I(n−1,sin) − b·I(n−1,cos)]/(a²+b²) Eₛ = e^(ax)(a·sin bx − b·cos bx)/(a²+b²) (a = 0 degenerates to the plain poly·trig recursion)

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.

special_functions.rb

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).

10 · Normalizer, Polynomial & Simplify

normalizer.rb — canonicalization (non-mutating)

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).

polynomial.rb

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).

simplification.rb

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.

A representative bug class. The + 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.
A second constant-folding bug, in the sibling branch. The + 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.

Every 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).
A dropped coefficient, and a dropped parenthesis. Two more of the same family, found together.

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.

Guarded by 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.
A rewrite that checks itself. The collectors above are polynomial-shaped — they go through 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.

It refuses to apply itself unless it can show the value is unchanged. Two of the bugs documented above were rewrites that silently altered an expression, and this one is more aggressive than either, so before replacing anything it evaluates the old and new subtrees at several sample points and abandons the rewrite on any disagreement (points where either side is non-finite prove nothing and are skipped, but at least one usable comparison is required). The probe tests the guard in both directions — that a correct collection is accepted and that one with a deliberately wrong coefficient is rejected — because a guard that only ever says yes is not a guard.
A hang, found by fuzzing rather than by guessing. 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.

How it was found matters more than the fix. 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.

11 · Verification — the honesty layer

After integration, verify_integral(integrand, result, variable, n_points: 15) checks the result against independent numerics, never against the engine's own reasoning:

for 15 sample points x: F'(x) ≈ (F(x+h) − F(x−h)) / 2h must match the integrand f(x) accept a point if relative error < 1% · accept the result if ≥ 90% of points pass

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.

A check whose outcome nobody sees is not a check. This verification has run on every experimental integration since it was written, and its verdict went nowhere. 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.

The snapshot now happens after verification, and the verdict is carried as a first-class field rather than a log line, so both transports hand it to the client and it appears as a badge beside the antiderivative. The absence of a badge is meaningful too: when the result still holds an unevaluated integral marker there is nothing to differentiate, so no check is attempted and none is claimed. Guarded by probe_integrate_verdict.rb (14 checks), which asserts both the presence of the verdict on solved integrals and its absence on x^x.

12 · Series — Taylor / Laurent / Padé

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.)

Padé approximants (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.

Spurious poles, flagged. Finite Padé tables can produce Froissart doublets — poles of Q where f is perfectly finite. Every real Q-root is therefore tested against f itself (does |f| actually blow up in a neighbourhood scaled to the pole-location error?); artifacts are labelled SPURIOUS in the UI rather than hidden. Verification: defining-relation residuals + numeric agreement with f near the center and across the window. 34/34 probe cases (amplitude-invariant pole tests and distant-pole root-finding hardened by an adversarial review pass). Fixing the Taylor-coefficient pipeline also uncovered and fixed a derivative-core bug: repeated differentiation accumulated ((n−1)−1)… exponent subtrees until 0·x⁻¹ = 0·∞ = NaN; exponents are now folded at derivative time (§5).

13 · Residue theorem

Why not every definite integral? The residue theorem computes contour integrals. To use it on a real integral you must embed that integral in a closed loop whose remaining part you can evaluate. ∫₀¹ 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.

Type 1 — improper rational integrals

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·Σ.

Three correctness fixes (found by a 16-case headless battery before de-experimentalizing): 16/16 closed forms exact, 4/4 inapplicable cases declined.

Type 2 — trigonometric integrals over a full period NEW

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:

cos θ = (z + 1/z)/2 sin θ = (z − 1/z)/(2i) dθ = dz/(iz) ∫₀^2π R(cos θ, sin θ) dθ = ∮|z|=1 R(…) · dz/(iz) = 2πi · Σ Res inside |z| < 1

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.

Refusals and the oracle. A pole on the unit circle means the integral diverges or is principal-value only — 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.
A bug this feature exposed. Both transports parsed integration bounds with 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.

14 · Vector calculus & transport

∇ Grad · Div · Curl · Laplacian — one driver, exact partials proven

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:

OperatorTakesReturnsDimensions
∇f gradientscalarvector, n componentsany n
∇·F divergencevector, n componentsscalarany n
∇×F curlvector, n componentsvector in 3D; scalar in 2Dn = 2 or 3 only
∇²f Laplacianscalarscalarany n
The mistake this replaced. An earlier build tried to save the user from typing a vector field: for curl and divergence it defaulted 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:
fill from ∇f, then ∇×F → (0, 0, 0) curl of a gradient, always fill from ∇f, then ∇·F → ∇²f which is what the Laplacian mode computes directly

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:

n = 1 → 0 components nothing for a curl to be n = 2 → 1 component a SCALAR: ∂F_y/∂x − ∂F_x/∂y n = 3 → 3 components a vector ← the coincidence that makes ∇×F a vector at all n = 4 → 6 components a bivector, not a vector n = 5 → 10 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 transport tools

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.

ToolDirectTheorem cross-check
Line integral (2D)work ∮ P dx + Q dy / flux ∮ P dy − Q dx / arc-lengthGreen & divergence over the enclosed region (MC).
Surface (3D)scalar ∬ f dS / flux ∬ F·dS, symbolic r_u × r_vStokes: boundary ∮ F·dr vs flux of curl. Gauss: surface flux vs MC volume integral of div.
Leibniz 1D/2D/3D, Reynoldsnumeric d/dt ∫_Ω f over a deforming interval / ellipse / ellipsoid / spherebulk ∫_Ω ∂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).

I(t) = det A(t) · ∫_B f(φ_t(u), t) du d/dt I = det A · ∫_B ∂f/∂t du + (det A)' ∫_B f du + det A ∫_B ∇f·φ' du └─ bulk = ∫_Ω ∂f/∂t ──┘ └──────── boundary motion = ∮/∬_∂Ω f (v_b·n) ────────┘

15 · Lebesgue & complex / quaternion

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).

16 · Tracer, strategy reporter & web/frontend

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.

17 · Equation discovery — SINDy

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) · Ξ Θ = library of candidate terms · Ξ = coefficients, forced SPARSE

Θ(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.

Pipeline

Verification — and a chaos caveat

The discovered model is judged only by oracles the regression never saw:

  1. Vector-field agreement — ‖f_disc(X) − f_true(X)‖ / ‖f_true(X)‖ at every sampled state. This carries the verdict.
  2. Re-simulation — RK4-integrate the discovered model from the same initial condition and compare trajectories.
Why the trajectory check cannot carry the verdict. For a chaotic system (Lorenz) two nearby trajectories separate exponentially even when the model is exactly right. Scoring long-horizon trajectory error would therefore condemn a perfect model. So the re-simulation is scored over roughly one Lyapunov time and reported as corroboration only, with the divergence labelled as genuine physics rather than model error. Reporting it any other way would be precisely the sort of confidently-wrong result the rest of the engine refuses to emit.

Measured results

SystemRecoveredVector-field error
Lorenz (chaotic)−10x + 10y, 28x − y − xz, −2.6667z + xy8.8e−07 — exact, incl. −8/3
Van der Poly, −x + 2y − 2x²y2.9e−08
Damped pendulumy, −0.2y − sin(x)3.5e−10
Linear / cubic oscillatorboth recovered6.7e−09 / 4.8e−06
Custom −9.8·sin(x) − 0.3yrecovered exactly3.6e−08
Pendulum, 1% noisestructure still exact2.3e−04
The honesty test. Remove 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.

What you should see. The engine hides a system's equations from itself and then has to find them again.

Run Lorenz at zero noise. Out of a 10-term library and 30 candidate coefficients, exactly 7 come back non-zero, and they are the right 7:
dx/dt = −10.0·x + 10.0·y dy/dt = 27.9999·x − 1.0·y − 1.0·x·z dz/dt = −2.6667·z + 1.0·x·y
including both quadratic couplings and −8/3 = −2.6667, with a vector-field relative error of 8.8×10⁻⁷. Nothing spurious survives.

Then turn λ down to zero and watch the point of the whole method. With thresholding off, the regression returns 30 of 30 non-zero coefficients and a vector-field error of 1.4×10⁻⁶ — a better fit than the sparse one, and not a law. Every extra term is fitting noise in the derivative estimate. Sparsity is not a tidiness preference; it is the prior that separates a model from a curve fit. Push the noise slider instead and the structure survives to about 0.1% (coefficients within 1%), then breaks: by 1% a spurious constant slips past the threshold and structure_match flips to false.

What honest failure looks like — the test worth running. Ask for the damped pendulum but uncheck the trig library. The truth is ẏ = −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.

18 · Conditional interval bounds

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.

What is checked. Dense samples test range containment and global-minimum lower bounds; integral probes compare against known results and independent quadrature. These checks catch implementation errors. They do not establish a uniform bound on every transcendental-library call. Unsupported functions, unbound variables and invalid domains are refused; unbounded answers remain visibly uninformative.
Try the dependency example. For 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.

19 · General relativity — metric → curvature

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):

CheckWhat it provesMeasured
Schwarzschild Ricci ≡ 0vacuum — one wrong sign or index anywhere and it failsmax |Rμν| < 1e−7 over random points
Kretschmann = 48M²/r⁶a NONZERO closed form: "everything is zero" cannot fake itrel. dev < 1e−6
de Sitter Rμν = 3H²gμνsolves the Λ-vacuum equation; the sign pins the conventiondev < 1e−7·3H²
Riemann identities + first Bianchiantisymmetries, pair symmetry, cyclicity — numeric, scaled to the summed terms< 1e−9
Geodesic conservationE, 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).

What you should see. A metric goes in and curvature comes out, with the mass left as a symbol.

Run the Schwarzschild preset. Nine non-zero Christoffel symbols appear with M still symbolic — not evaluated at some fiducial mass, but carried through as an unknown, which is what makes them checkable against a textbook. Spot-checked at M = 1, r = 5, θ = 1.1, every one matches its closed form to machine precision: Γttr = 0.0666666667 against M/(r(r−2M)); Γrtt = 0.024 against M(r−2M)/r³; Γrθθ = −3 against −(r−2M); Γθφφ = −0.4042482 against −sinθcosθ. This is the one place the connection itself is checked component by component rather than through a downstream contraction.

Read the verdicts in order of strength. All ten Ricci components vanish — a vacuum. Then the one that matters: Kretschmann = 48M²/r⁶ to better than 10⁻¹⁴. Ricci = 0 alone is a weak test, because a bug that zeroes everything passes it; a nonzero closed form cannot be faked that way, and the payload says exactly that. The four Riemann index identities hold to 10⁻¹⁴ — but those are structural self-consistency, not an external oracle, and the report distinguishes them.

Then fly the orbit. RK4 runs on the computed connection, not on a textbook formula, and energy, angular momentum and the 4-velocity norm drift by 10⁻¹⁴, 2×10⁻¹³ and 2×10⁻¹⁴ over four orbits — conservation laws holding on symbols the engine derived itself. The rosette advances 24.86° per orbit against a first-order post-Newtonian 6πM/ℓ of 22.5°. That is 10.5% apart, and it is not an error: at r ≈ 40–60M the first-order PN formula genuinely under-predicts. The payload labels it corroboration, not the verdict, which is the honest reading. Mercury, where the expansion is valid, gives 42.98″/century.

What honest failure looks like. Supply a custom diagonal metric and no closed-form oracle exists, so only the structural identities run — and the verdict says so rather than borrowing confidence from the presets. Kerr is out of reach entirely: its t–φ cross term needs a 2×2 block inverse the diagonal machinery does not have.

20 · Exact arithmetic (ℚ, ℚ(α)) — the gate

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:

EvaluatorColourAnswersCost
evaluate (float)amberalways, never refusessilently approximate: 0.1+0.2 → 0.30000000000000004
evaluate_interval (interval bounds, §18)greenconditional enclosure, or refusala range, not a number: [0.3, 0.3000000000000001]
evaluate_exact (ℚ) NEWpurpleexactly, or refusesonly 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.

Why the zero-test is the whole point

"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.

A measured failure, not a hypothetical one. Take 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.

What this unlocks

ℚ(α) — algebraic extensions NEW

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.

What "irreducible" is allowed to claim. ℚ[x]/(m) is a field only when m is irreducible, and proving irreducibility over ℚ in general is real work. The module states exactly what it checked: squarefree (exact) and no rational roots (exact) — which for degree ≤ 3 is a proof, and for degree ≥ 4 is not (m could split into two quadratics). Degree-4+ fields are therefore labelled ASSUMED, not proven. And if an inverse later fails because 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.

What you should see. Three lanes, and the interesting one is the lane that says nothing.

Run 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.

Then run the polynomial lab on f = (3x−1)⁴/81. Ask for gcd(f, f′). The exact lane returns 27x³ − 27x² + 9x − 1, degree 3. The float lane returns 1, degree 0 — "these polynomials share no factor". That is not a rounding error, it is a structurally wrong answer to a yes/no question, and it is why every decision procedure downstream of this (Risch, Rothstein–Trager) needs exact arithmetic rather than a tolerance. Yun's squarefree decomposition alongside it reports (3x−1) with multiplicity 4, and Res(f, f′) = 0 exactly.

What honest failure looks like. Ask for 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.

21 · Testing harnesses

22 · Limits & the Risch question

Is the integrator "Risch"? No.

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.

Do we still need it? — out of scope now, the natural successor later

Recommended sequencing. (1) exact arithmetic (rationals → algebraic numbers); (2) canonical polynomial / rational-function representation + a differential-field tower; (3) the transcendental Risch (Hermite + Rothstein–Trager + Risch DE) — the tractable ~80%. Skip full algebraic Risch (research-grade). High-value middle ground needing only step 1: Rothstein–Trager for the rational-function logarithmic part, which alone conquers 1/(x⁴+1) and the general P/Q log cases — today's biggest coverage gap.

Status: step 1 is now essentially done (§20). Exact rationals, exact polynomial GCD, squarefree decomposition, resultants, extended Euclid, and algebraic extensions ℚ(α) all exist and are probe-verified — that is the toolkit Rothstein–Trager consumes, including the field its constants live in. What remains before RT is a build rather than a research project: polynomials with coefficients in ℚ(α) (so 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.

Other known limits

23 · Where this sits — an honest comparison

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).

AreaState of the artThis engineVerdict
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.
What is actually distinctive. Not any single algorithm — every one above exists in a more capable form somewhere. Two habits are the unusual part.

First, the refusal discipline. Mainstream systems will hand you a wrong-branch antiderivative or a coefficient that is zero only to within a tolerance, with no indication that anything is uncertain. This engine is built so that the failure mode is a marker rather than a confident wrong answer: unsolved integrals come back as markers, residues decline when a pole sits on the contour, the exact evaluator returns nothing rather than a float, curl refuses in 4D rather than inventing a vector, and a variable named 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.

The stance has to be enforced, not assumed. Fact-checking the paragraph above found a place where it was simply false. The definite-integral path did not check for a singularity inside the interval, so it applied the Fundamental Theorem straight across a pole: ∫₋₁¹ 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.

Second, the testing method. Results are scored against oracles independent of the computation that produced them — closed forms, finite differences, conservation laws, a nonzero curvature invariant — never against the engine's own reasoning. And two techniques have repeatedly found what example-based tests missed: a property fuzz asserting that simplification never changes a value (§21), which found a non-terminating polynomial division on its first wide run, and diffing the whole regression output against a pristine worktree of 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.

~17
Ordered integration phases
159
Headless examples verified
0
Confidently-wrong answers
2-way
verification on every theorem

Symbolic Math Engine — Ruby + Sinatra + vanilla JS (+ ruby.wasm) — Back to Engine · History Part 3