Generate Figures and Animations

Page content

Cellular Automata From First Principles 56: 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)

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


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, fps=fps)
    plt.close(fig)

The exact writer depends on the output format installed in the environment, so keep the rendering backend configurable.


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

manifest = {
    "experiment_id": experiment_id,
    "figure": "traffic-flow.png",
    "source_result": "metrics.json",
    "generator": "plot_traffic_flow",
}

Now a publication artifact has lineage.


Figures should be rebuildable

The ideal command is conceptually:

run experiment
save raw outputs
generate figures
generate animation

not:

open notebook
click around
remember what looked good
save screenshot

That difference becomes critical when a book contains dozens of figures.


This closes the loop with the publication layer

The website does not need to own every experiment.

It needs trustworthy assets that can be traced back to code and results.

That is the architecture we discussed earlier, and this chapter gives it a concrete technical form.

Next we will assemble all of these pieces into one coherent cellular-automata laboratory.