Simulate Traffic with Rule 184

Page content

Cellular Automata From First Principles 10: Simulate Traffic with Rule 184

Rule 184 is a beautiful example of moving 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.


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.


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()

That invariant is stronger than testing only a few expected cells.

It expresses something the model must preserve under every valid update.


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.

Rule 184 traffic 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 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))

Rule 184 density-flow relationship

For deterministic Rule 184 on a ring, the characteristic shape is easy to interpret:

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.


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:

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.