diff --git a/examples/boid_flockers/Readme.md b/examples/boid_flockers/Readme.md index cb3292b4..d1f4a987 100644 --- a/examples/boid_flockers/Readme.md +++ b/examples/boid_flockers/Readme.md @@ -1,34 +1,47 @@ -# Flockers +# 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. It also demonstrates how to create custom visualization components. +## Installation + +To install the dependencies use pip and the requirements.txt in this directory. e.g. + +``` + $ pip install -r requirements.txt +``` + ## How to Run -Launch the model: +* To launch the visualization interactively, run ``mesa runserver`` in this directory. e.g. + +``` +$ mesa runserver +``` + +or + +Directly run the file ``run.py`` in the terminal. e.g. + ``` - $ python Flocker_Server.py + $ python run.py ``` -Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. +* Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run. ## Files -* [flockers/model.py](flockers/model.py): Core model file; contains the BoidModel class. -* [flockers/boid.py](flockers/boid.py): The Boid agent class. -* [flockers/SimpleContinuousModule.py](flockers/SimpleContinuousModule.py): Defines ``SimpleCanvas``, the Python side of a custom visualization module for drawing agents with continuous positions. -* [flockers/simple_continuous_canvas.js](flockers/simple_continuous_canvas.js): JavaScript side of the ``SimpleCanvas`` visualization module; takes the output generated by the Python ``SimpleCanvas`` element and draws it in the browser window via HTML5 canvas. -* [flockers/server.py](flockers/server.py): Sets up the visualization; uses the SimpleCanvas element defined above +* [boid_flockers/model.py](boid_flockers/model.py): Core model file; contains the Boid Model and Boid Agent class. +* [boid_flockers/SimpleContinuousModule.py](boid_flockers/SimpleContinuousModule.py): Defines ``SimpleCanvas``, the Python side of a custom visualization module for drawing agents with continuous positions. +* [boid_flockers/simple_continuous_canvas.js](boid_flockers/simple_continuous_canvas.js): JavaScript side of the ``SimpleCanvas`` visualization module; takes the output generated by the Python ``SimpleCanvas`` element and draws it in the browser window via HTML5 canvas. +* [boid_flockers/server.py](boid_flockers/server.py): Sets up the visualization; uses the SimpleCanvas element defined above * [run.py](run.py) Launches the visualization. -* [Flocker Test.ipynb](Flocker Test.ipynb): Tests the model in a Jupyter notebook. +* [Flocker_Test.ipynb](Flocker_Test.ipynb): Tests the model in a Jupyter notebook. ## Further Reading -======= -* Launch the visualization -``` -$ mesa runserver -``` -* Visit your browser: http://127.0.0.1:8521/ -* In your browser hit *run* +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 diff --git a/examples/boid_flockers/app.py b/examples/boid_flockers/app.py index 30b9fa28..5de317fe 100644 --- a/examples/boid_flockers/app.py +++ b/examples/boid_flockers/app.py @@ -16,8 +16,8 @@ def boid_draw(agent): } page = JupyterViz( - BoidFlockers, - model_params, + model_class=BoidFlockers, + model_params=model_params, measures=[], name="BoidFlockers", agent_portrayal=boid_draw, diff --git a/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py b/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py index 3f3da5dd..eabf077c 100644 --- a/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py +++ b/examples/boid_flockers/boid_flockers/SimpleContinuousModule.py @@ -3,11 +3,8 @@ class SimpleCanvas(mesa.visualization.VisualizationElement): local_includes = ["boid_flockers/simple_continuous_canvas.js"] - portrayal_method = None - canvas_height = 500 - canvas_width = 500 - def __init__(self, portrayal_method, canvas_height=500, canvas_width=500): + def __init__(self, portrayal_method=None, canvas_height=500, canvas_width=500): """ Instantiate a new SimpleCanvas """ diff --git a/examples/boid_flockers/boid_flockers/boid.py b/examples/boid_flockers/boid_flockers/boid.py deleted file mode 100644 index f427f9dd..00000000 --- a/examples/boid_flockers/boid_flockers/boid.py +++ /dev/null @@ -1,104 +0,0 @@ -import mesa -import numpy as np - - -class Boid(mesa.Agent): - """ - 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: try to fly in the same direction as the neighbors. - - Boids have a vision that defines the radius in which they look for their - neighbors to flock with. Their speed (a scalar) and velocity (a vector) - define their movement. Separation is their desired minimum distance from - any other Boid. - """ - - def __init__( - self, - unique_id, - model, - pos, - speed, - velocity, - vision, - separation, - cohere=0.025, - separate=0.25, - match=0.04, - ): - """ - Create a new Boid flocker agent. - - Args: - unique_id: Unique agent identifyer. - pos: Starting position - speed: Distance to move per step. - heading: 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: the relative importance of matching neighbors' positions - separate: the relative importance of avoiding close neighbors - match: the relative importance of matching neighbors' headings - """ - super().__init__(unique_id, model) - self.pos = np.array(pos) - self.speed = speed - self.velocity = velocity - self.vision = vision - self.separation = separation - self.cohere_factor = cohere - self.separate_factor = separate - self.match_factor = match - - def cohere(self, neighbors): - """ - Return the vector toward the center of mass of the local neighbors. - """ - cohere = np.zeros(2) - if neighbors: - for neighbor in neighbors: - cohere += self.model.space.get_heading(self.pos, neighbor.pos) - cohere /= len(neighbors) - return cohere - - def separate(self, neighbors): - """ - Return a vector away from any neighbors closer than separation dist. - """ - me = self.pos - them = (n.pos for n in neighbors) - separation_vector = np.zeros(2) - for other in them: - if self.model.space.get_distance(me, other) < self.separation: - separation_vector -= self.model.space.get_heading(me, other) - return separation_vector - - def match_heading(self, neighbors): - """ - Return a vector of the neighbors' average heading. - """ - match_vector = np.zeros(2) - if neighbors: - for neighbor in neighbors: - match_vector += neighbor.velocity - match_vector /= len(neighbors) - return match_vector - - def step(self): - """ - Get the Boid's neighbors, compute the new vector, and move accordingly. - """ - - neighbors = self.model.space.get_neighbors(self.pos, self.vision, False) - self.velocity += ( - self.cohere(neighbors) * self.cohere_factor - + self.separate(neighbors) * self.separate_factor - + self.match_heading(neighbors) * self.match_factor - ) / 2 - self.velocity /= np.linalg.norm(self.velocity) - new_pos = self.pos + self.velocity * self.speed - self.model.space.move_agent(self, new_pos) diff --git a/examples/boid_flockers/boid_flockers/model.py b/examples/boid_flockers/boid_flockers/model.py index 22e9dce6..ff443b52 100644 --- a/examples/boid_flockers/boid_flockers/model.py +++ b/examples/boid_flockers/boid_flockers/model.py @@ -8,7 +8,108 @@ import mesa import numpy as np -from .boid import Boid + +class Boid(mesa.Agent): + """ + 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: try to fly in the same direction as the 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, + unique_id, + model, + pos, + speed, + direction, + vision, + separation, + cohere=0.025, + separate=0.25, + match=0.04, + ): + """ + Create a new Boid flocker agent. + + Args: + unique_id: Unique agent identifyer. + pos: Starting position + 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: the relative importance of matching neighbors' positions + separate: the relative importance of avoiding close neighbors + match: the relative importance of matching neighbors' headings + """ + super().__init__(unique_id, model) + self.pos = np.array(pos) + 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 = None + + def cohere(self): + """ + Return the vector toward the center of mass of the local neighbors. + """ + cohere = np.zeros(2) + if self.neighbors: + for neighbor in self.neighbors: + cohere += self.model.space.get_heading(self.pos, neighbor.pos) + cohere /= len(self.neighbors) + return cohere + + def separate(self): + """ + Return a vector away from any neighbors closer than separation dist. + """ + me = self.pos + them = (n.pos for n in self.neighbors) + separation_vector = np.zeros(2) + for other in them: + if self.model.space.get_distance(me, other) < self.separation: + separation_vector -= self.model.space.get_heading(me, other) + return separation_vector + + def match_heading(self): + """ + Return a vector of the neighbors' average heading. + """ + match_vector = np.zeros(2) + if self.neighbors: + for neighbor in self.neighbors: + match_vector += neighbor.direction + match_vector /= len(self.neighbors) + return match_vector + + def step(self): + """ + Get the Boid's neighbors, compute the new vector, and move accordingly. + """ + + self.neighbors = self.model.space.get_neighbors(self.pos, self.vision, False) + self.direction += ( + self.cohere() * self.cohere_factor + + self.separate() * self.separate_factor + + self.match_heading() * self.match_factor + ) / 2 + self.direction /= np.linalg.norm(self.direction) + new_pos = self.pos + self.direction * self.speed + self.model.space.move_agent(self, new_pos) class BoidFlockers(mesa.Model): @@ -39,7 +140,8 @@ def __init__( separation: What's the minimum distance each Boid will attempt to keep from any other cohere, separate, match: factors for the relative importance of - the three drives.""" + the three drives. + """ super().__init__() self.population = population self.vision = vision @@ -49,7 +151,6 @@ def __init__( self.space = mesa.space.ContinuousSpace(width, height, True) self.factors = {"cohere": cohere, "separate": separate, "match": match} self.make_agents() - self.running = True def make_agents(self): """ @@ -59,15 +160,15 @@ def make_agents(self): x = self.random.random() * self.space.x_max y = self.random.random() * self.space.y_max pos = np.array((x, y)) - velocity = np.random.random(2) * 2 - 1 + direction = np.random.random(2) * 2 - 1 boid = Boid( - i, - self, - pos, - self.speed, - velocity, - self.vision, - self.separation, + unique_id=i, + model=self, + pos=pos, + speed=self.speed, + direction=direction, + vision=self.vision, + separation=self.separation, **self.factors, ) self.space.place_agent(boid, pos) diff --git a/examples/boid_flockers/boid_flockers/server.py b/examples/boid_flockers/boid_flockers/server.py index 4906df69..190c6533 100644 --- a/examples/boid_flockers/boid_flockers/server.py +++ b/examples/boid_flockers/boid_flockers/server.py @@ -5,19 +5,60 @@ def boid_draw(agent): - return {"Shape": "circle", "r": 2, "Filled": "true", "Color": "Red"} + if not agent.neighbors: # Only for the first Frame + neighbors = len(agent.model.space.get_neighbors(agent.pos, agent.vision, False)) + else: + neighbors = len(agent.neighbors) + if neighbors <= 1: + return {"Shape": "circle", "r": 2, "Filled": "true", "Color": "Red"} + elif neighbors >= 2: + return {"Shape": "circle", "r": 2, "Filled": "true", "Color": "Green"} -boid_canvas = SimpleCanvas(boid_draw, 500, 500) + +boid_canvas = SimpleCanvas( + portrayal_method=boid_draw, canvas_height=500, canvas_width=500 +) model_params = { - "population": 100, + "population": mesa.visualization.Slider( + name="Number of boids", + value=100, + min_value=10, + max_value=200, + step=10, + description="Choose how many agents to include in the model", + ), "width": 100, "height": 100, - "speed": 5, - "vision": 10, - "separation": 2, + "speed": mesa.visualization.Slider( + name="Speed of Boids", + value=5, + min_value=1, + max_value=20, + step=1, + description="How fast should the Boids move", + ), + "vision": mesa.visualization.Slider( + name="Vision of Bird (radius)", + value=10, + min_value=1, + max_value=50, + step=1, + description="How far around should each Boid look for its neighbors", + ), + "separation": mesa.visualization.Slider( + name="Minimum Separation", + value=2, + min_value=1, + max_value=20, + step=1, + description="What is the minimum distance each Boid will attempt to keep from any other", + ), } server = mesa.visualization.ModularServer( - BoidFlockers, [boid_canvas], "Boids", model_params + model_cls=BoidFlockers, + visualization_elements=[boid_canvas], + name="Boid Flocking Model", + model_params=model_params, ) diff --git a/examples/boid_flockers/boid_flockers/simple_continuous_canvas.js b/examples/boid_flockers/boid_flockers/simple_continuous_canvas.js index 20c0ded8..812cadce 100644 --- a/examples/boid_flockers/boid_flockers/simple_continuous_canvas.js +++ b/examples/boid_flockers/boid_flockers/simple_continuous_canvas.js @@ -6,7 +6,6 @@ const ContinuousVisualization = function(width, height, context) { if (p.Shape == "circle") this.drawCircle(p.x, p.y, p.r, p.Color, p.Filled); }; - }; this.drawCircle = function(x, y, radius, color, fill) {