Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

experimental: Integrate PropertyLayers into cell space #2319

Merged
merged 9 commits into from
Oct 1, 2024
32 changes: 31 additions & 1 deletion mesa/experimental/cell_space/cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

from __future__ import annotations

from collections.abc import Callable
from functools import cache, cached_property
from random import Random
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from mesa.experimental.cell_space.cell_collection import CellCollection
from mesa.space import PropertyLayer

if TYPE_CHECKING:
from mesa.agent import Agent
Expand Down Expand Up @@ -69,6 +71,7 @@
self.capacity: int = capacity
self.properties: dict[Coordinate, object] = {}
self.random = random
self.property_layers: dict[str, PropertyLayer] = {}
Copy link
Member

@quaquel quaquel Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry I don't understand why this is here. should this not be linked to the property layers defined at the grid level?

Copy link
Member Author

@EwoutH EwoutH Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also is, but it might be useful to be able to access your value in a PropertyLayer directly from the cell.

A few lines below you can see that a Cell can do get_property and set_property.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that and agree with it. I now also looked at add_property_layer and start to understand what is going on.

  1. I suggest making this self._property_layers so it's not declared as part of the public API. My concern is that one might add a layer via cell directly which would then not exist on the grid.
  2. I think overall, a more elegant solution is possible here (but that can be a separate PR). My idea would be to add on the fly Descriptors to cells for each property layer. This would make it possible to de cell.property_layer_name, which would return the value for this cell in the named property layer. Another benefit would be that we don't have code by default in cell that is only functional in OrthogonalGrids.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On 2) my high-level idea would be that we use the existing property dictionary of the cell and add to it an entry with the name of the property layer, so cell.properties[property_layer_name]. getting the value should be easy, but I don't know how that would work for setting the value. Any ideas?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of an immediate solution other than to subclass dict and have a dedicated PropertiesDict for all this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was thinking about that. It might be great as an API, but can also create naming conflicts.

Unfortunately, I'm really making a sprint on my thesis now, so not much conceptual-thought capacity to work on this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the one minor change (to a private attribute), I think we can merge this for now. I might have some time over the weekend to play around with more descriptor fun 😄 and see what I can do.

Copy link
Member Author

@EwoutH EwoutH Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go ahead in making the change and merging!

It's all experimental, we can go YOLO.


def connect(self, other: Cell, key: Coordinate | None = None) -> None:
"""Connects this cell to another cell.
Expand Down Expand Up @@ -191,3 +194,30 @@
if not include_center:
neighborhood.pop(self, None)
return neighborhood

# PropertyLayer methods
def add_property_layer(self, property_layer: PropertyLayer):
EwoutH marked this conversation as resolved.
Show resolved Hide resolved
"""Add a PropertyLayer to the cell."""
if property_layer.name in self.property_layers:
raise ValueError(f"Property layer {property_layer.name} already exists.")

Check warning on line 202 in mesa/experimental/cell_space/cell.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/cell_space/cell.py#L202

Added line #L202 was not covered by tests
self.property_layers[property_layer.name] = property_layer

def remove_property_layer(self, property_name: str):
"""Remove a PropertyLayer from the cell."""
del self.property_layers[property_name]

def get_property(self, property_name: str) -> Any:
"""Get the value of a property."""
return self.property_layers[property_name].data[self.coordinate]

def set_property(self, property_name: str, value: Any):
"""Set the value of a property."""
self.property_layers[property_name].set_cell(self.coordinate, value)

Check warning on line 215 in mesa/experimental/cell_space/cell.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/cell_space/cell.py#L215

Added line #L215 was not covered by tests

def modify_property(
self, property_name: str, operation: Callable, value: Any = None
):
"""Modify the value of a property."""
self.property_layers[property_name].modify_cell(

Check warning on line 221 in mesa/experimental/cell_space/cell.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/cell_space/cell.py#L221

Added line #L221 was not covered by tests
self.coordinate, operation, value
)
49 changes: 47 additions & 2 deletions mesa/experimental/cell_space/discrete_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

from __future__ import annotations

from collections.abc import Callable
from functools import cached_property
from random import Random
from typing import Generic, TypeVar
from typing import Any, Generic, TypeVar

from mesa.experimental.cell_space.cell import Cell
from mesa.experimental.cell_space.cell_collection import CellCollection
from mesa.space import PropertyLayer

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

Expand All @@ -21,21 +23,23 @@
random (Random): The random number generator
cell_klass (Type) : the type of cell class
empties (CellCollection) : collecction of all cells that are empty

property_layers (dict[str, PropertyLayer]): the property layers of the grid
"""

def __init__(
self,
capacity: int | None = None,
cell_klass: type[T] = Cell,
random: Random | None = None,
property_layers: None | PropertyLayer | list[PropertyLayer] = None,
):
"""Instantiate a DiscreteSpace.

Args:
capacity: capacity of cells
cell_klass: base class for all cells
random: random number generator
property_layers: property layers to add to the grid
"""
super().__init__()
self.capacity = capacity
Expand All @@ -47,6 +51,12 @@

self._empties: dict[tuple[int, ...], None] = {}
self._empties_initialized = False
self.property_layers: dict[str, PropertyLayer] = {}
if property_layers:
if isinstance(property_layers, PropertyLayer):
property_layers = [property_layers]

Check warning on line 57 in mesa/experimental/cell_space/discrete_space.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/cell_space/discrete_space.py#L57

Added line #L57 was not covered by tests
for layer in property_layers:
self.add_property_layer(layer)

Check warning on line 59 in mesa/experimental/cell_space/discrete_space.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/cell_space/discrete_space.py#L59

Added line #L59 was not covered by tests

@property
def cutoff_empties(self): # noqa
Expand All @@ -73,3 +83,38 @@
def select_random_empty_cell(self) -> T:
"""Select random empty cell."""
return self.random.choice(list(self.empties))

# PropertyLayer methods
def add_property_layer(
self, property_layer: PropertyLayer, add_to_cells: bool = True
):
"""Add a property layer to the grid."""
if property_layer.name in self.property_layers:
raise ValueError(f"Property layer {property_layer.name} already exists.")

Check warning on line 93 in mesa/experimental/cell_space/discrete_space.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/cell_space/discrete_space.py#L93

Added line #L93 was not covered by tests
self.property_layers[property_layer.name] = property_layer
if add_to_cells:
for cell in self._cells.values():
cell.add_property_layer(property_layer)

def remove_property_layer(self, property_name: str, remove_from_cells: bool = True):
"""Remove a property layer from the grid."""
del self.property_layers[property_name]
if remove_from_cells:
for cell in self._cells.values():
cell.remove_property_layer(property_name)

def set_property(
self, property_name: str, value, condition: Callable[[T], bool] | None = None
):
"""Set the value of a property for all cells in the grid."""
self.property_layers[property_name].set_cells(value, condition)
quaquel marked this conversation as resolved.
Show resolved Hide resolved

def modify_properties(
self,
property_name: str,
operation: Callable,
value: Any = None,
condition: Callable[[T], bool] | None = None,
):
"""Modify the values of a specific property for all cells in the grid."""
self.property_layers[property_name].modify_cells(operation, value, condition)
57 changes: 57 additions & 0 deletions tests/test_cell_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import random

import numpy as np
import pytest

from mesa import Model
Expand All @@ -15,6 +16,7 @@
OrthogonalVonNeumannGrid,
VoronoiGrid,
)
from mesa.space import PropertyLayer


def test_orthogonal_grid_neumann():
Expand Down Expand Up @@ -524,3 +526,58 @@ def test_cell_collection():

cells = collection.select()
assert len(cells) == len(collection)


def test_property_layer_integration():
"""Test integration of PropertyLayer with DiscreteSpace and Cell."""
width, height = 10, 10
grid = OrthogonalMooreGrid((width, height), torus=False)

# Test adding a PropertyLayer to the grid
elevation = PropertyLayer("elevation", width, height, default_value=0)
grid.add_property_layer(elevation)
assert "elevation" in grid.property_layers
assert len(grid.property_layers) == 1

# Test accessing PropertyLayer from a cell
cell = grid._cells[(0, 0)]
assert "elevation" in cell.property_layers
assert cell.get_property("elevation") == 0

# Test modifying PropertyLayer values
grid.set_property("elevation", 100, condition=lambda value: value == 0)
assert cell.get_property("elevation") == 100

# Test modifying PropertyLayer using numpy operations
grid.modify_properties("elevation", np.add, 50)
assert cell.get_property("elevation") == 150

# Test removing a PropertyLayer
grid.remove_property_layer("elevation")
assert "elevation" not in grid.property_layers
assert "elevation" not in cell.property_layers
assert len(grid.property_layers) == 0


def test_discrete_space_with_property_layer():
"""Test DiscreteSpace with PropertyLayer."""
width, height = 3, 3
grid = OrthogonalMooreGrid((width, height), torus=False, capacity=None)

# Add a property layer
prop_layer = PropertyLayer("temperature", width, height, default_value=20)
grid.add_property_layer(prop_layer)

assert "temperature" in grid.property_layers

# Set property values
grid.set_property("temperature", 25)

for cell in grid.all_cells:
assert cell.get_property("temperature") == 25

# Modify properties
grid.modify_properties("temperature", lambda x: x + 5)

for cell in grid.all_cells:
assert cell.get_property("temperature") == 30
Loading