Source code for omfit_classes.omfit_omas_d3d

"""
Functions for adding DIII-D 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
import copy
from omfit_classes.omfit_ascii import OMFITascii
from omfit_classes.omfit_mds import OMFITmds, OMFITmdsValue
from omfit_classes.utils_base import compare_version
from omfit_classes.omfit_omas_utils import ensure_consistent_experiment_info

try:
    import omas

    imas_version = omas.ODS().imas_version
except Exception:
    imas_version = '0.0'

__all__ = ['OMFITd3dcompfile']


# 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


# Utilities
def printq(*args):
    return printd(*args, topic='omfit_omas_d3d')


# Hardware descriptor functions
[docs]@make_available def setup_gas_injection_hardware_description_d3d(ods, shot): """ Sets up DIII-D gas injector data. R and Z are from the tips of the arrows in puff_loc.pro; phi from angle listed in labels in puff_loc.pro . I recorded the directions of the arrows on the EFITviewer overlay, but I don't know how to include them in IMAS, so I commented them out. Warning: changes to gas injector configuration with time are not yet included. This is just the best picture I could make of the 2018 configuration. Data sources: EFITVIEWER: iris:/fusion/usc/src/idl/efitview/diagnoses/DIII-D/puff_loc.pro accessed 2018 June 05, revised 20090317 DIII-D webpage: https://diii-d.gat.com/diii-d/Gas_Schematic accessed 2018 June 05 DIII-D wegpage: https://diii-d.gat.com/diii-d/Gas_PuffLocations accessed 2018 June 05 Updated 2018 June 05 by David Eldon :return: dict Information or instructions for follow up in central hardware description setup """ if shot < 100775: warnings.warn('DIII-D Gas valve locations not applicable for shots earlier than 100775 (2000 JAN 17)') imas_version = getattr(ods, 'imas_version', None) separate_valves = imas_version is not None and compare_version(imas_version, "3.34.0") >= 0 i = 0 # Pipe index iv = 0 # Valve index def pipe_copy(pipe_in): pipe_out = ods['gas_injection']['pipe'][i] for field in ['name', 'exit_position.r', 'exit_position.z', 'exit_position.phi']: pipe_out[field] = copy.copy(pipe_in[field]) if separate_valves: valve_out = ods['gas_injection']['valve'][iv] pipe_out['valve_indices'] = [iv] else: valve_out = None return pipe_out, valve_out # PFX1 pipe_indices = [] for angle in [12, 139, 259]: # degrees, DIII-D hardware left handed coords pipe_pfx1 = ods['gas_injection']['pipe'][i] pipe_pfx1['name'] = 'PFX1_{:03d}'.format(angle) pipe_pfx1['exit_position']['r'] = 1.286 # m pipe_pfx1['exit_position']['z'] = 1.279 # m pipe_pfx1['exit_position']['phi'] = -np.pi / 180.0 * angle # radians, right handed if separate_valves: pipe_pfx1['valve_indices'] = [iv] else: pipe_pfx1['valve'][0]['identifier'] = 'PFX1' pipe_indices += [i] dr = -1.116 + 1.286 dz = -1.38 + 1.279 # pipea['exit_position']['direction'] = 180/np.pi * tan(dz/dr) if dr != 0 else 90 * sign(dz) pipe_pfx1['second_point']['phi'] = pipe_pfx1['exit_position']['phi'] pipe_pfx1['second_point']['r'] = pipe_pfx1['exit_position']['r'] + dr pipe_pfx1['second_point']['z'] = pipe_pfx1['exit_position']['z'] + dz i += 1 if separate_valves: valve_pfx1 = ods['gas_injection']['valve'][iv] valve_pfx1['identifier'] = 'PFX1' valve_pfx1['name'] = 'PFX1' valve_pfx1['pipe_indices'] = pipe_indices iv += 1 # PFX2 injects at the same poloidal locations as PFX1, but at different toroidal angles pipe_indices = [] for angle in [79, 199, 319]: # degrees, DIII-D hardware left handed coords pipe_copy(pipe_pfx1) pipe_pfx2 = ods['gas_injection']['pipe'][i] pipe_pfx2['name'] = 'PFX2_{:03d}'.format(angle) pipe_pfx2['exit_position']['phi'] = -np.pi / 180.0 * angle # rad if separate_valves: pipe_pfx2['valve_indices'] = [iv] else: pipe_pfx2['valve'][0]['identifier'] = 'PFX2' pipe_indices += [i] pipe_pfx2['second_point']['phi'] = pipe_pfx2['exit_position']['phi'] i += 1 if separate_valves: valve_pfx2 = ods['gas_injection']['valve'][iv] valve_pfx2['identifier'] = 'PFX2' valve_pfx2['name'] = 'PFX2' valve_pfx2['pipe_indices'] = pipe_indices iv += 1 # GAS A pipea = ods['gas_injection']['pipe'][i] pipea['name'] = 'GASA_300' pipea['exit_position']['r'] = 1.941 # m pipea['exit_position']['z'] = 1.01 # m pipea['exit_position']['phi'] = -np.pi / 180.0 * 300 # rad # pipea['exit_position']['direction'] = 270. # degrees, giving dir of pipe leading towards injector, up is 90 pipea['second_point']['phi'] = pipea['exit_position']['phi'] pipea['second_point']['r'] = pipea['exit_position']['r'] pipea['second_point']['z'] = pipea['exit_position']['z'] - 0.01 if separate_valves: pipea['valve_indices'] = [iv] valvea = ods['gas_injection']['valve'][iv] valvea['identifier'] = 'GASA' valvea['name'] = 'GASA' valvea['pipe_indices'] = [i] else: pipea['valve'][0]['identifier'] = 'GASA' i += 1 iv += 1 # GAS B injects in the same place as GAS A pipeb, valveb = pipe_copy(pipea) pipeb['name'] = 'GASB_300' if separate_valves: valveb['name'] = valveb['identifier'] = 'GASB' valveb['pipe_indices'] = [i] else: pipeb['valve'][0]['identifier'] = 'GASB' i += 1 iv += 1 # GAS C pipec = ods['gas_injection']['pipe'][i] pipec['name'] = 'GASC_000' pipec['exit_position']['r'] = 1.481 # m pipec['exit_position']['z'] = -1.33 # m pipec['exit_position']['phi'] = -np.pi / 180.0 * 0 # pipec['exit_position']['direction'] = 90. # degrees, giving direction of pipe leading towards injector pipec['second_point']['phi'] = pipec['exit_position']['phi'] pipec['second_point']['r'] = pipec['exit_position']['r'] pipec['second_point']['z'] = pipec['exit_position']['z'] + 0.01 if separate_valves: valvec = ods['gas_injection']['valve'][iv] valvec['name'] = 'GASC' valvec['identifier'] = 'GASC' valvec['pipe_indices'] = [i] pipec['valve_indices'] = [iv] else: pipec['valve'][0]['identifier'] = 'GASC' i += 1 iv += 1 # GAS D injects at the same poloidal location as GAS A, but at a different toroidal angle. # There is a GASD piezo valve that splits into four injectors, all of which have their own gate valves and can be # turned on/off independently. Normally, only one would be used at at a time. pipe_copy(pipea) piped = ods['gas_injection']['pipe'][i] piped['name'] = 'GASD_225' # This is the injector name piped['exit_position']['phi'] = -np.pi / 180.0 * 225 piped['second_point']['phi'] = piped['exit_position']['phi'] if separate_valves: valved = ods['gas_injection']['valve'][iv] valved['name'] = valved['identifier'] = 'GASD' # This is the piezo name valved['pipe_indices'] = [i] piped['valve_indices'] = [iv] else: piped['valve'][0]['identifier'] = 'GASD' i += 1 iv += 1 # GASE is at the same poloidal location as GASC pipee, valvee = pipe_copy(pipec) if separate_valves: valvee['name'] = valvee['identifier'] = 'GASE' valvee['pipe_indices'] = [i] else: pipee['valve'][0]['identifier'] = 'GASE' i += 1 iv += 1 # Spare 225 is an extra branch of the GASD line after the GASD piezo (might not have a valve) pipe_copy(piped) pipes225 = ods['gas_injection']['pipe'][i] pipes225['name'] = 'Spare_225' # This is the injector name i += 1 # RF_170 and RF_190: gas ports near the 180 degree antenna, on the GASD line # I don't think these are connected anymore, but I don't know the shot number after which they were removed. for angle in [170, 190]: pipe_rf = ods['gas_injection']['pipe'][i] pipe_rf['name'] = 'RF_{:03d}'.format(angle) pipe_rf['exit_position']['r'] = 2.38 # m pipe_rf['exit_position']['z'] = -0.13 # m pipe_rf['exit_position']['phi'] = -np.pi / 180.0 * angle # rad i += 1 # DRDP pipe_copy(piped) pipedrdp = ods['gas_injection']['pipe'][i] pipedrdp['name'] = 'DRDP_225' if separate_valves: pipedrdp['valve_indices'] = [iv] valvedrdp = ods['gas_injection']['valve'][iv] valvedrdp['identifier'] = 'DRDP' valvedrdp['name'] = 'DRDP' valvedrdp['pipe_indices'] = [i] else: pipedrdp['valve'][0]['identifier'] = 'DRDP' i += 1 iv += 1 # UOB pipe_indices = [] for angle in [45, 165, 285]: pipe_uob = ods['gas_injection']['pipe'][i] pipe_uob['name'] = 'UOB_{:03d}'.format(angle) pipe_uob['exit_position']['r'] = 1.517 # m pipe_uob['exit_position']['z'] = 1.267 # m pipe_uob['exit_position']['phi'] = -np.pi / 180.0 * angle pipe_indices += [i] if separate_valves: pipe_uob['valve_indices'] = [iv] else: pipe_uob['valve'][0]['identifier'] = 'UOB' # pipe_uob['exit_position']['direction'] = 270. # degrees, giving dir of pipe leading to injector, up is 90 i += 1 if separate_valves: valve_uob = ods['gas_injection']['valve'][iv] valve_uob['name'] = valve_uob['identifier'] = 'UOB' valve_uob['pipe_indices'] = pipe_indices iv += 1 # LOB1 pipe_indices = [] for angle in [30, 120]: pipe_lob1 = ods['gas_injection']['pipe'][i] pipe_lob1['name'] = 'LOB1_{:03d}'.format(angle) pipe_lob1['exit_position']['r'] = 1.941 # m pipe_lob1['exit_position']['z'] = -1.202 # m pipe_lob1['exit_position']['phi'] = -np.pi / 180.0 * angle if separate_valves: pipe_lob1['valve_indices'] = [iv] else: pipe_lob1['valve'][0]['identifier'] = 'LOB1' pipe_indices += [i] # pipe_lob1['exit_position']['direction'] = 180. # degrees, giving dir of pipe leading to injector; up is 90 i += 1 if separate_valves: valve_lob1 = ods['gas_injection']['valve'][iv] valve_lob1['name'] = valve_lob1['identifier'] = 'LOB1' valve_lob1['pipe_indices'] = pipe_indices iv += 1 # Spare 75 is an extra branch of the GASC line after the LOB1 piezo pipes75 = ods['gas_injection']['pipe'][i] pipes75['name'] = 'Spare_075' pipes75['exit_position']['r'] = 2.249 # m (approximate / estimated from still image) pipes75['exit_position']['z'] = -0.797 # m (approximate / estimated from still image) pipes75['exit_position']['phi'] = 75 # degrees, DIII-D hardware left handed coords if separate_valves: pipes75['valve_indices'] = pipe_lob1['valve_indices'] valve_lob1['pipe_indices'] = np.append(valve_lob1['pipe_indices'], [i]) else: pipes75['valve'][0]['identifier'] = 'LOB1' # pipes75['exit_position']['direction'] = 180. # degrees, giving direction of pipe leading towards injector i += 1 # RF_010 & 350 for angle in [10, 350]: pipe_rf_lob1 = ods['gas_injection']['pipe'][i] pipe_rf_lob1['name'] = 'RF_{:03d}'.format(angle) pipe_rf_lob1['exit_position']['r'] = 2.38 # m pipe_rf_lob1['exit_position']['z'] = -0.13 # m pipe_rf_lob1['exit_position']['phi'] = -np.pi / 180.0 * angle if separate_valves: pipe_rf_lob1['valve_indices'] = pipe_lob1['valve_indices'] valve_lob1['pipe_indices'] = np.append(valve_lob1['pipe_indices'], [i]) else: pipe_rf_lob1['valve'][0]['identifier'] = 'LOB1' # pipe_rf10['exit_position']['direction'] = 180. # degrees, giving dir of pipe leading to injector; up is 90 i += 1 # DiMES chimney pipe_dimesc = ods['gas_injection']['pipe'][i] pipe_dimesc['name'] = 'DiMES_Chimney_165' pipe_dimesc['exit_position']['r'] = 1.481 # m pipe_dimesc['exit_position']['z'] = -1.33 # m pipe_dimesc['exit_position']['phi'] = -np.pi / 180.0 * 165 if separate_valves: pipe_dimesc['valve_indices'] = [iv] cpgas_valve = ods['gas_injection']['valve'][iv] cpgas_valve['identifier'] = '240R-2' cpgas_valve['name'] = '240R-2 (PCS use GASD)' cpgas_valve['pipe_indices'] = [i] else: pipe_dimesc['valve'][0]['identifier'] = '240R-2' # pipe_dimesc['exit_position']['direction'] = 90. # degrees, giving dir of pipe leading towards injector, up is 90 i += 1 iv += 1 # CPBOT pipe_cpbot = ods['gas_injection']['pipe'][i] pipe_cpbot['name'] = 'CPBOT_150' pipe_cpbot['exit_position']['r'] = 1.11 # m pipe_cpbot['exit_position']['z'] = -1.33 # m pipe_cpbot['exit_position']['phi'] = -np.pi / 180.0 * 150 if separate_valves: pipe_cpbot['valve_indices'] = pipe_dimesc['valve_indices'] cpgas_valve['pipe_indices'] = np.append(cpgas_valve['pipe_indices'], [i]) else: pipe_cpbot['valve'][0]['identifier'] = '240R-2' # pipe_cpbot['exit_position']['direction'] = 0. # degrees, giving dir of pipe leading towards injector, up is 90 i += 1 # LOB2 injects at the same poloidal locations as LOB1, but at different toroidal angles if separate_valves: valve_lob2 = ods['gas_injection']['valve'][iv] valve_lob2['identifier'] = 'LOB2' valve_lob2['name'] = 'LOB2' valve_lob2['pipe_indices'] = [] for angle in [210, 300]: pipe_copy(pipe_lob1) pipe_lob2 = ods['gas_injection']['pipe'][i] pipe_lob2['name'] = 'LOB2_{:03d}'.format(angle) pipe_lob2['exit_position']['phi'] = -np.pi / 180.0 * angle # degrees, DIII-D hardware left handed coords if separate_valves: pipe_lob2['valve_indices'] = [iv] valve_lob2['pipe_indices'] = np.append(valve_lob2['pipe_indices'], [i]) else: pipe_lob2['valve'][0]['identifier'] = 'LOB2' i += 1 iv += 1 # Dimes floor tile 165 pipe_copy(pipec) pipe_dimesf = ods['gas_injection']['pipe'][i] pipe_dimesf['name'] = 'DiMES_Tile_160' pipe_dimesf['exit_position']['phi'] = -np.pi / 180.0 * 165 if separate_valves: pipe_dimesf['valve_indices'] = pipe_lob2['valve_indices'] valve_lob2['pipe_indices'] = np.append(valve_lob2['pipe_indices'], [i]) else: pipe_dimesf['valve'][0]['identifier'] = 'LOB2' i += 1 # RF COMB pipe_rfcomb = ods['gas_injection']['pipe'][i] pipe_rfcomb['name'] = 'RF_COMB_' pipe_rfcomb['exit_position']['r'] = 2.38 # m pipe_rfcomb['exit_position']['z'] = -0.13 # m # pipe_rfcomb['exit_position']['phi'] = Unknown, sorry if separate_valves: pipe_rfcomb['valve_indices'] = pipe_lob2['valve_indices'] valve_lob2['pipe_indices'] = np.append(valve_lob2['pipe_indices'], [i]) else: pipe_rfcomb['valve'][0]['identifier'] = 'LOB2' # pipe_rf307['exit_position']['direction'] = 180. # degrees, giving dir of pipe leading towards injector, up is 90 i += 1 # RF307 pipe_rf307 = ods['gas_injection']['pipe'][i] pipe_rf307['name'] = 'RF_307' pipe_rf307['exit_position']['r'] = 2.38 # m pipe_rf307['exit_position']['z'] = -0.13 # m pipe_rf307['exit_position']['phi'] = -np.pi / 180.0 * 307 if separate_valves: pipe_rf307['valve_indices'] = pipe_lob2['valve_indices'] valve_lob2['pipe_indices'] = np.append(valve_lob2['pipe_indices'], [i]) else: pipe_rf307['valve'][0]['identifier'] = 'LOB2' # pipe_rf307['exit_position']['direction'] = 180. # degrees, giving dir of pipe leading towards injector, up is 90 i += 1 # GAS H injects in the same place as GAS C pipe_copy(pipec) pipeh = ods['gas_injection']['pipe'][i] pipeh['name'] = 'GASH_000' if separate_valves: pipeh['valve_indices'] = [] # This one's not on the manifold schematic i += 1 # GAS I injects in the same place as GAS C pipe_copy(pipec) pipei = ods['gas_injection']['pipe'][i] pipei['name'] = 'GASI_000' if separate_valves: pipei['valve_indices'] = [] # This one's not on the manifold schematic i += 1 # GAS J injects in the same place as GAS D pipe_copy(piped) pipej = ods['gas_injection']['pipe'][i] pipej['name'] = 'GASJ_225' if separate_valves: pipej['valve_indices'] = [] # This one's not on the manifold schematic i += 1 # RF260 pipe_rf260 = ods['gas_injection']['pipe'][i] pipe_rf260['name'] = 'RF_260' pipe_rf260['exit_position']['r'] = 2.38 # m pipe_rf260['exit_position']['z'] = 0.14 # m pipe_rf260['exit_position']['phi'] = -np.pi / 180.0 * 260 if separate_valves: pipe_rf260['valve_indices'] = pipe_lob2['valve_indices'] # Seems to have been removed. May have been on LOB2, though. valve_lob2['pipe_indices'] = np.append(valve_lob2['pipe_indices'], [i]) else: pipe_rf260['valve'][0]['identifier'] = 'LOB2' # pipe_rf260['exit_position']['direction'] = 180. # degrees, giving dir of pipe leading towards injector, up is 90 i += 1 # CPMID pipe_cpmid = ods['gas_injection']['pipe'][i] pipe_cpmid['name'] = 'CPMID' pipe_cpmid['exit_position']['r'] = 0.9 # m pipe_cpmid['exit_position']['z'] = -0.2 # m if separate_valves: pipe_cpmid['valve_indices'] = [] # Seems to have been removed. Not on schematic. # pipe_cpmid['exit_position']['direction'] = 0. # degrees, giving dir of pipe leading towards injector, up is 90 i += 1 return {}
# The magnetic hardware description function (for bpol probes and flux loops) has been generalized & is in omfit_omas.py # The pulse_schedule hardware description function has been generalized and is in omfit_omas.py # The position_control hardware description function (a subset of pulse_schedule) is in omfit_omas.py
[docs]@make_available def setup_pf_active_hardware_description_d3d(ods, *args): r""" Adds DIII-D 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 # From iris:/fusion/usc/src/idl/efitview/diagnoses/DIII-D/coils.dat , accessed 2018 June 08 D. Eldon fc_dat = np.array( [ # R Z dR dZ tilt1 tilt2 [0.8608, 0.16830, 0.0508, 0.32106, 0.0, 0.0], # 0 in the last column really means 90 degrees. [0.8614, 0.50810, 0.0508, 0.32106, 0.0, 0.0], [0.8628, 0.84910, 0.0508, 0.32106, 0.0, 0.0], [0.8611, 1.1899, 0.0508, 0.32106, 0.0, 0.0], [1.0041, 1.5169, 0.13920, 0.11940, 45.0, 0.0], [2.6124, 0.4376, 0.17320, 0.1946, 0.0, 92.40], [2.3733, 1.1171, 0.1880, 0.16920, 0.0, 108.06], [1.2518, 1.6019, 0.23490, 0.08510, 0.0, 0.0], [1.6890, 1.5874, 0.16940, 0.13310, 0.0, 0.0], [0.8608, -0.17370, 0.0508, 0.32106, 0.0, 0.0], [0.8607, -0.51350, 0.0508, 0.32106, 0.0, 0.0], [0.8611, -0.85430, 0.0508, 0.32106, 0.0, 0.0], [0.8630, -1.1957, 0.0508, 0.32106, 0.0, 0.0], [1.0025, -1.5169, 0.13920, 0.11940, -45.0, 0.0], [2.6124, -0.4376, 0.17320, 0.1946, 0.0, -92.40], [2.3834, -1.1171, 0.1880, 0.16920, 0.0, -108.06], [1.2524, -1.6027, 0.23490, 0.08510, 0.0, 0.0], [1.6889, -1.5780, 0.16940, 0.13310, 0.0, 0.0], ] ) ods = pf_coils_to_ods(ods, fc_dat) for i in range(len(fc_dat[:, 0])): fcid = 'F{}{}'.format((i % 9) + 1, 'AB'[int(fc_dat[i, 1] < 0)]) 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 setup_interferometer_hardware_description_d3d(ods, shot): """ Writes DIII-D CO2 interferometer chord locations into ODS. The chord endpoints ARE NOT RIGHT. Only the R for vertical lines or Z for horizontal lines is right. Data sources: DIII-D webpage: https://diii-d.gat.com/diii-d/Mci accessed 2018 June 07 D. Eldon :param ods: an OMAS ODS instance :param shot: int :return: dict Information or instructions for follow up in central hardware description setup """ # As of 2018 June 07, DIII-D has four interferometers # phi angles are compliant with odd COCOS ods['interferometer.channel.0.identifier'] = 'r0' r0 = ods['interferometer.channel.0.line_of_sight'] r0['first_point.phi'] = r0['second_point.phi'] = 225 * (-np.pi / 180.0) r0['first_point.r'], r0['second_point.r'] = 3.0, 0.8 # These are not the real endpoints r0['first_point.z'] = r0['second_point.z'] = 0.0 for i, r in enumerate([1.48, 1.94, 2.10]): ods['interferometer.channel'][i + 1]['identifier'] = 'v{}'.format(i + 1) los = ods['interferometer.channel'][i + 1]['line_of_sight'] los['first_point.phi'] = los['second_point.phi'] = 240 * (-np.pi / 180.0) los['first_point.r'] = los['second_point.r'] = r los['first_point.z'], los['second_point.z'] = -1.8, 1.8 # These are not the real points for i in range(len(ods['interferometer.channel'])): ch = ods['interferometer.channel'][i] ch['line_of_sight.third_point'] = copy.copy(ch['line_of_sight.first_point']) if shot < 125406: printw( 'DIII-D CO2 pointnames were different before shot 125406. The physical locations of the chords seems to ' 'have been the same, though, so there has not been a problem yet (I think).' ) return {}
def find_thomson_lens_d3d(shot, hw_call_sys, hw_revision='blessed'): """Read the Thomson scattering hardware map to figure out which lens each chord looks through""" cal_call = '.ts.{}.header.calib_nums'.format(hw_revision) cal_set = OMFITmdsValue(server='DIII-D', treename='ELECTRONS', shot=shot, TDI=cal_call).data()[0] hwi_call = '.{}.hwmapints'.format(hw_call_sys) printq(' Reading hw map int values: treename = "tscal", cal_set = {}, hwi_call = {}'.format(cal_set, hwi_call)) try: hw_ints = OMFITmdsValue(server='DIII-D', treename='tscal', shot=cal_set, TDI=hwi_call).data() except MDSplus.MdsException: printw('WARNING: Error reading Thomson scattering hardware map to determine which lenses were used!') return None else: if len(np.shape(hw_ints)) < 2: # Contingency needed for cases where all view-chords are taken off of divertor laser and reassigned to core hw_ints = hw_ints.reshape(1, -1) hw_lens = hw_ints[:, 2] return hw_lens
[docs]@make_available def setup_thomson_scattering_hardware_description_d3d(ods, shot, revision='BLESSED'): """ Gathers DIII-D Thomson measurement locations from MDSplus and loads them into OMAS :param revision: string Thomson scattering data revision, like 'BLESSED', 'REVISIONS.REVISION00', etc. :return: dict Information or instructions for follow up in central hardware description setup """ printq('Setting up DIII-D Thomson locations...') tsdat = OMFITmds(server='DIII-D', treename='ELECTRONS', shot=shot)['TS'][revision] is_subsys = np.array([np.all([item in tsdat[k] for item in ['DENSITY', 'TEMP', 'R', 'Z']]) for k in list(tsdat.keys())]) subsystems = np.array(list(tsdat.keys()))[is_subsys] i = 0 for sub in subsystems: lenses = find_thomson_lens_d3d(shot, sub, revision) try: nc = len(tsdat[sub]['R'].data()) except (TypeError, KeyError): nc = 0 for j in range(nc): ch = ods['thomson_scattering']['channel'][i] ch['name'] = 'TS_{sub:}_r{lens:+0d}_{ch:}'.format(sub=sub.lower(), ch=j, lens=lenses[j] if lenses is not None else -9) ch['identifier'] = '{}{:02d}'.format(sub[0], j) for pos in ['R', 'Z', 'PHI']: ch['position'][pos.lower()] = tsdat[sub][pos].data()[j] * (-np.pi / 180.0 if pos == 'PHI' else 1) i += 1 return {}
[docs]@make_available def setup_charge_exchange_hardware_description_d3d(ods, shot, analysis_type='CERQUICK'): """ Gathers DIII-D CER measurement locations from MDSplus and loads them into OMAS :param analysis_type: string CER analysis quality level like CERQUICK, CERAUTO, or CERFIT. CERQUICK is probably fine. :return: dict Information or instructions for follow up in central hardware description setup """ printq('Setting up DIII-D CER locations...') cerdat = OMFITmds(server='DIII-D', treename='IONS', shot=shot)['CER'][analysis_type] subsystems = np.array([k for k in list(cerdat.keys()) if 'CHANNEL01' in list(cerdat[k].keys())]) i = 0 for sub in subsystems: try: channels = [k for k in list(cerdat[sub].keys()) if 'CHANNEL' in k] except (TypeError, KeyError): channels = [] for j, channel in enumerate(channels): inc = 0 for pos in ['R', 'Z', 'VIEW_PHI']: postime = cerdat[sub][channel]['TIME'].data() posdat = cerdat[sub][channel][pos].data() if postime is not None: inc = 1 ch = ods['charge_exchange']['channel'][i] ch['name'] = 'imCERtang_{sub:}{ch:02d}'.format(sub=sub.lower()[0], ch=j + 1) ch['identifier'] = '{}{:02d}'.format(sub[0], j + 1) chpos = ch['position'][pos.lower().split('_')[-1]] chpos['time'] = postime / 1000.0 # Convert ms to s chpos['data'] = posdat * -np.pi / 180.0 if (pos == 'VIEW_PHI') and posdat is not None else posdat i += inc return {}
[docs]@make_available def setup_bolometer_hardware_description_d3d(ods, shot): """ Load DIII-D bolometer chord locations into the ODS Data sources: - iris:/fusion/usc/src/idl/efitview/diagnoses/DIII-D/bolometerpaths.pro - OMFIT-source/modules/_PCS_prad_control/SETTINGS/PHYSICS/reference/DIII-D/bolometer_geo , access 2018June11 Eldon :return: dict Information or instructions for follow up in central hardware description setup """ printq('Setting up DIII-D bolometer locations...') if shot < 91000: xangle = ( np.array( [ 292.40, 288.35, 284.30, 280.25, 276.20, 272.15, 268.10, 264.87, 262.27, 259.67, 257.07, 254.47, 251.87, 249.27, 246.67, 243.81, 235.81, 227.81, 219.81, 211.81, 203.81, 195.81, 187.81, 179.80, 211.91, 206.41, 200.91, 195.41, 189.91, 184.41, 178.91, 173.41, 167.91, 162.41, 156.91, 156.30, 149.58, 142.86, 136.14, 129.77, 126.77, 123.77, 120.77, 117.77, 114.77, 111.77, 108.77, 102.25, ] ) * np.pi / 180.0 ) # Converted to rad xangle_width = None zxray = ( np.array( [ 124.968, 124.968, 124.968, 124.968, 124.968, 124.968, 124.968, 124.968, 124.968, 124.968, 124.968, 124.968, 124.968, 124.968, 124.968, 129.870, 129.870, 129.870, 129.870, 129.870, 129.870, 129.870, 129.870, 129.870, -81.153, -81.153, -81.153, -81.153, -81.153, -81.153, -81.153, -81.153, -81.153, -81.153, -81.153, -72.009, -72.009, -72.009, -72.009, -72.009, -72.009, -72.009, -72.009, -72.009, -72.009, -72.009, -72.009, -72.009, ] ) / 100.0 ) # Converted to m rxray = ( np.array( [ 196.771, 196.771, 196.771, 196.771, 196.771, 196.771, 196.771, 196.771, 196.771, 196.771, 196.771, 196.771, 196.771, 196.771, 196.771, 190.071, 190.071, 190.071, 190.071, 190.071, 190.071, 190.071, 190.071, 190.071, 230.720, 230.720, 230.720, 230.720, 230.720, 230.720, 230.720, 230.720, 230.720, 230.720, 230.720, 232.900, 232.900, 232.900, 232.900, 232.900, 232.900, 232.900, 232.900, 232.900, 232.900, 232.900, 232.900, 232.900, ] ) / 100.0 ) # Converted to m else: xangle = ( np.array( [ # There is a bigger step before the very last channel. Found in two different sources. 269.4, 265.6, 261.9, 258.1, 254.4, 250.9, 247.9, 244.9, 241.9, 238.9, 235.9, 232.9, 228.0, 221.3, 214.5, 208.2, 201.1, 194.0, 187.7, 182.2, 176.7, 171.2, 165.7, 160.2, 213.7, 210.2, 206.7, 203.2, 199.7, 194.4, 187.4, 180.4, 173.4, 166.4, 159.4, 156.0, 149.2, 142.4, 135.8, 129.6, 126.6, 123.6, 120.6, 117.6, 114.6, 111.6, 108.6, 101.9, ] ) * np.pi / 180.0 ) # Converted to rad xangle_width = ( np.array( [ 3.082, 3.206, 3.317, 3.414, 3.495, 2.866, 2.901, 2.928, 2.947, 2.957, 2.960, 2.955, 6.497, 6.342, 6.103, 6.331, 6.697, 6.979, 5.510, 5.553, 5.546, 5.488, 5.380, 5.223, 3.281, 3.348, 3.402, 3.444, 3.473, 6.950, 6.911, 6.768, 6.526, 6.188, 5.757, 5.596, 5.978, 6.276, 6.490, 2.979, 2.993, 2.998, 2.995, 2.984, 2.965, 2.938, 2.902, 6.183, ] ) * np.pi / 180.0 ) # Angular full width of the view-chord: calculations assume it's a symmetric cone. zxray = ( np.array( [ 72.817, 72.817, 72.817, 72.817, 72.817, 72.817, 72.817, 72.817, 72.817, 72.817, 72.817, 72.817, 72.817, 72.817, 72.817, 82.332, 82.332, 82.332, 82.332, 82.332, 82.332, 82.332, 82.332, 82.332, -77.254, -77.254, -77.254, -77.254, -77.254, -77.254, -77.254, -77.254, -77.254, -77.254, -77.254, -66.881, -66.881, -66.881, -66.881, -66.881, -66.881, -66.881, -66.881, -66.881, -66.881, -66.881, -66.881, -66.881, ] ) / 100.0 ) # Converted to m rxray = ( np.array( [ 234.881, 234.881, 234.881, 234.881, 234.881, 234.881, 234.881, 234.881, 234.881, 234.881, 234.881, 234.881, 234.881, 234.881, 234.881, 231.206, 231.206, 231.206, 231.206, 231.206, 231.206, 231.206, 231.206, 231.206, 231.894, 231.894, 231.894, 231.894, 231.894, 231.894, 231.894, 231.894, 231.894, 231.894, 231.894, 234.932, 234.932, 234.932, 234.932, 234.932, 234.932, 234.932, 234.932, 234.932, 234.932, 234.932, 234.932, 234.932, ] ) / 100.0 ) # Converted to m line_len = 3 # m Make this long enough to go past the box for all chords. phi = np.array([60, 75])[(zxray > 0).astype(int)] * -np.pi / 180.0 # Convert to CCW radians fan = np.array(['Lower', 'Upper'])[(zxray > 0).astype(int)] fan_offset = np.array([0, int(len(rxray) // 2)])[(zxray < 0).astype(int)].astype(int) for i in range(len(zxray)): cnum = i + 1 - fan_offset[i] ods['bolometer']['channel'][i]['identifier'] = '{}{:02d}'.format(fan[i][0], cnum) ods['bolometer']['channel'][i]['name'] = '{} fan ch#{:02d}'.format(fan[i], cnum) cls = ods['bolometer']['channel'][i]['line_of_sight'] # Shortcut cls['first_point.r'] = rxray[i] cls['first_point.z'] = zxray[i] cls['first_point.phi'] = phi[i] cls['second_point.r'] = rxray[i] + line_len * np.cos(xangle[i]) cls['second_point.z'] = zxray[i] + line_len * np.sin(xangle[i]) cls['second_point.phi'] = cls['first_point.phi'] return {'postcommands': ['trim_bolometer_second_points_to_box(ods)']}
[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_d3d(ods, shot): """ Load DIII-D Langmuir probe locations into an ODS :param ods: ODS instance :param shot: int :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_d3d due to old IMAS version.') return {} ods['dataset_description.data_entry'].setdefault('machine', 'DIII-D') ods['dataset_description.data_entry'].setdefault('pulse', shot) tdi = r'GETNCI("\\langmuir::top.probe_*.r", "LENGTH")' # "LENGTH" is the size of the data, I think (in bits?). Single scalars seem to be length 12. printq('Setting up Langmuir probes hardware description, shot {}; checking availability, TDI={}'.format(shot, tdi)) m = OMFITmdsValue(server='DIII-D', shot=shot, treename='LANGMUIR', TDI=tdi) try: data_present = m.data() > 0 except MDSplus.MdsException: data_present = [] nprobe = len(data_present) printq('Looks like up to {} Langmuir probes might have valid data for {}'.format(nprobe, shot)) j = 0 for i in range(nprobe): if data_present[i]: r = OMFITmdsValue(server='DIII-D', shot=shot, treename='langmuir', TDI=r'\langmuir::top.probe_{:03d}.r'.format(i)) chk = r.check(debug=True, check_dim_of=-1) # Don't check dimensions on these data if chk['result'] and r.data() > 0: # Don't bother gathering more if r is junk z = OMFITmdsValue(server='DIII-D', shot=shot, treename='langmuir', TDI=r'\langmuir::top.probe_{:03d}.z'.format(i)) pnum = OMFITmdsValue(server='DIII-D', shot=shot, treename='langmuir', TDI=r'\langmuir::top.probe_{:03d}.pnum'.format(i)) label = OMFITmdsValue(server='DIII-D', shot=shot, treename='langmuir', TDI=r'\langmuir::top.probe_{:03d}.label'.format(i)) printq(' Probe i={i:}, j={j:}, label={label:} passed the check; r={r:}, z={z:}'.format(**locals())) ods['langmuir_probes.embedded'][j]['position.r'] = r.data()[0] ods['langmuir_probes.embedded'][j]['position.z'] = z.data()[0] ods['langmuir_probes.embedded'][j]['position.phi'] = np.NaN # Didn't find this in MDSplus ods['langmuir_probes.embedded'][j]['identifier'] = 'PROBE_{:03d}: PNUM={}'.format(i, pnum.data()[0]) ods['langmuir_probes.embedded'][j]['name'] = str(label.data()[0]).strip() j += 1 else: printq('Probe i={i:}, j={j:}, r={r:} failed the check with chk={chk:}'.format(**locals())) return {}
[docs]@make_available def find_active_d3d_probes(shot, allowed_probes=None): """ Serves LP functions by identifying active probes (those that have actual data saved) for a given shot Sorry, I couldn't figure out how to do this with a server-side loop over all the probes, so we have to loop MDS calls on the client side. At least I resampled to speed that part up. This could be a lot faster if I could figure out how to make the GETNCI commands work on records of array signals. :param shot: int :param allowed_probes: int array Restrict the search to a certain range of probe numbers to speed things up These are the numbers of storage trees in MDSplus, not the physical probe numbers :return: list of ints """ print(f'Searching for active DIII-D probes in shot {shot}...') omv_kw = dict(server='DIII-D', shot=shot, treename='langmuir') lp_paths_unfiltered = OMFITmdsValue(TDI=r'GETNCI("\\TOP.PROBE_*.TEMP", "MINPATH")', **omv_kw).data() lp_paths = [lpp.strip() for lpp in lp_paths_unfiltered if re.match('[0-9]', lpp.strip().split(':')[0].split('_')[-1])] print(f'{len(lp_paths)} possible probes found') lp_nums = np.array([int(lpp.strip().split(':')[0].split('_')[-1]) for lpp in lp_paths]) idx = lp_nums.argsort() lp_nums = lp_nums[idx] lp_paths = np.array(lp_paths)[idx] if allowed_probes is None: acceptable_probe_numbers = lp_nums acceptable_paths = lp_paths else: acceptable_probe_numbers = np.array([lpn for lpn in lp_nums if lpn in allowed_probes]) acceptable_paths = np.array([lp_paths[i] for i in range(len(lp_nums)) if lp_nums[i] in allowed_probes]) print(f'{len(acceptable_paths)} candidates remain after limiting search to only some specific probes.') valid = [None] * len(acceptable_paths) OMFIT['acceptable_paths'] = acceptable_paths OMFIT['acceptable_probe_numbers'] = acceptable_probe_numbers OMFIT['lp_nums'] = lp_nums OMFIT['lp_paths'] = lp_paths i = wd = tc = 0 for i in range(len(acceptable_paths)): ascii_progress_bar( i, 0, len(acceptable_paths), mess=f'Checking DIII-D#{shot} LP probes for valid data {lp_paths[i]} valid={valid[i]}; ' f'probes with valid data so far = {wd}/{tc}', ) m = OMFITmdsValue(TDI=f'resample({acceptable_paths[i]}, 0, 1, 1)', **omv_kw) valid[i] = m.check() and np.any(m.data() > 0) wd = np.sum(np.array(valid).astype(bool)) tc = len([a for a in valid if a is not None]) ascii_progress_bar( i + 1, 0, len(acceptable_paths), mess=f'Checked DIII-D#{shot} LP probes for valid data and found {wd} out of {tc} probes have valid data.', ) print() return acceptable_probe_numbers[np.array(valid).astype(bool)]
[docs]@make_available_if(compare_version(imas_version, '3.25.0') >= 0) # Might actually be 3.24.X. Close enough for now. def load_data_langmuir_probes_d3d( ods, shot, probes=None, allowed_probes=None, tstart=0, tend=0, dt=0.0002, overwrite=False, quantities=None ): """ Downloads LP probe data from MDSplus and loads them to the ODS :param ods: ODS instance :param shot: int :param probes: int array-like [optional] Integer array of DIII-D probe numbers. If not provided, find_active_d3d_probes() will be used. :param allowed_probes: int array-like [optional] Passed to find_active_d3d_probes(), if applicable. Improves speed by limiting search to a specific range of probe numbers. :param tstart: float Time to start resample (s) :param tend: float Time to end resample (s) Set to <= tstart to disable resample Server-side resampling does not work when time does not increase monotonically, which is a typical problem for DIII-D data. Resampling is not recommended for DIII-D. :param dt: float Resample interval (s) Set to 0 to disable resample Server-side resampling does not work when time does not increase monotonically, which is a typical problem for DIII-D data. Resampling is not recommended for DIII-D. :param overwrite: bool Download and write data even if they already are present in the ODS. :param quantities: list of strings [optional] List of quantities to gather. None to gather all available. Options are: ion_saturation_current, heat_flux_parallel, n_e, t_e', surface_area_effective, v_floating, and b_field_angle :return: ODS instance The data are added in-place, so catching the return is probably unnecessary. """ tf = 1e3 # DIII-D stores LP data in ms, not s # Make sure the ODS is for the right device/shot device = 'DIII-D' ensure_consistent_experiment_info(ods, device, shot) # Make sure the ODS already has probe positions in it hw_ready = True try: if not is_numeric(ods['langmuir_probes.embedded[0].position.r']): hw_ready = False if 'name' not in ods['langmuir_probes.embedded.0']: hw_ready = False except Exception: hw_ready = False if not hw_ready: setup_langmuir_probes_hardware_description_d3d(ods, shot) # Select probes to gather probes = probes or find_active_d3d_probes(shot, allowed_probes=allowed_probes) do_resample = (dt > 0) and (tend > tstart) if do_resample: printe('DIII-D often has 0s @ end of timebase for LP data in MDSplus, breaking server-side resampling.') printw('Recommended: abort and set tend=tstart or dt=0 to disable resampling.') available_quantities = dict( ion_saturation_current='ISAT', heat_flux_parallel='HEATFLUX', n_e='DENS', t_e='TEMP', surface_area_effective='AREA', v_floating='POT', # or is it 'v_plasma'? b_field_angle='ANGLE', ) if quantities is None: quantities = available_quantities else: quantities = {k: v for k, v in available_quantities.items() if k in quantities} factors = dict( ion_saturation_current=1, # A to A heat_flux_parallel=1e7, # kW cm^-2 to W m^-2 n_e=1e6, # cm^-3 to m^-3 t_e=1, # eV to eV surface_area_effective=1e-4, # cm^2 to m^2 v_floating=1, # V to V v_plasma=1, # V to V b_field_angle=np.pi / 180.0, # deg to rad ) has_err = ['t_e', 'n_e', 'v_floating'] i = 0 for i, probe in enumerate(probes): p_idx = probe - 1 tdi_base = rf'\TOP.PROBE_{probe:03d}' if not overwrite: try: _ = ods['langmuir_probes.embedded'][i]['time'] _ = ods['langmuir_probes.embedded'][i]['ion_saturation_current.data'] except Exception: pass else: printd(f'Skipping probe {probe} with index {p_idx} because it already has data', topic='omfit_omas_d3d') continue for j, ods_loc in enumerate(quantities): quantity = quantities[ods_loc] tdi = f'{tdi_base}.{quantity}' ascii_progress_bar( i * len(quantities) + j, 0, len(probes) * len(quantities), mess=f'Loading {device}#{shot} LP probe data ({probe}, {tdi})' ) f = factors.get(ods_loc, 1.0) if ods_loc in has_err: tdi_err = f'{tdi}_ERR' else: tdi_err = None if do_resample: tdi = f'resample({tdi}, {tstart*tf}, {tend*tf}, {dt*tf})' if tdi_err is not None: tdi_err = f'resample({tdi_err}, {tstart*tf}, {tend*tf}, {dt*tf})' m = OMFITmdsValue(server=device, shot=shot, treename='LANGMUIR', TDI=tdi) if tdi_err is not None: m_err = OMFITmdsValue(server=device, shot=shot, treename='LANGMUIR', TDI=tdi_err) else: m_err = None if m.check(): ods['langmuir_probes.embedded'][i]['time'] = m.dim_of(0) / tf if m_err is not None: ods['langmuir_probes.embedded'][i][ods_loc]['data'] = uarray(m.data() * f, abs(m_err.data()) * f) else: ods['langmuir_probes.embedded'][i][ods_loc]['data'] = m.data() * f # Just nominal values else: printd( f'Probe {i} (#{p_idx:03d}) does not appear to have good {quantity} ({ods_loc}) data. Skipping...', topic='omfit_omas_d3d', ) printd(f' TDI = {tdi}', topic='omfit_omas_d3d') ascii_progress_bar(i + 1, 0, len(probes), mess=f'Loaded {device}#{shot} LP probe data') if do_resample: printe('DIII-D often has 0s @ end of timebase for LP data in MDSplus, breaking server-side resampling.') printw('Recommended: check your data and repeat with dt=0 or tend=tstart if you find problems.') return ods
[docs]class OMFITd3dcompfile(SortedDict, OMFITascii): """ OMFIT class to read DIII-D compensation files such as btcomp.dat ccomp.dat and icomp.dat """ def __init__(self, filename, use_leading_comma=None, **kw): r""" OMFIT class to parse DIII-D MHD device files :param filename: filename :param \**kw: arguments passed to __init__ of OMFITascii """ OMFITascii.__init__(self, filename, **kw) SortedDict.__init__(self) self.dynaLoad = True
[docs] @dynaLoad def load(self): self.clear() with open(self.filename, 'r') as f: lines = f.readlines() for line in lines: linesplit = line.split() try: compshot = int(eval(linesplit[0])) self[compshot] = {} for compsig in linesplit[1:]: self[compshot][compsig.strip("'")] = {} except Exception: sig = linesplit[0][1:].strip() comps = [float(i) for i in linesplit[2:]] for comp, compsig in zip(comps, self[compshot]): self[compshot][compsig][sig] = comp return self