|
| 1 | + |
| 2 | +## Description |
| 3 | +# Boids Flockers |
| 4 | + |
| 5 | +## Summary |
| 6 | + |
| 7 | +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. |
| 8 | + |
| 9 | +This model tests Mesa's continuous space feature, and uses numpy arrays to represent vectors. It also demonstrates how to create custom visualization components. |
| 10 | + |
| 11 | +## Installation |
| 12 | + |
| 13 | +To install the dependencies use pip and the requirements.txt in this directory. e.g. |
| 14 | + |
| 15 | +``` |
| 16 | + $ pip install -r requirements.txt |
| 17 | +``` |
| 18 | + |
| 19 | +## How to Run |
| 20 | + |
| 21 | +* To launch the visualization interactively, run ``mesa runserver`` in this directory. e.g. |
| 22 | + |
| 23 | +``` |
| 24 | +$ mesa runserver |
| 25 | +``` |
| 26 | + |
| 27 | +or |
| 28 | + |
| 29 | +Directly run the file ``run.py`` in the terminal. e.g. |
| 30 | + |
| 31 | +``` |
| 32 | + $ python run.py |
| 33 | +``` |
| 34 | + |
| 35 | +* Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. |
| 36 | + |
| 37 | +## Files |
| 38 | + |
| 39 | +* [model.py](model.py): Core model file; contains the Boid Model and Boid Agent class. |
| 40 | +* [app.py](app.py): Visualization code. |
| 41 | + |
| 42 | +## Further Reading |
| 43 | + |
| 44 | +The following link can be visited for more information on the boid flockers model: |
| 45 | +https://cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/modeling-natural-systems/boids.html |
| 46 | + |
| 47 | + |
| 48 | +## Agents |
| 49 | + |
| 50 | +```python |
| 51 | +import numpy as np |
| 52 | + |
| 53 | +from mesa import Agent |
| 54 | + |
| 55 | + |
| 56 | +class Boid(Agent): |
| 57 | + """A Boid-style flocker agent. |
| 58 | +
|
| 59 | + The agent follows three behaviors to flock: |
| 60 | + - Cohesion: steering towards neighboring agents. |
| 61 | + - Separation: avoiding getting too close to any other agent. |
| 62 | + - Alignment: try to fly in the same direction as the neighbors. |
| 63 | +
|
| 64 | + Boids have a vision that defines the radius in which they look for their |
| 65 | + neighbors to flock with. Their speed (a scalar) and direction (a vector) |
| 66 | + define their movement. Separation is their desired minimum distance from |
| 67 | + any other Boid. |
| 68 | + """ |
| 69 | + |
| 70 | + def __init__( |
| 71 | + self, |
| 72 | + model, |
| 73 | + speed, |
| 74 | + direction, |
| 75 | + vision, |
| 76 | + separation, |
| 77 | + cohere=0.03, |
| 78 | + separate=0.015, |
| 79 | + match=0.05, |
| 80 | + ): |
| 81 | + """Create a new Boid flocker agent. |
| 82 | +
|
| 83 | + Args: |
| 84 | + speed: Distance to move per step. |
| 85 | + direction: numpy vector for the Boid's direction of movement. |
| 86 | + vision: Radius to look around for nearby Boids. |
| 87 | + separation: Minimum distance to maintain from other Boids. |
| 88 | + cohere: the relative importance of matching neighbors' positions |
| 89 | + separate: the relative importance of avoiding close neighbors |
| 90 | + match: the relative importance of matching neighbors' headings |
| 91 | + """ |
| 92 | + super().__init__(model) |
| 93 | + self.speed = speed |
| 94 | + self.direction = direction |
| 95 | + self.vision = vision |
| 96 | + self.separation = separation |
| 97 | + self.cohere_factor = cohere |
| 98 | + self.separate_factor = separate |
| 99 | + self.match_factor = match |
| 100 | + self.neighbors = None |
| 101 | + |
| 102 | + def step(self): |
| 103 | + """Get the Boid's neighbors, compute the new vector, and move accordingly.""" |
| 104 | + self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False) |
| 105 | + n = 0 |
| 106 | + match_vector, separation_vector, cohere = np.zeros((3, 2)) |
| 107 | + for neighbor in self.neighbors: |
| 108 | + n += 1 |
| 109 | + heading = self.model.space.get_heading(self.pos, neighbor.pos) |
| 110 | + cohere += heading |
| 111 | + if self.model.space.get_distance(self.pos, neighbor.pos) < self.separation: |
| 112 | + separation_vector -= heading |
| 113 | + match_vector += neighbor.direction |
| 114 | + n = max(n, 1) |
| 115 | + cohere = cohere * self.cohere_factor |
| 116 | + separation_vector = separation_vector * self.separate_factor |
| 117 | + match_vector = match_vector * self.match_factor |
| 118 | + self.direction += (cohere + separation_vector + match_vector) / n |
| 119 | + self.direction /= np.linalg.norm(self.direction) |
| 120 | + new_pos = self.pos + self.direction * self.speed |
| 121 | + self.model.space.move_agent(self, new_pos) |
| 122 | + |
| 123 | +``` |
| 124 | + |
| 125 | + |
| 126 | +## Model |
| 127 | + |
| 128 | +```python |
| 129 | +"""Flockers. |
| 130 | +============================================================= |
| 131 | +A Mesa implementation of Craig Reynolds's Boids flocker model. |
| 132 | +Uses numpy arrays to represent vectors. |
| 133 | +""" |
| 134 | + |
| 135 | +import numpy as np |
| 136 | + |
| 137 | +import mesa |
| 138 | + |
| 139 | +from .agents import Boid |
| 140 | + |
| 141 | + |
| 142 | +class BoidFlockers(mesa.Model): |
| 143 | + """Flocker model class. Handles agent creation, placement and scheduling.""" |
| 144 | + |
| 145 | + def __init__( |
| 146 | + self, |
| 147 | + seed=None, |
| 148 | + population=100, |
| 149 | + width=100, |
| 150 | + height=100, |
| 151 | + vision=10, |
| 152 | + speed=1, |
| 153 | + separation=1, |
| 154 | + cohere=0.03, |
| 155 | + separate=0.015, |
| 156 | + match=0.05, |
| 157 | + ): |
| 158 | + """Create a new Flockers model. |
| 159 | +
|
| 160 | + Args: |
| 161 | + population: Number of Boids |
| 162 | + width, height: Size of the space. |
| 163 | + speed: How fast should the Boids move. |
| 164 | + vision: How far around should each Boid look for its neighbors |
| 165 | + separation: What's the minimum distance each Boid will attempt to |
| 166 | + keep from any other |
| 167 | + cohere, separate, match: factors for the relative importance of |
| 168 | + the three drives. |
| 169 | + """ |
| 170 | + super().__init__(seed=seed) |
| 171 | + self.population = population |
| 172 | + self.vision = vision |
| 173 | + self.speed = speed |
| 174 | + self.separation = separation |
| 175 | + |
| 176 | + self.space = mesa.space.ContinuousSpace(width, height, True) |
| 177 | + self.factors = {"cohere": cohere, "separate": separate, "match": match} |
| 178 | + self.make_agents() |
| 179 | + |
| 180 | + def make_agents(self): |
| 181 | + """Create self.population agents, with random positions and starting headings.""" |
| 182 | + for _ in range(self.population): |
| 183 | + x = self.random.random() * self.space.x_max |
| 184 | + y = self.random.random() * self.space.y_max |
| 185 | + pos = np.array((x, y)) |
| 186 | + direction = np.random.random(2) * 2 - 1 |
| 187 | + boid = Boid( |
| 188 | + model=self, |
| 189 | + speed=self.speed, |
| 190 | + direction=direction, |
| 191 | + vision=self.vision, |
| 192 | + separation=self.separation, |
| 193 | + **self.factors, |
| 194 | + ) |
| 195 | + self.space.place_agent(boid, pos) |
| 196 | + |
| 197 | + def step(self): |
| 198 | + self.agents.shuffle_do("step") |
| 199 | + |
| 200 | +``` |
| 201 | + |
| 202 | + |
| 203 | +## App |
| 204 | + |
| 205 | +```python |
| 206 | +from mesa.visualization import Slider, SolaraViz, make_space_matplotlib |
| 207 | + |
| 208 | +from .model import BoidFlockers |
| 209 | + |
| 210 | + |
| 211 | +def boid_draw(agent): |
| 212 | + if not agent.neighbors: # Only for the first Frame |
| 213 | + neighbors = len(agent.model.space.get_neighbors(agent.pos, agent.vision, False)) |
| 214 | + else: |
| 215 | + neighbors = len(agent.neighbors) |
| 216 | + |
| 217 | + if neighbors <= 1: |
| 218 | + return {"color": "red", "size": 20} |
| 219 | + elif neighbors >= 2: |
| 220 | + return {"color": "green", "size": 20} |
| 221 | + |
| 222 | + |
| 223 | +model_params = { |
| 224 | + "population": Slider( |
| 225 | + label="Number of boids", |
| 226 | + value=100, |
| 227 | + min=10, |
| 228 | + max=200, |
| 229 | + step=10, |
| 230 | + ), |
| 231 | + "width": 100, |
| 232 | + "height": 100, |
| 233 | + "speed": Slider( |
| 234 | + label="Speed of Boids", |
| 235 | + value=5, |
| 236 | + min=1, |
| 237 | + max=20, |
| 238 | + step=1, |
| 239 | + ), |
| 240 | + "vision": Slider( |
| 241 | + label="Vision of Bird (radius)", |
| 242 | + value=10, |
| 243 | + min=1, |
| 244 | + max=50, |
| 245 | + step=1, |
| 246 | + ), |
| 247 | + "separation": Slider( |
| 248 | + label="Minimum Separation", |
| 249 | + value=2, |
| 250 | + min=1, |
| 251 | + max=20, |
| 252 | + step=1, |
| 253 | + ), |
| 254 | +} |
| 255 | + |
| 256 | +model = BoidFlockers() |
| 257 | + |
| 258 | +page = SolaraViz( |
| 259 | + model, |
| 260 | + [make_space_matplotlib(agent_portrayal=boid_draw)], |
| 261 | + model_params=model_params, |
| 262 | + name="Boid Flocking Model", |
| 263 | +) |
| 264 | +page # noqa |
| 265 | + |
| 266 | +``` |
0 commit comments