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,216 @@
1
+ """Config-time observability weighting heuristic — the derived (not tuned) rule.
2
+
3
+ Spec: docs/specs/PhysMAP_Ensemble_ConfigTime_Observability_Heuristic_Spec_v0_1.md
4
+
5
+ Given ONLY config-time-legal inputs — the surrogate's declared inputs, the regime's
6
+ resolved closures + their bound-variables, the Layer-2a observability_class, and (when
7
+ a real vehicle has calibrated it) the Layer-2c partial_degree — assign each bound
8
+ variable a detector-weight decision:
9
+
10
+ OBSERVABLE → trust the BASELINE (failure axis is a surrogate input; corpus redundant)
11
+ UNOBSERVABLE → trust the CORPUS (baseline structurally blind; its silence is *expected*)
12
+ PARTIAL → depends on degree_status:
13
+ uncalibrated, no lean → DEFER (honest UNCERTAIN; no weight invented)
14
+ uncalibrated + lean → LEAN (qualitative physics default, NOT a measured weight)
15
+ calibrated → GRADED (interpolate from the calibrated Layer-2c degree)
16
+
17
+ The poles are fully determined from structure today; the PARTIAL middle is honestly
18
+ deferred until a real vehicle calibrates its (regime, variable) degree, then graduates
19
+ to a graded weight WITHOUT any rule change — the same heuristic just reads a richer corpus.
20
+
21
+ DISCIPLINE (load-bearing): this module NEVER computes or estimates an observability
22
+ score/degree. The graded weight comes ONLY from a calibrated Layer-2c degree read via
23
+ `partial_degree_for()`; deriving a degree from structural inputs would fabricate an
24
+ empirical quantity (the same error the mapping spec forbids). There is deliberately NO
25
+ import of the cv_r2_knn estimator (`pipeline.observability`) anywhere in this module.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from dataclasses import dataclass
31
+ from typing import Sequence
32
+
33
+ from physmap.corpus.calibration import ClosureEntry
34
+ from physmap.guardrail.classify import coord_input_aliases, load_default_corpus_index
35
+ from physmap.guardrail.corpus_regimes import (
36
+ KNOWN_PARTIAL,
37
+ REGIME_TO_CLOSURES,
38
+ observability_class_for,
39
+ partial_degree_for,
40
+ )
41
+ from physmap.guardrail.enums import Observability, Regime
42
+
43
+
44
+ # Half-width of the near-pole "collapse" zone on the observability axis. A JUDGMENT
45
+ # CHOICE, not a fitted/tuned number: a calibrated degree within POLE_BAND of a pole is
46
+ # treated AS that pole (corpus-trust near 0, baseline-trust near 1); strictly between,
47
+ # the corpus fire is a calibrated SOFT-flag whose weight is scaled by (1 - degree).
48
+ # Chosen deliberately coarse (0.15) so a single mid-range calibration cannot masquerade
49
+ # as a hard pole; it is the one knob here and is meant to be revisited (not re-fit) as
50
+ # the corpus accumulates more calibrated degrees.
51
+ POLE_BAND = 0.15
52
+
53
+ # weight_target vocabulary — the per-variable decision the aggregator consumes.
54
+ WEIGHT_BASELINE = "baseline"
55
+ WEIGHT_CORPUS = "corpus"
56
+ WEIGHT_DEFER = "defer"
57
+ WEIGHT_LEAN = "lean"
58
+ WEIGHT_GRADED = "graded"
59
+
60
+ # graded sub-lean — which pole behavior a calibrated degree resolves to.
61
+ LEAN_CORPUS_TRUST = "corpus-trust"
62
+ LEAN_BASELINE_TRUST = "baseline-trust"
63
+ LEAN_SOFT_FLAG = "soft-flag"
64
+
65
+ # Structural-lean hook (spec-optional). A qualitative physics-knowledge default for an
66
+ # UNCALIBRATED known-partial cell — explicitly a LEAN pending confirmation, never a
67
+ # measured weight, and overridden the instant a calibrated degree lands. EMPTY today
68
+ # (no lean encoded) → the heuristic defers. When a real lean is authored it belongs
69
+ # alongside the other corpus prerequisites in corpus_regimes.py.
70
+ STRUCTURAL_LEAN_OVERRIDES: dict[tuple[str, str], str] = {}
71
+
72
+
73
+ def structural_lean_for(closure_id: str, coord: str) -> str | None:
74
+ """The qualitative physics-knowledge lean for an uncalibrated known-partial cell,
75
+ or None (→ DEFER). Never a measured weight."""
76
+ return STRUCTURAL_LEAN_OVERRIDES.get((closure_id, coord))
77
+
78
+
79
+ def _clamp01(x: float) -> float:
80
+ return 0.0 if x < 0.0 else 1.0 if x > 1.0 else float(x)
81
+
82
+
83
+ def graded_weights(degree: float) -> tuple[float, float]:
84
+ """(w_baseline, w_corpus) for a calibrated partial degree.
85
+
86
+ POLARITY (load-bearing — guard against sign inversion): the observability degree
87
+ runs 0 = baseline blind (unobservable end) → 1 = baseline sees the axis (observable
88
+ end). So a HIGHER degree means MORE baseline weight:
89
+
90
+ w_baseline = degree, w_corpus = 1 - degree
91
+
92
+ At the endpoints this reproduces the poles exactly:
93
+ graded_weights(0.0) == (0.0, 1.0) # all corpus (unobservable)
94
+ graded_weights(1.0) == (1.0, 0.0) # all baseline (observable)
95
+ """
96
+ d = _clamp01(degree)
97
+ return d, 1.0 - d
98
+
99
+
100
+ def graded_lean(degree: float) -> str:
101
+ """Which pole behavior a calibrated degree resolves to (the continuous-blend rule).
102
+
103
+ corpus-trust near the unobservable pole (degree <= POLE_BAND), baseline-trust near
104
+ the observable pole (degree >= 1 - POLE_BAND), else a calibrated soft-flag in between.
105
+ """
106
+ d = _clamp01(degree)
107
+ if d <= POLE_BAND:
108
+ return LEAN_CORPUS_TRUST
109
+ if d >= 1.0 - POLE_BAND:
110
+ return LEAN_BASELINE_TRUST
111
+ return LEAN_SOFT_FLAG
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class BoundWeightDecision:
116
+ """The config-time weight decision for one regime bound-variable. Inspectable: it
117
+ carries the class, the target, the (poles + graded) weights, the degree_status, and
118
+ the provenance, so the derived rule can be audited end-to-end."""
119
+ closure_id: str
120
+ coord: str
121
+ observability: Observability
122
+ weight_target: str # baseline | corpus | defer | lean | graded
123
+ w_baseline: float | None # set for poles + graded; None for defer/lean
124
+ w_corpus: float | None
125
+ degree_status: str # n/a | uncalibrated | calibrated
126
+ partial_degree: float | None # regime-resolved calibrated degree (graded only)
127
+ structural_lean: str | None # corpus_lean | baseline_lean | neutral (lean target only)
128
+ calibrated_by: tuple[str, ...]
129
+ rationale: str
130
+
131
+
132
+ def resolve_partial(
133
+ closure_id: str, coord: str, *, regime_value: str | None = None,
134
+ ) -> BoundWeightDecision:
135
+ """Weight decision for a PARTIAL (known-partial, absent) bound — the ONLY branch
136
+ that consults Layer-2c. Reads `partial_degree_for()`; never derives a degree.
137
+
138
+ The calibrated degree is regime-resolved: a degree calibrated for one regime must
139
+ not weight a deployment in a different regime, so a calibrated cell with no entry
140
+ for `regime_value` falls back to the honest uncalibrated behavior for this deployment.
141
+ """
142
+ degree_map, degree_status, calibrated_by = partial_degree_for(closure_id, coord)
143
+
144
+ degree: float | None = None
145
+ if degree_status == "calibrated" and degree_map:
146
+ degree = degree_map.get(regime_value) if regime_value is not None else None
147
+ if degree is None:
148
+ degree_status, calibrated_by = "uncalibrated", [] # not calibrated for THIS regime
149
+
150
+ if degree_status == "calibrated" and degree is not None:
151
+ w_b, w_c = graded_weights(degree)
152
+ lean = graded_lean(degree)
153
+ return BoundWeightDecision(
154
+ closure_id, coord, Observability.PARTIAL, WEIGHT_GRADED,
155
+ w_b, w_c, "calibrated", float(degree), None, tuple(calibrated_by),
156
+ f"partial graded: degree={degree:g} → (w_baseline={w_b:g}, w_corpus={w_c:g}), "
157
+ f"{lean}; calibrated_by={list(calibrated_by)}",
158
+ )
159
+
160
+ lean = structural_lean_for(closure_id, coord)
161
+ if lean is not None:
162
+ return BoundWeightDecision(
163
+ closure_id, coord, Observability.PARTIAL, WEIGHT_LEAN,
164
+ None, None, "uncalibrated", None, lean, (),
165
+ f"partial uncalibrated: applying labeled structural_lean={lean!r} "
166
+ f"(qualitative physics default, not a measured weight)",
167
+ )
168
+ return BoundWeightDecision(
169
+ closure_id, coord, Observability.PARTIAL, WEIGHT_DEFER,
170
+ None, None, "uncalibrated", None, None, (),
171
+ "partial uncalibrated: DEFER (UNCERTAIN) — no calibrated degree, no weight invented",
172
+ )
173
+
174
+
175
+ def config_time_weights(
176
+ surrogate_inputs: Sequence[str],
177
+ regime: Regime,
178
+ corpus_index: dict[str, ClosureEntry] | None = None,
179
+ ) -> list[BoundWeightDecision]:
180
+ """The full per-bound weight table for a deployment — the inspectable derived rule.
181
+
182
+ Mirrors `classify_observability`'s structural step (same `coord_input_aliases` +
183
+ `observability_class_for` primitives, so the OBSERVABLE/PARTIAL/UNOBSERVABLE split
184
+ cannot diverge from the canonical classifier) and additionally attaches the weight
185
+ decision + provenance per bound. PARTIAL delegates to `resolve_partial` (the only
186
+ Layer-2c read). UNLISTED (or no resolved closures) → [] → statistical-only.
187
+ """
188
+ if corpus_index is None:
189
+ corpus_index = load_default_corpus_index()
190
+ out: list[BoundWeightDecision] = []
191
+ if regime is Regime.UNLISTED:
192
+ return out
193
+ inputs = set(surrogate_inputs)
194
+ for closure_id in REGIME_TO_CLOSURES.get(regime, ()):
195
+ entry = corpus_index.get(closure_id)
196
+ if entry is None:
197
+ continue
198
+ for bound in entry.validated_range:
199
+ coord = bound.coord
200
+ if any(a in inputs for a in coord_input_aliases(coord)):
201
+ out.append(BoundWeightDecision(
202
+ closure_id, coord, Observability.OBSERVABLE, WEIGHT_BASELINE,
203
+ 1.0, 0.0, "n/a", None, None, (),
204
+ "observable: failure axis is a surrogate input → baseline sees the "
205
+ "failure region; corpus redundant",
206
+ ))
207
+ elif observability_class_for(closure_id, coord) == KNOWN_PARTIAL:
208
+ out.append(resolve_partial(closure_id, coord, regime_value=regime.value))
209
+ else:
210
+ out.append(BoundWeightDecision(
211
+ closure_id, coord, Observability.UNOBSERVABLE, WEIGHT_CORPUS,
212
+ 0.0, 1.0, "n/a", None, None, (),
213
+ "unobservable: failure axis absent + structural-binary → baseline "
214
+ "blind; trust corpus (its silence is expected, not reassuring)",
215
+ ))
216
+ return out
@@ -0,0 +1,23 @@
1
+ """Cross-cutting infrastructure shared across the architecture refactor and
2
+ the broader physmap system. Grouped under one subpackage during R7 of
3
+ the cleanup.
4
+
5
+ Modules:
6
+ corpus_runtime — Entry + corpus_error_magnitude (used by
7
+ pipeline.detectors.CorpusDetector)
8
+ gate_runner — generic gate-running orchestration
9
+ gates — gate definitions
10
+ ledger — append-only run ledger (12 consumers — load-bearing)
11
+ metrics — generic metrics helpers
12
+ physics_causal — physics-side causal/non-causal split
13
+ physics_synth — physics synthesis
14
+ schema — shared schema definitions
15
+ stage1_adapter — Stage-1 adapter
16
+ stage1_harness — Stage-1 harness
17
+
18
+ These modules are kept INSIDE the package (not archived) because they
19
+ have widely-distributed consumers. The infra subpackage isolates them
20
+ from the architecture refactor's canonical surface so a reader can tell
21
+ "new architecture" (pipeline / substrate / corpus / closures) from
22
+ "shared old infrastructure" (this subpackage) at a glance.
23
+ """
@@ -0,0 +1,356 @@
1
+ """PhysMAP Step 0 (S0.1) — two-mechanism hemolysis apparatus (truth + closures + surrogate).
2
+
3
+ The constructed-but-physically-motivated truth/closure pair for the blind-spot pilot.
4
+ Two distinguishable hemolysis mechanisms feed one QoI (the hemolysis index HI):
5
+
6
+ shear damage S(tau) — instantaneous, driven by shear stress tau
7
+ exposure damage E(t) — cumulative, driven by residence time t
8
+ HI = S + E — additive, so the mechanism contributions stay separable
9
+
10
+ Each mechanism's TRUTH saturates (hemolysis saturates as cells are destroyed); each
11
+ CLOSURE is a power-law fit on its own calibration box only (tracks truth in-box,
12
+ OVERSHOOTS beyond — truth saturates, the power-law does not). A surrogate is trained on
13
+ the closure across the full envelope, so it is confident even in the extrapolation
14
+ region where the closure is wrong. That confident-but-wrong region is the candidate
15
+ blind spot.
16
+
17
+ Parameters are chosen so (a) the closures fit their box well, (b) extrapolation error is
18
+ material at plausible saturation, and (c) materiality genuinely FLIPS across the grid
19
+ (shear dominates at high tau, exposure at low-tau/high-t) — the precondition for the
20
+ causal-vs-naive lift to be testable. The choice is VERIFIED by coherence_report(), not
21
+ eyeballed (same discipline as the synthesizer coupling fix).
22
+
23
+ Torch-free: numpy + scikit-learn only.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass, field
29
+
30
+ import numpy as np
31
+
32
+ # ── operating envelope (normalized shear tau, normalized residence time t) ────
33
+ # box = calibration region (closures fit here); envelope = surrogate training span.
34
+ TAU_LO, TAU_CAL_S, TAU_HI = 1.0, 3.0, 6.0
35
+ T_LO, T_CAL_E, T_HI = 1.0, 3.0, 6.0
36
+
37
+ # ── truth: saturating exponential per mechanism ───────────────────────────────
38
+ # 1 - exp(-(k x^a)) is power-law-like (~ k x^a) below the box edge so a power-law
39
+ # closure fits the box, and bends toward the ceiling beyond it.
40
+ # Shear grows STEEPLY (as=2.0) and its in-box closure coefficient (Cs ≈ SMAX*KS)
41
+ # DOMINATES exposure's (Ce ≈ EMAX*KE), so at high tau shear carries HI and exposure
42
+ # is immaterial even when out-of-box (the disc band). Exposure grows SHALLOWLY
43
+ # (ae=1.3) but reaches far in t, so at LOW tau / HIGH t (small shear) exposure
44
+ # dominates and is material — materiality genuinely FLIPS across the grid.
45
+ SMAX, KS, AS = 1.0, 0.12, 2.0 # Cs ≈ 0.12
46
+ EMAX, KE, AE = 0.40, 0.08, 1.3 # Ce ≈ 0.032 (< Cs → immaterial at high tau)
47
+
48
+ THETA_MAT = 0.20 # a mechanism is "material" if its closure contribution fraction >= this
49
+ TOL = 0.05 # trustworthy if |HI_sur - HI_true| / (SMAX+EMAX) <= this
50
+ N_TRAIN = 300 # surrogate training points across the full envelope
51
+ K_ENSEMBLE = 10 # bootstrap ensemble size (guardrail variance signal)
52
+ GRID_N = 26 # query grid is GRID_N x GRID_N over the envelope
53
+
54
+
55
+ def s_true(tau: np.ndarray) -> np.ndarray:
56
+ return SMAX * (1.0 - np.exp(-(KS * np.asarray(tau, float) ** AS)))
57
+
58
+
59
+ def e_true(t: np.ndarray) -> np.ndarray:
60
+ return EMAX * (1.0 - np.exp(-(KE * np.asarray(t, float) ** AE)))
61
+
62
+
63
+ def hi_true(tau: np.ndarray, t: np.ndarray) -> np.ndarray:
64
+ return s_true(tau) + e_true(t)
65
+
66
+
67
+ def fit_power_law(x: np.ndarray, y: np.ndarray) -> tuple[float, float]:
68
+ """Log-linear least squares: y = C * x**alpha -> log y = log C + alpha log x."""
69
+ lx, ly = np.log(np.asarray(x, float)), np.log(np.asarray(y, float))
70
+ alpha, logC = np.polyfit(lx, ly, 1)
71
+ return float(np.exp(logC)), float(alpha)
72
+
73
+
74
+ @dataclass
75
+ class Closures:
76
+ """The two power-law closures, each fit on its own calibration box."""
77
+ Cs: float
78
+ alphas: float
79
+ Ce: float
80
+ alphae: float
81
+
82
+ def s_clo(self, tau: np.ndarray) -> np.ndarray:
83
+ return self.Cs * np.asarray(tau, float) ** self.alphas
84
+
85
+ def e_clo(self, t: np.ndarray) -> np.ndarray:
86
+ return self.Ce * np.asarray(t, float) ** self.alphae
87
+
88
+ def hi_clo(self, tau: np.ndarray, t: np.ndarray) -> np.ndarray:
89
+ return self.s_clo(tau) + self.e_clo(t)
90
+
91
+
92
+ def fit_closures(n_box: int = 40, seed: int = 0) -> Closures:
93
+ """Fit each power-law closure to truth samples drawn ONLY from its calibration box."""
94
+ rng = np.random.default_rng(seed)
95
+ tau_box = rng.uniform(TAU_LO, TAU_CAL_S, n_box)
96
+ t_box = rng.uniform(T_LO, T_CAL_E, n_box)
97
+ Cs, alphas = fit_power_law(tau_box, s_true(tau_box))
98
+ Ce, alphae = fit_power_law(t_box, e_true(t_box))
99
+ return Closures(Cs, alphas, Ce, alphae)
100
+
101
+
102
+ def _make_gp():
103
+ from sklearn.gaussian_process import GaussianProcessRegressor
104
+ from sklearn.gaussian_process.kernels import RBF, ConstantKernel, WhiteKernel
105
+ # WhiteKernel fixed (not optimized) → no convergence-to-bound warning, deterministic.
106
+ kernel = (ConstantKernel(1.0) * RBF(length_scale=[1.0, 1.0])
107
+ + WhiteKernel(noise_level=1e-6, noise_level_bounds="fixed"))
108
+ return GaussianProcessRegressor(kernel=kernel, normalize_y=True, alpha=1e-8,
109
+ n_restarts_optimizer=0, random_state=0)
110
+
111
+
112
+ @dataclass
113
+ class Surrogate:
114
+ """GP fit to the CLOSURE across the full envelope + a K-member bootstrap ensemble.
115
+ The surrogate approximates the closure (confident everywhere trained); its error vs
116
+ truth is dominated by the closure's extrapolation error — the point of the pilot."""
117
+ members: list = field(default_factory=list)
118
+
119
+ def predict(self, X: np.ndarray) -> np.ndarray:
120
+ return self.members[0].predict(np.asarray(X, float))
121
+
122
+ def ensemble_variance(self, X: np.ndarray) -> np.ndarray:
123
+ preds = np.stack([m.predict(np.asarray(X, float)) for m in self.members], axis=0)
124
+ return preds.var(axis=0)
125
+
126
+
127
+ def train_surrogate(closures: Closures, n_train: int = N_TRAIN, k: int = K_ENSEMBLE,
128
+ seed: int = 0, box_only: bool = False) -> tuple[Surrogate, np.ndarray]:
129
+ """Train K surrogates on bootstrap resamples of N_train closure evaluations.
130
+ members[0] is the full-sample fit (the point predictor); the rest are bootstrap
131
+ members for the ensemble-variance guardrail. Returns (surrogate, X_train).
132
+
133
+ box_only=False → train across the full envelope (the deployed surrogate; confident
134
+ even in the extrapolation region — the blind spot). box_only=True → train ONLY on the
135
+ calibration box (the CONTROL arm: a correctly-scoped surrogate whose guardrails, when
136
+ recomputed against this box-only training distribution, should flag the extrapolation
137
+ region the full-envelope surrogate's guardrails miss)."""
138
+ rng = np.random.default_rng(seed)
139
+ tau_max = TAU_CAL_S if box_only else TAU_HI
140
+ t_max = T_CAL_E if box_only else T_HI
141
+ Xtr = np.column_stack([rng.uniform(TAU_LO, tau_max, n_train),
142
+ rng.uniform(T_LO, t_max, n_train)])
143
+ ytr = closures.hi_clo(Xtr[:, 0], Xtr[:, 1])
144
+ members = []
145
+ full = _make_gp()
146
+ full.fit(Xtr, ytr)
147
+ members.append(full)
148
+ for b in range(k - 1):
149
+ idx = rng.integers(0, n_train, n_train) # bootstrap resample
150
+ m = _make_gp()
151
+ m.fit(Xtr[idx], ytr[idx])
152
+ members.append(m)
153
+ return Surrogate(members), Xtr
154
+
155
+
156
+ def query_grid(grid_n: int = GRID_N) -> np.ndarray:
157
+ """Query points over the full envelope. A regular grid spans all four regions; a
158
+ targeted refinement block deliberately populates the disc band (high tau IN box ×
159
+ just-over-box t), where exposure is out-of-calibration but immaterial. Populating it
160
+ by construction is explicitly allowed (the spec: "populate it deliberately"); the
161
+ coherence report verifies those points are genuinely immaterial AND trustworthy."""
162
+ taus = np.linspace(TAU_LO, TAU_HI, grid_n)
163
+ ts = np.linspace(T_LO, T_HI, grid_n)
164
+ base = np.array([[a, b] for a in taus for b in ts], float)
165
+ # disc-band refinement: tau in the upper third of the box, t just beyond the box edge
166
+ d_tau = np.linspace(TAU_LO + 0.6 * (TAU_CAL_S - TAU_LO), TAU_CAL_S, 7)
167
+ d_t = np.linspace(T_CAL_E + 0.15, T_CAL_E + 1.4, 7)
168
+ disc = np.array([[a, b] for a in d_tau for b in d_t], float)
169
+ return np.vstack([base, disc])
170
+
171
+
172
+ @dataclass
173
+ class Dataset:
174
+ """All per-query-point quantities the pilot reasons over."""
175
+ X: np.ndarray # (n, 2) [tau, t]
176
+ hi_true: np.ndarray
177
+ hi_clo: np.ndarray
178
+ hi_sur: np.ndarray
179
+ c_s: np.ndarray # closure shear contribution
180
+ c_e: np.ndarray # closure exposure contribution
181
+ in_cal_s: np.ndarray # tau <= TAU_CAL_S
182
+ in_cal_e: np.ndarray # t <= T_CAL_E
183
+ material_e: np.ndarray # c_e / (c_s + c_e) >= THETA_MAT (closure-based)
184
+ material_s: np.ndarray # c_s / (c_s + c_e) >= THETA_MAT (closure-based)
185
+ closures: Closures
186
+ surrogate: Surrogate
187
+ X_train: np.ndarray
188
+
189
+
190
+ def build_dataset(seed: int = 0, grid_n: int = GRID_N, theta_mat: float = THETA_MAT,
191
+ box_only: bool = False) -> Dataset:
192
+ """box_only=False → the deployed full-envelope surrogate (default, the main pilot).
193
+ box_only=True → the control-arm surrogate trained only on the calibration box; the
194
+ oracle, closures, grid, and all closure-derived fields are IDENTICAL — only the
195
+ surrogate's training distribution (and hence hi_sur + X_train) changes."""
196
+ closures = fit_closures(seed=seed)
197
+ surrogate, Xtr = train_surrogate(closures, seed=seed, box_only=box_only)
198
+ X = query_grid(grid_n)
199
+ tau, t = X[:, 0], X[:, 1]
200
+ c_s, c_e = closures.s_clo(tau), closures.e_clo(t)
201
+ total = c_s + c_e
202
+ return Dataset(
203
+ X=X, hi_true=hi_true(tau, t), hi_clo=closures.hi_clo(tau, t),
204
+ hi_sur=surrogate.predict(X), c_s=c_s, c_e=c_e,
205
+ in_cal_s=tau <= TAU_CAL_S, in_cal_e=t <= T_CAL_E,
206
+ material_e=(c_e / total) >= theta_mat, material_s=(c_s / total) >= theta_mat,
207
+ closures=closures, surrogate=surrogate, X_train=Xtr,
208
+ )
209
+
210
+
211
+ # ── specificity-cell substrate (ensemble experiment, vehicle (a)) ────────────
212
+ # Tighter calibration box than the default — leaves a benign out-of-envelope band.
213
+ TAU_CAL_S_SPEC = 2.0
214
+ T_CAL_E_SPEC = 2.0
215
+
216
+
217
+ class _QuietSurrogate:
218
+ """Stub surrogate for the specificity STAND-IN: predicts closure exactly and
219
+ reports zero ensemble variance. Keeps the guardrail signals silent on the
220
+ out-of-envelope band where truth ≡ closure (the spec's 'baseline FA ≈ 0 by cell
221
+ definition' premise). Duck-types Surrogate's interface used by pilot_compute."""
222
+
223
+ def __init__(self, closures: "Closures") -> None:
224
+ self._closures = closures
225
+
226
+ def predict(self, X: np.ndarray) -> np.ndarray:
227
+ X = np.asarray(X, float)
228
+ return self._closures.hi_clo(X[:, 0], X[:, 1])
229
+
230
+ def ensemble_variance(self, X: np.ndarray) -> np.ndarray:
231
+ return np.zeros(len(np.asarray(X, float)))
232
+
233
+
234
+ def build_specificity_dataset(seed: int = 0, grid_n: int = GRID_N,
235
+ theta_mat: float = THETA_MAT,
236
+ tau_cal: float = TAU_CAL_S_SPEC,
237
+ t_cal: float = T_CAL_E_SPEC) -> Dataset:
238
+ """Specificity-cell synthetic substrate for the ensemble experiment (STAND-IN).
239
+
240
+ Builds a Dataset where:
241
+ - truth ≡ closure ≡ surrogate everywhere (err = 0 by construction; trustworthy.all())
242
+ - the surrogate is a quiet stub (exact closure, zero ensemble variance) so the
243
+ guardrail signals stay silent — matches the spec's 'baseline FA ≈ 0 by cell
244
+ definition' premise; a GP-trained surrogate's variance would fire on
245
+ out-of-envelope points even when truth tracks closure, contaminating the
246
+ baseline reading.
247
+ - the calibration box is tightened (tau_cal, t_cal) so the query grid contains a
248
+ BENIGN OUT-OF-ENVELOPE band: points beyond the box have closure tracking truth
249
+ but are flagged out-of-calibration by naive/causal.
250
+ - materiality (c_s, c_e fractions) varies across the band — some out-of-envelope
251
+ rows are causal-firing (material out-of-cal mechanism), some causal-silent
252
+ (immaterial out-of-cal mechanism); both must be present for H-harm to be
253
+ empirically falsifiable.
254
+
255
+ Status: STAND-IN — the EMPIRICAL upgrade is vehicle (b), a grounded convection
256
+ sub-range where the correlation is inside its validated range across a
257
+ materiality-varying sweep.
258
+ """
259
+ closures = fit_closures(seed=seed)
260
+ surrogate = _QuietSurrogate(closures)
261
+ X = query_grid(grid_n)
262
+ tau, t = X[:, 0], X[:, 1]
263
+ c_s, c_e = closures.s_clo(tau), closures.e_clo(t)
264
+ total = c_s + c_e
265
+ # Clip closure to the saturating bound. The corpus_runtime PDE-residual signal
266
+ # checks hi_sur > BOUND (SMAX + EMAX) as a physics constraint; the algebraic
267
+ # closures overshoot this bound at high (tau, t), which would fire the PDE arm
268
+ # as a spurious baseline FA in the specificity cell where truth tracks closure
269
+ # by definition. Clipping hi_sur AND hi_clo to BOUND preserves truth ≡ closure
270
+ # (both clip to the same value) while keeping the PDE signal silent.
271
+ bound = SMAX + EMAX
272
+ hi_clo_raw = closures.hi_clo(tau, t)
273
+ hi_clo_arr = np.clip(hi_clo_raw, 0.0, bound)
274
+ # truth ≡ closure ≡ surrogate everywhere (the specificity condition)
275
+ hi_true_arr = hi_clo_arr.copy()
276
+ hi_sur_arr = hi_clo_arr.copy()
277
+ # Uniform training reference for the OOD guardrail self-calibration. Sampling
278
+ # over the same envelope as the query keeps maha distances comparable, so
279
+ # baseline guardrails fire only on the ~5% Mahalanobis tail.
280
+ rng = np.random.default_rng(seed)
281
+ Xtr = np.column_stack([
282
+ rng.uniform(TAU_LO, TAU_HI, N_TRAIN),
283
+ rng.uniform(T_LO, T_HI, N_TRAIN),
284
+ ])
285
+ return Dataset(
286
+ X=X, hi_true=hi_true_arr, hi_clo=hi_clo_arr,
287
+ hi_sur=hi_sur_arr, c_s=c_s, c_e=c_e,
288
+ in_cal_s=tau <= tau_cal, in_cal_e=t <= t_cal,
289
+ material_e=(c_e / total) >= theta_mat, material_s=(c_s / total) >= theta_mat,
290
+ closures=closures, surrogate=surrogate, X_train=Xtr,
291
+ )
292
+
293
+
294
+ def coherence_report(ds: Dataset | None = None, seed: int = 0) -> dict:
295
+ """Verify the construction is sound (not manufactured): box fit good, extrapolation
296
+ error material, materiality flips, disc_region populated + genuinely trustworthy."""
297
+ if ds is None:
298
+ ds = build_dataset(seed=seed)
299
+ tau, t = ds.X[:, 0], ds.X[:, 1]
300
+ scale = SMAX + EMAX
301
+
302
+ # box-fit quality (max relative residual inside each box)
303
+ box_tau = np.linspace(TAU_LO, TAU_CAL_S, 50)
304
+ box_t = np.linspace(T_LO, T_CAL_E, 50)
305
+ s_box_res = float(np.max(np.abs(ds.closures.s_clo(box_tau) - s_true(box_tau)) / SMAX))
306
+ e_box_res = float(np.max(np.abs(ds.closures.e_clo(box_t) - e_true(box_t)) / EMAX))
307
+
308
+ # extrapolation error (closure vs truth) at the far corner
309
+ far = np.abs(ds.closures.hi_clo(np.array([TAU_HI]), np.array([T_HI]))
310
+ - hi_true(np.array([TAU_HI]), np.array([T_HI])))[0] / scale
311
+
312
+ err = np.abs(ds.hi_sur - ds.hi_true) / scale
313
+ trustworthy = err <= TOL
314
+
315
+ # disc_region = exactly one mechanism out-of-box AND that mechanism not material
316
+ exactly_one_out = (ds.in_cal_s ^ ds.in_cal_e)
317
+ # the out-of-box mechanism: if exposure out (not in_cal_e) it must be immaterial
318
+ exp_out_immaterial = (~ds.in_cal_e) & ds.in_cal_s & (~ds.material_e)
319
+ shear_out_immaterial = (~ds.in_cal_s) & ds.in_cal_e & (~ds.material_s)
320
+ disc = exp_out_immaterial | shear_out_immaterial
321
+
322
+ return {
323
+ "box_fit_max_rel_residual": {"shear": round(s_box_res, 4), "exposure": round(e_box_res, 4)},
324
+ "extrapolation_error_far_corner": round(float(far), 4),
325
+ "closure_alphas": round(ds.closures.alphas, 3),
326
+ "closure_alphae": round(ds.closures.alphae, 3),
327
+ "n_grid": len(ds.X),
328
+ "materiality_flip": {
329
+ "frac_exposure_material": round(float(ds.material_e.mean()), 3),
330
+ "frac_shear_material": round(float(ds.material_s.mean()), 3),
331
+ },
332
+ "region_counts": {
333
+ "both_in": int((ds.in_cal_s & ds.in_cal_e).sum()),
334
+ "exactly_one_out": int(exactly_one_out.sum()),
335
+ "both_out": int((~ds.in_cal_s & ~ds.in_cal_e).sum()),
336
+ "disc_region": int(disc.sum()),
337
+ },
338
+ "disc_region": {
339
+ "n": int(disc.sum()),
340
+ "frac_trustworthy": round(float(trustworthy[disc].mean()), 3) if disc.any() else None,
341
+ "mean_exposure_materiality": round(float((ds.c_e / (ds.c_s + ds.c_e))[disc].mean()), 3)
342
+ if disc.any() else None,
343
+ },
344
+ "n_untrustworthy": int((~trustworthy).sum()),
345
+ }
346
+
347
+
348
+ def main(argv=None) -> int:
349
+ import json
350
+ print("PhysMAP Step 0 — oracle coherence report\n")
351
+ print(json.dumps(coherence_report(), indent=2))
352
+ return 0
353
+
354
+
355
+ if __name__ == "__main__":
356
+ raise SystemExit(main())