"""
Functions for adding EAST hardware description data to the IMAS schema by writing data to ODS instances
"""
try:
# framework is running
from .startup_choice import *
except ImportError as _excp:
# class is imported by itself
if (
'attempted relative import with no known parent package' in str(_excp)
or 'No module named \'omfit_classes\'' in str(_excp)
or "No module named '__main__.startup_choice'" in str(_excp)
):
from startup_choice import *
else:
raise
import numpy as np
from omfit_classes.omfit_mds import OMFITmds, OMFITmdsValue
try:
import omas
imas_version = omas.ODS().imas_version
except Exception:
imas_version = '0.0'
approximate_is_okay = os.environ['USER'] in ['eldond']
__all__ = []
# Decorators
def _id(obj):
"""Trivial decorator as an alternative to make_available()"""
return obj
def make_available(f):
"""Decorator for listing a function in __all__ so it will be readily available in other scripts"""
__all__.append(f.__name__)
return f
def make_available_if(condition):
"""
Make available only if a condition is met
:param condition: bool
True: make_available decorator is used and function is added to __all__ to be readily available
False: _id decorator is used, function is not added to __all__, and it cannot be accessed quite so easily
Some functions inspect __all__ to determine which hardware systems are available
"""
if condition:
return make_available
return _id
# Reference information
east_divertor_corners = { # Upper, Lower, Outer, Inner
'uo': (1.70668, 1.16211),
'ui': (1.37929, 1.04859),
'lo': (1.76511, -1.17034),
'li': (1.33132, -1.01072),
}
# Utilities
def printq(*args):
return printd(*args, topic='omfit_omas_east')
# Hardware descriptor functions
[docs]@make_available
def setup_pf_active_hardware_description_east(ods, *args):
r"""
Adds EAST tokamak poloidal field coil hardware geometry to ODS
:param ods: ODS instance
:param \*args: catch unused args to allow a consistent call signature for hardware description functions
:return: dict
Information or instructions for follow up in central hardware description setup
"""
from omfit_classes.omfit_omas_utils import pf_coils_to_ods
# Coil data from iris:/fusion/usc/src/idl/efitview/diagnoses/EAST/coils_east.dat accessed 2019-12-30 by eldond
east_pf_coil_data = np.array(
[
[6.2866000e-1, 2.5132000e-1, 1.5078000e-1, 4.4260000e-1, 0, 0],
[6.2866000e-1, -2.513200e-1, 1.5078000e-1, 4.4260000e-1, 0, 0],
[6.2866000e-1, 7.5396000e-1, 1.5078000e-1, 4.4260000e-1, 0, 0],
[6.2866000e-1, -7.539600e-1, 1.5078000e-1, 4.4260000e-1, 0, 0],
[6.2866000e-1, 1.2566000e00, 1.5078000e-1, 4.4260000e-1, 0, 0],
[6.2866000e-1, -1.256600e00, 1.5078000e-1, 4.4260000e-1, 0, 0],
[1.0721700e00, 1.7537000e00, 2.3694000e-1, 8.8520000e-2, 0, 0],
[1.0721700e00, -1.753700e00, 2.3694000e-1, 8.8520000e-2, 0, 0],
[1.1367900e00, 1.9409200e00, 3.6618000e-1, 2.6556000e-1, 0, 0],
[1.1367900e00, -1.940920e00, 3.6618000e-1, 2.6556000e-1, 0, 0],
[2.9455800e00, 1.5907300e00, 1.1844000e-1, 2.0340000e-1, 0, 0],
[2.9455800e00, -1.590730e00, 1.1844000e-1, 2.0340000e-1, 0, 0],
[3.2698000e00, 9.0419000e-1, 8.8960000e-2, 1.6272000e-1, 0, 0],
[3.2698000e00, -9.041900e-1, 7.8960000e-2, 1.6272000e-1, 0, 0],
[2.4500000e00, 6.0000000e-1, 5.0000000e-2, 1.0000000e-1, 0, 0],
[2.4500000e00, -6.000000e-1, 5.0000000e-2, 1.0000000e-1, 0, 0],
]
)
pf_coils_to_ods(ods, east_pf_coil_data)
for i in range(len(east_pf_coil_data[:, 0])):
# Coil names can be found in figures 2, 3 and 4 of Xiao, et al. FED 2019
# https://www.sciencedirect.com/science/article/pii/S0920379619304594
if i < 14:
coilnum = i + 1
fcid = 'PF{}'.format(coilnum)
else:
coilnum = i - 13
fcid = 'IC{}'.format(coilnum)
ods['pf_active.coil'][i]['name'] = ods['pf_active.coil'][i]['identifier'] = fcid
ods['pf_active.coil'][i]['element.0.identifier'] = fcid
return {}
[docs]@make_available
def east_coords_along_wall(s, rlim, zlim, surface):
"""
Transforms s into R, Z. Useful for finding LP locations
:param s: numeric
Distance along the wall from a reference point (m)
:param rlim: 1D array
R coordinates along the limiting surface (m)
:param zlim:
Z coordinates along the limiting surface (m)
:param surface: str
Which surface / reference should be used?
'uo', 'ui', 'lo', or 'li'
:return: (R, Z)
R value(s) corresponding to s value(s) in m
Z value(s)
slim (S values corresponding to rlim and zlim)
"""
# Find corner and pick direction around limiter to go
r_corner, z_corner = east_divertor_corners[surface]
wc = np.where((r_corner == rlim) & (z_corner == zlim))[0]
dr_fwd, dz_fwd = rlim[wc + 1] - rlim[wc], zlim[wc + 1] - zlim[wc]
dr_rev, dz_rev = rlim[wc - 1] - rlim[wc], zlim[wc - 1] - zlim[wc]
theta_fwd, theta_rev = np.arctan2(dz_fwd, dr_fwd), np.arctan2(dz_rev, dr_rev)
theta_vert = np.pi / 2 if z_corner > 0 else -np.pi / 2
direction = 1 if abs(theta_fwd - theta_vert) < abs(theta_rev - theta_vert) else -1
# Get limiter path starting at the relevant corner and going toward the most vertical direction
rlim = np.roll(rlim, -wc - int(direction == -1))[::direction]
zlim = np.roll(zlim, -wc - int(direction == -1))[::direction]
# Get distance along limiter starting from the corner
ds = np.append(0, np.sqrt(np.diff(rlim) ** 2 + np.diff(zlim) ** 2))
slim = np.cumsum(ds)
# Make sure the path is set up correctly
assert rlim[0] == r_corner
assert zlim[0] == z_corner
assert abs(np.arctan2(zlim[1] - zlim[0], rlim[1] - rlim[0]) - (theta_fwd if direction == 1 else theta_rev)) < np.pi / 500
assert slim[0] == 0
# Interpolate R, Z of limiter vs. distance from corner onto the distances for the probes
r = interp1d(slim, rlim)(s)
z = interp1d(slim, zlim)(s)
slim2 = np.roll(slim, wc + int(direction == -1))[::direction]
return r, z, slim2
[docs]@make_available_if(compare_version(imas_version, '3.25.0') >= 0) # Might actually be 3.24.X. Close enough for now.
def setup_langmuir_probes_hardware_description_east(ods, shot=None):
"""
Load EAST Langmuir probe locations into an ODS
:param ods: ODS instance
:param shot: int
Will try to fill in from ODS machine data if None
:return: dict
Information or instructions for follow up in central hardware description setup
"""
import MDSplus
if compare_version(ods.imas_version, '3.25.0') < 0:
printe('langmuir_probes.embedded requires a newer version of IMAS. It was added by 3.25.0.')
printe('ABORTED setup_langmuir_probes_hardware_description_east due to old IMAS version.')
return {}
try:
device = ods['dataset_description.data_entry.machine'] # This could be EAST_US instead of plain EAST, I guess?
except (ValueError, KeyError):
device = 'EAST'
if shot is None:
shot = ods['dataset_description.data_entry.pulse']
try:
rlim = ods['wall.description_2d[0].limiter.unit[0].outline.r']
zlim = ods['wall.description_2d[0].limiter.unit[0].outline.z']
except (ValueError, KeyError):
# Don't have rlim and zlim yet
printq('No limiter in ods. Gathering it from MDSplus to support ')
lim = OMFITmdsValue(server=device, shot=shot, treename='EFIT_EAST', TDI=r'\top.results.geqdsk.lim').data()
rlim = lim[:, 0]
zlim = lim[:, 1]
j = 0
for ul in ['upper', 'lower']:
for oi in ['outer', 'inner']:
corner = '{}{}'.format(ul[0], oi[0])
pointname = r'\{}LPS'.format(corner.upper())
# These are distance along the wall from a reference point in m for a group of probes
m = OMFITmdsValue(server=device, shot=shot, treename='ANALYSIS', TDI=pointname)
if m.check(check_dim_of=-1):
# Data appear to be valid; proceed
printq('Processing data for probe group {}; {}'.format(corner.upper(), pointname))
s = m.data()
r, z, _ = east_coords_along_wall(s, rlim, zlim, corner)
numbering_starts_at = 1
for i in range(len(r)):
# Probe numbering scheme confirmed with EAST handbook using data access 2018-07-16 by D. Eldon
probe_number = i + numbering_starts_at
identifier = '{}{:02d}'.format(corner.upper(), probe_number)
ods['langmuir_probes.embedded'][j]['position.r'] = r[i]
ods['langmuir_probes.embedded'][j]['position.z'] = z[i]
ods['langmuir_probes.embedded'][j]['position.phi'] = np.NaN # Didn't find this in MDSplus
ods['langmuir_probes.embedded'][j]['identifier'] = identifier
ods['langmuir_probes.embedded'][j]['name'] = identifier
j += 1
else:
printq('Failed MDSplus data check for {}; data invalid. Halting.'.format(pointname))
return {}
@make_available_if(approximate_is_okay)
def setup_gas_injection_hardware_description_east(ods, shot):
"""
Sets up APPROXIMATE EAST gas injector data for some systems.
Data sources:
Figure downloaded from EAST handbook into notes
Updated 2020-01-01 by David Eldon
:return: dict
Information or instructions for follow up in central hardware description setup
"""
i = 0
def port_angle(port):
"""
Converts a port letter into a toroidal angle
EAST has 16 segments. If A is segment 0, P is 15. Assumes port centers are equally spaced, which they appear to
be, or at least nearly so, based on a drawing from the EAST handbook.
:return: float
Angle in radians
"""
# I am guessing a toroidal angle coordinate system. I could be wrong by an offset and a direction.
offset = 0 # radians
direction = 1 # +/- 1
return string.ascii_lowercase.find(port.lower()) / 16.0 * 2 * np.pi * direction + offset
# OUPEV2
# I think it's between probes 8 & 9. I am guessing. This gives R, Z
# I think it's in port O
pipe = ods['gas_injection']['pipe'][i]
phi = port_angle('o')
pipe['name'] = 'OUPEV2_{:03d}'.format(int(round(phi * 180 / np.pi)))
pipe['exit_position']['r'] = 1.73 # m
pipe['exit_position']['z'] = 1.057 # m
pipe['exit_position']['phi'] = phi
pipe['valve'][0]['identifier'] = 'OUPEV2'
pipe['second_point']['phi'] = phi
pipe['second_point']['r'] = 1.729
pipe['second_point']['z'] = 1.05675
i += 1
# ODPEV2
# It's in the lower divertor. I'll have to eyeball from a drawing. Also, I am guessing which tip it is.
# I think it's in port O
pipe = ods['gas_injection']['pipe'][i]
phi = port_angle('o')
pipe['name'] = 'ODPEV2_{:03d}'.format(int(round(phi * 180 / np.pi)))
pipe['exit_position']['r'] = 1.811 # m
pipe['exit_position']['z'] = -0.972 # m
pipe['exit_position']['phi'] = phi
pipe['valve'][0]['identifier'] = 'ODPEV2'
pipe['second_point']['phi'] = phi
pipe['second_point']['r'] = 1.806
pipe['second_point']['z'] = -0.9715
i += 1
return {}