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.
- physmap/__init__.py +61 -0
- physmap/_paths.py +69 -0
- physmap/applicability/__init__.py +0 -0
- physmap/applicability/fixtures.py +83 -0
- physmap/applicability/screen.py +99 -0
- physmap/baselines/__init__.py +0 -0
- physmap/benchmarks/__init__.py +0 -0
- physmap/benchmarks/benchmark_report.py +405 -0
- physmap/benchmarks/benchmark_v0_4.py +424 -0
- physmap/benchmarks/compare.py +149 -0
- physmap/benchmarks/registry.py +217 -0
- physmap/benchmarks/report.py +224 -0
- physmap/cli.py +301 -0
- physmap/closures/__init__.py +48 -0
- physmap/closures/data/__init__.py +7 -0
- physmap/closures/data/closure_index.json +2997 -0
- physmap/closures/formulas.py +213 -0
- physmap/closures/geometry_classes.py +109 -0
- physmap/closures/index.py +393 -0
- physmap/closures/registry.py +313 -0
- physmap/compat/__init__.py +0 -0
- physmap/core/__init__.py +0 -0
- physmap/core/mechanism.py +69 -0
- physmap/core/signals.py +50 -0
- physmap/corpus/__init__.py +12 -0
- physmap/corpus/calibration.py +543 -0
- physmap/corpus/data/__init__.py +12 -0
- physmap/corpus/data/corpus_seed.jsonl +15 -0
- physmap/corpus/data/evidence_claims_seed.jsonl +21 -0
- physmap/corpus/data/evidence_sources_seed.jsonl +8 -0
- physmap/corpus/data/premium_coverage.json +60 -0
- physmap/corpus/evidence.py +871 -0
- physmap/explain/__init__.py +0 -0
- physmap/explain/benchmark.py +101 -0
- physmap/explain/causal.py +82 -0
- physmap/guardrail/__init__.py +38 -0
- physmap/guardrail/aggregator_observability.py +187 -0
- physmap/guardrail/classify.py +147 -0
- physmap/guardrail/configs.py +120 -0
- physmap/guardrail/corpus_regimes.py +208 -0
- physmap/guardrail/detector_conformal.py +129 -0
- physmap/guardrail/detector_density.py +74 -0
- physmap/guardrail/enums.py +69 -0
- physmap/guardrail/graph.py +73 -0
- physmap/guardrail/guardrail.py +606 -0
- physmap/guardrail/io.py +201 -0
- physmap/guardrail/regime_observability.py +519 -0
- physmap/guardrail/render.py +159 -0
- physmap/guardrail/weighting_heuristic.py +216 -0
- physmap/infra/__init__.py +23 -0
- physmap/infra/blindspot_oracle.py +356 -0
- physmap/infra/corpus_runtime.py +275 -0
- physmap/integrations/__init__.py +0 -0
- physmap/materiality/__init__.py +0 -0
- physmap/materiality/estimator.py +239 -0
- physmap/materiality/independence.py +92 -0
- physmap/materiality/surrogate_fit.py +293 -0
- physmap/observability/__init__.py +0 -0
- physmap/pipeline/__init__.py +58 -0
- physmap/pipeline/aggregators.py +199 -0
- physmap/pipeline/assessment_v06.py +509 -0
- physmap/pipeline/core.py +442 -0
- physmap/pipeline/defeasible_aggregator.py +324 -0
- physmap/pipeline/detectors.py +309 -0
- physmap/pipeline/observability.py +430 -0
- physmap/pipeline/surrogate.py +251 -0
- physmap/pipeline/validity_signal.py +273 -0
- physmap/pipeline/vehicle_spec.py +287 -0
- physmap/release.py +81 -0
- physmap/stress_tests/__init__.py +9 -0
- physmap/stress_tests/lewis_reuse.py +517 -0
- physmap/substrate/__init__.py +28 -0
- physmap/substrate/corpus_real.py +206 -0
- physmap/substrate/engine.py +209 -0
- physmap/substrate/forrest.py +249 -0
- physmap/substrate/loaders.py +2176 -0
- physmap/substrate/naca_tn1451.py +379 -0
- physmap/substrate/naca_wpd_loader.py +187 -0
- physmap/substrate/stage1_ingest.py +187 -0
- physmap/substrate/vehicle_config.py +407 -0
- physmap-0.2.0.dist-info/METADATA +270 -0
- physmap-0.2.0.dist-info/RECORD +88 -0
- physmap-0.2.0.dist-info/WHEEL +5 -0
- physmap-0.2.0.dist-info/entry_points.txt +2 -0
- physmap-0.2.0.dist-info/licenses/LICENSE +21 -0
- physmap-0.2.0.dist-info/licenses/LICENSE-CORPUS +469 -0
- physmap-0.2.0.dist-info/licenses/NOTICE +77 -0
- 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
|
+
|