alkahest.crosscheck
Cross-CAS differential testing — ask a second CAS the same question.
Certificates cover the fragment where Alkahest can prove its answer. Outside
that fragment there is nothing checking the answer at all, and the failure that
matters for a search loop is not a crash but a silent error: a confident,
plausible, wrong result that the loop then builds a hundred derived claims on
top of (see tests/silent_errors/README.md). An independent implementation
is the cheapest instrument that catches exactly those.
This module runs one query through Alkahest and through an oracle and reports whether they agree — as a four-valued outcome, never a boolean:
agree
Both systems answered, and a named comparison rung settled it.
diverge
Both systems answered and the answers are not the same. The record names
two suspects, carries the witness point and both values, and never
words itself as "Alkahest is right".
incomparable
The question could not be posed identically to both systems (an
untranslatable node, an assumption context with no faithful mapping, an
operation one side refused). Never collapsed into agree.
unavailable
No oracle is installed. Also never agree — a loop that believes it is
cross-checking and is not is worse off than one that knows it isn't.
Quick start
>>> import alkahest as ak
>>> from alkahest.crosscheck import check, oracles
>>> "sympy" in oracles() # None when SymPy is not installed
True
>>> pool = ak.ExprPool()
>>> x = pool.symbol("x")
>>> out = check("integrate", ak.sin(x), x)
>>> out.outcome in {"agree", "unavailable"}
TrueDesign notes
One translator, not four. tests/ already carries four hand-rolled
_expr_to_sympy helpers, each covering a different subset. Translator
is the one implementation; the tests are meant to migrate onto it. Building the
QA harness and the runtime mode separately would produce two translators that
disagree, which is the worst possible outcome for a tool whose entire job is
detecting disagreement.
Total-or-refuse. Every tag Expr.node() can emit appears in
Translator._DISPATCH; an unknown tag, an unmapped primitive, or an
assumption context with no faithful counterpart raises
CrossCheckError (E-XCHECK-001), which check() turns
into incomparable. False divergences are worse than no signal — they
train both the loop and the team to ignore the alarm, and a best-effort
translator manufactures them by construction.
Opt-in per call site. There is deliberately no context(crosscheck=True):
that would put an oracle round-trip on every call, and stage-2 falsification
runs millions of times. Call check() where you want it — at stage-5
recording frequency, hundreds of times, not millions.
See docs/mdbook/src/crosscheck.md.
The outcome of one differential check.
operationstrOperation checked.
outcome{"agree", "diverge", "incomparable", "unavailable"}See the module docstring. Truthiness is deliberately not defined on this class:
if check(...)would read as "it agreed" and would be true for three of the four outcomes under any obvious convention.rungint or NoneWhich rung settled it (see
RUNG_NAMES);Nonewhen nothing did.reasonstrStable short code for why —
"invariant_holds","no_oracle","untranslatable","alkahest_refused","oracle_refused","ladder_exhausted", and so on. Machine triage should switch on this, not ondetail.detailstrOne human sentence.
oracle, oracle_versionstr or NoneWhich system answered, at which version.
alkahest_value, oracle_valuestr or NoneBoth answers, rendered.
witnessdict or None{"point", "alkahest", "oracle", "enclosure"}when a pointwise witness exists.divergenceDivergence or NonePopulated iff
outcome == "diverge".conclusiveboolWhether the settling rung proves what it reports. Rung 3 agreement is sampled, so it is reported as agreement with
conclusive=Falserather than being silently promoted or silently discarded.elapsed_msfloatWall-clock cost of the check, oracle round trip included.
Two systems answered, and the answers are not the same.
The wording is constrained on purpose. A divergence names two suspects:
it is evidence that one of the two is wrong, and on its own it is never
evidence about which. support carries whatever the rigorous
escalation could establish, and its default is "unresolved".
operationstrOperation that diverged.
oracle, oracle_versionstrWhich independent system, at which version. Without the version a recorded divergence rots the first time the oracle changes an answer.
pointdictWitness: symbol name -> value at which the two disagree. Empty when the divergence is symbolic rather than pointwise.
alkahest_value, oracle_valuestrBoth values, rendered. Never one without the other.
alkahest_enclosuretuple of float, optionalRigorous ball-arithmetic enclosure computed on the Alkahest side.
support{"unresolved", "alkahest_supported", "oracle_supported"}What the rigorous escalation established.
"oracle_supported"means an operation invariant was rigorously refuted on the Alkahest side — a silent-error finding, and the case that should be routed intotests/silent_errors/corpus/.regiondict or NoneWhen
verified_sign()certified that the failing invariant residual keeps one sign across the whole sampling box, the box:{"x": [0.35, 2.65]}. That upgrades a finding from "wrong at this point" to "wrong on this interval", which is a much shorter argument to hand a reviewer.detailstrOne sentence, phrased symmetrically.
One previously-observed comparison, pinned to an oracle version range.
The per-PR half of the two-tier arrangement. Every case records the oracle version range its expectation was established against: without that the corpus rots silently the first time the oracle changes an answer, which it will, and a red gate would then be indistinguishable from a real regression.
idstrStable, never reused.
operationstrAn
OPERATIONSkey.buildcallablepool -> tupleof arguments forcheck(). Takes the pool so each run gets a fresh one.expectedstrExpected
CrossCheck.outcome.expected_reasonstr or NoneExpected
CrossCheck.reason, when the case is pinning why.oraclestrOracle name the expectation belongs to.
oracle_versionsstrComma-separated PEP-440-ish range, e.g.
">=1.12,<2". A case whose range excludes the installed version is skipped, not failed.found_bystr"seeded sweep, seed=..."or a hand-written provenance note. The ratchet: a divergence the nightly finds must land here.notestrWhat this case is protecting.
How one operation is posed to both systems, and how it is compared.
namestrMatches the
alkahestmodule attribute of the same name.rungstuple of intLadder rungs to attempt, in order. Rung 4 leads wherever it exists, because it sidesteps equal-up-to-a-constant, up-to-ordering and up-to-a-unit entirely — the three things that make naive comparison of antiderivatives, solution sets and factorisations produce nothing but noise.
invariantstr or NoneName of the rung-4 invariant, or
Nonewhen the operation has none.result{"expr", "solutions"}Shape of the answer, which decides how it is compared and rendered.
normalizestr or NoneOperation-specific normalisation applied to the Alkahest answer before the ladder runs — currently only
"strip_big_o". This is not a rung: it removes a difference in how the two systems spell the same answer, and it must never remove a difference in the answer itself.
An independent CAS the comparator can interrogate.
The comparator talks to oracles only through this interface, so a second implementation (Wolfram, Maxima, a pinned older Alkahest) drops in without the ladder changing. Two consequences worth stating:
- every method may answer "I don't know" —
is_zero()returnsNone,run()raises — and the comparator turns that intoincomparablerather than guessing; lift()is optional in effect but not in signature: it is what lets rung 2 be attempted independently in both systems, which is what makes "either one proved it" a defensible notion of agreement.
namestrStable short identifier, used as the key in
oracles().
Alkahest Expr -> a term in this oracle's language.
Perform operation in this oracle's own engine.
- CrossCheckError
E-XCHECK-004when the oracle declines or returns an unevaluated form; a refusal is not a divergence.
Derivative of obj with respect to the translated variable var.
Substitute translated terms for translated symbols.
Substitute numbers for free symbols matched by name.
Distinct from subs() because the comparator only ever knows the
Alkahest symbol names at a witness point, and rebuilding an oracle
symbol from a name would have to re-derive its assumption flags — get
that wrong and the substitution silently misses.
Result of a seeded differential sweep.
seedintPrint this. A sweep is only useful as a bug report if the run that found something can be reproduced exactly.
oracle, oracle_versionstr or NoneWhich system, at which version. A sweep that turns red after a SymPy upgrade is a different event from one that turns red after an Alkahest change, and only the recorded version tells them apart.
checkstuple of CrossCheckEvery check run, in order.
SymPy as the reference implementation.
The first oracle, and the one the frozen corpus is pinned against. Its version is recorded on every record: without that the corpus rots silently the first time SymPy changes an answer, which it will.
- CrossCheckError
E-XCHECK-002on construction when SymPy is not importable. The absence is loud by construction — there is no code path in which a missing SymPy yields agreement.
SymPy term -> Alkahest Expr, refusing anything not exactly representable.
Deliberately narrow. A lift that "mostly works" would put an approximated value into the Alkahest-side rung-2 attempt, and a wrong answer there is indistinguishable from the divergence the module exists to find.
Translator into SymPy's expression language.
Walk an Expr into another CAS's term language.
Subclass once per oracle. The walk, the dispatch table, and the refusal discipline live here; a subclass supplies only the leaf and node constructors of its target language.
The dispatch table is total over NODE_TAGS — that is the
property that makes a divergence informative, and
tests/test_crosscheck.py asserts it against the tags the Rust binding
can actually emit rather than against this list, so a new node kind fails
the build instead of quietly translating to nothing.
translate() accepts the governing Assumptions and
maps its predicates onto per-symbol flags. A predicate with no faithful
counterpart (a relation between two symbols, a disjunction, a bound on a
composite expression) raises rather than being dropped: dropping it asks
the oracle a weaker question and any divergence that follows is an
artefact of the harness.
Translate expr, honouring assumptions, or refuse.
exprExpr or DerivedResultThe expression to translate.
assumptionsAssumptions, optionalGoverning assumption context. When
None, the ambientactive_assumptions()context is used, so a check run insidewith alkahest.context(assumptions=...)asks the oracle the same conditioned question the caller asked Alkahest.
- object
A term in the oracle's language.
- CrossCheckError
E-XCHECK-001for an unknown node tag, an unmapped primitive, or an assumption with no faithful counterpart.
FROZEN_CORPUS = (FrozenCase(id='integrate_polynomial', operation='integrate', build=_case_polynomial_antiderivative, expected='agree', expected_reason='invariant_holds', fou…No docstring.
FUNCTION_MAP = {'abs': 'Abs', 'acos': 'acos', 'acosh': 'acosh', 'arg': 'arg', 'asin': 'asin', 'asinh': 'asinh', 'atan': 'atan', 'atan2': 'atan2', 'atanh': 'atanh', 'ceil': …No docstring.
NODE_TAGS = frozenset({'symbol', 'integer', 'rational', 'float', 'add', 'mul', 'pow', 'func', 'piecewise', 'predicate', 'forall', 'exists', 'big_o', 'root_sum'})No docstring.
OPERATIONS = {'diff': Operation('diff', (1, 2, 3), None), 'integrate': Operation('integrate', (4, 1, 2, 3), 'antiderivative'), 'simplify': Operation('simplify', (4, 1, 2,…No docstring.
OUTCOMES = ('agree', 'diverge', 'incomparable', 'unavailable')No docstring.
PREDICATE_KINDS = frozenset({'lt', 'le', 'gt', 'ge', 'eq', 'ne', 'and', 'or', 'not', 'true', 'false'})No docstring.
REFUSED_FUNCTIONS = {'zeta_prime': 'SymPy has no `zeta_prime` function: it spells the derivative `Derivative(zeta(s), s)`, an unevaluated form a name-for-name table cannot expre…No docstring.
RUNG_NAMES = {1: 'syntactic', 2: 'symbolic', 3: 'rigorous_numeric', 4: 'invariant'}No docstring.
SAMPLE_BOX = (0.35, 2.65)No docstring.
SWEEP_OPERATIONS = ('diff', 'integrate', 'simplify')No docstring.