# Simulate Traffic with Rule 184 # Book: Cellular Automata From First Principles # Chapter URL: https://programmer.ie/books/cellular-automata-from-first-principles/11-chapter/ # Prose fingerprint (sha256, normalized): 31d01ed0eddb0b5479900f33ae20346b44673c7a8464e7bd6b012e2753d679fe # Snapshot generated at build time by Hugo. This is the complete rendered prose # of the chapter; it is not a transcript of the source file. 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/