Source code for spb.animation.panel

from spb.animation import BaseAnimation
from spb.defaults import TWO_D_B, THREE_D_B, cfg
from spb.interactive.panel import PanelCommon
from spb.interactive.bootstrap_spb import SymPyBootstrapTemplate
from spb.plotgrid import PlotGrid
from sympy.external import import_module
import warnings
from mergedeep import merge

param = import_module(
    'param',
    min_module_version='1.11.0',
    warn_not_installed=True)
pn = import_module(
    'panel',
    min_module_version='0.12.0',
    warn_not_installed=True)

pn.extension("mathjax", "plotly", sizing_mode="stretch_width")


class Animation(BaseAnimation, PanelCommon):
    def __init__(self, *series, **kwargs):
        self._layout = "bb"
        self._ncols = 1
        self._servable = kwargs.pop("servable", cfg["interactive"]["servable"])
        self._pane_kw = kwargs.pop("pane_kw", dict())
        self._template = kwargs.pop("template", None)
        self._name = kwargs.pop("name", "")
        self._original_params = kwargs.get("params", {})
        self.merge = merge

        plotgrid = kwargs.get("plotgrid", None)
        if plotgrid:
            self._backend = plotgrid
            self._post_init_plotgrid(**kwargs)
        else:
            is_3D = all([s.is_3D for s in series])
            Backend = kwargs.pop("backend", THREE_D_B if is_3D else TWO_D_B)
            kwargs["is_iplot"] = True
            kwargs["imodule"] = "panel"
            self._backend = Backend(*series, **kwargs)
            self._post_init_plot(**kwargs)

        self._play_widget = pn.widgets.Player(
            value=0,
            start=0,
            end=self.animation_data.n_frames - 1,
            step=1,
            interval=int(1000 / self.animation_data.fps),
        )
        self._binding = pn.bind(self._update, self._play_widget)

    def _update(self, frame_idx):
        self.update_animation(frame_idx)
        return self._backend.fig

    def _init_pane_for_plotgrid(self):
        # First, set the necessary data to create bindings for each subplot
        self._backend.pre_set_bindings(
            [1], # anything but None
            [self._play_widget]
        )
        self._backend.pre_set_animation(self)
        # Then, create the pn.GridSpec figure
        self.pane = self._backend.fig

    def _populate_template(self, template):
        template.main.append(pn.Column(self.pane, self._play_widget))

    @property
    def layout_controls(self):
        return self._play_widget


[docs] def animation(*series, show=True, **kwargs): """Create an animation containing the plot and a few interactive controls (play/pause/loop buttons). Parameters ========== series : BaseSeries Instances of :py:class:`spb.series.BaseSeries`, representing the symbolic expression to be plotted. animation : bool or dict * ``False`` (default value): no animation. * ``True``: the animation will use the following default values: ``fps=30, time=5``. * ``dict``: the dictionary should contain these optional keys: - ``"fps"``: frames per second of the animation. - ``"time"``: total animation time. It must be noted that these values are exact only if the animation is going to be saved on a file. For interactive applications, these values are just indicative: the animation is going to compute new data at each time step. Hence, the more data needs to be computed, the slower the update. params : dict A dictionary mapping the symbols to a parameter. The parameter can be: 1. a tuple of the form `(min, max, spacing)`, where: - min, max : float Minimum and maximum values. Must be finite numbers. - spacing : str, optional Specify the discretization spacing. Default to ``"linear"``, can be changed to ``"log"``. This can be used to simulate a slider. 2. A dictionary mapping specific animation times to parameter values. This is useful to simulate steps (or values from a dropdown widget, or a spinner widget). For example, let ``tf`` be the animation time. Then, this dictionary, ``{t1: v1, t2: v2}``, creates the following steps: - 0 for `0 <= t <= t1`. - v1 for `t1 <= t <= t2`. - v2 for `t2 <= t <= tf`. 3. A 1D numpy array, with length given by ``fps * time``, specifying the custom value at each animation frame (or, at each time step) associated to the parameter. servable : bool, optional Default to False, which will show the interactive application on the output cell of a Jupyter Notebook. If True, the application will be served on a new browser window. show : bool, optional Default to True. If True, it will return an object that will be rendered on the output cell of a Jupyter Notebook. If False, it returns an instance of ``Animation``, which can later be shown by calling the ``show()`` method, or saved to a GIF/MP4 file using the ``save()`` method. title : str or tuple The title to be shown on top of the figure. To specify a parametric title, write a tuple of the form:``(title_str, param_symbol1, ...)``, where: * ``title_str`` must be a formatted string, for example: ``"test = {:.2f}"``. * ``param_symbol1, ...`` must be a symbol or a symbolic expression whose free symbols are contained in the ``params`` dictionary. Notes ===== 1. Animations are a special kind of interactive applications. In particular, the output of this function can only be shown on a browser. 2. Animations can be exported to GIF files or MP4 videos by calling the ``save()`` method. More on this in the examples. 3. Saving K3D-Jupyter animations is particularly slow. Examples ======== NOTE: the following examples use the ordinary plotting functions because ``animation`` is already integrated with them. Simple animation with two parameters. Note that: * the first parameter goes from a maximum value to a minimum value. * the second parameter goes from a minimum value to a maximum value. * a parametric title has been set. * a set of buttons allow to control the playback of the animation. .. panel-screenshot:: :small-size: 800, 700 from sympy import * from spb import * a, b, x = symbols("a b x") max_r = 30 plot_parametric( x * cos(x), x * sin(x), prange(x, a, b), aspect="equal", use_cm=False, animation=True, params={a: (10, 0), b: (20, max_r)}, title=("a={:.2f}; b={:.2f}", a, b), xlim=(-max_r, max_r), ylim=(-max_r, max_r) ) Animation showing how to: * set the frames-per-second and total animation time. * set custom values to a parameter. Here, ``b`` will vary from 0 to 1 and back to 0 following a sinusoidal law. * set a parametric title. .. code-block:: from sympy import * from spb import * import numpy as np a, b, x = symbols("a b x") fps = 20 time = 6 frames = np.linspace(0, 2*np.pi, fps * time) b_values = (np.sin(frames - np.pi/2) + 1) / 2 plot( cos(a * x) * exp(-abs(x) * b), (x, -pi, pi), params={ a: (0.5, 3), b: b_values }, animation={"fps": fps, "time": time}, title=("a={:.2f}, b={:.2f}", a, b), ylim=(-1.25, 1.25) ) .. video:: ../_static/animations/matplotlib-animation.mp4 :width: 600 Animation of a 3D surface using K3D-Jupyter. Here we create an ``Animation`` object, which can later be used to save the animation to a file. .. code-block:: from sympy import * from spb import * import numpy as np r, theta, t, a = symbols("r, theta, t, a") expr = cos(r**2 - a) * exp(-r / 3) p = plot3d_revolution( expr, (r, 0, 5), (theta, 0, t), params={t: (1e-03, 2*pi), a: (0, 2*pi)}, use_cm=True, color_func=lambda x, y, z: np.sqrt(x**2 + y**2), is_polar=True, wireframe=True, wf_n1=30, wf_n2=30, wf_rendering_kw={"width": 0.005}, animation=True, title=(r"theta={:.4f}; \, a={:.4f}", t, a), backend=KB, grid=False, show=False ) p.show() # Use the mouse to properly orient the view and then save the animation p.save("3d-animation.mp4") .. video:: ../_static/animations/3d_animation.mp4 :width: 600 Evolution of a complex function using the graphics module and Plotly. Note that ``animation=True`` has been set in the ``graphics()`` function call. .. code-block:: from sympy import * from spb import * t, tf = symbols("t t_f", real=True) gamma, omega = Rational(1, 2), 7 f = exp(-gamma * t**2) * exp(I * omega * t) params = {tf: (-5, 5)} graphics( line_parametric_3d( t, re(f), im(f), range=(t, -5, tf), label="f(t)", params=params, use_cm=False ), line_parametric_3d( t, re(f), -2, range=(t, -5, tf), label="re(f(t))", params=params, use_cm=False ), line_parametric_3d( t, 2, im(f), range=(t, -5, tf), label="im(f(t))", params=params, use_cm=False ), line_parametric_3d( 5, re(f), im(f), range=(t, -5, tf), label="abs(f(t))", params=params, use_cm=False ), backend=PB, aspect=dict(x=3, y=1, z=1), ylim=(-2, 2), zlim=(-2, 2), xlabel="t", ylabel="re(f)", zlabel="im(f)", title="$f(t)=%s$" % latex(f), animation=True, size=(704, 512) ) .. video:: ../_static/animations/graphics-animation.mp4 :width: 600 Plotgrid animation. Note that: * Each plot is an interactive animation (they can also be ordinary plots). * ``p2, p3`` use defaults fps/time, whereas ``p1`` uses custom values. * the overall plotgrid parses the different animations, collecting times and fps. It then choses the highest numbers, and recreates all the parameters. Hence, in the following animation ``p1`` runs for the entire animation. .. code-block:: from sympy import * from spb import * from matplotlib.gridspec import GridSpec a, b, c, d, x, y, z = symbols("a:d x:z") p1 = plot( sin(a*x), cos(a*x), animation={"fps": 10, "time": 2}, params={a: (1, 5)}, show=False) max_r = 30 p2 = plot_parametric( x * cos(x), x * sin(x), prange(x, b, c), aspect="equal", use_cm=False, animation=True, params={b: (10, 0), c: (20, max_r)}, title=("b={:.2f}; c={:.2f}", b, c), show=False, xlim=(-max_r, max_r), ylim=(-max_r, max_r)) p3 = plot_complex(gamma(d*z), (z, -3-3*I, 3+3*I), title=r"$\gamma(d \, z)$", animation=True, params={d: (-1, 1)}, coloring="b", grid=False, show=False) gs = GridSpec(3, 4) mapping = { gs[2, :]: p1, gs[0:2, 0:2]: p2, gs[0:2, 2:]: p3, } plotgrid(gs=mapping) .. video:: ../_static/animations/plotgrid-animation.mp4 :width: 600 """ ani = Animation(*series, **kwargs) if show: return ani.show() return ani