Spaces:
Running
Running
File size: 3,039 Bytes
e2d54c9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | """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)
|