imas-python 2.1.0.post1__py3-none-any.whl → 2.3.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 +43 -15
- imas/_to_xarray.py +21 -14
- imas/_version.py +8 -18
- imas/assets/ITER_134173_106_equilibrium.ids +0 -2
- imas/assets/equilibrium.ids +0 -2
- imas/backends/db_entry_impl.py +9 -1
- imas/backends/imas_core/al_context.py +14 -3
- imas/backends/imas_core/db_entry_al.py +39 -19
- imas/backends/imas_core/imas_interface.py +14 -22
- imas/backends/imas_core/mdsplus_model.py +19 -2
- imas/backends/imas_core/uda_support.py +2 -2
- imas/backends/netcdf/db_entry_nc.py +24 -7
- imas/backends/netcdf/ids2nc.py +12 -46
- imas/backends/netcdf/ids_tensorizer.py +69 -10
- imas/backends/netcdf/nc2ids.py +23 -16
- imas/backends/netcdf/nc_metadata.py +3 -4
- imas/command/cli.py +68 -3
- imas/convert_core_edge_plasma.py +124 -0
- imas/db_entry.py +70 -7
- imas/exception.py +2 -4
- imas/ids_base.py +3 -3
- imas/ids_convert.py +69 -37
- imas/ids_defs.py +37 -77
- imas/ids_factory.py +18 -1
- imas/ids_identifiers.py +28 -15
- imas/ids_metadata.py +6 -6
- imas/ids_primitive.py +3 -3
- imas/ids_toplevel.py +3 -9
- imas/test/test_cli.py +38 -3
- imas/test/test_convert_core_edge_plasma.py +68 -0
- imas/test/test_dbentry.py +3 -3
- imas/test/test_dbentry_uda.py +80 -0
- imas/test/test_exception.py +1 -1
- imas/test/test_identifiers.py +18 -9
- imas/test/test_ids_ascii_data.py +1 -1
- imas/test/test_ids_convert.py +72 -0
- imas/test/test_ids_toplevel.py +1 -1
- imas/test/test_ids_validate.py +1 -1
- imas/test/test_latest_dd_autofill.py +1 -3
- imas/test/test_lazy_loading.py +5 -5
- imas/test/test_list_filled_paths.py +101 -0
- imas/test/test_nbc_change.py +1 -1
- imas/test/test_snippets.py +1 -1
- imas/test/test_static_ids.py +1 -1
- imas/test/test_to_xarray.py +26 -3
- imas/test/test_util.py +3 -3
- imas/training.py +11 -6
- {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/METADATA +1 -1
- {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/RECORD +53 -49
- {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/WHEEL +1 -1
- {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/entry_points.txt +0 -0
- {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/licenses/LICENSE.txt +0 -0
- {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/top_level.txt +0 -0
imas/__init__.py
CHANGED
|
@@ -1,30 +1,58 @@
|
|
|
1
1
|
# This file is part of IMAS-Python.
|
|
2
2
|
# You should have received the IMAS-Python LICENSE file with this project.
|
|
3
3
|
|
|
4
|
-
# isort: skip_file
|
|
5
|
-
|
|
6
4
|
from packaging.version import Version as _V
|
|
7
5
|
|
|
8
|
-
from ._version import version as __version__ # noqa: F401
|
|
9
|
-
from ._version import version_tuple # noqa: F401
|
|
10
|
-
|
|
11
6
|
# Import logging _first_
|
|
12
|
-
|
|
7
|
+
# isort: off
|
|
8
|
+
from . import setup_logging # noqa: F401
|
|
9
|
+
|
|
10
|
+
# isort: on
|
|
13
11
|
|
|
14
|
-
#
|
|
12
|
+
# Ensure that `imas.util` is loaded when importing imas
|
|
13
|
+
from . import util # noqa: F401
|
|
14
|
+
|
|
15
|
+
# Public API:
|
|
16
|
+
from ._version import version as __version__
|
|
17
|
+
from ._version import version_tuple
|
|
18
|
+
from .convert_core_edge_plasma import (
|
|
19
|
+
convert_to_plasma_profiles,
|
|
20
|
+
convert_to_plasma_sources,
|
|
21
|
+
convert_to_plasma_transport,
|
|
22
|
+
)
|
|
15
23
|
from .db_entry import DBEntry
|
|
16
|
-
from .ids_factory import IDSFactory
|
|
17
24
|
from .ids_convert import convert_ids
|
|
25
|
+
from .ids_data_type import IDSDataType
|
|
26
|
+
from .ids_factory import IDSFactory
|
|
18
27
|
from .ids_identifiers import identifiers
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
from . import
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
util,
|
|
25
|
-
)
|
|
28
|
+
from .ids_metadata import IDSMetadata, IDSType
|
|
29
|
+
from .ids_primitive import IDSPrimitive
|
|
30
|
+
from .ids_struct_array import IDSStructArray
|
|
31
|
+
from .ids_structure import IDSStructure
|
|
32
|
+
from .ids_toplevel import IDSToplevel
|
|
26
33
|
|
|
27
34
|
PUBLISHED_DOCUMENTATION_ROOT = "https://imas-python.readthedocs.io/en/latest/"
|
|
28
35
|
"""URL to the published documentation."""
|
|
29
36
|
OLDEST_SUPPORTED_VERSION = _V("3.22.0")
|
|
30
37
|
"""Oldest Data Dictionary version that is supported by IMAS-Python."""
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"__version__",
|
|
41
|
+
"version_tuple",
|
|
42
|
+
"DBEntry",
|
|
43
|
+
"IDSDataType",
|
|
44
|
+
"IDSFactory",
|
|
45
|
+
"IDSMetadata",
|
|
46
|
+
"IDSPrimitive",
|
|
47
|
+
"IDSStructure",
|
|
48
|
+
"IDSStructArray",
|
|
49
|
+
"IDSToplevel",
|
|
50
|
+
"IDSType",
|
|
51
|
+
"convert_ids",
|
|
52
|
+
"convert_to_plasma_profiles",
|
|
53
|
+
"convert_to_plasma_sources",
|
|
54
|
+
"convert_to_plasma_transport",
|
|
55
|
+
"identifiers",
|
|
56
|
+
"PUBLISHED_DOCUMENTATION_ROOT",
|
|
57
|
+
"OLDEST_SUPPORTED_VERSION",
|
|
58
|
+
]
|
imas/_to_xarray.py
CHANGED
|
@@ -8,7 +8,7 @@ from imas.backends.netcdf.ids_tensorizer import IDSTensorizer
|
|
|
8
8
|
from imas.ids_data_type import IDSDataType
|
|
9
9
|
|
|
10
10
|
fillvals = {
|
|
11
|
-
IDSDataType.INT: -(2**31) + 1,
|
|
11
|
+
IDSDataType.INT: numpy.int32(-(2**31) + 1),
|
|
12
12
|
IDSDataType.STR: "",
|
|
13
13
|
IDSDataType.FLT: numpy.nan,
|
|
14
14
|
IDSDataType.CPX: numpy.nan * (1 + 1j),
|
|
@@ -27,7 +27,7 @@ def to_xarray(ids: IDSToplevel, *paths: str) -> xarray.Dataset:
|
|
|
27
27
|
# block checks if the paths are valid, and by using "metadata.path_string" we ensure
|
|
28
28
|
# that / are used as separator.
|
|
29
29
|
try:
|
|
30
|
-
paths = [ids.metadata[path].path_string for path in paths]
|
|
30
|
+
paths: list[str] = [ids.metadata[path].path_string for path in paths]
|
|
31
31
|
except KeyError as exc:
|
|
32
32
|
raise ValueError(str(exc)) from None
|
|
33
33
|
|
|
@@ -50,21 +50,28 @@ def to_xarray(ids: IDSToplevel, *paths: str) -> xarray.Dataset:
|
|
|
50
50
|
var_name = path.replace("/", ".")
|
|
51
51
|
metadata = ids.metadata[path]
|
|
52
52
|
if metadata.data_type in (IDSDataType.STRUCTURE, IDSDataType.STRUCT_ARRAY):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
coordinates = tensorizer.filter_coordinates(path)
|
|
62
|
-
if coordinates:
|
|
63
|
-
coordinate_names.update(coordinates.split(" "))
|
|
64
|
-
attrs["coordinates"] = coordinates
|
|
53
|
+
# Metadata variables for (arrays of) structures
|
|
54
|
+
if paths and path not in paths:
|
|
55
|
+
continue
|
|
56
|
+
dimensions = ()
|
|
57
|
+
data = b""
|
|
58
|
+
else:
|
|
59
|
+
dimensions = tensorizer.get_dimensions(path)
|
|
60
|
+
data = tensorizer.tensorize(path, fillvals[metadata.data_type])
|
|
65
61
|
|
|
62
|
+
attrs = tensorizer.get_attributes(path, fillvals)
|
|
63
|
+
if "coordinates" in attrs:
|
|
64
|
+
coordinate_names.update(attrs["coordinates"].split(" "))
|
|
66
65
|
data_vars[var_name] = (dimensions, data, attrs)
|
|
67
66
|
|
|
67
|
+
# :shape array for sparse data
|
|
68
|
+
if path in tensorizer.shapes and metadata.ndim:
|
|
69
|
+
shape_name = f"{var_name}:shape"
|
|
70
|
+
dimensions = tensorizer.get_shape_dimensions(path)
|
|
71
|
+
data = tensorizer.shapes[path]
|
|
72
|
+
attrs = tensorizer.get_shape_attributes(var_name)
|
|
73
|
+
data_vars[shape_name] = (dimensions, data, attrs)
|
|
74
|
+
|
|
68
75
|
# Remove coordinates from data_vars and put in coordinates mapping:
|
|
69
76
|
coordinates = {}
|
|
70
77
|
for coordinate_name in coordinate_names:
|
imas/_version.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
__all__ = [
|
|
5
6
|
"__version__",
|
|
@@ -10,25 +11,14 @@ __all__ = [
|
|
|
10
11
|
"commit_id",
|
|
11
12
|
]
|
|
12
13
|
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
14
|
version: str
|
|
25
15
|
__version__: str
|
|
26
|
-
__version_tuple__:
|
|
27
|
-
version_tuple:
|
|
28
|
-
commit_id:
|
|
29
|
-
__commit_id__:
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
30
20
|
|
|
31
|
-
__version__ = version = '2.
|
|
32
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 3, 0)
|
|
33
23
|
|
|
34
24
|
__commit_id__ = commit_id = None
|
imas/assets/equilibrium.ids
CHANGED
imas/backends/db_entry_impl.py
CHANGED
|
@@ -78,7 +78,7 @@ class DBEntryImpl(ABC):
|
|
|
78
78
|
destination: IDSToplevel,
|
|
79
79
|
lazy: bool,
|
|
80
80
|
nbc_map: Optional[NBCPathMap],
|
|
81
|
-
) ->
|
|
81
|
+
) -> IDSToplevel:
|
|
82
82
|
"""Implement DBEntry.get/get_slice/get_sample. Load data from the data source.
|
|
83
83
|
|
|
84
84
|
Args:
|
|
@@ -120,3 +120,11 @@ class DBEntryImpl(ABC):
|
|
|
120
120
|
@abstractmethod
|
|
121
121
|
def list_all_occurrences(self, ids_name: str) -> List[int]:
|
|
122
122
|
"""Implement DBEntry.list_all_occurrences()"""
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def list_filled_paths(self, ids_name: str, occurrence: int) -> List[str]:
|
|
126
|
+
"""Implement DBEntry.list_filled_paths().
|
|
127
|
+
|
|
128
|
+
N.B. DD conversion is handled in DBEntry.list_filled_paths(), this method
|
|
129
|
+
returns the data paths as stored on-disk.
|
|
130
|
+
"""
|
|
@@ -115,7 +115,7 @@ class ALContext:
|
|
|
115
115
|
tmin: float,
|
|
116
116
|
tmax: float,
|
|
117
117
|
dtime: Optional[numpy.ndarray],
|
|
118
|
-
interpolation_method: int,
|
|
118
|
+
interpolation_method: Optional[int],
|
|
119
119
|
) -> "ALContext":
|
|
120
120
|
"""Begin a new timerange action for use in a ``with`` context."""
|
|
121
121
|
ctx = ll_interface.begin_timerange_action(
|
|
@@ -163,7 +163,7 @@ class ALContext:
|
|
|
163
163
|
"""Call ual_write_data with this context."""
|
|
164
164
|
status = ll_interface.write_data(self.ctx, path, timebasepath, data)
|
|
165
165
|
if status != 0:
|
|
166
|
-
raise LowlevelError(f"write data at {path!r}
|
|
166
|
+
raise LowlevelError(f"write data at {path!r}", status)
|
|
167
167
|
|
|
168
168
|
def list_all_occurrences(self, ids_name: str) -> List[int]:
|
|
169
169
|
"""List all occurrences of this IDS."""
|
|
@@ -174,6 +174,17 @@ class ALContext:
|
|
|
174
174
|
return list(occurrences)
|
|
175
175
|
return []
|
|
176
176
|
|
|
177
|
+
def list_filled_paths(self, path: str) -> List[str]:
|
|
178
|
+
"""List all filled paths in an IDS.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
path: IDS and occurrence as a string: <IDS>[/<occurrence>]
|
|
182
|
+
"""
|
|
183
|
+
status, result = ll_interface.list_filled_paths(self.ctx, path)
|
|
184
|
+
if status != 0:
|
|
185
|
+
raise LowlevelError(f"list filled paths for {path!r}", status)
|
|
186
|
+
return result
|
|
187
|
+
|
|
177
188
|
def close(self):
|
|
178
189
|
"""Close this ALContext."""
|
|
179
190
|
ll_interface.end_action(self.ctx)
|
|
@@ -359,7 +370,7 @@ class LazyALContext:
|
|
|
359
370
|
tmin: float,
|
|
360
371
|
tmax: float,
|
|
361
372
|
dtime: Optional[numpy.ndarray],
|
|
362
|
-
interpolation_method: int,
|
|
373
|
+
interpolation_method: Optional[int],
|
|
363
374
|
) -> Iterator["LazyALContext"]:
|
|
364
375
|
"""Lazily start a lowlevel timerange action, see
|
|
365
376
|
:meth:`ALContext.timerange_action`.
|
|
@@ -38,8 +38,8 @@ from imas.ids_toplevel import IDSToplevel
|
|
|
38
38
|
|
|
39
39
|
from .al_context import ALContext, LazyALContext
|
|
40
40
|
from .db_entry_helpers import delete_children, get_children, put_children
|
|
41
|
-
from .imas_interface import LLInterfaceError,
|
|
42
|
-
from .mdsplus_model import mdsplus_model_dir
|
|
41
|
+
from .imas_interface import LLInterfaceError, ll_interface
|
|
42
|
+
from .mdsplus_model import mdsplus_model_dir, get_mdsplus_model_var
|
|
43
43
|
from .uda_support import extract_idsdef, get_dd_version_from_idsdef_xml
|
|
44
44
|
|
|
45
45
|
_OPEN_MODES = {
|
|
@@ -52,14 +52,6 @@ _OPEN_MODES = {
|
|
|
52
52
|
logger = logging.getLogger(__name__)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def require_imas_available():
|
|
56
|
-
if not has_imas:
|
|
57
|
-
raise RuntimeError(
|
|
58
|
-
"The IMAS Core library is not available. Please install 'imas_core', "
|
|
59
|
-
"or load a supported IMAS module if you use an HPC environment."
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
55
|
class ALDBEntryImpl(DBEntryImpl):
|
|
64
56
|
"""DBEntry implementation using imas_core as a backend."""
|
|
65
57
|
|
|
@@ -86,7 +78,6 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
86
78
|
|
|
87
79
|
@classmethod
|
|
88
80
|
def from_uri(cls, uri: str, mode: str, factory: IDSFactory) -> "ALDBEntryImpl":
|
|
89
|
-
require_imas_available()
|
|
90
81
|
if mode not in _OPEN_MODES:
|
|
91
82
|
modes = list(_OPEN_MODES)
|
|
92
83
|
raise ValueError(f"Unknown mode {mode!r}, was expecting any of {modes}")
|
|
@@ -105,9 +96,6 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
105
96
|
options: Any,
|
|
106
97
|
factory: IDSFactory,
|
|
107
98
|
) -> "ALDBEntryImpl":
|
|
108
|
-
# Raise an error if imas is not available
|
|
109
|
-
require_imas_available()
|
|
110
|
-
|
|
111
99
|
# Set defaults
|
|
112
100
|
user_name = user_name or getpass.getuser()
|
|
113
101
|
data_version = data_version or factory.dd_version
|
|
@@ -131,7 +119,7 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
131
119
|
# Building the MDS+ models is required when creating a new Data Entry
|
|
132
120
|
ids_path = mdsplus_model_dir(factory)
|
|
133
121
|
if ids_path:
|
|
134
|
-
os.environ[
|
|
122
|
+
os.environ[get_mdsplus_model_var()] = ids_path
|
|
135
123
|
|
|
136
124
|
elif backend == "uda":
|
|
137
125
|
# Set IDSDEF_PATH to point the UDA backend to the selected DD version
|
|
@@ -149,7 +137,7 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
149
137
|
if idsdef_path is None:
|
|
150
138
|
# Extract XML from the DD zip and point UDA to it
|
|
151
139
|
idsdef_path = extract_idsdef(factory.version)
|
|
152
|
-
os.environ["IDSDEF_PATH"] = idsdef_path
|
|
140
|
+
os.environ["IDSDEF_PATH"] = str(idsdef_path)
|
|
153
141
|
|
|
154
142
|
elif backend in ["hdf5", "memory", "ascii", "flexbuffers"]:
|
|
155
143
|
pass # nothing to set up
|
|
@@ -184,7 +172,7 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
184
172
|
destination: IDSToplevel,
|
|
185
173
|
lazy: bool,
|
|
186
174
|
nbc_map: Optional[NBCPathMap],
|
|
187
|
-
) ->
|
|
175
|
+
) -> IDSToplevel:
|
|
188
176
|
if self._db_ctx is None:
|
|
189
177
|
raise RuntimeError("Database entry is not open.")
|
|
190
178
|
if lazy and self.backend == "ascii":
|
|
@@ -268,6 +256,22 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
268
256
|
raise DataEntryException(
|
|
269
257
|
f"IDS {ids_name!r}, occurrence {occurrence} is empty."
|
|
270
258
|
)
|
|
259
|
+
|
|
260
|
+
# UDA caching doesn't play well when the DD version of the on-disk IDS doesn't
|
|
261
|
+
# match the DD version of this DBEntry. See GH#97
|
|
262
|
+
if self.backend == "uda" and dd_version != self._ids_factory.dd_version:
|
|
263
|
+
cache_mode = self._querydict.get("cache_mode")
|
|
264
|
+
fetch = self._querydict.get("fetch")
|
|
265
|
+
if cache_mode != "none" and fetch not in ("1", "true"):
|
|
266
|
+
raise RuntimeError(
|
|
267
|
+
f"The Data Dictionary version of the data ({dd_version}) is "
|
|
268
|
+
"different from the Data Dictionary version of the DBEntry "
|
|
269
|
+
f"({self._ids_factory.dd_version}). This is not supported when "
|
|
270
|
+
f"using the UDA backend. See {imas.PUBLISHED_DOCUMENTATION_ROOT}"
|
|
271
|
+
"multi-dd.html#uda-backend-caching-and-data-dictionary-versions "
|
|
272
|
+
"for more details and workarounds."
|
|
273
|
+
)
|
|
274
|
+
|
|
271
275
|
return dd_version
|
|
272
276
|
|
|
273
277
|
def put(self, ids: IDSToplevel, occurrence: int, is_slice: bool) -> None:
|
|
@@ -299,7 +303,7 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
299
303
|
if occurrence != 0:
|
|
300
304
|
ll_path += f"/{occurrence}"
|
|
301
305
|
|
|
302
|
-
time_mode = ids.ids_properties.homogeneous_time
|
|
306
|
+
time_mode = int(ids.ids_properties.homogeneous_time)
|
|
303
307
|
if is_slice:
|
|
304
308
|
with self._db_ctx.global_action(ll_path, READ_OP) as read_ctx:
|
|
305
309
|
db_time_mode = read_ctx.read_data(
|
|
@@ -344,9 +348,12 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
344
348
|
ll_path += f"/{occurrence}"
|
|
345
349
|
ids = self._ids_factory.new(ids_name)
|
|
346
350
|
with self._db_ctx.global_action(ll_path, WRITE_OP) as write_ctx:
|
|
347
|
-
delete_children(ids.metadata, write_ctx
|
|
351
|
+
delete_children(ids.metadata, write_ctx)
|
|
348
352
|
|
|
349
353
|
def list_all_occurrences(self, ids_name: str) -> List[int]:
|
|
354
|
+
if self._db_ctx is None:
|
|
355
|
+
raise RuntimeError("Database entry is not open.")
|
|
356
|
+
|
|
350
357
|
try:
|
|
351
358
|
occurrence_list = self._db_ctx.list_all_occurrences(ids_name)
|
|
352
359
|
except LLInterfaceError:
|
|
@@ -357,6 +364,19 @@ class ALDBEntryImpl(DBEntryImpl):
|
|
|
357
364
|
) from None
|
|
358
365
|
return occurrence_list
|
|
359
366
|
|
|
367
|
+
def list_filled_paths(self, ids_name: str, occurrence: int) -> List[str]:
|
|
368
|
+
if self._db_ctx is None:
|
|
369
|
+
raise RuntimeError("Database entry is not open.")
|
|
370
|
+
ll_path = ids_name
|
|
371
|
+
if occurrence != 0:
|
|
372
|
+
ll_path += f"/{occurrence}"
|
|
373
|
+
paths = self._db_ctx.list_filled_paths(ll_path)
|
|
374
|
+
if not paths:
|
|
375
|
+
raise DataEntryException(
|
|
376
|
+
f"IDS {ids_name!r}, occurrence {occurrence} is empty."
|
|
377
|
+
)
|
|
378
|
+
return paths
|
|
379
|
+
|
|
360
380
|
def _check_uda_warnings(self, lazy: bool) -> None:
|
|
361
381
|
"""Various checks / warnings for the UDA backend."""
|
|
362
382
|
cache_mode = self._querydict.get("cache_mode")
|
|
@@ -12,30 +12,17 @@ import logging
|
|
|
12
12
|
|
|
13
13
|
from packaging.version import Version
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
# Import the Access Layer module
|
|
16
|
+
# First try to import imas_core, which is available since AL 5.2
|
|
17
|
+
from imas_core import _al_lowlevel as lowlevel
|
|
18
|
+
from imas_core import imasdef # noqa: F401
|
|
16
19
|
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
17
21
|
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
from imas_core import _al_lowlevel as lowlevel
|
|
23
|
-
from imas_core import imasdef
|
|
24
|
-
|
|
25
|
-
# Enable throwing exceptions from the _al_lowlevel interface
|
|
26
|
-
enable_exceptions = getattr(lowlevel, "imas_core_config_enable_exceptions", None)
|
|
27
|
-
if enable_exceptions:
|
|
28
|
-
enable_exceptions()
|
|
29
|
-
|
|
30
|
-
except ImportError as exc:
|
|
31
|
-
imas = None
|
|
32
|
-
has_imas = False
|
|
33
|
-
imasdef = None
|
|
34
|
-
lowlevel = None
|
|
35
|
-
logger.warning(
|
|
36
|
-
"Could not import 'imas_core': %s. Some functionality is not available.",
|
|
37
|
-
exc,
|
|
38
|
-
)
|
|
22
|
+
# Enable throwing exceptions from the _al_lowlevel interface
|
|
23
|
+
enable_exceptions = getattr(lowlevel, "imas_core_config_enable_exceptions", None)
|
|
24
|
+
if enable_exceptions:
|
|
25
|
+
enable_exceptions()
|
|
39
26
|
|
|
40
27
|
|
|
41
28
|
class LLInterfaceError(RuntimeError):
|
|
@@ -179,6 +166,11 @@ class LowlevelInterface:
|
|
|
179
166
|
):
|
|
180
167
|
raise self._minimal_version("5.4")
|
|
181
168
|
|
|
169
|
+
# New method in AL 5.7
|
|
170
|
+
|
|
171
|
+
def list_filled_paths(self, ctx, path):
|
|
172
|
+
raise self._minimal_version("5.7")
|
|
173
|
+
|
|
182
174
|
|
|
183
175
|
# Dummy documentation for interface:
|
|
184
176
|
for funcname in dir(LowlevelInterface):
|
|
@@ -14,6 +14,7 @@ import uuid
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from subprocess import CalledProcessError, check_output
|
|
16
16
|
from zlib import crc32
|
|
17
|
+
from packaging.version import Version
|
|
17
18
|
|
|
18
19
|
try:
|
|
19
20
|
from importlib.resources import as_file, files
|
|
@@ -24,6 +25,8 @@ from imas.dd_zip import get_dd_xml, get_dd_xml_crc
|
|
|
24
25
|
from imas.exception import MDSPlusModelError
|
|
25
26
|
from imas.ids_factory import IDSFactory
|
|
26
27
|
|
|
28
|
+
from .imas_interface import ll_interface
|
|
29
|
+
|
|
27
30
|
logger = logging.getLogger(__name__)
|
|
28
31
|
|
|
29
32
|
|
|
@@ -292,6 +295,20 @@ def create_model_ids_xml(cache_dir_path, fname, version):
|
|
|
292
295
|
raise e
|
|
293
296
|
|
|
294
297
|
|
|
298
|
+
def get_mdsplus_model_var() -> str:
|
|
299
|
+
"""
|
|
300
|
+
Return the environemnt variable name used by IMAS-Core to locate models:
|
|
301
|
+
|
|
302
|
+
- 'ids_path' for IMAS-Core<5.6
|
|
303
|
+
- 'MDSPLUS_MODELS_PATH' for IMAS-Core>=5.6
|
|
304
|
+
"""
|
|
305
|
+
return (
|
|
306
|
+
"ids_path"
|
|
307
|
+
if ll_interface._al_version < Version("5.6.0")
|
|
308
|
+
else "MDSPLUS_MODELS_PATH"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
295
312
|
def create_mdsplus_model(cache_dir_path: Path) -> None:
|
|
296
313
|
"""Use jtraverser to compile a valid MDS model file."""
|
|
297
314
|
try:
|
|
@@ -322,7 +339,7 @@ def create_mdsplus_model(cache_dir_path: Path) -> None:
|
|
|
322
339
|
env={
|
|
323
340
|
"PATH": os.environ.get("PATH", ""),
|
|
324
341
|
"LD_LIBRARY_PATH": os.environ.get("LD_LIBRARY_PATH", ""),
|
|
325
|
-
|
|
342
|
+
get_mdsplus_model_var(): str(cache_dir_path),
|
|
326
343
|
},
|
|
327
344
|
)
|
|
328
345
|
# Touch a file to show that we have finished the model
|
|
@@ -349,7 +366,7 @@ def jTraverser_jar() -> Path:
|
|
|
349
366
|
for component in os.environ.get("CLASSPATH", "").split(":"):
|
|
350
367
|
if component.endswith(".jar"):
|
|
351
368
|
if re.search(".*jTraverser.jar", component):
|
|
352
|
-
return component
|
|
369
|
+
return Path(component)
|
|
353
370
|
else: # assume its a directory (strip any '*' suffix)
|
|
354
371
|
search_dirs.append(component.rstrip("*"))
|
|
355
372
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Union
|
|
3
|
+
from typing import Union, Optional
|
|
4
4
|
from xml.etree import ElementTree as ET
|
|
5
5
|
|
|
6
6
|
from imas import dd_zip
|
|
@@ -10,7 +10,7 @@ from .mdsplus_model import _get_xdg_cache_dir
|
|
|
10
10
|
logger = logging.getLogger(__name__)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def get_dd_version_from_idsdef_xml(path: Union[str, Path]) -> str:
|
|
13
|
+
def get_dd_version_from_idsdef_xml(path: Union[str, Path]) -> Optional[str]:
|
|
14
14
|
"""Parse the IDSDef.xml up to the point where the Data Dictionary version is set.
|
|
15
15
|
|
|
16
16
|
Returns:
|
|
@@ -92,6 +92,14 @@ class NCDBEntryImpl(DBEntryImpl):
|
|
|
92
92
|
)
|
|
93
93
|
self._dataset.close()
|
|
94
94
|
|
|
95
|
+
def _get_group(self, ids_name: str, occurrence: int) -> "netCDF4.Group":
|
|
96
|
+
try:
|
|
97
|
+
return self._dataset[f"{ids_name}/{occurrence}"]
|
|
98
|
+
except LookupError as exc:
|
|
99
|
+
raise DataEntryException(
|
|
100
|
+
f"IDS {ids_name!r}, occurrence {occurrence} is not found."
|
|
101
|
+
) from exc
|
|
102
|
+
|
|
95
103
|
def get(
|
|
96
104
|
self,
|
|
97
105
|
ids_name: str,
|
|
@@ -100,7 +108,7 @@ class NCDBEntryImpl(DBEntryImpl):
|
|
|
100
108
|
destination: IDSToplevel,
|
|
101
109
|
lazy: bool,
|
|
102
110
|
nbc_map: Optional[NBCPathMap],
|
|
103
|
-
) ->
|
|
111
|
+
) -> IDSToplevel:
|
|
104
112
|
# Feature compatibility checks
|
|
105
113
|
if parameters is not None:
|
|
106
114
|
if isinstance(parameters, GetSliceParameters):
|
|
@@ -110,12 +118,7 @@ class NCDBEntryImpl(DBEntryImpl):
|
|
|
110
118
|
raise NotImplementedError(f"`{func}` is not available for netCDF files.")
|
|
111
119
|
|
|
112
120
|
# Check if the IDS/occurrence exists, and obtain the group it is stored in
|
|
113
|
-
|
|
114
|
-
group = self._dataset[f"{ids_name}/{occurrence}"]
|
|
115
|
-
except KeyError:
|
|
116
|
-
raise DataEntryException(
|
|
117
|
-
f"IDS {ids_name!r}, occurrence {occurrence} is not found."
|
|
118
|
-
)
|
|
121
|
+
group = self._get_group(ids_name, occurrence)
|
|
119
122
|
|
|
120
123
|
# Load data into the destination IDS
|
|
121
124
|
if self._ds_factory.dd_version == destination._dd_version:
|
|
@@ -183,3 +186,17 @@ class NCDBEntryImpl(DBEntryImpl):
|
|
|
183
186
|
|
|
184
187
|
occurrence_list.sort()
|
|
185
188
|
return occurrence_list
|
|
189
|
+
|
|
190
|
+
def list_filled_paths(self, ids_name: str, occurrence: int) -> List[str]:
|
|
191
|
+
# Check if the IDS/occurrence exists, and obtain the group it is stored in
|
|
192
|
+
group = self._get_group(ids_name, occurrence)
|
|
193
|
+
|
|
194
|
+
result = []
|
|
195
|
+
for name, variable in group.variables.items():
|
|
196
|
+
if variable.ndim == 0 and variable.dtype == "S1":
|
|
197
|
+
continue # (Array of) Structure metadata node, no data
|
|
198
|
+
if name.endswith(":shape"):
|
|
199
|
+
continue # Shape data, not a DD path
|
|
200
|
+
result.append(name.replace(".", "/"))
|
|
201
|
+
|
|
202
|
+
return result
|
imas/backends/netcdf/ids2nc.py
CHANGED
|
@@ -47,7 +47,6 @@ class IDS2NC(IDSTensorizer):
|
|
|
47
47
|
|
|
48
48
|
def create_variables(self) -> None:
|
|
49
49
|
"""Create netCDF variables."""
|
|
50
|
-
get_dimensions = self.ncmeta.get_dimensions
|
|
51
50
|
for path in self.filled_data:
|
|
52
51
|
metadata = self.ids.metadata[path]
|
|
53
52
|
var_name = path.replace("/", ".")
|
|
@@ -75,54 +74,21 @@ class IDS2NC(IDSTensorizer):
|
|
|
75
74
|
if dtype is not dtypes[IDSDataType.CPX]: # Set fillvalue
|
|
76
75
|
kwargs.update(fill_value=default_fillvals[metadata.data_type])
|
|
77
76
|
# Create variable
|
|
78
|
-
dimensions = get_dimensions(path
|
|
77
|
+
dimensions = self.get_dimensions(path)
|
|
79
78
|
var = self.group.createVariable(var_name, dtype, dimensions, **kwargs)
|
|
80
79
|
|
|
81
80
|
# Fill metadata attributes
|
|
82
|
-
var.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if metadata.data_type is not IDSDataType.STRUCT_ARRAY:
|
|
95
|
-
coordinates = self.filter_coordinates(path)
|
|
96
|
-
if coordinates:
|
|
97
|
-
var.coordinates = coordinates
|
|
98
|
-
|
|
99
|
-
# Sparsity and :shape array
|
|
100
|
-
if path in self.shapes:
|
|
101
|
-
if not metadata.ndim:
|
|
102
|
-
# Doesn't need a :shape array:
|
|
103
|
-
var.sparse = "Sparse data, missing data is filled with _FillValue"
|
|
104
|
-
var.sparse += f" ({default_fillvals[metadata.data_type]})"
|
|
105
|
-
|
|
106
|
-
else:
|
|
107
|
-
shape_name = f"{var_name}:shape"
|
|
108
|
-
var.sparse = f"Sparse data, data shapes are stored in {shape_name}"
|
|
109
|
-
|
|
110
|
-
# Create variable to store data shape
|
|
111
|
-
dimensions = get_dimensions(
|
|
112
|
-
self.ncmeta.aos.get(path), self.homogeneous_time
|
|
113
|
-
) + (f"{metadata.ndim}D",)
|
|
114
|
-
shape_var = self.group.createVariable(
|
|
115
|
-
shape_name,
|
|
116
|
-
SHAPE_DTYPE,
|
|
117
|
-
dimensions,
|
|
118
|
-
)
|
|
119
|
-
doc_indices = ",".join(chr(ord("i") + i) for i in range(3))
|
|
120
|
-
shape_var.documentation = (
|
|
121
|
-
f"Shape information for {var_name}.\n"
|
|
122
|
-
f"{shape_name}[{doc_indices},:] describes the shape of filled "
|
|
123
|
-
f"data of {var_name}[{doc_indices},...]. Data outside this "
|
|
124
|
-
"shape is unset (i.e. filled with _Fillvalue)."
|
|
125
|
-
)
|
|
81
|
+
var.setncatts(self.get_attributes(path, default_fillvals))
|
|
82
|
+
|
|
83
|
+
# :shape array for sparse data
|
|
84
|
+
if path in self.shapes and metadata.ndim:
|
|
85
|
+
shape_name = f"{var_name}:shape"
|
|
86
|
+
# Create variable to store data shape
|
|
87
|
+
dimensions = self.get_shape_dimensions(path)
|
|
88
|
+
shape_var = self.group.createVariable(
|
|
89
|
+
shape_name, SHAPE_DTYPE, dimensions
|
|
90
|
+
)
|
|
91
|
+
shape_var.setncatts(self.get_shape_attributes(var_name))
|
|
126
92
|
|
|
127
93
|
def store_data(self) -> None:
|
|
128
94
|
"""Store data in the netCDF variables"""
|