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.ca
to animate a cellular automatonuse the
.animation.ffmpeg
or.animation.frames
base functions together with.dag.graph
to animate networksuse the
.animation.ffmpeg
or.animation.frames
base functions for general animationsadjust the resolution of an animation using the
writer_kwargs
write your own animation function using the
@is_plot_func
decoratorwrite 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
update
function, andpassed the
update
function 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.