imas-python 2.1.0.post1__py3-none-any.whl → 2.3.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.
Files changed (53) hide show
  1. imas/__init__.py +43 -15
  2. imas/_to_xarray.py +21 -14
  3. imas/_version.py +8 -18
  4. imas/assets/ITER_134173_106_equilibrium.ids +0 -2
  5. imas/assets/equilibrium.ids +0 -2
  6. imas/backends/db_entry_impl.py +9 -1
  7. imas/backends/imas_core/al_context.py +14 -3
  8. imas/backends/imas_core/db_entry_al.py +39 -19
  9. imas/backends/imas_core/imas_interface.py +14 -22
  10. imas/backends/imas_core/mdsplus_model.py +19 -2
  11. imas/backends/imas_core/uda_support.py +2 -2
  12. imas/backends/netcdf/db_entry_nc.py +24 -7
  13. imas/backends/netcdf/ids2nc.py +12 -46
  14. imas/backends/netcdf/ids_tensorizer.py +69 -10
  15. imas/backends/netcdf/nc2ids.py +23 -16
  16. imas/backends/netcdf/nc_metadata.py +3 -4
  17. imas/command/cli.py +68 -3
  18. imas/convert_core_edge_plasma.py +124 -0
  19. imas/db_entry.py +70 -7
  20. imas/exception.py +2 -4
  21. imas/ids_base.py +3 -3
  22. imas/ids_convert.py +69 -37
  23. imas/ids_defs.py +37 -77
  24. imas/ids_factory.py +18 -1
  25. imas/ids_identifiers.py +28 -15
  26. imas/ids_metadata.py +6 -6
  27. imas/ids_primitive.py +3 -3
  28. imas/ids_toplevel.py +3 -9
  29. imas/test/test_cli.py +38 -3
  30. imas/test/test_convert_core_edge_plasma.py +68 -0
  31. imas/test/test_dbentry.py +3 -3
  32. imas/test/test_dbentry_uda.py +80 -0
  33. imas/test/test_exception.py +1 -1
  34. imas/test/test_identifiers.py +18 -9
  35. imas/test/test_ids_ascii_data.py +1 -1
  36. imas/test/test_ids_convert.py +72 -0
  37. imas/test/test_ids_toplevel.py +1 -1
  38. imas/test/test_ids_validate.py +1 -1
  39. imas/test/test_latest_dd_autofill.py +1 -3
  40. imas/test/test_lazy_loading.py +5 -5
  41. imas/test/test_list_filled_paths.py +101 -0
  42. imas/test/test_nbc_change.py +1 -1
  43. imas/test/test_snippets.py +1 -1
  44. imas/test/test_static_ids.py +1 -1
  45. imas/test/test_to_xarray.py +26 -3
  46. imas/test/test_util.py +3 -3
  47. imas/training.py +11 -6
  48. {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/METADATA +1 -1
  49. {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/RECORD +53 -49
  50. {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/WHEEL +1 -1
  51. {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/entry_points.txt +0 -0
  52. {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/licenses/LICENSE.txt +0 -0
  53. {imas_python-2.1.0.post1.dist-info → imas_python-2.3.0.dist-info}/top_level.txt +0 -0
imas/__init__.py CHANGED
@@ -1,30 +1,58 @@
1
1
  # This file is part of IMAS-Python.
2
2
  # You should have received the IMAS-Python LICENSE file with this project.
3
3
 
4
- # isort: skip_file
5
-
6
4
  from packaging.version import Version as _V
7
5
 
8
- from ._version import version as __version__ # noqa: F401
9
- from ._version import version_tuple # noqa: F401
10
-
11
6
  # Import logging _first_
12
- from . import setup_logging
7
+ # isort: off
8
+ from . import setup_logging # noqa: F401
9
+
10
+ # isort: on
13
11
 
14
- # Import main user API objects in the imas module
12
+ # Ensure that `imas.util` is loaded when importing imas
13
+ from . import util # noqa: F401
14
+
15
+ # Public API:
16
+ from ._version import version as __version__
17
+ from ._version import version_tuple
18
+ from .convert_core_edge_plasma import (
19
+ convert_to_plasma_profiles,
20
+ convert_to_plasma_sources,
21
+ convert_to_plasma_transport,
22
+ )
15
23
  from .db_entry import DBEntry
16
- from .ids_factory import IDSFactory
17
24
  from .ids_convert import convert_ids
25
+ from .ids_data_type import IDSDataType
26
+ from .ids_factory import IDSFactory
18
27
  from .ids_identifiers import identifiers
19
-
20
- # Load the IMAS-Python IMAS AL/DD core
21
- from . import (
22
- db_entry,
23
- dd_zip,
24
- util,
25
- )
28
+ from .ids_metadata import IDSMetadata, IDSType
29
+ from .ids_primitive import IDSPrimitive
30
+ from .ids_struct_array import IDSStructArray
31
+ from .ids_structure import IDSStructure
32
+ from .ids_toplevel import IDSToplevel
26
33
 
27
34
  PUBLISHED_DOCUMENTATION_ROOT = "https://imas-python.readthedocs.io/en/latest/"
28
35
  """URL to the published documentation."""
29
36
  OLDEST_SUPPORTED_VERSION = _V("3.22.0")
30
37
  """Oldest Data Dictionary version that is supported by IMAS-Python."""
38
+
39
+ __all__ = [
40
+ "__version__",
41
+ "version_tuple",
42
+ "DBEntry",
43
+ "IDSDataType",
44
+ "IDSFactory",
45
+ "IDSMetadata",
46
+ "IDSPrimitive",
47
+ "IDSStructure",
48
+ "IDSStructArray",
49
+ "IDSToplevel",
50
+ "IDSType",
51
+ "convert_ids",
52
+ "convert_to_plasma_profiles",
53
+ "convert_to_plasma_sources",
54
+ "convert_to_plasma_transport",
55
+ "identifiers",
56
+ "PUBLISHED_DOCUMENTATION_ROOT",
57
+ "OLDEST_SUPPORTED_VERSION",
58
+ ]
imas/_to_xarray.py CHANGED
@@ -8,7 +8,7 @@ from imas.backends.netcdf.ids_tensorizer import IDSTensorizer
8
8
  from imas.ids_data_type import IDSDataType
9
9
 
10
10
  fillvals = {
11
- IDSDataType.INT: -(2**31) + 1,
11
+ IDSDataType.INT: numpy.int32(-(2**31) + 1),
12
12
  IDSDataType.STR: "",
13
13
  IDSDataType.FLT: numpy.nan,
14
14
  IDSDataType.CPX: numpy.nan * (1 + 1j),
@@ -27,7 +27,7 @@ def to_xarray(ids: IDSToplevel, *paths: str) -> xarray.Dataset:
27
27
  # block checks if the paths are valid, and by using "metadata.path_string" we ensure
28
28
  # that / are used as separator.
29
29
  try:
30
- paths = [ids.metadata[path].path_string for path in paths]
30
+ paths: list[str] = [ids.metadata[path].path_string for path in paths]
31
31
  except KeyError as exc:
32
32
  raise ValueError(str(exc)) from None
33
33
 
@@ -50,21 +50,28 @@ def to_xarray(ids: IDSToplevel, *paths: str) -> xarray.Dataset:
50
50
  var_name = path.replace("/", ".")
51
51
  metadata = ids.metadata[path]
52
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
53
+ # Metadata variables for (arrays of) structures
54
+ if paths and path not in paths:
55
+ continue
56
+ dimensions = ()
57
+ data = b""
58
+ else:
59
+ dimensions = tensorizer.get_dimensions(path)
60
+ data = tensorizer.tensorize(path, fillvals[metadata.data_type])
65
61
 
62
+ attrs = tensorizer.get_attributes(path, fillvals)
63
+ if "coordinates" in attrs:
64
+ coordinate_names.update(attrs["coordinates"].split(" "))
66
65
  data_vars[var_name] = (dimensions, data, attrs)
67
66
 
67
+ # :shape array for sparse data
68
+ if path in tensorizer.shapes and metadata.ndim:
69
+ shape_name = f"{var_name}:shape"
70
+ dimensions = tensorizer.get_shape_dimensions(path)
71
+ data = tensorizer.shapes[path]
72
+ attrs = tensorizer.get_shape_attributes(var_name)
73
+ data_vars[shape_name] = (dimensions, data, attrs)
74
+
68
75
  # Remove coordinates from data_vars and put in coordinates mapping:
69
76
  coordinates = {}
70
77
  for coordinate_name in coordinate_names:
imas/_version.py CHANGED
@@ -1,5 +1,6 @@
1
- # file generated by setuptools-scm
1
+ # file generated by vcs-versioning
2
2
  # don't change, don't track in version control
3
+ from __future__ import annotations
3
4
 
4
5
  __all__ = [
5
6
  "__version__",
@@ -10,25 +11,14 @@ __all__ = [
10
11
  "commit_id",
11
12
  ]
12
13
 
13
- TYPE_CHECKING = False
14
- if TYPE_CHECKING:
15
- from typing import Tuple
16
- from typing import Union
17
-
18
- VERSION_TUPLE = Tuple[Union[int, str], ...]
19
- COMMIT_ID = Union[str, None]
20
- else:
21
- VERSION_TUPLE = object
22
- COMMIT_ID = object
23
-
24
14
  version: str
25
15
  __version__: str
26
- __version_tuple__: VERSION_TUPLE
27
- version_tuple: VERSION_TUPLE
28
- commit_id: COMMIT_ID
29
- __commit_id__: COMMIT_ID
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
30
20
 
31
- __version__ = version = '2.1.0.post1'
32
- __version_tuple__ = version_tuple = (2, 1, 0, 'post1')
21
+ __version__ = version = '2.3.0'
22
+ __version_tuple__ = version_tuple = (2, 3, 0)
33
23
 
34
24
  __commit_id__ = commit_id = None
@@ -26,8 +26,6 @@ equilibrium/vacuum_toroidal_field/b0
26
26
  dim: 1
27
27
  size: 3
28
28
  -5.2999999999999998e+00 -5.2999999999999998e+00 -5.2999999999999998e+00
29
- equilibrium/grids_ggd
30
- size: 1
31
29
  equilibrium/time_slice
32
30
  size: 3
33
31
  equilibrium/time_slice[0]/boundary/outline/r
@@ -26,8 +26,6 @@ equilibrium/vacuum_toroidal_field/b0
26
26
  dim: 1
27
27
  size: 3
28
28
  -5.2999999999999998e+00 -5.2999999999999998e+00 -5.2999999999999998e+00
29
- equilibrium/grids_ggd
30
- size: 1
31
29
  equilibrium/time_slice
32
30
  size: 3
33
31
  equilibrium/time_slice[0]/boundary/outline/r
@@ -78,7 +78,7 @@ class DBEntryImpl(ABC):
78
78
  destination: IDSToplevel,
79
79
  lazy: bool,
80
80
  nbc_map: Optional[NBCPathMap],
81
- ) -> None:
81
+ ) -> IDSToplevel:
82
82
  """Implement DBEntry.get/get_slice/get_sample. Load data from the data source.
83
83
 
84
84
  Args:
@@ -120,3 +120,11 @@ class DBEntryImpl(ABC):
120
120
  @abstractmethod
121
121
  def list_all_occurrences(self, ids_name: str) -> List[int]:
122
122
  """Implement DBEntry.list_all_occurrences()"""
123
+
124
+ @abstractmethod
125
+ def list_filled_paths(self, ids_name: str, occurrence: int) -> List[str]:
126
+ """Implement DBEntry.list_filled_paths().
127
+
128
+ N.B. DD conversion is handled in DBEntry.list_filled_paths(), this method
129
+ returns the data paths as stored on-disk.
130
+ """
@@ -115,7 +115,7 @@ class ALContext:
115
115
  tmin: float,
116
116
  tmax: float,
117
117
  dtime: Optional[numpy.ndarray],
118
- interpolation_method: int,
118
+ interpolation_method: Optional[int],
119
119
  ) -> "ALContext":
120
120
  """Begin a new timerange action for use in a ``with`` context."""
121
121
  ctx = ll_interface.begin_timerange_action(
@@ -163,7 +163,7 @@ class ALContext:
163
163
  """Call ual_write_data with this context."""
164
164
  status = ll_interface.write_data(self.ctx, path, timebasepath, data)
165
165
  if status != 0:
166
- raise LowlevelError(f"write data at {path!r}: {status=}")
166
+ raise LowlevelError(f"write data at {path!r}", status)
167
167
 
168
168
  def list_all_occurrences(self, ids_name: str) -> List[int]:
169
169
  """List all occurrences of this IDS."""
@@ -174,6 +174,17 @@ class ALContext:
174
174
  return list(occurrences)
175
175
  return []
176
176
 
177
+ def list_filled_paths(self, path: str) -> List[str]:
178
+ """List all filled paths in an IDS.
179
+
180
+ Args:
181
+ path: IDS and occurrence as a string: <IDS>[/<occurrence>]
182
+ """
183
+ status, result = ll_interface.list_filled_paths(self.ctx, path)
184
+ if status != 0:
185
+ raise LowlevelError(f"list filled paths for {path!r}", status)
186
+ return result
187
+
177
188
  def close(self):
178
189
  """Close this ALContext."""
179
190
  ll_interface.end_action(self.ctx)
@@ -359,7 +370,7 @@ class LazyALContext:
359
370
  tmin: float,
360
371
  tmax: float,
361
372
  dtime: Optional[numpy.ndarray],
362
- interpolation_method: int,
373
+ interpolation_method: Optional[int],
363
374
  ) -> Iterator["LazyALContext"]:
364
375
  """Lazily start a lowlevel timerange action, see
365
376
  :meth:`ALContext.timerange_action`.
@@ -38,8 +38,8 @@ from imas.ids_toplevel import IDSToplevel
38
38
 
39
39
  from .al_context import ALContext, LazyALContext
40
40
  from .db_entry_helpers import delete_children, get_children, put_children
41
- from .imas_interface import LLInterfaceError, has_imas, ll_interface
42
- from .mdsplus_model import mdsplus_model_dir
41
+ from .imas_interface import LLInterfaceError, ll_interface
42
+ from .mdsplus_model import mdsplus_model_dir, get_mdsplus_model_var
43
43
  from .uda_support import extract_idsdef, get_dd_version_from_idsdef_xml
44
44
 
45
45
  _OPEN_MODES = {
@@ -52,14 +52,6 @@ _OPEN_MODES = {
52
52
  logger = logging.getLogger(__name__)
53
53
 
54
54
 
55
- def require_imas_available():
56
- if not has_imas:
57
- raise RuntimeError(
58
- "The IMAS Core library is not available. Please install 'imas_core', "
59
- "or load a supported IMAS module if you use an HPC environment."
60
- )
61
-
62
-
63
55
  class ALDBEntryImpl(DBEntryImpl):
64
56
  """DBEntry implementation using imas_core as a backend."""
65
57
 
@@ -86,7 +78,6 @@ class ALDBEntryImpl(DBEntryImpl):
86
78
 
87
79
  @classmethod
88
80
  def from_uri(cls, uri: str, mode: str, factory: IDSFactory) -> "ALDBEntryImpl":
89
- require_imas_available()
90
81
  if mode not in _OPEN_MODES:
91
82
  modes = list(_OPEN_MODES)
92
83
  raise ValueError(f"Unknown mode {mode!r}, was expecting any of {modes}")
@@ -105,9 +96,6 @@ class ALDBEntryImpl(DBEntryImpl):
105
96
  options: Any,
106
97
  factory: IDSFactory,
107
98
  ) -> "ALDBEntryImpl":
108
- # Raise an error if imas is not available
109
- require_imas_available()
110
-
111
99
  # Set defaults
112
100
  user_name = user_name or getpass.getuser()
113
101
  data_version = data_version or factory.dd_version
@@ -131,7 +119,7 @@ class ALDBEntryImpl(DBEntryImpl):
131
119
  # Building the MDS+ models is required when creating a new Data Entry
132
120
  ids_path = mdsplus_model_dir(factory)
133
121
  if ids_path:
134
- os.environ["ids_path"] = ids_path
122
+ os.environ[get_mdsplus_model_var()] = ids_path
135
123
 
136
124
  elif backend == "uda":
137
125
  # Set IDSDEF_PATH to point the UDA backend to the selected DD version
@@ -149,7 +137,7 @@ class ALDBEntryImpl(DBEntryImpl):
149
137
  if idsdef_path is None:
150
138
  # Extract XML from the DD zip and point UDA to it
151
139
  idsdef_path = extract_idsdef(factory.version)
152
- os.environ["IDSDEF_PATH"] = idsdef_path
140
+ os.environ["IDSDEF_PATH"] = str(idsdef_path)
153
141
 
154
142
  elif backend in ["hdf5", "memory", "ascii", "flexbuffers"]:
155
143
  pass # nothing to set up
@@ -184,7 +172,7 @@ class ALDBEntryImpl(DBEntryImpl):
184
172
  destination: IDSToplevel,
185
173
  lazy: bool,
186
174
  nbc_map: Optional[NBCPathMap],
187
- ) -> None:
175
+ ) -> IDSToplevel:
188
176
  if self._db_ctx is None:
189
177
  raise RuntimeError("Database entry is not open.")
190
178
  if lazy and self.backend == "ascii":
@@ -268,6 +256,22 @@ class ALDBEntryImpl(DBEntryImpl):
268
256
  raise DataEntryException(
269
257
  f"IDS {ids_name!r}, occurrence {occurrence} is empty."
270
258
  )
259
+
260
+ # UDA caching doesn't play well when the DD version of the on-disk IDS doesn't
261
+ # match the DD version of this DBEntry. See GH#97
262
+ if self.backend == "uda" and dd_version != self._ids_factory.dd_version:
263
+ cache_mode = self._querydict.get("cache_mode")
264
+ fetch = self._querydict.get("fetch")
265
+ if cache_mode != "none" and fetch not in ("1", "true"):
266
+ raise RuntimeError(
267
+ f"The Data Dictionary version of the data ({dd_version}) is "
268
+ "different from the Data Dictionary version of the DBEntry "
269
+ f"({self._ids_factory.dd_version}). This is not supported when "
270
+ f"using the UDA backend. See {imas.PUBLISHED_DOCUMENTATION_ROOT}"
271
+ "multi-dd.html#uda-backend-caching-and-data-dictionary-versions "
272
+ "for more details and workarounds."
273
+ )
274
+
271
275
  return dd_version
272
276
 
273
277
  def put(self, ids: IDSToplevel, occurrence: int, is_slice: bool) -> None:
@@ -299,7 +303,7 @@ class ALDBEntryImpl(DBEntryImpl):
299
303
  if occurrence != 0:
300
304
  ll_path += f"/{occurrence}"
301
305
 
302
- time_mode = ids.ids_properties.homogeneous_time
306
+ time_mode = int(ids.ids_properties.homogeneous_time)
303
307
  if is_slice:
304
308
  with self._db_ctx.global_action(ll_path, READ_OP) as read_ctx:
305
309
  db_time_mode = read_ctx.read_data(
@@ -344,9 +348,12 @@ class ALDBEntryImpl(DBEntryImpl):
344
348
  ll_path += f"/{occurrence}"
345
349
  ids = self._ids_factory.new(ids_name)
346
350
  with self._db_ctx.global_action(ll_path, WRITE_OP) as write_ctx:
347
- delete_children(ids.metadata, write_ctx, "")
351
+ delete_children(ids.metadata, write_ctx)
348
352
 
349
353
  def list_all_occurrences(self, ids_name: str) -> List[int]:
354
+ if self._db_ctx is None:
355
+ raise RuntimeError("Database entry is not open.")
356
+
350
357
  try:
351
358
  occurrence_list = self._db_ctx.list_all_occurrences(ids_name)
352
359
  except LLInterfaceError:
@@ -357,6 +364,19 @@ class ALDBEntryImpl(DBEntryImpl):
357
364
  ) from None
358
365
  return occurrence_list
359
366
 
367
+ def list_filled_paths(self, ids_name: str, occurrence: int) -> List[str]:
368
+ if self._db_ctx is None:
369
+ raise RuntimeError("Database entry is not open.")
370
+ ll_path = ids_name
371
+ if occurrence != 0:
372
+ ll_path += f"/{occurrence}"
373
+ paths = self._db_ctx.list_filled_paths(ll_path)
374
+ if not paths:
375
+ raise DataEntryException(
376
+ f"IDS {ids_name!r}, occurrence {occurrence} is empty."
377
+ )
378
+ return paths
379
+
360
380
  def _check_uda_warnings(self, lazy: bool) -> None:
361
381
  """Various checks / warnings for the UDA backend."""
362
382
  cache_mode = self._querydict.get("cache_mode")
@@ -12,30 +12,17 @@ import logging
12
12
 
13
13
  from packaging.version import Version
14
14
 
15
- logger = logging.getLogger(__name__)
15
+ # Import the Access Layer module
16
+ # First try to import imas_core, which is available since AL 5.2
17
+ from imas_core import _al_lowlevel as lowlevel
18
+ from imas_core import imasdef # noqa: F401
16
19
 
20
+ logger = logging.getLogger(__name__)
17
21
 
18
- # Import the Access Layer module
19
- has_imas = True
20
- try:
21
- # First try to import imas_core, which is available since AL 5.2
22
- from imas_core import _al_lowlevel as lowlevel
23
- from imas_core import imasdef
24
-
25
- # Enable throwing exceptions from the _al_lowlevel interface
26
- enable_exceptions = getattr(lowlevel, "imas_core_config_enable_exceptions", None)
27
- if enable_exceptions:
28
- enable_exceptions()
29
-
30
- except ImportError as exc:
31
- imas = None
32
- has_imas = False
33
- imasdef = None
34
- lowlevel = None
35
- logger.warning(
36
- "Could not import 'imas_core': %s. Some functionality is not available.",
37
- exc,
38
- )
22
+ # Enable throwing exceptions from the _al_lowlevel interface
23
+ enable_exceptions = getattr(lowlevel, "imas_core_config_enable_exceptions", None)
24
+ if enable_exceptions:
25
+ enable_exceptions()
39
26
 
40
27
 
41
28
  class LLInterfaceError(RuntimeError):
@@ -179,6 +166,11 @@ class LowlevelInterface:
179
166
  ):
180
167
  raise self._minimal_version("5.4")
181
168
 
169
+ # New method in AL 5.7
170
+
171
+ def list_filled_paths(self, ctx, path):
172
+ raise self._minimal_version("5.7")
173
+
182
174
 
183
175
  # Dummy documentation for interface:
184
176
  for funcname in dir(LowlevelInterface):
@@ -14,6 +14,7 @@ import uuid
14
14
  from pathlib import Path
15
15
  from subprocess import CalledProcessError, check_output
16
16
  from zlib import crc32
17
+ from packaging.version import Version
17
18
 
18
19
  try:
19
20
  from importlib.resources import as_file, files
@@ -24,6 +25,8 @@ from imas.dd_zip import get_dd_xml, get_dd_xml_crc
24
25
  from imas.exception import MDSPlusModelError
25
26
  from imas.ids_factory import IDSFactory
26
27
 
28
+ from .imas_interface import ll_interface
29
+
27
30
  logger = logging.getLogger(__name__)
28
31
 
29
32
 
@@ -292,6 +295,20 @@ def create_model_ids_xml(cache_dir_path, fname, version):
292
295
  raise e
293
296
 
294
297
 
298
+ def get_mdsplus_model_var() -> str:
299
+ """
300
+ Return the environemnt variable name used by IMAS-Core to locate models:
301
+
302
+ - 'ids_path' for IMAS-Core<5.6
303
+ - 'MDSPLUS_MODELS_PATH' for IMAS-Core>=5.6
304
+ """
305
+ return (
306
+ "ids_path"
307
+ if ll_interface._al_version < Version("5.6.0")
308
+ else "MDSPLUS_MODELS_PATH"
309
+ )
310
+
311
+
295
312
  def create_mdsplus_model(cache_dir_path: Path) -> None:
296
313
  """Use jtraverser to compile a valid MDS model file."""
297
314
  try:
@@ -322,7 +339,7 @@ def create_mdsplus_model(cache_dir_path: Path) -> None:
322
339
  env={
323
340
  "PATH": os.environ.get("PATH", ""),
324
341
  "LD_LIBRARY_PATH": os.environ.get("LD_LIBRARY_PATH", ""),
325
- "ids_path": str(cache_dir_path),
342
+ get_mdsplus_model_var(): str(cache_dir_path),
326
343
  },
327
344
  )
328
345
  # Touch a file to show that we have finished the model
@@ -349,7 +366,7 @@ def jTraverser_jar() -> Path:
349
366
  for component in os.environ.get("CLASSPATH", "").split(":"):
350
367
  if component.endswith(".jar"):
351
368
  if re.search(".*jTraverser.jar", component):
352
- return component
369
+ return Path(component)
353
370
  else: # assume its a directory (strip any '*' suffix)
354
371
  search_dirs.append(component.rstrip("*"))
355
372
 
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from pathlib import Path
3
- from typing import Union
3
+ from typing import Union, Optional
4
4
  from xml.etree import ElementTree as ET
5
5
 
6
6
  from imas import dd_zip
@@ -10,7 +10,7 @@ from .mdsplus_model import _get_xdg_cache_dir
10
10
  logger = logging.getLogger(__name__)
11
11
 
12
12
 
13
- def get_dd_version_from_idsdef_xml(path: Union[str, Path]) -> str:
13
+ def get_dd_version_from_idsdef_xml(path: Union[str, Path]) -> Optional[str]:
14
14
  """Parse the IDSDef.xml up to the point where the Data Dictionary version is set.
15
15
 
16
16
  Returns:
@@ -92,6 +92,14 @@ class NCDBEntryImpl(DBEntryImpl):
92
92
  )
93
93
  self._dataset.close()
94
94
 
95
+ def _get_group(self, ids_name: str, occurrence: int) -> "netCDF4.Group":
96
+ try:
97
+ return self._dataset[f"{ids_name}/{occurrence}"]
98
+ except LookupError as exc:
99
+ raise DataEntryException(
100
+ f"IDS {ids_name!r}, occurrence {occurrence} is not found."
101
+ ) from exc
102
+
95
103
  def get(
96
104
  self,
97
105
  ids_name: str,
@@ -100,7 +108,7 @@ class NCDBEntryImpl(DBEntryImpl):
100
108
  destination: IDSToplevel,
101
109
  lazy: bool,
102
110
  nbc_map: Optional[NBCPathMap],
103
- ) -> None:
111
+ ) -> IDSToplevel:
104
112
  # Feature compatibility checks
105
113
  if parameters is not None:
106
114
  if isinstance(parameters, GetSliceParameters):
@@ -110,12 +118,7 @@ class NCDBEntryImpl(DBEntryImpl):
110
118
  raise NotImplementedError(f"`{func}` is not available for netCDF files.")
111
119
 
112
120
  # Check if the IDS/occurrence exists, and obtain the group it is stored in
113
- try:
114
- group = self._dataset[f"{ids_name}/{occurrence}"]
115
- except KeyError:
116
- raise DataEntryException(
117
- f"IDS {ids_name!r}, occurrence {occurrence} is not found."
118
- )
121
+ group = self._get_group(ids_name, occurrence)
119
122
 
120
123
  # Load data into the destination IDS
121
124
  if self._ds_factory.dd_version == destination._dd_version:
@@ -183,3 +186,17 @@ class NCDBEntryImpl(DBEntryImpl):
183
186
 
184
187
  occurrence_list.sort()
185
188
  return occurrence_list
189
+
190
+ def list_filled_paths(self, ids_name: str, occurrence: int) -> List[str]:
191
+ # Check if the IDS/occurrence exists, and obtain the group it is stored in
192
+ group = self._get_group(ids_name, occurrence)
193
+
194
+ result = []
195
+ for name, variable in group.variables.items():
196
+ if variable.ndim == 0 and variable.dtype == "S1":
197
+ continue # (Array of) Structure metadata node, no data
198
+ if name.endswith(":shape"):
199
+ continue # Shape data, not a DD path
200
+ result.append(name.replace(".", "/"))
201
+
202
+ return result
@@ -47,7 +47,6 @@ class IDS2NC(IDSTensorizer):
47
47
 
48
48
  def create_variables(self) -> None:
49
49
  """Create netCDF variables."""
50
- get_dimensions = self.ncmeta.get_dimensions
51
50
  for path in self.filled_data:
52
51
  metadata = self.ids.metadata[path]
53
52
  var_name = path.replace("/", ".")
@@ -75,54 +74,21 @@ class IDS2NC(IDSTensorizer):
75
74
  if dtype is not dtypes[IDSDataType.CPX]: # Set fillvalue
76
75
  kwargs.update(fill_value=default_fillvals[metadata.data_type])
77
76
  # Create variable
78
- dimensions = get_dimensions(path, self.homogeneous_time)
77
+ dimensions = self.get_dimensions(path)
79
78
  var = self.group.createVariable(var_name, dtype, dimensions, **kwargs)
80
79
 
81
80
  # Fill metadata attributes
82
- var.documentation = metadata.documentation
83
- if metadata.units:
84
- var.units = metadata.units
85
-
86
- ancillary_variables = " ".join(
87
- error_var
88
- for error_var in [f"{var_name}_error_upper", f"{var_name}_error_lower"]
89
- if error_var in self.filled_variables
90
- )
91
- if ancillary_variables:
92
- var.ancillary_variables = ancillary_variables
93
-
94
- if metadata.data_type is not IDSDataType.STRUCT_ARRAY:
95
- coordinates = self.filter_coordinates(path)
96
- if coordinates:
97
- var.coordinates = coordinates
98
-
99
- # Sparsity and :shape array
100
- if path in self.shapes:
101
- if not metadata.ndim:
102
- # Doesn't need a :shape array:
103
- var.sparse = "Sparse data, missing data is filled with _FillValue"
104
- var.sparse += f" ({default_fillvals[metadata.data_type]})"
105
-
106
- else:
107
- shape_name = f"{var_name}:shape"
108
- var.sparse = f"Sparse data, data shapes are stored in {shape_name}"
109
-
110
- # Create variable to store data shape
111
- dimensions = get_dimensions(
112
- self.ncmeta.aos.get(path), self.homogeneous_time
113
- ) + (f"{metadata.ndim}D",)
114
- shape_var = self.group.createVariable(
115
- shape_name,
116
- SHAPE_DTYPE,
117
- dimensions,
118
- )
119
- doc_indices = ",".join(chr(ord("i") + i) for i in range(3))
120
- shape_var.documentation = (
121
- f"Shape information for {var_name}.\n"
122
- f"{shape_name}[{doc_indices},:] describes the shape of filled "
123
- f"data of {var_name}[{doc_indices},...]. Data outside this "
124
- "shape is unset (i.e. filled with _Fillvalue)."
125
- )
81
+ var.setncatts(self.get_attributes(path, default_fillvals))
82
+
83
+ # :shape array for sparse data
84
+ if path in self.shapes and metadata.ndim:
85
+ shape_name = f"{var_name}:shape"
86
+ # Create variable to store data shape
87
+ dimensions = self.get_shape_dimensions(path)
88
+ shape_var = self.group.createVariable(
89
+ shape_name, SHAPE_DTYPE, dimensions
90
+ )
91
+ shape_var.setncatts(self.get_shape_attributes(var_name))
126
92
 
127
93
  def store_data(self) -> None:
128
94
  """Store data in the netCDF variables"""