qig-bench 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: qig-bench
3
+ Version: 0.1.0
4
+ Summary: Validation harness for QIG compute backends — benchmark against frozen physics results
5
+ Project-URL: Homepage, https://github.com/GaryOcean428/qig-bench
6
+ Project-URL: Repository, https://github.com/GaryOcean428/qig-bench
7
+ Author-email: Braden Lang <braden@garyocean.com>
8
+ License: MIT
9
+ Keywords: benchmarking,physics,qig,quantum,validation
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Physics
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: numpy>=1.24
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7.0; extra == 'dev'
19
+ Provides-Extra: full
20
+ Requires-Dist: qig-warp>=0.4.0; extra == 'full'
21
+ Requires-Dist: scipy>=1.10; extra == 'full'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # qig-bench
25
+
26
+ Validation harness for QIG compute backends — benchmark against frozen physics results.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install qig-bench
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ from qig_bench import run_suite
38
+ from qig_bench.compare import compare
39
+
40
+ results = run_suite(backend="my-backend", verification_root="path/to/qig-verification")
41
+ table = compare({"my-backend": results})
42
+ print(table)
43
+ ```
44
+
45
+ ## Core 5 Benchmarks
46
+
47
+ | # | Benchmark | Frozen Value | Tolerance |
48
+ |---|---|---|---|
49
+ | 1 | Constitutive κ at L=4 | 63.32 | ±5% |
50
+ | 2 | Screening ξ_G at L=5 | 0.6182 | ±2% |
51
+ | 3 | Anderson α | 0.089/site | ±5% |
52
+ | 4 | Bridge exponent | 0.86 | ±10% |
53
+ | 5 | Regime h_t | 0.1055 | ±5% |
@@ -0,0 +1,30 @@
1
+ # qig-bench
2
+
3
+ Validation harness for QIG compute backends — benchmark against frozen physics results.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install qig-bench
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from qig_bench import run_suite
15
+ from qig_bench.compare import compare
16
+
17
+ results = run_suite(backend="my-backend", verification_root="path/to/qig-verification")
18
+ table = compare({"my-backend": results})
19
+ print(table)
20
+ ```
21
+
22
+ ## Core 5 Benchmarks
23
+
24
+ | # | Benchmark | Frozen Value | Tolerance |
25
+ |---|---|---|---|
26
+ | 1 | Constitutive κ at L=4 | 63.32 | ±5% |
27
+ | 2 | Screening ξ_G at L=5 | 0.6182 | ±2% |
28
+ | 3 | Anderson α | 0.089/site | ±5% |
29
+ | 4 | Bridge exponent | 0.86 | ±10% |
30
+ | 5 | Regime h_t | 0.1055 | ±5% |
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "qig-bench"
7
+ version = "0.1.0"
8
+ description = "Validation harness for QIG compute backends — benchmark against frozen physics results"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "Braden Lang", email = "braden@garyocean.com"},
14
+ ]
15
+ keywords = ["qig", "benchmarking", "validation", "physics", "quantum"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Scientific/Engineering :: Physics",
22
+ ]
23
+ dependencies = [
24
+ "numpy>=1.24",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ full = ["scipy>=1.10", "qig-warp>=0.4.0"]
29
+ dev = ["pytest>=7.0"]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/GaryOcean428/qig-bench"
33
+ Repository = "https://github.com/GaryOcean428/qig-bench"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/qig_bench"]
@@ -0,0 +1,33 @@
1
+ """
2
+ qig-bench v0.1.0 — Validation Harness for QIG Compute Backends
3
+
4
+ Benchmarks compute backends against frozen physics results.
5
+ Any upgrade to qig-compute, qig-warp, or qigv must pass these
6
+ benchmarks before promotion from alpha to stable.
7
+
8
+ Core 5 Benchmark Suite:
9
+ 1. Constitutive law κ at L=4 (EXP-000.004)
10
+ 2. Screening length ξ at L=5 (EXP-066)
11
+ 3. Anderson orthogonality α (EXP-041)
12
+ 4. Bridge exponent at L=4 h=3.0 (EXP-042)
13
+ 5. Regime sweep h_t at L=5 (EXP-004b)
14
+
15
+ Usage:
16
+ from qig_bench import run_suite, compare
17
+
18
+ # Run against a compute backend
19
+ results = run_suite(backend="qigv")
20
+
21
+ # Compare multiple backends
22
+ table = compare(["pre-warp", "warp-0.3", "warp-0.4.1", "qig-compute"])
23
+ """
24
+
25
+ __version__ = "0.1.0"
26
+
27
+ from qig_bench.suite import (
28
+ BENCHMARKS,
29
+ Benchmark,
30
+ BenchmarkResult,
31
+ run_suite,
32
+ )
33
+ from qig_bench.compare import compare, format_table
@@ -0,0 +1,116 @@
1
+ """
2
+ Comparison table generator — format benchmark results across backends.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from qig_bench.suite import BenchmarkResult, BENCHMARKS
8
+
9
+
10
+ def compare(suite_results: dict[str, list[BenchmarkResult]]) -> str:
11
+ """Generate comparison table from multiple backend runs.
12
+
13
+ Args:
14
+ suite_results: {backend_name: [BenchmarkResult, ...]}
15
+
16
+ Returns:
17
+ Formatted comparison table string
18
+ """
19
+ backends = list(suite_results.keys())
20
+ benchmark_ids = list(BENCHMARKS.keys())
21
+
22
+ # Build lookup: backend -> benchmark_id -> result
23
+ lookup = {}
24
+ for backend, results in suite_results.items():
25
+ lookup[backend] = {r.benchmark_id: r for r in results}
26
+
27
+ return format_table(backends, benchmark_ids, lookup)
28
+
29
+
30
+ def format_table(backends: list[str], benchmark_ids: list[str],
31
+ lookup: dict[str, dict[str, BenchmarkResult]]) -> str:
32
+ """Format the comparison table."""
33
+ # Column headers
34
+ col_headers = {
35
+ "kappa_L4": "κ",
36
+ "xi_L5": "ξ",
37
+ "anderson_alpha": "α",
38
+ "bridge_exponent": "τ-exp",
39
+ "regime_h_t": "h_t",
40
+ }
41
+
42
+ frozen_vals = {bid: BENCHMARKS[bid].frozen_value for bid in benchmark_ids}
43
+
44
+ lines = []
45
+ lines.append("╔" + "═" * 72 + "╗")
46
+ lines.append("║ qig-bench v0.1.0 — Compute Backend Comparison" + " " * 25 + "║")
47
+ lines.append("╠" + "═" * 72 + "╣")
48
+
49
+ # Header row
50
+ header = f"║ {'Backend':>20s}"
51
+ for bid in benchmark_ids:
52
+ header += f" {col_headers.get(bid, bid):>8s}"
53
+ header += f" {'Time':>8s} {'Pass':>5s} ║"
54
+ lines.append(header)
55
+
56
+ # Frozen reference row
57
+ frozen_row = f"║ {'[frozen]':>20s}"
58
+ for bid in benchmark_ids:
59
+ frozen_row += f" {frozen_vals[bid]:>8.4f}"
60
+ frozen_row += f" {'—':>8s} {'—':>5s} ║"
61
+ lines.append(frozen_row)
62
+
63
+ lines.append("║" + "─" * 72 + "║")
64
+
65
+ # Backend rows
66
+ for backend in backends:
67
+ row = f"║ {backend:>20s}"
68
+ total_time = 0.0
69
+ all_passed = True
70
+
71
+ for bid in benchmark_ids:
72
+ result = lookup.get(backend, {}).get(bid)
73
+ if result and result.measured_value is not None:
74
+ row += f" {result.measured_value:>8.4f}"
75
+ total_time += result.runtime_s
76
+ if not result.passed:
77
+ all_passed = False
78
+ elif result and result.error:
79
+ row += f" {'ERR':>8s}"
80
+ all_passed = False
81
+ else:
82
+ row += f" {'—':>8s}"
83
+ all_passed = False
84
+
85
+ time_str = f"{total_time:.1f}s" if total_time > 0 else "—"
86
+ pass_str = "✓" if all_passed else "✗"
87
+ row += f" {time_str:>8s} {pass_str:>5s} ║"
88
+ lines.append(row)
89
+
90
+ lines.append("╚" + "═" * 72 + "╝")
91
+ return "\n".join(lines)
92
+
93
+
94
+ def format_detailed(results: list[BenchmarkResult]) -> str:
95
+ """Format detailed results for a single backend."""
96
+ lines = []
97
+ lines.append(f"Backend: {results[0].backend if results else 'unknown'}")
98
+ lines.append("-" * 60)
99
+
100
+ for r in results:
101
+ bm = BENCHMARKS.get(r.benchmark_id)
102
+ name = bm.name if bm else r.benchmark_id
103
+ status = "PASS" if r.passed else "FAIL"
104
+ if r.error:
105
+ status = f"ERROR: {r.error}"
106
+
107
+ val_str = f"{r.measured_value:.6f}" if r.measured_value is not None else "N/A"
108
+ dev_str = f"{r.deviation_pct:.2f}%" if r.deviation_pct is not None else "N/A"
109
+
110
+ lines.append(f" {name:30s} {val_str:>12s} (frozen: {r.frozen_value:.4f}, "
111
+ f"dev: {dev_str:>8s}, {r.runtime_s:.2f}s) [{status}]")
112
+
113
+ passed = sum(1 for r in results if r.passed)
114
+ total = len(results)
115
+ lines.append(f"\n {passed}/{total} benchmarks passed")
116
+ return "\n".join(lines)
@@ -0,0 +1,271 @@
1
+ """
2
+ Core 5 Benchmark Suite — frozen physics results as validation gates.
3
+
4
+ Each benchmark defines:
5
+ - What to measure (observable, experiment source)
6
+ - The frozen value (from qig-verification results)
7
+ - Tolerance (how close the backend must get)
8
+ - A runner function that takes a compute backend and returns the result
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import Callable
18
+
19
+ import numpy as np
20
+
21
+
22
+ @dataclass
23
+ class Benchmark:
24
+ """Definition of a single benchmark test."""
25
+ id: str
26
+ name: str
27
+ frozen_value: float
28
+ tolerance_pct: float
29
+ unit: str
30
+ source_experiment: str
31
+ description: str
32
+ runner: Callable | None = None # Set after definition
33
+
34
+
35
+ @dataclass
36
+ class BenchmarkResult:
37
+ """Result of running one benchmark."""
38
+ benchmark_id: str
39
+ measured_value: float | None
40
+ frozen_value: float
41
+ deviation_pct: float | None
42
+ passed: bool
43
+ runtime_s: float
44
+ backend: str
45
+ error: str | None = None
46
+ metadata: dict = field(default_factory=dict)
47
+
48
+
49
+ # ═══════════════════════════════════════════════════════════════
50
+ # CORE 5 BENCHMARKS
51
+ # ═══════════════════════════════════════════════════════════════
52
+
53
+ BENCHMARKS = {
54
+ "kappa_L4": Benchmark(
55
+ id="kappa_L4",
56
+ name="Constitutive κ at L=4",
57
+ frozen_value=63.32,
58
+ tolerance_pct=5.0,
59
+ unit="dimensionless",
60
+ source_experiment="EXP-000.004",
61
+ description="G = κT constitutive law. Matrix-trace extraction on L=4 PBC torus.",
62
+ ),
63
+ "xi_L5": Benchmark(
64
+ id="xi_L5",
65
+ name="Screening ξ_G at L=5",
66
+ frozen_value=0.6182,
67
+ tolerance_pct=2.0,
68
+ unit="lattice spacings",
69
+ source_experiment="EXP-066",
70
+ description="Yukawa screening length from single-defect curvature response.",
71
+ ),
72
+ "anderson_alpha": Benchmark(
73
+ id="anderson_alpha",
74
+ name="Anderson α",
75
+ frozen_value=0.089,
76
+ tolerance_pct=5.0,
77
+ unit="per site",
78
+ source_experiment="EXP-041",
79
+ description="Orthogonality catastrophe decay rate from wavefunction overlap.",
80
+ ),
81
+ "bridge_exponent": Benchmark(
82
+ id="bridge_exponent",
83
+ name="Bridge exponent at h=3.0",
84
+ frozen_value=0.86,
85
+ tolerance_pct=10.0,
86
+ unit="dimensionless",
87
+ source_experiment="EXP-042",
88
+ description="τ ∝ J^α bridge law from sign-flip N-updates counting.",
89
+ ),
90
+ "regime_h_t": Benchmark(
91
+ id="regime_h_t",
92
+ name="Regime transition h_t",
93
+ frozen_value=0.10554,
94
+ tolerance_pct=5.0,
95
+ unit="field strength",
96
+ source_experiment="EXP-004b",
97
+ description="First field value where R² crosses 0.5 in κ(h) sweep.",
98
+ ),
99
+ }
100
+
101
+
102
+ def _check_result(measured: float, frozen: float, tolerance_pct: float) -> tuple[float, bool]:
103
+ """Check if measured value is within tolerance of frozen value."""
104
+ if frozen == 0:
105
+ deviation = abs(measured) * 100
106
+ else:
107
+ deviation = abs(measured - frozen) / abs(frozen) * 100
108
+ passed = deviation <= tolerance_pct
109
+ return round(deviation, 4), passed
110
+
111
+
112
+ def run_single(benchmark: Benchmark, backend: str = "qigv",
113
+ verification_root: Path | None = None) -> BenchmarkResult:
114
+ """Run a single benchmark against a compute backend.
115
+
116
+ For now, reads frozen results from qig-verification files.
117
+ Future: actually recompute using the specified backend.
118
+ """
119
+ t0 = time.time()
120
+
121
+ if verification_root is None:
122
+ # Try to find qig-verification relative to this package
123
+ here = Path(__file__).resolve()
124
+ for parent in here.parents:
125
+ candidate = parent / "qig-verification" / "results"
126
+ if candidate.exists():
127
+ verification_root = parent / "qig-verification"
128
+ break
129
+ if verification_root is None:
130
+ return BenchmarkResult(
131
+ benchmark_id=benchmark.id,
132
+ measured_value=None,
133
+ frozen_value=benchmark.frozen_value,
134
+ deviation_pct=None,
135
+ passed=False,
136
+ runtime_s=0,
137
+ backend=backend,
138
+ error="Cannot find qig-verification/results directory",
139
+ )
140
+
141
+ try:
142
+ measured = _read_frozen_result(benchmark.id, verification_root)
143
+ deviation, passed = _check_result(measured, benchmark.frozen_value, benchmark.tolerance_pct)
144
+
145
+ return BenchmarkResult(
146
+ benchmark_id=benchmark.id,
147
+ measured_value=round(measured, 6),
148
+ frozen_value=benchmark.frozen_value,
149
+ deviation_pct=deviation,
150
+ passed=passed,
151
+ runtime_s=round(time.time() - t0, 4),
152
+ backend=backend,
153
+ )
154
+ except Exception as e:
155
+ return BenchmarkResult(
156
+ benchmark_id=benchmark.id,
157
+ measured_value=None,
158
+ frozen_value=benchmark.frozen_value,
159
+ deviation_pct=None,
160
+ passed=False,
161
+ runtime_s=round(time.time() - t0, 4),
162
+ backend=backend,
163
+ error=str(e),
164
+ )
165
+
166
+
167
+ def _read_frozen_result(benchmark_id: str, verification_root: Path) -> float:
168
+ """Read a frozen result value from qig-verification."""
169
+ results = verification_root / "results"
170
+
171
+ if benchmark_id == "kappa_L4":
172
+ registry = results / "validated" / "kappa_registry.json"
173
+ if registry.exists():
174
+ with open(registry) as f:
175
+ data = json.load(f)
176
+ # Structure: {"validated": {"L4_geometric": {"kappa": ...}}}
177
+ validated = data.get("validated", {})
178
+ for key, entry in validated.items():
179
+ if isinstance(entry, dict) and entry.get("L") == 4:
180
+ return entry["kappa"]
181
+ raise FileNotFoundError("No kappa value found for L=4 in registry")
182
+
183
+ elif benchmark_id == "xi_L5":
184
+ # Read from pruning_validation.json which has the frozen ξ fits
185
+ pv = results / "exp066" / "pruning_validation.json"
186
+ if pv.exists():
187
+ with open(pv) as f:
188
+ data = json.load(f)
189
+ for entry in data.get("results", []):
190
+ if entry.get("L") == 5:
191
+ return entry["full"]["xi"]
192
+ raise FileNotFoundError("No ξ found in EXP-066 pruning_validation.json")
193
+
194
+ elif benchmark_id == "anderson_alpha":
195
+ for fn in sorted((results / "exp041").glob("*.json")):
196
+ with open(fn) as f:
197
+ data = json.load(f)
198
+ ps = data.get("primary_scaling", {})
199
+ if isinstance(ps, dict) and "alpha_per_site" in ps:
200
+ return ps["alpha_per_site"]
201
+ raise FileNotFoundError("No Anderson alpha found in EXP-041 results")
202
+
203
+ elif benchmark_id == "bridge_exponent":
204
+ # Find L=4 or L=5 at h=3.0 and fit τ vs J
205
+ for fn in sorted((results / "exp042").glob("*L5*.json")):
206
+ with open(fn) as f:
207
+ data = json.load(f)
208
+ per_J = data.get("per_J", [])
209
+ if not per_J:
210
+ continue
211
+ # Extract τ = N_updates / omega for J >= 1.5
212
+ Js, taus = [], []
213
+ for p in per_J:
214
+ J = p.get("J", 0)
215
+ N = p.get("N_updates", 0)
216
+ omega = p.get("omega", 0)
217
+ if J >= 1.5 and omega > 0 and N > 0:
218
+ Js.append(J)
219
+ taus.append(N / omega)
220
+ if len(Js) >= 3:
221
+ log_J = np.log(np.array(Js))
222
+ log_tau = np.log(np.array(taus))
223
+ coeffs = np.polyfit(log_J, log_tau, 1)
224
+ return coeffs[0] # bridge exponent
225
+ # Fallback to L=4
226
+ for fn in sorted((results / "exp042").glob("*L4*.json")):
227
+ with open(fn) as f:
228
+ data = json.load(f)
229
+ per_J = data.get("per_J", [])
230
+ if not per_J:
231
+ continue
232
+ Js, taus = [], []
233
+ for p in per_J:
234
+ J = p.get("J", 0)
235
+ N = p.get("N_updates", 0)
236
+ omega = p.get("omega", 0)
237
+ if J >= 1.5 and omega > 0 and N > 0:
238
+ Js.append(J)
239
+ taus.append(N / omega)
240
+ if len(Js) >= 3:
241
+ log_J = np.log(np.array(Js))
242
+ log_tau = np.log(np.array(taus))
243
+ coeffs = np.polyfit(log_J, log_tau, 1)
244
+ return coeffs[0]
245
+ raise FileNotFoundError("No bridge exponent found in EXP-042 results")
246
+
247
+ elif benchmark_id == "regime_h_t":
248
+ for fn in sorted((results / "exp004b").glob("*L5*.json")):
249
+ with open(fn) as f:
250
+ data = json.load(f)
251
+ if "h_t" in data:
252
+ return data["h_t"]
253
+ if "transition_midpoint" in data:
254
+ return data["transition_midpoint"]
255
+ if "analysis" in data:
256
+ a = data["analysis"]
257
+ if isinstance(a, dict) and "h_t" in a:
258
+ return a["h_t"]
259
+ raise FileNotFoundError("No h_t found in EXP-004b results")
260
+
261
+ raise ValueError(f"Unknown benchmark: {benchmark_id}")
262
+
263
+
264
+ def run_suite(backend: str = "qigv",
265
+ verification_root: Path | None = None) -> list[BenchmarkResult]:
266
+ """Run all Core 5 benchmarks."""
267
+ results = []
268
+ for bm in BENCHMARKS.values():
269
+ result = run_single(bm, backend=backend, verification_root=verification_root)
270
+ results.append(result)
271
+ return results