#!/usr/bin/env python3 """run_all.py - top-level driver for the spectrum verification suite. Runs the three verification engines in turn and aggregates a PASS/FAIL verdict: 1. qn_check.py QUANTUM-NUMBER CONSISTENCY (Q/B/S/C/B' from constituents) 2. mass_relations.py PARAMETER-FREE QCD RELATIONS (GMO, decuplet spacing, isospin, Regge) 3. coverage_check.py COMPLETENESS (every dataset particle has a section home; counts reconcile) GOVERNANCE ---------- RUNNABLE-FIRST : bare Python 3, standard library only (no pip installs). FAIL-CLOSED : if ANY engine exits non-zero -- a quantum-number inconsistency, a missing required field, or a RELATION residual beyond its declared tolerance -- this driver prints FAIL and exits non-zero. A missing engine or unreadable report is ALSO a FAIL (we never silently treat absence as success). SAFE WORDING ------------ This suite verifies that every observed particle is quantum-number-consistent with its geometry-derived constituents and that the parameter-free QCD symmetry relations hold against PDG-2024; it does NOT compute or claim absolute hadron masses from geometry. Each engine is invoked as a subprocess via its documented CLI (using the same Python interpreter that runs this driver), so this driver is decoupled from the engines' internal APIs. The engines are the source of truth for PASS/FAIL; this driver aggregates their exit codes and (when present) their JSON reports. USAGE ----- python run_all.py python run_all.py --data data --out out --sections "" python run_all.py --config relations_config.json Exit code 0 == all engines PASS. Exit code 1 == at least one engine FAILed (or could not be run). No other exit codes are produced by this driver. """ import argparse import json import subprocess import sys import time from dataclasses import dataclass, field from pathlib import Path # --- canonical locations ----------------------------------------------------- # This file lives at the suite root; everything is resolved relative to it so # the driver runs identically from any working directory. SUITE_ROOT = Path(__file__).resolve().parent # Canonical accounting-sections directory (the 51 section files coverage_check # reconciles against). Overridable via --sections; see README.md. DEFAULT_SECTIONS = "/sections" # Engine contract: (module file, JSON report it emits, how to build its argv). # Keeping this as data makes the run order and the per-engine CLI explicit and # auditable -- no magic strings buried in control flow. @dataclass class EngineSpec: name: str # short label script: str # filename of the engine, relative to SUITE_ROOT report_json: str # JSON report it writes under --out (relative to out dir) description: str def build_argv(self, args) -> list: raise NotImplementedError @dataclass class QnSpec(EngineSpec): def build_argv(self, args): return ["--data", str(args.data), "--out", str(args.out)] @dataclass class RelationsSpec(EngineSpec): def build_argv(self, args): return ["--data", str(args.data), "--out", str(args.out), "--config", str(args.config)] @dataclass class CoverageSpec(EngineSpec): def build_argv(self, args): return ["--data", str(args.data), "--sections", str(args.sections), "--out", str(args.out)] ENGINES = [ QnSpec( name="qn_check", script="qn_check.py", report_json="qn_report.json", description="quantum-number consistency (Q/B/S/C/B' from quark content; color-singlet)", ), RelationsSpec( name="mass_relations", script="mass_relations.py", report_json="relations_report.json", description="parameter-free QCD relations (GMO octet, decuplet spacing, isospin signs, Regge)", ), CoverageSpec( name="coverage_check", script="coverage_check.py", report_json="coverage_report.json", description="completeness (every dataset particle has a section home; counts reconcile)", ), ] @dataclass class EngineResult: name: str ok: bool returncode: int seconds: float note: str = "" report: dict = field(default_factory=dict) def _load_report(out_dir: Path, report_json: str) -> dict: """Best-effort load of an engine's JSON report. Absence is NOT a pass-maker: the engine exit code is authoritative; this is only for the summary line.""" p = out_dir / report_json if not p.is_file(): return {} try: return json.loads(p.read_text(encoding="utf-8")) except (ValueError, OSError): return {} def _summary_line(name: str, report: dict) -> str: """Pull a human-readable one-liner out of whatever report shape the engine emitted. We probe a few common keys but never fabricate numbers.""" if not report: return "" bits = [] for key in ("total", "total_particles", "covered", "coverage_total", "particles_checked", "rows"): if key in report: bits.append(f"{key}={report[key]}") break for key in ("pass", "passes", "passed", "pass_count"): if key in report: bits.append(f"pass={report[key]}") break for key in ("fail", "failures", "failed", "fail_count"): if key in report: bits.append(f"fail={report[key]}") break for key in ("orphans", "orphan_count"): if key in report: bits.append(f"orphans={report[key]}") break return " (" + ", ".join(bits) + ")" if bits else "" def run_engine(spec: EngineSpec, args) -> EngineResult: script_path = SUITE_ROOT / spec.script if not script_path.is_file(): # FAIL-CLOSED: a missing engine is a failure, never a skip. return EngineResult( name=spec.name, ok=False, returncode=127, seconds=0.0, note=f"engine script not found: {script_path}", ) argv = [sys.executable, str(script_path)] + spec.build_argv(args) print(f">> {spec.name}: {spec.description}") print(f" $ {' '.join(argv)}") start = time.monotonic() try: proc = subprocess.run(argv, cwd=str(SUITE_ROOT)) except OSError as exc: # could not even launch the interpreter/script return EngineResult( name=spec.name, ok=False, returncode=126, seconds=time.monotonic() - start, note=f"failed to launch engine: {exc}", ) elapsed = time.monotonic() - start report = _load_report(args.out, spec.report_json) ok = proc.returncode == 0 return EngineResult( name=spec.name, ok=ok, returncode=proc.returncode, seconds=elapsed, report=report, note="" if ok else f"engine exited {proc.returncode}", ) def parse_args(argv=None): ap = argparse.ArgumentParser( prog="run_all.py", description="Driver: run qn_check, mass_relations, coverage_check; " "aggregate PASS/FAIL (fail-closed, stdlib-only).", ) ap.add_argument("--data", default=str(SUITE_ROOT / "data"), type=Path, help="dataset directory containing dataset_*.csv " "(default: /data)") ap.add_argument("--out", default=str(SUITE_ROOT / "out"), type=Path, help="output directory for the engine reports " "(default: /out)") ap.add_argument("--config", default=str(SUITE_ROOT / "relations_config.json"), type=Path, help="tolerance config for mass_relations " "(default: /relations_config.json)") ap.add_argument("--sections", default=DEFAULT_SECTIONS, type=Path, help="accounting-sections directory for coverage_check " f"(default: {DEFAULT_SECTIONS})") return ap.parse_args(argv) def main(argv=None) -> int: args = parse_args(argv) args.out.mkdir(parents=True, exist_ok=True) print("=" * 72) print("SPECTRUM VERIFICATION SUITE -- run_all.py") print("Verifies quantum-number consistency + parameter-free QCD relations") print("against PDG-2024. Does NOT claim absolute hadron masses from geometry.") print("=" * 72) print(f"data : {args.data}") print(f"out : {args.out}") print(f"config : {args.config}") print(f"sections : {args.sections}") print("-" * 72) results = [] for spec in ENGINES: results.append(run_engine(spec, args)) print("-" * 72) # --- summary -------------------------------------------------------------- print("SUMMARY") all_ok = True for r in results: verdict = "PASS" if r.ok else "FAIL" line = f" [{verdict}] {r.name:<16} ({r.seconds:5.2f}s)" line += _summary_line(r.name, r.report) if r.note: line += f" -- {r.note}" print(line) all_ok = all_ok and r.ok print("=" * 72) if all_ok: print("OVERALL: PASS -- all engines passed; spectrum is " "quantum-number-consistent and the parameter-free relations hold.") print("=" * 72) return 0 failed = [r.name for r in results if not r.ok] print(f"OVERALL: FAIL -- {len(failed)} engine(s) failed: {', '.join(failed)}") print("(fail-closed: see the per-engine output above and out/*_report.md)") print("=" * 72) return 1 if __name__ == "__main__": sys.exit(main())