2.21. Lecture 15: Herds, flocks, and traffic jams

Before this class you should:

  • Read Think Complexity, Chapter 10

Before next class you should:

  • Read Think Complexity, Chapter 11

Note taker: Nathan Chung

2.21.1. Overview

This chapter moves from grid-based agent models to agents operating in continuous space. The two main case studies are simulated cars on a circular highway (used to study spontaneous traffic jams) and simulated birds in 3D space (Boids). Both are used to explore emergence, which is the idea that macro-level behaviour can arise from simple local rules, without any central coordination.

2.21.2. Traffic Jams

Traffic jams don’t always need an obvious cause like an accident. They can emerge spontaneously. The simulation uses two classes: Highway and Driver. Cars are placed evenly on a circular road and always try to accelerate at the maximum rate. The Highway class is initialized as follows:

class Highway:

    def __init__(self, n=10, length=1000, eps=0):
        self.length = length
        self.eps = eps

        # create the drivers
        locs = np.linspace(0, length, n, endpoint=False)
        self.drivers = [Driver(loc) for loc in locs]

        # and link them up
        for i in range(n):
            j = (i+1) % n
            self.drivers[i].next = self.drivers[j]

Each time step, the highway moves every driver:

#Highway

    def step(self):
        for driver in self.drivers:
            self.move(driver)

The move method handles the physics: it checks the distance to the car ahead, applies the chosen acceleration, adds random noise controlled by eps, enforces a speed limit of 40, and stops the car if it would otherwise collide:

# Highway

def move(self, driver):
    dist = self.distance(driver)

    # let the driver choose acceleration
    acc = driver.choose_acceleration(dist)
    acc = min(acc, self.max_acc)
    acc = max(acc, self.min_acc)
    speed = driver.speed + acc

    # add random noise to speed
    speed *= np.random.uniform(1-self.eps, 1+self.eps)

    # keep it nonnegative and under the speed limit
    speed = max(speed, 0)
    speed = min(speed, self.speed_limit)

    # if current speed would collide, stop
    if speed > dist:
        speed = 0

    # update speed and loc
    driver.speed = speed
    driver.loc += speed

The basic Driver class always accelerates at the maximum rate:

class Driver:

    def __init__(self, loc, speed=0):
        self.loc = loc
        self.speed = speed

    def choose_acceleration(self, dist):
        return 1

Because of random noise, the spacing between cars gradually becomes uneven. Once a car is forced to stop, a jam can form and tends to persist. Cars approach from behind and pile up, while those at the front accelerate away. One interesting result is that the jam itself can propagate backwards even though every individual car is moving forward.

lectures/images/figure1.png

Figure 1: Circular highway simulation at three time steps. Squares = car positions; triangles = braking events.

2.21.3. Random Perturbation

Even a tiny amount of randomness has a large effect on highway capacity. With no noise (\(eps=0\)), the highway supports up to 25 cars at full speed. Beyond that, the spacing \(1000/n\) drops below 40 and speeds fall. With 0.1% noise, the maximum number of cars that can maintain full speed drops to \(\sim 20\), and with 1% noise, it drops further to \(\sim 10\). Small real-world imperfections compound into significant slowdowns.

lectures/images/figure2.png

Figure 2: Average speed vs. number of cars for \(eps=0\), \(eps=0.001\), and \(eps=0.01\).

2.21.4. Boids

In 1987 Craig Reynolds introduced Boids, an agent-based model of flocking. The name is a contraction of “bird-oid”. Boids are also used to model fish schools and herding animals. Each agent follows three simple rules using only local information:

  • Flock centering: move toward the centre of nearby Boids.

  • Collision avoidance: steer away from objects that are too close.

  • Velocity matching: align speed and direction with neighbours.

  • Carrot attraction: move toward a target “carrot” in the world.

Each Boid pays attention only to others within a field of view defined by a radius and a viewing angle. The implementation uses VPython for 3D graphics.

2.21.5. The Boid algorithm

The code is split into a Boid class (behaviours) and a World class (holds the list of Boids plus a “carrot” attractor). The Boid class has four methods, each returning an acceleration request:

  • center: finds neighbours and returns a vector toward their centroid.

  • avoid: same idea, smaller radius, wider range, result negated and steers away from nearby objects.

  • align: returns a vector toward the average velocity (not position) of neighbours.

  • love: returns a unit vector pointing toward the carrot.

The helper get_neighbors filters candidates by distance and viewing angle, and vector_toward_center computes a unit vector toward their mean position.

The get_neighbors method works as follows:

def get_neighbors(self, boids, radius, angle):
    neighbors = []
    for boid in boids:
        if boid is self:
            continue

        # if not in range, skip it
        offset = boid.pos - self.pos
        if offset.mag > radius:
            continue

        #  if not within viewing angle, skip it
        if self.vel.diff_angle(offset) > angle:
            continue

        # otherwise add it to the list
        neighbors.append(boid)

    return neighbors

vector_toward_center computes a unit vector toward the centroid:

def vector_toward_center(self, vecs):
    if vecs:
        center = np.mean(vecs)
        toward = vector(center - self.pos)
        return limit_vector(toward)
    else:
        return null_vector

2.21.6. Arbitration

The four acceleration requests are combined as a weighted sum: goal = w_center*center + w_avoid*avoid + w_align*align + w_love*love

The set_goal method implements this as follows:

def set_goal(self, boids, carrot):
    w_avoid = 10
    w_center = 3
    w_align = 1
    w_love = 10

    self.goal = (w_center * self.center(boids) +
                 w_avoid * self.avoid(boids, carrot) +
                 w_align * self.align(boids) +
                 w_love * self.love(carrot))
    self.goal.mag = 1

Typical weights are w_avoid=10, w_center=3, w_align=1, and w_love=10, meaning avoidance is prioritised to prevent collisions. Velocity is then updated as a blend of the old velocity and the goal: vel = (1-mu)*vel + mu*goal, where mu sets manoeuvrability. Changing the weights, radii, angles, and mu can produce qualitatively different collective behaviours, such as bird flocks, fish schools, or insect swarms.

2.21.7. Emergence and Free Will

A central theme across the book is that complex systems can exhibit properties that none of their parts have. Traffic jams move backward while every car moves forward. Boid flocks look centrally organised but aren’t. Schelling’s non-racist agents produce segregation. These are all examples of emergence.

The chapter uses this to open a discussion on free will. This raises an apparent conflict: if the brain is a deterministic physical system, how can choices be free? Two classic positions are:

  • William James: actions are generated by a random process then selected deterministically, so behaviour is fundamentally unpredictable.

  • David Hume: the feeling of choice is an illusion, so the system is fully deterministic.

The complex-systems view offers a third option: free will at the level of decisions is compatible with determinism at the level of neurons, just as a backward-moving traffic jam is compatible with forward-moving cars. This position is called compatibilism.

2.21.8. Notes

These notes are based on the in-class lecture and Chapter 10 of Think Complexity by Allen Downey.