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,405 @@
|
|
|
1
|
+
"""PhysMAP Benchmark v0.4 — derived artifacts, regenerated from the banked JSON.
|
|
2
|
+
|
|
3
|
+
Three views of the ONE banked source (`results/benchmark_v0_4/matrix.json` +
|
|
4
|
+
`architecture_axis.json`): the two-claims findings doc, the observability-axis
|
|
5
|
+
figure, and the results CSV. None re-runs the benchmark or recomputes an outcome;
|
|
6
|
+
none has a per-vehicle branch (cells are iterated uniformly, styled/emitted by the
|
|
7
|
+
declarative fields). So all three can only ever AGREE with the banked matrix — and
|
|
8
|
+
a newly-registered vehicle appears in each automatically.
|
|
9
|
+
|
|
10
|
+
CLI: `python -m physmap.benchmarks.benchmark_report` (or `physmap benchmark report`).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import csv as _csv
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
RESULTS = Path(__file__).resolve().parent.parent / "results" / "benchmark_v0_4"
|
|
19
|
+
FINDINGS = (Path(__file__).resolve().parent.parent.parent / "docs" / "findings"
|
|
20
|
+
/ "PhysMAP_Benchmark_v0_4_Findings_v0_1.md")
|
|
21
|
+
|
|
22
|
+
# A cell is "caveated" (provisional/thin) if its declared caveat flags it — used to
|
|
23
|
+
# mark such cells distinctly in the figure so the honest scope is visible IN the plot.
|
|
24
|
+
_CAVEAT_FLAGS = ("provisional", "thin", "small-n", "small n", "strict")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load(name: str) -> dict | None:
|
|
28
|
+
p = RESULTS / name
|
|
29
|
+
return json.loads(p.read_text()) if p.exists() else None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cells(matrix: dict) -> list[dict]:
|
|
33
|
+
return sorted(matrix.get("cells", []), key=lambda c: (c.get("domain", ""), c["vehicle_id"]))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_caveated(cell: dict) -> bool:
|
|
37
|
+
cav = (cell.get("caveat") or "").lower()
|
|
38
|
+
return any(f in cav for f in _CAVEAT_FLAGS)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _obs_label(cell: dict) -> str:
|
|
42
|
+
cls = cell.get("failure_observability") or "?"
|
|
43
|
+
score = cell.get("observability_score")
|
|
44
|
+
src = cell.get("observability_source", "")
|
|
45
|
+
s = f"{score:.2f}" if isinstance(score, (int, float)) else "—"
|
|
46
|
+
tag = "" if src == "measured" else " (pole)"
|
|
47
|
+
return f"{cls} ({s}{tag})"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _clean_rate(cell: dict) -> float:
|
|
51
|
+
"""Normalized corpus advantage: clean lift ÷ surrogate deploy failures (n_wrong) — the FRACTION
|
|
52
|
+
of the surrogate's deploy failures the corpus uniquely catches while the steelman baseline stays
|
|
53
|
+
quiet. In [0,1], comparable across vehicles (n_wrong does NOT vary with the detector operating-pct
|
|
54
|
+
— it depends only on prediction-vs-truth — so clean_lift_max / n_wrong_max is well-defined). 0.0
|
|
55
|
+
when there are no deploy failures (DO_NO_HARM: no advantage to have). This is the y-axis: a rate,
|
|
56
|
+
so the figure means what it looks like (no raw-count 'read position not height' caveat)."""
|
|
57
|
+
ct = cell.get("clean_lift_max", 0) or 0
|
|
58
|
+
nw = cell.get("n_wrong_max", 0) or 0
|
|
59
|
+
return (ct / nw) if nw else 0.0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── B1: findings generator (template framing + generated data sections) ───────
|
|
63
|
+
|
|
64
|
+
_HEADER = """# PhysMAP Benchmark v0.4 — Findings (the benchmark of record)
|
|
65
|
+
|
|
66
|
+
> **Generated** by `physmap.benchmarks.benchmark_report` from the banked
|
|
67
|
+
> `results/benchmark_v0_4/{{matrix,architecture_axis}}.json` — do not hand-edit the
|
|
68
|
+
> data sections; re-run `physmap benchmark report` after a benchmark run.
|
|
69
|
+
|
|
70
|
+
Spec: [`specs/PhysMAP_Benchmark_Spec_v0_4.md`](../specs/PhysMAP_Benchmark_Spec_v0_4.md);
|
|
71
|
+
repeatable-runner spec:
|
|
72
|
+
[`specs/PhysMAP_Benchmark_YAMLDriven_Repeatable_Runner_Spec_v0_1.md`](../specs/PhysMAP_Benchmark_YAMLDriven_Repeatable_Runner_Spec_v0_1.md).
|
|
73
|
+
Artifacts: [`results/benchmark_v0_4/`](../../physmap/results/benchmark_v0_4/)
|
|
74
|
+
(`matrix.json`, `architecture_axis.json`, `benchmark_results.csv`, the figure, and
|
|
75
|
+
`gpvar_floor_investigation.md`).
|
|
76
|
+
|
|
77
|
+
**Binding methodology (v0.4):** every cell is produced by the SHIPPED public API
|
|
78
|
+
`physmap.guardrail.CredibilityGuardrail` (construct → fit → assess,
|
|
79
|
+
observability-weighted), the DETECTOR the only swapped variable. Per-cell decisions
|
|
80
|
+
are read from the RAW `Assessment.signals[...]` fire (verified detector-independent);
|
|
81
|
+
the Pareto lift (corpus fires & baselines quiet & surrogate WRONG) is computed on
|
|
82
|
+
that API output. The vehicle set is the registry: every `physmap/vehicles/*.yaml`
|
|
83
|
+
with a `benchmark` block — adding one is the only change needed for a new cell.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
_METHODOLOGY = """## Methodology findings
|
|
87
|
+
|
|
88
|
+
- **Real-API reproduction (reproduce-or-explain).** Outcomes are computed from raw
|
|
89
|
+
signals; the registry-driven runner reproduces the banked outcomes exactly, and a
|
|
90
|
+
cell that diverges is investigated, not forced.
|
|
91
|
+
- **gp_variance dense-training floor (`GP_VARIANCE_REL_FLOOR`).** The
|
|
92
|
+
percentile-of-train-self gp_variance threshold collapses under dense training
|
|
93
|
+
(a GP interpolating near-duplicate inputs → ~0 self-variance → tau ~7e-4), firing
|
|
94
|
+
on benign sub-1% predictive variance. Confirmed per-row on two cases (Casper 1.06%;
|
|
95
|
+
NACA cross-validated 0.05–0.72%) and floored — the [0.02, 0.20] invariant band is
|
|
96
|
+
the evidence it is principled, not tuned-to-win. Full record:
|
|
97
|
+
[`results/benchmark_v0_4/gpvar_floor_investigation.md`](../../physmap/results/benchmark_v0_4/gpvar_floor_investigation.md).
|
|
98
|
+
- **Single-detector-verdict guard.** Detector raw fires are detector-independent;
|
|
99
|
+
the corpus-only verdict is honest REJECT on the unobservable axis and masked to
|
|
100
|
+
TRUSTWORTHY (do-no-harm) on the observable axis — so the matrix reads raw signals,
|
|
101
|
+
not the aggregated verdict.
|
|
102
|
+
- **Observability guard.** Each vehicle's declared `expected_observability_class` is
|
|
103
|
+
asserted against the computed class; a mismatch fails loudly (no silent banking).
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
_SUPPORTED = """## Supported / not supported
|
|
107
|
+
|
|
108
|
+
**Supported:** the cross-domain mechanism (both domains); discrimination (win where
|
|
109
|
+
baseline-blind, do-no-harm at the observable pole, decline at the baseline-visible
|
|
110
|
+
negative control); model-agnosticism where ≥2 architectures train and are guarded
|
|
111
|
+
identically.
|
|
112
|
+
|
|
113
|
+
**Not supported (do not claim):** full observability-spectrum coverage
|
|
114
|
+
(vehicle-limited); production-scale field-surrogate fidelity (the field /
|
|
115
|
+
neural-operator demonstration is deferred to a field-shaped vehicle); Casper as a
|
|
116
|
+
fresh-blind-holdout (it is construction-frozen-a-priori, small-n); dirker as a
|
|
117
|
+
stability-confirmed middle.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
_NEXT = """## Named next hardening steps
|
|
121
|
+
|
|
122
|
+
1. **Casper data request** — fresh quiet rms-vs-x conditions (more quiet points, more
|
|
123
|
+
than one Re) to convert the construction-frozen-a-priori win into a fresh-blind-holdout.
|
|
124
|
+
2. **Field-shaped vehicle** — for the neural-operator (DeepONet / NVIDIA PhysicsNeMo)
|
|
125
|
+
demonstration the scalar vehicles cannot support (out of scope for this matrix).
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _matrix_table(cells: list[dict]) -> str:
|
|
130
|
+
rows = ["| Vehicle | Domain | Observability | Outcome | Evidence |",
|
|
131
|
+
"|---|---|---|---|---|"]
|
|
132
|
+
for c in cells:
|
|
133
|
+
ev = []
|
|
134
|
+
if c.get("clean_lift_max") is not None:
|
|
135
|
+
ev.append(f"clean lift {c['clean_lift_max']}")
|
|
136
|
+
if c.get("n_test") is not None:
|
|
137
|
+
ev.append(f"n={c['n_test']}")
|
|
138
|
+
if _is_caveated(c):
|
|
139
|
+
ev.append("⚠ caveat")
|
|
140
|
+
rows.append(f"| **{c['vehicle_id']}** | {c.get('domain','')} | {_obs_label(c)} "
|
|
141
|
+
f"| {c['empirical_outcome']} | {'; '.join(ev)} |")
|
|
142
|
+
return "\n".join(rows)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _cross_domain_claim(cells: list[dict]) -> str:
|
|
146
|
+
wins = {c["domain"] for c in cells if c["empirical_outcome"] == "PHYSMAP_WINS"}
|
|
147
|
+
dnh = [c["vehicle_id"] for c in cells if c["empirical_outcome"] == "DO_NO_HARM"]
|
|
148
|
+
neg = [c["vehicle_id"] for c in cells if c["empirical_outcome"] == "BASELINE_VISIBLE"]
|
|
149
|
+
win_vehicles = [c["vehicle_id"] for c in cells if c["empirical_outcome"] == "PHYSMAP_WINS"]
|
|
150
|
+
partials = [c["vehicle_id"] for c in cells if c["empirical_outcome"] == "PARTIAL"]
|
|
151
|
+
wins_prose = " and ".join(sorted(wins))
|
|
152
|
+
status = ("DEMONSTRATED in BOTH domains" if len(wins) >= 2
|
|
153
|
+
else f"shown in {wins_prose}" if wins else "NOT shown")
|
|
154
|
+
return (
|
|
155
|
+
f"**1. Cross-domain (multiphysics) — {status}.** A confirmed PHYSMAP_WINS in "
|
|
156
|
+
f"{wins_prose} ({', '.join(win_vehicles)}), the partial-observability middle(s) "
|
|
157
|
+
f"({', '.join(partials) or 'none'}), the do-no-harm pole ({', '.join(dnh) or 'none'}), "
|
|
158
|
+
f"and the baseline-visible negative control ({', '.join(neg) or 'none'}) — the same "
|
|
159
|
+
f"mechanism (corpus catches a baseline-invisible failure, does no harm where the "
|
|
160
|
+
f"baseline suffices, declines where the baseline already sees it) across domains."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _model_agnosticism_claim(arch: dict | None) -> str:
|
|
165
|
+
if not arch:
|
|
166
|
+
return ("**2. Model-agnosticism — not run** (no architecture-axis artifact; flag "
|
|
167
|
+
"vehicles with `benchmark.architecture_axis: true` and run the axis).")
|
|
168
|
+
shown, not_testable = [], []
|
|
169
|
+
for vid, a in arch.get("agreement", {}).items():
|
|
170
|
+
if a.get("all_show_corpus_lift"):
|
|
171
|
+
shown.append(f"{vid} ({', '.join(a.get('trainable_architectures', []))})")
|
|
172
|
+
if a.get("not_trainable"):
|
|
173
|
+
not_testable.append(f"{vid} ({', '.join(a['not_trainable'])} NOT_TRAINABLE at n)")
|
|
174
|
+
return (
|
|
175
|
+
f"**2. Model-agnosticism — shown where demonstrated, not-testable elsewhere.** "
|
|
176
|
+
f"PhysMAP guards GP and structurally-different surrogates identically (detector "
|
|
177
|
+
f"fires are prediction-independent; only is-WRONG varies). Shown on: "
|
|
178
|
+
f"{'; '.join(shown) or 'none'}. Not testable on: {'; '.join(not_testable) or 'none'} "
|
|
179
|
+
f"(honest exclusion — no theater cells; never averaged into the claim)."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _caveat_block(cells: list[dict]) -> str:
|
|
184
|
+
lines = ["## Honest caveats (per-cell, carried from the YAML — credibility, not weakness)", ""]
|
|
185
|
+
for c in cells:
|
|
186
|
+
if c.get("caveat"):
|
|
187
|
+
lines.append(f"- **{c['vehicle_id']}**: {c['caveat']}")
|
|
188
|
+
return "\n".join(lines)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def generate_findings(matrix: dict | None = None, arch: dict | None = None,
|
|
192
|
+
write: bool = True) -> str:
|
|
193
|
+
matrix = matrix or _load("matrix.json")
|
|
194
|
+
if matrix is None:
|
|
195
|
+
raise FileNotFoundError(f"no banked matrix.json under {RESULTS}; run the benchmark first")
|
|
196
|
+
arch = arch if arch is not None else _load("architecture_axis.json")
|
|
197
|
+
cells = _cells(matrix)
|
|
198
|
+
doc = "\n".join([
|
|
199
|
+
_HEADER,
|
|
200
|
+
"## The two claims (kept separate)\n",
|
|
201
|
+
_cross_domain_claim(cells), "",
|
|
202
|
+
_model_agnosticism_claim(arch), "",
|
|
203
|
+
"## Cross-domain matrix (real API, registry-driven)\n",
|
|
204
|
+
_matrix_table(cells), "",
|
|
205
|
+
_METHODOLOGY,
|
|
206
|
+
_caveat_block(cells), "",
|
|
207
|
+
_SUPPORTED,
|
|
208
|
+
_NEXT,
|
|
209
|
+
])
|
|
210
|
+
if write:
|
|
211
|
+
FINDINGS.parent.mkdir(parents=True, exist_ok=True)
|
|
212
|
+
FINDINGS.write_text(doc)
|
|
213
|
+
return doc
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ── B3: results CSV (the tabular twin) ────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
_CSV_COLUMNS = ["vehicle", "domain", "observability", "observability_class",
|
|
219
|
+
"surrogate_inputs", "failure_driver", "baseline_fired", "corpus_fired",
|
|
220
|
+
"clean_lift", "n_wrong", "clean_lift_rate", "misaligned", "outcome", "n", "caveat"]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def generate_csv(matrix: dict | None = None, arch: dict | None = None,
|
|
224
|
+
write: bool = True) -> dict:
|
|
225
|
+
matrix = matrix or _load("matrix.json")
|
|
226
|
+
if matrix is None:
|
|
227
|
+
raise FileNotFoundError(f"no banked matrix.json under {RESULTS}")
|
|
228
|
+
arch = arch if arch is not None else _load("architecture_axis.json")
|
|
229
|
+
|
|
230
|
+
main_rows = []
|
|
231
|
+
for c in _cells(matrix):
|
|
232
|
+
main_rows.append({
|
|
233
|
+
"vehicle": c["vehicle_id"], "domain": c.get("domain", ""),
|
|
234
|
+
"observability": c.get("observability_score", ""),
|
|
235
|
+
"observability_class": c.get("failure_observability", ""),
|
|
236
|
+
"surrogate_inputs": "|".join(c.get("surrogate_inputs", [])),
|
|
237
|
+
"failure_driver": c.get("failure_var", ""),
|
|
238
|
+
"baseline_fired": c.get("ref_n_baseline_fired", ""),
|
|
239
|
+
"corpus_fired": c.get("ref_n_corpus_fired", ""),
|
|
240
|
+
"clean_lift": c.get("clean_lift_max", ""),
|
|
241
|
+
"n_wrong": c.get("n_wrong_max", ""),
|
|
242
|
+
"clean_lift_rate": round(_clean_rate(c), 3),
|
|
243
|
+
"misaligned": c.get("misaligned_min", ""),
|
|
244
|
+
"outcome": c["empirical_outcome"], "n": c.get("n_test", ""),
|
|
245
|
+
"caveat": c.get("caveat", ""),
|
|
246
|
+
})
|
|
247
|
+
paths = {}
|
|
248
|
+
if write:
|
|
249
|
+
RESULTS.mkdir(parents=True, exist_ok=True)
|
|
250
|
+
p = RESULTS / "benchmark_results.csv"
|
|
251
|
+
with p.open("w", newline="") as fh:
|
|
252
|
+
w = _csv.DictWriter(fh, fieldnames=_CSV_COLUMNS)
|
|
253
|
+
w.writeheader(); w.writerows(main_rows)
|
|
254
|
+
paths["matrix_csv"] = str(p)
|
|
255
|
+
|
|
256
|
+
# Architecture axis: one explicit row per vehicle×architecture — NOT_TRAINABLE is a
|
|
257
|
+
# row VALUE, never a dropped row (the no-theater discipline holds in the CSV too).
|
|
258
|
+
if arch:
|
|
259
|
+
arch_rows = []
|
|
260
|
+
for c in arch.get("cells", []):
|
|
261
|
+
g = c.get("gate1", {})
|
|
262
|
+
arch_rows.append({
|
|
263
|
+
"vehicle": c["vehicle_id"], "architecture": c["architecture"],
|
|
264
|
+
"outcome": c["outcome"], "trainable": g.get("trainable", ""),
|
|
265
|
+
"gate1_median_err_pct": g.get("median_err_pct", ""),
|
|
266
|
+
"clean_lift": c.get("clean_lift_max", ""),
|
|
267
|
+
"n_wrong_in_deploy": c.get("gate2_n_wrong_in_deploy", ""),
|
|
268
|
+
})
|
|
269
|
+
if write and arch_rows:
|
|
270
|
+
p = RESULTS / "architecture_results.csv"
|
|
271
|
+
cols = ["vehicle", "architecture", "outcome", "trainable",
|
|
272
|
+
"gate1_median_err_pct", "clean_lift", "n_wrong_in_deploy"]
|
|
273
|
+
with p.open("w", newline="") as fh:
|
|
274
|
+
w = _csv.DictWriter(fh, fieldnames=cols)
|
|
275
|
+
w.writeheader(); w.writerows(arch_rows)
|
|
276
|
+
paths["arch_csv"] = str(p)
|
|
277
|
+
return paths
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ── B2: results figure (the observability axis — derived, never hand-drawn) ────
|
|
281
|
+
|
|
282
|
+
_DOMAIN_COLOR = {"thermal-fluids": "#0072B2", "aerospace": "#D55E00"} # colorblind-safe
|
|
283
|
+
_OUTCOME_MARKER = {"PHYSMAP_WINS": "*", "PARTIAL": "o", "DO_NO_HARM": "s",
|
|
284
|
+
"BASELINE_VISIBLE": "X", "NO_FAILURE": "v",
|
|
285
|
+
"BASELINE_CATCHES_NO_LIFT": "P"}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def generate_figure(matrix: dict | None = None, arch: dict | None = None,
|
|
289
|
+
write: bool = True) -> list[str]:
|
|
290
|
+
"""Primary figure: observability (x) vs corpus clean-lift (y), colored by domain,
|
|
291
|
+
marked by outcome, OPEN markers for caveated (provisional/thin) cells so the scope
|
|
292
|
+
is honest IN the figure. Best-effort: returns [] if matplotlib is unavailable."""
|
|
293
|
+
matrix = matrix or _load("matrix.json")
|
|
294
|
+
if matrix is None:
|
|
295
|
+
raise FileNotFoundError(f"no banked matrix.json under {RESULTS}")
|
|
296
|
+
arch = arch if arch is not None else _load("architecture_axis.json")
|
|
297
|
+
try:
|
|
298
|
+
import matplotlib
|
|
299
|
+
matplotlib.use("Agg")
|
|
300
|
+
import matplotlib.pyplot as plt
|
|
301
|
+
from matplotlib.lines import Line2D
|
|
302
|
+
except ImportError:
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
cells = _cells(matrix)
|
|
306
|
+
fig, ax = plt.subplots(figsize=(8.5, 5.5))
|
|
307
|
+
_placed: dict[tuple, int] = {} # stagger labels for coincident points (e.g. the poles)
|
|
308
|
+
for c in cells:
|
|
309
|
+
x = c.get("observability_score")
|
|
310
|
+
y = _clean_rate(c)
|
|
311
|
+
if not isinstance(x, (int, float)):
|
|
312
|
+
continue
|
|
313
|
+
outcome = c["empirical_outcome"]
|
|
314
|
+
color = _DOMAIN_COLOR.get(c.get("domain", ""), "gray")
|
|
315
|
+
marker = _OUTCOME_MARKER.get(outcome, "o")
|
|
316
|
+
caveated = _is_caveated(c)
|
|
317
|
+
_key = (round(float(x), 2), round(float(y), 2))
|
|
318
|
+
_n = _placed.get(_key, 0)
|
|
319
|
+
_placed[_key] = _n + 1
|
|
320
|
+
# De-overlap coincident markers (e.g. Forrest & Marineau both at the observable pole
|
|
321
|
+
# (1.0, 0)): nudge the 2nd+ point sharing an (x, y) by a small x offset toward the axis
|
|
322
|
+
# interior so both markers are visible; the staggered label names each. Purely visual.
|
|
323
|
+
xj = float(x) + (-0.035 * _n if float(x) >= 0.5 else 0.035 * _n)
|
|
324
|
+
ax.scatter([xj], [y], s=320, marker=marker,
|
|
325
|
+
facecolors=("none" if caveated else color),
|
|
326
|
+
edgecolors=color, linewidths=2.2, zorder=3)
|
|
327
|
+
ax.annotate(c["vehicle_id"].replace("_hypersonic_transition", "").replace("_", " "),
|
|
328
|
+
(xj, y), textcoords="offset points", xytext=(9, 5 + 17 * _n), fontsize=9)
|
|
329
|
+
# Coverage-gap finding: 0.8-0.95 is the GENUINELY empty high-observability band — clear of the
|
|
330
|
+
# property-ratio partials (dirker 0.49, velazquez 0.57) AND below the observable pole (1.0). Scoped
|
|
331
|
+
# to where "no vehicle" actually holds (NOT 0.6-0.8, which is merely sparsely sampled just above
|
|
332
|
+
# velazquez). Flagged as a named future-vehicle target. Drawn first (zorder 0); text sits in the
|
|
333
|
+
# empty band, clear of the velazquez point/label at 0.57.
|
|
334
|
+
ax.axvspan(0.80, 0.95, color="0.5", alpha=0.09, zorder=0)
|
|
335
|
+
ax.text(0.875, 0.50, "coverage gap\n0.8–0.95\n(no vehicle —\nfuture target)",
|
|
336
|
+
ha="center", va="center", fontsize=7.0, color="0.4", style="italic", zorder=1)
|
|
337
|
+
ax.set_xlim(-0.07, 1.07)
|
|
338
|
+
ax.set_ylim(-0.05, 1.12) # rate in [0,1] (+headroom for the top label)
|
|
339
|
+
ax.set_xlabel("observability (driver invisible to inputs ←——→ driver is an input)",
|
|
340
|
+
fontsize=11)
|
|
341
|
+
ax.set_ylabel("corpus clean-lift rate\n(fraction of deploy failures the corpus uniquely catches)",
|
|
342
|
+
fontsize=11)
|
|
343
|
+
ax.set_title("PhysMAP: corpus advantage tracks observability position", fontsize=13)
|
|
344
|
+
ax.grid(True, alpha=0.25)
|
|
345
|
+
dom_handles = [Line2D([0], [0], marker="o", color="w", markerfacecolor=col,
|
|
346
|
+
markersize=11, label=dom) for dom, col in _DOMAIN_COLOR.items()]
|
|
347
|
+
out_handles = [Line2D([0], [0], marker=m, color="0.3", linestyle="none",
|
|
348
|
+
markersize=11, label=o) for o, m in _OUTCOME_MARKER.items()
|
|
349
|
+
if any(c["empirical_outcome"] == o for c in cells)]
|
|
350
|
+
cav_handle = [Line2D([0], [0], marker="o", color="0.3", markerfacecolor="none",
|
|
351
|
+
markersize=11, linestyle="none", label="caveated (provisional/thin)")]
|
|
352
|
+
# Legend OUTSIDE the axes (right) so it never obscures a data point (e.g. the
|
|
353
|
+
# high-clean-lift middle near mid-axis).
|
|
354
|
+
ax.legend(handles=dom_handles + out_handles + cav_handle, fontsize=8,
|
|
355
|
+
loc="center left", bbox_to_anchor=(1.01, 0.5), framealpha=0.9)
|
|
356
|
+
n_total = sum(c.get("n_test", 0) or 0 for c in cells)
|
|
357
|
+
cap = ("Each point: a benchmark vehicle. x = observability (cv_r2_knn; structural poles at 0/1). "
|
|
358
|
+
"y = corpus clean-lift RATE = clean lift / surrogate deploy failures (n_wrong) — the fraction "
|
|
359
|
+
"of the surrogate's deploy failures the corpus uniquely catches while the steelman baseline "
|
|
360
|
+
"stays quiet, in [0,1] (0 = no failures / no advantage). Color = domain; marker = computed "
|
|
361
|
+
"outcome; open marker = caveated; coincident points nudged in x for visibility. Shaded band "
|
|
362
|
+
f"(0.8–0.95) = coverage gap: the high-observability region no vehicle yet probes. {len(cells)} vehicles, total "
|
|
363
|
+
f"deploy n={n_total}. Derived from matrix.json (raw clean-lift counts + n in benchmark_results.csv).")
|
|
364
|
+
fig.text(0.5, -0.02, cap, ha="center", va="top", fontsize=7.5, wrap=True)
|
|
365
|
+
|
|
366
|
+
out: list[str] = []
|
|
367
|
+
if write:
|
|
368
|
+
RESULTS.mkdir(parents=True, exist_ok=True)
|
|
369
|
+
for ext, dpi in (("svg", None), ("pdf", None), ("png", 220)):
|
|
370
|
+
p = RESULTS / f"benchmark_matrix.{ext}"
|
|
371
|
+
fig.savefig(p, dpi=dpi, bbox_inches="tight")
|
|
372
|
+
out.append(str(p))
|
|
373
|
+
plt.close(fig)
|
|
374
|
+
return out
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ── orchestration ─────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
def report_all() -> dict:
|
|
380
|
+
matrix = _load("matrix.json")
|
|
381
|
+
if matrix is None:
|
|
382
|
+
raise FileNotFoundError(
|
|
383
|
+
f"no banked matrix.json under {RESULTS} — run `physmap benchmark run` first")
|
|
384
|
+
arch = _load("architecture_axis.json")
|
|
385
|
+
generate_findings(matrix, arch, write=True)
|
|
386
|
+
csvs = generate_csv(matrix, arch, write=True)
|
|
387
|
+
figs = generate_figure(matrix, arch, write=True)
|
|
388
|
+
return {"findings": str(FINDINGS), "csv": csvs, "figures": figs}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def main(argv=None) -> int:
|
|
392
|
+
arts = report_all()
|
|
393
|
+
print("regenerated benchmark report artifacts:")
|
|
394
|
+
print(f" findings: {arts['findings']}")
|
|
395
|
+
for k, v in arts["csv"].items():
|
|
396
|
+
print(f" {k}: {v}")
|
|
397
|
+
for f in arts["figures"]:
|
|
398
|
+
print(f" figure: {f}")
|
|
399
|
+
if not arts["figures"]:
|
|
400
|
+
print(" (figure skipped — matplotlib not available; install the [experiment] extra)")
|
|
401
|
+
return 0
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
if __name__ == "__main__":
|
|
405
|
+
raise SystemExit(main())
|