d0fus 2.2.0__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,14 +48,15 @@ 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,
54
55
  calculate_cable_current_density,
55
56
  eta_old, eta_spitzer, eta_sauter, eta_redl,
56
- f_TF_academic, f_TF_D0FUS,
57
- Winding_Pack_D0FUS, gamma_func, _last_graded_profile,
58
- f_CS_ACAD, f_CS_D0FUS, f_CS_CIRCE,
57
+ f_TF_academic, f_TF_refined,
58
+ Winding_Pack_refined, gamma_func, _last_graded_profile,
59
+ f_CS_ACAD, f_CS_refined, f_CS_CIRCE,
59
60
  F_CIRCE0D, compute_von_mises_stress,
60
61
  calculate_E_mag_TF,
61
62
  )
@@ -76,14 +77,15 @@ 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,
82
84
  calculate_cable_current_density,
83
85
  eta_old, eta_spitzer, eta_sauter, eta_redl,
84
- f_TF_academic, f_TF_D0FUS,
85
- Winding_Pack_D0FUS, gamma_func, _last_graded_profile,
86
- f_CS_ACAD, f_CS_D0FUS, f_CS_CIRCE,
86
+ f_TF_academic, f_TF_refined,
87
+ Winding_Pack_refined, gamma_func, _last_graded_profile,
88
+ f_CS_ACAD, f_CS_refined, f_CS_CIRCE,
87
89
  F_CIRCE0D, compute_von_mises_stress,
88
90
  calculate_E_mag_TF,
89
91
  )
@@ -193,9 +195,9 @@ def plot_shaping_profiles(
193
195
  """
194
196
  Plot radial elongation κ(ρ) and triangularity δ(ρ) profiles.
195
197
 
196
- Both the 'Academic' (constant-κ, δ=0) and the 'D0FUS PCHIP' profiles are
197
- shown. For δ, both positive and negative triangularity cases are
198
- superimposed.
198
+ Both the 'Academic' (constant-κ, δ=0) and the 'Refined PCHIP'
199
+ profiles are shown. For δ, both positive and negative triangularity
200
+ cases are superimposed.
199
201
 
200
202
  Parameters
201
203
  ----------
@@ -206,7 +208,7 @@ def plot_shaping_profiles(
206
208
 
207
209
  References
208
210
  ----------
209
- Christiansen et al., Nucl. Fusion 32, 291 (1992) — PCHIP parameterisation.
211
+ Fritsch & Carlson, SIAM J. Numer. Anal. 17, 238 (1980) — PCHIP scheme.
210
212
  Ball & Parra, PPCF 57, 045006 (2015) — κ core penetration.
211
213
  """
212
214
  rho = np.linspace(1e-4, 1.0, 500)
@@ -223,7 +225,7 @@ def plot_shaping_profiles(
223
225
  label=f"Academic: κ = κ_edge = {kappa_edge} (const)")
224
226
  ax.plot(rho, kappa_profile(rho, kappa_edge, kappa_95),
225
227
  color="tab:red", lw=2,
226
- label="D0FUS: flat core at κ₉₅, edge rise (PCHIP)")
228
+ label="Refined: flat core at κ₉₅, edge rise (PCHIP)")
227
229
  ax.axvline(rho_95, color="gray", lw=1, ls="--", alpha=0.7)
228
230
  ax.axhline(kappa_95, color="gray", lw=1, ls="--", alpha=0.7)
229
231
  ax.plot(rho_95, kappa_95, "o", color="tab:red", ms=7, zorder=5,
@@ -243,10 +245,10 @@ def plot_shaping_profiles(
243
245
  ax.axhline(0, color="tab:blue", lw=2, ls="-", label="Academic: δ = 0")
244
246
  ax.plot(rho, delta_profile(rho, delta_edge, delta_95),
245
247
  color="tab:red", lw=2,
246
- label=f"D0FUS PCHIP: δ_edge = +{delta_edge} (D-shape)")
248
+ label=f"Refined PCHIP: δ_edge = +{delta_edge} (D-shape)")
247
249
  ax.plot(rho, delta_profile(rho, delta_edge_neg, delta_95_neg),
248
250
  color="tab:purple", lw=2,
249
- label=f"D0FUS PCHIP: δ_edge = {delta_edge_neg} (neg. triang.)")
251
+ label=f"Refined PCHIP: δ_edge = {delta_edge_neg} (neg. triang.)")
250
252
  ax.axvline(rho_95, color="gray", lw=1, ls="--", alpha=0.7)
251
253
  ax.axhline(delta_95, color="tab:red", lw=1, ls="--", alpha=0.6)
252
254
  ax.plot(rho_95, delta_95, "o", color="tab:red", ms=7, zorder=5,
@@ -283,9 +285,9 @@ def plot_miller_surfaces(
283
285
  ) -> None:
284
286
  """
285
287
  Plot Miller flux surfaces for three geometry configurations side by side:
286
- 1. D0FUS PCHIP — positive triangularity
287
- 2. Academic — circular (κ const, δ = 0)
288
- 3. D0FUS PCHIP — negative triangularity
288
+ 1. Refined PCHIP — positive triangularity
289
+ 2. Academic — circular (κ const, δ = 0)
290
+ 3. Refined PCHIP — negative triangularity
289
291
 
290
292
  Parameters
291
293
  ----------
@@ -309,15 +311,15 @@ def plot_miller_surfaces(
309
311
  colors_fs = cm.Blues(np.linspace(0.20, 0.95, n_levels))
310
312
 
311
313
  configs = [
312
- ("D0FUS",
313
- f"D0FUS PCHIP — positive δ\n"
314
+ ("refined",
315
+ f"Refined PCHIP — positive δ\n"
314
316
  f"(κ₉₅ = {kappa_95:.3f}, δ_edge = +{delta_edge}, δ₉₅ = +{delta_95:.3f})",
315
317
  kappa_edge, delta_edge, kappa_95, delta_95),
316
318
  ("Academic",
317
319
  f"Academic (κ = {kappa_edge} const, δ = 0)",
318
320
  kappa_edge, 0.0, kappa_95, 0.0),
319
- ("D0FUS",
320
- f"D0FUS PCHIP — negative δ\n"
321
+ ("refined",
322
+ f"Refined PCHIP — negative δ\n"
321
323
  f"(κ₉₅ = {kappa_95:.3f}, δ_edge = {delta_edge_neg}, δ₉₅ = {delta_95_neg:.3f})",
322
324
  kappa_edge, delta_edge_neg, kappa_95, delta_95_neg),
323
325
  ]
@@ -348,7 +350,7 @@ def plot_miller_surfaces(
348
350
  ax.grid(True, alpha=0.3)
349
351
 
350
352
  plt.suptitle(
351
- f"Miller flux surfaces — Academic | D0FUS (+δ) | D0FUS (−δ)\n"
353
+ f"Miller flux surfaces — Academic | Refined (+δ) | Refined (−δ)\n"
352
354
  f"(R₀ = {R0} m, a = {a} m, κ = {kappa_edge}, |δ| = {delta_edge})",
353
355
  fontsize=12, fontweight="bold"
354
356
  )
@@ -461,7 +463,7 @@ def plot_first_wall_surface(
461
463
  ) -> None:
462
464
  """
463
465
  Compare first-wall surface area from the Academic (Ramanujan ellipse)
464
- and D0FUS (Miller LCFS numerical) models.
466
+ and refined (Miller LCFS numerical) models.
465
467
 
466
468
  Left panel : S vs κ (δ fixed).
467
469
  Right panel : S vs δ (κ fixed, including negative triangularity).
@@ -483,11 +485,11 @@ def plot_first_wall_surface(
483
485
 
484
486
  S_ac_k = [f_first_wall_surface(R0, a, k, delta_fix, "Academic")
485
487
  for k in kappa_scan]
486
- S_d0_k = [f_first_wall_surface(R0, a, k, delta_fix, "D0FUS")
488
+ S_d0_k = [f_first_wall_surface(R0, a, k, delta_fix, "refined")
487
489
  for k in kappa_scan]
488
490
  S_ac_d = [f_first_wall_surface(R0, a, kappa_fix, d, "Academic")
489
491
  for d in delta_scan]
490
- S_d0_d = [f_first_wall_surface(R0, a, kappa_fix, d, "D0FUS")
492
+ S_d0_d = [f_first_wall_surface(R0, a, kappa_fix, d, "refined")
491
493
  for d in delta_scan]
492
494
 
493
495
  fig, axes = plt.subplots(1, 2, figsize=(13, 5))
@@ -501,7 +503,7 @@ def plot_first_wall_surface(
501
503
  f"First wall surface vs δ (κ = {kappa_fix})"),
502
504
  ]:
503
505
  ax.plot(xarr, S_ac, "b-", lw=2, label="Academic (Ramanujan ellipse)")
504
- ax.plot(xarr, S_d0, "r--", lw=2, label="D0FUS (Miller LCFS)")
506
+ ax.plot(xarr, S_d0, "r--", lw=2, label="Refined (Miller LCFS)")
505
507
  ax.set_xlabel(xlabel, fontsize=12)
506
508
  ax.set_ylabel("S [m²]", fontsize=12)
507
509
  ax.set_title(title, fontsize=11)
@@ -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 — D0FUS 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 D0FUS 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)
@@ -937,7 +1135,7 @@ def plot_TF_thickness_vs_field(
937
1135
  ) -> None:
938
1136
  """
939
1137
  Plot TF coil total inboard thickness vs peak magnetic field for four
940
- mechanical models (Academic/D0FUS × Wedging/Bucking).
1138
+ mechanical models (Academic/refined × Wedging/Bucking).
941
1139
 
942
1140
  An optional MADE benchmark scatter dataset is superimposed on the Wedging
943
1141
  panel for EU-DEMO validation.
@@ -949,7 +1147,7 @@ def plot_TF_thickness_vs_field(
949
1147
  sigma_TF : float Allowable TF coil stress [Pa].
950
1148
  Default 867 MPa (SDC-IC Pm+Pb, austenitic steel at 4 K).
951
1149
  J_max_TF : float Current density on the non-steel area [A/m²].
952
- Default 60 MA/m². The D0FUS cable model (Maddock
1150
+ Default 60 MA/m². The refined cable model (Maddock
953
1151
  adiabatic hotspot) predicts ~40 MA/m² for HTS at
954
1152
  20 T,
955
1153
  From Giannini 2023 Fig. 20 (validated FEM
@@ -983,11 +1181,11 @@ def plot_TF_thickness_vs_field(
983
1181
  "Bucking", cfg.coef_inboard_tension,
984
1182
  cfg.F_CClamp)[0])
985
1183
  # c_BP = 0: MADE total radial build = WP + case vault (no backplate)
986
- d0_w.append(f_TF_D0FUS(a, b, R0, sigma_TF, J_max_TF, B,
1184
+ d0_w.append(f_TF_refined(a, b, R0, sigma_TF, J_max_TF, B,
987
1185
  "Wedging", 0.5, 1,
988
1186
  0.0, cfg.coef_inboard_tension,
989
1187
  cfg.F_CClamp)[0])
990
- d0_b.append(f_TF_D0FUS(a, b, R0, sigma_TF, J_max_TF, B,
1188
+ d0_b.append(f_TF_refined(a, b, R0, sigma_TF, J_max_TF, B,
991
1189
  "Bucking", 1.0, 1,
992
1190
  0.0, cfg.coef_inboard_tension,
993
1191
  cfg.F_CClamp)[0])
@@ -1007,7 +1205,7 @@ def plot_TF_thickness_vs_field(
1007
1205
  # Wedging panel
1008
1206
  ax = axes[0]
1009
1207
  ax.plot(B_vals, acad_w, color=colors[0], lw=2, label="Academic — Wedging")
1010
- ax.plot(B_vals, d0_w, color=colors[1], lw=2, label="D0FUS — Wedging")
1208
+ ax.plot(B_vals, d0_w, color=colors[1], lw=2, label="Refined — Wedging")
1011
1209
  ax.scatter(x_made, y_made, color="k", marker="x", s=80, label="MADE")
1012
1210
  ax.set_xlabel("Peak field B_max [T]", fontsize=12)
1013
1211
  ax.set_ylabel("TF total inboard thickness [m]", fontsize=12)
@@ -1018,7 +1216,7 @@ def plot_TF_thickness_vs_field(
1018
1216
  # Bucking panel
1019
1217
  ax = axes[1]
1020
1218
  ax.plot(B_vals, acad_b, color=colors[0], lw=2, label="Academic — Bucking")
1021
- ax.plot(B_vals, d0_b, color=colors[1], lw=2, label="D0FUS — Bucking")
1219
+ ax.plot(B_vals, d0_b, color=colors[1], lw=2, label="Refined — Bucking")
1022
1220
  ax.set_xlabel("Peak field B_max [T]", fontsize=12)
1023
1221
  ax.set_ylabel("TF total inboard thickness [m]", fontsize=12)
1024
1222
  ax.set_title("Bucking configuration", fontsize=12)
@@ -1067,9 +1265,9 @@ def plot_TF_grading_thickness_vs_field(
1067
1265
  c_g = np.full_like(B_scan, np.nan)
1068
1266
 
1069
1267
  for i, Bm in enumerate(B_scan):
1070
- ru = Winding_Pack_D0FUS(R0, a, b, sigma_TF, J_max_TF, Bm,
1268
+ ru = Winding_Pack_refined(R0, a, b, sigma_TF, J_max_TF, Bm,
1071
1269
  0.5, n, grading=False)
1072
- rg = Winding_Pack_D0FUS(R0, a, b, sigma_TF, J_max_TF, Bm,
1270
+ rg = Winding_Pack_refined(R0, a, b, sigma_TF, J_max_TF, Bm,
1073
1271
  0.5, n, grading=True)
1074
1272
  if np.isfinite(ru[0]):
1075
1273
  c_u[i] = ru[0] * 100
@@ -1116,9 +1314,9 @@ def plot_TF_grading_reduction(
1116
1314
  c_g = np.full_like(B_scan, np.nan)
1117
1315
 
1118
1316
  for i, Bm in enumerate(B_scan):
1119
- ru = Winding_Pack_D0FUS(R0, a, b, sigma_TF, J_max_TF, Bm,
1317
+ ru = Winding_Pack_refined(R0, a, b, sigma_TF, J_max_TF, Bm,
1120
1318
  0.5, n, grading=False)
1121
- rg = Winding_Pack_D0FUS(R0, a, b, sigma_TF, J_max_TF, Bm,
1319
+ rg = Winding_Pack_refined(R0, a, b, sigma_TF, J_max_TF, Bm,
1122
1320
  0.5, n, grading=True)
1123
1321
  if np.isfinite(ru[0]):
1124
1322
  c_u[i] = ru[0] * 100
@@ -1164,9 +1362,9 @@ def plot_TF_grading_alpha_profile(
1164
1362
  cfg = DEFAULT_CONFIG
1165
1363
 
1166
1364
  # Run graded (populates _last_graded_profile) then ungraded
1167
- rg = Winding_Pack_D0FUS(R0, a, b, sigma_TF, J_max_TF, B_max,
1365
+ rg = Winding_Pack_refined(R0, a, b, sigma_TF, J_max_TF, B_max,
1168
1366
  0.5, n, grading=True)
1169
- ru = Winding_Pack_D0FUS(R0, a, b, sigma_TF, J_max_TF, B_max,
1367
+ ru = Winding_Pack_refined(R0, a, b, sigma_TF, J_max_TF, B_max,
1170
1368
  0.5, n, grading=False)
1171
1369
 
1172
1370
  if not np.isfinite(rg[0]) or not np.isfinite(ru[0]):
@@ -1221,7 +1419,7 @@ def plot_CS_thickness_vs_flux(
1221
1419
  ) -> None:
1222
1420
  """
1223
1421
  Plot CS coil winding-pack thickness and peak field vs volt-second budget
1224
- for three mechanical models (Academic, D0FUS, CIRCE) and three
1422
+ for three mechanical models (Academic, refined, CIRCE) and three
1225
1423
  configuration types (Wedging, Bucking, Plug).
1226
1424
 
1227
1425
  For the Wedging configuration, a MADE benchmark scatter dataset
@@ -1234,7 +1432,7 @@ def plot_CS_thickness_vs_flux(
1234
1432
  Geometry: a_cs=3 m, b_cs=1.2 m, c_cs=2 m, R0=9 m, Gap=0.1 m
1235
1433
  → R_CS_ext = 9 - 3 - 1.2 - 2 - 0.1 = 2.7 m
1236
1434
 
1237
- Stress: σ_CS = 600 MPa. D0FUS applies fatigue_CS = 2 internally
1435
+ Stress: σ_CS = 600 MPa. The refined model applies fatigue_CS = 2 internally
1238
1436
  → σ_eff = 300 MPa as the fig.2 source
1239
1437
 
1240
1438
  J_wost = 85 MA/m², derived from Sarasola 2020 Sec. II-B:
@@ -1281,7 +1479,7 @@ def plot_CS_thickness_vs_flux(
1281
1479
  # subtracted). The MADE reference data published by Sarasola et al.
1282
1480
  # corresponds to the CS hardware design output at full bipolar swing
1283
1481
  # (the curve is parameterised on the total volt-second capacity of
1284
- # the coil, not on the plasma inductive demand). To compare D0FUS to
1482
+ # the coil, not on the plasma inductive demand). To compare the refined model to
1285
1483
  # MADE on the same footing, we feed the full-bipolar abscissa to the
1286
1484
  # solver and disable the additional 1/f_swing_usable rescaling. The
1287
1485
  # design-mode default (0.75 = 25% control reserve) is restored
@@ -1299,8 +1497,8 @@ def plot_CS_thickness_vs_flux(
1299
1497
 
1300
1498
  psi_values = np.linspace(0, psi_max, n_psi)
1301
1499
  mech_confs = ["Wedging", "Bucking", "Plug"]
1302
- model_funcs = {"Academic": f_CS_ACAD, "D0FUS": f_CS_D0FUS, "CIRCE": f_CS_CIRCE}
1303
- colors = {"Academic": "blue", "D0FUS": "green", "CIRCE": "red"}
1500
+ model_funcs = {"Academic": f_CS_ACAD, "refined": f_CS_refined, "CIRCE": f_CS_CIRCE}
1501
+ colors = {"Academic": "blue", "refined": "green", "CIRCE": "red"}
1304
1502
 
1305
1503
  # Storage: results[model][config] = {"thickness": [], "B": []}
1306
1504
  results = {m: {c: {"thickness": [], "B": []} for c in mech_confs}
@@ -1653,10 +1851,11 @@ def plot_shaping_run(
1653
1851
  Plot radial elongation κ(ρ) and triangularity δ(ρ) profiles for the
1654
1852
  geometry used in a D0FUS run.
1655
1853
 
1656
- In 'D0FUS' geometry mode, the Christiansen PCHIP parameterisation is
1657
- used (radially varying κ and δ with core penetration).
1658
- In 'Academic' mode, κ(ρ) = κ_edge = const and δ(ρ) = 0 everywhere,
1659
- consistent with the cylindrical-torus approximation.
1854
+ In 'refined' geometry mode, three-point PCHIP profiles are used for
1855
+ κ(ρ) and δ(ρ) (C¹ continuous globally, monotonicity-preserving,
1856
+ with radially varying core penetration). In 'Academic' mode,
1857
+ κ(ρ) = κ_edge = const and δ(ρ) = 0 everywhere, consistent with the
1858
+ cylindrical-torus approximation.
1660
1859
 
1661
1860
  Parameters
1662
1861
  ----------
@@ -1666,11 +1865,11 @@ def plot_shaping_run(
1666
1865
 
1667
1866
  References
1668
1867
  ----------
1669
- Christiansen et al., Nucl. Fusion 32, 291 (1992).
1868
+ Fritsch & Carlson, SIAM J. Numer. Anal. 17, 238 (1980) — PCHIP scheme.
1670
1869
  Ball & Parra, PPCF 57, 045006 (2015) — κ core penetration.
1671
1870
  """
1672
1871
  R0, a, kappa_edge, delta_edge, kappa_95, delta_95, _ = _resolve_geometry(run)
1673
- geom_mode = run.get("Plasma_geometry", "D0FUS")
1872
+ geom_mode = run.get("Plasma_geometry", "refined")
1674
1873
  rho = np.linspace(1e-4, 1.0, n_rho)
1675
1874
 
1676
1875
  if geom_mode == "Academic":
@@ -1678,7 +1877,7 @@ def plot_shaping_run(
1678
1877
  kap_arr = np.full_like(rho, kappa_edge)
1679
1878
  del_arr = np.zeros_like(rho)
1680
1879
  else:
1681
- # D0FUS PCHIP profiles with radial variation
1880
+ # Refined Miller PCHIP profiles with radial variation
1682
1881
  kap_arr = kappa_profile(rho, kappa_edge, kappa_95)
1683
1882
  del_arr = delta_profile(rho, delta_edge, delta_95)
1684
1883
 
@@ -1690,7 +1889,7 @@ def plot_shaping_run(
1690
1889
  ax.axhline(kappa_edge, color="tab:blue", lw=2.2,
1691
1890
  label=f"Academic: κ = κ_edge = {kappa_edge:.3f} (const)")
1692
1891
  else:
1693
- ax.plot(rho, kap_arr, "tab:blue", lw=2.2, label="D0FUS PCHIP")
1892
+ ax.plot(rho, kap_arr, "tab:blue", lw=2.2, label="Refined PCHIP")
1694
1893
  ax.axhline(kappa_edge, color="tab:orange", lw=1.4, ls="--",
1695
1894
  label=f"κ_edge = {kappa_edge:.3f}")
1696
1895
  ax.axhline(kappa_95, color="tab:gray", lw=1.0, ls=":",
@@ -1709,7 +1908,7 @@ def plot_shaping_run(
1709
1908
  ax.axhline(0, color="tab:blue", lw=2.2,
1710
1909
  label="Academic: δ = 0 (const)")
1711
1910
  else:
1712
- ax.plot(rho, del_arr, "tab:red", lw=2.2, label="D0FUS PCHIP")
1911
+ ax.plot(rho, del_arr, "tab:red", lw=2.2, label="Refined PCHIP")
1713
1912
  ax.axhline(delta_edge, color="tab:orange", lw=1.4, ls="--",
1714
1913
  label=f"δ_edge = {delta_edge:.3f}")
1715
1914
  ax.axhline(delta_95, color="tab:gray", lw=1.0, ls=":",
@@ -1723,7 +1922,7 @@ def plot_shaping_run(
1723
1922
  ax.grid(True, alpha=0.3)
1724
1923
  ax.set_xlim(0, 1)
1725
1924
 
1726
- _mode_str = "Academic" if geom_mode == "Academic" else "D0FUS PCHIP"
1925
+ _mode_str = "Academic" if geom_mode == "Academic" else "Refined PCHIP"
1727
1926
  plt.suptitle(
1728
1927
  f"Run shaping profiles ({_mode_str}) — R₀={R0} m, a={a} m\n"
1729
1928
  f"κ_edge={kappa_edge:.3f}, δ_edge={delta_edge:.3f}",
@@ -1746,8 +1945,9 @@ def plot_flux_surfaces_run(
1746
1945
  """
1747
1946
  Plot nested flux surfaces for the actual geometry of a D0FUS run.
1748
1947
 
1749
- When Plasma_geometry == 'D0FUS', surfaces follow the Miller PCHIP
1750
- parameterisation with radially varying κ(ρ) and δ(ρ).
1948
+ When Plasma_geometry == 'refined', surfaces follow the Miller
1949
+ parameterisation with radially varying κ(ρ) and δ(ρ) built from
1950
+ three-point PCHIP profiles (C¹ continuous, monotonicity-preserving).
1751
1951
  When Plasma_geometry == 'Academic' (or unset), surfaces are concentric
1752
1952
  ellipses with constant κ = κ_edge and δ = 0, consistent with the
1753
1953
  cylindrical-torus approximation used in the Academic solver branch.
@@ -1767,7 +1967,7 @@ def plot_flux_surfaces_run(
1767
1967
  Miller et al., Phys. Plasmas 5, 973 (1998).
1768
1968
  """
1769
1969
  R0, a, kappa_edge, delta_edge, kappa_95, delta_95, _ = _resolve_geometry(run)
1770
- geom_mode = run.get("Plasma_geometry", "D0FUS")
1970
+ geom_mode = run.get("Plasma_geometry", "refined")
1771
1971
 
1772
1972
  theta = np.linspace(0, 2 * np.pi, n_theta)
1773
1973
  rho_levels = np.linspace(0.1, 1.0, n_levels)
@@ -1781,7 +1981,7 @@ def plot_flux_surfaces_run(
1781
1981
  R_surf = R0 + rho_val * a * np.cos(theta)
1782
1982
  Z_surf = kappa_edge * rho_val * a * np.sin(theta)
1783
1983
  else:
1784
- # D0FUS PCHIP Miller surfaces with radially varying κ(ρ), δ(ρ)
1984
+ # Refined Miller PCHIP surfaces with radially varying κ(ρ), δ(ρ)
1785
1985
  R_surf, Z_surf = miller_RZ(rho_val, theta,
1786
1986
  R0, a, kappa_edge, delta_edge,
1787
1987
  kappa_95, delta_95)
@@ -1802,7 +2002,7 @@ def plot_flux_surfaces_run(
1802
2002
  if geom_mode == "Academic":
1803
2003
  _geom_label = "Academic (elliptic, δ = 0)"
1804
2004
  else:
1805
- _geom_label = f"D0FUS PCHIP (δ_edge = {delta_edge:.3f})"
2005
+ _geom_label = f"Refined PCHIP (δ_edge = {delta_edge:.3f})"
1806
2006
 
1807
2007
  ax.set_title(
1808
2008
  f"Flux surfaces — {_geom_label}\n"
@@ -1827,16 +2027,16 @@ def plot_q_profile(
1827
2027
  """
1828
2028
  Plot the safety factor profile q(rho).
1829
2029
 
1830
- Uses the parametric q-profile from f_q_profile_selfconsistent() when
1831
- available in run['_q_sc']. Falls back to the analytical f_q_profile()
1832
- model with a prescribed alpha_J otherwise.
2030
+ Picks data from run['_q_sc'], which is populated by the q,j dispatcher
2031
+ (f_q_profile_academic or f_q_profile_refined according to
2032
+ config.q_profile_mode). When the dict is unavailable, falls back to
2033
+ the analytical parametric q(rho) from f_q_profile() with alpha_J = 1.5.
1833
2034
 
1834
- The current density decomposition j_total = j_Ohm + j_CD + j_bs is
1835
- deliberately NOT plotted. Those profiles are built inside
1836
- f_q_profile_selfconsistent() only to close the self-consistency loop
1837
- on the integral scalar l_i(3); they are not calibrated quantities
1838
- that downstream D0FUS modules consume. The scalars that do propagate
1839
- (alpha_J, q_0, l_i, f_bs) are reported in the figure title.
2035
+ The current density decomposition j_Ohm + j_CD + j_bs is meaningful
2036
+ only in 'refined' mode (in 'academic' mode those components are zero
2037
+ by construction). The figure stays focused on q(rho) and reports the
2038
+ integral diagnostic l_i(3) along with q_0 and either q(0.95) or the
2039
+ imposed q95.
1840
2040
 
1841
2041
  Parameters
1842
2042
  ----------
@@ -1852,18 +2052,20 @@ def plot_q_profile(
1852
2052
  """
1853
2053
  q95 = float(run["q95"])
1854
2054
  _q_sc = run.get("_q_sc", None)
2055
+ q_mode = str(run.get("q_profile_mode", "refined")) # default for legacy runs
1855
2056
 
1856
- # ── Data source: parametric solve or analytical fallback ──────────
2057
+ # ── Data source: dispatcher solution or analytical fallback ──────────
1857
2058
  if _q_sc is not None and 'q_arr' in _q_sc:
1858
2059
  rho = _q_sc['rho']
1859
2060
  q_arr = _q_sc['q_arr']
1860
2061
  li = _q_sc['li']
1861
-
1862
- suptitle_str = r"Safety factor profile $q(\rho)$"
2062
+ q_at_95 = float(_q_sc.get('q_at_95', np.interp(0.95, rho, q_arr)))
2063
+ suptitle_str = (rf"Safety factor profile $q(\rho)$ "
2064
+ rf"({q_mode} mode)")
1863
2065
  else:
1864
- # Analytical fallback (no self-consistent data available)
2066
+ # Analytical fallback (no run output available — used by tests).
1865
2067
  rho = np.linspace(0.0, 0.99, n_rho)
1866
- alpha_J = 1.5 # IPDG89 reference
2068
+ alpha_J = float(run.get("alpha_J", 1.5))
1867
2069
  q_arr = f_q_profile(rho, q95=q95, rho95=0.95, alpha_J=alpha_J)
1868
2070
 
1869
2071
  # Cylindrical li estimate from the analytical form
@@ -1874,7 +2076,9 @@ def plot_q_profile(
1874
2076
  li_integ = np.where(rho > 1e-8, I_norm**2 / rho_s, 0.0)
1875
2077
  li = 2.0 * np.trapezoid(li_integ, rho)
1876
2078
 
1877
- suptitle_str = r"Safety factor profile $q(\rho)$ (analytical fallback)"
2079
+ q_at_95 = float(np.interp(0.95, rho, q_arr))
2080
+ suptitle_str = (rf"Safety factor profile $q(\rho)$ "
2081
+ rf"(analytical fallback, $\alpha_J = {alpha_J:.2f}$)")
1878
2082
 
1879
2083
  # ── Figure ────────────────────────────────────────────────────────
1880
2084
  fig, ax = plt.subplots(1, 1, figsize=(7, 5))
@@ -1883,12 +2087,23 @@ def plot_q_profile(
1883
2087
  ax.axhline(q95, color="gray", ls="--", lw=0.8, alpha=0.5)
1884
2088
  ax.axvline(0.95, color="gray", ls=":", lw=0.8, alpha=0.4)
1885
2089
  ax.plot(rho[0], q_arr[0], "o", color="tab:blue", ms=7, zorder=5)
1886
- ax.plot(0.95, q95, "s", color="tab:red", ms=7, zorder=5,
1887
- label=rf"$q_{{95}} = {q95:.2f}$")
1888
- ax.text(0.04, 0.95,
1889
- rf"$q_0 = {q_arr[0]:.2f}$" + "\n"
1890
- rf"$l_i(3) = {li:.2f}$" + "\n"
1891
- rf"$q(1) = {q_arr[-1]:.2f}$",
2090
+ # In academic mode, q(rho_95) = q95 by construction; in refined mode,
2091
+ # q(rho_95) generally differs and is a meaningful diagnostic.
2092
+ if q_mode == "refined":
2093
+ ax.plot(0.95, q_at_95, "o", color="tab:blue", ms=7, zorder=5)
2094
+ ax.plot(0.95, q95, "s", color="tab:red", ms=7, zorder=5,
2095
+ label=rf"$q_{{95}}^{{\rm scaling}} = {q95:.2f}$")
2096
+ info_text = (rf"$q_0 = {q_arr[0]:.2f}$" + "\n"
2097
+ rf"$q(\rho_{{95}}) = {q_at_95:.2f}$" + "\n"
2098
+ rf"$l_i(3) = {li:.2f}$" + "\n"
2099
+ rf"$q(1) = {q_arr[-1]:.2f}$")
2100
+ else: # academic mode: q95 is imposed
2101
+ ax.plot(0.95, q95, "s", color="tab:red", ms=7, zorder=5,
2102
+ label=rf"$q_{{95}} = {q95:.2f}$ (imposed)")
2103
+ info_text = (rf"$q_0 = {q_arr[0]:.2f}$" + "\n"
2104
+ rf"$l_i(3) = {li:.2f}$" + "\n"
2105
+ rf"$q(1) = {q_arr[-1]:.2f}$")
2106
+ ax.text(0.04, 0.95, info_text,
1892
2107
  transform=ax.transAxes, fontsize=10, va="top",
1893
2108
  bbox=dict(boxstyle="round,pad=0.3", fc="lightyellow", alpha=0.85))
1894
2109
  ax.set_xlabel(r"$\rho$", fontsize=12)
@@ -2646,10 +2861,10 @@ def plot_all(
2646
2861
 
2647
2862
  ── Plasma shaping [ 1– 7]
2648
2863
  ── Kinetic profiles [ 8–11]
2649
- ── Transport & current [12–14]
2650
- ── Radiation & impurities [1517]
2651
- ── Superconductor eng. [1820]
2652
- ── Coil sizing & mechanics [21–29]
2864
+ ── Transport & current [12–13]
2865
+ ── Radiation & impurities [1416]
2866
+ ── Superconductor eng. [1719]
2867
+ ── Coil sizing & mechanics [20–29]
2653
2868
  · TF grading [21–23]
2654
2869
  · CS / CIRCE / geometry [24–27]
2655
2870
  · Benchmarks [28–29]
@@ -2715,6 +2930,9 @@ def plot_all(
2715
2930
  _p(14, "Coronal cooling coefficient L_z(T)")
2716
2931
  plot_Lz_cooling(save_dir=save_dir)
2717
2932
 
2933
+ _p(15, "D-T reactivity ⟨σv⟩(T)")
2934
+ plot_DT_reactivity(save_dir=save_dir)
2935
+
2718
2936
  _p(16, "Helium ash fraction")
2719
2937
  plot_He_fraction(save_dir=save_dir)
2720
2938
 
@@ -3121,8 +3339,8 @@ def plot_TF_benchmark_table(cfg=None, save_dir=None) -> None:
3121
3339
  return f_TF_academic(a, b, R0, sigma, J_wost, B_max,
3122
3340
  conf, cfg.coef_inboard_tension, cfg.F_CClamp)
3123
3341
  else:
3124
- # f_TF_D0FUS returns same tuple layout
3125
- return f_TF_D0FUS(a, b, R0, sigma, J_wost, B_max,
3342
+ # f_TF_refined returns the same tuple layout
3343
+ return f_TF_refined(a, b, R0, sigma, J_wost, B_max,
3126
3344
  conf, omega, n_frac,
3127
3345
  cfg.c_BP, cfg.coef_inboard_tension, cfg.F_CClamp)
3128
3346
 
@@ -3133,7 +3351,7 @@ def plot_TF_benchmark_table(cfg=None, save_dir=None) -> None:
3133
3351
  # consistent with the CS benchmark.
3134
3352
  model_specs = [
3135
3353
  ("Academic", "#9C27B0"),
3136
- ("D0FUS", "#4CAF50"),
3354
+ ("refined", "#4CAF50"),
3137
3355
  ]
3138
3356
 
3139
3357
  cols = ["Machine", "SC", "Config", "B_max [T]", "J [MA/m²]", "σ [MPa]", "c [m]"]
@@ -3428,7 +3646,7 @@ def plot_CS_benchmark_table(cfg=None, save_dir=None) -> None:
3428
3646
  # Model definitions: (label, function, header colour)
3429
3647
  model_specs = [
3430
3648
  ("Academic", f_CS_ACAD, "#9C27B0"),
3431
- ("D0FUS", f_CS_D0FUS, "#2196F3"),
3649
+ ("refined", f_CS_refined, "#2196F3"),
3432
3650
  ("CIRCE", f_CS_CIRCE, "#F44336"),
3433
3651
  ]
3434
3652
 
@@ -3994,7 +4212,7 @@ if __name__ == "__main__":
3994
4212
  # Plasma geometry
3995
4213
  "R0": 6.2,
3996
4214
  "a": 2.0,
3997
- "Plasma_geometry": "D0FUS",
4215
+ "Plasma_geometry": "refined",
3998
4216
  "kappa_edge": 1.85,
3999
4217
  "delta_edge": 0.33,
4000
4218
  "Vprime_data": None,