Update app.py
Browse files
app.py
CHANGED
|
@@ -1,1020 +1,357 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
ASME
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
-
|
| 7 |
-
|
| 8 |
-
-
|
| 9 |
-
- UG-32 head thickness (ellipsoidal / torispherical / hemispherical)
|
| 10 |
-
- Improved UG-37-style nozzle reinforcement area method (conservative approx)
|
| 11 |
-
- PWHT recommendation (simplified, UCS-56 style)
|
| 12 |
-
- Impact test (Design MDMT vs Rated MDMT) approximation (UCS-66 style approximation)
|
| 13 |
-
- MAWP calculation from thickness
|
| 14 |
-
- CSV export and optional PDF export (reportlab)
|
| 15 |
-
|
| 16 |
-
ASSUMPTION: This is a preliminary tool. Approximations are labeled and conservative by design.
|
| 17 |
-
APPROXIMATION: MDMT and some reinforcement area calculations are simplified for demo/testing.
|
| 18 |
-
|
| 19 |
-
Requirements: streamlit, pandas, numpy, plotly (optional), reportlab (optional)
|
| 20 |
"""
|
| 21 |
|
| 22 |
from __future__ import annotations
|
| 23 |
-
|
| 24 |
import math
|
| 25 |
import datetime
|
|
|
|
| 26 |
|
| 27 |
-
import numpy as np
|
| 28 |
-
import pandas as pd
|
| 29 |
import streamlit as st
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
#
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
#
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
#
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
"material_group": "P1",
|
| 89 |
-
"is_pipe_grade": False,
|
| 90 |
-
},
|
| 91 |
-
},
|
| 92 |
-
"Stainless Steel": {
|
| 93 |
-
# plate/tube
|
| 94 |
-
"SA-240-304": {
|
| 95 |
-
"allowable_stress": {
|
| 96 |
-
"USC": { -20: 18800, 100: 18800, 200: 16200 },
|
| 97 |
-
"SI": { -29: 130, 38: 130, 93: 112 },
|
| 98 |
-
},
|
| 99 |
-
"carbon_equivalent": 0.08,
|
| 100 |
-
"material_group": "P8",
|
| 101 |
-
"is_pipe_grade": False,
|
| 102 |
-
},
|
| 103 |
-
"SA-240-316": {
|
| 104 |
-
"allowable_stress": {
|
| 105 |
-
"USC": { -20: 18800, 100: 18800, 200: 16600 },
|
| 106 |
-
"SI": { -29: 130, 38: 130, 93: 114 },
|
| 107 |
-
},
|
| 108 |
-
"carbon_equivalent": 0.08,
|
| 109 |
-
"material_group": "P8",
|
| 110 |
-
"is_pipe_grade": False,
|
| 111 |
-
},
|
| 112 |
-
"SA-240-304L": {
|
| 113 |
-
"allowable_stress": {
|
| 114 |
-
"USC": { -20: 18000, 100: 18000, 200: 16000 },
|
| 115 |
-
"SI": { -29: 124, 38: 124, 93: 110 },
|
| 116 |
-
},
|
| 117 |
-
"carbon_equivalent": 0.08,
|
| 118 |
-
"material_group": "P8",
|
| 119 |
-
"is_pipe_grade": False,
|
| 120 |
-
},
|
| 121 |
-
"SA-240-316L": {
|
| 122 |
-
"allowable_stress": {
|
| 123 |
-
"USC": { -20: 18000, 100: 18000, 200: 16400 },
|
| 124 |
-
"SI": { -29: 124, 38: 124, 93: 112 },
|
| 125 |
-
},
|
| 126 |
-
"carbon_equivalent": 0.08,
|
| 127 |
-
"material_group": "P8",
|
| 128 |
-
"is_pipe_grade": False,
|
| 129 |
-
},
|
| 130 |
-
# pipe
|
| 131 |
-
"SA-312-304": {
|
| 132 |
-
"allowable_stress": {
|
| 133 |
-
"USC": { -20: 18800, 100: 18800, 200: 16200 },
|
| 134 |
-
"SI": { -29: 130, 38: 130, 93: 112 },
|
| 135 |
-
},
|
| 136 |
-
"carbon_equivalent": 0.08,
|
| 137 |
-
"material_group": "P8",
|
| 138 |
-
"is_pipe_grade": True,
|
| 139 |
-
},
|
| 140 |
-
"SA-312-316": {
|
| 141 |
-
"allowable_stress": {
|
| 142 |
-
"USC": { -20: 18800, 100: 18800, 200: 16600 },
|
| 143 |
-
"SI": { -29: 130, 38: 130, 93: 114 },
|
| 144 |
-
},
|
| 145 |
-
"carbon_equivalent": 0.08,
|
| 146 |
-
"material_group": "P8",
|
| 147 |
-
"is_pipe_grade": True,
|
| 148 |
-
},
|
| 149 |
-
"SA-312-304L": {
|
| 150 |
-
"allowable_stress": {
|
| 151 |
-
"USC": { -20: 18000, 100: 18000, 200: 16000 },
|
| 152 |
-
"SI": { -29: 124, 38: 124, 93: 110 },
|
| 153 |
-
},
|
| 154 |
-
"carbon_equivalent": 0.08,
|
| 155 |
-
"material_group": "P8",
|
| 156 |
-
"is_pipe_grade": True,
|
| 157 |
-
},
|
| 158 |
-
"SA-312-316L": {
|
| 159 |
-
"allowable_stress": {
|
| 160 |
-
"USC": { -20: 18000, 100: 18000, 200: 16400 },
|
| 161 |
-
"SI": { -29: 124, 38: 124, 93: 112 },
|
| 162 |
-
},
|
| 163 |
-
"carbon_equivalent": 0.08,
|
| 164 |
-
"material_group": "P8",
|
| 165 |
-
"is_pipe_grade": True,
|
| 166 |
-
},
|
| 167 |
-
},
|
| 168 |
-
# User Defined handled separately
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
# ---------------------------
|
| 172 |
-
# Helper functions
|
| 173 |
-
# ---------------------------
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
def _interpolate_allowable(stress_table: Dict[float, float], temperature: float) -> float:
|
| 177 |
-
"""Linear interpolation for stress vs temperature table. Expects sorted keys possible unsorted."""
|
| 178 |
-
temps = sorted(stress_table.keys())
|
| 179 |
-
stresses = [stress_table[t] for t in temps]
|
| 180 |
-
if temperature <= temps[0]:
|
| 181 |
-
return float(stresses[0])
|
| 182 |
-
if temperature >= temps[-1]:
|
| 183 |
-
return float(stresses[-1])
|
| 184 |
-
return float(np.interp(temperature, temps, stresses))
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def get_allowable_stress(material_category: str, material_grade: str, design_temperature: float, units: str) -> Tuple[float, Optional[str]]:
|
| 188 |
-
"""
|
| 189 |
-
Returns (allowable_stress, warning_msg_or_none)
|
| 190 |
-
units: "USC" or "SI"
|
| 191 |
-
ASSUMPTION: MATERIALS table is representative. Replace with full ASME II table when available.
|
| 192 |
-
"""
|
| 193 |
-
if material_category == "User Defined":
|
| 194 |
-
# Should be handled elsewhere; return sentinel
|
| 195 |
-
raise ValueError("User Defined materials should provide allowable stress explicitly.")
|
| 196 |
-
try:
|
| 197 |
-
mat_info = MATERIALS[material_category][material_grade]
|
| 198 |
-
except KeyError:
|
| 199 |
-
raise ValueError(f"Material grade {material_grade} not found under category {material_category}")
|
| 200 |
-
|
| 201 |
-
table = mat_info["allowable_stress"].get(units)
|
| 202 |
-
if table is None:
|
| 203 |
-
raise ValueError(f"No allowable stress data for units '{units}' for material {material_grade}")
|
| 204 |
-
temps = sorted(table.keys())
|
| 205 |
-
wmsg = None
|
| 206 |
-
if design_temperature < temps[0] or design_temperature > temps[-1]:
|
| 207 |
-
wmsg = f"Design temperature {design_temperature} outside table range [{temps[0]}, {temps[-1]}]. Using nearest-end value."
|
| 208 |
-
S = _interpolate_allowable(table, design_temperature)
|
| 209 |
-
return S, wmsg
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
def convert_temp(value: float, from_unit: str, to_unit: str) -> float:
|
| 213 |
-
"""Convert temperature between 'F' and 'C'."""
|
| 214 |
-
if from_unit == to_unit:
|
| 215 |
-
return value
|
| 216 |
-
if from_unit == "F" and to_unit == "C":
|
| 217 |
-
return (value - 32.0) * 5.0 / 9.0
|
| 218 |
-
if from_unit == "C" and to_unit == "F":
|
| 219 |
-
return (value * 9.0 / 5.0) + 32.0
|
| 220 |
-
return value
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
def format_value(value: float, units: str, is_length: bool = False) -> str:
|
| 224 |
-
"""Format numeric values with sensible precision per unit."""
|
| 225 |
-
if is_length:
|
| 226 |
-
if units in ("in", "psi", "USC"): # treat as inches for length when USC
|
| 227 |
-
return f"{value:.{INCH_DECIMALS}f}"
|
| 228 |
-
else:
|
| 229 |
-
return f"{value:.{MM_DECIMALS}f}"
|
| 230 |
-
# default generic formatting
|
| 231 |
-
if abs(value) >= 1000:
|
| 232 |
-
return f"{value:,.0f}"
|
| 233 |
-
if abs(value) >= 1:
|
| 234 |
-
return f"{value:.3g}"
|
| 235 |
-
return f"{value:.4g}"
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
# ---------------------------
|
| 239 |
-
# Calculation functions (pure)
|
| 240 |
-
# ---------------------------
|
| 241 |
-
|
| 242 |
-
def calculate_shell_thickness(P: float, R: float, S: float, E: float) -> Dict[str, Any]:
|
| 243 |
-
"""
|
| 244 |
-
UG-27 circumferential formula (simplified).
|
| 245 |
-
P: design pressure (same units as S)
|
| 246 |
-
R: inside radius
|
| 247 |
-
S: allowable stress
|
| 248 |
-
E: joint efficiency (0 < E <= 1)
|
| 249 |
-
Returns a dict with required thicknesses and metadata.
|
| 250 |
-
"""
|
| 251 |
-
# Validate
|
| 252 |
-
if P < 0:
|
| 253 |
-
raise ValueError("Design pressure must be >= 0")
|
| 254 |
-
if R <= 0:
|
| 255 |
-
raise ValueError("Radius must be positive")
|
| 256 |
if S <= 0:
|
| 257 |
-
raise ValueError("Allowable stress must be positive")
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
else:
|
| 270 |
-
|
| 271 |
-
formula_used = "t = PR/(SE + 0.4P)"
|
| 272 |
-
condition = f"P > 0.385SE: {P:.3f} > {cond_val:.3f}"
|
| 273 |
-
|
| 274 |
-
# longitudinal t = PR / (2SE + 0.4P)
|
| 275 |
-
t_long = (P * R) / (2 * S * E + 0.4 * P)
|
| 276 |
-
|
| 277 |
-
required = max(t_circ, t_long)
|
| 278 |
-
governing = "Circumferential" if t_circ >= t_long else "Longitudinal"
|
| 279 |
-
|
| 280 |
-
return {
|
| 281 |
-
"circumferential_thickness": float(t_circ),
|
| 282 |
-
"longitudinal_thickness": float(t_long),
|
| 283 |
-
"required_thickness": float(required),
|
| 284 |
-
"governing_case": governing,
|
| 285 |
-
"formula_used": formula_used,
|
| 286 |
-
"condition_check": condition,
|
| 287 |
-
"asme_clause": "UG-27",
|
| 288 |
-
}
|
| 289 |
|
| 290 |
-
|
| 291 |
-
def calculate_head_thickness(P: float, D: float, S: float, E: float, head_type: str = "ellipsoidal") -> Dict[str, Any]:
|
| 292 |
-
"""
|
| 293 |
-
Return required head thickness per simplified UG-32 formulas.
|
| 294 |
-
P: design pressure
|
| 295 |
-
D: inside diameter
|
| 296 |
-
S: allowable stress
|
| 297 |
-
E: joint efficiency
|
| 298 |
-
head_type: 'ellipsoidal', 'torispherical', 'hemispherical'
|
| 299 |
-
"""
|
| 300 |
-
if P < 0:
|
| 301 |
-
raise ValueError("Design pressure must be >= 0")
|
| 302 |
-
if D <= 0:
|
| 303 |
-
raise ValueError("Diameter must be positive")
|
| 304 |
-
if S <= 0:
|
| 305 |
-
raise ValueError("Allowable stress must be positive")
|
| 306 |
-
if not (0 < E <= 1):
|
| 307 |
-
raise ValueError("Joint efficiency E must be in (0,1]")
|
| 308 |
-
|
| 309 |
-
if head_type == "ellipsoidal":
|
| 310 |
-
K = 1.0 # geometry factor
|
| 311 |
-
t = (P * D * K) / (2 * S * E - 0.2 * P)
|
| 312 |
-
formula = "t = P*D*K/(2SE - 0.2P)"
|
| 313 |
-
inter = {"K": K}
|
| 314 |
-
clause = "UG-32(d), Appendix 1-4(c)"
|
| 315 |
-
elif head_type == "torispherical":
|
| 316 |
-
# approximations for torispherical: crown radius L = D, knuckle r = 0.1D
|
| 317 |
-
L = D
|
| 318 |
-
r = 0.1 * D
|
| 319 |
-
M = 0.25 * (3.0 + math.sqrt(L / r))
|
| 320 |
-
t = (P * L * M) / (2 * S * E - 0.2 * P)
|
| 321 |
-
formula = f"t = P*L*M/(2SE - 0.2P), M={M:.3f}"
|
| 322 |
-
inter = {"L": L, "r": r, "M": M}
|
| 323 |
-
clause = "UG-32(e), Appendix 1-4(d)"
|
| 324 |
-
elif head_type == "hemispherical":
|
| 325 |
-
L = D / 2.0
|
| 326 |
-
t = (P * L) / (2 * S * E - 0.2 * P)
|
| 327 |
-
formula = "t = P*L/(2SE - 0.2P)"
|
| 328 |
-
inter = {"L": L}
|
| 329 |
-
clause = "UG-32(f), Appendix 1-4(e)"
|
| 330 |
-
else:
|
| 331 |
-
raise ValueError(f"Unsupported head type '{head_type}'")
|
| 332 |
-
|
| 333 |
-
return {
|
| 334 |
-
"required_thickness": float(t),
|
| 335 |
-
"head_type": head_type,
|
| 336 |
-
"formula_used": formula,
|
| 337 |
-
"intermediate_values": inter,
|
| 338 |
-
"asme_clause": clause,
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
def calculate_nozzle_reinforcement(
|
| 343 |
-
d_opening: float,
|
| 344 |
-
t_shell_required: float,
|
| 345 |
-
t_nozzle: float,
|
| 346 |
-
t_pad: float,
|
| 347 |
-
pad_width: float,
|
| 348 |
-
material_grade: Optional[str] = None,
|
| 349 |
-
shell_local_extra_thickness: float = 0.0,
|
| 350 |
-
weld_throat: float = 0.0,
|
| 351 |
-
weld_efficiency: float = 0.7,
|
| 352 |
-
) -> Dict[str, Any]:
|
| 353 |
"""
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
- Required area: A_req = d_opening * t_shell_required
|
| 358 |
-
- Nozzle contribution: A_nozzle = pi * d_opening * t_nozzle (cylindrical projection)
|
| 359 |
-
# APPROXIMATION: Using circumference*thickness as projected reinforcement area.
|
| 360 |
-
- Pad contribution: A_pad = pad_width * t_pad * 2 (count both faces as conservative rectangular pads)
|
| 361 |
-
- Shell local extra thickness contribution: A_shell_local = pi * d_opening * shell_local_extra_thickness
|
| 362 |
-
- Weld contribution: A_weld = weld_throat * pi * d_opening * weld_efficiency
|
| 363 |
-
- For pipe grades (SA-106B, SA-53B-ERW, SA-312-*) apply 12.5% thickness reduction to nozzle/pad/weld contributions.
|
| 364 |
-
- Assumptions and limitations are returned in 'assumptions' and should be reviewed by an engineer.
|
| 365 |
"""
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
if t_nozzle <= 0:
|
| 372 |
-
raise ValueError("Nozzle wall thickness must be positive.")
|
| 373 |
-
|
| 374 |
-
A_required = d_opening * t_shell_required # projection length * thickness
|
| 375 |
-
|
| 376 |
-
# Nozzle cylindrical contribution (projection)
|
| 377 |
-
A_nozzle = math.pi * d_opening * t_nozzle # circumference * thickness (approx)
|
| 378 |
-
# Pad contribution: two faces conservative rectangular pad
|
| 379 |
-
A_pad = pad_width * t_pad * 2.0 if (t_pad and pad_width) else 0.0
|
| 380 |
-
# Shell local extra thickness contribution (if user provides local reinforcement)
|
| 381 |
-
A_shell_local = math.pi * d_opening * shell_local_extra_thickness if shell_local_extra_thickness else 0.0
|
| 382 |
-
# Weld contribution
|
| 383 |
-
A_weld = weld_throat * math.pi * d_opening * weld_efficiency if weld_throat and weld_efficiency else 0.0
|
| 384 |
-
|
| 385 |
-
reduction_info = ""
|
| 386 |
-
# check pipe grade
|
| 387 |
-
if material_grade:
|
| 388 |
-
for cat in MATERIALS:
|
| 389 |
-
mg = MATERIALS[cat].get(material_grade)
|
| 390 |
-
if mg:
|
| 391 |
-
if mg.get("is_pipe_grade"):
|
| 392 |
-
# apply reduction to contributions that rely on nominal thickness (nozzle, pad, weld)
|
| 393 |
-
A_nozzle = A_nozzle * (1.0 - PIPE_THICKNESS_REDUCTION)
|
| 394 |
-
A_pad = A_pad * (1.0 - PIPE_THICKNESS_REDUCTION)
|
| 395 |
-
A_weld = A_weld * (1.0 - PIPE_THICKNESS_REDUCTION)
|
| 396 |
-
reduction_info = f"Applied {PIPE_THICKNESS_REDUCTION*100:.1f}% pipe thickness reduction to nozzle/pad/weld contributions."
|
| 397 |
-
break
|
| 398 |
-
|
| 399 |
-
A_available = A_shell_local + A_nozzle + A_pad + A_weld
|
| 400 |
-
|
| 401 |
-
adequate = A_available >= A_required
|
| 402 |
-
safety_factor = (A_available / A_required) if A_required > 0 else float("inf")
|
| 403 |
-
|
| 404 |
-
assumptions = [
|
| 405 |
-
"# ASSUMPTION: Using projection-area approximations (circumference*thickness) for nozzle contribution.",
|
| 406 |
-
"# APPROXIMATION: Pad is modeled as rectangular and both faces are counted for conservatism.",
|
| 407 |
-
"# ASSUMPTION: Shell local extra thickness is approximated as circumference * extra_thickness.",
|
| 408 |
-
"# ASSUMPTION: Weld contribution approximated as weld_throat * circumference * efficiency.",
|
| 409 |
-
"Real UG-37 uses more exact geometric projections and weld strength contribution; this is conservative and simplified.",
|
| 410 |
-
]
|
| 411 |
-
if reduction_info:
|
| 412 |
-
assumptions.append(reduction_info)
|
| 413 |
-
|
| 414 |
-
return {
|
| 415 |
-
"area_required": float(A_required),
|
| 416 |
-
"area_shell_local": float(A_shell_local),
|
| 417 |
-
"area_nozzle": float(A_nozzle),
|
| 418 |
-
"area_pad": float(A_pad),
|
| 419 |
-
"area_weld": float(A_weld),
|
| 420 |
-
"area_available_total": float(A_available),
|
| 421 |
-
"reinforcement_adequate": bool(adequate),
|
| 422 |
-
"safety_factor": float(safety_factor),
|
| 423 |
-
"assumptions": assumptions,
|
| 424 |
-
"asme_clause": "UG-37 (approx)",
|
| 425 |
-
}
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
def determine_pwht_requirements(material_group: str, thickness_in_in: float, carbon_equivalent: float) -> Dict[str, Any]:
|
| 429 |
"""
|
| 430 |
-
Very
|
| 431 |
-
|
|
|
|
|
|
|
| 432 |
"""
|
| 433 |
-
if
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
pwht_required = False
|
| 437 |
-
reasons = []
|
| 438 |
-
temp_range = ""
|
| 439 |
-
hold_time = ""
|
| 440 |
-
|
| 441 |
-
if material_group == "P1":
|
| 442 |
-
if thickness_in_in > 1.25:
|
| 443 |
-
pwht_required = True
|
| 444 |
-
reasons.append("Thickness > 1.25 inches (UCS-56 guidance).")
|
| 445 |
-
temp_range = "1100°F - 1200°F"
|
| 446 |
-
hold_time = f"Minimum {max(1, int(math.ceil(thickness_in_in)))} hours (approx)"
|
| 447 |
-
if carbon_equivalent > 0.35:
|
| 448 |
-
pwht_required = True
|
| 449 |
-
reasons.append("Carbon equivalent > 0.35% (UCS-56).")
|
| 450 |
-
elif material_group == "P8":
|
| 451 |
-
reasons.append("Stainless steel (P8): PWHT generally not required; follow spec.")
|
| 452 |
else:
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
if not pwht_required:
|
| 456 |
-
reasons.append("Below thickness/CE limits for mandatory PWHT (preliminary check).")
|
| 457 |
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
def _approximate_rated_mdmt(material_group: str, thickness_in_in: float) -> float:
|
| 462 |
-
"""
|
| 463 |
-
APPROXIMATION: Provide a conservative approximate rated MDMT based on material group and thickness.
|
| 464 |
-
This is NOT the full UCS-66 lookup. Use as a placeholder to compare with Design MDMT.
|
| 465 |
-
We define a simple table for P1 and P8 (°F).
|
| 466 |
-
"""
|
| 467 |
-
if material_group == "P8":
|
| 468 |
-
return -325.0
|
| 469 |
-
if thickness_in_in <= 0.5:
|
| 470 |
-
return 60.0
|
| 471 |
-
if thickness_in_in <= 1.0:
|
| 472 |
-
return 30.0
|
| 473 |
-
if thickness_in_in <= 2.0:
|
| 474 |
-
return 0.0
|
| 475 |
-
if thickness_in_in <= 4.0:
|
| 476 |
-
return -20.0
|
| 477 |
-
return -50.0
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
def determine_impact_test_requirements(material_group: str, design_mdmt: float, thickness_in_in: float, units: str) -> Dict[str, Any]:
|
| 481 |
"""
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
"""
|
| 486 |
-
if
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
raise ValueError("units must be 'USC' or 'SI'")
|
| 490 |
-
|
| 491 |
-
# Work in °F internally for rated_mdmt approximation
|
| 492 |
-
if units == "SI":
|
| 493 |
-
design_mdmt_f = convert_temp(design_mdmt, "C", "F")
|
| 494 |
else:
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
else:
|
| 506 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
-
|
| 509 |
-
"impact_test_required": bool(impact_required),
|
| 510 |
-
"rated_mdmt": rated_mdmt_f if units == "USC" else convert_temp(rated_mdmt_f, "F", "C"),
|
| 511 |
-
"design_mdmt": design_mdmt,
|
| 512 |
-
"exemption_coincident_ratio_info": coincident_info,
|
| 513 |
-
"test_temperature": test_temp,
|
| 514 |
-
"notes": "APPROXIMATION: rated MDMT is an estimate - use UCS-66 tables for precise determination.",
|
| 515 |
-
}
|
| 516 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
If component_type == 'shell', use P = (SE t) / (R + 0.6 t)
|
| 522 |
-
For heads, approximate using ellipsoidal style.
|
| 523 |
-
"""
|
| 524 |
-
if t <= 0:
|
| 525 |
-
raise ValueError("Thickness must be positive")
|
| 526 |
-
if R_or_D <= 0:
|
| 527 |
-
raise ValueError("R_or_D must be positive")
|
| 528 |
-
if S <= 0:
|
| 529 |
-
raise ValueError("Allowable stress must be positive")
|
| 530 |
-
if not (0 < E <= 1):
|
| 531 |
-
raise ValueError("Joint efficiency E must be in (0,1]")
|
| 532 |
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
formula = "P = SE*t/(R + 0.6t)"
|
| 536 |
-
else:
|
| 537 |
-
# approximate head MAWP (ellipsoidal form)
|
| 538 |
-
if head_type is None:
|
| 539 |
-
head_type = "ellipsoidal"
|
| 540 |
-
if head_type == "ellipsoidal":
|
| 541 |
-
K = 1.0
|
| 542 |
-
P = (2 * S * E * t * K) / (R_or_D + 0.2 * t)
|
| 543 |
-
formula = "P = 2SEtK/(D + 0.2t)"
|
| 544 |
-
elif head_type == "hemispherical":
|
| 545 |
-
P = (2 * S * E * t) / (R_or_D + 0.4 * t)
|
| 546 |
-
formula = "P = 2SEt/(R + 0.4t)"
|
| 547 |
-
else:
|
| 548 |
-
# fallback
|
| 549 |
-
P = (2 * S * E * t) / (R_or_D + 0.2 * t)
|
| 550 |
-
formula = "P = 2SEt/(D + 0.2t)"
|
| 551 |
-
|
| 552 |
-
return {"mawp": float(P), "formula": formula}
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
# ---------------------------
|
| 556 |
-
# Streamlit UI
|
| 557 |
-
# ---------------------------
|
| 558 |
-
|
| 559 |
-
def init_session_state_defaults():
|
| 560 |
-
"""Initialize session defaults for inputs."""
|
| 561 |
-
defaults = {
|
| 562 |
-
"unit_system": "USC",
|
| 563 |
-
"design_pressure": 150.0,
|
| 564 |
-
"design_temperature": 200.0,
|
| 565 |
-
"design_mdmt": -20.0,
|
| 566 |
-
"corrosion_allowance": DEFAULT_CORROSION_ALLOWANCE_IN,
|
| 567 |
-
"inside_diameter": 60.0,
|
| 568 |
-
"head_type": "ellipsoidal",
|
| 569 |
-
"joint_efficiency": 1.0,
|
| 570 |
-
"material_category": "Carbon Steel",
|
| 571 |
-
"material_grade": "SA-516-70",
|
| 572 |
-
"user_defined_allowable": 15000.0,
|
| 573 |
-
"nozzle_opening_diameter": 6.0,
|
| 574 |
-
"nozzle_wall_thickness": 0.5,
|
| 575 |
-
"use_reinforcing_pad": False,
|
| 576 |
-
"pad_thickness": 0.25,
|
| 577 |
-
"pad_width": 8.0,
|
| 578 |
-
"shell_local_extra_thickness": 0.0,
|
| 579 |
-
"weld_throat": 0.1,
|
| 580 |
-
"weld_efficiency": 0.7,
|
| 581 |
-
"calc_atm_shell": False,
|
| 582 |
-
"asme_edition": SUPPORTED_ASME_EDITIONS[0],
|
| 583 |
-
}
|
| 584 |
-
for k, v in defaults.items():
|
| 585 |
-
if k not in st.session_state:
|
| 586 |
-
st.session_state[k] = v
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
def reset_session_state():
|
| 590 |
-
keys_to_remove = [
|
| 591 |
-
"last_results",
|
| 592 |
-
"warnings",
|
| 593 |
-
"errors",
|
| 594 |
-
]
|
| 595 |
-
for k in keys_to_remove:
|
| 596 |
-
if k in st.session_state:
|
| 597 |
-
del st.session_state[k]
|
| 598 |
-
# Reset inputs to defaults
|
| 599 |
-
init_session_state_defaults()
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
def run_calculations(inputs: Dict[str, Any]) -> Dict[str, Any]:
|
| 603 |
-
"""
|
| 604 |
-
Orchestrate reads, computes and returns a results dict.
|
| 605 |
-
Returns also warnings and any messages.
|
| 606 |
-
"""
|
| 607 |
-
# Validate key inputs
|
| 608 |
-
errors = []
|
| 609 |
-
warnings = []
|
| 610 |
-
|
| 611 |
-
unit_system = inputs["unit_system"]
|
| 612 |
-
# Material handling
|
| 613 |
-
mat_cat = inputs["material_category"]
|
| 614 |
-
mat_grade = inputs["material_grade"]
|
| 615 |
-
|
| 616 |
-
# Allowable stress S
|
| 617 |
-
if mat_cat == "User Defined":
|
| 618 |
-
S = inputs.get("user_defined_allowable")
|
| 619 |
-
if S is None or S <= 0:
|
| 620 |
-
raise ValueError("User-defined allowable stress must be a positive number")
|
| 621 |
-
mg = inputs.get("user_defined_group", "P1")
|
| 622 |
-
ce = inputs.get("user_defined_ce", 0.30)
|
| 623 |
-
is_pipe_grade = False
|
| 624 |
-
else:
|
| 625 |
-
S, wmsg = get_allowable_stress(mat_cat, mat_grade, inputs["design_temperature"], "USC" if unit_system == "USC" else "SI")
|
| 626 |
-
if wmsg:
|
| 627 |
-
warnings.append(wmsg)
|
| 628 |
-
mg = MATERIALS[mat_cat][mat_grade]["material_group"]
|
| 629 |
-
ce = MATERIALS[mat_cat][mat_grade]["carbon_equivalent"]
|
| 630 |
-
is_pipe_grade = MATERIALS[mat_cat][mat_grade].get("is_pipe_grade", False)
|
| 631 |
-
|
| 632 |
-
# Geometry conversions: unify units so functions get consistent units
|
| 633 |
-
# In this implementation we expect S and P to be in consistent units already (controller)
|
| 634 |
-
P = inputs["design_pressure"]
|
| 635 |
-
D = inputs["inside_diameter"]
|
| 636 |
-
R = D / 2.0
|
| 637 |
-
E = inputs["joint_efficiency"]
|
| 638 |
-
ca = inputs["corrosion_allowance"]
|
| 639 |
-
|
| 640 |
-
# Compute shell thicknesses
|
| 641 |
-
shell = calculate_shell_thickness(P, R, S, E)
|
| 642 |
-
shell_required = shell["required_thickness"]
|
| 643 |
-
# Add corrosion allowance to total shell thickness
|
| 644 |
-
total_shell = shell_required + ca
|
| 645 |
-
|
| 646 |
-
# Optionally compute atmospheric shell
|
| 647 |
-
atm_shell = None
|
| 648 |
-
if inputs.get("calc_atm_shell"):
|
| 649 |
-
atm_shell = {"note": "Atmospheric shell check requested: with P=0 this calculation is geometry-specific. Provide loading (wind, vacuum) for meaningful thickness check."}
|
| 650 |
-
|
| 651 |
-
# Head thickness
|
| 652 |
-
head = calculate_head_thickness(P, D, S, E, inputs["head_type"])
|
| 653 |
-
head_required = head["required_thickness"]
|
| 654 |
-
total_head = head_required + ca
|
| 655 |
-
|
| 656 |
-
# Nozzle reinforcement (improved)
|
| 657 |
-
nozzle = calculate_nozzle_reinforcement(
|
| 658 |
-
inputs["nozzle_opening_diameter"],
|
| 659 |
-
total_shell,
|
| 660 |
-
inputs["nozzle_wall_thickness"],
|
| 661 |
-
inputs["pad_thickness"] if inputs["use_reinforcing_pad"] else 0.0,
|
| 662 |
-
inputs["pad_width"] if inputs["use_reinforcing_pad"] else 0.0,
|
| 663 |
-
mat_grade if mat_cat != "User Defined" else None,
|
| 664 |
-
shell_local_extra_thickness=inputs.get("shell_local_extra_thickness", 0.0),
|
| 665 |
-
weld_throat=inputs.get("weld_throat", 0.0),
|
| 666 |
-
weld_efficiency=inputs.get("weld_efficiency", 0.7),
|
| 667 |
-
)
|
| 668 |
-
|
| 669 |
-
# For pipe grades, mention 12.5% reduction in shell effective thickness for reinforcement checks
|
| 670 |
-
if is_pipe_grade:
|
| 671 |
-
reduced_shell_effective = total_shell * (1.0 - PIPE_THICKNESS_REDUCTION)
|
| 672 |
-
warnings.append(f"Pipe grade detected; applied {PIPE_THICKNESS_REDUCTION*100:.1f}% reduction to effective shell thickness for reinforcement checks: {reduced_shell_effective:.4f}")
|
| 673 |
-
else:
|
| 674 |
-
reduced_shell_effective = total_shell
|
| 675 |
|
| 676 |
-
# PWHT
|
| 677 |
-
if unit_system == "SI":
|
| 678 |
-
thickness_in_in = total_shell / 25.4
|
| 679 |
-
else:
|
| 680 |
-
thickness_in_in = total_shell
|
| 681 |
-
|
| 682 |
-
pwht = determine_pwht_requirements(mg, thickness_in_in, ce)
|
| 683 |
-
|
| 684 |
-
# Impact test / MDMT
|
| 685 |
-
design_mdmt = inputs["design_mdmt"]
|
| 686 |
-
impact = determine_impact_test_requirements(mg, design_mdmt, thickness_in_in, unit_system)
|
| 687 |
-
|
| 688 |
-
# MAWPs
|
| 689 |
-
shell_mawp = calculate_mawp_from_thickness(total_shell, R, S, E, "shell")
|
| 690 |
-
head_mawp = calculate_mawp_from_thickness(total_head, D, S, E, "head", inputs["head_type"])
|
| 691 |
-
governing_mawp = min(shell_mawp["mawp"], head_mawp["mawp"])
|
| 692 |
-
|
| 693 |
-
results = {
|
| 694 |
-
"shell": shell,
|
| 695 |
-
"total_shell_thickness": total_shell,
|
| 696 |
-
"head": head,
|
| 697 |
-
"total_head_thickness": total_head,
|
| 698 |
-
"nozzle": nozzle,
|
| 699 |
-
"pwht": pwht,
|
| 700 |
-
"impact": impact,
|
| 701 |
-
"shell_mawp": shell_mawp,
|
| 702 |
-
"head_mawp": head_mawp,
|
| 703 |
-
"governing_mawp": governing_mawp,
|
| 704 |
-
"reduced_shell_effective": reduced_shell_effective,
|
| 705 |
-
"warnings": warnings,
|
| 706 |
-
}
|
| 707 |
-
return results
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
# ---------------------------
|
| 711 |
-
# Streamlit app layout
|
| 712 |
-
# ---------------------------
|
| 713 |
-
|
| 714 |
-
def main():
|
| 715 |
-
st.set_page_config(page_title="ASME Section VIII Calculator", page_icon="🔧", layout="wide")
|
| 716 |
-
init_session_state_defaults()
|
| 717 |
-
|
| 718 |
-
st.markdown("<h2 style='color:#0b5fff'>🔧 ASME Section VIII — Preliminary Calculator</h2>", unsafe_allow_html=True)
|
| 719 |
-
st.caption("This tool is for preliminary design only. Final verification must be completed by a licensed professional engineer and checked against the latest ASME code editions.")
|
| 720 |
-
st.sidebar.title("Inputs")
|
| 721 |
-
|
| 722 |
-
# Unit system
|
| 723 |
-
unit_system = st.sidebar.selectbox("Unit System", ["USC", "SI"], index=0 if st.session_state["unit_system"] == "USC" else 1)
|
| 724 |
-
st.session_state["unit_system"] = unit_system
|
| 725 |
-
|
| 726 |
-
# ASME edition placeholder
|
| 727 |
-
st.session_state["asme_edition"] = st.sidebar.selectbox("ASME Section VIII Edition", SUPPORTED_ASME_EDITIONS, index=SUPPORTED_ASME_EDITIONS.index(st.session_state.get("asme_edition", SUPPORTED_ASME_EDITIONS[0])))
|
| 728 |
-
|
| 729 |
-
# Basic design
|
| 730 |
-
pressure_label = "Design Pressure (psi)" if unit_system == "USC" else "Design Pressure (bar)"
|
| 731 |
-
temp_label = "Design Temperature (°F)" if unit_system == "USC" else "Design Temperature (°C)"
|
| 732 |
-
mdmt_label = "Design MDMT (°F)" if unit_system == "USC" else "Design MDMT (°C)"
|
| 733 |
-
length_label = "Inside Diameter (in)" if unit_system == "USC" else "Inside Diameter (mm)"
|
| 734 |
-
ca_label = "Corrosion Allowance (in)" if unit_system == "USC" else "Corrosion Allowance (mm)"
|
| 735 |
-
|
| 736 |
-
st.session_state["design_pressure"] = st.sidebar.number_input(pressure_label, value=float(st.session_state["design_pressure"]))
|
| 737 |
-
st.session_state["design_temperature"] = st.sidebar.number_input(temp_label, value=float(st.session_state["design_temperature"]))
|
| 738 |
-
st.session_state["design_mdmt"] = st.sidebar.number_input(mdmt_label, value=float(st.session_state["design_mdmt"]))
|
| 739 |
-
st.session_state["corrosion_allowance"] = st.sidebar.number_input(ca_label, value=float(st.session_state["corrosion_allowance"]))
|
| 740 |
-
|
| 741 |
-
st.sidebar.markdown("---")
|
| 742 |
-
st.session_state["inside_diameter"] = st.sidebar.number_input(length_label, value=float(st.session_state["inside_diameter"]))
|
| 743 |
-
st.session_state["head_type"] = st.sidebar.selectbox("Head Type", ["ellipsoidal", "torispherical", "hemispherical"], index=0 if st.session_state["head_type"] == "ellipsoidal" else 0)
|
| 744 |
-
# Added E = 0.7 option as requested
|
| 745 |
-
st.session_state["joint_efficiency"] = st.sidebar.selectbox("Joint Efficiency (E)", [1.0, 0.95, 0.9, 0.85, 0.7], index=[1.0,0.95,0.9,0.85,0.7].index(st.session_state.get("joint_efficiency", 1.0)))
|
| 746 |
-
|
| 747 |
-
st.sidebar.markdown("---")
|
| 748 |
-
st.session_state["material_category"] = st.sidebar.selectbox("Material Category", ["Carbon Steel", "Stainless Steel", "User Defined"], index=0)
|
| 749 |
-
# material grade selection
|
| 750 |
-
if st.session_state["material_category"] == "User Defined":
|
| 751 |
-
st.session_state["material_grade"] = st.sidebar.text_input("Material Grade (user)", value="USER-MAT")
|
| 752 |
-
st.session_state["user_defined_allowable"] = st.sidebar.number_input("User-defined allowable stress (psi or MPa)", value=float(st.session_state["user_defined_allowable"]))
|
| 753 |
-
st.session_state["user_defined_group"] = st.sidebar.selectbox("User-defined material group", ["P1", "P8"], index=0)
|
| 754 |
-
st.session_state["user_defined_ce"] = st.sidebar.number_input("User-defined carbon equivalent (if known)", value=0.30)
|
| 755 |
else:
|
| 756 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 757 |
try:
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
st.session_state["nozzle_wall_thickness"] = st.sidebar.number_input("Nozzle Wall Thickness (same unit)", value=float(st.session_state["nozzle_wall_thickness"]))
|
| 767 |
-
st.session_state["use_reinforcing_pad"] = st.sidebar.checkbox("Use Reinforcing Pad", value=st.session_state["use_reinforcing_pad"])
|
| 768 |
-
if st.session_state["use_reinforcing_pad"]:
|
| 769 |
-
st.session_state["pad_thickness"] = st.sidebar.number_input("Pad Thickness (same unit)", value=float(st.session_state["pad_thickness"]))
|
| 770 |
-
st.session_state["pad_width"] = st.sidebar.number_input("Pad Width (same unit)", value=float(st.session_state["pad_width"]))
|
| 771 |
-
|
| 772 |
-
# New inputs for improved nozzle reinforcement
|
| 773 |
-
st.sidebar.markdown("---")
|
| 774 |
-
st.session_state["shell_local_extra_thickness"] = st.sidebar.number_input("Local shell extra thickness (for reinforcement) (same unit)", value=float(st.session_state.get("shell_local_extra_thickness", 0.0)))
|
| 775 |
-
st.session_state["weld_throat"] = st.sidebar.number_input("Effective weld throat (same unit) for weld contribution", value=float(st.session_state.get("weld_throat", 0.1)))
|
| 776 |
-
st.session_state["weld_efficiency"] = st.sidebar.slider("Weld efficiency factor", min_value=0.0, max_value=1.0, value=float(st.session_state.get("weld_efficiency", 0.7)), step=0.05)
|
| 777 |
-
|
| 778 |
-
st.sidebar.markdown("---")
|
| 779 |
-
st.session_state["calc_atm_shell"] = st.sidebar.checkbox("Also calculate shell at atmospheric pressure", value=st.session_state["calc_atm_shell"])
|
| 780 |
-
|
| 781 |
-
# Buttons
|
| 782 |
-
col1, col2 = st.columns([1, 1])
|
| 783 |
-
run_calc = col1.button("Calculate")
|
| 784 |
-
reset_btn = col2.button("Reset to defaults")
|
| 785 |
-
|
| 786 |
-
if reset_btn:
|
| 787 |
-
reset_session_state()
|
| 788 |
-
st.experimental_rerun()
|
| 789 |
-
|
| 790 |
-
# Prepare inputs dict
|
| 791 |
-
inputs = {
|
| 792 |
-
"unit_system": st.session_state["unit_system"],
|
| 793 |
-
"design_pressure": float(st.session_state["design_pressure"]),
|
| 794 |
-
"design_temperature": float(st.session_state["design_temperature"]),
|
| 795 |
-
"design_mdmt": float(st.session_state["design_mdmt"]),
|
| 796 |
-
"corrosion_allowance": float(st.session_state["corrosion_allowance"]),
|
| 797 |
-
"inside_diameter": float(st.session_state["inside_diameter"]),
|
| 798 |
-
"head_type": st.session_state["head_type"],
|
| 799 |
-
"joint_efficiency": float(st.session_state["joint_efficiency"]),
|
| 800 |
-
"material_category": st.session_state["material_category"],
|
| 801 |
-
"material_grade": st.session_state["material_grade"],
|
| 802 |
-
"user_defined_allowable": float(st.session_state.get("user_defined_allowable", 0.0)),
|
| 803 |
-
"user_defined_group": st.session_state.get("user_defined_group", "P1"),
|
| 804 |
-
"user_defined_ce": float(st.session_state.get("user_defined_ce", 0.30)),
|
| 805 |
-
"nozzle_opening_diameter": float(st.session_state["nozzle_opening_diameter"]),
|
| 806 |
-
"nozzle_wall_thickness": float(st.session_state["nozzle_wall_thickness"]),
|
| 807 |
-
"use_reinforcing_pad": bool(st.session_state["use_reinforcing_pad"]),
|
| 808 |
-
"pad_thickness": float(st.session_state.get("pad_thickness", 0.0)),
|
| 809 |
-
"pad_width": float(st.session_state.get("pad_width", 0.0)),
|
| 810 |
-
"shell_local_extra_thickness": float(st.session_state.get("shell_local_extra_thickness", 0.0)),
|
| 811 |
-
"weld_throat": float(st.session_state.get("weld_throat", 0.1)),
|
| 812 |
-
"weld_efficiency": float(st.session_state.get("weld_efficiency", 0.7)),
|
| 813 |
-
"calc_atm_shell": bool(st.session_state.get("calc_atm_shell", False)),
|
| 814 |
-
}
|
| 815 |
-
|
| 816 |
-
# Run calculations only when requested
|
| 817 |
-
if run_calc:
|
| 818 |
try:
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
st.
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
st.
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
#
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
if results:
|
| 842 |
-
shell = results["shell"]
|
| 843 |
-
st.write("**Formula used:**", shell["formula_used"])
|
| 844 |
-
st.write("**Condition:**", shell["condition_check"])
|
| 845 |
-
units_len = "in" if unit_system == "USC" else "mm"
|
| 846 |
-
st.metric("Circumferential thickness", f"{format_value(shell['circumferential_thickness'], units_len, True)} {units_len}")
|
| 847 |
-
st.metric("Longitudinal thickness", f"{format_value(shell['longitudinal_thickness'], units_len, True)} {units_len}")
|
| 848 |
-
st.metric("Required thickness (governing)", f"{format_value(shell['required_thickness'], units_len, True)} {units_len}")
|
| 849 |
-
st.markdown("**Total shell thickness including corrosion allowance:**")
|
| 850 |
-
st.write(f"{format_value(results['total_shell_thickness'], units_len, True)} {units_len}")
|
| 851 |
-
|
| 852 |
-
with st.expander("Show step-by-step shell calculations"):
|
| 853 |
-
st.code(f"Inputs: P={inputs['design_pressure']}, R={inputs['inside_diameter']/2.0}, S={get_allowable_stress(inputs['material_category'], inputs['material_grade'], inputs['design_temperature'], 'USC' if unit_system=='USC' else 'SI')[0]}, E={inputs['joint_efficiency']}")
|
| 854 |
-
st.write(shell)
|
| 855 |
-
else:
|
| 856 |
-
st.info("Press **Calculate** to run shell thickness calculations.")
|
| 857 |
-
|
| 858 |
-
with tab2:
|
| 859 |
-
st.header("Head Thickness (UG-32)")
|
| 860 |
-
if results:
|
| 861 |
-
head = results["head"]
|
| 862 |
-
units_len = "in" if unit_system == "USC" else "mm"
|
| 863 |
-
st.write("**Head Type:**", head["head_type"].title())
|
| 864 |
-
st.write("**Formula used:**", head["formula_used"])
|
| 865 |
-
st.metric("Required head thickness", f"{format_value(results['head']['required_thickness'], units_len, True)} {units_len}")
|
| 866 |
-
st.write("Total head thickness including corrosion allowance:", f"{format_value(results['total_head_thickness'], units_len, True)} {units_len}")
|
| 867 |
-
with st.expander("Show step-by-step head calculations"):
|
| 868 |
-
st.write(head["intermediate_values"])
|
| 869 |
-
else:
|
| 870 |
-
st.info("Press **Calculate** to run head thickness calculations.")
|
| 871 |
-
|
| 872 |
-
with tab3:
|
| 873 |
-
st.header("Nozzle Reinforcement (UG-37 approx - improved)")
|
| 874 |
-
st.info("Provide nozzle geometry inputs below. The method is a conservative projection-area approximation; see assumptions in the result.")
|
| 875 |
-
if results:
|
| 876 |
-
nozzle = results["nozzle"]
|
| 877 |
-
units_len = "in" if unit_system == "USC" else "mm"
|
| 878 |
-
st.write("Assumptions:")
|
| 879 |
-
for a in nozzle["assumptions"]:
|
| 880 |
-
st.write("-", a)
|
| 881 |
-
st.write("Required area (A_required = d_opening * t_shell):", f"{format_value(nozzle['area_required'], units_len)} {units_len}^2")
|
| 882 |
-
st.write("Shell local contribution (A_shell_local):", f"{format_value(nozzle['area_shell_local'], units_len)} {units_len}^2")
|
| 883 |
-
st.write("Nozzle contribution (A_nozzle = pi*d*t_nozzle):", f"{format_value(nozzle['area_nozzle'], units_len)} {units_len}^2")
|
| 884 |
-
st.write("Pad contribution (A_pad):", f"{format_value(nozzle['area_pad'], units_len)} {units_len}^2")
|
| 885 |
-
st.write("Weld contribution (A_weld):", f"{format_value(nozzle['area_weld'], units_len)} {units_len}^2")
|
| 886 |
-
st.write("Total available area:", f"{format_value(nozzle['area_available_total'], units_len)} {units_len}^2")
|
| 887 |
-
if nozzle["reinforcement_adequate"]:
|
| 888 |
-
st.success(f"Reinforcement adequate (SF = {nozzle['safety_factor']:.2f})")
|
| 889 |
-
else:
|
| 890 |
-
st.error(f"Reinforcement inadequate (SF = {nozzle['safety_factor']:.2f})")
|
| 891 |
-
with st.expander("Show step-by-step nozzle calculation (raw values)"):
|
| 892 |
-
st.write(nozzle)
|
| 893 |
-
else:
|
| 894 |
-
st.info("Enter inputs and press **Calculate** to run nozzle reinforcement check.")
|
| 895 |
-
|
| 896 |
-
with tab4:
|
| 897 |
-
st.header("PWHT & Impact Test")
|
| 898 |
-
if results:
|
| 899 |
-
pwht = results["pwht"]
|
| 900 |
-
impact = results["impact"]
|
| 901 |
-
|
| 902 |
-
st.subheader("PWHT")
|
| 903 |
-
if pwht["pwht_required"]:
|
| 904 |
-
st.error("PWHT REQUIRED")
|
| 905 |
-
else:
|
| 906 |
-
st.success("PWHT not required by preliminary check")
|
| 907 |
-
st.write("Reasons:")
|
| 908 |
-
for r in pwht["reasons"]:
|
| 909 |
-
st.write("-", r)
|
| 910 |
-
if pwht["temperature_range"]:
|
| 911 |
-
st.write("Suggested PWHT temperature range:", pwht["temperature_range"])
|
| 912 |
-
st.write("Suggested hold time:", pwht["hold_time"])
|
| 913 |
-
|
| 914 |
-
st.subheader("Impact Test (MDMT check)")
|
| 915 |
-
rated = impact["rated_mdmt"]
|
| 916 |
-
design = impact["design_mdmt"]
|
| 917 |
-
if unit_system == "SI":
|
| 918 |
-
st.write("Rated MDMT (°C):", f"{format_value(rated, 'C')} °C")
|
| 919 |
-
st.write("Design MDMT (°C):", f"{format_value(design, 'C')} °C")
|
| 920 |
else:
|
| 921 |
-
st.
|
| 922 |
-
|
|
|
|
| 923 |
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
else
|
| 928 |
-
|
| 929 |
-
st.
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
"
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
"Design MDMT",
|
| 944 |
-
"Inside Diameter",
|
| 945 |
-
"Material",
|
| 946 |
-
"Allowable Stress",
|
| 947 |
-
"Joint Efficiency",
|
| 948 |
-
"Corrosion Allowance",
|
| 949 |
-
"Shell Required Thickness",
|
| 950 |
-
"Shell Total Thickness",
|
| 951 |
-
"Head Required Thickness",
|
| 952 |
-
"Head Total Thickness",
|
| 953 |
-
"Governing MAWP",
|
| 954 |
-
],
|
| 955 |
-
"Value": []
|
| 956 |
}
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
f"
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
c.setFont("Helvetica", 12)
|
| 996 |
-
c.drawString(30, 750, "ASME Section VIII — Preliminary Summary Report")
|
| 997 |
-
y = 730
|
| 998 |
-
for i, (param, val) in df.iterrows():
|
| 999 |
-
c.drawString(30, y, f"{val['Parameter']}: {val['Value']}")
|
| 1000 |
-
y -= 15
|
| 1001 |
-
if y < 50:
|
| 1002 |
-
c.showPage()
|
| 1003 |
-
y = 750
|
| 1004 |
-
c.drawString(30, y - 20, f"Generated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
| 1005 |
-
c.save()
|
| 1006 |
-
pdf_data = buffer.getvalue()
|
| 1007 |
-
buffer.close()
|
| 1008 |
-
st.download_button("Download Summary PDF", pdf_data, file_name="asme_summary.pdf", mime="application/pdf")
|
| 1009 |
-
except Exception:
|
| 1010 |
-
st.info("PDF export requires optional package 'reportlab'. Install to enable PDF export.")
|
| 1011 |
-
|
| 1012 |
else:
|
| 1013 |
-
st.info("
|
| 1014 |
-
|
| 1015 |
-
#
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
| 1019 |
-
|
| 1020 |
-
|
|
|
|
|
|
|
|
|
| 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}.")
|