Skip to content

Commit ac183a8

Browse files
Corvincequaquel
andauthored
Add cell-centric discrete spaces (experimental) (#1994)
## Summary This PR introduces an alteranative implementation for discrete spaces. This implementation centers on the explicit inclusion of a Cell class. Agents can occupy cells. Cells have connections, specifying their neighbors. The resulting classes offer a cell centric API where agents interact with a cell, and query the cell for its neighbors. To capture a collection of cells, and their content (_i.e._, Agents), this PR adds a new CellCollection class. This is an immutable collection of cells with convenient attribute accessors to the cells, or their agents. This PR also includes a CellAgent class which extends the default Agent class by adding a `move_to` method that works in conjunction with the new discrete spaces. From a performance point of view, the current code is a bit slower in building the grid and cell data structure, but in most use cases this increase in time for model initialization will be more than offset by the faster retrieval of neighboring cells and the agents that occupy them. ## Motive The PR emerged out of various experiments aimed at improving the performance of the current discrete space code. Moreover, it turned out that a cell centric API resolved various open issues (_e.g._, #1900, #1903, #1953). ## Implementation The key idea is to have Cells with connections, and using this to generate neighborhoods for a given radius. So all discrete space classes are in essence a [linked data structure](https://en.wikipedia.org/wiki/Linked_data_structure). The cell centric API idea is used to implement 4 key discrete space classes: OrthogonalMooreGrid, OrthogonalVonNeumannGrid (alternative for SingleGrid and MultiGrid, and moore and von Neumann neighborhood) , HexGrid (alternative for SingleHexGrid and MultiHexGrid), and Network (alternative for NetworkGrid). Cells have a capacity, so there is no longer a need for seperating Single and Multi grids. Moore and von Neumann reflect different neighborhood connections and so are now implemented as seperate classes. --------- Co-authored-by: Jan Kwakkel <[email protected]>
1 parent 5f155c5 commit ac183a8

File tree

11 files changed

+1136
-56
lines changed

11 files changed

+1136
-56
lines changed

benchmarks/Schelling/schelling.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import mesa
1+
from mesa import Model
2+
from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid
3+
from mesa.time import RandomActivation
24

35

4-
class SchellingAgent(mesa.Agent):
6+
class SchellingAgent(CellAgent):
57
"""
68
Schelling segregation agent
79
"""
810

9-
def __init__(self, unique_id, model, agent_type):
11+
def __init__(self, unique_id, model, agent_type, radius, homophily):
1012
"""
1113
Create a new Schelling agent.
1214
Args:
@@ -16,31 +18,32 @@ def __init__(self, unique_id, model, agent_type):
1618
"""
1719
super().__init__(unique_id, model)
1820
self.type = agent_type
21+
self.radius = radius
22+
self.homophily = homophily
1923

2024
def step(self):
2125
similar = 0
22-
for neighbor in self.model.grid.iter_neighbors(
23-
self.pos, moore=True, radius=self.model.radius
24-
):
26+
neighborhood = self.cell.neighborhood(radius=self.radius)
27+
for neighbor in neighborhood.agents:
2528
if neighbor.type == self.type:
2629
similar += 1
2730

2831
# If unhappy, move:
29-
if similar < self.model.homophily:
30-
self.model.grid.move_to_empty(self)
32+
if similar < self.homophily:
33+
self.move_to(self.model.grid.select_random_empty_cell())
3134
else:
3235
self.model.happy += 1
3336

3437

35-
class Schelling(mesa.Model):
38+
class Schelling(Model):
3639
"""
3740
Model class for the Schelling segregation model.
3841
"""
3942

4043
def __init__(
4144
self,
42-
width=40,
4345
height=40,
46+
width=40,
4447
homophily=3,
4548
radius=1,
4649
density=0.8,
@@ -51,35 +54,40 @@ def __init__(
5154
Create a new Schelling model.
5255
5356
Args:
54-
width, height: Size of the space.
57+
height, width: Size of the space.
5558
density: Initial Chance for a cell to populated
5659
minority_pc: Chances for an agent to be in minority class
5760
homophily: Minimum number of agents of same class needed to be happy
5861
radius: Search radius for checking similarity
5962
seed: Seed for Reproducibility
6063
"""
6164
super().__init__(seed=seed)
62-
self.width = width
6365
self.height = height
66+
self.width = width
6467
self.density = density
6568
self.minority_pc = minority_pc
66-
self.homophily = homophily
67-
self.radius = radius
6869

69-
self.schedule = mesa.time.RandomActivation(self)
70-
self.grid = mesa.space.SingleGrid(width, height, torus=True)
70+
self.schedule = RandomActivation(self)
71+
self.grid = OrthogonalMooreGrid(
72+
[height, width],
73+
torus=True,
74+
capacity=1,
75+
random=self.random,
76+
)
7177

7278
self.happy = 0
7379

7480
# Set up agents
7581
# We use a grid iterator that returns
7682
# the coordinates of a cell as well as
7783
# its contents. (coord_iter)
78-
for _, pos in self.grid.coord_iter():
84+
for cell in self.grid:
7985
if self.random.random() < self.density:
8086
agent_type = 1 if self.random.random() < self.minority_pc else 0
81-
agent = SchellingAgent(self.next_id(), self, agent_type)
82-
self.grid.place_agent(agent, pos)
87+
agent = SchellingAgent(
88+
self.next_id(), self, agent_type, radius, homophily
89+
)
90+
agent.move_to(cell)
8391
self.schedule.add(agent)
8492

8593
def step(self):
@@ -93,13 +101,12 @@ def step(self):
93101
if __name__ == "__main__":
94102
import time
95103

96-
# model = Schelling(seed=15, width=40, height=40, homophily=3, radius=1, density=0.625)
104+
# model = Schelling(seed=15, height=40, width=40, homophily=3, radius=1, density=0.625)
97105
model = Schelling(
98-
seed=15, width=100, height=100, homophily=8, radius=2, density=0.8
106+
seed=15, height=100, width=100, homophily=8, radius=2, density=0.8
99107
)
100108

101109
start_time = time.perf_counter()
102110
for _ in range(100):
103111
model.step()
104-
105112
print(time.perf_counter() - start_time)

benchmarks/WolfSheep/wolf_sheep.py

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,41 +9,40 @@
99
Northwestern University, Evanston, IL.
1010
"""
1111

12-
import mesa
12+
import math
1313

14+
from mesa import Model
15+
from mesa.experimental.cell_space import CellAgent, OrthogonalVonNeumannGrid
16+
from mesa.time import RandomActivationByType
1417

15-
class Animal(mesa.Agent):
16-
def __init__(self, unique_id, model, moore, energy, p_reproduce, energy_from_food):
18+
19+
class Animal(CellAgent):
20+
def __init__(self, unique_id, model, energy, p_reproduce, energy_from_food):
1721
super().__init__(unique_id, model)
1822
self.energy = energy
1923
self.p_reproduce = p_reproduce
2024
self.energy_from_food = energy_from_food
21-
self.moore = moore
2225

2326
def random_move(self):
24-
next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True)
25-
next_move = self.random.choice(next_moves)
26-
# Now move:
27-
self.model.grid.move_agent(self, next_move)
27+
self.move_to(self.cell.neighborhood().select_random_cell())
2828

2929
def spawn_offspring(self):
3030
self.energy /= 2
3131
offspring = self.__class__(
3232
self.model.next_id(),
3333
self.model,
34-
self.moore,
3534
self.energy,
3635
self.p_reproduce,
3736
self.energy_from_food,
3837
)
39-
self.model.grid.place_agent(offspring, self.pos)
38+
offspring.move_to(self.cell)
4039
self.model.schedule.add(offspring)
4140

4241
def feed(self):
4342
...
4443

4544
def die(self):
46-
self.model.grid.remove_agent(self)
45+
self.cell.remove_agent(self)
4746
self.remove()
4847

4948
def step(self):
@@ -67,8 +66,9 @@ class Sheep(Animal):
6766

6867
def feed(self):
6968
# If there is grass available, eat it
70-
agents = self.model.grid.get_cell_list_contents(self.pos)
71-
grass_patch = next(obj for obj in agents if isinstance(obj, GrassPatch))
69+
grass_patch = next(
70+
obj for obj in self.cell.agents if isinstance(obj, GrassPatch)
71+
)
7272
if grass_patch.fully_grown:
7373
self.energy += self.energy_from_food
7474
grass_patch.fully_grown = False
@@ -80,8 +80,7 @@ class Wolf(Animal):
8080
"""
8181

8282
def feed(self):
83-
agents = self.model.grid.get_cell_list_contents(self.pos)
84-
sheep = [obj for obj in agents if isinstance(obj, Sheep)]
83+
sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)]
8584
if len(sheep) > 0:
8685
sheep_to_eat = self.random.choice(sheep)
8786
self.energy += self.energy_from_food
@@ -90,7 +89,7 @@ def feed(self):
9089
sheep_to_eat.die()
9190

9291

93-
class GrassPatch(mesa.Agent):
92+
class GrassPatch(CellAgent):
9493
"""
9594
A patch of grass that grows at a fixed rate and it is eaten by sheep
9695
"""
@@ -100,7 +99,7 @@ def __init__(self, unique_id, model, fully_grown, countdown):
10099
Creates a new patch of grass
101100
102101
Args:
103-
grown: (boolean) Whether the patch of grass is fully grown or not
102+
fully_grown: (boolean) Whether the patch of grass is fully grown or not
104103
countdown: Time for the patch of grass to be fully grown again
105104
"""
106105
super().__init__(unique_id, model)
@@ -117,7 +116,7 @@ def step(self):
117116
self.countdown -= 1
118117

119118

120-
class WolfSheep(mesa.Model):
119+
class WolfSheep(Model):
121120
"""
122121
Wolf-Sheep Predation Model
123122
@@ -126,7 +125,6 @@ class WolfSheep(mesa.Model):
126125

127126
def __init__(
128127
self,
129-
seed,
130128
height,
131129
width,
132130
initial_sheep,
@@ -136,7 +134,7 @@ def __init__(
136134
grass_regrowth_time,
137135
wolf_gain_from_food=13,
138136
sheep_gain_from_food=5,
139-
moore=False,
137+
seed=None,
140138
):
141139
"""
142140
Create a new Wolf-Sheep model with the given parameters.
@@ -152,6 +150,7 @@ def __init__(
152150
once it is eaten
153151
sheep_gain_from_food: Energy sheep gain from grass, if enabled.
154152
moore:
153+
seed
155154
"""
156155
super().__init__(seed=seed)
157156
# Set parameters
@@ -161,24 +160,25 @@ def __init__(
161160
self.initial_wolves = initial_wolves
162161
self.grass_regrowth_time = grass_regrowth_time
163162

164-
self.schedule = mesa.time.RandomActivationByType(self)
165-
self.grid = mesa.space.MultiGrid(self.height, self.width, torus=False)
163+
self.schedule = RandomActivationByType(self)
164+
self.grid = OrthogonalVonNeumannGrid(
165+
[self.height, self.width],
166+
torus=False,
167+
capacity=math.inf,
168+
random=self.random,
169+
)
166170

171+
# Create sheep:
167172
for _ in range(self.initial_sheep):
168173
pos = (
169174
self.random.randrange(self.width),
170175
self.random.randrange(self.height),
171176
)
172177
energy = self.random.randrange(2 * sheep_gain_from_food)
173178
sheep = Sheep(
174-
self.next_id(),
175-
self,
176-
moore,
177-
energy,
178-
sheep_reproduce,
179-
sheep_gain_from_food,
179+
self.next_id(), self, energy, sheep_reproduce, sheep_gain_from_food
180180
)
181-
self.grid.place_agent(sheep, pos)
181+
sheep.move_to(self.grid[pos])
182182
self.schedule.add(sheep)
183183

184184
# Create wolves
@@ -189,21 +189,21 @@ def __init__(
189189
)
190190
energy = self.random.randrange(2 * wolf_gain_from_food)
191191
wolf = Wolf(
192-
self.next_id(), self, moore, energy, wolf_reproduce, wolf_gain_from_food
192+
self.next_id(), self, energy, wolf_reproduce, wolf_gain_from_food
193193
)
194-
self.grid.place_agent(wolf, pos)
194+
wolf.move_to(self.grid[pos])
195195
self.schedule.add(wolf)
196196

197197
# Create grass patches
198198
possibly_fully_grown = [True, False]
199-
for _agent, pos in self.grid.coord_iter():
199+
for cell in self.grid:
200200
fully_grown = self.random.choice(possibly_fully_grown)
201201
if fully_grown:
202202
countdown = self.grass_regrowth_time
203203
else:
204204
countdown = self.random.randrange(self.grass_regrowth_time)
205205
patch = GrassPatch(self.next_id(), self, fully_grown, countdown)
206-
self.grid.place_agent(patch, pos)
206+
patch.move_to(cell)
207207
self.schedule.add(patch)
208208

209209
def step(self):
@@ -213,7 +213,7 @@ def step(self):
213213
if __name__ == "__main__":
214214
import time
215215

216-
model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20)
216+
model = WolfSheep(25, 25, 60, 40, 0.2, 0.1, 20, seed=15)
217217

218218
start_time = time.perf_counter()
219219
for _ in range(100):

mesa/experimental/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
from .jupyter_viz import JupyterViz, make_text, Slider # noqa
2+
from mesa.experimental import cell_space
3+
4+
5+
__all__ = ["JupyterViz", "make_text", "Slider", "cell_space"]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from mesa.experimental.cell_space.cell import Cell
2+
from mesa.experimental.cell_space.cell_agent import CellAgent
3+
from mesa.experimental.cell_space.cell_collection import CellCollection
4+
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
5+
from mesa.experimental.cell_space.grid import (
6+
Grid,
7+
HexGrid,
8+
OrthogonalMooreGrid,
9+
OrthogonalVonNeumannGrid,
10+
)
11+
from mesa.experimental.cell_space.network import Network
12+
13+
__all__ = [
14+
"CellCollection",
15+
"Cell",
16+
"CellAgent",
17+
"DiscreteSpace",
18+
"Grid",
19+
"HexGrid",
20+
"OrthogonalMooreGrid",
21+
"OrthogonalVonNeumannGrid",
22+
"Network",
23+
]

0 commit comments

Comments
 (0)