Build Your First Automaton
In the previous chapter we reduced a cellular automaton to four things:
space
state
neighborhood
rule
Now we are going to build one.
Not a framework.
Not a library.
One update step.
That is enough to expose the whole mechanism.
Start with a one-dimensional world
Create a row of cells and turn on the center cell:
import numpy as np
width = 41
state = np.zeros(width, dtype=np.uint8)
state[width // 2] = 1
print(state)
Conceptually:
....................#....................
We will use 0 for an empty cell and 1 for an active cell.
Define one local rule
Suppose a cell becomes active when exactly one of the three cells in its neighborhood is active.
That is easy to understand immediately — and it is also a genuine member of the elementary catalog. Counting the neighborhoods with exactly one active cell (100, 010, 001) gives outputs at bit positions 4, 2, and 1, so this rule is Rule 22 (16 + 4 + 2 = 22, or 00010110 in binary). The next chapter will explain that numbering scheme in full; for now it is enough to know our first rule has a name and an address.
def local_rule(left, centre, right):
return int(left + centre + right == 1)
Now apply it across the row:
def step(state, rule):
next_state = np.zeros_like(state)
for i in range(len(state)):
left = state[(i - 1) % len(state)]
centre = state[i]
right = state[(i + 1) % len(state)]
next_state[i] = rule(left, centre, right)
return next_state
We pass the rule in as an argument from the start. The engine handles neighborhoods and time stepping; the rule handles the local transition only. That separation will pay off as soon as we want to explore hundreds of rules.
The modulo operator gives us periodic boundaries:
left edge <----------------> right edge
The world wraps around like a ring.
Run several generations
def run(initial_state, rule, generations):
history = [initial_state.copy()]
state = initial_state.copy()
for _ in range(generations - 1):
state = step(state, rule)
history.append(state.copy())
return np.array(history)
Now:
history = run(state, local_rule, generations=25)
The result is a two-dimensional array:
rows = time
columns = space
This is one of the useful tricks of 1D cellular automata.
A one-dimensional system evolving through time naturally becomes a two-dimensional image. Each row is a complete configuration; stacking rows gives the space-time diagram for the run.
Visualize the history
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.imshow(history, cmap="binary", interpolation="nearest")
plt.xlabel("cell")
plt.ylabel("generation")
plt.show()
You have now built a cellular automaton.
The entire runtime is:
for every generation:
for every cell:
read neighborhood
apply rule
write next state
Why we need two states of the world
A common implementation mistake is to update the current row in place.
For example:
for i in range(len(state)):
state[i] = local_rule(...)
That changes the meaning of the simulation.
Cells later in the loop would observe already-updated neighbors while earlier cells observed old neighbors — the loop silently mixes two time steps:
flowchart LR
A[cell i reads neighbors at time t] --> B[cell i writes time t+1]
B --> C[cell i+1 reads: left neighbor already at t+1, right still at t]
C --> D[mixed-time neighborhoods propagate down the row]
Instead we need:
current generation
|
v
compute every update
|
v
next generation
Only after the whole generation has been calculated do we replace the current state.
This is synchronous updating: every cell moves from time t to time t + 1 together, which is the classical assumption and what makes the run reproducible. Later we will deliberately experiment with asynchronous updates — they are a legitimate modeling choice with their own literature — but they should be a model choice rather than an accidental bug.
Boundary conditions are part of the model
Our modulo indexing created periodic boundaries: the row is a ring, and patterns leaving one side reenter from the other.
We could instead use fixed zeros:
def get_cell(state, i):
if i < 0 or i >= len(state):
return 0
return state[i]
Or reflective boundaries, or an effectively infinite sparse world.
These choices matter, and they recur throughout the book — this chapter’s boundary menu, in one place:
| Boundary | Cells beyond the edge read as | Best for |
|---|---|---|
| Periodic | the opposite edge (ring/torus) | comparing rules without edge effects |
| Fixed | a prescribed state (usually 0) | maps with real edges: forests, caves |
| Reflective | mirrored interior values | contained physical quantities |
| Infinite/sparse | undefined until activity arrives | single gliders, localized seeds |
The update rule is not the entire model. The geometry and boundary behavior also determine what patterns are possible. Periodic boundaries remove the edge but add a different artifact: on a small ring a spreading pattern can wrap around and collide with itself, producing finite-size effects that would not occur on a larger or infinite lattice. Name the boundary in every experiment, and distrust any conclusion you have only seen at one width.
Separate mechanism from rule
Our step already keeps the two concerns apart:
simulation engine
|
+--> neighborhood lookup
+--> time stepping
+--> boundary behavior
rule
|
+--> local transition only
Because the rule is just a function of three bits, swapping in a new automaton means writing a new three-bit function — nothing else changes. The next chapter turns that observation into a complete encoding: every elementary rule as one integer.
A compact text renderer
Before reaching for plots, it is useful to have a tiny text view:
def render_row(state):
return "".join("#" if cell else "." for cell in state)
state = np.zeros(41, dtype=np.uint8)
state[len(state) // 2] = 1
for _ in range(20):
print(render_row(state))
state = step(state, local_rule)
Text output is excellent for debugging because it removes the visualization stack from the problem.
If a rule behaves unexpectedly, we can inspect the exact cells.
What we have built
Our automaton now has a reusable execution loop:
current = initial
for generation in range(n):
observe(current)
current = step(current, rule)
That shape will survive almost the entire book.
Even when the state becomes a tensor with many channels and the rule becomes a neural network, the conceptual loop remains:
observe local state
apply shared transition
advance time
In the next chapter we will replace our hand-written rule with a more powerful idea: encode the complete rule table as a single integer.
That gives us all 256 elementary cellular automata for free — including Rule 22, which we have already been running.
Research
Martinez, G. J., Adamatzky, A., Hoffmann, R., Deserable, D. & Zelinka, I. — On patterns and dynamics of Rule 22 cellular automaton. Confirms what this chapter now states: the “exactly one active cell” rule is Rule 22, a three-input XOR whose space-time dynamics show non-trivial, quasi-chaotic patterns. Useful if you want to see how much analysis a single elementary rule can sustain — mean-field theory, attractors, de Bruijn and subset diagrams, filters. https://arxiv.org/abs/2005.01480
Zenil, H. & Martinez, G. J. — Cellular Automata (Scholarpedia). Grounds the chapter’s two load-bearing implementation claims: classical automata update synchronously in parallel, and finite simulations must specify boundary conditions (periodic, fixed, reflective, absorbing, open) because the choice affects propagation and long-term behavior. Peer-reviewed and actively maintained. http://www.scholarpedia.org/article/Cellular_automata
Berto, F. & Tagliabue, J. — Cellular Automata (Stanford Encyclopedia of Philosophy). Supports the synchrony discussion from the other side: synchronous update is the usual assumption, not a law — the transition rule can be probabilistic and updating can be asynchronous (Ingerson & Buvel 1984). Read before treating async schedules as a bug rather than a model. https://plato.stanford.edu/entries/cellular-automata/
Wolfram, S. — Statistical Mechanics of Cellular Automata (Reviews of Modern Physics 55, 1983). The origin of the space-time diagram convention used here (successive configurations stacked as image rows) and of the systematic study of the rule space the next chapter encodes. Worth skimming for how Wolfram set up runs before classifying them. https://doi.org/10.1103/RevModPhys.55.601