imas-python 1.2.1.dev13__py3-none-any.whl → 2.0.0.post1__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/_to_xarray.py +73 -0
- imas/_version.py +9 -4
- imas/assets/IDSDef.zip +0 -0
- imas/backends/imas_core/al_context.py +13 -0
- imas/backends/imas_core/db_entry_helpers.py +3 -7
- imas/backends/imas_core/imas_interface.py +1 -1
- imas/backends/imas_core/mdsplus_model.py +34 -35
- imas/backends/netcdf/db_entry_nc.py +13 -12
- imas/backends/netcdf/ids2nc.py +5 -160
- imas/backends/netcdf/ids_tensorizer.py +205 -0
- imas/backends/netcdf/iterators.py +98 -0
- imas/backends/netcdf/nc2ids.py +176 -61
- imas/backends/netcdf/nc_validate.py +2 -1
- imas/ids_convert.py +2 -1
- imas/ids_structure.py +8 -8
- imas/test/test_ids_convert.py +2 -1
- imas/test/test_lazy_loading.py +53 -4
- imas/test/test_nbc_change.py +29 -8
- imas/test/test_nc_validation.py +26 -17
- imas/test/test_to_xarray.py +94 -0
- imas/util.py +75 -3
- {imas_python-1.2.1.dev13.dist-info → imas_python-2.0.0.post1.dist-info}/METADATA +16 -8
- {imas_python-1.2.1.dev13.dist-info → imas_python-2.0.0.post1.dist-info}/RECORD +27 -23
- {imas_python-1.2.1.dev13.dist-info → imas_python-2.0.0.post1.dist-info}/WHEEL +1 -1
- {imas_python-1.2.1.dev13.dist-info → imas_python-2.0.0.post1.dist-info}/LICENSE.txt +0 -0
- {imas_python-1.2.1.dev13.dist-info → imas_python-2.0.0.post1.dist-info}/entry_points.txt +0 -0
- {imas_python-1.2.1.dev13.dist-info → imas_python-2.0.0.post1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# This file is part of IMAS-Python.
|
|
2
|
+
# You should have received the IMAS-Python LICENSE file with this project.
|
|
3
|
+
"""Tensorization logic to convert IDSs to netCDF files and/or xarray Datasets."""
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
import numpy
|
|
9
|
+
|
|
10
|
+
from imas.backends.netcdf.iterators import indexed_tree_iter
|
|
11
|
+
from imas.backends.netcdf.nc_metadata import NCMetadata
|
|
12
|
+
from imas.ids_data_type import IDSDataType
|
|
13
|
+
from imas.ids_defs import IDS_TIME_MODE_HOMOGENEOUS
|
|
14
|
+
from imas.ids_toplevel import IDSToplevel
|
|
15
|
+
|
|
16
|
+
dtypes = {
|
|
17
|
+
IDSDataType.INT: numpy.dtype(numpy.int32),
|
|
18
|
+
IDSDataType.STR: str,
|
|
19
|
+
IDSDataType.FLT: numpy.dtype(numpy.float64),
|
|
20
|
+
IDSDataType.CPX: numpy.dtype(numpy.complex128),
|
|
21
|
+
}
|
|
22
|
+
SHAPE_DTYPE = numpy.int32
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IDSTensorizer:
|
|
26
|
+
"""Common functionality for tensorizing IDSs. Used in IDS2NC and util.to_xarray."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, ids: IDSToplevel, paths_to_tensorize: List[str]) -> None:
|
|
29
|
+
"""Initialize IDSTensorizer.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
ids: IDSToplevel to store in the netCDF group
|
|
33
|
+
paths_to_tensorize: Restrict tensorization to the provided paths. If an
|
|
34
|
+
empty list is provided, all filled quantities in the IDS will be
|
|
35
|
+
tensorized.
|
|
36
|
+
"""
|
|
37
|
+
self.ids = ids
|
|
38
|
+
"""IDS to tensorize."""
|
|
39
|
+
self.paths_to_tensorize = paths_to_tensorize
|
|
40
|
+
"""List of paths to tensorize"""
|
|
41
|
+
|
|
42
|
+
self.ncmeta = NCMetadata(ids.metadata)
|
|
43
|
+
"""NetCDF related metadata."""
|
|
44
|
+
self.dimension_size = {}
|
|
45
|
+
"""Map dimension name to its size."""
|
|
46
|
+
self.filled_data = {}
|
|
47
|
+
"""Map of IDS paths to filled data nodes."""
|
|
48
|
+
self.filled_variables = set()
|
|
49
|
+
"""Set of filled IDS variables"""
|
|
50
|
+
self.homogeneous_time = (
|
|
51
|
+
ids.ids_properties.homogeneous_time == IDS_TIME_MODE_HOMOGENEOUS
|
|
52
|
+
)
|
|
53
|
+
"""True iff the IDS time mode is homogeneous."""
|
|
54
|
+
self.shapes = {}
|
|
55
|
+
"""Map of IDS paths to data shape arrays."""
|
|
56
|
+
|
|
57
|
+
def include_coordinate_paths(self) -> None:
|
|
58
|
+
"""Append all paths that are coordinates of self.paths_to_tensorize"""
|
|
59
|
+
# Use a queue so we can also take coordinates of coordinates into account
|
|
60
|
+
queue = deque(self.paths_to_tensorize)
|
|
61
|
+
# Include all parent AoS as well:
|
|
62
|
+
for path in self.paths_to_tensorize:
|
|
63
|
+
while path:
|
|
64
|
+
path, _, _ = path.rpartition("/")
|
|
65
|
+
if self.ncmeta.get_dimensions(path, self.homogeneous_time):
|
|
66
|
+
queue.append(path)
|
|
67
|
+
|
|
68
|
+
self.paths_to_tensorize = []
|
|
69
|
+
while queue:
|
|
70
|
+
path = queue.popleft()
|
|
71
|
+
if path in self.paths_to_tensorize:
|
|
72
|
+
continue # already processed
|
|
73
|
+
self.paths_to_tensorize.append(path)
|
|
74
|
+
for coordinate in self.ncmeta.get_coordinates(path, self.homogeneous_time):
|
|
75
|
+
queue.append(coordinate.replace(".", "/"))
|
|
76
|
+
|
|
77
|
+
def collect_filled_data(self) -> None:
|
|
78
|
+
"""Collect all filled data in the IDS and determine dimension sizes.
|
|
79
|
+
|
|
80
|
+
Results are stored in :attr:`filled_data` and :attr:`dimension_size`.
|
|
81
|
+
"""
|
|
82
|
+
# Initialize dictionary with all paths that could exist in this IDS
|
|
83
|
+
filled_data = {path: {} for path in self.ncmeta.paths}
|
|
84
|
+
dimension_size = {}
|
|
85
|
+
get_dimensions = self.ncmeta.get_dimensions
|
|
86
|
+
|
|
87
|
+
if self.paths_to_tensorize:
|
|
88
|
+
# Restrict tensorization to provided paths
|
|
89
|
+
iterator = (
|
|
90
|
+
item
|
|
91
|
+
for path in self.paths_to_tensorize
|
|
92
|
+
for item in indexed_tree_iter(self.ids, self.ids.metadata[path])
|
|
93
|
+
if item[1].has_value # Skip nodes without value set
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
# Tensorize all non-empty nodes
|
|
97
|
+
iterator = indexed_tree_iter(self.ids)
|
|
98
|
+
|
|
99
|
+
for aos_index, node in iterator:
|
|
100
|
+
path = node.metadata.path_string
|
|
101
|
+
filled_data[path][aos_index] = node
|
|
102
|
+
ndim = node.metadata.ndim
|
|
103
|
+
if not ndim:
|
|
104
|
+
continue
|
|
105
|
+
dimensions = get_dimensions(path, self.homogeneous_time)
|
|
106
|
+
# We're only interested in the non-tensorized dimensions: [-ndim:]
|
|
107
|
+
for dim_name, size in zip(dimensions[-ndim:], node.shape):
|
|
108
|
+
dimension_size[dim_name] = max(dimension_size.get(dim_name, 0), size)
|
|
109
|
+
|
|
110
|
+
# Remove paths without data
|
|
111
|
+
self.filled_data = {path: data for path, data in filled_data.items() if data}
|
|
112
|
+
self.filled_variables = {path.replace("/", ".") for path in self.filled_data}
|
|
113
|
+
# Store dimension sizes
|
|
114
|
+
self.dimension_size = dimension_size
|
|
115
|
+
|
|
116
|
+
def determine_data_shapes(self) -> None:
|
|
117
|
+
"""Determine tensorized data shapes and sparsity, save in :attr:`shapes`."""
|
|
118
|
+
get_dimensions = self.ncmeta.get_dimensions
|
|
119
|
+
|
|
120
|
+
for path, nodes_dict in self.filled_data.items():
|
|
121
|
+
metadata = self.ids.metadata[path]
|
|
122
|
+
# Structures don't have a size
|
|
123
|
+
if metadata.data_type is IDSDataType.STRUCTURE:
|
|
124
|
+
continue
|
|
125
|
+
ndim = metadata.ndim
|
|
126
|
+
dimensions = get_dimensions(path, self.homogeneous_time)
|
|
127
|
+
|
|
128
|
+
# node shape if it is completely filled
|
|
129
|
+
full_shape = tuple(self.dimension_size[dim] for dim in dimensions[-ndim:])
|
|
130
|
+
|
|
131
|
+
if len(dimensions) == ndim:
|
|
132
|
+
# Data at this path is not tensorized
|
|
133
|
+
node = nodes_dict[()]
|
|
134
|
+
sparse = node.shape != full_shape
|
|
135
|
+
if sparse:
|
|
136
|
+
shapes = numpy.array(node.shape, dtype=SHAPE_DTYPE)
|
|
137
|
+
|
|
138
|
+
else:
|
|
139
|
+
# Data is tensorized, determine if it is homogeneously shaped
|
|
140
|
+
aos_dims = get_dimensions(self.ncmeta.aos[path], self.homogeneous_time)
|
|
141
|
+
shapes_shape = [self.dimension_size[dim] for dim in aos_dims]
|
|
142
|
+
if ndim:
|
|
143
|
+
shapes_shape.append(ndim)
|
|
144
|
+
shapes = numpy.zeros(shapes_shape, dtype=SHAPE_DTYPE)
|
|
145
|
+
|
|
146
|
+
if ndim: # ND types have a shape
|
|
147
|
+
for aos_coords, node in nodes_dict.items():
|
|
148
|
+
shapes[aos_coords] = node.shape
|
|
149
|
+
sparse = not numpy.array_equiv(shapes, full_shape)
|
|
150
|
+
|
|
151
|
+
else: # 0D types don't have a shape
|
|
152
|
+
for aos_coords in nodes_dict.keys():
|
|
153
|
+
shapes[aos_coords] = 1
|
|
154
|
+
sparse = not shapes.all()
|
|
155
|
+
shapes = None
|
|
156
|
+
|
|
157
|
+
if sparse:
|
|
158
|
+
self.shapes[path] = shapes
|
|
159
|
+
if ndim:
|
|
160
|
+
# Ensure there is a pseudo-dimension f"{ndim}D" for shapes variable
|
|
161
|
+
self.dimension_size[f"{ndim}D"] = ndim
|
|
162
|
+
|
|
163
|
+
def filter_coordinates(self, path: str) -> str:
|
|
164
|
+
"""Filter the coordinates list from NCMetadata to filled variables only."""
|
|
165
|
+
return " ".join(
|
|
166
|
+
coordinate
|
|
167
|
+
for coordinate in self.ncmeta.get_coordinates(path, self.homogeneous_time)
|
|
168
|
+
if coordinate in self.filled_variables
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def tensorize(self, path, fillvalue):
|
|
172
|
+
"""
|
|
173
|
+
Tensorizes the data at the given path with the specified fill value.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
path: The path to the data in the IDS.
|
|
177
|
+
fillvalue: The value to fill the tensor with. Can be of any type,
|
|
178
|
+
including strings.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
A tensor filled with the data from the specified path.
|
|
182
|
+
"""
|
|
183
|
+
dimensions = self.ncmeta.get_dimensions(path, self.homogeneous_time)
|
|
184
|
+
shape = tuple(self.dimension_size[dim] for dim in dimensions)
|
|
185
|
+
|
|
186
|
+
# TODO: depending on the data, tmp_var may be HUGE, we may need a more
|
|
187
|
+
# efficient assignment algorithm for large and/or irregular data
|
|
188
|
+
tmp_var = numpy.full(shape, fillvalue)
|
|
189
|
+
if isinstance(fillvalue, str):
|
|
190
|
+
tmp_var = numpy.asarray(tmp_var, dtype=object)
|
|
191
|
+
|
|
192
|
+
shapes = self.shapes.get(path)
|
|
193
|
+
nodes_dict = self.filled_data[path]
|
|
194
|
+
|
|
195
|
+
# Fill tmp_var
|
|
196
|
+
if shapes is None:
|
|
197
|
+
# Data is not sparse, so we can assign to the aos_coords
|
|
198
|
+
for aos_coords, node in nodes_dict.items():
|
|
199
|
+
tmp_var[aos_coords] = node.value
|
|
200
|
+
else:
|
|
201
|
+
# Data is sparse, so we must select a slice
|
|
202
|
+
for aos_coords, node in nodes_dict.items():
|
|
203
|
+
tmp_var[aos_coords + tuple(map(slice, node.shape))] = node.value
|
|
204
|
+
|
|
205
|
+
return tmp_var
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from typing import Iterator, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from imas.ids_base import IDSBase
|
|
4
|
+
from imas.ids_data_type import IDSDataType
|
|
5
|
+
from imas.ids_metadata import IDSMetadata
|
|
6
|
+
from imas.ids_struct_array import IDSStructArray
|
|
7
|
+
from imas.ids_structure import IDSStructure
|
|
8
|
+
from imas.ids_toplevel import IDSToplevel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _split_on_aos(metadata: IDSMetadata):
|
|
12
|
+
"""Split paths per IDS."""
|
|
13
|
+
paths = []
|
|
14
|
+
curpath = metadata.name
|
|
15
|
+
|
|
16
|
+
item = metadata
|
|
17
|
+
while item._parent.data_type is not None:
|
|
18
|
+
item = item._parent
|
|
19
|
+
if item.data_type is IDSDataType.STRUCT_ARRAY:
|
|
20
|
+
paths.append(curpath)
|
|
21
|
+
curpath = item.name
|
|
22
|
+
else:
|
|
23
|
+
curpath = f"{item.name}/{curpath}"
|
|
24
|
+
paths.append(curpath)
|
|
25
|
+
return paths[::-1]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
IndexedNode = Tuple[Tuple[int, ...], IDSBase]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def indexed_tree_iter(
|
|
32
|
+
ids: IDSToplevel, metadata: Optional[IDSMetadata] = None
|
|
33
|
+
) -> Iterator[IndexedNode]:
|
|
34
|
+
"""Tree iterator that tracks indices of all ancestor array of structures.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
ids: IDS top level element to iterate over
|
|
38
|
+
metadata: Iterate over all nodes inside the IDS at the metadata object.
|
|
39
|
+
If ``None``, all filled items in the IDS are iterated over.
|
|
40
|
+
|
|
41
|
+
Yields:
|
|
42
|
+
(aos_indices, node) for all filled nodes.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
>>> ids = imas.IDSFactory().new("core_profiles")
|
|
46
|
+
>>> ids.profiles_1d.resize(2)
|
|
47
|
+
>>> ids.profiles_1d[0].time = 1.0
|
|
48
|
+
>>> ids.profiles_1d[1].t_i_average = [1.0]
|
|
49
|
+
>>> list(indexed_tree_iter(ids))
|
|
50
|
+
[
|
|
51
|
+
((), <IDSStructArray (IDS:core_profiles, profiles_1d with 2 items)>),
|
|
52
|
+
((0,), <IDSFloat0D (IDS:core_profiles, profiles_1d[0]/time, FLT_0D)>),
|
|
53
|
+
((1,), <IDSNumericArray (IDS:core_profiles, profiles_1d[1]/t_i_average, FLT_1D)>)
|
|
54
|
+
]
|
|
55
|
+
>>> list(indexed_tree_iter(ids, ids.metadata["profiles_1d/time"]))
|
|
56
|
+
[
|
|
57
|
+
((0,), <IDSFloat0D (IDS:core_profiles, profiles_1d[0]/time, FLT_0D)>),
|
|
58
|
+
((1,), <IDSFloat0D (IDS:core_profiles, profiles_1d[1]/time, empty FLT_0D)>)
|
|
59
|
+
]
|
|
60
|
+
""" # noqa: E501
|
|
61
|
+
if metadata is None:
|
|
62
|
+
# Iterate over all filled nodes in the IDS
|
|
63
|
+
yield from _full_tree_iter(ids, ())
|
|
64
|
+
|
|
65
|
+
else:
|
|
66
|
+
paths = _split_on_aos(metadata)
|
|
67
|
+
if len(paths) == 1:
|
|
68
|
+
yield (), ids[paths[0]]
|
|
69
|
+
else:
|
|
70
|
+
yield from _tree_iter(ids, paths, ())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _tree_iter(
|
|
74
|
+
structure: IDSStructure, paths: List[str], curindex: Tuple[int, ...]
|
|
75
|
+
) -> Iterator[IndexedNode]:
|
|
76
|
+
aos_path, *paths = paths
|
|
77
|
+
aos = structure[aos_path]
|
|
78
|
+
|
|
79
|
+
if len(paths) == 1:
|
|
80
|
+
path = paths[0]
|
|
81
|
+
for i, node in enumerate(aos):
|
|
82
|
+
yield curindex + (i,), node[path]
|
|
83
|
+
|
|
84
|
+
else:
|
|
85
|
+
for i, node in enumerate(aos):
|
|
86
|
+
yield from _tree_iter(node, paths, curindex + (i,))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _full_tree_iter(
|
|
90
|
+
node: IDSStructure, cur_index: Tuple[int, ...]
|
|
91
|
+
) -> Iterator[IndexedNode]:
|
|
92
|
+
for child in node.iter_nonempty_():
|
|
93
|
+
yield (cur_index, child)
|
|
94
|
+
if isinstance(child, IDSStructArray):
|
|
95
|
+
for i in range(len(child)):
|
|
96
|
+
yield from _full_tree_iter(child[i], cur_index + (i,))
|
|
97
|
+
elif isinstance(child, IDSStructure):
|
|
98
|
+
yield from _full_tree_iter(child, cur_index)
|
imas/backends/netcdf/nc2ids.py
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional
|
|
4
4
|
|
|
5
5
|
import netCDF4
|
|
6
|
+
import numpy as np
|
|
6
7
|
|
|
7
8
|
from imas.backends.netcdf import ids2nc
|
|
8
9
|
from imas.backends.netcdf.nc_metadata import NCMetadata
|
|
10
|
+
from imas.backends.netcdf.iterators import indexed_tree_iter
|
|
9
11
|
from imas.exception import InvalidNetCDFEntry
|
|
10
|
-
from imas.
|
|
12
|
+
from imas.ids_convert import NBCPathMap
|
|
11
13
|
from imas.ids_data_type import IDSDataType
|
|
12
14
|
from imas.ids_defs import IDS_TIME_MODE_HOMOGENEOUS
|
|
13
15
|
from imas.ids_metadata import IDSMetadata
|
|
14
|
-
from imas.ids_structure import IDSStructure
|
|
15
16
|
from imas.ids_toplevel import IDSToplevel
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
@@ -24,68 +25,40 @@ def variable_error(var, issue, value, expected=None) -> InvalidNetCDFEntry:
|
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
def split_on_aos(metadata: IDSMetadata):
|
|
28
|
-
paths = []
|
|
29
|
-
curpath = metadata.name
|
|
30
|
-
|
|
31
|
-
item = metadata
|
|
32
|
-
while item._parent.data_type is not None:
|
|
33
|
-
item = item._parent
|
|
34
|
-
if item.data_type is IDSDataType.STRUCT_ARRAY:
|
|
35
|
-
paths.append(curpath)
|
|
36
|
-
curpath = item.name
|
|
37
|
-
else:
|
|
38
|
-
curpath = f"{item.name}/{curpath}"
|
|
39
|
-
paths.append(curpath)
|
|
40
|
-
return paths[::-1]
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
IndexedNode = Tuple[Tuple[int, ...], IDSBase]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def tree_iter(structure: IDSStructure, metadata: IDSMetadata) -> Iterator[IndexedNode]:
|
|
47
|
-
paths = split_on_aos(metadata)
|
|
48
|
-
if len(paths) == 1:
|
|
49
|
-
yield (), structure[paths[0]]
|
|
50
|
-
else:
|
|
51
|
-
yield from _tree_iter(structure, paths, ())
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _tree_iter(
|
|
55
|
-
structure: IDSStructure, paths: List[str], curindex: Tuple[int, ...]
|
|
56
|
-
) -> Iterator[IndexedNode]:
|
|
57
|
-
aos_path, *paths = paths
|
|
58
|
-
aos = structure[aos_path]
|
|
59
|
-
|
|
60
|
-
if len(paths) == 1:
|
|
61
|
-
path = paths[0]
|
|
62
|
-
for i, node in enumerate(aos):
|
|
63
|
-
yield curindex + (i,), node[path]
|
|
64
|
-
|
|
65
|
-
else:
|
|
66
|
-
for i, node in enumerate(aos):
|
|
67
|
-
yield from _tree_iter(node, paths, curindex + (i,))
|
|
68
|
-
|
|
69
|
-
|
|
70
28
|
class NC2IDS:
|
|
71
29
|
"""Class responsible for reading an IDS from a NetCDF group."""
|
|
72
30
|
|
|
73
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
group: netCDF4.Group,
|
|
34
|
+
ids: IDSToplevel,
|
|
35
|
+
ids_metadata: IDSMetadata,
|
|
36
|
+
nbc_map: Optional[NBCPathMap],
|
|
37
|
+
) -> None:
|
|
74
38
|
"""Initialize NC2IDS converter.
|
|
75
39
|
|
|
76
40
|
Args:
|
|
77
41
|
group: NetCDF group that stores the IDS data.
|
|
78
42
|
ids: Corresponding IDS toplevel to store the data in.
|
|
43
|
+
ids_metadata: Metadata corresponding to the DD version that the data is
|
|
44
|
+
stored in.
|
|
45
|
+
nbc_map: Path map for implicit DD conversions.
|
|
79
46
|
"""
|
|
80
47
|
self.group = group
|
|
81
48
|
"""NetCDF Group that the IDS is stored in."""
|
|
82
49
|
self.ids = ids
|
|
83
50
|
"""IDS to store the data in."""
|
|
51
|
+
self.ids_metadata = ids_metadata
|
|
52
|
+
"""Metadata of the IDS in the DD version that the data is stored in"""
|
|
53
|
+
self.nbc_map = nbc_map
|
|
54
|
+
"""Path map for implicit DD conversions."""
|
|
84
55
|
|
|
85
|
-
self.ncmeta = NCMetadata(
|
|
56
|
+
self.ncmeta = NCMetadata(ids_metadata)
|
|
86
57
|
"""NetCDF related metadata."""
|
|
87
58
|
self.variables = list(group.variables)
|
|
88
59
|
"""List of variable names stored in the netCDF group."""
|
|
60
|
+
|
|
61
|
+
self._lazy_map = {}
|
|
89
62
|
# Don't use masked arrays: they're slow and we'll handle most of the unset
|
|
90
63
|
# values through the `:shape` arrays
|
|
91
64
|
self.group.set_auto_mask(False)
|
|
@@ -99,7 +72,7 @@ class NC2IDS:
|
|
|
99
72
|
"Mandatory variable `ids_properties.homogeneous_time` does not exist."
|
|
100
73
|
)
|
|
101
74
|
var = group["ids_properties.homogeneous_time"]
|
|
102
|
-
self._validate_variable(var, ids.ids_properties
|
|
75
|
+
self._validate_variable(var, ids.metadata["ids_properties/homogeneous_time"])
|
|
103
76
|
if var[()] not in [0, 1, 2]:
|
|
104
77
|
raise InvalidNetCDFEntry(
|
|
105
78
|
f"Invalid value for ids_properties.homogeneous_time: {var[()]}. "
|
|
@@ -107,23 +80,52 @@ class NC2IDS:
|
|
|
107
80
|
)
|
|
108
81
|
self.homogeneous_time = var[()] == IDS_TIME_MODE_HOMOGENEOUS
|
|
109
82
|
|
|
110
|
-
def run(self) -> None:
|
|
83
|
+
def run(self, lazy: bool) -> None:
|
|
111
84
|
"""Load the data from the netCDF group into the IDS."""
|
|
112
85
|
self.variables.sort()
|
|
113
86
|
self.validate_variables()
|
|
87
|
+
if lazy:
|
|
88
|
+
self.ids._set_lazy_context(LazyContext(self))
|
|
114
89
|
for var_name in self.variables:
|
|
115
90
|
if var_name.endswith(":shape"):
|
|
116
91
|
continue
|
|
117
|
-
metadata = self.
|
|
92
|
+
metadata = self.ids_metadata[var_name]
|
|
118
93
|
|
|
119
94
|
if metadata.data_type is IDSDataType.STRUCTURE:
|
|
120
95
|
continue # This only contains DD metadata we already know
|
|
121
96
|
|
|
97
|
+
# Handle implicit DD version conversion
|
|
98
|
+
if self.nbc_map is None:
|
|
99
|
+
target_metadata = metadata # no conversion
|
|
100
|
+
elif metadata.path_string in self.nbc_map:
|
|
101
|
+
new_path = self.nbc_map.path[metadata.path_string]
|
|
102
|
+
if new_path is None:
|
|
103
|
+
logging.info(
|
|
104
|
+
"Not loading data for %s: no equivalent data structure exists "
|
|
105
|
+
"in the target Data Dictionary version.",
|
|
106
|
+
metadata.path_string,
|
|
107
|
+
)
|
|
108
|
+
continue
|
|
109
|
+
target_metadata = self.ids.metadata[new_path]
|
|
110
|
+
elif metadata.path_string in self.nbc_map.type_change:
|
|
111
|
+
logging.info(
|
|
112
|
+
"Not loading data for %s: cannot hanlde type changes when "
|
|
113
|
+
"implicitly converting data to the target Data Dictionary version.",
|
|
114
|
+
metadata.path_string,
|
|
115
|
+
)
|
|
116
|
+
continue
|
|
117
|
+
else:
|
|
118
|
+
target_metadata = metadata # no conversion required
|
|
119
|
+
|
|
122
120
|
var = self.group[var_name]
|
|
121
|
+
if lazy:
|
|
122
|
+
self._lazy_map[target_metadata.path_string] = var
|
|
123
|
+
continue
|
|
124
|
+
|
|
123
125
|
if metadata.data_type is IDSDataType.STRUCT_ARRAY:
|
|
124
126
|
if "sparse" in var.ncattrs():
|
|
125
127
|
shapes = self.group[var_name + ":shape"][()]
|
|
126
|
-
for index, node in
|
|
128
|
+
for index, node in indexed_tree_iter(self.ids, target_metadata):
|
|
127
129
|
node.resize(shapes[index][0])
|
|
128
130
|
|
|
129
131
|
else:
|
|
@@ -132,7 +134,7 @@ class NC2IDS:
|
|
|
132
134
|
metadata.path_string, self.homogeneous_time
|
|
133
135
|
)[-1]
|
|
134
136
|
size = self.group.dimensions[dim].size
|
|
135
|
-
for _, node in
|
|
137
|
+
for _, node in indexed_tree_iter(self.ids, target_metadata):
|
|
136
138
|
node.resize(size)
|
|
137
139
|
|
|
138
140
|
continue
|
|
@@ -144,23 +146,30 @@ class NC2IDS:
|
|
|
144
146
|
if "sparse" in var.ncattrs():
|
|
145
147
|
if metadata.ndim:
|
|
146
148
|
shapes = self.group[var_name + ":shape"][()]
|
|
147
|
-
for index, node in
|
|
149
|
+
for index, node in indexed_tree_iter(self.ids, target_metadata):
|
|
148
150
|
shape = shapes[index]
|
|
149
151
|
if shape.all():
|
|
150
|
-
|
|
152
|
+
# NOTE: bypassing IDSPrimitive.value.setter logic
|
|
153
|
+
node._IDSPrimitive__value = data[
|
|
154
|
+
index + tuple(map(slice, shape))
|
|
155
|
+
]
|
|
151
156
|
else:
|
|
152
|
-
for index, node in
|
|
157
|
+
for index, node in indexed_tree_iter(self.ids, target_metadata):
|
|
153
158
|
value = data[index]
|
|
154
159
|
if value != getattr(var, "_FillValue", None):
|
|
155
|
-
|
|
160
|
+
# NOTE: bypassing IDSPrimitive.value.setter logic
|
|
161
|
+
node._IDSPrimitive__value = value
|
|
156
162
|
|
|
157
163
|
elif metadata.path_string not in self.ncmeta.aos:
|
|
158
164
|
# Shortcut for assigning untensorized data
|
|
159
|
-
|
|
165
|
+
# Note: var[()] can return 0D numpy arrays. Instead of handling this
|
|
166
|
+
# here, we'll let IDSPrimitive.value.setter take care of it:
|
|
167
|
+
self.ids[target_metadata.path].value = data
|
|
160
168
|
|
|
161
169
|
else:
|
|
162
|
-
for index, node in
|
|
163
|
-
|
|
170
|
+
for index, node in indexed_tree_iter(self.ids, target_metadata):
|
|
171
|
+
# NOTE: bypassing IDSPrimitive.value.setter logic
|
|
172
|
+
node._IDSPrimitive__value = data[index]
|
|
164
173
|
|
|
165
174
|
def validate_variables(self) -> None:
|
|
166
175
|
"""Validate that all variables in the netCDF Group exist and match the DD."""
|
|
@@ -194,7 +203,7 @@ class NC2IDS:
|
|
|
194
203
|
# Check that the DD defines this variable, and validate its metadata
|
|
195
204
|
var = self.group[var_name]
|
|
196
205
|
try:
|
|
197
|
-
metadata = self.
|
|
206
|
+
metadata = self.ids_metadata[var_name]
|
|
198
207
|
except KeyError:
|
|
199
208
|
raise InvalidNetCDFEntry(
|
|
200
209
|
f"Invalid variable {var_name}: no such variable exists in the "
|
|
@@ -300,3 +309,109 @@ class NC2IDS:
|
|
|
300
309
|
raise variable_error(
|
|
301
310
|
shape_var, "dtype", shape_var.dtype, "any integer type"
|
|
302
311
|
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class LazyContext:
|
|
315
|
+
def __init__(self, nc2ids, index=()):
|
|
316
|
+
self.nc2ids = nc2ids
|
|
317
|
+
self.index = index
|
|
318
|
+
|
|
319
|
+
def get_child(self, child):
|
|
320
|
+
"""
|
|
321
|
+
Retrieves and sets the appropriate context or value for a given
|
|
322
|
+
child node based on its metadata.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
child: The child IDS node which should be lazy loaded.
|
|
326
|
+
|
|
327
|
+
"""
|
|
328
|
+
metadata = child.metadata
|
|
329
|
+
path = metadata.path_string
|
|
330
|
+
data_type = metadata.data_type
|
|
331
|
+
nc2ids = self.nc2ids
|
|
332
|
+
var = nc2ids._lazy_map.get(path)
|
|
333
|
+
|
|
334
|
+
if data_type is IDSDataType.STRUCT_ARRAY:
|
|
335
|
+
# Determine size of the aos
|
|
336
|
+
if var is None:
|
|
337
|
+
size = 0
|
|
338
|
+
elif "sparse" in var.ncattrs():
|
|
339
|
+
size = nc2ids.group[var.name + ":shape"][self.index][0]
|
|
340
|
+
else:
|
|
341
|
+
# FIXME: extract dimension name from nc file?
|
|
342
|
+
dim = nc2ids.ncmeta.get_dimensions(
|
|
343
|
+
metadata.path_string, nc2ids.homogeneous_time
|
|
344
|
+
)[-1]
|
|
345
|
+
size = nc2ids.group.dimensions[dim].size
|
|
346
|
+
|
|
347
|
+
child._set_lazy_context(LazyArrayStructContext(nc2ids, self.index, size))
|
|
348
|
+
|
|
349
|
+
elif data_type is IDSDataType.STRUCTURE:
|
|
350
|
+
child._set_lazy_context(self)
|
|
351
|
+
|
|
352
|
+
elif var is not None: # Data elements
|
|
353
|
+
value = None
|
|
354
|
+
if "sparse" in var.ncattrs():
|
|
355
|
+
if metadata.ndim:
|
|
356
|
+
shape_var = nc2ids.group[var.name + ":shape"]
|
|
357
|
+
shape = shape_var[self.index]
|
|
358
|
+
if shape.all():
|
|
359
|
+
value = var[self.index + tuple(map(slice, shape))]
|
|
360
|
+
else:
|
|
361
|
+
value = var[self.index]
|
|
362
|
+
if value == getattr(var, "_FillValue", None):
|
|
363
|
+
value = None # Skip setting
|
|
364
|
+
else:
|
|
365
|
+
value = var[self.index]
|
|
366
|
+
|
|
367
|
+
if value is not None:
|
|
368
|
+
if isinstance(value, np.ndarray):
|
|
369
|
+
# Convert the numpy array to a read-only view
|
|
370
|
+
value = value.view()
|
|
371
|
+
value.flags.writeable = False
|
|
372
|
+
# NOTE: bypassing IDSPrimitive.value.setter logic
|
|
373
|
+
child._IDSPrimitive__value = value
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class LazyArrayStructContext(LazyContext):
|
|
377
|
+
"""
|
|
378
|
+
LazyArrayStructContext is a subclass of LazyContext that provides a context for
|
|
379
|
+
handling structured arrays in a lazy manner. It is initialized with a NetCDF to
|
|
380
|
+
IDS mapping object, an index, and a size.
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
def __init__(self, nc2ids, index, size):
|
|
384
|
+
"""
|
|
385
|
+
Initialize the instance with nc2ids, index, and size.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
nc2ids: The NetCDF to IDS mapping object.
|
|
389
|
+
index: The index within the NetCDF file.
|
|
390
|
+
size: The size of the data to be processed.
|
|
391
|
+
"""
|
|
392
|
+
super().__init__(nc2ids, index)
|
|
393
|
+
self.size = size
|
|
394
|
+
|
|
395
|
+
def get_context(self):
|
|
396
|
+
"""
|
|
397
|
+
Returns the current context.
|
|
398
|
+
|
|
399
|
+
This method returns the current instance of the class, which is expected
|
|
400
|
+
to have a 'size' attribute as required by IDSStructArray.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
The current instance of the class.
|
|
404
|
+
"""
|
|
405
|
+
return self # IDSStructArray expects to get something with a size attribute
|
|
406
|
+
|
|
407
|
+
def iterate_to_index(self, index: int) -> LazyContext:
|
|
408
|
+
"""
|
|
409
|
+
Iterates to a specified index and returns a LazyContext object.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
index (int): The index to iterate to.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
LazyContext: A LazyContext object initialized with the updated index.
|
|
416
|
+
"""
|
|
417
|
+
return LazyContext(self.nc2ids, self.index + (index,))
|
|
@@ -47,8 +47,9 @@ def validate_netcdf_file(filename: str) -> None:
|
|
|
47
47
|
for ids_name in ids_names:
|
|
48
48
|
for occurrence in entry.list_all_occurrences(ids_name):
|
|
49
49
|
group = dataset[f"{ids_name}/{occurrence}"]
|
|
50
|
+
ids = factory.new(ids_name)
|
|
50
51
|
try:
|
|
51
|
-
NC2IDS(group,
|
|
52
|
+
NC2IDS(group, ids, ids.metadata, None).validate_variables()
|
|
52
53
|
except InvalidNetCDFEntry as exc:
|
|
53
54
|
occ = f":{occurrence}" if occurrence else ""
|
|
54
55
|
raise InvalidNetCDFEntry(f"Invalid IDS {ids_name}{occ}: {exc}")
|
imas/ids_convert.py
CHANGED
|
@@ -533,7 +533,8 @@ def _add_provenance_entry(
|
|
|
533
533
|
# DD version after IMAS-5304
|
|
534
534
|
node.reference.resize(len(node.reference) + 1, keep=True)
|
|
535
535
|
node.reference[-1].name = source_txt
|
|
536
|
-
|
|
536
|
+
utc = getattr(datetime, "UTC", datetime.timezone.utc)
|
|
537
|
+
timestamp = datetime.datetime.now(utc).isoformat(timespec="seconds")
|
|
537
538
|
node.reference[-1].timestamp = timestamp.replace("+00:00", "Z")
|
|
538
539
|
else:
|
|
539
540
|
# DD before IMAS-5304 (between 3.34.0 and 3.41.0)
|