Lab 2 · Phantom Jams on Your Laptop

Reproduce the 1959 GM experiment · local and string stability · when a platoon collides

Open In Colab Run this lab in your browser, with nothing to install.

In 1959, four researchers at General Motors (Herman, Montroll, Potts and Rothery) asked a simple question: if every driver just tries to match the speed of the car ahead, after a short reaction time, is traffic stable? Their answer explained phantom jams sixty years before anyone filmed one on a ring road.

In this lab you will rebuild their experiment from scratch:

  1. Simulate a line of cars with a reaction delay.
  2. Find the thresholds where one driver, and then a whole line of drivers, becomes unstable.
  3. Discover when a platoon crashes, even though no single driver does anything unreasonable.
import numpy as np
import matplotlib.pyplot as plt

def brake_pulse(t):
    """The leader brakes at 1 m/s² for 2 s, then accelerates back for 2 s."""
    return -1.0 if t < 2 else (1.0 if t < 4 else 0.0)

1 · The model

Car n+1 follows car n. After a reaction time T, the follower accelerates in proportion to the speed difference:

\ddot x_{n+1}(t) = \lambda\,\big[\dot x_n(t-T) - \dot x_{n+1}(t-T)\big].

Everything depends on C = \lambda T. We track u_n(t), each car’s speed change from cruising speed, and step forward in time with Euler’s method, u(t+\Delta t) = u(t) + a\,\Delta t. The delay means the acceleration now uses speeds from T seconds ago.

Your turn. Complete simulate below.

def simulate(C, n_cars=8, T=1.0, dt=0.01, t_end=50.0, leader_accel=brake_pulse):
    """Speed change of each car behind a leader, for the 1959 GM model.

    Returns (t, u): t has shape (steps + 1,), and u has shape (n_cars + 1, steps + 1).
    Row 0 of u is the leader; row n is the n-th follower. All speed changes start at 0.

    The follower's acceleration at step k uses speeds from `lag = round(T / dt)` steps earlier:
        a_n = (C / T) * (u[n-1, k-1-lag] - u[n, k-1-lag])     (and 0 while k-1-lag < 0)
    """
    lam = C / T
    lag = round(T / dt)
    steps = round(t_end / dt)
    t = np.arange(steps + 1) * dt
    u = np.zeros((n_cars + 1, steps + 1))
    # YOUR CODE HERE: step through time with Euler's method.
    # At each step: update the leader from leader_accel(t), then each follower from the delayed speeds.
    return None

2 · One follower: local stability

Start with just one car behind the leader, and try five values of C.

Cs = [0.3, 1 / np.e, 1.0, np.pi / 2, 1.7]
fig, axes = plt.subplots(1, len(Cs), figsize=(16, 3), sharey=True)
for ax, C in zip(axes, Cs):
    result = simulate(C, n_cars=1, t_end=40)
    if result is None:
        ax.text(0.5, 0.5, "complete simulate()", ha="center", transform=ax.transAxes)
    else:
        t, u = result
        ax.plot(t, u[0], "k--", lw=1, label="leader")
        ax.plot(t, u[1], color="#57068c", lw=2, label="follower")
    ax.set_title(f"C = {C:.3f}")
    ax.set_xlabel("Time (s)")
    ax.set_ylim(-4, 4)
axes[0].set_ylabel("Speed change (m/s)")
if result is not None:
    axes[0].legend()
plt.tight_layout()

Question 1. Describe the follower’s response in each panel. Herman and his colleagues proved that the response is smooth for C \le 1/e, oscillates but settles for 1/e < C < \pi/2, and grows for C > \pi/2. Do your plots agree?

3 · A line of cars: string stability

Now put 8 cars behind the leader. A disturbance can fade or grow as it passes back down the line. We measure the amplification: the largest speed change of the last car, divided by that of the first follower.

def amplification(C, n_cars=8):
    result = simulate(C, n_cars=n_cars, t_end=60)
    if result is None:
        return np.nan
    _, u = result
    peak = np.abs(u).max(axis=1)
    return peak[-1] / peak[1]

Cs = np.linspace(0.2, 1.0, 41)
amp = [amplification(C) for C in Cs]
plt.figure(figsize=(8, 4))
plt.semilogy(Cs, amp, "o-", color="#57068c")
plt.axhline(1, color="gray", ls="--")
plt.axvline(0.5, color="red", ls=":", label="C = 1/2 (theory)")
plt.xlabel("C = λT")
plt.ylabel("Amplification (last / first)")
plt.legend()
plt.grid(True, which="both", alpha=0.3)

Question 2. Theory says a long line of cars is stable only when C < 1/2. At what C does your amplification cross 1? It is not exactly 1/2. Give two reasons why. (Hint: how many cars are there, and how long does the leader brake?)

Question 3. Try n_cars=20. Does the crossing move towards 1/2?

4 · When does a platoon crash?

Speed changes are one thing; collisions are another. Give every car a real position: 20 m/s cruising, 15 m from bumper to bumper, 5 m long.

Your turn. Complete first_collision below.

def first_collision(C, n_cars=9, spacing=15.0, speed=20.0, car_length=5.0, **kw):
    """Time (s) of the first collision in the platoon, or None if nobody collides.

    Every car starts at `speed` m/s with `spacing` metres from front bumper to front bumper.
    Hint: position = initial position + integral of (speed + u) dt. A collision happens when
    the gap to the car ahead, minus car_length, reaches 0.
    """
    # YOUR CODE HERE
    return "not implemented"
for C in [0.4, 0.6, 0.8, 1.0]:
    hit = first_collision(C)
    if hit == "not implemented":
        print("Complete first_collision() above")
        break
    print(f"C = {C:.1f}: " + ("no collision" if hit is None else f"first collision at t = {hit:.2f} s"))
Complete first_collision() above

Question 4. Every driver in this platoon follows a perfectly sensible rule. Who is to blame for the collision? What does this tell you about designing automated vehicles?

Going further

  • Faster reactions. Keep \lambda fixed and halve T. What happens to C, and to the collision?
  • One automated car. Make car 4 react with C = 0.3 while the others keep C = 0.8. Does one careful driver protect the cars behind it?
  • Back to the ring. The ring road in Lecture 1 used a different model (IDM). Why did raising driver attentiveness dissolve its jam?