api581-engine 0.1.4__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.
- api581/__init__.py +80 -0
- api581/_amine_tables.py +105 -0
- api581/_h2sh2_tables.py +161 -0
- api581/_slabx_worker.py +174 -0
- api581/_sulfidic_tables.py +517 -0
- api581/_sulfuric_tables.py +328 -0
- api581/acid_sour_water.py +238 -0
- api581/acscc_screening.py +88 -0
- api581/alkaline_sour_water.py +267 -0
- api581/amine_corrosion.py +277 -0
- api581/amine_screening.py +103 -0
- api581/api/__init__.py +4 -0
- api581/api/api520.py +88 -0
- api581/api/bundle.py +121 -0
- api581/api/consequence.py +102 -0
- api581/api/damage.py +1831 -0
- api581/api/dispersion.py +121 -0
- api581/api/effects.py +318 -0
- api581/api/events.py +96 -0
- api581/api/financial.py +115 -0
- api581/api/frequency.py +40 -0
- api581/api/inspection.py +96 -0
- api581/api/level1.py +238 -0
- api581/api/level2.py +74 -0
- api581/api/planning.py +17 -0
- api581/api/prd.py +190 -0
- api581/api/properties.py +130 -0
- api581/api/release.py +252 -0
- api581/api/reliability.py +40 -0
- api581/api/risk.py +71 -0
- api581/api/tank.py +490 -0
- api581/api520.py +205 -0
- api581/area_contribution.py +72 -0
- api581/blast.py +232 -0
- api581/brittle.py +205 -0
- api581/bundle.py +300 -0
- api581/catalog.py +442 -0
- api581/caustic_screening.py +103 -0
- api581/clad_thinning.py +146 -0
- api581/clscc_screening.py +55 -0
- api581/co2_corrosion.py +313 -0
- api581/co2_screening.py +205 -0
- api581/consequences.py +250 -0
- api581/contracts.py +1038 -0
- api581/cooling_selection.py +204 -0
- api581/cooling_water.py +305 -0
- api581/core.py +126 -0
- api581/corrosion_rate.py +38 -0
- api581/cost_tables.py +175 -0
- api581/cracking.py +74 -0
- api581/data/coverage.json +21361 -0
- api581/data/digital-twin.schema.json +538 -0
- api581/data/direct-test-queue.json +7 -0
- api581/data/native-engine-evidence.json +44 -0
- api581/data/python-api.schema.json +92706 -0
- api581/data/reference-fixture-evidence.json +2956 -0
- api581/data/scope-boundaries.schema.json +58 -0
- api581/data/software-test-evidence.json +4278 -0
- api581/data/standard-metadata.json +1328 -0
- api581/data/standard.schema.json +233 -0
- api581/data/supporting-text-review.json +1319 -0
- api581/digital_twin.py +491 -0
- api581/dispersion.py +45 -0
- api581/effects.py +232 -0
- api581/evaporation.py +181 -0
- api581/evidence.py +81 -0
- api581/external_corrosion.py +148 -0
- api581/external_cracking.py +63 -0
- api581/fatigue.py +56 -0
- api581/fireball.py +163 -0
- api581/flash_pool.py +111 -0
- api581/flash_solver.py +122 -0
- api581/groundwater.py +101 -0
- api581/h2sh2_corrosion.py +128 -0
- api581/hic_screened.py +89 -0
- api581/htha.py +45 -0
- api581/hydrochloric_acid.py +272 -0
- api581/hydrofluoric_acid.py +236 -0
- api581/ignition.py +204 -0
- api581/inspection_effectiveness.py +601 -0
- api581/jet_handoff.py +60 -0
- api581/level1.py +311 -0
- api581/level1_summary.py +35 -0
- api581/level1_toxic.py +313 -0
- api581/level1_type1.py +91 -0
- api581/level2_summary.py +237 -0
- api581/lining.py +173 -0
- api581/native_toxic.py +67 -0
- api581/outputs.py +5672 -0
- api581/oxidation.py +145 -0
- api581/pascc_screening.py +78 -0
- api581/physical_explosion.py +190 -0
- api581/pof.py +207 -0
- api581/pool_fire.py +273 -0
- api581/portfolio.py +68 -0
- api581/prd.py +204 -0
- api581/prd_consequences.py +245 -0
- api581/prd_history.py +44 -0
- api581/py.typed +0 -0
- api581/release.py +307 -0
- api581/representative_fluids.py +172 -0
- api581/result.py +50 -0
- api581/risk.py +191 -0
- api581/saturation.py +66 -0
- api581/scc_mechanisms.py +242 -0
- api581/schema.py +45 -0
- api581/slabx_adapter.py +261 -0
- api581/soil_corrosion.py +191 -0
- api581/source_partition.py +169 -0
- api581/spatial.py +87 -0
- api581/ssc_screening.py +77 -0
- api581/standard.py +281 -0
- api581/storage.py +42 -0
- api581/sulfidic_corrosion.py +197 -0
- api581/sulfuric_acid.py +209 -0
- api581/tank.py +260 -0
- api581/tank_corrosion.py +256 -0
- api581/tank_environment.py +186 -0
- api581/tank_financial.py +202 -0
- api581/tank_integrated.py +51 -0
- api581/tank_shell.py +141 -0
- api581/tank_source.py +152 -0
- api581/tank_thinning.py +204 -0
- api581/temper.py +105 -0
- api581/thinning.py +172 -0
- api581/thinning_conditions.py +212 -0
- api581/thinning_history.py +118 -0
- api581/thinning_selection.py +81 -0
- api581/thinning_type.py +34 -0
- api581/toxic_criteria.py +111 -0
- api581/toxic_level2.py +233 -0
- api581/two_phase_events.py +120 -0
- api581/workflow.py +42 -0
- api581_engine-0.1.4.dist-info/METADATA +419 -0
- api581_engine-0.1.4.dist-info/RECORD +139 -0
- api581_engine-0.1.4.dist-info/WHEEL +5 -0
- api581_engine-0.1.4.dist-info/licenses/LICENSE +161 -0
- api581_engine-0.1.4.dist-info/licenses/NOTICE.md +18 -0
- api581_engine-0.1.4.dist-info/top_level.txt +1 -0
api581/scc_mechanisms.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""SCC mechanism-specific severity, inspection and age calculations (Part 2 §§6-14)."""
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from .core import Model, NonNegative, Positive, calculation, ensure
|
|
5
|
+
from .pof import InspectionEquivalent, inspection_equivalent
|
|
6
|
+
from .cracking import SccBase, scc_base
|
|
7
|
+
|
|
8
|
+
Susceptibility = Literal["none","low","medium","high"]
|
|
9
|
+
Mechanism = Literal["caustic","amine","ssc","hic-sohic-h2s","acscc","pascc","clscc","hsc-hf","hic-sohic-hf"]
|
|
10
|
+
PROFILES = {
|
|
11
|
+
"caustic":(6,"6.2",25,(0,50,500,5000)),
|
|
12
|
+
"amine":(7,"7.2",26,(0,10,100,1000)),
|
|
13
|
+
"ssc":(8,"8.4",27,(0,1,10,100)),
|
|
14
|
+
"hic-sohic-h2s":(9,"9.4",28,(0,1,10,100)),
|
|
15
|
+
"acscc":(10,"10.3",29,(0,10,100,1000)),
|
|
16
|
+
"pascc":(11,"11.3",30,(0,50,500,5000)),
|
|
17
|
+
"clscc":(12,"12.3",31,(0,50,500,5000)),
|
|
18
|
+
"hsc-hf":(13,"13.3",32,(0,1,10,100)),
|
|
19
|
+
"hic-sohic-hf":(14,"14.3",33,(0,1,10,100))}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Severity(Model):
|
|
23
|
+
mechanism: Mechanism
|
|
24
|
+
susceptibility: Susceptibility
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@calculation("damage/scc-severity", "Mechanism-specific SCC severity index", Severity,
|
|
28
|
+
["Part 2 Tables 6.2,7.2,8.4,9.4,10.3,11.3,12.3,13.3,14.3"],
|
|
29
|
+
limitations=["Caller must complete material/service susceptibility screening before selecting a severity."])
|
|
30
|
+
def severity(x):
|
|
31
|
+
return {"severity_index":PROFILES[x.mechanism][3][("none","low","medium","high").index(x.susceptibility)]}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class InspectionCounts(Model):
|
|
35
|
+
a: int = Field(ge=0,le=100000,strict=True)
|
|
36
|
+
b: int = Field(ge=0,le=100000,strict=True)
|
|
37
|
+
c: int = Field(ge=0,le=100000,strict=True)
|
|
38
|
+
d: int = Field(ge=0,le=100000,strict=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Assessment(Model):
|
|
42
|
+
susceptibility: Susceptibility
|
|
43
|
+
confirmed_cracking: bool
|
|
44
|
+
unrepaired_cracks: bool
|
|
45
|
+
years_since_qualifying_inspection: NonNegative
|
|
46
|
+
inspections: InspectionCounts
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Monitoring(Model):
|
|
50
|
+
monitoring: Literal["none","process_variables","hydrogen_probes","both"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@calculation("damage/hic-monitoring-factor", "HIC/SOHIC monitoring adjustment factor", Monitoring,
|
|
54
|
+
["Part 2 Tables 9.5,14.4; PDF pp193,221"])
|
|
55
|
+
def monitoring_factor(x):
|
|
56
|
+
return {"monitoring_factor":{"none":1,"process_variables":2,"hydrogen_probes":2,"both":4}[x.monitoring]}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class HicAssessment(Assessment):
|
|
60
|
+
monitoring: Literal["none","process_variables","hydrogen_probes","both"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def assess(mechanism,x):
|
|
64
|
+
ensure(not x.unrepaired_cracks,"Detected unrepaired cracks require an FFS-based evaluation", "ffs_required")
|
|
65
|
+
susceptibility="high" if x.confirmed_cracking else x.susceptibility
|
|
66
|
+
svi=severity(Severity(mechanism=mechanism,susceptibility=susceptibility))["severity_index"]
|
|
67
|
+
counts=inspection_equivalent(InspectionEquivalent(mechanism="scc",**x.inspections.model_dump()))
|
|
68
|
+
grade,count="E",0
|
|
69
|
+
for candidate in "abcd":
|
|
70
|
+
if counts[candidate]:
|
|
71
|
+
grade,count=candidate.upper(),counts[candidate]
|
|
72
|
+
break
|
|
73
|
+
ensure(count<=6,"Equivalent inspection count exceeds Table 6.3 support; no extrapolation")
|
|
74
|
+
base=scc_base(SccBase(severity_index=svi,inspection_grade=grade,inspection_count=count))["base_damage_factor"]
|
|
75
|
+
monitoring=monitoring_factor(Monitoring(monitoring=x.monitoring))["monitoring_factor"] if isinstance(x,HicAssessment) else 1
|
|
76
|
+
age_factor=max(x.years_since_qualifying_inspection,1)**1.1
|
|
77
|
+
return {"effective_susceptibility":susceptibility,"severity_index":svi,"equivalent_inspection_grade":grade,
|
|
78
|
+
"equivalent_inspection_count":count,"base_damage_factor":base,"age_factor":age_factor,
|
|
79
|
+
"monitoring_factor":monitoring,"damage_factor":base*age_factor/monitoring}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def register_mechanism(mechanism):
|
|
83
|
+
section,table,eq,_=PROFILES[mechanism]
|
|
84
|
+
model=HicAssessment if mechanism.startswith("hic-sohic") else Assessment
|
|
85
|
+
def calculate(x):
|
|
86
|
+
return assess(mechanism,x)
|
|
87
|
+
calculate.__name__=mechanism.replace("-","_")
|
|
88
|
+
return calculation("damage/"+mechanism,f"{mechanism} damage factor from qualified susceptibility and inspections",model,
|
|
89
|
+
[f"Part 2 §{section}.6, Table {table}, eq (2.{eq}), §3.4.3 and Table 6.3"],
|
|
90
|
+
limitations=["Material/service screening and susceptibility must be independently established. Inspection counts and age must follow the mechanism-specific qualifying period. Confirmed cracking forces High; unrepaired cracks are rejected for separate FFS. Ambiguous Table 6.3 E/500 cell remains unsupported."])(calculate)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
for _mechanism in PROFILES:
|
|
94
|
+
globals()[_mechanism.replace("-","_")]=register_mechanism(_mechanism)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class HscSusceptibility(Model):
|
|
98
|
+
measured_brinnell_hardness: Positive
|
|
99
|
+
pwht: bool
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@calculation("damage/hsc-hf-susceptibility", "HSC-HF susceptibility of eligible steel weldments", HscSusceptibility,
|
|
103
|
+
["Part 2 Table 13.2; PDF p215"], limitations=["Eligible material and HF exposure must be screened separately. Brinnell must be measured as Brinnell, not converted from microhardness."])
|
|
104
|
+
def hsc_susceptibility(x):
|
|
105
|
+
column=0 if x.measured_brinnell_hardness<200 else (1 if x.measured_brinnell_hardness<=237 else 2)
|
|
106
|
+
return {"susceptibility":(("none","low","high") if x.pwht else ("low","medium","high"))[column]}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SscSusceptibility(HscSusceptibility):
|
|
110
|
+
environmental_severity: Literal["low","moderate","high"]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@calculation("damage/ssc-susceptibility", "SSC hardness/heat-treatment susceptibility", SscSusceptibility,
|
|
114
|
+
["Part 2 Table 8.3; PDF p187"], limitations=["Environmental severity and cyanide-related susceptibility upgrade must be established separately. Table 'Not' is normalized to none."])
|
|
115
|
+
def ssc_susceptibility(x):
|
|
116
|
+
column=0 if x.measured_brinnell_hardness<200 else (1 if x.measured_brinnell_hardness<=237 else 2)
|
|
117
|
+
rows={False:{"high":("low","medium","high"),"moderate":("low","medium","high"),"low":("low","low","medium")},
|
|
118
|
+
True:{"high":("none","low","medium"),"moderate":("none","none","low"),"low":("none","none","none")}}
|
|
119
|
+
return {"susceptibility":rows[x.pwht][x.environmental_severity][column]}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class HicMaterial(Model):
|
|
123
|
+
steel_sulfur_weight_percent: NonNegative = Field(le=100)
|
|
124
|
+
product_form: Literal["plate_or_welded","seamless_or_extruded_pipe"]
|
|
125
|
+
pwht: bool
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@calculation("damage/hic-hf-susceptibility", "HIC/SOHIC-HF steel/product susceptibility", HicMaterial,
|
|
129
|
+
["Part 2 Table 14.2; PDF p220"], limitations=["Eligible steel with HF exposure only; material/service screening remains separate."])
|
|
130
|
+
def hic_hf_susceptibility(x):
|
|
131
|
+
if x.product_form=="seamless_or_extruded_pipe": level="low"
|
|
132
|
+
elif x.steel_sulfur_weight_percent>.01: level="high"
|
|
133
|
+
else: level="medium" if x.pwht else "high"
|
|
134
|
+
return {"susceptibility":level}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class HicH2sMaterial(HicMaterial):
|
|
138
|
+
environmental_severity: Literal["low","moderate","high"]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@calculation("damage/hic-h2s-susceptibility", "HIC/SOHIC-H2S steel/product susceptibility", HicH2sMaterial,
|
|
142
|
+
["Part 2 Table 9.3; PDF p193"], limitations=["Environmental severity, existing-crack rule and cyanide upgrade must be established separately."])
|
|
143
|
+
def hic_h2s_susceptibility(x):
|
|
144
|
+
if x.product_form=="seamless_or_extruded_pipe":
|
|
145
|
+
level="medium" if x.environmental_severity=="high" and not x.pwht else "low"
|
|
146
|
+
elif x.steel_sulfur_weight_percent>.01:
|
|
147
|
+
level={"high":("high","high"),"moderate":("high","medium"),"low":("medium","low")}[x.environmental_severity][x.pwht]
|
|
148
|
+
else:
|
|
149
|
+
level={"high":("high","medium"),"moderate":("medium","low"),"low":("low","low")}[x.environmental_severity][x.pwht]
|
|
150
|
+
return {"susceptibility":level}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ClsccSusceptibility(Model):
|
|
154
|
+
temperature_k: Positive
|
|
155
|
+
temperature_basis: Literal["printed_si", "printed_us_converted"] = "printed_si"
|
|
156
|
+
ph_over_10: bool
|
|
157
|
+
chloride_band_ppm: Literal["1-10","11-100","101-1000",">1000"]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@calculation("damage/clscc-susceptibility", "CLSCC SI/US temperature/chloride susceptibility", ClsccSusceptibility,
|
|
161
|
+
["Part 2 Tables 12.2M/12.2; PDF pp210-211"], limitations=["Eligible material and chloride band selected by caller; no interpolation across concentration gaps. Temperature basis is explicit. For pH>10 at exactly 93 C in the SI table, overlapping source rows require manual resolution."])
|
|
162
|
+
def clscc_susceptibility(x):
|
|
163
|
+
t=x.temperature_k-273.15
|
|
164
|
+
column=("1-10","11-100","101-1000",">1000").index(x.chloride_band_ppm)
|
|
165
|
+
if x.temperature_basis == "printed_si":
|
|
166
|
+
if x.ph_over_10:
|
|
167
|
+
ensure(abs(t-93)>1e-9,"Source rows overlap at pH>10 and 93 C", "ambiguous_source")
|
|
168
|
+
row=0 if t<=38 else (1 if t<93 else (2 if t<=149 else 3))
|
|
169
|
+
rows=[("none",)*4,("low",)*4,("low","low","low","medium"),("medium","medium","medium","high")]
|
|
170
|
+
else:
|
|
171
|
+
row=0 if t<=38 else (1 if t<=66 else (2 if t<=93 else (3 if t<=149 else 4)))
|
|
172
|
+
rows=[("low","low","low","medium"),("low","medium","medium","high"),("medium","medium","high","high"),("medium","high","high","high"),("high",)*4]
|
|
173
|
+
else:
|
|
174
|
+
tf=t*9/5+32
|
|
175
|
+
if x.ph_over_10:
|
|
176
|
+
row=0 if tf<=100 else (1 if tf<=200 else (2 if tf<=300 else 3))
|
|
177
|
+
rows=[("none",)*4,("low",)*4,("low","low","low","medium"),("medium","medium","medium","high")]
|
|
178
|
+
else:
|
|
179
|
+
row=0 if tf<=100 else (1 if tf<=150 else (2 if tf<=200 else (3 if tf<=300 else 4)))
|
|
180
|
+
rows=[("low","low","low","medium"),("low","medium","medium","high"),("medium","medium","high","high"),("medium","high","high","high"),("high",)*4]
|
|
181
|
+
return {"susceptibility":rows[row][column]}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class WetH2sEnvironment(Model):
|
|
185
|
+
water_ph: float = Field(allow_inf_nan=False,strict=True)
|
|
186
|
+
h2s_in_water_ppm: NonNegative
|
|
187
|
+
cyanides_present: bool
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@calculation("damage/wet-h2s-environment", "Wet H2S environmental severity and cyanide flag", WetH2sEnvironment,
|
|
191
|
+
["Part 2 Tables 8.2,9.2; PDF pp187,193"],
|
|
192
|
+
limitations=["The source pH rows leave gaps and the H2S columns overlap at 1000 ppm; these inputs require manual classification and are rejected. Cyanide flag requires upgrading the subsequent susceptibility, not the environmental-severity row."])
|
|
193
|
+
def wet_h2s_environment(x):
|
|
194
|
+
p=x.water_ph
|
|
195
|
+
if p<5.5: row=0
|
|
196
|
+
elif p<=7.5: row=1
|
|
197
|
+
elif 7.6<=p<=8.3: row=2
|
|
198
|
+
elif 8.4<=p<=8.9: row=3
|
|
199
|
+
elif p>9: row=4
|
|
200
|
+
else: ensure(False,"Water pH falls in a source-table gap", "ambiguous_source")
|
|
201
|
+
concentration=x.h2s_in_water_ppm
|
|
202
|
+
ensure(concentration!=1000,"Source H2S columns overlap at 1000 ppm", "ambiguous_source")
|
|
203
|
+
column=0 if concentration<50 else (1 if concentration<1000 else (2 if concentration<=10000 else 3))
|
|
204
|
+
rows=[("low","moderate","high","high"),("low","low","low","moderate"),
|
|
205
|
+
("low","moderate","moderate","moderate"),("low","moderate","moderate","high"),("low","moderate","high","high")]
|
|
206
|
+
return {"environmental_severity":rows[row][column],"susceptibility_upgrade_required":x.cyanides_present and p>8.3 and concentration>1000}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class AcsccSusceptibility(Model):
|
|
210
|
+
water_ph_band: Literal["below_7.5","7.5_to_8","8_to_9","at_least_9"]
|
|
211
|
+
carbonate_ppm: NonNegative
|
|
212
|
+
pwht: bool
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@calculation("damage/acscc-susceptibility", "ACSCC carbonate/PWHT susceptibility", AcsccSusceptibility,
|
|
216
|
+
["Part 2 Table 10.2; PDF p199"], limitations=["Caller selects source pH band and establishes eligible material/service. Band endpoint interpretation is not silently inferred from rounded pH readings."])
|
|
217
|
+
def acscc_susceptibility(x):
|
|
218
|
+
if x.pwht or x.water_ph_band=="below_7.5": result="none"
|
|
219
|
+
elif x.water_ph_band=="at_least_9": result="high"
|
|
220
|
+
elif x.carbonate_ppm<100: result="low"
|
|
221
|
+
else: result="medium" if x.water_ph_band=="7.5_to_8" else "high"
|
|
222
|
+
return {"susceptibility":result}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class PasccSusceptibility(Model):
|
|
226
|
+
operating_temperature_k: Positive
|
|
227
|
+
material_group: Literal["regular_300ss_or_alloy600_800","h_grade_300ss","l_grade_300ss","321ss","347ss_alloy20_625_or_austenitic_overlay"]
|
|
228
|
+
heat_treatment: Literal["solution_annealed","stabilized_before_welding","stabilized_after_welding"]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@calculation("damage/pascc-susceptibility", "PASCC temperature/material/heat-treatment susceptibility", PasccSusceptibility,
|
|
232
|
+
["Part 2 Table 11.2; PDF p205"], limitations=["Eligible material/service must be established separately. Blank heat-treatment cells are unsupported; SI threshold is 427 C."])
|
|
233
|
+
def pascc_susceptibility(x):
|
|
234
|
+
hot=x.operating_temperature_k>=700.15
|
|
235
|
+
if x.material_group in ("regular_300ss_or_alloy600_800","h_grade_300ss","l_grade_300ss"):
|
|
236
|
+
ensure(x.heat_treatment=="solution_annealed","Selected PASCC heat-treatment cell is blank", "unsupported_table_cell")
|
|
237
|
+
result={"regular_300ss_or_alloy600_800":("medium","high"),"h_grade_300ss":("high","high"),"l_grade_300ss":("low","medium")}[x.material_group][hot]
|
|
238
|
+
elif x.material_group=="321ss":
|
|
239
|
+
result="low" if x.heat_treatment=="stabilized_after_welding" else ("high" if hot else "medium")
|
|
240
|
+
else:
|
|
241
|
+
result="medium" if hot and x.heat_treatment=="solution_annealed" else "low"
|
|
242
|
+
return {"susceptibility":result}
|
api581/schema.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Read-only accessors for packaged Python and digital-twin JSON contracts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _read(name: str) -> dict[str, Any]:
|
|
11
|
+
resource = files("api581").joinpath("data", name)
|
|
12
|
+
return json.loads(resource.read_text(encoding="utf-8"))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def python_api_schema() -> dict[str, Any]:
|
|
16
|
+
"""Return a fresh mapping of the packaged calculation contracts."""
|
|
17
|
+
return _read("python-api.schema.json")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def digital_twin_schema() -> dict[str, Any]:
|
|
21
|
+
"""Return a fresh mapping of the packaged twin and batch contracts."""
|
|
22
|
+
return _read("digital-twin.schema.json")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def standard_schema() -> dict[str, Any]:
|
|
26
|
+
"""Return the packaged standard metadata and coverage contracts."""
|
|
27
|
+
return _read("standard.schema.json")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def packaged_schema_hash(schema: str) -> str:
|
|
31
|
+
"""Return the SHA-256 of a packaged schema by its public name."""
|
|
32
|
+
names = {
|
|
33
|
+
"python_api": "python-api.schema.json",
|
|
34
|
+
"digital_twin": "digital-twin.schema.json",
|
|
35
|
+
"standard": "standard.schema.json",
|
|
36
|
+
}
|
|
37
|
+
try:
|
|
38
|
+
name = names[schema]
|
|
39
|
+
except KeyError as exc:
|
|
40
|
+
raise ValueError(f"Unknown packaged schema: {schema}") from exc
|
|
41
|
+
resource = files("api581").joinpath("data", name)
|
|
42
|
+
return sha256(resource.read_bytes()).hexdigest()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = ["python_api_schema", "digital_twin_schema", "standard_schema", "packaged_schema_hash"]
|
api581/slabx_adapter.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Optional, process-isolated native SLABx pool adapter; no fallback physics."""
|
|
2
|
+
import json
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import importlib.util
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Literal
|
|
10
|
+
from pydantic import Field, model_validator, ValidationError
|
|
11
|
+
from .core import Model, Positive, NonNegative, Fraction, DomainError, REGISTRY, calculation, ensure
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NativeSubstance(Model):
|
|
15
|
+
fluid_id: str = Field(min_length=1,max_length=100)
|
|
16
|
+
molecular_weight_kg_kmol: Positive
|
|
17
|
+
vapor_heat_capacity_j_kg_k: Positive
|
|
18
|
+
liquid_heat_capacity_j_kg_k: Positive
|
|
19
|
+
latent_heat_j_kg: Positive
|
|
20
|
+
normal_boiling_point_k: Positive
|
|
21
|
+
liquid_density_kg_m3: Positive
|
|
22
|
+
property_source: str = Field(min_length=1,max_length=1000)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NativeEngineCapabilityRequest(Model):
|
|
26
|
+
requested_engines: list[Literal["slabx", "slabx-lh2"]] = Field(
|
|
27
|
+
default_factory=lambda: ["slabx", "slabx-lh2"], min_length=1, max_length=2
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@calculation("dispersion/native-engine-capabilities", "Inspect optional SLABx engine capabilities", NativeEngineCapabilityRequest,
|
|
32
|
+
["Native adapter deployment contract; tested mappings SLABx1.0.8 and SLABx-LH2 0.2.0"],
|
|
33
|
+
origin="optional_native_engine_capability_probe", limitations=[
|
|
34
|
+
"Deployment probe only; it performs no dispersion calculation and does not validate a native engine scientifically. Missing or mismatched packages are reported as status values rather than silently replaced. Available operations are the adapter's supported constant pool/jet boundaries, not a claim that every source domain is implemented."])
|
|
35
|
+
def native_engine_capabilities(x):
|
|
36
|
+
required = {"slabx": "1.0.8", "slabx-lh2": "0.2.0"}
|
|
37
|
+
requested = list(dict.fromkeys(x.requested_engines))
|
|
38
|
+
if "slabx-lh2" in requested and "slabx" not in requested:
|
|
39
|
+
requested.insert(0, "slabx")
|
|
40
|
+
installed: dict[str, str | None] = {}
|
|
41
|
+
status: dict[str, str] = {}
|
|
42
|
+
module_import_status: dict[str, str] = {}
|
|
43
|
+
module_names = {"slabx": "slabx", "slabx-lh2": "slabx_lh2"}
|
|
44
|
+
for name in requested:
|
|
45
|
+
try:
|
|
46
|
+
version = importlib.metadata.version(name)
|
|
47
|
+
except importlib.metadata.PackageNotFoundError:
|
|
48
|
+
version = None
|
|
49
|
+
installed[name] = version
|
|
50
|
+
status[name] = "missing" if version is None else "available" if version == required[name] else "version_mismatch"
|
|
51
|
+
if version is None:
|
|
52
|
+
module_import_status[name] = "not_installed"
|
|
53
|
+
else:
|
|
54
|
+
try:
|
|
55
|
+
module_import_status[name] = (
|
|
56
|
+
"importable" if importlib.util.find_spec(module_names[name]) is not None
|
|
57
|
+
else "not_importable"
|
|
58
|
+
)
|
|
59
|
+
except (ImportError, ModuleNotFoundError, ValueError):
|
|
60
|
+
module_import_status[name] = "not_importable"
|
|
61
|
+
if status[name] == "available" and module_import_status[name] != "importable":
|
|
62
|
+
status[name] = "not_importable"
|
|
63
|
+
return {"requested_engines": requested, "required_versions": {name: required[name] for name in requested},
|
|
64
|
+
"installed_versions": installed, "engine_status": status,
|
|
65
|
+
"module_import_status": module_import_status,
|
|
66
|
+
"worker_isolation": "subprocess",
|
|
67
|
+
"supported_operations": ["pool_centreline", "pool_footprint", "pool_flash_fire", "jet_centreline", "jet_footprint"],
|
|
68
|
+
"probe_scope": "package_presence_version_and_importability"}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class NativeAtmosphere(Model):
|
|
72
|
+
wind_speed_m_s: Positive
|
|
73
|
+
wind_reference_height_m: Positive
|
|
74
|
+
temperature_k: Positive
|
|
75
|
+
pressure_pa_abs: Literal[101325.0]
|
|
76
|
+
relative_humidity_fraction: Fraction
|
|
77
|
+
roughness_length_m: Positive
|
|
78
|
+
stability: Literal["A","B","C","D","E","F"]
|
|
79
|
+
|
|
80
|
+
@model_validator(mode="after")
|
|
81
|
+
def wind_reference(self):
|
|
82
|
+
if self.wind_reference_height_m <= self.roughness_length_m:
|
|
83
|
+
raise ValueError("Wind reference height must exceed roughness length")
|
|
84
|
+
if self.pressure_pa_abs != 101325.:
|
|
85
|
+
raise ValueError("Verified constant-property native mapping requires 101325 Pa")
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class NativeRun(Model):
|
|
90
|
+
engine: Literal["slabx","slabx-lh2"]
|
|
91
|
+
substance: NativeSubstance
|
|
92
|
+
atmosphere: NativeAtmosphere
|
|
93
|
+
receptor_height_m: NonNegative
|
|
94
|
+
averaging_time_s: Positive
|
|
95
|
+
plume_distance_limit_m: Positive = Field(le=1e6)
|
|
96
|
+
puff_time_limit_s: Positive | None = None
|
|
97
|
+
puff_steps: int = Field(default=40,ge=10,le=1000,strict=True)
|
|
98
|
+
threshold_volume_fraction: Positive = Field(le=1)
|
|
99
|
+
threshold_source: str = Field(min_length=1,max_length=1000)
|
|
100
|
+
source_qualification: str = Field(min_length=1,max_length=1000)
|
|
101
|
+
meander_closure: Literal["ermak","sigma_theta"]
|
|
102
|
+
lh2_plume_width_coupling: bool
|
|
103
|
+
timeout_s: Positive = Field(default=120,le=600)
|
|
104
|
+
|
|
105
|
+
@model_validator(mode="after")
|
|
106
|
+
def qualifications(self):
|
|
107
|
+
if not all(v.strip() for v in (self.threshold_source,self.source_qualification,self.substance.property_source)):
|
|
108
|
+
raise ValueError("Source, property and threshold qualifications must be nonblank")
|
|
109
|
+
if self.engine == "slabx" and self.lh2_plume_width_coupling:
|
|
110
|
+
raise ValueError("LH2 correction cannot be selected for slabx baseline")
|
|
111
|
+
if self.engine == "slabx-lh2" and (self.substance.fluid_id != "hydrogen" or not 2.0 <= self.substance.molecular_weight_kg_kmol <= 2.02):
|
|
112
|
+
raise ValueError("LH2 adapter requires explicitly identified hydrogen and qualified hydrogen properties")
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class NativePool(NativeRun):
|
|
117
|
+
vapor_source_rate_kg_s: Positive
|
|
118
|
+
source_area_m2: Positive
|
|
119
|
+
release_duration_s: Positive
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class NativePoolFootprint(NativePool):
|
|
123
|
+
crosswind_half_span_m: Positive
|
|
124
|
+
crosswind_points_per_side: int = Field(default=65,ge=3,le=501,strict=True)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@calculation("dispersion/native-pool-centreline", "Run native SLABx/SLABx-LH2 constant vapor pool in an isolated process", NativePool,
|
|
128
|
+
["Part3 §§5.8.5-5.8.6,5.9: external dispersion boundary",
|
|
129
|
+
"SLABx1.0.8 core.source.EvaporatingPool/core.plume.run_dispersion/post.concentration.concentration_field",
|
|
130
|
+
"SLABx-LH2 0.2.0 water_ice.with_sublimation/plume_width_coupling/diagnostics.premise_summary"],
|
|
131
|
+
origin="optional_native_dispersion_adapter",
|
|
132
|
+
limitations=["Optional native packages required: tested mapping slabx1.0.8 and slabx-lh2 0.2.0. Each run uses a fresh Python subprocess so LH2 module-global corrections cannot alter host calculations. No silent alternate engine/version or fallback model.",
|
|
133
|
+
"Only constant finite-duration ground-level pure-vapor evaporating pool at normal boiling temperature and101325Pa. Required rate is evaporated vapor, not liquid feed/orifice rate. Caller qualifies physical source, material properties, threshold, wind/stability and averaging. No automatic conversion from draft SourcePoint profiles, jets, droplets, mixtures, growing pools or source-ledger transitions.",
|
|
134
|
+
"Native x_max limits the plume grid, not the final puff extent. plume_distance_limit_m maps exactly to that parameter; puff_time_limit_s maps to native t_max, or null preserves native automatic puff duration. Actual grid extent is reported. Threshold censoring uses the final returned native grid, not an assumed x_max cutoff.",
|
|
135
|
+
"Returns time-averaged native centreline volume fractions and native endpoint interpolation, not a 2D contour area or flammable cloud mass. Last-node exceedance is boundary-censored, not a solved separation distance. No distance-to-circle area substitution. No LH2 safety factor is applied.",
|
|
136
|
+
"LH2 premise violation makes threshold_distance_m null while native_threshold_distance_m and centreline remain diagnostics. A passing premise check is not experimental/site validation. Baseline slabx applicability remains not_evaluated. External package versions/source fingerprints accompany result; API581 software-test status is not native model certification."])
|
|
137
|
+
def native_pool(x):
|
|
138
|
+
return _execute_worker(x,"dispersion/native-pool-centreline","centreline")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _execute_worker(x,identifier,operation):
|
|
142
|
+
try:
|
|
143
|
+
serialized = json.dumps({**x.model_dump(mode="json"),"_operation":operation},allow_nan=False)
|
|
144
|
+
# Keep the isolated worker usable from both an installed wheel and a
|
|
145
|
+
# source checkout. ``python -m`` otherwise cannot discover ``src/``
|
|
146
|
+
# when the parent process was started by a repository test runner.
|
|
147
|
+
worker_env = os.environ.copy()
|
|
148
|
+
package_root = str(Path(__file__).resolve().parents[1])
|
|
149
|
+
existing_pythonpath = worker_env.get("PYTHONPATH", "")
|
|
150
|
+
worker_env["PYTHONPATH"] = (
|
|
151
|
+
package_root if not existing_pythonpath
|
|
152
|
+
else package_root + os.pathsep + existing_pythonpath
|
|
153
|
+
)
|
|
154
|
+
process = subprocess.run([sys.executable,"-m","api581._slabx_worker"],input=serialized,
|
|
155
|
+
text=True,encoding="utf-8",capture_output=True,timeout=x.timeout_s,
|
|
156
|
+
env=worker_env)
|
|
157
|
+
except subprocess.TimeoutExpired as exc:
|
|
158
|
+
raise DomainError("engine_timeout","Native dispersion worker exceeded timeout") from exc
|
|
159
|
+
except OSError as exc:
|
|
160
|
+
raise DomainError("engine_execution_failed","Native dispersion worker could not start") from exc
|
|
161
|
+
try:
|
|
162
|
+
payload = json.loads(process.stdout)
|
|
163
|
+
except (ValueError,TypeError) as exc:
|
|
164
|
+
diagnostic = (getattr(process, "stderr", "") or "").strip().replace("\n", " ")[:500]
|
|
165
|
+
message = "Native dispersion worker returned no valid result"
|
|
166
|
+
if diagnostic:
|
|
167
|
+
message += f": {diagnostic}"
|
|
168
|
+
raise DomainError("engine_protocol_failure", message) from exc
|
|
169
|
+
if not isinstance(payload,dict):
|
|
170
|
+
raise DomainError("engine_protocol_failure","Native dispersion worker result must be an object")
|
|
171
|
+
if process.returncode or "error" in payload:
|
|
172
|
+
diagnostic = (getattr(process, "stderr", "") or "").strip().replace("\n", " ")[:500]
|
|
173
|
+
message = payload.get("error", "Native engine failed")
|
|
174
|
+
if diagnostic and diagnostic not in message:
|
|
175
|
+
message += f": {diagnostic}"
|
|
176
|
+
raise DomainError(payload.get("code","engine_execution_failed"), message)
|
|
177
|
+
from .outputs import OUTPUT_MODELS
|
|
178
|
+
try:
|
|
179
|
+
return OUTPUT_MODELS[identifier].model_validate(payload).model_dump()
|
|
180
|
+
except ValidationError as exc:
|
|
181
|
+
raise DomainError("engine_result_invalid","Native worker output violates the typed result contract") from exc
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@calculation("dispersion/native-pool-footprint", "Native SLABx/SLABx-LH2 planar concentration threshold footprint", NativePoolFootprint,
|
|
185
|
+
["Part3 §§5.8.5-5.8.6,5.9: external dispersion footprint boundary",
|
|
186
|
+
"SLABx1.0.8 post.concentration.ConcentrationField.at_y"],origin="optional_native_dispersion_spatial_postprocessing",
|
|
187
|
+
limitations=["Same explicitly qualified finite ground-level vapor pool, package versions, property/atmosphere constraints and process isolation as native-pool-centreline. Uses native at_y on a symmetric crosswind grid at one receptor height/averaging time. No conversion from centreline distance to circular area.",
|
|
188
|
+
"Area integrates a piecewise-linear triangular concentration interpolant on the native downwind grid and user crosswind grid. Not an exact continuous contour; supplied resolution and span require convergence checks. Plume x_max is not the final puff extent. Returned field includes negative/upwind positions, and source expansion remains native.",
|
|
189
|
+
"Any threshold exceedance at a sampled grid edge or LH2 premise violation sets threshold_area_m2 to null. sampled_exceedance_area_m2 and native field remain diagnostics. No boundary truncation reported as a resolved footprint, no LH2 safety factor, mass integration or individual/societal-risk inference. Baseline native applicability remains not_evaluated."])
|
|
190
|
+
def native_pool_footprint(x):
|
|
191
|
+
return _execute_worker(x,"dispersion/native-pool-footprint","footprint")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class NativePoolFlashFire(NativePoolFootprint):
|
|
195
|
+
criterion_role: Literal["qualified_lfl"]
|
|
196
|
+
receptor_height_m: Literal[0.0]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@calculation("dispersion/native-pool-flash-fire", "Native LFL footprint through API581 flash-fire consequence", NativePoolFlashFire,
|
|
200
|
+
["Part3 §§5.8.6,4.8.2.3", "Native ConcentrationField.at_y plus sampled triangular threshold area"],
|
|
201
|
+
origin="native_dispersion_to_api581_flash_fire_composition",
|
|
202
|
+
limitations=[*REGISTRY["dispersion/native-pool-footprint"].limitations,
|
|
203
|
+
"Ground-level receptor height0 required by §5.8.6. Caller must declare threshold as a qualified fluid-specific LFL using criterion_role=qualified_lfl. A declaration does not validate thermodynamics, exposure averaging or field applicability. Censored/premise-violating footprints fail explicitly instead of becoming zero or valid fire consequence. Not-reached yields zero only on the supplied sampled interpolant. Personnel area=LFL footprint; equipment=.25*footprint. No ignition/event/GFF weighting."])
|
|
204
|
+
def native_pool_flash_fire(x):
|
|
205
|
+
from .effects import FlashFire, flash_fire
|
|
206
|
+
result = native_pool_footprint(NativePoolFootprint(**x.model_dump(exclude={"criterion_role"})))
|
|
207
|
+
ensure(result["threshold_area_m2"] is not None,
|
|
208
|
+
f"LFL footprint unusable for consequence: {result['footprint_status']}","dispersion_footprint_unusable")
|
|
209
|
+
effect = flash_fire(FlashFire(area_within_lfl_m2=result["threshold_area_m2"]))
|
|
210
|
+
return {**result,**effect,"criterion_role":x.criterion_role}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class NativeJetGeometry(NativeRun):
|
|
214
|
+
orientation: Literal["horizontal_downwind","vertical_up"]
|
|
215
|
+
jet_exit_area_m2: Positive
|
|
216
|
+
release_height_m: NonNegative
|
|
217
|
+
source_temperature_k: Positive
|
|
218
|
+
temperature_basis: Literal["require_consistent","allow_native_clamp"]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class NativeJet(NativeJetGeometry):
|
|
222
|
+
mass_source_rate_kg_s: Positive
|
|
223
|
+
release_duration_s: Positive
|
|
224
|
+
jet_liquid_mass_fraction: Fraction = Field(lt=1)
|
|
225
|
+
|
|
226
|
+
@model_validator(mode="after")
|
|
227
|
+
def temperature(self):
|
|
228
|
+
boiling = self.substance.normal_boiling_point_k
|
|
229
|
+
if self.temperature_basis == "require_consistent" and (
|
|
230
|
+
self.source_temperature_k < boiling or self.jet_liquid_mass_fraction > 0 and self.source_temperature_k != boiling):
|
|
231
|
+
raise ValueError("Native source is at boiling point with droplets, or at/above boiling for pure vapor; select explicit native clamp basis otherwise")
|
|
232
|
+
return self
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class NativeJetFootprint(NativeJet):
|
|
236
|
+
crosswind_half_span_m: Positive
|
|
237
|
+
crosswind_points_per_side: int = Field(default=65,ge=3,le=501,strict=True)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
_JET_LIMITS = [
|
|
241
|
+
"Actual slabx1.0.8 HorizontalJet or VerticalJet at constant finite rate/duration, optional slabx-lh2 0.2.0 water/plume-width corrections in an isolated process. Direction is downwind horizontal or upward vertical only. No arbitrary yaw/downward/impinging jet, mixture carrier, physical-transition ledger or underexpanded-jet solver.",
|
|
242
|
+
"Input total pure-material vapor+droplet mass rate, effective atmospheric exit area, source temperature and liquid MASS fraction, not gas volume fraction. Native velocity derives from q/(rho*A); this API does not substitute a vessel choked-orifice area for a qualified expanded area. Source liquid fraction must be below1. Equilibrium droplets use native default closures; kinetic evaporation/rainout flags are not implicitly enabled.",
|
|
243
|
+
"Temperature basis is explicit: native clamps vapor below boiling to boiling and all droplet sources to boiling. require_consistent rejects a conflicting input; allow_native_clamp reports requested and used temperature. No EOS/flash reconciliation inferred.101325Pa ambient and supplied constant properties only.",
|
|
244
|
+
"Vertical buoyant-source rise is delegated to the installed SLABx VerticalJet implementation and returned as native diagnostics. The adapter does not substitute a separate plume model or apply an empirical safety factor. LH2 premise diagnostics still gate distance/area; passing them does not validate near-field jets or field applicability.",
|
|
245
|
+
"Native source/handoff may begin at x=1m or beyond the vertical rise. Full upstream/near-field extent is not reconstructed. A footprint touching first/last or lateral grid edge is censored, and threshold_area_m2 remains null. No source-boundary extrapolation or circle-area substitution. x_max limits plume, not full puff extent.",
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@calculation("dispersion/native-jet-centreline", "Native horizontal/vertical jet centreline dispersion", NativeJet,
|
|
250
|
+
["Part3 §§5.3,5.7,5.8.5-5.8.6,5.9 external dispersion source boundary",
|
|
251
|
+
"SLABx1.0.8 core.source.HorizontalJet/core.vertical_jet.VerticalJet"],origin="optional_native_jet_dispersion_adapter",limitations=_JET_LIMITS)
|
|
252
|
+
def native_jet(x):
|
|
253
|
+
return _execute_worker(x,"dispersion/native-jet-centreline","jet_centreline")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@calculation("dispersion/native-jet-footprint", "Native horizontal/vertical jet sampled threshold footprint", NativeJetFootprint,
|
|
257
|
+
["Part3 §§5.8.6,5.9 external spatial footprint boundary", "SLABx1.0.8 ConcentrationField.at_y"],
|
|
258
|
+
origin="optional_native_jet_spatial_postprocessing",limitations=_JET_LIMITS+[
|
|
259
|
+
"Planar area uses the same explicit main-diagonal triangular interpolant as threshold-grid-area. Native x grid and user symmetric y grid require resolution checks. Raw field and within-grid diagnostic area remain available when censored/premise-violating; no valid full footprint is claimed."])
|
|
260
|
+
def native_jet_footprint(x):
|
|
261
|
+
return _execute_worker(x,"dispersion/native-jet-footprint","jet_footprint")
|