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,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())