# file processed by 2to3
from __future__ import print_function, absolute_import
from builtins import map, filter, range
try:
    # framework is running
    from .startup_choice import *
except (ValueError, SystemError):  # catch error in Python2.x
    # class is imported by itself
    from startup_choice import *
except ImportError as _excp:  # catch error in Python3.x
    # 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
from omfit_classes.omfit_ascii import OMFITascii
from omfit_classes.fluxSurface import fluxSurfaces, miller_derived
from omfit_classes.utils_math import deriv, calcz, integz, interp1e, atomic_element
from omfit_classes.utils_fusion import fast_heating, fusion_power
from omfit_classes import namelist
# Explicit imports
from omfit_classes.gapy import Gapy
import numpy as np
import omas
from scipy.interpolate import interp1d
from scipy import integrate, constants
__all__ = ['OMFITgacode', 'OMFITinputprofiles', 'OMFITinputgacode']
# OMAS source mapper used
# i_vol, d_dvol indicate if volume integrated or density source
# IMAS identifier.index: see core_sources.source[:].identifier at https://gafusion.github.io/omas/schema/schema_core%20sources.html
# NOTE: the first identifier.index is what is used when generating ODSs
# NOTE: here a identifier.index==-1 means `catch all` when reading from ODSs
# NOTE: pow_ie, q_ie has an extra factor of 2 in normalization to avoid double counting ion and electron source
omas_source_mapper = {
    # ============================================================
    # volume integrated source densities
    # ============================================================
    'pow_e_fus': ['i_vol', [6], [(+1, 'electrons.energy', 1e6)]],
    'pow_i_fus': ['i_vol', [6], [(+1, 'total_ion_energy', 1e6)]],
    'pow_e_brem': ['i_vol', [8], [(-1, 'electrons.energy', 1e6)]],
    'pow_e_sync': ['i_vol', [9], [(-1, 'electrons.energy', 1e6)]],
    'pow_e_line': ['i_vol', [10], [(-1, 'electrons.energy', 1e6)]],
    'pow_ei': ['i_vol', [11], [(-1, 'electrons.energy', 2e6), (+1, 'total_ion_energy', 2e6)]],
    'pow_e_aux': ['i_vol', [100, -1], [(+1, 'electrons.energy', 1e6)]],
    'pow_i_aux': ['i_vol', [100, -1], [(+1, 'total_ion_energy', 1e6)]],
    'flow_wall': ['i_vol', [108, -1], [(+1, 'electrons.particles', 0.624e22)]],
    'flow_beam': ['i_vol', [2], [(+1, 'electrons.particles', 0.624e22)]],
    'flow_mom': ['i_vol', [2, -1], [(+1, 'momentum_tor', 1.0)]],
    # ============================================================
    # source densities
    # ============================================================
    'qohme': ['d_dvol', [7], [(+1, 'electrons.energy', 1e6)]],
    'qbeame': ['d_dvol', [2], [(+1, 'electrons.energy', 1e6)]],
    'qbeami': ['d_dvol', [2], [(+1, 'total_ion_energy', 1e6)]],
    'qrfe': ['d_dvol', [3, -1], [(+1, 'electrons.energy', 1e6)]],
    'qrfi': ['d_dvol', [5, -1], [(+1, 'total_ion_energy', 1e6)]],
    'qfuse': ['d_dvol', [6], [(+1, 'electrons.energy', 1e6)]],
    'qfusi': ['d_dvol', [6], [(+1, 'total_ion_energy', 1e6)]],
    'qbrem': ['d_dvol', [8], [(-1, 'electrons.energy', 1e6)]],
    'qsync': ['d_dvol', [9], [(-1, 'electrons.energy', 1e6)]],
    'qline': ['d_dvol', [10], [(-1, 'electrons.energy', 1e6)]],
    'qei': ['d_dvol', [11], [(-1, 'electrons.energy', 2e6), (+1, 'total_ion_energy', 2e6)]],
    'qione': ['d_dvol', [602], [(+1, 'electrons.energy', 1e6)]],
    'qioni': ['d_dvol', [602], [(+1, 'total_ion_energy', 1e6)]],
    'qcxi': ['d_dvol', [305], [(+1, 'total_ion_energy', 1e6)]],
    'qpar_wall': ['d_dvol', [108, -1], [(+1, 'electrons.particles', 0.624e22)]],
    'qpar_beam': ['d_dvol', [2], [(+1, 'electrons.particles', 0.624e22)]],
    'qmom': ['d_dvol', [2, -1], [(+1, 'momentum_tor', 1.0)]],
}
sources_internal_list = [6, 8, 9, 10, 11]
map_d_i = {
    'electrons.energy': 'electrons.power_inside',
    'total_ion_energy': 'total_ion_power_inside',
    'electrons.particles': 'electrons.particles_inside',
    'momentum_tor': 'torque_tor_inside',
}
[docs]class OMFITgacode(SortedDict, OMFITascii):
    r"""
    OMFIT class used to interface with GAcode input.XXX files
    This class supports .gen, .extra, .geo, .profiles file
    .plot() method is available for .profiles files
    :param GACODEtype: force 'profiles' parsing input.profiles format or use `None` for autodetection based on file name
    :param filename: filename passed to OMFITascii class
    :param \**kw: keyword dictionary passed to OMFITascii class
    """
    def __init__(self, filename=None, GACODEtype=None, **kw):
        kw['GACODEtype'] = GACODEtype
        SortedDict.__init__(self)
        OMFITascii.__init__(self, filename, **kw)
        # parse .gen files
        if re.match(r'input\.profiles\.gen.*$', os.path.split(self.filename)[1]) or re.match(r'.*\.gen$', os.path.split(self.filename)[1]):
            pass
        # parse input.profiles.extra files
        elif (
            self.GACODEtype == 'extra'
            or re.match(r'input\.profiles\.extra.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.extra$', os.path.split(self.filename)[1])
        ):
            self.GACODEtype = 'extra'
            self.__class__ = OMFITinputprofiles
            OMFITinputprofiles.init_profiles_names(self)
        # parse input.profiles.geo files
        elif (
            self.GACODEtype == 'geo'
            or re.match(r'input\.profiles\.geo.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.geo', os.path.split(self.filename)[1])
        ):
            self.GACODEtype = 'geo'
            self.__class__ = OMFITinputprofiles
            OMFITinputprofiles.init_profiles_names(self)
        # parse input.profiles files
        elif (
            self.GACODEtype == 'profiles'
            or re.match(r'input\.profiles.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.profiles$', os.path.split(self.filename)[1])
        ):
            self.GACODEtype = 'profiles'
            self.__class__ = OMFITinputprofiles
            OMFITinputprofiles.init_profiles_names(self)
        # parse input.gacode files
        elif (
            self.GACODEtype == 'gacode'
            or re.match(r'input\.gacode.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.gacode$', os.path.split(self.filename)[1])
        ):
            self.__class__ = OMFITinputgacode
            self.GACODEtype = 'gacode'
        self.dynaLoad = True
    @property
    def GACODEtype(self):
        return self.OMFITproperties.get('GACODEtype', None)
    @GACODEtype.setter
    def GACODEtype(self, value):
        self.OMFITproperties['GACODEtype'] = value
[docs]    @dynaLoad
    def load(self):
        """
        Method used to load the content of the file specified in the .filename attribute
        :return: None
        """
        self.clear()
        if self.filename is None or not os.stat(self.filename).st_size:
            return
        with open(self.filename, 'r') as f:
            lines = f.read()
        if len(lines):
            lines = lines.split('\n')
            lines = [line.strip() for line in lines]
            for h, line in enumerate(lines):
                if not len(line):
                    continue
                if line[0] != '#':
                    break
                self['__header_' + format(h, '04d') + '__'] = line
        # parse input.profiles.gen files (allowing for DIR directives of tgyro)
        if re.match(r'input\.profiles\.gen.*$', os.path.split(self.filename)[1]) or re.match(r'.*\.gen$', os.path.split(self.filename)[1]):
            ndir = 0
            for line in lines:
                if not len(line):
                    continue
                if ' ' not in line:
                    ndir = int(line)
                    self['DIR'] = SortedDict()
                    continue
                else:
                    item = line.split()[0]
                    key = line.split()[-1]
                if ndir > 0:
                    if len(line.strip().split()) == 2:
                        self['DIR'][item] = int(key)
                    else:
                        self['DIR'][item] = [int(line.split()[1]), line.split()[2]] + line.split()[3:]
                        if 'X=' not in self['DIR'][item][1]:
                            # uniform distribution of points
                            self['DIR'][item][1] = float(self['DIR'][item][1])
                            if self['DIR'][item][1] == -1:
                                self['DIR'][item][1] = -1
                else:
                    self[key] = namelist.interpreter(item)
        # parse input.profiles.extra files
        elif (
            self.GACODEtype == 'extra'
            or re.match(r'input\.profiles\.extra.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.extra$', os.path.split(self.filename)[1])
        ):
            self.GACODEtype = 'extra'
            self.__class__ = OMFITinputprofiles
            OMFITinputprofiles.init_profiles_names(self)
            OMFITinputprofiles.load(self)
        # parse input.profiles.geo files
        elif (
            self.GACODEtype == 'geo'
            or re.match(r'input\.profiles\.geo.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.geo', os.path.split(self.filename)[1])
        ):
            self.GACODEtype = 'geo'
            self.__class__ = OMFITinputprofiles
            OMFITinputprofiles.init_profiles_names(self)
            OMFITinputprofiles.load(self)
        # parse input.profiles files
        elif (
            self.GACODEtype == 'profiles'
            or re.match(r'input\.profiles.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.profiles$', os.path.split(self.filename)[1])
        ):
            self.GACODEtype = 'profiles'
            self.__class__ = OMFITinputprofiles
            OMFITinputprofiles.init_profiles_names(self)
            OMFITinputprofiles.load(self)
        # parse XXX files
        else:
            # allow parsing of DIR directives as used in TGYRO
            with open(self.filename, 'r') as f:
                DIR = SortedDict()
                while True:
                    line = f.readline()
                    if re.match('DIR .* [0-9]*', line):
                        dp = line.strip().split()
                        if len(dp) == 3:
                            DIR[dp[1]] = int(dp[2])
                        else:
                            DIR[dp[1]] = [int(dp[2])] + dp[3:]
                            DIR[dp[1]] = float(DIR[dp[1]][2])
                    else:
                        break
            tmp = lines
            if len(DIR):
                tmp = lines[len(DIR) :]
                self['DIR'] = DIR
            # normal parsing
            for h, line in enumerate(tmp):
                if not len(line) or '#' in line[0]:
                    self['__header_' + format(h, '04d') + '__'] = line
                else:
                    self[line.split('=')[0].strip()] = namelist.interpreter(line.split('=')[1]) 
[docs]    @dynaSave
    def save(self):
        """
        Method used to save the content of the object to the file specified in the .filename attribute
        :return: None
        """
        if (
            self.GACODEtype == 'extra'
            or re.match(r'input\.profiles\.extra.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.extra$', os.path.split(self.filename)[1])
        ):
            self.GACODEtype = 'extra'
            self.__class__ = OMFITinputprofiles
            OMFITinputprofiles.init_profiles_names(self)
            return OMFITinputprofiles.save(self)
        # parse input.profiles.geo files
        elif (
            self.GACODEtype == 'geo'
            or re.match(r'input\.profiles\.geo.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.geo', os.path.split(self.filename)[1])
        ):
            self.GACODEtype = 'geo'
            self.__class__ = OMFITinputprofiles
            OMFITinputprofiles.init_profiles_names(self)
            return OMFITinputprofiles.save(self)
        # parse input.profiles files
        elif (
            self.GACODEtype == 'profiles'
            or re.match(r'input\.profiles.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.profiles$', os.path.split(self.filename)[1])
        ):
            self.GACODEtype = 'profiles'
            self.__class__ = OMFITinputprofiles
            OMFITinputprofiles.init_profiles_names(self)
            return OMFITinputprofiles.save(self)
        # parse input.profiles files
        elif (
            self.GACODEtype == 'gacode'
            or re.match(r'input\.gacode.*$', os.path.split(self.filename)[1])
            or re.match(r'.*\.gacode$', os.path.split(self.filename)[1])
        ):
            self.GACODEtype = 'gacode'
            self.__class__ = OMFITinputgacode
            return OMFITinputgacode.save(self)
        # save XXX files
        else:
            header_ptrn = re.compile(r'^__header_.*__$')
            header = []
            for item in list(self.keys()):
                if re.match(header_ptrn, item):
                    header.append(self[item])
            if len(header):
                header.append('')
            with open(self.filename, 'w') as f:
                # save XXX.gen files (allowing for DIR directives of tgyro)
                if re.match(r'input\.profiles\.gen.*$', os.path.split(self.filename)[1]) or re.match(
                    r'.*\.gen$', os.path.split(self.filename)[1]
                ):
                    for key in [k for k in list(self.keys()) if k != 'DIR']:
                        f.write(namelist.encoder(self[key]) + '  ' + str(key) + '\n')
                    # write DIR directives at the end of the file as necessary for tgyro
                    if 'DIR' in self:
                        f.write(str(len(self['DIR'])) + '\n')
                        for key in list(self['DIR'].keys()):
                            f.write(str(key) + '  ' + ' '.join(map(str, tolist(self['DIR'][key]))) + '\n')
                # save XXX files
                else:
                    # write DIR directives at the top of the file as necessary for tgyro
                    if 'DIR' in list(self.keys()):
                        for dir in self['DIR']:
                            if len(tolist(self['DIR'][dir])) > 1 and self['DIR'][dir][1] != -1:
                                try:
                                    self['DIR'][dir][1] = 'X=%f' % float(self['DIR'][dir][1])
                                except ValueError:
                                    pass
                            f.write('DIR ' + dir + ' ' + ' '.join(map(str, tolist(self['DIR'][dir])[:2])) + '\n')
                    # write header
                    for ii, item in enumerate(self.keys()):
                        if not isinstance(self[item], np.ndarray) and not isinstance(self[item], dict):
                            if not re.match(hide_ptrn, item):
                                f.write(item + '=' + namelist.encoder(self[item]) + '\n')
                            else:
                                if self[item] or not ii == len(self) - 1:
                                    f.write(self[item] + '\n') 
    @dynaLoad
    def __getattr__(self, attr):
        # backward compatibility with older nomenclature of methods
        # that used `input_profile_` prefix for methods that were
        # meant to work only for `input.profiles` files.
        attr0 = attr
        if 'input_profiles_' in attr:
            attr = re.sub('input_profiles_', '', attr)
            if hasattr(self, attr):
                printw(self.__class__.__name__ + '.%s() is deprecated. Use .%s() instead' % (attr0, attr))
                return getattr(self, attr)
        raise AttributeError('bad attribute `%s`' % attr0) 
class _gacode_profiles(object):
    """
    GACODE profiles maniputlation methods
    to be shared between OMFITinputprofiles and OMFITinputgacode classes
    """
    def init_profiles_names(self):
        self.profNames = {}
        self.extraNames = {}
        self.profNames[0] = [
            ['rho', 'rmin', 'rmaj', 'q', 'kappa'],
            ['delta', 'Te', 'ne', 'z_eff', 'omega0'],
            ['flow_mom', 'pow_e', 'pow_i', 'pow_ei', 'zeta'],
            ['flow_beam', 'flow_wall', 'zmag', 'ptot', 'polflux'],
            ['ni_1', 'ni_2', 'ni_3', 'ni_4', 'ni_5'],
            ['Ti_1', 'Ti_2', 'Ti_3', 'Ti_4', 'Ti_5'],
            ['vtor_1', 'vtor_2', 'vtor_3', 'vtor_4', 'vtor_5'],
            ['vpol_1', 'vpol_2', 'vpol_3', 'vpol_4', 'vpol_5'],
            ['pow_e_aux', 'pow_i_aux', 'pow_e_fus', 'pow_i_fus', 'pow_e_sync'],
            ['pow_e_brem', 'pow_e_line', 'NULL', 'NULL', 'NULL'],
        ]
        self.extraNames[0] = [
            'bunit',
            's',
            'drmaj',
            'dzmag',
            'sdelta',
            'skappa',
            'szeta',
            'dlnnedr',
            'dlntedr',
            'dlnnidr_1',
            'dlnnidr_2',
            'dlnnidr_3',
            'dlnnidr_4',
            'dlnnidr_5',
            'dlntidr_1',
            'dlntidr_2',
            'dlntidr_3',
            'dlntidr_4',
            'dlntidr_5',
            'dlnptotdr',
            'drdrho',
            'w0p',
            'vol',
            'volp',
            'cs',
            'rhos',
            'ni_new',
            'dlnnidr_new',
            'grad_r0',
            'ave_grad_r',
            'bp0',
            'bt0',
            'gamma_e',
            'gamma_p',
            'mach',
        ]
        self.profNames[1] = [
            ['rho', 'rmin', 'polflux', 'q', 'omega0'],
            ['rmaj', 'zmag', 'kappa', 'delta', 'zeta'],
            ['ne', 'Te', 'ptot', 'z_eff', 'NULL'],
            ['ni_1', 'ni_2', 'ni_3', 'ni_4', 'ni_5'],
            ['ni_6', 'ni_7', 'ni_8', 'ni_9', 'ni_10'],
            ['Ti_1', 'Ti_2', 'Ti_3', 'Ti_4', 'Ti_5'],
            ['Ti_6', 'Ti_7', 'Ti_8', 'Ti_9', 'Ti_10'],
            ['vtor_1', 'vtor_2', 'vtor_3', 'vtor_4', 'vtor_5'],
            ['vtor_6', 'vtor_7', 'vtor_8', 'vtor_9', 'vtor_10'],
            ['vpol_1', 'vpol_2', 'vpol_3', 'vpol_4', 'vpol_5'],
            ['vpol_6', 'vpol_7', 'vpol_8', 'vpol_9', 'vpol_10'],
            ['flow_beam', 'flow_wall', 'flow_mom', 'NULL', 'NULL'],
            ['pow_e', 'pow_i', 'pow_ei', 'pow_e_aux', 'pow_i_aux'],
            ['pow_e_fus', 'pow_i_fus', 'pow_e_sync', 'pow_e_brem', 'pow_e_line'],
        ]
        self.extraNames[1] = [
            'bunit',
            's',
            'drmaj',
            'dzmag',
            'sdelta',
            'skappa',
            'szeta',
            'dlnnedr',
            'dlntedr',
            'dlnnidr_1',
            'dlnnidr_2',
            'dlnnidr_3',
            'dlnnidr_4',
            'dlnnidr_5',
            'dlnnidr_6',
            'dlnnidr_7',
            'dlnnidr_8',
            'dlnnidr_9',
            'dlnnidr_10',
            'dlntidr_1',
            'dlntidr_2',
            'dlntidr_3',
            'dlntidr_4',
            'dlntidr_5',
            'dlntidr_6',
            'dlntidr_7',
            'dlntidr_8',
            'dlntidr_9',
            'dlntidr_10',
            'dlnptotdr',
            'drdrho',
            'w0p',
            'vol',
            'volp',
            'cs',
            'rhos',
            'ni_new',
            'dlnnidr_new',
            'grad_r0',
            'ave_grad_r',
            'bp0',
            'bt0',
            'gamma_e',
            'gamma_p',
            'mach',
        ]
        self.profNames[2] = [
            ['rho', 'rmin', 'polflux', 'q', 'omega0'],
            ['rmaj', 'zmag', 'kappa', 'delta', 'zeta'],
            ['ne', 'Te', 'ptot', 'z_eff', 'NULL'],
            ['ni_1', 'ni_2', 'ni_3', 'ni_4', 'ni_5'],
            ['ni_6', 'ni_7', 'ni_8', 'ni_9', 'ni_10'],
            ['Ti_1', 'Ti_2', 'Ti_3', 'Ti_4', 'Ti_5'],
            ['Ti_6', 'Ti_7', 'Ti_8', 'Ti_9', 'Ti_10'],
            ['vtor_1', 'vtor_2', 'vtor_3', 'vtor_4', 'vtor_5'],
            ['vtor_6', 'vtor_7', 'vtor_8', 'vtor_9', 'vtor_10'],
            ['vpol_1', 'vpol_2', 'vpol_3', 'vpol_4', 'vpol_5'],
            ['vpol_6', 'vpol_7', 'vpol_8', 'vpol_9', 'vpol_10'],
            ['flow_beam', 'flow_wall', 'flow_mom', 'sbcx', 'sbeame'],
            ['pow_e', 'pow_i', 'pow_ei', 'pow_e_aux', 'pow_i_aux'],
            ['pow_e_fus', 'pow_i_fus', 'pow_e_sync', 'pow_e_brem', 'pow_e_line'],
        ]
        self.extraNames[2] = self.extraNames[1]
        self.profNames[3] = [
            ['rho', 'rmin', 'polflux', 'q', 'omega0'],
            ['rmaj', 'zmag', 'kappa', 'delta', 'zeta'],
            ['ne', 'Te', 'ptot', 'z_eff', 'NULL'],
            ['ni_1', 'ni_2', 'ni_3', 'ni_4', 'ni_5'],
            ['ni_6', 'ni_7', 'ni_8', 'ni_9', 'ni_10'],
            ['Ti_1', 'Ti_2', 'Ti_3', 'Ti_4', 'Ti_5'],
            ['Ti_6', 'Ti_7', 'Ti_8', 'Ti_9', 'Ti_10'],
            ['vtor_1', 'vtor_2', 'vtor_3', 'vtor_4', 'vtor_5'],
            ['vtor_6', 'vtor_7', 'vtor_8', 'vtor_9', 'vtor_10'],
            ['vpol_1', 'vpol_2', 'vpol_3', 'vpol_4', 'vpol_5'],
            ['vpol_6', 'vpol_7', 'vpol_8', 'vpol_9', 'vpol_10'],
            ['flow_beam', 'flow_wall', 'flow_mom', 'NULL', 'NULL'],
            ['pow_e', 'pow_i', 'pow_ei', 'pow_e_aux', 'pow_i_aux'],
            ['pow_e_fus', 'pow_i_fus', 'pow_e_sync', 'pow_e_brem', 'pow_e_line'],
            ['sbeame', 'sbcx', 'sscxl', 'NULL', 'NULL'],
        ]
        self.extraNames[3] = [
            'bunit',
            's',
            'drmaj',
            'dzmag',
            'sdelta',
            'skappa',
            'szeta',
            'dlnnedr',
            'dlntedr',
            'dlnnidr_1',
            'dlnnidr_2',
            'dlnnidr_3',
            'dlnnidr_4',
            'dlnnidr_5',
            'dlnnidr_6',
            'dlnnidr_7',
            'dlnnidr_8',
            'dlnnidr_9',
            'dlnnidr_10',
            'dlntidr_1',
            'dlntidr_2',
            'dlntidr_3',
            'dlntidr_4',
            'dlntidr_5',
            'dlntidr_6',
            'dlntidr_7',
            'dlntidr_8',
            'dlntidr_9',
            'dlntidr_10',
            'dlnptotdr',
            'drdrho',
            'w0p',
            'vol',
            'volp',
            'cs',
            'rhos',
            'ni_new',
            'dlnnidr_new',
            'grad_r0',
            'ave_grad_r',
            'bp0',
            'bt0',
            'gamma_e',
            'gamma_p',
            'mach',
            'ip',
            'sdlnnedr',
            'sdlntedr',
            'sdlnnidr_1',
            'sdlnnidr_2',
            'sdlnnidr_3',
            'sdlnnidr_4',
            'sdlnnidr_5',
            'sdlnnidr_6',
            'sdlnnidr_7',
            'sdlnnidr_8',
            'sdlnnidr_9',
            'sdlnnidr_10',
            'sdlntidr_1',
            'sdlntidr_2',
            'sdlntidr_3',
            'sdlntidr_4',
            'sdlntidr_5',
            'sdlntidr_6',
            'sdlntidr_7',
            'sdlntidr_8',
            'sdlntidr_9',
            'sdlntidr_10',
            'nuee',
        ]
        self.latest_version = self.version = 3
        # fmt: off
        self.profNamesPretty = \
             {'NULL'       : (''                          ,''             ,'[null]'),
              'rho'        : ('{\\hat \\rho}'             ,''             ,'rho(-)'),
              'rmin'       : ('a'                         ,'m'            ,'rmin(m)'),
              'rmaj'       : ('R_0'                       ,'m'            ,'rmaj(m)'),
              'q'          : ('q'                         ,''             ,'q(-)'),
              'kappa'      : ('\\kappa'                   ,''             ,'kappa(-)'),
              'delta'      : ('\\delta'                   ,''             ,'delta(-)'),
              'Te'         : ('T_e'                       ,'keV'          ,'Te(keV)'),
              'ne'         : ('n_e'                       ,'10^{19}/m^3'  ,'ne(10^19/m^3)'),
              'z_eff'      : ('Z_\\mathrm{eff}'           ,''             ,'zeff(-)'),
              'omega0'     : ('\\omega_0'                 ,'rad/s'        ,'omega0(rad/s)'),
              'flow_mom'   : ('S_\\mathrm{\\omega}'       ,'Nm'           ,'flow_mom(Nm)'),
              'sbcx'       : ('sbcx'                      ,'1/m^3/s'      ,'sbcx(/m^3/s)'),
              'sbeame'     : ('sbeame'                    ,'1/m^3/s'      ,'sbeame(/m^3/s)'),
              'sscxl'      : ('sscxl'                     ,'1/m^3/s'      ,'sscxl(/m^3/s)'),
              'pow_e'      : ('P_e'                       ,'MW'           ,'pow_e(MW)'),
              'pow_i'      : ('P_i'                       ,'MW'           ,'pow_i(MW)'),
              'pow_ei'     : ('P_{ei}'                    ,'MW'           ,'pow_ei(MW)'),
              'zeta'       : ('\\zeta'                    ,''             ,'zeta(-)'),
              'flow_beam'  : ('S_\\mathrm{n,beam}'        ,'kW/eV'        ,'flow_beam(kW/eV)'),
              'flow_wall'  : ('S_\\mathrm{n,wall}'        ,'kW/eV'        ,'flow_wall(kW/eV)'),
              'zmag'       : ('Z_0'                       ,'m'            ,'zmag(m)'),
              'ptot'       : ('p_\\mathrm{total}'         ,'Pa'           ,'ptot(Pa)'),
              'polflux'    : ('\\psi'                     ,'Wb/rad'       ,'polflux(Wb/rad)'),
              'pow_e_aux'  : ('P_{e,\\rm aux}'            ,'MW'           ,'pow_e_aux(MW)'),
              'pow_i_aux'  : ('P_{i,\\rm aux}'            ,'MW'           ,'pow_i_aux(MW)'),
              'pow_e_fus'  : ('P_{e,\\rm fus}'            ,'MW'           ,'pow_e_fus(MW)'),
              'pow_i_fus'  : ('P_{i,\\rm fus}'            ,'MW'           ,'pow_i_fus(MW)'),
              'pow_e_sync' : ('P_{e,\\rm sync}'           ,'MW'           ,'pow_e_sync(MW)'),
              'pow_e_brem' : ('P_{e,\\rm brem}'           ,'MW'           ,'pow_e_brem(MW)'),
              'pow_e_line' : ('P_{e,\\rm line}'           ,'MW'           ,'pow_e_line(MW)'),
              # extra (needs units)
              'bunit'      : ('B_\\mathrm{unit}'          ,'T'            ,''),
              's'          : ('s'                         ,''             ,''),
              'drmaj'      : ('dR_0/dr'                   ,''             ,''),
              'dzmag'      : ('dZ_0/dr'                   ,''             ,''),
              'sdelta'     : ('s_\\delta'                 ,''             ,''),
              'skappa'     : ('s_\\kappa'                 ,''             ,''),
              'szeta'      : ('s_\\zeta'                  ,''             ,''),
              'dlnnedr'    : ('-dln(n_e)/dr'              ,'1/m'          ,''),
              'dlntedr'    : ('-dln(T_e)/dr'              ,'1/m'          ,''),
              'dlnptotdr'  : ('-dln(p_\\mathrm{tot})/dr'  ,'1/m'          ,''),
              'drdrho'     : ('dr/d\\rho'                 ,''             ,''),
              'w0p'        : ('d(\\omega_0)/dr'           ,'rad/s/m'        ,''),
              'vol'        : ('V'                         ,'m^3'          ,''),
              'volp'       : ('dV/dr'                     ,'m^2'          ,''),
              'cs'         : ('c_\\mathrm{s}'             ,'m/s'          ,''),
              'rhos'       : ('\\rho_\\mathrm{s,unit}'     ,'m'            ,''),
              'ni_new'     : ('n_i'                       ,'10^{19}/m^3'  ,''),  # [Corrected for quasin.]
              'dlnnidr_new': ('-dln(n_i)/dr'              ,'1/m'          ,''),  # [Corrected for quasin.]
              'grad_r0'    : ('|\\nabla_r|_{\\theta=0}'   ,''             ,''),
              'ave_grad_r' : ('<|\\nabla_r|>'             ,''             ,''),
              'bp0'        : ('B_p|_{\\theta=0}'          ,'T'            ,''),
              'bt0'        : ('B_t|_{\\theta=0}'          ,'T'            ,''),
              'gamma_e'    : ('r/q d(\\omega_0)/dr'       ,'rad/s'          ,''),
              'gamma_p'    : ('R_0 d(\\omega_0)/dr'       ,'rad/s'          ,''),
              'mach'       : ('R_0 \\omega_0/c_s'         ,''             ,''),
              #jbs
              'expro_rho'  : ('\\rho'                     ,''             ,''),
              'jbs_err'    : ('j_\\mathrm{bs,err}'        ,'MA/m^2'       ,''),
              'jbs_neo'    : ('j_\\mathrm{bs,neo}'        ,'MA/m^2'       ,''),
              'jbs_sauter' : ('j_\\mathrm{bs,sauter}'     ,'MA/m^2'       ,''),
              'jbs_nclass' : ('j_\\mathrm{bs,nclass}'     ,'MA/m^2'       ,''),
              'jbs_koh'    : ('j_\\mathrm{bs,koh}'        ,'MA/m^2'       ,''),
              #expro
              'fpol'       : ('F'                         ,'T*m'          ,''),
              'jbs'        : ('j_\\mathrm{bs}'            ,'MA/m^2'       ,''),
              'jbstor'     : ('j_\\mathrm{bs,tor}'        ,'MA/m^2'       ,''),
              'jnb'        : ('j_\\mathrm{nb}'            ,'MA/m^2'       ,''),
              'johm'       : ('j_\\mathrm{oh}'            ,'MA/m^2'       ,''),
              'jrf'        : ('j_\\mathrm{rf}'            ,'MA/m^2'       ,''),
              'nuee'       : ('\\nu_{ee}'                 ,'1/s'          ,''),
              'qbeame'     : ('Q_{e,nb}'                  ,'W/m^3'        ,''),
              'qbeami'     : ('Q_{i,nb}'                  ,'W/m^3'        ,''),
              'qbrem'      : ('Q_{brem}'                  ,'W/m^3'        ,''),
              'qcxi'       : ('Q_{cxi}'                   ,'W/m^3'        ,''),
              'qei'        : ('Q_{ei}'                    ,'W/m^3'        ,''),
              'qfuse'      : ('Q_{e,fus}'                 ,'W/m^3'        ,''),
              'qfusi'      : ('Q_{i,fus}'                 ,'W/m^3'        ,''),
              'qione'      : ('Q_{ione}'                  ,'W/m^3'        ,''),
              'qioni'      : ('Q_{ioni}'                  ,'W/m^3'        ,''),
              'qline'      : ('Q_{line}'                  ,'W/m^3'        ,''),
              'qmom'       : ('Q_{mom}'                   ,'W/m^3'        ,''),
              'qohme'      : ('Q_{e,oh}'                  ,'W/m^3'        ,''),
              'qpar_beam'  : ('Q_{beam,par}'              ,'W/m^3'        ,''),
              'qpar_wall'  : ('Q_{wall,par}'              ,'W/m^3'        ,''),
              'qrfe'       : ('Q_{e,rf}'                  ,'W/m^3'        ,''),
              'qrfi'       : ('Q_{i,rf}'                  ,'W/m^3'        ,''),
              'qsync'      : ('Q_{sync}'                  ,'W/m^3'        ,''),
              'sigmapar'   : ('\\sigma_{\\parallel}'      ,'S'            ,''),
              'surf'       : ('A'                         ,'m^2'          ,''),
        }
        for _k in range(0, 11):
            self.profNamesPretty['ni_%d'%_k]              = ('n_{i,%d}'%_k          ,'10^{19}/m^3' ,'ni_%d(10^19/m^3)'%_k)
            self.profNamesPretty['Ti_%d'%_k]              = ('T_{i,%d}'%_k          ,'keV'         ,'Ti_%d(keV)'%_k)
            self.profNamesPretty['vtor_%d'%_k]            = ('v_{\\varphi,%d}'%_k   ,'m/s'         ,'vtor_%d(m/s)'%_k)
            self.profNamesPretty['vpol_%d'%_k]            = ('v_{\\theta,%d}'%_k    ,'m/s'         ,'vpol_%d(m/s)'%_k)
            self.profNamesPretty['dlntidr_%d'%_k]         = ('-dln(T_{i,%d})/dr'%_k ,'1/m'         ,'')
            self.profNamesPretty['dlnnidr_%d'%_k]         = ('-dln(n_{i,%d})/dr'%_k ,'1/m'         ,'')
            for g in ['cos','scos','sin','ssin']:
                self.profNamesPretty['shape_%s%d'%(g,_k)] = ('%s%d'%(g,_k)           ,''           ,'')
        # fmt: on
    def plot(self, what=None, only2D=False, axs={}, pretty_names=True, **kw):
        r"""
        Plot all profiles entries as function of rho
        :param what: list of quantities to plot. All quantities are plotted if set to None.
        :param only2D: plot 2D flux surfaces based on miller geometry coefficients
        :param axs: dictionary with axes for each of the quantities to be plotted
        :param pretty_names: use pretty names for subplots
        :param \**kw: extra arguments passed to plot functions
        :return: dictionary with all the plot axes that have been used
        """
        inputaxs = axs
        axs = {}
        if self.GACODEtype == 'geo':
            return self.plot_geo(**kw)
        import matplotlib
        from matplotlib import pyplot
        if only2D:
            ax = kw.pop('ax', None)
            if ax is None:
                ax = pyplot.gca()
            r, z = self.rz_geometry()
            ax.plot(r[:, 0], z[:, 0], **kw)
            kw['color'] = ax.lines[-1].get_color()
            ax.plot(r[:, 1:], z[:, 1:], **kw)
            ax.set_aspect('equal')
            return ax
        if what is None:
            what = sorted(
                [
                    item
                    for item in list(self.keys())
                    if isinstance(self[item], np.ndarray) and item != 'rho' and not np.all(self[item] == 0.0)
                ],
                key=lambda x: x.lower(),
            )
        else:
            what = tolist(what, None)
        nplot = len(what)
        cplot = max([np.floor(np.sqrt(nplot)), 1.0])
        rplot = np.ceil(nplot / cplot)
        pyplot.gcf().subplots_adjust(wspace=0.35, hspace=0.0)
        interactive = matplotlib.is_interactive()
        try:
            if interactive:
                pyplot.ioff()
            for k, item in enumerate(what):
                if len(inputaxs):
                    if item in inputaxs:
                        ax = inputaxs[item]
                    else:
                        continue
                else:
                    # cplot is used in other calculations so use another variable to cast to int.
                    # one for rplot is done for consistency
                    plot_cols = int(cplot)
                    plot_rows = int(rplot)
                    if k == 0:
                        ax1 = ax = pyplot.subplot(plot_rows, plot_cols, k + 1)
                    else:
                        ax = pyplot.subplot(plot_rows, plot_cols, k + 1, sharex=ax1)
                axs[item] = ax
                r = np.floor(k * 1.0 / cplot)
                c = k - r * cplot
                ax.ticklabel_format(style='sci', scilimits=(-3, 3))
                if 'rho' in self:
                    x = self['rho']
                    ax.set_xlabel('$\\rho$')
                elif 'drdrho' in self:
                    x = integrate.cumtrapz(1.0 / self['drdrho'], initial=0)
                    x = x / max(x)
                    ax.set_xlabel('$\\rho$')
                else:
                    x = np.arange(len(self[item]))
                    ax.set_xlabel('Array element')
                ax.plot(x, self[item], **kw)
                if 0 == self[item].min() == self[item].max():
                    ax.set_yticks([0])
                text = item
                if pretty_names:
                    if not hasattr(self, 'profNamesPretty'):
                        self.init_profiles_names()
                    text = '$' + self.profNamesPretty[item][0] + '~[' + self.profNamesPretty[item][1] + ']$'
                ax.text(0.5, 0.9, text, horizontalalignment='center', verticalalignment='top', size='medium', transform=ax.transAxes)
                if k >= len(what) - cplot:
                    pyplot.setp(ax.get_xticklabels(), visible=True)
            pyplot.xlim(min(x), max(x))
        finally:
            if interactive:
                pyplot.ion()
                pyplot.draw()
        return axs
    def plot_geo(self, **kw):
        """
        Plot equilibrum based on fourier representation
        """
        import matplotlib
        from matplotlib import pyplot
        kw.pop('only2D', None)
        ax = kw.pop('ax', None)
        if ax is None:
            ax = pyplot.gca()
        r, z = self.rz_geometry()
        ax.plot(r[:, 0], z[:, 0], **kw)
        kw['color'] = ax.lines[-1].get_color()
        ax.plot(r[:, 1:], z[:, 1:], **kw)
        ax.set_aspect('equal')
        ax.set_xlabel('R')
        ax.set_ylabel('Z')
        return ax
    def calc_ptot(self, in_place=False):
        """
        Calculates ptot from species densities and temperatures
        :param in_place: update z_eff array
        :return: calculated ptot array
        """
        ptot = []
        for item in ['e'] + ['i_%d' % i for i in range(1, 11)]:
            if 'n' + item in self and 'T' + item in self:
                ptot.append(self['n' + item] * self['T' + item] * 1e3 * constants.e * 1e19)
        ptot = np.sum(ptot, 0)
        if in_place:
            self['ptot'] = ptot
        return ptot
    def calc_zeff(self, use_ne=True, in_place=False):
        """
        Calculates z_eff from species densities and charges
        Two ways of calculating z_eff are identical when plasma is quasineutral but different otherwise
            * z_eff=np.sum(ni*Zi**2)/ne
            * z_eff=np.sum(ni*Zi**2)/np.sum(ni*Zi)
        :param use_ne: use z_eff=np.sum(ni*Zi**2)/ne
        :param in_place: update z_eff array
        :return: z_eff array
        """
        num = []
        den = []
        for k in self['IONS']:
            num.append(self['ni_%d' % k] * self['IONS'][k][1] ** 2)
            den.append(self['ni_%d' % k] * self['IONS'][k][1])
        if use_ne:
            z_eff = np.sum(num, 0) / self['ne']
        else:
            z_eff = np.sum(num, 0) / np.sum(den, 0)
        if in_place:
            self['z_eff'] = z_eff
        return z_eff
    def calc_pow_aux(self, in_place=False):
        """
        Calculates electron and ion auxiliary sources as the difference between total sources and the other known individual components
        :param in_place: Update pow_e_aux and pow_i_aux arrays
        :return: pow_e_aux, pow_i_aux arrays
        """
        pow_e_aux = self['pow_e'] - (-self['pow_ei'] + self['pow_e_fus'] - self['pow_e_sync'] - self['pow_e_brem'] - self['pow_e_line'])
        pow_i_aux = self['pow_i'] - (self['pow_ei'] + self['pow_i_fus'])
        if in_place:
            self['pow_e_aux'] = pow_e_aux
            self['pow_i_aux'] = pow_i_aux
        return pow_e_aux, pow_i_aux
    def calc_pow(self, in_place=False):
        """
        Calculates electron and ion sources as the difference between total sources and the other known individual components
        :param in_place: Update pow_e_aux and pow_i_aux arrays
        :return: pow_e_aux, pow_i_aux arrays
        """
        pow_e = self['pow_e_aux'] - self['pow_ei'] + self['pow_e_fus'] - self['pow_e_sync'] - self['pow_e_brem'] - self['pow_e_line']
        pow_i = self['pow_i_aux'] + self['pow_ei'] + self['pow_i_fus']
        if in_place:
            self['pow_e'] = pow_e
            self['pow_i'] = pow_i
        return pow_e, pow_i
    def checks(self):
        """
        runs a series of consistency checks on gacode profiles file
        """
        problems = []
        for item in ['pow_e_line', 'pow_e_sync', 'pow_e_brem']:
            if (self[item] < 0).any():
                problems.append(item + ' is negative (should always be positive)')
        for item in self:
            if isinstance(self[item], np.ndarray) and np.any(np.isnan(self[item])):
                problems.append(item + ' has NaNs')
        if np.any(np.abs(self['zeta']) > np.sqrt(2) / 2.0):
            problems.append('abs(zeta) is not between 0 and 0.7')
        if len(problems):
            printw('Possible problems found with gacode profiles file:')
            printw('\n'.join(map(lambda x: ' * ' + x, problems)))
        self['N_ION'] = len(self['IONS'])
    def ion_names(self, fast_last=False):
        """
        returns the ion names in gacode profiles file with the format `['D','C','D[fast]']` for example
        :param fast_last: return fast ions last
        :return: list with strings with ion names
        """
        ion_names = [self['IONS'][k][0] for k in self['IONS']]
        numd_ion_names = []
        for k in list(self['IONS'].keys()):
            ion = self['IONS'][k][0]
            if self['IONS'][k][3] == 'fast':
                ion += '[fast]'
            count = 1
            for x in numd_ion_names:
                if ion in x:
                    count += 1
            if count > 1:
                ion += '#' + str(count)
            numd_ion_names.append(ion)
        # additional sorting thermal/fast if requested
        if fast_last:
            fast_last = []
            for k in numd_ion_names:
                if '[fast]' not in k:
                    fast_last.append(k)
            for k in numd_ion_names:
                if '[fast]' in k:
                    fast_last.append(k)
            return fast_last
        else:
            return numd_ion_names
    def reorder_ions(self, reordered_ions, verbose=True):
        """
        Reorder ions in gacode profiles file by sorting their ['ni', 'Ti', 'vtor', 'vpol'] entries
            NOTE: This method operates in place
        :param reordered_ions: list of ion names or numbers (as per .ion_names() method)
            If reordered_ions is None, then nothing is done.
        :param verbose: print when ions are reordered
        :return: self (modified)
        """
        if reordered_ions is None:
            return self
        numd_ion_names = self.ion_names()
        # convert integers to ion names
        reordered_ion_names = copy.deepcopy(reordered_ions)
        for k, item in enumerate(reordered_ions):
            if isinstance(reordered_ions[k], int):
                reordered_ion_names[k] = numd_ion_names[item - 1]
        # check that ions are only reordered
        if len(self['IONS']) != len(reordered_ion_names):
            raise OMFITexception('You can only reorder the ions. Valid ions name are: %s' % numd_ion_names)
        if set(numd_ion_names) != set(reordered_ion_names):
            raise OMFITexception('Valid ions name for reordering are: %s' % numd_ion_names)
        if tuple(reordered_ion_names) != tuple(numd_ion_names):
            # update 'ni', 'Ti', 'vtor', 'vpol' quantities
            new = {}
            ion_quants = ['ni', 'Ti', 'vtor', 'vpol']
            for i, ion in enumerate(reordered_ion_names):
                for k in ion_quants:
                    new[k + '_%d' % (i + 1)] = self[k + '_%d' % (numd_ion_names.index(ion) + 1)]
            self.update(new)
            # update IONS dictionary
            new = {}
            for i, ion in enumerate(reordered_ion_names):
                new[i + 1] = self['IONS'][numd_ion_names.index(ion) + 1]
            self['IONS'].update(new)
            if verbose:
                printi('Reodered gacode profiles ions to: %s' % reordered_ion_names)
        return self
    def add_ion(
        self,
        ion_num,
        ion,
        Z,
        A,
        ni=None,
        concentration=0,
        thermal=True,
        aoLn=None,
        isl_mul=None,
        isl_add=None,
        aoLn_from=None,
        z_eff=None,
        remove_density_from_ion=1,
        temperature_and_velocities_from_ion=1,
        verbose=True,
    ):
        """
        Add ion to a gacode profiles file. When an ion is added, its 'ni', 'Ti', 'vtor', 'vpol' quantities are set.
        The ion density profile of the added ion can be set in different ways:
        1. as user defined density profile
        2. as user defined normalized inverse scale lenght + concentration
        3. copy density profile from other ion and modify it by isl_mul and isl_add
        4. target Zeff
        NOTE: this method operates in place
        :param ion_num: insert new ion as this number
        :param ion: ion name
        :param Z: ion charge
        :param A: ion mass
        :param ni: ion density profile
        :param concentration: ion concentration nX/ne
        :param thermal: thermal or fast ion
        :param aoLn: (optional) normalized inverse scale length of the ion density profile
            Necessary for impurity transport studies when you need a finite gradient for computing D, V.
            If set, then the profile has the average concentration set at the boundary rho=1
        :param isl_mul: (optional) multiply the inverse scale length of the ion density profile as specified by aoLn_from
        :param isl_add: (optional) add to the inverse scale length of the ion density profile as specified by aoLn_from
        :param aoLn_from: (optional) specify a species to take the density profile from as starting point for the added profile (as a string or as an index)
        :param z_eff: (optional) add ion to reach a certain z_eff
        :param remove_density_from_ion: ion name or number from which to steal density (numbering before adding the new ion)
        :param temperature_and_velocities_from_ion: ion name or number from which the temperature and velocities are copied from (numbering before adding the new ion)
        :return: self (modified)
        """
        numd_ion_names = self.ion_names()
        if isinstance(remove_density_from_ion, int):
            remove_density_from_ion = numd_ion_names[remove_density_from_ion - 1]
        remove_density_from_ion_num = numd_ion_names.index(remove_density_from_ion) + 1
        if isinstance(temperature_and_velocities_from_ion, int):
            temperature_and_velocities_from_ion = numd_ion_names[temperature_and_velocities_from_ion - 1]
        temperature_and_velocities_from_ion_num = numd_ion_names.index(temperature_and_velocities_from_ion) + 1
        if verbose:
            if isl_mul is not None and isl_add is not None and aoLn_from is not None:
                if isinstance(aoLn_from, int):
                    aoLn_from = numd_ion_names[aoLn_from - 1]
                printi('Adding ion based on %s' % aoLn_from)
            else:
                printi('Adding ion %s' % ion)
            printi('Copying ion temperature from ni_%d %s' % (temperature_and_velocities_from_ion_num, temperature_and_velocities_from_ion))
            printi('Removing ion charge density from ni_%d %s' % (remove_density_from_ion_num, remove_density_from_ion))
        # ion density directly provided by the user
        if ni is not None:
            pass
        # calculate ion density to match given Z_eff
        elif z_eff is not None:
            z_eff_start = self.calc_zeff()
            ni = self['ne'] * (z_eff - z_eff_start) / float(Z) ** 2
            if np.any(ni < 0):
                raise ValueError('requested z_eff leads to negative ion density')
        # ion density as concentration of electron density
        elif aoLn is None and aoLn_from is None:
            ni = self['ne'] * concentration
        # generate ion density profile by
        else:
            # modifying the inverse scale length of an existing profile of species aoLn by adding/multiplying
            if isl_add is not None and isl_mul is not None and aoLn_from is not None:
                if isinstance(aoLn_from, int):
                    aoLn_from = numd_ion_names[aoLn_from - 1]
                aoLn_from_num = numd_ion_names.index(aoLn_from) + 1
                rmin = self['rmin']
                n = self['ni_%d' % aoLn_from_num]
                # Calculate the inverse scale length and then modify it by multiplying or adding a factor,
                isl = calcz(rmin, n) * isl_mul + isl_add
                # Integrate the inverse scale length profile to generate a new density profile
                # NOTE: start integration from the core outward
                ni = concentration * integz(rmin, isl, rmin[0], n[0], rmin)
            # user given inverse scale length aoLn
            else:
                rmin = self['rmin']
                isl = np.zeros_like(rmin) + aoLn * rmin[-1]
                isl[0] = 0.0
                ni = integz(rmin, isl, rmin[0], 1, rmin)
                ni -= ni[-1] - 1.0
                ni *= np.mean(self['ne']) * concentration
        Z_del = self['IONS'][remove_density_from_ion_num][1]
        self['ni_%d' % remove_density_from_ion_num] -= (Z / float(Z_del)) * ni
        # Check for negative density values
        if np.min(self['ni_%d' % remove_density_from_ion_num]) < 0:
            printw(
                'WARNING: species addition resulted in negative density values for %s! Negative values were set to zero.'
                % 'ni_%d'
                % remove_density_from_ion_num
            )
            self['ni_%d' % remove_density_from_ion_num][self['ni_%d' % remove_density_from_ion_num] < 0] = 0.0
        # new ion
        tmp = {}
        tmp['ni_%d' % ion_num] = ni
        tmp['Ti_%d' % ion_num] = self['Ti_%d' % temperature_and_velocities_from_ion_num]
        tmp['vtor_%d' % ion_num] = self['vtor_%d' % temperature_and_velocities_from_ion_num]
        tmp['vpol_%d' % ion_num] = self['vpol_%d' % temperature_and_velocities_from_ion_num]
        # Add the ion to the IONS list by reverse shifting every ion with number>=ion_num up one
        # Also must shift other ion properties in gacode profiles file
        for i in list(reversed(list(range(ion_num, len(self['IONS']) + 1)))):
            self['IONS'][i + 1] = self['IONS'].pop(i)
            self['ni_%d' % (i + 1)] = self['ni_%d' % i]
            self['Ti_%d' % (i + 1)] = self['Ti_%d' % i]
            self['vtor_%d' % (i + 1)] = self['vtor_%d' % i]
            self['vpol_%d' % (i + 1)] = self['vpol_%d' % i]
        # Update ion data
        self.update(tmp)
        self['IONS'][ion_num] = [ion, int(Z), float(A), ['fast', 'therm'][thermal]]
        self['IONS'].sort()
        self['N_ION'] = len(self['IONS'])
        # make z_eff self consistent
        self['z_eff'] = self.calc_zeff()
        # There are occurances where the stafile (i.e. ONETWO example) can mess this one
        self['z_eff'][self['z_eff'] < 1] = 1.0
        # update ptot
        self['ptot'] = self.calc_ptot()
        return self
    def del_ion(self, ion, add_density_to_ion=1, verbose=True):
        """
        Remove ion from a gacode profiles file. When an ion is removed, its 'ni', 'Ti', 'vtor', 'vpol' quantities are set to zero.
            NOTE: this method operates in place
        :param ion: ion name or number as per .ion_names()
        :param add_density_to_ion: ion name or number to which to add the density of the ion that is being removed
        :param verbose: print when ion is removed
        :return: self (modified)
        """
        numd_ion_names = self.ion_names()
        if isinstance(ion, int):
            ion = numd_ion_names[ion - 1]
        if isinstance(add_density_to_ion, int):
            add_density_to_ion = numd_ion_names[add_density_to_ion - 1]
        ion_num = numd_ion_names.index(ion) + 1
        add_density_to_ion_num = numd_ion_names.index(add_density_to_ion) + 1
        if ion_num == add_density_to_ion_num:
            add_density_to_ion_num += 1
        # The ion's charge and density
        if verbose:
            printi('Removing ion %s' % ion)
            printi('Adding ion charge density to ni_{} {}'.format(add_density_to_ion_num, numd_ion_names[add_density_to_ion_num - 1]))
        Z_del = self['IONS'][ion_num][1]
        Z_add = self['IONS'][add_density_to_ion_num][1]
        ni_del = self['ni_%d' % ion_num]
        self['ni_%d' % add_density_to_ion_num] += (Z_del * ni_del) / float(Z_add)
        # Change the number of all following ions (i.e. if we remove ion 2 of 3, rename 3->2)
        for i in range(ion_num, len(self['IONS'])):
            self['IONS'][i] = self['IONS'][i + 1]
            self['ni_%d' % i] = self['ni_%d' % (i + 1)].copy()
            self['Ti_%d' % i] = self['Ti_%d' % (i + 1)].copy()
            self['vtor_%d' % i] = self['vtor_%d' % (i + 1)].copy()
            self['vpol_%d' % i] = self['vpol_%d' % (i + 1)].copy()
            if 'dlnnidr_%d' % (i + 1) in self:
                self['dlnnidr_%d' % i] = self['dlnnidr_%d' % (i + 1)].copy()
            if 'dlntidr_%d' % (i + 1) in self:
                self['dlntidr_%d' % i] = self['dlntidr_%d' % (i + 1)].copy()
        # Remove all evidence of the discarded ion
        ion_num = len(self['IONS'])
        del self['ni_{}'.format(ion_num)]
        del self['Ti_{}'.format(ion_num)]
        del self['vtor_{}'.format(ion_num)]
        del self['vpol_{}'.format(ion_num)]
        if 'dlnnidr_{}'.format(ion_num) in self:
            del self['dlnnidr_{}'.format(ion_num)]
        if 'dlntidr_{}'.format(ion_num) in self:
            del self['dlntidr_{}'.format(ion_num)]
        del self['IONS'][ion_num]
        self['N_ION'] = len(self['IONS'])
        # make z_eff self consistent
        self['z_eff'] = self.calc_zeff()
        # There are occurances where the stafile (i.e. ONETWO example) can can mess this one
        self['z_eff'][self['z_eff'] < 1] = 1.0
        # update ptot
        self['ptot'] = self.calc_ptot()
        return self
    def merge_DT(self, raise_no_DT=True, verbose=True):
        """
        merge thermal deuterium (D) and tritium (T) species into a single (DT) species with Z=1 and A=2.5
        :param raise_no_DT: raise exception if D and T species are not found
        :param verbose: print to stdout
        """
        names = self.ion_names()
        if 'T' in names and 'D' in names:
            if names.index('D') > names.index('T'):
                H1 = 'D'
                H2 = 'T'
            else:
                H2 = 'D'
                H1 = 'T'
            self.del_ion(H1, H2)
            self['IONS'][names.index(H2) + 1] = ['DT', 1, 2.5, 'therm']
        elif 'D' not in names and raise_no_DT:
            raise OMFITexception('gacode profiles does not have D species')
        elif 'T' not in names and raise_no_DT:
            raise OMFITexception('gacode profiles does not have T species')
        self.enforce_quasineutrality(verbose=verbose)
    def remove_fast(self, only_He=False, raise_no_fast=True, verbose=True):
        """
        delete all fast ions
        :param only_He: keep helium ash or not
        :param raise_no_fast: raise exception if there are no fast ions
        :param verbose: print to stdout
        """
        names = self.ion_names(fast_last=True)
        self.reorder_ions(names)
        no_fast = True
        for item in names[::-1]:
            if '[fast]' in item and (not only_He or item.startswith('He')):
                no_fast = False
                self.del_ion(item, names[0], verbose=verbose)
        if no_fast and raise_no_fast:
            raise OMFITexception('gacode profiles does not have fast ion species')
        self.enforce_quasineutrality(verbose=verbose)
    def remove_impurities(self, keep_He=True, raise_no_imp=True, verbose=True):
        """
        remove all impurity species, that is thermal ions with Z>1 (or Z>2 if keep_He is True)
        :param keep_He: keep Helium ions
        :param raise_no_imp: raise exception if there are no impurity species
        :param verbose: print to stdout
        """
        if keep_He:
            keep_He = 2
        else:
            keep_He = 1
        names = self.ion_names()
        no_imp = True
        for item in names[::-1]:
            if self['IONS'][names.index(item) + 1][-1] == 'therm' and self['IONS'][names.index(item) + 1][1] > keep_He:
                no_imp = False
                self.del_ion(item, names[0], verbose=verbose)
        if no_imp and raise_no_imp:
            raise OMFITexception('gacode profiles does not have impurity species')
        self.enforce_quasineutrality(verbose=verbose)
    def lump_impurities(self, impurity_symbol, z_eff=None, keep_He=True, verbose=True):
        """
        lump all impurity species into a single one
        :param impurity_symbol: impurity symbol
        :param z_eff: target Zeff
        :param keep_He: keep He impurities untouched
        :param verbose: print to stdout
        """
        if z_eff is None:
            self.enforce_quasineutrality(verbose=verbose)
            z_eff = self['z_eff']
        # remove all impurities
        self.remove_impurities(keep_He=keep_He, raise_no_imp=False)
        # add the requested impurity species to match a given Zeff
        imp = list(atomic_element(symbol=impurity_symbol).values())[0]
        self.add_ion(len(self.ion_names()) + 1, impurity_symbol, imp['Z'], imp['A'], z_eff=z_eff)
        self.enforce_quasineutrality(verbose=verbose)
        names = self.ion_names(fast_last=True)
        self.reorder_ions(names)
    def remove_dummy_ions(self):
        """
        Remove trailing ion species in gacode profiles that have no density or temperature
        NOTE: N_ION and IONS are set and trimmed accordingly
        """
        for k in reversed(list(range(1, 11))):
            if np.sum(self['ni_%d' % k] * self['Ti_%d' % k]) == 0:
                if self['N_ION'] > k:
                    self['N_ION'] = k
            else:
                break
        self['N_ION'] = k
        for k in range(self['N_ION'] + 1, 11):
            if k in self['IONS']:
                del self['IONS'][k]
    def enforce_quasineutrality(self, ion=1, balanced_DT=True, verbose=True):
        """
        Modify ion density and electron densities to make the plasma quasineutral.
            NOTE: this method operates in place
            NOTE: this method will also update Zeff and ptot
        :param ion: ion name or number as per .ion_names()
        :param balanced_DT: if ion is `D` or `T` in a DT plasma, then make plasma quasineutral by equally splitting differences on D and T ions
        :param verbose: print to stdout
        :return: self (modified)
        """
        # identify ion
        numd_ion_names = self.ion_names()
        if isinstance(ion, int):
            ion = numd_ion_names[ion - 1]
        ion_num = numd_ion_names.index(ion) + 1
        # get weighted ion densities
        ne = self['ne']
        zarr = []
        sum_nizi = ne * 0
        nizi = {}
        for i, k in enumerate(list(self['IONS'].keys()), 1):
            Z = self['IONS'][k][1]
            nizi['ni_%d' % i] = self['ni_%d' % i] * Z
            sum_nizi += nizi['ni_%d' % i]
        # warning message
        ratio = max((abs(ne - sum_nizi) / ne)[:-1]) * 100
        if ratio > 0.1 and verbose:
            printw('Starting profiles violated quasineutrality by %3.3f%% of electron density' % ratio)
        # update main ions density to ensure quasineutrality
        # handle balanced DT
        if ion in ['D', 'T'] and 'D' in numd_ion_names and 'T' in numd_ion_names and balanced_DT:
            d_ind = numd_ion_names.index('D') + 1
            t_ind = numd_ion_names.index('T') + 1
            Z = 1.0
            assert float(self['IONS'][d_ind][1]) == float(self['IONS'][t_ind][1]) == Z
            old = np.min([self['ni_%d' % d_ind], self['ni_%d' % t_ind]], 0)
            delta = ne - sum_nizi
            delta_ni = delta.copy() / float(Z)
            condition = np.min(np.vstack((self['ni_%d' % d_ind], self['ni_%d' % d_ind])), 0) + delta_ni / 2.0
            delta_ni[condition < 0] = 0.0
            delta_ne = -delta.copy()
            delta_ne[condition > 0] = 0.0
            self['ni_%d' % d_ind] += delta_ni / 2.0
            self['ni_%d' % t_ind] += delta_ni / 2.0
            ratio = max(abs(delta_ni / 2.0 / old)[:-1]) * 100
            if np.any(condition > 0) and ratio > 0.1 and verbose:
                printi('Modified D-T densities by a maximum of %3.3f%% each to enforce quasineutrality' % ratio)
        # handle single ion
        else:
            Z = float(self['IONS'][ion_num][1])
            ind_key = 'ni_%d' % ion_num
            delta = ne - sum_nizi
            delta_ni = delta.copy() / float(Z)
            condition = self[ind_key] + delta_ni
            delta_ni[condition < 0] = 0.0
            delta_ne = -delta.copy()
            delta_ne[condition > 0] = 0.0
            self[ind_key] += delta_ni
            ratio = max(abs(delta_ni / (self[ind_key] - delta_ni))[:-1]) * 100
            if np.any(condition > 0) and ratio > 0.1 and verbose:
                printi('Modified %s density by a maximum of %3.3f%% to enforce quasineutrality' % (ion, ratio))
        # making the plasma quasineutral may require modifying the electron density to avoid negative ion densities
        self['ne'] += delta_ne
        ratio = max(abs(delta_ne / (self['ne'] - delta_ne))[:-1]) * 100
        if np.any(condition < 0) and ratio > 0.1 and verbose:
            printw('Modified electron density by a maximum of %3.3f%% to enforce quasineutrality' % ratio)
        # update zeff
        self['z_eff'] = self.calc_zeff()
        # update total pressure
        self['ptot'] = self.calc_ptot()
        return self
    def volume(self):
        """
        return volume of each flux surface
        """
        if 'vol' in self:
            return self['vol']
        return miller_derived(self['rmin'], self['rmaj'], self['kappa'], self['delta'], self['zeta'], self['zmag'], self['q'])['volume']
    def volume_integral(self, quantity):
        """
        Integrate a quantity over the plasma volume
        :param quantity: if string, it integrates quantity in gacode profiles, else one must pass an array on the same grid of profiles in gacode profiles file
        :return: array of integrated quantity from axis to edge
        """
        if isinstance(quantity, str) and quantity in self:
            quantity = self[quantity]
        return integrate.cumtrapz(np.gradient(self.volume()) * quantity, initial=0)
    def monotonic(self, pivot=0, fast_ions=False, minimum_z=0.01):
        """
        Force densities and temperatures are monotonically decreasing from core to edge
        NOTE: might want to call self.enforce_quasineutrality() afterwards
        :param pivot: rho value (between 0 and 1) where the profiles will match the original ones
        :param fast_ions: apply monotonic transformation also to fast-ions
        :param minimum_z: minimum inverse scale-length value
        """
        for item in self:
            if item[0] in ['n', 'T'] and np.sum(self[item]) != 0 and item != 'nuee' and item != 'TIME':
                if item[1] == 'i' and self['IONS'][int(item.split('_')[1])][3] == 'fast' and not fast_ions:
                    continue
                z = calcz(self['rho'], self[item])
                z[z < minimum_z] = minimum_z
                self[item] = integz(self['rho'], z, pivot, interp1d(self['rho'], self[item])(pivot), self['rho'])
    def shot_time(self):
        """
        :return: tuple shot and time as written in the gacode profiles header (None if they cannot be found)
        """
        shot = None
        for p in [0, 1]:
            for k, v in list(self.items()):
                if not k.startswith('__header'):
                    continue
                if 'SHOT NUMBER' in v:
                    shot = int(v.split(':')[-1])
                elif shot is not None and str(shot) in v:
                    try:
                        time = int(re.sub('([0-9]+).*', r'\1', v.split(str(shot))[1].strip('.').strip()))
                    except Exception:
                        continue
                    return shot, time
        return shot, None
    def rz_geometry(self, poloidal_resolution=101):
        """
        Return R,Z coordinates for all flux surfaces from either fourier, ham, or miller geometry representation
        :param poloidal_resolution: integer with number of equispaced points in toroidal angle, or array of toroidal angles
        :return: 2D arrays with (R, Z) flux surface coordinates
        """
        if 'ar' in self:
            r, z = self.rz_fourier_geometry(poloidal_resolution=poloidal_resolution)
        elif 'shape_cos0' in self:
            r, z = self.rz_ham_geometry(poloidal_resolution=poloidal_resolution)
        else:
            r, z = self.rz_miller_geometry(poloidal_resolution=poloidal_resolution)
        return r, z
    def rz_miller_geometry(self, poloidal_resolution=101):
        """
        return R,Z coordinates for all flux surfaces from miller geometry coefficients in gacode profiles file
        based on gacode/gapy/src/gapy_geo.f90
        :param poloidal_resolution: integer with number of equispaced points in toroidal angle, or array of toroidal angles
        :return: 2D arrays with (R, Z) flux surface coordinates
        """
        if isinstance(poloidal_resolution, int):
            t0 = np.linspace(0, 2 * np.pi, poloidal_resolution)
        else:
            t0 = poloidal_resolution
        x = np.arcsin(self['delta'])
        # R
        a = t0[:, np.newaxis] + x[np.newaxis, :] * np.sin(t0[:, np.newaxis])
        r0 = self['rmaj'][np.newaxis, :] + self['rmin'][np.newaxis, :] * np.cos(a)
        # Z
        a = t0[:, np.newaxis] + self['zeta'][np.newaxis, :] * np.sin(2 * t0[:, np.newaxis])
        z0 = self['zmag'][np.newaxis, :] + self['kappa'][np.newaxis, :] * self['rmin'][np.newaxis, :] * np.sin(a)
        return r0, z0
    def rz_fourier_geometry(self, poloidal_resolution=101):
        """
        return R,Z coordinates for all flux surfaces from fourier geometry representation in gacode profiles file
        :param poloidal_resolution: integer with number of equispaced points in toroidal angle, or array of toroidal angles
        :return: 2D arrays with (R, Z) flux surface coordinates
        """
        ar = copy.deepcopy(self['ar'])
        ar[:, 0] = ar[:, 0] / 2.0
        br = self['br']
        az = copy.deepcopy(self['az'])
        az[:, 0] = az[:, 0] / 2.0
        bz = self['bz']
        if isinstance(poloidal_resolution, int):
            x = np.linspace(0, 1, poloidal_resolution)
        else:
            x = poloidal_resolution
        XN = x[:, np.newaxis] * np.arange(ar.shape[1])
        r = np.zeros((len(x), ar.shape[0]))
        z = np.zeros((len(x), az.shape[0]))
        for l in range(ar.shape[0]):
            r[:, l] = np.sum(ar[l, :] * np.cos(2 * np.pi * XN) + br[l, :] * np.sin(2 * np.pi * XN), 1)
            z[:, l] = np.sum(az[l, :] * np.cos(2 * np.pi * XN) + bz[l, :] * np.sin(2 * np.pi * XN), 1)
        return r, z
    def rz_ham_geometry(self, poloidal_resolution=101):
        """
        return R,Z coordinates for all flux surfaces from ham geometry representation in gacode profiles file
        :param poloidal_resolution: integer with number of equispaced points in toroidal angle, or array of toroidal angles
        :return: 2D arrays with (R, Z) flux surface coordinates
        """
        rmaj = self['rmaj']
        zmaj = self['zmag']
        r = self['rmin']
        k = self['kappa']
        s1 = np.arcsin(self['delta'])
        s2 = -self['zeta']
        s3 = self['shape_sin3']
        c0 = self['shape_cos0']
        c1 = self['shape_cos1']
        c2 = self['shape_cos2']
        c3 = self['shape_cos3']
        if isinstance(poloidal_resolution, int):
            t = np.linspace(0, 2 * np.pi, poloidal_resolution)
        else:
            t = poloidal_resolution
        x = rmaj[np.newaxis, :] + r[np.newaxis, :] * np.cos(
            t[:, np.newaxis]
            + c0[np.newaxis, :]
            + s1[np.newaxis, :] * np.sin(t[:, np.newaxis])
            + c1[np.newaxis, :] * np.cos(t[:, np.newaxis])
            + s2[np.newaxis, :] * np.sin(2 * t[:, np.newaxis])
            + c2[np.newaxis, :] * np.cos(2 * t[:, np.newaxis])
            + s3[np.newaxis, :] * np.sin(3 * t[:, np.newaxis])
            + c3[np.newaxis, :] * np.cos(3 * t[:, np.newaxis])
        )
        y = zmaj[np.newaxis, :] + k[np.newaxis, :] * r[np.newaxis, :] * np.sin(t[:, np.newaxis])
        return x, y
    def convert_kxky(self, kx, ky, rho, theta=0.0, poloidal_resolution=101, make_plot=False):
        """
        Translate dimensionless kx, ky wavenumbers into physical inverse-lengths.
        kx ~ kn*rhos (normal to flux surfaces)
        ky ~ kb*rhos (binormal to flux surfaces)
        References,
            [1] Candy et al. GYRO Technical Guide (2014)
            [2] Candy et al. "A high-accuracy..." (2016)
            [3] Ruiz Ruiz et al. "Interpreting radial..." (2022)
        :arg kx: normalized radial wavenumber
        :arg ky: normalized binormal wavenumber
        :arg rho: radial location at which to provide the scale factors
        :param theta: poloidal angle theta at which to provide scale factors. if theta=-1 use UDS + theta=0 limit.
        :param poloidal_resolution: res of theta grid, passed to self.rz_geometry()
        :param make_plot: (bool) imports matplotlib.pyplot and makes an array of debugging plots.
        :return kn: wavenumber in the direction normal to flux-surfaces. (np array) [nkx, nky]
        :return kb: wavenumber in the binormal direction. (np array) [nky]
        """
        # if trying to convert at one value of kx or ky,
        if isinstance(kx, float) or isinstance(kx, int):
            kx = np.array([kx])
        if isinstance(ky, float) or isinstance(ky, int):
            ky = np.array([ky])
        r = self["rmin"]
        q = self["q"]
        t = np.linspace(0, 2 * np.pi, poloidal_resolution)
        # rz_geometry() method provides R(r,\theta) and Z(r, \theta)
        #   parameterizations of the flux surfaces. shapes are [poloidal_res, radial_grid]
        RR, ZZ = self.rz_geometry(poloidal_resolution=poloidal_resolution)
        # np.gradient returns a list of np arrays corresponding to the derivative w.r.t each dimension.
        # e.g.  gradRR[0] is dR/d\theta with shape: [pol_res., grid_size]
        #       gradRR[1] is dR/dr with the same shape.
        gradRR = np.gradient(RR, t, r)
        gradZZ = np.gradient(ZZ, t, r)
        # Compute the Jacobian determinant, eq. 2.50 [1]
        #   Jr = R* ( dR/dr*dZ/dth - dR/dth*dZ/dr )
        Jr = RR * (gradRR[1] * gradZZ[0] - gradRR[0] * gradZZ[1])
        # the following are magnitudes of gradients,
        grad_r = RR / Jr * np.sqrt(gradRR[0] ** 2 + gradZZ[0] ** 2)  # cf. eq. 2.51 [1]
        grad_theta = RR / Jr * np.sqrt(gradRR[1] ** 2 + gradZZ[1] ** 2)
        grad_r_dot_grad_theta = -((RR / Jr) ** 2) * (gradRR[0] * gradRR[1] + gradZZ[0] * gradZZ[1])
        # The Clebsch-angle, alpha = phi + nu(r, theta) is used in this calculation.
        # We will need derivatives of alpha w.r.t. (r, theta) --> compute nu(r,theta).
        # The integral for nu(r, theta) is derived from eq. 2.9 and 2.6 [1]
        integrand = Jr / (RR**2)
        I_psi_p = 2 * np.pi * q / np.trapz(integrand, t, axis=0)  # I/psi'
        from scipy.integrate import cumulative_trapezoid
        nu = -1 * I_psi_p * cumulative_trapezoid(integrand, t, initial=0, axis=0)
        # gradient of \nu(r, theta) coordiante over (r, theta)
        dnu_dtheta, dnu_dr = np.gradient(nu, t, r)  # [d\nu/d\theta, d\nu/dr]
        # Compute: grad(alpha) \cdot grad(r)
        galpha_dot_gr = dnu_dr * np.square(grad_r) + dnu_dtheta * grad_r_dot_grad_theta
        # With all of these geometric [r, theta] arrays we can perform the mapping,
        # cf. equations 54, 55 in [2]
        rmin_at_rho = interp1d(self["rho"], r)(rho)
        rmin_ind = np.argmin(abs(r - rmin_at_rho))
        t_ind = np.argmin(abs(t - theta))
        q_at_rho = interp1d(self["rho"], q)(rho)
        # point at which to evaluate subsequent interpolants.
        if theta == -1:
            rt_eval = [0.0, rmin_at_rho]
        else:
            rt_eval = [theta, rmin_at_rho]
        from scipy.interpolate import RegularGridInterpolator
        term1 = RegularGridInterpolator((t, r), galpha_dot_gr / grad_r)(rt_eval)
        term2 = RegularGridInterpolator((t, r), grad_r)(rt_eval)
        # Final Equation converting (kx, ky) --> kn (Normal direction)
        # if theta = -1 use the theta=0; up-down-symmetric (UDS) limit.
        kn = kx * term2  # (nkx,)
        if theta != -1:
            # Generally kn is a function of kx AND ky... so kn should become 2D,
            print("INFO: (convert_kxky) for theta != 0; kn is 2D (Nkx, Nky)")
            kn = np.atleast_2d(kn).T + rmin_at_rho * ky / q_at_rho * term1  # [nkx, nky]
        # The ky --> k_binormal equation is more complicated,
        # start with grad(alpha) X grad(r)
        gtheta_cross_gr = RR / Jr
        gphi_cross_gr = 1 / Jr * np.sqrt(gradRR[0] ** 2 + gradZZ[0] ** 2)
        gradalpha_cross_gradr = np.sqrt(dnu_dtheta**2 * gtheta_cross_gr**2 + gphi_cross_gr**2)
        # We also need the magnitude of grad\nu
        grad_alpha = np.sqrt(
            1 / RR**2 + grad_r**2 * dnu_dr**2 + 2 * grad_r_dot_grad_theta * dnu_dr * dnu_dtheta + grad_theta**2 * dnu_dtheta**2
        )
        # Then we can compute the ky multiplier,
        term1 = galpha_dot_gr**2 / (grad_r * gradalpha_cross_gradr)
        term2 = grad_r * grad_alpha**2 / (gradalpha_cross_gradr)
        term1 = RegularGridInterpolator((t, r), term1)(rt_eval)
        term2 = RegularGridInterpolator((t, r), term2)(rt_eval)
        # Final Equation mapping (kx, ky) --> kb
        kb = rmin_at_rho / q_at_rho * ky * (term1 - term2)  # [nky]
        # Remove the rho_s,unit multiplier,
        rho_sunit = interp1d(self["rho"], self["rhos"])(rho) * 100  # cm
        print(f"INFO: (convert_kxky) rho_s,unit = {rho_sunit:.3f} [cm] at rho = {rho}")
        # --- --- --- --- --- --- ---
        kn, kb = kn / rho_sunit, kb / rho_sunit
        # --- --- --- --- --- --- ---
        # Courtesy calculation of rho_s,D
        cs = interp1d(self["rho"], self["cs"])(rho) * 100  # [cm/s] ~ sqrt(Te/mD)
        Btot = interp1d(self["rho"], self["bt0"])(rho)  # [T]
        OmegaD = 4.79e7 * Btot  # NRL formulary, [rad/s]
        rho_s = cs / OmegaD
        print(f"INFO: (convert_kxky) rho_s,D = {rho_s:.3f} [cm] at rho = {rho}")
        if make_plot:
            import matplotlib.pyplot as plt
            fig, axs = plt.subplots(2, 3, num="convert_kxky: debugging plots", figsize=(11, 5))
            gs = axs[0, 0].get_gridspec()
            for a in axs[:, 0]:
                a.remove()
            ax0 = fig.add_subplot(gs[:, 0])
            # plot the flux-surface,
            ax0.plot(RR[:, rmin_ind], ZZ[:, rmin_ind], 'b-', label=fr"$\rho={rho}$")
            ax0.plot(RR[t_ind, rmin_ind], ZZ[t_ind, rmin_ind], 'r o', ms=10)
            skip = 5
            # The vector at each point is dR/dr, dZ/dr
            ax0.quiver(
                RR[::skip, rmin_ind],
                ZZ[::skip, rmin_ind],
                gradRR[1][::skip, rmin_ind],
                gradZZ[1][::skip, rmin_ind],
                color="g",
                zorder=2,
                label=r"$[dR/dr,dZ/dr]$",
            )
            ax0.quiver(
                RR[::skip, rmin_ind],
                ZZ[::skip, rmin_ind],
                gradRR[0][::skip, rmin_ind],
                gradZZ[0][::skip, rmin_ind],
                color="r",
                scale=5,
                zorder=2,
                label=r"$[dR/d\theta,dZ/d\theta]$",
            )
            ax0.set_aspect("equal")
            for k in ["right", "top"]:
                ax0.spines[k].set_visible(False)
            ax0.set_xlabel("R [m]")
            ax0.set_ylabel("Z [m]")
            axs = axs.flatten("F")[2:]
            # benchmark against q(r)
            axs[0].plot(r, q, label="q")
            axs[0].plot(rmin_at_rho, q_at_rho, 'r o', ms=10)
            axs[0].plot(r, -1 * nu[-1, :] / (2 * np.pi), label=r'$-\nu/2\pi$')
            axs[0].legend()
            axs[0].set_xlabel("rmin")
            axs[0].set_ylabel("q")
            # multipliers,
            axs[1].plot(self["rho"], self["grad_r0"], 'b--', label="GA['grad_r0']")
            axs[1].plot(self["rho"], grad_r[0, :], 'c-', label=r"$|\nabla r|_{\theta=0}$")
            axs[1].plot(self["rho"], grad_r[t_ind, :], 'b-', lw=2, label=rf"$|\nabla r|_{{\theta={theta*180/np.pi:.1f}^\circ}}$")
            axs[1].axvline(rho, ls='--')
            axs[1].axhline(1.0, ls='-', color='k')
            axs[1].set_xlabel(r"$\rho$")
            # and the theta = 0, UDS limit ky multiplier,
            ky_mult = -1 * np.sqrt(1 / RR[0, :] ** 2 + dnu_dtheta[0, :] ** 2 / (r * self["kappa"]) ** 2) * r / q
            axs[1].plot(self["rho"], ky_mult, 'm-', label=r"lim UDS,$\theta=0$: $-r/q(b\times \nabla r/|\nabla r|)\cdot\nabla \alpha$")
            term1 = galpha_dot_gr**2 / (grad_r * gradalpha_cross_gradr)
            term2 = grad_r * grad_alpha**2 / (gradalpha_cross_gradr)
            ky_mult_general = r / q * (term1[t_ind, :] - term2[t_ind, :])
            axs[1].plot(
                self["rho"],
                ky_mult_general,
                'r-',
                lw=2,
                label=rf"$-r/q(b\times \nabla r/|\nabla r|)\cdot\nabla \alpha|_{{\theta={theta*180/np.pi:.1f}^\circ}}$",
            )
            axs[1].legend()
            # kx vs. kn - NOTE: if theta != 0 this is 2D
            axs[2].set_prop_cycle('color', plt.cm.viridis(np.linspace(0, 1, len(ky))))
            axs[2].plot(kx, kn, '-o', ms=4)
            axs[2].plot([min(kx), max(kx)], [min(kx), max(kx)], 'k--', label="kn=kx")
            axs[2].axhline(0, ls=':', color="k")
            axs[2].axvline(0, ls=":", color="k")
            axs[2].set_xlabel("input: kx")
            axs[2].set_ylabel("output: kn [1/cm]")
            # ky vs. kb
            axs[3].plot(ky, kb, '-o')
            axs[3].plot([min(ky), max(ky)], [min(ky), max(ky)], 'k--', label="kb=ky")
            axs[3].set_xlabel("input: ky")
            axs[3].set_ylabel("output: kb [1/cm]")
            fig.tight_layout()
        return kn, kb
    def from_omas(self, ods, time_index=0, clear=True):
        """
        Translate OMAS data structure to gacode profiles file
        :param time_index: time index to extract data from
        :param clear: clear gacode profiles content prior populating it
        :return: self
        """
        from omas import ODS, omas_environment
        cocosio = 2  # GACODE uses COCOS 2
        rho = ods['core_profiles.profiles_1d.%d.grid.rho_tor_norm' % time_index]
        rho = rho[rho <= 1]
        coordsio = {'core_profiles.profiles_1d.%d.grid.rho_tor_norm' % time_index: rho}
        if (
            'equilibrium.time_slice.%d.profiles_1d.psi' % time_index in ods
            and 'equilibrium.time_slice.%d.profiles_1d.rho_tor_norm' % time_index in ods
        ):
            with omas_environment(ods, cocosio=cocosio):
                psi1D = interp1e(
                    ods['equilibrium.time_slice.%d.profiles_1d.rho_tor_norm' % time_index],
                    ods['equilibrium.time_slice.%d.profiles_1d.psi' % time_index],
                )(rho)
                coordsio['equilibrium.time_slice.%d.profiles_1d.psi' % time_index] = psi1D
        with omas_environment(ods, cocosio=cocosio, coordsio=coordsio):
            prof1d = ods['core_profiles.profiles_1d'][time_index]
            eq = ods['equilibrium.time_slice'][time_index]
            eq1d = ods['equilibrium.time_slice'][time_index]['profiles_1d']
            if clear:
                self.clear()
            else:
                # only clear headers
                for item in list(self.keys()):
                    if re.match(comment_ptrn, item):
                        del self[item]
            # This is a fresh file, so turn off dynaLoad
            self.dynaLoad = False
            self['__header_0000__'] = '# gacode profiles - Generated by OMFIT via OMAS on ' + now()
            self['__header_0001__'] = '#'
            self['__header_0002__'] = '#                 IONS :  Name       Z    Mass'
            self['__header_0003__'] = '#'
            self['IONS'] = SortedDict()
            self['N_ION'] = 0
            self['N_EXP'] = 0
            if 'core_profiles.vacuum_toroidal_field.b0' in ods:
                self['BT_EXP'] = ods['core_profiles.vacuum_toroidal_field.b0'][time_index]
            elif 'equilibrium.vacuum_toroidal_field.b0' in ods:
                self['BT_EXP'] = ods['equilibrium.vacuum_toroidal_field.b0'][time_index]
            if 'global_quantities.ip' in eq:
                self['IP_EXP'] = eq['global_quantities.ip'] / 1e6
            if 'core_profiles.vacuum_toroidal_field.r0' in ods:
                self['RVBV'] = ods['core_profiles.vacuum_toroidal_field.r0'] * ods['core_profiles.vacuum_toroidal_field.b0'][time_index]
            elif 'equilibrium.vacuum_toroidal_field.r0' in ods:
                self['RVBV'] = ods['equilibrium.vacuum_toroidal_field.r0'] * ods['equilibrium.vacuum_toroidal_field.b0'][time_index]
            self['ARHO_EXP'] = 0
            self['rho'] = prof1d['grid.rho_tor_norm']
            self['N_EXP'] = len(self['rho'])
            # zero out arrays
            for item in np.array(self.profNames[self.latest_version]).flatten():
                if item not in ['NULL', 'rho']:
                    self[item] = self['rho'] * 0.0
            # polflux is in Wb/rad, so actually psi_ref in COCOS 2
            # should be zero on-axis
            self['polflux'] = eq1d['psi'] - eq1d['psi'][0]
            try:
                if 'centroid' not in eq1d:
                    raise LookupError('OMAS centroid not available')
                self['rmin'] = 0.5 * (eq1d['centroid.r_max'] - eq1d['centroid.r_min'])
                self['rmin'][0] = 0.0
                self['rmaj'] = 0.5 * (eq1d['centroid.r_max'] + eq1d['centroid.r_min'])
                self['kappa'] = eq1d['elongation']
                self['delta'] = 0.5 * (eq1d['triangularity_lower'] + eq1d['triangularity_upper'])
                if np.all(
                    [
                        k in eq1d
                        for k in ['squareness_upper_outer', 'squareness_upper_inner', 'squareness_lower_outer', 'squareness_lower_inner']
                    ]
                ):
                    self['zeta'] = 0.25 * (
                        eq1d['squareness_upper_outer']
                        + eq1d['squareness_upper_inner']
                        + eq1d['squareness_lower_outer']
                        + eq1d['squareness_lower_inner']
                    )
                else:
                    self['zeta'] = self['kappa'] * 0.0
                self['zmag'] = eq1d['centroid.z']
                self['q'] = eq1d['q']
            except (LookupError, TypeError):
                # if the geometric quantities are missing, then we have no choice but to trace the flux surfaces
                psin = eq1d['psi']
                psin = (psin - min(psin)) / (max(psin) - min(psin))
                tmp = fluxSurfaces(
                    Rin=eq['profiles_2d'][0]['r'][:, 0],
                    Zin=eq['profiles_2d'][0]['z'][0, :],
                    PSIin=eq['profiles_2d'][0]['psi'].T,
                    Btin=eq['profiles_2d'][0]['b_field_tor'].T,
                    Rcenter=eq['global_quantities.magnetic_axis.r'],
                    F=eq1d['f'],
                    P=eq1d['pressure'],
                    levels=psin,
                    cocosin=cocosio,
                    quiet=True,
                )
                tmp.dynaLoad = False
                if 'global_quantities.psi_boundary' in eq:
                    tmp.forceFindSeparatrix = False
                    tmp._findAxis()
                    tmp.flx = eq['global_quantities.psi_boundary']
                # tmp.PSIaxis = eq['global_quantities.psi_axis']
                tmp.load()
                self['rmin'] = tmp['geo']['a']
                self['rmaj'] = tmp['geo']['R']
                self['kappa'] = tmp['geo']['kap']
                self['delta'] = tmp['geo']['delta']
                self['zeta'] = tmp['geo']['zeta']
                self['zmag'] = tmp['geo']['Z']
                self['q'] = tmp['avg']['q']
            if "phi" in eq1d:
                self['ARHO_EXP'] = np.sqrt(2 * abs(eq1d['phi'][-1] / np.pi / 2 / self['BT_EXP']))
            else:
                # use apprixmation if phi isn't there
                self['ARHO_EXP'] = np.sqrt(self['kappa'][-1]) * self['rmin'][-1]
            self['ne'] = prof1d['electrons.density_thermal'] / 1e19
            self['Te'] = prof1d['electrons.temperature'] / 1e3
            derived = miller_derived(self['rmin'], self['rmaj'], self['kappa'], self['delta'], self['zeta'], self['zmag'], self['q'])
            R = self['rmaj'] + self['rmin']
            Bp = derived['bp0']
            Bt = derived['bt0']
            i = 0
            self['ptot'] = prof1d['electrons.density_thermal'] * prof1d['electrons.temperature'] * constants.e
            for therm_fast, density in [('therm', 'density_thermal'), ('fast', 'density_fast')]:
                for k in range(len(prof1d['ion'])):
                    if len(prof1d['ion']) and density in prof1d['ion'][k] and np.sum(np.abs(prof1d['ion'][k][density])) > 0:
                        i += 1
                        A = prof1d['ion'][k]['element'][0]['a']
                        Z_N = Z = prof1d['ion'][k]['element'][0]['z_n']
                        if 'z_ion' in prof1d['ion'][k]:
                            Z = prof1d['ion'][k]['z_ion']
                        if 'label' in prof1d['ion'][k]:
                            label = prof1d['ion'][k]['label'].strip().split()[0]
                        else:
                            try:
                                label = list(atomic_element(A=A, Z=Z_N).values())[0]['symbol']
                            except ValueError:
                                label = 'LUMPED'
                        self['IONS'][i] = self['ni_%d' % i] = [label, Z, A, therm_fast]
                        self['ni_%d' % i] = prof1d['ion'][k][density] / 1e19
                        if therm_fast == 'therm':
                            self['Ti_%d' % i] = prof1d['ion'][k]['temperature'] / 1e3
                            self['ptot'] += prof1d['ion'][k]['density_thermal'] * prof1d['ion'][k]['temperature'] * constants.e
                            if 'ion.%d.rotation.parallel_stream_function' % k in prof1d:
                                kpol = prof1d['ion.%d.rotation.parallel_stream_function' % k]
                                omegp = -Bt * kpol / R
                                self['vpol_%d' % i] = kpol * Bp
                                if 'ion.%d.rotation.perpendicular' % k in prof1d:
                                    omgvb = prof1d['ion.%d.rotation.perpendicular' % k]
                                elif ('rotation_frequency_tor_sonic' in prof1d) and ('ion.%d.rotation.diamagnetic' % k in prof1d):
                                    omgvb = prof1d['rotation_frequency_tor_sonic'] - prof1d['ion.%d.rotation.diamagnetic' % k]
                                else:
                                    self['vtor_%d' % i] = 0.0 * R
                                    continue
                                self['vtor_%d' % i] = R * (omgvb - omegp)
                            else:
                                self['vpol_%d' % i] = 0.0 * R
                        else:
                            self['Ti_%d' % i] = (
                                (
                                    (2 * prof1d['ion'][k]['pressure_fast_perpendicular'] + prof1d['ion'][k]['pressure_fast_parallel'])
                                    / prof1d['ion'][k]['density_fast']
                                )
                                / constants.e
                                / 1e3
                            )
                            self['ptot'] += 2 * prof1d['ion'][k]['pressure_fast_perpendicular'] + prof1d['ion'][k]['pressure_fast_parallel']
                            # Set finite temperature when density ~0  for TGYRO splines
                            navg = np.mean(prof1d['ion'][k]['density_fast'])
                            self['Ti_%d' % i][np.where(prof1d['ion'][k]['density_fast'] < 1e-6 * navg)] = np.mean(self['Ti_%d' % i])
            self['N_ION'] = i
            self['z_eff'] = prof1d['zeff']
            if 'rotation_frequency_tor_sonic' in prof1d:
                self['omega0'] = prof1d['rotation_frequency_tor_sonic']
            elif 'omega0' in prof1d:
                self['omega0'] = prof1d['omega0']
            else:
                self['omega0'] = prof1d['zeff'] * 0.0
        # =============
        # Core Sources
        # =============
        if 'core_sources' not in ods:
            printw('`core_sources` data not found in supplied ODS. Skip populating gacode profiles sources!')
        else:
            def d_dvol(y):
                return deriv(src['grid.volume'], y)
            def i_vol(y):
                return integrate.cumtrapz(y, src['grid.volume'], initial=0)
            source = ods['core_sources.source']
            for ks in range(len(source)):
                identifier = source[ks]['identifier.name']
                id_index = source[ks].get('identifier.index', None)
                # decide how to map data in OMAS to input.gacode
                # two passes: first try a matching index, second use the catch-all [-1]
                candidates = []
                for t in range(2):
                    for item in omas_source_mapper:
                        type, possible_id_index, details = omas_source_mapper[item]
                        if id_index is not None and ((t == 0 and id_index in possible_id_index) or (t > 0 and -1 in possible_id_index)):
                            if isinstance(self, OMFITinputprofiles) and type == 'i_vol':
                                candidates.append(item)
                            elif isinstance(self, OMFITinputgacode) and type == 'd_dvol':
                                candidates.append(item)
                if not len(candidates):
                    printe('Unrecognized source: %s of IMAS type index %d' % (identifier, id_index))
                    continue
                src = source[ks]['profiles_1d'][time_index]
                coordsio2 = copy.deepcopy(coordsio)
                coordsio2['core_sources.source.%d.profiles_1d.%d.grid.rho_tor_norm' % (ks, time_index)] = rho
                with omas_environment(ods, cocosio=cocosio, coordsio=coordsio2):
                    # NOTE: input.profiles sources are volume integrated
                    # NOTE: input.gacode sources are the not (though the derived integrated value is also available)
                    if 'volume' in src['grid']:
                        vol = src['grid.volume']
                    else:
                        try:
                            vol = interp1d(
                                ods['equilibrium']['time_slice'][time_index]['profiles_1d']['rho_tor_norm'],
                                ods['equilibrium']['time_slice'][time_index]['profiles_1d']['volume'],
                            )(rho)
                        except Exception:
                            vol = None
                    accounted = []
                    for candidate in candidates:
                        type, possible_id_index, details = omas_source_mapper[candidate]
                        for sign, location, norm in details:
                            ilocation = map_d_i[location]
                            # check if source density is available
                            if location not in accounted and location in src and np.sum(np.abs(src[location])) > 0:
                                if candidate not in self:
                                    self[candidate] = self['rho'] * 0.0
                                if isinstance(self, OMFITinputgacode):
                                    self[candidate] += sign * src[location] / norm
                                    print('%d) %s %s= %s[%s]/%g' % (ks, candidate, ['-', '+'][int(sign > 0)], identifier, location, norm))
                                elif isinstance(self, OMFITinputprofiles):
                                    self[candidate] += sign * i_vol(src[location]) / norm
                                    print(
                                        '%d) %s %s= i_vol(%s[%s])/%g'
                                        % (ks, candidate, ['-', '+'][int(sign > 0)], identifier, location, norm)
                                    )
                                accounted.append(location)
                            # alternatively get it from integrated source
                            elif location not in accounted and ilocation in src and np.sum(np.abs(src[ilocation])) > 0:
                                if candidate not in self:
                                    self[candidate] = self['rho'] * 0.0
                                if isinstance(self, OMFITinputgacode):
                                    self[candidate] += sign * d_dvol(src[ilocation]) / norm
                                    print(
                                        '%d) %s %s= d_dvol(%s[%s])/%g'
                                        % (ks, candidate, ['-', '+'][int(sign > 0)], identifier, ilocation, norm)
                                    )
                                elif isinstance(self, OMFITinputprofiles):
                                    self[candidate] += sign * src[ilocation] / norm
                                    print('%d) %s %s= %s[%s]/%g' % (ks, candidate, ['-', '+'][int(sign > 0)], identifier, ilocation, norm))
                                accounted.append(location)
        # set the GACODEtype
        self.GACODEtype = 'profiles'
        # update all derived quantities
        self.consistent_derived()
        # run checks
        self.checks()
        return self
    def to_omas(self, ods=None, time_index=0, update=['core_profiles', 'equilibrium', 'core_sources']):
        """
        Translate gacode profiles file to OMAS data structure
        :param ods: input ods to which data is added
        :param time_index: time index to which data is added
        :update: list of IDS to update from gacode profiles
        :return: ODS
        """
        from omas import ODS, omas_environment
        if ods is None:
            ods = ODS()
        cocosio = 2  # GACODE is COCOS 2
        # setup coordsio for automatic interpolation
        coordsio = {
            'core_profiles.profiles_1d.%d.grid.rho_tor_norm' % time_index: self['rho'],
            'equilibrium.time_slice.%d.profiles_1d.psi' % time_index: self['polflux'],
        }
        for i, s in enumerate(omas_source_mapper):
            coordsio['core_sources.source.%d.profiles_1d.%d.grid.rho_tor_norm' % (i, time_index)] = self['rho']
        # generate ODS
        with omas_environment(ods, cocosio=cocosio, coordsio=coordsio):
            eq = ods['equilibrium.time_slice'][time_index]
            # =============
            # Equilibrium
            # =============
            if 'equilibrium' in update:
                eq['global_quantities.magnetic_axis.b_field_tor'] = self['BT_EXP']
                if 'IP_EXP' in self:
                    eq['global_quantities.ip'] = self['IP_EXP'] * 1e6
                ods.set_time_array('core_profiles.vacuum_toroidal_field.b0', time_index, self['BT_EXP'])
                ods.set_time_array('equilibrium.vacuum_toroidal_field.b0', time_index, self['BT_EXP'])
                if 'RVBV' in self:
                    ods['core_profiles.vacuum_toroidal_field.r0'] = self['RVBV'] / self['BT_EXP']
                    ods['equilibrium.vacuum_toroidal_field.r0'] = ods['core_profiles.vacuum_toroidal_field.r0']
                r, z = self.rz_geometry()
                eq['boundary.outline.r'] = r[:, -1]
                eq['boundary.outline.z'] = z[:, -1]
                eq['profiles_1d.rho_tor_norm'] = self['rho']
                eq['profiles_1d.psi'] = self['polflux']
                eq['profiles_1d.elongation'] = self['kappa']
                eq['profiles_1d.triangularity_upper'] = self['delta']
                eq['profiles_1d.triangularity_lower'] = self['delta']
                eq['profiles_1d.squareness_upper_outer'] = self['zeta']
                eq['profiles_1d.squareness_upper_inner'] = self['zeta']
                eq['profiles_1d.squareness_lower_outer'] = self['zeta']
                eq['profiles_1d.squareness_lower_inner'] = self['zeta']
                eq['profiles_1d.r_outboard'] = self['rmaj'] + self['rmin']
                eq['profiles_1d.r_inboard'] = self['rmaj'] - self['rmin']
                eq['profiles_1d.centroid.r_max'] = self['rmaj'] + self['rmin']
                eq['profiles_1d.centroid.r_min'] = self['rmaj'] - self['rmin']
                eq['profiles_1d.centroid.z'] = self['zmag']
                eq['profiles_1d.q'] = self['q']
            # geometry info
            derived = miller_derived(self['rmin'], self['rmaj'], self['kappa'], self['delta'], self['zeta'], self['zmag'], self['q'])
            R = self['rmaj'] + self['rmin']
            Bp = derived['bp0'] + np.finfo(float).eps  # prevent division by zero
            Bt = derived['bt0']
            # =============
            # Core Profiles
            # =============
            if 'core_profiles' in update:
                prof1d = ods['core_profiles.profiles_1d'][time_index]
                prof1d['grid.rho_tor_norm'] = self['rho']
                prof1d['grid.volume'] = derived['volume']
                prof1d['electrons.density_thermal'] = self['ne'] * 1e19
                prof1d['electrons.temperature'] = self['Te'] * 1e3
                if 'ion' in prof1d:
                    prof1d['ion'].clear()
                ions = {}
                for i, ion in enumerate(self['IONS'].values()):
                    i += 1
                    ion_name, Z_ion, A, therm_fast = ion
                    if ion_name not in ions:
                        ions[ion_name] = len(ions)
                    k = ions[ion_name]
                    if ion_name.lower() in ['lumped']:
                        ion_details = {'symbol': ion_name, 'Z_ion': Z_ion, 'Z': Z_ion, 'A': A}
                    else:
                        ion_details = list(atomic_element(symbol=ion_name, Z_ion=Z_ion).values())[0]
                    prof1d['ion'][k]['label'] = ion_details['symbol']
                    prof1d['ion'][k]['z_ion'] = ion_details['Z_ion']
                    prof1d['ion'][k]['multiple_states_flag'] = 0
                    prof1d['ion'][k]['element'][0]['atoms_n'] = 1
                    prof1d['ion'][k]['element'][0]['z_n'] = ion_details['Z']
                    prof1d['ion'][k]['element'][0]['a'] = ion_details['A']
                    if therm_fast == 'therm':
                        density = 'density_thermal'
                    else:
                        density = 'density_fast'
                    prof1d['ion'][k][density] = self['ni_%d' % i] * 1e19
                    if therm_fast == 'therm':
                        prof1d['ion'][k]['temperature'] = self['Ti_%d' % i] * 1e3
                        # only send rotations to OMAS if non-zero
                        if np.any(self['vpol_%d' % i] != 0.0) or np.any(self['vtor_%d' % i] != 0.0):
                            kpol = self['vpol_%d' % i] / Bp
                            omeg = self['vtor_%d' % i] / R
                            omegp = -kpol * Bt / R
                            prof1d['ion'][k]['rotation.parallel_stream_function'] = kpol
                            prof1d['ion'][k]['rotation.perpendicular'] = omeg + omegp
                            prof1d['ion'][k]['rotation.diamagnetic'] = self['omega0'] - omeg - omegp
                    else:
                        prof1d['ion'][k]['pressure_fast_perpendicular'] = (
                            self['Ti_%d' % i] / 3.0 * prof1d['ion'][k]['density_fast'] * constants.e * 1e3
                        )
                        prof1d['ion'][k]['pressure_fast_parallel'] = (
                            self['Ti_%d' % i] / 3.0 * prof1d['ion'][k]['density_fast'] * constants.e * 1e3
                        )
                prof1d['zeff'] = self['z_eff']
                prof1d['rotation_frequency_tor_sonic'] = self['omega0']
            # =============
            # Core Sources
            # =============
            if 'core_sources' in update or 'core_sources-internal' in update or 'core_sources-aux' in update:
                source = ods['core_sources.source']
                if 'core_sources' in update:
                    source.clear()
                if 'core_sources-internal' in update:
                    for s in range(len(source) - 1, -1, -1):
                        if source[s]['identifier.index'] in sources_internal_list:
                            del source[s]
                if 'core_sources-aux' in update:
                    for s in range(len(source) - 1, -1, -1):
                        if source[s]['identifier.index'] not in sources_internal_list:
                            del source[s]
                volume = derived['volume']
                def d_dvol(y):
                    return deriv(source[s]['profiles_1d'][time_index]['grid.volume'], y)
                def i_vol(y):
                    return integrate.cumtrapz(y, source[s]['profiles_1d'][time_index]['grid.volume'], initial=0)
                for identifier in omas_source_mapper:
                    type, possible_id_index, details = omas_source_mapper[identifier]
                    id_index = possible_id_index[0]
                    if 'core_sources' not in update and not ('core_sources-internal' in update and 'core_sources-aux' in update):
                        if 'core_sources-aux' in update and id_index in sources_internal_list:
                            continue
                        if 'core_sources-internal' in update and id_index not in sources_internal_list:
                            continue
                    if type == 'd_dvol' and isinstance(self, OMFITinputprofiles):
                        continue
                    elif type == 'i_vol' and isinstance(self, OMFITinputgacode):
                        continue
                    s = len(source)
                    source[s]['profiles_1d'][time_index]['grid.volume'] = volume
                    source[s]['identifier.name'] = identifier
                    source[s]['identifier.index'] = id_index
                    src = source[s]['profiles_1d'][time_index]
                    for sign, location, norm in details:
                        ilocation = map_d_i[location]
                        # split collisional_equipartition between ions and electrons
                        if id_index == 11:
                            norm = norm / 2.0
                        # we write both source densities as well as integrated values
                        if location not in src:
                            src[location] = volume * 0.0
                        if ilocation not in src:
                            src[ilocation] = volume * 0.0
                        if type == 'd_dvol':
                            src[location] += sign * self[identifier] * norm
                            src[ilocation] += sign * i_vol(self[identifier]) * norm
                        elif type == 'i_vol':
                            src[location] += sign * d_dvol(self[identifier]) * norm
                            src[ilocation] += sign * self[identifier] * norm
            # =============
            # Summary
            # =============
            if 'ptransp' in self:
                if 'power_loss' not in ods['summary.global_quantities']:
                    ods['summary.global_quantities.power_loss.value'] = [self['ptransp'] * 1e6]
                else:
                    ods['summary.global_quantities.power_loss.value'][time_index] = self['ptransp'] * 1e6
            if 'vol' in self:
                norm = 1e19 * 1e3 * constants.e
                total_pressure = self['ne'] * self['Te'] * norm
                ion_names = self.ion_names()
                for idx, ion in enumerate(ion_names):
                    idx += 1
                    if 'fast' in ion.lower():
                        continue
                    else:
                        total_pressure += self[f'ni_{idx}'] * self[f'Ti_{idx}'] * norm
                if 'energy_thermal' not in ods['summary.global_quantities']:
                    ods['summary.global_quantities.energy_thermal.value'] = [3 / 2 * np.trapz(total_pressure, x=self['vol'])]
                else:
                    ods['summary.global_quantities.energy_thermal.value'][time_index] = 3 / 2 * np.trapz(total_pressure, x=self['vol'])
        return ods
    def __save_kw__(self):
        """
        :return: kw dictionary used to save the attributes to be passed when reloading from OMFITsave.txt
        """
        tmp = self.OMFITproperties.copy()
        if 'GACODEtype' in self.OMFITproperties and self.OMFITproperties['GACODEtype'] is None:
            tmp.pop('GACODEtype')
        return tmp
    def __popup_menu__(self):
        menu = []
        if self.GACODEtype in ['profiles', 'geo']:
            menu.append(['Plot flux surfaces', lambda: self.plot(only2D=True)])
        if self.GACODEtype == 'profiles':
            menu.append(['Consistency checks', self.checks])
        return menu
    def calcQ(self):
        """
        Calculate and return the fusion energy gain factor, Q,
        assuming D+T->alpha+neutron is the main reaction
        """
        # Factor of 5 in following is because total fusion power = 5*alpha power, correct for DT
        return (self['pow_i_fus'][-1] + self['pow_e_fus'][-1]) * 5 / (self['pow_i_aux'][-1] + self['pow_e_aux'][-1])
    def calc_powei_fusion(self, in_place=False):
        """
        calculate pow_e_fus and pow_i_fus given profiles
        :param in_place: update qfusi, qfusi, pow_e_fus and pow_i_fus
        :return: tuple with pow_e_fus, pow_i_fus, qfuse, qfusi
        """
        ion_names = self.ion_names()
        if 'DT' in ion_names:
            k = ion_names.index('DT') + 1
            Ti = self['Ti_%d' % k] * 1e3
            nD = self['ni_%d' % k] * 1e19 / 2.0
            nT = self['ni_%d' % k] * 1e19 / 2.0
        elif 'D' in ion_names and 'T' in ion_names:
            k = ion_names.index('D') + 1
            Ti = self['Ti_%d' % k] * 1e3
            nD = self['ni_%d' % k] * 1e19
            k = ion_names.index('T') + 1
            nT = self['ni_%d' % k] * 1e19
            Ti += self['Ti_%d' % k] * 1e3
            Ti /= 2.0
        else:
            k = 1
            Ti = self['Ti_%d' % k] * 1e3
            nD = self['ni_%d' % k] * 1e19 / 2.0
            nT = self['ni_%d' % k] * 1e19 / 2.0
        # fusion power
        qfus = fusion_power(nD, nT, Ti)
        pfus = self.volume_integral(qfus)
        # calculate alpha heating partion betweek electrons and ions
        ni = []
        zi = []
        mi = []
        for k, name in enumerate(self.ion_names()):
            if self['IONS'][k + 1][-1] == 'therm':
                ni.append(self['ni_%d' % (k + 1)])
                zi.append(self['IONS'][k + 1][1])
                mi.append(self['IONS'][k + 1][2])
        ne = self['ne']
        Te = self['Te']
        frac_ai = fast_heating(np.array(ni), zi, mi, ne, Te * 1e3, 3.5e6, 4 * constants.proton_mass)
        pow_e_fus = pfus * (1 - frac_ai) / 1e6
        pow_i_fus = pfus * frac_ai / 1e6
        qfuse = qfus * (1 - frac_ai) / 1e6
        qfusi = qfus * frac_ai / 1e6
        if in_place:
            self['pow_e'] = self['pow_e'] - self['pow_e_fus'] + pow_e_fus
            self['pow_e_fus'] = pow_e_fus
            self['pow_i'] = self['pow_i'] - self['pow_i_fus'] + pow_i_fus
            self['pow_i_fus'] = pow_i_fus
            self['qfuse'] = qfuse
            self['qfusi'] = qfusi
        return pow_e_fus, pow_i_fus, qfuse, qfusi
    def to_gen(self, input_profiles_version=-1):
        '''
        Generate input.XXX.gen file
        :param input_profiles_version: version of the input.profiles format to use (-1 uses latest)
        '''
        tmp = _gacode_profiles()
        tmp.init_profiles_names()
        if input_profiles_version == -1:
            input_profiles_version = self.latest_version
        txt = [
            f'''
{self.get('SHOT', 0)}  SHOT
{len(self['IONS'])}  N_ION
{self['N_EXP']}  N_EXP
{self.get('BT_EXP', 0.0)}  BT_EXP
{self.get('IP_EXP', 0.0)}  IP_EXP
{self.get('RVBV', 0.0)}  RVBV
{self['ARHO_EXP']} ARHO_EXP
    '''.strip()
        ]
        for row in tmp.profNames[input_profiles_version]:
            for k in row:
                if k == 'NULL' or k not in self:
                    txt.append('\n'.join(map(lambda x: '%10.10e' % x, self['rho'] * 0.0)))
                else:
                    txt.append('\n'.join(map(lambda x: '%10.10e' % x, self[k])))
        tmp = OMFITascii(os.path.split(self.filename)[1] + '.gen')
        tmp.write('\n'.join(txt))
        return tmp
# OMAS extra_structures
# omega0 is kept for backward compatibility
_extra_structures = {
    'core_profiles': {
        "core_profiles.profiles_1d[:].omega0": {
            "coordinates": ["core_profiles.profiles_1d[:].grid.rho_tor_norm"],
            "data_type": "FLT_1D",
            "documentation": "Plasma rotation",
            "full_path": "core_profiles/profiles_1d(itime)/omega0(:)",
            "data_type": "FLT_1D",
            "units": "rad/s",
            "cocos_signal": "TOR",
        },
        "core_profiles.profiles_1d[:].ion[:].rotation.perpendicular": {
            "coordinates": ["core_profiles.profiles_1d[:].grid.rho_tor_norm"],
            "data_type": "FLT_1D",
            "documentation": "ion perpendicular VxB rotation",
            "full_path": "core_profiles/profiles_1d(itime)/ion(i1)/rotation/perpendicular(:)",
            "data_type": "FLT_1D",
            "units": "rad/s",
            "cocos_signal": "TOR",
        },
        "core_profiles.profiles_1d[:].ion[:].rotation.diamagnetic": {
            "coordinates": ["core_profiles.profiles_1d[:].grid.rho_tor_norm"],
            "data_type": "FLT_1D",
            "documentation": "ion diamagnetic rotation",
            "full_path": "core_profiles/profiles_1d(itime)/ion(i1)/rotation/diamagnetic(:)",
            "data_type": "FLT_1D",
            "units": "rad/s",
            "cocos_signal": "TOR",
        },
        "core_profiles.profiles_1d[:].ion[:].rotation.parallel_stream_function": {
            "coordinates": ["core_profiles.profiles_1d[:].grid.rho_tor_norm"],
            "data_type": "FLT_1D",
            "documentation": "ion parallel stream function",
            "full_path": "core_profiles/profiles_1d(itime)/ion(i1)/rotation/parallel_stream_function(:)",
            "data_type": "FLT_1D",
            "units": "rad/s"
            # no COCOS transformation
        },
    }
}
omas.omas_utils._structures = {}
omas.omas_utils._structures_dict = {}
if not hasattr(omas.omas_utils, '_extra_structures'):
    omas.omas_utils._extra_structures = {}
for _ids in _extra_structures:
    for _item in _extra_structures[_ids]:
        _extra_structures[_ids][_item]['lifecycle_status'] = 'omfit'
    omas.omas_utils._extra_structures.setdefault(_ids, {}).update(_extra_structures[_ids])
############################################
if '__main__' == __name__:
    test_classes_main_header()
    import warnings
    warnings.simplefilter("error")
    if False:
        print('=' * 20)
        from pygacode.test import test_install
        print('=' * 20)
    # make a copy of the sample input.gacode in the current working directory
    OMFITascii(OMFITsrc + '/../samples/input.gacode').deploy('input.gacode')
    # check from_omas and to_omas
    tmp = OMFITinputgacode('input.gacode')
    ods = tmp.to_omas()
    tmp0 = OMFITinputgacode('input.gacode0')
    tmp0.from_omas(ods)
    if False:
        # plot
        from matplotlib import pyplot
        axs = tmp.plot(pretty_names=False)
        tmp0.plot(color='r', axs=axs, pretty_names=False)
        pyplot.show()