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,509 @@
|
|
|
1
|
+
"""Phase-2 Assessment as a v0.6-conformant UofA subgraph.
|
|
2
|
+
|
|
3
|
+
Per the v0.2 architecture refactor spec, Part 4: the Phase-1 flat Assessment
|
|
4
|
+
becomes a focused subgraph conforming to the v0.6 vocab
|
|
5
|
+
(`https://uofa.net/vocab#`). The mapping is mechanical from the Phase-1
|
|
6
|
+
field names (chosen as Phase-2 seeds during Steps 5–6):
|
|
7
|
+
|
|
8
|
+
Assessment.operating_point -> Discrepancy.id_ + discrepancyRegion
|
|
9
|
+
Assessment.verdict -> Disposition.actionClass (via adjudicator)
|
|
10
|
+
Assessment.decision_signals[*] -> CredibilityFactor (always)
|
|
11
|
+
+ WeakenerAnnotation (when fired)
|
|
12
|
+
DetectorResult.rationale -> WeakenerAnnotation.justification
|
|
13
|
+
Assessment.rationale -> Disposition.actionParameters
|
|
14
|
+
|
|
15
|
+
Phase-2 scope (per the spec): the ~8 IN-SCOPE node types
|
|
16
|
+
* Discrepancy — the surrogate point under review
|
|
17
|
+
* CredibilityFactor — one per detector (status: fired/quiet)
|
|
18
|
+
* WeakenerAnnotation — one per FIRED detector
|
|
19
|
+
* OffsetRationale — emitted by the defeasible adjudicator (Phase 2C)
|
|
20
|
+
* Disposition — actionClass from the v0.6 controlled vocab
|
|
21
|
+
* hasEvidence/hasJustification — link edges (carried as `affectedNode`
|
|
22
|
+
and the v0.6 `justification` property)
|
|
23
|
+
|
|
24
|
+
OUT-OF-SCOPE per point (these belong to the enclosing UnitOfAssurance
|
|
25
|
+
case, not the per-point assessment): VerificationActivity, ModelConfiguration,
|
|
26
|
+
SensitivityAnalysis, ProcessAttestation, DeploymentRecord, InputPedigreeLink,
|
|
27
|
+
ReviewActivity, full UnitOfAssurance lifecycle.
|
|
28
|
+
|
|
29
|
+
This module ships the dataclasses + the Phase-1 → v0.6 mapper. The
|
|
30
|
+
defeasible-adjudication aggregator (Phase 2C) plugs into
|
|
31
|
+
`_choose_action_class` and `_build_offset_rationales` so this layer
|
|
32
|
+
stays a pure structural mapping; Phase 2C reasons over it.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import re
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any, Iterable, Sequence
|
|
41
|
+
|
|
42
|
+
from physmap.pipeline.core import Assessment, DetectorResult
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Path the v0.6 @context is loaded from — relative to package root.
|
|
46
|
+
# Mirrors evidence_corpus.py's pattern (commit 2897392).
|
|
47
|
+
V06_CONTEXT_REF = "physmap/fixtures/context/v0.6.jsonld"
|
|
48
|
+
|
|
49
|
+
# Disposition controlled vocabulary (locked by uofa's disposition_shapes.ttl;
|
|
50
|
+
# any string outside this set fails SHACL).
|
|
51
|
+
DISPOSITION_ACTION_CLASSES = (
|
|
52
|
+
"restrict-cou", # narrow the context of use to avoid the failure
|
|
53
|
+
"acquire-validation", # get more validation data
|
|
54
|
+
"characterize-region", # describe where the failure occurs
|
|
55
|
+
"accept-residual-risk", # accept the failure with justification
|
|
56
|
+
"change-cou", # alter the context of use (model swap, etc.)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── factor / pattern dictionaries (Phase-1 -> v0.6 controlled-ish terms) ────
|
|
61
|
+
#
|
|
62
|
+
# These maps make the per-detector mapping mechanical. They are not fixed
|
|
63
|
+
# vocab (v0.6 doesn't pin factorType / patternId controlled strings the way
|
|
64
|
+
# it pins actionClass), but they ARE the names the EvidenceEnrichmentStage
|
|
65
|
+
# from Step 7 already uses for its provenance keys — preserving them lets
|
|
66
|
+
# Phase-1 + Phase-2 outputs line up without renames.
|
|
67
|
+
|
|
68
|
+
_FACTOR_TYPE_BY_DETECTOR = {
|
|
69
|
+
"distance": "input-distribution-novelty",
|
|
70
|
+
"gp_variance": "surrogate-prediction-variance",
|
|
71
|
+
"ensemble_variance": "ensemble-prediction-variance",
|
|
72
|
+
"corpus": "training-set-corpus-error",
|
|
73
|
+
"closure_validity": "literature-validity-distance",
|
|
74
|
+
"novelty_density": "input-distribution-novelty", # physmap GMM density baseline
|
|
75
|
+
}
|
|
76
|
+
# Note: EvidenceEnrichmentStage (in evidence_stage.py, Cleanup 6) attaches
|
|
77
|
+
# claim-centric provenance to justification_signals when the
|
|
78
|
+
# closure_validity gating signal fires. The justification payload's
|
|
79
|
+
# `claim_type` / `source` fields map to v0.6's hasEvidence and
|
|
80
|
+
# wasDerivedFrom edges in the Phase-2 graph.
|
|
81
|
+
|
|
82
|
+
_PATTERN_ID_BY_DETECTOR = {
|
|
83
|
+
"distance": "InputOutOfTrainingDistribution",
|
|
84
|
+
"gp_variance": "SurrogateVarianceExceedsThreshold",
|
|
85
|
+
"ensemble_variance": "EnsembleDisagreement",
|
|
86
|
+
"corpus": "CorpusErrorMagnitudeAboveThreshold",
|
|
87
|
+
"closure_validity": "OutOfValidatedRange",
|
|
88
|
+
# physmap GMM density baseline → an input-distribution novelty pattern (already
|
|
89
|
+
# in defeasible_aggregator.NOVELTY_PATTERNS), so a fired GMM adjudicates as novelty.
|
|
90
|
+
"novelty_density": "InputOutOfTrainingDistribution",
|
|
91
|
+
# observable-pole conformal statistical mode → a surrogate-variance-family weakener
|
|
92
|
+
# (a baseline-side credibility signal, never the corpus), so it adjudicates like gp_variance.
|
|
93
|
+
"conformal_residual": "SurrogateVarianceExceedsThreshold",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── v0.6 node dataclasses ───────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
# DiscrepancyShape pins measureType to this 2-entry vocab.
|
|
100
|
+
DISCREPANCY_MEASURE_TYPES = ("point", "aggregate")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class DiscrepancyNode:
|
|
105
|
+
"""uofa:Discrepancy — the surrogate point under review. Measured FACT;
|
|
106
|
+
carries surrogate-vs-truth + region/measure metadata. SHACL pins
|
|
107
|
+
surrogatePrediction, solverTruth, discrepancyMagnitude as xsd:double
|
|
108
|
+
(exactly one each) and `measureType ∈ {"point", "aggregate"}`."""
|
|
109
|
+
id_: str
|
|
110
|
+
surrogate_prediction: float
|
|
111
|
+
solver_truth: float
|
|
112
|
+
discrepancy_magnitude: float
|
|
113
|
+
discrepancy_region: str = ""
|
|
114
|
+
measure_type: str = "point" # one row = one operating-point measurement
|
|
115
|
+
|
|
116
|
+
def __post_init__(self) -> None:
|
|
117
|
+
if self.measure_type not in DISCREPANCY_MEASURE_TYPES:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"Discrepancy.measure_type={self.measure_type!r} not in v0.6 "
|
|
120
|
+
f"controlled vocab {DISCREPANCY_MEASURE_TYPES}. SHACL "
|
|
121
|
+
f"DiscrepancyShape will reject this; refuse at construction."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class CredibilityFactorNode:
|
|
127
|
+
"""uofa:CredibilityFactor — one per detector. `factor_type` names the
|
|
128
|
+
detector family; `factor_status` is "fired" or "quiet"; `factor_standard`
|
|
129
|
+
cites the literature standard backing the detector (e.g., the closure_id
|
|
130
|
+
a validity-range detector queries)."""
|
|
131
|
+
id_: str
|
|
132
|
+
factor_type: str
|
|
133
|
+
factor_status: str # "fired" | "quiet"
|
|
134
|
+
factor_standard: str = ""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class WeakenerAnnotationNode:
|
|
139
|
+
"""uofa:WeakenerAnnotation — emitted for EVERY fired detector. patternId
|
|
140
|
+
names the failure pattern; affectedNode points at the Discrepancy; the
|
|
141
|
+
`justification` text is the DetectorResult.rationale verbatim
|
|
142
|
+
(preserves the audit trail Phase-1 already built)."""
|
|
143
|
+
id_: str
|
|
144
|
+
pattern_id: str
|
|
145
|
+
affected_node: str
|
|
146
|
+
justification: str
|
|
147
|
+
severity: str | None = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True)
|
|
151
|
+
class OffsetRationaleNode:
|
|
152
|
+
"""uofa:OffsetRationale — emitted by the defeasible adjudicator (Phase 2C)
|
|
153
|
+
to say "this CredibilityFactor's fire is OFFSET by other evidence; do
|
|
154
|
+
not weigh it as a defeater." Phase 2A doesn't emit these; the dataclass
|
|
155
|
+
is defined here so the Phase 2C aggregator can construct them without
|
|
156
|
+
introducing new types later."""
|
|
157
|
+
id_: str
|
|
158
|
+
refers_to_factor: str
|
|
159
|
+
justification: str
|
|
160
|
+
offsetting_evidence: str = ""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class DispositionNode:
|
|
165
|
+
"""uofa:Disposition — the action taken in response to the assessment.
|
|
166
|
+
SHACL pins actionClass to the 5-entry controlled vocab
|
|
167
|
+
(DISPOSITION_ACTION_CLASSES). `actionParameters` is optional free text;
|
|
168
|
+
`confidenceLevel` is optional High/Medium/Low or numeric;
|
|
169
|
+
`residual_risk_justification` is required when actionClass is
|
|
170
|
+
'accept-residual-risk'."""
|
|
171
|
+
id_: str
|
|
172
|
+
action_class: str
|
|
173
|
+
action_parameters: str = ""
|
|
174
|
+
confidence_level: str | None = None
|
|
175
|
+
residual_risk_justification: str = ""
|
|
176
|
+
|
|
177
|
+
def __post_init__(self) -> None:
|
|
178
|
+
if self.action_class not in DISPOSITION_ACTION_CLASSES:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"Disposition.action_class={self.action_class!r} not in v0.6 "
|
|
181
|
+
f"controlled vocab {DISPOSITION_ACTION_CLASSES}. SHACL will "
|
|
182
|
+
f"reject this; refuse at construction."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass
|
|
187
|
+
class V06AssessmentSubgraph:
|
|
188
|
+
"""The full per-point Phase-2 subgraph: one Discrepancy + N
|
|
189
|
+
CredibilityFactors + M WeakenerAnnotations + 0-K OffsetRationales +
|
|
190
|
+
one Disposition."""
|
|
191
|
+
discrepancy: DiscrepancyNode
|
|
192
|
+
credibility_factors: list[CredibilityFactorNode]
|
|
193
|
+
weakener_annotations: list[WeakenerAnnotationNode]
|
|
194
|
+
offset_rationales: list[OffsetRationaleNode]
|
|
195
|
+
disposition: DispositionNode
|
|
196
|
+
operating_point: tuple
|
|
197
|
+
|
|
198
|
+
def to_jsonld(self, *, context_ref: str = V06_CONTEXT_REF) -> dict:
|
|
199
|
+
"""Serialize as a v0.6 JSON-LD doc with @context + @graph."""
|
|
200
|
+
graph: list[dict] = []
|
|
201
|
+
graph.append(_discrepancy_to_jsonld(self.discrepancy))
|
|
202
|
+
for f in self.credibility_factors:
|
|
203
|
+
graph.append(_credibility_factor_to_jsonld(f))
|
|
204
|
+
for w in self.weakener_annotations:
|
|
205
|
+
graph.append(_weakener_annotation_to_jsonld(w))
|
|
206
|
+
for o in self.offset_rationales:
|
|
207
|
+
graph.append(_offset_rationale_to_jsonld(o))
|
|
208
|
+
graph.append(_disposition_to_jsonld(self.disposition))
|
|
209
|
+
return {"@context": context_ref, "@graph": graph}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ── JSON-LD serializers (each emits one @graph entry) ───────────────────────
|
|
213
|
+
|
|
214
|
+
def _discrepancy_to_jsonld(d: DiscrepancyNode) -> dict:
|
|
215
|
+
payload: dict[str, Any] = {
|
|
216
|
+
"@type": "Discrepancy",
|
|
217
|
+
"id": d.id_,
|
|
218
|
+
# SHACL pins these as xsd:double; the @context coerces JSON numbers
|
|
219
|
+
# to xsd:double automatically.
|
|
220
|
+
"surrogatePrediction": float(d.surrogate_prediction),
|
|
221
|
+
"solverTruth": float(d.solver_truth),
|
|
222
|
+
"discrepancyMagnitude": float(d.discrepancy_magnitude),
|
|
223
|
+
}
|
|
224
|
+
if d.discrepancy_region:
|
|
225
|
+
payload["discrepancyRegion"] = d.discrepancy_region
|
|
226
|
+
if d.measure_type:
|
|
227
|
+
payload["measureType"] = d.measure_type
|
|
228
|
+
return payload
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _credibility_factor_to_jsonld(f: CredibilityFactorNode) -> dict:
|
|
232
|
+
payload: dict[str, Any] = {
|
|
233
|
+
"@type": "CredibilityFactor",
|
|
234
|
+
"id": f.id_,
|
|
235
|
+
"factorType": f.factor_type,
|
|
236
|
+
"factorStatus": f.factor_status,
|
|
237
|
+
}
|
|
238
|
+
if f.factor_standard:
|
|
239
|
+
payload["factorStandard"] = f.factor_standard
|
|
240
|
+
return payload
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _weakener_annotation_to_jsonld(w: WeakenerAnnotationNode) -> dict:
|
|
244
|
+
payload: dict[str, Any] = {
|
|
245
|
+
"@type": "WeakenerAnnotation",
|
|
246
|
+
"id": w.id_,
|
|
247
|
+
"patternId": w.pattern_id,
|
|
248
|
+
"affectedNode": w.affected_node,
|
|
249
|
+
"justification": w.justification,
|
|
250
|
+
}
|
|
251
|
+
if w.severity is not None:
|
|
252
|
+
payload["severity"] = w.severity
|
|
253
|
+
return payload
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _offset_rationale_to_jsonld(o: OffsetRationaleNode) -> dict:
|
|
257
|
+
payload: dict[str, Any] = {
|
|
258
|
+
"@type": "OffsetRationale",
|
|
259
|
+
"id": o.id_,
|
|
260
|
+
"refersToFactor": o.refers_to_factor,
|
|
261
|
+
"justification": o.justification,
|
|
262
|
+
}
|
|
263
|
+
if o.offsetting_evidence:
|
|
264
|
+
payload["offsettingEvidence"] = o.offsetting_evidence
|
|
265
|
+
return payload
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _disposition_to_jsonld(d: DispositionNode) -> dict:
|
|
269
|
+
payload: dict[str, Any] = {
|
|
270
|
+
"@type": "Disposition",
|
|
271
|
+
"id": d.id_,
|
|
272
|
+
"actionClass": d.action_class,
|
|
273
|
+
}
|
|
274
|
+
if d.action_parameters:
|
|
275
|
+
payload["actionParameters"] = d.action_parameters
|
|
276
|
+
if d.confidence_level is not None:
|
|
277
|
+
payload["confidenceLevel"] = d.confidence_level
|
|
278
|
+
if d.residual_risk_justification:
|
|
279
|
+
payload["residualRiskJustification"] = d.residual_risk_justification
|
|
280
|
+
return payload
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ── Phase-1 → v0.6 mapper ───────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
# Match a closure_id inside a rationale string. ValidityRangeDistanceDetector
|
|
286
|
+
# rationales surface 'gnielinski-1976' / 'modified-sparrow-cur-...' in single
|
|
287
|
+
# quotes; pull them out to populate factorStandard.
|
|
288
|
+
_CLOSURE_ID_RE = re.compile(r"'([a-z][a-z0-9-]+(?:-[a-z0-9]+)*-\d{4})'")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _factor_standard_from_rationale(result: DetectorResult) -> str:
|
|
292
|
+
"""Extract a literature-standard string from a DetectorResult rationale.
|
|
293
|
+
Today only the closure_validity rationale carries a closure_id; other
|
|
294
|
+
detectors leave the field blank.
|
|
295
|
+
"""
|
|
296
|
+
if result.detector_name != "closure_validity":
|
|
297
|
+
return ""
|
|
298
|
+
match = _CLOSURE_ID_RE.search(result.rationale)
|
|
299
|
+
return match.group(1) if match else ""
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _default_action_class_for_verdict(verdict: str,
|
|
303
|
+
weakeners: list[WeakenerAnnotationNode],
|
|
304
|
+
) -> tuple[str, str]:
|
|
305
|
+
"""Phase-2A default action-class chooser. The Phase 2C defeasible
|
|
306
|
+
aggregator replaces this with proper offset/agreement/threshold-distance
|
|
307
|
+
reasoning. Defaults:
|
|
308
|
+
verdict='quiet' AND no weakeners -> accept-residual-risk
|
|
309
|
+
verdict='fire' AND >=1 weakener -> characterize-region
|
|
310
|
+
|
|
311
|
+
Returns (action_class, residual_risk_justification).
|
|
312
|
+
"""
|
|
313
|
+
if verdict == "quiet":
|
|
314
|
+
return ("accept-residual-risk",
|
|
315
|
+
"No decision-signal weakeners fired; accepting with residual risk.")
|
|
316
|
+
# verdict == "fire"
|
|
317
|
+
weakener_patterns = sorted({w.pattern_id for w in weakeners})
|
|
318
|
+
return ("characterize-region",
|
|
319
|
+
f"Weakeners fired on patterns {weakener_patterns}; downstream "
|
|
320
|
+
f"reasoning must characterize the affected region before deploying. "
|
|
321
|
+
f"(Phase-2A default; Phase-2C adjudicator may upgrade/downgrade.)")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _op_to_id_suffix(operating_point: tuple) -> str:
|
|
325
|
+
"""Make a stable id-safe slug from an operating-point tuple. Avoids
|
|
326
|
+
collisions across vehicles by including the structure verbatim."""
|
|
327
|
+
parts = ["%g" % float(p) if isinstance(p, (int, float)) else str(p)
|
|
328
|
+
for p in operating_point]
|
|
329
|
+
raw = "x".join(parts)
|
|
330
|
+
return re.sub(r"[^A-Za-z0-9._-]", "_", raw)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def assessment_to_v06_subgraph(
|
|
334
|
+
phase1: Assessment,
|
|
335
|
+
*,
|
|
336
|
+
surrogate_prediction: float,
|
|
337
|
+
solver_truth: float,
|
|
338
|
+
point_id_prefix: str = "p1pt",
|
|
339
|
+
discrepancy_region: str = "",
|
|
340
|
+
measure_type: str = "point",
|
|
341
|
+
confidence_level: str | None = None,
|
|
342
|
+
action_class_chooser=None,
|
|
343
|
+
adjudicator=None,
|
|
344
|
+
) -> V06AssessmentSubgraph:
|
|
345
|
+
"""Map a Phase-1 Assessment + the underlying surrogate/truth pair into a
|
|
346
|
+
v0.6-conformant subgraph.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
phase1: the Phase-1 Assessment from Pipeline.run / phase1_gate.
|
|
350
|
+
surrogate_prediction: scalar — the row's surrogate_prediction (often
|
|
351
|
+
the matched-closure prediction the engine populated).
|
|
352
|
+
solver_truth: scalar — the row's cfd_truth (measured Nu).
|
|
353
|
+
point_id_prefix: id prefix for the generated nodes (so multiple
|
|
354
|
+
subgraphs can coexist in one document without colliding).
|
|
355
|
+
discrepancy_region: free-text region descriptor surfaced into
|
|
356
|
+
Discrepancy.discrepancyRegion (e.g., "x_over_D=2.5, Re=55570").
|
|
357
|
+
measure_type: what the discrepancy is in (default "Nu").
|
|
358
|
+
confidence_level: optional override for Disposition.confidenceLevel.
|
|
359
|
+
action_class_chooser: optional callable
|
|
360
|
+
(verdict, weakeners) -> (action_class, residual_risk_justification).
|
|
361
|
+
Defaults to `_default_action_class_for_verdict`. The defeasible
|
|
362
|
+
adjudicator (Phase 2C) plugs in here.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
A V06AssessmentSubgraph carrying the typed nodes. Serialize to
|
|
366
|
+
JSON-LD via `.to_jsonld()`.
|
|
367
|
+
"""
|
|
368
|
+
suffix = _op_to_id_suffix(phase1.operating_point)
|
|
369
|
+
base = f"{point_id_prefix}/{suffix}"
|
|
370
|
+
|
|
371
|
+
discrepancy = DiscrepancyNode(
|
|
372
|
+
id_=f"discrepancy:{base}",
|
|
373
|
+
surrogate_prediction=float(surrogate_prediction),
|
|
374
|
+
solver_truth=float(solver_truth),
|
|
375
|
+
discrepancy_magnitude=abs(float(surrogate_prediction) - float(solver_truth)),
|
|
376
|
+
discrepancy_region=discrepancy_region,
|
|
377
|
+
measure_type=measure_type,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
factors: list[CredibilityFactorNode] = []
|
|
381
|
+
weakeners: list[WeakenerAnnotationNode] = []
|
|
382
|
+
for name, det_result in phase1.decision_signals.items():
|
|
383
|
+
f = CredibilityFactorNode(
|
|
384
|
+
id_=f"factor:{base}/{name}",
|
|
385
|
+
factor_type=_FACTOR_TYPE_BY_DETECTOR.get(name, name),
|
|
386
|
+
factor_status="fired" if det_result.fired else "quiet",
|
|
387
|
+
factor_standard=_factor_standard_from_rationale(det_result),
|
|
388
|
+
)
|
|
389
|
+
factors.append(f)
|
|
390
|
+
if det_result.fired:
|
|
391
|
+
weakeners.append(WeakenerAnnotationNode(
|
|
392
|
+
id_=f"weakener:{base}/{name}",
|
|
393
|
+
pattern_id=_PATTERN_ID_BY_DETECTOR.get(name, name),
|
|
394
|
+
affected_node=discrepancy.id_,
|
|
395
|
+
justification=det_result.rationale,
|
|
396
|
+
# The DetectorResult doesn't carry a separate "severity"
|
|
397
|
+
# term; leave None so SHACL/audit can see absence.
|
|
398
|
+
severity=None,
|
|
399
|
+
))
|
|
400
|
+
|
|
401
|
+
# Adjudicator overrides chooser when set (Phase-2C path). Otherwise
|
|
402
|
+
# fall through to the Phase-2A default chooser.
|
|
403
|
+
offset_rationales: list[OffsetRationaleNode] = []
|
|
404
|
+
if adjudicator is not None:
|
|
405
|
+
result = adjudicator.adjudicate(
|
|
406
|
+
weakeners=weakeners,
|
|
407
|
+
decision_signals=phase1.decision_signals,
|
|
408
|
+
discrepancy_id=discrepancy.id_,
|
|
409
|
+
)
|
|
410
|
+
action_class = result.action_class
|
|
411
|
+
residual_just = result.residual_risk_justification
|
|
412
|
+
offset_rationales = list(result.offset_rationales)
|
|
413
|
+
else:
|
|
414
|
+
chooser = action_class_chooser or _default_action_class_for_verdict
|
|
415
|
+
action_class, residual_just = chooser(phase1.verdict, weakeners)
|
|
416
|
+
|
|
417
|
+
disposition = DispositionNode(
|
|
418
|
+
id_=f"disposition:{base}",
|
|
419
|
+
action_class=action_class,
|
|
420
|
+
action_parameters=phase1.rationale,
|
|
421
|
+
confidence_level=confidence_level,
|
|
422
|
+
residual_risk_justification=(
|
|
423
|
+
residual_just if action_class == "accept-residual-risk" else ""
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return V06AssessmentSubgraph(
|
|
428
|
+
discrepancy=discrepancy,
|
|
429
|
+
credibility_factors=factors,
|
|
430
|
+
weakener_annotations=weakeners,
|
|
431
|
+
offset_rationales=offset_rationales,
|
|
432
|
+
disposition=disposition,
|
|
433
|
+
operating_point=phase1.operating_point,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def assessments_to_v06_document(
|
|
438
|
+
phase1_assessments: Sequence[Assessment],
|
|
439
|
+
*,
|
|
440
|
+
surrogates: Sequence[float],
|
|
441
|
+
truths: Sequence[float],
|
|
442
|
+
regions: Sequence[str] | None = None,
|
|
443
|
+
point_id_prefix: str = "naca-p1pt",
|
|
444
|
+
measure_type: str = "point",
|
|
445
|
+
action_class_chooser=None,
|
|
446
|
+
adjudicator=None,
|
|
447
|
+
) -> dict:
|
|
448
|
+
"""Map a sequence of Phase-1 Assessments into a single v0.6 JSON-LD
|
|
449
|
+
document with one shared @context and a flat @graph of all nodes
|
|
450
|
+
across all points. The id_prefix keeps points cleanly addressable.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
phase1_assessments: list of Phase-1 Assessments (e.g. from phase1_gate).
|
|
454
|
+
surrogates, truths: parallel arrays of per-point surrogate/truth pairs.
|
|
455
|
+
regions: optional parallel array of free-text region descriptors.
|
|
456
|
+
point_id_prefix: id prefix used per subgraph.
|
|
457
|
+
action_class_chooser: defeasible adjudicator (Phase 2C). If None,
|
|
458
|
+
defaults to the Phase-2A heuristic.
|
|
459
|
+
"""
|
|
460
|
+
n = len(phase1_assessments)
|
|
461
|
+
if len(surrogates) != n or len(truths) != n:
|
|
462
|
+
raise ValueError(
|
|
463
|
+
f"surrogates ({len(surrogates)}) and truths ({len(truths)}) must "
|
|
464
|
+
f"match the assessment count ({n})."
|
|
465
|
+
)
|
|
466
|
+
if regions is None:
|
|
467
|
+
regions = [""] * n
|
|
468
|
+
elif len(regions) != n:
|
|
469
|
+
raise ValueError(
|
|
470
|
+
f"regions ({len(regions)}) must match assessment count ({n})."
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
graph: list[dict] = []
|
|
474
|
+
for i, assess in enumerate(phase1_assessments):
|
|
475
|
+
sub = assessment_to_v06_subgraph(
|
|
476
|
+
assess,
|
|
477
|
+
surrogate_prediction=float(surrogates[i]),
|
|
478
|
+
solver_truth=float(truths[i]),
|
|
479
|
+
point_id_prefix=point_id_prefix,
|
|
480
|
+
discrepancy_region=regions[i],
|
|
481
|
+
measure_type=measure_type,
|
|
482
|
+
action_class_chooser=action_class_chooser,
|
|
483
|
+
adjudicator=adjudicator,
|
|
484
|
+
)
|
|
485
|
+
graph.append(_discrepancy_to_jsonld(sub.discrepancy))
|
|
486
|
+
for f in sub.credibility_factors:
|
|
487
|
+
graph.append(_credibility_factor_to_jsonld(f))
|
|
488
|
+
for w in sub.weakener_annotations:
|
|
489
|
+
graph.append(_weakener_annotation_to_jsonld(w))
|
|
490
|
+
for o in sub.offset_rationales:
|
|
491
|
+
graph.append(_offset_rationale_to_jsonld(o))
|
|
492
|
+
graph.append(_disposition_to_jsonld(sub.disposition))
|
|
493
|
+
|
|
494
|
+
return {"@context": V06_CONTEXT_REF, "@graph": graph}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
__all__ = [
|
|
498
|
+
"DISPOSITION_ACTION_CLASSES",
|
|
499
|
+
"DISCREPANCY_MEASURE_TYPES",
|
|
500
|
+
"V06_CONTEXT_REF",
|
|
501
|
+
"DiscrepancyNode",
|
|
502
|
+
"CredibilityFactorNode",
|
|
503
|
+
"WeakenerAnnotationNode",
|
|
504
|
+
"OffsetRationaleNode",
|
|
505
|
+
"DispositionNode",
|
|
506
|
+
"V06AssessmentSubgraph",
|
|
507
|
+
"assessment_to_v06_subgraph",
|
|
508
|
+
"assessments_to_v06_document",
|
|
509
|
+
]
|