imas-python 0.1.dev1618__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of imas-python might be problematic. Click here for more details.
- imas/__init__.py +34 -0
- imas/__main__.py +17 -0
- imas/_util.py +260 -0
- imas/_version.py +16 -0
- imas/assets/IDSDef2MDSpreTree.xsl +161 -0
- imas/assets/IDS_fake_toplevel.xml +318 -0
- imas/assets/IDS_minimal.xml +37 -0
- imas/assets/IDS_minimal_2.xml +37 -0
- imas/assets/IDS_minimal_struct_array.xml +43 -0
- imas/assets/IDS_minimal_types.xml +169 -0
- imas/assets/ITER_134173_106_core_profiles.ids +2438 -0
- imas/assets/ITER_134173_106_equilibrium.ids +3172 -0
- imas/assets/README.md +5 -0
- imas/assets/core_profiles.ids +2438 -0
- imas/assets/equilibrium.ids +3172 -0
- imas/backends/__init__.py +12 -0
- imas/backends/db_entry_impl.py +122 -0
- imas/backends/imas_core/__init__.py +4 -0
- imas/backends/imas_core/al_context.py +391 -0
- imas/backends/imas_core/db_entry_al.py +405 -0
- imas/backends/imas_core/db_entry_helpers.py +200 -0
- imas/backends/imas_core/imas_interface.py +216 -0
- imas/backends/imas_core/mdsplus_model.py +367 -0
- imas/backends/imas_core/uda_support.py +56 -0
- imas/backends/netcdf/__init__.py +4 -0
- imas/backends/netcdf/db_entry_nc.py +176 -0
- imas/backends/netcdf/ids2nc.py +306 -0
- imas/backends/netcdf/nc2ids.py +302 -0
- imas/backends/netcdf/nc_metadata.py +357 -0
- imas/backends/netcdf/nc_validate.py +53 -0
- imas/command/cli.py +239 -0
- imas/command/db_analysis.py +290 -0
- imas/command/helpers.py +45 -0
- imas/command/timer.py +77 -0
- imas/db_entry.py +802 -0
- imas/dd_helpers.py +300 -0
- imas/dd_zip.py +297 -0
- imas/exception.py +107 -0
- imas/ids_base.py +149 -0
- imas/ids_convert.py +920 -0
- imas/ids_coordinates.py +400 -0
- imas/ids_data_type.py +115 -0
- imas/ids_defs.py +171 -0
- imas/ids_factory.py +108 -0
- imas/ids_identifiers.py +125 -0
- imas/ids_metadata.py +307 -0
- imas/ids_path.py +254 -0
- imas/ids_primitive.py +510 -0
- imas/ids_struct_array.py +233 -0
- imas/ids_structure.py +331 -0
- imas/ids_toplevel.py +381 -0
- imas/setup_logging.py +64 -0
- imas/test/test_all_dd_versions.py +28 -0
- imas/test/test_cli.py +44 -0
- imas/test/test_dbentry.py +104 -0
- imas/test/test_dd_helpers.py +134 -0
- imas/test/test_dd_zip.py +29 -0
- imas/test/test_deepcopy.py +25 -0
- imas/test/test_exception.py +12 -0
- imas/test/test_get_sample.py +388 -0
- imas/test/test_hash.py +188 -0
- imas/test/test_helpers.py +354 -0
- imas/test/test_identifiers.py +105 -0
- imas/test/test_ids2nc.py +166 -0
- imas/test/test_ids_ascii_data.py +39 -0
- imas/test/test_ids_convert.py +415 -0
- imas/test/test_ids_coordinates.py +165 -0
- imas/test/test_ids_data_type.py +40 -0
- imas/test/test_ids_factory.py +35 -0
- imas/test/test_ids_metadata.py +66 -0
- imas/test/test_ids_mixin.py +20 -0
- imas/test/test_ids_path.py +192 -0
- imas/test/test_ids_primitive.py +109 -0
- imas/test/test_ids_struct_array.py +89 -0
- imas/test/test_ids_structure.py +70 -0
- imas/test/test_ids_toplevel.py +55 -0
- imas/test/test_ids_validate.py +343 -0
- imas/test/test_latest_dd_autofill.py +74 -0
- imas/test/test_latest_dd_manual.py +38 -0
- imas/test/test_latest_dd_resample.py +102 -0
- imas/test/test_lazy_loading.py +201 -0
- imas/test/test_list_occurrences.py +82 -0
- imas/test/test_minimal.py +37 -0
- imas/test/test_minimal_conversion.py +44 -0
- imas/test/test_minimal_io.py +21 -0
- imas/test/test_minimal_struct_array_io.py +60 -0
- imas/test/test_minimal_types.py +436 -0
- imas/test/test_minimal_types_io.py +70 -0
- imas/test/test_minimal_types_io_automatic.py +66 -0
- imas/test/test_nbc_change.py +408 -0
- imas/test/test_nc_autofill.py +13 -0
- imas/test/test_nc_entry.py +28 -0
- imas/test/test_nc_metadata.py +153 -0
- imas/test/test_nc_validation.py +197 -0
- imas/test/test_snippets.py +25 -0
- imas/test/test_static_ids.py +39 -0
- imas/test/test_str_1d.py +99 -0
- imas/test/test_time_slicing.py +182 -0
- imas/test/test_uda_support.py +12 -0
- imas/test/test_util.py +209 -0
- imas/training.py +45 -0
- imas/util.py +526 -0
- imas_python-0.1.dev1618.dist-info/LICENSE.txt +165 -0
- imas_python-0.1.dev1618.dist-info/METADATA +288 -0
- imas_python-0.1.dev1618.dist-info/RECORD +108 -0
- imas_python-0.1.dev1618.dist-info/WHEEL +5 -0
- imas_python-0.1.dev1618.dist-info/entry_points.txt +3 -0
- imas_python-0.1.dev1618.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""DBEntry implementation using NetCDF as a backend."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Optional, Union
|
|
5
|
+
|
|
6
|
+
from imas.backends.db_entry_impl import (
|
|
7
|
+
DBEntryImpl,
|
|
8
|
+
GetSampleParameters,
|
|
9
|
+
GetSliceParameters,
|
|
10
|
+
)
|
|
11
|
+
from imas.backends.netcdf.ids2nc import IDS2NC
|
|
12
|
+
from imas.backends.netcdf.nc2ids import NC2IDS
|
|
13
|
+
from imas.exception import DataEntryException, InvalidNetCDFEntry
|
|
14
|
+
from imas.ids_convert import NBCPathMap, convert_ids
|
|
15
|
+
from imas.ids_factory import IDSFactory
|
|
16
|
+
from imas.ids_toplevel import IDSToplevel
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import netCDF4
|
|
22
|
+
except ImportError:
|
|
23
|
+
netCDF4 = None
|
|
24
|
+
logger.debug("Could not import netCDF4", exc_info=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NCDBEntryImpl(DBEntryImpl):
|
|
28
|
+
"""DBEntry implementation for netCDF storage."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, fname: str, mode: str, factory: IDSFactory) -> None:
|
|
31
|
+
if netCDF4 is None:
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
"The `netCDF4` python module is not available. Please install this "
|
|
34
|
+
"module to read/write IMAS netCDF files with imas-python."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
self._dataset = netCDF4.Dataset(
|
|
38
|
+
fname,
|
|
39
|
+
mode,
|
|
40
|
+
format="NETCDF4",
|
|
41
|
+
auto_complex=True,
|
|
42
|
+
)
|
|
43
|
+
"""NetCDF4 dataset."""
|
|
44
|
+
self._factory = factory
|
|
45
|
+
"""Factory (DD version) that the user wishes to use."""
|
|
46
|
+
self._ds_factory = factory # Overwritten if data exists, see _init_dd_version
|
|
47
|
+
"""Factory (DD version) that the data is stored in."""
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
self._init_dd_version(fname, mode, factory)
|
|
51
|
+
except Exception:
|
|
52
|
+
self._dataset.close()
|
|
53
|
+
raise
|
|
54
|
+
|
|
55
|
+
def _init_dd_version(self, fname: str, mode: str, factory: IDSFactory) -> None:
|
|
56
|
+
"""Check or setup data dictionary version."""
|
|
57
|
+
# Check if there is already data in this dataset:
|
|
58
|
+
if self._dataset.dimensions or self._dataset.variables or self._dataset.groups:
|
|
59
|
+
if "data_dictionary_version" not in self._dataset.ncattrs():
|
|
60
|
+
raise InvalidNetCDFEntry(
|
|
61
|
+
"Invalid netCDF file: `data_dictionary_version` missing"
|
|
62
|
+
)
|
|
63
|
+
dataset_dd_version = self._dataset.data_dictionary_version
|
|
64
|
+
if dataset_dd_version != factory.dd_version:
|
|
65
|
+
self._ds_factory = IDSFactory(dataset_dd_version)
|
|
66
|
+
|
|
67
|
+
elif mode not in ["w", "x", "r+", "a"]:
|
|
68
|
+
# Reading an empty file...
|
|
69
|
+
raise InvalidNetCDFEntry(f"Invalid netCDF file: `{fname}` is empty.")
|
|
70
|
+
else:
|
|
71
|
+
# This is an empty netCDF dataset: set global attributes
|
|
72
|
+
self._dataset.Conventions = "IMAS"
|
|
73
|
+
self._dataset.data_dictionary_version = factory.dd_version
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_uri(cls, uri: str, mode: str, factory: IDSFactory) -> "NCDBEntryImpl":
|
|
77
|
+
return cls(uri, mode, factory)
|
|
78
|
+
|
|
79
|
+
def close(self, *, erase: bool = False) -> None:
|
|
80
|
+
if erase:
|
|
81
|
+
logger.info(
|
|
82
|
+
"The netCDF backend does not support the `erase` keyword argument "
|
|
83
|
+
"to DBEntry.close(): this argument is ignored."
|
|
84
|
+
)
|
|
85
|
+
self._dataset.close()
|
|
86
|
+
|
|
87
|
+
def get(
|
|
88
|
+
self,
|
|
89
|
+
ids_name: str,
|
|
90
|
+
occurrence: int,
|
|
91
|
+
parameters: Union[None, GetSliceParameters, GetSampleParameters],
|
|
92
|
+
destination: IDSToplevel,
|
|
93
|
+
lazy: bool,
|
|
94
|
+
nbc_map: Optional[NBCPathMap],
|
|
95
|
+
) -> None:
|
|
96
|
+
# Feature compatibility checks
|
|
97
|
+
if parameters is not None:
|
|
98
|
+
if isinstance(parameters, GetSliceParameters):
|
|
99
|
+
func = "get_slice"
|
|
100
|
+
else:
|
|
101
|
+
func = "get_sample"
|
|
102
|
+
raise NotImplementedError(f"`{func}` is not available for netCDF files.")
|
|
103
|
+
if lazy:
|
|
104
|
+
raise NotImplementedError(
|
|
105
|
+
"Lazy loading is not implemented for netCDF files."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Check if the IDS/occurrence exists, and obtain the group it is stored in
|
|
109
|
+
try:
|
|
110
|
+
group = self._dataset[f"{ids_name}/{occurrence}"]
|
|
111
|
+
except KeyError:
|
|
112
|
+
raise DataEntryException(
|
|
113
|
+
f"IDS {ids_name!r}, occurrence {occurrence} is not found."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Load data into the destination IDS
|
|
117
|
+
if self._ds_factory.dd_version == destination._dd_version:
|
|
118
|
+
NC2IDS(group, destination).run()
|
|
119
|
+
else:
|
|
120
|
+
# FIXME: implement automatic conversion using nbc_map
|
|
121
|
+
# As a work-around: do an explicit conversion, but automatic conversion
|
|
122
|
+
# will also be needed to implement lazy loading.
|
|
123
|
+
ids = self._ds_factory.new(ids_name)
|
|
124
|
+
NC2IDS(group, ids).run()
|
|
125
|
+
convert_ids(ids, None, target=destination)
|
|
126
|
+
|
|
127
|
+
return destination
|
|
128
|
+
|
|
129
|
+
def read_dd_version(self, ids_name: str, occurrence: int) -> str:
|
|
130
|
+
return self._ds_factory.version # All IDSs must be stored in this DD version
|
|
131
|
+
|
|
132
|
+
def put(self, ids: IDSToplevel, occurrence: int, is_slice: bool) -> None:
|
|
133
|
+
if is_slice:
|
|
134
|
+
raise NotImplementedError("`put_slice` is not available for netCDF files.")
|
|
135
|
+
if self._ds_factory.dd_version != ids._dd_version:
|
|
136
|
+
# FIXME: implement automatic conversion?
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
f"Cannot store an IDS with DD version {ids._dd_version} in a "
|
|
139
|
+
f"netCDF file with DD version {self._ds_factory.version}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
ids_name = ids.metadata.name
|
|
143
|
+
# netCDF4 limitation: cannot overwrite existing groups
|
|
144
|
+
if ids_name in self._dataset.groups:
|
|
145
|
+
if str(occurrence) in self._dataset[ids_name].groups:
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
f"IDS {ids_name}, occurrence {occurrence} already exists. "
|
|
148
|
+
"Cannot overwrite existing data."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if hasattr(ids.ids_properties, "version_put"):
|
|
152
|
+
# Ensure the correct DD version:
|
|
153
|
+
ids.ids_properties.version_put.data_dictionary = self._ds_factory.version
|
|
154
|
+
|
|
155
|
+
group = self._dataset.createGroup(f"{ids_name}/{occurrence}")
|
|
156
|
+
IDS2NC(ids, group).run()
|
|
157
|
+
|
|
158
|
+
def access_layer_version(self) -> str:
|
|
159
|
+
return "N/A" # We don't use the Access Layer
|
|
160
|
+
|
|
161
|
+
def delete_data(self, ids_name: str, occurrence: int) -> None:
|
|
162
|
+
raise NotImplementedError("The netCDF backend does not support deleting IDSs.")
|
|
163
|
+
|
|
164
|
+
def list_all_occurrences(self, ids_name: str) -> List[int]:
|
|
165
|
+
occurrence_list = []
|
|
166
|
+
if ids_name in self._dataset.groups:
|
|
167
|
+
for group in self._dataset[ids_name].groups:
|
|
168
|
+
try:
|
|
169
|
+
occurrence_list.append(int(group))
|
|
170
|
+
except ValueError:
|
|
171
|
+
logger.warning(
|
|
172
|
+
"Invalid occurrence %r found for IDS %s", group, ids_name
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
occurrence_list.sort()
|
|
176
|
+
return occurrence_list
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# This file is part of imas-python.
|
|
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
|
|
7
|
+
|
|
8
|
+
import netCDF4
|
|
9
|
+
import numpy
|
|
10
|
+
|
|
11
|
+
from imas.backends.netcdf.nc_metadata import NCMetadata
|
|
12
|
+
from imas.ids_base import IDSBase
|
|
13
|
+
from imas.ids_data_type import IDSDataType
|
|
14
|
+
from imas.ids_defs import IDS_TIME_MODE_HOMOGENEOUS
|
|
15
|
+
from imas.ids_struct_array import IDSStructArray
|
|
16
|
+
from imas.ids_structure import IDSStructure
|
|
17
|
+
from imas.ids_toplevel import IDSToplevel
|
|
18
|
+
|
|
19
|
+
default_fillvals = {
|
|
20
|
+
IDSDataType.INT: netCDF4.default_fillvals["i4"],
|
|
21
|
+
IDSDataType.STR: "",
|
|
22
|
+
IDSDataType.FLT: netCDF4.default_fillvals["f8"],
|
|
23
|
+
IDSDataType.CPX: netCDF4.default_fillvals["f8"] * (1 + 1j),
|
|
24
|
+
}
|
|
25
|
+
dtypes = {
|
|
26
|
+
IDSDataType.INT: numpy.dtype(numpy.int32),
|
|
27
|
+
IDSDataType.STR: str,
|
|
28
|
+
IDSDataType.FLT: numpy.dtype(numpy.float64),
|
|
29
|
+
IDSDataType.CPX: numpy.dtype(numpy.complex128),
|
|
30
|
+
}
|
|
31
|
+
SHAPE_DTYPE = numpy.int32
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def nc_tree_iter(
|
|
35
|
+
node: IDSStructure, aos_index: Tuple[int, ...] = ()
|
|
36
|
+
) -> Iterator[Tuple[Tuple[int, ...], IDSBase]]:
|
|
37
|
+
"""Tree iterator that tracks indices of all ancestor array of structures.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
node: IDS node to iterate over
|
|
41
|
+
|
|
42
|
+
Yields:
|
|
43
|
+
(aos_index, node) for all filled nodes.
|
|
44
|
+
"""
|
|
45
|
+
for child in node.iter_nonempty_():
|
|
46
|
+
yield (aos_index, child)
|
|
47
|
+
if isinstance(child, IDSStructArray):
|
|
48
|
+
for i in range(len(child)):
|
|
49
|
+
yield from nc_tree_iter(child[i], aos_index + (i,))
|
|
50
|
+
elif isinstance(child, IDSStructure):
|
|
51
|
+
yield from nc_tree_iter(child, aos_index)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class IDS2NC:
|
|
55
|
+
"""Class responsible for storing an IDS to a NetCDF file."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, ids: IDSToplevel, group: netCDF4.Group) -> None:
|
|
58
|
+
"""Initialize IDS2NC converter.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
ids: IDSToplevel to store in the netCDF group
|
|
62
|
+
group: Empty netCDF group to store the IDS in.
|
|
63
|
+
"""
|
|
64
|
+
self.ids = ids
|
|
65
|
+
"""IDS to store."""
|
|
66
|
+
self.group = group
|
|
67
|
+
"""NetCDF Group to store the IDS in."""
|
|
68
|
+
|
|
69
|
+
self.ncmeta = NCMetadata(ids.metadata)
|
|
70
|
+
"""NetCDF related metadata."""
|
|
71
|
+
self.dimension_size = {}
|
|
72
|
+
"""Map dimension name to its size."""
|
|
73
|
+
self.filled_data = {}
|
|
74
|
+
"""Map of IDS paths to filled data nodes."""
|
|
75
|
+
self.filled_variables = set()
|
|
76
|
+
"""Set of filled IDS variables"""
|
|
77
|
+
self.homogeneous_time = (
|
|
78
|
+
ids.ids_properties.homogeneous_time == IDS_TIME_MODE_HOMOGENEOUS
|
|
79
|
+
)
|
|
80
|
+
"""True iff the IDS time mode is homogeneous."""
|
|
81
|
+
self.shapes = {}
|
|
82
|
+
"""Map of IDS paths to data shape arrays."""
|
|
83
|
+
|
|
84
|
+
def run(self) -> None:
|
|
85
|
+
"""Store the IDS in the NetCDF group."""
|
|
86
|
+
self.collect_filled_data()
|
|
87
|
+
self.determine_data_shapes()
|
|
88
|
+
self.create_dimensions()
|
|
89
|
+
self.create_variables()
|
|
90
|
+
# Synchronize variables to disk
|
|
91
|
+
# This is not strictly required (automatically done by netCDF4 when needed), but
|
|
92
|
+
# by separating it we get more meaningful profiling statistics
|
|
93
|
+
self.group.sync()
|
|
94
|
+
self.store_data()
|
|
95
|
+
|
|
96
|
+
def collect_filled_data(self) -> None:
|
|
97
|
+
"""Collect all filled data in the IDS and determine dimension sizes.
|
|
98
|
+
|
|
99
|
+
Results are stored in :attr:`filled_data` and :attr:`dimension_size`.
|
|
100
|
+
"""
|
|
101
|
+
# Initialize dictionary with all paths that could exist in this IDS
|
|
102
|
+
filled_data = {path: {} for path in self.ncmeta.paths}
|
|
103
|
+
dimension_size = {}
|
|
104
|
+
get_dimensions = self.ncmeta.get_dimensions
|
|
105
|
+
|
|
106
|
+
for aos_index, node in nc_tree_iter(self.ids):
|
|
107
|
+
path = node.metadata.path_string
|
|
108
|
+
filled_data[path][aos_index] = node
|
|
109
|
+
ndim = node.metadata.ndim
|
|
110
|
+
if not ndim:
|
|
111
|
+
continue
|
|
112
|
+
dimensions = get_dimensions(path, self.homogeneous_time)
|
|
113
|
+
# We're only interested in the non-tensorized dimensions: [-ndim:]
|
|
114
|
+
for dim_name, size in zip(dimensions[-ndim:], node.shape):
|
|
115
|
+
dimension_size[dim_name] = max(dimension_size.get(dim_name, 0), size)
|
|
116
|
+
|
|
117
|
+
# Remove paths without data
|
|
118
|
+
self.filled_data = {path: data for path, data in filled_data.items() if data}
|
|
119
|
+
self.filled_variables = {path.replace("/", ".") for path in self.filled_data}
|
|
120
|
+
# Store dimension sizes
|
|
121
|
+
self.dimension_size = dimension_size
|
|
122
|
+
|
|
123
|
+
def determine_data_shapes(self) -> None:
|
|
124
|
+
"""Determine tensorized data shapes and sparsity, save in :attr:`shapes`."""
|
|
125
|
+
get_dimensions = self.ncmeta.get_dimensions
|
|
126
|
+
|
|
127
|
+
for path, nodes_dict in self.filled_data.items():
|
|
128
|
+
metadata = self.ids.metadata[path]
|
|
129
|
+
# Structures don't have a size
|
|
130
|
+
if metadata.data_type is IDSDataType.STRUCTURE:
|
|
131
|
+
continue
|
|
132
|
+
ndim = metadata.ndim
|
|
133
|
+
dimensions = get_dimensions(path, self.homogeneous_time)
|
|
134
|
+
|
|
135
|
+
# node shape if it is completely filled
|
|
136
|
+
full_shape = tuple(self.dimension_size[dim] for dim in dimensions[-ndim:])
|
|
137
|
+
|
|
138
|
+
if len(dimensions) == ndim:
|
|
139
|
+
# Data at this path is not tensorized
|
|
140
|
+
node = nodes_dict[()]
|
|
141
|
+
sparse = node.shape != full_shape
|
|
142
|
+
if sparse:
|
|
143
|
+
shapes = numpy.array(node.shape, dtype=SHAPE_DTYPE)
|
|
144
|
+
|
|
145
|
+
else:
|
|
146
|
+
# Data is tensorized, determine if it is homogeneously shaped
|
|
147
|
+
aos_dims = get_dimensions(self.ncmeta.aos[path], self.homogeneous_time)
|
|
148
|
+
shapes_shape = [self.dimension_size[dim] for dim in aos_dims]
|
|
149
|
+
if ndim:
|
|
150
|
+
shapes_shape.append(ndim)
|
|
151
|
+
shapes = numpy.zeros(shapes_shape, dtype=SHAPE_DTYPE)
|
|
152
|
+
|
|
153
|
+
if ndim: # ND types have a shape
|
|
154
|
+
for aos_coords, node in nodes_dict.items():
|
|
155
|
+
shapes[aos_coords] = node.shape
|
|
156
|
+
sparse = not numpy.array_equiv(shapes, full_shape)
|
|
157
|
+
|
|
158
|
+
else: # 0D types don't have a shape
|
|
159
|
+
for aos_coords in nodes_dict.keys():
|
|
160
|
+
shapes[aos_coords] = 1
|
|
161
|
+
sparse = not shapes.all()
|
|
162
|
+
shapes = None
|
|
163
|
+
|
|
164
|
+
if sparse:
|
|
165
|
+
self.shapes[path] = shapes
|
|
166
|
+
if ndim:
|
|
167
|
+
# Ensure there is a pseudo-dimension f"{ndim}D" for shapes variable
|
|
168
|
+
self.dimension_size[f"{ndim}D"] = ndim
|
|
169
|
+
|
|
170
|
+
def create_dimensions(self) -> None:
|
|
171
|
+
"""Create netCDF dimensions."""
|
|
172
|
+
for dimension, size in self.dimension_size.items():
|
|
173
|
+
self.group.createDimension(dimension, size)
|
|
174
|
+
|
|
175
|
+
def create_variables(self) -> None:
|
|
176
|
+
"""Create netCDF variables."""
|
|
177
|
+
get_dimensions = self.ncmeta.get_dimensions
|
|
178
|
+
for path in self.filled_data:
|
|
179
|
+
metadata = self.ids.metadata[path]
|
|
180
|
+
var_name = path.replace("/", ".")
|
|
181
|
+
|
|
182
|
+
if metadata.data_type in (IDSDataType.STRUCTURE, IDSDataType.STRUCT_ARRAY):
|
|
183
|
+
# Create a 0D dummy variable for metadata
|
|
184
|
+
var = self.group.createVariable(var_name, "S1", ())
|
|
185
|
+
|
|
186
|
+
else:
|
|
187
|
+
dtype = dtypes[metadata.data_type]
|
|
188
|
+
kwargs = {}
|
|
189
|
+
if dtype is not str: # Enable compression:
|
|
190
|
+
kwargs.update(compression="zlib", complevel=1)
|
|
191
|
+
if dtype is not dtypes[IDSDataType.CPX]: # Set fillvalue
|
|
192
|
+
kwargs.update(fill_value=default_fillvals[metadata.data_type])
|
|
193
|
+
# Create variable
|
|
194
|
+
dimensions = get_dimensions(path, self.homogeneous_time)
|
|
195
|
+
var = self.group.createVariable(var_name, dtype, dimensions, **kwargs)
|
|
196
|
+
|
|
197
|
+
# Fill metadata attributes
|
|
198
|
+
var.documentation = metadata.documentation
|
|
199
|
+
if metadata.units:
|
|
200
|
+
var.units = metadata.units
|
|
201
|
+
|
|
202
|
+
ancillary_variables = " ".join(
|
|
203
|
+
error_var
|
|
204
|
+
for error_var in [f"{var_name}_error_upper", f"{var_name}_error_lower"]
|
|
205
|
+
if error_var in self.filled_variables
|
|
206
|
+
)
|
|
207
|
+
if ancillary_variables:
|
|
208
|
+
var.ancillary_variables = ancillary_variables
|
|
209
|
+
|
|
210
|
+
if metadata.data_type is not IDSDataType.STRUCT_ARRAY:
|
|
211
|
+
coordinates = self.filter_coordinates(path)
|
|
212
|
+
if coordinates:
|
|
213
|
+
var.coordinates = coordinates
|
|
214
|
+
|
|
215
|
+
# Sparsity and :shape array
|
|
216
|
+
if path in self.shapes:
|
|
217
|
+
if not metadata.ndim:
|
|
218
|
+
# Doesn't need a :shape array:
|
|
219
|
+
var.sparse = "Sparse data, missing data is filled with _FillValue"
|
|
220
|
+
var.sparse += f" ({default_fillvals[metadata.data_type]})"
|
|
221
|
+
|
|
222
|
+
else:
|
|
223
|
+
shape_name = f"{var_name}:shape"
|
|
224
|
+
var.sparse = f"Sparse data, data shapes are stored in {shape_name}"
|
|
225
|
+
|
|
226
|
+
# Create variable to store data shape
|
|
227
|
+
dimensions = get_dimensions(
|
|
228
|
+
self.ncmeta.aos.get(path), self.homogeneous_time
|
|
229
|
+
) + (f"{metadata.ndim}D",)
|
|
230
|
+
shape_var = self.group.createVariable(
|
|
231
|
+
shape_name,
|
|
232
|
+
SHAPE_DTYPE,
|
|
233
|
+
dimensions,
|
|
234
|
+
)
|
|
235
|
+
doc_indices = ",".join(chr(ord("i") + i) for i in range(3))
|
|
236
|
+
shape_var.documentation = (
|
|
237
|
+
f"Shape information for {var_name}.\n"
|
|
238
|
+
f"{shape_name}[{doc_indices},:] describes the shape of filled "
|
|
239
|
+
f"data of {var_name}[{doc_indices},...]. Data outside this "
|
|
240
|
+
"shape is unset (i.e. filled with _Fillvalue)."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def filter_coordinates(self, path: str) -> str:
|
|
244
|
+
"""Filter the coordinates list from NCMetadata to filled variables only."""
|
|
245
|
+
return " ".join(
|
|
246
|
+
coordinate
|
|
247
|
+
for coordinate in self.ncmeta.get_coordinates(path, self.homogeneous_time)
|
|
248
|
+
if coordinate in self.filled_variables
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def store_data(self) -> None:
|
|
252
|
+
"""Store data in the netCDF variables"""
|
|
253
|
+
for path, nodes_dict in self.filled_data.items():
|
|
254
|
+
metadata = self.ids.metadata[path]
|
|
255
|
+
var_name = path.replace("/", ".")
|
|
256
|
+
|
|
257
|
+
# No data/shapes to store for structures
|
|
258
|
+
if metadata.data_type is IDSDataType.STRUCTURE:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
shapes = self.shapes.get(path)
|
|
262
|
+
if shapes is not None:
|
|
263
|
+
self.group[f"{var_name}:shape"][()] = shapes
|
|
264
|
+
|
|
265
|
+
# No data to store for arrays of structures
|
|
266
|
+
if metadata.data_type is IDSDataType.STRUCT_ARRAY:
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
var = self.group[var_name]
|
|
270
|
+
if var.ndim == metadata.ndim:
|
|
271
|
+
# Not tensorized: directly set value
|
|
272
|
+
node = nodes_dict[()]
|
|
273
|
+
if metadata.data_type is IDSDataType.STR and metadata.ndim == 1:
|
|
274
|
+
# NetCDF doesn't support setting slices for vlen data types
|
|
275
|
+
for i in range(len(node)):
|
|
276
|
+
var[i] = node[i]
|
|
277
|
+
elif shapes is None:
|
|
278
|
+
# Data is not sparse and we can assign everything
|
|
279
|
+
var[()] = node.value
|
|
280
|
+
else:
|
|
281
|
+
# Data is sparse, so we set a slice
|
|
282
|
+
# var[tuple(map(slice, node.shape))] is equivalent to doing
|
|
283
|
+
# var[:node.shape[0], :node.shape[1], (etc.)]
|
|
284
|
+
var[tuple(map(slice, node.shape))] = node.value
|
|
285
|
+
|
|
286
|
+
else:
|
|
287
|
+
# Data is tensorized: tensorize in-memory
|
|
288
|
+
# TODO: depending on the data, tmp_var may be HUGE, we may need a more
|
|
289
|
+
# efficient assignment algorithm for large and/or irregular data
|
|
290
|
+
tmp_var = numpy.full(var.shape, default_fillvals[metadata.data_type])
|
|
291
|
+
if metadata.data_type is IDSDataType.STR:
|
|
292
|
+
tmp_var = numpy.asarray(tmp_var, dtype=object)
|
|
293
|
+
|
|
294
|
+
# Fill tmp_var
|
|
295
|
+
if shapes is None:
|
|
296
|
+
# Data is not sparse, so we can assign to the aos_coords
|
|
297
|
+
for aos_coords, node in nodes_dict.items():
|
|
298
|
+
tmp_var[aos_coords] = node.value
|
|
299
|
+
else:
|
|
300
|
+
# Data is sparse, so we must select a slice
|
|
301
|
+
for aos_coords, node in nodes_dict.items():
|
|
302
|
+
tmp_var[aos_coords + tuple(map(slice, node.shape))] = node.value
|
|
303
|
+
|
|
304
|
+
# Assign data to variable
|
|
305
|
+
var[()] = tmp_var
|
|
306
|
+
del tmp_var
|