Measure a Cellular Automaton
Cellular Automata From First Principles 17: Measure a Cellular Automaton
Up to this point we have mostly looked at cellular automata.
That is useful.
It is also limiting.
A human can inspect a handful of spacetime diagrams.
We cannot reliably inspect:
256 elementary rules
× several initial conditions
× several widths
× hundreds of generations
× repeated stochastic runs
by eye.
If we want to compare rules, search for interesting behavior or eventually optimize a rule for some objective, we need to turn behavior into data.
This chapter builds the measurement layer.
A simulation produces a trajectory
For a one-dimensional binary cellular automaton, a complete run can be stored as:
history.shape
# (generations, cells)
Every row is one state of the world.
So:
history[t]
means:
the complete spatial state at generation t
The matrix is not merely something to plot.
It is a dataset.
We can ask:
How many cells are active?
How much changes between generations?
How fragmented is the spatial pattern?
Does the system become fixed?
Does it repeat?
How sensitive is it to initial conditions?
No single answer defines complexity.
But each answer exposes one observable property of the dynamics.
Density
The simplest measurement is the fraction of active cells:
import numpy as np
def density(state):
return float(np.mean(state))
For:
00011101
four of eight cells are active:
density = 0.5
Across an entire run:
def density_curve(history):
return np.mean(history, axis=1)
Now we have:
generation -> active-cell fraction
Different systems can behave very differently.
A rule may:
die out
-> density approaches 0
saturate
-> density approaches 1
remain mixed
-> density stays between the extremes
oscillate
-> density changes periodically
But density alone does not tell us whether the cells are actually changing.
Change rate
Two generations can have exactly the same density while containing active cells in completely different positions.
So measure temporal change directly:
def change_rate(previous, current):
return float(
np.mean(previous != current)
)
Across a run:
def change_curve(history):
return np.mean(
history[1:] != history[:-1],
axis=1,
)
Now:
change rate = 0
means the state is unchanged from the previous generation.
A fixed point has:
density = constant
change = 0
But an oscillator may have:
density = constant
change > 0
That distinction is exactly why we need more than one observable.
Spatial transition rate
Temporal change tells us what happens between generations.
We can also measure structure inside one generation.
Count how often neighboring cells differ:
def transition_rate(state):
right = np.roll(state, -1)
return float(
np.mean(state != right)
)
Compare:
0000000011111111
with:
0101010101010101
Both contain equal numbers of zeros and ones.
So both have:
density = 0.5
But their spatial organization is completely different.
The first has only a few boundaries.
The second changes almost every cell.
Transition rate gives us a crude measure of local spatial fragmentation.
The same system needs several views
Consider three questions:
How much is active?
-> density
How much changes over time?
-> change rate
How rough is the spatial arrangement?
-> transition rate
These are different properties.
That is the key lesson of this chapter.

The figure compares several rules from the same initial-condition protocol.
No single curve tells the full story.
Together they begin to form a behavioral fingerprint.
Summarize one run
We can combine basic measurements:
def summarize(history):
changes = change_curve(history)
transitions = np.array([
transition_rate(state)
for state in history
])
return {
"final_density": float(
density(history[-1])
),
"mean_density": float(
np.mean(history)
),
"mean_change": float(
np.mean(changes)
) if len(changes) else 0.0,
"mean_transition_rate": float(
np.mean(transitions)
),
"final_transition_rate": float(
transitions[-1]
),
}
Now a trajectory has a compact numerical description.
But that description is only meaningful if we also know how the trajectory was generated.
A measurement without experiment context is incomplete
Rule 30 started from a single active cell is not the same experiment as Rule 30 started from random noise.
Likewise:
periodic boundaries
and:
fixed boundaries
can produce different trajectories.
So a measurement record should include its experimental context.
For example:
result = {
"rule": 30,
"width": 201,
"generations": 200,
"initial_condition": "single",
"boundary": "periodic",
"seed": None,
"metrics": summarize(history),
}
For a stochastic run we might instead record:
"seed": 42
The rule is:
Never separate a metric from the experiment that produced it.
Turn every rule into a record
Now evaluate all elementary cellular automata:
records = []
for rule_number in range(256):
history = run_rule(
rule_number,
width=201,
generations=200,
)
records.append({
"rule": rule_number,
**summarize(history),
})
We have transformed:
256 images
into:
256 structured records
Now we can sort:
most_active = sorted(
records,
key=lambda row: row["mean_change"],
reverse=True,
)
Filter:
candidates = [
row
for row in records
if 0.2 < row["mean_density"] < 0.8
and row["mean_change"] > 0.1
]
Or plot rules in measurement space.
This is the beginning of automated exploration.
Preserve trajectories and summaries separately
The summary is convenient.
The trajectory is evidence.
Do not throw away the complete history simply because you calculated a few metrics.
A useful experimental record has two levels:
raw trajectory
↓
derived measurements
If a metric later turns out to be misleading, we can calculate a better one from the original run.
That is much harder if only the summary survived.
One metric is never enough
A checkerboard has:
high transition rate
but it is extremely regular.
Random noise can have:
high entropy
without having persistent structure.
An oscillator can have:
high temporal activity
while remaining perfectly predictable.
So we are not searching for:
the complexity number
There probably is no single scalar that captures everything we care about.
Instead we are building a collection of observables:
density
activity
spatial variation
entropy
periodicity
attractor structure
sensitivity
persistence
Different questions require different measurements.
The measurement pipeline
We now have a new architecture:
experiment configuration
↓
simulation
↓
trajectory
↓
measurements
↓
result record
The next stages will add:
comparison
classification
search
selection
This changes the role of the cellular automaton.
It is no longer only something we render.
It becomes something we can experiment on systematically.
One idea to keep
A measurement does not explain a cellular automaton.
It gives us another way to interrogate it.
The strongest workflow is:
look
↓
measure
↓
compare
↓
form hypothesis
↓
run another experiment
In the next chapter we will focus on the first two observables — activity and density — and use them to distinguish frozen, saturated and persistently changing systems.