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).
Represent the road state
We store the system with two arrays of length length:
occupied[i]tells us whether cellicontains 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:
- Accelerate up to \(v_{\max}\).
- Brake so it does not collide with the next car.
- Slow down randomly with probability \(p_{\text{slow}}\).
- 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_speedsdef 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_speedsSimulate 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.