Update app.py
Browse files
app.py
CHANGED
|
@@ -2,16 +2,7 @@
|
|
| 2 |
Tubesheet Thickness Calculator (ASME VIII-1 preliminary)
|
| 3 |
Single-file Streamlit app.
|
| 4 |
|
| 5 |
-
|
| 6 |
-
- Units toggle (SI / Imperial)
|
| 7 |
-
- Material dropdown with small bundled allowable-stress table (interpolated)
|
| 8 |
-
- Manual override for allowable stress
|
| 9 |
-
- Show detailed calculations toggle / button
|
| 10 |
-
- Outputs: required thickness, final thickness (with CA + machining), rounded thickness
|
| 11 |
-
- Schematic (plotly) and CSV export
|
| 12 |
-
|
| 13 |
-
Notes:
|
| 14 |
-
- This is a preliminary engineering tool. Final design must be verified per ASME Section VIII Division 1.
|
| 15 |
"""
|
| 16 |
|
| 17 |
import streamlit as st
|
|
@@ -28,7 +19,6 @@ st.title("Tubesheet Thickness Calculator — ASME VIII-1 (Preliminary)")
|
|
| 28 |
# -----------------------
|
| 29 |
# Bundled (example) material table (sample values)
|
| 30 |
# NOTE: These are illustrative. Replace with verified ASME Section II data for production use.
|
| 31 |
-
# The units here: temperature_C, allowable_MPa (MPa)
|
| 32 |
MATERIAL_LOOKUP = {
|
| 33 |
"SA-516 Gr 70": [
|
| 34 |
{"temperature_C": 20, "allowable_MPa": 138},
|
|
@@ -125,7 +115,8 @@ with st.sidebar:
|
|
| 125 |
project_name = st.text_input("Project name", value="Tubesheet Calculation")
|
| 126 |
designer_name = st.text_input("Designer", value="")
|
| 127 |
units = st.selectbox("Units system", options=["SI (mm, bar, °C)", "Imperial (in, psi, °F)"])
|
| 128 |
-
|
|
|
|
| 129 |
u_len = "mm"
|
| 130 |
u_press = "bar"
|
| 131 |
u_temp = "°C"
|
|
@@ -135,31 +126,81 @@ with st.sidebar:
|
|
| 135 |
u_temp = "°F"
|
| 136 |
|
| 137 |
st.subheader("Geometry")
|
| 138 |
-
OD_ts = st.number_input(f"Tubesheet outer diameter ({u_len})",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
effective_radius_override = st.checkbox("Override effective calculation radius (optional)")
|
| 140 |
if effective_radius_override:
|
| 141 |
-
eff_radius = st.number_input(f"Effective radius ({u_len})",
|
|
|
|
|
|
|
|
|
|
| 142 |
else:
|
| 143 |
eff_radius = None
|
| 144 |
|
| 145 |
st.subheader("Tube field")
|
| 146 |
N_tubes = st.number_input("Number of tubes (N)", min_value=1, value=100, step=1)
|
| 147 |
-
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
layout = st.selectbox("Tube pitch layout", options=["Triangular", "Square"])
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
st.subheader("Pressure & Temperature")
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
st.subheader("Material & Allowances")
|
| 159 |
material_list = ["SA-516 Gr 70", "SA-240 Type 304", "SA-240 Type 316", "SA-105", "SA-36", "SA-387 Grade 11", "Other / Manual"]
|
| 160 |
material = st.selectbox("Tubesheet material (ASME II examples)", options=material_list)
|
|
|
|
| 161 |
# Determine default usable temperature for lookup
|
| 162 |
-
if
|
| 163 |
T_shell_C = float(T_shell)
|
| 164 |
T_tube_C = float(T_tube)
|
| 165 |
else:
|
|
@@ -179,14 +220,21 @@ with st.sidebar:
|
|
| 179 |
|
| 180 |
S_override = st.checkbox("Manual override allowable stress (MPa)")
|
| 181 |
if S_override:
|
| 182 |
-
|
|
|
|
| 183 |
else:
|
| 184 |
S_allowable_manual = None
|
| 185 |
|
| 186 |
st.subheader("Allowances & Options")
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
edge_condition = st.selectbox("Edge condition (plate support)", options=["Clamped (conservative)", "Simply supported (less conservative)"])
|
| 191 |
safety_factor = st.number_input("Conservative multiplier (SF) applied to allowable stress", min_value=0.5, max_value=3.0, value=1.0, step=0.05)
|
| 192 |
show_detail = st.checkbox("Show detailed calculations (expandable)", value=False)
|
|
@@ -196,9 +244,9 @@ with st.sidebar:
|
|
| 196 |
# Calculation logic
|
| 197 |
# -----------------------
|
| 198 |
def compute_all():
|
| 199 |
-
# Unit conversions into consistent
|
| 200 |
# lengths in mm, pressure in MPa, stresses in MPa
|
| 201 |
-
if
|
| 202 |
OD_ts_mm = float(OD_ts)
|
| 203 |
OD_tube_mm = float(OD_tube)
|
| 204 |
pitch_mm = float(pitch)
|
|
@@ -223,7 +271,7 @@ def compute_all():
|
|
| 223 |
|
| 224 |
# effective radius
|
| 225 |
if eff_radius is not None:
|
| 226 |
-
if
|
| 227 |
a_mm = float(eff_radius)
|
| 228 |
else:
|
| 229 |
a_mm = inchtomm(float(eff_radius))
|
|
@@ -232,8 +280,7 @@ def compute_all():
|
|
| 232 |
|
| 233 |
# differential pressure
|
| 234 |
deltaP_MPa = abs(P_shell_MPa - P_tube_MPa) # MPa
|
| 235 |
-
|
| 236 |
-
q = deltaP_MPa # MPa (N/mm^2)
|
| 237 |
|
| 238 |
# allowable stress selection
|
| 239 |
if S_override and S_allowable_manual is not None:
|
|
@@ -252,19 +299,14 @@ def compute_all():
|
|
| 252 |
if edge_condition.startswith("Clamped"):
|
| 253 |
k = 0.308
|
| 254 |
else:
|
| 255 |
-
# approximate simply supported coefficient (less conservative)
|
| 256 |
k = 0.375
|
| 257 |
|
| 258 |
-
# Apply safety factor (applied to allowable stress denominator)
|
| 259 |
SF = float(safety_factor)
|
| 260 |
|
| 261 |
# required thickness from plate bending approx (mm)
|
| 262 |
-
# formula used: t_req = sqrt( (q * a^2) / (k * S_allow * SF) )
|
| 263 |
-
# Units note: q and S_allow are MPa (N/mm^2), a in mm -> t in mm (consistent algebraically for this approximation)
|
| 264 |
a = a_mm
|
| 265 |
numerator = q * (a ** 2)
|
| 266 |
denom = k * S_allow_MPa * SF
|
| 267 |
-
# guard against negative/zero denom
|
| 268 |
if denom <= 0:
|
| 269 |
t_req_mm = 0.0
|
| 270 |
else:
|
|
@@ -277,21 +319,16 @@ def compute_all():
|
|
| 277 |
|
| 278 |
# local bearing / ligament check (simple)
|
| 279 |
net_area_mm2 = max(plate_area_mm2 - hole_area_mm2, 1.0)
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
avg_membrane_stress_approx = (deltaP_MPa * plate_area_mm2) / net_area_mm2 # MPa (approx)
|
| 283 |
-
bearing_flag = avg_membrane_stress_approx > S_allow_MPa * 0.9 # warn if close to allowable
|
| 284 |
|
| 285 |
# final thickness with allowances
|
| 286 |
t_final_unrounded_mm = t_req_mm + CA_mm + machining_mm
|
| 287 |
-
# enforce min manufacturing thickness
|
| 288 |
t_final_unrounded_mm = max(t_final_unrounded_mm, t_min_mm)
|
| 289 |
-
|
| 290 |
-
# rounding
|
| 291 |
t_final_rounded_mm = round_up_standard_mm(t_final_unrounded_mm)
|
| 292 |
|
| 293 |
-
#
|
| 294 |
-
if
|
| 295 |
results = {
|
| 296 |
"t_req_mm": t_req_mm,
|
| 297 |
"t_final_unrounded_mm": t_final_unrounded_mm,
|
|
@@ -301,7 +338,8 @@ def compute_all():
|
|
| 301 |
"hole_removed_pct": hole_removed_pct,
|
| 302 |
"avg_membrane_stress_approx_MPa": avg_membrane_stress_approx,
|
| 303 |
"bearing_flag": bearing_flag,
|
| 304 |
-
"a_mm": a_mm
|
|
|
|
| 305 |
}
|
| 306 |
else:
|
| 307 |
results = {
|
|
@@ -316,7 +354,8 @@ def compute_all():
|
|
| 316 |
"avg_membrane_stress_approx_MPa": avg_membrane_stress_approx,
|
| 317 |
"avg_membrane_stress_approx_psi": mpato_psi(avg_membrane_stress_approx),
|
| 318 |
"bearing_flag": bearing_flag,
|
| 319 |
-
"a_in": mmtoinch(a_mm)
|
|
|
|
| 320 |
}
|
| 321 |
|
| 322 |
# build calculation log
|
|
@@ -326,8 +365,8 @@ def compute_all():
|
|
| 326 |
log_lines.append("---- Units (internal calculations): lengths=mm, pressures=MPa, stress=MPa ----")
|
| 327 |
log_lines.append(f"Selected units: {units}")
|
| 328 |
log_lines.append(f"OD_ts = {OD_ts} {u_len} -> a = {a_mm:.2f} mm")
|
| 329 |
-
log_lines.append(f"N_tubes = {N_tubes}, OD_tube = {OD_tube} {u_len} -> OD_tube_mm = {OD_tube_mm:.
|
| 330 |
-
log_lines.append(f"Pressures: P_shell = {P_shell} {u_press} ({P_shell_MPa:.
|
| 331 |
log_lines.append(f"ΔP (governing) = |P_shell - P_tube| = {deltaP_MPa:.6f} MPa")
|
| 332 |
log_lines.append(f"Material: {material} (S source: {source_S}), S_used = {S_allow_MPa:.3f} MPa")
|
| 333 |
log_lines.append(f"Edge condition k = {k}, safety multiplier SF = {SF}")
|
|
@@ -345,7 +384,6 @@ def compute_all():
|
|
| 345 |
|
| 346 |
# produce summary table (pandas)
|
| 347 |
summary = []
|
| 348 |
-
# inputs
|
| 349 |
summary.append(["Tubesheet OD", f"{OD_ts} {u_len}"])
|
| 350 |
summary.append(["Number of tubes", f"{N_tubes}"])
|
| 351 |
summary.append(["Tube OD", f"{OD_tube} {u_len}"])
|
|
@@ -353,7 +391,7 @@ def compute_all():
|
|
| 353 |
summary.append(["Material", material])
|
| 354 |
summary.append(["S_used (MPa)", f"{S_allow_MPa:.3f}"])
|
| 355 |
summary.append(["ΔP", f"{deltaP_MPa:.6f} MPa ({mpato_psi(deltaP_MPa):.2f} psi)"])
|
| 356 |
-
if
|
| 357 |
summary.append(["t_req (mm)", f"{t_req_mm:.3f}"])
|
| 358 |
summary.append(["t_final_unrounded (mm)", f"{t_final_unrounded_mm:.3f}"])
|
| 359 |
summary.append(["t_final_rounded (mm)", f"{t_final_rounded_mm:.3f}"])
|
|
@@ -379,7 +417,7 @@ if compute:
|
|
| 379 |
|
| 380 |
# Show KPI cards
|
| 381 |
st.subheader("Results")
|
| 382 |
-
if
|
| 383 |
c1, c2, c3, c4 = st.columns(4)
|
| 384 |
c1.metric("Required thickness (t_req)", f"{results['t_req_mm']:.2f} mm")
|
| 385 |
c2.metric("Final thickness (unrounded)", f"{results['t_final_unrounded_mm']:.2f} mm")
|
|
@@ -402,22 +440,15 @@ if compute:
|
|
| 402 |
|
| 403 |
with right:
|
| 404 |
st.markdown("**Schematic (not to scale)**")
|
| 405 |
-
# create a simple schematic: circle for tubesheet, points for tube field (
|
| 406 |
-
if units.startswith("SI"):
|
| 407 |
-
radius_plot = float(OD_ts)/2.0
|
| 408 |
-
else:
|
| 409 |
-
radius_plot = float(OD_ts)/2.0 # in inches; visual only
|
| 410 |
-
# We'll plot using normalized coordinates
|
| 411 |
fig = go.Figure()
|
| 412 |
-
# circle
|
| 413 |
theta = np.linspace(0, 2*np.pi, 200)
|
| 414 |
fig.add_trace(go.Scatter(x=np.cos(theta), y=np.sin(theta), mode="lines", name="Tubesheet OD",
|
| 415 |
line=dict(color="RoyalBlue")))
|
| 416 |
-
|
| 417 |
-
n_mark = min(200, int(N_tubes))
|
| 418 |
-
r = 0.85
|
| 419 |
xs = []
|
| 420 |
ys = []
|
|
|
|
| 421 |
for i in range(n_mark):
|
| 422 |
ang = 2*np.pi*i / n_mark
|
| 423 |
xs.append(0.5 * r * np.cos(ang))
|
|
@@ -444,12 +475,12 @@ else:
|
|
| 444 |
# -----------------------
|
| 445 |
# Presets (quick-load examples)
|
| 446 |
# -----------------------
|
|
|
|
| 447 |
st.sidebar.markdown("---")
|
| 448 |
st.sidebar.markdown("### Presets")
|
| 449 |
-
if st.sidebar.button("
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
st.experimental_rerun()
|
| 454 |
|
| 455 |
# End of app
|
|
|
|
| 2 |
Tubesheet Thickness Calculator (ASME VIII-1 preliminary)
|
| 3 |
Single-file Streamlit app.
|
| 4 |
|
| 5 |
+
Updated: fixed tube OD / pitch input limits so small imperial fractions (< 1") are allowed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
import streamlit as st
|
|
|
|
| 19 |
# -----------------------
|
| 20 |
# Bundled (example) material table (sample values)
|
| 21 |
# NOTE: These are illustrative. Replace with verified ASME Section II data for production use.
|
|
|
|
| 22 |
MATERIAL_LOOKUP = {
|
| 23 |
"SA-516 Gr 70": [
|
| 24 |
{"temperature_C": 20, "allowable_MPa": 138},
|
|
|
|
| 115 |
project_name = st.text_input("Project name", value="Tubesheet Calculation")
|
| 116 |
designer_name = st.text_input("Designer", value="")
|
| 117 |
units = st.selectbox("Units system", options=["SI (mm, bar, °C)", "Imperial (in, psi, °F)"])
|
| 118 |
+
use_SI = units.startswith("SI")
|
| 119 |
+
if use_SI:
|
| 120 |
u_len = "mm"
|
| 121 |
u_press = "bar"
|
| 122 |
u_temp = "°C"
|
|
|
|
| 126 |
u_temp = "°F"
|
| 127 |
|
| 128 |
st.subheader("Geometry")
|
| 129 |
+
OD_ts = st.number_input(f"Tubesheet outer diameter ({u_len})",
|
| 130 |
+
min_value=10.0 if use_SI else 0.5,
|
| 131 |
+
value=1000.0 if use_SI else 40.0,
|
| 132 |
+
step=1.0 if use_SI else 0.25)
|
| 133 |
+
|
| 134 |
effective_radius_override = st.checkbox("Override effective calculation radius (optional)")
|
| 135 |
if effective_radius_override:
|
| 136 |
+
eff_radius = st.number_input(f"Effective radius ({u_len})",
|
| 137 |
+
min_value=1.0 if use_SI else 0.1,
|
| 138 |
+
value=(OD_ts/2.0),
|
| 139 |
+
step=1.0 if use_SI else 0.01)
|
| 140 |
else:
|
| 141 |
eff_radius = None
|
| 142 |
|
| 143 |
st.subheader("Tube field")
|
| 144 |
N_tubes = st.number_input("Number of tubes (N)", min_value=1, value=100, step=1)
|
| 145 |
+
|
| 146 |
+
# Allow small tube ODs: set min depending on units. e.g., allow 0.5 mm or 1/32 in
|
| 147 |
+
if use_SI:
|
| 148 |
+
tube_od_min = 0.5 # mm
|
| 149 |
+
tube_od_default = 25.4 # mm
|
| 150 |
+
tube_od_step = 0.1
|
| 151 |
+
else:
|
| 152 |
+
tube_od_min = 1.0 / 64.0 # 1/64 in ~0.015625 in
|
| 153 |
+
tube_od_default = 1.0 # in
|
| 154 |
+
tube_od_step = 1.0 / 64.0
|
| 155 |
+
|
| 156 |
+
OD_tube = st.number_input(f"Tube outside diameter ({u_len})",
|
| 157 |
+
min_value=tube_od_min,
|
| 158 |
+
value=tube_od_default,
|
| 159 |
+
step=tube_od_step,
|
| 160 |
+
format="%.6f" if not use_SI else "%.3f")
|
| 161 |
+
|
| 162 |
+
# tube wall thickness min: allow small values
|
| 163 |
+
if use_SI:
|
| 164 |
+
t_tube_min = 0.1
|
| 165 |
+
t_tube_default = 1.0
|
| 166 |
+
else:
|
| 167 |
+
t_tube_min = 1.0 / 128.0
|
| 168 |
+
t_tube_default = 0.035 # ~0.035 in ~0.9 mm
|
| 169 |
+
t_tube = st.number_input(f"Tube wall thickness ({u_len})",
|
| 170 |
+
min_value=t_tube_min,
|
| 171 |
+
value=t_tube_default,
|
| 172 |
+
step=0.01,
|
| 173 |
+
format="%.6f" if not use_SI else "%.3f")
|
| 174 |
+
|
| 175 |
layout = st.selectbox("Tube pitch layout", options=["Triangular", "Square"])
|
| 176 |
+
|
| 177 |
+
# compute default pitch based on OD_tube; user can edit
|
| 178 |
+
default_pitch = (OD_tube * 1.25) if (OD_tube is not None) else (25.4 * 1.25 if use_SI else 1.0 * 1.25)
|
| 179 |
+
pitch = st.number_input(f"Tube pitch ({u_len})",
|
| 180 |
+
min_value=max(OD_tube * 1.0, tube_od_min),
|
| 181 |
+
value=default_pitch,
|
| 182 |
+
step=tube_od_step,
|
| 183 |
+
format="%.6f" if not use_SI else "%.3f")
|
| 184 |
|
| 185 |
st.subheader("Pressure & Temperature")
|
| 186 |
+
# default pressures
|
| 187 |
+
if use_SI:
|
| 188 |
+
P_shell = st.number_input(f"Design shell-side pressure ({u_press})", value=3.0, step=0.1)
|
| 189 |
+
P_tube = st.number_input(f"Design tube-side pressure ({u_press})", value=1.0, step=0.1)
|
| 190 |
+
T_shell = st.number_input(f"Design shell-side temperature ({u_temp})", value=100.0, step=1.0)
|
| 191 |
+
T_tube = st.number_input(f"Design tube-side temperature ({u_temp})", value=100.0, step=1.0)
|
| 192 |
+
else:
|
| 193 |
+
P_shell = st.number_input(f"Design shell-side pressure ({u_press})", value=50.0, step=1.0)
|
| 194 |
+
P_tube = st.number_input(f"Design tube-side pressure ({u_press})", value=14.7, step=1.0)
|
| 195 |
+
T_shell = st.number_input(f"Design shell-side temperature ({u_temp})", value=212.0, step=1.0)
|
| 196 |
+
T_tube = st.number_input(f"Design tube-side temperature ({u_temp})", value=212.0, step=1.0)
|
| 197 |
|
| 198 |
st.subheader("Material & Allowances")
|
| 199 |
material_list = ["SA-516 Gr 70", "SA-240 Type 304", "SA-240 Type 316", "SA-105", "SA-36", "SA-387 Grade 11", "Other / Manual"]
|
| 200 |
material = st.selectbox("Tubesheet material (ASME II examples)", options=material_list)
|
| 201 |
+
|
| 202 |
# Determine default usable temperature for lookup
|
| 203 |
+
if use_SI:
|
| 204 |
T_shell_C = float(T_shell)
|
| 205 |
T_tube_C = float(T_tube)
|
| 206 |
else:
|
|
|
|
| 220 |
|
| 221 |
S_override = st.checkbox("Manual override allowable stress (MPa)")
|
| 222 |
if S_override:
|
| 223 |
+
default_S = S_allowable_auto if S_allowable_auto is not None else 100.0
|
| 224 |
+
S_allowable_manual = st.number_input("S_allowable (MPa)", min_value=1.0, value=float(default_S), step=1.0)
|
| 225 |
else:
|
| 226 |
S_allowable_manual = None
|
| 227 |
|
| 228 |
st.subheader("Allowances & Options")
|
| 229 |
+
if use_SI:
|
| 230 |
+
CA = st.number_input(f"Corrosion allowance ({u_len})", min_value=0.0, value=1.0, step=0.1)
|
| 231 |
+
machining = st.number_input(f"Machining allowance ({u_len})", min_value=0.0, value=2.0, step=0.1)
|
| 232 |
+
t_min_manufacture = st.number_input(f"Min manufacturing thickness ({u_len})", min_value=0.1, value=6.0, step=0.1)
|
| 233 |
+
else:
|
| 234 |
+
CA = st.number_input(f"Corrosion allowance ({u_len})", min_value=0.0, value=0.04, step=0.01)
|
| 235 |
+
machining = st.number_input(f"Machining allowance ({u_len})", min_value=0.0, value=0.08, step=0.01)
|
| 236 |
+
t_min_manufacture = st.number_input(f"Min manufacturing thickness ({u_len})", min_value=0.01, value=0.25, step=0.01)
|
| 237 |
+
|
| 238 |
edge_condition = st.selectbox("Edge condition (plate support)", options=["Clamped (conservative)", "Simply supported (less conservative)"])
|
| 239 |
safety_factor = st.number_input("Conservative multiplier (SF) applied to allowable stress", min_value=0.5, max_value=3.0, value=1.0, step=0.05)
|
| 240 |
show_detail = st.checkbox("Show detailed calculations (expandable)", value=False)
|
|
|
|
| 244 |
# Calculation logic
|
| 245 |
# -----------------------
|
| 246 |
def compute_all():
|
| 247 |
+
# Unit conversions into consistent internal units:
|
| 248 |
# lengths in mm, pressure in MPa, stresses in MPa
|
| 249 |
+
if use_SI:
|
| 250 |
OD_ts_mm = float(OD_ts)
|
| 251 |
OD_tube_mm = float(OD_tube)
|
| 252 |
pitch_mm = float(pitch)
|
|
|
|
| 271 |
|
| 272 |
# effective radius
|
| 273 |
if eff_radius is not None:
|
| 274 |
+
if use_SI:
|
| 275 |
a_mm = float(eff_radius)
|
| 276 |
else:
|
| 277 |
a_mm = inchtomm(float(eff_radius))
|
|
|
|
| 280 |
|
| 281 |
# differential pressure
|
| 282 |
deltaP_MPa = abs(P_shell_MPa - P_tube_MPa) # MPa
|
| 283 |
+
q = deltaP_MPa # MPa (N/mm^2) as a uniform load approximation
|
|
|
|
| 284 |
|
| 285 |
# allowable stress selection
|
| 286 |
if S_override and S_allowable_manual is not None:
|
|
|
|
| 299 |
if edge_condition.startswith("Clamped"):
|
| 300 |
k = 0.308
|
| 301 |
else:
|
|
|
|
| 302 |
k = 0.375
|
| 303 |
|
|
|
|
| 304 |
SF = float(safety_factor)
|
| 305 |
|
| 306 |
# required thickness from plate bending approx (mm)
|
|
|
|
|
|
|
| 307 |
a = a_mm
|
| 308 |
numerator = q * (a ** 2)
|
| 309 |
denom = k * S_allow_MPa * SF
|
|
|
|
| 310 |
if denom <= 0:
|
| 311 |
t_req_mm = 0.0
|
| 312 |
else:
|
|
|
|
| 319 |
|
| 320 |
# local bearing / ligament check (simple)
|
| 321 |
net_area_mm2 = max(plate_area_mm2 - hole_area_mm2, 1.0)
|
| 322 |
+
avg_membrane_stress_approx = (deltaP_MPa * plate_area_mm2) / net_area_mm2 # approximate MPa
|
| 323 |
+
bearing_flag = avg_membrane_stress_approx > S_allow_MPa * 0.9
|
|
|
|
|
|
|
| 324 |
|
| 325 |
# final thickness with allowances
|
| 326 |
t_final_unrounded_mm = t_req_mm + CA_mm + machining_mm
|
|
|
|
| 327 |
t_final_unrounded_mm = max(t_final_unrounded_mm, t_min_mm)
|
|
|
|
|
|
|
| 328 |
t_final_rounded_mm = round_up_standard_mm(t_final_unrounded_mm)
|
| 329 |
|
| 330 |
+
# prepare results depending on units
|
| 331 |
+
if use_SI:
|
| 332 |
results = {
|
| 333 |
"t_req_mm": t_req_mm,
|
| 334 |
"t_final_unrounded_mm": t_final_unrounded_mm,
|
|
|
|
| 338 |
"hole_removed_pct": hole_removed_pct,
|
| 339 |
"avg_membrane_stress_approx_MPa": avg_membrane_stress_approx,
|
| 340 |
"bearing_flag": bearing_flag,
|
| 341 |
+
"a_mm": a_mm,
|
| 342 |
+
"source_S": source_S
|
| 343 |
}
|
| 344 |
else:
|
| 345 |
results = {
|
|
|
|
| 354 |
"avg_membrane_stress_approx_MPa": avg_membrane_stress_approx,
|
| 355 |
"avg_membrane_stress_approx_psi": mpato_psi(avg_membrane_stress_approx),
|
| 356 |
"bearing_flag": bearing_flag,
|
| 357 |
+
"a_in": mmtoinch(a_mm),
|
| 358 |
+
"source_S": source_S
|
| 359 |
}
|
| 360 |
|
| 361 |
# build calculation log
|
|
|
|
| 365 |
log_lines.append("---- Units (internal calculations): lengths=mm, pressures=MPa, stress=MPa ----")
|
| 366 |
log_lines.append(f"Selected units: {units}")
|
| 367 |
log_lines.append(f"OD_ts = {OD_ts} {u_len} -> a = {a_mm:.2f} mm")
|
| 368 |
+
log_lines.append(f"N_tubes = {N_tubes}, OD_tube = {OD_tube} {u_len} -> OD_tube_mm = {OD_tube_mm:.6f} mm")
|
| 369 |
+
log_lines.append(f"Pressures: P_shell = {P_shell} {u_press} ({P_shell_MPa:.6f} MPa), P_tube = {P_tube} {u_press} ({P_tube_MPa:.6f} MPa)")
|
| 370 |
log_lines.append(f"ΔP (governing) = |P_shell - P_tube| = {deltaP_MPa:.6f} MPa")
|
| 371 |
log_lines.append(f"Material: {material} (S source: {source_S}), S_used = {S_allow_MPa:.3f} MPa")
|
| 372 |
log_lines.append(f"Edge condition k = {k}, safety multiplier SF = {SF}")
|
|
|
|
| 384 |
|
| 385 |
# produce summary table (pandas)
|
| 386 |
summary = []
|
|
|
|
| 387 |
summary.append(["Tubesheet OD", f"{OD_ts} {u_len}"])
|
| 388 |
summary.append(["Number of tubes", f"{N_tubes}"])
|
| 389 |
summary.append(["Tube OD", f"{OD_tube} {u_len}"])
|
|
|
|
| 391 |
summary.append(["Material", material])
|
| 392 |
summary.append(["S_used (MPa)", f"{S_allow_MPa:.3f}"])
|
| 393 |
summary.append(["ΔP", f"{deltaP_MPa:.6f} MPa ({mpato_psi(deltaP_MPa):.2f} psi)"])
|
| 394 |
+
if use_SI:
|
| 395 |
summary.append(["t_req (mm)", f"{t_req_mm:.3f}"])
|
| 396 |
summary.append(["t_final_unrounded (mm)", f"{t_final_unrounded_mm:.3f}"])
|
| 397 |
summary.append(["t_final_rounded (mm)", f"{t_final_rounded_mm:.3f}"])
|
|
|
|
| 417 |
|
| 418 |
# Show KPI cards
|
| 419 |
st.subheader("Results")
|
| 420 |
+
if use_SI:
|
| 421 |
c1, c2, c3, c4 = st.columns(4)
|
| 422 |
c1.metric("Required thickness (t_req)", f"{results['t_req_mm']:.2f} mm")
|
| 423 |
c2.metric("Final thickness (unrounded)", f"{results['t_final_unrounded_mm']:.2f} mm")
|
|
|
|
| 440 |
|
| 441 |
with right:
|
| 442 |
st.markdown("**Schematic (not to scale)**")
|
| 443 |
+
# create a simple schematic: circle for tubesheet, points for tube field (visual)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
fig = go.Figure()
|
|
|
|
| 445 |
theta = np.linspace(0, 2*np.pi, 200)
|
| 446 |
fig.add_trace(go.Scatter(x=np.cos(theta), y=np.sin(theta), mode="lines", name="Tubesheet OD",
|
| 447 |
line=dict(color="RoyalBlue")))
|
| 448 |
+
n_mark = min(300, int(N_tubes))
|
|
|
|
|
|
|
| 449 |
xs = []
|
| 450 |
ys = []
|
| 451 |
+
r = 0.85
|
| 452 |
for i in range(n_mark):
|
| 453 |
ang = 2*np.pi*i / n_mark
|
| 454 |
xs.append(0.5 * r * np.cos(ang))
|
|
|
|
| 475 |
# -----------------------
|
| 476 |
# Presets (quick-load examples)
|
| 477 |
# -----------------------
|
| 478 |
+
# Simple preset buttons that instruct the user to manually change fields if needed.
|
| 479 |
st.sidebar.markdown("---")
|
| 480 |
st.sidebar.markdown("### Presets")
|
| 481 |
+
if st.sidebar.button("Example 1 (SI)"):
|
| 482 |
+
st.write("Preset example: OD 1000 mm, 100 tubes, tube OD 25.4 mm, P_shell=3 bar, P_tube=1 bar.")
|
| 483 |
+
if st.sidebar.button("Example 2 (Imperial)"):
|
| 484 |
+
st.write("Preset example: OD 40 in, 200 tubes, tube OD 0.75 in, P_shell=50 psi, P_tube=14.7 psi")
|
|
|
|
| 485 |
|
| 486 |
# End of app
|