Skip to content

Commit a09186a

Browse files
committed
Initial implementation of equilibrium DD3to4 conversion for boundary_separatrix
1 parent 9446e62 commit a09186a

File tree

2 files changed

+157
-1
lines changed

2 files changed

+157
-1
lines changed

imas/ids_convert.py

Lines changed: 69 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

@@ -343,6 +352,13 @@ def _apply_3to4_conversion(self, old: Element, new: Element) -> None:
343352
new_path = self.old_to_new.path.get(old_path, old_path)
344353
self.new_to_old.post_process[new_path] = _cocos_change
345354
self.old_to_new.post_process[old_path] = _cocos_change
355+
# Convert equilibrium boundary_separatrix and populate contour_tree
356+
if self.ids_name == "equilibrium":
357+
self.old_to_new.post_process_ids.append(_equilibrium_boundary_3to4)
358+
self.old_to_new.ignore_missing_paths |= {
359+
"time_slice/boundary_separatrix",
360+
"time_slice/boundary_secondary_separatrix",
361+
}
346362
# Definition change for pf_active circuit/connections
347363
if self.ids_name == "pf_active":
348364
path = "circuit/connections"
@@ -544,6 +560,10 @@ def convert_ids(
544560
else:
545561
_copy_structure(toplevel, target, deepcopy, rename_map)
546562

563+
# Global post-processing functions
564+
for callback in rename_map.post_process_ids:
565+
callback(toplevel, target, deepcopy)
566+
547567
logger.info("Conversion of IDS %s finished.", ids_name)
548568
if provenance_origin_uri:
549569
_add_provenance_entry(target, toplevel._version, provenance_origin_uri)
@@ -1063,3 +1083,51 @@ def _pulse_schedule_resample_callback(timebase, item: IDSBase, target_item: IDSB
10631083
assume_sorted=True,
10641084
)(timebase)
10651085
target_item.value = value.astype(numpy.int32) if is_integer else value
1086+
1087+
1088+
def _equilibrium_boundary_3to4(eq3: IDSToplevel, eq4: IDSToplevel, deepcopy: bool):
1089+
"""Convert DD3 boundary[[_secondary]_separatrix] to DD4 contour_tree"""
1090+
# Implement https://github.com/iterorganization/IMAS-Python/issues/60
1091+
copy = numpy.copy if deepcopy else lambda x: x
1092+
for ts3, ts4 in zip(eq3.time_slice, eq4.time_slice):
1093+
n_nodes = 1 # magnetic axis
1094+
if ts3.boundary_separatrix.psi.has_value:
1095+
n_nodes = 2
1096+
if ( # boundary_secondary_separatrix is introduced in DD 3.32.0
1097+
hasattr(ts3, "boundary_secondary_separatrix")
1098+
and ts3.boundary_secondary_separatrix.psi.has_value
1099+
):
1100+
n_nodes = 3
1101+
ts4.contour_tree.node.resize(n_nodes)
1102+
# Magnetic axis (primary O-point)
1103+
node = ts4.contour_tree.node
1104+
node[0].critical_type = 0 # minimum (?)
1105+
node[0].r = ts3.global_quantities.magnetic_axis.r
1106+
node[0].z = ts3.global_quantities.magnetic_axis.z
1107+
node[0].psi = -ts3.global_quantities.psi_axis # COCOS change
1108+
1109+
# X-points
1110+
if n_nodes >= 2:
1111+
if ts3.boundary_separatrix.type == 0: # limiter plasma
1112+
node[1].critical_type = 2 # maximum (?)
1113+
node[1].r = ts3.boundary_separatrix.active_limiter_point.r
1114+
node[1].z = ts3.boundary_separatrix.active_limiter_point.z
1115+
else:
1116+
node[1].critical_type = 1 # saddle-point (x-point)
1117+
if len(ts3.boundary_separatrix.x_point):
1118+
node[1].r = ts3.boundary_separatrix.x_point[0].r
1119+
node[1].z = ts3.boundary_separatrix.x_point[0].z
1120+
# TODO: what if there are multiple x-points?
1121+
node[1].psi = -ts3.boundary_separatrix.psi # COCOS change
1122+
node[1].levelset.r = copy(ts3.boundary_separatrix.outline.r)
1123+
node[1].levelset.z = copy(ts3.boundary_separatrix.outline.z)
1124+
1125+
if n_nodes >= 3:
1126+
node[2].critical_type = 1 # saddle-point (x-point)
1127+
if len(ts3.boundary_secondary_separatrix.x_point):
1128+
node[2].r = ts3.boundary_secondary_separatrix.x_point[0].r
1129+
node[2].z = ts3.boundary_secondary_separatrix.x_point[0].z
1130+
# TODO: what if there are multiple x-points?
1131+
node[2].psi = -ts3.boundary_secondary_separatrix.psi # COCOS change
1132+
node[2].levelset.r = copy(ts3.boundary_secondary_separatrix.outline.r)
1133+
node[2].levelset.z = copy(ts3.boundary_secondary_separatrix.outline.z)

imas/test/test_ids_convert.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,3 +529,91 @@ def test_3to4_migrate_deprecated_fields(): # GH#55
529529
del cp342.profiles_1d[0].ion[0].label
530530
cp4 = convert_ids(cp342, "4.0.0")
531531
assert cp4.profiles_1d[0].ion[0].name == "y"
532+
533+
534+
def test_3to4_equilibrium_boundary():
535+
eq342 = IDSFactory("3.42.0").equilibrium()
536+
eq342.time_slice.resize(5)
537+
538+
for i, ts in enumerate(eq342.time_slice):
539+
# Always fill boundary and magnetic axis
540+
ts.boundary.psi = 1
541+
ts.boundary.outline.r = [1.0, 3.0, 2.0, 1.0]
542+
ts.boundary.outline.z = [1.0, 2.0, 3.0, 1.0]
543+
ts.global_quantities.psi_axis = 1.0
544+
ts.global_quantities.magnetic_axis.r = 2.0
545+
ts.global_quantities.magnetic_axis.z = 2.0
546+
547+
if i > 0:
548+
# Fill separatrix
549+
ts.boundary_separatrix.psi = -1.0
550+
# Use limiter for time_slice[1], otherwise divertor:
551+
if i == 1:
552+
ts.boundary_separatrix.type = 0
553+
ts.boundary_separatrix.active_limiter_point.r = 3.0
554+
ts.boundary_separatrix.active_limiter_point.z = 2.0
555+
else:
556+
ts.boundary_separatrix.type = 1
557+
ts.boundary_separatrix.outline.r = [1.0, 3.0, 2.0, 1.0]
558+
ts.boundary_separatrix.outline.z = [1.0, 2.0, 3.0, 1.0]
559+
ts.boundary_separatrix.x_point.resize(1)
560+
ts.boundary_separatrix.x_point[0].r = 1.0
561+
ts.boundary_separatrix.x_point[0].z = 1.0
562+
# These are not part of the conversion:
563+
ts.boundary_separatrix.strike_point.resize(2)
564+
ts.boundary_separatrix.closest_wall_point.r = 1.0
565+
ts.boundary_separatrix.closest_wall_point.z = 1.0
566+
ts.boundary_separatrix.closest_wall_point.distance = 0.2
567+
ts.boundary_separatrix.dr_dz_zero_point.r = 3.0
568+
ts.boundary_separatrix.dr_dz_zero_point.z = 2.0
569+
ts.boundary_separatrix.gap.resize(1)
570+
if i == 3:
571+
# Fill second_separatrix
572+
ts.boundary_secondary_separatrix.psi = -1.0
573+
# Use limiter for time_slice[1], otherwise divertor:
574+
ts.boundary_secondary_separatrix.outline.r = [0.9, 3.1, 2.1, 0.9]
575+
ts.boundary_secondary_separatrix.outline.z = [0.9, 2.1, 3.1, 0.9]
576+
ts.boundary_secondary_separatrix.x_point.resize(1)
577+
ts.boundary_secondary_separatrix.x_point[0].r = 2.1
578+
ts.boundary_secondary_separatrix.x_point[0].z = 3.1
579+
# These are not part of the conversion:
580+
ts.boundary_secondary_separatrix.distance_inner_outer = 0.1
581+
ts.boundary_secondary_separatrix.strike_point.resize(2)
582+
if i == 4:
583+
ts.boundary_separatrix.x_point.resize(2, keep=True)
584+
ts.boundary_separatrix.x_point[1].r = 2.0
585+
ts.boundary_separatrix.x_point[1].z = 3.0
586+
587+
eq4 = convert_ids(eq342, "4.0.0")
588+
assert len(eq4.time_slice) == 5
589+
for i, ts in enumerate(eq4.time_slice):
590+
node = ts.contour_tree.node
591+
assert len(node) == [1, 2, 2, 3, 2][i]
592+
# Test magnetic axis
593+
assert node[0].critical_type == 0
594+
assert node[0].r == node[0].z == 2.0
595+
assert len(node[0].levelset.r) == len(node[0].levelset.z) == 0
596+
# boundary_separatrix
597+
if i == 1: # node[1] is boundary for limiter plasma
598+
assert node[1].critical_type == 2
599+
assert node[1].r == 3.0
600+
assert node[1].z == 2.0
601+
elif i > 1: # node[1] is boundary for divertor plasma
602+
assert node[1].critical_type == 1
603+
assert node[1].r == node[1].z == 1.0
604+
if i > 0:
605+
assert numpy.array_equal(node[1].levelset.r, [1.0, 3.0, 2.0, 1.0])
606+
assert numpy.array_equal(node[1].levelset.z, [1.0, 2.0, 3.0, 1.0])
607+
# boundary_secondary_separatrix
608+
if i == 3:
609+
assert node[2].critical_type == 1
610+
assert node[2].r == 2.1
611+
assert node[2].z == 3.1
612+
assert numpy.array_equal(node[2].levelset.r, [0.9, 3.1, 2.1, 0.9])
613+
assert numpy.array_equal(node[2].levelset.z, [0.9, 2.1, 3.1, 0.9])
614+
615+
# not deepcopied, should share numpy arrays
616+
assert (
617+
eq342.time_slice[1].boundary_separatrix.outline.r.value
618+
is eq4.time_slice[1].contour_tree.node[1].levelset.r.value
619+
)

0 commit comments

Comments
 (0)