Umar4321 commited on
Commit
80419a4
·
verified ·
1 Parent(s): b886d40

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +92 -61
app.py CHANGED
@@ -7,7 +7,7 @@ Single-file Streamlit app with:
7
  - Material database (selectable)
8
  - UG-27 shell thickness (circumferential & longitudinal checks)
9
  - UG-32 head thickness (ellipsoidal / torispherical / hemispherical)
10
- - Simplified UG-37-style nozzle reinforcement area method (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
@@ -29,7 +29,7 @@ import pandas as pd
29
  import streamlit as st
30
 
31
  # Module metadata
32
- __version__ = "0.1.0"
33
 
34
  # ---------------------------
35
  # Constants
@@ -37,7 +37,7 @@ __version__ = "0.1.0"
37
  ATM_PRESSURE_PSI = 14.7 # used only for context
38
  PIPE_THICKNESS_REDUCTION = 0.125 # 12.5% reduction for pipe grades
39
  DEFAULT_CORROSION_ALLOWANCE_IN = 0.125
40
- SUPPORTED_ASME_EDITIONS = ["2019", "2021", "custom"]
41
 
42
  # Units formatting
43
  INCH_DECIMALS = 4
@@ -339,64 +339,84 @@ def calculate_head_thickness(P: float, D: float, S: float, E: float, head_type:
339
  }
340
 
341
 
342
- def calculate_nozzle_reinforcement(d_opening: float, t_shell_required: float, t_nozzle: float, t_pad: float, pad_width: float, material_grade: Optional[str] = None) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
343
  """
344
- Simplified UG-37 area method approximation.
345
- - A_required = d_opening * t_shell_required
346
- - A_nozzle approximated as t_nozzle * height (height approximated)
347
- - A_pad = t_pad * pad_width
348
- - A_shell conservative assumed zero (unless pad provides)
349
- ASSUMPTION: This is conservative and simplified; real UG-37 includes integration over reinforcement area and weld contributions.
 
 
 
 
 
350
  """
351
- if d_opening <= 0 or t_shell_required <= 0 or t_nozzle <= 0:
352
- raise ValueError("Nozzle dimensions must be positive")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
- A_required = d_opening * t_shell_required
355
-
356
- # Approximate nozzle contribution: height limited to 2.5 * smaller thickness
357
- height_limit = min(2.5 * t_shell_required, 2.5 * t_nozzle)
358
- A_nozzle = t_nozzle * height_limit
359
-
360
- # Pad contribution
361
- A_pad = t_pad * pad_width if (t_pad and pad_width) else 0.0
362
-
363
- # Shell area conservative: we count none (ASSUMPTION: conservative)
364
- A_shell = 0.0
365
-
366
- # Apply pipe grade thickness reduction for nozzle if material_grade indicates pipe
367
  reduction_info = ""
 
368
  if material_grade:
369
- # find if given material is in data and pipe grade
370
  for cat in MATERIALS:
371
  mg = MATERIALS[cat].get(material_grade)
372
  if mg:
373
  if mg.get("is_pipe_grade"):
374
- # reduce nozzle effective thickness for reinforcement calc
375
- effective_nozzle_t = t_nozzle * (1.0 - PIPE_THICKNESS_REDUCTION)
376
- # recalc A_nozzle conservatively with reduced t
377
- height_limit2 = min(2.5 * t_shell_required, 2.5 * effective_nozzle_t)
378
- A_nozzle = effective_nozzle_t * height_limit2
379
- reduction_info = f"Applied {PIPE_THICKNESS_REDUCTION*100:.1f}% pipe thickness reduction to nozzle (effective nozzle thickness = {effective_nozzle_t:.4f})."
380
  break
381
 
382
- A_available = A_shell + A_nozzle + A_pad
 
383
  adequate = A_available >= A_required
384
  safety_factor = (A_available / A_required) if A_required > 0 else float("inf")
385
 
386
  assumptions = [
387
- "A_shell conservative assumed zero (no excess shell area counted).",
388
- "Nozzle area approximated with a limited height = min(2.5*t_shell, 2.5*t_nozzle).",
389
- "Pad area = pad_thickness * pad_width (rectangular assumption).",
390
- "Real UG-37 requires geometric reinforcement calculations; this is a simplified conservative estimate.",
 
391
  ]
392
  if reduction_info:
393
  assumptions.append(reduction_info)
394
 
395
  return {
396
  "area_required": float(A_required),
397
- "area_shell": float(A_shell),
398
  "area_nozzle": float(A_nozzle),
399
  "area_pad": float(A_pad),
 
400
  "area_available_total": float(A_available),
401
  "reinforcement_adequate": bool(adequate),
402
  "safety_factor": float(safety_factor),
@@ -442,14 +462,10 @@ def _approximate_rated_mdmt(material_group: str, thickness_in_in: float) -> floa
442
  """
443
  APPROXIMATION: Provide a conservative approximate rated MDMT based on material group and thickness.
444
  This is NOT the full UCS-66 lookup. Use as a placeholder to compare with Design MDMT.
445
- We define a simple table for P1 and P8:
446
- - For P1 (carbon): thicker -> higher (warmer) rated MDMT threshold (conservative)
447
- - For P8 (stainless): very low rated MDMT (stainless generally exempt)
448
- Returns MDMT in °F.
449
  """
450
  if material_group == "P8":
451
- return -325.0 # effectively exempt in many cases (very low)
452
- # P1 conservative regression-like mapping (values illustrative)
453
  if thickness_in_in <= 0.5:
454
  return 60.0
455
  if thickness_in_in <= 1.0:
@@ -559,6 +575,9 @@ def init_session_state_defaults():
559
  "use_reinforcing_pad": False,
560
  "pad_thickness": 0.25,
561
  "pad_width": 8.0,
 
 
 
562
  "calc_atm_shell": False,
563
  "asme_edition": SUPPORTED_ASME_EDITIONS[0],
564
  }
@@ -627,8 +646,6 @@ def run_calculations(inputs: Dict[str, Any]) -> Dict[str, Any]:
627
  # Optionally compute atmospheric shell
628
  atm_shell = None
629
  if inputs.get("calc_atm_shell"):
630
- # For atmospheric only, P = 0 -> thickness tends to 0. Use minimal thickness assumption.
631
- # ASSUMPTION: Provide thickness for vacuum containment? Here we just return a message.
632
  atm_shell = {"note": "Atmospheric shell check requested: with P=0 this calculation is geometry-specific. Provide loading (wind, vacuum) for meaningful thickness check."}
633
 
634
  # Head thickness
@@ -636,12 +653,17 @@ def run_calculations(inputs: Dict[str, Any]) -> Dict[str, Any]:
636
  head_required = head["required_thickness"]
637
  total_head = head_required + ca
638
 
639
- # Nozzle reinforcement
640
  nozzle = calculate_nozzle_reinforcement(
641
- inputs["nozzle_opening_diameter"], total_shell, inputs["nozzle_wall_thickness"],
 
 
642
  inputs["pad_thickness"] if inputs["use_reinforcing_pad"] else 0.0,
643
  inputs["pad_width"] if inputs["use_reinforcing_pad"] else 0.0,
644
  mat_grade if mat_cat != "User Defined" else None,
 
 
 
645
  )
646
 
647
  # For pipe grades, mention 12.5% reduction in shell effective thickness for reinforcement checks
@@ -652,10 +674,7 @@ def run_calculations(inputs: Dict[str, Any]) -> Dict[str, Any]:
652
  reduced_shell_effective = total_shell
653
 
654
  # PWHT
655
- # thickness for pwht in inches: if units SI, convert mm to inches (we assume inside_diameter units but thickness returned in same length units)
656
- # ASSUMPTION: inputs thickness are in inches when unit_system == USC, mm when SI. For PWHT we need inches.
657
  if unit_system == "SI":
658
- # convert mm to inches: assume thickness in mm -> mm / 25.4
659
  thickness_in_in = total_shell / 25.4
660
  else:
661
  thickness_in_in = total_shell
@@ -705,7 +724,7 @@ def main():
705
  st.session_state["unit_system"] = unit_system
706
 
707
  # ASME edition placeholder
708
- st.session_state["asme_edition"] = st.sidebar.selectbox("ASME Section VIII Edition", SUPPORTED_ASME_EDITIONS, index=0)
709
 
710
  # Basic design
711
  pressure_label = "Design Pressure (psi)" if unit_system == "USC" else "Design Pressure (bar)"
@@ -722,7 +741,8 @@ def main():
722
  st.sidebar.markdown("---")
723
  st.session_state["inside_diameter"] = st.sidebar.number_input(length_label, value=float(st.session_state["inside_diameter"]))
724
  st.session_state["head_type"] = st.sidebar.selectbox("Head Type", ["ellipsoidal", "torispherical", "hemispherical"], index=0 if st.session_state["head_type"] == "ellipsoidal" else 0)
725
- st.session_state["joint_efficiency"] = st.sidebar.selectbox("Joint Efficiency (E)", [1.0, 0.95, 0.9, 0.85], index=0)
 
726
 
727
  st.sidebar.markdown("---")
728
  st.session_state["material_category"] = st.sidebar.selectbox("Material Category", ["Carbon Steel", "Stainless Steel", "User Defined"], index=0)
@@ -734,7 +754,6 @@ def main():
734
  st.session_state["user_defined_ce"] = st.sidebar.number_input("User-defined carbon equivalent (if known)", value=0.30)
735
  else:
736
  grades = list(MATERIALS[st.session_state["material_category"]].keys())
737
- # select current grade if available
738
  try:
739
  idx = grades.index(st.session_state["material_grade"])
740
  except Exception:
@@ -750,6 +769,12 @@ def main():
750
  st.session_state["pad_thickness"] = st.sidebar.number_input("Pad Thickness (same unit)", value=float(st.session_state["pad_thickness"]))
751
  st.session_state["pad_width"] = st.sidebar.number_input("Pad Width (same unit)", value=float(st.session_state["pad_width"]))
752
 
 
 
 
 
 
 
753
  st.sidebar.markdown("---")
754
  st.session_state["calc_atm_shell"] = st.sidebar.checkbox("Also calculate shell at atmospheric pressure", value=st.session_state["calc_atm_shell"])
755
 
@@ -782,6 +807,9 @@ def main():
782
  "use_reinforcing_pad": bool(st.session_state["use_reinforcing_pad"]),
783
  "pad_thickness": float(st.session_state.get("pad_thickness", 0.0)),
784
  "pad_width": float(st.session_state.get("pad_width", 0.0)),
 
 
 
785
  "calc_atm_shell": bool(st.session_state.get("calc_atm_shell", False)),
786
  }
787
 
@@ -823,7 +851,6 @@ def main():
823
 
824
  with st.expander("Show step-by-step shell calculations"):
825
  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']}")
826
- # Show computed values numerically
827
  st.write(shell)
828
  else:
829
  st.info("Press **Calculate** to run shell thickness calculations.")
@@ -843,23 +870,28 @@ def main():
843
  st.info("Press **Calculate** to run head thickness calculations.")
844
 
845
  with tab3:
846
- st.header("Nozzle Reinforcement (UG-37 approx)")
 
847
  if results:
848
  nozzle = results["nozzle"]
849
  units_len = "in" if unit_system == "USC" else "mm"
850
  st.write("Assumptions:")
851
  for a in nozzle["assumptions"]:
852
  st.write("-", a)
853
- st.write("Required area:", f"{format_value(nozzle['area_required'], units_len)} {units_len}^2")
854
- st.write("Available area:", f"{format_value(nozzle['area_available_total'], units_len)} {units_len}^2")
 
 
 
 
855
  if nozzle["reinforcement_adequate"]:
856
  st.success(f"Reinforcement adequate (SF = {nozzle['safety_factor']:.2f})")
857
  else:
858
  st.error(f"Reinforcement inadequate (SF = {nozzle['safety_factor']:.2f})")
859
- with st.expander("Show step-by-step nozzle calculation"):
860
  st.write(nozzle)
861
  else:
862
- st.info("Press **Calculate** to run nozzle reinforcement calculations.")
863
 
864
  with tab4:
865
  st.header("PWHT & Impact Test")
@@ -880,7 +912,6 @@ def main():
880
  st.write("Suggested hold time:", pwht["hold_time"])
881
 
882
  st.subheader("Impact Test (MDMT check)")
883
- # show rated and design MDMT
884
  rated = impact["rated_mdmt"]
885
  design = impact["design_mdmt"]
886
  if unit_system == "SI":
 
7
  - Material database (selectable)
8
  - UG-27 shell thickness (circumferential & longitudinal checks)
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
 
29
  import streamlit as st
30
 
31
  # Module metadata
32
+ __version__ = "0.1.1" # bumped for nozzle + edition + E updates
33
 
34
  # ---------------------------
35
  # Constants
 
37
  ATM_PRESSURE_PSI = 14.7 # used only for context
38
  PIPE_THICKNESS_REDUCTION = 0.125 # 12.5% reduction for pipe grades
39
  DEFAULT_CORROSION_ALLOWANCE_IN = 0.125
40
+ SUPPORTED_ASME_EDITIONS = ["2019", "2021", "2025", "custom"] # added 2025
41
 
42
  # Units formatting
43
  INCH_DECIMALS = 4
 
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
+ Improved simplified UG-37 area method approximation.
355
+
356
+ Method (conservative approximations):
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
+ # Input validation
367
+ if d_opening <= 0:
368
+ raise ValueError("Nozzle opening diameter must be positive.")
369
+ if t_shell_required <= 0:
370
+ raise ValueError("Shell required thickness must be positive.")
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),
 
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:
 
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
  }
 
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
 
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
 
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
 
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)"
 
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)
 
754
  st.session_state["user_defined_ce"] = st.sidebar.number_input("User-defined carbon equivalent (if known)", value=0.30)
755
  else:
756
  grades = list(MATERIALS[st.session_state["material_category"]].keys())
 
757
  try:
758
  idx = grades.index(st.session_state["material_grade"])
759
  except Exception:
 
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
 
 
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
 
 
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.")
 
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")
 
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":