physmap 0.2.1__tar.gz → 0.2.3__tar.gz

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 (106) hide show
  1. {physmap-0.2.1 → physmap-0.2.3}/NOTICE +5 -0
  2. {physmap-0.2.1/src/physmap.egg-info → physmap-0.2.3}/PKG-INFO +158 -58
  3. {physmap-0.2.1 → physmap-0.2.3}/README.md +157 -57
  4. {physmap-0.2.1 → physmap-0.2.3}/pyproject.toml +1 -1
  5. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/__init__.py +1 -1
  6. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/benchmarks/report.py +31 -0
  7. {physmap-0.2.1 → physmap-0.2.3/src/physmap.egg-info}/PKG-INFO +158 -58
  8. {physmap-0.2.1 → physmap-0.2.3}/tests/test_benchmark_boundaries.py +4 -1
  9. {physmap-0.2.1 → physmap-0.2.3}/tests/test_talk_package.py +12 -0
  10. {physmap-0.2.1 → physmap-0.2.3}/LICENSE +0 -0
  11. {physmap-0.2.1 → physmap-0.2.3}/LICENSE-CORPUS +0 -0
  12. {physmap-0.2.1 → physmap-0.2.3}/setup.cfg +0 -0
  13. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/_paths.py +0 -0
  14. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/applicability/__init__.py +0 -0
  15. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/applicability/fixtures.py +0 -0
  16. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/applicability/screen.py +0 -0
  17. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/baselines/__init__.py +0 -0
  18. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/benchmarks/__init__.py +0 -0
  19. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/benchmarks/benchmark_report.py +0 -0
  20. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/benchmarks/benchmark_v0_4.py +0 -0
  21. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/benchmarks/compare.py +0 -0
  22. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/benchmarks/registry.py +0 -0
  23. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/cli.py +0 -0
  24. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/closures/__init__.py +0 -0
  25. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/closures/data/__init__.py +0 -0
  26. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/closures/data/closure_index.json +0 -0
  27. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/closures/formulas.py +0 -0
  28. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/closures/geometry_classes.py +0 -0
  29. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/closures/index.py +0 -0
  30. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/closures/registry.py +0 -0
  31. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/compat/__init__.py +0 -0
  32. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/core/__init__.py +0 -0
  33. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/core/mechanism.py +0 -0
  34. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/core/signals.py +0 -0
  35. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/corpus/__init__.py +0 -0
  36. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/corpus/calibration.py +0 -0
  37. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/corpus/data/__init__.py +0 -0
  38. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/corpus/data/corpus_seed.jsonl +0 -0
  39. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/corpus/data/evidence_claims_seed.jsonl +0 -0
  40. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/corpus/data/evidence_sources_seed.jsonl +0 -0
  41. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/corpus/data/premium_coverage.json +0 -0
  42. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/corpus/evidence.py +0 -0
  43. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/explain/__init__.py +0 -0
  44. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/explain/benchmark.py +0 -0
  45. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/explain/causal.py +0 -0
  46. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/__init__.py +0 -0
  47. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/aggregator_observability.py +0 -0
  48. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/classify.py +0 -0
  49. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/configs.py +0 -0
  50. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/corpus_regimes.py +0 -0
  51. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/detector_conformal.py +0 -0
  52. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/detector_density.py +0 -0
  53. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/enums.py +0 -0
  54. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/graph.py +0 -0
  55. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/guardrail.py +0 -0
  56. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/io.py +0 -0
  57. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/regime_observability.py +0 -0
  58. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/render.py +0 -0
  59. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/guardrail/weighting_heuristic.py +0 -0
  60. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/infra/__init__.py +0 -0
  61. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/infra/blindspot_oracle.py +0 -0
  62. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/infra/corpus_runtime.py +0 -0
  63. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/integrations/__init__.py +0 -0
  64. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/materiality/__init__.py +0 -0
  65. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/materiality/estimator.py +0 -0
  66. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/materiality/independence.py +0 -0
  67. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/materiality/surrogate_fit.py +0 -0
  68. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/observability/__init__.py +0 -0
  69. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/pipeline/__init__.py +0 -0
  70. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/pipeline/aggregators.py +0 -0
  71. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/pipeline/assessment_v06.py +0 -0
  72. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/pipeline/core.py +0 -0
  73. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/pipeline/defeasible_aggregator.py +0 -0
  74. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/pipeline/detectors.py +0 -0
  75. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/pipeline/observability.py +0 -0
  76. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/pipeline/surrogate.py +0 -0
  77. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/pipeline/validity_signal.py +0 -0
  78. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/pipeline/vehicle_spec.py +0 -0
  79. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/release.py +0 -0
  80. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/stress_tests/__init__.py +0 -0
  81. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/stress_tests/lewis_reuse.py +0 -0
  82. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/substrate/__init__.py +0 -0
  83. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/substrate/corpus_real.py +0 -0
  84. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/substrate/engine.py +0 -0
  85. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/substrate/forrest.py +0 -0
  86. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/substrate/loaders.py +0 -0
  87. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/substrate/naca_tn1451.py +0 -0
  88. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/substrate/naca_wpd_loader.py +0 -0
  89. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/substrate/stage1_ingest.py +0 -0
  90. {physmap-0.2.1 → physmap-0.2.3}/src/physmap/substrate/vehicle_config.py +0 -0
  91. {physmap-0.2.1 → physmap-0.2.3}/src/physmap.egg-info/SOURCES.txt +0 -0
  92. {physmap-0.2.1 → physmap-0.2.3}/src/physmap.egg-info/dependency_links.txt +0 -0
  93. {physmap-0.2.1 → physmap-0.2.3}/src/physmap.egg-info/entry_points.txt +0 -0
  94. {physmap-0.2.1 → physmap-0.2.3}/src/physmap.egg-info/requires.txt +0 -0
  95. {physmap-0.2.1 → physmap-0.2.3}/src/physmap.egg-info/top_level.txt +0 -0
  96. {physmap-0.2.1 → physmap-0.2.3}/tests/test_applicability_screen.py +0 -0
  97. {physmap-0.2.1 → physmap-0.2.3}/tests/test_benchmark_reproduces.py +0 -0
  98. {physmap-0.2.1 → physmap-0.2.3}/tests/test_causal_materiality.py +0 -0
  99. {physmap-0.2.1 → physmap-0.2.3}/tests/test_cli_without_checkout.py +0 -0
  100. {physmap-0.2.1 → physmap-0.2.3}/tests/test_guardrail_public_api.py +0 -0
  101. {physmap-0.2.1 → physmap-0.2.3}/tests/test_no_conflation.py +0 -0
  102. {physmap-0.2.1 → physmap-0.2.3}/tests/test_no_heavy_imports.py +0 -0
  103. {physmap-0.2.1 → physmap-0.2.3}/tests/test_render_ood_wording.py +0 -0
  104. {physmap-0.2.1 → physmap-0.2.3}/tests/test_stress_test_lewis_reuse.py +0 -0
  105. {physmap-0.2.1 → physmap-0.2.3}/tests/test_surrogate_fit.py +0 -0
  106. {physmap-0.2.1 → physmap-0.2.3}/tests/test_version.py +0 -0
@@ -75,3 +75,8 @@ REMOVAL ON OBJECTION
75
75
  Any rights holder who objects gets the data removed. Open an issue at
76
76
  https://github.com/cloudronin/physmap/issues, or contact the maintainer.
77
77
  No argument will be made; the files come out.
78
+
79
+ Removal reaches this repository and every later release. It cannot
80
+ reach a version already archived: each release is archived on Zenodo
81
+ with its own DOI, and an archived version is a permanent record that
82
+ only Zenodo can withdraw. The packages on PyPI do not include data/.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: physmap
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Physics-aware credibility checks for AI surrogates in multiphysics simulation: closure validity, surrogate observability, and causal materiality.
5
5
  Author: Vishnu Vettrivel
6
6
  License-Expression: MIT
@@ -44,6 +44,9 @@ PhysMAP reads the bound variable from the test coordinates instead of from the
44
44
  surrogate's inputs, checks it against the closure relation's validated range, and knows
45
45
  at fit time whether that variable is structurally observable to the surrogate at all.
46
46
 
47
+ **That is the point of PhysMAP: it catches the failures an input-based OOD detector cannot
48
+ see, because what broke the surrogate is not one of its inputs.**
49
+
47
50
  ```
48
51
  verdicts: {'REJECT': 45}
49
52
  rationale: Prediction relies on gnielinski-1976 beyond its validated x_over_D bound
@@ -57,17 +60,109 @@ No LLM is in any path. Every explanation is a deterministic rendered template.
57
60
 
58
61
  Run it yourself: [`examples/naca_entrance_region.py`](https://github.com/cloudronin/physmap/blob/main/examples/naca_entrance_region.py).
59
62
 
63
+ ## What PhysMAP adds to OOD detection
64
+
65
+ ![What PhysMAP adds to input-based OOD detection, per dataset](https://raw.githubusercontent.com/cloudronin/physmap/main/docs/talk/figures/bench_3_what_physmap_adds.png)
66
+
67
+ An input-based OOD detector judges a prediction by where its inputs sit relative to the
68
+ training data. It asks that well — but it cannot see a change in something that is not an
69
+ input. PhysMAP adds the check it cannot make: it reads the variables the surrogate never saw,
70
+ tests the physics relation behind the surrogate against its validated range, and knows at
71
+ setup which of those variables the detectors can see. It keeps the OOD detectors, and
72
+ overrides them only where they are structurally blind.
73
+
74
+ The seven-vehicle benchmark measures exactly that, per dataset, at the default setting (the
75
+ 99th percentile):
76
+
77
+ | Dataset | Can the OOD detectors see the cause? | Wrong predictions caught only by PhysMAP | Right predictions flagged anyway |
78
+ |---|---|---|---|
79
+ | `naca_tn1451` — heated pipe, entrance region | no | 20 of 20 | 24 |
80
+ | `casper_hypersonic_transition` — hypersonic transition | no | 4 of 8 | 0 |
81
+ | `jin_sco2_buoyancy` — supercritical CO2, vertical tube | no | 15 of 26 | 0 |
82
+ | `velazquez_sco2` — supercritical CO2, property variation | partly | 18 of 67 | 0 |
83
+ | `dirker_water` — water, horizontal tube | partly | 2 of 11 | 16 |
84
+ | `marineau_hypersonic_transition` — hypersonic transition | yes | 0 of 6 | 0 |
85
+ | `forrest` — rectangular channel | yes | not tested: one training row | — |
86
+
87
+ - **Where it helps:** when the cause of failure is hidden from the surrogate's inputs. There
88
+ PhysMAP catches wrong predictions the OOD detectors miss entirely — all 20 for the pipe
89
+ entrance.
90
+ - **Where it doesn't:** when the cause is an input, the OOD detectors already see it and
91
+ PhysMAP adds nothing. `marineau_hypersonic_transition` is the control that shows it.
92
+ - **The cost:** the closure check flags anything outside a relation's validated range, even
93
+ when the surrogate happens to be right — 24 false alarms for the pipe entrance, 16 for
94
+ `dirker_water`.
95
+
96
+ Counts are rows of each dataset, not independent cases, and are never pooled into a rate
97
+ across datasets; the source values are digitised from publications. The benchmark's
98
+ input-based detectors are distance-to-training and GP variance. `physmap benchmark report`
99
+ prints these counts, and `physmap benchmark run` recomputes them from a clone.
100
+
101
+ This is a different question from PhysMAP's causal-materiality check — whether a mechanism
102
+ the surrogate never saw is large enough to matter — which is demonstrated separately below.
103
+ Neither is evidence for the other.
104
+
105
+ ## The benchmark: seven vehicles, all rerunnable
106
+
107
+ ```bash
108
+ physmap benchmark report # all seven outcomes and the counts above, each marked recomputed or banked
109
+ physmap benchmark run # recomputes all seven from this checkout, diffs against the bank
110
+ physmap benchmark coverage # what that subset does and does not cover
111
+ ```
112
+
113
+ All seven vehicles ran, all seven outcomes are reported, and **all seven now ship their
114
+ source data**.
115
+
116
+ **`physmap benchmark run` recomputes all seven from this checkout and diffs the result
117
+ against the committed matrix.** Every field of every cell, not just the headline outcome.
118
+ It exits non-zero if anything drifted, and never writes the bank it is checking itself
119
+ against.
120
+
121
+ Floats are compared within `1e-9` relative, everything else exactly — and the distinction
122
+ is load-bearing rather than a convenience. Every field that decides an outcome is an int
123
+ or a string (counts, verdicts, outcome labels, observability classes), so the tolerance
124
+ cannot absorb a real change. It exists because a clean-clone check on numpy 2.5 found
125
+ `dirker_water.observability_score` differing from the banked value by **one unit in the
126
+ last place**. A match that needed the tolerance is reported as such, not as "identical".
127
+
128
+ `physmap benchmark report` reads the bank without running anything, and says so — the
129
+ report distinguishes a recomputed row from a banked one, and the counts are computed
130
+ rather than written down, so they cannot quietly go stale.
131
+
132
+ **Shipping is also not licensing.** Two of the seven carry affirmative permission:
133
+ `naca_tn1451` (public domain) and `velazquez_sco2` (CC BY 4.0). **Five do not**, and the
134
+ registry and report label them rather than calling everything clear:
135
+
136
+ - `marineau_hypersonic_transition` — no licence and **no prohibition**. Values transcribed
137
+ from a published table in a publicly funded, public-release, government-hosted document.
138
+ - `forrest`, `casper_hypersonic_transition`, `dirker_water`, `jin_sco2_buoyancy` —
139
+ published **against** express publisher terms. AIAA prohibits using its content to
140
+ develop machine-learning models; ASME and Elsevier require permission to reproduce, and
141
+ Elsevier's licence forbids systematic redistribution. Risks accepted knowingly, not
142
+ findings that the terms do not apply.
143
+
144
+ All five redistribute **numbers only** — no paper, figure or PDF, enforced by two audits —
145
+ and all five are removed on objection, from the repository and every later release. A version
146
+ already archived on Zenodo stays archived; only Zenodo can withdraw it. The full basis,
147
+ including the arguments against, is in [data/REDISTRIBUTION.md](https://github.com/cloudronin/physmap/blob/main/data/REDISTRIBUTION.md) and [NOTICE](https://github.com/cloudronin/physmap/blob/main/NOTICE).
148
+
149
+ **One dataset is published but not benchmark-grade.** `forrest`'s own header calls its
150
+ values visual estimates for triage only, and its cell is degenerate — one training row, no
151
+ detector fit — so its `DO_NO_HARM` outcome is short-circuited rather than earned. Being
152
+ legal to publish and being fit to benchmark on are different questions; the registry tracks
153
+ them on separate axes. `physmap benchmark coverage` prints it.
154
+
60
155
  ## Three checks, deliberately kept apart
61
156
 
62
157
  These are different claims resting on different evidence. Conflating them is the
63
158
  specific error this project is built to avoid, so nothing here attributes the results of
64
159
  one to another.
65
160
 
66
- | Check | Question | Status |
67
- |---|---|---|
68
- | **Closure validity** | Is this closure relation being applied outside the range it was calibrated on? | Shipping |
69
- | **Surrogate observability** | Can the surrogate's inputs even represent the variable that governs the failure? | Shipping |
70
- | **Causal materiality** | Is the out-of-range mechanism large enough to matter for the quantity of interest? | Preview — method, plus one controlled stress test |
161
+ | Check | Question | Status | Evidence |
162
+ |---|---|---|---|
163
+ | **Closure validity** | Is this closure relation being applied outside the range it was calibrated on? | Shipping | the seven-vehicle benchmark, above |
164
+ | **Surrogate observability** | Can the surrogate's inputs even represent the variable that governs the failure? | Shipping | the seven-vehicle benchmark, above |
165
+ | **Causal materiality** | Is the out-of-range mechanism large enough to matter for the quantity of interest? | Preview — method, plus one controlled stress test | Lewis 35A, below |
71
166
 
72
167
  ## What this release claims
73
168
 
@@ -82,8 +177,9 @@ screen, the independence guard, and deterministic explanations. Its fixtures are
82
177
  description. Beyond them, **one controlled stress test runs the causal path on a real
83
178
  experiment** (below) — a development demonstration on a single run, not an evaluation.
84
179
 
85
- **It makes no performance claim.** No precision, recall or F1 is computed, reported or
86
- shipped anywhere in this package. A test parses the package and fails if one appears. The causal-materiality results presented in the NAFEMS
180
+ **No precision, recall or F1.** None is computed, reported or shipped anywhere in this
181
+ package, and a test parses the package and fails if one appears. The benchmark reports counts
182
+ per dataset, never a rate across datasets. The causal-materiality results presented in the NAFEMS
87
183
  Multiphysics 2026 abstract are **not** reproduced here: the original study's inputs are
88
184
  gone, and the basis for its experimental truth is unresolved. A reconstruction is under
89
185
  way under a locked protocol that fixes its rules before any rebuilt number is examined.
@@ -94,7 +190,11 @@ not reproduction of the published numbers. The protocol defines `REPRODUCED` and
94
190
  unreachable, so the word cannot drift onto a weaker result. See
95
191
  [`protocols/`](https://github.com/cloudronin/physmap/tree/main/protocols).
96
192
 
97
- ## A controlled model-reuse stress test: Lewis 35A
193
+ ## A separate question: causal materiality, in a controlled stress test (Lewis 35A)
194
+
195
+ The benchmark asks whether PhysMAP catches failures the OOD detectors cannot see. This asks
196
+ something else: when a mechanism the surrogate never saw becomes active, does it matter for the
197
+ answer — and where?
98
198
 
99
199
  ```bash
100
200
  physmap stress-test lewis-reuse
@@ -145,55 +245,6 @@ OpenFOAM. Full record:
145
245
  talk package — figures, facts sheet, claims ledger, and a clean-clone reproduction record — is
146
246
  in [docs/talk/](https://github.com/cloudronin/physmap/blob/main/docs/talk/README.md).
147
247
 
148
- ## The benchmark: seven vehicles, all rerunnable
149
-
150
- ```bash
151
- physmap benchmark report # all seven outcomes, each marked recomputed or banked
152
- physmap benchmark run # recomputes all seven from this checkout, diffs against the bank
153
- physmap benchmark coverage # what that subset does and does not cover
154
- ```
155
-
156
- All seven vehicles ran, all seven outcomes are reported, and **all seven now ship their
157
- source data**.
158
-
159
- **`physmap benchmark run` recomputes all seven from this checkout and diffs the result
160
- against the committed matrix.** Every field of every cell, not just the headline outcome.
161
- It exits non-zero if anything drifted, and never writes the bank it is checking itself
162
- against.
163
-
164
- Floats are compared within `1e-9` relative, everything else exactly — and the distinction
165
- is load-bearing rather than a convenience. Every field that decides an outcome is an int
166
- or a string (counts, verdicts, outcome labels, observability classes), so the tolerance
167
- cannot absorb a real change. It exists because a clean-clone check on numpy 2.5 found
168
- `dirker_water.observability_score` differing from the banked value by **one unit in the
169
- last place**. A match that needed the tolerance is reported as such, not as "identical".
170
-
171
- `physmap benchmark report` reads the bank without running anything, and says so — the
172
- report distinguishes a recomputed row from a banked one, and the counts are computed
173
- rather than written down, so they cannot quietly go stale.
174
-
175
- **Shipping is also not licensing.** Two of the seven carry affirmative permission:
176
- `naca_tn1451` (public domain) and `velazquez_sco2` (CC BY 4.0). **Five do not**, and the
177
- registry and report label them rather than calling everything clear:
178
-
179
- - `marineau_hypersonic_transition` — no licence and **no prohibition**. Values transcribed
180
- from a published table in a publicly funded, public-release, government-hosted document.
181
- - `forrest`, `casper_hypersonic_transition`, `dirker_water`, `jin_sco2_buoyancy` —
182
- published **against** express publisher terms. AIAA prohibits using its content to
183
- develop machine-learning models; ASME and Elsevier require permission to reproduce, and
184
- Elsevier's licence forbids systematic redistribution. Risks accepted knowingly, not
185
- findings that the terms do not apply.
186
-
187
- All five redistribute **numbers only** — no paper, figure or PDF, enforced by two audits —
188
- and all five are removed on objection. The full basis, including the arguments against, is
189
- in [data/REDISTRIBUTION.md](https://github.com/cloudronin/physmap/blob/main/data/REDISTRIBUTION.md) and [NOTICE](https://github.com/cloudronin/physmap/blob/main/NOTICE).
190
-
191
- **One dataset is published but not benchmark-grade.** `forrest`'s own header calls its
192
- values visual estimates for triage only, and its cell is degenerate — one training row, no
193
- detector fit — so its `DO_NO_HARM` outcome is short-circuited rather than earned. Being
194
- legal to publish and being fit to benchmark on are different questions; the registry tracks
195
- them on separate axes. `physmap benchmark coverage` prints it.
196
-
197
248
  ## Install
198
249
 
199
250
  ```bash
@@ -217,6 +268,55 @@ redistribution terms and partly because it is not runtime data. From a plain
217
268
  Optional extras: `[jsonld]` adds evidence export via `uofa`, `[experiment]` adds
218
269
  `matplotlib` for benchmark figures, `[dev]` adds the test tooling.
219
270
 
271
+ ## Using the guardrail
272
+
273
+ The guardrail does not guess which checks to combine. Its setup decides.
274
+
275
+ ```python
276
+ from physmap import ColumnMap, CredibilityGuardrail, Regime
277
+
278
+ guard = CredibilityGuardrail(
279
+ surrogate_inputs=["Re", "Pr"], # exactly the inputs your surrogate takes
280
+ regime=Regime.ENTRANCE_REGION_PIPE, # the physics it is used in: picks the closures
281
+ )
282
+ cols = ColumnMap(inputs=["Re", "Pr", "x_over_D"]) # the surrogate inputs, plus every
283
+ # variable those closures put a bound on
284
+ guard.fit(train_X, train_y=train_y, columns=cols) # arrays whose columns follow `cols`
285
+ for a in guard.assess(test_X, columns=cols):
286
+ print(a.verdict.name, a.rationale)
287
+ ```
288
+
289
+ **What each piece does**
290
+
291
+ - **`surrogate_inputs`** — the columns your surrogate actually uses. The input-based OOD
292
+ detectors are fitted on exactly these.
293
+ - **`regime`** — picks the closure relations whose validated ranges the closure check uses.
294
+ Choose the regime that matches your physics, or name one closure with `closure_id=`. If none
295
+ matches, use `Regime.UNLISTED`: the guardrail then runs the OOD detectors alone.
296
+ - **The data columns** — must include every variable those closures bound, even ones the
297
+ surrogate never sees (here `x_over_D`). If one is missing, `fit` stops and names it.
298
+ - **`train_y`** — the training targets. The GP-variance detector needs them.
299
+ - **`detectors=`** — optional. The default is novelty density and GP variance (the input-based
300
+ OOD detectors) plus closure validity. Distance-to-training and a conformal residual check are
301
+ also available.
302
+
303
+ **How the verdict is combined, for each prediction**
304
+
305
+ - At setup, each bounded variable is classed **observable** (it is a surrogate input),
306
+ **unobservable** (it is not), or **partial**. `guard.observability_classification` shows the
307
+ result.
308
+ - The closure check fires on an **unobservable** variable → `REJECT`. The OOD detectors
309
+ cannot see that variable, so the closure check is trusted.
310
+ - It fires on a **partial** variable → a calibrated blend where a calibration exists,
311
+ otherwise `UNCERTAIN`.
312
+ - Otherwise — the closure check is quiet, or the variable is one the detectors can see — the
313
+ OOD detectors decide: none fired → `TRUSTWORTHY`, a reject-level fire → `REJECT`, anything
314
+ else → `WARN`.
315
+
316
+ Every listed regime's closures ship in the published corpus. A complete, runnable version of
317
+ the example above is
318
+ [`examples/naca_entrance_region.py`](https://github.com/cloudronin/physmap/blob/main/examples/naca_entrance_region.py).
319
+
220
320
  ## The corpus
221
321
 
222
322
  The code is MIT and the corpus data is CC BY 4.0 — see the licences below.
@@ -12,6 +12,9 @@ PhysMAP reads the bound variable from the test coordinates instead of from the
12
12
  surrogate's inputs, checks it against the closure relation's validated range, and knows
13
13
  at fit time whether that variable is structurally observable to the surrogate at all.
14
14
 
15
+ **That is the point of PhysMAP: it catches the failures an input-based OOD detector cannot
16
+ see, because what broke the surrogate is not one of its inputs.**
17
+
15
18
  ```
16
19
  verdicts: {'REJECT': 45}
17
20
  rationale: Prediction relies on gnielinski-1976 beyond its validated x_over_D bound
@@ -25,17 +28,109 @@ No LLM is in any path. Every explanation is a deterministic rendered template.
25
28
 
26
29
  Run it yourself: [`examples/naca_entrance_region.py`](https://github.com/cloudronin/physmap/blob/main/examples/naca_entrance_region.py).
27
30
 
31
+ ## What PhysMAP adds to OOD detection
32
+
33
+ ![What PhysMAP adds to input-based OOD detection, per dataset](https://raw.githubusercontent.com/cloudronin/physmap/main/docs/talk/figures/bench_3_what_physmap_adds.png)
34
+
35
+ An input-based OOD detector judges a prediction by where its inputs sit relative to the
36
+ training data. It asks that well — but it cannot see a change in something that is not an
37
+ input. PhysMAP adds the check it cannot make: it reads the variables the surrogate never saw,
38
+ tests the physics relation behind the surrogate against its validated range, and knows at
39
+ setup which of those variables the detectors can see. It keeps the OOD detectors, and
40
+ overrides them only where they are structurally blind.
41
+
42
+ The seven-vehicle benchmark measures exactly that, per dataset, at the default setting (the
43
+ 99th percentile):
44
+
45
+ | Dataset | Can the OOD detectors see the cause? | Wrong predictions caught only by PhysMAP | Right predictions flagged anyway |
46
+ |---|---|---|---|
47
+ | `naca_tn1451` — heated pipe, entrance region | no | 20 of 20 | 24 |
48
+ | `casper_hypersonic_transition` — hypersonic transition | no | 4 of 8 | 0 |
49
+ | `jin_sco2_buoyancy` — supercritical CO2, vertical tube | no | 15 of 26 | 0 |
50
+ | `velazquez_sco2` — supercritical CO2, property variation | partly | 18 of 67 | 0 |
51
+ | `dirker_water` — water, horizontal tube | partly | 2 of 11 | 16 |
52
+ | `marineau_hypersonic_transition` — hypersonic transition | yes | 0 of 6 | 0 |
53
+ | `forrest` — rectangular channel | yes | not tested: one training row | — |
54
+
55
+ - **Where it helps:** when the cause of failure is hidden from the surrogate's inputs. There
56
+ PhysMAP catches wrong predictions the OOD detectors miss entirely — all 20 for the pipe
57
+ entrance.
58
+ - **Where it doesn't:** when the cause is an input, the OOD detectors already see it and
59
+ PhysMAP adds nothing. `marineau_hypersonic_transition` is the control that shows it.
60
+ - **The cost:** the closure check flags anything outside a relation's validated range, even
61
+ when the surrogate happens to be right — 24 false alarms for the pipe entrance, 16 for
62
+ `dirker_water`.
63
+
64
+ Counts are rows of each dataset, not independent cases, and are never pooled into a rate
65
+ across datasets; the source values are digitised from publications. The benchmark's
66
+ input-based detectors are distance-to-training and GP variance. `physmap benchmark report`
67
+ prints these counts, and `physmap benchmark run` recomputes them from a clone.
68
+
69
+ This is a different question from PhysMAP's causal-materiality check — whether a mechanism
70
+ the surrogate never saw is large enough to matter — which is demonstrated separately below.
71
+ Neither is evidence for the other.
72
+
73
+ ## The benchmark: seven vehicles, all rerunnable
74
+
75
+ ```bash
76
+ physmap benchmark report # all seven outcomes and the counts above, each marked recomputed or banked
77
+ physmap benchmark run # recomputes all seven from this checkout, diffs against the bank
78
+ physmap benchmark coverage # what that subset does and does not cover
79
+ ```
80
+
81
+ All seven vehicles ran, all seven outcomes are reported, and **all seven now ship their
82
+ source data**.
83
+
84
+ **`physmap benchmark run` recomputes all seven from this checkout and diffs the result
85
+ against the committed matrix.** Every field of every cell, not just the headline outcome.
86
+ It exits non-zero if anything drifted, and never writes the bank it is checking itself
87
+ against.
88
+
89
+ Floats are compared within `1e-9` relative, everything else exactly — and the distinction
90
+ is load-bearing rather than a convenience. Every field that decides an outcome is an int
91
+ or a string (counts, verdicts, outcome labels, observability classes), so the tolerance
92
+ cannot absorb a real change. It exists because a clean-clone check on numpy 2.5 found
93
+ `dirker_water.observability_score` differing from the banked value by **one unit in the
94
+ last place**. A match that needed the tolerance is reported as such, not as "identical".
95
+
96
+ `physmap benchmark report` reads the bank without running anything, and says so — the
97
+ report distinguishes a recomputed row from a banked one, and the counts are computed
98
+ rather than written down, so they cannot quietly go stale.
99
+
100
+ **Shipping is also not licensing.** Two of the seven carry affirmative permission:
101
+ `naca_tn1451` (public domain) and `velazquez_sco2` (CC BY 4.0). **Five do not**, and the
102
+ registry and report label them rather than calling everything clear:
103
+
104
+ - `marineau_hypersonic_transition` — no licence and **no prohibition**. Values transcribed
105
+ from a published table in a publicly funded, public-release, government-hosted document.
106
+ - `forrest`, `casper_hypersonic_transition`, `dirker_water`, `jin_sco2_buoyancy` —
107
+ published **against** express publisher terms. AIAA prohibits using its content to
108
+ develop machine-learning models; ASME and Elsevier require permission to reproduce, and
109
+ Elsevier's licence forbids systematic redistribution. Risks accepted knowingly, not
110
+ findings that the terms do not apply.
111
+
112
+ All five redistribute **numbers only** — no paper, figure or PDF, enforced by two audits —
113
+ and all five are removed on objection, from the repository and every later release. A version
114
+ already archived on Zenodo stays archived; only Zenodo can withdraw it. The full basis,
115
+ including the arguments against, is in [data/REDISTRIBUTION.md](https://github.com/cloudronin/physmap/blob/main/data/REDISTRIBUTION.md) and [NOTICE](https://github.com/cloudronin/physmap/blob/main/NOTICE).
116
+
117
+ **One dataset is published but not benchmark-grade.** `forrest`'s own header calls its
118
+ values visual estimates for triage only, and its cell is degenerate — one training row, no
119
+ detector fit — so its `DO_NO_HARM` outcome is short-circuited rather than earned. Being
120
+ legal to publish and being fit to benchmark on are different questions; the registry tracks
121
+ them on separate axes. `physmap benchmark coverage` prints it.
122
+
28
123
  ## Three checks, deliberately kept apart
29
124
 
30
125
  These are different claims resting on different evidence. Conflating them is the
31
126
  specific error this project is built to avoid, so nothing here attributes the results of
32
127
  one to another.
33
128
 
34
- | Check | Question | Status |
35
- |---|---|---|
36
- | **Closure validity** | Is this closure relation being applied outside the range it was calibrated on? | Shipping |
37
- | **Surrogate observability** | Can the surrogate's inputs even represent the variable that governs the failure? | Shipping |
38
- | **Causal materiality** | Is the out-of-range mechanism large enough to matter for the quantity of interest? | Preview — method, plus one controlled stress test |
129
+ | Check | Question | Status | Evidence |
130
+ |---|---|---|---|
131
+ | **Closure validity** | Is this closure relation being applied outside the range it was calibrated on? | Shipping | the seven-vehicle benchmark, above |
132
+ | **Surrogate observability** | Can the surrogate's inputs even represent the variable that governs the failure? | Shipping | the seven-vehicle benchmark, above |
133
+ | **Causal materiality** | Is the out-of-range mechanism large enough to matter for the quantity of interest? | Preview — method, plus one controlled stress test | Lewis 35A, below |
39
134
 
40
135
  ## What this release claims
41
136
 
@@ -50,8 +145,9 @@ screen, the independence guard, and deterministic explanations. Its fixtures are
50
145
  description. Beyond them, **one controlled stress test runs the causal path on a real
51
146
  experiment** (below) — a development demonstration on a single run, not an evaluation.
52
147
 
53
- **It makes no performance claim.** No precision, recall or F1 is computed, reported or
54
- shipped anywhere in this package. A test parses the package and fails if one appears. The causal-materiality results presented in the NAFEMS
148
+ **No precision, recall or F1.** None is computed, reported or shipped anywhere in this
149
+ package, and a test parses the package and fails if one appears. The benchmark reports counts
150
+ per dataset, never a rate across datasets. The causal-materiality results presented in the NAFEMS
55
151
  Multiphysics 2026 abstract are **not** reproduced here: the original study's inputs are
56
152
  gone, and the basis for its experimental truth is unresolved. A reconstruction is under
57
153
  way under a locked protocol that fixes its rules before any rebuilt number is examined.
@@ -62,7 +158,11 @@ not reproduction of the published numbers. The protocol defines `REPRODUCED` and
62
158
  unreachable, so the word cannot drift onto a weaker result. See
63
159
  [`protocols/`](https://github.com/cloudronin/physmap/tree/main/protocols).
64
160
 
65
- ## A controlled model-reuse stress test: Lewis 35A
161
+ ## A separate question: causal materiality, in a controlled stress test (Lewis 35A)
162
+
163
+ The benchmark asks whether PhysMAP catches failures the OOD detectors cannot see. This asks
164
+ something else: when a mechanism the surrogate never saw becomes active, does it matter for the
165
+ answer — and where?
66
166
 
67
167
  ```bash
68
168
  physmap stress-test lewis-reuse
@@ -113,55 +213,6 @@ OpenFOAM. Full record:
113
213
  talk package — figures, facts sheet, claims ledger, and a clean-clone reproduction record — is
114
214
  in [docs/talk/](https://github.com/cloudronin/physmap/blob/main/docs/talk/README.md).
115
215
 
116
- ## The benchmark: seven vehicles, all rerunnable
117
-
118
- ```bash
119
- physmap benchmark report # all seven outcomes, each marked recomputed or banked
120
- physmap benchmark run # recomputes all seven from this checkout, diffs against the bank
121
- physmap benchmark coverage # what that subset does and does not cover
122
- ```
123
-
124
- All seven vehicles ran, all seven outcomes are reported, and **all seven now ship their
125
- source data**.
126
-
127
- **`physmap benchmark run` recomputes all seven from this checkout and diffs the result
128
- against the committed matrix.** Every field of every cell, not just the headline outcome.
129
- It exits non-zero if anything drifted, and never writes the bank it is checking itself
130
- against.
131
-
132
- Floats are compared within `1e-9` relative, everything else exactly — and the distinction
133
- is load-bearing rather than a convenience. Every field that decides an outcome is an int
134
- or a string (counts, verdicts, outcome labels, observability classes), so the tolerance
135
- cannot absorb a real change. It exists because a clean-clone check on numpy 2.5 found
136
- `dirker_water.observability_score` differing from the banked value by **one unit in the
137
- last place**. A match that needed the tolerance is reported as such, not as "identical".
138
-
139
- `physmap benchmark report` reads the bank without running anything, and says so — the
140
- report distinguishes a recomputed row from a banked one, and the counts are computed
141
- rather than written down, so they cannot quietly go stale.
142
-
143
- **Shipping is also not licensing.** Two of the seven carry affirmative permission:
144
- `naca_tn1451` (public domain) and `velazquez_sco2` (CC BY 4.0). **Five do not**, and the
145
- registry and report label them rather than calling everything clear:
146
-
147
- - `marineau_hypersonic_transition` — no licence and **no prohibition**. Values transcribed
148
- from a published table in a publicly funded, public-release, government-hosted document.
149
- - `forrest`, `casper_hypersonic_transition`, `dirker_water`, `jin_sco2_buoyancy` —
150
- published **against** express publisher terms. AIAA prohibits using its content to
151
- develop machine-learning models; ASME and Elsevier require permission to reproduce, and
152
- Elsevier's licence forbids systematic redistribution. Risks accepted knowingly, not
153
- findings that the terms do not apply.
154
-
155
- All five redistribute **numbers only** — no paper, figure or PDF, enforced by two audits —
156
- and all five are removed on objection. The full basis, including the arguments against, is
157
- in [data/REDISTRIBUTION.md](https://github.com/cloudronin/physmap/blob/main/data/REDISTRIBUTION.md) and [NOTICE](https://github.com/cloudronin/physmap/blob/main/NOTICE).
158
-
159
- **One dataset is published but not benchmark-grade.** `forrest`'s own header calls its
160
- values visual estimates for triage only, and its cell is degenerate — one training row, no
161
- detector fit — so its `DO_NO_HARM` outcome is short-circuited rather than earned. Being
162
- legal to publish and being fit to benchmark on are different questions; the registry tracks
163
- them on separate axes. `physmap benchmark coverage` prints it.
164
-
165
216
  ## Install
166
217
 
167
218
  ```bash
@@ -185,6 +236,55 @@ redistribution terms and partly because it is not runtime data. From a plain
185
236
  Optional extras: `[jsonld]` adds evidence export via `uofa`, `[experiment]` adds
186
237
  `matplotlib` for benchmark figures, `[dev]` adds the test tooling.
187
238
 
239
+ ## Using the guardrail
240
+
241
+ The guardrail does not guess which checks to combine. Its setup decides.
242
+
243
+ ```python
244
+ from physmap import ColumnMap, CredibilityGuardrail, Regime
245
+
246
+ guard = CredibilityGuardrail(
247
+ surrogate_inputs=["Re", "Pr"], # exactly the inputs your surrogate takes
248
+ regime=Regime.ENTRANCE_REGION_PIPE, # the physics it is used in: picks the closures
249
+ )
250
+ cols = ColumnMap(inputs=["Re", "Pr", "x_over_D"]) # the surrogate inputs, plus every
251
+ # variable those closures put a bound on
252
+ guard.fit(train_X, train_y=train_y, columns=cols) # arrays whose columns follow `cols`
253
+ for a in guard.assess(test_X, columns=cols):
254
+ print(a.verdict.name, a.rationale)
255
+ ```
256
+
257
+ **What each piece does**
258
+
259
+ - **`surrogate_inputs`** — the columns your surrogate actually uses. The input-based OOD
260
+ detectors are fitted on exactly these.
261
+ - **`regime`** — picks the closure relations whose validated ranges the closure check uses.
262
+ Choose the regime that matches your physics, or name one closure with `closure_id=`. If none
263
+ matches, use `Regime.UNLISTED`: the guardrail then runs the OOD detectors alone.
264
+ - **The data columns** — must include every variable those closures bound, even ones the
265
+ surrogate never sees (here `x_over_D`). If one is missing, `fit` stops and names it.
266
+ - **`train_y`** — the training targets. The GP-variance detector needs them.
267
+ - **`detectors=`** — optional. The default is novelty density and GP variance (the input-based
268
+ OOD detectors) plus closure validity. Distance-to-training and a conformal residual check are
269
+ also available.
270
+
271
+ **How the verdict is combined, for each prediction**
272
+
273
+ - At setup, each bounded variable is classed **observable** (it is a surrogate input),
274
+ **unobservable** (it is not), or **partial**. `guard.observability_classification` shows the
275
+ result.
276
+ - The closure check fires on an **unobservable** variable → `REJECT`. The OOD detectors
277
+ cannot see that variable, so the closure check is trusted.
278
+ - It fires on a **partial** variable → a calibrated blend where a calibration exists,
279
+ otherwise `UNCERTAIN`.
280
+ - Otherwise — the closure check is quiet, or the variable is one the detectors can see — the
281
+ OOD detectors decide: none fired → `TRUSTWORTHY`, a reject-level fire → `REJECT`, anything
282
+ else → `WARN`.
283
+
284
+ Every listed regime's closures ship in the published corpus. A complete, runnable version of
285
+ the example above is
286
+ [`examples/naca_entrance_region.py`](https://github.com/cloudronin/physmap/blob/main/examples/naca_entrance_region.py).
287
+
188
288
  ## The corpus
189
289
 
190
290
  The code is MIT and the corpus data is CC BY 4.0 — see the licences below.
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "physmap"
9
- version = "0.2.1"
9
+ version = "0.2.3"
10
10
  description = "Physics-aware credibility checks for AI surrogates in multiphysics simulation: closure validity, surrogate observability, and causal materiality."
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
@@ -27,7 +27,7 @@ from physmap.release import (
27
27
  ReleaseState,
28
28
  )
29
29
 
30
- __version__ = "0.2.1"
30
+ __version__ = "0.2.3"
31
31
 
32
32
 
33
33
  # The CredibilityGuardrail public surface is re-exported LAZILY (PEP 562). A bare
@@ -103,6 +103,34 @@ def coverage_note() -> str:
103
103
  return "\n".join(lines)
104
104
 
105
105
 
106
+ def _what_physmap_adds(cells: list[dict | None]) -> list[str]:
107
+ """Per vehicle, at the shipped reference operating percentile: the wrong predictions the
108
+ closure-validity check caught while the input-based detectors stayed quiet, and the right
109
+ predictions it flagged anyway. Row counts, never pooled into a rate across vehicles."""
110
+ pcts = {int(c["ref_pct"]) for c in cells if c is not None and c.get("ref_pct")}
111
+ at = f"percentile {', '.join(str(p) for p in sorted(pcts))}" if pcts else "percentile"
112
+ out = [
113
+ "What the closure-validity check adds to the input-based detectors",
114
+ f" at the reference operating {at}. Counts are rows of each dataset, not",
115
+ " independent cases, and the datasets are not pooled.",
116
+ "",
117
+ f" {'vehicle':34} {'failure variable':17} {'wrong: caught only by PhysMAP':31} "
118
+ f"right: flagged anyway",
119
+ ]
120
+ for cell in cells:
121
+ if cell is None:
122
+ continue
123
+ ref = (cell.get("per_pct") or {}).get(str(int(cell.get("ref_pct") or 0)))
124
+ head = f" {cell['vehicle_id']:34} {cell['failure_observability']:17} "
125
+ if not ref:
126
+ out.append(head + f"not tested -- {cell.get('n_train', 0)} training row(s), "
127
+ f"no detector fit")
128
+ continue
129
+ caught = f"{ref['clean_lift']} of {ref['n_wrong']}"
130
+ out.append(head + f"{caught:31} {ref['misaligned']}")
131
+ return out
132
+
133
+
106
134
  def render_report(rerun_results: dict[str, Any] | None = None) -> str:
107
135
  """The full seven-vehicle table.
108
136
 
@@ -165,6 +193,9 @@ def render_report(rerun_results: dict[str, Any] | None = None) -> str:
165
193
  out.append("")
166
194
  out.append(f"observability guards: {'all passed' if matrix['all_guards_passed'] else 'FAILED'}")
167
195
  out.append("")
196
+ out.extend(_what_physmap_adds(
197
+ [rerun_results.get(v.vehicle_id) or cells.get(v.vehicle_id) for v in VEHICLES]))
198
+ out.append("")
168
199
  unlicensed = unlicensed_shipped_ids()
169
200
  if unlicensed:
170
201
  out.append("Redistribution basis -- read this before reusing any of this data")