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,313 @@
1
+ """Closure registry — the keyed `{closure_id -> ClosureEntry}` map.
2
+
3
+ Each entry binds the corpus `closure_id` (the slug used in
4
+ `results/calibration_corpus/corpus.jsonl`) to:
5
+ - the executable formula (`fn`)
6
+ - the inputs the formula needs (`required_inputs`) — keys the substrate
7
+ engine populates from the row
8
+ - the geometry class the formula was derived for (`geometry_class`) — the
9
+ invariant compares this to `VehicleConfig.geometry.class_`
10
+ - cached validity bounds (`re_range`, `pr_range`, `ra_range`) — mirror the
11
+ calibration table; corpus.jsonl remains the source of truth
12
+ - the bound status (`status`) — mirrors `bound_status` in the calibration
13
+ table (`confirmed`, `claimed`, `extrapolated`, `confirmed-contested`),
14
+ plus the local `not-in-corpus` sentinel for reference-only formulas
15
+ that have no corpus entry (Petukhov, Churchill blend).
16
+
17
+ The registry is constructed at import time. Its keys MUST be a subset of
18
+ the calibration corpus closure_ids, except for entries marked
19
+ `status="not-in-corpus"`.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass, field
25
+ from typing import Callable
26
+
27
+ from physmap.closures.formulas import (
28
+ modified_sparrow_cur_nu,
29
+ gnielinski_nu,
30
+ dittus_boelter_nu,
31
+ petukhov_nu,
32
+ sieder_tate_nu,
33
+ pohlhausen_forced_local_nu,
34
+ mcadams_natural_local_nu,
35
+ churchill_mixed_nu,
36
+ aung_worku_mixed_nu,
37
+ pate_freestream_noise_bound,
38
+ marineau_entropy_shock_bound,
39
+ )
40
+ from physmap.closures.geometry_classes import (
41
+ NARROW_RECT_CHANNEL_ONE_SIDED,
42
+ CIRCULAR_PIPE,
43
+ FLAT_PLATE_EXTERNAL_FORCED,
44
+ VERTICAL_PLATE_EXTERNAL_NATURAL,
45
+ FLAT_PLATE_EXTERNAL_MIXED,
46
+ HYPERSONIC_SHARP_CONE,
47
+ HYPERSONIC_BLUNT_CONE,
48
+ assert_known_geometry,
49
+ )
50
+
51
+
52
+ # Bound-status vocabulary mirrors the corpus calibration table, plus one
53
+ # local sentinel for closures whose formula we ship but whose calibration
54
+ # bounds are not yet in corpus.jsonl. The ClosureValidityDetector skips
55
+ # entries with status="not-in-corpus" (no validity rectangle to test against).
56
+ ClosureStatus = str # "confirmed" | "claimed" | "extrapolated" | "confirmed-contested" | "not-in-corpus"
57
+
58
+ # Sentinel for "this formula has no calibration entry" — kept in registry
59
+ # only so the substrate engine can compute it for reference predictions.
60
+ STATUS_NOT_IN_CORPUS = "not-in-corpus"
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class ClosureEntry:
65
+ """One executable closure, bridged to its corpus calibration entry."""
66
+
67
+ closure_id: str
68
+ fn: Callable[..., object]
69
+ required_inputs: tuple[str, ...] # keys substrate must pass as kwargs
70
+ geometry_class: str # exact string from geometry_classes.py
71
+ status: ClosureStatus # mirrors corpus bound_status (or sentinel)
72
+ re_range: tuple[float, float] | None = None # cached from corpus calibration
73
+ pr_range: tuple[float, float] | None = None
74
+ ra_range: tuple[float, float] | None = None # for natural-convection closures
75
+ ri_range: tuple[float, float] | None = None # for mixed-convection (richardson_number) anchors
76
+ bound_range: tuple[float, float] | None = None # generic validated band for a non-(Re/Pr/Ra/Ri) bound
77
+ # coord (freestream-noise %, S_T/X_SW); mirrors corpus
78
+ note: str = "" # optional human-readable rationale
79
+
80
+ def __post_init__(self) -> None:
81
+ assert_known_geometry(self.geometry_class)
82
+ if not self.closure_id:
83
+ raise ValueError("ClosureEntry.closure_id must be non-empty.")
84
+ if not self.required_inputs:
85
+ raise ValueError(
86
+ f"ClosureEntry {self.closure_id!r} declared no required_inputs; "
87
+ f"the substrate engine cannot dispatch the formula."
88
+ )
89
+
90
+
91
+ # ── REGISTRY ─────────────────────────────────────────────────────────────────
92
+ # Keys MUST match the slug in results/calibration_corpus/corpus.jsonl, except
93
+ # for STATUS_NOT_IN_CORPUS entries (Petukhov, Churchill) which have no corpus
94
+ # row. Ranges below are CACHED from the corpus calibration table and must be
95
+ # kept in sync; the calibration table is the source of truth, the registry is
96
+ # the executable bridge.
97
+
98
+ REGISTRY: dict[str, ClosureEntry] = {
99
+
100
+ # ── narrow_rect_channel_one_sided (Forrest matched) ──────────────────────
101
+ "modified-sparrow-cur-asym-narrow-rect-channel-2014": ClosureEntry(
102
+ closure_id="modified-sparrow-cur-asym-narrow-rect-channel-2014",
103
+ fn=modified_sparrow_cur_nu,
104
+ required_inputs=("Re", "Pr"),
105
+ geometry_class=NARROW_RECT_CHANNEL_ONE_SIDED,
106
+ status="extrapolated",
107
+ re_range=(10000.0, 70000.0),
108
+ pr_range=(2.2, 5.4),
109
+ note="Forrest 2014 Eq. 7. Matched closure for Forrest narrow-rect mini-channel.",
110
+ ),
111
+
112
+ # ── circular_pipe (Gnielinski/D-B/Sieder-Tate canonical triple) ──────────
113
+ "gnielinski-1976": ClosureEntry(
114
+ closure_id="gnielinski-1976",
115
+ fn=gnielinski_nu,
116
+ required_inputs=("Re", "Pr"),
117
+ geometry_class=CIRCULAR_PIPE,
118
+ status="confirmed",
119
+ re_range=(3000.0, 5.0e6),
120
+ pr_range=(0.5, 2000.0),
121
+ note="Gnielinski 1976. Canonical turbulent circular-pipe Nu.",
122
+ ),
123
+ "gnielinski-constprop-sco2": ClosureEntry(
124
+ closure_id="gnielinski-constprop-sco2",
125
+ fn=gnielinski_nu,
126
+ required_inputs=("Re", "Pr"),
127
+ geometry_class=CIRCULAR_PIPE,
128
+ status="claimed",
129
+ re_range=(3000.0, 5.0e6),
130
+ pr_range=(0.5, 2000.0),
131
+ note=("Constant-property Gnielinski applied to supercritical CO2 (SAME Nu "
132
+ "formula as gnielinski-1976 -> identical surrogate prediction). "
133
+ "REGIME-SPECIFIC entry so its corpus validated_range can carry a "
134
+ "wall/bulk viscosity-ratio (property-variation) validity bound WITHOUT "
135
+ "mutating the shared gnielinski-1976 entry used by NACA/Forrest "
136
+ "(the dirker lesson). Velazquez sCO2 property-variation vehicle."),
137
+ ),
138
+ "dittus-boelter-1930": ClosureEntry(
139
+ closure_id="dittus-boelter-1930",
140
+ fn=dittus_boelter_nu,
141
+ required_inputs=("Re", "Pr"),
142
+ geometry_class=CIRCULAR_PIPE,
143
+ status="claimed",
144
+ re_range=(10000.0, 1.2e6),
145
+ pr_range=(0.7, 160.0),
146
+ note="McAdams form of Dittus-Boelter. Heating default; cooling via heating=False kwarg.",
147
+ ),
148
+ "dittus-boelter-buoyancy-sco2": ClosureEntry(
149
+ closure_id="dittus-boelter-buoyancy-sco2",
150
+ fn=dittus_boelter_nu,
151
+ required_inputs=("Re", "Pr"),
152
+ geometry_class=CIRCULAR_PIPE,
153
+ status="claimed",
154
+ re_range=(10000.0, 1.2e6),
155
+ pr_range=(0.5, 2000.0),
156
+ note=("Constant-property Dittus-Boelter applied to supercritical CO2 (SAME Nu formula as "
157
+ "dittus-boelter-1930 -> identical surrogate prediction). REGIME-SPECIFIC entry so its "
158
+ "corpus validated_range can carry the Liu buoyancy-parameter (Bu) validity bound WITHOUT "
159
+ "mutating the shared dittus-boelter-1930 entry (the dirker/velazquez lesson). Re/Pr are the "
160
+ "broad sCO2 turbulent envelope (Pr spikes near T_pc); the buoyancy bound Bu<=1.3e-5 is the "
161
+ "discriminator. Jin et al. 2023 sCO2 vertical-tube buoyancy vehicle (Liu Bu, Eq. 21)."),
162
+ ),
163
+ "sieder-tate-1936": ClosureEntry(
164
+ closure_id="sieder-tate-1936",
165
+ fn=sieder_tate_nu,
166
+ required_inputs=("Re", "Pr"),
167
+ geometry_class=CIRCULAR_PIPE,
168
+ status="claimed",
169
+ re_range=(10000.0, 1.2e6),
170
+ pr_range=(0.7, 17000.0),
171
+ note="Includes mu_b/mu_w correction (defaults to 1.0 if not supplied).",
172
+ ),
173
+ "petukhov-1970": ClosureEntry(
174
+ closure_id="petukhov-1970",
175
+ fn=petukhov_nu,
176
+ required_inputs=("Re", "Pr"),
177
+ geometry_class=CIRCULAR_PIPE,
178
+ status="claimed",
179
+ re_range=(10000.0, 5.0e6),
180
+ pr_range=(0.5, 2000.0),
181
+ note=("Petukhov 1970 turbulent circular-pipe Nu. Added to corpus.jsonl "
182
+ "as part of evidence_corpus v0.1 (commit 2897392); the registry "
183
+ "entry was previously tagged not-in-corpus."),
184
+ ),
185
+
186
+ # ── circular_pipe (mixed-convection richardson_number anchor) ────────────
187
+ # Corpus-validity anchor for the Stage-3 MIDDLE vehicle (Dirker/Meyer/Reid
188
+ # water tube). main's regime framework maps MIXED_CONVECTION_HORIZONTAL_TUBE
189
+ # -> this closure; it carries the richardson_number bound (Ri in [0.1, 10]).
190
+ # No executable Nu predictor (the vehicle's surrogate is a data-driven GP);
191
+ # registered so the geometry-match invariant + matched lookup resolve. Fills
192
+ # the registry gap noted in the regime->observability mapping work.
193
+ "aung-worku-mixed-convection-1986": ClosureEntry(
194
+ closure_id="aung-worku-mixed-convection-1986",
195
+ fn=aung_worku_mixed_nu,
196
+ required_inputs=("Re", "Pr"),
197
+ geometry_class=CIRCULAR_PIPE,
198
+ status="claimed",
199
+ ri_range=(0.1, 10.0),
200
+ note=("Mixed forced-natural convection richardson_number anchor "
201
+ "(Ri in [0.1, 10]); corpus-validity bound only, Nu predictor not "
202
+ "implemented (Stage-3 dirker_water uses a GP surrogate)."),
203
+ ),
204
+
205
+ # ── hypersonic_sharp_cone (Casper freestream-noise transition anchor) ────
206
+ # Corpus-validity anchor for the aerospace PHYSMAP_WINS vehicle (Casper
207
+ # quiet-vs-noisy hypersonic transition). Carries the freestream-noise bound
208
+ # (Pate-Stainback: conventional tunnels validated for RMS Pitot >= ~0.5%);
209
+ # fires on the flight-like QUIET deploy (~0.05%). No executable predictor
210
+ # (Casper uses a GP transition surrogate); registered so the geometry-match
211
+ # invariant + matched lookup resolve.
212
+ "pate-stainback-freestream-noise-hypersonic-1980": ClosureEntry(
213
+ closure_id="pate-stainback-freestream-noise-hypersonic-1980",
214
+ fn=pate_freestream_noise_bound,
215
+ required_inputs=("freestream_noise_pct",),
216
+ geometry_class=HYPERSONIC_SHARP_CONE,
217
+ status="claimed",
218
+ bound_range=(0.5, 10.0), # freestream_noise_rms_pitot_pct band (mirrors corpus.jsonl)
219
+ note=("Pate & Stainback freestream-disturbance transition correlation; "
220
+ "corpus carries the freestream_noise_rms_pitot_pct bound. Corpus-"
221
+ "validity anchor only — Casper uses a GP surrogate (loader "
222
+ "casper_hypersonic_transition)."),
223
+ ),
224
+
225
+ # ── hypersonic_blunt_cone (Marineau entropy-layer/shock anchor) ──────────
226
+ # Corpus-validity anchor for the aerospace NEGATIVE-CONTROL vehicle
227
+ # (Marineau bluntness). Carries the entropy-layer/shock-interaction bound
228
+ # (S_T/X_SW >= 0.1 for the e^N / 2nd-mode regime); fires on large-bluntness
229
+ # deploy — but the steelman baseline ALSO fires (nose radius is a surrogate
230
+ # input) so PhysMAP correctly declines. No executable predictor; registered
231
+ # for the geometry-match invariant.
232
+ "marineau-entropy-layer-shock-interaction-2014": ClosureEntry(
233
+ closure_id="marineau-entropy-layer-shock-interaction-2014",
234
+ fn=marineau_entropy_shock_bound,
235
+ required_inputs=("st_xsw_ratio",),
236
+ geometry_class=HYPERSONIC_BLUNT_CONE,
237
+ status="claimed",
238
+ bound_range=(0.1, 1.0e9), # entropy_layer_shock_ratio band (mirrors corpus.jsonl)
239
+ note=("Marineau et al. (2014, SAND2014-4326C) entropy-layer/shock-wave "
240
+ "interaction transition boundary (S_T/X_SW >= 0.1); corpus carries "
241
+ "the entropy_layer_shock_ratio bound. Corpus-validity anchor only — "
242
+ "Marineau is the baseline-visible negative control (nose radius is "
243
+ "a surrogate input)."),
244
+ ),
245
+
246
+ # ── flat_plate_external_forced (Lance & Smith Pohlhausen) ────────────────
247
+ "blasius-pohlhausen-flat-plate-forced-1921": ClosureEntry(
248
+ closure_id="blasius-pohlhausen-flat-plate-forced-1921",
249
+ fn=pohlhausen_forced_local_nu,
250
+ required_inputs=("Re", "Pr"),
251
+ geometry_class=FLAT_PLATE_EXTERNAL_FORCED,
252
+ status="confirmed",
253
+ re_range=(0.0, 5.0e5),
254
+ pr_range=(0.6, 50.0),
255
+ note=("Local form Nu_x = 0.332 Re_x^(1/2) Pr^(1/3). Substrate engine "
256
+ "passes Re_x as the `Re` kwarg per row."),
257
+ ),
258
+
259
+ # ── vertical_plate_external_natural (Lance & Smith McAdams) ──────────────
260
+ "mcadams-vertical-plate-natural-1954": ClosureEntry(
261
+ closure_id="mcadams-vertical-plate-natural-1954",
262
+ fn=mcadams_natural_local_nu,
263
+ required_inputs=("Ra",),
264
+ geometry_class=VERTICAL_PLATE_EXTERNAL_NATURAL,
265
+ status="confirmed",
266
+ re_range=None,
267
+ pr_range=(0.6, 7.0),
268
+ ra_range=(1.0e4, 1.0e9),
269
+ note=("Ra_x = Gr_x * Pr. Substrate engine passes per-row Ra_x as the "
270
+ "`Ra` kwarg."),
271
+ ),
272
+
273
+ # ── flat_plate_external_mixed (Lance & Smith matched closure) ────────────
274
+ # Churchill blend itself isn't in corpus; it's a Nu(Nu_F, Nu_N) combinator.
275
+ # We register it as the matched closure for the L&S vehicle so the geometry
276
+ # invariant has something to compare against, but flag status accordingly.
277
+ "churchill-mixed-convection-flat-plate": ClosureEntry(
278
+ closure_id="churchill-mixed-convection-flat-plate",
279
+ fn=churchill_mixed_nu,
280
+ required_inputs=("Nu_forced", "Nu_natural"),
281
+ geometry_class=FLAT_PLATE_EXTERNAL_MIXED,
282
+ status=STATUS_NOT_IN_CORPUS,
283
+ re_range=None,
284
+ pr_range=None,
285
+ note=("Mixed-convection blend Nu_M^n = Nu_F^n + Nu_N^n (n=3, assisting). "
286
+ "Not a primary closure in the corpus; composes Pohlhausen + McAdams. "
287
+ "Kept as L&S matched closure so the geometry invariant works."),
288
+ ),
289
+ }
290
+
291
+
292
+ def get_closure(closure_id: str) -> ClosureEntry:
293
+ """Look up a closure by id; raise KeyError with a helpful message on miss."""
294
+ try:
295
+ return REGISTRY[closure_id]
296
+ except KeyError as exc:
297
+ raise KeyError(
298
+ f"Unknown closure_id {closure_id!r}. "
299
+ f"Known: {sorted(REGISTRY.keys())}. "
300
+ f"If you're adding a new closure, register it in "
301
+ f"physmap/closures/registry.py with its formula in formulas.py."
302
+ ) from exc
303
+
304
+
305
+ def closure_ids_for_geometry(geometry_class: str) -> list[str]:
306
+ """Return all closure_ids registered for a given geometry class.
307
+
308
+ Used by the substrate engine and tests to confirm at least one matched
309
+ closure exists for a vehicle's geometry, and by diagnostics.
310
+ """
311
+ assert_known_geometry(geometry_class)
312
+ return sorted(cid for cid, entry in REGISTRY.items()
313
+ if entry.geometry_class == geometry_class)
File without changes
File without changes
@@ -0,0 +1,69 @@
1
+ """A mechanism and its calibration window. Domain-neutral on purpose.
2
+
3
+ Nothing here mentions Nusselt numbers, Reynolds numbers or heat transfer. The causal
4
+ method is a statement about mechanisms, quantities of interest and calibration windows;
5
+ the moment a heat-transfer field appears in this layer, the method stops being general
6
+ and starts being a convection tool wearing a general name.
7
+
8
+ Adapted from the substrate ingest in the monorepo, with the heat-transfer fields
9
+ removed and `contribution` deliberately dropped -- see physmap.materiality for why a
10
+ contribution fraction is not a materiality.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+
17
+ __all__ = ["CalibrationWindow", "Mechanism"]
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class CalibrationWindow:
22
+ """The range of a governing variable over which a mechanism was calibrated.
23
+
24
+ An open end is None, not an infinity: "no upper bound was stated" and "the upper
25
+ bound is very large" are different claims about the evidence.
26
+ """
27
+
28
+ variable: str
29
+ low: float | None = None
30
+ high: float | None = None
31
+
32
+ def __post_init__(self) -> None:
33
+ if self.low is not None and self.high is not None and self.low > self.high:
34
+ raise ValueError(
35
+ f"calibration window for {self.variable!r} is inverted: "
36
+ f"low={self.low} > high={self.high}"
37
+ )
38
+
39
+ def contains(self, value: float) -> bool:
40
+ if self.low is not None and value < self.low:
41
+ return False
42
+ if self.high is not None and value > self.high:
43
+ return False
44
+ return True
45
+
46
+ def describe(self) -> str:
47
+ if self.low is None and self.high is None:
48
+ return f"{self.variable} (no stated bound)"
49
+ if self.low is None:
50
+ return f"{self.variable} <= {self.high:g}"
51
+ if self.high is None:
52
+ return f"{self.variable} >= {self.low:g}"
53
+ return f"{self.low:g} <= {self.variable} <= {self.high:g}"
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class Mechanism:
58
+ """One mechanism at one operating point."""
59
+
60
+ mechanism_id: str
61
+ name: str
62
+ window: CalibrationWindow
63
+ operating_value: float
64
+
65
+ def in_calibration(self) -> bool:
66
+ return self.window.contains(self.operating_value)
67
+
68
+ def outside_calibration(self) -> bool:
69
+ return not self.in_calibration()
@@ -0,0 +1,50 @@
1
+ """Signal kinds. The no-conflation rule, made structural.
2
+
3
+ Four different questions get asked about a prediction, and they rest on different
4
+ evidence. Mixing them up is the failure this project exists to prevent, so a signal
5
+ cannot be constructed without saying which kind it is, and nothing aggregates two
6
+ kinds into a single number.
7
+
8
+ closure_validity Is a closure being used outside its calibrated range?
9
+ observability Can the surrogate's inputs represent the governing variable?
10
+ causal_materiality Is the out-of-range mechanism big enough to matter for the QoI?
11
+ statistical_baseline Does an input-space novelty detector fire?
12
+
13
+ Each carries its own threshold and its own rationale. A `causal_materiality` signal is
14
+ never evidence for an `observability` claim, and the reverse is equally false.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from enum import Enum
21
+
22
+ __all__ = ["SignalKind", "Signal"]
23
+
24
+
25
+ class SignalKind(str, Enum):
26
+ CLOSURE_VALIDITY = "closure_validity"
27
+ OBSERVABILITY = "observability"
28
+ CAUSAL_MATERIALITY = "causal_materiality"
29
+ STATISTICAL_BASELINE = "statistical_baseline"
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class Signal:
34
+ """One signal of one kind. `value` and `threshold` are None when the signal could
35
+ not be computed -- which is different from computing it and getting zero."""
36
+
37
+ kind: SignalKind
38
+ fired: bool
39
+ rationale: str
40
+ value: float | None = None
41
+ threshold: float | None = None
42
+
43
+ def __post_init__(self) -> None:
44
+ if not isinstance(self.kind, SignalKind):
45
+ raise TypeError(f"kind must be a SignalKind, got {type(self.kind).__name__}")
46
+ if self.fired and self.value is None:
47
+ raise ValueError(
48
+ f"{self.kind.value} signal claims to have fired with no value. A signal "
49
+ f"that could not be computed must not fire."
50
+ )
@@ -0,0 +1,12 @@
1
+ """Corpus side of the architecture refactor — calibration table + claim-centric
2
+ evidence corpus.
3
+
4
+ Module renames during R5 (dropping the redundant _corpus suffix):
5
+ calibration_corpus.py -> calibration.py
6
+ evidence_corpus.py -> evidence.py
7
+
8
+ The two corpora live in separate JSONL files under
9
+ `physmap/results/calibration_corpus/` and `physmap/results/evidence_corpus/`;
10
+ they are JOINED at closure_id in downstream consumers (the validity
11
+ detector + the EvidenceEnrichmentStage).
12
+ """