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_ivpcomputes the trajectory; the state now has two components.matplotlib.pyplotplots trajectories, nullclines, fixed points and time traces.matplotlib.animationupdates the plot frame by frame, in the phase plane and in time.matplotlib.backend_basesgives 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(ori) is the frame index (0, 1, 2, …).xyis the data to plot (here, the solutionsol.yreturned bysolve_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 animatefunction: the callback that updates plot elements on each framefargs: extra arguments passed tofunction(everything except the frame index)interval: delay between frames (milliseconds)blit: ifTrue, redraws only the updated artists (faster). When usingblit=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 againThis 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.
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.pyAdd a text artist that shows the current time \(t\), and update it in animate together with the line.