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/brittle.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Part 2 §§20,22,23 brittle-fracture and embrittlement calculations.
|
|
2
|
+
|
|
3
|
+
SI tables are used as printed, except Table 20.3 where the unambiguous US
|
|
4
|
+
table is converted to SI explicitly. Material qualification is caller supplied.
|
|
5
|
+
"""
|
|
6
|
+
from bisect import bisect_left
|
|
7
|
+
import math
|
|
8
|
+
from typing import Annotated, Literal
|
|
9
|
+
from pydantic import Field, StrictBool
|
|
10
|
+
from .core import Model, Positive, Fraction, calculation, ensure
|
|
11
|
+
|
|
12
|
+
Finite = Annotated[float, Field(strict=True, allow_inf_nan=False)]
|
|
13
|
+
Interpolation = Literal["exact", "linear"]
|
|
14
|
+
KSI_PA = 6894757.293168
|
|
15
|
+
|
|
16
|
+
# Rows: CET - Tref, K (= Celsius difference). Columns: printed SI thickness, mm.
|
|
17
|
+
DELTA_K = (56, 44, 33, 22, 11, 0, -11, -22, -33, -44, -56)
|
|
18
|
+
THICKNESS_MM = (6.4, 12.7, 25.4, 38.1, 50.8, 63.5, 76.2, 88.9, 101.6)
|
|
19
|
+
NON_PWHT = (
|
|
20
|
+
(0,0,0,0,0,0,.9,1.1,1.2),
|
|
21
|
+
(0,0,0,.8,1.1,2,2,4,6),
|
|
22
|
+
(0,0,1,2,4,9,19,36,60),
|
|
23
|
+
(0,.7,2,9,29,69,133,224,338),
|
|
24
|
+
(.1,1.3,10,49,143,296,500,741,1008),
|
|
25
|
+
(.9,3,39,175,424,759,1142,1545,1950),
|
|
26
|
+
(1.2,7,109,405,850,1366,1897,2415,2903),
|
|
27
|
+
(2,16,220,697,1317,1969,2596,3176,3703),
|
|
28
|
+
(2,30,350,988,1740,2479,3160,3769,4310),
|
|
29
|
+
(3,46,474,1239,2080,2873,3581,4203,4746),
|
|
30
|
+
(4,61,579,1436,2336,3160,3883,4509,5000),
|
|
31
|
+
)
|
|
32
|
+
PWHT = (
|
|
33
|
+
(0,0,0,0,0,0,0,0,0),
|
|
34
|
+
(0,0,0,0,0,0,0,0,.2),
|
|
35
|
+
(0,0,0,0,0,.5,.9,1.1,1.3),
|
|
36
|
+
(0,0,0,.5,1.1,1.3,2,3,4),
|
|
37
|
+
(0,0,.6,1.2,2,4,7,13,23),
|
|
38
|
+
(0,0,1.1,2,6,14,29,53,88),
|
|
39
|
+
(0,.4,2,5,17,41,83,144,224),
|
|
40
|
+
(0,.9,3,12,38,90,171,281,416),
|
|
41
|
+
(0,1.1,5,22,68,153,277,436,623),
|
|
42
|
+
(0,1.2,7,34,102,219,382,582,810),
|
|
43
|
+
(0,1.3,9,46,133,277,472,704,962),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _weights(nodes, value, mode):
|
|
48
|
+
"""Ascending grid; snap only floating-point roundoff at printed nodes."""
|
|
49
|
+
for i, node in enumerate(nodes):
|
|
50
|
+
if math.isclose(value, node, rel_tol=1e-12, abs_tol=1e-9):
|
|
51
|
+
return ((i, 1.0),)
|
|
52
|
+
ensure(nodes[0] < value < nodes[-1], "Input outside tabulated domain; extrapolation is not supported")
|
|
53
|
+
ensure(mode == "linear", "Input is not a printed table node; explicitly select linear interpolation", "interpolation_required")
|
|
54
|
+
upper = bisect_left(nodes, value)
|
|
55
|
+
weight = (value - nodes[upper-1]) / (nodes[upper] - nodes[upper-1])
|
|
56
|
+
return ((upper-1, 1-weight), (upper, weight))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _lookup(nodes, values, value, mode):
|
|
60
|
+
return sum(values[i] * weight for i, weight in _weights(nodes, value, mode))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BrittleReference(Model):
|
|
64
|
+
material_group: Literal["carbon_steel", "low_alloy_steel"]
|
|
65
|
+
minimum_yield_strength_pa: Positive
|
|
66
|
+
exemption_curve: Literal["A", "B", "C", "D"]
|
|
67
|
+
interpolation: Interpolation
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@calculation("damage/brittle-reference-temperature", "Brittle fracture reference temperature", BrittleReference,
|
|
71
|
+
["Part 2 §20.6.3 STEP 3; Table 20.3; PDF p268"],
|
|
72
|
+
limitations=["Caller qualifies material and assigns ASME exemption curve. Uses the printed US Table 20.3 (ksi, F), explicitly converted to Pa and K; not rounded SI Table 20.3M. Linear interpolation is a caller-selected engineering interpolation; no extrapolation."])
|
|
73
|
+
def brittle_reference(x):
|
|
74
|
+
base = ((104,97,91,86,81,78,74,71,68,66,63) if x.material_group == "carbon_steel"
|
|
75
|
+
else (124,115,107,101,96,92,88,85,81,79,76,73,71,69,67,65,63,62,60,58,57,56,54,53,52,51))
|
|
76
|
+
yield_nodes = tuple(range(30, 30 + 2*len(base), 2))
|
|
77
|
+
offset = {"A":0, "B":38, "C":76, "D":102}[x.exemption_curve]
|
|
78
|
+
temp_f = _lookup(yield_nodes, tuple(v-offset for v in base), x.minimum_yield_strength_pa/KSI_PA, x.interpolation)
|
|
79
|
+
return {"reference_temperature_k": (temp_f-32)*5/9+273.15, "table_basis": "20.3_US_converted_to_SI"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BrittleService(Model):
|
|
83
|
+
component_thickness_m: Positive
|
|
84
|
+
nominal_uncorroded_thickness_m: Positive
|
|
85
|
+
equipment_type: Literal["pressure_vessel", "piping"]
|
|
86
|
+
p1_or_p3_steel: StrictBool
|
|
87
|
+
design_temperature_k: Positive
|
|
88
|
+
construction_code_satisfied: StrictBool
|
|
89
|
+
stable_conditions_no_more_severe_future: StrictBool
|
|
90
|
+
cet_at_mawp_k: Positive
|
|
91
|
+
cyclic_fatigue_or_vibration_design_service: StrictBool
|
|
92
|
+
environmental_cracking: StrictBool
|
|
93
|
+
shock_chilling: StrictBool
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@calculation("damage/brittle-service-factor", "Brittle fracture service-experience adjustment", BrittleService,
|
|
97
|
+
["Part 2 §20.6.3 STEP 6; eq (2.86); PDF pp264-265"],
|
|
98
|
+
limitations=["Owner qualifies code compliance, operating history and service exclusions. Thin-component criterion is an independent OR; all eight service criteria are otherwise required. Printed SI thresholds are used."])
|
|
99
|
+
def brittle_service(x):
|
|
100
|
+
checks = {
|
|
101
|
+
"p1_p3_and_design_temperature": x.p1_or_p3_steel and x.design_temperature_k <= 616.15,
|
|
102
|
+
"construction_code": x.construction_code_satisfied,
|
|
103
|
+
"stable_service": x.stable_conditions_no_more_severe_future,
|
|
104
|
+
"cet_at_mawp": x.cet_at_mawp_k >= (244.15 if x.equipment_type == "pressure_vessel" else 169.15),
|
|
105
|
+
"nominal_thickness": x.nominal_uncorroded_thickness_m <= .0508,
|
|
106
|
+
"no_cyclic_service": not x.cyclic_fatigue_or_vibration_design_service,
|
|
107
|
+
"no_environmental_cracking": not x.environmental_cracking,
|
|
108
|
+
"no_shock_chilling": not x.shock_chilling,
|
|
109
|
+
}
|
|
110
|
+
thin = x.component_thickness_m <= .0127
|
|
111
|
+
return {"service_experience_factor": .01 if thin or all(checks.values()) else 1,
|
|
112
|
+
"thin_component_credit": thin, "service_criteria": checks}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class BrittleBase(Model):
|
|
116
|
+
component_thickness_m: Positive
|
|
117
|
+
cet_minus_reference_k: Finite
|
|
118
|
+
post_weld_heat_treated: StrictBool
|
|
119
|
+
interpolation: Interpolation
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@calculation("damage/brittle-base", "Brittle fracture base damage table", BrittleBase,
|
|
123
|
+
["Part 2 Tables 20.4M,20.5M; PDF pp270-271"],
|
|
124
|
+
limitations=["Printed SI grid: thickness 6.4-101.6 mm and CET-Tref -56 to 56 K. Linear mode is caller-selected bilinear interpolation of DF, not log DF. No extrapolation or service credit applied here."])
|
|
125
|
+
def brittle_base(x):
|
|
126
|
+
table = PWHT if x.post_weld_heat_treated else NON_PWHT
|
|
127
|
+
rows = _weights(tuple(reversed(DELTA_K)), x.cet_minus_reference_k, x.interpolation)
|
|
128
|
+
cols = _weights(THICKNESS_MM, x.component_thickness_m*1000, x.interpolation)
|
|
129
|
+
value = sum(table[len(DELTA_K)-1-r][c]*rw*cw for r,rw in rows for c,cw in cols)
|
|
130
|
+
return {"base_damage_factor": value, "table": "20.5M" if x.post_weld_heat_treated else "20.4M"}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class BrittleFracture(Model):
|
|
134
|
+
reference: BrittleReference
|
|
135
|
+
service: BrittleService
|
|
136
|
+
critical_exposure_temperature_k: Positive
|
|
137
|
+
post_weld_heat_treated: StrictBool
|
|
138
|
+
interpolation: Interpolation
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@calculation("damage/brittle-fracture", "Brittle fracture reference/base/service calculation", BrittleFracture,
|
|
142
|
+
["Part 2 §20.6.3; Tables 20.3,20.4M,20.5M; eq (2.86)"],
|
|
143
|
+
limitations=["Caller establishes applicability, ASME curve and CET (including credited controls) per API 579. No inspection credit. Uses US reference-temperature table converted to SI plus printed SI base-DF tables. Caller selects interpolation separately for yield and base-DF grids; no extrapolation."])
|
|
144
|
+
def brittle_fracture(x):
|
|
145
|
+
reference = brittle_reference(x.reference)
|
|
146
|
+
delta = x.critical_exposure_temperature_k-reference["reference_temperature_k"]
|
|
147
|
+
base = brittle_base(BrittleBase(component_thickness_m=x.service.component_thickness_m,
|
|
148
|
+
cet_minus_reference_k=delta, post_weld_heat_treated=x.post_weld_heat_treated, interpolation=x.interpolation))
|
|
149
|
+
service = brittle_service(x.service)
|
|
150
|
+
return {**reference, **base, **service, "cet_minus_reference_k": delta,
|
|
151
|
+
"damage_factor": base["base_damage_factor"]*service["service_experience_factor"]}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Embrittlement885(Model):
|
|
155
|
+
minimum_evaluation_temperature_k: Positive
|
|
156
|
+
reference_temperature_k: Positive = 301.15
|
|
157
|
+
interpolation: Interpolation
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@calculation("damage/embrittlement-885f", "885 F embrittlement damage factor", Embrittlement885,
|
|
161
|
+
["Part 2 §22.6.3; Table 22.3; PDF pp279-280"],
|
|
162
|
+
limitations=["Qualified susceptible ferritic material and evaluation temperature supplied by caller; no inspection credit. Default reference is printed 28 C (301.15 K), which is not the exact conversion of printed 80 F. Uses printed SI delta-temperature rows. Above +56 K DF=0; at +56 K DF=2. Below -56 K rejected. Linear interpolation is an explicit caller choice."])
|
|
163
|
+
def embrittlement_885(x):
|
|
164
|
+
delta = x.minimum_evaluation_temperature_k-x.reference_temperature_k
|
|
165
|
+
# Preserve the explicit open-ended row; never interpolate across its discontinuity.
|
|
166
|
+
if delta > 56 and not math.isclose(delta, 56, rel_tol=0, abs_tol=1e-9):
|
|
167
|
+
factor = 0.0
|
|
168
|
+
else:
|
|
169
|
+
factor = _lookup(tuple(reversed(DELTA_K)), (1381,1216,1022,806,581,371,200,87,30,8,2), delta, x.interpolation)
|
|
170
|
+
return {"reference_temperature_k": x.reference_temperature_k, "minimum_minus_reference_k": delta,
|
|
171
|
+
"damage_factor": factor}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class SigmaContent(Model):
|
|
175
|
+
sigma_fraction: Fraction
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@calculation("damage/sigma-content", "Sigma-phase content category", SigmaContent,
|
|
179
|
+
["Part 2 Table 23.1; PDF p284"],
|
|
180
|
+
limitations=["Content >1% is tabulated: low >1 to <5%, medium >=5 to <10%, high >=10%. At or below 1% the printed categories are unresolved and rejected; no assumed zero DF."])
|
|
181
|
+
def sigma_content(x):
|
|
182
|
+
ensure(x.sigma_fraction > .01, "Sigma content at or below 1% has no unique category in Table 23.1", "unresolved_applicability")
|
|
183
|
+
return {"sigma_category": "low" if x.sigma_fraction < .05 else ("medium" if x.sigma_fraction < .1 else "high")}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
SIGMA_TEMP_C = (-46,-18,10,38,66,93,204,316,427,538,649)
|
|
187
|
+
SIGMA_FACTORS = {
|
|
188
|
+
"low": (1.1,1,.9,.6,.3,.1,0,0,0,0,0),
|
|
189
|
+
"medium": (34,20,11,7,5,3,1.3,.9,.2,0,0),
|
|
190
|
+
"high": (4196,4196,4196,4196,3871,3202,1333,481,160,53,18),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class SigmaDamage(Model):
|
|
195
|
+
evaluation_temperature_k: Positive
|
|
196
|
+
sigma_category: Literal["low", "medium", "high"]
|
|
197
|
+
interpolation: Interpolation
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@calculation("damage/sigma-embrittlement", "Sigma-phase embrittlement damage factor", SigmaDamage,
|
|
201
|
+
["Part 2 §23.6.3; Table 23.3; PDF pp283,285"],
|
|
202
|
+
limitations=["Qualified sigmatized austenitic stainless steel and content category supplied. No automatic exposure-to-sigma kinetics model or inspection credit. Printed SI evaluation-temperature grid -46 to 649 C; no extrapolation. Linear interpolation is caller-selected."])
|
|
203
|
+
def sigma_damage(x):
|
|
204
|
+
return {"damage_factor": _lookup(SIGMA_TEMP_C, SIGMA_FACTORS[x.sigma_category], x.evaluation_temperature_k-273.15, x.interpolation),
|
|
205
|
+
"sigma_category": x.sigma_category}
|
api581/bundle.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Part 1 §8 bundle life, cost and replacement planning primitives."""
|
|
2
|
+
import math
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from pydantic import Field, StrictBool
|
|
5
|
+
from .core import Model, NonNegative, Positive, Fraction, calculation, ensure
|
|
6
|
+
from .pof import Weibull, weibull
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LifeRecord(Model):
|
|
10
|
+
duration_years: NonNegative
|
|
11
|
+
failed: StrictBool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BundleHistory(Model):
|
|
15
|
+
shape: Positive
|
|
16
|
+
cycles: list[LifeRecord] = Field(min_length=2, max_length=10000)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _eta(shape, cycles):
|
|
20
|
+
failures = sum(record.failed for record in cycles)
|
|
21
|
+
ensure(failures > 0, "Zero failures give no finite point estimate of characteristic life", "insufficient_failure_data")
|
|
22
|
+
scale = max(record.duration_years for record in cycles)
|
|
23
|
+
ensure(scale > 0, "At least one positive life duration is required")
|
|
24
|
+
eta = scale*(math.fsum((record.duration_years/scale)**shape for record in cycles)/failures)**(1/shape)
|
|
25
|
+
ensure(eta > 0 and math.isfinite(eta), "Characteristic life outside numerical domain", "numerical_failure")
|
|
26
|
+
return eta, failures
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@calculation("bundle/weibayes-history", "Fixed-shape bundle Weibayes life estimate", BundleHistory,
|
|
30
|
+
["Part 1 §8.3.4.3; eqs (1.62)-(1.63); PDF p100"],
|
|
31
|
+
limitations=["At least two matched life records and one failure required; failures and right-censored suspensions enter the powered exposure sum. Owner qualifies unchanged design/material/service and externally selected shape. Does not fit shape, perform database matching, goodness-of-fit or Fisher confidence bounds."])
|
|
32
|
+
def bundle_history(x):
|
|
33
|
+
eta, failures = _eta(x.shape, x.cycles)
|
|
34
|
+
return {"shape": x.shape, "characteristic_life_years": eta,
|
|
35
|
+
"failure_count": failures, "record_count": len(x.cycles)}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BundleConditionUpdate(BundleHistory):
|
|
39
|
+
predicted_failure_life_years: Positive
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@calculation("bundle/condition-update", "Add inspection-predicted bundle failure point", BundleConditionUpdate,
|
|
43
|
+
["Part 1 §8.6.4(e); eqs (1.76)-(1.77); PDF pp104-105"],
|
|
44
|
+
limitations=["Adds predicted total life as one additional failure point, including one additional failure in denominator. This is not another right-censored observation. Owner supplies qualified predicted total life; statistical confidence and additional uncertainty are separate."])
|
|
45
|
+
def bundle_condition_update(x):
|
|
46
|
+
cycles = [*x.cycles, LifeRecord(duration_years=x.predicted_failure_life_years, failed=True)]
|
|
47
|
+
eta, failures = _eta(x.shape, cycles)
|
|
48
|
+
return {"shape":x.shape, "characteristic_life_years":eta, "failure_count":failures, "record_count":len(cycles)}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BundleThicknessLife(Model):
|
|
52
|
+
original_thickness_m: Positive
|
|
53
|
+
measured_thickness_m: Positive
|
|
54
|
+
service_duration_years: Positive
|
|
55
|
+
remaining_wall_failure_fraction: Fraction = Field(gt=0, lt=1)
|
|
56
|
+
formula_basis: Literal["printed_eq_1_72", "remaining_wall_threshold"]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@calculation("bundle/thickness-life", "Inspection-based tube thinning and predicted life", BundleThicknessLife,
|
|
60
|
+
["Part 1 §8.6.4(c); eqs (1.71)-(1.74); PDF pp103-104"],
|
|
61
|
+
origin="api581_equation_or_explicit_remaining_wall_interpretation",
|
|
62
|
+
limitations=["General corrosion and average tube thickness only. Printed eq (1.72) uses RWT*t_original/rate although RWT is described as remaining wall fraction; a physical threshold extrapolation uses (1-RWT)*t_original/rate. Caller must select basis explicitly. Both agree for the printed 0.5 example. Zero measured loss gives no finite extrapolated life, not a manufactured failure point."])
|
|
63
|
+
def bundle_thickness_life(x):
|
|
64
|
+
ensure(x.measured_thickness_m <= x.original_thickness_m, "Measured thickness exceeds original; negative thinning is outside this model")
|
|
65
|
+
rate = (x.original_thickness_m-x.measured_thickness_m)/x.service_duration_years
|
|
66
|
+
fraction = x.remaining_wall_failure_fraction
|
|
67
|
+
threshold_reached = x.measured_thickness_m <= fraction*x.original_thickness_m
|
|
68
|
+
life = None if rate == 0 else ((fraction if x.formula_basis == "printed_eq_1_72" else 1-fraction)*x.original_thickness_m/rate)
|
|
69
|
+
return {"thinning_rate_m_year":rate, "predicted_bundle_life_years":life,
|
|
70
|
+
"remaining_life_years": None if life is None else max(0,life-x.service_duration_years),
|
|
71
|
+
"remaining_wall_threshold_reached":threshold_reached, "formula_basis":x.formula_basis}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class BundleEstimatedLife(Model):
|
|
75
|
+
service_duration_years: NonNegative
|
|
76
|
+
estimated_remaining_life_years: NonNegative
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@calculation("bundle/estimated-life", "Predicted bundle life from estimated remaining life", BundleEstimatedLife,
|
|
80
|
+
["Part 1 §8.6.4(d); eq (1.75); PDF p104"])
|
|
81
|
+
def bundle_estimated_life(x):
|
|
82
|
+
return {"predicted_bundle_life_years": x.service_duration_years+x.estimated_remaining_life_years}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class BundleLifeExtension(Model):
|
|
86
|
+
age_at_action_years: NonNegative
|
|
87
|
+
life_extension_factor: Fraction
|
|
88
|
+
years_since_action: NonNegative = 0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@calculation("bundle/life-extension", "Effective bundle age after life-extension action", BundleLifeExtension,
|
|
92
|
+
["Part 1 §8.6.5; eqs (1.79)-(1.81); PDF p105"],
|
|
93
|
+
limitations=["Owner supplies qualified LEF. Returns effective age rather than imposing a date/day-count convention. Original age is not reset by an ordinary inspection or cleaning; apply only after a qualifying life-extension action."])
|
|
94
|
+
def bundle_life_extension(x):
|
|
95
|
+
adjusted = (1-x.life_extension_factor)*x.age_at_action_years
|
|
96
|
+
return {"adjusted_age_at_action_years":adjusted, "effective_age_years":adjusted+x.years_since_action,
|
|
97
|
+
"installation_shift_years": x.age_at_action_years-adjusted}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class BundleConsequence(Model):
|
|
101
|
+
daily_production_margin: NonNegative
|
|
102
|
+
production_rate_reduction_fraction: Fraction
|
|
103
|
+
shutdown_days: NonNegative
|
|
104
|
+
environmental_cost: NonNegative
|
|
105
|
+
replacement_cost: NonNegative
|
|
106
|
+
maintenance_cost: NonNegative
|
|
107
|
+
currency: str = Field(min_length=1, max_length=20)
|
|
108
|
+
cost_basis_year: int = Field(ge=1900, le=2200, strict=True)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@calculation("bundle/consequence", "Bundle failure or planned replacement financial consequence", BundleConsequence,
|
|
112
|
+
["Part 1 §8.4.1; eqs (1.64)-(1.67), (1.88); PDF pp100-101,107"],
|
|
113
|
+
limitations=["All monetary inputs must share declared currency and price year. Caller explicitly chooses planned or unplanned shutdown duration and environmental cost. No silent historical environmental default or inflation assumption."])
|
|
114
|
+
def bundle_consequence(x):
|
|
115
|
+
production = x.daily_production_margin*x.production_rate_reduction_fraction*x.shutdown_days
|
|
116
|
+
return {"production_loss_cost":production, "financial_consequence":production+x.environmental_cost+x.replacement_cost+x.maintenance_cost,
|
|
117
|
+
"currency":x.currency, "cost_basis_year":x.cost_basis_year}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class BundleRisk(Model):
|
|
121
|
+
shape: Positive
|
|
122
|
+
characteristic_life_years: Positive
|
|
123
|
+
age_years: NonNegative
|
|
124
|
+
financial_consequence: NonNegative
|
|
125
|
+
currency: str = Field(min_length=1, max_length=20)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@calculation("bundle/risk", "Bundle unreliability-weighted consequence", BundleRisk,
|
|
129
|
+
["Part 1 §8.5; eq (1.68); PDF p101"],
|
|
130
|
+
limitations=["Weibull cumulative probability times consequence. Despite source annual-risk terminology, this product is a probability-weighted monetary amount at age, not an instantaneous annual failure-frequency risk. No confidence-bound/inspection-uncertainty shift is inferred."])
|
|
131
|
+
def bundle_risk(x):
|
|
132
|
+
probability = weibull(Weibull(shape=x.shape, characteristic_life_years=x.characteristic_life_years,age_years=x.age_years))["cumulative_failure_probability"]
|
|
133
|
+
return {"cumulative_failure_probability":probability, "probability_weighted_consequence":probability*x.financial_consequence, "currency":x.currency}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class BundleTarget(Model):
|
|
137
|
+
shape: Positive
|
|
138
|
+
characteristic_life_years: Positive
|
|
139
|
+
target_probability_weighted_consequence: NonNegative
|
|
140
|
+
financial_consequence: Positive
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@calculation("bundle/target-age", "Bundle age at owner probability-weighted risk target", BundleTarget,
|
|
144
|
+
["Part 1 §8.6.1; eqs (1.57),(1.69)-(1.70); PDF pp97,102"],
|
|
145
|
+
limitations=["Uses an explicitly supplied target on the same probability-weighted consequence basis as bundle/risk. Target >= consequence has no finite crossing. Owner must supply any confidence-bound/uncertainty-adjusted Weibull parameters; raw parameters are not automatically treated as 90% LBC."])
|
|
146
|
+
def bundle_target(x):
|
|
147
|
+
ratio = x.target_probability_weighted_consequence/x.financial_consequence
|
|
148
|
+
age = None if ratio >= 1 else x.characteristic_life_years*(-math.log1p(-ratio))**(1/x.shape)
|
|
149
|
+
return {"target_failure_probability":min(1,ratio), "target_age_years":age,
|
|
150
|
+
"status":"no_finite_crossing" if age is None else "finite_crossing"}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class BundleDeferral(Model):
|
|
154
|
+
shape: Positive
|
|
155
|
+
characteristic_life_years: Positive
|
|
156
|
+
first_turnaround_age_years: NonNegative
|
|
157
|
+
second_turnaround_age_years: Positive
|
|
158
|
+
financial_consequence: NonNegative
|
|
159
|
+
inspection_cost: NonNegative
|
|
160
|
+
replacement_cost: NonNegative
|
|
161
|
+
maintenance_cost: NonNegative
|
|
162
|
+
hurdle_rate: NonNegative = 0
|
|
163
|
+
currency: str = Field(min_length=1, max_length=20)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@calculation("bundle/deferral-decision", "Conditional incremental risk and inspection/replacement economics", BundleDeferral,
|
|
167
|
+
["Part 1 §8.7.2; eqs (1.82)-(1.84); PDF p106"],
|
|
168
|
+
limitations=["Conditioned on survival to first turnaround; uses survival ratio, not the difference of unconditional CDFs. Source prose reverses action-cost inequality; explicit eqs (1.83)-(1.84) are followed: action cost*(1+ROR) < EIR. Both economic flags may be true; this is not an exclusive inspection plan."])
|
|
169
|
+
def bundle_deferral(x):
|
|
170
|
+
ensure(x.second_turnaround_age_years > x.first_turnaround_age_years, "Second turnaround must follow first")
|
|
171
|
+
# Compute cumulative-hazard difference without subtracting two underflowed survivals.
|
|
172
|
+
a,b = x.first_turnaround_age_years,x.second_turnaround_age_years
|
|
173
|
+
zb = (b/x.characteristic_life_years)**x.shape
|
|
174
|
+
if a == 0:
|
|
175
|
+
difference = zb
|
|
176
|
+
else:
|
|
177
|
+
log_ratio = math.log(a)-math.log(b) if a <= b/2 else math.log1p((a-b)/b)
|
|
178
|
+
difference = zb*(-math.expm1(x.shape*log_ratio))
|
|
179
|
+
probability = -math.expm1(-difference)
|
|
180
|
+
risk = x.financial_consequence*probability
|
|
181
|
+
inspect = (x.inspection_cost+x.maintenance_cost)*(1+x.hurdle_rate)
|
|
182
|
+
replace = (x.replacement_cost+x.maintenance_cost)*(1+x.hurdle_rate)
|
|
183
|
+
return {"conditional_failure_probability":probability,"expected_incremental_risk":risk,
|
|
184
|
+
"inspection_hurdle_cost":inspect,"replacement_hurdle_cost":replace,
|
|
185
|
+
"inspection_economically_justified":inspect < risk,"replacement_economically_justified":replace < risk,
|
|
186
|
+
"currency":x.currency}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class BundleReplacement(Model):
|
|
190
|
+
shape: Positive
|
|
191
|
+
characteristic_life_years: Positive
|
|
192
|
+
unplanned_failure_consequence: NonNegative
|
|
193
|
+
planned_replacement_consequence: Positive
|
|
194
|
+
step_days: Positive = 7
|
|
195
|
+
maximum_replacement_age_years: Positive
|
|
196
|
+
currency: str = Field(min_length=1, max_length=20)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@calculation("bundle/replacement-optimization", "Bundle planned-replacement grid cost optimization", BundleReplacement,
|
|
200
|
+
["Part 1 §8.7.4; eqs (1.85)-(1.93); PDF pp107-109"],
|
|
201
|
+
limitations=["Source right-endpoint failure-life integration on uniform grid; days/year=365.24. Global minimum is over supplied finite grid only, not an unbounded certified optimum. Boundary minima are flagged. No confidence bounds, cost inflation or automatic inspection effects. Grid limited to 10000 points."])
|
|
202
|
+
def bundle_replacement(x):
|
|
203
|
+
step = x.step_days/365.24
|
|
204
|
+
count = math.floor(x.maximum_replacement_age_years/step)
|
|
205
|
+
ensure(1 <= count <= 10000, "Replacement grid must contain 1-10000 points")
|
|
206
|
+
fail_life, previous, samples = 0.0,0.0,[]
|
|
207
|
+
for n in range(1,count+1):
|
|
208
|
+
age = n*step
|
|
209
|
+
z = (age/x.characteristic_life_years)**x.shape
|
|
210
|
+
p,s = -math.expm1(-z),math.exp(-z)
|
|
211
|
+
fail_life += age*(p-previous)
|
|
212
|
+
life = fail_life+age*s
|
|
213
|
+
ensure(life > 0, "Estimated service life underflow", "numerical_failure")
|
|
214
|
+
cost = (x.unplanned_failure_consequence*p+x.planned_replacement_consequence*s)/(365.24*life)
|
|
215
|
+
samples.append({"replacement_age_years":age,"failure_probability":p,
|
|
216
|
+
"expected_service_life_years":life,"total_cost_per_day":cost})
|
|
217
|
+
previous = p
|
|
218
|
+
index = min(range(len(samples)),key=lambda i:samples[i]["total_cost_per_day"])
|
|
219
|
+
best = samples[index]
|
|
220
|
+
return {"optimal_grid_age_years":best["replacement_age_years"],"minimum_cost_per_day":best["total_cost_per_day"],
|
|
221
|
+
"minimum_at_grid_boundary":index in (0,count-1),"grid_points":count,"samples":samples,"currency":x.currency}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
MATERIAL_FACTORS = {
|
|
225
|
+
"Carbon Steel":1, "C-1/2 Mo":2, "1-1/4 Cr":2, "2-1/4 Cr":2.8,
|
|
226
|
+
"5 Cr":3.2, "9 Cr":3.3, "12 Cr":3.4, "70/30 CuNi":3.5, "90/10 CuNi":3.5,
|
|
227
|
+
"Monel 400":7, "Nickel 200":8.5, "304/309/310 SS":2.6, "304L/321/347 SS":2.8,
|
|
228
|
+
"316 SS":3, "316L SS":3, "317L SS":4.2, "410/439 SS":2.8, "444 SS":3.2,
|
|
229
|
+
"904L":7, "2205 Duplex SS":3, "2304 Duplex SS":2.8, "2507 Duplex SS":4,
|
|
230
|
+
"AL6XN/254 SMO":7, "Seacure/E-Brite":6,
|
|
231
|
+
"Admiralty Brass/Aluminum Brass/Red Brass/Muntz":2.5, "Aluminum Alloy":3,
|
|
232
|
+
"Alloy 20 Cb3":6.5, "Alloy 600":9.5, "Alloy 625":11, "Alloy 800":7,
|
|
233
|
+
"Alloy 825":8, "Alloy C276":11, "Ferralium 255":7, "Bimetallic":4.5,
|
|
234
|
+
"Ceramic":1, "Plastic":1, "Titanium Gr. 2":6, "Titanium Gr. 12":10,
|
|
235
|
+
"Titanium Gr. 16":14, "Zeron 100":4, "Zirconium Alloy":15,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class BundleMaterial(Model):
|
|
240
|
+
material: str
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@calculation("bundle/material-factor", "Bundle replacement material cost factor", BundleMaterial,
|
|
244
|
+
["Part 1 Table8.3; PDF pp119-120"],
|
|
245
|
+
limitations=["Historical generic bundle factors, not current quotations or the fixed-equipment material factors. Caller may supply a different qualified factor directly to replacement-cost."])
|
|
246
|
+
def bundle_material(x):
|
|
247
|
+
ensure(x.material in MATERIAL_FACTORS, "Unsupported material; use a printed Table8.3 material label")
|
|
248
|
+
return {"material_factor":MATERIAL_FACTORS[x.material]}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class BundleReplacementCost(Model):
|
|
252
|
+
shell_inside_diameter_m: Positive
|
|
253
|
+
tube_length_m: Positive
|
|
254
|
+
material_factor: Positive
|
|
255
|
+
reference_bundle_cost_usd: NonNegative = 22000
|
|
256
|
+
price_index_multiplier: Positive
|
|
257
|
+
output_currency_per_usd: Positive
|
|
258
|
+
output_currency: str = Field(min_length=1, max_length=20)
|
|
259
|
+
output_cost_basis_year: int = Field(strict=True, ge=1900, le=2200)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@calculation("bundle/replacement-cost", "Volume/material-scaled bundle replacement cost", BundleReplacementCost,
|
|
263
|
+
["Part 1 §8.4.1; eq (1.66); PDF p101"],
|
|
264
|
+
limitations=["Printed reference volume 3.016 m3 and default reference quote USD22000. Reference quote date is not inferred from edition year; caller supplies qualified price-index multiplier and FX to declared output price year/currency. This is a size/material estimate, not a vendor quote."])
|
|
265
|
+
def bundle_replacement_cost(x):
|
|
266
|
+
volume = math.pi*x.shell_inside_diameter_m**2*x.tube_length_m/4
|
|
267
|
+
reference = x.reference_bundle_cost_usd*volume*x.material_factor/3.016
|
|
268
|
+
return {"bundle_volume_m3":volume,"reference_basis_cost_usd":reference,
|
|
269
|
+
"replacement_cost":reference*x.price_index_multiplier*x.output_currency_per_usd,
|
|
270
|
+
"currency":x.output_currency,"cost_basis_year":x.output_cost_basis_year}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class BundleExtensionFactor(Model):
|
|
274
|
+
method: Literal["plug_tubes", "rotate_180", "partial_retube", "total_retube", "install_spare", "tube_ferrules"]
|
|
275
|
+
ferrules_for_localized_tube_end_damage: StrictBool = False
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@calculation("bundle/life-extension-factor", "Suggested bundle life-extension action factor", BundleExtensionFactor,
|
|
279
|
+
["Part 1 Table8.5 Effects of Bundle Life Extension Methods; PDF p121"],
|
|
280
|
+
limitations=["This is the second table numbered8.5; inspection uncertainty is a different table. Suggested values need owner qualification. Spare factor .5 assumes unknown condition. Ferrules .75 only for localized tube-end erosion/corrosion/impingement protection."])
|
|
281
|
+
def bundle_extension_factor(x):
|
|
282
|
+
ensure(x.method != "tube_ferrules" or x.ferrules_for_localized_tube_end_damage,
|
|
283
|
+
"Ferrule LEF requires qualified protection against localized tube-end damage")
|
|
284
|
+
factors={"plug_tubes":.1,"rotate_180":.5,"partial_retube":.5,"total_retube":.9,"install_spare":.5,"tube_ferrules":.75}
|
|
285
|
+
return {"life_extension_factor":factors[x.method]}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class BundleCategories(Model):
|
|
289
|
+
cumulative_failure_probability: Fraction
|
|
290
|
+
financial_consequence_usd: NonNegative
|
|
291
|
+
consequence_basis: Literal["printed_table_USD_basis"]
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@calculation("bundle/categories", "Bundle probability and financial consequence categories", BundleCategories,
|
|
295
|
+
["Part 1 Table8.6; PDF p121"],
|
|
296
|
+
limitations=["Uses displayed probability and financial-USD thresholds, not inconsistent DF/area footnotes. Caller explicitly declares consequence on printed USD basis; no automatic present-value inflation or FX normalization. Returns categories only; owner risk-color matrix is separate."])
|
|
297
|
+
def bundle_categories(x):
|
|
298
|
+
p = 1+sum(x.cumulative_failure_probability > b for b in (.1,.2,.3,.5))
|
|
299
|
+
c = sum(x.financial_consequence_usd > b for b in (10000,50000,150000,1000000))
|
|
300
|
+
return {"probability_category":p,"consequence_category":"ABCDE"[c]}
|