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
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
"""Inspection effectiveness classification from API 581 Annex 2.C examples."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from .core import Model, NonNegative, calculation, ensure
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EffectivenessCategory(Model):
|
|
11
|
+
category: Literal["A", "B", "C", "D", "E"]
|
|
12
|
+
owner_user_basis: str = Field(min_length=1, max_length=1000)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@calculation(
|
|
16
|
+
"inspection/effectiveness-category",
|
|
17
|
+
"API 581 Annex 2.C common inspection effectiveness category metadata",
|
|
18
|
+
EffectivenessCategory,
|
|
19
|
+
["Part 2 Annex C §2.C.2-§2.C.2.3; PDF pp443-445"],
|
|
20
|
+
origin="api581_annex_2c2_effectiveness_category",
|
|
21
|
+
limitations=[
|
|
22
|
+
"Returns the common qualitative category metadata only. It does not decide whether a particular NDE method satisfies a mechanism-specific table; use the dedicated inspection effectiveness APIs for that decision.",
|
|
23
|
+
"Category E means ineffective or insufficiently evidenced for inspection credit. A-D labels are qualitative and do not by themselves define a numeric damage-factor reduction."
|
|
24
|
+
],
|
|
25
|
+
)
|
|
26
|
+
def effectiveness_category(x):
|
|
27
|
+
labels = {
|
|
28
|
+
"A": "Highly Effective", "B": "Usually Effective",
|
|
29
|
+
"C": "Fairly Effective", "D": "Poorly Effective", "E": "Ineffective",
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
"category": x.category,
|
|
33
|
+
"ordinal": {"A": 1, "B": 2, "C": 3, "D": 4, "E": 5}[x.category],
|
|
34
|
+
"label": labels[x.category],
|
|
35
|
+
"inspection_credit_allowed": x.category != "E",
|
|
36
|
+
"owner_user_basis": x.owner_user_basis,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ThinningInspectionEffectiveness(Model):
|
|
41
|
+
damage_type: Literal["general", "local"]
|
|
42
|
+
inspection_mode: Literal["intrusive", "non_intrusive"]
|
|
43
|
+
visual_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
44
|
+
spot_thickness_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
45
|
+
advanced_scan_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
46
|
+
profile_radiography_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
47
|
+
follow_up_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
48
|
+
inspection_quality_high: bool = True
|
|
49
|
+
locations_qualified: bool = True
|
|
50
|
+
technique_effective: bool = True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@calculation(
|
|
54
|
+
"inspection/thinning-effectiveness",
|
|
55
|
+
"API 581 Annex 2.C.8 thinning inspection effectiveness category",
|
|
56
|
+
ThinningInspectionEffectiveness,
|
|
57
|
+
["Part 2 Annex C §2.C.8.1-§2.C.8.2, Tables 2.C.8.1-2.C.8.2; PDF pp454-455"],
|
|
58
|
+
origin="api581_annex_2c8_inspection_effectiveness",
|
|
59
|
+
limitations=[
|
|
60
|
+
"Implements the printed general/local thinning percentage examples only. It classifies inspection effectiveness; it does not calculate inspection uncertainty, PoF, damage factor or inspection credit.",
|
|
61
|
+
"The caller must qualify inspection quality, CML/location selection and technique effectiveness. A failed qualification returns E rather than manufacturing credit. Thresholds follow the table's percentage bands; ambiguous technique descriptions are not inferred."
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
def thinning_effectiveness(x):
|
|
65
|
+
qualified = x.inspection_quality_high and x.locations_qualified and x.technique_effective
|
|
66
|
+
if not qualified:
|
|
67
|
+
return {
|
|
68
|
+
"effectiveness_category": "E",
|
|
69
|
+
"damage_type": x.damage_type,
|
|
70
|
+
"inspection_mode": x.inspection_mode,
|
|
71
|
+
"qualification_status": "failed",
|
|
72
|
+
"basis": "inspection_quality_or_location_or_technique_not_qualified",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
visual = x.visual_coverage_percent
|
|
76
|
+
spot = x.spot_thickness_coverage_percent
|
|
77
|
+
scan = x.advanced_scan_coverage_percent
|
|
78
|
+
profile = x.profile_radiography_coverage_percent
|
|
79
|
+
follow = x.follow_up_coverage_percent
|
|
80
|
+
|
|
81
|
+
if x.damage_type == "local":
|
|
82
|
+
if x.inspection_mode == "intrusive":
|
|
83
|
+
if visual >= 100 and follow >= 100:
|
|
84
|
+
category, basis = "A", "100_percent_visual_and_follow_up"
|
|
85
|
+
elif visual > 75 and follow >= 100:
|
|
86
|
+
category, basis = "B", "over_75_percent_visual_and_follow_up"
|
|
87
|
+
elif visual > 50 and follow >= 100:
|
|
88
|
+
category, basis = "C", "over_50_percent_visual_and_follow_up"
|
|
89
|
+
elif visual > 20 and follow >= 100:
|
|
90
|
+
category, basis = "D", "over_20_percent_visual_and_follow_up"
|
|
91
|
+
else:
|
|
92
|
+
category, basis = "E", "local_intrusive_threshold_not_met"
|
|
93
|
+
else:
|
|
94
|
+
coverage = max(spot, scan, profile)
|
|
95
|
+
if coverage >= 100:
|
|
96
|
+
category, basis = "A", "100_percent_suspect_area_coverage"
|
|
97
|
+
elif coverage > 75:
|
|
98
|
+
category, basis = "B", "over_75_percent_suspect_area_coverage"
|
|
99
|
+
elif coverage > 50:
|
|
100
|
+
category, basis = "C", "over_50_percent_suspect_area_coverage"
|
|
101
|
+
elif coverage > 20:
|
|
102
|
+
category, basis = "D", "over_20_percent_suspect_area_coverage"
|
|
103
|
+
else:
|
|
104
|
+
category, basis = "E", "local_non_intrusive_threshold_not_met"
|
|
105
|
+
elif x.inspection_mode == "intrusive":
|
|
106
|
+
if (visual > 50 and spot > 50) or scan >= 10 or profile >= 10:
|
|
107
|
+
category, basis = "A", "general_intrusive_high_coverage"
|
|
108
|
+
elif (visual > 25 and spot > 25) or scan > 5 or profile > 5:
|
|
109
|
+
category, basis = "B", "general_intrusive_usually_effective"
|
|
110
|
+
elif visual > 5 and spot > 5 or profile > 0:
|
|
111
|
+
category, basis = "C", "general_intrusive_fairly_effective"
|
|
112
|
+
elif visual > 0:
|
|
113
|
+
category, basis = "D", "general_intrusive_poorly_effective"
|
|
114
|
+
else:
|
|
115
|
+
category, basis = "E", "general_intrusive_threshold_not_met"
|
|
116
|
+
else:
|
|
117
|
+
if spot >= 100 or scan >= 10 or profile >= 10:
|
|
118
|
+
category, basis = "A", "general_non_intrusive_high_coverage"
|
|
119
|
+
elif spot > 75 or scan > 5 or profile > 5:
|
|
120
|
+
category, basis = "B", "general_non_intrusive_usually_effective"
|
|
121
|
+
elif spot > 50 or profile > 0:
|
|
122
|
+
category, basis = "C", "general_non_intrusive_fairly_effective"
|
|
123
|
+
elif spot > 25:
|
|
124
|
+
category, basis = "D", "general_non_intrusive_poorly_effective"
|
|
125
|
+
else:
|
|
126
|
+
category, basis = "E", "general_non_intrusive_threshold_not_met"
|
|
127
|
+
|
|
128
|
+
ensure(category in {"A", "B", "C", "D", "E"}, "Invalid inspection effectiveness category")
|
|
129
|
+
return {
|
|
130
|
+
"effectiveness_category": category,
|
|
131
|
+
"damage_type": x.damage_type,
|
|
132
|
+
"inspection_mode": x.inspection_mode,
|
|
133
|
+
"qualification_status": "qualified",
|
|
134
|
+
"basis": basis,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SccInspectionEffectiveness(Model):
|
|
139
|
+
mechanism_family: Literal["weld_scc", "clscc_pta"]
|
|
140
|
+
inspection_mode: Literal["intrusive", "non_intrusive"]
|
|
141
|
+
targeted_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
142
|
+
ultrasonic_scan_percent: NonNegative = Field(default=0, le=100)
|
|
143
|
+
radiography_percent: NonNegative = Field(default=0, le=100)
|
|
144
|
+
acoustic_emission_follow_up_percent: NonNegative = Field(default=0, le=100)
|
|
145
|
+
relevant_indication_follow_up_percent: NonNegative = Field(default=0, le=100)
|
|
146
|
+
inspection_quality_high: bool = True
|
|
147
|
+
locations_qualified: bool = True
|
|
148
|
+
technique_effective: bool = True
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@calculation(
|
|
152
|
+
"inspection/scc-effectiveness",
|
|
153
|
+
"API 581 Annex 2.C.9 stress-corrosion-cracking inspection effectiveness category",
|
|
154
|
+
SccInspectionEffectiveness,
|
|
155
|
+
["Part 2 Annex C §2.C.9.1-§2.C.9.9, Tables 2.C.9.1-2.C.9.9; PDF pp456-464"],
|
|
156
|
+
origin="api581_annex_2c9_inspection_effectiveness",
|
|
157
|
+
limitations=[
|
|
158
|
+
"Implements the common percentage bands in the SCC example tables. It classifies inspection effectiveness only; it does not calculate SCC susceptibility, inspection uncertainty, PoF or damage factor.",
|
|
159
|
+
"For CLSCC/PTA the source provides no non-intrusive A-level technique; this API returns E unless a lower category threshold is met. Caller must qualify inspection quality, suspect-area definition and indication follow-up."
|
|
160
|
+
],
|
|
161
|
+
)
|
|
162
|
+
def scc_effectiveness(x):
|
|
163
|
+
if not (x.inspection_quality_high and x.locations_qualified and x.technique_effective):
|
|
164
|
+
return {"effectiveness_category": "E", "mechanism_family": x.mechanism_family,
|
|
165
|
+
"inspection_mode": x.inspection_mode, "qualification_status": "failed",
|
|
166
|
+
"basis": "inspection_quality_or_location_or_technique_not_qualified"}
|
|
167
|
+
|
|
168
|
+
targeted = x.targeted_coverage_percent
|
|
169
|
+
scan = x.ultrasonic_scan_percent
|
|
170
|
+
radiography = x.radiography_percent
|
|
171
|
+
follow = x.relevant_indication_follow_up_percent
|
|
172
|
+
ae_follow = x.acoustic_emission_follow_up_percent
|
|
173
|
+
|
|
174
|
+
if x.mechanism_family == "weld_scc":
|
|
175
|
+
if x.inspection_mode == "intrusive":
|
|
176
|
+
if targeted >= 100 and follow >= 100:
|
|
177
|
+
category, basis = "A", "100_percent_targeted_and_indication_follow_up"
|
|
178
|
+
elif (targeted > 75 or ae_follow >= 100) and follow >= 100:
|
|
179
|
+
category, basis = "B", "over_75_percent_targeted_or_AE_with_follow_up"
|
|
180
|
+
elif targeted > 35 and follow >= 100 or radiography > 35:
|
|
181
|
+
category, basis = "C", "over_35_percent_targeted_or_radiography"
|
|
182
|
+
elif targeted > 10 and follow >= 100 or radiography > 10:
|
|
183
|
+
category, basis = "D", "over_10_percent_targeted_or_radiography"
|
|
184
|
+
else:
|
|
185
|
+
category, basis = "E", "weld_intrusive_threshold_not_met"
|
|
186
|
+
else:
|
|
187
|
+
if scan >= 100:
|
|
188
|
+
category, basis = "A", "100_percent_ultrasonic_scan"
|
|
189
|
+
elif scan > 75:
|
|
190
|
+
category, basis = "B", "over_75_percent_ultrasonic_scan"
|
|
191
|
+
elif scan > 35:
|
|
192
|
+
category, basis = "C", "over_35_percent_ultrasonic_scan"
|
|
193
|
+
elif scan > 10:
|
|
194
|
+
category, basis = "D", "over_10_percent_ultrasonic_scan"
|
|
195
|
+
else:
|
|
196
|
+
category, basis = "E", "weld_non_intrusive_threshold_not_met"
|
|
197
|
+
elif x.inspection_mode == "intrusive":
|
|
198
|
+
if targeted >= 100 and follow >= 100:
|
|
199
|
+
category, basis = "A", "100_percent_targeted_and_indication_follow_up"
|
|
200
|
+
elif (targeted > 65 or ae_follow >= 100) and follow >= 100:
|
|
201
|
+
category, basis = "B", "over_65_percent_targeted_or_AE_with_follow_up"
|
|
202
|
+
elif targeted > 35 and follow >= 100 or radiography > 65:
|
|
203
|
+
category, basis = "C", "over_35_percent_targeted_or_over_65_percent_radiography"
|
|
204
|
+
elif targeted > 10 and follow >= 100 or radiography > 35:
|
|
205
|
+
category, basis = "D", "over_10_percent_targeted_or_over_35_percent_radiography"
|
|
206
|
+
else:
|
|
207
|
+
category, basis = "E", "CLSCC_PTA_intrusive_threshold_not_met"
|
|
208
|
+
else:
|
|
209
|
+
if scan >= 100 and x.mechanism_family == "weld_scc":
|
|
210
|
+
category, basis = "A", "100_percent_ultrasonic_scan"
|
|
211
|
+
elif scan > 65:
|
|
212
|
+
category, basis = "B", "over_65_percent_ultrasonic_scan"
|
|
213
|
+
elif scan > 35:
|
|
214
|
+
category, basis = "C", "over_35_percent_ultrasonic_scan"
|
|
215
|
+
elif scan > 10:
|
|
216
|
+
category, basis = "D", "over_10_percent_ultrasonic_scan"
|
|
217
|
+
else:
|
|
218
|
+
category, basis = "E", "SCC_non_intrusive_threshold_not_met_or_A_unavailable"
|
|
219
|
+
return {"effectiveness_category": category, "mechanism_family": x.mechanism_family,
|
|
220
|
+
"inspection_mode": x.inspection_mode, "qualification_status": "qualified",
|
|
221
|
+
"basis": basis}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class ExternalInspectionEffectiveness(Model):
|
|
225
|
+
mechanism_family: Literal["external_corrosion", "external_clscc", "cui", "cui_clscc"]
|
|
226
|
+
insulation_status: Literal["not_applicable", "removed", "not_removed"] = "not_applicable"
|
|
227
|
+
visual_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
228
|
+
targeted_ndt_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
229
|
+
insulation_removed_percent: NonNegative = Field(default=0, le=100)
|
|
230
|
+
follow_up_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
231
|
+
inspection_quality_high: bool = True
|
|
232
|
+
locations_qualified: bool = True
|
|
233
|
+
technique_effective: bool = True
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@calculation(
|
|
237
|
+
"inspection/external-effectiveness",
|
|
238
|
+
"API 581 Annex 2.C.10 external-damage inspection effectiveness category",
|
|
239
|
+
ExternalInspectionEffectiveness,
|
|
240
|
+
["Part 2 Annex C §2.C.10.1-§2.C.10.2, Tables 2.C.10.1-2.C.10.4; PDF pp463-465"],
|
|
241
|
+
origin="api581_annex_2c10_inspection_effectiveness",
|
|
242
|
+
limitations=[
|
|
243
|
+
"Implements the printed external-corrosion, external CLSCC, CUI and CUI-CLSCC percentage examples. It classifies inspection effectiveness only and does not calculate external damage factor or inspection uncertainty.",
|
|
244
|
+
"CUI-CLSCC/non-removed insulation has no source technique for A-D and therefore remains E. Caller must qualify exposed/suspect area definition, inspection quality and follow-up."
|
|
245
|
+
],
|
|
246
|
+
)
|
|
247
|
+
def external_effectiveness(x):
|
|
248
|
+
if not (x.inspection_quality_high and x.locations_qualified and x.technique_effective):
|
|
249
|
+
return {"effectiveness_category": "E", "mechanism_family": x.mechanism_family,
|
|
250
|
+
"insulation_status": x.insulation_status, "qualification_status": "failed",
|
|
251
|
+
"basis": "inspection_quality_or_location_or_technique_not_qualified"}
|
|
252
|
+
|
|
253
|
+
visual = x.visual_coverage_percent
|
|
254
|
+
ndt = x.targeted_ndt_coverage_percent
|
|
255
|
+
removed = x.insulation_removed_percent
|
|
256
|
+
follow = x.follow_up_coverage_percent
|
|
257
|
+
if x.mechanism_family == "external_corrosion":
|
|
258
|
+
if visual > 95 and follow >= 100: category, basis = "A", "over_95_percent_visual_with_follow_up"
|
|
259
|
+
elif visual > 60 and follow >= 100: category, basis = "B", "over_60_percent_visual_with_follow_up"
|
|
260
|
+
elif visual > 30 and follow >= 100: category, basis = "C", "over_30_percent_visual_with_follow_up"
|
|
261
|
+
elif visual > 5 and follow >= 100: category, basis = "D", "over_5_percent_visual_with_follow_up"
|
|
262
|
+
else: category, basis = "E", "external_corrosion_threshold_not_met"
|
|
263
|
+
elif x.mechanism_family == "external_clscc":
|
|
264
|
+
if ndt >= 100 and follow >= 100: category, basis = "A", "100_percent_dye_or_eddy_with_follow_up"
|
|
265
|
+
elif ndt > 60 and follow >= 100: category, basis = "B", "over_60_percent_dye_or_eddy_with_follow_up"
|
|
266
|
+
elif ndt > 30 and follow >= 100: category, basis = "C", "over_30_percent_dye_or_eddy_with_follow_up"
|
|
267
|
+
elif ndt > 5 and follow >= 100: category, basis = "D", "over_5_percent_dye_or_eddy_with_follow_up"
|
|
268
|
+
else: category, basis = "E", "external_clscc_threshold_not_met"
|
|
269
|
+
elif x.mechanism_family == "cui":
|
|
270
|
+
if visual < 100 or follow < 100:
|
|
271
|
+
category, basis = "E", "cui_requires_full_visual_and_follow_up"
|
|
272
|
+
elif x.insulation_status == "removed":
|
|
273
|
+
if removed >= 100: category, basis = "A", "cui_removed_100_percent_suspect_area"
|
|
274
|
+
elif removed > 50: category, basis = "B", "cui_removed_over_50_percent_suspect_area"
|
|
275
|
+
elif removed > 25: category, basis = "C", "cui_removed_over_25_percent_suspect_area"
|
|
276
|
+
elif removed > 5: category, basis = "D", "cui_removed_over_5_percent_total_area"
|
|
277
|
+
else: category, basis = "E", "cui_removed_threshold_not_met"
|
|
278
|
+
elif x.insulation_status == "not_removed":
|
|
279
|
+
if ndt >= 100: category, basis = "A", "cui_not_removed_100_percent_profile_radiography"
|
|
280
|
+
elif ndt > 65: category, basis = "B", "cui_not_removed_over_65_percent_profile_radiography"
|
|
281
|
+
elif ndt > 35: category, basis = "C", "cui_not_removed_over_35_percent_profile_radiography"
|
|
282
|
+
elif ndt > 5: category, basis = "D", "cui_not_removed_over_5_percent_profile_radiography"
|
|
283
|
+
else: category, basis = "E", "cui_not_removed_threshold_not_met"
|
|
284
|
+
else: category, basis = "E", "cui_insulation_status_required"
|
|
285
|
+
elif x.insulation_status == "removed" and visual >= 100 and follow >= 100:
|
|
286
|
+
if ndt >= 100: category, basis = "A", "cui_clscc_removed_100_percent_targeted_ndt"
|
|
287
|
+
elif ndt > 60: category, basis = "B", "cui_clscc_removed_over_60_percent_targeted_ndt"
|
|
288
|
+
elif ndt > 30: category, basis = "C", "cui_clscc_removed_over_30_percent_targeted_ndt"
|
|
289
|
+
elif ndt > 5: category, basis = "D", "cui_clscc_removed_over_5_percent_targeted_ndt"
|
|
290
|
+
else: category, basis = "E", "cui_clscc_removed_threshold_not_met"
|
|
291
|
+
else:
|
|
292
|
+
category, basis = "E", "cui_clscc_non_removed_or_qualification_not_met"
|
|
293
|
+
return {"effectiveness_category": category, "mechanism_family": x.mechanism_family,
|
|
294
|
+
"insulation_status": x.insulation_status, "qualification_status": "qualified",
|
|
295
|
+
"basis": basis}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class BuriedInspectionEffectiveness(Model):
|
|
299
|
+
inspection_mode: Literal["intrusive", "non_intrusive"]
|
|
300
|
+
coverage_percent: NonNegative = Field(default=0, le=100)
|
|
301
|
+
state_of_art_internal_technology: bool = False
|
|
302
|
+
selected_internal_technology: bool = False
|
|
303
|
+
statistical_or_extreme_value_analysis: bool = False
|
|
304
|
+
partial_smart_pig_or_specialized_crawler: bool = False
|
|
305
|
+
hydrostatic_test: bool = False
|
|
306
|
+
full_excavation_visual_ndt: bool = False
|
|
307
|
+
external_ndt_crawler: bool = False
|
|
308
|
+
cathodic_protection_managed: bool = False
|
|
309
|
+
close_interval_survey: bool = False
|
|
310
|
+
soil_water_sampling: bool = False
|
|
311
|
+
dcvg_coating_survey: bool = False
|
|
312
|
+
guided_wave_and_corrosion_management: bool = False
|
|
313
|
+
spot_check_exposed_areas: bool = False
|
|
314
|
+
inspection_quality_high: bool = True
|
|
315
|
+
qualification_basis: str = Field(min_length=1, max_length=1000)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@calculation(
|
|
319
|
+
"inspection/buried-effectiveness",
|
|
320
|
+
"API 581 Annex 2.C.7 buried-component inspection effectiveness category",
|
|
321
|
+
BuriedInspectionEffectiveness,
|
|
322
|
+
["Part 2 Annex C §2.C.7.1-§2.C.7.2, Table 2.C.7.1; PDF pp452-453"],
|
|
323
|
+
origin="api581_annex_2c7_buried_inspection_effectiveness",
|
|
324
|
+
limitations=[
|
|
325
|
+
"Classifies the example intrusive/non-intrusive evidence bundles in Table 2.C.7.1. It does not infer corrosion rate, cathodic-protection adequacy, soil corrosivity, PoF or damage factor.",
|
|
326
|
+
"The caller must qualify the evidence flags and coverage. A missing qualification basis or low inspection quality returns E; partial excavation and non-intrusive evidence are not silently promoted to full coverage."
|
|
327
|
+
],
|
|
328
|
+
)
|
|
329
|
+
def buried_effectiveness(x):
|
|
330
|
+
if not x.inspection_quality_high or not x.qualification_basis.strip():
|
|
331
|
+
return {"effectiveness_category": "E", "inspection_mode": x.inspection_mode,
|
|
332
|
+
"qualification_status": "failed", "basis": "inspection_quality_or_basis_not_qualified"}
|
|
333
|
+
if x.inspection_mode == "intrusive":
|
|
334
|
+
if x.state_of_art_internal_technology and x.coverage_percent >= 100:
|
|
335
|
+
category, basis = "A", "100_percent_state_of_art_internal_inspection"
|
|
336
|
+
elif x.selected_internal_technology and x.statistical_or_extreme_value_analysis:
|
|
337
|
+
category, basis = "B", "selected_internal_inspection_with_statistical_analysis"
|
|
338
|
+
elif x.partial_smart_pig_or_specialized_crawler:
|
|
339
|
+
category, basis = "C", "partial_smart_pig_or_specialized_crawler"
|
|
340
|
+
elif x.hydrostatic_test:
|
|
341
|
+
category, basis = "D", "hydrostatic_test"
|
|
342
|
+
else:
|
|
343
|
+
category, basis = "E", "intrusive_evidence_bundle_not_met"
|
|
344
|
+
elif (
|
|
345
|
+
x.full_excavation_visual_ndt and x.coverage_percent >= 100
|
|
346
|
+
and x.soil_water_sampling and x.cathodic_protection_managed
|
|
347
|
+
) or (
|
|
348
|
+
x.external_ndt_crawler and x.coverage_percent >= 100
|
|
349
|
+
and x.cathodic_protection_managed
|
|
350
|
+
):
|
|
351
|
+
category, basis = "A", "qualified_full_external_inspection_and_CP_controls"
|
|
352
|
+
elif (
|
|
353
|
+
x.external_ndt_crawler and x.statistical_or_extreme_value_analysis
|
|
354
|
+
) or (
|
|
355
|
+
x.close_interval_survey and x.full_excavation_visual_ndt
|
|
356
|
+
and x.cathodic_protection_managed
|
|
357
|
+
):
|
|
358
|
+
category, basis = "B", "selected_external_inspection_with_statistics_or_CP_guided_excavation"
|
|
359
|
+
elif x.guided_wave_and_corrosion_management and x.spot_check_exposed_areas:
|
|
360
|
+
category, basis = "C", "guided_wave_with_corrosion_management_and_spot_check"
|
|
361
|
+
elif x.coverage_percent > 5 and (x.full_excavation_visual_ndt or x.external_ndt_crawler):
|
|
362
|
+
category, basis = "D", "limited_external_visual_or_NDE_inspection"
|
|
363
|
+
else:
|
|
364
|
+
category, basis = "E", "non_intrusive_evidence_bundle_not_met"
|
|
365
|
+
return {"effectiveness_category": category, "inspection_mode": x.inspection_mode,
|
|
366
|
+
"qualification_status": "qualified", "basis": basis}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class PrdInspectionEffectiveness(Model):
|
|
370
|
+
component_type: Literal["pressure_relief_device", "rupture_disk"]
|
|
371
|
+
as_received_bench_test: bool = False
|
|
372
|
+
leak_pressure_documented: bool = False
|
|
373
|
+
opening_pressure_documented: bool = False
|
|
374
|
+
reseat_pressure_documented: bool = False
|
|
375
|
+
inlet_outlet_examined: bool = False
|
|
376
|
+
cleaned_or_steamed_before_test: bool = False
|
|
377
|
+
detailed_internal_visual: bool = False
|
|
378
|
+
in_situ_actual_process_fluid: bool = False
|
|
379
|
+
removed_and_visually_inspected: bool = False
|
|
380
|
+
in_situ_non_process_fluid: bool = False
|
|
381
|
+
leakage_monitoring: bool = False
|
|
382
|
+
overhaul_without_pop_test: bool = False
|
|
383
|
+
internal_details_documented: bool = False
|
|
384
|
+
qualification_basis: str = Field(min_length=1, max_length=1000)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@calculation(
|
|
388
|
+
"inspection/prd-effectiveness",
|
|
389
|
+
"API 581 Annex 2.C.3 pressure-relief-device inspection effectiveness category",
|
|
390
|
+
PrdInspectionEffectiveness,
|
|
391
|
+
["Part 2 Annex C §2.C.3-§2.C.3.1, Table 2.C.3.1; PDF pp445-446"],
|
|
392
|
+
origin="api581_annex_2c3_prd_inspection_effectiveness",
|
|
393
|
+
limitations=[
|
|
394
|
+
"Implements the Table 2.C.3.1 evidence examples. It classifies inspection effectiveness only and does not calculate PRD failure frequency, service-age factor, set-pressure factor or leak consequence.",
|
|
395
|
+
"Rupture-disk A-level inspection is explicitly unavailable in the source table. Missing qualification basis or incomplete evidence returns E rather than an optimistic category."
|
|
396
|
+
],
|
|
397
|
+
)
|
|
398
|
+
def prd_effectiveness(x):
|
|
399
|
+
if not x.qualification_basis.strip():
|
|
400
|
+
return {"effectiveness_category": "E", "component_type": x.component_type,
|
|
401
|
+
"qualification_status": "failed", "basis": "qualification_basis_missing"}
|
|
402
|
+
if x.component_type == "pressure_relief_device":
|
|
403
|
+
if (x.as_received_bench_test and x.leak_pressure_documented
|
|
404
|
+
and x.opening_pressure_documented and x.reseat_pressure_documented
|
|
405
|
+
and x.inlet_outlet_examined):
|
|
406
|
+
category, basis = "A", "as_received_bench_test_pressures_and_piping_exam"
|
|
407
|
+
elif ((x.as_received_bench_test and x.cleaned_or_steamed_before_test and x.detailed_internal_visual)
|
|
408
|
+
or x.in_situ_actual_process_fluid):
|
|
409
|
+
category, basis = "B", "cleaned_bench_test_with_visual_or_actual_fluid_in_situ_test"
|
|
410
|
+
elif x.detailed_internal_visual or x.in_situ_non_process_fluid:
|
|
411
|
+
category, basis = "C", "documented_visual_or_non_process_fluid_in_situ_test"
|
|
412
|
+
elif x.overhaul_without_pop_test:
|
|
413
|
+
category, basis = "D", "overhaul_without_pop_test"
|
|
414
|
+
else:
|
|
415
|
+
category, basis = "E", "PRD_evidence_bundle_not_met"
|
|
416
|
+
else:
|
|
417
|
+
if x.removed_and_visually_inspected:
|
|
418
|
+
category, basis = "B", "rupture_disk_removed_and_visually_inspected"
|
|
419
|
+
elif x.leakage_monitoring:
|
|
420
|
+
category, basis = "C", "rupture_disk_leakage_monitoring"
|
|
421
|
+
elif x.internal_details_documented:
|
|
422
|
+
category, basis = "D", "rupture_disk_internal_details_only"
|
|
423
|
+
else:
|
|
424
|
+
category, basis = "E", "rupture_disk_no_effective_method"
|
|
425
|
+
return {"effectiveness_category": category, "component_type": x.component_type,
|
|
426
|
+
"qualification_status": "qualified", "basis": basis}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class LiningInspectionEffectiveness(Model):
|
|
430
|
+
inspection_mode: Literal["intrusive", "non_intrusive"]
|
|
431
|
+
visual_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
432
|
+
holiday_test_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
433
|
+
disbonding_test_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
434
|
+
ultrasonic_scan_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
435
|
+
inspection_quality_high: bool = True
|
|
436
|
+
locations_qualified: bool = True
|
|
437
|
+
qualification_basis: str = Field(min_length=1, max_length=1000)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@calculation(
|
|
441
|
+
"inspection/lining-effectiveness",
|
|
442
|
+
"API 581 Annex 2.C.6 non-metallic lining inspection effectiveness category",
|
|
443
|
+
LiningInspectionEffectiveness,
|
|
444
|
+
["Part 2 Annex C §2.C.6.1-§2.C.6.2, Table 2.C.6.1; PDF pp451-452"],
|
|
445
|
+
origin="api581_annex_2c6_lining_inspection_effectiveness",
|
|
446
|
+
limitations=[
|
|
447
|
+
"Implements the example percentage bands for corrosion-resistant non-metallic liners. The source states this inspection effectiveness is not currently used in the lining damage factor calculation; this API does not change that rule.",
|
|
448
|
+
"Non-intrusive A-level inspection is explicitly unavailable in the source table. Inspection quality, suspect-area definition and test suitability remain owner-qualified inputs."
|
|
449
|
+
],
|
|
450
|
+
)
|
|
451
|
+
def lining_effectiveness(x):
|
|
452
|
+
if not (x.inspection_quality_high and x.locations_qualified and x.qualification_basis.strip()):
|
|
453
|
+
return {"effectiveness_category": "E", "inspection_mode": x.inspection_mode,
|
|
454
|
+
"qualification_status": "failed", "basis": "inspection_quality_location_or_basis_not_qualified"}
|
|
455
|
+
visual = x.visual_coverage_percent
|
|
456
|
+
holiday = x.holiday_test_coverage_percent
|
|
457
|
+
disbonding = x.disbonding_test_coverage_percent
|
|
458
|
+
scan = x.ultrasonic_scan_coverage_percent
|
|
459
|
+
if x.inspection_mode == "intrusive":
|
|
460
|
+
if visual >= 100 and holiday >= 100 and disbonding >= 100:
|
|
461
|
+
category, basis = "A", "100_percent_visual_holiday_and_disbonding_tests"
|
|
462
|
+
elif visual > 65 and holiday > 65 and disbonding > 65:
|
|
463
|
+
category, basis = "B", "over_65_percent_visual_holiday_and_disbonding_tests"
|
|
464
|
+
elif max(visual, holiday, disbonding) > 35:
|
|
465
|
+
category, basis = "C", "over_35_percent_intrusive_liner_test"
|
|
466
|
+
elif max(visual, holiday, disbonding) > 5:
|
|
467
|
+
category, basis = "D", "over_5_percent_intrusive_liner_test"
|
|
468
|
+
else:
|
|
469
|
+
category, basis = "E", "intrusive_liner_threshold_not_met"
|
|
470
|
+
else:
|
|
471
|
+
if scan >= 100:
|
|
472
|
+
category, basis = "B", "100_percent_automated_or_manual_ultrasonic_scan"
|
|
473
|
+
elif scan > 65:
|
|
474
|
+
category, basis = "C", "over_65_percent_ultrasonic_scan"
|
|
475
|
+
elif scan > 35:
|
|
476
|
+
category, basis = "D", "over_35_percent_ultrasonic_scan"
|
|
477
|
+
else:
|
|
478
|
+
category, basis = "E", "non_intrusive_liner_threshold_or_A_unavailable"
|
|
479
|
+
return {"effectiveness_category": category, "inspection_mode": x.inspection_mode,
|
|
480
|
+
"qualification_status": "qualified", "basis": basis}
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class TubeBundleInspectionUncertainty(Model):
|
|
484
|
+
inspection_effectiveness: Literal["A", "B", "C", "D", "E"]
|
|
485
|
+
owner_user_basis: str = Field(min_length=1, max_length=1000)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@calculation(
|
|
489
|
+
"inspection/tube-bundle-uncertainty",
|
|
490
|
+
"API 581 Annex 2.C.4.1 default tube-bundle inspection uncertainty",
|
|
491
|
+
TubeBundleInspectionUncertainty,
|
|
492
|
+
["Part 2 Annex C §2.C.4.1.1-§2.C.4.1.3, Table 2.C.4.1; PDF pp446-448"],
|
|
493
|
+
origin="api581_annex_2c4_tube_bundle_uncertainty",
|
|
494
|
+
limitations=[
|
|
495
|
+
"Returns the recommended additional-uncertainty percentage only: A=5%, B=10%, C=20%, D=30%, E=50%. It does not shift a Weibull curve or calculate tube-bundle PoF by itself.",
|
|
496
|
+
"The source says tube-bundle LoIE tables are owner-user defined; the category and basis remain caller-qualified and must not be inferred from an NDE name alone."
|
|
497
|
+
],
|
|
498
|
+
)
|
|
499
|
+
def tube_bundle_uncertainty(x):
|
|
500
|
+
values = {"A": 5.0, "B": 10.0, "C": 20.0, "D": 30.0, "E": 50.0}
|
|
501
|
+
percent = values[x.inspection_effectiveness]
|
|
502
|
+
return {"inspection_effectiveness": x.inspection_effectiveness,
|
|
503
|
+
"uncertainty_percent": percent,
|
|
504
|
+
"uncertainty_fraction": percent / 100.0,
|
|
505
|
+
"baseline_no_history_percent": 50.0,
|
|
506
|
+
"owner_user_basis": x.owner_user_basis}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class AstInspectionEffectiveness(Model):
|
|
510
|
+
component_type: Literal["shell_internal", "shell_external", "tank_bottom"]
|
|
511
|
+
inspection_side: Literal["internal", "external", "soil_side", "product_side"]
|
|
512
|
+
insulated: bool = False
|
|
513
|
+
visual_coverage_percent: NonNegative = Field(default=0, le=100)
|
|
514
|
+
internal_video_or_visual: bool = False
|
|
515
|
+
pit_depth_measurement: bool = False
|
|
516
|
+
ut_scan_percent: NonNegative = Field(default=0, le=100)
|
|
517
|
+
ut_follow_up: bool = False
|
|
518
|
+
insulation_removed_percent: NonNegative = Field(default=0, le=100)
|
|
519
|
+
pulse_eddy_percent: NonNegative = Field(default=0, le=100)
|
|
520
|
+
floor_scan_percent: NonNegative = Field(default=0, le=100)
|
|
521
|
+
vacuum_box_percent: NonNegative = Field(default=0, le=100)
|
|
522
|
+
sponge_test_percent: NonNegative = Field(default=0, le=100)
|
|
523
|
+
statistical_floor_analysis: bool = False
|
|
524
|
+
qualification_basis: str = Field(min_length=1, max_length=1000)
|
|
525
|
+
inspection_quality_high: bool = True
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@calculation(
|
|
529
|
+
"inspection/ast-effectiveness",
|
|
530
|
+
"API 581 Annex 2.C.5 atmospheric storage tank inspection effectiveness category",
|
|
531
|
+
AstInspectionEffectiveness,
|
|
532
|
+
["Part 2 Annex C §2.C.5.1-§2.C.5.2, Tables 2.C.5.1-2.C.5.3; PDF pp449-451"],
|
|
533
|
+
origin="api581_annex_2c5_ast_inspection_effectiveness",
|
|
534
|
+
limitations=[
|
|
535
|
+
"Implements the example AST shell-internal, shell-external and tank-bottom evidence bands. It does not calculate tank corrosion rate, minimum thickness, PoF or financial consequence.",
|
|
536
|
+
"The caller must select the correct side and qualify inspection quality, exposed/suspect area and API 653/NDE interpretation. Inputs from multiple sides are not silently combined into a higher category."
|
|
537
|
+
],
|
|
538
|
+
)
|
|
539
|
+
def ast_effectiveness(x):
|
|
540
|
+
if not (x.inspection_quality_high and x.qualification_basis.strip()):
|
|
541
|
+
return {"effectiveness_category": "E", "component_type": x.component_type,
|
|
542
|
+
"inspection_side": x.inspection_side, "qualification_status": "failed",
|
|
543
|
+
"basis": "inspection_quality_or_basis_not_qualified"}
|
|
544
|
+
if x.component_type == "shell_internal":
|
|
545
|
+
if x.internal_video_or_visual and x.pit_depth_measurement and x.ut_scan_percent >= 100 and x.ut_follow_up:
|
|
546
|
+
category, basis = "A", "internal_visual_pit_and_full_UT_confirmation"
|
|
547
|
+
elif x.ut_scan_percent > 0 and x.internal_video_or_visual and x.ut_follow_up:
|
|
548
|
+
category, basis = "B", "internal_video_with_external_UT_follow_up"
|
|
549
|
+
elif x.ut_scan_percent > 0 and x.internal_video_or_visual:
|
|
550
|
+
category, basis = "C", "external_spot_UT_with_internal_information"
|
|
551
|
+
elif x.ut_scan_percent > 0:
|
|
552
|
+
category, basis = "D", "external_spot_UT_without_internal_information"
|
|
553
|
+
else:
|
|
554
|
+
category, basis = "E", "shell_internal_evidence_bundle_not_met"
|
|
555
|
+
elif x.component_type == "shell_external":
|
|
556
|
+
if x.insulated:
|
|
557
|
+
if x.visual_coverage_percent > 95 and x.insulation_removed_percent > 90 and x.pulse_eddy_percent > 90:
|
|
558
|
+
category, basis = "A", "insulated_shell_full_visual_removal_and_pulse_eddy"
|
|
559
|
+
elif x.visual_coverage_percent > 95 and x.insulation_removed_percent > 50 and x.pulse_eddy_percent > 50:
|
|
560
|
+
category, basis = "B", "insulated_shell_over_50_percent_suspect_inspection"
|
|
561
|
+
elif x.visual_coverage_percent > 95 and x.insulation_removed_percent > 30 and x.pulse_eddy_percent > 30:
|
|
562
|
+
category, basis = "C", "insulated_shell_over_30_percent_suspect_inspection"
|
|
563
|
+
elif x.visual_coverage_percent > 95 and x.insulation_removed_percent > 10 and x.pulse_eddy_percent > 10:
|
|
564
|
+
category, basis = "D", "insulated_shell_over_10_percent_suspect_inspection"
|
|
565
|
+
else:
|
|
566
|
+
category, basis = "E", "insulated_shell_threshold_not_met"
|
|
567
|
+
elif x.visual_coverage_percent > 95 and x.ut_follow_up:
|
|
568
|
+
category, basis = "A", "uninsulated_shell_over_95_percent_visual_with_follow_up"
|
|
569
|
+
elif x.visual_coverage_percent > 50 and x.ut_follow_up:
|
|
570
|
+
category, basis = "B", "uninsulated_shell_over_50_percent_visual_with_follow_up"
|
|
571
|
+
elif x.visual_coverage_percent > 25 and x.ut_follow_up:
|
|
572
|
+
category, basis = "C", "uninsulated_shell_over_25_percent_visual_with_follow_up"
|
|
573
|
+
elif x.visual_coverage_percent > 10 and x.ut_follow_up:
|
|
574
|
+
category, basis = "D", "uninsulated_shell_over_10_percent_visual_with_follow_up"
|
|
575
|
+
else:
|
|
576
|
+
category, basis = "E", "uninsulated_shell_threshold_not_met"
|
|
577
|
+
elif x.inspection_side == "soil_side":
|
|
578
|
+
if x.floor_scan_percent > 90 and x.ut_follow_up:
|
|
579
|
+
category, basis = "A", "soil_side_over_90_percent_floor_scan_and_UT_follow_up"
|
|
580
|
+
elif (x.floor_scan_percent > 50 and x.ut_follow_up) or x.statistical_floor_analysis:
|
|
581
|
+
category, basis = "B", "soil_side_over_50_percent_scan_or_statistical_analysis"
|
|
582
|
+
elif x.floor_scan_percent > 5 and x.ut_follow_up:
|
|
583
|
+
category, basis = "C", "soil_side_partial_floor_scan_and_UT_follow_up"
|
|
584
|
+
elif x.ut_scan_percent > 0 or x.visual_coverage_percent > 50:
|
|
585
|
+
category, basis = "D", "soil_side_spot_or_limited_visual_inspection"
|
|
586
|
+
else:
|
|
587
|
+
category, basis = "E", "soil_side_threshold_not_met"
|
|
588
|
+
else:
|
|
589
|
+
if x.visual_coverage_percent >= 100 and x.vacuum_box_percent >= 100:
|
|
590
|
+
category, basis = "A", "product_side_full_visual_and_vacuum_box"
|
|
591
|
+
elif x.sponge_test_percent > 75 or x.visual_coverage_percent >= 100:
|
|
592
|
+
category, basis = "B", "product_side_full_visual_or_over_75_percent_sponge"
|
|
593
|
+
elif 50 <= x.sponge_test_percent <= 75 or x.visual_coverage_percent > 50:
|
|
594
|
+
category, basis = "C", "product_side_intermediate_liner_or_visual_inspection"
|
|
595
|
+
elif x.sponge_test_percent > 0 or x.visual_coverage_percent > 0:
|
|
596
|
+
category, basis = "D", "product_side_limited_inspection"
|
|
597
|
+
else:
|
|
598
|
+
category, basis = "E", "product_side_threshold_not_met"
|
|
599
|
+
return {"effectiveness_category": category, "component_type": x.component_type,
|
|
600
|
+
"inspection_side": x.inspection_side, "qualification_status": "qualified",
|
|
601
|
+
"basis": basis}
|
api581/jet_handoff.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Qualified API581 rainout/flash partition -> actual native jet source."""
|
|
2
|
+
import math
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from .core import Model, Positive, REGISTRY, calculation, ensure, finite_tree
|
|
5
|
+
from .source_partition import SourcePartition, source_partition
|
|
6
|
+
from .slabx_adapter import NativeJetGeometry, NativeJet, NativeJetFootprint, native_jet, native_jet_footprint
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PartitionedJet(Model):
|
|
10
|
+
partition: SourcePartition
|
|
11
|
+
jet: NativeJetGeometry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class JetFootprintGeometry(NativeJetGeometry):
|
|
15
|
+
crosswind_half_span_m: Positive
|
|
16
|
+
crosswind_points_per_side: int = Field(default=65,ge=3,le=501,strict=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PartitionedJetFootprint(Model):
|
|
20
|
+
partition: SourcePartition
|
|
21
|
+
jet: JetFootprintGeometry
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_LIMITS = [*["Source-partition stage only: "+item for item in REGISTRY["release/level2-source-partition"].limitations],
|
|
25
|
+
*REGISTRY["dispersion/native-jet-centreline"].limitations,
|
|
26
|
+
"Native q is partition jet-total vapor+droplet rate; liquid MASS fraction is partition entrained-liquid fraction. Duration is jet mass/rate from same partition. No independent duplicate source rate/composition/duration accepted. Pool rainout remains a separate source, not added to native jet. Empty/zero-duration jet and pure-liquid jet fail explicitly. Caller still qualifies effective atmospheric exit area, temperature, flash/rainout data and native source applicability."]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _handoff(x,footprint):
|
|
30
|
+
partition = source_partition(x.partition)
|
|
31
|
+
finite_tree(partition)
|
|
32
|
+
q,mass = partition["jet_total_rate_kg_s"],partition["jet_total_mass_kg"]
|
|
33
|
+
ensure(q > 0 and mass > 0 and partition["jet_fraction_nonzero"],"Partition has no positive finite-duration jet source","empty_jet_source")
|
|
34
|
+
liquid = partition["entrained_liquid_mass_fraction"]
|
|
35
|
+
ensure(liquid is not None and 0 <= liquid < 1,"Native jet cannot accept pure liquid/no-vapor partition","unsupported_engine_source_domain")
|
|
36
|
+
duration = mass/q
|
|
37
|
+
ensure(math.isfinite(duration) and duration > 0,"Jet handoff duration outside numerical range","numerical_failure")
|
|
38
|
+
if x.jet.temperature_basis == "require_consistent":
|
|
39
|
+
boiling = x.jet.substance.normal_boiling_point_k
|
|
40
|
+
ensure(x.jet.source_temperature_k >= boiling and (liquid == 0 or x.jet.source_temperature_k == boiling),
|
|
41
|
+
"Partition composition conflicts with required native source temperature","native_source_state_inconsistent")
|
|
42
|
+
model = NativeJetFootprint if footprint else NativeJet
|
|
43
|
+
request = model(**x.jet.model_dump(),mass_source_rate_kg_s=q,release_duration_s=duration,jet_liquid_mass_fraction=liquid)
|
|
44
|
+
output = (native_jet_footprint if footprint else native_jet)(request)
|
|
45
|
+
return {**output,**partition,"release_duration_s":duration,"integration_scope":"qualified_partition_to_native_jet"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@calculation("dispersion/partitioned-jet-centreline", "API581 source partition through native jet centreline", PartitionedJet,
|
|
49
|
+
[*REGISTRY["release/level2-source-partition"].references,*REGISTRY["dispersion/native-jet-centreline"].references],
|
|
50
|
+
origin="api581_partition_to_native_jet_composition",limitations=_LIMITS)
|
|
51
|
+
def partitioned_jet(x):
|
|
52
|
+
return _handoff(x,False)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@calculation("dispersion/partitioned-jet-footprint", "API581 source partition through native planar jet footprint", PartitionedJetFootprint,
|
|
56
|
+
[*REGISTRY["release/level2-source-partition"].references,*REGISTRY["dispersion/native-jet-footprint"].references],
|
|
57
|
+
origin="api581_partition_to_native_jet_spatial_composition",limitations=_LIMITS+[
|
|
58
|
+
"Area is sampled-grid triangular interpolation and needs spatial convergence. Source/handoff-edge concentration, outer-grid exceedance or LH2 premise violation censors usable area; diagnostic field/area preserved. No missing near-field completion or source-face extrapolation."])
|
|
59
|
+
def partitioned_jet_footprint(x):
|
|
60
|
+
return _handoff(x,True)
|