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
physmap/guardrail/io.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""save / load — a .physmap bundle that is both fitted-state reuse and an
|
|
2
|
+
inspectable audit artifact.
|
|
3
|
+
|
|
4
|
+
The bundle is a zip: a readable JSON manifest (config + observability
|
|
5
|
+
classification + resolved closures + corpus version/hash + package version + fit
|
|
6
|
+
timestamp) and the fitted baseline models (joblib). The surrogate is NOT
|
|
7
|
+
serialized (re-supply on load); the corpus is NOT serialized (the manifest
|
|
8
|
+
records its version + content hash, and load WARNS on mismatch). The closure-
|
|
9
|
+
validity detectors carry no fit state — they are rebuilt from the installed
|
|
10
|
+
corpus deterministically.
|
|
11
|
+
|
|
12
|
+
Only the fitted INNER detectors are pickled: the DetectorAdapter wraps a
|
|
13
|
+
non-picklable rationale closure, so load rewraps the inner with the factory's
|
|
14
|
+
rationale builder + the saved threshold.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import warnings
|
|
21
|
+
import zipfile
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from io import BytesIO
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import physmap
|
|
27
|
+
from physmap.corpus.evidence import load_claims, load_sources
|
|
28
|
+
from physmap.guardrail.configs import (
|
|
29
|
+
ClosureValidityDetectorConfig,
|
|
30
|
+
DistanceDetectorConfig,
|
|
31
|
+
GPVarianceDetectorConfig,
|
|
32
|
+
NoveltyDetectorConfig,
|
|
33
|
+
)
|
|
34
|
+
from physmap.guardrail.corpus_regimes import corpus_fingerprint
|
|
35
|
+
from physmap.guardrail.enums import AggregatorKind, DensityMethod, Device, DetectorKind, Regime
|
|
36
|
+
from physmap.pipeline.validity_signal import ValidityRangeDistanceDetector
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _detector_to_dict(spec) -> dict:
|
|
40
|
+
if isinstance(spec, DetectorKind):
|
|
41
|
+
return {"type": "kind", "kind": spec.value}
|
|
42
|
+
if isinstance(spec, NoveltyDetectorConfig):
|
|
43
|
+
return {"type": "NoveltyDetectorConfig", "method": spec.method.value,
|
|
44
|
+
"components": spec.components, "warn_pct": spec.warn_pct,
|
|
45
|
+
"reject_pct": spec.reject_pct, "device": spec.device.value}
|
|
46
|
+
if isinstance(spec, DistanceDetectorConfig):
|
|
47
|
+
return {"type": "DistanceDetectorConfig", "k": spec.k, "device": spec.device.value}
|
|
48
|
+
if isinstance(spec, GPVarianceDetectorConfig):
|
|
49
|
+
return {"type": "GPVarianceDetectorConfig", "kernel": spec.kernel,
|
|
50
|
+
"device": spec.device.value}
|
|
51
|
+
if isinstance(spec, ClosureValidityDetectorConfig):
|
|
52
|
+
return {"type": "ClosureValidityDetectorConfig", "graded": spec.graded,
|
|
53
|
+
"status_weighting": spec.status_weighting}
|
|
54
|
+
raise TypeError(f"cannot serialize detector spec of type {type(spec).__name__}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _detector_from_dict(d: dict):
|
|
58
|
+
t = d["type"]
|
|
59
|
+
if t == "kind":
|
|
60
|
+
return DetectorKind(d["kind"])
|
|
61
|
+
if t == "NoveltyDetectorConfig":
|
|
62
|
+
return NoveltyDetectorConfig(method=DensityMethod(d["method"]), components=d["components"],
|
|
63
|
+
warn_pct=d["warn_pct"], reject_pct=d["reject_pct"],
|
|
64
|
+
device=Device(d["device"]))
|
|
65
|
+
if t == "DistanceDetectorConfig":
|
|
66
|
+
return DistanceDetectorConfig(k=d["k"], device=Device(d["device"]))
|
|
67
|
+
if t == "GPVarianceDetectorConfig":
|
|
68
|
+
return GPVarianceDetectorConfig(kernel=d["kernel"], device=Device(d["device"]))
|
|
69
|
+
if t == "ClosureValidityDetectorConfig":
|
|
70
|
+
return ClosureValidityDetectorConfig(graded=d["graded"], status_weighting=d["status_weighting"])
|
|
71
|
+
raise ValueError(f"unknown detector spec type {t!r}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def save_guardrail(guard, path) -> None:
|
|
75
|
+
if not guard._fitted:
|
|
76
|
+
raise RuntimeError("save() called before fit(); nothing fitted to persist.")
|
|
77
|
+
import joblib
|
|
78
|
+
|
|
79
|
+
fp = corpus_fingerprint() # {version, sha256, n_entries, tier} of the active corpus
|
|
80
|
+
manifest = {
|
|
81
|
+
"physmap_version": physmap.__version__,
|
|
82
|
+
"fit_timestamp": datetime.now(timezone.utc).isoformat(),
|
|
83
|
+
"config": {
|
|
84
|
+
"surrogate_inputs": guard.surrogate_inputs,
|
|
85
|
+
"regime": guard.regime.value,
|
|
86
|
+
"aggregator": guard.aggregator_kind.value,
|
|
87
|
+
"operating_pct": guard.operating_pct,
|
|
88
|
+
"detectors": [_detector_to_dict(s) for s in guard.detectors],
|
|
89
|
+
},
|
|
90
|
+
"observability_classification": {k: v.value for k, v in guard._observability.items()},
|
|
91
|
+
"resolved_closures": list(guard._resolved_closures),
|
|
92
|
+
"baseline_feature_names": list(guard._baseline_feature_names),
|
|
93
|
+
"validity_feature_names": list(guard._validity_feature_names),
|
|
94
|
+
"graded": guard._graded,
|
|
95
|
+
"status_weighting": guard._status_weighting,
|
|
96
|
+
"corpus": fp,
|
|
97
|
+
# Top-level audit field: which corpus TIER produced this artifact. The
|
|
98
|
+
# tier also lives in corpus.tier; surfaced here so audit tooling can read
|
|
99
|
+
# it without reaching into the fingerprint.
|
|
100
|
+
"corpus_tier": fp["tier"],
|
|
101
|
+
"thresholds": {
|
|
102
|
+
"distance": guard._distance.threshold if guard._distance else None,
|
|
103
|
+
"gp_variance": guard._gp.threshold if guard._gp else None,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
with zipfile.ZipFile(Path(path), "w", zipfile.ZIP_DEFLATED) as z:
|
|
108
|
+
z.writestr("manifest.json", json.dumps(manifest, indent=2))
|
|
109
|
+
if guard._distance is not None:
|
|
110
|
+
buf = BytesIO(); joblib.dump(guard._distance.inner, buf)
|
|
111
|
+
z.writestr("models/distance.joblib", buf.getvalue())
|
|
112
|
+
if guard._gp is not None:
|
|
113
|
+
buf = BytesIO(); joblib.dump(guard._gp.inner, buf)
|
|
114
|
+
z.writestr("models/gp.joblib", buf.getvalue())
|
|
115
|
+
if guard._density is not None:
|
|
116
|
+
buf = BytesIO(); joblib.dump(guard._density, buf)
|
|
117
|
+
z.writestr("models/density.joblib", buf.getvalue())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_guardrail(cls, path, *, surrogate=None):
|
|
121
|
+
import joblib
|
|
122
|
+
from physmap.pipeline.core import DetectorAdapter, _distance_rationale, _gp_rationale
|
|
123
|
+
|
|
124
|
+
with zipfile.ZipFile(Path(path), "r") as z:
|
|
125
|
+
manifest = json.loads(z.read("manifest.json"))
|
|
126
|
+
cfg = manifest["config"]
|
|
127
|
+
guard = cls(
|
|
128
|
+
surrogate_inputs=cfg["surrogate_inputs"],
|
|
129
|
+
regime=Regime(cfg["regime"]),
|
|
130
|
+
detectors=[_detector_from_dict(d) for d in cfg["detectors"]],
|
|
131
|
+
aggregator=AggregatorKind(cfg["aggregator"]),
|
|
132
|
+
surrogate=surrogate,
|
|
133
|
+
operating_pct=cfg["operating_pct"],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
saved = manifest["corpus"]
|
|
137
|
+
cur = corpus_fingerprint()
|
|
138
|
+
saved_tier = manifest.get("corpus_tier") or saved.get("tier", "unknown")
|
|
139
|
+
cur_tier = cur.get("tier", "unknown")
|
|
140
|
+
if cur["version"] != saved["version"] or cur["sha256"] != saved["sha256"]:
|
|
141
|
+
tier_note = ""
|
|
142
|
+
if saved_tier != cur_tier and "unknown" not in (saved_tier, cur_tier):
|
|
143
|
+
# Tier drift is a WARN, not an error: a seed-fitted guard loaded
|
|
144
|
+
# in a premium env is fully valid (seed closures ⊂ premium); a
|
|
145
|
+
# premium-fit guard in a seed env loses premium-only bounds (the
|
|
146
|
+
# validity-detector rebuild below drops them gracefully).
|
|
147
|
+
tier_note = (
|
|
148
|
+
f" Tier changed: fit on the '{saved_tier}' corpus, loaded in a "
|
|
149
|
+
f"'{cur_tier}' environment."
|
|
150
|
+
)
|
|
151
|
+
warnings.warn(
|
|
152
|
+
f"corpus version/content differs from the saved guard "
|
|
153
|
+
f"(saved {saved['version']}/{saved['sha256'][:8]} vs installed "
|
|
154
|
+
f"{cur['version']}/{cur['sha256'][:8]}); validity bounds may have "
|
|
155
|
+
f"changed since this guard was fit.{tier_note}",
|
|
156
|
+
stacklevel=2,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
th = manifest["thresholds"]
|
|
160
|
+
names = set(z.namelist())
|
|
161
|
+
if "models/distance.joblib" in names:
|
|
162
|
+
inner = joblib.load(BytesIO(z.read("models/distance.joblib")))
|
|
163
|
+
guard._distance = DetectorAdapter(
|
|
164
|
+
name="distance", inner=inner, threshold=th.get("distance"),
|
|
165
|
+
rationale_fn=_distance_rationale(getattr(inner, "k", 3)))
|
|
166
|
+
if "models/gp.joblib" in names:
|
|
167
|
+
inner = joblib.load(BytesIO(z.read("models/gp.joblib")))
|
|
168
|
+
guard._gp = DetectorAdapter(
|
|
169
|
+
name="gp_variance", inner=inner, threshold=th.get("gp_variance"),
|
|
170
|
+
rationale_fn=_gp_rationale())
|
|
171
|
+
if "models/density.joblib" in names:
|
|
172
|
+
guard._density = joblib.load(BytesIO(z.read("models/density.joblib")))
|
|
173
|
+
|
|
174
|
+
guard._graded = manifest["graded"]
|
|
175
|
+
guard._status_weighting = manifest["status_weighting"]
|
|
176
|
+
if "closure_validity" in guard._detector_configs and guard._resolved_closures:
|
|
177
|
+
# Rebuild validity detectors from the ACTIVE corpus, skipping any closure
|
|
178
|
+
# whose bounds are absent (a premium-fit guard loaded in a seed env). This
|
|
179
|
+
# degrades gracefully — warn, not error — per the case-2a semantics.
|
|
180
|
+
rebuilt = []
|
|
181
|
+
for cid in guard._resolved_closures:
|
|
182
|
+
try:
|
|
183
|
+
rebuilt.append((cid, ValidityRangeDistanceDetector(
|
|
184
|
+
closure_id=cid, feature_names=guard._validity_feature_names)))
|
|
185
|
+
except ValueError:
|
|
186
|
+
pass
|
|
187
|
+
guard._validity_detectors = rebuilt
|
|
188
|
+
saved_resolved = list(manifest.get("resolved_closures", []))
|
|
189
|
+
dropped = [c for c in saved_resolved if c not in guard._resolved_closures]
|
|
190
|
+
if dropped:
|
|
191
|
+
warnings.warn(
|
|
192
|
+
f"closure-validity bounds for {dropped} resolved at fit time but are "
|
|
193
|
+
f"not in the active corpus now (likely a premium-fit guard loaded in a "
|
|
194
|
+
f"seed environment); those validity detectors are dropped. The guard "
|
|
195
|
+
f"still loads and runs on its remaining detectors.",
|
|
196
|
+
stacklevel=2,
|
|
197
|
+
)
|
|
198
|
+
guard._claims = load_claims()
|
|
199
|
+
guard._sources_index = {s.source_id: s for s in load_sources()}
|
|
200
|
+
guard._fitted = True
|
|
201
|
+
return guard
|