Umar4321 commited on
Commit
0a173c8
·
verified ·
1 Parent(s): 44d4848

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -67
app.py CHANGED
@@ -2,16 +2,7 @@
2
  Tubesheet Thickness Calculator (ASME VIII-1 preliminary)
3
  Single-file Streamlit app.
4
 
5
- Features:
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
- if units.startswith("SI"):
 
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})", min_value=10.0, value=1000.0, step=1.0)
 
 
 
 
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})", min_value=1.0, value=OD_ts/2.0)
 
 
 
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
- OD_tube = st.number_input(f"Tube outside diameter ({u_len})", min_value=1.0, value=(25.4 if units.startswith("SI")==False else 25.4), step=0.1)
148
- t_tube = st.number_input(f"Tube wall thickness ({u_len})", min_value=0.1, value=1.0, step=0.1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  layout = st.selectbox("Tube pitch layout", options=["Triangular", "Square"])
150
- pitch = st.number_input(f"Tube pitch ({u_len})", min_value=OD_tube*1.0, value=OD_tube*1.25, step=0.1)
 
 
 
 
 
 
 
151
 
152
  st.subheader("Pressure & Temperature")
153
- P_shell = st.number_input(f"Design shell-side pressure ({u_press})", value=(3.0 if units.startswith("SI") else 50.0))
154
- T_shell = st.number_input(f"Design shell-side temperature ({u_temp})", value=(100.0 if units.startswith("SI") else 212.0))
155
- P_tube = st.number_input(f"Design tube-side pressure ({u_press})", value=(1.0 if units.startswith("SI") else 14.7))
156
- T_tube = st.number_input(f"Design tube-side temperature ({u_temp})", value=(100.0 if units.startswith("SI") else 212.0))
 
 
 
 
 
 
 
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 units.startswith("SI"):
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
- S_allowable_manual = st.number_input("S_allowable (MPa)", min_value=1.0, value=(S_allowable_auto if S_allowable_auto else 100.0))
 
183
  else:
184
  S_allowable_manual = None
185
 
186
  st.subheader("Allowances & Options")
187
- CA = st.number_input(f"Corrosion allowance ({u_len})", min_value=0.0, value=1.0, step=0.1)
188
- machining = st.number_input(f"Machining allowance ({u_len})", min_value=0.0, value=2.0, step=0.1)
189
- t_min_manufacture = st.number_input(f"Min manufacturing thickness ({u_len})", min_value=0.1, value=(6.0 if units.startswith("SI") else 0.25), step=0.1)
 
 
 
 
 
 
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 SI-like internal units:
200
  # lengths in mm, pressure in MPa, stresses in MPa
201
- if units.startswith("SI"):
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 units.startswith("SI"):
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
- # distributed load q (use same units as stress (MPa))
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
- # Average load on plate = q * plate_area (MPa * mm2 -> N*mm?) but we produce a simple check
281
- # calculate an approximate average membrane stress = (deltaP * plate_area) / net_area -> in MPa-like units
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
- # convert back to imperial for display if needed
294
- if units.startswith("SI"):
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:.3f} mm")
330
- log_lines.append(f"Pressures: P_shell = {P_shell} {u_press} ({P_shell_MPa:.4f} MPa), P_tube = {P_tube} {u_press} ({P_tube_MPa:.4f} 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 units.startswith("SI"):
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 units.startswith("SI"):
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 (random-ish)
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
- # place a few tube markers in a grid pattern (for visualization - not exact packing)
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("Load Example 1 (SI)"):
450
- # Example 1
451
- st.experimental_rerun() # simple rerun to allow user to change manually; easier to instruct user to fill presets manually
452
- if st.sidebar.button("Load Example 2 (Imperial)"):
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