d0fus 2.2.1__tar.gz → 2.3.0__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.
@@ -170,12 +170,39 @@ def detect_mode_from_input(input_file):
170
170
  #%% Main functions
171
171
 
172
172
  def print_banner():
173
- """Display D0FUS banner"""
174
- banner = """
173
+ """Display the D0FUS startup banner.
174
+
175
+ Simple double-line framed banner with title, tagline, version, author,
176
+ license and repository URL. Can be silenced with the environment variable
177
+ ``D0FUS_NO_BANNER`` (useful for batch scans, HPC jobs, CI).
178
+ """
179
+ if os.environ.get("D0FUS_NO_BANNER", "").strip() not in ("", "0", "false", "False"):
180
+ return
181
+
182
+ # Resolve installed package version, fall back to a static string
183
+ try:
184
+ from importlib.metadata import version, PackageNotFoundError
185
+ try:
186
+ v = version("d0fus")
187
+ except PackageNotFoundError:
188
+ v = "dev"
189
+ except ImportError:
190
+ v = "dev"
191
+
192
+ # The version is the only dynamic line; the others are hardcoded for clarity.
193
+ # Inside width is 51 characters; the version is centered programmatically.
194
+ version_line = f"Version {v}".center(51)
195
+
196
+ banner = f"""
175
197
  ╔═══════════════════════════════════════════════════╗
176
198
  ║ ║
177
199
  ║ D0FUS ║
178
- Design 0-dimensional for Fusion Systems
200
+ Design 0-dimensional for Fusion Systems
201
+ ║ ║
202
+ ╠═══════════════════════════════════════════════════╣
203
+ ║{version_line}║
204
+ ║ T. Auclair, CEA-IRFM | CeCILL-C ║
205
+ ║ https://github.com/IRFM/D0FUS ║
179
206
  ║ ║
180
207
  ╚═══════════════════════════════════════════════════╝
181
208
  """
@@ -48,6 +48,8 @@ if __name__ != "__main__":
48
48
  get_Lz,
49
49
  f_He_fraction,
50
50
  f_q_profile,
51
+ f_sigmav,
52
+ _two_point_core, M_F_DT,
51
53
  )
52
54
  from .D0FUS_radial_build_functions import (
53
55
  J_non_Cu_NbTi, J_non_Cu_Nb3Sn, J_non_Cu_REBCO,
@@ -76,6 +78,8 @@ else:
76
78
  get_Lz,
77
79
  f_He_fraction,
78
80
  f_q_profile,
81
+ f_sigmav,
82
+ _two_point_core, M_F_DT,
79
83
  )
80
84
  from D0FUS_BIB.D0FUS_radial_build_functions import (
81
85
  J_non_Cu_NbTi, J_non_Cu_Nb3Sn, J_non_Cu_REBCO,
@@ -639,6 +643,96 @@ def plot_density_line_vol(
639
643
  # 3. Nuclear / radiation physics
640
644
  # =============================================================================
641
645
 
646
+ def plot_DT_reactivity(
647
+ T_min_keV: float = 1.0,
648
+ T_max_keV: float = 100.0,
649
+ n_points: int = 600,
650
+ T_op_min: float = 10.0,
651
+ T_op_max: float = 25.0,
652
+ save_dir: str | None = None,
653
+ ) -> None:
654
+ """
655
+ Plot the D-T Maxwellian reactivity ⟨σv⟩(T) used by D0FUS, together with
656
+ the pressure-limited fusion power density metric ⟨σv⟩/T².
657
+
658
+ Left panel:
659
+ ⟨σv⟩(T) computed from the Bosch & Hale (1992) parameterisation,
660
+ with the power plant operating window [T_op_min, T_op_max] shaded
661
+ and the reactivity maximum marked.
662
+
663
+ Right panel:
664
+ Pressure-limited figure of merit ⟨σv⟩/T² (arbitrary units).
665
+ At fixed plasma pressure p = nT, the fuel ion density scales as
666
+ n ∝ 1/T, so the volumetric fusion power density p_fus ∝ n² ⟨σv⟩
667
+ is proportional to ⟨σv⟩/T². This identifies the optimal operating
668
+ temperature for a β-limited tokamak near 14 keV.
669
+
670
+ Parameters
671
+ ----------
672
+ T_min_keV, T_max_keV : float Ion temperature scan range [keV].
673
+ n_points : int Number of temperature grid points.
674
+ T_op_min, T_op_max : float Power plant operating window bounds [keV].
675
+ save_dir : str or None
676
+
677
+ References
678
+ ----------
679
+ Bosch & Hale, Nucl. Fusion 32, 611 (1992) — DT reactivity fit (Table IV).
680
+ Freidberg, Plasma Physics and Fusion Energy (2007) — pressure-limited optimum.
681
+ """
682
+ # Temperature grid (linear, since the operating window lies in the rapid-rise zone)
683
+ T_arr = np.linspace(T_min_keV, T_max_keV, n_points)
684
+ sv_arr = f_sigmav(T_arr)
685
+
686
+ # Reactivity maximum
687
+ i_peak = int(np.argmax(sv_arr))
688
+ T_peak, sv_peak = T_arr[i_peak], sv_arr[i_peak]
689
+
690
+ # Pressure-limited metric ⟨σv⟩/T² (units arbitrary, normalised below)
691
+ metric = sv_arr / T_arr**2
692
+ i_opt = int(np.argmax(metric))
693
+ T_opt, m_opt = T_arr[i_opt], metric[i_opt]
694
+
695
+ # --- Figure ------------------------------------------------------------
696
+ fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))
697
+
698
+ # Left panel: reactivity curve on log scale
699
+ ax = axes[0]
700
+ ax.semilogy(T_arr, sv_arr, color="tab:red", lw=2.0,
701
+ label="Bosch & Hale (1992)")
702
+ ax.axvspan(T_op_min, T_op_max, color="goldenrod", alpha=0.20,
703
+ label=f"Operating window\n{T_op_min:.0f}–{T_op_max:.0f} keV")
704
+ ax.axvline(T_peak, color="k", lw=1.0, ls="--",
705
+ label=f"peak: T = {T_peak:.0f} keV")
706
+ ax.set_xlabel(r"Ion temperature $T$ [keV]", fontsize=11)
707
+ ax.set_ylabel(r"$\langle\sigma v\rangle_{DT}$ [m$^3$ s$^{-1}$]", fontsize=11)
708
+ ax.set_title("D-T Maxwellian reactivity", fontsize=11)
709
+ ax.set_xlim(T_min_keV, T_max_keV)
710
+ ax.set_ylim(1e-25, 3e-21)
711
+ ax.grid(True, which="both", alpha=0.25)
712
+ ax.legend(fontsize=9, loc="lower right")
713
+
714
+ # Right panel: pressure-limited figure of merit
715
+ ax = axes[1]
716
+ ax.plot(T_arr, metric / m_opt, color="tab:blue", lw=2.0,
717
+ label=r"$\langle\sigma v\rangle / T^2$ (normalised)")
718
+ ax.axvspan(T_op_min, T_op_max, color="goldenrod", alpha=0.20,
719
+ label=f"Operating window\n{T_op_min:.0f}–{T_op_max:.0f} keV")
720
+ ax.axvline(T_opt, color="k", lw=1.0, ls="--",
721
+ label=f"optimum: T = {T_opt:.1f} keV")
722
+ ax.set_xlabel(r"Ion temperature $T$ [keV]", fontsize=11)
723
+ ax.set_ylabel(r"$\langle\sigma v\rangle / T^2$ [normalised]", fontsize=11)
724
+ ax.set_title(r"Pressure-limited fusion power figure of merit", fontsize=11)
725
+ ax.set_xlim(0, 50)
726
+ ax.set_ylim(0, 1.08)
727
+ ax.grid(True, alpha=0.25)
728
+ ax.legend(fontsize=9, loc="lower right")
729
+
730
+ plt.suptitle("Fusion reactivity and operating temperature window",
731
+ fontsize=12, fontweight="bold")
732
+ plt.tight_layout()
733
+ _save_or_show(fig, save_dir, "DT_reactivity")
734
+
735
+
642
736
  def plot_Lz_cooling(
643
737
  Te_min_keV: float = 0.05,
644
738
  Te_max_keV: float = 100.0,
@@ -711,7 +805,8 @@ def plot_He_fraction(
711
805
  nbar: float = 1.0,
712
806
  Tbar: float = 8.9,
713
807
  tauE: float = 3.7,
714
- C_Alpha: float = 5.0,
808
+ C_Alpha_ITER: float = 5.0,
809
+ C_Alpha_DEMO: float = 7.0,
715
810
  nu_T: float = 1.0,
716
811
  save_dir: str | None = None,
717
812
  ) -> None:
@@ -720,16 +815,25 @@ def plot_He_fraction(
720
815
  C_α = τ_α / τ_E for ITER and EU-DEMO reference parameters, comparing
721
816
  academic (no pedestal) and H-mode pedestal profile assumptions.
722
817
 
818
+ Two D0FUS default values for C_α are highlighted with vertical dotted
819
+ lines: C_α = 5 for ITER (consistent with Progress in the ITER Physics
820
+ Basis projections) and C_α = 7 for EU-DEMO 2017 (PROCESS reference run).
821
+
723
822
  Parameters
724
823
  ----------
725
824
  nbar, Tbar, tauE : float Reference plasma parameters [10²⁰ m⁻³, keV, s].
726
- C_Alpha : float Reference C_α value (drawn as vertical line) [-].
825
+ C_Alpha_ITER : float D0FUS default C_α for ITER (vertical line) [-].
826
+ C_Alpha_DEMO : float D0FUS default C_α for EU-DEMO 2017 (vertical line) [-].
727
827
  nu_T : float Temperature peaking exponent [-].
728
828
  save_dir : str or None
729
829
 
730
830
  References
731
831
  ----------
732
832
  ITER Physics Basis, Nucl. Fusion 39, §2.4 (1999).
833
+ Shimada et al., Progress in the ITER Physics Basis, Ch. 1,
834
+ Nucl. Fusion 47, S1 (2007).
835
+ Kovari et al., Fus. Eng. Des. 89, 3054 (2014) — PROCESS systems code.
836
+ Reiter, Wolf and Kever, Nucl. Fusion 30, 2141 (1990) — ignition bound on C_α.
733
837
  """
734
838
  C_arr = np.linspace(2, 15, 150)
735
839
 
@@ -739,16 +843,22 @@ def plot_He_fraction(
739
843
  for C in C_arr]
740
844
  fa_DEMO = [f_He_fraction(1.2, 12.5, 4.6, C, nu_T) * 100 for C in C_arr]
741
845
 
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)
846
+ fig, ax = plt.subplots(figsize=(8.0, 5.2))
847
+ ax.plot(C_arr, fa_ITER, "b-", lw=2.0, label="ITER — academic (no pedestal)")
848
+ ax.plot(C_arr, fa_ITER_ped, "b--", lw=1.6, label="ITER — Refined H-mode pedestal")
849
+ ax.plot(C_arr, fa_DEMO, "r-", lw=2.0, label="EU-DEMO — academic")
850
+ # Two D0FUS default operating points: ITER and EU-DEMO 2017
851
+ ax.axvline(C_Alpha_ITER, color="tab:blue", lw=1.6, ls=":",
852
+ label=f"ITER default $C_\\alpha$ = {C_Alpha_ITER:.0f}")
853
+ ax.axvline(C_Alpha_DEMO, color="tab:red", lw=1.6, ls=":",
854
+ label=f"EU-DEMO 2017 default $C_\\alpha$ = {C_Alpha_DEMO:.0f}")
855
+ ax.axhspan(4, 6, color="grey", alpha=0.12, label="ITER target 4–6 %")
856
+ ax.set_xlabel(r"Removal efficiency $C_\alpha = \tau_\alpha / \tau_E$", fontsize=14)
857
+ ax.set_ylabel(r"Helium ash fraction $f_\alpha$ [%]", fontsize=14)
858
+ ax.set_title("He ash fraction — academic vs refined H-mode pedestal",
859
+ fontsize=13)
860
+ ax.legend(fontsize=11, loc="upper left")
861
+ ax.tick_params(axis="both", labelsize=12)
752
862
  ax.set_xlim(2, 15)
753
863
  ax.set_ylim(0, 25)
754
864
  ax.grid(True, alpha=0.3)
@@ -761,12 +871,84 @@ def plot_He_fraction(
761
871
  # 4. Superconductor / cable engineering
762
872
  # =============================================================================
763
873
 
874
+ # -----------------------------------------------------------------------------
875
+ # NHMFL engineering current density reference data (T = 4.2 K)
876
+ # -----------------------------------------------------------------------------
877
+ # Source: National High Magnetic Field Laboratory (NHMFL / MagLab) "Engineering
878
+ # Current Density Plot", file Je_vs_B-041118a (updated 2021-01-04). Values are
879
+ # whole-strand / whole-tape engineering current density Je [A/mm²] vs applied
880
+ # field B [T]. Only the three series directly comparable to the D0FUS strand
881
+ # scalings (NbTi, Nb3Sn bronze, REBCO worst-orientation) are kept here.
882
+ #
883
+ # Per-series provenance:
884
+ # * REBCO B perp tape plane : SuperPower SP26, 50 µm substrate, 7.5%Zr,
885
+ # measured at NHMFL (Braccini, Jaroszynski, Xu) - DOI 10.1088/0953-2048/24/3/035001
886
+ # * Nb3Sn High Sn Bronze : Miyazaki et al., MT-18 (IEEE TASC 14:2, 2004)
887
+ # DOI 10.1109/TASC.2004.830344
888
+ # * NbTi LHC 4.2 K : Boutboul et al., MT-19 (IEEE TASC 16:2, 2006)
889
+ # DOI 10.1109/TASC.2006.870777
890
+ _NHMFL_JE_REFERENCE = {
891
+ "NbTi": {
892
+ "label": "NbTi LHC strand",
893
+ "B": np.array([0.61, 0.95, 1.34, 1.68, 2.23, 3.18, 4.15, 5.12, 6.10]),
894
+ "Je": np.array([5106.88, 4054.15, 3180.60, 2710.23, 2217.46,
895
+ 1724.69, 1411.11, 1187.12, 918.34]),
896
+ },
897
+ "Nb3Sn": {
898
+ "label": "Nb₃Sn high-Sn bronze",
899
+ "B": np.array([18.00, 19.01, 20.01, 21.01, 22.02, 23.00, 24.00, 25.00]),
900
+ "Je": np.array([166.64, 137.81, 111.10, 84.38, 60.82, 40.08, 19.34, 8.09]),
901
+ },
902
+ "REBCO": {
903
+ "label": "REBCO tape, B⊥ (worst case)",
904
+ "B": np.array([1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0,
905
+ 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 18.0,
906
+ 20.0, 22.0, 24.0, 25.0, 26.0, 28.0, 30.0, 31.0]),
907
+ "Je": np.array([3665.00, 2920.00, 2380.00, 1796.15, 1447.69, 1188.46,
908
+ 1034.37, 956.92, 851.83, 780.84, 720.00, 679.44,
909
+ 626.09, 593.24, 563.48, 535.38, 516.52, 469.56,
910
+ 433.08, 406.96, 391.30, 367.69, 360.00, 344.35,
911
+ 328.70, 322.31]),
912
+ },
913
+ }
914
+
915
+
916
+ def _overlay_nhmfl_reference(ax, color_map: dict, alpha: float = 0.30) -> None:
917
+ """
918
+ Overlay the NHMFL engineering current density reference (4.2 K) on ``ax``
919
+ as transparent markers connected by a thin line, color-matched per material.
920
+
921
+ Parameters
922
+ ----------
923
+ ax : matplotlib Axes Target axes (assumed log-y in A/mm²).
924
+ color_map : dict Mapping {material_key: hex_color} for NbTi,
925
+ Nb3Sn, REBCO; the same colours used for the
926
+ D0FUS curves so the visual pairing is direct.
927
+ alpha : float Transparency for both markers and connecting
928
+ line (default 0.30, "background" appearance).
929
+ """
930
+ for key, ref in _NHMFL_JE_REFERENCE.items():
931
+ col = color_map.get(key, "#808080")
932
+ ax.plot(
933
+ ref["B"], ref["Je"],
934
+ linestyle="-", linewidth=1.0,
935
+ marker="o", markersize=4.5,
936
+ color=col, alpha=alpha,
937
+ zorder=1,
938
+ )
939
+ # Single neutral legend handle for the whole NHMFL reference set.
940
+ ax.plot([], [], linestyle="-", linewidth=1.0, marker="o", markersize=4.5,
941
+ color="#555555", alpha=alpha,
942
+ label="NHMFL strand/tape data @ 4.2 K (2011 vintage, ref.)")
943
+
944
+
764
945
  def plot_Jc_scaling(
765
946
  B_min: float = 0.5,
766
947
  B_max: float = 45.0,
767
948
  T_op: float = 4.2,
768
949
  f_non_Cu_LTS: float = 0.50,
769
950
  f_non_Cu_HTS: float = 0.60,
951
+ show_nhmfl_ref: bool = True,
770
952
  save_dir: str | None = None,
771
953
  ) -> None:
772
954
  """
@@ -776,29 +958,47 @@ def plot_Jc_scaling(
776
958
  * Nb₃Sn (LTS, ITER/EU-DEMO TF/CS)
777
959
  * REBCO (HTS, ARC / SPARC)
778
960
 
961
+ When ``show_nhmfl_ref`` is True, the NHMFL/MagLab experimental engineering
962
+ current density data at 4.2 K is overlaid as a transparent background
963
+ reference (color-matched markers + thin line per material).
964
+
779
965
  Parameters
780
966
  ----------
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
967
+ B_min, B_max : float Field scan range [T].
968
+ T_op : float Operating temperature [K].
969
+ f_non_Cu_LTS : float Non-copper fraction for LTS strands [-].
970
+ f_non_Cu_HTS : float Non-copper fraction for HTS tapes [-].
971
+ show_nhmfl_ref : bool Overlay NHMFL 4.2 K reference data (default True).
972
+ save_dir : str or None
786
973
 
787
974
  References
788
975
  ----------
789
976
  ITER TF strand specifications; Nijhuis (2008); Fleiter & Ballarino (2014).
977
+ NHMFL/MagLab Engineering Current Density Plot, updated 2021-01-04.
790
978
  """
791
979
  B_vals = np.linspace(B_min, B_max, 300)
792
980
  J_NbTi = J_non_Cu_NbTi(B_vals, T_op) * f_non_Cu_LTS / 1e6 # [A/mm²]
793
981
  J_Nb3Sn = J_non_Cu_Nb3Sn(B_vals, T_op, Eps=-0.003) * f_non_Cu_LTS / 1e6
794
982
  J_REBCO = J_non_Cu_REBCO(B_vals, T_op, Tet=0) * f_non_Cu_HTS / 1e6
795
983
 
984
+ # Colour palette shared between D0FUS curves and NHMFL reference overlay.
985
+ col_NbTi, col_Nb3Sn, col_REBCO = "#A06AB4", "#E06C75", "#D4B000"
986
+
796
987
  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")
988
+
989
+ # NHMFL background reference (drawn first lower z-order).
990
+ if show_nhmfl_ref:
991
+ _overlay_nhmfl_reference(
992
+ ax,
993
+ color_map={"NbTi": col_NbTi, "Nb3Sn": col_Nb3Sn, "REBCO": col_REBCO},
994
+ alpha=0.30,
995
+ )
996
+
997
+ ax.plot(B_vals, J_NbTi, lw=2, color=col_NbTi, label="NbTi strand", zorder=3)
998
+ ax.plot(B_vals, J_Nb3Sn, lw=2, color=col_Nb3Sn, label="Nb₃Sn strand", zorder=3)
999
+ ax.plot(B_vals, J_REBCO, lw=2, color=col_REBCO, label="REBCO tape", zorder=3)
800
1000
  ax.set_xlabel("Magnetic field B [T]", fontsize=12)
801
- ax.set_ylabel("Engineering current density J [A/mm²]", fontsize=12)
1001
+ ax.set_ylabel("Strand/Tape current density [A/mm²]", fontsize=12)
802
1002
  ax.set_title(f"Superconductor Jc scalings @ {T_op} K", fontsize=12)
803
1003
  ax.legend(loc="upper right", fontsize=10)
804
1004
  ax.grid(True, alpha=0.3)
@@ -2016,6 +2216,85 @@ def plot_radiation_profile(
2016
2216
  _save_or_show(fig, save_dir, "run_radiation_profile")
2017
2217
 
2018
2218
 
2219
+ def plot_divertor_two_point(
2220
+ run: dict,
2221
+ n_pts: int = 300,
2222
+ save_dir: str | None = None,
2223
+ ) -> None:
2224
+ """
2225
+ Simple two-point-model view: target temperature versus SOL dissipation.
2226
+
2227
+ A single panel shows the target electron temperature T_et as a function of
2228
+ the SOL power-loss fraction f_cooling, at the converged upstream conditions
2229
+ (q_par_u, p_u read from the run). The dashed line is the detachment
2230
+ threshold T_et = 10 eV (Stangeby 2018). The flat plateau at high T_et is the
2231
+ attached sheath-limited branch where the two-point model no longer applies.
2232
+ The minimum dissipation required for target survival (Eq. 14) is given in
2233
+ the title and marked by the dotted vertical line.
2234
+
2235
+ Parameters
2236
+ ----------
2237
+ run : dict D0FUS run output; reads the 'divertor' sub-dict produced
2238
+ by f_heat_two_point.
2239
+ n_pts : int Number of f_cooling samples.
2240
+ save_dir : str or None
2241
+
2242
+ References
2243
+ ----------
2244
+ P.C. Stangeby, Plasma Phys. Control. Fusion 60 (2018) 044022.
2245
+ """
2246
+ div = run.get("divertor", {}) or {}
2247
+ if not {"q_par_u", "T_eu", "n_sep"}.issubset(div):
2248
+ print(" [skip] no two-point-model divertor solution in run dict")
2249
+ return
2250
+
2251
+ q_par_u = div["q_par_u"] # [MW/m^2]
2252
+ T_eu = div["T_eu"] # [eV]
2253
+ n_sep = div["n_sep"] # [m^-3]
2254
+ f_c_op = div.get("f_cooling", 0.0)
2255
+ f_m_op = div.get("f_mom", 0.0)
2256
+ T_et_op = div.get("T_et", np.nan)
2257
+ f_pwr_req = div.get("f_pwr_loss_req", np.nan)
2258
+ R0 = run.get("R0", "?")
2259
+
2260
+ q_par_u_SI = q_par_u * 1e6
2261
+ p_u = 2.0 * n_sep * E_ELEM * T_eu
2262
+
2263
+ f_c = np.linspace(0.0, 0.995, n_pts)
2264
+ T_et = np.array([min(_two_point_core(q_par_u_SI, p_u, fc, f_m_op, 7.0, M_F_DT)[0],
2265
+ T_eu) for fc in f_c])
2266
+
2267
+ fig, ax = plt.subplots(figsize=(7, 4.5))
2268
+ ax.plot(f_c, T_et, color="#4477AA", lw=2.4)
2269
+ ax.axhline(10.0, color="#EE6677", lw=1.3, ls="--")
2270
+ ax.text(0.015, 11.0, "detachment (10 eV)", color="#EE6677",
2271
+ fontsize=9, va="bottom")
2272
+ if np.isfinite(f_pwr_req):
2273
+ ax.axvline(f_pwr_req, color="0.55", lw=1.0, ls=":")
2274
+ if np.isfinite(T_et_op):
2275
+ ax.plot([f_c_op], [T_et_op], "o", ms=9, color="k", zorder=5)
2276
+ near_left = f_c_op < 0.5
2277
+ near_top = T_et_op > 0.3 * T_eu
2278
+ ax.annotate(f"operating point\n$T_{{e,t}}$ = {T_et_op:.1f} eV",
2279
+ (f_c_op, T_et_op), textcoords="offset points",
2280
+ xytext=(14 if near_left else -12, -16 if near_top else 14),
2281
+ ha="left" if near_left else "right",
2282
+ va="top" if near_top else "bottom", fontsize=9)
2283
+
2284
+ ax.set_yscale("log")
2285
+ ax.set_xlim(0, 1)
2286
+ ax.set_xlabel(r"SOL power-loss fraction $f_{\rm cooling}$", fontsize=12)
2287
+ ax.set_ylabel(r"Target electron temperature $T_{e,t}$ [eV]", fontsize=12)
2288
+ ttl = (rf"Divertor two-point model: $R_0$={R0} m, "
2289
+ rf"$q_{{\parallel u}}$={q_par_u/1e3:.2f} GW/m$^2$")
2290
+ if np.isfinite(f_pwr_req):
2291
+ ttl += f", required dissipation = {f_pwr_req:.2f}"
2292
+ ax.set_title(ttl, fontsize=11)
2293
+ ax.grid(True, which="both", alpha=0.3)
2294
+ plt.tight_layout()
2295
+ _save_or_show(fig, save_dir, "run_divertor_two_point")
2296
+
2297
+
2019
2298
  # ---------------------------------------------------------------------------
2020
2299
  # A — Convenience wrapper
2021
2300
  # ---------------------------------------------------------------------------
@@ -2663,10 +2942,10 @@ def plot_all(
2663
2942
 
2664
2943
  ── Plasma shaping [ 1– 7]
2665
2944
  ── Kinetic profiles [ 8–11]
2666
- ── Transport & current [12–14]
2667
- ── Radiation & impurities [1517]
2668
- ── Superconductor eng. [1820]
2669
- ── Coil sizing & mechanics [21–29]
2945
+ ── Transport & current [12–13]
2946
+ ── Radiation & impurities [1416]
2947
+ ── Superconductor eng. [1719]
2948
+ ── Coil sizing & mechanics [20–29]
2670
2949
  · TF grading [21–23]
2671
2950
  · CS / CIRCE / geometry [24–27]
2672
2951
  · Benchmarks [28–29]
@@ -2732,6 +3011,9 @@ def plot_all(
2732
3011
  _p(14, "Coronal cooling coefficient L_z(T)")
2733
3012
  plot_Lz_cooling(save_dir=save_dir)
2734
3013
 
3014
+ _p(15, "D-T reactivity ⟨σv⟩(T)")
3015
+ plot_DT_reactivity(save_dir=save_dir)
3016
+
2735
3017
  _p(16, "Helium ash fraction")
2736
3018
  plot_He_fraction(save_dir=save_dir)
2737
3019
 
@@ -2797,23 +3079,24 @@ def plot_run(
2797
3079
  save_dir: str | None = None,
2798
3080
  ) -> None:
2799
3081
  """
2800
- Render the run-specific figure set (10 figures).
3082
+ Render the run-specific figure set (11 figures).
2801
3083
 
2802
3084
  This is the subset called after each D0FUS run. It contains only the
2803
3085
  figures that depend on the current run configuration and results —
2804
3086
  no validation curves, no benchmarks, no scaling-law surveys.
2805
3087
 
2806
3088
  Figures produced:
2807
- [ 1/10] Tokamak LCFS comparison (with D0FUS overlay)
2808
- [ 2/10] Miller flux surfaces (run geometry)
2809
- [ 3/10] Shaping profiles κ(ρ), δ(ρ)
2810
- [ 4/10] Kinetic profiles n(ρ), T(ρ), p(ρ)
2811
- [ 5/10] Safety factor q(ρ) and current decomposition
2812
- [ 6/10] Radiation profiles
2813
- [ 7/10] TF coil side view
2814
- [ 8/10] CICC TF conductor
2815
- [ 9/10] CS cross-section
2816
- [10/10] CICC CS conductor
3089
+ [ 1/11] Tokamak LCFS comparison (with D0FUS overlay)
3090
+ [ 2/11] Miller flux surfaces (run geometry)
3091
+ [ 3/11] Shaping profiles κ(ρ), δ(ρ)
3092
+ [ 4/11] Kinetic profiles n(ρ), T(ρ), p(ρ)
3093
+ [ 5/11] Safety factor q(ρ) and current decomposition
3094
+ [ 6/11] Radiation profiles
3095
+ [ 7/11] Divertor two-point model (detachment vs SOL dissipation)
3096
+ [ 8/11] TF coil side view
3097
+ [ 9/11] CICC TF conductor
3098
+ [10/11] CS cross-section
3099
+ [11/11] CICC CS conductor
2817
3100
 
2818
3101
  Parameters
2819
3102
  ----------
@@ -2822,7 +3105,7 @@ def plot_run(
2822
3105
  If provided, figures are saved as PNG files.
2823
3106
  Pass ``None`` to display interactively.
2824
3107
  """
2825
- N = 10
3108
+ N = 11
2826
3109
 
2827
3110
  def _p(i, label):
2828
3111
  print(f" [{i:2d}/{N}] {label}")
@@ -2850,17 +3133,20 @@ def plot_run(
2850
3133
  _p(6, "Radiation profiles")
2851
3134
  plot_radiation_profile(run, save_dir=save_dir)
2852
3135
 
3136
+ _p(7, "Divertor two-point model")
3137
+ plot_divertor_two_point(run, save_dir=save_dir)
3138
+
2853
3139
  # ── Coils & conductors ────────────────────────────────────────────
2854
- _p(7, "TF coil side view")
3140
+ _p(8, "TF coil side view")
2855
3141
  plot_TF_side_view(run, save_dir=save_dir)
2856
3142
 
2857
- _p(8, "CICC TF conductor")
3143
+ _p(9, "CICC TF conductor")
2858
3144
  plot_CICC_cross_section(build_conductor_from_run(run, coil="TF"), save_dir=save_dir)
2859
3145
 
2860
- _p(9, "CS cross-section")
3146
+ _p(10, "CS cross-section")
2861
3147
  plot_CS_cross_section(run, save_dir=save_dir)
2862
3148
 
2863
- _p(10, "CICC CS conductor")
3149
+ _p(11, "CICC CS conductor")
2864
3150
  plot_CICC_cross_section(build_conductor_from_run(run, coil="CS"), save_dir=save_dir)
2865
3151
 
2866
3152
  print("Done.")
@@ -3107,11 +3393,11 @@ def plot_TF_benchmark_table(cfg=None, save_dir=None) -> None:
3107
3393
  "ARC": {"a": 1.10, "b": 0.89, "R0": 3.30, "σ": 1000e6, "T_op": 20.0,
3108
3394
  "B_max": 23.0, "n_TF": 1, "sc": "REBCO", "config": "Plug",
3109
3395
  "κ": 1.84, "I_cond": 50e3, "V_max": 10e3, "N_sub": 6,
3110
- "tau_h": 20, "J_wost": 120e6},
3396
+ "tau_h": 20, "J_wost": 200e6},
3111
3397
  "SPARC": {"a": 0.57, "b": 0.18, "R0": 1.85, "σ": 1000e6, "T_op": 20.0,
3112
3398
  "B_max": 20.0, "n_TF": 1, "sc": "REBCO", "config": "Bucking",
3113
- "κ": 1.75, "I_cond": 40.5e3,"V_max": 10e3, "N_sub": 4,
3114
- "tau_h": 20, "J_wost": 120e6},
3399
+ "κ": 1.75, "I_cond": 40.5e3,"V_max": 10e3, "N_sub": 6,
3400
+ "tau_h": 20, "J_wost": 200e6},
3115
3401
  }
3116
3402
 
3117
3403
  def _clean(val):
@@ -18,11 +18,13 @@ os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
18
18
 
19
19
  import json
20
20
  import math
21
+ import multiprocessing
21
22
  import random
22
23
  import re
23
24
  import shutil
24
25
  import sys
25
26
  import time
27
+ import traceback
26
28
  import warnings
27
29
  import importlib
28
30
  from datetime import datetime
@@ -34,7 +36,7 @@ import numpy as np
34
36
  import pandas as pd
35
37
  import sympy as sp
36
38
  from typing import List, Tuple
37
- from dataclasses import dataclass
39
+ from dataclasses import dataclass, replace, asdict
38
40
 
39
41
  #%% Scipy - Optimization and Numerical Methods
40
42
 
@@ -66,11 +68,13 @@ import matplotlib.colors as mcolors
66
68
  import matplotlib.lines as mlines
67
69
  import matplotlib.patches as mpatches
68
70
  import matplotlib.cm as cm
71
+ from matplotlib.animation import FuncAnimation, PillowWriter
69
72
  from matplotlib.colors import Normalize
70
73
  from matplotlib.gridspec import GridSpec
71
74
  from matplotlib.patches import Circle, Rectangle, Patch
72
75
  from matplotlib.ticker import MultipleLocator
73
76
  from mpl_toolkits.axes_grid1 import make_axes_locatable
77
+ from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (registers the '3d' projection)
74
78
  from pandas.plotting import table
75
79
  from tqdm import tqdm
76
80
  from dataclasses import dataclass