imas-python 2.0.0__py3-none-any.whl → 2.0.1__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.
imas/__init__.py CHANGED
@@ -20,7 +20,6 @@ from .ids_identifiers import identifiers
20
20
  # Load the IMAS-Python IMAS AL/DD core
21
21
  from . import (
22
22
  db_entry,
23
- dd_helpers,
24
23
  dd_zip,
25
24
  util,
26
25
  )
imas/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.0.0'
21
- __version_tuple__ = version_tuple = (2, 0, 0)
20
+ __version__ = version = '2.0.1'
21
+ __version_tuple__ = version_tuple = (2, 0, 1)
@@ -1,7 +1,6 @@
1
1
  # This file is part of IMAS-Python.
2
2
  # You should have received the IMAS-Python LICENSE file with this project.
3
- """Object-oriented interface to the IMAS lowlevel.
4
- """
3
+ """Object-oriented interface to the IMAS lowlevel."""
5
4
 
6
5
  import logging
7
6
  import weakref
@@ -61,17 +60,21 @@ class ALContext:
61
60
  def __exit__(self, exc_type, exc_value, traceback) -> None:
62
61
  ll_interface.end_action(self.ctx)
63
62
 
64
- def global_action(self, path: str, rwmode: int) -> "ALContext":
63
+ def global_action(self, path: str, rwmode: int, datapath: str = "") -> "ALContext":
65
64
  """Begin a new global action for use in a ``with`` context.
66
65
 
67
66
  Args:
68
67
  path: access layer path for this global action: ``<idsname>[/<occurrence>]``
69
68
  rwmode: read-only or read-write operation mode: ``READ_OP``/``WRITE_OP``
69
+ datapath: used by UDA backend to fetch only part of the data.
70
70
 
71
71
  Returns:
72
72
  The created context.
73
73
  """
74
- status, ctx = ll_interface.begin_global_action(self.ctx, path, rwmode)
74
+ args = [self.ctx, path, rwmode]
75
+ if datapath: # AL4 compatibility: datapath arg was added in AL5
76
+ args.append(datapath)
77
+ status, ctx = ll_interface.begin_global_action(*args)
75
78
  if status != 0:
76
79
  raise LowlevelError("global_action", status)
77
80
  return ALContext(ctx)
@@ -257,7 +257,8 @@ class ALDBEntryImpl(DBEntryImpl):
257
257
  if occurrence != 0:
258
258
  ll_path += f"/{occurrence}"
259
259
 
260
- with self._db_ctx.global_action(ll_path, READ_OP) as read_ctx:
260
+ datapath = "ids_properties" if self.backend == "uda" else ""
261
+ with self._db_ctx.global_action(ll_path, READ_OP, datapath) as read_ctx:
261
262
  time_mode_path = "ids_properties/homogeneous_time"
262
263
  time_mode = read_ctx.read_data(time_mode_path, "", INTEGER_DATA, 0)
263
264
  # This is already checked by read_dd_version, but ensure:
@@ -314,7 +315,8 @@ class ALDBEntryImpl(DBEntryImpl):
314
315
  if occurrence != 0:
315
316
  ll_path += f"/{occurrence}"
316
317
 
317
- with self._db_ctx.global_action(ll_path, READ_OP) as read_ctx:
318
+ datapath = "ids_properties" if self.backend == "uda" else ""
319
+ with self._db_ctx.global_action(ll_path, READ_OP, datapath) as read_ctx:
318
320
  time_mode_path = "ids_properties/homogeneous_time"
319
321
  time_mode = read_ctx.read_data(time_mode_path, "", INTEGER_DATA, 0)
320
322
  dd_version_path = "ids_properties/version_put/data_dictionary"
@@ -12,7 +12,6 @@ import tempfile
12
12
  import time
13
13
  import uuid
14
14
  from pathlib import Path
15
- from saxonche import PySaxonProcessor
16
15
  from subprocess import CalledProcessError, check_output
17
16
  from zlib import crc32
18
17
 
@@ -244,11 +243,20 @@ def transform_with_xslt(xslt_processor, source, xslfile, output_file):
244
243
 
245
244
  def create_model_ids_xml(cache_dir_path, fname, version):
246
245
  """Use Saxon/C to compile an ids.xml suitable for creating an MDSplus model."""
246
+ try:
247
+ import saxonche
248
+ except ImportError:
249
+ raise RuntimeError(
250
+ "Building mdsplus models requires the 'saxonche' python package. "
251
+ "Please install this package (for example with 'pip install saxonche') "
252
+ "and try again."
253
+ )
254
+
247
255
  try:
248
256
  with as_file(files("imas") / "assets" / "IDSDef2MDSpreTree.xsl") as xslfile:
249
257
  output_file = Path(cache_dir_path) / "ids.xml"
250
258
 
251
- with PySaxonProcessor(license=False) as proc:
259
+ with saxonche.PySaxonProcessor(license=False) as proc:
252
260
  xslt_processor = proc.new_xslt30_processor()
253
261
  xdm_ddgit = proc.make_string_value(str(version) or fname)
254
262
  xslt_processor.set_parameter("DD_GIT_DESCRIBE", xdm_ddgit)
imas/dd_zip.py CHANGED
@@ -1,103 +1,27 @@
1
1
  # This file is part of IMAS-Python.
2
2
  # You should have received the IMAS-Python LICENSE file with this project.
3
- """ Extract DD versions from a zip file.
3
+ """Extract DD versions from the imas-data-dictionaries distribution."""
4
4
 
5
- The zip file contains files as
6
- * `data-dictionary/3.30.0.xml`
7
- * `data-dictionary/3.29.0.xml`
8
-
9
- multiple paths are checked. See `ZIPFILE_LOCATIONS`.
10
- First the environment variable IMAS_DDZIP is checked.
11
- If that exists and points to a file we will attempt to open it.
12
- Then, IDSDef.zip is searched in site-packages, the current folder,
13
- in .config/imas/ (`$$XDG_CONFIG_HOME`) and in
14
- the assets/ folder within the IMAS-Python package.
15
-
16
- 1. `$$IMAS_DDZIP`
17
- 2. The virtual environment
18
- 3. USER_BASE`imas/IDSDef.zip`
19
- 4. All `site-packages/imas/IDSDef.zip`
20
- 5. `./IDSDef.zip`
21
- 6. `~/.config/imas/IDSDef.zip`
22
- 7. `__file__/../../imas/assets/IDSDef.zip`
23
-
24
- All files are checked, i.e. if your .config/imas/IDSDef.zip is outdated
25
- the IMAS-Python-packaged version will be used.
26
-
27
- The `assets/IDSDef.zip` provided with the package can be updated
28
- with the `python setup.py build_DD` command, which is also performed on install
29
- if you have access to the ITER data-dictionary git repo.
30
- Reinstalling imas thus also will give you access to the latest DD versions.
31
- """
32
5
  import logging
33
6
  import os
34
- import re
35
7
  import xml.etree.ElementTree as ET
36
- from contextlib import contextmanager, nullcontext
37
8
  from functools import lru_cache
38
9
  from pathlib import Path
39
- from typing import Dict, Iterator, List, Tuple, Union
40
- from zipfile import ZipFile
41
-
42
- try:
43
- from importlib.resources import as_file, files
44
-
45
- try:
46
- from importlib.resources.abc import Traversable
47
- except ModuleNotFoundError: # Python 3.9/3.10 support
48
- from importlib.abc import Traversable
49
-
50
- except ImportError: # Python 3.8 support
51
- from importlib_resources import as_file, files
52
- from importlib_resources.abc import Traversable
53
10
 
54
- from packaging.version import InvalidVersion, Version
11
+ # These methods in imas_data_dictionaries used to be defined here. We import them here
12
+ # for backwards compatibility:
13
+ from imas_data_dictionaries import dd_identifiers # noqa: F401
14
+ from imas_data_dictionaries import get_dd_xml_crc # noqa: F401
15
+ from imas_data_dictionaries import get_identifier_xml # noqa: F401
16
+ from imas_data_dictionaries import dd_xml_versions, get_dd_xml, parse_dd_version
17
+ from packaging.version import InvalidVersion
55
18
 
56
19
  import imas
57
- from imas.exception import UnknownDDVersion
20
+ from imas.exception import UnknownDDVersion # noqa: F401
58
21
 
59
22
  logger = logging.getLogger(__name__)
60
23
 
61
24
 
62
- def _get_xdg_config_dir():
63
- """
64
- Return the XDG config directory, according to the XDG base directory spec:
65
-
66
- https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
67
- """
68
- return os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
69
-
70
-
71
- def _generate_zipfile_locations() -> Iterator[Union[Path, Traversable]]:
72
- """Build a list of potential data dictionary locations.
73
- We start with the path (if any) of the IMAS_DDZIP env var.
74
- Then we look for IDSDef.zip in the current folder, in the
75
- default XDG config dir (~/.config/imas/IDSDef.zip) and
76
- finally in the assets distributed with this package.
77
- """
78
- zip_name = "IDSDef.zip"
79
-
80
- environ = os.environ.get("IMAS_DDZIP")
81
- if environ:
82
- yield Path(environ).resolve()
83
-
84
- yield Path(zip_name).resolve()
85
- yield Path(_get_xdg_config_dir()).resolve() / "imas" / zip_name
86
- yield files(imas) / "assets" / zip_name
87
-
88
-
89
- def parse_dd_version(version: str) -> Version:
90
- try:
91
- return Version(version)
92
- except InvalidVersion:
93
- # This is probably a dev build of the DD, of which the version is obtained with
94
- # `git describe` in the format X.Y.Z-<ncommits>-g<hash> with X.Y.Z the previous
95
- # released version: try again after converting the first dash to a + and treat
96
- # it like a `local` version specifier, which is recognized as newer.
97
- # https://packaging.python.org/en/latest/specifications/version-specifiers/
98
- return Version(version.replace("-", "+", 1))
99
-
100
-
101
25
  # Expected use case is one, maximum two DD versions
102
26
  # Cache is bigger than that: in pytest we currently use the following DD versions:
103
27
  # - 3.22.0
@@ -112,7 +36,6 @@ def parse_dd_version(version: str) -> Version:
112
36
  # - IDS_minimal_struct_array.xml
113
37
  # - IDS_minimal_types.xml
114
38
  _DD_CACHE_SIZE = 8
115
- ZIPFILE_LOCATIONS = list(_generate_zipfile_locations())
116
39
 
117
40
 
118
41
  def dd_etree(version=None, xml_path=None):
@@ -168,117 +91,6 @@ def _load_etree(version, xml_path):
168
91
  return tree
169
92
 
170
93
 
171
- @contextmanager
172
- def _open_zipfile(path: Union[Path, Traversable]) -> Iterator[ZipFile]:
173
- """Open a zipfile, given a Path or Traversable."""
174
- if isinstance(path, Path):
175
- ctx = nullcontext(path)
176
- else:
177
- ctx = as_file(path)
178
- with ctx as file:
179
- with ZipFile(file) as zipfile:
180
- yield zipfile
181
-
182
-
183
- @lru_cache
184
- def _read_dd_versions() -> Dict[str, Tuple[Union[Path, Traversable], str]]:
185
- """Traverse all possible DD zip files and return a map of known versions.
186
-
187
- Returns:
188
- version_map: version -> (zipfile path, filename)
189
- """
190
- versions = {}
191
- xml_re = re.compile(r"^data-dictionary/([0-9.]+)\.xml$")
192
- for path in ZIPFILE_LOCATIONS:
193
- if not path.is_file():
194
- continue
195
- with _open_zipfile(path) as zipfile:
196
- for fname in zipfile.namelist():
197
- match = xml_re.match(fname)
198
- if match:
199
- version = match.group(1)
200
- if version not in versions:
201
- versions[version] = (path, fname)
202
- if not versions:
203
- raise RuntimeError(
204
- "Could not find any data dictionary definitions. "
205
- f"Looked in: {', '.join(map(repr, ZIPFILE_LOCATIONS))}."
206
- )
207
- return versions
208
-
209
-
210
- @lru_cache
211
- def _read_identifiers() -> Dict[str, Tuple[Union[Path, Traversable], str]]:
212
- """Traverse all possible DD zip files and return a map of known identifiers.
213
-
214
- Returns:
215
- identifier_map: identifier -> (zipfile path, filename)
216
- """
217
- identifiers = {}
218
- xml_re = re.compile(r"^identifiers/\w+/(\w+_identifier).xml$")
219
- for path in ZIPFILE_LOCATIONS:
220
- if not path.is_file():
221
- continue
222
- with _open_zipfile(path) as zipfile:
223
- for fname in zipfile.namelist():
224
- match = xml_re.match(fname)
225
- if match:
226
- identifier_name = match.group(1)
227
- if identifier_name not in identifiers:
228
- identifiers[identifier_name] = (path, fname)
229
- return identifiers
230
-
231
-
232
- @lru_cache
233
- def dd_xml_versions() -> List[str]:
234
- """Parse IDSDef.zip to find version numbers available"""
235
-
236
- def sort_key(version):
237
- try:
238
- return parse_dd_version(version)
239
- except InvalidVersion:
240
- # Don't fail when a malformatted version is present in the DD zip
241
- logger.error(
242
- f"Could not convert DD XML version {version} to a Version.", exc_info=1
243
- )
244
- return Version(0)
245
-
246
- return sorted(_read_dd_versions(), key=sort_key)
247
-
248
-
249
- @lru_cache
250
- def dd_identifiers() -> List[str]:
251
- """Parse IDSDef.zip to find available identifiers"""
252
-
253
- return sorted(_read_identifiers())
254
-
255
-
256
- def get_dd_xml(version):
257
- """Read XML file for the given data dictionary version."""
258
- dd_versions = dd_xml_versions()
259
- if version not in dd_versions:
260
- raise UnknownDDVersion(version, dd_versions)
261
- path, fname = _read_dd_versions()[version]
262
- with _open_zipfile(path) as zipfile:
263
- return zipfile.read(fname)
264
-
265
-
266
- def get_dd_xml_crc(version):
267
- """Given a version string, return its CRC checksum"""
268
- # Note, by this time get_dd_xml is already called, so we don't need to check if the
269
- # version is known
270
- path, fname = _read_dd_versions()[version]
271
- with _open_zipfile(path) as zipfile:
272
- return zipfile.getinfo(fname).CRC
273
-
274
-
275
- def get_identifier_xml(identifier_name):
276
- """Get identifier XML for the given identifier name"""
277
- path, fname = _read_identifiers()[identifier_name]
278
- with _open_zipfile(path) as zipfile:
279
- return zipfile.read(fname)
280
-
281
-
282
94
  def print_supported_version_warning(version):
283
95
  try:
284
96
  if parse_dd_version(version) < imas.OLDEST_SUPPORTED_VERSION:
imas/exception.py CHANGED
@@ -1,11 +1,14 @@
1
1
  # This file is part of IMAS-Python.
2
2
  # You should have received the IMAS-Python LICENSE file with this project.
3
- """Exception classes used in IMAS-Python.
4
- """
3
+ """Exception classes used in IMAS-Python."""
5
4
 
6
5
  import difflib
7
6
  import logging
8
- from typing import TYPE_CHECKING, List
7
+ from typing import TYPE_CHECKING
8
+
9
+ # This exception from imas_data_dictionaries used to be defined here. We import it here
10
+ # for backwards compatibility:
11
+ from imas_data_dictionaries import UnknownDDVersion # noqa: F401
9
12
 
10
13
  from imas.backends.imas_core import imas_interface as _imas_interface
11
14
 
@@ -23,20 +26,6 @@ else:
23
26
  ALException = None
24
27
 
25
28
 
26
- class UnknownDDVersion(ValueError):
27
- """Error raised when an unknown DD version is specified."""
28
-
29
- def __init__(self, version: str, available: List[str], note: str = "") -> None:
30
- close_matches = difflib.get_close_matches(version, available, n=1)
31
- if close_matches:
32
- suggestions = f"Did you mean {close_matches[0]!r}?"
33
- else:
34
- suggestions = f"Available versions are {', '.join(reversed(available))}"
35
- super().__init__(
36
- f"Data dictionary version {version!r} cannot be found. {suggestions}{note}"
37
- )
38
-
39
-
40
29
  class IDSNameError(ValueError):
41
30
  """Error raised by DBEntry.get(_slice) when providing an invalid IDS name."""
42
31
 
imas/ids_convert.py CHANGED
@@ -1,36 +1,35 @@
1
1
  # This file is part of IMAS-Python.
2
2
  # You should have received the IMAS-Python LICENSE file with this project.
3
- """Functionality for converting IDSToplevels between DD versions.
4
- """
3
+ """Functionality for converting IDSToplevels between DD versions."""
5
4
 
6
5
  import copy
7
6
  import datetime
8
7
  import logging
9
- from functools import lru_cache
8
+ from functools import lru_cache, partial
10
9
  from pathlib import Path
11
10
  from typing import Callable, Dict, Iterator, Optional, Set, Tuple
12
11
  from xml.etree.ElementTree import Element, ElementTree
13
12
 
14
13
  import numpy
15
14
  from packaging.version import InvalidVersion, Version
15
+ from scipy.interpolate import interp1d
16
16
 
17
17
  import imas
18
18
  from imas.dd_zip import parse_dd_version
19
19
  from imas.ids_base import IDSBase
20
20
  from imas.ids_data_type import IDSDataType
21
+ from imas.ids_defs import IDS_TIME_MODE_HETEROGENEOUS
21
22
  from imas.ids_factory import IDSFactory
22
23
  from imas.ids_path import IDSPath
23
- from imas.ids_primitive import (
24
- IDSNumeric0D,
25
- IDSNumericArray,
26
- IDSPrimitive,
27
- IDSString0D,
28
- )
24
+ from imas.ids_primitive import IDSNumeric0D, IDSNumericArray, IDSPrimitive, IDSString0D
29
25
  from imas.ids_struct_array import IDSStructArray
30
26
  from imas.ids_structure import IDSStructure
31
27
  from imas.ids_toplevel import IDSToplevel
32
28
 
33
29
  logger = logging.getLogger(__name__)
30
+ # Store for which paths we already emitted a warning that the target could not be found
31
+ # to prevent polluting the output with lots of repeated items.
32
+ _missing_paths_warning = set()
34
33
 
35
34
 
36
35
  def iter_parents(path: str) -> Iterator[str]:
@@ -334,12 +333,13 @@ class DDVersionMap:
334
333
  # Additional conversion rules for DDv3 to DDv4
335
334
  if self.version_old.major == 3 and new_version and new_version.major == 4:
336
335
  # Postprocessing for COCOS definition change:
337
- xpath_query = ".//field[@cocos_label_transformation='psi_like']"
338
- for old_item in old.iterfind(xpath_query):
339
- old_path = old_item.get("path")
340
- new_path = self.old_to_new.path.get(old_path, old_path)
341
- self.new_to_old.post_process[new_path] = _cocos_change
342
- self.old_to_new.post_process[old_path] = _cocos_change
336
+ for psi_like in ["psi_like", "dodpsi_like"]:
337
+ xpath_query = f".//field[@cocos_label_transformation='{psi_like}']"
338
+ for old_item in old.iterfind(xpath_query):
339
+ old_path = old_item.get("path")
340
+ new_path = self.old_to_new.path.get(old_path, old_path)
341
+ self.new_to_old.post_process[new_path] = _cocos_change
342
+ self.old_to_new.post_process[old_path] = _cocos_change
343
343
  # Definition change for pf_active circuit/connections
344
344
  if self.ids_name == "pf_active":
345
345
  path = "circuit/connections"
@@ -474,32 +474,48 @@ def convert_ids(
474
474
  raise RuntimeError(
475
475
  f"There is no IDS with name {ids_name} in DD version {version}."
476
476
  )
477
- target_ids = factory.new(ids_name)
478
- else:
479
- target_ids = target
477
+ target = factory.new(ids_name)
480
478
 
481
479
  source_version = parse_dd_version(toplevel._version)
482
- target_version = parse_dd_version(target_ids._version)
480
+ target_version = parse_dd_version(target._version)
483
481
  logger.info(
484
482
  "Starting conversion of IDS %s from version %s to version %s.",
485
483
  ids_name,
486
484
  source_version,
487
485
  target_version,
488
486
  )
487
+ global _missing_paths_warning
488
+ _missing_paths_warning = set() # clear for which paths we emitted a warning
489
489
 
490
- source_is_new = source_version > target_version
491
490
  source_tree = toplevel._parent._etree
492
- target_tree = target_ids._parent._etree
493
- if source_is_new:
491
+ target_tree = target._parent._etree
492
+ if source_version > target_version:
494
493
  version_map = _DDVersionMap(ids_name, target_tree, source_tree, target_version)
494
+ rename_map = version_map.new_to_old
495
495
  else:
496
496
  version_map = _DDVersionMap(ids_name, source_tree, target_tree, source_version)
497
+ rename_map = version_map.old_to_new
498
+
499
+ # Special case for DD3to4 pulse_schedule conversion
500
+ if (
501
+ toplevel.metadata.name == "pulse_schedule"
502
+ and toplevel.ids_properties.homogeneous_time == IDS_TIME_MODE_HETEROGENEOUS
503
+ and source_version < Version("3.40.0")
504
+ and target_version >= Version("3.40.0")
505
+ ):
506
+ try:
507
+ # Suppress "'.../time' does not exist in the target IDS." log messages.
508
+ logger.addFilter(_pulse_schedule_3to4_logfilter)
509
+ _pulse_schedule_3to4(toplevel, target, deepcopy, rename_map)
510
+ finally:
511
+ logger.removeFilter(_pulse_schedule_3to4_logfilter)
512
+ else:
513
+ _copy_structure(toplevel, target, deepcopy, rename_map)
497
514
 
498
- _copy_structure(toplevel, target_ids, deepcopy, source_is_new, version_map)
499
515
  logger.info("Conversion of IDS %s finished.", ids_name)
500
516
  if provenance_origin_uri:
501
- _add_provenance_entry(target_ids, toplevel._version, provenance_origin_uri)
502
- return target_ids
517
+ _add_provenance_entry(target, toplevel._version, provenance_origin_uri)
518
+ return target
503
519
 
504
520
 
505
521
  def _add_provenance_entry(
@@ -541,12 +557,50 @@ def _add_provenance_entry(
541
557
  node.sources.append(source_txt) # sources is a STR_1D (=list of strings)
542
558
 
543
559
 
560
+ def _get_target_item(
561
+ item: IDSBase, target: IDSStructure, rename_map: NBCPathMap
562
+ ) -> Optional[IDSBase]:
563
+ """Find and return the corresponding target item if it exists.
564
+
565
+ This method follows NBC renames (as stored in the rename map). It returns None if
566
+ there is no corresponding target item in the target structure.
567
+ """
568
+ path = item.metadata.path_string
569
+
570
+ # Follow NBC renames:
571
+ if path in rename_map:
572
+ if rename_map.path[path] is None:
573
+ if path not in rename_map.ignore_missing_paths:
574
+ # Only warn the first time that we encounter this path:
575
+ if path not in _missing_paths_warning:
576
+ if path in rename_map.type_change:
577
+ msg = "Element %r changed type in the target IDS."
578
+ else:
579
+ msg = "Element %r does not exist in the target IDS."
580
+ logger.warning(msg + " Data is not copied.", path)
581
+ _missing_paths_warning.add(path)
582
+ return None
583
+ else:
584
+ return IDSPath(rename_map.path[path]).goto(target)
585
+
586
+ # No NBC renames:
587
+ try:
588
+ return target[item.metadata.name]
589
+ except AttributeError:
590
+ # In exceptional cases the item does not exist in the target. Example:
591
+ # neutron_diagnostic IDS between DD 3.40.1 and 3.41.0. has renamed
592
+ # synthetic_signals/fusion_power -> fusion_power. The synthetic_signals
593
+ # structure no longer exists but we need to descend into it to get the
594
+ # total_neutron_flux.
595
+ return target
596
+
597
+
544
598
  def _copy_structure(
545
599
  source: IDSStructure,
546
600
  target: IDSStructure,
547
601
  deepcopy: bool,
548
- source_is_new: bool,
549
- version_map: DDVersionMap,
602
+ rename_map: NBCPathMap,
603
+ callback: Optional[Callable] = None,
550
604
  ):
551
605
  """Recursively copy data, following NBC renames.
552
606
 
@@ -557,31 +611,14 @@ def _copy_structure(
557
611
  source_is_new: True iff the DD version of the source is newer than that of the
558
612
  target.
559
613
  version_map: Version map containing NBC renames.
614
+ callback: Optional callback that is called for every copied node.
560
615
  """
561
- rename_map = version_map.new_to_old if source_is_new else version_map.old_to_new
562
616
  for item in source.iter_nonempty_():
563
617
  path = item.metadata.path_string
564
- if path in rename_map:
565
- if rename_map.path[path] is None:
566
- if path not in rename_map.ignore_missing_paths:
567
- if path in rename_map.type_change:
568
- msg = "Element %r changed type in the target IDS."
569
- else:
570
- msg = "Element %r does not exist in the target IDS."
571
- logger.warning(msg + " Data is not copied.", path)
572
- continue
573
- else:
574
- target_item = IDSPath(rename_map.path[path]).goto(target)
575
- else:
576
- try:
577
- target_item = target[item.metadata.name]
578
- except AttributeError:
579
- # In exceptional cases the item does not exist in the target. Example:
580
- # neutron_diagnostic IDS between DD 3.40.1 and 3.41.0. has renamed
581
- # synthetic_signals/fusion_power -> fusion_power. The synthetic_signals
582
- # structure no longer exists but we need to descend into it to get the
583
- # total_neutron_flux.
584
- target_item = target
618
+ target_item = _get_target_item(item, target, rename_map)
619
+ if target_item is None:
620
+ continue
621
+
585
622
  if path in rename_map.type_change:
586
623
  # Handle type change
587
624
  new_items = rename_map.type_change[path](item, target_item)
@@ -594,21 +631,17 @@ def _copy_structure(
594
631
  size = len(item)
595
632
  target_item.resize(size)
596
633
  for i in range(size):
597
- _copy_structure(
598
- item[i], target_item[i], deepcopy, source_is_new, version_map
599
- )
634
+ _copy_structure(item[i], target_item[i], deepcopy, rename_map, callback)
600
635
  elif isinstance(item, IDSStructure):
601
- _copy_structure(item, target_item, deepcopy, source_is_new, version_map)
636
+ _copy_structure(item, target_item, deepcopy, rename_map, callback)
602
637
  else:
603
- if deepcopy:
604
- # No nested types are used as data, so a shallow copy is sufficient
605
- target_item.value = copy.copy(item.value)
606
- else:
607
- target_item.value = item.value
638
+ target_item.value = copy.copy(item.value) if deepcopy else item.value
608
639
 
609
640
  # Post-process the node:
610
641
  if path in rename_map.post_process:
611
642
  rename_map.post_process[path](target_item)
643
+ if callback is not None:
644
+ callback(item, target_item)
612
645
 
613
646
 
614
647
  ########################################################################################
@@ -919,3 +952,82 @@ def _ids_properties_source(source: IDSString0D, provenance: IDSStructure) -> Non
919
952
  provenance.node.resize(1)
920
953
  provenance.node[0].reference.resize(1)
921
954
  provenance.node[0].reference[0].name = source.value
955
+
956
+
957
+ def _pulse_schedule_3to4(
958
+ source: IDSStructure,
959
+ target: IDSStructure,
960
+ deepcopy: bool,
961
+ rename_map: NBCPathMap,
962
+ ):
963
+ """Recursively copy data, following NBC renames, and converting time bases for the
964
+ pulse_schedule IDS.
965
+
966
+ Args:
967
+ source: Source structure.
968
+ target: Target structure.
969
+ deepcopy: See :func:`convert_ids`.
970
+ rename_map: Map containing NBC renames.
971
+ """
972
+ # All prerequisites are checked before calling this function:
973
+ # - source and target are pulse_schedule IDSs
974
+ # - source has DD version < 3.40.0
975
+ # - target has DD version >= 4.0.0, < 5.0
976
+ # - IDS is using heterogeneous time
977
+
978
+ for item in source.iter_nonempty_():
979
+ name = item.metadata.name
980
+ target_item = _get_target_item(item, target, rename_map)
981
+ if target_item is None:
982
+ continue
983
+
984
+ # Special cases for non-dynamic stuff
985
+ if name in ["ids_properties", "code"]:
986
+ _copy_structure(item, target_item, deepcopy, rename_map)
987
+ elif name == "time":
988
+ target_item.value = item.value if not deepcopy else copy.copy(item.value)
989
+ elif name == "event":
990
+ size = len(item)
991
+ target_item.resize(size)
992
+ for i in range(size):
993
+ _copy_structure(item[i], target_item[i], deepcopy, rename_map)
994
+ else:
995
+ # Find all time bases
996
+ time_bases = [
997
+ node.value
998
+ for node in imas.util.tree_iter(item)
999
+ if node.metadata.name == "time"
1000
+ ]
1001
+ # Construct the common time base
1002
+ timebase = numpy.unique(numpy.concatenate(time_bases)) if time_bases else []
1003
+ target_item.time = timebase
1004
+ # Do the conversion
1005
+ callback = partial(_pulse_schedule_resample_callback, timebase)
1006
+ _copy_structure(item, target_item, deepcopy, rename_map, callback)
1007
+
1008
+
1009
+ def _pulse_schedule_3to4_logfilter(logrecord: logging.LogRecord) -> bool:
1010
+ """Suppress "'.../time' does not exist in the target IDS." log messages."""
1011
+ return not (logrecord.args and str(logrecord.args[0]).endswith("/time"))
1012
+
1013
+
1014
+ def _pulse_schedule_resample_callback(timebase, item: IDSBase, target_item: IDSBase):
1015
+ """Callback from _copy_structure to resample dynamic data on the new timebase"""
1016
+ if item.metadata.ndim == 1 and item.metadata.coordinates[0].is_time_coordinate:
1017
+ # Interpolate 1D dynamic quantities to the common time base
1018
+ time = item.coordinates[0]
1019
+ if len(item) != len(time):
1020
+ raise ValueError(
1021
+ f"Array {item} has a different size than its time base {time}."
1022
+ )
1023
+ is_integer = item.metadata.data_type is IDSDataType.INT
1024
+ value = interp1d(
1025
+ time.value,
1026
+ item.value,
1027
+ "previous" if is_integer else "linear",
1028
+ copy=False,
1029
+ bounds_error=False,
1030
+ fill_value=(item[0], item[-1]),
1031
+ assume_sorted=True,
1032
+ )(timebase)
1033
+ target_item.value = value.astype(numpy.int32) if is_integer else value
@@ -7,6 +7,7 @@ from datetime import datetime, timedelta, timezone
7
7
  from unittest.mock import MagicMock
8
8
 
9
9
  import numpy
10
+ from numpy import array_equal
10
11
  import pytest
11
12
 
12
13
  from imas import identifiers
@@ -27,7 +28,7 @@ from imas.ids_defs import (
27
28
  from imas.ids_factory import IDSFactory
28
29
  from imas.ids_struct_array import IDSStructArray
29
30
  from imas.ids_structure import IDSStructure
30
- from imas.test.test_helpers import compare_children, open_dbentry
31
+ from imas.test.test_helpers import compare_children, fill_consistent, open_dbentry
31
32
 
32
33
  UTC = timezone.utc
33
34
 
@@ -287,22 +288,22 @@ def test_3to4_repeat_children_first_point_conditional(dd4factory):
287
288
  for i in range(2):
288
289
  outline_inner = wall4.description_2d[0].vessel.unit[i].annular.outline_inner
289
290
  if i == 0: # open outline, first point not repeated:
290
- assert numpy.array_equal(outline_inner.r, [1.0, 2.0, 3.0])
291
- assert numpy.array_equal(outline_inner.z, [-1.0, -2.0, -3.0])
291
+ assert array_equal(outline_inner.r, [1.0, 2.0, 3.0])
292
+ assert array_equal(outline_inner.z, [-1.0, -2.0, -3.0])
292
293
  else: # closed outline, first point repeated:
293
- assert numpy.array_equal(outline_inner.r, [1.0, 2.0, 3.0, 1.0])
294
- assert numpy.array_equal(outline_inner.z, [-1.0, -2.0, -3.0, -1.0])
294
+ assert array_equal(outline_inner.r, [1.0, 2.0, 3.0, 1.0])
295
+ assert array_equal(outline_inner.z, [-1.0, -2.0, -3.0, -1.0])
295
296
 
296
297
  # Test conversion for case 2:
297
298
  assert len(wall4.description_2d[0].limiter.unit) == 2
298
299
  for i in range(2):
299
300
  unit = wall4.description_2d[0].limiter.unit[i]
300
301
  if i == 0: # open outline, first point not repeated:
301
- assert numpy.array_equal(unit.outline.r, [1.0, 2.0, 3.0])
302
- assert numpy.array_equal(unit.outline.z, [-1.0, -2.0, -3.0])
302
+ assert array_equal(unit.outline.r, [1.0, 2.0, 3.0])
303
+ assert array_equal(unit.outline.z, [-1.0, -2.0, -3.0])
303
304
  else: # closed outline, first point repeated:
304
- assert numpy.array_equal(unit.outline.r, [1.0, 2.0, 3.0, 1.0])
305
- assert numpy.array_equal(unit.outline.z, [-1.0, -2.0, -3.0, -1.0])
305
+ assert array_equal(unit.outline.r, [1.0, 2.0, 3.0, 1.0])
306
+ assert array_equal(unit.outline.z, [-1.0, -2.0, -3.0, -1.0])
306
307
 
307
308
  # Test conversion for case 3:
308
309
  assert len(wall4.description_2d[0].mobile.unit) == 2
@@ -310,11 +311,11 @@ def test_3to4_repeat_children_first_point_conditional(dd4factory):
310
311
  unit = wall4.description_2d[0].mobile.unit[i]
311
312
  for j in range(3):
312
313
  if i == 0: # open outline, first point not repeated:
313
- assert numpy.array_equal(unit.outline[j].r, [1.0, 2.0, 3.0])
314
- assert numpy.array_equal(unit.outline[j].z, [-1.0, -2.0, -3.0])
314
+ assert array_equal(unit.outline[j].r, [1.0, 2.0, 3.0])
315
+ assert array_equal(unit.outline[j].z, [-1.0, -2.0, -3.0])
315
316
  else: # closed outline, first point repeated:
316
- assert numpy.array_equal(unit.outline[j].r, [1.0, 2.0, 3.0, 1.0])
317
- assert numpy.array_equal(unit.outline[j].z, [-1.0, -2.0, -3.0, -1.0])
317
+ assert array_equal(unit.outline[j].r, [1.0, 2.0, 3.0, 1.0])
318
+ assert array_equal(unit.outline[j].z, [-1.0, -2.0, -3.0, -1.0])
318
319
  assert unit.outline[j].time == pytest.approx(j / 5)
319
320
 
320
321
  # Test conversion for case 4:
@@ -322,9 +323,9 @@ def test_3to4_repeat_children_first_point_conditional(dd4factory):
322
323
  for i in range(2):
323
324
  thickness = wall4.description_2d[1].vessel.unit[i].annular.thickness
324
325
  if i == 0: # open outline, there was one value too many, drop the last one
325
- assert numpy.array_equal(thickness, [1, 0.9])
326
+ assert array_equal(thickness, [1, 0.9])
326
327
  else: # closed outline, thickness values kept
327
- assert numpy.array_equal(thickness, [1, 0.9, 0.9])
328
+ assert array_equal(thickness, [1, 0.9, 0.9])
328
329
 
329
330
  # Test conversion back
330
331
  wall3 = convert_ids(wall4, "3.39.0")
@@ -340,8 +341,8 @@ def test_3to4_repeat_children_first_point(dd4factory):
340
341
 
341
342
  iron_core4 = convert_ids(iron_core, None, factory=dd4factory)
342
343
  geometry = iron_core4.segment[0].geometry
343
- assert numpy.array_equal(geometry.outline.r, [1.0, 2.0, 3.0, 1.0])
344
- assert numpy.array_equal(geometry.outline.z, [-1.0, -2.0, -3.0, -1.0])
344
+ assert array_equal(geometry.outline.r, [1.0, 2.0, 3.0, 1.0])
345
+ assert array_equal(geometry.outline.z, [-1.0, -2.0, -3.0, -1.0])
345
346
 
346
347
  iron_core3 = convert_ids(iron_core4, "3.39.0")
347
348
  compare_children(iron_core, iron_core3)
@@ -356,11 +357,11 @@ def test_3to4_cocos_change(dd4factory):
356
357
  cp.profiles_1d[0].grid.psi = numpy.linspace(10, 20, 11)
357
358
 
358
359
  cp4 = convert_ids(cp, None, factory=dd4factory)
359
- assert numpy.array_equal(
360
+ assert array_equal(
360
361
  cp4.profiles_1d[0].grid.rho_tor_norm,
361
362
  cp.profiles_1d[0].grid.rho_tor_norm,
362
363
  )
363
- assert numpy.array_equal(
364
+ assert array_equal(
364
365
  cp4.profiles_1d[0].grid.psi,
365
366
  -cp.profiles_1d[0].grid.psi,
366
367
  )
@@ -368,6 +369,26 @@ def test_3to4_cocos_change(dd4factory):
368
369
  cp3 = convert_ids(cp4, "3.39.0")
369
370
  compare_children(cp, cp3)
370
371
 
372
+ eq = IDSFactory("3.39.0").equilibrium()
373
+ eq.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS
374
+ eq.time = [1.0]
375
+ eq.time_slice.resize(1)
376
+ eq.time_slice[0].profiles_1d.psi = numpy.linspace(0, 1, 11)
377
+ eq.time_slice[0].profiles_1d.dpressure_dpsi = numpy.linspace(1, 2, 11)
378
+
379
+ eq4 = convert_ids(eq, None, factory=dd4factory)
380
+ assert array_equal(
381
+ eq4.time_slice[0].profiles_1d.psi,
382
+ -eq.time_slice[0].profiles_1d.psi,
383
+ )
384
+ assert array_equal(
385
+ eq4.time_slice[0].profiles_1d.dpressure_dpsi,
386
+ -eq.time_slice[0].profiles_1d.dpressure_dpsi,
387
+ )
388
+
389
+ eq3 = convert_ids(eq4, "3.39.0")
390
+ compare_children(eq, eq3)
391
+
371
392
 
372
393
  def test_3to4_circuit_connections(dd4factory, caplog):
373
394
  pfa = IDSFactory("3.39.0").pf_active()
@@ -380,7 +401,7 @@ def test_3to4_circuit_connections(dd4factory, caplog):
380
401
  ]
381
402
 
382
403
  pfa4 = convert_ids(pfa, None, factory=dd4factory)
383
- assert numpy.array_equal(
404
+ assert array_equal(
384
405
  pfa4.circuit[0].connections, [[-1, 0, 1], [0, 1, -1], [1, -1, 0]]
385
406
  )
386
407
 
@@ -397,7 +418,7 @@ def test_3to4_circuit_connections(dd4factory, caplog):
397
418
  with caplog.at_level(logging.ERROR):
398
419
  pfa4 = convert_ids(pfa, None, factory=dd4factory)
399
420
  # Incorrect shape, data is not converted:
400
- assert numpy.array_equal(pfa.circuit[0].connections, pfa4.circuit[0].connections)
421
+ assert array_equal(pfa.circuit[0].connections, pfa4.circuit[0].connections)
401
422
  # Check that a message with ERROR severity was logged
402
423
  assert len(caplog.record_tuples) == 1
403
424
  assert caplog.record_tuples[0][1] == logging.ERROR
@@ -410,7 +431,53 @@ def test_3to4_cocos_magnetics_workaround(dd4factory):
410
431
  mag.flux_loop[0].flux.data = [1.0, 2.0]
411
432
 
412
433
  mag4 = convert_ids(mag, None, factory=dd4factory)
413
- assert numpy.array_equal(mag4.flux_loop[0].flux.data, [-1.0, -2.0])
434
+ assert array_equal(mag4.flux_loop[0].flux.data, [-1.0, -2.0])
414
435
 
415
436
  mag3 = convert_ids(mag4, "3.39.0")
416
437
  compare_children(mag, mag3)
438
+
439
+
440
+ def test_3to4_pulse_schedule():
441
+ ps = IDSFactory("3.39.0").pulse_schedule()
442
+ ps.ids_properties.homogeneous_time = IDS_TIME_MODE_HETEROGENEOUS
443
+
444
+ ps.ec.launcher.resize(3)
445
+ ps.ec.launcher[0].power.reference.data = [1.0, 2.0, 3.0]
446
+ ps.ec.launcher[0].power.reference.time = [1.0, 2.0, 3.0]
447
+ ps.ec.launcher[1].power.reference.data = [0.0, 2.0, 5.0]
448
+ ps.ec.launcher[1].power.reference.time = [0.0, 2.0, 5.0]
449
+ ps.ec.launcher[2].power.reference.data = [1.0, 1.5]
450
+ ps.ec.launcher[2].power.reference.time = [1.0, 1.5]
451
+
452
+ ps.ec.mode.data = [1, 2, 5]
453
+ ps.ec.mode.time = [1.0, 2.0, 5.0]
454
+
455
+ ps4 = convert_ids(ps, "4.0.0")
456
+ assert array_equal(ps4.ec.time, [0.0, 1.0, 1.5, 2.0, 3.0, 5.0])
457
+ item = "power_launched/reference"
458
+ assert array_equal(ps4.ec.beam[0][item], [1.0, 1.0, 1.5, 2.0, 3.0, 3.0])
459
+ assert array_equal(ps4.ec.beam[1][item], [0.0, 1.0, 1.5, 2.0, 3.0, 5.0])
460
+ assert array_equal(ps4.ec.beam[2][item], [1.0, 1.0, 1.5, 1.5, 1.5, 1.5])
461
+ assert array_equal(ps4.ec.mode, [1, 1, 1, 2, 2, 5])
462
+
463
+
464
+ def test_3to4_pulse_schedule_exceptions():
465
+ ps = IDSFactory("3.39.0").pulse_schedule()
466
+ ps.ids_properties.homogeneous_time = IDS_TIME_MODE_HETEROGENEOUS
467
+
468
+ ps.ec.launcher.resize(3)
469
+ ps.ec.launcher[0].power.reference.data = [1.0, 2.0, 3.0]
470
+ with pytest.raises(ValueError): # missing time base
471
+ convert_ids(ps, "4.0.0")
472
+
473
+ ps.ec.launcher[0].power.reference.time = [1.0, 2.0]
474
+ with pytest.raises(ValueError): # incorrect size of time base
475
+ convert_ids(ps, "4.0.0")
476
+
477
+
478
+ def test_3to4_pulse_schedule_fuzz():
479
+ ps = IDSFactory("3.39.0").pulse_schedule()
480
+ ps.ids_properties.homogeneous_time = IDS_TIME_MODE_HETEROGENEOUS
481
+
482
+ fill_consistent(ps)
483
+ convert_ids(ps, "4.0.0")
@@ -78,7 +78,7 @@ def test_lazy_loading_distributions_random_netcdf(tmp_path):
78
78
 
79
79
  def run_lazy_loading_distributions_random(dbentry):
80
80
  ids = IDSFactory().new("distributions")
81
- fill_consistent(ids)
81
+ fill_consistent(ids, skip_complex=True)
82
82
  dbentry.put(ids)
83
83
 
84
84
  def iterate(structure):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: imas-python
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: Pythonic wrappers for the IMAS Access Layer
5
5
  Author: ITER Organization
6
6
  Author-email: Karel Lucas van de Plassche <karelvandeplassche@gmail.com>, Daan van Vugt <dvanvugt@ignitioncomputing.com>, Maarten Sebregts <msebregts@ignitioncomputing.com>, imas-support@iter.org
@@ -205,15 +205,14 @@ Classifier: Topic :: Utilities
205
205
  Requires-Python: >=3.7
206
206
  Description-Content-Type: text/markdown
207
207
  License-File: LICENSE.txt
208
- Requires-Dist: numpy<2,>=1.15.4
208
+ Requires-Dist: numpy>=1.15.4
209
209
  Requires-Dist: rich
210
210
  Requires-Dist: scipy
211
211
  Requires-Dist: click
212
212
  Requires-Dist: importlib_resources; python_version < "3.9"
213
213
  Requires-Dist: packaging
214
214
  Requires-Dist: xxhash>=2
215
- Requires-Dist: saxonche
216
- Requires-Dist: gitpython
215
+ Requires-Dist: imas_data_dictionaries
217
216
  Provides-Extra: all
218
217
  Requires-Dist: imas-python[docs,test]; extra == "all"
219
218
  Provides-Extra: docs
@@ -227,6 +226,8 @@ Provides-Extra: h5py
227
226
  Requires-Dist: h5py; extra == "h5py"
228
227
  Provides-Extra: xarray
229
228
  Requires-Dist: xarray; extra == "xarray"
229
+ Provides-Extra: saxonche
230
+ Requires-Dist: saxonche; extra == "saxonche"
230
231
  Provides-Extra: test
231
232
  Requires-Dist: pytest>=5.4.1; extra == "test"
232
233
  Requires-Dist: pytest-cov>=0.6; extra == "test"
@@ -236,7 +237,8 @@ Requires-Dist: matplotlib; extra == "test"
236
237
  Requires-Dist: asv==0.6.1; extra == "test"
237
238
  Requires-Dist: virtualenv; extra == "test"
238
239
  Requires-Dist: pint; extra == "test"
239
- Requires-Dist: imas-python[h5py,netcdf,xarray]; extra == "test"
240
+ Requires-Dist: imas-python[h5py,netcdf,saxonche,xarray]; extra == "test"
241
+ Dynamic: license-file
240
242
 
241
243
  # IMAS-Python
242
244
 
@@ -248,13 +250,20 @@ Data Model.
248
250
 
249
251
  ## Install
250
252
 
251
- Install steps are described in the documentation generated from `/docs/source/installing.rst`.
253
+ Simply install IMAS-Python with ``pip``:
254
+ ```bash
255
+ pip install imas-python
256
+ ```
257
+ or with optional dependencies for netCDF and xarray support:
258
+ ```bash
259
+ pip install imas-python[netcdf,xarray]
260
+ ```
252
261
 
253
- Documentation is autogenerated from the source using [Sphinx](http://sphinx-doc.org/)
262
+ More details are described in the documentation generated from `/docs/source/installing.rst`.
263
+ The documentation is autogenerated from the source using [Sphinx](http://sphinx-doc.org/)
254
264
  and can be found at the [readthedocs](https://imas-python.readthedocs.io/en/latest/)
255
265
 
256
- The documentation can be manually generated by installing sphinx and running:
257
-
266
+ To generate the documentation yourself, install the ``docs`` optional dependencies and do:
258
267
  ```bash
259
268
  make -C docs html
260
269
  ```
@@ -1,14 +1,13 @@
1
- imas/__init__.py,sha256=LyuR1kEQUkXdHae3VTBBZCHLtFhrJ2fMPY_j6fX7dhc,878
1
+ imas/__init__.py,sha256=lJPQRtGz6WP4e298IbK_qeaslaipe2GzT0mS6h3pPKU,862
2
2
  imas/__main__.py,sha256=nei61YSa3KE4V601qcApNQYgv9hKMenoMJyZLWmsHRQ,418
3
3
  imas/_to_xarray.py,sha256=l3Vc5tE0zcc9elt5RCRJjqzjmvvnNLCjkzqXQoep4zU,2713
4
4
  imas/_util.py,sha256=3CwP5yoyXNipyaUdMb-IwzVDlqV35VjTFxfaronO1AI,9484
5
- imas/_version.py,sha256=2thmcF9DS_Zp1zHI3N0kjBeMAuCP9mdDGnL0clqQpS8,511
5
+ imas/_version.py,sha256=0PAtIHDEagdGmwgz1nnz5zUKeyYjsxtV8Nbsv7dXl8w,511
6
6
  imas/db_entry.py,sha256=GgzNXbU-4gL5ac2vVo28LtnF3JIKIqyQzg1MIhyMkkQ,31755
7
- imas/dd_helpers.py,sha256=MmD_ZSwW-c-7eq2lKwbCWvAZoW0nFAuL0T70dd-yZSk,6195
8
- imas/dd_zip.py,sha256=k0JFUqWBMXwuQmSOzukEujOJV21PF77jCBsNJQDcpGc,10322
9
- imas/exception.py,sha256=0IoVTpFZJPZVE-eeWQfR7lGX_v6ZYjgiT9S7ilBjEYM,3750
7
+ imas/dd_zip.py,sha256=Fiu81NjIyQ-4tdmpaUNJtgKohiNYWIzYcNz96S9zrZ4,3988
8
+ imas/exception.py,sha256=rX-dOIRzPEassnMCT3xXxK1NBMcBbjzpPEfYiVjyIzM,3354
10
9
  imas/ids_base.py,sha256=1jfnpRis-mlS3VbbaarBQ1HVyD3kwsjxhHkyPthlH_Q,5439
11
- imas/ids_convert.py,sha256=jU86M-svO3xca6pOknne3IS4GiSh1T_1rGIeiUBGb8g,38601
10
+ imas/ids_convert.py,sha256=62NACbsBBjbQgPiUCzZtuCMnJfcddB0pP66py-DHYTY,43235
12
11
  imas/ids_coordinates.py,sha256=OT_9V9-0_mVZqutj6uuddnrhdgZ6_YocrdgeRWY-oyc,16955
13
12
  imas/ids_data_type.py,sha256=N3nQXmIht0q8BbqE99sPsTmZ62CTUHv10Pfj_rtYi54,3302
14
13
  imas/ids_defs.py,sha256=WRLnhHRlePdXey6jm_05w-z2qmZPHOW39rvHs7ifJco,5417
@@ -23,7 +22,6 @@ imas/ids_toplevel.py,sha256=MQVFFjgKxF-7tC5PLPCJEtJCjzOvwRxflrO6d0DThXs,14915
23
22
  imas/setup_logging.py,sha256=0v4_haGLsUJr8fHkl46UmTEgMImBzvfL2xU5kudB9ac,1872
24
23
  imas/training.py,sha256=QM447EzkfN1o6OKcvquh99omIU_4LbxEzW_HynSPdwI,1576
25
24
  imas/util.py,sha256=up6VH1IAfkn5WKmJ-1aQqaefbp2l8aDkOWEEPDds5zA,20763
26
- imas/assets/IDSDef.zip,sha256=4jCydzzVoCf59glCNUq3Vwul3DUTs1RHvDkaXD1zp10,27383258
27
25
  imas/assets/IDSDef2MDSpreTree.xsl,sha256=V9eDmDnxe6oYhDP81gmdYip8cF2yST9dGxmZBaATLdA,7296
28
26
  imas/assets/IDS_fake_toplevel.xml,sha256=TJhukQuDHHVTW2U7fK_-KmTQiYOFZMY-ECZXBHQE2mk,18683
29
27
  imas/assets/IDS_minimal.xml,sha256=4V8a7sYKyQnUeJQsWGIG1W8kWaTgZ0LjTGCvd9dkOlc,1740
@@ -38,11 +36,11 @@ imas/assets/equilibrium.ids,sha256=uYpVwTAyGVcWCrBUdSR3427qDY5zx1WXeX4Hd-o_2Jo,3
38
36
  imas/backends/__init__.py,sha256=mRL7PcVmJYdAjTEYwrNBQxgDR_BCjCdrVJeZDxqmhJU,469
39
37
  imas/backends/db_entry_impl.py,sha256=jGZDCxHnN9m2haUqp2RIPD1kZDnv4gFI5nQojf-m4X4,3995
40
38
  imas/backends/imas_core/__init__.py,sha256=Xdbe5WbZzPEfl2zgPzrAvU6k_tB6N70D1Qey9Gvoljg,190
41
- imas/backends/imas_core/al_context.py,sha256=zQYZqK5JJF1S6awjreXMZ-sdifb5gqDBR_QIIQG7hYk,14533
42
- imas/backends/imas_core/db_entry_al.py,sha256=-qPK8qBtG59oJGbEj0xuuT4mHhVoKt2qmBLEPHYTmHs,14893
39
+ imas/backends/imas_core/al_context.py,sha256=i9GRd6ZHBhCfRug2UWvDJGoSqjjfyCvTVnduSF_seEA,14756
40
+ imas/backends/imas_core/db_entry_al.py,sha256=ZtK5x-Nw9EGjKoXLAVeR2CsVI2nR-YlLvFai9DhGHbU,15051
43
41
  imas/backends/imas_core/db_entry_helpers.py,sha256=fjvb3gEBiAzexDCyIsoGTwEcWuvPwhjZ0epXa-p_9hA,7883
44
42
  imas/backends/imas_core/imas_interface.py,sha256=Ml28BrN-6MOUgxXgjUvm7tixhgkWwUWygKaHeI98cCk,7980
45
- imas/backends/imas_core/mdsplus_model.py,sha256=pBo_pOMkD852ISBqyLBIFpE_9S-8E6toUwcFXaPyS7c,14756
43
+ imas/backends/imas_core/mdsplus_model.py,sha256=NdZ5fbBksU3ipk3iTMWUK5uPJxby2FoPsWXlwLq_I5U,15016
46
44
  imas/backends/imas_core/uda_support.py,sha256=lxHzby6Fkkpn5j6yWpQrlqlwng-gA9O_hVK03Wf_Ij8,1770
47
45
  imas/backends/netcdf/__init__.py,sha256=T1xOq4F7fJtFKYtzQQwVOV_9r9plNH6kgpUsDx6vDN0,191
48
46
  imas/backends/netcdf/db_entry_nc.py,sha256=0y44SkvapH7QRJOa_wVMCEE7g6PpVH120MonPseBLmE,7260
@@ -59,7 +57,6 @@ imas/command/timer.py,sha256=kyYGOGVTzeYzq8FWAvLqlGS-vVM7lO6hTozh-0aBUBA,2328
59
57
  imas/test/test_all_dd_versions.py,sha256=8T9q9S7Vhwa96y6KUDv7HoeuOPlc6RovEhPZEseDoDs,790
60
58
  imas/test/test_cli.py,sha256=ZSQdO5t4n0wa2ohJNsRQEVftJOxyDIHsj_IUZxKuszA,1677
61
59
  imas/test/test_dbentry.py,sha256=fq49bh7fa2O7ocLiXiDSB5EEVII6Cx3zeoiU5uzjyo8,4025
62
- imas/test/test_dd_helpers.py,sha256=dV5m6J5EMXGij0UOtXDxD2St9sKrfGhixk-QlH1v2-E,1691
63
60
  imas/test/test_dd_zip.py,sha256=SgKPufIvk3tK_Q4JpHKq57g0lKthEj1IgcdZ2c-IzwI,820
64
61
  imas/test/test_deepcopy.py,sha256=vYTwpEqRpi-G-xVSRWVIwLL_jBY7f6v31QPOU0e_FKs,606
65
62
  imas/test/test_exception.py,sha256=bL_JlWsdtT-emKkSHx1yFbDKLqWOx8piCFBNHJ4osig,408
@@ -69,7 +66,7 @@ imas/test/test_helpers.py,sha256=ynhwAW5lotJfbscub_S2cppHWyruxDiT304ELyjjFIA,146
69
66
  imas/test/test_identifiers.py,sha256=UXjgZJBRYQVnHejpdug6umNcnutdgQkAnWuTNduxgig,3895
70
67
  imas/test/test_ids2nc.py,sha256=zLp0F6GW04gU6HYp63bWivFS3Let_dwMohYy6Rgbis0,6040
71
68
  imas/test/test_ids_ascii_data.py,sha256=apah_SmGXKuATDEq-ADePQejh9qW4gXphMbCGKA8X34,1116
72
- imas/test/test_ids_convert.py,sha256=H0sc6Cz6i2q-PXxhlg2o-P1Eifdj_w6JUQho7z-BtpM,16172
69
+ imas/test/test_ids_convert.py,sha256=syB6YPN0uqFPPownoXvbN8kbCedsDliOQ7Ar7mw5i7U,18511
73
70
  imas/test/test_ids_coordinates.py,sha256=d6o5wxNE-V_JFZ0Qq6Vz3UGC2KuhuVwVah06REnnbcI,6002
74
71
  imas/test/test_ids_data_type.py,sha256=VGQnJ-aeHzStJJQsnv3B2jr4rs33vRKKkOJxG-Szk-Y,1443
75
72
  imas/test/test_ids_factory.py,sha256=5izNPvMmpt64wgOxh2ResvzsRgE2ZVtaNZlYuU_27YY,1062
@@ -84,7 +81,7 @@ imas/test/test_ids_validate.py,sha256=0tRwmId61JFLlFmz-17DRKxntQtsKn_kRZULznt4d-
84
81
  imas/test/test_latest_dd_autofill.py,sha256=3SZD9J4jm_NQznJYT7ypnrmJCRAMV2iM8IJZnymrfJQ,2052
85
82
  imas/test/test_latest_dd_manual.py,sha256=XINCYCw3ZtbRch6Ug_gSPRyYXIg0dAorM7Y270kSYvI,1428
86
83
  imas/test/test_latest_dd_resample.py,sha256=ziPV-vReTiMr3is7l9-UHqVEyeHqinJ7PbXfK_0KO70,2859
87
- imas/test/test_lazy_loading.py,sha256=8cPDAeTnwrpwvr-7Fg1ycxVoA_tpVWdrDHvXCTsPvDo,8424
84
+ imas/test/test_lazy_loading.py,sha256=gOtIMSG3XVo95Bb6qyR1Zu8tog0cTLCiZPECWVNkBrk,8443
88
85
  imas/test/test_list_occurrences.py,sha256=Fdzf2ap8vODkg1xdpUPZ2_k1RxlBvcE0pih0FcX-W48,2986
89
86
  imas/test/test_minimal.py,sha256=NWfzH_zWOGzNmQ8lVTr-FyLTdadrnI82L6eRZJ26eWQ,1419
90
87
  imas/test/test_minimal_conversion.py,sha256=4wufUlbLZzAgh57VJVSgpjqd2ybrcaxcmgsmL7aJV4M,1678
@@ -105,9 +102,9 @@ imas/test/test_time_slicing.py,sha256=FwbdggRJ75Suc268-89daCSaVXhzl8pZZm3Od13EiR
105
102
  imas/test/test_to_xarray.py,sha256=9eXRRNq0M-tF2OOQzQxG-mXTsOX1JfPVTEM5JIixyrY,2867
106
103
  imas/test/test_uda_support.py,sha256=AmakuexebZfJ2-vk_DHfNRPwHmNM2RnwzXqT0eWwWJ0,338
107
104
  imas/test/test_util.py,sha256=Dbl07LhaBZiabm7yh9cfkSS4VkEFsBNoo3fmhQPAC1g,6814
108
- imas_python-2.0.0.dist-info/LICENSE.txt,sha256=RzxtV7MG0zbF6dDvQsaZ1OKsHZ7CRyeILenjUIol5rs,7651
109
- imas_python-2.0.0.dist-info/METADATA,sha256=WS_iO0u3qUEvicGX2oTyzSjjlAW9Ti_VW41yi9bouTc,13877
110
- imas_python-2.0.0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
111
- imas_python-2.0.0.dist-info/entry_points.txt,sha256=e8oXj2SLwyFEiBeJRaAXRc9EYQ8eHWWn7NWdjixjeDM,99
112
- imas_python-2.0.0.dist-info/top_level.txt,sha256=_WFOlA1rmpYm0T73sNo5Ntvs7S3x4cR7UE8Oy5I5eJA,5
113
- imas_python-2.0.0.dist-info/RECORD,,
105
+ imas_python-2.0.1.dist-info/licenses/LICENSE.txt,sha256=RzxtV7MG0zbF6dDvQsaZ1OKsHZ7CRyeILenjUIol5rs,7651
106
+ imas_python-2.0.1.dist-info/METADATA,sha256=7pMSO_q4pKcVkNEdqrGhuQDnP2OOoF6rIhx2UjtAa28,14168
107
+ imas_python-2.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
+ imas_python-2.0.1.dist-info/entry_points.txt,sha256=PI0yiL4MmzUS-sIQjt4gSEnBCdL-rB_XHGASKNsTf6g,46
109
+ imas_python-2.0.1.dist-info/top_level.txt,sha256=_WFOlA1rmpYm0T73sNo5Ntvs7S3x4cR7UE8Oy5I5eJA,5
110
+ imas_python-2.0.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ imas = imas.command.cli:cli
imas/assets/IDSDef.zip DELETED
Binary file
imas/dd_helpers.py DELETED
@@ -1,167 +0,0 @@
1
- # This file is part of IMAS-Python.
2
- # You should have received the IMAS-Python LICENSE file with this project.
3
- """Helper functions to build IDSDef.xml"""
4
-
5
- import logging
6
- import os
7
- import shutil
8
- from pathlib import Path
9
- from typing import Tuple
10
- from zipfile import ZIP_DEFLATED, ZipFile
11
-
12
- from packaging.version import Version as V
13
- from saxonche import PySaxonProcessor
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
- _idsdef_zip_relpath = Path("imas/assets/IDSDef.zip")
18
- _build_dir = Path("build")
19
-
20
-
21
- def prepare_data_dictionaries():
22
- """Build IMAS IDSDef.xml files for each tagged version in the DD repository
23
- 1. Use saxonche for transformations
24
- 2. Clone the DD repository (ask for user/pass unless ssh key access is available)
25
- 3. Generate IDSDef.xml and rename to IDSDef_${version}.xml
26
- 4. Zip all these IDSDefs together and include in wheel
27
- """
28
- from git import Repo
29
-
30
- repo: Repo = get_data_dictionary_repo()
31
- if repo:
32
- newest_version_and_tag = (V("0"), None)
33
- for tag in repo.tags:
34
- version_and_tag = (V(str(tag)), tag)
35
- if V(str(tag)) > V("3.21.1"):
36
- newest_version_and_tag = max(newest_version_and_tag, version_and_tag)
37
- logger.debug("Building data dictionary version %s", tag)
38
- build_data_dictionary(repo, tag)
39
-
40
- logger.info("Creating zip file of DD versions")
41
-
42
- if _idsdef_zip_relpath.is_file():
43
- logger.warning("Overwriting '%s'", _idsdef_zip_relpath)
44
-
45
- with ZipFile(
46
- _idsdef_zip_relpath,
47
- mode="w", # this needs w, since zip can have multiple same entries
48
- compression=ZIP_DEFLATED,
49
- ) as dd_zip:
50
- for filename in _build_dir.glob("[0-9]*.xml"):
51
- arcname = Path("data-dictionary").joinpath(*filename.parts[1:])
52
- dd_zip.write(filename, arcname=arcname)
53
- # Include identifiers from latest tag in zip file
54
- repo.git.checkout(newest_version_and_tag[1], force=True)
55
- # DD layout <= 4.0.0
56
- for filename in Path("data-dictionary").glob("*/*identifier.xml"):
57
- arcname = Path("identifiers").joinpath(*filename.parts[1:])
58
- dd_zip.write(filename, arcname=arcname)
59
- # DD layout > 4.0.0
60
- for filename in Path("data-dictionary").glob("schemas/*/*identifier.xml"):
61
- arcname = Path("identifiers").joinpath(*filename.parts[2:])
62
- dd_zip.write(filename, arcname=arcname)
63
-
64
-
65
- def get_data_dictionary_repo() -> Tuple[bool, bool]:
66
- try:
67
- import git # Import git here, the user might not have it!
68
- except ModuleNotFoundError:
69
- raise RuntimeError(
70
- "Could not find 'git' module, try 'pip install gitpython'. \
71
- Will not build Data Dictionaries!"
72
- )
73
-
74
- # We need the actual source code (for now) so grab it from ITER
75
- dd_repo_path = "data-dictionary"
76
-
77
- if "DD_DIRECTORY" in os.environ:
78
- logger.info("Found DD_DIRECTORY, copying")
79
- try:
80
- shutil.copytree(os.environ["DD_DIRECTORY"], dd_repo_path)
81
- except FileExistsError:
82
- pass
83
- else:
84
- logger.info("Trying to pull data dictionary git repo from ITER")
85
-
86
- # Set up a bare repo and fetch the data-dictionary repository in it
87
- os.makedirs(dd_repo_path, exist_ok=True)
88
- try:
89
- repo = git.Repo(dd_repo_path)
90
- except git.exc.InvalidGitRepositoryError:
91
- repo = git.Repo.init(dd_repo_path)
92
- logger.info("Set up local git repository {!s}".format(repo))
93
-
94
- try:
95
- origin = repo.remote()
96
- except ValueError:
97
- dd_repo_url = "https://github.com/iterorganization/imas-data-dictionary.git"
98
- origin = repo.create_remote("origin", url=dd_repo_url)
99
- logger.info("Set up remote '{!s}' linking to '{!s}'".format(origin, origin.url))
100
-
101
- try:
102
- origin.fetch(tags=True)
103
- except git.exc.GitCommandError as ee:
104
- logger.warning(
105
- "Could not fetch tags from %s. Git reports:\n %s." "\nTrying to continue",
106
- list(origin.urls),
107
- ee,
108
- )
109
- else:
110
- logger.info("Remote tags fetched")
111
- return repo
112
-
113
-
114
- def _run_xsl_transformation(
115
- xsd_file: Path, xsl_file: Path, tag: str, output_file: Path
116
- ) -> None:
117
- """
118
- This function performs an XSL transformation using Saxon-HE (saxonche)
119
- with the provided XSD file, XSL file, tag, and output file.
120
-
121
- Args:
122
- xsd_file (Path): XML Schema Definition (XSD) file
123
- xsl_file (Path): The `xsl_file` parameter
124
- tag (str): tag name to provide to 'DD_GIT_DESCRIBE' parameter
125
- output_file (Path): The `output_file` parameter for resulting xml
126
- """
127
- with PySaxonProcessor(license=False) as proc:
128
- logger.debug("Initializing Saxon Processor")
129
- xsltproc = proc.new_xslt30_processor()
130
- xdm_ddgit = proc.make_string_value(tag)
131
- xsltproc.set_parameter("DD_GIT_DESCRIBE", xdm_ddgit)
132
- xsltproc.transform_to_file(
133
- source_file=str(xsd_file),
134
- stylesheet_file=str(xsl_file),
135
- output_file=str(output_file),
136
- )
137
-
138
-
139
- def build_data_dictionary(repo, tag: str, rebuild=False) -> None:
140
- """Build a single version of the data dictionary given by the tag argument
141
- if the IDS does not already exist.
142
-
143
- In the data-dictionary repository sometimes IDSDef.xml is stored
144
- directly, in which case we do not call make.
145
-
146
- Args:
147
- repo: Repository object containing the DD source code
148
- tag: The DD version tag that will be build
149
- rebuild: If true, overwrites existing pre-build tagged DD version
150
- """
151
- _build_dir.mkdir(exist_ok=True)
152
- result_xml = _build_dir / f"{tag}.xml"
153
-
154
- if result_xml.exists() and not rebuild:
155
- logger.debug(f"XML for tag '{tag}' already exists, skipping")
156
- return
157
-
158
- repo.git.checkout(tag, force=True)
159
-
160
- # Perform the XSL transformation with saxonche
161
- dd_xsd = Path("data-dictionary/dd_data_dictionary.xml.xsd")
162
- dd_xsl = Path("data-dictionary/dd_data_dictionary.xml.xsl")
163
- _run_xsl_transformation(dd_xsd, dd_xsl, tag.name, result_xml)
164
-
165
-
166
- if __name__ == "__main__":
167
- prepare_data_dictionaries()
@@ -1,53 +0,0 @@
1
- from pathlib import Path
2
- import shutil
3
- import pytest
4
- import os
5
- import zipfile
6
-
7
- from imas.dd_helpers import prepare_data_dictionaries, _idsdef_zip_relpath, _build_dir
8
-
9
- _idsdef_unzipped_relpath = Path("idsdef_unzipped")
10
-
11
-
12
- @pytest.mark.skip(reason="skipping IDSDef.zip generation")
13
- def test_prepare_data_dictionaries():
14
- prepare_data_dictionaries()
15
- assert os.path.exists(
16
- _idsdef_zip_relpath
17
- ), f"IDSDef.zip file does not exist at path: {_idsdef_zip_relpath}"
18
-
19
- expected_xml_files = [
20
- _build_dir / "3.40.0.xml",
21
- _build_dir / "3.41.0.xml",
22
- _build_dir / "3.42.0.xml",
23
- _build_dir / "4.0.0.xml",
24
- ]
25
-
26
- for xml_file in expected_xml_files:
27
- assert os.path.exists(xml_file), f"{xml_file} does not exist"
28
-
29
- with zipfile.ZipFile(_idsdef_zip_relpath, "r") as zip_ref:
30
- zip_ref.extractall(_idsdef_unzipped_relpath)
31
-
32
- expected_ids_directories = [
33
- _idsdef_unzipped_relpath / "data-dictionary" / "3.40.0.xml",
34
- _idsdef_unzipped_relpath / "data-dictionary" / "3.41.0.xml",
35
- _idsdef_unzipped_relpath / "data-dictionary" / "3.42.0.xml",
36
- _idsdef_unzipped_relpath / "data-dictionary" / "4.0.0.xml",
37
- _idsdef_unzipped_relpath
38
- / "identifiers"
39
- / "core_sources"
40
- / "core_source_identifier.xml",
41
- _idsdef_unzipped_relpath
42
- / "identifiers"
43
- / "equilibrium"
44
- / "equilibrium_profiles_2d_identifier.xml",
45
- ]
46
-
47
- for file_path in expected_ids_directories:
48
- assert os.path.exists(
49
- file_path
50
- ), f"Expected_ids_directories {file_path} does not exist"
51
-
52
- if _build_dir.exists():
53
- shutil.rmtree(_idsdef_unzipped_relpath)
@@ -1,3 +0,0 @@
1
- [console_scripts]
2
- build_DD = imas.dd_helpers:prepare_data_dictionaries
3
- imas = imas.command.cli:cli