Skip to content

Commit

Permalink
Reimplementation of Continuous Space (projectmesa#2584)
Browse files Browse the repository at this point in the history
  • Loading branch information
quaquel authored Jan 10, 2025
1 parent 1ce3f37 commit 29d0f3b
Show file tree
Hide file tree
Showing 11 changed files with 889 additions and 81 deletions.
9 changes: 7 additions & 2 deletions benchmarks/configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,19 @@
"seeds": 25,
"replications": 3,
"steps": 20,
"parameters": {"population": 200, "width": 100, "height": 100, "vision": 5},
"parameters": {
"population_size": 200,
"width": 100,
"height": 100,
"vision": 5,
},
},
"large": {
"seeds": 10,
"replications": 3,
"steps": 10,
"parameters": {
"population": 400,
"population_size": 400,
"width": 150,
"height": 150,
"vision": 15,
Expand Down
63 changes: 25 additions & 38 deletions mesa/examples/basic/boid_flockers/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

import numpy as np

from mesa import Agent
from mesa.experimental.continuous_space import ContinuousSpaceAgent


class Boid(Agent):
class Boid(ContinuousSpaceAgent):
"""A Boid-style flocker agent.
The agent follows three behaviors to flock:
Expand All @@ -26,10 +26,12 @@ class Boid(Agent):
def __init__(
self,
model,
speed,
direction,
vision,
separation,
space,
position=(0, 0),
speed=1,
direction=(1, 1),
vision=1,
separation=1,
cohere=0.03,
separate=0.015,
match=0.05,
Expand All @@ -46,7 +48,8 @@ def __init__(
separate: Relative importance of avoiding close neighbors (default: 0.015)
match: Relative importance of matching neighbors' directions (default: 0.05)
"""
super().__init__(model)
super().__init__(space, model)
self.position = position
self.speed = speed
self.direction = direction
self.vision = vision
Expand All @@ -58,47 +61,31 @@ def __init__(

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, True)
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 self.neighbors:
new_pos = self.pos + self.direction * self.speed
self.model.space.move_agent(self, new_pos)
if not neighbors:
self.position += self.direction * self.speed
return

# Initialize vectors for the three flocking behaviors
cohere = np.zeros(2) # Cohesion vector
match_vector = np.zeros(2) # Alignment vector
separation_vector = np.zeros(2) # Separation vector
delta = self.space.calculate_difference_vector(self.position, agents=neighbors)

# Calculate the contribution of each neighbor to the three behaviors
for neighbor in self.neighbors:
heading = self.model.space.get_heading(self.pos, neighbor.pos)
distance = self.model.space.get_distance(self.pos, neighbor.pos)

# Cohesion - steer towards the average position of neighbors
cohere += heading

# Separation - avoid getting too close
if distance < self.separation:
separation_vector -= heading

# Alignment - match neighbors' flying direction
match_vector += neighbor.direction

# Weight each behavior by its factor and normalize by number of neighbors
n = len(self.neighbors)
cohere = cohere * self.cohere_factor
separation_vector = separation_vector * self.separate_factor
match_vector = match_vector * self.match_factor
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 + separation_vector + match_vector) / n
self.direction += (cohere_vector + separation_vector + match_vector) / len(
neighbors
)

# Normalize direction vector
self.direction /= np.linalg.norm(self.direction)

# Move boid
new_pos = self.pos + self.direction * self.speed
self.model.space.move_agent(self, new_pos)
self.position += self.direction * self.speed
7 changes: 6 additions & 1 deletion mesa/examples/basic/boid_flockers/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
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

Expand All @@ -17,7 +22,7 @@ def boid_draw(agent):
"value": 42,
"label": "Random Seed",
},
"population": Slider(
"population_size": Slider(
label="Number of boids",
value=100,
min=10,
Expand Down
67 changes: 30 additions & 37 deletions mesa/examples/basic/boid_flockers/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
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.space import ContinuousSpace
from mesa.experimental.continuous_space import ContinuousSpace


class BoidFlockers(Model):
"""Flocker model class. Handles agent creation, placement and scheduling."""

def __init__(
self,
population=100,
population_size=100,
width=100,
height=100,
speed=1,
Expand All @@ -31,7 +37,7 @@ def __init__(
"""Create a new Boids Flocking model.
Args:
population: Number of Boids in the simulation (default: 100)
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)
Expand All @@ -44,48 +50,35 @@ def __init__(
"""
super().__init__(seed=seed)

# Model Parameters
self.population = population
self.vision = vision
self.speed = speed
self.separation = separation

# Set up the space
self.space = ContinuousSpace(width, height, torus=True)

# Store flocking weights
self.factors = {"cohere": cohere, "separate": separate, "match": match}
self.space = ContinuousSpace(
[[0, width], [0, height]],
torus=True,
random=self.random,
n_agents=population_size,
)

# Create and place the Boid agents
self.make_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 make_agents(self):
"""Create and place all Boid agents randomly in the space."""
for _ in range(self.population):
# Random position
x = self.random.random() * self.space.x_max
y = self.random.random() * self.space.y_max
pos = np.array((x, y))

# Random initial direction
direction = np.random.random(2) * 2 - 1 # Random vector between -1 and 1
direction /= np.linalg.norm(direction) # Normalize

# Create and place the Boid
boid = Boid(
model=self,
speed=self.speed,
direction=direction,
vision=self.vision,
separation=self.separation,
**self.factors,
)
self.space.place_agent(boid, pos)

def update_average_heading(self):
"""Calculate the average heading (direction) of all Boids."""
if not self.agents:
Expand Down
4 changes: 2 additions & 2 deletions mesa/experimental/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
- Features graduate from experimental status once their APIs are stabilized
"""

from mesa.experimental import cell_space, devs, mesa_signals
from mesa.experimental import cell_space, continuous_space, devs, mesa_signals

__all__ = ["cell_space", "devs", "mesa_signals"]
__all__ = ["cell_space", "continuous_space", "devs", "mesa_signals"]
8 changes: 8 additions & 0 deletions mesa/experimental/continuous_space/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Continuous space support."""

from mesa.experimental.continuous_space.continuous_space import ContinuousSpace
from mesa.experimental.continuous_space.continuous_space_agents import (
ContinuousSpaceAgent,
)

__all__ = ["ContinuousSpace", "ContinuousSpaceAgent"]
Loading

0 comments on commit 29d0f3b

Please sign in to comment.