d0fus 2.2.1__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,6 +48,7 @@ if __name__ != "__main__":
48
48
  get_Lz,
49
49
  f_He_fraction,
50
50
  f_q_profile,
51
+ f_sigmav,
51
52
  )
52
53
  from .D0FUS_radial_build_functions import (
53
54
  J_non_Cu_NbTi, J_non_Cu_Nb3Sn, J_non_Cu_REBCO,
@@ -76,6 +77,7 @@ else:
76
77
  get_Lz,
77
78
  f_He_fraction,
78
79
  f_q_profile,
80
+ f_sigmav,
79
81
  )
80
82
  from D0FUS_BIB.D0FUS_radial_build_functions import (
81
83
  J_non_Cu_NbTi, J_non_Cu_Nb3Sn, J_non_Cu_REBCO,
@@ -639,6 +641,96 @@ def plot_density_line_vol(
639
641
  # 3. Nuclear / radiation physics
640
642
  # =============================================================================
641
643
 
644
+ def plot_DT_reactivity(
645
+ T_min_keV: float = 1.0,
646
+ T_max_keV: float = 100.0,
647
+ n_points: int = 600,
648
+ T_op_min: float = 10.0,
649
+ T_op_max: float = 25.0,
650
+ save_dir: str | None = None,
651
+ ) -> None:
652
+ """
653
+ Plot the D-T Maxwellian reactivity ⟨σv⟩(T) used by D0FUS, together with
654
+ the pressure-limited fusion power density metric ⟨σv⟩/T².
655
+
656
+ Left panel:
657
+ ⟨σv⟩(T) computed from the Bosch & Hale (1992) parameterisation,
658
+ with the power plant operating window [T_op_min, T_op_max] shaded
659
+ and the reactivity maximum marked.
660
+
661
+ Right panel:
662
+ Pressure-limited figure of merit ⟨σv⟩/T² (arbitrary units).
663
+ At fixed plasma pressure p = nT, the fuel ion density scales as
664
+ n ∝ 1/T, so the volumetric fusion power density p_fus ∝ n² ⟨σv⟩
665
+ is proportional to ⟨σv⟩/T². This identifies the optimal operating
666
+ temperature for a β-limited tokamak near 14 keV.
667
+
668
+ Parameters
669
+ ----------
670
+ T_min_keV, T_max_keV : float Ion temperature scan range [keV].
671
+ n_points : int Number of temperature grid points.
672
+ T_op_min, T_op_max : float Power plant operating window bounds [keV].
673
+ save_dir : str or None
674
+
675
+ References
676
+ ----------
677
+ Bosch & Hale, Nucl. Fusion 32, 611 (1992) — DT reactivity fit (Table IV).
678
+ Freidberg, Plasma Physics and Fusion Energy (2007) — pressure-limited optimum.
679
+ """
680
+ # Temperature grid (linear, since the operating window lies in the rapid-rise zone)
681
+ T_arr = np.linspace(T_min_keV, T_max_keV, n_points)
682
+ sv_arr = f_sigmav(T_arr)
683
+
684
+ # Reactivity maximum
685
+ i_peak = int(np.argmax(sv_arr))
686
+ T_peak, sv_peak = T_arr[i_peak], sv_arr[i_peak]
687
+
688
+ # Pressure-limited metric ⟨σv⟩/T² (units arbitrary, normalised below)
689
+ metric = sv_arr / T_arr**2
690
+ i_opt = int(np.argmax(metric))
691
+ T_opt, m_opt = T_arr[i_opt], metric[i_opt]
692
+
693
+ # --- Figure ------------------------------------------------------------
694
+ fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
695
+
696
+ # Left panel: reactivity curve on log scale
697
+ ax = axes[0]
698
+ ax.semilogy(T_arr, sv_arr, color="tab:red", lw=2.0,
699
+ label="Bosch & Hale (1992)")
700
+ ax.axvspan(T_op_min, T_op_max, color="goldenrod", alpha=0.20,
701
+ label=f"Operating window\n{T_op_min:.0f}–{T_op_max:.0f} keV")
702
+ ax.axvline(T_peak, color="k", lw=1.0, ls="--",
703
+ label=f"peak: T = {T_peak:.0f} keV")
704
+ ax.set_xlabel(r"Ion temperature $T$ [keV]", fontsize=11)
705
+ ax.set_ylabel(r"$\langle\sigma v\rangle_{DT}$ [m$^3$ s$^{-1}$]", fontsize=11)
706
+ ax.set_title("D-T Maxwellian reactivity", fontsize=11)
707
+ ax.set_xlim(T_min_keV, T_max_keV)
708
+ ax.set_ylim(1e-25, 3e-21)
709
+ ax.grid(True, which="both", alpha=0.25)
710
+ ax.legend(fontsize=9, loc="lower right")
711
+
712
+ # Right panel: pressure-limited figure of merit
713
+ ax = axes[1]
714
+ ax.plot(T_arr, metric / m_opt, color="tab:blue", lw=2.0,
715
+ label=r"$\langle\sigma v\rangle / T^2$ (normalised)")
716
+ ax.axvspan(T_op_min, T_op_max, color="goldenrod", alpha=0.20,
717
+ label=f"Operating window\n{T_op_min:.0f}–{T_op_max:.0f} keV")
718
+ ax.axvline(T_opt, color="k", lw=1.0, ls="--",
719
+ label=f"optimum: T = {T_opt:.1f} keV")
720
+ ax.set_xlabel(r"Ion temperature $T$ [keV]", fontsize=11)
721
+ ax.set_ylabel(r"$\langle\sigma v\rangle / T^2$ [normalised]", fontsize=11)
722
+ ax.set_title(r"Pressure-limited fusion power figure of merit", fontsize=11)
723
+ ax.set_xlim(0, 50)
724
+ ax.set_ylim(0, 1.08)
725
+ ax.grid(True, alpha=0.25)
726
+ ax.legend(fontsize=9, loc="lower right")
727
+
728
+ plt.suptitle("Fusion reactivity and operating temperature window",
729
+ fontsize=12, fontweight="bold")
730
+ plt.tight_layout()
731
+ _save_or_show(fig, save_dir, "DT_reactivity")
732
+
733
+
642
734
  def plot_Lz_cooling(
643
735
  Te_min_keV: float = 0.05,
644
736
  Te_max_keV: float = 100.0,
@@ -711,7 +803,8 @@ def plot_He_fraction(
711
803
  nbar: float = 1.0,
712
804
  Tbar: float = 8.9,
713
805
  tauE: float = 3.7,
714
- C_Alpha: float = 5.0,
806
+ C_Alpha_ITER: float = 5.0,
807
+ C_Alpha_DEMO: float = 7.0,
715
808
  nu_T: float = 1.0,
716
809
  save_dir: str | None = None,
717
810
  ) -> None:
@@ -720,16 +813,25 @@ def plot_He_fraction(
720
813
  C_α = τ_α / τ_E for ITER and EU-DEMO reference parameters, comparing
721
814
  academic (no pedestal) and H-mode pedestal profile assumptions.
722
815
 
816
+ Two D0FUS default values for C_α are highlighted with vertical dotted
817
+ lines: C_α = 5 for ITER (consistent with Progress in the ITER Physics
818
+ Basis projections) and C_α = 7 for EU-DEMO 2017 (PROCESS reference run).
819
+
723
820
  Parameters
724
821
  ----------
725
822
  nbar, Tbar, tauE : float Reference plasma parameters [10²⁰ m⁻³, keV, s].
726
- C_Alpha : float Reference C_α value (drawn as vertical line) [-].
823
+ C_Alpha_ITER : float D0FUS default C_α for ITER (vertical line) [-].
824
+ C_Alpha_DEMO : float D0FUS default C_α for EU-DEMO 2017 (vertical line) [-].
727
825
  nu_T : float Temperature peaking exponent [-].
728
826
  save_dir : str or None
729
827
 
730
828
  References
731
829
  ----------
732
830
  ITER Physics Basis, Nucl. Fusion 39, §2.4 (1999).
831
+ Shimada et al., Progress in the ITER Physics Basis, Ch. 1,
832
+ Nucl. Fusion 47, S1 (2007).
833
+ Kovari et al., Fus. Eng. Des. 89, 3054 (2014) — PROCESS systems code.
834
+ Reiter, Wolf and Kever, Nucl. Fusion 30, 2141 (1990) — ignition bound on C_α.
733
835
  """
734
836
  C_arr = np.linspace(2, 15, 150)
735
837
 
@@ -739,16 +841,22 @@ def plot_He_fraction(
739
841
  for C in C_arr]
740
842
  fa_DEMO = [f_He_fraction(1.2, 12.5, 4.6, C, nu_T) * 100 for C in C_arr]
741
843
 
742
- fig, ax = plt.subplots(figsize=(6.5, 4.2))
743
- ax.plot(C_arr, fa_ITER, "b-", lw=1.8, label="ITER — academic (no pedestal)")
744
- ax.plot(C_arr, fa_ITER_ped, "b--", lw=1.4, label="ITER — Refined H-mode pedestal")
745
- ax.plot(C_arr, fa_DEMO, "r-", lw=1.8, label="EU-DEMO — academic")
746
- ax.axvline(C_Alpha, color="k", lw=0.9, ls=":", label=f"$C_\\alpha$ = {C_Alpha:.0f}")
747
- ax.axhspan(4, 6, color="grey", alpha=0.12, label="ITER target 5 %")
748
- ax.set_xlabel(r"Removal efficiency $C_\alpha = \tau_\alpha / \tau_E$", fontsize=11)
749
- ax.set_ylabel(r"Helium ash fraction $f_\alpha$ [%]", fontsize=11)
750
- ax.set_title("He ash fraction academic vs refined H-mode pedestal", fontsize=10)
751
- ax.legend(fontsize=8)
844
+ fig, ax = plt.subplots(figsize=(8.0, 5.2))
845
+ ax.plot(C_arr, fa_ITER, "b-", lw=2.0, label="ITER — academic (no pedestal)")
846
+ ax.plot(C_arr, fa_ITER_ped, "b--", lw=1.6, label="ITER — Refined H-mode pedestal")
847
+ ax.plot(C_arr, fa_DEMO, "r-", lw=2.0, label="EU-DEMO — academic")
848
+ # Two D0FUS default operating points: ITER and EU-DEMO 2017
849
+ ax.axvline(C_Alpha_ITER, color="tab:blue", lw=1.6, ls=":",
850
+ label=f"ITER default $C_\\alpha$ = {C_Alpha_ITER:.0f}")
851
+ ax.axvline(C_Alpha_DEMO, color="tab:red", lw=1.6, ls=":",
852
+ label=f"EU-DEMO 2017 default $C_\\alpha$ = {C_Alpha_DEMO:.0f}")
853
+ ax.axhspan(4, 6, color="grey", alpha=0.12, label="ITER target 4–6 %")
854
+ ax.set_xlabel(r"Removal efficiency $C_\alpha = \tau_\alpha / \tau_E$", fontsize=14)
855
+ ax.set_ylabel(r"Helium ash fraction $f_\alpha$ [%]", fontsize=14)
856
+ ax.set_title("He ash fraction — academic vs refined H-mode pedestal",
857
+ fontsize=13)
858
+ ax.legend(fontsize=11, loc="upper left")
859
+ ax.tick_params(axis="both", labelsize=12)
752
860
  ax.set_xlim(2, 15)
753
861
  ax.set_ylim(0, 25)
754
862
  ax.grid(True, alpha=0.3)
@@ -761,12 +869,84 @@ def plot_He_fraction(
761
869
  # 4. Superconductor / cable engineering
762
870
  # =============================================================================
763
871
 
872
+ # -----------------------------------------------------------------------------
873
+ # NHMFL engineering current density reference data (T = 4.2 K)
874
+ # -----------------------------------------------------------------------------
875
+ # Source: National High Magnetic Field Laboratory (NHMFL / MagLab) "Engineering
876
+ # Current Density Plot", file Je_vs_B-041118a (updated 2021-01-04). Values are
877
+ # whole-strand / whole-tape engineering current density Je [A/mm²] vs applied
878
+ # field B [T]. Only the three series directly comparable to the D0FUS strand
879
+ # scalings (NbTi, Nb3Sn bronze, REBCO worst-orientation) are kept here.
880
+ #
881
+ # Per-series provenance:
882
+ # * REBCO B perp tape plane : SuperPower SP26, 50 µm substrate, 7.5%Zr,
883
+ # measured at NHMFL (Braccini, Jaroszynski, Xu) - DOI 10.1088/0953-2048/24/3/035001
884
+ # * Nb3Sn High Sn Bronze : Miyazaki et al., MT-18 (IEEE TASC 14:2, 2004)
885
+ # DOI 10.1109/TASC.2004.830344
886
+ # * NbTi LHC 4.2 K : Boutboul et al., MT-19 (IEEE TASC 16:2, 2006)
887
+ # DOI 10.1109/TASC.2006.870777
888
+ _NHMFL_JE_REFERENCE = {
889
+ "NbTi": {
890
+ "label": "NbTi LHC strand",
891
+ "B": np.array([0.61, 0.95, 1.34, 1.68, 2.23, 3.18, 4.15, 5.12, 6.10]),
892
+ "Je": np.array([5106.88, 4054.15, 3180.60, 2710.23, 2217.46,
893
+ 1724.69, 1411.11, 1187.12, 918.34]),
894
+ },
895
+ "Nb3Sn": {
896
+ "label": "Nb₃Sn high-Sn bronze",
897
+ "B": np.array([18.00, 19.01, 20.01, 21.01, 22.02, 23.00, 24.00, 25.00]),
898
+ "Je": np.array([166.64, 137.81, 111.10, 84.38, 60.82, 40.08, 19.34, 8.09]),
899
+ },
900
+ "REBCO": {
901
+ "label": "REBCO tape, B⊥ (worst case)",
902
+ "B": np.array([1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0,
903
+ 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 18.0,
904
+ 20.0, 22.0, 24.0, 25.0, 26.0, 28.0, 30.0, 31.0]),
905
+ "Je": np.array([3665.00, 2920.00, 2380.00, 1796.15, 1447.69, 1188.46,
906
+ 1034.37, 956.92, 851.83, 780.84, 720.00, 679.44,
907
+ 626.09, 593.24, 563.48, 535.38, 516.52, 469.56,
908
+ 433.08, 406.96, 391.30, 367.69, 360.00, 344.35,
909
+ 328.70, 322.31]),
910
+ },
911
+ }
912
+
913
+
914
+ def _overlay_nhmfl_reference(ax, color_map: dict, alpha: float = 0.30) -> None:
915
+ """
916
+ Overlay the NHMFL engineering current density reference (4.2 K) on ``ax``
917
+ as transparent markers connected by a thin line, color-matched per material.
918
+
919
+ Parameters
920
+ ----------
921
+ ax : matplotlib Axes Target axes (assumed log-y in A/mm²).
922
+ color_map : dict Mapping {material_key: hex_color} for NbTi,
923
+ Nb3Sn, REBCO; the same colours used for the
924
+ D0FUS curves so the visual pairing is direct.
925
+ alpha : float Transparency for both markers and connecting
926
+ line (default 0.30, "background" appearance).
927
+ """
928
+ for key, ref in _NHMFL_JE_REFERENCE.items():
929
+ col = color_map.get(key, "#808080")
930
+ ax.plot(
931
+ ref["B"], ref["Je"],
932
+ linestyle="-", linewidth=1.0,
933
+ marker="o", markersize=4.5,
934
+ color=col, alpha=alpha,
935
+ zorder=1,
936
+ )
937
+ # Single neutral legend handle for the whole NHMFL reference set.
938
+ ax.plot([], [], linestyle="-", linewidth=1.0, marker="o", markersize=4.5,
939
+ color="#555555", alpha=alpha,
940
+ label="NHMFL strand/tape data @ 4.2 K (2011 vintage, ref.)")
941
+
942
+
764
943
  def plot_Jc_scaling(
765
944
  B_min: float = 0.5,
766
945
  B_max: float = 45.0,
767
946
  T_op: float = 4.2,
768
947
  f_non_Cu_LTS: float = 0.50,
769
948
  f_non_Cu_HTS: float = 0.60,
949
+ show_nhmfl_ref: bool = True,
770
950
  save_dir: str | None = None,
771
951
  ) -> None:
772
952
  """
@@ -776,29 +956,47 @@ def plot_Jc_scaling(
776
956
  * Nb₃Sn (LTS, ITER/EU-DEMO TF/CS)
777
957
  * REBCO (HTS, ARC / SPARC)
778
958
 
959
+ When ``show_nhmfl_ref`` is True, the NHMFL/MagLab experimental engineering
960
+ current density data at 4.2 K is overlaid as a transparent background
961
+ reference (color-matched markers + thin line per material).
962
+
779
963
  Parameters
780
964
  ----------
781
- B_min, B_max : float Field scan range [T].
782
- T_op : float Operating temperature [K].
783
- f_non_Cu_LTS : float Non-copper fraction for LTS strands [-].
784
- f_non_Cu_HTS : float Non-copper fraction for HTS tapes [-].
785
- save_dir : str or None
965
+ B_min, B_max : float Field scan range [T].
966
+ T_op : float Operating temperature [K].
967
+ f_non_Cu_LTS : float Non-copper fraction for LTS strands [-].
968
+ f_non_Cu_HTS : float Non-copper fraction for HTS tapes [-].
969
+ show_nhmfl_ref : bool Overlay NHMFL 4.2 K reference data (default True).
970
+ save_dir : str or None
786
971
 
787
972
  References
788
973
  ----------
789
974
  ITER TF strand specifications; Nijhuis (2008); Fleiter & Ballarino (2014).
975
+ NHMFL/MagLab Engineering Current Density Plot, updated 2021-01-04.
790
976
  """
791
977
  B_vals = np.linspace(B_min, B_max, 300)
792
978
  J_NbTi = J_non_Cu_NbTi(B_vals, T_op) * f_non_Cu_LTS / 1e6 # [A/mm²]
793
979
  J_Nb3Sn = J_non_Cu_Nb3Sn(B_vals, T_op, Eps=-0.003) * f_non_Cu_LTS / 1e6
794
980
  J_REBCO = J_non_Cu_REBCO(B_vals, T_op, Tet=0) * f_non_Cu_HTS / 1e6
795
981
 
982
+ # Colour palette shared between D0FUS curves and NHMFL reference overlay.
983
+ col_NbTi, col_Nb3Sn, col_REBCO = "#A06AB4", "#E06C75", "#D4B000"
984
+
796
985
  fig, ax = plt.subplots(figsize=(7, 5))
797
- ax.plot(B_vals, J_NbTi, lw=2, color="#A06AB4", label="NbTi strand")
798
- ax.plot(B_vals, J_Nb3Sn, lw=2, color="#E06C75", label="Nb₃Sn strand")
799
- ax.plot(B_vals, J_REBCO, lw=2, color="#D4B000", label="REBCO tape")
986
+
987
+ # NHMFL background reference (drawn first lower z-order).
988
+ if show_nhmfl_ref:
989
+ _overlay_nhmfl_reference(
990
+ ax,
991
+ color_map={"NbTi": col_NbTi, "Nb3Sn": col_Nb3Sn, "REBCO": col_REBCO},
992
+ alpha=0.30,
993
+ )
994
+
995
+ ax.plot(B_vals, J_NbTi, lw=2, color=col_NbTi, label="NbTi strand", zorder=3)
996
+ ax.plot(B_vals, J_Nb3Sn, lw=2, color=col_Nb3Sn, label="Nb₃Sn strand", zorder=3)
997
+ ax.plot(B_vals, J_REBCO, lw=2, color=col_REBCO, label="REBCO tape", zorder=3)
800
998
  ax.set_xlabel("Magnetic field B [T]", fontsize=12)
801
- ax.set_ylabel("Engineering current density J [A/mm²]", fontsize=12)
999
+ ax.set_ylabel("Strand/Tape current density [A/mm²]", fontsize=12)
802
1000
  ax.set_title(f"Superconductor Jc scalings @ {T_op} K", fontsize=12)
803
1001
  ax.legend(loc="upper right", fontsize=10)
804
1002
  ax.grid(True, alpha=0.3)
@@ -2663,10 +2861,10 @@ def plot_all(
2663
2861
 
2664
2862
  ── Plasma shaping [ 1– 7]
2665
2863
  ── Kinetic profiles [ 8–11]
2666
- ── Transport & current [12–14]
2667
- ── Radiation & impurities [1517]
2668
- ── Superconductor eng. [1820]
2669
- ── Coil sizing & mechanics [21–29]
2864
+ ── Transport & current [12–13]
2865
+ ── Radiation & impurities [1416]
2866
+ ── Superconductor eng. [1719]
2867
+ ── Coil sizing & mechanics [20–29]
2670
2868
  · TF grading [21–23]
2671
2869
  · CS / CIRCE / geometry [24–27]
2672
2870
  · Benchmarks [28–29]
@@ -2732,6 +2930,9 @@ def plot_all(
2732
2930
  _p(14, "Coronal cooling coefficient L_z(T)")
2733
2931
  plot_Lz_cooling(save_dir=save_dir)
2734
2932
 
2933
+ _p(15, "D-T reactivity ⟨σv⟩(T)")
2934
+ plot_DT_reactivity(save_dir=save_dir)
2935
+
2735
2936
  _p(16, "Helium ash fraction")
2736
2937
  plot_He_fraction(save_dir=save_dir)
2737
2938
 
@@ -3063,7 +3063,7 @@ if __name__ == "__main__":
3063
3063
  #%% Academic model
3064
3064
 
3065
3065
  def f_TF_academic(a, b, R0, σ_TF, J_max_TF, B_max_TF, Choice_Buck_Wedg,
3066
- coef_inboard_tension, F_CClamp):
3066
+ coef_inboard_tension, F_CClamp, delta_port=0.0):
3067
3067
  """
3068
3068
  Calculate the thickness of the TF coil using a 2-layer thin cylinder model.
3069
3069
 
@@ -3094,6 +3094,12 @@ def f_TF_academic(a, b, R0, σ_TF, J_max_TF, B_max_TF, Choice_Buck_Wedg,
3094
3094
  Fraction of total tension carried by inboard leg [-].
3095
3095
  F_CClamp : float
3096
3096
  Clamping force subtracted from tension [N].
3097
+ delta_port : float, optional
3098
+ Additional radial margin between the blanket outer edge and the
3099
+ TF outer-leg conductor [m], driven by toroidal-ripple and minimum
3100
+ port-access constraints. Pushes R2 outward so that the F_z log
3101
+ term ln(R2 / R1) reflects the actual outer-leg position. Default:
3102
+ 0.0 (matches the original analytic formula).
3097
3103
 
3098
3104
  Returns
3099
3105
  -------
@@ -3149,9 +3155,11 @@ def f_TF_academic(a, b, R0, σ_TF, J_max_TF, B_max_TF, Choice_Buck_Wedg,
3149
3155
  # 5. Inner layer thickness c1 derived from the circular cross-section
3150
3156
  c_WP = R1_0 - np.sqrt(R1_0**2 - S_cond / np.pi)
3151
3157
 
3152
- # 6. Calculate new radii after adding c1
3158
+ # 6. Calculate new radii after adding c1.
3159
+ # delta_port shifts R2 outward to capture the ripple and port-access
3160
+ # margin between the blanket outer edge and the TF outer-leg conductor.
3153
3161
  R1 = R1_0 - c_WP # Effective inner radius
3154
- R2 = R2_0 + c_WP # Effective outer radius
3162
+ R2 = R2_0 + c_WP + delta_port # Effective outer radius
3155
3163
 
3156
3164
  # 7. Vertical separating force from Maxwell stress integral through midplane:
3157
3165
  # T_sep = π B0² R0² / μ0 × ln(R2/R1), corrected by clamping and inboard fraction.
@@ -3197,20 +3205,28 @@ def f_TF_academic(a, b, R0, σ_TF, J_max_TF, B_max_TF, Choice_Buck_Wedg,
3197
3205
  #%% Refined model
3198
3206
 
3199
3207
  def Winding_Pack_refined(R_0, a, b, sigma_max, J_max, B_max, omega, n,
3200
- grading=False):
3201
-
3208
+ grading=False, delta_port=0.0):
3209
+
3202
3210
  """
3203
3211
  Computes the winding pack thickness and stress ratio under Tresca criterion.
3204
-
3212
+
3205
3213
  When grading=False (default):
3206
3214
  Uniform α. Log-spaced adaptive bracket search + brentq root-finding.
3207
3215
  Tresca is satisfied at the most loaded point only.
3208
-
3216
+
3209
3217
  When grading=True:
3210
3218
  Radially varying α(R). Tresca is saturated at every radius.
3211
3219
  Uses inward radial integration with self-consistent σ_z (Picard).
3212
3220
  Typically reduces c_WP by 8 to 25% depending on B_max.
3213
-
3221
+ A strict-refinement guarantee is enforced: the graded result is
3222
+ compared against the ungraded analytical baseline and the smaller
3223
+ c_WP is returned. The ungraded solver acts as a safety net when
3224
+ the graded radial integration converges on an unphysical state
3225
+ (e.g. NI(R) reaching R = 0 before being consumed, which would
3226
+ otherwise propagate as c_WP ≈ R_ext, TF occupying all the
3227
+ inboard radial budget). Grading can therefore never degrade the
3228
+ ungraded baseline.
3229
+
3214
3230
  Args:
3215
3231
  R_0: Major radius [m]
3216
3232
  a: Plasma minor radius [m]
@@ -3224,7 +3240,15 @@ def Winding_Pack_refined(R_0, a, b, sigma_max, J_max, B_max, omega, n,
3224
3240
  n: Geometric factor for gamma (steel area fraction) [dimensionless]
3225
3241
  grading: bool, optional
3226
3242
  If True, use radially graded α(R) model. Default: False.
3227
-
3243
+ delta_port: float, optional
3244
+ Additional radial margin between the blanket outer edge and the
3245
+ TF outer-leg conductor [m]. Sized to satisfy a maximum toroidal
3246
+ field ripple at the plasma edge and a minimum port-access width,
3247
+ typically computed by Number_TF_coils(). Enters the F_z log
3248
+ argument as ln((R_0 + a + b + delta_port) / (R_0 - a - b)),
3249
+ increasing the predicted vertical tension. Default: 0.0
3250
+ (matches Eq. 5 of the reference paper, slightly non-conservative).
3251
+
3228
3252
  Returns:
3229
3253
  winding_pack_thickness: R_ext - R_sep [m]
3230
3254
  sigma_r: Radial stress at solution [Pa]
@@ -3232,7 +3256,7 @@ def Winding_Pack_refined(R_0, a, b, sigma_max, J_max, B_max, omega, n,
3232
3256
  sigma_theta: Hoop stress at solution [Pa]
3233
3257
  Steel_fraction: 1 - alpha (structural fraction) [-]
3234
3258
  For graded: area-weighted average ⟨1 - α⟩.
3235
-
3259
+
3236
3260
  Limitations:
3237
3261
  The axial stress σ_z accounts for the vertical component of the
3238
3262
  in-plane Lorentz force (centering force), but does NOT include:
@@ -3246,7 +3270,7 @@ def Winding_Pack_refined(R_0, a, b, sigma_max, J_max, B_max, omega, n,
3246
3270
  These omissions make the present model slightly non-conservative
3247
3271
  for compact / low-A designs.
3248
3272
  """
3249
-
3273
+
3250
3274
  R_ext = R_0 - a - b
3251
3275
 
3252
3276
  # Validate J_max before proceeding
@@ -3257,17 +3281,15 @@ def Winding_Pack_refined(R_0, a, b, sigma_max, J_max, B_max, omega, n,
3257
3281
  return np.nan, np.nan, np.nan, np.nan, np.nan
3258
3282
  # raise ValueError("R_ext must be positive. Check R_0, a, and b.")
3259
3283
 
3260
- ln_term = np.log((R_0 + a + b) / (R_ext))
3284
+ ln_term = np.log((R_0 + a + b + delta_port) / (R_ext))
3261
3285
  if ln_term <= 0:
3262
3286
  return np.nan, np.nan, np.nan, np.nan, np.nan
3263
3287
  # raise ValueError("Invalid logarithmic term: ensure R_0 + a + b > R_0 - a - b")
3264
3288
 
3265
- # Graded branch: delegate to radial integration solver
3266
- if grading:
3267
- return _solve_graded_wp(R_ext, B_max, J_max, sigma_max,
3268
- omega, n, ln_term)
3269
-
3270
- # Ungraded branch: analytical α, brentq root-finding (original model)
3289
+ # === Ungraded analytical solution ===
3290
+ # Always computed: used directly when grading=False, and as the
3291
+ # mandatory fallback baseline when grading=True to enforce the
3292
+ # strict-refinement principle (see docstring).
3271
3293
  def alpha(R_sep):
3272
3294
  denom = R_ext**2 - R_sep**2
3273
3295
  if denom <= 0:
@@ -3302,8 +3324,6 @@ def Winding_Pack_refined(R_0, a, b, sigma_max, J_max, B_max, omega, n,
3302
3324
  # narrow valid islands at high field that pure log spacing can miss.
3303
3325
  # Typical call budget: ~35 evaluations (Pass 1 success) to ~85 (worst case).
3304
3326
 
3305
- R_sep_solution = None
3306
-
3307
3327
  d_lo = 1e-3 # Minimum WP thickness [m]
3308
3328
  d_hi = R_ext - 1e-3 # Maximum WP thickness (nearly solid cylinder)
3309
3329
 
@@ -3320,32 +3340,60 @@ def Winding_Pack_refined(R_0, a, b, sigma_max, J_max, B_max, omega, n,
3320
3340
  select='smallest')
3321
3341
 
3322
3342
  if np.isnan(d_solution):
3323
- return np.nan, np.nan, np.nan, np.nan, np.nan
3324
-
3325
- R_sep_solution = R_ext - d_solution
3326
-
3327
- # === Final stress calculation ===
3328
- a_val = alpha(R_sep_solution)
3329
- g_val = gamma_func(a_val, n)
3330
-
3331
- if np.isnan(a_val) or np.isnan(g_val):
3332
- return np.nan, np.nan, np.nan, np.nan, np.nan
3333
-
3334
- try:
3335
- sigma_r = B_max**2 / (2 * μ0 * g_val)
3336
- sigma_z = (omega / (1 - a_val)) * B_max**2 * R_ext**2 / (2 * μ0 * (R_ext**2 - R_sep_solution**2)) * ln_term
3337
- sigma_theta = 0
3338
- except Exception:
3339
- return np.nan, np.nan, np.nan, np.nan, np.nan
3340
-
3341
- winding_pack_thickness = R_ext - R_sep_solution
3342
- Steel_fraction = (1-a_val)
3343
+ result_ungraded = (np.nan, np.nan, np.nan, np.nan, np.nan)
3344
+ else:
3345
+ R_sep_solution = R_ext - d_solution
3343
3346
 
3344
- return winding_pack_thickness, sigma_r, sigma_z, sigma_theta, Steel_fraction
3347
+ # Final stress calculation for the ungraded solution
3348
+ a_val = alpha(R_sep_solution)
3349
+ g_val = gamma_func(a_val, n)
3345
3350
 
3351
+ if np.isnan(a_val) or np.isnan(g_val):
3352
+ result_ungraded = (np.nan, np.nan, np.nan, np.nan, np.nan)
3353
+ else:
3354
+ try:
3355
+ sigma_r = B_max**2 / (2 * μ0 * g_val)
3356
+ sigma_z = (omega / (1 - a_val)) * B_max**2 * R_ext**2 / (2 * μ0 * (R_ext**2 - R_sep_solution**2)) * ln_term
3357
+ sigma_theta = 0
3358
+ winding_pack_thickness = R_ext - R_sep_solution
3359
+ Steel_fraction = (1 - a_val)
3360
+ result_ungraded = (winding_pack_thickness, sigma_r, sigma_z,
3361
+ sigma_theta, Steel_fraction)
3362
+ except Exception:
3363
+ result_ungraded = (np.nan, np.nan, np.nan, np.nan, np.nan)
3364
+
3365
+ # Ungraded path: return immediately.
3366
+ if not grading:
3367
+ return result_ungraded
3368
+
3369
+ # === Graded branch with strict-refinement guarantee ===
3370
+ # The graded radial-integration solver _solve_graded_wp can in some
3371
+ # regimes return a converged-but-unphysical result, where the inward
3372
+ # integration of NI(R) consumes the available radial space before
3373
+ # depleting the ampere-turns (R reaches the cylinder centre with
3374
+ # NI > 0). This propagates as c_WP ≈ R_ext, effectively reporting a
3375
+ # TF that occupies all the inboard space.
3376
+ #
3377
+ # To prevent the graded model from ever degrading the ungraded
3378
+ # baseline, we compute both and return the smaller c_WP. The
3379
+ # ungraded analytical solver is robust on the same domain and acts
3380
+ # as a safety net. Where the graded model legitimately improves the
3381
+ # result, the gain is preserved; otherwise, the ungraded result is
3382
+ # returned unchanged.
3383
+ result_graded = _solve_graded_wp(R_ext, B_max, J_max, sigma_max,
3384
+ omega, n, ln_term)
3385
+
3386
+ c_ungraded = result_ungraded[0]
3387
+ c_graded = result_graded[0]
3388
+
3389
+ if not np.isfinite(c_graded):
3390
+ return result_ungraded
3391
+ if not np.isfinite(c_ungraded):
3392
+ return result_graded
3393
+ return result_graded if c_graded <= c_ungraded else result_ungraded
3346
3394
 
3347
3395
  def Nose_refined(R_ext_Nose, sigma_max, omega, B_max, R_0, a, b,
3348
- coef_inboard_tension):
3396
+ coef_inboard_tension, delta_port=0.0):
3349
3397
  """
3350
3398
  Compute the inner radius of the TF nose (inner structural casing).
3351
3399
 
@@ -3376,6 +3424,11 @@ def Nose_refined(R_ext_Nose, sigma_max, omega, B_max, R_0, a, b,
3376
3424
  coef_inboard_tension : float
3377
3425
  Correction factor for inboard tension distribution [-].
3378
3426
  Accounts for non-uniform current distribution across the WP.
3427
+ delta_port : float, optional
3428
+ Additional radial margin between the blanket outer edge and the
3429
+ TF outer-leg conductor [m], driven by toroidal-ripple and minimum
3430
+ port-access constraints. Enters the F_z log argument as
3431
+ ln((R_0 + a + b + delta_port) / (R_0 - a - b)). Default: 0.0.
3379
3432
 
3380
3433
  Returns
3381
3434
  -------
@@ -3390,8 +3443,10 @@ def Nose_refined(R_ext_Nose, sigma_max, omega, B_max, R_0, a, b,
3390
3443
  # at R_nose is amplified by the circumference ratio R_TF / R_nose.
3391
3444
  P = (B_max**2) / (2 * μ0) * (R_0 - a - b) / R_ext_Nose
3392
3445
 
3393
- # Compute the logarithmic term
3394
- log_term = np.log((R_0 + a + b) / (R_0 - a - b))
3446
+ # Logarithmic term entering F_z. The numerator uses the actual TF outer-leg
3447
+ # location (R_0 + a + b + delta_port), not just the blanket outer edge,
3448
+ # to capture the ripple and port-access margin (see Number_TF_coils()).
3449
+ log_term = np.log((R_0 + a + b + delta_port) / (R_0 - a - b))
3395
3450
 
3396
3451
  # Compute the full expression under the square root
3397
3452
  term_intermediate = (R_ext_Nose**2 / sigma_max) * (2 * P + (1 - omega) * (B_max**2 * coef_inboard_tension / μ0) * log_term)
@@ -3405,7 +3460,8 @@ def Nose_refined(R_ext_Nose, sigma_max, omega, B_max, R_0, a, b,
3405
3460
  return Ri
3406
3461
 
3407
3462
  def f_TF_refined(a, b, R0, σ_TF, J_max_TF, B_max_TF, Choice_Buck_Wedg, omega, n,
3408
- c_BP, coef_inboard_tension, F_CClamp, TF_grading=False):
3463
+ c_BP, coef_inboard_tension, F_CClamp, TF_grading=False,
3464
+ delta_port=0.0):
3409
3465
 
3410
3466
  """
3411
3467
  Calculate the thickness of the TF coil using a 2 layer thick cylinder model
@@ -3425,6 +3481,14 @@ def f_TF_refined(a, b, R0, σ_TF, J_max_TF, B_max_TF, Choice_Buck_Wedg, omega, n
3425
3481
  F_CClamp : Clamping force [N]
3426
3482
  TF_grading : bool, optional
3427
3483
  If True, use radially graded α(R) in the WP. Default: False.
3484
+ delta_port : float, optional
3485
+ Additional radial margin between the blanket outer edge and the
3486
+ TF outer-leg conductor [m], required to satisfy a target toroidal
3487
+ field ripple and a minimum port-access width. Typically obtained
3488
+ from Number_TF_coils(R0, a, b, ripple_adm, L_min). Propagated to
3489
+ Winding_Pack_refined and Nose_refined so that F_z and σ_z are
3490
+ evaluated with the actual outer-leg position rather than the
3491
+ blanket outer edge. Default: 0.0 (paper convention).
3428
3492
 
3429
3493
  Returns:
3430
3494
  c : TF total inboard radial thickness [m]
@@ -3439,14 +3503,14 @@ def f_TF_refined(a, b, R0, σ_TF, J_max_TF, B_max_TF, Choice_Buck_Wedg, omega, n
3439
3503
 
3440
3504
  if Choice_Buck_Wedg == "Wedging":
3441
3505
 
3442
- c_WP, σ_r, σ_z, σ_theta, Steel_fraction = Winding_Pack_refined( R0, a, b, σ_TF, J_max_TF, B_max_TF, omega, n, grading=TF_grading)
3506
+ c_WP, σ_r, σ_z, σ_theta, Steel_fraction = Winding_Pack_refined( R0, a, b, σ_TF, J_max_TF, B_max_TF, omega, n, grading=TF_grading, delta_port=delta_port)
3443
3507
 
3444
3508
  # Vérification que c_WP est valide
3445
3509
  if c_WP is None or np.isnan(c_WP) or c_WP < 0:
3446
3510
  return np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan
3447
3511
 
3448
3512
  c_Nose = R0 - a - b - c_WP - Nose_refined(R0 - a - b - c_WP, σ_TF, omega, B_max_TF, R0, a, b,
3449
- coef_inboard_tension)
3513
+ coef_inboard_tension, delta_port=delta_port)
3450
3514
 
3451
3515
  # Vérification que c_Nose est valide
3452
3516
  if c_Nose is None or np.isnan(c_Nose) or c_Nose < 0:
@@ -3467,7 +3531,7 @@ def f_TF_refined(a, b, R0, σ_TF, J_max_TF, B_max_TF, Choice_Buck_Wedg, omega, n
3467
3531
 
3468
3532
  elif Choice_Buck_Wedg == "Bucking" or Choice_Buck_Wedg == "Plug":
3469
3533
 
3470
- c_WP, σ_r, σ_z, σ_theta, Steel_fraction = Winding_Pack_refined(R0, a, b, σ_TF, J_max_TF, B_max_TF, omega, n, grading=TF_grading)
3534
+ c_WP, σ_r, σ_z, σ_theta, Steel_fraction = Winding_Pack_refined(R0, a, b, σ_TF, J_max_TF, B_max_TF, omega, n, grading=TF_grading, delta_port=delta_port)
3471
3535
 
3472
3536
  c = c_WP
3473
3537
  c_Nose = 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d0fus
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: Design 0-dimensional for Fusion Systems - Tokamak power plant design and optimization tool
5
5
  Author-email: Timothé Auclair <timothe.auclair@gmail.com>
6
6
  Maintainer-email: Timothé Auclair <timothe.auclair@gmail.com>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: d0fus
3
- Version: 2.2.1
3
+ Version: 2.2.2
4
4
  Summary: Design 0-dimensional for Fusion Systems - Tokamak power plant design and optimization tool
5
5
  Author-email: Timothé Auclair <timothe.auclair@gmail.com>
6
6
  Maintainer-email: Timothé Auclair <timothe.auclair@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "d0fus"
7
- version = "2.2.1"
7
+ version = "2.2.2"
8
8
  description = "Design 0-dimensional for Fusion Systems - Tokamak power plant design and optimization tool"
9
9
  readme = "README.md"
10
10
  license = {text = "CeCILL-C"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes