Umar4321 commited on
Commit
c8107d1
·
verified ·
1 Parent(s): ba5ed52

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +250 -248
app.py CHANGED
@@ -1,357 +1,359 @@
1
  #!/usr/bin/env python3
2
  """
3
- ASME Calculator (updated)add Unit System (USC / SI) support.
4
 
5
- Notes:
6
- - Calculation functions assume consistent units (i.e. pressure and allowable stress in matching systems;
7
- lengths all in the same linear unit). This app ensures the user supplies values in a single system.
8
- - If you want the app to accept mixed units and convert internally, we can add that later.
 
 
9
  """
10
 
11
  from __future__ import annotations
12
  import os
13
  import math
14
  import datetime
15
- from typing import Dict, Any
16
 
17
  import streamlit as st
18
  import pandas as pd
19
  import numpy as np
20
 
21
- # PDF generator (fpdf2)
22
- from fpdf import FPDF
 
 
 
23
 
24
- # Groq client (optional - requires GROQ_API_KEY in environment or HF secrets)
25
  try:
26
- from groq import Groq
 
 
27
  except Exception:
28
- Groq = None # groq may not be installed or env var not set
29
-
30
- # ========== PAGE CONFIG ==========
31
- st.set_page_config(page_title="ASME Calculator", layout="wide")
32
- st.title("🛠️ ASME CALCULATOR")
33
- st.caption("Preliminary ASME Section VIII calculations. Verify with a licensed engineer and latest ASME edition.")
34
-
35
- # ========== API CLIENT ==========
36
- groq_api_key = os.getenv("GROQ_API_KEY")
37
- groq_client = Groq(api_key=groq_api_key) if (Groq is not None and groq_api_key) else None
38
-
39
- # ========== PDF GENERATOR ==========
40
- class PDF(FPDF):
41
- def header(self):
42
- self.set_font("Helvetica", "B", 14)
43
- self.cell(0, 10, "ASME VIII Div.1 Vessel Design Report", 0, 1, "C")
44
-
45
- def chapter_title(self, title):
46
- self.set_font("Helvetica", "B", 12)
47
- self.cell(0, 10, title, 0, 1, "L")
48
-
49
- def chapter_body(self, body):
50
- self.set_font("Helvetica", "", 11)
51
- self.multi_cell(0, 8, body)
52
-
53
- # ========== UNIT HELPERS ==========
54
- # Conversion factors
55
- MPA_TO_PSI = 145.037737797 # 1 MPa = 145.0377 psi
56
- MM_TO_IN = 0.03937007874015748 # 1 mm = 0.0393701 in
57
- IN_TO_MM = 25.4
58
  PSI_TO_MPA = 1.0 / MPA_TO_PSI
 
 
59
 
60
- def mpato_psi(x: float) -> float:
61
- return x * MPA_TO_PSI
62
 
63
  def psi_to_mpa(x: float) -> float:
64
- return x * PSI_TO_MPA
65
 
66
  def mm_to_in(x: float) -> float:
67
- return x * MM_TO_IN
68
 
69
  def in_to_mm(x: float) -> float:
70
- return x * IN_TO_MM
71
 
72
- # ========== CALCULATION FUNCTIONS ==========
73
  def shell_thickness(P: float, R: float, S: float, E: float, corrosion: float) -> float:
74
- """Return governing shell thickness (uses circumferential formula as originally)."""
75
- # Validation (basic)
 
 
76
  if E <= 0 or E > 1:
77
  raise ValueError("Joint efficiency E must be in (0,1].")
78
  if S <= 0:
79
  raise ValueError("Allowable stress must be positive.")
80
- # Use the formula as provided originally (assumes P, R, S in consistent units)
81
  denom = S * E - 0.6 * P
82
  if denom <= 0:
83
- # Avoid division by zero; return a large thickness suggestion or raise
84
- raise ValueError("Denominator (S*E - 0.6*P) <= 0. Check input values (pressure too high or S/E too low).")
85
  t = (P * R) / denom
86
- return t + corrosion
87
 
88
  def head_thickness(P: float, R: float, S: float, E: float, corrosion: float, head_type: str) -> float:
89
- """Return head thickness per simplified formulas (unit-consistent)."""
 
 
 
90
  if E <= 0 or E > 1:
91
  raise ValueError("Joint efficiency E must be in (0,1].")
92
  if head_type == "Ellipsoidal":
93
- denom = S * E - 0.1 * P
94
  if denom <= 0:
95
- raise ValueError("Denominator invalid for Ellipsoidal head formula.")
96
- return (0.5 * P * R) / denom + corrosion
97
  elif head_type == "Torispherical":
98
- denom = S * E - 0.1 * P
 
 
 
99
  if denom <= 0:
100
- raise ValueError("Denominator invalid for Torispherical head formula.")
101
- return (0.885 * P * R) / denom + corrosion
102
  elif head_type == "Hemispherical":
 
103
  denom = 2 * S * E - 0.2 * P
104
  if denom <= 0:
105
- raise ValueError("Denominator invalid for Hemispherical head formula.")
106
- return (P * R) / denom + corrosion
107
  else:
108
  raise ValueError("Unsupported head type.")
 
109
 
110
- def nozzle_reinforcement(P: float, d: float, t_shell: float, t_nozzle: float, S: float, E: float) -> bool:
111
  """
112
- Simplified conservative check originally provided:
113
- (P * d) / (2 * S * E) <= (t_shell + t_nozzle)
114
- Works if units are consistent.
115
  """
116
- lhs = (P * d) / (2 * S * E)
 
 
117
  rhs = (t_shell + t_nozzle)
118
- return lhs <= rhs
 
119
 
120
- def pwht_required(thickness: float, material: str = "CS", unit_system: str = "SI") -> bool:
121
  """
122
- Very simple rule kept from original:
123
- In original code thickness threshold 38 (assumed mm). We'll adapt threshold by unit system:
124
- - SI: 38 mm threshold (original)
125
- - USC: convert 38 mm to inches (~1.496 in)
126
  """
127
  if unit_system == "SI":
128
- threshold = 38.0
129
  else:
130
- threshold = mm_to_in(38.0)
131
- return (material == "CS") and (thickness > threshold)
132
 
133
- def impact_test_required(thickness: float, MDMT: float = -20.0, material: str = "CS", unit_system: str = "SI") -> bool:
134
  """
135
- Original logic was nonspecific; we keep the original behaviour but map units consistently.
136
- The original check: material == CS and (MDMT < -29 and thickness > 12)
137
- - 12 assumed mm in original code.
138
- We'll adapt thresholds per unit_system:
139
- - SI: thickness threshold = 12 mm (original)
140
- - USC: convert 12 mm to inches (~0.4724 in)
141
- - MDMT comparisons assumed in °C in original? Original used -29 which looks like °C (or °F?). The original code
142
- mixed units; we'll treat MDMT input as per user's unit system and compare raw values exactly as original.
143
- WARNING: This is a placeholder approximation. For real MDMT logic use UCS-66 tables.
144
  """
145
  if unit_system == "SI":
 
146
  thickness_threshold = 12.0 # mm
147
- mdmt_check_val = -29.0
148
  else:
149
- thickness_threshold = mm_to_in(12.0)
150
- mdmt_check_val = -29.0 # user will supply °F when using USC; this retains same numeric test as original.
151
- return (material == "CS") and (MDMT < mdmt_check_val and thickness > thickness_threshold)
152
-
153
- # ========== SESSION STATE ==========
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  if "run_done" not in st.session_state:
155
  st.session_state.run_done = False
156
- if "ai_done" not in st.session_state:
157
- st.session_state.ai_done = False
158
-
159
- # ========== SIDEBAR INPUTS ==========
160
- with st.sidebar.expander("📥 Manual Design Inputs", expanded=True):
161
- # Unit System selection (new)
162
- unit_system = st.radio("Unit System:", ["SI (MPa / mm)", "USC (psi / in)"], index=0)
163
- use_si = unit_system.startswith("SI")
164
 
165
- input_mode = st.radio("Input Mode:", ["Manual Entry", "Upload CSV"])
166
- run_calculation = False
 
 
167
 
168
- # Set sensible defaults depending on unit system (based on previous defaults)
169
  if use_si:
170
- default_P = 2.0 # MPa (approx previous)
171
- default_R = 1000.0 # mm (previous)
172
- default_S = 120.0 # MPa (previous)
173
- default_corrosion = 1.5 # mm
174
- default_d_nozzle = 200.0 # mm
175
- default_t_shell = 12.0 # mm
176
- default_t_nozzle = 10.0 # mm
177
- default_thickness = 40.0 # mm
178
- default_mdmt = -20.0 # °C
179
  else:
180
- # convert defaults to USC
181
- default_P = mpato_psi(2.0) # psi
182
- default_R = mm_to_in(1000.0) # in
183
- default_S = mpato_psi(120.0) # psi
184
- default_corrosion = mm_to_in(1.5) # in
185
- default_d_nozzle = mm_to_in(200.0) # in
186
- default_t_shell = mm_to_in(12.0) # in
187
- default_t_nozzle = mm_to_in(10.0) # in
188
- default_thickness = mm_to_in(40.0) # in
189
- default_mdmt = mm_to_in(-20.0) if False else -20.0 # MDMT keep numeric, user interprets units (°F)
190
-
191
- if input_mode == "Manual Entry":
192
- # Labels and formats adjusted per unit system
193
- if use_si:
194
- P = st.number_input("Design Pressure (MPa)", value=float(default_P), format="%.3f")
195
- R = st.number_input("Internal Radius (mm)", value=float(default_R), format="%.2f")
196
- S = st.number_input("Allowable Stress (MPa)", value=float(default_S), format="%.2f")
197
- corrosion = st.number_input("Corrosion Allowance (mm)", value=float(default_corrosion), format="%.2f")
198
- mdmt = st.number_input("Design MDMT (°C)", value=float(default_mdmt))
199
- else:
200
- P = st.number_input("Design Pressure (psi)", value=float(default_P), format="%.2f")
201
- R = st.number_input("Internal Radius (in)", value=float(default_R), format="%.3f")
202
- S = st.number_input("Allowable Stress (psi)", value=float(default_S), format="%.1f")
203
- corrosion = st.number_input("Corrosion Allowance (in)", value=float(default_corrosion), format="%.3f")
204
- mdmt = st.number_input("Design MDMT (°F)", value=float(default_mdmt))
205
-
206
- joint_method = st.radio("Joint Efficiency Selection", ["Preset (UW-12)", "Manual Entry"])
207
- if joint_method == "Preset (UW-12)":
208
- # include the requested 0.7 option
209
- E = st.selectbox("Select E (Joint Efficiency)", [1.0, 0.95, 0.9, 0.85, 0.7, 0.65, 0.6, 0.45], index=0)
210
- else:
211
- E = st.number_input("Manual Joint Efficiency (0-1)", value=0.85, min_value=0.1, max_value=1.0, format="%.2f")
212
-
213
- head_type = st.selectbox("Head Type", ["Ellipsoidal", "Torispherical", "Hemispherical"])
214
-
215
- if use_si:
216
- d_nozzle = st.number_input("Nozzle Diameter (mm)", value=float(default_d_nozzle), format="%.1f")
217
- t_shell = st.number_input("Shell Thickness Provided (mm)", value=float(default_t_shell), format="%.2f")
218
- t_nozzle = st.number_input("Nozzle Thickness Provided (mm)", value=float(default_t_nozzle), format="%.2f")
219
- thickness = st.number_input("Governing Thickness (mm)", value=float(default_thickness), format="%.2f")
220
- else:
221
- d_nozzle = st.number_input("Nozzle Diameter (in)", value=float(default_d_nozzle), format="%.3f")
222
- t_shell = st.number_input("Shell Thickness Provided (in)", value=float(default_t_shell), format="%.4f")
223
- t_nozzle = st.number_input("Nozzle Thickness Provided (in)", value=float(default_t_nozzle), format="%.4f")
224
- thickness = st.number_input("Governing Thickness (in)", value=float(default_thickness), format="%.4f")
225
-
226
- if st.button("🚀 Run Calculation", use_container_width=True):
227
- st.session_state.run_done = True
228
- run_calculation = True
229
-
230
- if st.session_state.run_done:
231
- st.success("✅ Calculations completed! See results in the tabs.")
232
-
233
  else:
234
- uploaded_file = st.file_uploader("Upload CSV File", type=["csv"])
235
- if uploaded_file:
236
- df = pd.read_csv(uploaded_file)
237
- st.dataframe(df.head())
238
- if st.button("🚀 Run Calculation", use_container_width=True):
239
- # NOTE: CSV processing / mapping not implemented here — expecting columns that match manual inputs
240
- st.session_state.run_done = True
241
- run_calculation = True
242
 
243
- if st.session_state.run_done:
244
- st.success(" Calculations completed! See results in the tabs.")
 
245
 
246
- # ========== TABS ==========
247
- tabs = st.tabs(["Shell", "Head", "Nozzle", "PWHT", "Impact Test", "Summary", "AI Explanation"])
248
 
249
  if st.session_state.run_done:
250
- # --- SHELL TAB ---
251
  with tabs[0]:
252
  try:
253
- t_shell_calc = shell_thickness(P, R, S, E, corrosion)
254
- unit_thickness = "mm" if use_si else "in"
255
- st.metric(f"Required Shell Thickness ({unit_thickness})", f"{t_shell_calc:.4f}")
 
 
256
  except Exception as exc:
257
  st.error(f"Error computing shell thickness: {exc}")
258
 
259
- # --- HEAD TAB ---
260
  with tabs[1]:
261
  try:
262
- t_head_calc = head_thickness(P, R, S, E, corrosion, head_type)
263
- unit_thickness = "mm" if use_si else "in"
264
- st.metric(f"Required {head_type} Head Thickness ({unit_thickness})", f"{t_head_calc:.4f}")
 
 
265
  except Exception as exc:
266
  st.error(f"Error computing head thickness: {exc}")
267
 
268
- # --- NOZZLE TAB ---
269
  with tabs[2]:
270
  try:
271
- safe_nozzle = nozzle_reinforcement(P, d_nozzle, t_shell, t_nozzle, S, E)
272
- st.write("Nozzle Reinforcement Check:", "✅ Safe" if safe_nozzle else "❌ Not Safe")
273
- st.write("Note: This is a simplified conservative check. For full UG-37 use geometric projection method.")
 
 
 
 
 
 
 
274
  except Exception as exc:
275
  st.error(f"Error computing nozzle reinforcement: {exc}")
276
 
277
- # --- PWHT TAB ---
278
  with tabs[3]:
279
  try:
280
- pwht_ans = pwht_required(thickness, material="CS", unit_system="SI" if use_si else "USC")
281
- st.write("PWHT Required:", "✅ Yes" if pwht_ans else "❌ No")
282
- if use_si:
283
- st.caption("Threshold used: 38 mm (approx).")
284
- else:
285
- st.caption(f"Threshold used: {mpato_psi(0):.4f} (converted threshold from 38 mm).")
286
  except Exception as exc:
287
  st.error(f"Error computing PWHT requirement: {exc}")
288
 
289
- # --- IMPACT TEST TAB ---
290
  with tabs[4]:
291
  try:
292
- impact_ans = impact_test_required(thickness, MDMT=mdmt, material="CS", unit_system="SI" if use_si else "USC")
293
- st.write("Impact Test Required:", "✅ Yes" if impact_ans else "❌ No")
294
- st.caption("This is a placeholder approximation. Consult UCS-66 for precise MDMT rules.")
 
 
 
295
  except Exception as exc:
296
  st.error(f"Error computing impact test requirement: {exc}")
297
 
298
- # --- SUMMARY TAB ---
299
  with tabs[5]:
300
  try:
301
- summary_data = {
302
- "Shell Thickness": t_shell_calc,
303
- "Head Thickness": t_head_calc,
304
- "Nozzle Safe": safe_nozzle,
305
- "PWHT Required": pwht_required(thickness, material="CS", unit_system="SI" if use_si else "USC"),
306
- "Impact Test Required": impact_test_required(thickness, MDMT=mdmt, material="CS", unit_system="SI" if use_si else "USC"),
307
- "Unit System": "SI (MPa/mm)" if use_si else "USC (psi/in)",
 
 
 
308
  }
309
- df_summary = pd.DataFrame([summary_data])
310
- st.dataframe(df_summary)
311
-
312
- # CSV export
313
- csv = df_summary.to_csv(index=False).encode("utf-8")
314
- st.download_button("📥 Download Results (CSV)", csv, "results.csv")
315
-
316
- # PDF export
317
- pdf = PDF()
318
- pdf.chapter_title("Calculation Summary")
319
- pdf.chapter_body(str(summary_data))
320
- pdf_file = "results.pdf"
321
- pdf.output(pdf_file)
322
- with open(pdf_file, "rb") as f:
323
- st.download_button("📄 Download PDF Report", f, "results.pdf")
324
  except Exception as exc:
325
- st.error(f"Error generating summary: {exc}")
326
-
327
- # --- AI EXPLANATION TAB ---
328
- with tabs[6]:
329
- st.markdown("### 🤖 Ask AI for Explanation")
330
- if groq_client:
331
- if st.button("✨ Ask AI", use_container_width=True):
332
- st.session_state.ai_done = True
333
- with st.spinner("AI is preparing explanation..."):
334
- prompt = f"Explain these ASME vessel design results in simple terms: {summary_data}"
335
- try:
336
- chat_completion = groq_client.chat.completions.create(
 
337
  messages=[{"role": "user", "content": prompt}],
338
  model="llama-3.1-8b-instant",
339
  )
340
- explanation = chat_completion.choices[0].message.content
341
- st.success("AI Explanation Generated Below")
342
  st.write(explanation)
343
- except Exception as exc:
344
- st.error(f"AI request failed: {exc}")
345
- if st.session_state.ai_done:
346
- st.info("✨ AI explanation already generated. Rerun to refresh.")
347
  else:
348
- st.info("ℹ️ Add your GROQ_API_KEY in Hugging Face secrets to enable AI explanations.")
349
  else:
350
- # Placeholders if not run
351
- for i, msg in enumerate([
352
- "Shell results", "Head results", "Nozzle results",
353
- "PWHT decision", "Impact Test decision",
354
- "Summary", "AI explanation"
355
- ]):
356
  with tabs[i]:
357
- st.info(f"Run calculation to see {msg}.")
 
1
  #!/usr/bin/env python3
2
  """
3
+ ASME Section VIIIPreliminary Pressure Vessel Calculator (Streamlit)
4
 
5
+ This version guards the PDF generator import so the app does NOT crash if 'fpdf2' is not installed.
6
+ PDF export remains available when 'fpdf2' is present; otherwise the app offers CSV export and a friendly message.
7
+
8
+ Note:
9
+ - Groq AI integration is optional and guarded (requires GROQ_API_KEY in environment / HF secrets).
10
+ - Calculations expect consistent units (you choose SI or USC in the sidebar). The app labels inputs accordingly.
11
  """
12
 
13
  from __future__ import annotations
14
  import os
15
  import math
16
  import datetime
17
+ from typing import Dict, Any, Optional
18
 
19
  import streamlit as st
20
  import pandas as pd
21
  import numpy as np
22
 
23
+ # Optional AI client (Groq). If not installed or key not provided, AI features are disabled.
24
+ try:
25
+ from groq import Groq # optional
26
+ except Exception:
27
+ Groq = None
28
 
29
+ # Optional PDF generator (fpdf2). Guard import to avoid ModuleNotFoundError.
30
  try:
31
+ from fpdf import FPDF
32
+
33
+ FPDF_AVAILABLE = True
34
  except Exception:
35
+ FPDF = None
36
+ FPDF_AVAILABLE = False
37
+
38
+ from io import BytesIO
39
+
40
+ # --------- Page config ----------
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(
44
+ "Preliminary calculations only. Final verification must be completed by a licensed professional engineer "
45
+ "and checked against the latest ASME code editions."
46
+ )
47
+
48
+ # --------- Conversions & constants ----------
49
+ MPA_TO_PSI = 145.037737797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  PSI_TO_MPA = 1.0 / MPA_TO_PSI
51
+ MM_TO_IN = 0.03937007874015748
52
+ IN_TO_MM = 25.4
53
 
54
+ def mpa_to_psi(x: float) -> float:
55
+ return float(x) * MPA_TO_PSI
56
 
57
  def psi_to_mpa(x: float) -> float:
58
+ return float(x) * PSI_TO_MPA
59
 
60
  def mm_to_in(x: float) -> float:
61
+ return float(x) * MM_TO_IN
62
 
63
  def in_to_mm(x: float) -> float:
64
+ return float(x) * IN_TO_MM
65
 
66
+ # --------- Simple calculation helpers ----------
67
  def shell_thickness(P: float, R: float, S: float, E: float, corrosion: float) -> float:
68
+ """
69
+ Simple UG-27-like circumferential thickness calculation (keeps original app's simplified logic).
70
+ Requires inputs in consistent units (e.g., SI: MPa, mm; USC: psi, in).
71
+ """
72
  if E <= 0 or E > 1:
73
  raise ValueError("Joint efficiency E must be in (0,1].")
74
  if S <= 0:
75
  raise ValueError("Allowable stress must be positive.")
76
+ # Avoid division by zero: use the conservative branch if denominator non-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 / allowable stress / 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
+ """
85
+ Simplified head thickness formulas; return thickness including corrosion allowance.
86
+ head_type: "Ellipsoidal", "Torispherical", "Hemispherical"
87
+ """
88
  if E <= 0 or E > 1:
89
  raise ValueError("Joint efficiency E must be in (0,1].")
90
  if head_type == "Ellipsoidal":
91
+ denom = 2 * S * E - 0.2 * P
92
  if denom <= 0:
93
+ raise ValueError("Invalid inputs for ellipsoidal head formula (denominator <= 0).")
94
+ t = (P * 2.0 * R) / denom # simplified geometry mapping D ≈ 2R
95
  elif head_type == "Torispherical":
96
+ L = 2.0 * R
97
+ r = 0.1 * L
98
+ M = 0.25 * (3.0 + math.sqrt(L / r))
99
+ denom = 2 * S * E - 0.2 * P
100
  if denom <= 0:
101
+ raise ValueError("Invalid inputs for torispherical head formula (denominator <= 0).")
102
+ t = (P * L * M) / denom
103
  elif head_type == "Hemispherical":
104
+ L = R
105
  denom = 2 * S * E - 0.2 * P
106
  if denom <= 0:
107
+ raise ValueError("Invalid inputs for hemispherical head formula (denominator <= 0).")
108
+ t = (P * L) / denom
109
  else:
110
  raise ValueError("Unsupported head type.")
111
+ return float(t + corrosion)
112
 
113
+ def nozzle_reinforcement_simple(P: float, d: float, t_shell: float, t_nozzle: float, S: float, E: float) -> Dict[str, Any]:
114
  """
115
+ Conservative, simplified nozzle reinforcement check (keeps original logic but returns details).
116
+ Returns a dict with boolean 'adequate' and diagnostic values.
 
117
  """
118
+ if any(val <= 0 for val in (d, S, E)):
119
+ raise ValueError("Nozzle diameter, allowable stress and joint efficiency must be positive.")
120
+ lhs = (P * d) / (2.0 * S * E)
121
  rhs = (t_shell + t_nozzle)
122
+ adequate = lhs <= rhs
123
+ return {"lhs": float(lhs), "rhs": float(rhs), "adequate": bool(adequate)}
124
 
125
+ def pwht_required(thickness: float, unit_system: str = "SI") -> bool:
126
  """
127
+ Simple PWHT rule:
128
+ - SI: threshold 38 mm
129
+ - USC: threshold ~1.496 in (converted from 38 mm)
 
130
  """
131
  if unit_system == "SI":
132
+ return thickness > 38.0
133
  else:
134
+ return thickness > mm_to_in(38.0)
 
135
 
136
+ def impact_test_required(thickness: float, design_mdmt: float, unit_system: str = "SI") -> Dict[str, Any]:
137
  """
138
+ Simplified MDMT check placeholder.
139
+ Returns dict with 'impact_required' and demonstration values.
140
+ (This is an approximation use UCS-66 for real decisions.)
 
 
 
 
 
 
141
  """
142
  if unit_system == "SI":
143
+ rated_mdmt = -29.0 # approximate representative number (°C)
144
  thickness_threshold = 12.0 # mm
145
+ test_temp = design_mdmt - 17.0 # suggested test temp (°C)
146
  else:
147
+ rated_mdmt = -20.0 # representative (°F)
148
+ thickness_threshold = mm_to_in(12.0) # inches
149
+ test_temp = design_mdmt - 30.0 # suggested test temp (°F)
150
+
151
+ impact_required = (design_mdmt > rated_mdmt) and (thickness > thickness_threshold)
152
+ return {
153
+ "impact_required": bool(impact_required),
154
+ "rated_mdmt": rated_mdmt,
155
+ "threshold_thickness": thickness_threshold,
156
+ "suggested_test_temp": test_temp,
157
+ "note": "APPROXIMATION: use UCS-66 tables for exact rated MDMT decisions."
158
+ }
159
+
160
+ # --------- PDF generation helper ----------
161
+ def generate_pdf_bytes_from_dict(info: Dict[str, Any]) -> Optional[bytes]:
162
+ """
163
+ Generate a small PDF bytes payload from a dict summary.
164
+ Requires fpdf2 (FPDF). If not available, return None.
165
+ """
166
+ if not FPDF_AVAILABLE:
167
+ return None
168
+ pdf = FPDF()
169
+ pdf.set_auto_page_break(auto=True, margin=15)
170
+ pdf.add_page()
171
+ pdf.set_font("Helvetica", "B", 14)
172
+ pdf.cell(0, 10, "ASME Section VIII — Summary Report", ln=True, align="C")
173
+ pdf.ln(4)
174
+ pdf.set_font("Helvetica", size=11)
175
+ for k, v in info.items():
176
+ pdf.multi_cell(0, 7, f"{k}: {v}")
177
+ # fpdf2 can output to string (dest='S') or write to a BytesIO by passing a buffer-like
178
+ try:
179
+ pdf_bytes = pdf.output(dest="S")
180
+ # output(dest="S") returns a str in some versions — convert to bytes
181
+ if isinstance(pdf_bytes, str):
182
+ pdf_bytes = pdf_bytes.encode("latin-1", errors="replace")
183
+ return pdf_bytes
184
+ except Exception:
185
+ # fallback: write to BytesIO via file path (temp) — but avoid filesystem use; just return None
186
+ return None
187
+
188
+ # --------- App state defaults ----------
189
  if "run_done" not in st.session_state:
190
  st.session_state.run_done = False
 
 
 
 
 
 
 
 
191
 
192
+ # --------- Sidebar inputs ----------
193
+ with st.sidebar.expander("Inputs", expanded=True):
194
+ unit_system_choice = st.selectbox("Unit System", ["SI (MPa / mm / °C)", "USC (psi / in / °F)"])
195
+ use_si = unit_system_choice.startswith("SI")
196
 
197
+ st.markdown("---")
198
  if use_si:
199
+ # SI defaults
200
+ P = st.number_input("Design Pressure (MPa)", value=2.0, format="%.3f")
201
+ inside_d = st.number_input("Inside Diameter (mm)", value=1500.0, format="%.1f")
202
+ R = inside_d / 2.0
203
+ S = st.number_input("Allowable Stress (MPa)", value=120.0, format="%.2f")
204
+ corrosion = st.number_input("Corrosion Allowance (mm)", value=1.5, format="%.2f")
205
+ design_mdmt = st.number_input("Design MDMT (°C)", value=-20.0)
 
 
206
  else:
207
+ P = st.number_input("Design Pressure (psi)", value=mpa_to_psi(2.0), format="%.1f")
208
+ inside_d = st.number_input("Inside Diameter (in)", value=mm_to_in(1500.0), format="%.3f")
209
+ R = inside_d / 2.0
210
+ S = st.number_input("Allowable Stress (psi)", value=mpa_to_psi(120.0), format="%.1f")
211
+ corrosion = st.number_input("Corrosion Allowance (in)", value=mm_to_in(1.5), format="%.4f")
212
+ design_mdmt = st.number_input("Design MDMT (°F)", value=-20.0)
213
+
214
+ st.markdown("---")
215
+ head_type = st.selectbox("Head Type", ["Ellipsoidal", "Torispherical", "Hemispherical"])
216
+ E = st.selectbox("Joint Efficiency (E)", [1.0, 0.95, 0.9, 0.85, 0.7], index=0)
217
+
218
+ st.markdown("---")
219
+ # Nozzle inputs
220
+ if use_si:
221
+ d_nozzle = st.number_input("Nozzle Opening Diameter (mm)", value=200.0, format="%.1f")
222
+ t_shell_provided = st.number_input("Shell thickness available (mm)", value=12.0, format="%.2f")
223
+ t_nozzle = st.number_input("Nozzle thickness (mm)", value=10.0, format="%.2f")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  else:
225
+ d_nozzle = st.number_input("Nozzle Opening Diameter (in)", value=mm_to_in(200.0), format="%.3f")
226
+ t_shell_provided = st.number_input("Shell thickness available (in)", value=mm_to_in(12.0), format="%.4f")
227
+ t_nozzle = st.number_input("Nozzle thickness (in)", value=mm_to_in(10.0), format="%.4f")
228
+
229
+ st.markdown("---")
230
+ st.button("Run Calculation", key="run_button", help="Click to run calculations")
231
+ st.button("Reset", key="reset_button", help="Reset not implemented in this lightweight version")
 
232
 
233
+ # --------- Run when user clicked Run Calculation ----------
234
+ if st.sidebar.button("Calculate Now") or st.session_state.get("run_button"):
235
+ st.session_state.run_done = True
236
 
237
+ # --------- Tabs and outputs ----------
238
+ tabs = st.tabs(["Shell", "Head", "Nozzle", "PWHT", "Impact Test", "Summary"])
239
 
240
  if st.session_state.run_done:
241
+ # Shell
242
  with tabs[0]:
243
  try:
244
+ t_shell = shell_thickness(P, R, S, E, corrosion)
245
+ unit_thk = "mm" if use_si else "in"
246
+ st.metric("Required shell thickness (including corrosion)", f"{t_shell:.4f} {unit_thk}")
247
+ st.write("Details:")
248
+ st.write({"P": P, "R": R, "S": S, "E": E, "corrosion": corrosion})
249
  except Exception as exc:
250
  st.error(f"Error computing shell thickness: {exc}")
251
 
252
+ # Head
253
  with tabs[1]:
254
  try:
255
+ t_head = head_thickness(P, R, S, E, corrosion, head_type)
256
+ unit_thk = "mm" if use_si else "in"
257
+ st.metric(f"Required {head_type} head thickness (including corrosion)", f"{t_head:.4f} {unit_thk}")
258
+ st.write("Details:")
259
+ st.write({"P": P, "D (approx)": 2.0 * R, "S": S, "E": E})
260
  except Exception as exc:
261
  st.error(f"Error computing head thickness: {exc}")
262
 
263
+ # Nozzle
264
  with tabs[2]:
265
  try:
266
+ nozzle_res = nozzle_reinforcement_simple(P, d_nozzle, t_shell_provided, t_nozzle, S, E)
267
+ ok = nozzle_res["adequate"]
268
+ st.write("Nozzle reinforcement conservative check (lhs <= rhs):")
269
+ st.write(f"LHS = (P * d) / (2SE) = {nozzle_res['lhs']:.4g}")
270
+ st.write(f"RHS = t_shell + t_nozzle = {nozzle_res['rhs']:.4g}")
271
+ if ok:
272
+ st.success("Conservative nozzle reinforcement check PASSED")
273
+ else:
274
+ st.error("Conservative nozzle reinforcement check FAILED")
275
+ st.write("Note: This is a conservative/simple check. For full ASME UG-37, use the exact projection-area method.")
276
  except Exception as exc:
277
  st.error(f"Error computing nozzle reinforcement: {exc}")
278
 
279
+ # PWHT
280
  with tabs[3]:
281
  try:
282
+ # choose thickness measure to check: use the governing shell thickness
283
+ check_thickness = t_shell if "t_shell" in locals() else t_shell_provided
284
+ pwht = pwht_required(check_thickness, unit_system="SI" if use_si else "USC")
285
+ st.write("PWHT required (preliminary):", "YES" if pwht else "NO")
286
+ st.caption("This is a simplified check; consult UCS-56 and your material spec for final decision.")
 
287
  except Exception as exc:
288
  st.error(f"Error computing PWHT requirement: {exc}")
289
 
290
+ # Impact test
291
  with tabs[4]:
292
  try:
293
+ check_thickness = t_shell if "t_shell" in locals() else t_shell_provided
294
+ impact_info = impact_test_required(check_thickness, design_mdmt, unit_system="SI" if use_si else "USC")
295
+ st.write("Impact test required (preliminary):", "YES" if impact_info["impact_required"] else "NO")
296
+ st.write("Rated MDMT used (approx):", impact_info["rated_mdmt"])
297
+ st.write("Suggested test temperature (approx):", impact_info["suggested_test_temp"])
298
+ st.caption(impact_info["note"])
299
  except Exception as exc:
300
  st.error(f"Error computing impact test requirement: {exc}")
301
 
302
+ # Summary
303
  with tabs[5]:
304
  try:
305
+ summary = {
306
+ "unit_system": "SI (MPa/mm/°C)" if use_si else "USC (psi/in/°F)",
307
+ "design_pressure": P,
308
+ "inside_diameter": inside_d,
309
+ "shell_thickness": t_shell if "t_shell" in locals() else None,
310
+ "head_thickness": t_head if "t_head" in locals() else None,
311
+ "nozzle_check_passed": nozzle_res["adequate"] if "nozzle_res" in locals() else None,
312
+ "pwht_required": pwht if "pwht" in locals() else None,
313
+ "impact_test_required": impact_info["impact_required"] if "impact_info" in locals() else None,
314
+ "generated": datetime.datetime.now().isoformat(),
315
  }
316
+ df_sum = pd.DataFrame([summary])
317
+ st.dataframe(df_sum, use_container_width=True)
318
+ csv = df_sum.to_csv(index=False).encode("utf-8")
319
+ st.download_button("Download CSV", csv, file_name="asme_summary.csv", mime="text/csv")
320
+ if FPDF_AVAILABLE:
321
+ pdf_bytes = generate_pdf_bytes_from_dict(summary)
322
+ if pdf_bytes:
323
+ st.download_button("Download PDF", pdf_bytes, file_name="asme_summary.pdf", mime="application/pdf")
324
+ else:
325
+ st.info("PDF generation failed at runtime; try installing 'fpdf2' or check runtime logs.")
326
+ else:
327
+ st.info("PDF export disabled — install 'fpdf2' (add to requirements.txt) to enable PDF export.")
328
+ st.caption("To enable PDF export add `fpdf2` to requirements.txt, or install locally: pip install fpdf2")
 
 
329
  except Exception as exc:
330
+ st.error(f"Error building summary/export: {exc}")
331
+
332
+ # AI explanation tab (optional)
333
+ with tabs[6] if len(tabs) > 6 else st.container():
334
+ # Show optional AI if Groq client exists and key present
335
+ groq_key = os.getenv("GROQ_API_KEY")
336
+ if Groq is not None and groq_key:
337
+ try:
338
+ groq_client = Groq(api_key=groq_key)
339
+ if st.button("Generate AI Explanation (Groq)"):
340
+ with st.spinner("Generating explanation..."):
341
+ prompt = f"Explain these ASME results simply: {summary}"
342
+ resp = groq_client.chat.completions.create(
343
  messages=[{"role": "user", "content": prompt}],
344
  model="llama-3.1-8b-instant",
345
  )
346
+ explanation = resp.choices[0].message.content
347
+ st.markdown("**AI Explanation**")
348
  st.write(explanation)
349
+ except Exception as e:
350
+ st.info("Groq AI not available or failed. Set GROQ_API_KEY in environment/secrets to enable.")
 
 
351
  else:
352
+ st.info("Groq AI not configured. Add GROQ_API_KEY to environment to enable optional explanations.")
353
  else:
354
+ # placeholders explaining how to run
355
+ with tabs[0]:
356
+ st.info("Enter inputs in the sidebar and click 'Run Calculation' -> 'Calculate Now' or the Run button to execute.")
357
+ for i in range(1, 6):
 
 
358
  with tabs[i]:
359
+ st.info("Results will appear here after running calculations.")