d0fus 2.3.0__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.
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS_BIB/D0FUS_figures.py +189 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS_BIB/D0FUS_parameterization.py +2 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS_EXE/D0FUS_genetic.py +45 -17
- {d0fus-2.3.0 → d0fus-2.3.2}/PKG-INFO +1 -1
- {d0fus-2.3.0 → d0fus-2.3.2}/d0fus.egg-info/PKG-INFO +1 -1
- {d0fus-2.3.0 → d0fus-2.3.2}/pyproject.toml +1 -1
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS.py +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS_BIB/D0FUS_cost_data.py +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS_BIB/D0FUS_cost_functions.py +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS_BIB/D0FUS_import.py +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS_BIB/D0FUS_physical_functions.py +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS_BIB/D0FUS_radial_build_functions.py +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS_EXE/D0FUS_run.py +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/D0FUS_EXE/D0FUS_scan.py +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/LICENSE +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/README.md +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/d0fus.egg-info/SOURCES.txt +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/d0fus.egg-info/dependency_links.txt +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/d0fus.egg-info/entry_points.txt +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/d0fus.egg-info/requires.txt +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/d0fus.egg-info/top_level.txt +0 -0
- {d0fus-2.3.0 → d0fus-2.3.2}/setup.cfg +0 -0
|
@@ -60,6 +60,7 @@ if __name__ != "__main__":
|
|
|
60
60
|
f_CS_ACAD, f_CS_refined, f_CS_CIRCE,
|
|
61
61
|
F_CIRCE0D, compute_von_mises_stress,
|
|
62
62
|
calculate_E_mag_TF,
|
|
63
|
+
Number_TF_coils,
|
|
63
64
|
)
|
|
64
65
|
from .D0FUS_parameterization import DEFAULT_CONFIG, E_ELEM
|
|
65
66
|
|
|
@@ -90,6 +91,7 @@ else:
|
|
|
90
91
|
f_CS_ACAD, f_CS_refined, f_CS_CIRCE,
|
|
91
92
|
F_CIRCE0D, compute_von_mises_stress,
|
|
92
93
|
calculate_E_mag_TF,
|
|
94
|
+
Number_TF_coils,
|
|
93
95
|
)
|
|
94
96
|
from D0FUS_BIB.D0FUS_parameterization import DEFAULT_CONFIG, E_ELEM
|
|
95
97
|
|
|
@@ -4278,6 +4280,193 @@ def plot_CICC_cross_section(
|
|
|
4278
4280
|
# Stand-alone execution — smoke test
|
|
4279
4281
|
# =============================================================================
|
|
4280
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
|
+
|
|
4281
4470
|
if __name__ == "__main__":
|
|
4282
4471
|
|
|
4283
4472
|
parser = argparse.ArgumentParser(
|
|
@@ -101,6 +101,7 @@ class GlobalConfig:
|
|
|
101
101
|
tau_i_e : float = 1.0 # Ion-to-electron temperature ratio T_i/T_e [-]
|
|
102
102
|
# 1.0 -> single-temperature plasma (T_i = T_e).
|
|
103
103
|
# Prescribed: T_i(rho) = tau_i_e * T_e(rho),
|
|
104
|
+
H : float = 1.0 # Confinement enhancement factor (H-factor) [-]
|
|
104
105
|
|
|
105
106
|
# ── 2. Physics and operation ─────────────────────────────────────────────
|
|
106
107
|
Operation_mode : str = 'Pulsed' # 'Steady-State' or 'Pulsed'
|
|
@@ -171,6 +172,7 @@ class GlobalConfig:
|
|
|
171
172
|
# kink_parameter='q95' → q_limit ≈ 3.0–3.5 (ITER/EU-DEMO practice)
|
|
172
173
|
q_limit : float = 3.0 # Kink safety factor threshold [-]
|
|
173
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)
|
|
174
176
|
ms : float = 0.3 # Vertical stability margin parameter [-]
|
|
175
177
|
|
|
176
178
|
# ── 4. Plasma composition ────────────────────────────────────────────────
|
|
@@ -395,7 +395,10 @@ def _summarize_cloud_per_gen(ga_cloud):
|
|
|
395
395
|
|
|
396
396
|
def compute_stability_penalty(nbar_line, nG, betaT, betaN, q_kink,
|
|
397
397
|
q_min=DEFAULT_CONFIG.q_limit,
|
|
398
|
+
betaN_limit=DEFAULT_CONFIG.betaN_limit,
|
|
399
|
+
Ip=None, Ip_limit=None,
|
|
398
400
|
penalty_step=2.0,
|
|
401
|
+
penalty_lin=10.0,
|
|
399
402
|
penalty_slope=20.0,
|
|
400
403
|
margin_width=0.05,
|
|
401
404
|
margin_amplitude=0.15):
|
|
@@ -425,12 +428,12 @@ def compute_stability_penalty(nbar_line, nG, betaT, betaN, q_kink,
|
|
|
425
428
|
fitness is multiplied by 1.15 (a 15% cost surcharge).
|
|
426
429
|
At 5% margin, penalty → 0 (no surcharge).
|
|
427
430
|
|
|
428
|
-
penalty_per_constraint (violation) = penalty_step + penalty_slope × excess²
|
|
431
|
+
penalty_per_constraint (violation) = penalty_step + penalty_lin × excess + penalty_slope × excess²
|
|
429
432
|
penalty_per_constraint (margin zone) = margin_amplitude × (1 − margin)³
|
|
430
433
|
|
|
431
434
|
Examples (per constraint, violation side):
|
|
432
435
|
At boundary (ε → 0⁺): 2.0 → multiplier ≈ 3.0
|
|
433
|
-
At 10% excess: 2.0 + 0.2 =
|
|
436
|
+
At 10% excess: 2.0 + 1.0 + 0.2 = 3.2 → multiplier ≈ 4.2
|
|
434
437
|
|
|
435
438
|
Examples (per constraint, margin zone):
|
|
436
439
|
At 0% margin (just feasible): 0.15 → multiplier ≈ 1.15
|
|
@@ -456,6 +459,11 @@ def compute_stability_penalty(nbar_line, nG, betaT, betaN, q_kink,
|
|
|
456
459
|
penalty_step : float
|
|
457
460
|
Discontinuity cost at the stability boundary. A value of 2.0
|
|
458
461
|
means the fitness is multiplied by ≥ 3 for any violation.
|
|
462
|
+
penalty_lin : float
|
|
463
|
+
Linear growth coefficient beyond the boundary. Provides a non-zero
|
|
464
|
+
slope AT the boundary so a barely-infeasible design is pulled back to
|
|
465
|
+
the limit; must exceed ~3x the cost elasticity to remove the infeasible
|
|
466
|
+
local minimum.
|
|
459
467
|
penalty_slope : float
|
|
460
468
|
Quadratic growth coefficient beyond the boundary.
|
|
461
469
|
margin_width : float
|
|
@@ -471,7 +479,7 @@ def compute_stability_penalty(nbar_line, nG, betaT, betaN, q_kink,
|
|
|
471
479
|
density_ratio = nbar_line / nG
|
|
472
480
|
if density_ratio > 1.0:
|
|
473
481
|
excess = density_ratio - 1.0
|
|
474
|
-
penalty = penalty_step + penalty_slope * excess ** 2
|
|
482
|
+
penalty = penalty_step + penalty_lin * excess + penalty_slope * excess ** 2
|
|
475
483
|
penalty_terms.append(penalty)
|
|
476
484
|
violations['density'] = {
|
|
477
485
|
'value': float(nbar_line), 'limit': float(nG),
|
|
@@ -481,26 +489,40 @@ def compute_stability_penalty(nbar_line, nG, betaT, betaN, q_kink,
|
|
|
481
489
|
margin_used = (density_ratio - (1.0 - margin_width)) / margin_width
|
|
482
490
|
penalty_terms.append(margin_amplitude * margin_used ** 3)
|
|
483
491
|
|
|
484
|
-
#
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
beta_ratio = betaT_percent / betaN
|
|
492
|
+
# Troyon beta limit: normalised beta_N must stay below betaN_limit
|
|
493
|
+
if betaN_limit > 0:
|
|
494
|
+
beta_ratio = betaN / betaN_limit
|
|
488
495
|
if beta_ratio > 1.0:
|
|
489
496
|
excess = beta_ratio - 1.0
|
|
490
|
-
penalty = penalty_step + penalty_slope * excess ** 2
|
|
497
|
+
penalty = penalty_step + penalty_lin * excess + penalty_slope * excess ** 2
|
|
491
498
|
penalty_terms.append(penalty)
|
|
492
499
|
violations['beta'] = {
|
|
493
|
-
'value': float(
|
|
500
|
+
'value': float(betaN), 'limit': float(betaN_limit),
|
|
494
501
|
'ratio': float(beta_ratio)
|
|
495
502
|
}
|
|
496
503
|
elif beta_ratio > (1.0 - margin_width):
|
|
497
504
|
margin_used = (beta_ratio - (1.0 - margin_width)) / margin_width
|
|
498
505
|
penalty_terms.append(margin_amplitude * margin_used ** 3)
|
|
499
506
|
|
|
507
|
+
# Plasma-current ceiling: Ip must stay below Ip_limit (disruption headroom)
|
|
508
|
+
if Ip is not None and isinstance(Ip_limit, (int, float)) and Ip_limit > 0:
|
|
509
|
+
ip_ratio = Ip / Ip_limit
|
|
510
|
+
if ip_ratio > 1.0:
|
|
511
|
+
excess = ip_ratio - 1.0
|
|
512
|
+
penalty = penalty_step + penalty_lin * excess + penalty_slope * excess ** 2
|
|
513
|
+
penalty_terms.append(penalty)
|
|
514
|
+
violations['Ip'] = {
|
|
515
|
+
'value': float(Ip), 'limit': float(Ip_limit),
|
|
516
|
+
'ratio': float(ip_ratio)
|
|
517
|
+
}
|
|
518
|
+
elif ip_ratio > (1.0 - margin_width):
|
|
519
|
+
margin_used = (ip_ratio - (1.0 - margin_width)) / margin_width
|
|
520
|
+
penalty_terms.append(margin_amplitude * margin_used ** 3)
|
|
521
|
+
|
|
500
522
|
# Kink safety factor: q_kink > q_min (q_kink = q* or q95)
|
|
501
523
|
if q_kink < q_min:
|
|
502
524
|
deficit = (q_min - q_kink) / q_min
|
|
503
|
-
penalty = penalty_step + penalty_slope * deficit ** 2
|
|
525
|
+
penalty = penalty_step + penalty_lin * deficit + penalty_slope * deficit ** 2
|
|
504
526
|
penalty_terms.append(penalty)
|
|
505
527
|
violations['q_kink'] = {
|
|
506
528
|
'value': float(q_kink), 'limit': float(q_min),
|
|
@@ -609,6 +631,7 @@ def evaluate_individual(individual, verbose=False):
|
|
|
609
631
|
R0_abcd = _safe_real(output[_IDX['r_d']])
|
|
610
632
|
c_TF = _safe_real(output[_IDX['c']])
|
|
611
633
|
d_CS = _safe_real(output[_IDX['d']])
|
|
634
|
+
Ip = _safe_real(output[_IDX['Ip']])
|
|
612
635
|
|
|
613
636
|
# Select kink parameter (q* or q95) for stability checks.
|
|
614
637
|
_kink_param = static_inputs.get('kink_parameter', DEFAULT_CONFIG.kink_parameter)
|
|
@@ -628,7 +651,9 @@ def evaluate_individual(individual, verbose=False):
|
|
|
628
651
|
q_lim = static_inputs.get('q_limit', DEFAULT_CONFIG.q_limit)
|
|
629
652
|
is_stable, penalty_multiplier, violations = compute_stability_penalty(
|
|
630
653
|
nbar_line, nG, betaT, betaN, q_kink,
|
|
631
|
-
q_min=q_lim
|
|
654
|
+
q_min=q_lim,
|
|
655
|
+
betaN_limit=static_inputs.get('betaN_limit', DEFAULT_CONFIG.betaN_limit),
|
|
656
|
+
Ip=Ip, Ip_limit=static_inputs.get('Ip_limit', DEFAULT_CONFIG.Ip_limit)
|
|
632
657
|
)
|
|
633
658
|
|
|
634
659
|
# ── Compute fitness value based on selected objective ────────────
|
|
@@ -2602,11 +2627,11 @@ def run_genetic_optimization(input_file,
|
|
|
2602
2627
|
|
|
2603
2628
|
# Check stability (using line-averaged density for Greenwald comparison)
|
|
2604
2629
|
q_lim = static_inputs.get('q_limit', DEFAULT_CONFIG.q_limit)
|
|
2630
|
+
betaN_lim = static_inputs.get('betaN_limit', DEFAULT_CONFIG.betaN_limit)
|
|
2631
|
+
Ip_lim = static_inputs.get('Ip_limit', DEFAULT_CONFIG.Ip_limit)
|
|
2605
2632
|
is_stable, _, violations = compute_stability_penalty(
|
|
2606
|
-
nbar_line, nG, betaT, betaN, q_kink, q_min=q_lim
|
|
2607
|
-
|
|
2608
|
-
# betaT is fraction, convert to % for display
|
|
2609
|
-
betaT_percent = betaT * 100
|
|
2633
|
+
nbar_line, nG, betaT, betaN, q_kink, q_min=q_lim, betaN_limit=betaN_lim,
|
|
2634
|
+
Ip=Ip, Ip_limit=Ip_lim)
|
|
2610
2635
|
|
|
2611
2636
|
# Compute Sheffield COE for the best design (regardless of objective)
|
|
2612
2637
|
_COE_best = np.nan
|
|
@@ -2650,7 +2675,9 @@ def run_genetic_optimization(input_file,
|
|
|
2650
2675
|
print(f" Ip: {Ip:.2f} MA")
|
|
2651
2676
|
print(f" P_elec: {P_elec:.1f} MW")
|
|
2652
2677
|
print(f" n_line/nG: {nbar_line/nG:.3f} ({(1-nbar_line/nG)*100:+.1f}% margin)")
|
|
2653
|
-
print(f"
|
|
2678
|
+
print(f" betaN/betaN_limit: {betaN/betaN_lim:.3f} ({(1-betaN/betaN_lim)*100:+.1f}% margin)")
|
|
2679
|
+
if Ip_lim is not None:
|
|
2680
|
+
print(f" Ip/Ip_limit: {Ip/Ip_lim:.3f} ({(1-Ip/Ip_lim)*100:+.1f}% margin)")
|
|
2654
2681
|
q_lim = static_inputs.get('q_limit', DEFAULT_CONFIG.q_limit)
|
|
2655
2682
|
print(f" {_q_label}/q_limit: {q_kink/q_lim:.3f} ({(q_kink/q_lim-1)*100:+.1f}% margin)")
|
|
2656
2683
|
print(f" c_TF: {c_TF:.3f} m d_CS: {d_CS:.3f} m r_d: {r_d:.3f} m")
|
|
@@ -2764,7 +2791,8 @@ def run_genetic_optimization(input_file,
|
|
|
2764
2791
|
"n_line_over_nG": to_serializable(nbar_line/nG),
|
|
2765
2792
|
"q_kink_over_qlim": to_serializable(q_kink/q_lim),
|
|
2766
2793
|
"kink_parameter": _kink_param,
|
|
2767
|
-
"
|
|
2794
|
+
"betaN_over_betaN_limit": to_serializable(betaN/betaN_lim) if betaN_lim > 0 else None,
|
|
2795
|
+
"Ip_over_Ip_limit": to_serializable(Ip/Ip_lim) if Ip_lim is not None else None
|
|
2768
2796
|
},
|
|
2769
2797
|
"radial_build": {
|
|
2770
2798
|
"c_TF_m": to_serializable(c_TF),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: d0fus
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: Design 0-dimensional for Fusion Systems - Tokamak power plant design and optimization tool
|
|
5
5
|
Author-email: Timothé Auclair <timothe.auclair@gmail.com>
|
|
6
6
|
Maintainer-email: Timothé Auclair <timothe.auclair@gmail.com>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: d0fus
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: Design 0-dimensional for Fusion Systems - Tokamak power plant design and optimization tool
|
|
5
5
|
Author-email: Timothé Auclair <timothe.auclair@gmail.com>
|
|
6
6
|
Maintainer-email: Timothé Auclair <timothe.auclair@gmail.com>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "d0fus"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.2"
|
|
8
8
|
description = "Design 0-dimensional for Fusion Systems - Tokamak power plant design and optimization tool"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "CeCILL-C"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|