Skip to content

Commit e5ce58a

Browse files
authored
Move cell movement into a descriptor (#2333)
1 parent 63a679c commit e5ce58a

File tree

5 files changed

+93
-69
lines changed

5 files changed

+93
-69
lines changed

benchmarks/Schelling/schelling.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,36 @@
11
"""Schelling separation for performance benchmarking."""
22

3+
from __future__ import annotations
4+
35
from mesa import Model
4-
from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid
6+
from mesa.experimental.cell_space import Cell, CellAgent, OrthogonalMooreGrid
57

68

79
class SchellingAgent(CellAgent):
810
"""Schelling segregation agent."""
911

10-
def __init__(self, model, agent_type, radius, homophily):
12+
def __init__(
13+
self,
14+
model: Schelling,
15+
agent_type: int,
16+
radius: int,
17+
homophily: float,
18+
cell: Cell,
19+
):
1120
"""Create a new Schelling agent.
1221
1322
Args:
1423
model: model instance
1524
agent_type: type of agent (minority=1, majority=0)
1625
radius: size of neighborhood of agent
1726
homophily: fraction of neighbors of the same type that triggers movement
27+
cell: the cell in which the agent is located
1828
"""
1929
super().__init__(model)
2030
self.type = agent_type
2131
self.radius = radius
2232
self.homophily = homophily
33+
self.cell = cell
2334

2435
def step(self):
2536
"""Run one step of the agent."""
@@ -31,7 +42,7 @@ def step(self):
3142

3243
# If unhappy, move:
3344
if similar < self.homophily:
34-
self.move_to(self.model.grid.select_random_empty_cell())
45+
self.cell = self.model.grid.select_random_empty_cell()
3546
else:
3647
self.model.happy += 1
3748

@@ -41,18 +52,19 @@ class Schelling(Model):
4152

4253
def __init__(
4354
self,
55+
simulator=None,
4456
height=40,
4557
width=40,
4658
homophily=3,
4759
radius=1,
4860
density=0.8,
4961
minority_pc=0.5,
5062
seed=None,
51-
simulator=None,
5263
):
5364
"""Create a new Schelling model.
5465
5566
Args:
67+
simulator: simulator instance
5668
height: height of the grid
5769
width: width of the grid
5870
homophily: Minimum number of agents of same class needed to be happy
@@ -63,8 +75,9 @@ def __init__(
6375
simulator: a simulator instance
6476
"""
6577
super().__init__(seed=seed)
66-
self.minority_pc = minority_pc
6778
self.simulator = simulator
79+
self.minority_pc = minority_pc
80+
self.happy = 0
6881

6982
self.grid = OrthogonalMooreGrid(
7083
[height, width],
@@ -80,8 +93,7 @@ def __init__(
8093
for cell in self.grid:
8194
if self.random.random() < density:
8295
agent_type = 1 if self.random.random() < self.minority_pc else 0
83-
agent = SchellingAgent(self, agent_type, radius, homophily)
84-
agent.move_to(cell)
96+
SchellingAgent(self, agent_type, radius, homophily, cell)
8597

8698
def step(self):
8799
"""Run one step of the model."""

benchmarks/WolfSheep/wolf_sheep.py

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,51 +17,44 @@
1717
class Animal(CellAgent):
1818
"""The base animal class."""
1919

20-
def __init__(self, model, energy, p_reproduce, energy_from_food):
20+
def __init__(self, model, energy, p_reproduce, energy_from_food, cell):
2121
"""Initializes an animal.
2222
2323
Args:
2424
model: a model instance
2525
energy: starting amount of energy
2626
p_reproduce: probability of sexless reproduction
2727
energy_from_food: energy obtained from 1 unit of food
28+
cell: the cell in which the animal starts
2829
"""
2930
super().__init__(model)
3031
self.energy = energy
3132
self.p_reproduce = p_reproduce
3233
self.energy_from_food = energy_from_food
33-
34-
def random_move(self):
35-
"""Move to a random neighboring cell."""
36-
self.move_to(self.cell.neighborhood.select_random_cell())
34+
self.cell = cell
3735

3836
def spawn_offspring(self):
3937
"""Create offspring."""
4038
self.energy /= 2
41-
offspring = self.__class__(
39+
self.__class__(
4240
self.model,
4341
self.energy,
4442
self.p_reproduce,
4543
self.energy_from_food,
44+
self.cell,
4645
)
47-
offspring.move_to(self.cell)
4846

4947
def feed(self): ... # noqa: D102
5048

51-
def die(self):
52-
"""Die."""
53-
self.cell.remove_agent(self)
54-
self.remove()
55-
5649
def step(self):
5750
"""One step of the agent."""
58-
self.random_move()
51+
self.cell = self.cell.neighborhood.select_random_cell()
5952
self.energy -= 1
6053

6154
self.feed()
6255

6356
if self.energy < 0:
64-
self.die()
57+
self.remove()
6558
elif self.random.random() < self.p_reproduce:
6659
self.spawn_offspring()
6760

@@ -91,7 +84,7 @@ def feed(self):
9184
self.energy += self.energy_from_food
9285

9386
# Kill the sheep
94-
sheep_to_eat.die()
87+
sheep_to_eat.remove()
9588

9689

9790
class GrassPatch(CellAgent):
@@ -112,19 +105,19 @@ def fully_grown(self, value: bool) -> None:
112105
function_args=[self, "fully_grown", True],
113106
)
114107

115-
def __init__(self, model, fully_grown, countdown, grass_regrowth_time):
108+
def __init__(self, model, countdown, grass_regrowth_time, cell):
116109
"""Creates a new patch of grass.
117110
118111
Args:
119112
model: a model instance
120-
fully_grown: (boolean) Whether the patch of grass is fully grown or not
121113
countdown: Time for the patch of grass to be fully grown again
122114
grass_regrowth_time : time to fully regrow grass
115+
cell: the cell to which the patch of grass belongs
123116
"""
124-
# TODO:: fully grown can just be an int --> so one less param (i.e. countdown)
125117
super().__init__(model)
126-
self._fully_grown = fully_grown
118+
self._fully_grown = True if countdown == 0 else False # Noqa: SIM210
127119
self.grass_regrowth_time = grass_regrowth_time
120+
self.cell = cell
128121

129122
if not self.fully_grown:
130123
self.model.simulator.schedule_event_relative(
@@ -191,13 +184,7 @@ def __init__(
191184
self.random.randrange(self.height),
192185
)
193186
energy = self.random.randrange(2 * sheep_gain_from_food)
194-
sheep = Sheep(
195-
self,
196-
energy,
197-
sheep_reproduce,
198-
sheep_gain_from_food,
199-
)
200-
sheep.move_to(self.grid[pos])
187+
Sheep(self, energy, sheep_reproduce, sheep_gain_from_food, self.grid[pos])
201188

202189
# Create wolves
203190
for _ in range(self.initial_wolves):
@@ -206,24 +193,14 @@ def __init__(
206193
self.random.randrange(self.height),
207194
)
208195
energy = self.random.randrange(2 * wolf_gain_from_food)
209-
wolf = Wolf(
210-
self,
211-
energy,
212-
wolf_reproduce,
213-
wolf_gain_from_food,
214-
)
215-
wolf.move_to(self.grid[pos])
196+
Wolf(self, energy, wolf_reproduce, wolf_gain_from_food, self.grid[pos])
216197

217198
# Create grass patches
218199
possibly_fully_grown = [True, False]
219200
for cell in self.grid:
220201
fully_grown = self.random.choice(possibly_fully_grown)
221-
if fully_grown:
222-
countdown = grass_regrowth_time
223-
else:
224-
countdown = self.random.randrange(grass_regrowth_time)
225-
patch = GrassPatch(self, fully_grown, countdown, grass_regrowth_time)
226-
patch.move_to(cell)
202+
countdown = 0 if fully_grown else self.random.randrange(grass_regrowth_time)
203+
GrassPatch(self, countdown, grass_regrowth_time, cell)
227204

228205
def step(self):
229206
"""Run one step of the model."""

mesa/experimental/cell_space/cell.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ def remove_agent(self, agent: CellAgent) -> None:
117117
118118
"""
119119
self.agents.remove(agent)
120-
agent.cell = None
121120

122121
@property
123122
def is_empty(self) -> bool:

mesa/experimental/cell_space/cell_agent.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,36 @@
44

55
from typing import TYPE_CHECKING
66

7-
from mesa import Agent, Model
7+
from mesa import Agent
88

99
if TYPE_CHECKING:
1010
from mesa.experimental.cell_space.cell import Cell
1111

1212

13-
class CellAgent(Agent):
13+
class HasCell:
14+
"""Descriptor for cell movement behavior."""
15+
16+
_mesa_cell: Cell = None
17+
18+
@property
19+
def cell(self) -> Cell | None: # noqa: D102
20+
return self._mesa_cell
21+
22+
@cell.setter
23+
def cell(self, cell: Cell | None) -> None:
24+
# remove from current cell
25+
if self.cell is not None:
26+
self.cell.remove_agent(self)
27+
28+
# update private attribute
29+
self._mesa_cell = cell
30+
31+
# add to new cell
32+
if cell is not None:
33+
cell.add_agent(self)
34+
35+
36+
class CellAgent(Agent, HasCell):
1437
"""Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces.
1538
1639
Attributes:
@@ -20,23 +43,7 @@ class CellAgent(Agent):
2043
cell: (Cell | None): the cell which the agent occupies
2144
"""
2245

23-
def __init__(self, model: Model) -> None:
24-
"""Create a new agent.
25-
26-
Args:
27-
model (Model): The model instance in which the agent exists.
28-
"""
29-
super().__init__(model)
30-
self.cell: Cell | None = None
31-
32-
def move_to(self, cell) -> None:
33-
"""Move agent to cell.
34-
35-
Args:
36-
cell: cell to which agent is to move
37-
38-
"""
39-
if self.cell is not None:
40-
self.cell.remove_agent(self)
41-
self.cell = cell
42-
cell.add_agent(self)
46+
def remove(self):
47+
"""Remove the agent from the model."""
48+
super().remove()
49+
self.cell = None # ensures that we are also removed from cell

tests/test_cell_space.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,32 @@ def test_cell_collection():
524524

525525
cells = collection.select()
526526
assert len(cells) == len(collection)
527+
528+
529+
def test_cell_agent(): # noqa: D103
530+
cell1 = Cell((1,), capacity=None, random=random.Random())
531+
cell2 = Cell((2,), capacity=None, random=random.Random())
532+
533+
# connect
534+
# add_agent
535+
model = Model()
536+
agent = CellAgent(model)
537+
538+
agent.cell = cell1
539+
assert agent in cell1.agents
540+
541+
agent.cell = None
542+
assert agent not in cell1.agents
543+
544+
agent.cell = cell2
545+
assert agent not in cell1.agents
546+
assert agent in cell2.agents
547+
548+
agent.cell = cell1
549+
assert agent in cell1.agents
550+
assert agent not in cell2.agents
551+
552+
agent.remove()
553+
assert agent not in model._all_agents
554+
assert agent not in cell1.agents
555+
assert agent not in cell2.agents

0 commit comments

Comments
 (0)