Boids Flockers#
Summary#
An implementation of Craig Reynolds’s Boids flocker model. Agents (simulated birds) try to fly towards the average position of their neighbors and in the same direction as them, while maintaining a minimum distance. This produces flocking behavior.
This model tests Mesa’s continuous space feature, and uses numpy arrays to represent vectors.
How to Run#
To launch the visualization interactively, run
solara run app.py
in this directory.It will automatically open a browser page.
Files#
Further Reading#
The following link can be visited for more information on the boid flockers model: https://cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/modeling-natural-systems/boids.html
Agents#
"""A Boid (bird-oid) agent for implementing Craig Reynolds's Boids flocking model.
This implementation uses numpy arrays to represent vectors for efficient computation
of flocking behavior.
"""
import numpy as np
from mesa.experimental.continuous_space import ContinuousSpaceAgent
class Boid(ContinuousSpaceAgent):
"""A Boid-style flocker agent.
The agent follows three behaviors to flock:
- Cohesion: steering towards neighboring agents
- Separation: avoiding getting too close to any other agent
- Alignment: trying to fly in the same direction as neighbors
Boids have a vision that defines the radius in which they look for their
neighbors to flock with. Their speed (a scalar) and direction (a vector)
define their movement. Separation is their desired minimum distance from
any other Boid.
"""
def __init__(
self,
model,
space,
position=(0, 0),
speed=1,
direction=(1, 1),
vision=1,
separation=1,
cohere=0.03,
separate=0.015,
match=0.05,
):
"""Create a new Boid flocker agent.
Args:
model: Model instance the agent belongs to
speed: Distance to move per step
direction: numpy vector for the Boid's direction of movement
vision: Radius to look around for nearby Boids
separation: Minimum distance to maintain from other Boids
cohere: Relative importance of matching neighbors' positions (default: 0.03)
separate: Relative importance of avoiding close neighbors (default: 0.015)
match: Relative importance of matching neighbors' directions (default: 0.05)
"""
super().__init__(space, model)
self.position = position
self.speed = speed
self.direction = direction
self.vision = vision
self.separation = separation
self.cohere_factor = cohere
self.separate_factor = separate
self.match_factor = match
self.neighbors = []
def step(self):
"""Get the Boid's neighbors, compute the new vector, and move accordingly."""
neighbors, distances = self.get_neighbors_in_radius(radius=self.vision)
self.neighbors = [n for n in neighbors if n is not self]
# If no neighbors, maintain current direction
if not neighbors:
self.position += self.direction * self.speed
return
delta = self.space.calculate_difference_vector(self.position, agents=neighbors)
cohere_vector = delta.sum(axis=0) * self.cohere_factor
separation_vector = (
-1 * delta[distances < self.separation].sum(axis=0) * self.separate_factor
)
match_vector = (
np.asarray([n.direction for n in neighbors]).sum(axis=0) * self.match_factor
)
# Update direction based on the three behaviors
self.direction += (cohere_vector + separation_vector + match_vector) / len(
neighbors
)
# Normalize direction vector
self.direction /= np.linalg.norm(self.direction)
# Move boid
self.position += self.direction * self.speed
Model#
"""
Boids Flocking Model
===================
A Mesa implementation of Craig Reynolds's Boids flocker model.
Uses numpy arrays to represent vectors.
"""
import os
import sys
sys.path.insert(0, os.path.abspath("../../../.."))
import numpy as np
from mesa import Model
from mesa.examples.basic.boid_flockers.agents import Boid
from mesa.experimental.continuous_space import ContinuousSpace
class BoidFlockers(Model):
"""Flocker model class. Handles agent creation, placement and scheduling."""
def __init__(
self,
population_size=100,
width=100,
height=100,
speed=1,
vision=10,
separation=2,
cohere=0.03,
separate=0.015,
match=0.05,
seed=None,
):
"""Create a new Boids Flocking model.
Args:
population_size: Number of Boids in the simulation (default: 100)
width: Width of the space (default: 100)
height: Height of the space (default: 100)
speed: How fast the Boids move (default: 1)
vision: How far each Boid can see (default: 10)
separation: Minimum distance between Boids (default: 2)
cohere: Weight of cohesion behavior (default: 0.03)
separate: Weight of separation behavior (default: 0.015)
match: Weight of alignment behavior (default: 0.05)
seed: Random seed for reproducibility (default: None)
"""
super().__init__(seed=seed)
# Set up the space
self.space = ContinuousSpace(
[[0, width], [0, height]],
torus=True,
random=self.random,
n_agents=population_size,
)
# Create and place the Boid agents
positions = self.rng.random(size=(population_size, 2)) * self.space.size
directions = self.rng.uniform(-1, 1, size=(population_size, 2))
Boid.create_agents(
self,
population_size,
self.space,
position=positions,
direction=directions,
cohere=cohere,
separate=separate,
match=match,
speed=speed,
vision=vision,
separation=separation,
)
# For tracking statistics
self.average_heading = None
self.update_average_heading()
def update_average_heading(self):
"""Calculate the average heading (direction) of all Boids."""
if not self.agents:
self.average_heading = 0
return
headings = np.array([agent.direction for agent in self.agents])
mean_heading = np.mean(headings, axis=0)
self.average_heading = np.arctan2(mean_heading[1], mean_heading[0])
def step(self):
"""Run one step of the model.
All agents are activated in random order using the AgentSet shuffle_do method.
"""
self.agents.shuffle_do("step")
self.update_average_heading()
App#
import os
import sys
sys.path.insert(0, os.path.abspath("../../../.."))
from mesa.examples.basic.boid_flockers.model import BoidFlockers
from mesa.visualization import Slider, SolaraViz, make_space_component
def boid_draw(agent):
neighbors = len(agent.neighbors)
if neighbors <= 1:
return {"color": "red", "size": 20}
elif neighbors >= 2:
return {"color": "green", "size": 20}
model_params = {
"seed": {
"type": "InputText",
"value": 42,
"label": "Random Seed",
},
"population_size": Slider(
label="Number of boids",
value=100,
min=10,
max=200,
step=10,
),
"width": 100,
"height": 100,
"speed": Slider(
label="Speed of Boids",
value=5,
min=1,
max=20,
step=1,
),
"vision": Slider(
label="Vision of Bird (radius)",
value=10,
min=1,
max=50,
step=1,
),
"separation": Slider(
label="Minimum Separation",
value=2,
min=1,
max=20,
step=1,
),
}
model = BoidFlockers()
page = SolaraViz(
model,
components=[make_space_component(agent_portrayal=boid_draw, backend="matplotlib")],
model_params=model_params,
name="Boid Flocking Model",
)
page # noqa