#!/usr/bin/env python

"""
LPU output visualization.
"""

import collections
from collections import OrderedDict
import itertools
import os
import h5py

from future.utils import itervalues, iteritems
import matplotlib
from matplotlib import cm
from matplotlib.colors import Normalize
import matplotlib.pyplot as plt
plt.ioff() # interactive mode can interfere with frame updates
from matplotlib.animation import FFMpegFileWriter, AVConvFileWriter
from matplotlib.colors import hsv_to_rgb
import networkx as nx
import numpy as np
from scipy.interpolate import griddata
from shutilwhich import which

import neurokernel.LPU.utils.simpleio as sio

class visualizer(object):
    """
    Visualize the output produced by LPU models.

    Examples
    --------
    >>> import neurokernel.LPU.utils.visualizer as vis
    >>> V = vis.visualizer()
    >>> config1 = {}
    >>> config1['type'] = 'image'
    >>> config1['shape'] = [32,24]
    >>> config1['clim'] = [-0.6,0.5]
    >>> config2 = config1.copy()
    >>> config2['clim'] = [-0.55,-0.45]
    >>> V.add_LPU('lamina_output.h5', 'lamina.gexf.gz','lamina')
    >>> V.add_plot(config1, 'lamina', 'R1')
    >>> V.add_plot(config2, 'lamina', 'L1')
    >>> V.update_interval = 50
    >>> V.out_filename = 'test.avi'
    >>> V.run()
    """

    def __init__(self):
        self._xlim = [0,1]
        self._ylim = [-1,1]
        self._imlim = [-1, 1]
        self._update_interval = -1
        self._out_file = None
        self._fps = 5
        self._codec = 'libtheora'
        self._config = OrderedDict()
        self._rows = 0
        self._cols = 0
        self._figsize = (16,9)
        self._fontsize = 18
        self._t = 0
        self._dts = {}
        self._sample_intervals = {}
        self._start_times = {}
        self._data = {}
        self._uids = {}
        self._maxt = None
        self._title = None
        self._FFMpeg = None

    def add_LPU(self, data_file, LPU='', win=None, is_input=False,gexf_file=None,
                sample_interval=1, start_time=0, dt=1e-4, transpose_axes = [1,0]):
        """
        Add data associated with a specific LPU to a visualization.

        To add a plot containing neurons from a particular LPU,
        the LPU needs to be added to the visualization using this
        function. Note that outputs from multiple neurons can
        be visualized using the same visualizer object. The IDs
        specified in the arguments passed to `add_plot()` are assumed to be
        indices into array stored in the HDF5 file.

        Parameters
        ----------
        data_file : str
             Location of the HDF5 file generated by neurokernel
             containing the output of the LPU
        LPU : str
            Name of the LPU. Will be used as identifier to add plots.
            For input signals, the name of the LPU will be prepended
            with 'input_'. For example::

                V.add_LPU('vision_in.h5', LPU='vision')

            will create the LPU identifier 'input_vision'.
            Therefore, adding a plot depicting this input can be done by::

                V.add_plot({''type':'image',imlim':[-0.5,0.5]},LPU='input_vision)
        win : slice/list (Optional)
            Can be used to limit the visualization to a specific time window.
        gexf_file : string (Optional)
            Location of gexf file containg the graph of the LPU configuration
        graph : networkx graph object(Otional)
            NetworkX graph object representing the LPU configuration
        is_input : Boolean (optional)
            Set to true if the data_file represents input
        dt, start_time, sample_interval: double, double, int (All Optional)
            These arguments will only be used to set these attributes for
            input h5 files. For all other cases, these will be read from
            the h5 file

        All arguments beyond LPU should be considered strictly keyword only
        """

        if is_input:
            LPU = 'input_' + str(LPU)
            self._sample_intervals[LPU] = sample_interval
            self._dts[LPU] = dt * sample_interval
            self._start_times[LPU] = start_time
            f = h5py.File(data_file)
            self._uids[LPU] = {}
            self._data[LPU] = {}
            for k, d in iteritems(f):
                self._uids[LPU][k] = [a.decode('utf8') for a in f[k]['uids'].value]
                self._data[LPU][k] = np.transpose(f[k]['data'].value,
                                                  axes = transpose_axes)

            self._config[LPU] = []
            if self._maxt:
                self._maxt = min(self._maxt,
                                 (self._data[LPU][k].shape[1]-1)*self._dts[LPU])
            else:
                self._maxt = (self._data[LPU][k].shape[1]-1)*self._dts[LPU]
            f.close()
            return

        self._config[LPU] = []
        f = h5py.File(data_file)

        self._sample_intervals[LPU] = f['metadata'].attrs['sample_interval']
        self._dts[LPU] = f['metadata'].attrs['dt'] * self._sample_intervals[LPU]
        self._start_times[LPU] = f['metadata'].attrs['start_time']
        self._uids[LPU] = {}
        self._data[LPU] = {}
        for k, d in iteritems(f):
            if k=='metadata': continue
            self._uids[LPU][k] = [a.decode('utf8') for a in f[k]['uids'].value]
            self._data[LPU][k] = np.transpose(f[k]['data'].value,
                                              axes = transpose_axes)

        if win is not None:
            for k in self._data[LPU]:
                self._data[LPU][k] = self._data[LPU][k][:,win]
        k = list(self._data[LPU])[0]
        if self._maxt:
            self._maxt = min(self._maxt,
                             (self._data[LPU][k].shape[1]-1)*self._dts[LPU])
        else:
            self._maxt = (self._data[LPU][k].shape[1]-1)*self._dts[LPU]
        f.close()

    def run(self, final_frame_name=None, dpi=300):
        """
        Starts the visualization process.

        If the property `out_filename` is set, the visualization is saved as a
        video to the disk; if not, the animation is displayed on screen.
        Please refer to documentation of `add_LPU`, `add_plot`
        and the properties of this class on how to configure the visualizer
        before calling this method. An example can be found in the class doc
        string.

        Parameters
        ----------
        final_frame_name : str, optional
            If specified, the final frame of the animation is saved
            to disk.
        dpi : int, default=300
            Resolution at which final frame is saved to disk if
            `final_frame_name` is specified.

        Notes
        -----
        If `update_interval` is set to 0 or None, it will be replaced by the
        index of the final time step. As a result, the visualizer will only
        generate and save the final frame if `final_frame_name` is set.
        """

        self.final_frame_name = final_frame_name
        self._initialize()
        if not self._update_interval:
            self._update_interval = self._maxt
            self._t = self._maxt
        if self._update_interval == -1:
            self._update_interval = max(np.asarray(list(self._dts.values())))*50
        for _ in np.arange(self._t,self._maxt*(1+np.finfo(float).eps),
                           self._update_interval):
            self._update()
        if final_frame_name is not None:
            self.f.savefig(final_frame_name, dpi=dpi)
        if self.out_filename:
            self._close()

    def _set_wrapper(self, obj, name, value):
        name = name.lower()
        func = getattr(obj, 'set_'+name, None)
        if func:
            try:
                func(value, fontsize=self._fontsize, weight='bold')
            except:
                try:
                    func(value)
                except:
                    pass

    def _initialize(self):

        # Count number of plots to create:
        num_plots = 0
        for config in itervalues(self._config):
            num_plots += len(config)

        # Set default grid of plot positions:
        if not self._rows*self._cols == num_plots:
            self._cols = int(np.ceil(np.sqrt(num_plots)))
            self._rows = int(np.ceil(num_plots/float(self._cols)))
        self.f, self.axarr = plt.subplots(self._rows, self._cols,
                                          figsize=self._figsize)

        # Remove unused subplots:
        for i in range(num_plots, self._rows*self._cols):
            plt.delaxes(self.axarr[np.unravel_index(i, (self._rows, self._cols))])
        cnt = 0
        self.handles = []
        self.types = []
        keywds = ['handle', 'ydata', 'fmt', 'type', 'ids', 'shape', 'norm']
        # TODO: Irregular grid in U will make the plot better
        U, V = np.mgrid[0:np.pi/2:complex(0, 60),
                        0:2*np.pi:complex(0, 60)]
        X = np.cos(V)*np.sin(U)
        Y = np.sin(V)*np.sin(U)
        Z = np.cos(U)
        self._dome_pos_flat = (X.flatten(), Y.flatten(), Z.flatten())
        self._dome_pos = (X, Y, Z)
        self._dome_arr_shape = X.shape
        if not isinstance(self.axarr, np.ndarray):
            self.axarr = np.asarray([self.axarr])
        for LPU, configs in iteritems(self._config):
            dt = self._dts[LPU]
            for plt_id, config in enumerate(configs):
                var = config['variable']
                ind = np.unravel_index(cnt, self.axarr.shape)
                cnt+=1
                # Some plot types require specific numbers of
                # neuron ID arrays:
                if 'type' in config:
                    if config['type'] == 'quiver':
                        assert len(config['ids'])==2
                        config['type'] = 0
                    elif config['type'] == 'hsv':
                        assert len(config['ids'])==2
                        config['type'] = 1
                    elif config['type'] == 'image':
                        assert len(config['ids'])==1
                        config['type'] = 2
                    elif config['type'] == 'waveform':
                        config['type'] = 3
                    elif config['type'] == 'raster':
                        config['type'] = 4
                    elif config['type'] == 'rate':
                        config['type'] = 5
                    elif config['type'] == 'dome':
                        config['type'] = 6
                        assert('lat' in config and 'long' in config)
                    else:
                        raise ValueError('Plot type not supported')
                else:
                    if (str(LPU).startswith('input') and not
                        self._graph[LPU].node[str(config['ids'][0][0])]['spiking']):
                        config['type'] = 2
                    else:
                        config['type'] = 3

                if config['type'] < 3:
                    if not 'shape' in config:
                        num_neurons = len(config['ids'][0])
                        config['shape'] = [int(np.ceil(np.sqrt(num_neurons)))]
                        config['shape'].append(int(np.ceil(num_neurons/float(config['shape'][0]))))

                if config['type'] == 0:
                    config['handle'] = self.axarr[ind].quiver(\
                               np.reshape(self._data[LPU][var][config['ids'][0],0],config['shape']),\
                               np.reshape(self._data[LPU][var][config['ids'][1],0],config['shape']))
                elif config['type'] == 1:
                    X = np.reshape(self._data[LPU][var][config['ids'][0],0],config['shape'])
                    Y = np.reshape(self._data[LPU][var][config['ids'][1],0],config['shape'])
                    V = (X**2 + Y**2)**0.5
                    H = (np.arctan2(X,Y)+np.pi)/(2*np.pi)
                    S = np.ones_like(V)
                    HSV = np.dstack((H,S,V))
                    RGB = hsv_to_rgb(HSV)
                    config['handle'] = self.axarr[ind].imshow(RGB)
                elif config['type'] == 2:
                    if 'trans' in config:
                        if config['trans'] is True:
                            to_transpose = True
                        else:
                            to_transpose = False
                    else:
                        to_transpose = False
                        config['trans'] = False

                    if to_transpose:
                        temp = self.axarr[ind].imshow(np.transpose(np.reshape(\
                                self._data[LPU][var][config['ids'][0],0], config['shape'])))
                    else:
                        temp = self.axarr[ind].imshow(np.reshape(\
                                self._data[LPU][var][config['ids'][0],0], config['shape']))

                    temp.set_clim(self._imlim)
                    temp.set_cmap(plt.cm.gist_gray)
                    config['handle'] = temp
                elif config['type'] == 3:
                    fmt = config['fmt'] if 'fmt' in config else ''
                    self.axarr[ind].set_xlim(self._xlim)
                    self.axarr[ind].set_ylim(self._ylim)
                    if len(config['ids'][0])==1:
                        config['handle'] = self.axarr[ind].plot([0], \
                                            [self._data[LPU][var][config['ids'][0][0],0]], fmt)[0]
                        config['ydata'] = [self._data[LPU][var][config['ids'][0][0],0]]
                    else:
                        config['handle'] = self.axarr[ind].plot(self._data[LPU][var][config['ids'][0],0])[0]

                elif config['type'] == 4:
                    config['handle'] = self.axarr[ind]
                    config['handle'].vlines(0, 0, 0.01)
                    config['handle'].set_ylim([.5, len(config['ids'][0]) + .5])
                    config['handle'].set_ylabel('Neurons',
                                                fontsize=self._fontsize-1, weight='bold')
                    config['handle'].set_xlabel('Time (s)',fontsize=self._fontsize-1, weight='bold')

                    config['handle'].set_xlim([0,self._data[LPU][var].shape[1]*dt])
                    config['handle'].axes.set_yticks([])
                    eps = np.finfo(float).eps
                    config['handle'].axes.set_xticks(\
                            np.linspace(0+self._start_times[LPU],
                                        self._start_times[LPU]+self._maxt,11))
                elif config['type'] == 6:
                    self.axarr[ind].axes.set_yticks([])
                    self.axarr[ind].axes.set_xticks([])
                    self.axarr[ind] = self.f.add_subplot(self._rows,
                                                         self._cols,
                                                         cnt,
                                                         projection='3d')
                    config['handle' ] = self.axarr[ind]
                    config['handle'].axes.set_yticks([])
                    config['handle'].axes.set_xticks([])
                    config['handle'].xaxis.set_ticks([])
                    config['handle'].yaxis.set_ticks([])
                    config['handle'].zaxis.set_ticks([])
                    if 'norm' not in config:
                        config['norm'] = Normalize(vmin=-70, vmax=0, clip=True)
                    elif config['norm'] == 'auto':
                        if self._data[LPU][var].shape[1] > 100:
                            config['norm'] = Normalize(vmin = np.min(self._data[LPU][var][config['ids'][0],100:]),
                                                       vmax = np.max(self._data[LPU][var][config['ids'][0],100:]),
                                                       clip = True)
                        else:
                            config['norm'] = Normalize(vmin = np.min(self._data[LPU][var][config['ids'][0],:]),
                                                       vmax = np.max(self._data[LPU][var][config['ids'][0],:]),
                                                       clip = True)

                    node_dict = self._graph[LPU].node
                    latpositions = config['lat']
                    longpositions = config['long']
                    xx = np.cos(longpositions) * np.sin(latpositions)
                    yy = np.sin(longpositions) * np.sin(latpositions)
                    zz = np.cos(latpositions)
                    config['positions'] = (xx, yy, zz)
                    colors = griddata(config['positions'], self._data[LPU][var][config['ids'][0],0],
                                      self._dome_pos_flat, 'nearest').reshape(self._dome_arr_shape)
                    colors = config['norm'](colors).data
                    colors = np.tile(np.reshape(colors,
                                                [self._dome_arr_shape[0],self._dome_arr_shape[1],1])
                                     ,[1,1,4])
                    colors[:,:,3] = 1.0
                    config['handle'].plot_surface(self._dome_pos[0], self._dome_pos[1],
                                                  self._dome_pos[2], rstride=1, cstride=1,
                                                  facecolors=colors, antialiased=False,
                                                  shade=False)

                for key in config:
                    if key not in keywds:
                        try:
                            self._set_wrapper(self.axarr[ind],key, config[key])
                        except:
                            pass
                        try:
                            self._set_wrapper(config['handle'],key, config[key])
                        except:
                            pass

                if config['type']<3:
                    config['handle'].axes.set_xticks([])
                    config['handle'].axes.set_yticks([])

            if self.suptitle is not None:
                self.f.suptitle(self._title, fontsize=self._fontsize+1, x=0.5,y=0.03, weight='bold')

        plt.tight_layout()

        if self.out_filename and self.update_interval:
            if self.FFMpeg is None:
                if which(matplotlib.rcParams['animation.ffmpeg_path']):
                    self.writer = FFMpegFileWriter(fps=self.fps, codec=self.codec)
                elif which(matplotlib.rcParams['animation.avconv_path']):
                    self.writer = AVConvFileWriter(fps=self.fps, codec=self.codec)
                else:
                    raise RuntimeError('cannot find ffmpeg or avconv')
            elif self.FFMpeg:
                if which(matplotlib.rcParams['animation.ffmpeg_path']):
                    self.writer = FFMpegFileWriter(fps=self.fps, codec=self.codec)
                else:
                    raise RuntimeError('cannot find ffmpeg')
            else:
                if which(matplotlib.rcParams['animation.avconv_path']):
                    self.writer = AVConvFileWriter(fps=self.fps, codec=self.codec)
                else:
                    raise RuntimeError('cannot find avconv')

            # Use the output file to determine the name of the temporary frame
            # files so that two concurrently run visualizations don't clobber
            # each other's frames:
            self.writer.setup(self.f, self.out_filename, dpi=80,
                              frame_prefix=os.path.splitext(self.out_filename)[0]+'_')
            self.writer.frame_format = 'png'
            self.writer.grab_frame()
        elif not self.final_frame_name:
            self.f.show()

    def _update(self):
        t = self._t
        for LPU, configs in iteritems(self._config):
            dt = self._dts[LPU]
            for config in configs:
                var = config['variable']
                data = self._data[LPU][var]
                if config['type'] == 3:
                    if round(float(t)/dt) >= data.shape[1] : continue
                    if len(config['ids'][0])==1:
                        s = int(round(float(t-self._update_interval)/dt))
                        e = int(round(float(t)/dt))
                        config['ydata'].extend(
                            np.reshape(np.double(data[config['ids'][0], \
                                        max(0,s):min(e,data.shape[1])]),(-1,)))
                        config['handle'].set_xdata(dt* np.arange(0, \
                                len(config['ydata']))+self._start_times[LPU])
                        config['handle'].set_ydata(np.asarray(config['ydata']))
                    else:
                        config['handle'].set_ydata(data[config['ids'][0],
                                                        int(round(float(t)/dt))])
                elif config['type']==4:
                    if int(round(float(t)/dt)) >= data.shape[1] : continue
                    for j, id in enumerate((config['ids'][0])):
                        s = int(round(float(t-self._update_interval)/dt))
                        e = int(round(float(t)/dt))
                        tmp = np.where(data[id, \
                                      max(0,s):min(e,data.shape[1])])[0]
                        for tind in tmp:
                            shift = self._start_times[LPU]
                            config['handle'].vlines(float(shift+(max(0,s)+tind)*dt),
                                                    j+0.75, j+1.25)
                elif config['type'] == 0:
                    if int(round(float(t)/dt)) >= data.shape[1] : continue
                    ind = int(round(float(t)/dt))
                    shape = config['shape']
                    ids = config['ids']
                    config['handle'].U = np.reshape(data[ids[0], ind],shape)
                    config['handle'].V = np.reshape(data[ids[1], ind],shape)
                elif config['type']==1:
                    if int(round(float(t)/dt)) >= data.shape[1] : continue
                    ind = int(round(float(t)/dt))
                    shape = config['shape']
                    ids = config['ids']
                    X = np.reshape(data[ids[0], ind],shape)
                    Y = np.reshape(data[ids[1], ind],shape)
                    V = (X**2 + Y**2)**0.5
                    H = (np.arctan2(X,Y)+np.pi)/(2*np.pi)
                    S = np.ones_like(V)
                    HSV = np.dstack((H,S,V))
                    RGB = hsv_to_rgb(HSV)
                    config['handle'].set_data(RGB)
                elif config['type'] == 2:
                    if int(round(float(t)/dt)) >= data.shape[1] : continue
                    ind = int(round(float(t)/dt))
                    ids = config['ids']
                    if config['trans']:
                        config['handle'].set_data(
                            np.transpose(np.reshape(data[ids[0], ind],
                                                    config['shape'])))
                    else:
                        config['handle'].set_data(
                            np.reshape(data[ids[0], ind], config['shape']))
                elif config['type'] == 6:
                    if int(round(float(t)/dt)) >= data.shape[1] : continue
                    ind = int(round(float(t)/dt))
                    ids = config['ids']
                    d = data[ids[0], ind]
                    colors = griddata(config['positions'], d,
                                      self._dome_pos_flat, 'nearest').reshape(self._dome_arr_shape)
                    colors = config['norm'](colors).data
                    colors = np.tile(np.reshape(colors,
                                                [self._dome_arr_shape[0],self._dome_arr_shape[1],1])
                                     ,[1,1,4])
                    colors[:,:,3] = 1.0

                    config['handle'].clear()
                    config['handle'].xaxis.set_ticks([])
                    config['handle'].yaxis.set_ticks([])
                    config['handle'].zaxis.set_ticks([])

                    config['handle'].plot_surface(self._dome_pos[0], self._dome_pos[1],
                                                  self._dome_pos[2], rstride=1, cstride=1,
                                                  facecolors=colors, antialiased=False,
                                                  shade=False)
                keywds = ['handle', 'ydata', 'fmt', 'type', 'ids', 'shape', 'norm']
                for key in config:
                    if key not in keywds:
                        try:
                            self._set_wrapper(self.axarr[ind],key, config[key])
                        except:
                            pass

                        try:
                            self._set_wrapper(config['handle'],key, config[key])
                        except:
                            pass
        self.f.canvas.draw()
        if self.out_filename:
            self.writer.grab_frame()

        self._t+=self._update_interval

    def add_plot(self, config_dict, LPU):
        """
        Add a plot to the visualizer

        Parameters
        ----------
        config_dict: dict
            A dictionary specifying the plot attributes. The attribute
            names should be the keys.

            The following are the plot attributes that can be specfied using
            this dict.

            type - str  (Required)
                This specifies the type of the plot. Has to be one of
                ['waveform', 'raster', 'image','hsv','quiver', 'dome']
                'dome' requires lat and long to specified. The size of lat and long
                should be the same as uids
            variable - str (Required)
                The variable to plot
            uids - list/tuple of either 1 or 2 list/tuple of uids
                Specifies the neuron uids from the associated LPU.
                The list of uids consistent with uids in the h5file.
                For example::

                    {'variable':'V','uids':[['1','2']]}

                will plot neurons with uids 1 and 2.
                Two lists of uids are needed if the plot is
                of type 'hsv' or 'quiver'
                For example::

                     {'variable':'V:,
                     'uids':[map(str,range(768)),map(str,range(768,1536)],'type':'HSV'}

                can be used to generate a HSV plot where the hue channel is
                controlled by the angle of the vector with the membrane
                potentials of the component with uids '0' through '767' as the x component
                and potentials of neurons with 'uids' '768' through '1535' as the y component.
                The value will be the magnitude of the same vector.

                This parameter must be specified for plots of type 'HSV' or 'quiver'

                If not specified, all components in the h5 file for which that variable was recorded
                will be plotted

            shape - list or tuple with two entries
                This attribute specifies the dimensions for plots of type image,
                hsv or quiver.

            title - str
                Optional. Can be used to control the title of the plot.


            In addition to the above, any parameter supported by matlpotlib
            for the particular type of plot can be specified.
            For example - 'imlim','clim','xlim','ylim' etc.
        LPU: str
            The name of the LPU associated to this plot.
        """

        config = config_dict.copy()
        assert('variable' in config)
        var = config['variable']
        assert(LPU in self._uids and LPU in self._data)
        if 'uids' in config:
            if not (isinstance(config['uids'], list) or
                    isinstance(config['uids'], np.ndarray)):
                config['uids'] = [config['uids']]
            config['ids'] = []
            for uids in config['uids']:
                tmp_list = []
                for uid in uids:
                    tmp = np.where(np.array(self._uids[LPU][var])==uid)[0]
                    if len(tmp):
                        tmp_list.append(tmp[0])
                config['ids'].append(tmp_list)
            self._config[LPU].append(config)
        elif str(LPU).startswith('input'):
            config['ids'] = [range(0, self._data[LPU][var].shape[0])]
            self._config[LPU].append(config)
        else:
            config['uids'] = self._uids[LPU][var]
            config['ids'] = [[range(0,len(config['uids']))]]
            self._config[LPU].append(config)
            #raise ValueError('uids must be provided')
            '''
            config['ids'] = {}
            for i,name in enumerate(names):
                config['ids'][i]=[]
                for id in range(len(self._graph[LPU].node)):
                    if self._graph[LPU].node[str(id)]['name'] == name:
                        config['ids'][i].append(id-shift)

            self._config[LPU].append(config)
            '''
        if not 'title' in config:
            if str(LPU).startswith('input_'):
                config['title'] = LPU.split('_',1)[1] + ' - ' + 'Input'
            else:
                config['title'] = str(LPU)

    def _close(self):
        self.writer.finish()
        plt.close(self.f)

    @property
    def xlim(self):
        """
        X-axis limits for all the raster and waveform plots. Can be superseded
        for individual plots by specifying xlim in the config_dict for that
        plot.

        See Also
        --------
        add_plot

        """
        return self._xlim

    @xlim.setter
    def xlim(self, value):
        self._xlim = value

    @property
    def ylim(self):
        """
        Get or set the limits of the y-axis for all the raster and waveform plots.
        Can be superseded for individual plots by specifying xlim in the config_dict
        for that plot.

        See Also
        --------
        add_plot
        """
        return self._ylim

    @ylim.setter
    def ylim(self, value):
        self._ylim = value

    @property
    def FFMpeg(self): return self._FFMpeg

    @FFMpeg.setter
    def FFMpeg(self, value):
        self._FFMpeg = value

    @property
    def imlim(self): return self._imlim

    @imlim.setter
    def imlim(self, value):
        self._imlim = value

    @property
    def out_filename(self): return self._out_file

    @out_filename.setter
    def out_filename(self, value):
        assert(isinstance(value, str))
        self._out_file = value

    @property
    def fps(self): return self._fps

    @fps.setter
    def fps(self, value):
        assert(isinstance(value, int))
        self._fps = value

    @property
    def codec(self): return self._codec

    @codec.setter
    def codec(self, value):
        assert(isinstance(value, str))
        self._codec = value

    @property
    def rows(self): return self._rows

    @rows.setter
    def rows(self, value):
        self._rows = value

    @property
    def cols(self): return self._cols

    @cols.setter
    def cols(self, value):
        self._cols = value

    @property
    def figsize(self): return self._figsize

    @figsize.setter
    def figsize(self, value):
        assert(isinstance(value, tuple) and len(value)==2)
        self._figsize = value

    @property
    def fontsize(self): return self._fontsize

    @fontsize.setter
    def fontsize(self, value):
        self._fontsize = value

    @property
    def suptitle(self): return self._title

    @suptitle.setter
    def suptitle(self, value):
        self._title = value

    @property
    def update_interval(self):
        """
        Update interval (in unit of time same as dt in the h5 files) for the animation.
        """
        return self._update_interval

    @update_interval.setter
    def update_interval(self, value):
        self._update_interval = value
