From dd54f2127943b8c8b0177dfb5dc14c0e29b8cc9f Mon Sep 17 00:00:00 2001 From: Corvince Date: Fri, 19 Jan 2024 14:32:56 +0100 Subject: [PATCH 01/63] Add draft implementation for CellSpace --- mesa/agent.py | 7 +- mesa/gridspace.py | 197 ++++++++++++++++++++++++++++++++++++++++++++++ mesa/schelling.py | 86 ++++++++++++++++++++ 3 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 mesa/gridspace.py create mode 100644 mesa/schelling.py diff --git a/mesa/agent.py b/mesa/agent.py index fcf643d3289..ecf0d33a0af 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -22,6 +22,7 @@ if TYPE_CHECKING: # We ensure that these are not imported during runtime to prevent cyclic # dependency. + from mesa.gridspace import Cell from mesa.model import Model from mesa.space import Position @@ -46,7 +47,7 @@ def __init__(self, unique_id: int, model: Model) -> None: """ self.unique_id = unique_id self.model = model - self.pos: Position | None = None + self.cell: Cell | None = None # register agent try: @@ -62,6 +63,10 @@ def __init__(self, unique_id: int, model: Model) -> None: stacklevel=2, ) + @property + def pos(self): + return self.cell.coords + def remove(self) -> None: """Remove and delete the agent from the model.""" with contextlib.suppress(KeyError): diff --git a/mesa/gridspace.py b/mesa/gridspace.py new file mode 100644 index 00000000000..d4ce654b3e3 --- /dev/null +++ b/mesa/gridspace.py @@ -0,0 +1,197 @@ +import itertools +import random +from functools import cache +from typing import Any + +from mesa import Agent + +Coordinates = tuple[int, int] + + +def create_neighborhood_getter(moore=True, include_center=False, radius=1): + @cache + def of(cell: Cell): + if radius == 0: + return {cell: cell.content} + + neighborhood = {} + for neighbor in cell.connections: + if ( + moore + or neighbor.coords[0] == cell.coords[0] + or neighbor.coords[1] == cell.coords[1] + ): + neighborhood[neighbor] = neighbor.content + + if radius > 1: + for neighbor in list(neighborhood.keys()): + neighborhood.update( + create_neighborhood_getter(moore, include_center, radius - 1)( + neighbor + ) + ) + + if not include_center: + neighborhood.pop(cell, None) + + return CellCollection(neighborhood) + + return of + + +class Cell: + __slots__ = ["coords", "connections", "content"] + + def __init__(self, i: int, j: int) -> None: + self.coords = (i, j) + self.connections: list[Cell] = [] + self.content: list[Agent] = [] + + def connect(self, other) -> None: + """Connects this cell to another cell.""" + self.connections.append(other) + + def disconnect(self, other) -> None: + """Disconnects this cell from another cell.""" + self.connections.remove(other) + + def add_agent(self, agent: Agent) -> None: + """Adds an agent to the cell.""" + self.content.append(agent) + agent.cell = self + + def remove_agent(self, agent: Agent) -> None: + """Removes an agent from the cell.""" + if agent in self.content: + self.content.remove(agent) + + def __repr__(self): + return f"Cell({self.coords})" + + +class CellCollection: + def __init__(self, cells: dict[Cell, list[Agent]]) -> None: + self.cells = cells + + def __iter__(self): + return iter(self.cells) + + def __getitem__(self, key): + return self.cells[key] + + def __len__(self): + return len(self.cells) + + def __repr__(self): + return f"CellCollection({self.cells})" + + @property + def agents(self): + return itertools.chain.from_iterable(self.cells.values()) + + def select_random(self): + return random.choice(list(self.cells.keys())) + + +class Space: + cells: dict[Coordinates, Cell] + + def _connect_single_cell(self, cell): # <= different for every concrete Space + ... + + def __iter__(self): + return iter(self.cells.values()) + + def get_neighborhood(self, coords: Coordinates, neighborhood_getter: Any): + return neighborhood_getter(self.cells[coords]) + + def move_agent(self, agent: Agent, pos) -> None: + """Move an agent from its current position to a new position.""" + if (old_cell := agent.cell) is not None: + old_cell.remove_agent(agent) + if self._empties_built: + self._empties.add(old_cell.coords) + + new_cell = self.cells[pos] + new_cell.add_agent(agent) + if self._empties_built: + self._empties.discard(new_cell.coords) + + @property + def empties(self) -> CellCollection: + if not self._empties_built: + self.build_empties() + + return CellCollection( + { + self.cells[coords]: self.cells[coords].content + for coords in sorted(self._empties) + } + ) + + def build_empties(self) -> None: + self._empties = set(filter(self.is_cell_empty, self.cells.keys())) + self._empties_built = True + + 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 = self.empties.select_random().coords + self.move_agent(agent, new_pos) + + def is_cell_empty(self, pos) -> bool: + """Returns a bool of the contents of a cell.""" + return len(self.cells[pos].content) == 0 + + +class Grid(Space): + def __init__(self, width: int, height: int, torus: bool = False) -> None: + self.width = width + self.height = height + self.torus = torus + self.cells = {(i, j): Cell(i, j) for j in range(width) for i in range(height)} + + self._empties_built = False + self.cutoff_empties = 7.953 * len(self.cells) ** 0.384 + + for cell in self.cells.values(): + self._connect_single_cell(cell) + + def _connect_single_cell(self, cell): + i, j = cell.coords + directions = [ + (-1, -1), + (-1, 0), + (-1, 1), + (0, -1), + (0, 1), + (1, -1), + (1, 0), + (1, 1), + ] + for di, dj in directions: + ni, nj = (i + di, j + dj) + if self.torus: + ni, nj = ni % self.height, nj % self.width + if 0 <= ni < self.height and 0 <= nj < self.width: + cell.connect(self.cells[ni, nj]) + + def get_neighborhood(self, coords, neighborhood_getter: Any) -> CellCollection: + return neighborhood_getter(self.cells[coords]) diff --git a/mesa/schelling.py b/mesa/schelling.py new file mode 100644 index 00000000000..43aa90e3d3d --- /dev/null +++ b/mesa/schelling.py @@ -0,0 +1,86 @@ +import mesa +from mesa.gridspace import Grid, create_neighborhood_getter + + +class SchellingAgent(mesa.Agent): + """ + Schelling segregation agent + """ + + def __init__(self, pos, model, agent_type, cell): + """ + Create a new Schelling agent. + + Args: + unique_id: Unique identifier for the agent. + x, y: Agent initial location. + agent_type: Indicator for the agent's type (minority=1, majority=0) + """ + super().__init__(pos, model) + self.pos = pos + self.cell = cell + self.type = agent_type + self.get_neighborhood = create_neighborhood_getter() + + def step(self): + similar = 0 + for neighbor in self.get_neighborhood(self.cell).agents: + if neighbor.type == self.type: + similar += 1 + + # If unhappy, move: + if similar < self.model.homophily: + self.model.grid.move_to_empty(self) + else: + self.model.happy += 1 + + +class Schelling(mesa.Model): + """ + Model class for the Schelling segregation model. + """ + + def __init__(self, width=20, height=20, density=0.8, minority_pc=0.2, homophily=3): + """ """ + + self.width = width + self.height = height + self.density = density + self.minority_pc = minority_pc + self.homophily = homophily + + self.schedule = mesa.time.RandomActivation(self) + self.grid = Grid(width, height, torus=True) + + self.happy = 0 + self.datacollector = mesa.DataCollector( + {"happy": "happy"}, # Model-level count of happy agents + # For testing purposes, agent's individual x and y + # {"x": lambda a: a.pos.coords[0], "y": lambda a: a.pos.coords[1]}, + ) + + # Set up agents + # We use a grid iterator that returns + # the coordinates of a cell as well as + # its contents. (coord_iter) + for cell in self.grid: + if self.random.random() < self.density: + agent_type = 1 if self.random.random() < self.minority_pc else 0 + agent = SchellingAgent(None, self, agent_type, cell) + self.grid.move_agent(agent, cell.coords) + self.schedule.add(agent) + + self.running = True + self.datacollector.collect(self) + + def step(self): + """ + Run one step of the model. If All agents are happy, halt the model. + """ + self.happy = 0 # Reset counter of happy agents + self.schedule.step() + # collect data + # self.datacollector.collect(self) + + if self.happy == self.schedule.get_agent_count(): + self.running = False From 7b58d8eb79f372a2e28281d224c6c6776a524f7c Mon Sep 17 00:00:00 2001 From: Corvince Date: Tue, 23 Jan 2024 08:10:35 +0100 Subject: [PATCH 02/63] add features to cell --- mesa/agent.py | 5 +- mesa/gridspace.py | 157 +++++++++++++++++++++++----------------------- mesa/schelling.py | 7 +-- 3 files changed, 83 insertions(+), 86 deletions(-) diff --git a/mesa/agent.py b/mesa/agent.py index ecf0d33a0af..8f4296888da 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -48,6 +48,7 @@ def __init__(self, unique_id: int, model: Model) -> None: self.unique_id = unique_id self.model = model self.cell: Cell | None = None + self.pos: Position | None = None # register agent try: @@ -63,10 +64,6 @@ def __init__(self, unique_id: int, model: Model) -> None: stacklevel=2, ) - @property - def pos(self): - return self.cell.coords - def remove(self) -> None: """Remove and delete the agent from the model.""" with contextlib.suppress(KeyError): diff --git a/mesa/gridspace.py b/mesa/gridspace.py index d4ce654b3e3..28bcb9523f8 100644 --- a/mesa/gridspace.py +++ b/mesa/gridspace.py @@ -1,27 +1,27 @@ import itertools import random -from functools import cache -from typing import Any +from functools import cache, cached_property +from typing import Any, Callable, Optional -from mesa import Agent +from .agent import Agent -Coordinates = tuple[int, int] +Coordinate = tuple[int, int] def create_neighborhood_getter(moore=True, include_center=False, radius=1): @cache def of(cell: Cell): if radius == 0: - return {cell: cell.content} + return {cell: cell.agents} neighborhood = {} - for neighbor in cell.connections: + for neighbor in cell._connections: if ( moore - or neighbor.coords[0] == cell.coords[0] - or neighbor.coords[1] == cell.coords[1] + or neighbor.coordinate[0] == cell.coordinate[0] + or neighbor.coordinate[1] == cell.coordinate[1] ): - neighborhood[neighbor] = neighbor.content + neighborhood[neighbor] = neighbor.agents if radius > 1: for neighbor in list(neighborhood.keys()): @@ -40,125 +40,129 @@ def of(cell: Cell): class Cell: - __slots__ = ["coords", "connections", "content"] + __slots__ = ["coordinate", "_connections", "agents", "capacity", "properties"] - def __init__(self, i: int, j: int) -> None: - self.coords = (i, j) - self.connections: list[Cell] = [] - self.content: list[Agent] = [] + def __init__(self, i: int, j: int, capacity: int = 1) -> None: + self.coordinate = (i, j) + self._connections: list[Cell] = [] + self.agents: list[Agent] = [] + self.capacity = capacity + self.properties: dict[str, Any] = {} def connect(self, other) -> None: """Connects this cell to another cell.""" - self.connections.append(other) + self._connections.append(other) def disconnect(self, other) -> None: """Disconnects this cell from another cell.""" - self.connections.remove(other) + self._connections.remove(other) def add_agent(self, agent: Agent) -> None: """Adds an agent to the cell.""" - self.content.append(agent) + if len(self.agents) >= self.capacity: + raise Exception("ERROR: Cell is full") + if isinstance(agent.cell, Cell): + agent.cell.remove_agent(agent) + self.agents.append(agent) agent.cell = self def remove_agent(self, agent: Agent) -> None: """Removes an agent from the cell.""" - if agent in self.content: - self.content.remove(agent) + self.agents.remove(agent) + agent.cell = None + + @property + def is_empty(self) -> bool: + """Returns a bool of the contents of a cell.""" + return len(self.agents) == 0 + + @property + def is_full(self) -> bool: + """Returns a bool of the contents of a cell.""" + return len(self.agents) == self.capacity def __repr__(self): - return f"Cell({self.coords})" + return f"Cell({self.coordinate}, {self.agents})" class CellCollection: def __init__(self, cells: dict[Cell, list[Agent]]) -> None: - self.cells = cells + self._cells = cells def __iter__(self): - return iter(self.cells) + return iter(self._cells) def __getitem__(self, key): - return self.cells[key] + return self._cells[key] def __len__(self): - return len(self.cells) + return len(self._cells) def __repr__(self): - return f"CellCollection({self.cells})" + return f"CellCollection({self._cells})" + + @cached_property + def cells(self): + return list(self._cells.keys()) @property def agents(self): - return itertools.chain.from_iterable(self.cells.values()) + return itertools.chain.from_iterable(self._cells.values()) - def select_random(self): - return random.choice(list(self.cells.keys())) + def select_random_cell(self): + return random.choice(self.cells) + + def select_random_agent(self): + return random.choice(list(self.agents)) + + def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): + if filter_func is None and n == 0: + return self + + return CellCollection( + { + cell: agents + for cell, agents in self._cells.items() + if filter_func is None or filter_func(cell) + } + ) class Space: - cells: dict[Coordinates, Cell] + cells: dict[Coordinate, Cell] = {} - def _connect_single_cell(self, cell): # <= different for every concrete Space + def _connect_single_cell(self, cell): ... + @cached_property + def all_cells(self): + return CellCollection({cell: cell.agents for cell in self.cells.values()}) + def __iter__(self): return iter(self.cells.values()) - def get_neighborhood(self, coords: Coordinates, neighborhood_getter: Any): - return neighborhood_getter(self.cells[coords]) + def __getitem__(self, key): + return self.cells[key] def move_agent(self, agent: Agent, pos) -> None: """Move an agent from its current position to a new position.""" if (old_cell := agent.cell) is not None: old_cell.remove_agent(agent) - if self._empties_built: - self._empties.add(old_cell.coords) new_cell = self.cells[pos] new_cell.add_agent(agent) - if self._empties_built: - self._empties.discard(new_cell.coords) @property def empties(self) -> CellCollection: - if not self._empties_built: - self.build_empties() - - return CellCollection( - { - self.cells[coords]: self.cells[coords].content - for coords in sorted(self._empties) - } - ) - - def build_empties(self) -> None: - self._empties = set(filter(self.is_cell_empty, self.cells.keys())) - self._empties_built = True + return self.all_cells.select(lambda cell: cell.is_empty) 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 = self.empties.select_random().coords - self.move_agent(agent, new_pos) - - def is_cell_empty(self, pos) -> bool: - """Returns a bool of the contents of a cell.""" - return len(self.cells[pos].content) == 0 + # TODO: Add Heuristic for almost full grids + while True: + new_cell = self.all_cells.select_random_cell() + if new_cell.is_empty: + new_cell.add_agent(agent) + return class Grid(Space): @@ -175,7 +179,7 @@ def __init__(self, width: int, height: int, torus: bool = False) -> None: self._connect_single_cell(cell) def _connect_single_cell(self, cell): - i, j = cell.coords + i, j = cell.coordinate directions = [ (-1, -1), (-1, 0), @@ -192,6 +196,3 @@ def _connect_single_cell(self, cell): ni, nj = ni % self.height, nj % self.width if 0 <= ni < self.height and 0 <= nj < self.width: cell.connect(self.cells[ni, nj]) - - def get_neighborhood(self, coords, neighborhood_getter: Any) -> CellCollection: - return neighborhood_getter(self.cells[coords]) diff --git a/mesa/schelling.py b/mesa/schelling.py index 43aa90e3d3d..e01bdaf5c01 100644 --- a/mesa/schelling.py +++ b/mesa/schelling.py @@ -7,7 +7,7 @@ class SchellingAgent(mesa.Agent): Schelling segregation agent """ - def __init__(self, pos, model, agent_type, cell): + def __init__(self, pos, model, agent_type): """ Create a new Schelling agent. @@ -18,7 +18,6 @@ def __init__(self, pos, model, agent_type, cell): """ super().__init__(pos, model) self.pos = pos - self.cell = cell self.type = agent_type self.get_neighborhood = create_neighborhood_getter() @@ -66,8 +65,8 @@ def __init__(self, width=20, height=20, density=0.8, minority_pc=0.2, homophily= for cell in self.grid: if self.random.random() < self.density: agent_type = 1 if self.random.random() < self.minority_pc else 0 - agent = SchellingAgent(None, self, agent_type, cell) - self.grid.move_agent(agent, cell.coords) + agent = SchellingAgent(None, self, agent_type) + cell.add_agent(agent) self.schedule.add(agent) self.running = True From aef4a8bc6c0c596ac09e6f7310913e1e69723e3e Mon Sep 17 00:00:00 2001 From: Corvince Date: Tue, 23 Jan 2024 21:10:20 +0100 Subject: [PATCH 03/63] remove create_neighborhood_getter --- .../cell_space.py} | 129 +++++++++--------- 1 file changed, 68 insertions(+), 61 deletions(-) rename mesa/{gridspace.py => experimental/cell_space.py} (60%) diff --git a/mesa/gridspace.py b/mesa/experimental/cell_space.py similarity index 60% rename from mesa/gridspace.py rename to mesa/experimental/cell_space.py index 28bcb9523f8..ebac83827b4 100644 --- a/mesa/gridspace.py +++ b/mesa/experimental/cell_space.py @@ -1,53 +1,24 @@ import itertools import random +from collections.abc import Iterable from functools import cache, cached_property -from typing import Any, Callable, Optional +from typing import TYPE_CHECKING, Callable, Optional -from .agent import Agent +if TYPE_CHECKING: + from mesa import Agent Coordinate = tuple[int, int] -def create_neighborhood_getter(moore=True, include_center=False, radius=1): - @cache - def of(cell: Cell): - if radius == 0: - return {cell: cell.agents} - - neighborhood = {} - for neighbor in cell._connections: - if ( - moore - or neighbor.coordinate[0] == cell.coordinate[0] - or neighbor.coordinate[1] == cell.coordinate[1] - ): - neighborhood[neighbor] = neighbor.agents - - if radius > 1: - for neighbor in list(neighborhood.keys()): - neighborhood.update( - create_neighborhood_getter(moore, include_center, radius - 1)( - neighbor - ) - ) - - if not include_center: - neighborhood.pop(cell, None) - - return CellCollection(neighborhood) - - return of - - class Cell: __slots__ = ["coordinate", "_connections", "agents", "capacity", "properties"] - def __init__(self, i: int, j: int, capacity: int = 1) -> None: + def __init__(self, i: int, j: int, capacity: int | None = 1) -> None: self.coordinate = (i, j) self._connections: list[Cell] = [] self.agents: list[Agent] = [] self.capacity = capacity - self.properties: dict[str, Any] = {} + self.properties: dict[str, object] = {} def connect(self, other) -> None: """Connects this cell to another cell.""" @@ -59,7 +30,7 @@ def disconnect(self, other) -> None: def add_agent(self, agent: Agent) -> None: """Adds an agent to the cell.""" - if len(self.agents) >= self.capacity: + if self.capacity and len(self.agents) >= self.capacity: raise Exception("ERROR: Cell is full") if isinstance(agent.cell, Cell): agent.cell.remove_agent(agent) @@ -84,10 +55,29 @@ def is_full(self) -> bool: def __repr__(self): return f"Cell({self.coordinate}, {self.agents})" + @cache + def neighborhood(self, radius=1, include_center=False): + if radius == 0: + return {self: self.agents} + if radius == 1: + return CellCollection( + {neighbor: neighbor.agents for neighbor in self._connections} + ) + else: + neighborhood = {} + for neighbor in self._connections: + neighborhood.update(neighbor.neighorhood(radius - 1, include_center)) + if not include_center: + neighborhood.pop(self, None) + return CellCollection(neighborhood) + class CellCollection: - def __init__(self, cells: dict[Cell, list[Agent]]) -> None: - self._cells = cells + def __init__(self, cells: dict[Cell, list[Agent]] | Iterable[Cell]) -> None: + if isinstance(cells, dict): + self._cells = cells + else: + self._cells = {cell: cell.agents for cell in cells} def __iter__(self): return iter(self._cells) @@ -95,6 +85,7 @@ def __iter__(self): def __getitem__(self, key): return self._cells[key] + @cached_property def __len__(self): return len(self._cells) @@ -144,52 +135,68 @@ def __iter__(self): def __getitem__(self, key): return self.cells[key] - def move_agent(self, agent: Agent, pos) -> None: + def move_agent(self, agent: Agent, pos: Coordinate) -> None: """Move an agent from its current position to a new position.""" - if (old_cell := agent.cell) is not None: - old_cell.remove_agent(agent) - - new_cell = self.cells[pos] - new_cell.add_agent(agent) + self.cells[pos].add_agent(agent) @property def empties(self) -> CellCollection: return self.all_cells.select(lambda cell: cell.is_empty) def move_to_empty(self, agent: Agent) -> None: - # TODO: Add Heuristic for almost full grids while True: new_cell = self.all_cells.select_random_cell() if new_cell.is_empty: new_cell.add_agent(agent) return + # TODO: Adjust cutoff value for performance + for _ in range(len(self.all_cells) // 10): + new_cell = self.all_cells.select_random_cell() + if new_cell.is_empty: + new_cell.add_agent(agent) + return + + try: + self.empties.select_random_cell().add_agent(agent) + except IndexError as err: + raise Exception("ERROR: No empty cell found") from err + class Grid(Space): - def __init__(self, width: int, height: int, torus: bool = False) -> None: + def __init__( + self, width: int, height: int, torus: bool = False, moore=True, capacity=1 + ) -> None: self.width = width self.height = height self.torus = torus - self.cells = {(i, j): Cell(i, j) for j in range(width) for i in range(height)} - - self._empties_built = False - self.cutoff_empties = 7.953 * len(self.cells) ** 0.384 + self.moore = moore + self.capacity = capacity + self.cells = { + (i, j): Cell(i, j, capacity) for j in range(width) for i in range(height) + } - for cell in self.cells.values(): + for cell in self.all_cells: self._connect_single_cell(cell) def _connect_single_cell(self, cell): i, j = cell.coordinate - directions = [ - (-1, -1), - (-1, 0), - (-1, 1), - (0, -1), - (0, 1), - (1, -1), - (1, 0), - (1, 1), - ] + + # fmt: off + if self.moore: + directions = [ + (-1, -1), (-1, 0), (-1, 1), + ( 0, -1), ( 0, 1), + ( 1, -1), ( 1, 0), ( 1, 1), + ] + else: # Von Neumann neighborhood + directions = [ + (-1, 0), + ( 0, -1), (0, 1), + ( 1, 0), + ] + # fmt: on + for di, dj in directions: ni, nj = (i + di, j + dj) if self.torus: From 3240a2b7984b4aca41df40e94ab693ee733f2b7f Mon Sep 17 00:00:00 2001 From: Corvince Date: Tue, 23 Jan 2024 21:34:29 +0100 Subject: [PATCH 04/63] update benchmark models --- benchmarks/Schelling/schelling.py | 12 +++++------- benchmarks/WolfSheep/agents.py | 25 +++++++++++-------------- benchmarks/WolfSheep/random_walk.py | 7 +++---- benchmarks/WolfSheep/wolf_sheep.py | 28 +++++++++++----------------- mesa/experimental/__init__.py | 1 + mesa/experimental/cell_space.py | 3 +-- 6 files changed, 32 insertions(+), 44 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 3f9e7567e45..1935f148382 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,6 +1,6 @@ from mesa import Agent, Model -from mesa.space import SingleGrid from mesa.time import RandomActivation +from mesa.experimental import Grid class SchellingAgent(Agent): @@ -21,9 +21,7 @@ def __init__(self, unique_id, model, agent_type): def step(self): similar = 0 - for neighbor in self.model.grid.iter_neighbors( - self.pos, moore=True, radius=self.model.radius - ): + for neighbor in self.cell.neighborhood().agents: if neighbor.type == self.type: similar += 1 @@ -52,7 +50,7 @@ def __init__( self.radius = radius self.schedule = RandomActivation(self) - self.grid = SingleGrid(height, width, torus=True) + self.grid = Grid(height, width, torus=True) self.happy = 0 @@ -60,11 +58,11 @@ def __init__( # We use a grid iterator that returns # the coordinates of a cell as well as # its contents. (coord_iter) - for _cont, pos in self.grid.coord_iter(): + for cell in self.grid: if self.random.random() < self.density: agent_type = 1 if self.random.random() < self.minority_pc else 0 agent = SchellingAgent(self.next_id(), self, agent_type) - self.grid.place_agent(agent, pos) + cell.add_agent(agent) self.schedule.add(agent) def step(self): diff --git a/benchmarks/WolfSheep/agents.py b/benchmarks/WolfSheep/agents.py index 9ad9f7652cc..77f2c978ba7 100644 --- a/benchmarks/WolfSheep/agents.py +++ b/benchmarks/WolfSheep/agents.py @@ -24,23 +24,24 @@ def step(self): self.energy -= 1 # If there is grass available, eat it - this_cell = self.model.grid.get_cell_list_contents(self.pos) - grass_patch = next(obj for obj in this_cell if isinstance(obj, GrassPatch)) + grass_patch = next( + obj for obj in self.cell.agents if isinstance(obj, GrassPatch) + ) if grass_patch.fully_grown: self.energy += self.model.sheep_gain_from_food grass_patch.fully_grown = False # Death if self.energy < 0: - self.model.grid.remove_agent(self) + self.cell.remove_agent(self) self.model.schedule.remove(self) elif self.random.random() < self.model.sheep_reproduce: # Create a new sheep: self.energy /= 2 lamb = Sheep( - self.model.next_id(), self.pos, self.model, self.moore, self.energy + self.model.next_id(), None, self.model, self.moore, self.energy ) - self.model.grid.place_agent(lamb, self.pos) + self.cell.add_agent(lamb) self.model.schedule.add(lamb) @@ -58,28 +59,24 @@ def step(self): self.energy -= 1 # If there are sheep present, eat one - x, y = self.pos - this_cell = self.model.grid.get_cell_list_contents([self.pos]) - sheep = [obj for obj in this_cell if isinstance(obj, Sheep)] + sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)] if len(sheep) > 0: sheep_to_eat = self.random.choice(sheep) self.energy += self.model.wolf_gain_from_food # Kill the sheep - self.model.grid.remove_agent(sheep_to_eat) + self.cell.remove_agent(sheep_to_eat) self.model.schedule.remove(sheep_to_eat) # Death or reproduction if self.energy < 0: - self.model.grid.remove_agent(self) + self.cell.remove_agent(self) self.model.schedule.remove(self) elif self.random.random() < self.model.wolf_reproduce: # Create a new wolf cub self.energy /= 2 - cub = Wolf( - self.model.next_id(), self.pos, self.model, self.moore, self.energy - ) - self.model.grid.place_agent(cub, cub.pos) + cub = Wolf(self.model.next_id(), None, self.model, self.moore, self.energy) + self.cell.add_agent(cub) self.model.schedule.add(cub) diff --git a/benchmarks/WolfSheep/random_walk.py b/benchmarks/WolfSheep/random_walk.py index e55a1f7ed2b..755a53e34bf 100644 --- a/benchmarks/WolfSheep/random_walk.py +++ b/benchmarks/WolfSheep/random_walk.py @@ -3,9 +3,11 @@ """ from mesa import Agent +from mesa.experimental import Cell class RandomWalker(Agent): + cell: Cell """ Class implementing random walker methods in a generalized manner. @@ -31,7 +33,4 @@ def random_move(self): Step one cell in any allowable direction. """ # Pick the next cell from the adjacent cells. - next_moves = self.model.grid.get_neighborhood(self.pos, self.moore, True) - next_move = self.random.choice(next_moves) - # Now move: - self.model.grid.move_agent(self, next_move) + self.cell.neighborhood().select_random_cell().add_agent(self) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index b08afbc57ba..5e56a747293 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -10,7 +10,7 @@ """ import mesa -from mesa.space import MultiGrid +from mesa.experimental import Grid from mesa.time import RandomActivationByType from .agents import GrassPatch, Sheep, Wolf @@ -63,40 +63,34 @@ def __init__( self.sheep_gain_from_food = sheep_gain_from_food self.schedule = RandomActivationByType(self) - self.grid = MultiGrid(self.height, self.width, torus=False) + self.grid = Grid(self.height, self.width, torus=False, capacity=None) # Create sheep: for _i in range(self.initial_sheep): - pos = ( - self.random.randrange(self.width), - self.random.randrange(self.height), - ) + cell = self.grid.all_cells.select_random_cell() energy = self.random.randrange(2 * self.sheep_gain_from_food) - sheep = Sheep(self.next_id(), pos, self, True, energy) - self.grid.place_agent(sheep, pos) + sheep = Sheep(self.next_id(), None, self, True, energy) + cell.add_agent(sheep) self.schedule.add(sheep) # Create wolves for _i in range(self.initial_wolves): - pos = ( - self.random.randrange(self.width), - self.random.randrange(self.height), - ) + cell = self.grid.all_cells.select_random_cell() energy = self.random.randrange(2 * self.wolf_gain_from_food) - wolf = Wolf(self.next_id(), pos, self, True, energy) - self.grid.place_agent(wolf, pos) + wolf = Wolf(self.next_id(), None, self, True, energy) + cell.add_agent(wolf) self.schedule.add(wolf) # Create grass patches possibly_fully_grown = [True, False] - for _agent, pos in self.grid.coord_iter(): + for cell in self.grid: fully_grown = self.random.choice(possibly_fully_grown) if fully_grown: countdown = self.grass_regrowth_time else: countdown = self.random.randrange(self.grass_regrowth_time) - patch = GrassPatch(self.next_id(), pos, self, fully_grown, countdown) - self.grid.place_agent(patch, pos) + patch = GrassPatch(self.next_id(), None, self, fully_grown, countdown) + cell.add_agent(patch) self.schedule.add(patch) def step(self): diff --git a/mesa/experimental/__init__.py b/mesa/experimental/__init__.py index 964dc5d19a3..81d92de12c3 100644 --- a/mesa/experimental/__init__.py +++ b/mesa/experimental/__init__.py @@ -1 +1,2 @@ from .jupyter_viz import JupyterViz, make_text # noqa +from .cell_space import Cell, Grid, CellCollection, Space # noqa diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index ebac83827b4..49e981f9e06 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -4,8 +4,7 @@ from functools import cache, cached_property from typing import TYPE_CHECKING, Callable, Optional -if TYPE_CHECKING: - from mesa import Agent +from .. import Agent Coordinate = tuple[int, int] From d3df44e33040ddad7bd9d34b1bfe39add1a1a07f Mon Sep 17 00:00:00 2001 From: Corvince Date: Tue, 23 Jan 2024 22:02:06 +0100 Subject: [PATCH 05/63] try to import Grid directly from experimental --- benchmarks/Schelling/schelling.py | 2 +- benchmarks/WolfSheep/random_walk.py | 2 +- benchmarks/WolfSheep/wolf_sheep.py | 2 +- mesa/experimental/__init__.py | 1 - mesa/experimental/cell_space.py | 2 +- mesa/experimental/components/matplotlib.py | 4 +- mesa/schelling.py | 85 ---------------------- 7 files changed, 6 insertions(+), 92 deletions(-) delete mode 100644 mesa/schelling.py diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 1935f148382..2983f23e4d9 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,6 +1,6 @@ from mesa import Agent, Model +from mesa.experimental.cell_space import Grid from mesa.time import RandomActivation -from mesa.experimental import Grid class SchellingAgent(Agent): diff --git a/benchmarks/WolfSheep/random_walk.py b/benchmarks/WolfSheep/random_walk.py index 755a53e34bf..e13b52917b2 100644 --- a/benchmarks/WolfSheep/random_walk.py +++ b/benchmarks/WolfSheep/random_walk.py @@ -3,7 +3,7 @@ """ from mesa import Agent -from mesa.experimental import Cell +from mesa.experimental.cell_space import Cell class RandomWalker(Agent): diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 5e56a747293..472e55b59b2 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -10,7 +10,7 @@ """ import mesa -from mesa.experimental import Grid +from mesa.experimental.cell_space import Grid from mesa.time import RandomActivationByType from .agents import GrassPatch, Sheep, Wolf diff --git a/mesa/experimental/__init__.py b/mesa/experimental/__init__.py index 81d92de12c3..964dc5d19a3 100644 --- a/mesa/experimental/__init__.py +++ b/mesa/experimental/__init__.py @@ -1,2 +1 @@ from .jupyter_viz import JupyterViz, make_text # noqa -from .cell_space import Cell, Grid, CellCollection, Space # noqa diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 49e981f9e06..039b4649255 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -2,7 +2,7 @@ import random from collections.abc import Iterable from functools import cache, cached_property -from typing import TYPE_CHECKING, Callable, Optional +from typing import Callable, Optional from .. import Agent diff --git a/mesa/experimental/components/matplotlib.py b/mesa/experimental/components/matplotlib.py index d67a0862cc5..b688e07f8f7 100644 --- a/mesa/experimental/components/matplotlib.py +++ b/mesa/experimental/components/matplotlib.py @@ -93,12 +93,12 @@ def portray(space): return out # Determine border style based on space.torus - border_style = 'solid' if not space.torus else (0, (5, 10)) + border_style = "solid" if not space.torus else (0, (5, 10)) # Set the border of the plot for spine in space_ax.spines.values(): spine.set_linewidth(1.5) - spine.set_color('black') + spine.set_color("black") spine.set_linestyle(border_style) width = space.x_max - space.x_min diff --git a/mesa/schelling.py b/mesa/schelling.py deleted file mode 100644 index e01bdaf5c01..00000000000 --- a/mesa/schelling.py +++ /dev/null @@ -1,85 +0,0 @@ -import mesa -from mesa.gridspace import Grid, create_neighborhood_getter - - -class SchellingAgent(mesa.Agent): - """ - Schelling segregation agent - """ - - def __init__(self, pos, model, agent_type): - """ - Create a new Schelling agent. - - Args: - unique_id: Unique identifier for the agent. - x, y: Agent initial location. - agent_type: Indicator for the agent's type (minority=1, majority=0) - """ - super().__init__(pos, model) - self.pos = pos - self.type = agent_type - self.get_neighborhood = create_neighborhood_getter() - - def step(self): - similar = 0 - for neighbor in self.get_neighborhood(self.cell).agents: - if neighbor.type == self.type: - similar += 1 - - # If unhappy, move: - if similar < self.model.homophily: - self.model.grid.move_to_empty(self) - else: - self.model.happy += 1 - - -class Schelling(mesa.Model): - """ - Model class for the Schelling segregation model. - """ - - def __init__(self, width=20, height=20, density=0.8, minority_pc=0.2, homophily=3): - """ """ - - self.width = width - self.height = height - self.density = density - self.minority_pc = minority_pc - self.homophily = homophily - - self.schedule = mesa.time.RandomActivation(self) - self.grid = Grid(width, height, torus=True) - - self.happy = 0 - self.datacollector = mesa.DataCollector( - {"happy": "happy"}, # Model-level count of happy agents - # For testing purposes, agent's individual x and y - # {"x": lambda a: a.pos.coords[0], "y": lambda a: a.pos.coords[1]}, - ) - - # Set up agents - # We use a grid iterator that returns - # the coordinates of a cell as well as - # its contents. (coord_iter) - for cell in self.grid: - if self.random.random() < self.density: - agent_type = 1 if self.random.random() < self.minority_pc else 0 - agent = SchellingAgent(None, self, agent_type) - cell.add_agent(agent) - self.schedule.add(agent) - - self.running = True - self.datacollector.collect(self) - - def step(self): - """ - Run one step of the model. If All agents are happy, halt the model. - """ - self.happy = 0 # Reset counter of happy agents - self.schedule.step() - # collect data - # self.datacollector.collect(self) - - if self.happy == self.schedule.get_agent_count(): - self.running = False From c80a0bd9b0ed331e15c495ebdb90459bc783a14b Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 24 Jan 2024 19:15:09 +0100 Subject: [PATCH 06/63] adds a pythonic WolfSheep implementation --- benchmarks/WolfSheep/agents.py | 108 ----------------- benchmarks/WolfSheep/random_walk.py | 36 ------ benchmarks/WolfSheep/wolf_sheep.py | 172 +++++++++++++++++++++++----- 3 files changed, 142 insertions(+), 174 deletions(-) delete mode 100644 benchmarks/WolfSheep/agents.py delete mode 100644 benchmarks/WolfSheep/random_walk.py diff --git a/benchmarks/WolfSheep/agents.py b/benchmarks/WolfSheep/agents.py deleted file mode 100644 index 77f2c978ba7..00000000000 --- a/benchmarks/WolfSheep/agents.py +++ /dev/null @@ -1,108 +0,0 @@ -from mesa import Agent - -from .random_walk import RandomWalker - - -class Sheep(RandomWalker): - """ - A sheep that walks around, reproduces (asexually) and gets eaten. - - The init is the same as the RandomWalker. - """ - - def __init__(self, unique_id, pos, model, moore, energy=None): - super().__init__(unique_id, pos, model, moore=moore) - self.energy = energy - - def step(self): - """ - A model step. Move, then eat grass and reproduce. - """ - self.random_move() - - # Reduce energy - self.energy -= 1 - - # If there is grass available, eat it - grass_patch = next( - obj for obj in self.cell.agents if isinstance(obj, GrassPatch) - ) - if grass_patch.fully_grown: - self.energy += self.model.sheep_gain_from_food - grass_patch.fully_grown = False - - # Death - if self.energy < 0: - self.cell.remove_agent(self) - self.model.schedule.remove(self) - elif self.random.random() < self.model.sheep_reproduce: - # Create a new sheep: - self.energy /= 2 - lamb = Sheep( - self.model.next_id(), None, self.model, self.moore, self.energy - ) - self.cell.add_agent(lamb) - self.model.schedule.add(lamb) - - -class Wolf(RandomWalker): - """ - A wolf that walks around, reproduces (asexually) and eats sheep. - """ - - def __init__(self, unique_id, pos, model, moore, energy=None): - super().__init__(unique_id, pos, model, moore=moore) - self.energy = energy - - def step(self): - self.random_move() - self.energy -= 1 - - # If there are sheep present, eat one - sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)] - if len(sheep) > 0: - sheep_to_eat = self.random.choice(sheep) - self.energy += self.model.wolf_gain_from_food - - # Kill the sheep - self.cell.remove_agent(sheep_to_eat) - self.model.schedule.remove(sheep_to_eat) - - # Death or reproduction - if self.energy < 0: - self.cell.remove_agent(self) - self.model.schedule.remove(self) - elif self.random.random() < self.model.wolf_reproduce: - # Create a new wolf cub - self.energy /= 2 - cub = Wolf(self.model.next_id(), None, self.model, self.moore, self.energy) - self.cell.add_agent(cub) - self.model.schedule.add(cub) - - -class GrassPatch(Agent): - """ - A patch of grass that grows at a fixed rate and it is eaten by sheep - """ - - def __init__(self, unique_id, pos, model, fully_grown, countdown): - """ - Creates a new patch of grass - - Args: - grown: (boolean) Whether the patch of grass is fully grown or not - countdown: Time for the patch of grass to be fully grown again - """ - super().__init__(unique_id, model) - self.fully_grown = fully_grown - self.countdown = countdown - self.pos = pos - - def step(self): - if not self.fully_grown: - if self.countdown <= 0: - # Set as fully grown - self.fully_grown = True - self.countdown = self.model.grass_regrowth_time - else: - self.countdown -= 1 diff --git a/benchmarks/WolfSheep/random_walk.py b/benchmarks/WolfSheep/random_walk.py deleted file mode 100644 index e13b52917b2..00000000000 --- a/benchmarks/WolfSheep/random_walk.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Generalized behavior for random walking, one grid cell at a time. -""" - -from mesa import Agent -from mesa.experimental.cell_space import Cell - - -class RandomWalker(Agent): - cell: Cell - """ - Class implementing random walker methods in a generalized manner. - - Not intended to be used on its own, but to inherit its methods to multiple - other agents. - - """ - - def __init__(self, unique_id, pos, model, moore=True): - """ - grid: The MultiGrid object in which the agent lives. - x: The agent's current x coordinate - y: The agent's current y coordinate - moore: If True, may move in all 8 directions. - Otherwise, only up, down, left, right. - """ - super().__init__(unique_id, model) - self.pos = pos - self.moore = moore - - def random_move(self): - """ - Step one cell in any allowable direction. - """ - # Pick the next cell from the adjacent cells. - self.cell.neighborhood().select_random_cell().add_agent(self) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 472e55b59b2..7b9aadd2e9c 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -8,15 +8,112 @@ Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. """ +import math -import mesa +from mesa import Model, Agent from mesa.experimental.cell_space import Grid from mesa.time import RandomActivationByType -from .agents import GrassPatch, Sheep, Wolf +class Animal(Agent): -class WolfSheep(mesa.Model): + def __init__(self, unique_id, model, energy, p_reproduce, energy_from_food): + super().__init__(unique_id, model) + self.energy = energy + self.p_reproduce = p_reproduce + self.energy_from_food = energy_from_food + + def random_move(self): + new_cell = self.cell.neighborhood().select_random_cell() + self.cell.remove_agent(self) + new_cell.add_agent(self) + + def spawn_offspring(self): + self.energy /= 2 + offspring = self.__class__( + self.model.next_id(), self.model, self.energy, self.p_reproduce, self.energy_from_food + ) + self.cell.add_agent(offspring) + self.model.schedule.add(offspring) + + def feed(self): + ... + + def die(self): + self.cell.remove_agent(self) + self.remove() + + def step(self): + self.random_move() + self.energy -= 1 + + self.feed() + + if self.energy < 0: + self.die() + elif self.random.random() < self.p_reproduce: + self.spawn_offspring() + + +class Sheep(Animal): + """ + A sheep that walks around, reproduces (asexually) and gets eaten. + + The init is the same as the RandomWalker. + """ + + def feed(self): + # If there is grass available, eat it + this_cell = self.cell.agents + grass_patch = next(obj for obj in this_cell if isinstance(obj, GrassPatch)) + if grass_patch.fully_grown: + self.energy += self.energy_from_food + grass_patch.fully_grown = False + +class Wolf(Animal): + """ + A wolf that walks around, reproduces (asexually) and eats sheep. + """ + + def feed(self): + this_cell = self.cell.agents + sheep = [obj for obj in this_cell if isinstance(obj, Sheep)] + if len(sheep) > 0: + sheep_to_eat = self.random.choice(sheep) + self.energy += self.energy + + # Kill the sheep + sheep_to_eat.die() + + +class GrassPatch(Agent): + """ + A patch of grass that grows at a fixed rate and it is eaten by sheep + """ + + def __init__(self, unique_id, model, fully_grown, countdown): + """ + Creates a new patch of grass + + Args: + grown: (boolean) Whether the patch of grass is fully grown or not + countdown: Time for the patch of grass to be fully grown again + """ + super().__init__(unique_id, model) + self.fully_grown = fully_grown + self.countdown = countdown + + def step(self): + if not self.fully_grown: + if self.countdown <= 0: + # Set as fully grown + self.fully_grown = True + self.countdown = self.model.grass_regrowth_time + else: + self.countdown -= 1 + + +class WolfSheep(Model): """ Wolf-Sheep Predation Model @@ -24,17 +121,18 @@ class WolfSheep(mesa.Model): """ def __init__( - self, - seed, - height, - width, - initial_sheep, - initial_wolves, - sheep_reproduce, - wolf_reproduce, - grass_regrowth_time, - wolf_gain_from_food=13, - sheep_gain_from_food=5, + self, + seed, + height, + width, + initial_sheep, + initial_wolves, + sheep_reproduce, + wolf_reproduce, + grass_regrowth_time, + wolf_gain_from_food=13, + sheep_gain_from_food=5, + moore=False ): """ Create a new Wolf-Sheep model with the given parameters. @@ -49,6 +147,7 @@ def __init__( grass_regrowth_time: How long it takes for a grass patch to regrow once it is eaten sheep_gain_from_food: Energy sheep gain from grass, if enabled. + moore: """ super().__init__(seed=seed) # Set parameters @@ -56,29 +155,31 @@ def __init__( self.width = width self.initial_sheep = initial_sheep self.initial_wolves = initial_wolves - self.sheep_reproduce = sheep_reproduce - self.wolf_reproduce = wolf_reproduce - self.wolf_gain_from_food = wolf_gain_from_food self.grass_regrowth_time = grass_regrowth_time - self.sheep_gain_from_food = sheep_gain_from_food self.schedule = RandomActivationByType(self) - self.grid = Grid(self.height, self.width, torus=False, capacity=None) + self.grid = Grid(self.height, self.width, moore=moore, torus=False, capacity=math.inf) # Create sheep: - for _i in range(self.initial_sheep): - cell = self.grid.all_cells.select_random_cell() - energy = self.random.randrange(2 * self.sheep_gain_from_food) - sheep = Sheep(self.next_id(), None, self, True, energy) - cell.add_agent(sheep) + for _ in range(self.initial_sheep): + pos = ( + self.random.randrange(self.width), + self.random.randrange(self.height), + ) + energy = self.random.randrange(2 * sheep_gain_from_food) + sheep = Sheep(self.next_id(), self, energy, sheep_reproduce, sheep_gain_from_food) + self.grid.cells[pos].add_agent(sheep) self.schedule.add(sheep) # Create wolves - for _i in range(self.initial_wolves): - cell = self.grid.all_cells.select_random_cell() - energy = self.random.randrange(2 * self.wolf_gain_from_food) - wolf = Wolf(self.next_id(), None, self, True, energy) - cell.add_agent(wolf) + for _ in range(self.initial_wolves): + pos = ( + self.random.randrange(self.width), + self.random.randrange(self.height), + ) + energy = self.random.randrange(2 * wolf_gain_from_food) + wolf = Wolf(self.next_id(), self, energy, wolf_reproduce, wolf_gain_from_food) + self.grid.cells[pos].add_agent(wolf) self.schedule.add(wolf) # Create grass patches @@ -89,9 +190,20 @@ def __init__( countdown = self.grass_regrowth_time else: countdown = self.random.randrange(self.grass_regrowth_time) - patch = GrassPatch(self.next_id(), None, self, fully_grown, countdown) + patch = GrassPatch(self.next_id(), self, fully_grown, countdown) cell.add_agent(patch) self.schedule.add(patch) def step(self): self.schedule.step() + + +if __name__ == "__main__": + import time + + model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20) + + start_time = time.perf_counter() + for _ in range(100): + model.step() + print("Time:", time.perf_counter() - start_time) From 93d7db5af0668931687ea2a3a9f1a03fcbbdff8e Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 24 Jan 2024 19:16:16 +0100 Subject: [PATCH 07/63] replace radius check with value error in neighborhood the recursion will never call with radius 0, because if radius is 1 you return a cellcollection rather than calling the content of the collection --- mesa/experimental/cell_space.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 039b4649255..80237344ab0 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -56,8 +56,10 @@ def __repr__(self): @cache def neighborhood(self, radius=1, include_center=False): - if radius == 0: - return {self: self.agents} + # if radius == 0: + # return {self: self.agents} + if radius < 1: + raise ValueError("radius must be larger than one") if radius == 1: return CellCollection( {neighbor: neighbor.agents for neighbor in self._connections} From be7b843e98677e27541ea949599ae20db0ba7672 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 24 Jan 2024 19:22:43 +0100 Subject: [PATCH 08/63] minor further code cleanup of wolfsheep --- benchmarks/WolfSheep/wolf_sheep.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 7b9aadd2e9c..98d7235c962 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -64,8 +64,7 @@ class Sheep(Animal): def feed(self): # If there is grass available, eat it - this_cell = self.cell.agents - grass_patch = next(obj for obj in this_cell if isinstance(obj, GrassPatch)) + grass_patch = next(obj for obj in self.cell.agents if isinstance(obj, GrassPatch)) if grass_patch.fully_grown: self.energy += self.energy_from_food grass_patch.fully_grown = False @@ -76,8 +75,7 @@ class Wolf(Animal): """ def feed(self): - this_cell = self.cell.agents - sheep = [obj for obj in this_cell if isinstance(obj, Sheep)] + sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)] if len(sheep) > 0: sheep_to_eat = self.random.choice(sheep) self.energy += self.energy From bd788ecce13b8b5a5a75f0344dee76fb6cede191 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 24 Jan 2024 19:22:48 +0100 Subject: [PATCH 09/63] add HexGrid --- mesa/experimental/cell_space.py | 43 ++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 80237344ab0..f4af3e4c9c7 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -166,7 +166,7 @@ def move_to_empty(self, agent: Agent) -> None: class Grid(Space): def __init__( - self, width: int, height: int, torus: bool = False, moore=True, capacity=1 + self, width: int, height: int, torus: bool = False, moore: bool = True, capacity=1 ) -> None: self.width = width self.height = height @@ -204,3 +204,44 @@ def _connect_single_cell(self, cell): ni, nj = ni % self.height, nj % self.width if 0 <= ni < self.height and 0 <= nj < self.width: cell.connect(self.cells[ni, nj]) + + +class HexGrid(Space): + def __init__( + self, width: int, height: int, torus: bool = False, capacity=1 + ) -> None: + self.width = width + self.height = height + self.torus = torus + self.capacity = capacity + self.cells = { + (i, j): Cell(i, j, capacity) for j in range(width) for i in range(height) + } + + for cell in self.all_cells: + self._connect_single_cell(cell) + + def _connect_single_cell(self, cell): + i, j = cell.coordinate + + # fmt: off + if i%2 == 0: + directions = [ + (-1, -1), (-1, 0), + (0, -1), (0, 1), + ( 1, -1), ( 1, 0), + ] + else: + directions = [ + (-1, 0), (-1, 1), + (0, -1), (0, 1), + ( 1, 0), ( 1, 1), + ] + # fmt: on + + for di, dj in directions: + ni, nj = (i + di, j + dj) + if self.torus: + ni, nj = ni % self.height, nj % self.width + if 0 <= ni < self.height and 0 <= nj < self.width: + cell.connect(self.cells[ni, nj]) From cb0ef18b1da403ee9bc999b4f8a869d8b1798fb2 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 24 Jan 2024 19:26:07 +0100 Subject: [PATCH 10/63] typo fix --- mesa/experimental/cell_space.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index f4af3e4c9c7..2727f19e819 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -67,7 +67,7 @@ def neighborhood(self, radius=1, include_center=False): else: neighborhood = {} for neighbor in self._connections: - neighborhood.update(neighbor.neighorhood(radius - 1, include_center)) + neighborhood.update(neighbor.neighborhood(radius - 1, include_center)) if not include_center: neighborhood.pop(self, None) return CellCollection(neighborhood) From 94f06b17ca8b35be913a87d033b79bfa496ef3b0 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 27 Jan 2024 20:42:33 +0100 Subject: [PATCH 11/63] various updates * add cell agen * add move_to method to CellAgent * rename to space to discrete space * placeholder for random * revert agent in agent.py back to old version to isolate experimental features --- benchmarks/Schelling/schelling.py | 10 ++--- benchmarks/WolfSheep/wolf_sheep.py | 20 ++++----- mesa/agent.py | 3 +- mesa/experimental/cell_space.py | 72 +++++++++++++++++++++--------- 4 files changed, 67 insertions(+), 38 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 2983f23e4d9..af9caba3721 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,9 +1,9 @@ -from mesa import Agent, Model -from mesa.experimental.cell_space import Grid +from mesa import Model +from mesa.experimental.cell_space import Grid, CellAgent from mesa.time import RandomActivation -class SchellingAgent(Agent): +class SchellingAgent(CellAgent): """ Schelling segregation agent """ @@ -27,7 +27,7 @@ def step(self): # If unhappy, move: if similar < self.model.homophily: - self.model.grid.move_to_empty(self) + self.move_to(self.model.grid.select_random_empty(self)) else: self.model.happy += 1 @@ -62,7 +62,7 @@ def __init__( if self.random.random() < self.density: agent_type = 1 if self.random.random() < self.minority_pc else 0 agent = SchellingAgent(self.next_id(), self, agent_type) - cell.add_agent(agent) + agent.move_to(cell) self.schedule.add(agent) def step(self): diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 98d7235c962..488f4ead762 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -10,12 +10,12 @@ """ import math -from mesa import Model, Agent -from mesa.experimental.cell_space import Grid +from mesa import Model +from mesa.experimental.cell_space import Grid, CellAgent from mesa.time import RandomActivationByType -class Animal(Agent): +class Animal(CellAgent): def __init__(self, unique_id, model, energy, p_reproduce, energy_from_food): super().__init__(unique_id, model) @@ -24,16 +24,14 @@ def __init__(self, unique_id, model, energy, p_reproduce, energy_from_food): self.energy_from_food = energy_from_food def random_move(self): - new_cell = self.cell.neighborhood().select_random_cell() - self.cell.remove_agent(self) - new_cell.add_agent(self) + self.move_to(self.cell.neighborhood().select_random_cell()) def spawn_offspring(self): self.energy /= 2 offspring = self.__class__( self.model.next_id(), self.model, self.energy, self.p_reproduce, self.energy_from_food ) - self.cell.add_agent(offspring) + offspring.move_to(self.cell) self.model.schedule.add(offspring) def feed(self): @@ -84,7 +82,7 @@ def feed(self): sheep_to_eat.die() -class GrassPatch(Agent): +class GrassPatch(CellAgent): """ A patch of grass that grows at a fixed rate and it is eaten by sheep """ @@ -166,7 +164,7 @@ def __init__( ) energy = self.random.randrange(2 * sheep_gain_from_food) sheep = Sheep(self.next_id(), self, energy, sheep_reproduce, sheep_gain_from_food) - self.grid.cells[pos].add_agent(sheep) + sheep.move_to(self.grid.cells[pos]) self.schedule.add(sheep) # Create wolves @@ -177,7 +175,7 @@ def __init__( ) energy = self.random.randrange(2 * wolf_gain_from_food) wolf = Wolf(self.next_id(), self, energy, wolf_reproduce, wolf_gain_from_food) - self.grid.cells[pos].add_agent(wolf) + wolf.move_to(self.grid.cells[pos]) self.schedule.add(wolf) # Create grass patches @@ -189,7 +187,7 @@ def __init__( else: countdown = self.random.randrange(self.grass_regrowth_time) patch = GrassPatch(self.next_id(), self, fully_grown, countdown) - cell.add_agent(patch) + patch.move_to(cell) self.schedule.add(patch) def step(self): diff --git a/mesa/agent.py b/mesa/agent.py index 8f4296888da..1a6416ab866 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -47,7 +47,6 @@ def __init__(self, unique_id: int, model: Model) -> None: """ self.unique_id = unique_id self.model = model - self.cell: Cell | None = None self.pos: Position | None = None # register agent @@ -80,6 +79,8 @@ def random(self) -> Random: return self.model.random + + class AgentSet(MutableSet, Sequence): """ .. warning:: diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 2727f19e819..8ae0d914001 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -1,14 +1,46 @@ import itertools -import random +from random import Random from collections.abc import Iterable from functools import cache, cached_property from typing import Callable, Optional -from .. import Agent +from .. import Agent, Model Coordinate = tuple[int, int] +class CellAgent(Agent): + """ + Base class for a model agent in Mesa. + + Attributes: + unique_id (int): A unique identifier for this agent. + model (Model): A reference to the model instance. + self.pos: Position | None = None + """ + + def __init__(self, unique_id: int, 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__(unique_id, model) + self.cell: Cell | None = None + + @property + def random(self) -> Random: + return self.model.random + + def move_to(self, cell) -> None: + if self.cell is not None: + self.cell.remove_agent(self) + self.cell = cell + cell.add_agent(self) + + class Cell: __slots__ = ["coordinate", "_connections", "agents", "capacity", "properties"] @@ -31,10 +63,7 @@ def add_agent(self, agent: Agent) -> None: """Adds an agent to the cell.""" if self.capacity and len(self.agents) >= self.capacity: raise Exception("ERROR: Cell is full") - if isinstance(agent.cell, Cell): - agent.cell.remove_agent(agent) self.agents.append(agent) - agent.cell = self def remove_agent(self, agent: Agent) -> None: """Removes an agent from the cell.""" @@ -56,22 +85,23 @@ def __repr__(self): @cache def neighborhood(self, radius=1, include_center=False): + return CellCollection(self._neighborhood(radius=radius, include_center=include_center)) + + @cache + def _neighborhood(self, radius=1, include_center=False): # if radius == 0: # return {self: self.agents} if radius < 1: raise ValueError("radius must be larger than one") if radius == 1: - return CellCollection( - {neighbor: neighbor.agents for neighbor in self._connections} - ) + return {neighbor: neighbor.agents for neighbor in self._connections} else: neighborhood = {} for neighbor in self._connections: - neighborhood.update(neighbor.neighborhood(radius - 1, include_center)) + neighborhood.update(neighbor._neighborhood(radius - 1, include_center)) if not include_center: neighborhood.pop(self, None) - return CellCollection(neighborhood) - + return neighborhood class CellCollection: def __init__(self, cells: dict[Cell, list[Agent]] | Iterable[Cell]) -> None: @@ -79,6 +109,7 @@ def __init__(self, cells: dict[Cell, list[Agent]] | Iterable[Cell]) -> None: self._cells = cells else: self._cells = {cell: cell.agents for cell in cells} + self.random = Random() # FIXME def __iter__(self): return iter(self._cells) @@ -102,10 +133,10 @@ def agents(self): return itertools.chain.from_iterable(self._cells.values()) def select_random_cell(self): - return random.choice(self.cells) + return self.random.choice(self.cells) def select_random_agent(self): - return random.choice(list(self.agents)) + return self.random.choice(list(self.agents)) def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): if filter_func is None and n == 0: @@ -120,7 +151,7 @@ def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): ) -class Space: +class DiscreteSpace: cells: dict[Coordinate, Cell] = {} def _connect_single_cell(self, cell): @@ -144,12 +175,11 @@ def move_agent(self, agent: Agent, pos: Coordinate) -> None: def empties(self) -> CellCollection: return self.all_cells.select(lambda cell: cell.is_empty) - def move_to_empty(self, agent: Agent) -> None: + def select_random_empty(self, agent: Agent) -> None: while True: - new_cell = self.all_cells.select_random_cell() - if new_cell.is_empty: - new_cell.add_agent(agent) - return + cell = self.all_cells.select_random_cell() + if cell.is_empty: + return cell # TODO: Adjust cutoff value for performance for _ in range(len(self.all_cells) // 10): @@ -164,7 +194,7 @@ def move_to_empty(self, agent: Agent) -> None: raise Exception("ERROR: No empty cell found") from err -class Grid(Space): +class Grid(DiscreteSpace): def __init__( self, width: int, height: int, torus: bool = False, moore: bool = True, capacity=1 ) -> None: @@ -206,7 +236,7 @@ def _connect_single_cell(self, cell): cell.connect(self.cells[ni, nj]) -class HexGrid(Space): +class HexGrid(DiscreteSpace): def __init__( self, width: int, height: int, torus: bool = False, capacity=1 ) -> None: From 875d0edbadcb1aafcf89980b08451870f9630f3a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 27 Jan 2024 21:09:41 +0100 Subject: [PATCH 12/63] add NetworkGrid --- mesa/experimental/cell_space.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 8ae0d914001..7547f01007d 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -2,7 +2,7 @@ from random import Random from collections.abc import Iterable from functools import cache, cached_property -from typing import Callable, Optional +from typing import Callable, Optional, Any from .. import Agent, Model @@ -44,8 +44,8 @@ def move_to(self, cell) -> None: class Cell: __slots__ = ["coordinate", "_connections", "agents", "capacity", "properties"] - def __init__(self, i: int, j: int, capacity: int | None = 1) -> None: - self.coordinate = (i, j) + def __init__(self, coordinate, capacity: int | None = 1) -> None: + self.coordinate = coordinate self._connections: list[Cell] = [] self.agents: list[Agent] = [] self.capacity = capacity @@ -204,7 +204,7 @@ def __init__( self.moore = moore self.capacity = capacity self.cells = { - (i, j): Cell(i, j, capacity) for j in range(width) for i in range(height) + (i, j): Cell((i, j), capacity) for j in range(width) for i in range(height) } for cell in self.all_cells: @@ -275,3 +275,25 @@ def _connect_single_cell(self, cell): ni, nj = ni % self.height, nj % self.width if 0 <= ni < self.height and 0 <= nj < self.width: cell.connect(self.cells[ni, nj]) + +class NetworkGrid(DiscreteSpace): + def __init__(self, g: Any, capacity: int = 1) -> None: + """Create a new network. + + Args: + G: a NetworkX graph instance. + """ + super().__init__() + self.G = g + self.capacity = capacity + + self.cells = {} + for node_id in self.G.nodes: + self.cells[node_id] = Cell(node_id, capacity) + + for cell in self.all_cells: + self._connect_single_cell(cell) + + def _connect_single_cell(self, cell): + neighbors = [self.cells[node_id] for node_id in self.G.neighbors(cell.coordinate)] + cell.connect(neighbors) \ No newline at end of file From e4d121de2dc89801d9ddfb012e4bbe2e49bcf3e8 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 27 Jan 2024 21:16:17 +0100 Subject: [PATCH 13/63] add NetworkGrid --- mesa/experimental/cell_space.py | 66 ++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 7547f01007d..fe792ce5c56 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -1,8 +1,8 @@ import itertools -from random import Random from collections.abc import Iterable from functools import cache, cached_property -from typing import Callable, Optional, Any +from random import Random +from typing import Any, Callable, Optional from .. import Agent, Model @@ -103,13 +103,14 @@ def _neighborhood(self, radius=1, include_center=False): neighborhood.pop(self, None) return neighborhood + class CellCollection: def __init__(self, cells: dict[Cell, list[Agent]] | Iterable[Cell]) -> None: if isinstance(cells, dict): self._cells = cells else: self._cells = {cell: cell.agents for cell in cells} - self.random = Random() # FIXME + self.random = Random() # FIXME def __iter__(self): return iter(self._cells) @@ -196,8 +197,19 @@ def select_random_empty(self, agent: Agent) -> None: class Grid(DiscreteSpace): def __init__( - self, width: int, height: int, torus: bool = False, moore: bool = True, capacity=1 + self, width: int, height: int, torus: bool = False, moore: bool = True, capacity=1 ) -> None: + """Rectangular grid + + Args: + width (int): width of the grid + height (int): height of the grid + torus (bool): whether the space is a torus + moore (bool): whether the space used Moore or von Neumann neighborhood + capacity (int): the number of agents that can simultaneously occupy a cell + + + """ self.width = width self.height = height self.torus = torus @@ -217,14 +229,14 @@ def _connect_single_cell(self, cell): if self.moore: directions = [ (-1, -1), (-1, 0), (-1, 1), - ( 0, -1), ( 0, 1), - ( 1, -1), ( 1, 0), ( 1, 1), + (0, -1), (0, 1), + (1, -1), (1, 0), (1, 1), ] - else: # Von Neumann neighborhood + else: # Von Neumann neighborhood directions = [ - (-1, 0), - ( 0, -1), (0, 1), - ( 1, 0), + (-1, 0), + (0, -1), (0, 1), + (1, 0), ] # fmt: on @@ -238,8 +250,17 @@ def _connect_single_cell(self, cell): class HexGrid(DiscreteSpace): def __init__( - self, width: int, height: int, torus: bool = False, capacity=1 + self, width: int, height: int, torus: bool = False, capacity=1 ) -> None: + """Hexagonal Grid + + Args: + width (int): width of the grid + height (int): height of the grid + torus (bool): whether the space is a torus + capacity (int): the number of agents that can simultaneously occupy a cell + + """ self.width = width self.height = height self.torus = torus @@ -255,17 +276,17 @@ def _connect_single_cell(self, cell): i, j = cell.coordinate # fmt: off - if i%2 == 0: + if i % 2 == 0: directions = [ - (-1, -1), (-1, 0), - (0, -1), (0, 1), - ( 1, -1), ( 1, 0), + (-1, -1), (-1, 0), + (0, -1), (0, 1), + (1, -1), (1, 0), ] else: directions = [ - (-1, 0), (-1, 1), - (0, -1), (0, 1), - ( 1, 0), ( 1, 1), + (-1, 0), (-1, 1), + (0, -1), (0, 1), + (1, 0), (1, 1), ] # fmt: on @@ -276,12 +297,15 @@ def _connect_single_cell(self, cell): if 0 <= ni < self.height and 0 <= nj < self.width: cell.connect(self.cells[ni, nj]) + class NetworkGrid(DiscreteSpace): def __init__(self, g: Any, capacity: int = 1) -> None: - """Create a new network. + """A Networked grid Args: - G: a NetworkX graph instance. + G: a NetworkX Graph instance. + capacity (int) : the capacity of the cell + """ super().__init__() self.G = g @@ -296,4 +320,4 @@ def __init__(self, g: Any, capacity: int = 1) -> None: def _connect_single_cell(self, cell): neighbors = [self.cells[node_id] for node_id in self.G.neighbors(cell.coordinate)] - cell.connect(neighbors) \ No newline at end of file + cell.connect(neighbors) From 5769a053f1ddd65e3c09457a8afa0e7fbd12747f Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 27 Jan 2024 21:44:35 +0100 Subject: [PATCH 14/63] change how empties is handled --- benchmarks/Schelling/schelling.py | 2 +- mesa/experimental/cell_space.py | 128 ++++++++++++++++++++++-------- 2 files changed, 94 insertions(+), 36 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index af9caba3721..0c98a6a9a17 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -27,7 +27,7 @@ def step(self): # If unhappy, move: if similar < self.model.homophily: - self.move_to(self.model.grid.select_random_empty(self)) + self.move_to(self.model.grid.select_random_empty_cell(self)) else: self.model.happy += 1 diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index fe792ce5c56..c5eb60a038c 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -42,14 +42,15 @@ def move_to(self, cell) -> None: class Cell: - __slots__ = ["coordinate", "_connections", "agents", "capacity", "properties"] + __slots__ = ["coordinate", "_connections", "owner", "agents", "capacity", "properties"] - def __init__(self, coordinate, capacity: int | None = 1) -> None: + def __init__(self, coordinate, owner, capacity: int | None = 1) -> None: self.coordinate = coordinate self._connections: list[Cell] = [] self.agents: list[Agent] = [] self.capacity = capacity self.properties: dict[str, object] = {} + self.owner = owner def connect(self, other) -> None: """Connects this cell to another cell.""" @@ -61,14 +62,20 @@ def disconnect(self, other) -> None: def add_agent(self, agent: Agent) -> None: """Adds an agent to the cell.""" + if len(self.agents) == 0: + self.owner._empties.pop(self, None) + if self.capacity and len(self.agents) >= self.capacity: - raise Exception("ERROR: Cell is full") + raise Exception("ERROR: Cell is full") # FIXME we need MESA errors or a proper error + self.agents.append(agent) def remove_agent(self, agent: Agent) -> None: """Removes an agent from the cell.""" self.agents.remove(agent) - agent.cell = None + if len(self.agents) == 0: + self.owner._empties[self] = None + @property def is_empty(self) -> bool: @@ -153,11 +160,28 @@ def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): class DiscreteSpace: - cells: dict[Coordinate, Cell] = {} + + + def __init__(self, capacity): + super().__init__() + self.capacity = capacity + cells: dict[Coordinate, Cell] = {} + + self._empties = {} + self.cutoff_empties = -1 + self.empties_initialized = False def _connect_single_cell(self, cell): ... + def select_random_empty_cell(self) -> Cell: + ... + + def _initialize_empties(self): + self._empties = self._empties = {cell:None for cell in self.cells.values() if cell.is_empty} + self.cutoff_empties = 7.953 *len(self.cells) ** 0.384 + self.empties_initialized = True + @cached_property def all_cells(self): return CellCollection({cell: cell.agents for cell in self.cells.values()}) @@ -168,32 +192,10 @@ def __iter__(self): def __getitem__(self, key): return self.cells[key] - def move_agent(self, agent: Agent, pos: Coordinate) -> None: - """Move an agent from its current position to a new position.""" - self.cells[pos].add_agent(agent) - @property def empties(self) -> CellCollection: return self.all_cells.select(lambda cell: cell.is_empty) - def select_random_empty(self, agent: Agent) -> None: - while True: - cell = self.all_cells.select_random_cell() - if cell.is_empty: - return cell - - # TODO: Adjust cutoff value for performance - for _ in range(len(self.all_cells) // 10): - new_cell = self.all_cells.select_random_cell() - if new_cell.is_empty: - new_cell.add_agent(agent) - return - - try: - self.empties.select_random_cell().add_agent(agent) - except IndexError as err: - raise Exception("ERROR: No empty cell found") from err - class Grid(DiscreteSpace): def __init__( @@ -210,13 +212,13 @@ def __init__( """ + super().__init__(capacity) self.width = width self.height = height self.torus = torus self.moore = moore - self.capacity = capacity self.cells = { - (i, j): Cell((i, j), capacity) for j in range(width) for i in range(height) + (i, j): Cell((i, j), self, capacity) for j in range(width) for i in range(height) } for cell in self.all_cells: @@ -247,6 +249,34 @@ def _connect_single_cell(self, cell): if 0 <= ni < self.height and 0 <= nj < self.width: cell.connect(self.cells[ni, nj]) + def select_random_empty_cell(self, agent: Agent) -> None: + # FIXME I now copy paste this code into HexGrid and Grid + if not self.empties_initialized: + self._initialize_empties() + + 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), + ) + cell = self.cells[new_pos] + if cell.is_empty: + break + else: + cell = agent.random.choice(sorted(self.empties)) + + return cell + class HexGrid(DiscreteSpace): def __init__( @@ -261,12 +291,12 @@ def __init__( capacity (int): the number of agents that can simultaneously occupy a cell """ + super().__init__(capacity) self.width = width self.height = height self.torus = torus - self.capacity = capacity self.cells = { - (i, j): Cell(i, j, capacity) for j in range(width) for i in range(height) + (i, j): Cell(i, j, self, capacity) for j in range(width) for i in range(height) } for cell in self.all_cells: @@ -297,6 +327,33 @@ def _connect_single_cell(self, cell): if 0 <= ni < self.height and 0 <= nj < self.width: cell.connect(self.cells[ni, nj]) + def select_random_emtpy_cell(self, agent: Agent) -> Cell: + # FIXME I now copy paste this code + if not self.empties_initialized: + self._initialize_empties() + + 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), + ) + cell = self.cells[new_pos] + if cell.is_empty: + break + else: + cell = agent.random.choice(sorted(self.empties)) + + return cell class NetworkGrid(DiscreteSpace): def __init__(self, g: Any, capacity: int = 1) -> None: @@ -307,13 +364,11 @@ def __init__(self, g: Any, capacity: int = 1) -> None: capacity (int) : the capacity of the cell """ - super().__init__() + super().__init__(capacity) self.G = g - self.capacity = capacity - self.cells = {} for node_id in self.G.nodes: - self.cells[node_id] = Cell(node_id, capacity) + self.cells[node_id] = Cell(node_id, self, capacity) for cell in self.all_cells: self._connect_single_cell(cell) @@ -321,3 +376,6 @@ def __init__(self, g: Any, capacity: int = 1) -> None: def _connect_single_cell(self, cell): neighbors = [self.cells[node_id] for node_id in self.G.neighbors(cell.coordinate)] cell.connect(neighbors) + + def select_random_empty_cell(self, agent: Agent) -> Cell: + raise NotImplementedError \ No newline at end of file From 82a64d900134d1ee579c9a019f6d986cf6da600b Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 27 Jan 2024 21:57:22 +0100 Subject: [PATCH 15/63] further cleanup of handling of empties adds a new intermediate Grid class from which both OrthogonalGrid and HexGrid derive. --- benchmarks/Schelling/schelling.py | 6 +- benchmarks/WolfSheep/wolf_sheep.py | 4 +- mesa/experimental/cell_space.py | 123 ++++++++++++----------------- 3 files changed, 55 insertions(+), 78 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 0c98a6a9a17..4630a9136f8 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,5 +1,5 @@ from mesa import Model -from mesa.experimental.cell_space import Grid, CellAgent +from mesa.experimental.cell_space import OrthogonalGrid, CellAgent from mesa.time import RandomActivation @@ -27,7 +27,7 @@ def step(self): # If unhappy, move: if similar < self.model.homophily: - self.move_to(self.model.grid.select_random_empty_cell(self)) + self.move_to(self.model.grid.select_random_empty_cell()) else: self.model.happy += 1 @@ -50,7 +50,7 @@ def __init__( self.radius = radius self.schedule = RandomActivation(self) - self.grid = Grid(height, width, torus=True) + self.grid = OrthogonalGrid(height, width, torus=True) self.happy = 0 diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 488f4ead762..d09a26de1ff 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -11,7 +11,7 @@ import math from mesa import Model -from mesa.experimental.cell_space import Grid, CellAgent +from mesa.experimental.cell_space import OrthogonalGrid, CellAgent from mesa.time import RandomActivationByType @@ -154,7 +154,7 @@ def __init__( self.grass_regrowth_time = grass_regrowth_time self.schedule = RandomActivationByType(self) - self.grid = Grid(self.height, self.width, moore=moore, torus=False, capacity=math.inf) + self.grid = OrthogonalGrid(self.height, self.width, moore=moore, torus=False, capacity=math.inf) # Create sheep: for _ in range(self.initial_sheep): diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index c5eb60a038c..cdbc492cde4 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -66,7 +66,7 @@ def add_agent(self, agent: Agent) -> None: self.owner._empties.pop(self, None) if self.capacity and len(self.agents) >= self.capacity: - raise Exception("ERROR: Cell is full") # FIXME we need MESA errors or a proper error + raise Exception("ERROR: Cell is full") # FIXME we need MESA errors or a proper error self.agents.append(agent) @@ -76,7 +76,6 @@ def remove_agent(self, agent: Agent) -> None: if len(self.agents) == 0: self.owner._empties[self] = None - @property def is_empty(self) -> bool: """Returns a bool of the contents of a cell.""" @@ -161,11 +160,11 @@ def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): class DiscreteSpace: - def __init__(self, capacity): super().__init__() self.capacity = capacity - cells: dict[Coordinate, Cell] = {} + self.cells: dict[Coordinate, Cell] = {} + self.random = Random() # FIXME self._empties = {} self.cutoff_empties = -1 @@ -178,8 +177,8 @@ def select_random_empty_cell(self) -> Cell: ... def _initialize_empties(self): - self._empties = self._empties = {cell:None for cell in self.cells.values() if cell.is_empty} - self.cutoff_empties = 7.953 *len(self.cells) ** 0.384 + self._empties = self._empties = {cell: None for cell in self.cells.values() if cell.is_empty} + self.cutoff_empties = 7.953 * len(self.cells) ** 0.384 self.empties_initialized = True @cached_property @@ -198,8 +197,44 @@ def empties(self) -> CellCollection: class Grid(DiscreteSpace): + + def __init__(self, width: int, height: int, torus: bool = False, capacity: int = 1) -> None: + super().__init__(capacity) + self.torus = torus + self.width = width + self.height = height + + def select_random_empty_cell(self) -> Cell: + if not self.empties_initialized: + self._initialize_empties() + + 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 = ( + self.random.randrange(self.width), + self.random.randrange(self.height), + ) + cell = self.cells[new_pos] + if cell.is_empty: + break + else: + cell = self.random.choice(sorted(self._empties)) + + return cell + + +class OrthogonalGrid(Grid): def __init__( - self, width: int, height: int, torus: bool = False, moore: bool = True, capacity=1 + self, width: int, height: int, torus: bool = False, moore: bool = True, capacity: int = 1 ) -> None: """Rectangular grid @@ -212,10 +247,7 @@ def __init__( """ - super().__init__(capacity) - self.width = width - self.height = height - self.torus = torus + super().__init__(width, height, torus, capacity) self.moore = moore self.cells = { (i, j): Cell((i, j), self, capacity) for j in range(width) for i in range(height) @@ -249,36 +281,8 @@ def _connect_single_cell(self, cell): if 0 <= ni < self.height and 0 <= nj < self.width: cell.connect(self.cells[ni, nj]) - def select_random_empty_cell(self, agent: Agent) -> None: - # FIXME I now copy paste this code into HexGrid and Grid - if not self.empties_initialized: - self._initialize_empties() - 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), - ) - cell = self.cells[new_pos] - if cell.is_empty: - break - else: - cell = agent.random.choice(sorted(self.empties)) - - return cell - - -class HexGrid(DiscreteSpace): +class HexGrid(Grid): def __init__( self, width: int, height: int, torus: bool = False, capacity=1 ) -> None: @@ -291,10 +295,7 @@ def __init__( capacity (int): the number of agents that can simultaneously occupy a cell """ - super().__init__(capacity) - self.width = width - self.height = height - self.torus = torus + super().__init__(width, height, torus, capacity) self.cells = { (i, j): Cell(i, j, self, capacity) for j in range(width) for i in range(height) } @@ -327,33 +328,6 @@ def _connect_single_cell(self, cell): if 0 <= ni < self.height and 0 <= nj < self.width: cell.connect(self.cells[ni, nj]) - def select_random_emtpy_cell(self, agent: Agent) -> Cell: - # FIXME I now copy paste this code - if not self.empties_initialized: - self._initialize_empties() - - 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), - ) - cell = self.cells[new_pos] - if cell.is_empty: - break - else: - cell = agent.random.choice(sorted(self.empties)) - - return cell class NetworkGrid(DiscreteSpace): def __init__(self, g: Any, capacity: int = 1) -> None: @@ -377,5 +351,8 @@ def _connect_single_cell(self, cell): neighbors = [self.cells[node_id] for node_id in self.G.neighbors(cell.coordinate)] cell.connect(neighbors) - def select_random_empty_cell(self, agent: Agent) -> Cell: - raise NotImplementedError \ No newline at end of file + def select_random_empty_cell(self) -> Cell: + if not self.empties_initialized: + self._initialize_empties() + + return self.random.choice(self._empties) From 5fdc78782f53706591b9986f694ccb2c93d66614 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 27 Jan 2024 22:12:40 +0100 Subject: [PATCH 16/63] correct handling of radius in Schelling large --- benchmarks/Schelling/schelling.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 4630a9136f8..6c7c7377259 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -8,7 +8,7 @@ class SchellingAgent(CellAgent): Schelling segregation agent """ - def __init__(self, unique_id, model, agent_type): + def __init__(self, unique_id, model, agent_type, radius): """ Create a new Schelling agent. Args: @@ -18,10 +18,11 @@ def __init__(self, unique_id, model, agent_type): """ super().__init__(unique_id, model) self.type = agent_type + self.radius = radius def step(self): similar = 0 - for neighbor in self.cell.neighborhood().agents: + for neighbor in self.cell.neighborhood(radius=self.radius).agents: if neighbor.type == self.type: similar += 1 @@ -47,7 +48,6 @@ def __init__( self.density = density self.minority_pc = minority_pc self.homophily = homophily - self.radius = radius self.schedule = RandomActivation(self) self.grid = OrthogonalGrid(height, width, torus=True) @@ -61,7 +61,7 @@ def __init__( for cell in self.grid: if self.random.random() < self.density: agent_type = 1 if self.random.random() < self.minority_pc else 0 - agent = SchellingAgent(self.next_id(), self, agent_type) + agent = SchellingAgent(self.next_id(), self, agent_type, radius) agent.move_to(cell) self.schedule.add(agent) From 152995cd95075b0f9dc0a7588a38a884c1cbffce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 27 Jan 2024 21:17:16 +0000 Subject: [PATCH 17/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/Schelling/schelling.py | 2 +- benchmarks/WolfSheep/wolf_sheep.py | 52 ++++++++++++++++++------------ mesa/agent.py | 3 -- mesa/experimental/cell_space.py | 48 ++++++++++++++++++++------- 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 6c7c7377259..8d44df94ad0 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,5 +1,5 @@ from mesa import Model -from mesa.experimental.cell_space import OrthogonalGrid, CellAgent +from mesa.experimental.cell_space import CellAgent, OrthogonalGrid from mesa.time import RandomActivation diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index d09a26de1ff..f38be2a0e89 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -11,12 +11,11 @@ import math from mesa import Model -from mesa.experimental.cell_space import OrthogonalGrid, CellAgent +from mesa.experimental.cell_space import CellAgent, OrthogonalGrid from mesa.time import RandomActivationByType class Animal(CellAgent): - def __init__(self, unique_id, model, energy, p_reproduce, energy_from_food): super().__init__(unique_id, model) self.energy = energy @@ -29,7 +28,11 @@ def random_move(self): def spawn_offspring(self): self.energy /= 2 offspring = self.__class__( - self.model.next_id(), self.model, self.energy, self.p_reproduce, self.energy_from_food + self.model.next_id(), + self.model, + self.energy, + self.p_reproduce, + self.energy_from_food, ) offspring.move_to(self.cell) self.model.schedule.add(offspring) @@ -62,18 +65,21 @@ class Sheep(Animal): def feed(self): # If there is grass available, eat it - grass_patch = next(obj for obj in self.cell.agents if isinstance(obj, GrassPatch)) + grass_patch = next( + obj for obj in self.cell.agents if isinstance(obj, GrassPatch) + ) if grass_patch.fully_grown: self.energy += self.energy_from_food grass_patch.fully_grown = False + class Wolf(Animal): """ A wolf that walks around, reproduces (asexually) and eats sheep. """ def feed(self): - sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)] + sheep = [obj for obj in self.cell.agents if isinstance(obj, Sheep)] if len(sheep) > 0: sheep_to_eat = self.random.choice(sheep) self.energy += self.energy @@ -117,18 +123,18 @@ class WolfSheep(Model): """ def __init__( - self, - seed, - height, - width, - initial_sheep, - initial_wolves, - sheep_reproduce, - wolf_reproduce, - grass_regrowth_time, - wolf_gain_from_food=13, - sheep_gain_from_food=5, - moore=False + self, + seed, + height, + width, + initial_sheep, + initial_wolves, + sheep_reproduce, + wolf_reproduce, + grass_regrowth_time, + wolf_gain_from_food=13, + sheep_gain_from_food=5, + moore=False, ): """ Create a new Wolf-Sheep model with the given parameters. @@ -154,7 +160,9 @@ def __init__( self.grass_regrowth_time = grass_regrowth_time self.schedule = RandomActivationByType(self) - self.grid = OrthogonalGrid(self.height, self.width, moore=moore, torus=False, capacity=math.inf) + self.grid = OrthogonalGrid( + self.height, self.width, moore=moore, torus=False, capacity=math.inf + ) # Create sheep: for _ in range(self.initial_sheep): @@ -163,7 +171,9 @@ def __init__( self.random.randrange(self.height), ) energy = self.random.randrange(2 * sheep_gain_from_food) - sheep = Sheep(self.next_id(), self, energy, sheep_reproduce, sheep_gain_from_food) + sheep = Sheep( + self.next_id(), self, energy, sheep_reproduce, sheep_gain_from_food + ) sheep.move_to(self.grid.cells[pos]) self.schedule.add(sheep) @@ -174,7 +184,9 @@ def __init__( self.random.randrange(self.height), ) energy = self.random.randrange(2 * wolf_gain_from_food) - wolf = Wolf(self.next_id(), self, energy, wolf_reproduce, wolf_gain_from_food) + wolf = Wolf( + self.next_id(), self, energy, wolf_reproduce, wolf_gain_from_food + ) wolf.move_to(self.grid.cells[pos]) self.schedule.add(wolf) diff --git a/mesa/agent.py b/mesa/agent.py index ebf10761284..6155284ba42 100644 --- a/mesa/agent.py +++ b/mesa/agent.py @@ -22,7 +22,6 @@ if TYPE_CHECKING: # We ensure that these are not imported during runtime to prevent cyclic # dependency. - from mesa.gridspace import Cell from mesa.model import Model from mesa.space import Position @@ -80,8 +79,6 @@ def random(self) -> Random: return self.model.random - - class AgentSet(MutableSet, Sequence): """ .. warning:: diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index cdbc492cde4..1a9a7038d07 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -42,7 +42,14 @@ def move_to(self, cell) -> None: class Cell: - __slots__ = ["coordinate", "_connections", "owner", "agents", "capacity", "properties"] + __slots__ = [ + "coordinate", + "_connections", + "owner", + "agents", + "capacity", + "properties", + ] def __init__(self, coordinate, owner, capacity: int | None = 1) -> None: self.coordinate = coordinate @@ -66,7 +73,9 @@ def add_agent(self, agent: Agent) -> None: self.owner._empties.pop(self, None) if self.capacity and len(self.agents) >= self.capacity: - raise Exception("ERROR: Cell is full") # FIXME we need MESA errors or a proper error + raise Exception( + "ERROR: Cell is full" + ) # FIXME we need MESA errors or a proper error self.agents.append(agent) @@ -91,7 +100,9 @@ def __repr__(self): @cache def neighborhood(self, radius=1, include_center=False): - return CellCollection(self._neighborhood(radius=radius, include_center=include_center)) + return CellCollection( + self._neighborhood(radius=radius, include_center=include_center) + ) @cache def _neighborhood(self, radius=1, include_center=False): @@ -159,7 +170,6 @@ def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): class DiscreteSpace: - def __init__(self, capacity): super().__init__() self.capacity = capacity @@ -177,7 +187,9 @@ def select_random_empty_cell(self) -> Cell: ... def _initialize_empties(self): - self._empties = self._empties = {cell: None for cell in self.cells.values() if cell.is_empty} + self._empties = self._empties = { + cell: None for cell in self.cells.values() if cell.is_empty + } self.cutoff_empties = 7.953 * len(self.cells) ** 0.384 self.empties_initialized = True @@ -197,8 +209,9 @@ def empties(self) -> CellCollection: class Grid(DiscreteSpace): - - def __init__(self, width: int, height: int, torus: bool = False, capacity: int = 1) -> None: + def __init__( + self, width: int, height: int, torus: bool = False, capacity: int = 1 + ) -> None: super().__init__(capacity) self.torus = torus self.width = width @@ -234,7 +247,12 @@ def select_random_empty_cell(self) -> Cell: class OrthogonalGrid(Grid): def __init__( - self, width: int, height: int, torus: bool = False, moore: bool = True, capacity: int = 1 + self, + width: int, + height: int, + torus: bool = False, + moore: bool = True, + capacity: int = 1, ) -> None: """Rectangular grid @@ -250,7 +268,9 @@ def __init__( super().__init__(width, height, torus, capacity) self.moore = moore self.cells = { - (i, j): Cell((i, j), self, capacity) for j in range(width) for i in range(height) + (i, j): Cell((i, j), self, capacity) + for j in range(width) + for i in range(height) } for cell in self.all_cells: @@ -284,7 +304,7 @@ def _connect_single_cell(self, cell): class HexGrid(Grid): def __init__( - self, width: int, height: int, torus: bool = False, capacity=1 + self, width: int, height: int, torus: bool = False, capacity=1 ) -> None: """Hexagonal Grid @@ -297,7 +317,9 @@ def __init__( """ super().__init__(width, height, torus, capacity) self.cells = { - (i, j): Cell(i, j, self, capacity) for j in range(width) for i in range(height) + (i, j): Cell(i, j, self, capacity) + for j in range(width) + for i in range(height) } for cell in self.all_cells: @@ -348,7 +370,9 @@ def __init__(self, g: Any, capacity: int = 1) -> None: self._connect_single_cell(cell) def _connect_single_cell(self, cell): - neighbors = [self.cells[node_id] for node_id in self.G.neighbors(cell.coordinate)] + neighbors = [ + self.cells[node_id] for node_id in self.G.neighbors(cell.coordinate) + ] cell.connect(neighbors) def select_random_empty_cell(self) -> Cell: From 972ecc4a99a87da833ba7a464f9ec69657e9797f Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 28 Jan 2024 10:13:34 +0100 Subject: [PATCH 18/63] Update cell_space.py --- mesa/experimental/cell_space.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 1a9a7038d07..1f920c9e184 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -54,7 +54,7 @@ class Cell: def __init__(self, coordinate, owner, capacity: int | None = 1) -> None: self.coordinate = coordinate self._connections: list[Cell] = [] - self.agents: list[Agent] = [] + self.agents: dict[Agent, None] = {} self.capacity = capacity self.properties: dict[str, object] = {} self.owner = owner @@ -69,21 +69,23 @@ def disconnect(self, other) -> None: def add_agent(self, agent: Agent) -> None: """Adds an agent to the cell.""" - if len(self.agents) == 0: - self.owner._empties.pop(self, None) + n = len(self.agents) + + if n == 0: + self.owner._empties.pop(self.coordinate, None) - if self.capacity and len(self.agents) >= self.capacity: + if self.capacity and n >= self.capacity: raise Exception( "ERROR: Cell is full" ) # FIXME we need MESA errors or a proper error - self.agents.append(agent) + self.agents[agent] = None def remove_agent(self, agent: Agent) -> None: """Removes an agent from the cell.""" - self.agents.remove(agent) + self.agents.pop(agent, None) if len(self.agents) == 0: - self.owner._empties[self] = None + self.owner._empties[self.coordinate] = None @property def is_empty(self) -> bool: @@ -240,7 +242,8 @@ def select_random_empty_cell(self) -> Cell: if cell.is_empty: break else: - cell = self.random.choice(sorted(self._empties)) + coordinate = self.random.choice(list(self._empties)) + cell = self.cells[coordinate] return cell From a656d933aa0c82e398fc4eee71db2a799f315202 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 28 Jan 2024 19:55:09 +0100 Subject: [PATCH 19/63] change from Random to random This is a temporary fix. All random number generation for now uses the default rng from random. --- benchmarks/WolfSheep/wolf_sheep.py | 2 +- mesa/experimental/cell_space.py | 59 ++++++++++++++++-------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index f38be2a0e89..a35cb4b190a 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -98,7 +98,7 @@ def __init__(self, unique_id, model, fully_grown, countdown): Creates a new patch of grass Args: - grown: (boolean) Whether the patch of grass is fully grown or not + fully_grown: (boolean) Whether the patch of grass is fully grown or not countdown: Time for the patch of grass to be fully grown again """ super().__init__(unique_id, model) diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 1f920c9e184..2516a358b62 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -1,7 +1,7 @@ import itertools +import random from collections.abc import Iterable from functools import cache, cached_property -from random import Random from typing import Any, Callable, Optional from .. import Agent, Model @@ -30,10 +30,6 @@ def __init__(self, unique_id: int, model: Model) -> None: super().__init__(unique_id, model) self.cell: Cell | None = None - @property - def random(self) -> Random: - return self.model.random - def move_to(self, cell) -> None: if self.cell is not None: self.cell.remove_agent(self) @@ -53,8 +49,8 @@ class Cell: def __init__(self, coordinate, owner, capacity: int | None = 1) -> None: self.coordinate = coordinate - self._connections: list[Cell] = [] - self.agents: dict[Agent, None] = {} + self._connections: list[Cell] = [] # TODO: change to CellCollection? + self.agents: dict[Agent, None] = {} # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) self.capacity = capacity self.properties: dict[str, object] = {} self.owner = owner @@ -84,6 +80,7 @@ def add_agent(self, agent: Agent) -> None: def remove_agent(self, agent: Agent) -> None: """Removes an agent from the cell.""" self.agents.pop(agent, None) + agent.cell = None if len(self.agents) == 0: self.owner._empties[self.coordinate] = None @@ -122,6 +119,9 @@ def _neighborhood(self, radius=1, include_center=False): neighborhood.pop(self, None) return neighborhood + def __repr__(self): + return f"Cell({self.coords})" + class CellCollection: def __init__(self, cells: dict[Cell, list[Agent]] | Iterable[Cell]) -> None: @@ -129,16 +129,16 @@ def __init__(self, cells: dict[Cell, list[Agent]] | Iterable[Cell]) -> None: self._cells = cells else: self._cells = {cell: cell.agents for cell in cells} - self.random = Random() # FIXME + self.random = random # FIXME def __iter__(self): return iter(self._cells) - def __getitem__(self, key): + def __getitem__(self, key:Cell) -> Iterable[Agent]: return self._cells[key] @cached_property - def __len__(self): + def __len__(self) -> int: return len(self._cells) def __repr__(self): @@ -149,7 +149,7 @@ def cells(self): return list(self._cells.keys()) @property - def agents(self): + def agents(self) -> Iterable[Agent]: return itertools.chain.from_iterable(self._cells.values()) def select_random_cell(self): @@ -172,11 +172,15 @@ def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): class DiscreteSpace: + # FIXME:: random should become a keyword argument + # FIXME:: defaulting to the same rng as model.random. + # FIXME:: all children should be also like that. + def __init__(self, capacity): super().__init__() self.capacity = capacity self.cells: dict[Coordinate, Cell] = {} - self.random = Random() # FIXME + self.random = random # FIXME self._empties = {} self.cutoff_empties = -1 @@ -190,7 +194,7 @@ def select_random_empty_cell(self) -> Cell: def _initialize_empties(self): self._empties = self._empties = { - cell: None for cell in self.cells.values() if cell.is_empty + cell.coordinate: None for cell in self.cells.values() if cell.is_empty } self.cutoff_empties = 7.953 * len(self.cells) ** 0.384 self.empties_initialized = True @@ -257,7 +261,7 @@ def __init__( moore: bool = True, capacity: int = 1, ) -> None: - """Rectangular grid + """Orthogonal grid Args: width (int): width of the grid @@ -266,7 +270,6 @@ def __init__( moore (bool): whether the space used Moore or von Neumann neighborhood capacity (int): the number of agents that can simultaneously occupy a cell - """ super().__init__(width, height, torus, capacity) self.moore = moore @@ -286,14 +289,14 @@ def _connect_single_cell(self, cell): if self.moore: directions = [ (-1, -1), (-1, 0), (-1, 1), - (0, -1), (0, 1), - (1, -1), (1, 0), (1, 1), + ( 0, -1), ( 0, 1), + ( 1, -1), ( 1, 0), ( 1, 1), ] else: # Von Neumann neighborhood directions = [ - (-1, 0), - (0, -1), (0, 1), - (1, 0), + (-1, 0), + (0, -1), (0, 1), + ( 1, 0), ] # fmt: on @@ -334,15 +337,15 @@ def _connect_single_cell(self, cell): # fmt: off if i % 2 == 0: directions = [ - (-1, -1), (-1, 0), - (0, -1), (0, 1), - (1, -1), (1, 0), + (-1, -1), (-1, 0), + (0, -1), (0, 1), + ( 1, -1), (1, 0), ] else: directions = [ - (-1, 0), (-1, 1), - (0, -1), (0, 1), - (1, 0), (1, 1), + (-1, 0), (-1, 1), + (0, -1), (0, 1), + ( 1, 0), (1, 1), ] # fmt: on @@ -355,7 +358,7 @@ def _connect_single_cell(self, cell): class NetworkGrid(DiscreteSpace): - def __init__(self, g: Any, capacity: int = 1) -> None: + def __init__(self, G: Any, capacity: int = 1) -> None: """A Networked grid Args: @@ -364,7 +367,7 @@ def __init__(self, g: Any, capacity: int = 1) -> None: """ super().__init__(capacity) - self.G = g + self.G = G for node_id in self.G.nodes: self.cells[node_id] = Cell(node_id, self, capacity) From 8df92760fdacbdfd1f2ef4f6d28731b10d52ffb6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 28 Jan 2024 18:55:17 +0000 Subject: [PATCH 20/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 2516a358b62..3ca57e2708f 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -49,8 +49,10 @@ class Cell: def __init__(self, coordinate, owner, capacity: int | None = 1) -> None: self.coordinate = coordinate - self._connections: list[Cell] = [] # TODO: change to CellCollection? - self.agents: dict[Agent, None] = {} # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) + self._connections: list[Cell] = [] # TODO: change to CellCollection? + self.agents: dict[ + Agent, None + ] = {} # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) self.capacity = capacity self.properties: dict[str, object] = {} self.owner = owner @@ -67,7 +69,7 @@ def add_agent(self, agent: Agent) -> None: """Adds an agent to the cell.""" n = len(self.agents) - if n == 0: + if n == 0: self.owner._empties.pop(self.coordinate, None) if self.capacity and n >= self.capacity: @@ -134,7 +136,7 @@ def __init__(self, cells: dict[Cell, list[Agent]] | Iterable[Cell]) -> None: def __iter__(self): return iter(self._cells) - def __getitem__(self, key:Cell) -> Iterable[Agent]: + def __getitem__(self, key: Cell) -> Iterable[Agent]: return self._cells[key] @cached_property From f4a10894b1753ea638828a9ba2e8ea0b490ea7fc Mon Sep 17 00:00:00 2001 From: Corvince Date: Sun, 28 Jan 2024 22:44:39 +0100 Subject: [PATCH 21/63] small fixes and default capacity=None --- benchmarks/Schelling/schelling.py | 2 +- mesa/experimental/cell_space.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 8d44df94ad0..70eb8fd339d 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -50,7 +50,7 @@ def __init__( self.homophily = homophily self.schedule = RandomActivation(self) - self.grid = OrthogonalGrid(height, width, torus=True) + self.grid = OrthogonalGrid(height, width, torus=True, capacity=1) self.happy = 0 diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 3ca57e2708f..40bcc1f1e40 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -47,7 +47,7 @@ class Cell: "properties", ] - def __init__(self, coordinate, owner, capacity: int | None = 1) -> None: + def __init__(self, coordinate, owner, capacity: int | None = None) -> None: self.coordinate = coordinate self._connections: list[Cell] = [] # TODO: change to CellCollection? self.agents: dict[ @@ -65,7 +65,7 @@ def disconnect(self, other) -> None: """Disconnects this cell from another cell.""" self._connections.remove(other) - def add_agent(self, agent: Agent) -> None: + def add_agent(self, agent: CellAgent) -> None: """Adds an agent to the cell.""" n = len(self.agents) @@ -79,7 +79,7 @@ def add_agent(self, agent: Agent) -> None: self.agents[agent] = None - def remove_agent(self, agent: Agent) -> None: + def remove_agent(self, agent: CellAgent) -> None: """Removes an agent from the cell.""" self.agents.pop(agent, None) agent.cell = None @@ -161,6 +161,7 @@ def select_random_agent(self): return self.random.choice(list(self.agents)) def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): + # FIXME: n is not considered if filter_func is None and n == 0: return self @@ -178,7 +179,10 @@ class DiscreteSpace: # FIXME:: defaulting to the same rng as model.random. # FIXME:: all children should be also like that. - def __init__(self, capacity): + def __init__( + self, + capacity: int | None = None, + ): super().__init__() self.capacity = capacity self.cells: dict[Coordinate, Cell] = {} @@ -195,7 +199,7 @@ def select_random_empty_cell(self) -> Cell: ... def _initialize_empties(self): - self._empties = self._empties = { + self._empties = { cell.coordinate: None for cell in self.cells.values() if cell.is_empty } self.cutoff_empties = 7.953 * len(self.cells) ** 0.384 @@ -240,11 +244,7 @@ def select_random_empty_cell(self) -> Cell: # 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 = ( - self.random.randrange(self.width), - self.random.randrange(self.height), - ) - cell = self.cells[new_pos] + cell = self.all_cells.select_random_cell() if cell.is_empty: break else: @@ -261,7 +261,7 @@ def __init__( height: int, torus: bool = False, moore: bool = True, - capacity: int = 1, + capacity: int | None = None, ) -> None: """Orthogonal grid @@ -325,7 +325,7 @@ def __init__( """ super().__init__(width, height, torus, capacity) self.cells = { - (i, j): Cell(i, j, self, capacity) + (i, j): Cell((i, j), self, capacity) for j in range(width) for i in range(height) } @@ -360,7 +360,7 @@ def _connect_single_cell(self, cell): class NetworkGrid(DiscreteSpace): - def __init__(self, G: Any, capacity: int = 1) -> None: + def __init__(self, G: Any, capacity: int | None = None) -> None: """A Networked grid Args: From 1ebcf0624eb0286e4552dbb0a9ab8dc300ee84a6 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 29 Jan 2024 20:40:29 +0100 Subject: [PATCH 22/63] initial tests for cell_space --- mesa/experimental/cell_space.py | 23 +++--- tests/test_cell_space.py | 123 ++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 tests/test_cell_space.py diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 40bcc1f1e40..993d49825c3 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -112,17 +112,22 @@ def _neighborhood(self, radius=1, include_center=False): if radius < 1: raise ValueError("radius must be larger than one") if radius == 1: - return {neighbor: neighbor.agents for neighbor in self._connections} + neighborhood = {neighbor: neighbor.agents for neighbor in self._connections} + if not include_center: + return neighborhood + else: + neighborhood[self] = self.agents + return neighborhood else: neighborhood = {} for neighbor in self._connections: - neighborhood.update(neighbor._neighborhood(radius - 1, include_center)) + neighborhood.update(neighbor._neighborhood(radius - 1, include_center=True)) if not include_center: neighborhood.pop(self, None) return neighborhood def __repr__(self): - return f"Cell({self.coords})" + return f"Cell({self.coordinate})" class CellCollection: @@ -139,7 +144,7 @@ def __iter__(self): def __getitem__(self, key: Cell) -> Iterable[Agent]: return self._cells[key] - @cached_property + # @cached_property def __len__(self) -> int: return len(self._cells) @@ -219,6 +224,12 @@ def __getitem__(self, key): def empties(self) -> CellCollection: return self.all_cells.select(lambda cell: cell.is_empty) + def select_random_empty_cell(self) -> Cell: + if not self.empties_initialized: + self._initialize_empties() + + return self.random.choice(self._empties) + class Grid(DiscreteSpace): def __init__( @@ -383,8 +394,4 @@ def _connect_single_cell(self, cell): ] cell.connect(neighbors) - def select_random_empty_cell(self) -> Cell: - if not self.empties_initialized: - self._initialize_empties() - return self.random.choice(self._empties) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py new file mode 100644 index 00000000000..fe769b13fa1 --- /dev/null +++ b/tests/test_cell_space.py @@ -0,0 +1,123 @@ + +from mesa.experimental.cell_space import CellAgent, CellCollection, OrthogonalGrid, HexGrid, NetworkGrid + + + +def test_orthogonal_grid(): + width = 10 + height = 10 + grid = OrthogonalGrid(width, height, torus=False, moore=False, capacity=None) + + assert len(grid.cells) == width * height + + # von neumann neighborhood, torus false, top corner + assert len(grid.cells[(0, 0)]._connections) == 2 + for connection in grid.cells[(0, 0)]._connections: + assert connection.coordinate in {(0, 1), (1, 0)} + + # von neumann neighborhood middle of grid + assert len(grid.cells[(5, 5)]._connections) == 4 + for connection in grid.cells[(5, 5)]._connections: + assert connection.coordinate in {(4, 5), (5, 4), (5,6), (6,5)} + + # von neumann neighborhood, torus True, top corner + grid = OrthogonalGrid(width, height, torus=True, moore=False, capacity=None) + assert len(grid.cells[(0, 0)]._connections) == 4 + for connection in grid.cells[(0, 0)]._connections: + assert connection.coordinate in {(0, 1), (1, 0), (0, 9), (9,0)} + + + # Moore neighborhood, torus false, top corner + grid = OrthogonalGrid(width, height, torus=False, moore=True, capacity=None) + assert len(grid.cells[(0, 0)]._connections) == 3 + for connection in grid.cells[(0, 0)]._connections: + assert connection.coordinate in {(0, 1), (1, 0), (1,1)} + + # Moore neighborhood middle of grid + assert len(grid.cells[(5, 5)]._connections) == 8 + for connection in grid.cells[(5, 5)]._connections: + # fmt: off + assert connection.coordinate in {(4, 4), (4, 5), (4, 6), + (5, 4), (5, 6), + (6, 4), (6, 5), (6, 6)} + # fmt: on + + # Moore neighborhood, torus True, top corner + grid = OrthogonalGrid(10, 10, torus=True, moore=True, capacity=None) + assert len(grid.cells[(0, 0)]._connections) == 8 + for connection in grid.cells[(0, 0)]._connections: + # fmt: off + assert connection.coordinate in {(9, 9), (9, 0), (9, 1), + (0, 9), (0, 1), + (1, 9), (1, 0), (1, 1)} + # fmt: on + +def test_cell_neighborhood(): + # orthogonal grid + width = 10 + height = 10 + + ## von Neumann + grid = OrthogonalGrid(width, height, torus=False, moore=False, capacity=None) + for radius, n in zip(range(1, 4), [2, 5, 9]): + neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) + assert len(neighborhood) == n + + width = 10 + height = 10 + + grid = OrthogonalGrid(width, height, torus=False, moore=True, capacity=None) + for radius, n in zip(range(1, 4), [3, 8, 15]): + neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) + assert len(neighborhood) == n + + ## Moore + + + # hexgrid + + # networkgrid + + +def test_hexgrid(): + width = 10 + height = 10 + + grid = HexGrid(width, height, torus=False) + assert len(grid.cells) == width * height + + # first row + assert len(grid.cells[(0, 0)]._connections) == 2 + for connection in grid.cells[(0, 0)]._connections: + assert connection.coordinate in {(0, 1), (1, 0)} + + # second row + assert len(grid.cells[(1, 0)]._connections) == 5 + for connection in grid.cells[(1, 0)]._connections: + # fmt: off + assert connection.coordinate in {(0, 0), (0, 1), + (1, 1), + (2, 0), (2, 1)} + + # middle odd row + assert len(grid.cells[(5, 5)]._connections) == 6 + for connection in grid.cells[(5, 5)]._connections: + # fmt: off + assert connection.coordinate in {(4, 5), (4, 6), + (5, 4), (5, 6), + (6, 5), (6, 6)} + + # fmt: on + + # middle even row + assert len(grid.cells[(4, 4)]._connections) == 6 + for connection in grid.cells[(4, 4)]._connections: + # fmt: off + assert connection.coordinate in {(3, 3), (3, 4), + (4, 3), (4, 5), + (5, 3), (5, 4)} + + # fmt: on + +# def test_networkgrid(): +# grid = NetworkGrid() \ No newline at end of file From d362baeeedc4922ce2099c5cf2660e89983b260e Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 29 Jan 2024 21:37:00 +0100 Subject: [PATCH 23/63] some additional tests --- tests/test_cell_space.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index fe769b13fa1..13f1987c1d7 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -1,3 +1,4 @@ +import pytest from mesa.experimental.cell_space import CellAgent, CellCollection, OrthogonalGrid, HexGrid, NetworkGrid @@ -71,6 +72,9 @@ def test_cell_neighborhood(): neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n + with pytest.raises(ValueError): + grid.cells[(0, 0)].neighborhood(radius=0) + ## Moore @@ -119,5 +123,18 @@ def test_hexgrid(): # fmt: on + grid = HexGrid(width, height, torus=True) + assert len(grid.cells) == width * height + + # first row + assert len(grid.cells[(0, 0)]._connections) == 6 + for connection in grid.cells[(0, 0)]._connections: + # fmt: off + assert connection.coordinate in {(9, 9), (9, 0), + (0, 9), (0, 1), + (1, 9), (1, 0)} + + # fmt: on + # def test_networkgrid(): # grid = NetworkGrid() \ No newline at end of file From c7d82d0806f52a5782455f2496ce823741f2348c Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 30 Jan 2024 20:52:09 +0100 Subject: [PATCH 24/63] additional unit tests --- mesa/experimental/cell_space.py | 18 +++------- tests/test_cell_space.py | 60 ++++++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py index 993d49825c3..d8081bfbc9c 100644 --- a/mesa/experimental/cell_space.py +++ b/mesa/experimental/cell_space.py @@ -126,9 +126,6 @@ def _neighborhood(self, radius=1, include_center=False): neighborhood.pop(self, None) return neighborhood - def __repr__(self): - return f"Cell({self.coordinate})" - class CellCollection: def __init__(self, cells: dict[Cell, list[Agent]] | Iterable[Cell]) -> None: @@ -159,10 +156,10 @@ def cells(self): def agents(self) -> Iterable[Agent]: return itertools.chain.from_iterable(self._cells.values()) - def select_random_cell(self): + def select_random_cell(self) -> Cell: return self.random.choice(self.cells) - def select_random_agent(self): + def select_random_agent(self) -> CellAgent: return self.random.choice(list(self.agents)) def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): @@ -200,9 +197,6 @@ def __init__( def _connect_single_cell(self, cell): ... - def select_random_empty_cell(self) -> Cell: - ... - def _initialize_empties(self): self._empties = { cell.coordinate: None for cell in self.cells.values() if cell.is_empty @@ -228,7 +222,7 @@ def select_random_empty_cell(self) -> Cell: if not self.empties_initialized: self._initialize_empties() - return self.random.choice(self._empties) + return self.cells[self.random.choice(list(self._empties))] class Grid(DiscreteSpace): @@ -389,9 +383,7 @@ def __init__(self, G: Any, capacity: int | None = None) -> None: self._connect_single_cell(cell) def _connect_single_cell(self, cell): - neighbors = [ - self.cells[node_id] for node_id in self.G.neighbors(cell.coordinate) - ] - cell.connect(neighbors) + for node_id in self.G.neighbors(cell.coordinate): + cell.connect( self.cells[node_id]) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 13f1987c1d7..358f1a1aa49 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -1,5 +1,6 @@ import pytest +from mesa import Model from mesa.experimental.cell_space import CellAgent, CellCollection, OrthogonalGrid, HexGrid, NetworkGrid @@ -55,18 +56,19 @@ def test_orthogonal_grid(): def test_cell_neighborhood(): # orthogonal grid - width = 10 - height = 10 + ## von Neumann + width = 10 + height = 10 grid = OrthogonalGrid(width, height, torus=False, moore=False, capacity=None) for radius, n in zip(range(1, 4), [2, 5, 9]): neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n + ## Moore width = 10 height = 10 - grid = OrthogonalGrid(width, height, torus=False, moore=True, capacity=None) for radius, n in zip(range(1, 4), [3, 8, 15]): neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) @@ -75,10 +77,20 @@ def test_cell_neighborhood(): with pytest.raises(ValueError): grid.cells[(0, 0)].neighborhood(radius=0) - ## Moore - - # hexgrid + width = 10 + height = 10 + grid = HexGrid(width, height, torus=False, capacity=None) + for radius, n in zip(range(1, 4), [2, 6, 11]): + neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) + assert len(neighborhood) == n + + width = 10 + height = 10 + grid = HexGrid(width, height, torus=False, capacity=None) + for radius, n in zip(range(1, 4), [5, 10, 17]): + neighborhood = grid.cells[(1, 0)].neighborhood(radius=radius) + assert len(neighborhood) == n # networkgrid @@ -136,5 +148,37 @@ def test_hexgrid(): # fmt: on -# def test_networkgrid(): -# grid = NetworkGrid() \ No newline at end of file +def test_networkgrid(): + import networkx as nx + + n = 10 + m = 20 + seed = 42 + G = nx.gnm_random_graph(n, m, seed=seed) + grid = NetworkGrid(G) + + assert len(grid.cells) == n + + for i, cell in grid.cells.items(): + for connection in cell._connections: + assert connection.coordinate in G.neighbors(i) + + +def test_empties_space(): + import networkx as nx + + n = 10 + m = 20 + seed = 42 + G = nx.gnm_random_graph(n, m, seed=seed) + grid = NetworkGrid(G) + + assert len(grid.empties) == n + + + model = Model() + for i in range(8): + grid.cells[i].add_agent(CellAgent(i, model)) + + cell = grid.select_random_empty_cell() + assert cell.coordinate in {8, 9} From b2842c04ed1974c598ea0d4431552ab441aa2d87 Mon Sep 17 00:00:00 2001 From: Corvince Date: Wed, 31 Jan 2024 15:39:29 +0100 Subject: [PATCH 25/63] restructure files and folders --- mesa/experimental/__init__.py | 4 + mesa/experimental/cell_space.py | 389 ------------------ mesa/experimental/cell_space/__init__.py | 17 + mesa/experimental/cell_space/cell.py | 102 +++++ mesa/experimental/cell_space/cell_agent.py | 30 ++ .../cell_space/cell_collection.py | 60 +++ .../experimental/cell_space/discrete_space.py | 55 +++ mesa/experimental/cell_space/grid.py | 140 +++++++ mesa/experimental/cell_space/network.py | 27 ++ tests/test_cell_space.py | 31 +- 10 files changed, 452 insertions(+), 403 deletions(-) delete mode 100644 mesa/experimental/cell_space.py create mode 100644 mesa/experimental/cell_space/__init__.py create mode 100644 mesa/experimental/cell_space/cell.py create mode 100644 mesa/experimental/cell_space/cell_agent.py create mode 100644 mesa/experimental/cell_space/cell_collection.py create mode 100644 mesa/experimental/cell_space/discrete_space.py create mode 100644 mesa/experimental/cell_space/grid.py create mode 100644 mesa/experimental/cell_space/network.py diff --git a/mesa/experimental/__init__.py b/mesa/experimental/__init__.py index c04f22589b3..961b8762791 100644 --- a/mesa/experimental/__init__.py +++ b/mesa/experimental/__init__.py @@ -1 +1,5 @@ from .jupyter_viz import JupyterViz, make_text, Slider # noqa +from mesa.experimental import cell_space + + +__all__ = ["JupyterViz", "make_text", "Slider", "cell_space"] diff --git a/mesa/experimental/cell_space.py b/mesa/experimental/cell_space.py deleted file mode 100644 index d8081bfbc9c..00000000000 --- a/mesa/experimental/cell_space.py +++ /dev/null @@ -1,389 +0,0 @@ -import itertools -import random -from collections.abc import Iterable -from functools import cache, cached_property -from typing import Any, Callable, Optional - -from .. import Agent, Model - -Coordinate = tuple[int, int] - - -class CellAgent(Agent): - """ - Base class for a model agent in Mesa. - - Attributes: - unique_id (int): A unique identifier for this agent. - model (Model): A reference to the model instance. - self.pos: Position | None = None - """ - - def __init__(self, unique_id: int, 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__(unique_id, model) - self.cell: Cell | None = None - - def move_to(self, cell) -> None: - if self.cell is not None: - self.cell.remove_agent(self) - self.cell = cell - cell.add_agent(self) - - -class Cell: - __slots__ = [ - "coordinate", - "_connections", - "owner", - "agents", - "capacity", - "properties", - ] - - def __init__(self, coordinate, owner, capacity: int | None = None) -> None: - self.coordinate = coordinate - self._connections: list[Cell] = [] # TODO: change to CellCollection? - self.agents: dict[ - Agent, None - ] = {} # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) - self.capacity = capacity - self.properties: dict[str, object] = {} - self.owner = owner - - def connect(self, other) -> None: - """Connects this cell to another cell.""" - self._connections.append(other) - - def disconnect(self, other) -> None: - """Disconnects this cell from another cell.""" - self._connections.remove(other) - - def add_agent(self, agent: CellAgent) -> None: - """Adds an agent to the cell.""" - n = len(self.agents) - - if n == 0: - self.owner._empties.pop(self.coordinate, None) - - if self.capacity and n >= self.capacity: - raise Exception( - "ERROR: Cell is full" - ) # FIXME we need MESA errors or a proper error - - self.agents[agent] = None - - def remove_agent(self, agent: CellAgent) -> None: - """Removes an agent from the cell.""" - self.agents.pop(agent, None) - agent.cell = None - if len(self.agents) == 0: - self.owner._empties[self.coordinate] = None - - @property - def is_empty(self) -> bool: - """Returns a bool of the contents of a cell.""" - return len(self.agents) == 0 - - @property - def is_full(self) -> bool: - """Returns a bool of the contents of a cell.""" - return len(self.agents) == self.capacity - - def __repr__(self): - return f"Cell({self.coordinate}, {self.agents})" - - @cache - def neighborhood(self, radius=1, include_center=False): - return CellCollection( - self._neighborhood(radius=radius, include_center=include_center) - ) - - @cache - def _neighborhood(self, radius=1, include_center=False): - # if radius == 0: - # return {self: self.agents} - if radius < 1: - raise ValueError("radius must be larger than one") - if radius == 1: - neighborhood = {neighbor: neighbor.agents for neighbor in self._connections} - if not include_center: - return neighborhood - else: - neighborhood[self] = self.agents - return neighborhood - else: - neighborhood = {} - for neighbor in self._connections: - neighborhood.update(neighbor._neighborhood(radius - 1, include_center=True)) - if not include_center: - neighborhood.pop(self, None) - return neighborhood - - -class CellCollection: - def __init__(self, cells: dict[Cell, list[Agent]] | Iterable[Cell]) -> None: - if isinstance(cells, dict): - self._cells = cells - else: - self._cells = {cell: cell.agents for cell in cells} - self.random = random # FIXME - - def __iter__(self): - return iter(self._cells) - - def __getitem__(self, key: Cell) -> Iterable[Agent]: - return self._cells[key] - - # @cached_property - def __len__(self) -> int: - return len(self._cells) - - def __repr__(self): - return f"CellCollection({self._cells})" - - @cached_property - def cells(self): - return list(self._cells.keys()) - - @property - def agents(self) -> Iterable[Agent]: - return itertools.chain.from_iterable(self._cells.values()) - - def select_random_cell(self) -> Cell: - return self.random.choice(self.cells) - - def select_random_agent(self) -> CellAgent: - return self.random.choice(list(self.agents)) - - def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): - # FIXME: n is not considered - if filter_func is None and n == 0: - return self - - return CellCollection( - { - cell: agents - for cell, agents in self._cells.items() - if filter_func is None or filter_func(cell) - } - ) - - -class DiscreteSpace: - # FIXME:: random should become a keyword argument - # FIXME:: defaulting to the same rng as model.random. - # FIXME:: all children should be also like that. - - def __init__( - self, - capacity: int | None = None, - ): - super().__init__() - self.capacity = capacity - self.cells: dict[Coordinate, Cell] = {} - self.random = random # FIXME - - self._empties = {} - self.cutoff_empties = -1 - self.empties_initialized = False - - def _connect_single_cell(self, cell): - ... - - def _initialize_empties(self): - self._empties = { - cell.coordinate: None for cell in self.cells.values() if cell.is_empty - } - self.cutoff_empties = 7.953 * len(self.cells) ** 0.384 - self.empties_initialized = True - - @cached_property - def all_cells(self): - return CellCollection({cell: cell.agents for cell in self.cells.values()}) - - def __iter__(self): - return iter(self.cells.values()) - - def __getitem__(self, key): - return self.cells[key] - - @property - def empties(self) -> CellCollection: - return self.all_cells.select(lambda cell: cell.is_empty) - - def select_random_empty_cell(self) -> Cell: - if not self.empties_initialized: - self._initialize_empties() - - return self.cells[self.random.choice(list(self._empties))] - - -class Grid(DiscreteSpace): - def __init__( - self, width: int, height: int, torus: bool = False, capacity: int = 1 - ) -> None: - super().__init__(capacity) - self.torus = torus - self.width = width - self.height = height - - def select_random_empty_cell(self) -> Cell: - if not self.empties_initialized: - self._initialize_empties() - - 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: - cell = self.all_cells.select_random_cell() - if cell.is_empty: - break - else: - coordinate = self.random.choice(list(self._empties)) - cell = self.cells[coordinate] - - return cell - - -class OrthogonalGrid(Grid): - def __init__( - self, - width: int, - height: int, - torus: bool = False, - moore: bool = True, - capacity: int | None = None, - ) -> None: - """Orthogonal grid - - Args: - width (int): width of the grid - height (int): height of the grid - torus (bool): whether the space is a torus - moore (bool): whether the space used Moore or von Neumann neighborhood - capacity (int): the number of agents that can simultaneously occupy a cell - - """ - super().__init__(width, height, torus, capacity) - self.moore = moore - self.cells = { - (i, j): Cell((i, j), self, capacity) - for j in range(width) - for i in range(height) - } - - for cell in self.all_cells: - self._connect_single_cell(cell) - - def _connect_single_cell(self, cell): - i, j = cell.coordinate - - # fmt: off - if self.moore: - directions = [ - (-1, -1), (-1, 0), (-1, 1), - ( 0, -1), ( 0, 1), - ( 1, -1), ( 1, 0), ( 1, 1), - ] - else: # Von Neumann neighborhood - directions = [ - (-1, 0), - (0, -1), (0, 1), - ( 1, 0), - ] - # fmt: on - - for di, dj in directions: - ni, nj = (i + di, j + dj) - if self.torus: - ni, nj = ni % self.height, nj % self.width - if 0 <= ni < self.height and 0 <= nj < self.width: - cell.connect(self.cells[ni, nj]) - - -class HexGrid(Grid): - def __init__( - self, width: int, height: int, torus: bool = False, capacity=1 - ) -> None: - """Hexagonal Grid - - Args: - width (int): width of the grid - height (int): height of the grid - torus (bool): whether the space is a torus - capacity (int): the number of agents that can simultaneously occupy a cell - - """ - super().__init__(width, height, torus, capacity) - self.cells = { - (i, j): Cell((i, j), self, capacity) - for j in range(width) - for i in range(height) - } - - for cell in self.all_cells: - self._connect_single_cell(cell) - - def _connect_single_cell(self, cell): - i, j = cell.coordinate - - # fmt: off - if i % 2 == 0: - directions = [ - (-1, -1), (-1, 0), - (0, -1), (0, 1), - ( 1, -1), (1, 0), - ] - else: - directions = [ - (-1, 0), (-1, 1), - (0, -1), (0, 1), - ( 1, 0), (1, 1), - ] - # fmt: on - - for di, dj in directions: - ni, nj = (i + di, j + dj) - if self.torus: - ni, nj = ni % self.height, nj % self.width - if 0 <= ni < self.height and 0 <= nj < self.width: - cell.connect(self.cells[ni, nj]) - - -class NetworkGrid(DiscreteSpace): - def __init__(self, G: Any, capacity: int | None = None) -> None: - """A Networked grid - - Args: - G: a NetworkX Graph instance. - capacity (int) : the capacity of the cell - - """ - super().__init__(capacity) - self.G = G - - for node_id in self.G.nodes: - self.cells[node_id] = Cell(node_id, self, capacity) - - for cell in self.all_cells: - self._connect_single_cell(cell) - - def _connect_single_cell(self, cell): - for node_id in self.G.neighbors(cell.coordinate): - cell.connect( self.cells[node_id]) - - diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py new file mode 100644 index 00000000000..f5a4af97ca0 --- /dev/null +++ b/mesa/experimental/cell_space/__init__.py @@ -0,0 +1,17 @@ +from mesa.experimental.cell_space.cell import Cell +from mesa.experimental.cell_space.cell_agent import CellAgent +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 Grid, HexGrid, OrthogonalGrid +from mesa.experimental.cell_space.network import Network + +__all__ = [ + "CellCollection", + "Cell", + "CellAgent", + "DiscreteSpace", + "Grid", + "HexGrid", + "OrthogonalGrid", + "Network", +] diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py new file mode 100644 index 00000000000..f124ba0112e --- /dev/null +++ b/mesa/experimental/cell_space/cell.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +from functools import cache +from typing import TYPE_CHECKING + +from mesa.agent import Agent +from mesa.experimental.cell_space.cell_collection import CellCollection + +if TYPE_CHECKING: + from mesa.experimental.cell_space.cell_agent import CellAgent + + +class Cell: + __slots__ = [ + "coordinate", + "_connections", + "owner", + "agents", + "capacity", + "properties", + ] + + def __init__(self, coordinate, owner, capacity: int | None = None) -> None: + self.coordinate = coordinate + self._connections: list[Cell] = [] # TODO: change to CellCollection? + self.agents: dict[ + Agent, None + ] = {} # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) + self.capacity = capacity + self.properties: dict[str, object] = {} + self.owner = owner + + def connect(self, other) -> None: + """Connects this cell to another cell.""" + self._connections.append(other) + + def disconnect(self, other) -> None: + """Disconnects this cell from another cell.""" + self._connections.remove(other) + + def add_agent(self, agent: CellAgent) -> None: + """Adds an agent to the cell.""" + n = len(self.agents) + + if n == 0: + self.owner._empties.pop(self.coordinate, None) + + if self.capacity and n >= self.capacity: + raise Exception( + "ERROR: Cell is full" + ) # FIXME we need MESA errors or a proper error + + self.agents[agent] = None + + def remove_agent(self, agent: CellAgent) -> None: + """Removes an agent from the cell.""" + self.agents.pop(agent, None) + agent.cell = None + if len(self.agents) == 0: + self.owner._empties[self.coordinate] = None + + @property + def is_empty(self) -> bool: + """Returns a bool of the contents of a cell.""" + return len(self.agents) == 0 + + @property + def is_full(self) -> bool: + """Returns a bool of the contents of a cell.""" + return len(self.agents) == self.capacity + + def __repr__(self): + return f"Cell({self.coordinate}, {self.agents})" + + @cache + def neighborhood(self, radius=1, include_center=False): + return CellCollection( + self._neighborhood(radius=radius, include_center=include_center) + ) + + @cache + def _neighborhood(self, radius=1, include_center=False): + # if radius == 0: + # return {self: self.agents} + if radius < 1: + raise ValueError("radius must be larger than one") + if radius == 1: + neighborhood = {neighbor: neighbor.agents for neighbor in self._connections} + if not include_center: + return neighborhood + else: + neighborhood[self] = self.agents + return neighborhood + else: + neighborhood = {} + for neighbor in self._connections: + neighborhood.update( + neighbor._neighborhood(radius - 1, include_center=True) + ) + if not include_center: + neighborhood.pop(self, None) + return neighborhood diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py new file mode 100644 index 00000000000..de05c29c93e --- /dev/null +++ b/mesa/experimental/cell_space/cell_agent.py @@ -0,0 +1,30 @@ +from mesa import Agent, Model +from mesa.experimental.cell_space.cell import Cell + + +class CellAgent(Agent): + """ + Base class for a model agent in Mesa. + + Attributes: + unique_id (int): A unique identifier for this agent. + model (Model): A reference to the model instance. + self.pos: Position | None = None + """ + + def __init__(self, unique_id: int, 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__(unique_id, model) + self.cell: Cell | None = None + + def move_to(self, cell) -> None: + if self.cell is not None: + self.cell.remove_agent(self) + self.cell = cell + cell.add_agent(self) diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py new file mode 100644 index 00000000000..188d435a56b --- /dev/null +++ b/mesa/experimental/cell_space/cell_collection.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import itertools +import random +from collections.abc import Iterable +from functools import cached_property +from typing import TYPE_CHECKING, Callable, Optional + +if TYPE_CHECKING: + from mesa.experimental.cell_space.cell import Cell + from mesa.experimental.cell_space.cell_agent import CellAgent + + +class CellCollection: + def __init__(self, cells: dict[Cell, list[CellAgent]] | Iterable[Cell]) -> None: + if isinstance(cells, dict): + self._cells = cells + else: + self._cells = {cell: cell.agents for cell in cells} + self.random = random # FIXME + + def __iter__(self): + return iter(self._cells) + + def __getitem__(self, key: Cell) -> Iterable[CellAgent]: + return self._cells[key] + + # @cached_property + def __len__(self) -> int: + return len(self._cells) + + def __repr__(self): + return f"CellCollection({self._cells})" + + @cached_property + def cells(self): + return list(self._cells.keys()) + + @property + def agents(self) -> Iterable[CellAgent]: + return itertools.chain.from_iterable(self._cells.values()) + + def select_random_cell(self) -> Cell: + return self.random.choice(self.cells) + + def select_random_agent(self) -> CellAgent: + return self.random.choice(list(self.agents)) + + def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): + # FIXME: n is not considered + if filter_func is None and n == 0: + return self + + return CellCollection( + { + cell: agents + for cell, agents in self._cells.items() + if filter_func is None or filter_func(cell) + } + ) diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py new file mode 100644 index 00000000000..9b203035c13 --- /dev/null +++ b/mesa/experimental/cell_space/discrete_space.py @@ -0,0 +1,55 @@ +import random +from functools import cached_property + +from mesa.experimental.cell_space.cell import Cell +from mesa.experimental.cell_space.cell_collection import CellCollection +from mesa.space import Coordinate + + +class DiscreteSpace: + # FIXME:: random should become a keyword argument + # FIXME:: defaulting to the same rng as model.random. + # FIXME:: all children should be also like that. + + def __init__( + self, + capacity: int | None = None, + ): + super().__init__() + self.capacity = capacity + self.cells: dict[Coordinate, Cell] = {} + self.random = random # FIXME + + self._empties: dict[Coordinate, None] = {} + self.cutoff_empties = -1 + self.empties_initialized = False + + def _connect_single_cell(self, cell): + ... + + def _initialize_empties(self): + self._empties = { + cell.coordinate: None for cell in self.cells.values() if cell.is_empty + } + self.cutoff_empties = 7.953 * len(self.cells) ** 0.384 + self.empties_initialized = True + + @cached_property + def all_cells(self): + return CellCollection({cell: cell.agents for cell in self.cells.values()}) + + def __iter__(self): + return iter(self.cells.values()) + + def __getitem__(self, key): + return self.cells[key] + + @property + def empties(self) -> CellCollection: + return self.all_cells.select(lambda cell: cell.is_empty) + + def select_random_empty_cell(self) -> Cell: + if not self.empties_initialized: + self._initialize_empties() + + return self.cells[self.random.choice(list(self._empties))] diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py new file mode 100644 index 00000000000..880b198aee2 --- /dev/null +++ b/mesa/experimental/cell_space/grid.py @@ -0,0 +1,140 @@ +from mesa.experimental.cell_space import Cell, DiscreteSpace + + +class Grid(DiscreteSpace): + def __init__( + self, width: int, height: int, torus: bool = False, capacity: int | None = None + ) -> None: + super().__init__(capacity) + self.torus = torus + self.width = width + self.height = height + + def select_random_empty_cell(self) -> Cell: + if not self.empties_initialized: + self._initialize_empties() + + 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: + cell = self.all_cells.select_random_cell() + if cell.is_empty: + break + else: + coordinate = self.random.choice(list(self._empties)) + cell = self.cells[coordinate] + + return cell + + +class OrthogonalGrid(Grid): + def __init__( + self, + width: int, + height: int, + torus: bool = False, + moore: bool = True, + capacity: int | None = None, + ) -> None: + """Orthogonal grid + + Args: + width (int): width of the grid + height (int): height of the grid + torus (bool): whether the space is a torus + moore (bool): whether the space used Moore or von Neumann neighborhood + capacity (int): the number of agents that can simultaneously occupy a cell + + """ + super().__init__(width, height, torus, capacity) + self.moore = moore + self.cells = { + (i, j): Cell((i, j), self, capacity) + for j in range(width) + for i in range(height) + } + + for cell in self.all_cells: + self._connect_single_cell(cell) + + def _connect_single_cell(self, cell): + i, j = cell.coordinate + + # fmt: off + if self.moore: + directions = [ + (-1, -1), (-1, 0), (-1, 1), + ( 0, -1), ( 0, 1), + ( 1, -1), ( 1, 0), ( 1, 1), + ] + else: # Von Neumann neighborhood + directions = [ + (-1, 0), + (0, -1), (0, 1), + ( 1, 0), + ] + # fmt: on + + for di, dj in directions: + ni, nj = (i + di, j + dj) + if self.torus: + ni, nj = ni % self.height, nj % self.width + if 0 <= ni < self.height and 0 <= nj < self.width: + cell.connect(self.cells[ni, nj]) + + +class HexGrid(Grid): + def __init__( + self, width: int, height: int, torus: bool = False, capacity=1 + ) -> None: + """Hexagonal Grid + + Args: + width (int): width of the grid + height (int): height of the grid + torus (bool): whether the space is a torus + capacity (int): the number of agents that can simultaneously occupy a cell + + """ + super().__init__(width, height, torus, capacity) + self.cells = { + (i, j): Cell((i, j), self, capacity) + for j in range(width) + for i in range(height) + } + + for cell in self.all_cells: + self._connect_single_cell(cell) + + def _connect_single_cell(self, cell): + i, j = cell.coordinate + + # fmt: off + if i % 2 == 0: + directions = [ + (-1, -1), (-1, 0), + (0, -1), (0, 1), + ( 1, -1), (1, 0), + ] + else: + directions = [ + (-1, 0), (-1, 1), + (0, -1), (0, 1), + ( 1, 0), (1, 1), + ] + # fmt: on + + for di, dj in directions: + ni, nj = (i + di, j + dj) + if self.torus: + ni, nj = ni % self.height, nj % self.width + if 0 <= ni < self.height and 0 <= nj < self.width: + cell.connect(self.cells[ni, nj]) diff --git a/mesa/experimental/cell_space/network.py b/mesa/experimental/cell_space/network.py new file mode 100644 index 00000000000..818d2604866 --- /dev/null +++ b/mesa/experimental/cell_space/network.py @@ -0,0 +1,27 @@ +from typing import Any + +from mesa.experimental.cell_space.cell import Cell +from mesa.experimental.cell_space.discrete_space import DiscreteSpace + + +class Network(DiscreteSpace): + def __init__(self, G: Any, capacity: int | None = None) -> None: + """A Networked grid + + Args: + G: a NetworkX Graph instance. + capacity (int) : the capacity of the cell + + """ + super().__init__(capacity) + self.G = G + + for node_id in self.G.nodes: + self.cells[node_id] = Cell(node_id, self, capacity) + + for cell in self.all_cells: + self._connect_single_cell(cell) + + def _connect_single_cell(self, cell): + for node_id in self.G.neighbors(cell.coordinate): + cell.connect(self.cells[node_id]) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 358f1a1aa49..3d87779df6e 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -1,8 +1,12 @@ import pytest from mesa import Model -from mesa.experimental.cell_space import CellAgent, CellCollection, OrthogonalGrid, HexGrid, NetworkGrid - +from mesa.experimental.cell_space import ( + CellAgent, + HexGrid, + Network, + OrthogonalGrid, +) def test_orthogonal_grid(): @@ -20,20 +24,19 @@ def test_orthogonal_grid(): # von neumann neighborhood middle of grid assert len(grid.cells[(5, 5)]._connections) == 4 for connection in grid.cells[(5, 5)]._connections: - assert connection.coordinate in {(4, 5), (5, 4), (5,6), (6,5)} + assert connection.coordinate in {(4, 5), (5, 4), (5, 6), (6, 5)} # von neumann neighborhood, torus True, top corner grid = OrthogonalGrid(width, height, torus=True, moore=False, capacity=None) assert len(grid.cells[(0, 0)]._connections) == 4 for connection in grid.cells[(0, 0)]._connections: - assert connection.coordinate in {(0, 1), (1, 0), (0, 9), (9,0)} - + assert connection.coordinate in {(0, 1), (1, 0), (0, 9), (9, 0)} # Moore neighborhood, torus false, top corner grid = OrthogonalGrid(width, height, torus=False, moore=True, capacity=None) assert len(grid.cells[(0, 0)]._connections) == 3 for connection in grid.cells[(0, 0)]._connections: - assert connection.coordinate in {(0, 1), (1, 0), (1,1)} + assert connection.coordinate in {(0, 1), (1, 0), (1, 1)} # Moore neighborhood middle of grid assert len(grid.cells[(5, 5)]._connections) == 8 @@ -54,15 +57,15 @@ def test_orthogonal_grid(): (1, 9), (1, 0), (1, 1)} # fmt: on + def test_cell_neighborhood(): # orthogonal grid - ## von Neumann width = 10 height = 10 grid = OrthogonalGrid(width, height, torus=False, moore=False, capacity=None) - for radius, n in zip(range(1, 4), [2, 5, 9]): + for radius, n in zip(range(1, 4), [2, 5, 9]): neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n @@ -70,7 +73,7 @@ def test_cell_neighborhood(): width = 10 height = 10 grid = OrthogonalGrid(width, height, torus=False, moore=True, capacity=None) - for radius, n in zip(range(1, 4), [3, 8, 15]): + for radius, n in zip(range(1, 4), [3, 8, 15]): neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n @@ -81,14 +84,14 @@ def test_cell_neighborhood(): width = 10 height = 10 grid = HexGrid(width, height, torus=False, capacity=None) - for radius, n in zip(range(1, 4), [2, 6, 11]): + for radius, n in zip(range(1, 4), [2, 6, 11]): neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n width = 10 height = 10 grid = HexGrid(width, height, torus=False, capacity=None) - for radius, n in zip(range(1, 4), [5, 10, 17]): + for radius, n in zip(range(1, 4), [5, 10, 17]): neighborhood = grid.cells[(1, 0)].neighborhood(radius=radius) assert len(neighborhood) == n @@ -148,6 +151,7 @@ def test_hexgrid(): # fmt: on + def test_networkgrid(): import networkx as nx @@ -155,7 +159,7 @@ def test_networkgrid(): m = 20 seed = 42 G = nx.gnm_random_graph(n, m, seed=seed) - grid = NetworkGrid(G) + grid = Network(G) assert len(grid.cells) == n @@ -171,11 +175,10 @@ def test_empties_space(): m = 20 seed = 42 G = nx.gnm_random_graph(n, m, seed=seed) - grid = NetworkGrid(G) + grid = Network(G) assert len(grid.empties) == n - model = Model() for i in range(8): grid.cells[i].add_agent(CellAgent(i, model)) From 740f0032173b86802f09ae8796063a23ec9bcf0a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 2 Feb 2024 19:14:40 +0100 Subject: [PATCH 26/63] various updates * new tests * pass through of random keyword argument * add CellKlass as an optional keyword argument --- benchmarks/Schelling/schelling.py | 2 +- benchmarks/WolfSheep/wolf_sheep.py | 2 +- mesa/experimental/cell_space/cell.py | 61 +++++++++----- .../cell_space/cell_collection.py | 11 ++- .../experimental/cell_space/discrete_space.py | 12 ++- mesa/experimental/cell_space/grid.py | 40 +++++++--- mesa/experimental/cell_space/network.py | 15 +++- tests/test_cell_space.py | 80 +++++++++++++++++++ 8 files changed, 182 insertions(+), 41 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 70eb8fd339d..c405ea2e761 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -50,7 +50,7 @@ def __init__( self.homophily = homophily self.schedule = RandomActivation(self) - self.grid = OrthogonalGrid(height, width, torus=True, capacity=1) + self.grid = OrthogonalGrid(height, width, torus=True, capacity=1, random=self.random) self.happy = 0 diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index a35cb4b190a..2b2159e4bec 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -161,7 +161,7 @@ def __init__( self.schedule = RandomActivationByType(self) self.grid = OrthogonalGrid( - self.height, self.width, moore=moore, torus=False, capacity=math.inf + self.height, self.width, moore=moore, torus=False, capacity=math.inf, random=self.random ) # Create sheep: diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index f124ba0112e..70bfa94f0b8 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -1,7 +1,8 @@ from __future__ import annotations +from random import Random from functools import cache -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from mesa.agent import Agent from mesa.experimental.cell_space.cell_collection import CellCollection @@ -14,50 +15,72 @@ class Cell: __slots__ = [ "coordinate", "_connections", - "owner", "agents", "capacity", "properties", + "random" ] - def __init__(self, coordinate, owner, capacity: int | None = None) -> None: + def __init__(self, coordinate: Any, capacity: int | None = None, random: Random = None) -> None: + """" + + Args: + coordinate: + capacity (int) : the capacity of the cell. If None, the capacity is infinite + random (Random) : the random number generator to use + + """ + super().__init__() self.coordinate = coordinate self._connections: list[Cell] = [] # TODO: change to CellCollection? - self.agents: dict[ - Agent, None - ] = {} # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) + self.agents = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) self.capacity = capacity self.properties: dict[str, object] = {} - self.owner = owner + self.random = random def connect(self, other) -> None: - """Connects this cell to another cell.""" + """Connects this cell to another cell. + + Args: + other (Cell): other cell to connect to + + """ self._connections.append(other) def disconnect(self, other) -> None: - """Disconnects this cell from another cell.""" + """Disconnects this cell from another cell. + + Args: + other (Cell): other cell to remove from connections + + """ self._connections.remove(other) def add_agent(self, agent: CellAgent) -> None: - """Adds an agent to the cell.""" - n = len(self.agents) + """Adds an agent to the cell. + + Args: + agent (CellAgent): agent to add to this Cell - if n == 0: - self.owner._empties.pop(self.coordinate, None) + """ + n = len(self.agents) if self.capacity and n >= self.capacity: raise Exception( "ERROR: Cell is full" ) # FIXME we need MESA errors or a proper error - self.agents[agent] = None + self.agents.append(agent) def remove_agent(self, agent: CellAgent) -> None: - """Removes an agent from the cell.""" - self.agents.pop(agent, None) + """Removes an agent from the cell. + + Args: + agent (CellAgent): agent to remove from this cell + + """ + self.agents.remove(agent) agent.cell = None - if len(self.agents) == 0: - self.owner._empties[self.coordinate] = None @property def is_empty(self) -> bool: @@ -75,7 +98,7 @@ def __repr__(self): @cache def neighborhood(self, radius=1, include_center=False): return CellCollection( - self._neighborhood(radius=radius, include_center=include_center) + self._neighborhood(radius=radius, include_center=include_center), random=self.random ) @cache diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 188d435a56b..0fca9714463 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -1,7 +1,7 @@ from __future__ import annotations import itertools -import random +from random import Random from collections.abc import Iterable from functools import cached_property from typing import TYPE_CHECKING, Callable, Optional @@ -12,12 +12,15 @@ class CellCollection: - def __init__(self, cells: dict[Cell, list[CellAgent]] | Iterable[Cell]) -> None: + def __init__(self, cells: dict[Cell, list[CellAgent]] | Iterable[Cell], random: Random = None) -> None: if isinstance(cells, dict): self._cells = cells else: self._cells = {cell: cell.agents for cell in cells} - self.random = random # FIXME + + if random is None: + random = Random() # FIXME + self.random = random def __iter__(self): return iter(self._cells) @@ -33,7 +36,7 @@ def __repr__(self): return f"CellCollection({self._cells})" @cached_property - def cells(self): + def cells(self) -> list[Cell]: return list(self._cells.keys()) @property diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 9b203035c13..4be77123c2b 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -1,4 +1,5 @@ -import random +import numbers +from random import Random from functools import cached_property from mesa.experimental.cell_space.cell import Cell @@ -8,17 +9,20 @@ class DiscreteSpace: # FIXME:: random should become a keyword argument - # FIXME:: defaulting to the same rng as model.random. - # FIXME:: all children should be also like that. def __init__( self, capacity: int | None = None, + CellKlass: type[Cell] = Cell, + random: Random = None ): super().__init__() self.capacity = capacity self.cells: dict[Coordinate, Cell] = {} - self.random = random # FIXME + if random is None: + random = Random() # FIXME should default to default rng from model + self.random = random + self.CellKlass = CellKlass self._empties: dict[Coordinate, None] = {} self.cutoff_empties = -1 diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 880b198aee2..37be7b0e2ac 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -1,11 +1,21 @@ from mesa.experimental.cell_space import Cell, DiscreteSpace - +from random import Random class Grid(DiscreteSpace): + """Base class for all grid and network classes + + + """ def __init__( - self, width: int, height: int, torus: bool = False, capacity: int | None = None + self, + width: int, + height: int, + torus: bool = False, + capacity: int | None = None, + random: Random = None, + CellKlass: type[Cell] = Cell, ) -> None: - super().__init__(capacity) + super().__init__(capacity=capacity, random=random, CellKlass=CellKlass) self.torus = torus self.width = width self.height = height @@ -43,6 +53,8 @@ def __init__( torus: bool = False, moore: bool = True, capacity: int | None = None, + random: Random = None, + CellKlass: type[Cell] = Cell ) -> None: """Orthogonal grid @@ -52,12 +64,15 @@ def __init__( torus (bool): whether the space is a torus moore (bool): whether the space used Moore or von Neumann neighborhood capacity (int): the number of agents that can simultaneously occupy a cell + random (random): + CellKlass (type[Cell]): The Cell class to use in the OrthogonalGrid + """ - super().__init__(width, height, torus, capacity) + super().__init__(width, height, torus, capacity=capacity, CellKlass=CellKlass, random=random) self.moore = moore self.cells = { - (i, j): Cell((i, j), self, capacity) + (i, j): self.CellKlass((i, j), capacity, random=self.random) for j in range(width) for i in range(height) } @@ -93,7 +108,13 @@ def _connect_single_cell(self, cell): class HexGrid(Grid): def __init__( - self, width: int, height: int, torus: bool = False, capacity=1 + self, + width: int, + height: int, + torus: bool = False, + capacity: int = None, + random: Random = None, + CellKlass: type[Cell] = Cell ) -> None: """Hexagonal Grid @@ -102,11 +123,12 @@ def __init__( height (int): height of the grid torus (bool): whether the space is a torus capacity (int): the number of agents that can simultaneously occupy a cell - + random (random): + CellKlass (type[Cell]): The Cell class to use in the HexGrid """ - super().__init__(width, height, torus, capacity) + super().__init__(width, height, torus, capacity=capacity, random=random, CellKlass=CellKlass) self.cells = { - (i, j): Cell((i, j), self, capacity) + (i, j): self.CellKlass((i, j), capacity, random=self.random) for j in range(width) for i in range(height) } diff --git a/mesa/experimental/cell_space/network.py b/mesa/experimental/cell_space/network.py index 818d2604866..90396ff4bf2 100644 --- a/mesa/experimental/cell_space/network.py +++ b/mesa/experimental/cell_space/network.py @@ -1,3 +1,4 @@ +from random import Random from typing import Any from mesa.experimental.cell_space.cell import Cell @@ -5,19 +6,27 @@ class Network(DiscreteSpace): - def __init__(self, G: Any, capacity: int | None = None) -> None: + def __init__( + self, + G: Any, + capacity: int | None = None, + random: Random = None, + CellKlass: type[Cell] = Cell + ) -> None: """A Networked grid Args: G: a NetworkX Graph instance. capacity (int) : the capacity of the cell + random (Random): + CellKlass (type[Cell]): The base Cell class to use in the Network """ - super().__init__(capacity) + super().__init__(capacity=capacity,random=random, CellKlass=CellKlass) self.G = G for node_id in self.G.nodes: - self.cells[node_id] = Cell(node_id, self, capacity) + self.cells[node_id] = self.CellKlass(node_id, capacity, random=self.random) for cell in self.all_cells: self._connect_single_cell(cell) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 3d87779df6e..36c29e2178c 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -1,3 +1,5 @@ +import random + import pytest from mesa import Model @@ -6,6 +8,8 @@ HexGrid, Network, OrthogonalGrid, + Cell, + CellCollection ) @@ -185,3 +189,79 @@ def test_empties_space(): cell = grid.select_random_empty_cell() assert cell.coordinate in {8, 9} + + +def test_cell(): + + cell1 = Cell(1, capacity=None, random=random.Random()) + cell2 = Cell(2, capacity=None, random=random.Random()) + + # connect + cell1.connect(cell2) + assert cell2 in cell1._connections + + # disconnect + cell1.disconnect(cell2) + assert cell2 not in cell1._connections + + # remove cell not in connections + with pytest.raises(ValueError): + cell1.disconnect(cell2) + + # add_agent + model = Model() + agent = CellAgent(1, model) + + cell1.add_agent(agent) + assert agent in cell1.agents + + # remove_agent + cell1.remove_agent(agent) + assert agent not in cell1.agents + + with pytest.raises(ValueError): + cell1.remove_agent(agent) + + cell1 = Cell(1, capacity=1, random=random.Random()) + cell1.add_agent(CellAgent(1, model)) + assert cell1.is_full + + with pytest.raises(Exception): + cell1.add_agent(CellAgent(2, model)) + + +def test_cell_collection(): + cell1 = Cell(1, capacity=None, random=random.Random()) + + collection = CellCollection({cell1:cell1.agents}, random=random.Random()) + assert len(collection) == 1 + assert cell1 in collection + + + rng = random.Random() + n = 10 + collection = CellCollection([Cell(i, random=rng) for i in range(n)], random=rng) + assert len(collection) == n + + cell = collection.select_random_cell() + assert cell in collection + + cells = collection.cells + assert len(cells) == n + + agents = collection.agents + assert len(list(agents)) == 0 + + cells = collection.cells + model = Model() + cells[0].add_agent(CellAgent(1, model)) + cells[3].add_agent(CellAgent(2, model)) + cells[7].add_agent(CellAgent(3, model)) + agents = collection.agents + assert len(list(agents)) == 3 + + agent = collection.select_random_agent() + assert agent in set(collection.agents) + + agents = collection[cells[0]] + assert agents == cells[0].agents From 2d8fcb56b147d2a39587fde4554e330bbde094d3 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 9 Feb 2024 20:05:25 +0100 Subject: [PATCH 27/63] additional tests and temporary fix for select_random_empty_cell --- .../experimental/cell_space/discrete_space.py | 17 ++--- mesa/experimental/cell_space/grid.py | 68 ++++++++++--------- tests/test_cell_space.py | 33 ++++++++- 3 files changed, 71 insertions(+), 47 deletions(-) diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 4be77123c2b..b78539ce280 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -25,19 +25,14 @@ def __init__( self.CellKlass = CellKlass self._empties: dict[Coordinate, None] = {} - self.cutoff_empties = -1 self.empties_initialized = False + @property + def cutoff_empties(self): + return 7.953 * len(self.cells) ** 0.384 def _connect_single_cell(self, cell): ... - def _initialize_empties(self): - self._empties = { - cell.coordinate: None for cell in self.cells.values() if cell.is_empty - } - self.cutoff_empties = 7.953 * len(self.cells) ** 0.384 - self.empties_initialized = True - @cached_property def all_cells(self): return CellCollection({cell: cell.agents for cell in self.cells.values()}) @@ -53,7 +48,5 @@ def empties(self) -> CellCollection: return self.all_cells.select(lambda cell: cell.is_empty) def select_random_empty_cell(self) -> Cell: - if not self.empties_initialized: - self._initialize_empties() - - return self.cells[self.random.choice(list(self._empties))] + """select random empty cell""" + return self.random.choice(list(self.empties)) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 37be7b0e2ac..947461f1eec 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -1,11 +1,18 @@ from mesa.experimental.cell_space import Cell, DiscreteSpace from random import Random + class Grid(DiscreteSpace): """Base class for all grid and network classes + Attributes: + width (int): width of the grid + height (int): height of the grid + torus (bool): whether the grid is a torus + _try_random (bool): whether to get empty cell be repeatedly trying random cell """ + def __init__( self, width: int, @@ -19,42 +26,37 @@ def __init__( self.torus = torus self.width = width self.height = height + self._try_random = True def select_random_empty_cell(self) -> Cell: - if not self.empties_initialized: - self._initialize_empties() - - num_empty_cells = len(self._empties) - if num_empty_cells == 0: - raise Exception("ERROR: No empty cells") - + # FIXME:: currently just a simple boolean to control behavior + # FIXME:: basically if grid is close to 99% full, creating empty list can be faster + # FIXME:: note however that the old results don't apply because in this implementation + # FIXME:: because empties list needs to be rebuild each time # 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: + if self._try_random: while True: cell = self.all_cells.select_random_cell() if cell.is_empty: - break + return cell else: - coordinate = self.random.choice(list(self._empties)) - cell = self.cells[coordinate] - - return cell + return super().select_random_empty_cell() class OrthogonalGrid(Grid): def __init__( - self, - width: int, - height: int, - torus: bool = False, - moore: bool = True, - capacity: int | None = None, - random: Random = None, - CellKlass: type[Cell] = Cell + self, + width: int, + height: int, + torus: bool = False, + moore: bool = True, + capacity: int | None = None, + random: Random = None, + CellKlass: type[Cell] = Cell ) -> None: """Orthogonal grid @@ -87,14 +89,14 @@ def _connect_single_cell(self, cell): if self.moore: directions = [ (-1, -1), (-1, 0), (-1, 1), - ( 0, -1), ( 0, 1), - ( 1, -1), ( 1, 0), ( 1, 1), + (0, -1), (0, 1), + (1, -1), (1, 0), (1, 1), ] else: # Von Neumann neighborhood directions = [ - (-1, 0), - (0, -1), (0, 1), - ( 1, 0), + (-1, 0), + (0, -1), (0, 1), + (1, 0), ] # fmt: on @@ -128,7 +130,7 @@ def __init__( """ super().__init__(width, height, torus, capacity=capacity, random=random, CellKlass=CellKlass) self.cells = { - (i, j): self.CellKlass((i, j), capacity, random=self.random) + (i, j): self.CellKlass((i, j), capacity, random=self.random) for j in range(width) for i in range(height) } @@ -142,15 +144,15 @@ def _connect_single_cell(self, cell): # fmt: off if i % 2 == 0: directions = [ - (-1, -1), (-1, 0), - (0, -1), (0, 1), - ( 1, -1), (1, 0), + (-1, -1), (-1, 0), + (0, -1), (0, 1), + (1, -1), (1, 0), ] else: directions = [ - (-1, 0), (-1, 1), - (0, -1), (0, 1), - ( 1, 0), (1, 1), + (-1, 0), (-1, 1), + (0, -1), (0, 1), + (1, 0), (1, 1), ] # fmt: on diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 36c29e2178c..e5ceab16374 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -13,18 +13,31 @@ ) -def test_orthogonal_grid(): +def test_orthogonal_grid_neumann(): width = 10 height = 10 grid = OrthogonalGrid(width, height, torus=False, moore=False, capacity=None) assert len(grid.cells) == width * height - # von neumann neighborhood, torus false, top corner + # von neumann neighborhood, torus false, top left corner assert len(grid.cells[(0, 0)]._connections) == 2 for connection in grid.cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0)} + # von neumann neighborhood, torus false, top right corner + for connection in grid.cells[(0, width-1)]._connections: + assert connection.coordinate in {(0, width-2), (1, width-1)} + + # von neumann neighborhood, torus false, bottom left corner + for connection in grid.cells[(height-1, 0)]._connections: + assert connection.coordinate in {(height-1, 1), (height-2, 0)} + + # von neumann neighborhood, torus false, bottom right corner + for connection in grid.cells[(height-1, width-1)]._connections: + assert connection.coordinate in {(height-1, width-2), (height-2, width-1)} + + # von neumann neighborhood middle of grid assert len(grid.cells[(5, 5)]._connections) == 4 for connection in grid.cells[(5, 5)]._connections: @@ -36,6 +49,22 @@ def test_orthogonal_grid(): for connection in grid.cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0), (0, 9), (9, 0)} + # von neumann neighborhood, torus True, top right corner + for connection in grid.cells[(0, width-1)]._connections: + assert connection.coordinate in {(0, 8), (0, 0), (1, 9), (9, 9)} + + # von neumann neighborhood, torus True, bottom left corner + for connection in grid.cells[(9, 0)]._connections: + assert connection.coordinate in {(9, 1), (9, 9), (0, 0), (8, 0)} + + # von neumann neighborhood, torus True, bottom right corner + for connection in grid.cells[(9, 9)]._connections: + assert connection.coordinate in {(9, 0), (9, 8), (8, 9), (0, 9)} + +def test_orthogonal_grid_moore(): + width = 10 + height = 10 + # Moore neighborhood, torus false, top corner grid = OrthogonalGrid(width, height, torus=False, moore=True, capacity=None) assert len(grid.cells[(0, 0)]._connections) == 3 From 565ae0a0616182eec28215acb96e63f99b4d2a67 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 19:06:41 +0000 Subject: [PATCH 28/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/Schelling/schelling.py | 4 +- benchmarks/WolfSheep/wolf_sheep.py | 7 ++- mesa/experimental/cell_space/cell.py | 14 +++-- .../cell_space/cell_collection.py | 8 ++- .../experimental/cell_space/discrete_space.py | 6 +- mesa/experimental/cell_space/grid.py | 55 ++++++++++--------- mesa/experimental/cell_space/network.py | 12 ++-- tests/test_cell_space.py | 31 ++++++----- 8 files changed, 77 insertions(+), 60 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index c405ea2e761..a5aa9a017a7 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -50,7 +50,9 @@ def __init__( self.homophily = homophily self.schedule = RandomActivation(self) - self.grid = OrthogonalGrid(height, width, torus=True, capacity=1, random=self.random) + self.grid = OrthogonalGrid( + height, width, torus=True, capacity=1, random=self.random + ) self.happy = 0 diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 2b2159e4bec..b4151a37c86 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -161,7 +161,12 @@ def __init__( self.schedule = RandomActivationByType(self) self.grid = OrthogonalGrid( - self.height, self.width, moore=moore, torus=False, capacity=math.inf, random=self.random + self.height, + self.width, + moore=moore, + torus=False, + capacity=math.inf, + random=self.random, ) # Create sheep: diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 70bfa94f0b8..a1022150d48 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -1,10 +1,9 @@ from __future__ import annotations -from random import Random from functools import cache +from random import Random from typing import TYPE_CHECKING, Any -from mesa.agent import Agent from mesa.experimental.cell_space.cell_collection import CellCollection if TYPE_CHECKING: @@ -18,11 +17,13 @@ class Cell: "agents", "capacity", "properties", - "random" + "random", ] - def __init__(self, coordinate: Any, capacity: int | None = None, random: Random = None) -> None: - """" + def __init__( + self, coordinate: Any, capacity: int | None = None, random: Random = None + ) -> None: + """ " Args: coordinate: @@ -98,7 +99,8 @@ def __repr__(self): @cache def neighborhood(self, radius=1, include_center=False): return CellCollection( - self._neighborhood(radius=radius, include_center=include_center), random=self.random + self._neighborhood(radius=radius, include_center=include_center), + random=self.random, ) @cache diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 0fca9714463..2958bdfad1d 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -1,9 +1,9 @@ from __future__ import annotations import itertools -from random import Random from collections.abc import Iterable from functools import cached_property +from random import Random from typing import TYPE_CHECKING, Callable, Optional if TYPE_CHECKING: @@ -12,7 +12,9 @@ class CellCollection: - def __init__(self, cells: dict[Cell, list[CellAgent]] | Iterable[Cell], random: Random = None) -> None: + def __init__( + self, cells: dict[Cell, list[CellAgent]] | Iterable[Cell], random: Random = None + ) -> None: if isinstance(cells, dict): self._cells = cells else: @@ -49,7 +51,7 @@ def select_random_cell(self) -> Cell: def select_random_agent(self) -> CellAgent: return self.random.choice(list(self.agents)) - def select(self, filter_func: Optional[Callable[[Cell], bool]] = None, n=0): + def select(self, filter_func: Callable[[Cell], bool] | None = None, n=0): # FIXME: n is not considered if filter_func is None and n == 0: return self diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index b78539ce280..4a5bc592559 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -1,6 +1,5 @@ -import numbers -from random import Random from functools import cached_property +from random import Random from mesa.experimental.cell_space.cell import Cell from mesa.experimental.cell_space.cell_collection import CellCollection @@ -14,7 +13,7 @@ def __init__( self, capacity: int | None = None, CellKlass: type[Cell] = Cell, - random: Random = None + random: Random = None, ): super().__init__() self.capacity = capacity @@ -30,6 +29,7 @@ def __init__( @property def cutoff_empties(self): return 7.953 * len(self.cells) ** 0.384 + def _connect_single_cell(self, cell): ... diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 947461f1eec..8af6f6fb641 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -1,6 +1,7 @@ -from mesa.experimental.cell_space import Cell, DiscreteSpace from random import Random +from mesa.experimental.cell_space import Cell, DiscreteSpace + class Grid(DiscreteSpace): """Base class for all grid and network classes @@ -14,13 +15,13 @@ class Grid(DiscreteSpace): """ def __init__( - self, - width: int, - height: int, - torus: bool = False, - capacity: int | None = None, - random: Random = None, - CellKlass: type[Cell] = Cell, + self, + width: int, + height: int, + torus: bool = False, + capacity: int | None = None, + random: Random = None, + CellKlass: type[Cell] = Cell, ) -> None: super().__init__(capacity=capacity, random=random, CellKlass=CellKlass) self.torus = torus @@ -49,14 +50,14 @@ def select_random_empty_cell(self) -> Cell: class OrthogonalGrid(Grid): def __init__( - self, - width: int, - height: int, - torus: bool = False, - moore: bool = True, - capacity: int | None = None, - random: Random = None, - CellKlass: type[Cell] = Cell + self, + width: int, + height: int, + torus: bool = False, + moore: bool = True, + capacity: int | None = None, + random: Random = None, + CellKlass: type[Cell] = Cell, ) -> None: """Orthogonal grid @@ -71,7 +72,9 @@ def __init__( """ - super().__init__(width, height, torus, capacity=capacity, CellKlass=CellKlass, random=random) + super().__init__( + width, height, torus, capacity=capacity, CellKlass=CellKlass, random=random + ) self.moore = moore self.cells = { (i, j): self.CellKlass((i, j), capacity, random=self.random) @@ -110,13 +113,13 @@ def _connect_single_cell(self, cell): class HexGrid(Grid): def __init__( - self, - width: int, - height: int, - torus: bool = False, - capacity: int = None, - random: Random = None, - CellKlass: type[Cell] = Cell + self, + width: int, + height: int, + torus: bool = False, + capacity: int = None, + random: Random = None, + CellKlass: type[Cell] = Cell, ) -> None: """Hexagonal Grid @@ -128,7 +131,9 @@ def __init__( random (random): CellKlass (type[Cell]): The Cell class to use in the HexGrid """ - super().__init__(width, height, torus, capacity=capacity, random=random, CellKlass=CellKlass) + super().__init__( + width, height, torus, capacity=capacity, random=random, CellKlass=CellKlass + ) self.cells = { (i, j): self.CellKlass((i, j), capacity, random=self.random) for j in range(width) diff --git a/mesa/experimental/cell_space/network.py b/mesa/experimental/cell_space/network.py index 90396ff4bf2..75928df706f 100644 --- a/mesa/experimental/cell_space/network.py +++ b/mesa/experimental/cell_space/network.py @@ -7,11 +7,11 @@ class Network(DiscreteSpace): def __init__( - self, - G: Any, - capacity: int | None = None, - random: Random = None, - CellKlass: type[Cell] = Cell + self, + G: Any, + capacity: int | None = None, + random: Random = None, + CellKlass: type[Cell] = Cell, ) -> None: """A Networked grid @@ -22,7 +22,7 @@ def __init__( CellKlass (type[Cell]): The base Cell class to use in the Network """ - super().__init__(capacity=capacity,random=random, CellKlass=CellKlass) + super().__init__(capacity=capacity, random=random, CellKlass=CellKlass) self.G = G for node_id in self.G.nodes: diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index e5ceab16374..0e56703ec13 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -4,12 +4,12 @@ from mesa import Model from mesa.experimental.cell_space import ( + Cell, CellAgent, + CellCollection, HexGrid, Network, OrthogonalGrid, - Cell, - CellCollection ) @@ -26,17 +26,19 @@ def test_orthogonal_grid_neumann(): assert connection.coordinate in {(0, 1), (1, 0)} # von neumann neighborhood, torus false, top right corner - for connection in grid.cells[(0, width-1)]._connections: - assert connection.coordinate in {(0, width-2), (1, width-1)} + for connection in grid.cells[(0, width - 1)]._connections: + assert connection.coordinate in {(0, width - 2), (1, width - 1)} # von neumann neighborhood, torus false, bottom left corner - for connection in grid.cells[(height-1, 0)]._connections: - assert connection.coordinate in {(height-1, 1), (height-2, 0)} + for connection in grid.cells[(height - 1, 0)]._connections: + assert connection.coordinate in {(height - 1, 1), (height - 2, 0)} # von neumann neighborhood, torus false, bottom right corner - for connection in grid.cells[(height-1, width-1)]._connections: - assert connection.coordinate in {(height-1, width-2), (height-2, width-1)} - + for connection in grid.cells[(height - 1, width - 1)]._connections: + assert connection.coordinate in { + (height - 1, width - 2), + (height - 2, width - 1), + } # von neumann neighborhood middle of grid assert len(grid.cells[(5, 5)]._connections) == 4 @@ -50,17 +52,18 @@ def test_orthogonal_grid_neumann(): assert connection.coordinate in {(0, 1), (1, 0), (0, 9), (9, 0)} # von neumann neighborhood, torus True, top right corner - for connection in grid.cells[(0, width-1)]._connections: + for connection in grid.cells[(0, width - 1)]._connections: assert connection.coordinate in {(0, 8), (0, 0), (1, 9), (9, 9)} # von neumann neighborhood, torus True, bottom left corner for connection in grid.cells[(9, 0)]._connections: - assert connection.coordinate in {(9, 1), (9, 9), (0, 0), (8, 0)} + assert connection.coordinate in {(9, 1), (9, 9), (0, 0), (8, 0)} # von neumann neighborhood, torus True, bottom right corner for connection in grid.cells[(9, 9)]._connections: assert connection.coordinate in {(9, 0), (9, 8), (8, 9), (0, 9)} + def test_orthogonal_grid_moore(): width = 10 height = 10 @@ -221,7 +224,6 @@ def test_empties_space(): def test_cell(): - cell1 = Cell(1, capacity=None, random=random.Random()) cell2 = Cell(2, capacity=None, random=random.Random()) @@ -251,7 +253,7 @@ def test_cell(): with pytest.raises(ValueError): cell1.remove_agent(agent) - cell1 = Cell(1, capacity=1, random=random.Random()) + cell1 = Cell(1, capacity=1, random=random.Random()) cell1.add_agent(CellAgent(1, model)) assert cell1.is_full @@ -262,11 +264,10 @@ def test_cell(): def test_cell_collection(): cell1 = Cell(1, capacity=None, random=random.Random()) - collection = CellCollection({cell1:cell1.agents}, random=random.Random()) + collection = CellCollection({cell1: cell1.agents}, random=random.Random()) assert len(collection) == 1 assert cell1 in collection - rng = random.Random() n = 10 collection = CellCollection([Cell(i, random=rng) for i in range(n)], random=rng) From ab87b7095a3a89487309d3bdf2996b1b71366ec8 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 9 Feb 2024 20:21:45 +0100 Subject: [PATCH 29/63] improved annotations --- mesa/experimental/cell_space/cell.py | 7 +-- .../cell_space/cell_collection.py | 10 +-- .../experimental/cell_space/discrete_space.py | 8 +-- mesa/experimental/cell_space/grid.py | 62 +++++++++---------- mesa/experimental/cell_space/network.py | 14 ++--- 5 files changed, 48 insertions(+), 53 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index a1022150d48..de4edbeeb62 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -20,10 +20,9 @@ class Cell: "random", ] - def __init__( - self, coordinate: Any, capacity: int | None = None, random: Random = None - ) -> None: - """ " + + def __init__(self, coordinate: Any, capacity: int | None = None, random: Random | None = None) -> None: + """" Args: coordinate: diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 2958bdfad1d..00991599270 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -4,7 +4,7 @@ from collections.abc import Iterable from functools import cached_property from random import Random -from typing import TYPE_CHECKING, Callable, Optional +from typing import TYPE_CHECKING, Callable if TYPE_CHECKING: from mesa.experimental.cell_space.cell import Cell @@ -12,9 +12,9 @@ class CellCollection: - def __init__( - self, cells: dict[Cell, list[CellAgent]] | Iterable[Cell], random: Random = None - ) -> None: + def __init__(self, + cells: dict[Cell, list[CellAgent]] | Iterable[Cell], + random: Random | None = None) -> None: if isinstance(cells, dict): self._cells = cells else: @@ -51,7 +51,7 @@ def select_random_cell(self) -> Cell: def select_random_agent(self) -> CellAgent: return self.random.choice(list(self.agents)) - def select(self, filter_func: Callable[[Cell], bool] | None = None, n=0): + def select(self, filter_func: [Callable[[Cell], bool]] = None, n=0): # FIXME: n is not considered if filter_func is None and n == 0: return self diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 4a5bc592559..0d1172e34f3 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -11,9 +11,9 @@ class DiscreteSpace: def __init__( self, - capacity: int | None = None, - CellKlass: type[Cell] = Cell, - random: Random = None, + capacity: Optional[int] = None, + cell_klass: type[Cell] = Cell, + random: Optional[Random] = None ): super().__init__() self.capacity = capacity @@ -21,7 +21,7 @@ def __init__( if random is None: random = Random() # FIXME should default to default rng from model self.random = random - self.CellKlass = CellKlass + self.cell_klass = cell_klass self._empties: dict[Coordinate, None] = {} self.empties_initialized = False diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 8af6f6fb641..5812003d58c 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -15,15 +15,15 @@ class Grid(DiscreteSpace): """ def __init__( - self, - width: int, - height: int, - torus: bool = False, - capacity: int | None = None, - random: Random = None, - CellKlass: type[Cell] = Cell, + self, + width: int, + height: int, + torus: bool = False, + capacity: int | None = None, + random: Random | None = None, + cell_klass: type[Cell] = Cell, ) -> None: - super().__init__(capacity=capacity, random=random, CellKlass=CellKlass) + super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) self.torus = torus self.width = width self.height = height @@ -50,14 +50,14 @@ def select_random_empty_cell(self) -> Cell: class OrthogonalGrid(Grid): def __init__( - self, - width: int, - height: int, - torus: bool = False, - moore: bool = True, - capacity: int | None = None, - random: Random = None, - CellKlass: type[Cell] = Cell, + self, + width: int, + height: int, + torus: bool = False, + moore: bool = True, + capacity: int | None = None, + random: Random | None = None, + cell_klass: type[Cell] = Cell ) -> None: """Orthogonal grid @@ -68,16 +68,14 @@ def __init__( moore (bool): whether the space used Moore or von Neumann neighborhood capacity (int): the number of agents that can simultaneously occupy a cell random (random): - CellKlass (type[Cell]): The Cell class to use in the OrthogonalGrid + cell_klass (type[Cell]): The Cell class to use in the OrthogonalGrid """ - super().__init__( - width, height, torus, capacity=capacity, CellKlass=CellKlass, random=random - ) + super().__init__(width, height, torus, capacity=capacity, cell_klass=cell_klass, random=random) self.moore = moore self.cells = { - (i, j): self.CellKlass((i, j), capacity, random=self.random) + (i, j): self.cell_klass((i, j), capacity, random=self.random) for j in range(width) for i in range(height) } @@ -113,13 +111,13 @@ def _connect_single_cell(self, cell): class HexGrid(Grid): def __init__( - self, - width: int, - height: int, - torus: bool = False, - capacity: int = None, - random: Random = None, - CellKlass: type[Cell] = Cell, + self, + width: int, + height: int, + torus: bool = False, + capacity: int | None = None, + random: Random | None = None, + cell_klass: type[Cell] = Cell ) -> None: """Hexagonal Grid @@ -129,13 +127,11 @@ def __init__( torus (bool): whether the space is a torus capacity (int): the number of agents that can simultaneously occupy a cell random (random): - CellKlass (type[Cell]): The Cell class to use in the HexGrid + cell_klass (type[Cell]): The Cell class to use in the HexGrid """ - super().__init__( - width, height, torus, capacity=capacity, random=random, CellKlass=CellKlass - ) + super().__init__(width, height, torus, capacity=capacity, random=random, cell_klass=cell_klass) self.cells = { - (i, j): self.CellKlass((i, j), capacity, random=self.random) + (i, j): self.cell_klass((i, j), capacity, random=self.random) for j in range(width) for i in range(height) } diff --git a/mesa/experimental/cell_space/network.py b/mesa/experimental/cell_space/network.py index 75928df706f..8434e633341 100644 --- a/mesa/experimental/cell_space/network.py +++ b/mesa/experimental/cell_space/network.py @@ -7,11 +7,11 @@ class Network(DiscreteSpace): def __init__( - self, - G: Any, - capacity: int | None = None, - random: Random = None, - CellKlass: type[Cell] = Cell, + self, + G: Any, + capacity: int | None = None, + random: Random | None = None, + cell_klass: type[Cell] = Cell ) -> None: """A Networked grid @@ -22,11 +22,11 @@ def __init__( CellKlass (type[Cell]): The base Cell class to use in the Network """ - super().__init__(capacity=capacity, random=random, CellKlass=CellKlass) + super().__init__(capacity=capacity,random=random, cell_klass=cell_klass) self.G = G for node_id in self.G.nodes: - self.cells[node_id] = self.CellKlass(node_id, capacity, random=self.random) + self.cells[node_id] = self.cell_klass(node_id, capacity, random=self.random) for cell in self.all_cells: self._connect_single_cell(cell) From ab13bbcfa392a48f58a3ee8cbcdeacb9e988e484 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 19:21:53 +0000 Subject: [PATCH 30/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/cell.py | 7 ++- .../cell_space/cell_collection.py | 8 ++- .../experimental/cell_space/discrete_space.py | 2 +- mesa/experimental/cell_space/grid.py | 62 ++++++++++++------- mesa/experimental/cell_space/network.py | 12 ++-- 5 files changed, 54 insertions(+), 37 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index de4edbeeb62..47ed037a870 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -20,9 +20,10 @@ class Cell: "random", ] - - def __init__(self, coordinate: Any, capacity: int | None = None, random: Random | None = None) -> None: - """" + def __init__( + self, coordinate: Any, capacity: int | None = None, random: Random | None = None + ) -> None: + """ " Args: coordinate: diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 00991599270..a15ffe4c531 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -12,9 +12,11 @@ class CellCollection: - def __init__(self, - cells: dict[Cell, list[CellAgent]] | Iterable[Cell], - random: Random | None = None) -> None: + def __init__( + self, + cells: dict[Cell, list[CellAgent]] | Iterable[Cell], + random: Random | None = None, + ) -> None: if isinstance(cells, dict): self._cells = cells else: diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 0d1172e34f3..075f6b757cc 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -13,7 +13,7 @@ def __init__( self, capacity: Optional[int] = None, cell_klass: type[Cell] = Cell, - random: Optional[Random] = None + random: Optional[Random] = None, ): super().__init__() self.capacity = capacity diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 5812003d58c..1a4925ef2d9 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -15,13 +15,13 @@ class Grid(DiscreteSpace): """ def __init__( - self, - width: int, - height: int, - torus: bool = False, - capacity: int | None = None, - random: Random | None = None, - cell_klass: type[Cell] = Cell, + self, + width: int, + height: int, + torus: bool = False, + capacity: int | None = None, + random: Random | None = None, + cell_klass: type[Cell] = Cell, ) -> None: super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) self.torus = torus @@ -50,14 +50,14 @@ def select_random_empty_cell(self) -> Cell: class OrthogonalGrid(Grid): def __init__( - self, - width: int, - height: int, - torus: bool = False, - moore: bool = True, - capacity: int | None = None, - random: Random | None = None, - cell_klass: type[Cell] = Cell + self, + width: int, + height: int, + torus: bool = False, + moore: bool = True, + capacity: int | None = None, + random: Random | None = None, + cell_klass: type[Cell] = Cell, ) -> None: """Orthogonal grid @@ -72,7 +72,14 @@ def __init__( """ - super().__init__(width, height, torus, capacity=capacity, cell_klass=cell_klass, random=random) + super().__init__( + width, + height, + torus, + capacity=capacity, + cell_klass=cell_klass, + random=random, + ) self.moore = moore self.cells = { (i, j): self.cell_klass((i, j), capacity, random=self.random) @@ -111,13 +118,13 @@ def _connect_single_cell(self, cell): class HexGrid(Grid): def __init__( - self, - width: int, - height: int, - torus: bool = False, - capacity: int | None = None, - random: Random | None = None, - cell_klass: type[Cell] = Cell + self, + width: int, + height: int, + torus: bool = False, + capacity: int | None = None, + random: Random | None = None, + cell_klass: type[Cell] = Cell, ) -> None: """Hexagonal Grid @@ -129,7 +136,14 @@ def __init__( random (random): cell_klass (type[Cell]): The Cell class to use in the HexGrid """ - super().__init__(width, height, torus, capacity=capacity, random=random, cell_klass=cell_klass) + super().__init__( + width, + height, + torus, + capacity=capacity, + random=random, + cell_klass=cell_klass, + ) self.cells = { (i, j): self.cell_klass((i, j), capacity, random=self.random) for j in range(width) diff --git a/mesa/experimental/cell_space/network.py b/mesa/experimental/cell_space/network.py index 8434e633341..ccfb3172f78 100644 --- a/mesa/experimental/cell_space/network.py +++ b/mesa/experimental/cell_space/network.py @@ -7,11 +7,11 @@ class Network(DiscreteSpace): def __init__( - self, - G: Any, - capacity: int | None = None, - random: Random | None = None, - cell_klass: type[Cell] = Cell + self, + G: Any, + capacity: int | None = None, + random: Random | None = None, + cell_klass: type[Cell] = Cell, ) -> None: """A Networked grid @@ -22,7 +22,7 @@ def __init__( CellKlass (type[Cell]): The base Cell class to use in the Network """ - super().__init__(capacity=capacity,random=random, cell_klass=cell_klass) + super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) self.G = G for node_id in self.G.nodes: From 8f2e3dd2fb5cd5d5d25907118251fd0080e3efc6 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 9 Feb 2024 20:23:07 +0100 Subject: [PATCH 31/63] Update discrete_space.py --- mesa/experimental/cell_space/discrete_space.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 0d1172e34f3..372a689efa1 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -11,9 +11,9 @@ class DiscreteSpace: def __init__( self, - capacity: Optional[int] = None, + capacity: int | None = None, cell_klass: type[Cell] = Cell, - random: Optional[Random] = None + random: Random | None = None ): super().__init__() self.capacity = capacity From aa039622995847acb0931ecaa0e57da90404db25 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:26:42 +0000 Subject: [PATCH 32/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/discrete_space.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 372a689efa1..17dae59a164 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -13,7 +13,7 @@ def __init__( self, capacity: int | None = None, cell_klass: type[Cell] = Cell, - random: Random | None = None + random: Random | None = None, ): super().__init__() self.capacity = capacity From 9b28f9dd689659a1f92eea3ee7eb0d951b81757a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 16 Feb 2024 09:04:11 +0100 Subject: [PATCH 33/63] correct handling of seeds when running examples in issolation --- benchmarks/Schelling/schelling.py | 4 ++-- benchmarks/WolfSheep/wolf_sheep.py | 5 +++-- mesa/experimental/cell_space/discrete_space.py | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index a5aa9a017a7..328effa9046 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -39,7 +39,7 @@ class Schelling(Model): """ def __init__( - self, seed, height, width, homophily, radius, density, minority_pc=0.5 + self, height, width, homophily, radius, density, minority_pc=0.5, seed=None ): """ """ super().__init__(seed=seed) @@ -79,7 +79,7 @@ def step(self): import time # model = Schelling(15, 40, 40, 3, 1, 0.625) - model = Schelling(15, 100, 100, 8, 2, 0.8) + model = Schelling(100, 100, 8, 2, 0.8, seed=15) start_time = time.perf_counter() for _ in range(100): diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index b4151a37c86..646bab76ce3 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -124,7 +124,6 @@ class WolfSheep(Model): def __init__( self, - seed, height, width, initial_sheep, @@ -135,6 +134,7 @@ def __init__( wolf_gain_from_food=13, sheep_gain_from_food=5, moore=False, + seed=None ): """ Create a new Wolf-Sheep model with the given parameters. @@ -150,6 +150,7 @@ def __init__( once it is eaten sheep_gain_from_food: Energy sheep gain from grass, if enabled. moore: + seed """ super().__init__(seed=seed) # Set parameters @@ -214,7 +215,7 @@ def step(self): if __name__ == "__main__": import time - model = WolfSheep(15, 25, 25, 60, 40, 0.2, 0.1, 20) + model = WolfSheep(25, 25, 60, 40, 0.2, 0.1, 20, seed=15) start_time = time.perf_counter() for _ in range(100): diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 372a689efa1..1044f1401d7 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + from functools import cached_property from random import Random From fe5a93f9f9bba6e8a03e1d713a41c20908d3034c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 08:05:19 +0000 Subject: [PATCH 34/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/WolfSheep/wolf_sheep.py | 2 +- mesa/experimental/cell_space/discrete_space.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 646bab76ce3..877c39adfc9 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -134,7 +134,7 @@ def __init__( wolf_gain_from_food=13, sheep_gain_from_food=5, moore=False, - seed=None + seed=None, ): """ Create a new Wolf-Sheep model with the given parameters. diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 3a6c89dd3d1..4396bbe3f90 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -1,6 +1,5 @@ from __future__ import annotations - from functools import cached_property from random import Random From 2c80330cf7a590be42cde604d2a13d51eb1844c5 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 17 Feb 2024 16:36:10 +0100 Subject: [PATCH 35/63] added docstrings --- mesa/experimental/cell_space/cell.py | 11 +++++++++ mesa/experimental/cell_space/cell_agent.py | 9 ++++---- .../cell_space/cell_collection.py | 8 +++++++ .../experimental/cell_space/discrete_space.py | 23 +++++++++++++------ mesa/experimental/cell_space/grid.py | 2 +- mesa/experimental/cell_space/network.py | 6 +++-- tests/test_cell_space.py | 6 ++--- 7 files changed, 48 insertions(+), 17 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 47ed037a870..b203d0bb2f8 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -11,6 +11,17 @@ class Cell: + """The cell represents a position in a discrete space. + + Attributes: + coordinate (Tuple[int, int]) : the position of the cell in the discrete space + agents (List[Agent]): the agents occupying the cell + capacity (int): the maximum number of agents that can simultaneously occupy the cell + properties (dict[str, Any]): the properties of the cell + random (Random): the random number generator + + """ + __slots__ = [ "coordinate", "_connections", diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index de05c29c93e..c305ff19143 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -3,13 +3,14 @@ class CellAgent(Agent): - """ - Base class for a model agent in Mesa. + """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces + Attributes: unique_id (int): A unique identifier for this agent. - model (Model): A reference to the model instance. - self.pos: Position | None = None + 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 """ def __init__(self, unique_id: int, model: Model) -> None: diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index a15ffe4c531..34d7b16bf1c 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -12,6 +12,14 @@ class CellCollection: + """An immutable collection of cells + + Attributes: + cells (List[Cell]): The list of cells this collection represents + agents (List[CellAgent]) : List of agents occupying the cells in this collection + random (Random) : The random number generator + + """ def __init__( self, cells: dict[Cell, list[CellAgent]] | Iterable[Cell], diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 4396bbe3f90..5c7d71898b3 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -9,7 +9,16 @@ class DiscreteSpace: - # FIXME:: random should become a keyword argument + """Base class for all discrete spaces. + + Attributes: + capacity (int): The capacity of the cells in the discrete space + all_cells (CellCollection): The cells composing the discrete space + random (Random): The random number generator + cell_klass (Type) : the type of cell class + empties (CellCollection) : collecction of all cells that are empty + + """ def __init__( self, @@ -19,31 +28,31 @@ def __init__( ): super().__init__() self.capacity = capacity - self.cells: dict[Coordinate, Cell] = {} + self._cells: dict[Coordinate, Cell] = {} if random is None: random = Random() # FIXME should default to default rng from model self.random = random self.cell_klass = cell_klass self._empties: dict[Coordinate, None] = {} - self.empties_initialized = False + self._empties_initialized = False @property def cutoff_empties(self): - return 7.953 * len(self.cells) ** 0.384 + return 7.953 * len(self._cells) ** 0.384 def _connect_single_cell(self, cell): ... @cached_property def all_cells(self): - return CellCollection({cell: cell.agents for cell in self.cells.values()}) + return CellCollection({cell: cell.agents for cell in self._cells.values()}) def __iter__(self): - return iter(self.cells.values()) + return iter(self._cells.values()) def __getitem__(self, key): - return self.cells[key] + return self._cells[key] @property def empties(self) -> CellCollection: diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 1a4925ef2d9..56db22858e7 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -4,7 +4,7 @@ class Grid(DiscreteSpace): - """Base class for all grid and network classes + """Base class for all grid classes Attributes: width (int): width of the grid diff --git a/mesa/experimental/cell_space/network.py b/mesa/experimental/cell_space/network.py index ccfb3172f78..5b48784e7cf 100644 --- a/mesa/experimental/cell_space/network.py +++ b/mesa/experimental/cell_space/network.py @@ -6,6 +6,8 @@ class Network(DiscreteSpace): + """A networked discrete space""" + def __init__( self, G: Any, @@ -26,11 +28,11 @@ def __init__( self.G = G for node_id in self.G.nodes: - self.cells[node_id] = self.cell_klass(node_id, capacity, random=self.random) + self._cells[node_id] = self.cell_klass(node_id, capacity, random=self.random) for cell in self.all_cells: self._connect_single_cell(cell) def _connect_single_cell(self, cell): for node_id in self.G.neighbors(cell.coordinate): - cell.connect(self.cells[node_id]) + cell.connect(self._cells[node_id]) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 0e56703ec13..ca5529829ed 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -197,9 +197,9 @@ def test_networkgrid(): G = nx.gnm_random_graph(n, m, seed=seed) grid = Network(G) - assert len(grid.cells) == n + assert len(grid._cells) == n - for i, cell in grid.cells.items(): + for i, cell in grid._cells.items(): for connection in cell._connections: assert connection.coordinate in G.neighbors(i) @@ -217,7 +217,7 @@ def test_empties_space(): model = Model() for i in range(8): - grid.cells[i].add_agent(CellAgent(i, model)) + grid._cells[i].add_agent(CellAgent(i, model)) cell = grid.select_random_empty_cell() assert cell.coordinate in {8, 9} From c79aaf07d16d10083f4d4d7ac9c22781bed7b5c4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 15:36:18 +0000 Subject: [PATCH 36/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/cell_collection.py | 1 + mesa/experimental/cell_space/network.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 34d7b16bf1c..ecf479fbaa7 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -20,6 +20,7 @@ class CellCollection: random (Random) : The random number generator """ + def __init__( self, cells: dict[Cell, list[CellAgent]] | Iterable[Cell], diff --git a/mesa/experimental/cell_space/network.py b/mesa/experimental/cell_space/network.py index 5b48784e7cf..501b263d406 100644 --- a/mesa/experimental/cell_space/network.py +++ b/mesa/experimental/cell_space/network.py @@ -28,7 +28,9 @@ def __init__( self.G = G for node_id in self.G.nodes: - self._cells[node_id] = self.cell_klass(node_id, capacity, random=self.random) + self._cells[node_id] = self.cell_klass( + node_id, capacity, random=self.random + ) for cell in self.all_cells: self._connect_single_cell(cell) From 8384cf1832da4f601192605a6a59223e178afd69 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 17 Feb 2024 16:49:26 +0100 Subject: [PATCH 37/63] reformating and minor update to tests --- mesa/experimental/cell_space/grid.py | 30 ++++++------ tests/test_cell_space.py | 72 ++++++++++++++-------------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 56db22858e7..3334f7e01f6 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -81,7 +81,7 @@ def __init__( random=random, ) self.moore = moore - self.cells = { + self._cells = { (i, j): self.cell_klass((i, j), capacity, random=self.random) for j in range(width) for i in range(height) @@ -97,14 +97,14 @@ def _connect_single_cell(self, cell): if self.moore: directions = [ (-1, -1), (-1, 0), (-1, 1), - (0, -1), (0, 1), - (1, -1), (1, 0), (1, 1), + ( 0, -1), ( 0, 1), + ( 1, -1), ( 1, 0), ( 1, 1), ] else: # Von Neumann neighborhood directions = [ - (-1, 0), - (0, -1), (0, 1), - (1, 0), + (-1, 0), + ( 0, -1), (0, 1), + ( 1, 0), ] # fmt: on @@ -113,7 +113,7 @@ def _connect_single_cell(self, cell): if self.torus: ni, nj = ni % self.height, nj % self.width if 0 <= ni < self.height and 0 <= nj < self.width: - cell.connect(self.cells[ni, nj]) + cell.connect(self._cells[ni, nj]) class HexGrid(Grid): @@ -144,7 +144,7 @@ def __init__( random=random, cell_klass=cell_klass, ) - self.cells = { + self._cells = { (i, j): self.cell_klass((i, j), capacity, random=self.random) for j in range(width) for i in range(height) @@ -159,15 +159,15 @@ def _connect_single_cell(self, cell): # fmt: off if i % 2 == 0: directions = [ - (-1, -1), (-1, 0), - (0, -1), (0, 1), - (1, -1), (1, 0), + (-1, -1), (-1, 0), + ( 0, -1), ( 0, 1), + ( 1, -1), ( 1, 0), ] else: directions = [ - (-1, 0), (-1, 1), - (0, -1), (0, 1), - (1, 0), (1, 1), + (-1, 0), (-1, 1), + ( 0, -1), ( 0, 1), + ( 1, 0), ( 1, 1), ] # fmt: on @@ -176,4 +176,4 @@ def _connect_single_cell(self, cell): if self.torus: ni, nj = ni % self.height, nj % self.width if 0 <= ni < self.height and 0 <= nj < self.width: - cell.connect(self.cells[ni, nj]) + cell.connect(self._cells[ni, nj]) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index ca5529829ed..7c1c0edbc0a 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -18,49 +18,49 @@ def test_orthogonal_grid_neumann(): height = 10 grid = OrthogonalGrid(width, height, torus=False, moore=False, capacity=None) - assert len(grid.cells) == width * height + assert len(grid._cells) == width * height # von neumann neighborhood, torus false, top left corner - assert len(grid.cells[(0, 0)]._connections) == 2 - for connection in grid.cells[(0, 0)]._connections: + assert len(grid._cells[(0, 0)]._connections) == 2 + for connection in grid._cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0)} # von neumann neighborhood, torus false, top right corner - for connection in grid.cells[(0, width - 1)]._connections: + for connection in grid._cells[(0, width - 1)]._connections: assert connection.coordinate in {(0, width - 2), (1, width - 1)} # von neumann neighborhood, torus false, bottom left corner - for connection in grid.cells[(height - 1, 0)]._connections: + for connection in grid._cells[(height - 1, 0)]._connections: assert connection.coordinate in {(height - 1, 1), (height - 2, 0)} # von neumann neighborhood, torus false, bottom right corner - for connection in grid.cells[(height - 1, width - 1)]._connections: + for connection in grid._cells[(height - 1, width - 1)]._connections: assert connection.coordinate in { (height - 1, width - 2), (height - 2, width - 1), } # von neumann neighborhood middle of grid - assert len(grid.cells[(5, 5)]._connections) == 4 - for connection in grid.cells[(5, 5)]._connections: + assert len(grid._cells[(5, 5)]._connections) == 4 + for connection in grid._cells[(5, 5)]._connections: assert connection.coordinate in {(4, 5), (5, 4), (5, 6), (6, 5)} # von neumann neighborhood, torus True, top corner grid = OrthogonalGrid(width, height, torus=True, moore=False, capacity=None) - assert len(grid.cells[(0, 0)]._connections) == 4 - for connection in grid.cells[(0, 0)]._connections: + assert len(grid._cells[(0, 0)]._connections) == 4 + for connection in grid._cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0), (0, 9), (9, 0)} # von neumann neighborhood, torus True, top right corner - for connection in grid.cells[(0, width - 1)]._connections: + for connection in grid._cells[(0, width - 1)]._connections: assert connection.coordinate in {(0, 8), (0, 0), (1, 9), (9, 9)} # von neumann neighborhood, torus True, bottom left corner - for connection in grid.cells[(9, 0)]._connections: + for connection in grid._cells[(9, 0)]._connections: assert connection.coordinate in {(9, 1), (9, 9), (0, 0), (8, 0)} # von neumann neighborhood, torus True, bottom right corner - for connection in grid.cells[(9, 9)]._connections: + for connection in grid._cells[(9, 9)]._connections: assert connection.coordinate in {(9, 0), (9, 8), (8, 9), (0, 9)} @@ -70,13 +70,13 @@ def test_orthogonal_grid_moore(): # Moore neighborhood, torus false, top corner grid = OrthogonalGrid(width, height, torus=False, moore=True, capacity=None) - assert len(grid.cells[(0, 0)]._connections) == 3 - for connection in grid.cells[(0, 0)]._connections: + assert len(grid._cells[(0, 0)]._connections) == 3 + for connection in grid._cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0), (1, 1)} # Moore neighborhood middle of grid - assert len(grid.cells[(5, 5)]._connections) == 8 - for connection in grid.cells[(5, 5)]._connections: + assert len(grid._cells[(5, 5)]._connections) == 8 + for connection in grid._cells[(5, 5)]._connections: # fmt: off assert connection.coordinate in {(4, 4), (4, 5), (4, 6), (5, 4), (5, 6), @@ -85,8 +85,8 @@ def test_orthogonal_grid_moore(): # Moore neighborhood, torus True, top corner grid = OrthogonalGrid(10, 10, torus=True, moore=True, capacity=None) - assert len(grid.cells[(0, 0)]._connections) == 8 - for connection in grid.cells[(0, 0)]._connections: + assert len(grid._cells[(0, 0)]._connections) == 8 + for connection in grid._cells[(0, 0)]._connections: # fmt: off assert connection.coordinate in {(9, 9), (9, 0), (9, 1), (0, 9), (0, 1), @@ -102,7 +102,7 @@ def test_cell_neighborhood(): height = 10 grid = OrthogonalGrid(width, height, torus=False, moore=False, capacity=None) for radius, n in zip(range(1, 4), [2, 5, 9]): - neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) + neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n ## Moore @@ -110,25 +110,25 @@ def test_cell_neighborhood(): height = 10 grid = OrthogonalGrid(width, height, torus=False, moore=True, capacity=None) for radius, n in zip(range(1, 4), [3, 8, 15]): - neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) + neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n with pytest.raises(ValueError): - grid.cells[(0, 0)].neighborhood(radius=0) + grid._cells[(0, 0)].neighborhood(radius=0) # hexgrid width = 10 height = 10 grid = HexGrid(width, height, torus=False, capacity=None) for radius, n in zip(range(1, 4), [2, 6, 11]): - neighborhood = grid.cells[(0, 0)].neighborhood(radius=radius) + neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n width = 10 height = 10 grid = HexGrid(width, height, torus=False, capacity=None) for radius, n in zip(range(1, 4), [5, 10, 17]): - neighborhood = grid.cells[(1, 0)].neighborhood(radius=radius) + neighborhood = grid._cells[(1, 0)].neighborhood(radius=radius) assert len(neighborhood) == n # networkgrid @@ -139,24 +139,24 @@ def test_hexgrid(): height = 10 grid = HexGrid(width, height, torus=False) - assert len(grid.cells) == width * height + assert len(grid._cells) == width * height # first row - assert len(grid.cells[(0, 0)]._connections) == 2 - for connection in grid.cells[(0, 0)]._connections: + assert len(grid._cells[(0, 0)]._connections) == 2 + for connection in grid._cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0)} # second row - assert len(grid.cells[(1, 0)]._connections) == 5 - for connection in grid.cells[(1, 0)]._connections: + assert len(grid._cells[(1, 0)]._connections) == 5 + for connection in grid._cells[(1, 0)]._connections: # fmt: off assert connection.coordinate in {(0, 0), (0, 1), (1, 1), (2, 0), (2, 1)} # middle odd row - assert len(grid.cells[(5, 5)]._connections) == 6 - for connection in grid.cells[(5, 5)]._connections: + assert len(grid._cells[(5, 5)]._connections) == 6 + for connection in grid._cells[(5, 5)]._connections: # fmt: off assert connection.coordinate in {(4, 5), (4, 6), (5, 4), (5, 6), @@ -165,8 +165,8 @@ def test_hexgrid(): # fmt: on # middle even row - assert len(grid.cells[(4, 4)]._connections) == 6 - for connection in grid.cells[(4, 4)]._connections: + assert len(grid._cells[(4, 4)]._connections) == 6 + for connection in grid._cells[(4, 4)]._connections: # fmt: off assert connection.coordinate in {(3, 3), (3, 4), (4, 3), (4, 5), @@ -175,11 +175,11 @@ def test_hexgrid(): # fmt: on grid = HexGrid(width, height, torus=True) - assert len(grid.cells) == width * height + assert len(grid._cells) == width * height # first row - assert len(grid.cells[(0, 0)]._connections) == 6 - for connection in grid.cells[(0, 0)]._connections: + assert len(grid._cells[(0, 0)]._connections) == 6 + for connection in grid._cells[(0, 0)]._connections: # fmt: off assert connection.coordinate in {(9, 9), (9, 0), (0, 9), (0, 1), From ebdee8e991d5a8e2fd907be376fe0d6211c6079d Mon Sep 17 00:00:00 2001 From: Corvince Date: Sat, 17 Feb 2024 21:30:56 +0100 Subject: [PATCH 38/63] Add optional neighborhood_func to Grid class --- benchmarks/Schelling/schelling.py | 4 +- benchmarks/WolfSheep/wolf_sheep.py | 10 +- mesa/experimental/cell_space/__init__.py | 10 +- mesa/experimental/cell_space/grid.py | 153 ++++++++--------------- tests/test_cell_space.py | 15 +-- 5 files changed, 74 insertions(+), 118 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 328effa9046..ca9d73e3549 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,5 +1,5 @@ from mesa import Model -from mesa.experimental.cell_space import CellAgent, OrthogonalGrid +from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid from mesa.time import RandomActivation @@ -50,7 +50,7 @@ def __init__( self.homophily = homophily self.schedule = RandomActivation(self) - self.grid = OrthogonalGrid( + self.grid = OrthogonalMooreGrid( height, width, torus=True, capacity=1, random=self.random ) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 877c39adfc9..a5acc9358fc 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -8,10 +8,11 @@ Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL. """ + import math from mesa import Model -from mesa.experimental.cell_space import CellAgent, OrthogonalGrid +from mesa.experimental.cell_space import CellAgent, OrthogonalVonNeumannGrid from mesa.time import RandomActivationByType @@ -37,8 +38,7 @@ def spawn_offspring(self): offspring.move_to(self.cell) self.model.schedule.add(offspring) - def feed(self): - ... + def feed(self): ... def die(self): self.cell.remove_agent(self) @@ -133,7 +133,6 @@ def __init__( grass_regrowth_time, wolf_gain_from_food=13, sheep_gain_from_food=5, - moore=False, seed=None, ): """ @@ -161,10 +160,9 @@ def __init__( self.grass_regrowth_time = grass_regrowth_time self.schedule = RandomActivationByType(self) - self.grid = OrthogonalGrid( + self.grid = OrthogonalVonNeumannGrid( self.height, self.width, - moore=moore, torus=False, capacity=math.inf, random=self.random, diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py index f5a4af97ca0..dce296aebce 100644 --- a/mesa/experimental/cell_space/__init__.py +++ b/mesa/experimental/cell_space/__init__.py @@ -2,7 +2,12 @@ from mesa.experimental.cell_space.cell_agent import CellAgent 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 Grid, HexGrid, OrthogonalGrid +from mesa.experimental.cell_space.grid import ( + Grid, + HexGrid, + OrthogonalMooreGrid, + OrthogonalVonNeumannGrid, +) from mesa.experimental.cell_space.network import Network __all__ = [ @@ -12,6 +17,7 @@ "DiscreteSpace", "Grid", "HexGrid", - "OrthogonalGrid", + "OrthogonalMooreGrid", + "OrthogonalVonNeumannGrid", "Network", ] diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 3334f7e01f6..abf58ad1c2b 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -1,4 +1,5 @@ from random import Random +from typing import Callable from mesa.experimental.cell_space import Cell, DiscreteSpace @@ -22,12 +23,31 @@ def __init__( capacity: int | None = None, random: Random | None = None, cell_klass: type[Cell] = Cell, + neighborhood_func: Callable[[Cell], list[tuple[int, int]]] | None = None, ) -> None: super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) self.torus = torus self.width = width self.height = height self._try_random = True + if neighborhood_func is not None: + self.neighborhood_func = neighborhood_func + else: + self.neighborhood_func = self._default_neighborhood_func + + self._cells = { + (i, j): self.cell_klass((i, j), capacity, random=self.random) + for j in range(width) + for i in range(height) + } + + for cell in self.all_cells: + self._connect_single_cell(cell) + + @staticmethod + def _default_neighborhood_func(cell: Cell) -> list[tuple[int, int]]: + # Default implementation + return [] def select_random_empty_cell(self) -> Cell: # FIXME:: currently just a simple boolean to control behavior @@ -47,68 +67,10 @@ def select_random_empty_cell(self) -> Cell: else: return super().select_random_empty_cell() - -class OrthogonalGrid(Grid): - def __init__( - self, - width: int, - height: int, - torus: bool = False, - moore: bool = True, - capacity: int | None = None, - random: Random | None = None, - cell_klass: type[Cell] = Cell, - ) -> None: - """Orthogonal grid - - Args: - width (int): width of the grid - height (int): height of the grid - torus (bool): whether the space is a torus - moore (bool): whether the space used Moore or von Neumann neighborhood - capacity (int): the number of agents that can simultaneously occupy a cell - random (random): - cell_klass (type[Cell]): The Cell class to use in the OrthogonalGrid - - - """ - super().__init__( - width, - height, - torus, - capacity=capacity, - cell_klass=cell_klass, - random=random, - ) - self.moore = moore - self._cells = { - (i, j): self.cell_klass((i, j), capacity, random=self.random) - for j in range(width) - for i in range(height) - } - - for cell in self.all_cells: - self._connect_single_cell(cell) - def _connect_single_cell(self, cell): i, j = cell.coordinate - # fmt: off - if self.moore: - directions = [ - (-1, -1), (-1, 0), (-1, 1), - ( 0, -1), ( 0, 1), - ( 1, -1), ( 1, 0), ( 1, 1), - ] - else: # Von Neumann neighborhood - directions = [ - (-1, 0), - ( 0, -1), (0, 1), - ( 1, 0), - ] - # fmt: on - - for di, dj in directions: + for di, dj in self.neighborhood_func(cell): ni, nj = (i + di, j + dj) if self.torus: ni, nj = ni % self.height, nj % self.width @@ -116,44 +78,38 @@ def _connect_single_cell(self, cell): cell.connect(self._cells[ni, nj]) -class HexGrid(Grid): - def __init__( - self, - width: int, - height: int, - torus: bool = False, - capacity: int | None = None, - random: Random | None = None, - cell_klass: type[Cell] = Cell, - ) -> None: - """Hexagonal Grid - - Args: - width (int): width of the grid - height (int): height of the grid - torus (bool): whether the space is a torus - capacity (int): the number of agents that can simultaneously occupy a cell - random (random): - cell_klass (type[Cell]): The Cell class to use in the HexGrid - """ - super().__init__( - width, - height, - torus, - capacity=capacity, - random=random, - cell_klass=cell_klass, - ) - self._cells = { - (i, j): self.cell_klass((i, j), capacity, random=self.random) - for j in range(width) - for i in range(height) - } +class OrthogonalMooreGrid(Grid): - for cell in self.all_cells: - self._connect_single_cell(cell) + @staticmethod + def _default_neighborhood_func(cell): + # fmt: off + directions = [ + (-1, -1), (-1, 0), (-1, 1), + ( 0, -1), ( 0, 1), + ( 1, -1), ( 1, 0), ( 1, 1), + ] + # fmt: on + return directions - def _connect_single_cell(self, cell): + +class OrthogonalVonNeumannGrid(Grid): + + @staticmethod + def _default_neighborhood_func(cell): + # fmt: off + directions = [ + (0, -1), + (-1, 0), ( 1, 0), + (0, 1), + ] + # fmt: on + return directions + + +class HexGrid(Grid): + + @staticmethod + def _default_neighborhood_func(cell): i, j = cell.coordinate # fmt: off @@ -171,9 +127,4 @@ def _connect_single_cell(self, cell): ] # fmt: on - for di, dj in directions: - ni, nj = (i + di, j + dj) - if self.torus: - ni, nj = ni % self.height, nj % self.width - if 0 <= ni < self.height and 0 <= nj < self.width: - cell.connect(self._cells[ni, nj]) + return directions diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 7c1c0edbc0a..d1fc110f0bf 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -9,14 +9,15 @@ CellCollection, HexGrid, Network, - OrthogonalGrid, + OrthogonalMooreGrid, + OrthogonalVonNeumannGrid, ) def test_orthogonal_grid_neumann(): width = 10 height = 10 - grid = OrthogonalGrid(width, height, torus=False, moore=False, capacity=None) + grid = OrthogonalVonNeumannGrid(width, height, torus=False, capacity=None) assert len(grid._cells) == width * height @@ -46,7 +47,7 @@ def test_orthogonal_grid_neumann(): assert connection.coordinate in {(4, 5), (5, 4), (5, 6), (6, 5)} # von neumann neighborhood, torus True, top corner - grid = OrthogonalGrid(width, height, torus=True, moore=False, capacity=None) + grid = OrthogonalVonNeumannGrid(width, height, torus=True, capacity=None) assert len(grid._cells[(0, 0)]._connections) == 4 for connection in grid._cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0), (0, 9), (9, 0)} @@ -69,7 +70,7 @@ def test_orthogonal_grid_moore(): height = 10 # Moore neighborhood, torus false, top corner - grid = OrthogonalGrid(width, height, torus=False, moore=True, capacity=None) + grid = OrthogonalMooreGrid(width, height, torus=False, capacity=None) assert len(grid._cells[(0, 0)]._connections) == 3 for connection in grid._cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0), (1, 1)} @@ -84,7 +85,7 @@ def test_orthogonal_grid_moore(): # fmt: on # Moore neighborhood, torus True, top corner - grid = OrthogonalGrid(10, 10, torus=True, moore=True, capacity=None) + grid = OrthogonalMooreGrid(10, 10, torus=True, capacity=None) assert len(grid._cells[(0, 0)]._connections) == 8 for connection in grid._cells[(0, 0)]._connections: # fmt: off @@ -100,7 +101,7 @@ def test_cell_neighborhood(): ## von Neumann width = 10 height = 10 - grid = OrthogonalGrid(width, height, torus=False, moore=False, capacity=None) + grid = OrthogonalVonNeumannGrid(width, height, torus=False, capacity=None) for radius, n in zip(range(1, 4), [2, 5, 9]): neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n @@ -108,7 +109,7 @@ def test_cell_neighborhood(): ## Moore width = 10 height = 10 - grid = OrthogonalGrid(width, height, torus=False, moore=True, capacity=None) + grid = OrthogonalMooreGrid(width, height, torus=False, capacity=None) for radius, n in zip(range(1, 4), [3, 8, 15]): neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n From bb2fc52e78efb11d49c7e83a3e161b095d8ca81b Mon Sep 17 00:00:00 2001 From: Corvince Date: Sat, 17 Feb 2024 21:37:43 +0100 Subject: [PATCH 39/63] allow n-dimensional grids --- benchmarks/Schelling/schelling.py | 2 +- benchmarks/WolfSheep/wolf_sheep.py | 3 +-- mesa/experimental/cell_space/grid.py | 35 +++++++++++++++++----------- tests/test_cell_space.py | 20 ++++++++-------- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index ca9d73e3549..2d88474ac62 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -51,7 +51,7 @@ def __init__( self.schedule = RandomActivation(self) self.grid = OrthogonalMooreGrid( - height, width, torus=True, capacity=1, random=self.random + [height, width], torus=True, capacity=1, random=self.random ) self.happy = 0 diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index a5acc9358fc..85fa86e3885 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -161,8 +161,7 @@ def __init__( self.schedule = RandomActivationByType(self) self.grid = OrthogonalVonNeumannGrid( - self.height, - self.width, + [self.height, self.width], torus=False, capacity=math.inf, random=self.random, diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index abf58ad1c2b..40fe57f9459 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -1,3 +1,4 @@ +from itertools import product from random import Random from typing import Callable @@ -17,8 +18,7 @@ class Grid(DiscreteSpace): def __init__( self, - width: int, - height: int, + dimensions: list[int], torus: bool = False, capacity: int | None = None, random: Random | None = None, @@ -27,18 +27,18 @@ def __init__( ) -> None: super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) self.torus = torus - self.width = width - self.height = height + self.dimensions = dimensions self._try_random = True if neighborhood_func is not None: self.neighborhood_func = neighborhood_func else: self.neighborhood_func = self._default_neighborhood_func + coordinates = product(*(range(dim) for dim in self.dimensions)) + self._cells = { - (i, j): self.cell_klass((i, j), capacity, random=self.random) - for j in range(width) - for i in range(height) + coord: cell_klass(coord, capacity, random=self.random) + for coord in coordinates } for cell in self.all_cells: @@ -68,14 +68,23 @@ def select_random_empty_cell(self) -> Cell: return super().select_random_empty_cell() def _connect_single_cell(self, cell): - i, j = cell.coordinate + coord = cell.coordinate - for di, dj in self.neighborhood_func(cell): - ni, nj = (i + di, j + dj) + for d_coord in self.neighborhood_func(cell): + n_coord = tuple(c + dc for c, dc in zip(coord, d_coord)) if self.torus: - ni, nj = ni % self.height, nj % self.width - if 0 <= ni < self.height and 0 <= nj < self.width: - cell.connect(self._cells[ni, nj]) + n_coord = tuple(nc % d for nc, d in zip(n_coord, self.dimensions)) + if all(0 <= nc < d for nc, d in zip(n_coord, self.dimensions)): + cell.connect(self._cells[n_coord]) + + # i, j = cell.coordinate + + # for di, dj in self.neighborhood_func(cell): + # ni, nj = (i + di, j + dj) + # if self.torus: + # ni, nj = ni % self.height, nj % self.width + # if 0 <= ni < self.height and 0 <= nj < self.width: + # cell.connect(self._cells[ni, nj]) class OrthogonalMooreGrid(Grid): diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index d1fc110f0bf..7f08ac58cab 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -17,7 +17,7 @@ def test_orthogonal_grid_neumann(): width = 10 height = 10 - grid = OrthogonalVonNeumannGrid(width, height, torus=False, capacity=None) + grid = OrthogonalVonNeumannGrid([width, height], torus=False, capacity=None) assert len(grid._cells) == width * height @@ -47,7 +47,7 @@ def test_orthogonal_grid_neumann(): assert connection.coordinate in {(4, 5), (5, 4), (5, 6), (6, 5)} # von neumann neighborhood, torus True, top corner - grid = OrthogonalVonNeumannGrid(width, height, torus=True, capacity=None) + grid = OrthogonalVonNeumannGrid([width, height], torus=True, capacity=None) assert len(grid._cells[(0, 0)]._connections) == 4 for connection in grid._cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0), (0, 9), (9, 0)} @@ -70,7 +70,7 @@ def test_orthogonal_grid_moore(): height = 10 # Moore neighborhood, torus false, top corner - grid = OrthogonalMooreGrid(width, height, torus=False, capacity=None) + grid = OrthogonalMooreGrid([width, height], torus=False, capacity=None) assert len(grid._cells[(0, 0)]._connections) == 3 for connection in grid._cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0), (1, 1)} @@ -85,7 +85,7 @@ def test_orthogonal_grid_moore(): # fmt: on # Moore neighborhood, torus True, top corner - grid = OrthogonalMooreGrid(10, 10, torus=True, capacity=None) + grid = OrthogonalMooreGrid([10, 10], torus=True, capacity=None) assert len(grid._cells[(0, 0)]._connections) == 8 for connection in grid._cells[(0, 0)]._connections: # fmt: off @@ -101,7 +101,7 @@ def test_cell_neighborhood(): ## von Neumann width = 10 height = 10 - grid = OrthogonalVonNeumannGrid(width, height, torus=False, capacity=None) + grid = OrthogonalVonNeumannGrid([width, height], torus=False, capacity=None) for radius, n in zip(range(1, 4), [2, 5, 9]): neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n @@ -109,7 +109,7 @@ def test_cell_neighborhood(): ## Moore width = 10 height = 10 - grid = OrthogonalMooreGrid(width, height, torus=False, capacity=None) + grid = OrthogonalMooreGrid([width, height], torus=False, capacity=None) for radius, n in zip(range(1, 4), [3, 8, 15]): neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n @@ -120,14 +120,14 @@ def test_cell_neighborhood(): # hexgrid width = 10 height = 10 - grid = HexGrid(width, height, torus=False, capacity=None) + grid = HexGrid([width, height], torus=False, capacity=None) for radius, n in zip(range(1, 4), [2, 6, 11]): neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n width = 10 height = 10 - grid = HexGrid(width, height, torus=False, capacity=None) + grid = HexGrid([width, height], torus=False, capacity=None) for radius, n in zip(range(1, 4), [5, 10, 17]): neighborhood = grid._cells[(1, 0)].neighborhood(radius=radius) assert len(neighborhood) == n @@ -139,7 +139,7 @@ def test_hexgrid(): width = 10 height = 10 - grid = HexGrid(width, height, torus=False) + grid = HexGrid([width, height], torus=False) assert len(grid._cells) == width * height # first row @@ -175,7 +175,7 @@ def test_hexgrid(): # fmt: on - grid = HexGrid(width, height, torus=True) + grid = HexGrid([width, height], torus=True) assert len(grid._cells) == width * height # first row From b3efdee0c5382276505d522903f2e8f1a7e3f4f5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 17 Feb 2024 20:38:00 +0000 Subject: [PATCH 40/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/WolfSheep/wolf_sheep.py | 3 ++- mesa/experimental/cell_space/grid.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 85fa86e3885..af05d43e254 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -38,7 +38,8 @@ def spawn_offspring(self): offspring.move_to(self.cell) self.model.schedule.add(offspring) - def feed(self): ... + def feed(self): + ... def die(self): self.cell.remove_agent(self) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 40fe57f9459..b0a457e097a 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -88,7 +88,6 @@ def _connect_single_cell(self, cell): class OrthogonalMooreGrid(Grid): - @staticmethod def _default_neighborhood_func(cell): # fmt: off @@ -102,7 +101,6 @@ def _default_neighborhood_func(cell): class OrthogonalVonNeumannGrid(Grid): - @staticmethod def _default_neighborhood_func(cell): # fmt: off @@ -116,7 +114,6 @@ def _default_neighborhood_func(cell): class HexGrid(Grid): - @staticmethod def _default_neighborhood_func(cell): i, j = cell.coordinate From 8c7ad05dddc1ffc77798804a19ebc59f3a8d83a3 Mon Sep 17 00:00:00 2001 From: Corvince Date: Sun, 18 Feb 2024 20:06:12 +0100 Subject: [PATCH 41/63] add validation, remove neighborhood_func, add tests --- mesa/experimental/cell_space/grid.py | 105 +++++++++++-------- tests/test_cell_space.py | 146 +++++++++++++++++++++++++++ 2 files changed, 211 insertions(+), 40 deletions(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index b0a457e097a..2fd190a9ea0 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -23,16 +23,13 @@ def __init__( capacity: int | None = None, random: Random | None = None, cell_klass: type[Cell] = Cell, - neighborhood_func: Callable[[Cell], list[tuple[int, int]]] | None = None, ) -> None: super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) self.torus = torus self.dimensions = dimensions self._try_random = True - if neighborhood_func is not None: - self.neighborhood_func = neighborhood_func - else: - self.neighborhood_func = self._default_neighborhood_func + + self._validate_parameters() coordinates = product(*(range(dim) for dim in self.dimensions)) @@ -44,8 +41,15 @@ def __init__( for cell in self.all_cells: self._connect_single_cell(cell) - @staticmethod - def _default_neighborhood_func(cell: Cell) -> list[tuple[int, int]]: + def _validate_parameters(self): + if not all(isinstance(dim, int) and dim > 0 for dim in self.dimensions): + raise ValueError("Dimensions must be a list of positive integers.") + if not isinstance(self.torus, bool): + raise ValueError("Torus must be a boolean.") + if self.capacity is not None and not isinstance(self.capacity, int): + raise ValueError("Capacity must be an integer or None.") + + def _calculate_neighborhood_offsets(self, cell: Cell) -> list[tuple[int, int]]: # Default implementation return [] @@ -70,67 +74,88 @@ def select_random_empty_cell(self) -> Cell: def _connect_single_cell(self, cell): coord = cell.coordinate - for d_coord in self.neighborhood_func(cell): + for d_coord in self._calculate_neighborhood_offsets(cell): n_coord = tuple(c + dc for c, dc in zip(coord, d_coord)) if self.torus: n_coord = tuple(nc % d for nc, d in zip(n_coord, self.dimensions)) if all(0 <= nc < d for nc, d in zip(n_coord, self.dimensions)): cell.connect(self._cells[n_coord]) - # i, j = cell.coordinate - # for di, dj in self.neighborhood_func(cell): - # ni, nj = (i + di, j + dj) - # if self.torus: - # ni, nj = ni % self.height, nj % self.width - # if 0 <= ni < self.height and 0 <= nj < self.width: - # cell.connect(self._cells[ni, nj]) +class OrthogonalMooreGrid(Grid): + """Grid where cells are connected to their 8 neighbors. + + Example for two dimensions: + directions = [ + (-1, -1), (-1, 0), (-1, 1), + ( 0, -1), ( 0, 1), + ( 1, -1), ( 1, 0), ( 1, 1), + ] + """ + def _calculate_neighborhood_offsets(self, cell): -class OrthogonalMooreGrid(Grid): - @staticmethod - def _default_neighborhood_func(cell): - # fmt: off - directions = [ - (-1, -1), (-1, 0), (-1, 1), - ( 0, -1), ( 0, 1), - ( 1, -1), ( 1, 0), ( 1, 1), - ] - # fmt: on - return directions + offsets = list(product([-1, 0, 1], repeat=len(self.dimensions))) + offsets.remove((0,) * len(self.dimensions)) # Remove the central cell + return offsets class OrthogonalVonNeumannGrid(Grid): - @staticmethod - def _default_neighborhood_func(cell): - # fmt: off - directions = [ - (0, -1), - (-1, 0), ( 1, 0), - (0, 1), - ] - # fmt: on - return directions + """Grid where cells are connected to their 4 neighbors. + + Example for two dimensions: + directions = [ + (0, -1), + (-1, 0), ( 1, 0), + (0, 1), + ] + """ + + def _calculate_neighborhood_offsets(self, cell: Cell): + """ + Calculates the offsets for a Von Neumann neighborhood in an n-dimensional grid. + This neighborhood includes all cells that are one step away in any single dimension. + + Returns: + A list of tuples representing the relative positions of neighboring cells. + """ + offsets = [] + dimensions = len(self.dimensions) + for dim in range(dimensions): + for delta in [ + -1, + 1, + ]: # Move one step in each direction for the current dimension + offset = [0] * dimensions + offset[dim] = delta + offsets.append(tuple(offset)) + return offsets class HexGrid(Grid): + + def _validate_parameters(self): + super()._validate_parameters() + if len(self.dimensions) != 2: + raise ValueError("HexGrid must have exactly 2 dimensions.") + @staticmethod - def _default_neighborhood_func(cell): + def _calculate_neighborhood_offsets(cell): i, j = cell.coordinate # fmt: off if i % 2 == 0: - directions = [ + offsets = [ (-1, -1), (-1, 0), ( 0, -1), ( 0, 1), ( 1, -1), ( 1, 0), ] else: - directions = [ + offsets = [ (-1, 0), (-1, 1), ( 0, -1), ( 0, 1), ( 1, 0), ( 1, 1), ] # fmt: on - return directions + return offsets diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 7f08ac58cab..7fc41d97a9b 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -65,6 +65,69 @@ def test_orthogonal_grid_neumann(): assert connection.coordinate in {(9, 0), (9, 8), (8, 9), (0, 9)} +def test_orthogonal_grid_neumann_3d(): + width = 10 + height = 10 + depth = 10 + grid = OrthogonalVonNeumannGrid([width, height, depth], torus=False, capacity=None) + + assert len(grid._cells) == width * height * depth + + # von neumann neighborhood, torus false, top left corner + assert len(grid._cells[(0, 0, 0)]._connections) == 3 + for connection in grid._cells[(0, 0, 0)]._connections: + assert connection.coordinate in {(0, 0, 1), (0, 1, 0), (1, 0, 0)} + + # von neumann neighborhood, torus false, top right corner + for connection in grid._cells[(0, width - 1, 0)]._connections: + assert connection.coordinate in { + (0, width - 1, 1), + (0, width - 2, 0), + (1, width - 1, 0), + } + + # von neumann neighborhood, torus false, bottom left corner + for connection in grid._cells[(height - 1, 0, 0)]._connections: + assert connection.coordinate in { + (height - 1, 0, 1), + (height - 1, 1, 0), + (height - 2, 0, 0), + } + + # von neumann neighborhood, torus false, bottom right corner + for connection in grid._cells[(height - 1, width - 1, 0)]._connections: + assert connection.coordinate in { + (height - 1, width - 1, 1), + (height - 1, width - 2, 0), + (height - 2, width - 1, 0), + } + + # von neumann neighborhood middle of grid + assert len(grid._cells[(5, 5, 5)]._connections) == 6 + for connection in grid._cells[(5, 5, 5)]._connections: + assert connection.coordinate in { + (4, 5, 5), + (5, 4, 5), + (5, 5, 4), + (5, 5, 6), + (5, 6, 5), + (6, 5, 5), + } + + # von neumann neighborhood, torus True, top corner + grid = OrthogonalVonNeumannGrid([width, height, depth], torus=True, capacity=None) + assert len(grid._cells[(0, 0, 0)]._connections) == 6 + for connection in grid._cells[(0, 0, 0)]._connections: + assert connection.coordinate in { + (0, 0, 1), + (0, 1, 0), + (1, 0, 0), + (0, 0, 9), + (0, 9, 0), + (9, 0, 0), + } + + def test_orthogonal_grid_moore(): width = 10 height = 10 @@ -95,6 +158,89 @@ def test_orthogonal_grid_moore(): # fmt: on +def test_orthogonal_grid_moore_3d(): + width = 10 + height = 10 + depth = 10 + + # Moore neighborhood, torus false, top corner + grid = OrthogonalMooreGrid([width, height, depth], torus=False, capacity=None) + assert len(grid._cells[(0, 0, 0)]._connections) == 7 + for connection in grid._cells[(0, 0, 0)]._connections: + assert connection.coordinate in { + (0, 0, 1), + (0, 1, 0), + (0, 1, 1), + (1, 0, 0), + (1, 0, 1), + (1, 1, 0), + (1, 1, 1), + } + + # Moore neighborhood middle of grid + assert len(grid._cells[(5, 5, 5)]._connections) == 26 + for connection in grid._cells[(5, 5, 5)]._connections: + # fmt: off + assert connection.coordinate in {(4, 4, 4), (4, 4, 5), (4, 4, 6), (4, 5, 4), (4, 5, 5), (4, 5, 6), (4, 6, 4), (4, 6, 5), (4, 6, 6), + (5, 4, 4), (5, 4, 5), (5, 4, 6), (5, 5, 4), (5, 5, 6), (5, 6, 4), (5, 6, 5), (5, 6, 6), + (6, 4, 4), (6, 4, 5), (6, 4, 6), (6, 5, 4), (6, 5, 5), (6, 5, 6), (6, 6, 4), (6, 6, 5), (6, 6, 6)} + # fmt: on + + # Moore neighborhood, torus True, top corner + grid = OrthogonalMooreGrid([width, height, depth], torus=True, capacity=None) + assert len(grid._cells[(0, 0, 0)]._connections) == 26 + for connection in grid._cells[(0, 0, 0)]._connections: + # fmt: off + assert connection.coordinate in {(9, 9, 9), (9, 9, 0), (9, 9, 1), (9, 0, 9), (9, 0, 0), (9, 0, 1), (9, 1, 9), (9, 1, 0), (9, 1, 1), + (0, 9, 9), (0, 9, 0), (0, 9, 1), (0, 0, 9), (0, 0, 1), (0, 1, 9), (0, 1, 0), (0, 1, 1), + (1, 9, 9), (1, 9, 0), (1, 9, 1), (1, 0, 9), (1, 0, 0), (1, 0, 1), (1, 1, 9), (1, 1, 0), (1, 1, 1)} + # fmt: on + + +def test_orthogonal_grid_moore_4d(): + width = 10 + height = 10 + depth = 10 + time = 10 + + # Moore neighborhood, torus false, top corner + grid = OrthogonalMooreGrid([width, height, depth, time], torus=False, capacity=None) + assert len(grid._cells[(0, 0, 0, 0)]._connections) == 15 + for connection in grid._cells[(0, 0, 0, 0)]._connections: + assert connection.coordinate in { + (0, 0, 0, 1), + (0, 0, 1, 0), + (0, 0, 1, 1), + (0, 1, 0, 0), + (0, 1, 0, 1), + (0, 1, 1, 0), + (0, 1, 1, 1), + (1, 0, 0, 0), + (1, 0, 0, 1), + (1, 0, 1, 0), + (1, 0, 1, 1), + (1, 1, 0, 0), + (1, 1, 0, 1), + (1, 1, 1, 0), + (1, 1, 1, 1), + } + + # Moore neighborhood middle of grid + assert len(grid._cells[(5, 5, 5, 5)]._connections) == 80 + for connection in grid._cells[(5, 5, 5, 5)]._connections: + # fmt: off + assert connection.coordinate in {(4, 4, 4, 4), (4, 4, 4, 5), (4, 4, 4, 6), (4, 4, 5, 4), (4, 4, 5, 5), (4, 4, 5, 6), (4, 4, 6, 4), (4, 4, 6, 5), (4, 4, 6, 6), + (4, 5, 4, 4), (4, 5, 4, 5), (4, 5, 4, 6), (4, 5, 5, 4), (4, 5, 5, 5), (4, 5, 5, 6), (4, 5, 6, 4), (4, 5, 6, 5), (4, 5, 6, 6), + (4, 6, 4, 4), (4, 6, 4, 5), (4, 6, 4, 6), (4, 6, 5, 4), (4, 6, 5, 5), (4, 6, 5, 6), (4, 6, 6, 4), (4, 6, 6, 5), (4, 6, 6, 6), + (5, 4, 4, 4), (5, 4, 4, 5), (5, 4, 4, 6), (5, 4, 5, 4), (5, 4, 5, 5), (5, 4, 5, 6), (5, 4, 6, 4), (5, 4, 6, 5), (5, 4, 6, 6), + (5, 5, 4, 4), (5, 5, 4, 5), (5, 5, 4, 6), (5, 5, 5, 4), (5, 5, 5, 6), (5, 5, 6, 4), (5, 5, 6, 5), (5, 5, 6, 6), + (5, 6, 4, 4), (5, 6, 4, 5), (5, 6, 4, 6), (5, 6, 5, 4), (5, 6, 5, 5), (5, 6, 5, 6), (5, 6, 6, 4), (5, 6, 6, 5), (5, 6, 6, 6), + (6, 4, 4, 4), (6, 4, 4, 5), (6, 4, 4, 6), (6, 4, 5, 4), (6, 4, 5, 5), (6, 4, 5, 6), (6, 4, 6, 4), (6, 4, 6, 5), (6, 4, 6, 6), + (6, 5, 4, 4), (6, 5, 4, 5), (6, 5, 4, 6), (6, 5, 5, 4), (6, 5, 5, 5), (6, 5, 5, 6), (6, 5, 6, 4), (6, 5, 6, 5), (6, 5, 6, 6), + (6, 6, 4, 4), (6, 6, 4, 5), (6, 6, 4, 6), (6, 6, 5, 4), (6, 6, 5, 5), (6, 6, 5, 6), (6, 6, 6, 4), (6, 6, 6, 5), (6, 6, 6, 6)} + # fmt: on + + def test_cell_neighborhood(): # orthogonal grid From 819e903f68736a947b204a88047fef7fdbc6927a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:07:40 +0000 Subject: [PATCH 42/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/grid.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 2fd190a9ea0..f51721ff68e 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -1,6 +1,5 @@ from itertools import product from random import Random -from typing import Callable from mesa.experimental.cell_space import Cell, DiscreteSpace @@ -94,7 +93,6 @@ class OrthogonalMooreGrid(Grid): """ def _calculate_neighborhood_offsets(self, cell): - offsets = list(product([-1, 0, 1], repeat=len(self.dimensions))) offsets.remove((0,) * len(self.dimensions)) # Remove the central cell return offsets @@ -133,7 +131,6 @@ def _calculate_neighborhood_offsets(self, cell: Cell): class HexGrid(Grid): - def _validate_parameters(self): super()._validate_parameters() if len(self.dimensions) != 2: From 0f49dc7d85984a3181707d22f0171692b07f0438 Mon Sep 17 00:00:00 2001 From: Corvince Date: Mon, 19 Feb 2024 11:13:50 +0100 Subject: [PATCH 43/63] Make Cell* generic --- mesa/experimental/cell_space/cell.py | 11 +++- mesa/experimental/cell_space/cell_agent.py | 8 ++- .../cell_space/cell_collection.py | 18 ++--- .../experimental/cell_space/discrete_space.py | 21 +++--- mesa/experimental/cell_space/grid.py | 38 +++++++---- tests/test_cell_space.py | 65 ++++++++++++------- 6 files changed, 102 insertions(+), 59 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index b203d0bb2f8..b1c60359b3e 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -2,7 +2,7 @@ from functools import cache from random import Random -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Optional from mesa.experimental.cell_space.cell_collection import CellCollection @@ -32,7 +32,10 @@ class Cell: ] def __init__( - self, coordinate: Any, capacity: int | None = None, random: Random | None = None + self, + coordinate: tuple[int, ...], + capacity: Optional[int] = None, + random: Optional[Random] = None, ) -> None: """ " @@ -45,7 +48,9 @@ def __init__( super().__init__() self.coordinate = coordinate self._connections: list[Cell] = [] # TODO: change to CellCollection? - self.agents = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) + self.agents = ( + [] + ) # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) self.capacity = capacity self.properties: dict[str, object] = {} self.random = random diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index c305ff19143..f78f03294af 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -1,8 +1,12 @@ +from typing import Generic, TypeVar + from mesa import Agent, Model from mesa.experimental.cell_space.cell import Cell +T = TypeVar("T", bound=Cell) + -class CellAgent(Agent): +class CellAgent(Agent, Generic[T]): """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces @@ -22,7 +26,7 @@ def __init__(self, unique_id: int, model: Model) -> None: model (Model): The model instance in which the agent exists. """ super().__init__(unique_id, model) - self.cell: Cell | None = None + self.cell: T | None = None def move_to(self, cell) -> None: if self.cell is not None: diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index ecf479fbaa7..87885398db6 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -1,17 +1,19 @@ from __future__ import annotations import itertools -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from functools import cached_property from random import Random -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, Generic, Optional, TypeVar if TYPE_CHECKING: from mesa.experimental.cell_space.cell import Cell from mesa.experimental.cell_space.cell_agent import CellAgent +T = TypeVar("T", bound=Cell) -class CellCollection: + +class CellCollection(Generic[T]): """An immutable collection of cells Attributes: @@ -23,7 +25,7 @@ class CellCollection: def __init__( self, - cells: dict[Cell, list[CellAgent]] | Iterable[Cell], + cells: Mapping[T, list[CellAgent]] | Iterable[T], random: Random | None = None, ) -> None: if isinstance(cells, dict): @@ -38,7 +40,7 @@ def __init__( def __iter__(self): return iter(self._cells) - def __getitem__(self, key: Cell) -> Iterable[CellAgent]: + def __getitem__(self, key: T) -> Iterable[CellAgent]: return self._cells[key] # @cached_property @@ -49,20 +51,20 @@ def __repr__(self): return f"CellCollection({self._cells})" @cached_property - def cells(self) -> list[Cell]: + def cells(self) -> list[T]: return list(self._cells.keys()) @property def agents(self) -> Iterable[CellAgent]: return itertools.chain.from_iterable(self._cells.values()) - def select_random_cell(self) -> Cell: + def select_random_cell(self) -> T: return self.random.choice(self.cells) def select_random_agent(self) -> CellAgent: return self.random.choice(list(self.agents)) - def select(self, filter_func: [Callable[[Cell], bool]] = None, n=0): + def select(self, filter_func: Optional[Callable[[T], bool]] = None, n=0): # FIXME: n is not considered if filter_func is None and n == 0: return self diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 5c7d71898b3..068e9c2595f 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -2,13 +2,15 @@ from functools import cached_property from random import Random +from typing import Generic, Optional, TypeVar from mesa.experimental.cell_space.cell import Cell from mesa.experimental.cell_space.cell_collection import CellCollection -from mesa.space import Coordinate +T = TypeVar("T", bound=Cell) -class DiscreteSpace: + +class DiscreteSpace(Generic[T]): """Base class for all discrete spaces. Attributes: @@ -22,27 +24,26 @@ class DiscreteSpace: def __init__( self, - capacity: int | None = None, - cell_klass: type[Cell] = Cell, - random: Random | None = None, + capacity: Optional[int] = None, + cell_klass: type[T] = Cell, + random: Optional[Random] = None, ): super().__init__() self.capacity = capacity - self._cells: dict[Coordinate, Cell] = {} + self._cells: dict[tuple[int, ...], T] = {} if random is None: random = Random() # FIXME should default to default rng from model self.random = random self.cell_klass = cell_klass - self._empties: dict[Coordinate, None] = {} + self._empties: dict[tuple[int, ...], None] = {} self._empties_initialized = False @property def cutoff_empties(self): return 7.953 * len(self._cells) ** 0.384 - def _connect_single_cell(self, cell): - ... + def _connect_single_cell(self, cell: T): ... @cached_property def all_cells(self): @@ -58,6 +59,6 @@ def __getitem__(self, key): def empties(self) -> CellCollection: return self.all_cells.select(lambda cell: cell.is_empty) - def select_random_empty_cell(self) -> Cell: + def select_random_empty_cell(self) -> T: """select random empty cell""" return self.random.choice(list(self.empties)) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index f51721ff68e..3ee8c52c910 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -1,10 +1,14 @@ +from collections.abc import Sequence from itertools import product from random import Random +from typing import Generic, Optional, TypeVar from mesa.experimental.cell_space import Cell, DiscreteSpace +T = TypeVar("T", bound=Cell) -class Grid(DiscreteSpace): + +class Grid(DiscreteSpace, Generic[T]): """Base class for all grid classes Attributes: @@ -17,11 +21,11 @@ class Grid(DiscreteSpace): def __init__( self, - dimensions: list[int], + dimensions: Sequence[int], torus: bool = False, - capacity: int | None = None, - random: Random | None = None, - cell_klass: type[Cell] = Cell, + capacity: Optional[int] = None, + random: Optional[Random] = None, + cell_klass: type[T] = Cell, ) -> None: super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) self.torus = torus @@ -48,11 +52,11 @@ def _validate_parameters(self): if self.capacity is not None and not isinstance(self.capacity, int): raise ValueError("Capacity must be an integer or None.") - def _calculate_neighborhood_offsets(self, cell: Cell) -> list[tuple[int, int]]: + def _calculate_neighborhood_offsets(self, cell: T) -> list[tuple[int, ...]]: # Default implementation return [] - def select_random_empty_cell(self) -> Cell: + def select_random_empty_cell(self) -> T: # FIXME:: currently just a simple boolean to control behavior # FIXME:: basically if grid is close to 99% full, creating empty list can be faster # FIXME:: note however that the old results don't apply because in this implementation @@ -70,7 +74,7 @@ def select_random_empty_cell(self) -> Cell: else: return super().select_random_empty_cell() - def _connect_single_cell(self, cell): + def _connect_single_cell(self, cell: T) -> None: coord = cell.coordinate for d_coord in self._calculate_neighborhood_offsets(cell): @@ -81,7 +85,7 @@ def _connect_single_cell(self, cell): cell.connect(self._cells[n_coord]) -class OrthogonalMooreGrid(Grid): +class OrthogonalMooreGrid(Grid[T]): """Grid where cells are connected to their 8 neighbors. Example for two dimensions: @@ -92,13 +96,18 @@ class OrthogonalMooreGrid(Grid): ] """ - def _calculate_neighborhood_offsets(self, cell): + def _calculate_neighborhood_offsets(self, cell: T) -> list[tuple[int, ...]]: + """ + Calculates the offsets for a Moore neighborhood in an n-dimensional grid. + This neighborhood includes all cells that are one step away in any dimension. + """ + offsets = list(product([-1, 0, 1], repeat=len(self.dimensions))) offsets.remove((0,) * len(self.dimensions)) # Remove the central cell return offsets -class OrthogonalVonNeumannGrid(Grid): +class OrthogonalVonNeumannGrid(Grid[T]): """Grid where cells are connected to their 4 neighbors. Example for two dimensions: @@ -109,7 +118,7 @@ class OrthogonalVonNeumannGrid(Grid): ] """ - def _calculate_neighborhood_offsets(self, cell: Cell): + def _calculate_neighborhood_offsets(self, cell: T) -> list[tuple[int, ...]]: """ Calculates the offsets for a Von Neumann neighborhood in an n-dimensional grid. This neighborhood includes all cells that are one step away in any single dimension. @@ -130,14 +139,15 @@ def _calculate_neighborhood_offsets(self, cell: Cell): return offsets -class HexGrid(Grid): +class HexGrid(Grid[T]): + def _validate_parameters(self): super()._validate_parameters() if len(self.dimensions) != 2: raise ValueError("HexGrid must have exactly 2 dimensions.") @staticmethod - def _calculate_neighborhood_offsets(cell): + def _calculate_neighborhood_offsets(cell: T) -> list[tuple[int, int]]: i, j = cell.coordinate # fmt: off diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 7fc41d97a9b..40c0961e2e1 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -17,7 +17,7 @@ def test_orthogonal_grid_neumann(): width = 10 height = 10 - grid = OrthogonalVonNeumannGrid([width, height], torus=False, capacity=None) + grid = OrthogonalVonNeumannGrid((width, height), torus=False, capacity=None) assert len(grid._cells) == width * height @@ -47,7 +47,7 @@ def test_orthogonal_grid_neumann(): assert connection.coordinate in {(4, 5), (5, 4), (5, 6), (6, 5)} # von neumann neighborhood, torus True, top corner - grid = OrthogonalVonNeumannGrid([width, height], torus=True, capacity=None) + grid = OrthogonalVonNeumannGrid((width, height), torus=True, capacity=None) assert len(grid._cells[(0, 0)]._connections) == 4 for connection in grid._cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0), (0, 9), (9, 0)} @@ -69,7 +69,7 @@ def test_orthogonal_grid_neumann_3d(): width = 10 height = 10 depth = 10 - grid = OrthogonalVonNeumannGrid([width, height, depth], torus=False, capacity=None) + grid = OrthogonalVonNeumannGrid((width, height, depth), torus=False, capacity=None) assert len(grid._cells) == width * height * depth @@ -115,7 +115,7 @@ def test_orthogonal_grid_neumann_3d(): } # von neumann neighborhood, torus True, top corner - grid = OrthogonalVonNeumannGrid([width, height, depth], torus=True, capacity=None) + grid = OrthogonalVonNeumannGrid((width, height, depth), torus=True, capacity=None) assert len(grid._cells[(0, 0, 0)]._connections) == 6 for connection in grid._cells[(0, 0, 0)]._connections: assert connection.coordinate in { @@ -133,7 +133,7 @@ def test_orthogonal_grid_moore(): height = 10 # Moore neighborhood, torus false, top corner - grid = OrthogonalMooreGrid([width, height], torus=False, capacity=None) + grid = OrthogonalMooreGrid((width, height), torus=False, capacity=None) assert len(grid._cells[(0, 0)]._connections) == 3 for connection in grid._cells[(0, 0)]._connections: assert connection.coordinate in {(0, 1), (1, 0), (1, 1)} @@ -164,7 +164,7 @@ def test_orthogonal_grid_moore_3d(): depth = 10 # Moore neighborhood, torus false, top corner - grid = OrthogonalMooreGrid([width, height, depth], torus=False, capacity=None) + grid = OrthogonalMooreGrid((width, height, depth), torus=False, capacity=None) assert len(grid._cells[(0, 0, 0)]._connections) == 7 for connection in grid._cells[(0, 0, 0)]._connections: assert connection.coordinate in { @@ -187,7 +187,7 @@ def test_orthogonal_grid_moore_3d(): # fmt: on # Moore neighborhood, torus True, top corner - grid = OrthogonalMooreGrid([width, height, depth], torus=True, capacity=None) + grid = OrthogonalMooreGrid((width, height, depth), torus=True, capacity=None) assert len(grid._cells[(0, 0, 0)]._connections) == 26 for connection in grid._cells[(0, 0, 0)]._connections: # fmt: off @@ -204,7 +204,7 @@ def test_orthogonal_grid_moore_4d(): time = 10 # Moore neighborhood, torus false, top corner - grid = OrthogonalMooreGrid([width, height, depth, time], torus=False, capacity=None) + grid = OrthogonalMooreGrid((width, height, depth, time), torus=False, capacity=None) assert len(grid._cells[(0, 0, 0, 0)]._connections) == 15 for connection in grid._cells[(0, 0, 0, 0)]._connections: assert connection.coordinate in { @@ -241,13 +241,34 @@ def test_orthogonal_grid_moore_4d(): # fmt: on +def test_orthogonal_grid_moore_1d(): + width = 10 + + # Moore neighborhood, torus false, left edge + grid = OrthogonalMooreGrid((width,), torus=False, capacity=None) + assert len(grid._cells[(0,)]._connections) == 1 + for connection in grid._cells[(0,)]._connections: + assert connection.coordinate in {(1,)} + + # Moore neighborhood middle of grid + assert len(grid._cells[(5,)]._connections) == 2 + for connection in grid._cells[(5,)]._connections: + assert connection.coordinate in {(4,), (6,)} + + # Moore neighborhood, torus True, left edge + grid = OrthogonalMooreGrid((width,), torus=True, capacity=None) + assert len(grid._cells[(0,)]._connections) == 2 + for connection in grid._cells[(0,)]._connections: + assert connection.coordinate in {(1,), (9,)} + + def test_cell_neighborhood(): # orthogonal grid ## von Neumann width = 10 height = 10 - grid = OrthogonalVonNeumannGrid([width, height], torus=False, capacity=None) + grid = OrthogonalVonNeumannGrid((width, height), torus=False, capacity=None) for radius, n in zip(range(1, 4), [2, 5, 9]): neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n @@ -255,7 +276,7 @@ def test_cell_neighborhood(): ## Moore width = 10 height = 10 - grid = OrthogonalMooreGrid([width, height], torus=False, capacity=None) + grid = OrthogonalMooreGrid((width, height), torus=False, capacity=None) for radius, n in zip(range(1, 4), [3, 8, 15]): neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n @@ -266,14 +287,14 @@ def test_cell_neighborhood(): # hexgrid width = 10 height = 10 - grid = HexGrid([width, height], torus=False, capacity=None) + grid = HexGrid((width, height), torus=False, capacity=None) for radius, n in zip(range(1, 4), [2, 6, 11]): neighborhood = grid._cells[(0, 0)].neighborhood(radius=radius) assert len(neighborhood) == n width = 10 height = 10 - grid = HexGrid([width, height], torus=False, capacity=None) + grid = HexGrid((width, height), torus=False, capacity=None) for radius, n in zip(range(1, 4), [5, 10, 17]): neighborhood = grid._cells[(1, 0)].neighborhood(radius=radius) assert len(neighborhood) == n @@ -285,7 +306,7 @@ def test_hexgrid(): width = 10 height = 10 - grid = HexGrid([width, height], torus=False) + grid = HexGrid((width, height), torus=False) assert len(grid._cells) == width * height # first row @@ -321,7 +342,7 @@ def test_hexgrid(): # fmt: on - grid = HexGrid([width, height], torus=True) + grid = HexGrid((width, height), torus=True) assert len(grid._cells) == width * height # first row @@ -341,7 +362,7 @@ def test_networkgrid(): n = 10 m = 20 seed = 42 - G = nx.gnm_random_graph(n, m, seed=seed) + G = nx.gnm_random_graph(n, m, seed=seed) # noqa: N806 grid = Network(G) assert len(grid._cells) == n @@ -357,22 +378,22 @@ def test_empties_space(): n = 10 m = 20 seed = 42 - G = nx.gnm_random_graph(n, m, seed=seed) + G = nx.gnm_random_graph(n, m, seed=seed) # noqa: N806 grid = Network(G) assert len(grid.empties) == n model = Model() for i in range(8): - grid._cells[i].add_agent(CellAgent(i, model)) + grid._cells[(i,)].add_agent(CellAgent(i, model)) cell = grid.select_random_empty_cell() assert cell.coordinate in {8, 9} def test_cell(): - cell1 = Cell(1, capacity=None, random=random.Random()) - cell2 = Cell(2, capacity=None, random=random.Random()) + cell1 = Cell((1,), capacity=None, random=random.Random()) + cell2 = Cell((2,), capacity=None, random=random.Random()) # connect cell1.connect(cell2) @@ -400,7 +421,7 @@ def test_cell(): with pytest.raises(ValueError): cell1.remove_agent(agent) - cell1 = Cell(1, capacity=1, random=random.Random()) + cell1 = Cell((1,), capacity=1, random=random.Random()) cell1.add_agent(CellAgent(1, model)) assert cell1.is_full @@ -409,7 +430,7 @@ def test_cell(): def test_cell_collection(): - cell1 = Cell(1, capacity=None, random=random.Random()) + cell1 = Cell((1,), capacity=None, random=random.Random()) collection = CellCollection({cell1: cell1.agents}, random=random.Random()) assert len(collection) == 1 @@ -417,7 +438,7 @@ def test_cell_collection(): rng = random.Random() n = 10 - collection = CellCollection([Cell(i, random=rng) for i in range(n)], random=rng) + collection = CellCollection([Cell((i,), random=rng) for i in range(n)], random=rng) assert len(collection) == n cell = collection.select_random_cell() From da94fb1482942ea085eb437b32f61697b12a5c37 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:15:06 +0000 Subject: [PATCH 44/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/cell.py | 8 +++----- mesa/experimental/cell_space/cell_collection.py | 2 +- mesa/experimental/cell_space/discrete_space.py | 7 ++++--- mesa/experimental/cell_space/grid.py | 1 - 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index b1c60359b3e..d5f9d7aa0d9 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -34,8 +34,8 @@ class Cell: def __init__( self, coordinate: tuple[int, ...], - capacity: Optional[int] = None, - random: Optional[Random] = None, + capacity: int | None = None, + random: Random | None = None, ) -> None: """ " @@ -48,9 +48,7 @@ def __init__( super().__init__() self.coordinate = coordinate self._connections: list[Cell] = [] # TODO: change to CellCollection? - self.agents = ( - [] - ) # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) + self.agents = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) self.capacity = capacity self.properties: dict[str, object] = {} self.random = random diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 87885398db6..aa0ebad1d83 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -64,7 +64,7 @@ def select_random_cell(self) -> T: def select_random_agent(self) -> CellAgent: return self.random.choice(list(self.agents)) - def select(self, filter_func: Optional[Callable[[T], bool]] = None, n=0): + def select(self, filter_func: Callable[[T], bool] | None = None, n=0): # FIXME: n is not considered if filter_func is None and n == 0: return self diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 068e9c2595f..f7c049daab1 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -24,9 +24,9 @@ class DiscreteSpace(Generic[T]): def __init__( self, - capacity: Optional[int] = None, + capacity: int | None = None, cell_klass: type[T] = Cell, - random: Optional[Random] = None, + random: Random | None = None, ): super().__init__() self.capacity = capacity @@ -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): diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 3ee8c52c910..4e8aad5dfa6 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -140,7 +140,6 @@ def _calculate_neighborhood_offsets(self, cell: T) -> list[tuple[int, ...]]: class HexGrid(Grid[T]): - def _validate_parameters(self): super()._validate_parameters() if len(self.dimensions) != 2: From b210686f3da71dea26ba5ec983ddbe9018da59bc Mon Sep 17 00:00:00 2001 From: Corvince Date: Mon, 19 Feb 2024 11:13:50 +0100 Subject: [PATCH 45/63] Make Cell* generic --- mesa/experimental/cell_space/cell.py | 14 +++++++++----- mesa/experimental/cell_space/cell_collection.py | 9 ++++----- mesa/experimental/cell_space/discrete_space.py | 7 +++---- mesa/experimental/cell_space/network.py | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index d5f9d7aa0d9..14e27a69a45 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -34,8 +34,8 @@ class Cell: def __init__( self, coordinate: tuple[int, ...], - capacity: int | None = None, - random: Random | None = None, + capacity: Optional[int] = None, + random: Optional[Random] = None, ) -> None: """ " @@ -48,7 +48,9 @@ def __init__( super().__init__() self.coordinate = coordinate self._connections: list[Cell] = [] # TODO: change to CellCollection? - self.agents = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) + self.agents = ( + [] + ) # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) self.capacity = capacity self.properties: dict[str, object] = {} self.random = random @@ -110,14 +112,16 @@ def is_full(self) -> bool: def __repr__(self): return f"Cell({self.coordinate}, {self.agents})" - @cache + # FIXME: Revisit caching strategy on methods + @cache # noqa: B019 def neighborhood(self, radius=1, include_center=False): return CellCollection( self._neighborhood(radius=radius, include_center=include_center), random=self.random, ) - @cache + # FIXME: Revisit caching strategy on methods + @cache # noqa: B019 def _neighborhood(self, radius=1, include_center=False): # if radius == 0: # return {self: self.agents} diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index aa0ebad1d83..0232ab2405d 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -4,11 +4,10 @@ from collections.abc import Iterable, Mapping from functools import cached_property from random import Random -from typing import TYPE_CHECKING, Callable, Generic, Optional, TypeVar +from typing import Callable, Generic, Optional, TypeVar -if TYPE_CHECKING: - from mesa.experimental.cell_space.cell import Cell - from mesa.experimental.cell_space.cell_agent import CellAgent +from mesa.experimental.cell_space.cell import Cell +from mesa.experimental.cell_space.cell_agent import CellAgent T = TypeVar("T", bound=Cell) @@ -64,7 +63,7 @@ def select_random_cell(self) -> T: def select_random_agent(self) -> CellAgent: return self.random.choice(list(self.agents)) - def select(self, filter_func: Callable[[T], bool] | None = None, n=0): + def select(self, filter_func: Optional[Callable[[T], bool]] = None, n=0): # FIXME: n is not considered if filter_func is None and n == 0: return self diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index f7c049daab1..068e9c2595f 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -24,9 +24,9 @@ class DiscreteSpace(Generic[T]): def __init__( self, - capacity: int | None = None, + capacity: Optional[int] = None, cell_klass: type[T] = Cell, - random: Random | None = None, + random: Optional[Random] = None, ): super().__init__() self.capacity = capacity @@ -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/network.py b/mesa/experimental/cell_space/network.py index 501b263d406..3983287e4ef 100644 --- a/mesa/experimental/cell_space/network.py +++ b/mesa/experimental/cell_space/network.py @@ -10,7 +10,7 @@ class Network(DiscreteSpace): def __init__( self, - G: Any, + G: Any, # noqa: N803 capacity: int | None = None, random: Random | None = None, cell_klass: type[Cell] = Cell, From c35591ceb9398b8f717b1c288d4c71a2aae75bfc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:26:13 +0000 Subject: [PATCH 46/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/cell.py | 8 +++----- mesa/experimental/cell_space/cell_collection.py | 2 +- mesa/experimental/cell_space/discrete_space.py | 7 ++++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 14e27a69a45..3d368555d07 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -34,8 +34,8 @@ class Cell: def __init__( self, coordinate: tuple[int, ...], - capacity: Optional[int] = None, - random: Optional[Random] = None, + capacity: int | None = None, + random: Random | None = None, ) -> None: """ " @@ -48,9 +48,7 @@ def __init__( super().__init__() self.coordinate = coordinate self._connections: list[Cell] = [] # TODO: change to CellCollection? - self.agents = ( - [] - ) # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) + self.agents = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) self.capacity = capacity self.properties: dict[str, object] = {} self.random = random diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 0232ab2405d..c18abf65f56 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -63,7 +63,7 @@ def select_random_cell(self) -> T: def select_random_agent(self) -> CellAgent: return self.random.choice(list(self.agents)) - def select(self, filter_func: Optional[Callable[[T], bool]] = None, n=0): + def select(self, filter_func: Callable[[T], bool] | None = None, n=0): # FIXME: n is not considered if filter_func is None and n == 0: return self diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 068e9c2595f..f7c049daab1 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -24,9 +24,9 @@ class DiscreteSpace(Generic[T]): def __init__( self, - capacity: Optional[int] = None, + capacity: int | None = None, cell_klass: type[T] = Cell, - random: Optional[Random] = None, + random: Random | None = None, ): super().__init__() self.capacity = capacity @@ -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): From 065e5eae96cc85bd8623655a057ecf845fe88a61 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 19 Feb 2024 19:39:23 +0100 Subject: [PATCH 47/63] fixes to tests --- mesa/experimental/cell_space/cell_agent.py | 4 ++-- mesa/experimental/cell_space/cell_collection.py | 4 ++-- tests/test_cell_space.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index f78f03294af..7583e620f7f 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -1,9 +1,9 @@ from typing import Generic, TypeVar from mesa import Agent, Model -from mesa.experimental.cell_space.cell import Cell +# from mesa.experimental.cell_space.cell import Cell -T = TypeVar("T", bound=Cell) +T = TypeVar("T", bound="Cell") class CellAgent(Agent, Generic[T]): diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index c18abf65f56..1f72d90a370 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -6,10 +6,10 @@ from random import Random from typing import Callable, Generic, Optional, TypeVar -from mesa.experimental.cell_space.cell import Cell +# from mesa.experimental.cell_space.cell import Cell from mesa.experimental.cell_space.cell_agent import CellAgent -T = TypeVar("T", bound=Cell) +T = TypeVar("T", bound="Cell") class CellCollection(Generic[T]): diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 40c0961e2e1..050e39a09a6 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -385,7 +385,7 @@ def test_empties_space(): model = Model() for i in range(8): - grid._cells[(i,)].add_agent(CellAgent(i, model)) + grid._cells[i].add_agent(CellAgent(i, model)) cell = grid.select_random_empty_cell() assert cell.coordinate in {8, 9} From 1bfa009484dc0973c5b24092e3e3d0c0efdb09c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:39:32 +0000 Subject: [PATCH 48/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/cell.py | 2 +- mesa/experimental/cell_space/cell_agent.py | 1 + mesa/experimental/cell_space/cell_collection.py | 2 +- mesa/experimental/cell_space/discrete_space.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 3d368555d07..843fd293543 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -2,7 +2,7 @@ from functools import cache from random import Random -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from mesa.experimental.cell_space.cell_collection import CellCollection diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index 7583e620f7f..56565cd04cb 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -1,6 +1,7 @@ from typing import Generic, TypeVar from mesa import Agent, Model + # from mesa.experimental.cell_space.cell import Cell T = TypeVar("T", bound="Cell") diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 1f72d90a370..2865686f732 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -4,7 +4,7 @@ from collections.abc import Iterable, Mapping from functools import cached_property from random import Random -from typing import Callable, Generic, Optional, TypeVar +from typing import Callable, Generic, TypeVar # from mesa.experimental.cell_space.cell import Cell from mesa.experimental.cell_space.cell_agent import CellAgent diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index f7c049daab1..d2161c5b46a 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -2,7 +2,7 @@ from functools import cached_property from random import Random -from typing import Generic, Optional, TypeVar +from typing import Generic, TypeVar from mesa.experimental.cell_space.cell import Cell from mesa.experimental.cell_space.cell_collection import CellCollection From 6b1c454993602c7883fbfd2d8e7ec1510ee0a7bf Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 19 Feb 2024 19:44:36 +0100 Subject: [PATCH 49/63] type hint 3.9 fix in network.py --- mesa/experimental/cell_space/network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mesa/experimental/cell_space/network.py b/mesa/experimental/cell_space/network.py index 3983287e4ef..57c4d492bb0 100644 --- a/mesa/experimental/cell_space/network.py +++ b/mesa/experimental/cell_space/network.py @@ -1,5 +1,5 @@ from random import Random -from typing import Any +from typing import Any, Optional from mesa.experimental.cell_space.cell import Cell from mesa.experimental.cell_space.discrete_space import DiscreteSpace @@ -11,8 +11,8 @@ class Network(DiscreteSpace): def __init__( self, G: Any, # noqa: N803 - capacity: int | None = None, - random: Random | None = None, + capacity: Optional[int] = None, + random: Optional[Random] = None, cell_klass: type[Cell] = Cell, ) -> None: """A Networked grid From cf789f21abd3ceef7f320541691b86dd48a5abaa Mon Sep 17 00:00:00 2001 From: Corvince Date: Mon, 19 Feb 2024 21:41:39 +0100 Subject: [PATCH 50/63] fix type checking --- mesa/experimental/cell_space/cell_agent.py | 5 +++-- mesa/experimental/cell_space/cell_collection.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index 56565cd04cb..8d076fd78f2 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -1,8 +1,9 @@ -from typing import Generic, TypeVar +from typing import TYPE_CHECKING, Generic, TypeVar from mesa import Agent, Model -# from mesa.experimental.cell_space.cell import Cell +if TYPE_CHECKING: + from mesa.experimental.cell_space.cell import Cell T = TypeVar("T", bound="Cell") diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 2865686f732..c52a3fd9a2e 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -4,10 +4,11 @@ from collections.abc import Iterable, Mapping from functools import cached_property from random import Random -from typing import Callable, Generic, TypeVar +from typing import TYPE_CHECKING, Callable, Generic, TypeVar -# from mesa.experimental.cell_space.cell import Cell -from mesa.experimental.cell_space.cell_agent import CellAgent +if TYPE_CHECKING: + from mesa.experimental.cell_space.cell import Cell + from mesa.experimental.cell_space.cell_agent import CellAgent T = TypeVar("T", bound="Cell") From 50666c1833888ba7e15109b9052617fffdfd0a93 Mon Sep 17 00:00:00 2001 From: Corvince Date: Mon, 19 Feb 2024 21:59:59 +0100 Subject: [PATCH 51/63] update capacity, add annotations import --- mesa/experimental/cell_space/cell.py | 2 +- mesa/experimental/cell_space/cell_agent.py | 2 ++ mesa/experimental/cell_space/grid.py | 12 +++++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 843fd293543..2d3f6fa156b 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -34,7 +34,7 @@ class Cell: def __init__( self, coordinate: tuple[int, ...], - capacity: int | None = None, + capacity: float | None = None, random: Random | None = None, ) -> None: """ " diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index 8d076fd78f2..08aa0930888 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Generic, TypeVar from mesa import Agent, Model diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 4e8aad5dfa6..6e2c2b5eb10 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from collections.abc import Sequence from itertools import product from random import Random -from typing import Generic, Optional, TypeVar +from typing import Generic, TypeVar from mesa.experimental.cell_space import Cell, DiscreteSpace @@ -23,8 +25,8 @@ def __init__( self, dimensions: Sequence[int], torus: bool = False, - capacity: Optional[int] = None, - random: Optional[Random] = None, + capacity: float | None = None, + random: Random | None = None, cell_klass: type[T] = Cell, ) -> None: super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) @@ -49,8 +51,8 @@ def _validate_parameters(self): raise ValueError("Dimensions must be a list of positive integers.") if not isinstance(self.torus, bool): raise ValueError("Torus must be a boolean.") - if self.capacity is not None and not isinstance(self.capacity, int): - raise ValueError("Capacity must be an integer or None.") + if self.capacity is not None and not isinstance(self.capacity, (float, int)): + raise ValueError("Capacity must be a number or None.") def _calculate_neighborhood_offsets(self, cell: T) -> list[tuple[int, ...]]: # Default implementation From e520d3c22297f7d90051b6cf031067fc49e1f773 Mon Sep 17 00:00:00 2001 From: Corvince Date: Tue, 20 Feb 2024 08:18:25 +0100 Subject: [PATCH 52/63] fix wolf_sheep --- benchmarks/WolfSheep/wolf_sheep.py | 7 +++---- mesa/experimental/cell_space/cell_agent.py | 9 ++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index af05d43e254..19774b8627b 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -38,8 +38,7 @@ def spawn_offspring(self): offspring.move_to(self.cell) self.model.schedule.add(offspring) - def feed(self): - ... + def feed(self): ... def die(self): self.cell.remove_agent(self) @@ -178,7 +177,7 @@ def __init__( sheep = Sheep( self.next_id(), self, energy, sheep_reproduce, sheep_gain_from_food ) - sheep.move_to(self.grid.cells[pos]) + sheep.move_to(self.grid[pos]) self.schedule.add(sheep) # Create wolves @@ -191,7 +190,7 @@ def __init__( wolf = Wolf( self.next_id(), self, energy, wolf_reproduce, wolf_gain_from_food ) - wolf.move_to(self.grid.cells[pos]) + wolf.move_to(self.grid[pos]) self.schedule.add(wolf) # Create grass patches diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index 08aa0930888..96dbdcd9761 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -1,16 +1,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING from mesa import Agent, Model if TYPE_CHECKING: from mesa.experimental.cell_space.cell import Cell -T = TypeVar("T", bound="Cell") - -class CellAgent(Agent, Generic[T]): +class CellAgent(Agent): """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces @@ -21,6 +19,8 @@ class CellAgent(Agent, Generic[T]): cell: (Cell | None): the cell which the agent occupies """ + cell: Cell + def __init__(self, unique_id: int, model: Model) -> None: """ Create a new agent. @@ -30,7 +30,6 @@ def __init__(self, unique_id: int, model: Model) -> None: model (Model): The model instance in which the agent exists. """ super().__init__(unique_id, model) - self.cell: T | None = None def move_to(self, cell) -> None: if self.cell is not None: From 85251f08135ff4ca79befc09ab91f62639861ce9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 07:19:42 +0000 Subject: [PATCH 53/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/WolfSheep/wolf_sheep.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index 19774b8627b..ad5bc6edc2b 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -38,7 +38,8 @@ def spawn_offspring(self): offspring.move_to(self.cell) self.model.schedule.add(offspring) - def feed(self): ... + def feed(self): + ... def die(self): self.cell.remove_agent(self) From 3a247cbaee53f7a86856af8252a0d8464eb491f0 Mon Sep 17 00:00:00 2001 From: Corvince Date: Tue, 20 Feb 2024 08:18:25 +0100 Subject: [PATCH 54/63] fix wolf_sheep --- benchmarks/WolfSheep/wolf_sheep.py | 4 ++-- mesa/experimental/cell_space/cell_agent.py | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index af05d43e254..ad5bc6edc2b 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -178,7 +178,7 @@ def __init__( sheep = Sheep( self.next_id(), self, energy, sheep_reproduce, sheep_gain_from_food ) - sheep.move_to(self.grid.cells[pos]) + sheep.move_to(self.grid[pos]) self.schedule.add(sheep) # Create wolves @@ -191,7 +191,7 @@ def __init__( wolf = Wolf( self.next_id(), self, energy, wolf_reproduce, wolf_gain_from_food ) - wolf.move_to(self.grid.cells[pos]) + wolf.move_to(self.grid[pos]) self.schedule.add(wolf) # Create grass patches diff --git a/mesa/experimental/cell_space/cell_agent.py b/mesa/experimental/cell_space/cell_agent.py index 08aa0930888..abc5155a670 100644 --- a/mesa/experimental/cell_space/cell_agent.py +++ b/mesa/experimental/cell_space/cell_agent.py @@ -1,16 +1,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import TYPE_CHECKING from mesa import Agent, Model if TYPE_CHECKING: from mesa.experimental.cell_space.cell import Cell -T = TypeVar("T", bound="Cell") - -class CellAgent(Agent, Generic[T]): +class CellAgent(Agent): """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces @@ -30,7 +28,7 @@ def __init__(self, unique_id: int, model: Model) -> None: model (Model): The model instance in which the agent exists. """ super().__init__(unique_id, model) - self.cell: T | None = None + self.cell: Cell | None = None def move_to(self, cell) -> None: if self.cell is not None: From fb19879c37d62c8eec44aed8a5130aca54be7e55 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 21 Feb 2024 19:29:30 +0100 Subject: [PATCH 55/63] seperate code path for 2d and nd grids when connecting cells --- benchmarks/Schelling/schelling.py | 12 +- mesa/experimental/cell_space/cell.py | 4 +- mesa/experimental/cell_space/grid.py | 157 +++++++++++++++++---------- 3 files changed, 107 insertions(+), 66 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 2d88474ac62..79eeb4e0cfa 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -8,7 +8,7 @@ class SchellingAgent(CellAgent): Schelling segregation agent """ - def __init__(self, unique_id, model, agent_type, radius): + def __init__(self, unique_id, model, agent_type, radius, homophily): """ Create a new Schelling agent. Args: @@ -19,6 +19,7 @@ def __init__(self, unique_id, model, agent_type, radius): super().__init__(unique_id, model) self.type = agent_type self.radius = radius + self.homophily = homophily def step(self): similar = 0 @@ -27,7 +28,7 @@ def step(self): similar += 1 # If unhappy, move: - if similar < self.model.homophily: + if similar < self.homophily: self.move_to(self.model.grid.select_random_empty_cell()) else: self.model.happy += 1 @@ -47,7 +48,6 @@ def __init__( self.width = width self.density = density self.minority_pc = minority_pc - self.homophily = homophily self.schedule = RandomActivation(self) self.grid = OrthogonalMooreGrid( @@ -63,7 +63,7 @@ def __init__( for cell in self.grid: if self.random.random() < self.density: agent_type = 1 if self.random.random() < self.minority_pc else 0 - agent = SchellingAgent(self.next_id(), self, agent_type, radius) + agent = SchellingAgent(self.next_id(), self, agent_type, radius, homophily) agent.move_to(cell) self.schedule.add(agent) @@ -78,11 +78,11 @@ def step(self): if __name__ == "__main__": import time - # model = Schelling(15, 40, 40, 3, 1, 0.625) + # model = Schelling(40, 40, 3, 1, 0.625, seed=15) model = Schelling(100, 100, 8, 2, 0.8, seed=15) start_time = time.perf_counter() - for _ in range(100): + for _ in range(20): model.step() print(time.perf_counter() - start_time) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 2d3f6fa156b..949bbe5c0f4 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -53,7 +53,7 @@ def __init__( self.properties: dict[str, object] = {} self.random = random - def connect(self, other) -> None: + def connect(self, other: "Cell") -> None: """Connects this cell to another cell. Args: @@ -62,7 +62,7 @@ def connect(self, other) -> None: """ self._connections.append(other) - def disconnect(self, other) -> None: + def disconnect(self, other: "Cell") -> None: """Disconnects this cell from another cell. Args: diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 6e2c2b5eb10..c9032d466d1 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -14,37 +14,49 @@ class Grid(DiscreteSpace, Generic[T]): """Base class for all grid classes Attributes: - width (int): width of the grid - height (int): height of the grid + dimensions (Sequence[int]): the dimensions of the grid torus (bool): whether the grid is a torus + capacity (int): the capacity of a grid cell + random (Random): the random number generator _try_random (bool): whether to get empty cell be repeatedly trying random cell """ def __init__( - self, - dimensions: Sequence[int], - torus: bool = False, - capacity: float | None = None, - random: Random | None = None, - cell_klass: type[T] = Cell, + self, + dimensions: Sequence[int], + torus: bool = False, + capacity: float | None = None, + random: Random | None = None, + cell_klass: type[T] = Cell, ) -> None: super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) self.torus = torus self.dimensions = dimensions self._try_random = True - + self._ndims = len(dimensions) self._validate_parameters() + coordinates = product(*(range(dim) for dim in self.dimensions)) self._cells = { coord: cell_klass(coord, capacity, random=self.random) for coord in coordinates } + self._connect_cells() - for cell in self.all_cells: - self._connect_single_cell(cell) + def _connect_cells(self) -> None: + if self._ndims == 2: + self._connect_cells_2d() + else: + self._connect_cells_nd() + + def _connect_cells_2d(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): @@ -54,9 +66,6 @@ def _validate_parameters(self): if self.capacity is not None and not isinstance(self.capacity, (float, int)): raise ValueError("Capacity must be a number or None.") - def _calculate_neighborhood_offsets(self, cell: T) -> list[tuple[int, ...]]: - # Default implementation - return [] def select_random_empty_cell(self) -> T: # FIXME:: currently just a simple boolean to control behavior @@ -76,16 +85,27 @@ def select_random_empty_cell(self) -> T: else: return super().select_random_empty_cell() - def _connect_single_cell(self, cell: T) -> None: + def _connect_single_cell_nd(self, cell: T, offsets: list[tuple[int, ...]]) -> None: coord = cell.coordinate - for d_coord in self._calculate_neighborhood_offsets(cell): + for d_coord in offsets: n_coord = tuple(c + dc for c, dc in zip(coord, d_coord)) if self.torus: n_coord = tuple(nc % d for nc, d in zip(n_coord, self.dimensions)) if all(0 <= nc < d for nc, d in zip(n_coord, self.dimensions)): cell.connect(self._cells[n_coord]) + def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> None: + i, j = cell.coordinate + height, width = self.dimensions + + for di, dj in offsets: + ni, nj = (i + di, j + dj) + if self.torus: + ni, nj = ni % height, nj % width + if 0 <= ni < height and 0 <= nj < width: + cell.connect(self._cells[ni, nj]) + class OrthogonalMooreGrid(Grid[T]): """Grid where cells are connected to their 8 neighbors. @@ -98,16 +118,25 @@ class OrthogonalMooreGrid(Grid[T]): ] """ - def _calculate_neighborhood_offsets(self, cell: T) -> list[tuple[int, ...]]: - """ - Calculates the offsets for a Moore neighborhood in an n-dimensional grid. - This neighborhood includes all cells that are one step away in any dimension. - """ + def _connect_cells_2d(self) ->None: + # fmt: off + offsets = [ + (-1, -1), (-1, 0), (-1, 1), + ( 0, -1), ( 0, 1), + ( 1, -1), ( 1, 0), ( 1, 1), + ] + # fmt: on + height, width = self.dimensions + + for cell in self.all_cells: + self._connect_single_cell_2d(cell, offsets) + def _connect_cells_nd(self) -> None: offsets = list(product([-1, 0, 1], repeat=len(self.dimensions))) offsets.remove((0,) * len(self.dimensions)) # Remove the central cell - return offsets + for cell in self.all_cells: + self._connect_single_cell_nd(cell, offsets) class OrthogonalVonNeumannGrid(Grid[T]): """Grid where cells are connected to their 4 neighbors. @@ -120,50 +149,62 @@ class OrthogonalVonNeumannGrid(Grid[T]): ] """ - def _calculate_neighborhood_offsets(self, cell: T) -> list[tuple[int, ...]]: - """ - Calculates the offsets for a Von Neumann neighborhood in an n-dimensional grid. - This neighborhood includes all cells that are one step away in any single dimension. + def _connect_cells_2d(self) -> None: + # fmt: off + offsets = [ + (-1, 0), + (0, -1), (0, 1), + ( 1, 0), + ] + # fmt: on + height, width = self.dimensions + + for cell in self.all_cells: + self._connect_single_cell_2d(cell, offsets) - Returns: - A list of tuples representing the relative positions of neighboring cells. - """ + def _connect_cells_nd(self) -> None: offsets = [] dimensions = len(self.dimensions) for dim in range(dimensions): - for delta in [ - -1, - 1, - ]: # Move one step in each direction for the current dimension - offset = [0] * dimensions - offset[dim] = delta - offsets.append(tuple(offset)) - return offsets + for delta in [ + -1, + 1, + ]: # Move one step in each direction for the current dimension + offset = [0] * dimensions + offset[dim] = delta + offsets.append(tuple(offset)) + for cell in self.all_cells: + self._connect_single_cell_nd(cell, offsets) class HexGrid(Grid[T]): - def _validate_parameters(self): - super()._validate_parameters() - if len(self.dimensions) != 2: - raise ValueError("HexGrid must have exactly 2 dimensions.") - - @staticmethod - def _calculate_neighborhood_offsets(cell: T) -> list[tuple[int, int]]: - i, j = cell.coordinate + def _connect_cells_2d(self) -> None: # fmt: off - if i % 2 == 0: - offsets = [ - (-1, -1), (-1, 0), - ( 0, -1), ( 0, 1), - ( 1, -1), ( 1, 0), - ] - else: - offsets = [ - (-1, 0), (-1, 1), - ( 0, -1), ( 0, 1), - ( 1, 0), ( 1, 1), - ] + even_offsets = [ + (-1, -1), (-1, 0), + ( 0, -1), ( 0, 1), + ( 1, -1), ( 1, 0), + ] + odd_offsets = [ + (-1, 0), (-1, 1), + ( 0, -1), ( 0, 1), + ( 1, 0), ( 1, 1), + ] # fmt: on - return offsets + for cell in self.all_cells: + i = cell.coordinate[0] + + if i % 2 == 0: + offsets = even_offsets + else: + offsets = odd_offsets + + self._connect_single_cell_2d(cell, offsets=offsets) + def _connect_cells_nd(self) -> None: + raise NotImplementedError("HexGrids are only defined for 2 dimensions") + def _validate_parameters(self): + super()._validate_parameters() + if len(self.dimensions) != 2: + raise ValueError("HexGrid must have exactly 2 dimensions.") From 5ea5d3519f34c7f5c2a04da7ef8f396e74da0d3d Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 21 Feb 2024 19:29:39 +0100 Subject: [PATCH 56/63] seed as kwarg --- benchmarks/WolfSheep/wolf_sheep.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index ad5bc6edc2b..eb8178635cd 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -213,7 +213,8 @@ def step(self): if __name__ == "__main__": import time - model = WolfSheep(25, 25, 60, 40, 0.2, 0.1, 20, seed=15) + model = WolfSheep(25, 25, 60, 40, 0.2, + 0.1, 20, seed=15) start_time = time.perf_counter() for _ in range(100): From b7ac86cb6bcd5d34ba727cebef5ccfad12db4884 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 18:29:51 +0000 Subject: [PATCH 57/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/Schelling/schelling.py | 4 +++- benchmarks/WolfSheep/wolf_sheep.py | 3 +-- mesa/experimental/cell_space/cell.py | 4 ++-- mesa/experimental/cell_space/grid.py | 35 ++++++++++++++-------------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 79eeb4e0cfa..1ef4502c028 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -63,7 +63,9 @@ def __init__( for cell in self.grid: if self.random.random() < self.density: agent_type = 1 if self.random.random() < self.minority_pc else 0 - agent = SchellingAgent(self.next_id(), self, agent_type, radius, homophily) + agent = SchellingAgent( + self.next_id(), self, agent_type, radius, homophily + ) agent.move_to(cell) self.schedule.add(agent) diff --git a/benchmarks/WolfSheep/wolf_sheep.py b/benchmarks/WolfSheep/wolf_sheep.py index eb8178635cd..ad5bc6edc2b 100644 --- a/benchmarks/WolfSheep/wolf_sheep.py +++ b/benchmarks/WolfSheep/wolf_sheep.py @@ -213,8 +213,7 @@ def step(self): if __name__ == "__main__": import time - model = WolfSheep(25, 25, 60, 40, 0.2, - 0.1, 20, seed=15) + model = WolfSheep(25, 25, 60, 40, 0.2, 0.1, 20, seed=15) start_time = time.perf_counter() for _ in range(100): diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 949bbe5c0f4..9e04cd7ad14 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -53,7 +53,7 @@ def __init__( self.properties: dict[str, object] = {} self.random = random - def connect(self, other: "Cell") -> None: + def connect(self, other: Cell) -> None: """Connects this cell to another cell. Args: @@ -62,7 +62,7 @@ def connect(self, other: "Cell") -> None: """ self._connections.append(other) - def disconnect(self, other: "Cell") -> None: + def disconnect(self, other: Cell) -> None: """Disconnects this cell from another cell. Args: diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index c9032d466d1..a0d9d49df97 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -23,12 +23,12 @@ class Grid(DiscreteSpace, Generic[T]): """ def __init__( - self, - dimensions: Sequence[int], - torus: bool = False, - capacity: float | None = None, - random: Random | None = None, - cell_klass: type[T] = Cell, + self, + dimensions: Sequence[int], + torus: bool = False, + capacity: float | None = None, + random: Random | None = None, + cell_klass: type[T] = Cell, ) -> None: super().__init__(capacity=capacity, random=random, cell_klass=cell_klass) self.torus = torus @@ -37,7 +37,6 @@ def __init__( self._ndims = len(dimensions) self._validate_parameters() - coordinates = product(*(range(dim) for dim in self.dimensions)) self._cells = { @@ -66,7 +65,6 @@ def _validate_parameters(self): if self.capacity is not None and not isinstance(self.capacity, (float, int)): raise ValueError("Capacity must be a number or None.") - def select_random_empty_cell(self) -> T: # FIXME:: currently just a simple boolean to control behavior # FIXME:: basically if grid is close to 99% full, creating empty list can be faster @@ -118,7 +116,7 @@ class OrthogonalMooreGrid(Grid[T]): ] """ - def _connect_cells_2d(self) ->None: + def _connect_cells_2d(self) -> None: # fmt: off offsets = [ (-1, -1), (-1, 0), (-1, 1), @@ -138,6 +136,7 @@ def _connect_cells_nd(self) -> None: for cell in self.all_cells: self._connect_single_cell_nd(cell, offsets) + class OrthogonalVonNeumannGrid(Grid[T]): """Grid where cells are connected to their 4 neighbors. @@ -166,19 +165,19 @@ def _connect_cells_nd(self) -> None: offsets = [] dimensions = len(self.dimensions) for dim in range(dimensions): - for delta in [ - -1, - 1, - ]: # Move one step in each direction for the current dimension - offset = [0] * dimensions - offset[dim] = delta - offsets.append(tuple(offset)) + for delta in [ + -1, + 1, + ]: # Move one step in each direction for the current dimension + offset = [0] * dimensions + offset[dim] = delta + offsets.append(tuple(offset)) for cell in self.all_cells: self._connect_single_cell_nd(cell, offsets) -class HexGrid(Grid[T]): +class HexGrid(Grid[T]): def _connect_cells_2d(self) -> None: # fmt: off even_offsets = [ @@ -202,8 +201,10 @@ def _connect_cells_2d(self) -> None: offsets = odd_offsets self._connect_single_cell_2d(cell, offsets=offsets) + def _connect_cells_nd(self) -> None: raise NotImplementedError("HexGrids are only defined for 2 dimensions") + def _validate_parameters(self): super()._validate_parameters() if len(self.dimensions) != 2: From 76ca4a55243b5bc23024294670021a033afbbd04 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Wed, 21 Feb 2024 19:40:02 +0100 Subject: [PATCH 58/63] code formatting fix --- mesa/experimental/cell_space/grid.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index a0d9d49df97..cc4b4b9e489 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -194,12 +194,7 @@ def _connect_cells_2d(self) -> None: for cell in self.all_cells: i = cell.coordinate[0] - - if i % 2 == 0: - offsets = even_offsets - else: - offsets = odd_offsets - + offsets = even_offsets if i % 2 == 0 else odd_offsets self._connect_single_cell_2d(cell, offsets=offsets) def _connect_cells_nd(self) -> None: From 45e3e9ff2313a6edf47be1030e6d882012410c8b Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Thu, 22 Feb 2024 07:35:11 +0100 Subject: [PATCH 59/63] testing --- benchmarks/Schelling/schelling.py | 13 +++-- mesa/experimental/cell_space/__init__.py | 3 +- mesa/experimental/cell_space/cell.py | 55 +++++++++++++++++++ .../cell_space/cell_collection.py | 9 ++- 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 1ef4502c028..bc98286fedd 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,8 +1,10 @@ from mesa import Model -from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid +from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid, SingleAgentCell from mesa.time import RandomActivation +from line_profiler_pycharm import profile + class SchellingAgent(CellAgent): """ Schelling segregation agent @@ -21,9 +23,11 @@ def __init__(self, unique_id, model, agent_type, radius, homophily): self.radius = radius self.homophily = homophily + @profile def step(self): similar = 0 - for neighbor in self.cell.neighborhood(radius=self.radius).agents: + neighborhood = self.cell.neighborhood(radius=self.radius) + for neighbor in neighborhood.agents: if neighbor.type == self.type: similar += 1 @@ -51,7 +55,7 @@ def __init__( self.schedule = RandomActivation(self) self.grid = OrthogonalMooreGrid( - [height, width], torus=True, capacity=1, random=self.random + [height, width], torus=True, capacity=1, random=self.random, cell_klass=SingleAgentCell ) self.happy = 0 @@ -84,7 +88,6 @@ def step(self): model = Schelling(100, 100, 8, 2, 0.8, seed=15) start_time = time.perf_counter() - for _ in range(20): + for _ in range(100): model.step() - print(time.perf_counter() - start_time) diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py index dce296aebce..bd5352bd0ae 100644 --- a/mesa/experimental/cell_space/__init__.py +++ b/mesa/experimental/cell_space/__init__.py @@ -1,4 +1,4 @@ -from mesa.experimental.cell_space.cell import Cell +from mesa.experimental.cell_space.cell import Cell, SingleAgentCell from mesa.experimental.cell_space.cell_agent import CellAgent from mesa.experimental.cell_space.cell_collection import CellCollection from mesa.experimental.cell_space.discrete_space import DiscreteSpace @@ -20,4 +20,5 @@ "OrthogonalMooreGrid", "OrthogonalVonNeumannGrid", "Network", + "SingleAgentCell" ] diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 9e04cd7ad14..356b7f29755 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -31,6 +31,15 @@ class Cell: "random", ] + # def __new__(cls, + # coordinate: tuple[int, ...], + # capacity: float | None = None, + # random: Random | None = None,): + # if capacity != 1: + # return object.__new__(cls) + # else: + # return object.__new__(SingleAgentCell) + def __init__( self, coordinate: tuple[int, ...], @@ -141,3 +150,49 @@ def _neighborhood(self, radius=1, include_center=False): if not include_center: neighborhood.pop(self, None) return neighborhood + + +class SingleAgentCell(Cell): + def __init__( + self, + coordinate: tuple[int, ...], + capacity: float | None = None, + random: Random | None = None, + ) -> None: + super().__init__(coordinate, capacity, random) + self.agents = None + + def add_agent(self, agent: CellAgent) -> None: + """Adds an agent to the cell. + + Args: + agent (CellAgent): agent to add to this Cell + + """ + + if self.agents is not None: + raise Exception( + "ERROR: Cell is full" + ) # FIXME we need MESA errors or a proper error + + self.agents = agent + + def remove_agent(self, agent: CellAgent) -> None: + """Removes an agent from the cell. + + Args: + agent (CellAgent): agent to remove from this cell + + """ + self.agents = None + agent.cell = None + + @property + def is_empty(self) -> bool: + """Returns a bool of the contents of a cell.""" + return self.agents is None + + @property + def is_full(self) -> bool: + """Returns a bool of the contents of a cell.""" + return self.agents is not None \ No newline at end of file diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index c52a3fd9a2e..440ce694740 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -28,11 +28,15 @@ def __init__( cells: Mapping[T, list[CellAgent]] | Iterable[T], random: Random | None = None, ) -> None: + if isinstance(cells, dict): self._cells = cells else: self._cells = {cell: cell.agents for cell in cells} + # + self._capacity: int = next(iter(self._cells.keys())).capacity + if random is None: random = Random() # FIXME self.random = random @@ -56,7 +60,10 @@ def cells(self) -> list[T]: @property def agents(self) -> Iterable[CellAgent]: - return itertools.chain.from_iterable(self._cells.values()) + if self._capacity == 1: + return (entry for entry in self._cells.values() if entry is not None) + else: + return itertools.chain.from_iterable(self._cells.values()) def select_random_cell(self) -> T: return self.random.choice(self.cells) From 3d8cdfd87bbc17bbb89015a18925ddc906f12211 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 22 Feb 2024 06:35:19 +0000 Subject: [PATCH 60/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/Schelling/schelling.py | 10 +++++++--- mesa/experimental/cell_space/__init__.py | 2 +- mesa/experimental/cell_space/cell.py | 2 +- mesa/experimental/cell_space/cell_collection.py | 1 - 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index bc98286fedd..20ba5226c14 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,10 +1,10 @@ +from line_profiler_pycharm import profile + from mesa import Model from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid, SingleAgentCell from mesa.time import RandomActivation -from line_profiler_pycharm import profile - class SchellingAgent(CellAgent): """ Schelling segregation agent @@ -55,7 +55,11 @@ def __init__( self.schedule = RandomActivation(self) self.grid = OrthogonalMooreGrid( - [height, width], torus=True, capacity=1, random=self.random, cell_klass=SingleAgentCell + [height, width], + torus=True, + capacity=1, + random=self.random, + cell_klass=SingleAgentCell, ) self.happy = 0 diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py index bd5352bd0ae..48a98c83b47 100644 --- a/mesa/experimental/cell_space/__init__.py +++ b/mesa/experimental/cell_space/__init__.py @@ -20,5 +20,5 @@ "OrthogonalMooreGrid", "OrthogonalVonNeumannGrid", "Network", - "SingleAgentCell" + "SingleAgentCell", ] diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 356b7f29755..7a4bad34f7c 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -195,4 +195,4 @@ def is_empty(self) -> bool: @property def is_full(self) -> bool: """Returns a bool of the contents of a cell.""" - return self.agents is not None \ No newline at end of file + return self.agents is not None diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 440ce694740..0315aaf2edb 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -28,7 +28,6 @@ def __init__( cells: Mapping[T, list[CellAgent]] | Iterable[T], random: Random | None = None, ) -> None: - if isinstance(cells, dict): self._cells = cells else: From 62c844e13b8fd3be2612789e84395f87c2f4b33d Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Fri, 23 Feb 2024 19:49:43 +0100 Subject: [PATCH 61/63] minor docstring update --- benchmarks/Schelling/schelling.py | 4 +- mesa/experimental/cell_space/__init__.py | 3 +- mesa/experimental/cell_space/cell.py | 46 ------------------- .../cell_space/cell_collection.py | 5 +- 4 files changed, 3 insertions(+), 55 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 20ba5226c14..78791feadfd 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,7 +1,7 @@ from line_profiler_pycharm import profile from mesa import Model -from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid, SingleAgentCell +from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid from mesa.time import RandomActivation @@ -23,7 +23,6 @@ def __init__(self, unique_id, model, agent_type, radius, homophily): self.radius = radius self.homophily = homophily - @profile def step(self): similar = 0 neighborhood = self.cell.neighborhood(radius=self.radius) @@ -59,7 +58,6 @@ def __init__( torus=True, capacity=1, random=self.random, - cell_klass=SingleAgentCell, ) self.happy = 0 diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py index 48a98c83b47..dce296aebce 100644 --- a/mesa/experimental/cell_space/__init__.py +++ b/mesa/experimental/cell_space/__init__.py @@ -1,4 +1,4 @@ -from mesa.experimental.cell_space.cell import Cell, SingleAgentCell +from mesa.experimental.cell_space.cell import Cell from mesa.experimental.cell_space.cell_agent import CellAgent from mesa.experimental.cell_space.cell_collection import CellCollection from mesa.experimental.cell_space.discrete_space import DiscreteSpace @@ -20,5 +20,4 @@ "OrthogonalMooreGrid", "OrthogonalVonNeumannGrid", "Network", - "SingleAgentCell", ] diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 7a4bad34f7c..55264f68daa 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -150,49 +150,3 @@ def _neighborhood(self, radius=1, include_center=False): if not include_center: neighborhood.pop(self, None) return neighborhood - - -class SingleAgentCell(Cell): - def __init__( - self, - coordinate: tuple[int, ...], - capacity: float | None = None, - random: Random | None = None, - ) -> None: - super().__init__(coordinate, capacity, random) - self.agents = None - - def add_agent(self, agent: CellAgent) -> None: - """Adds an agent to the cell. - - Args: - agent (CellAgent): agent to add to this Cell - - """ - - if self.agents is not None: - raise Exception( - "ERROR: Cell is full" - ) # FIXME we need MESA errors or a proper error - - self.agents = agent - - def remove_agent(self, agent: CellAgent) -> None: - """Removes an agent from the cell. - - Args: - agent (CellAgent): agent to remove from this cell - - """ - self.agents = None - agent.cell = None - - @property - def is_empty(self) -> bool: - """Returns a bool of the contents of a cell.""" - return self.agents is None - - @property - def is_full(self) -> bool: - """Returns a bool of the contents of a cell.""" - return self.agents is not None diff --git a/mesa/experimental/cell_space/cell_collection.py b/mesa/experimental/cell_space/cell_collection.py index 0315aaf2edb..9ca36589849 100644 --- a/mesa/experimental/cell_space/cell_collection.py +++ b/mesa/experimental/cell_space/cell_collection.py @@ -59,10 +59,7 @@ def cells(self) -> list[T]: @property def agents(self) -> Iterable[CellAgent]: - if self._capacity == 1: - return (entry for entry in self._cells.values() if entry is not None) - else: - return itertools.chain.from_iterable(self._cells.values()) + return itertools.chain.from_iterable(self._cells.values()) def select_random_cell(self) -> T: return self.random.choice(self.cells) From adff7aab8fe9156482a2060d7ceb04d77f8a94ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 18:50:24 +0000 Subject: [PATCH 62/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/Schelling/schelling.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index 78791feadfd..47a84646057 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -1,5 +1,3 @@ -from line_profiler_pycharm import profile - from mesa import Model from mesa.experimental.cell_space import CellAgent, OrthogonalMooreGrid from mesa.time import RandomActivation From b32af9f4a7d45efb7d4e55af00c09b21158a3711 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 12:29:40 +0000 Subject: [PATCH 63/63] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/Schelling/schelling.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/benchmarks/Schelling/schelling.py b/benchmarks/Schelling/schelling.py index ed6dbcb5ee0..c7dd3bf1deb 100644 --- a/benchmarks/Schelling/schelling.py +++ b/benchmarks/Schelling/schelling.py @@ -102,7 +102,9 @@ def step(self): import time # model = Schelling(seed=15, height=40, width=40, homophily=3, radius=1, density=0.625) - model = Schelling(seed=15, height=100, width=100, homophily=8, radius=2, density=0.8) + model = Schelling( + seed=15, height=100, width=100, homophily=8, radius=2, density=0.8 + ) start_time = time.perf_counter() for _ in range(100):