#!/usr/bin/env python3
r"""mgf-evaluate.py — standalone evaluator for the Laurent polynomial of the
weight-8 dihedral (four-edge banana) modular graph function C_{2,2,2,2}(tau).

    C_{2,2,2,2}|_Laurent = sum_k c_k * y^k,   y = pi*tau_2,
    k in {+8, +1, -1, -2, -3, -4, -5, -6, -7}   (all other c_k = 0).

Every coefficient is COMPUTED at runtime from its closed form
(rational x zeta-monomial); nothing is embedded as a floating-point literal.
The single non-classical constant, the depth-3 single-valued MZV
zeta_sv(5,3,5), is computed live from Brown's explicit formula
[arXiv:1309.5309 eq.(7.4)] with the underlying MZVs evaluated by the
Richardson-Hurwitz nsum machinery (ported from c5_verify350.py / c5_close.py,
dps-keyed caches).

Closed forms (provenance):
  c_{+8}..c_{-7} products : C2222_final.json 'closed' records; two-precision
      PSLQ at dps 150/200 on 280d sector-sum input (C2222_2prec_v2.json),
      identical gcd-canonical vectors, resid <= 1e-147/1e-197.
  c_{-5} sv form          : c5_sv.json PSLQ on the 5-elt svMZV_13 basis,
      legs (200,250) identical, positive control PASS; equivalent to the
      13-term raw-MZV vector in c5_close.json (P13_indep_with_z2).
      Reverified: 343.97d vs a fresh 350d sector sum (c5_verify350.json),
      93.97d beyond the 250-digit precision of the fit.

Reference values (--check): the recorded 280-digit sector-sum strings from
C2222_dps280.json (laurent_full.py winding/Poisson sector expansion). That
sector-expansion computation produced the PSLQ *input*; the closed forms were
fit with legs at 200/250 dps, so agreement beyond 250 digits is beyond the
precision of the fit, and any agreement here is a computation of the closed
form, never a copy of the reference string.

Usage:
    python3 mgf-evaluate.py --dps 120 --check
    python3 mgf-evaluate.py --dps 200 --tau2 21

Requires: Python 3 + mpmath only. Arbitrary precision via --dps.

Provenance (blog staging): copied 2026-07-05 from the MGF campaign's copy of
record (audited: mpmath-only, all Laurent coefficients computed at runtime
from closed forms incl. live zeta_sv(5,3,5) via Brown's formula; the embedded
280d strings serve only as checks, never as output).

Changelog:
  2026-07-05  blog staging copy: provenance + changelog header added. No
              functional change; CLI (--dps N [--tau2 T] [--check]) and the
              check-table format are unchanged.
  2026-07-05  (1) the reference-string check is now UNCONDITIONAL — every
      run prints the CHECK line and fails closed (exit 1) on any coefficient
      drift; --check is kept for CLI compatibility (adds the explicit exit-0
      on pass, as before). (2) the mzv2/mzv3 Richardson-nsum evaluations now
      carry a dps+30 two-precision agreement RAISE (threshold 10^-(dps-10);
      calibrated 2026-07-05 on the campaign's own runs, worst healthy
      residual 10^-(dps-0.7) at dps 30 => >= 10^9 headroom). CLI and the
      check-table output format are unchanged.
  2026-09-03  public render: internal working vocabulary and run paths
      removed from comments, strings and printed output; numerics, CLI and
      exit codes unchanged.
"""
import argparse
import sys
import time

import mpmath as mp
from mpmath import mpf, zeta, hurwitz, pi

# ---------------------------------------------------------------------------
# MZV machinery — Richardson-Hurwitz nsum (port of c5_verify350.py / c5_close.py
# mzv3 and tools/tornheim.py mzv2), with dps-KEYED caches (tornheim cache-poison
# bug of 2026-07-01: caches not keyed by dps capped inputs at ~155d; fixed here
# by construction).
# Conventions: mzv2(s,t) = sum_{m>n>=1} m^-s n^-t ;
#              mzv3(a,b,c) = sum_{m>n>k>=1} m^-a n^-b k^-c.
# Brown's convention zeta(n1,..,nr) = sum_{k1<..<kr} means
# Brown-zeta(3,5) = mzv2(5,3); zeta(5,3,5) is palindromic (same both ways).
# ---------------------------------------------------------------------------
_MZV2 = {}
_MZV3 = {}

# Two-precision agreement RAISE on the Richardson-nsum MZV evaluations
# (2026-07-05). nsum(method='richardson') is an adaptive-heuristic
# extrapolation with no acted-on error bound; each mzv2/mzv3 value is therefore
# re-evaluated at dps+MZV_XPREC (a genuinely different internal Richardson
# depth) and the run RAISES unless the two agree to 10^-(dps-MZV_AGREE_MARGIN).
# Calibration (on the campaign's own runs, 2026-07-05): worst
# measured healthy relative residual over mzv2(5,3)/mzv3(5,3,5) at dps
# 30/60/75/135/150 is 4.77e-30 at dps 30 = 10^-(dps-0.7); MARGIN=10 leaves
# >= 10^9.3 headroom (healthy runs never false-fire), while a silent
# Richardson precision loss (the 2026-07-01 tornheim ~155d class) fails loudly.
MZV_XPREC = 30
MZV_AGREE_MARGIN = 10


def _two_prec_raise(name, key, v_lo, compute):
    """RAISE unless compute() redone at dps+MZV_XPREC agrees with the
    ambient-dps value v_lo to 10^-(dps - MZV_AGREE_MARGIN)."""
    d = mp.mp.dps
    with mp.workdps(d + MZV_XPREC):
        v_hi = compute()
        rel = abs(v_lo - v_hi) / abs(v_hi)
        tol = mp.mpf(10) ** (-(d - MZV_AGREE_MARGIN))
        if not rel < tol:
            raise RuntimeError(
                f"{name}{key}: two-precision agreement FAILED at dps={d}: "
                f"|v(dps) - v(dps+{MZV_XPREC})|/|v| = {mp.nstr(rel, 3)} >= "
                f"tol {mp.nstr(tol, 3)} -- refusing to return an uncertified MZV")


def mzv2(s, t):
    key = (s, t, mp.mp.dps)
    if key in _MZV2:
        return _MZV2[key]

    def compute():
        return mp.nsum(lambda n: hurwitz(s, int(n) + 1) / n**t, [1, mp.inf],
                       method='richardson')
    v = compute()
    _two_prec_raise('mzv2', (s, t), v, compute)
    _MZV2[key] = v
    return v


def mzv3(a, b, c):
    key = (a, b, c, mp.mp.dps)
    if key in _MZV3:
        return _MZV3[key]

    def compute():
        zc = zeta(c)
        return mp.nsum(lambda n: hurwitz(a, int(n) + 1)
                       * (zc - hurwitz(c, int(n))) / n**b,
                       [1, mp.inf], method='richardson')
    v = compute()
    _two_prec_raise('mzv3', (a, b, c), v, compute)
    _MZV3[key] = v
    return v


def zeta_sv_535():
    """zeta_sv(5,3,5) from Brown arXiv:1309.5309 eq.(7.4):
        zeta_sv(5,3,5) = 2 zeta(5,3,5) - 22 zeta5*zeta(3,5)
                         - 120 zeta3*zeta5^2 - 10 zeta5*zeta8
    (Brown convention; Brown-zeta(3,5) = mzv2(5,3), zeta(5,3,5) palindromic.)
    Formula validated at weight 11 against independent mzv3 numerics
    (c5_sv.json: brown_formula_reproduced_from_mzv3 = True)."""
    z3, z5, z8 = zeta(3), zeta(5), zeta(8)
    return (2 * mzv3(5, 3, 5) - 22 * z5 * mzv2(5, 3)
            - 120 * z3 * z5**2 - 10 * z5 * z8)


# ---------------------------------------------------------------------------
# Closed-form coefficients. Rationals vendored EXACTLY from the recorded PSLQ
# closure records (see module docstring for per-coefficient provenance).
# ---------------------------------------------------------------------------
def coefficients():
    """Return {k: mpf} — all 9 nonzero Laurent coefficients of C_{2,2,2,2},
    computed from closed forms at the current mp.mp.dps."""
    z5, z7, z9, z11, z13, z15 = (zeta(n) for n in (5, 7, 9, 11, 13, 15))
    z3 = zeta(3)
    return {
        # weight 0 : two-prec PSLQ (C2222_final.json /closed/+8)
        8:  mpf(229) / 3322873125,
        # weight 7 : zeta(7)/540
        1:  z7 / 540,
        # weight 9 : -7*zeta(9)/180
        -1: -7 * z9 / 180,
        # weight 10: zeta(5)^2/12
        -2: z5**2 / 12,
        # weight 11: 63*zeta(11)/80
        -3: mpf(63) * z11 / 80,
        # weight 12: -21*zeta(5)*zeta(7)/4
        -4: -mpf(21) * z5 * z7 / 4,
        # weight 13: 13803/800 z13 + 153/10 z3 z5^2 + 51/400 zeta_sv(5,3,5)
        #            (c5_sv.json sv_closed_form; legs 200/250, control PASS)
        -5: (mpf(13803) / 800 * z13 + mpf(153) / 10 * z3 * z5**2
             + mpf(51) / 400 * zeta_sv_535()),
        # weight 14: -(810*zeta(5)*zeta(9)+567*zeta(7)^2)/64
        -6: -(810 * z5 * z9 + 567 * z7**2) / 64,
        # weight 15: 6825*zeta(15)/512
        -7: mpf(6825) * z15 / 512,
    }


def laurent_C2222(tau2):
    """Assembled Laurent polynomial at real tau_2 (y = pi*tau_2)."""
    y = pi * mpf(tau2)
    return mp.fsum(v * y**k for k, v in coefficients().items())


# ---------------------------------------------------------------------------
# Reference strings — recorded 280-digit sector-sum values,
# C2222_dps280.json (laurent_full.py @ dps 280, 2026-07-01). Used ONLY for
# comparison in the CHECK; never fed into the computed values above.
# ---------------------------------------------------------------------------
REFERENCE_280D = {
    8:  '0.00000006891626354226208982926484742025773253078990188648866934996201517624901793384001081895054298830624175576971510159600210435359309723870362940805932817552280152134908852410667951699179004916114574792860922127443550827719309927007820980224756550101502897436085526136361285235649796289',
    1:  '0.001867313476633190420073699166388512517777525112157849456328302104762224959846955065455735127618243192535113803058316805346827365592296917482066293150062934374298276784417118345524729564216662162401280421859841359162028176627597778257491762924727978463118908764703527056867711339927',
    -1: '-0.03896699305434764167180538547014935790777356088757900719911209061871386318739893773695973046605659501723255685200516345461098676552809450595268848606305420909170226742976765438088376971012004161563252085933557442750862028450399983860713535106504947982300099219017265371901788496342',
    -2: '0.08960159744888904472592927797408432119482036056228714547540305636088555664719262227584984073815159802980887580507149089976923506747867038276908885335228418484045458399651250943705387380150111655386216751944804179974452757695032378442947253780141444317691098030870885472651579520045',
    -3: '0.7878891735257440783399780474895950749690275681595892859813238809287925720910981040782910763810539959954610324466612726500985908777852205302951871821829955518944348564838336235553630334436686871283390807356724783475253240190983865581254165701929595517965736274967712807570646140770',
    -4: '-5.489323101129401499172078810366274006709029634754982294071127697565979046737485790935120635547824351947526057130314296873302030737285314903359247548285034789089122924168405881093835275821009057747826263538291104315815520649310678010728818752948782842461114104198621594021395092018',
    -5: '15.81891735480428029267465954293430268323578690411356503082200821447619075295904423594135939120891140498157473853589049957888978651626155319059975044518992781447893848028067054406590603394825953119340730309004957512965597642343132474215034532938398958546860530055299788112914114658',
    -6: '-22.15790562854609730381544936081389392789052672105791088181877008130372783395729821642435449981710917121317916098023373738213842937870912684452204195840973668958833961660621853566715947762964871118633686733229734876571564666740538546441207578113706759892377227423156149288508120416',
    -7: '13.33048586857967854466502059977568857843859521469591222945769490624412641277741417594964586312186478217588781178125265273814105381707744081957945557720184215385484415662720761669901623074945847646475837837982336877941724185819087546942762969659665164403717693231388928837965066425',
}

CLOSED_FORM_STR = {
    8:  '229/3322873125',
    1:  'zeta(7)/540',
    -1: '-7*zeta(9)/180',
    -2: 'zeta(5)^2/12',
    -3: '63*zeta(11)/80',
    -4: '-21*zeta(5)*zeta(7)/4',
    -5: '13803/800*zeta(13) + 153/10*zeta(3)*zeta(5)^2 + 51/400*zeta_sv(5,3,5)',
    -6: '-(810*zeta(5)*zeta(9)+567*zeta(7)^2)/64',
    -7: '6825*zeta(15)/512',
}

GUARD = 15  # internal guard digits


def main():
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    ap.add_argument('--dps', type=int, default=120,
                    help='output precision in decimal digits (default 120)')
    ap.add_argument('--check', action='store_true',
                    help='kept for compatibility: the reference-string check '
                         'now runs UNCONDITIONALLY on every run (fails '
                         'closed, exit 1); --check additionally exits 0 on '
                         'pass')
    ap.add_argument('--tau2', type=float, default=None,
                    help='also evaluate the assembled Laurent polynomial at '
                         'this tau_2')
    args = ap.parse_args()
    if args.dps < 15:
        ap.error('--dps must be >= 15')

    t0 = time.time()
    mp.mp.dps = args.dps + GUARD
    C = coefficients()
    mp.mp.dps = args.dps

    print(f"# C_{{2,2,2,2}} Laurent coefficients, computed at dps={args.dps} "
          f"(+{GUARD} guard)")
    npass = 0
    worst = mpf('inf')
    for k in sorted(C, reverse=True):
        print(f"c_{{{k:+d}}}  = {CLOSED_FORM_STR[k]}")
        print(f"        = {mp.nstr(C[k], args.dps)}")
        ref = mpf(REFERENCE_280D[k])
        agree = float(-mp.log10(abs(C[k] - ref) / abs(ref)))
        print(f"        vs recorded 280d sector-sum: {agree:.1f} matched digits")
        worst = min(worst, agree)
        # limited by output dps (minus small nsum/rounding slack) or by the
        # ~280-digit reference string, whichever is shorter
        if agree >= min(args.dps, 265) - 12:
            npass += 1

    if args.tau2 is not None:
        mp.mp.dps = args.dps + GUARD
        val = laurent_C2222(args.tau2)
        mp.mp.dps = args.dps
        print(f"Laurent(tau_2={args.tau2}) = {mp.nstr(val, args.dps)}")

    wall = time.time() - t0
    print(f"# wall = {wall:.2f} s")
    # 2026-07-05: the check is UNCONDITIONAL — every run prints the CHECK
    # line and fails closed (exit 1) on any drift from the recorded
    # reference strings; --check keeps its explicit exit-0-on-pass semantics.
    ok = (npass == 9)
    print(f"CHECK: {npass}/9 coefficients agree with reference strings "
          f"(worst {float(worst):.1f}d, threshold {min(args.dps, 265) - 12}d) "
          f"-> {'PASS' if ok else 'FAIL'}")
    if not ok:
        sys.exit(1)
    if args.check:
        sys.exit(0)


if __name__ == '__main__':
    main()
