Animations and Interactivity

Module 1: Low-Dimensional Dynamics

This page summarizes the core Matplotlib pattern we use to animate trajectories in the phase plane, and how to restart an animation when the user changes parameters or initial conditions (Hunter 2007; Virtanen et al. 2020).

If you get stuck, compare with the complete CDIMA animation in the repository.

What you need

  • scipy.integrate.solve_ivp computes the trajectory; the state now has two components.
  • matplotlib.pyplot plots trajectories, nullclines, fixed points and time traces.
  • matplotlib.animation updates the plot frame by frame, in the phase plane and in time.
  • matplotlib.backend_bases gives you the event system, so a click in the phase plane can set a new initial condition and restart the simulation.

Why scripts instead of Streamlit here? The most direct interactive behaviour comes from Matplotlib animations and click events, and those are simpler to prototype in standalone scripts than in a Streamlit app.

Minimal Animation Template

import matplotlib.animation as animation
import matplotlib.pyplot as plt

# ...
# Your code to compute trajectories, nullclines, fixed point...
# Example: sol = solve_ivp(...)
# ...

# Open a new figure (white canvas)
fig = plt.figure()
ax = plt.gca()  # get current axis

# Initialize the line object for animation on the phase plane
(plot_trajectory,) = ax.plot([], [], lw=2)
# This trajectory would be x-y for the
# phase plane of the CDIMA and Van der Pol models, and v-w for the FitzHugh-Nagumo model


def animate(frame: int, xy: tuple[np.ndarray, np.ndarray]):
    """Update function called once per frame."""
    # unpack the solution (x(t), y(t)) or (v(t), w(t))
    x, y = xy
    # update the line data to show the trajectory up to the current frame
    plot_trajectory.set_data(x[:frame], y[:frame])
    # return the updated artist (line) for blitting
    return (plot_trajectory,)


# The animation object that will run the animation
ani = animation.FuncAnimation(
    fig,
    animate,
    fargs=(sol.y,),
    frames=len(sol.t),
    interval=1,  # delay between frames in milliseconds
    blit=True,  # redraw only the updated artists for efficiency
)

What Happens in an Animation?

The animation function is called once per frame:

  • frame (or i) is the frame index (0, 1, 2, …).
  • xy is the data to plot (here, the solution sol.y returned by solve_ivp).
  • the function must update the artists (lines, points, text, etc.).
  • it must return an iterable with the updated artists when blit=True. This tells Matplotlib which parts of the plot to redraw for the next frame, improving performance.

The Animation Function in Detail

Template:

def animate_function(i: int, *args):
    # update plot elements
    return (artist1, artist2, ...)

In our case, the second input argument is the solution from solve_ivp. Updating a line is typically done with:

line.set_data(x, y)

This replaces the line coordinates by the new points \((x,y)\).

Creating the Animation (FuncAnimation)

import matplotlib.animation as animation

ani = animation.FuncAnimation(
    fig,
    function,
    fargs=(sol.y,),
    interval=1,
    blit=True,
)

Arguments:

  • fig: the Matplotlib figure to animate
  • function: the callback that updates plot elements on each frame
  • fargs: extra arguments passed to function (everything except the frame index)
  • interval: delay between frames (milliseconds)
  • blit: if True, redraws only the updated artists (faster). When using blit=True, your animation function must return the artists that were updated.

Interactivity: Restarting the Animation

When the user clicks (new initial condition) or changes a parameter, a common pattern is:

ani.event_source.stop()          # stop the current animation
ani.frame_seq = ani.new_frame_seq()  # reset frame generator
ani._args = (xy, ...)            # replace animation inputs (like new fargs)
ani.event_source.start()         # start again

This is the mechanism used in the interactive scripts: recompute solve_ivp(...), update nullclines/fixed point, then restart the animation with the new data.

Mouse Clicks

Matplotlib figures can react to user input via the event system. A common pattern is to capture clicks in a specific axis and use the click position as a new initial condition.

Exercise

Complete mouse_click so that a click in the phase plane ax1 restarts the animation from the clicked point. You will need to stop the current animation, clear it, update the initial condition, and start over (see the previous section).

from matplotlib.backend_bases import MouseEvent


def mouse_click(event: MouseEvent):
    # Check if the mouse click happens in the axis "ax1"
    if event.inaxes == ax1:
        # Get the position of the mouse click on the axis
        x0 = event.xdata
        y0 = event.ydata
    else:
        return

    # This part of the code should update the plot.
    # Use the animation functions we have learnt:
    # 1) stop the current animation
    # 2) clear/reset it
    # 3) recompute the solution (and nullclines/fixed point if needed)
    # 4) set new animation arguments
    # 5) start again


# Make the figure aware of user clicks.
# Whenever the user clicks, we run the function "mouse_click".
fig.canvas.mpl_connect("button_press_event", mouse_click)

This assumes fig, ax1, ani, the model cdima, the parameters a, b, t_span and t_eval are defined as in the template above. update_simulation in amlab/odes_2d/cdima.py is the full version, which also updates nullclines and the fixed point.

def mouse_click(event: MouseEvent):
    if event.inaxes != ax1:
        return
    x0, y0 = event.xdata, event.ydata

    sol = solve_ivp(cdima, t_span, [x0, y0], args=(a, b), t_eval=t_eval)

    ani.event_source.stop()
    ani.frame_seq = ani.new_frame_seq()
    ani._args = (sol.y,)
    ani.event_source.start()


fig.canvas.mpl_connect("button_press_event", mouse_click)

Validation

Run the reference script and click in the phase plane. Each click should restart the animation from the clicked point, and the trajectory should follow the direction of the vector field.

python amlab/odes_2d/cdima.py
Optional extension

Add a text artist that shows the current time \(t\), and update it in animate together with the line.

Next

Hunter, John D. 2007. “Matplotlib: A 2D Graphics Environment.” Computing in Science & Engineering 9 (3): 90–95. https://doi.org/10.1109/MCSE.2007.55.
Virtanen, Pauli, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, et al. 2020. “SciPy 1.0: Fundamental Algorithms for Scientific Computing in Python.” Nature Methods 17 (3): 261–72. https://doi.org/10.1038/s41592-019-0686-2.