Umar4321 commited on
Commit
6ee0f96
·
verified ·
1 Parent(s): 42bcf91

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +130 -234
app.py CHANGED
@@ -2,42 +2,32 @@
2
  """
3
  ASME Section VIII — Preliminary Pressure Vessel Calculator (Streamlit)
4
 
5
- This version:
6
- - Makes PDF export optional and robust (sanitizes and wraps text, avoids unbreakable lines).
7
- - Guards against PDF runtime errors and returns None if PDF can't be built.
8
- - Single Run Calculation button; unit system (SI/USC) support.
9
- - Simplified calculations for preliminary checks only. Verify with licensed engineer.
10
  """
11
 
12
  from __future__ import annotations
13
  import os
14
  import math
15
  import datetime
16
- import unicodedata
17
- import textwrap
18
  from typing import Dict, Any, Optional
19
 
20
  import streamlit as st
21
  import pandas as pd
22
  import numpy as np
23
 
24
- # Optional Groq AI client
25
  try:
26
  from groq import Groq
27
  except Exception:
28
  Groq = None
29
 
30
- # Optional fpdf2 PDF generator
31
- try:
32
- from fpdf import FPDF
33
- FPDF_AVAILABLE = True
34
- except Exception:
35
- FPDF = None
36
- FPDF_AVAILABLE = False
37
-
38
- # ----------------------
39
- # Page config + header
40
- # ----------------------
41
  st.set_page_config(page_title="ASME Section VIII Calculator", page_icon="🔧", layout="wide")
42
  st.title("🔧 ASME Section VIII — Preliminary Calculator")
43
  st.caption(
@@ -45,9 +35,9 @@ st.caption(
45
  "and checked against the latest ASME code editions."
46
  )
47
 
48
- # ----------------------
49
- # Unit conversions & constants
50
- # ----------------------
51
  MPA_TO_PSI = 145.037737797
52
  PSI_TO_MPA = 1.0 / MPA_TO_PSI
53
  MM_TO_IN = 0.03937007874015748
@@ -65,23 +55,30 @@ def mm_to_in(x: float) -> float:
65
  def in_to_mm(x: float) -> float:
66
  return float(x) * IN_TO_MM
67
 
68
- # ----------------------
69
- # Calculation helpers (simplified, preliminary)
70
- # ----------------------
71
- def shell_thickness(P: float, R: float, S: float, E: float, corrosion: float) -> float:
72
- """Simplified UG-27-like circumferential thickness (user-supplied consistent units)."""
 
 
 
 
73
  if E <= 0 or E > 1:
74
  raise ValueError("Joint efficiency E must be in (0,1].")
75
  if S <= 0:
76
  raise ValueError("Allowable stress must be positive.")
77
  denom = S * E - 0.6 * P
78
  if denom <= 0:
79
- raise ValueError("Invalid combination: denominator (S*E - 0.6*P) <= 0. Check pressure / S / E.")
80
  t = (P * R) / denom
81
  return float(t + corrosion)
82
 
83
- def head_thickness(P: float, R: float, S: float, E: float, corrosion: float, head_type: str) -> float:
84
- """Simplified head thickness formulas; returns thickness including corrosion allowance."""
 
 
 
85
  if E <= 0 or E > 1:
86
  raise ValueError("Joint efficiency E must be in (0,1].")
87
  if head_type == "Ellipsoidal":
@@ -107,38 +104,38 @@ def head_thickness(P: float, R: float, S: float, E: float, corrosion: float, hea
107
  raise ValueError("Unsupported head type.")
108
  return float(t + corrosion)
109
 
110
- def nozzle_reinforcement_simple(P: float, d: float, t_shell: float, t_nozzle: float, S: float, E: float) -> Dict[str, Any]:
111
  """
112
  Conservative simplified nozzle reinforcement check:
113
  lhs = (P * d) / (2 * S * E)
114
  rhs = t_shell + t_nozzle
115
- Returns diagnostic dict.
116
  """
117
- if any(val <= 0 for val in (d, S, E)):
118
  raise ValueError("Nozzle diameter, allowable stress and joint efficiency must be positive.")
119
- lhs = (P * d) / (2.0 * S * E)
120
  rhs = (t_shell + t_nozzle)
121
  adequate = lhs <= rhs
122
  return {"lhs": float(lhs), "rhs": float(rhs), "adequate": bool(adequate)}
123
 
124
- def pwht_required(thickness: float, unit_system: str = "SI") -> bool:
125
- """Very simplified PWHT rule: SI threshold 38 mm; USC equivalent converted."""
126
  if unit_system == "SI":
127
- return thickness > 38.0
128
  else:
129
- return thickness > mm_to_in(38.0)
130
 
131
- def impact_test_required(thickness: float, design_mdmt: float, unit_system: str = "SI") -> Dict[str, Any]:
132
  """
133
- Simplified MDMT approximation. This is NOT UCS-66 — it's a placeholder for preliminary checks.
134
- Returns dict with impact_required boolean and suggested test temp.
135
  """
136
  if unit_system == "SI":
137
- rated_mdmt = -29.0 # °C approx placeholder
138
  thickness_threshold = 12.0 # mm
139
  suggested_test_temp = design_mdmt - 17.0 # °C
140
  else:
141
- rated_mdmt = -20.0 # °F approx placeholder
142
  thickness_threshold = mm_to_in(12.0) # in
143
  suggested_test_temp = design_mdmt - 30.0 # °F
144
  impact_required = (design_mdmt > rated_mdmt) and (thickness > thickness_threshold)
@@ -147,255 +144,154 @@ def impact_test_required(thickness: float, design_mdmt: float, unit_system: str
147
  "rated_mdmt": rated_mdmt,
148
  "threshold_thickness": thickness_threshold,
149
  "suggested_test_temp": suggested_test_temp,
150
- "note": "APPROXIMATION: use UCS-66 tables for exact rated MDMT decisions.",
151
  }
152
 
153
- # ----------------------
154
- # PDF helper: sanitize & wrap text to avoid fpdf glyph / width issues
155
- # ----------------------
156
- def _sanitize_for_pdf(s: str) -> str:
157
- """Make a string safe for Latin-1 PDF fonts: replace common unicode and normalize."""
158
- if s is None:
159
- return ""
160
- if not isinstance(s, str):
161
- s = str(s)
162
- # Replace characters known to break basic PDF fonts
163
- s = s.replace("—", "-").replace("–", "-")
164
- s = s.replace("“", "\"").replace("”", "\"").replace("‘", "'").replace("’", "'")
165
- # Normalize
166
- s = unicodedata.normalize("NFKD", s)
167
- # Encode to latin-1 and replace unrepresentable characters
168
- s = s.encode("latin-1", "replace").decode("latin-1")
169
- # Collapse very long unbroken sequences by inserting spaces every 120 chars
170
- if len(s) > 120 and " " not in s:
171
- # insert spaces to allow wrapping
172
- parts = [s[i:i+100] for i in range(0, len(s), 100)]
173
- s = " ".join(parts)
174
- return s
175
-
176
- def generate_pdf_bytes_from_dict(info: Dict[str, Any]) -> Optional[bytes]:
177
- """
178
- Build a PDF bytes object from a dict in a safe way.
179
- Returns bytes when successful, otherwise None.
180
- """
181
- if not FPDF_AVAILABLE:
182
- return None
183
-
184
- try:
185
- pdf = FPDF(unit="mm", format="letter")
186
- pdf.set_auto_page_break(auto=True, margin=15)
187
- pdf.add_page()
188
- # Title
189
- pdf.set_font("Helvetica", "B", 14)
190
- title = _sanitize_for_pdf("ASME Section VIII - Summary Report")
191
- pdf.cell(0, 10, title, ln=True, align="C")
192
- pdf.ln(4)
193
- pdf.set_font("Helvetica", size=11)
194
-
195
- # For each key/value, sanitize and wrap to safe width using textwrap
196
- for k, v in info.items():
197
- line = f"{k}: {v}"
198
- safe = _sanitize_for_pdf(line)
199
- # Wrap to roughly 90 characters per line (safe for letter-size with standard margins)
200
- wrapped = textwrap.wrap(safe, width=90)
201
- if not wrapped:
202
- wrapped = [""]
203
- for wl in wrapped:
204
- # Avoid empty-line multi_cell issues by writing at least a space
205
- try:
206
- pdf.multi_cell(0, 7, wl if wl.strip() != "" else " ")
207
- except Exception:
208
- # If writing fails for some line, attempt to write a truncated version
209
- trunc = wl[:250]
210
- try:
211
- pdf.multi_cell(0, 7, trunc if trunc.strip() != "" else " ")
212
- except Exception:
213
- # Give up on PDF generation safely
214
- return None
215
-
216
- # Add generated timestamp
217
- pdf.ln(3)
218
- try:
219
- pdf.set_font("Helvetica", size=9)
220
- gen_line = _sanitize_for_pdf(f"Generated: {datetime.datetime.now().isoformat()}")
221
- pdf.multi_cell(0, 6, gen_line)
222
- except Exception:
223
- pass
224
-
225
- pdf_bytes = pdf.output(dest="S")
226
- # fpdf2 may return str — convert to bytes
227
- if isinstance(pdf_bytes, str):
228
- pdf_bytes = pdf_bytes.encode("latin-1", errors="replace")
229
- return pdf_bytes
230
-
231
- except Exception:
232
- return None
233
-
234
- # ----------------------
235
- # App state defaults
236
- # ----------------------
237
  if "run_done" not in st.session_state:
238
  st.session_state.run_done = False
239
 
240
- # ----------------------
241
- # Sidebar: inputs
242
- # ----------------------
243
  with st.sidebar.expander("Inputs", expanded=True):
244
  unit_system_choice = st.selectbox("Unit System", ["SI (MPa / mm / °C)", "USC (psi / in / °F)"])
245
  use_si = unit_system_choice.startswith("SI")
246
 
247
  st.markdown("---")
248
  if use_si:
249
- P = st.number_input("Design Pressure (MPa)", value=2.0, format="%.3f")
250
- inside_d = st.number_input("Inside Diameter (mm)", value=1500.0, format="%.1f")
251
- R = inside_d / 2.0
252
- S = st.number_input("Allowable Stress (MPa)", value=120.0, format="%.2f")
253
- corrosion = st.number_input("Corrosion Allowance (mm)", value=1.5, format="%.2f")
254
  design_mdmt = st.number_input("Design MDMT (°C)", value=-20.0)
255
  else:
256
- P = st.number_input("Design Pressure (psi)", value=mpa_to_psi(2.0), format="%.1f")
257
- inside_d = st.number_input("Inside Diameter (in)", value=mm_to_in(1500.0), format="%.3f")
258
- R = inside_d / 2.0
259
- S = st.number_input("Allowable Stress (psi)", value=mpa_to_psi(120.0), format="%.1f")
260
- corrosion = st.number_input("Corrosion Allowance (in)", value=mm_to_in(1.5), format="%.4f")
261
  design_mdmt = st.number_input("Design MDMT (°F)", value=-20.0)
262
 
263
  st.markdown("---")
264
  head_type = st.selectbox("Head Type", ["Ellipsoidal", "Torispherical", "Hemispherical"])
265
- E = st.selectbox("Joint Efficiency (E)", [1.0, 0.95, 0.9, 0.85, 0.7], index=0)
266
 
267
  st.markdown("---")
 
268
  if use_si:
269
- d_nozzle = st.number_input("Nozzle Opening Diameter (mm)", value=200.0, format="%.1f")
270
- t_shell_provided = st.number_input("Shell thickness available (mm)", value=12.0, format="%.2f")
271
- t_nozzle = st.number_input("Nozzle thickness (mm)", value=10.0, format="%.2f")
272
  else:
273
- d_nozzle = st.number_input("Nozzle Opening Diameter (in)", value=mm_to_in(200.0), format="%.3f")
274
- t_shell_provided = st.number_input("Shell thickness available (in)", value=mm_to_in(12.0), format="%.4f")
275
- t_nozzle = st.number_input("Nozzle thickness (in)", value=mm_to_in(10.0), format="%.4f")
276
 
277
  st.markdown("---")
278
- run_now = st.button("Run Calculation", help="Click to run calculations now")
279
- if st.button("Reset"):
 
 
 
280
  st.session_state.run_done = False
281
  st.experimental_rerun()
282
 
283
- if 'run_now' in locals() and run_now:
284
  st.session_state.run_done = True
285
 
286
- # ----------------------
287
- # Tabs
288
- # ----------------------
289
  groq_key = os.getenv("GROQ_API_KEY")
290
  ai_enabled = (Groq is not None and bool(groq_key))
291
- tab_labels = ["Shell", "Head", "Nozzle", "PWHT", "Impact Test", "Summary"] + (["AI Explanation"] if ai_enabled else [])
292
- tabs = st.tabs(tab_labels)
293
 
294
- # ----------------------
295
- # Run calculations when requested
296
- # ----------------------
297
  if st.session_state.run_done:
298
- # Shell
299
  with tabs[0]:
300
  try:
301
- t_shell = shell_thickness(P, R, S, E, corrosion)
302
- unit_thk = "mm" if use_si else "in"
303
- st.metric("Required shell thickness (including corrosion)", f"{t_shell:.4f} {unit_thk}")
304
- st.write("Inputs:", {"P": P, "R": R, "S": S, "E": E, "corrosion": corrosion})
305
  except Exception as exc:
306
- st.error(f"Error computing shell thickness: {exc}")
307
 
308
- # Head
309
  with tabs[1]:
310
  try:
311
- t_head = head_thickness(P, R, S, E, corrosion, head_type)
312
- unit_thk = "mm" if use_si else "in"
313
- st.metric(f"Required {head_type} head thickness (including corrosion)", f"{t_head:.4f} {unit_thk}")
314
- st.write("Inputs:", {"P": P, "D (approx)": 2.0 * R, "S": S, "E": E})
315
  except Exception as exc:
316
- st.error(f"Error computing head thickness: {exc}")
317
 
318
- # Nozzle
319
  with tabs[2]:
320
  try:
321
- nozzle_res = nozzle_reinforcement_simple(P, d_nozzle, t_shell_provided, t_nozzle, S, E)
322
- st.write("Conservative nozzle reinforcement check (lhs <= rhs):")
323
- st.write(f"LHS = (P * d) / (2SE) = {nozzle_res['lhs']:.4g}")
324
- st.write(f"RHS = t_shell + t_nozzle = {nozzle_res['rhs']:.4g}")
325
- if nozzle_res['adequate']:
326
  st.success("Conservative nozzle reinforcement check PASSED")
327
  else:
328
  st.error("Conservative nozzle reinforcement check FAILED")
329
- st.caption("Note: This is a conservative/simple check; for full ASME UG-37 use the projection-area method.")
330
  except Exception as exc:
331
- st.error(f"Error computing nozzle reinforcement: {exc}")
332
 
333
- # PWHT
334
  with tabs[3]:
335
  try:
336
- check_thickness = t_shell if "t_shell" in locals() else t_shell_provided
337
- pwht = pwht_required(check_thickness, unit_system="SI" if use_si else "USC")
338
- st.write("PWHT required (preliminary):", "YES" if pwht else "NO")
339
- st.caption("This is a simplified check; consult UCS-56 and your material spec for final decision.")
340
- except Exception as exc:
341
- st.error(f"Error computing PWHT requirement: {exc}")
342
-
343
- # Impact Test
344
- with tabs[4]:
345
- try:
346
- check_thickness = t_shell if "t_shell" in locals() else t_shell_provided
347
- impact_info = impact_test_required(check_thickness, design_mdmt, unit_system="SI" if use_si else "USC")
348
- st.write("Impact test required (preliminary):", "YES" if impact_info["impact_required"] else "NO")
349
  st.write("Rated MDMT used (approx):", impact_info["rated_mdmt"])
350
  st.write("Suggested test temperature (approx):", impact_info["suggested_test_temp"])
351
  st.caption(impact_info["note"])
352
  except Exception as exc:
353
- st.error(f"Error computing impact test requirement: {exc}")
354
 
355
- # Summary + exports
356
- with tabs[5]:
357
  try:
358
  summary = {
359
- "unit_system": "SI (MPa/mm/°C)" if use_si else "USC (psi/in/°F)",
360
- "design_pressure": P,
361
- "inside_diameter": inside_d,
362
- "shell_thickness": t_shell if "t_shell" in locals() else None,
363
- "head_thickness": t_head if "t_head" in locals() else None,
364
- "nozzle_check_passed": nozzle_res["adequate"] if "nozzle_res" in locals() else None,
365
- "pwht_required": pwht if "pwht" in locals() else None,
366
- "impact_test_required": impact_info["impact_required"] if "impact_info" in locals() else None,
367
- "generated": datetime.datetime.now().isoformat(),
368
  }
369
- df_sum = pd.DataFrame([summary])
370
- st.dataframe(df_sum, use_container_width=True)
371
-
372
- # CSV
373
- csv = df_sum.to_csv(index=False).encode("utf-8")
374
- st.download_button("Download CSV", csv, file_name="asme_summary.csv", mime="text/csv")
375
-
376
- # PDF (optional, robust)
377
- if FPDF_AVAILABLE:
378
- pdf_bytes = generate_pdf_bytes_from_dict(summary)
379
- if pdf_bytes:
380
- st.download_button("Download PDF", pdf_bytes, file_name="asme_summary.pdf", mime="application/pdf")
381
- else:
382
- st.info("PDF generation failed (safe fallback). If you need PDF, check fpdf2 installation or try again.")
383
- else:
384
- st.info("PDF export disabled — install 'fpdf2' to enable PDF export.")
385
 
 
 
386
  except Exception as exc:
387
- st.error(f"Error building summary/export: {exc}")
388
 
389
- # AI Explanation tab (optional)
390
  if ai_enabled:
391
  with tabs[-1]:
392
  groq_key = os.getenv("GROQ_API_KEY")
393
  if Groq is not None and groq_key:
394
- try:
395
- groq_client = Groq(api_key=groq_key)
396
- if st.button("Generate AI Explanation"):
397
- with st.spinner("Generating explanation..."):
398
- prompt = f"Explain these ASME results simply: {summary}"
399
  resp = groq_client.chat.completions.create(
400
  messages=[{"role": "user", "content": prompt}],
401
  model="llama-3.1-8b-instant",
@@ -403,13 +299,13 @@ if st.session_state.run_done:
403
  explanation = resp.choices[0].message.content
404
  st.markdown("**AI Explanation**")
405
  st.write(explanation)
406
- except Exception:
407
- st.info("Groq AI not available or request failed. Ensure GROQ_API_KEY is set and 'groq' package is installed.")
408
  else:
409
- st.info("Groq AI not configured. Add GROQ_API_KEY to environment/secrets to enable optional AI explanations.")
410
 
411
  else:
412
- # placeholders
413
- for i in range(len(tab_labels) if 'tab_labels' in locals() else 6):
414
  with tabs[i]:
415
- st.info("Enter inputs in the sidebar and click 'Run Calculation' to execute.")
 
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
+ - Optional Groq AI explanation is still available if 'groq' is installed and GROQ_API_KEY is set.
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
 
12
  from __future__ import annotations
13
  import os
14
  import math
15
  import datetime
 
 
16
  from typing import Dict, Any, Optional
17
 
18
  import streamlit as st
19
  import pandas as pd
20
  import numpy as np
21
 
22
+ # Optional Groq AI client (guarded)
23
  try:
24
  from groq import Groq
25
  except Exception:
26
  Groq = None
27
 
28
+ # -----------------------------
29
+ # Page config and header
30
+ # -----------------------------
 
 
 
 
 
 
 
 
31
  st.set_page_config(page_title="ASME Section VIII Calculator", page_icon="🔧", layout="wide")
32
  st.title("🔧 ASME Section VIII — Preliminary Calculator")
33
  st.caption(
 
35
  "and checked against the latest ASME code editions."
36
  )
37
 
38
+ # -----------------------------
39
+ # Unit conversion helpers & constants
40
+ # -----------------------------
41
  MPA_TO_PSI = 145.037737797
42
  PSI_TO_MPA = 1.0 / MPA_TO_PSI
43
  MM_TO_IN = 0.03937007874015748
 
55
  def in_to_mm(x: float) -> float:
56
  return float(x) * IN_TO_MM
57
 
58
+ # -----------------------------
59
+ # Calculation helpers (pure functions)
60
+ # -----------------------------
61
+ def calculate_shell_thickness(P: float, R: float, S: float, E: float, corrosion: float) -> float:
62
+ """
63
+ Simplified UG-27 circumferential thickness formula (preliminary).
64
+ Inputs must use consistent units (user-selected system).
65
+ Returns thickness including corrosion allowance.
66
+ """
67
  if E <= 0 or E > 1:
68
  raise ValueError("Joint efficiency E must be in (0,1].")
69
  if S <= 0:
70
  raise ValueError("Allowable stress must be positive.")
71
  denom = S * E - 0.6 * P
72
  if denom <= 0:
73
+ raise ValueError("Invalid combination: denominator (S*E - 0.6*P) <= 0. Check inputs.")
74
  t = (P * R) / denom
75
  return float(t + corrosion)
76
 
77
+ def calculate_head_thickness(P: float, R: float, S: float, E: float, corrosion: float, head_type: str) -> float:
78
+ """
79
+ Simplified UG-32 style head thickness formulas (preliminary).
80
+ head_type: 'Ellipsoidal', 'Torispherical', 'Hemispherical'
81
+ """
82
  if E <= 0 or E > 1:
83
  raise ValueError("Joint efficiency E must be in (0,1].")
84
  if head_type == "Ellipsoidal":
 
104
  raise ValueError("Unsupported head type.")
105
  return float(t + corrosion)
106
 
107
+ def nozzle_reinforcement_check(P: float, d_opening: float, t_shell: float, t_nozzle: float, S: float, E: float) -> Dict[str, Any]:
108
  """
109
  Conservative simplified nozzle reinforcement check:
110
  lhs = (P * d) / (2 * S * E)
111
  rhs = t_shell + t_nozzle
112
+ Returns diagnostic dict {lhs, rhs, adequate}.
113
  """
114
+ if any(val <= 0 for val in (d_opening, S, E)):
115
  raise ValueError("Nozzle diameter, allowable stress and joint efficiency must be positive.")
116
+ lhs = (P * d_opening) / (2.0 * S * E)
117
  rhs = (t_shell + t_nozzle)
118
  adequate = lhs <= rhs
119
  return {"lhs": float(lhs), "rhs": float(rhs), "adequate": bool(adequate)}
120
 
121
+ def pwht_decision(thickness: float, unit_system: str = "SI") -> bool:
122
+ """Very simplified PWHT rule (preliminary)."""
123
  if unit_system == "SI":
124
+ return thickness > 38.0 # mm
125
  else:
126
+ return thickness > mm_to_in(38.0) # in
127
 
128
+ def impact_test_decision(thickness: float, design_mdmt: float, unit_system: str = "SI") -> Dict[str, Any]:
129
  """
130
+ Simplified MDMT/impact test check (placeholder).
131
+ This is NOT a replacement for UCS-66. It's for preliminary checks only.
132
  """
133
  if unit_system == "SI":
134
+ rated_mdmt = -29.0 # °C placeholder
135
  thickness_threshold = 12.0 # mm
136
  suggested_test_temp = design_mdmt - 17.0 # °C
137
  else:
138
+ rated_mdmt = -20.0 # °F placeholder
139
  thickness_threshold = mm_to_in(12.0) # in
140
  suggested_test_temp = design_mdmt - 30.0 # °F
141
  impact_required = (design_mdmt > rated_mdmt) and (thickness > thickness_threshold)
 
144
  "rated_mdmt": rated_mdmt,
145
  "threshold_thickness": thickness_threshold,
146
  "suggested_test_temp": suggested_test_temp,
147
+ "note": "APPROXIMATION use UCS-66 for final decisions.",
148
  }
149
 
150
+ # -----------------------------
151
+ # Session defaults & sidebar UI
152
+ # -----------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  if "run_done" not in st.session_state:
154
  st.session_state.run_done = False
155
 
 
 
 
156
  with st.sidebar.expander("Inputs", expanded=True):
157
  unit_system_choice = st.selectbox("Unit System", ["SI (MPa / mm / °C)", "USC (psi / in / °F)"])
158
  use_si = unit_system_choice.startswith("SI")
159
 
160
  st.markdown("---")
161
  if use_si:
162
+ design_pressure = st.number_input("Design Pressure (MPa)", value=2.0, format="%.3f")
163
+ inside_diameter = st.number_input("Inside Diameter (mm)", value=1500.0, format="%.1f")
164
+ R = inside_diameter / 2.0
165
+ allowable_stress = st.number_input("Allowable Stress (MPa)", value=120.0, format="%.2f")
166
+ corrosion_allowance = st.number_input("Corrosion Allowance (mm)", value=1.5, format="%.2f")
167
  design_mdmt = st.number_input("Design MDMT (°C)", value=-20.0)
168
  else:
169
+ design_pressure = st.number_input("Design Pressure (psi)", value=mpa_to_psi(2.0), format="%.1f")
170
+ inside_diameter = st.number_input("Inside Diameter (in)", value=mm_to_in(1500.0), format="%.3f")
171
+ R = inside_diameter / 2.0
172
+ allowable_stress = st.number_input("Allowable Stress (psi)", value=mpa_to_psi(120.0), format="%.1f")
173
+ corrosion_allowance = st.number_input("Corrosion Allowance (in)", value=mm_to_in(1.5), format="%.4f")
174
  design_mdmt = st.number_input("Design MDMT (°F)", value=-20.0)
175
 
176
  st.markdown("---")
177
  head_type = st.selectbox("Head Type", ["Ellipsoidal", "Torispherical", "Hemispherical"])
178
+ joint_eff = st.selectbox("Joint Efficiency (E)", [1.0, 0.95, 0.9, 0.85, 0.7], index=0)
179
 
180
  st.markdown("---")
181
+ # Nozzle inputs (requesting the required inputs)
182
  if use_si:
183
+ nozzle_opening_diameter = st.number_input("Nozzle Opening Diameter (mm)", value=200.0, format="%.1f")
184
+ nozzle_wall_thickness = st.number_input("Nozzle Wall Thickness (mm)", value=10.0, format="%.2f")
185
+ shell_thickness_available = st.number_input("Shell thickness available (mm)", value=12.0, format="%.2f")
186
  else:
187
+ nozzle_opening_diameter = st.number_input("Nozzle Opening Diameter (in)", value=mm_to_in(200.0), format="%.3f")
188
+ nozzle_wall_thickness = st.number_input("Nozzle Wall Thickness (in)", value=mm_to_in(10.0), format="%.4f")
189
+ shell_thickness_available = st.number_input("Shell thickness available (in)", value=mm_to_in(12.0), format="%.4f")
190
 
191
  st.markdown("---")
192
+ # Single run and reset
193
+ run_calc = st.button("Run Calculation")
194
+ reset = st.button("Reset to defaults")
195
+ if reset:
196
+ # quick reset: clear session and reload page
197
  st.session_state.run_done = False
198
  st.experimental_rerun()
199
 
200
+ if run_calc:
201
  st.session_state.run_done = True
202
 
203
+ # -----------------------------
204
+ # Tabs and results
205
+ # -----------------------------
206
  groq_key = os.getenv("GROQ_API_KEY")
207
  ai_enabled = (Groq is not None and bool(groq_key))
208
+ tab_list = ["Shell", "Head", "Nozzle", "PWHT & Impact", "Summary"] + (["AI Explanation"] if ai_enabled else [])
209
+ tabs = st.tabs(tab_list)
210
 
 
 
 
211
  if st.session_state.run_done:
212
+ # Shell tab
213
  with tabs[0]:
214
  try:
215
+ shell_res = calculate_shell_thickness(design_pressure, R, allowable_stress, joint_eff, corrosion_allowance)
216
+ units_len = "mm" if use_si else "in"
217
+ st.metric("Required Shell Thickness (incl. corrosion)", f"{shell_res:.4f} {units_len}")
218
+ st.write("Inputs:", {"P": design_pressure, "R": R, "S": allowable_stress, "E": joint_eff})
219
  except Exception as exc:
220
+ st.error(f"Shell calculation error: {exc}")
221
 
222
+ # Head tab
223
  with tabs[1]:
224
  try:
225
+ head_res = calculate_head_thickness(design_pressure, R, allowable_stress, joint_eff, corrosion_allowance, head_type)
226
+ units_len = "mm" if use_si else "in"
227
+ st.metric(f"Required {head_type} Head Thickness (incl. corrosion)", f"{head_res:.4f} {units_len}")
228
+ st.write("Intermediate values: D ", 2.0 * R)
229
  except Exception as exc:
230
+ st.error(f"Head calculation error: {exc}")
231
 
232
+ # Nozzle tab
233
  with tabs[2]:
234
  try:
235
+ nozzle_check = nozzle_reinforcement_check(design_pressure, nozzle_opening_diameter, shell_thickness_available, nozzle_wall_thickness, allowable_stress, joint_eff)
236
+ st.write("Nozzle reinforcement conservative check:")
237
+ st.write(f"LHS = (P * d) / (2SE) = {nozzle_check['lhs']:.6g}")
238
+ st.write(f"RHS = t_shell + t_nozzle = {nozzle_check['rhs']:.6g}")
239
+ if nozzle_check["adequate"]:
240
  st.success("Conservative nozzle reinforcement check PASSED")
241
  else:
242
  st.error("Conservative nozzle reinforcement check FAILED")
243
+ st.caption("This is a simplified, conservative approximation of UG-37. Use full UG-37 projection-area method for final design.")
244
  except Exception as exc:
245
+ st.error(f"Nozzle calculation error: {exc}")
246
 
247
+ # PWHT & Impact tab
248
  with tabs[3]:
249
  try:
250
+ thickness_for_checks = shell_res if "shell_res" in locals() else shell_thickness_available
251
+ pwht_flag = pwht_decision(thickness_for_checks, unit_system="SI" if use_si else "USC")
252
+ impact_info = impact_test_decision(thickness_for_checks, design_mdmt, unit_system="SI" if use_si else "USC")
253
+ st.subheader("PWHT (Preliminary)")
254
+ st.write("PWHT required:", "YES" if pwht_flag else "NO")
255
+ st.subheader("Impact Test (MDMT) - Preliminary")
256
+ st.write("Impact test required:", "YES" if impact_info["impact_required"] else "NO")
 
 
 
 
 
 
257
  st.write("Rated MDMT used (approx):", impact_info["rated_mdmt"])
258
  st.write("Suggested test temperature (approx):", impact_info["suggested_test_temp"])
259
  st.caption(impact_info["note"])
260
  except Exception as exc:
261
+ st.error(f"PWHT/Impact calculation error: {exc}")
262
 
263
+ # Summary tab
264
+ with tabs[4]:
265
  try:
266
  summary = {
267
+ "Unit System": "SI (MPa/mm/°C)" if use_si else "USC (psi/in/°F)",
268
+ "Design Pressure": design_pressure,
269
+ "Inside Diameter": inside_diameter,
270
+ "Shell Required Thickness": shell_res if "shell_res" in locals() else None,
271
+ "Head Required Thickness": head_res if "head_res" in locals() else None,
272
+ "Nozzle Check Passed": nozzle_check["adequate"] if "nozzle_check" in locals() else None,
273
+ "PWHT Required": pwht_flag if "pwht_flag" in locals() else None,
274
+ "Impact Test Required": impact_info["impact_required"] if "impact_info" in locals() else None,
275
+ "Generated": datetime.datetime.now().isoformat(),
276
  }
277
+ df_summary = pd.DataFrame([summary])
278
+ st.dataframe(df_summary, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
+ csv_bytes = df_summary.to_csv(index=False).encode("utf-8")
281
+ st.download_button("Download Summary CSV", csv_bytes, file_name="asme_summary.csv", mime="text/csv")
282
  except Exception as exc:
283
+ st.error(f"Summary building error: {exc}")
284
 
285
+ # AI Explanation (optional)
286
  if ai_enabled:
287
  with tabs[-1]:
288
  groq_key = os.getenv("GROQ_API_KEY")
289
  if Groq is not None and groq_key:
290
+ groq_client = Groq(api_key=groq_key)
291
+ if st.button("Generate AI Explanation"):
292
+ with st.spinner("Asking AI..."):
293
+ prompt = f"Explain these preliminary ASME vessel results simply: {summary}"
294
+ try:
295
  resp = groq_client.chat.completions.create(
296
  messages=[{"role": "user", "content": prompt}],
297
  model="llama-3.1-8b-instant",
 
299
  explanation = resp.choices[0].message.content
300
  st.markdown("**AI Explanation**")
301
  st.write(explanation)
302
+ except Exception as e:
303
+ st.error(f"AI request failed: {e}")
304
  else:
305
+ st.info("Groq AI not available. Add GROQ_API_KEY and install 'groq' to enable.")
306
 
307
  else:
308
+ # tabs placeholders
309
+ for i, lbl in enumerate(tab_list):
310
  with tabs[i]:
311
+ st.info("Set inputs in the sidebar and click 'Run Calculation' to execute.")