Skip to content

Commit 038c9c2

Browse files
EwoutHquaquelpre-commit-ci[bot]
authored
experimental: Integrate PropertyLayers into cell space (#2319)
* experimental: Integrate PropertyLayers into cell space Integrate the PropertyLayers into the cell space. Initially only in the DiscreteSpace, and thereby the Grids. * Add tests for PropertyLayers in cell space --------- Co-authored-by: Jan Kwakkel <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e5ce58a commit 038c9c2

File tree

3 files changed

+173
-3
lines changed

3 files changed

+173
-3
lines changed

mesa/experimental/cell_space/cell.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Callable
56
from functools import cache, cached_property
67
from random import Random
7-
from typing import TYPE_CHECKING
8+
from typing import TYPE_CHECKING, Any
89

910
from mesa.experimental.cell_space.cell_collection import CellCollection
11+
from mesa.space import PropertyLayer
1012

1113
if TYPE_CHECKING:
1214
from mesa.agent import Agent
@@ -34,6 +36,7 @@ class Cell:
3436
"capacity",
3537
"properties",
3638
"random",
39+
"_mesa_property_layers",
3740
"__dict__",
3841
]
3942

@@ -69,6 +72,7 @@ def __init__(
6972
self.capacity: int = capacity
7073
self.properties: dict[Coordinate, object] = {}
7174
self.random = random
75+
self._mesa_property_layers: dict[str, PropertyLayer] = {}
7276

7377
def connect(self, other: Cell, key: Coordinate | None = None) -> None:
7478
"""Connects this cell to another cell.
@@ -190,3 +194,20 @@ def _neighborhood(
190194
if not include_center:
191195
neighborhood.pop(self, None)
192196
return neighborhood
197+
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+
)

mesa/experimental/cell_space/discrete_space.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Callable
56
from functools import cached_property
67
from random import Random
7-
from typing import Generic, TypeVar
8+
from typing import Any, Generic, TypeVar
89

910
from mesa.experimental.cell_space.cell import Cell
1011
from mesa.experimental.cell_space.cell_collection import CellCollection
12+
from mesa.space import PropertyLayer
1113

1214
T = TypeVar("T", bound=Cell)
1315

@@ -21,7 +23,7 @@ class DiscreteSpace(Generic[T]):
2123
random (Random): The random number generator
2224
cell_klass (Type) : the type of cell class
2325
empties (CellCollection) : collecction of all cells that are empty
24-
26+
property_layers (dict[str, PropertyLayer]): the property layers of the discrete space
2527
"""
2628

2729
def __init__(
@@ -47,6 +49,7 @@ def __init__(
4749

4850
self._empties: dict[tuple[int, ...], None] = {}
4951
self._empties_initialized = False
52+
self.property_layers: dict[str, PropertyLayer] = {}
5053

5154
@property
5255
def cutoff_empties(self): # noqa
@@ -73,3 +76,61 @@ def empties(self) -> CellCollection[T]:
7376
def select_random_empty_cell(self) -> T:
7477
"""Select random empty cell."""
7578
return self.random.choice(list(self.empties))
79+
80+
# PropertyLayer methods
81+
def add_property_layer(
82+
self, property_layer: PropertyLayer, add_to_cells: bool = True
83+
):
84+
"""Add a property layer to the grid.
85+
86+
Args:
87+
property_layer: the property layer to add
88+
add_to_cells: whether to add the property layer to all cells (default: True)
89+
"""
90+
if property_layer.name in self.property_layers:
91+
raise ValueError(f"Property layer {property_layer.name} already exists.")
92+
self.property_layers[property_layer.name] = property_layer
93+
if add_to_cells:
94+
for cell in self._cells.values():
95+
cell._mesa_property_layers[property_layer.name] = property_layer
96+
97+
def remove_property_layer(self, property_name: str, remove_from_cells: bool = True):
98+
"""Remove a property layer from the grid.
99+
100+
Args:
101+
property_name: the name of the property layer to remove
102+
remove_from_cells: whether to remove the property layer from all cells (default: True)
103+
"""
104+
del self.property_layers[property_name]
105+
if remove_from_cells:
106+
for cell in self._cells.values():
107+
del cell._mesa_property_layers[property_name]
108+
109+
def set_property(
110+
self, property_name: str, value, condition: Callable[[T], bool] | None = None
111+
):
112+
"""Set the value of a property for all cells in the grid.
113+
114+
Args:
115+
property_name: the name of the property to set
116+
value: the value to set
117+
condition: a function that takes a cell and returns a boolean
118+
"""
119+
self.property_layers[property_name].set_cells(value, condition)
120+
121+
def modify_properties(
122+
self,
123+
property_name: str,
124+
operation: Callable,
125+
value: Any = None,
126+
condition: Callable[[T], bool] | None = None,
127+
):
128+
"""Modify the values of a specific property for all cells in the grid.
129+
130+
Args:
131+
property_name: the name of the property to modify
132+
operation: the operation to perform
133+
value: the value to use in the operation
134+
condition: a function that takes a cell and returns a boolean (used to filter cells)
135+
"""
136+
self.property_layers[property_name].modify_cells(operation, value, condition)

tests/test_cell_space.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import random
44

5+
import numpy as np
56
import pytest
67

78
from mesa import Model
@@ -15,6 +16,7 @@
1516
OrthogonalVonNeumannGrid,
1617
VoronoiGrid,
1718
)
19+
from mesa.space import PropertyLayer
1820

1921

2022
def test_orthogonal_grid_neumann():
@@ -526,6 +528,92 @@ def test_cell_collection():
526528
assert len(cells) == len(collection)
527529

528530

531+
### PropertyLayer tests
532+
def test_property_layer_integration():
533+
"""Test integration of PropertyLayer with DiscrateSpace and Cell."""
534+
width, height = 10, 10
535+
grid = OrthogonalMooreGrid((width, height), torus=False)
536+
537+
# Test adding a PropertyLayer to the grid
538+
elevation = PropertyLayer("elevation", width, height, default_value=0)
539+
grid.add_property_layer(elevation)
540+
assert "elevation" in grid.property_layers
541+
assert len(grid.property_layers) == 1
542+
543+
# Test accessing PropertyLayer from a cell
544+
cell = grid._cells[(0, 0)]
545+
assert "elevation" in cell._mesa_property_layers
546+
assert cell.get_property("elevation") == 0
547+
548+
# Test setting property value for a cell
549+
cell.set_property("elevation", 100)
550+
assert cell.get_property("elevation") == 100
551+
552+
# Test modifying property value for a cell
553+
cell.modify_property("elevation", lambda x: x + 50)
554+
assert cell.get_property("elevation") == 150
555+
556+
cell.modify_property("elevation", np.add, 50)
557+
assert cell.get_property("elevation") == 200
558+
559+
# Test modifying PropertyLayer values
560+
grid.set_property("elevation", 100, condition=lambda value: value == 200)
561+
assert cell.get_property("elevation") == 100
562+
563+
# Test modifying PropertyLayer using numpy operations
564+
grid.modify_properties("elevation", np.add, 50)
565+
assert cell.get_property("elevation") == 150
566+
567+
# Test removing a PropertyLayer
568+
grid.remove_property_layer("elevation")
569+
assert "elevation" not in grid.property_layers
570+
assert "elevation" not in cell._mesa_property_layers
571+
572+
573+
def test_multiple_property_layers():
574+
"""Test initialization of DiscrateSpace with PropertyLayers."""
575+
width, height = 5, 5
576+
elevation = PropertyLayer("elevation", width, height, default_value=0)
577+
temperature = PropertyLayer("temperature", width, height, default_value=20)
578+
579+
# Test initialization with a single PropertyLayer
580+
grid1 = OrthogonalMooreGrid((width, height), torus=False)
581+
grid1.add_property_layer(elevation)
582+
assert "elevation" in grid1.property_layers
583+
assert len(grid1.property_layers) == 1
584+
585+
# Test initialization with multiple PropertyLayers
586+
grid2 = OrthogonalMooreGrid((width, height), torus=False)
587+
grid2.add_property_layer(temperature, add_to_cells=False)
588+
grid2.add_property_layer(elevation, add_to_cells=True)
589+
590+
assert "temperature" in grid2.property_layers
591+
assert "elevation" in grid2.property_layers
592+
assert len(grid2.property_layers) == 2
593+
594+
# Modify properties
595+
grid2.modify_properties("elevation", lambda x: x + 10)
596+
grid2.modify_properties("temperature", lambda x: x + 5)
597+
598+
for cell in grid2.all_cells:
599+
assert cell.get_property("elevation") == 10
600+
# Assert error temperature, since it was not added to cells
601+
with pytest.raises(KeyError):
602+
cell.get_property("temperature")
603+
604+
605+
def test_property_layer_errors():
606+
"""Test error handling for PropertyLayers."""
607+
width, height = 5, 5
608+
grid = OrthogonalMooreGrid((width, height), torus=False)
609+
elevation = PropertyLayer("elevation", width, height, default_value=0)
610+
611+
# Test adding a PropertyLayer with an existing name
612+
grid.add_property_layer(elevation)
613+
with pytest.raises(ValueError, match="Property layer elevation already exists."):
614+
grid.add_property_layer(elevation)
615+
616+
529617
def test_cell_agent(): # noqa: D103
530618
cell1 = Cell((1,), capacity=None, random=random.Random())
531619
cell2 = Cell((2,), capacity=None, random=random.Random())

0 commit comments

Comments
 (0)