Umar4321 commited on
Commit
bf6888a
·
verified ·
1 Parent(s): 5be89fb

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +431 -0
app.py ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import plotly.graph_objects as go
5
+ from datetime import datetime
6
+ import math
7
+
8
+ # ---------------------------
9
+ # Streamlit ASME VIII App
10
+ # Cleaned, self-contained, ready for deployment to Hugging Face Spaces
11
+ # Requirements: streamlit, pandas, numpy, plotly
12
+ # ---------------------------
13
+
14
+ st.set_page_config(
15
+ page_title="ASME Section VIII Calculator",
16
+ page_icon="🔧",
17
+ layout="wide",
18
+ initial_sidebar_state="expanded",
19
+ )
20
+
21
+ # --- Simple styling ---
22
+ st.markdown(
23
+ """
24
+ <style>
25
+ .main-header { font-size: 2.2rem; font-weight: 700; color: #0b5fff; }
26
+ .calc-section { background-color: #f8f9fa; padding: 0.8rem; border-radius: 8px; }
27
+ .result-box { background-color: #e8f5e8; padding: 0.8rem; border-radius: 8px; }
28
+ .warning-box { background-color: #fff3cd; padding: 0.8rem; border-radius: 8px; }
29
+ @media (max-width: 768px) { .main-header { font-size: 1.6rem; } }
30
+ </style>
31
+ """,
32
+ unsafe_allow_html=True,
33
+ )
34
+
35
+ # ---------------------------
36
+ # Material database (compact)
37
+ # ---------------------------
38
+ MATERIALS = {
39
+ "Carbon Steel": {
40
+ "SA-516 Grade 60": {
41
+ "allowable_stress": {
42
+ "USC": {-20: 15000, 100: 15000, 200: 15000, 300: 15000, 400: 15000, 500: 14750, 600: 14200, 650: 13100},
43
+ "SI": {-29: 103, 38: 103, 93: 103, 149: 103, 204: 103, 260: 102, 316: 98, 343: 90},
44
+ },
45
+ "carbon_equivalent": 0.30,
46
+ "material_group": "P1",
47
+ },
48
+ "SA-516 Grade 70": {
49
+ "allowable_stress": {
50
+ "USC": {-20: 17500, 100: 17500, 200: 17500, 300: 17500, 400: 17500, 500: 17200, 600: 16550, 650: 15300},
51
+ "SI": {-29: 121, 38: 121, 93: 121, 149: 121, 204: 121, 260: 119, 316: 114, 343: 105},
52
+ },
53
+ "carbon_equivalent": 0.31,
54
+ "material_group": "P1",
55
+ },
56
+ },
57
+ "Stainless Steel": {
58
+ "SA-240 Type 304": {
59
+ "allowable_stress": {
60
+ "USC": {-20: 18800, 100: 18800, 200: 16200, 300: 15100, 400: 14200, 500: 13500, 600: 12800, 700: 12100},
61
+ "SI": {-29: 130, 38: 130, 93: 112, 149: 104, 204: 98, 260: 93, 316: 88, 371: 83},
62
+ },
63
+ "carbon_equivalent": 0.08,
64
+ "material_group": "P8",
65
+ },
66
+ "SA-240 Type 316": {
67
+ "allowable_stress": {
68
+ "USC": {-20: 18800, 100: 18800, 200: 16600, 300: 15600, 400: 14800, 500: 14200, 600: 13600, 700: 13000},
69
+ "SI": {-29: 130, 38: 130, 93: 114, 149: 107, 204: 102, 260: 98, 316: 94, 371: 90},
70
+ },
71
+ "carbon_equivalent": 0.08,
72
+ "material_group": "P8",
73
+ },
74
+ },
75
+ }
76
+
77
+ # ---------------------------
78
+ # Helper functions
79
+ # ---------------------------
80
+
81
+ def get_allowable_stress(material_category, material_grade, temperature, units):
82
+ """Interpolate allowable stress table for the selected unit system."""
83
+ try:
84
+ stress_data = MATERIALS[material_category][material_grade]["allowable_stress"][units]
85
+ temps = sorted(stress_data.keys())
86
+ stresses = [stress_data[t] for t in temps]
87
+ if temperature <= temps[0]:
88
+ return stresses[0]
89
+ if temperature >= temps[-1]:
90
+ return stresses[-1]
91
+ return float(np.interp(temperature, temps, stresses))
92
+ except Exception:
93
+ # Fallback safe value
94
+ return 15000.0 if units == "USC" else 103.0
95
+
96
+
97
+ def convert_temperature(temp, from_unit, to_unit):
98
+ if from_unit == to_unit:
99
+ return temp
100
+ if from_unit == "C" and to_unit == "F":
101
+ return (temp * 9.0 / 5.0) + 32.0
102
+ if from_unit == "F" and to_unit == "C":
103
+ return (temp - 32.0) * 5.0 / 9.0
104
+ return temp
105
+
106
+
107
+ def calculate_shell_thickness(P, R, S, E):
108
+ """UG-27 shell thickness. P = design pressure, R = inside radius, S = allowable stress, E = joint eff."""
109
+ # Use the UG-27 circumferential and longitudinal formulas
110
+ if P <= 0.385 * S * E:
111
+ t_circum = (P * R) / (S * E - 0.6 * P)
112
+ formula_used = "t = PR/(SE - 0.6P)"
113
+ condition = f"P ≤ 0.385SE: {P:.3f} ≤ {0.385 * S * E:.3f}"
114
+ else:
115
+ t_circum = (P * R) / (S * E + 0.4 * P)
116
+ formula_used = "t = PR/(SE + 0.4P)"
117
+ condition = f"P > 0.385SE: {P:.3f} > {0.385 * S * E:.3f}"
118
+
119
+ t_long = (P * R) / (2 * S * E + 0.4 * P)
120
+ t_required = max(t_circum, t_long)
121
+ governing = "Circumferential" if t_circum >= t_long else "Longitudinal"
122
+
123
+ return {
124
+ "circumferential_thickness": float(t_circum),
125
+ "longitudinal_thickness": float(t_long),
126
+ "required_thickness": float(t_required),
127
+ "governing_case": governing,
128
+ "formula_used": formula_used,
129
+ "condition_check": condition,
130
+ "asme_clause": "UG-27",
131
+ }
132
+
133
+
134
+ def calculate_head_thickness(P, D, S, E, head_type="ellipsoidal"):
135
+ """UG-32 head thickness (basic implementations). D is inside diameter."""
136
+ if head_type == "ellipsoidal":
137
+ K = 1.0
138
+ t = (P * D * K) / (2 * S * E - 0.2 * P)
139
+ clause = "UG-32(d), Appendix 1-4(c)"
140
+ formula = "t = PDK/(2SE - 0.2P)"
141
+ elif head_type == "torispherical":
142
+ L = D
143
+ r = 0.1 * D
144
+ M = 0.25 * (3.0 + math.sqrt(L / r))
145
+ t = (P * L * M) / (2 * S * E - 0.2 * P)
146
+ clause = "UG-32(e), Appendix 1-4(d)"
147
+ formula = f"t = PLM/(2SE - 0.2P), M={M:.3f}"
148
+ elif head_type == "hemispherical":
149
+ L = D / 2.0
150
+ t = (P * L) / (2 * S * E - 0.2 * P)
151
+ clause = "UG-32(f), Appendix 1-4(e)"
152
+ formula = "t = PL/(2SE - 0.2P)"
153
+ else:
154
+ raise ValueError("Unsupported head type")
155
+
156
+ return {"required_thickness": float(t), "head_type": head_type, "formula_used": formula, "asme_clause": clause}
157
+
158
+
159
+ def calculate_nozzle_reinforcement(d, t_shell, t_nozzle, t_pad, pad_width):
160
+ """Very conservative, simplified area method for nozzle reinforcement (UG-37 family).
161
+ d should be the nozzle inside diameter (or opening) — units must be consistent with thickness."""
162
+ A_required = d * t_shell
163
+ # conservative shell area (A1) assumed zero if no excess thickness
164
+ A_shell = 0.0
165
+ # nozzle area contribution (A2) approximate
166
+ height_limit = min(2.5 * t_shell, 2.5 * t_nozzle)
167
+ A_nozzle = t_nozzle * height_limit
168
+ # pad area contribution
169
+ A_pad = t_pad * pad_width if t_pad > 0 and pad_width > 0 else 0.0
170
+ A_welds = 0.0
171
+ A_available = A_shell + A_nozzle + A_pad + A_welds
172
+
173
+ return {
174
+ "area_required": float(A_required),
175
+ "area_shell": float(A_shell),
176
+ "area_nozzle": float(A_nozzle),
177
+ "area_pad": float(A_pad),
178
+ "area_welds": float(A_welds),
179
+ "area_available_total": float(A_available),
180
+ "reinforcement_adequate": A_available >= A_required,
181
+ "safety_factor": float(A_available / A_required) if A_required > 0 else 0.0,
182
+ "asme_clause": "UG-37 through UG-45",
183
+ }
184
+
185
+
186
+ def determine_pwht_requirements(material_category, material_grade, thickness_in, units):
187
+ """Very simplified PWHT decision logic based on UCS-56 guidance."""
188
+ if units == "SI":
189
+ thickness_inch = thickness_in / 25.4
190
+ else:
191
+ thickness_inch = thickness_in
192
+
193
+ info = MATERIALS[material_category][material_grade]
194
+ mg = info["material_group"]
195
+ ce = info["carbon_equivalent"]
196
+
197
+ pwht_required = False
198
+ reasons = []
199
+ temp_range = ""
200
+ hold_time = ""
201
+
202
+ if mg == "P1":
203
+ if thickness_inch > 1.25:
204
+ pwht_required = True
205
+ reasons.append("Thickness > 1.25 inches (UCS-56)")
206
+ temp_range = "1100°F - 1200°F" if units == "USC" else "593°C - 649°C"
207
+ hold_time = f"Minimum {max(1, thickness_inch):.1f} hours"
208
+ if ce > 0.35:
209
+ pwht_required = True
210
+ reasons.append("Carbon equivalent > 0.35% (UCS-56)")
211
+ elif mg == "P8":
212
+ reasons.append("Stainless steel: PWHT generally not required (consult spec)")
213
+
214
+ if not pwht_required:
215
+ reasons.append("Below thickness and CE limits")
216
+
217
+ return {"pwht_required": pwht_required, "reasons": reasons, "temperature_range": temp_range, "hold_time": hold_time}
218
+
219
+
220
+ def determine_impact_test_requirements(material_category, material_grade, design_temp, thickness_in, units):
221
+ """Very simplified impact test exemption logic (UCS-66 style)."""
222
+ if units == "SI":
223
+ design_temp_f = convert_temperature(design_temp, "C", "F")
224
+ thickness_inch = thickness_in / 25.4
225
+ else:
226
+ design_temp_f = design_temp
227
+ thickness_inch = thickness_in
228
+
229
+ info = MATERIALS[material_category][material_grade]
230
+ mg = info["material_group"]
231
+
232
+ exemption_map_P1 = {0.5: 60, 1.0: 30, 2.0: 0, 4.0: -20}
233
+ exemption_map_P8 = {0.5: -325, 1.0: -325, 2.0: -325, 4.0: -325}
234
+
235
+ if mg == "P1":
236
+ xs = sorted(exemption_map_P1.keys())
237
+ ys = [exemption_map_P1[x] for x in xs]
238
+ if thickness_inch <= xs[0]:
239
+ ex_temp = ys[0]
240
+ elif thickness_inch >= xs[-1]:
241
+ ex_temp = ys[-1]
242
+ else:
243
+ ex_temp = float(np.interp(thickness_inch, xs, ys))
244
+ else:
245
+ ex_temp = exemption_map_P8[0.5]
246
+
247
+ impact_required = design_temp_f < ex_temp
248
+ test_temp = design_temp_f - 30
249
+
250
+ return {
251
+ "impact_test_required": impact_required,
252
+ "design_temperature_f": design_temp_f,
253
+ "exemption_temperature_f": ex_temp,
254
+ "test_temperature_f": test_temp,
255
+ }
256
+
257
+
258
+ def calculate_mawp(t, R_or_D, S, E, component_type="shell", head_type="ellipsoidal"):
259
+ """Calculate MAWP from thickness (t), geometry and allowable stress S.
260
+ For shell: R_or_D is inside radius; for head we accept D for formulas used earlier."""
261
+ if component_type == "shell":
262
+ # P = SE t / (R + 0.6 t)
263
+ P = (S * E * t) / (R_or_D + 0.6 * t)
264
+ formula = "P = SEt/(R + 0.6t)"
265
+ else:
266
+ # approximate head MAWP using ellipsoidal/hemispherical style
267
+ if head_type == "ellipsoidal":
268
+ K = 1.0
269
+ P = (2 * S * E * t * K) / (R_or_D + 0.2 * t)
270
+ formula = "P = 2SEtK/(D + 0.2t)"
271
+ elif head_type == "hemispherical":
272
+ P = (2 * S * E * t) / (R_or_D + 0.4 * t)
273
+ formula = "P = 2SEt/(R + 0.4t)"
274
+ else:
275
+ P = (2 * S * E * t) / (R_or_D + 0.2 * t)
276
+ formula = "P = 2SEt/(D + 0.2t)"
277
+
278
+ return {"mawp": float(P), "formula": formula}
279
+
280
+
281
+ # ---------------------------
282
+ # Streamlit UI
283
+ # ---------------------------
284
+
285
+ def main():
286
+ st.markdown('<div class="main-header">🔧 ASME Section VIII Pressure Vessel Calculator</div>', unsafe_allow_html=True)
287
+
288
+ # Sidebar
289
+ st.sidebar.title("Input Parameters")
290
+ units = st.sidebar.selectbox("Unit System", ["USC", "SI"], index=0)
291
+ unit_system = "USC" if units == "USC" else "SI"
292
+
293
+ if unit_system == "USC":
294
+ pressure_unit = "psi"
295
+ length_unit = "in"
296
+ temp_unit = "°F"
297
+ stress_unit = "psi"
298
+ else:
299
+ pressure_unit = "bar"
300
+ length_unit = "mm"
301
+ temp_unit = "°C"
302
+ stress_unit = "MPa"
303
+
304
+ st.sidebar.subheader("Design Conditions")
305
+ design_pressure = st.sidebar.number_input(f"Design Pressure ({pressure_unit})", value=150.0)
306
+ design_temperature = st.sidebar.number_input(f"Design Temperature ({temp_unit})", value=200.0)
307
+ corrosion_allowance = st.sidebar.number_input(f"Corrosion Allowance ({length_unit})", value=0.125)
308
+
309
+ st.sidebar.subheader("Material & Geometry")
310
+ mat_cat = st.sidebar.selectbox("Material Category", list(MATERIALS.keys()))
311
+ mat_grade = st.sidebar.selectbox("Material Grade", list(MATERIALS[mat_cat].keys()))
312
+ joint_eff = st.sidebar.selectbox("Joint Efficiency (E)", [1.0, 0.95, 0.9, 0.85], index=0)
313
+ inside_diameter = st.sidebar.number_input(f"Inside Diameter ({length_unit})", value=60.0)
314
+ head_type = st.sidebar.selectbox("Head Type", ["ellipsoidal", "torispherical", "hemispherical"])
315
+
316
+ # Derived
317
+ inside_radius = inside_diameter / 2.0
318
+ allowable_stress = get_allowable_stress(mat_cat, mat_grade, design_temperature, unit_system)
319
+
320
+ tab1, tab2, tab3, tab4, tab5 = st.tabs(["Shell", "Head", "Nozzle", "PWHT", "Summary"])
321
+
322
+ with tab1:
323
+ st.header("Shell Thickness (UG-27)")
324
+ shell = calculate_shell_thickness(design_pressure, inside_radius, allowable_stress, joint_eff)
325
+ st.write("**Formula used:**", shell["formula_used"])
326
+ st.write("**Condition:**", shell["condition_check"])
327
+ st.write("Circumferential thickness:", f"{shell['circumferential_thickness']:.4f} {length_unit}")
328
+ st.write("Longitudinal thickness:", f"{shell['longitudinal_thickness']:.4f} {length_unit}")
329
+ st.write("Required thickness:", f"{shell['required_thickness']:.4f} {length_unit}")
330
+
331
+ with tab2:
332
+ st.header("Head Thickness (UG-32)")
333
+ head = calculate_head_thickness(design_pressure, inside_diameter, allowable_stress, joint_eff, head_type)
334
+ st.write("**Head Type:**", head["head_type"].title())
335
+ st.write("**Formula used:**", head["formula_used"])
336
+ st.write("Required thickness:", f"{head['required_thickness']:.4f} {length_unit}")
337
+
338
+ with tab3:
339
+ st.header("Nozzle Reinforcement (UG-37)")
340
+ nozzle_d = st.number_input("Nozzle Opening Diameter (same unit as vessel)", value=6.0)
341
+ nozzle_t = st.number_input("Nozzle Wall Thickness", value=0.5)
342
+ use_pad = st.checkbox("Use Reinforcing Pad")
343
+ pad_t = pad_w = 0.0
344
+ if use_pad:
345
+ pad_t = st.number_input("Pad Thickness", value=0.25)
346
+ pad_w = st.number_input("Pad Width", value=8.0)
347
+
348
+ shell_total_thickness = shell['required_thickness'] + corrosion_allowance
349
+ nozzle_res = calculate_nozzle_reinforcement(nozzle_d, shell_total_thickness, nozzle_t, pad_t, pad_w)
350
+ st.write("Required area:", f"{nozzle_res['area_required']:.4f} {length_unit}^2")
351
+ st.write("Available area:", f"{nozzle_res['area_available_total']:.4f} {length_unit}^2")
352
+ if nozzle_res['reinforcement_adequate']:
353
+ st.success(f"Reinforcement adequate (SF={nozzle_res['safety_factor']:.2f})")
354
+ else:
355
+ st.error(f"Reinforcement inadequate (SF={nozzle_res['safety_factor']:.2f})")
356
+
357
+ with tab4:
358
+ st.header("PWHT & Impact"")
359
+ total_thick = shell['required_thickness'] + corrosion_allowance
360
+ pwht = determine_pwht_requirements(mat_cat, mat_grade, total_thick, unit_system)
361
+ impact = determine_impact_test_requirements(mat_cat, mat_grade, design_temperature, total_thick, unit_system)
362
+
363
+ st.subheader("PWHT")
364
+ if pwht['pwht_required']:
365
+ st.error("PWHT REQUIRED")
366
+ st.write("Reasons:")
367
+ for r in pwht['reasons']:
368
+ st.write(f"- {r}")
369
+ st.write("Temperature range:", pwht['temperature_range'])
370
+ else:
371
+ st.success("PWHT not required")
372
+
373
+ st.subheader("Impact Test")
374
+ st.write("Design temperature (°F):", f"{impact['design_temperature_f']:.1f}")
375
+ st.write("Exemption temperature (°F):", f"{impact['exemption_temperature_f']:.1f}")
376
+ if impact['impact_test_required']:
377
+ st.error("Impact test required — Charpy V-notch")
378
+ st.write("Test temp (°F):", f"{impact['test_temperature_f']:.1f}")
379
+ else:
380
+ st.success("Impact test not required")
381
+
382
+ with tab5:
383
+ st.header("Summary Report")
384
+ total_shell = shell['required_thickness'] + corrosion_allowance
385
+ total_head = head['required_thickness'] + corrosion_allowance
386
+
387
+ shell_mawp = calculate_mawp(total_shell, inside_radius, allowable_stress, joint_eff, "shell")
388
+ head_mawp = calculate_mawp(total_head, inside_diameter, allowable_stress, joint_eff, "head", head_type)
389
+ governing_mawp = min(shell_mawp['mawp'], head_mawp['mawp'])
390
+
391
+ st.metric("Governing MAWP", f"{governing_mawp:.2f} {pressure_unit}")
392
+ st.write("Shell MAWP:", f"{shell_mawp['mawp']:.2f} {pressure_unit}")
393
+ st.write("Head MAWP:", f"{head_mawp['mawp']:.2f} {pressure_unit}")
394
+
395
+ df = pd.DataFrame({
396
+ "Parameter": [
397
+ "Design Pressure",
398
+ "Design Temperature",
399
+ "Inside Diameter",
400
+ "Material",
401
+ "Allowable Stress",
402
+ "Joint Efficiency",
403
+ "Corrosion Allowance",
404
+ "Shell Required Thickness",
405
+ "Shell Total Thickness",
406
+ "Head Required Thickness",
407
+ "Head Total Thickness",
408
+ ],
409
+ "Value": [
410
+ f"{design_pressure} {pressure_unit}",
411
+ f"{design_temperature} {temp_unit}",
412
+ f"{inside_diameter} {length_unit}",
413
+ f"{mat_grade}",
414
+ f"{allowable_stress:.0f} {stress_unit}",
415
+ str(joint_eff),
416
+ f"{corrosion_allowance} {length_unit}",
417
+ f"{shell['required_thickness']:.4f} {length_unit}",
418
+ f"{total_shell:.4f} {length_unit}",
419
+ f"{head['required_thickness']:.4f} {length_unit}",
420
+ f"{total_head:.4f} {length_unit}",
421
+ ],
422
+ })
423
+
424
+ st.dataframe(df, use_container_width=True)
425
+ st.markdown("---")
426
+ st.caption("Note: This tool is for preliminary design only. Final calculations must be verified by a licensed engineer and checked against the latest ASME code editions.")
427
+ st.write("Report generated:", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
428
+
429
+
430
+ if __name__ == "__main__":
431
+ main()