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)