Skip to content

Commit 567339f

Browse files
authored
Add full support for property layers to cell spaces (#2512)
1 parent adad7a2 commit 567339f

File tree

8 files changed

+740
-161
lines changed

8 files changed

+740
-161
lines changed

mesa/experimental/cell_space/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
OrthogonalVonNeumannGrid,
2121
)
2222
from mesa.experimental.cell_space.network import Network
23+
from mesa.experimental.cell_space.property_layer import PropertyLayer
2324
from mesa.experimental.cell_space.voronoi import VoronoiGrid
2425

2526
__all__ = [
@@ -34,5 +35,6 @@
3435
"Network",
3536
"OrthogonalMooreGrid",
3637
"OrthogonalVonNeumannGrid",
38+
"PropertyLayer",
3739
"VoronoiGrid",
3840
]

mesa/experimental/cell_space/cell.py

+6-33
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
from __future__ import annotations
44

5-
from collections.abc import Callable
65
from functools import cache, cached_property
76
from random import Random
8-
from typing import TYPE_CHECKING, Any
7+
from typing import TYPE_CHECKING
98

109
from mesa.experimental.cell_space.cell_agent import CellAgent
1110
from mesa.experimental.cell_space.cell_collection import CellCollection
12-
from mesa.space import PropertyLayer
1311

1412
if TYPE_CHECKING:
1513
from mesa.agent import Agent
@@ -24,14 +22,12 @@ class Cell:
2422
coordinate (Tuple[int, int]) : the position of the cell in the discrete space
2523
agents (List[Agent]): the agents occupying the cell
2624
capacity (int): the maximum number of agents that can simultaneously occupy the cell
27-
properties (dict[str, Any]): the properties of the cell
2825
random (Random): the random number generator
2926
3027
"""
3128

3229
__slots__ = [
3330
"__dict__",
34-
"_mesa_property_layers",
3531
"agents",
3632
"capacity",
3733
"connections",
@@ -40,15 +36,6 @@ class Cell:
4036
"random",
4137
]
4238

43-
# def __new__(cls,
44-
# coordinate: tuple[int, ...],
45-
# capacity: float | None = None,
46-
# random: Random | None = None,):
47-
# if capacity != 1:
48-
# return object.__new__(cls)
49-
# else:
50-
# return object.__new__(SingleAgentCell)
51-
5239
def __init__(
5340
self,
5441
coordinate: Coordinate,
@@ -70,9 +57,10 @@ def __init__(
7057
Agent
7158
] = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
7259
self.capacity: int | None = capacity
73-
self.properties: dict[Coordinate, object] = {}
60+
self.properties: dict[
61+
Coordinate, object
62+
] = {} # fixme still used by voronoi mesh
7463
self.random = random
75-
self._mesa_property_layers: dict[str, PropertyLayer] = {}
7664

7765
def connect(self, other: Cell, key: Coordinate | None = None) -> None:
7866
"""Connects this cell to another cell.
@@ -105,6 +93,7 @@ def add_agent(self, agent: CellAgent) -> None:
10593
10694
"""
10795
n = len(self.agents)
96+
self.empty = False
10897

10998
if self.capacity and n >= self.capacity:
11099
raise Exception(
@@ -121,6 +110,7 @@ def remove_agent(self, agent: CellAgent) -> None:
121110
122111
"""
123112
self.agents.remove(agent)
113+
self.empty = self.is_empty
124114

125115
@property
126116
def is_empty(self) -> bool:
@@ -195,23 +185,6 @@ def _neighborhood(
195185
neighborhood.pop(self, None)
196186
return neighborhood
197187

198-
# PropertyLayer methods
199-
def get_property(self, property_name: str) -> Any:
200-
"""Get the value of a property."""
201-
return self._mesa_property_layers[property_name].data[self.coordinate]
202-
203-
def set_property(self, property_name: str, value: Any):
204-
"""Set the value of a property."""
205-
self._mesa_property_layers[property_name].set_cell(self.coordinate, value)
206-
207-
def modify_property(
208-
self, property_name: str, operation: Callable, value: Any = None
209-
):
210-
"""Modify the value of a property."""
211-
self._mesa_property_layers[property_name].modify_cell(
212-
self.coordinate, operation, value
213-
)
214-
215188
def __getstate__(self):
216189
"""Return state of the Cell with connections set to empty."""
217190
# fixme, once we shift to 3.11, replace this with super. __getstate__

mesa/experimental/cell_space/discrete_space.py

+1-63
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33
from __future__ import annotations
44

55
import warnings
6-
from collections.abc import Callable
76
from functools import cached_property
87
from random import Random
9-
from typing import Any, Generic, TypeVar
8+
from typing import Generic, TypeVar
109

1110
from mesa.agent import AgentSet
1211
from mesa.experimental.cell_space.cell import Cell
1312
from mesa.experimental.cell_space.cell_collection import CellCollection
14-
from mesa.space import PropertyLayer
1513

1614
T = TypeVar("T", bound=Cell)
1715

@@ -61,8 +59,6 @@ def __init__(
6159
self.cell_klass = cell_klass
6260

6361
self._empties: dict[tuple[int, ...], None] = {}
64-
self._empties_initialized = False
65-
self.property_layers: dict[str, PropertyLayer] = {}
6662

6763
@property
6864
def cutoff_empties(self): # noqa
@@ -98,64 +94,6 @@ def select_random_empty_cell(self) -> T:
9894
"""Select random empty cell."""
9995
return self.random.choice(list(self.empties))
10096

101-
# PropertyLayer methods
102-
def add_property_layer(
103-
self, property_layer: PropertyLayer, add_to_cells: bool = True
104-
):
105-
"""Add a property layer to the grid.
106-
107-
Args:
108-
property_layer: the property layer to add
109-
add_to_cells: whether to add the property layer to all cells (default: True)
110-
"""
111-
if property_layer.name in self.property_layers:
112-
raise ValueError(f"Property layer {property_layer.name} already exists.")
113-
self.property_layers[property_layer.name] = property_layer
114-
if add_to_cells:
115-
for cell in self._cells.values():
116-
cell._mesa_property_layers[property_layer.name] = property_layer
117-
118-
def remove_property_layer(self, property_name: str, remove_from_cells: bool = True):
119-
"""Remove a property layer from the grid.
120-
121-
Args:
122-
property_name: the name of the property layer to remove
123-
remove_from_cells: whether to remove the property layer from all cells (default: True)
124-
"""
125-
del self.property_layers[property_name]
126-
if remove_from_cells:
127-
for cell in self._cells.values():
128-
del cell._mesa_property_layers[property_name]
129-
130-
def set_property(
131-
self, property_name: str, value, condition: Callable[[T], bool] | None = None
132-
):
133-
"""Set the value of a property for all cells in the grid.
134-
135-
Args:
136-
property_name: the name of the property to set
137-
value: the value to set
138-
condition: a function that takes a cell and returns a boolean
139-
"""
140-
self.property_layers[property_name].set_cells(value, condition)
141-
142-
def modify_properties(
143-
self,
144-
property_name: str,
145-
operation: Callable,
146-
value: Any = None,
147-
condition: Callable[[T], bool] | None = None,
148-
):
149-
"""Modify the values of a specific property for all cells in the grid.
150-
151-
Args:
152-
property_name: the name of the property to modify
153-
operation: the operation to perform
154-
value: the value to use in the operation
155-
condition: a function that takes a cell and returns a boolean (used to filter cells)
156-
"""
157-
self.property_layers[property_name].modify_cells(operation, value, condition)
158-
15997
def __setstate__(self, state):
16098
"""Set the state of the discrete space and rebuild the connections."""
16199
self.__dict__ = state

mesa/experimental/cell_space/grid.py

+62-3
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,50 @@
22

33
from __future__ import annotations
44

5+
import copyreg
56
from collections.abc import Sequence
67
from itertools import product
78
from random import Random
8-
from typing import Generic, TypeVar
9+
from typing import Any, Generic, TypeVar
910

1011
from mesa.experimental.cell_space import Cell, DiscreteSpace
12+
from mesa.experimental.cell_space.property_layer import (
13+
HasPropertyLayers,
14+
PropertyDescriptor,
15+
)
1116

1217
T = TypeVar("T", bound=Cell)
1318

1419

15-
class Grid(DiscreteSpace[T], Generic[T]):
20+
def pickle_gridcell(obj):
21+
"""Helper function for pickling GridCell instances."""
22+
# we have the base class and the state via __getstate__
23+
args = obj.__class__.__bases__[0], obj.__getstate__()
24+
return unpickle_gridcell, args
25+
26+
27+
def unpickle_gridcell(parent, fields):
28+
"""Helper function for unpickling GridCell instances."""
29+
# since the class is dynamically created, we recreate it here
30+
cell_klass = type(
31+
"GridCell",
32+
(parent,),
33+
{"_mesa_properties": set()},
34+
)
35+
instance = cell_klass(
36+
(0, 0)
37+
) # we use a default coordinate and overwrite it with the correct value next
38+
39+
# __gestate__ returns a tuple with dict and slots, but slots contains the dict so we can just use the
40+
# second item only
41+
for k, v in fields[1].items():
42+
if k != "__dict__":
43+
setattr(instance, k, v)
44+
45+
return instance
46+
47+
48+
class Grid(DiscreteSpace[T], Generic[T], HasPropertyLayers):
1649
"""Base class for all grid classes.
1750
1851
Attributes:
@@ -60,14 +93,23 @@ def __init__(
6093
self._try_random = True
6194
self._ndims = len(dimensions)
6295
self._validate_parameters()
96+
self.cell_klass = type(
97+
"GridCell",
98+
(self.cell_klass,),
99+
{"_mesa_properties": set()},
100+
)
101+
102+
# we register the pickle_gridcell helper function
103+
copyreg.pickle(self.cell_klass, pickle_gridcell)
63104

64105
coordinates = product(*(range(dim) for dim in self.dimensions))
65106

66107
self._cells = {
67-
coord: cell_klass(coord, capacity, random=self.random)
108+
coord: self.cell_klass(coord, capacity, random=self.random)
68109
for coord in coordinates
69110
}
70111
self._connect_cells()
112+
self.create_property_layer("empty", default_value=True, dtype=bool)
71113

72114
def _connect_cells(self) -> None:
73115
if self._ndims == 2:
@@ -126,6 +168,23 @@ def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> No
126168
if 0 <= ni < height and 0 <= nj < width:
127169
cell.connect(self._cells[ni, nj], (di, dj))
128170

171+
def __getstate__(self) -> dict[str, Any]:
172+
"""Custom __getstate__ for handling dynamic GridCell class and PropertyDescriptors."""
173+
state = super().__getstate__()
174+
state = {k: v for k, v in state.items() if k != "cell_klass"}
175+
return state
176+
177+
def __setstate__(self, state: dict[str, Any]) -> None:
178+
"""Custom __setstate__ for handling dynamic GridCell class and PropertyDescriptors."""
179+
self.__dict__ = state
180+
self._connect_cells() # using super fails for this for some reason, so we repeat ourselves
181+
182+
self.cell_klass = type(
183+
self._cells[(0, 0)]
184+
) # the __reduce__ function handles this for us nicely
185+
for layer in self._mesa_property_layers.values():
186+
setattr(self.cell_klass, layer.name, PropertyDescriptor(layer))
187+
129188

130189
class OrthogonalMooreGrid(Grid[T]):
131190
"""Grid where cells are connected to their 8 neighbors.

0 commit comments

Comments
 (0)