Skip to content
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

space: Let move_agent choose from multiple positions #1920

Merged
merged 2 commits into from
Dec 24, 2023
Merged
Show file tree
Hide file tree
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
63 changes: 63 additions & 0 deletions mesa/space.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,69 @@ def move_agent(self, agent: Agent, pos: Coordinate) -> None:
self.remove_agent(agent)
self.place_agent(agent, pos)

def move_agent_to_one_of(
self,
agent: Agent,
pos: list[Coordinate],
selection: str = "random",
tpike3 marked this conversation as resolved.
Show resolved Hide resolved
handle_empty: str | None = None,
) -> None:
"""
Move an agent to one of the given positions.

Args:
agent: Agent object to move. Assumed to have its current location stored in a 'pos' tuple.
pos: List of possible positions.
selection: String, either "random" (default) or "closest". If "closest" is selected and multiple
cells are the same distance, one is chosen randomly.
handle_empty: String, either "warning", "error" or None (default). If "warning" or "error" is selected
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
closest_pos = None
min_distance = float("inf")
for p in pos:
distance = self._distance_squared(p, current_pos)
if distance < min_distance:
min_distance = distance
closest_pos = p
chosen_pos = closest_pos
else:
raise ValueError(
f"Invalid selection method {selection}. Choose 'random' or 'closest'."
)
# 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":
raise ValueError(
f"No positions given, could not move agent {agent.unique_id}."
)

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
dx, dy = abs(pos1[0] - pos2[0]), abs(pos1[1] - pos2[1])
if self.torus:
dx = min(dx, self.width - dx)
dy = min(dy, self.height - dy)
return dx**2 + dy**2

def swap_pos(self, agent_a: Agent, agent_b: Agent) -> None:
"""Swap agents positions"""
agents_no_pos = []
Expand Down
89 changes: 89 additions & 0 deletions tests/test_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,33 @@ def move_agent(self):
assert self.space[initial_pos[0]][initial_pos[1]] is None
assert self.space[final_pos[0]][final_pos[1]] == _agent

def test_move_agent_random_selection(self):
agent = self.agents[0]
possible_positions = [(10, 10), (20, 20), (30, 30)]
self.space.move_agent_to_one_of(agent, possible_positions, selection="random")
assert agent.pos in possible_positions

def test_move_agent_closest_selection(self):
agent = self.agents[0]
agent.pos = (5, 5)
possible_positions = [(6, 6), (10, 10), (20, 20)]
self.space.move_agent_to_one_of(agent, possible_positions, selection="closest")
assert agent.pos == (6, 6)

def test_move_agent_invalid_selection(self):
agent = self.agents[0]
possible_positions = [(10, 10), (20, 20), (30, 30)]
with self.assertRaises(ValueError):
self.space.move_agent_to_one_of(
agent, possible_positions, selection="invalid_option"
)

def test_distance_squared(self):
pos1 = (3, 4)
pos2 = (0, 0)
expected_distance_squared = 3**2 + 4**2
assert self.space._distance_squared(pos1, pos2) == expected_distance_squared

def test_iter_cell_list_contents(self):
"""
Test neighborhood retrieval
Expand All @@ -350,6 +377,68 @@ def test_iter_cell_list_contents(self):
assert len(cell_list_4) == 1


class TestSingleGridTorus(unittest.TestCase):
def setUp(self):
self.space = SingleGrid(50, 50, True) # Torus is True here
self.agents = []
for i, pos in enumerate(TEST_AGENTS_GRID):
a = MockAgent(i, None)
self.agents.append(a)
self.space.place_agent(a, pos)

def test_move_agent_random_selection(self):
agent = self.agents[0]
possible_positions = [(49, 49), (1, 1), (25, 25)]
self.space.move_agent_to_one_of(agent, possible_positions, selection="random")
assert agent.pos in possible_positions

def test_move_agent_closest_selection(self):
agent = self.agents[0]
agent.pos = (0, 0)
possible_positions = [(3, 3), (49, 49), (25, 25)]
self.space.move_agent_to_one_of(agent, possible_positions, selection="closest")
# Expecting (49, 49) to be the closest in a torus grid
assert agent.pos == (49, 49)

def test_move_agent_invalid_selection(self):
agent = self.agents[0]
possible_positions = [(10, 10), (20, 20), (30, 30)]
with self.assertRaises(ValueError):
self.space.move_agent_to_one_of(
agent, possible_positions, selection="invalid_option"
)

def test_move_agent_empty_list(self):
agent = self.agents[0]
possible_positions = []
agent.pos = (3, 3)
self.space.move_agent_to_one_of(agent, possible_positions, selection="random")
assert agent.pos == (3, 3)

def test_move_agent_empty_list_warning(self):
agent = self.agents[0]
possible_positions = []
# Should assert RuntimeWarning
with self.assertWarns(RuntimeWarning):
self.space.move_agent_to_one_of(
agent, possible_positions, selection="random", handle_empty="warning"
)

def test_move_agent_empty_list_error(self):
agent = self.agents[0]
possible_positions = []
with self.assertRaises(ValueError):
self.space.move_agent_to_one_of(
agent, possible_positions, selection="random", handle_empty="error"
)

def test_distance_squared_torus(self):
pos1 = (0, 0)
pos2 = (49, 49)
expected_distance_squared = 1**2 + 1**2 # In torus, these points are close
assert self.space._distance_squared(pos1, pos2) == expected_distance_squared


class TestSingleNetworkGrid(unittest.TestCase):
GRAPH_SIZE = 10

Expand Down