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,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