Research-Stack/5-Applications/tools-scripts/verifier/audit_gsp_variants.py

392 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
audit_gsp_variants.py
=====================
Run every GSP closure backend against the burgers_verifier gate suite.
Wraps each backend in `5-Applications/scripts/gsp/backends.py` and
`5-Applications/scripts/gsp/perceval_backend.py` as a `closure_fn(t, a, ν₀)`
of the form
ν_eff = ν₀ · (1 + β · Ω(a))
per `BurgersHarmonicPeelingVerification.md`. The verifier evaluates each
candidate against:
G1 Cole-Hopf or pseudo-spectral cosim (ν=0.01 → pseudo-spectral)
G2 energy dissipation property (ν_eff > 0 ⇒ dE/dt ≤ 0)
G4 ν → ∞ heat-equation limit (sanity in stiff regime)
Writes a comparison table + JSON bundle. The result is the
*verifier-evaluated* ranking, independent of any agent text.
"""
from __future__ import annotations
import json
import sys
from dataclasses import asdict
from pathlib import Path
REPO = Path(__file__).resolve().parents[3]
# Import GSP backends as a package so the relative import in perceval_backend.py works.
sys.path.insert(0, str(REPO / "5-Applications" / "scripts"))
sys.path.insert(0, str(REPO / "5-Applications" / "tools-scripts" / "verifier"))
from gsp.backends import ( # noqa: E402
ClassicalHeuristicBackend,
EddyViscosityROMBackend,
LearnedClosureBaselineBackend,
SoftwareTriangleBackend,
)
from gsp.perceval_backend import PercevalGeometryShaverBackend # noqa: E402
# Verifier.
from burgers_verifier import ( # noqa: E402
DEFAULT_AMPS,
PseudoSpectralReference,
closure_constant,
gate_g1_cole_hopf_cosim,
gate_g2_energy_dissipation,
gate_g4_heat_equation_limit,
integrate_triad,
)
from run_dag import RunDAG # noqa: E402
Q16_ONE = 1 << 16
VERIFIER_PATH = REPO / "5-Applications" / "tools-scripts" / "verifier" / "burgers_verifier.py"
AUDIT_PATH = REPO / "5-Applications" / "tools-scripts" / "verifier" / "audit_gsp_variants.py"
RUN_DAG_PATH = REPO / "5-Applications" / "tools-scripts" / "verifier" / "run_dag.py"
# =============================================================================
# Backend → closure_fn adapter
# =============================================================================
def make_closure_fn(backend, beta: float = 0.5, n_samples: int = 256, seed: int = 42,
memoize: bool = True, memo_decimals: int = 5,
formula: str = "additive"):
"""Wrap a VirtualSubstrateBackend as a closure_fn(t, a, ν₀) → ν_eff.
`formula` selects the ν_eff formula:
- "additive" : ν_eff = ν₀ + β · Ω (matches runner.py:135)
- "multiplicative" : ν_eff = ν₀ · (1 + β · Ω) (matches the spec doc and
the upstream Perceval
repro from 2026-05-03)
The additive form caused 25× ν_eff inflation for variant D at ν₀=0.01,
Ω≈0.5 — a wrapper-side bug, not a Perceval-side bug. This option lets the
audit measure both forms head-to-head against the same gates.
`memoize` caches Ω by quantised state-tuple — RK45 evaluates the RHS at
many sub-step stages with similar a, and each photonic-sampler call costs
real time. The cache key is rounded to `memo_decimals` decimal places (5 ≈
1e-5, well below shot-noise resolution for the witness).
"""
cache: dict[tuple, float] = {}
if formula == "additive":
def combine(nu0, omega): return nu0 + beta * omega
elif formula == "multiplicative":
def combine(nu0, omega): return nu0 * (1.0 + beta * omega)
else:
raise ValueError(f"unknown formula {formula!r}; use 'additive' or 'multiplicative'")
def closure_fn(t, a_float, nu0):
if memoize:
key = tuple(round(x, memo_decimals) for x in a_float)
if key in cache:
return combine(nu0, cache[key])
a_q16 = tuple(int(round(x * Q16_ONE)) for x in a_float)
theta = backend.encode(a_q16)
substrate = backend.program(theta)
hist = backend.sample(substrate, n_samples, seed)
omega_q16 = backend.witness(hist)
omega_float = omega_q16 / Q16_ONE
if memoize:
cache[key] = omega_float
return combine(nu0, omega_float)
return closure_fn
# =============================================================================
# The variants (matches the conversation's AG nomenclature)
# =============================================================================
VARIANTS = {
# Variant A — no viscosity at all. Hard limit case.
"A_no_viscosity": (lambda t, a, nu0: 0.0, "ν_eff = 0 (no closure, no diffusion)"),
# Variant B — fixed viscosity, no closure correction. The honest baseline.
"B_constant_baseline": (closure_constant, "ν_eff = ν₀ (constant; the bar to beat)"),
# Variant C — classical heuristic.
"C_classical_heuristic": (
make_closure_fn(ClassicalHeuristicBackend(c1=0.1, c2=0.1, c3=0.0)),
"ν_eff = ν₀ + 0.5·(0.1|a₁a₃| + 0.1|a₂a₃|)"
),
# Variant D — Perceval photonic witness (M=6, 3-photon Fock input).
# n_samples=64 keeps shot noise reasonable while making the RK45-driven
# repeat-call burden tractable; memoization in make_closure_fn dedupes
# within-step state-tuple repeats from the integrator.
"D_perceval_local": (
make_closure_fn(PercevalGeometryShaverBackend(M=6, exhaust_modes=(3, 4, 5)),
n_samples=64),
"ν_eff = ν₀ + 0.5·Ω_Q, Ω_Q = Σ exhaust-mode photon-counts (M=6, 3-photon Fock)"
),
# Variant F — learned-closure baseline (mock LSTM/NODE).
"F_learned_closure": (
make_closure_fn(LearnedClosureBaselineBackend()),
"mock LSTM/NODE: nonlinear fn of recent 3-step history"
),
# Variant G — eddy-viscosity ROM (Smagorinsky-style).
"G_eddy_viscosity": (
make_closure_fn(EddyViscosityROMBackend(c_smag=0.1)),
"ν_eff = ν₀ + 0.5·0.1·|a₃| (Smagorinsky-style)"
),
# Variant null — SoftwareTriangleBackend explicit (Ω=0, identical to B).
"null_software_triangle": (
make_closure_fn(SoftwareTriangleBackend()),
"Ω = 0 always (identical to B in effect; sanity check on adapter)"
),
# ===== Multiplicative-formula variants (ν_eff = ν₀·(1 + β·Ω)) =====
# Spec-aligned form. Differs from the runner.py:135 additive form;
# added per the 2026-05-03 Perceval repro analysis to test whether the
# over-damping localises to the additive formula vs the photonic substrate.
"C2_classical_multiplicative": (
make_closure_fn(ClassicalHeuristicBackend(c1=0.1, c2=0.1, c3=0.0),
formula="multiplicative"),
"ν_eff = ν₀·(1 + 0.5·(0.1|a₁a₃| + 0.1|a₂a₃|)) (multiplicative)"
),
"D2_perceval_multiplicative": (
make_closure_fn(PercevalGeometryShaverBackend(M=6, exhaust_modes=(3, 4, 5)),
n_samples=64, formula="multiplicative"),
"ν_eff = ν₀·(1 + 0.5·Ω_Q) (multiplicative; per spec + repro)"
),
"F2_learned_multiplicative": (
make_closure_fn(LearnedClosureBaselineBackend(), formula="multiplicative"),
"mock LSTM/NODE, multiplicative ν_eff"
),
"G2_eddy_multiplicative": (
make_closure_fn(EddyViscosityROMBackend(c_smag=0.1), formula="multiplicative"),
"ν_eff = ν₀·(1 + 0.5·0.1·|a₃|) (Smagorinsky, multiplicative)"
),
}
# =============================================================================
# Run
# =============================================================================
def _build_variant_dag(name: str, desc: str, run, g1, g2, g4, ref,
nu0: float, t_final: float, n_eval: int, beta: float,
dag_dir: Path) -> tuple[str, Path]:
"""Build a per-variant Merkle DAG of inputs → reference → integrate → gates → verdict."""
dag = RunDAG(
run_type=f"gsp_variant_audit:{name}",
code_paths=[VERIFIER_PATH, AUDIT_PATH, RUN_DAG_PATH],
)
# Inputs
dag.add_input("input.amps", list(DEFAULT_AMPS))
dag.add_input("input.nu0", nu0)
dag.add_input("input.t_final", t_final)
dag.add_input("input.n_eval", n_eval)
dag.add_input("input.beta", beta)
dag.add_input("input.closure_name", name)
dag.add_input("input.closure_description", desc)
# Reference (independent of closure — same for all variants but recorded per-DAG)
dag.add_compute(
"compute.reference",
function="PseudoSpectralReference",
parents=["input.amps", "input.nu0"],
output_summary={"type": "PseudoSpectralReference", "N": ref.N, "dt": ref.dt},
)
# Integration of the candidate closure
dag.add_compute(
"compute.integrate_triad",
function="solve_ivp(triad_rhs_with_closure)",
parents=["input.amps", "input.nu0", "input.t_final", "input.n_eval",
"input.beta", "input.closure_name"],
output_summary={
"n_steps": int(run.t.size),
"a_final": run.a[-1].tolist(),
"energy_initial": float(run.energy[0]),
"energy_final": float(run.energy[-1]),
"nu_eff_min": float(run.nu_eff.min()),
"nu_eff_max": float(run.nu_eff.max()),
},
)
# Heat-equation-limit run (separate solve at large ν)
dag.add_compute(
"compute.heat_limit_run",
function="integrate_triad(closure, nu=50, t∈[0,0.05])",
parents=["input.closure_name"],
output_summary={"nu_large": 50.0, "t_span": [0.0, 0.05]},
)
# Gates
dag.add_gate(
"gate.G1_reference_cosim",
function="gate_g1_cole_hopf_cosim",
parents=["compute.integrate_triad", "compute.reference"],
result={"passes": g1.passes, "metric": g1.metric, "threshold": g1.threshold,
"note": g1.note, "detail": g1.detail},
)
dag.add_gate(
"gate.G2_energy_dissipation",
function="gate_g2_energy_dissipation",
parents=["compute.integrate_triad"],
result={"passes": g2.passes, "metric": g2.metric, "threshold": g2.threshold,
"note": g2.note, "detail": g2.detail},
)
dag.add_gate(
"gate.G4_heat_equation_limit",
function="gate_g4_heat_equation_limit",
parents=["compute.heat_limit_run"],
result={"passes": g4.passes, "metric": g4.metric, "threshold": g4.threshold,
"note": g4.note, "detail": g4.detail},
)
dag.add_verdict("verdict",
gate_ids=["gate.G1_reference_cosim",
"gate.G2_energy_dissipation",
"gate.G4_heat_equation_limit"])
out_path = dag_dir / f"variant_{name}.dag.json"
dag.emit(out_path)
return dag.merkle_root(), out_path
def audit(nu0: float = 0.01, t_final: float = 2.0, n_eval: int = 101,
beta: float = 0.5) -> dict:
print("=" * 80)
print(f"GSP VARIANT AUDIT — verifier-grounded (ν₀={nu0}, t_final={t_final})")
print("=" * 80)
print("reference: PseudoSpectralReference (validated by G5 vs Cole-Hopf at ν=0.05)")
print()
ref = PseudoSpectralReference(DEFAULT_AMPS, nu0, dt=5e-4)
print(f"reference resolution: N={ref.N}, dt={ref.dt}")
print()
dag_dir = REPO / "shared-data" / "artifacts" / "burgers_verifier" / "dag"
dag_dir.mkdir(parents=True, exist_ok=True)
print(f"{'variant':<26} {'G1 max-rel':>12} {'G2 dissip.':>12} {'G4 heat lim':>12} "
f"{'gates':>6} {'merkle_root':<20}")
print("-" * 100)
results: dict[str, dict] = {}
for name, (closure, desc) in VARIANTS.items():
try:
run = integrate_triad(closure, nu0, t_span=(0.0, t_final), n_eval=n_eval)
g1 = gate_g1_cole_hopf_cosim(run, ref)
g2 = gate_g2_energy_dissipation(run)
g4 = gate_g4_heat_equation_limit(closure)
n_passed = sum([g1.passes, g2.passes, g4.passes])
merkle_root, dag_path = _build_variant_dag(
name, desc, run, g1, g2, g4, ref, nu0, t_final, n_eval, beta, dag_dir
)
results[name] = {
"description": desc,
"G1": asdict(g1),
"G2": asdict(g2),
"G4": asdict(g4),
"gates_passed": n_passed,
"gates_total": 3,
"dag_path": str(dag_path.relative_to(REPO)),
"dag_merkle_root": merkle_root,
}
print(f"{name:<26} {g1.metric:>12.4f} "
f"{g2.metric:>12.2e} "
f"{g4.metric:>12.4f} "
f"{n_passed:>4}/3 {merkle_root[:20]}")
except Exception as exc:
print(f"{name:<26} ERROR: {type(exc).__name__}: {exc}")
results[name] = {"description": desc, "exception": f"{type(exc).__name__}: {exc}"}
# Ranking by G1 (the primary closure-quality metric).
ranked = sorted(
[(n, r) for n, r in results.items() if "G1" in r],
key=lambda kv: kv[1]["G1"]["metric"]
)
print()
print("ranking by G1 (Cole-Hopf/pseudo-spectral cosim, lower is better):")
print(f"{'rank':<6} {'variant':<26} {'G1 max-rel':>12} description")
print("-" * 100)
for i, (name, r) in enumerate(ranked, 1):
marker = " ← best" if i == 1 else " ← worst" if i == len(ranked) else ""
print(f"{i:<6} {name:<26} {r['G1']['metric']:>12.4f} {r['description']}{marker}")
# Master DAG aggregating per-variant verdicts.
master = RunDAG(run_type="gsp_audit_master",
code_paths=[VERIFIER_PATH, AUDIT_PATH, RUN_DAG_PATH])
master.add_input("input.audit_config",
{"nu0": nu0, "t_final": t_final, "n_eval": n_eval,
"beta": beta, "amps": list(DEFAULT_AMPS)})
master.add_input("input.variants", list(VARIANTS.keys()))
# One compute node per variant (records merkle root from sub-DAG)
variant_node_ids: list[str] = []
for name, r in results.items():
if "exception" in r:
continue
nid = f"variant.{name}"
master.add_compute(
nid,
function="audit_variant",
parents=["input.audit_config", "input.variants"],
output_summary={
"G1_metric": r["G1"]["metric"],
"G2_passes": r["G2"]["passes"],
"G4_passes": r["G4"]["passes"],
"gates_passed": r["gates_passed"],
"child_dag_path": r["dag_path"],
"child_dag_merkle_root": r["dag_merkle_root"],
},
)
# Synthetic gate node so the verdict sees pass-state per variant
master.add_gate(
f"gate.{name}_meets_baseline",
function="meets_constant_baseline_G1",
parents=[nid],
result={"passes": r["G1"]["metric"] < results.get("B_constant_baseline", {})
.get("G1", {}).get("metric", float("inf"))},
)
variant_node_ids.append(f"gate.{name}_meets_baseline")
if variant_node_ids:
master.add_verdict("verdict.master", gate_ids=variant_node_ids)
master_path = dag_dir / "audit_master.dag.json"
master.emit(master_path)
print(f"\nmaster DAG: {master_path.relative_to(REPO)} merkle_root={master.merkle_root()[:30]}")
return {
"nu0": nu0, "t_final": t_final, "n_eval": n_eval,
"amps": DEFAULT_AMPS,
"reference_type": "PseudoSpectralReference",
"reference_N": ref.N,
"reference_dt": ref.dt,
"beta": beta,
"variants": results,
"ranking_by_G1": [n for n, _ in ranked],
"dag_dir": str(dag_dir.relative_to(REPO)),
"master_dag_path": str(master_path.relative_to(REPO)),
"master_dag_merkle_root": master.merkle_root(),
}
def main():
out_dir = REPO / "shared-data" / "artifacts" / "burgers_verifier"
out_dir.mkdir(parents=True, exist_ok=True)
bundle = audit(nu0=0.01, t_final=2.0, n_eval=101)
out = out_dir / "audit_gsp_variants.json"
out.write_text(json.dumps(bundle, indent=2, default=str))
print(f"\nwrote: {out}")
if __name__ == "__main__":
main()