#!/usr/bin/env python3
r"""reduced_statement.py -- the stand-alone exact check of the REDUCED STATEMENT of the four-loop equal-mass banana at eps^0:
the served theta-system's Picard-Fuchs operator L4 (AESZ #34) applied to the served I^(0) series gives 5! exactly,
and the MUM boundary of the top master is the log-polynomial (0, 80 zeta_3, 0, 0, -5).

STAMP (date -u at emission): 2026-09-10T13:57:59Z
extends: banana-4loop-evaluate.py (the served row-34 evaluator beside this file: its embedded DATA block -- the exact
         theta-system theta M-hat = C(z, eps) M-hat with den = 20(1+z)(1+9z)(1+25z), the derived boundary vector v in
         the zeta-ring, the rescale a = (0,-1,1,1,2) -- is read from the served file by regex, sha256-pinned to the
         served DATA_SHA256 and to the file's own sha; nothing is imported or executed from it).  This file is the
         check of the statement, not its derivation.
reference: an independent reference derivation of 2026-09-10, on file with the authors
         (L_z[s I^(0)] = 120 with the boundary (-s) I^(0) = -5 L^4 + 80 zeta(3) L + O(1/s), L = log(-1/s)).

CONVENTIONS (the served file's): z = -1/p^2, L = log z, M-hat_1 = z^{-1} e^{4 gamma_E eps} m_1 with m_1 = I_11111;
at eps^0 the pole layers of m_1 vanish, so M-hat_1|_{eps^0} = z^{-1} I^(0)(p^2) = (-p^2) I^(0) = (-s) I^(0) with
s = p^2, and the served L = log z = log(-1/s) is the reference derivation's L.  The served
    L4 = th^4 + z (35 th^4 + 70 th^3 + 63 th^2 + 28 th + 5) + z^2 (259 th^4 + 1036 th^3 + 1580 th^2 + 1088 th + 285)
         + 225 z^3 (th+1)^2 (th+2)^2,  th = z d/dz,
is the reference derivation's L_z under its variable 1/s = -z (the odd powers of z change sign).  Therefore
    L4 [ M-hat_1|_{eps^0} ] = L4 [ (-s) I^(0) ] = -120   <=>   L4 [ s I^(0) ] = +120 = 5!     (the reduced statement)
and the n = 0 log-tower of M-hat_1|_{eps^0} is  -5 L^4 + 0 L^3 + 0 L^2 + 80 zeta_3 L + 0, i.e. the coefficient vector
(L^0, L^1, L^2, L^3, L^4) = (0, 80 zeta_3, 0, 0, -5)  [the stated vector, for (-s) I^(0) in powers of log(-1/s)].

WHAT --reduced-statement DOES
  1. reads the served DATA block (pin-checked), rebuilds C_0 = Num(0)/20 (integer matrix on the 25 components
     (master, eps-order)), checks den = 20(1+z)(1+9z)(1+25z) and C_0 nilpotent of index 8 in exact integers (the
     served structural checks, redone here);
  2. the constants vector v in the zeta-ring Q[zeta_2, zeta_3, zeta_4] (exact: polynomials in three formal symbols
     with Fraction coefficients) and the served log-Frobenius recursion
        F_0(L) = e^{C_0 L} v,   (n den(0) - Num(0)) F_{n,j} = [sum_{m=1..6} (Num_m - den_m ((n-m) + d/dL)) F_{n-m}]_j
                                                              - den(0) (j+1) F_{n,j+1}
     run EXACTLY in the zeta-ring to --nterms (default 12);
  3. the BOUNDARY: the (m_1, eps^0) component of F_0(L) as a polynomial in L must be EXACTLY
     -5 L^4 + 80 zeta_3 L (every other coefficient 0 in the ring) -- printed as the vector and PASS/FAIL;
  4. the OPERATOR: L4 applied to the double series sum_n z^n sum_j F_{n,j}[m_1, eps^0] L^j (theta z^n L^j = n z^n L^j
     + j z^n L^{j-1}) must be EXACTLY -120 (the z^0 L^0 coefficient) with every other (n, j) coefficient 0 in the
     ring through n <= nterms - 3 (the operator's z-degree 3 consumes three terms) -- printed and PASS/FAIL, with
     the equivalent statement L4[s I^(0)] = +120 = 5!;
  5. a NUMERIC cross-check at --dps (>= 50): the (m_1, eps^0) series summed at z_0 = 1/128 with zeta values,
     L4 evaluated by the same exact algebra (already exact), and the reference closed form
     -120 varpi_4 + 80 zeta(3) varpi_1 (the AESZ-34 Frobenius generating function's rho-derivatives, rebuilt here from
     the recurrence) compared to the served series at z_0: >= dps - 5 digits or FAIL by name;
  6. --planted: the boundary constant 80 -> 81 (m_2's eps^{-1} entry, which feeds m_1 through the log chains, is
     shifted) -> checks 3 and 4 must FAIL by name (the control).
EXIT: 0 every check PASS; 1 a named FAIL; 2 usage; 3 the served DATA block does not match its pin.
Self-contained: python3 + mpmath + the served banana-4loop-evaluate.py by path (default: the copy beside this file in the
download directory; else BANANA4_EVALUATE, BOOTSTRAP_ROOT or the tree above; --served PATH).
"""
import argparse
import hashlib
import json
import math
import os
import re
import subprocess
import sys
from fractions import Fraction as Fr

import mpmath as mp

STAMP = "2026-09-10T13:57:59Z"
EXIT_FAIL, EXIT_USAGE, EXIT_PIN = 1, 2, 3
def _served_default():
    """the served evaluator: banana-4loop-evaluate.py BESIDE this file first (the two ship in one download directory), else
    BANANA4_EVALUATE env, else blog/files/banana/banana-4loop-evaluate.py under BOOTSTRAP_ROOT or the tree above this file;
    --served PATH names it explicitly."""
    here = os.path.dirname(os.path.abspath(__file__))
    beside = os.path.join(here, "banana-4loop-evaluate.py")
    if os.path.exists(beside):
        return beside
    if os.environ.get("BANANA4_EVALUATE"):
        return os.environ["BANANA4_EVALUATE"]
    cands = [beside]
    roots = [os.environ.get("BOOTSTRAP_ROOT", "")]
    h = here
    for _ in range(10):
        roots.append(h); h = os.path.dirname(h)
    cands += [os.path.join(r, "blog", "files", "banana", "banana-4loop-evaluate.py") for r in roots if r]
    for c in cands:
        if os.path.exists(c):
            return c
    return cands[0]


SERVED_DEFAULT = _served_default()
SERVED_FILE_SHA256 = "961496c2e17af613f0dca7f9bed57ecbba4b06837ac6dc2ce5b6f312a2f2a01f"      # the served file's sha256 at emission (pinned; refused on mismatch)
JMAX = 7

# ------------------------------------------------------------------ the zeta-ring: {(e2, e3, e4): Fraction}
def ring(c=0):
    return {(0, 0, 0): Fr(c)} if c else {}


def radd(a, b):
    out = dict(a)
    for k, v in b.items():
        out[k] = out.get(k, Fr(0)) + v
    return {k: v for k, v in out.items() if v != 0}


def rscale(a, c):
    c = Fr(c)
    return {k: v * c for k, v in a.items() if v * c != 0}


def rmul(a, b):
    out = {}
    for k1, v1 in a.items():
        for k2, v2 in b.items():
            k = (k1[0] + k2[0], k1[1] + k2[1], k1[2] + k2[2])
            out[k] = out.get(k, Fr(0)) + v1 * v2
    return {k: v for k, v in out.items() if v != 0}


def rstr(a):
    if not a:
        return "0"
    names = {(0, 0, 0): "1", (1, 0, 0): "z2", (0, 1, 0): "z3", (0, 0, 1): "z4"}
    parts = []
    for k in sorted(a):
        nm = names.get(k, "z2^%d z3^%d z4^%d" % k)
        parts.append(f"({a[k]})*{nm}" if nm != "1" else f"({a[k]})")
    return " + ".join(parts)


def rval(a, dps):
    with mp.workdps(dps):
        tot = mp.mpf(0)
        for (e2, e3, e4), v in a.items():
            tot += mp.mpf(v.numerator) / v.denominator * mp.zeta(2) ** e2 * mp.zeta(3) ** e3 * mp.zeta(4) ** e4
        return +tot


# ------------------------------------------------------------------ the served data
def read_served(path):
    src = open(path, "rb").read()
    fsha = hashlib.sha256(src).hexdigest()
    txt = src.decode("utf-8")
    m = re.search(r'DATA_TEXT = r"""(.*?)"""', txt, re.S)
    pin = re.search(r'DATA_SHA256 = "([0-9a-f]{64})"', txt).group(1)
    data_txt = m.group(1)
    got = hashlib.sha256(data_txt.encode("ascii")).hexdigest()
    return fsha, pin, got, json.loads(data_txt)


def structural(D):
    zf = D["z_frame"]
    den = [int(c) for c in zf["den"]]
    assert den == [20, 700, 5180, 4500], den
    N0 = {}
    for k, cl in zf["num"].items():
        n, mm = map(int, k.split(","))
        c0 = int(cl[0])
        if c0:
            N0[(n, mm)] = c0

    def smul(A, B):
        out = {}
        for (i, j), a in A.items():
            for (jj, k), b in B.items():
                if jj == j:
                    out[(i, k)] = out.get((i, k), 0) + a * b
        return {k: v for k, v in out.items() if v}
    P = dict(N0)
    idx = None
    for p in range(2, 10):
        P = smul(P, N0)
        if not P:
            idx = p
            break
    return den, idx


def build_recursion(D, nterms, plant=None):
    """F[n][j] = list over the 25 components of ring elements, exactly."""
    STATE = [(i, K) for i in range(5) for K in range(-4, 1)]
    IDX = {s: n for n, s in enumerate(STATE)}
    NS = len(STATE)
    zf = D["z_frame"]
    den = [Fr(c) for c in zf["den"]] + [Fr(0)] * (7 - len(zf["den"]))
    numk = [[] for _ in range(7)]
    for key, cl in zf["num"].items():
        n, mm = map(int, key.split(","))
        for k in range(min(7, len(cl))):
            c = Fr(cl[k])
            if c:
                numk[k].append((n, mm, c))
    N0 = numk[0]
    d0 = den[0]
    # constants vector v in the ring
    v = []
    for (i, K) in STATE:
        c = D["constants_named"].get(f"{i},{K}") or {}
        el = {}
        for nm, fr in c.items():
            key = {"1": (0, 0, 0), "z2": (1, 0, 0), "z3": (0, 1, 0), "z4": (0, 0, 1)}[nm]
            el[key] = Fr(fr)
        v.append(el)
    if plant:
        (i, K, nm, delta) = plant
        key = {"1": (0, 0, 0), "z2": (1, 0, 0), "z3": (0, 1, 0), "z4": (0, 0, 1)}[nm]
        v[IDX[(i, K)]] = radd(v[IDX[(i, K)]], {key: Fr(delta)})

    def matvec(M, x):
        out = [{} for _ in range(NS)]
        for (rr, cx, mv) in M:
            out[rr] = radd(out[rr], rscale(x[cx], mv))
        return out

    def solve_A(n, B):
        nd0 = n * d0
        u = [rscale(b, 1 / nd0) for b in B]
        x = list(u)
        for _ in range(7):
            u = [rscale(w, 1 / nd0) for w in matvec(N0, u)]
            x = [radd(x[k], u[k]) for k in range(NS)]
        return x
    F = []
    F0 = []
    X = list(v)
    fact = 1
    for j in range(JMAX + 1):
        F0.append([rscale(x, Fr(1, fact)) for x in X])
        X = [rscale(w, 1 / d0) for w in matvec(N0, X)]
        fact *= (j + 1)
    F.append(F0)
    for n in range(1, nterms + 1):
        RHS = [[{} for _ in range(NS)] for _ in range(JMAX + 1)]
        for m_ in range(1, min(n, 6) + 1):
            Fnm = F[n - m_]
            Nm = numk[m_]
            dm = den[m_]
            for j in range(JMAX + 1):
                T = matvec(Nm, Fnm[j])
                if dm != 0:
                    T = [radd(T[k], rscale(Fnm[j][k], -dm * (n - m_))) for k in range(NS)]
                    if j + 1 <= JMAX:
                        T = [radd(T[k], rscale(Fnm[j + 1][k], -dm * (j + 1))) for k in range(NS)]
                RHS[j] = [radd(RHS[j][k], T[k]) for k in range(NS)]
        Fn = [None] * (JMAX + 1)
        for j in range(JMAX, -1, -1):
            B = RHS[j]
            if j + 1 <= JMAX:
                B = [radd(B[k], rscale(Fn[j + 1][k], -d0 * (j + 1))) for k in range(NS)]
            Fn[j] = solve_A(n, B)
        F.append(Fn)
    return F, STATE, IDX


L4_Q = [[0, 0, 0, 0, 1], [5, 28, 63, 70, 35], [285, 1088, 1580, 1036, 259], [900, 2700, 2925, 1350, 225]]   # Q_k(th), z^k


def apply_L4(series, nmax):
    """series: {(n, j): ring}; theta (z^n L^j) = n z^n L^j + j z^n L^{j-1}. Returns {(n, j): ring} of L4[series]
    for n <= nmax."""
    def theta(S):
        out = {}
        for (n, j), c in S.items():
            if n:
                out[(n, j)] = radd(out.get((n, j), {}), rscale(c, n))
            if j:
                out[(n, j - 1)] = radd(out.get((n, j - 1), {}), rscale(c, j))
        return {k: v for k, v in out.items() if v}
    powers = [dict(series)]
    for _ in range(4):
        powers.append(theta(powers[-1]))
    out = {}
    for k, Q in enumerate(L4_Q):
        for p, q in enumerate(Q):
            if q == 0:
                continue
            for (n, j), c in powers[p].items():
                if n + k <= nmax:
                    out[(n + k, j)] = radd(out.get((n + k, j), {}), rscale(c, q))
    return {k: v for k, v in out.items() if v}


# ------------------------------------------------------------------ the reference closed form (AESZ-34 Frobenius rho-series)
def aesz34_frobenius(nterms, K=5):
    """a_n(rho) mod rho^K for the served L4 (z chart): the recurrence sum_k Q_k(n - k + rho) a_{n-k} = 0 with
    Q_0(x) = x^4; varpi(z, rho) = z^rho sum_n a_n(rho) z^n.  Returns a[n] = [rho^0..rho^{K-1}] Fractions."""
    def q_at(Q, x0):
        out = [Fr(0)] * K
        for j, c in enumerate(Q):
            if c == 0:
                continue
            for i in range(min(j, K - 1) + 1):
                out[i] += c * math.comb(j, i) * x0 ** (j - i)
        return out

    def smul(p, q):
        out = [Fr(0)] * K
        for i, x in enumerate(p):
            if x == 0:
                continue
            for j, y in enumerate(q):
                if i + j >= K:
                    break
                out[i + j] += x * y
        return out

    def sdiv(num, den):
        out = [Fr(0)] * K
        inv0 = 1 / den[0]
        for n in range(K):
            acc = num[n]
            for k in range(1, n + 1):
                acc -= den[k] * out[n - k]
            out[n] = acc * inv0
        return out
    a = [[Fr(1)] + [Fr(0)] * (K - 1)]
    for n in range(1, nterms + 1):
        acc = [Fr(0)] * K
        for k in range(1, 4):
            if n - k < 0:
                break
            t = smul(q_at(L4_Q[k], Fr(n - k)), a[n - k])
            acc = [x + y for x, y in zip(acc, t)]
        den = q_at(L4_Q[0], Fr(n))     # (n + rho)^4
        a.append([-x for x in sdiv(acc, den)])
    return a


def varpi_j_at(a, j, z0, dps):
    """varpi_j = (1/j!) d^j/drho^j [z^rho sum a_n(rho) z^n] at rho = 0 = sum_{i<=j} L^i/i! * sum_n a_n^{(j-i)} z^n."""
    with mp.workdps(dps):
        Lz = mp.log(mp.mpf(z0))
        tot = mp.mpf(0)
        for i in range(j + 1):
            S = mp.mpf(0)
            for n, an in enumerate(a):
                c = an[j - i]
                if c:
                    S += mp.mpf(c.numerator) / c.denominator * mp.mpf(z0) ** n
            tot += Lz ** i / mp.factorial(i) * S
        return +tot


def main(argv=None):
    ap = argparse.ArgumentParser(description="the reduced statement D I^(0) = 5! with boundary (0, 80 zeta_3, 0, 0, -5): the stand-alone exact check")
    ap.add_argument("--reduced-statement", action="store_true")
    ap.add_argument("--served", default=SERVED_DEFAULT)
    ap.add_argument("--nterms", type=int, default=12)
    ap.add_argument("--dps", type=int, default=60)
    ap.add_argument("--planted", action="store_true", help="the control: shift the boundary constant that feeds the 80 zeta_3 (must FAIL by name)")
    ap.add_argument("--json", default=None)
    a = ap.parse_args(argv)
    if not a.reduced_statement:
        ap.error("--reduced-statement")
    if a.dps < 50:
        ap.error("--dps >= 50")
    print(f"reduced_statement.py STAMP {STAMP}; served evaluator {a.served}")
    fsha, pin, got, D = read_served(a.served)
    print(f"served file sha256 {fsha[:16]}...; embedded DATA block sha256 {got[:16]}... vs the served pin {pin[:16]}...: {'MATCH' if got == pin else 'MISMATCH'}")
    if got != pin:
        sys.stderr.write("REFUSED (exit 3): the served DATA block does not match its own pin\n")
        return EXIT_PIN
    if fsha != SERVED_FILE_SHA256:   # unconditional: a served file other than the pinned one is refused
        sys.stderr.write(f"REFUSED (exit 3): served file sha256 {fsha[:16]} != the pin of this check {SERVED_FILE_SHA256[:16]}\n")
        return EXIT_PIN
    rep = {"stamp": STAMP, "served": {"path": a.served, "sha256": fsha, "data_block_sha256": got}, "nterms": a.nterms, "dps": a.dps, "planted": a.planted}
    den, idx = structural(D)
    print(f"[structure] den(z) = 20(1+z)(1+9z)(1+25z): {den}; residue Num(0)/20 nilpotent of index {idx} (served: 8)")
    rep["structure"] = {"den": den, "nilpotency_index": idx}
    fails = []
    if idx != 8:
        fails.append("residue not nilpotent of index 8")
    plant = (2, -1, "z3", Fr(1, 100)) if a.planted else None
    F, STATE, IDX = build_recursion(D, a.nterms, plant=plant)
    m1 = IDX[(1, 0)]
    # 3. boundary
    bvec = [F[0][j][m1] for j in range(JMAX + 1)]
    expect = [ring(0), {(0, 1, 0): Fr(80)}, ring(0), ring(0), {(0, 0, 0): Fr(-5)}, ring(0), ring(0), ring(0)]
    b_ok = all(bvec[j] == expect[j] for j in range(JMAX + 1))
    print("[boundary] n = 0 log-tower of M-hat_1|eps^0 = (-s) I^(0):  coefficients of L^0..L^7:")
    for j in range(JMAX + 1):
        print(f"    L^{j}: {rstr(bvec[j])}")
    print(f"    expected (L^0, L^1, L^2, L^3, L^4) = (0, 80 zeta_3, 0, 0, -5), L^5..L^7 = 0: {'PASS (exact in the zeta-ring)' if b_ok else 'FAIL'}")
    rep["boundary"] = {"coefficients": [rstr(x) for x in bvec], "PASS": b_ok}
    if not b_ok:
        fails.append("boundary vector != (0, 80 zeta_3, 0, 0, -5)")
    # 4. operator
    series = {}
    for n in range(a.nterms + 1):
        for j in range(JMAX + 1):
            if F[n][j][m1]:
                series[(n, j)] = F[n][j][m1]
    nmax = a.nterms - 3
    L4S = apply_L4(series, nmax)
    const = L4S.get((0, 0), {})
    others = {k: v for k, v in L4S.items() if k != (0, 0)}
    o_ok = (const == {(0, 0, 0): Fr(-120)}) and not others
    print(f"[operator] L4[(-s) I^(0)] through z^{nmax}: z^0 L^0 coefficient = {rstr(const)}; other nonzero (n, j) coefficients: {len(others)}")
    if others:
        for k in sorted(others)[:6]:
            print(f"    residual at z^{k[0]} L^{k[1]}: {rstr(others[k])}")
    print(f"    => L4[(-s) I^(0)] = -120 exactly, i.e. L4[s I^(0)] = +120 = 5! (the reference derivation's normalisation): {'PASS' if o_ok else 'FAIL'}")
    rep["operator"] = {"z0L0": rstr(const), "n_other_nonzero": len(others), "checked_through_z_power": nmax, "PASS": o_ok}
    if not o_ok:
        fails.append("L4[(-s) I^(0)] != -120 exactly")
    # 5. numeric cross-check vs the reference closed form at z0 = 1/128
    z0 = Fr(1, 128)
    with mp.workdps(a.dps + 10):
        Lz = mp.log(mp.mpf(z0.numerator) / z0.denominator)
        served_val = mp.mpf(0)
        for (n, j), c in series.items():
            served_val += rval(c, a.dps + 10) * (mp.mpf(z0.numerator) / z0.denominator) ** n * Lz ** j
        # truncation: the series at z0 = 1/128 with nterms terms; the nearest pole 1/25: (25/128)^nterms
        trunc = float((25 / 128) ** a.nterms)
        aF = aesz34_frobenius(a.nterms, K=5)
        w4 = varpi_j_at(aF, 4, mp.mpf(z0.numerator) / z0.denominator, a.dps + 10)
        w1 = varpi_j_at(aF, 1, mp.mpf(z0.numerator) / z0.denominator, a.dps + 10)
        modelB = -120 * w4 + 80 * mp.zeta(3) * w1
        diff = abs(served_val - modelB) / abs(modelB)
        dd = float(-mp.log10(diff)) if diff > 0 else float(a.dps)
    cap = -math.log10(trunc)
    print(f"[numeric] at z0 = 1/128 with {a.nterms} terms (truncation ~ (25/128)^{a.nterms} = 1e{math.log10(trunc):.1f}): served series {mp.nstr(served_val, 30)} vs the reference closed form -120 varpi_4 + 80 zeta_3 varpi_1 = {mp.nstr(modelB, 30)}: {dd:.1f} d (truncation cap {cap:.1f} d)")
    num_ok = dd >= min(cap - 1, a.dps - 5)
    rep["numeric"] = {"z0": "1/128", "served": mp.nstr(served_val, a.dps), "reference_closed_form": mp.nstr(modelB, a.dps), "agree_d": round(dd, 1), "truncation_cap_d": round(cap, 1), "PASS": num_ok,
                      "note": "raise --nterms for more digits; the check is the exact algebra above, this row is the cross-check of the two normalisations"}
    if not num_ok:
        fails.append(f"numeric cross-check vs the reference closed form {dd:.1f} d below the cap")
    rep["fails"] = fails
    rep["VERDICT"] = "PASS" if not fails else "FAIL"
    print("REDUCED STATEMENT:", rep["VERDICT"], ("" if not fails else str(fails)))
    if a.json:
        with open(a.json, "x") as f:
            json.dump(rep, f, indent=1, default=str)
        print("report written:", a.json)
    return 0 if not fails else EXIT_FAIL


if __name__ == "__main__":
    sys.exit(main())
