Source code for omfit_classes.omfit_focus

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.omfit_ascii import OMFITascii

__all__ = ['OMFITfocusboundary', 'OMFITfocusharmonics']


[docs]class OMFITfocusboundary(SortedDict, OMFITascii): """ OMFITobject used to interface with boundary ascii files used in FOCUS. :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, 'r') as f: lines = f.readlines() # allow class to be initialized with a new (empty) file if len(lines) == 0: return # remove any empty lines someone might have thought harmless lines = [l for l in lines if len(l.translate({ord(char): '' for char in ' \n\t\r'})) > 0] # first is a single line of defining parameters keys = ['bmn', 'bnfp', 'nbf'] # lines[0].replace('#', '').split() vals = list(map(int, lines[1].split())) for k, v in zip(keys, vals): self[k.lower()] = v # then comes the plasma boundary start, end = 4, 4 + self['bmn'] keys = ['n', 'm', 'rbc', 'rbs', 'zbc', 'zbs'] # lines[start - 1].replace('#', '').split() vals = [] for l in lines[start:end]: vals.append(list(map(np.float, l.split()))) vals = np.array(vals).T self['boundary'] = SortedDict() for k, v in zip(keys, vals): if k in ('n', 'm'): self['boundary'][k.lower()] = v.astype(int) else: self['boundary'][k.lower()] = v # finally there is the bnormal coefficients start, end = end + 2, end + 2 + self['nbf'] keys = ['n', 'm', 'bnc', 'bns'] # lines[start - 1].replace('#', '').split() vals = [] for l in lines[start:end]: vals.append(list(map(np.float, l.split()))) vals = np.array(vals).T self['bn'] = SortedDict() if self['nbf'] > 0: for k, v in zip(keys, vals): if k in ('n', 'm'): self['bn'][k.lower()] = v.astype(int) else: self['bn'][k.lower()] = v else: for k in keys: self['bn'][k] = np.array([]) return
[docs] @dynaSave def save(self): """Write the data in standard ascii format""" with open(self.filename, 'w') as f: # write the defining parameters f.write('#{:>11}{:>12}{:>12}\n'.format('bmn', 'bnfp', 'nbf')) f.write('{:12}{:12}{:12}\n'.format(self['bmn'], self['bnfp'], self['nbf'])) # write the plasma boundary info f.write('#--------- plasma boundary --------\n') f.write('#{:>5}{:>6}{:>17}{:>17}{:>17}{:>17}\n'.format(*list(self['boundary'].keys()))) for i in range(self['bmn']): f.write( '{:6}{:6}{:17.9e}{:17.9e}{:17.9e}{:17.9e}\n'.format( self['boundary']['n'][i], self['boundary']['m'][i], self['boundary']['rbc'][i], self['boundary']['rbs'][i], self['boundary']['zbc'][i], self['boundary']['zbs'][i], ) ) # write the normal field info f.write(' #--------- bn harmonics --------\n') f.write(' #{:>4}{:>6}{:>17}{:>17}\n'.format(*list(self['bn'].keys()))) for i in range(self['nbf']): f.write( '{:6}{:6}{:17.9e}{:17.9e}\n'.format(self['bn']['n'][i], self['bn']['m'][i], self['bn']['bnc'][i], self['bn']['bns'][i]) ) return
[docs] @dynaLoad def plot(self, nd=3, ax=None, cmap='RdBu_r', vmin=None, vmax=None, colorbar=True, nzeta=60, ntheta=120, force_zeta=None, **kwargs): r""" Plot the normal field on the 3D boundary surface or as a contour by angle. :param nd: int. Choose 3 to plot a surface in 3D, and 2 to plot a contour in angle. :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. :param nzeta: int. Number of points in the toroidal angle. :param ntheta: int. Number of points in the poloidal angle (must be integer multiple of nzeta). :param force_zeta: list. Specific toroidal angle points (in degrees) used instead of nzeta grid. When nd=1, defaults to [0]. :param \**kwargs: dict. All other key word arguments are passed to the mplot3d plot_trisurf function. :return: Figure. """ 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: 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 try: ax = pyplot.subplot(111, aspect='equal', projection=projection) except NotImplementedError: ax = pyplot.subplot(111, projection=projection) # 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() # ensure multiples for trisurf if ntheta > nzeta: ntheta = int(np.round(float(ntheta) / nzeta)) * nzeta else: nzeta = int(np.round(float(nzeta) / ntheta)) * ntheta zeta = np.linspace(0, 2 * np.pi, nzeta, endpoint=(nd == 3)).reshape(1, nzeta) theta = np.linspace(-np.pi, np.pi, ntheta, endpoint=(nd == 3)).reshape(ntheta, 1) # use specific zeta values (usually for 1D plot) if nd == 1 and force_zeta is None: force_zeta = [0] if force_zeta is not None: force_zeta = np.deg2rad(np.atleast_1d(force_zeta)) zeta = force_zeta.reshape(1, len(force_zeta)) # inverse Fourier transform the boundary bo = self['boundary'] r, z = 0, 0 for i in range(self['bmn']): r += bo['rbc'][i] * np.cos(bo['m'][i] * theta - bo['n'][i] * zeta) + bo['rbs'][i] * np.sin( bo['m'][i] * theta - bo['n'][i] * zeta ) z += bo['zbc'][i] * np.cos(bo['m'][i] * theta - bo['n'][i] * zeta) + bo['zbs'][i] * np.sin( bo['m'][i] * theta - bo['n'][i] * zeta ) x = r * np.cos(zeta) y = r * np.sin(zeta) # inverse Fourier transform the normal field c = 0 * zeta * theta if 'bn' in self: bn = self['bn'] for i in range(self['nbf']): c += bn['bnc'][i] * np.cos(bn['m'][i] * theta - bn['n'][i] * zeta) + bn['bns'][i] * np.sin( bn['m'][i] * theta - bn['n'][i] * zeta ) if nd == 3: # manually map values to colors norm = pyplot.Normalize(vmin=vmin, vmax=vmax) colors = getattr(pyplot.cm, cmap)(norm(c)) # manually make a mappable object for the colorbar sm = pyplot.cm.ScalarMappable(cmap=cmap, norm=norm) sm.set_array(c) # 3d colored surface plot # im = ax.plot_surface(x, y, z, rstride=1, cstride=1, facecolors=colors, linewidth=0, # antialiased=False, shade=True, **kwargs) # trisurf creates a smoother surface, but coloring it properly is harder # it is somehow much faster (3 times faster for 90 by 180) # it can mess up if the number of points is not a multiple of three x, y, z = [np.ravel(x) for x in (x, y, z)] colors = list(map(matplotlib.colors.rgb2hex, colors.reshape(-1, 4))) tri = matplotlib.tri.Triangulation(*list(map(np.ravel, np.meshgrid(theta.ravel(), zeta.ravel())))) im = ax.plot_trisurf( x, y, z, triangles=tri.triangles, linewidth=0, facecolors=colors, antialiased=False, shade=True, cmap=getattr(pyplot.cm, cmap), **kwargs, ) # im.set_facecolors(colors) # does not work colors = np.mean(np.ravel(c)[tri.triangles], axis=1) im.set_array(colors) # im.autoscale() if colorbar: cb = f.colorbar(sm, ax=ax, shrink=0.8) cb.set_label('Normal Field (T)') # force equal aspect bound = np.max(np.hstack((np.abs(x.ravel()), np.abs(y.ravel()), np.abs(z.ravel())))) * 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)) elif nd == 2: # "unraveled" contour plot im = ax.imshow(c, origin='lower', extent=[0, 360, -180, 180], interpolation='gaussian', cmap=cmap, vmin=vmin, vmax=vmax) if colorbar: cb = f.colorbar(im) cb.set_label('Normal Field (T)') ax.set_xlabel('zeta (deg.)') ax.set_ylabel('theta (deg.)') elif nd == 1: for i, zeta_i in enumerate(zeta[0, :]): xi, yi, zi, ci = [val[:, i] for val in (x, y, z, c)] ri = np.sqrt(xi**2 + yi**2) # connect the endpoints ri = np.hstack((ri, ri[:1])) zi = np.hstack((zi, zi[:1])) ci = np.hstack((ci, ci[:1])) # make a line with changing color (https://matplotlib.org/3.1.1/gallery/lines_bars_and_markers/multicolored_line.html) points = np.array([ri, zi]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) norm = pyplot.Normalize(c.min(), c.max()) lc = matplotlib.collections.LineCollection(segments, cmap=cmap, norm=norm) # Set the values used for colormapping lc.set_array(ci) lw = kwargs.get('linewidth', kwargs.get('lw', rcParams['lines.linewidth'] + 3)) lc.set_linewidth(lw) (l,) = ax.plot(ri, zi, label=r'$\zeta$ = ' + '{:.0f}'.format(np.rad2deg(zeta_i)), lw=lw / 4) if not all(ci == 0): im = ax.add_collection(lc) else: colorbar = False im = l ax.autoscale(tight=False) ax.legend(loc=0, frameon=False, hide_markers=True, text_same_color=True) if colorbar: cb = f.colorbar(im, ax=ax) cb.set_label('Normal Field (T)') else: raise ValueError("Key word argument 'nd' must be 1, 2, or 3.") return im
[docs]class OMFITfocusharmonics(SortedDict, OMFITascii): """ OMFITobject used to interface with harmonics ascii files used in FOCUS. :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, 'r') as f: lines = f.readlines() # allow class to be initialized with a new (empty) file if len(lines) == 0: return # remove any empty lines someone might have thought harmless lines = [l for l in lines if len(l.translate({ord(char): '' for char in ' \n\t\r'})) > 0] # first is a single line of defining parameters self['nbf'] = int(lines[1]) # rest is one big table vals = np.array([list(map(np.float, l.split())) for l in lines[3:]]) self['n'] = vals[:, 0].astype(int) self['m'] = vals[:, 1].astype(int) self['bnc'] = vals[:, 2] self['bns'] = vals[:, 3] self['w'] = vals[:, 4]
[docs] @dynaSave def save(self): """Write the data in standard ascii format""" with open(self.filename, 'w') as f: # write the defining parameters f.write('#{:>5}\n'.format('nbf')) f.write('{:12}\n'.format(self['nbf'])) # write the normal field info f.write('#{:>5}{:>6}{:>17}{:>17}{:>17}\n'.format('n', 'm', 'bnc', 'bns', 'w')) for i in range(self['nbf']): f.write( '{:6}{:6}{:17.9e}{:17.9e}{:17.9e}\n'.format(self['n'][i], self['m'][i], self['bnc'][i], self['bns'][i], self['w'][i]) ) return
[docs] @dynaLoad def plot(self, axes=None, **kwargs): r""" Plot the normal field spectra and weighting. :param ax: list. 3 Axes into which the amplitude, phase, and weight plots will be made. :param \**kwargs: dict. All other key word arguments are passed to the plot function. :return: list. All the lines plotted. """ if axes 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 f, axes = pyplot.subplots(3, 1, sharex=True) else: # OMFIT (or the gcf needed to check OMFIT) made a empty figure for us ax = pyplot.subplot(3, 1, 1) axes = np.array([ax, pyplot.subplot(3, 1, 2, sharex=ax), pyplot.subplot(3, 1, 3, sharex=ax)]) else: f = axes[0].get_figure() c = self['bnc'] - 1j * self['bns'] lines = [] prefix = kwargs.pop('label', '') if prefix: prefix = prefix + ', ' for n in sorted(set(self['n'])): lbl = prefix + 'n = {:}'.format(n) i = np.where(self['n'] == n)[0] lines += axes[0].plot(self['m'][i], np.abs(c[i]), label=lbl, **kwargs) lines += axes[1].plot(self['m'][i], np.angle(c[i], deg=True), label=lbl, **kwargs) lines += axes[2].plot(self['m'][i], self['w'], label=lbl, **kwargs) # aesthetics axes[0].set_ylabel('Amplitude (T)') axes[1].set_ylabel('Phase (deg.)') axes[2].set_ylabel('Weight') for a in axes: a.set_xlabel('m') a.legend(loc=0).draggable(True) return lines