Update app.py
Browse files
app.py
CHANGED
|
@@ -1,357 +1,359 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
ASME
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
| 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 |
-
#
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
#
|
| 25 |
try:
|
| 26 |
-
from
|
|
|
|
|
|
|
| 27 |
except Exception:
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 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
|
| 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 |
-
#
|
| 73 |
def shell_thickness(P: float, R: float, S: float, E: float, corrosion: float) -> float:
|
| 74 |
-
"""
|
| 75 |
-
|
|
|
|
|
|
|
| 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 |
-
#
|
| 81 |
denom = S * E - 0.6 * P
|
| 82 |
if denom <= 0:
|
| 83 |
-
|
| 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 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 94 |
if denom <= 0:
|
| 95 |
-
raise ValueError("
|
| 96 |
-
|
| 97 |
elif head_type == "Torispherical":
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
| 99 |
if denom <= 0:
|
| 100 |
-
raise ValueError("
|
| 101 |
-
|
| 102 |
elif head_type == "Hemispherical":
|
|
|
|
| 103 |
denom = 2 * S * E - 0.2 * P
|
| 104 |
if denom <= 0:
|
| 105 |
-
raise ValueError("
|
| 106 |
-
|
| 107 |
else:
|
| 108 |
raise ValueError("Unsupported head type.")
|
|
|
|
| 109 |
|
| 110 |
-
def
|
| 111 |
"""
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
Works if units are consistent.
|
| 115 |
"""
|
| 116 |
-
|
|
|
|
|
|
|
| 117 |
rhs = (t_shell + t_nozzle)
|
| 118 |
-
|
|
|
|
| 119 |
|
| 120 |
-
def pwht_required(thickness: float,
|
| 121 |
"""
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
-
|
| 125 |
-
- USC: convert 38 mm to inches (~1.496 in)
|
| 126 |
"""
|
| 127 |
if unit_system == "SI":
|
| 128 |
-
|
| 129 |
else:
|
| 130 |
-
|
| 131 |
-
return (material == "CS") and (thickness > threshold)
|
| 132 |
|
| 133 |
-
def impact_test_required(thickness: float,
|
| 134 |
"""
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 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 |
-
|
| 148 |
else:
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 166 |
-
|
|
|
|
|
|
|
| 167 |
|
| 168 |
-
|
| 169 |
if use_si:
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
default_thickness = 40.0 # mm
|
| 178 |
-
default_mdmt = -20.0 # °C
|
| 179 |
else:
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 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 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
run_calculation = True
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
|
|
|
| 245 |
|
| 246 |
-
#
|
| 247 |
-
tabs = st.tabs(["Shell", "Head", "Nozzle", "PWHT", "Impact Test", "Summary"
|
| 248 |
|
| 249 |
if st.session_state.run_done:
|
| 250 |
-
#
|
| 251 |
with tabs[0]:
|
| 252 |
try:
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
st.metric(
|
|
|
|
|
|
|
| 256 |
except Exception as exc:
|
| 257 |
st.error(f"Error computing shell thickness: {exc}")
|
| 258 |
|
| 259 |
-
#
|
| 260 |
with tabs[1]:
|
| 261 |
try:
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
st.metric(f"Required {head_type}
|
|
|
|
|
|
|
| 265 |
except Exception as exc:
|
| 266 |
st.error(f"Error computing head thickness: {exc}")
|
| 267 |
|
| 268 |
-
#
|
| 269 |
with tabs[2]:
|
| 270 |
try:
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
st.write("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
except Exception as exc:
|
| 275 |
st.error(f"Error computing nozzle reinforcement: {exc}")
|
| 276 |
|
| 277 |
-
#
|
| 278 |
with tabs[3]:
|
| 279 |
try:
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
if use_si
|
| 283 |
-
|
| 284 |
-
|
| 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 |
-
#
|
| 290 |
with tabs[4]:
|
| 291 |
try:
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
st.
|
|
|
|
|
|
|
|
|
|
| 295 |
except Exception as exc:
|
| 296 |
st.error(f"Error computing impact test requirement: {exc}")
|
| 297 |
|
| 298 |
-
#
|
| 299 |
with tabs[5]:
|
| 300 |
try:
|
| 301 |
-
|
| 302 |
-
"
|
| 303 |
-
"
|
| 304 |
-
"
|
| 305 |
-
"
|
| 306 |
-
"
|
| 307 |
-
"
|
|
|
|
|
|
|
|
|
|
| 308 |
}
|
| 309 |
-
|
| 310 |
-
st.dataframe(
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 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
|
| 326 |
-
|
| 327 |
-
#
|
| 328 |
-
with tabs[6]:
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
|
|
|
| 337 |
messages=[{"role": "user", "content": prompt}],
|
| 338 |
model="llama-3.1-8b-instant",
|
| 339 |
)
|
| 340 |
-
explanation =
|
| 341 |
-
st.
|
| 342 |
st.write(explanation)
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
if st.session_state.ai_done:
|
| 346 |
-
st.info("✨ AI explanation already generated. Rerun to refresh.")
|
| 347 |
else:
|
| 348 |
-
st.info("
|
| 349 |
else:
|
| 350 |
-
#
|
| 351 |
-
|
| 352 |
-
"
|
| 353 |
-
|
| 354 |
-
"Summary", "AI explanation"
|
| 355 |
-
]):
|
| 356 |
with tabs[i]:
|
| 357 |
-
st.info(
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
ASME Section VIII — Preliminary 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.")
|