imas-python 2.2.0__py3-none-any.whl → 2.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- imas/_to_xarray.py +20 -13
- imas/_version.py +8 -18
- imas/assets/ITER_134173_106_equilibrium.ids +0 -2
- imas/assets/equilibrium.ids +0 -2
- imas/backends/db_entry_impl.py +8 -0
- imas/backends/imas_core/al_context.py +11 -0
- imas/backends/imas_core/db_entry_al.py +13 -0
- imas/backends/imas_core/imas_interface.py +5 -0
- imas/backends/netcdf/db_entry_nc.py +23 -6
- imas/backends/netcdf/ids2nc.py +12 -46
- imas/backends/netcdf/ids_tensorizer.py +69 -10
- imas/backends/netcdf/nc2ids.py +13 -14
- imas/db_entry.py +67 -4
- imas/ids_convert.py +66 -32
- imas/ids_identifiers.py +28 -15
- imas/ids_primitive.py +3 -3
- imas/test/test_convert_core_edge_plasma.py +1 -1
- imas/test/test_identifiers.py +18 -9
- imas/test/test_ids_convert.py +72 -0
- imas/test/test_list_filled_paths.py +101 -0
- imas/test/test_to_xarray.py +25 -2
- imas/training.py +11 -6
- {imas_python-2.2.0.dist-info → imas_python-2.3.0.dist-info}/METADATA +1 -1
- {imas_python-2.2.0.dist-info → imas_python-2.3.0.dist-info}/RECORD +28 -27
- {imas_python-2.2.0.dist-info → imas_python-2.3.0.dist-info}/WHEEL +1 -1
- {imas_python-2.2.0.dist-info → imas_python-2.3.0.dist-info}/entry_points.txt +0 -0
- {imas_python-2.2.0.dist-info → imas_python-2.3.0.dist-info}/licenses/LICENSE.txt +0 -0
- {imas_python-2.2.0.dist-info → imas_python-2.3.0.dist-info}/top_level.txt +0 -0
imas/ids_convert.py
CHANGED
|
@@ -274,16 +274,7 @@ class DDVersionMap:
|
|
|
274
274
|
self.version_old,
|
|
275
275
|
)
|
|
276
276
|
elif self._check_data_type(old_item, new_item):
|
|
277
|
-
# use class helper to register simple renames and
|
|
278
|
-
# reciprocal mappings
|
|
279
277
|
self._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
|
-
self._add_rename(path, npath)
|
|
287
278
|
elif nbc_description == "type_changed":
|
|
288
279
|
pass # We will handle this (if possible) in self._check_data_type
|
|
289
280
|
elif nbc_description == "repeat_children_first_point":
|
|
@@ -334,28 +325,40 @@ class DDVersionMap:
|
|
|
334
325
|
# Additional conversion rules for DDv3 to DDv4
|
|
335
326
|
if self.version_old.major == 3 and new_version and new_version.major == 4:
|
|
336
327
|
self._apply_3to4_conversion(old, new)
|
|
328
|
+
# 3to4 rules may have introduced additional missing items in self.old_to_new
|
|
329
|
+
self._map_missing(
|
|
330
|
+
False, old_path_set.difference(new_path_set, self.old_to_new)
|
|
331
|
+
)
|
|
337
332
|
|
|
338
|
-
def _add_rename(
|
|
333
|
+
def _add_rename(
|
|
334
|
+
self, old_path: str, new_path: str, reciprocal: bool = True
|
|
335
|
+
) -> None:
|
|
339
336
|
"""Register a simple rename from old_path -> new_path using the
|
|
340
337
|
path->Element maps stored on the instance (self.old_paths/self.new_paths).
|
|
341
338
|
This will also add the reciprocal mapping when possible.
|
|
342
339
|
"""
|
|
343
340
|
old_item = self.old_paths[old_path]
|
|
344
341
|
new_item = self.new_paths[new_path]
|
|
345
|
-
|
|
346
|
-
# forward mapping
|
|
342
|
+
# Forward mapping
|
|
347
343
|
self.old_to_new[old_path] = (
|
|
348
344
|
new_path,
|
|
349
345
|
_get_tbp(new_item, self.new_paths),
|
|
350
346
|
_get_ctxpath(new_path, self.new_paths),
|
|
351
347
|
)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
348
|
+
# Reciprocal mapping
|
|
349
|
+
if reciprocal:
|
|
350
|
+
self.new_to_old[new_path] = (
|
|
351
|
+
old_path,
|
|
352
|
+
_get_tbp(old_item, self.old_paths),
|
|
353
|
+
_get_ctxpath(old_path, self.old_paths),
|
|
354
|
+
)
|
|
355
|
+
# Apply to descendent nodes as well if the item is a struct or AoS
|
|
356
|
+
for item in old_item.findall("field"):
|
|
357
|
+
path = item.get("path")
|
|
358
|
+
assert path is not None and path.startswith(old_path)
|
|
359
|
+
npath = path.replace(old_path, new_path, 1)
|
|
360
|
+
if npath in self.new_paths:
|
|
361
|
+
self._add_rename(path, npath, reciprocal)
|
|
359
362
|
|
|
360
363
|
def _apply_3to4_conversion(self, old: Element, new: Element) -> None:
|
|
361
364
|
# Postprocessing for COCOS definition change:
|
|
@@ -421,6 +424,13 @@ class DDVersionMap:
|
|
|
421
424
|
to_update[p] = v
|
|
422
425
|
self.old_to_new.path.update(to_update)
|
|
423
426
|
|
|
427
|
+
# Migrate additional obsolescent nodes
|
|
428
|
+
# TODO: define migrations in a separate variable (as with the sign flips)?
|
|
429
|
+
if self.ids_name == "magnetics":
|
|
430
|
+
self._add_rename("bpol_probe", "b_field_pol_probe", reciprocal=False)
|
|
431
|
+
self._add_rename("method", "ip", reciprocal=False)
|
|
432
|
+
self.old_to_new.type_change["method"] = _magnetics_method_to_ip
|
|
433
|
+
|
|
424
434
|
# GH#59: To improve further the conversion of DD3 to DD4, especially the
|
|
425
435
|
# Machine Description part of the IDSs, we would like to add a 3to4 specific
|
|
426
436
|
# rule to convert any siblings name + identifier (that are not part of an
|
|
@@ -431,19 +441,20 @@ class DDVersionMap:
|
|
|
431
441
|
# Only perform the mapping if the corresponding target fields exist in the
|
|
432
442
|
# new DD and if we don't already have a mapping for the involved paths.
|
|
433
443
|
# use self.old_paths and self.new_paths set in _build_map
|
|
434
|
-
for
|
|
444
|
+
for name_path in self.old_paths:
|
|
435
445
|
# look for name children
|
|
436
|
-
if not
|
|
446
|
+
if not name_path.endswith("/name"):
|
|
437
447
|
continue
|
|
438
|
-
parent =
|
|
439
|
-
name_path = f"{parent}/name"
|
|
448
|
+
parent = name_path.rsplit("/", 1)[0]
|
|
440
449
|
id_path = f"{parent}/identifier"
|
|
441
450
|
index_path = f"{parent}/index"
|
|
442
|
-
|
|
443
|
-
|
|
451
|
+
# Follow renames of parent structure
|
|
452
|
+
new_parent = self.old_to_new.path.get(parent) or parent
|
|
453
|
+
desc_path = f"{new_parent}/description"
|
|
454
|
+
new_name_path = f"{new_parent}/name"
|
|
444
455
|
|
|
445
|
-
# If
|
|
446
|
-
if
|
|
456
|
+
# If 'identifier' doesn't exist in the old DD, skip this parent
|
|
457
|
+
if id_path not in self.old_paths:
|
|
447
458
|
continue
|
|
448
459
|
# exclude identifier-structure (has index sibling)
|
|
449
460
|
if index_path in self.old_paths:
|
|
@@ -454,12 +465,11 @@ class DDVersionMap:
|
|
|
454
465
|
continue
|
|
455
466
|
|
|
456
467
|
# Map DD3 name -> DD4 description
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
468
|
+
self._add_rename(name_path, desc_path)
|
|
469
|
+
# GH#114: Also preserve name in DD4 name when identifier is empty
|
|
470
|
+
self.old_to_new.type_change[name_path] = _name_identifier_3to4
|
|
460
471
|
# Map DD3 identifier -> DD4 name
|
|
461
|
-
|
|
462
|
-
self._add_rename(id_path, new_name_path)
|
|
472
|
+
self._add_rename(id_path, new_name_path)
|
|
463
473
|
|
|
464
474
|
def _map_missing(self, is_new: bool, missing_paths: Set[str]):
|
|
465
475
|
rename_map = self.new_to_old if is_new else self.old_to_new
|
|
@@ -1154,6 +1164,19 @@ def _circuit_connections_4to3(node: IDSPrimitive) -> None:
|
|
|
1154
1164
|
node.value = new_value
|
|
1155
1165
|
|
|
1156
1166
|
|
|
1167
|
+
def _name_identifier_3to4(source_name: IDSBase, target_description: IDSBase) -> None:
|
|
1168
|
+
"""Preserve name when identifier is empty, see GH#114."""
|
|
1169
|
+
# Always copy DD3 name -> DD4 description
|
|
1170
|
+
target_description.value = source_name.value
|
|
1171
|
+
|
|
1172
|
+
# When DD3 identifier is empty, also preserve name in DD4 name
|
|
1173
|
+
source_parent = source_name._parent
|
|
1174
|
+
source_identifier = getattr(source_parent, "identifier", None)
|
|
1175
|
+
if source_identifier is None or not source_identifier.value:
|
|
1176
|
+
target_parent = target_description._parent
|
|
1177
|
+
target_parent.name = source_name.value
|
|
1178
|
+
|
|
1179
|
+
|
|
1157
1180
|
def _ids_properties_source(source: IDSString0D, provenance: IDSStructure) -> None:
|
|
1158
1181
|
"""Handle DD3to4 migration of ids_properties/source to ids_properties/provenance."""
|
|
1159
1182
|
if len(provenance.node) > 0:
|
|
@@ -1314,3 +1337,14 @@ def _equilibrium_boundary_3to4(eq3: IDSToplevel, eq4: IDSToplevel, deepcopy: boo
|
|
|
1314
1337
|
node[2].psi = -ts3.boundary_secondary_separatrix.psi # COCOS change
|
|
1315
1338
|
node[2].levelset.r = copy(ts3.boundary_secondary_separatrix.outline.r)
|
|
1316
1339
|
node[2].levelset.z = copy(ts3.boundary_secondary_separatrix.outline.z)
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def _magnetics_method_to_ip(method: IDSBase, ip: IDSBase) -> None:
|
|
1343
|
+
"""Convert obsolescent method(:) to ip(:) in the magnetics IDS."""
|
|
1344
|
+
if not len(method):
|
|
1345
|
+
return
|
|
1346
|
+
ip.resize(len(method))
|
|
1347
|
+
for old_item, new_item in zip(method, ip, strict=True):
|
|
1348
|
+
new_item.method_name.value = old_item.name.value
|
|
1349
|
+
new_item.data.value = old_item.ip.data.value
|
|
1350
|
+
new_item.time.value = old_item.ip.time.value
|
imas/ids_identifiers.py
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import Iterable, List, Type
|
|
7
|
+
from typing import Iterable, List, Type, Optional
|
|
8
8
|
from xml.etree.ElementTree import fromstring
|
|
9
9
|
|
|
10
10
|
from imas import dd_zip
|
|
@@ -15,19 +15,29 @@ logger = logging.getLogger(__name__)
|
|
|
15
15
|
class IDSIdentifier(Enum):
|
|
16
16
|
"""Base class for all identifier enums."""
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
name: str
|
|
19
|
+
"""Name of this identifier value."""
|
|
20
|
+
index: int
|
|
21
|
+
"""Unique index for this identifier value."""
|
|
22
|
+
description: str
|
|
23
|
+
"""Description for this identifier value."""
|
|
24
|
+
aliases: list[str]
|
|
25
|
+
"""Alternative names for this identifier value."""
|
|
26
|
+
units: Optional[str]
|
|
27
|
+
"""Units of the quantity/quantities altered by this identifier. May be ``None`` if
|
|
28
|
+
the Data Dictionary doesn't provide this metadata."""
|
|
29
|
+
|
|
30
|
+
def __new__(
|
|
31
|
+
cls, value: int, description: str, aliases: list[str], units: Optional[str]
|
|
32
|
+
):
|
|
19
33
|
obj = object.__new__(cls)
|
|
20
34
|
obj._value_ = value
|
|
35
|
+
obj.index = value
|
|
36
|
+
obj.description = description
|
|
37
|
+
obj.aliases = aliases
|
|
38
|
+
obj.units = units
|
|
21
39
|
return obj
|
|
22
40
|
|
|
23
|
-
def __init__(self, value: int, description: str, aliases: list = []) -> None:
|
|
24
|
-
self.index = value
|
|
25
|
-
"""Unique index for this identifier value."""
|
|
26
|
-
self.description = description
|
|
27
|
-
"""Description for this identifier value."""
|
|
28
|
-
self.aliases = aliases
|
|
29
|
-
"""Alternative names for this identifier value."""
|
|
30
|
-
|
|
31
41
|
def __eq__(self, other):
|
|
32
42
|
if self is other:
|
|
33
43
|
return True
|
|
@@ -68,19 +78,22 @@ class IDSIdentifier(Enum):
|
|
|
68
78
|
def _from_xml(cls, identifier_name, xml) -> Type["IDSIdentifier"]:
|
|
69
79
|
element = fromstring(xml)
|
|
70
80
|
enum_values = {}
|
|
71
|
-
aliases = {}
|
|
72
81
|
for int_element in element.iterfind("int"):
|
|
73
82
|
name = int_element.get("name")
|
|
74
83
|
value = int_element.text
|
|
84
|
+
assert value is not None
|
|
75
85
|
description = int_element.get("description")
|
|
76
86
|
# alias attribute may contain multiple comma-separated aliases
|
|
77
87
|
alias_attr = int_element.get("alias", "")
|
|
78
88
|
aliases = [a.strip() for a in alias_attr.split(",") if a.strip()]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
#
|
|
89
|
+
units = int_element.get("units")
|
|
90
|
+
|
|
91
|
+
# Identifier can be looked up by its name or any of its aliases:
|
|
92
|
+
enumvalue = (int(value), description, aliases, units)
|
|
93
|
+
enum_values[name] = enumvalue
|
|
82
94
|
for alias in aliases:
|
|
83
|
-
enum_values[alias] =
|
|
95
|
+
enum_values[alias] = enumvalue
|
|
96
|
+
|
|
84
97
|
# Create the enumeration
|
|
85
98
|
enum = cls(
|
|
86
99
|
identifier_name,
|
imas/ids_primitive.py
CHANGED
|
@@ -334,7 +334,7 @@ class IDSNumeric0D(IDSPrimitive):
|
|
|
334
334
|
__doc__ = IDSDoc(__doc__)
|
|
335
335
|
__slots__ = ()
|
|
336
336
|
|
|
337
|
-
def __array__(self, dtype=None):
|
|
337
|
+
def __array__(self, dtype=None, copy=None):
|
|
338
338
|
return np.array(self.value, dtype=dtype)
|
|
339
339
|
|
|
340
340
|
def __str__(self):
|
|
@@ -437,8 +437,8 @@ class IDSNumericArray(IDSPrimitive, np.lib.mixins.NDArrayOperatorsMixin):
|
|
|
437
437
|
# list, to support operations like np.add(array_like, list)
|
|
438
438
|
_HANDLED_TYPES = (np.ndarray, Number)
|
|
439
439
|
|
|
440
|
-
def __array__(self, dtype=None):
|
|
441
|
-
return self.value.astype(dtype, copy=
|
|
440
|
+
def __array__(self, dtype=None, copy=None):
|
|
441
|
+
return self.value.astype(dtype, copy=bool(copy))
|
|
442
442
|
|
|
443
443
|
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
|
444
444
|
out = kwargs.get("out", ())
|
|
@@ -12,7 +12,7 @@ def assert_equal(core_edge, plasma):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def test_convert_training_core_profiles():
|
|
15
|
-
with imas.training.get_training_db_entry() as entry:
|
|
15
|
+
with imas.training.get_training_db_entry(convert=True) as entry:
|
|
16
16
|
cp = entry.get("core_profiles")
|
|
17
17
|
|
|
18
18
|
pp = imas.convert_to_plasma_profiles(cp)
|
imas/test/test_identifiers.py
CHANGED
|
@@ -6,11 +6,9 @@ from imas.dd_zip import dd_identifiers
|
|
|
6
6
|
from imas.ids_factory import IDSFactory
|
|
7
7
|
from imas.ids_identifiers import IDSIdentifier, identifiers
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
"4.1.0"
|
|
11
|
-
|
|
12
|
-
requires_aliases = pytest.mark.skipif(
|
|
13
|
-
not has_aliases, reason="Requires DD 4.1.0 for identifier aliases"
|
|
9
|
+
requires_dd4_1 = pytest.mark.skipif(
|
|
10
|
+
Version(importlib.metadata.version("imas_data_dictionaries")) < Version("4.1.0"),
|
|
11
|
+
reason="Test requires DD 4.1.0 for additional identifier metadata",
|
|
14
12
|
)
|
|
15
13
|
|
|
16
14
|
|
|
@@ -112,7 +110,7 @@ Materials used in the device mechanical structures
|
|
|
112
110
|
assert identifier.CxHy.aliases == ["alias1", "alias2", "3alias"]
|
|
113
111
|
|
|
114
112
|
|
|
115
|
-
@
|
|
113
|
+
@requires_dd4_1
|
|
116
114
|
def test_identifier_struct_assignment_with_aliases():
|
|
117
115
|
"""Test identifier struct assignment with aliases using materials_identifier."""
|
|
118
116
|
mid = identifiers.materials_identifier
|
|
@@ -174,7 +172,7 @@ def test_invalid_identifier_assignment():
|
|
|
174
172
|
cs.source[0].identifier = -1
|
|
175
173
|
|
|
176
174
|
|
|
177
|
-
@
|
|
175
|
+
@requires_dd4_1
|
|
178
176
|
def test_identifier_aliases():
|
|
179
177
|
"""Test identifier enum aliases functionality."""
|
|
180
178
|
mid = identifiers.materials_identifier
|
|
@@ -197,7 +195,7 @@ def test_identifier_aliases():
|
|
|
197
195
|
assert mid[alias] is mid.U_235
|
|
198
196
|
|
|
199
197
|
|
|
200
|
-
@
|
|
198
|
+
@requires_dd4_1
|
|
201
199
|
def test_identifier_alias_equality():
|
|
202
200
|
"""Test that identifiers with aliases are equal when comparing names and aliases."""
|
|
203
201
|
mid = identifiers.materials_identifier
|
|
@@ -276,7 +274,7 @@ def test_identifier_alias_equality():
|
|
|
276
274
|
assert mat5.descriptions[2] == mid["U_235"].description
|
|
277
275
|
|
|
278
276
|
|
|
279
|
-
@
|
|
277
|
+
@requires_dd4_1
|
|
280
278
|
def test_identifier_alias_equality_non_ggd():
|
|
281
279
|
"""Test identifier aliases functionality on non-ggd material"""
|
|
282
280
|
mid = identifiers.materials_identifier
|
|
@@ -293,3 +291,14 @@ def test_identifier_alias_equality_non_ggd():
|
|
|
293
291
|
summary_ids.wall.material.name = "235U" # Use canonical name
|
|
294
292
|
assert summary_ids.wall.material == mid["235U"]
|
|
295
293
|
assert summary_ids.wall.material == mid["U_235"]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@requires_dd4_1
|
|
297
|
+
def test_identifier_units():
|
|
298
|
+
ppcid = identifiers.poloidal_plane_coordinates_identifier
|
|
299
|
+
assert ppcid.rectangular.units == "m,m"
|
|
300
|
+
assert ppcid.inverse.units == "m,rad"
|
|
301
|
+
|
|
302
|
+
# materials identifier doesn't have units (and I don't expect they'll ever get any)
|
|
303
|
+
mid = identifiers.materials_identifier
|
|
304
|
+
assert mid.W.units is None
|
imas/test/test_ids_convert.py
CHANGED
|
@@ -439,6 +439,55 @@ def test_3to4_cocos_magnetics_workaround(dd4factory):
|
|
|
439
439
|
compare_children(mag, mag3)
|
|
440
440
|
|
|
441
441
|
|
|
442
|
+
def test_3to4_deprecated_magnetics(dd4factory):
|
|
443
|
+
# Test migrating deprecated bpol_probe
|
|
444
|
+
mag = IDSFactory("3.39.0").magnetics()
|
|
445
|
+
mag.bpol_probe.resize(2)
|
|
446
|
+
mag.bpol_probe[0].name = "name1"
|
|
447
|
+
mag.bpol_probe[0].identifier = "identifier1"
|
|
448
|
+
mag.bpol_probe[0].position.r = 1
|
|
449
|
+
mag.bpol_probe[0].field.data = [0.1, 0.2, 0.3]
|
|
450
|
+
mag.bpol_probe[1].name = "name2"
|
|
451
|
+
mag.bpol_probe[1].voltage.data = [0.1, 0.2, 0.3]
|
|
452
|
+
|
|
453
|
+
mag.method.resize(2)
|
|
454
|
+
for i, method in enumerate(mag.method):
|
|
455
|
+
method.name = f"name{i}"
|
|
456
|
+
method.ip.data = [i, 1.0, 2.0]
|
|
457
|
+
method.ip.time = [i + 1, 2.0, 3.0]
|
|
458
|
+
|
|
459
|
+
mag4 = convert_ids(mag, None, factory=dd4factory)
|
|
460
|
+
assert len(mag4.b_field_pol_probe) == 2
|
|
461
|
+
assert mag4.b_field_pol_probe[0].name == "identifier1"
|
|
462
|
+
assert mag4.b_field_pol_probe[0].description == "name1"
|
|
463
|
+
assert mag4.b_field_pol_probe[0].position.r == 1
|
|
464
|
+
assert array_equal(mag4.b_field_pol_probe[0].field.data, [0.1, 0.2, 0.3])
|
|
465
|
+
assert mag4.b_field_pol_probe[1].name == "name2"
|
|
466
|
+
assert mag4.b_field_pol_probe[1].description == "name2"
|
|
467
|
+
assert array_equal(mag4.b_field_pol_probe[1].voltage.data, [0.1, 0.2, 0.3])
|
|
468
|
+
|
|
469
|
+
assert len(mag4.ip) == 2
|
|
470
|
+
assert mag4.ip[0].method_name == "name0"
|
|
471
|
+
assert array_equal(mag4.ip[0].data, [0.0, 1.0, 2.0])
|
|
472
|
+
assert array_equal(mag4.ip[0].time, [1.0, 2.0, 3.0])
|
|
473
|
+
assert mag4.ip[1].method_name == "name1"
|
|
474
|
+
assert array_equal(mag4.ip[1].data, [1.0, 1.0, 2.0])
|
|
475
|
+
assert array_equal(mag4.ip[1].time, [2.0, 2.0, 3.0])
|
|
476
|
+
|
|
477
|
+
# If both the deprecated and the "correct" quantity exist, we expect only the
|
|
478
|
+
# correct one to be converted to DD4:
|
|
479
|
+
mag.b_field_pol_probe.resize(1)
|
|
480
|
+
mag.b_field_pol_probe[0].name = "test"
|
|
481
|
+
mag.ip.resize(1)
|
|
482
|
+
mag.ip[0].method_name = "ip"
|
|
483
|
+
|
|
484
|
+
mag4 = convert_ids(mag, None, factory=dd4factory)
|
|
485
|
+
assert len(mag4.b_field_pol_probe) == 1
|
|
486
|
+
assert mag4.b_field_pol_probe[0].name == "test"
|
|
487
|
+
assert len(mag4.ip) == 1
|
|
488
|
+
assert mag4.ip[0].method_name == "ip"
|
|
489
|
+
|
|
490
|
+
|
|
442
491
|
def test_3to4_pulse_schedule():
|
|
443
492
|
ps = IDSFactory("3.39.0").pulse_schedule()
|
|
444
493
|
ps.ids_properties.homogeneous_time = IDS_TIME_MODE_HETEROGENEOUS
|
|
@@ -575,6 +624,29 @@ def test_4to3_name_identifier_mapping_magnetics():
|
|
|
575
624
|
assert dst.b_field_pol_probe[0].identifier == "TEST_NAME"
|
|
576
625
|
|
|
577
626
|
|
|
627
|
+
def test_3to4_name_identifier_empty_identifier():
|
|
628
|
+
"""GH#114: name must be preserved when identifier is empty."""
|
|
629
|
+
factory = IDSFactory("3.40.1")
|
|
630
|
+
|
|
631
|
+
src = factory.pf_active()
|
|
632
|
+
src.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS
|
|
633
|
+
src.coil.resize(2)
|
|
634
|
+
# Case 1: name populated, identifier empty
|
|
635
|
+
src.coil[0].name = "TEST_NAME"
|
|
636
|
+
src.coil[0].identifier = ""
|
|
637
|
+
# Case 2: name populated, identifier not set at all
|
|
638
|
+
src.coil[1].name = "TEST_NAME2"
|
|
639
|
+
|
|
640
|
+
dst = convert_ids(src, "4.0.0")
|
|
641
|
+
|
|
642
|
+
# name must be preserved in DD4 name (not overwritten by empty identifier)
|
|
643
|
+
assert dst.coil[0].name == "TEST_NAME"
|
|
644
|
+
assert dst.coil[0].description == "TEST_NAME"
|
|
645
|
+
|
|
646
|
+
assert dst.coil[1].name == "TEST_NAME2"
|
|
647
|
+
assert dst.coil[1].description == "TEST_NAME2"
|
|
648
|
+
|
|
649
|
+
|
|
578
650
|
def test_3to4_cocos_hardcoded_paths():
|
|
579
651
|
# Check for existence in 3.42.0
|
|
580
652
|
factory = IDSFactory("3.42.0")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import imas
|
|
4
|
+
from imas_core import _al_lowlevel
|
|
5
|
+
from imas.exception import DataEntryException
|
|
6
|
+
from imas.ids_defs import IDS_TIME_MODE_HOMOGENEOUS, IDS_TIME_MODE_INDEPENDENT
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if not hasattr(_al_lowlevel, "al_list_filled_paths"):
|
|
10
|
+
marker = pytest.mark.xfail(reason="list_filled_paths not available in imas_core")
|
|
11
|
+
else:
|
|
12
|
+
marker = []
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(params=["netcdf", pytest.param("hdf5", marks=marker)])
|
|
16
|
+
def testuri(request, tmp_path):
|
|
17
|
+
if request.param == "netcdf":
|
|
18
|
+
return str(tmp_path / "list_filled_paths.nc")
|
|
19
|
+
return f"imas:{request.param}?path={tmp_path}/list_filled_paths_{request.param}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_list_filled_paths(testuri):
|
|
23
|
+
with imas.DBEntry(testuri, "w", dd_version="4.0.0") as dbentry:
|
|
24
|
+
# No IDSs in the DBEntry yet, expect an exception
|
|
25
|
+
with pytest.raises(DataEntryException):
|
|
26
|
+
dbentry.list_filled_paths("core_profiles")
|
|
27
|
+
|
|
28
|
+
cp = dbentry.factory.core_profiles()
|
|
29
|
+
cp.ids_properties.homogeneous_time = IDS_TIME_MODE_HOMOGENEOUS
|
|
30
|
+
cp.ids_properties.comment = "comment"
|
|
31
|
+
cp.time = [0.1, 0.2]
|
|
32
|
+
cp.profiles_1d.resize(2)
|
|
33
|
+
cp.profiles_1d[0].grid.rho_tor_norm = [1.0, 2.0]
|
|
34
|
+
cp.profiles_1d[0].ion.resize(2)
|
|
35
|
+
cp.profiles_1d[0].ion[1].temperature = [1.0, 2.0]
|
|
36
|
+
cp.profiles_1d[1].grid.psi = [1.0, 2.0]
|
|
37
|
+
cp.profiles_1d[1].q = [1.0, 2.0]
|
|
38
|
+
cp.profiles_1d[1].e_field.radial = [1.0, 2.0]
|
|
39
|
+
cp.profiles_1d[1].neutral.resize(2)
|
|
40
|
+
cp.global_quantities.ip = [1.0, 2.0]
|
|
41
|
+
|
|
42
|
+
dbentry.put(cp)
|
|
43
|
+
|
|
44
|
+
filled_paths = dbentry.list_filled_paths("core_profiles")
|
|
45
|
+
assert isinstance(filled_paths, list)
|
|
46
|
+
assert set(filled_paths) == {
|
|
47
|
+
"ids_properties/version_put/access_layer",
|
|
48
|
+
"ids_properties/version_put/access_layer_language",
|
|
49
|
+
"ids_properties/version_put/data_dictionary",
|
|
50
|
+
"ids_properties/homogeneous_time",
|
|
51
|
+
"ids_properties/comment",
|
|
52
|
+
"time",
|
|
53
|
+
"profiles_1d/grid/rho_tor_norm",
|
|
54
|
+
"profiles_1d/ion/temperature",
|
|
55
|
+
"profiles_1d/grid/psi",
|
|
56
|
+
"profiles_1d/q",
|
|
57
|
+
"profiles_1d/e_field/radial",
|
|
58
|
+
"profiles_1d/e_field/radial",
|
|
59
|
+
"global_quantities/ip",
|
|
60
|
+
}
|
|
61
|
+
# Other occurrence should still raise an error:
|
|
62
|
+
with pytest.raises(DataEntryException):
|
|
63
|
+
dbentry.list_filled_paths("core_profiles", 1)
|
|
64
|
+
# Until we write data to the occurrence:
|
|
65
|
+
dbentry.put(cp, 3)
|
|
66
|
+
assert set(filled_paths) == set(dbentry.list_filled_paths("core_profiles", 3))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_list_filled_paths_autoconvert(testuri):
|
|
70
|
+
with imas.DBEntry(testuri, "w", dd_version="3.25.0") as entry:
|
|
71
|
+
ps = entry.factory.pulse_schedule()
|
|
72
|
+
ps.ids_properties.homogeneous_time = IDS_TIME_MODE_INDEPENDENT
|
|
73
|
+
ps.ec.antenna.resize(1)
|
|
74
|
+
ps.ec.antenna[0].launching_angle_pol.reference_name = "test"
|
|
75
|
+
entry.put(ps)
|
|
76
|
+
|
|
77
|
+
filled_paths = entry.list_filled_paths("pulse_schedule")
|
|
78
|
+
assert set(filled_paths) == {
|
|
79
|
+
"ids_properties/version_put/access_layer",
|
|
80
|
+
"ids_properties/version_put/access_layer_language",
|
|
81
|
+
"ids_properties/version_put/data_dictionary",
|
|
82
|
+
"ids_properties/homogeneous_time",
|
|
83
|
+
"ec/antenna/launching_angle_pol/reference_name",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Check autoconvert with DD 3.28.0
|
|
87
|
+
with imas.DBEntry(testuri, "r", dd_version="3.28.0") as entry:
|
|
88
|
+
assert set(entry.list_filled_paths("pulse_schedule", autoconvert=False)) == {
|
|
89
|
+
"ids_properties/version_put/access_layer",
|
|
90
|
+
"ids_properties/version_put/access_layer_language",
|
|
91
|
+
"ids_properties/version_put/data_dictionary",
|
|
92
|
+
"ids_properties/homogeneous_time",
|
|
93
|
+
"ec/antenna/launching_angle_pol/reference_name", # original name
|
|
94
|
+
}
|
|
95
|
+
assert set(entry.list_filled_paths("pulse_schedule")) == {
|
|
96
|
+
"ids_properties/version_put/access_layer",
|
|
97
|
+
"ids_properties/version_put/access_layer_language",
|
|
98
|
+
"ids_properties/version_put/data_dictionary",
|
|
99
|
+
"ids_properties/homogeneous_time",
|
|
100
|
+
"ec/launcher/steering_angle_pol/reference_name", # autoconverted name
|
|
101
|
+
}
|
imas/test/test_to_xarray.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import netCDF4
|
|
2
3
|
import pytest
|
|
3
4
|
|
|
4
5
|
import imas
|
|
5
6
|
import imas.training
|
|
7
|
+
from imas.test.test_helpers import compare_children
|
|
6
8
|
from imas.util import to_xarray
|
|
7
9
|
|
|
8
|
-
pytest.importorskip("xarray")
|
|
10
|
+
xarray = pytest.importorskip("xarray")
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
@pytest.fixture
|
|
@@ -87,8 +89,29 @@ def test_to_xarray():
|
|
|
87
89
|
ids.profiles_1d[0].time = 0.0
|
|
88
90
|
|
|
89
91
|
# These should all be identical:
|
|
90
|
-
ds1 = to_xarray(ids)
|
|
92
|
+
ds1 = to_xarray(ids).drop_vars(
|
|
93
|
+
["profiles_1d", "profiles_1d.electrons", "profiles_1d.grid"]
|
|
94
|
+
)
|
|
91
95
|
ds2 = to_xarray(ids, "profiles_1d.electrons.temperature")
|
|
92
96
|
ds3 = to_xarray(ids, "profiles_1d/electrons/temperature")
|
|
93
97
|
assert ds1.equals(ds2)
|
|
94
98
|
assert ds2.equals(ds3)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.parametrize("idsname", ["core_profiles", "equilibrium"])
|
|
102
|
+
def test_roundtrip_xarray_netcdf(tmp_path, entry, idsname):
|
|
103
|
+
ids = entry.get(idsname)
|
|
104
|
+
xrds = to_xarray(ids)
|
|
105
|
+
fname = f"{tmp_path}/test-{idsname}-xarray.nc"
|
|
106
|
+
# First write mandatory file-level metadata
|
|
107
|
+
with netCDF4.Dataset(fname, "x") as ds:
|
|
108
|
+
ds.data_dictionary_version = imas.util.get_data_dictionary_version(ids)
|
|
109
|
+
# Then use xarray to write the IDS
|
|
110
|
+
xrds.to_netcdf(fname, "a", format="NETCDF4", group=f"{idsname}/0")
|
|
111
|
+
# And read it back with a DBEntry
|
|
112
|
+
with imas.DBEntry(fname, "r") as entry:
|
|
113
|
+
ids2 = entry.get(idsname)
|
|
114
|
+
compare_children(ids, ids2)
|
|
115
|
+
# Reading the netCDF file with xarray should produce an identical dataset
|
|
116
|
+
ncxrds = xarray.load_dataset(fname, group=f"{idsname}/0")
|
|
117
|
+
assert xrds.equals(ncxrds)
|
imas/training.py
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
# You should have received the IMAS-Python LICENSE file with this project.
|
|
3
3
|
"""Functions that are useful for the IMAS-Python training courses."""
|
|
4
4
|
|
|
5
|
-
from unittest.mock import patch
|
|
6
|
-
|
|
7
5
|
try:
|
|
8
6
|
from importlib.resources import files
|
|
9
7
|
except ImportError: # Python 3.8 support
|
|
@@ -12,15 +10,22 @@ except ImportError: # Python 3.8 support
|
|
|
12
10
|
import imas
|
|
13
11
|
|
|
14
12
|
|
|
15
|
-
def get_training_db_entry() -> imas.DBEntry:
|
|
16
|
-
"""Open and return an ``imas.DBEntry`` pointing to the training data.
|
|
13
|
+
def get_training_db_entry(convert=False) -> imas.DBEntry:
|
|
14
|
+
"""Open and return an ``imas.DBEntry`` pointing to the training data.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
convert: if True, converts assets to default DD version
|
|
18
|
+
"""
|
|
17
19
|
assets_path = files(imas) / "assets/"
|
|
18
20
|
entry = imas.DBEntry(f"imas:ascii?path={assets_path}", "r")
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
version = None if convert else "3.39.0"
|
|
23
|
+
output_entry = imas.DBEntry("imas:memory?path=/", "w", dd_version=version)
|
|
21
24
|
for ids_name in ["core_profiles", "equilibrium"]:
|
|
22
25
|
ids = entry.get(ids_name, autoconvert=False)
|
|
23
|
-
|
|
26
|
+
if convert:
|
|
24
27
|
output_entry.put(imas.convert_ids(ids, output_entry.dd_version))
|
|
28
|
+
else:
|
|
29
|
+
output_entry.put(ids)
|
|
25
30
|
entry.close()
|
|
26
31
|
return output_entry
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imas-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
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
|