Skip to content

Commit bd3110c

Browse files
committed
backsolve pedestal peaking in jch profiles and provide pedestal peaking outputs so that inputs and outputs are consistent regardless of which profiles are selected
1 parent 128e47a commit bd3110c

5 files changed

Lines changed: 731 additions & 125 deletions

File tree

cfspopcon/formulas/atomic_data/atomic_data.py

Lines changed: 104 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Module defining the AtomicData class, used for interfacing with radas files."""
22

33
import warnings
4+
from collections.abc import Callable
45
from pathlib import Path
56
from typing import Union
67

@@ -14,6 +15,19 @@
1415
from .coeff_interpolator import CoeffInterpolator
1516

1617

18+
class _LazyValueDict(dict):
19+
"""Dictionary which populates missing values on first access."""
20+
21+
def __init__(self, loader: Callable):
22+
super().__init__()
23+
self._loader = loader
24+
25+
def __missing__(self, key):
26+
value = self._loader(key)
27+
self[key] = value
28+
return value
29+
30+
1731
class AtomicData:
1832
"""A class to manage atomic data for various species, providing facilities for accessing datasets directly or by constructing interpolators for different datasets (i.e. radiated power curves).
1933
@@ -28,62 +42,116 @@ class AtomicData:
2842
"""
2943

3044
def __init__(self, atomic_data_directory: Path = Path() / "radas_dir") -> None:
31-
"""Initializes the AtomicData object by loading atomic data from the specified directory.
45+
"""Initializes the AtomicData object by indexing the atomic data files in the specified directory.
3246
3347
Parameters:
3448
- atomic_data_directory (Path): The path to the directory containing atomic data files.
3549
"""
3650
self.atomic_data_directory = atomic_data_directory
37-
self.datasets = self.read_atomic_data(atomic_data_directory) # Load atomic data into the datasets attribute
38-
self.available_species = list(self.datasets.keys()) # List available species based on the loaded datasets
39-
40-
# Initialize dictionaries to hold interpolators for different data types and conditions
41-
self.coronal_Lz_interpolators: dict[AtomicSpecies, CoeffInterpolator] = dict()
42-
self.coronal_Z_interpolators: dict[AtomicSpecies, CoeffInterpolator] = dict()
43-
self.noncoronal_Lz_interpolators: dict[tuple[AtomicSpecies, float], CoeffInterpolator] = dict()
44-
self.noncoronal_Z_interpolators: dict[tuple[AtomicSpecies, float], CoeffInterpolator] = dict()
45-
46-
self.species_ne_tau: dict[AtomicSpecies, xr.DataArray] = dict()
51+
self.atomic_data_files = self.find_atomic_data_files(atomic_data_directory)
52+
self.datasets: dict[AtomicSpecies, xr.Dataset] = _LazyValueDict(self._load_dataset)
53+
self.available_species = list(self.atomic_data_files.keys())
54+
55+
# Build interpolators only when a particular species/table is requested.
56+
self.coronal_Lz_interpolators: dict[AtomicSpecies, CoeffInterpolator] = _LazyValueDict(self._load_coronal_Lz_interpolator)
57+
self.coronal_Z_interpolators: dict[AtomicSpecies, CoeffInterpolator] = _LazyValueDict(self._load_coronal_Z_interpolator)
58+
self.noncoronal_Lz_interpolators: dict[tuple[AtomicSpecies, float], CoeffInterpolator] = _LazyValueDict(
59+
self._load_noncoronal_Lz_interpolator
60+
)
61+
self.noncoronal_Z_interpolators: dict[tuple[AtomicSpecies, float], CoeffInterpolator] = _LazyValueDict(
62+
self._load_noncoronal_Z_interpolator
63+
)
64+
65+
self.species_ne_tau: dict[AtomicSpecies, xr.DataArray] = _LazyValueDict(self._load_species_ne_tau)
4766
self.ne_tau_units = ureg.m**-3 * ureg.s
4867

49-
self.radas_version: str = ""
68+
self._radas_version: str = ""
69+
self._radas_version_checked_species: set[AtomicSpecies] = set()
5070

71+
@property
72+
def radas_version(self) -> str:
73+
"""Return the RADAS version after checking the available datasets."""
5174
for species in self.available_species:
52-
dataset = self[species]
53-
54-
ref = dict(
55-
reference_electron_density=dataset.reference_electron_density,
56-
reference_electron_temp=dataset.reference_electron_temp,
57-
)
58-
59-
self.coronal_Lz_interpolators[species] = CoeffInterpolator(dataset.coronal_Lz, **ref)
60-
self.coronal_Z_interpolators[species] = CoeffInterpolator(dataset.coronal_mean_charge_state, **ref)
61-
62-
self.species_ne_tau[species] = dataset["ne_tau"].pint.to(self.ne_tau_units).pint.dequantify()
63-
64-
for ne_tau, dataset_at_single_ne_tau in dataset.groupby("dim_ne_tau"):
65-
subds = dataset_at_single_ne_tau.squeeze(dim="dim_ne_tau")
66-
self.noncoronal_Lz_interpolators[(species, ne_tau)] = CoeffInterpolator(subds.equilibrium_Lz, **ref)
67-
self.noncoronal_Z_interpolators[(species, ne_tau)] = CoeffInterpolator(subds.equilibrium_mean_charge_state, **ref)
68-
69-
self._check_radas_version(getattr(dataset, "radas_version", "UNDEFINED"))
75+
_ = self[species]
76+
return self._radas_version
77+
78+
def _load_dataset(self, species: AtomicSpecies) -> xr.Dataset:
79+
dataset = xr.open_dataset(self.atomic_data_files[species]).pint.quantify()
80+
if species not in self._radas_version_checked_species:
81+
self._check_radas_version(getattr(dataset, "radas_version", "UNDEFINED"))
82+
self._radas_version_checked_species.add(species)
83+
return dataset
84+
85+
def _get_reference_values(self, species: AtomicSpecies) -> dict[str, Quantity]:
86+
dataset = self[species]
87+
return dict(
88+
reference_electron_density=dataset.reference_electron_density,
89+
reference_electron_temp=dataset.reference_electron_temp,
90+
)
91+
92+
def _load_coronal_Lz_interpolator(self, species: AtomicSpecies) -> CoeffInterpolator:
93+
dataset = self[species]
94+
return CoeffInterpolator(dataset.coronal_Lz, **self._get_reference_values(species))
95+
96+
def _load_coronal_Z_interpolator(self, species: AtomicSpecies) -> CoeffInterpolator:
97+
dataset = self[species]
98+
return CoeffInterpolator(dataset.coronal_mean_charge_state, **self._get_reference_values(species))
99+
100+
def _load_species_ne_tau(self, species: AtomicSpecies) -> xr.DataArray:
101+
return self[species]["ne_tau"].pint.to(self.ne_tau_units).pint.dequantify()
102+
103+
def _load_noncoronal_Lz_interpolator(self, key: tuple[AtomicSpecies, float]) -> CoeffInterpolator:
104+
species, ne_tau = key
105+
dataset = self[species].sel(dim_ne_tau=ne_tau)
106+
if "dim_ne_tau" in dataset.dims:
107+
dataset = dataset.squeeze(dim="dim_ne_tau")
108+
return CoeffInterpolator(dataset.equilibrium_Lz, **self._get_reference_values(species))
109+
110+
def _load_noncoronal_Z_interpolator(self, key: tuple[AtomicSpecies, float]) -> CoeffInterpolator:
111+
species, ne_tau = key
112+
dataset = self[species].sel(dim_ne_tau=ne_tau)
113+
if "dim_ne_tau" in dataset.dims:
114+
dataset = dataset.squeeze(dim="dim_ne_tau")
115+
return CoeffInterpolator(dataset.equilibrium_mean_charge_state, **self._get_reference_values(species))
70116

71117
def _check_radas_version(self, test_version: str) -> None:
72118
"""Checks that the provided test_version matches radas_version (if set).
73119
74120
If radas_version is not set, sets radas_version = test_version.
75121
If a mismatch is found, sets radas_version = UNDEFINED.
76122
"""
77-
if self.radas_version == "":
78-
self.radas_version = test_version
79-
elif self.radas_version != test_version:
123+
if self._radas_version == "":
124+
self._radas_version = test_version
125+
elif self._radas_version != test_version:
80126
warnings.warn(
81-
f"Found multiple radas radas versions ({self.radas_version} != {test_version}) in the requested atomic data. Will set radas_version = UNDEFINED.",
127+
f"Found multiple radas radas versions ({self._radas_version} != {test_version}) in the requested atomic data. Will set radas_version = UNDEFINED.",
82128
stacklevel=2,
83129
)
84130

85131
@staticmethod
86-
def read_atomic_data(atomic_data_directory: Path = Path() / "radas_dir") -> dict[AtomicSpecies, xr.Dataset]:
132+
def find_atomic_data_files(atomic_data_directory: Path = Path() / "radas_dir") -> dict[AtomicSpecies, Path]:
133+
"""Return the available RADAS netCDF files indexed by species."""
134+
if not atomic_data_directory.exists():
135+
raise FileNotFoundError(f"atomic_data_directory ({atomic_data_directory.absolute()}) does not exist.")
136+
137+
if not (atomic_data_directory / "output").exists():
138+
raise FileNotFoundError(
139+
f"atomic_data_directory ({atomic_data_directory}) does not contain a subfolder called 'output'. Make sure you have executed `poetry run radas` before calling this function."
140+
)
141+
142+
atomic_data_files: dict[AtomicSpecies, Path] = dict()
143+
for file in (atomic_data_directory / "output").iterdir():
144+
if file.suffix == ".nc":
145+
species = file.stem
146+
try:
147+
atomic_data_files[AtomicSpecies[species.capitalize()]] = file
148+
except KeyError:
149+
print(f"No AtomicSpecies found corresponding to {species}")
150+
151+
return atomic_data_files
152+
153+
@classmethod
154+
def read_atomic_data(cls, atomic_data_directory: Path = Path() / "radas_dir") -> dict[AtomicSpecies, xr.Dataset]:
87155
"""Reads atomic data from netCDF files located in the specified directory.
88156
89157
This function scans a directory for netCDF files (.nc), each representing atomic data for a different species.
@@ -99,30 +167,7 @@ def read_atomic_data(atomic_data_directory: Path = Path() / "radas_dir") -> dict
99167
Raises:
100168
- FileNotFoundError: If the atomic_data_directory or its 'output' subdirectory does not exist.
101169
"""
102-
# Ensure the atomic data directory and its 'output' subdirectory exist
103-
if not atomic_data_directory.exists():
104-
raise FileNotFoundError(f"atomic_data_directory ({atomic_data_directory.absolute()}) does not exist.")
105-
106-
if not (atomic_data_directory / "output").exists():
107-
raise FileNotFoundError(
108-
f"atomic_data_directory ({atomic_data_directory}) does not contain a subfolder called 'output'. Make sure you have executed `poetry run radas` before calling this function."
109-
)
110-
111-
atomic_data = dict()
112-
# Iterate through each netCDF file in the 'output' directory
113-
for file in (atomic_data_directory / "output").iterdir():
114-
if file.suffix == ".nc": # Check if the file is a netCDF file
115-
species = file.stem # Extract the species name from the file name
116-
try:
117-
# Attempt to map the file name to an AtomicSpecies enum
118-
species_enum = AtomicSpecies[species.capitalize()]
119-
# Read the netCDF file into an xarray Dataset and quantify it with pint
120-
atomic_data[species_enum] = xr.open_dataset(file).pint.quantify()
121-
except KeyError:
122-
# If no matching AtomicSpecies enum is found, print a warning
123-
print(f"No AtomicSpecies found corresponding to {species}")
124-
125-
return atomic_data
170+
return {species: xr.open_dataset(file).pint.quantify() for species, file in cls.find_atomic_data_files(atomic_data_directory).items()}
126171

127172
@staticmethod
128173
def key_to_enum(species: Union[str, AtomicSpecies]) -> AtomicSpecies:

0 commit comments

Comments
 (0)