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.
Files changed (24) hide show
  1. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS.py +12 -1
  2. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_figures.py +199 -22
  3. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_import.py +25 -0
  4. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_parameterization.py +34 -1
  5. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_physical_functions.py +670 -5
  6. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_radial_build_functions.py +119 -2
  7. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_EXE/D0FUS_genetic.py +11 -7
  8. d0fus-2.3.4/D0FUS_EXE/D0FUS_popcon.py +468 -0
  9. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_EXE/D0FUS_run.py +165 -17
  10. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_EXE/D0FUS_scan.py +8 -5
  11. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_EXE/D0FUS_uncertainty.py +220 -6
  12. {d0fus-2.3.3/d0fus.egg-info → d0fus-2.3.4}/PKG-INFO +1 -1
  13. {d0fus-2.3.3 → d0fus-2.3.4/d0fus.egg-info}/PKG-INFO +1 -1
  14. {d0fus-2.3.3 → d0fus-2.3.4}/d0fus.egg-info/SOURCES.txt +1 -0
  15. {d0fus-2.3.3 → d0fus-2.3.4}/pyproject.toml +1 -1
  16. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_cost_data.py +0 -0
  17. {d0fus-2.3.3 → d0fus-2.3.4}/D0FUS_BIB/D0FUS_cost_functions.py +0 -0
  18. {d0fus-2.3.3 → d0fus-2.3.4}/LICENSE +0 -0
  19. {d0fus-2.3.3 → d0fus-2.3.4}/README.md +0 -0
  20. {d0fus-2.3.3 → d0fus-2.3.4}/d0fus.egg-info/dependency_links.txt +0 -0
  21. {d0fus-2.3.3 → d0fus-2.3.4}/d0fus.egg-info/entry_points.txt +0 -0
  22. {d0fus-2.3.3 → d0fus-2.3.4}/d0fus.egg-info/requires.txt +0 -0
  23. {d0fus-2.3.3 → d0fus-2.3.4}/d0fus.egg-info/top_level.txt +0 -0
  24. {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
- if __name__ == "__main__":
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
- plot_all(ITER_RUN, save_dir=_out)
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
- import os
4569
- from collections import Counter
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
- import numpy as np
4572
- import matplotlib
4573
- import matplotlib.pyplot as plt
4574
- from matplotlib.patches import Patch
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 # Greenwald density fraction limit [-]
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).