imas-python 2.0.0.post1__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 +0 -1
- imas/_version.py +2 -2
- imas/backends/imas_core/al_context.py +7 -4
- imas/backends/imas_core/db_entry_al.py +4 -2
- imas/backends/imas_core/mdsplus_model.py +10 -2
- imas/dd_zip.py +9 -197
- imas/exception.py +6 -17
- imas/ids_convert.py +170 -58
- imas/test/test_ids_convert.py +89 -22
- imas/test/test_lazy_loading.py +1 -1
- {imas_python-2.0.0.post1.dist-info → imas_python-2.0.1.dist-info}/METADATA +19 -10
- {imas_python-2.0.0.post1.dist-info → imas_python-2.0.1.dist-info}/RECORD +16 -19
- {imas_python-2.0.0.post1.dist-info → imas_python-2.0.1.dist-info}/WHEEL +1 -1
- imas_python-2.0.1.dist-info/entry_points.txt +2 -0
- imas/assets/IDSDef.zip +0 -0
- imas/dd_helpers.py +0 -167
- imas/test/test_dd_helpers.py +0 -53
- imas_python-2.0.0.post1.dist-info/entry_points.txt +0 -3
- {imas_python-2.0.0.post1.dist-info → imas_python-2.0.1.dist-info/licenses}/LICENSE.txt +0 -0
- {imas_python-2.0.0.post1.dist-info → imas_python-2.0.1.dist-info}/top_level.txt +0 -0
imas/__init__.py
CHANGED
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.
|
|
21
|
-
__version_tuple__ = version_tuple = (2, 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
493
|
-
if
|
|
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(
|
|
502
|
-
return
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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,
|
|
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
|
imas/test/test_ids_convert.py
CHANGED
|
@@ -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
|
|
291
|
-
assert
|
|
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
|
|
294
|
-
assert
|
|
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
|
|
302
|
-
assert
|
|
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
|
|
305
|
-
assert
|
|
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
|
|
314
|
-
assert
|
|
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
|
|
317
|
-
assert
|
|
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
|
|
326
|
+
assert array_equal(thickness, [1, 0.9])
|
|
326
327
|
else: # closed outline, thickness values kept
|
|
327
|
-
assert
|
|
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
|
|
344
|
-
assert
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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")
|
imas/test/test_lazy_loading.py
CHANGED
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: imas-python
|
|
3
|
-
Version: 2.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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
5
|
+
imas/_version.py,sha256=0PAtIHDEagdGmwgz1nnz5zUKeyYjsxtV8Nbsv7dXl8w,511
|
|
6
6
|
imas/db_entry.py,sha256=GgzNXbU-4gL5ac2vVo28LtnF3JIKIqyQzg1MIhyMkkQ,31755
|
|
7
|
-
imas/
|
|
8
|
-
imas/
|
|
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=
|
|
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=nm865sXPDExMeOLLBXc-z10qs8UDC4i-3_63yBdvVc8,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=
|
|
42
|
-
imas/backends/imas_core/db_entry_al.py,sha256
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
109
|
-
imas_python-2.0.
|
|
110
|
-
imas_python-2.0.
|
|
111
|
-
imas_python-2.0.
|
|
112
|
-
imas_python-2.0.
|
|
113
|
-
imas_python-2.0.
|
|
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,,
|
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()
|
imas/test/test_dd_helpers.py
DELETED
|
@@ -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)
|
|
File without changes
|
|
File without changes
|