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_path.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
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 paths to elements in an IDS
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Tuple, Union
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING: # Prevent circular imports
|
|
11
|
+
from imas.ids_base import IDSBase
|
|
12
|
+
from imas.ids_metadata import IDSMetadata
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
_IndexType = Union[None, str, int, "IDSPath", slice]
|
|
17
|
+
"""Type of an index in a path."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _split_on_matching_parens(path: str, open: str, close: str) -> List[str]:
|
|
21
|
+
"""Split a string on matching parentheses ``(``, ``)``.
|
|
22
|
+
|
|
23
|
+
Return a list [before_first_(, between(), between)(, ..., after_last)]
|
|
24
|
+
"""
|
|
25
|
+
cur_index = 0
|
|
26
|
+
ret = []
|
|
27
|
+
while True:
|
|
28
|
+
next_open = path.find(open, cur_index)
|
|
29
|
+
next_close = path.find(close, cur_index)
|
|
30
|
+
if next_open != -1:
|
|
31
|
+
if next_open > next_close:
|
|
32
|
+
raise ValueError(f"Unmatched parentheses in: {path}")
|
|
33
|
+
ret.append(path[cur_index:next_open])
|
|
34
|
+
# find matching closing bracket
|
|
35
|
+
cur_index = next_open + 1
|
|
36
|
+
next_open = path.find(open, next_open + 1)
|
|
37
|
+
while next_open != -1 and next_open < next_close:
|
|
38
|
+
next_close = path.find(close, next_close + 1)
|
|
39
|
+
if next_close == -1:
|
|
40
|
+
raise ValueError(f"Unmatched parentheses in: {path}")
|
|
41
|
+
next_open = path.find(open, next_open + 1)
|
|
42
|
+
ret.append(path[cur_index:next_close])
|
|
43
|
+
cur_index = next_close + 1
|
|
44
|
+
else:
|
|
45
|
+
ret.append(path[cur_index:])
|
|
46
|
+
break
|
|
47
|
+
return ret
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_number = re.compile(r"\d+", re.ASCII)
|
|
51
|
+
_slice = re.compile(r"\d*:\d*", re.ASCII)
|
|
52
|
+
_generic_index = re.compile(r"itime|i\d", re.ASCII)
|
|
53
|
+
# Naming conventions of DD nodes: start with a-z, a-z, 0-9 and _ allowed, but no
|
|
54
|
+
# consecutive double underscores.
|
|
55
|
+
_valid_field = re.compile(r"[a-z]([a-z0-9]|_(?!_))*", re.ASCII)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _parse_path(
|
|
59
|
+
path: str,
|
|
60
|
+
) -> Tuple[Tuple[str, ...], Tuple[_IndexType, ...]]:
|
|
61
|
+
"""Parse an IDS path into its constituent parts.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
path: an IDS path (e.g. ``"profiles_1d(itime)/zeff"``)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
path_parts: all field names, e.g. ``("profiles_1d", "zeff")``
|
|
68
|
+
path_indices: all field indices, e.g. ``("itime", None)``
|
|
69
|
+
"""
|
|
70
|
+
path_parts: List[str] = []
|
|
71
|
+
path_indices: List[Union[str, int, "IDSPath", slice]] = []
|
|
72
|
+
fortran_style = "(" in path
|
|
73
|
+
if fortran_style and "[" in path:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Cannot mix Fortran-style indexation with Python-style indexation: {path}"
|
|
76
|
+
)
|
|
77
|
+
split_path = _split_on_matching_parens(
|
|
78
|
+
path, "[("[fortran_style], "])"[fortran_style]
|
|
79
|
+
)
|
|
80
|
+
# split_path always has an odd number of items, iterate over pairs first
|
|
81
|
+
for i in range(0, len(split_path) - 1, 2):
|
|
82
|
+
part, index = split_path[i : i + 2]
|
|
83
|
+
if not part:
|
|
84
|
+
raise ValueError(f"Invalid empty node name in path: {path}")
|
|
85
|
+
parts = (part[1:] if part[0] == "/" else part).split("/")
|
|
86
|
+
path_parts.extend(parts)
|
|
87
|
+
path_indices.extend([None] * len(parts))
|
|
88
|
+
if _generic_index.fullmatch(index):
|
|
89
|
+
path_indices[-1] = index # keep as string
|
|
90
|
+
elif _number.fullmatch(index):
|
|
91
|
+
# if fortran-style (1-based index), decrease index by one
|
|
92
|
+
path_indices[-1] = int(index) - fortran_style
|
|
93
|
+
elif _slice.fullmatch(index):
|
|
94
|
+
start, stop = index.split(":")
|
|
95
|
+
# When using Fortran style, we should decrement the start index by one
|
|
96
|
+
# e.g. Fortran-style (2:3) == [1:3] == range(1, 3)
|
|
97
|
+
path_indices[-1] = slice(
|
|
98
|
+
int(start) - fortran_style if start else None,
|
|
99
|
+
int(stop) if stop else None,
|
|
100
|
+
)
|
|
101
|
+
else: # it must be another path, parse it:
|
|
102
|
+
path_indices[-1] = IDSPath(index)
|
|
103
|
+
if split_path[-1]:
|
|
104
|
+
part = split_path[-1]
|
|
105
|
+
parts = (part[1:] if part[0] == "/" else part).split("/")
|
|
106
|
+
path_parts.extend(parts)
|
|
107
|
+
path_indices.extend([None] * len(parts))
|
|
108
|
+
for part in path_parts:
|
|
109
|
+
if not _valid_field.fullmatch(part):
|
|
110
|
+
raise ValueError(f"Invalid node name '{part}' in path: {path}")
|
|
111
|
+
return tuple(path_parts), tuple(path_indices)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class IDSPath:
|
|
115
|
+
"""Represent a path in an IDS.
|
|
116
|
+
|
|
117
|
+
An IDS Path indicates the relative position of an element in an IDS and is similar
|
|
118
|
+
to a directory structure.
|
|
119
|
+
|
|
120
|
+
Paths consist of elements with an optional index, separated by slashes. For example:
|
|
121
|
+
|
|
122
|
+
- ``ids_properties/version_put/data_dictionary``, no indices
|
|
123
|
+
- ``profiles_1d(itime)/zeff``, using a dummy ``itime`` index
|
|
124
|
+
- ``distribution(1)/process(:)/nbi_unit``, using a Fortran-style explicit index (1)
|
|
125
|
+
and Fortran-style range selector (:)
|
|
126
|
+
- ``distribution[0]/process[:]/nbi_unit``, using a Python-style explicit index [0]
|
|
127
|
+
and Python-style range selector [:]
|
|
128
|
+
- ``coordinate_system(process(i1)/coordinate_index)/coordinate(1)`` using another
|
|
129
|
+
IDSPath as index.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
_cache: Dict[str, "IDSPath"] = {}
|
|
133
|
+
|
|
134
|
+
def __new__(cls, path: str) -> "IDSPath":
|
|
135
|
+
if path not in cls._cache:
|
|
136
|
+
cls._cache[path] = super().__new__(cls)
|
|
137
|
+
return cls._cache[path]
|
|
138
|
+
|
|
139
|
+
def __init__(self, path: str) -> None:
|
|
140
|
+
if hasattr(self, "_init_done"):
|
|
141
|
+
return # Already initialized, __new__ returned from cache
|
|
142
|
+
self._path = path
|
|
143
|
+
self.parts, self.indices = _parse_path(path)
|
|
144
|
+
self.is_time_path = self.parts and self.parts[-1] == "time"
|
|
145
|
+
# Prevent accidentally modifying attributes
|
|
146
|
+
self._init_done = True
|
|
147
|
+
|
|
148
|
+
def __setattr__(self, name: str, value: Any):
|
|
149
|
+
if hasattr(self, "_init_done"):
|
|
150
|
+
raise RuntimeError("Cannot set attribute: IDSPath is read-only.")
|
|
151
|
+
super().__setattr__(name, value)
|
|
152
|
+
|
|
153
|
+
def __len__(self) -> int:
|
|
154
|
+
return len(self.parts)
|
|
155
|
+
|
|
156
|
+
def __str__(self) -> str:
|
|
157
|
+
return self._path
|
|
158
|
+
|
|
159
|
+
def __repr__(self) -> str:
|
|
160
|
+
return f"IDSPath({self._path!r})"
|
|
161
|
+
|
|
162
|
+
def __hash__(self) -> int:
|
|
163
|
+
"""IDSPaths are immutable, we can be used e.g. as dict key."""
|
|
164
|
+
return hash(self._path)
|
|
165
|
+
|
|
166
|
+
def items(self) -> Iterator[Tuple[str, _IndexType]]:
|
|
167
|
+
"""``(part, index)`` iterator"""
|
|
168
|
+
return zip(self.parts, self.indices)
|
|
169
|
+
|
|
170
|
+
def goto(self, from_element: "IDSBase", *, from_root: bool = True) -> "IDSBase":
|
|
171
|
+
"""Go to this path, taking from_element as reference.
|
|
172
|
+
|
|
173
|
+
This returns the IDS node at the specified path, or raises an error when that
|
|
174
|
+
path cannot be found.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
from_element: IDS element to start the goto from
|
|
178
|
+
from_root: True iff we should start at the root, otherwise we start from the
|
|
179
|
+
provided IDS element (see example)
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
.. code-block:: python
|
|
183
|
+
|
|
184
|
+
cp = imas.IDSFactory().core_profiles()
|
|
185
|
+
cp.profiles_1d.resize(1)
|
|
186
|
+
element = cp.profiles_1d[0]
|
|
187
|
+
path1 = IDSPath("ids_properties/homogeneous_time")
|
|
188
|
+
assert path1.goto(element, True) is cp.ids_properties.homogeneous_time
|
|
189
|
+
path2 = IDSPath("electrons/temperature")
|
|
190
|
+
assert path2.goto(element, False) is element.electrons.temperature
|
|
191
|
+
"""
|
|
192
|
+
from_path = from_element.metadata.path # this path doesn't have indices
|
|
193
|
+
element = None if from_root else from_element
|
|
194
|
+
for i, (part, index) in enumerate(self.items()):
|
|
195
|
+
if isinstance(index, slice):
|
|
196
|
+
raise NotImplementedError("Cannot go to slices")
|
|
197
|
+
if element is None:
|
|
198
|
+
if i < len(from_path) and part == from_path.parts[i]:
|
|
199
|
+
if index is not None and not isinstance(index, str):
|
|
200
|
+
logger.warning(
|
|
201
|
+
"Ignoring index %s while resolving path %s.", index, self
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
# last common parent found, set element to it
|
|
205
|
+
element = from_element
|
|
206
|
+
for _ in range(len(from_path) - i):
|
|
207
|
+
element = element._dd_parent
|
|
208
|
+
# Not using `else` on the next line, because element might be set above
|
|
209
|
+
if element is not None:
|
|
210
|
+
element = getattr(element, part)
|
|
211
|
+
if index is not None:
|
|
212
|
+
if isinstance(index, str):
|
|
213
|
+
raise ValueError(f"Unexpected index {index} in path '{self}'.")
|
|
214
|
+
if isinstance(index, IDSPath):
|
|
215
|
+
val = index.goto(from_element).value
|
|
216
|
+
if not isinstance(val, int):
|
|
217
|
+
raise ValueError(f"Invalid index {index} in path '{self}'.")
|
|
218
|
+
index = val - 1 # Stored as 1-based index
|
|
219
|
+
element = element[index]
|
|
220
|
+
if element is None: # We point to from_element or a direct ancestor
|
|
221
|
+
element = from_element
|
|
222
|
+
for _ in range(len(from_path) - len(self)):
|
|
223
|
+
element = element._dd_parent
|
|
224
|
+
return element
|
|
225
|
+
|
|
226
|
+
def goto_metadata(self, from_metadata: "IDSMetadata") -> "IDSMetadata":
|
|
227
|
+
"""Go to the metadata of this path, taking from_metadata as reference.
|
|
228
|
+
|
|
229
|
+
This returns the metadata of the metadata IDS node at the specified path,
|
|
230
|
+
or raises an error when that path cannot be found. It is useful when you want to
|
|
231
|
+
retrieve the metadata of an IDS node without having to traverse the IDS nodes.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
from_metadata: IDS metadata node to start the goto from
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
.. code-block:: python
|
|
238
|
+
|
|
239
|
+
es = imas.IDSFactory().edge_sources()
|
|
240
|
+
path = IDSPath("source/ggd/ion/energy")
|
|
241
|
+
energy_metadata = path.goto_metadata(es.metadata)
|
|
242
|
+
"""
|
|
243
|
+
metadata_node = from_metadata
|
|
244
|
+
for part, _ in self.items():
|
|
245
|
+
if part in metadata_node._children:
|
|
246
|
+
metadata_node = metadata_node[part]
|
|
247
|
+
else:
|
|
248
|
+
raise ValueError(f"Could not find {part} in {metadata_node}.")
|
|
249
|
+
return metadata_node
|
|
250
|
+
|
|
251
|
+
def is_ancestor_of(self, other_path: "IDSPath") -> bool:
|
|
252
|
+
"""Test if this path is an ancestor of the other path."""
|
|
253
|
+
len_self = len(self)
|
|
254
|
+
return len_self < len(other_path) and other_path.parts[:len_self] == self.parts
|