d0fus 2.3.3__tar.gz → 2.3.4__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.3 → d0fus-2.3.4}/D0FUS.py +12 -1
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_figures.py +199 -22
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_import.py +25 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_parameterization.py +34 -1
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_physical_functions.py +670 -5
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_radial_build_functions.py +119 -2
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_EXE/D0FUS_genetic.py +11 -7
- d0fus-2.3.4/D0FUS_EXE/D0FUS_popcon.py +468 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_EXE/D0FUS_run.py +165 -17
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_EXE/D0FUS_scan.py +8 -5
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_EXE/D0FUS_uncertainty.py +220 -6
- {d0fus-2.3.3/d0fus.egg-info → d0fus-2.3.4}/PKG-INFO +1 -1
- {d0fus-2.3.3 → d0fus-2.3.4/d0fus.egg-info}/PKG-INFO +1 -1
- {d0fus-2.3.3 → d0fus-2.3.4}/d0fus.egg-info/SOURCES.txt +1 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/pyproject.toml +1 -1
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_cost_data.py +0 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_cost_functions.py +0 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/LICENSE +0 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/README.md +0 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/d0fus.egg-info/dependency_links.txt +0 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/d0fus.egg-info/entry_points.txt +0 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/d0fus.egg-info/requires.txt +0 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/d0fus.egg-info/top_level.txt +0 -0
- {d0fus-2.3.3 → d0fus-2.3.4}/setup.cfg +0 -0
|
@@ -20,7 +20,7 @@ sys.path.insert(0, project_root)
|
|
|
20
20
|
|
|
21
21
|
# Import all necessary modules
|
|
22
22
|
from D0FUS_BIB.D0FUS_parameterization import *
|
|
23
|
-
from D0FUS_EXE import D0FUS_scan, D0FUS_run, D0FUS_genetic, D0FUS_uncertainty
|
|
23
|
+
from D0FUS_EXE import D0FUS_scan, D0FUS_run, D0FUS_genetic, D0FUS_uncertainty, D0FUS_popcon
|
|
24
24
|
|
|
25
25
|
#%% Mode detection
|
|
26
26
|
|
|
@@ -41,6 +41,11 @@ def detect_mode_from_input(input_file):
|
|
|
41
41
|
with open(input_file, 'r', encoding='utf-8') as f:
|
|
42
42
|
content = f.read()
|
|
43
43
|
|
|
44
|
+
# POPCON mode: a [POPCON] section. Checked first so that the grid triples
|
|
45
|
+
# nbar_line/Tbar = [min, max, n] do not make the file look like a SCAN.
|
|
46
|
+
if re.search(r'^\s*\[\s*popcon\s*\]', content, re.MULTILINE | re.IGNORECASE):
|
|
47
|
+
return 'popcon', None
|
|
48
|
+
|
|
44
49
|
# UNCERTAINTY mode: an [UNCERTAINTY] section, or any tri()/norm()/unif()/envelope()
|
|
45
50
|
# marginal. Checked first so that optional map axes (a = [min, max, n]) do not make
|
|
46
51
|
# the file look like a SCAN.
|
|
@@ -354,6 +359,12 @@ def execute_with_mode_detection(input_file):
|
|
|
354
359
|
print("="*60 + "\n")
|
|
355
360
|
D0FUS_run.main(input_file, save_figures=True)
|
|
356
361
|
|
|
362
|
+
elif mode == 'popcon':
|
|
363
|
+
print("\n" + "="*60)
|
|
364
|
+
print("Mode: POPCON (operating-space contour map)")
|
|
365
|
+
print(f"Input: {os.path.basename(input_file)}")
|
|
366
|
+
print("="*60 + "\n")
|
|
367
|
+
D0FUS_popcon.main(input_file, save_figures=True)
|
|
357
368
|
elif mode == 'scan':
|
|
358
369
|
# SCAN mode detected
|
|
359
370
|
param_names = [p[0] for p in params]
|
|
@@ -4483,19 +4483,8 @@ def plot_port_access(
|
|
|
4483
4483
|
_save_or_show(fig, save_dir, "port_access")
|
|
4484
4484
|
|
|
4485
4485
|
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
parser = argparse.ArgumentParser(
|
|
4489
|
-
description="D0FUS_figures.py — stand-alone smoke test.\n"
|
|
4490
|
-
"Renders all figures interactively by default.\n"
|
|
4491
|
-
"Pass --save-dir to write PNG files instead.")
|
|
4492
|
-
parser.add_argument("--save-dir", default=None,
|
|
4493
|
-
help="Directory to save PNG figures (suppresses interactive display)")
|
|
4494
|
-
args = parser.parse_args()
|
|
4495
|
-
|
|
4496
|
-
_out = args.save_dir
|
|
4497
|
-
if _out is not None:
|
|
4498
|
-
os.makedirs(_out, exist_ok=True)
|
|
4486
|
+
def demo_run_dict():
|
|
4487
|
+
"""ITER Q=10 reference run-dict used by the stand-alone figure demo."""
|
|
4499
4488
|
|
|
4500
4489
|
# ITER Q=10 reference case — Shimada et al., Nucl. Fusion 47 (2007) S1
|
|
4501
4490
|
ITER_RUN = {
|
|
@@ -4544,7 +4533,144 @@ if __name__ == "__main__":
|
|
|
4544
4533
|
"e_shield": 0.50,
|
|
4545
4534
|
}
|
|
4546
4535
|
|
|
4547
|
-
|
|
4536
|
+
return ITER_RUN
|
|
4537
|
+
|
|
4538
|
+
|
|
4539
|
+
# =============================================================================
|
|
4540
|
+
# FIGURE REGISTRY & STAND-ALONE CATALOGUE
|
|
4541
|
+
# =============================================================================
|
|
4542
|
+
|
|
4543
|
+
def build_figure_registry():
|
|
4544
|
+
"""
|
|
4545
|
+
Auto-populated catalogue of every figure function in this module.
|
|
4546
|
+
|
|
4547
|
+
A figure function is any module-level callable named plot_* or fig_*.
|
|
4548
|
+
The one-line description is the first line of its docstring. The
|
|
4549
|
+
category is inferred from the first positional argument, following the
|
|
4550
|
+
module conventions:
|
|
4551
|
+
run dict -> 'run' (rendered by plot_all on a run dict)
|
|
4552
|
+
results -> 'uncertainty' (Monte-Carlo robustness figures)
|
|
4553
|
+
names -> 'uncertainty' (Sobol indices figure)
|
|
4554
|
+
scan_results -> 'scan'
|
|
4555
|
+
anything else -> 'standalone' (parametric / validation figures
|
|
4556
|
+
taking explicit physics arguments)
|
|
4557
|
+
|
|
4558
|
+
Returns
|
|
4559
|
+
-------
|
|
4560
|
+
dict : name -> dict(func, description, category, signature)
|
|
4561
|
+
"""
|
|
4562
|
+
import inspect as _inspect
|
|
4563
|
+
registry = {}
|
|
4564
|
+
for name, obj in sorted(globals().items()):
|
|
4565
|
+
if not callable(obj) or not (name.startswith('plot_')
|
|
4566
|
+
or name.startswith('fig_')):
|
|
4567
|
+
continue
|
|
4568
|
+
try:
|
|
4569
|
+
sig = _inspect.signature(obj)
|
|
4570
|
+
params = list(sig.parameters)
|
|
4571
|
+
except (TypeError, ValueError):
|
|
4572
|
+
sig, params = None, []
|
|
4573
|
+
doc = (_inspect.getdoc(obj) or '').strip().split('\n')
|
|
4574
|
+
desc = doc[0] if doc and doc[0] else '(no description)'
|
|
4575
|
+
first = params[0] if params else ''
|
|
4576
|
+
if name == 'plot_all':
|
|
4577
|
+
cat = 'meta'
|
|
4578
|
+
elif first == 'run':
|
|
4579
|
+
cat = 'run'
|
|
4580
|
+
elif first in ('results', 'names'):
|
|
4581
|
+
cat = 'uncertainty'
|
|
4582
|
+
elif first == 'scan_results' or 'scan' in name:
|
|
4583
|
+
cat = 'scan'
|
|
4584
|
+
else:
|
|
4585
|
+
cat = 'standalone'
|
|
4586
|
+
registry[name] = dict(func=obj, description=desc, category=cat,
|
|
4587
|
+
signature=str(sig) if sig else '(?)')
|
|
4588
|
+
return registry
|
|
4589
|
+
|
|
4590
|
+
|
|
4591
|
+
_CATEGORY_ORDER = ('meta', 'run', 'standalone', 'scan', 'uncertainty')
|
|
4592
|
+
_CATEGORY_LABEL = {
|
|
4593
|
+
'meta': 'META — full catalogue renderer',
|
|
4594
|
+
'run': 'RUN-DICT FIGURES — rendered from a D0FUS run dict',
|
|
4595
|
+
'standalone': 'STANDALONE FIGURES — parametric / validation '
|
|
4596
|
+
'(explicit physics arguments)',
|
|
4597
|
+
'scan': 'SCAN FIGURES — rendered from scan-mode outputs',
|
|
4598
|
+
'uncertainty': 'UNCERTAINTY FIGURES — rendered from UQ / Sobol outputs',
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
|
|
4602
|
+
def print_figure_catalog(registry=None):
|
|
4603
|
+
"""Print the grouped figure catalogue (name + one-line description)."""
|
|
4604
|
+
registry = registry or build_figure_registry()
|
|
4605
|
+
width = max(len(n) for n in registry)
|
|
4606
|
+
print("=" * 78)
|
|
4607
|
+
print(f"D0FUS figure catalogue — {len(registry)} functions")
|
|
4608
|
+
print("=" * 78)
|
|
4609
|
+
for cat in _CATEGORY_ORDER:
|
|
4610
|
+
entries = {n: e for n, e in registry.items() if e['category'] == cat}
|
|
4611
|
+
if not entries:
|
|
4612
|
+
continue
|
|
4613
|
+
print(f"\n── {_CATEGORY_LABEL[cat]}")
|
|
4614
|
+
for name, e in entries.items():
|
|
4615
|
+
print(f" {name:<{width}} {e['description']}")
|
|
4616
|
+
print("\nUsage: --list (catalogue only) | --only name1,name2 | --save-dir DIR")
|
|
4617
|
+
print("=" * 78)
|
|
4618
|
+
|
|
4619
|
+
|
|
4620
|
+
def _standalone_main():
|
|
4621
|
+
parser = argparse.ArgumentParser(
|
|
4622
|
+
description="D0FUS_figures.py — stand-alone figure catalogue.\n"
|
|
4623
|
+
"Prints the catalogue, then renders the full demo "
|
|
4624
|
+
"sequence (plot_all on the ITER reference run dict) "
|
|
4625
|
+
"interactively by default.")
|
|
4626
|
+
parser.add_argument("--save-dir", default=None,
|
|
4627
|
+
help="Directory to save PNG figures "
|
|
4628
|
+
"(suppresses interactive display)")
|
|
4629
|
+
parser.add_argument("--list", action="store_true",
|
|
4630
|
+
help="Print the figure catalogue and exit")
|
|
4631
|
+
parser.add_argument("--only", default=None,
|
|
4632
|
+
help="Comma-separated figure names to render "
|
|
4633
|
+
"(run-dict figures use the ITER demo dict; "
|
|
4634
|
+
"standalone figures need all-default arguments)")
|
|
4635
|
+
args = parser.parse_args()
|
|
4636
|
+
|
|
4637
|
+
registry = build_figure_registry()
|
|
4638
|
+
print_figure_catalog(registry)
|
|
4639
|
+
if args.list:
|
|
4640
|
+
return
|
|
4641
|
+
|
|
4642
|
+
_out = args.save_dir
|
|
4643
|
+
if _out is not None:
|
|
4644
|
+
os.makedirs(_out, exist_ok=True)
|
|
4645
|
+
|
|
4646
|
+
if args.only:
|
|
4647
|
+
wanted = [w.strip() for w in args.only.split(',') if w.strip()]
|
|
4648
|
+
run = demo_run_dict()
|
|
4649
|
+
for name in wanted:
|
|
4650
|
+
if name not in registry:
|
|
4651
|
+
print(f" [skip] unknown figure '{name}' (see catalogue above)")
|
|
4652
|
+
continue
|
|
4653
|
+
e = registry[name]
|
|
4654
|
+
print(f" rendering {name} ...")
|
|
4655
|
+
try:
|
|
4656
|
+
if e['category'] in ('run', 'meta'):
|
|
4657
|
+
e['func'](run, save_dir=_out)
|
|
4658
|
+
else:
|
|
4659
|
+
# Standalone figures: only callable here when every
|
|
4660
|
+
# physics argument has a default; pass save_dir when the
|
|
4661
|
+
# signature accepts it, otherwise call bare.
|
|
4662
|
+
try:
|
|
4663
|
+
e['func'](save_dir=_out)
|
|
4664
|
+
except TypeError:
|
|
4665
|
+
e['func']()
|
|
4666
|
+
except TypeError:
|
|
4667
|
+
print(f" [skip] {name} requires explicit arguments: "
|
|
4668
|
+
f"{name}{e['signature']} — call it from Python or "
|
|
4669
|
+
f"through its execution mode.")
|
|
4670
|
+
return
|
|
4671
|
+
|
|
4672
|
+
plot_all(demo_run_dict(), save_dir=_out)
|
|
4673
|
+
|
|
4548
4674
|
# =============================================================================
|
|
4549
4675
|
# UNCERTAINTY-MODE FIGURES
|
|
4550
4676
|
# Appended to support the D0FUS UNCERTAINTY (Monte-Carlo) execution mode.
|
|
@@ -4565,13 +4691,62 @@ Each figure carries a one-line plain-language reading note so it stands on its o
|
|
|
4565
4691
|
- fig_models : impact of the model-form choices (confinement scaling, elongation, ...)
|
|
4566
4692
|
on feasibility and on a key physics output, one row per model combination.
|
|
4567
4693
|
"""
|
|
4568
|
-
|
|
4569
|
-
|
|
4694
|
+
def fig_sobol(names, sobol, meta, save_dir=None, show=False):
|
|
4695
|
+
"""
|
|
4696
|
+
Sobol sensitivity bar charts: one panel per analysed QoI, horizontal bars
|
|
4697
|
+
for the first-order (S1, filled) and total (ST, hatched outline) indices
|
|
4698
|
+
of every uncertain parameter, per envelope combo (one figure per combo).
|
|
4570
4699
|
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4700
|
+
Parameters mirror the return values of
|
|
4701
|
+
D0FUS_uncertainty.run_sobol_from_file.
|
|
4702
|
+
"""
|
|
4703
|
+
figs = []
|
|
4704
|
+
for label, per_out in sobol.items():
|
|
4705
|
+
outputs = [k for k in per_out if np.isfinite(per_out[k]['ST']).any()]
|
|
4706
|
+
if not outputs:
|
|
4707
|
+
continue
|
|
4708
|
+
ncol = min(3, len(outputs))
|
|
4709
|
+
nrow = int(np.ceil(len(outputs) / ncol))
|
|
4710
|
+
fig, axes = plt.subplots(nrow, ncol,
|
|
4711
|
+
figsize=(4.2 * ncol, 0.6 * len(names) * nrow + 1.8),
|
|
4712
|
+
squeeze=False)
|
|
4713
|
+
y = np.arange(len(names))
|
|
4714
|
+
for k, out_key in enumerate(outputs):
|
|
4715
|
+
ax = axes[k // ncol][k % ncol]
|
|
4716
|
+
idx = per_out[out_key]
|
|
4717
|
+
order = np.argsort(np.nan_to_num(idx['ST']))
|
|
4718
|
+
ax.barh(y - 0.18, idx['ST'][order], height=0.36, color='lightsteelblue',
|
|
4719
|
+
edgecolor='navy', hatch='//', label='ST (total)')
|
|
4720
|
+
ax.barh(y + 0.18, idx['S1'][order], height=0.36, color='steelblue',
|
|
4721
|
+
label='S1 (first order)')
|
|
4722
|
+
ax.set_yticks(y)
|
|
4723
|
+
ax.set_yticklabels([names[j] for j in order], fontsize=8)
|
|
4724
|
+
ax.axvline(0.0, color='k', lw=0.6)
|
|
4725
|
+
ax.set_xlim(left=min(0.0, np.nanmin(idx['S1']) - 0.05),
|
|
4726
|
+
right=max(1.0, np.nanmax(idx['ST']) + 0.05))
|
|
4727
|
+
_std = idx.get('std', None)
|
|
4728
|
+
ax.set_title(out_key if _std is None
|
|
4729
|
+
else f"{out_key} (std = {_std:.3g})", fontsize=10)
|
|
4730
|
+
ax.grid(axis='x', alpha=0.3)
|
|
4731
|
+
if k == 0:
|
|
4732
|
+
ax.legend(fontsize=8, loc='lower right')
|
|
4733
|
+
for k in range(len(outputs), nrow * ncol):
|
|
4734
|
+
axes[k // ncol][k % ncol].axis('off')
|
|
4735
|
+
fig.suptitle(f"Sobol indices — combo [{label}] "
|
|
4736
|
+
f"(N={meta['n_base']}, {meta['n_eval']} evaluations)",
|
|
4737
|
+
fontsize=11)
|
|
4738
|
+
fig.tight_layout(rect=(0, 0, 1, 0.96))
|
|
4739
|
+
if save_dir is not None:
|
|
4740
|
+
safe = label.replace(' ', '').replace(',', '_').replace('=', '-')
|
|
4741
|
+
fig.savefig(os.path.join(save_dir, f"sobol_{safe}.png"), dpi=170)
|
|
4742
|
+
figs.append(fig)
|
|
4743
|
+
if not show:
|
|
4744
|
+
plt.close(fig)
|
|
4745
|
+
return figs
|
|
4746
|
+
|
|
4747
|
+
|
|
4748
|
+
# (os, Counter, numpy, matplotlib, pyplot and Patch are all exported by
|
|
4749
|
+
# D0FUS_import.py through the wildcard import at the top of this module.)
|
|
4575
4750
|
|
|
4576
4751
|
_GREEN, _AMBER, _RED = 'tab:green', 'tab:orange', 'tab:red'
|
|
4577
4752
|
_BIND_COLOR = {'greenwald': 'tab:blue', 'troyon': 'tab:red',
|
|
@@ -4717,7 +4892,6 @@ def scan_feasibility(uq_file, scan_specs, n_samples=200, n_jobs=-1, combo=None,
|
|
|
4717
4892
|
scan_specs : {param: (lo, hi, n_points)}
|
|
4718
4893
|
Returns : {param: (x_values, P_feasible[%], design_value)}
|
|
4719
4894
|
"""
|
|
4720
|
-
from joblib import Parallel, delayed
|
|
4721
4895
|
from D0FUS_EXE import D0FUS_uncertainty as UQ
|
|
4722
4896
|
|
|
4723
4897
|
base, spec, envelope, controls, deck_path = UQ.parse_uq_file(uq_file)
|
|
@@ -4844,4 +5018,7 @@ def fig_models(results, qoi='Q', save_dir=None):
|
|
|
4844
5018
|
xy=(0.5, -0.30 if len(labels) <= 4 else -0.18), xycoords='axes fraction',
|
|
4845
5019
|
ha='center', fontsize=9.5, color='dimgray')
|
|
4846
5020
|
plt.tight_layout()
|
|
4847
|
-
return _save(fig, save_dir, 'uq_models')
|
|
5021
|
+
return _save(fig, save_dir, 'uq_models')
|
|
5022
|
+
|
|
5023
|
+
if __name__ == "__main__":
|
|
5024
|
+
_standalone_main()
|
|
@@ -16,6 +16,7 @@ os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
|
|
|
16
16
|
|
|
17
17
|
#%% Standard Library Imports
|
|
18
18
|
|
|
19
|
+
import itertools
|
|
19
20
|
import json
|
|
20
21
|
import math
|
|
21
22
|
import multiprocessing
|
|
@@ -25,14 +26,29 @@ import shutil
|
|
|
25
26
|
import sys
|
|
26
27
|
import time
|
|
27
28
|
import traceback
|
|
29
|
+
import tempfile
|
|
28
30
|
import warnings
|
|
29
31
|
import importlib
|
|
32
|
+
from collections import Counter
|
|
30
33
|
from datetime import datetime
|
|
31
34
|
from pathlib import Path
|
|
32
35
|
|
|
33
36
|
#%% Scientific Computing Libraries
|
|
34
37
|
|
|
35
38
|
import numpy as np
|
|
39
|
+
|
|
40
|
+
# ── NumPy floating-point error policy ────────────────────────────────────────
|
|
41
|
+
# Root-finding and optimisation drivers (brentq bracketing, differential
|
|
42
|
+
# evolution, GA evaluation) routinely probe unphysical corners of parameter
|
|
43
|
+
# space; the resulting divide-by-zero / invalid-value / overflow floating-point
|
|
44
|
+
# warnings are pure numerical noise handled through NaN returns downstream.
|
|
45
|
+
# Silencing them HERE, at the NumPy level only, replaces the former module-wide
|
|
46
|
+
# warnings.filterwarnings("ignore", RuntimeWarning), which had the harmful side
|
|
47
|
+
# effect of also swallowing genuine physics warnings emitted with
|
|
48
|
+
# warnings.warn(...) (validity-domain violations, missing-argument fallbacks).
|
|
49
|
+
# Those now reach the user.
|
|
50
|
+
np.seterr(divide='ignore', invalid='ignore', over='ignore')
|
|
51
|
+
|
|
36
52
|
import pandas as pd
|
|
37
53
|
import sympy as sp
|
|
38
54
|
from typing import List, Tuple
|
|
@@ -84,6 +100,15 @@ from typing import Optional, Dict, List, Tuple
|
|
|
84
100
|
|
|
85
101
|
import argparse
|
|
86
102
|
|
|
103
|
+
#%% Parallel Computing
|
|
104
|
+
|
|
105
|
+
from joblib import Parallel, delayed
|
|
106
|
+
|
|
107
|
+
#%% Statistics (uncertainty-quantification mode)
|
|
108
|
+
|
|
109
|
+
from scipy import stats
|
|
110
|
+
from scipy.stats import qmc
|
|
111
|
+
|
|
87
112
|
#%% Genetic Algorithm Libraries
|
|
88
113
|
|
|
89
114
|
from deap import algorithms, base, creator, tools
|
|
@@ -98,6 +98,17 @@ class GlobalConfig:
|
|
|
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
100
|
Tbar : float = 14.0 # Volume-averaged electron temperature T_e [keV]
|
|
101
|
+
Tbar_mode : str = 'manual'
|
|
102
|
+
# Temperature prescription mode:
|
|
103
|
+
# 'manual' : Tbar above is used directly (historical behaviour).
|
|
104
|
+
# 'greenwald' : Tbar is SOLVED by scalar root-finding (brentq) so that
|
|
105
|
+
# the converged operating point sits at the requested
|
|
106
|
+
# Greenwald fraction f_GW_target = nbar_line / (Ip/pi a^2).
|
|
107
|
+
# The search bracket is [Tbar_min, Tbar_max]; the solve
|
|
108
|
+
# wraps the full run() (one design solve per iteration).
|
|
109
|
+
f_GW_target : float = 0.85 # Target Greenwald fraction (Tbar_mode='greenwald') [-]
|
|
110
|
+
Tbar_min : float = 4.0 # Lower Tbar bracket for the f_GW solve [keV]
|
|
111
|
+
Tbar_max : float = 30.0 # Upper Tbar bracket for the f_GW solve [keV]
|
|
101
112
|
tau_i_e : float = 1.0 # Ion-to-electron temperature ratio T_i/T_e [-]
|
|
102
113
|
# 1.0 -> single-temperature plasma (T_i = T_e).
|
|
103
114
|
# Prescribed: T_i(rho) = tau_i_e * T_e(rho),
|
|
@@ -171,7 +182,18 @@ class GlobalConfig:
|
|
|
171
182
|
# kink_parameter='q_star' → q_limit ≈ 2.0–2.5 (Freidberg 2015: q*>2)
|
|
172
183
|
# kink_parameter='q95' → q_limit ≈ 3.0–3.5 (ITER/EU-DEMO practice)
|
|
173
184
|
q_limit : float = 3.0 # Kink safety factor threshold [-]
|
|
174
|
-
Greenwald_limit : float = 1.0 #
|
|
185
|
+
Greenwald_limit : float = 1.0 # Density-limit fraction (margin) applied to the
|
|
186
|
+
# selected density-limit model [-]
|
|
187
|
+
density_limit_model : str = 'greenwald'
|
|
188
|
+
# Density-limit model used by the feasibility constraint (nbar_line < limit):
|
|
189
|
+
# 'greenwald' : n_GW = Ip/(pi a^2) [Greenwald, PPCF 44 (2002) R27]
|
|
190
|
+
# 'giacomin' : power-dependent edge limit [Giacomin et al., PRL 128 (2022)
|
|
191
|
+
# 185003, Eq. 12], converted to a line-averaged cap through
|
|
192
|
+
# n_sep = f_n_sep_line * nbar_line.
|
|
193
|
+
# 'zanca' : power-balance line-avg limit [Zanca et al., NF 59 (2019)
|
|
194
|
+
# 126011, Eq. 20].
|
|
195
|
+
alpha_giacomin : float = 3.3 # Giacomin fitted prefactor (3.3 +/- 0.3)
|
|
196
|
+
f0_zanca : float = 0.5 # Zanca effective neutral concentration [%]
|
|
175
197
|
Ip_limit : float = None # Upper bound on plasma current [MA]; None = no ceiling (GA)
|
|
176
198
|
ms : float = 0.3 # Vertical stability margin parameter [-]
|
|
177
199
|
|
|
@@ -193,6 +215,17 @@ class GlobalConfig:
|
|
|
193
215
|
# concentrations n_imp/n_e. Empty string = disabled (pure D-T).
|
|
194
216
|
# Typical: W 1e-5–5e-4, Ar/Ne 1e-3–5e-3.
|
|
195
217
|
impurity_species : str = '' # Comma-separated species: 'W', 'W, Ne', '' = none
|
|
218
|
+
detachment_impurity : str = 'Ne'
|
|
219
|
+
# Seeding species used by the Lengyel detachment diagnostic ('N', 'Ne'
|
|
220
|
+
# or 'Ar'; '' disables the diagnostic). The required SOL concentration
|
|
221
|
+
# to radiate the two-point-model power-loss fraction f_pwr_loss_req is
|
|
222
|
+
# reported in the run output (diagnostic only, no constraint).
|
|
223
|
+
lengyel_T_target_eV : float = 25.0
|
|
224
|
+
# Desired post-seeding target electron temperature [eV] for the Lengyel
|
|
225
|
+
# integral lower bound (cfspopcon SPARC PRD convention: 25 eV). This is
|
|
226
|
+
# the DESIRED detached/low-recycling target condition, deliberately
|
|
227
|
+
# decoupled from the two-point-model operating T_et (which can be
|
|
228
|
+
# sheath-limited at T_u when the operating point is unmitigated).
|
|
196
229
|
f_imp_core : str = '' # Matching concentrations: '5e-5', '1e-5, 3e-3'
|
|
197
230
|
# Core/edge radiation boundary for τ_E and P_sep convention.
|
|
198
231
|
# ρ < rho_rad_core → subtracted from P_heat (core). ρ > → edge (divertor load).
|