Spaces:
Running
Running
| """Shared machinery for the symbolic proof certificates (Route A). | |
| The proofs in Appendix B.3 never touch individual atoms: every step is a statement | |
| about the *aggregates* | |
| A_i := integral over S_{1,eps} of e^{r_i} dp_t | |
| B_i := integral over S_{2,eps} of e^{r_i} dp_t | |
| M_i := integral over X \\ S_eps of e^{r_i} dp_t (so Z_i = A_i+B_i+M_i) | |
| together with the masses (a_t, b_t, m_t). Reformulating a proof over these | |
| finitely many symbols is therefore *exact*, not a discretisation, and it lets | |
| sympy verify each step for ALL admissible parameter values at once -- which is | |
| what a universally quantified lemma needs and a grid search can never give. | |
| Convention throughout: r_1* = r_2* = 0 (WLOG, since the update is invariant under | |
| adding a constant to r_i -- Z_i absorbs it; that invariance is itself verified in | |
| `symbolic_claim1.check_shift_invariance`). | |
| """ | |
| from __future__ import annotations | |
| import sympy as sp | |
| def prove_nonneg(expr: sp.Expr, name: str) -> tuple[bool, str]: | |
| """Verify expr >= 0 for every value consistent with its symbols' assumptions.""" | |
| simplified = sp.simplify(expr) | |
| verdict = simplified.is_nonnegative | |
| if verdict is True: | |
| return True, f"{name}: sympy proves {simplified} >= 0 from symbol assumptions" | |
| # Fall back to a factorisation whose factors are individually signed. | |
| factored = sp.factor(simplified) | |
| if factored.is_nonnegative is True: | |
| return True, f"{name}: factored as {factored} >= 0" | |
| return False, f"{name}: could NOT establish {simplified} >= 0 (is_nonnegative={verdict})" | |
| def prove_identity(lhs: sp.Expr, rhs: sp.Expr, name: str) -> tuple[bool, str]: | |
| """Verify lhs == rhs as an algebraic identity.""" | |
| diff = sp.simplify(sp.together(sp.expand(lhs - rhs))) | |
| ok = sp.simplify(diff) == 0 | |
| return ok, f"{name}: simplify(lhs - rhs) = {diff}" + ("" if ok else " <-- NOT zero") | |
| def prove_positive(expr: sp.Expr, name: str) -> tuple[bool, str]: | |
| simplified = sp.simplify(expr) | |
| if simplified.is_positive is True: | |
| return True, f"{name}: sympy proves {simplified} > 0" | |
| factored = sp.factor(simplified) | |
| if factored.is_positive is True: | |
| return True, f"{name}: factored as {factored} > 0" | |
| return False, f"{name}: could NOT establish {simplified} > 0" | |
| def implies_under_constraints( | |
| hypothesis: list[sp.Expr], conclusion: sp.Expr, syms: list[sp.Symbol], name: str | |
| ) -> tuple[bool, str]: | |
| """Check that `conclusion >= 0` follows from `h >= 0 for h in hypothesis`. | |
| Strategy: substitute each hypothesis by a fresh non-negative slack symbol and | |
| ask sympy to establish the conclusion's sign. Used where a bare `simplify` | |
| cannot see the sign because it depends on the hypotheses. | |
| """ | |
| subs = {} | |
| slacks = [] | |
| for k, h in enumerate(hypothesis): | |
| s = sp.Symbol(f"slack_{k}", nonnegative=True) | |
| slacks.append((h, s)) | |
| subs[h] = s | |
| expr = conclusion | |
| for h, s in slacks: | |
| expr = expr.subs(h, s) | |
| return prove_nonneg(expr, name) | |