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
physmap/__init__.py ADDED
@@ -0,0 +1,61 @@
1
+ """PhysMAP — physics-aware credibility checks for AI surrogates.
2
+
3
+ Two distinct methods live here, and they are deliberately kept apart:
4
+
5
+ **Closure validity and surrogate observability.** A surrogate trained on (Re, Pr) is
6
+ blind to x/D, so it fails silently in a pipe entrance region -- and so do input-space
7
+ novelty detectors, because they see only (Re, Pr) too. This path reads the bound
8
+ variable from the test coordinates, checks it against the closure's validated range,
9
+ and knows at fit time whether that variable is structurally observable to the
10
+ surrogate. Where it is observable, the guard defers to the statistical baselines.
11
+
12
+ **Causal materiality.** A mechanism outside its calibration range only matters if it
13
+ materially reaches the requested quantity of interest. This path flags a prediction
14
+ only when a mechanism is both out of range AND material to that QoI.
15
+
16
+ These are different claims with different evidence. Nothing in this package attributes
17
+ the results of one to the other. See `physmap.release` for what this build may claim.
18
+
19
+ No LLM is in any path. Explanations are deterministic rendered templates.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from physmap.release import (
25
+ CURRENT_RELEASE_STATE,
26
+ EvidenceState,
27
+ ReleaseState,
28
+ )
29
+
30
+ __version__ = "0.2.0"
31
+
32
+
33
+ # The CredibilityGuardrail public surface is re-exported LAZILY (PEP 562). A bare
34
+ # `import physmap` must stay free of numpy, sklearn, scipy and joblib -- a test
35
+ # asserts it -- while `from physmap import CredibilityGuardrail` pulls the
36
+ # guardrail and its scientific stack, which is fair because you are about to use
37
+ # the guard. Adding an eager import here breaks that guarantee silently.
38
+ _GUARDRAIL_EXPORTS = frozenset({
39
+ "CredibilityGuardrail",
40
+ "Regime", "DetectorKind", "DensityMethod", "AggregatorKind", "Device",
41
+ "Observability", "Verdict", "Disposition",
42
+ "NoveltyDetectorConfig", "DistanceDetectorConfig", "GPVarianceDetectorConfig",
43
+ "ClosureValidityDetectorConfig", "ColumnMap", "DetectorResult", "Assessment",
44
+ })
45
+
46
+ _RELEASE_EXPORTS = frozenset({
47
+ "CURRENT_RELEASE_STATE", "ReleaseState", "EvidenceState",
48
+ })
49
+
50
+ __all__ = ["__version__", *sorted(_RELEASE_EXPORTS), *sorted(_GUARDRAIL_EXPORTS)]
51
+
52
+
53
+ def __getattr__(name: str):
54
+ if name in _GUARDRAIL_EXPORTS:
55
+ import physmap.guardrail as _g
56
+ return getattr(_g, name)
57
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
58
+
59
+
60
+ def __dir__():
61
+ return sorted(set(globals()) | _GUARDRAIL_EXPORTS)
physmap/_paths.py ADDED
@@ -0,0 +1,69 @@
1
+ """Repo-checkout paths, resolved once and failing loudly.
2
+
3
+ Several artifacts in this project live in the CHECKOUT, not in the installed
4
+ package: the digitised NACA fixture, the regime/observability mapping, the
5
+ registry-expansion table, and the benchmark substrate CSVs. They are deliberately
6
+ outside the wheel -- see the note on redistribution in LICENSE-CORPUS, and the
7
+ README's warning that the benchmark requires an editable install.
8
+
9
+ The trap this module exists to close: code that derives such a path with
10
+ ``Path(__file__).parents[n]`` silently produces a WRONG path under the src/
11
+ layout rather than an error, and a caller that catches FileNotFoundError then
12
+ degrades quietly. A missing corpus becomes an empty corpus, and an empty corpus
13
+ still runs -- it just answers nothing. Every resolver here raises instead.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pathlib import Path
19
+
20
+ __all__ = ["repo_root", "checkout_path", "have_checkout", "CheckoutRequired"]
21
+
22
+ _MARKERS = ("pyproject.toml", ".git")
23
+ REPO_URL = "https://github.com/cloudronin/physmap"
24
+
25
+
26
+ class CheckoutRequired(FileNotFoundError):
27
+ """Checkout-only data was needed from an installed wheel.
28
+
29
+ A FileNotFoundError, so every existing handler still catches it. The CLI catches it by
30
+ name and prints this one sentence instead of a traceback.
31
+ """
32
+
33
+ def __init__(self, what: str):
34
+ self.what = what
35
+ super().__init__(
36
+ f"this needs {what} from the repository checkout, and a pip-installed wheel "
37
+ f"does not include that data. Clone {REPO_URL} and run `pip install -e .` inside "
38
+ f"the clone to use it."
39
+ )
40
+
41
+
42
+ def repo_root() -> Path | None:
43
+ """The checkout root, or None when running from an installed wheel."""
44
+ for parent in Path(__file__).resolve().parents:
45
+ if any((parent / m).exists() for m in _MARKERS):
46
+ return parent
47
+ return None
48
+
49
+
50
+ def have_checkout() -> bool:
51
+ return repo_root() is not None
52
+
53
+
54
+ def checkout_path(*parts: str, what: str) -> Path:
55
+ """A path inside the checkout. Raises unless it exists.
56
+
57
+ `what` names the artifact in the error, so a failure says which file is
58
+ missing and why it is not in the wheel.
59
+ """
60
+ root = repo_root()
61
+ if root is None:
62
+ raise CheckoutRequired(what)
63
+ p = root.joinpath(*parts)
64
+ if not p.exists():
65
+ raise FileNotFoundError(
66
+ f"{what} not found at {p}. Expected it in the checkout at "
67
+ f"{'/'.join(parts)}."
68
+ )
69
+ return p
File without changes
@@ -0,0 +1,83 @@
1
+ """Declarative screening fixtures. Stated cases, not measured ones.
2
+
3
+ Two cases were screened out of the original study. They are reproduced here as
4
+ DECLARATIVE fixtures: their preconditions are asserted from the case description, not
5
+ read from data, because the evidence inputs and provenance for them are not available.
6
+
7
+ What they demonstrate is the refusal logic and its reason codes. What they are not is
8
+ executable evidence-backed cases, and nothing in the output lets them read as such --
9
+ every result carries `evidence_state = DECLARATIVE`, and the CLI prints it.
10
+
11
+ They become measured cases only when their evidence inputs and provenance arrive. Until
12
+ then a fixture that claimed otherwise would be the exact error this project is about:
13
+ a stated thing wearing the clothes of a measured one.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from physmap.applicability.screen import ScreenResult, screen_case
19
+ from physmap.release import EvidenceState
20
+
21
+ __all__ = ["FIXTURE_IDS", "conjugate_heat_transfer", "fda_blood_pump", "get_fixture"]
22
+
23
+ FIXTURE_IDS = ("conjugate-heat-transfer", "fda-blood-pump")
24
+
25
+
26
+ def conjugate_heat_transfer() -> ScreenResult:
27
+ """Solid-fluid conjugate heat transfer, QoI = peak solid temperature.
28
+
29
+ Screened out because the QoI does not decompose over fluid-side mechanisms. Peak
30
+ solid temperature is set by conduction through the solid coupled to the fluid-side
31
+ heat flux; removing a fluid-side convection mechanism changes the coupled boundary
32
+ condition rather than subtracting a term. There is no "the same case with buoyancy
33
+ off" whose peak temperature differs only by the buoyancy contribution.
34
+ """
35
+ return screen_case(
36
+ "conjugate-heat-transfer", "peak_solid_temperature",
37
+ qoi_decomposes=False,
38
+ mechanisms_separable=False,
39
+ has_calibration_window=True,
40
+ ablation_available=None,
41
+ evidence_state=EvidenceState.DECLARATIVE,
42
+ notes=(
43
+ "Preconditions are asserted from the case description, not measured.",
44
+ "Reported to demonstrate refusal logic only.",
45
+ ),
46
+ )
47
+
48
+
49
+ def fda_blood_pump() -> ScreenResult:
50
+ """FDA benchmark centrifugal blood pump, QoI = haemolysis index.
51
+
52
+ Screened out because the mechanisms are not separable. The haemolysis index
53
+ integrates a damage model along pathlines through a rotating machine; shear
54
+ generation, turbulence and residence time are coupled through the same flow field.
55
+ Switching one off does not hold the others fixed -- it produces a different flow.
56
+ """
57
+ return screen_case(
58
+ "fda-blood-pump", "haemolysis_index",
59
+ qoi_decomposes=True,
60
+ mechanisms_separable=False,
61
+ has_calibration_window=True,
62
+ ablation_available=None,
63
+ evidence_state=EvidenceState.DECLARATIVE,
64
+ notes=(
65
+ "Preconditions are asserted from the case description, not measured.",
66
+ "Reported to demonstrate refusal logic only.",
67
+ ),
68
+ )
69
+
70
+
71
+ _FIXTURES = {
72
+ "conjugate-heat-transfer": conjugate_heat_transfer,
73
+ "fda-blood-pump": fda_blood_pump,
74
+ }
75
+
76
+
77
+ def get_fixture(case_id: str) -> ScreenResult:
78
+ try:
79
+ return _FIXTURES[case_id]()
80
+ except KeyError:
81
+ raise KeyError(
82
+ f"unknown screening fixture {case_id!r}; available: {list(FIXTURE_IDS)}"
83
+ ) from None
@@ -0,0 +1,99 @@
1
+ """The applicability screen: is the causal method even the right tool here?
2
+
3
+ Cheap, and deliberately run first. The causal method needs three things to be true of a
4
+ case: the quantity of interest must decompose, the mechanism must be separable enough to
5
+ remove on its own, and an ablation must be obtainable. Where any of those fails, the right
6
+ answer is a refusal with a reason code -- not a confident number produced by machinery
7
+ operating outside the conditions it assumes.
8
+
9
+ A refusal here is a result. It is also the cheapest honest output the method has.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum
16
+
17
+ from physmap.release import EvidenceState
18
+
19
+ __all__ = ["Applicability", "ReasonCode", "ScreenResult", "screen_case"]
20
+
21
+
22
+ class Applicability(str, Enum):
23
+ APPLICABLE = "applicable"
24
+ NOT_APPLICABLE = "not_applicable"
25
+ INSUFFICIENT_EVIDENCE = "insufficient_evidence"
26
+
27
+
28
+ class ReasonCode(str, Enum):
29
+ #: The QoI is not a sum or blend over mechanisms, so removing one is undefined.
30
+ QOI_DOES_NOT_DECOMPOSE = "qoi_does_not_decompose"
31
+ #: The mechanisms are coupled: you cannot switch one off and hold the rest fixed.
32
+ MECHANISMS_NOT_SEPARABLE = "mechanisms_not_separable"
33
+ #: No calibration window is recorded, so "outside calibration" has no meaning.
34
+ NO_CALIBRATION_WINDOW = "no_calibration_window"
35
+ #: No matched ablation is obtainable for this case.
36
+ NO_ABLATION_AVAILABLE = "no_ablation_available"
37
+ #: Everything needed is present.
38
+ SATISFIED = "satisfied"
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class ScreenResult:
43
+ case_id: str
44
+ qoi: str
45
+ applicability: Applicability
46
+ reason_code: ReasonCode
47
+ rationale: str
48
+ evidence_state: EvidenceState
49
+ notes: tuple[str, ...] = field(default_factory=tuple)
50
+
51
+ @property
52
+ def is_declarative(self) -> bool:
53
+ return self.evidence_state is EvidenceState.DECLARATIVE
54
+
55
+
56
+ def screen_case(
57
+ case_id: str,
58
+ qoi: str,
59
+ *,
60
+ qoi_decomposes: bool | None,
61
+ mechanisms_separable: bool | None,
62
+ has_calibration_window: bool | None,
63
+ ablation_available: bool | None,
64
+ evidence_state: EvidenceState,
65
+ notes: tuple[str, ...] = (),
66
+ ) -> ScreenResult:
67
+ """Screen a case. `None` for any precondition means "not established", which is
68
+ INSUFFICIENT_EVIDENCE -- distinct from establishing that it is false."""
69
+
70
+ def _r(app: Applicability, code: ReasonCode, why: str) -> ScreenResult:
71
+ return ScreenResult(
72
+ case_id=case_id, qoi=qoi, applicability=app, reason_code=code,
73
+ rationale=why, evidence_state=evidence_state, notes=tuple(notes),
74
+ )
75
+
76
+ checks = (
77
+ (qoi_decomposes, ReasonCode.QOI_DOES_NOT_DECOMPOSE,
78
+ f"the quantity of interest ({qoi}) does not decompose over mechanisms, so "
79
+ f"removing one is not defined"),
80
+ (mechanisms_separable, ReasonCode.MECHANISMS_NOT_SEPARABLE,
81
+ "the mechanisms are coupled: one cannot be switched off while the others are "
82
+ "held fixed, so an ablation would change more than the mechanism under test"),
83
+ (has_calibration_window, ReasonCode.NO_CALIBRATION_WINDOW,
84
+ "no calibration window is recorded, so 'outside calibration' has no meaning "
85
+ "for this case"),
86
+ (ablation_available, ReasonCode.NO_ABLATION_AVAILABLE,
87
+ "no matched ablation is obtainable, so the counterfactual cannot be formed"),
88
+ )
89
+
90
+ for value, code, why in checks:
91
+ if value is None:
92
+ return _r(Applicability.INSUFFICIENT_EVIDENCE, code,
93
+ f"not established whether {why.split(',')[0]}")
94
+ if value is False:
95
+ return _r(Applicability.NOT_APPLICABLE, code, why)
96
+
97
+ return _r(Applicability.APPLICABLE, ReasonCode.SATISFIED,
98
+ "quantity of interest decomposes, mechanisms are separable, a calibration "
99
+ "window is recorded, and a matched ablation is obtainable")
File without changes
File without changes