From ff611c920da09afa9398478377672ea8d27fd80a Mon Sep 17 00:00:00 2001 From: corvince <13568919+Corvince@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:48:26 +0200 Subject: [PATCH 01/16] add some agents --- mesa/experimental/cell_space/cell.py | 6 +- mesa/experimental/cell_space/cell_agent.py | 68 ++++++++++++------- .../experimental/cell_space/discrete_space.py | 5 +- mesa/experimental/cell_space/grid.py | 8 ++- mesa/visualization/__init__.py | 3 +- tests/test_cell_space.py | 19 ++++-- 6 files changed, 69 insertions(+), 40 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 55264f68daa..f04f156b210 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -7,7 +7,7 @@ from mesa.experimental.cell_space.cell_collection import CellCollection if TYPE_CHECKING: - from mesa.experimental.cell_space.cell_agent import CellAgent + from mesa.experimental.cell_space.cell_agent import CellHolder class Cell: @@ -80,7 +80,7 @@ def disconnect(self, other: Cell) -> None: """ self._connections.remove(other) - def add_agent(self, agent: CellAgent) -> None: + def add_agent(self, agent: CellHolder) -> None: """Adds an agent to the cell. Args: @@ -96,7 +96,7 @@ def add_agent(self, agent: CellAgent) -> None: self.agents.append(agent) - def remove_agent(self, agent: CellAgent) -> None: + def remove_agent(self, agent: CellHolder) -> None: """Removes an agent from the cell. Args: diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index 5f1cca5cbcc..ff8c21305b7 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -1,37 +1,59 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Protocol, runtime_checkable -from mesa import Agent, Model +from mesa.experimental.cell_space.discrete_space import DiscreteSpace if TYPE_CHECKING: - from mesa.experimental.cell_space.cell import Cell + from mesa.experimental.cell_space import Cell, Grid -class CellAgent(Agent): - """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces +@runtime_checkable +class CellHolder(Protocol): + cell: Cell | None - Attributes: - unique_id (int): A unique identifier for this agent. - model (Model): The model instance to which the agent belongs - pos: (Position | None): The position of the agent in the space - cell: (Cell | None): the cell which the agent occupies - """ +class CellAgent: + cell: Cell | None + space: DiscreteSpace[Cell] + """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces""" - def __init__(self, model: Model) -> None: - """ - Create a new agent. - - Args: - unique_id (int): A unique identifier for this agent. - model (Model): The model instance in which the agent exists. - """ - super().__init__(model) - self.cell: Cell | None = None - - def move_to(self, cell) -> None: + def move_to(self: CellHolder, cell: Cell) -> None: if self.cell is not None: self.cell.remove_agent(self) self.cell = cell cell.add_agent(self) + + def move_relative(self, directions: tuple[int, ...], distance: int = 1): + new_position = tuple( + self.cell.coordinate[i] + directions[i] * distance + for i in range(len(directions)) + if self.cell + ) + new_cell = self.space[new_position] + self.move_to(new_cell) + + +class Grid2DMovingAgent(CellAgent): + grid: Grid[Cell] + + def move(self, direction: str, distance: int = 1): + match direction: + case "N" | "North" | "Up": + self.move_relative((-1, 0), distance) + case "S" | "South" | "Down": + self.move_relative((1, 0), distance) + case "E" | "East" | "Right": + self.move_relative((0, 1), distance) + case "W" | "West" | "Left": + self.move_relative((0, -1), distance) + case "NE" | "NorthEast" | "UpRight": + self.move_relative((-1, 1), distance) + case "NW" | "NorthWest" | "UpLeft": + self.move_relative((-1, -1), distance) + case "SE" | "SouthEast" | "DownRight": + self.move_relative((1, 1), distance) + case "SW" | "SouthWest" | "DownLeft": + self.move_relative((1, -1), distance) + case _: + raise ValueError(f"Invalid direction: {direction}") diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 6c92c320cb1..2b61d8d8aa4 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -43,7 +43,8 @@ def __init__( def cutoff_empties(self): return 7.953 * len(self._cells) ** 0.384 - def _connect_single_cell(self, cell: T): ... + def _connect_single_cell(self, cell: T): + ... @cached_property def all_cells(self): @@ -56,7 +57,7 @@ def __getitem__(self, key): return self._cells[key] @property - def empties(self) -> CellCollection: + def empties(self) -> CellCollection[T]: return self.all_cells.select(lambda cell: cell.is_empty) def select_random_empty_cell(self) -> T: diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index f08657d2107..3d00c349b35 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -10,7 +10,7 @@ T = TypeVar("T", bound=Cell) -class Grid(DiscreteSpace, Generic[T]): +class Grid(DiscreteSpace[T], Generic[T]): """Base class for all grid classes Attributes: @@ -51,9 +51,11 @@ def _connect_cells(self) -> None: else: self._connect_cells_nd() - def _connect_cells_2d(self) -> None: ... + def _connect_cells_2d(self) -> None: + ... - def _connect_cells_nd(self) -> None: ... + def _connect_cells_nd(self) -> None: + ... def _validate_parameters(self): if not all(isinstance(dim, int) and dim > 0 for dim in self.dimensions): diff --git a/mesa/visualization/__init__.py b/mesa/visualization/__init__.py index d8a0ebecf86..c37acd54cf5 100644 --- a/mesa/visualization/__init__.py +++ b/mesa/visualization/__init__.py @@ -1,12 +1,11 @@ from .components.altair import make_space_altair from .components.matplotlib import make_plot_measure, make_space_matplotlib -from .solara_viz import JupyterViz, SolaraViz, make_text +from .solara_viz import JupyterViz, SolaraViz from .UserParam import Slider __all__ = [ "JupyterViz", "SolaraViz", - "make_text", "Slider", "make_space_altair", "make_space_matplotlib", diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 4127847d17b..101d38864e9 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -3,6 +3,7 @@ import pytest from mesa import Model +from mesa.agent import Agent from mesa.experimental.cell_space import ( Cell, CellAgent, @@ -15,6 +16,10 @@ ) +class TestAgent(Agent, CellAgent): + pass + + def test_orthogonal_grid_neumann(): width = 10 height = 10 @@ -408,7 +413,7 @@ def test_empties_space(): model = Model() for i in range(8): - grid._cells[i].add_agent(CellAgent(model)) + grid._cells[i].add_agent(TestAgent(model)) cell = grid.select_random_empty_cell() assert cell.coordinate in {8, 9} @@ -432,7 +437,7 @@ def test_cell(): # add_agent model = Model() - agent = CellAgent(model) + agent = TestAgent(model) cell1.add_agent(agent) assert agent in cell1.agents @@ -445,11 +450,11 @@ def test_cell(): cell1.remove_agent(agent) cell1 = Cell((1,), capacity=1, random=random.Random()) - cell1.add_agent(CellAgent(model)) + cell1.add_agent(TestAgent(model)) assert cell1.is_full with pytest.raises(Exception): - cell1.add_agent(CellAgent(model)) + cell1.add_agent(TestAgent(model)) def test_cell_collection(): @@ -475,9 +480,9 @@ def test_cell_collection(): cells = collection.cells model = Model() - cells[0].add_agent(CellAgent(model)) - cells[3].add_agent(CellAgent(model)) - cells[7].add_agent(CellAgent(model)) + cells[0].add_agent(TestAgent(model)) + cells[3].add_agent(TestAgent(model)) + cells[7].add_agent(TestAgent(model)) agents = collection.agents assert len(list(agents)) == 3 From ec36c08c587eb0ee7ca2b7da478eb43a20923ccd Mon Sep 17 00:00:00 2001 From: corvince <13568919+Corvince@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:15:06 +0200 Subject: [PATCH 02/16] restructure and rename --- mesa/experimental/cell_space/cell.py | 6 +-- mesa/experimental/cell_space/cell_agent.py | 52 ++++++++++++++++------ 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index f04f156b210..6c5d7316f4c 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -7,7 +7,7 @@ from mesa.experimental.cell_space.cell_collection import CellCollection if TYPE_CHECKING: - from mesa.experimental.cell_space.cell_agent import CellHolder + from mesa.experimental.cell_space.cell_agent import DiscreteSpaceAgent class Cell: @@ -80,7 +80,7 @@ def disconnect(self, other: Cell) -> None: """ self._connections.remove(other) - def add_agent(self, agent: CellHolder) -> None: + def add_agent(self, agent: DiscreteSpaceAgent[Cell]) -> None: """Adds an agent to the cell. Args: @@ -96,7 +96,7 @@ def add_agent(self, agent: CellHolder) -> None: self.agents.append(agent) - def remove_agent(self, agent: CellHolder) -> None: + def remove_agent(self, agent: DiscreteSpaceAgent[Cell]) -> None: """Removes an agent from the cell. Args: diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index ff8c21305b7..e3486e10dce 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -1,24 +1,52 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, Protocol, TypeVar from mesa.experimental.cell_space.discrete_space import DiscreteSpace if TYPE_CHECKING: - from mesa.experimental.cell_space import Cell, Grid + from mesa.experimental.cell_space import Cell +T = TypeVar("T", bound="Cell") -@runtime_checkable -class CellHolder(Protocol): - cell: Cell | None + +class DiscreteSpaceAgent(Protocol[T]): + cell: T | None + space: DiscreteSpace[T] + + def move_to(self, cell: T) -> None: + ... + + def move_relative(self, directions: tuple[int, ...], distance: int = 1): + ... class CellAgent: - cell: Cell | None - space: DiscreteSpace[Cell] - """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces""" + """Cell Agent is an Agent class that adds behavior for moving in discrete spaces + + Attributes: + space (DiscreteSpace): the discrete space the agent is in + cell (Cell): the cell the agent is in + """ - def move_to(self: CellHolder, cell: Cell) -> None: + def __init__( + self, + space: DiscreteSpace[Cell], + cell: Cell | None = None, + *args: tuple[Any], + **kwargs: dict[str, Any], + ): + super().__init__(*args, **kwargs) + self.space = space + self.cell = cell + if cell is not None: + cell.add_agent(self) + + @property + def coordinate(self) -> tuple[int, ...]: + return self.cell.coordinate if self.cell else () + + def move_to(self, cell: Cell) -> None: if self.cell is not None: self.cell.remove_agent(self) self.cell = cell @@ -34,10 +62,8 @@ def move_relative(self, directions: tuple[int, ...], distance: int = 1): self.move_to(new_cell) -class Grid2DMovingAgent(CellAgent): - grid: Grid[Cell] - - def move(self, direction: str, distance: int = 1): +class Grid2DMovingAgent: + def move(self: DiscreteSpaceAgent[Cell], direction: str, distance: int = 1): match direction: case "N" | "North" | "Up": self.move_relative((-1, 0), distance) From 86f8cbcff5821c24e9580f8286ca476e08d0e5fa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:47:07 +0000 Subject: [PATCH 03/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/cell_agent.py | 6 ++---- mesa/experimental/cell_space/discrete_space.py | 3 +-- mesa/experimental/cell_space/grid.py | 6 ++---- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index e3486e10dce..1f6f6ea94be 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -14,11 +14,9 @@ class DiscreteSpaceAgent(Protocol[T]): cell: T | None space: DiscreteSpace[T] - def move_to(self, cell: T) -> None: - ... + def move_to(self, cell: T) -> None: ... - def move_relative(self, directions: tuple[int, ...], distance: int = 1): - ... + def move_relative(self, directions: tuple[int, ...], distance: int = 1): ... class CellAgent: diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 2b61d8d8aa4..dc51e15713a 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -43,8 +43,7 @@ def __init__( def cutoff_empties(self): return 7.953 * len(self._cells) ** 0.384 - def _connect_single_cell(self, cell: T): - ... + def _connect_single_cell(self, cell: T): ... @cached_property def all_cells(self): diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 3d00c349b35..87c13314720 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -51,11 +51,9 @@ def _connect_cells(self) -> None: else: self._connect_cells_nd() - def _connect_cells_2d(self) -> None: - ... + def _connect_cells_2d(self) -> None: ... - def _connect_cells_nd(self) -> None: - ... + def _connect_cells_nd(self) -> None: ... def _validate_parameters(self): if not all(isinstance(dim, int) and dim > 0 for dim in self.dimensions): From 28379ed372a553f57fb822b16a94828b9287e73c Mon Sep 17 00:00:00 2001 From: corvince <13568919+Corvince@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:12:42 +0200 Subject: [PATCH 04/16] Restructure mixins --- mesa/experimental/cell_space/cell.py | 11 +- mesa/experimental/cell_space/cell_agent.py | 119 ++++++++++++--------- tests/test_cell_space.py | 19 ++-- 3 files changed, 83 insertions(+), 66 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index c5b3645df03..d674d193c79 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -6,6 +6,7 @@ from random import Random from typing import TYPE_CHECKING +from mesa.experimental.cell_space.cell_agent import CellAgent from mesa.experimental.cell_space.cell_collection import CellCollection if TYPE_CHECKING: @@ -65,7 +66,7 @@ def __init__( self.agents: list[ Agent ] = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) - self.capacity: int = capacity + self.capacity: int | None = capacity self.properties: dict[Coordinate, object] = {} self.random = random @@ -92,7 +93,7 @@ def disconnect(self, other: Cell) -> None: for key in keys_to_remove: del self.connections[key] - def add_agent(self, agent: DiscreteSpaceAgent[Cell]) -> None: + def add_agent(self, agent: CellAgent) -> None: """Adds an agent to the cell. Args: @@ -108,7 +109,7 @@ def add_agent(self, agent: DiscreteSpaceAgent[Cell]) -> None: self.agents.append(agent) - def remove_agent(self, agent: DiscreteSpaceAgent[Cell]) -> None: + def remove_agent(self, agent: CellAgent) -> None: """Removes an agent from the cell. Args: @@ -132,7 +133,7 @@ def __repr__(self): # noqa return f"Cell({self.coordinate}, {self.agents})" @cached_property - def neighborhood(self) -> CellCollection: + def neighborhood(self) -> CellCollection[Cell]: """Returns the direct neighborhood of the cell. This is equivalent to cell.get_neighborhood(radius=1) @@ -144,7 +145,7 @@ def neighborhood(self) -> CellCollection: @cache # noqa: B019 def get_neighborhood( self, radius: int = 1, include_center: bool = False - ) -> CellCollection: + ) -> CellCollection[Cell]: """Returns a list of all neighboring cells for the given radius. For getting the direct neighborhood (i.e., radius=1) you can also use diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index a4e62c1c461..ee9a6e16e94 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -2,84 +2,105 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol, TypeVar +from typing import TYPE_CHECKING, Protocol -from mesa.experimental.cell_space.discrete_space import DiscreteSpace +from mesa.agent import Agent if TYPE_CHECKING: from mesa.experimental.cell_space import Cell -T = TypeVar("T", bound="Cell") +class HasCellProtocol(Protocol): + """Protocol for discrete space cell holders.""" -class DiscreteSpaceAgent(Protocol[T]): - cell: T | None - space: DiscreteSpace[T] + cell: Cell - def move_to(self, cell: T) -> None: ... - def move_relative(self, directions: tuple[int, ...], distance: int = 1): ... +class HasCell: + """Descriptor for cell movement behavior.""" + _mesa_cell: Cell | None = None -class CellAgent: - """Cell Agent is an Agent class that adds behavior for moving in discrete spaces + @property + def cell(self) -> Cell | None: # noqa: D102 + return self._mesa_cell - Attributes: - space (DiscreteSpace): the discrete space the agent is in - cell (Cell): the cell the agent is in - """ + @cell.setter + def cell(self, cell: Cell | None) -> None: + # remove from current cell + if self.cell is not None: + self.cell.remove_agent(self) - def __init__( - self, - space: DiscreteSpace[Cell], - cell: Cell | None = None, - *args: tuple[Any], - **kwargs: dict[str, Any], - ): - super().__init__(*args, **kwargs) - self.space = space - self.cell = cell + # update private attribute + self._mesa_cell = cell + + # add to new cell if cell is not None: cell.add_agent(self) - @property - def coordinate(self) -> tuple[int, ...]: - return self.cell.coordinate if self.cell else () - def move_to(self, cell: Cell) -> None: - if self.cell is not None: - self.cell.remove_agent(self) +class BasicMovement: + """Mixin for moving agents in discrete space.""" + + def move_to(self: HasCellProtocol, cell: Cell) -> None: + """Move to a new cell.""" self.cell = cell - cell.add_agent(self) - def move_relative(self, directions: tuple[int, ...], distance: int = 1): - new_position = tuple( - self.cell.coordinate[i] + directions[i] * distance - for i in range(len(directions)) - if self.cell - ) - new_cell = self.space[new_position] - self.move_to(new_cell) + def move_relative(self: HasCellProtocol, direction: tuple[int, ...]): + """Move to a cell relative to the current cell. + Args: + direction: The direction to move in. + """ + new_cell = self.cell.connections.get(direction) + if new_cell is not None: + self.cell = new_cell + else: + raise ValueError(f"No cell in direction {direction}") -class Grid2DMovingAgent: - def move(self: DiscreteSpaceAgent[Cell], direction: str, distance: int = 1): + +class CellAgent(Agent, HasCell, BasicMovement): + """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces. + + Attributes: + cell (Cell): The cell the agent is currently in. + """ + + def remove(self): + """Remove the agent from the model.""" + super().remove() + self.cell = None # ensures that we are also removed from cell + + +class Grid2DMovingAgent(BasicMovement): + """Mixin for moving agents in 2D grids.""" + + def move(self, direction: str, distance: int = 1): + """Move the agent in a cardinal direction. + + Args: + direction: The cardinal direction to move in. + distance: The distance to move. + """ match direction: case "N" | "North" | "Up": - self.move_relative((-1, 0), distance) + self.move_relative((-1, 0)) case "S" | "South" | "Down": - self.move_relative((1, 0), distance) + self.move_relative((1, 0)) case "E" | "East" | "Right": - self.move_relative((0, 1), distance) + self.move_relative((0, 1)) case "W" | "West" | "Left": - self.move_relative((0, -1), distance) + self.move_relative((0, -1)) case "NE" | "NorthEast" | "UpRight": - self.move_relative((-1, 1), distance) + self.move_relative((-1, 1)) case "NW" | "NorthWest" | "UpLeft": - self.move_relative((-1, -1), distance) + self.move_relative((-1, -1)) case "SE" | "SouthEast" | "DownRight": - self.move_relative((1, 1), distance) + self.move_relative((1, 1)) case "SW" | "SouthWest" | "DownLeft": - self.move_relative((1, -1), distance) + self.move_relative((1, -1)) case _: raise ValueError(f"Invalid direction: {direction}") + + for _ in range(distance): + self.move(direction) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 45f757e6b05..01420043797 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -5,7 +5,6 @@ import pytest from mesa import Model -from mesa.agent import Agent from mesa.experimental.cell_space import ( Cell, CellAgent, @@ -18,10 +17,6 @@ ) -class TestAgent(Agent, CellAgent): - pass - - def test_orthogonal_grid_neumann(): """Test orthogonal grid with von Neumann neighborhood.""" width = 10 @@ -438,7 +433,7 @@ def test_empties_space(): model = Model() for i in range(8): - grid._cells[i].add_agent(TestAgent(model)) + grid._cells[i].add_agent(CellAgent(model)) cell = grid.select_random_empty_cell() assert cell.coordinate in {8, 9} @@ -462,7 +457,7 @@ def test_cell(): # add_agent model = Model() - agent = TestAgent(model) + agent = CellAgent(model) cell1.add_agent(agent) assert agent in cell1.agents @@ -475,11 +470,11 @@ def test_cell(): cell1.remove_agent(agent) cell1 = Cell((1,), capacity=1, random=random.Random()) - cell1.add_agent(TestAgent(model)) + cell1.add_agent(CellAgent(model)) assert cell1.is_full with pytest.raises(Exception): - cell1.add_agent(TestAgent(model)) + cell1.add_agent(CellAgent(model)) def test_cell_collection(): @@ -506,9 +501,9 @@ def test_cell_collection(): cells = collection.cells model = Model() - cells[0].add_agent(TestAgent(model)) - cells[3].add_agent(TestAgent(model)) - cells[7].add_agent(TestAgent(model)) + cells[0].add_agent(CellAgent(model)) + cells[3].add_agent(CellAgent(model)) + cells[7].add_agent(CellAgent(model)) agents = collection.agents assert len(list(agents)) == 3 From a5f5930be64083cd6b87145077e5e10ab22cad19 Mon Sep 17 00:00:00 2001 From: corvince <13568919+Corvince@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:21:16 +0200 Subject: [PATCH 05/16] rename and update Grid2DMovement --- mesa/experimental/cell_space/cell_agent.py | 38 ++++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index ee9a6e16e94..90e732a879f 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -72,7 +72,7 @@ def remove(self): self.cell = None # ensures that we are also removed from cell -class Grid2DMovingAgent(BasicMovement): +class Grid2DMovement(BasicMovement): """Mixin for moving agents in 2D grids.""" def move(self, direction: str, distance: int = 1): @@ -82,25 +82,27 @@ def move(self, direction: str, distance: int = 1): direction: The cardinal direction to move in. distance: The distance to move. """ + direction = direction.lower() # Convert direction to lowercase + match direction: - case "N" | "North" | "Up": - self.move_relative((-1, 0)) - case "S" | "South" | "Down": - self.move_relative((1, 0)) - case "E" | "East" | "Right": - self.move_relative((0, 1)) - case "W" | "West" | "Left": - self.move_relative((0, -1)) - case "NE" | "NorthEast" | "UpRight": - self.move_relative((-1, 1)) - case "NW" | "NorthWest" | "UpLeft": - self.move_relative((-1, -1)) - case "SE" | "SouthEast" | "DownRight": - self.move_relative((1, 1)) - case "SW" | "SouthWest" | "DownLeft": - self.move_relative((1, -1)) + case "n" | "north" | "up": + move_vector = (-1, 0) + case "s" | "south" | "down": + move_vector = (1, 0) + case "e" | "east" | "right": + move_vector = (0, 1) + case "w" | "west" | "left": + move_vector = (0, -1) + case "ne" | "northeast" | "upright": + move_vector = (-1, 1) + case "nw" | "northwest" | "upleft": + move_vector = (-1, -1) + case "se" | "southeast" | "downright": + move_vector = (1, 1) + case "sw" | "southwest" | "downleft": + move_vector = (1, -1) case _: raise ValueError(f"Invalid direction: {direction}") for _ in range(distance): - self.move(direction) + self.move_relative(move_vector) From 9e38f6a9771b4a64a1f26efcc669bd3326f0d816 Mon Sep 17 00:00:00 2001 From: corvince <13568919+Corvince@users.noreply.github.com> Date: Tue, 1 Oct 2024 09:06:39 +0200 Subject: [PATCH 06/16] use direction map instead of match --- mesa/experimental/cell_space/cell_agent.py | 39 ++++++++++------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index 36c34bd1479..563275764d0 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -72,8 +72,21 @@ def remove(self): self.cell = None # ensures that we are also removed from cell -class Grid2DMovement(BasicMovement): - """Mixin for moving agents in a 2D grid.""" +class Grid2DMovingAgent(BasicMovement): + """Mixin for moving agents in 2D grids.""" + + # fmt: off + DIRECTION_MAP = { + "n": (-1, 0), "north": (-1, 0), "up": (-1, 0), + "s": (1, 0), "south": (1, 0), "down": (1, 0), + "e": (0, 1), "east": (0, 1), "right": (0, 1), + "w": (0, -1), "west": (0, -1), "left": (0, -1), + "ne": (-1, 1), "northeast": (-1, 1), "upright": (-1, 1), + "nw": (-1, -1), "northwest": (-1, -1), "upleft": (-1, -1), + "se": (1, 1), "southeast": (1, 1), "downright": (1, 1), + "sw": (1, -1), "southwest": (1, -1), "downleft": (1, -1) + } + # fmt: on def move(self, direction: str, distance: int = 1): """Move the agent in a cardinal direction. @@ -84,25 +97,9 @@ def move(self, direction: str, distance: int = 1): """ direction = direction.lower() # Convert direction to lowercase - match direction: - case "n" | "north" | "up": - move_vector = (-1, 0) - case "s" | "south" | "down": - move_vector = (1, 0) - case "e" | "east" | "right": - move_vector = (0, 1) - case "w" | "west" | "left": - move_vector = (0, -1) - case "ne" | "northeast" | "upright": - move_vector = (-1, 1) - case "nw" | "northwest" | "upleft": - move_vector = (-1, -1) - case "se" | "southeast" | "downright": - move_vector = (1, 1) - case "sw" | "southwest" | "downleft": - move_vector = (1, -1) - case _: - raise ValueError(f"Invalid direction: {direction}") + if direction not in self.DIRECTION_MAP: + raise ValueError(f"Invalid direction: {direction}") + move_vector = self.DIRECTION_MAP[direction] for _ in range(distance): self.move_relative(move_vector) From b0e95a029ebd7b976a3ea907007ca9f474cf8469 Mon Sep 17 00:00:00 2001 From: corvince <13568919+Corvince@users.noreply.github.com> Date: Tue, 1 Oct 2024 10:04:55 +0200 Subject: [PATCH 07/16] Add Patch --- mesa/experimental/cell_space/cell_agent.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index 563275764d0..99f79270206 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -59,6 +59,22 @@ def move_relative(self: HasCellProtocol, direction: tuple[int, ...]): raise ValueError(f"No cell in direction {direction}") +class FixedCell(HasCell): + """Mixin for agents that are fixed to a cell.""" + + @property + def cell(self) -> Cell | None: # noqa: D102 + return self._mesa_cell + + @cell.setter + def cell(self, cell: Cell) -> None: + if self.cell is not None: + raise ValueError("Cannot move agent in FixedCell") + self._mesa_cell = cell + + cell.add_agent(self) + + class CellAgent(Agent, HasCell, BasicMovement): """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces. @@ -72,6 +88,10 @@ def remove(self): self.cell = None # ensures that we are also removed from cell +class Patch(Agent, FixedCell): + """A patch in a 2D grid.""" + + class Grid2DMovingAgent(BasicMovement): """Mixin for moving agents in 2D grids.""" From 9ad1a2a18ea8b0c3013bd76bdce63b5a3d48f5b3 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 2 Oct 2024 20:57:15 +0200 Subject: [PATCH 08/16] tests for all new stuff --- mesa/experimental/cell_space/__init__.py | 4 +- mesa/experimental/cell_space/cell_agent.py | 11 ++++- tests/test_cell_space.py | 50 ++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py index 792dde611b9..4c088067999 100644 --- a/mesa/experimental/cell_space/__init__.py +++ b/mesa/experimental/cell_space/__init__.py @@ -6,7 +6,7 @@ """ from mesa.experimental.cell_space.cell import Cell -from mesa.experimental.cell_space.cell_agent import CellAgent +from mesa.experimental.cell_space.cell_agent import CellAgent, Grid2DMovingAgent, Patch from mesa.experimental.cell_space.cell_collection import CellCollection from mesa.experimental.cell_space.discrete_space import DiscreteSpace from mesa.experimental.cell_space.grid import ( @@ -22,6 +22,8 @@ "CellCollection", "Cell", "CellAgent", + "Grid2DMovingAgent", + "Patch", "DiscreteSpace", "Grid", "HexGrid", diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index 99f79270206..bbedc79a7e7 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -91,8 +91,17 @@ def remove(self): class Patch(Agent, FixedCell): """A patch in a 2D grid.""" + def remove(self): + """Remove the agent from the model.""" + super().remove() + + # fixme we leave self._mesa_cell on the original value + # so you cannot hijack remove() to move patches + self.cell.remove_agent(self) + + -class Grid2DMovingAgent(BasicMovement): +class Grid2DMovingAgent(Agent, HasCell, BasicMovement): """Mixin for moving agents in 2D grids.""" # fmt: off diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index a8e4abad336..2d5e7fafab5 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -15,6 +15,8 @@ OrthogonalMooreGrid, OrthogonalVonNeumannGrid, VoronoiGrid, + Grid2DMovingAgent, + Patch ) from mesa.space import PropertyLayer @@ -641,3 +643,51 @@ def test_cell_agent(): # noqa: D103 assert agent not in model._all_agents assert agent not in cell1.agents assert agent not in cell2.agents + + model = Model() + agent = CellAgent(model) + agent.cell = cell1 + agent.move_to(cell2) + assert agent not in cell1.agents + assert agent in cell2.agents + + +def test_grid2DMovingAgent(): + # we first test on a moore grid because all directions are defined + grid = OrthogonalMooreGrid((10, 10), torus=False) + + model = Model() + agent = Grid2DMovingAgent(model) + + agent.cell = grid[4,4] + agent.move('up') + assert agent.cell == grid[3, 4] + + grid = OrthogonalVonNeumannGrid((10, 10), torus=False) + + model = Model() + agent = Grid2DMovingAgent(model) + agent.cell = grid[4,4] + + with pytest.raises(ValueError): # test for invalid direction + agent.move('upright') + + with pytest.raises(ValueError): # test for unknown direction + agent.move('back') + + +def test_patch(): + cell1 = Cell((1,), capacity=None, random=random.Random()) + cell2 = Cell((2,), capacity=None, random=random.Random()) + + # connect + # add_agent + model = Model() + agent = Patch(model) + agent.cell = cell1 + + with pytest.raises(ValueError): + agent.cell = cell2 + + agent.remove() + assert agent not in model._agents \ No newline at end of file From 052cd7cc41f2e9a07baf2f823cd551d4a5542cce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:58:11 +0000 Subject: [PATCH 09/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/cell_agent.py | 1 - tests/test_cell_space.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index bbedc79a7e7..81bfcfd24d3 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -100,7 +100,6 @@ def remove(self): self.cell.remove_agent(self) - class Grid2DMovingAgent(Agent, HasCell, BasicMovement): """Mixin for moving agents in 2D grids.""" diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 2d5e7fafab5..b704a292efd 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -10,13 +10,13 @@ Cell, CellAgent, CellCollection, + Grid2DMovingAgent, HexGrid, Network, OrthogonalMooreGrid, OrthogonalVonNeumannGrid, + Patch, VoronoiGrid, - Grid2DMovingAgent, - Patch ) from mesa.space import PropertyLayer @@ -659,21 +659,21 @@ def test_grid2DMovingAgent(): model = Model() agent = Grid2DMovingAgent(model) - agent.cell = grid[4,4] - agent.move('up') + agent.cell = grid[4, 4] + agent.move("up") assert agent.cell == grid[3, 4] grid = OrthogonalVonNeumannGrid((10, 10), torus=False) model = Model() agent = Grid2DMovingAgent(model) - agent.cell = grid[4,4] + agent.cell = grid[4, 4] with pytest.raises(ValueError): # test for invalid direction - agent.move('upright') + agent.move("upright") with pytest.raises(ValueError): # test for unknown direction - agent.move('back') + agent.move("back") def test_patch(): @@ -690,4 +690,4 @@ def test_patch(): agent.cell = cell2 agent.remove() - assert agent not in model._agents \ No newline at end of file + assert agent not in model._agents From 51946badad05f0c51096668f62767d6f665ec3e6 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 2 Oct 2024 21:02:52 +0200 Subject: [PATCH 10/16] Update cell_agent.py --- mesa/experimental/cell_space/cell_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index bbedc79a7e7..2863abb98c9 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -101,7 +101,7 @@ def remove(self): -class Grid2DMovingAgent(Agent, HasCell, BasicMovement): +class Grid2DMovingAgent(CellAgent): """Mixin for moving agents in 2D grids.""" # fmt: off From 1d8a1118491a1f9ce1ec3d0e08eaf5bf37de5c9f Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 2 Oct 2024 21:13:46 +0200 Subject: [PATCH 11/16] Update test_cell_space.py --- tests/test_cell_space.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index b704a292efd..af57c09a0bd 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -652,7 +652,7 @@ def test_cell_agent(): # noqa: D103 assert agent in cell2.agents -def test_grid2DMovingAgent(): +def test_grid2DMovingAgent(): # noqa: D103 # we first test on a moore grid because all directions are defined grid = OrthogonalMooreGrid((10, 10), torus=False) @@ -676,7 +676,7 @@ def test_grid2DMovingAgent(): agent.move("back") -def test_patch(): +def test_patch(): # noqa: D103 cell1 = Cell((1,), capacity=None, random=random.Random()) cell2 = Cell((2,), capacity=None, random=random.Random()) From 87d60258595c4d29fb18c76c6c118d9ae9902d6e Mon Sep 17 00:00:00 2001 From: Corvince <13568919+Corvince@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:17:38 +0200 Subject: [PATCH 12/16] Rename Patch to FixedAgent Co-authored-by: Ewout ter Hoeven --- mesa/experimental/cell_space/cell_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index ea1c72b44ad..5dc967ee76d 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -88,7 +88,7 @@ def remove(self): self.cell = None # ensures that we are also removed from cell -class Patch(Agent, FixedCell): +class FixedAgent(Agent, FixedCell): """A patch in a 2D grid.""" def remove(self): From 5d280ff6f696a2e3879d2446145a0469fa269e2d Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Fri, 4 Oct 2024 09:49:49 +0200 Subject: [PATCH 13/16] Rename Patch to FixedAgent in tests --- mesa/experimental/cell_space/__init__.py | 4 ++-- tests/test_cell_space.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py index 4c088067999..3cb39404021 100644 --- a/mesa/experimental/cell_space/__init__.py +++ b/mesa/experimental/cell_space/__init__.py @@ -6,7 +6,7 @@ """ from mesa.experimental.cell_space.cell import Cell -from mesa.experimental.cell_space.cell_agent import CellAgent, Grid2DMovingAgent, Patch +from mesa.experimental.cell_space.cell_agent import CellAgent, Grid2DMovingAgent, FixedAgent from mesa.experimental.cell_space.cell_collection import CellCollection from mesa.experimental.cell_space.discrete_space import DiscreteSpace from mesa.experimental.cell_space.grid import ( @@ -23,7 +23,7 @@ "Cell", "CellAgent", "Grid2DMovingAgent", - "Patch", + "FixedAgent", "DiscreteSpace", "Grid", "HexGrid", diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index af57c09a0bd..a7866dcc552 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -15,7 +15,7 @@ Network, OrthogonalMooreGrid, OrthogonalVonNeumannGrid, - Patch, + FixedAgent, VoronoiGrid, ) from mesa.space import PropertyLayer @@ -683,7 +683,7 @@ def test_patch(): # noqa: D103 # connect # add_agent model = Model() - agent = Patch(model) + agent = FixedAgent(model) agent.cell = cell1 with pytest.raises(ValueError): From ddecebc4049fe0c5a1fd419148f3c161ffae64eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 07:50:02 +0000 Subject: [PATCH 14/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/__init__.py | 6 +++++- tests/test_cell_space.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py index 3cb39404021..69386a4cacf 100644 --- a/mesa/experimental/cell_space/__init__.py +++ b/mesa/experimental/cell_space/__init__.py @@ -6,7 +6,11 @@ """ from mesa.experimental.cell_space.cell import Cell -from mesa.experimental.cell_space.cell_agent import CellAgent, Grid2DMovingAgent, FixedAgent +from mesa.experimental.cell_space.cell_agent import ( + CellAgent, + FixedAgent, + Grid2DMovingAgent, +) from mesa.experimental.cell_space.cell_collection import CellCollection from mesa.experimental.cell_space.discrete_space import DiscreteSpace from mesa.experimental.cell_space.grid import ( diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index a7866dcc552..4d52e159045 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -10,12 +10,12 @@ Cell, CellAgent, CellCollection, + FixedAgent, Grid2DMovingAgent, HexGrid, Network, OrthogonalMooreGrid, OrthogonalVonNeumannGrid, - FixedAgent, VoronoiGrid, ) from mesa.space import PropertyLayer From d6b3015d292127b7f72692c49afd3bc109c1e5cd Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Fri, 4 Oct 2024 09:53:12 +0200 Subject: [PATCH 15/16] Use FixedAgent in examples/benchmarks --- benchmarks/WolfSheep/wolf_sheep.py | 4 ++-- mesa/experimental/devs/examples/wolf_sheep.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 9fd71846b7f..8c0b5b3cd90 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -10,7 +10,7 @@ import math from mesa import Model -from mesa.experimental.cell_space import CellAgent, OrthogonalVonNeumannGrid +from mesa.experimental.cell_space import CellAgent, OrthogonalVonNeumannGrid, FixedAgent from mesa.experimental.devs import ABMSimulator @@ -87,7 +87,7 @@ def feed(self): sheep_to_eat.remove() -class GrassPatch(CellAgent): +class GrassPatch(FixedAgent): """A patch of grass that grows at a fixed rate and it is eaten by sheep.""" @property diff --git a/mesa/experimental/devs/examples/wolf_sheep.py b/mesa/experimental/devs/examples/wolf_sheep.py index 8d7d16d671a..74318ef88af 100644 --- a/mesa/experimental/devs/examples/wolf_sheep.py +++ b/mesa/experimental/devs/examples/wolf_sheep.py @@ -1,6 +1,7 @@ """Example of using ABM simulator for Wolf-Sheep Predation Model.""" import mesa +from mesa.experimental.cell_space import FixedAgent from mesa.experimental.devs.simulator import ABMSimulator @@ -90,7 +91,7 @@ def feed(self): sheep_to_eat.die() -class GrassPatch(mesa.Agent): +class GrassPatch(FixedAgent): """A patch of grass that grows at a fixed rate and it is eaten by sheep.""" @property From 5d605c283f8b5bd18124a109d48b4756bac95085 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 07:53:22 +0000 Subject: [PATCH 16/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/WolfSheep/wolf_sheep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 8c0b5b3cd90..f085ce429df 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -10,7 +10,7 @@ import math from mesa import Model -from mesa.experimental.cell_space import CellAgent, OrthogonalVonNeumannGrid, FixedAgent +from mesa.experimental.cell_space import CellAgent, FixedAgent, OrthogonalVonNeumannGrid from mesa.experimental.devs import ABMSimulator