physmap 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- physmap/__init__.py +61 -0
- physmap/_paths.py +69 -0
- physmap/applicability/__init__.py +0 -0
- physmap/applicability/fixtures.py +83 -0
- physmap/applicability/screen.py +99 -0
- physmap/baselines/__init__.py +0 -0
- physmap/benchmarks/__init__.py +0 -0
- physmap/benchmarks/benchmark_report.py +405 -0
- physmap/benchmarks/benchmark_v0_4.py +424 -0
- physmap/benchmarks/compare.py +149 -0
- physmap/benchmarks/registry.py +217 -0
- physmap/benchmarks/report.py +224 -0
- physmap/cli.py +301 -0
- physmap/closures/__init__.py +48 -0
- physmap/closures/data/__init__.py +7 -0
- physmap/closures/data/closure_index.json +2997 -0
- physmap/closures/formulas.py +213 -0
- physmap/closures/geometry_classes.py +109 -0
- physmap/closures/index.py +393 -0
- physmap/closures/registry.py +313 -0
- physmap/compat/__init__.py +0 -0
- physmap/core/__init__.py +0 -0
- physmap/core/mechanism.py +69 -0
- physmap/core/signals.py +50 -0
- physmap/corpus/__init__.py +12 -0
- physmap/corpus/calibration.py +543 -0
- physmap/corpus/data/__init__.py +12 -0
- physmap/corpus/data/corpus_seed.jsonl +15 -0
- physmap/corpus/data/evidence_claims_seed.jsonl +21 -0
- physmap/corpus/data/evidence_sources_seed.jsonl +8 -0
- physmap/corpus/data/premium_coverage.json +60 -0
- physmap/corpus/evidence.py +871 -0
- physmap/explain/__init__.py +0 -0
- physmap/explain/benchmark.py +101 -0
- physmap/explain/causal.py +82 -0
- physmap/guardrail/__init__.py +38 -0
- physmap/guardrail/aggregator_observability.py +187 -0
- physmap/guardrail/classify.py +147 -0
- physmap/guardrail/configs.py +120 -0
- physmap/guardrail/corpus_regimes.py +208 -0
- physmap/guardrail/detector_conformal.py +129 -0
- physmap/guardrail/detector_density.py +74 -0
- physmap/guardrail/enums.py +69 -0
- physmap/guardrail/graph.py +73 -0
- physmap/guardrail/guardrail.py +606 -0
- physmap/guardrail/io.py +201 -0
- physmap/guardrail/regime_observability.py +519 -0
- physmap/guardrail/render.py +159 -0
- physmap/guardrail/weighting_heuristic.py +216 -0
- physmap/infra/__init__.py +23 -0
- physmap/infra/blindspot_oracle.py +356 -0
- physmap/infra/corpus_runtime.py +275 -0
- physmap/integrations/__init__.py +0 -0
- physmap/materiality/__init__.py +0 -0
- physmap/materiality/estimator.py +239 -0
- physmap/materiality/independence.py +92 -0
- physmap/materiality/surrogate_fit.py +293 -0
- physmap/observability/__init__.py +0 -0
- physmap/pipeline/__init__.py +58 -0
- physmap/pipeline/aggregators.py +199 -0
- physmap/pipeline/assessment_v06.py +509 -0
- physmap/pipeline/core.py +442 -0
- physmap/pipeline/defeasible_aggregator.py +324 -0
- physmap/pipeline/detectors.py +309 -0
- physmap/pipeline/observability.py +430 -0
- physmap/pipeline/surrogate.py +251 -0
- physmap/pipeline/validity_signal.py +273 -0
- physmap/pipeline/vehicle_spec.py +287 -0
- physmap/release.py +81 -0
- physmap/stress_tests/__init__.py +9 -0
- physmap/stress_tests/lewis_reuse.py +517 -0
- physmap/substrate/__init__.py +28 -0
- physmap/substrate/corpus_real.py +206 -0
- physmap/substrate/engine.py +209 -0
- physmap/substrate/forrest.py +249 -0
- physmap/substrate/loaders.py +2176 -0
- physmap/substrate/naca_tn1451.py +379 -0
- physmap/substrate/naca_wpd_loader.py +187 -0
- physmap/substrate/stage1_ingest.py +187 -0
- physmap/substrate/vehicle_config.py +407 -0
- physmap-0.2.0.dist-info/METADATA +270 -0
- physmap-0.2.0.dist-info/RECORD +88 -0
- physmap-0.2.0.dist-info/WHEEL +5 -0
- physmap-0.2.0.dist-info/entry_points.txt +2 -0
- physmap-0.2.0.dist-info/licenses/LICENSE +21 -0
- physmap-0.2.0.dist-info/licenses/LICENSE-CORPUS +469 -0
- physmap-0.2.0.dist-info/licenses/NOTICE +77 -0
- physmap-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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())
|