← Cellular Automata From First Principles

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 elementRecords provenance ofMissing it means
experiment_idwhich run produced thisunrepeatable image
source_resultraw metrics/state behind itnumbers untraceable to runs
generator + versioncode that rendered itre-rendering may differ silently
run config + seedexact initial conditionssame figure, different world
frame interval / writertemporal sampling, encodermotion 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: FuncAnimation mechanics, 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.html

  • The 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/