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,517 @@
|
|
|
1
|
+
"""Controlled model-reuse stress test: Lewis (1992) Test 35A.
|
|
2
|
+
|
|
3
|
+
WHAT IT IS
|
|
4
|
+
The surrogate was trained for forced convection, where gravity did not vary and was not an
|
|
5
|
+
input. It was then reused in vertical heated flow, where buoyancy became material. A
|
|
6
|
+
mixed-convection surrogate designed for this regime should include Richardson number, Grashof
|
|
7
|
+
number, or equivalent physical information.
|
|
8
|
+
|
|
9
|
+
THE CLAIM
|
|
10
|
+
PhysMAP detects when model reuse activates a physically relevant mechanism outside the
|
|
11
|
+
surrogate's observable input space. An input-only OOD detector cannot identify a change absent
|
|
12
|
+
from its input contract.
|
|
13
|
+
|
|
14
|
+
WHAT IT IS NOT
|
|
15
|
+
Not a claim that OOD detectors fail in general -- the input-based OOD detector does exactly
|
|
16
|
+
its job here. Not a suggestion that gravity should be left out: a mixed-convection surrogate
|
|
17
|
+
built correctly exposes the relevant physics. Not a claim about NVIDIA PhysicsNeMo, whose
|
|
18
|
+
out-of-distribution check and physics checks are distinct and were not run.
|
|
19
|
+
|
|
20
|
+
HOW
|
|
21
|
+
Design M (headline): the surrogate is trained on thirteen gravity-off CFD runs, one of them at
|
|
22
|
+
35A's own operating point, so every visible deployment input is an exact training input.
|
|
23
|
+
Design A3 (secondary): the same without that run, so 35A falls between training runs.
|
|
24
|
+
|
|
25
|
+
Both are scored twice with IDENTICAL visible inputs: against gravity-off CFD at 35A -- the
|
|
26
|
+
accurate control -- and against Lewis's measurement, where buoyancy is active. The OOD
|
|
27
|
+
detector is the seven-vehicle benchmark's own, imported unchanged. PhysMAP's output is the
|
|
28
|
+
causal path: the applicability screen, matched-ablation materiality and the flag rule.
|
|
29
|
+
|
|
30
|
+
STATUS
|
|
31
|
+
Development demonstration. ONE run, already inspected during development; its stations are
|
|
32
|
+
not independent cases; no performance metric is computed or implied. theta is unlocked, so
|
|
33
|
+
materiality is reported as continuous values and theta = 0.10 appears only as an illustration.
|
|
34
|
+
|
|
35
|
+
Reproduces cfd/lewis_head_to_head.py's pre-declared design M to the bit from the CFD-derived
|
|
36
|
+
inputs banked in data/stress_tests/lewis_reuse/.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import csv
|
|
42
|
+
import json
|
|
43
|
+
import math
|
|
44
|
+
import tempfile
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
import numpy as np
|
|
48
|
+
|
|
49
|
+
from physmap._paths import checkout_path
|
|
50
|
+
|
|
51
|
+
STRESS_TEST_ID = "lewis-reuse"
|
|
52
|
+
BANKED_RECORD = ("results", "lewis35A_head_to_head", "stress_test_lewis_reuse.json")
|
|
53
|
+
|
|
54
|
+
FRAMING = (
|
|
55
|
+
"The surrogate was trained for forced convection, where gravity did not vary and was not "
|
|
56
|
+
"an input. It was then reused in vertical heated flow, where buoyancy became material. A "
|
|
57
|
+
"mixed-convection surrogate designed for this regime should include Richardson number, "
|
|
58
|
+
"Grashof number, or equivalent physical information."
|
|
59
|
+
)
|
|
60
|
+
CLAIM = (
|
|
61
|
+
"PhysMAP detects when model reuse activates a physically relevant mechanism outside the "
|
|
62
|
+
"surrogate's observable input space. An input-only OOD detector cannot identify a change "
|
|
63
|
+
"absent from its input contract."
|
|
64
|
+
)
|
|
65
|
+
NOT_CLAIMED = (
|
|
66
|
+
"OOD detectors do not fail in general; this one does exactly its job.",
|
|
67
|
+
"Gravity should not be omitted: a mixed-convection surrogate built correctly exposes Ri, Gr "
|
|
68
|
+
"or equivalent physical information.",
|
|
69
|
+
"Nothing is claimed about NVIDIA PhysicsNeMo; its OOD and physics checks are distinct and "
|
|
70
|
+
"were not run.",
|
|
71
|
+
)
|
|
72
|
+
STATUS = (
|
|
73
|
+
"Development demonstration. One run (Lewis 35A), already inspected during development. "
|
|
74
|
+
"Its stations are not independent cases. No performance metric is computed or implied."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
SURROGATE_INPUTS = ("Re", "Pr", "x_over_D")
|
|
78
|
+
WITHHELD = ("gravity", "Ri", "Gr", "wall heat flux", "flow direction")
|
|
79
|
+
ILLUSTRATIVE_THETA = 0.10 # the original study's value, recorded before any Lewis work
|
|
80
|
+
# Lewis disowns three stations on his own authority, for reasons unrelated to buoyancy.
|
|
81
|
+
DISOWNED = {0.31: "axial wall conduction (Lewis Sec 7.1.1)",
|
|
82
|
+
0.85: "axial wall conduction (Lewis Sec 7.1.1)",
|
|
83
|
+
159.33: "'suspect' -- flange heat loss (Lewis Sec 7.1.1)"}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── inputs ───────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def _load_inputs() -> dict:
|
|
89
|
+
cfd = json.loads(checkout_path("data", "stress_tests", "lewis_reuse", "cfd_profiles.json",
|
|
90
|
+
what="the Lewis stress test's banked CFD profiles").read_text())
|
|
91
|
+
manifest = json.loads(checkout_path("data", "stress_tests", "lewis_reuse", "manifest.json",
|
|
92
|
+
what="the Lewis stress test's manifest").read_text())
|
|
93
|
+
lewis = json.loads(checkout_path("data", "lewis1992", "test_35A_reduction.json",
|
|
94
|
+
what="Lewis Test 35A reduction").read_text())
|
|
95
|
+
ib = lewis["dimensionless_by_basis"]["inlet_bulk"]
|
|
96
|
+
return {"cfd": cfd, "manifest": manifest,
|
|
97
|
+
"Re": ib["Re"], "Pr": ib["Pr"], "Gr_q": ib["Gr_q_heat_flux_based"],
|
|
98
|
+
"measured": [(p["x_over_d"], p["Nu"]) for p in lewis["local_nu_inlet_bulk_basis"]]}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _training_rows(inp: dict, design: str) -> list[dict]:
|
|
102
|
+
rows = []
|
|
103
|
+
for run in inp["cfd"]["training_runs"]:
|
|
104
|
+
for x, nu in zip(run["x_over_D"], run["Nu"]):
|
|
105
|
+
rows.append({"run": run["case"], "Re": run["inlet_bulk"]["Re"],
|
|
106
|
+
"Pr": run["inlet_bulk"]["Pr"], "x_over_D": x, "Nu": nu})
|
|
107
|
+
if design == "M":
|
|
108
|
+
m = inp["cfd"]["matched_training_run"]
|
|
109
|
+
# Labelled with the EVALUATED inputs, as pre-declared: its polynomial-evaluated inlet
|
|
110
|
+
# values differ by -0.062 % (Re) and +0.141 % (Pr), inside Lewis's own property spread.
|
|
111
|
+
for x, nu in zip(m["x_over_D"], m["Nu"]):
|
|
112
|
+
rows.append({"run": m["case"], "Re": inp["Re"], "Pr": inp["Pr"], "x_over_D": x,
|
|
113
|
+
"Nu": nu})
|
|
114
|
+
return rows
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ── the surrogate: a forced-convection GP on gravity-off CFD ─────────────────
|
|
118
|
+
|
|
119
|
+
def _X(rows):
|
|
120
|
+
return np.array([[math.log10(r["Re"]), r["Pr"], math.log10(r["x_over_D"])] for r in rows])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _fit_surrogate(rows):
|
|
124
|
+
from sklearn.gaussian_process import GaussianProcessRegressor
|
|
125
|
+
from sklearn.gaussian_process.kernels import ConstantKernel, Matern, WhiteKernel
|
|
126
|
+
X, y = _X(rows), np.log([r["Nu"] for r in rows])
|
|
127
|
+
mu, sd = X.mean(0), X.std(0)
|
|
128
|
+
kern = ConstantKernel(1.0) * Matern(length_scale=[1.0, 1.0, 1.0], nu=2.5) + WhiteKernel(1e-6)
|
|
129
|
+
gp = GaussianProcessRegressor(kernel=kern, normalize_y=True, n_restarts_optimizer=3,
|
|
130
|
+
random_state=0).fit((X - mu) / sd, y)
|
|
131
|
+
return lambda rs: np.exp(gp.predict((_X(rs) - mu) / sd))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ── the input-based OOD detector: the benchmark's own, unchanged ─────────────
|
|
135
|
+
|
|
136
|
+
def _write_csv(rows, path: Path, pred) -> None:
|
|
137
|
+
with path.open("w", newline="") as fh:
|
|
138
|
+
w = csv.writer(fh)
|
|
139
|
+
w.writerow(["Re", "Pr", "x_over_D", "truth", "prediction"])
|
|
140
|
+
for r, p in zip(rows, pred):
|
|
141
|
+
w.writerow([r["Re"], r["Pr"], r["x_over_D"], r["Nu"], float(p)])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _ood(train, train_pred, tests: dict) -> dict:
|
|
145
|
+
"""Fit the benchmark's detector tuple once per operating percentile, exactly as run_cell
|
|
146
|
+
does, and assess every test set with it. Each test set is written to its OWN file with its
|
|
147
|
+
own truth column, so an unchanged score also shows that truth never reached the detector."""
|
|
148
|
+
from physmap.benchmarks.benchmark_v0_4 import _DETECTORS, OPERATING_PERCENTILES
|
|
149
|
+
from physmap.guardrail.configs import ColumnMap
|
|
150
|
+
from physmap.guardrail.enums import DetectorKind, Regime
|
|
151
|
+
from physmap.guardrail.guardrail import CredibilityGuardrail
|
|
152
|
+
|
|
153
|
+
colmap = ColumnMap(inputs=list(SURROGATE_INPUTS), truth="truth", prediction="prediction")
|
|
154
|
+
out = {name: {} for name in tests}
|
|
155
|
+
with tempfile.TemporaryDirectory() as td:
|
|
156
|
+
tr = Path(td) / "train.csv"
|
|
157
|
+
_write_csv(train, tr, train_pred)
|
|
158
|
+
files = {}
|
|
159
|
+
for name, (rows, pred) in tests.items():
|
|
160
|
+
files[name] = Path(td) / f"{name}.csv"
|
|
161
|
+
_write_csv(rows, files[name], pred)
|
|
162
|
+
for pct in OPERATING_PERCENTILES:
|
|
163
|
+
# UNLISTED: the shipped corpus has no laminar vertical-tube closure, so the
|
|
164
|
+
# guardrail's closure layer is left inactive rather than fired on Re < 3000 by a
|
|
165
|
+
# turbulent closure for a reason unrelated to buoyancy.
|
|
166
|
+
g = CredibilityGuardrail(surrogate_inputs=list(SURROGATE_INPUTS),
|
|
167
|
+
regime=Regime.UNLISTED, detectors=_DETECTORS,
|
|
168
|
+
operating_pct=float(pct))
|
|
169
|
+
g.fit(tr, columns=colmap)
|
|
170
|
+
for name, f in files.items():
|
|
171
|
+
res = []
|
|
172
|
+
for a in g.assess(f, columns=colmap):
|
|
173
|
+
d = a.signals[DetectorKind.DISTANCE_TO_TRAINING]
|
|
174
|
+
v = a.signals[DetectorKind.GP_VARIANCE]
|
|
175
|
+
res.append({
|
|
176
|
+
"distance": {"score": float(d.score), "threshold": float(d.threshold),
|
|
177
|
+
"fired": bool(d.fired)},
|
|
178
|
+
"gp_variance": {"score": float(v.score), "threshold": float(v.threshold),
|
|
179
|
+
"fired": bool(v.fired)},
|
|
180
|
+
"fired": bool(d.fired or v.fired)})
|
|
181
|
+
out[name][str(pct)] = res
|
|
182
|
+
return out
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ── PhysMAP: the causal path ─────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
def _physmap(inp: dict, n: int) -> dict:
|
|
188
|
+
from physmap.applicability.screen import screen_case
|
|
189
|
+
from physmap.core.mechanism import CalibrationWindow, Mechanism
|
|
190
|
+
from physmap.materiality.estimator import (
|
|
191
|
+
AblationInputs, AblationProvenance, estimate_materiality, materiality_signal)
|
|
192
|
+
from physmap.release import EvidenceState
|
|
193
|
+
|
|
194
|
+
screen = screen_case("lewis-35A", "local_Nusselt_number", qoi_decomposes=True,
|
|
195
|
+
mechanisms_separable=True, has_calibration_window=True,
|
|
196
|
+
ablation_available=True, evidence_state=EvidenceState.MEASURED)
|
|
197
|
+
# The surrogate's training never saw gravity, so its calibrated window for buoyancy is
|
|
198
|
+
# Ri = 0 exactly. Derived from the training data, not chosen.
|
|
199
|
+
window = CalibrationWindow("richardson_number", 0.0, 0.0)
|
|
200
|
+
ri_on = inp["Gr_q"] / inp["Re"] ** 2
|
|
201
|
+
full = inp["cfd"]["ablation_full"]["Nu"]
|
|
202
|
+
removed = inp["cfd"]["ablation_removed"]["Nu"]
|
|
203
|
+
states = {}
|
|
204
|
+
for state, ri, pairs in (("gravity_on", ri_on, list(zip(full, removed))),
|
|
205
|
+
("gravity_off", 0.0, list(zip(removed, removed)))):
|
|
206
|
+
mech = Mechanism("buoyancy-vertical-pipe-aiding", "buoyancy (Richardson number)",
|
|
207
|
+
window, ri)
|
|
208
|
+
rows = []
|
|
209
|
+
for q_full, q_abl in pairs[:n]:
|
|
210
|
+
r = estimate_materiality(mech, "local_Nusselt_number",
|
|
211
|
+
AblationInputs(q_full, q_abl,
|
|
212
|
+
AblationProvenance.MATCHED_ABLATION),
|
|
213
|
+
evidence_state=EvidenceState.MEASURED)
|
|
214
|
+
rows.append({"materiality": r.value, "status": r.status.value,
|
|
215
|
+
"flag_at_illustrative_theta": materiality_signal(
|
|
216
|
+
mech, r, theta=ILLUSTRATIVE_THETA).fired})
|
|
217
|
+
states[state] = {"Ri": ri, "outside_calibration": mech.outside_calibration(),
|
|
218
|
+
"stations": rows}
|
|
219
|
+
return {"applicability": screen.applicability.value, "reason_code": screen.reason_code.value,
|
|
220
|
+
"window": window.describe(), "states": states}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ── one design, both gravity states ──────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
def _design(inp: dict, design: str) -> dict:
|
|
226
|
+
train = _training_rows(inp, design)
|
|
227
|
+
surrogate = _fit_surrogate(train)
|
|
228
|
+
xds = [x for x, _ in inp["measured"]]
|
|
229
|
+
deploy = [{"Re": inp["Re"], "Pr": inp["Pr"], "x_over_D": x} for x in xds]
|
|
230
|
+
truth = {"gravity_on": [nu for _, nu in inp["measured"]],
|
|
231
|
+
"gravity_off": inp["cfd"]["ablation_removed"]["Nu"]}
|
|
232
|
+
tests = {s: ([dict(d, Nu=t) for d, t in zip(deploy, truth[s])], None) for s in truth}
|
|
233
|
+
pred = surrogate(tests["gravity_on"][0])
|
|
234
|
+
tests = {s: (rows, pred) for s, (rows, _) in tests.items()}
|
|
235
|
+
ood = _ood(train, surrogate(train), tests)
|
|
236
|
+
|
|
237
|
+
seen = {(r["Re"], r["Pr"], r["x_over_D"]) for r in train}
|
|
238
|
+
overlap = [(d["Re"], d["Pr"], d["x_over_D"]) in seen for d in deploy]
|
|
239
|
+
return {"design": design, "training_runs": len({r["run"] for r in train}),
|
|
240
|
+
"training_rows": len(train),
|
|
241
|
+
"visible_inputs_exactly_in_training": overlap,
|
|
242
|
+
"surrogate_prediction": [float(p) for p in pred],
|
|
243
|
+
"truth": truth, "ood": ood}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ── the record ───────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
def run() -> dict:
|
|
249
|
+
"""Run the stress test and return its full record. Asserts nothing; `check` does."""
|
|
250
|
+
import warnings
|
|
251
|
+
from physmap.benchmarks.benchmark_v0_4 import OPERATING_PERCENTILES, REFERENCE_PCT
|
|
252
|
+
inp = _load_inputs()
|
|
253
|
+
xds = [x for x, _ in inp["measured"]]
|
|
254
|
+
comparable = [x not in DISOWNED for x in xds]
|
|
255
|
+
# scikit-learn's L-BFGS hyperparameter search warns when it stops at an iteration cap.
|
|
256
|
+
# The warning is silenced here and ONLY here, and the fit is not taken on trust for it:
|
|
257
|
+
# the control accuracy this record carries measures the surrogate against gravity-off
|
|
258
|
+
# CFD at every deployment station, and `check` fails the run if that ever degrades.
|
|
259
|
+
with warnings.catch_warnings():
|
|
260
|
+
warnings.filterwarnings("ignore", module="sklearn")
|
|
261
|
+
M, A3 = _design(inp, "M"), _design(inp, "A3")
|
|
262
|
+
pm = _physmap(inp, len(xds))
|
|
263
|
+
ref = str(int(REFERENCE_PCT))
|
|
264
|
+
on_mat = [s["materiality"] for s in pm["states"]["gravity_on"]["stations"]]
|
|
265
|
+
comp_mat = [m for m, c in zip(on_mat, comparable) if c]
|
|
266
|
+
|
|
267
|
+
def stations(d: dict) -> list[dict]:
|
|
268
|
+
out = []
|
|
269
|
+
for i, x in enumerate(xds):
|
|
270
|
+
p = d["surrogate_prediction"][i]
|
|
271
|
+
out.append({
|
|
272
|
+
"x_over_D": x, "comparable": comparable[i], "why_not": DISOWNED.get(x),
|
|
273
|
+
"surrogate_prediction": p,
|
|
274
|
+
"control_gravity_off_truth": d["truth"]["gravity_off"][i],
|
|
275
|
+
"control_error_pct": 100.0 * (p / d["truth"]["gravity_off"][i] - 1.0),
|
|
276
|
+
"experiment_Nu": d["truth"]["gravity_on"][i],
|
|
277
|
+
"experimental_error_pct": 100.0 * (p / d["truth"]["gravity_on"][i] - 1.0),
|
|
278
|
+
"ood_gravity_off": {pc: d["ood"]["gravity_off"][pc][i] for pc in d["ood"]["gravity_off"]},
|
|
279
|
+
"ood_gravity_on": {pc: d["ood"]["gravity_on"][pc][i] for pc in d["ood"]["gravity_on"]},
|
|
280
|
+
"materiality_gravity_off": pm["states"]["gravity_off"]["stations"][i]["materiality"],
|
|
281
|
+
"materiality_gravity_on": on_mat[i],
|
|
282
|
+
"physmap_flag_gravity_off_at_illustrative_theta":
|
|
283
|
+
pm["states"]["gravity_off"]["stations"][i]["flag_at_illustrative_theta"],
|
|
284
|
+
"physmap_flag_gravity_on_at_illustrative_theta":
|
|
285
|
+
pm["states"]["gravity_on"]["stations"][i]["flag_at_illustrative_theta"],
|
|
286
|
+
})
|
|
287
|
+
return out
|
|
288
|
+
|
|
289
|
+
def ood_summary(d: dict) -> dict:
|
|
290
|
+
per_pct = {}
|
|
291
|
+
for pc in d["ood"]["gravity_on"]:
|
|
292
|
+
per_pct[pc] = {s: sum(1 for i, c in enumerate(comparable)
|
|
293
|
+
if c and d["ood"][s][pc][i]["fired"])
|
|
294
|
+
for s in ("gravity_off", "gravity_on")}
|
|
295
|
+
return {"comparable_stations_fired_by_pct": per_pct,
|
|
296
|
+
"identical_between_gravity_states": d["ood"]["gravity_off"] == d["ood"]["gravity_on"]}
|
|
297
|
+
|
|
298
|
+
headline, secondary = stations(M), stations(A3)
|
|
299
|
+
return {
|
|
300
|
+
"stress_test": STRESS_TEST_ID,
|
|
301
|
+
"kind": "controlled model-reuse stress test",
|
|
302
|
+
"framing": FRAMING, "claim": CLAIM, "not_claimed": list(NOT_CLAIMED), "status": STATUS,
|
|
303
|
+
"input_contract": {
|
|
304
|
+
"surrogate_inputs": list(SURROGATE_INPUTS),
|
|
305
|
+
"ood_detector_inputs": list(SURROGATE_INPUTS),
|
|
306
|
+
"ood_detector_features": ["log10_Re", "Pr", "x_over_D"],
|
|
307
|
+
"withheld_from_both": list(WITHHELD),
|
|
308
|
+
"deployment_inputs": {"Re": inp["Re"], "Pr": inp["Pr"], "x_over_D": xds},
|
|
309
|
+
"design_M_every_visible_input_exactly_in_training":
|
|
310
|
+
all(M["visible_inputs_exactly_in_training"]),
|
|
311
|
+
"design_A3_visible_inputs_in_training": sum(A3["visible_inputs_exactly_in_training"]),
|
|
312
|
+
},
|
|
313
|
+
"matched_ablation": {**inp["manifest"]["matched_ablation"],
|
|
314
|
+
"full": {k: inp["cfd"]["ablation_full"][k] for k in
|
|
315
|
+
("case", "gravity_on", "iterations", "energy_closure_pct",
|
|
316
|
+
"final_initial_residuals",
|
|
317
|
+
"reversed_cells_in_heated_section")},
|
|
318
|
+
"removed": {k: inp["cfd"]["ablation_removed"][k] for k in
|
|
319
|
+
("case", "gravity_on", "iterations", "energy_closure_pct",
|
|
320
|
+
"final_initial_residuals",
|
|
321
|
+
"reversed_cells_in_heated_section")}},
|
|
322
|
+
"inputs_sha256": inp["manifest"]["cfd_profiles_sha256"],
|
|
323
|
+
"ood_detector": {"object": "physmap.benchmarks.benchmark_v0_4._DETECTORS, unchanged",
|
|
324
|
+
"operating_percentiles": list(OPERATING_PERCENTILES),
|
|
325
|
+
"reference_pct": REFERENCE_PCT},
|
|
326
|
+
"physmap": {"applicability": pm["applicability"], "reason_code": pm["reason_code"],
|
|
327
|
+
"calibration_window": pm["window"],
|
|
328
|
+
"Ri_gravity_on": pm["states"]["gravity_on"]["Ri"],
|
|
329
|
+
"outside_calibration": {s: pm["states"][s]["outside_calibration"]
|
|
330
|
+
for s in pm["states"]}},
|
|
331
|
+
"headline_design_M": {"training_runs": M["training_runs"],
|
|
332
|
+
"training_rows": M["training_rows"],
|
|
333
|
+
"ood": ood_summary(M), "stations": headline},
|
|
334
|
+
"secondary_design_A3": {"training_runs": A3["training_runs"],
|
|
335
|
+
"training_rows": A3["training_rows"],
|
|
336
|
+
"ood": ood_summary(A3), "stations": secondary},
|
|
337
|
+
"threshold_dependence": [
|
|
338
|
+
{"result": "OOD scores identical between gravity off and gravity on",
|
|
339
|
+
"threshold": "none", "depends_on_unlocked_threshold": False,
|
|
340
|
+
"note": "exact equality of every score"},
|
|
341
|
+
{"result": "every visible deployment input is a training input (design M)",
|
|
342
|
+
"threshold": "none", "depends_on_unlocked_threshold": False,
|
|
343
|
+
"note": "exact set membership"},
|
|
344
|
+
{"result": "materiality: 0 with gravity off, continuous values with gravity on",
|
|
345
|
+
"threshold": "none", "depends_on_unlocked_threshold": False,
|
|
346
|
+
"note": "reported as values"},
|
|
347
|
+
{"result": "control and experimental errors",
|
|
348
|
+
"threshold": "none", "depends_on_unlocked_threshold": False,
|
|
349
|
+
"note": "reported as values; no right/wrong label, the tolerance being unlocked"},
|
|
350
|
+
{"result": "OOD fired or quiet at a station",
|
|
351
|
+
"threshold": f"operating percentile -- shipped reference {REFERENCE_PCT:g}, fixed "
|
|
352
|
+
f"in code before this work",
|
|
353
|
+
"depends_on_unlocked_threshold": False,
|
|
354
|
+
"note": "the full sweep is reported; the fired/quiet pattern is identical between "
|
|
355
|
+
"gravity states at every percentile"},
|
|
356
|
+
{"result": "PhysMAP flags nothing with gravity off",
|
|
357
|
+
"threshold": "none", "depends_on_unlocked_threshold": False,
|
|
358
|
+
"note": "holds for every theta: materiality is 0 and Ri = 0 is inside the window"},
|
|
359
|
+
{"result": "PhysMAP flags at least one comparable station with gravity on",
|
|
360
|
+
"threshold": "theta -- UNLOCKED", "depends_on_unlocked_threshold": True,
|
|
361
|
+
"holds_for_theta_up_to": max(comp_mat)},
|
|
362
|
+
{"result": "which stations PhysMAP flags",
|
|
363
|
+
"threshold": "theta -- UNLOCKED", "depends_on_unlocked_threshold": True,
|
|
364
|
+
"note": f"a station flags for theta up to its own materiality; shown at "
|
|
365
|
+
f"theta = {ILLUSTRATIVE_THETA:.2f}, illustrative only"},
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def check(record: dict) -> list[str]:
|
|
371
|
+
"""The stress test's own assertions. Returns the failures; empty means all hold."""
|
|
372
|
+
fails = []
|
|
373
|
+
ic = record["input_contract"]
|
|
374
|
+
worst = max(abs(s["control_error_pct"]) for s in record["headline_design_M"]["stations"])
|
|
375
|
+
if worst > 0.5:
|
|
376
|
+
fails.append(f"design M: the surrogate misses the accurate gravity-off control by "
|
|
377
|
+
f"{worst:.2f} %, so it is no longer a trustworthy forced-convection model")
|
|
378
|
+
if not ic["design_M_every_visible_input_exactly_in_training"]:
|
|
379
|
+
fails.append("design M: a visible deployment input is not an exact training input")
|
|
380
|
+
if ic["surrogate_inputs"] != ic["ood_detector_inputs"]:
|
|
381
|
+
fails.append("the OOD detector does not receive exactly the surrogate's inputs")
|
|
382
|
+
for key in ("headline_design_M", "secondary_design_A3"):
|
|
383
|
+
if not record[key]["ood"]["identical_between_gravity_states"]:
|
|
384
|
+
fails.append(f"{key}: OOD scores changed between gravity off and gravity on")
|
|
385
|
+
for s in record["headline_design_M"]["stations"]:
|
|
386
|
+
if s["materiality_gravity_off"] != 0.0:
|
|
387
|
+
fails.append(f"x/D {s['x_over_D']}: materiality with gravity off is not zero")
|
|
388
|
+
return fails
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def load_banked() -> dict:
|
|
392
|
+
return json.loads(checkout_path(*BANKED_RECORD,
|
|
393
|
+
what="the banked Lewis stress-test record").read_text())
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
#: How closely a recomputed record must match the bank. The benchmark matrix uses 1e-9 relative,
|
|
397
|
+
#: which absorbs last-bit BLAS noise and nothing more. This test also re-optimises a
|
|
398
|
+
#: Gaussian-process surrogate's hyperparameters, and another numpy/BLAS build or CPU moves that
|
|
399
|
+
#: optimum a little. Measured against the bank (macOS x86-64, numpy 2.2): macOS arm64 with
|
|
400
|
+
#: numpy 2.4, Linux arm64 with numpy 2.5, and Linux x86-64 with the bank's own numpy 2.2 moved
|
|
401
|
+
#: surrogate predictions by up to 1.1e-7 relative, OOD GP-variance scores by up to 7.4e-6
|
|
402
|
+
#: relative, and the error percentages by up to 1.3e-5 percentage points -- errors are
|
|
403
|
+
#: differences of two near-equal numbers, so they get an absolute tolerance. Both tolerances sit at least ten times under the digits the command
|
|
404
|
+
#: prints (errors to 0.01, scores to 0.001 and 0.0001). What decides the result is compared
|
|
405
|
+
#: exactly whatever the tolerance: every fired/quiet flag, every PhysMAP flag, every count and
|
|
406
|
+
#: label. `check` separately asserts the exact input overlap and the exact identity of the OOD
|
|
407
|
+
#: scores between the two gravity states of one run.
|
|
408
|
+
BANK_REL_TOL = 1e-4 # every float but the error percentages
|
|
409
|
+
BANK_ERROR_ABS_TOL = 1e-3 # the error percentages, in percentage points
|
|
410
|
+
BANK_TOLERANCE_NOTE = "1e-4 relative, and 0.001 percentage points for errors"
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def bank_tolerance(path: str) -> tuple[float, float]:
|
|
414
|
+
"""(relative, absolute) tolerance for one field of the record."""
|
|
415
|
+
if path.endswith("_error_pct"):
|
|
416
|
+
return 0.0, BANK_ERROR_ABS_TOL
|
|
417
|
+
return BANK_REL_TOL, 0.0
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def compare_with_bank(record: dict):
|
|
421
|
+
"""The command's drift check: `record` against the committed bank, at the tolerances
|
|
422
|
+
above."""
|
|
423
|
+
from physmap.benchmarks.compare import compare_records
|
|
424
|
+
return compare_records(record, load_banked(), tolerance=bank_tolerance,
|
|
425
|
+
tolerance_note=BANK_TOLERANCE_NOTE)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def render(record: dict) -> str:
|
|
429
|
+
"""Deterministic report. Continuous values first; the illustrative threshold last."""
|
|
430
|
+
from physmap.benchmarks.benchmark_v0_4 import REFERENCE_PCT
|
|
431
|
+
ref = str(int(REFERENCE_PCT))
|
|
432
|
+
L = []
|
|
433
|
+
L.append("CONTROLLED MODEL-REUSE STRESS TEST -- Lewis (1992) Test 35A")
|
|
434
|
+
L.append("")
|
|
435
|
+
L.append(_wrap(record["framing"]))
|
|
436
|
+
L.append("")
|
|
437
|
+
L.append("Claim: " + _wrap(record["claim"], indent=7).lstrip())
|
|
438
|
+
L.append("")
|
|
439
|
+
L.append(_wrap(record["status"]))
|
|
440
|
+
L.append("")
|
|
441
|
+
ic = record["input_contract"]
|
|
442
|
+
L.append("INPUT CONTRACT")
|
|
443
|
+
L.append(f" surrogate receives {', '.join(ic['surrogate_inputs'])}")
|
|
444
|
+
L.append(f" OOD detector receives {', '.join(ic['ood_detector_inputs'])}")
|
|
445
|
+
L.append(f" neither receives {', '.join(ic['withheld_from_both'])}")
|
|
446
|
+
L.append(f" deployment Re {ic['deployment_inputs']['Re']}, "
|
|
447
|
+
f"Pr {ic['deployment_inputs']['Pr']}, x/D at Lewis's 12 stations")
|
|
448
|
+
L.append(" every visible deployment input exactly matches a training input (design M): "
|
|
449
|
+
+ ("YES" if ic["design_M_every_visible_input_exactly_in_training"] else "NO"))
|
|
450
|
+
L.append("")
|
|
451
|
+
ma = record["matched_ablation"]
|
|
452
|
+
L.append("MATCHED ABLATION")
|
|
453
|
+
L.append(f" provenance {ma['provenance']}; {ma['cases'][0]} vs {ma['cases'][1]}")
|
|
454
|
+
L.append(f" only difference: {ma['only_difference'].split(';')[0]}")
|
|
455
|
+
L.append(f" energy closure {ma['full']['energy_closure_pct']:+.2f} % (full), "
|
|
456
|
+
f"{ma['removed']['energy_closure_pct']:+.2f} % (removed), against the inlet-property "
|
|
457
|
+
f"energy balance; {ma['iterations']} iterations each")
|
|
458
|
+
L.append("")
|
|
459
|
+
L.append("HEADLINE -- design M: every visible deployment input is a training input")
|
|
460
|
+
L.append(f" {'x/D':>7} | {'surrogate error':^21} | {'input-based OOD detector':^27} | "
|
|
461
|
+
f"{'PhysMAP materiality':^19}")
|
|
462
|
+
L.append(f" {'':>7} | {'control':>9} {'experiment':>11} | {'distance':>9} {'GP var':>8} "
|
|
463
|
+
f"{'@' + ref:>8} | {'grav off':>8} {'grav on':>9}")
|
|
464
|
+
for s in record["headline_design_M"]["stations"]:
|
|
465
|
+
on = s["ood_gravity_on"][ref]
|
|
466
|
+
mark = " " if s["comparable"] else "*"
|
|
467
|
+
L.append(f" {s['x_over_D']:>7.2f} {mark} | {s['control_error_pct']:>+8.2f}% "
|
|
468
|
+
f"{s['experimental_error_pct']:>+10.1f}% | {on['distance']['score']:>9.3f} "
|
|
469
|
+
f"{on['gp_variance']['score']:>8.4f} {('fires' if on['fired'] else 'quiet'):>8} | "
|
|
470
|
+
f"{s['materiality_gravity_off']:>8.3f} {s['materiality_gravity_on']:>9.3f}")
|
|
471
|
+
thr = record["headline_design_M"]["stations"][0]["ood_gravity_on"][ref]
|
|
472
|
+
L.append(f" * disowned by Lewis. OOD thresholds at the reference percentile {ref}: "
|
|
473
|
+
f"distance {thr['distance']['threshold']:.3f}, GP variance "
|
|
474
|
+
f"{thr['gp_variance']['threshold']:.3f}.")
|
|
475
|
+
ident = record["headline_design_M"]["ood"]["identical_between_gravity_states"]
|
|
476
|
+
L.append(" The OOD columns are the SAME with gravity off and on -- every score, every "
|
|
477
|
+
"percentile: " + ("YES" if ident else "NO"))
|
|
478
|
+
sweep = record["headline_design_M"]["ood"]["comparable_stations_fired_by_pct"]
|
|
479
|
+
n_comp = sum(1 for s in record["headline_design_M"]["stations"] if s["comparable"])
|
|
480
|
+
L.append(" Comparable stations where it fires, by operating percentile (both states): "
|
|
481
|
+
+ ", ".join(f"{pc}: {v['gravity_on']}/{n_comp}" for pc, v in sweep.items()))
|
|
482
|
+
L.append(" (At low percentiles it fires on its own training data too -- see "
|
|
483
|
+
"design_M_low_pct_mechanism.json.)")
|
|
484
|
+
L.append("")
|
|
485
|
+
L.append("SECONDARY -- design A3, the visible operating point falls BETWEEN training runs")
|
|
486
|
+
sec = record["secondary_design_A3"]["ood"]
|
|
487
|
+
n_comp = sum(1 for s in record["secondary_design_A3"]["stations"] if s["comparable"])
|
|
488
|
+
L.append(f" OOD fires at {sec['comparable_stations_fired_by_pct'][ref]['gravity_off']} of "
|
|
489
|
+
f"{n_comp} comparable stations with gravity off (surrogate accurate) and "
|
|
490
|
+
f"{sec['comparable_stations_fired_by_pct'][ref]['gravity_on']} of {n_comp} with "
|
|
491
|
+
f"gravity on; scores identical between them: "
|
|
492
|
+
+ ("YES" if sec["identical_between_gravity_states"] else "NO"))
|
|
493
|
+
L.append(" PhysMAP's materiality is the same as in the headline: it follows the physics, "
|
|
494
|
+
"not the training design.")
|
|
495
|
+
L.append("")
|
|
496
|
+
L.append("THRESHOLDS -- which results depend on one")
|
|
497
|
+
for t in record["threshold_dependence"]:
|
|
498
|
+
extra = (f"holds for theta <= {t['holds_for_theta_up_to']:.3f}"
|
|
499
|
+
if "holds_for_theta_up_to" in t else t.get("note", ""))
|
|
500
|
+
L.append(f" {t['result']}")
|
|
501
|
+
L.append(f" threshold: {t['threshold']}. {extra}")
|
|
502
|
+
flagged = [s["x_over_D"] for s in record["headline_design_M"]["stations"]
|
|
503
|
+
if s["comparable"] and s["physmap_flag_gravity_on_at_illustrative_theta"]]
|
|
504
|
+
L.append(f" At the ILLUSTRATIVE theta = {ILLUSTRATIVE_THETA:.2f} (not a verdict; theta is "
|
|
505
|
+
f"unlocked) PhysMAP would flag x/D {', '.join(f'{x:g}' for x in flagged)} with "
|
|
506
|
+
f"gravity on, and nothing with gravity off.")
|
|
507
|
+
L.append("")
|
|
508
|
+
L.append("NOT CLAIMED")
|
|
509
|
+
for n in record["not_claimed"]:
|
|
510
|
+
L.append(" - " + _wrap(n, indent=4).lstrip())
|
|
511
|
+
return "\n".join(L)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _wrap(text: str, width: int = 92, indent: int = 0) -> str:
|
|
515
|
+
import textwrap
|
|
516
|
+
return textwrap.fill(text, width=width, initial_indent=" " * indent,
|
|
517
|
+
subsequent_indent=" " * indent)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Substrate machinery — engine + configs + per-vehicle adapters.
|
|
2
|
+
|
|
3
|
+
The architecture refactor's substrate-side surface, grouped under one
|
|
4
|
+
subpackage. Read order for newcomers:
|
|
5
|
+
|
|
6
|
+
vehicle_config — VehicleConfig dataclass + strict-schema YAML loader.
|
|
7
|
+
engine — build_substrate(config, registry, corpus) with the
|
|
8
|
+
geometry-match invariant (the Sparrow-Cur lesson
|
|
9
|
+
encoded as code).
|
|
10
|
+
loaders — LOADERS dispatch registry; one adapter per vehicle.
|
|
11
|
+
stage1_ingest — Row + Mechanism dataclasses + the independence guard.
|
|
12
|
+
corpus_real — SubstrateMeta dataclass + per-row physics-coords helper.
|
|
13
|
+
|
|
14
|
+
Legacy per-vehicle modules (post-Cleanup-5; CSV helpers + thin shims to
|
|
15
|
+
the engine path):
|
|
16
|
+
forrest — Forrest mini-channel (cell_assignment + dims only).
|
|
17
|
+
lance_smith — L&S transient (CSV I/O helpers + lance_smith_to_rows shim).
|
|
18
|
+
naca_tn1451 — NACA TN-1451 (NACARow + from_synthetic_shape).
|
|
19
|
+
naca_wpd_loader — NACA WPD CSV loader (called by the engine adapter).
|
|
20
|
+
|
|
21
|
+
Module renames during R3:
|
|
22
|
+
substrate_engine.py -> engine.py
|
|
23
|
+
substrate_loaders.py -> loaders.py
|
|
24
|
+
forrest_substrate.py -> forrest.py
|
|
25
|
+
lance_smith_substrate.py -> lance_smith.py
|
|
26
|
+
naca_tn1451_substrate.py -> naca_tn1451.py
|
|
27
|
+
(vehicle_config, stage1_ingest, corpus_real, naca_wpd_loader keep names.)
|
|
28
|
+
"""
|