d0fus 2.2.2__tar.gz → 2.3.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.
@@ -170,12 +170,39 @@ def detect_mode_from_input(input_file):
170
170
  #%% Main functions
171
171
 
172
172
  def print_banner():
173
- """Display D0FUS banner"""
174
- banner = """
173
+ """Display the D0FUS startup banner.
174
+
175
+ Simple double-line framed banner with title, tagline, version, author,
176
+ license and repository URL. Can be silenced with the environment variable
177
+ ``D0FUS_NO_BANNER`` (useful for batch scans, HPC jobs, CI).
178
+ """
179
+ if os.environ.get("D0FUS_NO_BANNER", "").strip() not in ("", "0", "false", "False"):
180
+ return
181
+
182
+ # Resolve installed package version, fall back to a static string
183
+ try:
184
+ from importlib.metadata import version, PackageNotFoundError
185
+ try:
186
+ v = version("d0fus")
187
+ except PackageNotFoundError:
188
+ v = "dev"
189
+ except ImportError:
190
+ v = "dev"
191
+
192
+ # The version is the only dynamic line; the others are hardcoded for clarity.
193
+ # Inside width is 51 characters; the version is centered programmatically.
194
+ version_line = f"Version {v}".center(51)
195
+
196
+ banner = f"""
175
197
  ╔═══════════════════════════════════════════════════╗
176
198
  ║ ║
177
199
  ║ D0FUS ║
178
- Design 0-dimensional for Fusion Systems
200
+ Design 0-dimensional for Fusion Systems
201
+ ║ ║
202
+ ╠═══════════════════════════════════════════════════╣
203
+ ║{version_line}║
204
+ ║ T. Auclair, CEA-IRFM | CeCILL-C ║
205
+ ║ https://github.com/IRFM/D0FUS ║
179
206
  ║ ║
180
207
  ╚═══════════════════════════════════════════════════╝
181
208
  """
@@ -49,6 +49,7 @@ if __name__ != "__main__":
49
49
  f_He_fraction,
50
50
  f_q_profile,
51
51
  f_sigmav,
52
+ _two_point_core, M_F_DT,
52
53
  )
53
54
  from .D0FUS_radial_build_functions import (
54
55
  J_non_Cu_NbTi, J_non_Cu_Nb3Sn, J_non_Cu_REBCO,
@@ -59,6 +60,7 @@ if __name__ != "__main__":
59
60
  f_CS_ACAD, f_CS_refined, f_CS_CIRCE,
60
61
  F_CIRCE0D, compute_von_mises_stress,
61
62
  calculate_E_mag_TF,
63
+ Number_TF_coils,
62
64
  )
63
65
  from .D0FUS_parameterization import DEFAULT_CONFIG, E_ELEM
64
66
 
@@ -78,6 +80,7 @@ else:
78
80
  f_He_fraction,
79
81
  f_q_profile,
80
82
  f_sigmav,
83
+ _two_point_core, M_F_DT,
81
84
  )
82
85
  from D0FUS_BIB.D0FUS_radial_build_functions import (
83
86
  J_non_Cu_NbTi, J_non_Cu_Nb3Sn, J_non_Cu_REBCO,
@@ -88,6 +91,7 @@ else:
88
91
  f_CS_ACAD, f_CS_refined, f_CS_CIRCE,
89
92
  F_CIRCE0D, compute_von_mises_stress,
90
93
  calculate_E_mag_TF,
94
+ Number_TF_coils,
91
95
  )
92
96
  from D0FUS_BIB.D0FUS_parameterization import DEFAULT_CONFIG, E_ELEM
93
97
 
@@ -2214,6 +2218,85 @@ def plot_radiation_profile(
2214
2218
  _save_or_show(fig, save_dir, "run_radiation_profile")
2215
2219
 
2216
2220
 
2221
+ def plot_divertor_two_point(
2222
+ run: dict,
2223
+ n_pts: int = 300,
2224
+ save_dir: str | None = None,
2225
+ ) -> None:
2226
+ """
2227
+ Simple two-point-model view: target temperature versus SOL dissipation.
2228
+
2229
+ A single panel shows the target electron temperature T_et as a function of
2230
+ the SOL power-loss fraction f_cooling, at the converged upstream conditions
2231
+ (q_par_u, p_u read from the run). The dashed line is the detachment
2232
+ threshold T_et = 10 eV (Stangeby 2018). The flat plateau at high T_et is the
2233
+ attached sheath-limited branch where the two-point model no longer applies.
2234
+ The minimum dissipation required for target survival (Eq. 14) is given in
2235
+ the title and marked by the dotted vertical line.
2236
+
2237
+ Parameters
2238
+ ----------
2239
+ run : dict D0FUS run output; reads the 'divertor' sub-dict produced
2240
+ by f_heat_two_point.
2241
+ n_pts : int Number of f_cooling samples.
2242
+ save_dir : str or None
2243
+
2244
+ References
2245
+ ----------
2246
+ P.C. Stangeby, Plasma Phys. Control. Fusion 60 (2018) 044022.
2247
+ """
2248
+ div = run.get("divertor", {}) or {}
2249
+ if not {"q_par_u", "T_eu", "n_sep"}.issubset(div):
2250
+ print(" [skip] no two-point-model divertor solution in run dict")
2251
+ return
2252
+
2253
+ q_par_u = div["q_par_u"] # [MW/m^2]
2254
+ T_eu = div["T_eu"] # [eV]
2255
+ n_sep = div["n_sep"] # [m^-3]
2256
+ f_c_op = div.get("f_cooling", 0.0)
2257
+ f_m_op = div.get("f_mom", 0.0)
2258
+ T_et_op = div.get("T_et", np.nan)
2259
+ f_pwr_req = div.get("f_pwr_loss_req", np.nan)
2260
+ R0 = run.get("R0", "?")
2261
+
2262
+ q_par_u_SI = q_par_u * 1e6
2263
+ p_u = 2.0 * n_sep * E_ELEM * T_eu
2264
+
2265
+ f_c = np.linspace(0.0, 0.995, n_pts)
2266
+ T_et = np.array([min(_two_point_core(q_par_u_SI, p_u, fc, f_m_op, 7.0, M_F_DT)[0],
2267
+ T_eu) for fc in f_c])
2268
+
2269
+ fig, ax = plt.subplots(figsize=(7, 4.5))
2270
+ ax.plot(f_c, T_et, color="#4477AA", lw=2.4)
2271
+ ax.axhline(10.0, color="#EE6677", lw=1.3, ls="--")
2272
+ ax.text(0.015, 11.0, "detachment (10 eV)", color="#EE6677",
2273
+ fontsize=9, va="bottom")
2274
+ if np.isfinite(f_pwr_req):
2275
+ ax.axvline(f_pwr_req, color="0.55", lw=1.0, ls=":")
2276
+ if np.isfinite(T_et_op):
2277
+ ax.plot([f_c_op], [T_et_op], "o", ms=9, color="k", zorder=5)
2278
+ near_left = f_c_op < 0.5
2279
+ near_top = T_et_op > 0.3 * T_eu
2280
+ ax.annotate(f"operating point\n$T_{{e,t}}$ = {T_et_op:.1f} eV",
2281
+ (f_c_op, T_et_op), textcoords="offset points",
2282
+ xytext=(14 if near_left else -12, -16 if near_top else 14),
2283
+ ha="left" if near_left else "right",
2284
+ va="top" if near_top else "bottom", fontsize=9)
2285
+
2286
+ ax.set_yscale("log")
2287
+ ax.set_xlim(0, 1)
2288
+ ax.set_xlabel(r"SOL power-loss fraction $f_{\rm cooling}$", fontsize=12)
2289
+ ax.set_ylabel(r"Target electron temperature $T_{e,t}$ [eV]", fontsize=12)
2290
+ ttl = (rf"Divertor two-point model: $R_0$={R0} m, "
2291
+ rf"$q_{{\parallel u}}$={q_par_u/1e3:.2f} GW/m$^2$")
2292
+ if np.isfinite(f_pwr_req):
2293
+ ttl += f", required dissipation = {f_pwr_req:.2f}"
2294
+ ax.set_title(ttl, fontsize=11)
2295
+ ax.grid(True, which="both", alpha=0.3)
2296
+ plt.tight_layout()
2297
+ _save_or_show(fig, save_dir, "run_divertor_two_point")
2298
+
2299
+
2217
2300
  # ---------------------------------------------------------------------------
2218
2301
  # A — Convenience wrapper
2219
2302
  # ---------------------------------------------------------------------------
@@ -2998,23 +3081,24 @@ def plot_run(
2998
3081
  save_dir: str | None = None,
2999
3082
  ) -> None:
3000
3083
  """
3001
- Render the run-specific figure set (10 figures).
3084
+ Render the run-specific figure set (11 figures).
3002
3085
 
3003
3086
  This is the subset called after each D0FUS run. It contains only the
3004
3087
  figures that depend on the current run configuration and results —
3005
3088
  no validation curves, no benchmarks, no scaling-law surveys.
3006
3089
 
3007
3090
  Figures produced:
3008
- [ 1/10] Tokamak LCFS comparison (with D0FUS overlay)
3009
- [ 2/10] Miller flux surfaces (run geometry)
3010
- [ 3/10] Shaping profiles κ(ρ), δ(ρ)
3011
- [ 4/10] Kinetic profiles n(ρ), T(ρ), p(ρ)
3012
- [ 5/10] Safety factor q(ρ) and current decomposition
3013
- [ 6/10] Radiation profiles
3014
- [ 7/10] TF coil side view
3015
- [ 8/10] CICC TF conductor
3016
- [ 9/10] CS cross-section
3017
- [10/10] CICC CS conductor
3091
+ [ 1/11] Tokamak LCFS comparison (with D0FUS overlay)
3092
+ [ 2/11] Miller flux surfaces (run geometry)
3093
+ [ 3/11] Shaping profiles κ(ρ), δ(ρ)
3094
+ [ 4/11] Kinetic profiles n(ρ), T(ρ), p(ρ)
3095
+ [ 5/11] Safety factor q(ρ) and current decomposition
3096
+ [ 6/11] Radiation profiles
3097
+ [ 7/11] Divertor two-point model (detachment vs SOL dissipation)
3098
+ [ 8/11] TF coil side view
3099
+ [ 9/11] CICC TF conductor
3100
+ [10/11] CS cross-section
3101
+ [11/11] CICC CS conductor
3018
3102
 
3019
3103
  Parameters
3020
3104
  ----------
@@ -3023,7 +3107,7 @@ def plot_run(
3023
3107
  If provided, figures are saved as PNG files.
3024
3108
  Pass ``None`` to display interactively.
3025
3109
  """
3026
- N = 10
3110
+ N = 11
3027
3111
 
3028
3112
  def _p(i, label):
3029
3113
  print(f" [{i:2d}/{N}] {label}")
@@ -3051,17 +3135,20 @@ def plot_run(
3051
3135
  _p(6, "Radiation profiles")
3052
3136
  plot_radiation_profile(run, save_dir=save_dir)
3053
3137
 
3138
+ _p(7, "Divertor two-point model")
3139
+ plot_divertor_two_point(run, save_dir=save_dir)
3140
+
3054
3141
  # ── Coils & conductors ────────────────────────────────────────────
3055
- _p(7, "TF coil side view")
3142
+ _p(8, "TF coil side view")
3056
3143
  plot_TF_side_view(run, save_dir=save_dir)
3057
3144
 
3058
- _p(8, "CICC TF conductor")
3145
+ _p(9, "CICC TF conductor")
3059
3146
  plot_CICC_cross_section(build_conductor_from_run(run, coil="TF"), save_dir=save_dir)
3060
3147
 
3061
- _p(9, "CS cross-section")
3148
+ _p(10, "CS cross-section")
3062
3149
  plot_CS_cross_section(run, save_dir=save_dir)
3063
3150
 
3064
- _p(10, "CICC CS conductor")
3151
+ _p(11, "CICC CS conductor")
3065
3152
  plot_CICC_cross_section(build_conductor_from_run(run, coil="CS"), save_dir=save_dir)
3066
3153
 
3067
3154
  print("Done.")
@@ -3308,11 +3395,11 @@ def plot_TF_benchmark_table(cfg=None, save_dir=None) -> None:
3308
3395
  "ARC": {"a": 1.10, "b": 0.89, "R0": 3.30, "σ": 1000e6, "T_op": 20.0,
3309
3396
  "B_max": 23.0, "n_TF": 1, "sc": "REBCO", "config": "Plug",
3310
3397
  "κ": 1.84, "I_cond": 50e3, "V_max": 10e3, "N_sub": 6,
3311
- "tau_h": 20, "J_wost": 120e6},
3398
+ "tau_h": 20, "J_wost": 200e6},
3312
3399
  "SPARC": {"a": 0.57, "b": 0.18, "R0": 1.85, "σ": 1000e6, "T_op": 20.0,
3313
3400
  "B_max": 20.0, "n_TF": 1, "sc": "REBCO", "config": "Bucking",
3314
- "κ": 1.75, "I_cond": 40.5e3,"V_max": 10e3, "N_sub": 4,
3315
- "tau_h": 20, "J_wost": 120e6},
3401
+ "κ": 1.75, "I_cond": 40.5e3,"V_max": 10e3, "N_sub": 6,
3402
+ "tau_h": 20, "J_wost": 200e6},
3316
3403
  }
3317
3404
 
3318
3405
  def _clean(val):
@@ -4193,6 +4280,193 @@ def plot_CICC_cross_section(
4193
4280
  # Stand-alone execution — smoke test
4194
4281
  # =============================================================================
4195
4282
 
4283
+
4284
+ # =============================================================================
4285
+ # 7. TF coil number - ripple and port-access constraints
4286
+ # =============================================================================
4287
+
4288
+ # D0FUS-consistent palette (matches the hex codes used elsewhere in this file)
4289
+ _RIP_C_PLASMA = "#f4a582" # plasma fill
4290
+ _RIP_C_COIL = "#2166ac" # TF coil footprint
4291
+ _RIP_C_ACCESS = "#4dac26" # maintenance / heating access sector
4292
+
4293
+
4294
+ def plot_tf_ripple(
4295
+ R0: float = 6.2,
4296
+ a: float = 2.0,
4297
+ b: float = 1.2,
4298
+ ripple_adm: float = 0.01,
4299
+ L_min: float = 3.6,
4300
+ grid: int = 460,
4301
+ save_dir: str | None = None,
4302
+ ) -> None:
4303
+ """
4304
+ Top-view map of the toroidal field magnitude produced by the discrete TF
4305
+ coils, illustrating the field ripple.
4306
+
4307
+ The coil number, ripple and outboard standoff (N_coil, ripple, Delta_ext)
4308
+ are taken from the package routine ``Number_TF_coils`` so the figure stays
4309
+ consistent with the D0FUS sizing. Each coil is modelled as a pair of
4310
+ infinite straight filaments (inner leg at R0 - a - b carrying +I, outer leg
4311
+ at R0 + a + b + Delta_ext carrying -I, at the same toroidal angle); the field
4312
+ magnitude |B| is evaluated on a Cartesian grid in the toroidal mid-plane.
4313
+ The prefactor mu0*I/(2*pi) is dropped, so |B| is in arbitrary units. The
4314
+ iso-|B| contours scallop near the coils: that scalloping is the ripple.
4315
+
4316
+ Parameters
4317
+ ----------
4318
+ R0, a : float Major and minor radius [m].
4319
+ b : float Aggregated inboard build (FW + blanket + shield + gaps) [m].
4320
+ ripple_adm : float Admissible ripple fraction (default 1 %).
4321
+ L_min : float Minimum toroidal access per coil [m] (default 3.6 m, the
4322
+ ITER-reproducing value; the manuscript also uses 3.0 m).
4323
+ grid : int Number of grid points per axis for the field map.
4324
+ save_dir : str or None
4325
+
4326
+ References
4327
+ ----------
4328
+ Wesson, Tokamaks, 4th ed., p.169 - ripple model.
4329
+ Goldston & Rutherford, Introduction to Plasma Physics, IOP (1995), ch.14.
4330
+ """
4331
+ n_coil, ripple_sel, Delta_ext = Number_TF_coils(R0, a, b, ripple_adm, L_min)
4332
+ R_in = R0 - a - b
4333
+ R_out = R0 + a + b + Delta_ext
4334
+ theta = 2.0 * np.pi * np.arange(n_coil) / n_coil
4335
+
4336
+ # filament positions (inner + outer legs) and signed currents
4337
+ fx = np.concatenate([R_in * np.cos(theta), R_out * np.cos(theta)])
4338
+ fy = np.concatenate([R_in * np.sin(theta), R_out * np.sin(theta)])
4339
+ cur = np.concatenate([np.ones(n_coil), -np.ones(n_coil)])
4340
+
4341
+ # |B| on a Cartesian grid (loop over filaments to limit memory)
4342
+ L = R_out + 0.7
4343
+ gx = np.linspace(-L, L, grid)
4344
+ X, Y = np.meshgrid(gx, gx)
4345
+ Bx = np.zeros_like(X)
4346
+ By = np.zeros_like(X)
4347
+ dmin = np.full(X.shape, np.inf)
4348
+ for xi, yi, ci in zip(fx, fy, cur):
4349
+ dx = X - xi
4350
+ dy = Y - yi
4351
+ d2 = dx * dx + dy * dy
4352
+ Bx += ci * (-dy) / d2
4353
+ By += ci * (dx) / d2
4354
+ dmin = np.minimum(dmin, np.sqrt(d2))
4355
+ rr = np.sqrt(X ** 2 + Y ** 2)
4356
+ Bmag = np.sqrt(Bx ** 2 + By ** 2)
4357
+ # cap the colour scale to the plasma-region field, then hide near-wire
4358
+ # spikes and the central column for legibility
4359
+ vmax = np.percentile(Bmag[(rr > R0 - a) & (rr < R0 + a) & (dmin > 0.22)], 99)
4360
+ Bmag = np.ma.array(Bmag, mask=(dmin < 0.22) | (rr < R0 - a))
4361
+
4362
+ th = np.linspace(0, 2 * np.pi, 400)
4363
+ fig, ax = plt.subplots(figsize=(6.4, 5.9))
4364
+ ax.set_aspect("equal")
4365
+ pcm = ax.pcolormesh(X, Y, Bmag, shading="auto", cmap="magma", vmin=0, vmax=vmax)
4366
+ ax.contour(X, Y, Bmag, levels=np.linspace(0.35 * vmax, vmax, 7),
4367
+ colors="white", linewidths=0.45, alpha=0.55)
4368
+ ax.add_patch(mpatches.Circle((0, 0), R0 - a, facecolor="0.75",
4369
+ edgecolor="0.5", lw=0.8, zorder=4))
4370
+ ax.text(0, 0, "central\ncolumn", ha="center", va="center", fontsize=8,
4371
+ color="0.25", zorder=5)
4372
+ ax.plot((R0 + a) * np.cos(th), (R0 + a) * np.sin(th), color="cyan",
4373
+ lw=1.3, ls="--", alpha=0.9, zorder=4)
4374
+ ax.plot(R_out * np.cos(th), R_out * np.sin(th), color="white", lw=0.7,
4375
+ ls=":", alpha=0.5, zorder=4)
4376
+ ax.plot(R_out * np.cos(theta), R_out * np.sin(theta), "o", color="white",
4377
+ ms=5, mec="k", mew=0.5, zorder=5)
4378
+ ax.text(0, (R0 + a) + 0.15, "plasma edge", color="cyan", fontsize=8,
4379
+ ha="center", va="bottom", zorder=6)
4380
+ ax.text(-L + 0.3, L - 0.5,
4381
+ rf"$\delta_\mathrm{{ripple}} = {ripple_sel * 100:.2f}\,\%$",
4382
+ color="white", fontsize=11, va="top")
4383
+ ax.set_xlim(-L, L)
4384
+ ax.set_ylim(-L, L)
4385
+ ax.set_xlabel("x [m]", fontsize=12)
4386
+ ax.set_ylabel("y [m]", fontsize=12)
4387
+ ax.set_title(rf"Toroidal field magnitude, top view ($N_\mathrm{{coil}} = {n_coil}$)",
4388
+ fontsize=11)
4389
+ fig.colorbar(pcm, ax=ax, shrink=0.85, label=r"$|B|$ (arb. units)")
4390
+ plt.tight_layout()
4391
+ _save_or_show(fig, save_dir, "tf_ripple")
4392
+
4393
+
4394
+ def plot_port_access(
4395
+ R0: float = 6.2,
4396
+ a: float = 2.0,
4397
+ b: float = 1.2,
4398
+ ripple_adm: float = 0.01,
4399
+ L_min: float = 3.6,
4400
+ save_dir: str | None = None,
4401
+ ) -> None:
4402
+ """
4403
+ Clean top view of the tokamak showing the TF coils and the angular sectors
4404
+ left free between them for maintenance and heating access.
4405
+
4406
+ The coil number and outboard standoff come from ``Number_TF_coils`` (same
4407
+ call as plot_tf_ripple), so the layout matches the D0FUS sizing.
4408
+
4409
+ Parameters
4410
+ ----------
4411
+ R0, a, b : float Plasma geometry and aggregated inboard build [m].
4412
+ ripple_adm : float Admissible ripple fraction (for the N_coil selection).
4413
+ L_min : float Minimum toroidal pitch per coil for maintenance [m].
4414
+ save_dir : str or None
4415
+ """
4416
+ n_coil, ripple_sel, Delta_ext = Number_TF_coils(R0, a, b, ripple_adm, L_min)
4417
+ R_out = R0 + a + b + Delta_ext
4418
+ theta_deg = np.degrees(2.0 * np.pi * np.arange(n_coil) / n_coil)
4419
+ pitch = 360.0 / n_coil
4420
+ coil_hw = 0.30 * pitch # illustrative coil half-width
4421
+ band = (R_out + 0.6) - (R0 + a)
4422
+
4423
+ fig, ax = plt.subplots(figsize=(6.2, 6.0))
4424
+ ax.set_aspect("equal")
4425
+
4426
+ # plasma annulus + central column
4427
+ ax.add_patch(mpatches.Wedge((0, 0), R0 + a, 0, 360, width=2 * a,
4428
+ facecolor=_RIP_C_PLASMA, alpha=0.45, lw=0, zorder=1))
4429
+ ax.add_patch(mpatches.Circle((0, 0), R0 - a, facecolor="0.85",
4430
+ edgecolor="0.6", lw=0.8, zorder=2))
4431
+ ax.text(0, 0, "central\ncolumn", ha="center", va="center", fontsize=8.5,
4432
+ color="0.3", zorder=3)
4433
+ ax.text(0, -R0, "plasma", ha="center", va="center", fontsize=9,
4434
+ color="#9c4a2f", zorder=3,
4435
+ bbox=dict(boxstyle="round,pad=0.15", fc="white", ec="none", alpha=0.7))
4436
+
4437
+ # access corridors (gaps) and coil footprints
4438
+ for t in theta_deg:
4439
+ ax.add_patch(mpatches.Wedge((0, 0), R_out + 0.6, t + coil_hw,
4440
+ t + pitch - coil_hw, width=band,
4441
+ facecolor=_RIP_C_ACCESS, alpha=0.16, lw=0,
4442
+ zorder=2))
4443
+ ax.add_patch(mpatches.Wedge((0, 0), R_out + 0.6, t - coil_hw,
4444
+ t + coil_hw, width=1.7,
4445
+ facecolor=_RIP_C_COIL, edgecolor="none",
4446
+ alpha=0.95, zorder=3))
4447
+
4448
+ # single neutral label on one access sector
4449
+ tlab = theta_deg[0] + pitch / 2
4450
+ r_lab = R0 + a + 0.9
4451
+ ax.annotate("maintenance /\nheating access",
4452
+ xy=(r_lab * np.cos(np.radians(tlab)), r_lab * np.sin(np.radians(tlab))),
4453
+ xytext=(1.12 * R_out, 1.02 * R_out), fontsize=9, color="#2f6b16",
4454
+ ha="left", arrowprops=dict(arrowstyle="->", color="#2f6b16", lw=1.0))
4455
+
4456
+ ax.plot([], [], color=_RIP_C_COIL, lw=6, label=rf"TF coils ($N = {n_coil}$)")
4457
+ ax.plot([], [], color=_RIP_C_ACCESS, lw=6, alpha=0.4, label="access sectors")
4458
+ lim = R_out + 1.6
4459
+ ax.set_xlim(-lim, lim)
4460
+ ax.set_ylim(-lim, lim)
4461
+ ax.set_xlabel("x [m]", fontsize=12)
4462
+ ax.set_ylabel("y [m]", fontsize=12)
4463
+ ax.set_title(rf"TF coils and maintenance access, top view ($N_\mathrm{{coil}} = {n_coil}$)",
4464
+ fontsize=11)
4465
+ ax.legend(fontsize=9, loc="upper left")
4466
+ plt.tight_layout()
4467
+ _save_or_show(fig, save_dir, "port_access")
4468
+
4469
+
4196
4470
  if __name__ == "__main__":
4197
4471
 
4198
4472
  parser = argparse.ArgumentParser(
@@ -18,11 +18,13 @@ os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
18
18
 
19
19
  import json
20
20
  import math
21
+ import multiprocessing
21
22
  import random
22
23
  import re
23
24
  import shutil
24
25
  import sys
25
26
  import time
27
+ import traceback
26
28
  import warnings
27
29
  import importlib
28
30
  from datetime import datetime
@@ -34,7 +36,7 @@ import numpy as np
34
36
  import pandas as pd
35
37
  import sympy as sp
36
38
  from typing import List, Tuple
37
- from dataclasses import dataclass
39
+ from dataclasses import dataclass, replace, asdict
38
40
 
39
41
  #%% Scipy - Optimization and Numerical Methods
40
42
 
@@ -66,11 +68,13 @@ import matplotlib.colors as mcolors
66
68
  import matplotlib.lines as mlines
67
69
  import matplotlib.patches as mpatches
68
70
  import matplotlib.cm as cm
71
+ from matplotlib.animation import FuncAnimation, PillowWriter
69
72
  from matplotlib.colors import Normalize
70
73
  from matplotlib.gridspec import GridSpec
71
74
  from matplotlib.patches import Circle, Rectangle, Patch
72
75
  from matplotlib.ticker import MultipleLocator
73
76
  from mpl_toolkits.axes_grid1 import make_axes_locatable
77
+ from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (registers the '3d' projection)
74
78
  from pandas.plotting import table
75
79
  from tqdm import tqdm
76
80
  from dataclasses import dataclass
@@ -97,7 +97,10 @@ class GlobalConfig:
97
97
  Bmax_TF : float = 12.0 # Peak magnetic field on TF conductor [T]
98
98
  Bmax_CS_adm : float = 25.0 # Peak magnetic field allowed on the CS [T]
99
99
  P_fus : float = 2000.0 # Total fusion power [MW]
100
- Tbar : float = 14.0 # Volume-averaged ion temperature [keV]
100
+ Tbar : float = 14.0 # Volume-averaged electron temperature T_e [keV]
101
+ tau_i_e : float = 1.0 # Ion-to-electron temperature ratio T_i/T_e [-]
102
+ # 1.0 -> single-temperature plasma (T_i = T_e).
103
+ # Prescribed: T_i(rho) = tau_i_e * T_e(rho),
101
104
  H : float = 1.0 # Confinement enhancement factor (H-factor) [-]
102
105
 
103
106
  # ── 2. Physics and operation ─────────────────────────────────────────────
@@ -122,7 +125,7 @@ class GlobalConfig:
122
125
  # 'Sauter' — uses LCFS values (κ_edge, δ_edge). Sauter, FED 112 (2016) Eq. 30.
123
126
  # 'ITER_1989' — uses ψ_N = 0.95 values (κ₉₅, δ₉₅). Uckan (1989), also Johner (2011).
124
127
  Option_q95 : str = 'Sauter' # q₉₅ formula: 'Sauter' (default) or 'ITER_1989'
125
- Option_Kappa : str = 'Wenninger' # Elongation model: 'Wenninger', 'Stambaugh', 'Freidberg', 'Manual'
128
+ Option_Kappa : str = 'Wenninger' # Elongation model: 'Wenninger', 'Stambaugh', 'Freidberg', 'Manual'
126
129
  κ_manual : float = 1.9 # Elongation (Manual mode only) [-]
127
130
 
128
131
  # ── 2a. Safety factor and current density profiles ────────────────────────
@@ -169,11 +172,20 @@ class GlobalConfig:
169
172
  # kink_parameter='q95' → q_limit ≈ 3.0–3.5 (ITER/EU-DEMO practice)
170
173
  q_limit : float = 3.0 # Kink safety factor threshold [-]
171
174
  Greenwald_limit : float = 1.0 # Greenwald density fraction limit [-]
175
+ Ip_limit : float = None # Upper bound on plasma current [MA]; None = no ceiling (GA)
172
176
  ms : float = 0.3 # Vertical stability margin parameter [-]
173
177
 
174
178
  # ── 4. Plasma composition ────────────────────────────────────────────────
175
179
  Atomic_mass : float = 2.5 # Volume-averaged ionic mass [AMU] (D-T: 2.5)
176
- Zeff : float = 2.0 # Effective plasma charge [-]
180
+ Zeff : float = None # Effective plasma charge [-].
181
+ # None -> computed self-consistently from the impurity
182
+ # inventory (impurity_species + f_imp_core) and the
183
+ # helium ash fraction (see _compute_Zeff_effective):
184
+ # Z_eff = 1 + 2 f_He - sum_j <Z_j> c_j
185
+ # + sum_j <Z_j>^2 c_j
186
+ # An empty impurity inventory therefore yields a clean
187
+ # D-T+He plasma with Z_eff ~ 1 + 2 f_He (~1.1).
188
+ # float -> manual override (legacy behaviour, e.g. Zeff = 2.0).
177
189
  r_synch : float = 0.5 # Synchrotron radiation wall reflectivity [-]
178
190
  C_Alpha : float = 7.0 # Helium ash dilution tuning parameter [-] (default taken from PROCESS)
179
191
  # Impurity line radiation (0D bulk-plasma estimate).
@@ -215,6 +227,26 @@ class GlobalConfig:
215
227
  Young_modul_Steel : float = 200e9 # Steel Young's modulus [Pa] (only used in CIRCE model)
216
228
  Young_modul_GF : float = 90e9 # S-glass fiber Young's modulus [Pa] (only used in CIRCE model)
217
229
  fatigue_CS : float = 2.0 # CS fatigue knockdown factor (pulsed & wedging only) [-]
230
+ # ── Steel allowable safety factors ───────────────────────────────────────
231
+ # Dimensionless knockdown applied to the steel mechanical allowable inside
232
+ # the TF and CS thickness solvers: σ_eff = σ_allowable / SF.
233
+ # Captures effects not represented by the idealised CICC area model:
234
+ # – Realistic winding-pack filling factor: round/square cables inside a
235
+ # rectangular envelope leave 15–30% of the WP cross-section occupied
236
+ # by ground insulation, inter-pancake plates, helium manifolds,
237
+ # terminations and assembly clearances. The structural jacket carries
238
+ # the electromagnetic load through a section reduced by this factor.
239
+ # – Stress concentrations at jacket corners and transitions (K_t ≈ 1.2–1.5).
240
+ # – Weld efficiency on jacket seams (η_w ≈ 0.85–0.90).
241
+ # – Manufacturing thickness tolerances (≈ ±5%).
242
+ # Default 1.0 preserves backward compatibility (raw material allowable used
243
+ # directly). Realistic engineering value for both TF and CS large-magnet
244
+ # CICC designs is ≈ 1.5 (composition of the four contributions above).
245
+ # The CS fatigue knockdown (fatigue_CS) multiplies on top of SF_CS, since
246
+ # the two factors address disjoint phenomena: primary static stress (SF)
247
+ # versus cyclic damage accumulation (fatigue_CS).
248
+ SF_TF : float = 1.0 # Safety factor on TF steel allowable [-] (suggested ≈ 1.5 for realistic CICC packing)
249
+ SF_CS : float = 1.0 # Safety factor on CS steel allowable [-] (suggested ≈ 1.5 for realistic CICC packing)
218
250
 
219
251
  # ── 7. TF coil engineering ───────────────────────────────────────────────
220
252
  Radial_build_model : str = 'refined' # Stress model: 'academic', 'refined', 'CIRCE'
@@ -257,7 +289,11 @@ class GlobalConfig:
257
289
  f_void : float = None # Interstitial void fraction in strand bundle [-]
258
290
  # None = auto: 0.33 (LTS) / 0.00 (REBCO).
259
291
  # Set explicitly (e.g. 0.30) to override.
260
- f_In : float = 0.15 # Insulation area fraction [-]
292
+ f_In : float = 0.05 # Insulation area fraction [-]
293
+ # Winding-pack overhead fraction in wost, treated exactly like the
294
+ # helium fraction: ground and inter-pancake insulation plus assembly
295
+ # clearances that occupy volume but carry neither current nor load.
296
+ f_gap : float = 0.15 # WP insulation + clearance fraction in wost [-]
261
297
 
262
298
  # Temperature margins above T_helium defining T_operating [K]
263
299
  # Conservative baseline: Corato et al., "Common operating values for DEMO…" (2016)
@@ -297,7 +333,7 @@ class GlobalConfig:
297
333
  # P_CD_total = P_LH + P_ECRH + P_NBI + P_ICRH (P_aux_input ignored).
298
334
  P_LH : float = 0.0 # LHCD plasma power [MW]
299
335
  P_ECRH : float = 0.0 # ECRH plasma power [MW]
300
- P_NBI : float = 0.0 # NBI injected power [MW] (5 % losses applied internally)
336
+ P_NBI : float = 0.0 # NBI injected power[MW]
301
337
  P_ICRH : float = 0.0 # ICRH plasma power [MW] (heating only, no current drive)
302
338
  # --- Steady-State mode: power fractions per source [-] ---
303
339
  # Used when Operation_mode = 'Steady-State' and CD_source = 'Multi'
@@ -325,6 +361,16 @@ class GlobalConfig:
325
361
  # ── 12. Plasma-facing components ─────────────────────────────────────────
326
362
  theta_deg : float = 2.7 # Divertor strike-point grazing angle [deg]
327
363
 
364
+ # Refined divertor exhaust (two-point model, Stangeby 2018). All used only
365
+ # in the post-convergence f_heat_two_point evaluation; they do not feed the
366
+ # core power balance.
367
+ f_n_sep : float = 0.20 # n_sep / volume-average density n̄ [-].
368
+ # Groth value taken from https://arxiv.org/pdf/2406.15693
369
+ f_cooling_div : float = 0.0 # SOL volumetric power-loss fraction [-] (2PM input)
370
+ f_mom_div : float = 0.0 # SOL volumetric momentum-loss fraction [-] (2PM input)
371
+ q_dep_limit : float = 5.0 # Tolerable deposited target heat flux [MW/m²]
372
+ flux_expansion: float = 1.0 # Target/upstream flux expansion R_t/R_u [-]
373
+
328
374
  # ── 13. Maintenance constraints ──────────────────────────────────────────
329
375
  ripple_adm : float = 0.01 # Admissible toroidal field ripple [-] (default 1%)
330
376
  L_min : float = 3.00 # Minimum toroidal maintenance access width [m]