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
imas/_to_xarray.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# xarray is an optional dependency, but this module won't be imported when xarray is not
|
|
2
|
+
# available
|
|
3
|
+
import numpy
|
|
4
|
+
import xarray
|
|
5
|
+
|
|
6
|
+
from imas.ids_toplevel import IDSToplevel
|
|
7
|
+
from imas.backends.netcdf.ids_tensorizer import IDSTensorizer
|
|
8
|
+
from imas.ids_data_type import IDSDataType
|
|
9
|
+
|
|
10
|
+
fillvals = {
|
|
11
|
+
IDSDataType.INT: -(2**31) + 1,
|
|
12
|
+
IDSDataType.STR: "",
|
|
13
|
+
IDSDataType.FLT: numpy.nan,
|
|
14
|
+
IDSDataType.CPX: numpy.nan * (1 + 1j),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def to_xarray(ids: IDSToplevel, *paths: str) -> xarray.Dataset:
|
|
19
|
+
"""See :func:`imas.util.to_xarray`"""
|
|
20
|
+
# We really need an IDS toplevel element
|
|
21
|
+
if not isinstance(ids, IDSToplevel):
|
|
22
|
+
raise TypeError(
|
|
23
|
+
f"to_xarray needs a toplevel IDS element as first argument, but got {ids!r}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Valid path can use / or . as separator, but IDSTensorizer expects /. The following
|
|
27
|
+
# block checks if the paths are valid, and by using "metadata.path_string" we ensure
|
|
28
|
+
# that / are used as separator.
|
|
29
|
+
try:
|
|
30
|
+
paths = [ids.metadata[path].path_string for path in paths]
|
|
31
|
+
except KeyError as exc:
|
|
32
|
+
raise ValueError(str(exc)) from None
|
|
33
|
+
|
|
34
|
+
# Converting lazy-loaded IDSs requires users to specify at least one path
|
|
35
|
+
if ids._lazy and not paths:
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
"This IDS is lazy loaded. Please provide at least one path to convert to"
|
|
38
|
+
" xarray."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Use netcdf IDS Tensorizer to tensorize the data and determine metadata
|
|
42
|
+
tensorizer = IDSTensorizer(ids, paths)
|
|
43
|
+
tensorizer.include_coordinate_paths()
|
|
44
|
+
tensorizer.collect_filled_data()
|
|
45
|
+
tensorizer.determine_data_shapes()
|
|
46
|
+
|
|
47
|
+
data_vars = {}
|
|
48
|
+
coordinate_names = set()
|
|
49
|
+
for path in tensorizer.filled_data:
|
|
50
|
+
var_name = path.replace("/", ".")
|
|
51
|
+
metadata = ids.metadata[path]
|
|
52
|
+
if metadata.data_type in (IDSDataType.STRUCTURE, IDSDataType.STRUCT_ARRAY):
|
|
53
|
+
continue # We don't store these in xarray
|
|
54
|
+
|
|
55
|
+
dimensions = tensorizer.ncmeta.get_dimensions(path, tensorizer.homogeneous_time)
|
|
56
|
+
data = tensorizer.tensorize(path, fillvals[metadata.data_type])
|
|
57
|
+
|
|
58
|
+
attrs = dict(documentation=metadata.documentation)
|
|
59
|
+
if metadata.units:
|
|
60
|
+
attrs["units"] = metadata.units
|
|
61
|
+
coordinates = tensorizer.filter_coordinates(path)
|
|
62
|
+
if coordinates:
|
|
63
|
+
coordinate_names.update(coordinates.split(" "))
|
|
64
|
+
attrs["coordinates"] = coordinates
|
|
65
|
+
|
|
66
|
+
data_vars[var_name] = (dimensions, data, attrs)
|
|
67
|
+
|
|
68
|
+
# Remove coordinates from data_vars and put in coordinates mapping:
|
|
69
|
+
coordinates = {}
|
|
70
|
+
for coordinate_name in coordinate_names:
|
|
71
|
+
coordinates[coordinate_name] = data_vars.pop(coordinate_name)
|
|
72
|
+
|
|
73
|
+
return xarray.Dataset(data_vars, coordinates)
|
imas/_version.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
3
6
|
TYPE_CHECKING = False
|
|
4
7
|
if TYPE_CHECKING:
|
|
5
|
-
from typing import Tuple
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
6
11
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
12
|
else:
|
|
8
13
|
VERSION_TUPLE = object
|
|
@@ -12,5 +17,5 @@ __version__: str
|
|
|
12
17
|
__version_tuple__: VERSION_TUPLE
|
|
13
18
|
version_tuple: VERSION_TUPLE
|
|
14
19
|
|
|
15
|
-
__version__ = version = '
|
|
16
|
-
__version_tuple__ = version_tuple = (
|
|
20
|
+
__version__ = version = '2.0.0.post1'
|
|
21
|
+
__version_tuple__ = version_tuple = (2, 0, 0, 'post1')
|
imas/assets/IDSDef.zip
CHANGED
|
Binary file
|
|
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Callable, Iterator, List, Optional, Tuple
|
|
|
10
10
|
|
|
11
11
|
import numpy
|
|
12
12
|
|
|
13
|
+
import imas
|
|
13
14
|
from imas.backends.imas_core.imas_interface import ll_interface
|
|
14
15
|
from imas.exception import LowlevelError
|
|
15
16
|
from imas.ids_defs import (
|
|
@@ -280,6 +281,18 @@ class LazyALContext:
|
|
|
280
281
|
self.context = None
|
|
281
282
|
"""Potential weak reference to opened context."""
|
|
282
283
|
|
|
284
|
+
def get_child(self, child):
|
|
285
|
+
"""
|
|
286
|
+
Retrieve a child entry from the field.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
child (str): The name or identifier of the child entry to retrieve.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The child entry retrieved from the database.
|
|
293
|
+
"""
|
|
294
|
+
imas.backends.imas_core.db_entry_helpers._get_child(child, self)
|
|
295
|
+
|
|
283
296
|
def get_context(self) -> ALContext:
|
|
284
297
|
"""Create and yield the actual ALContext."""
|
|
285
298
|
if self.dbentry._db_ctx is not self.dbentry_ctx:
|
|
@@ -22,7 +22,7 @@ def get_children(
|
|
|
22
22
|
structure: IDSStructure,
|
|
23
23
|
ctx: ALContext,
|
|
24
24
|
time_mode: int,
|
|
25
|
-
nbc_map: Optional[NBCPathMap],
|
|
25
|
+
nbc_map: Optional["NBCPathMap"],
|
|
26
26
|
) -> None:
|
|
27
27
|
"""Recursively get all children of an IDSStructure."""
|
|
28
28
|
# NOTE: changes in this method must be propagated to _get_child and vice versa
|
|
@@ -77,15 +77,11 @@ def get_children(
|
|
|
77
77
|
getattr(structure, name)._IDSPrimitive__value = data
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def _get_child(child: IDSBase, ctx:
|
|
80
|
+
def _get_child(child: IDSBase, ctx: LazyALContext):
|
|
81
81
|
"""Get a single child when required (lazy loading)."""
|
|
82
82
|
# NOTE: changes in this method must be propagated to _get_children and vice versa
|
|
83
83
|
# Performance: this method is specialized for the lazy get
|
|
84
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
85
|
time_mode = ctx.time_mode
|
|
90
86
|
if time_mode == IDS_TIME_MODE_INDEPENDENT and child.metadata.type.is_dynamic:
|
|
91
87
|
return # skip dynamic (time-dependent) nodes
|
|
@@ -148,7 +144,7 @@ def put_children(
|
|
|
148
144
|
ctx: ALContext,
|
|
149
145
|
time_mode: int,
|
|
150
146
|
is_slice: bool,
|
|
151
|
-
nbc_map: Optional[NBCPathMap],
|
|
147
|
+
nbc_map: Optional["NBCPathMap"],
|
|
152
148
|
verify_maxoccur: bool,
|
|
153
149
|
) -> None:
|
|
154
150
|
"""Recursively put all children of an IDSStructure"""
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Helper functions to create MDSPlus reference models
|
|
2
2
|
# and store them in a cache directory (.cache/imas/MDSPlus/name-HASH/)
|
|
3
|
-
"""Module for generating and working with MDSplus models.
|
|
4
|
-
"""
|
|
3
|
+
"""Module for generating and working with MDSplus models."""
|
|
5
4
|
|
|
6
5
|
import errno
|
|
7
6
|
import getpass
|
|
@@ -235,6 +234,14 @@ def model_exists(path: Path) -> bool:
|
|
|
235
234
|
)
|
|
236
235
|
|
|
237
236
|
|
|
237
|
+
def transform_with_xslt(xslt_processor, source, xslfile, output_file):
|
|
238
|
+
return xslt_processor.transform_to_file(
|
|
239
|
+
source_file=str(source),
|
|
240
|
+
stylesheet_file=str(xslfile),
|
|
241
|
+
output_file=str(output_file),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
238
245
|
def create_model_ids_xml(cache_dir_path, fname, version):
|
|
239
246
|
"""Use Saxon/C to compile an ids.xml suitable for creating an MDSplus model."""
|
|
240
247
|
try:
|
|
@@ -243,40 +250,32 @@ def create_model_ids_xml(cache_dir_path, fname, version):
|
|
|
243
250
|
|
|
244
251
|
with PySaxonProcessor(license=False) as proc:
|
|
245
252
|
xslt_processor = proc.new_xslt30_processor()
|
|
246
|
-
|
|
247
|
-
xslt_processor.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if fname:
|
|
251
|
-
source_file = str(fname)
|
|
252
|
-
elif input_xml:
|
|
253
|
-
source_file = input_xml # Use standard input for the XML string
|
|
254
|
-
else:
|
|
255
|
-
raise ValueError(
|
|
256
|
-
"Either 'fname' or 'version' must be provided to generate XML."
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
# xdm_ddgit = proc.make_string_value(str(version or fname))
|
|
260
|
-
# xsltproc.set_parameter("DD_GIT_DESCRIBE", xdm_ddgit)
|
|
261
|
-
# xdm_algit = proc.make_string_value(os.environ.get
|
|
262
|
-
# ("AL_VERSION", "0.0.0"))
|
|
263
|
-
# xsltproc.set_parameter("AL_GIT_DESCRIBE", xdm_algit)
|
|
264
|
-
# Transform XML
|
|
265
|
-
result = xslt_processor.transform_to_file(
|
|
266
|
-
source_file=source_file,
|
|
267
|
-
output_file=str(output_file),
|
|
268
|
-
initial_template_params={
|
|
269
|
-
"DD_GIT_DESCRIBE": str(version or fname),
|
|
270
|
-
"AL_GIT_DESCRIBE": os.environ.get("AL_VERSION", "0.0.0"),
|
|
271
|
-
},
|
|
253
|
+
xdm_ddgit = proc.make_string_value(str(version) or fname)
|
|
254
|
+
xslt_processor.set_parameter("DD_GIT_DESCRIBE", xdm_ddgit)
|
|
255
|
+
xdm_algit = proc.make_string_value(
|
|
256
|
+
os.environ.get("AL_VERSION", "0.0.0")
|
|
272
257
|
)
|
|
273
|
-
|
|
274
|
-
if
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
258
|
+
xslt_processor.set_parameter("AL_GIT_DESCRIBE", xdm_algit)
|
|
259
|
+
if (
|
|
260
|
+
fname is not None
|
|
261
|
+
and fname != "-"
|
|
262
|
+
and fname != ""
|
|
263
|
+
and os.path.exists(fname)
|
|
264
|
+
):
|
|
265
|
+
transform_with_xslt(xslt_processor, fname, xslfile, output_file)
|
|
266
|
+
elif version is not None and version != "":
|
|
267
|
+
xml_string = get_dd_xml(version)
|
|
268
|
+
|
|
269
|
+
with tempfile.NamedTemporaryFile(
|
|
270
|
+
delete=True, mode="w+b"
|
|
271
|
+
) as temp_file:
|
|
272
|
+
temp_file.write(xml_string)
|
|
273
|
+
temp_file.seek(0)
|
|
274
|
+
transform_with_xslt(
|
|
275
|
+
xslt_processor, temp_file.name, xslfile, output_file
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
raise MDSPlusModelError("Either fname or version must be provided")
|
|
280
279
|
except Exception as e:
|
|
281
280
|
if fname:
|
|
282
281
|
logger.error("Error making MDSplus model IDS.xml for %s", fname)
|
|
@@ -11,7 +11,7 @@ from imas.backends.db_entry_impl import (
|
|
|
11
11
|
from imas.backends.netcdf.ids2nc import IDS2NC
|
|
12
12
|
from imas.backends.netcdf.nc2ids import NC2IDS
|
|
13
13
|
from imas.exception import DataEntryException, InvalidNetCDFEntry
|
|
14
|
-
from imas.ids_convert import NBCPathMap,
|
|
14
|
+
from imas.ids_convert import NBCPathMap, dd_version_map_from_factories
|
|
15
15
|
from imas.ids_factory import IDSFactory
|
|
16
16
|
from imas.ids_toplevel import IDSToplevel
|
|
17
17
|
|
|
@@ -108,10 +108,6 @@ class NCDBEntryImpl(DBEntryImpl):
|
|
|
108
108
|
else:
|
|
109
109
|
func = "get_sample"
|
|
110
110
|
raise NotImplementedError(f"`{func}` is not available for netCDF files.")
|
|
111
|
-
if lazy:
|
|
112
|
-
raise NotImplementedError(
|
|
113
|
-
"Lazy loading is not implemented for netCDF files."
|
|
114
|
-
)
|
|
115
111
|
|
|
116
112
|
# Check if the IDS/occurrence exists, and obtain the group it is stored in
|
|
117
113
|
try:
|
|
@@ -123,14 +119,19 @@ class NCDBEntryImpl(DBEntryImpl):
|
|
|
123
119
|
|
|
124
120
|
# Load data into the destination IDS
|
|
125
121
|
if self._ds_factory.dd_version == destination._dd_version:
|
|
126
|
-
NC2IDS(group, destination).run()
|
|
122
|
+
NC2IDS(group, destination, destination.metadata, None).run(lazy)
|
|
127
123
|
else:
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
124
|
+
# Construct relevant NBCPathMap, the one we get from DBEntry has the reverse
|
|
125
|
+
# mapping from what we need. The imas_core logic does the mapping from
|
|
126
|
+
# in-memory to on-disk, while we take what is on-disk and map it to
|
|
127
|
+
# in-memory.
|
|
128
|
+
ddmap, source_is_older = dd_version_map_from_factories(
|
|
129
|
+
ids_name, self._ds_factory, self._factory
|
|
130
|
+
)
|
|
131
|
+
nbc_map = ddmap.old_to_new if source_is_older else ddmap.new_to_old
|
|
132
|
+
NC2IDS(
|
|
133
|
+
group, destination, self._ds_factory.new(ids_name).metadata, nbc_map
|
|
134
|
+
).run(lazy)
|
|
134
135
|
|
|
135
136
|
return destination
|
|
136
137
|
|
imas/backends/netcdf/ids2nc.py
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
# This file is part of IMAS-Python.
|
|
2
2
|
# You should have received the IMAS-Python LICENSE file with this project.
|
|
3
|
-
"""NetCDF IO support for IMAS-Python. Requires [netcdf] extra dependencies.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from typing import Iterator, Tuple
|
|
3
|
+
"""NetCDF IO support for IMAS-Python. Requires [netcdf] extra dependencies."""
|
|
7
4
|
|
|
8
5
|
import netCDF4
|
|
9
|
-
import numpy
|
|
10
6
|
from packaging import version
|
|
11
7
|
|
|
12
|
-
from imas.backends.netcdf.
|
|
8
|
+
from imas.backends.netcdf.ids_tensorizer import SHAPE_DTYPE, IDSTensorizer, dtypes
|
|
13
9
|
from imas.exception import InvalidNetCDFEntry
|
|
14
|
-
from imas.ids_base import IDSBase
|
|
15
10
|
from imas.ids_data_type import IDSDataType
|
|
16
|
-
from imas.ids_defs import IDS_TIME_MODE_HOMOGENEOUS
|
|
17
|
-
from imas.ids_struct_array import IDSStructArray
|
|
18
|
-
from imas.ids_structure import IDSStructure
|
|
19
11
|
from imas.ids_toplevel import IDSToplevel
|
|
20
12
|
|
|
21
13
|
default_fillvals = {
|
|
@@ -24,36 +16,9 @@ default_fillvals = {
|
|
|
24
16
|
IDSDataType.FLT: netCDF4.default_fillvals["f8"],
|
|
25
17
|
IDSDataType.CPX: netCDF4.default_fillvals["f8"] * (1 + 1j),
|
|
26
18
|
}
|
|
27
|
-
dtypes = {
|
|
28
|
-
IDSDataType.INT: numpy.dtype(numpy.int32),
|
|
29
|
-
IDSDataType.STR: str,
|
|
30
|
-
IDSDataType.FLT: numpy.dtype(numpy.float64),
|
|
31
|
-
IDSDataType.CPX: numpy.dtype(numpy.complex128),
|
|
32
|
-
}
|
|
33
|
-
SHAPE_DTYPE = numpy.int32
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def nc_tree_iter(
|
|
37
|
-
node: IDSStructure, aos_index: Tuple[int, ...] = ()
|
|
38
|
-
) -> Iterator[Tuple[Tuple[int, ...], IDSBase]]:
|
|
39
|
-
"""Tree iterator that tracks indices of all ancestor array of structures.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
node: IDS node to iterate over
|
|
43
19
|
|
|
44
|
-
Yields:
|
|
45
|
-
(aos_index, node) for all filled nodes.
|
|
46
|
-
"""
|
|
47
|
-
for child in node.iter_nonempty_():
|
|
48
|
-
yield (aos_index, child)
|
|
49
|
-
if isinstance(child, IDSStructArray):
|
|
50
|
-
for i in range(len(child)):
|
|
51
|
-
yield from nc_tree_iter(child[i], aos_index + (i,))
|
|
52
|
-
elif isinstance(child, IDSStructure):
|
|
53
|
-
yield from nc_tree_iter(child, aos_index)
|
|
54
20
|
|
|
55
|
-
|
|
56
|
-
class IDS2NC:
|
|
21
|
+
class IDS2NC(IDSTensorizer):
|
|
57
22
|
"""Class responsible for storing an IDS to a NetCDF file."""
|
|
58
23
|
|
|
59
24
|
def __init__(self, ids: IDSToplevel, group: netCDF4.Group) -> None:
|
|
@@ -63,112 +28,18 @@ class IDS2NC:
|
|
|
63
28
|
ids: IDSToplevel to store in the netCDF group
|
|
64
29
|
group: Empty netCDF group to store the IDS in.
|
|
65
30
|
"""
|
|
66
|
-
|
|
67
|
-
"""IDS to store."""
|
|
31
|
+
super().__init__(ids, []) # pass empty list: tensorize full IDS
|
|
68
32
|
self.group = group
|
|
69
33
|
"""NetCDF Group to store the IDS in."""
|
|
70
34
|
|
|
71
|
-
self.ncmeta = NCMetadata(ids.metadata)
|
|
72
|
-
"""NetCDF related metadata."""
|
|
73
|
-
self.dimension_size = {}
|
|
74
|
-
"""Map dimension name to its size."""
|
|
75
|
-
self.filled_data = {}
|
|
76
|
-
"""Map of IDS paths to filled data nodes."""
|
|
77
|
-
self.filled_variables = set()
|
|
78
|
-
"""Set of filled IDS variables"""
|
|
79
|
-
self.homogeneous_time = (
|
|
80
|
-
ids.ids_properties.homogeneous_time == IDS_TIME_MODE_HOMOGENEOUS
|
|
81
|
-
)
|
|
82
|
-
"""True iff the IDS time mode is homogeneous."""
|
|
83
|
-
self.shapes = {}
|
|
84
|
-
"""Map of IDS paths to data shape arrays."""
|
|
85
|
-
|
|
86
35
|
def run(self) -> None:
|
|
87
36
|
"""Store the IDS in the NetCDF group."""
|
|
88
37
|
self.collect_filled_data()
|
|
89
38
|
self.determine_data_shapes()
|
|
90
39
|
self.create_dimensions()
|
|
91
40
|
self.create_variables()
|
|
92
|
-
# Synchronize variables to disk
|
|
93
|
-
# This is not strictly required (automatically done by netCDF4 when needed), but
|
|
94
|
-
# by separating it we get more meaningful profiling statistics
|
|
95
|
-
self.group.sync()
|
|
96
41
|
self.store_data()
|
|
97
42
|
|
|
98
|
-
def collect_filled_data(self) -> None:
|
|
99
|
-
"""Collect all filled data in the IDS and determine dimension sizes.
|
|
100
|
-
|
|
101
|
-
Results are stored in :attr:`filled_data` and :attr:`dimension_size`.
|
|
102
|
-
"""
|
|
103
|
-
# Initialize dictionary with all paths that could exist in this IDS
|
|
104
|
-
filled_data = {path: {} for path in self.ncmeta.paths}
|
|
105
|
-
dimension_size = {}
|
|
106
|
-
get_dimensions = self.ncmeta.get_dimensions
|
|
107
|
-
|
|
108
|
-
for aos_index, node in nc_tree_iter(self.ids):
|
|
109
|
-
path = node.metadata.path_string
|
|
110
|
-
filled_data[path][aos_index] = node
|
|
111
|
-
ndim = node.metadata.ndim
|
|
112
|
-
if not ndim:
|
|
113
|
-
continue
|
|
114
|
-
dimensions = get_dimensions(path, self.homogeneous_time)
|
|
115
|
-
# We're only interested in the non-tensorized dimensions: [-ndim:]
|
|
116
|
-
for dim_name, size in zip(dimensions[-ndim:], node.shape):
|
|
117
|
-
dimension_size[dim_name] = max(dimension_size.get(dim_name, 0), size)
|
|
118
|
-
|
|
119
|
-
# Remove paths without data
|
|
120
|
-
self.filled_data = {path: data for path, data in filled_data.items() if data}
|
|
121
|
-
self.filled_variables = {path.replace("/", ".") for path in self.filled_data}
|
|
122
|
-
# Store dimension sizes
|
|
123
|
-
self.dimension_size = dimension_size
|
|
124
|
-
|
|
125
|
-
def determine_data_shapes(self) -> None:
|
|
126
|
-
"""Determine tensorized data shapes and sparsity, save in :attr:`shapes`."""
|
|
127
|
-
get_dimensions = self.ncmeta.get_dimensions
|
|
128
|
-
|
|
129
|
-
for path, nodes_dict in self.filled_data.items():
|
|
130
|
-
metadata = self.ids.metadata[path]
|
|
131
|
-
# Structures don't have a size
|
|
132
|
-
if metadata.data_type is IDSDataType.STRUCTURE:
|
|
133
|
-
continue
|
|
134
|
-
ndim = metadata.ndim
|
|
135
|
-
dimensions = get_dimensions(path, self.homogeneous_time)
|
|
136
|
-
|
|
137
|
-
# node shape if it is completely filled
|
|
138
|
-
full_shape = tuple(self.dimension_size[dim] for dim in dimensions[-ndim:])
|
|
139
|
-
|
|
140
|
-
if len(dimensions) == ndim:
|
|
141
|
-
# Data at this path is not tensorized
|
|
142
|
-
node = nodes_dict[()]
|
|
143
|
-
sparse = node.shape != full_shape
|
|
144
|
-
if sparse:
|
|
145
|
-
shapes = numpy.array(node.shape, dtype=SHAPE_DTYPE)
|
|
146
|
-
|
|
147
|
-
else:
|
|
148
|
-
# Data is tensorized, determine if it is homogeneously shaped
|
|
149
|
-
aos_dims = get_dimensions(self.ncmeta.aos[path], self.homogeneous_time)
|
|
150
|
-
shapes_shape = [self.dimension_size[dim] for dim in aos_dims]
|
|
151
|
-
if ndim:
|
|
152
|
-
shapes_shape.append(ndim)
|
|
153
|
-
shapes = numpy.zeros(shapes_shape, dtype=SHAPE_DTYPE)
|
|
154
|
-
|
|
155
|
-
if ndim: # ND types have a shape
|
|
156
|
-
for aos_coords, node in nodes_dict.items():
|
|
157
|
-
shapes[aos_coords] = node.shape
|
|
158
|
-
sparse = not numpy.array_equiv(shapes, full_shape)
|
|
159
|
-
|
|
160
|
-
else: # 0D types don't have a shape
|
|
161
|
-
for aos_coords in nodes_dict.keys():
|
|
162
|
-
shapes[aos_coords] = 1
|
|
163
|
-
sparse = not shapes.all()
|
|
164
|
-
shapes = None
|
|
165
|
-
|
|
166
|
-
if sparse:
|
|
167
|
-
self.shapes[path] = shapes
|
|
168
|
-
if ndim:
|
|
169
|
-
# Ensure there is a pseudo-dimension f"{ndim}D" for shapes variable
|
|
170
|
-
self.dimension_size[f"{ndim}D"] = ndim
|
|
171
|
-
|
|
172
43
|
def create_dimensions(self) -> None:
|
|
173
44
|
"""Create netCDF dimensions."""
|
|
174
45
|
for dimension, size in self.dimension_size.items():
|
|
@@ -253,14 +124,6 @@ class IDS2NC:
|
|
|
253
124
|
"shape is unset (i.e. filled with _Fillvalue)."
|
|
254
125
|
)
|
|
255
126
|
|
|
256
|
-
def filter_coordinates(self, path: str) -> str:
|
|
257
|
-
"""Filter the coordinates list from NCMetadata to filled variables only."""
|
|
258
|
-
return " ".join(
|
|
259
|
-
coordinate
|
|
260
|
-
for coordinate in self.ncmeta.get_coordinates(path, self.homogeneous_time)
|
|
261
|
-
if coordinate in self.filled_variables
|
|
262
|
-
)
|
|
263
|
-
|
|
264
127
|
def store_data(self) -> None:
|
|
265
128
|
"""Store data in the netCDF variables"""
|
|
266
129
|
for path, nodes_dict in self.filled_data.items():
|
|
@@ -298,22 +161,4 @@ class IDS2NC:
|
|
|
298
161
|
|
|
299
162
|
else:
|
|
300
163
|
# Data is tensorized: tensorize in-memory
|
|
301
|
-
|
|
302
|
-
# efficient assignment algorithm for large and/or irregular data
|
|
303
|
-
tmp_var = numpy.full(var.shape, default_fillvals[metadata.data_type])
|
|
304
|
-
if metadata.data_type is IDSDataType.STR:
|
|
305
|
-
tmp_var = numpy.asarray(tmp_var, dtype=object)
|
|
306
|
-
|
|
307
|
-
# Fill tmp_var
|
|
308
|
-
if shapes is None:
|
|
309
|
-
# Data is not sparse, so we can assign to the aos_coords
|
|
310
|
-
for aos_coords, node in nodes_dict.items():
|
|
311
|
-
tmp_var[aos_coords] = node.value
|
|
312
|
-
else:
|
|
313
|
-
# Data is sparse, so we must select a slice
|
|
314
|
-
for aos_coords, node in nodes_dict.items():
|
|
315
|
-
tmp_var[aos_coords + tuple(map(slice, node.shape))] = node.value
|
|
316
|
-
|
|
317
|
-
# Assign data to variable
|
|
318
|
-
var[()] = tmp_var
|
|
319
|
-
del tmp_var
|
|
164
|
+
var[()] = self.tensorize(path, default_fillvals[metadata.data_type])
|