3.6 The Boids!#

Estimated time to complete this notebook: 45 minutes.

⚠️ Warning: Advanced Topic! ⚠️

Our earlier discussion of NumPy was very theoretical, but let’s go through a practical example, and see how powerful NumPy can be.

Note this is more a showcase of what you can do with numpy than an exhaustive notebook to work through

3.6.1 Flocking#

The aggregate motion of a flock of birds, a herd of land animals, or a school of fish is a beautiful and familiar part of the natural world… The aggregate motion of the simulated flock is created by a distributed behavioral model much like that at work in a natural flock; the birds choose their own course. Each simulated bird is implemented as an independent actor that navigates according to its local perception of the dynamic environment, the laws of simulated physics that rule its motion, and a set of behaviors programmed into it… The aggregate motion of the simulated flock is the result of the dense interaction of the relatively simple behaviors of the individual simulated birds.

– Craig W. Reynolds, “Flocks, Herds, and Schools: A Distributed Behavioral Model”, Computer Graphics 21 4 1987, pp 25-34

We will demonstrate an algorithm to simulate flocking behaviour in numpy. The simulation consists of a set of individual bird-like objects that we will call ‘boids’ following the nomenclature of the original paper for more details.

  • Collision Avoidance: avoid collisions with nearby flockmates

  • Velocity Matching: attempt to match velocity with nearby flockmates

  • Flock Centering: attempt to stay close to nearby flockmates

3.6.2 Setting up the Boids#

Our boids will each have an x velocity and a y velocity, and an x position and a y position.

We’ll build this up in NumPy notation, and eventually, have an animated simulation of our flying boids.

import numpy as np

Let’s start with simple flying in a straight line.

Our positions, for each of our N boids, will be an array, shape \(2 \times N\), with the x positions in the first row, and y positions in the second row.

boid_count = 10

We’ll want to be able to seed our Boids in a random position.

We’d better define the edges of our simulation area:

limits = np.array([2000, 2000])
positions = np.random.rand(2, boid_count) * limits[:, np.newaxis]
array([[1469.30273805, 1974.20093305,  466.31181046,  974.18910549,
          88.93015656,  437.19012488, 1096.64793516, 1374.47388504,
         976.09255964,  306.61982305],
       [1574.08191225,  446.65970454, 1281.42970812, 1085.79252945,
        1894.15030894,  951.18768334,  108.86417594, 1020.29384278,
         870.54477731,  116.85914325]])
(2, 10)

We used broadcasting with np.newaxis to apply our upper limit to each boid. rand gives us a random number between 0 and 1. We multiply by our limits to get a number up to that limit.

limits[:, np.newaxis]
limits[:, np.newaxis].shape
(2, 1)
np.random.rand(2, boid_count).shape
(2, 10)

So we multiply a \(2\times1\) array by a \(2 \times 10\) array – and get a \(2\times 10\) array.

Let’s put that in a function:

def new_flock(count, lower_limits, upper_limits):
    width = upper_limits - lower_limits
    return lower_limits[:, np.newaxis] + np.random.rand(2, count) * width[:, np.newaxis]

For example, let’s assume that we want our initial positions to vary between 100 and 200 in the x axis, and 900 and 1100 in the y axis. We can generate random positions within these constraints with:

positions = new_flock(boid_count, np.array([100, 900]), np.array([200, 1100]))

But each boid will also need a starting velocity. Let’s make these random too:

We can reuse the new_flock function defined above, since we’re again essentially just generating random numbers from given limits. This saves us some code, but keep in mind that using a function for something other than what its name indicates can become confusing!

Here, we will let the initial x velocities range over \([0, 10]\) and the y velocities over \([-20, 20]\).

velocities = new_flock(boid_count, np.array([0, -20]), np.array([10, 20]))
array([[  6.11741937,   8.46349346,   4.39221673,   7.99412569,
          0.66413959,   5.85312869,   5.88237949,   5.51073276,
          9.42240838,   0.030474  ],
       [ -2.39277863,   0.92740242,  -1.98751009,  12.17621129,
          6.0136092 ,  -3.93489978,   8.90194992,  -4.95790977,
         -8.86321783, -19.56530663]])

3.6.3 Flying in a Straight Line#

Now we see the real amazingness of NumPy: if we want to move our whole flock according to

\(\delta_x = \delta_t \cdot \frac{dv}{dt}\)

we just do:

positions += velocities

3.6.4 Matplotlib Animations#

So now we can animate our Boids using the matplotlib animation tools All we have to do is import the relevant libraries:

from matplotlib import animation
from matplotlib import pyplot as plt

Then, we make a static plot, showing our first frame:

# create a simple plot
# initial x position in [100, 200], initial y position in [900, 1100]
# initial x velocity in [0, 10], initial y velocity in [-20, 20]
positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))
velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))

figure = plt.figure()
axes = plt.axes(xlim=(0, limits[0]), ylim=(0, limits[1]))
scatter = axes.scatter(
    positions[0, :], positions[1, :], marker="o", edgecolor="k", lw=0.5
<matplotlib.collections.PathCollection at 0x7fcb4c1fbc70>

Then, we define a function which updates the figure for each timestep

def update_boids(positions, velocities):
    positions += velocities

def animate(frame):
    update_boids(positions, velocities)

Call FuncAnimation, and specify how many frames we want:

anim = animation.FuncAnimation(figure, animate, frames=50, interval=50)

Save out the figure:

positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))
velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))

And download the saved animation.

You can even view the results directly in the notebook.

from IPython.display import HTML

positions = new_flock(100, np.array([100, 900]), np.array([200, 1100]))
velocities = new_flock(100, np.array([0, -20]), np.array([10, 20]))