physmap 0.2.0__py3-none-any.whl

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.
Files changed (88) hide show
  1. physmap/__init__.py +61 -0
  2. physmap/_paths.py +69 -0
  3. physmap/applicability/__init__.py +0 -0
  4. physmap/applicability/fixtures.py +83 -0
  5. physmap/applicability/screen.py +99 -0
  6. physmap/baselines/__init__.py +0 -0
  7. physmap/benchmarks/__init__.py +0 -0
  8. physmap/benchmarks/benchmark_report.py +405 -0
  9. physmap/benchmarks/benchmark_v0_4.py +424 -0
  10. physmap/benchmarks/compare.py +149 -0
  11. physmap/benchmarks/registry.py +217 -0
  12. physmap/benchmarks/report.py +224 -0
  13. physmap/cli.py +301 -0
  14. physmap/closures/__init__.py +48 -0
  15. physmap/closures/data/__init__.py +7 -0
  16. physmap/closures/data/closure_index.json +2997 -0
  17. physmap/closures/formulas.py +213 -0
  18. physmap/closures/geometry_classes.py +109 -0
  19. physmap/closures/index.py +393 -0
  20. physmap/closures/registry.py +313 -0
  21. physmap/compat/__init__.py +0 -0
  22. physmap/core/__init__.py +0 -0
  23. physmap/core/mechanism.py +69 -0
  24. physmap/core/signals.py +50 -0
  25. physmap/corpus/__init__.py +12 -0
  26. physmap/corpus/calibration.py +543 -0
  27. physmap/corpus/data/__init__.py +12 -0
  28. physmap/corpus/data/corpus_seed.jsonl +15 -0
  29. physmap/corpus/data/evidence_claims_seed.jsonl +21 -0
  30. physmap/corpus/data/evidence_sources_seed.jsonl +8 -0
  31. physmap/corpus/data/premium_coverage.json +60 -0
  32. physmap/corpus/evidence.py +871 -0
  33. physmap/explain/__init__.py +0 -0
  34. physmap/explain/benchmark.py +101 -0
  35. physmap/explain/causal.py +82 -0
  36. physmap/guardrail/__init__.py +38 -0
  37. physmap/guardrail/aggregator_observability.py +187 -0
  38. physmap/guardrail/classify.py +147 -0
  39. physmap/guardrail/configs.py +120 -0
  40. physmap/guardrail/corpus_regimes.py +208 -0
  41. physmap/guardrail/detector_conformal.py +129 -0
  42. physmap/guardrail/detector_density.py +74 -0
  43. physmap/guardrail/enums.py +69 -0
  44. physmap/guardrail/graph.py +73 -0
  45. physmap/guardrail/guardrail.py +606 -0
  46. physmap/guardrail/io.py +201 -0
  47. physmap/guardrail/regime_observability.py +519 -0
  48. physmap/guardrail/render.py +159 -0
  49. physmap/guardrail/weighting_heuristic.py +216 -0
  50. physmap/infra/__init__.py +23 -0
  51. physmap/infra/blindspot_oracle.py +356 -0
  52. physmap/infra/corpus_runtime.py +275 -0
  53. physmap/integrations/__init__.py +0 -0
  54. physmap/materiality/__init__.py +0 -0
  55. physmap/materiality/estimator.py +239 -0
  56. physmap/materiality/independence.py +92 -0
  57. physmap/materiality/surrogate_fit.py +293 -0
  58. physmap/observability/__init__.py +0 -0
  59. physmap/pipeline/__init__.py +58 -0
  60. physmap/pipeline/aggregators.py +199 -0
  61. physmap/pipeline/assessment_v06.py +509 -0
  62. physmap/pipeline/core.py +442 -0
  63. physmap/pipeline/defeasible_aggregator.py +324 -0
  64. physmap/pipeline/detectors.py +309 -0
  65. physmap/pipeline/observability.py +430 -0
  66. physmap/pipeline/surrogate.py +251 -0
  67. physmap/pipeline/validity_signal.py +273 -0
  68. physmap/pipeline/vehicle_spec.py +287 -0
  69. physmap/release.py +81 -0
  70. physmap/stress_tests/__init__.py +9 -0
  71. physmap/stress_tests/lewis_reuse.py +517 -0
  72. physmap/substrate/__init__.py +28 -0
  73. physmap/substrate/corpus_real.py +206 -0
  74. physmap/substrate/engine.py +209 -0
  75. physmap/substrate/forrest.py +249 -0
  76. physmap/substrate/loaders.py +2176 -0
  77. physmap/substrate/naca_tn1451.py +379 -0
  78. physmap/substrate/naca_wpd_loader.py +187 -0
  79. physmap/substrate/stage1_ingest.py +187 -0
  80. physmap/substrate/vehicle_config.py +407 -0
  81. physmap-0.2.0.dist-info/METADATA +270 -0
  82. physmap-0.2.0.dist-info/RECORD +88 -0
  83. physmap-0.2.0.dist-info/WHEEL +5 -0
  84. physmap-0.2.0.dist-info/entry_points.txt +2 -0
  85. physmap-0.2.0.dist-info/licenses/LICENSE +21 -0
  86. physmap-0.2.0.dist-info/licenses/LICENSE-CORPUS +469 -0
  87. physmap-0.2.0.dist-info/licenses/NOTICE +77 -0
  88. physmap-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,424 @@
1
+ """PhysMAP Benchmark v0.4 — the cross-domain matrix through the REAL library API.
2
+
3
+ Binding methodology requirement (spec v0.4): every cell is produced by the
4
+ shipped public API `physmap.guardrail.CredibilityGuardrail` (construct -> fit ->
5
+ assess), with the DETECTOR as the only swapped variable — NOT the gate_core /
6
+ AnyFired experiment harness. The baseline column is the library's statistical /
7
+ input-space detectors (distance + GP-variance); the PhysMAP column is the
8
+ corpus / closure-validity detector (ValidityRangeDistanceDetector); the ensemble
9
+ verdict comes from the shipped observability-weighted aggregator.
10
+
11
+ Per the verified design (a Plan-agent investigation ran the library and confirmed
12
+ it): the per-detector decisions are read from the RAW `Assessment.signals[...]`
13
+ .fired (always honest, detector-independent), and the observability-weighted
14
+ `verdict` is used only for the narrative. The Pareto lift (clean_lift = corpus
15
+ fires & baselines quiet & surrogate WRONG) is then computed exactly as
16
+ gate_core.pareto_verdict does, but on the public API's output.
17
+
18
+ This runner reproduces the already-landed outcomes (reproduce-or-explain):
19
+ NACA / Velazquez / Casper -> PHYSMAP_WINS (Casper & NACA UNOBSERVABLE pole;
20
+ Velazquez PARTIAL-calibrated middle), Forrest -> DO_NO_HARM, Marineau ->
21
+ NEGATIVE_CONTROL, dirker -> PARTIAL[provisional].
22
+
23
+ CLI: python -m physmap.benchmarks.benchmark_v0_4
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import csv as _csv
28
+ import json
29
+ import tempfile
30
+ from dataclasses import dataclass, field
31
+ from pathlib import Path
32
+
33
+ import numpy as np
34
+
35
+ from physmap.guardrail.guardrail import CredibilityGuardrail
36
+ from physmap.guardrail.classify import coord_to_meta_key, input_to_feature
37
+ from physmap.guardrail.configs import (
38
+ ClosureValidityDetectorConfig,
39
+ ColumnMap,
40
+ DistanceDetectorConfig,
41
+ GPVarianceDetectorConfig,
42
+ )
43
+ from physmap.guardrail.enums import DetectorKind, Observability, Regime, Verdict
44
+ from physmap.pipeline.observability import vehicle_observability
45
+ from physmap.pipeline.surrogate import ThresholdCalibration
46
+ from physmap.pipeline.vehicle_spec import vehicle_spec
47
+ from physmap.substrate.engine import build_substrate
48
+ from physmap.substrate.vehicle_config import (
49
+ _vehicles_dir,
50
+ load_named_vehicle,
51
+ load_vehicle_config,
52
+ )
53
+
54
+
55
+ OPERATING_PERCENTILES = (50, 75, 90, 95, 99, 100) # matches phase1_gate / vehicle_gate_sweep
56
+ MIN_TRAIN_FOR_DETECTORS = 3
57
+ REFERENCE_PCT = 99.0 # pct used for the "fires in region" narrative
58
+
59
+ # The detector set is FIXED at the steelman input-space baselines + the corpus
60
+ # detector. distance is explicitly included and novelty_density is excluded so the
61
+ # baseline matches gate_core's (distance, gp_variance) union (the banked baseline).
62
+ _DETECTORS = (
63
+ DistanceDetectorConfig(),
64
+ GPVarianceDetectorConfig(),
65
+ ClosureValidityDetectorConfig(),
66
+ )
67
+ _BASELINE_KINDS = (DetectorKind.DISTANCE_TO_TRAINING, DetectorKind.GP_VARIANCE)
68
+ _CORPUS_KIND = DetectorKind.CLOSURE_VALIDITY
69
+
70
+ RESULTS_DIR = Path(__file__).resolve().parent.parent / "results" / "benchmark_v0_4"
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class BenchSpec:
75
+ """Per-vehicle benchmark cell config, DERIVED from the vehicle YAML's `benchmark`
76
+ block (no hardcoded table).
77
+
78
+ `surrogate_inputs` are the SHORT data names the guardrail maps to baseline
79
+ features (Re->log10_Re, M->M, ...). `column_inputs` (= surrogate_inputs + the
80
+ failure driver, deduped) is the ColumnMap superset the closure-validity detector
81
+ reads. `expected_observability_class` is the a-priori INTENT (guarded against the
82
+ COMPUTED class); the cell OUTCOME is computed from raw signals, never declared.
83
+ """
84
+ vehicle_id: str
85
+ domain: str
86
+ regime: Regime
87
+ surrogate_inputs: tuple[str, ...]
88
+ column_inputs: tuple[str, ...]
89
+ calib: ThresholdCalibration
90
+ expected_observability_class: str
91
+ failure_driver: str
92
+ architecture_axis: bool = False
93
+ caveat: str = ""
94
+
95
+
96
+ def bench_spec_from_config(cfg) -> "BenchSpec | None":
97
+ """Build a BenchSpec from a vehicle's `benchmark` block, or None if the vehicle
98
+ has no block or opts out (`include != true`). The cell config is DERIVED, not
99
+ tabled: `column_inputs` = surrogate_inputs + the failure driver (deduped — a
100
+ driver that IS a surrogate input, e.g. Forrest's Re, adds nothing); calib from
101
+ the block's `threshold_calibration` or the library default."""
102
+ b = cfg.benchmark
103
+ if b is None or not b.include:
104
+ return None
105
+ surrogate_inputs = tuple(b.surrogate_inputs)
106
+ column_inputs = surrogate_inputs + (
107
+ (b.failure_driver,) if b.failure_driver not in surrogate_inputs else ())
108
+ calib = (
109
+ ThresholdCalibration(paper_uncertainty_pct=float(b.threshold_calibration[0]),
110
+ digitization_uncertainty_pct=float(b.threshold_calibration[1]))
111
+ if b.threshold_calibration is not None else ThresholdCalibration()
112
+ )
113
+ return BenchSpec(
114
+ vehicle_id=cfg.vehicle_id, domain=b.domain, regime=Regime[b.regime],
115
+ surrogate_inputs=surrogate_inputs, column_inputs=column_inputs, calib=calib,
116
+ expected_observability_class=b.expected_observability_class,
117
+ failure_driver=b.failure_driver, architecture_axis=b.architecture_axis,
118
+ caveat=b.caveat,
119
+ )
120
+
121
+
122
+ def discover_benchmark_vehicles() -> list[BenchSpec]:
123
+ """The ONLY source of the benchmark vehicle list: every `physmap/vehicles/*.yaml`
124
+ carrying a `benchmark: {include: true}` block. Re-scanned on each call, so adding
125
+ a YAML makes its cell appear on the next run with NO runner edit (and removing it
126
+ removes the cell). No hardcoded vehicle name lives in this module."""
127
+ specs: list[BenchSpec] = []
128
+ for path in sorted(_vehicles_dir().glob("*.yaml")):
129
+ spec = bench_spec_from_config(load_vehicle_config(path))
130
+ if spec is not None:
131
+ specs.append(spec)
132
+ return specs
133
+
134
+
135
+ # Import-time snapshot (convenience for tests / the architecture axis); `run_matrix`
136
+ # RE-discovers on each run so a newly-registered YAML appears without reimport.
137
+ VEHICLE_BENCH: tuple[BenchSpec, ...] = tuple(discover_benchmark_vehicles())
138
+
139
+
140
+ # ── CSV emit (the Path+ColumnMap branch carries truth; the ndarray path does not) ──
141
+
142
+ def _write_csv(rows, column_inputs: tuple[str, ...], path: Path) -> None:
143
+ """Write one CSV with the ColumnMap input columns + truth + prediction.
144
+
145
+ truth = Row.cfd_truth (measured), prediction = Row.surrogate_prediction (the
146
+ vehicle's native surrogate). solver_truth on the returned Assessment is only
147
+ populated on this Path branch (guardrail.py ndarray path hardcodes truth=None).
148
+ """
149
+ header = list(column_inputs) + ["truth", "prediction"]
150
+ with path.open("w", newline="") as fh:
151
+ w = _csv.writer(fh)
152
+ w.writerow(header)
153
+ for r in rows:
154
+ line = [r.meta[c] for c in column_inputs]
155
+ line += [r.cfd_truth, r.surrogate_prediction]
156
+ w.writerow(line)
157
+
158
+
159
+ # ── Pareto from Assessments (mirrors gate_core.pareto_verdict, on public API output) ──
160
+
161
+ def _pareto_at_pct(assessments, calib: ThresholdCalibration) -> dict:
162
+ """clean_lift / misaligned + per-detector fire counts from the public API's
163
+ Assessments. Per row: baseline = distance OR gp_variance raw .fired (counted
164
+ separately too, so a divergence is self-documenting); corpus = closure_validity
165
+ raw .fired; WRONG/ACCURATE from prediction vs truth and the calib lift/accuracy
166
+ thresholds (gate_core.pareto_verdict logic, verbatim)."""
167
+ clean_lift = misaligned = n_wrong = n_base = n_dist = n_gp = n_corpus = 0
168
+ verdicts: dict[str, int] = {}
169
+ for a in assessments:
170
+ d = a.signals.get(DetectorKind.DISTANCE_TO_TRAINING)
171
+ g = a.signals.get(DetectorKind.GP_VARIANCE)
172
+ dist_fired = bool(d.fired) if d is not None else False
173
+ gp_fired = bool(g.fired) if g is not None else False
174
+ base_fired = dist_fired or gp_fired
175
+ cv = a.signals.get(_CORPUS_KIND)
176
+ corpus_fired = bool(cv.fired) if cv is not None else False
177
+ pred, truth = a.surrogate_prediction, a.solver_truth
178
+ rel_pct = (abs(pred - truth) / abs(truth) * 100.0) if truth else 0.0
179
+ is_wrong = rel_pct > calib.lift_threshold_pct
180
+ is_accurate = rel_pct <= calib.accuracy_threshold_pct
181
+ n_wrong += int(is_wrong)
182
+ n_base += int(base_fired); n_dist += int(dist_fired); n_gp += int(gp_fired)
183
+ n_corpus += int(corpus_fired)
184
+ if corpus_fired and not base_fired and is_wrong:
185
+ clean_lift += 1
186
+ if corpus_fired and not base_fired and is_accurate:
187
+ misaligned += 1
188
+ verdicts[a.verdict.value] = verdicts.get(a.verdict.value, 0) + 1
189
+ return {
190
+ "clean_lift": clean_lift, "misaligned": misaligned, "n_wrong": n_wrong,
191
+ "n_baseline_fired": n_base, "n_distance_fired": n_dist,
192
+ "n_gp_var_fired": n_gp, "n_corpus_fired": n_corpus,
193
+ "n_test": len(assessments), "verdicts": verdicts,
194
+ }
195
+
196
+
197
+ # ── cell-outcome classification (reproduce-or-explain) ────────────────────────
198
+
199
+ def _classify(failure_obs, clean_lift_max: int, n_wrong_max: int) -> str:
200
+ """Empirical cell outcome, keyed on the failure coord's OBSERVABILITY-POSITION
201
+ (the spec's primary axis) + the Pareto clean-lift over the sweep:
202
+
203
+ * Gate-2 fail (surrogate accurate in deploy) -> NO_FAILURE
204
+ * OBSERVABLE failure axis (baseline-visible pole) -> BASELINE_VISIBLE
205
+ (the role — DO_NO_HARM vs NEGATIVE_CONTROL — is a-priori; both are
206
+ "baseline sees it", not a corpus win)
207
+ * UNOBSERVABLE/PARTIAL + clean_lift>0 (corpus catches wrong rows the FULL
208
+ steelman baseline misses, at some operating pct) -> PHYSMAP_WINS / PARTIAL
209
+ * UNOBSERVABLE/PARTIAL + NO clean_lift (surrogate wrong but the full
210
+ steelman baseline already fires on every wrong row) -> BASELINE_CATCHES_NO_LIFT
211
+ (the honest non-win: e.g. Casper's gp_variance catches the quiet cluster)
212
+ """
213
+ if n_wrong_max == 0:
214
+ return "NO_FAILURE"
215
+ if failure_obs is Observability.OBSERVABLE:
216
+ return "BASELINE_VISIBLE"
217
+ if clean_lift_max > 0:
218
+ return "PHYSMAP_WINS" if failure_obs is Observability.UNOBSERVABLE else "PARTIAL"
219
+ return "BASELINE_CATCHES_NO_LIFT"
220
+
221
+
222
+ def _observability_score(vehicle_id: str, failure_obs) -> tuple[float, str]:
223
+ """Numeric observability in [0,1] for the figure's x-axis. Prefer the MEASURED
224
+ cv_r2_knn estimator (`vehicle_observability`); fall back to the STRUCTURAL pole
225
+ from the class (UNOBSERVABLE->0.0, OBSERVABLE->1.0, PARTIAL->0.5) when the
226
+ estimator is unavailable/degenerate (e.g. a constant failure-var deploy region)."""
227
+ try:
228
+ score = float(vehicle_observability(vehicle_id).score)
229
+ if score == score: # not NaN
230
+ return score, "measured"
231
+ except Exception:
232
+ pass
233
+ pole = {Observability.UNOBSERVABLE: 0.0, Observability.OBSERVABLE: 1.0,
234
+ Observability.PARTIAL: 0.5}.get(failure_obs)
235
+ return (pole if pole is not None else float("nan")), "structural_pole"
236
+
237
+
238
+ def run_cell(spec: BenchSpec) -> dict:
239
+ """Run one vehicle through the real CredibilityGuardrail API over the
240
+ operating-pct sweep and return the cell record. The OUTCOME is computed from raw
241
+ signals; the YAML's `expected_observability_class` is an intent GUARD only."""
242
+ cfg = load_named_vehicle(spec.vehicle_id)
243
+ rows, _ref, _meta = build_substrate(cfg)
244
+ vspec = vehicle_spec(cfg)
245
+
246
+ # YAML<->substrate consistency (hard invariant: a mis-declared cell fails loudly
247
+ # rather than being silently benchmarked).
248
+ if spec.failure_driver != vspec.failure_var:
249
+ raise ValueError(
250
+ f"{spec.vehicle_id}: benchmark.failure_driver={spec.failure_driver!r} != "
251
+ f"vehicle_spec.failure_var={vspec.failure_var!r}")
252
+ feats = {input_to_feature(s) for s in spec.surrogate_inputs}
253
+ if not feats.issubset(set(vspec.baseline_feature_names)):
254
+ raise ValueError(
255
+ f"{spec.vehicle_id}: benchmark.surrogate_inputs map to features "
256
+ f"{sorted(feats)}, not a subset of vehicle_spec.baseline_feature_names "
257
+ f"{sorted(vspec.baseline_feature_names)}")
258
+
259
+ train = [r for r in rows if vspec.split.train_predicate(r.meta)]
260
+ test = [r for r in rows if vspec.split.test_predicate(r.meta)]
261
+
262
+ # Failure-coord observability (pct-independent; set at __init__, no fit needed).
263
+ probe = CredibilityGuardrail(
264
+ surrogate_inputs=list(spec.surrogate_inputs), regime=spec.regime,
265
+ detectors=_DETECTORS,
266
+ )
267
+ obs_map = probe.observability_classification
268
+ failure_coord = next(
269
+ (c for c in obs_map if coord_to_meta_key(c) == vspec.failure_var), None)
270
+ failure_obs = obs_map.get(failure_coord)
271
+ # Observability GUARD (spec Step 1): the computed class must match the declared
272
+ # intent; a mismatch is surfaced (run/CLI exits non-zero), never silently absorbed.
273
+ obs_guard = (failure_obs is not None
274
+ and failure_obs.name == spec.expected_observability_class)
275
+ obs_score, obs_source = _observability_score(spec.vehicle_id, failure_obs)
276
+
277
+ base = {
278
+ "vehicle_id": spec.vehicle_id, "domain": spec.domain,
279
+ "regime": spec.regime.value, "failure_var": vspec.failure_var,
280
+ "failure_observability": failure_obs.value if failure_obs else None,
281
+ "expected_observability_class": spec.expected_observability_class,
282
+ "observability_guard_passed": bool(obs_guard),
283
+ "observability_score": obs_score, "observability_source": obs_source,
284
+ "caveat": spec.caveat, "surrogate_inputs": list(spec.surrogate_inputs),
285
+ "n_train": len(train), "n_test": len(test),
286
+ "calib": {"lift_threshold_pct": spec.calib.lift_threshold_pct,
287
+ "accuracy_threshold_pct": spec.calib.accuracy_threshold_pct},
288
+ }
289
+ if not test:
290
+ return {**base, "empirical_outcome": "NO_DATA",
291
+ "rationale": "no failure-region (deploy) points"}
292
+ if len(train) < MIN_TRAIN_FOR_DETECTORS:
293
+ # Observable degenerate pole (Forrest): the failure axis IS a surrogate input,
294
+ # so the baseline is sufficient by construction — DO_NO_HARM. Too few
295
+ # in-distribution rows to fit detectors (thin n; matches degenerate_short_circuit).
296
+ if failure_obs is Observability.OBSERVABLE:
297
+ return {**base, "empirical_outcome": "DO_NO_HARM", "clean_lift_max": 0,
298
+ "rationale": (f"observable degenerate pole; only {len(train)} train "
299
+ f"rows (thin) — baseline-sufficient by construction, "
300
+ f"no detector fit (matches degenerate_short_circuit)")}
301
+ return {**base, "empirical_outcome": "INSUFFICIENT_TRAIN",
302
+ "rationale": f"{len(train)} train rows < {MIN_TRAIN_FOR_DETECTORS}"}
303
+
304
+ colmap = ColumnMap(inputs=list(spec.column_inputs), truth="truth", prediction="prediction")
305
+ with tempfile.TemporaryDirectory() as td:
306
+ tdp = Path(td)
307
+ train_csv, test_csv = tdp / "train.csv", tdp / "test.csv"
308
+ _write_csv(train, spec.column_inputs, train_csv)
309
+ _write_csv(test, spec.column_inputs, test_csv)
310
+ per_pct: dict[str, dict] = {}
311
+ ref_row = None
312
+ for pct in OPERATING_PERCENTILES:
313
+ guard = CredibilityGuardrail(
314
+ surrogate_inputs=list(spec.surrogate_inputs), regime=spec.regime,
315
+ detectors=_DETECTORS, operating_pct=float(pct),
316
+ )
317
+ guard.fit(train_csv, columns=colmap)
318
+ assessments = guard.assess(test_csv, columns=colmap)
319
+ row = _pareto_at_pct(assessments, spec.calib)
320
+ per_pct[str(pct)] = row
321
+ if float(pct) == REFERENCE_PCT:
322
+ ref_row = row
323
+
324
+ ref_row = ref_row or per_pct[str(OPERATING_PERCENTILES[-1])]
325
+ clean_lift_max = max(r["clean_lift"] for r in per_pct.values())
326
+ n_wrong_max = max(r["n_wrong"] for r in per_pct.values())
327
+ min_base = min(r["n_baseline_fired"] for r in per_pct.values())
328
+ empirical = _classify(failure_obs, clean_lift_max, n_wrong_max)
329
+ return {
330
+ **base,
331
+ "clean_lift_max": clean_lift_max,
332
+ "misaligned_min": min(r["misaligned"] for r in per_pct.values()),
333
+ "n_wrong_max": n_wrong_max,
334
+ "min_baseline_fired_over_sweep": min_base,
335
+ "ref_pct": REFERENCE_PCT,
336
+ "ref_n_baseline_fired": ref_row["n_baseline_fired"],
337
+ "ref_n_distance_fired": ref_row["n_distance_fired"],
338
+ "ref_n_gp_var_fired": ref_row["n_gp_var_fired"],
339
+ "ref_n_corpus_fired": ref_row["n_corpus_fired"],
340
+ "ref_verdicts": ref_row["verdicts"],
341
+ "empirical_outcome": empirical,
342
+ "per_pct": per_pct,
343
+ }
344
+
345
+
346
+ def run_matrix(write: bool = True) -> dict:
347
+ """Discover the benchmark vehicles from the registry (no hardcoded list) and run
348
+ each cell. `all_guards_passed` = every cell's computed observability class matched
349
+ its declared intent."""
350
+ cells = [run_cell(s) for s in discover_benchmark_vehicles()]
351
+ matrix = {
352
+ "benchmark": "PhysMAP Benchmark v0.4 (cross-domain, real-API, registry-driven)",
353
+ "api": "physmap.guardrail.CredibilityGuardrail (fit/assess, observability-weighted)",
354
+ "detectors": "baseline=(distance, gp_variance); corpus=closure_validity (raw signals read)",
355
+ "operating_percentiles": list(OPERATING_PERCENTILES),
356
+ "cells": cells,
357
+ "all_guards_passed": all(c.get("observability_guard_passed", False) for c in cells),
358
+ }
359
+ if write:
360
+ RESULTS_DIR.mkdir(parents=True, exist_ok=True)
361
+ out = RESULTS_DIR / "matrix.json"
362
+ out.write_text(json.dumps(matrix, indent=2))
363
+ matrix["_path"] = str(out)
364
+ return matrix
365
+
366
+
367
+ def load_banked_matrix() -> dict | None:
368
+ """The previously-banked matrix.json (the reproduce-or-explain baseline), or None."""
369
+ p = RESULTS_DIR / "matrix.json"
370
+ return json.loads(p.read_text()) if p.exists() else None
371
+
372
+
373
+ def reproduce_regressions(new_matrix: dict, banked: dict | None) -> list[str]:
374
+ """Vehicles present in BOTH whose computed `empirical_outcome` changed vs the
375
+ banked baseline — the reproduce-or-explain gate. A NEW vehicle (absent from the
376
+ baseline) is NOT a regression: it is reported and becomes the baseline once
377
+ reviewed."""
378
+ if not banked:
379
+ return []
380
+ prior = {c["vehicle_id"]: c.get("empirical_outcome") for c in banked.get("cells", [])}
381
+ return [
382
+ f"{c['vehicle_id']}: {prior[c['vehicle_id']]} -> {c.get('empirical_outcome')}"
383
+ for c in new_matrix.get("cells", [])
384
+ if c["vehicle_id"] in prior and c.get("empirical_outcome") != prior[c["vehicle_id"]]
385
+ ]
386
+
387
+
388
+ def _print_table(matrix: dict) -> None:
389
+ print(f"\n{matrix['benchmark']}")
390
+ print(f"API: {matrix['api']}")
391
+ hdr = (f"{'vehicle':<32}{'domain':<12}{'obs_class':<13}{'obs':>5}{'clean':>6}"
392
+ f"{'dist':>5}{'gpv':>5}{'corp':>5} {'empirical':<26}{'exp_obs':<13}{'guard':>6}")
393
+ print(hdr); print("-" * len(hdr))
394
+ for c in matrix["cells"]:
395
+ os_ = c.get("observability_score")
396
+ print(f"{c['vehicle_id']:<32}{c['domain']:<12}"
397
+ f"{str(c.get('failure_observability')):<13}"
398
+ f"{(f'{os_:.2f}' if isinstance(os_, (int, float)) else '-'):>5}"
399
+ f"{c.get('clean_lift_max', '-'):>6}"
400
+ f"{c.get('ref_n_distance_fired', '-'):>5}{c.get('ref_n_gp_var_fired', '-'):>5}"
401
+ f"{c.get('ref_n_corpus_fired', '-'):>5} "
402
+ f"{c['empirical_outcome']:<26}{c.get('expected_observability_class', ''):<13}"
403
+ f"{'Y' if c.get('observability_guard_passed') else 'N':>6}")
404
+ print(f"\nall observability guards passed: {matrix['all_guards_passed']}")
405
+ print("(obs = numeric observability; dist/gpv/corp = #deploy rows each detector "
406
+ "fires on at ref pct; clean = max clean Pareto lift over the sweep)")
407
+
408
+
409
+ def main(argv=None) -> int:
410
+ banked = load_banked_matrix()
411
+ matrix = run_matrix(write=True)
412
+ _print_table(matrix)
413
+ regressions = reproduce_regressions(matrix, banked)
414
+ if regressions:
415
+ print("\nREPRODUCE-OR-EXPLAIN REGRESSION (existing cells changed):")
416
+ for r in regressions:
417
+ print(f" {r}")
418
+ if matrix.get("_path"):
419
+ print(f"\nbanked: {matrix['_path']}")
420
+ return 0 if (matrix["all_guards_passed"] and not regressions) else 1
421
+
422
+
423
+ if __name__ == "__main__":
424
+ raise SystemExit(main())
@@ -0,0 +1,149 @@
1
+ """Compare a fresh benchmark run against the banked matrix.
2
+
3
+ Exact equality is the wrong test, and a clean-clone check proved it: on numpy 2.5 /
4
+ scikit-learn 1.9, `dirker_water.observability_score` came back 0.4912044133088568 where
5
+ the matrix banked 0.49120441330885667. That is a relative difference of 2.6e-16 -- one
6
+ unit in the last place, from a different BLAS reduction order. Nothing about the result
7
+ changed.
8
+
9
+ So floats are compared with a tolerance and everything else is compared exactly. The
10
+ split matters, because the fields that decide an outcome are not floats:
11
+
12
+ * outcomes, verdicts, observability classes -- strings, exact
13
+ * every count (n_train, n_test, n_wrong, clean_lift, fire counts) -- ints, exact
14
+ * scores and calibrated thresholds -- floats, within tolerance
15
+
16
+ A tolerance loose enough to absorb ULP noise (1e-9 relative) is still roughly seven
17
+ orders of magnitude tighter than any change that would mean something. And a match that
18
+ needed the tolerance is reported as such rather than as "identical", because those are
19
+ different statements.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Callable
25
+ from dataclasses import dataclass, field
26
+
27
+ __all__ = ["MatrixComparison", "compare_matrices", "compare_records", "DEFAULT_REL_TOL"]
28
+
29
+ #: Relative tolerance for float fields. Absorbs last-bit differences between numpy /
30
+ #: BLAS builds; far tighter than any numerically meaningful change.
31
+ DEFAULT_REL_TOL = 1e-9
32
+
33
+
34
+ @dataclass
35
+ class MatrixComparison:
36
+ #: Real differences: any non-float mismatch, or a float beyond tolerance.
37
+ drift: list[str] = field(default_factory=list)
38
+ #: Float fields that differ but are within tolerance. Not drift; worth reporting.
39
+ within_tolerance: list[str] = field(default_factory=list)
40
+ rel_tol: float = DEFAULT_REL_TOL
41
+ #: Optional per-field tolerance, `path -> (rel_tol, abs_tol)`, overriding `rel_tol`. For
42
+ #: records whose floats are not all alike -- a difference of two near-equal numbers needs
43
+ #: an absolute tolerance, where everything else is compared relatively.
44
+ tolerance: Callable[[str], tuple[float, float]] | None = None
45
+ #: How a summary describes `tolerance`.
46
+ tolerance_note: str = ""
47
+
48
+ @property
49
+ def matches(self) -> bool:
50
+ return not self.drift
51
+
52
+ @property
53
+ def bit_identical(self) -> bool:
54
+ return not self.drift and not self.within_tolerance
55
+
56
+ def summary(self) -> str:
57
+ if self.drift:
58
+ return f"DRIFT in {len(self.drift)} field(s)"
59
+ if self.within_tolerance:
60
+ if self.tolerance is not None:
61
+ return (
62
+ f"matches within {self.tolerance_note} "
63
+ f"({len(self.within_tolerance)} float field(s) differ, all by less than that)"
64
+ )
65
+ return (
66
+ f"matches within {self.rel_tol:g} relative "
67
+ f"({len(self.within_tolerance)} float field(s) differ in their last bits)"
68
+ )
69
+ return "bit-identical"
70
+
71
+ def drifting_vehicles(self) -> list[str]:
72
+ return sorted({d.split(".")[0] for d in self.drift})
73
+
74
+
75
+ def _close(a: float, b: float, rel_tol: float, abs_tol: float = 0.0) -> bool:
76
+ if a == b:
77
+ return True
78
+ scale = max(abs(a), abs(b))
79
+ return abs(a - b) <= max(rel_tol * scale, abs_tol)
80
+
81
+
82
+ def _walk(fresh, banked, path: str, cmp: MatrixComparison) -> None:
83
+ if isinstance(fresh, bool) or isinstance(banked, bool):
84
+ # bool is an int subclass; compare it exactly, never numerically
85
+ if fresh != banked:
86
+ cmp.drift.append(f"{path}: fresh={fresh!r} banked={banked!r}")
87
+ return
88
+ if isinstance(fresh, dict) and isinstance(banked, dict):
89
+ for k in sorted(set(fresh) | set(banked)):
90
+ if k not in fresh:
91
+ cmp.drift.append(f"{path}.{k}: missing from the fresh run")
92
+ elif k not in banked:
93
+ cmp.drift.append(f"{path}.{k}: missing from the banked matrix")
94
+ else:
95
+ _walk(fresh[k], banked[k], f"{path}.{k}", cmp)
96
+ return
97
+ if isinstance(fresh, list) and isinstance(banked, list):
98
+ if len(fresh) != len(banked):
99
+ cmp.drift.append(f"{path}: length {len(fresh)} vs {len(banked)}")
100
+ return
101
+ for i, (x, y) in enumerate(zip(fresh, banked)):
102
+ _walk(x, y, f"{path}[{i}]", cmp)
103
+ return
104
+ if isinstance(fresh, float) or isinstance(banked, float):
105
+ if not isinstance(fresh, (int, float)) or not isinstance(banked, (int, float)):
106
+ cmp.drift.append(f"{path}: fresh={fresh!r} banked={banked!r}")
107
+ elif fresh == banked:
108
+ return
109
+ elif _close(float(fresh), float(banked),
110
+ *(cmp.tolerance(path) if cmp.tolerance else (cmp.rel_tol, 0.0))):
111
+ cmp.within_tolerance.append(f"{path}: fresh={fresh!r} banked={banked!r}")
112
+ else:
113
+ cmp.drift.append(f"{path}: fresh={fresh!r} banked={banked!r}")
114
+ return
115
+ if fresh != banked:
116
+ cmp.drift.append(f"{path}: fresh={fresh!r} banked={banked!r}")
117
+
118
+
119
+ def compare_matrices(fresh: dict, banked: dict, *, rel_tol: float = DEFAULT_REL_TOL) -> MatrixComparison:
120
+ """Compare two matrices cell by cell and field by field."""
121
+ cmp = MatrixComparison(rel_tol=rel_tol)
122
+
123
+ f_cells = {c["vehicle_id"]: c for c in fresh.get("cells", [])}
124
+ b_cells = {c["vehicle_id"]: c for c in banked.get("cells", [])}
125
+ for vid in sorted(set(f_cells) | set(b_cells)):
126
+ if vid not in f_cells:
127
+ cmp.drift.append(f"{vid}: absent from the fresh run")
128
+ elif vid not in b_cells:
129
+ cmp.drift.append(f"{vid}: absent from the banked matrix")
130
+ else:
131
+ _walk(f_cells[vid], b_cells[vid], vid, cmp)
132
+
133
+ for key in ("benchmark", "api", "detectors", "operating_percentiles",
134
+ "all_guards_passed"):
135
+ _walk(fresh.get(key), banked.get(key), key, cmp)
136
+ return cmp
137
+
138
+
139
+ def compare_records(fresh: dict, banked: dict, *, rel_tol: float = DEFAULT_REL_TOL,
140
+ tolerance: Callable[[str], tuple[float, float]] | None = None,
141
+ tolerance_note: str = "") -> MatrixComparison:
142
+ """Compare any two nested records under the same rule as the matrix: floats within
143
+ tolerance -- `rel_tol`, or per field via `tolerance` -- and everything else exactly. For
144
+ banked results that are not a vehicle matrix -- the stress tests -- so they drift-check
145
+ the same way `benchmark run` does."""
146
+ cmp = MatrixComparison(rel_tol=rel_tol, tolerance=tolerance, tolerance_note=tolerance_note)
147
+ _walk(fresh, banked, "record", cmp)
148
+ return cmp
149
+