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,128 @@
1
+ """2016 high-temperature H2S/H2 corrosion source tables and thinning."""
2
+ from bisect import bisect_left
3
+ from typing import Literal
4
+ from pydantic import Field
5
+ from .core import Model, NonNegative, Positive, Fraction, calculation, ensure
6
+ from ._h2sh2_tables import TABLES
7
+ from .thinning import Posterior, WallLoss
8
+ from .clad_thinning import PressureStrength, CodeStrength
9
+ from .thinning_conditions import ThinningConditions, QualifiedUnclad, qualified_unclad
10
+
11
+ Material=Literal['carbon_steel','cr1_0p2mo','cr1_0p5mo','cr1p25_0p5mo','cr2p25_1mo','cr3_1mo',
12
+ 'cr5_0p5mo','cr7','cr9_1mo','cr12','ss_304','ss_304l','ss_316','ss_316l','ss_321','ss_347']
13
+ Units=Literal['printed_si','printed_us_converted']
14
+ _CONCENTRATIONS=tuple(c/100 for c in (.002,.0035,.008,.035,.08,.30,.75,1.))
15
+
16
+
17
+ def _group(material):
18
+ if material.startswith('ss_'): return 'stainless'
19
+ return {'cr5_0p5mo':'cr5','cr7':'cr7','cr9_1mo':'cr9','cr12':'cr12'}.get(material,'carbon_low_alloy')
20
+
21
+
22
+ class H2SH2Axes(Model):
23
+ material: Material
24
+ unit_basis: Units
25
+
26
+
27
+ @calculation('damage/h2sh2-table-axes','High-temperature H2S/H2 material and table axes',H2SH2Axes,
28
+ ['Part2 Tables2.B.4.2-4.7M; PDF pp354-362'],limitations=[
29
+ 'Six source material groups,16named materials. Public vapor H2S mole fraction, not molepercent/massfraction/partial pressure.0.002mole%=0.00002fraction=20ppmv. SI218..524C and US425..975F nodes retain source rounding. Stream type required onlyCS/lowCr groups, excluded for12Cr and named stainless. Axes not process qualification; no unlisted alloy interpolation.'])
30
+ def table_axes(x):
31
+ group=_group(x.material)
32
+ temps=tuple(t+273.15 for t in (218,246,274,302,329,357,385,413,441,468,496,524)) if x.unit_basis=='printed_si' else tuple((t-32)*5/9+273.15 for t in (425,475,525,575,625,675,725,775,825,875,925,975))
33
+ return {'h2s_mole_fraction_nodes':list(_CONCENTRATIONS),'temperature_nodes_k':list(temps),'table_group':group,
34
+ 'hydrocarbon_curve_required':group not in ('cr12','stainless'),'source_table_id':'2.B.4.'+str(('carbon_low_alloy','cr5','cr7','cr9','cr12','stainless').index(group)+2)+('M' if x.unit_basis=='printed_si' else '')}
35
+
36
+
37
+ def _bracket(nodes,value):
38
+ # Fraction endpoints need a tighter tolerance than temperature coordinates.
39
+ tolerance=1e-14 if nodes[-1]<=1 else 1e-10
40
+ value=next((n for n in nodes if abs(n-value)<=tolerance),value)
41
+ ensure(nodes[0]<=value<=nodes[-1],'Outside high-temperature H2S/H2 table; no extrapolation')
42
+ i=bisect_left(nodes,value)
43
+ return (i,i,0.) if value==nodes[i] else (i-1,i,(value-nodes[i-1])/(nodes[i]-nodes[i-1]))
44
+
45
+
46
+ class H2SH2Rate(H2SH2Axes):
47
+ h2s_mole_fraction: Fraction
48
+ maximum_temperature_k: Positive
49
+ stream_case: Literal['naphtha','gas_oil','hydrocarbon_free'] | None=None
50
+ hydrocarbon_free_curve: Literal['naphtha','gas_oil'] | None=None
51
+ hydrocarbon_free_qualification_basis: str | None=None
52
+ lookup_policy: Literal['exact_nodes_only','bilinear','maximum_bracketing_value']
53
+
54
+
55
+ @calculation('damage/h2sh2-table-rate','High-temperature H2S/H2 concentration-temperature corrosion rate',H2SH2Rate,
56
+ ['Part2 Annex2.B §4.1-4.3,Tables4.2-4.7M,Figure4.1; PDF pp353-363'],limitations=[
57
+ '1920physical SI/US cells. Naphtha/gas-oil curves categorical, no fluid blending. Typical boiling430F distinction requires process classification, not automatic maximum-temperature comparison. Hydrocarbon-free source says same correlations adequate despite possible50% greater rates: explicit qualified choice of source curve, no added1.5multiplier.12Cr/stainless tables have no hydrocarbon branch:unused stream fields rejected. Positive H2S below20ppmv or above1mole% and temperatures outside printed table rejected, never zero/cap. Approx204C onset is not first table218C: gap not extrapolated. Bilinear uses linear concentration, not inferred log correlation; explicit numerical policy. No HTHA,wetH2S cracking,catalyst chemistry or automatic material certification.'])
58
+ def table_rate(x):
59
+ axes=table_axes(x)
60
+ branch=axes['hydrocarbon_curve_required']
61
+ if not branch:
62
+ ensure(x.stream_case is None and x.hydrocarbon_free_curve is None and x.hydrocarbon_free_qualification_basis is None,'12Cr/stainless table cannot use stream curve inputs')
63
+ curve=None
64
+ elif x.stream_case=='hydrocarbon_free':
65
+ ensure(x.hydrocarbon_free_curve is not None and x.hydrocarbon_free_qualification_basis is not None and bool(x.hydrocarbon_free_qualification_basis.strip()),'Hydrocarbon-free service needs qualified source curve selection')
66
+ curve=x.hydrocarbon_free_curve
67
+ else:
68
+ ensure(x.stream_case in ('naphtha','gas_oil') and x.hydrocarbon_free_curve is None and x.hydrocarbon_free_qualification_basis is None,'LowCr source table needs only applicable naphtha/gas-oil curve')
69
+ curve=x.stream_case
70
+ a,b,w=_bracket(axes['h2s_mole_fraction_nodes'],x.h2s_mole_fraction)
71
+ i,j,v=_bracket(axes['temperature_nodes_k'],x.maximum_temperature_k)
72
+ ensure(x.lookup_policy!='exact_nodes_only' or (a==b and i==j),'Exact H2S/H2 policy does not interpolate')
73
+ data=TABLES[axes['table_group']+'_'+x.unit_basis]
74
+ def cell(c,t): return data[2*c+int(curve=='gas_oil') if branch else c][t]
75
+ native=max(cell(c,t) for c in (a,b) for t in (i,j)) if x.lookup_policy=='maximum_bracketing_value' else (1-w)*((1-v)*cell(a,i)+v*cell(a,j))+w*((1-v)*cell(b,i)+v*cell(b,j))
76
+ return {'corrosion_rate_m_year':native*(.001 if x.unit_basis=='printed_si' else .0000254),'source_table_id':axes['source_table_id'],
77
+ 'selected_hydrocarbon_curve':curve,'hydrocarbon_free_curve_selection_used':x.stream_case=='hydrocarbon_free','source_thinning_type':'general'}
78
+
79
+
80
+ class H2SH2Service(H2SH2Rate):
81
+ hydrogen_sulfidation_environment_qualified: bool
82
+ material_and_stream_qualification_basis: str=Field(min_length=1)
83
+
84
+
85
+ class H2SH2Assessment(Model):
86
+ h2s_h2_service_present: bool
87
+ applicability_basis: str=Field(min_length=1)
88
+ service_model: H2SH2Service | None=None
89
+
90
+
91
+ @calculation('damage/h2sh2-corrosion-assessment','Qualified high-temperature H2S/H2 corrosion assessment',H2SH2Assessment,
92
+ ['Part2 Annex2.B §4,Table1.2; PDF pp323,353-363'],origin='api581_composition_with_explicit_source_policies',limitations=[
93
+ 'Requires actual high-temperature hydrogen-sulfidation environment and material/stream qualification; sulfur and hydrogen alone do not establish catalytic conversion. Same source rate applies with no extra50% vapor correction. No-service0 is mechanism-specific with absent diagnostics, not all-mechanismDF0 or temperature-based immunity. Actual maximum process temperature and vapor composition external; low-temperature gap,traceH2S or unlisted material not filled by unrelated wet-sour/oxidation model. General source thinning does not validate inspection distribution.'])
94
+ def assessment(x):
95
+ ensure(bool(x.applicability_basis.strip()),'Environment and service applicability basis required')
96
+ if not x.h2s_h2_service_present:
97
+ ensure(x.service_model is None,'No-H2S/H2 branch cannot carry unused source model')
98
+ return {'corrosion_rate_m_year':0.,'source_table_id':None,'selected_hydrocarbon_curve':None,'hydrocarbon_free_curve_selection_used':False,
99
+ 'source_thinning_type':None,'calculation_branch':'no_h2s_h2_service'}
100
+ ensure(x.service_model is not None,'Exposed high-temperature H2S/H2 service requires model')
101
+ m=x.service_model
102
+ ensure(m.hydrogen_sulfidation_environment_qualified and bool(m.material_and_stream_qualification_basis.strip()),'Qualified hydrogen-sulfidation environment/material/stream required')
103
+ return {**table_rate(m),'calculation_branch':'qualified_high_temperature_h2s_h2'}
104
+
105
+
106
+ class H2SH2Thinning(Model):
107
+ corrosion: H2SH2Assessment
108
+ assessment_distribution: Literal['general','local']
109
+ distribution_qualification_basis: str=Field(min_length=1)
110
+ starting_thickness_m: Positive
111
+ elapsed_years: NonNegative
112
+ strength: PressureStrength | CodeStrength=Field(discriminator='method')
113
+ inspections: Posterior
114
+ conditions: ThinningConditions
115
+ qualification_basis: str=Field(min_length=1)
116
+
117
+
118
+ @calculation('damage/h2sh2-thinning-assessment','High-temperature H2S/H2 rate through qualified unclad thinning DF',H2SH2Thinning,
119
+ ['Part2 Annex2.B §4,Table1.2; §4.5.7,Table4.8'],origin='api581_composition_with_explicit_source_policies',limitations=[
120
+ 'Same computed rate drives wall loss and dedicated high-temperatureH2S/H2 monitoring row, not amine/wet-sour row. Actual distribution/inspection coverage separately qualified. No-service0 retains generalDF floor/uncertainty. Non-AST unclad only; no wetH2S/HTHA or other mechanism aggregation,code/FFS acceptance or added vapor multiplier.'])
121
+ def thinning_assessment(x):
122
+ ensure(bool(x.distribution_qualification_basis.strip()),'Actual distribution and inspection qualification required')
123
+ corrosion=assessment(x.corrosion)
124
+ ensure(x.conditions.monitoring.row.mechanism=='high_temperature_h2s_h2','H2S/H2 source requires dedicated high-temperature monitoring row')
125
+ result=qualified_unclad(QualifiedUnclad(assessment_scope='non_ast_unclad',qualification_basis=x.qualification_basis,
126
+ wall=WallLoss(starting_thickness_m=x.starting_thickness_m,corrosion_rate_m_year=corrosion['corrosion_rate_m_year'],elapsed_years=x.elapsed_years),
127
+ strength=x.strength,inspections=x.inspections,conditions=x.conditions))
128
+ return {**corrosion,**result,'assessment_distribution':x.assessment_distribution}
api581/hic_screened.py ADDED
@@ -0,0 +1,89 @@
1
+ """Qualified carbon-steel HIC/SOHIC wet-H2S composition."""
2
+ from typing import Literal
3
+ from pydantic import Field
4
+ from .core import Model, NonNegative, Positive, calculation, ensure
5
+ from .ssc_screening import SscScreen, hic_h2s_screening
6
+ from .scc_mechanisms import (WetH2sEnvironment, wet_h2s_environment, HicH2sMaterial,
7
+ hic_h2s_susceptibility, HicAssessment, InspectionCounts, assess)
8
+
9
+
10
+ class HicQualifiedService(Model):
11
+ environment: WetH2sEnvironment
12
+ steel_sulfur_weight_percent: NonNegative = Field(le=100)
13
+ product_form: Literal['plate_or_welded','seamless_or_extruded_pipe']
14
+ pwht: bool
15
+ confirmed_cracking: bool
16
+ service_basis: str = Field(min_length=1)
17
+
18
+
19
+ @calculation('damage/hic-h2s-qualified-susceptibility','HIC/SOHIC-H2S environment/material/cyanide susceptibility',HicQualifiedService,
20
+ ['Part2 §9.6.4 STEP1-2,Tables9.2-9.3; PDF pp190,193'],limitations=[
21
+ 'Carbon-steel Table9.3 applicability qualified externally. Sulfur input is weight percent, not mass fraction. Confirmed cracking forces environmental severity high under STEP1 and final susceptibility high under section9.5; measured environment remains diagnostic. Cyanide note upgrades subsequent susceptibility one category capped high, not environment. Existing pH gaps/1000ppm overlap fail. No sulfur inference from steel grade/year or automatic low-alloy table applicability.'])
22
+ def hic_qualified_susceptibility(x):
23
+ ensure(bool(x.service_basis.strip()),'Carbon-steel chemistry/product/PWHT applicability basis required')
24
+ environment=wet_h2s_environment(x.environment)
25
+ effective='high' if x.confirmed_cracking else environment['environmental_severity']
26
+ base=hic_h2s_susceptibility(HicH2sMaterial(steel_sulfur_weight_percent=x.steel_sulfur_weight_percent,
27
+ product_form=x.product_form,pwht=x.pwht,environmental_severity=effective))['susceptibility']
28
+ levels=('none','low','medium','high')
29
+ adjusted=levels[min(3,levels.index(base)+int(environment['susceptibility_upgrade_required']))]
30
+ return {**environment,'effective_environmental_severity':effective,'unadjusted_susceptibility':base,
31
+ 'cyanide_adjusted_susceptibility':adjusted,'susceptibility':'high' if x.confirmed_cracking else adjusted}
32
+
33
+
34
+ class HicMonitoringSelection(Model):
35
+ monitoring: Literal['none','process_variables','hydrogen_probes','both']
36
+ experience_factor: Positive | None = None
37
+ experience_basis: str | None = None
38
+
39
+
40
+ @calculation('damage/hic-h2s-monitoring-selection','Table9.5 or qualified experience replacement monitoring factor',HicMonitoringSelection,
41
+ ['Part2 Table9.5 note; PDF p193'],limitations=[
42
+ 'Table factors1/2/2/4 or one positive finite experience-based replacement with nonblank basis. Replacement is not multiplied by the table factor. Source gives no numeric range for experience substitutes; no automatic clamp or empirical calibration. Replacement requires an active monitoring method; no credit inferred for absent monitoring. Physical program effectiveness remains owner-qualified.'])
43
+ def hic_monitoring_selection(x):
44
+ table={'none':1.,'process_variables':2.,'hydrogen_probes':2.,'both':4.}[x.monitoring]
45
+ custom=x.experience_factor is not None
46
+ ensure((x.experience_basis is not None)==custom,'Experience factor and basis must be supplied together')
47
+ if custom:
48
+ ensure(x.monitoring!='none' and bool(x.experience_basis.strip()),'Experience replacement requires active monitoring and qualified basis')
49
+ return {'table_monitoring_factor':table,'monitoring_factor':x.experience_factor if custom else table,
50
+ 'monitoring_factor_source':'qualified_experience_replacement' if custom else 'table9_5'}
51
+
52
+
53
+ class HicQualifiedAssessment(Model):
54
+ screening: SscScreen
55
+ service: HicQualifiedService
56
+ known_blisters_or_cracks: bool
57
+ ffs_disposition: Literal['not_required','repaired','qualified_ffs_basis']
58
+ ffs_basis: str | None = None
59
+ years_since_qualifying_inspection: NonNegative
60
+ inspections: InspectionCounts
61
+ monitoring: Literal['none','process_variables','hydrogen_probes','both']
62
+ experience_factor: Positive | None = None
63
+ experience_basis: str | None = None
64
+ inspection_monitoring_basis: str = Field(min_length=1)
65
+
66
+
67
+ @calculation('damage/hic-h2s-qualified-assessment','Screened HIC/SOHIC-H2S service through monitoring-adjusted DF',HicQualifiedAssessment,
68
+ ['Part2 §§9.3,9.5-9.6,Tables9.2-9.5,eq(2.28); PDF pp189-193'],
69
+ origin='qualified_hic_h2s_service_material_monitoring_to_df',limitations=[
70
+ 'Carbon-steel integration only, despite broader carbon/low-alloy screening. Known blisters/cracks require explicit repaired or externally qualified FFS disposition and basis; this API performs no FFS. Confirmed cracking must also be declared known. Inspection and age follow section9.6.4, not generic thinning periods. Monitoring is applied once using existing factors1/2/2/4; qualification is mandatory. Existing six-equivalent-inspection and source table guards retained. No whole-asset risk or automatic damage detection.'])
71
+ def hic_qualified_assessment(x):
72
+ ensure(not x.service.confirmed_cracking or x.known_blisters_or_cracks,'Confirmed cracking requires known damage declaration')
73
+ if x.known_blisters_or_cracks:
74
+ ensure(x.ffs_disposition!='not_required' and x.ffs_basis is not None and bool(x.ffs_basis.strip()),
75
+ 'Known blisters/cracks require repaired or externally qualified FFS disposition','ffs_required')
76
+ else:
77
+ ensure(x.ffs_disposition=='not_required' and x.ffs_basis is None,'FFS disposition is unused without known damage')
78
+ ensure(bool(x.inspection_monitoring_basis.strip()),'HIC/SOHIC-specific inspection age and monitoring qualification required')
79
+ screen=hic_h2s_screening(x.screening)
80
+ ensure(screen['evaluate_hic_sohic_h2s'],'HIC/SOHIC wet-H2S screen not met','hic_outside_screen')
81
+ ensure(x.screening.material=='carbon_steel','Table9.3 integration requires qualified carbon steel','unsupported_hic_material')
82
+ service=hic_qualified_susceptibility(x.service)
83
+ monitoring=hic_monitoring_selection(HicMonitoringSelection(monitoring=x.monitoring,
84
+ experience_factor=x.experience_factor,experience_basis=x.experience_basis))
85
+ result=assess('hic-sohic-h2s',HicAssessment(susceptibility=service['susceptibility'],confirmed_cracking=x.service.confirmed_cracking,
86
+ unrepaired_cracks=False,years_since_qualifying_inspection=x.years_since_qualifying_inspection,
87
+ inspections=x.inspections,monitoring='none'))
88
+ result['damage_factor']=result['base_damage_factor']*result['age_factor']/monitoring['monitoring_factor']
89
+ return {**screen,**service,**result,**monitoring,'ffs_disposition':x.ffs_disposition}
api581/htha.py ADDED
@@ -0,0 +1,45 @@
1
+ """HTHA partial-pressure, bounded screening and damage table."""
2
+ from typing import Literal
3
+ from .core import Model, Positive, NonNegative, Fraction, calculation, ensure
4
+
5
+
6
+ class HydrogenPressure(Model):
7
+ total_pressure_pa_abs: Positive
8
+ hydrogen_mole_fraction: Fraction
9
+
10
+
11
+ @calculation("properties/hydrogen-partial-pressure", "Hydrogen ideal-mixture partial pressure", HydrogenPressure,
12
+ ["Part 2 Table 19.1; PDF p259"], limitations=["Mole fraction times total absolute pressure; not a fugacity model."])
13
+ def partial_pressure(x):
14
+ return {"hydrogen_partial_pressure_pa_abs":x.total_pressure_pa_abs*x.hydrogen_mole_fraction}
15
+
16
+
17
+ class HthaDamage(Model):
18
+ susceptibility: Literal["damage_observed","high","medium","low","none"]
19
+
20
+
21
+ @calculation("damage/htha-factor", "HTHA susceptibility damage factor", HthaDamage,
22
+ ["Part 2 Table 19.2; PDF p260"], limitations=["Qualified susceptibility supplied; historical exposure and pre-1970 fabrication may require more conservative owner review."])
23
+ def htha_factor(x):
24
+ return {"damage_factor":{"damage_observed":5000,"high":5000,"medium":2000,"low":100,"none":0}[x.susceptibility]}
25
+
26
+
27
+ class CarbonHtha(Model):
28
+ material_group: Literal["carbon_steel","c_half_mo"]
29
+ exposure_temperature_k: Positive
30
+ hydrogen_partial_pressure_pa_abs: NonNegative
31
+ damage_history: Literal["none","observed_not_replaced","replaced_in_kind","upgraded_material"]
32
+
33
+
34
+ @calculation("damage/htha-carbon-screen", "Carbon/C-half-Mo HTHA screening and factor", CarbonHtha,
35
+ ["Part 2 §19.6.3 STEP 1-4; PDF pp258-260"],
36
+ limitations=["Current material must match material_group. Uses printed SI 177 C and 0.345 MPa thresholds. Mixed high/low temperature-pressure cases are not assigned by the printed carbon-steel rules and require engineering review. Cr-Mo/API 941 curve screening is separate."])
37
+ def carbon_htha(x):
38
+ if x.damage_history=="observed_not_replaced": susceptibility="damage_observed"
39
+ elif x.damage_history=="replaced_in_kind": susceptibility="high"
40
+ else:
41
+ hot=x.exposure_temperature_k>450.15
42
+ high_pressure=x.hydrogen_partial_pressure_pa_abs>345000
43
+ ensure(hot==high_pressure,"Mixed HTHA temperature/pressure threshold case requires engineering review", "unresolved_applicability")
44
+ susceptibility="high" if hot else "none"
45
+ return {"susceptibility":susceptibility,**htha_factor(HthaDamage(susceptibility=susceptibility))}
@@ -0,0 +1,272 @@
1
+ """2016 dilute HCl tables, chemistry conversion and qualified thinning composition."""
2
+ from bisect import bisect_left
3
+ from typing import Literal
4
+ from pydantic import Field
5
+ from .core import Model, NonNegative, Positive, Fraction, calculation, ensure
6
+ from .thinning import Posterior, WallLoss
7
+ from .clad_thinning import PressureStrength, CodeStrength
8
+ from .thinning_conditions import ThinningConditions, QualifiedUnclad, qualified_unclad
9
+
10
+ Units = Literal["printed_si", "printed_us_converted"]
11
+ Lookup = Literal["exact_nodes_only", "bilinear", "maximum_bracketing_value"]
12
+ PHMaterial = Literal["carbon_steel", "ss_304", "ss_316", "ss_321", "ss_347"]
13
+ Alloy = Literal["alloy_825", "alloy_20", "alloy_625", "alloy_c276", "alloy_b2", "alloy_400"]
14
+ _PH = (.5,.8,1.25,1.75,2.25,2.75,3.25,3.75,4.25,4.75,5.25,5.75,6.25,6.8)
15
+ _CONCENTRATION = (.005,.0075,.01)
16
+ # ppm intervals are deliberately not made contiguous; the printed integer gaps remain.
17
+ _BANDS = ((0.,1.,5.),(1.,2.,4.5),(3.,5.,4.),(6.,15.,3.5),(16.,35.,3.),
18
+ (36.,120.,2.5),(121.,360.,2.),(361.,1200.,1.5),(1201.,3600.,1.),(3601.,12000.,.5))
19
+ _CS_US = ((999,999,999,999),(900,999,999,999),(400,999,999,999),(200,700,999,999),
20
+ (100,300,400,560),(60,130,200,280),(40,70,100,140),(30,50,90,125),(20,40,70,100),
21
+ (10,30,50,70),(7,20,30,40),(4,15,20,30),(3,10,15,20),(2,5,7,10))
22
+ _CS_SI = ((25.37,25.37,25.37,25.37),(22.86,25.37,25.37,25.37),(10.16,25.37,25.37,25.37),
23
+ (5.08,17.78,25.37,25.37),(2.54,7.62,10.16,14.22),(1.52,3.30,5.08,7.11),(1.02,1.78,2.54,3.56),
24
+ (.76,1.27,2.29,3.18),(.51,1.02,1.78,2.54),(.25,.76,1.27,1.78),(.18,.51,.76,1.02),
25
+ (.10,.38,.51,.76),(.08,.25,.38,.51),(.05,.13,.18,.25))
26
+ _SS_US = ((900,999,999,999),(500,999,999,999),(300,500,700,999),(150,260,400,500),
27
+ (80,140,200,250),(50,70,100,120),(30,40,50,65),(20,25,30,35),(10,15,20,25),
28
+ (5,7,10,12),(4,5,6,7),(3,4,5,6),(2,3,4,5),(1,2,3,4))
29
+ _SS_SI = ((22.86,25.37,25.37,25.37),(12.70,25.37,25.37,25.37),(7.62,12.70,17.78,25.37),
30
+ (3.81,6.60,10.16,12.70),(2.03,3.56,5.08,6.35),(1.27,1.78,2.54,3.05),(.76,1.02,1.27,1.65),
31
+ (.51,.64,.76,.89),(.25,.38,.51,.64),(.13,.18,.25,.30),(.10,.13,.15,.18),
32
+ (.08,.10,.13,.15),(.05,.08,.10,.13),(.03,.05,.08,.10))
33
+ _HIGH_US = {
34
+ "825_20":((1,3,40,200),(2,5,80,400),(10,70,300,999)),
35
+ "625":((1,2,15,75),(1,5,25,125),(2,70,200,400)),
36
+ "c276":((1,2,8,30),(1,2,15,75),(2,10,60,300)),
37
+ "b2_no":((1,1,2,4),(1,1,5,20),(2,5,10,25)),
38
+ "b2_yes":((4,4,8,16),(4,4,20,80),(8,20,40,100)),
39
+ "400_no":((1,3,30,300),(2,5,80,800),(19,25,150,900)),
40
+ "400_yes":((4,12,120,999),(10,20,320,999),(40,100,600,999))}
41
+ _HIGH_SI = {
42
+ "825_20":((.03,.08,1.02,5.08),(.05,.13,2.03,10.16),(.25,1.78,7.62,25.37)),
43
+ "625":((.03,.05,.38,1.91),(.03,.13,.64,3.18),(.05,1.78,5.08,10.16)),
44
+ "c276":((.03,.05,.20,.76),(.03,.05,.38,1.91),(.05,.25,1.52,7.62)),
45
+ "b2_no":((.03,.03,.05,.1),(.03,.03,.13,.51),(.05,.13,.25,.64)),
46
+ "b2_yes":((.1,.1,.2,.41),(.1,.1,.51,2.03),(.2,.51,1.02,2.54)),
47
+ "400_no":((.03,.08,.76,7.62),(.05,.13,2.03,20.32),(.48,.64,3.81,22.86)),
48
+ "400_yes":((.1,.3,3.05,25.37),(.25,.51,8.13,25.37),(1.02,2.54,15.24,25.37))}
49
+
50
+
51
+ def _qualified(text, message):
52
+ ensure(bool(text.strip()), message)
53
+
54
+
55
+ class HClPHFromChloride(Model):
56
+ chloride_mass_fraction: Fraction
57
+ no_alkaline_agents_qualified: bool
58
+ qualification_basis: str = Field(min_length=1)
59
+
60
+
61
+ @calculation("damage/hcl-ph-from-chloride", "Dilute HCl pH estimate from printed chloride concentration bands", HClPHFromChloride,
62
+ ["Part2 Table2.B.2.2; PDF p325"], limitations=[
63
+ "Mass fraction converted to wppm. Assumes no ammonia, neutralizing amines or caustic: explicit qualification required. Literal bands contain continuous gaps, e.g.2..3 and35..36ppm; gaps and>12000ppm rejected without rounding or band extension. This is table approximation for separated acid phase, not acid equilibrium, HCl wt% or measured pH."])
64
+ def ph_from_chloride(x):
65
+ ensure(x.no_alkaline_agents_qualified,"Source pH estimate requires absence of alkaline agents")
66
+ _qualified(x.qualification_basis,"Chloride/neutralization qualification required")
67
+ ppm = x.chloride_mass_fraction*1e6
68
+ # Snap only floating-point roundoff at a printed endpoint, not analytical uncertainty.
69
+ ppm = next((n for lo,hi,_ in _BANDS for n in (lo,hi) if abs(ppm-n) <= 1e-10),ppm)
70
+ for lo,hi,ph in _BANDS:
71
+ if lo <= ppm and (ppm < hi if ph == 5 else ppm <= hi):
72
+ return {"estimated_ph":ph,"chloride_band_lower_wppm":lo,"chloride_band_upper_wppm":hi,"chloride_band_upper_inclusive":ph != 5}
73
+ ensure(False,"Chloride falls in unassigned source band gap or exceeds table","hcl_chloride_band_unassigned")
74
+
75
+
76
+ class HClChlorideInterval(Model):
77
+ ph: float = Field(ge=.5,le=5,strict=True,allow_inf_nan=False)
78
+ no_alkaline_agents_qualified: bool
79
+ qualification_basis: str = Field(min_length=1)
80
+
81
+
82
+ @calculation("damage/hcl-chloride-interval-from-ph", "Printed chloride interval corresponding to a tabulated HCl pH", HClChlorideInterval,
83
+ ["Part2 Table2.B.2.2,Figure2.B.2.1; PDF pp325,330"], limitations=[
84
+ "Inverse table is an interval, not unique concentration. Only printed pH values0.5,1,...5 allowed; no log-law, midpoint or invented interpolation. Requires no alkaline agents. High-alloy use must explicitly select/qualify concentration inside interval and rate-table range; lower-pH/high-chloride assumptions cannot be inferred from unrelated aqueous chemistry."])
85
+ def chloride_interval(x):
86
+ ensure(x.no_alkaline_agents_qualified,"Source inverse estimate requires absence of alkaline agents")
87
+ _qualified(x.qualification_basis,"Inverse chemistry qualification required")
88
+ for lo,hi,ph in _BANDS:
89
+ if x.ph == ph:
90
+ return {"chloride_band_lower_wppm":lo,"chloride_band_upper_wppm":hi,"chloride_band_upper_inclusive":ph != 5}
91
+ ensure(False,"No inverse chloride band for this pH","hcl_ph_inverse_unassigned")
92
+
93
+
94
+ def _bracket(nodes,value):
95
+ value = next((n for n in nodes if abs(n-value) <= 1e-10),value)
96
+ ensure(nodes[0] <= value <= nodes[-1],"Outside printed HCl rate table; extrapolation disabled")
97
+ i = bisect_left(nodes,value)
98
+ return (i,i,0.) if value == nodes[i] else (i-1,i,(value-nodes[i-1])/(nodes[i]-nodes[i-1]))
99
+
100
+
101
+ def _lookup(table,us_table,nodes,value,temp,units,policy):
102
+ temperatures = tuple(c+273.15 for c in (38,52,79,93)) if units == "printed_si" else tuple((f-32)*5/9+273.15 for f in (100,125,175,200))
103
+ a,b,w = _bracket(nodes,value)
104
+ c,d,v = _bracket(temperatures,temp)
105
+ ensure(policy != "exact_nodes_only" or (a == b and c == d),"Exact-node policy does not interpolate")
106
+ corners = [table[i][j] for i in (a,b) for j in (c,d)]
107
+ native = max(corners) if policy == "maximum_bracketing_value" else (1-w)*((1-v)*table[a][c]+v*table[a][d])+w*((1-v)*table[b][c]+v*table[b][d])
108
+ return {"corrosion_rate_m_year":native*(.001 if units == "printed_si" else .0000254),
109
+ "source_999_corner_used":any(us_table[i][j] == 999 for i in (a,b) for j in (c,d))}
110
+
111
+
112
+ class HClPHRate(Model):
113
+ material: PHMaterial
114
+ ph: float = Field(ge=.5,le=6.8,strict=True,allow_inf_nan=False)
115
+ maximum_temperature_k: Positive
116
+ unit_basis: Units
117
+ lookup_policy: Lookup
118
+
119
+
120
+ @calculation("damage/hcl-ph-rate", "HCl carbon-steel and 304/316/321/347 pH-temperature rate tables", HClPHRate,
121
+ ["Part2 Tables2.B.2.3/2.3M,2.4/2.4M; PDF pp326-327"], limitations=[
122
+ "14pH x4temperature x2materials x2unit tables,224physical cells. Named four stainless grades share source table; other300 grades/low-alloy not inferred. Stainless values already10x general for localized pitting: do not multiply10 again or divide to claim qualified general rate. Printed999mpy/25.37mm/y retained literally and flagged for any contributing cell; not infinity, missing or immunity. Explicit interpolation policy only; no extrapolation. Unit tables preserve rounding. SCC/crevice corrosion not calculated."])
123
+ def ph_rate(x):
124
+ cs = x.material == "carbon_steel"
125
+ us = _CS_US if cs else _SS_US
126
+ table = (_CS_SI if cs else _SS_SI) if x.unit_basis == "printed_si" else us
127
+ return {**_lookup(table,us,_PH,x.ph,x.maximum_temperature_k,x.unit_basis,x.lookup_policy),
128
+ "localized_pitting_already_included":not cs,"source_table_id":"2.B.2."+("3" if cs else "4")+("M" if x.unit_basis == "printed_si" else "")}
129
+
130
+
131
+ class HClAlloyRate(Model):
132
+ material: Alloy
133
+ chloride_mass_fraction: Fraction
134
+ maximum_temperature_k: Positive
135
+ oxidants_present: bool | None = None
136
+ unit_basis: Units
137
+ lookup_policy: Lookup
138
+
139
+
140
+ @calculation("damage/hcl-alloy-rate", "HCl high-alloy chloride-temperature and oxidant rate tables", HClAlloyRate,
141
+ ["Part2 Tables2.B.2.5/2.5M,2.6/2.6M; PDF pp328-329"], limitations=[
142
+ "Chloride-ion mass fraction .005/.0075/.01 (0.5/0.75/1wt%), not HCl acid fraction.825/20 share table;625/C276 distinct. B2/400 require explicit oxidant state; supplied oxidant field on other table rejected as unused. All168physical SI/US high-alloy cells preserved, including400 no-oxidant1wt%,100F=19mpy. No universal4x oxidant multiplier. Source999 literal/rounding retained and flagged. No extrapolation or oxidant interpolation; heat-treatment, salt deposits and actual process qualification external."])
143
+ def alloy_rate(x):
144
+ affected = x.material in ("alloy_b2","alloy_400")
145
+ ensure((x.oxidants_present is not None) if affected else x.oxidants_present is None,"B2/400 require oxidant state; other table cannot use oxidant input")
146
+ key = "825_20" if x.material in ("alloy_825","alloy_20") else x.material.removeprefix("alloy_")
147
+ if affected:
148
+ key += "_yes" if x.oxidants_present else "_no"
149
+ us = _HIGH_US[key]
150
+ table = _HIGH_SI[key] if x.unit_basis == "printed_si" else us
151
+ return {**_lookup(table,us,_CONCENTRATION,x.chloride_mass_fraction,x.maximum_temperature_k,x.unit_basis,x.lookup_policy),
152
+ "localized_pitting_already_included":False,"source_table_id":"2.B.2."+("6" if affected else "5")+("M" if x.unit_basis == "printed_si" else "")}
153
+
154
+
155
+ class MeasuredHClPH(Model):
156
+ method: Literal["supplied_ph"]
157
+ ph: float = Field(ge=.5,le=6.8,strict=True,allow_inf_nan=False)
158
+ qualification_basis: str = Field(min_length=1)
159
+
160
+
161
+ class EstimatedHClPH(HClPHFromChloride):
162
+ method: Literal["chloride_table"]
163
+
164
+
165
+ class SuppliedHClChloride(Model):
166
+ method: Literal["supplied_chloride"]
167
+ chloride_mass_fraction: Fraction
168
+ qualification_basis: str = Field(min_length=1)
169
+
170
+
171
+ class SelectedHClChloride(HClChlorideInterval):
172
+ method: Literal["ph_interval_selection"]
173
+ chloride_mass_fraction: Fraction
174
+ selection_qualification_basis: str = Field(min_length=1)
175
+
176
+
177
+ class HClPHWet(Model):
178
+ model_family: Literal["ph_table"]
179
+ material: PHMaterial
180
+ acidity: MeasuredHClPH | EstimatedHClPH = Field(discriminator="method")
181
+ maximum_temperature_k: Positive
182
+ unit_basis: Units
183
+ lookup_policy: Lookup
184
+ qualification_basis: str = Field(min_length=1)
185
+
186
+
187
+ class HClAlloyWet(Model):
188
+ model_family: Literal["alloy_table"]
189
+ material: Alloy
190
+ chloride: SuppliedHClChloride | SelectedHClChloride = Field(discriminator="method")
191
+ maximum_temperature_k: Positive
192
+ oxidants_present: bool | None = None
193
+ material_heat_treatment_qualified: bool
194
+ unit_basis: Units
195
+ lookup_policy: Lookup
196
+ qualification_basis: str = Field(min_length=1)
197
+
198
+
199
+ class HClAssessment(Model):
200
+ free_water_present: bool
201
+ dilute_hcl_service_qualified: bool
202
+ applicability_basis: str = Field(min_length=1)
203
+ wet_model: HClPHWet | HClAlloyWet | None = Field(default=None,discriminator="model_family")
204
+
205
+
206
+ @calculation("damage/hcl-assessment", "Qualified dilute HCl chemistry material and corrosion-rate assessment", HClAssessment,
207
+ ["Part2 Annex2.B §2,Tables2.1-2.6M,Figure2.1; Table1.2; PDF pp323-330"],origin="api581_composition_with_explicit_source_policies",limitations=[
208
+ "Only qualified dilute free-water HCl service and listed materials. Supplied separated acid-phase pH preferred; chloride-derived pH gated on no alkaline agents. Inverse pH route returns band and requires explicitly selected qualified chloride inside it; no unique inverse inferred. Maximum local temperature and material/heat-treatment applicability qualified externally. Source class local, stainless pitting already included. Dry0 HCl-specific, diagnosticsNone; not total damage. No shock-condensation/salt deposition dynamics, SCC or concentrated acid extrapolation."])
209
+ def assessment(x):
210
+ _qualified(x.applicability_basis,"Dilute acid phase/applicability qualification required")
211
+ if not x.free_water_present:
212
+ ensure(x.wet_model is None,"Dry HCl branch cannot carry unused wet model")
213
+ return {"corrosion_rate_m_year":0.,"source_999_corner_used":False,"localized_pitting_already_included":False,
214
+ "source_table_id":None,"ph_used":None,"chloride_mass_fraction_used":None,"chemistry_method":None,
215
+ "source_thinning_type":None,"calculation_branch":"no_free_water"}
216
+ ensure(x.dilute_hcl_service_qualified and x.wet_model is not None,"Wet model requires qualified dilute HCl service")
217
+ m = x.wet_model
218
+ _qualified(m.qualification_basis,"Maximum local temperature and material applicability required")
219
+ ph = None
220
+ chloride = None
221
+ if m.model_family == "ph_table":
222
+ c = m.acidity
223
+ if c.method == "supplied_ph":
224
+ _qualified(c.qualification_basis,"Separated acid-phase pH qualification required")
225
+ ph = c.ph
226
+ else:
227
+ ph = ph_from_chloride(c)["estimated_ph"]
228
+ chloride = c.chloride_mass_fraction
229
+ result = ph_rate(HClPHRate(material=m.material,ph=ph,maximum_temperature_k=m.maximum_temperature_k,unit_basis=m.unit_basis,lookup_policy=m.lookup_policy))
230
+ else:
231
+ ensure(m.material_heat_treatment_qualified,"High-alloy material/heat-treatment qualification required")
232
+ c = m.chloride
233
+ chloride = c.chloride_mass_fraction
234
+ if c.method == "supplied_chloride":
235
+ _qualified(c.qualification_basis,"Separated acid-phase chloride qualification required")
236
+ else:
237
+ chloride_interval(c)
238
+ _qualified(c.selection_qualification_basis,"Inverse chloride selection requires explicit engineering basis")
239
+ # Reuse the forward classifier, including exact endpoint tolerance.
240
+ selected = ph_from_chloride(HClPHFromChloride(chloride_mass_fraction=chloride,
241
+ no_alkaline_agents_qualified=c.no_alkaline_agents_qualified,qualification_basis=c.qualification_basis))
242
+ ensure(selected["estimated_ph"] == c.ph,"Selected chloride is outside inverse pH interval")
243
+ ph = c.ph
244
+ result = alloy_rate(HClAlloyRate(material=m.material,chloride_mass_fraction=chloride,
245
+ maximum_temperature_k=m.maximum_temperature_k,oxidants_present=m.oxidants_present,unit_basis=m.unit_basis,lookup_policy=m.lookup_policy))
246
+ return {**result,"ph_used":ph,"chloride_mass_fraction_used":chloride,"chemistry_method":c.method,
247
+ "source_thinning_type":"local","calculation_branch":"wet_dilute_hcl_model"}
248
+
249
+
250
+ class HClThinning(Model):
251
+ corrosion: HClAssessment
252
+ assessment_distribution: Literal["general","local"]
253
+ distribution_qualification_basis: str = Field(min_length=1)
254
+ starting_thickness_m: Positive
255
+ elapsed_years: NonNegative
256
+ strength: PressureStrength | CodeStrength = Field(discriminator="method")
257
+ inspections: Posterior
258
+ conditions: ThinningConditions
259
+ qualification_basis: str = Field(min_length=1)
260
+
261
+
262
+ @calculation("damage/hcl-thinning-assessment", "Dilute HCl rate through qualified non-AST unclad thinning DF", HClThinning,
263
+ ["Part2 Annex2.B §2,Table1.2; §4.5.7,Table4.8"],origin="api581_composition_with_explicit_source_policies",limitations=[
264
+ "Same HCl rate drives wall loss with HCl monitoring row and explicit inspection/distribution qualification. Source local class retained apart from owner assessment distribution. Stainless pitting factor already in table, never added again. Dry0 not DF0; general thinning uncertainty/floor still applies. Non-AST unclad only; no crack/crevice/SCC/FFS or automatic all-mechanism risk."])
265
+ def thinning_assessment(x):
266
+ _qualified(x.distribution_qualification_basis,"Actual distribution and inspection qualification required")
267
+ corrosion = assessment(x.corrosion)
268
+ ensure(x.conditions.monitoring.row.mechanism == "hydrochloric_acid","HCl model requires HCl monitoring row")
269
+ result = qualified_unclad(QualifiedUnclad(assessment_scope="non_ast_unclad",qualification_basis=x.qualification_basis,
270
+ wall=WallLoss(starting_thickness_m=x.starting_thickness_m,corrosion_rate_m_year=corrosion["corrosion_rate_m_year"],elapsed_years=x.elapsed_years),
271
+ strength=x.strength,inspections=x.inspections,conditions=x.conditions))
272
+ return {**corrosion,**result,"assessment_distribution":x.assessment_distribution}