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
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""DBEntry implementation using imas_core as a backend."""
|
|
2
|
+
|
|
3
|
+
import gc
|
|
4
|
+
import getpass
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from collections import deque
|
|
8
|
+
from typing import Any, Deque, List, Optional, Union
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
from imas.backends.db_entry_impl import GetSampleParameters, GetSliceParameters
|
|
12
|
+
from imas.db_entry import DBEntryImpl
|
|
13
|
+
from imas.exception import DataEntryException, LowlevelError
|
|
14
|
+
from imas.ids_convert import NBCPathMap, dd_version_map_from_factories
|
|
15
|
+
from imas.ids_defs import (
|
|
16
|
+
ASCII_BACKEND,
|
|
17
|
+
CHAR_DATA,
|
|
18
|
+
CLOSE_PULSE,
|
|
19
|
+
CREATE_PULSE,
|
|
20
|
+
ERASE_PULSE,
|
|
21
|
+
FORCE_CREATE_PULSE,
|
|
22
|
+
FORCE_OPEN_PULSE,
|
|
23
|
+
HDF5_BACKEND,
|
|
24
|
+
IDS_TIME_MODE_UNKNOWN,
|
|
25
|
+
IDS_TIME_MODES,
|
|
26
|
+
INTEGER_DATA,
|
|
27
|
+
MDSPLUS_BACKEND,
|
|
28
|
+
MEMORY_BACKEND,
|
|
29
|
+
OPEN_PULSE,
|
|
30
|
+
READ_OP,
|
|
31
|
+
UDA_BACKEND,
|
|
32
|
+
UNDEFINED_INTERP,
|
|
33
|
+
UNDEFINED_TIME,
|
|
34
|
+
WRITE_OP,
|
|
35
|
+
)
|
|
36
|
+
from imas.ids_factory import IDSFactory
|
|
37
|
+
from imas.ids_metadata import IDSType
|
|
38
|
+
from imas.ids_toplevel import IDSToplevel
|
|
39
|
+
|
|
40
|
+
from .al_context import ALContext, LazyALContext
|
|
41
|
+
from .db_entry_helpers import delete_children, get_children, put_children
|
|
42
|
+
from .imas_interface import LLInterfaceError, has_imas, ll_interface
|
|
43
|
+
from .mdsplus_model import ensure_data_dir, mdsplus_model_dir
|
|
44
|
+
from .uda_support import extract_idsdef, get_dd_version_from_idsdef_xml
|
|
45
|
+
|
|
46
|
+
_BACKEND_NAME = {
|
|
47
|
+
ASCII_BACKEND: "ascii",
|
|
48
|
+
HDF5_BACKEND: "hdf5",
|
|
49
|
+
MEMORY_BACKEND: "memory",
|
|
50
|
+
MDSPLUS_BACKEND: "mdsplus",
|
|
51
|
+
UDA_BACKEND: "uda",
|
|
52
|
+
}
|
|
53
|
+
_OPEN_MODES = {
|
|
54
|
+
"r": OPEN_PULSE,
|
|
55
|
+
"a": FORCE_OPEN_PULSE,
|
|
56
|
+
"w": FORCE_CREATE_PULSE,
|
|
57
|
+
"x": CREATE_PULSE,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
logger = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def require_imas_available():
|
|
64
|
+
if not has_imas:
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
"The IMAS Core library is not available. Please install 'imas_core', "
|
|
67
|
+
"or load a supported IMAS module if you use an HPC environment."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ALDBEntryImpl(DBEntryImpl):
|
|
72
|
+
"""DBEntry implementation using imas_core as a backend."""
|
|
73
|
+
|
|
74
|
+
"""Map to the expected open_pulse (AL4) / begin_dataentry_action (AL5) argument."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, backend: str, ctx: ALContext, factory: IDSFactory):
|
|
77
|
+
self.backend = backend
|
|
78
|
+
self._db_ctx = ctx
|
|
79
|
+
self._ids_factory = factory
|
|
80
|
+
self._lazy_ctx_cache: Deque[ALContext] = deque()
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_uri(cls, uri: str, mode: str, factory: IDSFactory) -> "ALDBEntryImpl":
|
|
84
|
+
require_imas_available()
|
|
85
|
+
if mode not in _OPEN_MODES:
|
|
86
|
+
modes = list(_OPEN_MODES)
|
|
87
|
+
raise ValueError(f"Unknown mode {mode!r}, was expecting any of {modes}")
|
|
88
|
+
return cls._from_uri(uri, _OPEN_MODES[mode], factory)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_pulse_run(
|
|
92
|
+
cls,
|
|
93
|
+
backend_id: int,
|
|
94
|
+
db_name: str,
|
|
95
|
+
pulse: int,
|
|
96
|
+
run: int,
|
|
97
|
+
user_name: Optional[str],
|
|
98
|
+
data_version: Optional[str],
|
|
99
|
+
mode: int,
|
|
100
|
+
options: Any,
|
|
101
|
+
factory: IDSFactory,
|
|
102
|
+
) -> "ALDBEntryImpl":
|
|
103
|
+
# Raise an error if imas is not available
|
|
104
|
+
require_imas_available()
|
|
105
|
+
|
|
106
|
+
# Set defaults
|
|
107
|
+
user_name = user_name or getpass.getuser()
|
|
108
|
+
data_version = data_version or factory.dd_version
|
|
109
|
+
options = options if options else ""
|
|
110
|
+
|
|
111
|
+
if ll_interface._al_version.major >= 5:
|
|
112
|
+
# We need a URI for AL 5 or later, construct from legacy parameters
|
|
113
|
+
status, uri = ll_interface.build_uri_from_legacy_parameters(
|
|
114
|
+
backend_id, pulse, run, user_name, db_name, data_version, options
|
|
115
|
+
)
|
|
116
|
+
if status != 0:
|
|
117
|
+
raise LowlevelError("build URI from legacy parameters", status)
|
|
118
|
+
|
|
119
|
+
return cls._from_uri(uri, mode, factory)
|
|
120
|
+
|
|
121
|
+
else:
|
|
122
|
+
# AL4 legacy support:
|
|
123
|
+
backend = _BACKEND_NAME.get(backend_id, "")
|
|
124
|
+
cls._setup_backend(backend, mode, factory, user_name, db_name, run)
|
|
125
|
+
|
|
126
|
+
status, ctx = ll_interface.begin_pulse_action(
|
|
127
|
+
backend_id, pulse, run, user_name, db_name, data_version
|
|
128
|
+
)
|
|
129
|
+
if status != 0:
|
|
130
|
+
raise LowlevelError("begin pulse action", status)
|
|
131
|
+
|
|
132
|
+
status = ll_interface.open_pulse(ctx, mode, options)
|
|
133
|
+
if status != 0:
|
|
134
|
+
raise LowlevelError("opening/creating data entry", status)
|
|
135
|
+
|
|
136
|
+
return cls(backend, ALContext(ctx), factory)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def _from_uri(cls, uri: str, mode: int, factory: IDSFactory) -> "ALDBEntryImpl":
|
|
140
|
+
"""Helper method to actually open/create the dataentry."""
|
|
141
|
+
backend = urlparse(uri).path.lower().lstrip("/")
|
|
142
|
+
cls._setup_backend(backend, mode, factory)
|
|
143
|
+
|
|
144
|
+
status, ctx = ll_interface.begin_dataentry_action(uri, mode)
|
|
145
|
+
if status != 0:
|
|
146
|
+
raise LowlevelError("opening/creating data entry", status)
|
|
147
|
+
|
|
148
|
+
return cls(backend, ALContext(ctx), factory)
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def _setup_backend(
|
|
152
|
+
cls,
|
|
153
|
+
backend: str,
|
|
154
|
+
mode: int,
|
|
155
|
+
factory: IDSFactory,
|
|
156
|
+
user_name: str = "",
|
|
157
|
+
db_name="",
|
|
158
|
+
run=1,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Custom logic for preparing some backends.
|
|
161
|
+
|
|
162
|
+
Note: user_name, db_name and run are only used for AL 4.x, they can be
|
|
163
|
+
omitted when using AL 5 or later.
|
|
164
|
+
"""
|
|
165
|
+
if backend == "mdsplus":
|
|
166
|
+
# MDSplus models:
|
|
167
|
+
if mode != OPEN_PULSE:
|
|
168
|
+
# Building the MDS+ models is required when creating a new Data Entry
|
|
169
|
+
ids_path = mdsplus_model_dir(factory)
|
|
170
|
+
if ids_path:
|
|
171
|
+
os.environ["ids_path"] = ids_path
|
|
172
|
+
|
|
173
|
+
if ll_interface._al_version.major == 4:
|
|
174
|
+
# Ensure the data directory exists
|
|
175
|
+
# Note: MDSPLUS model directory only uses the major version component of
|
|
176
|
+
# IMAS_VERSION, so we'll take the first character of IMAS_VERSION:
|
|
177
|
+
version = factory.version[0]
|
|
178
|
+
ensure_data_dir(user_name, db_name, version, run)
|
|
179
|
+
|
|
180
|
+
elif backend == "hdf5":
|
|
181
|
+
pass # nothing to set up
|
|
182
|
+
|
|
183
|
+
elif backend == "memory":
|
|
184
|
+
pass # nothing to set up
|
|
185
|
+
|
|
186
|
+
elif backend == "ascii":
|
|
187
|
+
pass # nothing to set up
|
|
188
|
+
|
|
189
|
+
elif backend == "uda":
|
|
190
|
+
# Set IDSDEF_PATH to point the UDA backend to the selected DD version
|
|
191
|
+
idsdef_path = None
|
|
192
|
+
|
|
193
|
+
if factory._xml_path is not None:
|
|
194
|
+
# Factory was constructed with an explicit XML path, point UDA to that:
|
|
195
|
+
idsdef_path = factory._xml_path
|
|
196
|
+
|
|
197
|
+
elif "IMAS_PREFIX" in os.environ:
|
|
198
|
+
# Check if UDA can use the IDSDef.xml stored in $IMAS_PREFIX/include/
|
|
199
|
+
idsdef_path = os.environ["IMAS_PREFIX"] + "/include/IDSDef.xml"
|
|
200
|
+
if get_dd_version_from_idsdef_xml(idsdef_path) != factory.version:
|
|
201
|
+
idsdef_path = None
|
|
202
|
+
|
|
203
|
+
if idsdef_path is None:
|
|
204
|
+
# Extract XML from the DD zip and point UDA to it
|
|
205
|
+
idsdef_path = extract_idsdef(factory.version)
|
|
206
|
+
|
|
207
|
+
os.environ["IDSDEF_PATH"] = idsdef_path
|
|
208
|
+
logger.warning(
|
|
209
|
+
"The UDA backend is not tested with "
|
|
210
|
+
"imas-python and may not work properly. "
|
|
211
|
+
"Please raise any issues you find."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
elif backend == "flexbuffers":
|
|
215
|
+
pass # nothing to set up
|
|
216
|
+
|
|
217
|
+
else:
|
|
218
|
+
logger.warning("Backend %s is unknown to imas-python", backend)
|
|
219
|
+
|
|
220
|
+
def close(self, *, erase: bool = False) -> None:
|
|
221
|
+
if self._db_ctx is None:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
self._clear_lazy_ctx_cache()
|
|
225
|
+
|
|
226
|
+
mode = ERASE_PULSE if erase else CLOSE_PULSE
|
|
227
|
+
status = ll_interface.close_pulse(self._db_ctx.ctx, mode)
|
|
228
|
+
if status != 0:
|
|
229
|
+
raise LowlevelError("close data entry", status)
|
|
230
|
+
|
|
231
|
+
ll_interface.end_action(self._db_ctx.ctx)
|
|
232
|
+
self._db_ctx = None
|
|
233
|
+
|
|
234
|
+
def _clear_lazy_ctx_cache(self) -> None:
|
|
235
|
+
"""Close any cached lazy contexts"""
|
|
236
|
+
while self._lazy_ctx_cache:
|
|
237
|
+
self._lazy_ctx_cache.pop().close()
|
|
238
|
+
|
|
239
|
+
def get(
|
|
240
|
+
self,
|
|
241
|
+
ids_name: str,
|
|
242
|
+
occurrence: int,
|
|
243
|
+
parameters: Union[None, GetSliceParameters, GetSampleParameters],
|
|
244
|
+
destination: IDSToplevel,
|
|
245
|
+
lazy: bool,
|
|
246
|
+
nbc_map: Optional[NBCPathMap],
|
|
247
|
+
) -> None:
|
|
248
|
+
if self._db_ctx is None:
|
|
249
|
+
raise RuntimeError("Database entry is not open.")
|
|
250
|
+
if lazy and self.backend == "ascii":
|
|
251
|
+
raise RuntimeError("Lazy loading is not supported by the ASCII backend.")
|
|
252
|
+
|
|
253
|
+
# Mixing contexts can be problematic, ensure all lazy contexts are closed:
|
|
254
|
+
self._clear_lazy_ctx_cache()
|
|
255
|
+
|
|
256
|
+
ll_path = ids_name
|
|
257
|
+
if occurrence != 0:
|
|
258
|
+
ll_path += f"/{occurrence}"
|
|
259
|
+
|
|
260
|
+
with self._db_ctx.global_action(ll_path, READ_OP) as read_ctx:
|
|
261
|
+
time_mode_path = "ids_properties/homogeneous_time"
|
|
262
|
+
time_mode = read_ctx.read_data(time_mode_path, "", INTEGER_DATA, 0)
|
|
263
|
+
# This is already checked by read_dd_version, but ensure:
|
|
264
|
+
assert time_mode in IDS_TIME_MODES
|
|
265
|
+
|
|
266
|
+
if lazy:
|
|
267
|
+
context = LazyALContext(dbentry=self, nbc_map=nbc_map, time_mode=time_mode)
|
|
268
|
+
else:
|
|
269
|
+
context = self._db_ctx
|
|
270
|
+
# Now fill the IDSToplevel
|
|
271
|
+
if parameters is None or destination.metadata.type is IDSType.CONSTANT:
|
|
272
|
+
# called from get(), or when the IDS is constant (see IMAS-3330)
|
|
273
|
+
manager = context.global_action(ll_path, READ_OP)
|
|
274
|
+
elif isinstance(parameters, GetSliceParameters):
|
|
275
|
+
manager = context.slice_action(
|
|
276
|
+
ll_path,
|
|
277
|
+
READ_OP,
|
|
278
|
+
parameters.time_requested,
|
|
279
|
+
parameters.interpolation_method,
|
|
280
|
+
)
|
|
281
|
+
elif isinstance(parameters, GetSampleParameters):
|
|
282
|
+
manager = context.timerange_action(
|
|
283
|
+
ll_path,
|
|
284
|
+
READ_OP,
|
|
285
|
+
parameters.tmin,
|
|
286
|
+
parameters.tmax,
|
|
287
|
+
parameters.dtime,
|
|
288
|
+
parameters.interpolation_method,
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
raise TypeError(f"Incorrect type for parameters: {type(parameters)}.")
|
|
292
|
+
|
|
293
|
+
with manager as read_ctx:
|
|
294
|
+
if lazy:
|
|
295
|
+
destination._set_lazy_context(read_ctx)
|
|
296
|
+
else:
|
|
297
|
+
# Get may create LOTS of new objects. Temporarily disable Python's
|
|
298
|
+
# garbage collector to speed up the get:
|
|
299
|
+
gc_enabled = gc.isenabled()
|
|
300
|
+
gc.disable()
|
|
301
|
+
get_children(destination, read_ctx, time_mode, nbc_map)
|
|
302
|
+
if gc_enabled:
|
|
303
|
+
gc.enable()
|
|
304
|
+
|
|
305
|
+
return destination
|
|
306
|
+
|
|
307
|
+
def read_dd_version(self, ids_name: str, occurrence: int) -> str:
|
|
308
|
+
if self._db_ctx is None:
|
|
309
|
+
raise RuntimeError("Database entry is not open.")
|
|
310
|
+
# Mixing contexts can be problematic, ensure all lazy contexts are closed:
|
|
311
|
+
self._clear_lazy_ctx_cache()
|
|
312
|
+
|
|
313
|
+
ll_path = ids_name
|
|
314
|
+
if occurrence != 0:
|
|
315
|
+
ll_path += f"/{occurrence}"
|
|
316
|
+
|
|
317
|
+
with self._db_ctx.global_action(ll_path, READ_OP) as read_ctx:
|
|
318
|
+
time_mode_path = "ids_properties/homogeneous_time"
|
|
319
|
+
time_mode = read_ctx.read_data(time_mode_path, "", INTEGER_DATA, 0)
|
|
320
|
+
dd_version_path = "ids_properties/version_put/data_dictionary"
|
|
321
|
+
dd_version = read_ctx.read_data(dd_version_path, "", CHAR_DATA, 1)
|
|
322
|
+
|
|
323
|
+
if time_mode not in IDS_TIME_MODES:
|
|
324
|
+
raise DataEntryException(
|
|
325
|
+
f"IDS {ids_name!r}, occurrence {occurrence} is empty."
|
|
326
|
+
)
|
|
327
|
+
return dd_version
|
|
328
|
+
|
|
329
|
+
def put(self, ids: IDSToplevel, occurrence: int, is_slice: bool) -> None:
|
|
330
|
+
if self._db_ctx is None:
|
|
331
|
+
raise RuntimeError("Database entry is not open.")
|
|
332
|
+
|
|
333
|
+
# Mixing contexts can be problematic, ensure all lazy contexts are closed:
|
|
334
|
+
self._clear_lazy_ctx_cache()
|
|
335
|
+
|
|
336
|
+
ids_name = ids.metadata.name
|
|
337
|
+
# Create a version conversion map, if needed
|
|
338
|
+
nbc_map = None
|
|
339
|
+
if ids._version != self._ids_factory._version:
|
|
340
|
+
ddmap, source_is_older = dd_version_map_from_factories(
|
|
341
|
+
ids_name, ids._parent, self._ids_factory
|
|
342
|
+
)
|
|
343
|
+
nbc_map = ddmap.old_to_new if source_is_older else ddmap.new_to_old
|
|
344
|
+
|
|
345
|
+
ll_path = ids_name
|
|
346
|
+
if occurrence != 0:
|
|
347
|
+
ll_path += f"/{occurrence}"
|
|
348
|
+
|
|
349
|
+
time_mode = ids.ids_properties.homogeneous_time
|
|
350
|
+
if is_slice:
|
|
351
|
+
with self._db_ctx.global_action(ll_path, READ_OP) as read_ctx:
|
|
352
|
+
db_time_mode = read_ctx.read_data(
|
|
353
|
+
"ids_properties/homogeneous_time", "", INTEGER_DATA, 0
|
|
354
|
+
)
|
|
355
|
+
if db_time_mode == IDS_TIME_MODE_UNKNOWN:
|
|
356
|
+
# No data yet on disk, so just put everything
|
|
357
|
+
is_slice = False
|
|
358
|
+
elif db_time_mode != time_mode:
|
|
359
|
+
raise DataEntryException(
|
|
360
|
+
f"Cannot change homogeneous_time from {db_time_mode} to {time_mode}"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
if not is_slice:
|
|
364
|
+
# put() must first delete any existing data
|
|
365
|
+
with self._db_ctx.global_action(ll_path, WRITE_OP) as write_ctx:
|
|
366
|
+
# New IDS to ensure all fields in "our" DD version are deleted
|
|
367
|
+
# If ids is in another version, we might not erase all fields
|
|
368
|
+
delete_children(self._ids_factory.new(ids_name).metadata, write_ctx)
|
|
369
|
+
|
|
370
|
+
if is_slice:
|
|
371
|
+
manager = self._db_ctx.slice_action(
|
|
372
|
+
ll_path, WRITE_OP, UNDEFINED_TIME, UNDEFINED_INTERP
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
manager = self._db_ctx.global_action(ll_path, WRITE_OP)
|
|
376
|
+
verify_maxoccur = self.backend == "mdsplus"
|
|
377
|
+
with manager as write_ctx:
|
|
378
|
+
put_children(ids, write_ctx, time_mode, is_slice, nbc_map, verify_maxoccur)
|
|
379
|
+
|
|
380
|
+
def access_layer_version(self) -> str:
|
|
381
|
+
return ll_interface._al_version_str
|
|
382
|
+
|
|
383
|
+
def delete_data(self, ids_name: str, occurrence: int) -> None:
|
|
384
|
+
if self._db_ctx is None:
|
|
385
|
+
raise RuntimeError("Database entry is not open.")
|
|
386
|
+
# Mixing contexts can be problematic, ensure all lazy contexts are closed:
|
|
387
|
+
self._clear_lazy_ctx_cache()
|
|
388
|
+
|
|
389
|
+
ll_path = ids_name
|
|
390
|
+
if occurrence != 0:
|
|
391
|
+
ll_path += f"/{occurrence}"
|
|
392
|
+
ids = self._ids_factory.new(ids_name)
|
|
393
|
+
with self._db_ctx.global_action(ll_path, WRITE_OP) as write_ctx:
|
|
394
|
+
delete_children(ids.metadata, write_ctx, "")
|
|
395
|
+
|
|
396
|
+
def list_all_occurrences(self, ids_name: str) -> List[int]:
|
|
397
|
+
try:
|
|
398
|
+
occurrence_list = self._db_ctx.list_all_occurrences(ids_name)
|
|
399
|
+
except LLInterfaceError:
|
|
400
|
+
# al_get_occurrences is not available in the lowlevel
|
|
401
|
+
raise RuntimeError(
|
|
402
|
+
"list_all_occurrences is not available. "
|
|
403
|
+
"Access Layer 5.1 or newer is required."
|
|
404
|
+
) from None
|
|
405
|
+
return occurrence_list
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# This file is part of imas-python.
|
|
2
|
+
# You should have received the imas-python LICENSE file with this project.
|
|
3
|
+
"""Helper methods for loading data from and storing data to Data Entries.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from imas.ids_base import IDSBase
|
|
11
|
+
from imas.ids_convert import NBCPathMap
|
|
12
|
+
from imas.ids_data_type import IDSDataType
|
|
13
|
+
from imas.ids_defs import IDS_TIME_MODE_HOMOGENEOUS, IDS_TIME_MODE_INDEPENDENT
|
|
14
|
+
from imas.ids_metadata import IDSMetadata
|
|
15
|
+
from imas.ids_struct_array import IDSStructArray
|
|
16
|
+
from imas.ids_structure import IDSStructure
|
|
17
|
+
|
|
18
|
+
from .al_context import ALContext, LazyALContext
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_children(
|
|
22
|
+
structure: IDSStructure,
|
|
23
|
+
ctx: ALContext,
|
|
24
|
+
time_mode: int,
|
|
25
|
+
nbc_map: Optional[NBCPathMap],
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Recursively get all children of an IDSStructure."""
|
|
28
|
+
# NOTE: changes in this method must be propagated to _get_child and vice versa
|
|
29
|
+
# Performance: this method is specialized for the non-lazy get
|
|
30
|
+
|
|
31
|
+
for name, child_meta in structure._children.items():
|
|
32
|
+
if time_mode == IDS_TIME_MODE_INDEPENDENT and child_meta.type.is_dynamic:
|
|
33
|
+
continue # skip dynamic (time-dependent) nodes
|
|
34
|
+
|
|
35
|
+
path = child_meta.path_string
|
|
36
|
+
data_type = child_meta.data_type
|
|
37
|
+
|
|
38
|
+
if nbc_map and path in nbc_map:
|
|
39
|
+
if nbc_map.path[path] is None:
|
|
40
|
+
continue # element does not exist in the on-disk DD version
|
|
41
|
+
new_path = nbc_map.ctxpath[path]
|
|
42
|
+
timebase = nbc_map.tbp[path]
|
|
43
|
+
elif nbc_map and path in nbc_map.type_change:
|
|
44
|
+
continue # we don't handle type changes when converting implicitly
|
|
45
|
+
else:
|
|
46
|
+
new_path = child_meta._ctx_path
|
|
47
|
+
timebase = child_meta.timebasepath
|
|
48
|
+
|
|
49
|
+
# Override time base for homogeneous time
|
|
50
|
+
if timebase and time_mode == IDS_TIME_MODE_HOMOGENEOUS:
|
|
51
|
+
timebase = "/time"
|
|
52
|
+
|
|
53
|
+
if data_type is IDSDataType.STRUCT_ARRAY:
|
|
54
|
+
# Regular get/get_slice:
|
|
55
|
+
with ctx.arraystruct_action(new_path, timebase, 0) as (new_ctx, size):
|
|
56
|
+
if size > 0:
|
|
57
|
+
element = getattr(structure, name)
|
|
58
|
+
element.resize(size)
|
|
59
|
+
for item in element:
|
|
60
|
+
get_children(item, new_ctx, time_mode, nbc_map)
|
|
61
|
+
new_ctx.iterate_over_arraystruct(1)
|
|
62
|
+
|
|
63
|
+
elif data_type is IDSDataType.STRUCTURE:
|
|
64
|
+
element = getattr(structure, name)
|
|
65
|
+
get_children(element, ctx, time_mode, nbc_map)
|
|
66
|
+
|
|
67
|
+
else: # Data elements
|
|
68
|
+
ndim = child_meta._al_ndim
|
|
69
|
+
data = ctx.read_data(new_path, timebase, data_type.al_type, ndim)
|
|
70
|
+
if not (
|
|
71
|
+
# Empty arrays and STR_1D
|
|
72
|
+
data is None
|
|
73
|
+
# EMPTY_INT, EMPTY_FLOAT, EMPTY_COMPLEX, empty string
|
|
74
|
+
or (child_meta.ndim == 0 and data == data_type.default)
|
|
75
|
+
):
|
|
76
|
+
# NOTE: bypassing IDSPrimitive.value.setter logic
|
|
77
|
+
getattr(structure, name)._IDSPrimitive__value = data
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_child(child: IDSBase, ctx: Optional[LazyALContext]):
|
|
81
|
+
"""Get a single child when required (lazy loading)."""
|
|
82
|
+
# NOTE: changes in this method must be propagated to _get_children and vice versa
|
|
83
|
+
# Performance: this method is specialized for the lazy get
|
|
84
|
+
|
|
85
|
+
# ctx can be None when the parent structure does not exist in the on-disk DD version
|
|
86
|
+
if ctx is None:
|
|
87
|
+
return # There is no data to be loaded
|
|
88
|
+
|
|
89
|
+
time_mode = ctx.time_mode
|
|
90
|
+
if time_mode == IDS_TIME_MODE_INDEPENDENT and child.metadata.type.is_dynamic:
|
|
91
|
+
return # skip dynamic (time-dependent) nodes
|
|
92
|
+
|
|
93
|
+
child_meta = child.metadata
|
|
94
|
+
path = child_meta.path_string
|
|
95
|
+
data_type = child_meta.data_type
|
|
96
|
+
nbc_map = ctx.nbc_map
|
|
97
|
+
|
|
98
|
+
if nbc_map and path in nbc_map:
|
|
99
|
+
if nbc_map.path[path] is None:
|
|
100
|
+
return # element does not exist in the on-disk DD version
|
|
101
|
+
new_path = nbc_map.ctxpath[path]
|
|
102
|
+
timebase = nbc_map.tbp[path]
|
|
103
|
+
elif nbc_map and path in nbc_map.type_change:
|
|
104
|
+
return # we don't handle type changes when converting implicitly
|
|
105
|
+
else:
|
|
106
|
+
new_path = child_meta._ctx_path
|
|
107
|
+
timebase = child_meta.timebasepath
|
|
108
|
+
|
|
109
|
+
# Override time base for homogeneous time
|
|
110
|
+
if timebase and time_mode == IDS_TIME_MODE_HOMOGENEOUS:
|
|
111
|
+
timebase = "/time"
|
|
112
|
+
|
|
113
|
+
if data_type is IDSDataType.STRUCT_ARRAY:
|
|
114
|
+
aos_ctx = ctx.arraystruct_action(new_path, timebase, 0)
|
|
115
|
+
child._set_lazy_context(aos_ctx)
|
|
116
|
+
|
|
117
|
+
elif data_type is IDSDataType.STRUCTURE:
|
|
118
|
+
child._set_lazy_context(ctx)
|
|
119
|
+
|
|
120
|
+
else: # Data elements
|
|
121
|
+
ndim = child_meta._al_ndim
|
|
122
|
+
data = ctx.get_context().read_data(new_path, timebase, data_type.al_type, ndim)
|
|
123
|
+
if not (
|
|
124
|
+
# Empty arrays and STR_1D
|
|
125
|
+
data is None
|
|
126
|
+
# EMPTY_INT, EMPTY_FLOAT, EMPTY_COMPLEX, empty string
|
|
127
|
+
or (child_meta.ndim == 0 and data == data_type.default)
|
|
128
|
+
):
|
|
129
|
+
if isinstance(data, np.ndarray):
|
|
130
|
+
# Convert the numpy array to a read-only view
|
|
131
|
+
data = data.view()
|
|
132
|
+
data.flags.writeable = False
|
|
133
|
+
# NOTE: bypassing IDSPrimitive.value.setter logic
|
|
134
|
+
child._IDSPrimitive__value = data
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def delete_children(structure: IDSMetadata, ctx: ALContext) -> None:
|
|
138
|
+
"""Recursively delete all children of an IDSStructure"""
|
|
139
|
+
for child_meta in structure._children.values():
|
|
140
|
+
if child_meta.data_type is IDSDataType.STRUCTURE:
|
|
141
|
+
delete_children(child_meta, ctx)
|
|
142
|
+
else:
|
|
143
|
+
ctx.delete_data(child_meta._ctx_path)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def put_children(
|
|
147
|
+
structure: IDSStructure,
|
|
148
|
+
ctx: ALContext,
|
|
149
|
+
time_mode: int,
|
|
150
|
+
is_slice: bool,
|
|
151
|
+
nbc_map: Optional[NBCPathMap],
|
|
152
|
+
verify_maxoccur: bool,
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Recursively put all children of an IDSStructure"""
|
|
155
|
+
# Note: when putting a slice, we do not need to descend into IDSStructure and
|
|
156
|
+
# IDSStructArray elements if they don't contain dynamic data nodes. That is hard to
|
|
157
|
+
# detect now, so we just recurse and check the data elements
|
|
158
|
+
for element in structure.iter_nonempty_():
|
|
159
|
+
if time_mode == IDS_TIME_MODE_INDEPENDENT and element.metadata.type.is_dynamic:
|
|
160
|
+
continue # skip dynamic data when in time independent mode
|
|
161
|
+
|
|
162
|
+
path = element.metadata.path_string
|
|
163
|
+
if nbc_map and path in nbc_map:
|
|
164
|
+
if nbc_map.path[path] is None:
|
|
165
|
+
continue # element does not exist in the on-disk DD version
|
|
166
|
+
new_path = nbc_map.ctxpath[path]
|
|
167
|
+
timebase = nbc_map.tbp[path]
|
|
168
|
+
elif nbc_map and path in nbc_map.type_change:
|
|
169
|
+
continue # we don't handle type changes when converting implicitly
|
|
170
|
+
else:
|
|
171
|
+
new_path = element.metadata._ctx_path
|
|
172
|
+
timebase = element.metadata.timebasepath
|
|
173
|
+
|
|
174
|
+
# Override time base for homogeneous time
|
|
175
|
+
if timebase and time_mode == IDS_TIME_MODE_HOMOGENEOUS:
|
|
176
|
+
timebase = "/time"
|
|
177
|
+
|
|
178
|
+
if isinstance(element, IDSStructArray):
|
|
179
|
+
size = len(element)
|
|
180
|
+
if verify_maxoccur:
|
|
181
|
+
maxoccur = element.metadata.maxoccur
|
|
182
|
+
if maxoccur and size > maxoccur:
|
|
183
|
+
raise RuntimeError(
|
|
184
|
+
f"Exceeding maximum number of occurrences ({maxoccur}) "
|
|
185
|
+
f"of {element._path}"
|
|
186
|
+
)
|
|
187
|
+
with ctx.arraystruct_action(new_path, timebase, size) as (new_ctx, _):
|
|
188
|
+
for item in element:
|
|
189
|
+
put_children(
|
|
190
|
+
item, new_ctx, time_mode, is_slice, nbc_map, verify_maxoccur
|
|
191
|
+
)
|
|
192
|
+
new_ctx.iterate_over_arraystruct(1)
|
|
193
|
+
|
|
194
|
+
elif isinstance(element, IDSStructure):
|
|
195
|
+
put_children(element, ctx, time_mode, is_slice, nbc_map, verify_maxoccur)
|
|
196
|
+
|
|
197
|
+
else: # Data elements
|
|
198
|
+
if is_slice and not element.metadata.type.is_dynamic:
|
|
199
|
+
continue # put_slice only stores dynamic data
|
|
200
|
+
ctx.write_data(new_path, timebase, element.value)
|