11"""Module defining the AtomicData class, used for interfacing with radas files."""
22
33import warnings
4+ from collections .abc import Callable
45from pathlib import Path
56from typing import Union
67
1415from .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+
1731class 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