Animations#
Animations can make complex dynamics much more comprehensible, and are easy to create using the plotting framework.
Summary
On this page, you will see how to
use
.plot.cato animate a cellular automatonuse the
.animation.ffmpegor.animation.framesbase functions together with.dag.graphto animate networksuse the
.animation.ffmpegor.animation.framesbase functions for general animationsadjust the resolution of an animation using the
writer_kwargswrite your own animation function using the
@is_plot_funcdecoratorwrite your own animation plot configuration
Complete example: Animated cellular automaton
animated_ca:
based_on:
- .creator.universe
- .plot.ca
select:
kind: kind
# Select the range to plot
frames_isel: !range [0, 50]
# Animation kwargs
animation:
writer_kwargs:
ffmpeg:
saving:
dpi: 192
# Do not plot a time stamp
suptitle_fstr: False
to_plot:
kind:
# Use a custom cmap; these colors must be globally defined somewhere
# Will automatically be mapped to values [0, 1, 2, 3]
cmap:
empty: *white
susceptible: *lightgreen
infected: *red
recovered: *darkgreen
# Optional: use latex and set font size
style:
text.usetex: True
mathtext.fontset: stix
font.family: serif
font.size: 10
Complete example: Animated network
animated_network:
based_on:
- .creator.universe
- .plot.graph
- .animation.ffmpeg
select:
graph_group: nw
graph_creation:
node_props: [opinion]
edge_props: [edge_weights]
at_time_idx: -1
graph_drawing:
positions:
model: spring
k: 2
nodes:
node_color:
from_property: opinion
node_size:
from_property: degree
scale_to_interval: [1, 200]
cmap:
continuous: true
from_values:
0: *red
0.5: *yellow
1: *mediumblue
vmin: 0.0
vmax: 1.0
colorbar:
label: opinion $\sigma$
edges:
arrowsize: 4
width:
from_property: edge_weights
graph_animation:
sel:
time:
from_property: opinion
animation:
writer_kwargs:
ffmpeg:
init:
fps: 3
saving:
dpi: 192
Animated Cellular Automaton#
A particularly common case is animating cellular automata, such as the SEIRD model,
for which the .plot.ca function provides a simple interface to create an animated plot.
animated_ca:
# Base your plot on .plot.ca
based_on:
- .creator.universe
- .plot.ca
# Select the different 'kinds' of agents to plot
select:
kind: kind
# .plot.ca requires a 'to_plot' key
to_plot:
kind:
# title: My title
vmin: 0
vmax: 3
cmap:
empty: white
susceptible: lightgreen
infected: crimson
recovered: teal
.plot.ca requires a to_plot key, specifying what to put into each subplot.
The keys (here: kind) need to correspond to the selected data entries.
In addition, it is used to specify the limits of the colormap (vmin and vmax) and other information like the subplot’s title.
Here, we are only plotting empty cells and cells with susceptible, infected, and recovered agents, denoted by the integer kind values of 0 to 3.
To ensure that the colormap stays in that range, we set vmin and vmax accordingly and define the colors we want for each kind; they are associated depending on the order within cmap, starting at vmin and going up to vmax in integer steps:
For animations, you will typically want a fairly high plotting resolution.
Change the resolution of the plot by adding an animation block to the plot configuration:
animated_ca:
# all the previous entries ...
animation:
writer_kwargs:
ffmpeg:
saving:
dpi: 400
A higher dpi will give you a higher resolution and prevent interpolation issues, but will also take longer to plot and require more storage.
You can restrict yourself to a smaller range of frames to plot using the frames_isel key (which selects indices).
This can be useful for long simulation runs, and when only wanting to visualise a small part of the dynamics.
Simply add
animated_ca:
# all the previous entries ...
frames_isel: !range [30, 60]
This will only plot the frames from 30 to 59.
You can also manually specify a list, i.e. frames_isel: [10, 20, 30, 40].
Hint
As an alternative for plotting heatmaps that is not specialized on CA, you can use the .plot.facet_grid.pcolormesh base configuration.
See the article on heatmaps for more details.
Animated Network Plots#
Let’s look at another example: in the previous section we saw how to plot
networks. There, we used a node property called opinion to color the network nodes.
We can now animate them, showing how this node property changes over time.
The configuration can only requires minor modification. If you already have a
static graph plot static_network, you can amend it in the following way:
static_network:
# Plot configuration for a static network plot ...
animated_network:
based_on:
- static_network
- .animation.ffmpeg # Use the ffmpeg writer
# Add this entry to make the 'opinion' change over time
graph_animation:
sel:
time:
from_property: opinion
And that’s it! Instead of ffmpeg, you can also use the frames writer by instead basing your plot on .animation.frames.
Increase the resolution of the animation by adding and updating the following entry:
animation:
writer_kwargs:
frames:
saving:
dpi: 400
ffmpeg:
init:
fps: 10
saving:
dpi: 400
You only need to add the key for the animation writer you are actually using.
Take a look at the Utopia Opinionet model for a working demo of an animated network.
Writing your own animation#
Implementing the animation function#
Writing your own animated plot is simple with the inclusion of the PlotHelper and the is_plot_func decorator.
The fundamental structure of a plot function that supports animation should follow this scaffolding:
first, use the @is_plot_func decorator to denote a function as a plot function:
from utopya import DataManager, UniverseGroup
from utopya.eval import UniversePlotCreator, is_plot_func, PlotHelper
@is_plot_func(use_dag=True, supports_animation=True)
def my_plot(
*, hlpr: PlotHelper, data: dict, dim: str, time: int = 0, **kwargs
):
# Select data
d = data[dim]
# ...
Set use_dag and supports_animation to True.
Next, write your plot function. It should plot the data at a single time, and then contain an update function that loops over the time steps, plotting a frame of the animation at each step:
from utopya import DataManager, UniverseGroup
from utopya.eval import UniversePlotCreator, is_plot_func, PlotHelper
@is_plot_func(use_dag=True, supports_animation=True)
def my_plot(
*, hlpr: PlotHelper, data: dict, dim: str, time: int = 0, **kwargs
):
d = data[dim]
hlpr.ax.plot(d[time], **kwargs)
def update():
for idx, y_data in enumerate(data):
# Clear the plot and plot anew
hlpr.ax.clear()
hlpr.ax.plot(y_data, **kwargs)
# Set the title
hlpr.invoke_helper("set_title", title=f"Time {idx}")
# Done with this frame. Yield control to the plot framework,
# which will take care of grabbing the frame.
yield
While whatever happens before the registration of the animation function is also executed, the animation update function should be built such as to also include the initial frame of the animation. This is to allow the plot function itself to be more flexible, and the animation update need not distinguish between initial frame and other frames.
Finally, register the animation with the plot helper:
from utopya import DataManager, UniverseGroup
from utopya.eval import UniversePlotCreator, is_plot_func, PlotHelper
@is_plot_func(use_dag=True, supports_animation=True)
def my_plot(
*, hlpr: PlotHelper, data: dict, dim: str, time: int = 0, **kwargs
):
# as above ...
def update():
pass
# as above ...
hlpr.register_animation_update(update)
To summarise, we
marked the plot function as
supports_animation,defined an
updatefunction, andpassed the
updatefunction to the helper viaregister_animation_update()
Hint
To learn more about plot function signatures and animation, have a look at the dantro documentation:
Animation configuration#
Now let’s look at what the plot_cfg.yml needs to contain.
There are two base plot configurations you can use: .animation.frames and .animation.ffmpeg.
They use different writers for the animation.
Basing your plot on either of them is sufficient for the animation to run:
my_plot:
based_on:
- .creator.universe
- .animation.ffmpeg # or .animation.frames
- # other base settings
You can change the resolution and frame rates of the animation by adding an animation entry to the plot configuration
my_plot:
based_on :
- .creator.universe
- .animation.ffmpeg
- # ...
module: # your module here
plot_func: # your plot func here
# Other settings, such as select, transform, and plot-specific arguments ...
# Animation configuration
animation:
writer_kwargs: # additional configuration for each writer
frames: # passed to 'frames' writer
saving: # passed to Writer.saving method
dpi: 400
ffmpeg:
init: # passed to Writer.__init__ method
fps: 15
saving:
dpi: 400
grab_frame: {} # passed to Writer.grab_frame and from there to savefig
Finally, you can also pass any additional kwargs to the update function you defined by adding
my_plot:
# same as above ...
animation:
animation_update_kwargs: {}
These end up as arguments to the update function.
Hint
You can turn the animation off like this:
animation:
enabled: false
Alternatively, include the .animation.disabled base plot entry:
based_on:
- # ...
- .animation.disabled
This can be useful to avoid plotting lengthy animations for every run.