core/evals/math_symbolic_equivalence/v1
Shay 169cec710e
feat(ADR-0131.1.B): harden symbolic equivalence lane with generated corpus + exact algebra (#169)
* feat(evals): add deterministic symbolic equivalence generated corpus

* feat(evals): add symbolic equivalence replay helpers

* feat(evals): load generated symbolic equivalence corpus

* feat(evals): emit symbolic equivalence replay manifest

* feat(symbolic): support multivariable integer polynomials

* feat(symbolic): support exact rational polynomial coefficients

* feat(symbolic): align equivalence API with multivariable normalization

* test(ADR-0131.1.B): reconcile v1 expectations to v1.B scope expansion

The v1.B refactor (univariate int → sparse multivariable Fraction) deliberately
admits multivariable polynomials and constant-denominator division. The v1
dataset and tests pinned the old refusal behavior, so the lane runner reported
wrong=4 and 10 unit tests failed.

Reconcile:

- cases.jsonl: flip sym-eq-v1-0029 ('x+y' vs 'x+1') and sym-eq-v1-0030
  ('x/2' vs 'x') from expected=refused to expected=not_equivalent; rename
  categories to multivariable_distinct / constant_denominator_distinct;
  extend provenance with adr-0131.1b:scope-expanded.
- generated_cases.py: split _refusal_cases into scope_expanded (admits)
  and templates (still refused); the first two adversarial cases move to
  the scope-expanded list with expected=not_equivalent.
- test_math_symbolic_normalizer.py: replace test_undefined_variable and
  test_unknown_operator_division with positive scope-expansion tests +
  symbolic-denominator refusal; rewrite TestPolynomialInvariants for the
  new terms/variables constructor (Polynomial(terms={...}, variables=(...)))
  with float-rejection and zero-coef-collapse invariants.
- test_math_symbolic_equivalence.py: TestRefused.test_empty_left reason
  string matches new normalizer error; flip multivariable + constant-
  denominator cases to NOT_EQUIVALENT; add symbolic-denominator-refused
  case; relax canonical_a assertion in test_a_normalizes_b_refuses (engine
  now zeroes both on either-side refusal).
- report.json + manifest.json: regenerated; lane PASS 185/185 wrong=0.

Lane invariants reaffirmed by the new tests: wrong==0, refusal-first for
truly out-of-scope inputs (symbolic denominator, transcendental, malformed,
negative exponent), determinism via byte-equal report.
2026-05-23 10:47:57 -07:00
..
__init__.py feat(ADR-0131.1): symbolic equivalence benchmark v1 + lane PASSED (#167) 2026-05-23 09:58:26 -07:00
cases.jsonl feat(ADR-0131.1.B): harden symbolic equivalence lane with generated corpus + exact algebra (#169) 2026-05-23 10:47:57 -07:00
generated_cases.py feat(ADR-0131.1.B): harden symbolic equivalence lane with generated corpus + exact algebra (#169) 2026-05-23 10:47:57 -07:00
manifest.json feat(ADR-0131.1.B): harden symbolic equivalence lane with generated corpus + exact algebra (#169) 2026-05-23 10:47:57 -07:00
README.md feat(ADR-0131.1): symbolic equivalence benchmark v1 + lane PASSED (#167) 2026-05-23 09:58:26 -07:00
replay.py feat(ADR-0131.1.B): harden symbolic equivalence lane with generated corpus + exact algebra (#169) 2026-05-23 10:47:57 -07:00
report.json feat(ADR-0131.1.B): harden symbolic equivalence lane with generated corpus + exact algebra (#169) 2026-05-23 10:47:57 -07:00
runner.py feat(ADR-0131.1.B): harden symbolic equivalence lane with generated corpus + exact algebra (#169) 2026-05-23 10:47:57 -07:00
sealed_holdout.age feat(ADR-0131.1.S): sealed holdout for symbolic equivalence v1 (#173) 2026-05-23 10:44:23 -07:00
sealed_holdout.pubkey feat(ADR-0131.1.S): sealed holdout for symbolic equivalence v1 (#173) 2026-05-23 10:44:23 -07:00
sealed_report.json feat(ADR-0131.1.S): sealed holdout for symbolic equivalence v1 (#173) 2026-05-23 10:44:23 -07:00
sealed_runner.py feat(ADR-0131.1.S): sealed holdout for symbolic equivalence v1 (#173) 2026-05-23 10:44:23 -07:00

Symbolic Equivalence Benchmark v1 (ADR-0131.1)

The primary discriminator for the mathematics_logic expert promotion under ADR-0131. Tests whether the engine can determine that two algebraic expressions are equivalent under deterministic polynomial normalization.

Scope (v1, intentionally narrow)

  • Single variable (x by default).
  • Integer coefficients only.
  • Operators: +, -, *, **/^ (positive integer exponents).
  • Parentheses for grouping.
  • No division (other than trivial).
  • No transcendental functions, no multi-variable, no rationals.

The narrowness is by design. The architecture's strength is exact recall + replay determinism; the benchmark stays inside that envelope so the result is a clean measure of that strength, not a proxy for it.

Pipeline

expression_a -> normalize -> canonical_string_a
expression_b -> normalize -> canonical_string_b
verdict      = (canonical_string_a == canonical_string_b)
                 ? EQUIVALENT : NOT_EQUIVALENT
or REFUSED   if either expression is out-of-scope

normalize is generate/math_symbolic_normalizer.py: recursive-descent parser → polynomial expand-and-collect → canonical string serialization. check_equivalence is generate/math_symbolic_equivalence.py.

Dataset

cases.jsonl ships 30 hand-curated cases covering:

Category Count Examples
commutative_add / commutative_mul 2 x+1 ≡ 1+x, 3*x ≡ x*3
distributive 2 2*(x+3) ≡ 2*x+6
square_of_binomial 3 (x+1)^2 ≡ x^2+2*x+1
difference_of_squares 2 (x+1)*(x-1) ≡ x^2-1
cube_of_binomial 2 (x+1)^3 ≡ x^3+3*x^2+3*x+1
foil 1 (x+2)*(x+3) ≡ x^2+5*x+6
collect_like_terms 2 2*x+3*x ≡ 5*x
zero_cancellation 1 x-x ≡ 0
repeated_addition 1 x+x+x+x ≡ 4*x
exponent_combine 1 x^2*x ≡ x^3
product_of_factors 1 x*(x+1)*(x-1) ≡ x^3-x
unary_neg_distribute 1 -(x+1) ≡ -x-1
distributive_collect 1 3*(x+1)+2*(x-1) ≡ 5*x+1
different_constant / coefficient / degree 3 x+1 ≢ x+2
sign_flipped 2 (x+1)^2 ≢ (x-1)^2
distributive_miss / foil_miss / cube_miss 3 2*(x+3) ≢ 2*x+3
out_of_scope_variable 1 x+y → REFUSED
out_of_scope_division 1 x/2 → REFUSED

20 expected-equivalent + 8 expected-not-equivalent + 2 expected-refused.

Exit criterion (per ADR-0131 Benchmark 1)

correct_rate >= 0.95
wrong         == 0

wrong is incremented only when the engine produces a definite answer that disagrees with the expected verdict. Refusal on an out-of-scope case is correct when expected; refused when unexpected (which the lane test flags as a normalizer-coverage regression).

Running the lane

python -m evals.math_symbolic_equivalence.v1.runner
# exits 0 if exit criterion passes, 1 otherwise
# writes report.json with counts + per-case verdicts

v1 result (baseline at landing)

correct = 30 / 30   (100.0%)
wrong   =  0 / 30   (wrong == 0 invariant satisfied)
refused =  0 / 30   (both expected-refused cases were caught correctly)
exit:   PASSED

This is the first benchmark on the mathematics_logic lane where the architecture's structural strengths fully express. The result is not a claim about how hard the cases are; it's a claim about the architecture-benchmark fit being correct.

Future expansion (ADR-0131.1.B and beyond)

  • Multi-variable polynomials (x, y, z simultaneous).
  • Rational coefficients (Fraction).
  • Larger dataset (~500 cases per ADR-0131's Benchmark 1 spec).
  • Sealed holdout (mirror ADR-0119.7's pyrage X25519 pattern).
  • More algebraic identities (Pascal triangle expansions, factoring, partial fractions for rationals).

v1 ships the minimum viable substrate. The exit criterion is met; the dataset can grow without breaking the contract.