Traffic Model

Nagel-Schreckenberg Dynamics

Traffic flow is a natural agent-based model. Each car moves according to simple local rules, but repeated interactions create stop-and-go waves, jams, and phase transitions between free flow and congestion (Nagel and Schreckenberg 1992).

Figure 1: Traffic CA space-time diagram (cars as black pixels).

Represent the road state

We store the system with two arrays of length length:

  • occupied[i] tells us whether cell i contains a car.
  • speeds[i] stores the speed of that car, measured in cells per step.

This lets the road stay discrete while each agent still has its own internal state.

Initialize the agents

The initialization function samples an occupancy pattern and assigns a random speed to each occupied cell.

occupied, speeds = initialize_road(length=100, density=0.2, vmax=5)

The density is

\[ \rho = \frac{\text{number of cars}}{\text{road length}}. \]

Implement one synchronous update

At each time step, every car follows the same four rules:

  1. Accelerate up to \(v_{\max}\).
  2. Brake so it does not collide with the next car.
  3. Slow down randomly with probability \(p_{\text{slow}}\).
  4. Move forward by its new speed.

The key point is that all cars update synchronously.

def step(occupied, speeds, vmax=5, p_slow=0.3, rng=None):
    if rng is None:
        rng = np.random.default_rng()

    # TODO: accelerate
    # TODO: brake to avoid collisions
    # TODO: random slowdown
    # TODO: move cars
    return new_occupied, new_speeds
def step(occupied, speeds, vmax=5, p_slow=0.3, rng=None):
    if rng is None:
        rng = np.random.default_rng()

    length = len(occupied)
    positions = np.where(occupied)[0]
    speeds_new = speeds.copy()

    speeds_new[positions] = np.minimum(speeds_new[positions] + 1, vmax)

    gaps = np.zeros_like(positions)
    for idx, pos in enumerate(positions):
        next_pos = positions[(idx + 1) % len(positions)]
        gaps[idx] = (next_pos - pos - 1) % length
    speeds_new[positions] = np.minimum(speeds_new[positions], gaps)

    slow_mask = rng.random(len(positions)) < p_slow
    speeds_new[positions[slow_mask]] = np.maximum(
        speeds_new[positions[slow_mask]] - 1, 0
    )

    new_occupied = np.zeros_like(occupied)
    new_speeds = np.zeros_like(speeds)
    new_positions = (positions + speeds_new[positions]) % length
    new_occupied[new_positions] = True
    new_speeds[new_positions] = speeds_new[positions]

    return new_occupied, new_speeds

Simulate a full run

Wrap the update in a loop and save the occupancy pattern at every time step.

grid = simulate(steps=200, length=100, density=0.2, vmax=5, p_slow=0.3, seed=1)

Plot the space-time diagram

plt.imshow(grid, cmap="binary", interpolation="nearest", aspect="auto")
plt.xlabel("Road position")
plt.ylabel("Time")
plt.show()

What the dark bands mean

In the diagram, black cells are occupied sites. When dense diagonal bands become nearly horizontal, cars are moving slowly and congestion is forming. When the pattern is sparse and diagonal, cars are advancing freely.

This is the central modeling lesson of the session: a jam is not imposed externally. It emerges from repeated local braking and stochastic slowdown.

Full reference: amlab/cellular_automata/traffic.py.

What’s Next?

Once the simulator works, the next step is to measure macroscopic quantities such as average flow and to build the traffic fundamental diagram.

Flow and Congestion Back to Session Index

References

Nagel, Kai, and Michael Schreckenberg. 1992. “A Cellular Automaton Model for Freeway Traffic.” Journal de Physique I 2 (12): 2221–29. https://doi.org/10.1051/jp1:1992277.