#!/usr/bin/env python3
r"""sunrise-row10-evaluate.py -- two-loop sunrise, unequal squared masses (1,2,3), eps^0:
STANDALONE evaluator (row 10 of the paper's table of integrals).

Two-loop sunrise S_111(2-2eps, t), one external scale t = p^2 (Euclidean t < 0), three
massive propagators with squared masses (m1^2, m2^2, m3^2) = (1, 2, 3) -- the fully
generic assignment: all three Abel-Jacobi marked points distinct.  mu = 1.
Object: the eps^0 coefficient J[1,1,1]^(0)(t) of the (1,1,1,0,0) master (d = 2 - 2 eps,
AMFlow normalization), and its period-stripped form E^(0) = J^(0)/psihat1.

CLOSED FORM (uniform per-puncture Kronecker-eMPL formula, zero free parameters):

    J^(0)/psihat1 = -C_{4,2}(t) - (6/(2 pi)^3) * (1/3) * sum_{j=1}^{3} [ W(z_j,1) - 8 W(z_j,2) ]

  * psihat1 = |psi1_F|/pi: BMSW Feynman-curve holomorphic period (elliptic K).
  * z1, z2, z3: Abel-Jacobi images of the three punctures (incomplete elliptic
    integrals), all distinct for this row; z1+z2+z3 = 1 enforced (branch
    repair, in the shared layer's fcurve).
  * W(z,N) = I(1, g^(3)(z, N tau_C); q_C): depth-two Kronecker-eMPL word,
    convergent q-series with the PROVEN tail bound |b_n| <= C n^2,
    C = (2 pi)^3 zeta(2) = 408.03, certified + ENFORCED at runtime by
    sunrise_empl.Nq_for (fail-closed at the depth cap); the achieved bound is
    propagated to value level and printed (BOUND, not estimate).
  * C_{4,2} = sum_j (1/2i)[Li2(w_j) - Li2(1/w_j)]: elliptic-dilogarithm boundary.
  * Per-puncture coefficients [-1, -1/3, +8/3]: PSLQ-exact rationals, verified at
    139-159-digit closure on the reference points below; one of them, t = -3, was
    held out of the PSLQ identification, and a second, t = -9, was computed after the
    form was fixed (the data file's held_out_in_pslq_fit flags; no other point is flagged).  ZERO free parameters at runtime.

All machinery lives in the shared module sunrise_empl.py (same directory): pure
mpmath, self-contained q-series from the printed kernel definitions, no IBP
reduction, no differential-equation solver, no network at runtime.

WHAT THE DEFAULT RUN CHECKS (exit 0 only if every leg passes):
  1. positive control: the same marked-point machinery at equal mass (1,1,1) vs
     the independent classical Gamma_1(6) route (Eisenstein series -> newform f3
     -> I(1,f3;q_C), boundary (3/2) sqrt3 L(chi_{-3},2) via Hurwitz zeta);
  2. independent-reference gate: J^(0) at 6 Euclidean points (two of them, t = -3 and t = -9,
     held out of the PSLQ fit) vs 159-309-digit AMFlow reference values (data file;
     comparison only, never in the evaluation); bar: worst agreement STRICTLY
     > 30 digits and relative error < 10^-30/2;
  3. the paper prints NO digits for this row (nothing to compare digit by digit);
     it states "held out to 139-159 digits" (the paper's wording) -- measured here
     as the worst agreement over the 6 reference points, tested when the run's
     precision reaches it (the default does).

FILES (beside this script; sha256-pinned below and REFUSED on any mismatch):
  sunrise_empl.py             shared Kronecker-eMPL layer (curve, marked points, words, gates)
  sunrise-row10-data.json     independent reference values (t = -3 and t = -9 held out of the fit) + the paper's stated digit count

DOMAIN: Euclidean t < 0.  |q_C| -> 0 at the soft point t -> 0^-; |q_C| >= 0.8995
(astronomically deep |t|; keeps 1/(1-|q_C|) < 10, one budgeted guard digit) is
refused explicitly.  Gate-verified window -9 <= t <= -1/3.  Physical t > 0 is
NOT wired (analytic continuation of the q-series frame not vendored).

EXIT CODES: 0 every gate passes; 1 a gate fails (this is what --mutate must
produce); 2 usage error or --point domain refusal; 3 a pinned file's sha256 does
not match (refused before any computation); 4 a required file or mpmath is missing.

USAGE
  python3 sunrise-row10-evaluate.py                    # default: dps 150, all checks (seconds)
  python3 sunrise-row10-evaluate.py --dps 60           # lower precision (paper claim then SKIPs)
  python3 sunrise-row10-evaluate.py --point=-11/4      # any Euclidean t < 0 (use --point= for negatives)
  python3 sunrise-row10-evaluate.py --check            # two-precision rule: rerun gates at dps+60, diff
  python3 sunrise-row10-evaluate.py --mutate           # control: +8 -> 8(1+1e-12); MUST exit nonzero

mp.dps is set inside main() after argparse (module-level mpf footgun avoided).
Dependency: python3 + mpmath (pip install mpmath).
"""
import argparse
import hashlib
import json
import os
import sys

EXIT_PASS, EXIT_FAIL, EXIT_USAGE, EXIT_PIN, EXIT_MISSING = 0, 1, 2, 3, 4

ROW = 10
HERE = os.path.dirname(os.path.abspath(__file__))
EMPL_FILE = os.path.join(HERE, "sunrise_empl.py")
DATA_FILE = os.path.join(HERE, "sunrise-row10-data.json")
EMPL_SHA256 = "1928e35f0edd8197fc10550ea439acaa21e5c4ae4136396b0493fe49331f62bf"
DATA_SHA256 = "90e8988c95e4e9435648f7f3ce3a10562bb933a80156d5702528b8730c5910d7"


def _pinned_bytes(path, pin):
    """Read a companion file and refuse it unless its sha256 matches the pin."""
    name = os.path.basename(path)
    if not os.path.exists(path):
        print(f"MISSING: {name} must sit beside this script (download it from the same page); "
              f"nothing computed")
        sys.exit(EXIT_MISSING)
    raw = open(path, "rb").read()
    sha = hashlib.sha256(raw).hexdigest()
    if sha != pin:
        print(f"REFUSED: {name} sha256 {sha} ({len(raw)} bytes) does not match the pin "
              f"{pin} -- the file was altered or is not the released version; nothing computed")
        sys.exit(EXIT_PIN)
    return raw


def main():
    ap = argparse.ArgumentParser(
        description="Row 10: unequal-mass sunrise (1,2,3) eps^0, "
                    "uniform per-puncture Kronecker-eMPL closed form")
    ap.add_argument("--dps", type=int, default=150,
                    help="reported decimal digits (default 150; +10 guard digits internally)")
    ap.add_argument("--point", action="append", default=[],
                    help="extra Euclidean t < 0 (rational -7/2 or decimal; "
                         "use --point=-7/2 syntax for negatives); repeatable")
    ap.add_argument("--check", action="store_true",
                    help="two-precision rule: rerun all gate points at dps+60 and diff")
    ap.add_argument("--mutate", action="store_true",
                    help="control: perturb the exact coefficient +8 to 8(1+1e-12); run must exit nonzero")
    args = ap.parse_args()
    if args.dps < 30:
        ap.error("--dps must be >= 30 (the gate bar is 30 digits)")   # argparse exits 2

    try:
        import mpmath as mp
    except ImportError:
        print("MISSING: python3 module mpmath (pip install mpmath); nothing computed")
        return EXIT_MISSING

    raw_data = _pinned_bytes(DATA_FILE, DATA_SHA256)   # refused before any computation
    _pinned_bytes(EMPL_FILE, EMPL_SHA256)
    sys.path.insert(0, HERE)
    import sunrise_empl as R  # noqa: E402
    print(f"[pins] {os.path.basename(DATA_FILE)} sha256 {DATA_SHA256[:16]}... and "
          f"sunrise_empl.py sha256 {EMPL_SHA256[:16]}... match")

    mp.mp.dps = args.dps + 10  # guard digits; gate digits reported vs the references
    data = json.loads(raw_data)
    if data["row"] != ROW:
        print(f"REFUSED: data file is for row {data['row']}, not row {ROW}")
        return EXIT_PIN
    try:
        worst, wall = R.run_row(ROW, data, args)   # SystemExit(2) on a --point refusal
    except RuntimeError as e:
        print(f"\n[gate] FAIL (exit {EXIT_FAIL}): {e}")
        return EXIT_FAIL
    print(f"\n[gate] PASS: positive control, independent-reference table (worst {worst} digits, "
          f"strict bar > 30) and the paper's stated digit count all clear"
          + (" (two-precision --check stable)" if args.check else ""))
    return EXIT_PASS


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