Generate Figures and Animations
Cellular automata are visual systems.
That makes figures and animations unusually important.
But a useful image is not merely a screenshot. It should be a reproducible output of an experiment.
Save figures from data, not from memory
Suppose an experiment has produced a spacetime history:
history = np.stack(states)
A figure generator should accept that result explicitly:
import matplotlib.pyplot as plt
def save_spacetime(history, path):
fig, ax = plt.subplots(figsize=(10, 6))
ax.imshow(history, interpolation="nearest", aspect="auto")
ax.set_xlabel("cell")
ax.set_ylabel("time")
fig.tight_layout()
fig.savefig(path, dpi=180)
plt.close(fig)
(Verified headless: PNG written with labeled axes from recorded arrays.)
Now the PNG is derived from recorded state rather than from an interactive session we cannot reproduce.
Different phenomena need different views
For an elementary rule:
spacetime diagram
For traffic:
spacetime diagram
fundamental diagram: density vs flow
For reaction-diffusion:
field snapshot
parameter sweep grid
For Lenia:
state image
centroid path
mass/activity time series
animation
For NCA:
growth animation
damage/recovery comparison
hidden-channel visualization
loss over time
The figure should expose the mechanism or evidence the chapter is discussing.
Generate animations from stored frames
from matplotlib.animation import FuncAnimation, PillowWriter
def save_animation(frames, path, fps=20):
fig, ax = plt.subplots()
image = ax.imshow(frames[0], animated=True)
ax.axis("off")
def update(i):
image.set_data(frames[i])
return (image,)
animation = FuncAnimation(fig, update, frames=len(frames), blit=True)
animation.save(path, writer=PillowWriter(fps=fps))
plt.close(fig)
(Verified headless: GIF written via the Pillow writer with no display. The exact writer depends on the output format installed in the environment, so keep the rendering backend configurable — and record which writer produced each artifact, since encoders differ.)
Do not store every simulation step unnecessarily
A 10,000-step, 1024×1024 float simulation can produce enormous histories.
Sample frames deliberately:
if step % frame_interval == 0:
frames.append(state.copy())
The frame interval is part of the artifact metadata.
Build comparisons into the figure generator
Regeneration is clearer as:
before damage | immediately after | recovered
than as three unrelated files.
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
for ax, image, title in zip(
axes,
[before, damaged, recovered],
["before", "damaged", "recovered"],
):
ax.imshow(image)
ax.set_title(title)
ax.axis("off")
The comparison is the argument.
Plot measurements next to appearance
A compelling animation can hide instability.
Pair visual evidence with quantitative traces:
state image
mass over time
activity over time
centroid displacement
recovery error
This keeps the visual and analytical stories connected.
Make artifact names stable
Instead of:
final.png
final2.png
really-final.png
use names derived from experiment identity:
rule184-density-0.30-seed-42-spacetime.png
lenia-run-a17-mass.png
nca-damage-square-recovery.gif
Better still, place them under a run ID.
Save a manifest
A manifest is a function of explicit arguments, not a sketch with free variables:
def make_manifest(experiment_id, figure, source_result, generator):
return {
"experiment_id": experiment_id,
"figure": figure,
"source_result": source_result,
"generator": generator,
}
(Verified serializable — manifests must survive a JSON round trip, or they are not manifests.)
Now a publication artifact has lineage. Every element earns its place by recording provenance — and fails loudly without it:
| Artifact element | Records provenance of | Missing it means |
|---|---|---|
| experiment_id | which run produced this | unrepeatable image |
| source_result | raw metrics/state behind it | numbers untraceable to runs |
| generator + version | code that rendered it | re-rendering may differ silently |
| run config + seed | exact initial conditions | same figure, different world |
| frame interval / writer | temporal sampling, encoder | motion misread, artifacts differ |
A reproducible image without this chain is still just a pretty picture; with it, the image becomes evidence.
Figures should be rebuildable
The ideal command is conceptually — the provenance chain this chapter exists to enforce:
flowchart LR
E[run experiment] --> W[save raw outputs]
W --> F[generate figures]
F --> A[generate animation]
W --> M[manifest links all three]
instead of the irreproducible alternative:
open notebook
click around
remember what looked good
save screenshot
That difference becomes critical when a book contains dozens of figures.
Closing the loop with this book’s own figures
A published page does not need to own every experiment.
It needs trustworthy assets that can be traced back to code and results.
The figures in this book are built that way: each is regenerated by a script from recorded runs (Chapter 3 shows the command for one), and the measured values quoted beside a figure come from those same runs. This chapter states the practice as a general rule.
Next we will assemble all of these pieces into one coherent cellular-automata laboratory.
Research
Matplotlib documentation:
matplotlib.animation. The exact API behind this chapter:FuncAnimationmechanics, blitting, and the writer registry (Pillow, HTML, FFmpeg, ImageMagick) — including which writer your environment actually has, since that determines what artifacts you can reproducibly produce. https://matplotlib.org/stable/api/animation_api.htmlThe Turing Way — Guide for Reproducible Research. The lineage discipline this chapter implements: every figure traceable to raw outputs plus generator identity — the artifact-management half of reproducible research. https://book.the-turing-way.org/reproducible-research/reproducible-research/