Source code for omfit_classes.omfit_eqdsk

try:
    # framework is running
    from .startup_choice import *
except ImportError as _excp:
    # class is imported by itself
    if (
        'attempted relative import with no known parent package' in str(_excp)
        or 'No module named \'omfit_classes\'' in str(_excp)
        or "No module named '__main__.startup_choice'" in str(_excp)
    ):
        from startup_choice import *
    else:
        raise

from omfit_classes.exceptions_omfit import doNotReportException as DoNotReportException

from omfit_classes.omfit_ascii import OMFITascii
from omfit_classes.omfit_nc import OMFITnc
from omfit_classes.omfit_namelist import OMFITnamelist

from omfit_classes import fluxSurface
from omfit_classes.fluxSurface import fluxSurfaces, fluxSurfaceTraces, boundaryShape, BoundaryShape, fluxGeo, rz_miller, miller_derived
from omfit_classes import namelist
from omfit_classes.omfit_mds import OMFITmdsValue
from omfit_classes.omfit_error import OMFITerror

from omfit_classes import utils_fusion
from omfit_classes.utils_fusion import is_device
from omfit_classes.utils_math import contourPaths, closestIndex, RectBivariateSplineNaN, interp1e, interp1dPeriodic, fourier_boundary

from omas import ODS, omas_environment, cocos_transform, define_cocos
import scipy
from scipy import interpolate, integrate
from matplotlib import pyplot
import numpy as np
import itertools
import fortranformat
import omas
import traceback

__all__ = [
    'read_basic_eq_from_mds',
    'from_mds_plus',
    'OMFIT_pcs_shape',
    'read_basic_eq_from_toksearch',
    'x_point_search',
    'x_point_quick_search',
    'gEQDSK_COCOS_identify',
]
for k in ['', 'a', 'g', 'k', 'm', 's']:
    __all__.append('OMFIT%seqdsk' % k)
__all__.extend(fluxSurface.__all__)

omas.omas_utils._structures = {}
omas.omas_utils._structures_dict = {}

############################
# auto CLASS OMFITeqdsk    #
############################
[docs]def OMFITeqdsk(filename, EFITtype=None, **kw): r""" Automatically determine the type of an EFIT file and parse it with the appropriate class. It is faster to just directly use the appropriate class. Using the right class also avoids problems because some files technically can be parsed with more than one class (no exceptions thrown), giving junk results. :param filename: string Name of the file on disk, including path :param EFITtype: string Letter giving the type of EFIT file, like 'g'. Should be in 'gamks'. If None, then the first letter in the filename is used to determine the file type If this is also not helping, then a brute-force load is attempted :param strict: bool Filename (not including path) must include the letter giving the file type. Prevents errors like using sEQDSK to parse g133221.01000, which might otherwise be possible. :param \**kw: Other keywords to pass to the class that is chosen. :return: OMFIT*eqdsk instance """ if EFITtype is None: EFITtype = os.path.split(filename)[1][0].lower() if EFITtype in ['g', 'e']: return OMFITgeqdsk(filename, **kw) elif EFITtype == 'a': return OMFITaeqdsk(filename, **kw) elif EFITtype in ['k', 'r', 'x']: return OMFITkeqdsk(filename, **kw) elif EFITtype == 'm': return OMFITmeqdsk(filename, **kw) elif EFITtype == 's': return OMFITseqdsk(filename, **kw) else: eqdsk_classes = SortedDict([['g', OMFITgeqdsk], ['a', OMFITaeqdsk], ['m', OMFITmeqdsk], ['k', OMFITkeqdsk], ['s', OMFITseqdsk]]) loaded = False exceptions = [] for EFITtype in eqdsk_classes: try: tmp = eqdsk_classes[EFITtype](filename, **kw) tmp.load() except Exception as _excp: exceptions += ['\n\nNot {}EQDSK:\n{}'.format(EFITtype, repr(_excp))] else: loaded = True tmp.close() break if not loaded: raise TypeError(''.join(exceptions)) printe( '''You have just loaded a `%s` file: %s The OMFITeqdsk wrapper is slower than just using the OMFIT%seqdsk For better performance, please adjust your code. OMFITeqdsk will be deprecated.''' % (EFITtype, os.path.split(filename)[1], EFITtype) ) return tmp
class XPointSearchFail(ValueError, DoNotReportException): """x_point_search failed""" class OMFITd3dfitweight(SortedDict, OMFITascii): """ OMFIT class to read DIII-D fitweight file """ def __init__(self, filename, use_leading_comma=None, **kw): r""" OMFIT class to parse DIII-D device files :param filename: filename :param \**kw: arguments passed to __init__ of OMFITascii """ OMFITascii.__init__(self, filename, **kw) SortedDict.__init__(self) self.dynaLoad = True @dynaLoad def load(self): self.clear() magpri67 = 29 magpri322 = 31 magprirdp = 8 magudom = 5 maglds = 3 nsilds = 3 nsilol = 41 with open(self.filename, 'r') as f: data = f.read() data = data.strip().split() for i in data: ifloat = float(i) if ifloat > 100: ishot = ifloat self[ifloat] = [] else: self[ishot].append(ifloat) for irshot in self: if irshot < 124985: mloop = nsilol else: mloop = nsilol + nsilds if irshot < 59350: mprobe = magpri67 elif irshot < 91000: mprobe = magpri67 + magpri322 elif irshot < 100771: mprobe = magpri67 + magpri322 + magprirdp elif irshot < 124985: mprobe = magpri67 + magpri322 + magprirdp + magudom else: mprobe = magpri67 + magpri322 + magprirdp + magudom + maglds fwtmp2 = self[irshot][mloop : mloop + mprobe] fwtsi = self[irshot][0:mloop] self[irshot] = {} self[irshot]['fwtmp2'] = fwtmp2 self[irshot]['fwtsi'] = fwtsi return self ############################ # G-FILE CLASS OMFITgeqdsk # ############################
[docs]class OMFITgeqdsk(SortedDict, OMFITascii): r""" class used to interface G files generated by EFIT :param filename: filename passed to OMFITascii class :param \**kw: keyword dictionary passed to OMFITascii class """ transform_signals = { 'SIMAG': 'PSI', 'SIBRY': 'PSI', 'BCENTR': 'BT', 'CURRENT': 'IP', 'FPOL': 'BT', 'FFPRIM': 'dPSI', 'PPRIME': 'dPSI', 'PSIRZ': 'PSI', 'QPSI': 'Q', } def __init__(self, filename, **kw): OMFITascii.__init__(self, filename, **kw) SortedDict.__init__(self, caseInsensitive=True) self._cocos = 1 self._AuxNamelistString = None self.dynaLoad = True def __getattr__(self, attr): try: return SortedDict.__getattr__(self, attr) except Exception: raise AttributeError('bad attribute `%s`' % attr)
[docs] def surface_integral(self, *args, **kw): """ Cross section integral of a quantity :param what: quantity to be integrated specified as array at flux surface :return: array of the integration from core to edge """ return self['fluxSurfaces'].surface_integral(*args, **kw)
[docs] def volume_integral(self, *args, **kw): """ Volume integral of a quantity :param what: quantity to be integrated specified as array at flux surface :return: array of the integration from core to edge """ return self['fluxSurfaces'].volume_integral(*args, **kw)
[docs] def surfAvg(self, Q, interp='linear'): """ Flux surface averaging of a quantity at each flux surface :param Q: 2D quantity to do the flux surface averaging (either 2D array or string from 'AuxQuantities', e.g. RHORZ) :param interp: interpolation method ['linear','quadratic','cubic'] :return: array of the quantity fluxs surface averaged for each flux surface >> OMFIT['test']=OMFITgeqdsk(OMFITsrc+"/../samples/g133221.01000") >> jpar=OMFIT['test'].surfAvg('Jpar') >> pyplot.plot(OMFIT['test']['rhovn'],jpar) """ Z = self['AuxQuantities']['Z'] R = self['AuxQuantities']['R'] if isinstance(Q, str): Q = self['AuxQuantities'][Q] if callable(Q): avg_function = Q else: def avg_function(r, z): return RectBivariateSplineNaN(Z, R, Q, kx=interp, ky=interp).ev(z, r) if interp == 'linear': interp = 1 elif interp == 'quadratic': interp = 2 elif interp == 'cubic': interp = 3 return self['fluxSurfaces'].surfAvg(avg_function)
@property @dynaLoad def cocos(self): """ Return COCOS of current gEQDSK as represented in memory """ if self._cocos is None: return self.native_cocos() return self._cocos @cocos.setter def cocos(self, value): raise OMFITexception("gEQDSK COCOS should not be defined via .cocos property: use .cocosify() method")
[docs] @dynaLoad def load(self, raw=False, add_aux=True): """ Method used to read g-files :param raw: bool load gEQDSK exactly as it's on file, regardless of COCOS :param add_aux: bool Add AuxQuantities and fluxSurfaces when using `raw` mode. When not raw, these will be loaded regardless. """ if self.filename is None or not os.stat(self.filename).st_size: return # todo should be rewritten using FortranRecordReader # based on w3.pppl.gov/ntcc/TORAY/G_EQDSK.pdf def splitter(inv, step=16): value = [] for k in range(len(inv) // step): value.append(inv[step * k : step * (k + 1)]) return value def merge(inv): if not len(inv): return '' if len(inv[0]) > 80: # SOLPS gEQDSK files add spaces between numbers # and positive numbers are preceeded by a + return (''.join(inv)).replace(' ', '') else: return ''.join(inv) self.clear() # clean lines from the carriage returns with open(self.filename, 'r') as f: EQDSK = f.read().splitlines() # first line is description and sizes self['CASE'] = np.array(splitter(EQDSK[0][0:48], 8)) try: tmp = list([_f for _f in EQDSK[0][48:].split(' ') if _f]) [IDUM, self['NW'], self['NH']] = list(map(int, tmp[:3])) except ValueError: # Can happen if no space between numbers, such as 10231023 IDUM = int(EQDSK[0][48:52]) self['NW'] = int(EQDSK[0][52:56]) self['NH'] = int(EQDSK[0][56:60]) tmp = [] printd('IDUM, NW, NH', IDUM, self['NW'], self['NH'], topic='OMFITgeqdsk.load') if len(tmp) > 3: self['EXTRA_HEADER'] = EQDSK[0][49 + len(re.findall('%d +%d +%d ' % (IDUM, self['NW'], self['NH']), EQDSK[0][49:])[0]) + 2 :] offset = 1 # now, the next 20 numbers (5 per row) # fmt: off [self['RDIM'], self['ZDIM'], self['RCENTR'], self['RLEFT'], self['ZMID'], self['RMAXIS'], self['ZMAXIS'], self['SIMAG'], self['SIBRY'], self['BCENTR'], self['CURRENT'], self['SIMAG'], XDUM, self['RMAXIS'], XDUM, self['ZMAXIS'], XDUM, self['SIBRY'], XDUM, XDUM] = list(map(eval, splitter(merge(EQDSK[offset:offset + 4])))) # fmt: on offset = offset + 4 # now I have to read NW elements nlNW = int(np.ceil(self['NW'] / 5.0)) self['FPOL'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNW]))))) offset = offset + nlNW self['PRES'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNW]))))) offset = offset + nlNW self['FFPRIM'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNW]))))) offset = offset + nlNW self['PPRIME'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNW]))))) offset = offset + nlNW try: # official gEQDSK file format saves PSIRZ as a single flat array of size rowsXcols nlNWNH = int(np.ceil(self['NW'] * self['NH'] / 5.0)) self['PSIRZ'] = np.reshape( np.fromiter(splitter(merge(EQDSK[offset : offset + nlNWNH])), dtype=np.float64)[: self['NH'] * self['NW']], (self['NH'], self['NW']), ) offset = offset + nlNWNH except ValueError: # sometimes gEQDSK files save row by row of the PSIRZ grid (eg. FIESTA code) nlNWNH = self['NH'] * nlNW self['PSIRZ'] = np.reshape( np.fromiter(splitter(merge(EQDSK[offset : offset + nlNWNH])), dtype=np.float64)[: self['NH'] * self['NW']], (self['NH'], self['NW']), ) offset = offset + nlNWNH self['QPSI'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNW]))))) offset = offset + nlNW # now vacuum vessel and limiters if len(EQDSK) > (offset + 1): self['NBBBS'], self['LIMITR'] = list(map(int, [_f for _f in EQDSK[offset : offset + 1][0].split(' ') if _f][:2])) offset += 1 nlNBBBS = int(np.ceil(self['NBBBS'] * 2 / 5.0)) self['RBBBS'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNBBBS]))))[0::2])[: self['NBBBS']] self['ZBBBS'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNBBBS]))))[1::2])[: self['NBBBS']] offset = offset + max(nlNBBBS, 1) try: # this try/except is to handle some gEQDSK files written by older versions of ONETWO nlLIMITR = int(np.ceil(self['LIMITR'] * 2 / 5.0)) self['RLIM'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlLIMITR]))))[0::2])[: self['LIMITR']] self['ZLIM'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlLIMITR]))))[1::2])[: self['LIMITR']] offset = offset + nlLIMITR except ValueError: # if it fails make the limiter as a rectangle around the plasma boundary that does not exceed the computational domain self['LIMITR'] = 5 dd = self['RDIM'] / 10.0 R = np.linspace(0, self['RDIM'], 2) + self['RLEFT'] Z = np.linspace(0, self['ZDIM'], 2) - self['ZDIM'] / 2.0 + self['ZMID'] self['RLIM'] = np.array( [ max([R[0], np.min(self['RBBBS']) - dd]), min([R[1], np.max(self['RBBBS']) + dd]), min([R[1], np.max(self['RBBBS']) + dd]), max([R[0], np.min(self['RBBBS']) - dd]), max([R[0], np.min(self['RBBBS']) - dd]), ] ) self['ZLIM'] = np.array( [ max([Z[0], np.min(self['ZBBBS']) - dd]), max([Z[0], np.min(self['ZBBBS']) - dd]), min([Z[1], np.max(self['ZBBBS']) + dd]), min([Z[1], np.max(self['ZBBBS']) + dd]), max([Z[0], np.min(self['ZBBBS']) - dd]), ] ) else: self['NBBBS'] = 0 self['LIMITR'] = 0 self['RBBBS'] = [] self['ZBBBS'] = [] self['RLIM'] = [] self['ZLIM'] = [] try: [self['KVTOR'], self['RVTOR'], self['NMASS']] = list(map(float, [_f for _f in EQDSK[offset : offset + 1][0].split(' ') if _f])) offset = offset + 1 if self['KVTOR'] > 0: self['PRESSW'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNW]))))) offset = offset + nlNW self['PWPRIM'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNW]))))) offset = offset + nlNW if self['NMASS'] > 0: self['DMION'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNW]))))) offset = offset + nlNW self['RHOVN'] = np.array(list(map(float, splitter(merge(EQDSK[offset : offset + nlNW]))))) offset = offset + nlNW self['KEECUR'] = int(EQDSK[offset]) offset = offset + 1 if self['KEECUR'] > 0: self['EPOTEN'] = np.array(splitter(merge(EQDSK[offset : offset + nlNW])), dtype=float) offset = offset + nlNW # This will only work when IPLCOUT==2, which is not available in older versions of EFIT self['PCURRT'] = np.reshape( np.fromiter(splitter(merge(EQDSK[offset : offset + nlNWNH])), dtype=np.float64)[: self['NH'] * self['NW']], (self['NH'], self['NW']), ) offset = offset + nlNWNH self['CJOR'] = np.array(splitter(merge(EQDSK[offset : offset + nlNW])), dtype=float) offset = offset + nlNW self['R1SURF'] = np.array(splitter(merge(EQDSK[offset : offset + nlNW])), dtype=float) offset = offset + nlNW self['R2SURF'] = np.array(splitter(merge(EQDSK[offset : offset + nlNW])), dtype=float) offset = offset + nlNW self['VOLP'] = np.array(splitter(merge(EQDSK[offset : offset + nlNW])), dtype=float) offset = offset + nlNW self['BPOLSS'] = np.array(splitter(merge(EQDSK[offset : offset + nlNW])), dtype=float) offset = offset + nlNW except Exception: pass # add RHOVN if missing if 'RHOVN' not in self or not len(self['RHOVN']) or not np.sum(self['RHOVN']): self.add_rhovn() # fix some gEQDSK files that do not fill PRES info (eg. EAST) if not np.sum(self['PRES']): pres = integrate.cumtrapz(self['PPRIME'], np.linspace(self['SIMAG'], self['SIBRY'], len(self['PPRIME'])), initial=0) self['PRES'] = pres - pres[-1] # parse auxiliary namelist self.addAuxNamelist() if raw and add_aux: # add AuxQuantities and fluxSurfaces self.addAuxQuantities() self.addFluxSurfaces(**self.OMFITproperties) elif not raw: # Convert tree representation to COCOS 1 self._cocos = self.native_cocos() self.cocosify(1, calcAuxQuantities=True, calcFluxSurfaces=True) self.add_geqdsk_documentation()
[docs] @dynaSave def save(self, raw=False): """ Method used to write g-files :param raw: save gEQDSK exactly as it's in the the tree, regardless of COCOS """ # Change gEQDSK to its native COCOS before saving if not raw: original = self.cocos native = self.native_cocos() self.cocosify(native, calcAuxQuantities=False, calcFluxSurfaces=False) try: XDUM = 0.0 IDUM = 0 f2000 = fortranformat.FortranRecordWriter('6a8,3i4') f2020 = fortranformat.FortranRecordWriter('5e16.9') f2020NaN = fortranformat.FortranRecordWriter('5a16') f2022 = fortranformat.FortranRecordWriter('2i5') f2024 = fortranformat.FortranRecordWriter('i5,e16.9,i5') f2026 = fortranformat.FortranRecordWriter('i5') tmps = f2000.write( [ self['CASE'][0], self['CASE'][1], self['CASE'][2], self['CASE'][3], self['CASE'][4], self['CASE'][5], IDUM, self['NW'], self['NH'], ] ) if 'EXTRA_HEADER' in self: tmps += ' ' + self['EXTRA_HEADER'] tmps += '\n' tmps += f2020.write([self['RDIM'], self['ZDIM'], self['RCENTR'], self['RLEFT'], self['ZMID']]) + '\n' tmps += f2020.write([self['RMAXIS'], self['ZMAXIS'], self['SIMAG'], self['SIBRY'], self['BCENTR']]) + '\n' tmps += f2020.write([self['CURRENT'], self['SIMAG'], XDUM, self['RMAXIS'], XDUM]) + '\n' tmps += f2020.write([self['ZMAXIS'], XDUM, self['SIBRY'], XDUM, XDUM]) + '\n' tmps += f2020.write(self['FPOL']) + '\n' tmps += f2020.write(self['PRES']) + '\n' tmps += f2020.write(self['FFPRIM']) + '\n' tmps += f2020.write(self['PPRIME']) + '\n' psirz = list(['%16.9e' % x for x in self['PSIRZ'].flatten()]) for p in range(4, int(self['NW'] * self['NH']) - 1, 5): psirz[p] = psirz[p] + '\n' tmps += ''.join(psirz) + '\n' tmps += f2020.write(self['QPSI']) + '\n' if 'NBBBS' not in self: self['NBBBS'] = len(self['RBBBS']) if 'LIMITR' not in self: self['LIMITR'] = len(self['RLIM']) tmps += f2022.write([self['NBBBS'], self['LIMITR']]) + '\n' tmps += f2020.write(list((np.transpose([self['RBBBS'], self['ZBBBS']])).flatten())) + '\n' tmps += f2020.write(list((np.transpose([self['RLIM'], self['ZLIM']])).flatten())) + '\n' if 'KVTOR' in self and 'RVTOR' in self and 'NMASS' in self: tmps += f2024.write([self['KVTOR'], self['RVTOR'], self['NMASS']]) + '\n' if self['KVTOR'] > 0 and 'PRESSW' in self and 'PWPRIM' in self: tmps += f2020.write(self['PRESSW']) + '\n' tmps += f2020.write(self['PWPRIM']) + '\n' if self['NMASS'] > 0 and 'DMION' in self: try: tmps += f2020.write(self['DMION']) + '\n' except Exception: tmps += f2020NaN.write(map(str, self['DMION'])) + '\n' if 'RHOVN' in self: tmps += f2020.write(self['RHOVN']) + '\n' if 'KEECUR' in self and 'EPOTEN' in self: tmps += f2026.write([self['KEECUR']]) + '\n' tmps += f2020.write(self['EPOTEN']) + '\n' else: tmps += ' 0\n' # This will only be available when IPLCOUT==2, which is not available in older versions of EFIT if 'PCURRT' in self: pcurrt = ['%16.9e' % x for x in self['PCURRT'].flatten()] for p in range(4, int(self['NW'] * self['NH']) - 1, 5): pcurrt[p] = pcurrt[p] + '\n' tmps += ''.join(pcurrt) + '\n' # write file with open(self.filename, 'w') as f: f.write(tmps) if 'AuxNamelist' in self: if self._AuxNamelistString is not None: f.write(self._AuxNamelistString) else: self['AuxNamelist'].save(f) finally: if not raw: # Return gEQDSK to the original COCOS self.cocosify(original, calcAuxQuantities=False, calcFluxSurfaces=False)
[docs] def cocosify(self, cocosnum, calcAuxQuantities, calcFluxSurfaces, inplace=True): """ Method used to convert gEQDSK quantities to desired COCOS :param cocosnum: desired COCOS number (1-8, 11-18) :param calcAuxQuantities: add AuxQuantities based on new cocosnum :param calcFluxSurfaces: add fluxSurfaces based on new cocosnum :param inplace: change values in True: current gEQDSK, False: new gEQDSK :return: gEQDSK with proper cocos """ if inplace: gEQDSK = self else: gEQDSK = copy.deepcopy(self) if self.cocos != cocosnum: # how different gEQDSK quantities should transform transform = cocos_transform(self.cocos, cocosnum) # transform the gEQDSK quantities appropriately for key in self: if key in list(self.transform_signals.keys()): gEQDSK[key] = transform[self.transform_signals[key]] * self[key] # set the COCOS attribute of the gEQDSK gEQDSK._cocos = cocosnum # recalculate AuxQuantities and fluxSurfaces if necessary if calcAuxQuantities: gEQDSK.addAuxQuantities() if calcFluxSurfaces: gEQDSK.addFluxSurfaces(**self.OMFITproperties) return gEQDSK
[docs] def native_cocos(self): """ Returns the native COCOS that an unmodified gEQDSK would obey, defined by sign(Bt) and sign(Ip) In order for psi to increase from axis to edge and for q to be positive: All use sigma_RpZ=+1 (phi is counterclockwise) and exp_Bp=0 (psi is flux/2.*pi) We want sign(psi_edge-psi_axis) = sign(Ip)*sigma_Bp > 0 (psi always increases in gEQDSK) sign(q) = sign(Ip)*sign(Bt)*sigma_rhotp > 0 (q always positive in gEQDSK) :: ============================================ Bt Ip sigma_Bp sigma_rhotp COCOS ============================================ +1 +1 +1 +1 1 +1 -1 -1 -1 3 -1 +1 +1 -1 5 -1 -1 -1 +1 7 """ try: return gEQDSK_COCOS_identify(self['BCENTR'], self['CURRENT']) except Exception as _excp: printe("Assuming COCOS=1: " + repr(_excp)) return 1
[docs] def flip_Bt_Ip(self): """ Flip direction of the magnetic field and current without changing COCOS """ cocosnum = self.cocos # artificially flip phi to the opposite direction if np.mod(cocosnum, 2) == 0: self._cocos -= 1 elif np.mod(cocosnum, 2) == 1: self._cocos += 1 # change back to original COCOS, flipping phi & all relevant quantities self.cocosify(cocosnum, calcAuxQuantities=True, calcFluxSurfaces=True)
[docs] def flip_ip(self): """ Flip sign of IP and related quantities without changing COCOS """ for key in self: if self.transform_signals.get(key, None) in ['PSI', 'IP', 'dPSI', 'Q']: self[key] = -self[key] self.addAuxQuantities() self.addFluxSurfaces(**self.OMFITproperties)
[docs] def flip_bt(self): """ Flip sign of BT and related quantities without changing COCOS """ for key in self: if self.transform_signals.get(key, None) in ['BT', 'Q']: self[key] = -self[key] self.addAuxQuantities() self.addFluxSurfaces(**self.OMFITproperties)
[docs] def bateman_scale(self, BCENTR=None, CURRENT=None): """ Scales toroidal field and current in such a way as to hold poloidal beta constant, keeping flux surface geometry unchanged - The psi, p', and FF' are all scaled by a constant factor to achieve the desired current - The edge F=R*Bt is changed to achieve the desired toroidal field w/o affecting FF' - Scaling of other quantities follow from this The result is a valid Grad-Shafranov equilibrium (if self is one) Based on the scaling from Bateman and Peng, PRL 38, 829 (1977) https://link.aps.org/doi/10.1103/PhysRevLett.38.829 """ if (BCENTR is None) and (CURRENT is None): return Fedge_0 = self['FPOL'][-1] if BCENTR is None: Fedge = Fedge_0 else: Fedge = BCENTR * self['RCENTR'] if CURRENT is None: sfactor = 1.0 else: sfactor = CURRENT / self['CURRENT'] FPOL_0 = copy.deepcopy(self['FPOL']) dF2_0 = FPOL_0**2 - FPOL_0[-1] ** 2 self['FPOL'] = np.sign(Fedge) * np.sqrt(Fedge**2 + sfactor**2 * dF2_0) self['FFPRIM'] *= sfactor self['BCENTR'] = Fedge / self['RCENTR'] self['PRES'] *= sfactor**2 self['PPRIME'] *= sfactor self['PSIRZ'] *= sfactor self['SIMAG'] *= sfactor self['SIBRY'] *= sfactor self['CURRENT'] *= sfactor self['QPSI'] *= self['FPOL'] / (FPOL_0 * sfactor) self.addAuxQuantities() self.addFluxSurfaces(**self.OMFITproperties) self['RHOVN'] = np.sqrt(self['AuxQuantities']['PHI'] / self['AuxQuantities']['PHI'][-1]) return
[docs] def combineGEQDSK(self, other, alpha): """ Method used to linearly combine current equilibrium (eq1) with other g-file All quantities are linearly combined, except 'RBBBS','ZBBBS','NBBBS','LIMITR','RLIM','ZLIM','NW','NH' OMFIT['eq3']=OMFIT['eq1'].combineGEQDSK(OMFIT['eq2'],alpha) means: eq3=alpha*eq1+(1-alpha)*eq2 :param other: g-file for eq2 :param alpha: linear combination parameter :return: g-file for eq3 """ out = copy.deepcopy(self) # gEQDSKs need to be in the same COCOS to combine if self.cocos != other.cocos: # change other to self's COCOS, but don't modify other eq2 = other.cocosify(self.cocos, calcAuxQuantities=True, calcFluxSurfaces=True, inplace=False) else: eq2 = other keys_self = set(self.keys()) keys_other = set(self.keys()) keys_ignore = set(['RBBBS', 'ZBBBS', 'NBBBS', 'LIMITR', 'RLIM', 'ZLIM', 'NW', 'NH']) keys = keys_self.intersection(keys_other).difference(keys_ignore) for key in keys: if is_numlike(self[key]) and is_numlike(eq2[key]): out[key] = alpha * self[key] + (1.0 - alpha) * eq2[key] # combine the separatrix t_self = np.arctan2(self['ZBBBS'] - self['ZMAXIS'], self['RBBBS'] - self['RMAXIS']) t_other = np.arctan2( eq2['ZBBBS'] - self['ZMAXIS'], eq2['RBBBS'] - self['RMAXIS'] ) # must be defined with respect to the same center for key in ['RBBBS', 'ZBBBS']: out[key] = alpha * self[key] + interp1dPeriodic(t_other, eq2[key])(t_self) * (1 - alpha) out.addAuxQuantities() out.addFluxSurfaces() return out
[docs] def addAuxNamelist(self): """ Adds ['AuxNamelist'] to the current object :return: Namelist object containing auxiliary quantities """ if self.filename is None or not os.stat(self.filename).st_size: self['AuxNamelist'] = namelist.NamelistFile(input_string='') return self['AuxNamelist'] self['AuxNamelist'] = namelist.NamelistFile(self.filename, nospaceIsComment=True, retain_comments=False, skip_to_symbol='&') self._AuxNamelistString = None tmp = self.read() self._AuxNamelistString = tmp[tmp.find('&') :] return self['AuxNamelist']
[docs] def delAuxNamelist(self): """ Removes ['AuxNamelist'] from the current object """ self._AuxNamelistString = None self.safe_del('AuxNamelist') return
[docs] def addAuxQuantities(self): """ Adds ['AuxQuantities'] to the current object :return: SortedDict object containing auxiliary quantities """ self['AuxQuantities'] = self._auxQuantities() return self['AuxQuantities']
[docs] def fourier(self, surface=1.0, nf=128, symmetric=True, resolution=2, **kw): r""" Reconstructs Fourier decomposition of the boundary for fixed boundary codes to use :param surface: Use this normalised flux surface for the boundary (if <0 then original gEQDSK BBBS boundary is used), else the flux surfaces are from FluxSurfaces. :param nf: number of Fourier modes :param symmetric: return symmetric boundary :param resolution: FluxSurfaces resolution factor :param \**kw: additional keyword arguments are passed to FluxSurfaces.findSurfaces """ if surface < 0: rb = self['RBBBS'] zb = self['ZBBBS'] else: flx = copy.deepcopy(self['fluxSurfaces']) kw.setdefault('map', None) flx.changeResolution(resolution) flx.findSurfaces(np.linspace(surface - 0.01, surface, 3), **kw) rb = flx['flux'][1]['R'] zb = flx['flux'][1]['Z'] bndfour = fourier_boundary(nf, rb, zb, symmetric=symmetric) fm = np.zeros(nf) if symmetric: fm = bndfour.realfour else: fm[0::2] = bndfour.realfour fm[1::2] = bndfour.imagfour amin = bndfour.amin r0 = bndfour.r0 return (bndfour, fm, amin, r0)
def _auxQuantities(self): """ Calculate auxiliary quantities based on the g-file equilibria These AuxQuantities obey the COCOS of self.cocos so some sign differences from the gEQDSK file itself :return: SortedDict object containing some auxiliary quantities """ aux = SortedDict() iterpolationType = 'linear' # note that interpolation should not be oscillatory -> use linear or pchip aux['R'] = np.linspace(0, self['RDIM'], self['NW']) + self['RLEFT'] aux['Z'] = np.linspace(0, self['ZDIM'], self['NH']) - self['ZDIM'] / 2.0 + self['ZMID'] if self['CURRENT'] != 0.0: # poloidal flux and normalized poloidal flux aux['PSI'] = np.linspace(self['SIMAG'], self['SIBRY'], len(self['PRES'])) aux['PSI_NORM'] = np.linspace(0.0, 1.0, len(self['PRES'])) aux['PSIRZ'] = self['PSIRZ'] if self['SIBRY'] != self['SIMAG']: aux['PSIRZ_NORM'] = abs((self['PSIRZ'] - self['SIMAG']) / (self['SIBRY'] - self['SIMAG'])) else: aux['PSIRZ_NORM'] = abs(self['PSIRZ'] - self['SIMAG']) # rho poloidal aux['RHOp'] = np.sqrt(aux['PSI_NORM']) aux['RHOpRZ'] = np.sqrt(aux['PSIRZ_NORM']) # extend functions in PSI to be clamped at edge value when outside of PSI range (i.e. outside of LCFS) dp = aux['PSI'][1] - aux['PSI'][0] ext_psi_mesh = np.hstack((aux['PSI'][0] - dp * 1e6, aux['PSI'], aux['PSI'][-1] + dp * 1e6)) def ext_arr(inv): return np.hstack((inv[0], inv, inv[-1])) # map functions in PSI to RZ coordinate for name in ['FPOL', 'PRES', 'QPSI', 'FFPRIM', 'PPRIME', 'PRESSW', 'PWPRIM']: if name in self and len(self[name]): aux[name + 'RZ'] = interpolate.interp1d(ext_psi_mesh, ext_arr(self[name]), kind=iterpolationType, bounds_error=False)( aux['PSIRZ'] ) # Correct Pressure by rotation term (eq 26 & 30 of Lao et al., FST 48.2 (2005): 968-977. aux['PRES0RZ'] = copy.deepcopy(aux['PRESRZ']) if 'PRESSW' in self: aux['PRES0RZ'] = copy.deepcopy(aux['PRESRZ']) aux['PPRIME0RZ'] = PP0 = copy.deepcopy(aux['PPRIMERZ']) R = aux['R'][None, :] R0 = self['RCENTR'] Pw = aux['PRESSWRZ'] P0 = aux['PRES0RZ'] aux['PRESRZ'] = P = P0 * np.exp(Pw / P0 * (R - R0) / R0) PPw = aux['PWPRIMRZ'] aux['PPRIMERZ'] = PP0 * P / P0 * (1.0 - Pw / P0 * (R**2 - R0**2) / R0**2) aux['PPRIMERZ'] += PPw * P / P0 * (R**2 - R0**2) / R0**2 else: # vacuum gEQDSK aux['PSIRZ'] = self['PSIRZ'] # from the definition of flux COCOS = define_cocos(self.cocos) if (aux['Z'][1] != aux['Z'][0]) and (aux['R'][1] != aux['R'][0]): [dPSIdZ, dPSIdR] = np.gradient(aux['PSIRZ'], aux['Z'][1] - aux['Z'][0], aux['R'][1] - aux['R'][0]) else: [dPSIdZ, dPSIdR] = np.gradient(aux['PSIRZ']) [R, Z] = np.meshgrid(aux['R'], aux['Z']) aux['Br'] = (dPSIdZ / R) * COCOS['sigma_RpZ'] * COCOS['sigma_Bp'] / (2.0 * np.pi) ** COCOS['exp_Bp'] aux['Bz'] = (-dPSIdR / R) * COCOS['sigma_RpZ'] * COCOS['sigma_Bp'] / (2.0 * np.pi) ** COCOS['exp_Bp'] if self['CURRENT'] != 0.0: signTheta = COCOS['sigma_RpZ'] * COCOS['sigma_rhotp'] # + CW, - CCW signBp = signTheta * np.sign((Z - self['ZMAXIS']) * aux['Br'] - (R - self['RMAXIS']) * aux['Bz']) # sign(theta)*sign(r x B) aux['Bp'] = signBp * np.sqrt(aux['Br'] ** 2 + aux['Bz'] ** 2) # once I have the poloidal flux as a function of RZ I can calculate the toroidal field (showing DIA/PARAmagnetism) aux['Bt'] = aux['FPOLRZ'] / R else: aux['Bt'] = self['BCENTR'] * self['RCENTR'] / R # now the current densities as curl B = mu0 J in cylindrical coords if (aux['Z'][2] != aux['Z'][1]) and (aux['R'][2] != aux['R'][1]): [dBrdZ, dBrdR] = np.gradient(aux['Br'], aux['Z'][2] - aux['Z'][1], aux['R'][2] - aux['R'][1]) [dBzdZ, dBzdR] = np.gradient(aux['Bz'], aux['Z'][2] - aux['Z'][1], aux['R'][2] - aux['R'][1]) [dBtdZ, dBtdR] = np.gradient(aux['Bt'], aux['Z'][2] - aux['Z'][1], aux['R'][2] - aux['R'][1]) [dRBtdZ, dRBtdR] = np.gradient(R * aux['Bt'], aux['Z'][2] - aux['Z'][1], aux['R'][2] - aux['R'][1]) else: [dBrdZ, dBrdR] = np.gradient(aux['Br']) [dBzdZ, dBzdR] = np.gradient(aux['Bz']) [dBtdZ, dBtdR] = np.gradient(aux['Bt']) [dRBtdZ, dRBtdR] = np.gradient(R * aux['Bt']) aux['Jr'] = COCOS['sigma_RpZ'] * (-dBtdZ) / (4 * np.pi * 1e-7) aux['Jz'] = COCOS['sigma_RpZ'] * (dRBtdR / R) / (4 * np.pi * 1e-7) if 'PCURRT' in self: aux['Jt'] = self['PCURRT'] else: aux['Jt'] = COCOS['sigma_RpZ'] * (dBrdZ - dBzdR) / (4 * np.pi * 1e-7) if self['CURRENT'] != 0.0: signJp = signTheta * np.sign((Z - self['ZMAXIS']) * aux['Jr'] - (R - self['RMAXIS']) * aux['Jz']) # sign(theta)*sign(r x J) aux['Jp'] = signJp * np.sqrt(aux['Jr'] ** 2 + aux['Jz'] ** 2) aux['Jt_fb'] = ( -COCOS['sigma_Bp'] * ((2.0 * np.pi) ** COCOS['exp_Bp']) * (aux['PPRIMERZ'] * R + aux['FFPRIMRZ'] / R / (4 * np.pi * 1e-7)) ) aux['Jpar'] = (aux['Jr'] * aux['Br'] + aux['Jz'] * aux['Bz'] + aux['Jt'] * aux['Bt']) / np.sqrt( aux['Br'] ** 2 + aux['Bz'] ** 2 + aux['Bt'] ** 2 ) # The toroidal flux PHI can be found by recognizing that the safety factor is the ratio of the differential toroidal and poloidal fluxes if 'QPSI' in self and len(self['QPSI']): aux['PHI'] = ( COCOS['sigma_Bp'] * COCOS['sigma_rhotp'] * integrate.cumtrapz(self['QPSI'], aux['PSI'], initial=0) * (2.0 * np.pi) ** (1.0 - COCOS['exp_Bp']) ) if aux['PHI'][-1] != 0 and np.isfinite(aux['PHI'][-1]): aux['PHI_NORM'] = aux['PHI'] / aux['PHI'][-1] else: aux['PHI_NORM'] = aux['PHI'] * np.NaN printw('Warning: unable to properly normalize PHI') if abs(np.diff(aux['PSI'])).min() > 0: aux['PHIRZ'] = interpolate.interp1d( aux['PSI'], aux['PHI'], kind=iterpolationType, bounds_error=False, fill_value='extrapolate' )(aux['PSIRZ']) else: aux['PHIRZ'] = aux['PSIRZ'] * np.NaN if self['BCENTR'] != 0: aux['RHOm'] = float(np.sqrt(abs(aux['PHI'][-1] / np.pi / self['BCENTR']))) else: aux['RHOm'] = np.NaN aux['RHO'] = np.sqrt(aux['PHI_NORM']) with np.errstate(invalid='ignore'): aux['RHORZ'] = np.nan_to_num(np.sqrt(aux['PHIRZ'] / aux['PHI'][-1])) aux['Rx1'], aux['Zx1'] = x_point_search(aux['R'], aux['Z'], self['PSIRZ'], psi_boundary=self['SIBRY']) aux['Rx2'], aux['Zx2'] = x_point_search(aux['R'], aux['Z'], self['PSIRZ'], zsign=-np.sign(aux['Zx1'])) return aux
[docs] def addFluxSurfaces(self, **kw): r""" Adds ['fluxSurface'] to the current object :param \**kw: keyword dictionary passed to fluxSurfaces class :return: fluxSurfaces object based on the current gEQDSK file """ if self['CURRENT'] == 0.0: printw('Skipped tracing of fluxSurfaces for vacuum equilibrium') return options = {} options.update(kw) options['quiet'] = kw.pop('quiet', self['NW'] <= 129) options['levels'] = kw.pop('levels', True) options['resolution'] = kw.pop('resolution', 0) options['calculateAvgGeo'] = kw.pop('calculateAvgGeo', True) # N.B., the middle option accounts for the new version of CHEASE # where self['CASE'][1] = 'OM CHEAS' if ( self['CASE'] is not None and self['CASE'][0] is not None and self['CASE'][1] is not None and ('CHEASE' in self['CASE'][0] or 'CHEAS' in self['CASE'][1] or 'TRXPL' in self['CASE'][0]) ): options['forceFindSeparatrix'] = kw.pop('forceFindSeparatrix', False) else: options['forceFindSeparatrix'] = kw.pop('forceFindSeparatrix', True) try: self['fluxSurfaces'] = fluxSurfaces(gEQDSK=self, **options) except Exception as _excp: warnings.warn('Error tracing flux surfaces: ' + repr(_excp)) self['fluxSurfaces'] = OMFITerror('Error tracing flux surfaces: ' + repr(_excp)) return self['fluxSurfaces']
[docs] def calc_masks(self): """ Calculate grid masks for limiters, vessel, core and edge plasma :return: SortedDict object with 2D maps of masks """ import matplotlib if 'AuxQuantities' not in self: aux = self._auxQuantities() else: aux = self['AuxQuantities'] [R, Z] = np.meshgrid(aux['R'], aux['Z']) masks = SortedDict() # masking limiter_path = matplotlib.path.Path(np.transpose(np.array([self['RLIM'], self['ZLIM']]))) masks['limiter_mask'] = 1 - np.reshape( np.array(list(map(limiter_path.contains_point, list(map(tuple, np.transpose(np.array([R.flatten(), Z.flatten()]))))))), (self['NW'], self['NH']), ) masks['vessel_mask'] = 1 - masks['limiter_mask'] plasma_path = matplotlib.path.Path(np.transpose(np.array([self['RBBBS'], self['ZBBBS']]))) masks['core_plasma_mask'] = np.reshape( np.array(list(map(plasma_path.contains_point, list(map(tuple, np.transpose(np.array([R.flatten(), Z.flatten()]))))))), (self['NW'], self['NH']), ) masks['edge_plasma_mask'] = (1 - masks['limiter_mask']) - masks['core_plasma_mask'] for vname in [_f for _f in [re.findall(r'.*masks', value) for value in list(aux.keys())] if _f]: aux[vname[0]] = np.array(aux[vname[0]], float) aux[vname[0]][aux[vname[0]] == 0] = np.nan return masks
[docs] def plot( self, usePsi=False, only1D=False, only2D=False, top2D=False, q_contour_n=0, label_contours=False, levels=None, mask_vessel=True, show_limiter=True, xlabel_in_legend=False, useRhop=False, **kw, ): r""" Function used to plot g-files. This plot shows flux surfaces in the vessel, pressure, q profiles, P' and FF' :param usePsi: In the plots, use psi instead of rho, or both :param only1D: only make plofile plots :param only2D: only make flux surface plot :param top2D: Plot top-view 2D cross section :param q_contour_n: If above 0, plot q contours in 2D plot corresponding to rational surfaces of the given n :param label_contours: Adds labels to 2D contours :param levels: list of sorted numeric values to pass to 2D plot as contour levels :param mask_vessel: mask contours with vessel :param show_limiter: Plot the limiter outline in (R,Z) 2D plots :param xlabel_in_legend: Show x coordinate in legend instead of under axes (usefull for overplots with psi and rho) :param label: plot item label to apply lines in 1D plots (only the q plot has legend called by the geqdsk class itself) and to the boundary contour in the 2D plot (this plot doesn't call legend by itself) :param ax: Axes instance to plot in when using only2D :param \**kw: Standard plot keywords (e.g. color, linewidth) will be passed to Axes.plot() calls. """ import matplotlib # backward compatibility: remove deprecated kw (not used anywhere in repo) garbage = kw.pop('contour_smooth', None) if sum(self['RHOVN']) == 0.0: usePsi = True def plot2D(what, ax, levels=levels, Z_in=None, **kw): if levels is None: if what in ['PHIRZ_NORM', 'RHOpRZ', 'RHORZ', 'PSIRZ_NORM']: levels = np.r_[0.1:10:0.1] label_levels = levels[:9] elif what in ['QPSIRZ']: q1 = self['QPSI'][-2] # go one in because edge can be jagged in contour and go outside seperatrix q0 = self['QPSI'][0] qsign = np.sign(q0) # q profile can be negative depending on helicity levels = np.arange(np.ceil(qsign * q0), np.floor(qsign * q1), 1.0 / int(q_contour_n))[:: int(qsign)] * qsign label_levels = levels else: levels = np.linspace(np.nanmin(self['AuxQuantities'][what]), np.nanmax(self['AuxQuantities'][what]), 20) label_levels = levels else: label_levels = levels label = kw.pop('label', None) # Take this out so the legend doesn't get spammed by repeated labels # use this to set up the plot key word args, get the next line color, and move the color cycler along (l,) = ax.plot(self['AuxQuantities']['R'], self['AuxQuantities']['R'] * np.nan, **kw) # contours cs = ax.contour( self['AuxQuantities']['R'], self['AuxQuantities']['Z'], self['AuxQuantities'][what], levels, colors=[l.get_color()] * len(levels), linewidths=l.get_linewidth(), alpha=l.get_alpha(), linestyles=l.get_linestyle(), ) # optional labeling of contours if label_contours: label_step = max(len(label_levels) // 4, 1) ax.clabel(cs, label_levels[::label_step], inline=True, fontsize=8, fmt='%1.1f') # optional masking of contours outside of limiter surface if len(self['RLIM']) > 2 and mask_vessel and not np.any(np.isnan(self['RLIM'])) and not np.any(np.isnan(self['ZLIM'])): path = matplotlib.path.Path(np.transpose(np.array([self['RLIM'], self['ZLIM']]))) patch = matplotlib.patches.PathPatch(path, facecolor='none') ax.add_patch(patch) for col in cs.collections: col.set_clip_path(patch) # get the color kw1 = copy.copy(kw) kw1['linewidth'] = kw['linewidth'] + 1 kw1.setdefault('color', ax.lines[-1].get_color()) # boundary ax.plot(self['RBBBS'], self['ZBBBS'], label=label, **kw1) # magnetic axis ax.plot(self['RMAXIS'], self['ZMAXIS'], '+', **kw1) # limiter if len(self['RLIM']) > 2: if show_limiter: ax.plot(self['RLIM'], self['ZLIM'], 'k', linewidth=2) try: ax.axis([np.nanmin(self['RLIM']), np.nanmax(self['RLIM']), np.nanmin(self['ZLIM']), np.nanmax(self['ZLIM'])]) except ValueError: pass # aspect_ratio ax.set_aspect('equal') def plot2DTop(what, ax, levels=levels, Z_in=None, **kw): # If z_in is specified then plot a vertical slice else plot the outer and innermost R value of each flux surface if levels is None: if what in ['PHIRZ_NORM', 'RHOpRZ', 'RHORZ', 'PSIRZ_NORM']: levels = np.r_[0.1:10:0.1] elif what in ['PHIRZ', 'PSIRZ']: levels = np.linspace(np.nanmin(self['AuxQuantities'][what]), np.nanmax(self['AuxQuantities'][what]), 20) else: raise ValueError(what + " is not supported for top view plot.") # use this to set up the plot key word args, get the next line color, and move the color cycler along (l,) = ax.plot(self['AuxQuantities']['R'], self['AuxQuantities']['R'] * np.nan, **kw) if Z_in is None: # Plots the outer and inner most points of a flux surface in topview what_sort = np.argsort(self['AuxQuantities'][what.replace("RZ", "")]) psi_map = interpolate.interp1d( self['AuxQuantities'][what.replace("RZ", "")][what_sort], self['AuxQuantities']['PSI'][what_sort] ) psi_levels = [] for level in levels: if (level > np.min(self['AuxQuantities'][what.replace("RZ", "")])) and level < np.max( self['AuxQuantities'][what.replace("RZ", "")] ): psi_levels.append(psi_map(level)) psi_levels = np.asarray(psi_levels) psi_surf = np.zeros(len(self['fluxSurfaces']["flux"]) + 1) R_in = np.zeros(len(self['fluxSurfaces']["flux"]) + 1) R_out = np.zeros(len(self['fluxSurfaces']["flux"]) + 1) psi_surf[0] = self["SIMAG"] R_in[0] = self["RMAXIS"] R_out[0] = R_in[0] for iflux in range(len(self['fluxSurfaces']["flux"])): psi_surf[iflux + 1] = self['fluxSurfaces']["flux"][iflux]["psi"] R_in[iflux + 1] = np.max(self['fluxSurfaces']["flux"][iflux]["R"]) R_out[iflux + 1] = np.min(self['fluxSurfaces']["flux"][iflux]["R"]) # In case of decreasing flux psi_sort = np.argsort(psi_surf) R_in_spl = interpolate.InterpolatedUnivariateSpline(psi_surf[psi_sort], R_in[psi_sort]) R_out_spl = interpolate.InterpolatedUnivariateSpline(psi_surf[psi_sort], R_out[psi_sort]) R_cont = R_in_spl(psi_levels) R_cont = np.sort(np.concatenate([R_cont, R_out_spl(psi_levels)])) # Boundary optional masking of contours outside of limiter surface and plotting boundary R_max = np.max(R_cont) R_min = np.min(R_cont) if len(self['RLIM']) > 2 and not np.any(np.isnan(self['RLIM'])): R_vessel_in = np.min(self['RLIM']) R_vessel_out = np.max(self['RLIM']) if mask_vessel: R_max = R_vessel_out R_min = R_vessel_in ax.add_patch(matplotlib.patches.Circle([0.0, 0.0], R_vessel_in, edgecolor='k', facecolor='none', linestyle="-")) ax.add_patch(matplotlib.patches.Circle([0.0, 0.0], R_vessel_out, edgecolor='k', facecolor='none', linestyle="-")) for R in R_cont: if R >= R_min and R <= R_max: ax.add_patch( matplotlib.patches.Circle( [0.0, 0.0], R, edgecolor=l.get_color(), linewidth=l.get_linewidth(), linestyle=l.get_linestyle(), facecolor='none', ) ) ax.add_patch(matplotlib.patches.Circle([0.0, 0.0], self["RMAXIS"], edgecolor='b', facecolor='none', linestyle="-")) if self["SIBRY"] < np.max(psi_surf): R_sep_in = R_in_spl(self["SIBRY"]) R_sep_out = R_out_spl(self["SIBRY"]) ax.add_patch(matplotlib.patches.Circle([0.0, 0.0], R_sep_in, edgecolor='b', facecolor='none', linestyle="-")) ax.add_patch(matplotlib.patches.Circle([0.0, 0.0], R_sep_out, edgecolor='b', facecolor='none', linestyle="-")) else: # Plots the R and z values of the wall and the choosen magnetic coordiante for a specific z level what_spl = interpolate.RectBivariateSpline( self['AuxQuantities']['R'], self['AuxQuantities']['Z'], self['AuxQuantities'][what].T ) R_cut = np.linspace(np.min(self['AuxQuantities']['R']), np.max(self['AuxQuantities']['R']), self["NW"]) Z_cut = np.zeros(self["NW"]) Z_cut[:] = Z_in what_cut = what_spl(R_cut, Z_cut, grid=False) R_max = -np.inf for level in levels: root_spl = interpolate.InterpolatedUnivariateSpline(R_cut, what_cut - level) roots = root_spl.roots() if len(roots) == 2: if np.max(roots) > R_max: R_max = np.max(roots) if level == 1.0: ax.add_patch( matplotlib.patches.Circle([0.0, 0.0], np.min(roots), edgecolor='b', facecolor='none', linestyle="-") ) ax.add_patch( matplotlib.patches.Circle([0.0, 0.0], np.max(roots), edgecolor='b', facecolor='none', linestyle="-") ) else: ax.add_patch( matplotlib.patches.Circle( [0.0, 0.0], np.min(roots), edgecolor=l.get_color(), linewidth=l.get_linewidth(), linestyle=l.get_linestyle(), facecolor='none', ) ) ax.add_patch( matplotlib.patches.Circle( [0.0, 0.0], np.max(roots), edgecolor=l.get_color(), linewidth=l.get_linewidth(), linestyle=l.get_linestyle(), facecolor='none', ) ) s_wall = np.linspace(0, 1, len(self["RLIM"])) wall_R_spl = interpolate.InterpolatedUnivariateSpline(s_wall, self["RLIM"]) wall_Z_root_spl = interpolate.InterpolatedUnivariateSpline(s_wall, self["ZLIM"] - Z_in) wall_roots = wall_Z_root_spl.roots() if len(wall_roots) < 2: printw("WARNING in OMFITgeqdsk.plot2DTop: Did not find intersection with wall!") else: if np.max(wall_R_spl(wall_roots)) > R_max: R_max = np.max(wall_R_spl(wall_roots)) ax.add_patch( matplotlib.patches.Circle( [0.0, 0.0], np.min(wall_R_spl(wall_roots)), edgecolor='k', facecolor='none', linestyle="-" ) ) ax.add_patch( matplotlib.patches.Circle( [0.0, 0.0], np.max(wall_R_spl(wall_roots)), edgecolor='k', facecolor='none', linestyle="-" ) ) ax.set_aspect('equal') ax.set_xlim(-R_max, R_max) ax.set_ylim(-R_max, R_max) kw.setdefault('linewidth', 1) if not only2D: fig = pyplot.gcf() kw.pop('ax', None) # This option can't be used in this context, so remove it to avoid trouble. pyplot.subplots_adjust(wspace=0.23) if usePsi: xName = '$\\psi$' x = np.linspace(0, 1, len(self['PRES'])) elif useRhop: xName = '$\\rho_\\mathrm{pol}$' x = self['AuxQuantities']['RHOp'] else: xName = '$\\rho$' if 'RHOVN' in self and np.sum(self['RHOVN']): x = self['RHOVN'] else: x = self['AuxQuantities']['RHO'] if 'label' not in kw: kw['label'] = (' '.join([a.strip() for a in self['CASE'][3:]])).strip() if not len(kw['label']): kw['label'] = (' '.join([a.strip() for a in self['CASE']])).strip() if not len(kw['label']): kw['label'] = os.path.split(self.filename)[1] if xlabel_in_legend: kw['label'] += ' vs ' + xName ax = pyplot.subplot(232) ax.plot(x, self['PRES'], **kw) kw.setdefault('color', ax.lines[-1].get_color()) ax.set_title(r'$\,$ Pressure') ax.ticklabel_format(style='sci', scilimits=(-1, 2), axis='y') pyplot.setp(ax.get_xticklabels(), visible=False) ax = pyplot.subplot(233, sharex=ax) ax.plot(x, self['QPSI'], **kw) ax.set_title('$q$ Safety Factor') ax.ticklabel_format(style='sci', scilimits=(-1, 2), axis='y') try: ax.legend(labelspacing=0.2, loc=0).draggable(state=True) except Exception: pass pyplot.setp(ax.get_xticklabels(), visible=False) ax = pyplot.subplot(235, sharex=ax) ax.plot(x, self['PPRIME'], **kw) ax.set_title(r"$P\,^\prime$ Source") ax.ticklabel_format(style='sci', scilimits=(-1, 2), axis='y') ax.set_xlabel((not xlabel_in_legend) * xName) ax = pyplot.subplot(236, sharex=ax) ax.plot(x, self['FFPRIM'], **kw) ax.set_title(r"$FF\,^\prime$ Source") ax.ticklabel_format(style='sci', scilimits=(-1, 2), axis='y') ax.set_xlabel((not xlabel_in_legend) * xName) ax = pyplot.subplot(131, aspect='equal') ax.set_frame_on(False) ax.xaxis.set_ticks_position('bottom') ax.yaxis.set_ticks_position('left') else: if 'ax' not in kw: ax = pyplot.gca() else: ax = kw.pop('ax') if not only1D: if usePsi: if "PSIRZ_NORM" in self['AuxQuantities']: what = 'PSIRZ_NORM' else: what = 'PSIRZ' elif q_contour_n > 0: what = 'QPSIRZ' elif useRhop: what = 'RHOpRZ' else: what = 'RHORZ' if top2D: plot2DTop(what, ax, **kw) else: plot2D(what, ax, **kw)
[docs] def get2D(self, Q, r, z, interp='linear'): """ Function to retrieve 2D quantity at coordinates :param Q: Quantity to be retrieved (either 2D array or string from 'AuxQuantities', e.g. RHORZ) :param r: r coordinate for retrieval :param z: z coordinate for retrieval :param interp: interpolation method ['linear','quadratic','cubic'] >> OMFIT['test']=OMFITgeqdsk(OMFITsrc+"/../samples/g133221.01000") >> r=np.linspace(min(OMFIT['test']['RBBBS']),max(OMFIT['test']['RBBBS']),100) >> z=r*0 >> tmp=OMFIT['test'].get2D('Br',r,z) >> pyplot.plot(r,tmp) """ Z = self['AuxQuantities']['Z'] R = self['AuxQuantities']['R'] if isinstance(Q, str): Q = self['AuxQuantities'][Q] if interp == 'linear': interp = 1 elif interp == 'quadratic': interp = 2 elif interp == 'cubic': interp = 3 return np.reshape(RectBivariateSplineNaN(Z, R, Q, kx=interp, ky=interp).ev(z.flatten(), r.flatten()), r.size)
[docs] def map2D(self, x, y, X, interp='linear', maskName='core_plasma_mask', outsideOfMask=np.nan): """ Function to map 1D quantity to 2D grid :param x: abscissa of 1D quantity :param y: 1D quantity :param X: 2D distribution of 1D quantity abscissa :param interp: interpolation method ['linear','cubic'] :param maskName: one among `limiter_mask`, `vessel_mask`, `core_plasma_mask`, `edge_plasma_mask` or None :param outsideOfMask: value to use outside of the mask """ dp = x[1] - x[0] Y = interp1e(x, y, kind=interp)(X) if maskName is not None: mask = self.calc_masks()[maskName] Y *= mask Y[np.where(mask <= 0)] = outsideOfMask return Y
[docs] def calc_pprime_ffprim(self, press=None, pprime=None, Jt=None, Jt_over_R=None, fpol=None): """ This method returns the P' and FF' given P or P' and J or J/R based on the current equilibrium fluxsurfaces geometry :param press: pressure :param pprime: pressure*pressure' :param Jt: toroidal current :param Jt_over_R: flux surface averaged toroidal current density over major radius :param fpol: F :return: P', FF' """ COCOS = define_cocos(self.cocos) if press is not None: pprime = deriv(np.linspace(self['SIMAG'], self['SIBRY'], len(press)), press) if fpol is not None: ffprim = deriv(np.linspace(self['SIMAG'], self['SIBRY'], len(press)), fpol) * fpol if Jt is not None: ffprim = Jt * COCOS['sigma_Bp'] / (2.0 * np.pi) ** COCOS['exp_Bp'] + pprime * self['fluxSurfaces']['avg']['R'] ffprim *= -4 * np.pi * 1e-7 / (self['fluxSurfaces']['avg']['1/R']) elif Jt_over_R is not None: ffprim = Jt_over_R * COCOS['sigma_Bp'] / (2.0 * np.pi) ** COCOS['exp_Bp'] + pprime ffprim *= -4 * np.pi * 1e-7 / self['fluxSurfaces']['avg']['1/R**2'] return pprime, ffprim
[docs] def calc_Ip(self, Jt_over_R=None): """ This method returns the toroidal current within the flux surfaces based on the current equilibrium fluxsurfaces geometry :param Jt_over_R: flux surface averaged toroidal current density over major radius :return: Ip """ if Jt_over_R is None: Jt_over_R = self['fluxSurfaces']['avg']['Jt/R'] return integrate.cumtrapz(self['fluxSurfaces']['avg']['vp'] * Jt_over_R, self['fluxSurfaces']['geo']['psi'], initial=0) / ( 2.0 * np.pi )
[docs] def add_rhovn(self): """ Calculate RHOVN from PSI and `q` profile """ # add RHOVN if QPSI is non-zero (ie. vacuum gEQDSK) if np.sum(np.abs(self['QPSI'])): phi = integrate.cumtrapz(self['QPSI'], np.linspace(self['SIMAG'], self['SIBRY'], len(self['QPSI'])), initial=0) # only needed if the dimensions of phi are wanted # self['RHOVN'] = np.sqrt(np.abs(2 * np.pi * phi / (np.pi * self['BCENTR']))) self['RHOVN'] = np.sqrt(np.abs(phi)) if np.nanmax(self['RHOVN']) > 0: self['RHOVN'] = self['RHOVN'] / np.nanmax(self['RHOVN']) else: # if no QPSI information, then set RHOVN to zeros self['RHOVN'] = self['QPSI'] * 0.0
[docs] def case_info(self): """ Interprets the CASE field of the GEQDSK and converts it into a dictionary :return: dict Contains as many values as can be determined. Fills in None when the correct value cannot be determined. device shot time (within shot) date (of code execution) efitid (aka snap file or tree name) code_version """ device = None shot = None time = None date = None efitid = None code_version = None # Make a list of substrings that should be contained by each field of CASE for each form. # Form 1: CASE is a 6 element list containing code_version, month/day, /year, #shot, time, efitid caseform_contains = {1: ['', '/', '/', '#', 'ms', '']} caseform = None possible_forms = [] # Go through each known form and test whether it could apply for caseform_, contains in caseform_contains.items(): if (len(self['CASE']) == len(contains)) and all(c in self['CASE'][i] for i, c in enumerate(contains)): possible_forms += [caseform_] if len(possible_forms) == 1: caseform = possible_forms[0] else: printe('More than one form of CASE could be valid.') # Assign info based on which form CASE takes. if caseform == 1: device = None shot = int(self['CASE'][3].split('#')[1].strip()) time = float(self['CASE'][4].split('ms')[0].strip()) year = int(self['CASE'][2].split('/')[1].strip()) month, day = self['CASE'][1].split('/') date = datetime.datetime(year=year, month=int(month), day=int(day)) efitid = self['CASE'][5].strip() code_version = self['CASE'][0].strip() return dict(device=device, shot=shot, time=time, date=date, efitid=efitid, code_version=code_version)
[docs] @dynaLoad def to_omas(self, ods=None, time_index=0, allow_derived_data=True): """ translate gEQDSK class to OMAS data structure :param ods: input ods to which data is added :param time_index: time index to which data is added :param allow_derived_data: bool Allow data to be drawn from fluxSurfaces, AuxQuantities, etc. May trigger dynamic loading. :return: ODS """ if ods is None: ods = ODS() if self.cocos is None: cocosio = self.native_cocos() # assume native gEQDSK COCOS else: cocosio = self.cocos # delete time_slice before writing, since these quantities all need to be consistent if 'equilibrium.time_slice.%d' % time_index in ods: ods['equilibrium.time_slice.%d' % time_index] = ODS() # write derived quantities from fluxSurfaces if self['CURRENT'] != 0.0: flx = self['fluxSurfaces'] ods = flx.to_omas(ods, time_index=time_index) eqt = ods[f'equilibrium.time_slice.{time_index}'] # align psi grid psi = np.linspace(self['SIMAG'], self['SIBRY'], len(self['PRES'])) if f'equilibrium.time_slice.{time_index}.profiles_1d.psi' in ods: with omas_environment(ods, cocosio=cocosio): m0 = psi[0] M0 = psi[-1] m1 = eqt['profiles_1d.psi'][0] M1 = eqt['profiles_1d.psi'][-1] psi = (psi - m0) / (M0 - m0) * (M1 - m1) + m1 coordsio = {f'equilibrium.time_slice.{time_index}.profiles_1d.psi': psi} # add gEQDSK quantities with omas_environment(ods, cocosio=cocosio, coordsio=coordsio): try: ods['dataset_description.data_entry.pulse'] = int( re.sub('[a-zA-Z]([0-9]+).([0-9]+).*', r'\1', os.path.split(self.filename)[1]) ) except Exception: ods['dataset_description.data_entry.pulse'] = 0 try: separator = '' ods['equilibrium.ids_properties.comment'] = self['CASE'][0] except Exception: ods['equilibrium.ids_properties.comment'] = 'omasEQ' try: # TODO: this removes any sub ms time info and should be fixed eqt['time'] = float(re.sub('[a-zA-Z]([0-9]+).([0-9]+).*', r'\2', os.path.split(self.filename)[1])) / 1000.0 except Exception: eqt['time'] = 0.0 # ********************* # ESSENTIAL # ********************* if 'RHOVN' in self: # EAST gEQDSKs from MDSplus do not always have RHOVN defined rhovn = self['RHOVN'] else: printd('RHOVN is missing from top level geqdsk, so falling back to RHO from AuxQuantities', topic='OMFITgeqdsk') rhovn = self['AuxQuantities']['RHO'] # ============0D eqt['global_quantities.magnetic_axis.r'] = self['RMAXIS'] eqt['global_quantities.magnetic_axis.z'] = self['ZMAXIS'] eqt['global_quantities.psi_axis'] = self['SIMAG'] eqt['global_quantities.psi_boundary'] = self['SIBRY'] eqt['global_quantities.ip'] = self['CURRENT'] # ============0D time dependent vacuum_toroidal_field ods['equilibrium.vacuum_toroidal_field.r0'] = self['RCENTR'] ods.set_time_array('equilibrium.vacuum_toroidal_field.b0', time_index, self['BCENTR']) # ============1D eqt['profiles_1d.f'] = self['FPOL'] eqt['profiles_1d.pressure'] = self['PRES'] eqt['profiles_1d.f_df_dpsi'] = self['FFPRIM'] eqt['profiles_1d.dpressure_dpsi'] = self['PPRIME'] eqt['profiles_1d.q'] = self['QPSI'] eqt['profiles_1d.rho_tor_norm'] = rhovn # ============2D eqt['profiles_2d.0.grid_type.index'] = 1 eqt['profiles_2d.0.grid.dim1'] = np.linspace(0, self['RDIM'], self['NW']) + self['RLEFT'] eqt['profiles_2d.0.grid.dim2'] = np.linspace(0, self['ZDIM'], self['NH']) - self['ZDIM'] / 2.0 + self['ZMID'] eqt['profiles_2d.0.psi'] = self['PSIRZ'].T if 'PCURRT' in self: eqt['profiles_2d.0.j_tor'] = self['PCURRT'].T # ********************* # DERIVED # ********************* if self['CURRENT'] != 0.0: # ============0D eqt['global_quantities.magnetic_axis.b_field_tor'] = self['BCENTR'] * self['RCENTR'] / self['RMAXIS'] eqt['global_quantities.q_axis'] = self['QPSI'][0] eqt['global_quantities.q_95'] = interpolate.interp1d(np.linspace(0.0, 1.0, len(self['QPSI'])), self['QPSI'])(0.95) eqt['global_quantities.q_min.value'] = self['QPSI'][np.argmin(abs(self['QPSI']))] eqt['global_quantities.q_min.rho_tor_norm'] = rhovn[np.argmin(abs(self['QPSI']))] # ============1D Psi1D = np.linspace(self['SIMAG'], self['SIBRY'], len(self['FPOL'])) # eqt['profiles_1d.psi'] = Psi1D #no need bacause of coordsio eqt['profiles_1d.phi'] = self['AuxQuantities']['PHI'] eqt['profiles_1d.rho_tor'] = rhovn * self['AuxQuantities']['RHOm'] # ============2D eqt['profiles_2d.0.b_field_r'] = self['AuxQuantities']['Br'].T eqt['profiles_2d.0.b_field_tor'] = self['AuxQuantities']['Bt'].T eqt['profiles_2d.0.b_field_z'] = self['AuxQuantities']['Bz'].T eqt['profiles_2d.0.phi'] = (interp1e(Psi1D, self['AuxQuantities']['PHI'])(self['PSIRZ'])).T if self['CURRENT'] != 0.0: # These quantities don't require COCOS or coordinate transformation eqt['boundary.outline.r'] = self['RBBBS'] eqt['boundary.outline.z'] = self['ZBBBS'] if allow_derived_data and 'Rx1' in self['AuxQuantities'] and 'Zx1' in self['AuxQuantities']: eqt['boundary.x_point.0.r'] = self['AuxQuantities']['Rx1'] eqt['boundary.x_point.0.z'] = self['AuxQuantities']['Zx1'] if allow_derived_data and 'Rx2' in self['AuxQuantities'] and 'Zx2' in self['AuxQuantities']: eqt['boundary.x_point.1.r'] = self['AuxQuantities']['Rx2'] eqt['boundary.x_point.1.z'] = self['AuxQuantities']['Zx2'] # Set the time array ods.set_time_array('equilibrium.time', time_index, eqt['time']) # ============WALL ods['wall.description_2d.0.limiter.type.name'] = 'first_wall' ods['wall.description_2d.0.limiter.type.index'] = 0 ods['wall.description_2d.0.limiter.type.description'] = 'first wall' ods['wall.description_2d.0.limiter.unit.0.outline.r'] = self['RLIM'] ods['wall.description_2d.0.limiter.unit.0.outline.z'] = self['ZLIM'] # Set the time array (yes... also for the wall) ods.set_time_array('wall.time', time_index, eqt['time']) # Set reconstucted current (not yet in m-files) ods['equilibrium.time_slice'][time_index]['constraints']['ip.reconstructed'] = self['CURRENT'] # Store auxiliary namelists code_parameters = ods['equilibrium.code.parameters'] if 'time_slice' not in code_parameters: code_parameters['time_slice'] = ODS() if time_index not in code_parameters['time_slice']: code_parameters['time_slice'][time_index] = ODS() if 'AuxNamelist' in self: for items in self['AuxNamelist']: if '__comment' not in items: # probably not needed code_parameters['time_slice'][time_index][items.lower()] = ODS() for item in self['AuxNamelist'][items]: code_parameters['time_slice'][time_index][items.lower()][item.lower()] = self['AuxNamelist'][items.upper()][ item.upper() ] return ods
[docs] def from_omas(self, ods, time_index=0, profiles_2d_index=0, time=None): """ translate OMAS data structure to gEQDSK :param time_index: time index to extract data from :param profiles_2d_index: index of profiles_2d to extract data from :param time: time in seconds where to extract the data (if set it superseeds time_index) :return: self """ cocosio = 1 # from OMAS always makes a gEQDSK in COCOS 1 COCOS = define_cocos(cocosio) # handle shot and time try: shot = int(ods['dataset_description.data_entry.pulse']) except Exception: try: tmp = re.match('g([0-9]+).([0-9]+)', os.path.basename(self.filename)) shot = int(tmp.groups()[0]) except Exception: shot = 0 if time is not None: time_index = np.argmin(np.abs(ods['equilibrium.time'] - time)) time = int(np.round(ods['equilibrium.time'][time_index] * 1000)) eqt = ods[f'equilibrium.time_slice.{time_index}'] # setup coordinates with omas_environment(ods, cocosio=cocosio): psi = np.linspace( eqt['profiles_1d.psi'][0], eqt['profiles_1d.psi'][-1], eqt[f'profiles_2d.{profiles_2d_index}.grid.dim1'].size, ) coordsio = {f'equilibrium.time_slice.{time_index}.profiles_1d.psi': psi} # assign data in gEQDSK class with omas_environment(ods, cocosio=cocosio, coordsio=coordsio): R = eqt[f'profiles_2d.{profiles_2d_index}.grid.dim1'] Z = eqt[f'profiles_2d.{profiles_2d_index}.grid.dim2'] # ============0D today = datetime.datetime.now().strftime(' %d/%m_/%Y ').split('_') self['CASE'] = [ods.get('equilibrium.ids_properties.comment', ' EFITD ')] + today + [' #%6d' % shot, ' %dms' % time, ' omas'] self['NW'] = eqt[f'profiles_2d.{profiles_2d_index}.grid.dim1'].size self['NH'] = eqt[f'profiles_2d.{profiles_2d_index}.grid.dim2'].size self['RDIM'] = max(R) - min(R) self['ZDIM'] = max(Z) - min(Z) self['RLEFT'] = min(R) self['ZMID'] = (max(Z) + min(Z)) / 2.0 self['RMAXIS'] = eqt['global_quantities.magnetic_axis.r'] self['ZMAXIS'] = eqt['global_quantities.magnetic_axis.z'] if 'equilibrium.vacuum_toroidal_field.b0' in ods: self['RCENTR'] = ods['equilibrium.vacuum_toroidal_field.r0'] self['BCENTR'] = ods['equilibrium.vacuum_toroidal_field.b0'][time_index] else: self['RCENTR'] = (max(R) + min(R)) / 2.0 Baxis = eqt['global_quantities.magnetic_axis.b_field_tor'] self['BCENTR'] = Baxis * self['RMAXIS'] / self['RCENTR'] self['CURRENT'] = eqt['global_quantities.ip'] self['SIMAG'] = eqt['global_quantities.psi_axis'] self['SIBRY'] = eqt['global_quantities.psi_boundary'] self['KVTOR'] = 0.0 self['RVTOR'] = self['RCENTR'] self['NMASS'] = 0.0 # ============1D self['FPOL'] = eqt['profiles_1d.f'] self['PRES'] = eqt['profiles_1d.pressure'] self['FFPRIM'] = eqt['profiles_1d.f_df_dpsi'] self['PPRIME'] = eqt['profiles_1d.dpressure_dpsi'] self['QPSI'] = eqt['profiles_1d.q'] if 'profiles_1d.rho_tor_norm' in eqt: self['RHOVN'] = eqt['profiles_1d.rho_tor_norm'] elif 'profiles_1d.rho_tor' in eqt: rho = eqt['profiles_1d.rho_tor'] self['RHOVN'] = rho / np.max(rho) else: if 'profiles_1d.phi' in eqt: phi = eqt['profiles_1d.phi'] elif 'profiles_1d.q' in eqt: phi = integrate.cumtrapz( eqt['profiles_1d.q'], eqt['profiles_1d.psi'], initial=0, ) phi *= COCOS['sigma_Bp'] * COCOS['sigma_rhotp'] * (2.0 * np.pi) ** (1.0 - COCOS['exp_Bp']) self['RHOVN'] = np.sqrt(phi / phi[-1]) # ============2D self['PSIRZ'] = eqt[f'profiles_2d.{profiles_2d_index}.psi'].T if f'profiles_2d.{profiles_2d_index}.j_tor' in eqt: self['PCURRT'] = eqt[f'profiles_2d.{profiles_2d_index}.j_tor'].T # These quantities don't require COCOS or coordinate transformation self['RBBBS'] = eqt['boundary.outline.r'] self['ZBBBS'] = eqt['boundary.outline.z'] self['NBBBS'] = len(self['RBBBS']) # ============WALL self['RLIM'] = ods['wall.description_2d.0.limiter.unit.0.outline.r'] self['ZLIM'] = ods['wall.description_2d.0.limiter.unit.0.outline.z'] self['LIMITR'] = len(self['RLIM']) self.addAuxNamelist() # cocosify to have AuxQuantities and fluxSurfaces creater properly self._cocos = cocosio self.cocosify(cocosio, calcAuxQuantities=True, calcFluxSurfaces=True) # automatically set gEQDSK filename if self.filename was None if self.filename is None: self.filename = OMFITobject('g%06d.%05d' % (shot, time)).filename self.dynaLoad = False return self
[docs] def resample(self, nw_new): """ Change gEQDSK resolution NOTE: This method operates in place :param nw_new: new grid resolution :return: self """ old1d = np.linspace(0, 1, len(self['PRES'])) old2dw = np.linspace(0, 1, self['NW']) old2dh = np.linspace(0, 1, self['NH']) new = np.linspace(0, 1, nw_new) for item in list(self.keys()): if item in ['PSIRZ', 'PCURRT']: self[item] = RectBivariateSplineNaN(old2dh, old2dw, self[item])(new, new) elif isinstance(self[item], np.ndarray) and self[item].size == len(old1d): self[item] = interpolate.interp1d(old1d, self[item], kind=3)(new) self['NW'] = nw_new self['NH'] = nw_new if 'AuxQuantities' in self: self.addAuxQuantities() if 'fluxSurfaces' in self: self.addFluxSurfaces(**self.OMFITproperties) return self
[docs] def downsample_limiter(self, max_lim=None, in_place=True): """ Downsample the limiter :param max_lim: If max_lim is specified and the number of limiter points - before downsampling is smaller than max_lim, then no downsampling is performed after downsampling is larger than max_lim, then an error is raised :param in_place: modify this object in place or not :return: downsampled rlim and zlim arrays """ from omfit_classes.utils_math import simplify_polygon if 'LIMITR' not in self: raise KeyError('LIMITR: Limiter does not exist for this geqdsk') rlim, zlim = self['RLIM'], self['ZLIM'] if max_lim and self['LIMITR'] <= max_lim: printd('Not downsampling number of limiter points', topic='omfit_geqdsk') return rlim, zlim printd('Downsampling number of limiter points', topic='omfit_geqdsk') printd('- Started with %d' % self['LIMITR'], topic='omfit_geqdsk') tolerance = simplify_polygon(rlim, zlim, tolerance=None) max_tolerance = np.sqrt((np.max(rlim) - np.min(rlim)) ** 2 + (np.max(zlim) - np.min(zlim)) ** 2) nlim = len(rlim) it = 0 while nlim > 3: it += 1 if it > 1000: raise RuntimeError('Too many interations downsampling limiter') rlim, zlim = simplify_polygon(self['RLIM'], self['ZLIM'], tolerance=tolerance) if max_lim is None: tolerance = simplify_polygon(rlim, zlim, tolerance=None) else: tolerance = tolerance * 2.0 if max_lim is None and len(rlim) >= nlim: break elif max_lim is not None and len(rlim) <= max_lim: break elif tolerance >= max_tolerance: break nlim = len(rlim) nlim = len(rlim) if max_lim and nlim > max_lim: raise RuntimeError('After downsampling limiter has too many points: %d' % self['LIMITR']) if in_place: self['RLIM'] = rlim self['ZLIM'] = zlim self['LIMITR'] = nlim printd('- Ended with %d' % nlim, topic='omfit_geqdsk') return rlim, zlim
[docs] def downsample_boundary(self, max_bnd=None, in_place=True): """ Downsample the boundary :param max_bnd: If max_bnd is specified and the number of boundary points - before downsampling is smaller than max_bnd, then no downsampling is performed - after downsampling is larger than max_bnd, then an error is raised :param in_place: modify this object in place or not :return: downsampled rbnd and zbnd arrays """ from omfit_classes.utils_math import simplify_polygon rbnd, zbnd = self['RBBBS'], self['ZBBBS'] if max_bnd and self['NBBBS'] <= max_bnd: printd('Not downsampling number of boundary points', topic='omfit_geqdsk') return rbnd, zbnd printd('Downsampling number of boundary points', topic='omfit_geqdsk') printd('- Started with %d' % self['NBBBS'], topic='omfit_geqdsk') tolerance = simplify_polygon(rbnd, zbnd, tolerance=None) max_tolerance = np.sqrt((np.max(rbnd) - np.min(rbnd)) ** 2 + (np.max(zbnd) - np.min(zbnd)) ** 2) nbnd = len(rbnd) it = 0 while nbnd > 3: it += 1 if it > 1000: raise RuntimeError('Too many interations downsampling boundary') rbnd, zbnd = simplify_polygon(self['RBBBS'], self['ZBBBS'], tolerance=tolerance) if max_bnd is None: tolerance = simplify_polygon(rbnd, zbnd, tolerance=None) else: tolerance = tolerance * 2.0 if max_bnd is None and len(rbnd) >= nbnd: break elif max_bnd is not None and len(rbnd) <= max_bnd: break elif tolerance >= max_tolerance: break nbnd = len(rbnd) nbnd = len(rbnd) if max_bnd and nbnd > max_bnd: raise RuntimeError('After downsampling boundary has too many points: %d' % self['NBBBS']) if in_place: self['RBBBS'] = rbnd self['ZBBBS'] = zbnd self['NBBBS'] = nbnd printd('- Ended with %d' % nbnd, topic='omfit_geqdsk') return rbnd, zbnd
[docs] def from_mdsplus( self, device=None, shot=None, time=None, exact=False, SNAPfile='EFIT01', time_diff_warning_threshold=10, fail_if_out_of_range=True, show_missing_data_warnings=None, quiet=False, ): """ Fill in gEQDSK data from MDSplus :param device: The tokamak that the data correspond to ('DIII-D', 'NSTX', etc.) :param shot: Shot number from which to read data :param time: time slice from which to read data :param exact: get data from the exact time-slice :param SNAPfile: A string containing the name of the MDSplus tree to connect to, like 'EFIT01', 'EFIT02', 'EFIT03', ... :param time_diff_warning_threshold: raise error/warning if closest time slice is beyond this treshold :param fail_if_out_of_range: Raise error or warn if closest time slice is beyond time_diff_warning_threshold :param show_missing_data_warnings: Print warnings for missing data 1 or True: yes, print the warnings 2 or 'once': print only unique warnings; no repeats for the same quantities missing from many time slices 0 or False: printd instead of printw None: select based on device. Most will chose 'once'. :param quiet: verbosity :return: self """ if device is None: raise ValueError('Must specify device') if shot is None: raise ValueError('Must specify shot') if time is None: raise ValueError('Must specify time') tmp = from_mds_plus( device=device, shot=shot, times=[time], exact=exact, snap_file=SNAPfile, time_diff_warning_threshold=time_diff_warning_threshold, fail_if_out_of_range=fail_if_out_of_range, get_afile=False, show_missing_data_warnings=show_missing_data_warnings, debug=False, quiet=quiet, )['gEQDSK'][time] self.__dict__ = tmp.__dict__ self.update(tmp) return self
[docs] def from_rz(self, r, z, psival, p, f, q, B0, R0, ip, resolution, shot=0, time=0, RBFkw={}): """ Generate gEQDSK file from r, z points :param r: 2D array with R coordinates with 1st dimension being the flux surface index and the second theta :param z: 2D array with Z coordinates with 1st dimension being the flux surface index and the second theta :param psival: 1D array with psi values :param p: 1D array with pressure values :param f: 1D array with fpoloidal values :param q: 1D array with safety factor values :param B0: scalar vacuum B toroidal at R0 :param R0: scalar R where B0 is defined :param ip: toroidal current :param resolution: g-file grid resolution :param shot: used to set g-file string :param time: used to set g-file string :param RBFkw: keywords passed to internal Rbf interpolator :return: self """ from scipy.interpolate import Rbf # a minuscule amount of smoothing prevents numerical issues RBFkw.setdefault('smooth', 1e-6) # define gEQDSK grid rg = np.linspace(np.min(r) - 0.2, np.max(r) + 0.2, resolution) zg = np.linspace(np.min(z) - 0.2, np.max(z) + 0.2, resolution) RG, ZG = np.meshgrid(rg, zg) # pick out the separatrix values rbbbs = r[-1, :] zbbbs = z[-1, :] # RBF does not need a regular grid. # we random sample the r,z grid points to limit the number of points taken based on the requested resolution # this is necessary because Rbf scales very poorly with number of input samples psi = np.array([psival] * r.shape[1]).T r0 = [] z0 = [] psi0 = [] if (np.sum(np.abs(r[0, :] - r[0, 0])) + np.sum(np.abs(z[0, :] - z[0, 0]))) < 1e-6: raxis = r[0, 0] zaxis = z[0, 0] r0 = [r[0, 0]] z0 = [z[0, 0]] psi0 = [psi[0, 0]] r = r[1:, :] z = z[1:, :] psi = psi[1:, :] else: raxis = np.mean(r[0, :]) zaxis = np.mean(z[0, :]) r = np.hstack((r0, r.flatten())) z = np.hstack((z0, z.flatten())) psi = np.hstack((psi0, psi.flatten())) index = list(range(len(psi))) np.random.shuffle(index) index = index[: int(resolution**2 // 2)] # heuristic choice to pick the max number of points used in the reconstruction # interpolate to EFIT grid PSI = Rbf(r[index], z[index], psi[index], **RBFkw)(RG, ZG) # case today = datetime.datetime.now().strftime(' %d/%m_/%Y ').split('_') self['CASE'] = [' EFITD '] + today + [' #%6d' % shot, ' %dms' % time, 'rz_2_g'] # scalars self['NW'] = resolution self['NH'] = resolution self['RDIM'] = max(rg) - min(rg) self['ZDIM'] = max(zg) - min(zg) self['RLEFT'] = min(rg) self['ZMID'] = (max(zg) + min(zg)) / 2.0 self['RCENTR'] = R0 self['BCENTR'] = B0 self['CURRENT'] = ip self['RMAXIS'] = raxis self['ZMAXIS'] = zaxis self['SIMAG'] = np.min(psival) self['SIBRY'] = np.max(psival) # 1d quantiites psibase = np.linspace(self['SIMAG'], self['SIBRY'], self['NW']) self['PRES'] = interpolate.interp1d(psival, p)(psibase) self['QPSI'] = interpolate.interp1d(psival, q)(psibase) self['FPOL'] = interpolate.interp1d(psival, f)(psibase) self['FFPRIM'] = self['FPOL'] * np.gradient(self['FPOL'], psibase) self['PPRIME'] = np.gradient(self['PRES'], psibase) # 2d quantities self['PSIRZ'] = PSI # square limiter self['RLIM'] = np.array([min(rg) + 0.1, max(rg) - 0.1, max(rg) - 0.1, min(rg) + 0.1, min(rg) + 0.1]) self['ZLIM'] = np.array([min(zg) + 0.1, min(zg) + 0.1, max(zg) - 0.1, max(zg) - 0.1, min(zg) + 0.1]) self['LIMITR'] = 5 # lcfs self['RBBBS'] = rbbbs self['ZBBBS'] = zbbbs self['NBBBS'] = len(self['ZBBBS']) # add extras self.add_rhovn() self.addAuxQuantities() self.addFluxSurfaces() return self
[docs] def from_uda(self, shot=99999, time=0.0, pfx='efm', device='MAST'): """ Read in data from Unified Data Access (UDA) :param shot: shot number to read in :param time: time to read in data :param pfx: UDA data source prefix e.g. pfx+'_psi' :param device: tokamak name """ self.status = False try: import pyuda except Exception: raise ImportError("No UDA module found, cannot load MAST shot") client = pyuda.Client() if shot > 43000: if pfx == 'efm': pfx = 'epm' self.from_uda_mastu(shot=shot, time=time, device='MAST', pfx=pfx) return self try: _psi = client.get(pfx + "_psi(r,z)", shot) except pyuda.ProtocolException: printw("Please deselect Fetch in parallel") return _r = client.get(pfx + "_grid(r)", shot) _z = client.get(pfx + "_grid(z)", shot) _psi_axis = client.get(pfx + "_psi_axis", shot) _psi_bnd = client.get(pfx + "_psi_boundary", shot) _rcent = client.get(pfx + "_bvac_R", shot) _ipmhd = client.get(pfx + "_plasma_curr(C)", shot) _bphi = client.get(pfx + "_bvac_val", shot) _axisr = client.get(pfx + "_magnetic_axis_r", shot) _axisz = client.get(pfx + "_magnetic_axis_z", shot) _fpol = client.get(pfx + "_f(psi)_(c)", shot) _ppres = client.get(pfx + "_p(psi)_(c)", shot) _ffprime = client.get(pfx + "_ffprime", shot) _pprime = client.get(pfx + "_pprime", shot) _qprof = client.get(pfx + "_q(psi)_(c)", shot) _nbbbs = client.get(pfx + "_lcfs(n)_(c)", shot) _rbbbs = client.get(pfx + "_lcfs(r)_(c)", shot) _zbbbs = client.get(pfx + "_lcfs(z)_(c)", shot) _rlim = client.get(pfx + "_limiter(r)", shot) _zlim = client.get(pfx + "_limiter(z)", shot) tind = np.abs(_psi.time.data - time).argmin() _time = _psi.time.data[tind] tind_ax = np.abs(_psi_axis.time.data - time).argmin() tind_bnd = np.abs(_psi_bnd.time.data - time).argmin() tind_Bt = np.abs(_bphi.time.data - time).argmin() tind_sigBp = np.abs(_ipmhd.time.data - time).argmin() tind_xpt = np.abs(_axisr.time.data - time).argmin() tind_qpf = np.abs(_qprof.time.data - time).argmin() # case self['CASE'] = ['EFIT++ ', device, ' #%6d' % shot, ' #%4dms' % int(time * 1000), ' ', ' '] # scalars self['NW'] = len(_r.data[0, :]) self['NH'] = len(_z.data[0, :]) self['RDIM'] = max(_r.data[0, :]) - min(_r.data[0, :]) self['ZDIM'] = max(_z.data[0, :]) - min(_z.data[0, :]) self['RLEFT'] = min(_r.data[0, :]) self['ZMID'] = (max(_z.data[0, :]) + min(_z.data[0, :])) / 2.0 self['RCENTR'] = _rcent.data[tind_Bt] self['BCENTR'] = _bphi.data[tind_Bt] self['CURRENT'] = _ipmhd.data[tind_sigBp] self['RMAXIS'] = _axisr.data[tind_xpt] self['ZMAXIS'] = _axisz.data[tind_xpt] self['SIMAG'] = _psi_axis.data[tind_ax] self['SIBRY'] = _psi_bnd.data[tind_bnd] # 1d quantiites self['PRES'] = _ppres.data[tind_qpf, :] self['QPSI'] = _qprof.data[tind_qpf, :] self['FPOL'] = _fpol.data[tind_qpf, :] self['FFPRIM'] = _ffprime.data[tind_qpf, :] self['PPRIME'] = _pprime.data[tind_qpf, :] # 2d quantities self['PSIRZ'] = _psi.data[tind, :, :] # limiter self['RLIM'] = _rlim.data[0, :] self['ZLIM'] = _zlim.data[0, :] self['LIMITR'] = len(_rlim.data[0, :]) # lcfs nbbbs = _nbbbs.data[tind_qpf] self['RBBBS'] = _rbbbs.data[tind_qpf, :nbbbs] self['ZBBBS'] = _zbbbs.data[tind_qpf, :nbbbs] self['NBBBS'] = nbbbs # cocosify to have AuxQuantities and fluxSurfaces created properly self._cocos = 3 self.cocosify(1, calcAuxQuantities=True, calcFluxSurfaces=True) self.add_rhovn() self.status = True return self
[docs] def from_uda_mastu(self, shot=99999, time=0.0, device='MAST', pfx='epm'): """ Read in data from Unified Data Access (UDA) for MAST-U :param shot: shot number to read in :param time: time to read in data :param device: tokamak name :param pfx: equilibrium type """ self.status = False try: import pyuda except Exception: raise ImportError("No UDA module found, cannot load MAST shot") pfx = pfx.upper() client = pyuda.Client() _psi = client.get(f'/{pfx}/OUTPUT/PROFILES2D/POLOIDALFLUX', shot) _r = client.get(f'/{pfx}/OUTPUT/PROFILES2D/R', shot) _z = client.get(f'/{pfx}/OUTPUT/PROFILES2D/Z', shot) _bVac = client.get(f'/{pfx}/INPUT/BVACRADIUSPRODUCT', shot) _ppres = client.get(f'/{pfx}/OUTPUT/FLUXFUNCTIONPROFILES/STATICPRESSURE', shot) _qprof = client.get(f'/{pfx}/OUTPUT/FLUXFUNCTIONPROFILES/Q', shot) _ffprime = client.get(f'/{pfx}/OUTPUT/FLUXFUNCTIONPROFILES/FFPRIME', shot) _pprime = client.get(f'/{pfx}/OUTPUT/FLUXFUNCTIONPROFILES/STATICPPRIME', shot) _fpol = client.get(f'/{pfx}/OUTPUT/FLUXFUNCTIONPROFILES/RBPHI', shot) _psipr = client.get(f'/{pfx}/OUTPUT/FLUXFUNCTIONPROFILES/NORMALIZEDPOLOIDALFLUX', shot) _psi_axis = client.get(f'/{pfx}/OUTPUT/GLOBALPARAMETERS/PSIAXIS', shot) _psi_bnd = client.get(f'/{pfx}/OUTPUT/GLOBALPARAMETERS/PSIBOUNDARY', shot) _ipmhd = client.get(f'/{pfx}/OUTPUT/GLOBALPARAMETERS/PLASMACURRENT', shot) _axisr = client.get(f'/{pfx}/OUTPUT/GLOBALPARAMETERS/MAGNETICAXIS/R', shot) _axisz = client.get(f'/{pfx}/OUTPUT/GLOBALPARAMETERS/MAGNETICAXIS/Z', shot) _rlim = client.get(f'/{pfx}/INPUT/LIMITER/RVALUES', shot) _zlim = client.get(f'/{pfx}/INPUT/LIMITER/ZVALUES', shot) _rbbbs = client.get(f'/{pfx}/OUTPUT/SEPARATRIXGEOMETRY/RBOUNDARY', shot) _zbbbs = client.get(f'/{pfx}/OUTPUT/SEPARATRIXGEOMETRY/ZBOUNDARY', shot) tind = np.abs(_psi.time.data - time).argmin() _time = _psi.time.data[tind] tind_ax = np.abs(_psi_axis.time.data - time).argmin() tind_bnd = np.abs(_psi_bnd.time.data - time).argmin() tind_Bt = np.abs(_bVac.time.data - time).argmin() tind_sigBp = np.abs(_ipmhd.time.data - time).argmin() tind_xpt = np.abs(_axisr.time.data - time).argmin() tind_qpf = np.abs(_qprof.time.data - time).argmin() # Define global parameters device_name = device if is_device(device, 'MAST') and shot > 40000: device_name = 'MASTU' specs = utils_fusion.device_specs(device=device_name) # case self['CASE'] = ['EFIT++ ', device, ' #%6d' % shot, ' #%4dms' % int(time * 1000), ' ', ' '] # scalars self['NW'] = len(_r.data) self['NH'] = len(_z.data) self['RDIM'] = max(_r.data) - min(_r.data) self['ZDIM'] = max(_z.data) - min(_z.data) self['RLEFT'] = min(_r.data) self['ZMID'] = (max(_z.data) + min(_z.data)) / 2.0 self['RCENTR'] = specs['R0'] self['BCENTR'] = _bVac.data[tind_Bt] / specs['R0'] self['CURRENT'] = _ipmhd.data[tind_sigBp] self['RMAXIS'] = _axisr.data[tind_xpt] self['ZMAXIS'] = _axisz.data[tind_xpt] self['SIMAG'] = _psi_axis.data[tind_ax] self['SIBRY'] = _psi_bnd.data[tind_bnd] # 1d quantiites self['PRES'] = _ppres.data[tind_qpf, :] self['QPSI'] = _qprof.data[tind_qpf, :] self['FPOL'] = _fpol.data[tind_qpf, :] self['FFPRIM'] = _ffprime.data[tind_qpf, :] self['PPRIME'] = _pprime.data[tind_qpf, :] # 2d quantities self['PSIRZ'] = np.transpose(_psi.data[tind, :, :]) # limiter self['RLIM'] = _rlim.data self['ZLIM'] = _zlim.data self['LIMITR'] = len(_rlim.data) # lcfs nbbbs = np.shape(_rbbbs.data)[-1] self['RBBBS'] = _rbbbs.data[tind_qpf, :nbbbs] self['ZBBBS'] = _zbbbs.data[tind_qpf, :nbbbs] self['NBBBS'] = nbbbs # cocosify to have AuxQuantities and fluxSurfaces created properly self._cocos = 7 self.cocosify(1, calcAuxQuantities=True, calcFluxSurfaces=True) self.add_rhovn() self.status = True return self
[docs] def from_ppf(self, shot=99999, time=0.0, dda='EFIT', uid='jetppf', seq=0, device='JET'): """ Read in data from JET PPF :param shot: shot number to read in :param time: time to read in data :param dda: Equilibrium source diagnostic data area :param uid: Equilibrium source user ID :param seq: Equilibrium source sequence number """ self.status = False try: _times = np.squeeze(OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/PSI/' + str(seq), shot=shot).dim_of(1)) except KeyError: raise OMFITexception("Data does not exist for DDA: {0}, UID: {1}, SEQ: {2}".format(dda, uid, seq)) _r = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/PSIR/' + str(seq), shot=shot).data() _z = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/PSIZ/' + str(seq), shot=shot).data() _psi = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/PSI/' + str(seq), shot=shot).data() _psi = np.reshape(_psi, (-1, _r.size, _z.size)) _psi_axis = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/FAXS/' + str(seq), shot=shot).data() _psi_bnd = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/FBND/' + str(seq), shot=shot).data() _ipmhd = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/XIPC/' + str(seq), shot=shot).data() _bphi = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/BVAC/' + str(seq), shot=shot).data() _axisr = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/RMAG/' + str(seq), shot=shot).data() _axisz = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/ZMAG/' + str(seq), shot=shot).data() _fpol = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/F/' + str(seq), shot=shot).data() _ppres = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/P/' + str(seq), shot=shot).data() _ffprime = 4.0 * np.pi * 1.0e-7 * OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/DFDP/' + str(seq), shot=shot).data() _pprime = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/DPDP/' + str(seq), shot=shot).data() _qprof = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/Q/' + str(seq), shot=shot).data() _rbbbs = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/RBND/' + str(seq), shot=shot).data() _zbbbs = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/ZBND/' + str(seq), shot=shot).data() _nbbbs = OMFITmdsValue(server=device, TDI=uid + '@PPF/' + dda + '/NBND/' + str(seq), shot=shot).data() tind = np.abs(_times - time).argmin() # case self['CASE'] = ['EFIT++ ', device, ' #%6d' % shot, ' #%4dms' % int(time * 1000), ' ', ' '] # scalars self['NW'] = len(_r[0, :]) self['NH'] = len(_z[0, :]) self['RDIM'] = max(_r[0, :]) - min(_r[0, :]) self['ZDIM'] = max(_z[0, :]) - min(_z[0, :]) self['RLEFT'] = min(_r[0, :]) self['ZMID'] = (max(_z[0, :]) + min(_z[0, :])) / 2.0 self['RCENTR'] = (_times * 0 + 2.96)[tind] self['BCENTR'] = _bphi[tind] self['CURRENT'] = _ipmhd[tind] self['RMAXIS'] = _axisr[tind] self['ZMAXIS'] = _axisz[tind] self['SIMAG'] = _psi_axis[tind] self['SIBRY'] = _psi_bnd[tind] # 1d quantiites self['PRES'] = _ppres[tind, :] self['QPSI'] = _qprof[tind, :] self['FPOL'] = _fpol[tind, :] self['FFPRIM'] = _ffprime[tind, :] self['PPRIME'] = _pprime[tind, :] # 2d quantities self['PSIRZ'] = _psi[tind, :, :] # limiter # fmt: off self['RLIM'] = np.array([3.28315,3.32014,3.36284,3.43528,3.50557,3.56982,3.62915,3.68080,3.72864,3.77203,3.80670,3.83648,3.85929,3.87677,3.88680,3.89095,3.88851,3.87962,3.86452,3.84270,3.81509,3.78134,3.74114,3.69522,3.67115,3.63730,3.64211,3.38176,3.33154,3.28182,3.18634,3.13665,3.00098,2.86066,2.76662,2.66891,2.56922,2.47303,2.38133,2.29280,2.19539,2.18241,2.06756,1.96130,1.94246,1.92612,1.91105,1.89707,1.88438,1.87173,1.85942,1.84902,1.84139,1.83714,1.83617,1.83847,1.84408,1.85296,1.86502,1.88041,1.89901,1.92082,1.94570,1.97056,2.00911,2.20149,2.14463,2.29362,2.29362,2.29544,2.35993,2.39619,2.40915,2.41225,2.41293,2.41293,2.41224,2.40762,2.39801,2.41921,2.42117,2.41880,2.41628,2.40573,2.31498,2.35349,2.37428,2.42744,2.44623,2.52369,2.52459,2.55911,2.55296,2.57391,2.63299,2.63369,2.69380,2.69434,2.75443,2.75517,2.81471,2.81467,2.80425,2.85703,2.87846,2.93644,2.95732,2.98698,2.89768,2.88199,2.88163,2.90045,2.89049,2.88786,2.88591,2.88591,2.88946,2.90082,2.91335,2.96348,3.00975,3.06005,3.19404,3.20225,3.30634,3.28315]) self['ZLIM'] = np.array([-1.12439,-1.07315,-1.02794,-0.94610,-0.85735,-0.76585,-0.67035,-0.57428,-0.47128,-0.36188,-0.25689,-0.14639,-0.03751,0.07869,0.18627,0.30192,0.41715,0.52975,0.64099,0.75310,0.86189,0.96912,1.07530,1.17853,1.22759,1.33388,1.40768,1.64453,1.70412,1.73872,1.81753,1.85212,1.88341,1.94241,1.96996,1.98344,1.98201,1.96596,1.93599,1.89157,1.82284,1.82372,1.59819,1.32058,1.23457,1.14816,1.05033,0.95108,0.85152,0.75232,0.65348,0.55536,0.45663,0.35785,0.25926,0.16033,0.06101,-0.03766,-0.13519,-0.23314,-0.33022,-0.42668,-0.52195,-0.60693,-0.78399,-1.24842,-1.27494,-1.31483,-1.33144,-1.33443,-1.33443,-1.37323,-1.40030,-1.42198,-1.43148,-1.46854,-1.47678,-1.50441,-1.51641,-1.59223,-1.61022,-1.64283,-1.65610,-1.68971,-1.73870,-1.73870,-1.73504,-1.71349,-1.70983,-1.70983,-1.69997,-1.65498,-1.63799,-1.60180,-1.61714,-1.61989,-1.63550,-1.63821,-1.65481,-1.65658,-1.67203,-1.70788,-1.71158,-1.71158,-1.71602,-1.74139,-1.74595,-1.74595,-1.68233,-1.62282,-1.59160,-1.51041,-1.49841,-1.48925,-1.47397,-1.43566,-1.41714,-1.39278,-1.37624,-1.33481,-1.33481,-1.29777,-1.21404,-1.20891,-1.20891,-1.12439]) self['LIMITR'] = len(self['RLIM']) # fmt: on # lcfs nbbbs = int(_nbbbs[tind]) self['RBBBS'] = _rbbbs[tind, :nbbbs] self['ZBBBS'] = _zbbbs[tind, :nbbbs] self['NBBBS'] = nbbbs # cocosify to have AuxQuantities and fluxSurfaces created properly self._cocos = 7 self.cocosify(1, calcAuxQuantities=True, calcFluxSurfaces=True) self.status = True return self
[docs] def from_efitpp(self, ncfile=None, shot=99999, time=0.0, device='MAST', pfx=None): """ Read in data from EFIT++ netCDF :param filenc: EFIT++ netCDF file :param shot: shot number to read in :param time: time to read in data """ try: from netCDF4 import Dataset except Exception: raise ImportError("Cannot load netcdf file") rootd = Dataset(ncfile, "r") if 'output' not in rootd.groups.keys(): self.from_efitpp_mastu(ncfile=ncfile, shot=shot, time=time, device=device, pfx=pfx) return self try: _psi = np.transpose(rootd.groups['output'].groups['profiles2D'].variables['poloidalFlux'], (0, 2, 1)) _r = rootd.groups['output'].groups['profiles2D'].variables['r'] _z = rootd.groups['output'].groups['profiles2D'].variables['z'] _radius = rootd.groups['output'].groups['radialProfiles'].variables['r'] _bVac = rootd.groups['input'].groups['bVacRadiusProduct'].variables['values'] _ppres = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['staticPressure'] _rppres = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['rotationalPressure'] _qprof = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['q'] _ffprim = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['ffPrime'] _pprime = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['staticPPrime'] _fpol = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['rBphi'] _psipr = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['normalizedPoloidalFlux'] _psi_axis = rootd.groups['output'].groups['globalParameters'].variables['psiAxis'] _psi_bnd = rootd.groups['output'].groups['globalParameters'].variables['psiBoundary'] _ipmhd = rootd.groups['output'].groups['globalParameters'].variables['plasmaCurrent'] _axis = rootd.groups['output'].groups['globalParameters'].variables['magneticAxis'] _rlim = rootd.groups['input'].groups['limiter'].variables['rValues'] _zlim = rootd.groups['input'].groups['limiter'].variables['zValues'] _rbbbs = rootd.groups['output'].groups['separatrixGeometry'].variables['boundaryCoords'][:]['R'] _zbbbs = rootd.groups['output'].groups['separatrixGeometry'].variables['boundaryCoords'][:]['Z'] self._timenc = np.array(rootd.variables['time']) tind = np.abs(self._timenc - time).argmin() # case self['CASE'] = ['EFIT++ ', device, ' #%6d' % shot, ' #%4dms' % int(time * 1000), ' ', ' '] # Define global parameters specs = utils_fusion.device_specs(device=device) check = np.isfinite(_psi_axis[tind]) if not check: print("Skipping time slice: EFIT++ failed to converge for timeslice: ", time) self.status = False return # scalars self['NW'] = len(_r[tind, :]) self['NH'] = len(_z[tind, :]) self['RDIM'] = max(_r[tind, :]) - min(_r[tind, :]) self['ZDIM'] = max(_z[tind, :]) - min(_z[tind, :]) self['RLEFT'] = min(_r[tind, :]) self['ZMID'] = (max(_z[tind, :]) + min(_z[tind, :])) / 2.0 self['RCENTR'] = specs['R0'] self['BCENTR'] = _bVac[tind] / specs['R0'] self['CURRENT'] = _ipmhd[tind] self['RMAXIS'] = (_axis[tind])[0] self['ZMAXIS'] = (_axis[tind])[1] self['SIMAG'] = _psi_axis[tind] self['SIBRY'] = _psi_bnd[tind] # 1d quantiites self['PRES'] = _ppres[tind, :] self['QPSI'] = _qprof[tind, :] self['FPOL'] = _fpol[tind, :] self['FFPRIM'] = _ffprim[tind, :] self['PPRIME'] = _pprime[tind, :] # 2d quantities self['PSIRZ'] = _psi[tind, :, :] # limiter self['RLIM'] = _rlim[:] self['ZLIM'] = _zlim[:] self['LIMITR'] = len(self['RLIM']) # lcfs self['RBBBS'] = _rbbbs[tind] self['ZBBBS'] = _zbbbs[tind] self['NBBBS'] = len(_zbbbs[tind]) # cocosify to have AuxQuantities and fluxSurfaces created properly self._cocos = 7 self.cocosify(1, calcAuxQuantities=True, calcFluxSurfaces=True) self.add_rhovn() self.status = True finally: rootd.close() return self
[docs] def from_efitpp_mastu(self, ncfile=None, shot=99999, time=0.0, device='MAST', pfx=None): """ Read in data from EFIT++ netCDF :param filenc: EFIT++ netCDF file :param shot: shot number to read in :param time: time to read in data :param device: machine :param pfx: equilibrium type """ try: from netCDF4 import Dataset except Exception: raise ImportError("Cannot load netcdf file") netcdf = Dataset(ncfile, "r") if pfx is None: pfx = list(netcdf.groups.keys())[0] rootd = netcdf.groups[pfx] try: _psi = np.transpose(rootd.groups['output'].groups['profiles2D'].variables['poloidalFlux'], (0, 2, 1)) _r = rootd.groups['output'].groups['profiles2D'].variables['r'] _z = rootd.groups['output'].groups['profiles2D'].variables['z'] _radius = rootd.groups['output'].groups['radialProfiles'].variables['r'] _bVac = rootd.groups['input'].variables['bVacRadiusProduct'] _ppres = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['staticPressure'] _rppres = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['rotationalPressure'] _qprof = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['q'] _ffprim = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['ffPrime'] _pprime = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['staticPPrime'] _fpol = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['rBphi'] _psipr = rootd.groups['output'].groups['fluxFunctionProfiles'].variables['normalizedPoloidalFlux'] _psi_axis = rootd.groups['output'].groups['globalParameters'].variables['psiAxis'] _psi_bnd = rootd.groups['output'].groups['globalParameters'].variables['psiBoundary'] _ipmhd = rootd.groups['output'].groups['globalParameters'].variables['plasmaCurrent'] _raxis = rootd.groups['output'].groups['globalParameters'].groups['magneticAxis'].variables['R'] _zaxis = rootd.groups['output'].groups['globalParameters'].groups['magneticAxis'].variables['Z'] _rlim = rootd.groups['input'].groups['limiter'].variables['rValues'] _zlim = rootd.groups['input'].groups['limiter'].variables['zValues'] _rbbbs = rootd.groups['output'].groups['separatrixGeometry'].variables['rBoundary'] _zbbbs = rootd.groups['output'].groups['separatrixGeometry'].variables['zBoundary'] self._timenc = np.array(rootd.variables['time']) tind = np.abs(self._timenc - time).argmin() # case self['CASE'] = ['EFIT++ ', device, ' #%6d' % shot, ' #%4dms' % int(time * 1000), ' ', ' '] # Define global parameters specs = utils_fusion.device_specs(device=device) check = np.isfinite(_psi_axis[tind]) if not check: print("Skipping time slice: EFIT++ failed to converge for timeslice: ", time) self.status = False return # scalars self['NW'] = len(_r) self['NH'] = len(_z) self['RDIM'] = max(_r) - min(_r) self['ZDIM'] = max(_z) - min(_z) self['RLEFT'] = min(_r) self['ZMID'] = (max(_z) + min(_z)) / 2.0 self['RCENTR'] = specs['R0'] self['BCENTR'] = _bVac[tind] / specs['R0'] self['CURRENT'] = _ipmhd[tind] self['RMAXIS'] = _raxis[tind] self['ZMAXIS'] = _zaxis[tind] self['SIMAG'] = _psi_axis[tind] self['SIBRY'] = _psi_bnd[tind] # 1d quantiites self['PRES'] = _ppres[tind, :] self['QPSI'] = _qprof[tind, :] self['FPOL'] = _fpol[tind, :] self['FFPRIM'] = _ffprim[tind, :] self['PPRIME'] = _pprime[tind, :] # 2d quantities self['PSIRZ'] = _psi[tind, :, :] # limiter self['RLIM'] = _rlim[:] self['ZLIM'] = _zlim[:] self['LIMITR'] = len(self['RLIM']) # lcfs self['RBBBS'] = _rbbbs[tind] self['ZBBBS'] = _zbbbs[tind] self['NBBBS'] = len(_zbbbs[tind]) # cocosify to have AuxQuantities and fluxSurfaces created properly self._cocos = 7 self.cocosify(1, calcAuxQuantities=True, calcFluxSurfaces=True) self.add_rhovn() self.status = True finally: netcdf.close() return self
[docs] def from_aug_sfutils(self, shot=None, time=None, eq_shotfile='EQI', ed=1): """ Fill in gEQDSK data from aug_sfutils, which processes magnetic equilibrium results from the AUG CLISTE code. Note that this function requires aug_sfutils to be locally installed (pip install aug_sfutils will do). Users also need to have access to the AUG shotfile system. :param shot: AUG shot number from which to read data :param time: time slice from which to read data :param eq_shotfile: equilibrium reconstruction to fetch (EQI, EQH, IDE, ...) :param ed: edition of the equilibrium reconstruction shotfile :return: self """ if shot is None: raise ValueError('Must specify shot') if time is None: raise ValueError('Must specify time') try: import aug_sfutils as sf except ImportError as e: raise ImportError('aug_sfutils does not seem to be installed: ' + str(e)) # Reading equilibrium into a class eqm = sf.EQU(shot, diag=eq_shotfile, ed=ed) # reads AUG equilibrium into a class # get start point for geqdsk dictionary from aug_sfutils geq = sf.to_geqdsk(eqm, t_in=time) # corrections to keep consistency with latest aug_sfutils versions geq['PSIRZ'] = geq['PSIRZ'].T geq['LIMITR'] = len(geq['RLIM']) geq['NBBBS'] = len(geq['ZBBBS']) # now fill up OMFITgeqdsk object self.update(geq) # a few extra things to enable greater use of omfit_eqdsk self.add_rhovn() # ensure correct cocos and then calculate extra quantities self._cocos = eqm.cocos self.cocosify(1, calcAuxQuantities=True, calcFluxSurfaces=True) # set to cocos=1 return self
[docs] def add_geqdsk_documentation(self): gdesc = self['_desc'] = SortedDict() gdesc['CASE'] = 'Identification character string' gdesc['NW'] = 'Number of horizontal R grid points' gdesc['NH'] = 'Number of vertical Z grid points' gdesc['RDIM'] = 'Horizontal dimension in meter of computational box' gdesc['ZDIM'] = 'Vertical dimension in meter of computational box' gdesc['RCENTR'] = 'R in meter of vacuum toroidal magnetic field BCENTR' gdesc['RLEFT'] = 'Minimum R in meter of rectangular computational box' gdesc['ZMID'] = 'Z of center of computational box in meter' gdesc['RMAXIS'] = 'R of magnetic axis in meter' gdesc['ZMAXIS'] = 'Z of magnetic axis in meter' gdesc['SIMAG'] = 'poloidal flux at magnetic axis in Weber /rad' gdesc['SIBRY'] = 'poloidal flux at the plasma boundary in Weber /rad' gdesc['BCENTR'] = 'Vacuum toroidal magnetic field in Tesla at RCENTR' gdesc['CURRENT'] = 'Plasma current in Ampere' gdesc['FPOL'] = 'Poloidal current function in m-T, F = RBT on flux grid' gdesc['PRES'] = 'Plasma pressure in nt / m2 on uniform flux grid' gdesc['FFPRIM'] = 'FF’(ψ) in (mT)2 / (Weber /rad) on uniform flux grid' gdesc['PPRIME'] = 'P’(ψ) in (nt /m2) / (Weber /rad) on uniform flux grid' gdesc['PSIRZ'] = 'Poloidal flux in Weber / rad on the rectangular grid points' gdesc['QPSI'] = 'q values on uniform flux grid from axis to boundary' gdesc['NBBBS'] = 'Number of boundary points' gdesc['LIMITR'] = 'Number of limiter points' gdesc['RBBBS'] = 'R of boundary points in meter' gdesc['ZBBBS'] = 'Z of boundary points in meter' gdesc['RLIM'] = 'R of surrounding limiter contour in meter' gdesc['ZLIM'] = 'Z of surrounding limiter contour in meter' gdesc['KVTOR'] = 'Toroidal rotation switch' gdesc['RVTOR'] = 'Toroidal rotation characteristic major radius in m' gdesc['NMASS'] = 'Mass density switch' gdesc['RHOVN'] = 'Normalized toroidal flux on uniform poloidal flux grid' gdesc['AuxNamelist'] = SortedDict() gdesc['AuxQuantities'] = SortedDict() gdesc['fluxSurfaces'] = SortedDict() ### AUX NAMELIST ### andesc = gdesc['AuxNamelist'] ## EFITIN ## andesc['efitin'] = SortedDict() andesc['efitin']['scrape'] = '' andesc['efitin']['nextra'] = '' andesc['efitin']['itek'] = '' andesc['efitin']['ICPROF'] = '' andesc['efitin']['qvfit'] = '' andesc['efitin']['fwtbp'] = '' andesc['efitin']['kffcur'] = '' andesc['efitin']['kppcur'] = '' andesc['efitin']['fwtqa'] = '' andesc['efitin']['zelip'] = '' andesc['efitin']['iavem'] = '' andesc['efitin']['iavev'] = '' andesc['efitin']['n1coil'] = '' andesc['efitin']['nccoil'] = '' andesc['efitin']['nicoil'] = '' andesc['efitin']['iout'] = '' andesc['efitin']['fwtsi'] = '' andesc['efitin']['fwtmp2'] = '' andesc['efitin']['fwtcur'] = '' andesc['efitin']['fitdelz'] = '' andesc['efitin']['fwtfc'] = '' andesc['efitin']['fitsiref'] = '' andesc['efitin']['kersil'] = '' andesc['efitin']['ifitdelz'] = '' andesc['efitin']['ERROR'] = '' andesc['efitin']['ERRMIN'] = '' andesc['efitin']['MXITER'] = '' andesc['efitin']['fcurbd'] = '' andesc['efitin']['pcurbd'] = '' andesc['efitin']['kcalpa'] = '' andesc['efitin']['kcgama'] = '' andesc['efitin']['xalpa'] = '' andesc['efitin']['xgama'] = '' andesc['efitin']['RELAX'] = '' andesc['efitin']['keqdsk'] = '' andesc['efitin']['CALPA'] = '' andesc['efitin']['CGAMA'] = '' ## OUT1 ## andesc['OUT1'] = SortedDict() andesc['OUT1']['ISHOT'] = '' andesc['OUT1']['ITIME'] = '' andesc['OUT1']['BETAP0'] = '' andesc['OUT1']['RZERO'] = '' andesc['OUT1']['QENP'] = '' andesc['OUT1']['ENP'] = '' andesc['OUT1']['EMP'] = '' andesc['OUT1']['PLASMA'] = '' andesc['OUT1']['EXPMP2'] = '' andesc['OUT1']['COILS'] = '' andesc['OUT1']['BTOR'] = '' andesc['OUT1']['RCENTR'] = '' andesc['OUT1']['BRSP'] = '' andesc['OUT1']['ICURRT'] = '' andesc['OUT1']['RBDRY'] = '' andesc['OUT1']['ZBDRY'] = '' andesc['OUT1']['NBDRY'] = '' andesc['OUT1']['FWTSI'] = '' andesc['OUT1']['FWTCUR'] = '' andesc['OUT1']['MXITER'] = '' andesc['OUT1']['NXITER'] = '' andesc['OUT1']['LIMITR'] = '' andesc['OUT1']['XLIM'] = '' andesc['OUT1']['YLIM'] = '' andesc['OUT1']['ERROR'] = '' andesc['OUT1']['ICONVR'] = '' andesc['OUT1']['IBUNMN'] = '' andesc['OUT1']['PRESSR'] = '' andesc['OUT1']['RPRESS'] = '' andesc['OUT1']['QPSI'] = '' andesc['OUT1']['PRESSW'] = '' andesc['OUT1']['PRES'] = '' andesc['OUT1']['NQPSI'] = '' andesc['OUT1']['NPRESS'] = '' andesc['OUT1']['SIGPRE'] = '' ## BASIS ## andesc['BASIS'] = SortedDict() andesc['BASIS']['KPPFNC'] = '' andesc['BASIS']['KPPKNT'] = '' andesc['BASIS']['PPKNT'] = '' andesc['BASIS']['PPTENS'] = '' andesc['BASIS']['KFFFNC'] = '' andesc['BASIS']['KFFKNT'] = '' andesc['BASIS']['FFKNT'] = '' andesc['BASIS']['FFTENS'] = '' andesc['BASIS']['KWWFNC'] = '' andesc['BASIS']['KWWKNT'] = '' andesc['BASIS']['WWKNT'] = '' andesc['BASIS']['WWTENS'] = '' andesc['BASIS']['PPBDRY'] = '' andesc['BASIS']['PP2BDRY'] = '' andesc['BASIS']['KPPBDRY'] = '' andesc['BASIS']['KPP2BDRY'] = '' andesc['BASIS']['FFBDRY'] = '' andesc['BASIS']['FF2BDRY'] = '' andesc['BASIS']['KFFBDRY'] = '' andesc['BASIS']['KFF2BDRY'] = '' andesc['BASIS']['WWBDRY'] = '' andesc['BASIS']['WW2BDRY'] = '' andesc['BASIS']['KWWBDRY'] = '' andesc['BASIS']['KWW2BDRY'] = '' andesc['BASIS']['KEEFNC'] = '' andesc['BASIS']['KEEKNT'] = '' andesc['BASIS']['EEKNT'] = '' andesc['BASIS']['EETENS'] = '' andesc['BASIS']['EEBDRY'] = '' andesc['BASIS']['EE2BDRY'] = '' andesc['BASIS']['KEEBDRY'] = '' andesc['BASIS']['KEE2BDRY'] = '' ## CHITOUT ## andesc['CHIOUT'] = SortedDict() andesc['CHIOUT']['SAISIL'] = '' andesc['CHIOUT']['SAIMPI'] = '' andesc['CHIOUT']['SAIPR'] = '' andesc['CHIOUT']['SAIIP'] = '' ### AUX QUANTITIES ### aqdesc = gdesc['AuxQuantities'] aqdesc['R'] = 'all R in the eqdsk grid (m)' aqdesc['Z'] = 'all Z in the eqdsk grid (m)' aqdesc['PSI'] = 'Poloidal flux in Weber / rad' aqdesc['PSI_NORM'] = 'Normalized polodial flux (psin = (psi-min(psi))/(max(psi)-min(psi))' aqdesc['PSIRZ'] = 'Poloidal flux in Weber / rad on the rectangular grid points' aqdesc['PSIRZ_NORM'] = 'Normalized poloidal flux in Weber / rad on the rectangular grid points' aqdesc['RHOp'] = 'sqrt(PSI_NORM)' aqdesc['RHOpRZ'] = 'sqrt(PSI_NORM) on the rectangular grid points' aqdesc['FPOLRZ'] = 'Poloidal current function on the rectangular grid points' aqdesc['PRESRZ'] = 'Pressure on the rectangular grid points' aqdesc['QPSIRZ'] = 'Safety factor on the rectangular grid points' aqdesc['FFPRIMRZ'] = "FF' on the rectangular grid points" aqdesc['PPRIMERZ'] = "P' on the rectangular grid points" aqdesc['PRES0RZ'] = 'Pressure by rotation term (eq 26 & 30 of Lao et al., FST 48.2 (2005): 968-977' aqdesc['Br'] = 'Radial magnetic field in Tesla on the rectangular grid points' aqdesc['Bz'] = 'Vertical magnetic field in Tesla on the rectangular grid points' aqdesc['Bp'] = 'Poloidal magnetic field in Tesla on the rectangular grid points' aqdesc['Bt'] = 'Toroidal magnetic field in Tesla on the rectangular grid points' aqdesc['Jr'] = 'Radial current density on the rectangular grid points' aqdesc['Jz'] = 'Vertical current density on the rectangular grid points' aqdesc['Jt'] = 'Toroidal current density on the rectangular grid points' aqdesc['Jp'] = 'Poloidal current density on the rectangular grid points' aqdesc['Jt_fb'] = '' aqdesc['Jpar'] = 'Parallel current density on the rectangular grid points' aqdesc['PHI'] = 'Toroidal flux in Weber / rad' aqdesc['PHI_NORM'] = 'Normalize toroidal flux (phin = (phi-min(phi))/(max(phi)-min(phi))' aqdesc['PHIRZ'] = 'Toroidal flux in Weber / rad on the rectangular grid points' aqdesc['RHOm'] = 'sqrt(|PHI/pi/BCENTR|)' aqdesc['RHO'] = 'sqrt(PHI_NORM)' aqdesc['RHORZ'] = 'sqrt(PHI_NORM) on the rectangular grid points' aqdesc['Rx1'] = '' aqdesc['Zx1'] = '' aqdesc['Rx2'] = '' aqdesc['Zx2'] = '' ### FLUX SURFACES ### fsdesc = gdesc['fluxSurfaces'] ## MAIN ## fsdesc['R0'] = gdesc['RMAXIS'] + ' from eqdsk' fsdesc['Z0'] = gdesc['ZMAXIS'] + ' from eqdsk' fsdesc['RCENTR'] = gdesc['RCENTR'] fsdesc['R0_interp'] = 'R0 from fit paraboloid in the vicinity of the grid-based center (m)' fsdesc['Z0_interp'] = 'Z0 from fit paraboloid in the vicinity of the grid-based center (m)' fsdesc['levels'] = "flux surfaces (normalized psi) for the 'flux' tree" fsdesc['BCENTR'] = gdesc['BCENTR'] + " (BCENTR = Fpol[-1] / RCENTR)" fsdesc['CURRENT'] = gdesc['CURRENT'] ## FLUX ## fsdesc['flux'] = SortedDict() fsdesc['flux']['psi'] = 'poloidal flux in Weber / rad on flux surface' fsdesc['flux']['R'] = 'R in meters along flux surface surface' fsdesc['flux']['Z'] = 'Z in meters along flux surface surface' fsdesc['flux']['F'] = 'poloidal current function in m-T on flux surface' fsdesc['flux']['P'] = 'pressure in Pa on flux surface' fsdesc['flux']['PPRIME'] = 'P’(ψ) in (nt /m2) / (Weber /rad) on flux surface' fsdesc['flux']['FFPRIM'] = 'FF’(ψ) in (mT)2 / (Weber /rad) on flux surface' fsdesc['flux']['Br'] = 'Br in Tesla along flux surface surface' fsdesc['flux']['Bz'] = 'Bz in Tesla along flux surface surface' fsdesc['flux']['Jt'] = 'toroidal current density along flux surface' fsdesc['flux']['Bmax'] = 'maximum B on flux surface' fsdesc['flux']['q'] = 'safety factor on flux surface' ## AVG ## fsdesc['avg'] = SortedDict() fsdesc['avg']['R'] = 'flux surface average of major radius (m)' fsdesc['avg']['a'] = 'flux surface average of minor radius (m)' fsdesc['avg']['R**2'] = 'flux surface average of R^2 (m^2)' fsdesc['avg']['1/R'] = 'flux surface average of 1/R (1/m)' fsdesc['avg']['1/R**2'] = 'flux surface average of 1/R^2 (1/m^2)' fsdesc['avg']['Bp'] = 'flux surface average of poloidal B (T)' fsdesc['avg']['Bp**2'] = 'flux surface average of Bp^2 (T^2)' fsdesc['avg']['Bp*R'] = 'flux surface average of Bp*R (T m)' fsdesc['avg']['Bp**2*R**2'] = 'flux surface average of Bp^2*R^2 (T^2 m^2)' fsdesc['avg']['Btot'] = 'flux surface average of total B (T)' fsdesc['avg']['Btot**2'] = 'flux surface average of Btot^2 (T^2)' fsdesc['avg']['Bt'] = 'flux surface average of toroidal B (T)' fsdesc['avg']['Bt**2'] = 'flux surface average of Bt^2 (T^2)' fsdesc['avg']['ip'] = '' fsdesc['avg']['vp'] = '' fsdesc['avg']['q'] = 'flux surface average of saftey factor' fsdesc['avg']['hf'] = '' fsdesc['avg']['Jt'] = 'flux surface average torioidal current density' fsdesc['avg']['Jt/R'] = 'flux surface average torioidal current density / R' fsdesc['avg']['fc'] = 'flux surface average of passing particle fraction' fsdesc['avg']['grad_term'] = '' fsdesc['avg']['P'] = 'flux surface average of pressure (Pa)' fsdesc['avg']['F'] = 'flux surface average of Poloidal current function F (T m)' fsdesc['avg']['PPRIME'] = 'flux surface average of P’ in (nt /m2) / (Weber /rad)' fsdesc['avg']['FFPRIM'] = 'flux surface average of FF’ in (mT)2 / (Weber /rad)' fsdesc['avg']['dip/dpsi'] = '' fsdesc['avg']['Jeff'] = '' fsdesc['avg']['beta_t'] = 'volume averaged toroidal beta' fsdesc['avg']['beta_n'] = 'volume averaged normalized beta' fsdesc['avg']['beta_p'] = 'volume averaged poloidal beta' fsdesc['avg']['fcap'] = '' fsdesc['avg']['hcap'] = '' fsdesc['avg']['gcap'] = '' ## GEO ## fsdesc['geo'] = SortedDict() fsdesc['geo']['psi'] = 'Poloidal flux (Wb / rad)' fsdesc['geo']['psin'] = 'Normalized poloidal flux' fsdesc['geo']['R'] = 'R0 of each flux surface (m)' fsdesc['geo']['R_centroid'] = '' fsdesc['geo']['Rmax_centroid'] = '' fsdesc['geo']['Rmin_centroid'] = '' fsdesc['geo']['Z'] = 'Z0 of each flux surface (m)' fsdesc['geo']['Z_centroid'] = '' fsdesc['geo']['a'] = 'Minor radius (m)' fsdesc['geo']['dell'] = 'Lower triangularity' fsdesc['geo']['delta'] = 'Average triangularity' fsdesc['geo']['delu'] = 'Upper triangularity' fsdesc['geo']['eps'] = 'Inverse aspect ratio' fsdesc['geo']['kap'] = 'Average elongation' fsdesc['geo']['kapl'] = 'Lower elongation' fsdesc['geo']['kapu'] = 'Upper elongation' fsdesc['geo']['lonull'] = '' fsdesc['geo']['per'] = '' fsdesc['geo']['surfArea'] = 'Plasma surface area (m^2)' fsdesc['geo']['upnull'] = '' fsdesc['geo']['zeta'] = 'Average squareness' fsdesc['geo']['zetail'] = 'Inner lower squareness' fsdesc['geo']['zetaiu'] = 'Inner upper squareness' fsdesc['geo']['zetaol'] = 'Outer lower squareness' fsdesc['geo']['zetaou'] = 'Outer upper squareness' fsdesc['geo']['zoffset'] = '' fsdesc['geo']['vol'] = 'Plasma volume (m^3)' fsdesc['geo']['cxArea'] = 'Plasma cross-sectional area (m^2)' fsdesc['geo']['phi'] = 'Toroidal flux in Weber / rad' fsdesc['geo']['bunit'] = '' fsdesc['geo']['rho'] = 'sqrt(|PHI/pi/BCENTR|)' fsdesc['geo']['rhon'] = 'sqrt(PHI_NORM)' ## MIDPLANE ## fsdesc['midplane'] = SortedDict() fsdesc['midplane']['R'] = 'R values of midplane slice in meters' fsdesc['midplane']['Z'] = 'Z values of midplane slice in meters' fsdesc['midplane']['Br'] = "Br at (R_midplane, Zmidplane) in Tesla" fsdesc['midplane']['Bz'] = "Br at (R_midplane, Zmidplane) in Tesla" fsdesc['midplane']['Bp'] = "Bp at (R_midplane, Zmidplane) in Tesla" fsdesc['midplane']['Bt'] = "Bt at (R_midplane, Zmidplane) in Tesla" ## INFO ## fsdesc['info'] = SortedDict() fsdesc['info']['internal_inductance'] = SortedDict() fsdesc['info']['internal_inductance']['li_from_definition'] = 'Bp2_vol / vol / mu_0^2 / ip&2 * circum^2' fsdesc['info']['internal_inductance']['li_(1)_TLUCE'] = 'li_from_definition / circum^2 * 2 * vol / r_0 * correction_factor' fsdesc['info']['internal_inductance']['li_(2)_TLUCE'] = 'li_from_definition / circum^2 * 2 * vol / r_axis' fsdesc['info']['internal_inductance']['li_(3)_TLUCE'] = 'li_from_definition / circum^2 * 2 * vol / r_0' fsdesc['info']['internal_inductance']['li_(1)_EFIT'] = 'circum^2 * Bp2_vol / (vol * mu_0^2 * ip^2)' fsdesc['info']['internal_inductance']['li_(3)_IMAS'] = '2 * Bp2_vol / r_0 / ip^2 / mu_0^2' fsdesc['info']['J_efit_norm'] = 'EFIT current normalization' fsdesc['info']['open_separatrix'] = SortedDict() fsdesc['info']['open_separatrix']['psi'] = 'psi of last closed flux surface (Wb/rad)' fsdesc['info']['open_separatrix']['rhon'] = 'psi_n of last closed flux surface' fsdesc['info']['open_separatrix']['R'] = 'R of last closed flux surface (m)' fsdesc['info']['open_separatrix']['Z'] = 'Z of last closed flux surface (m)' fsdesc['info']['open_separatrix']['Br'] = 'Br along last closed flux surface (T)' fsdesc['info']['open_separatrix']['Bz'] = 'Bz along last closed flux surface (T)' fsdesc['info']['open_separatrix']['s'] = '' fsdesc['info']['open_separatrix']['mid_index'] = 'index of outer midplane location in open_separatrix arrays' fsdesc['info']['open_separatrix']['rho'] = 'rho of last closed flux surface (Wb/rad)' fsdesc['info']['rvsin'] = '' fsdesc['info']['rvsout'] = '' fsdesc['info']['zvsin'] = '' fsdesc['info']['zvsout'] = '' fsdesc['info']['xpoint'] = '(R, Z) of x-point in meters' fsdesc['info']['xpoint_inner_strike'] = '(R, Z) of inner strike line near the x-point in meters' fsdesc['info']['xpoint_outer_strike'] = '(R, Z) of outer strike line near the x-point in meters' fsdesc['info']['xpoint_outer_midplane'] = '(R, Z) of outer LCFS near the x-point in meters' fsdesc['info']['xpoint_inner_midplane'] = '(R, Z) of inner LCFS near the x-point in meters' fsdesc['info']['xpoint_private_region'] = '(R, Z) of private flux region near the x-point in meters' fsdesc['info']['xpoint_outer_region'] = '(R, Z) of outer SOL region near the x-point in meters' fsdesc['info']['xpoint_core_region'] = '(R, Z) of core region near the x-point in meters' fsdesc['info']['xpoint_inner_region'] = '(R, Z) of inner SOL region near the x-point in meters' fsdesc['info']['xpoint2'] = '(R, Z) of second x-point in meters' fsdesc['info']['rlim'] = gdesc['RLIM'] fsdesc['info']['zlim'] = gdesc['ZLIM']
[docs]def gEQDSK_COCOS_identify(bt, ip): """ Returns the native COCOS that an unmodified gEQDSK would obey, defined by sign(Bt) and sign(Ip) In order for psi to increase from axis to edge and for q to be positive: All use sigma_RpZ=+1 (phi is counterclockwise) and exp_Bp=0 (psi is flux/2.*pi) We want sign(psi_edge-psi_axis) = sign(Ip)*sigma_Bp > 0 (psi always increases in gEQDSK) sign(q) = sign(Ip)*sign(Bt)*sigma_rhotp > 0 (q always positive in gEQDSK) :: ============================================ Bt Ip sigma_Bp sigma_rhotp COCOS ============================================ +1 +1 +1 +1 1 +1 -1 -1 -1 3 -1 +1 +1 -1 5 -1 -1 -1 +1 7 """ COCOS = define_cocos(1) # get sign of Bt and Ip with respect to CCW phi sign_Bt = int(COCOS['sigma_RpZ'] * np.sign(bt)) sign_Ip = int(COCOS['sigma_RpZ'] * np.sign(ip)) g_cocos = { (+1, +1): 1, # +Bt, +Ip (+1, -1): 3, # +Bt, -Ip (-1, +1): 5, # -Bt, +Ip (-1, -1): 7, # -Bt, -Ip (+1, 0): 1, # +Bt, No current (-1, 0): 3, } # -Bt, No current return g_cocos.get((sign_Bt, sign_Ip), None)
OMFITgeqdsk.volume_integral.__doc__ = fluxSurfaces.volume_integral.__doc__ OMFITgeqdsk.surface_integral.__doc__ = fluxSurfaces.surface_integral.__doc__ ############################ # A-FILE CLASS OMFITaeqdsk # ############################
[docs]class OMFITaeqdsk(SortedDict, OMFITascii): r""" class used to interface A files generated by EFIT :param filename: filename passed to OMFITascii class :param \**kw: keyword dictionary passed to OMFITascii class """ def __init__(self, filename, **kw): OMFITascii.__init__(self, filename, **kw) SortedDict.__init__(self, caseInsensitive=True, sorted=True) self.dynaLoad = True
[docs] @dynaLoad def load(self, **kw): """ Method used to read a-files """ if self.filename is None or not os.stat(self.filename).st_size: return f1040 = fortranformat.FortranRecordReader('1x,4e16.9') f1041 = fortranformat.FortranRecordReader('1x,4i5') f1060 = fortranformat.FortranRecordReader('A1,f8.3,9x,i5,11x,i5,1x,a3,1x,i3,1x,i3,1x,a3,1x,2i5') def read_f1040(input_str): try: return f1040.read(input_str) except Exception: return [0.0] * 4 self.clear() # use this class as iterator for debugging class AFILE_dbg(object): def __init__(self, obj): self.obj = obj self.k = 0 def __iter__(self): return self def __next__(self): tmp = self.obj[self.k] self.k += 1 print(tmp.rstrip()) return tmp with open(self.filename, 'r') as f: AFILE = iter(f.readlines()) self['__header__'] = '' k = 0 for line in AFILE: if line[0] == '*': break else: self['__header__'] += line if k == 1: self['shot'] = int(line[:7]) k += 1 try: ( dummy, self['time'], self['jflag'], self['lflag'], self['limloc'], self['mco2v'], self['mco2r'], self['qmflag'], self['nlold'], self['nlnew'], ) = f1060.read(line) except Exception: ( self['jflag'], self['lflag'], self['limloc'], self['mco2v'], self['mco2r'], self['qmflag'], self['nlold'], self['nlnew'], ) = line.split()[1:] printe('bad time in aEQDSK file: %s' % (self.filename)) self['time'] = 0 for k in ['jflag', 'lflag', 'mco2v', 'mco2r', 'nlold', 'nlnew']: self[k] = int(self[k]) for k in ['nlold', 'nlnew']: if self[k] is None: self[k] = 0 self['rseps'] = np.zeros(2) self['zseps'] = np.zeros(2) # fmt: off self['chisq'], self['rcencm'], self['bcentr'], self['ipmeas'] = read_f1040(next(AFILE)) self['ipmhd'], self['rcntr'], self['zcntr'], self['aminor'] = read_f1040(next(AFILE)) self['elong'], self['utri'], self['ltri'], self['volume'] = read_f1040(next(AFILE)) self['rcurrt'], self['zcurrt'], self['qstar'], self['betat'] = read_f1040(next(AFILE)) self['betap'], self['li'], self['gapin'], self['gapout'] = read_f1040(next(AFILE)) self['gaptop'], self['gapbot'], self['q95'], self['vertn'] = read_f1040(next(AFILE)) # fmt: on for arr, adim in (('rco2v', 'mco2v'), ('dco2v', 'mco2v'), ('rco2r', 'mco2r'), ('dco2r', 'mco2r')): tmp = [] for k in range(int(np.ceil(self[adim] / 4.0))): tmp.extend(read_f1040(next(AFILE))) self[arr] = tmp[: self[adim]] # fmt: off self['shear'], self['bpolav'], self['s1'], self['s2'] = read_f1040(next(AFILE)) self['s3'], self['qout'], self['sepin'], self['sepout'] = read_f1040(next(AFILE)) self['septop'], self['sibdry'], self['area'], self['wmhd'] = read_f1040(next(AFILE)) self['error'], self['elongm'], self['qm'], self['cdflux'] = read_f1040(next(AFILE)) self['alpha'], self['rttt'], self['psiref'], self['indent'] = read_f1040(next(AFILE)) self['rseps'][0], self['zseps'][0], self['rseps'][1], self['zseps'][1] = read_f1040(next(AFILE)) self['sepexp'], self['sepbot'], self['btaxp'], self['btaxv'] = read_f1040(next(AFILE)) self['aq1'], self['aq2'], self['aq3'], self['dsep'] = read_f1040(next(AFILE)) self['rm'], self['zm'], self['psim'], self['taumhd'] = read_f1040(next(AFILE)) self['betapd'], self['betatd'], self['wdia'], self['diamag'] = read_f1040(next(AFILE)) self['vloop'], self['taudia'], self['qmerci'], self['tavem'] = read_f1040(next(AFILE)) self['nsilop0'], self['magpri0'], self['nfcoil0'], self['nesum0'] = f1041.read(next(AFILE)) # fmt: on tmp = [] for k in range(int(np.ceil((self['nsilop0'] + self['magpri0']) / 4.0))): tmp.extend(read_f1040(next(AFILE))) self['csilop'] = tmp[: self['nsilop0']] self['cmpr2'] = tmp[self['nsilop0'] : (self['nsilop0'] + self['magpri0'])] self['ccbrsp'] = [] for k in range(int(np.ceil((self['nfcoil0']) / 4.0))): self['ccbrsp'].extend(read_f1040(next(AFILE))) self['eccurt'] = [] for k in range(int(np.ceil((self['nesum0']) / 4.0))): self['eccurt'].extend(read_f1040(next(AFILE))) try: # fmt: off self['pbinj'], self['rvsin'], self['zvsin'], self['rvsout'] = read_f1040(next(AFILE)) self['zvsout'], self['vsurf'], self['wpdot'], self['wbdot'] = read_f1040(next(AFILE)) self['slantu'], self['slantl'], self['zuperts'], self['chipre'] = read_f1040(next(AFILE)) self['cjor95'], self['pp95'], self['drsep'], self['yyy2'] = read_f1040(next(AFILE)) self['xnnc'], self['cprof'], self['oring'], self['cjor0'] = read_f1040(next(AFILE)) self['fexpan'], self['qmin'], self['chimse'], self['ssi01'] = read_f1040(next(AFILE)) self['fexpvs'], self['sepnose'], self['ssi95'], self['rhoqmin'] = read_f1040(next(AFILE)) self['cjor99'], self['cj1ave'], self['rmidin'], self['rmidout'] = read_f1040(next(AFILE)) self['psurfa'], self['peak'], self['dminux'], self['dminlx'] = read_f1040(next(AFILE)) self['dolubaf'],self['dolubafm'],self['diludom'], self['diludomm']= read_f1040(next(AFILE)) self['ratsol'], self['rvsiu'], self['zvsiu'], self['rvsid'] = read_f1040(next(AFILE)) self['zvsid'], self['rvsou'], self['zvsou'], self['rvsod'] = read_f1040(next(AFILE)) self['zvsod'], self['condno'], self['psin32'], self['psin21'] = read_f1040(next(AFILE)) self['rq32in'], self['rq21top'], self['chilibt'], self['li3'] = read_f1040(next(AFILE)) self['xbetapr'],self['tflux'], self['tchimls'], self['twagap'] = read_f1040(next(AFILE)) # fmt: on except StopIteration as _excp: pass # anything extra go in the footer self['__footer__'] = '' try: for line in AFILE: self['__footer__'] += line except Exception: pass # add betaN calculation to a-file # if it is not a vacuum shot if self['ipmhd'] != 0.0: i = self['ipmhd'] / 1e6 a = self['aminor'] / 100.0 bt = self['bcentr'] * self['rcencm'] / self['rcntr'] i_n = i / a / bt self['betan'] = abs(self['betat'] / i_n) # lists into arrays for var in self: if isinstance(self[var], list): self[var] = np.array([_f for _f in self[var] if _f is not None]) # remove NaN from aEQDSK file to allow saving for k in self: if isinstance(self[k], np.ndarray) and np.any(np.isnan(self[k])): self[k][np.isnan(self[k])] = 0 printe('%s array is NaN in aEQDSK file: %s' % (k, self.filename)) elif is_float(self[k]) and np.any(tolist(np.isnan(self[k]))): self[k] = 0 printe('%s entry is NaN in aEQDSK file: %s' % (k, self.filename)) self.add_aeqdsk_documentation()
[docs] @dynaSave def save(self): """ Method used to write a-files """ def write_f1040(list_4_items): if not np.all([tolist(k)[0] in self for k in list_4_items]): return '' list_4_values = [] for item in list_4_items: if not isinstance(item, str): list_4_values.append(self.get(item[0], [0.0] * item[1])[item[1]]) else: list_4_values.append(self.get(item, 0.0)) return f1040.write(list_4_values) + '\n' f1040 = fortranformat.FortranRecordWriter('1x,4e16.9') f1041 = fortranformat.FortranRecordWriter('1x,4i5') f1060 = fortranformat.FortranRecordWriter('A1,f8.3,9x,i5,11x,i5,1x,a3,1x,i3,1x,i3,1x,a3,1x,2i5') tmp = self['__header__'].split('\n') tmps = tmp[0] + '\n' tmps += '%7d' % self['shot'] + tmp[1][7:] + '\n' tmps += fortranformat.FortranRecordWriter('1x,e16.9').write([self['time']]) + '\n' tmps += ( f1060.write( [ '*', self['time'], self['jflag'], self['lflag'], self['limloc'], self['mco2v'], self['mco2r'], self['qmflag'], self['nlold'], self['nlnew'], ] ) + '\n' ) tmps += write_f1040(['chisq', 'rcencm', 'bcentr', 'ipmeas']) tmps += write_f1040(['ipmhd', 'rcntr', 'zcntr', 'aminor']) tmps += write_f1040(['elong', 'utri', 'ltri', 'volume']) tmps += write_f1040(['rcurrt', 'zcurrt', 'qstar', 'betat']) tmps += write_f1040(['betap', 'li', 'gapin', 'gapout']) tmps += write_f1040(['gaptop', 'gapbot', 'q95', 'vertn']) for arr, adim in (('rco2v', 'mco2v'), ('dco2v', 'mco2v'), ('rco2r', 'mco2r'), ('dco2r', 'mco2r')): for k in range(int(np.ceil(self[adim] / 4.0))): tmps += f1040.write(self[arr][k * 4 : (k + 1) * 4]) + '\n' tmps += write_f1040(['shear', 'bpolav', 's1', 's2']) tmps += write_f1040(['s3', 'qout', 'sepin', 'sepout']) tmps += write_f1040(['septop', 'sibdry', 'area', 'wmhd']) tmps += write_f1040(['error', 'elongm', 'qm', 'cdflux']) tmps += write_f1040(['alpha', 'rttt', 'psiref', 'indent']) tmps += write_f1040([('rseps', 0), ('zseps', 0), ('rseps', 1), ('zseps', 1)]) tmps += write_f1040(['sepexp', 'sepbot', 'btaxp', 'btaxv']) tmps += write_f1040(['aq1', 'aq2', 'aq3', 'dsep']) tmps += write_f1040(['rm', 'zm', 'psim', 'taumhd']) tmps += write_f1040(['betapd', 'betatd', 'wdia', 'diamag']) tmps += write_f1040(['vloop', 'taudia', 'qmerci', 'tavem']) tmps += f1041.write([self['nsilop0'], self['magpri0'], self['nfcoil0'], self['nesum0']]) + '\n' tmp = np.hstack((self['csilop'], self['cmpr2'])) for k in range(int(np.ceil((self['nsilop0'] + self['magpri0']) / 4.0))): tmps += f1040.write(tmp[k * 4 : (k + 1) * 4]) + '\n' for k in range(int(np.ceil((self['nfcoil0']) / 4.0))): tmps += f1040.write(self['ccbrsp'][k * 4 : (k + 1) * 4]) + '\n' for k in range(int(np.ceil((self['nesum0']) / 4.0))): tmps += f1040.write(self['eccurt'][k * 4 : (k + 1) * 4]) + '\n' tmps += write_f1040(['pbinj', 'rvsin', 'zvsin', 'rvsout']) tmps += write_f1040(['zvsout', 'vsurf', 'wpdot', 'wbdot']) tmps += write_f1040(['slantu', 'slantl', 'zuperts', 'chipre']) tmps += write_f1040(['cjor95', 'pp95', 'drsep', 'yyy2']) tmps += write_f1040(['xnnc', 'cprof', 'oring', 'cjor0']) tmps += write_f1040(['fexpan', 'qmin', 'chimse', 'ssi01']) tmps += write_f1040(['fexpvs', 'sepnose', 'ssi95', 'rhoqmin']) tmps += write_f1040(['cjor99', 'cj1ave', 'rmidin', 'rmidout']) tmps += write_f1040(['psurfa', 'peak', 'dminux', 'dminlx']) tmps += write_f1040(['dolubaf', 'dolubafm', 'diludom', 'diludomm']) tmps += write_f1040(['ratsol', 'rvsiu', 'zvsiu', 'rvsid']) tmps += write_f1040(['zvsid', 'rvsou', 'zvsou', 'rvsod']) tmps += write_f1040(['zvsod', 'condno', 'psin32', 'psin21']) tmps += write_f1040(['rq32in', 'rq21top', 'chilibt', 'li3']) tmps += write_f1040(['xbetapr', 'tflux', 'tchimls', 'twagap']) tmps += self['__footer__'] with open(self.filename, 'w') as f: f.write(tmps)
[docs] def from_mdsplus( self, device=None, shot=None, time=None, exact=False, SNAPfile='EFIT01', time_diff_warning_threshold=10, fail_if_out_of_range=True, show_missing_data_warnings=None, quiet=False, ): """ Fill in aEQDSK data from MDSplus :param device: The tokamak that the data correspond to ('DIII-D', 'NSTX', etc.) :param shot: Shot number from which to read data :param time: time slice from which to read data :param exact: get data from the exact time-slice :param SNAPfile: A string containing the name of the MDSplus tree to connect to, like 'EFIT01', 'EFIT02', 'EFIT03', ... :param time_diff_warning_threshold: raise error/warning if closest time slice is beyond this treshold :param fail_if_out_of_range: Raise error or warn if closest time slice is beyond time_diff_warning_threshold :param show_missing_data_warnings: Print warnings for missing data 1 or True: display with printw 2 or 'once': only print the first time 0 or False: display all but with printd instead of printw None: select based on device. Most will chose 'once'. :param quiet: verbosity :return: self """ if device is None: raise ValueError('Must specify device') if shot is None: raise ValueError('Must specify shot') if time is None: raise ValueError('Must specify time') tmp = from_mds_plus( device=device, shot=shot, times=[time], exact=exact, snap_file=SNAPfile, time_diff_warning_threshold=time_diff_warning_threshold, fail_if_out_of_range=fail_if_out_of_range, get_afile=True, show_missing_data_warnings=show_missing_data_warnings, debug=False, quiet=quiet, )['aEQDSK'][time] self.__dict__ = tmp.__dict__ self.update(tmp) return self
[docs] def add_aeqdsk_documentation(self): desc = self['_desc'] = SortedDict() desc['aq1'] = 'minor radius of q=1 surface in cm, 100 if not found' desc['aq2'] = 'minor radius of q=2 surface in cm, 100 if not found' desc['aq3'] = 'minor radius of q=3 surface in cm, 100 if not found' desc['alpha'] = 'Shafranov boundary line integral parameter' desc['aminor'] = 'plasma minor radius in cm' desc['area'] = 'cross sectional area in cm2' desc['bcentr'] = 'vacuum toroidal magnetic field in Tesla at RCENCM' desc['betan'] = 'normalized β in %' desc['betap'] = 'poloidal β with normalization average poloidal magnetic BPOLAV defined through Ampere’s law ' desc['betapd'] = 'diamagnetic poloidal β' desc['betat'] = 'toroidal β in %' desc['betatd'] = 'diamagnetic toroidal β in %' desc['bpolav'] = 'average poloidal magnetic field in Tesla defined through Ampere’s law ' desc['btaxp'] = 'toroidal magnetic field at magnetic axis in Tesla' desc['btaxv'] = 'vacuum toroidal magnetic field at magnetic axis in Tesla' desc['ccbrsp'] = 'computed external coil currents in Ampere' desc['cdflux'] = 'computed diamagnetic flux in Volt-sec' desc['chilibt'] = 'total χ2 Li beam' desc['chimse'] = 'total χ2 MSE' desc['chipre'] = 'total χ2 pressure' desc['chisq'] = 'total χ2 from magnetic probes, flux loops, Rogowskiand external coils ' desc['cj1ave'] = 'normalized average current density in plasma outer 5% normalized poloidal flux region ' desc['cjor0'] = 'normalized axial flux surface average current density' desc['cjor95'] = 'normalized flux surface average current density at 95% of normalized poloidal flux ' desc['cjor99'] = 'normalized flux surface average current density at 99% of normalized poloidal flux ' desc['cmpr2'] = '' desc['condno'] = 'Condition number' desc['cprof'] = 'current profile parametrization parameter' desc['csilop'] = 'computed flux loop signals in Weber' desc['dco2r'] = 'line average electron density in cm3 from radial CO2 chord' desc['dco2v'] = 'line average electron density in cm3 from vertical CO2 chord' desc['diamag'] = '' desc['diludom'] = 'distance between separatrix inner leg to upper dome in cm' desc['diludomm'] = 'distance between separatrix surface and upper dome at Rmin in cm' desc['dminlx'] = 'minimum distance between lower X point to limiter surface in cm' desc['dminux'] = 'minimum distance between upper X point to limiter surface in cm' desc['dolubaf'] = 'distance between separatrix outer leg to upper baffle in cm' desc['dolubafm'] = 'distance between separatrix surface and upper baffle at Rmax in cm' desc[ 'drsep' ] = 'outboard radial distance to external second separatrix in cm for single null configurations, > 0 for SNT, < 0 for SNB, defaults to 40 cm ' desc[ 'dsep' ] = '> 0 for minimum gap in cm in divertor configurations, < 0 absolute value for minimum distance to external separatrix in limiter configurations' desc['eccurt'] = 'measured E-coil current in Ampere' desc['elong'] = 'Plasma boundary elongation' desc['elongm'] = 'elongation at magnetic axis' desc['error'] = 'equilibrium convergence error' desc['fexpan'] = 'flux expansion at x point' desc['fexpvs'] = 'flux expansion at outer lower vessel hit spot' desc['fluxx'] = 'measured diamagnetic flux in Volt_sec' desc['gapbot'] = 'plasma bottom gap in cm' desc['gapin'] = 'plasma inner gap in cm' desc['gapout'] = 'plasma outer gap in cm' desc['gaptop'] = 'plasma top gap in cm' desc['indent'] = 'plasma boundary indentation' desc['ipmeas'] = 'measured plasma toroidal current in Ampere' desc['ipmhd'] = 'fitted plasma toroidal current in Ampere-turn' desc['jflag'] = 'error flag, 0 for error' desc['lflag'] = 'error flag, > 0 for error' desc[ 'limloc' ] = 'plasma configuration. IN, OUT, TOP, and BOT for limiter configurations limited at inside, outside, top, and bottom. SNT, SNB, and DN for single null top, single null bottom, and double null configurations. MAR for marginally diverted configurations. ' desc['li'] = 'li with normalization average poloidal magnetic defined through Ampere’s law ' desc['li3'] = 'li definition used by IMAS 2/R0/mu0^2/Ip^2 * int(Bp^2 dV)' desc['ltri'] = 'upper triangularity' desc['magpri0'] = '' desc['mco2r'] = 'number of radial CO2 density chords' desc['mco2v'] = 'number of vertical CO2 density chords' desc['nesum0'] = '' desc['nfcoil0'] = '' desc['nlnew'] = 'number of WRITE statements' desc['nlold'] = 'number of previous version WRITE statements' desc['nsilop0'] = '' desc['oring'] = 'gap bewtween plasma and slanted face in cm' desc['pbinj'] = 'neutral beam injection power in Watts' desc['peak'] = 'ratio of central pressure to average pressure' desc['pp95'] = 'normalized P’(ψ) at 95% normalized poloidal flux' desc['psim'] = 'boundary poloidal flux in Weber/rad at magnetic axis' desc['psin21'] = 'normalized ψ at q = 2 surface' desc['psin32'] = 'normalized ψ at q = 3/2 surface' desc['psiref'] = 'reference poloidal flux in VS/rad' desc['psurfa'] = 'plasma boundary surface area in m2' desc['qmerci'] = 'Mercier stability criterion on axial q(0), q(0) > QMERCI for stability' desc['qmflag'] = 'axial q(0) flag, FIX if constrained and CLC for float' desc['qout'] = 'q at plasma boundary' desc['q95'] = 'q at 95% of poloidal flux' desc['qm'] = 'axial safety factor q(0)' desc['qmin'] = 'minimum safety factor qmin' desc['qstar'] = 'equivalent safety factor q*' desc['ratsol'] = 'ratio of the 1 cm external field line distances to separatrix surfaceat Rmin and Rmax ' desc['rcencm'] = 'major radius in cm for vacuum field BCENTR' desc['rcntr'] = 'major radius of geometric center in cm' desc['rco2r'] = 'path length in cm of radial CO2 density chord' desc['rco2v'] = 'path length in cm of vertical CO2 density chord' desc['rcurrt'] = 'major radius in cm of current centroid' desc['rhoqmin'] = 'normalized radius of qmin , square root of normalized volume' desc['rm'] = 'major radius in cm at magnetic axis' desc['rmidin'] = 'inner major radius in m at Z=0.0' desc['rmidout'] = 'outer major radius in m at Z=0.0' desc['rq21top'] = 'Major radius in cm at maximum Z of q=2 surface' desc['rq32in'] = 'Minimum major radius in cm of q=3/2 surface' desc['rseps'] = 'major radius of x point in cm' desc['rttt'] = 'Shafranov boundary line integral parameter' desc['rvsid'] = 'major radius of lower vessel inner strike point in cm' desc['rvsin'] = 'major radius of vessel inner hit spot in cm' desc['rvsiu'] = 'major radius of upper vessel inner strike point in cm' desc['rvsod'] = 'major radius of lower vessel outer strike point in cm' desc['rvsou'] = 'major radius of upper vessel outer strike point in cm' desc['rvsout'] = 'major radius of vessel outer hit spot in cm' desc['s1'] = 'Shafranov boundary line integral' desc['s2'] = 'Shafranov boundary line integral' desc['s3'] = 'Shafranov boundary line integral' desc['sepbot'] = 'bottom gap of external second separatrix in cm' desc['sepexp'] = 'separatrix radial expansion in cm' desc['sepin'] = 'inner gap of external second separatrix in cm' desc['sepnose'] = 'radial distance in cm between x point and external field line at ZNOSE ' desc['septop'] = 'top gap of external second separatrix in cm' desc['sepout'] = 'outer gap of external second separatrix in cm' desc['shear'] = 'magnetic shear at 95% enclosed normalized poloidal flux' desc['shot'] = 'Machine specific shot number' desc['sibdry'] = 'plasma boundary poloidal flux in Weber/rad' desc['slantl'] = 'gap to lower outboard limiter in cm' desc['slantu'] = 'gap to upper outboard limiter in cm' desc['ssi01'] = 'magnetic shear at 1% of normalized poloidal flux' desc['ssi95'] = 'magnetic shear at 95% of normalized poloidal flux' desc['taudia'] = 'diamagnetic energy confinement time in ms' desc['taumhd'] = 'energy confinement time in ms' desc['tavem'] = 'average time in ms for magnetic and MSE data' desc['tchimls'] = '' desc['tflux'] = '' desc['time'] = 'time in ms' desc['twagap'] = '' desc['utri'] = 'upper triangularity' desc['vertn'] = 'vacuum field index at current centroid' desc['vloop'] = 'measured loop voltage in volt' desc['volume'] = 'plasma volume in m^3' desc['vsurf'] = 'not computed (always zero)' desc['wbdot'] = 'not computed (always zero)' desc['wdia'] = 'diamagnetic plasma stored energy in Joules' desc['wmhd'] = 'plasma stored energy in Joules' desc['wpdot'] = 'not computed (always zero)' desc['xbetapr'] = '' desc['xnnc'] = 'vertical stability parameter, vacuum field index normalized to critical index value ' desc['yyy2'] = 'Shafranov Y2 current moment' desc['zcntr'] = 'Z of geometric center in cm' desc['zcurrt'] = 'Z in cm at current centroid' desc['zm'] = 'Z in cm at magnetic axis' desc['zseps'] = 'Z of x point in cm' desc['zuperts'] = '' desc['zvsid'] = 'Z of lower vessel inner strike point in cm' desc['zvsin'] = 'Z of vessel inner hit spot in cm' desc['zvsiu'] = 'Z of upper vessel inner strike point in cm' desc['zvsod'] = 'Z of lower vessel outer strike point in cm' desc['zvsou'] = 'Z of upper vessel outer strike point in cm' desc['zvsout'] = 'Z of vessel outer hit spot in cm'
############################ # M-FILE CLASS OMFITmeqdsk # ############################
[docs]class OMFITmeqdsk(OMFITnc): r""" class used to interface M files generated by EFIT :param filename: filename passed to OMFITascii class :param \**kw: keyword dictionary passed to OMFITascii class """ signal_info = { # Calc measured weight uncert short_desc letter 'mag': ['cmpr2', 'expmpi', 'fwtmp2', None, 'magnetics', 'm'], # Magnetic probes 'ecc': ['cecurr', 'eccurt', 'fwtec', None, 'E-coil current', 'e'], # E-coil currents 'fcc': ['ccbrsp', 'fccurt', 'fwtfc', None, 'F-coil current', 'f'], # F-coil currents 'lop': ['csilop', 'silopt', 'fwtsi', None, 'flux loops', 'l'], # Flux loops 'pre': ['cpress', 'pressr', None, 'sigpre', 'pressure', 'p'], # Pressure constraint 'xxj': ['aux_calxxj', 'aux_mxxj', 'aux_fwtxxj', 'aux_sigxxj', 'current density', 'j'], # Current density 'gam': ['cmgam', 'tangam', None, 'siggam', 'MSE tan(gamma)', 'g'], # MSE tan(gamma) } # Full set of quantities is only available in EFIT-AI and rt-EFIT (as long as the old version is tested they can't be used here though # signal_info = { # Calc measured weight uncert short_desc letter # 'mag': ['cmpr2', 'expmpi', 'fwtmp2', 'sigmpi', 'magnetics', 'm'], # Magnetic probes # 'ecc': ['cecurr', 'eccurt', 'fwtec', 'sigecc', 'E-coil current', 'e'], # E-coil currents # 'fcc': ['ccbrsp', 'fccurt', 'fwtfc', 'sigfcc', 'F-coil current', 'f'], # F-coil currents # 'lop': ['csilop', 'silopt', 'fwtsi', 'sigsil', 'flux loops', 'l'], # Flux loops # 'ref': ['csiref', 'saisref', 'fwtref', 'sigref', 'ref loops', 'r'], # Reference flux loops # 'cur': ['cpasma', 'plasma', 'fwtcur', 'sigcur', 'plasma current', 'i'], # Plasma current # 'pre': ['cpress', 'pressr', 'fwtpre', 'sigpre', 'pressure', 'p'], # Pressure constraint # 'prw': ['cpresw', 'presw', 'fwtprw', 'sigprw', 'pressure_rotational', 'w'], # Rotational pressure constraint # 'jtr': ['aux_calxxj', 'vzeroj', 'aux_fwtxxj', 'aux_sigxxj', 'current density', 'j'], # Current density # 'gam': ['cmgam', 'tangam', 'fwtgam', 'siggam', 'MSE tan(gamma)', 'g'], # MSE tan(gamma) # } def __init__(self, filename, **kw): OMFITnc.__init__(self, filename, **kw)
[docs] def pack_it(self, x, tag, name, dim1=None, is_tmp=True): """ Utility function for saving results into the mEQDSK as new OMFITncData instances. :param x: array of data to pack :param tag: string (SHORT: dictionary key is derived from this) :param name: string (LONG: this is a description that goes in a field) :param dim1: string or None: name of dimension other than time :param is_tmp: bool: Choose OMFITncDataTmp() instead of OMFITncData() to prevent saving """ from omfit_classes.omfit_nc import OMFITncData, OMFITncDataTmp self[tag] = OMFITncDataTmp() if is_tmp else OMFITncData() self[tag]['data'] = np.array([x]) self[tag]['long_name'] = name self[tag]['__dtype__'] = np.dtype(type(np.atleast_1d(x).flatten()[0])) self[tag]['__dimensions__'] = ('dim_time', dim1) if dim1 is not None else ('dim_time',) return
def _get_signal_info(self, which): """Utility for pulling out signal_info while doing some checks; returns a list of items which are str or None""" info = self.signal_info.get(which, None) if info is None: printw('FAIL: Unrecognized normality test base quantity: {}'.format(which)) return None if info[0] not in self: printw('FAIL: {} not found. Unable to perform residual normality test.'.format(info[0])) if info[0].startswith('aux_'): printw('This an an auxiliary quantity. You must extend the mEQDSK before you can analyze it.') return None return info
[docs] def residual_normality_test(self, which='mag', is_tmp='some'): """ Tests whether residuals conform to normal distribution and saves a P-value. A good fit should have random residuals following a normal distribution (due to random measurement errors following a normal distribution). The P-value assesses the probability that the distribution of residuals could be at least as far from a normal distribution as are the measurements. A low P-value is indicative of a bad model or some other problem. https://www.graphpad.com/guides/prism/5/user-guide/prism5help.html?reg_diagnostics_tab_5_3.htm https://www.graphpad.com/support/faqid/1577/ :param which: string Parameter to do test on. Options: ['mag', 'ecc', 'fcc', 'lop', 'gam', 'pre', 'xxj'] :param is_tmp: string How many of the stats quantities should be loaded as OMFITncDataTmp (not saved to disk)? 'some', 'none', or 'all' """ # EFIT-AI and rt-EFIT extend the list of available test parameters # Parameter to do test on. Options: ['mag', 'ecc', 'fcc', 'cur', 'lop', 'ref', 'gam', 'pre', 'prw', 'jtr'] if which in ['pre', 'prw', 'xxj']: printw( 'WARNING: The test for normality of residual distribution for {} has been requested, but this ' 'quantity is an input constraint, not a measurement. Deviation between model and constraint is ' 'probably not due to normally distributed random errors in the constraint profile, so this test is ' 'not meaningful.'.format(which) ) naughty = ' This test is probably not meaningful for this quantity and should be ignored.' else: naughty = '' info = self._get_signal_info(which) if info is None: return calc = self[info[0]]['data'][0] meas = self[info[1]]['data'][0] if info[2] is None: uncert = self[info[3]]['data'][0] weight = np.zeros(len(uncert)) weight[uncert > 0] = 1.0 / uncert[uncert > 0] else: weight = self[info[2]]['data'][0] if np.all(weight == 0): printd('Skipping {} because all 0s'.format(which)) return if len(np.atleast_1d(weight)) == 1: weight = np.atleast_1d(weight)[0] * np.ones(np.shape(meas)) residual = (meas - calc) * weight residual[weight <= 0] = np.NaN fwt_flag = self[info[2]]['data'][0] if info[2] is not None else None if fwt_flag is None: pass elif len(np.atleast_1d(fwt_flag)) == len(residual): residual[fwt_flag <= 0] = np.NaN elif (len(np.atleast_1d(fwt_flag)) == 1) and (np.atleast_1d(fwt_flag) <= 0): residual[:] = np.NaN with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') try: normalstat, p_value = scipy.stats.normaltest(residual[weight > 0]) except ValueError as exc: printw('WARNING: residual normality test failed for {}: {}'.format(which, exc)) normalstat = p_value = np.NaN if np.any([str(wa.message).startswith('kurtosistest only valid') for wa in w]): # Make probability negative to indicate invalid test p_value *= -1 printd('Invalid result for p value of mag replicates') self.pack_it( residual.astype(np.float), 'stats_resid{}'.format(info[5]), 'Residual between {} and calculation'.format(info[4]), self[info[0]]['__dimensions__'][1], is_tmp=is_tmp != 'none', ) self.pack_it( normalstat, 'stats_nrs{}'.format(which), 'Combined normality statistic for {} residual, based on skewness and kurtosis.{}'.format(info[4], naughty), is_tmp=is_tmp != 'none', ) self.pack_it( p_value, 'stats_nrp{}'.format(which), 'P-value for normality of {} residuals: probability that residuals would be at least this far ' 'from normal distribution assuming correct model and actual measurements.{}'.format(info[4], naughty), is_tmp=is_tmp == 'all', ) return
[docs] def rsq_test(self, which='mag', is_tmp='some'): """ Calculates R^2 value for fit to a category of signals (like magnetics). The result will be saved into the mEQDSK instance as stats_rsq***. R^2 measures the fraction of variance in the data which is explained by the model and can range from 1 (model explains data) through 0 (model does no better than a flat line through the average) to -1 (model goes exactly the wrong way). https://www.graphpad.com/guides/prism/5/user-guide/prism5help.html?reg_diagnostics_tab_5_3.htm :param which: string (See residual_normality_test doc) :param is_tmp: string (See residual_normality_test doc) """ info = self._get_signal_info(which) if info is None: return calc = self[info[0]]['data'][0] meas = self[info[1]]['data'][0] fwt = self[info[2]]['data'][0] if info[2] is not None else None if fwt is not None and len(np.atleast_1d(fwt)) < len(calc): # Handle weird stuff like scalar weight for current density (FWTXXJ) fwt = np.atleast_1d(fwt)[0] * np.ones(len(calc)) uncert = self[info[3]]['data'][0] if info[3] is not None else None mask = np.ones(len(calc), bool) mask *= (fwt > 0) if fwt is not None else True mask *= (uncert > 0) if uncert is not None else True if uncert is not None: uncert_bad = uncert <= 0 uncert_nz = copy.copy(uncert) uncert_nz[uncert_bad] = 1 weight = 1.0 / uncert_nz weight[uncert_bad] = 0 weight = weight[mask] else: weight = fwt[mask] if fwt is not None else 1 if np.any(np.atleast_1d(mask)): sstot = np.sum(((meas[mask] - np.mean(meas[mask])) * weight) ** 2) ssres = np.sum(((meas[mask] - calc[mask]) * weight) ** 2) rsq = 1.0 - ssres / sstot else: sstot = ssres = np.NaN rsq = np.NaN self.pack_it( np.float32(sstot), 'stats_sst{}'.format(which), 'Sum of Squares, Total, for {}. Used for R^2 calc.'.format(info[4]), is_tmp=is_tmp != 'none', ) self.pack_it( np.float32(ssres), 'stats_ssr{}'.format(which), 'Sum of Squares, Residual, for {}. Used for R^2 calc.'.format(info[4]), is_tmp=is_tmp != 'none', ) self.pack_it( np.float32(rsq), 'stats_rsq{}'.format(which), 'R^2 of fit to {}: fraction of variance in {} explained by model'.format(info[4], info[4]), is_tmp=is_tmp == 'all', ) return
[docs] def combo_rsq_tests(self, is_tmp='some'): """ Combined R^2 from various groups of signals, including 'all'. Needs stats_sst* and stats_ssr* and will call rsq_test to make them if not already available. :param is_tmp: string (See residual_normality_test doc) """ groups = { 'all': self.signal_info.keys(), 'alm': ['mag', 'lop', 'ecc', 'fcc'], 'mes': ['mag', 'lop', 'gam', 'ecc', 'fcc'], 'con': ['pre', 'xxj'], 'coi': ['ecc', 'fcc'], } # EFIT-AI and rt-EFIT additions # groups = { # 'all': self.signal_info.keys(), # 'alm': ['mag', 'lop', 'ecc', 'fcc', 'cur'], # 'mes': ['mag', 'lop', 'gam', 'ecc', 'fcc', 'cur'], # 'con': ['pre', 'prw', 'jtr'], # 'coi': ['ecc', 'fcc'], # } descriptions = { 'all': 'all input data', 'alm': 'all magnetics data (probes, flux loops, coils)', # 'alm': 'all magnetics data (probes, flux loops, coils, current)', # EFIT-AI and rt-EFIT 'mes': 'all direct measurements (excluding kinetic constraint profiles)', 'con': 'all kinetic constraint profiles', 'coi': 'all coil currents', } for group, items in groups.items(): sst = 0 ssr = 0 for item in items: sst_sig, ssr_sig = 'stats_sst{}'.format(item), 'stats_ssr{}'.format(item) if sst_sig not in self or ssr_sig not in self: self.rsq_test(which=item, is_tmp=is_tmp) sst_inc = self.get(sst_sig, {}).get('data', [0])[0] ssr_inc = self.get(ssr_sig, {}).get('data', [0])[0] if (not np.isnan(sst_inc)) and (not np.isnan(ssr_inc)): sst += sst_inc ssr += ssr_inc rsq = (1.0 - ssr / sst) if sst > 0 else np.NaN # TODO: is NaN allowed, or should I just use 0 or -100? self.pack_it( np.float32(rsq), 'stats_rsq{}'.format(group), 'R^2 of fit to {}: fraction of variance explained by model'.format(descriptions[group]), is_tmp=is_tmp == 'all', )
[docs] def plot(self, **kw): """ Function used to plot chisquares stored in m-files This method is called by .plot() when the object is a m-file """ fig = pyplot.gcf() quants = ['saipre', 'saisil', 'saimpi', 'chigam'] kw.setdefault('marker', 'o') kw.setdefault('linestyle', '') for qi, q in enumerate(quants): ax = pyplot.subplot(2, 2, qi + 1) data = self[q]['data'].flatten() ax.plot(list(range(1, len(data) + 1)), data, label='$\\chi^2_{\\rm tot}=%3.3g$' % (np.sum(data)), **kw) ax.set_title(self[q]['long_name']) if qi in [2, 3]: ax.set_xlabel('Constraint number') if qi in [0, 2]: ax.set_ylabel('$\\chi^2$') try: pyplot.legend(labelspacing=0.1, loc=0).draggable(state=True) except Exception: pass
[docs] @dynaLoad def to_omas(self, ods=None, time_index=0, time_index_efit=0): """ translate mEQDSK class to OMAS data structure :param ods: input ods to which data is added :param time_index: time index to which data is added to ods :param time_index_efit: time index from mEQDSK :return: ODS """ if ods is None: ods = ODS() with omas_environment(ods, cocosio=1): if 'device' in self: ods['dataset_description.data_entry.machine'] = str(self['device']['data']).strip() device = tokamak(str(self['device']['data']).strip(), 'OMAS', True, {'nstx': 'nstxu'}) if 'shot' in self: shot = self['shot']['data'][time_index_efit] ods['dataset_description.data_entry.pulse'] = shot constr = ods['equilibrium.time_slice'][time_index]['constraints'] time = ods['equilibrium.time'][time_index] # Magnetic probes if 'saimpi' in self: nconstr = len(self['saimpi']['data'][time_index_efit, :]) for i in range(nconstr): constr['bpol_probe'][i]['chi_squared'] = self['saimpi']['data'][time_index_efit, i] constr['bpol_probe'][i]['measured'] = self['expmpi']['data'][time_index_efit, i] constr['bpol_probe'][i]['reconstructed'] = self['cmpr2']['data'][time_index_efit, i] constr['bpol_probe'][i]['weight'] = self['fwtmp2']['data'][time_index_efit, i] constr['bpol_probe'][i]['exact'] = 0 # Only in EFIT-AI and rt-EFIT version if 'sigmpi' in self: for i in range(nconstr): constr['bpol_probe'][i]['measured_error_upper'] = self['sigmpi']['data'][time_index_efit, i] # MSE signal # Is arctan correct translation of signal? # Even if it is, chi^2 probably needs to be recalculated if 'chigam' in self: nconstr = len(self['chigam']['data'][time_index_efit, :]) for i in range(nconstr): constr['mse_polarisation_angle'][i]['chi_squared'] = self['chigam']['data'][time_index_efit, i] constr['mse_polarisation_angle'][i]['measured'] = np.arctan(self['tangam']['data'][time_index_efit, i]) constr['mse_polarisation_angle'][i]['reconstructed'] = np.arctan(self['cmgam']['data'][time_index_efit, i]) constr['mse_polarisation_angle'][i]['weight'] = self['fwtgam']['data'][time_index_efit, i] constr['mse_polarisation_angle'][i]['exact'] = 0 constr['mse_polarisation_angle'][i]['measured_error_upper'] = np.arctan(self['siggam']['data'][time_index_efit, i]) # PSI loops if 'saisil' in self: nconstr = len(self['saisil']['data'][time_index_efit, :]) for i in range(nconstr): constr['flux_loop'][i]['chi_squared'] = self['saisil']['data'][time_index_efit, i] constr['flux_loop'][i]['measured'] = self['silopt']['data'][time_index_efit, i] constr['flux_loop'][i]['reconstructed'] = self['csilop']['data'][time_index_efit, i] constr['flux_loop'][i]['weight'] = self['fwtsi']['data'][time_index_efit, i] constr['flux_loop'][i]['exact'] = 0 # Only in EFIT-AI and rt-EFIT version if 'sigsil' in self: for i in range(nconstr): constr['flux_loop'][i]['measured_error_upper'] = self['sigsil']['data'][time_index_efit, i] # Reference PSI loops (not in IMAS, are these worth adding?) # Only in EFIT-AI and rt-EFIT version # if 'sairef' in self: # constr['reference_flux_loop']['chi_squared'] = self['saisref']['data'][time_index_efit] # constr['reference_flux_loop']['measured'] = self['psiref']['data'][time_index_efit] # constr['reference_flux_loop']['reconstructed'] = self['csiref']['data'][time_index_efit] # constr['reference_flux_loop']['weight'] = self['fwref']['data'][time_index_efit] # constr['reference_flux_loop']['measured_error_upper'] = self['sigref']['data'][time_index_efit] # constr['reference_flux_loop']['exact'] = 0 # Pressure profile if 'cpress' in self: nconstr = len(self['cpress']['data'][time_index_efit]) for i in range(nconstr): constr['pressure'][i]['chi_squared'] = self['saipre']['data'][time_index_efit, i] constr['pressure'][i]['measured'] = self['pressr']['data'][time_index_efit, i] constr['pressure'][i]['reconstructed'] = self['cpress']['data'][time_index_efit, i] constr['pressure'][i]['exact'] = 0 constr['pressure'][i]['measured_error_upper'] = self['sigpre']['data'][time_index_efit, i] # Only in EFIT-AI and rt-EFIT version if 'fwtpre' in self: for i in range(nconstr): constr['pressure'][i]['weight'] = self['fwtpre']['data'][time_index_efit, i] # Rotational pressure profile # Only in EFIT-AI version if 'cpresw' in self: nconstr = len(self['cpresw']['data'][time_index_efit]) for i in range(nconstr): constr['pressure_rotational'][i]['chi_squared'] = self['saiprw']['data'][time_index_efit, i] constr['pressure_rotational'][i]['measured'] = self['presw']['data'][time_index_efit, i] constr['pressure_rotational'][i]['reconstructed'] = self['cpresw']['data'][time_index_efit, i] constr['pressure_rotational'][i]['exact'] = 0 constr['pressure_rotational'][i]['measured_error_upper'] = self['sigprw']['data'][time_index_efit, i] constr['pressure_rotational'][i]['weight'] = self['fwtprw']['data'][time_index_efit, i] icoil = 0 icoil_ai = 0 # E-coils if 'eccurt' in self: nconstr = len(self['eccurt']['data'][time_index_efit]) for i in range(nconstr): constr['pf_current'][icoil]['measured'] = self['eccurt']['data'][time_index_efit, i] constr['pf_current'][icoil]['reconstructed'] = self['cecurr']['data'][time_index_efit, i] constr['pf_current'][icoil]['weight'] = self['fwtec']['data'][time_index_efit, i] constr['pf_current'][icoil]['exact'] = 0 icoil += 1 # Only in EFIT-AI and rg-EFIT version if 'sigecc' in self: for i in range(nconstr): constr['pf_current'][icoil_ai]['measured_error_upper'] = self['sigecc']['data'][time_index_efit, i] constr['pf_current'][icoil_ai]['chi_squared'] = self['chiecc']['data'][time_index_efit, i] icoil_ai += 1 # F-coils if 'fccurt' in self: nconstr = len(self['fccurt']['data'][time_index_efit]) for i in range(nconstr): # some devices have currents in A-turns, but IMAS wants A if 'device' in self and 'shot' in self: if 'nstx' in device or 'mast' in device: turns = 1 else: mapping = ODS() with mapping.open('machine', device, shot): turns = mapping[f'pf_active.coil[{icoil}].element[0].turns_with_sign'] else: turns = 1 constr['pf_current'][icoil]['measured'] = self['fccurt']['data'][time_index_efit, i] / turns constr['pf_current'][icoil]['reconstructed'] = self['ccbrsp']['data'][time_index_efit, i] / turns constr['pf_current'][icoil]['weight'] = self['fwtfc']['data'][time_index_efit, i] constr['pf_current'][icoil]['exact'] = 0 icoil += 1 # Only in EFIT-AI and rt-EFIT version if 'sigfcc' in self: for i in range(nconstr): constr['pf_current'][icoil_ai]['measured_error_upper'] = self['sigfcc']['data'][time_index_efit, i] / turns constr['pf_current'][icoil_ai]['chi_squared'] = self['chifcc']['data'][time_index_efit, i] icoil_ai += 1 # A coils # Only in EFIT-AI version if 'accurt' in self: nconstr = len(self['accurt']['data'][time_index_efit]) for i in range(nconstr): constr['pf_current'][icoil_ai]['measured'] = self['accurt']['data'][time_index_efit, i] constr['pf_current'][icoil_ai]['reconstructed'] = self['caccurt']['data'][time_index_efit, i] constr['pf_current'][icoil_ai]['exact'] = 0 icoil_ai += 1 # ip constr['ip.exact'] = 0 constr['ip.measured'] = self['plasma']['data'][time_index_efit] # Only in EFIT-AI and rt-EFIT version if 'sigpasma' in self: constr['ip.measured_error_upper'] = self['sigpasma']['data'][time_index_efit] constr['ip.weight'] = self['fwtpasma']['data'][time_index_efit] constr['ip.reconstructed'] = self['cpasma']['data'][time_index_efit] constr['ip.chi_squared'] = self['chipasma']['data'][time_index_efit] # Diamagnetic flux constr['diamagnetic_flux.exact'] = 0 constr['diamagnetic_flux.measured'] = self['diamag']['data'][time_index_efit] constr['diamagnetic_flux.measured_error_upper'] = self['sigdia']['data'][time_index_efit] # Only in EFIT-AI and rt-EFIT version if 'fwtdlc' in self: constr['diamagnetic_flux.weight'] = self['fwtdflux']['data'][time_index_efit] constr['diamagnetic_flux.reconstructed'] = self['cdflux']['data'][time_index_efit] constr['diamagnetic_flux.chi_squared'] = self['chidflux']['data'][time_index_efit] conv = ods['equilibrium.time_slice'][time_index]['convergence'] # Quality metrics i = self['cerror']['data'].shape[1] conv['iterations_n'] = i conv['grad_shafranov_deviation_expression.index'] = 3 conv['grad_shafranov_deviation_expression.name'] = 'max_absolute_psi_residual' conv[ 'grad_shafranov_deviation_expression.description' ] = 'Maximum absolute difference over the plasma poloidal cross-section of the poloidal flux between the current and preceding iteration, on fixed grid points' conv['grad_shafranov_deviation_value'] = self['cerror']['data'][time_index_efit, i - 1] # EFIT reports the total chi_squared, but IMAS only includes an entry for the reduced chi_squared (total divided by degrees of freedom) # According to the scientist who proposed it, the DOF should be approximated as the number of observations minus the number of model parameters (only documented in PR 4207, not in the schema) # Since this is not commonly used, we will set DOF=1 for now constr['chi_squared_reduced'] = self['cchisq']['data'][time_index_efit, i - 1] constr['freedom_degrees_n'] = 1 # Only in EFIT-AI version if 'chitot' in self: # include chi squared contribution from all constraints (not just magnetic and mse) constr['chi_squared_reduced'] = self['chitot']['data'][time_index_efit] elif 'chifin' in self: # better to use the updated chi squared value rather than the last iteration, when available constr['chi_squared_reduced'] = self['chifin']['data'][time_index_efit] # C coils # Only in EFIT-AI version if 'curc79' in self: ods['coils_non_axisymmetric']['coil'][0]['identifier'] = 'C79' ods.set_time_array('coils_non_axisymmetric.coil.0.current.time', time_index, time) ods.set_time_array('coils_non_axisymmetric.coil.0.current.data', time_index, self['curc79']['data'][time_index_efit]) ods['coils_non_axisymmetric']['coil'][1]['identifier'] = 'C139' ods.set_time_array('coils_non_axisymmetric.coil.1.current.time', time_index, time) ods.set_time_array('coils_non_axisymmetric.coil.1.current.data', time_index, self['curc139']['data'][time_index_efit]) ods['coils_non_axisymmetric']['coil'][2]['identifier'] = 'C199' ods.set_time_array('coils_non_axisymmetric.coil.2.current.time', time_index, time) ods.set_time_array('coils_non_axisymmetric.coil.2.current.data', time_index, self['curc199']['data'][time_index_efit]) # I coils # Only in EFIT-AI version if 'curi30' in self: ods['coils_non_axisymmetric']['coil'][3]['identifier'] = 'IU30' ods.set_time_array('coils_non_axisymmetric.coil.3.current.time', time_index, time) ods.set_time_array('coils_non_axisymmetric.coil.3.current.data', time_index, self['curiu30']['data'][time_index_efit]) ods['coils_non_axisymmetric']['coil'][4]['identifier'] = 'IU90' ods.set_time_array('coils_non_axisymmetric.coil.4.current.time', time_index, time) ods.set_time_array('coils_non_axisymmetric.coil.4.current.data', time_index, self['curiu90']['data'][time_index_efit]) ods['coils_non_axisymmetric']['coil'][5]['identifier'] = 'IU150' ods.set_time_array('coils_non_axisymmetric.coil.5.current.time', time_index, time) ods.set_time_array('coils_non_axisymmetric.coil.5.current.data', time_index, self['curiu150']['data'][time_index_efit]) ods['coils_non_axisymmetric']['coil'][6]['identifier'] = 'IL30' ods.set_time_array('coils_non_axisymmetric.coil.6.current.time', time_index, time) ods.set_time_array('coils_non_axisymmetric.coil.6.current.data', time_index, self['curil30']['data'][time_index_efit]) ods['coils_non_axisymmetric']['coil'][7]['identifier'] = 'IL90' ods.set_time_array('coils_non_axisymmetric.coil.7.current.time', time_index, time) ods.set_time_array('coils_non_axisymmetric.coil.7.current.data', time_index, self['curil90']['data'][time_index_efit]) ods['coils_non_axisymmetric']['coil'][8]['identifier'] = 'IL150' ods.set_time_array('coils_non_axisymmetric.coil.8.current.time', time_index, time) ods.set_time_array('coils_non_axisymmetric.coil.8.current.data', time_index, self['curil150']['data'][time_index_efit]) return ods
############################ # K-FILE CLASS OMFITkeqdsk # ############################
[docs]class OMFITkeqdsk(OMFITnamelist): r""" class used to interface with K files used by EFIT :param filename: filename passed to OMFITascii class :param \**kw: keyword dictionary passed to OMFITascii class """ linecycle = itertools.cycle(['--', '-.', ':']) markercycle = itertools.cycle(['d', 'o', '*', 'x', 'p']) def __init__(self, filename, **kw): kw0 = {} kw0['collect_arrays'] = {'__default__': 0} # Collect arrays to access array data consistently within OMFIT kw0['outsideOfNamelistIsComment'] = True kw0.update(kw) OMFITnamelist.__init__(self, filename, **kw0) # Takes care of setting up the class attributes for k in list(kw0.keys()): if k not in list(kw.keys()): self.OMFITproperties.pop(k, None)
[docs] @dynaLoad def load(self, *args, **kw): OMFITnamelist.load(self, *args, **kw) self.addAuxQuantities()
[docs] @dynaSave def save(self, *args, **kw): tmp = None if 'AuxQuantities' in self: tmp = self['AuxQuantities'] del self['AuxQuantities'] try: OMFITnamelist.save(self, *args, **kw) finally: if tmp is not None: self['AuxQuantities'] = tmp
############# # Utilities # #############
[docs] def remove_duplicates(self, keep_first_or_last='first', update_original=True, make_new_copy=False): """ Searches through all the groups in the k-file namelist (IN1, INS,EFITIN, etc.) and deletes duplicated variables. You can keep the first instance or the last instance. :param keep_first_or_last: string ('first' or 'last') - If there are duplicates, only one can be kept. Should it be the first one or the last one? :param update_original: bool Set False to leave the original untouched during testing. Use with make_new_copy. :param make_new_copy: bool Create a copy of the OMFITkeqdsk instance and return it. Useful if the original is not being modified. :return: None or OMFITkeqdsk instance (depending on make_new_copy) """ already = [] # list of variables we've seen before filename = self.filename.split('/')[-1] # name of current k-file without the whole path subnames = [ k for k in list(self.keys()) if isinstance(self[k], namelist.NamelistName) ] # names of namelist.NamelistNames within the top level namelist if make_new_copy: new = scratch['new_k_file'] = OMFITnamelist(filename) # make a new namelist to which we will copy original items other_top_level_items = [ k for k in list(self.keys()) if not isinstance(self[k], namelist.NamelistName) ] # other as in not the namelist.NamelistNames for otli in other_top_level_items: new[otli] = copy.copy(self[otli]) # copy across top level data/notes (not namelist.NamelistNames) else: new = None if keep_first_or_last.lower() == 'last': printd('going backward; the last instance of a tag will be the first one seen so it will be kept') direction = -1 # go backward, keeping the last instance of each tag else: # keep_first_or_last=='first' printd('going foward; the first instance of a tag will be the first one seen so it will be kept') direction = 1 # go forward, keeping the first instance of each tag (default) for sn in subnames[::direction]: if make_new_copy: new[sn] = namelist.NamelistName() # create sub-namelists for k in list(self[sn].keys())[::direction]: if not k.lower() in already: if make_new_copy: new[sn][k] = copy.copy(self[sn][k]) # save keys that haven't been seen before already = already + [k.lower()] # add key to the list of things we've seen already printd(' added {:}/{:}'.format(sn, k)) else: if update_original: del self[sn][k] # remove duplicate keys printd(' key {:}/{:} already seen'.format(sn, k)) if make_new_copy: return new else: return None
def _combine_namelists(self): """ Combines k-file sub-namelists together so we don't have to deal with finding something in IN1 or INWANT, especially because some items in INWANT can (apparently?) go in IN1 (is this right? Documentation is unclear, but this combo namelist will work either way) :return: NamelistName instance Combined namelists """ subnames = [k for k in list(self.keys()) if isinstance(self[k], namelist.NamelistName)] if len(subnames) > 1: combo = self[subnames[0]].copy() for sn in subnames[1:]: if sn.lower() != 'efitin': # Exclude efitin because it's just a record of the snap file (I think) combo.update(self[sn]) else: combo = namelist.NamelistName() return combo ################## # Aux quantities # ##################
[docs] def addAuxQuantities(self): """ Adds ['AuxQuantities'] to the current object :return: SortedDict object containing auxiliary quantities """ self['AuxQuantities'] = self._auxQuantities() return self['AuxQuantities']
def _auxQuantities(self, combo=None): """ Calculate auxiliary quantities based on the k-file contents :return: SortedDict object containing some auxiliary quantities """ c = self._combine_namelists() if combo is None else combo aux = SortedDict() # Fit variables nvarys = 0 # Look up fitting parameters https://fusion.gat.com/theory/Efitfunc#KPPCUR kffcur, kfffnc, kffknt = c.get('KFFCUR', 1), c.get('KFFFNC', 0), c.get('KFFKNT', 0) # Current density kppcur, kppfnc, kppknt = c.get('KPPCUR', 3), c.get('KPPFNC', 0), c.get('KPPKNT', 0) # Pressure kwwcur, kwwfnc, kwwknt = c.get('KWWCUR', 0), c.get('KWWFNC', 0), c.get('KWWKNT', 0) # Rotation nvarys += kffcur if kfffnc == 0 else 2 * kffknt + 1 if kfffnc == 6 else 0 nvarys += kppcur if kppfnc == 0 else 2 * kppknt + 1 if kppfnc == 6 else 0 nvarys += 2 * kwwknt + 1 if kwwfnc == 6 else 0 # Constraints nconstrain = 0 constrain_if_1 = ['FCURBD', 'PCURBD', 'FWTQA', 'FWTCUR'] for thing in constrain_if_1: # Logic based on Lao 2005 FST nconstrain += int(c.get(thing, 0) == 1) # Measurements / input data ndata = 0 for enable_flag in ['FWTMP2', 'FWTSI', 'FWTFC', 'FWTEC', 'FWTGAM', 'FWTPRE', 'FWTPRW']: ndata += np.sum(np.atleast_1d(c.get(enable_flag, 0) > -1).astype(int)) ndata += c.get('FWTXXJ', 0) * len(np.atleast_1d(c.get('VZEROJ', []))) # Record aux['num_input_data'] = int(ndata) aux['num_fit_variables'] = int(nvarys) aux['num_hard_constraints'] = int(nconstrain) aux['degrees_of_freedom'] = int(ndata - nvarys - nconstrain) avg_xxj_uncertainty = 0.1 # J profile is already normalized aux['sigxxj'] = np.ones(len(np.atleast_1d(c.get('VZEROJ', [0])))) * avg_xxj_uncertainty # TODO: clean up these notes once everything is figured out: # 0.1 or 0.01 of current (A) in the cell on axis # would like to have discrepancy in current density be <= 1% of average of J_efit_norm # check chi^2 assuming 1% of average current density return aux
[docs] def get_weights(self, fitweights, ishot=None): if ishot is None: ishot = self['IN1']['ISHOT'] fitshot = -1 for shot in fitweights: if shot < ishot: fitshot = shot print(fitshot) for item in fitweights[fitshot]: self['IN1'][item.upper()] = np.array(fitweights[fitshot][item])
[docs] def from_efitin(self): inwant_vars = ['NCCOIL', 'NICOIL', 'FITDELZ', 'IFITDELZ'] drop_vars = ['IAVEV'] for item in self['efitin']: if item.upper() in inwant_vars: self['INWANT'][item.upper()] = self['efitin'][item] elif item.upper() in drop_vars: continue else: self['IN1'][item.upper()] = self['efitin'][item] return self
################## # OMAS # ##################
[docs] def from_omas(self, ods, time_index=0, time=None): """ Generate kEQDSK from OMAS data structure. Currently this fuction just writes from code_parameters. In the future, parameters including ITIME,PLASMA,EXPMP2,COILS,BTOR,DENR,DENV, SIREF,BRSP,ECURRT,VLOOP,DFLUX,SIGDLC,CURRC79,CURRC139,CURRC199,CURRIU30,CURRIU90,CURRIU150, CURRIL30,CURRIL90, CURRIL150 should be specified from ods raw parameters. :param ods: input ods from which data is added :param time_index: time index from which data is added to ods :param time: time in seconds where to compare the data (if set it superseeds time_index) :return: ODS """ if time is not None: time_index = np.argmin(np.abs(ods['equilibrium.time'] - time)) time = int(np.round(ods['equilibrium.time'][time_index] * 1000)) if 'equilibrium.code.parameters' in ods: code_parameters = ods['equilibrium.code.parameters'] for items in code_parameters['time_slice'][time_index]: self[items.upper()] = namelist.NamelistName() for item in code_parameters['time_slice'][time_index][items]: self[items.upper()][item.upper()] = code_parameters['time_slice'][time_index][items][item] # figure out what index ranges are for OH coils Vs for PF coils names = ods['equilibrium.time_slice[0].constraints.pf_current.:.source'] koh = [k for k, n in enumerate(names) if (n.startswith("IOH") or n.startswith("OH") or n.startswith("E"))] koh = [koh[0], koh[-1] + 1] kpf = [k for k, n in enumerate(names) if not (n.startswith("IOH") or n.startswith("OH") or n.startswith("E"))] kpf = [kpf[0], kpf[-1] + 1] mappings = { 'oh': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.pf_current.{koh[0]}:{koh[-1]}.measured', 'kfile': ['ECURRT', 'BITEC', 'FWTEC'], 'scalar': False, }, 'pf_active': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.pf_current.{kpf[0]}:{kpf[-1]}.measured', 'kfile': ['BRSP', 'BITFC', 'FWTFC'], 'scalar': False, }, 'bpol_probe': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.bpol_probe.:.measured', 'kfile': ['EXPMP2', 'BITMPI', 'FWTMP2'], 'scalar': False, }, 'flux_loop': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.flux_loop.:.measured', 'kfile': ['COILS', 'PSIBIT', 'FWTSI'], 'scalar': False, }, 'ip': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.ip.measured', 'kfile': ['PLASMA', 'BITIP', 'FWTCUR'], 'scalar': True, }, 'diamagnetic_flux': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.diamagnetic_flux.measured', 'kfile': ['DFLUX', 'SIGDLC', 'FWTDLC'], 'norm': 1e3, 'scalar': True, }, 'b_field_tor_vacuum_r': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.b_field_tor_vacuum_r.measured', 'kfile': ['BTOR', '', ''], 'norm': 1 / ods['tf.r0'], 'scalar': True, }, } if 'IN1' not in self: self['IN1'] = namelist.NamelistName() self['IN1']['RCENTR'] = ods['tf.r0'] self['IN1']['ISHOT'] = ods['dataset_description.data_entry.pulse'] self['IN1']['ITIME'] = time with omas_environment(ods, cocosio=1): for k, (name, case) in enumerate(mappings.items()): if case['omas'] in ods: print(f"kEQDSK @ {time}ms: {case['kfile'][0]} \u00B1 {case['kfile'][2]} = {case['omas']}") # assign the values self['IN1'][case['kfile'][0]] = np.array(ods[case['omas']]) * case.get('norm', 1.0) # if this quantity handles uncertainties and weights if case['kfile'][2]: if case['omas'] + '_error_upper' in ods: self['IN1'][case['kfile'][1]] = ods[case['omas'] + '_error_upper'] * case.get('norm', 1.0) else: printe(f"Warning: Missing data in {case['omas'] + '_error_upper'} ") self['IN1'][case['kfile'][1]] = self['IN1'][case['kfile'][0]] * 0.0 # errors in k-file are BIT errors (whatever that means, there is a factor of 10) # NOTE: that's true for all entries but not the diamagnetic flux error if name != 'diamagnetic_flux': self['IN1'][case['kfile'][1]] *= 0.1 if case['kfile'][2] not in self['IN1']: self['IN1'][case['kfile'][2]] = np.ones(np.shape(self['IN1'][case['kfile'][0]])) # for array quantities set the weights to zero when there are NaN's if not isinstance(ods[case['omas']], float): self['IN1'][case['kfile'][2]] = self['IN1'][case['kfile'][2]][: len(self['IN1'][case['kfile'][0]])] tmp = np.isnan(self['IN1'][case['kfile'][0]]) tmp = tmp | np.isnan(self['IN1'][case['kfile'][1]]) tmp = tmp | np.isnan(self['IN1'][case['kfile'][2]]) self['IN1'][case['kfile'][0]][tmp] = 0.0 self['IN1'][case['kfile'][1]][tmp] = 0.0 self['IN1'][case['kfile'][2]][tmp] = 0.0 # turn scalars into simple floats if case['scalar']: for k in range(3): if case['kfile'][k] != '': self['IN1'][case['kfile'][k]] = float(self['IN1'][case['kfile'][k]]) else: printw(f"Could not set {name}: {case['kfile']} in kfile") # EFIT is setup for some devices to expect F-coil currents in A-turns, but IMAS uses A if 'machine' in ods['dataset_description.data_entry']: device = ods['dataset_description.data_entry.machine'] if not is_device(device, ['NSTX', 'NSTX-U', 'MAST', 'MAST-U']): for i in range(len(self['IN1']['BRSP'])): channel = kpf[i] if f'pf_active.coil.{channel}.element.0.turns_with_sign' in ods: self['IN1']['BRSP'][i] *= ods[f'pf_active.coil.{channel}.element.0.turns_with_sign'] self['IN1']['BITFC'][i] *= ods[f'pf_active.coil.{channel}.element.0.turns_with_sign'] else: print(f'WARNING: pf_active.coil[{channel}].element.0.turns_with_sign is missing') if f'equilibrium.time_slice.{time_index}.constraints.mse_polarisation_angle[0].measured' in ods: self['INS'] = namelist.NamelistName() self['INS']['KDOMSE'] = 0 self['INS']['RRRGAM'] = r = ods['mse.channel.:.active_spatial_resolution.0.centre.r'] self['INS']['ZZZGAM'] = z = ods['mse.channel.:.active_spatial_resolution.0.centre.z'] coeffs = ods[f'mse.channel.:.active_spatial_resolution.0.geometric_coefficients'] # # mapping between IMAS geometric_coefficients and EFIT AAxGAM # coeffs0: AA1 # coeffs1: AA8 # coeffs2: 0 # coeffs3: AA5 # coeffs4: AA4 # coeffs5: AA3 # coeffs6: AA2 # coeffs7: AA7 # coeffs8: AA6 # # mapping between EFIT AAxGAM and IMAS geometric_coefficients # AA1: coeffs0 # AA2: coeffs6 # AA3: coeffs5 # AA4: coeffs4 # AA5: coeffs3 # AA6: coeffs8 # AA7: coeffs7 # AA8: coeffs1 # AA9: does not exist # self['INS']['AA1GAM'] = coeffs[:, 0] self['INS']['AA2GAM'] = coeffs[:, 6] self['INS']['AA3GAM'] = coeffs[:, 5] self['INS']['AA4GAM'] = coeffs[:, 4] self['INS']['AA5GAM'] = coeffs[:, 3] self['INS']['AA6GAM'] = coeffs[:, 8] self['INS']['AA7GAM'] = coeffs[:, 7] self['INS']['IPLOTS'] = 1 self['INS']['TGAMMA'] = ods[f'equilibrium.time_slice.{time_index}.constraints.mse_polarisation_angle.:.measured'] self['INS']['SGAMMA'] = ods[f'equilibrium.time_slice.{time_index}.constraints.mse_polarisation_angle.:.measured_error_upper'] self['INS']['FWTGAM'] = z * 0 + 1 for k in ['TGAMMA', 'SGAMMA']: index = np.isnan(self['INS'][k]) self['INS'][k][index] = 0.0 self['INS']['FWTGAM'][index] = 0.0 return self
[docs] def to_omas(self, ods=None, time_index=0, time=None): """ Generate OMAS data structure from kEQDSK. Currently this fuction just reads code_parameters. In the future, parameters including ITIME,PLASMA,EXPMP2,COILS,BTOR,DENR,DENV, SIREF,BRSP,ECURRT,VLOOP,DFLUX,SIGDLC,CURRC79,CURRC139,CURRC199,CURRIU30,CURRIU90,CURRIU150, CURRIL30,CURRIL90, CURRIL150 should be written to ods raw parameters. :param ods: input ods to which data is added :param time_index: time index to which data is added to ods :param time: time in seconds where to compare the data (if set it superseeds time_index) :return: ODS """ if ods is None: ods = ODS() if time is not None: time_index = np.argmin(np.abs(ods['equilibrium.time'] - time)) time = int(np.round(ods['equilibrium.time'][time_index] * 1000)) code_parameters = ods['equilibrium.code.parameters'] if 'time_slice' not in code_parameters: code_parameters['time_slice'] = ODS() if time_index not in code_parameters['time_slice']: code_parameters['time_slice'][time_index] = ODS() for items in self: if '__comment' not in items: code_parameters['time_slice'][time_index][items.lower()] = ODS() for item in self[items]: code_parameters['time_slice'][time_index][items.lower()][item.lower()] = self[items.upper()][item.upper()] return ods
[docs] def compare_omas_constraints(self, ods, time_index=None, time=None, plot_invalid=False): """ Plots comparing constraints in the kEQDSK with respect to what are in an ODS :param ods: ods to use for comparison :param time_index: force time_index of the ods constraint to compare :param time: force time in seconds where to compare the ods data (if set it superseeds time_index) :param plot_invalid: toggle plotting of data points that are marked to be in kfile :return: figure handler """ from omfit_classes import utils_plot if time is None and time_index is None: time = self['IN1']['ITIME'] / 1000.0 if time is not None: time_index = np.argmin(np.abs(ods['equilibrium.time'] - time)) time = int(np.round(ods['equilibrium.time'][time_index] * 1000)) checks = { 'pf_active': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.pf_current.:.measured', 'kfile': ['BRSP', 'BITFC', 'FWTFC'], 'names': 'pf_active.coil[:].name', }, 'bpol_probe': { 'omas': f'equilibrium.time_slice.{time_index}..constraints.bpol_probe.:.measured', 'kfile': ['EXPMP2', 'BITMPI', 'FWTMP2'], 'names': 'magnetics.b_field_pol_probe[:].name', }, 'flux_loop': { 'omas': f'equilibrium.time_slice.{time_index}..constraints.flux_loop.:.measured', 'kfile': ['COILS', 'PSIBIT', 'FWTSI'], 'names': 'magnetics.flux_loop[:].name', }, 'mse_polarisation_angle': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.mse_polarisation_angle.:.measured', 'kfile': ['TGAMMA', 'SGAMMA', 'FWTGAM'], 'names': 'mse.channel[:].name', }, 'ip': {'omas': f'equilibrium.time_slice.{time_index}.constraints.ip.measured', 'kfile': ['PLASMA', 'BITIP', 'FWTCUR']}, 'diamagnetic_flux': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.diamagnetic_flux.measured', 'kfile': ['DFLUX', 'SIGDLC', 'FWTDLC'], 'norm': 1e3, }, 'b_field_tor_vacuum_r': { 'omas': f'equilibrium.time_slice.{time_index}.constraints.b_field_tor_vacuum_r.measured', 'kfile': ['BTOR', '', ''], 'norm': 1 / ods['tf.r0'], }, } fig = pyplot.figure(num='kfile - omas comparison') fig.clf() for k, (name, case) in enumerate(list(checks.items())[:4]): try: odata = nominal_values(ods[case['omas']]) oerr = nominal_values(ods[case['omas'] + '_error_upper']) n = len(odata) for where in ['IN1', 'INS']: if where in self and case['kfile'][0] in self[where]: kdata = copy.deepcopy(self[where][case['kfile'][0]])[:n] kerr = copy.deepcopy(self[where][case['kfile'][1]])[:n] break # errors in k-file are BIT errors (whatever that means, there is a factor of 10) # NOTE: that's true for all entries but not the diamagnetic flux error kerr = kerr * 10.0 if not plot_invalid: kvalid = copy.deepcopy(self[where][case['kfile'][2]])[:n] kdata[kvalid == 0] = np.nan kerr[kvalid == 0] = np.nan # regression ax = pyplot.subplot(2, 4, k + 1, aspect='equal') ax.set_title(name.replace('_', ' ')) utils_plot.axdline(color='r', ax=ax) ax.errorbar(kdata, odata, yerr=oerr, xerr=kerr, marker='.', ls='') ax.set_aspect(1.0 / ax.get_data_ratio(), adjustable='box') ax.set_xlabel('kfile') ax.set_ylabel('omas') if 'names' in case and case['names'] in ods: utils_plot.infoScatter(kdata, odata, ods[case['names']]) # error histograms ax = pyplot.subplot(2, 4, 4 + k + 1) ax.hist((odata - kdata) / np.sqrt(kdata**2 + odata**2 + oerr**2 + kerr**2) * 100, 31) ax.set_aspect(1.0 / ax.get_data_ratio(), adjustable='box') ax.set_xlabel('% relative error') except Exception as _excp: print(f"error in comparison of {name}: " + repr(_excp)) # plasma current print('plasma current:') try: print( f" * omas = {ods[f'equilibrium.time_slice.{time_index}.constraints.ip.measured']:8.8} \u00b1 {ods[f'equilibrium.time_slice.{time_index}.constraints.ip.measured_error_upper']:8.8} [A]" ) print(f" * kfile = {self['IN1']['PLASMA']:8.8} \u00b1 {self['IN1']['BITIP'] * 10:8.8} [A]") except Exception as _excp: print("error in comparison: " + repr(_excp)) # diamagnetic flux print('diamagnetic flux:') try: print( f" * omas = {ods[f'equilibrium.time_slice.{time_index}.constraints.diamagnetic_flux.measured']:8.8} \u00b1 {ods[f'equilibrium.time_slice.{time_index}.constraints.diamagnetic_flux.measured_error_upper']:8.8} [Wb]" ) print(f" * kfile = {self['IN1']['DFLUX'] / 1E3:8.8} \u00b1 {self['IN1']['SIGDLC'] / 1E3:8.8} [Wb]") except Exception as _excp: print("error in comparison: " + repr(_excp)) # toroidal vacuum field * R print('toroidal vacuum field * R:') try: print( f" * omas = {ods[f'equilibrium.time_slice.{time_index}.constraints.b_field_tor_vacuum_r.measured']:8.8} \u00b1 {ods[f'equilibrium.time_slice.{time_index}.constraints.b_field_tor_vacuum_r.measured_error_upper']:8.8} [T*m]" ) print(f" * kfile = {self['IN1']['BTOR'] * self['IN1']['RCENTR']:8.8} \u00b1 0.0 [T*m]") except Exception as _excp: print("error in comparison: " + repr(_excp)) return fig
################## # Plot utilities # ################## def _bad_plot(self, name=None, ax=None, in1=None, **kw): """ Placeholder / indicator for bad plots. Used when one of the requested subplots can't be displayed due to missing data. :param name: string Name of the thing that had a problem leading to the need to indicate a problem with this placeholder plot :param ax: Axes instance :param in1: [Optional] dict-like Defaults to the result of self._combine_namelists() """ in1 = self._combine_namelists() if in1 is None else in1 ax = pyplot.gca() if ax is None else ax if len(ax.lines) == 0: # If this is just an empty plot with a warning message, it looks nicer to suppress the tick labels ax.tick_params(axis='both', which='both', bottom=False, top=False, right=False, left=False, labelbottom=False, labelleft=False) ax.text( 0.5, 0.5, 'Unable to find k-file data for topic: {:}\n shot {:} @ {:} ms'.format(name, in1['ISHOT'], in1['ITIME']), transform=ax.transAxes, ha='center', va='center', color='red', ) return def _undo_bad_plot(self, ax=None): """ Reverses style changes made by bad_plot. Use when overlaying good data onto a plot that has a notification about missing data :param ax: Axes instance """ ax = pyplot.gca() if ax is None else ax ax.tick_params(axis='both', which='major', bottom=True, top=True, right=True, left=True, labelbottom=True, labelleft=True) def _shot_time_label(self, label='', combo=None): """ Provides a generic tag for displaying shot and time in labels in the legends :param label: string :param combo: [optional] dict-like Defaults to self._combine_namelists() :return: string Blank label or label with shot, time, and some description """ if label is None: return '' # disable by setting label=None combo = self._combine_namelists() if combo is None else combo s = combo['ISHOT'] t = combo['ITIME'] if label == '': return '{} {} ms'.format(s, t) return ' {:}, {:} {:} ms'.format(label, s, t) def _keyword_setup(self, in1, ax, label): """Avoid duplication of common plot keyword setup junk""" in1 = self._combine_namelists() if in1 is None else in1 # Make sure we have the combined namelist ax = pyplot.gca() if ax is None else ax self._undo_bad_plot(ax=ax) stlab = self._shot_time_label(label=label, combo=in1) stlab2 = ' [{:}]'.format(stlab) if stlab else '' # Make secondary shot time label w/ [ ] around it return in1, ax, stlab, stlab2 ########################### # Specific plot functions # ###########################
[docs] def plot_press_constraint( self, in1=None, fig=None, ax=None, label='', color=None, no_extra_info_in_legend=False, no_legend=False, no_marks_for_uniform_weight=True, ): """ Plots pressure constrait in kEQDSK. For general information on K-FILES, see - https://fusion.gat.com/theory/Efitinputs - https://fusion.gat.com/theory/Efitin1 Specific quantities related to extracting the pressure profile ------ KPRFIT: kinetic fitting mode: 0 off, 1 vs psi, 2 vs R-Z, 3 includes rotation NPRESS: number of valid points in PRESSR; positive number: number of input data, negative number: read in data from EDAT file, 0: rotational pressure only RPRESS: -: input pressure profile as a function of dimensionless fluxes (psi_N), +: R coordinates of input pressure profile in m ZPRESS: gives Z coordinates to go with R coordinates in RPRESS if RPRESS>0 PRESSR: pressure in N/m^2 (or Pa) vs. normalized flux (psi_N) for fitting PRESSBI: pressure at boundary SIGPREBI: standard deviation for pressure at boundary PRESSBI KPRESSB: 0: don't put a pressure point at boundary (Default), 1: put a pressure point at the boundary Specific quantities related to understanding KNOTS & basis functions ------ KPPFNC basis function for P': 0 = polynomial, 6 = spline KPPCUR number of coefficients for poly representation of P', ignored if spline. Default = 3 KPPKNT number of knots for P' spline, ignored unless KPPFNC=6 PPKNT P' knot locations in psi_N, vector length KPPKNT, ignored unless KPPFNC=6 PPTENS spline tension for P'. Large (like 10) ---> approaches piecewise linear. small (like 0.1) ---> like a cubic spline KPPBDRY constraint switch for P'. Vector of length KPPKNT. Ignored unless KPPFNC=6 PPBDRY values of P' at each knot location where KPPBDRY=1 KPP2BDRY on/off for PP2BDRY PP2BDRY values of (P')' at each knot location where KPP2BDRY=1 :param in1: NamelistName instance :param fig: Figure instance (unused, but accepted to maintain consistent format) :param ax: Axes instance :param label: string :param color: Matplotlib color specification :param no_extra_info_in_legend: bool :param no_legend: bool :param no_marks_for_uniform_weight: bool :return: Matplotlib color specification """ from matplotlib import pyplot in1, ax, stlab, stlab2 = self._keyword_setup(in1, ax, label) # Get the X,Y values of the constraint profile try: rpress = in1['RPRESS'] # Position basis for pressure constraint (psi_N, stored as - OR R in m, stored as +) press = in1['PRESSR'] # Pressure constraint (Pa) except KeyError: self._bad_plot('pressure', ax=ax) printw('Could not find pressure constraint in K-file IN1: RPRESS and PRESSR missing') return color # Decide if we're plotting vs. R or vs. psi_N if np.mean(rpress) <= 0: # vs psi w = rpress <= 0 # Filter to select only R-type measurements, just in case there's a mix of psi and R. x = -rpress[w] xlab = r'$\psi_N$' boundary = 1 xrangemin = 0 else: # vs R w = rpress > 0 x = rpress[w] xlab = '$R$ (m)' xrangemin = min(x) boundary = None # Don't know where the boundary is press = press[w] npress = in1.get('NPRESS', len(press)) # Get the number of constraint points error = in1.get('SIGPRE', None) # Get 1 sigma uncertainty in pressure constraint (Pa) weight = in1.get('FWTPRE', np.ones(npress)) # Weighting factor (in addition to sigma) for press, defaults to 1 label = 'Constraint{:}, {:} = {:}'.format(stlab, r'$\overline{weight}$', np.mean(weight)) # Check for and get boundary pressure constraint # kpressb = in1.get('KPRESSB', 0) # Boundary pressure constraint on/off pressbi = in1.get('PRESSBI', None) # Boundary pressure constraint value sigprebi = in1.get('SIGPREBI', 0) # Boundary pressure constraint uncertainty # Draw the main plot if error is None: line = ax.plot(x, press, '.-', label=label, color=color) else: line = ax.errorbar(x, press, error, label=label, color=color) color = line[0].get_color() if color is None else color ax.set_xlim(xrangemin) # Although uncertainty factors into the weight pretty well, there is also another weight. # Display the other weight with different sized diamonds, but only if there is something interesting to see if weight is not None and ((np.std(weight) > 0) or (no_marks_for_uniform_weight is False)): for i in range(np): lab = 'Area of diamonds is proportional to fit weight' if i == 0 else '' ax.plot( x[i], press[i], linestyle=' ', marker='d', markersize=np.sqrt(weight[i]) * 10, label=lab, markeredgecolor='k', color='none', ) ax.legend(loc=0).draggable() # Mark the boundary pressure constraint if there is one if pressbi is not None: if boundary is None: ax.axhline(pressbi, linestyle='--', color='k', label='Boundary pressure constraint' + stlab) else: ax.errorbar(boundary, pressbi, sigprebi, marker='x', label='Boundary pressure constraint' + stlab, color=color) # Mark knot locations if in1.get('kppfnc', 0) == 6: xknot = in1.get('ppknt', 0) vlab = 'Knot locations{:}'.format(stlab) ls = next(self.linecycle) mark = next(self.markercycle) for xk in xknot: ax.axvline(xk, linestyle=ls, color=color, label=vlab, marker=mark) vlab = '' # Finish up with labels and a legend ax.set_xlabel(xlab) ax.set_ylabel('$p_{kin}$ (Pa)') ax.set_title('EFIT k-file kinetic pressure constraint') if not no_extra_info_in_legend: # Add some extra labels to go on the legend extra_info = [] extra_info += ( ['Kinetic fitting mode{:}: {:}'.format(stlab, ['off', r'P($\psi_N$)', 'P(R,Z)', 'Include rotation'][in1['KPRFIT']])] if 'KPRFIT' in in1 else [] ) for ei in extra_info: ax.plot(-1, np.mean(press), color=color, label=ei, linestyle=' ', marker='s') if not no_legend: ax.legend(loc=0, numpoints=1).draggable() return color
[docs] def plot_fast_ion_constraints( self, in1=None, fig=None, ax=None, label='', color=None, density=False, no_extra_info_in_legend=False, no_legend=False ): """ Documentation on fast ion information in K-FILES: https://fusion.gat.com/theory/Efitin1 https://fusion.gat.com/theory/Efitinputs --- KPRFIT: kinetic fitting mode: 0 off, 1 vs psi, 2 vs R-Z, 3 includes rotation NBEAM: number of points for beam data in kinetic mode (in vector DNBEAM) DNBEAM: beam particle density for kinetic EFIT PBEAM: beam pressure in Pa vs psi_N for kinetic fitting PNBEAM: defaults to 0. That is all we know SIBEAM: psi_N values corresponding to PBEAM :param in1: NamelistName :param fig: Figure instance (unused) :param ax: Axes instance :param label: string :param color: Matplotlib color spec :param density: bool :param no_extra_info_in_legend: bool :param no_legend: bool :return: Matplotlib color spec """ in1, ax, stlab, stlab2 = self._keyword_setup(in1, ax, label) try: x = in1['SIBEAM'] # psi_N for beam pressure and density (fast ion pressure and density) except KeyError: self._bad_plot('fast ions', ax=ax) printw('Could not find SIBEAM: psi_N coordinates for fast ion constraint profiles in k-file') return color if density: # Plot fast ion density try: y = in1['DNBEAM'] # Beam density in m^-3 except KeyError: self._bad_plot('fast ion density', ax=ax) printw('Could not find fast ion density DNBEAM in k-file') return color ax.set_ylabel('$n_{fast}$ (m$^{-3}$)') ax.set_title('EFIT k-file fast ion density constraint') else: # Plot fast ion pressure try: y = in1['PBEAM'] # Beam pressure in Pa except KeyError: self._bad_plot('fast ion pressure', ax=ax) printw('Could not find fast ion press PBEAM in k-file') return color ax.set_ylabel('$p_{fast}$ (Pa)') ax.set_title('EFIT k-file fast ion pressure constraint') ax.set_xlabel(r'$\psi_N$') line = ax.plot(x, y, '.-', label='Constraint{:}'.format(stlab), color=color) color = line[0].get_color() if color is None else color if not no_legend: ax.legend(loc=0).draggable() return color
[docs] def plot_current_constraint(self, in1=None, fig=None, ax=None, label='', color=None, no_extra_info_in_legend=False, no_legend=False): """ K-FILES see documentation on IN1 namelist in k-file: https://fusion.gat.com/theory/Efitin1 see https://fusion.gat.com/theory/Efitinputs KZEROJ: constrain FF' and P' by applying constraints specified by RZEROJ >0: number of constraints to apply 0: don't apply constraints (default) SIZEROJ: vector of locations at which Jt is constrainted when KZEROJ>0. When KZEROJ=1, PSIWANT can be used instead of SIZEROJ(1) by setting SIZEROJ(1)<0 see KZEROJ, RZEROJ, VZEROJ, PSIWANT default SIZEROJ(1)=-1.0 RZEROJ: vector of radii at which to apply constraints. For each element in vector & corresponding elements in SIZEROJ, VZEROJ, if RZEROJ>0: set Jt=0 @ coordinate RZEROJ,SIZEROJ RZEROJ=0: set flux surface average current equal to VZEROJ @ surface specified by normalized flux SIZEROJ RZEROJ<0: set Jt=0 @ separatrix applied only if KZEROJ>0. Default RZEROJ(1)=0.0 If KZEROJ=1, may specify SIZEROJ(1) w/ PSIWANT. If KZEROJ=1 and SIZEROJ(1)<0 then SIZEROJ(1) is set equal to PSIWANT PSIWANT: normalized flux value of surface where J constraint is desired. See KZEROJ, RZEROJ, VZEROJ. Default=1.0 VZEROJ: Desired value(s) of normalized J (w.r.t. I/area) at the flux surface PSIWANT (or surfaces SIZEROJ). Must have KZEROJ = 1 or >1 and RZEROJ=0.0. Default=0.0 summary: you should set k to some number of constraint points, then use the SIZEROJ and VZEROJ vectors to set up the psi_N and Jt values at the constraint points KNOTS & basis functions KFFFNC basis function for FF': 0 polynomial, 6 = spline ICPROF specific choice of current profile: 0 = current profile is not specified by this variable, 1 = no edge current density allowed 2 = free edge current density 3 = weak edge current density constraint KFFCUR number of coefficients for poly representation of FF', ignored if spline. Default = 1 KFFKNT number of knots for FF'. Ignored unless KFFFNC=6 FFKNT knot locations for FF' in psi_N, vector length should be KFFKNT. Ignored unless kfffnc=6 FFTENS spline tension for FF'. Large (like 10) ---> approaches piecewise linear. small (like 0.1) ---> like a cubic spline KFFBDRY constraint switch for FF' (0/1 off/on) for each knot. default to zeros FFBDRY value of FF' for each knot, used only when KFFBDRY=1 KFF2BDRY: on/off (1/0) switch for each knot FF2BDRY value of (FF')' for each knot, used only when KFF2BDRY=1 :param in1: NamelistName :param fig: Figure :param ax: Axes :param label: string :param color: Matplotlib color spec :param no_extra_info_in_legend: bool :param no_legend: bool :return: Matplotlib color spec """ in1, ax, stlab, stlab2 = self._keyword_setup(in1, ax, label) n = in1.get('KZEROJ', 0) # Get number of points if n == 0: printw('Current constraints seem to be turned off') self._bad_plot('current', ax=ax) return color # Stop this because the current constraints are turned off # Get psi_N (except apparently sometimes this variable means Z in m instead of psi_N) if 'SIZEROJ' in in1: psin = in1['SIZEROJ'] if n == 1 and psin[0] < 0: # Use psiwant instead of sizeroj[0] psin[0] = in1['PSIWANT'] if 'PSIWANT' in in1 else 1.0 else: psin = np.array([-1.0]) r = in1.get('RZEROJ', np.array([0.0])) # Get major radius in m (not always used) current = in1.get('VZEROJ', 0.0) # Get current @ constraint points given by # psi_N (SIZEROJ) or R,Z (RZEROJ,SIZEROJ) weight = in1.get('FWTCUR', 1.0) # Get the weight of the current constraint # Decide if the current constraint is vs. psi_N or vs. R,Z if r[0] == 0: x = psin y = current xlab = r'$\psi_N$' xrangemin = 0 elif r[0] > 0: # This mode sets current to zero at R,Z defined by r,SIZEROJ # (SIZEROJ is normally psi_N but this time it's Z I guess?) x, z = r, psin y = current * 0 xlab = '$R$ (m)' xrangemin = min(r) else: # r[0] < 0: # Set Jt @separatrix x = 1.0 # Assume it's psi I guess y = 0 xlab = r'$\psi_N$' xrangemin = 0 # Plot the constraint and apply labels line = ax.plot(x, y, '.-', label='Constraint{:}, weight = {:}'.format(stlab, weight), color=color) color = line[0].get_color() if color is None else color ax.set_xlabel(xlab) ax.set_ylabel('$J_t$ $/$ $(I_p/Area)$') ax.set_title('EFIT k-file normalized toroidal current constraint') ax.set_xlim(xrangemin) # Mark knot locations if in1.get('kppfnc', 0) == 6: # Spline xknot = in1.get('ffknt', 0) vlab = 'Knot locations{:}'.format(stlab) ls = next(self.linecycle) mark = next(self.markercycle) for xk in xknot: ax.axvline(xk, linestyle=ls, color=color, label=vlab, marker=mark) vlab = '' if not no_extra_info_in_legend: # Add some extra labels to go on the legend extra_info = [] extra_info += ['$B_T = {:0.2f}$ T'.format(in1['BTOR'])] if 'BTOR' in in1 else [] extra_info += ['$I_p = {:0.2f}$ MA'.format(in1['PLASMA'] / 1e6)] if 'PLASMA' in in1 else [] for ei in extra_info: ax.plot(-1, np.mean(y), color=color, label='{:}{:}'.format(ei, stlab2), linestyle=' ', marker='s') if not no_legend: ax.legend(loc=0, numpoints=1).draggable() return color
[docs] def plot_mse(self, in1=None, fig=None, ax=None, label='', color=None, no_extra_info_in_legend=False, no_legend=False): """ K-FILES plot MSE constraints see https://fusion.gat.com/theory/Efitinputs RRRGAM R in meters of the MSE observation point ZZZGAM Z in meters of the MSE observation point TGAMMA "tangent gamma". Tangent of the measured MSE polarization angle, TGAMMA=(A1*Bz+A5*Er)/(A2*Bt+...) SGAMMA standard deviation (uncertainty) for TGAMMA FWTGAM "fit weight gamma": 1/0 on/off switches for MSE channels DTMSEFULL full width of MSE dat time average window in ms AA#GAM where # is 1,2,3,...: geometric correction coefficients for MSE data, generated by EFIT during mode 5 :param in1: NamelistName instance :param fig: Figure instance :param ax: Axes instance :param label: string :param color: Matplotlib color spec :param no_extra_info_in_legend: bool :param no_legend: bool :return: Matplotlib color spec """ in1, ax, stlab, stlab2 = self._keyword_setup(in1, ax, label) try: r = in1['RRRGAM'] tgamma = in1['TGAMMA'] except KeyError: printw('Could not find MSE data in k-file') self._bad_plot('MSE', ax=ax) return color # z = in1.get('ZZZGAM', r*0) sgamma = in1.get('SGAMMA', tgamma * 0) # Default to no uncertainty (no error bars displayed) fwtgam = in1.get('FWTGAM', tgamma * 0) # Default to all off (fail) nw = min([len(sgamma), len(fwtgam)]) w = fwtgam.astype(bool)[:nw] # Mask if nw < len(sgamma): # Make sure w is long enough to cover tgamma and sgamma. If it is not, turn off any extra channels. w = np.append(w, np.zeros(len(sgamma) - nw, bool)) if max(w) == 0: printw('all MSE channels are turned off') self._bad_plot('MSE [all channels are off]', ax=ax) return color line = ax.errorbar(r[w], tgamma[w], sgamma[w], label='Constraint{:}'.format(stlab), color=color) color = line[0].get_color() if color is None else color ax.set_xlabel('R (m)') ax.set_ylabel(r'tan($\gamma$)') ax.set_title('EFIT k-file MSE constraint') if not no_legend: ax.legend(loc=0).draggable() return color
[docs] def plot_mass_density(self, combo=None, fig=None, ax=None, label='', color=None, no_legend=False): """ K-files plot mass density profile see https://fusion.gat.com/theory/Efitin1 NMASS: number of valid points in DMASS DMASS: density mass. Mass density in kg/m^3 I am *ASSUMING* that this uses RPRESS (psi or R_major for pressure constraint) to get the position coordinates :param combo: NamelistName instance :param fig: Figure instance :param ax: Axes instance :param label: string :param color: mpl color spec :param no_legend: bool :return: Matplotlib color spec """ combo, ax, stlab, stlab2 = self._keyword_setup(combo, ax, label) # Get the X,Y values of the constraint profile try: rpress = combo['RPRESS'] # Position basis for pressure constraint # (psi_N, stored as negative OR R in m, stored as positive) dmass = combo['DMASS'] # Density constraint (kg/m^3) #assumed to correspond to rpress except KeyError: self._bad_plot('mass density', ax=ax) printw('Could not find density constraint in K-file IN1: RPRESS or DMASS missing') return color # Decide if we're plotting vs. R or vs. psi_N if np.mean(rpress) <= 0: # vs psi w = rpress <= 0 x = -rpress[w] xlab = r'$\psi_N$' xrangemin = 0 else: # vs R w = rpress > 0 x = rpress[w] xlab = '$R$ (m)' xrangemin = min(x) dmass = dmass[w] # Just in case there was a mixture, we'll pick the dominant group # (I don't know if mixtures are even allowed) line = ax.plot(x, dmass, '.-', label='Constraint{:}'.format(stlab)) # Not sure how this works as a constraint color = line[0].get_color() if color is None else color ax.set_xlim(xrangemin) ax.set_xlabel(xlab) ax.set_ylabel(r'$\rho$ (kg/m$^3$)') ax.set_title('EFIT k-file mass density profile') if not no_legend: ax.legend(loc=0).draggable() return color
[docs] def plot_pressure_and_current_constraints( self, in1=None, fig=None, ax=None, label='', color=None, no_extra_info_in_legend=False, no_legend=False, no_marks_for_uniform_weight=True, ): """Plot pressure and current constraints together""" in1 = self._combine_namelists() if in1 is None else in1 n_rows = 3 n_cols = 1 n_plots = n_rows * n_cols base = n_rows * 100 + n_cols * 10 # Make sure we have enough subplots in the figure fig = pyplot.gcf() if fig is None else fig ax = fig.get_axes() if ax is None else ax if len(np.atleast_1d(ax).flatten()) >= n_plots: axx = np.array(ax).flatten() ax1 = axx[0] ax2 = axx[1] ax3 = axx[2] new_plot_opened = False else: if len(np.array(ax).flatten()) >= 1: # We got here because len(ax)<n_plots, which means our output doesn't fit. # But if len(ax)=0, then we can reuse the figure we have because it's blank. fig = pyplot.figure() new_plot_opened = True # Let the main plot script know that we need a cornernote for the new plot else: # A new plot could've been opened by pyplot.gcf(), but we already have that possibility covered below # in the main plot function. new_plot_opened = False ax1 = pyplot.subplot(base + 1) ax2 = pyplot.subplot(base + 2, sharex=ax1) ax3 = pyplot.subplot(base + 3, sharex=ax1) # Color will be updated by the first plot if it is None, and then all plots will have the same color even if # some subfigs are missing data for some of the k-files. If the first plot fails, the second plot can update # color. # Plot 1: pressure color = self.plot_press_constraint( in1, fig=fig, ax=ax1, label=label, color=color, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend, no_marks_for_uniform_weight=no_marks_for_uniform_weight, ) # Plot 2: current color = self.plot_current_constraint( in1, fig=fig, ax=ax2, label=label, color=color, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend ) # Plot 3: fast ion pressure self.plot_fast_ion_constraints( in1, fig=fig, ax=ax3, label=label, color=color, density=False, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend, ) # Make sure it's pretty if os.environ.get('OMFIT_NO_GUI', '0') == '0': import matplotlib.pyplot pyplot.tight_layout() return new_plot_opened
[docs] def plot_everything( self, combo=None, fig=None, ax=None, label='', color=None, no_extra_info_in_legend=False, no_legend=False, no_marks_for_uniform_weight=True, ): """Plot pressure, mass density, current, fast ion pressure, fast ion density, MSE""" combo = self._combine_namelists() if combo is None else combo n_rows = 3 n_cols = 2 n_plots = n_rows * n_cols base = n_rows * 100 + n_cols * 10 # Make sure we have enough subplots in the figure fig = pyplot.gcf() if fig is None else fig ax = fig.get_axes() if ax is None else ax if len(np.array(ax).flatten()) >= n_plots: axx = np.array(ax).flatten() ax1 = axx[0] ax2 = axx[1] ax3 = axx[2] ax4 = axx[3] ax5 = axx[4] ax6 = axx[5] new_plot_opened = False else: if len(np.array(ax).flatten()) >= 1: # We got here because len(ax)<n_plots, which means our output doesn't fit. But if len(ax)=0, then we # can reuse the figure we have because it's blank. fig = pyplot.figure() new_plot_opened = True # Let the main plot script know that we need a cornernote for the new plot else: # a new plot could've been opened by pyplot.gcf(), but we already have that possibility covered below # in the main plot function new_plot_opened = False ax1 = pyplot.subplot(base + 1 + 0 * n_cols) ax2 = pyplot.subplot(base + 1 + 1 * n_cols, sharex=ax1) ax3 = pyplot.subplot(base + 1 + 2 * n_cols, sharex=ax1) ax4 = pyplot.subplot(base + 2 + 0 * n_cols, sharex=ax1, sharey=ax1) # 4 and 1 are both pressure ax5 = pyplot.subplot(base + 2 + 1 * n_cols, sharex=ax1) # The first 5 will typically be vs. psi ax6 = pyplot.subplot(base + 2 + 2 * n_cols) # This one is always going to be vs. R # Plot 1: pressure color = self.plot_press_constraint( combo, # Color will be updated by the first plot if it is None fig=fig, ax=ax1, label=label, color=color, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend, no_marks_for_uniform_weight=no_marks_for_uniform_weight, ) # Plot 2: mass density color = self.plot_mass_density( combo, # The first plot could fail and leave color as None. We'd better try to update color at every turn fig=fig, ax=ax2, label=label, color=color, ) # Plot 3: current color = self.plot_current_constraint( combo, fig=fig, ax=ax3, label=label, color=color, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend ) # Plot 4: fast ion pressure color = self.plot_fast_ion_constraints( combo, fig=fig, ax=ax4, label=label, color=color, density=False, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend, ) # Plot 5: fast ion density color = self.plot_fast_ion_constraints( combo, fig=fig, ax=ax5, label=label, color=color, density=True, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend, ) # Plot 6: MSE self.plot_mse( combo, fig=fig, ax=ax6, label=label, color=color, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend ) # Make sure it's pretty if os.environ.get('OMFIT_NO_GUI', '0') == '0': pyplot.tight_layout() return new_plot_opened
####################### # K-FILE PLOT MANAGER # #######################
[docs] def plot(self, plottype=None, fig=None, ax=None, label='', color=None, no_extra_info_in_legend=False, no_legend=False): """ Plot manager for k-file class OMFITkeqdsk Function used to decide what real plot function to call and to apply generic plot labels. You can also access the individual plots directly, but you won't get the final annotations. EFIT k-file inputs are documented at https://fusion.gat.com/theory/Efitinputs :param plottype: string What kind of plot? - 'everything' - 'pressure and current' - 'pressure' - 'current' - 'fast ion density' - 'fast ion pressure' - 'mse' - 'mass density' :param fig: [Optional] Figure instance Define fig and ax to override automatic determination of plot output destination. :param ax: [Optional] Axes instance or array of Axes instances Define fig and ax to override automatic determination of plot output destination. Ignored if there are not enough subplots to contain the plots ordered by plottype. :param label: [Optional] string Provide a custom label to include in legends. May be useful when overlaying two k-files. Default: ''. Set label=None to disable shot and time in legend entries. :param no_extra_info_in_legend: bool Do not add extra text labels to the legend to display things like Bt, etc. :param no_legend: bool Do not add legends to the plots """ combo = self._combine_namelists() fignums = ( pyplot.get_fignums() ) # Get a list of figure numbers that are open at the start so we can detect the case where there were no figures open new_plot_opened = False # This is for a specific case where a plot that needs several subplots doesn't have enough so it has to make a new window. Assume False unless set True later # Pick which plot we're making if plottype is None: plottype = 'everything' # Change None to default option # Now find and call the appropriate function if plottype.lower() in ['everything', 'all', 'all plots', 'every plot']: # Combo plot of everything new_plot_opened = self.plot_everything( combo, fig=fig, ax=ax, label=label, color=color, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend ) elif plottype.lower() in ['pressure and current', 'pressure_and_current', 'presscurr', 'pc']: # Combo plot of kinetic pressure, fast ion pressure, and current new_plot_opened = self.plot_pressure_and_current_constraints( combo, fig=fig, ax=ax, label=label, color=color, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend ) elif plottype.lower() in ['pressure_constraint', 'pressure constraint', 'press', 'pressure']: # Total kinetic pressure self.plot_press_constraint( combo, fig=fig, ax=ax, label=label, color=color, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend ) elif plottype.lower() in ['current_constraint', 'current constraint', 'current', 'curr', 'curr constraint']: # Toroidal current self.plot_current_constraint( combo, fig=fig, ax=ax, label=label, color=color, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend ) elif plottype.lower() in [ 'fast_ion_density', 'fast ion density', 'n_fast', 'fast density', 'fast dens', 'beam density', 'beam ion density', 'n_beam', ]: # Fast ion density self.plot_fast_ion_constraints( combo, fig=fig, ax=ax, label=label, color=color, density=True, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend, ) elif plottype.lower() in [ 'fast_ion_pressure', 'fast ion pressure', 'p_fast', 'fast pressure', 'fast press', 'p_beam', 'beam pressure', 'beam ion pressure', ]: # Fast ion pressure self.plot_fast_ion_constraints( combo, fig=fig, ax=ax, label=label, color=color, density=False, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend, ) elif plottype.lower() in ['mse']: # MSE tan(gamma) self.plot_mse( combo, fig=fig, ax=ax, label=label, color=color, no_extra_info_in_legend=no_extra_info_in_legend, no_legend=no_legend ) elif plottype.lower() in ['mass_density', 'dmass', 'density', 'dens', 'mass density']: # Mass density self.plot_mass_density(combo, fig=fig, ax=ax, label=label, color=color) elif plottype.lower() in ['bad_plot']: # Test the bad plot warning self._bad_plot('(TEST MISSING DATA NOTIFICATION)', ax=ax, label=label) else: printe('Unrecognized keqdsk plot type: {:}'.format(plottype)) return # Final annotations try: from omfit_classes.utils_plot import cornernote except ImportError: pass else: do_cornernote = False # By default, no cornernote if fignums == []: # If there were no plots open when we started, then we must've made a plot, so we can put a cornernote on it do_cornernote = True if new_plot_opened: do_cornernote = True if do_cornernote: cornernote('', '{} {} ms'.format(combo['ISHOT'], combo['ITIME'])) else: cornernote(remove=True)
############################ # S-FILE CLASS OMFITseqdsk # ############################
[docs]class OMFITseqdsk(SortedDict, OMFITascii): r""" class used to interface S files generated by EFIT :param filename: filename passed to OMFITascii class :param \**kw: keyword dictionary passed to OMFITascii class """ def __init__(self, filename, **kw): OMFITascii.__init__(self, filename, **kw) SortedDict.__init__(self, caseInsensitive=False, sorted=False) self.dynaLoad = True
[docs] @dynaLoad def load(self, **kw): if self.filename is None or not os.stat(self.filename).st_size: return with open(self.filename, 'r') as f: lines = f.read().split('\n') for k in ['x', 'y', 'dx', 'dy']: self[k] = [] for k, line in enumerate(lines): try: x, y, dx, dy = list(map(float, line.split())) self['x'].append(x) self['y'].append(y) self['dx'].append(dx) self['dy'].append(dy) except Exception: if k < 3: if k == 0: self['xlabel'] = line.strip() elif k == 1: self['ylabel'] = line.strip() elif k == 2: self['title'] = line.strip() for k in ['x', 'y', 'dx', 'dy']: self[k] = np.array(self[k])
[docs] @dynaSave def save(self): tmp = np.array([self['x'], self['y'], self['dx'], self['dy']]).T with open(self.filename, 'w') as f: if 'xlabel' in self: f.write(self['xlabel'] + '\n') if 'ylabel' in self: f.write(self['ylabel'] + '\n') if 'title' in self: f.write(self['title'] + '\n') savetxt(f, tmp, fmt='%10.5f')
[docs] def plot(self, **kw): if 'ylabel' in self: kw.setdefault('label', self['ylabel']) else: kw.setdefault('label', os.path.split(self.filename)[1]) errorbar(self['x'], self['y'], self['dy'], self['dx'], **kw) if 'xlabel' in self: pyplot.xlabel(self['xlabel']) if 'title' in self: pyplot.title(self['title']) if 'ylabel' in self: pyplot.legend(labelspacing=0.1, loc=0).draggable(state=True)
############################################ # Read basic equilibrium data from MDSplus # ############################################ def _prep_derived_for_read_basic_eq_from_mds(device, quiet=False, **kw): """ Examines quantities requested and makes extensions as needed to support requested derived quantities :param device: str Name of the tokamak :param quiet: bool Be quiet; don't print stuff :param g_file_quantities: list of strings Quantities to read from the sub-tree corresponding with the EFIT g-file. Example: ['r', 'z', 'rhovn'] :param a_file_quantities: list of strings Quantities to read from the sub-tree corresponding with the EFIT a-file. Example: ['area'] :param measurements: list of strings Quantities to read from the MEASUREMENTS tree. Example: ['fccurt'] :param derived_quantities: list of strings Derived quantities to be calculated and returned. This script understands a limited set of simple calculations: 'time', 'psin', 'psin1d' Example: ['psin', 'psin1d', 'time'] :param other_results: list of strings Other quantities to be gathered from the parent tree that holds gEQDSK and aEQDSK. Example: ['DATE_RUN'] :param get_all_meas: bool Fetch measurement signals according to its time basis which includes extra time slices that failed to fit. The time 'axis' will be avaliabe in ['mtimes'] :return: tuple containing: g_params: list of strings a_params: list of strings d_params: list of strings o_params: list of strings measurements: list of strings """ def printdq(*arg, **kw): if not quiet: printd(*arg, **kw) g_params = tolist(kw.pop('g_file_quantities', ['r', 'z', 'rhovn'])) a_params = tolist(kw.pop('a_file_quantities', ['area'])) d_params = tolist(kw.pop('derived_quantities', ['psin', 'psin1d', 'time'])) o_params = tolist(kw.pop('other_results', ['DATE_RUN'])) measurements = tolist(kw.pop('measurements', ['fccurt'])) get_all_meas = kw.get('get_all_meas', False) if get_all_meas and ('mtime' not in measurements): measurements.append('mtime') # make sure the m file time basis get fetched # Sanitize measurements if we are not looking at supported devices (they need further implementation tests). supported_devices = ['DIII-D', 'KSTAR', 'NSTX', 'NSTX-U'] if not is_device(device, supported_devices): if not quiet: printw( f'WARNING: read_basic_eq_from_mds: data from the "measurements" branch are not available for the ' f'selected device: {device}. Supported devices are: {supported_devices}. The following pointnames ' f'in measurements will not be gathered: {measurements}' ) measurements = [] get_mfile = False else: printdq(f' No need to suppress measurements for device {device}') # Add dependencies for derived quantities more_g = [] more_a = [] more_d = [] if 'psin' in d_params: more_g += ['psirz', 'ssimag', 'ssibry'] if 'psin1d' in d_params: more_g += ['mw'] if any([a in d_params for a in ['br', 'bz', 'Br', 'Bz']]): more_g += ['r', 'z', 'psirz', 'cpasma'] if measurements or 'nebar_r0' in a_params or 'nebar_v1' in a_params or 'nebar_v2' in a_params or 'nebar_v3' in a_params: more_a = ['atime'] for gg in more_g: if gg not in g_params: g_params += [gg] for aa in more_a: if aa not in a_params: a_params += [aa] for dd in more_d: if dd not in d_params: d_params += [dd] # Make sure atime comes first if it is being gathered. a_params = list(set(a_params)) # Eliminating duplicates might change the order of keys, so do it first if 'atime' in a_params: a_params.remove('atime') a_params = ['atime'] + a_params return list(set(g_params)), a_params, list(set(d_params)), list(set(o_params)), list(set(measurements))
[docs]def read_basic_eq_from_mds(device='DIII-D', shot=None, tree='EFIT01', quiet=False, toksearch_mds=None, **kw): """ Read basic equilibrium data from MDSplus This is a lightweight function for reading simple data from all EFIT slices at once without making g-files. :param device: str The tokamak that the data correspond to ('DIII-D', 'NSTX', etc.) :param server: str [Optional, special purpose] MDSplus server to draw data from. Use this if you are connecting to a server that is not recognized by the tokamak() command, like vidar, EAST_US, etc. If this is None, it will be copied from device. :param shot: int Shot number from which to read data :param tree: str Name of the MDSplus tree to connect to, like 'EFIT01', 'EFIT02', 'EFIT03', ... :param g_file_quantities: list of strings Quantities to read from the sub-tree corresponding with the EFIT g-file. Example: ['r', 'z', 'rhovn'] :param a_file_quantities: list of strings Quantities to read from the sub-tree corresponding with the EFIT a-file. Example: ['area'] :param measurements: list of strings Quantities to read from the MEASUREMENTS tree. Example: ['fccurt'] :param derived_quantities: list of strings Derived quantities to be calculated and returned. This script understands a limited set of simple calculations: 'time', 'psin', 'psin1d' Example: ['psin', 'psin1d', 'time'] :param other_results: list of strings Other quantities to be gathered from the parent tree that holds gEQDSK and aEQDSK. Example: ['DATE_RUN'] :param quiet: bool :param get_all_meas: bool Fetch measurement signals according to its time basis which includes extra time slices that failed to fit. The time 'axis' will be avaliabe in ['mtimes'] :param toksearch_mds: OMFITtoksearch instance An already fetched and loaded OMFITtoksearch object, expected to have fetched all of the signals for the mdsValues in this file. :param allow_shot_tree_translation: bool Allow the real shot and tree to be translated to the fake shot stored in the EFIT tree :return: dict """ if toksearch_mds is None: from MDSplus import MdsException from omfit_classes.omfit_mds import OMFITmdsValue, OMFITmds else: OMFITmdsValue = toksearch_mds def printdq(*arg, **kw): if not quiet: printd(*arg, **kw) # Setup ------------------------------------------------------------------------------------------------------- # Sanitize inputs and assign defaults as needed device = utils_fusion.tokamak(device) server = kw.pop('server', device) if server is None: server = device g_params, a_params, d_params, o_params, measurements = _prep_derived_for_read_basic_eq_from_mds(device=device, quiet=quiet, **kw) get_all_meas = kw.pop('get_all_meas', False) # Handle shot/tree translation, if needed if kw.pop('allow_shot_tree_translation', True) and tree.startswith(str(shot)): # Scratch EFITs get stored in a tree named `EFIT` with fake shot numbers that are the real shot# plus a # counter. However, tools for looking up EFIT trees for a given shot will return these in the list. To # access them, the shot and tree need to be translated. try: nom = OMFITmdsValue(server, shot=shot, treename=tree, TDI=rf'GETNCI(\top, "number_of_members")').data() if not nom[0]: raise ValueError(f'Tree {tree} has no members for {server}#{shot}') except Exception: # I want (MdsException, TypeError, ValueError), but can't import MDS exceptions reliably real_shot = shot shot = tree tree = 'EFIT' printw(f'Shot {real_shot}, tree {shot} did not exist; translated to shot {shot}, tree {tree}') else: printw(f'This treename seems like it should be fake, yet it has data. Skipping translation.') if shot is None: raise ValueError('Shot number must be not be None!') # Make sure the tree exists try: if toksearch_mds is None: OMFITmds(server=server, treename=tree, shot=shot, quiet=quiet).load() except MdsException: printe('FAIL! Tree {:} does not exist for shot {:} on MDSplus server {:}'.format(tree, shot, server)) printd(''.join(traceback.format_exception(*sys.exc_info())), topic='omfit_eqdsk') return None # Some BAD PEOPLE saved nebar_* with different dimensions than everything else. # They interpolated path*, which GOES WITH nebar_*, but did not interpolate nebar_* itself. special_timebase = ['nebar_r0', 'nebar_v1', 'nebar_v2', 'nebar_v3'] # Look up how data are organized for the device in question --------------------------------------------------- # Format code device_formats = { 'DIII-D': 'A', 'NSTX': 'B', 'NSTXU': 'B', 'CMOD': 'C', # Format C also triggers transposition of 2D data. 'EAST': 'B', 'EAST_US': 'B', 'KSTAR': 'A', 'ST40': 'B', } default_format = 'A' # For unrecognized devices device_format = device_formats.get(device, default_format) if is_device(device, 'CMOD') and tree == 'ANALYSIS': field = 'TOP.EFIT.RESULTS' else: field = 'TOP.RESULTS' # Format of TDI calls to MDSplus to a-file or g-file tree_formats = { 'A': '\\{efit_tree:}::TOP.RESULTS.{letter:}EQDSK.{signal:}', 'B': '\\{efit_tree:}::TOP.RESULTS.{letter:}EQDSK.{signal:}', 'C': '\\{efit_tree:}::%s.{letter:}_EQDSK.{signal:}' % field, } # Format of TDI calls to MDSplus to top level results tree tree_formats_other_results = { 'A': '\\{efit_tree:}::TOP.RESULTS.{signal:}', 'B': '\\{efit_tree:}::TOP.RESULTS.{signal:}', 'C': '\\{efit_tree:}::%s.{signal:}' % field, } # Each transpose plan is a list of data array dimensions to transpose transpose_plans = {'A': [], 'B': [], 'C': [2]} # Don't transpose anything # Don't transpose anything # Transpose 2D data only time_axis_3d_info = {'A': 2, 'B': 0, 'C': 2} # Unit conversions for time are handled automatically - see below # Select device specific format form = tree_formats[device_format] form2 = tree_formats_other_results[device_format] form3 = form2.replace('RESULTS', 'MEASUREMENTS') transpose_plan = transpose_plans[device_format] time_axis_3d = time_axis_3d_info[device_format] # Get results ------------------------------------------------------------------------------------------------------ results = {} # Always get time # Time is a derived quantity because it is pulled from dim_of() on psirz and the unit conversion # is handled automatically. I prefer to get time from psirz because psirz is so fundamental and # must be present in order to do anything useful, whereas some joker could've renamed time to # gtime or efittime or times or who knows what else. letter = 'G' signal = 'PSIRZ' # Step 1: look up the units of time and pick a conversion factor psirz_call = form.format(efit_tree=tree, letter=letter, signal=signal) units_obj = OMFITmdsValue( server=server, treename=tree, shot=shot, TDI=form.format(efit_tree=tree, letter=letter, signal=signal), quiet=quiet ) units = units_obj.units_dim_of(time_axis_3d) printdq('units({}) = {}'.format(time_axis_3d, units)) time_call = 'dim_of({:}, {:})'.format(form.format(efit_tree=tree, letter=letter, signal=signal), time_axis_3d) ucf = {'s': 1000.0, 'sec': 1000.0, 'ms': 1.0, '': -1, ' ': -1} psirz_sig = OMFITmdsValue(server=server, treename=tree, shot=shot, TDI=psirz_call, quiet=quiet) units = psirz_sig.units_dim_of(time_axis_3d) unit_conversion = ucf.get(units, None) if unit_conversion == -1: # Blank units; we can try to look them up another way. Activate contingency plans! printdq(' Blank time units.') printdq(' Attempting contingency plan 1 to get EFIT time units...') # Attempt to work this out by reading the time call separately and taking .units() of it time_raw1 = psirz_sig.dim_of(time_axis_3d) try: units = time_raw1.units() unit_conversion = ucf.get(units, None) except Exception: unit_conversion = None if unit_conversion is None: printdq(' Unrecognized time units from contingency plan 1 ({}). FAIL.'.format(repr(units))) contingency_failed = True elif unit_conversion == -1: # Attempt to work this out by reading gtime printdq(' Blank time units from contingency plan 1.') printdq(' Attempting contingency plan 2 to get EFIT time units: read units of gtime...') call = form.format(efit_tree=tree, letter=letter, signal='gtime') gtime = OMFITmdsValue(server=server, treename=tree, shot=shot, TDI=call, quiet=quiet) if gtime is not None and gtime.data() is not None: if np.all(np.atleast_1d(time_raw1 == gtime)): printdq(' gtime matches time_raw1; can use its units') units = gtime.units() unit_conversion = ucf.get(units, None) if unit_conversion is None or unit_conversion == -1: contingency_failed = True printdq(' gtime had unrecognized or missing units: {}. FAIL'.format(units)) else: # It worked; continue printdq( ' Contingency plan 2 for getting time units was successful. ' 'units = {}, unit_conversion = {}'.format(units, unit_conversion) ) contingency_failed = False else: contingency_failed = True printdq(' gtime does not match time_raw1; cannot borrow its units. Fail.') else: printdq(' Failed to read gtime. Cannot get time units that way. Out of options. Fail.') contingency_failed = True else: # It worked; continue printdq( ' Contingency plan 1 for getting time units was successful. units = {}, unit_conversion = {}'.format( units, unit_conversion ) ) contingency_failed = False elif unit_conversion is None: # Unrecognized units printdq(' Unrecognized time units: {}'.format(units)) contingency_failed = True else: printdq(' Recognized time units from first attempt. No contingency plans needed or attempted.') contingency_failed = False # Contingency is not even needed if contingency_failed: if not quiet: printw( 'WARNING: read_basic_eq_from_mds: Did not recognize units of time ({})! ' 'Unable to guarantee conversion to ms! ' 'Unit conversion factor has been set to 1.0 for lack of better options.'.format(repr(units)) ) unit_conversion = 1.0 # Step 2 read the time and convert to ms printdq(' read_basic_eq_from_mds: gathering time; call = {}'.format(time_call)) time_raw = psirz_sig.dim_of(time_axis_3d) printdq( ' read_basic_eq_from_mds: time_raw.min() = {}, time_raw.max() = {}, len(time_raw) = {}, ' 'unit_conversion = {}'.format(time_raw.min(), time_raw.max(), len(time_raw), unit_conversion) ) # Now loop through A-files and G-files and get quantities for letter, signal in zip('G' * len(g_params) + 'A' * len(a_params), g_params + a_params): call = form.format(efit_tree=tree, letter=letter, signal=signal) if signal.lower() in special_timebase: printdq(' read_basic_eq_from_mds: Interpolating quantity with special_timebase: {}'.format(signal)) tmp_res = OMFITmdsValue(server=server, treename=tree, shot=shot, TDI=call, quiet=quiet) try: tmp_res_data = tmp_res.data() except Exception as e: res = None tmp_units = None printw( f"WARNING: MDSplus returned the following exception when attempting to access {call} for shot {shot} on {tree} at {server}" ) printw(e) printw("The variable was replaced with None") else: if len(tmp_res_data) > 1: res = interpolate.interp1d(tmp_res.dim_of(0), tmp_res_data, bounds_error=False, fill_value=0)(results['atime']) else: res = tmp_res_data tmp_units = tmp_res.units() else: try: tmp_res = OMFITmdsValue(server=server, treename=tree, shot=shot, TDI=call, quiet=quiet) res = tmp_res.data() tmp_units = tmp_res.units() except Exception as e: # any of the MDSplus errors, they are hard to classify res = None tmp_units = None printw( f"WARNING: MDSplus returned the following exception when attempting to access {call} for shot {shot} on {tree} at {server}" ) printw(e) printw("The variable was replaced with None") if len(np.shape(res)) in transpose_plan: res = res.T if isinstance(tmp_units, str) and is_numeric(res): # Units will be handled by converting all of the MDS results to m, s, MJ. # From there, any different units in the EFIT file specifications can be obtained. # Without doing this step first, there's not a robust way to know what units were # used when saving to MDSplus, because they are definitely not the same as the EFIT # files at DIII-D. u = tmp_units.strip() if u == 'cm': factor = 1e-2 # Convert cm to m. Quantities which are supposed to be in cm will be converted back. elif u == 'cm^2': factor = 1e-4 # Convert cm^2 to m^2 elif u == 'cm^3': factor = 1e-6 elif u in ['W', 'J']: factor = 1e-6 # Convert W to MW, J to MJ elif u == 'ms' and signal not in ['atime', 'time', 'gtime']: # Slice times are handled differently. factor = 1e-3 # Convert ms to s elif (signal.lower() in ['atime', 'gtime']) and u.lower() in ['s', 'sec', 'seconds']: factor = 1e3 # time is in ms, so for consistency, atime and gtime should be, too. else: factor = 1 res *= factor printdq(' read_basic_eq_from_mds() Converted units of {}EQDSK quantity {} with factor {}.'.format(letter, signal, factor)) results[signal] = res # Get measurements (mostly M file quantities are here) # Get meas_time # Elements in the measurements subtree have the same time basis, but can be different from results tree call = form3.format(efit_tree=tree, signal='cpasma') cpasma = OMFITmdsValue(server=server, treename=tree, shot=shot, TDI=call, quiet=quiet) meas_time = cpasma.dim_of(0) # Get meas signals for signal in measurements: printdq(' Now processing signal {} in measurements...'.format(signal)) if signal in ['mtime', 'm_time']: if get_all_meas: results[signal] = meas_time else: results[signal] = results['atime'] # It will be reduced to g/a files time basis continue elif signal == 'cpasma': res0 = cpasma # avoid refetching to save time else: call = form3.format(efit_tree=tree, signal=signal) res0 = OMFITmdsValue(server=server, treename=tree, shot=shot, TDI=call, quiet=quiet) # Measurements can have more time-slices than results, because results can fail. We have to deal with that ndim = len(np.shape(res0.data())) if ndim == 1: time_axis = 0 else: # ndim == 2: # There are no 3D measurements. time_axis = 1 sig_time = res0.dim_of(time_axis) if sig_time is None: # This var don't exist on this tree results[signal] = None # Always return something for requested signals. Makes downstream printd(f"{signal} is missing from MDS+, skipped") continue if len(meas_time) == 1: # For single-time EFITs (can happen for kinetic EFITs), measurements are saved in 1xN arrays # and don't need to be interpolated because they are only saved for the time in question. res = res0.data()[0] # : is assumed for any subsequent dimensions. https://docs.scipy.org elif get_all_meas: # Fetch measurement signals on its own time basis res = res0.data() else: # Interpolate. In reality, measurement contains extra times where EFIT failed to fit. (Particularly in # EFIT01/02). These extra times needs to be removed to fit time basis of the g/a files. Interp1e is just # a fast way of doing that. res = interp1e(sig_time, res0.data(), axis=0)(results['atime']) # sig_time is either None or meas_time according to how uploads at DIII-D processed, but use sig_time for # robustness if signal not in results: # This test prevents overwriting a signal that was found in gEQDSK or aEQDSK with a missing signal. if len(np.shape(res)) in transpose_plan: res = res.T results[signal] = res else: # Still attach it, but with a name suffix results[signal + '_meas'] = res for signal in o_params: call = form2.format(efit_tree=tree, signal=signal) res = OMFITmdsValue(server=server, treename=tree, shot=shot, TDI=call, quiet=quiet).data() if res is not None or signal not in results: # This test prevents overwriting a signal that was found in gEQDSK or aEQDSK with a missing signal. if len(np.shape(res)) in transpose_plan: res = res.T results[signal] = res # Calculate derived quantities -------------------------------------------------------------------------------- if 'psin' in d_params: psi_norm_f = results['ssibry'] - results['ssimag'] # Prevent divide by 0 error by replacing 0s in the denominator problems = psi_norm_f == 0 psi_norm_f[problems] = 1.0 results['psin'] = (results['psirz'] - results['ssimag'][:, np.newaxis, np.newaxis]) / psi_norm_f[:, np.newaxis, np.newaxis] results['psin'][problems] = 0 if 'psin1d' in d_params: mw = tolist(results['mw'])[0] if mw is None: mw = np.shape(results['psirz'])[1] results['psin1d'] = np.linspace(0, 1, int(mw)) if 'br' in d_params or 'bz' in d_params or 'Br' in d_params or 'Bz' in d_params: r = results['r'] z = results['z'] psirz = results['psirz'] * np.sign(results['cpasma'])[:, np.newaxis, np.newaxis] rr, zz = np.meshgrid(results['r'], results['z']) [dpsi_dz, dpsi_dr] = np.gradient(psirz, z[1] - z[0], r[1] - r[0], axis=(1, 2)) results['Br'] = dpsi_dz / rr results['Bz'] = -dpsi_dr / rr results['time'] = time_raw * unit_conversion return results
############################################## # Read basic equilibrium data from TOKSEARCH # ############################################## # THIS FUNCTION IS ONLY SUPPORTED FOR SERVER='DIII-D'
[docs]def read_basic_eq_from_toksearch( device='DIII-D', server=None, shots=None, tree='EFIT01', quiet=False, g_file_quantities=['r', 'z', 'rhovn'], a_file_quantities=['area'], derived_quantities=['psin', 'psin1d', 'time'], measurements=['fccurt'], other_results=['DATE_RUN'], ): from omfit_classes.omfit_toksearch import OMFITtoksearch, TKS_MdsSignal signals = {} # Format of TDI calls to MDSplus g_params = tolist(g_file_quantities) a_params = tolist(a_file_quantities) d_params = tolist(derived_quantities) measurements = tolist(measurements) o_params = tolist(other_results) # Add dependencies for derived quantities more_g = [] more_a = [] if 'psin' in d_params: more_g += ['psirz', 'ssimag', 'ssibry'] if 'psin1d' in d_params: more_g += ['mw'] if measurements or 'nebar_r0' in a_params or 'nebar_v1' in a_params or 'nebar_v2' in a_params or 'nebar_v3' in a_params: more_a = ['atime'] for gg in more_g: if gg not in g_params: g_params += [gg] for aa in more_a: if aa not in a_params: a_params += [aa] if 'atime' in a_params: a_params.remove('atime') if device != 'DIII-D': raise RuntimeError("ONLY DIII-D DEVICES ARE SUPPORTED FOR TOKSEARCH USE") device_format = 'A' # Select device specific format form = '\\{efit_tree:}::TOP.RESULTS.{letter:}EQDSK.{signal:}' form2 = '\\{efit_tree:}::TOP.RESULTS.{signal:}' form3 = form2.replace('RESULTS', 'MEASUREMENTS') time_axis_3d = 2 dims = [str(i) for i in range(time_axis_3d + 1)] # num dimensions to be retrieved letter = 'G' signal = 'PSIRZ' psirz_call = form.format(efit_tree=tree, letter=letter, signal=signal) signals[psirz_call] = TKS_MdsSignal(psirz_call, tree, dims=dims) call = form.format(efit_tree=tree, letter=letter, signal='gtime') signals[call] = TKS_MdsSignal(call, tree) for letter, signal in zip('G' * len(g_params) + 'A' * len(a_params), g_params + a_params): call = form.format(efit_tree=tree, letter=letter, signal=signal) signals[call] = TKS_MdsSignal(call, tree) for signal in measurements: call = form3.format(efit_tree=tree, signal=signal) signals[call] = TKS_MdsSignal(call, tree, dims=dims) for signal in o_params: call = form2.format(efit_tree=tree, signal=signal) signals[call] = TKS_MdsSignal(call, tree) ## TO DO: CREATE A LIST OF signals toksearch_object = OMFITtoksearch(shots, signals).load() results = {} for shot in shots: results[shot] = read_basic_eq_from_mds( device, server, shot, tree, quiet, g_file_quantities, a_file_quantities, derived_quantities, measurements, other_results, toksearch_mds=toksearch_object, ) return results
########################################### # Create G-files and A-files from MDSplus # ########################################### # fmt: off
[docs]def from_mds_plus( device=None, shot=None, times=None, exact=False, snap_file='EFIT01', time_diff_warning_threshold=10, fail_if_out_of_range=True, get_afile=True, get_mfile=False, show_missing_data_warnings=None, debug=False, quiet=False, close=False, ): """ Gathers EFIT data from MDSplus, interpolates to the desired times, and creates a set of g/a/m-files from the results. Links to EFIT documentation:: https://fusion.gat.com/theory/Efit Home https://fusion.gat.com/theory/Efitiofiles List of input/output files https://fusion.gat.com/theory/Efitin1 IN1 namelist description (primary input) :param device: string Name of the tokamak or MDSserver from whence cometh the data. :param shot: int Shot for which data are to be gathered. :param times: numeric iterable Time slices to gather in ms, even if working with an MDS server that normally operates in seconds. :param exact: bool Fail instead of interpolating if the exact time-slices are not available. :param snap_file: string Description of which EFIT tree to gather from. :param time_diff_warning_threshold: float Issue a warning if the difference between a requested time slice and the closest time slice in the source EFIT exceeds this threshold. :param fail_if_out_of_range: bool Skip requested times that fail the above threshold test. :param get_afile: bool gather A-file quantities as well as G-file quantities. :param get_mfile: bool gather M-file quantities as well as G-file quantities. :param show_missing_data_warnings: bool 1 or True: Print a warning for each missing item when setting it to a default value. May not be necessary because some things in the a-file don't seem very important and are always missing from MDSplus. 2 or "once": print warning messages for missing items if the message would be unique. Don't repeat warnings about the same missing quanitty for subsequent time-slices. 0 or False: printd instead (use quiet if you really don't want it to print anything) None: select based on device. Most devices should default to 'once'. :param debug: bool Save intermediate results to the tree for inspection. :param quiet: bool :param close: bool Close each file at each time before going on to the next time :return: a dictionary containing a set of G-files in another dictioanry named 'gEQDSK', and, optionally, a set of A-files under 'aEQDSK' and M-filess under 'mEQDSK' """ # Get angry about bad inputs if shot is None or device is None or times is None: raise ValueError('Must specify shot, times, and device for from_mds_plus()!') # Sanitize inputs times = np.atleast_1d(times) # device = tokamak(device) def printq(*arg, **kw): if not quiet: print(*arg, **kw) def printdq(*arg, **kw): if not quiet: printd(*arg, **kw) # Announcements printq('Gathering EFIT from MDSplus: shot = {:}, snap/tree = {:}, times = {:}'.format(shot, snap_file, times)) # Set up warning message behavior warning_messages = {} bundled_warning_messages = {} if show_missing_data_warnings is None: # Most of these quantities seem to be present for DIII-D, so DIII-D's decault is to always warn. # For general other devices, the default should be to print warnings once. Other devices can have # their own defaults added. show_missing_data_warnings = {'DIII-D': True}.get(device, 'once') # Basic setup and info def printw2(warning_message, already_handled_once=False): if not quiet: if (show_missing_data_warnings in [2, 'once', 'Once']) and not already_handled_once: if warning_message not in warning_messages: # Only print the warning message if it is unique printw(warning_message + ' (warnings about this quantity being missing in subsequent time slices will be suppressed)') warning_messages[warning_message] = True elif show_missing_data_warnings: printw(warning_message) else: printd(warning_message) f_coil_counts = { # Determines default nfcoil0 (device dependent number of F-coils). 'DIII-D': 18, # 18 external PF coils (can be separate circuits) plus a separate CS. All copper. 'EAST': 14, # 12 superconducting external PF coils (6 of these are the CS) + 2 internal copper PF coils. 'KSTAR': 18, # 14 superconducting external PF coils (8 of these are the CS) + 4 internal copper PF coils. 'NSTX': 10, # 10 PF coils plus a separate CS. Copper. 'NSTX-U': 10, # I hope this didn't change from NSTX. } if exact: time_diff_warning_threshold = 0 output = {} # PART 0: General gathering ======================================================================================== # Part 0.1: g-file info -------------------------------------------------------------------------------------------- # Set up translation table translate = {"RZERO": "RCENTR", "MH": "NH", "MW": "NW", "XDIM": "RDIM", "SSIMAG": "SIMAG", "SSIBRY": "SIBRY", "CPASMA": "CURRENT"} # List of time varying quantities to interpolate time_varying = [ 'ZDIM', 'RMAXIS', 'ZMAXIS', 'BCENTR', 'FPOL', 'PRES', 'FFPRIM', 'PPRIME', 'PSIRZ', 'QPSI', 'NBBBS', 'LIMITR', 'RBBBS', 'ZBBBS', 'RHOVN', ] other_quantities = ['ECASE', 'CASE', 'RGRID', 'ZMID', 'LIM', 'RLIM', 'ZLIM', 'KVTOR', 'RVTOR', 'NMASS', 'DATE_RUN', 'R', 'Z'] # Part 0.2: a-file info -------------------------------------------------------------------------------------------- a_time_varying = [ 'rq1', 'rq2', 'rq3', 'li', 'li3', 'alpha', 'area', 'atime', 'aminor', 'bcentr', 'betan', 'betap', 'betapd', 'betat', 'betatd', 'bpolav', 'bt0', 'bt0vac', 'diamgc', 'chilibt', 'chipre', 'j1n', 'j0n', 'j95n', 'j99n', 'condno', 'cprof', 'diamag', 'diludom', 'diludomm', 'dminlx', 'dminux', 'dolubaf', 'dolubafm', 'tritop', 'tribot', 'kappa', 'kappa0', 'fexpan', 'fexpvs', 'limloc', 'chimse', 'sepbot', 'gapbot', 'sepin', 'gapin', 'oring', 'sepout', 'gapout', 'gaptop', 'septop', 'ipmeas', 'pbinj', 'peak', 'pp95', 'psin21', 'psin32', 'psiref', 'psurfa', 'qmerci', 'qmflag', 'ql', 'q95', 'qmin', 'q0', 'qstar', 'ratsol', 'rbcent', 'rcur', 'r0', 'rmidin', 'rmidout', 'rsurf', 'rq21top', 'rq32in', 'rhoqmin', 'rttt', 'rvsid', 'rvsin', 'rvsiu', 'rvsod', 'rvsou', 'rvsout', 's1', 's2', 's3', 'sepexp', 'seplim', 'sepnose', 'shear', 'psibdy', 'psi0', 'slantl', 'slantu', 'drsep', 'ssi01', 'ssi95', 'taudia', 'taumhd', 'tavem', 'tchimls', 'error', 'tflux', 'chisq', 'twagap', 'nindx', 'vloopmhd', 'volume', 'vsurf', 'wbdot', 'wpdot', 'wmhd', 'wdia', 'xbetapr', 'indent', 'xnnc', 'yyy2', 'zcur', 'z0', 'zsurf', 'zuperts', 'zvsid', 'zvsin', 'zvsiu', 'zvsod', 'zvsou', 'zvsout', 'ipmhd', ] special_a_time_varying = [ # These won't default to 0 if missing at the end (because they'll get popped out) 'nebar_r0', 'nebar_v1', 'nebar_v2', 'nebar_v3', 'pathr0', 'pathv1', 'pathv2', 'pathv3', 'rxpt1', 'rxpt2', 'zxpt1', 'zxpt2', ] a_meas = ['ccbrsp', 'csilop', 'cmpr2', 'eccurt',] # Part 0.3: m-file info -------------------------------------------------------------------------------------------- m_time_varying = [ 'mtime', 'a1gam', 'a2gam', 'a3gam', 'a4gam', 'a5gam', 'a6gam', 'a7gam', 'a8gam', 'ccbrsp', 'cchisq', 'cdflux', 'cecurr', 'cerror', 'chigam', 'cmgam', 'cmpr2', 'cpasma', 'cpress', 'csilop', 'czmaxi', 'darea', 'diamag', 'eccurt', 'expmpi', 'fccurt', 'fixgam', 'fwtec', 'fwtfc', 'fwtgam', 'fwtmp2', 'fwtsi', 'mcal_gain', 'mcal_offset', 'mcal_scale', 'mcal_slope', 'msebkp', 'msefitfun', 'plasma', 'pressr', 'rpress', 'rrgam', 'saimpi', 'saipre', 'saisil', 'sigdia', 'siggam', 'sigpre', 'silopt', 'tangam', 'tangam_uncor', 'vport', 'xrsp', 'zpress', 'zzgam', ] # Descriptive names of the vars m_longnames = { 'a1gam': 'viewing geometry coefficients of MSE channels', 'a2gam': 'viewing geometry coefficients of MSE channels', 'a3gam': 'viewing geometry coefficients of MSE channels', 'a4gam': 'viewing geometry coefficients of MSE channels', 'a5gam': 'viewing geometry coefficients of MSE channels', 'a6gam': 'viewing geometry coefficients of MSE channels', 'a7gam': 'viewing geometry coefficients of MSE channels', 'a8gam': 'viewing geometry coefficients of MSE channels', 'ccbrsp': 'calculated F-coil currents (Amp)', 'cchisq': 'chisq vs. iteration', 'cdflux': 'calculated diamagnetic flux', 'cecurr': 'calculated E-coil currents (Amp)', 'cerror': 'error vs. iteration', 'chigam': 'chisq vs. polarimetries', 'cmgam': 'calculated polarimetry signals', 'cmpr2': 'calculated magnetic probes', 'cpasma': 'calculated plasma current (Amp)', 'cpress': 'calculated pressure vs. normalized flux (kinetic fits only)', 'csilop': 'calculated flux loops', 'czmaxi': 'Zm (cm) vs. iteration', 'darea': 'plasma coefficients normalization', 'diamag': 'measured diamagnetic flux', 'eccurt': 'measured E-coil currents (Amp)', 'expmpi': 'measured magnetic probes', 'fccurt': 'measured F-coil currents (Amp)', 'fixgam': 'radians correction of tangam for spatial averaging effects', 'fwtec': 'weight for E-coil currents', 'fwtfc': 'weight for F-coil currents', 'fwtgam': 'fitting weight for MSE channels', 'fwtmp2': 'weight for magnetic probes', 'fwtsi': 'weight for flux loops', 'mcal_gain': 'gain param for tangent offset function', 'mcal_offset': 'offset param for tangent offset function', 'mcal_scale': 'scale param for tangent offset function', 'mcal_slope': 'slope param for tangent offset function', 'msebkp': 'background substraction switch', 'msefitfun': 'MSE fit function', 'plasma': 'measured plasma current (Amp)', 'pressr': 'measured pressure vs. normalized flux (kinetic fits only)', 'rpress': '<0 - input pressure profile vs. flux; >0 - R coordinates ofinput pressure profile (m)', 'rrgam': 'radius of MSE channels', 'saimpi': 'chisq vs. magnetic probes', 'saipre': 'chisq of pressure', 'saisil': 'chisq vs. PSI loops', 'shot': 'shot number', 'sigdia': 'uncertainty of diamagnetic flux', 'siggam': 'uncertainty of tangam', 'sigpre': 'uncertainty for pressure', 'silopt': 'measured flux loops', 'tangam': 'tangent of measured MSE pitch angle', 'tangam_uncor': 'tangent of measured MSE pitch angle w/o cer correction', 'vport': 'view port locations of MSE system', 'xrsp': 'plasma coefficients', 'zpress': 'Z coordinates of input pressure profile (m)', 'zzgam': 'Z position of MSE channels', } # only names of the second dim here, first dim is always dim_time m_dim_names = { "a1gam": "dim_nstark", "a2gam": "dim_nstark", "a3gam": "dim_nstark", "a4gam": "dim_nstark", "a5gam": "dim_nstark", "a6gam": "dim_nstark", "a7gam": "dim_nstark", "a8gam": "dim_nstark", "ccbrsp": "dim_nfcoil", "cchisq": "dim_nitera", "cdflux": None, "cecurr": "dim_nesum", "cerror": "dim_nitera", "chigam": "dim_nstark", "cmgam": "dim_nstark", "cmpr2": "dim_magpri", "cpasma": None, "cpress": "dim_npress", "csilop": "dim_nsilop", "czmaxi": "dim_nitera", "darea": None, "diamag": None, "eccurt": "dim_nesum", "expmpi": "dim_magpri", "fccurt": "dim_nfcoil", "fixgam": "dim_nstark", "fwtec": "dim_nesum", "fwtfc": "dim_nfcoil", "fwtgam": "dim_nstark", "fwtmp2": "dim_magpri", "fwtsi": "dim_nsilop", "mcal_gain": "dim_nstark", "mcal_offset": "dim_nstark", "mcal_scale": "dim_nstark", "mcal_slope": "dim_nstark", "msebkp": None, "msefitfun": None, "plasma": None, "pressr": "dim_npress", "rpress": "dim_npress", "rrgam": "dim_nstark", "saimpi": "dim_magpri", "saipre": "dim_npress", "saisil": "dim_nsilop", "shot": None, "sigdia": None, "siggam": "dim_nstark", "sigpre": "dim_npress", "silopt": "dim_nsilop", "tangam": "dim_nstark", "tangam_uncor": "dim_nstark", "time": None, "vport": "dim_nstark", "xrsp": "dim_npcurn", "zpress": "dim_npress", "zzgam": "dim_nstark", } dim_dict = SortedDict( { 'dim_magpri': 1, 'dim_nesum': 1, 'dim_nfcoil': 1, 'dim_nitera': 1, 'dim_npcurn': 1, 'dim_npress': 1, 'sim_nsilop': 1, 'dim_nstark': 1, 'dim_scalar': 1, 'dim_time': 1, } ) # Part 0.4: the units and whatnot ---------------------------------------------------------------------------------- # This is a temporary measure intended to prevent apparently bad results from MDSplus from being used normally. bad_a_quantities = [] # Not needed presently, so left empty. # Unit conversion factors. There is not a clean way to assign these automatically because the EFIT files use a # mixture of m and cm. The units of each signal have to be assigned on a case-by-case basis. # These are the names BEFORE translation! <-- !!!! ucf = { 1e2: [ # List quantities which need a unit conversion factor of 1e2, such as from cm to m # This following set was determined by reading the EFIT documentation for the A-file: 'rq1', 'rq2', 'rq3', 'aminor', 'gapbot', 'sepbot', 'gapin', 'sepin', 'gapout', 'sepout', 'gaptop', 'septop', 'rbcent', 'rco2r', 'rco2v', 'rcur', 'r0', 'rsurf', 'rseps', 'rvsin', 'rvsout', 'rxpt1', 'rxpt2', 'sepexp', 'seplim', 'sepnose', 'slantl', 'slantu', 'drsep', 'zcur', 'z0', 'zsurf', 'zxpt1', 'zxpt2', 'zvsin', 'zvsout', # Units not listed in EFIT A-file documentation as being cm, but which seem to need to be converted to cm: 'diludom', 'diludomm', 'dminlx', 'dminux', 'dolubaf', 'dolubafm', 'rvsid', 'rvsiu', 'rvsod', 'rvsou', 'zuperts', 'zvsid', 'zvsiu', 'zvsod', 'zvsou', 'zxpt1', 'zxpt2', ], 1e3: [ # List quantities which need a conversion factor of 1e3, such as would be needed to go from s to ms. 'taumhd', 'taudia', 'tavem', ], 1e4: ['area',], 1e6: ['wmhd', 'wdia', 'volume',], } # Transform the unit conversion factors into something easier to use in a loop through quantities aeqdsk_unit_factors = {} for factor in list(ucf.keys()): for quantity in ucf[factor]: aeqdsk_unit_factors[quantity] = factor # List of quantities which should be positive absolute = ['wmhd',] # Part 0.x: gather it ---------------------------------------------------------------------------------------------- # Determine what we need from measurements meas = [] if get_afile: if is_device(device,['NSTX','NSTX-U']): a_time_varying += a_meas else: meas = copy.copy(a_meas) if get_mfile: meas += copy.copy(m_time_varying) # Read data from MDSplus. read_basic_eq_from_mds() will handle transposes and unit conversions. efit_info = read_basic_eq_from_mds( device=device, server=None, shot=shot, tree=snap_file, g_file_quantities=time_varying + list(translate.keys()) + other_quantities, a_file_quantities=a_time_varying + special_a_time_varying if get_afile else [], measurements=meas, get_all_meas=get_mfile, # If we are getting m files, get real m times to determine strict time matching, because in theory a-file time might not be in measurement time at all. derived_quantities=['time'], other_results=['DATE_RUN', 'CODE_VERSION'], quiet=quiet, ) # It's okay for DATE_RUN to be in two places; DATE_RUN from other_results will overwrite DATE_RUN from # g_file_quantities if it exists, but if other_results/DATE_RUN returns None, it won't overwrite # g_file_quantities/DATE_RUN. if debug: output.setdefault('debug', {})['efit_info'] = efit_info if efit_info is None: raise OMFITexception('Fail! Could not gather EFIT data from MDSplus for shot = {}, snap = {}.'.format(shot, snap_file)) # Get timing efit_time = efit_info['time'] nte = efit_time.shape[0] # Number of time slices in the source EFIT # nt = len(times) # Number of time slices in the desired output # PART 1: G-FILE =================================================================================================== # Get the case information. This is a string array case = efit_info['ECASE'] if case is None: # read_basic_eq_from_mds() returns None for things it can't find. case = efit_info['CASE'] if debug: output['debug']['case'] = case if case is not None and hasattr(case, 'shape'): # handle the case where CASE is stored as a string if case.shape == (1,): case = np.array(case[0].split('\n')) # handle the case where CASE is stored as a unidimensional array if efit_time.shape[0] not in case.shape: case = case.reshape((nte, -1)) if isinstance(case[0], str): efit_code_ver = case[0].split()[0] efit_month_day = '/'.join(case[0].split()[1].split('/')[:2]) if is_device(device, 'CMOD'): efit_year = '/' + case[0].split()[2].split('/')[1] efit_shot_string = case[0].split()[3] else: efit_year = '/' + case[0].split()[1].split('/')[2] efit_shot_string = case[0].split()[2] efit_unused = '' else: efit_code_ver = case[0][0] efit_month_day = case[0][1] efit_year = case[0][2] efit_shot_string = case[0][3] # efit_first_time_string = case[0][4] efit_unused = case[0][5] else: efit_code_ver = ' EFITD ' efit_shot_string = '#' + str(shot) # efit_first_time_string = str(efit_time[0]) + 'ms' efit_unused = ' ' if efit_info['DATE_RUN'] is not None: # Make sure it's string, not a 1 element array holding a string date_run = ' '.join(tolist(efit_info['DATE_RUN'])) months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] month = str(months.index(date_run.split()[1])) efit_month_day = month + '/' + date_run.split()[2] efit_year = '/' + date_run.split()[-1] else: efit_year = efit_month_day = None efit_time_string = '{:6}ms' # The {:6} field will be filled in later, using .format(t) # Assemble the new version of the case field new_case = [efit_code_ver, efit_month_day, efit_year, efit_shot_string, efit_time_string, efit_unused] new_case[-1] = snap_file # Field index 4 of new_case is a string with {:} ready to accept time. # Set up interpolating functions. This is done before the time loop and the results evaluated in the time loop. interpolations = {} dummy_interpolation = interpolate.interp1d([0, 1], [np.NaN] * 2, bounds_error=False, axis=0) # Handle missing / None class JustReturn(object): """ A sort of dummy interpolation to make it simple to deal with things that only have one time-slice Give it the value you want it to return all the time. Call it like it's an interpolation, but it ignores the independent coordiante you pass to __call__ and just returns the value you initialized with. """ def __init__(self, y): self.y = y def __call__(self, x): return self.y for item in translate: if efit_info[item] is None: interpolations[item] = dummy_interpolation else: if len(efit_info[item]) == 1: # Handle cases where there is only one sample, which could happen with a special EFIT of only one slice. interpolations[item] = JustReturn(efit_info[item][0]) else: interpolations[item] = interpolate.interp1d(efit_time, efit_info[item], bounds_error=False, axis=0) for item in time_varying: if efit_info[item] is None: interpolations[item] = dummy_interpolation else: if len(efit_info[item]) == 1: # Handle cases where "time varying" data have only one sample, because they are not really time varying. # For example: LIMITR at EAST. Solve by making a simple, two-point dummy interpolation to eval later. interpolations[item] = JustReturn(efit_info[item][0]) else: interpolations[item] = interpolate.interp1d(efit_time, efit_info[item], bounds_error=False, axis=0) # Set up a collection to contain output out = output.setdefault('gEQDSK', {}) # Loop through the requested times and make a G-file for each one for t in times: g_file_name = 'g' + format(int(shot), "06d") + "." + format(int(t), "05d") if t - int(t) >= 1e-3: # Handle sub-ms timing to adding _000 to the filename g_file_name += '_{:03d}'.format(int((t - int(t)) * 1000)) if min(abs(t - efit_time)) > time_diff_warning_threshold: closest_efit = efit_time[closestIndex(efit_time, t)] if fail_if_out_of_range: note = 'FAIL' else: note = 'WARNING' printw( '{:}: requested time {:} ms is more than {:} ms away from closest time ({:} ms) in source EFIT!'.format( note, t, time_diff_warning_threshold, closest_efit ) ) if fail_if_out_of_range: printe('Skipped {:} because time difference was too large.'.format(g_file_name)) continue printq(' Loading {:}:'.format(g_file_name), end='') printq('CASE', end='') # Initialize the new g-file out[t] = g_file = OMFITgeqdsk(g_file_name) g_case = copy.copy(new_case) g_case[4] = g_case[4].format(t) g_file['CASE'] = g_case # Handle items that need tag name translation and time interpolation for item in translate: printq(',' + item, end='') g_file[translate[item]] = interpolations[item](t) # Items which do not vary with time - simple copy printq(', RLEFT', end='') g_file['RLEFT'] = efit_info['RGRID'][0] if isinstance(g_file['RLEFT'], np.ndarray): g_file['RLEFT'] = g_file['RLEFT'][0] printq(',ZMID', end='') if efit_info['ZMID'] is not None: g_file['ZMID'] = efit_info['ZMID'][0] if isinstance(g_file['ZMID'], np.ndarray): g_file['ZMID'] = g_file['ZMID'][0] else: g_file['ZMID'] = 0 # I don't know if efit_info['LIM'] is not None: printq(', RLIM', end='') g_file['RLIM'] = efit_info['LIM'][:, 0] printq(', ZLIM', end='') g_file['ZLIM'] = efit_info['LIM'][:, 1] else: printq(', RLIM', end='') g_file['RLIM'] = efit_info['RLIM'][0, :] printq(', ZLIM', end='') # Is this 1 index correct if LIM is already split into RLIM and ZLIM? g_file['ZLIM'] = efit_info['ZLIM'][1, :] if is_device(device, 'CMOD'): # limiter surface description in EFIT output is incomplete. Load rlim & zlim explicitely from MDSplus: g_file['RLIM'] = OMFITmdsValue('CMOD', 'analysis', shot, '\\analysis::top.limiters.wall.rlim').data() g_file['ZLIM'] = OMFITmdsValue('CMOD', 'analysis', shot, '\\analysis::top.limiters.wall.zlim').data() g_file['KVTOR'] = 0.0 g_file['RVTOR'] = g_file['RCENTR'] g_file['NMASS'] = 0.0 # Time varying items which require interpolation for item in time_varying: printq(',' + item, end='') g_file[item] = interpolations[item](t) printq('') # Replace NaNs with 0s for item in g_file: if is_numeric(g_file[item]) and np.atleast_1d(~np.isfinite(g_file[item])).all(): g_file[item] = np.zeros(np.shape(g_file[item])) # Convert length 1 arrays to floats for item in g_file.keys(): if item not in ['AuxNamelist', 'AuxQuantities', 'fluxSurfaces'] and np.atleast_1d(g_file[item]).shape[0] == 1: if is_numeric(np.atleast_1d(g_file[item])[0]): g_file[item] = float(g_file[item]) else: g_file[item] = np.atleast_1d(g_file[item])[0] # Convert floats to integers for item in g_file.keys(): if ( np.atleast_1d(is_numeric(g_file[item])).all() and np.atleast_1d(np.isfinite(g_file[item])).all() and isinstance(g_file[item], (float, np.floating)) and g_file[item] == int(g_file[item]) ): g_file[item] = int(g_file[item]) # Touch ups if ('NBBBS' not in g_file) or (g_file['NBBBS'] is None) or ~np.isfinite(g_file['NBBBS']): # The R grid never reaches all the way to the machine axis, so points at R=0 are padding or junk. with np.errstate(invalid='ignore'): g_file['NBBBS'] = np.sum(g_file['RBBBS'] > 0) if len(g_file['RBBBS']) else 0 try: g_file['NBBBS'] = int(g_file['NBBBS']) except ValueError: g_file['NBBBS'] = 0 printdq("omfit_eqdsk/from_mds_plus: g_file['NBBBS'] = {}".format(g_file['NBBBS'])) g_file['RBBBS'] = np.atleast_1d(g_file['RBBBS'])[: g_file['NBBBS']] g_file['ZBBBS'] = np.atleast_1d(g_file['ZBBBS'])[: g_file['NBBBS']] if debug: output['g_file_debug'] = g_file if g_file['NH'] == 0: # These are always square and sometimes transposed, so I'm not 100% sure which is 0 and which is 1. Sorry. g_file['NH'] = np.shape(g_file['PSIRZ'])[0] if g_file['NW'] == 0: g_file['NW'] = np.shape(g_file['PSIRZ'])[1] if g_file['ZDIM'] == 0: g_file['ZDIM'] = efit_info['Z'].max() - efit_info['Z'].min() if g_file['RDIM'] == 0: g_file['RDIM'] = efit_info['R'].max() - efit_info['R'].min() if g_file['LIMITR'] == 0: g_file['LIMITR'] = len(g_file['RLIM']) # Cleanup must_be_array = ['RHOVN'] for mba in must_be_array: # Delete invalid entries: items that must be arrays in order to be valid try: lmba = len(g_file.get(mba, None)) except TypeError: lmba = 0 if lmba < 2: printq('Removing quantity {} because it is supposed to be an array but its value is {}'.format(mba, g_file.get(mba, None))) g_file.pop(mba, None) # COCOS if (g_file['CURRENT'] == 0) or (g_file['BCENTR'] == 0): printw('Skipping COCOS transforms for G-file {} @ {} because CURRENT or BCENTR is zero'.format(shot, t)) g_file.addAuxQuantities() if close: g_file.close() else: cocosnum = 1 g_file._cocos = g_file.native_cocos() if close: g_file.cocosify(cocosnum, calcAuxQuantities=False, calcFluxSurfaces=False) g_file.close() else: g_file.cocosify(cocosnum, calcAuxQuantities=True, calcFluxSurfaces=True) printq('Done getting G-files from MDSplus: {shot:}, {snap:}, {times:}'.format(shot=shot, snap=snap_file, times=times)) # PART 2: A-FILE =================================================================================================== if get_afile: a_time = efit_info['atime'] if get_mfile: m_time = efit_info.get('mtime', None) a_interpolations = {} a_take_nearest = {} for item in a_time_varying + special_a_time_varying: if item in efit_info and efit_info[item] is not None and len(tolist(efit_info[item])) and item not in bad_a_quantities: if is_string(tolist(efit_info[item])[0]) or (len(a_time) < 2): # Handle strings or cases where there is only one time-slice printdq(' A-file: taking nearest value for {}'.format(item)) a_take_nearest[item] = efit_info[item] else: printdq(' A-file: interpolating {}'.format(item)) # Unit conversion has to happen on the way in to avoid interacting with default values. u_factor = aeqdsk_unit_factors.get(item, 1.0) a_interpolations[item] = interpolate.interp1d(a_time, efit_info[item] * u_factor, bounds_error=False, axis=0) if (u_factor != 1.0) and (not quiet): printd(' Applied unit conversion factor of {} to {}'.format(u_factor, item)) else: if (item in bad_a_quantities) and (not quiet): printd(' A-file: suppressing {} because it is flagged as being recorded badly in MDSplus.'.format(item)) else: printdq(' A-file: SKIPPING {} because it is missing or is None.'.format(item)) printdq("np.shape(efit_info['csilop']) = {}".format(np.shape(efit_info.get('csilop', None)))) a_meas_avail = [am for am in a_meas if am in efit_info] if get_mfile and m_time is not None: am_time = m_time printdq('A-file measured quantities will be interpolated to M-file timebase', topic='from_mds_plus') else: am_time = a_time printd('Will try to interpolate A-file measured quantities to A-file timebase', topic='from_mds_plus') for item in a_meas_avail: if (len(am_time) < 2) or (np.shape(np.atleast_1d(efit_info[item]))[0] < 2): # Dummy interpolation to make it return the value we want all the time, since there is only one slice a_interpolations[item] = JustReturn(np.atleast_1d(efit_info[item])[0]) printw2(f' A-file: {item} could not be proccessed because axis 0 is too short or A-file timebase is too short.') elif np.shape(np.atleast_1d(efit_info[item]))[0] != len(am_time): # Axis 0 of this array doesn't seem to be time. Don't know what to do with this thing. a_interpolations[item] = JustReturn(efit_info[item]) printw2(f' A-file: {item} does not seem to have time as axis 0; might need transpose. Skipping processing.') else: a_interpolations[item] = interpolate.interp1d(am_time, efit_info[item], bounds_error=False, axis=0) printdq(f' A-file: {item} was interpolated', topic='from_mds_plus') # Set up a collection to contain output out = output.setdefault('aEQDSK', {}) # Loop through the requested times and make an A-file for each one for t in times: a_file_name = 'a' + format(shot, "06d") + "." + format(int(t), "05d") if t - int(t) >= 1e-3: # Handle sub-ms timing to adding _000 to the filename a_file_name += '_{:03d}'.format(int(t - int(t) * 1000)) if min(abs(t - a_time)) > time_diff_warning_threshold: closest_efit = a_time[closestIndex(a_time, t)] if fail_if_out_of_range: note = 'FAIL' else: note = 'WARNING' printw( '{:}: requested time {:} ms is more than {:} ms away from closest time ({:} ms) in source EFIT!'.format( note, t, time_diff_warning_threshold, closest_efit ) ) if fail_if_out_of_range: printe('Skipped {:} because time difference was too large.'.format(a_file_name)) continue # Initialize the new a-file out[t] = a_file = OMFITaeqdsk(a_file_name) printdq('Initial keys in aeqdsk: {}'.format(list(a_file.keys()))) printq(' Loading {:}:'.format(a_file_name), end='') printq('CASE', end='') # Header a_file['__header__'] = '{datetime:} {code_version:}\n{shot:7d}{one:16d}\n{time:}'.format( shot=shot, time=t, one=1, datetime=np.atleast_1d(efit_info['DATE_RUN'])[0], code_version=np.atleast_1d(efit_info['CODE_VERSION'])[0], ) a_file['__footer__'] = '' # Time varying items which require interpolation or selecting nearest slice for item in a_time_varying + special_a_time_varying: if item in a_interpolations: printq(',' + item, end='') a_file[item.lower()] = a_interpolations[item](t) elif item in a_take_nearest: printq(',' + item, end='') a_file[item.lower()] = a_take_nearest[item][closestIndex(a_time, t)] if item in a_file and len(np.atleast_1d(a_file[item])) == 1: a_file[item.lower()] = np.atleast_1d(a_file[item])[0] printq('') printdq('Keys in aeqdsk after loading basics: {}'.format(list(a_file.keys()))) # Measurements for item in a_meas_avail: a_file[item.lower()] = a_interpolations[item](t) # Things that need to be renamed and/or converted (old, gathered in MDSplus : new, saved in A-file) translations = { 'bt0': 'btaxp', # Toroidal magnetic field at magnetic axis in Tesla 'bt0vac': 'btaxv', # Vacuum toroidal magnetic field at magnetic axis in Tesla 'atime': 'time', # Time in ms 'zcur': 'zcurrt', # Z in cm at current centroid 'rcur': 'rcurrt', 'kappa': 'elong', 'kappa0': 'elongm', 'rbcent': 'rcencm', 'vloopmhd': 'vloop', 'r0': 'rm', 'z0': 'zm', 'psi0': 'psim', 'q0': 'qm', 'ql': 'qout', 'j1n': 'cj1ave', 'j0n': 'cjor0', 'j95n': 'cjor95', 'j99n': 'cjor99', 'rq1': 'aq1', 'rq2': 'aq2', 'rq3': 'aq3', 'nindx': 'vertn', 'diamgc': 'cdflux', # Computed diamagnetic flux in Volt-sec 'tritop': 'utri', 'tribot': 'ltri', 'psibdy': 'sibdry', 'rsurf': 'rcntr', 'zsurf': 'zcntr', 'seplim': 'dsep', } # MDS : EFIT ''' # The translation table may be obtained by using this code snippet in the command box: shot = 173237 device = 'DIII-D' a = OMFITmds(treename='efit01', shot=shot, server=device)['RESULTS']['AEQDSK'] high_priority = {} low_priority = {} if True: # Change to False for repeats to save time for k in a.keys(): if 'EFIT_NAME' in a[k] and np.atleast_1d(a[k]['EFIT_NAME'].data())[0]: high_priority[np.atleast_1d(a[k]['EFIT_NAME'].data())[0]] = k else: low_priority[k] = k things = copy.deepcopy(low_priority) things.update(high_priority) print(repr(things)) for k, v in things.items(): if k != v and k.strip(): print("'{}': '{}',".format(v.lower(), k.lower())) ''' for old, new in list(translations.items()): if old in a_file: a_file[new] = a_file.pop(old) else: printw2('WARNING: {} missing from A-file. Cannot translate {} --> {}!'.format(old, old, new)) # Other information a_file['shot'] = shot # Error flags. I am setting these to their no-error state on the assumption that the case wouldn't be in MDS # if there were an error. a_file.setdefault('jflag', 1) # This would be 0 if there were an error. a_file.setdefault('lflag', 0) # Another error flag, this one is bad if > 0. # Handle some special quantities a_file['rseps'] = np.array([a_file.pop('rxpt1'), a_file.pop('rxpt2')]) a_file['zseps'] = np.array([a_file.pop('zxpt1'), a_file.pop('zxpt2')]) # CO2 density measurements--- # CO2 radial system; just one chord as of 20171013, so just fill it in. # The complexity is from turning it into an array and converting units. if 'nebar_r0' in a_file: # Line average electron density in cm3 from radial CO2 chord a_file['dco2r'] = np.array([a_file.pop('nebar_r0')]) a_file['mco2r'] = len(a_file['dco2r']) # Number of radial CO2 density chords else: a_file['mco2r'] = 1 a_file['dco2r'] = np.zeros(a_file['mco2r']) printw2(' A-file: Missing radial CO2 density information, setting MCO2R=1 and DCO2R=[0].') if 'pathr0' in a_file: a_file['rco2r'] = np.array([a_file.pop('pathr0') * 100.0]) # Path len (cm), radial CO2 density chord else: a_file['rco2r'] = np.zeros(a_file['mco2r']) printw2(' A-file: Missing radial CO2 density path length RCO2R, filling in with [0].') # CO2 vertical system; three chords as of 20171013. Allow for the possibility that some but not all # densities are missing, and a different subset of path lengths may be missing. a_file['mco2v'] = 0 a_file['dco2v'] = np.array([]) a_file['rco2v'] = np.array([]) for i, vert in enumerate(['v1', 'v2', 'v3']): if 'nebar_{}'.format(vert) in a_file or 'path{}'.format(vert) in a_file: # Either the density or the path length is recorded, so we have to deal with this one. a_file['mco2v'] += 1 # Number of vertical CO2 density chords # Line avg electron density in cm3 from vertical CO2 chord if 'nebar_{}'.format(vert) in a_file: a_file['dco2v'] = np.append(a_file['dco2v'], a_file.pop('nebar_{}'.format(vert))) else: printw2( ' A-file: Missing CO2 density for chord {} (DCO2V[{}]), ' 'although path length is available; filling with 0.'.format(vert, i) ) a_file['dco2v'] = np.append(a_file['dco2v'], 0) # Path length in cm of vertical CO2 density chord if 'path{}'.format(vert) in a_file: a_file['rco2v'] = np.append(a_file['rco2v'], a_file.pop('path{}'.format(vert)) * 100.0) else: printw2( ' A-file: Missing CO2 path length for chord {} (DCO2V[{}]), ' 'although density is available; filling with 0.'.format(vert, i) ) a_file['rco2v'] = np.append(a_file['rco2v'], 0) if a_file['mco2v'] == 0: printw(' A-file: Missing vertical CO2 density information MCO2V and DCO2V, filling in with 1 and [0].') a_file['mco2v'] = 1 a_file['dco2v'] = np.zeros(a_file['mco2v']) # Undocumented EFIT parameters that are needed for the format, but don't seem to be saved in MDSplus by # default. lengths_of_things = {'magpri0': 'cmpr2', 'nesum0': 'eccurt', 'nfcoil0': 'ccbrsp', 'nsilop0': 'csilop',} for length, thing in list(lengths_of_things.items()): if thing in a_file and a_file[thing] is not None: a_file[thing] = np.atleast_1d(a_file[thing]) a_file.setdefault(length, len(a_file[thing])) else: a_file.setdefault(length, 0) filler = ['nlnew', 'nlold'] for fill in filler: a_file.setdefault(fill, 0) a_file.setdefault('cmpr2', np.zeros(76)) # Length of array may vary. # Fill in non-zero defaults def_100s = ['aq1', 'aq2', 'aq3'] # Minor radius of q = 1, 2, 3 surfaces in cm, 100 if not found. for item in def_100s: if item not in a_file and item not in list(translations.keys()): a_file[item] = 100.0 # cm printw2(' A-file: {} was missing, so was filled in with 100.0.'.format(item)) # If anything else is still missing, fill it with zeros for item in a_time_varying + a_meas + list(translations.values()): if item not in a_file and item not in list(translations.keys()) and item not in special_a_time_varying: a_file[item] = 0.0 printw2(' A-file: {} was missing, so was filled in with zero.'.format(item)) if len(np.atleast_1d(a_file['zseps'])) == 1: a_file['zseps'] = np.array([a_file['zseps'], 0.0]) # Z of x point in cm printw2(' A-file: ZSEPS had 1 element, so 0-padded to 2 elements & is now {}'.format(a_file['zseps'])) if (a_file.get('ccbrsp', None) is None) or np.array_equal(np.atleast_1d(a_file['ccbrsp']), [0]): # Computed external coil currents in Ampere; should be an array, should have len = 18 for DIII-D because # 18 F-coils in DIII-D. if a_file.get('nfcoil0', 0) < f_coil_counts.get(device, 18): a_file['nfcoil0'] = f_coil_counts.get(device, 18) a_file['ccbrsp'] = np.zeros(a_file['nfcoil0']) printw2(f' A-file: CCBRSP was 0 or missing, so padded out to [0] * {a_file["nfcoil0"]} since it should be an array.') if (a_file.get('csilop', None) is None) or np.array_equal(np.atleast_1d(a_file['csilop']), [0]): # Computed flux loop signals in Weber. Length of array may vary. if a_file['nsilop0'] == 0: a_file['nsilop0'] = 44 # Just guess that there might be 44 flux loops. There were at one point in # DIII-D. I don't think it's critical to get this right because it's just # getting zero filled at this point, anyway. a_file['csilop'] = np.zeros(a_file['nsilop0']) printw2(f' A-file: CSILOP was 0 or missing, so padded out to [0] * {a_file["nsilop0"]} since it should be an array') for absl in absolute: a_file[absl] = abs(a_file[absl]) if close: a_file.close() printq('Done getting A-files from MDSplus: {shot:}, {snap:}, {times:}'.format(shot=shot, snap=snap_file, times=times)) # PART 3: M-FILE =================================================================================================== if get_mfile and 'mtime' in efit_info: m_time = efit_info.pop('mtime', None) m_interpolations = {} m_take_nearest = {} for item in m_time_varying: if item in efit_info and efit_info[item] is not None and len(tolist(efit_info[item])): if is_string(tolist(efit_info[item])[0]) or (len(m_time) < 2): # Handle strings or cases where there is only one time-slice printdq(' M-file: taking nearest value for {}'.format(item)) m_take_nearest[item] = efit_info[item] else: printdq(' M-file: interpolating {}'.format(item)) y = efit_info[item] if len(y) != len(m_time): y = efit_info[ item + '_meas' ] # There are identicaly named vars on a file and meas trees, this disambigous-izes the issue m_interpolations[item] = interpolate.interp1d(m_time, y, bounds_error=False, axis=0) else: printdq(' M-file: SKIPPING {} because it is missing or is None.'.format(item)) # Find out the dimentions dim_dict['dim_magpri'] = efit_info['expmpi'].shape[1] # n mag probles if len(efit_info['eccurt'].shape) < 2: dim_dict['dim_nesum'] = 1 printd('No E-coil for this device, apparently.') # Some devices, like KSTAR, don't have a separate CS. else: dim_dict['dim_nesum'] = efit_info['eccurt'].shape[1] # n e coils dim_dict['dim_nfcoil'] = efit_info['fccurt'].shape[1] # n f coils dim_dict['dim_nitera'] = efit_info['cchisq'].shape[1] # n iterations dim_dict['dim_npcurn'] = efit_info['xrsp'].shape[1] # ?? dim_dict['dim_nsilop'] = efit_info['silopt'].shape[1] # n flux loops dim_dict['dim_nstark'] = efit_info['a1gam'].shape[1] # n max MSE views EFIT can accomodate try: dim_dict['dim_npress'] = efit_info['pressr'].shape[1] # n pressure constraints except IndexError: # if there is no kinetic constraint, pressr will be 1D (dim_time only), and shape[1] will fail # a1gam don't have this issue, it will always be a 2D array even with no MSE dim_dict['dim_npress'] = 1 # Set up a collection to contain output out = output.setdefault('mEQDSK', {}) for t in times: # Figure out name and time window things m_file_name = 'm' + format(shot, "06d") + "." + format(int(t), "05d") if t - int(t) >= 1e-3: # Handle sub-ms timing to adding _000 to the filename m_file_name += '_{:03d}'.format(int(t - int(t) * 1000)) if min(abs(t - m_time)) > time_diff_warning_threshold: closest_efit = m_time[closestIndex(m_time, t)] if fail_if_out_of_range: note = 'FAIL' else: note = 'WARNING' printw( '{:}: requested time {:} ms is more than {:} ms away from closest time ({:} ms) in source EFIT!'.format( note, t, time_diff_warning_threshold, closest_efit ) ) if fail_if_out_of_range: printe('Skipped {:} because time difference was too large.'.format(m_file_name)) continue out[t] = m_file = OMFITmeqdsk(m_file_name) printdq('Initial keys in meqdsk: {}'.format(list(m_file.keys()))) # Save dimensions m_file['__dimensions__'] = dim_dict printq(' Loading {:}:'.format(m_file_name), end='') printq('CASE', end='') # Now stuff m file vars in. warn_mes = '' for item in m_time_varying: if item == 'mtime': continue # mtime is special, not a real var in m-files if item in m_interpolations: printq(',' + item, end='') x = m_interpolations[item](t) elif item in m_take_nearest: printq(',' + item, end='') x = m_take_nearest[item][closestIndex(a_time, t)] else: # Var was not uploaded to MDS+, implying it was arrays of 0 # The warning messages are accumulated and printed as once because otherwise they will be # awkwardly interleaved with the quantities being processed which are printed by printq. new_mes = f' M-file: {item} was missing, so was filled in with zero.' if (show_missing_data_warnings in [2, 'once', 'Once']): if new_mes not in bundled_warning_messages: bundled_warning_messages[new_mes] = True warn_mes += f' {new_mes} (this warning will not be repeated for the same quantity in subsequent time slices)\n' else: warn_mes += f' {new_mes}\n' # Don't want to interrupt the printq strings x = np.array([0]) x = np.atleast_1d(x) # check dimensions dim_name = m_dim_names[item] if dim_name is not None: dim = dim_dict[dim_name] if len(x) < dim: # pad out with 0 x = np.pad(x, (0, dim - len(x))) elif len(x) > dim: printw(f"WARNING: {item} as stored on MDS+ is too large an array. It will be trimmed down!") # This should never happen unless the uploader hit a bug! x = x[0:dim] else: # if dim_name is None -> there is no secondary dimention, only dimension is time # x needs to be an array of one element then x = np.atleast_1d(x) if len(x) > 1: printw(f"WARNING: {item} as stored on MDS+ is too large an array. It will be trimmed down!") x = x[0] elif len(x) < 0: # empty array is also not allowed x[0] = 0.0 # Make sure it is in float32, that what m files like their vars as. x = x.astype(np.float32) # shot is the only exception m_file.pack_it(x, item, m_longnames[item], dim1=dim_name, is_tmp=False) # Yes, dim1=None is ok # ---- # End of variable packing loop # 'Pack' special vars in the m file from omfit_classes.omfit_nc import OMFITncData m_file['shot'] = OMFITncData() m_file['shot']['data'] = np.array([shot]).astype(np.int32) m_file['shot']['long_name'] = 'shot number' m_file['shot']['__dimensions__'] = ('dim_scalar',) m_file['shot']['__dtype__'] = np.dtype(np.int32) m_file['time'] = OMFITncData() m_file['time']['data'] = np.array([t]).astype(np.float32) m_file['time']['units'] = 'msec' # yes, this the only var in the mfile with a unit m_file['time']['__dimensions__'] = ('dim_time',) m_file['time']['__dtype__'] = np.dtype(np.float32) printw2(warn_mes, already_handled_once=True) # save and maybe close m_file.save() if close: m_file.close() printq('Done getting M-files from MDSplus: {shot:}, {snap:}, {times:}'.format(shot=shot, snap=snap_file, times=times)) elif get_mfile: printe(f'Failed to gather mEQDSK data for {device}#{shot}, {snap_file}') printq('Done gathering EFIT from MDSplus.') return output
# fmt: on
[docs]class OMFIT_pcs_shape(OMFITascii, SortedDict): def __init__(self, filename, **kw): OMFITascii.__init__(self, filename, **kw) SortedDict.__init__(self, **kw) self.load() self['boundary'] = fluxSurface.BoundaryShape(rbbbs=self['Rbdry'], zbbbs=self['Zbdry'])
[docs] def load(self): with open(self.filename) as f: lines = f.readlines() bdry = False R = [] Z = [] for l in lines: if ':' in l: k, v = l.split(':', 2) v = v.strip() try: v = float(v) except Exception: pass self[k.strip()] = v continue if 'Xpoint 2' in self and l.strip() == '': bdry = True continue if bdry: if 'R' in l and 'Z' in l: continue i, r, z = list(map(float, l.split()[0:3])) R.append(r) Z.append(z) continue if l.strip() == '': continue print(l, 'not parsed') self['Rbdry'] = np.array(R) self['Zbdry'] = np.array(Z)
############################################ if '__main__' == __name__: test_classes_main_header() tmp = OMFITgeqdsk(OMFITsrc + '/../samples/g128913.01500')