imas-python 1.2.1.dev13__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of imas-python might be problematic. Click here for more details.

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 setuptools_scm
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, Union
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 = '1.2.1.dev13'
16
- __version_tuple__ = version_tuple = (1, 2, 1, 'dev13')
20
+ __version__ = version = '2.0.0'
21
+ __version_tuple__ = version_tuple = (2, 0, 0)
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: Optional[LazyALContext]):
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"""
@@ -32,7 +32,7 @@ except ImportError as exc:
32
32
  imasdef = None
33
33
  lowlevel = None
34
34
  logger.critical(
35
- "Could not import 'al_core': %s. Some functionality is not available.",
35
+ "Could not import 'imas_core': %s. Some functionality is not available.",
36
36
  exc,
37
37
  )
38
38
 
@@ -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.compile_stylesheet(stylesheet_file=str(xslfile))
248
-
249
- input_xml = get_dd_xml(version) if version else None
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 result is False:
275
- logger.error(
276
- "Transformation failed: Check Saxon/C logs for details."
277
- )
278
- raise RuntimeError("Saxon/C XSLT transformation failed.")
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, convert_ids
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
- # FIXME: implement automatic conversion using nbc_map
129
- # As a work-around: do an explicit conversion, but automatic conversion
130
- # will also be needed to implement lazy loading.
131
- ids = self._ds_factory.new(ids_name)
132
- NC2IDS(group, ids).run()
133
- convert_ids(ids, None, target=destination)
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
 
@@ -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.nc_metadata import NCMetadata
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
- self.ids = ids
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
- # TODO: depending on the data, tmp_var may be HUGE, we may need a more
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])