Skip to content

Fixed move_agent_to_one_of to handle empty positions correctly #2732

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 49 additions & 65 deletions mesa/space.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ def move_agent(self, agent: Agent, pos: Coordinate) -> None:
def move_agent_to_one_of(
self,
agent: Agent,
pos: list[Coordinate],
pos: list[Coordinate] = None,
selection: str = "random",
handle_empty: str | None = None,
) -> None:
Expand All @@ -477,46 +477,44 @@ def move_agent_to_one_of(
and no positions are given (an empty list), a warning or error is raised respectively.
"""
# Only move agent if there are positions given (non-empty list)
if pos:
if selection == "random":
chosen_pos = agent.random.choice(pos)
elif selection == "closest":
current_pos = agent.pos
# Find the closest position without sorting all positions
# TODO: See if this method can be optimized further
closest_pos = []
min_distance = float("inf")
agent.random.shuffle(pos)
for p in pos:
distance = self._distance_squared(p, current_pos)
if distance < min_distance:
min_distance = distance
closest_pos.clear()
closest_pos.append(p)
elif distance == min_distance:
closest_pos.append(p)

chosen_pos = agent.random.choice(closest_pos)

else:
if not pos:
if handle_empty == "warning":
warn(
f"No positions given, moving agent {agent.unique_id} to an empty cell.",
RuntimeWarning,
stacklevel=2,
)
elif handle_empty == "error":
raise ValueError(
f"Invalid selection method {selection}. Choose 'random' or 'closest'."
f"No positions given, could not move agent {agent.unique_id}."
)
# Move agent to chosen position
self.move_agent(agent, chosen_pos)

# If no positions are given, throw warning/error if selected
elif handle_empty == "warning":
warn(
f"No positions given, could not move agent {agent.unique_id}.",
RuntimeWarning,
stacklevel=2,
)
elif handle_empty == "error":
# Move to a random empty cell instead
return

if selection == "random":
chosen_pos = agent.random.choice(pos)
elif selection == "closest":
current_pos = agent.pos
closest_pos = []
min_distance = float("inf")
agent.random.shuffle(pos)
for p in pos:
distance = self._distance_squared(p, current_pos)
if distance < min_distance:
min_distance = distance
closest_pos.clear()
closest_pos.append(p)
elif distance == min_distance:
closest_pos.append(p)
chosen_pos = agent.random.choice(closest_pos)
else:
raise ValueError(
f"No positions given, could not move agent {agent.unique_id}."
f"Invalid selection method {selection}. Choose 'random' or 'closest'."
)

self.move_agent(agent, chosen_pos)

def _distance_squared(self, pos1: Coordinate, pos2: Coordinate) -> float:
"""Calculate the squared Euclidean distance between two points for performance."""
# Use squared Euclidean distance to avoid sqrt operation
Expand Down Expand Up @@ -553,27 +551,11 @@ def is_cell_empty(self, pos: Coordinate) -> bool:

def move_to_empty(self, agent: Agent) -> None:
"""Moves agent to a random empty cell, vacating agent's old cell."""
num_empty_cells = len(self.empties)
if num_empty_cells == 0:
raise Exception("ERROR: No empty cells")

# This method is based on Agents.jl's random_empty() implementation. See
# https://github.com/JuliaDynamics/Agents.jl/pull/541. For the discussion, see
# https://github.com/projectmesa/mesa/issues/1052 and
# https://github.com/projectmesa/mesa/pull/1565. The cutoff value provided
# is the break-even comparison with the time taken in the else branching point.
if num_empty_cells > self.cutoff_empties:
while True:
new_pos = (
agent.random.randrange(self.width),
agent.random.randrange(self.height),
)
if self.is_cell_empty(new_pos):
break
else:
new_pos = agent.random.choice(sorted(self.empties))
self.remove_agent(agent)
self.place_agent(agent, new_pos)
if not self.empties:
raise ValueError("No empty cells available.")

new_pos = agent.random.choice(tuple(self.empties)) # Select a random empty cell
self.move_agent(agent, new_pos) # Move agent to selected empty cell

def exists_empty_cells(self) -> bool:
"""Return True if any cells empty else False."""
Expand Down Expand Up @@ -994,15 +976,17 @@ class SingleGrid(_PropertyGrid):
@warn_if_agent_has_position_already
def place_agent(self, agent: Agent, pos: Coordinate) -> None:
"""Place the agent at the specified location, and set its pos variable."""
if self.is_cell_empty(pos):
x, y = pos
self._grid[x][y] = agent
if self._empties_built:
self._empties.discard(pos)
self._empty_mask[pos] = False
agent.pos = pos
else:
raise Exception("Cell not empty")
if not self.is_cell_empty(pos):
raise ValueError(
f"Cannot place agent {agent.unique_id} at {pos}, cell is not empty."
)

x, y = pos
self._grid[x][y] = agent
if self._empties_built:
self._empties.discard(pos)
self._empty_mask[pos] = False
agent.pos = pos

def remove_agent(self, agent: Agent) -> None:
"""Remove the agent from the grid and set its pos attribute to None."""
Expand Down
Loading