imas-python 0.1.dev1618__py3-none-any.whl
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.
Potentially problematic release.
This version of imas-python might be problematic. Click here for more details.
- imas/__init__.py +34 -0
- imas/__main__.py +17 -0
- imas/_util.py +260 -0
- imas/_version.py +16 -0
- imas/assets/IDSDef2MDSpreTree.xsl +161 -0
- imas/assets/IDS_fake_toplevel.xml +318 -0
- imas/assets/IDS_minimal.xml +37 -0
- imas/assets/IDS_minimal_2.xml +37 -0
- imas/assets/IDS_minimal_struct_array.xml +43 -0
- imas/assets/IDS_minimal_types.xml +169 -0
- imas/assets/ITER_134173_106_core_profiles.ids +2438 -0
- imas/assets/ITER_134173_106_equilibrium.ids +3172 -0
- imas/assets/README.md +5 -0
- imas/assets/core_profiles.ids +2438 -0
- imas/assets/equilibrium.ids +3172 -0
- imas/backends/__init__.py +12 -0
- imas/backends/db_entry_impl.py +122 -0
- imas/backends/imas_core/__init__.py +4 -0
- imas/backends/imas_core/al_context.py +391 -0
- imas/backends/imas_core/db_entry_al.py +405 -0
- imas/backends/imas_core/db_entry_helpers.py +200 -0
- imas/backends/imas_core/imas_interface.py +216 -0
- imas/backends/imas_core/mdsplus_model.py +367 -0
- imas/backends/imas_core/uda_support.py +56 -0
- imas/backends/netcdf/__init__.py +4 -0
- imas/backends/netcdf/db_entry_nc.py +176 -0
- imas/backends/netcdf/ids2nc.py +306 -0
- imas/backends/netcdf/nc2ids.py +302 -0
- imas/backends/netcdf/nc_metadata.py +357 -0
- imas/backends/netcdf/nc_validate.py +53 -0
- imas/command/cli.py +239 -0
- imas/command/db_analysis.py +290 -0
- imas/command/helpers.py +45 -0
- imas/command/timer.py +77 -0
- imas/db_entry.py +802 -0
- imas/dd_helpers.py +300 -0
- imas/dd_zip.py +297 -0
- imas/exception.py +107 -0
- imas/ids_base.py +149 -0
- imas/ids_convert.py +920 -0
- imas/ids_coordinates.py +400 -0
- imas/ids_data_type.py +115 -0
- imas/ids_defs.py +171 -0
- imas/ids_factory.py +108 -0
- imas/ids_identifiers.py +125 -0
- imas/ids_metadata.py +307 -0
- imas/ids_path.py +254 -0
- imas/ids_primitive.py +510 -0
- imas/ids_struct_array.py +233 -0
- imas/ids_structure.py +331 -0
- imas/ids_toplevel.py +381 -0
- imas/setup_logging.py +64 -0
- imas/test/test_all_dd_versions.py +28 -0
- imas/test/test_cli.py +44 -0
- imas/test/test_dbentry.py +104 -0
- imas/test/test_dd_helpers.py +134 -0
- imas/test/test_dd_zip.py +29 -0
- imas/test/test_deepcopy.py +25 -0
- imas/test/test_exception.py +12 -0
- imas/test/test_get_sample.py +388 -0
- imas/test/test_hash.py +188 -0
- imas/test/test_helpers.py +354 -0
- imas/test/test_identifiers.py +105 -0
- imas/test/test_ids2nc.py +166 -0
- imas/test/test_ids_ascii_data.py +39 -0
- imas/test/test_ids_convert.py +415 -0
- imas/test/test_ids_coordinates.py +165 -0
- imas/test/test_ids_data_type.py +40 -0
- imas/test/test_ids_factory.py +35 -0
- imas/test/test_ids_metadata.py +66 -0
- imas/test/test_ids_mixin.py +20 -0
- imas/test/test_ids_path.py +192 -0
- imas/test/test_ids_primitive.py +109 -0
- imas/test/test_ids_struct_array.py +89 -0
- imas/test/test_ids_structure.py +70 -0
- imas/test/test_ids_toplevel.py +55 -0
- imas/test/test_ids_validate.py +343 -0
- imas/test/test_latest_dd_autofill.py +74 -0
- imas/test/test_latest_dd_manual.py +38 -0
- imas/test/test_latest_dd_resample.py +102 -0
- imas/test/test_lazy_loading.py +201 -0
- imas/test/test_list_occurrences.py +82 -0
- imas/test/test_minimal.py +37 -0
- imas/test/test_minimal_conversion.py +44 -0
- imas/test/test_minimal_io.py +21 -0
- imas/test/test_minimal_struct_array_io.py +60 -0
- imas/test/test_minimal_types.py +436 -0
- imas/test/test_minimal_types_io.py +70 -0
- imas/test/test_minimal_types_io_automatic.py +66 -0
- imas/test/test_nbc_change.py +408 -0
- imas/test/test_nc_autofill.py +13 -0
- imas/test/test_nc_entry.py +28 -0
- imas/test/test_nc_metadata.py +153 -0
- imas/test/test_nc_validation.py +197 -0
- imas/test/test_snippets.py +25 -0
- imas/test/test_static_ids.py +39 -0
- imas/test/test_str_1d.py +99 -0
- imas/test/test_time_slicing.py +182 -0
- imas/test/test_uda_support.py +12 -0
- imas/test/test_util.py +209 -0
- imas/training.py +45 -0
- imas/util.py +526 -0
- imas_python-0.1.dev1618.dist-info/LICENSE.txt +165 -0
- imas_python-0.1.dev1618.dist-info/METADATA +288 -0
- imas_python-0.1.dev1618.dist-info/RECORD +108 -0
- imas_python-0.1.dev1618.dist-info/WHEEL +5 -0
- imas_python-0.1.dev1618.dist-info/entry_points.txt +3 -0
- imas_python-0.1.dev1618.dist-info/top_level.txt +1 -0
imas/ids_coordinates.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# This file is part of imas-python.
|
|
2
|
+
# You should have received the imas-python LICENSE file with this project.
|
|
3
|
+
"""Logic for interpreting coordinates in an IDS.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from imas.exception import CoordinateError, CoordinateLookupError, ValidationError
|
|
13
|
+
from imas.ids_data_type import IDSDataType
|
|
14
|
+
from imas.ids_defs import EMPTY_FLOAT
|
|
15
|
+
from imas.ids_defs import IDS_TIME_MODE_HETEROGENEOUS as HETEROGENEOUS_TIME
|
|
16
|
+
from imas.ids_defs import IDS_TIME_MODE_HOMOGENEOUS as HOMOGENEOUS_TIME
|
|
17
|
+
from imas.ids_path import IDSPath
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # Prevent circular imports
|
|
20
|
+
from imas.ids_base import IDSBase
|
|
21
|
+
from imas.ids_primitive import IDSPrimitive
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IDSCoordinate:
|
|
27
|
+
"""Class representing a coordinate reference from the DD.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
- Coordinates are an index:
|
|
31
|
+
|
|
32
|
+
- ``1...N``: any number of items allowed
|
|
33
|
+
- ``1...3``: max 3 items allowed
|
|
34
|
+
|
|
35
|
+
- Coordinates refer to other quantities:
|
|
36
|
+
|
|
37
|
+
- ``time``: refers to the ``time`` quantity in the IDS toplevel
|
|
38
|
+
- ``profiles_1d(itime)/time``: refers to the ``time`` quantity in the
|
|
39
|
+
``profiles_1d`` IDSStructArray with (dummy) index itime
|
|
40
|
+
|
|
41
|
+
- Coordinates specify alternatives:
|
|
42
|
+
|
|
43
|
+
- ``distribution(i1)/profiles_2d(itime)/grid/r OR
|
|
44
|
+
distribution(i1)/profiles_2d(itime)/grid/rho_tor_norm``: coordinate can
|
|
45
|
+
be ``r`` or ``rho_tor_norm`` (only one may be filled)
|
|
46
|
+
- ``coherent_wave(i1)/beam_tracing(itime)/beam(i2)/length OR 1...1``: either
|
|
47
|
+
use ``length`` as coordinate, or this dimension must have size one.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
_cache: Dict[str, "IDSCoordinate"] = {}
|
|
51
|
+
|
|
52
|
+
def __new__(cls, coordinate_spec: str) -> "IDSCoordinate":
|
|
53
|
+
if coordinate_spec not in cls._cache:
|
|
54
|
+
cls._cache[coordinate_spec] = super().__new__(cls)
|
|
55
|
+
return cls._cache[coordinate_spec]
|
|
56
|
+
|
|
57
|
+
def __init__(self, coordinate_spec: str) -> None:
|
|
58
|
+
if hasattr(self, "_init_done"):
|
|
59
|
+
return # Already initialized, __new__ returned from cache
|
|
60
|
+
self._coordinate_spec = coordinate_spec
|
|
61
|
+
self.size: Optional[int] = None
|
|
62
|
+
"""Exact size of this dimension. For example, 2 when coordinate = 1...2."""
|
|
63
|
+
|
|
64
|
+
refs: List[IDSPath] = []
|
|
65
|
+
specs = coordinate_spec.split(" OR ")
|
|
66
|
+
for spec in specs:
|
|
67
|
+
if spec.startswith("1..."):
|
|
68
|
+
if spec != "1...N":
|
|
69
|
+
try:
|
|
70
|
+
self.size = int(spec[4:])
|
|
71
|
+
except ValueError:
|
|
72
|
+
logger.debug(
|
|
73
|
+
f"Ignoring invalid coordinate specifier {spec}",
|
|
74
|
+
exc_info=True,
|
|
75
|
+
)
|
|
76
|
+
elif spec.startswith("IDS:"):
|
|
77
|
+
pass # Coordinate is in another IDS, we don't handle that
|
|
78
|
+
elif spec:
|
|
79
|
+
try:
|
|
80
|
+
refs.append(IDSPath(spec))
|
|
81
|
+
except ValueError:
|
|
82
|
+
logger.debug(
|
|
83
|
+
f"Ignoring invalid coordinate specifier {spec}", exc_info=True
|
|
84
|
+
)
|
|
85
|
+
self.references: "tuple[IDSPath]" = tuple(refs)
|
|
86
|
+
"""A tuple paths that this coordinate refers to.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
num_rules = len(self.references) + (self.size is not None)
|
|
90
|
+
self.has_validation: bool = num_rules > 0
|
|
91
|
+
"""True iff this coordinate specifies a validation rule."""
|
|
92
|
+
self.has_alternatives: bool = num_rules > 1
|
|
93
|
+
"""True iff exclusive alternative coordinates are specified."""
|
|
94
|
+
self.is_time_coordinate: bool = any(ref.is_time_path for ref in self.references)
|
|
95
|
+
"""True iff this coordinate refers to ``time``."""
|
|
96
|
+
|
|
97
|
+
# Prevent accidentally modifying attributes
|
|
98
|
+
self._init_done = True
|
|
99
|
+
|
|
100
|
+
def __setattr__(self, name: str, value: Any):
|
|
101
|
+
if hasattr(self, "_init_done"):
|
|
102
|
+
raise RuntimeError("Cannot set attribute: IDSCoordinate is read-only.")
|
|
103
|
+
super().__setattr__(name, value)
|
|
104
|
+
|
|
105
|
+
def __str__(self) -> str:
|
|
106
|
+
return self._coordinate_spec
|
|
107
|
+
|
|
108
|
+
def __repr__(self) -> str:
|
|
109
|
+
return f"IDSCoordinate({self._coordinate_spec!r})"
|
|
110
|
+
|
|
111
|
+
def __hash__(self) -> int:
|
|
112
|
+
"""IDSCoordinate objects are immutable, we can be used e.g. as dict key."""
|
|
113
|
+
return hash(self._coordinate_spec)
|
|
114
|
+
|
|
115
|
+
def format_refs(self, element: "IDSBase") -> str:
|
|
116
|
+
"""Return a comma-separated list of paths representing self.references.
|
|
117
|
+
|
|
118
|
+
Useful when constructing error messages to users.
|
|
119
|
+
"""
|
|
120
|
+
ref_paths = []
|
|
121
|
+
for ref in self.references:
|
|
122
|
+
try:
|
|
123
|
+
ref_paths.append(ref.goto(element)._path)
|
|
124
|
+
except (ValueError, AttributeError, LookupError):
|
|
125
|
+
ref_paths.append(str(ref))
|
|
126
|
+
return ", ".join(f"`{ref}`" for ref in ref_paths)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _goto(path: IDSPath, element: "IDSBase") -> "IDSPrimitive":
|
|
130
|
+
"""Wrapper around IDSPath.goto to raise more meaningful errors."""
|
|
131
|
+
try:
|
|
132
|
+
return path.goto(element)
|
|
133
|
+
except (ValueError, AttributeError) as exc:
|
|
134
|
+
raise RuntimeError(
|
|
135
|
+
f"The data dictionary coordinate definition '{path}' cannot be found: {exc}"
|
|
136
|
+
) from None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class IDSCoordinates:
|
|
140
|
+
"""Class representing coordinates of an IDSStructArray or IDSPrimitive.
|
|
141
|
+
|
|
142
|
+
Can be used to automatically retrieve coordinate values via the indexing operator.
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
>>> import imas
|
|
146
|
+
>>> core_profiles = imas.IDSFactory().core_profiles()
|
|
147
|
+
>>> core_profiles.ids_properties.homogeneous_time = \\
|
|
148
|
+
... imas.ids_defs.IDS_TIME_MODE_HOMOGENEOUS
|
|
149
|
+
>>> core_profiles.profiles_1d.coordinates[0]
|
|
150
|
+
IDSNumericArray("/core_profiles/time", array([], dtype=float64))
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
__slots__ = ["_node"]
|
|
154
|
+
|
|
155
|
+
def __init__(self, node: "IDSBase") -> None:
|
|
156
|
+
self._node = node
|
|
157
|
+
|
|
158
|
+
def __repr__(self) -> str:
|
|
159
|
+
coordinates = []
|
|
160
|
+
for i in range(len(self)):
|
|
161
|
+
coor_str = f"\n {i}: '{self._node.metadata.coordinates[i]}'"
|
|
162
|
+
same_as = self._node.metadata.coordinates_same_as[i]
|
|
163
|
+
if same_as.references:
|
|
164
|
+
coor_str += f" (same as '{same_as}')"
|
|
165
|
+
coordinates.append(coor_str)
|
|
166
|
+
return f"<IDSCoordinates of '{self._node._path}'>{''.join(coordinates)}"
|
|
167
|
+
|
|
168
|
+
def __len__(self) -> int:
|
|
169
|
+
"""Number of coordinates is equal to the dimension of the bound IDS node."""
|
|
170
|
+
return self._node.metadata.ndim
|
|
171
|
+
|
|
172
|
+
def __getitem__(self, key: int) -> Union["IDSPrimitive", np.ndarray]:
|
|
173
|
+
"""Get the coordinate of the given dimension.
|
|
174
|
+
|
|
175
|
+
When the coordinate is an index (e.g. 1...N) this will return a numpy arange
|
|
176
|
+
with the same size as the data currently has in that dimension.
|
|
177
|
+
|
|
178
|
+
When one coordinate path is defined in the DD, the corresponding IDSPrimitive
|
|
179
|
+
object is returned.
|
|
180
|
+
|
|
181
|
+
When multiple coordinate paths are defined, the one that is set is returned. A
|
|
182
|
+
ValueError is raised when multiple or none are defined.
|
|
183
|
+
"""
|
|
184
|
+
coordinate = self._node.metadata.coordinates[key]
|
|
185
|
+
if not coordinate.references:
|
|
186
|
+
return np.arange(self._node.shape[key])
|
|
187
|
+
coordinate_path: Optional[IDSPath] = None
|
|
188
|
+
# Time is a special coordinate:
|
|
189
|
+
if coordinate.is_time_coordinate:
|
|
190
|
+
time_mode = self._node._time_mode
|
|
191
|
+
if time_mode == HOMOGENEOUS_TIME:
|
|
192
|
+
coordinate_path = IDSPath("time")
|
|
193
|
+
elif time_mode == HETEROGENEOUS_TIME:
|
|
194
|
+
# Time coordinates are guaranteed to be unique (no alternatives)
|
|
195
|
+
coordinate_path = coordinate.references[0]
|
|
196
|
+
else:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
"Invalid IDS time mode: ids_properties/homogeneous_time is "
|
|
199
|
+
f"{time_mode}, was expecting {HETEROGENEOUS_TIME} or "
|
|
200
|
+
f"{HOMOGENEOUS_TIME}."
|
|
201
|
+
)
|
|
202
|
+
elif not coordinate.has_alternatives:
|
|
203
|
+
coordinate_path = coordinate.references[0]
|
|
204
|
+
|
|
205
|
+
# Check if the coordinate is inside the AoS
|
|
206
|
+
if coordinate_path is not None:
|
|
207
|
+
if self._node.metadata.data_type is IDSDataType.STRUCT_ARRAY:
|
|
208
|
+
if self._node.metadata.path.is_ancestor_of(coordinate_path):
|
|
209
|
+
# Create a numpy array with the contents of all elements in the AoS
|
|
210
|
+
# TODO: move this to IDSPath?
|
|
211
|
+
data = [_goto(coordinate_path, ele).value for ele in self._node]
|
|
212
|
+
return np.array(data)
|
|
213
|
+
coordinate_node = _goto(coordinate_path, self._node)
|
|
214
|
+
if not coordinate_node.metadata.alternative_coordinates:
|
|
215
|
+
return coordinate_node
|
|
216
|
+
|
|
217
|
+
# This coordinate has alternatives, check which are set
|
|
218
|
+
nonzero_alternatives = [coordinate_node] if len(coordinate_node) > 0 else []
|
|
219
|
+
for alternative_path in coordinate_node.metadata.alternative_coordinates:
|
|
220
|
+
alternative_node = alternative_path.goto(coordinate_node)
|
|
221
|
+
if len(alternative_node) > 0:
|
|
222
|
+
nonzero_alternatives.append(alternative_node)
|
|
223
|
+
if not nonzero_alternatives:
|
|
224
|
+
# None of the coordinates are set, return the primary coordinate
|
|
225
|
+
return coordinate_node
|
|
226
|
+
# Check if the lengths of all nonzero alternatives agree
|
|
227
|
+
if len(set(map(len, nonzero_alternatives))) != 1:
|
|
228
|
+
sizes = "\n".join(
|
|
229
|
+
f" `{alt._path}` has size {len(alt)}"
|
|
230
|
+
for alt in nonzero_alternatives
|
|
231
|
+
)
|
|
232
|
+
raise CoordinateLookupError(
|
|
233
|
+
f"Element `{self._node._path}` has "
|
|
234
|
+
"multiple alternative coordinates set, but they don't have "
|
|
235
|
+
f"matching sizes:\n{sizes}"
|
|
236
|
+
)
|
|
237
|
+
if len(nonzero_alternatives) > 1:
|
|
238
|
+
logger.info("Multiple alternative coordinates are set, using the first")
|
|
239
|
+
return nonzero_alternatives[0]
|
|
240
|
+
|
|
241
|
+
# Handle alternative coordinates, currently (DD 3.38.1) the `coordinate in
|
|
242
|
+
# structure` logic is not applicable for these cases:
|
|
243
|
+
refs = [_goto(ref, self._node) for ref in coordinate.references]
|
|
244
|
+
ref_is_defined = [len(ref.value) > 0 for ref in refs]
|
|
245
|
+
if sum(ref_is_defined) == 0:
|
|
246
|
+
if coordinate.size is not None:
|
|
247
|
+
# alternatively we can be an index
|
|
248
|
+
return np.arange(self._node.shape[key])
|
|
249
|
+
coordinate_refs = coordinate.format_refs(self._node)
|
|
250
|
+
if ", " in coordinate_refs:
|
|
251
|
+
coordinate_refs = "any of " + coordinate_refs
|
|
252
|
+
raise CoordinateLookupError(
|
|
253
|
+
f"Element `{self._node._path}` must have its coordinate in dimension "
|
|
254
|
+
f"{key + 1} ({coordinate_refs}) filled."
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if sum(ref_is_defined) == 1:
|
|
258
|
+
for i in range(len(refs)):
|
|
259
|
+
if ref_is_defined[i]:
|
|
260
|
+
return refs[i]
|
|
261
|
+
|
|
262
|
+
raise CoordinateLookupError(
|
|
263
|
+
f"Element `{self._node._path}` must have "
|
|
264
|
+
f"exactly one of its coordinates ({coordinate.format_refs(self._node)}) "
|
|
265
|
+
"set, but multiple are set."
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def time_index(self) -> Optional[int]:
|
|
270
|
+
"""Get the index of the time coordinate
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
The index of the time coordinate, or None if there is no time coordinate.
|
|
274
|
+
"""
|
|
275
|
+
for i, coor in enumerate(self._node.metadata.coordinates):
|
|
276
|
+
if coor.is_time_coordinate:
|
|
277
|
+
return i
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def _validate(self):
|
|
281
|
+
"""Coordinate validation checks.
|
|
282
|
+
|
|
283
|
+
See also:
|
|
284
|
+
:py:meth:`imas.ids_toplevel.IDSToplevel.validate`.
|
|
285
|
+
"""
|
|
286
|
+
node = self._node
|
|
287
|
+
shape = node.shape
|
|
288
|
+
metadata = node.metadata
|
|
289
|
+
|
|
290
|
+
# Validate coordinate
|
|
291
|
+
for dim in range(metadata.ndim):
|
|
292
|
+
coordinate = metadata.coordinates[dim]
|
|
293
|
+
if not coordinate.has_validation:
|
|
294
|
+
continue # Nothing to validate
|
|
295
|
+
|
|
296
|
+
# Validate size
|
|
297
|
+
if coordinate.size:
|
|
298
|
+
if shape[dim] == coordinate.size:
|
|
299
|
+
continue # Correct size
|
|
300
|
+
elif not coordinate.has_alternatives:
|
|
301
|
+
# If the coordinate has alternatives (see test case
|
|
302
|
+
# test_validate_reference_or_fixed_size) we continue checking the
|
|
303
|
+
# references below.
|
|
304
|
+
# If only size is specified, the dimension is not the correct size:
|
|
305
|
+
raise CoordinateError(node, dim, coordinate.size, None)
|
|
306
|
+
|
|
307
|
+
# Validate references
|
|
308
|
+
assert coordinate.references
|
|
309
|
+
with self._capture_goto_errors(dim, coordinate) as captured:
|
|
310
|
+
other_element = self[dim]
|
|
311
|
+
if captured:
|
|
312
|
+
continue # Ignored error, continue to next dimension
|
|
313
|
+
|
|
314
|
+
if isinstance(other_element, np.ndarray):
|
|
315
|
+
if coordinate.size:
|
|
316
|
+
# other_element may be a numpy array when coordinate = "path OR
|
|
317
|
+
# 1...1" and path is unset
|
|
318
|
+
raise CoordinateError(node, dim, coordinate.size, None)
|
|
319
|
+
# Otherwise, this is a dynamic AoS with heterogeneous_time, verify that
|
|
320
|
+
# none of the values are EMPTY_FLOAT
|
|
321
|
+
if EMPTY_FLOAT in other_element:
|
|
322
|
+
(n,) = np.where(other_element == EMPTY_FLOAT)
|
|
323
|
+
raise ValidationError(
|
|
324
|
+
f"Coordinate `{node._path}[{n[0]}]/time` is empty."
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
with self._capture_goto_errors(dim, coordinate) as captured:
|
|
328
|
+
# other_element may (incorrectly) be a struct in older DD versions
|
|
329
|
+
expected_size = other_element.shape[0]
|
|
330
|
+
if captured:
|
|
331
|
+
continue # Ignored error, continue to next dimension
|
|
332
|
+
if shape[dim] != expected_size:
|
|
333
|
+
other_path = other_element._path
|
|
334
|
+
raise CoordinateError(node, dim, expected_size, other_path)
|
|
335
|
+
|
|
336
|
+
# Validate coordinate_same_as
|
|
337
|
+
for dim in range(metadata.ndim):
|
|
338
|
+
same_as = metadata.coordinates_same_as[dim]
|
|
339
|
+
if not same_as.has_validation:
|
|
340
|
+
continue # Nothing to validate
|
|
341
|
+
|
|
342
|
+
assert len(same_as.references) == 1
|
|
343
|
+
with self._capture_goto_errors(dim, coordinate) as captured:
|
|
344
|
+
other_element = same_as.references[0].goto(self._node)
|
|
345
|
+
if captured:
|
|
346
|
+
continue # Ignored error, continue to next dimension
|
|
347
|
+
|
|
348
|
+
expected_size = other_element.shape[dim]
|
|
349
|
+
if shape[dim] != expected_size:
|
|
350
|
+
other_path = other_element._path
|
|
351
|
+
raise CoordinateError(node, dim, expected_size, other_path)
|
|
352
|
+
|
|
353
|
+
@contextmanager
|
|
354
|
+
def _capture_goto_errors(self, dim, coordinate):
|
|
355
|
+
"""Helper method for _validate to capture errors encountered during
|
|
356
|
+
IDSPath.goto().
|
|
357
|
+
"""
|
|
358
|
+
did_capture = []
|
|
359
|
+
path = self._node._path
|
|
360
|
+
try:
|
|
361
|
+
yield did_capture
|
|
362
|
+
except CoordinateLookupError as exc:
|
|
363
|
+
raise ValidationError(exc.args[0])
|
|
364
|
+
except IndexError as exc:
|
|
365
|
+
# Can happen in IDSPath.goto when an invalid index is encountered.
|
|
366
|
+
coordinate_refs = coordinate.format_refs(self._node)
|
|
367
|
+
raise ValidationError(
|
|
368
|
+
f"Error while validating element `{path}`: dimension {dim + 1} has an "
|
|
369
|
+
f"invalid index for coordinate(s) {coordinate_refs}."
|
|
370
|
+
) from exc
|
|
371
|
+
except Exception as exc:
|
|
372
|
+
# Ignore all other exceptions and log them
|
|
373
|
+
if "Unexpected index" in str(exc):
|
|
374
|
+
logger.debug(
|
|
375
|
+
"Ignored AoS coordinate outside our tree (see IMAS-4675) of "
|
|
376
|
+
"element `%s`, dimension %s, coordinate `%s`",
|
|
377
|
+
path,
|
|
378
|
+
dim,
|
|
379
|
+
coordinate.references,
|
|
380
|
+
)
|
|
381
|
+
else:
|
|
382
|
+
if self._node._version <= "3.38.1":
|
|
383
|
+
version_error = (
|
|
384
|
+
"This is expected to happen in DD versions <= 3.38.1, where "
|
|
385
|
+
"some coordinate metadata is incorrect."
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
version_error = (
|
|
389
|
+
"Please report this issue to the imas-python developers."
|
|
390
|
+
)
|
|
391
|
+
logger.warning(
|
|
392
|
+
"An error occurred while finding coordinate `%s` of dimension %s, "
|
|
393
|
+
"which is ignored. %s",
|
|
394
|
+
coordinate.references,
|
|
395
|
+
dim,
|
|
396
|
+
version_error,
|
|
397
|
+
exc_info=1,
|
|
398
|
+
)
|
|
399
|
+
# Flag to the caller that an error was suppressed
|
|
400
|
+
did_capture.append(1)
|
imas/ids_data_type.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# This file is part of imas-python.
|
|
2
|
+
# You should have received the imas-python LICENSE file with this project.
|
|
3
|
+
"""Data Dictionary type handling functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from typing import Optional, Tuple
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from imas.ids_defs import (
|
|
13
|
+
CHAR_DATA,
|
|
14
|
+
COMPLEX_DATA,
|
|
15
|
+
DOUBLE_DATA,
|
|
16
|
+
EMPTY_COMPLEX,
|
|
17
|
+
EMPTY_FLOAT,
|
|
18
|
+
EMPTY_INT,
|
|
19
|
+
INTEGER_DATA,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class IDSDataType(Enum):
|
|
24
|
+
"""Enum representing the possible data types in an IDS"""
|
|
25
|
+
|
|
26
|
+
STRUCTURE = "structure"
|
|
27
|
+
"""IDS structure. Maps to an IDSStructure object."""
|
|
28
|
+
|
|
29
|
+
STRUCT_ARRAY = "struct_array"
|
|
30
|
+
"""IDS array of structures. Maps to an IDSStructArray object with IDSStructure
|
|
31
|
+
children."""
|
|
32
|
+
|
|
33
|
+
STR = "STR"
|
|
34
|
+
"""String data."""
|
|
35
|
+
|
|
36
|
+
INT = "INT"
|
|
37
|
+
"""Integer data."""
|
|
38
|
+
|
|
39
|
+
FLT = "FLT"
|
|
40
|
+
"""Floating point data."""
|
|
41
|
+
|
|
42
|
+
CPX = "CPX"
|
|
43
|
+
"""Complex data."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, value) -> None:
|
|
46
|
+
self.default = {
|
|
47
|
+
"STR": "",
|
|
48
|
+
"INT": EMPTY_INT,
|
|
49
|
+
"FLT": EMPTY_FLOAT,
|
|
50
|
+
"CPX": EMPTY_COMPLEX,
|
|
51
|
+
}.get(value, None)
|
|
52
|
+
"""Default value for a field with this type."""
|
|
53
|
+
|
|
54
|
+
self.al_type = {
|
|
55
|
+
"STR": CHAR_DATA,
|
|
56
|
+
"INT": INTEGER_DATA,
|
|
57
|
+
"FLT": DOUBLE_DATA,
|
|
58
|
+
"CPX": COMPLEX_DATA,
|
|
59
|
+
}.get(value, None)
|
|
60
|
+
"""Lowlevel identifier for this type."""
|
|
61
|
+
|
|
62
|
+
self.python_type = {
|
|
63
|
+
"STR": str,
|
|
64
|
+
"INT": int,
|
|
65
|
+
"FLT": float,
|
|
66
|
+
"CPX": complex,
|
|
67
|
+
}.get(value, None)
|
|
68
|
+
"""Python type for 0D instances of this type."""
|
|
69
|
+
|
|
70
|
+
self.numpy_dtype = {
|
|
71
|
+
"INT": np.int32,
|
|
72
|
+
"FLT": np.float64,
|
|
73
|
+
"CPX": np.complex128,
|
|
74
|
+
}.get(value, None)
|
|
75
|
+
"""Numpy dtype for array instances of this type."""
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
@lru_cache(maxsize=None)
|
|
79
|
+
def parse(data_type: Optional[str]) -> Tuple[Optional["IDSDataType"], int]:
|
|
80
|
+
"""Parse data type string from the Data Dictionary.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
data_type: Data type string from the DD.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
IDSDataType instance representing the parsed data type and number of
|
|
87
|
+
dimensions.
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
>>> IDSDataType.parse("STR_1D")
|
|
91
|
+
(<IDSDataType.STR: 'STR'>, 1)
|
|
92
|
+
>>> IDSDataType.parse("struct_array")
|
|
93
|
+
(<IDSDataType.STRUCT_ARRAY: 'struct_array'>, 1)
|
|
94
|
+
>>> IDSDataType.parse("structure")
|
|
95
|
+
(<IDSDataType.STRUCTURE: 'structure'>, 0)
|
|
96
|
+
>>> IDSDataType.parse("CPX_5D")
|
|
97
|
+
(<IDSDataType.CPX: 'CPX'>, 5)
|
|
98
|
+
"""
|
|
99
|
+
if data_type is None:
|
|
100
|
+
return None, 0
|
|
101
|
+
if data_type == "structure":
|
|
102
|
+
ndim = 0
|
|
103
|
+
elif data_type == "struct_array":
|
|
104
|
+
ndim = 1
|
|
105
|
+
else:
|
|
106
|
+
dtype, *rest = data_type.upper().split("_")
|
|
107
|
+
if rest == ["TYPE"]: # legacy str_type, int_type, flt_type, cpx_type:
|
|
108
|
+
ndim = 0
|
|
109
|
+
elif rest and "0" <= rest[0][0] <= "9":
|
|
110
|
+
# works for both legacy flt_1d_type and regular TYP_ND
|
|
111
|
+
ndim = int(rest[0][0])
|
|
112
|
+
else:
|
|
113
|
+
raise ValueError(f"Unknown IDS data type: {data_type}")
|
|
114
|
+
data_type = dtype
|
|
115
|
+
return IDSDataType(data_type), ndim
|
imas/ids_defs.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# This file is part of imas-python.
|
|
2
|
+
# You should have received the imas-python LICENSE file with this project.
|
|
3
|
+
""" Load imas-python libs to provide constants
|
|
4
|
+
|
|
5
|
+
.. _`Backend identifiers`:
|
|
6
|
+
|
|
7
|
+
Backend identifiers
|
|
8
|
+
-------------------
|
|
9
|
+
|
|
10
|
+
The following constants are identifiers for the different Access Layer backends. Please
|
|
11
|
+
see the Access Layer documentation for more details on the different backends.
|
|
12
|
+
|
|
13
|
+
.. data:: ASCII_BACKEND
|
|
14
|
+
|
|
15
|
+
Identifier for the ASCII backend.
|
|
16
|
+
|
|
17
|
+
.. data:: MDSPLUS_BACKEND
|
|
18
|
+
|
|
19
|
+
Identifier for the MDS+ backend.
|
|
20
|
+
|
|
21
|
+
.. data:: HDF5_BACKEND
|
|
22
|
+
|
|
23
|
+
Identifier for the HDF5 backend.
|
|
24
|
+
|
|
25
|
+
.. data:: MEMORY_BACKEND
|
|
26
|
+
|
|
27
|
+
Identifier for the memory backend.
|
|
28
|
+
|
|
29
|
+
.. data:: UDA_BACKEND
|
|
30
|
+
|
|
31
|
+
Identifier for the UDA backend.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
Time modes
|
|
35
|
+
----------
|
|
36
|
+
|
|
37
|
+
.. data:: IDS_TIME_MODE_HOMOGENEOUS
|
|
38
|
+
|
|
39
|
+
Use homogeneous time, all time coordinates refer to the toplevel ``time`` array.
|
|
40
|
+
|
|
41
|
+
.. data:: IDS_TIME_MODE_HETEROGENEOUS
|
|
42
|
+
|
|
43
|
+
Use heterogeneous time, time coordinates refer to their local ``time`` array.
|
|
44
|
+
|
|
45
|
+
.. data:: IDS_TIME_MODE_INDEPENDENT
|
|
46
|
+
|
|
47
|
+
Indicates that this IDS does not store time-dependent data.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
Interpolation modes
|
|
51
|
+
-------------------
|
|
52
|
+
|
|
53
|
+
.. data:: CLOSEST_INTERP
|
|
54
|
+
|
|
55
|
+
Interpolation method that returns the `closest` time slice in the original IDS (can
|
|
56
|
+
break causality as it can return data ahead of requested time).
|
|
57
|
+
|
|
58
|
+
.. data:: PREVIOUS_INTERP
|
|
59
|
+
|
|
60
|
+
Interpolation method that returns the `previous` time slice if the requested time
|
|
61
|
+
does not exactly exist in the original IDS.
|
|
62
|
+
|
|
63
|
+
.. data:: LINEAR_INTERP
|
|
64
|
+
|
|
65
|
+
Interpolation method that returns a linear interpolation between the existing slices
|
|
66
|
+
before and after the requested time.
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
Serializer protocols
|
|
70
|
+
--------------------
|
|
71
|
+
|
|
72
|
+
.. data:: ASCII_SERIALIZER_PROTOCOL
|
|
73
|
+
|
|
74
|
+
Identifier for the ASCII serialization protocol.
|
|
75
|
+
|
|
76
|
+
.. data:: FLEXBUFFERS_SERIALIZER_PROTOCOL
|
|
77
|
+
|
|
78
|
+
Identifier for the Flexbuffers serialization protocol. This protocol is more
|
|
79
|
+
performant and results in a smaller buffer size than the
|
|
80
|
+
:data:`ASCII_SERIALIZER_PROTOCOL`.
|
|
81
|
+
|
|
82
|
+
.. versionadded:: 1.1 Requires ``imas_core`` version 5.3 or newer.
|
|
83
|
+
|
|
84
|
+
.. data:: DEFAULT_SERIALIZER_PROTOCOL
|
|
85
|
+
|
|
86
|
+
Identifier for the default serialization protocol.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
import functools
|
|
90
|
+
import logging
|
|
91
|
+
|
|
92
|
+
from imas.backends.imas_core.imas_interface import has_imas, imasdef
|
|
93
|
+
|
|
94
|
+
logger = logging.getLogger(__name__)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if has_imas:
|
|
98
|
+
ASCII_BACKEND = imasdef.ASCII_BACKEND
|
|
99
|
+
CHAR_DATA = imasdef.CHAR_DATA
|
|
100
|
+
CLOSE_PULSE = imasdef.CLOSE_PULSE
|
|
101
|
+
CLOSEST_INTERP = imasdef.CLOSEST_INTERP
|
|
102
|
+
CREATE_PULSE = imasdef.CREATE_PULSE
|
|
103
|
+
DOUBLE_DATA = imasdef.DOUBLE_DATA
|
|
104
|
+
COMPLEX_DATA = imasdef.COMPLEX_DATA
|
|
105
|
+
EMPTY_COMPLEX = imasdef.EMPTY_COMPLEX
|
|
106
|
+
EMPTY_FLOAT = imasdef.EMPTY_FLOAT
|
|
107
|
+
EMPTY_INT = imasdef.EMPTY_INT
|
|
108
|
+
ERASE_PULSE = imasdef.ERASE_PULSE
|
|
109
|
+
FORCE_CREATE_PULSE = imasdef.FORCE_CREATE_PULSE
|
|
110
|
+
FORCE_OPEN_PULSE = imasdef.FORCE_OPEN_PULSE
|
|
111
|
+
HDF5_BACKEND = imasdef.HDF5_BACKEND
|
|
112
|
+
IDS_TIME_MODE_HETEROGENEOUS = imasdef.IDS_TIME_MODE_HETEROGENEOUS
|
|
113
|
+
IDS_TIME_MODE_HOMOGENEOUS = imasdef.IDS_TIME_MODE_HOMOGENEOUS
|
|
114
|
+
IDS_TIME_MODE_INDEPENDENT = imasdef.IDS_TIME_MODE_INDEPENDENT
|
|
115
|
+
IDS_TIME_MODE_UNKNOWN = imasdef.IDS_TIME_MODE_UNKNOWN
|
|
116
|
+
IDS_TIME_MODES = imasdef.IDS_TIME_MODES
|
|
117
|
+
INTEGER_DATA = imasdef.INTEGER_DATA
|
|
118
|
+
LINEAR_INTERP = imasdef.LINEAR_INTERP
|
|
119
|
+
MDSPLUS_BACKEND = imasdef.MDSPLUS_BACKEND
|
|
120
|
+
MEMORY_BACKEND = imasdef.MEMORY_BACKEND
|
|
121
|
+
NODE_TYPE_STRUCTURE = imasdef.NODE_TYPE_STRUCTURE
|
|
122
|
+
OPEN_PULSE = imasdef.OPEN_PULSE
|
|
123
|
+
PREVIOUS_INTERP = imasdef.PREVIOUS_INTERP
|
|
124
|
+
READ_OP = imasdef.READ_OP
|
|
125
|
+
UDA_BACKEND = imasdef.UDA_BACKEND
|
|
126
|
+
UNDEFINED_INTERP = imasdef.UNDEFINED_INTERP
|
|
127
|
+
UNDEFINED_TIME = imasdef.UNDEFINED_TIME
|
|
128
|
+
WRITE_OP = imasdef.WRITE_OP
|
|
129
|
+
ASCII_SERIALIZER_PROTOCOL = getattr(imasdef, "ASCII_SERIALIZER_PROTOCOL", 60)
|
|
130
|
+
FLEXBUFFERS_SERIALIZER_PROTOCOL = getattr(
|
|
131
|
+
imasdef, "FLEXBUFFERS_SERIALIZER_PROTOCOL", None
|
|
132
|
+
)
|
|
133
|
+
DEFAULT_SERIALIZER_PROTOCOL = getattr(imasdef, "DEFAULT_SERIALIZER_PROTOCOL", 60)
|
|
134
|
+
|
|
135
|
+
else:
|
|
136
|
+
# Preset some constants which are used elsewhere
|
|
137
|
+
# this is a bit ugly, perhaps reuse the list of imports from above?
|
|
138
|
+
# it seems no problem to use None, since the use of the values should not
|
|
139
|
+
# be allowed, they are only used in operations which use the backend,
|
|
140
|
+
# which we (should) gate
|
|
141
|
+
ASCII_BACKEND = CHAR_DATA = CLOSE_PULSE = CLOSEST_INTERP = DOUBLE_DATA = None
|
|
142
|
+
FORCE_OPEN_PULSE = CREATE_PULSE = ERASE_PULSE = None
|
|
143
|
+
COMPLEX_DATA = FORCE_CREATE_PULSE = HDF5_BACKEND = None
|
|
144
|
+
INTEGER_DATA = LINEAR_INTERP = MDSPLUS_BACKEND = MEMORY_BACKEND = None
|
|
145
|
+
NODE_TYPE_STRUCTURE = OPEN_PULSE = PREVIOUS_INTERP = READ_OP = None
|
|
146
|
+
UDA_BACKEND = UNDEFINED_INTERP = UNDEFINED_TIME = WRITE_OP = None
|
|
147
|
+
# These constants are also useful when not working with the AL
|
|
148
|
+
EMPTY_FLOAT = -9e40
|
|
149
|
+
EMPTY_INT = -999_999_999
|
|
150
|
+
EMPTY_COMPLEX = complex(EMPTY_FLOAT, EMPTY_FLOAT)
|
|
151
|
+
IDS_TIME_MODE_UNKNOWN = EMPTY_INT
|
|
152
|
+
IDS_TIME_MODE_HETEROGENEOUS = 0
|
|
153
|
+
IDS_TIME_MODE_HOMOGENEOUS = 1
|
|
154
|
+
IDS_TIME_MODE_INDEPENDENT = 2
|
|
155
|
+
IDS_TIME_MODES = [0, 1, 2]
|
|
156
|
+
ASCII_SERIALIZER_PROTOCOL = 60
|
|
157
|
+
FLEXBUFFERS_SERIALIZER_PROTOCOL = None
|
|
158
|
+
DEFAULT_SERIALIZER_PROTOCOL = 60
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def needs_imas(func):
|
|
162
|
+
if has_imas:
|
|
163
|
+
return func
|
|
164
|
+
|
|
165
|
+
@functools.wraps(func)
|
|
166
|
+
def wrapper(*args, **kwargs):
|
|
167
|
+
raise RuntimeError(
|
|
168
|
+
f"Function {func.__name__} requires IMAS, but IMAS is not available."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return wrapper
|