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.
Files changed (139) hide show
  1. api581/__init__.py +80 -0
  2. api581/_amine_tables.py +105 -0
  3. api581/_h2sh2_tables.py +161 -0
  4. api581/_slabx_worker.py +174 -0
  5. api581/_sulfidic_tables.py +517 -0
  6. api581/_sulfuric_tables.py +328 -0
  7. api581/acid_sour_water.py +238 -0
  8. api581/acscc_screening.py +88 -0
  9. api581/alkaline_sour_water.py +267 -0
  10. api581/amine_corrosion.py +277 -0
  11. api581/amine_screening.py +103 -0
  12. api581/api/__init__.py +4 -0
  13. api581/api/api520.py +88 -0
  14. api581/api/bundle.py +121 -0
  15. api581/api/consequence.py +102 -0
  16. api581/api/damage.py +1831 -0
  17. api581/api/dispersion.py +121 -0
  18. api581/api/effects.py +318 -0
  19. api581/api/events.py +96 -0
  20. api581/api/financial.py +115 -0
  21. api581/api/frequency.py +40 -0
  22. api581/api/inspection.py +96 -0
  23. api581/api/level1.py +238 -0
  24. api581/api/level2.py +74 -0
  25. api581/api/planning.py +17 -0
  26. api581/api/prd.py +190 -0
  27. api581/api/properties.py +130 -0
  28. api581/api/release.py +252 -0
  29. api581/api/reliability.py +40 -0
  30. api581/api/risk.py +71 -0
  31. api581/api/tank.py +490 -0
  32. api581/api520.py +205 -0
  33. api581/area_contribution.py +72 -0
  34. api581/blast.py +232 -0
  35. api581/brittle.py +205 -0
  36. api581/bundle.py +300 -0
  37. api581/catalog.py +442 -0
  38. api581/caustic_screening.py +103 -0
  39. api581/clad_thinning.py +146 -0
  40. api581/clscc_screening.py +55 -0
  41. api581/co2_corrosion.py +313 -0
  42. api581/co2_screening.py +205 -0
  43. api581/consequences.py +250 -0
  44. api581/contracts.py +1038 -0
  45. api581/cooling_selection.py +204 -0
  46. api581/cooling_water.py +305 -0
  47. api581/core.py +126 -0
  48. api581/corrosion_rate.py +38 -0
  49. api581/cost_tables.py +175 -0
  50. api581/cracking.py +74 -0
  51. api581/data/coverage.json +21361 -0
  52. api581/data/digital-twin.schema.json +538 -0
  53. api581/data/direct-test-queue.json +7 -0
  54. api581/data/native-engine-evidence.json +44 -0
  55. api581/data/python-api.schema.json +92706 -0
  56. api581/data/reference-fixture-evidence.json +2956 -0
  57. api581/data/scope-boundaries.schema.json +58 -0
  58. api581/data/software-test-evidence.json +4278 -0
  59. api581/data/standard-metadata.json +1328 -0
  60. api581/data/standard.schema.json +233 -0
  61. api581/data/supporting-text-review.json +1319 -0
  62. api581/digital_twin.py +491 -0
  63. api581/dispersion.py +45 -0
  64. api581/effects.py +232 -0
  65. api581/evaporation.py +181 -0
  66. api581/evidence.py +81 -0
  67. api581/external_corrosion.py +148 -0
  68. api581/external_cracking.py +63 -0
  69. api581/fatigue.py +56 -0
  70. api581/fireball.py +163 -0
  71. api581/flash_pool.py +111 -0
  72. api581/flash_solver.py +122 -0
  73. api581/groundwater.py +101 -0
  74. api581/h2sh2_corrosion.py +128 -0
  75. api581/hic_screened.py +89 -0
  76. api581/htha.py +45 -0
  77. api581/hydrochloric_acid.py +272 -0
  78. api581/hydrofluoric_acid.py +236 -0
  79. api581/ignition.py +204 -0
  80. api581/inspection_effectiveness.py +601 -0
  81. api581/jet_handoff.py +60 -0
  82. api581/level1.py +311 -0
  83. api581/level1_summary.py +35 -0
  84. api581/level1_toxic.py +313 -0
  85. api581/level1_type1.py +91 -0
  86. api581/level2_summary.py +237 -0
  87. api581/lining.py +173 -0
  88. api581/native_toxic.py +67 -0
  89. api581/outputs.py +5672 -0
  90. api581/oxidation.py +145 -0
  91. api581/pascc_screening.py +78 -0
  92. api581/physical_explosion.py +190 -0
  93. api581/pof.py +207 -0
  94. api581/pool_fire.py +273 -0
  95. api581/portfolio.py +68 -0
  96. api581/prd.py +204 -0
  97. api581/prd_consequences.py +245 -0
  98. api581/prd_history.py +44 -0
  99. api581/py.typed +0 -0
  100. api581/release.py +307 -0
  101. api581/representative_fluids.py +172 -0
  102. api581/result.py +50 -0
  103. api581/risk.py +191 -0
  104. api581/saturation.py +66 -0
  105. api581/scc_mechanisms.py +242 -0
  106. api581/schema.py +45 -0
  107. api581/slabx_adapter.py +261 -0
  108. api581/soil_corrosion.py +191 -0
  109. api581/source_partition.py +169 -0
  110. api581/spatial.py +87 -0
  111. api581/ssc_screening.py +77 -0
  112. api581/standard.py +281 -0
  113. api581/storage.py +42 -0
  114. api581/sulfidic_corrosion.py +197 -0
  115. api581/sulfuric_acid.py +209 -0
  116. api581/tank.py +260 -0
  117. api581/tank_corrosion.py +256 -0
  118. api581/tank_environment.py +186 -0
  119. api581/tank_financial.py +202 -0
  120. api581/tank_integrated.py +51 -0
  121. api581/tank_shell.py +141 -0
  122. api581/tank_source.py +152 -0
  123. api581/tank_thinning.py +204 -0
  124. api581/temper.py +105 -0
  125. api581/thinning.py +172 -0
  126. api581/thinning_conditions.py +212 -0
  127. api581/thinning_history.py +118 -0
  128. api581/thinning_selection.py +81 -0
  129. api581/thinning_type.py +34 -0
  130. api581/toxic_criteria.py +111 -0
  131. api581/toxic_level2.py +233 -0
  132. api581/two_phase_events.py +120 -0
  133. api581/workflow.py +42 -0
  134. api581_engine-0.1.4.dist-info/METADATA +419 -0
  135. api581_engine-0.1.4.dist-info/RECORD +139 -0
  136. api581_engine-0.1.4.dist-info/WHEEL +5 -0
  137. api581_engine-0.1.4.dist-info/licenses/LICENSE +161 -0
  138. api581_engine-0.1.4.dist-info/licenses/NOTICE.md +18 -0
  139. api581_engine-0.1.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,38 @@
1
+ """Measured corrosion-rate primitives from API 581 thinning guidance."""
2
+
3
+ from pydantic import Field
4
+
5
+ from .core import Model, Positive, NonNegative, calculation, ensure
6
+
7
+
8
+ class MeasuredCorrosionRate(Model):
9
+ initial_thickness_m: Positive
10
+ current_thickness_m: Positive
11
+ elapsed_years: Positive
12
+ measurement_basis: str = Field(min_length=1, max_length=1000)
13
+
14
+
15
+ @calculation(
16
+ "damage/measured-corrosion-rate",
17
+ "Measured thickness-loss corrosion rate",
18
+ MeasuredCorrosionRate,
19
+ ["Part 2 Annex B §1.1-§1.2; Part 2 §4.5.5, PDF pp143-152"],
20
+ origin="api581_measured_thickness_loss_rate",
21
+ limitations=[
22
+ "Computes the direct constant-rate interpretation (initial thickness minus current thickness divided by elapsed time). It does not select inspection history, uncertainty distributions, mechanism applicability or a future thinning rate.",
23
+ "Thickness increase is rejected as a measurement/baseline inconsistency rather than silently converted to zero corrosion. The caller must qualify comparable locations, units, dates and measurement quality."
24
+ ],
25
+ )
26
+ def measured_corrosion_rate(x):
27
+ ensure(x.current_thickness_m <= x.initial_thickness_m,
28
+ "Current thickness cannot exceed the qualified initial thickness", "inconsistent_thickness_baseline")
29
+ loss = x.initial_thickness_m - x.current_thickness_m
30
+ return {
31
+ "metal_loss_m": loss,
32
+ "corrosion_rate_m_year": loss / x.elapsed_years,
33
+ "initial_thickness_m": x.initial_thickness_m,
34
+ "current_thickness_m": x.current_thickness_m,
35
+ "elapsed_years": x.elapsed_years,
36
+ "measurement_basis": x.measurement_basis,
37
+ "rate_basis": "direct_two_thickness_difference",
38
+ }
api581/cost_tables.py ADDED
@@ -0,0 +1,175 @@
1
+ """Part 3 Tables 4.15-4.18M and associated financial calculations."""
2
+ import math
3
+ from typing import Literal
4
+ from pydantic import Field
5
+ from .core import Model, Positive, NonNegative, Fraction, calculation, ensure
6
+ from .pof import Component, generic, HOLES
7
+
8
+ REPAIR = {
9
+ "COMPC":(10000,20000,100000,300000),"COMPR":(5000,10000,50000,100000),
10
+ "HEXSS":(1000,2000,20000,60000),"HEXTS":(1000,2000,20000,60000),"HEXTUBE":(1000,2000,20000,60000),
11
+ "PIPE-1":(5,0,0,20),"PIPE-2":(5,0,0,40),"PIPE-4":(5,10,0,60),"PIPE-6":(5,20,0,120),
12
+ "PIPE-8":(5,30,60,180),"PIPE-10":(5,40,80,240),"PIPE-12":(5,60,120,360),"PIPE-16":(5,80,160,500),"PIPEGT16":(10,120,240,700),
13
+ "PUMP2S":(1000,2500,5000,5000),"PUMP1S":(1000,2500,5000,5000),"PUMPR":(1000,2500,5000,10000),
14
+ "TANKBOTTOM":(5000,0,0,120000),"COURSE-1-10":(5000,12000,20000,40000),
15
+ "KODRUM":(5000,12000,20000,40000),"DRUM":(5000,12000,20000,40000),"FINFAN":(1000,2000,20000,60000),
16
+ "FILTER":(1000,2000,4000,10000),"REACTOR":(10000,24000,40000,80000),
17
+ "COLTOP":(10000,25000,50000,100000),"COLMID":(10000,25000,50000,100000),"COLBTM":(10000,25000,50000,100000)}
18
+
19
+ MATERIAL = {"carbon_steel":1,"1.25Cr-0.5Mo":1.3,"2.25Cr-1Mo":1.7,"5Cr-0.5Mo":1.7,"7Cr-0.5Mo":2,
20
+ "clad_304_ss":2.1,"polypropylene_lined":2.5,"9Cr-1Mo":2.6,"405_ss":2.8,"410_ss":2.8,"304_ss":3.2,
21
+ "clad_316_ss":3.3,"cs_saran_lined":3.4,"cs_rubber_lined":4.4,"316_ss":4.8,"cs_glass_lined":5.8,"clad_alloy_400":6.4,
22
+ "90_10_CuNi":6.8,"clad_alloy_600":7,"cs_teflon_lined":7.8,"clad_nickel":8,"alloy_800":8.4,"70_30_CuNi":8.5,
23
+ "904L":8.8,"alloy_20":11,"alloy_400":15,"alloy_600":15,"nickel":18,"alloy_625":26,"titanium":28,
24
+ "alloy_C":29,"zirconium":34,"alloy_B":36,"tantalum":535}
25
+
26
+ OUTAGE = {"COMPC":(None,3,7,None),"COMPR":(None,3,7,None),"HEXSS":(2,3,3,10),"HEXTS":(2,3,3,10),
27
+ "HEXTUBE":(None,None,None,None),"PIPE-1":(0,None,None,1),"PIPE-2":(0,None,None,1),"PIPE-4":(0,1,None,2),
28
+ "PIPE-6":(0,1,2,3),"PIPE-8":(0,2,2,3),"PIPE-10":(0,2,2,4),"PIPE-12":(1,3,4,4),"PIPE-16":(1,3,4,5),"PIPEGT16":(1,4,5,7),
29
+ "PUMP2S":(0,0,0,None),"PUMPR":(0,0,0,None),"PUMP1S":(0,0,0,None),"TANKBOTTOM":(5,None,None,50),
30
+ "COURSE-1-10":(2,3,3,14),"KODRUM":(2,3,3,10),"FINFAN":(0,0,2,3),"FILTER":(0,1,2,3),"DRUM":(2,3,3,10),
31
+ "REACTOR":(4,6,6,21),"COLTOP":(3,4,5,21),"COLMID":(3,4,5,21),"COLBTM":(3,4,5,21)}
32
+
33
+ # MW kg/kmol, tabulated liquid density kg/m3, NBP Celsius, fraction evaporated in 24h.
34
+ LEAK = {"C1-C2":(23,250.513,-125,1),"C3-C5":(58,580.012,-1,1),"C6-C8":(100,684.020,99,.9),
35
+ "C9-C12":(149,734.014,184,.5),"C13-C16":(205,764.529,261,.1),"C17-C25":(280,775.021,344,.05),"C25+":(422,900.029,527,.02),
36
+ "Acid":(18,997.950,100,.9),"H2":(2,71.010,-253,1),"H2S":(34,993.032,-59,1),"HF":(20,967.034,20,1),
37
+ "CO":(28,800.923,-191,1),"DEE":(74,720.831,35,1),"HCL":(36,1185.366,-85,1),"Nitric Acid":(63,1521.754,121,.8),
38
+ "NO2":(90,929.071,135,.75),"Phosgene":(99,1377.588,83,1),"TDI":(174,1217.403,251,.15),"Methanol":(32,800.923,65,1),
39
+ "PO":(58,832.960,34,1),"Styrene":(104,683.988,145,.6),"EEA":(132,977.126,156,.65),"EE":(90,929.071,135,.75),
40
+ "EG":(62,1105.274,197,.45),"EO":(44,881.015,0,1)}
41
+
42
+
43
+ @calculation("financial/repair-table", "2001 USD component repair cost table", Component,
44
+ ["Part 3 Table 4.15; PDF p532"], limitations=["Historical 2001 USD, not current prices. COURSE-1-10 normalizes source COURSES-10 label."])
45
+ def repair_table(x):
46
+ ensure(x.component_type in REPAIR,"Unsupported repair cost component")
47
+ return {"cost_by_hole_usd_2001":dict(zip(HOLES,REPAIR[x.component_type])),"cost_basis_year":2001,"currency":"USD"}
48
+
49
+
50
+ class Material(Model):
51
+ material_id: Literal[tuple(MATERIAL)]
52
+
53
+
54
+ @calculation("financial/material-factor", "Material repair cost multiplier", Material,["Part 3 Table 4.16; PDF p533"])
55
+ def material_factor(x):
56
+ ensure(x.material_id in MATERIAL,"Unsupported material_id; inspect MODULE_CATALOG or table constants")
57
+ return {"material_factor":MATERIAL[x.material_id]}
58
+
59
+
60
+ @calculation("financial/outage-table", "Estimated equipment outage table", Component,
61
+ ["Part 3 Table 4.17; PDF p534"], limitations=["NA is returned as null, never zero. Historical estimates require owner review."])
62
+ def outage_table(x):
63
+ ensure(x.component_type in OUTAGE,"Unsupported outage component")
64
+ return {"days_by_hole":dict(zip(HOLES,OUTAGE[x.component_type]))}
65
+
66
+
67
+ class RepairCost(Model):
68
+ component_type: str
69
+ material_id: Literal[tuple(MATERIAL)]
70
+ price_factor_since_2001: Positive
71
+ usd_to_output_currency: Positive
72
+ cost_basis_year: int = Field(ge=2001,le=2200)
73
+ currency: str = Field(pattern=r"^[A-Z]{3}$")
74
+
75
+
76
+ @calculation("financial/component-repair", "GFF-weighted material-adjusted component repair cost", RepairCost,
77
+ ["Part 3 eq (3.86), Tables 4.15/4.16"], limitations=["Inflation and currency conversion are explicit caller assumptions, not fetched market data. Non-tank GFF support only."])
78
+ def repair_cost(x):
79
+ gff=generic(Component(component_type=x.component_type))
80
+ costs=repair_table(Component(component_type=x.component_type))["cost_by_hole_usd_2001"]
81
+ factor=material_factor(Material(material_id=x.material_id))["material_factor"]
82
+ base=math.fsum(gff["by_hole_per_year"][h]*costs[h] for h in HOLES)/gff["total_per_year"]*factor
83
+ return {"weighted_repair_cost_usd_2001":base,"cost":base*x.price_factor_since_2001*x.usd_to_output_currency,
84
+ "material_factor":factor,"cost_basis_year":x.cost_basis_year,"currency":x.currency}
85
+
86
+
87
+ class ComponentOutage(Model):
88
+ component_type: str
89
+ outage_multiplier: Positive
90
+ override_days: dict[Literal["small","medium","large","rupture"],NonNegative] = Field(default_factory=dict)
91
+
92
+
93
+ @calculation("financial/component-outage", "GFF-weighted component outage duration", ComponentOutage,
94
+ ["Part 3 eq (3.88), Table 4.17"], limitations=["A nonzero-frequency hole with NA outage requires an explicit owner-supplied override; NA is not silently filled."])
95
+ def component_outage(x):
96
+ gff=generic(Component(component_type=x.component_type))
97
+ days=outage_table(Component(component_type=x.component_type))["days_by_hole"]
98
+ days.update(x.override_days)
99
+ terms=[]
100
+ for h in HOLES:
101
+ frequency=gff["by_hole_per_year"][h]
102
+ ensure(days[h] is not None or frequency==0,f"Outage NA for nonzero-frequency hole {h}; supply override_days", "missing_outage_override")
103
+ terms.append(frequency*(days[h] if days[h] is not None else 0))
104
+ return {"component_outage_days":math.fsum(terms)/gff["total_per_year"]*x.outage_multiplier,"effective_days_by_hole":days}
105
+
106
+
107
+ class SurroundingOutage(Model):
108
+ surrounding_damage_cost_usd: NonNegative
109
+
110
+
111
+ @calculation("financial/surrounding-outage", "Surrounding equipment repair outage correlation", SurroundingOutage,
112
+ ["Part 3 eq (3.89); PDF p506"], limitations=["USD cost input only; correlation monetary basis must be consistent with the source methodology. Zero damage cost returns the continuous limit 0 days."])
113
+ def surrounding_outage(x):
114
+ # Algebraic power form equals 10**(1.242+.585*log10(cost*1e-6)), and handles cost=0.
115
+ return {"surrounding_outage_days":10**1.242*(x.surrounding_damage_cost_usd/1e6)**.585}
116
+
117
+
118
+ class LeakProperties(Model):
119
+ fluid: Literal[tuple(LEAK)]
120
+
121
+
122
+ @calculation("properties/leak-table", "Tabulated SI leak/remediation properties", LeakProperties,
123
+ ["Part 3 Table 4.18M; PDF p536"], limitations=["Table liquid density for remediation, not operating gas density or a thermodynamic property solver. Source C3-C5 label is retained and not silently aliased to C3-C4."])
124
+ def leak_properties(x):
125
+ ensure(x.fluid in LEAK,"Unsupported leak-properties fluid")
126
+ mw,density,nbp,evap=LEAK[x.fluid]
127
+ return {"molecular_weight_kg_kmol":mw,"tabulated_liquid_density_kg_m3":density,"normal_boiling_point_k":nbp+273.15,
128
+ "evaporated_fraction_24h":evap}
129
+
130
+
131
+ class CleanupEligibility(Model):
132
+ release_phase: Literal["gas","liquid"]
133
+ normal_boiling_point_k: Positive
134
+ autoignition_likely: bool
135
+
136
+
137
+ @calculation("financial/cleanup-eligibility", "Level 1 environmental cleanup screening", CleanupEligibility,
138
+ ["Part 3 §4.12.6"], limitations=["Uses the text's rounded 93 Celsius threshold; source also states 200 Fahrenheit. Screening only; no cost or evaporation-rate model."])
139
+ def cleanup_eligibility(x):
140
+ eligible=x.release_phase=="liquid" and x.normal_boiling_point_k>=366.15 and not x.autoignition_likely
141
+ return {"include_cleanup_cost":eligible}
142
+
143
+
144
+ class FinancialConsequence(Model):
145
+ repair: RepairCost
146
+ outage: ComponentOutage
147
+ equipment_area_m2: NonNegative
148
+ personnel_area_m2: NonNegative
149
+ equipment_cost_per_m2: NonNegative
150
+ production_cost_per_day: NonNegative
151
+ population_density_per_m2: NonNegative
152
+ cost_per_injury: NonNegative
153
+ weighted_cleanup_volume_m3: NonNegative
154
+ cleanup_cost_per_m3: NonNegative
155
+
156
+
157
+ @calculation("financial/level1-consequence", "Compose five Level 1 financial consequence categories", FinancialConsequence,
158
+ ["Part 3 §§4.12.2-4.12.7 eqs (3.85)-(3.94)"],
159
+ limitations=["Input areas and remediation volume must already be hole-weighted and screened. All user monetary inputs use repair.currency and repair.cost_basis_year. Surrounding-outage correlation receives equivalent 2001 USD via the explicit price/currency factors."])
160
+ def financial_consequence(x):
161
+ ensure(x.repair.component_type==x.outage.component_type,"Repair and outage component types must match")
162
+ repair=repair_cost(x.repair)
163
+ outage=component_outage(x.outage)
164
+ surrounding=x.equipment_area_m2*x.equipment_cost_per_m2
165
+ reference_cost=surrounding/x.repair.usd_to_output_currency/x.repair.price_factor_since_2001
166
+ downtime=surrounding_outage(SurroundingOutage(surrounding_damage_cost_usd=reference_cost))["surrounding_outage_days"]
167
+ production=(outage["component_outage_days"]+downtime)*x.production_cost_per_day
168
+ injury=x.personnel_area_m2*x.population_density_per_m2*x.cost_per_injury
169
+ environment=x.weighted_cleanup_volume_m3*x.cleanup_cost_per_m3
170
+ return {"component_repair_cost":repair["cost"],"surrounding_equipment_cost":surrounding,
171
+ "business_interruption_cost":production,"injury_cost":injury,"environmental_cost":environment,
172
+ "component_outage_days":outage["component_outage_days"],"surrounding_outage_days":downtime,
173
+ "surrounding_cost_reference_usd_2001":reference_cost,
174
+ "financial_consequence":math.fsum([repair["cost"],surrounding,production,injury,environment]),
175
+ "currency":x.repair.currency,"cost_basis_year":x.repair.cost_basis_year}
api581/cracking.py ADDED
@@ -0,0 +1,74 @@
1
+ """SCC base table and caustic escalation; susceptibility screening is external."""
2
+ from typing import Literal
3
+ from pydantic import Field
4
+ from .core import Model, NonNegative, calculation, ensure
5
+
6
+ # Columns are D,C,B,A for each of 1..6 inspections. PDF p175 visually checked.
7
+ SCC = {
8
+ 0: [(0, 0, 0, 0)] * 6,
9
+ 1: [(1, 1, 1, 1)] * 6,
10
+ 10: [(8, 3, 1, 1), (6, 2, 1, 1), (4, 1, 1, 1), (2, 1, 1, 1), (1, 1, 1, 1), (1, 1, 1, 1)],
11
+ 50: [(40, 17, 5, 3), (30, 10, 2, 1), (20, 5, 1, 1), (10, 2, 1, 1), (5, 1, 1, 1), (1, 1, 1, 1)],
12
+ 100: [(80, 33, 10, 5), (60, 20, 4, 1), (40, 10, 2, 1), (20, 5, 1, 1), (10, 2, 1, 1), (5, 1, 1, 1)],
13
+ 500: [(400, 170, 50, 25), (300, 100, 20, 5), (200, 50, 8, 1), (100, 25, 2, 1), (50, 10, 1, 1), (25, 5, 1, 1)],
14
+ 1000: [(800, 330, 100, 50), (600, 200, 40, 10), (400, 100, 16, 2), (200, 50, 5, 1), (100, 25, 2, 1), (50, 10, 1, 1)],
15
+ 5000: [(4000, 1670, 500, 250), (3000, 1000, 250, 50), (2000, 500, 80, 10), (1000, 250, 25, 2), (500, 125, 5, 1), (250, 50, 2, 1)],
16
+ }
17
+
18
+
19
+ class SccBase(Model):
20
+ severity_index: Literal[0, 1, 10, 50, 100, 500, 1000, 5000]
21
+ inspection_grade: Literal["A", "B", "C", "D", "E"]
22
+ inspection_count: int = Field(ge=0, le=6)
23
+
24
+
25
+ @calculation("damage/scc-base", "SCC base damage factor lookup", SccBase,
26
+ ["Part 2 Table 6.3; PDF p175"],
27
+ limitations=["Severity and highest equivalent inspection category supplied by caller. Count >6 is not extrapolated. Ambiguous E/500 cell is rejected."])
28
+ def scc_base(x):
29
+ if x.inspection_grade == "E":
30
+ ensure(x.inspection_count == 0, "E means no effective inspections; count must be 0")
31
+ ensure(x.severity_index != 500, "Published E/500 cell is ambiguous; requires source reconciliation", "ambiguous_source")
32
+ value = x.severity_index
33
+ else:
34
+ ensure(x.inspection_count >= 1, "A-D lookup requires at least one inspection")
35
+ value = SCC[x.severity_index][x.inspection_count - 1]["DCBA".index(x.inspection_grade)]
36
+ return {"base_damage_factor": value}
37
+
38
+
39
+ class CausticSeverity(Model):
40
+ susceptibility: Literal["none", "low", "medium", "high"]
41
+
42
+
43
+ @calculation("damage/caustic-severity", "Caustic cracking severity index", CausticSeverity,
44
+ ["Part 2 Table 6.2; PDF p174"], limitations=["This low-level severity lookup accepts qualified susceptibility; use damage/caustic-screening, damage/caustic-figure-susceptibility or damage/caustic-screened-assessment for the implemented material/exposure and Figure 6.2 routing. It does not infer applicability from chemistry."])
45
+ def caustic_severity(x):
46
+ return {"severity_index": {"none": 0, "low": 50, "medium": 500, "high": 5000}[x.susceptibility]}
47
+
48
+
49
+ class CausticEscalation(Model):
50
+ base_damage_factor: NonNegative
51
+ years_since_qualifying_inspection: NonNegative
52
+
53
+
54
+ @calculation("damage/caustic-escalation", "Caustic cracking time escalation", CausticEscalation,
55
+ ["Part 2 §6.6.3 eq (2.25); PDF p173"],
56
+ limitations=["Age must follow qualifying A/B/C inspection or repair criteria; detected unrepaired cracks require a separate FFS evaluation."])
57
+ def caustic_escalation(x):
58
+ return {"damage_factor": x.base_damage_factor * max(x.years_since_qualifying_inspection, 1) ** 1.1}
59
+
60
+
61
+ class LiningAdjustment(Model):
62
+ base_damage_factor: NonNegative
63
+ condition: Literal["good", "average", "poor"]
64
+ effective_online_monitoring: bool
65
+
66
+
67
+ @calculation("damage/lining-adjust", "Lining condition and monitoring adjustment", LiningAdjustment,
68
+ ["Part 2 eq (2.24), Table 5.6; PDF pp167,171"],
69
+ limitations=["Base DF supplied by caller; lining-type/age Tables 5.4/5.5 not selected automatically."])
70
+ def lining_adjust(x):
71
+ condition = {"good": 1, "average": 2, "poor": 10}[x.condition]
72
+ monitoring = .1 if x.effective_online_monitoring else 1
73
+ return {"condition_factor": condition, "monitoring_factor": monitoring,
74
+ "lining_damage_factor": x.base_damage_factor * condition * monitoring}