Source code for omfit_classes.omfit_coils

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

import numpy as np
import scipy as scipy
from omfit_classes.omfit_ascii import OMFITascii

__all__ = ['OMFITcoils', 'OMFITfocuscoils', 'OMFITGPECcoils']


[docs]class OMFITcoils(SortedDict, OMFITascii): """ OMFITobject used to interface with coils ascii files used in FOCUS and STELLOPT codes. :param filename: filename passed to OMFITascii class All additional key word arguments passed to OMFITascii """ def __init__(self, filename, **kwargs): SortedDict.__init__(self) OMFITascii.__init__(self, filename, **kwargs) self.dynaLoad = True
[docs] @dynaLoad def load(self): """Load the file and parse it into a sorted dictionary""" with open(self.filename) as f: lines = f.readlines() # allow class to be initialized with a new (empty) file if len(lines) == 0: return # first three lines are global parameters # only the first line has meanings for i in range(1): k, v = lines[i].split() if v.isdigit(): self[k] = int(v) else: self[k] = v self['num_coils'] = 0 # find end of data nlines = len(lines) end_line = 3 for i in range(-1, -nlines, -1): if lines[i].lstrip().lower().startswith('end'): end_line = nlines + i break # if no 'end' detected, use the last line if end_line == 3: end_line = nlines # find the start, stop of each coil start = 3 check = 0 num_coil = 0 while start < end_line: # defend against runaway while loops check += 1 if check > 1e6: raise RuntimeError("Found over 1e6 coils. That is too many!") # read each coil xyzc = [] for i in range(start, end_line): xyzc.append(list(map(np.float, lines[i].split()[:4]))) if len(lines[i].split()) != 4: # detect new coil group, name = lines[i].split()[4:6] num_coil += 1 # assign labels for each coil, even they have the same names if name not in self: label = name # default name else: for icoil in range(1, 10001): label = '{:}_{:}'.format(name, icoil) if label not in self: break if icoil == 10000: raise OMFITexception('Only 10000 or fewer coils can have the same name.') xyzc = np.array(xyzc) self[label] = SortedDict() self[label]['x'] = xyzc[:, 0] self[label]['y'] = xyzc[:, 1] self[label]['z'] = xyzc[:, 2] self[label]['I'] = xyzc[0, 3] # coil current is just a float value self[label]['group'] = int(group) self[label]['name'] = name start = i + 1 break self['num_coils'] = num_coil # total number of coils return
[docs] @dynaSave def save(self): """Write the data in standard ascii format""" with open(self.filename, 'w') as f: # write the defining parameters periods = list(self.keys())[0] f.write('{:} {:}\n'.format(periods, self[periods])) # the first line with periods f.write('{:} {:}\n'.format('begin', 'filament')) f.write('{:} {:}\n'.format('mirror', 'NIL')) # write each coil for key, coil in list(self.items()): if not isinstance(coil, dict): continue for i in range(len(coil['x']) - 1): f.write('{:23.15E}{:23.15E}{:23.15E}{:23.15E}\n'.format(coil['x'][i], coil['y'][i], coil['z'][i], coil['I'])) i = len(coil['x']) - 1 f.write( '{:23.15E}{:23.15E}{:23.15E}{:23.15E} {:>11} {:>11}\n'.format( coil['x'][i], coil['y'][i], coil['z'][i], 0.0, coil['group'], coil['name'] ) ) f.write('end\n') return
[docs] @dynaLoad def plot(self, ax=None, nd=3, cmap='coolwarm_r', vmin=None, vmax=None, colorbar=True, **kwargs): r""" Plot the coils in 3D, colored by their current values. :param ax: Axes. Axis into which the plot will be made. :param cmap: string. A valid matplolib color map name. :param vmin: float. Minimum of the color scaling. :param vmax: float. Maximum of the color scaling. :param colorbar: bool. Draw a colorbar for the currents. :param \**kwargs: dict. All other key word arguments are passed to the mplot3d plot function. :return: list of Line3D objects. The line plotted for each coil. """ if nd == 3: projection = '3d' else: projection = None if ax is None: # extra logic to handle OMFIT's auto-figure making when double-clicked in tree f = pyplot.gcf() if len(f.axes): # normal situation in which existing figures should be respected and left alone try: f, ax = pyplot.subplots(subplot_kw={'aspect': 'equal', 'projection': projection}) except NotImplementedError: # python3 matplolib did not have this for some reason pyplot.close(pyplot.gcf()) # will have opened a figure than failed to set the aspect f, ax = pyplot.subplots(subplot_kw={'projection': projection}) else: # OMFIT (or the gcf needed to check OMFIT) made a empty figure for us ax = f.use_subplot(111, projection=projection) try: ax.set_aspect('equal') except NotImplementedError: pass # aesthetics if nd == 3: ax.w_xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) ax.w_yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) ax.w_zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) ax.set_xlabel('x (m)') ax.set_ylabel('y (m)') ax.set_zlabel('z (m)') ax.set_axis_off() else: f = ax.get_figure() # defaults kwargs.setdefault('linewidth', 3) if 'lw' in kwargs: kwargs['linewidth'] = kwargs.pop('lw') keys = list(self.keys()) curs, lines = [], [] bound = 0 for key, coil in list(self.items()): if not isinstance(coil, dict): continue curs.append(coil['I']) if nd == 3: lines.append(ax.plot(coil['x'], coil['y'], coil['z'], label=key, **kwargs)[0]) bound = max(bound, np.max(np.abs(np.hstack((coil['x'], coil['y'], coil['z']))))) elif nd == 1: r = np.sqrt(coil['x'] ** 2 + coil['y'] ** 2) lines.append(ax.plot(r, coil['z'], label=key, **kwargs)[0]) bound = max(bound, np.max(np.abs(np.hstack((r, coil['z']))))) else: raise OMFITexception("Only 3D and 1D plots are available") # color by current from omfit_classes.utils_plot import set_linearray sm = set_linearray(lines, values=curs, vmin=vmin, vmax=vmax, cmap=cmap) if colorbar: cb = f.colorbar(sm, ax=ax, shrink=0.8) cb.set_label('Current (A)') # force equal aspect if nd == 3: bound *= 0.6 ax.set_xlim(-bound, bound) ax.set_ylim(-bound, bound) ax.set_zlim(-bound, bound) ax.auto_scale_xyz((-bound, bound), (-bound, bound), (-bound, bound)) return lines
[docs] @dynaLoad def to_OMFITfocuscoils(self, filename, nfcoil=8, target_length=0, coil_type=1, coil_symm=0, nseg=None, i_flag=1, l_flag=0): """ Convert to the OMFITfocuscoils ascii file format, which stores Fourier Harmonics of the coils. These files have additional settings, which can be set using key word arguments. Be sure to sync these with the focus input namelist! :param filename: str. Name of new file :param nfcoil: int. Number of harmonics in decomposition :param target_length: float. Target length of coils. Zero targets the initial length. :param coil_type: int. :param coil_symm: int. 0 for no symmetry. 1 for toroidal symmetry matching the boundary bnfp. :param nseg: int. Number of segments. Default (None) uses the number of segments in the original file :param i_flag: int. :param l_flag: int. :return: OMFITcoils """ coils = SortedDict() ckeys = [k for k, v in self.items() if isinstance(v, dict)] for i, key in enumerate(ckeys): # decompose x,y,z A = [] theta = np.linspace(0, 2 * np.pi, len(self[key]['x'])) for t in theta: A.append(np.ravel([(np.cos(m * t), np.sin(m * t)) for m in range(nfcoil + 1)])) A = np.array(A) csx = scipy.linalg.lstsq(A, self[key]['x'], 1e-2)[0] csy = scipy.linalg.lstsq(A, self[key]['y'], 1e-2)[0] csz = scipy.linalg.lstsq(A, self[key]['z'], 1e-2)[0] length = np.sum(np.sqrt(np.diff(self[key]['x']) ** 2 + np.diff(self[key]['y']) ** 2 + np.diff(self[key]['z']) ** 2)) # fill in info coils[key] = SortedDict() coils[key]['coil_type'] = coil_type # todo: learn what this is coils[key]['coil_symm'] = coil_symm if nseg is None: coils[key]['nseg'] = len(self[key]['x']) - 1 else: coils[key]['nseg'] = nseg coils[key]['current'] = self[key]['I'] coils[key]['i_flag'] = i_flag coils[key]['length'] = length coils[key]['l_flag'] = l_flag if target_length < 0: coils[key]['target_length'] = length else: coils[key]['target_length'] = target_length coils[key]['nfcoil'] = nfcoil coils[key]['xc'] = csx[0::2] coils[key]['xs'] = csx[1::2] coils[key]['yc'] = csy[0::2] coils[key]['ys'] = csy[1::2] coils[key]['zc'] = csz[0::2] coils[key]['zs'] = csz[1::2] # Use the OMFIT class to create an empty file and then update it in memory result = OMFITfocuscoils(filename) result.update(coils) return result
[docs] @dynaLoad def to_OMFITGPECcoils(self, filename): """ Convert OMFITcoils to the OMFITGPECcoils ascii file format, which writes x,y,z points but no current information. :param filename: str. Name of new file :return: OMFITGPECcoils """ # Use the OMFIT class to create an empty file and then update it in memory result = OMFITGPECcoils(filename) result.update(self) return result
[docs]class OMFITfocuscoils(SortedDict, OMFITascii): """ OMFITobject used to interface with focus ascii files used to describe coils in native FOCUS decomposition. :param filename: filename passed to OMFITascii class All additional key word arguments passed to OMFITascii """ def __init__(self, filename, **kwargs): SortedDict.__init__(self) OMFITascii.__init__(self, filename, **kwargs) self.dynaLoad = True
[docs] @dynaLoad def load(self): """Load the file and parse it into a sorted dictionary""" with open(self.filename) as f: lines = f.readlines() # allow class to be initialized with a new (empty) file if len(lines) == 0: return ncoils = int(lines[1].strip()) for i in range(ncoils): start = 2 + i * 14 ctype, csym, cname = lines[start + 2].split() self[cname] = SortedDict() self[cname]['coil_type'] = int(ctype) self[cname]['coil_symm'] = int(csym) keys = lines[start + 3].split()[1:] vals = lines[start + 4].split() self[cname]['nseg'] = int(vals[0]) self[cname]['current'] = float(vals[1]) self[cname]['i_flag'] = int(vals[2]) self[cname]['length'] = float(vals[3]) self[cname]['l_flag'] = int(vals[4]) self[cname]['target_length'] = float(vals[5]) self[cname]['nfcoil'] = int(lines[start + 6].strip()) for j, key in enumerate(['xc', 'xs', 'yc', 'ys', 'zc', 'zs']): self[cname][key] = np.array(lines[start + 8 + j].split(), dtype=float) return
[docs] @dynaSave def save(self): """Write the data in standard ascii format""" with open(self.filename, 'w') as f: # write the total number f.write(' # ncoils\n') f.write('{:12}\n'.format(len(self))) for i, key in enumerate(self.keys()): f.write(' #--------------------------------------------\n') f.write(' # coil_type coil_symm coil_name\n') f.write(' {:12} {:12} {:>12}\n'.format(self[key]['coil_type'], self[key]['coil_symm'], key)) keys = ['nseg', 'current', 'i_flag', 'length', 'l_flag', 'target_length'] vals = [self[key][k] for k in keys] f.write(' # {:>6} {:>18} {:>6} {:>18} {:>6} {:>18}\n'.format(*keys)) f.write(' {:6} {:18.9E} {:6} {:18.9E} {:6} {:18.9E}\n'.format(*vals)) f.write(' # nfcoil\n') f.write(' {:12}\n'.format(self[key]['nfcoil'])) f.write(' # Fourier harmonics for coils ( xc; xs; yc; ys; zc; zs)\n') for k in ['xc', 'xs', 'yc', 'ys', 'zc', 'zs']: f.write((' {:23.15E}' * len(self[key][k]) + '\n').format(*self[key][k])) return
[docs] @dynaLoad def plot(self, bnfp=1, ax=None, cmap='coolwarm_r', vmin=None, vmax=None, colorbar=True, **kwargs): r""" Plot the coils in 3D, colored by their current values. :param bnfp: int. Toroidal mode number of symmetry used if coil_symm is 1 :param ax: Axes. Axis into which the plot will be made. :param cmap: string. A valid matplolib color map name. :param vmin: float. Minimum of the color scaling. :param vmax: float. Maximum of the color scaling. :param colorbar: bool. Draw a colorbar for the currents. :param \**kwargs: dict. All other key word arguments are passed to the mplot3d plot function. :return: list of Line3D objects. The line plotted for each coil. """ if ax is None: # extra logic to handle OMFIT's auto-figure making when double-clicked in tree f = pyplot.gcf() if len(f.axes): # normal situation in which existing figures should be respected and left alone try: f, ax = pyplot.subplots(subplot_kw={'aspect': 'equal', 'projection': '3d'}) except NotImplementedError: # python3 matplolib did not have this for some reason pyplot.close(pyplot.gcf()) # will have opened a figure than failed to set the aspect f, ax = pyplot.subplots(subplot_kw={'projection': '3d'}) else: # OMFIT (or the gcf needed to check OMFIT) made a empty figure for us ax = pyplot.subplot(111, projection='3d') try: ax.set_aspect('equal') except NotImplementedError: pass # aesthetics ax.w_xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) ax.w_yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) ax.w_zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0)) ax.set_xlabel('x (m)') ax.set_ylabel('y (m)') ax.set_zlabel('z (m)') ax.set_axis_off() else: f = ax.get_figure() curs, lines = [], [] bound = 0 for k, v in list(self.items()): curs.append(v['current']) nseg = v['nseg'] eta = np.linspace(0, 2 * np.pi, nseg + 1, True) ms = np.arange(v['nfcoil'] + 1) x = np.real(np.sum((v['xc'] - 1j * v['xs'])[:, None] * np.exp(1j * ms[:, None] * eta), 0)) y = np.real(np.sum((v['yc'] - 1j * v['ys'])[:, None] * np.exp(1j * ms[:, None] * eta), 0)) z = np.real(np.sum((v['zc'] - 1j * v['zs'])[:, None] * np.exp(1j * ms[:, None] * eta), 0)) lines.append(ax.plot(x, y, z, label=k, **kwargs)[0]) bound = max(bound, np.max(np.abs(np.hstack((x, y, z))))) if v['coil_symm'] == 1: for isym in range(1, bnfp): sx = np.real(x * np.exp(1j * 2 * np.pi * isym / bnfp)) sy = np.real(y * np.exp(1j * 2 * np.pi * isym / bnfp)) sz = z * 1.0 curs.append(v['current']) lines.append(ax.plot(sx, sy, sz, label=f'{k}_symm{isym}', **kwargs)[0]) bound = max(bound, np.max(np.abs(np.hstack((sx, sy, sz))))) # color by current from omfit_classes.utils_plot import set_linearray sm = set_linearray(lines, values=curs, vmin=vmin, vmax=vmax, cmap=cmap) if colorbar: cb = f.colorbar(sm, ax=ax, shrink=0.8) cb.set_label('Current (A)') # force equal aspect bound *= 0.6 ax.set_xlim(-bound, bound) ax.set_ylim(-bound, bound) ax.set_zlim(-bound, bound) ax.auto_scale_xyz((-bound, bound), (-bound, bound), (-bound, bound)) return lines
@dynaLoad def _to_OMFITcoils(self, filename, bnfp=1, gpec_format=False): """ Convert to the standard OMFITcoils ascii file format, which stores x,y,z,i points by inverse Fourier transforming the FOCUS coil decompositions. :param filename: str. Name of new file :param bnfp: int. Toroidal mode number of symmetry used if coil_symm is 1 :param gpec_format: bool. Convert to OMFITGPECcoils ascii formatting instead :return: OMFITcoils or OMFITGPECcoils """ coils = SortedDict() coils['periods'] = 1 coils['num_coils'] = len(list(self.keys())) for i, (key, coil) in enumerate(list(self.items())): nseg = coil['nseg'] eta = np.linspace(0, 2 * np.pi, nseg + 1, True) ms = np.arange(coil['nfcoil'] + 1) x = np.real(np.sum((coil['xc'] - 1j * coil['xs'])[:, None] * np.exp(1j * ms[:, None] * eta), 0)) y = np.real(np.sum((coil['yc'] - 1j * coil['ys'])[:, None] * np.exp(1j * ms[:, None] * eta), 0)) z = np.real(np.sum((coil['zc'] - 1j * coil['zs'])[:, None] * np.exp(1j * ms[:, None] * eta), 0)) coils[key] = SortedDict() coils[key]['x'] = x coils[key]['y'] = y coils[key]['z'] = z coils[key]['I'] = coil['current'] * 1.0 coils[key]['group'] = i + 1 # todo: can we get the group from FOCUS coils? coils[key]['name'] = key # if there is symmetry we need to add appropriately rotated coils if coil['coil_symm'] == 1: for isym in range(1, bnfp): skey = f'{key}_symm{isym}' coils[skey] = SortedDict() coils[skey]['x'] = np.real(x * np.exp(1j * 2 * np.pi * isym / bnfp)) coils[skey]['y'] = np.real(y * np.exp(1j * 2 * np.pi * isym / bnfp)) coils[skey]['z'] = z * 1.0 coils[skey]['I'] = coil['current'] * 1.0 coils[skey]['group'] = i + 1 # todo: Confirm this should match for symm coils coils[skey]['name'] = skey # Use the OMFIT class to create an empty file and then update it in memory if gpec_format: result = OMFITGPECcoils(filename) else: result = OMFITcoils(filename) result.update(coils) return result
[docs] def to_OMFITcoils(self, filename, bnfp=1): """ Convert to the standard OMFITcoils ascii file format, which stores x,y,z,i points by inverse Fourier transforming the FOCUS coil decompositions. :param filename: str. Name of new file :param bnfp: int. Toroidal mode number of symmetry used if coil_symm is 1 :param gpec_format: bool. Convert to OMFITGPECcoils ascii formatting instead :return: OMFITcoils or OMFITGPECcoils """ # Use a central functions so we do not duplicate complicated # inverse Fourier transformation code result = self._to_OMFITcoils(filename, bnfp=bnfp, gpec_format=False) return result
[docs] @dynaLoad def to_OMFITGPECcoils(self, filename): """ Convert OMFITcoils to the OMFITGPECcoils ascii file format, which writes x,y,z points but no current information. :param filename: str. Name of new file :param bnfp: int. Toroidal mode number of symmetry used if coil_symm is 1 :return: OMFITGPECcoils """ # Use a central functions so we do not duplicate complicated # inverse Fourier transformation code result = self._to_OMFITcoils(filename, bnfp=bnfp, gpec_format=True) return result
[docs]class OMFITGPECcoils(OMFITcoils): """ Class that reads ascii GPEC .dat coil files and converts them to the standard coil file format used by the FOCUS and STELLOPT codes. NOTE: This will clobber the original ascii formatting!! Do not connect to original file paths!! """
[docs] @dynaLoad def load(self): """Load the file and parse it into a sorted dictionary""" # read the ascii file try: with open(self.filename, 'r') as f: line1 = f.readline() ncoil, s, nsec, nw = list(map(int, list(map(float, line1.split())))) x, y, z = np.genfromtxt(self.filename, skip_header=1).T.reshape(3, ncoil, s, nsec) c = nw # fill in all the info needed for a coil file coils = self coils['periods'] = s # this isn't really right, but we need somewhere to store the info coils['num_coils'] = ncoil nc = 0 for i in range(ncoil): for j in range(s): key = 'coil_{:}'.format(nc) coils[key] = SortedDict() coils[key]['x'] = x[i, j, :] coils[key]['y'] = y[i, j, :] coils[key]['z'] = z[i, j, :] coils[key]['I'] = nw # should be multiplied by nominal current coils[key]['group'] = i + 1 coils[key]['name'] = os.path.split(self.filename)[-1].split('.')[0] nc += 1 # coils['num_coils'] = nc # why was this here? already set ncoil above... why was this ncoil + 1? except Exception: OMFITcoils.load(self)
[docs] @dynaSave def save(self): """Write the data in standard ascii format""" print(self.filename) with open(self.filename, 'w') as f: # write the defining parameters ncoil = self['num_coils'] s = self['periods'] # hack storing this info here nsec = len(self[list(self.keys())[-1]]['x']) nw = self[list(self.keys())[-1]]['I'] if nsec < 1e4: f.write('{:>5}{:>5}{:>5}{:8.2f}\n'.format(ncoil, s, nsec, nw)) # the first line with periods else: f.write('{:>5}{:>5} {:}{:8.2f}\n'.format(ncoil, s, nsec, nw)) # the first line with periods # write each coil x, y, z for key, coil in list(self.items()): if not isinstance(coil, dict): continue for i in range(len(coil['x'])): f.write('{:13.4e}{:13.4e}{:13.4e}\n'.format(coil['x'][i], coil['y'][i], coil['z'][i])) return
[docs] @dynaLoad def to_OMFITcoils(self, filename): """ Convert to the standard OMFITcoils ascii file format, which stores x,y,z,i points. :param filename: str. Name of new file :return: OMFITcoils """ # Use the OMFIT class to create an empty file and then update it in memory result = OMFITcoils(filename) result.update(self) # convert from gpec convention of coils with sections to standard individual coils convention self['num_coils'] = self['num_coils'] * self['periods'] coils['periods'] = 1 # remove hack where gpec stores it's info about coil sections return result