d0fus 2.3.4__tar.gz → 2.3.5__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.
@@ -27,6 +27,166 @@ else:
27
27
  from D0FUS_BIB.D0FUS_import import *
28
28
  from D0FUS_BIB.D0FUS_parameterization import *
29
29
 
30
+ if __name__ == "__main__":
31
+ # ════════════════════════════════════════════════════════════════════
32
+ # Executable verification suite
33
+ # ────────────────────────────────────────────────────────────────────
34
+ # Active only under direct execution:
35
+ # python D0FUS_BIB/D0FUS_physical_functions.py
36
+ #
37
+ # Each function group below is followed by a __main__ block that
38
+ # (i) re-evaluates the published anchors of its functions, and/or
39
+ # (ii) advances ONE step of a single coherent ITER chain, the same
40
+ # machine from A to Z, mirroring the production solver of
41
+ # D0FUS_EXE/D0FUS_run.py on the shipped deck
42
+ # D0FUS_INPUTS/1_run_ITER.txt.
43
+ #
44
+ # Every block ends with a uniform comparison table and a PASS/FAIL
45
+ # verdict (_bench below). A global summary closes the file and exits
46
+ # with a non-zero status if any check failed, so the suite can serve
47
+ # as a regression guard.
48
+ # ════════════════════════════════════════════════════════════════════
49
+
50
+ _BENCH = {"checks": 0, "failed": [], "blocks": 0}
51
+
52
+ def _fmt(v):
53
+ """Compact numeric formatting for the verification tables."""
54
+ if isinstance(v, str):
55
+ return v
56
+ if v is None:
57
+ return "-"
58
+ av = abs(v)
59
+ if av != 0.0 and (av < 1e-3 or av >= 1e5):
60
+ return f"{v:.3e}"
61
+ return f"{v:.4g}"
62
+
63
+ def _bench(title, rows, notes=()):
64
+ """Print a uniform verification table with a PASS/FAIL verdict.
65
+
66
+ Parameters
67
+ ----------
68
+ title : str
69
+ Block title, printed as a section header.
70
+ rows : list of tuple
71
+ (quantity, value, reference, rel_tol, source) with:
72
+ - rel_tol = float : checked row, |value/reference - 1| < rel_tol;
73
+ - reference = (lo, hi) tuple : checked row, lo <= value <= hi;
74
+ - rel_tol = None (and reference not a tuple) : informative row,
75
+ printed but not counted.
76
+ notes : iterable of str, optional
77
+ Footnotes printed under the table.
78
+ """
79
+ _BENCH["blocks"] += 1
80
+ print(f"\n-- {title} " + "-" * max(2, 92 - len(title)))
81
+ print(f" {'Quantity':<36} {'D0FUS':>11} {'Reference':>11} "
82
+ f"{'D [%]':>8} {'tol':>6} Source")
83
+ print(" " + "-" * 94)
84
+ n_pass, n_chk = 0, 0
85
+ for qty, val, ref, tol, src in rows:
86
+ if tol is None and not isinstance(ref, tuple):
87
+ print(f" {qty:<36} {_fmt(val):>11} {_fmt(ref):>11} "
88
+ f"{'-':>8} {'info':>6} {src}")
89
+ continue
90
+ n_chk += 1
91
+ _BENCH["checks"] += 1
92
+ if isinstance(ref, tuple):
93
+ lo, hi = ref
94
+ ok = bool(lo <= val <= hi)
95
+ dev_s, tol_s = ("in" if ok else "OUT"), "range"
96
+ ref_s = f"{_fmt(lo)}..{_fmt(hi)}"
97
+ else:
98
+ dev = val / ref - 1.0
99
+ ok = bool(abs(dev) < tol)
100
+ dev_s = f"{dev * 100:+.2f}"
101
+ tol_s = f"{tol * 100:g}%"
102
+ ref_s = _fmt(ref)
103
+ if ok:
104
+ n_pass += 1
105
+ else:
106
+ _BENCH["failed"].append((title, qty))
107
+ print(f" {qty:<36} {_fmt(val):>11} {ref_s:>11} "
108
+ f"{dev_s:>8} {tol_s:>6} {src:<22} "
109
+ f"{'ok' if ok else '** FAIL **'}")
110
+ for note in notes:
111
+ print(f" . {note}")
112
+ if n_chk:
113
+ verdict = "PASSED" if n_pass == n_chk else "** FAILED **"
114
+ print(f" -> {verdict} ({n_pass}/{n_chk} checks within tolerance)")
115
+
116
+ def _bench_summary():
117
+ """Global verdict over all blocks; non-zero exit on any failure."""
118
+ print("\n" + "=" * 96)
119
+ if not _BENCH["failed"]:
120
+ print(f"ALL TESTS PASSED - {_BENCH['checks']} checks "
121
+ f"in {_BENCH['blocks']} blocks")
122
+ else:
123
+ print(f"{len(_BENCH['failed'])} CHECK(S) FAILED "
124
+ f"out of {_BENCH['checks']}:")
125
+ for blk, qty in _BENCH["failed"]:
126
+ print(f" - [{blk}] {qty}")
127
+ raise SystemExit(1)
128
+
129
+ # ────────────────────────────────────────────────────────────────────
130
+ # Shared ITER Q=10 reference case - single source of truth for the
131
+ # chain blocks below.
132
+ #
133
+ # ITER : deck inputs, mirroring D0FUS_INPUTS/1_run_ITER.txt
134
+ # (benchmark conventions: peak TF field referenced at the
135
+ # winding-pack front face, published inboard stack
136
+ # b = 1.10 m, Tbar solved by resolve_Tbar for the Greenwald
137
+ # fraction f_GW = 0.85). The dict is progressively enriched
138
+ # with the chain results (V, nbar, pbar, P_rad, tau_E, Ip...)
139
+ # so that every block consumes the outputs of the previous
140
+ # ones, exactly like the production solver.
141
+ # FROZEN : converged outputs of that deck (frozen 2026-06),
142
+ # re-asserted by the final full-deck regression block. Chain
143
+ # blocks read FROZEN only (i) to check consistency, and
144
+ # (ii) as forward references where the file order places a
145
+ # function after its first use (q95 for the bootstrap and
146
+ # SOL blocks, I_Ohm for the ohmic-power block, f_alpha and
147
+ # f_imp_dil for the density block); each forward reference
148
+ # is then independently re-derived and closed by the chain
149
+ # block that follows the corresponding definition.
150
+ #
151
+ # Published anchors: Shimada et al., Nucl. Fusion 47 (2007) S1
152
+ # (machine values) and Kim et al., Nucl. Fusion 58 (2018) 056013
153
+ # (flat-top heating mix and profiles), as documented in the deck.
154
+ # ────────────────────────────────────────────────────────────────────
155
+ ITER = dict(
156
+ # Geometry and field (deck section 1)
157
+ R0=6.2, a=2.0, b=1.10, Bmax_TF=10.60,
158
+ # Power and operation
159
+ P_fus=500.0,
160
+ P_NBI=33.0, P_ECRH=6.7, P_ICRH=10.0, P_LH=0.0, # flat-top mix [Kim 2018]
161
+ P_aux=33.0 + 6.7 + 10.0, # = 49.7 MW
162
+ Tbar=7.754, # solved by resolve_Tbar for f_GW_target = 0.85
163
+ H=1.0, M=2.5,
164
+ # Profiles (deck section 2c)
165
+ nu_n=0.01, nu_T=2.80,
166
+ rho_ped=0.95, n_ped_frac=0.99, T_ped_frac=0.55,
167
+ # Composition and radiation (deck section 4)
168
+ Zeff=1.65, imp={'W': 2e-5, 'Ne': 7e-3}, r_synch=0.6,
169
+ rho_rad_core=0.75, C_Alpha=5.0,
170
+ # Current-drive deposition (deck section 11)
171
+ A_beam=2, E_beam_keV=1000.0, rho_NBI=0.30, rho_EC=0.40,
172
+ angle_NBI_deg=20.0,
173
+ )
174
+
175
+ FROZEN = dict(
176
+ kappa=1.8792, kappa95=1.67785, delta=0.527517, delta95=0.351678,
177
+ V=847.587, S=687.331,
178
+ f_alpha=0.0297618, f_imp_dil=0.07103,
179
+ nbar=0.98561, nbar_line=1.01245, pbar=0.267547, nG=1.19112,
180
+ B0=5.300, W_th=340.154, betaT=0.023938, betaP=0.651514, betaN=1.63515,
181
+ P_Ohm=0.3910, I_Ohm=8.52244,
182
+ P_Brem=13.3005, P_syn=3.65678, P_line_core=24.9375, P_line=44.8641,
183
+ tauE=3.14385, Ip=14.9681, q95=3.59843, Ib=4.81601,
184
+ eta_LH=0.310283, eta_EC=0.0463759, eta_NBI=0.292349, I_CD=1.62962,
185
+ Q=9.98184, P_sep=87.8792, P_LH_th=73.522,
186
+ B_pol=0.715156, lambda_q_mm=1.12391, Gamma_n=0.58196,
187
+ tau_alpha=15.7193,
188
+ )
189
+
30
190
  #%% Geometry formulas
31
191
 
32
192
  # Explicit scipy import for the three-point shaping profiles below.
@@ -657,13 +817,6 @@ if __name__ == "__main__":
657
817
  import D0FUS_BIB.D0FUS_figures as figs
658
818
  figs.plot_volume_comparison(R0=6.2, a=2.0)
659
819
 
660
- if __name__ == "__main__":
661
- # ITER plasma volume anchor — 831 m³ (Shimada et al., NF 47 (2007) S1).
662
- # Analytic shaped-torus vs true separatrix: 6 % tolerance.
663
- _V = f_plasma_volume(6.2, 2.0, 1.85, 0.485)
664
- assert abs(_V/831. - 1) < 0.06, _V
665
- print(f"OK Volume ITER: {_V:.0f} m³ vs 831 publié")
666
-
667
820
  def f_first_wall_surface(R0, a, kappa_edge, delta_edge=0.0,
668
821
  geometry_model='Academic', N_theta=2000):
669
822
  """
@@ -716,6 +869,47 @@ if __name__ == "__main__":
716
869
  import D0FUS_BIB.D0FUS_figures as figs
717
870
  figs.plot_first_wall_surface(R0=6.2, a=2.0)
718
871
 
872
+ if __name__ == "__main__":
873
+ # ── ITER chain (1/12) - shaping and geometry ─────────────────────────
874
+ # Deck options: Option_Kappa = 'Wenninger' (shaping derived from the
875
+ # aspect ratio, NOT imposed), refined Miller geometry on the
876
+ # production grid (N_rho = 500, N_theta = 200).
877
+ # Published anchor: V = 831 m3 inside the true separatrix (Shimada
878
+ # 2007, Table 2); the analytic shaped torus carries a known +1.5 %
879
+ # bias at the published shaping (kappa = 1.85, delta = 0.485),
880
+ # tolerance 6 %.
881
+ _k = f_Kappa(ITER['R0'] / ITER['a'], 'Wenninger', 1.85, 0.3)
882
+ _k95 = f_Kappa_95(_k)
883
+ _d = f_Delta(_k)
884
+ _d95 = f_Delta_95(_d)
885
+ ITER_Vpd = precompute_Vprime(ITER['R0'], ITER['a'], _k, _d,
886
+ geometry_model='refined',
887
+ kappa_95=_k95, delta_95=_d95,
888
+ N_rho=500, N_theta=200)
889
+ _V = f_plasma_volume(ITER['R0'], ITER['a'], _k, _d, Vprime_data=ITER_Vpd)
890
+ _S = f_first_wall_surface(ITER['R0'], ITER['a'], _k, _d,
891
+ geometry_model='refined')
892
+ _V_pub = f_plasma_volume(6.2, 2.0, 1.85, 0.485) # analytic, published shaping
893
+ _kappa_a = _V / (2.0 * np.pi**2 * ITER['R0'] * ITER['a']**2)
894
+ ITER.update(kappa=_k, kappa95=_k95, delta=_d, delta95=_d95,
895
+ V=_V, S=_S, kappa_a=_kappa_a)
896
+ _bench("ITER chain 1/12 - shaping and geometry", [
897
+ ("kappa_sep (Wenninger)", _k, FROZEN['kappa'], 1e-3, "deck frozen"),
898
+ ("kappa_95", _k95, FROZEN['kappa95'], 1e-3, "deck frozen"),
899
+ ("delta_sep", _d, FROZEN['delta'], 1e-3, "deck frozen"),
900
+ ("delta_95", _d95, FROZEN['delta95'], 1e-3, "deck frozen"),
901
+ ("V analytic, published shaping [m3]", _V_pub, 831.0, 0.06, "Shimada 2007"),
902
+ ("V Miller, deck shaping [m3]", _V, FROZEN['V'], 5e-3, "deck frozen"),
903
+ ("V Miller, deck shaping [m3]", _V, 831.0, 0.06, "Shimada 2007"),
904
+ ("S first wall, Miller [m2]", _S, FROZEN['S'], 5e-3, "deck frozen"),
905
+ ("kappa_area = V/(2 pi^2 R0 a^2)", _kappa_a, None, None, "IPB98 convention"),
906
+ ], notes=[
907
+ "Published separatrix shaping: kappa = 1.85, delta = 0.485 "
908
+ "(Shimada 2007, Table 2); the Wenninger scaling at A = 3.1 "
909
+ "returns kappa = 1.879, delta = 0.528.",
910
+ ])
911
+
912
+
719
913
  #%% n, T, p and Pfus
720
914
 
721
915
  """
@@ -1129,14 +1323,20 @@ def f_sigmav(T):
1129
1323
 
1130
1324
 
1131
1325
  if __name__ == "__main__":
1132
- # DT reactivity anchors Bosch & Hale, NF 32 (1992) 611, Table VII fit.
1133
- # Coefficients cross-checked against cfspopcon 8.0.0 (machine precision);
1134
- # values consistent with Table VIII (1.136e-16 cm³/s at 10 keV).
1135
- for _T, _sv in {1.0: 6.8569e-27, 10.0: 1.1362e-22, 20.0: 4.3302e-22}.items():
1136
- assert abs(f_sigmav(_T)/_sv - 1) < 5e-4, (_T, f_sigmav(_T))
1326
+ # ── Published anchors - D-T reactivity (Bosch & Hale 1992) ──────────
1327
+ # Bosch & Hale, NF 32 (1992) 611, Table VII fit. Coefficients
1328
+ # cross-checked against cfspopcon 8.0.0 (machine precision); values
1329
+ # consistent with Table VIII (1.136e-16 cm3/s at 10 keV). The peak of
1330
+ # the reactivity sits near 64 keV (Wesson, Tokamaks).
1331
+ _sv_rows = [(f"<sigma v> at {_T:g} keV [m3/s]", f_sigmav(_T), _sv, 5e-4,
1332
+ "B&H 1992 Tab. VIII")
1333
+ for _T, _sv in ((1.0, 6.8569e-27), (10.0, 1.1362e-22),
1334
+ (20.0, 4.3302e-22))]
1137
1335
  _Tg = np.linspace(20., 150., 600)
1138
- assert 55 < _Tg[np.argmax(f_sigmav(_Tg))] < 75 # peak 64 keV (Wesson)
1139
- print("OK Bosch-Hale ⟨σv⟩: 3 ancres Table VIII + pic ≈ 64 keV")
1336
+ _sv_rows.append(("peak position [keV]",
1337
+ float(_Tg[np.argmax(f_sigmav(_Tg))]),
1338
+ (55.0, 75.0), 1, "Wesson 2004"))
1339
+ _bench("Published anchors - D-T reactivity (Bosch-Hale)", _sv_rows)
1140
1340
 
1141
1341
  # =============================================================================
1142
1342
  # Required electron density for a target fusion power
@@ -1564,75 +1764,68 @@ def f_density_limit(model, Ip, a, P_sol=None, P_tot=None, R0=None, kappa=None,
1564
1764
 
1565
1765
 
1566
1766
  if __name__ == "__main__":
1567
- # Density-limit anchors.
1568
- # Greenwald, PPCF 44 (2002) R27: n_GW = Ip/(πa²), exact for ITER 15 MA.
1569
- assert abs(f_nG(15., 2.) - 15/(4*np.pi)) < 1e-12
1570
- # Giacomin et al., PRL 128 (2022) 185003, Eq. 12 the paper's own
1571
- # machine predictions (A = 2): ITER 2.5e20, SPARC 8.7e20 (rounding tol).
1572
- assert abs(f_n_limit_giacomin(50., 6.2, 2., 1.8, 5.3, 3.) - 2.5) < 0.15
1573
- assert abs(f_n_limit_giacomin(28., 1.85, .57, 2., 12.2, 3.) - 8.7) < 0.5
1574
- # Zanca et al., NF 59 (2019) 126011, Eq. 20 (as in Manz NF 63 (2023)
1575
- # 076026, Eq. 7) algebraic identity at an ITER-like point.
1576
- _zref = (0.4*1.7**(4/9)*(0.5+1.7-1)**(-5/9)*(50/15)**(4/9)
1577
- * f_nG(15., 2.)**(8/9))
1578
- assert abs(f_n_limit_zanca(50., 15., 2., Z_eff=1.7) - _zref) < 1e-12
1579
- print("OK Limites de densité: Greenwald exact, Giacomin 2.5/8.7, Zanca identité")
1767
+ # ── Published anchors - operational density limits ───────────────────
1768
+ # Greenwald, PPCF 44 (2002) R27: n_GW = Ip/(pi a^2), exact identity at
1769
+ # the ITER 15 MA point. Giacomin et al., PRL 128 (2022) 185003,
1770
+ # Eq. 12: the paper's own machine predictions (A = 2) are 2.5e20 for
1771
+ # ITER and 8.7e20 for SPARC (rounding of the quoted inputs). Zanca et
1772
+ # al., NF 59 (2019) 126011, Eq. 20 (as in Manz, NF 63 (2023) 076026,
1773
+ # Eq. 7): algebraic identity at an ITER-like point.
1774
+ _zref = (0.4 * 1.7**(4 / 9) * (0.5 + 1.7 - 1)**(-5 / 9) * (50 / 15)**(4 / 9)
1775
+ * f_nG(15., 2.)**(8 / 9))
1776
+ _bench("Published anchors - density limits", [
1777
+ ("n_GW ITER identity [1e20 m-3]", f_nG(15., 2.), 15 / (4 * np.pi),
1778
+ 1e-12, "Greenwald 2002"),
1779
+ ("n_lim Giacomin ITER [1e20 m-3]",
1780
+ f_n_limit_giacomin(50., 6.2, 2., 1.8, 5.3, 3.), 2.5, 0.06,
1781
+ "Giacomin 2022"),
1782
+ ("n_lim Giacomin SPARC [1e20 m-3]",
1783
+ f_n_limit_giacomin(28., 1.85, .57, 2., 12.2, 3.), 8.7, 0.06,
1784
+ "Giacomin 2022"),
1785
+ ("n_lim Zanca identity [1e20 m-3]",
1786
+ f_n_limit_zanca(50., 15., 2., Z_eff=1.7), _zref, 1e-9,
1787
+ "Zanca 2019"),
1788
+ ])
1580
1789
 
1581
1790
  if __name__ == "__main__":
1582
-
1583
- # ITER geometry and plasma parameters Shimada et al., Nucl. Fusion 47 (2007) S1
1584
- R0_IT = 6.2; a_IT = 2.0
1585
- kap_IT = 1.85; del_IT = 0.50; Ip_IT = 15.0 # MA
1586
- Pfus = 500.0 # MW
1587
- Tbar = 8.9 # keV
1588
- falpha = 0.06 # He-ash fraction ITER Physics Basis (1999), Sec. 2.4
1589
-
1590
- # H-mode pedestal profile parameters
1591
- # Density: nearly flat core, high pedestal — Coleman et al., Nucl. Fusion 65 (2025) 036039
1592
- # Temperature: moderate peaking — ITER Physics Basis (1999), Sec. 2.3
1593
- nu_n = 0.1 # Density peaking exponent [-]
1594
- nu_T = 1.0 # Temperature peaking exponent [-]
1595
- rho_ped = 0.94 # Normalised pedestal radius [-]
1596
- n_ped_frac = 0.90 # n_ped / n_vol [-]
1597
- T_ped_frac = 0.40 # T_ped / T_vol [-]
1598
-
1599
- # Academic mode
1600
- n_ac = f_nbar(Pfus, nu_n, nu_T, falpha, Tbar, R0_IT, a_IT, kap_IT,
1601
- rho_ped=rho_ped, n_ped_frac=n_ped_frac, T_ped_frac=T_ped_frac)
1602
- p_ac = f_pbar(nu_n, nu_T, n_ac, Tbar,
1603
- rho_ped=rho_ped, n_ped_frac=n_ped_frac, T_ped_frac=T_ped_frac)
1604
- nG = f_nG(Ip_IT, a_IT)
1605
- V_ac = 2.0 * np.pi**2 * R0_IT * kap_IT * a_IT**2
1606
-
1607
- # refined (Miller) mode geometry functions defined in §1 above
1608
- k95 = f_Kappa_95(kap_IT)
1609
- d95 = f_Delta_95(del_IT)
1610
- Vpd = precompute_Vprime(R0_IT, a_IT, kap_IT, del_IT,
1611
- geometry_model='refined',
1612
- kappa_95=k95, delta_95=d95)
1613
- V_d0 = Vpd[2]
1614
- n_d0 = f_nbar(Pfus, nu_n, nu_T, falpha, Tbar, R0_IT, a_IT, kap_IT,
1615
- rho_ped=rho_ped, n_ped_frac=n_ped_frac, T_ped_frac=T_ped_frac,
1616
- Vprime_data=Vpd)
1617
- p_d0 = f_pbar(nu_n, nu_T, n_d0, Tbar,
1618
- rho_ped=rho_ped, n_ped_frac=n_ped_frac, T_ped_frac=T_ped_frac,
1619
- Vprime_data=Vpd)
1620
-
1621
- # Console summary
1622
- print("\n── ITER reference: Academic vs refined — H-mode pedestal profile ──")
1623
- print(f" Profile: nu_n={nu_n}, nu_T={nu_T}, rho_ped={rho_ped}, "
1624
- f"n_ped={n_ped_frac}, T_ped={T_ped_frac}")
1625
- print(f" {'Quantity':<30} {'Academic':>10} {'refined':>10} {'ITER ref.':>10}")
1626
- print(" " + "─"*62)
1627
- print(f" {'V [m3]':<30} {V_ac:>10.1f} {V_d0:>10.1f} {'830':>10}")
1628
- print(f" {'n_e [1e20 m-3]':<30} {n_ac:>10.3f} {n_d0:>10.3f} {'1.01':>10}")
1629
- print(f" {'p_bar [MPa]':<30} {p_ac:>10.3f} {p_d0:>10.3f} {'0.28':>10}")
1630
- print(f" {'n_G [1e20 m-3]':<30} {nG:>10.3f} {nG:>10.3f} {'1.19':>10}")
1631
- print(f" {'f_n = n_e/n_G [-]':<30} {n_ac/nG:>10.3f} {n_d0/nG:>10.3f} {'0.85':>10}")
1791
+ # ── ITER chain (2/12) - operating point: density and pressure ───────
1792
+ # The deck specifies the operating point through the Greenwald
1793
+ # fraction (Tbar_mode = greenwald, f_GW_target = 0.85): resolve_Tbar
1794
+ # solves Tbar = 7.754 keV, used here as the chain input. Two forward
1795
+ # references are taken from FROZEN and closed downstream:
1796
+ # - f_alpha (helium ash fraction), closed by chain 12;
1797
+ # - f_imp_dil = sum_j <Z_j> c_j (fuel dilution by the W + Ne mix),
1798
+ # closed by chain 4 where get_Z_mean is defined.
1799
+ _n = f_nbar(ITER['P_fus'], ITER['nu_n'], ITER['nu_T'], FROZEN['f_alpha'],
1800
+ ITER['Tbar'], ITER['R0'], ITER['a'], ITER['kappa'],
1801
+ rho_ped=ITER['rho_ped'], n_ped_frac=ITER['n_ped_frac'],
1802
+ T_ped_frac=ITER['T_ped_frac'],
1803
+ Vprime_data=ITER_Vpd, f_imp=FROZEN['f_imp_dil'], tau_i_e=1.0)
1804
+ _p = f_pbar(ITER['nu_n'], ITER['nu_T'], _n, ITER['Tbar'],
1805
+ rho_ped=ITER['rho_ped'], n_ped_frac=ITER['n_ped_frac'],
1806
+ T_ped_frac=ITER['T_ped_frac'],
1807
+ Vprime_data=ITER_Vpd, tau_i_e=1.0)
1808
+ _nl = f_nbar_line(_n, ITER['nu_n'], ITER['rho_ped'], ITER['n_ped_frac'])
1809
+ _nl_flat = f_nbar_line(1.0, 0.0) # analytic identity for a flat profile
1810
+ ITER.update(nbar=_n, pbar=_p, nbar_line=_nl)
1811
+ _bench("ITER chain 2/12 - operating point (density, pressure)", [
1812
+ ("nbar volume-avg [1e20 m-3]", _n, FROZEN['nbar'], 1e-3, "deck frozen"),
1813
+ ("nbar line-avg [1e20 m-3]", _nl, FROZEN['nbar_line'], 1e-3,
1814
+ "deck frozen"),
1815
+ ("nbar line-avg [1e20 m-3]", _nl, 1.01, 0.01, "Shimada 2007"),
1816
+ ("pbar [MPa]", _p, FROZEN['pbar'], 1e-3, "deck frozen"),
1817
+ ("flat-profile identity n_line/n_vol", _nl_flat, 1.0, 1e-9,
1818
+ "analytic"),
1819
+ ("f_GW at frozen Ip", _nl / f_nG(FROZEN['Ip'], ITER['a']), 0.85,
1820
+ 5e-3, "deck target"),
1821
+ ], notes=[
1822
+ "f_GW is re-closed in chain 11 with the chain Ip from the "
1823
+ "scaling-law inversion.",
1824
+ ])
1632
1825
 
1633
1826
  #%% B and beta
1634
1827
 
1635
- def f_B0(Bmax, a, b, R0):
1828
+ def f_B0(Bmax, a, b, R0, b_cover=0.0):
1636
1829
  """
1637
1830
  Estimate the on-axis toroidal magnetic field B0 from the peak field at the
1638
1831
  TF coil inboard midplane, using the 1/R dependence of a toroidal solenoid.
@@ -1665,7 +1858,7 @@ def f_B0(Bmax, a, b, R0):
1665
1858
  Wesson, Tokamaks, 4th ed. (2011), §3.2.
1666
1859
  Freidberg, Plasma Physics and Fusion Energy (2007), §12.3.
1667
1860
  """
1668
- B0 = Bmax * (1.0 - (a + b) / R0)
1861
+ B0 = Bmax * (1.0 - (a + b + b_cover) / R0)
1669
1862
  return B0
1670
1863
 
1671
1864
  def f_Bpol(q95, B_tor, a, R0, kappa=1.0):
@@ -2009,40 +2202,43 @@ def f_beta_fast_alpha(P_alpha_MW, Te_keV, ne_20, B0, V_m3, Z_eff=1.65,
2009
2202
 
2010
2203
 
2011
2204
  if __name__ == "__main__":
2012
-
2013
- # ------------------------------------------------------------------
2014
- # ITER Q=10 reference Shimada et al., Nucl. Fusion 47 (2007) S1
2015
- # Beta functions only; p_bar taken as given design values.
2016
- # ------------------------------------------------------------------
2017
- R0_IT = 6.2; a_IT = 2.0; kap_IT = 1.85; Ip_IT = 15.0 # MA
2018
- Bmax = 11.8 # Peak TF inboard field [T]
2019
- B0_IT = 5.3 # On-axis field [T] Shimada 2007, Table 1
2020
- b_IT = R0_IT * (1.0 - B0_IT / Bmax) - a_IT # Inboard radial build [m]
2021
-
2022
- # Pressure inputs from §2 ITER console block (H-mode pedestal profile)
2023
- p_ac = 0.260 # Academic mode [MPa]
2024
- p_d0 = 0.262 # refined mode [MPa]
2025
-
2026
- B0 = f_B0(Bmax, a_IT, b_IT, R0_IT)
2027
- res = {}
2028
- for label, p in [('Academic', p_ac), ('refined', p_d0)]:
2029
- bT = f_beta_T(p, B0)
2030
- bP = f_beta_P(a_IT, kap_IT, p, Ip_IT)
2031
- b = f_beta(bT, bP)
2032
- bN = f_beta_N(b, a_IT, B0, Ip_IT)
2033
- res[label] = (bT, bP, b, bN)
2034
-
2035
- print(f"\n── ITER β (B0={B0:.2f} T, b_inboard={b_IT:.3f} m) ────────────────────")
2036
- print(f" {'Quantity':<24} {'Academic':>10} {'refined':>10} {'ITER ref.':>10}")
2037
- print(" " + ""*56)
2038
- print(f" {'beta_T [%]':<24} {res['Academic'][0]*100:>10.3f} {res['refined'][0]*100:>10.3f} {'2.50':>10}")
2039
- print(f" {'beta_P [-]':<24} {res['Academic'][1]:>10.3f} {res['refined'][1]:>10.3f} {'0.65':>10}")
2040
- print(f" {'beta [%]':<24} {res['Academic'][2]*100:>10.3f} {res['refined'][2]*100:>10.3f} {'2.42':>10}")
2041
- print(f" {'beta_N [% m T/MA]':<24} {res['Academic'][3]:>10.3f} {res['refined'][3]:>10.3f} {'1.77':>10}")
2042
- print(f" {'Troyon limit (< 2.8)':<24} "
2043
- f"{'OK' if res['Academic'][3]<2.8 else 'WARN':>10} "
2044
- f"{'OK' if res['refined'][3]<2.8 else 'WARN':>10}")
2045
-
2205
+ # ── ITER chain (3/12) - on-axis field, stored energy and beta ───────
2206
+ # Field convention of the deck: Bmax_TF = 10.60 T is the peak field at
2207
+ # the winding-pack FRONT FACE, at radius R0 - a - b with the published
2208
+ # inboard stack b = 1.10 m. ITER quotes 11.8 T on the conductor,
2209
+ # deeper inside the winding pack; both describe the same machine and
2210
+ # the front-face convention reproduces B0 = 5.30 T exactly.
2211
+ # W_th = (3/2) pbar V is evaluated inline (definition of f_W_th,
2212
+ # re-checked through f_W_th in chain 11). Ip is a forward reference
2213
+ # (FROZEN), closed by the scaling-law inversion of chain 11.
2214
+ _B0 = f_B0(ITER['Bmax_TF'], ITER['a'], ITER['b'], ITER['R0'])
2215
+ _W = 1.5 * ITER['pbar'] * 1e6 * ITER['V'] / 1e6 # [MJ]
2216
+ _bT = f_beta_T(ITER['pbar'], _B0)
2217
+ _bP = f_beta_P(ITER['a'], ITER['kappa'], ITER['pbar'], FROZEN['Ip'])
2218
+ _bN = f_beta_N(f_beta(_bT, _bP), ITER['a'], _B0, FROZEN['Ip'])
2219
+ # Fast-alpha pressure (Stix slowing-down), to be added to the thermal
2220
+ # beta for MHD-limit comparisons. P_alpha = P_fus/5 = f_P_alpha
2221
+ # (defined in the next section, re-checked in chain 7).
2222
+ _bfa, _tau_se, _W_fast = f_beta_fast_alpha(
2223
+ ITER['P_fus'] / 5.0, ITER['Tbar'], ITER['nbar'], _B0, ITER['V'],
2224
+ Z_eff=ITER['Zeff'])
2225
+ ITER.update(B0=_B0, W_th=_W, betaN=_bN)
2226
+ _bench("ITER chain 3/12 - field, stored energy and beta", [
2227
+ ("B0 on axis [T]", _B0, 5.30, 1e-3, "Shimada 2007"),
2228
+ ("W_th [MJ]", _W, FROZEN['W_th'], 1e-3, "deck frozen"),
2229
+ ("W_th [MJ]", _W, 350.0, 0.05, "Shimada 2007"),
2230
+ ("beta_T [-]", _bT, FROZEN['betaT'], 2e-3, "deck frozen"),
2231
+ ("beta_P [-]", _bP, FROZEN['betaP'], 2e-3, "deck frozen"),
2232
+ ("beta_N [% m T/MA]", _bN, FROZEN['betaN'], 2e-3, "deck frozen"),
2233
+ ("beta_N [% m T/MA]", _bN, "1.8", None, "Shimada 2007"),
2234
+ ("Troyon margin beta_N", _bN, (0.0, 2.8), 1, "deck limit"),
2235
+ ("beta_fast_alpha / beta_T [-]", _bfa / _bT, None, None,
2236
+ "Stix slowing-down"),
2237
+ ], notes=[
2238
+ "beta_N solves below the published 1.8 because the deck-solved "
2239
+ "temperature (7.75 keV) sits under the 8.9 keV design assumption; "
2240
+ "the deviation follows the stored energy and density directly.",
2241
+ ])
2046
2242
 
2047
2243
  #%% Power definitions
2048
2244
 
@@ -2431,12 +2627,15 @@ def f_P_bremsstrahlung(nbar, Tbar, Z_eff, V, nu_n=0.0, nu_T=0.0,
2431
2627
 
2432
2628
 
2433
2629
  if __name__ == "__main__":
2434
- # Bremsstrahlung constant Wesson, Tokamaks, Sec. 4.25 (5.35e-37 SI;
2435
- # same in the NRL Formulary). Flat unit plasma (n = 1e20 m⁻³, T = 1 keV,
2436
- # Z_eff = 1, V = 1 m³) 5.35e3 W = 5.35e-3 MW.
2437
- assert abs(f_P_bremsstrahlung(1., 1., 1., 1., nu_n=0., nu_T=0.)
2438
- / 5.35e-3 - 1) < 0.02
2439
- print("OK Bremsstrahlung: constante Wesson 5.35e-37 (plasma plat unité)")
2630
+ # ── Published anchor - bremsstrahlung constant ───────────────────────
2631
+ # Wesson, Tokamaks, Sec. 4.25 (5.35e-37 W m3 in SI; same constant in
2632
+ # the NRL Formulary). Flat unit plasma (n = 1e20 m-3, T = 1 keV,
2633
+ # Z_eff = 1, V = 1 m3) radiates 5.35e3 W = 5.35e-3 MW.
2634
+ _bench("Published anchor - bremsstrahlung constant (Wesson)", [
2635
+ ("P_brem flat unit plasma [MW]",
2636
+ f_P_bremsstrahlung(1., 1., 1., 1., nu_n=0., nu_T=0.),
2637
+ 5.35e-3, 0.02, "Wesson 2004"),
2638
+ ])
2440
2639
 
2441
2640
  def f_P_line_radiation(nbar, f_imp, L_z, V):
2442
2641
  """
@@ -3092,29 +3291,62 @@ if __name__ == "__main__":
3092
3291
  figs.plot_Lz_cooling()
3093
3292
 
3094
3293
  if __name__ == "__main__":
3095
- # ITER Q=10 radiation budget — Shimada et al., NF 47 (2007) S1
3096
- R0, a, kap, B0, nbar, Z_eff, r_synch = 6.2, 2.0, 1.85, 5.3, 1.01, 1.6, 0.4
3097
- V = 2.0 * np.pi**2 * R0 * kap * a**2
3098
- T = 8.9 # keV
3099
-
3100
- P_s = f_P_synchrotron(T, R0, a, B0, nbar, kap, nu_n=0.1, nu_T=1.0, r_synch=r_synch)
3101
- P_b = f_P_bremsstrahlung(nbar, T, Z_eff, V)
3102
- P_lW = f_P_line_radiation(nbar, 1e-5, get_Lz('W', T), V)
3103
- P_lAr = f_P_line_radiation(nbar, 1e-3, get_Lz('Ar', T), V)
3104
- P_lNe = f_P_line_radiation(nbar, 3e-3, get_Lz('Ne', T), V)
3105
- P_tot = P_s + P_b + P_lW
3106
-
3107
- print(f"\n── ITER radiation budget (academic, parabolic) {'─'*30}")
3108
- print(f" {'Channel':<32} {'[MW]':>7} {'Ref.':>10}")
3109
- print(" " + "─" * 53)
3110
- print(f" {'Bremsstrahlung (Z_eff=1.6)':<32} {P_b:>7.1f} {'20–25':>10}")
3111
- print(f" {'Synchrotron (r_synch=0.4)':<32} {P_s:>7.1f} {'3–6':>10}")
3112
- print(f" {'W line (f_W=1e-5)':<32} {P_lW:>7.1f} {'2–4':>10}")
3113
- print(f" {'Ar line (f_Ar=1e-3)':<32} {P_lAr:>7.1f} {'':>10}")
3114
- print(f" {'Ne line (f_Ne=3e-3)':<32} {P_lNe:>7.1f} {'—':>10}")
3115
- print(" " + "─" * 53)
3116
- print(f" {'Total (brem+sync+W)':<32} {P_tot:>7.1f} {'~30':>10}")
3117
-
3294
+ # ── ITER chain (4/12) - radiation budget ─────────────────────────────
3295
+ # Deck composition: W (2e-5) + Ne (7e-3) with the Zeff = 1.65
3296
+ # override, wall reflectivity r_synch = 0.6, profile-integrated
3297
+ # radiation with the core/edge split at rho_rad_core = 0.75 and
3298
+ # coreradiationfraction = 1 (deck defaults).
3299
+ # Convention: the bremsstrahlung term uses the FUEL effective charge
3300
+ # Z_eff,fuel = 1 + 2 f_alpha - f_imp_dil (D, T, He only); the
3301
+ # impurity contribution (line + recombination + impurity
3302
+ # bremsstrahlung) is carried by the Mavrin cooling rates inside
3303
+ # P_line. Term-by-term comparisons with published decompositions are
3304
+ # therefore convention-dependent; the underlying pieces are anchored
3305
+ # separately (Wesson constant above, Mavrin/TORAX block below).
3306
+ # This block also closes the f_imp_dil forward reference of chain 2.
3307
+ _fdil = sum(get_Z_mean(s, ITER['Tbar']) * c for s, c in ITER['imp'].items())
3308
+ _zimp2 = sum(get_Z_mean(s, ITER['Tbar'])**2 * c
3309
+ for s, c in ITER['imp'].items())
3310
+ _zf = 1.0 + 2.0 * FROZEN['f_alpha'] - _fdil
3311
+ _Pb = f_P_bremsstrahlung(ITER['nbar'], ITER['Tbar'], _zf, ITER['V'],
3312
+ ITER['nu_n'], ITER['nu_T'],
3313
+ rho_ped=ITER['rho_ped'],
3314
+ n_ped_frac=ITER['n_ped_frac'],
3315
+ T_ped_frac=ITER['T_ped_frac'],
3316
+ Vprime_data=ITER_Vpd)
3317
+ _Ps = f_P_synchrotron(ITER['Tbar'], ITER['R0'], ITER['a'], ITER['B0'],
3318
+ ITER['nbar'], ITER['kappa'], ITER['nu_n'],
3319
+ ITER['nu_T'], ITER['r_synch'],
3320
+ rho_ped=ITER['rho_ped'],
3321
+ n_ped_frac=ITER['n_ped_frac'],
3322
+ T_ped_frac=ITER['T_ped_frac'],
3323
+ Vprime_data=ITER_Vpd)
3324
+ _Plc, _Plt = 0.0, 0.0
3325
+ for _sp, _c in ITER['imp'].items():
3326
+ _pc, _pt = f_P_line_radiation_profile(
3327
+ _sp, _c, ITER['nbar'], ITER['Tbar'], ITER['nu_n'], ITER['nu_T'],
3328
+ ITER['V'], rho_ped=ITER['rho_ped'],
3329
+ n_ped_frac=ITER['n_ped_frac'], T_ped_frac=ITER['T_ped_frac'],
3330
+ Vprime_data=ITER_Vpd, rho_core=ITER['rho_rad_core'], N=150)
3331
+ _Plc += _pc
3332
+ _Plt += _pt
3333
+ _Prc = _Pb + _Ps + _Plc # coreradiationfraction = 1 (deck default)
3334
+ _Prt = _Pb + _Ps + _Plt
3335
+ ITER.update(P_rad_core=_Prc, P_rad_tot=_Prt)
3336
+ _bench("ITER chain 4/12 - radiation budget (W + Ne, pedestal profiles)", [
3337
+ ("fuel dilution sum <Z_j> c_j [-]", _fdil, FROZEN['f_imp_dil'], 2e-3,
3338
+ "deck frozen"),
3339
+ ("Z_eff,fuel [-]", _zf, None, None, "convention"),
3340
+ ("Z_eff computed (fuel + imp) [-]", _zf + _zimp2, "1.65", None,
3341
+ "deck override"),
3342
+ ("P_brem (fuel) [MW]", _Pb, FROZEN['P_Brem'], 2e-3, "deck frozen"),
3343
+ ("P_synchrotron [MW]", _Ps, FROZEN['P_syn'], 2e-3, "deck frozen"),
3344
+ ("P_line core (rho < 0.75) [MW]", _Plc, FROZEN['P_line_core'], 2e-3,
3345
+ "deck frozen"),
3346
+ ("P_line total [MW]", _Plt, FROZEN['P_line'], 2e-3, "deck frozen"),
3347
+ ("P_rad,core -> tau_E, Ip [MW]", _Prc, None, None, "chain 11"),
3348
+ ("P_rad,total -> P_sep [MW]", _Prt, None, None, "chain 6"),
3349
+ ])
3118
3350
 
3119
3351
  #%% Current drive
3120
3352
  """
@@ -3170,31 +3402,41 @@ Gormezano et al. (ITER PIPB Ch. 6), Nucl. Fusion 47 (2007) S285.
3170
3402
 
3171
3403
 
3172
3404
  if __name__ == "__main__":
3173
- # Atomic-data anchors.
3174
- # Mavrin (2018) Z(T_e) coefficients cross-checked against TORAX
3175
- # (google-deepmind/torax, charge_states.py): Ne fully stripped > 5 keV;
3176
- # W(8.9 keV) = 53.06 (hand evaluation of the interval-3 polynomial).
3177
- assert get_Z_mean('Ne', 8.9) == 10.0
3178
- assert abs(get_Z_mean('W', 8.9) - 53.06) < 0.05
3179
- # Frozen non-coronal SOL Lz tables (OpenADAS/radas, ne·τ = 5e16 m⁻³s,
3180
- # cfspopcon SPARC-PRD convention): peak integrity at freezing time, and
3181
- # the ONE-SIDED invariant Lz_nc ≥ Lz_coronal on the 0.1-2 keV overlap
3182
- # (finite ne·τ keeps line-radiating states alive; N reaches ×50 at
3183
- # 1-2 keV where coronal N is fully stripped — no convergence expected).
3405
+ # ── Published anchors - atomic data (Mavrin, SOL tables, Lengyel) ───
3406
+ # Mavrin (2018) <Z>(T_e): coefficients cross-checked against TORAX
3407
+ # (google-deepmind/torax, charge_states.py); Ne fully stripped above
3408
+ # 5 keV; W(8.9 keV) = 53.06 (hand evaluation of the interval-3
3409
+ # polynomial). Frozen non-coronal SOL L_z tables (OpenADAS/radas,
3410
+ # ne tau = 5e16 m-3 s, cfspopcon SPARC-PRD convention): peak
3411
+ # integrity at freezing time, and the one-sided invariant
3412
+ # Lz_SOL >= Lz_coronal on the 0.1-2 keV overlap (the finite residence
3413
+ # time keeps line-radiating charge states alive). Lengyel: frozen
3414
+ # cfspopcon SPARC-PRD regression point (chain validated to 0.3 % over
3415
+ # five decades of q_par, 2026-06 review).
3416
+ _rows = [
3417
+ ("<Z> Ne at 8.9 keV [-]", get_Z_mean('Ne', 8.9), 10.0, 1e-9,
3418
+ "Mavrin 2018 / TORAX"),
3419
+ ("<Z> W at 8.9 keV [-]", get_Z_mean('W', 8.9), 53.06, 1e-3,
3420
+ "Mavrin 2018 / TORAX"),
3421
+ ]
3184
3422
  for _sp, (_Tp, _Lp) in {'N': (13.2, 5.745e-32), 'Ne': (49.8, 5.642e-32),
3185
3423
  'Ar': (22.6, 2.121e-31)}.items():
3186
- _Tt = np.logspace(0, 3, 600); _lz = get_Lz_SOL(_sp, _Tt)
3187
- _k = int(np.argmax(_lz))
3188
- assert abs(_lz[_k]/_Lp - 1) < 0.05 and 1/1.6 < _Tt[_k]/_Tp < 1.6, _sp
3189
- for _Tk in (0.1, 0.5, 2.0):
3190
- assert get_Lz_SOL(_sp, _Tk*1e3)/get_Lz(_sp, _Tk) > 0.95, (_sp, _Tk)
3191
- # Lengyel frozen cfspopcon SPARC-PRD regression point (chain validated
3192
- # to 0.3 % over five decades of q_par, 2026-06 review): Ar,
3193
- # q = 9.101 GW/m², f_loss = 0.988, T_u = 280.1 eV, T_t = 25 eV,
3194
- # n_u = 2.1e19 m⁻³ → c_z = 0.7569.
3195
- _cz = f_lengyel_concentration(9101., 2.1e19, 280.1, 25.0, 'Ar', 0.988)
3196
- assert abs(_cz/0.7569 - 1) < 0.02, _cz
3197
- print("OK Données atomiques: Mavrin ⟨Z⟩, tables Lz SOL gelées, ancre Lengyel")
3424
+ _Tt = np.logspace(0, 3, 600)
3425
+ _lz = get_Lz_SOL(_sp, _Tt)
3426
+ _kk = int(np.argmax(_lz))
3427
+ _rows.append((f"Lz_SOL {_sp} peak value [W m3]", float(_lz[_kk]),
3428
+ _Lp, 0.05, "frozen table"))
3429
+ _rows.append((f"Lz_SOL {_sp} peak position ratio", float(_Tt[_kk]) / _Tp,
3430
+ (1 / 1.6, 1.6), 1, "frozen table"))
3431
+ _rmin = min(get_Lz_SOL(_sp, _Tk * 1e3) / get_Lz(_sp, _Tk)
3432
+ for _Tk in (0.1, 0.5, 2.0))
3433
+ _rows.append((f"invariant Lz_SOL/Lz_coronal {_sp}", float(_rmin),
3434
+ (0.95, 1e9), 1, "one-sided physics"))
3435
+ _rows.append(("Lengyel c_z (Ar, SPARC point) [-]",
3436
+ f_lengyel_concentration(9101., 2.1e19, 280.1, 25.0, 'Ar',
3437
+ 0.988),
3438
+ 0.7569, 0.02, "cfspopcon ref."))
3439
+ _bench("Published anchors - atomic data and Lengyel chain", _rows)
3198
3440
 
3199
3441
  def _ln_Lambda_CD(Te_keV, ne_20):
3200
3442
  """
@@ -3658,75 +3900,61 @@ def f_etaCD_NBI_physics(A_beam, E_beam_keV, a, R0, Tbar, nbar, Z_eff,
3658
3900
 
3659
3901
 
3660
3902
  if __name__ == "__main__":
3661
- # ── NBCD physics model validation against METIS reference ─────────
3662
- # Expected values from METIS zicd0.m benchmark (test_NBCD_physics.py).
3663
- # Tolerance: 2% (accounts for lnL formula difference NRL vs METIS).
3664
- print("\n── NBCD physics model (Stix/Cordey/Lin-Liu) ─────────────────────────")
3665
- _TOL = 0.02
3666
- _cases = [
3667
- # METIS ref (at local Te/ne from f_Tprof parabolic profile)
3668
- ("ITER 1MeV D", 0.3336, dict(A_beam=2, E_beam_keV=1000, a=2.0, R0=6.2,
3669
- Tbar=8.9, nbar=1.0, Z_eff=1.65, nu_T=1.5, nu_n=0.3,
3670
- rho_NBI=0.3, f_alpha=0.04, angle_NBI_deg=20.0)),
3671
- ("ARC 150keV D", 0.1147, dict(A_beam=2, E_beam_keV=150, a=1.13, R0=3.3,
3672
- Tbar=14.0, nbar=1.8, Z_eff=1.5, nu_T=1.2, nu_n=0.5,
3673
- rho_NBI=0.4, f_alpha=0.06, angle_NBI_deg=25.0)),
3674
- ("EU-DEMO 1MeV", 0.4006, dict(A_beam=2, E_beam_keV=1000, a=2.88, R0=9.07,
3675
- Tbar=12.0, nbar=0.8, Z_eff=1.6, nu_T=1.5, nu_n=0.3,
3676
- rho_NBI=0.3, f_alpha=0.05, angle_NBI_deg=20.0)),
3903
+ # ── Published anchors - NBCD physics model vs METIS ──────────────────
3904
+ # Expected values from the METIS zicd0.m benchmark
3905
+ # (test_NBCD_physics.py). Tolerance 2 % (lnLambda formula difference,
3906
+ # NRL vs METIS). Perpendicular injection must drive zero current.
3907
+ _nbcd_cases = [
3908
+ ("ITER 1 MeV D", 0.3336, dict(A_beam=2, E_beam_keV=1000, a=2.0, R0=6.2,
3909
+ Tbar=8.9, nbar=1.0, Z_eff=1.65, nu_T=1.5,
3910
+ nu_n=0.3, rho_NBI=0.3, f_alpha=0.04,
3911
+ angle_NBI_deg=20.0)),
3912
+ ("ARC 150 keV D", 0.1147, dict(A_beam=2, E_beam_keV=150, a=1.13,
3913
+ R0=3.3, Tbar=14.0, nbar=1.8, Z_eff=1.5,
3914
+ nu_T=1.2, nu_n=0.5, rho_NBI=0.4, f_alpha=0.06,
3915
+ angle_NBI_deg=25.0)),
3916
+ ("EU-DEMO 1 MeV", 0.4006, dict(A_beam=2, E_beam_keV=1000, a=2.88,
3917
+ R0=9.07, Tbar=12.0, nbar=0.8, Z_eff=1.6,
3918
+ nu_T=1.5, nu_n=0.3, rho_NBI=0.3, f_alpha=0.05,
3919
+ angle_NBI_deg=20.0)),
3677
3920
  ]
3678
- print(f" {'Case':<16s} {'D0FUS':>8s} {'METIS':>8s} {'err':>6s} status")
3679
- print(" " + "-" * 55)
3680
- _all_ok = True
3681
- for _name, _metis_ref, _kw in _cases:
3682
- _g = f_etaCD_NBI_physics(**_kw)
3683
- _err = abs(_g / _metis_ref - 1)
3684
- _ok = _err < _TOL
3685
- _all_ok = _all_ok and _ok
3686
- print(f" {_name:<16s} {_g:8.4f} {_metis_ref:8.4f} {_err*100:5.1f}% {'PASS' if _ok else 'FAIL'}")
3687
- # Physics sanity checks
3688
- _g_perp, _ = f_etaCD_NBI_physics(2, 500, 2.0, 6.2, 10.0, 1.0, 1.65,
3689
- 1.5, 0.3, 0.3, angle_NBI_deg=90.0), None
3690
- _ok_perp = abs(_g_perp) < 1e-6
3691
- _all_ok = _all_ok and _ok_perp
3692
- print(f" {'perp. -> 0':<16s} {_g_perp:8.1e} {'0':>8s} {'':>6s} {'PASS' if _ok_perp else 'FAIL'}")
3693
- print(" " + "-" * 55)
3694
- print(f" {'ALL PASS' if _all_ok else 'SOME FAILED'}")
3695
-
3921
+ _rows = [(f"gamma_NBI {_nm}", f_etaCD_NBI_physics(**_kw), _ref, 0.02,
3922
+ "METIS zicd0.m")
3923
+ for _nm, _ref, _kw in _nbcd_cases]
3924
+ _g_perp = f_etaCD_NBI_physics(2, 500, 2.0, 6.2, 10.0, 1.0, 1.65,
3925
+ 1.5, 0.3, 0.3, angle_NBI_deg=90.0)
3926
+ _rows.append(("gamma_NBI perpendicular limit", float(_g_perp),
3927
+ (-1e-6, 1e-6), 1, "physics limit"))
3928
+ _bench("Published anchors - NBCD (Stix/Cordey/Lin-Liu) vs METIS", _rows)
3696
3929
 
3697
3930
  if __name__ == "__main__":
3698
- # ── ECCD physics model validation against METIS reference ───────────
3699
- # Formula is identical to METIS zicd0.m lhmode=5 (Giruzzi 1987 + Lin-Liu).
3700
- # Tolerance: machine epsilon (same formula, no approximation).
3701
- print("\n── ECCD physics model (Giruzzi/Lin-Liu) ──────────────────────────")
3702
- _TOL_EC = 1e-10
3703
-
3704
- # Reference values computed from the METIS formula at local Te
3705
- # (Te_local evaluated from parabolic profile at rho_EC)
3931
+ # ── Published anchors - ECCD physics model vs METIS ──────────────────
3932
+ # The formula is identical to METIS zicd0.m lhmode = 5 (Giruzzi 1987 +
3933
+ # Lin-Liu trapped-particle correction): the reference is re-evaluated
3934
+ # inline at the same local T_e, so the agreement must hold to machine
3935
+ # precision. Physics ordering: HFS > top > LFS > 0.
3706
3936
  _ec_cases = [
3707
- # name, theta_p, expected_gamma (from METIS at same Te_loc)
3708
- # Tbar=8.9, nu_T=1.0 => Te_loc(0.3) = 16.2 keV
3709
- ("ITER HFS 160°", 160.0, dict(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0,
3710
- Z_eff=1.65, nu_T=1.0, nu_n=0.3,
3711
- rho_EC=0.3)),
3712
- ("ITER LFS 0°", 0.0, dict(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0,
3713
- Z_eff=1.65, nu_T=1.0, nu_n=0.3,
3714
- rho_EC=0.3)),
3715
- ("ITER top 90°", 90.0, dict(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0,
3716
- Z_eff=1.65, nu_T=1.0, nu_n=0.3,
3717
- rho_EC=0.3)),
3718
- ("EU-DEMO HFS", 160.0, dict(a=2.88, R0=9.07, Tbar=12.0, nbar=0.8,
3719
- Z_eff=1.6, nu_T=1.5, nu_n=0.3,
3720
- rho_EC=0.3)),
3937
+ ("ITER HFS 160 deg", 160.0, dict(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0,
3938
+ Z_eff=1.65, nu_T=1.0, nu_n=0.3,
3939
+ rho_EC=0.3)),
3940
+ ("ITER LFS 0 deg", 0.0, dict(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0,
3941
+ Z_eff=1.65, nu_T=1.0, nu_n=0.3,
3942
+ rho_EC=0.3)),
3943
+ ("ITER top 90 deg", 90.0, dict(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0,
3944
+ Z_eff=1.65, nu_T=1.0, nu_n=0.3,
3945
+ rho_EC=0.3)),
3946
+ ("EU-DEMO HFS 160 deg", 160.0, dict(a=2.88, R0=9.07, Tbar=12.0,
3947
+ nbar=0.8, Z_eff=1.6, nu_T=1.5,
3948
+ nu_n=0.3, rho_EC=0.3)),
3721
3949
  ]
3722
- print(f" {'Case':<18s} {'γ_D0FUS':>8s} {'θ_p':>5s} status")
3723
- print(" " + "-" * 45)
3724
- _all_ok_ec = True
3725
-
3726
- for _name, _theta, _kw in _ec_cases:
3950
+ _rows = []
3951
+ _g_by_case = {}
3952
+ for _nm, _theta, _kw in _ec_cases:
3727
3953
  _g = f_etaCD_EC_physics(**_kw, theta_EC_pol_deg=_theta)
3728
- # Cross-check: compute METIS reference at same local Te
3729
- _Te_loc = max(float(f_Tprof(_kw['Tbar'], _kw['nu_T'], _kw['rho_EC'])), 0.1)
3954
+ _g_by_case[_nm] = _g
3955
+ # METIS reference evaluated inline at the same local T_e
3956
+ _Te_loc = max(float(f_Tprof(_kw['Tbar'], _kw['nu_T'], _kw['rho_EC'])),
3957
+ 0.1)
3730
3958
  _eps = abs(_kw['rho_EC']) * _kw['a'] / _kw['R0']
3731
3959
  _theta_r = np.radians(_theta)
3732
3960
  _mut = np.sqrt(max(_eps * (1 + np.cos(_theta_r))
@@ -3737,30 +3965,21 @@ if __name__ == "__main__":
3737
3965
  _fc = 1.0 - np.sqrt(2.0 * _eps / (1.0 + _eps))
3738
3966
  _g_metis = (_Te_loc / (_Te_loc + 100.0) * _Gt
3739
3967
  * 6.0 / (1.0 + 4.0 * _fc + _kw['Z_eff']))
3740
- _err = abs(_g / _g_metis - 1.0) if abs(_g_metis) > 1e-15 else abs(_g)
3741
- _ok = _err < _TOL_EC
3742
- _all_ok_ec = _all_ok_ec and _ok
3743
- print(f" {_name:<18s} {_g:8.4f} {_theta:5.0f} {'PASS' if _ok else 'FAIL'}")
3744
-
3745
- # Physics: HFS > top > LFS
3746
- _g_hfs = f_etaCD_EC_physics(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0,
3747
- Z_eff=1.65, nu_T=1.0, nu_n=0.3,
3748
- rho_EC=0.3, theta_EC_pol_deg=180.0)
3749
- _g_top = f_etaCD_EC_physics(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0,
3750
- Z_eff=1.65, nu_T=1.0, nu_n=0.3,
3751
- rho_EC=0.3, theta_EC_pol_deg=90.0)
3752
- _g_lfs = f_etaCD_EC_physics(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0,
3753
- Z_eff=1.65, nu_T=1.0, nu_n=0.3,
3754
- rho_EC=0.3, theta_EC_pol_deg=0.0)
3755
- _ok_order = _g_hfs > _g_top > _g_lfs > 0
3756
- _all_ok_ec = _all_ok_ec and _ok_order
3757
- print(f" {'HFS>top>LFS>0':<18s} {'':>8s} {'':>5s} {'PASS' if _ok_order else 'FAIL'}"
3758
- f" ({_g_hfs:.3f}>{_g_top:.3f}>{_g_lfs:.3f})")
3759
- print(" " + "-" * 45)
3760
- print(f" {'ALL PASS' if _all_ok_ec else 'SOME FAILED'}")
3761
-
3762
-
3763
-
3968
+ _rows.append((f"gamma_EC {_nm}", _g, _g_metis, 1e-9, "METIS zicd0.m"))
3969
+ _g_hfs = f_etaCD_EC_physics(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0, Z_eff=1.65,
3970
+ nu_T=1.0, nu_n=0.3, rho_EC=0.3,
3971
+ theta_EC_pol_deg=180.0)
3972
+ _g_top = f_etaCD_EC_physics(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0, Z_eff=1.65,
3973
+ nu_T=1.0, nu_n=0.3, rho_EC=0.3,
3974
+ theta_EC_pol_deg=90.0)
3975
+ _g_lfs = f_etaCD_EC_physics(a=2.0, R0=6.2, Tbar=8.9, nbar=1.0, Z_eff=1.65,
3976
+ nu_T=1.0, nu_n=0.3, rho_EC=0.3,
3977
+ theta_EC_pol_deg=0.0)
3978
+ _rows.append(("ordering gamma_HFS/gamma_top", _g_hfs / _g_top,
3979
+ (1.0 + 1e-9, 100.0), 1, "trapping physics"))
3980
+ _rows.append(("ordering gamma_top/gamma_LFS", _g_top / _g_lfs,
3981
+ (1.0 + 1e-9, 100.0), 1, "trapping physics"))
3982
+ _bench("Published anchors - ECCD (Giruzzi/Lin-Liu) vs METIS", _rows)
3764
3983
 
3765
3984
  def f_etaCD_effective(config, a, R0, B0, nbar, Tbar, nu_n, nu_T, Z_eff,
3766
3985
  rho_ped=1.0, n_ped_frac=0.0, T_ped_frac=0.0):
@@ -4055,56 +4274,71 @@ def f_PLH(eta_RF, f_RP, P_CD):
4055
4274
 
4056
4275
 
4057
4276
  if __name__ == "__main__":
4058
-
4059
- # ------------------------------------------------------------------
4060
- # §6 Validation CD figures of merit at ITER Q=10.
4061
- #
4062
- # Reference: Shimada et al., Nucl. Fusion 47 (2007) S1.
4063
- # ITER PIPB Ch.6 projections (Gormezano et al., NF 47 (2007) S285):
4064
- # γ_LH 0.24, γ_EC 0.20, γ_NBI 0.15 MA/(MW m²)
4065
- # ------------------------------------------------------------------
4066
-
4067
- kw = dict(a=2.0, R0=6.2, B0=5.3, nbar=1.01, Tbar=8.9,
4068
- nu_n=0.1, nu_T=1.0, Z_eff=1.6, beta_T=0.025,
4069
- rho_ped=0.94, n_ped_frac=0.80, T_ped_frac=0.40)
4070
-
4071
- # LHCD: METIS mode 0 (Artaud 2018), no calibration constant
4072
- gLH = f_etaCD_LH_physics(kw['Tbar'], kw['Z_eff'])
4073
-
4074
- # ECCD and NBCD: local T_e at ρ_dep = 0.3, with trapped-particle correction
4075
- gEC = f_etaCD_EC_physics(kw['a'], kw['R0'], kw['Tbar'], kw['nbar'], kw['Z_eff'],
4076
- kw['nu_T'], kw['nu_n'], rho_EC=0.3,
4077
- rho_ped=kw['rho_ped'], n_ped_frac=kw['n_ped_frac'],
4078
- T_ped_frac=kw['T_ped_frac'])
4079
-
4080
- # NBI reference: 0.20–0.40 (Gormezano 2007) spans all ITER scenarios.
4081
- # For Scenario 2 (Q=10, n̄ ~ 1.01×10²⁰ m⁻³, 15 MA), the lower end
4082
- # ~0.15–0.20 applies; the upper end (0.40) is Scenario 4 (Q=5,
4083
- # ~ 0.67×10²⁰ m⁻³) where lower density greatly increases efficiency.
4084
- # D0FUS result 0.163 is consistent with Q=10 conditions.
4085
- gNBI = f_etaCD_NBI_physics(2, 1000., kw['a'], kw['R0'], kw['Tbar'], kw['nbar'],
4086
- kw['Z_eff'], kw['nu_T'], kw['nu_n'], rho_NBI=0.3,
4087
- rho_ped=kw['rho_ped'], n_ped_frac=kw['n_ped_frac'],
4088
- T_ped_frac=kw['T_ped_frac'])
4089
-
4090
- P_EC, P_NBI = 20.0, 33.0 # absorbed CD power [MW] — Shimada 2007
4091
- I_EC = f_I_CD(kw['R0'], kw['nbar'], gEC, P_EC)
4092
- I_NBI = f_I_CD(kw['R0'], kw['nbar'], gNBI, P_NBI * 0.95) # 5% orbit loss
4093
-
4094
- print("\n── CD figures of merit — ITER Q=10 (H-mode pedestal) " + "─"*18)
4095
- print(f" {'Source':<18} {'γ [MA/(MW m²)]':>14} {'I_CD [MA]':>10}"
4096
- f" {'P_inj [MW]':>11} {'ITER ref.':>10}")
4097
- print(" " + "─"*68)
4098
- print(f" {'LHCD':<18} {gLH:>14.4f} {'—':>10} {'—':>11} {'~0.33':>10}")
4099
- print(f" {'ECCD (ρ=0.3)':<18} {gEC:>14.4f} {I_EC:>10.3f}"
4100
- f" {P_EC:>11.1f} {'~0.20':>10}")
4101
- print(f" {'NBCD D,1MeV':<18} {gNBI:>14.4f} {I_NBI:>10.3f}"
4102
- f" {P_NBI:>11.1f} {'~0.15':>10}")
4103
-
4104
- # Current budget check (Eriksson et al., Nucl. Fusion 64 (2024) 126033):
4105
- # Inductive ~ 10 MA, non-inductive ~ 5 MA (bootstrap dominant at ~3.5 MA),
4106
- # I_NBI + I_EC ~ 1-2 MA. D0FUS total: 0.634 + 0.814 = 1.45 MA. Consistent.
4107
-
4277
+ # ── ITER chain (5/12) - current drive and current decomposition ─────
4278
+ # Part A: PIPB-style figures of merit at the historical comparison
4279
+ # point (rho = 0.3, Tbar = 8.9 keV) against the ITER Chapter 6
4280
+ # projections (Gormezano et al., NF 47 (2007) S285), kept as
4281
+ # informative context: gamma_LH ~ 0.24-0.33, gamma_EC ~ 0.20,
4282
+ # gamma_NBI ~ 0.15-0.40 MA/(MW m2) across the ITER scenarios.
4283
+ _kwq = dict(a=2.0, R0=6.2, Tbar=8.9, nbar=1.01, nu_n=0.1, nu_T=1.0,
4284
+ Z_eff=1.6, rho_ped=0.94, n_ped_frac=0.80, T_ped_frac=0.40)
4285
+ _gLH_pub = f_etaCD_LH_physics(_kwq['Tbar'], _kwq['Z_eff'])
4286
+ _gEC_pub = f_etaCD_EC_physics(_kwq['a'], _kwq['R0'], _kwq['Tbar'],
4287
+ _kwq['nbar'], _kwq['Z_eff'], _kwq['nu_T'],
4288
+ _kwq['nu_n'], rho_EC=0.3,
4289
+ rho_ped=_kwq['rho_ped'],
4290
+ n_ped_frac=_kwq['n_ped_frac'],
4291
+ T_ped_frac=_kwq['T_ped_frac'])
4292
+ _gNBI_pub = f_etaCD_NBI_physics(2, 1000., _kwq['a'], _kwq['R0'],
4293
+ _kwq['Tbar'], _kwq['nbar'], _kwq['Z_eff'],
4294
+ _kwq['nu_T'], _kwq['nu_n'], rho_NBI=0.3,
4295
+ rho_ped=_kwq['rho_ped'],
4296
+ n_ped_frac=_kwq['n_ped_frac'],
4297
+ T_ped_frac=_kwq['T_ped_frac'])
4298
+ # Part B: chain evaluation at the deck point (Tbar = 7.754 keV,
4299
+ # rho_EC = 0.40 for NTM control, rho_NBI = 0.30, 1 MeV D, 20 deg).
4300
+ _eLH = f_etaCD_LH_physics(ITER['Tbar'], ITER['Zeff'])
4301
+ _eEC = f_etaCD_EC_physics(ITER['a'], ITER['R0'], ITER['Tbar'],
4302
+ ITER['nbar'], ITER['Zeff'], ITER['nu_T'],
4303
+ ITER['nu_n'], ITER['rho_EC'],
4304
+ rho_ped=ITER['rho_ped'],
4305
+ n_ped_frac=ITER['n_ped_frac'],
4306
+ T_ped_frac=ITER['T_ped_frac'])
4307
+ _eNBI = f_etaCD_NBI_physics(ITER['A_beam'], ITER['E_beam_keV'], ITER['a'],
4308
+ ITER['R0'], ITER['Tbar'], ITER['nbar'],
4309
+ ITER['Zeff'], ITER['nu_T'], ITER['nu_n'],
4310
+ ITER['rho_NBI'], f_alpha=FROZEN['f_alpha'],
4311
+ angle_NBI_deg=ITER['angle_NBI_deg'],
4312
+ rho_ped=ITER['rho_ped'],
4313
+ n_ped_frac=ITER['n_ped_frac'],
4314
+ T_ped_frac=ITER['T_ped_frac'])
4315
+ _IEC = f_I_CD(ITER['R0'], ITER['nbar'], _eEC, ITER['P_ECRH'])
4316
+ _INBI = f_I_CD(ITER['R0'], ITER['nbar'], _eNBI, ITER['P_NBI'])
4317
+ _ICD = _IEC + _INBI # ICRH drives no current (gamma_ICR = 0)
4318
+ ITER.update(I_CD=_ICD)
4319
+ _bench("ITER chain 5/12 - current drive (Multi: NBI + EC + IC)", [
4320
+ ("gamma_LH, PIPB point [MA/(MW m2)]", _gLH_pub, "~0.24-0.33", None,
4321
+ "Gormezano 2007"),
4322
+ ("gamma_EC, PIPB point (rho=0.3)", _gEC_pub, "~0.20", None,
4323
+ "Gormezano 2007"),
4324
+ ("gamma_NBI, PIPB point", _gNBI_pub, "0.15-0.40", None,
4325
+ "Gormezano 2007"),
4326
+ ("eta_LH chain [MA/(MW m2)]", _eLH, FROZEN['eta_LH'], 2e-3,
4327
+ "deck frozen"),
4328
+ ("eta_EC chain (rho=0.40)", _eEC, FROZEN['eta_EC'], 2e-3,
4329
+ "deck frozen"),
4330
+ ("eta_NBI chain (1 MeV, 20 deg)", _eNBI, FROZEN['eta_NBI'], 2e-3,
4331
+ "deck frozen"),
4332
+ ("I_EC [MA]", _IEC, None, None, "P_EC = 6.7 MW"),
4333
+ ("I_NBI [MA]", _INBI, None, None, "P_NBI = 33 MW"),
4334
+ ("I_CD total [MA]", _ICD, FROZEN['I_CD'], 2e-3, "deck frozen"),
4335
+ ], notes=[
4336
+ "Chain efficiencies are lower than the PIPB-point values because "
4337
+ "the deck deposits EC off-axis (rho = 0.40, NTM control) at the "
4338
+ "solved Tbar = 7.75 keV.",
4339
+ "Current budget context (Eriksson et al., NF 64 (2024) 126033): "
4340
+ "inductive ~10 MA, non-inductive ~5 MA with bootstrap dominant.",
4341
+ ])
4108
4342
 
4109
4343
  #%% L-H transition threshold
4110
4344
 
@@ -4338,44 +4572,55 @@ def f_P_thresh(nbar, B0, a, R0, kappa, M_ion, Ip=None,
4338
4572
 
4339
4573
 
4340
4574
  if __name__ == "__main__":
4341
- # L-H threshold anchors Martin, JPCS 123 (2008) 012033, against the
4342
- # PUBLISHED ITER predictions of Table 5 in the ITPA TC-26 paper
4343
- # (NF 2026, 10.1088/1741-4326/ae39f2): 52.3 MW @ 0.5e20 and
4344
- # 86.0 MW @ 1.0e20 (deuterium, full field). Tol 10 % (Ramanujan-ellipse
4345
- # surface vs the true ITER LCFS 680 m²).
4346
- for _n20, _Pref in ((0.5, 52.3), (1.0, 86.0)):
4347
- _P = P_Thresh_Martin(_n20, 5.3, 2.0, 6.2, 1.85, 2.0)
4348
- assert abs(_P/_Pref - 1) < 0.10, (_n20, _P)
4349
- # 'New_S' (TC-26 draft 2017, Delabie) sits within the published 1σ of
4350
- # TC-26(Bt): P/S = (0.0441±0.0025)·B^(0.580±0.039)·n^(1.08±0.03)·
4351
- # (2/M)^(0.975±0.032). Updating to the published central values
4352
- # (~2-4 % on P_LH) is a deliberate maintainer decision.
4353
- _b = P_Thresh_New_S(1., 1., 2., 6.2, 1.7, 2.)
4354
- assert abs(np.log2(P_Thresh_New_S(1., 2., 2., 6.2, 1.7, 2.)/_b) - 0.580) < 0.04
4355
- assert abs(np.log2(P_Thresh_New_S(2., 1., 2., 6.2, 1.7, 2.)/_b) - 1.08) < 0.04
4356
- print("OK Martin 2008: ancres ITER publiées 52.3/86.0 MW; New_S dans le 1σ TC-26")
4575
+ # ── Published anchors - L-H power threshold ──────────────────────────
4576
+ # Martin, JPCS 123 (2008) 012033, against the PUBLISHED ITER
4577
+ # predictions of Table 5 in the ITPA TC-26 paper (NF 2026,
4578
+ # 10.1088/1741-4326/ae39f2): 52.3 MW at 0.5e20 m-3 and 86.0 MW at
4579
+ # 1.0e20 m-3 (deuterium, full field). Tolerance 10 %: D0FUS evaluates
4580
+ # the plasma surface from the Ramanujan ellipse perimeter, the
4581
+ # reference from the true ITER LCFS (~680 m2). The 'New_S' option
4582
+ # (TC-26 draft 2017, Delabie) must sit within the published 1-sigma
4583
+ # intervals of TC-26(Bt): P/S = (0.0441+-0.0025) B^(0.580+-0.039)
4584
+ # n^(1.08+-0.03) (2/M)^(0.975+-0.032); updating to the published
4585
+ # central values (2-4 % on P_LH) is a deliberate maintainer decision.
4586
+ _b_ref = P_Thresh_New_S(1., 1., 2., 6.2, 1.7, 2.)
4587
+ _bench("Published anchors - L-H threshold (Martin, New_S)", [
4588
+ ("P_LH Martin, 0.5e20, D [MW]",
4589
+ P_Thresh_Martin(0.5, 5.3, 2.0, 6.2, 1.85, 2.0), 52.3, 0.10,
4590
+ "ITPA TC-26 2026"),
4591
+ ("P_LH Martin, 1.0e20, D [MW]",
4592
+ P_Thresh_Martin(1.0, 5.3, 2.0, 6.2, 1.85, 2.0), 86.0, 0.10,
4593
+ "ITPA TC-26 2026"),
4594
+ ("New_S exponent on B [-]",
4595
+ float(np.log2(P_Thresh_New_S(1., 2., 2., 6.2, 1.7, 2.) / _b_ref)),
4596
+ 0.580, 0.07, "TC-26 1-sigma"),
4597
+ ("New_S exponent on n [-]",
4598
+ float(np.log2(P_Thresh_New_S(2., 1., 2., 6.2, 1.7, 2.) / _b_ref)),
4599
+ 1.08, 0.037, "TC-26 1-sigma"),
4600
+ ])
4357
4601
 
4358
4602
  if __name__ == "__main__":
4359
-
4360
- # ITER Q=10 DT baseline Shimada et al., Nucl. Fusion 47 (2007) S1
4361
- # M_eff = 2.5 for DT already included; isotope factor P_LH 1/M applied.
4362
- # Metal-wall correction (W/Be vs C): ×~0.70 [Ryter et al., NF 53 (2013) 113003;
4363
- # Maggi et al., NF 54 (2014) 023007] best estimate ~50 MW.
4364
- p_IT = dict(nbar=1.0, B0=5.3, a=2.0, R0=6.2, kappa=1.7, Ip=15.0, M=2.5)
4365
- PM = P_Thresh_Martin(p_IT['nbar'], p_IT['B0'], p_IT['a'], p_IT['R0'],
4366
- p_IT['kappa'], p_IT['M'])
4367
- PS = P_Thresh_New_S (p_IT['nbar'], p_IT['B0'], p_IT['a'], p_IT['R0'],
4368
- p_IT['kappa'], p_IT['M'])
4369
- PI = P_Thresh_New_Ip(p_IT['nbar'], p_IT['B0'], p_IT['a'], p_IT['R0'],
4370
- p_IT['kappa'], p_IT['Ip'], p_IT['M'])
4371
-
4372
- print("\n── L-H power threshold — ITER Q=10 (DT, W/Be wall) ─────────────────────────")
4373
- print(f" {'Scaling':<10} {'P_LH [MW]':>10} {'ITER ref. [MW]':>14}")
4374
- print(" " + "" * 72)
4375
- print(f" {'Martin':<10} {PM:>10.1f} {'~50':>14}")
4376
- print(f" {'New_S':<10} {PS:>10.1f} {'~50':>14}")
4377
- print(f" {'New_Ip':<10} {PI:>10.1f} {'~50':>14}")
4378
-
4603
+ # ── ITER chain (6/12) - L-H threshold and separatrix power ──────────
4604
+ # The Martin scaling is evaluated with the LINE-averaged chain density
4605
+ # and the D-T isotope mass M = 2.5 (P_LH proportional to 1/M), as in
4606
+ # the production solver. P_sep = P_alpha + P_CD - P_rad,total
4607
+ # (f_P_sep; P_Ohm is not included, matching D0FUS_run.py). The
4608
+ # metal-wall correction (W/Be vs C, factor ~0.70: Ryter et al., NF 53
4609
+ # (2013) 113003; Maggi et al., NF 54 (2014) 023007) yields the ~50 MW
4610
+ # best estimate usually quoted for ITER.
4611
+ _PLH = P_Thresh_Martin(ITER['nbar_line'], ITER['B0'], ITER['a'],
4612
+ ITER['R0'], ITER['kappa'], ITER['M'])
4613
+ _Psep = f_P_sep(ITER['P_fus'], ITER['P_aux'], ITER['P_rad_tot'])
4614
+ ITER.update(P_sep=_Psep, P_LH_th=_PLH)
4615
+ _bench("ITER chain 6/12 - L-H threshold and P_sep", [
4616
+ ("P_LH Martin (D-T, M=2.5) [MW]", _PLH, FROZEN['P_LH_th'], 2e-3,
4617
+ "deck frozen"),
4618
+ ("P_LH x 0.70 metal wall [MW]", 0.70 * _PLH, "~50", None,
4619
+ "Ryter 2013 / Maggi 2014"),
4620
+ ("P_sep [MW]", _Psep, FROZEN['P_sep'], 2e-3, "deck frozen"),
4621
+ ("H-mode access P_sep/P_LH [-]", _Psep / _PLH, (1.0, 3.0), 1,
4622
+ "operational"),
4623
+ ])
4379
4624
 
4380
4625
  def f_P_LH_thresh(nbar, B0, a, R0, kappa, M_ion, Ip=None,
4381
4626
  Option_PLH='Martin'):
@@ -4745,6 +4990,37 @@ def f_Vloop(I_Ohm, a, kappa, R0, Tbar, nbar, Z_eff, q95, nu_T, nu_n,
4745
4990
  # The duplicate definition that was previously here has been removed to avoid
4746
4991
  # silent name shadowing in Python's module namespace.
4747
4992
 
4993
+ if __name__ == "__main__":
4994
+ # ── ITER chain (7/12) - ohmic power and resistivity models ──────────
4995
+ # P_Ohm closes the power balance at the 0.4 MW level, negligible for
4996
+ # ITER but the natural place to exercise the resistivity chain. The
4997
+ # frozen I_Ohm and q95 are forward references, both closed by
4998
+ # chain 12 (I_Ohm = Ip - I_bs - I_CD and the q95 inversion). The
4999
+ # four implemented resistivity models are compared at the chain
5000
+ # point: the neoclassical values (Sauter 1999; Redl 2021) must exceed
5001
+ # Spitzer because trapped electrons cannot carry parallel current.
5002
+ _Palpha = f_P_alpha(ITER['P_fus'])
5003
+ _po = {m: f_P_Ohm(FROZEN['I_Ohm'], ITER['Tbar'], ITER['R0'], ITER['a'],
5004
+ ITER['kappa'], Z_eff=ITER['Zeff'], nbar=ITER['nbar'],
5005
+ eta_model=m, q95=FROZEN['q95'])
5006
+ for m in ('old', 'spitzer', 'sauter', 'redl')}
5007
+ ITER.update(P_alpha=_Palpha, P_Ohm=_po['sauter']) # deck: eta_model=sauter
5008
+ _bench("ITER chain 7/12 - ohmic power and resistivity models", [
5009
+ ("P_alpha = P_fus Ea/(Ea+En) [MW]", _Palpha,
5010
+ ITER['P_fus'] * E_ALPHA / (E_ALPHA + E_N), 1e-12, "definition"),
5011
+ ("P_alpha approx P_fus/5 [MW]", _Palpha, 100.0, 1e-3, "rule of thumb"),
5012
+ ("P_Ohm Sauter (deck) [MW]", _po['sauter'], FROZEN['P_Ohm'], 5e-3,
5013
+ "deck frozen"),
5014
+ ("P_Ohm Spitzer [MW]", _po['spitzer'], None, None, "model comparison"),
5015
+ ("P_Ohm Redl [MW]", _po['redl'], None, None, "model comparison"),
5016
+ ("P_Ohm Wesson 'old' [MW]", _po['old'], None, None, "model comparison"),
5017
+ ("neoclassical enhancement Sauter/Spitzer",
5018
+ _po['sauter'] / _po['spitzer'], (1.0, 5.0), 1, "trapped electrons"),
5019
+ ("Sauter vs Redl consistency",
5020
+ _po['sauter'] / _po['redl'], (0.7, 1.4), 1, "model agreement"),
5021
+ ])
5022
+
5023
+
4748
5024
  def f_I_Ohm(Ip, Ib, I_CD):
4749
5025
  """
4750
5026
  Inductive (Ohmic) plasma current from the current balance.
@@ -4829,16 +5105,21 @@ def f_Q_multiaux(P_fus, P_LH, P_ECRH, P_NBI, P_ICRH, P_Ohm):
4829
5105
 
4830
5106
 
4831
5107
  if __name__ == "__main__":
4832
- # ITER Q=10 baseline: P_fus=500 MW, P_ECRH=20 MW, P_NBI=13 MW, P_Ohm~1.5 MW
4833
- # Reference: Shimada et al., Nucl. Fusion 47 (2007) S1; PIPB Ch.6 (Gormezano 2007)
4834
- Q_calc = f_Q_multiaux(P_fus=500.0, P_LH=20.0, P_ECRH=20.0,
4835
- P_NBI=13.0, P_ICRH=0.0, P_Ohm=1.5)
4836
-
4837
- print("\n── Fusion gain Q — ITER Q=10 baseline ─────────────────────────────────────")
4838
- print(f" {'Quantity':<30} {'D0FUS':>8} {'ITER ref.':>10}")
4839
- print(" " + "─" * 64)
4840
- print(f" {'Q (500 MW / 54.5 MW ext)':<30} {Q_calc:>8.2f} {'10':>10}")
4841
-
5108
+ # ── ITER chain (8/12) - fusion gain ──────────────────────────────────
5109
+ # Q = P_fus / (P_aux + P_Ohm) with the flat-top heating mix of the
5110
+ # deck (33 NBI + 6.7 EC + 10 IC = 49.7 MW [Kim 2018]) and the chain
5111
+ # ohmic power from chain 4. The published design target is Q = 10
5112
+ # with ~50 MW of auxiliary power (Shimada 2007).
5113
+ _Q = f_Q_multiaux(P_fus=ITER['P_fus'], P_LH=ITER['P_LH'],
5114
+ P_ECRH=ITER['P_ECRH'], P_NBI=ITER['P_NBI'],
5115
+ P_ICRH=ITER['P_ICRH'], P_Ohm=ITER['P_Ohm'])
5116
+ ITER.update(Q=_Q)
5117
+ _bench("ITER chain 8/12 - fusion gain", [
5118
+ ("Q = P_fus/(P_aux + P_Ohm) [-]", _Q, FROZEN['Q'], 2e-3,
5119
+ "deck frozen"),
5120
+ ("Q [-]", _Q, 10.0, 0.05, "Shimada 2007"),
5121
+ ])
5122
+
4842
5123
  # Historical model extracted from
4843
5124
  # D.J. Segal, A.J. Cerfon, J.P. Freidberg, "Steady state versus pulsed tokamak reactors",
4844
5125
  # Nuclear Fusion, 61(4), 045001, 2021.
@@ -4959,12 +5240,6 @@ def f_Segal_Ib(nu_n, nu_T, epsilon, kappa, n20, Tk, R0, I_M,
4959
5240
 
4960
5241
  return I_b
4961
5242
 
4962
- if __name__ == "__main__":
4963
- # ITER Q=10 baseline — Shimada et al., Nucl. Fusion 47 (2007) S1
4964
- Ib_Segal = f_Segal_Ib(nu_n=0.5, nu_T=1.0, epsilon=2.0/6.2, kappa=1.75,
4965
- n20=1.01, Tk=8.9, R0=6.2, I_M=15.0)
4966
-
4967
-
4968
5243
  """
4969
5244
  Neoclassical Bootstrap Current Model - Sauter et al. (1999)
4970
5245
 
@@ -5176,14 +5451,20 @@ def _nu_e_star(n_e, T_e, q, R0, epsilon, Z_eff):
5176
5451
 
5177
5452
 
5178
5453
  if __name__ == "__main__":
5179
- # Collisionality identity Sauter, Angioni & Lin-Liu, Phys. Plasmas 6
5180
- # (1999) 2834, Eq. 18b with lnΛ_e of Eq. 18d. NB: the helper takes T_e
5181
- # in eV. ITER core ν*_e ≈ 0.03 (banana regime).
5454
+ # ── Published anchor - electron collisionality identity ──────────────
5455
+ # Sauter, Angioni & Lin-Liu, Phys. Plasmas 6 (1999) 2834, Eq. 18b
5456
+ # with the Coulomb logarithm of Eq. 18d (helper takes T_e in eV).
5457
+ # The ITER core sits deep in the banana regime, nu*_e ~ 0.03.
5182
5458
  _ne, _Te = 1.0e20, 8900.0
5183
- _lnL = 31.3 - np.log(np.sqrt(_ne)/_Te)
5184
- _nref = 6.921e-18 * 3.0 * 6.2 * _ne * 1.7 * _lnL / (_Te**2 * (2/6.2)**1.5)
5185
- assert abs(float(_nu_e_star(_ne, _Te, 3.0, 6.2, 2/6.2, 1.7))/_nref - 1) < 1e-6
5186
- print(f"OK Collisionnalité Sauter: identité Eq. 18b (ν*_e,ITER = {_nref:.3f})")
5459
+ _lnL = 31.3 - np.log(np.sqrt(_ne) / _Te)
5460
+ _nref = 6.921e-18 * 3.0 * 6.2 * _ne * 1.7 * _lnL / (_Te**2 * (2 / 6.2)**1.5)
5461
+ _bench("Published anchor - Sauter collisionality (Eq. 18b)", [
5462
+ ("nu*_e identity, ITER core [-]",
5463
+ float(_nu_e_star(_ne, _Te, 3.0, 6.2, 2 / 6.2, 1.7)), _nref, 1e-6,
5464
+ "Sauter 1999"),
5465
+ ("banana-regime check nu*_e", float(_nref), (0.0, 0.1), 1,
5466
+ "ITER core"),
5467
+ ])
5187
5468
 
5188
5469
  def _nu_i_star(n_i, T_i, q, R0, epsilon):
5189
5470
  """Ion collisionality [Eq. 18c]."""
@@ -6368,20 +6649,34 @@ def f_q_profile_refined(
6368
6649
 
6369
6650
 
6370
6651
  if __name__ == "__main__":
6371
- # Bootstrap comparison table only Segal and Sauter-Redl remain.
6372
- # Ib_Segal was computed earlier (~line 4220) at module-level test time.
6373
- _bs_kw = dict(R0=6.2, a=2.0, kappa=1.75, B0=5.3,
6374
- nbar=1.01, Tbar=8.9, q95=3.0,
6375
- Z_eff=1.65, nu_n=0.1, nu_T=1.0,
6376
- rho_ped=0.94, n_ped_frac=0.90, T_ped_frac=0.40)
6377
- Ib_SauterRedl = f_Sauter_Redl_Ib(**_bs_kw)
6378
-
6379
- print("\n── Bootstrap current — ITER Q=10 baseline ───────────────────────────────────")
6380
- print(f" {'Model':<20} {'I_bs [MA]':>10} {'ITER ref. [MA]':>14}")
6381
- print(" " + "─" * 72)
6382
- print(f" {'Segal (2021)':<20} {Ib_Segal:>10.2f} {'3.0':>14}")
6383
- print(f" {'Sauter-Redl (2021)':<20} {Ib_SauterRedl:>10.2f} {'3.0':>14}")
6384
-
6652
+ # ── ITER chain (9/12) - bootstrap current ────────────────────────────
6653
+ # Sauter-Redl evaluated at the chain operating point with the deck
6654
+ # pedestal profiles. q95 is a forward reference (FROZEN), closed by
6655
+ # chain 12 after f_q95 is defined. The production solver additionally
6656
+ # refines the collisionality with the Picard q(rho) cache, worth
6657
+ # ~0.2 % on I_bs, hence the 1 % tolerance. The Segal (2021) model is
6658
+ # reported for model comparison at the same point.
6659
+ _Ib = f_Sauter_Redl_Ib(ITER['R0'], ITER['a'], ITER['kappa'], ITER['B0'],
6660
+ ITER['nbar'], ITER['Tbar'], FROZEN['q95'],
6661
+ ITER['Zeff'], ITER['nu_n'], ITER['nu_T'],
6662
+ rho_ped=ITER['rho_ped'],
6663
+ n_ped_frac=ITER['n_ped_frac'],
6664
+ T_ped_frac=ITER['T_ped_frac'],
6665
+ Vprime_data=ITER_Vpd, kappa_95=ITER['kappa95'],
6666
+ tau_i_e=1.0)
6667
+ _Ib_seg = f_Segal_Ib(ITER['nu_n'], ITER['nu_T'], ITER['a'] / ITER['R0'],
6668
+ ITER['kappa'], ITER['nbar'], ITER['Tbar'],
6669
+ ITER['R0'], FROZEN['Ip'],
6670
+ rho_ped=ITER['rho_ped'],
6671
+ n_ped_frac=ITER['n_ped_frac'],
6672
+ T_ped_frac=ITER['T_ped_frac'])
6673
+ ITER.update(Ib=_Ib)
6674
+ _bench("ITER chain 9/12 - bootstrap current (Sauter-Redl)", [
6675
+ ("I_bs Sauter-Redl [MA]", _Ib, FROZEN['Ib'], 0.01, "deck frozen"),
6676
+ ("bootstrap fraction I_bs/Ip [-]", _Ib / FROZEN['Ip'], None, None,
6677
+ "chain"),
6678
+ ("I_bs Segal (2021) [MA]", _Ib_seg, None, None, "model comparison"),
6679
+ ])
6385
6680
 
6386
6681
  #%% Other parameters
6387
6682
  # ─────────────────────────────────────────────────────────────────────────────
@@ -6586,13 +6881,29 @@ def f_heat_PFU_Eich(P_sol, B_pol, R, eps, theta_deg,
6586
6881
 
6587
6882
 
6588
6883
  if __name__ == "__main__":
6589
- # SOL width anchor Eich et al., NF 53 (2013) 093031, Table 6,
6590
- # regression #15: λq [mm] = 1.35 P_SOL⁻⁰·⁰² R⁰·⁰⁴ Bpol⁻⁰·⁹² (a/R)⁰·⁴².
6591
- # Closed anchor: the paper's own ITER evaluation gives λq = 0.73 mm
6592
- # (P_SOL = 100 MW, Bpol,MP = 1.185 T, R = 6.2 m, a = 2 m). Tol 8 %.
6593
- _lam, _, _ = f_heat_PFU_Eich(100., 1.185, 6.2, 2/6.2, 3.0, B0=5.3)
6594
- assert abs(_lam*1e3/0.73 - 1) < 0.08, _lam*1e3
6595
- print(f"OK Eich #15: λq,ITER = {_lam*1e3:.2f} mm vs 0.73 publié")
6884
+ # ── SOL width: published anchor and ITER chain (10/12) ──────────────
6885
+ # Published anchor: Eich et al., NF 53 (2013) 093031, Table 6,
6886
+ # regression #15: lambda_q [mm] = 1.35 P_SOL^-0.02 R^0.04 Bpol^-0.92
6887
+ # (a/R)^0.42. Closed worked example: the paper's own ITER evaluation
6888
+ # gives lambda_q = 0.73 mm (P_SOL = 100 MW, Bpol,MP = 1.185 T,
6889
+ # R = 6.2 m, a = 2 m); tolerance 8 % (rounding of the quoted
6890
+ # midplane poloidal field).
6891
+ # Chain: B_pol from the q-inversion at the frozen q95 (closed by
6892
+ # chain 12) and lambda_q at the chain P_sep.
6893
+ _lam_pub, _, _ = f_heat_PFU_Eich(100., 1.185, 6.2, 2 / 6.2, 3.0, B0=5.3)
6894
+ _Bpol = f_Bpol(FROZEN['q95'], ITER['B0'], ITER['a'], ITER['R0'],
6895
+ kappa=ITER['kappa'])
6896
+ _lam, _, _ = f_heat_PFU_Eich(ITER['P_sep'], _Bpol, ITER['R0'],
6897
+ ITER['a'] / ITER['R0'], FROZEN['q95'],
6898
+ B0=ITER['B0'])
6899
+ _bench("ITER chain 10/12 - SOL heat-flux width (Eich #15)", [
6900
+ ("lambda_q paper inputs [mm]", _lam_pub * 1e3, 0.73, 0.08,
6901
+ "Eich 2013"),
6902
+ ("B_pol outer midplane [T]", _Bpol, FROZEN['B_pol'], 2e-3,
6903
+ "deck frozen"),
6904
+ ("lambda_q chain point [mm]", _lam * 1e3, FROZEN['lambda_q_mm'],
6905
+ 2e-3, "deck frozen"),
6906
+ ])
6596
6907
 
6597
6908
  # =============================================================================
6598
6909
  # Refined divertor exhaust — two-point model (Stangeby 2018)
@@ -7274,24 +7585,36 @@ def f_Get_parameter_scaling_law(Scaling_Law):
7274
7585
  # ── Global energy and confinement descriptors ─────────────────────────────────
7275
7586
 
7276
7587
  if __name__ == "__main__":
7277
- # Confinement-scaling registry vs published exponents (exact equality).
7588
+ # ── Published anchors - confinement-scaling registry ─────────────────
7589
+ # The registry must store the published exponents EXACTLY:
7278
7590
  # IPB98(y,2): ITER Physics Basis, NF 39 (1999) 2175 / Doyle, NF 47
7279
7591
  # (2007) S18. ITPA20: Verdoolaege, NF 61 (2021) 076006. ITER89-P:
7280
- # Yushmanov, NF 30 (1990) 1999 C(n19) = 0.048·10⁻⁰·¹ (published n20
7281
- # prefactor converted to the D0FUS n19 convention).
7592
+ # Yushmanov, NF 30 (1990) 1999, with the prefactor converted to the
7593
+ # D0FUS n19 convention: C(n19) = 0.048 x 10^-0.1 = 0.0381.
7594
+ # Exact tuple equality is asserted (structural identity).
7282
7595
  assert f_Get_parameter_scaling_law('IPB98(y,2)') == \
7283
7596
  (0.0562, 0, 0.19, 0.78, 0.58, 1.97, 0.15, 0.41, 0.93, -0.69)
7284
7597
  assert f_Get_parameter_scaling_law('ITPA20') == \
7285
7598
  (0.053, 0.36, 0.2, 0.8, 0.35, 1.71, 0.22, 0.24, 0.98, -0.669)
7286
- assert abs(f_Get_parameter_scaling_law('ITER89-P')[0] - 0.048*10**-0.1) < 2e-4
7287
- # IPB98 at the ITER Q=10 point reproduces the published τ_E ≈ 3.7 s
7288
- # (Ip = 15, B = 5.3, n19 = 10.1, M = 2.5, R = 6.2, κ_x = 1.70,
7289
- # P_loss = 87 MW; tol 8 %: κ convention + P_loss definition spread).
7290
- _Csl,_ad,_aM,_ak,_ae,_aR,_aB,_an,_aI,_aP = f_Get_parameter_scaling_law('IPB98(y,2)')
7291
- _tau = (_Csl * 15**_aI * 5.3**_aB * 10.1**_an * 2.5**_aM * 6.2**_aR
7292
- * (2/6.2)**_ae * 1.70**_ak * 87.0**_aP)
7293
- assert abs(_tau/3.7 - 1) < 0.08, _tau
7294
- print(f"OK Lois de confinement: exposants publiés exacts; τ_IPB98,ITER = {_tau:.2f} s vs 3.7")
7599
+ # IPB98 at the ITER Q=10 point with the PUBLISHED loss-power
7600
+ # convention (P = 87 MW, radiation NOT subtracted; kappa_x = 1.70,
7601
+ # n19 = 10.1): tolerance 8 % covers the kappa-convention and P_loss
7602
+ # definition spread between sources. The production (PROCESS-like)
7603
+ # convention, which subtracts the core radiation, is exercised by
7604
+ # chain 11.
7605
+ _Csl, _ad, _aM, _ak, _ae, _aR, _aB, _an, _aI, _aP = \
7606
+ f_Get_parameter_scaling_law('IPB98(y,2)')
7607
+ _tau98 = (_Csl * 15**_aI * 5.3**_aB * 10.1**_an * 2.5**_aM * 6.2**_aR
7608
+ * (2 / 6.2)**_ae * 1.70**_ak * 87.0**_aP)
7609
+ _bench("Published anchors - confinement scaling laws", [
7610
+ ("IPB98(y,2) / ITPA20 exponents", "exact", "published", None,
7611
+ "registry assert"),
7612
+ ("ITER89-P prefactor C(n19) [-]",
7613
+ f_Get_parameter_scaling_law('ITER89-P')[0], 0.048 * 10**-0.1, 5e-3,
7614
+ "Yushmanov 1990"),
7615
+ ("tau_IPB98, published convention [s]", _tau98, 3.7, 0.08,
7616
+ "IPB 1999"),
7617
+ ])
7295
7618
 
7296
7619
  def f_tauE(pbar, V, P_Alpha, P_Aux, P_Ohm, P_rad):
7297
7620
  """
@@ -7441,6 +7764,57 @@ def f_Q(P_fus, P_CD, P_Ohm):
7441
7764
  return P_fus / P_heat
7442
7765
 
7443
7766
 
7767
+ if __name__ == "__main__":
7768
+ # ── ITER chain (11/12) - stored energy, tau_E and plasma current ────
7769
+ # Loss-power convention: D0FUS subtracts the CORE radiated power from
7770
+ # the heating power, P_loss = P_alpha + P_aux + P_Ohm - P_rad,core,
7771
+ # in BOTH tau_E and the scaling-law inversion for Ip (PROCESS-like
7772
+ # convention; see the note in f_tauE). With this convention and the
7773
+ # chain radiation budget, the module-level tau_E and Ip must land on
7774
+ # the full-device values. The published tau_E = 3.7 s instead uses
7775
+ # P_loss = 87 MW with no radiation subtracted: the same stored energy
7776
+ # then gives W_th/87 = 3.91 s, bracketing the published value.
7777
+ # The IPB98 inversion uses the area elongation kappa_a = V/(2 pi^2 R0
7778
+ # a^2) and the LINE-averaged density (fitting conventions of the law).
7779
+ _W = f_W_th(ITER['pbar'], ITER['V']) / 1e6 # [MJ]
7780
+ _tau = f_tauE(ITER['pbar'], ITER['V'], ITER['P_alpha'], ITER['P_aux'],
7781
+ ITER['P_Ohm'], ITER['P_rad_core'])
7782
+ _Ploss = (ITER['P_alpha'] + ITER['P_aux'] + ITER['P_Ohm']
7783
+ - ITER['P_rad_core'])
7784
+ _Csl, _ad, _aM, _ak, _ae, _aR, _aB, _an, _aI, _aP = \
7785
+ f_Get_parameter_scaling_law('IPB98(y,2)')
7786
+ _Ip = f_Ip(_tau, ITER['R0'], ITER['a'], ITER['kappa_a'], ITER['delta'],
7787
+ ITER['nbar_line'], ITER['B0'], ITER['M'],
7788
+ ITER['P_alpha'], ITER['P_Ohm'], ITER['P_aux'],
7789
+ ITER['P_rad_core'], ITER['H'], _Csl,
7790
+ _ad, _aM, _ak, _ae, _aR, _aB, _an, _aI, _aP)
7791
+ _nG = f_nG(_Ip, ITER['a'])
7792
+ ITER.update(tauE=_tau, Ip=_Ip, W_th=_W)
7793
+ _bench("ITER chain 11/12 - tau_E and plasma current (IPB98 inversion)", [
7794
+ ("W_th [MJ]", _W, FROZEN['W_th'], 1e-3, "deck frozen"),
7795
+ ("P_loss = P_heat - P_rad,core [MW]", _Ploss, None, None,
7796
+ "convention"),
7797
+ ("tau_E, core radiation subtracted [s]", _tau, FROZEN['tauE'], 1e-3,
7798
+ "deck frozen"),
7799
+ ("energy-balance closure W/(tau P_loss)",
7800
+ f_W_th(ITER['pbar'], ITER['V']) / 1e6 / (_tau * _Ploss), 1.0, 1e-9,
7801
+ "identity"),
7802
+ ("tau_E, published convention [s]", _W / 87.0, "3.7", None,
7803
+ "IPB 1999 (P=87 MW)"),
7804
+ ("Ip from IPB98 inversion [MA]", _Ip, FROZEN['Ip'], 5e-3,
7805
+ "deck frozen"),
7806
+ ("Ip [MA]", _Ip, 15.0, 0.01, "Shimada 2007"),
7807
+ ("n_GW at chain Ip [1e20 m-3]", _nG, FROZEN['nG'], 2e-3,
7808
+ "deck frozen"),
7809
+ ("f_GW = n_line/n_GW [-]", ITER['nbar_line'] / _nG, 0.85, 5e-3,
7810
+ "deck target"),
7811
+ ], notes=[
7812
+ "Neither Ip nor the density is imposed: the deck receives the "
7813
+ "geometry, B at the front face, P_fus, P_aux and f_GW = 0.85, and "
7814
+ "the chain closes on the published 15 MA / 1.01e20 m-3 point.",
7815
+ ])
7816
+
7817
+
7444
7818
  # ── Helium ash accumulation model ─────────────────────────────────────────────
7445
7819
 
7446
7820
  def _sigmav_vol(T_bar, nu_T, rho_ped=1.0, T_ped_frac=0.0, N=200,
@@ -8483,64 +8857,93 @@ def compute_RE_indicators(Ip, nbar, Tbar, a, R0, κ, Z_eff, li,
8483
8857
  # ── Validation ────────────────────────────────────────────────────────────────
8484
8858
 
8485
8859
  if __name__ == "__main__":
8486
- # Helium ash fraction f_alpha vs C_alpha ITER Q=10 validation
8487
- # Console: q95, neutron wall load, f_alpha reference check
8488
- # Figure: f_alpha(C_alpha) sensitivity curve academic vs H-mode pedestal
8489
-
8490
- # ITER Q=10 reference parameters (Shimada et al., NF 47 (2007) S1)
8491
- # Shimada Table 1 reports ψ_N=0.95 values: κ₉₅=1.70, δ₉₅=0.33.
8492
- # LCFS values from ITER baseline (single-null): κ_edge≈1.85, δ_edge≈0.49.
8493
- _R0, _a = 6.2, 2.0
8494
- _κ_edge, _δ_edge = 1.85, 0.49 # LCFS shaping (Sauter formula)
8495
- _κ_95, _δ_95 = 1.70, 0.33 # 95%-surface shaping (ITER_1989)
8496
- _B0, _Ip = 5.3, 15.0
8497
- _nbar, _Tbar, _tauE = 1.0, 8.9, 3.7
8498
- _P_fus, _nu_T, _C_α = 500.0, 1.0, 5.0
8499
-
8500
- _q95 = f_q95(_B0, _Ip, _R0, _a, _κ_edge, _δ_edge, _κ_95, _δ_95)
8501
- _Gamma_n = f_Gamma_n(_a, _P_fus, _R0, _κ_edge)
8502
- _f_alpha = f_He_fraction(_nbar, _Tbar, _tauE, _C_α, _nu_T)
8503
-
8504
- print("\n── Safety factor q₉₅ — ITER Q=10 ──────────────────────────────────────────")
8505
- print(f" {'Quantity':<20} {'D0FUS':>8} {'ITER ref.':>10}")
8506
- print(" " + "─" * 58)
8507
- print(f" {'q₉₅':<20} {_q95:>8.2f} {'3.0':>10}")
8508
-
8509
- print("\n── Neutron wall load Γ_n — ITER Q=10 ──────────────────────────────────────")
8510
- print(f" {'Quantity':<20} {'D0FUS':>8} {'ITER ref.':>10}")
8511
- print(" " + "─" * 58)
8512
- print(f" {'Γ_n [MW/m²]':<20} {_Gamma_n:>8.3f} {'0.57':>10}")
8513
-
8514
- print("\n── Helium ash fraction f_α ITER Q=10 ────────────────────────────────────")
8515
- print(f" {'Quantity':<20} {'D0FUS':>8} {'ITER ref.':>10}")
8516
- print(" " + "─" * 58)
8517
- print(f" {f'f_α [%] (C_α={_C_α:.0f})':<20} {_f_alpha*100:>8.1f} {'5':>10}")
8518
-
8519
- # ── Runaway electrons ITER Q=10 ────────────────────────────────────────
8520
- # Hot-tail seed: Smith, Phys. Plasmas 15, 072502 (2008)
8521
- # Avalanche: Breizman et al., Nucl. Fusion 59, 083001 (2019) Eq. 99
8522
-
8523
- print("\n── Hot-tail seed — Stahl (2016) benchmark ──────────────────────────────────")
8524
- print(f" {'Quantity':<20} {'D0FUS':>12} {'Reference':>12} Source")
8525
- print(" " + "─" * 58)
8526
- _f_RE_stahl = _hot_tail_fraction_local(
8527
- 2.8e19, 3.1e3, 1.4e6, Te_final_eV=31.0, tau_TQ=0.3e-3, Z_eff=1.0)
8528
- print(f" {'f_RE (local)':<20} {_f_RE_stahl:>12.3e} {'4-5e-4':>12} Stahl (2016) Fig.2(b)")
8529
-
8530
- print("\n── Avalanche — Breizman (2019) Fig. 17 (li=1, Z=4, Ip=15 MA) ───────────────")
8531
- print(f" lnΛ = ln(λ_D/λ_C) = {_coulomb_log_relativistic(1e20, 5.0):.2f} "
8532
- f"(relativistic, ne=10²⁰, Te=5 eV)")
8533
- print(f" {'I_RE0 [A]':<20} {'D0FUS [MA]':>12} {'Eq.99 [MA]':>12} {'Fig.17':>12}")
8534
- print(" " + "─" * 62)
8535
- for _ire0, _ref99, _fig17 in [(1.0, 3.306, "~2-3"), (1e3, 7.999, "~7-9"), (1e6, 13.002, "~12-14")]:
8536
- _ire_inf = f_RE_avalanche(15e6, _ire0, 1e20, 5.0, 1.0, 4)
8537
- print(f" {_ire0:<20.0e} {_ire_inf/1e6:>12.3f} {_ref99:>12.3f} {_fig17:>12}")
8860
+ # ── ITER chain (12/12) - q95, wall load and helium ash closure ───────
8861
+ # q95: the Sauter (2016) formula at the chain Ip and LCFS shaping
8862
+ # closes the forward reference used by chains 4, 9 and 10; the
8863
+ # ITER-1989 guideline formula at the PUBLISHED 95 % shaping and 15 MA
8864
+ # reproduces the ITER design value q95 = 3.0.
8865
+ # Helium ash: f_alpha solves the Sarazin steady-state balance with
8866
+ # the deck removal efficiency C_alpha = 5 (tau*_alpha = C_alpha
8867
+ # tau_E) at the chain tau_E. The value must close on the f_alpha
8868
+ # forward reference injected in chain 2, which is the convergence
8869
+ # criterion of the production solver. The production solver evaluates
8870
+ # the ash balance with the cylindrical volume weight even in refined
8871
+ # geometry (see D0FUS_run.py); the chain mirrors that call exactly.
8872
+ _q95 = f_q95(ITER['B0'], ITER['Ip'], ITER['R0'], ITER['a'],
8873
+ ITER['kappa'], ITER['delta'], ITER['kappa95'],
8874
+ ITER['delta95'], Option_q95='Sauter')
8875
+ _q95_pub = f_q95(5.3, 15.0, 6.2, 2.0, 1.85, 0.485, 1.70, 0.33,
8876
+ Option_q95='ITER_1989')
8877
+ _Gam = f_Gamma_n(ITER['a'], ITER['P_fus'], ITER['R0'], ITER['kappa'],
8878
+ S_wall=ITER['S'])
8879
+ _fa = f_He_fraction(ITER['nbar'], ITER['Tbar'], ITER['tauE'],
8880
+ ITER['C_Alpha'], ITER['nu_T'],
8881
+ rho_ped=ITER['rho_ped'],
8882
+ T_ped_frac=ITER['T_ped_frac'], tau_i_e=1.0)
8883
+ _ta = f_tau_alpha(ITER['nbar'], ITER['Tbar'], ITER['tauE'],
8884
+ ITER['C_Alpha'], ITER['nu_T'],
8885
+ rho_ped=ITER['rho_ped'],
8886
+ T_ped_frac=ITER['T_ped_frac'])
8887
+ _IOhm = f_I_Ohm(ITER['Ip'], ITER['Ib'], ITER['I_CD'])
8888
+ _bench("ITER chain 12/12 - q95, neutron wall load, helium ash", [
8889
+ ("q95 Sauter, chain Ip [-]", _q95, FROZEN['q95'], 2e-3,
8890
+ "deck frozen"),
8891
+ ("q95 ITER-1989, published point [-]", _q95_pub, 3.0, 0.01,
8892
+ "Uckan 1990"),
8893
+ ("Gamma_n, Miller wall [MW/m2]", _Gam, FROZEN['Gamma_n'], 2e-3,
8894
+ "deck frozen"),
8895
+ ("Gamma_n [MW/m2]", _Gam, 0.57, 0.05, "Shimada 2007"),
8896
+ ("f_He closure (C_alpha = 5) [-]", _fa, FROZEN['f_alpha'], 1e-3,
8897
+ "solver fixed point"),
8898
+ ("f_He, IPB exhaust assumption [%]", _fa * 100, "4.4", None,
8899
+ "IPB 1999"),
8900
+ ("tau*_alpha = C_alpha tau_E [s]", _ta, FROZEN['tau_alpha'], 1e-3,
8901
+ "deck frozen"),
8902
+ ("I_Ohm = Ip - I_bs - I_CD [MA]", _IOhm, FROZEN['I_Ohm'], 5e-3,
8903
+ "deck frozen"),
8904
+ ], notes=[
8905
+ "The f_He closure is the global loop of the chain: chain 2 "
8906
+ "injected the frozen f_alpha into the density solve, and the same "
8907
+ "value re-emerges from the ash balance at the chain tau_E.",
8908
+ "The IPB exhaust value (4.4 %) corresponds to the ITER Physics "
8909
+ "Basis assumption at its own operating point.",
8910
+ "The I_Ohm row closes the forward reference of chain 7 "
8911
+ "(0.2 % residual inherited from the Picard q-profile cache of "
8912
+ "the bootstrap step).",
8913
+ ])
8914
+
8915
+ if __name__ == "__main__":
8916
+ # ── Published anchors - runaway electrons ────────────────────────────
8917
+ # Hot-tail seed: Smith & Verwichte model evaluated at the Stahl
8918
+ # (2016) Fig. 2(b) point (informative: the figure quotes 4-5e-4).
8919
+ # Avalanche: Breizman et al., NF 59 (2019) 083001, Eq. 99 at the
8920
+ # Fig. 17 conditions (li = 1, Z = 4, Ip = 15 MA); the implementation
8921
+ # must reproduce the analytic Eq. 99 values, and the Fig. 17 ranges
8922
+ # are quoted for context.
8923
+ _f_RE = _hot_tail_fraction_local(2.8e19, 3.1e3, 1.4e6, Te_final_eV=31.0,
8924
+ tau_TQ=0.3e-3, Z_eff=1.0)
8925
+ _re_rows = [
8926
+ ("hot-tail f_RE (local) [-]", float(_f_RE), "4-5e-4", None,
8927
+ "Stahl 2016 Fig. 2b"),
8928
+ ("relativistic lnLambda (1e20, 5 eV)",
8929
+ float(_coulomb_log_relativistic(1e20, 5.0)), None, None,
8930
+ "Breizman 2019"),
8931
+ ]
8932
+ for _ire0, _ref99, _fig17 in ((1.0, 3.306, "~2-3"), (1e3, 7.999, "~7-9"),
8933
+ (1e6, 13.002, "~12-14")):
8934
+ _ire = f_RE_avalanche(15e6, _ire0, 1e20, 5.0, 1.0, 4) / 1e6
8935
+ _re_rows.append((f"I_RE avalanche, seed {_ire0:.0e} A [MA]",
8936
+ float(_ire), _ref99, 1e-3, "Breizman Eq. 99"))
8937
+ _re_rows.append((f" Fig. 17 range, seed {_ire0:.0e} A [MA]",
8938
+ float(_ire), _fig17, None, "Breizman Fig. 17"))
8939
+ _bench("Published anchors - runaway electrons (hot tail, avalanche)",
8940
+ _re_rows)
8538
8941
 
8539
8942
  import D0FUS_BIB.D0FUS_figures as figs
8540
8943
  # plot_He_fraction takes separate ITER/DEMO removal efficiencies
8541
- # (C_Alpha_ITER=5.0, C_Alpha_DEMO=7.0 by default); the old single 'C_Alpha'
8542
- # keyword no longer exists. Defaults match the benchmark above (C_α = 5).
8543
- figs.plot_He_fraction(C_Alpha_ITER=_C_α)
8944
+ # (C_Alpha_ITER=5.0, C_Alpha_DEMO=7.0 by default); defaults match the
8945
+ # chain above (C_alpha = 5).
8946
+ figs.plot_He_fraction(C_Alpha_ITER=ITER['C_Alpha'])
8544
8947
 
8545
8948
  #%%
8546
8949
 
@@ -8549,21 +8952,35 @@ if __name__ == "__main__":
8549
8952
 
8550
8953
  if __name__ == "__main__":
8551
8954
  # ─────────────────────────────────────────────────────────────────────
8552
- # General ITER check: the shipped reference deck must reproduce the
8553
- # frozen 2026-06 values (anti-drift guard; intentional physics changes
8554
- # must update these anchors). Skipped gracefully if the deck is absent.
8555
- # Indices follow the save_run_output tuple map.
8955
+ # Full-device regression: the shipped reference deck must reproduce
8956
+ # the frozen 2026-06 values (anti-drift guard; intentional physics
8957
+ # changes must update these anchors AND the FROZEN dict at the top of
8958
+ # this file). Skipped gracefully if the deck is absent. Indices
8959
+ # follow the save_run_output tuple map of D0FUS_EXE/D0FUS_run.py.
8556
8960
  # ─────────────────────────────────────────────────────────────────────
8557
8961
  try:
8558
8962
  from D0FUS_EXE.D0FUS_run import load_config_from_file, run
8559
8963
  _deck = os.path.join(os.path.dirname(os.path.dirname(
8560
8964
  os.path.abspath(__file__))), 'D0FUS_INPUTS', '1_run_ITER.txt')
8561
8965
  _res = run(load_config_from_file(_deck), verbose=0)
8562
- _frozen = {0: 5.300, 3: 3.144, 5: 9.982, 8: 14.968, 13: 1.012,
8563
- 14: 1.191, 16: 1.635, 20: 3.598, 23: 73.522}
8564
- for _i, _v in _frozen.items():
8565
- assert abs(float(_res[_i])/_v - 1) < 5e-3, (_i, float(_res[_i]), _v)
8566
- assert abs(float(_res[4])/1e6/340.15 - 1) < 5e-3 # W_th [MJ]
8567
- print("OK Deck ITER complet: 10 valeurs gelées 2026-06, dérive < 0.5 %")
8966
+ _frozen_idx = [
8967
+ (0, "B0 [T]", 5.300), (3, "tau_E [s]", 3.144),
8968
+ (5, "Q [-]", 9.982), (8, "Ip [MA]", 14.968),
8969
+ (9, "I_bs [MA]", 4.816), (13, "n_line [1e20 m-3]", 1.012),
8970
+ (14, "n_GW [1e20 m-3]", 1.191), (16, "beta_N [-]", 1.635),
8971
+ (20, "q95 [-]", 3.598), (23, "P_LH [MW]", 73.522),
8972
+ (40, "f_alpha [-]", 0.029762),
8973
+ ]
8974
+ _rows = [(f"deck[{_i}] {_nm}", float(_res[_i]), _v, 5e-3,
8975
+ "frozen 2026-06") for _i, _nm, _v in _frozen_idx]
8976
+ _rows.append(("deck[4] W_th [MJ]", float(_res[4]) / 1e6, 340.15,
8977
+ 5e-3, "frozen 2026-06"))
8978
+ _bench("Full-device regression - shipped ITER deck", _rows, notes=[
8979
+ "Closes the chain: every forward reference (f_alpha, "
8980
+ "f_imp_dil, q95, I_Ohm) and every chain output is "
8981
+ "re-produced by the assembled solver on the shipped deck.",
8982
+ ])
8568
8983
  except FileNotFoundError:
8569
- print("-- Deck ITER absent : test général sauté")
8984
+ print("-- ITER deck not found: full-device regression skipped")
8985
+
8986
+ _bench_summary()