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.

Files changed (108) hide show
  1. imas/__init__.py +34 -0
  2. imas/__main__.py +17 -0
  3. imas/_util.py +260 -0
  4. imas/_version.py +16 -0
  5. imas/assets/IDSDef2MDSpreTree.xsl +161 -0
  6. imas/assets/IDS_fake_toplevel.xml +318 -0
  7. imas/assets/IDS_minimal.xml +37 -0
  8. imas/assets/IDS_minimal_2.xml +37 -0
  9. imas/assets/IDS_minimal_struct_array.xml +43 -0
  10. imas/assets/IDS_minimal_types.xml +169 -0
  11. imas/assets/ITER_134173_106_core_profiles.ids +2438 -0
  12. imas/assets/ITER_134173_106_equilibrium.ids +3172 -0
  13. imas/assets/README.md +5 -0
  14. imas/assets/core_profiles.ids +2438 -0
  15. imas/assets/equilibrium.ids +3172 -0
  16. imas/backends/__init__.py +12 -0
  17. imas/backends/db_entry_impl.py +122 -0
  18. imas/backends/imas_core/__init__.py +4 -0
  19. imas/backends/imas_core/al_context.py +391 -0
  20. imas/backends/imas_core/db_entry_al.py +405 -0
  21. imas/backends/imas_core/db_entry_helpers.py +200 -0
  22. imas/backends/imas_core/imas_interface.py +216 -0
  23. imas/backends/imas_core/mdsplus_model.py +367 -0
  24. imas/backends/imas_core/uda_support.py +56 -0
  25. imas/backends/netcdf/__init__.py +4 -0
  26. imas/backends/netcdf/db_entry_nc.py +176 -0
  27. imas/backends/netcdf/ids2nc.py +306 -0
  28. imas/backends/netcdf/nc2ids.py +302 -0
  29. imas/backends/netcdf/nc_metadata.py +357 -0
  30. imas/backends/netcdf/nc_validate.py +53 -0
  31. imas/command/cli.py +239 -0
  32. imas/command/db_analysis.py +290 -0
  33. imas/command/helpers.py +45 -0
  34. imas/command/timer.py +77 -0
  35. imas/db_entry.py +802 -0
  36. imas/dd_helpers.py +300 -0
  37. imas/dd_zip.py +297 -0
  38. imas/exception.py +107 -0
  39. imas/ids_base.py +149 -0
  40. imas/ids_convert.py +920 -0
  41. imas/ids_coordinates.py +400 -0
  42. imas/ids_data_type.py +115 -0
  43. imas/ids_defs.py +171 -0
  44. imas/ids_factory.py +108 -0
  45. imas/ids_identifiers.py +125 -0
  46. imas/ids_metadata.py +307 -0
  47. imas/ids_path.py +254 -0
  48. imas/ids_primitive.py +510 -0
  49. imas/ids_struct_array.py +233 -0
  50. imas/ids_structure.py +331 -0
  51. imas/ids_toplevel.py +381 -0
  52. imas/setup_logging.py +64 -0
  53. imas/test/test_all_dd_versions.py +28 -0
  54. imas/test/test_cli.py +44 -0
  55. imas/test/test_dbentry.py +104 -0
  56. imas/test/test_dd_helpers.py +134 -0
  57. imas/test/test_dd_zip.py +29 -0
  58. imas/test/test_deepcopy.py +25 -0
  59. imas/test/test_exception.py +12 -0
  60. imas/test/test_get_sample.py +388 -0
  61. imas/test/test_hash.py +188 -0
  62. imas/test/test_helpers.py +354 -0
  63. imas/test/test_identifiers.py +105 -0
  64. imas/test/test_ids2nc.py +166 -0
  65. imas/test/test_ids_ascii_data.py +39 -0
  66. imas/test/test_ids_convert.py +415 -0
  67. imas/test/test_ids_coordinates.py +165 -0
  68. imas/test/test_ids_data_type.py +40 -0
  69. imas/test/test_ids_factory.py +35 -0
  70. imas/test/test_ids_metadata.py +66 -0
  71. imas/test/test_ids_mixin.py +20 -0
  72. imas/test/test_ids_path.py +192 -0
  73. imas/test/test_ids_primitive.py +109 -0
  74. imas/test/test_ids_struct_array.py +89 -0
  75. imas/test/test_ids_structure.py +70 -0
  76. imas/test/test_ids_toplevel.py +55 -0
  77. imas/test/test_ids_validate.py +343 -0
  78. imas/test/test_latest_dd_autofill.py +74 -0
  79. imas/test/test_latest_dd_manual.py +38 -0
  80. imas/test/test_latest_dd_resample.py +102 -0
  81. imas/test/test_lazy_loading.py +201 -0
  82. imas/test/test_list_occurrences.py +82 -0
  83. imas/test/test_minimal.py +37 -0
  84. imas/test/test_minimal_conversion.py +44 -0
  85. imas/test/test_minimal_io.py +21 -0
  86. imas/test/test_minimal_struct_array_io.py +60 -0
  87. imas/test/test_minimal_types.py +436 -0
  88. imas/test/test_minimal_types_io.py +70 -0
  89. imas/test/test_minimal_types_io_automatic.py +66 -0
  90. imas/test/test_nbc_change.py +408 -0
  91. imas/test/test_nc_autofill.py +13 -0
  92. imas/test/test_nc_entry.py +28 -0
  93. imas/test/test_nc_metadata.py +153 -0
  94. imas/test/test_nc_validation.py +197 -0
  95. imas/test/test_snippets.py +25 -0
  96. imas/test/test_static_ids.py +39 -0
  97. imas/test/test_str_1d.py +99 -0
  98. imas/test/test_time_slicing.py +182 -0
  99. imas/test/test_uda_support.py +12 -0
  100. imas/test/test_util.py +209 -0
  101. imas/training.py +45 -0
  102. imas/util.py +526 -0
  103. imas_python-0.1.dev1618.dist-info/LICENSE.txt +165 -0
  104. imas_python-0.1.dev1618.dist-info/METADATA +288 -0
  105. imas_python-0.1.dev1618.dist-info/RECORD +108 -0
  106. imas_python-0.1.dev1618.dist-info/WHEEL +5 -0
  107. imas_python-0.1.dev1618.dist-info/entry_points.txt +3 -0
  108. imas_python-0.1.dev1618.dist-info/top_level.txt +1 -0
@@ -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