Skip to content

Commit c24b612

Browse files
committed
[PERF] Improve move_to_empty performance
1 parent 246c69d commit c24b612

File tree

2 files changed

+49
-4
lines changed

2 files changed

+49
-4
lines changed

mesa/space.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939
FloatCoordinate = Union[Tuple[float, float], np.ndarray]
4040

4141

42+
def clamp(x: float, lowest: float, highest: float) -> float:
43+
# This should be faster than np.clip for a scalar x.
44+
# TODO: measure how much faster this function is.
45+
return max(lowest, min(x, highest))
46+
47+
4248
def accept_tuple_argument(wrapped_function):
4349
"""Decorator to allow grid methods that take a list of (x, y) coord tuples
4450
to also handle a single position, by automatically wrapping tuple in
@@ -410,12 +416,46 @@ def is_cell_empty(self, pos: Coordinate) -> bool:
410416
x, y = pos
411417
return self.grid[x][y] == self.default_val()
412418

413-
def move_to_empty(self, agent: Agent) -> None:
419+
def move_to_empty(
420+
self, agent: Agent, cutoff: float = 0.998, num_agents: Optional[int] = None
421+
) -> None:
414422
"""Moves agent to a random empty cell, vacating agent's old cell."""
415423
pos = agent.pos
416424
if len(self.empties) == 0:
417425
raise Exception("ERROR: No empty cells")
418-
new_pos = agent.random.choice(sorted(self.empties))
426+
if num_agents is None:
427+
try:
428+
num_agents = agent.model.schedule.get_agent_count()
429+
except AttributeError:
430+
raise Exception(
431+
"Your agent is not attached to a model, and so Mesa is unable\n"
432+
"to figure out the total number of agents you have created.\n"
433+
"This number is required in order to calculate the threshold\n"
434+
"for using a much faster algorithm to find an empty cell.\n"
435+
"In this case, you must specify `num_agents`."
436+
)
437+
new_pos = (0, 0) # Initialize it with a starting value.
438+
# This method is based on Agents.jl's random_empty() implementation.
439+
# See https://github.com/JuliaDynamics/Agents.jl/pull/541.
440+
# For the discussion, see
441+
# https://github.com/projectmesa/mesa/issues/1052.
442+
# This switch assumes the worst case (for this algorithm) of one
443+
# agent per position, which is not true in general but is appropriate
444+
# here.
445+
if clamp(num_agents / (self.width * self.height), 0.0, 1.0) < cutoff:
446+
# The default cutoff value provided is the break-even comparison
447+
# with the time taken in the else branching point.
448+
# The number is measured to be 0.998 in Agents.jl, but since Mesa
449+
# run under different environment, the number is different here.
450+
while True:
451+
new_pos = (
452+
agent.random.randrange(self.width),
453+
agent.random.randrange(self.height),
454+
)
455+
if self.is_cell_empty(new_pos):
456+
break
457+
else:
458+
new_pos = agent.random.choice(sorted(self.empties))
419459
self._place_agent(new_pos, agent)
420460
agent.pos = new_pos
421461
self._remove_agent(pos, agent)

tests/test_grid.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ def setUp(self):
229229
a = MockAgent(counter, None)
230230
self.agents.append(a)
231231
self.grid.place_agent(a, (x, y))
232+
self.num_agents = len(self.agents)
232233

233234
def test_enforcement(self):
234235
"""
@@ -242,25 +243,29 @@ def test_enforcement(self):
242243

243244
# Place the agent in an empty cell
244245
self.grid.position_agent(a)
246+
self.num_agents += 1
245247
# Test whether after placing, the empty cells are reduced by 1
246248
assert a.pos not in self.grid.empties
247249
assert len(self.grid.empties) == 8
248250
for i in range(10):
249-
self.grid.move_to_empty(a)
251+
# Since the agents and the grid are not associated with a model, we
252+
# must explicitly tell move_to_empty the number of agents.
253+
self.grid.move_to_empty(a, num_agents=self.num_agents)
250254
assert len(self.grid.empties) == 8
251255

252256
# Place agents until the grid is full
253257
empty_cells = len(self.grid.empties)
254258
for i in range(empty_cells):
255259
a = MockAgent(101 + i, None)
256260
self.grid.position_agent(a)
261+
self.num_agents += 1
257262
assert len(self.grid.empties) == 0
258263

259264
a = MockAgent(110, None)
260265
with self.assertRaises(Exception):
261266
self.grid.position_agent(a)
262267
with self.assertRaises(Exception):
263-
self.move_to_empty(self.agents[0])
268+
self.move_to_empty(self.agents[0], num_agents=self.num_agents)
264269

265270

266271
# Number of agents at each position for testing

0 commit comments

Comments
 (0)