Skip to content

Commit 7f2e15f

Browse files
authored
Merge pull request #62 from maarten-ic/feature/dd3to4-boundary-conversion
Equilibrium DD3to4 conversion for boundary_separatrix
2 parents a149238 + 85d9037 commit 7f2e15f

File tree

4 files changed

+201
-2
lines changed

4 files changed

+201
-2
lines changed

docs/source/multi-dd.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ explicit conversion mechanisms.
166166
Changed definition of ``space/coordinates_type`` in GGD grids, Yes, No
167167
Migrate obsolescent ``ids_properties/source`` to ``ids_properties/provenance``, Yes, No
168168
Convert the multiple time-bases in the ``pulse_schedule`` IDS [#ps3to4]_, Yes, No
169+
Convert name + identifier -> description + name, Yes, Yes
170+
Convert equilibrium ``boundary\_[secondary\_]separatrix`` to ``contour_tree`` [#contourtree]_, Yes, No
169171

170172
.. [#rename] Quantities which have been renamed between the two DD versions. For
171173
example, the ``ec/beam`` Array of Structures in the ``pulse_schedule`` IDS,
@@ -205,6 +207,12 @@ explicit conversion mechanisms.
205207
interpolation otherwise. See also:
206208
https://github.com/iterorganization/IMAS-Python/issues/21.
207209
210+
.. [#contourtree] Fills the `contour_tree
211+
<https://imas-data-dictionary.readthedocs.io/en/latest/generated/ids/equilibrium.html#equilibrium-time_slice-contour_tree>`__
212+
in the ``equilibrium`` IDS based on data in the ``boundary_separatrix`` and
213+
``boundary_secondary_separatrix`` structures from DD3. See also:
214+
https://github.com/iterorganization/IMAS-Python/issues/60.
215+
208216
.. _`DD background`:
209217

210218
Background information

imas/db_entry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ def _get(
608608
logger.warning(
609609
"On-disk data is stored in DD %s which has a different major "
610610
"version than the requested DD version (%s). IMAS-Python will "
611-
"convert the data automatically, but this does not cover all"
611+
"convert the data automatically, but this does not cover all "
612612
"changes. "
613613
"See %s/multi-dd.html#conversion-of-idss-between-dd-versions",
614614
dd_version,

imas/ids_convert.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import logging
88
from functools import lru_cache, partial
99
from pathlib import Path
10-
from typing import Callable, Dict, Iterator, Optional, Set, Tuple
10+
from typing import Callable, Dict, Iterator, List, Optional, Set, Tuple
1111
from xml.etree.ElementTree import Element, ElementTree
1212

1313
import numpy
@@ -87,6 +87,15 @@ def __init__(self) -> None:
8787
converted.
8888
"""
8989

90+
self.post_process_ids: List[
91+
Callable[[IDSToplevel, IDSToplevel, bool], None]
92+
] = []
93+
"""Postprocess functions to be applied to the whole IDS.
94+
95+
These postprocess functions should be applied to the whole IDS after all data is
96+
converted. The arguments supplied are: source IDS, target IDS, deepcopy boolean.
97+
"""
98+
9099
self.ignore_missing_paths: Set[str] = set()
91100
"""Set of paths that should not be logged when data is present."""
92101

@@ -362,6 +371,13 @@ def _apply_3to4_conversion(self, old: Element, new: Element) -> None:
362371
self.new_to_old.post_process[new_path] = _cocos_change
363372
self.old_to_new.post_process[old_path] = _cocos_change
364373

374+
# Convert equilibrium boundary_separatrix and populate contour_tree
375+
if self.ids_name == "equilibrium":
376+
self.old_to_new.post_process_ids.append(_equilibrium_boundary_3to4)
377+
self.old_to_new.ignore_missing_paths |= {
378+
"time_slice/boundary_separatrix",
379+
"time_slice/boundary_secondary_separatrix",
380+
}
365381
# Definition change for pf_active circuit/connections
366382
if self.ids_name == "pf_active":
367383
path = "circuit/connections"
@@ -603,6 +619,10 @@ def convert_ids(
603619
else:
604620
_copy_structure(toplevel, target, deepcopy, rename_map)
605621

622+
# Global post-processing functions
623+
for callback in rename_map.post_process_ids:
624+
callback(toplevel, target, deepcopy)
625+
606626
logger.info("Conversion of IDS %s finished.", ids_name)
607627
if provenance_origin_uri:
608628
_add_provenance_entry(target, toplevel._version, provenance_origin_uri)
@@ -1229,3 +1249,70 @@ def _pulse_schedule_resample_callback(timebase, item: IDSBase, target_item: IDSB
12291249
assume_sorted=True,
12301250
)(timebase)
12311251
target_item.value = value.astype(numpy.int32) if is_integer else value
1252+
1253+
1254+
def _equilibrium_boundary_3to4(eq3: IDSToplevel, eq4: IDSToplevel, deepcopy: bool):
1255+
"""Convert DD3 boundary[[_secondary]_separatrix] to DD4 contour_tree"""
1256+
# Implement https://github.com/iterorganization/IMAS-Python/issues/60
1257+
copy = numpy.copy if deepcopy else lambda x: x
1258+
for ts3, ts4 in zip(eq3.time_slice, eq4.time_slice):
1259+
if not ts3.global_quantities.psi_axis.has_value:
1260+
# No magnetic axis, assume no boundary either:
1261+
continue
1262+
n_nodes = 1 # magnetic axis
1263+
if ts3.boundary_separatrix.psi.has_value:
1264+
n_nodes = 2
1265+
if ( # boundary_secondary_separatrix is introduced in DD 3.32.0
1266+
hasattr(ts3, "boundary_secondary_separatrix")
1267+
and ts3.boundary_secondary_separatrix.psi.has_value
1268+
):
1269+
n_nodes = 3
1270+
node = ts4.contour_tree.node
1271+
node.resize(n_nodes)
1272+
# Magnetic axis (primary O-point)
1273+
gq = ts3.global_quantities
1274+
# Note the sign flip for psi due to the COCOS change between DD3 and DD4!
1275+
axis_is_psi_minimum = -gq.psi_axis < -gq.psi_boundary
1276+
1277+
node[0].critical_type = 0 if axis_is_psi_minimum else 2
1278+
node[0].r = gq.magnetic_axis.r
1279+
node[0].z = gq.magnetic_axis.z
1280+
node[0].psi = -gq.psi_axis # COCOS change
1281+
1282+
# X-points
1283+
if n_nodes >= 2:
1284+
if ts3.boundary_separatrix.type == 0: # limiter plasma
1285+
node[1].critical_type = 2 if axis_is_psi_minimum else 0
1286+
node[1].r = ts3.boundary_separatrix.active_limiter_point.r
1287+
node[1].z = ts3.boundary_separatrix.active_limiter_point.z
1288+
else:
1289+
node[1].critical_type = 1 # saddle-point (x-point)
1290+
if len(ts3.boundary_separatrix.x_point):
1291+
node[1].r = ts3.boundary_separatrix.x_point[0].r
1292+
node[1].z = ts3.boundary_separatrix.x_point[0].z
1293+
# Additional x-points. N.B. levelset is only stored on the first node
1294+
for i in range(1, len(ts3.boundary_separatrix.x_point)):
1295+
node.resize(len(node) + 1, keep=True)
1296+
node[-1].critical_type = 1
1297+
node[-1].r = ts3.boundary_separatrix.x_point[i].r
1298+
node[-1].z = ts3.boundary_separatrix.x_point[i].z
1299+
node[-1].psi = -ts3.boundary_separatrix.psi
1300+
node[1].psi = -ts3.boundary_separatrix.psi # COCOS change
1301+
node[1].levelset.r = copy(ts3.boundary_separatrix.outline.r)
1302+
node[1].levelset.z = copy(ts3.boundary_separatrix.outline.z)
1303+
1304+
if n_nodes >= 3:
1305+
node[2].critical_type = 1 # saddle-point (x-point)
1306+
if len(ts3.boundary_secondary_separatrix.x_point):
1307+
node[2].r = ts3.boundary_secondary_separatrix.x_point[0].r
1308+
node[2].z = ts3.boundary_secondary_separatrix.x_point[0].z
1309+
# Additional x-points. N.B. levelset is only stored on the first node
1310+
for i in range(1, len(ts3.boundary_secondary_separatrix.x_point)):
1311+
node.resize(len(node) + 1, keep=True)
1312+
node[-1].critical_type = 1
1313+
node[-1].r = ts3.boundary_secondary_separatrix.x_point[i].r
1314+
node[-1].z = ts3.boundary_secondary_separatrix.x_point[i].z
1315+
node[-1].psi = -ts3.boundary_secondary_separatrix.psi
1316+
node[2].psi = -ts3.boundary_secondary_separatrix.psi # COCOS change
1317+
node[2].levelset.r = copy(ts3.boundary_secondary_separatrix.outline.r)
1318+
node[2].levelset.z = copy(ts3.boundary_secondary_separatrix.outline.z)

imas/test/test_ids_convert.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,3 +592,107 @@ def test_3to4_cocos_hardcoded_paths():
592592

593593
eq4 = convert_ids(eq, "4.0.0")
594594
assert eq4.time_slice[0].boundary.psi == -3.141
595+
596+
597+
def test_3to4_equilibrium_boundary():
598+
eq342 = IDSFactory("3.42.0").equilibrium()
599+
eq342.time_slice.resize(5)
600+
601+
for i, ts in enumerate(eq342.time_slice):
602+
# Always fill boundary and magnetic axis
603+
ts.boundary.psi = 1
604+
ts.boundary.outline.r = [1.0, 3.0, 2.0, 1.0]
605+
ts.boundary.outline.z = [1.0, 2.0, 3.0, 1.0]
606+
ts.global_quantities.psi_axis = 1.0
607+
ts.global_quantities.magnetic_axis.r = 2.0
608+
ts.global_quantities.magnetic_axis.z = 2.0
609+
610+
if i > 0:
611+
# Fill separatrix
612+
ts.boundary_separatrix.psi = -1.0
613+
# Use limiter for time_slice[1], otherwise divertor:
614+
if i == 1:
615+
ts.boundary_separatrix.type = 0
616+
ts.boundary_separatrix.active_limiter_point.r = 3.0
617+
ts.boundary_separatrix.active_limiter_point.z = 2.0
618+
else:
619+
ts.boundary_separatrix.type = 1
620+
ts.boundary_separatrix.outline.r = [1.0, 3.0, 2.0, 1.0]
621+
ts.boundary_separatrix.outline.z = [1.0, 2.0, 3.0, 1.0]
622+
ts.boundary_separatrix.x_point.resize(1)
623+
ts.boundary_separatrix.x_point[0].r = 1.0
624+
ts.boundary_separatrix.x_point[0].z = 1.0
625+
# These are not part of the conversion:
626+
ts.boundary_separatrix.strike_point.resize(2)
627+
ts.boundary_separatrix.closest_wall_point.r = 1.0
628+
ts.boundary_separatrix.closest_wall_point.z = 1.0
629+
ts.boundary_separatrix.closest_wall_point.distance = 0.2
630+
ts.boundary_separatrix.dr_dz_zero_point.r = 3.0
631+
ts.boundary_separatrix.dr_dz_zero_point.z = 2.0
632+
ts.boundary_separatrix.gap.resize(1)
633+
if i == 3:
634+
# Fill second_separatrix
635+
ts.boundary_secondary_separatrix.psi = -1.1
636+
# Use limiter for time_slice[1], otherwise divertor:
637+
ts.boundary_secondary_separatrix.outline.r = [0.9, 3.1, 2.1, 0.9]
638+
ts.boundary_secondary_separatrix.outline.z = [0.9, 2.1, 3.1, 0.9]
639+
ts.boundary_secondary_separatrix.x_point.resize(1)
640+
ts.boundary_secondary_separatrix.x_point[0].r = 2.1
641+
ts.boundary_secondary_separatrix.x_point[0].z = 3.1
642+
# These are not part of the conversion:
643+
ts.boundary_secondary_separatrix.distance_inner_outer = 0.1
644+
ts.boundary_secondary_separatrix.strike_point.resize(2)
645+
if i == 4:
646+
ts.boundary_separatrix.x_point.resize(2, keep=True)
647+
ts.boundary_separatrix.x_point[1].r = 2.0
648+
ts.boundary_separatrix.x_point[1].z = 3.0
649+
650+
eq4 = convert_ids(eq342, "4.0.0")
651+
assert len(eq4.time_slice) == 5
652+
for i, ts in enumerate(eq4.time_slice):
653+
node = ts.contour_tree.node
654+
assert len(node) == [1, 2, 2, 3, 3][i]
655+
# Test magnetic axis
656+
assert node[0].critical_type == 0
657+
assert node[0].r == node[0].z == 2.0
658+
assert node[0].psi == -1.0
659+
assert len(node[0].levelset.r) == len(node[0].levelset.z) == 0
660+
# boundary_separatrix
661+
if i == 1: # node[1] is boundary for limiter plasma
662+
assert node[1].critical_type == 2
663+
assert node[1].r == 3.0
664+
assert node[1].z == 2.0
665+
elif i > 1: # node[1] is boundary for divertor plasma
666+
assert node[1].critical_type == 1
667+
assert node[1].r == node[1].z == 1.0
668+
if i > 0:
669+
assert node[1].psi == 1.0
670+
assert numpy.array_equal(node[1].levelset.r, [1.0, 3.0, 2.0, 1.0])
671+
assert numpy.array_equal(node[1].levelset.z, [1.0, 2.0, 3.0, 1.0])
672+
# boundary_secondary_separatrix
673+
if i == 3:
674+
assert node[2].critical_type == 1
675+
assert node[2].r == 2.1
676+
assert node[2].z == 3.1
677+
assert node[2].psi == 1.1
678+
assert numpy.array_equal(node[2].levelset.r, [0.9, 3.1, 2.1, 0.9])
679+
assert numpy.array_equal(node[2].levelset.z, [0.9, 2.1, 3.1, 0.9])
680+
# Second x-point from boundary_separatrix
681+
if i == 4:
682+
assert node[2].critical_type == 1
683+
assert node[2].r == 2.0
684+
assert node[2].z == 3.0
685+
assert node[2].psi == node[1].psi == 1.0
686+
# Levelset is only filled for the main x-point (node[1])
687+
assert not node[2].levelset.r.has_value
688+
assert not node[2].levelset.z.has_value
689+
690+
# not deepcopied, should share numpy arrays
691+
slice1_outline_r = eq342.time_slice[1].boundary_separatrix.outline.r.value
692+
assert slice1_outline_r is eq4.time_slice[1].contour_tree.node[1].levelset.r.value
693+
694+
# deepcopy should create a copy of the numpy arrays
695+
eq4_cp = convert_ids(eq342, "4.0.0", deepcopy=True)
696+
assert not numpy.may_share_memory(
697+
slice1_outline_r, eq4_cp.time_slice[1].contour_tree.node[1].levelset.r.value
698+
)

0 commit comments

Comments
 (0)