Simulate Traffic with Rule 184
The forest fire consumed its fuel. Rule 184 is the opposite case: a conserved quantity that moves. It is also a clean example of going from an abstract rule table to a model with a concrete interpretation.
Use a one-dimensional ring road:
1 = car
0 = empty road
A car moves one cell to the right when the destination is empty.
That is enough to produce free flow, queues and a macroscopic density-flow relationship.
The mechanism below is written directly in traffic language, but it is exactly Rule 184 underneath: traffic_step agrees with the elementary update for rule 184 (10111000) on every tested configuration. The number and the metaphor are the same object.
Write the traffic mechanism directly
import numpy as np
def traffic_step(road):
cars = road == 1
empty_ahead = (
np.roll(road, -1) == 0
)
moving = cars & empty_ahead
next_road = road.copy()
next_road[moving] = 0
next_road[
np.roll(moving, 1)
] = 1
return next_road
The road is periodic, so the final road cell connects back to the first.
For this model that is deliberate:
closed ring road
not an implementation accident. The ring fixes the car count and removes boundary inflow, so density becomes a clean control parameter — at the price that every result is conditional on closed-system assumptions.
Conservation gives us a strong invariant
Cars do not appear or disappear:
road = np.array(
[1, 0, 1, 1, 0, 0, 1],
dtype=np.uint8,
)
next_road = traffic_step(road)
assert road.sum() == next_road.sum()
(Verified over extended runs: the count never changes, including under the stochastic variant below, which moves cars but creates none.)
That invariant is stronger than testing only a few expected cells.
It expresses something the model must preserve under every valid update.
Conservation is doing structural work here: it is what makes density a meaningful axis for the sweep that follows. Name the conserved quantity whenever a model has one; later chapters will reuse the habit.
The whole mechanism in one place — local condition, movement, and what conservation buys:
| Neighborhood | Car moves iff | Interpretation | Conservation consequence |
|---|---|---|---|
| cell ahead empty | one step right | free flow | count unchanged |
| cell ahead occupied | stays | queueing, jams | count unchanged |
| hesitation draw fails | stays | driver delay (stochastic variant) | count unchanged |
Initialize by density
def make_road(
length=240,
density=0.35,
seed=42,
):
rng = np.random.default_rng(seed)
return (
rng.random(length) < density
).astype(np.uint8)
Run it:
road = make_road(density=0.62)
history = []
for _ in range(180):
history.append(road.copy())
road = traffic_step(road)
history = np.array(history)
Because rows represent time and columns represent road position, the result is another spacetime diagram.

Diagonal traces show cars advancing.
Dense structures reveal blocked movement.
One local exclusion rule is enough to create collective congestion.
Measure movement, not only occupancy
Density tells us how much of the road is occupied.
Flow — flux, the number of cars passing per site per step — tells us how much movement occurs.
def traffic_step_with_flow(road):
cars = road == 1
empty_ahead = (
np.roll(road, -1) == 0
)
moving = cars & empty_ahead
next_road = road.copy()
next_road[moving] = 0
next_road[
np.roll(moving, 1)
] = 1
return next_road, int(moving.sum())
Normalize movement by road length:
flow_per_cell = moving_cars / len(road)
Now sweep density after allowing a warm-up period.
def average_flow(
density,
steps=700,
warmup=200,
length=600,
seed=1,
):
road = make_road(
length,
density,
seed,
)
values = []
for t in range(steps):
road, moving = (
traffic_step_with_flow(road)
)
if t >= warmup:
values.append(
moving / length
)
return float(np.mean(values))

For deterministic Rule 184 on a ring, the steady-state shape is quantitative, not just qualitative: flow follows approximately min(density, 1 − density), peaking near 0.5 at density 0.5 and falling symmetrically (verified: measured 0.095/0.303/0.483/0.293/0.098 at densities 0.1, 0.3, 0.5, 0.7 and 0.9). Below half-filling, cars are the scarce resource; above it, empty spaces are.
The curve reads in three regimes:
low density:
few cars exist
-> low total flow
intermediate density:
many cars can move
-> high flow
high density:
empty destinations are scarce
-> flow falls
The macroscopic relationship is not coded directly.
It emerges from local occupancy constraints.
A traffic jam is an observer-level object
No cell contains:
JAM = True
No car computes queue length.
Each car only needs to know:
am I here?
is the cell ahead empty?
Yet we can observe a persistent region of blocked vehicles and call it a traffic jam.
This is the same ontological split we saw with gliders:
implementation:
bits + local rules
observer:
cars + queues + flow
Add stochastic slowing
Real drivers do not always move whenever space is available.
A hesitation probability adds another mechanism:
def stochastic_traffic_step(
road,
rng,
slow_probability=0.1,
):
cars = road == 1
empty_ahead = (
np.roll(road, -1) == 0
)
willing = (
rng.random(len(road))
>= slow_probability
)
moving = (
cars
& empty_ahead
& willing
)
next_road = road.copy()
next_road[moving] = 0
next_road[
np.roll(moving, 1)
] = 1
return next_road
Now two sources can reduce flow:
physical blocking
random hesitation
Those should be measured separately.
This is the entry point to the famous Nagel–Schreckenberg traffic model (1992), which builds on this deterministic core by adding variable speeds, braking, and stochastic slowing. Rule 184 is its minimal ancestor: one speed, one lane, hesitation optional.
Rule 184 is one traffic model, not traffic itself
The model assumes:
one lane
one cell per vehicle
maximum speed one cell per step
no overtaking
closed ring road
synchronous updates
Those assumptions are useful because they isolate the mechanism.
A richer model can add velocity, braking, lane changes or open boundaries.
But the discipline stays the same, with the guardrail this chapter earns:
conservation law
≠ realistic traffic model
≠ predictive transportation model
Start from the smallest local information needed to express the mechanism you care about.
One idea to keep
Rule 184 turns a microscopic rule into a macroscopic observable.
That gives us a useful experimental pattern:
local update
↓
trajectory
↓
observable
↓
parameter sweep
↓
emergent relationship
In the next chapter we will apply that pattern to a continuous quantity and show how local exchange creates diffusion.
Research
Zenil, H. & Martinez, G. J. — Cellular Automata (Scholarpedia). Situates this chapter’s model in the traffic lineage: stochastic automata with conditionally independent draws, and the Nagel–Schreckenberg freeway model as the prominent descendant of deterministic cores like Rule 184. Read before extending the model toward realism. http://www.scholarpedia.org/article/Cellular_automata
Berto, F. & Tagliabue, J. — Cellular Automata (Stanford Encyclopedia of Philosophy). Frames Rule 184-style work as discrete dynamical-system simulation: local conservation laws reproducing macroscopic behavior is the mechanism, and the value lies in what the simple model generates rather than in predictive fidelity. https://plato.stanford.edu/entries/cellular-automata/