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
imas/db_entry.py ADDED
@@ -0,0 +1,802 @@
1
+ # This file is part of imas-python.
2
+ # You should have received the imas-python LICENSE file with this project.
3
+ """Logic for interacting with IMAS Data Entries.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ from typing import Any, List, Optional, Tuple, Type, Union, overload
9
+
10
+ import numpy
11
+
12
+ import imas
13
+ from imas.backends.db_entry_impl import (
14
+ DBEntryImpl,
15
+ GetSampleParameters,
16
+ GetSliceParameters,
17
+ )
18
+ from imas.dd_zip import dd_xml_versions
19
+ from imas.exception import IDSNameError, UnknownDDVersion, ValidationError
20
+ from imas.ids_base import IDSBase
21
+ from imas.ids_convert import dd_version_map_from_factories
22
+ from imas.ids_defs import (
23
+ CREATE_PULSE,
24
+ FORCE_CREATE_PULSE,
25
+ FORCE_OPEN_PULSE,
26
+ IDS_TIME_MODE_INDEPENDENT,
27
+ IDS_TIME_MODES,
28
+ OPEN_PULSE,
29
+ )
30
+ from imas.ids_factory import IDSFactory
31
+ from imas.ids_metadata import IDSType
32
+ from imas.ids_toplevel import IDSToplevel
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ def _get_uri_mode(uri, mode) -> Tuple[str, str]:
38
+ """Helper method to parse arguments of DBEntry.__init__."""
39
+ return uri, mode
40
+
41
+
42
+ def _get_legacy_params(
43
+ backend_id, db_name, pulse, run, user_name=None, data_version=None
44
+ ) -> Tuple[int, str, int, int, Optional[str], Optional[str]]:
45
+ """Helper method to parse arguments of DBEntry.__init__."""
46
+ return backend_id, db_name, pulse, run, user_name, data_version
47
+
48
+
49
+ class DBEntry:
50
+ """Represents an IMAS database entry, which is a collection of stored IDSs.
51
+
52
+ A ``DBEntry`` can be used as a :external:ref:`context manager <context-managers>`:
53
+
54
+ .. code-block:: python
55
+
56
+ import imas
57
+
58
+ # AL4-style constructor:
59
+ with imas.DBEntry(imas.ids_defs.HDF5_BACKEND, "test", 1, 12) as dbentry:
60
+ # dbentry is now opened and can be used for reading data:
61
+ ids = dbentry.get(...)
62
+ # The dbentry is now closed
63
+
64
+ # AL5-style constructor also allows creating the Data Entry with the mode
65
+ # argument
66
+ with imas.DBEntry("imas:hdf5?path=testdb", "w") as dbentry:
67
+ # dbentry is now created and can be used for writing data:
68
+ dbentry.put(ids)
69
+ # The dbentry is now closed
70
+ """
71
+
72
+ @overload
73
+ def __init__(
74
+ self,
75
+ uri: str,
76
+ mode: str,
77
+ *,
78
+ dd_version: Optional[str] = None,
79
+ xml_path: Optional[str] = None,
80
+ ) -> None: ...
81
+
82
+ @overload
83
+ def __init__(
84
+ self,
85
+ backend_id: int,
86
+ db_name: str,
87
+ pulse: int,
88
+ run: int,
89
+ user_name: Optional[str] = None,
90
+ data_version: Optional[str] = None,
91
+ *,
92
+ shot: Optional[int] = None,
93
+ dd_version: Optional[str] = None,
94
+ xml_path: Optional[str] = None,
95
+ ) -> None: ...
96
+
97
+ def __init__(
98
+ self,
99
+ *args,
100
+ dd_version: Optional[str] = None,
101
+ xml_path: Optional[str] = None,
102
+ **kwargs,
103
+ ):
104
+ """Open or create a Data Entry based on the provided URI and mode, or prepare a
105
+ DBEntry using `legacy` parameters.
106
+
107
+ Note:
108
+ When using `legacy` parameters (:param:`backend_id`, :param:`db_name`,
109
+ :param:`pulse`, :param:`run`), the DBEntry is not opened.
110
+ You have to call :meth:`open` or :meth:`create` after creating the DBEntry
111
+ object before you can use it for reading or writing data.
112
+
113
+ Args:
114
+ uri: URI to the data entry, see explanation above.
115
+ mode: Mode to open the Data Entry in:
116
+
117
+ - ``"r"``: Open an existing data entry. Raises an error when the data
118
+ entry does not exist.
119
+
120
+ .. note:: The opened data entry is not read-only, it can be written to.
121
+ - ``"a"``: Open an existing data entry, create the data entry if it does
122
+ not exist.
123
+ - ``"w"``: Create a data entry, overwriting any existing.
124
+
125
+ .. caution:: This will irreversibly delete any existing data.
126
+ - ``"x"``: Create a data entry. Raises an error when a data entry already
127
+ exists.
128
+
129
+ backend_id: ID of the backend to use. See :ref:`Backend identifiers`.
130
+ db_name: Database name, e.g. "ITER".
131
+ pulse: Pulse number of the database entry.
132
+ run: Run number of the database entry.
133
+ user_name: User name of the database, retrieved from environment when not
134
+ supplied.
135
+ data_version: Major version of the DD used by the the access layer.
136
+
137
+ Keyword Args:
138
+ shot: Legacy alternative for :param:`pulse`.
139
+ dd_version: Use a specific Data Dictionary version instead of the default
140
+ one. See :ref:`multi-dd training`.
141
+ xml_path: Use a specific Data Dictionary build by pointing to the
142
+ IDSDef.xml. See :ref:`Using custom builds of the Data Dictionary`.
143
+ """
144
+ try:
145
+ # Try to map *args and **kwargs to (uri, mode)
146
+ uri, mode = _get_uri_mode(*args, **kwargs)
147
+ legacy = False
148
+
149
+ except TypeError as exc1:
150
+ # map legacy `shot` to `pulse`
151
+ if "shot" in kwargs:
152
+ if "pulse" in kwargs:
153
+ raise ValueError("Cannot provide a value for both shot and pulse")
154
+ kwargs["pulse"] = kwargs.pop("shot")
155
+ # Try to map *args and **kwargs to legacy call pattern
156
+ try:
157
+ legacy_params = _get_legacy_params(*args, **kwargs)
158
+ legacy = True
159
+ except TypeError as exc2:
160
+ raise TypeError(
161
+ f"Incorrect arguments to {__class__.__name__}.__init__(): "
162
+ f"{exc1.args[0]}, {exc2.args[0]}"
163
+ ) from None
164
+
165
+ # Actual intializiation
166
+ self._dbe_impl: Optional[DBEntryImpl] = None
167
+ self._dd_version = dd_version
168
+ self._xml_path = xml_path
169
+ self._ids_factory = IDSFactory(dd_version, xml_path)
170
+
171
+ if legacy:
172
+ # Unpack legacy params
173
+ (
174
+ self.backend_id,
175
+ self.db_name,
176
+ self.pulse,
177
+ self.run,
178
+ self.user_name,
179
+ self.data_version,
180
+ ) = legacy_params
181
+ self.uri = None
182
+ self.mode = None
183
+ else:
184
+ self.uri = str(uri)
185
+ self.mode = mode
186
+ cls = self._select_implementation(self.uri)
187
+ self._dbe_impl = cls.from_uri(self.uri, mode, self._ids_factory)
188
+
189
+ @staticmethod
190
+ def _select_implementation(uri: Optional[str]) -> Type[DBEntryImpl]:
191
+ """Select which DBEntry implementation to use based on the URI."""
192
+ if uri and uri.endswith(".nc") and not uri.startswith("imas:"):
193
+ from imas.backends.netcdf.db_entry_nc import NCDBEntryImpl as impl
194
+ else:
195
+ from imas.backends.imas_core.db_entry_al import ALDBEntryImpl as impl
196
+ return impl
197
+
198
+ def __enter__(self):
199
+ # Context manager protocol
200
+ if self._dbe_impl is None:
201
+ # Open if the DBEntry was not already opened or created
202
+ self.open()
203
+ return self
204
+
205
+ def __exit__(self, exc_type, exc_value, traceback):
206
+ # Context manager protocol
207
+ self.close()
208
+
209
+ @property
210
+ def factory(self) -> IDSFactory:
211
+ """Get the IDS factory used by this DB entry."""
212
+ return self._ids_factory
213
+
214
+ @property
215
+ def dd_version(self) -> str:
216
+ """Get the DD version used by this DB entry"""
217
+ return self._ids_factory.version
218
+
219
+ def close(self, *, erase=False):
220
+ """Close this Database Entry.
221
+
222
+ Keyword Args:
223
+ erase: Remove the pulse file from the database. Note: this parameter may be
224
+ ignored by the backend. It is best to not use it.
225
+ """
226
+ if self._dbe_impl is None:
227
+ return
228
+ self._dbe_impl.close(erase=erase)
229
+ self._dbe_impl = None
230
+
231
+ def create(self, *, options=None, force=True) -> None:
232
+ """Create a new database entry.
233
+
234
+ This method may not be called when using the URI constructor of DBEntry.
235
+
236
+ Caution:
237
+ This method erases the previous entry if it existed!
238
+
239
+ Keyword Args:
240
+ options: Backend specific options.
241
+ force: Whether to force create the database entry.
242
+
243
+ Example:
244
+ .. code-block:: python
245
+
246
+ import imas
247
+ from imas.ids_defs import HDF5_BACKEND
248
+
249
+ imas_entry = imas.DBEntry(HDF5_BACKEND, "test", 1, 1234)
250
+ imas_entry.create()
251
+ """
252
+ self._open_pulse(FORCE_CREATE_PULSE if force else CREATE_PULSE, options)
253
+
254
+ def open(self, mode=OPEN_PULSE, *, options=None, force=False) -> None:
255
+ """Open an existing database entry.
256
+
257
+ This method may not be called when using the URI constructor of DBEntry.
258
+
259
+ Keyword Args:
260
+ options: Backend specific options.
261
+ force: Whether to force open the database entry.
262
+
263
+ Example:
264
+ .. code-block:: python
265
+
266
+ import imas
267
+ from imas.ids_defs import HDF5_BACKEND
268
+
269
+ imas_entry = imas.DBEntry(HDF5_BACKEND, "test", 1, 1234)
270
+ imas_entry.open()
271
+ """
272
+ if force:
273
+ mode = FORCE_OPEN_PULSE
274
+ logger.warning(
275
+ "DBEntry.open(force=True) is deprecated, "
276
+ "use DBEntry.open(FORCE_OPEN_PULSE) instead"
277
+ )
278
+ self._open_pulse(mode, options)
279
+
280
+ def _open_pulse(self, mode: int, options: Any) -> None:
281
+ """Internal method implementing open()/create()."""
282
+ if self._dbe_impl is not None:
283
+ raise RuntimeError("This DBEntry is already open")
284
+ if self.uri is not None:
285
+ raise RuntimeError(
286
+ "This DBEntry was opened using an URI: "
287
+ "DBEntry.open/create is not available."
288
+ )
289
+
290
+ cls = self._select_implementation(self.uri)
291
+ self._dbe_impl = cls.from_pulse_run(
292
+ self.backend_id,
293
+ self.db_name,
294
+ self.pulse,
295
+ self.run,
296
+ self.user_name,
297
+ self.data_version,
298
+ mode,
299
+ options,
300
+ self._ids_factory,
301
+ )
302
+
303
+ def get(
304
+ self,
305
+ ids_name: str,
306
+ occurrence: int = 0,
307
+ *,
308
+ lazy: bool = False,
309
+ autoconvert: bool = True,
310
+ ignore_unknown_dd_version: bool = False,
311
+ destination: Optional[IDSToplevel] = None,
312
+ ) -> IDSToplevel:
313
+ """Read the contents of an IDS into memory.
314
+
315
+ This method fetches an IDS in its entirety, with all time slices it may contain.
316
+ See :meth:`get_slice` for reading a specific time slice.
317
+
318
+ Args:
319
+ ids_name: Name of the IDS to read from the backend.
320
+ occurrence: Which occurrence of the IDS to read.
321
+
322
+ Keyword Args:
323
+ lazy: When set to ``True``, values in this IDS will be retrieved only when
324
+ needed (instead of getting the full IDS immediately). See :ref:`Lazy
325
+ loading` for more details.
326
+
327
+ .. note:: Lazy loading is not supported by the ASCII backend.
328
+ autoconvert: Automatically convert IDSs.
329
+
330
+ If enabled (default), a call to ``get()`` or ``get_slice()`` will return
331
+ an IDS from the Data Dictionary version attached to this Data Entry.
332
+ Data is automatically converted between the on-disk version and the
333
+ in-memory version.
334
+
335
+ When set to ``False``, the IDS will be returned in the DD version it was
336
+ stored in.
337
+ ignore_unknown_dd_version: When an IDS is stored with an unknown DD version,
338
+ do not attempt automatic conversion and fetch the data in the Data
339
+ Dictionary version attached to this Data Entry.
340
+ destination: Populate this IDSToplevel instead of creating an empty one.
341
+
342
+ Returns:
343
+ The loaded IDS.
344
+
345
+ Example:
346
+ .. code-block:: python
347
+
348
+ import imas
349
+
350
+ imas_entry = imas.DBEntry(imas.ids_defs.MDSPLUS_BACKEND, "ITER", 131024, 41, "public")
351
+ imas_entry.open()
352
+ core_profiles = imas_entry.get("core_profiles")
353
+ """ # noqa
354
+ return self._get(
355
+ ids_name,
356
+ occurrence,
357
+ None,
358
+ destination,
359
+ lazy,
360
+ autoconvert,
361
+ ignore_unknown_dd_version,
362
+ )
363
+
364
+ def get_slice(
365
+ self,
366
+ ids_name: str,
367
+ time_requested: float,
368
+ interpolation_method: int,
369
+ occurrence: int = 0,
370
+ *,
371
+ lazy: bool = False,
372
+ autoconvert: bool = True,
373
+ ignore_unknown_dd_version: bool = False,
374
+ destination: Optional[IDSToplevel] = None,
375
+ ) -> IDSToplevel:
376
+ """Read a single time slice from an IDS in this Database Entry.
377
+
378
+ This method returns an IDS object with all constant/static data filled. The
379
+ dynamic data is interpolated on the requested time slice. This means that the
380
+ size of the time dimension in the returned data is 1.
381
+
382
+ Args:
383
+ ids_name: Name of the IDS to read from the backend.
384
+ time_requested: Requested time slice
385
+ interpolation_method: Interpolation method to use. Available options:
386
+
387
+ - :const:`~imas.ids_defs.CLOSEST_INTERP`
388
+ - :const:`~imas.ids_defs.PREVIOUS_INTERP`
389
+ - :const:`~imas.ids_defs.LINEAR_INTERP`
390
+
391
+ occurrence: Which occurrence of the IDS to read.
392
+
393
+ Keyword Args:
394
+ lazy: When set to ``True``, values in this IDS will be retrieved only when
395
+ needed (instead of getting the full IDS immediately). See :ref:`Lazy
396
+ loading` for more details.
397
+ autoconvert: Automatically convert IDSs.
398
+
399
+ If enabled (default), a call to ``get()`` or ``get_slice()`` will return
400
+ an IDS from the Data Dictionary version attached to this Data Entry.
401
+ Data is automatically converted between the on-disk version and the
402
+ in-memory version.
403
+
404
+ When set to ``False``, the IDS will be returned in the DD version it was
405
+ stored in.
406
+ ignore_unknown_dd_version: When an IDS is stored with an unknown DD version,
407
+ do not attempt automatic conversion and fetch the data in the Data
408
+ Dictionary version attached to this Data Entry.
409
+ destination: Populate this IDSToplevel instead of creating an empty one.
410
+
411
+ Returns:
412
+ The loaded IDS.
413
+
414
+ Example:
415
+ .. code-block:: python
416
+
417
+ import imas
418
+
419
+ imas_entry = imas.DBEntry(imas.ids_defs.MDSPLUS_BACKEND, "ITER", 131024, 41, "public")
420
+ imas_entry.open()
421
+ core_profiles = imas_entry.get_slice("core_profiles", 370, imas.ids_defs.PREVIOUS_INTERP)
422
+ """ # noqa
423
+ return self._get(
424
+ ids_name,
425
+ occurrence,
426
+ GetSliceParameters(time_requested, interpolation_method),
427
+ destination,
428
+ lazy,
429
+ autoconvert,
430
+ ignore_unknown_dd_version,
431
+ )
432
+
433
+ def get_sample(
434
+ self,
435
+ ids_name: str,
436
+ tmin: float,
437
+ tmax: float,
438
+ dtime: Optional[Union[float, numpy.ndarray]] = None,
439
+ interpolation_method: Optional[int] = None,
440
+ occurrence: int = 0,
441
+ *,
442
+ lazy: bool = False,
443
+ autoconvert: bool = True,
444
+ ignore_unknown_dd_version: bool = False,
445
+ destination: Optional[IDSToplevel] = None,
446
+ ) -> IDSToplevel:
447
+ """Read a range of time slices from an IDS in this Database Entry.
448
+
449
+ This method has three different modes, depending on the provided arguments:
450
+
451
+ 1. No interpolation. This method is selected when :param:`dtime` and
452
+ :param:`interpolation_method` are not provided.
453
+
454
+ This mode returns an IDS object with all constant/static data filled. The
455
+ dynamic data is retrieved for the provided time range [tmin, tmax].
456
+
457
+ 2. Interpolate dynamic data on a uniform time base. This method is selected
458
+ when :param:`dtime` and :param:`interpolation_method` are provided.
459
+ :param:`dtime` must be a number or a numpy array of size 1.
460
+
461
+ This mode will generate an IDS with a homogeneous time vector ``[tmin, tmin
462
+ + dtime, tmin + 2*dtime, ...`` up to ``tmax``. The chosen interpolation
463
+ method will have no effect on the time vector, but may have an impact on the
464
+ other dynamic values. The returned IDS always has
465
+ ``ids_properties.homogeneous_time = 1``.
466
+
467
+ 3. Interpolate dynamic data on an explicit time base. This method is selected
468
+ when :param:`dtime` and :param:`interpolation_method` are provided.
469
+ :param:`dtime` must be a numpy array of size larger than 1.
470
+
471
+ This mode will generate an IDS with a homogeneous time vector equal to
472
+ :param:`dtime`. :param:`tmin` and :param:`tmax` are ignored in this mode.
473
+ The chosen interpolation method will have no effect on the time vector, but
474
+ may have an impact on the other dynamic values. The returned IDS always has
475
+ ``ids_properties.homogeneous_time = 1``.
476
+
477
+ Args:
478
+ ids_name: Name of the IDS to read from the backend
479
+ tmin: Lower bound of the requested time range
480
+ tmax: Upper bound of the requested time range, must be larger than or
481
+ equal to :param:`tmin`
482
+ dtime: Interval to use when interpolating, must be positive, or numpy array
483
+ containing an explicit time base to interpolate.
484
+ interpolation_method: Interpolation method to use. Available options:
485
+
486
+ - :const:`~imas.ids_defs.CLOSEST_INTERP`
487
+ - :const:`~imas.ids_defs.PREVIOUS_INTERP`
488
+ - :const:`~imas.ids_defs.LINEAR_INTERP`
489
+
490
+ occurrence: Which occurrence of the IDS to read.
491
+
492
+ Keyword Args:
493
+ lazy: When set to ``True``, values in this IDS will be retrieved only when
494
+ needed (instead of getting the full IDS immediately). See :ref:`Lazy
495
+ loading` for more details.
496
+ autoconvert: Automatically convert IDSs.
497
+
498
+ If enabled (default), a call to ``get_sample()`` will return
499
+ an IDS from the Data Dictionary version attached to this Data Entry.
500
+ Data is automatically converted between the on-disk version and the
501
+ in-memory version.
502
+
503
+ When set to ``False``, the IDS will be returned in the DD version it was
504
+ stored in.
505
+ ignore_unknown_dd_version: When an IDS is stored with an unknown DD version,
506
+ do not attempt automatic conversion and fetch the data in the Data
507
+ Dictionary version attached to this Data Entry.
508
+ destination: Populate this IDSToplevel instead of creating an empty one.
509
+
510
+ Returns:
511
+ The loaded IDS.
512
+
513
+ Example:
514
+ .. code-block:: python
515
+
516
+ import imas
517
+ import numpy
518
+ from imas import ids_defs
519
+
520
+ imas_entry = imas.DBEntry(
521
+ "imas:mdsplus?user=public;pulse=131024;run=41;database=ITER", "r")
522
+
523
+ # All time slices between t=200 and t=370
524
+ core_profiles = imas_entry.get_sample("core_profiles", 200, 370)
525
+
526
+ # Closest points to [0, 100, 200, ..., 1000]
527
+ core_profiles_interp = imas_entry.get_sample(
528
+ "core_profiles", 0, 1000, 100, ids_defs.CLOSEST_INTERP)
529
+
530
+ # Linear interpolation for [10, 11, 12, 14, 16, 20, 30, 40, 50]
531
+ times = numpy.array([10, 11, 12, 14, 16, 20, 30, 40, 50])
532
+ core_profiles_interp = imas_entry.get_sample(
533
+ "core_profiles", 0, 0, times, ids_defs.LINEAR_INTERP)
534
+ """
535
+ if dtime is not None:
536
+ dtime = numpy.atleast_1d(dtime) # Convert floats and 0D arrays to 1D array
537
+ return self._get(
538
+ ids_name,
539
+ occurrence,
540
+ GetSampleParameters(tmin, tmax, dtime, interpolation_method),
541
+ destination,
542
+ lazy,
543
+ autoconvert,
544
+ ignore_unknown_dd_version,
545
+ )
546
+
547
+ def _get(
548
+ self,
549
+ ids_name: str,
550
+ occurrence: int,
551
+ parameters: Union[None, GetSliceParameters, GetSampleParameters],
552
+ destination: Optional[IDSToplevel],
553
+ lazy: bool,
554
+ autoconvert: bool,
555
+ ignore_unknown_dd_version: bool,
556
+ ) -> IDSToplevel:
557
+ """Actual implementation of get() and get_slice()"""
558
+ if self._dbe_impl is None:
559
+ raise RuntimeError("Database entry is not open.")
560
+ if lazy and destination:
561
+ raise ValueError("Cannot supply a destination IDS when lazy loading.")
562
+ if not self._ids_factory.exists(ids_name):
563
+ raise IDSNameError(ids_name, self._ids_factory)
564
+
565
+ # Note: this will raise an exception when the ids/occurrence is not filled:
566
+ dd_version = self._dbe_impl.read_dd_version(ids_name, occurrence)
567
+
568
+ # DD version sanity checks:
569
+ if not dd_version:
570
+ # No DD version stored in the IDS, load as if it was stored with
571
+ # self.dd_version
572
+ logger.warning(
573
+ "Loaded IDS (%s, occurrence %s) does not specify a data dictionary "
574
+ "version. Some data may not be loaded.",
575
+ ids_name,
576
+ occurrence,
577
+ )
578
+ elif dd_version != self.dd_version and dd_version not in dd_xml_versions():
579
+ # We don't know the DD version that this IDS was written with
580
+ if ignore_unknown_dd_version:
581
+ # User chooses to ignore this problem, load as if it was stored with
582
+ # self.dd_version
583
+ logger.info("Ignoring unknown data dictionary version %s", dd_version)
584
+ dd_version = None
585
+ else:
586
+ note = (
587
+ "\nYou may set the get/get_slice parameter "
588
+ "ignore_unknown_dd_version=True to ignore this and get an IDS in "
589
+ f"the default DD version ({self.dd_version})"
590
+ )
591
+ raise UnknownDDVersion(dd_version, dd_xml_versions(), note)
592
+
593
+ # Version conversion:
594
+ if not destination:
595
+ # Construct IDS object that the backend can store data in
596
+ if autoconvert or not dd_version:
597
+ # Store results in our DD version
598
+ destination = self._ids_factory.new(ids_name, _lazy=lazy)
599
+ else:
600
+ # Store results in the on-disk version
601
+ destination = IDSFactory(dd_version).new(ids_name, _lazy=lazy)
602
+
603
+ nbc_map = None
604
+ if dd_version and dd_version != destination._dd_version:
605
+ if dd_version.split(".")[0] != destination._dd_version.split(".")[0]:
606
+ logger.warning(
607
+ "On-disk data is stored in DD %s which has a different major "
608
+ "version than the requested DD version (%s). imas-python will "
609
+ "convert the data automatically, but this does not cover all"
610
+ "changes. "
611
+ "See %s/multi-dd.html#conversion-of-idss-between-dd-versions",
612
+ dd_version,
613
+ destination._dd_version,
614
+ imas.PUBLISHED_DOCUMENTATION_ROOT,
615
+ )
616
+ ddmap, source_is_older = dd_version_map_from_factories(
617
+ ids_name, IDSFactory(version=dd_version), self._ids_factory
618
+ )
619
+ nbc_map = ddmap.new_to_old if source_is_older else ddmap.old_to_new
620
+
621
+ # Pass on to the DBEntry implementation:
622
+ return self._dbe_impl.get(
623
+ ids_name,
624
+ occurrence,
625
+ parameters,
626
+ destination,
627
+ lazy,
628
+ nbc_map,
629
+ )
630
+
631
+ def put(self, ids: IDSToplevel, occurrence: int = 0) -> None:
632
+ """Write the contents of an IDS into this Database Entry.
633
+
634
+ The IDS is written entirely, with all time slices it may contain.
635
+
636
+ Caution:
637
+ The put method deletes any previously existing data within the target IDS
638
+ occurrence in the Database Entry.
639
+
640
+ Args:
641
+ ids: IDS object to put.
642
+ occurrence: Which occurrence of the IDS to write to.
643
+
644
+ Example:
645
+ .. code-block:: python
646
+
647
+ ids = imas.IDSFactory().pf_active()
648
+ ... # fill the pf_active IDS here
649
+ imas_entry.put(ids)
650
+ """
651
+ self._put(ids, occurrence, False)
652
+
653
+ def put_slice(self, ids: IDSToplevel, occurrence: int = 0) -> None:
654
+ """Append a time slice of the provided IDS to the Database Entry.
655
+
656
+ Time slices must be appended in strictly increasing time order, since the Access
657
+ Layer is not reordering time arrays. Doing otherwise will result in
658
+ non-monotonic time arrays, which will create confusion and make subsequent
659
+ :meth:`get_slice` commands to fail.
660
+
661
+ Although being put progressively time slice by time slice, the final IDS must be
662
+ compliant with the data dictionary. A typical error when constructing IDS
663
+ variables time slice by time slice is to change the size of the IDS fields
664
+ during the time loop, which is not allowed but for the children of an array of
665
+ structure which has time as its coordinate.
666
+
667
+ The :meth:`put_slice` command is appending data, so does not modify previously
668
+ existing data within the target IDS occurrence in the Data Entry.
669
+
670
+ It is possible possible to append several time slices to a node of the IDS in
671
+ one :meth:`put_slice` call, however the user must ensure that the size of the
672
+ time dimension of the node remains consistent with the size of its timebase.
673
+
674
+ Args:
675
+ ids: IDS object to put.
676
+ occurrence: Which occurrence of the IDS to write to.
677
+
678
+ Example:
679
+ A frequent use case is storing IMAS data progressively in a time loop. You
680
+ can fill the constant and static values only once and progressively append
681
+ the dynamic values calculated in each step of the time loop with
682
+ :meth:`put_slice`.
683
+
684
+ .. code-block:: python
685
+
686
+ ids = imas.IDSFactory().pf_active() ... # fill the static data of the
687
+ pf_active IDS here for i in range(N):
688
+ ... # fill time slice of the pf_active IDS imas_entry.put_slice(ids)
689
+ """
690
+ self._put(ids, occurrence, True)
691
+
692
+ def _put(self, ids: IDSToplevel, occurrence: int, is_slice: bool):
693
+ """Actual implementation of put() and put_slice()"""
694
+ if self._dbe_impl is None:
695
+ raise RuntimeError("Database entry is not open.")
696
+ if ids._lazy:
697
+ raise ValueError("Lazy loaded IDSs cannot be used in put or put_slice.")
698
+
699
+ # Automatic validation
700
+ disable_validate = os.environ.get("IMAS_AL_DISABLE_VALIDATE")
701
+ if not disable_validate or disable_validate == "0":
702
+ try:
703
+ ids.validate()
704
+ except ValidationError:
705
+ logger.error(
706
+ "IDS %s is not valid. You can disable automatic IDS validation by "
707
+ "setting the environment variable IMAS_AL_DISABLE_VALIDATE=1.",
708
+ ids.metadata.name,
709
+ )
710
+ raise
711
+
712
+ ids_name = ids.metadata.name
713
+ # Verify homogeneous_time is set
714
+ time_mode = ids.ids_properties.homogeneous_time
715
+ # TODO: allow unset homogeneous_time and quit with no action?
716
+ if time_mode not in IDS_TIME_MODES:
717
+ raise ValueError("'ids_properties.homogeneous_time' is not set or invalid.")
718
+ # IMAS-3330: automatically set time mode to independent:
719
+ if ids.metadata.type is IDSType.CONSTANT:
720
+ if time_mode != IDS_TIME_MODE_INDEPENDENT:
721
+ logger.warning(
722
+ "ids_properties/homogeneous_time has been set to 2 for the constant"
723
+ " IDS %s/%d. Please check the program which has filled this IDS"
724
+ " since this is the mandatory value for a constant IDS",
725
+ ids_name,
726
+ occurrence,
727
+ )
728
+ ids.ids_properties.homogeneous_time = IDS_TIME_MODE_INDEPENDENT
729
+ if is_slice and time_mode == IDS_TIME_MODE_INDEPENDENT:
730
+ raise RuntimeError("Cannot use put_slice with IDS_TIME_MODE_INDEPENDENT.")
731
+
732
+ # Set version_put properties (version_put was added in DD 3.22)
733
+ if hasattr(ids.ids_properties, "version_put"):
734
+ version_put = ids.ids_properties.version_put
735
+ version_put.data_dictionary = self._ids_factory._version
736
+ version_put.access_layer = self._dbe_impl.access_layer_version()
737
+ version_put.access_layer_language = f"imas {imas.__version__}"
738
+
739
+ self._dbe_impl.put(ids, occurrence, is_slice)
740
+
741
+ def delete_data(self, ids_name: str, occurrence: int = 0) -> None:
742
+ """Delete the provided IDS occurrence from this IMAS database entry.
743
+
744
+ Args:
745
+ ids_name: Name of the IDS to delete from the backend.
746
+ occurrence: Which occurrence of the IDS to delete.
747
+ """
748
+ if self._dbe_impl is None:
749
+ raise RuntimeError("Database entry is not open.")
750
+ self._dbe_impl.delete_data(ids_name, occurrence)
751
+
752
+ @overload
753
+ def list_all_occurrences(
754
+ self, ids_name: str, node_path: None = None
755
+ ) -> List[int]: ...
756
+
757
+ @overload
758
+ def list_all_occurrences(
759
+ self, ids_name: str, node_path: str
760
+ ) -> Tuple[List[int], List[IDSBase]]: ...
761
+
762
+ def list_all_occurrences(self, ids_name, node_path=None):
763
+ """List all non-empty occurrences of an IDS
764
+
765
+ Note: this is only available with Access Layer core version 5.1 or newer.
766
+
767
+ Args:
768
+ ids_name: name of the IDS (e.g. "magnetics", "core_profiles" or
769
+ "equilibrium")
770
+ node_path: path to a Data-Dictionary node (e.g. "ids_properties/comment",
771
+ "code/name", "ids_properties/provider").
772
+
773
+ Returns:
774
+ tuple or list:
775
+ When no ``node_path`` is supplied, a (sorted) list with non-empty
776
+ occurrence numbers is returned.
777
+
778
+ When ``node_path`` is supplied, a tuple ``(occurrence_list,
779
+ node_content_list)`` is returned. The ``occurrence_list`` is a (sorted)
780
+ list of non-empty occurrence numbers. The ``node_content_list`` contains
781
+ the contents of the node in the corresponding occurrences.
782
+
783
+ Example:
784
+ .. code-block:: python
785
+
786
+ dbentry = imas.DBEntry(uri, "r")
787
+ occurrence_list, node_content_list = \\
788
+ dbentry.list_all_occurrences("magnetics", "ids_properties/comment")
789
+ dbentry.close()
790
+ """
791
+ if self._dbe_impl is None:
792
+ raise RuntimeError("Database entry is not open.")
793
+
794
+ occurrence_list = self._dbe_impl.list_all_occurrences(ids_name)
795
+
796
+ if node_path is None:
797
+ return occurrence_list
798
+
799
+ node_content_list = [
800
+ self.get(ids_name, occ, lazy=True)[node_path] for occ in occurrence_list
801
+ ]
802
+ return occurrence_list, node_content_list