Umar4321 commited on
Commit
a76f1aa
Β·
verified Β·
1 Parent(s): 3227516

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -402
app.py CHANGED
@@ -1,429 +1,195 @@
1
- #!/usr/bin/env python3
2
- """
3
- ASME Section VIII β€” Preliminary Pressure Vessel Calculator (Streamlit)
4
-
5
- - PDF export removed to avoid runtime font/encoding/width issues.
6
- - CSV export remains.
7
- - Impact/MDMT checks improved using a conservative UCS-66 approximation (preliminary).
8
- - Single Run Calculation button; Reset; SI/USC support; joint efficiency includes 0.7.
9
- - Simplified, preliminary checks only. Verify with a licensed engineer.
10
-
11
- Notes:
12
- This implements a conservative approximation of UCS-66 curves and the coincident-ratio
13
- reduction (FIG UCS-66.1) for preliminary MDMT / impact-test decisions. It is NOT a
14
- replacement for a code check with the official ASME figures and a licensed engineer.
15
- """
16
-
17
- from __future__ import annotations
18
- import os
19
- import math
20
- import datetime
21
- from typing import Dict, Any, Optional, Tuple
22
-
23
  import streamlit as st
24
  import pandas as pd
25
- import numpy as np
26
-
27
- # -----------------------------
28
- # Unit conversion helpers & constants
29
- # -----------------------------
30
- MPA_TO_PSI = 145.037737797
31
- PSI_TO_MPA = 1.0 / MPA_TO_PSI
32
- MM_TO_IN = 0.03937007874015748
33
- IN_TO_MM = 25.4
34
-
35
- def mpa_to_psi(x: float) -> float:
36
- return float(x) * MPA_TO_PSI
37
-
38
- def psi_to_mpa(x: float) -> float:
39
- return float(x) * PSI_TO_MPA
40
-
41
- def mm_to_in(x: float) -> float:
42
- return float(x) * MM_TO_IN
43
-
44
- def in_to_mm(x: float) -> float:
45
- return float(x) * IN_TO_MM
46
-
47
- def f_to_c(t_f: float) -> float:
48
- return (t_f - 32.0) * 5.0 / 9.0
49
-
50
- def c_to_f(t_c: float) -> float:
51
- return t_c * 9.0 / 5.0 + 32.0
52
-
53
- # -----------------------------
54
- # UCS-66 table data (approximate tabulation of Fig. UCS-66)
55
- # Source: public summaries of TABLE UCS-66 (used here for conservative, preliminary checks).
56
- # This is an approximation for preliminary use only β€” see ASME Fig. UCS-66 for final.
57
- # -----------------------------
58
- # thickness (in) sampled and corresponding minimum permissible temperature without impact test (deg F)
59
- UCS66_THICK_IN = np.array([
60
- 0.3125, 0.375, 0.4375, 0.5, 0.5625, 0.625, 0.6875, 0.75, 0.8125,
61
- 0.875, 0.9375, 1.0, 1.0625, 1.125, 1.1875, 1.25, 1.3125, 1.375, 1.4375, 1.5
62
- ])
63
- # Columns: curve A, B, C, D (deg F)
64
- UCS66_TABLE_F = np.array([
65
- [18, -20, -55, -55],
66
- [18, -20, -55, -55],
67
- [25, -13, -40, -55],
68
- [32, -7, -34, -55],
69
- [37, -1, -26, -51],
70
- [43, 5, -22, -48],
71
- [48, 10, -18, -45],
72
- [53, 15, -15, -42],
73
- [57, 19, -12, -38],
74
- [61, 23, -9, -36],
75
- [65, 27, -6, -33],
76
- [68, 31, -3, -30],
77
- [72, 34, -1, -28],
78
- [75, 37, 2, -26],
79
- [77, 40, 2, -23],
80
- [80, 43, 6, -21],
81
- [82, 45, 8, -19],
82
- [84, 47, 10, -18],
83
- [86, 49, 12, -16],
84
- [88, 51, 14, -14],
85
- ], dtype=float)
86
-
87
- CURVE_LABELS = ["A", "B", "C", "D"]
88
-
89
- # -----------------------------
90
- # Calculation helpers (pure functions)
91
- # -----------------------------
92
-
93
- def calculate_shell_thickness(P: float, R: float, S: float, E: float, corrosion: float) -> float:
94
- if E <= 0 or E > 1:
95
- raise ValueError("Joint efficiency E must be in (0,1].")
96
- if S <= 0:
97
- raise ValueError("Allowable stress must be positive.")
98
- denom = S * E - 0.6 * P
99
- if denom <= 0:
100
- raise ValueError("Invalid combination: denominator (S*E - 0.6*P) <= 0. Check inputs.")
101
- t = (P * R) / denom
102
- return float(t + corrosion)
103
-
104
-
105
- def calculate_head_thickness(P: float, R: float, S: float, E: float, corrosion: float, head_type: str) -> float:
106
- if E <= 0 or E > 1:
107
- raise ValueError("Joint efficiency E must be in (0,1].")
108
  if head_type == "Ellipsoidal":
109
- denom = 2 * S * E - 0.2 * P
110
- if denom <= 0:
111
- raise ValueError("Invalid inputs for ellipsoidal head formula (denominator <= 0).")
112
- t = (P * 2.0 * R) / denom
113
  elif head_type == "Torispherical":
114
- L = 2.0 * R
115
- r = 0.1 * L
116
- M = 0.25 * (3.0 + math.sqrt(L / r))
117
- denom = 2 * S * E - 0.2 * P
118
- if denom <= 0:
119
- raise ValueError("Invalid inputs for torispherical head formula (denominator <= 0).")
120
- t = (P * L * M) / denom
121
  elif head_type == "Hemispherical":
122
- L = R
123
- denom = 2 * S * E - 0.2 * P
124
- if denom <= 0:
125
- raise ValueError("Invalid inputs for hemispherical head formula (denominator <= 0).")
126
- t = (P * L) / denom
127
- else:
128
- raise ValueError("Unsupported head type.")
129
- return float(t + corrosion)
130
 
 
 
131
 
132
- def nozzle_reinforcement_check(P: float, d_opening: float, t_shell: float, t_nozzle: float, S: float, E: float) -> Dict[str, Any]:
133
- if any(val <= 0 for val in (d_opening, S, E)):
134
- raise ValueError("Nozzle diameter, allowable stress and joint efficiency must be positive.")
135
- lhs = (P * d_opening) / (2.0 * S * E)
136
- rhs = (t_shell + t_nozzle)
137
- adequate = lhs <= rhs
138
- return {"lhs": float(lhs), "rhs": float(rhs), "adequate": bool(adequate)}
139
 
 
 
140
 
141
- def pwht_decision(thickness: float, unit_system: str = "SI") -> bool:
142
- if unit_system == "SI":
143
- return thickness > 38.0 # mm
144
- else:
145
- return thickness > mm_to_in(38.0) # in
146
-
147
- # -----------------------------
148
- # UCS-66 based impact test decision (improved)
149
- # -----------------------------
150
-
151
- def _interpolate_ucs66_min_temp_f(thickness_in: float, curve: str) -> float:
152
- """
153
- Interpolate the UCS-66 table (inches -> deg F) for selected curve (A-D).
154
- If thickness is outside table bounds, we extrapolate conservatively using edge values.
155
- """
156
- if curve not in CURVE_LABELS:
157
- raise ValueError("Curve must be one of A/B/C/D")
158
- col = CURVE_LABELS.index(curve)
159
- # ensure monotonic increasing thickness array
160
- if thickness_in <= UCS66_THICK_IN[0]:
161
- return UCS66_TABLE_F[0, col]
162
- if thickness_in >= UCS66_THICK_IN[-1]:
163
- return UCS66_TABLE_F[-1, col]
164
- return float(np.interp(thickness_in, UCS66_THICK_IN, UCS66_TABLE_F[:, col]))
165
-
166
-
167
- def impact_test_decision_ucs66(
168
- governing_thickness: float,
169
- nominal_thickness: float,
170
- corrosion_allowance: float,
171
- joint_efficiency: float,
172
- design_mdmt: float,
173
- material_curve: str,
174
- unit_system: str = "SI",
175
- ) -> Dict[str, Any]:
176
- """
177
- Determine preliminary impact-test requirement using UCS-66 approximation.
178
-
179
- Parameters:
180
- governing_thickness: governing design thickness (use required design thickness where applicable)
181
- nominal_thickness: nominal plate/weld thickness used in table UCS-66
182
- corrosion_allowance: corrosion allowance
183
- joint_efficiency: E
184
- design_mdmt: MDMT (Β°C if SI, Β°F if USC)
185
- material_curve: 'A'|'B'|'C'|'D'
186
- unit_system: 'SI' or 'USC'
187
-
188
- Returns dict with keys:
189
- 'impact_required' (bool),
190
- 'ucs66_min_temp' (deg in user's unit),
191
- 'adjusted_min_temp' (after coincident-ratio reduction),
192
- 'coincident_ratio',
193
- 'note'
194
-
195
- NOTE: This is an approximation for preliminary screening only. Always check the
196
- official ASME Fig. UCS-66 and Fig. UCS-66.1 and notes and consult a qualified engineer.
197
- """
198
- # convert thickness to inches for table lookup
199
- if unit_system == "SI":
200
- t_g_in = mm_to_in(governing_thickness)
201
- tn_in = mm_to_in(nominal_thickness)
202
- c_in = mm_to_in(corrosion_allowance)
203
- mdmt_f = c_to_f(design_mdmt)
204
- else:
205
- t_g_in = governing_thickness
206
- tn_in = nominal_thickness
207
- c_in = corrosion_allowance
208
- mdmt_f = design_mdmt
209
-
210
- # step 1: lookup UCS-66 minimum permissible temp (deg F) for governing thickness
211
- ucs66_min_f = _interpolate_ucs66_min_temp_f(t_g_in, material_curve)
212
-
213
- # step 2: coincident ratio (FIG UCS-66.1) to possibly reduce the required minimum temp
214
- # Ratio = tr * E / (tn - c)
215
- denom = (tn_in - c_in)
216
- ratio = (t_g_in * joint_efficiency / denom) if denom > 0 else 1.0
217
-
218
- # If ratio >= 1: cannot reduce; adjusted = ucs66_min_f
219
- # If ratio < 1: FIG UCS-66.1 gives allowable reduction. We implement a conservative
220
- # approximation: reduction (deg F) = min( (1 - ratio) * 40, 30 )
221
- # (This is an approximation for screening only.)
222
- if ratio >= 1.0:
223
- adjusted_min_f = ucs66_min_f
224
- else:
225
- reduction = min((1.0 - ratio) * 40.0, 30.0) # cap at 30Β°F
226
- adjusted_min_f = ucs66_min_f - reduction
227
 
228
- # final decision: if MDMT >= adjusted_min_f -> impact not required
229
- impact_required = mdmt_f < adjusted_min_f
230
 
231
- # convert temps back to user's unit
232
- if unit_system == "SI":
233
- ucs66_min = f_to_c(ucs66_min_f)
234
- adjusted_min = f_to_c(adjusted_min_f)
235
- else:
236
- ucs66_min = ucs66_min_f
237
- adjusted_min = adjusted_min_f
238
-
239
- # note: enforce thickness limitations mentioned in code notes (UCS-66(d) and others)
240
- # For conservative handling: if nominal thickness <= 0.1 in (β‰ˆ2.5 mm) allow limited exemptions per UCS-66(d)
241
- small_thickness_exempt = (tn_in <= 0.098) # 0.098 in β‰ˆ 2.5 mm
242
-
243
- note = (
244
- "Preliminary UCS-66 screening (approximated table + conservative coincident-ratio reduction)."
245
- " Use official ASME Fig. UCS-66 / UCS-66.1 for final decisions."
246
- )
247
-
248
- return {
249
- "impact_required": bool(impact_required and not small_thickness_exempt),
250
- "ucs66_min_temp_user_unit": ucs66_min,
251
- "adjusted_min_temp_user_unit": adjusted_min,
252
- "coincident_ratio": float(ratio),
253
- "mdmt_user_unit": design_mdmt,
254
- "small_thickness_exempt": bool(small_thickness_exempt),
255
- "note": note,
256
- }
257
-
258
- # -----------------------------
259
- # Streamlit UI
260
- # -----------------------------
261
-
262
- st.set_page_config(page_title="ASME Section VIII Calculator", page_icon="πŸ”§", layout="wide")
263
- st.title("πŸ”§ ASME Section VIII β€” Preliminary Calculator (UCS-66 improved)")
264
- st.caption(
265
- "Preliminary calculations only. Final verification must be completed by a licensed professional engineer "
266
- "and checked against the latest ASME code editions. This tool uses a conservative UCS-66 approximation for screening."
267
- )
268
 
269
- if "run_done" not in st.session_state:
270
- st.session_state.run_done = False
 
 
 
271
 
272
- with st.sidebar.expander("Inputs", expanded=True):
273
- unit_system_choice = st.selectbox("Unit System", ["SI (MPa / mm / Β°C)", "USC (psi / in / Β°F)"])
274
- use_si = unit_system_choice.startswith("SI")
275
-
276
- st.markdown("---")
277
- if use_si:
278
- design_pressure = st.number_input("Design Pressure (MPa)", value=2.0, format="%.3f")
279
- inside_diameter = st.number_input("Inside Diameter (mm)", value=1500.0, format="%.1f")
280
- R = inside_diameter / 2.0
281
- allowable_stress = st.number_input("Allowable Stress (MPa)", value=120.0, format="%.2f")
282
- corrosion_allowance = st.number_input("Corrosion Allowance (mm)", value=1.5, format="%.2f")
283
- design_mdmt = st.number_input("Design MDMT (Β°C)", value=-20.0)
284
- else:
285
- design_pressure = st.number_input("Design Pressure (psi)", value=mpa_to_psi(2.0), format="%.1f")
286
- inside_diameter = st.number_input("Inside Diameter (in)", value=mm_to_in(1500.0), format="%.3f")
287
- R = inside_diameter / 2.0
288
- allowable_stress = st.number_input("Allowable Stress (psi)", value=mpa_to_psi(120.0), format="%.1f")
289
- corrosion_allowance = st.number_input("Corrosion Allowance (in)", value=mm_to_in(1.5), format="%.4f")
290
- design_mdmt = st.number_input("Design MDMT (Β°F)", value=-20.0)
291
-
292
- st.markdown("---")
293
- head_type = st.selectbox("Head Type", ["Ellipsoidal", "Torispherical", "Hemispherical"])
294
- joint_eff = st.selectbox("Joint Efficiency (E)", [1.0, 0.95, 0.9, 0.85, 0.7], index=0)
295
-
296
- st.markdown("---")
297
- # Nozzle inputs (requesting the required inputs)
298
- if use_si:
299
- nozzle_opening_diameter = st.number_input("Nozzle Opening Diameter (mm)", value=200.0, format="%.1f")
300
- nozzle_wall_thickness = st.number_input("Nozzle Wall Thickness (mm)", value=10.0, format="%.2f")
301
- shell_thickness_available = st.number_input("Shell thickness available (mm)", value=12.0, format="%.2f")
302
- else:
303
- nozzle_opening_diameter = st.number_input("Nozzle Opening Diameter (in)", value=mm_to_in(200.0), format="%.3f")
304
- nozzle_wall_thickness = st.number_input("Nozzle Wall Thickness (in)", value=mm_to_in(10.0), format="%.4f")
305
- shell_thickness_available = st.number_input("Shell thickness available (in)", value=mm_to_in(12.0), format="%.4f")
306
 
307
- st.markdown("---")
308
- # UCS-66 / material curve selection for impact testing
309
- material_curve = st.selectbox("Material Curve for UCS-66", ["A", "B", "C", "D"], index=3)
 
 
310
 
311
- st.markdown("---")
312
- # Single run and reset
313
- run_calc = st.button("Run Calculation")
314
- reset = st.button("Reset to defaults")
315
- if reset:
316
- st.session_state.run_done = False
317
- st.experimental_rerun()
318
 
319
- if run_calc:
320
- st.session_state.run_done = True
321
 
322
- # Tabs and results
323
- tab_list = ["Shell", "Head", "Nozzle", "PWHT & Impact", "Summary"]
324
- tabs = st.tabs(tab_list)
 
 
 
 
 
 
 
 
 
 
 
325
 
326
  if st.session_state.run_done:
327
- # Shell tab
328
  with tabs[0]:
329
- try:
330
- shell_res = calculate_shell_thickness(design_pressure, R, allowable_stress, joint_eff, corrosion_allowance)
331
- units_len = "mm" if use_si else "in"
332
- st.metric("Required Shell Thickness (incl. corrosion)", f"{shell_res:.4f} {units_len}")
333
- st.write("Inputs:", {"P": design_pressure, "R": R, "S": allowable_stress, "E": joint_eff})
334
- except Exception as exc:
335
- st.error(f"Shell calculation error: {exc}")
336
-
337
- # Head tab
338
  with tabs[1]:
339
- try:
340
- head_res = calculate_head_thickness(design_pressure, R, allowable_stress, joint_eff, corrosion_allowance, head_type)
341
- units_len = "mm" if use_si else "in"
342
- st.metric(f"Required {head_type} Head Thickness (incl. corrosion)", f"{head_res:.4f} {units_len}")
343
- st.write("Intermediate values: D β‰ˆ", 2.0 * R)
344
- except Exception as exc:
345
- st.error(f"Head calculation error: {exc}")
346
-
347
- # Nozzle tab
348
  with tabs[2]:
349
- try:
350
- nozzle_check = nozzle_reinforcement_check(design_pressure, nozzle_opening_diameter, shell_thickness_available, nozzle_wall_thickness, allowable_stress, joint_eff)
351
- st.write("Nozzle reinforcement conservative check:")
352
- st.write(f"LHS = (P * d) / (2SE) = {nozzle_check['lhs']:.6g}")
353
- st.write(f"RHS = t_shell + t_nozzle = {nozzle_check['rhs']:.6g}")
354
- if nozzle_check["adequate"]:
355
- st.success("Conservative nozzle reinforcement check PASSED")
356
- else:
357
- st.error("Conservative nozzle reinforcement check FAILED")
358
- st.caption("This is a simplified, conservative approximation of UG-37. Use full UG-37 projection-area method for final design.")
359
- except Exception as exc:
360
- st.error(f"Nozzle calculation error: {exc}")
361
-
362
- # PWHT & Impact tab
363
  with tabs[3]:
364
- try:
365
- thickness_for_checks = shell_res if "shell_res" in locals() else shell_thickness_available
366
- pwht_flag = pwht_decision(thickness_for_checks, unit_system="SI" if use_si else "USC")
367
-
368
- impact_info = impact_test_decision_ucs66(
369
- governing_thickness=thickness_for_checks,
370
- nominal_thickness=shell_thickness_available,
371
- corrosion_allowance=corrosion_allowance,
372
- joint_efficiency=joint_eff,
373
- design_mdmt=design_mdmt,
374
- material_curve=material_curve,
375
- unit_system="SI" if use_si else "USC",
376
- )
377
-
378
- st.subheader("PWHT (Preliminary)")
379
- st.write("PWHT required:", "YES" if pwht_flag else "NO")
380
-
381
- st.subheader("Impact Test (MDMT) - Preliminary (UCS-66 approximation)")
382
- st.write("Material curve:", material_curve)
383
- st.write("Governing thickness:", f"{thickness_for_checks:.3f} {'mm' if use_si else 'in'}")
384
- st.write("Nominal thickness used for table:", f"{shell_thickness_available:.3f} {'mm' if use_si else 'in'}")
385
- st.write("Coincident ratio (tr*E/(tn-c)):", f"{impact_info['coincident_ratio']:.3f}")
386
- st.write("Minimum permissible MDMT without impact testing (UCS-66, approx):", f"{impact_info['ucs66_min_temp_user_unit']:.2f} {'Β°C' if use_si else 'Β°F'}")
387
- st.write("Adjusted minimum after coincident-ratio allowance (approx):", f"{impact_info['adjusted_min_temp_user_unit']:.2f} {'Β°C' if use_si else 'Β°F'}")
388
- st.write("Design MDMT:", f"{design_mdmt:.2f} {'Β°C' if use_si else 'Β°F'}")
389
-
390
- if impact_info['small_thickness_exempt']:
391
- st.info("Nominal thickness is <= 2.5 mm β€” small-thickness exemption per UCS-66(d) may apply (preliminary).")
392
-
393
- if impact_info['impact_required']:
394
- st.error("Impact test REQUIRED (preliminary).")
395
- else:
396
- st.success("Impact test NOT required (preliminary).")
397
-
398
- st.caption(impact_info['note'])
399
- except Exception as exc:
400
- st.error(f"PWHT/Impact calculation error: {exc}")
401
-
402
- # Summary tab
403
  with tabs[4]:
404
- try:
405
- summary = {
406
- "Unit System": "SI (MPa/mm/Β°C)" if use_si else "USC (psi/in/Β°F)",
407
- "Design Pressure": design_pressure,
408
- "Inside Diameter": inside_diameter,
409
- "Shell Required Thickness": shell_res if "shell_res" in locals() else None,
410
- "Head Required Thickness": head_res if "head_res" in locals() else None,
411
- "Nozzle Check Passed": nozzle_check["adequate"] if "nozzle_check" in locals() else None,
412
- "PWHT Required": pwht_flag if "pwht_flag" in locals() else None,
413
- "Impact Test Required": impact_info["impact_required"] if "impact_info" in locals() else None,
414
- "UCS66 Curve": material_curve,
415
- "Generated": datetime.datetime.now().isoformat(),
416
- }
417
- df_summary = pd.DataFrame([summary])
418
- st.dataframe(df_summary, use_container_width=True)
419
-
420
- csv_bytes = df_summary.to_csv(index=False).encode("utf-8")
421
- st.download_button("Download Summary CSV", csv_bytes, file_name="asme_summary.csv", mime="text/csv")
422
- except Exception as exc:
423
- st.error(f"Summary building error: {exc}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
  else:
426
- for i, lbl in enumerate(tab_list):
 
 
 
 
 
427
  with tabs[i]:
428
- st.info("Set inputs in the sidebar and click 'Run Calculation' to execute.")
429
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import pandas as pd
3
+ import math
4
+ from fpdf import FPDF
5
+ import os
6
+ import matplotlib.pyplot as plt
7
+ from groq import Groq
8
+
9
+ # ========== PAGE CONFIG ==========
10
+ st.set_page_config(page_title="ASME Calculator", layout="wide")
11
+
12
+ # App Title + Description
13
+ st.markdown("<h1 style='text-align: center;'>πŸ› οΈ ASME CALCULATOR</h1>", unsafe_allow_html=True)
14
+ st.markdown(
15
+ "<p style='text-align: center;'>This tool calculates <b>required thicknesses, nozzle reinforcement, "
16
+ "PWHT, and impact test requirements</b><br>using ASME Section VIII Division 1 formulas.</p>",
17
+ unsafe_allow_html=True
18
+ )
19
+
20
+ # ========== API CLIENT ==========
21
+ groq_api_key = os.getenv("GROQ_API_KEY")
22
+ groq_client = Groq(api_key=groq_api_key) if groq_api_key else None
23
+
24
+ # ========== PDF GENERATOR ==========
25
+ class PDF(FPDF):
26
+ def __init__(self):
27
+ super().__init__()
28
+ self.add_page()
29
+ self.set_font("Helvetica", "", 12)
30
+
31
+ def header(self):
32
+ self.set_font("Helvetica", "B", 14)
33
+ self.cell(0, 10, "ASME VIII Div.1 Vessel Design Report", 0, 1, "C")
34
+
35
+ def chapter_title(self, title):
36
+ self.set_font("Helvetica", "B", 12)
37
+ self.cell(0, 10, title, 0, 1, "L")
38
+
39
+ def chapter_body(self, body):
40
+ self.set_font("Helvetica", "", 11)
41
+ self.multi_cell(0, 8, body)
42
+
43
+ # ========== CALCULATION FUNCTIONS ==========
44
+ def shell_thickness(P, R, S, E, corrosion):
45
+ return (P * R) / (S * E - 0.6 * P) + corrosion
46
+
47
+ def head_thickness(P, R, S, E, corrosion, head_type):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  if head_type == "Ellipsoidal":
49
+ return (0.5 * P * R) / (S * E - 0.1 * P) + corrosion
 
 
 
50
  elif head_type == "Torispherical":
51
+ return (0.885 * P * R) / (S * E - 0.1 * P) + corrosion
 
 
 
 
 
 
52
  elif head_type == "Hemispherical":
53
+ return (P * R) / (2 * S * E - 0.2 * P) + corrosion
54
+ return None
 
 
 
 
 
 
55
 
56
+ def nozzle_reinforcement(P, d, t_shell, t_nozzle, S, E):
57
+ return (P * d) / (2 * S * E) <= (t_shell + t_nozzle)
58
 
59
+ def pwht_required(thickness, material="CS"):
60
+ return material == "CS" and thickness > 38
 
 
 
 
 
61
 
62
+ def impact_test_required(thickness, MDMT=-20, material="CS"):
63
+ return material == "CS" and (MDMT < -29 and thickness > 12)
64
 
65
+ # ========== SESSION STATE ==========
66
+ if "run_done" not in st.session_state:
67
+ st.session_state.run_done = False
68
+ if "ai_done" not in st.session_state:
69
+ st.session_state.ai_done = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ # ========== SIDEBAR INPUTS ==========
72
+ with st.sidebar.expander("πŸ“₯ Manual Design Inputs", expanded=True):
73
 
74
+ input_mode = st.radio("Input Mode:", ["Manual Entry", "Upload CSV"])
75
+ run_calculation = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ if input_mode == "Manual Entry":
78
+ P = st.number_input("Design Pressure (MPa)", value=2.0, format="%.2f")
79
+ R = st.number_input("Internal Radius (mm)", value=1000.0, format="%.1f")
80
+ S = st.number_input("Allowable Stress (MPa)", value=120.0, format="%.1f")
81
+ corrosion = st.number_input("Corrosion Allowance (mm)", value=1.5, format="%.2f")
82
 
83
+ joint_method = st.radio("Joint Efficiency Selection", ["Preset (UW-12)", "Manual Entry"])
84
+ if joint_method == "Preset (UW-12)":
85
+ E = st.selectbox("Select E (Joint Efficiency)", [1.0, 0.85, 0.7, 0.65, 0.6, 0.45])
86
+ else:
87
+ E = st.number_input("Manual Joint Efficiency (0-1)", value=0.85, min_value=0.1, max_value=1.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ head_type = st.selectbox("Head Type", ["Ellipsoidal", "Torispherical", "Hemispherical"])
90
+ d_nozzle = st.number_input("Nozzle Diameter (mm)", value=200.0, format="%.1f")
91
+ t_shell = st.number_input("Shell Thickness Provided (mm)", value=12.0, format="%.1f")
92
+ t_nozzle = st.number_input("Nozzle Thickness Provided (mm)", value=10.0, format="%.1f")
93
+ thickness = st.number_input("Governing Thickness (mm)", value=40.0, format="%.1f")
94
 
95
+ if st.button("πŸš€ Run Calculation", use_container_width=True):
96
+ st.session_state.run_done = True
97
+ run_calculation = True
 
 
 
 
98
 
99
+ if st.session_state.run_done:
100
+ st.success("βœ… Calculations completed! See results in the tabs.")
101
 
102
+ else:
103
+ uploaded_file = st.file_uploader("Upload CSV File", type=["csv"])
104
+ if uploaded_file:
105
+ df = pd.read_csv(uploaded_file)
106
+ st.dataframe(df.head())
107
+ if st.button("πŸš€ Run Calculation", use_container_width=True):
108
+ st.session_state.run_done = True
109
+ run_calculation = True
110
+
111
+ if st.session_state.run_done:
112
+ st.success("βœ… Calculations completed! See results in the tabs.")
113
+
114
+ # ========== TABS ==========
115
+ tabs = st.tabs(["Shell", "Head", "Nozzle", "PWHT", "Impact Test", "Summary", "AI Explanation"])
116
 
117
  if st.session_state.run_done:
118
+ # --- SHELL TAB ---
119
  with tabs[0]:
120
+ t_shell_calc = shell_thickness(P, R, S, E, corrosion)
121
+ st.metric("Required Shell Thickness (mm)", f"{t_shell_calc:.2f}")
122
+
123
+ # --- HEAD TAB ---
 
 
 
 
 
124
  with tabs[1]:
125
+ t_head_calc = head_thickness(P, R, S, E, corrosion, head_type)
126
+ st.metric(f"Required {head_type} Head Thickness (mm)", f"{t_head_calc:.2f}")
127
+
128
+ # --- NOZZLE TAB ---
 
 
 
 
 
129
  with tabs[2]:
130
+ safe_nozzle = nozzle_reinforcement(P, d_nozzle, t_shell, t_nozzle, S, E)
131
+ st.write("Nozzle Reinforcement Check:", "βœ… Safe" if safe_nozzle else "❌ Not Safe")
132
+
133
+ # --- PWHT TAB ---
 
 
 
 
 
 
 
 
 
 
134
  with tabs[3]:
135
+ st.write("PWHT Required:", "βœ… Yes" if pwht_required(thickness) else "❌ No")
136
+
137
+ # --- IMPACT TEST TAB ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  with tabs[4]:
139
+ st.write("Impact Test Required:", "βœ… Yes" if impact_test_required(thickness) else "❌ No")
140
+
141
+ # --- SUMMARY TAB ---
142
+ with tabs[5]:
143
+ summary_data = {
144
+ "Shell Thickness": t_shell_calc,
145
+ "Head Thickness": t_head_calc,
146
+ "Nozzle Safe": safe_nozzle,
147
+ "PWHT Required": pwht_required(thickness),
148
+ "Impact Test Required": impact_test_required(thickness),
149
+ }
150
+ df_summary = pd.DataFrame([summary_data])
151
+ st.dataframe(df_summary)
152
+
153
+ # CSV export
154
+ csv = df_summary.to_csv(index=False).encode("utf-8")
155
+ st.download_button("πŸ“₯ Download Results (CSV)", csv, "results.csv")
156
+
157
+ # PDF export
158
+ pdf = PDF()
159
+ pdf.chapter_title("Calculation Summary")
160
+ pdf.chapter_body(str(summary_data))
161
+ pdf_file = "results.pdf"
162
+ pdf.output(pdf_file)
163
+ with open(pdf_file, "rb") as f:
164
+ st.download_button("πŸ“„ Download PDF Report", f, "results.pdf")
165
+
166
+ # --- AI EXPLANATION TAB ---
167
+ with tabs[6]:
168
+ st.markdown("### πŸ€– Ask AI for Explanation")
169
+ if groq_client:
170
+ if st.button("✨ Ask AI", use_container_width=True):
171
+ st.session_state.ai_done = True
172
+ with st.spinner("AI is preparing explanation..."):
173
+ prompt = f"Explain these ASME vessel design results in simple terms: {summary_data}"
174
+ chat_completion = groq_client.chat.completions.create(
175
+ messages=[{"role": "user", "content": prompt}],
176
+ model="llama-3.1-8b-instant",
177
+ )
178
+ explanation = chat_completion.choices[0].message.content
179
+ st.success("βœ… AI Explanation Generated Below")
180
+ st.write(explanation)
181
+
182
+ if st.session_state.ai_done:
183
+ st.info("✨ AI explanation already generated. Rerun to refresh.")
184
+ else:
185
+ st.info("ℹ️ Add your GROQ_API_KEY in Hugging Face secrets to enable AI explanations.")
186
 
187
  else:
188
+ # Placeholders
189
+ for i, msg in enumerate([
190
+ "Shell results", "Head results", "Nozzle results",
191
+ "PWHT decision", "Impact Test decision",
192
+ "Summary", "AI explanation"
193
+ ]):
194
  with tabs[i]:
195
+ st.info(f"Run calculation to see {msg}.")