"""
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