From 5105ddecd29e48f4440ac3268a6522c09c34e96d Mon Sep 17 00:00:00 2001 From: GaelLucero Date: Fri, 27 May 2022 17:34:47 -0400 Subject: [PATCH 1/8] adding heading information and created move_forward_or_backward function, which calculates the next position the agent will move. move_forward function, which moves the agent forward by specified amount. move_backward function, which moves the agent backwards by specified amount. turn_right function, turns the agent right by specified degree. turn_left function, turns the agent left by specified degree. --- mesa/agent.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/mesa/agent.py b/mesa/agent.py index 6d3d67919c6..bcbb03e70a3 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -28,6 +28,7 @@ def __init__(self, unique_id: int, model: "Model") -> None: self.unique_id = unique_id self.model = model self.pos: Optional[Position] = None + self.heading = 90 def step(self) -> None: """A single step of the agent.""" @@ -36,6 +37,33 @@ def step(self) -> None: def advance(self) -> None: pass + def move_forward_or_backward(self, amount, sign): + """Does the calculation to find the agent's next move and is used within the forward and backward functions""" + new_x = float(self.pos[0]) + sign * math.cos(self.heading * math.pi / 180) * amount + new_y = float(self.pos[1]) + sign * math.sin(self.heading * math.pi / -180) * amount + next_pos = (new_x, new_y) + try: + self.model.space.move_agent(self, next_pos) + except: + print("agent.py (forward_backwards): could not move agent within self.model.space") + + def move_forward(self, amount): + """Moves the agent forward by the amount given""" + self.move_forward_or_backward(amount, 1) + + def move_backward(self, amount): + """Moves the agent backwards from where its facing by the given amount""" + self.move_forward_or_backward(amount, -1) + + def turn_right(self, degree): + """Turns the agent right by the given degree""" + self.heading = (self.heading - degree) % 360 + + def turn_left(self, degree): + """Turns the agent left by the given degree""" + self.heading = (self.heading + degree) % 360 + + @property def random(self) -> Random: return self.model.random From a6fd559bdc35e90bcd90f4f8aee496cb0de629ce Mon Sep 17 00:00:00 2001 From: GaelLucero Date: Fri, 27 May 2022 17:47:03 -0400 Subject: [PATCH 2/8] Created setxy function which sets the agents position to the specified parameters. set_pos function, sets the current position to the specified pos parameter. distancexy function, gives you the distance of the agent and the given coordinate. distance function, gives you the distance between the agent and another agent. --- mesa/agent.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mesa/agent.py b/mesa/agent.py index bcbb03e70a3..c151269f944 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -63,6 +63,22 @@ def turn_left(self, degree): """Turns the agent left by the given degree""" self.heading = (self.heading + degree) % 360 + def setxy(self, x, y): + """Sets the current position to the specified x,y parameters""" + self.pos = (x, y) + + def set_pos(self, apos): + """Sets the current position to the specified pos parameter""" + self.pos = apos + + def distancexy(self, x, y): + """Gives you the distance of the agent and the given coordinate""" + return math.dist(self.pos, (x, y)) + + def distance(self, another_agent): + """Gives you the distance between the agent and another agent""" + return self.distancexy(another_agent.pos[0], another_agent.pos[1]) + @property def random(self) -> Random: From 89d628638aaa8d85c66a5cb37c507f254a1dfdd9 Mon Sep 17 00:00:00 2001 From: GaelLucero Date: Fri, 27 May 2022 17:55:52 -0400 Subject: [PATCH 3/8] Created die function which, removes the agent from the schedule and the grid. towardsxy function, calculates angle between a given coordinate and horizon as if the current position is the origin. towards function, calculates angle between an agent and horizon as if the current position is the origin. facexy function, makes agent face a given coordinate. face function, makes agent face another agent --- mesa/agent.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/mesa/agent.py b/mesa/agent.py index c151269f944..08937bd112c 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -79,6 +79,38 @@ def distance(self, another_agent): """Gives you the distance between the agent and another agent""" return self.distancexy(another_agent.pos[0], another_agent.pos[1]) + def die(self): + """Removes the agent from the schedule and the grid """ + try: + self.model.schedule.remove(self) + except: + print("agent.py (die): could not remove agent from self.model.schedule") + try: + self.model.space.remove_agent(self) + except: + print("agent.py (die): could not remove agent from self.model.space") + + def towardsxy(self, x, y): + """Calculates angle between a given coordinate and horizon as if the current position is the origin""" + dx = x - float(self.pos[0]) + dy = float(self.pos[1]) - y + if dx == 0: + return 90 if dy > 0 else 270 + else: + return math.degrees(math.atan2(dy, dx)) + + def towards(self, another_agent): + """Calculates angle between an agent and horizon as if the current position is the origin""" + return self.towardsxy(*another_agent.pos) + + def facexy(self, x, y): + """Makes agent face a given coordinate""" + self.heading = self.towardsxy(x, y) + + def face(self, another_agent): + """Makes agent face another agent""" + x, y = another_agent.pos + self.facexy(x, y) @property def random(self) -> Random: From 99323be0b44d536f34727dd684eb62daf321134f Mon Sep 17 00:00:00 2001 From: Catherine Devlin Date: Sun, 26 May 2024 16:23:32 -0400 Subject: [PATCH 4/8] Tweaks to GaelLucero's agent spatial methods - Omitted .setxy, .set_pos as unnecessary setters - added defaults - raise exception if model has no space - moved .die to separate PR - added tests --- tests/test_agent_spatial_methods.py | 75 +++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/test_agent_spatial_methods.py diff --git a/tests/test_agent_spatial_methods.py b/tests/test_agent_spatial_methods.py new file mode 100644 index 00000000000..3f5137c6f03 --- /dev/null +++ b/tests/test_agent_spatial_methods.py @@ -0,0 +1,75 @@ +import pytest + +from mesa.agent import Agent +from mesa.model import Model +from mesa.space import ContinuousSpace, MultiGrid + + +@pytest.fixture +def agent_in_space(): + model = Model() + model.space = ContinuousSpace(10, 10, torus=True) + agent = Agent(1, model) + agent.pos = (2, 1) + agent.heading = 0 + return agent + + +def test_move_forward(agent_in_space): + + agent_in_space.heading = 90 + agent_in_space.move_forward(1) + assert agent_in_space.pos[0] == pytest.approx(2) + assert agent_in_space.pos[1] == pytest.approx(0) + + +def test_turn_right(agent_in_space): + + agent_in_space.heading = 0 + agent_in_space.turn_right(60) + assert agent_in_space.heading == 300 + agent_in_space.move_forward(1) + assert agent_in_space.pos[0] == pytest.approx(2.5) + assert agent_in_space.pos[1] == pytest.approx(1.8660254) + + +def test_move_forward_toroid(agent_in_space): + "Verify that toroidal wrapping applies to move_forward" + + agent_in_space.move_forward(10.0) + assert agent_in_space.pos[0] == pytest.approx(2) + assert agent_in_space.pos[1] == pytest.approx(1) + + +def test_move_forward_raises_if_no_space(): + """move_forward only applies for models with ContinuousSpace""" + + model = Model() + model.grid = MultiGrid(10, 10, torus=True) + agent = Agent(1, model) + agent.pos = (2, 1) + with pytest.raises(Exception): + agent.move_forward(10.0) + + +def test_towards(agent_in_space): + agent2 = Agent(2, agent_in_space.model) + agent2.pos = (5, 1) + assert agent_in_space.towards(agent2) == pytest.approx(0) + agent2.pos = (2, 4) + assert agent_in_space.towards(agent2) == pytest.approx(270) + agent2.pos = (5, 4) + assert agent_in_space.towards(agent2) == pytest.approx(-45) + + +def test_facexy(agent_in_space): + agent_in_space.facexy(2, 5) + assert agent_in_space.heading == pytest.approx(270) + + +def test_face(agent_in_space): + + agent2 = Agent(2, agent_in_space.model) + agent2.pos = (5, 1) + agent_in_space.face(agent2) + assert agent_in_space.heading == pytest.approx(0) From e47993db6dbf39a1145a3f147a1c4349dce855f6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 26 May 2024 20:35:10 +0000 Subject: [PATCH 5/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_agent_spatial_methods.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_agent_spatial_methods.py b/tests/test_agent_spatial_methods.py index 3f5137c6f03..afce6ac1203 100644 --- a/tests/test_agent_spatial_methods.py +++ b/tests/test_agent_spatial_methods.py @@ -16,7 +16,6 @@ def agent_in_space(): def test_move_forward(agent_in_space): - agent_in_space.heading = 90 agent_in_space.move_forward(1) assert agent_in_space.pos[0] == pytest.approx(2) @@ -24,7 +23,6 @@ def test_move_forward(agent_in_space): def test_turn_right(agent_in_space): - agent_in_space.heading = 0 agent_in_space.turn_right(60) assert agent_in_space.heading == 300 @@ -68,7 +66,6 @@ def test_facexy(agent_in_space): def test_face(agent_in_space): - agent2 = Agent(2, agent_in_space.model) agent2.pos = (5, 1) agent_in_space.face(agent2) From 43fa2205c6b01ef6b0138b70c4fcccd05db6f53e Mon Sep 17 00:00:00 2001 From: Catherine Devlin Date: Sun, 26 May 2024 16:44:48 -0400 Subject: [PATCH 6/8] Changes omitted from 99323b --- mesa/agent.py | 50 ++++++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index 0f95579d191..0ad8559cf42 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -10,6 +10,7 @@ import contextlib import copy +import math import operator import warnings import weakref @@ -47,7 +48,6 @@ def __init__(self, unique_id: int, model: Model) -> None: """ self.unique_id = unique_id self.model = model -<<<<<<< HEAD self.pos: Position | None = None # register agent @@ -69,7 +69,7 @@ def remove(self) -> None: """Remove and delete the agent from the model.""" with contextlib.suppress(KeyError): self.model.agents_[type(self)].pop(self) - self.pos: Optional[Position] = None + self.pos: Position | None self.heading = 90 def step(self) -> None: @@ -78,58 +78,48 @@ def step(self) -> None: def advance(self) -> None: pass + @property + def random(self) -> Random: + return self.model.random + def move_forward_or_backward(self, amount, sign): """Does the calculation to find the agent's next move and is used within the forward and backward functions""" new_x = float(self.pos[0]) + sign * math.cos(self.heading * math.pi / 180) * amount new_y = float(self.pos[1]) + sign * math.sin(self.heading * math.pi / -180) * amount next_pos = (new_x, new_y) + try: self.model.space.move_agent(self, next_pos) - except: - print("agent.py (forward_backwards): could not move agent within self.model.space") + except AttributeError as exc: + raise AttributeError("Agent's model does not define space") from exc + except Exception as exc: + raise UserWarning(f"agent.py (forward_backwards): could not move agent " + f"{self.unique_id} within self.model.space") from exc - def move_forward(self, amount): + def move_forward(self, amount=1): """Moves the agent forward by the amount given""" self.move_forward_or_backward(amount, 1) - def move_backward(self, amount): + def move_backward(self, amount=1): """Moves the agent backwards from where its facing by the given amount""" self.move_forward_or_backward(amount, -1) - def turn_right(self, degree): + def turn_right(self, degree=90): """Turns the agent right by the given degree""" self.heading = (self.heading - degree) % 360 - def turn_left(self, degree): + def turn_left(self, degree=90): """Turns the agent left by the given degree""" self.heading = (self.heading + degree) % 360 - def setxy(self, x, y): - """Sets the current position to the specified x,y parameters""" - self.pos = (x, y) - - def set_pos(self, apos): - """Sets the current position to the specified pos parameter""" - self.pos = apos - def distancexy(self, x, y): """Gives you the distance of the agent and the given coordinate""" + return math.dist(self.pos, (x, y)) def distance(self, another_agent): """Gives you the distance between the agent and another agent""" - return self.distancexy(another_agent.pos[0], another_agent.pos[1]) - - def die(self): - """Removes the agent from the schedule and the grid """ - try: - self.model.schedule.remove(self) - except: - print("agent.py (die): could not remove agent from self.model.schedule") - try: - self.model.space.remove_agent(self) - except: - print("agent.py (die): could not remove agent from self.model.space") + return self.distancexy(**(another_agent.pos)) def towardsxy(self, x, y): """Calculates angle between a given coordinate and horizon as if the current position is the origin""" @@ -153,10 +143,6 @@ def face(self, another_agent): x, y = another_agent.pos self.facexy(x, y) - @property - def random(self) -> Random: - return self.model.random - class AgentSet(MutableSet, Sequence): """ From b9243188236ba214df22502bc3676b8e4ab1acc0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 26 May 2024 20:45:01 +0000 Subject: [PATCH 7/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/agent.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index 0ad8559cf42..d34870611af 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -84,8 +84,12 @@ def random(self) -> Random: def move_forward_or_backward(self, amount, sign): """Does the calculation to find the agent's next move and is used within the forward and backward functions""" - new_x = float(self.pos[0]) + sign * math.cos(self.heading * math.pi / 180) * amount - new_y = float(self.pos[1]) + sign * math.sin(self.heading * math.pi / -180) * amount + new_x = ( + float(self.pos[0]) + sign * math.cos(self.heading * math.pi / 180) * amount + ) + new_y = ( + float(self.pos[1]) + sign * math.sin(self.heading * math.pi / -180) * amount + ) next_pos = (new_x, new_y) try: @@ -93,8 +97,10 @@ def move_forward_or_backward(self, amount, sign): except AttributeError as exc: raise AttributeError("Agent's model does not define space") from exc except Exception as exc: - raise UserWarning(f"agent.py (forward_backwards): could not move agent " - f"{self.unique_id} within self.model.space") from exc + raise UserWarning( + f"agent.py (forward_backwards): could not move agent " + f"{self.unique_id} within self.model.space" + ) from exc def move_forward(self, amount=1): """Moves the agent forward by the amount given""" From ed72aa9ed79ea7200713da9d76a6c8ace33b4ffe Mon Sep 17 00:00:00 2001 From: Catherine Devlin Date: Sun, 26 May 2024 17:43:46 -0400 Subject: [PATCH 8/8] warnings.warn rather than raise UserWarning interrupts execution; behavior depends on config We don't necessarily want failures in agent motion to interrupt overall execution --- mesa/agent.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index d34870611af..3a70d83bf46 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -97,10 +97,10 @@ def move_forward_or_backward(self, amount, sign): except AttributeError as exc: raise AttributeError("Agent's model does not define space") from exc except Exception as exc: - raise UserWarning( + warnings.warn( f"agent.py (forward_backwards): could not move agent " - f"{self.unique_id} within self.model.space" - ) from exc + f"{self.unique_id} within self.model.space\n{exc}" + ) def move_forward(self, amount=1): """Moves the agent forward by the amount given"""