imas-python 0.1.dev1618__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of imas-python might be problematic. Click here for more details.
- imas/__init__.py +34 -0
- imas/__main__.py +17 -0
- imas/_util.py +260 -0
- imas/_version.py +16 -0
- imas/assets/IDSDef2MDSpreTree.xsl +161 -0
- imas/assets/IDS_fake_toplevel.xml +318 -0
- imas/assets/IDS_minimal.xml +37 -0
- imas/assets/IDS_minimal_2.xml +37 -0
- imas/assets/IDS_minimal_struct_array.xml +43 -0
- imas/assets/IDS_minimal_types.xml +169 -0
- imas/assets/ITER_134173_106_core_profiles.ids +2438 -0
- imas/assets/ITER_134173_106_equilibrium.ids +3172 -0
- imas/assets/README.md +5 -0
- imas/assets/core_profiles.ids +2438 -0
- imas/assets/equilibrium.ids +3172 -0
- imas/backends/__init__.py +12 -0
- imas/backends/db_entry_impl.py +122 -0
- imas/backends/imas_core/__init__.py +4 -0
- imas/backends/imas_core/al_context.py +391 -0
- imas/backends/imas_core/db_entry_al.py +405 -0
- imas/backends/imas_core/db_entry_helpers.py +200 -0
- imas/backends/imas_core/imas_interface.py +216 -0
- imas/backends/imas_core/mdsplus_model.py +367 -0
- imas/backends/imas_core/uda_support.py +56 -0
- imas/backends/netcdf/__init__.py +4 -0
- imas/backends/netcdf/db_entry_nc.py +176 -0
- imas/backends/netcdf/ids2nc.py +306 -0
- imas/backends/netcdf/nc2ids.py +302 -0
- imas/backends/netcdf/nc_metadata.py +357 -0
- imas/backends/netcdf/nc_validate.py +53 -0
- imas/command/cli.py +239 -0
- imas/command/db_analysis.py +290 -0
- imas/command/helpers.py +45 -0
- imas/command/timer.py +77 -0
- imas/db_entry.py +802 -0
- imas/dd_helpers.py +300 -0
- imas/dd_zip.py +297 -0
- imas/exception.py +107 -0
- imas/ids_base.py +149 -0
- imas/ids_convert.py +920 -0
- imas/ids_coordinates.py +400 -0
- imas/ids_data_type.py +115 -0
- imas/ids_defs.py +171 -0
- imas/ids_factory.py +108 -0
- imas/ids_identifiers.py +125 -0
- imas/ids_metadata.py +307 -0
- imas/ids_path.py +254 -0
- imas/ids_primitive.py +510 -0
- imas/ids_struct_array.py +233 -0
- imas/ids_structure.py +331 -0
- imas/ids_toplevel.py +381 -0
- imas/setup_logging.py +64 -0
- imas/test/test_all_dd_versions.py +28 -0
- imas/test/test_cli.py +44 -0
- imas/test/test_dbentry.py +104 -0
- imas/test/test_dd_helpers.py +134 -0
- imas/test/test_dd_zip.py +29 -0
- imas/test/test_deepcopy.py +25 -0
- imas/test/test_exception.py +12 -0
- imas/test/test_get_sample.py +388 -0
- imas/test/test_hash.py +188 -0
- imas/test/test_helpers.py +354 -0
- imas/test/test_identifiers.py +105 -0
- imas/test/test_ids2nc.py +166 -0
- imas/test/test_ids_ascii_data.py +39 -0
- imas/test/test_ids_convert.py +415 -0
- imas/test/test_ids_coordinates.py +165 -0
- imas/test/test_ids_data_type.py +40 -0
- imas/test/test_ids_factory.py +35 -0
- imas/test/test_ids_metadata.py +66 -0
- imas/test/test_ids_mixin.py +20 -0
- imas/test/test_ids_path.py +192 -0
- imas/test/test_ids_primitive.py +109 -0
- imas/test/test_ids_struct_array.py +89 -0
- imas/test/test_ids_structure.py +70 -0
- imas/test/test_ids_toplevel.py +55 -0
- imas/test/test_ids_validate.py +343 -0
- imas/test/test_latest_dd_autofill.py +74 -0
- imas/test/test_latest_dd_manual.py +38 -0
- imas/test/test_latest_dd_resample.py +102 -0
- imas/test/test_lazy_loading.py +201 -0
- imas/test/test_list_occurrences.py +82 -0
- imas/test/test_minimal.py +37 -0
- imas/test/test_minimal_conversion.py +44 -0
- imas/test/test_minimal_io.py +21 -0
- imas/test/test_minimal_struct_array_io.py +60 -0
- imas/test/test_minimal_types.py +436 -0
- imas/test/test_minimal_types_io.py +70 -0
- imas/test/test_minimal_types_io_automatic.py +66 -0
- imas/test/test_nbc_change.py +408 -0
- imas/test/test_nc_autofill.py +13 -0
- imas/test/test_nc_entry.py +28 -0
- imas/test/test_nc_metadata.py +153 -0
- imas/test/test_nc_validation.py +197 -0
- imas/test/test_snippets.py +25 -0
- imas/test/test_static_ids.py +39 -0
- imas/test/test_str_1d.py +99 -0
- imas/test/test_time_slicing.py +182 -0
- imas/test/test_uda_support.py +12 -0
- imas/test/test_util.py +209 -0
- imas/training.py +45 -0
- imas/util.py +526 -0
- imas_python-0.1.dev1618.dist-info/LICENSE.txt +165 -0
- imas_python-0.1.dev1618.dist-info/METADATA +288 -0
- imas_python-0.1.dev1618.dist-info/RECORD +108 -0
- imas_python-0.1.dev1618.dist-info/WHEEL +5 -0
- imas_python-0.1.dev1618.dist-info/entry_points.txt +3 -0
- imas_python-0.1.dev1618.dist-info/top_level.txt +1 -0
imas/ids_convert.py
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
# This file is part of imas-python.
|
|
2
|
+
# You should have received the imas-python LICENSE file with this project.
|
|
3
|
+
"""Functionality for converting IDSToplevels between DD versions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
import datetime
|
|
8
|
+
import logging
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, Dict, Iterator, Optional, Set, Tuple
|
|
12
|
+
from xml.etree.ElementTree import Element, ElementTree
|
|
13
|
+
|
|
14
|
+
import numpy
|
|
15
|
+
from packaging.version import InvalidVersion, Version
|
|
16
|
+
|
|
17
|
+
import imas
|
|
18
|
+
from imas.dd_zip import parse_dd_version
|
|
19
|
+
from imas.ids_base import IDSBase
|
|
20
|
+
from imas.ids_data_type import IDSDataType
|
|
21
|
+
from imas.ids_factory import IDSFactory
|
|
22
|
+
from imas.ids_path import IDSPath
|
|
23
|
+
from imas.ids_primitive import (
|
|
24
|
+
IDSNumeric0D,
|
|
25
|
+
IDSNumericArray,
|
|
26
|
+
IDSPrimitive,
|
|
27
|
+
IDSString0D,
|
|
28
|
+
)
|
|
29
|
+
from imas.ids_struct_array import IDSStructArray
|
|
30
|
+
from imas.ids_structure import IDSStructure
|
|
31
|
+
from imas.ids_toplevel import IDSToplevel
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def iter_parents(path: str) -> Iterator[str]:
|
|
37
|
+
"""Iterate over parents of this path, starting with the highest-level parent.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> list(iter_parents("abc/def/ghi"))
|
|
41
|
+
["abc", "abc/def"]
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
path: Path to get parents of.
|
|
45
|
+
|
|
46
|
+
Yields:
|
|
47
|
+
Parent paths of the provided path.
|
|
48
|
+
"""
|
|
49
|
+
i_slash = path.find("/")
|
|
50
|
+
while i_slash != -1:
|
|
51
|
+
yield path[:i_slash]
|
|
52
|
+
i_slash = path.find("/", i_slash + 1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NBCPathMap:
|
|
56
|
+
"""Object mapping paths in one DD version to path, timebasepath and context path."""
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self.path: Dict[str, Optional[str]] = {}
|
|
60
|
+
"""Dictionary mapping the ids path
|
|
61
|
+
|
|
62
|
+
- When no changes have occurred (which is assumed to be the default case), the
|
|
63
|
+
path is not present in the dictionary.
|
|
64
|
+
- When an element is renamed it maps the old to the new name (and vice versa).
|
|
65
|
+
- When an element does not exist in the other version, it is mapped to None.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
self.tbp: Dict[str, str] = {}
|
|
69
|
+
"""Map providing the timebasepath for renamed elements."""
|
|
70
|
+
|
|
71
|
+
self.ctxpath: Dict[str, str] = {}
|
|
72
|
+
"""Map providing the lowlevel context path for renamed elements."""
|
|
73
|
+
|
|
74
|
+
self.type_change: Dict[str, Optional[Callable[[IDSBase, IDSBase], None]]] = {}
|
|
75
|
+
"""Dictionary of paths that had a type change.
|
|
76
|
+
|
|
77
|
+
Type changes are mapped to None in :py:attr:`path`, this ``dict`` allows to
|
|
78
|
+
distinguish between a type change and a removed node.
|
|
79
|
+
|
|
80
|
+
Optionally, a function can be set to the path. This function can be called to
|
|
81
|
+
perform a supported conversion between the new and old type (or vice versa).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
self.post_process: Dict[str, Callable[[IDSBase], None]] = {}
|
|
85
|
+
"""Map providing postprocess functions for paths.
|
|
86
|
+
|
|
87
|
+
The postprocess function should be applied to the nodes after all the data is
|
|
88
|
+
converted.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
self.ignore_missing_paths: Set[str] = set()
|
|
92
|
+
"""Set of paths that should not be logged when data is present."""
|
|
93
|
+
|
|
94
|
+
def __setitem__(self, path: str, value: Tuple[Optional[str], str, str]) -> None:
|
|
95
|
+
self.path[path], self.tbp[path], self.ctxpath[path] = value
|
|
96
|
+
|
|
97
|
+
def __contains__(self, path: str) -> bool:
|
|
98
|
+
return path in self.path
|
|
99
|
+
|
|
100
|
+
def __iter__(self) -> Iterator[str]:
|
|
101
|
+
return iter(self.path)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Expected typical use case is conversion between two versions only. With 74 IDSs
|
|
105
|
+
# (DD 3.39.0) a cache of 128 items should be big enough.
|
|
106
|
+
@lru_cache(maxsize=128)
|
|
107
|
+
def _DDVersionMap(*args) -> "DDVersionMap":
|
|
108
|
+
return DDVersionMap(*args)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class DDVersionMap:
|
|
112
|
+
"""Mapping of an IDS between two Data Dictionary versions."""
|
|
113
|
+
|
|
114
|
+
RENAMED_DESCRIPTIONS = {"aos_renamed", "leaf_renamed", "structure_renamed"}
|
|
115
|
+
"""Supported ``nbc_description`` values for renames."""
|
|
116
|
+
STRUCTURE_TYPES = {"structure", "struct_array"}
|
|
117
|
+
"""Data dictionary ``data_type`` corresponding to structure-like types."""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
ids_name: str,
|
|
122
|
+
old_version: ElementTree,
|
|
123
|
+
new_version: ElementTree,
|
|
124
|
+
version_old: Version,
|
|
125
|
+
):
|
|
126
|
+
self.ids_name = ids_name
|
|
127
|
+
self.old_version = old_version
|
|
128
|
+
self.new_version = new_version
|
|
129
|
+
self.version_old = version_old
|
|
130
|
+
|
|
131
|
+
self.old_to_new = NBCPathMap()
|
|
132
|
+
self.new_to_old = NBCPathMap()
|
|
133
|
+
|
|
134
|
+
old_ids_object = old_version.find(f"IDS[@name='{ids_name}']")
|
|
135
|
+
new_ids_object = new_version.find(f"IDS[@name='{ids_name}']")
|
|
136
|
+
if old_ids_object is None or new_ids_object is None:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"Cannot find IDS {ids_name} in the provided DD definitions."
|
|
139
|
+
)
|
|
140
|
+
self._build_map(old_ids_object, new_ids_object)
|
|
141
|
+
|
|
142
|
+
def _check_data_type(self, old_item: Element, new_item: Element):
|
|
143
|
+
"""Check if data type hasn't changed.
|
|
144
|
+
|
|
145
|
+
Record paths in mapping if data type change is unsupported.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
True iff the data type of both items are the same.
|
|
149
|
+
"""
|
|
150
|
+
new_data_type = new_item.get("data_type")
|
|
151
|
+
old_data_type = old_item.get("data_type")
|
|
152
|
+
if IDSDataType.parse(new_data_type) == IDSDataType.parse(old_data_type):
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
# Data type changed, record in type_change sets
|
|
156
|
+
new_path = new_item.get("path")
|
|
157
|
+
old_path = old_item.get("path")
|
|
158
|
+
assert new_path is not None
|
|
159
|
+
assert old_path is not None
|
|
160
|
+
|
|
161
|
+
# Check if we (maybe) support this type change
|
|
162
|
+
data_types = sorted([new_data_type, old_data_type])
|
|
163
|
+
if data_types == ["struct_array", "structure"]:
|
|
164
|
+
self.new_to_old.type_change[new_path] = _type_changed_structure_aos
|
|
165
|
+
self.old_to_new.type_change[old_path] = _type_changed_structure_aos
|
|
166
|
+
elif data_types in (
|
|
167
|
+
["INT_0D", "INT_1D"],
|
|
168
|
+
["FLT_0D", "FLT_1D"],
|
|
169
|
+
["CPX_0D", "CPX_1D"],
|
|
170
|
+
["STR_0D", "STR_1D"],
|
|
171
|
+
):
|
|
172
|
+
self.new_to_old.type_change[new_path] = _type_changed_0d_1d_data
|
|
173
|
+
self.old_to_new.type_change[old_path] = _type_changed_0d_1d_data
|
|
174
|
+
elif old_data_type == "INT_1D" and new_item.get("doc_identifier"):
|
|
175
|
+
# DD3to4: Support change of grid_ggd/grid/space/coordinates_type from an
|
|
176
|
+
# INT_1D to a proper identifier
|
|
177
|
+
self.new_to_old.type_change[new_path] = _type_changed_to_identifier
|
|
178
|
+
self.old_to_new.type_change[old_path] = _type_changed_to_identifier
|
|
179
|
+
elif data_types == ["FLT_0D", "INT_0D"]:
|
|
180
|
+
# Support conversion of FLT_0D -> INT_0D when the float is integral
|
|
181
|
+
self.new_to_old.type_change[new_path] = _type_changed_flt0d_int0d
|
|
182
|
+
self.old_to_new.type_change[old_path] = _type_changed_flt0d_int0d
|
|
183
|
+
else:
|
|
184
|
+
logger.debug(
|
|
185
|
+
"Data type of %s changed from %s to %s. This change is not "
|
|
186
|
+
"supported by imas-python: no conversion will be done.",
|
|
187
|
+
new_item.get("path"),
|
|
188
|
+
old_item.get("data_type"),
|
|
189
|
+
new_item.get("data_type"),
|
|
190
|
+
)
|
|
191
|
+
self.new_to_old.path[new_path] = None
|
|
192
|
+
self.new_to_old.type_change[new_path] = None
|
|
193
|
+
self.old_to_new.path[old_path] = None
|
|
194
|
+
self.old_to_new.type_change[old_path] = None
|
|
195
|
+
return False
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
def _build_map(self, old: Element, new: Element) -> None:
|
|
199
|
+
"""Build the NBC translation map between old <-> new."""
|
|
200
|
+
old_paths = {field.get("path", ""): field for field in old.iterfind(".//field")}
|
|
201
|
+
new_paths = {field.get("path", ""): field for field in new.iterfind(".//field")}
|
|
202
|
+
old_path_set = set(old_paths)
|
|
203
|
+
new_path_set = set(new_paths)
|
|
204
|
+
|
|
205
|
+
def process_parent_renames(path: str) -> str:
|
|
206
|
+
# Apply any parent AoS/structure rename
|
|
207
|
+
# Loop in reverse order to find the closest parent which was renamed:
|
|
208
|
+
for parent in reversed(list(iter_parents(path))):
|
|
209
|
+
parent_rename = self.new_to_old.path.get(parent)
|
|
210
|
+
if parent_rename:
|
|
211
|
+
if new_paths[parent].get("data_type") in self.STRUCTURE_TYPES:
|
|
212
|
+
path = path.replace(parent, parent_rename, 1)
|
|
213
|
+
break
|
|
214
|
+
return path
|
|
215
|
+
|
|
216
|
+
def get_old_path(path: str, previous_name: str) -> str:
|
|
217
|
+
"""Calculate old path from the path and change_nbc_previous_name"""
|
|
218
|
+
# Apply rename
|
|
219
|
+
i_slash = path.rfind("/")
|
|
220
|
+
if i_slash != -1:
|
|
221
|
+
old_path = path[:i_slash] + "/" + previous_name
|
|
222
|
+
else:
|
|
223
|
+
old_path = previous_name
|
|
224
|
+
return process_parent_renames(old_path)
|
|
225
|
+
|
|
226
|
+
def add_rename(old_path: str, new_path: str):
|
|
227
|
+
old_item = old_paths[old_path]
|
|
228
|
+
new_item = new_paths[new_path]
|
|
229
|
+
self.new_to_old[new_path] = (
|
|
230
|
+
old_path,
|
|
231
|
+
_get_tbp(old_item, old_paths),
|
|
232
|
+
_get_ctxpath(old_path, old_paths),
|
|
233
|
+
)
|
|
234
|
+
self.old_to_new[old_path] = (
|
|
235
|
+
new_path,
|
|
236
|
+
_get_tbp(new_item, new_paths),
|
|
237
|
+
_get_ctxpath(new_path, new_paths),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Iterate through all NBC metadata and add entries
|
|
241
|
+
for new_item in new.iterfind(".//field[@change_nbc_description]"):
|
|
242
|
+
new_path = new_item.get("path")
|
|
243
|
+
assert new_path is not None
|
|
244
|
+
nbc_description = new_item.get("change_nbc_description")
|
|
245
|
+
# change_nbc_version may be a comma-separated list of versions
|
|
246
|
+
# the only supported case is multiple renames in succession
|
|
247
|
+
nbc_version = new_item.get("change_nbc_version")
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
parsed_nbc_versions = [
|
|
251
|
+
Version(version) for version in nbc_version.split(",")
|
|
252
|
+
]
|
|
253
|
+
except InvalidVersion:
|
|
254
|
+
log_args = (nbc_version, new_path)
|
|
255
|
+
logger.error("Ignoring invalid NBC version: %r for %r.", *log_args)
|
|
256
|
+
continue
|
|
257
|
+
assert sorted(parsed_nbc_versions) == parsed_nbc_versions
|
|
258
|
+
|
|
259
|
+
if parsed_nbc_versions[-1] <= self.version_old:
|
|
260
|
+
continue
|
|
261
|
+
if nbc_description in DDVersionMap.RENAMED_DESCRIPTIONS:
|
|
262
|
+
previous_names = new_item.get("change_nbc_previous_name").split(",")
|
|
263
|
+
assert len(previous_names) == len(parsed_nbc_versions)
|
|
264
|
+
# select the correct previous name:
|
|
265
|
+
for i, version in enumerate(parsed_nbc_versions):
|
|
266
|
+
if version > self.version_old:
|
|
267
|
+
previous_name = previous_names[i]
|
|
268
|
+
break
|
|
269
|
+
old_path = get_old_path(new_path, previous_name)
|
|
270
|
+
old_item = old_paths.get(old_path)
|
|
271
|
+
if old_item is None:
|
|
272
|
+
logger.debug(
|
|
273
|
+
"Skipped NBC change for %r: renamed path %r not found in %s.",
|
|
274
|
+
new_path,
|
|
275
|
+
old_path,
|
|
276
|
+
self.version_old,
|
|
277
|
+
)
|
|
278
|
+
elif self._check_data_type(old_item, new_item):
|
|
279
|
+
add_rename(old_path, new_path)
|
|
280
|
+
if old_item.get("data_type") in DDVersionMap.STRUCTURE_TYPES:
|
|
281
|
+
# Add entries for common sub-elements
|
|
282
|
+
for path in old_paths:
|
|
283
|
+
if path.startswith(old_path):
|
|
284
|
+
npath = path.replace(old_path, new_path, 1)
|
|
285
|
+
if npath in new_path_set:
|
|
286
|
+
add_rename(path, npath)
|
|
287
|
+
elif nbc_description == "type_changed":
|
|
288
|
+
pass # We will handle this (if possible) in self._check_data_type
|
|
289
|
+
elif nbc_description == "repeat_children_first_point":
|
|
290
|
+
old_path = process_parent_renames(new_path)
|
|
291
|
+
self.new_to_old.post_process[new_path] = _remove_last_point
|
|
292
|
+
self.old_to_new.post_process[old_path] = _repeat_first_point
|
|
293
|
+
elif nbc_description == "repeat_children_first_point_conditional":
|
|
294
|
+
# This conversion needs access to the old data structure to get the
|
|
295
|
+
# DDv3 `closed` node which is removed in DDv4, so we handle it as a type
|
|
296
|
+
# change:
|
|
297
|
+
old_path = process_parent_renames(new_path)
|
|
298
|
+
self.new_to_old.type_change[new_path] = _remove_last_point_conditional
|
|
299
|
+
self.old_to_new.type_change[old_path] = _repeat_first_point_conditional
|
|
300
|
+
self.old_to_new.ignore_missing_paths.add(f"{old_path}/closed")
|
|
301
|
+
elif nbc_description in (
|
|
302
|
+
"repeat_children_first_point_conditional_sibling",
|
|
303
|
+
"repeat_children_first_point_conditional_sibling_dynamic",
|
|
304
|
+
):
|
|
305
|
+
old_path = process_parent_renames(new_path)
|
|
306
|
+
self.new_to_old.type_change[new_path] = _remove_last_point_conditional
|
|
307
|
+
self.old_to_new.type_change[old_path] = _repeat_first_point_conditional
|
|
308
|
+
closed_path = Path(old_path).parent / "closed"
|
|
309
|
+
self.old_to_new.ignore_missing_paths.add(str(closed_path))
|
|
310
|
+
elif nbc_description == "remove_last_point_if_open_annular_centreline":
|
|
311
|
+
old_path = process_parent_renames(new_path)
|
|
312
|
+
self.new_to_old.type_change[new_path] = _repeat_last_point_centreline
|
|
313
|
+
self.old_to_new.type_change[old_path] = _remove_last_point_centreline
|
|
314
|
+
closed_path = Path(old_path) / "closed"
|
|
315
|
+
self.old_to_new.ignore_missing_paths.add(str(closed_path))
|
|
316
|
+
else: # Ignore unknown NBC changes
|
|
317
|
+
log_args = (nbc_description, new_path)
|
|
318
|
+
logger.error("Ignoring unsupported NBC change: %r for %r.", *log_args)
|
|
319
|
+
|
|
320
|
+
# Check if all common elements are still valid
|
|
321
|
+
for common_path in old_path_set & new_path_set:
|
|
322
|
+
if common_path in self.new_to_old or common_path in self.old_to_new:
|
|
323
|
+
continue # This path is part of an NBC change, we can skip it
|
|
324
|
+
self._check_data_type(old_paths[common_path], new_paths[common_path])
|
|
325
|
+
|
|
326
|
+
# Record missing items
|
|
327
|
+
self._map_missing(True, new_path_set.difference(old_path_set, self.new_to_old))
|
|
328
|
+
self._map_missing(False, old_path_set.difference(new_path_set, self.old_to_new))
|
|
329
|
+
|
|
330
|
+
new_version = None
|
|
331
|
+
new_version_node = self.new_version.find("version")
|
|
332
|
+
if new_version_node is not None:
|
|
333
|
+
new_version = parse_dd_version(new_version_node.text)
|
|
334
|
+
# Additional conversion rules for DDv3 to DDv4
|
|
335
|
+
if self.version_old.major == 3 and new_version and new_version.major == 4:
|
|
336
|
+
# 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
|
|
343
|
+
# Definition change for pf_active circuit/connections
|
|
344
|
+
if self.ids_name == "pf_active":
|
|
345
|
+
path = "circuit/connections"
|
|
346
|
+
self.new_to_old.post_process[path] = _circuit_connections_4to3
|
|
347
|
+
self.old_to_new.post_process[path] = _circuit_connections_3to4
|
|
348
|
+
# Migrate ids_properties/source to ids_properties/provenance
|
|
349
|
+
# Only implement forward conversion (DD3 -> 4):
|
|
350
|
+
# - Pretend that this is a rename from ids_properties/source -> provenance
|
|
351
|
+
# - And register type_change handler which will be called with the source
|
|
352
|
+
# element and the new provenance structure
|
|
353
|
+
path = "ids_properties/source"
|
|
354
|
+
self.old_to_new.path[path] = "ids_properties/provenance"
|
|
355
|
+
self.old_to_new.type_change[path] = _ids_properties_source
|
|
356
|
+
|
|
357
|
+
def _map_missing(self, is_new: bool, missing_paths: Set[str]):
|
|
358
|
+
rename_map = self.new_to_old if is_new else self.old_to_new
|
|
359
|
+
# Find all structures which have a renamed sub-item
|
|
360
|
+
structures_with_renames = set()
|
|
361
|
+
for path in rename_map:
|
|
362
|
+
for parent in iter_parents(path):
|
|
363
|
+
structures_with_renames.add(parent)
|
|
364
|
+
|
|
365
|
+
skipped_paths = set()
|
|
366
|
+
for path in sorted(missing_paths):
|
|
367
|
+
# Only mark a non-existing structure if there are no renames inside it, so a
|
|
368
|
+
# structure marked in the rename_map as None can be skipped completely.
|
|
369
|
+
if path not in structures_with_renames:
|
|
370
|
+
# Only mark if there is no parent structure already skipped
|
|
371
|
+
for parent in iter_parents(path):
|
|
372
|
+
if parent in skipped_paths:
|
|
373
|
+
break
|
|
374
|
+
else:
|
|
375
|
+
skipped_paths.add(path)
|
|
376
|
+
for path in skipped_paths:
|
|
377
|
+
rename_map.path[path] = None
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _get_ctxpath(path: str, paths: Dict[str, Element]) -> str:
|
|
381
|
+
"""Get the path of the nearest parent AoS."""
|
|
382
|
+
for parent_path in reversed(list(iter_parents(path))):
|
|
383
|
+
if paths[parent_path].get("data_type") == "struct_array":
|
|
384
|
+
return path[len(parent_path) + 1 :]
|
|
385
|
+
return path # no nearest parent AoS
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _get_tbp(element: Element, paths: Dict[str, Element]):
|
|
389
|
+
"""Calculate the timebasepath to use for the lowlevel."""
|
|
390
|
+
if element.get("data_type") == "struct_array":
|
|
391
|
+
# https://git.iter.org/projects/IMAS/repos/access-layer/browse/pythoninterface/py_ids.xsl?at=refs%2Ftags%2F4.11.4#367-384
|
|
392
|
+
if element.get("type") != "dynamic":
|
|
393
|
+
return ""
|
|
394
|
+
# Find path of first ancestor that is an AoS
|
|
395
|
+
path = element.get("path")
|
|
396
|
+
assert path is not None
|
|
397
|
+
return _get_ctxpath(path, paths) + "/time"
|
|
398
|
+
# https://git.iter.org/projects/IMAS/repos/access-layer/browse/pythoninterface/py_ids.xsl?at=refs%2Ftags%2F4.11.4#1524-1566
|
|
399
|
+
return element.get("timebasepath", "")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def dd_version_map_from_factories(
|
|
403
|
+
ids_name: str, factory1: IDSFactory, factory2: IDSFactory
|
|
404
|
+
) -> Tuple[DDVersionMap, bool]:
|
|
405
|
+
"""Build a DDVersionMap from two IDSFactories."""
|
|
406
|
+
assert factory1._version
|
|
407
|
+
assert factory2._version
|
|
408
|
+
factory1_version = parse_dd_version(factory1._version)
|
|
409
|
+
factory2_version = parse_dd_version(factory2._version)
|
|
410
|
+
old_version, old_factory, new_factory = min(
|
|
411
|
+
(factory1_version, factory1, factory2),
|
|
412
|
+
(factory2_version, factory2, factory1),
|
|
413
|
+
)
|
|
414
|
+
return (
|
|
415
|
+
_DDVersionMap(ids_name, old_factory._etree, new_factory._etree, old_version),
|
|
416
|
+
old_factory is factory1,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def convert_ids(
|
|
421
|
+
toplevel: IDSToplevel,
|
|
422
|
+
version: Optional[str],
|
|
423
|
+
*,
|
|
424
|
+
deepcopy: bool = False,
|
|
425
|
+
provenance_origin_uri: str = "",
|
|
426
|
+
xml_path: Optional[str] = None,
|
|
427
|
+
factory: Optional[IDSFactory] = None,
|
|
428
|
+
target: Optional[IDSToplevel] = None,
|
|
429
|
+
) -> IDSToplevel:
|
|
430
|
+
"""Convert an IDS to the specified data dictionary version.
|
|
431
|
+
|
|
432
|
+
Newer data dictionary versions may introduce non-backwards-compatible (NBC) changes.
|
|
433
|
+
For example, the ``global_quantities.power_from_plasma`` quantity in the ``wall``
|
|
434
|
+
IDS was renamed in DD version 3.31.0 to ``global_quantities.power_incident``. When
|
|
435
|
+
converting from a version older than 3.31.0 to a version that is newer than that,
|
|
436
|
+
this method will migrate the data.
|
|
437
|
+
|
|
438
|
+
By default, this method performs a `shallow copy` of numerical data. All
|
|
439
|
+
multi-dimensional numpy arrays from the returned IDS share their data with the
|
|
440
|
+
original IDS. When performing `in-place` operations on numpy arrays, the data will
|
|
441
|
+
be changed in both IDSs! If this is not desired, you may set the ``deepcopy``
|
|
442
|
+
keyword argument to True.
|
|
443
|
+
|
|
444
|
+
See also:
|
|
445
|
+
:ref:`Conversion of IDSs between DD versions`.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
toplevel: The IDS element to convert.
|
|
449
|
+
version: The data dictionary version to convert to, for example "3.38.0". Must
|
|
450
|
+
be None when using ``xml_path`` or ``factory``.
|
|
451
|
+
|
|
452
|
+
Keyword Args:
|
|
453
|
+
deepcopy: When True, performs a deep copy of all data. When False (default),
|
|
454
|
+
numpy arrays are not copied and the converted IDS shares the same underlying
|
|
455
|
+
data buffers.
|
|
456
|
+
provenance_origin_uri: When nonempty, add an entry in the provenance data in
|
|
457
|
+
``ids_properties`` to indicate that this IDS has been converted, and it was
|
|
458
|
+
originally stored at the given uri.
|
|
459
|
+
xml_path: Path to a data dictionary XML file that should be used instead of the
|
|
460
|
+
released data dictionary version specified by ``version``.
|
|
461
|
+
factory: Existing IDSFactory to use for as target version.
|
|
462
|
+
target: Use this IDSToplevel as target toplevel instead of creating one.
|
|
463
|
+
"""
|
|
464
|
+
if toplevel._lazy:
|
|
465
|
+
raise NotImplementedError(
|
|
466
|
+
"IDS conversion is not implemented for lazy-loaded IDSs"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
ids_name = toplevel.metadata.name
|
|
470
|
+
if target is None:
|
|
471
|
+
if factory is None:
|
|
472
|
+
factory = IDSFactory(version, xml_path)
|
|
473
|
+
if not factory.exists(ids_name):
|
|
474
|
+
raise RuntimeError(
|
|
475
|
+
f"There is no IDS with name {ids_name} in DD version {version}."
|
|
476
|
+
)
|
|
477
|
+
target_ids = factory.new(ids_name)
|
|
478
|
+
else:
|
|
479
|
+
target_ids = target
|
|
480
|
+
|
|
481
|
+
source_version = parse_dd_version(toplevel._version)
|
|
482
|
+
target_version = parse_dd_version(target_ids._version)
|
|
483
|
+
logger.info(
|
|
484
|
+
"Starting conversion of IDS %s from version %s to version %s.",
|
|
485
|
+
ids_name,
|
|
486
|
+
source_version,
|
|
487
|
+
target_version,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
source_is_new = source_version > target_version
|
|
491
|
+
source_tree = toplevel._parent._etree
|
|
492
|
+
target_tree = target_ids._parent._etree
|
|
493
|
+
if source_is_new:
|
|
494
|
+
version_map = _DDVersionMap(ids_name, target_tree, source_tree, target_version)
|
|
495
|
+
else:
|
|
496
|
+
version_map = _DDVersionMap(ids_name, source_tree, target_tree, source_version)
|
|
497
|
+
|
|
498
|
+
_copy_structure(toplevel, target_ids, deepcopy, source_is_new, version_map)
|
|
499
|
+
logger.info("Conversion of IDS %s finished.", ids_name)
|
|
500
|
+
if provenance_origin_uri:
|
|
501
|
+
_add_provenance_entry(target_ids, toplevel._version, provenance_origin_uri)
|
|
502
|
+
return target_ids
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _add_provenance_entry(
|
|
506
|
+
target_ids: IDSToplevel, source_version: str, provenance_origin_uri: str
|
|
507
|
+
) -> None:
|
|
508
|
+
# provenance node was added in DD 3.34.0
|
|
509
|
+
if not hasattr(target_ids.ids_properties, "provenance"):
|
|
510
|
+
logger.warning(
|
|
511
|
+
"Cannot add provenance entry for DD conversion: "
|
|
512
|
+
"target IDS does not have a provenance property."
|
|
513
|
+
)
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
# Find the node corresponding to the whole IDS, or create one if there is none
|
|
517
|
+
for node in target_ids.ids_properties.provenance.node:
|
|
518
|
+
if node.path == "":
|
|
519
|
+
break
|
|
520
|
+
else:
|
|
521
|
+
# No node found for the whole IDS, create a new one:
|
|
522
|
+
curlen = len(target_ids.ids_properties.provenance.node)
|
|
523
|
+
target_ids.ids_properties.provenance.node.resize(curlen + 1, keep=True)
|
|
524
|
+
node = target_ids.ids_properties.provenance.node[-1]
|
|
525
|
+
|
|
526
|
+
# Populate the node
|
|
527
|
+
source_txt = (
|
|
528
|
+
f"{provenance_origin_uri}; "
|
|
529
|
+
f"This IDS has been converted from DD {source_version} to "
|
|
530
|
+
f"DD {target_ids._dd_version} by imas-python {imas.__version__}."
|
|
531
|
+
)
|
|
532
|
+
if hasattr(node, "reference"):
|
|
533
|
+
# DD version after IMAS-5304
|
|
534
|
+
node.reference.resize(len(node.reference) + 1, keep=True)
|
|
535
|
+
node.reference[-1].name = source_txt
|
|
536
|
+
timestamp = datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds")
|
|
537
|
+
node.reference[-1].timestamp = timestamp.replace("+00:00", "Z")
|
|
538
|
+
else:
|
|
539
|
+
# DD before IMAS-5304 (between 3.34.0 and 3.41.0)
|
|
540
|
+
node.sources.append(source_txt) # sources is a STR_1D (=list of strings)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _copy_structure(
|
|
544
|
+
source: IDSStructure,
|
|
545
|
+
target: IDSStructure,
|
|
546
|
+
deepcopy: bool,
|
|
547
|
+
source_is_new: bool,
|
|
548
|
+
version_map: DDVersionMap,
|
|
549
|
+
):
|
|
550
|
+
"""Recursively copy data, following NBC renames.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
source: Source structure.
|
|
554
|
+
target: Target structure.
|
|
555
|
+
deepcopy: See :func:`convert_ids`.
|
|
556
|
+
source_is_new: True iff the DD version of the source is newer than that of the
|
|
557
|
+
target.
|
|
558
|
+
version_map: Version map containing NBC renames.
|
|
559
|
+
"""
|
|
560
|
+
rename_map = version_map.new_to_old if source_is_new else version_map.old_to_new
|
|
561
|
+
for item in source.iter_nonempty_():
|
|
562
|
+
path = item.metadata.path_string
|
|
563
|
+
if path in rename_map:
|
|
564
|
+
if rename_map.path[path] is None:
|
|
565
|
+
if path not in rename_map.ignore_missing_paths:
|
|
566
|
+
if path in rename_map.type_change:
|
|
567
|
+
msg = "Element %r changed type in the target IDS."
|
|
568
|
+
else:
|
|
569
|
+
msg = "Element %r does not exist in the target IDS."
|
|
570
|
+
logger.warning(msg + " Data is not copied.", path)
|
|
571
|
+
continue
|
|
572
|
+
else:
|
|
573
|
+
target_item = IDSPath(rename_map.path[path]).goto(target)
|
|
574
|
+
else:
|
|
575
|
+
try:
|
|
576
|
+
target_item = target[item.metadata.name]
|
|
577
|
+
except AttributeError:
|
|
578
|
+
# In exceptional cases the item does not exist in the target. Example:
|
|
579
|
+
# neutron_diagnostic IDS between DD 3.40.1 and 3.41.0. has renamed
|
|
580
|
+
# synthetic_signals/fusion_power -> fusion_power. The synthetic_signals
|
|
581
|
+
# structure no longer exists but we need to descend into it to get the
|
|
582
|
+
# total_neutron_flux.
|
|
583
|
+
target_item = target
|
|
584
|
+
if path in rename_map.type_change:
|
|
585
|
+
# Handle type change
|
|
586
|
+
new_items = rename_map.type_change[path](item, target_item)
|
|
587
|
+
if new_items is None:
|
|
588
|
+
continue # handled
|
|
589
|
+
else:
|
|
590
|
+
item, target_item = new_items
|
|
591
|
+
|
|
592
|
+
if isinstance(item, IDSStructArray):
|
|
593
|
+
size = len(item)
|
|
594
|
+
target_item.resize(size)
|
|
595
|
+
for i in range(size):
|
|
596
|
+
_copy_structure(
|
|
597
|
+
item[i], target_item[i], deepcopy, source_is_new, version_map
|
|
598
|
+
)
|
|
599
|
+
elif isinstance(item, IDSStructure):
|
|
600
|
+
_copy_structure(item, target_item, deepcopy, source_is_new, version_map)
|
|
601
|
+
else:
|
|
602
|
+
if deepcopy:
|
|
603
|
+
# No nested types are used as data, so a shallow copy is sufficient
|
|
604
|
+
target_item.value = copy.copy(item.value)
|
|
605
|
+
else:
|
|
606
|
+
target_item.value = item.value
|
|
607
|
+
|
|
608
|
+
# Post-process the node:
|
|
609
|
+
if path in rename_map.post_process:
|
|
610
|
+
rename_map.post_process[path](target_item)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
########################################################################################
|
|
614
|
+
# Type changed handlers and post-processing functions #
|
|
615
|
+
########################################################################################
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _type_changed_structure_aos(
|
|
619
|
+
source_node: IDSBase, target_node: IDSBase
|
|
620
|
+
) -> Optional[Tuple[IDSStructure, IDSStructure]]:
|
|
621
|
+
"""Handle changed type, structure to array of structures (or vice versa): IMAS-5170.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
source_node: data source (must be IDSStructure or IDSStructArray)
|
|
625
|
+
target_node: data target (must be IDSStructArray or IDSStructure)
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
(source_node, target_node): new source and target node to continue copying, or
|
|
629
|
+
None if no copying needs to be done.
|
|
630
|
+
"""
|
|
631
|
+
if isinstance(source_node, IDSStructure):
|
|
632
|
+
# Structure to AOS is always supported
|
|
633
|
+
assert isinstance(target_node, IDSStructArray)
|
|
634
|
+
target_node.resize(1)
|
|
635
|
+
return source_node, target_node[0]
|
|
636
|
+
assert isinstance(source_node, IDSStructArray)
|
|
637
|
+
assert len(source_node) > 0 # empty AoS should not be provided as source_node
|
|
638
|
+
assert isinstance(target_node, IDSStructure)
|
|
639
|
+
if len(source_node) == 1:
|
|
640
|
+
# AoS to Structure is supported when the AOS has size 1
|
|
641
|
+
return source_node[0], target_node
|
|
642
|
+
# source_node has more than 1 element, this is not supported:
|
|
643
|
+
logger.warning(
|
|
644
|
+
"Element %r changed type in the target IDS and has more than 1 item. "
|
|
645
|
+
"Data is not copied.",
|
|
646
|
+
source_node.metadata.path_string,
|
|
647
|
+
)
|
|
648
|
+
return None
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _type_changed_0d_1d_data(
|
|
652
|
+
source_node: IDSPrimitive, target_node: IDSPrimitive
|
|
653
|
+
) -> None:
|
|
654
|
+
"""Handle changed type, 0D to 1D (or vice versa): IMAS-5170.
|
|
655
|
+
|
|
656
|
+
Note: underlying data type (INT, FLT, CPX or STR) should not change.
|
|
657
|
+
|
|
658
|
+
Args:
|
|
659
|
+
source_node: Data source
|
|
660
|
+
target_node: Data target
|
|
661
|
+
"""
|
|
662
|
+
if source_node.metadata.ndim == 0:
|
|
663
|
+
# 0D to 1D is always supported
|
|
664
|
+
target_node.value = [source_node.value]
|
|
665
|
+
elif len(source_node) == 1:
|
|
666
|
+
# 1D to 0D is only supported when 1 item is present
|
|
667
|
+
target_node.value = source_node[0]
|
|
668
|
+
else:
|
|
669
|
+
logger.warning(
|
|
670
|
+
"Element %r changed type in the target IDS and has more than 1 item. "
|
|
671
|
+
"Data is not copied.",
|
|
672
|
+
source_node.metadata.path_string,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _type_changed_to_identifier(source_node: IDSBase, target_node: IDSBase) -> None:
|
|
677
|
+
"""Handle a type change from list of indexes to a proper identifier."""
|
|
678
|
+
if len(source_node) == 0:
|
|
679
|
+
return
|
|
680
|
+
|
|
681
|
+
if source_node.metadata.data_type is IDSDataType.INT:
|
|
682
|
+
# target_node is an array of identifier structures
|
|
683
|
+
target_node.resize(len(source_node))
|
|
684
|
+
identifier_enum = target_node.metadata.identifier_enum
|
|
685
|
+
if identifier_enum is not None:
|
|
686
|
+
for i in range(len(source_node)):
|
|
687
|
+
try:
|
|
688
|
+
# Look up the index in the identifier and assign to set name, index
|
|
689
|
+
# and description
|
|
690
|
+
target_node[i] = identifier_enum(source_node[i])
|
|
691
|
+
except ValueError:
|
|
692
|
+
# That may fail for unknown values (e.g. negative values): fall back
|
|
693
|
+
# to only setting index and leaving name and description empty
|
|
694
|
+
target_node[i].index = source_node[i]
|
|
695
|
+
else:
|
|
696
|
+
# We couldn't get the identifier enum, so just copy into the index array
|
|
697
|
+
for i in range(len(source_node)):
|
|
698
|
+
target_node[i].index = source_node[i]
|
|
699
|
+
|
|
700
|
+
else:
|
|
701
|
+
# source_node is an array of identifier structures, target_node is an INT_1D
|
|
702
|
+
target_node.value = numpy.array(
|
|
703
|
+
[node.index for node in source_node], numpy.int32
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _type_changed_flt0d_int0d(source_node: IDSNumeric0D, target_node: IDSNumeric0D):
|
|
708
|
+
"""Handle a type change from FLT_0D to INT_0D.
|
|
709
|
+
|
|
710
|
+
Support type change of ion/element/z_n from DD3 to DD4.
|
|
711
|
+
|
|
712
|
+
Note:
|
|
713
|
+
When the floating point data cannot be converted to an integer without loss of
|
|
714
|
+
data, a warning is issued and the data is not copied.
|
|
715
|
+
"""
|
|
716
|
+
if source_node.metadata.data_type is IDSDataType.INT:
|
|
717
|
+
# INT to FLT can always be represented exactly
|
|
718
|
+
target_node.value = float(source_node.value)
|
|
719
|
+
|
|
720
|
+
else:
|
|
721
|
+
float_value: float = source_node.value
|
|
722
|
+
int_value = numpy.int32(float_value)
|
|
723
|
+
if int_value == float_value:
|
|
724
|
+
target_node.value = int_value # no data lost on conversion
|
|
725
|
+
else:
|
|
726
|
+
logger.warning(
|
|
727
|
+
"Element %r with value %f is cannot be represented as an integer. "
|
|
728
|
+
"Data is not copied.",
|
|
729
|
+
source_node.metadata.path_string,
|
|
730
|
+
float_value,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _remove_last_point(node: IDSBase) -> None:
|
|
735
|
+
"""Postprocess method for nbc_description=repeat_children_first_point.
|
|
736
|
+
|
|
737
|
+
This method handles postprocessing when converting from new (DDv4) to old (DDv3).
|
|
738
|
+
"""
|
|
739
|
+
for child in node.iter_nonempty_():
|
|
740
|
+
child.value = child.value[:-1]
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _repeat_first_point(node: IDSBase) -> None:
|
|
744
|
+
"""Postprocess method for nbc_description=repeat_children_first_point.
|
|
745
|
+
|
|
746
|
+
This method handles postprocessing when converting from old (DDv3) to new (DDv4).
|
|
747
|
+
"""
|
|
748
|
+
for child in node.iter_nonempty_():
|
|
749
|
+
child.value = numpy.concatenate((child.value, [child.value[0]]))
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _remove_last_point_conditional(
|
|
753
|
+
source_node: IDSStructure, target_node: IDSStructure
|
|
754
|
+
) -> None:
|
|
755
|
+
"""Type change method for nbc_description=repeat_children_first_point_conditional*.
|
|
756
|
+
|
|
757
|
+
This method handles converting from new (DDv4) to old (DDv3).
|
|
758
|
+
"""
|
|
759
|
+
closed_node = getattr(target_node, "closed", None)
|
|
760
|
+
if closed_node is None: # closed is a sibling node
|
|
761
|
+
closed_node = target_node._dd_parent.closed
|
|
762
|
+
|
|
763
|
+
# Figure out if the contour is closed:
|
|
764
|
+
closed = True
|
|
765
|
+
# source_node may be an AoS for the ...conditional_sibling_dynamic case
|
|
766
|
+
if source_node.metadata.data_type is IDSDataType.STRUCT_ARRAY:
|
|
767
|
+
iterator = source_node
|
|
768
|
+
else:
|
|
769
|
+
iterator = [source_node]
|
|
770
|
+
for source in iterator:
|
|
771
|
+
for component in source.iter_nonempty_():
|
|
772
|
+
if component.metadata.name != "time":
|
|
773
|
+
if not numpy.isclose(component[0], component[-1], rtol=1e-6, atol=0):
|
|
774
|
+
closed = False
|
|
775
|
+
break
|
|
776
|
+
if not closed:
|
|
777
|
+
break
|
|
778
|
+
|
|
779
|
+
# Copy data
|
|
780
|
+
closed_node.value = int(closed)
|
|
781
|
+
if source_node.metadata.data_type is IDSDataType.STRUCT_ARRAY:
|
|
782
|
+
target_node.resize(len(source_node))
|
|
783
|
+
iterator = zip(source_node, target_node)
|
|
784
|
+
else:
|
|
785
|
+
iterator = [(source_node, target_node)]
|
|
786
|
+
for source, target in iterator:
|
|
787
|
+
for child in source.iter_nonempty_():
|
|
788
|
+
value = child.value
|
|
789
|
+
if (
|
|
790
|
+
closed
|
|
791
|
+
and child.metadata.name != "time"
|
|
792
|
+
and isinstance(child, IDSNumericArray)
|
|
793
|
+
):
|
|
794
|
+
# repeat first point:
|
|
795
|
+
value = value[:-1]
|
|
796
|
+
target[child.metadata.name] = value
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _repeat_first_point_conditional(
|
|
800
|
+
source_node: IDSStructure, target_node: IDSStructure
|
|
801
|
+
) -> None:
|
|
802
|
+
"""Type change method for nbc_description=repeat_children_first_point_conditional*.
|
|
803
|
+
|
|
804
|
+
This method handles converting from old (DDv3) to new (DDv4).
|
|
805
|
+
"""
|
|
806
|
+
closed_node = getattr(source_node, "closed", None)
|
|
807
|
+
if closed_node is None: # closed is a sibling node
|
|
808
|
+
closed_node = source_node._dd_parent.closed
|
|
809
|
+
closed = bool(closed_node)
|
|
810
|
+
|
|
811
|
+
# source_node may be an AoS for the ...conditional_sibling_dynamic case
|
|
812
|
+
if source_node.metadata.data_type is IDSDataType.STRUCT_ARRAY:
|
|
813
|
+
target_node.resize(len(source_node))
|
|
814
|
+
iterator = zip(source_node, target_node)
|
|
815
|
+
else:
|
|
816
|
+
iterator = [(source_node, target_node)]
|
|
817
|
+
for source, target in iterator:
|
|
818
|
+
for child in source.iter_nonempty_():
|
|
819
|
+
if child.metadata.name != "closed" and not child.metadata.name.endswith(
|
|
820
|
+
"_error_index"
|
|
821
|
+
):
|
|
822
|
+
value = child.value
|
|
823
|
+
if (
|
|
824
|
+
closed
|
|
825
|
+
and child.metadata.name != "time"
|
|
826
|
+
and isinstance(child, IDSNumericArray)
|
|
827
|
+
):
|
|
828
|
+
# repeat first point:
|
|
829
|
+
value = numpy.concatenate((value, [value[0]]))
|
|
830
|
+
target[child.metadata.name] = value
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _remove_last_point_centreline(source_node: IDSBase, target_node: IDSBase) -> None:
|
|
834
|
+
"""Type change method for
|
|
835
|
+
nbc_description=repeat_children_first_point_conditional_centreline.
|
|
836
|
+
|
|
837
|
+
This method handles converting from old (DDv3) to new (DDv4).
|
|
838
|
+
|
|
839
|
+
If a centreline is a closed contour, we should do nothing.
|
|
840
|
+
If it is an open contour the thickness variable had too many entries,
|
|
841
|
+
and we'll drop the last one.
|
|
842
|
+
"""
|
|
843
|
+
closed = bool(source_node._parent.centreline.closed)
|
|
844
|
+
|
|
845
|
+
if closed:
|
|
846
|
+
target_node.value = source_node.value
|
|
847
|
+
else:
|
|
848
|
+
target_node.value = source_node.value[:-1]
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _repeat_last_point_centreline(source_node: IDSBase, target_node: IDSBase) -> None:
|
|
852
|
+
"""Type change method for
|
|
853
|
+
nbc_description=repeat_children_first_point_conditional_centreline.
|
|
854
|
+
|
|
855
|
+
This method handles converting from new (DDv4) to old (DDv3).
|
|
856
|
+
|
|
857
|
+
If a centreline is a closed contour, we should do nothing.
|
|
858
|
+
If it is an open contour the thickness variable in the older
|
|
859
|
+
dd has one extra entry, so repeat the last one.
|
|
860
|
+
"""
|
|
861
|
+
closed = bool(target_node._parent.centreline.closed)
|
|
862
|
+
if closed:
|
|
863
|
+
target_node.value = source_node.value
|
|
864
|
+
else:
|
|
865
|
+
target_node.value = numpy.concatenate(
|
|
866
|
+
(source_node.value, [source_node.value[-1]])
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _cocos_change(node: IDSBase) -> None:
|
|
871
|
+
"""Handle COCOS definition change: multiply values by -1."""
|
|
872
|
+
if not isinstance(node, IDSPrimitive):
|
|
873
|
+
if node.metadata.path_string == "flux_loop/flux":
|
|
874
|
+
# Workaround for DD definition issue with flux_loop/flux in magnetics IDS
|
|
875
|
+
node.data.value = -node.data.value
|
|
876
|
+
else:
|
|
877
|
+
logger.error(
|
|
878
|
+
"Error while applying COCOS transformation (DD3->4): cannot multiply "
|
|
879
|
+
"element %r by -1.",
|
|
880
|
+
node.metadata.path_string,
|
|
881
|
+
)
|
|
882
|
+
else:
|
|
883
|
+
node.value = -node.value
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _circuit_connections_3to4(node: IDSPrimitive) -> None:
|
|
887
|
+
"""Handle definition change for pf_active circuit/connections."""
|
|
888
|
+
shape = node.shape
|
|
889
|
+
if shape[1] % 2: # second dimension not divisible by 2:
|
|
890
|
+
logger.error(
|
|
891
|
+
f"Error while converting {node}. Size of the second dimension should "
|
|
892
|
+
"be divisible by 2. Data was not converted."
|
|
893
|
+
)
|
|
894
|
+
else:
|
|
895
|
+
node.value = node.value[:, ::2] - node.value[:, 1::2]
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def _circuit_connections_4to3(node: IDSPrimitive) -> None:
|
|
899
|
+
"""Handle definition change for pf_active circuit/connections."""
|
|
900
|
+
shape = node.shape
|
|
901
|
+
new_value = numpy.zeros((shape[0], 2 * shape[1]), dtype=numpy.int32)
|
|
902
|
+
new_value[:, ::2] = node.value == 1
|
|
903
|
+
new_value[:, 1::2] = node.value == -1
|
|
904
|
+
node.value = new_value
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _ids_properties_source(source: IDSString0D, provenance: IDSStructure) -> None:
|
|
908
|
+
"""Handle DD3to4 migration of ids_properties/source to ids_properties/provenance."""
|
|
909
|
+
if len(provenance.node) > 0:
|
|
910
|
+
logger.warning(
|
|
911
|
+
"Element %r could not be migrated: %r is alreadys set.",
|
|
912
|
+
source.metadata.path_string,
|
|
913
|
+
provenance.node.metadata.path_string,
|
|
914
|
+
)
|
|
915
|
+
return
|
|
916
|
+
|
|
917
|
+
# Populate ids_properties/provenance/node[0]/reference[0].name with source.value
|
|
918
|
+
provenance.node.resize(1)
|
|
919
|
+
provenance.node[0].reference.resize(1)
|
|
920
|
+
provenance.node[0].reference[0].name = source.value
|