Update app.py
Browse files
app.py
CHANGED
|
@@ -2,42 +2,32 @@
|
|
| 2 |
"""
|
| 3 |
ASME Section VIII — Preliminary Pressure Vessel Calculator (Streamlit)
|
| 4 |
|
| 5 |
-
|
| 6 |
-
-
|
| 7 |
-
-
|
| 8 |
-
- Single Run Calculation button;
|
| 9 |
-
- Simplified
|
| 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 |
-
#
|
| 31 |
-
|
| 32 |
-
|
| 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
|
| 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 (
|
| 70 |
-
#
|
| 71 |
-
def
|
| 72 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 80 |
t = (P * R) / denom
|
| 81 |
return float(t + corrosion)
|
| 82 |
|
| 83 |
-
def
|
| 84 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 (
|
| 118 |
raise ValueError("Nozzle diameter, allowable stress and joint efficiency must be positive.")
|
| 119 |
-
lhs = (P *
|
| 120 |
rhs = (t_shell + t_nozzle)
|
| 121 |
adequate = lhs <= rhs
|
| 122 |
return {"lhs": float(lhs), "rhs": float(rhs), "adequate": bool(adequate)}
|
| 123 |
|
| 124 |
-
def
|
| 125 |
-
"""Very simplified PWHT rule
|
| 126 |
if unit_system == "SI":
|
| 127 |
-
return thickness > 38.0
|
| 128 |
else:
|
| 129 |
-
return thickness > mm_to_in(38.0)
|
| 130 |
|
| 131 |
-
def
|
| 132 |
"""
|
| 133 |
-
Simplified MDMT
|
| 134 |
-
|
| 135 |
"""
|
| 136 |
if unit_system == "SI":
|
| 137 |
-
rated_mdmt = -29.0 # °C
|
| 138 |
thickness_threshold = 12.0 # mm
|
| 139 |
suggested_test_temp = design_mdmt - 17.0 # °C
|
| 140 |
else:
|
| 141 |
-
rated_mdmt = -20.0 # °F
|
| 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
|
| 151 |
}
|
| 152 |
|
| 153 |
-
#
|
| 154 |
-
#
|
| 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 |
-
|
| 250 |
-
|
| 251 |
-
R =
|
| 252 |
-
|
| 253 |
-
|
| 254 |
design_mdmt = st.number_input("Design MDMT (°C)", value=-20.0)
|
| 255 |
else:
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
R =
|
| 259 |
-
|
| 260 |
-
|
| 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 |
-
|
| 266 |
|
| 267 |
st.markdown("---")
|
|
|
|
| 268 |
if use_si:
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
else:
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
|
| 277 |
st.markdown("---")
|
| 278 |
-
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
| 280 |
st.session_state.run_done = False
|
| 281 |
st.experimental_rerun()
|
| 282 |
|
| 283 |
-
if
|
| 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 |
-
|
| 292 |
-
tabs = st.tabs(
|
| 293 |
|
| 294 |
-
# ----------------------
|
| 295 |
-
# Run calculations when requested
|
| 296 |
-
# ----------------------
|
| 297 |
if st.session_state.run_done:
|
| 298 |
-
# Shell
|
| 299 |
with tabs[0]:
|
| 300 |
try:
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
st.metric("Required
|
| 304 |
-
st.write("Inputs:", {"P":
|
| 305 |
except Exception as exc:
|
| 306 |
-
st.error(f"
|
| 307 |
|
| 308 |
-
# Head
|
| 309 |
with tabs[1]:
|
| 310 |
try:
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
st.metric(f"Required {head_type}
|
| 314 |
-
st.write("
|
| 315 |
except Exception as exc:
|
| 316 |
-
st.error(f"
|
| 317 |
|
| 318 |
-
# Nozzle
|
| 319 |
with tabs[2]:
|
| 320 |
try:
|
| 321 |
-
|
| 322 |
-
st.write("
|
| 323 |
-
st.write(f"LHS = (P * d) / (2SE) = {
|
| 324 |
-
st.write(f"RHS = t_shell + t_nozzle = {
|
| 325 |
-
if
|
| 326 |
st.success("Conservative nozzle reinforcement check PASSED")
|
| 327 |
else:
|
| 328 |
st.error("Conservative nozzle reinforcement check FAILED")
|
| 329 |
-
st.caption("
|
| 330 |
except Exception as exc:
|
| 331 |
-
st.error(f"
|
| 332 |
|
| 333 |
-
# PWHT
|
| 334 |
with tabs[3]:
|
| 335 |
try:
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
st.
|
| 340 |
-
|
| 341 |
-
st.
|
| 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"
|
| 354 |
|
| 355 |
-
# Summary
|
| 356 |
-
with tabs[
|
| 357 |
try:
|
| 358 |
summary = {
|
| 359 |
-
"
|
| 360 |
-
"
|
| 361 |
-
"
|
| 362 |
-
"
|
| 363 |
-
"
|
| 364 |
-
"
|
| 365 |
-
"
|
| 366 |
-
"
|
| 367 |
-
"
|
| 368 |
}
|
| 369 |
-
|
| 370 |
-
st.dataframe(
|
| 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"
|
| 388 |
|
| 389 |
-
# AI Explanation
|
| 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 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 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 |
-
|
| 407 |
-
|
| 408 |
else:
|
| 409 |
-
st.info("Groq AI not
|
| 410 |
|
| 411 |
else:
|
| 412 |
-
# placeholders
|
| 413 |
-
for i
|
| 414 |
with tabs[i]:
|
| 415 |
-
st.info("
|
|
|
|
| 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.")
|