Skip to content

Commit 51b30bf

Browse files
committed
Add slice_time and slice_index arguments to IMAS geometry loader
Added an IDS with multiple time slices for testing
1 parent af8c9e1 commit 51b30bf

File tree

6 files changed

+109
-4
lines changed

6 files changed

+109
-4
lines changed

torax/_src/geometry/pydantic_model.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
"""Pydantic model for geometry."""
1616

17-
from collections.abc import Callable, Mapping
17+
from collections.abc import Callable
18+
from collections.abc import Mapping
1819
import functools
1920
import inspect
2021
import logging
@@ -296,6 +297,9 @@ class IMASConfig(torax_pydantic.BaseModelFrozen):
296297
the with running TORAX using the provided APIs. To use this option you
297298
must implement a custom run loop. Only one of imas_filepath, imas_uri or
298299
equilibrium_object can be set.
300+
slice_time: Time of slice to load from IMAS IDS. If given, overrides
301+
slice_index.
302+
slice_index: Index of slice to load from IMAS IDS (default 0).
299303
"""
300304

301305
geometry_type: Annotated[Literal['imas'], TIME_INVARIANT] = 'imas'
@@ -306,6 +310,8 @@ class IMASConfig(torax_pydantic.BaseModelFrozen):
306310
imas_filepath: str | None = 'ITERhybrid_COCOS17_IDS_ddv4.nc'
307311
imas_uri: str | None = None
308312
equilibrium_object: ids_toplevel.IDSToplevel | None = None
313+
slice_index: pydantic.NonNegativeInt = 0
314+
slice_time: pydantic.NonNegativeFloat | None = None
309315

310316
@pydantic.model_validator(mode='after')
311317
def _validate_model(self) -> typing_extensions.Self:

torax/_src/geometry/standard_geometry.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,8 @@ def from_IMAS(
10101010
Ip_from_parameters: bool,
10111011
n_rho: int,
10121012
hires_factor: int,
1013+
slice_time: float | None = None,
1014+
slice_index: int = 0,
10131015
equilibrium_object: ids_toplevel.IDSToplevel | None = None,
10141016
imas_uri: str | None = None,
10151017
imas_filepath: str | None = None,
@@ -1024,6 +1026,9 @@ def from_IMAS(
10241026
values in the Geometry are rescaled to match the new Ip.
10251027
n_rho: Radial grid points (num cells).
10261028
hires_factor: High resolution factor for calculations.
1029+
slice_time: Time of slice to load from IMAS IDS. If given, overrides
1030+
slice_index.
1031+
slice_index: Index of slice to load from IMAS IDS.
10271032
equilibrium_object: The equilibrium IDS containing the relevant data.
10281033
imas_uri: The IMAS uri containing the equilibrium data.
10291034
imas_filepath: The path to the IMAS netCDF file containing the equilibrium
@@ -1041,6 +1046,8 @@ def from_IMAS(
10411046
Ip_from_parameters=Ip_from_parameters,
10421047
n_rho=n_rho,
10431048
hires_factor=hires_factor,
1049+
slice_time=slice_time,
1050+
slice_index=slice_index,
10441051
)
10451052
return cls(geometry_type=geometry.GeometryType.IMAS, **inputs)
10461053

torax/_src/imas_tools/input/equilibrium.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ def geometry_from_IMAS(
3030
Ip_from_parameters: bool = False,
3131
n_rho: int = 25,
3232
hires_factor: int = 4,
33+
slice_time: float | None = None,
34+
slice_index: int = 0,
3335
equilibrium_object: ids_toplevel.IDSToplevel | None = None,
3436
imas_uri: str | None = None,
3537
imas_filepath: str | None = None,
@@ -46,6 +48,9 @@ def geometry_from_IMAS(
4648
n_rho: Radial grid points (num cells)
4749
hires_factor: Grid refinement factor for poloidal flux <--> plasma current
4850
calculations.
51+
slice_time: Time of slice to load from IMAS IDS. If given, overrides
52+
slice_index.
53+
slice_index: Index of slice to load from IMAS IDS.
4954
equilibrium_object: The equilibrium IDS containing the relevant data.
5055
imas_uri: The IMAS uri containing the equilibrium data.
5156
imas_filepath: The path to the IMAS netCDF file containing the equilibrium
@@ -72,9 +77,36 @@ def geometry_from_IMAS(
7277
raise ValueError(
7378
"equilibrium_object must be a string (file path) or an IDS"
7479
)
75-
# TODO(b/431977390): Currently only the first time slice is used, extend to
80+
# TODO(b/431977390): Currently only a single time slice is used, extend to
7681
# support multiple time slices.
77-
IMAS_data = equilibrium.time_slice[0]
82+
# Convert time to index
83+
if slice_time is not None:
84+
if not np.all(equilibrium.time[:-1] <= equilibrium.time[1:]):
85+
sorting_indices = np.argsort(equilibrium.time)
86+
else:
87+
sorting_indices = np.arange(len(equilibrium.time))
88+
# Find the closest time in the IDS that is <= slice_time
89+
slice_index = (
90+
np.searchsorted(
91+
equilibrium.time[sorting_indices], slice_time, side="right"
92+
)
93+
- 1
94+
)
95+
if not np.allclose(
96+
equilibrium.time[sorting_indices][slice_index], slice_time, atol=1e-9
97+
):
98+
logging.warn(
99+
f"Requested t={slice_time} not in IDS; "
100+
f"using t={equilibrium.time[slice_index]})"
101+
)
102+
103+
if slice_index >= len(equilibrium.time_slice):
104+
raise IndexError(
105+
f"slice_index={slice_index} out of range for IDS with "
106+
f"{len(equilibrium.time_slice)} time slices"
107+
)
108+
IMAS_data = equilibrium.time_slice[slice_index]
109+
B_0 = np.abs(IMAS_data.global_quantities.magnetic_axis.b_field_phi)
78110
R_major = np.asarray(equilibrium.vacuum_toroidal_field.r0)
79111
B_0 = np.asarray(np.abs(equilibrium.vacuum_toroidal_field.b0[0]))
80112

torax/_src/imas_tools/input/tests/equilibrium_test.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from absl.testing import parameterized
1919
import imas
2020
import numpy as np
21+
import pydantic
2122
from torax._src.geometry import geometry_loader
2223
from torax._src.geometry import pydantic_model as geometry_pydantic_model
2324
from torax._src.imas_tools.input import equilibrium as imas_geometry
@@ -97,6 +98,59 @@ def test_IMAS_input_with_equilibrium_object(self):
9798
)
9899
config.build_geometry()
99100

101+
def test_IMAS_loading_specific_slice(self):
102+
def _check_geo_match(geo1, geo2):
103+
for key in geo1.__dict__.keys():
104+
if key not in [
105+
'geometry_type',
106+
'torax_mesh',
107+
'R_major',
108+
'B_0',
109+
'rho_hires_norm',
110+
'Phi_b_dot',
111+
'Ip_from_parameters',
112+
]:
113+
np.testing.assert_allclose(
114+
geo1.__dict__[key],
115+
geo2.__dict__[key],
116+
err_msg=(
117+
f'Value {key} mismatched between slice_time and slice_index'
118+
),
119+
)
120+
121+
filename = 'ITERhybrid_rampup_11_time_slices_COCOS17_IDS_ddv4.nc'
122+
config_at_0 = geometry_pydantic_model.IMASConfig(imas_filepath=filename)
123+
config_at_slice_from_time = geometry_pydantic_model.IMASConfig(
124+
imas_filepath=filename, slice_time=40
125+
)
126+
config_at_slice_from_index = geometry_pydantic_model.IMASConfig(
127+
imas_filepath=filename, slice_index=5
128+
)
129+
130+
geo_at_0 = config_at_0.build_geometry()
131+
geo_at_slice_from_time = config_at_slice_from_time.build_geometry()
132+
geo_at_slice_from_index = config_at_slice_from_index.build_geometry()
133+
134+
_check_geo_match(geo_at_slice_from_time, geo_at_slice_from_index)
135+
136+
# Check that the geometry is not the same as at t=0
137+
with self.assertRaisesRegex(
138+
AssertionError, 'mismatched between slice_time and slice_index'
139+
):
140+
_check_geo_match(geo_at_0, geo_at_slice_from_index)
141+
142+
def test_IMAS_raises_if_slice_out_of_range(self):
143+
filename = 'ITERhybrid_COCOS17_IDS_ddv4.nc'
144+
with self.assertRaisesRegex(
145+
IndexError,
146+
'out of range',
147+
):
148+
config = geometry_pydantic_model.IMASConfig(
149+
imas_filepath=filename,
150+
slice_index=100,
151+
)
152+
config.build_geometry()
153+
100154

101155
if __name__ == '__main__':
102156
absltest.main()
Binary file not shown.

torax/data/third_party/geo/README

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,10 @@ The mapping of datafile: LICENSE for the geometry files in this directory are as
2727
All associated LICENSE files are also located in this directory.
2828

2929
ITER_hybrid_citrin_equil_cheasedata.mat2cols: LICENSE_PINT
30-
eqdsk_cocos02.eqdsk: LICENSE_PINT
30+
eqdsk_cocos02.eqdsk: LICENSE_PINT
31+
32+
The IMAS netCDF file ITERhybrid_rampup_11_time_slices_COCOS17_IDS_ddv4.nc was generated
33+
by extracting p' and ff' profiles for each timeslice from a TORAX simulation with the
34+
iterhybrid_rampup.py config, and passing the profiles in to CHEASE.
35+
The LCFS was kept fixed for all timeslices, using the values from
36+
ITER_hybrid_citrin_equil_cheasedata.mat2cols.

0 commit comments

Comments
 (0)