This page presents a classic boid flocking model—an archetypal Agent-Based Modeling (ABM) example showing how simple local rules can produce complex collective behavior such as flocking, swarming, or schooling. We cover the theory and origin of Boids, a Mesa 2.4 implementation, the key parameters, and a custom PyQtGraph visualization that colors dynamically detected swarms.
The Boids model was introduced by Craig W. Reynolds (1987) to simulate the lifelike motion of flocks using three simple rules applied locally by each agent (boid):
When combined with a limited perception radius, these rules yield emergent, coordinated group motion. See the Boids (Wikipedia) overview, and Mesa’s modern example Boid Flockers (Mesa 3.x).
| Parameter | Meaning | Typical Effect |
|---|---|---|
population |
Number of boids (agents) in the environment. | Higher values increase density and interaction, often strengthening flock formation. |
width, height |
Continuous space size (environment bounds). | Smaller spaces raise encounter frequency; larger spaces disperse flocks. |
speed |
Preferred speed or max velocity per step. | Higher speed increases dynamism; may require higher vision/separation to remain stable. |
vision |
Perception radius (how far a boid senses neighbors). | Too small → fragmented motion; too large → over-synchronization or “traffic jams”. |
separation |
Repulsion distance / weight to prevent crowding. | Higher values spread agents out; too high can fragment flocks. |
alignment_weight, cohesion_weight, separation_weight |
Weights for the three steering forces (if exposed in your model). | Balances smoothness vs. responsiveness; tuning is key for realistic flocking. |
Below is a minimal sketch of the classic Mesa 2.x server for Boids. It uses
the legacy ModularServer + JS canvas components (pre-Solara).
If you are on Mesa 3.x+, refer to the Solara example linked above.
# server.py (Mesa 2.4 style)
from mesa.visualization.ModularVisualization import ModularServer
from SimpleContinuousModule import SimpleCanvas
from model import BoidFlockers
def boid_draw(agent):
return {"Shape": "circle", "r": 2, "Filled": "true", "Color": "Red"}
boid_canvas = SimpleCanvas(boid_draw, 500, 500)
model_params = dict(population=100, width=100, height=100, speed=5, vision=10, separation=2)
server = ModularServer(BoidFlockers, [boid_canvas], "Boid Flockers", model_params)
# run.py
from server import server
server.launch(open_browser=True)
For Mesa ≥ 3.x, visualization is Solara-based; porting is required (no JS injection). See: Mesa 3.x Boid Flockers.
To avoid browser dependencies and gain speed, we use a PyQtGraph viewer.
It steps the model via a QTimer, draws boids as a scatter, and adds
dynamic swarm coloring based on proximity (clusters are recolored each frame).
# run_pyqt.py (launcher)
import sys
from PyQt6 import QtWidgets
from view_pyqt import BoidQtViewer
from model import BoidFlockers
params = dict(population=100, width=100, height=100, speed=5, vision=10, separation=2)
model = BoidFlockers(**params)
app = QtWidgets.QApplication(sys.argv)
viewer = BoidQtViewer(model=model, fps=30, point_size=8.0, swarm_radius=6.0)
viewer.show()
sys.exit(app.exec())
# view_pyqt.py (core viewer: dynamic clustering + coloring)
from PyQt6 import QtCore, QtWidgets
import pyqtgraph as pg
import numpy as np
import random
class BoidQtViewer(QtWidgets.QMainWindow):
def __init__(self, model, fps=30, point_size=6.0, swarm_radius=5.0, parent=None):
super().__init__(parent)
self.model = model
self.ms_per_frame = int(1000 / max(1, fps))
self.swarm_radius = swarm_radius
self.setWindowTitle("Boid Flockers — PyQtGraph (Dynamic Swarm Coloring)")
self.resize(1000, 800)
self.canvas = pg.GraphicsLayoutWidget(show=True)
self.setCentralWidget(self.canvas)
self.plot = self.canvas.addPlot()
self.plot.setAspectLocked(True)
self.plot.showGrid(x=True, y=True, alpha=0.3)
sxmin = getattr(self.model.space, "x_min", 0.0)
sxmax = getattr(self.model.space, "x_max", 100.0)
symin = getattr(self.model.space, "y_min", 0.0)
symax = getattr(self.model.space, "y_max", 100.0)
self.plot.setXRange(sxmin, sxmax)
self.plot.setYRange(symin, symax)
self.scatter = pg.ScatterPlotItem(pxMode=False, size=point_size, name="Agents")
self.plot.addItem(self.scatter)
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self._step_and_render)
self.timer.start(self.ms_per_frame)
def _detect_swarms(self, positions, radius=5.0):
N = len(positions)
labels = np.full(N, -1, dtype=int)
cluster_id = 0
for i in range(N):
if labels[i] != -1:
continue
labels[i] = cluster_id
stack = [i]
while stack:
j = stack.pop()
dists = np.linalg.norm(positions[j] - positions, axis=1)
neighbors = np.where((labels == -1) & (dists < radius))[0]
labels[neighbors] = cluster_id
stack.extend(neighbors)
cluster_id += 1
return labels
def _make_group_colors(self, n_groups):
random.seed(42)
return [(random.randint(50,255), random.randint(50,255), random.randint(50,255))
for _ in range(n_groups)]
def _step_and_render(self):
self.model.step()
agents = list(getattr(self.model.schedule, "agents", []))
if not agents:
return
positions = np.array([a.pos for a in agents])
xs, ys = positions[:,0], positions[:,1]
labels = self._detect_swarms(positions, radius=self.swarm_radius)
n_clusters = len(np.unique(labels))
colors = self._make_group_colors(n_clusters)
brushes = [pg.mkBrush(colors[l % n_clusters]) for l in labels]
self.scatter.setData(xs, ys, brush=brushes)
Figure: Boids simulated in Mesa with dynamic swarm coloring using a proximity-based clusterer (Population = 300.0, speed = 3.0, vision = 10.0, radius = 6.0, seperation = 1.0). Over time, the boids self-organize into distinct flocks (swarms) that move cohesively across the environment..
The full source code of this demo is available on GitHub: 📥 Download / View Source
Craig W. Reynolds (1987). “Flocks, Herds, and Schools: A Distributed Behavioral Model.” SIGGRAPH ’87, 21(4), 25–34. (Wikipedia summary)
Mesa 3.x Boid Flockers: https://mesa.readthedocs.io/stable/examples/basic/boid_flockers.html
Mesa (Framework): https://mesa.readthedocs.io/en/stable/