imas-python 2.0.0.post1__py3-none-any.whl → 2.1.0__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.
- imas/__init__.py +0 -1
- imas/_version.py +16 -3
- imas/backends/imas_core/al_context.py +4 -4
- imas/backends/imas_core/db_entry_al.py +71 -93
- imas/backends/imas_core/imas_interface.py +6 -32
- imas/backends/imas_core/mdsplus_model.py +10 -24
- imas/backends/netcdf/nc2ids.py +6 -3
- imas/command/cli.py +0 -1
- imas/command/db_analysis.py +102 -7
- imas/command/helpers.py +1 -1
- imas/db_entry.py +35 -35
- imas/dd_zip.py +9 -197
- imas/exception.py +6 -17
- imas/ids_convert.py +486 -89
- imas/ids_coordinates.py +4 -3
- imas/ids_factory.py +8 -5
- imas/ids_identifiers.py +30 -15
- imas/ids_structure.py +4 -3
- imas/ids_toplevel.py +8 -17
- imas/test/test_cli.py +62 -10
- imas/test/test_dbentry.py +1 -6
- imas/test/test_helpers.py +66 -55
- imas/test/test_identifiers.py +191 -1
- imas/test/test_ids_convert.py +304 -22
- imas/test/test_lazy_loading.py +5 -3
- imas/test/test_time_slicing.py +0 -7
- imas/training.py +7 -26
- {imas_python-2.0.0.post1.dist-info → imas_python-2.1.0.dist-info}/METADATA +20 -10
- {imas_python-2.0.0.post1.dist-info → imas_python-2.1.0.dist-info}/RECORD +33 -36
- {imas_python-2.0.0.post1.dist-info → imas_python-2.1.0.dist-info}/WHEEL +1 -1
- imas_python-2.1.0.dist-info/entry_points.txt +2 -0
- imas/assets/IDSDef.zip +0 -0
- imas/dd_helpers.py +0 -167
- imas/test/test_dd_helpers.py +0 -53
- imas_python-2.0.0.post1.dist-info/entry_points.txt +0 -3
- {imas_python-2.0.0.post1.dist-info → imas_python-2.1.0.dist-info/licenses}/LICENSE.txt +0 -0
- {imas_python-2.0.0.post1.dist-info → imas_python-2.1.0.dist-info}/top_level.txt +0 -0
imas/__init__.py
CHANGED
imas/_version.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '2.
|
|
21
|
-
__version_tuple__ = version_tuple = (2,
|
|
31
|
+
__version__ = version = '2.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# This file is part of IMAS-Python.
|
|
2
2
|
# You should have received the IMAS-Python LICENSE file with this project.
|
|
3
|
-
"""Object-oriented interface to the IMAS lowlevel.
|
|
4
|
-
"""
|
|
3
|
+
"""Object-oriented interface to the IMAS lowlevel."""
|
|
5
4
|
|
|
6
5
|
import logging
|
|
7
6
|
import weakref
|
|
@@ -61,17 +60,18 @@ class ALContext:
|
|
|
61
60
|
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
62
61
|
ll_interface.end_action(self.ctx)
|
|
63
62
|
|
|
64
|
-
def global_action(self, path: str, rwmode: int) -> "ALContext":
|
|
63
|
+
def global_action(self, path: str, rwmode: int, datapath: str = "") -> "ALContext":
|
|
65
64
|
"""Begin a new global action for use in a ``with`` context.
|
|
66
65
|
|
|
67
66
|
Args:
|
|
68
67
|
path: access layer path for this global action: ``<idsname>[/<occurrence>]``
|
|
69
68
|
rwmode: read-only or read-write operation mode: ``READ_OP``/``WRITE_OP``
|
|
69
|
+
datapath: used by UDA backend to fetch only part of the data.
|
|
70
70
|
|
|
71
71
|
Returns:
|
|
72
72
|
The created context.
|
|
73
73
|
"""
|
|
74
|
-
status, ctx = ll_interface.begin_global_action(self.ctx, path, rwmode)
|
|
74
|
+
status, ctx = ll_interface.begin_global_action(self.ctx, path, rwmode, datapath)
|
|
75
75
|
if status != 0:
|
|
76
76
|
raise LowlevelError("global_action", status)
|
|
77
77
|
return ALContext(ctx)
|
|
@@ -5,30 +5,29 @@ import getpass
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
from collections import deque
|
|
8
|
+
import re
|
|
8
9
|
from typing import Any, Deque, List, Optional, Union
|
|
9
10
|
from urllib.parse import urlparse
|
|
10
11
|
|
|
12
|
+
from packaging.version import Version
|
|
13
|
+
|
|
14
|
+
import imas
|
|
11
15
|
from imas.backends.db_entry_impl import GetSampleParameters, GetSliceParameters
|
|
12
16
|
from imas.db_entry import DBEntryImpl
|
|
13
17
|
from imas.exception import DataEntryException, LowlevelError
|
|
14
18
|
from imas.ids_convert import NBCPathMap, dd_version_map_from_factories
|
|
15
19
|
from imas.ids_defs import (
|
|
16
|
-
ASCII_BACKEND,
|
|
17
20
|
CHAR_DATA,
|
|
18
21
|
CLOSE_PULSE,
|
|
19
22
|
CREATE_PULSE,
|
|
20
23
|
ERASE_PULSE,
|
|
21
24
|
FORCE_CREATE_PULSE,
|
|
22
25
|
FORCE_OPEN_PULSE,
|
|
23
|
-
HDF5_BACKEND,
|
|
24
26
|
IDS_TIME_MODE_UNKNOWN,
|
|
25
27
|
IDS_TIME_MODES,
|
|
26
28
|
INTEGER_DATA,
|
|
27
|
-
MDSPLUS_BACKEND,
|
|
28
|
-
MEMORY_BACKEND,
|
|
29
29
|
OPEN_PULSE,
|
|
30
30
|
READ_OP,
|
|
31
|
-
UDA_BACKEND,
|
|
32
31
|
UNDEFINED_INTERP,
|
|
33
32
|
UNDEFINED_TIME,
|
|
34
33
|
WRITE_OP,
|
|
@@ -40,16 +39,9 @@ from imas.ids_toplevel import IDSToplevel
|
|
|
40
39
|
from .al_context import ALContext, LazyALContext
|
|
41
40
|
from .db_entry_helpers import delete_children, get_children, put_children
|
|
42
41
|
from .imas_interface import LLInterfaceError, has_imas, ll_interface
|
|
43
|
-
from .mdsplus_model import
|
|
42
|
+
from .mdsplus_model import mdsplus_model_dir
|
|
44
43
|
from .uda_support import extract_idsdef, get_dd_version_from_idsdef_xml
|
|
45
44
|
|
|
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
45
|
_OPEN_MODES = {
|
|
54
46
|
"r": OPEN_PULSE,
|
|
55
47
|
"a": FORCE_OPEN_PULSE,
|
|
@@ -71,13 +63,26 @@ def require_imas_available():
|
|
|
71
63
|
class ALDBEntryImpl(DBEntryImpl):
|
|
72
64
|
"""DBEntry implementation using imas_core as a backend."""
|
|
73
65
|
|
|
74
|
-
|
|
66
|
+
def __init__(self, uri: str, mode: int, factory: IDSFactory):
|
|
67
|
+
# Setup backend and lowlevel Access Layer:
|
|
68
|
+
backend = urlparse(uri).path.lower().lstrip("/")
|
|
69
|
+
self._setup_backend(backend, mode, factory)
|
|
70
|
+
status, ctx = ll_interface.begin_dataentry_action(uri, mode)
|
|
71
|
+
if status != 0:
|
|
72
|
+
raise LowlevelError("opening/creating data entry", status)
|
|
75
73
|
|
|
76
|
-
def __init__(self, backend: str, ctx: ALContext, factory: IDSFactory):
|
|
77
74
|
self.backend = backend
|
|
78
|
-
self._db_ctx = ctx
|
|
75
|
+
self._db_ctx = ALContext(ctx)
|
|
79
76
|
self._ids_factory = factory
|
|
80
77
|
self._lazy_ctx_cache: Deque[ALContext] = deque()
|
|
78
|
+
self._uri = uri
|
|
79
|
+
|
|
80
|
+
# Parse query options, mimic logic in AL-Core instead of using
|
|
81
|
+
# urllib.parse.parse_qs(..). See https://github.com/jholloc/simple-uri-parser
|
|
82
|
+
self._querydict = {}
|
|
83
|
+
for option in re.split("[&;?]", urlparse(self._uri).query):
|
|
84
|
+
name, _, value = option.partition("=")
|
|
85
|
+
self._querydict[name] = value
|
|
81
86
|
|
|
82
87
|
@classmethod
|
|
83
88
|
def from_uri(cls, uri: str, mode: str, factory: IDSFactory) -> "ALDBEntryImpl":
|
|
@@ -85,7 +90,7 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
85
90
|
if mode not in _OPEN_MODES:
|
|
86
91
|
modes = list(_OPEN_MODES)
|
|
87
92
|
raise ValueError(f"Unknown mode {mode!r}, was expecting any of {modes}")
|
|
88
|
-
return cls
|
|
93
|
+
return cls(uri, _OPEN_MODES[mode], factory)
|
|
89
94
|
|
|
90
95
|
@classmethod
|
|
91
96
|
def from_pulse_run(
|
|
@@ -108,60 +113,18 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
108
113
|
data_version = data_version or factory.dd_version
|
|
109
114
|
options = options if options else ""
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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)
|
|
116
|
+
# Construct URI from legacy parameters
|
|
117
|
+
status, uri = ll_interface.build_uri_from_legacy_parameters(
|
|
118
|
+
backend_id, pulse, run, user_name, db_name, data_version, options
|
|
119
|
+
)
|
|
145
120
|
if status != 0:
|
|
146
|
-
raise LowlevelError("
|
|
121
|
+
raise LowlevelError("build URI from legacy parameters", status)
|
|
147
122
|
|
|
148
|
-
return cls(
|
|
123
|
+
return cls(uri, mode, factory)
|
|
149
124
|
|
|
150
125
|
@classmethod
|
|
151
|
-
def _setup_backend(
|
|
152
|
-
|
|
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
|
-
"""
|
|
126
|
+
def _setup_backend(cls, backend: str, mode: int, factory: IDSFactory) -> None:
|
|
127
|
+
"""Custom logic for preparing some backends."""
|
|
165
128
|
if backend == "mdsplus":
|
|
166
129
|
# MDSplus models:
|
|
167
130
|
if mode != OPEN_PULSE:
|
|
@@ -170,22 +133,6 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
170
133
|
if ids_path:
|
|
171
134
|
os.environ["ids_path"] = ids_path
|
|
172
135
|
|
|
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
136
|
elif backend == "uda":
|
|
190
137
|
# Set IDSDEF_PATH to point the UDA backend to the selected DD version
|
|
191
138
|
idsdef_path = None
|
|
@@ -193,7 +140,6 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
193
140
|
if factory._xml_path is not None:
|
|
194
141
|
# Factory was constructed with an explicit XML path, point UDA to that:
|
|
195
142
|
idsdef_path = factory._xml_path
|
|
196
|
-
|
|
197
143
|
elif "IMAS_PREFIX" in os.environ:
|
|
198
144
|
# Check if UDA can use the IDSDef.xml stored in $IMAS_PREFIX/include/
|
|
199
145
|
idsdef_path = os.environ["IMAS_PREFIX"] + "/include/IDSDef.xml"
|
|
@@ -203,15 +149,9 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
203
149
|
if idsdef_path is None:
|
|
204
150
|
# Extract XML from the DD zip and point UDA to it
|
|
205
151
|
idsdef_path = extract_idsdef(factory.version)
|
|
206
|
-
|
|
207
152
|
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
153
|
|
|
214
|
-
elif backend
|
|
154
|
+
elif backend in ["hdf5", "memory", "ascii", "flexbuffers"]:
|
|
215
155
|
pass # nothing to set up
|
|
216
156
|
|
|
217
157
|
else:
|
|
@@ -249,6 +189,8 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
249
189
|
raise RuntimeError("Database entry is not open.")
|
|
250
190
|
if lazy and self.backend == "ascii":
|
|
251
191
|
raise RuntimeError("Lazy loading is not supported by the ASCII backend.")
|
|
192
|
+
if self.backend == "uda":
|
|
193
|
+
self._check_uda_warnings(lazy)
|
|
252
194
|
|
|
253
195
|
# Mixing contexts can be problematic, ensure all lazy contexts are closed:
|
|
254
196
|
self._clear_lazy_ctx_cache()
|
|
@@ -257,7 +199,8 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
257
199
|
if occurrence != 0:
|
|
258
200
|
ll_path += f"/{occurrence}"
|
|
259
201
|
|
|
260
|
-
|
|
202
|
+
datapath = "ids_properties" if self.backend == "uda" else ""
|
|
203
|
+
with self._db_ctx.global_action(ll_path, READ_OP, datapath) as read_ctx:
|
|
261
204
|
time_mode_path = "ids_properties/homogeneous_time"
|
|
262
205
|
time_mode = read_ctx.read_data(time_mode_path, "", INTEGER_DATA, 0)
|
|
263
206
|
# This is already checked by read_dd_version, but ensure:
|
|
@@ -314,7 +257,8 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
314
257
|
if occurrence != 0:
|
|
315
258
|
ll_path += f"/{occurrence}"
|
|
316
259
|
|
|
317
|
-
|
|
260
|
+
datapath = "ids_properties" if self.backend == "uda" else ""
|
|
261
|
+
with self._db_ctx.global_action(ll_path, READ_OP, datapath) as read_ctx:
|
|
318
262
|
time_mode_path = "ids_properties/homogeneous_time"
|
|
319
263
|
time_mode = read_ctx.read_data(time_mode_path, "", INTEGER_DATA, 0)
|
|
320
264
|
dd_version_path = "ids_properties/version_put/data_dictionary"
|
|
@@ -337,6 +281,15 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
337
281
|
# Create a version conversion map, if needed
|
|
338
282
|
nbc_map = None
|
|
339
283
|
if ids._version != self._ids_factory._version:
|
|
284
|
+
if ids._version.split(".")[0] != self._ids_factory._version.split(".")[0]:
|
|
285
|
+
raise RuntimeError(
|
|
286
|
+
f"Provided IDS uses DD {ids._version} which has a different major "
|
|
287
|
+
f"version than the Data Entry ({self._ids_factory._version}). "
|
|
288
|
+
"IMAS-Python will not automatically convert this data for you."
|
|
289
|
+
"See the documentation for more details and fixes: "
|
|
290
|
+
f"{imas.PUBLISHED_DOCUMENTATION_ROOT}"
|
|
291
|
+
"/multi-dd.html#storing-idss-with-a-different-major-version"
|
|
292
|
+
)
|
|
340
293
|
ddmap, source_is_older = dd_version_map_from_factories(
|
|
341
294
|
ids_name, ids._parent, self._ids_factory
|
|
342
295
|
)
|
|
@@ -403,3 +356,28 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
403
356
|
"Access Layer 5.1 or newer is required."
|
|
404
357
|
) from None
|
|
405
358
|
return occurrence_list
|
|
359
|
+
|
|
360
|
+
def _check_uda_warnings(self, lazy: bool) -> None:
|
|
361
|
+
"""Various checks / warnings for the UDA backend."""
|
|
362
|
+
cache_mode = self._querydict.get("cache_mode")
|
|
363
|
+
if lazy and cache_mode != "none":
|
|
364
|
+
# cache_mode=none requires imas core 5.5.1 or newer, and a recent enough UDA
|
|
365
|
+
# server plugin (which we cannot check...)
|
|
366
|
+
cache_mode_hint = ""
|
|
367
|
+
if ll_interface._al_version >= Version("5.5.1"):
|
|
368
|
+
cache_mode_hint = (
|
|
369
|
+
"\nYou may add the parameter ';cache_mode=none' to the IMAS URI "
|
|
370
|
+
"to avoid loading all of the data from the remote server."
|
|
371
|
+
)
|
|
372
|
+
logger.warning(
|
|
373
|
+
"The UDA backend will load all IDS data from the remote server. "
|
|
374
|
+
"Lazy loading with the UDA backend may therefore still be slow.%s",
|
|
375
|
+
cache_mode_hint,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if cache_mode == "none" and ll_interface._al_version < Version("5.5.1"):
|
|
379
|
+
logger.warning(
|
|
380
|
+
"UDA option 'cache_mode=None' may not work correctly with "
|
|
381
|
+
"IMAS Core version %s.",
|
|
382
|
+
ll_interface._al_version,
|
|
383
|
+
)
|
|
@@ -6,6 +6,7 @@ Helper module for providing a version-independent interface to the Access Layer.
|
|
|
6
6
|
This module tries to abstract away most API incompatibilities between the supported
|
|
7
7
|
Access Layer versions (for example the rename of _ual_lowlevel to _al_lowlevel).
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
import inspect
|
|
10
11
|
import logging
|
|
11
12
|
|
|
@@ -31,7 +32,7 @@ except ImportError as exc:
|
|
|
31
32
|
has_imas = False
|
|
32
33
|
imasdef = None
|
|
33
34
|
lowlevel = None
|
|
34
|
-
logger.
|
|
35
|
+
logger.warning(
|
|
35
36
|
"Could not import 'imas_core': %s. Some functionality is not available.",
|
|
36
37
|
exc,
|
|
37
38
|
)
|
|
@@ -61,9 +62,6 @@ class LowlevelInterface:
|
|
|
61
62
|
|
|
62
63
|
- If the lowlevel drops methods, we need to update the implementation fo the method
|
|
63
64
|
to provide a proper error message or a workaround.
|
|
64
|
-
- Renamed methods (if this will ever happen) are perhaps best handled in the
|
|
65
|
-
``__init__`` by providing a mapping of new to old name, so far this was only
|
|
66
|
-
relevant for the ``ual_`` to ``al_`` rename.
|
|
67
65
|
"""
|
|
68
66
|
|
|
69
67
|
def __init__(self, lowlevel):
|
|
@@ -84,23 +82,13 @@ class LowlevelInterface:
|
|
|
84
82
|
# Introduced after 5.0.0
|
|
85
83
|
self._al_version_str = self._lowlevel.get_al_version()
|
|
86
84
|
self._al_version = Version(self._al_version_str)
|
|
87
|
-
|
|
88
|
-
# In AL 5.0.0, all `ual_` methods were renamed to `al_`
|
|
85
|
+
else:
|
|
89
86
|
self._al_version_str = "5.0.0"
|
|
90
87
|
self._al_version = Version(self._al_version_str)
|
|
91
|
-
else:
|
|
92
|
-
# AL 4, don't try to determine in more detail
|
|
93
|
-
self._al_version_str = "4.?.?"
|
|
94
|
-
self._al_version = Version("4")
|
|
95
|
-
public_methods.remove("close_pulse")
|
|
96
88
|
|
|
97
|
-
if self._al_version < Version("5"):
|
|
98
|
-
method_prefix = "ual_"
|
|
99
|
-
else:
|
|
100
|
-
method_prefix = "al_"
|
|
101
89
|
# Overwrite all of our methods that are implemented in the lowlevel
|
|
102
90
|
for method in public_methods:
|
|
103
|
-
ll_method = getattr(lowlevel,
|
|
91
|
+
ll_method = getattr(lowlevel, f"al_{method}", None)
|
|
104
92
|
if ll_method is not None:
|
|
105
93
|
setattr(self, method, ll_method)
|
|
106
94
|
|
|
@@ -115,24 +103,10 @@ class LowlevelInterface:
|
|
|
115
103
|
f"but the current version is {self._al_version_str}"
|
|
116
104
|
)
|
|
117
105
|
|
|
118
|
-
# AL 4 lowlevel API
|
|
119
|
-
|
|
120
|
-
def begin_pulse_action(self, backendID, shot, run, user, tokamak, version):
|
|
121
|
-
# Removed in AL5, compatibility handled in DBEntry
|
|
122
|
-
raise LLInterfaceError(f"{__name__} is not implemented")
|
|
123
|
-
|
|
124
|
-
def open_pulse(self, pulseCtx, mode, options):
|
|
125
|
-
# Removed in AL5, compatibility handled in DBEntry
|
|
126
|
-
raise LLInterfaceError(f"{__name__} is not implemented")
|
|
127
|
-
|
|
128
106
|
def close_pulse(self, pulseCtx, mode):
|
|
129
|
-
|
|
130
|
-
# This method is overwritten in AL5, but for AL4 we need to do this:
|
|
131
|
-
return lowlevel.ual_close_pulse(pulseCtx, mode, None)
|
|
107
|
+
raise LLInterfaceError(f"{__name__} is not implemented")
|
|
132
108
|
|
|
133
|
-
def begin_global_action(self, pulseCtx, dataobjectname, rwmode, datapath
|
|
134
|
-
# datapath was added in AL5 to support more efficient partial_get in the
|
|
135
|
-
# UDA backend. TODO: figure out if this is useful for lazy loading.
|
|
109
|
+
def begin_global_action(self, pulseCtx, dataobjectname, rwmode, datapath):
|
|
136
110
|
raise LLInterfaceError(f"{__name__} is not implemented")
|
|
137
111
|
|
|
138
112
|
def begin_slice_action(self, pulseCtx, dataobjectname, rwmode, time, interpmode):
|
|
@@ -12,7 +12,6 @@ import tempfile
|
|
|
12
12
|
import time
|
|
13
13
|
import uuid
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from saxonche import PySaxonProcessor
|
|
16
15
|
from subprocess import CalledProcessError, check_output
|
|
17
16
|
from zlib import crc32
|
|
18
17
|
|
|
@@ -244,11 +243,20 @@ def transform_with_xslt(xslt_processor, source, xslfile, output_file):
|
|
|
244
243
|
|
|
245
244
|
def create_model_ids_xml(cache_dir_path, fname, version):
|
|
246
245
|
"""Use Saxon/C to compile an ids.xml suitable for creating an MDSplus model."""
|
|
246
|
+
try:
|
|
247
|
+
import saxonche
|
|
248
|
+
except ImportError:
|
|
249
|
+
raise RuntimeError(
|
|
250
|
+
"Building mdsplus models requires the 'saxonche' python package. "
|
|
251
|
+
"Please install this package (for example with 'pip install saxonche') "
|
|
252
|
+
"and try again."
|
|
253
|
+
)
|
|
254
|
+
|
|
247
255
|
try:
|
|
248
256
|
with as_file(files("imas") / "assets" / "IDSDef2MDSpreTree.xsl") as xslfile:
|
|
249
257
|
output_file = Path(cache_dir_path) / "ids.xml"
|
|
250
258
|
|
|
251
|
-
with PySaxonProcessor(license=False) as proc:
|
|
259
|
+
with saxonche.PySaxonProcessor(license=False) as proc:
|
|
252
260
|
xslt_processor = proc.new_xslt30_processor()
|
|
253
261
|
xdm_ddgit = proc.make_string_value(str(version) or fname)
|
|
254
262
|
xslt_processor.set_parameter("DD_GIT_DESCRIBE", xdm_ddgit)
|
|
@@ -356,25 +364,3 @@ def jTraverser_jar() -> Path:
|
|
|
356
364
|
return jar_path
|
|
357
365
|
else:
|
|
358
366
|
raise MDSPlusModelError("jTraverser.jar not found. Is MDSplus-Java available?")
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def ensure_data_dir(user: str, tokamak: str, version: str, run: int) -> None:
|
|
362
|
-
"""Ensure that a data dir exists with a similar algorithm that
|
|
363
|
-
the MDSplus backend uses to set the data path.
|
|
364
|
-
See also mdsplus_backend.cpp:751 (setDataEnv)"""
|
|
365
|
-
if user == "public":
|
|
366
|
-
if "IMAS_HOME" not in os.environ:
|
|
367
|
-
raise RuntimeError(
|
|
368
|
-
"Environment variable IMAS_HOME must be set to access "
|
|
369
|
-
"the public database."
|
|
370
|
-
)
|
|
371
|
-
dbdir = Path(os.environ["IMAS_HOME"]) / "shared" / "imasdb" / tokamak / version
|
|
372
|
-
elif user[0] == "/":
|
|
373
|
-
dbdir = Path(user) / tokamak / version
|
|
374
|
-
else:
|
|
375
|
-
dbdir = Path.home() / "public" / "imasdb" / tokamak / version
|
|
376
|
-
|
|
377
|
-
# Check subfolder based on run
|
|
378
|
-
assert 0 <= run <= 99_999
|
|
379
|
-
index = run // 10_000
|
|
380
|
-
(dbdir / str(index)).mkdir(parents=True, exist_ok=True)
|
imas/backends/netcdf/nc2ids.py
CHANGED
|
@@ -366,9 +366,12 @@ class LazyContext:
|
|
|
366
366
|
|
|
367
367
|
if value is not None:
|
|
368
368
|
if isinstance(value, np.ndarray):
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
369
|
+
if value.ndim == 0: # Unpack 0D numpy arrays:
|
|
370
|
+
value = value.item()
|
|
371
|
+
else:
|
|
372
|
+
# Convert the numpy array to a read-only view
|
|
373
|
+
value = value.view()
|
|
374
|
+
value.flags.writeable = False
|
|
372
375
|
# NOTE: bypassing IDSPrimitive.value.setter logic
|
|
373
376
|
child._IDSPrimitive__value = value
|
|
374
377
|
|
imas/command/cli.py
CHANGED
|
@@ -102,7 +102,6 @@ def print_ids(uri, ids, occurrence, print_all):
|
|
|
102
102
|
ids Name of the IDS to print (e.g. "core_profiles").
|
|
103
103
|
occurrence Which occurrence to print (defaults to 0).
|
|
104
104
|
"""
|
|
105
|
-
min_version_guard(Version("5.0"))
|
|
106
105
|
setup_rich_log_handler(False)
|
|
107
106
|
|
|
108
107
|
with DBEntry(uri, "r") as dbentry:
|
imas/command/db_analysis.py
CHANGED
|
@@ -7,6 +7,8 @@ import platform
|
|
|
7
7
|
import re
|
|
8
8
|
import readline
|
|
9
9
|
import sys
|
|
10
|
+
from csv import writer as csvwriter
|
|
11
|
+
from collections import Counter, defaultdict
|
|
10
12
|
from dataclasses import dataclass, field
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from typing import Dict, Iterable, List, Optional
|
|
@@ -139,12 +141,70 @@ def ids_info(idsfile: Path):
|
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
|
|
144
|
+
@dataclass
|
|
145
|
+
class _PathUsage:
|
|
146
|
+
num_occurrences: int = 0
|
|
147
|
+
path_counter: Counter = field(default_factory=Counter)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _write_usage_stats_to_csv(
|
|
151
|
+
writer, usage_per_entry, usage_per_occurrence, num_entries
|
|
152
|
+
):
|
|
153
|
+
"""Write usage statistics to csv file.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
writer: an instance of csv.writer
|
|
157
|
+
usage_per_entry: path usage statistics per data entry
|
|
158
|
+
usage_per_occurrence: path usage statistics per occurrence
|
|
159
|
+
num_entries: number of data entries
|
|
160
|
+
"""
|
|
161
|
+
# Write header
|
|
162
|
+
writer.writerow(
|
|
163
|
+
[
|
|
164
|
+
"IDS",
|
|
165
|
+
"Path in IDS",
|
|
166
|
+
"Uses errorbar",
|
|
167
|
+
"Frequency (without occurrences)",
|
|
168
|
+
"Frequency (with occurences)",
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
for ids_name in sorted(usage_per_entry):
|
|
173
|
+
entry_usage = usage_per_entry[ids_name]
|
|
174
|
+
occurrence_usage = usage_per_occurrence[ids_name]
|
|
175
|
+
|
|
176
|
+
# Usage statistics of the IDS (# entries with this IDS / # entries)
|
|
177
|
+
freq = entry_usage.num_occurrences / num_entries
|
|
178
|
+
writer.writerow([ids_name, "", "", freq, ""])
|
|
179
|
+
|
|
180
|
+
for path, entry_count in sorted(entry_usage.path_counter.items()):
|
|
181
|
+
if "_error_" in path:
|
|
182
|
+
continue # Skip error nodes
|
|
183
|
+
occurrence_count = occurrence_usage.path_counter[path]
|
|
184
|
+
|
|
185
|
+
uses_error = f"{path}_error_upper" in entry_usage.path_counter
|
|
186
|
+
# Frequency without occurrences, see GH#84 for details
|
|
187
|
+
freq1 = entry_count / entry_usage.num_occurrences
|
|
188
|
+
# Frequency with occurences
|
|
189
|
+
freq2 = occurrence_count / occurrence_usage.num_occurrences
|
|
190
|
+
|
|
191
|
+
# Write data row
|
|
192
|
+
writer.writerow([ids_name, path, "X" if uses_error else "", freq1, freq2])
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
_csv_help_text = (
|
|
196
|
+
"Write analysis output to the provided CSV file. For details, "
|
|
197
|
+
"see https://github.com/iterorganization/IMAS-Python/issues/84."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
142
201
|
@click.command("process-db-analysis")
|
|
143
202
|
@click.argument(
|
|
144
203
|
"infiles", metavar="INPUT_FILES...", nargs=-1, type=infile_path, required=True
|
|
145
204
|
)
|
|
146
205
|
@click.option("--show-empty-ids", is_flag=True, help="Show empty IDSs in the overview.")
|
|
147
|
-
|
|
206
|
+
@click.option("--csv", type=outfile_path, help=_csv_help_text)
|
|
207
|
+
def process_db_analysis(infiles, show_empty_ids, csv):
|
|
148
208
|
"""Process supplied Data Entry analyses, and display statistics.
|
|
149
209
|
|
|
150
210
|
\b
|
|
@@ -153,9 +213,10 @@ def process_db_analysis(infiles, show_empty_ids):
|
|
|
153
213
|
"""
|
|
154
214
|
setup_rich_log_handler(False)
|
|
155
215
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
216
|
+
usage_per_entry = defaultdict(_PathUsage)
|
|
217
|
+
usage_per_occurrence = defaultdict(_PathUsage)
|
|
218
|
+
num_entries = 0
|
|
219
|
+
|
|
159
220
|
logger.info("Reading %d input files...", len(infiles))
|
|
160
221
|
|
|
161
222
|
# Read input data and collate usage info per IDS
|
|
@@ -164,17 +225,51 @@ def process_db_analysis(infiles, show_empty_ids):
|
|
|
164
225
|
data = json.load(file)
|
|
165
226
|
|
|
166
227
|
for entry in data:
|
|
228
|
+
usage_for_this_entry = defaultdict(_PathUsage)
|
|
167
229
|
for ids_info in entry["ids_info"]:
|
|
168
|
-
|
|
169
|
-
|
|
230
|
+
ids_name = ids_info["name"]
|
|
231
|
+
filled_paths = ids_info["filled_data"]
|
|
232
|
+
# Update counters for this entry
|
|
233
|
+
usage_for_this_entry[ids_name].path_counter.update(filled_paths)
|
|
234
|
+
# Update counters for all occurrecnes
|
|
235
|
+
usage_per_occurrence[ids_name].num_occurrences += 1
|
|
236
|
+
usage_per_occurrence[ids_name].path_counter.update(filled_paths)
|
|
237
|
+
# Update data entry usage
|
|
238
|
+
for ids_name, usage in usage_for_this_entry.items():
|
|
239
|
+
usage_per_entry[ids_name].num_occurrences += 1
|
|
240
|
+
usage_per_entry[ids_name].path_counter.update(usage.path_counter.keys())
|
|
241
|
+
num_entries += 1
|
|
170
242
|
|
|
171
243
|
logger.info("Done reading input files.")
|
|
244
|
+
|
|
245
|
+
if csv is not None:
|
|
246
|
+
# Output to CSV file
|
|
247
|
+
logger.info("Writing output to CSV file: %s", csv)
|
|
248
|
+
with open(csv, "w") as csvfile:
|
|
249
|
+
writer = csvwriter(csvfile)
|
|
250
|
+
_write_usage_stats_to_csv(
|
|
251
|
+
writer, usage_per_entry, usage_per_occurrence, num_entries
|
|
252
|
+
)
|
|
253
|
+
logger.info("Done.")
|
|
254
|
+
return
|
|
255
|
+
|
|
172
256
|
logger.info("Analyzing filled data...")
|
|
257
|
+
factory = imas.IDSFactory()
|
|
258
|
+
logger.info("Using Data Dictionary version %s.", factory.dd_version)
|
|
173
259
|
|
|
174
260
|
# Construct AnalysisNodes per IDS
|
|
175
261
|
analysis_nodes: Dict[str, _AnalysisNode] = {}
|
|
176
|
-
for ids_name,
|
|
262
|
+
for ids_name, usage in usage_per_occurrence.items():
|
|
263
|
+
if ids_name not in factory.ids_names():
|
|
264
|
+
logger.warning(
|
|
265
|
+
"Founds IDS %s in data files, but this IDS is not available "
|
|
266
|
+
"in DD version %s. Statistics will not be tracked.",
|
|
267
|
+
ids_name,
|
|
268
|
+
factory.dd_version,
|
|
269
|
+
)
|
|
270
|
+
continue
|
|
177
271
|
metadata = factory.new(ids_name).metadata
|
|
272
|
+
filled = set(usage.path_counter.keys())
|
|
178
273
|
ids_analysis_node = _AnalysisNode("")
|
|
179
274
|
|
|
180
275
|
def walk_metadata_tree(metadata: IDSMetadata, node: _AnalysisNode):
|
imas/command/helpers.py
CHANGED
|
@@ -36,7 +36,7 @@ def min_version_guard(al_version: Version):
|
|
|
36
36
|
al_version: Minimum imas_core version required for this command.
|
|
37
37
|
"""
|
|
38
38
|
used_version = ll_interface._al_version
|
|
39
|
-
if used_version >= al_version:
|
|
39
|
+
if used_version and used_version >= al_version:
|
|
40
40
|
return
|
|
41
41
|
click.echo(
|
|
42
42
|
f"This command requires at least version {al_version} of the Access Layer."
|