Source code for boutpy.boutdata.field

"""Field class
"""

from __future__ import (absolute_import, division,
                        print_function, unicode_literals)

__all__ = ['Field']

__date__ = '10192017'
__version__ = '0.1.2'
__author__ = 'J. G. Chen'
__email__ = 'cjgls@pku.edu.cn'

import os

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.axes_grid1 import make_axes_locatable


# test animation writer for movies
try:
    # using system's ffmpeg
    animation.writers['ffmpeg']
except:
    # using ~/bin/ffmpeg
    # conda install -c conda-forge ffmpeg
    # or download from
    # github.com/imageio/imageio-binaries/tree/master/ffmpeg
    ffmpeg = os.path.join(os.path.dirname(__file__),
                          "../bin/ffmpeg")
    plt.rcParams['animation.ffmpeg_path'] = ffmpeg

# global vars
_pause = False


def _onclick(event, anim):
    """Pause/continue animation by mouse event

    Parameters
    ----------
    event: :class:`matplotlib.backend_bases.Event`
    anim: :class:`matplotlib.animation.FuncAnimation`

    Notes
    -----
    using ``lambda`` function to pass the `arguments` to this function:
        mpl_connect('button_press_event', lambda event: _onclick(event, anim))

    """

    global _pause
    _pause ^= True
    if _pause:
        anim.event_source.stop()
    else:
        anim.event_source.start()


def _cal_lim(data, percent=0.05):
    """ calculate data limit for axis-limit in plot

    Parameters
    ----------
    data: data in any type
    percent: scalar, >= 0, [default: 0.05]
        extend the limit in both side by `percent` of total range
        if percent < 0, auto-reset to 0

    Retruns
    -------
    ret: list
        [min, max]

    Notes
    -----
    if max(data) == min(data), then
        ret = [min-0.05*min, min+0.05*min]
        (ret = [min-1, min+1] if min=0)

    Examples
    --------
    >>> data = [0, 1]
    >>> _cal_lim(data, 0.5)
    # [-0.5, 1.5]
    >>> data = [1, 1]
    >>> _cal_lim(data, 0.5)
    # [0.9499999999999996, 1.05]

    """

    dmin, dmax = np.min(data), np.max(data)
    length = dmax - dmin    # length >= 0
    if percent < 0:
        percent = 0
    extend = length * percent
    if length <= 0:
        # dmax = dmin
        extend = 0.05 * np.abs(dmax) if np.abs(dmax) > 0 else 1
    return [dmin - extend, dmax + extend]


[docs]class Field(np.ndarray): """Field(data, dtype=None, copy=True) Methods ------- moment_xyzt(axis=-1) return (dc, rms) parts at axis=`axis`. dc(axis=-1) return dc parts at axis=`axis`. rms(axis=-1) return rms parts at axis=`axis`. normalize(norm=None) data normalized by `norm` if `norm` is given, otherwise `maximum` used. plot(\*arg, \*\*kwargs) plot line(s) or animated lines for 1D/2D data. contourf(\*args, \*\*kwargs) plot (animated) contour/contourf/surface plot for 2D/3D data. showdata(\*args, \*\*kwargs) Visualiaztion and animation for 1/2/3D data. """ __array_priority__ = 100000 def __new__(subtype, data, dtype=None, copy=True, order=None, subok=False, ndmin=0): if isinstance(data, Field): dtype2 = data.dtype if (dtype is None): dtype = dtype2 if (dtype2 == dtype) and (not copy): return data if type(data) is not subtype and not(subok and isinstance(data, subtype)): data = data.view(subtype) return np.array(data, dtype=dtype, copy=copy, order=order, subok=True, ndmin=ndmin) if isinstance(data, np.ndarray): if dtype is None: intype = data.dtype else: intype = np.dtype(dtype) new = data.view(subtype) if intype != data.dtype: return new.astype(intype) if copy: return new.copy() else: return new ret = np.array(data, dtype=dtype, copy=copy, order=order, subok=True, ndmin=ndmin).view(subtype) return ret def __array_wrap__(self, obj): """Called after ufuncs to do cleanup work. """ # if ufunc output is scalar, return it if obj.shape == (): return obj[()] else: return np.ndarray.__array_wrap__(self, obj) def __array_finalize__(self, obj): pass ''' WARNING: This operator overload change the rules of numpy.ndarray operation. See Also: Broadcasting of numpy It can be achieved by 'self + other[:, :, None]' without overload operator # overload operator +, - def __add__(self, other): """ overload add function for case self.shape[:-1] = other.shape Examples -------- >> a = Field(np.arange(2*3).reshape(2,3)) >> b = np.asarray([10, 10]) >> a.shape (2, 3) >> b.shape (2,) >> a+b Field([[10, 11, 12], [13, 14, 15]]) """ # sort dimension size sdata, ldata = ((self, np.asarray(other)) if np.asarray(other).ndim > self.ndim else (np.asarray(other), self)) sshape, lshape = sdata.shape, ldata.shape # if they are in same shape, or one is just a scalar if sshape == lshape or len(sshape) == 0: ret = self.view(np.ndarray) + np.asarray(other) elif lshape[:-1] == sshape: ret = np.asarray(ldata) + np.tile( np.reshape(sdata, sshape+(1,)), lshape[-1]) else: raise ValueError("operands could not be broadcast together with " "shapes {} {}".format(self.shape, np.asarray(other).shape)) return ret.view(Field) def __sub__(self, other): return self + (-other) def __radd__(self, other): return self + other def __rsub__(self, other): return -self + other def __iadd__(self, other): return self + other def __isub__(self, other): return self + (-other) '''
[docs] def moment_xyzt(self, axis=-1): """return (dc, rms) parts at axis=`axis`. """ return self.mean(axis=axis).squeeze(), self.std(axis=axis).squeeze()
[docs] def dc(self, axis=-1): """return dc part at axis=`axis`. """ return self.mean(axis=axis)
[docs] def rms(self, axis=-1): """return rms part at axis=`axis`. """ return self.std(axis=axis)
[docs] def normalize(self, norm=None): """return normalizaed array. Parameters ---------- norm : float, optional return result normalized to ``norm`` if it is given, otherwise result is normalized to maximum value by default. """ if not isinstance(norm, (int, float)): norm = self.max() return self/float(norm)
[docs] def plot(self, *args, **kwargs): """plot line(s) or animated lines for 1D/2D data Parameters ---------- kind: str, optional, default: 'surface' work for 2D data ['m', 'mlines', 'multilines']: multi-lines in one figure 'animation': animated lines newfig: bool, optional, default: True if Ture[default], create new figure, otherwise, plot on current figure and axes x/ylabel: str, optional, default: None labels for x/y-axis, xlim: 2-elements data, optional, default: None axis limit for x-axis ylim: [2-elements data | 'fixed'], optional, default: None axis limit for y-axis 'fixed': automatically set the ylim according to the data limit. title: str, optional, default: None figure title in animation, it is passed to 'title.format(frame_number)' or 'index = {}".format(frame_number) if it is None. grid: bool, optional, default: True grid on if True. output: str, optional, default: None filename to store the figure or animation. It should include suffix. '.gif' format is recommended. **key-word arguments for 2D data**: index: int | array_like, default: 1 - array_like: only using data[:, index] for animation. - int: only using data[:, ::index] for animation, the last frame is included as well. **key-word argments for animation**: interval: int, optional, default: 20 delay between frames in milliseconds. fps: number, optional, default: 5 frames per second in the movie. metadata: dict, optional, default: {artist:$USER} Dictionary of keys and values for metadata to include in the output file. Some keys that may be use include: title, artist, genre, subject, copyright, srcform, comment. bitrate: number, optional, default: -1 Specifies the number of bits used per second in the compressed movie, in kilobits per second. A higher number means a higher quality movie, but at the cost of increased file size. -1 means let utility auto-determine. Returns ------- ret: figure object|animation object See Also -------- :func:`matplotlib.pyplot.plot` """ # create new fig? newfig = kwargs.pop('newfig', True) if newfig: fig, ax = plt.subplots() else: fig = plt.gcf() ax = plt.gca() # auto squeeze array ndim = self.squeeze().ndim if ndim not in [1, 2]: raise ValueError("ERROR: only 1D/2D data supported.") return None xsize = self.squeeze().shape[0] # xdata for x-axis x = kwargs.pop('x', None) if x is None: x = np.arange(xsize) # properties of fig xlabel = kwargs.pop('xlabel', None) ylabel = kwargs.pop('ylabel', None) xlim = kwargs.pop('xlim', None) ylim = kwargs.pop('ylim', None) title = kwargs.pop('title', None) if xlabel: ax.set_xlabel(xlabel) if ylabel: ax.set_ylabel(ylabel) if xlim: ax.set_xlim(xlim) if ylim == 'fixed': ax.set_ylim(_cal_lim(self)) elif isinstance(ylim, (list, tuple, np.ndarray)): ax.set_ylim(ylim) if title: ax.set_title(title) grid = kwargs.pop('grid', True) if grid: ax.grid() # save fig/animation to as `output` output = kwargs.pop('output', None) # animation or multi-lines for 2D data kind = kwargs.pop('kind', 'animation') # TODO: output option # animation along selected axis?? # or axis=-1 by default # plot line if ndim == 1: ret = ax.plot(x, self.squeeze(), *args, **kwargs) if output: plt.tight_layout() fig.savefig(output) return ret # animation[default]/multi-lines if ndim == 2: index = kwargs.pop('index', 1) ysize = self.squeeze().shape[1] if isinstance(index, int): index = range(0, ysize, index) # include last step if index[-1] != ysize - 1: index.append(ysize - 1) # options for animation interval = kwargs.pop('interval', 20) fps = kwargs.pop('fps', 5) metadata = kwargs.pop('metadata', dict(artist=os.environ['USER'])) bitrate = kwargs.pop('bitrate', -1) # multi-lines if kind in ['m', 'mlines', 'multilines']: ret = ax.plot(x, self.squeeze()[:, index], *args, **kwargs) ax.legend(index, loc=0) return ret # animation [default] line, = ax.plot([], [], *args, **kwargs) if xlim is None: ax.set_xlim(_cal_lim(x)) # initialization function: plot the background of each frame def init(): line.set_data([], []) return line, # animation function: This is called sequentially def animate(i): # update title, xlim, ylim and line if ylim == 'fixed': ax.set_ylim(_cal_lim(self[:, index])) elif isinstance(ylim, (list, tuple, np.ndarray)): ax.set_ylim(ylim) else: ax.set_ylim(_cal_lim(self[:, i])) ax.set_title(title.format(i) if title else "index = {}".format(i)) # ax.figure.canvas.draw() line.set_data(x, self[:, i]) return line, anim = animation.FuncAnimation( fig, animate, frames=index, init_func=init, blit=False, # if True, then cannot update `title/xlim/ylim` repeat=True, interval=interval, repeat_delay=1000, # milliseconds ) fig.canvas.mpl_connect('button_press_event', lambda event: _onclick(event, anim)) plt.show() if output: if output.endswith('.gif'): writer = 'imagemagick' try: animation.writers['imagemagick'] except KeyError: raise ValueError( "WARNING: save animation to gif not supported!\n" " try to intstall ImageMagick by:\n" " $ conda install -c kalefranz imagemagick!") else: print("WARNING: the movie in this format may not work " " perfectly. '.gif' format is recommended!") writer = 'ffmpeg' anim.save(output, writer=writer, fps=fps, metadata=metadata, bitrate=bitrate) return anim
[docs] def contourf(self, *args, **kwargs): """Plot (animated) contour/contourf/surface plot for 2D/3D data. Parameters ---------- kind: str, optional, default: 'surface' - ['c', 'contour', 1, 'cf', 'contourf', 2]: contour/contourf plot - 'surface': 3D-surface plot newfig: bool, optional, default: True if Ture[default], create new figure, otherwise, plot on current figure and axes x/y: 1D array_like, optional, default: None the size should be same as data in x/y direction. x/y/zlabel: str, optional, default: None labels for x/y/z-axis, zleft: bool, optional, default: True draw z-axis at left side x/ylim: 2-elements data, optional, default: None axis limit for x/y-axis if None, auto-set to its index: [0, 1, ..., x/ysize-1] zlim: 2-elements data, optional, default: None axis limit for z-axis if None, auto-set to [`vmin`, `vmax`] vmin/vmax: number, optional, default: None set color limit to [`vmin`, `vmax`] if None, auto-set to the range of data title: str, optional, default: None figure title in animation, it is passed to 'title.format(frame_number)' or 'index = {}".format(frame_number) if it is None. cmap: Colormap, optional, default: plt.cm.bwr A cm Colormap instance. colorbar: bool, optional, default: True if True, plot colorbar cticks: 1D array_like, optional, default: None colorbar ticks. if None, auto-set to evenly spaced interval [`vmin`, `vmax`] by factor `ncticks`. ncticks: int, optional, default: 5 the number of ticks of colorbar when `cticks` is not set. levels: 1D array_like, optional, default: None a list of floating point numbers indicating the level curves to draw, in increasing order. if None, auto-set to evenly spaced interval [`vmin`, `vmax`] by factor `nlevels`. nlevels: int, optional, default: 40 the number of levels to contour/contourf draw. output: str, optional, default: None filename to store the figure or animation. It should include suffix. '.gif' format is recommended. **key-word arguments for 2D data**: index: int | array_like, default: 1 - array_like: only using data[:, index] for animation. - int: only using data[:, ::index] for animation, the last frame is included as well. **key-word argments for animation**: interval: int, optional, default: 20 delay between frames in milliseconds. fps: number, optional, default: 5 frames per second in the movie. metadata: dict, optional, default: {artist:$USER} Dictionary of keys and values for metadata to include in the output file. Some keys that may be use include: title, artist, genre, subject, copyright, srcform, comment. bitrate: number, optional, default: -1 Specifies the number of bits used per second in the compressed movie, in kilobits per second. A higher number means a higher quality movie, but at the cost of increased file size. -1 means let utility auto-determine. Returns ------- ret: (figure object|animation object, colorbar object if exists) See Also -------- :func:`matplotlib.pyplot.contour` :func:`matplotlib.pyplot.contourf` :meth:`mpl_toolkits.mplot3d.Axes3D.plot_surface` """ # create new fig? newfig = kwargs.pop('newfig', True) if newfig: fig, ax = plt.subplots() else: fig = plt.gcf() ax = plt.gca() # auto squeeze array ndim = self.squeeze().ndim if ndim not in [2, 3]: raise ValueError("ERROR: only 2D/3D data supported.") return None xsize, ysize = self.squeeze().shape[:2] # xdata/ydata for x/y-axis x = kwargs.pop('x', None) y = kwargs.pop('y', None) if x is None: x = np.arange(xsize) if y is None: y = np.arange(ysize) # properties of fig xlabel = kwargs.pop('xlabel', None) ylabel = kwargs.pop('ylabel', None) zlabel = kwargs.pop('zlabel', None) zleft = kwargs.pop('zleft', True) if xlabel: ax.set_xlabel(xlabel) if ylabel: ax.set_ylabel(ylabel) xlim = kwargs.pop('xlim', None) ylim = kwargs.pop('ylim', None) zlim = kwargs.pop('zlim', None) title = kwargs.pop('title', None) # colorbar # NOTE: if colorbar range is larger than data range, # than explicit 'levels' should be set to show # the full color range. colorbar = kwargs.pop('colorbar', True) cb = None # colorbar object # set color limit vmin = kwargs.get('vmin', None) vmax = kwargs.get('vmax', None) if vmin is None: vmin = self.min() if vmax is None: vmax = self.max() if vmin == vmax: vmax += 1 kwargs['vmin'], kwargs['vmax'] = vmin, vmax if zlim is None: zlim = [vmin, vmax] cticks = kwargs.pop('cticks', None) ncticks = kwargs.pop('ncticks', 5) if cticks is None: cticks = np.linspace(vmin, vmax, ncticks, endpoint=True) if 'cmap' not in kwargs: kwargs['cmap'] = plt.cm.bwr levels = kwargs.pop('levels', None) nlevels = kwargs.pop('nlevels', 40) # set number of contour color levels if levels is None: levels = np.linspace(vmin, vmax, nlevels, endpoint=True) # save fig/animation to as `output` output = kwargs.pop('output', None) # plot kinds: contour, contourf, surface kind = kwargs.pop('kind', 'surface') if kind in ['c', 'contour', 1]: kind = 'contour' elif kind in ['cf', 'contourf', 2]: kind = 'contourf' else: kind = 'surface' if kind == 'surface': ax.remove() ax = fig.add_subplot(111, projection='3d') # draw z-axis at left side # http://stackoverflow.com/questions/15042129/changing-position # -of-vertical-z-axis-of-3d-plot-matplotlib/15048653#15048653 if zleft: tmp_planes = ax.zaxis._PLANES ax.zaxis._PLANES = (tmp_planes[2], tmp_planes[3], tmp_planes[0], tmp_planes[1], tmp_planes[4], tmp_planes[5]) view_1 = (25, -135) view_2 = (25, -45) init_view = view_1 ax.view_init(*init_view) ax.tick_params(axis='z', pad=20) ax.zaxis.set_rotate_label(False) X, Y = np.meshgrid(y, x) if xlabel: ax.set_xlabel(xlabel, labelpad=25) if ylabel: ax.set_ylabel(ylabel, labelpad=25) if zlabel: ax.set_zlabel(zlabel, labelpad=45, rotation=90) if zlim: ax.set_zlim(zlim) if xlim: ax.set_xlim(xlim) if ylim: ax.set_ylim(ylim) if title: ax.set_title(title) # 2D data if ndim == 2: if kind in ['contour', 'contourf']: kwargs['levels'] = levels if kind == 'contour': ret = ax.contour(x, y, self.squeeze().T, *args, **kwargs) ret.set_clim(vmin, vmax) elif kind == 'contourf': ret = ax.contourf(x, y, self.squeeze().T, *args, **kwargs) ret.set_clim(vmin, vmax) else: ret = ax.plot_surface(X, Y, self.squeeze(), *args, **kwargs) if colorbar: cb = plt.colorbar(ret, ticks=cticks) plt.tight_layout() if cb: return ret, cb return ret # animation if ndim == 3: # position the colorbar, it's easier to animate the colorbar # div = make_axes_locatable(ax) # cax = div.append_axes('right', '2%', '3%') # plt.subplots_adjust(right=0.85) zsize = self.squeeze().shape[2] index = kwargs.pop('index', 1) if isinstance(index, int): index = range(0, zsize, index) # include last step if index[-1] != zsize - 1: index.append(zsize - 1) # options for animation interval = kwargs.pop('interval', 20) fps = kwargs.pop('fps', 5) metadata = kwargs.pop('metadata', dict(artist=os.environ['USER'])) bitrate = kwargs.pop('bitrate', -1) # initialization function: plot the background of each frame def init(): ax.cla() if xlabel: ax.set_xlabel(xlabel) return # animation function: This is called sequentially def animate(i): # update plot ax.cla() if zlim and kind == 'surface': ax.set_zlim(zlim) # set_title by 'position' to avoid change in animation ax.set_title(title.format(i) if title else "index = {}".format(i), position=(0.5, 1.05), transform=ax.transAxes, horizontalalignment='center') if xlabel: ax.set_xlabel(xlabel) if ylabel: ax.set_ylabel(ylabel) if zlabel: ax.set_zlabel(zlabel) # ax.figure.canvas.draw() if kind == 'contour': ret = ax.contour(x, y, self[:, :, i].squeeze().T, *args, **kwargs) elif kind == 'contourf': ret = ax.contourf(x, y, self[:, :, i].squeeze().T, *args, **kwargs) else: ret = ax.plot_surface(X, Y, self[:, :, i].squeeze(), *args, **kwargs) return ret if colorbar and kind != 'surface': kwargs['levels'] = levels ret = animate(index[0]) cb = fig.colorbar(ret, ticks=cticks) anim = animation.FuncAnimation( fig, animate, frames=index, init_func=init, blit=False, # if True, then cannot update `title/xlim/ylim` repeat=True, interval=interval, repeat_delay=1000, # milliseconds ) fig.canvas.mpl_connect('button_press_event', lambda event: _onclick(event, anim)) plt.show() if output: if output.endswith('.gif'): writer = 'imagemagick' try: animation.writers['imagemagick'] except KeyError: raise ValueError( "WARNING: save animation to gif not supported!\n" " try to intstall ImageMagick by:\n" " $ conda install -c kalefranz imagemagick!") else: writer = 'ffmpeg' anim.save(output, writer=writer, fps=fps, metadata=metadata, bitrate=bitrate) if cb: return anim, cb return anim
[docs] def showdata(self, *args, **kwargs): """Visualisation and animation for 1-3D data. This function is just a wrapper function for **plot** and **contourf**. All **args** and **kwargs** are passed to these functions according to the dimension of data and key 'kind'. Parameters ---------- kind: str plot type. the value for different dimension of data: - 1D-data: None: plot single line - 2D-data: - 'surface': surface plot [default] - 'c', 'contour', 1, 'cf', 'contourf', 2: contour/contourf plot - 'm', 'multi', 'multilines': multi-lines in one figure - 'animation': animated lines - 3D-data: 'c', 'contour', 1; 'cf', 'contourf', 2; 'surface': animated contour/contourf/surface plot Other parameters: check function :meth:`~boutpy.boutdata.Field.plot` and :meth:`boutpy.boutdata.Field.contourf` for details See Also -------- :meth:`boutpy.boutdata.Field.plot`: plot 1D data and 2D data(multilines or animation). :meth:`boutpy.boutdata.Field.contourf`: plot 2D data and 3D data(contourf/surface or animation) """ # auto squeeze array ndim = self.squeeze().ndim if ndim not in [1, 2, 3]: raise ValueError("ERROR: only 1,2,3-D data supported.") return None if ndim == 1: return self.plot(*args, **kwargs) elif ndim == 2: if 'kind' not in kwargs: kwargs['kind'] = 'surface' if kwargs['kind'] in ['m', 'multi', 'multilines', 'animation']: return self.plot(*args, **kwargs) else: return self.contourf(*args, **kwargs) else: return self.contourf(*args, **kwargs)