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.

Files changed (108) hide show
  1. imas/__init__.py +34 -0
  2. imas/__main__.py +17 -0
  3. imas/_util.py +260 -0
  4. imas/_version.py +16 -0
  5. imas/assets/IDSDef2MDSpreTree.xsl +161 -0
  6. imas/assets/IDS_fake_toplevel.xml +318 -0
  7. imas/assets/IDS_minimal.xml +37 -0
  8. imas/assets/IDS_minimal_2.xml +37 -0
  9. imas/assets/IDS_minimal_struct_array.xml +43 -0
  10. imas/assets/IDS_minimal_types.xml +169 -0
  11. imas/assets/ITER_134173_106_core_profiles.ids +2438 -0
  12. imas/assets/ITER_134173_106_equilibrium.ids +3172 -0
  13. imas/assets/README.md +5 -0
  14. imas/assets/core_profiles.ids +2438 -0
  15. imas/assets/equilibrium.ids +3172 -0
  16. imas/backends/__init__.py +12 -0
  17. imas/backends/db_entry_impl.py +122 -0
  18. imas/backends/imas_core/__init__.py +4 -0
  19. imas/backends/imas_core/al_context.py +391 -0
  20. imas/backends/imas_core/db_entry_al.py +405 -0
  21. imas/backends/imas_core/db_entry_helpers.py +200 -0
  22. imas/backends/imas_core/imas_interface.py +216 -0
  23. imas/backends/imas_core/mdsplus_model.py +367 -0
  24. imas/backends/imas_core/uda_support.py +56 -0
  25. imas/backends/netcdf/__init__.py +4 -0
  26. imas/backends/netcdf/db_entry_nc.py +176 -0
  27. imas/backends/netcdf/ids2nc.py +306 -0
  28. imas/backends/netcdf/nc2ids.py +302 -0
  29. imas/backends/netcdf/nc_metadata.py +357 -0
  30. imas/backends/netcdf/nc_validate.py +53 -0
  31. imas/command/cli.py +239 -0
  32. imas/command/db_analysis.py +290 -0
  33. imas/command/helpers.py +45 -0
  34. imas/command/timer.py +77 -0
  35. imas/db_entry.py +802 -0
  36. imas/dd_helpers.py +300 -0
  37. imas/dd_zip.py +297 -0
  38. imas/exception.py +107 -0
  39. imas/ids_base.py +149 -0
  40. imas/ids_convert.py +920 -0
  41. imas/ids_coordinates.py +400 -0
  42. imas/ids_data_type.py +115 -0
  43. imas/ids_defs.py +171 -0
  44. imas/ids_factory.py +108 -0
  45. imas/ids_identifiers.py +125 -0
  46. imas/ids_metadata.py +307 -0
  47. imas/ids_path.py +254 -0
  48. imas/ids_primitive.py +510 -0
  49. imas/ids_struct_array.py +233 -0
  50. imas/ids_structure.py +331 -0
  51. imas/ids_toplevel.py +381 -0
  52. imas/setup_logging.py +64 -0
  53. imas/test/test_all_dd_versions.py +28 -0
  54. imas/test/test_cli.py +44 -0
  55. imas/test/test_dbentry.py +104 -0
  56. imas/test/test_dd_helpers.py +134 -0
  57. imas/test/test_dd_zip.py +29 -0
  58. imas/test/test_deepcopy.py +25 -0
  59. imas/test/test_exception.py +12 -0
  60. imas/test/test_get_sample.py +388 -0
  61. imas/test/test_hash.py +188 -0
  62. imas/test/test_helpers.py +354 -0
  63. imas/test/test_identifiers.py +105 -0
  64. imas/test/test_ids2nc.py +166 -0
  65. imas/test/test_ids_ascii_data.py +39 -0
  66. imas/test/test_ids_convert.py +415 -0
  67. imas/test/test_ids_coordinates.py +165 -0
  68. imas/test/test_ids_data_type.py +40 -0
  69. imas/test/test_ids_factory.py +35 -0
  70. imas/test/test_ids_metadata.py +66 -0
  71. imas/test/test_ids_mixin.py +20 -0
  72. imas/test/test_ids_path.py +192 -0
  73. imas/test/test_ids_primitive.py +109 -0
  74. imas/test/test_ids_struct_array.py +89 -0
  75. imas/test/test_ids_structure.py +70 -0
  76. imas/test/test_ids_toplevel.py +55 -0
  77. imas/test/test_ids_validate.py +343 -0
  78. imas/test/test_latest_dd_autofill.py +74 -0
  79. imas/test/test_latest_dd_manual.py +38 -0
  80. imas/test/test_latest_dd_resample.py +102 -0
  81. imas/test/test_lazy_loading.py +201 -0
  82. imas/test/test_list_occurrences.py +82 -0
  83. imas/test/test_minimal.py +37 -0
  84. imas/test/test_minimal_conversion.py +44 -0
  85. imas/test/test_minimal_io.py +21 -0
  86. imas/test/test_minimal_struct_array_io.py +60 -0
  87. imas/test/test_minimal_types.py +436 -0
  88. imas/test/test_minimal_types_io.py +70 -0
  89. imas/test/test_minimal_types_io_automatic.py +66 -0
  90. imas/test/test_nbc_change.py +408 -0
  91. imas/test/test_nc_autofill.py +13 -0
  92. imas/test/test_nc_entry.py +28 -0
  93. imas/test/test_nc_metadata.py +153 -0
  94. imas/test/test_nc_validation.py +197 -0
  95. imas/test/test_snippets.py +25 -0
  96. imas/test/test_static_ids.py +39 -0
  97. imas/test/test_str_1d.py +99 -0
  98. imas/test/test_time_slicing.py +182 -0
  99. imas/test/test_uda_support.py +12 -0
  100. imas/test/test_util.py +209 -0
  101. imas/training.py +45 -0
  102. imas/util.py +526 -0
  103. imas_python-0.1.dev1618.dist-info/LICENSE.txt +165 -0
  104. imas_python-0.1.dev1618.dist-info/METADATA +288 -0
  105. imas_python-0.1.dev1618.dist-info/RECORD +108 -0
  106. imas_python-0.1.dev1618.dist-info/WHEEL +5 -0
  107. imas_python-0.1.dev1618.dist-info/entry_points.txt +3 -0
  108. 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