From 9b8e6a02e5a5d9fe3f4b7446c39e7808fa6d1d3e Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 17 Nov 2024 23:46:18 +0100 Subject: [PATCH 01/55] initial commit --- .../experimental/cell_space/discrete_space.py | 62 +-- .../experimental/cell_space/property_layer.py | 425 ++++++++++++++++++ 2 files changed, 427 insertions(+), 60 deletions(-) create mode 100644 mesa/experimental/cell_space/property_layer.py diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index a017d06bda8..0415253d783 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -61,8 +61,8 @@ def __init__( self.cell_klass = cell_klass self._empties: dict[tuple[int, ...], None] = {} - self._empties_initialized = False - self.property_layers: dict[str, PropertyLayer] = {} + # self._empties_initialized = False + # self.property_layers: dict[str, PropertyLayer] = {} @property def cutoff_empties(self): # noqa @@ -98,64 +98,6 @@ 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. - - Args: - property_layer: the property layer to add - add_to_cells: whether to add the property layer to all cells (default: True) - """ - if property_layer.name in self.property_layers: - raise ValueError(f"Property layer {property_layer.name} already exists.") - self.property_layers[property_layer.name] = property_layer - if add_to_cells: - for cell in self._cells.values(): - cell._mesa_property_layers[property_layer.name] = property_layer - - def remove_property_layer(self, property_name: str, remove_from_cells: bool = True): - """Remove a property layer from the grid. - - Args: - property_name: the name of the property layer to remove - remove_from_cells: whether to remove the property layer from all cells (default: True) - """ - del self.property_layers[property_name] - if remove_from_cells: - for cell in self._cells.values(): - del cell._mesa_property_layers[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. - - Args: - property_name: the name of the property to set - value: the value to set - condition: a function that takes a cell and returns a boolean - """ - self.property_layers[property_name].set_cells(value, condition) - - 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. - - Args: - property_name: the name of the property to modify - operation: the operation to perform - value: the value to use in the operation - condition: a function that takes a cell and returns a boolean (used to filter cells) - """ - self.property_layers[property_name].modify_cells(operation, value, condition) - def __setstate__(self, state): """Set the state of the discrete space and rebuild the connections.""" self.__dict__ = state diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py new file mode 100644 index 00000000000..dd42b8ff724 --- /dev/null +++ b/mesa/experimental/cell_space/property_layer.py @@ -0,0 +1,425 @@ + +import inspect +import warnings +from collections.abc import Callable +from typing import Any, TypeVar, Sequence + +import numpy as np + +from .cell import Cell +from .grid import Grid + +Coordinate = Sequence[int] +T = TypeVar("T", bound=Cell) +class PropertyLayer: + """A class representing a layer of properties in a two-dimensional grid. + + Each cell in the grid can store a value of a specified data type. + + Attributes: + name: The name of the property layer. + dimensions: The width of the grid (number of columns). + data: A NumPy array representing the grid data. + + # Fixme do we need this class at all? + # what does it add to just a numpy array? + + """ + + propertylayer_experimental_warning_given = False + + def __init__( + self, name: str, dimensions: Sequence[int], default_value=0.0, dtype=float + ): + """Initializes a new PropertyLayer instance. + + Args: + name: The name of the property layer. + width: The width of the grid (number of columns). + height: The height of the grid (number of rows). + default_value: The default value to initialize each cell in the grid. Should ideally + be of the same type as specified by the dtype parameter. + dtype (data-type, optional): The desired data-type for the grid's elements. Default is float. + + Notes: + A UserWarning is raised if the default_value is not of a type compatible with dtype. + The dtype parameter can accept both Python data types (like bool, int or float) and NumPy data types + (like np.int64 or np.float64). Using NumPy data types is recommended (except for bool) for better control + over the precision and efficiency of data storage and computations, especially in cases of large data + volumes or specialized numerical operations. + """ + self.name = name + self.dimensions = dimensions + + # Check if the dtype is suitable for the data + if not isinstance(default_value, dtype): + warnings.warn( + f"Default value {default_value} ({type(default_value).__name__}) might not be best suitable with dtype={dtype.__name__}.", + UserWarning, + stacklevel=2, + ) + + # fixme why not initialize with empty? + self.data = np.full(self.dimensions, default_value, dtype=dtype) + + if not self.__class__.propertylayer_experimental_warning_given: + warnings.warn( + "The property layer functionality and associated classes are experimental. It may be changed or removed in any and all future releases, including patch releases.\n" + "We would love to hear what you think about this new feature. If you have any thoughts, share them with us here: https://github.com/projectmesa/mesa/discussions/1932", + FutureWarning, + stacklevel=2, + ) + self.__class__.propertylayer_experimental_warning_given = True + + + def set_cell(self, position: Coordinate, value): + """Update a single cell's value in-place.""" + self.data[position] = value + + def set_cells(self, value, condition: Callable|None = None): + """Perform a batch update either on the entire grid or conditionally, in-place. + + Args: + value: The value to be used for the update. + condition: (Optional) A callable (like a lambda function or a NumPy ufunc) + that returns a boolean array when applied to the data. + """ + if condition is None: + np.copyto(self.data, value) # In-place update + else: + if isinstance(condition, np.ufunc): + # Directly apply NumPy ufunc + condition_result = condition(self.data) + else: + # Vectorize non-ufunc conditions + vectorized_condition = np.vectorize(condition) + condition_result = vectorized_condition(self.data) + + if ( + not isinstance(condition_result, np.ndarray) + or condition_result.shape != self.data.shape + ): + raise ValueError( + "Result of condition must be a NumPy array with the same shape as the grid." + ) + + np.copyto(self.data, value, where=condition_result) + + def modify_cell(self, position: Coordinate, operation: Callable, value=None): + """Modify a single cell using an operation, which can be a lambda function or a NumPy ufunc. + + If a NumPy ufunc is used, an additional value should be provided. + + Args: + position: The grid coordinates of the cell to modify. + operation: A function to apply. Can be a lambda function or a NumPy ufunc. + value: The value to be used if the operation is a NumPy ufunc. Ignored for lambda functions. + """ + current_value = self.data[position] + + # Determine if the operation is a lambda function or a NumPy ufunc + if is_single_argument_function(operation): + # Lambda function case + self.data[position] = operation(current_value) + elif value is not None: + # NumPy ufunc case + self.data[position] = operation(current_value, value) + else: + raise ValueError("Invalid operation or missing value for NumPy ufunc.") + + def modify_cells(self, operation: Callable, value=None, condition_function: Callable|None = None): + """Modify cells using an operation, which can be a lambda function or a NumPy ufunc. + + If a NumPy ufunc is used, an additional value should be provided. + + Args: + operation: A function to apply. Can be a lambda function or a NumPy ufunc. + value: The value to be used if the operation is a NumPy ufunc. Ignored for lambda functions. + condition_function: (Optional) A callable that returns a boolean array when applied to the data. + """ + condition_array = np.ones_like( + self.data, dtype=bool + ) # Default condition (all cells) + if condition_function is not None: + if isinstance(condition_function, np.ufunc): + condition_array = condition_function(self.data) + else: + vectorized_condition = np.vectorize(condition_function) + condition_array = vectorized_condition(self.data) + + # Check if the operation is a lambda function or a NumPy ufunc + if isinstance(operation, np.ufunc): + if ufunc_requires_additional_input(operation): + if value is None: + raise ValueError("This ufunc requires an additional input value.") + modified_data = operation(self.data, value) + else: + modified_data = operation(self.data) + else: + # Vectorize non-ufunc operations + vectorized_operation = np.vectorize(operation) + modified_data = vectorized_operation(self.data) + + self.data = np.where(condition_array, modified_data, self.data) + + def select_cells(self, condition: Callable, return_list=True): + """Find cells that meet a specified condition using NumPy's boolean indexing, in-place. + + # fixme: consider splitting into two separate functions + # select_cells_boolean + # select_cells_index + + Args: + condition: A callable that returns a boolean array when applied to the data. + return_list: (Optional) If True, return a list of (x, y) tuples. Otherwise, return a boolean array. + + Returns: + A list of (x, y) tuples or a boolean array. + """ + condition_array = condition(self.data) + if return_list: + return list(zip(*np.where(condition_array))) + else: + return condition_array + + def aggregate_property(self, operation: Callable): + """Perform an aggregate operation (e.g., sum, mean) on a property across all cells. + + Args: + operation: A function to apply. Can be a lambda function or a NumPy ufunc. + """ + return operation(self.data) + + +class HasPropertyLayers: + """Mixin-like class to add property layer functionality to Grids.""" + # fixme is there a way to indicate that a mixin only works with specific classes? + def __init__(self, *args, **kwargs): + """Initialize a HasPropertyLayers instance.""" + super().__init__(*args, **kwargs) + self.property_layers = {} + # Initialize an empty mask as a boolean NumPy array + + # fixme: in space.py this is a boolean mask with empty and non empty cells + # this does not easily translate (unless we handle it in the celll, via add and remove agents?) + # but then we might better treat this as a default property layer that is just allways added + self.add_property_layer("empty", True, dtype=bool) + + def add_property_layer( + self, name: str, default_value=0.0, dtype=float, + ): + """Add a property layer to the grid. + + Args: + name: The name of the property layer. + default_value: The default value of the property layer. + dtype: The data type of the property layer. + """ + # fixme, do we want to have the ability to add both predefined layers + # as well as just by name? + + self.property_layers[name] = PropertyLayer(name, self.dimensions, default_value=default_value, dtype=dtype) + # fixme: this should be automagical + # if add_to_cells: + # for cell in self._cells.values(): + # cell._mesa_property_layers[property_layer.name] = property_layer + + def remove_property_layer(self, property_name: str, remove_from_cells: bool = True): + """Remove a property layer from the grid. + + Args: + property_name: the name of the property layer to remove + remove_from_cells: whether to remove the property layer from all cells (default: True) + """ + del self.property_layers[property_name] + # fixme: this should be automagical + # if remove_from_cells: + # for cell in self._cells.values(): + # del cell._mesa_property_layers[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. + + Args: + property_name: the name of the property to set + value: the value to set + condition: a function that takes a cell and returns a boolean + """ + self.property_layers[property_name].set_cells(value, condition) + + 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. + + Args: + property_name: the name of the property to modify + operation: the operation to perform + value: the value to use in the operation + condition: a function that takes a cell and returns a boolean (used to filter cells) + """ + self.property_layers[property_name].modify_cells(operation, value, condition) + + + def get_neighborhood_mask( + self, pos: Coordinate, moore: bool, include_center: bool, radius: int + ) -> np.ndarray: + """Generate a boolean mask representing the neighborhood. + + Args: + pos (Coordinate): Center of the neighborhood. + moore (bool): True for Moore neighborhood, False for Von Neumann. + include_center (bool): Include the central cell in the neighborhood. + radius (int): The radius of the neighborhood. + + Returns: + np.ndarray: A boolean mask representing the neighborhood. + """ + # Fixme, should this not move into the cell? + neighborhood = self.get_neighborhood(pos, moore, include_center, radius) + mask = np.zeros((self.width, self.height), dtype=bool) + + # Convert the neighborhood list to a NumPy array and use advanced indexing + coords = np.array(neighborhood) + mask[coords[:, 0], coords[:, 1]] = True + return mask + + def select_cells( + self, + conditions: dict | None = None, + extreme_values: dict | None = None, + masks: np.ndarray | list[np.ndarray] = None, + only_empty: bool = False, + return_list: bool = True, + ) -> list[Coordinate] | np.ndarray: + """Select cells based on property conditions, extreme values, and/or masks, with an option to only select empty cells. + + Args: + conditions (dict): A dictionary where keys are property names and values are callables that return a boolean when applied. + extreme_values (dict): A dictionary where keys are property names and values are either 'highest' or 'lowest'. + masks (np.ndarray | list[np.ndarray], optional): A mask or list of masks to restrict the selection. + only_empty (bool, optional): If True, only select cells that are empty. Default is False. + return_list (bool, optional): If True, return a list of coordinates, otherwise return a mask. + + Returns: + Union[list[Coordinate], np.ndarray]: Coordinates where conditions are satisfied or the combined mask. + """ + # fixme: consider splitting into two separate functions + # select_cells_boolean + # select_cells_index + # also we might want to change the naming to avoid classes with PropertyLayer + + + # Initialize the combined mask + combined_mask = np.ones((self.width, self.height), dtype=bool) + + # Apply the masks + if masks is not None: + if isinstance(masks, list): + for mask in masks: + combined_mask = np.logical_and(combined_mask, mask) + else: + combined_mask = np.logical_and(combined_mask, masks) + + # Apply the empty mask if only_empty is True + if only_empty: + combined_mask = np.logical_and(combined_mask, self.empty_mask) + + # Apply conditions + if conditions: + for prop_name, condition in conditions.items(): + prop_layer = self.property_layers[prop_name].data + prop_mask = condition(prop_layer) + combined_mask = np.logical_and(combined_mask, prop_mask) + + # Apply extreme values + if extreme_values: + for property_name, mode in extreme_values.items(): + prop_values = self.property_layers[property_name].data + + # Create a masked array using the combined_mask + masked_values = np.ma.masked_array(prop_values, mask=~combined_mask) + + if mode == "highest": + target_value = masked_values.max() + elif mode == "lowest": + target_value = masked_values.min() + else: + raise ValueError( + f"Invalid mode {mode}. Choose from 'highest' or 'lowest'." + ) + + extreme_value_mask = prop_values == target_value + combined_mask = np.logical_and(combined_mask, extreme_value_mask) + + # Generate output + if return_list: + selected_cells = list(zip(*np.where(combined_mask))) + return selected_cells + else: + return combined_mask + + +class HasProperties: + """Mixin-like class to add property layer functionality to cells.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.properties = {} + # do we want attribute like access to properties or not? + + def _add_property(self, name): + pass + + def _remove_property(self): + pass + + + # PropertyLayer methods + def get_property(self, property_name: str) -> Any: + """Get the value of a property.""" + return self._mesa_property_layers[property_name].data[self.coordinate] + + def set_property(self, property_name: str, value: Any): + """Set the value of a property.""" + self._mesa_property_layers[property_name].set_cell(self.coordinate, value) + + def modify_property( + self, property_name: str, operation: Callable, value: Any = None + ): + """Modify the value of a property.""" + # fixme, do we want to support this? + self._mesa_property_layers[property_name].modify_cell( + self.coordinate, operation, value + ) + + +class PropertyDescriptor: + + def __get__(self): + pass + + def __set__(self, value): + pass + + def __set_name__(self, owner, name): + pass + + +def is_single_argument_function(function): + """Check if a function is a single argument function.""" + return ( + inspect.isfunction(function) + and len(inspect.signature(function).parameters) == 1 + ) + +def ufunc_requires_additional_input(ufunc): # noqa: D103 + # NumPy ufuncs have a 'nargs' attribute indicating the number of input arguments + # For binary ufuncs (like np.add), nargs is 2 + return ufunc.nargs > 1 From a6161c7a4713f97ab30ec0265157ce89550fbaca Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 17 Nov 2024 23:53:03 +0100 Subject: [PATCH 02/55] Update property_layer.py --- .../experimental/cell_space/property_layer.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index dd42b8ff724..649c245f0d4 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -1,13 +1,12 @@ import inspect import warnings -from collections.abc import Callable -from typing import Any, TypeVar, Sequence +from collections.abc import Callable, Sequence +from typing import Any, TypeVar import numpy as np from .cell import Cell -from .grid import Grid Coordinate = Sequence[int] T = TypeVar("T", bound=Cell) @@ -401,16 +400,17 @@ def modify_property( class PropertyDescriptor: - - def __get__(self): - pass - - def __set__(self, value): - pass - - def __set_name__(self, owner, name): - pass - + """Descriptor for giving cells attribute like access to values defined in property layers.""" + + def __init__(self, property_layer: PropertyLayer): # noqa: D105 + self.layer = property_layer.data + def __get__(self, instance: Cell): # noqa: D105 + return self.layer[self.coordinate] + def __set__(self, instance: Cell, value): # noqa: D105 + self.layer[self.coordinate] = value + def __set_name__(self, owner: Cell, name: str): # noqa: D105 + self.public_name = name + self.private_name = f"_{name}" def is_single_argument_function(function): """Check if a function is a single argument function.""" From 7aec942efc3dac4cdff6d6295c7f8347c46c098d Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 17 Nov 2024 23:53:38 +0100 Subject: [PATCH 03/55] Update property_layer.py --- mesa/experimental/cell_space/property_layer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 649c245f0d4..e66694ad478 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -404,7 +404,7 @@ class PropertyDescriptor: def __init__(self, property_layer: PropertyLayer): # noqa: D105 self.layer = property_layer.data - def __get__(self, instance: Cell): # noqa: D105 + def __get__(self, instance: Cell, owner): # noqa: D105 return self.layer[self.coordinate] def __set__(self, instance: Cell, value): # noqa: D105 self.layer[self.coordinate] = value From d62ef3e2c304006fbbbb80c82393c0e531888944 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 18 Nov 2024 14:50:46 +0100 Subject: [PATCH 04/55] first set of tests --- mesa/experimental/cell_space/__init__.py | 2 + mesa/experimental/cell_space/cell.py | 16 +- .../experimental/cell_space/discrete_space.py | 4 +- mesa/experimental/cell_space/grid.py | 3 +- .../experimental/cell_space/property_layer.py | 200 +++++++----------- tests/test_cell_space.py | 128 +++++------ 6 files changed, 151 insertions(+), 202 deletions(-) diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py index 69386a4cacf..8e1c7160007 100644 --- a/mesa/experimental/cell_space/__init__.py +++ b/mesa/experimental/cell_space/__init__.py @@ -21,6 +21,7 @@ ) from mesa.experimental.cell_space.network import Network from mesa.experimental.cell_space.voronoi import VoronoiGrid +from mesa.experimental.cell_space.property_layer import PropertyLayer __all__ = [ "CellCollection", @@ -35,4 +36,5 @@ "OrthogonalVonNeumannGrid", "Network", "VoronoiGrid", + "PropertyLayer" ] diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 4bc6e874a64..0c1ff05d8fe 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -9,7 +9,6 @@ from mesa.experimental.cell_space.cell_agent import CellAgent from mesa.experimental.cell_space.cell_collection import CellCollection -from mesa.space import PropertyLayer if TYPE_CHECKING: from mesa.agent import Agent @@ -24,7 +23,6 @@ class Cell: 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 """ @@ -34,21 +32,10 @@ class Cell: "connections", "agents", "capacity", - "properties", "random", - "_mesa_property_layers", "__dict__", ] - # 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: Coordinate, @@ -69,10 +56,9 @@ def __init__( self.agents: list[ Agent ] = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) + self.properties = {} # fixme still used by voronoi mesh self.capacity: int | None = capacity - self.properties: dict[Coordinate, object] = {} self.random = random - self._mesa_property_layers: dict[str, PropertyLayer] = {} def connect(self, other: Cell, key: Coordinate | None = None) -> None: """Connects this cell to another cell. diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index 0415253d783..f7259ebc425 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -3,15 +3,13 @@ from __future__ import annotations import warnings -from collections.abc import Callable from functools import cached_property from random import Random -from typing import Any, Generic, TypeVar +from typing import Generic, TypeVar from mesa.agent import AgentSet 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) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 299572faf5e..30c3410e1a4 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -8,11 +8,12 @@ from typing import Generic, TypeVar from mesa.experimental.cell_space import Cell, DiscreteSpace +from mesa.experimental.cell_space.property_layer import HasPropertyLayers T = TypeVar("T", bound=Cell) -class Grid(DiscreteSpace[T], Generic[T]): +class Grid(DiscreteSpace[T], Generic[T], HasPropertyLayers): """Base class for all grid classes. Attributes: diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index e66694ad478..5463c63a458 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -1,8 +1,9 @@ +"""This module provides functionality for working with property layers in grids.""" import inspect import warnings from collections.abc import Callable, Sequence -from typing import Any, TypeVar +from typing import Any, TypeVar, TYPE_CHECKING import numpy as np @@ -10,6 +11,8 @@ Coordinate = Sequence[int] T = TypeVar("T", bound=Cell) + + class PropertyLayer: """A class representing a layer of properties in a two-dimensional grid. @@ -28,14 +31,13 @@ class PropertyLayer: propertylayer_experimental_warning_given = False def __init__( - self, name: str, dimensions: Sequence[int], default_value=0.0, dtype=float + self, name: str, dimensions: Sequence[int], default_value=0.0, dtype=float ): """Initializes a new PropertyLayer instance. Args: name: The name of the property layer. - width: The width of the grid (number of columns). - height: The height of the grid (number of rows). + dimensions: the dimensions of the property layer. default_value: The default value to initialize each cell in the grid. Should ideally be of the same type as specified by the dtype parameter. dtype (data-type, optional): The desired data-type for the grid's elements. Default is float. @@ -43,9 +45,7 @@ def __init__( Notes: A UserWarning is raised if the default_value is not of a type compatible with dtype. The dtype parameter can accept both Python data types (like bool, int or float) and NumPy data types - (like np.int64 or np.float64). Using NumPy data types is recommended (except for bool) for better control - over the precision and efficiency of data storage and computations, especially in cases of large data - volumes or specialized numerical operations. + (like np.int64 or np.float64). """ self.name = name self.dimensions = dimensions @@ -70,12 +70,7 @@ def __init__( ) self.__class__.propertylayer_experimental_warning_given = True - - def set_cell(self, position: Coordinate, value): - """Update a single cell's value in-place.""" - self.data[position] = value - - def set_cells(self, value, condition: Callable|None = None): + def set_cells(self, value, condition: Callable | None = None): """Perform a batch update either on the entire grid or conditionally, in-place. Args: @@ -95,8 +90,8 @@ def set_cells(self, value, condition: Callable|None = None): condition_result = vectorized_condition(self.data) if ( - not isinstance(condition_result, np.ndarray) - or condition_result.shape != self.data.shape + not isinstance(condition_result, np.ndarray) + or condition_result.shape != self.data.shape ): raise ValueError( "Result of condition must be a NumPy array with the same shape as the grid." @@ -104,29 +99,7 @@ def set_cells(self, value, condition: Callable|None = None): np.copyto(self.data, value, where=condition_result) - def modify_cell(self, position: Coordinate, operation: Callable, value=None): - """Modify a single cell using an operation, which can be a lambda function or a NumPy ufunc. - - If a NumPy ufunc is used, an additional value should be provided. - - Args: - position: The grid coordinates of the cell to modify. - operation: A function to apply. Can be a lambda function or a NumPy ufunc. - value: The value to be used if the operation is a NumPy ufunc. Ignored for lambda functions. - """ - current_value = self.data[position] - - # Determine if the operation is a lambda function or a NumPy ufunc - if is_single_argument_function(operation): - # Lambda function case - self.data[position] = operation(current_value) - elif value is not None: - # NumPy ufunc case - self.data[position] = operation(current_value, value) - else: - raise ValueError("Invalid operation or missing value for NumPy ufunc.") - - def modify_cells(self, operation: Callable, value=None, condition_function: Callable|None = None): + def modify_cells(self, operation: Callable, value=None, condition_function: Callable | None = None): """Modify cells using an operation, which can be a lambda function or a NumPy ufunc. If a NumPy ufunc is used, an additional value should be provided. @@ -192,20 +165,25 @@ def aggregate_property(self, operation: Callable): class HasPropertyLayers: """Mixin-like class to add property layer functionality to Grids.""" + # fixme is there a way to indicate that a mixin only works with specific classes? def __init__(self, *args, **kwargs): """Initialize a HasPropertyLayers instance.""" super().__init__(*args, **kwargs) - self.property_layers = {} + self._mesa_property_layers = {} # Initialize an empty mask as a boolean NumPy array # fixme: in space.py this is a boolean mask with empty and non empty cells # this does not easily translate (unless we handle it in the celll, via add and remove agents?) # but then we might better treat this as a default property layer that is just allways added - self.add_property_layer("empty", True, dtype=bool) - - def add_property_layer( - self, name: str, default_value=0.0, dtype=float, + # + # fixme this won't work at the moment with the descriptor like access..... + # we might make some modifications to is_empty? + # I cannot add a layer here because the init of the other classes has not yet completed + # self.create_property_layer("empty", True, dtype=bool) + + def create_property_layer( + self, name: str, default_value=0.0, dtype=float, ): """Add a property layer to the grid. @@ -216,28 +194,39 @@ def add_property_layer( """ # fixme, do we want to have the ability to add both predefined layers # as well as just by name? + layer = PropertyLayer(name, self.dimensions, default_value=default_value, dtype=dtype) + self._mesa_property_layers[name] = layer + + # fixme: how will this interact with slots and can I dynamically change slots? + setattr(self.cell_klass, name, PropertyDescriptor(layer)) + + def add_property_layer(self, layer: PropertyLayer): + """Add a predefined property layer to the grid. + + Args: + layer: The property layer to add. + + Raises: + ValueError: If the dimensions of the layer and the grid are not the same. - self.property_layers[name] = PropertyLayer(name, self.dimensions, default_value=default_value, dtype=dtype) - # fixme: this should be automagical - # if add_to_cells: - # for cell in self._cells.values(): - # cell._mesa_property_layers[property_layer.name] = property_layer + """ + if layer.dimensions != self.dimensions: + raise ValueError("Dimensions of property layer do not match the dimensions of the grid") + self._mesa_property_layers[layer.name] = layer + setattr(self.cell_klass, layer.name, PropertyDescriptor(layer)) # fixme: curious to see if this works - def remove_property_layer(self, property_name: str, remove_from_cells: bool = True): + def remove_property_layer(self, property_name: str): """Remove a property layer from the grid. Args: property_name: the name of the property layer to remove remove_from_cells: whether to remove the property layer from all cells (default: True) """ - del self.property_layers[property_name] - # fixme: this should be automagical - # if remove_from_cells: - # for cell in self._cells.values(): - # del cell._mesa_property_layers[property_name] + del self._mesa_property_layers[property_name] + delattr(Cell, property_name) def set_property( - self, property_name: str, value, condition: Callable[[T], bool] | None = None + self, property_name: str, value, condition: Callable[[T], bool] | None = None ): """Set the value of a property for all cells in the grid. @@ -246,14 +235,14 @@ def set_property( value: the value to set condition: a function that takes a cell and returns a boolean """ - self.property_layers[property_name].set_cells(value, condition) + self._mesa_property_layers[property_name].set_cells(value, condition) def modify_properties( - self, - property_name: str, - operation: Callable, - value: Any = None, - condition: Callable[[T], bool] | None = None, + 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. @@ -263,39 +252,36 @@ def modify_properties( value: the value to use in the operation condition: a function that takes a cell and returns a boolean (used to filter cells) """ - self.property_layers[property_name].modify_cells(operation, value, condition) - + self._mesa_property_layers[property_name].modify_cells(operation, value, condition) def get_neighborhood_mask( - self, pos: Coordinate, moore: bool, include_center: bool, radius: int + self, pos: Coordinate, include_center: bool, radius: int ) -> np.ndarray: """Generate a boolean mask representing the neighborhood. Args: pos (Coordinate): Center of the neighborhood. - moore (bool): True for Moore neighborhood, False for Von Neumann. include_center (bool): Include the central cell in the neighborhood. radius (int): The radius of the neighborhood. Returns: np.ndarray: A boolean mask representing the neighborhood. """ - # Fixme, should this not move into the cell? - neighborhood = self.get_neighborhood(pos, moore, include_center, radius) - mask = np.zeros((self.width, self.height), dtype=bool) + neighborhood = self._cells[pos].get_neighborhood(include_center=include_center, radius=radius) + mask = np.zeros(self.dimensions, dtype=bool) # Convert the neighborhood list to a NumPy array and use advanced indexing - coords = np.array(neighborhood) - mask[coords[:, 0], coords[:, 1]] = True + coords = np.array(c.dimenions for c in neighborhood) + mask[coords[:, 0], coords[:, 1]] = True # fixme, must work for nd, so coords must be valid for indexing return mask def select_cells( - self, - conditions: dict | None = None, - extreme_values: dict | None = None, - masks: np.ndarray | list[np.ndarray] = None, - only_empty: bool = False, - return_list: bool = True, + self, + conditions: dict | None = None, + extreme_values: dict | None = None, + masks: np.ndarray | list[np.ndarray] = None, + only_empty: bool = False, + return_list: bool = True, ) -> list[Coordinate] | np.ndarray: """Select cells based on property conditions, extreme values, and/or masks, with an option to only select empty cells. @@ -314,9 +300,8 @@ def select_cells( # select_cells_index # also we might want to change the naming to avoid classes with PropertyLayer - # Initialize the combined mask - combined_mask = np.ones((self.width, self.height), dtype=bool) + combined_mask = np.ones(self.dimensions, dtype=bool) # Apply the masks if masks is not None: @@ -327,20 +312,20 @@ def select_cells( combined_mask = np.logical_and(combined_mask, masks) # Apply the empty mask if only_empty is True - if only_empty: + if only_empty: # fixme does not currently work combined_mask = np.logical_and(combined_mask, self.empty_mask) # Apply conditions if conditions: for prop_name, condition in conditions.items(): - prop_layer = self.property_layers[prop_name].data + prop_layer = self._mesa_property_layers[prop_name].data prop_mask = condition(prop_layer) combined_mask = np.logical_and(combined_mask, prop_mask) # Apply extreme values if extreme_values: for property_name, mode in extreme_values.items(): - prop_values = self.property_layers[property_name].data + prop_values = self._mesa_property_layers[property_name].data # Create a masked array using the combined_mask masked_values = np.ma.masked_array(prop_values, mask=~combined_mask) @@ -365,60 +350,33 @@ def select_cells( return combined_mask -class HasProperties: - """Mixin-like class to add property layer functionality to cells.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.properties = {} - # do we want attribute like access to properties or not? - - def _add_property(self, name): - pass - - def _remove_property(self): - pass - - - # PropertyLayer methods - def get_property(self, property_name: str) -> Any: - """Get the value of a property.""" - return self._mesa_property_layers[property_name].data[self.coordinate] - - def set_property(self, property_name: str, value: Any): - """Set the value of a property.""" - self._mesa_property_layers[property_name].set_cell(self.coordinate, value) - - def modify_property( - self, property_name: str, operation: Callable, value: Any = None - ): - """Modify the value of a property.""" - # fixme, do we want to support this? - self._mesa_property_layers[property_name].modify_cell( - self.coordinate, operation, value - ) - - class PropertyDescriptor: """Descriptor for giving cells attribute like access to values defined in property layers.""" - def __init__(self, property_layer: PropertyLayer): # noqa: D105 - self.layer = property_layer.data + def __init__(self, property_layer: PropertyLayer): # noqa: D107 + self.layer: np.ndarray = property_layer + self.public_name: str + self.private_name: str + def __get__(self, instance: Cell, owner): # noqa: D105 - return self.layer[self.coordinate] + return self.layer.data[instance.coordinate] + def __set__(self, instance: Cell, value): # noqa: D105 - self.layer[self.coordinate] = value + self.layer.data[instance.coordinate] = value + def __set_name__(self, owner: Cell, name: str): # noqa: D105 self.public_name = name self.private_name = f"_{name}" + def is_single_argument_function(function): """Check if a function is a single argument function.""" return ( - inspect.isfunction(function) - and len(inspect.signature(function).parameters) == 1 + inspect.isfunction(function) + and len(inspect.signature(function).parameters) == 1 ) + def ufunc_requires_additional_input(ufunc): # noqa: D103 # NumPy ufuncs have a 'nargs' attribute indicating the number of input arguments # For binary ufuncs (like np.add), nargs is 2 diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index b11701745cd..67499fcb41e 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -17,8 +17,9 @@ OrthogonalMooreGrid, OrthogonalVonNeumannGrid, VoronoiGrid, + PropertyLayer ) -from mesa.space import PropertyLayer + def test_orthogonal_grid_neumann(): @@ -620,87 +621,90 @@ def test_empty_cell_collection(): ### PropertyLayer tests def test_property_layer_integration(): """Test integration of PropertyLayer with DiscrateSpace and Cell.""" - width, height = 10, 10 - grid = OrthogonalMooreGrid((width, height), torus=False, random=random.Random(42)) + dimensions = (10, 10) + grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) # Test adding a PropertyLayer to the grid - elevation = PropertyLayer("elevation", width, height, default_value=0) + elevation = PropertyLayer("elevation", dimensions, default_value=0.0) grid.add_property_layer(elevation) - assert "elevation" in grid.property_layers - assert len(grid.property_layers) == 1 + assert "elevation" in grid._mesa_property_layers + assert len(grid._mesa_property_layers) == 1 # Test accessing PropertyLayer from a cell cell = grid._cells[(0, 0)] - assert "elevation" in cell._mesa_property_layers - assert cell.get_property("elevation") == 0 + assert hasattr(cell, "elevation") + assert cell.elevation == 0.0 # Test setting property value for a cell - cell.set_property("elevation", 100) - assert cell.get_property("elevation") == 100 + cell.elevation = 100 + assert cell.elevation == 100 + assert elevation.data[0, 0] == 100 # Test modifying property value for a cell - cell.modify_property("elevation", lambda x: x + 50) - assert cell.get_property("elevation") == 150 + cell.elevation += 50 + assert cell.elevation == 150 + assert elevation.data[0, 0] == 150 - cell.modify_property("elevation", np.add, 50) - assert cell.get_property("elevation") == 200 + cell.elevation = np.add(cell.elevation, 50) + assert cell.elevation == 200 + assert elevation.data[0, 0] == 200 # Test modifying PropertyLayer values grid.set_property("elevation", 100, condition=lambda value: value == 200) - assert cell.get_property("elevation") == 100 + assert cell.elevation == 100 # Test modifying PropertyLayer using numpy operations grid.modify_properties("elevation", np.add, 50) - assert cell.get_property("elevation") == 150 + assert cell.elevation == 150 # Test removing a PropertyLayer grid.remove_property_layer("elevation") - assert "elevation" not in grid.property_layers - assert "elevation" not in cell._mesa_property_layers - - -def test_multiple_property_layers(): - """Test initialization of DiscrateSpace with PropertyLayers.""" - width, height = 5, 5 - elevation = PropertyLayer("elevation", width, height, default_value=0) - temperature = PropertyLayer("temperature", width, height, default_value=20) - - # Test initialization with a single PropertyLayer - grid1 = OrthogonalMooreGrid((width, height), torus=False, random=random.Random(42)) - grid1.add_property_layer(elevation) - assert "elevation" in grid1.property_layers - assert len(grid1.property_layers) == 1 - - # Test initialization with multiple PropertyLayers - grid2 = OrthogonalMooreGrid((width, height), torus=False, random=random.Random(42)) - grid2.add_property_layer(temperature, add_to_cells=False) - grid2.add_property_layer(elevation, add_to_cells=True) - - assert "temperature" in grid2.property_layers - assert "elevation" in grid2.property_layers - assert len(grid2.property_layers) == 2 - - # Modify properties - grid2.modify_properties("elevation", lambda x: x + 10) - grid2.modify_properties("temperature", lambda x: x + 5) - - for cell in grid2.all_cells: - assert cell.get_property("elevation") == 10 - # Assert error temperature, since it was not added to cells - with pytest.raises(KeyError): - cell.get_property("temperature") - - -def test_property_layer_errors(): - """Test error handling for PropertyLayers.""" - width, height = 5, 5 - grid = OrthogonalMooreGrid((width, height), torus=False, random=random.Random(42)) - elevation = PropertyLayer("elevation", width, height, default_value=0) - - # Test adding a PropertyLayer with an existing name - grid.add_property_layer(elevation) - with pytest.raises(ValueError, match="Property layer elevation already exists."): - grid.add_property_layer(elevation) + assert "elevation" not in grid._mesa_property_layers + assert not hasattr(cell, "elevation") +# +# +# def test_multiple_property_layers(): +# """Test initialization of DiscrateSpace with PropertyLayers.""" +# width, height = 5, 5 +# elevation = PropertyLayer("elevation", width, height, default_value=0) +# temperature = PropertyLayer("temperature", width, height, default_value=20) +# +# # Test initialization with a single PropertyLayer +# grid1 = OrthogonalMooreGrid((width, height), torus=False, random=random.Random(42)) +# grid1.add_property_layer(elevation) +# assert "elevation" in grid1.property_layers +# assert len(grid1.property_layers) == 1 +# +# # Test initialization with multiple PropertyLayers +# grid2 = OrthogonalMooreGrid((width, height), torus=False, random=random.Random(42)) +# grid2.add_property_layer(temperature, add_to_cells=False) +# grid2.add_property_layer(elevation, add_to_cells=True) +# +# assert "temperature" in grid2.property_layers +# assert "elevation" in grid2.property_layers +# assert len(grid2.property_layers) == 2 +# +# # Modify properties +# grid2.modify_properties("elevation", lambda x: x + 10) +# grid2.modify_properties("temperature", lambda x: x + 5) +# +# for cell in grid2.all_cells: +# assert cell.get_property("elevation") == 10 +# # Assert error temperature, since it was not added to cells +# with pytest.raises(KeyError): +# cell.get_property("temperature") +# +# +# def test_property_layer_errors(): +# """Test error handling for PropertyLayers.""" +# width, height = 5, 5 +# grid = OrthogonalMooreGrid((width, height), torus=False, random=random.Random(42)) +# elevation = PropertyLayer("elevation", width, height, default_value=0) +# +# # Test adding a PropertyLayer with an existing name +# grid.add_property_layer(elevation) +# with pytest.raises(ValueError, match="Property layer elevation already exists."): +# grid.add_property_layer(elevation) def test_cell_agent(): # noqa: D103 From 57cfae551e617101cb4892526b77b9817669e15a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:11:02 +0000 Subject: [PATCH 05/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/__init__.py | 4 +- .../experimental/cell_space/property_layer.py | 74 ++++++++++++------- tests/test_cell_space.py | 5 +- 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py index 8e1c7160007..6d29405cbfc 100644 --- a/mesa/experimental/cell_space/__init__.py +++ b/mesa/experimental/cell_space/__init__.py @@ -20,8 +20,8 @@ OrthogonalVonNeumannGrid, ) from mesa.experimental.cell_space.network import Network -from mesa.experimental.cell_space.voronoi import VoronoiGrid from mesa.experimental.cell_space.property_layer import PropertyLayer +from mesa.experimental.cell_space.voronoi import VoronoiGrid __all__ = [ "CellCollection", @@ -36,5 +36,5 @@ "OrthogonalVonNeumannGrid", "Network", "VoronoiGrid", - "PropertyLayer" + "PropertyLayer", ] diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 5463c63a458..eb989377c16 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -3,7 +3,7 @@ import inspect import warnings from collections.abc import Callable, Sequence -from typing import Any, TypeVar, TYPE_CHECKING +from typing import Any, TypeVar import numpy as np @@ -31,7 +31,7 @@ class PropertyLayer: propertylayer_experimental_warning_given = False def __init__( - self, name: str, dimensions: Sequence[int], default_value=0.0, dtype=float + self, name: str, dimensions: Sequence[int], default_value=0.0, dtype=float ): """Initializes a new PropertyLayer instance. @@ -90,8 +90,8 @@ def set_cells(self, value, condition: Callable | None = None): condition_result = vectorized_condition(self.data) if ( - not isinstance(condition_result, np.ndarray) - or condition_result.shape != self.data.shape + not isinstance(condition_result, np.ndarray) + or condition_result.shape != self.data.shape ): raise ValueError( "Result of condition must be a NumPy array with the same shape as the grid." @@ -99,7 +99,12 @@ def set_cells(self, value, condition: Callable | None = None): np.copyto(self.data, value, where=condition_result) - def modify_cells(self, operation: Callable, value=None, condition_function: Callable | None = None): + def modify_cells( + self, + operation: Callable, + value=None, + condition_function: Callable | None = None, + ): """Modify cells using an operation, which can be a lambda function or a NumPy ufunc. If a NumPy ufunc is used, an additional value should be provided. @@ -183,7 +188,10 @@ def __init__(self, *args, **kwargs): # self.create_property_layer("empty", True, dtype=bool) def create_property_layer( - self, name: str, default_value=0.0, dtype=float, + self, + name: str, + default_value=0.0, + dtype=float, ): """Add a property layer to the grid. @@ -194,7 +202,9 @@ def create_property_layer( """ # fixme, do we want to have the ability to add both predefined layers # as well as just by name? - layer = PropertyLayer(name, self.dimensions, default_value=default_value, dtype=dtype) + layer = PropertyLayer( + name, self.dimensions, default_value=default_value, dtype=dtype + ) self._mesa_property_layers[name] = layer # fixme: how will this interact with slots and can I dynamically change slots? @@ -211,9 +221,13 @@ def add_property_layer(self, layer: PropertyLayer): """ if layer.dimensions != self.dimensions: - raise ValueError("Dimensions of property layer do not match the dimensions of the grid") + raise ValueError( + "Dimensions of property layer do not match the dimensions of the grid" + ) self._mesa_property_layers[layer.name] = layer - setattr(self.cell_klass, layer.name, PropertyDescriptor(layer)) # fixme: curious to see if this works + setattr( + self.cell_klass, layer.name, PropertyDescriptor(layer) + ) # fixme: curious to see if this works def remove_property_layer(self, property_name: str): """Remove a property layer from the grid. @@ -226,7 +240,7 @@ def remove_property_layer(self, property_name: str): delattr(Cell, property_name) def set_property( - self, property_name: str, value, condition: Callable[[T], bool] | None = None + self, property_name: str, value, condition: Callable[[T], bool] | None = None ): """Set the value of a property for all cells in the grid. @@ -238,11 +252,11 @@ def set_property( self._mesa_property_layers[property_name].set_cells(value, condition) def modify_properties( - self, - property_name: str, - operation: Callable, - value: Any = None, - condition: Callable[[T], bool] | None = None, + 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. @@ -252,10 +266,12 @@ def modify_properties( value: the value to use in the operation condition: a function that takes a cell and returns a boolean (used to filter cells) """ - self._mesa_property_layers[property_name].modify_cells(operation, value, condition) + self._mesa_property_layers[property_name].modify_cells( + operation, value, condition + ) def get_neighborhood_mask( - self, pos: Coordinate, include_center: bool, radius: int + self, pos: Coordinate, include_center: bool, radius: int ) -> np.ndarray: """Generate a boolean mask representing the neighborhood. @@ -267,21 +283,25 @@ def get_neighborhood_mask( Returns: np.ndarray: A boolean mask representing the neighborhood. """ - neighborhood = self._cells[pos].get_neighborhood(include_center=include_center, radius=radius) + neighborhood = self._cells[pos].get_neighborhood( + include_center=include_center, radius=radius + ) mask = np.zeros(self.dimensions, dtype=bool) # Convert the neighborhood list to a NumPy array and use advanced indexing coords = np.array(c.dimenions for c in neighborhood) - mask[coords[:, 0], coords[:, 1]] = True # fixme, must work for nd, so coords must be valid for indexing + mask[coords[:, 0], coords[:, 1]] = ( + True # fixme, must work for nd, so coords must be valid for indexing + ) return mask def select_cells( - self, - conditions: dict | None = None, - extreme_values: dict | None = None, - masks: np.ndarray | list[np.ndarray] = None, - only_empty: bool = False, - return_list: bool = True, + self, + conditions: dict | None = None, + extreme_values: dict | None = None, + masks: np.ndarray | list[np.ndarray] = None, + only_empty: bool = False, + return_list: bool = True, ) -> list[Coordinate] | np.ndarray: """Select cells based on property conditions, extreme values, and/or masks, with an option to only select empty cells. @@ -372,8 +392,8 @@ def __set_name__(self, owner: Cell, name: str): # noqa: D105 def is_single_argument_function(function): """Check if a function is a single argument function.""" return ( - inspect.isfunction(function) - and len(inspect.signature(function).parameters) == 1 + inspect.isfunction(function) + and len(inspect.signature(function).parameters) == 1 ) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 67499fcb41e..cfd6df79d63 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -16,12 +16,11 @@ Network, OrthogonalMooreGrid, OrthogonalVonNeumannGrid, + PropertyLayer, VoronoiGrid, - PropertyLayer ) - def test_orthogonal_grid_neumann(): """Test orthogonal grid with von Neumann neighborhood.""" width = 10 @@ -661,6 +660,8 @@ def test_property_layer_integration(): grid.remove_property_layer("elevation") assert "elevation" not in grid._mesa_property_layers assert not hasattr(cell, "elevation") + + # # # def test_multiple_property_layers(): From a8b4a8451f5ef2dae1566ef109d2a6f8c84d94d0 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 18 Nov 2024 15:12:09 +0100 Subject: [PATCH 06/55] Update cell.py --- mesa/experimental/cell_space/cell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 0c1ff05d8fe..471e6481f95 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -56,8 +56,8 @@ def __init__( self.agents: list[ Agent ] = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) - self.properties = {} # fixme still used by voronoi mesh self.capacity: int | None = capacity + self.properties: dict[Coordinate, object] = {} # fixme still used by voronoi mesh self.random = random def connect(self, other: Cell, key: Coordinate | None = None) -> None: From 3817633dd9c3ca180ee3e5bd7427395af7a9ac23 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 18 Nov 2024 17:16:10 +0100 Subject: [PATCH 07/55] typo fixes --- mesa/experimental/cell_space/property_layer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index eb989377c16..5aa04384595 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -179,8 +179,8 @@ def __init__(self, *args, **kwargs): # Initialize an empty mask as a boolean NumPy array # fixme: in space.py this is a boolean mask with empty and non empty cells - # this does not easily translate (unless we handle it in the celll, via add and remove agents?) - # but then we might better treat this as a default property layer that is just allways added + # this does not easily translate (unless we handle it in the cell, via add and remove agents?) + # but then we might better treat this as a default property layer that is just always added # # fixme this won't work at the moment with the descriptor like access..... # we might make some modifications to is_empty? @@ -289,9 +289,9 @@ def get_neighborhood_mask( mask = np.zeros(self.dimensions, dtype=bool) # Convert the neighborhood list to a NumPy array and use advanced indexing - coords = np.array(c.dimenions for c in neighborhood) + coords = np.array(c.dimensions for c in neighborhood) mask[coords[:, 0], coords[:, 1]] = ( - True # fixme, must work for nd, so coords must be valid for indexing + True # fixme, must work for n dimensions, so coords must be valid for indexing ) return mask From 67c8af2194415948e57e2e0034bef4842c8ac1e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:16:51 +0000 Subject: [PATCH 08/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/cell.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 471e6481f95..32a7ffe2d61 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -57,7 +57,9 @@ def __init__( Agent ] = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, ) self.capacity: int | None = capacity - self.properties: dict[Coordinate, object] = {} # fixme still used by voronoi mesh + self.properties: dict[ + Coordinate, object + ] = {} # fixme still used by voronoi mesh self.random = random def connect(self, other: Cell, key: Coordinate | None = None) -> None: From a4718a6cafbb7e4e1b4537b1d381354c4a61429d Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 18 Nov 2024 17:25:20 +0100 Subject: [PATCH 09/55] fix mpl_space_drawing tests --- mesa/visualization/mpl_space_drawing.py | 4 ++-- tests/test_components_matplotlib.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mesa/visualization/mpl_space_drawing.py b/mesa/visualization/mpl_space_drawing.py index ea7687c4210..d368da9c1c2 100644 --- a/mesa/visualization/mpl_space_drawing.py +++ b/mesa/visualization/mpl_space_drawing.py @@ -178,7 +178,7 @@ def draw_property_layers( property_layers = space.properties except AttributeError: # new style spaces - property_layers = space.property_layers + property_layers = space._mesa_property_layers for layer_name, portrayal in propertylayer_portrayal.items(): layer = property_layers.get(layer_name, None) @@ -186,7 +186,7 @@ def draw_property_layers( continue data = layer.data.astype(float) if layer.data.dtype == bool else layer.data - width, height = data.shape if space is None else (space.width, space.height) + width, height = data.shape # if space is None else (space.width, space.height) if space and data.shape != (width, height): warnings.warn( diff --git a/tests/test_components_matplotlib.py b/tests/test_components_matplotlib.py index f258b58d90b..7a83f0e34f6 100644 --- a/tests/test_components_matplotlib.py +++ b/tests/test_components_matplotlib.py @@ -231,7 +231,7 @@ def test_draw_property_layers(): model = Model(seed=42) grid = OrthogonalMooreGrid((10, 10), torus=True, random=model.random, capacity=1) - grid.add_property_layer(PropertyLayer("test", grid.width, grid.height, 0)) + grid.create_property_layer("test",0.0) fig, ax = plt.subplots() draw_property_layers(grid, {"test": {"colormap": "viridis", "colorbar": True}}, ax) From 46e6957e43949334c3caadb146c00cde224ffe72 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:26:43 +0000 Subject: [PATCH 10/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/visualization/mpl_space_drawing.py | 2 +- tests/test_components_matplotlib.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mesa/visualization/mpl_space_drawing.py b/mesa/visualization/mpl_space_drawing.py index d368da9c1c2..5fc7578a34e 100644 --- a/mesa/visualization/mpl_space_drawing.py +++ b/mesa/visualization/mpl_space_drawing.py @@ -186,7 +186,7 @@ def draw_property_layers( continue data = layer.data.astype(float) if layer.data.dtype == bool else layer.data - width, height = data.shape # if space is None else (space.width, space.height) + width, height = data.shape # if space is None else (space.width, space.height) if space and data.shape != (width, height): warnings.warn( diff --git a/tests/test_components_matplotlib.py b/tests/test_components_matplotlib.py index 7a83f0e34f6..8b773464dea 100644 --- a/tests/test_components_matplotlib.py +++ b/tests/test_components_matplotlib.py @@ -231,7 +231,7 @@ def test_draw_property_layers(): model = Model(seed=42) grid = OrthogonalMooreGrid((10, 10), torus=True, random=model.random, capacity=1) - grid.create_property_layer("test",0.0) + grid.create_property_layer("test", 0.0) fig, ax = plt.subplots() draw_property_layers(grid, {"test": {"colormap": "viridis", "colorbar": True}}, ax) From 738f251a116c3c8da8824995314b9b5a568034f4 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 18 Nov 2024 17:34:34 +0100 Subject: [PATCH 11/55] reenable second set of tests --- tests/test_cell_space.py | 59 ++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index cfd6df79d63..d2dceebdbe7 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -661,41 +661,36 @@ def test_property_layer_integration(): assert "elevation" not in grid._mesa_property_layers assert not hasattr(cell, "elevation") - -# -# -# def test_multiple_property_layers(): -# """Test initialization of DiscrateSpace with PropertyLayers.""" -# width, height = 5, 5 -# elevation = PropertyLayer("elevation", width, height, default_value=0) -# temperature = PropertyLayer("temperature", width, height, default_value=20) -# -# # Test initialization with a single PropertyLayer -# grid1 = OrthogonalMooreGrid((width, height), torus=False, random=random.Random(42)) -# grid1.add_property_layer(elevation) -# assert "elevation" in grid1.property_layers -# assert len(grid1.property_layers) == 1 -# -# # Test initialization with multiple PropertyLayers -# grid2 = OrthogonalMooreGrid((width, height), torus=False, random=random.Random(42)) -# grid2.add_property_layer(temperature, add_to_cells=False) -# grid2.add_property_layer(elevation, add_to_cells=True) -# -# assert "temperature" in grid2.property_layers -# assert "elevation" in grid2.property_layers -# assert len(grid2.property_layers) == 2 -# -# # Modify properties -# grid2.modify_properties("elevation", lambda x: x + 10) -# grid2.modify_properties("temperature", lambda x: x + 5) +def test_multiple_property_layers(): + """Test initialization of DiscrateSpace with PropertyLayers.""" + dimensions = (5, 5) + elevation = PropertyLayer("elevation", dimensions, default_value=0.0) + temperature = PropertyLayer("temperature", dimensions, default_value=20.0) # -# for cell in grid2.all_cells: -# assert cell.get_property("elevation") == 10 -# # Assert error temperature, since it was not added to cells -# with pytest.raises(KeyError): -# cell.get_property("temperature") + # Test initialization with a single PropertyLayer + grid1 = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) + grid1.add_property_layer(elevation) + assert "elevation" in grid1._mesa_property_layers + assert len(grid1._mesa_property_layers) == 1 # + # Test initialization with multiple PropertyLayers + grid2 = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) + grid2.add_property_layer(temperature) + grid2.add_property_layer(elevation) # + assert "temperature" in grid2._mesa_property_layers + assert "elevation" in grid2._mesa_property_layers + assert len(grid2._mesa_property_layers) == 2 + + # Modify properties + grid2.modify_properties("elevation", lambda x: x + 10) + grid2.modify_properties("temperature", lambda x: x + 5) + + for cell in grid2.all_cells: + assert cell.elevation == 10 + assert cell.temperature == 25 + + # def test_property_layer_errors(): # """Test error handling for PropertyLayers.""" # width, height = 5, 5 From b1d257788be9655eef2cb10eaecc1228aaa2f149 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 18 Nov 2024 17:40:10 +0100 Subject: [PATCH 12/55] reenable last set of tests --- .../experimental/cell_space/property_layer.py | 3 +++ tests/test_cell_space.py | 27 ++++++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 5aa04384595..2c3e3ded7e2 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -224,6 +224,9 @@ def add_property_layer(self, layer: PropertyLayer): raise ValueError( "Dimensions of property layer do not match the dimensions of the grid" ) + if layer.name in self._mesa_property_layers: + raise ValueError(f"Property layer {layer.name} already exists.") + self._mesa_property_layers[layer.name] = layer setattr( self.cell_klass, layer.name, PropertyDescriptor(layer) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index d2dceebdbe7..126670d4e2c 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -691,16 +691,23 @@ def test_multiple_property_layers(): assert cell.temperature == 25 -# def test_property_layer_errors(): -# """Test error handling for PropertyLayers.""" -# width, height = 5, 5 -# grid = OrthogonalMooreGrid((width, height), torus=False, random=random.Random(42)) -# elevation = PropertyLayer("elevation", width, height, default_value=0) -# -# # Test adding a PropertyLayer with an existing name -# grid.add_property_layer(elevation) -# with pytest.raises(ValueError, match="Property layer elevation already exists."): -# grid.add_property_layer(elevation) +def test_property_layer_errors(): + """Test error handling for PropertyLayers.""" + dimensions = 5, 5 + grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) + elevation = PropertyLayer("elevation",dimensions, default_value=0.0) + + # Test adding a PropertyLayer with an existing name + grid.add_property_layer(elevation) + with pytest.raises(ValueError, match="Property layer elevation already exists."): + grid.add_property_layer(elevation) + + # test adding a layer with different dimensions than space + dimensions = 5, 5 + grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) + elevation = PropertyLayer("elevation", (10, 10), default_value=0.0) + with pytest.raises(ValueError, match="Dimensions of property layer do not match the dimensions of the grid"): + grid.add_property_layer(elevation) def test_cell_agent(): # noqa: D103 From 73c37e79de607e30739da9e6aa083275aa9ed91a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:40:46 +0000 Subject: [PATCH 13/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_cell_space.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 126670d4e2c..09adeabda23 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -661,23 +661,24 @@ def test_property_layer_integration(): assert "elevation" not in grid._mesa_property_layers assert not hasattr(cell, "elevation") + def test_multiple_property_layers(): """Test initialization of DiscrateSpace with PropertyLayers.""" dimensions = (5, 5) elevation = PropertyLayer("elevation", dimensions, default_value=0.0) temperature = PropertyLayer("temperature", dimensions, default_value=20.0) -# + # # Test initialization with a single PropertyLayer grid1 = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) grid1.add_property_layer(elevation) assert "elevation" in grid1._mesa_property_layers assert len(grid1._mesa_property_layers) == 1 -# + # # Test initialization with multiple PropertyLayers grid2 = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) grid2.add_property_layer(temperature) grid2.add_property_layer(elevation) -# + # assert "temperature" in grid2._mesa_property_layers assert "elevation" in grid2._mesa_property_layers assert len(grid2._mesa_property_layers) == 2 @@ -695,7 +696,7 @@ def test_property_layer_errors(): """Test error handling for PropertyLayers.""" dimensions = 5, 5 grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) - elevation = PropertyLayer("elevation",dimensions, default_value=0.0) + elevation = PropertyLayer("elevation", dimensions, default_value=0.0) # Test adding a PropertyLayer with an existing name grid.add_property_layer(elevation) @@ -706,7 +707,10 @@ def test_property_layer_errors(): dimensions = 5, 5 grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) elevation = PropertyLayer("elevation", (10, 10), default_value=0.0) - with pytest.raises(ValueError, match="Dimensions of property layer do not match the dimensions of the grid"): + with pytest.raises( + ValueError, + match="Dimensions of property layer do not match the dimensions of the grid", + ): grid.add_property_layer(elevation) From bb6c06e77d0a50434c324be91f917bb02272f736 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 19 Nov 2024 03:47:38 +0100 Subject: [PATCH 14/55] make mask work --- mesa/experimental/cell_space/property_layer.py | 16 ++++++---------- tests/test_cell_space.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 2c3e3ded7e2..abbe6437180 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -274,7 +274,7 @@ def modify_properties( ) def get_neighborhood_mask( - self, pos: Coordinate, include_center: bool, radius: int + self, coordinate: Coordinate, include_center: bool=True, radius: int=1 ) -> np.ndarray: """Generate a boolean mask representing the neighborhood. @@ -286,16 +286,16 @@ def get_neighborhood_mask( Returns: np.ndarray: A boolean mask representing the neighborhood. """ - neighborhood = self._cells[pos].get_neighborhood( + cell = self._cells[coordinate] + neighborhood = cell.get_neighborhood( include_center=include_center, radius=radius ) mask = np.zeros(self.dimensions, dtype=bool) # Convert the neighborhood list to a NumPy array and use advanced indexing - coords = np.array(c.dimensions for c in neighborhood) - mask[coords[:, 0], coords[:, 1]] = ( - True # fixme, must work for n dimensions, so coords must be valid for indexing - ) + coords = np.array(list(c.coordinate for c in neighborhood)) + indices = [coords[:, i] for i in range(coords.shape[1])] + mask[*indices] = True return mask def select_cells( @@ -387,10 +387,6 @@ def __get__(self, instance: Cell, owner): # noqa: D105 def __set__(self, instance: Cell, value): # noqa: D105 self.layer.data[instance.coordinate] = value - def __set_name__(self, owner: Cell, name: str): # noqa: D105 - self.public_name = name - self.private_name = f"_{name}" - def is_single_argument_function(function): """Check if a function is a single argument function.""" diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 09adeabda23..b1553437a69 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -691,6 +691,16 @@ def test_multiple_property_layers(): assert cell.elevation == 10 assert cell.temperature == 25 +# test masks ets +def test_property_layer_masks(): + dimensions = (5, 5) + grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) + grid.create_property_layer("elevation", default_value=0.0) + + # elevation.select_cells() + grid.get_neighborhood_mask((2, 2)) +# + def test_property_layer_errors(): """Test error handling for PropertyLayers.""" From f6d6bc6fa6192a07b32bb4113a4184424c053dbd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 02:49:24 +0000 Subject: [PATCH 15/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/property_layer.py | 2 +- tests/test_cell_space.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index abbe6437180..ba694db13f9 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -274,7 +274,7 @@ def modify_properties( ) def get_neighborhood_mask( - self, coordinate: Coordinate, include_center: bool=True, radius: int=1 + self, coordinate: Coordinate, include_center: bool = True, radius: int = 1 ) -> np.ndarray: """Generate a boolean mask representing the neighborhood. diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index b1553437a69..3126aa8bb87 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -691,6 +691,7 @@ def test_multiple_property_layers(): assert cell.elevation == 10 assert cell.temperature == 25 + # test masks ets def test_property_layer_masks(): dimensions = (5, 5) @@ -699,6 +700,8 @@ def test_property_layer_masks(): # elevation.select_cells() grid.get_neighborhood_mask((2, 2)) + + # From 5efa89fce06ea6196888478afeedb8a9390a2648 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 19 Nov 2024 03:57:13 +0100 Subject: [PATCH 16/55] precommit fixes --- .../experimental/cell_space/property_layer.py | 8 +++---- tests/test_cell_space.py | 21 +++++++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index ba694db13f9..89aa84b2ff6 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -279,9 +279,9 @@ def get_neighborhood_mask( """Generate a boolean mask representing the neighborhood. Args: - pos (Coordinate): Center of the neighborhood. - include_center (bool): Include the central cell in the neighborhood. - radius (int): The radius of the neighborhood. + coordinate: Center of the neighborhood. + include_center: Include the central cell in the neighborhood. + radius: The radius of the neighborhood. Returns: np.ndarray: A boolean mask representing the neighborhood. @@ -293,7 +293,7 @@ def get_neighborhood_mask( mask = np.zeros(self.dimensions, dtype=bool) # Convert the neighborhood list to a NumPy array and use advanced indexing - coords = np.array(list(c.coordinate for c in neighborhood)) + coords = np.array([c.coordinate for c in neighborhood]) indices = [coords[:, i] for i in range(coords.shape[1])] mask[*indices] = True return mask diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 3126aa8bb87..da0403b4fb1 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -692,17 +692,30 @@ def test_multiple_property_layers(): assert cell.temperature == 25 -# test masks ets -def test_property_layer_masks(): +def test_get_neighborhood_mask(): + """Test get_neighborhood_mask.""" dimensions = (5, 5) grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) grid.create_property_layer("elevation", default_value=0.0) - # elevation.select_cells() grid.get_neighborhood_mask((2, 2)) + mask = grid.get_neighborhood_mask((2, 2)) + for cell in grid._cells[(2,2)].connections.values(): + assert mask[cell.coordinate] + assert mask[grid._cells[(2,2)].coordinate] -# + mask = grid.get_neighborhood_mask((2, 2), include_center=False) + for cell in grid._cells[(2,2)].connections.values(): + assert mask[cell.coordinate] + assert mask[grid._cells[(2,2)].coordinate] == False + + +def test_select_cells(): + """test select_cells.""" + dimensions = (5, 5) + grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) + grid.create_property_layer("elevation", default_value=0.0) def test_property_layer_errors(): From 02c07adcdf0ee6dd82f76860ebd25c3bf6a9b82a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 02:58:43 +0000 Subject: [PATCH 17/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_cell_space.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index da0403b4fb1..e769cb39c5e 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -701,18 +701,18 @@ def test_get_neighborhood_mask(): grid.get_neighborhood_mask((2, 2)) mask = grid.get_neighborhood_mask((2, 2)) - for cell in grid._cells[(2,2)].connections.values(): + for cell in grid._cells[(2, 2)].connections.values(): assert mask[cell.coordinate] - assert mask[grid._cells[(2,2)].coordinate] + assert mask[grid._cells[(2, 2)].coordinate] mask = grid.get_neighborhood_mask((2, 2), include_center=False) - for cell in grid._cells[(2,2)].connections.values(): + for cell in grid._cells[(2, 2)].connections.values(): assert mask[cell.coordinate] - assert mask[grid._cells[(2,2)].coordinate] == False + assert mask[grid._cells[(2, 2)].coordinate] == False def test_select_cells(): - """test select_cells.""" + """Test select_cells.""" dimensions = (5, 5) grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) grid.create_property_layer("elevation", default_value=0.0) From b11eaec852103829a2a37ba3ff1b20e460230703 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 19 Nov 2024 04:00:06 +0100 Subject: [PATCH 18/55] Update test_cell_space.py --- tests/test_cell_space.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index e769cb39c5e..55175353aab 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -708,7 +708,7 @@ def test_get_neighborhood_mask(): mask = grid.get_neighborhood_mask((2, 2), include_center=False) for cell in grid._cells[(2, 2)].connections.values(): assert mask[cell.coordinate] - assert mask[grid._cells[(2, 2)].coordinate] == False + assert not mask[grid._cells[(2, 2)].coordinate] def test_select_cells(): From 64edf8f426b4b3dc7a669c6165091992e5171baa Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 19 Nov 2024 18:45:46 +0100 Subject: [PATCH 19/55] make empty property layer work --- mesa/experimental/cell_space/cell.py | 2 ++ mesa/experimental/cell_space/grid.py | 4 +++- mesa/experimental/cell_space/property_layer.py | 10 +--------- tests/test_cell_space.py | 10 +++++----- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 32a7ffe2d61..8bf2058caaf 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -93,6 +93,7 @@ def add_agent(self, agent: CellAgent) -> None: """ n = len(self.agents) + self.empty = False if self.capacity and n >= self.capacity: raise Exception( @@ -109,6 +110,7 @@ def remove_agent(self, agent: CellAgent) -> None: """ self.agents.remove(agent) + self.empty = self.is_empty @property def is_empty(self) -> bool: diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 30c3410e1a4..a0908c04788 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -61,14 +61,16 @@ def __init__( self._try_random = True self._ndims = len(dimensions) self._validate_parameters() + self.cell_klass = type('GridCell', (self.cell_klass,), {}) # fixme name needs to dynamic to support multiple grids in parallel coordinates = product(*(range(dim) for dim in self.dimensions)) self._cells = { - coord: cell_klass(coord, capacity, random=self.random) + coord: self.cell_klass(coord, capacity, random=self.random) for coord in coordinates } self._connect_cells() + self.create_property_layer("empty", default_value=True, dtype=bool) def _connect_cells(self) -> None: if self._ndims == 2: diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 89aa84b2ff6..c3f7a26e0b2 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -240,7 +240,7 @@ def remove_property_layer(self, property_name: str): remove_from_cells: whether to remove the property layer from all cells (default: True) """ del self._mesa_property_layers[property_name] - delattr(Cell, property_name) + delattr(self.cell_klass, property_name) def set_property( self, property_name: str, value, condition: Callable[[T], bool] | None = None @@ -388,14 +388,6 @@ def __set__(self, instance: Cell, value): # noqa: D105 self.layer.data[instance.coordinate] = value -def is_single_argument_function(function): - """Check if a function is a single argument function.""" - return ( - inspect.isfunction(function) - and len(inspect.signature(function).parameters) == 1 - ) - - def ufunc_requires_additional_input(ufunc): # noqa: D103 # NumPy ufuncs have a 'nargs' attribute indicating the number of input arguments # For binary ufuncs (like np.add), nargs is 2 diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 55175353aab..5a1541b2126 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -627,7 +627,7 @@ def test_property_layer_integration(): elevation = PropertyLayer("elevation", dimensions, default_value=0.0) grid.add_property_layer(elevation) assert "elevation" in grid._mesa_property_layers - assert len(grid._mesa_property_layers) == 1 + assert len(grid._mesa_property_layers) == 2 ## empty is allways there # Test accessing PropertyLayer from a cell cell = grid._cells[(0, 0)] @@ -667,13 +667,13 @@ def test_multiple_property_layers(): dimensions = (5, 5) elevation = PropertyLayer("elevation", dimensions, default_value=0.0) temperature = PropertyLayer("temperature", dimensions, default_value=20.0) - # + # Test initialization with a single PropertyLayer grid1 = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) grid1.add_property_layer(elevation) assert "elevation" in grid1._mesa_property_layers - assert len(grid1._mesa_property_layers) == 1 - # + assert len(grid1._mesa_property_layers) == 2 ## empty is already there + # Test initialization with multiple PropertyLayers grid2 = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) grid2.add_property_layer(temperature) @@ -681,7 +681,7 @@ def test_multiple_property_layers(): # assert "temperature" in grid2._mesa_property_layers assert "elevation" in grid2._mesa_property_layers - assert len(grid2._mesa_property_layers) == 2 + assert len(grid2._mesa_property_layers) == 3 # Modify properties grid2.modify_properties("elevation", lambda x: x + 10) From b33a2756adc6eb35266fd1c158e9ec7678173650 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 17:48:28 +0000 Subject: [PATCH 20/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/grid.py | 4 +++- mesa/experimental/cell_space/property_layer.py | 1 - tests/test_cell_space.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index a0908c04788..106edf46481 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -61,7 +61,9 @@ def __init__( self._try_random = True self._ndims = len(dimensions) self._validate_parameters() - self.cell_klass = type('GridCell', (self.cell_klass,), {}) # fixme name needs to dynamic to support multiple grids in parallel + self.cell_klass = type( + "GridCell", (self.cell_klass,), {} + ) # fixme name needs to dynamic to support multiple grids in parallel coordinates = product(*(range(dim) for dim in self.dimensions)) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index c3f7a26e0b2..1c6f9dfe82f 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -1,6 +1,5 @@ """This module provides functionality for working with property layers in grids.""" -import inspect import warnings from collections.abc import Callable, Sequence from typing import Any, TypeVar diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 5a1541b2126..5711e73c25d 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -627,7 +627,7 @@ def test_property_layer_integration(): elevation = PropertyLayer("elevation", dimensions, default_value=0.0) grid.add_property_layer(elevation) assert "elevation" in grid._mesa_property_layers - assert len(grid._mesa_property_layers) == 2 ## empty is allways there + assert len(grid._mesa_property_layers) == 2 ## empty is allways there # Test accessing PropertyLayer from a cell cell = grid._cells[(0, 0)] From 1f6a5c2d77f952aa1ca0c12b84de0a192d7fbf06 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 19 Nov 2024 18:56:22 +0100 Subject: [PATCH 21/55] Update test_cell_space.py --- tests/test_cell_space.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 5711e73c25d..28c0799a1a0 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -627,7 +627,7 @@ def test_property_layer_integration(): elevation = PropertyLayer("elevation", dimensions, default_value=0.0) grid.add_property_layer(elevation) assert "elevation" in grid._mesa_property_layers - assert len(grid._mesa_property_layers) == 2 ## empty is allways there + assert len(grid._mesa_property_layers) == 2 ## empty is always there # Test accessing PropertyLayer from a cell cell = grid._cells[(0, 0)] From 6966d0004e78be2d88c8b6834dc40a4bb6570fcf Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 23 Nov 2024 16:24:20 +0100 Subject: [PATCH 22/55] minor tweaks --- .../experimental/cell_space/property_layer.py | 24 ++++++++++++++--- tests/test_cell_space.py | 26 ++++++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 1c6f9dfe82f..fb9c3705823 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -6,7 +6,7 @@ import numpy as np -from .cell import Cell +from mesa.experimental.cell_space import Cell Coordinate = Sequence[int] T = TypeVar("T", bound=Cell) @@ -69,6 +69,24 @@ def __init__( ) self.__class__.propertylayer_experimental_warning_given = True + + @classmethod + def from_data(cls, name:str, data:np.ndarray): + """create a property layer from a NumPy array. + + Args: + name: The name of the property layer. + data: A NumPy array representing the grid data. + + """ + + layer = cls(name, data.shape, default_value=data[*[0 for _ in range(len(data.shape))]], dtype=data.dtype.type) + layer.set_cells(data) + return layer + + + + def set_cells(self, value, condition: Callable | None = None): """Perform a batch update either on the entire grid or conditionally, in-place. @@ -334,8 +352,8 @@ def select_cells( combined_mask = np.logical_and(combined_mask, masks) # Apply the empty mask if only_empty is True - if only_empty: # fixme does not currently work - combined_mask = np.logical_and(combined_mask, self.empty_mask) + if only_empty: + combined_mask = np.logical_and(combined_mask, self._mesa_property_layers["empty"]) # Apply conditions if conditions: diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 28c0799a1a0..bd7213ee173 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -715,8 +715,32 @@ def test_select_cells(): """Test select_cells.""" dimensions = (5, 5) grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) - grid.create_property_layer("elevation", default_value=0.0) + data = np.random.default_rng(12456).random((5,5)) + grid.add_property_layer(PropertyLayer.from_data("elevation", data)) + + # fixme, add an agent and update the np.all test accordingly + mask = grid.select_cells(conditions={"elevation": lambda x: x > 0.5}, return_list=False, only_empty=True) + assert mask.shape == (5, 5) + assert np.all(mask == (data > 0.5)) + + mask = grid.select_cells(conditions={"elevation": lambda x: x > 0.5}, return_list=False, only_empty=False) + assert mask.shape == (5, 5) + assert np.all(mask == (data > 0.5)) + + # fixme add extreme_values heighest and lowest + mask = grid.select_cells(extreme_values={'elevation' : 'highest'}, return_list=False, only_empty=False) + assert mask.shape == (5, 5) + assert np.all(mask == (data==data.max())) + + mask = grid.select_cells(extreme_values={'elevation' : 'lowest'}, return_list=False, only_empty=False) + assert mask.shape == (5, 5) + assert np.all(mask == (data==data.min())) + + with pytest.raises(ValueError): + grid.select_cells(extreme_values={'elevation': 'weird'}, return_list=False, only_empty=False) + + # fixme add pre-specified mask to any other option def test_property_layer_errors(): """Test error handling for PropertyLayers.""" From 4872a9ac969a12cad542293983a666c152b8cecc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:24:50 +0000 Subject: [PATCH 23/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../experimental/cell_space/property_layer.py | 20 +++++++------- tests/test_cell_space.py | 27 +++++++++++++------ 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index fb9c3705823..e7426d4f485 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -69,24 +69,24 @@ def __init__( ) self.__class__.propertylayer_experimental_warning_given = True - @classmethod - def from_data(cls, name:str, data:np.ndarray): - """create a property layer from a NumPy array. + def from_data(cls, name: str, data: np.ndarray): + """Create a property layer from a NumPy array. Args: name: The name of the property layer. data: A NumPy array representing the grid data. """ - - layer = cls(name, data.shape, default_value=data[*[0 for _ in range(len(data.shape))]], dtype=data.dtype.type) + layer = cls( + name, + data.shape, + default_value=data[*[0 for _ in range(len(data.shape))]], + dtype=data.dtype.type, + ) layer.set_cells(data) return layer - - - def set_cells(self, value, condition: Callable | None = None): """Perform a batch update either on the entire grid or conditionally, in-place. @@ -353,7 +353,9 @@ def select_cells( # Apply the empty mask if only_empty is True if only_empty: - combined_mask = np.logical_and(combined_mask, self._mesa_property_layers["empty"]) + combined_mask = np.logical_and( + combined_mask, self._mesa_property_layers["empty"] + ) # Apply conditions if conditions: diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index bd7213ee173..32b66357aa2 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -716,32 +716,43 @@ def test_select_cells(): dimensions = (5, 5) grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) - data = np.random.default_rng(12456).random((5,5)) + data = np.random.default_rng(12456).random((5, 5)) grid.add_property_layer(PropertyLayer.from_data("elevation", data)) # fixme, add an agent and update the np.all test accordingly - mask = grid.select_cells(conditions={"elevation": lambda x: x > 0.5}, return_list=False, only_empty=True) + mask = grid.select_cells( + conditions={"elevation": lambda x: x > 0.5}, return_list=False, only_empty=True + ) assert mask.shape == (5, 5) assert np.all(mask == (data > 0.5)) - mask = grid.select_cells(conditions={"elevation": lambda x: x > 0.5}, return_list=False, only_empty=False) + mask = grid.select_cells( + conditions={"elevation": lambda x: x > 0.5}, return_list=False, only_empty=False + ) assert mask.shape == (5, 5) assert np.all(mask == (data > 0.5)) # fixme add extreme_values heighest and lowest - mask = grid.select_cells(extreme_values={'elevation' : 'highest'}, return_list=False, only_empty=False) + mask = grid.select_cells( + extreme_values={"elevation": "highest"}, return_list=False, only_empty=False + ) assert mask.shape == (5, 5) - assert np.all(mask == (data==data.max())) + assert np.all(mask == (data == data.max())) - mask = grid.select_cells(extreme_values={'elevation' : 'lowest'}, return_list=False, only_empty=False) + mask = grid.select_cells( + extreme_values={"elevation": "lowest"}, return_list=False, only_empty=False + ) assert mask.shape == (5, 5) - assert np.all(mask == (data==data.min())) + assert np.all(mask == (data == data.min())) with pytest.raises(ValueError): - grid.select_cells(extreme_values={'elevation': 'weird'}, return_list=False, only_empty=False) + grid.select_cells( + extreme_values={"elevation": "weird"}, return_list=False, only_empty=False + ) # fixme add pre-specified mask to any other option + def test_property_layer_errors(): """Test error handling for PropertyLayers.""" dimensions = 5, 5 From fc72b6cc3b23ee5c4c665f7e5f32234988305315 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 23 Nov 2024 16:25:06 +0100 Subject: [PATCH 24/55] Update property_layer.py --- mesa/experimental/cell_space/property_layer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index e7426d4f485..ce502b27ce0 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -22,11 +22,10 @@ class PropertyLayer: dimensions: The width of the grid (number of columns). data: A NumPy array representing the grid data. + """ # Fixme do we need this class at all? # what does it add to just a numpy array? - """ - propertylayer_experimental_warning_given = False def __init__( From 6975781237ec0a4d0c00f31db733f916b2949550 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:25:23 +0000 Subject: [PATCH 25/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/property_layer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index ce502b27ce0..890ac1759d9 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -23,6 +23,7 @@ class PropertyLayer: data: A NumPy array representing the grid data. """ + # Fixme do we need this class at all? # what does it add to just a numpy array? From 8acc4874462c1e497feab779ee0b963886aa75d5 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sat, 23 Nov 2024 16:28:56 +0100 Subject: [PATCH 26/55] cleanup --- mesa/experimental/cell_space/property_layer.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 890ac1759d9..e8b8871f53f 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -193,16 +193,6 @@ def __init__(self, *args, **kwargs): """Initialize a HasPropertyLayers instance.""" super().__init__(*args, **kwargs) self._mesa_property_layers = {} - # Initialize an empty mask as a boolean NumPy array - - # fixme: in space.py this is a boolean mask with empty and non empty cells - # this does not easily translate (unless we handle it in the cell, via add and remove agents?) - # but then we might better treat this as a default property layer that is just always added - # - # fixme this won't work at the moment with the descriptor like access..... - # we might make some modifications to is_empty? - # I cannot add a layer here because the init of the other classes has not yet completed - # self.create_property_layer("empty", True, dtype=bool) def create_property_layer( self, @@ -222,10 +212,7 @@ def create_property_layer( layer = PropertyLayer( name, self.dimensions, default_value=default_value, dtype=dtype ) - self._mesa_property_layers[name] = layer - - # fixme: how will this interact with slots and can I dynamically change slots? - setattr(self.cell_klass, name, PropertyDescriptor(layer)) + self.add_property_layer(layer) def add_property_layer(self, layer: PropertyLayer): """Add a predefined property layer to the grid. From a4943350e3c670868277ccd21eac3d559a705771 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 24 Nov 2024 20:17:07 +0100 Subject: [PATCH 27/55] various additional tests --- .../experimental/cell_space/property_layer.py | 36 ++++--------- tests/test_cell_space.py | 50 ++++++++++++++++++- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index e8b8871f53f..ce21f2a1a02 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -92,35 +92,20 @@ def set_cells(self, value, condition: Callable | None = None): Args: value: The value to be used for the update. - condition: (Optional) A callable (like a lambda function or a NumPy ufunc) - that returns a boolean array when applied to the data. + condition: (Optional) A callable that returns a boolean array when applied to the data. """ if condition is None: np.copyto(self.data, value) # In-place update else: - if isinstance(condition, np.ufunc): - # Directly apply NumPy ufunc - condition_result = condition(self.data) - else: - # Vectorize non-ufunc conditions - vectorized_condition = np.vectorize(condition) - condition_result = vectorized_condition(self.data) - - if ( - not isinstance(condition_result, np.ndarray) - or condition_result.shape != self.data.shape - ): - raise ValueError( - "Result of condition must be a NumPy array with the same shape as the grid." - ) - + vectorized_condition = np.vectorize(condition) + condition_result = vectorized_condition(self.data) np.copyto(self.data, value, where=condition_result) def modify_cells( self, operation: Callable, value=None, - condition_function: Callable | None = None, + condition: Callable | None = None, ): """Modify cells using an operation, which can be a lambda function or a NumPy ufunc. @@ -129,17 +114,14 @@ def modify_cells( Args: operation: A function to apply. Can be a lambda function or a NumPy ufunc. value: The value to be used if the operation is a NumPy ufunc. Ignored for lambda functions. - condition_function: (Optional) A callable that returns a boolean array when applied to the data. + condition: (Optional) A callable that returns a boolean array when applied to the data. """ condition_array = np.ones_like( self.data, dtype=bool ) # Default condition (all cells) - if condition_function is not None: - if isinstance(condition_function, np.ufunc): - condition_array = condition_function(self.data) - else: - vectorized_condition = np.vectorize(condition_function) - condition_array = vectorized_condition(self.data) + if condition is not None: + vectorized_condition = np.vectorize(condition) + condition_array = vectorized_condition(self.data) # Check if the operation is a lambda function or a NumPy ufunc if isinstance(operation, np.ufunc): @@ -176,7 +158,7 @@ def select_cells(self, condition: Callable, return_list=True): else: return condition_array - def aggregate_property(self, operation: Callable): + def aggregate(self, operation: Callable): """Perform an aggregate operation (e.g., sum, mean) on a property across all cells. Args: diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 32b66357aa2..2b7d7330ca3 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -732,7 +732,7 @@ def test_select_cells(): assert mask.shape == (5, 5) assert np.all(mask == (data > 0.5)) - # fixme add extreme_values heighest and lowest + # fixme add extreme_values highest and lowest mask = grid.select_cells( extreme_values={"elevation": "highest"}, return_list=False, only_empty=False ) @@ -753,6 +753,51 @@ def test_select_cells(): # fixme add pre-specified mask to any other option +def test_property_layer(): + """Test various property layer methods.""" + elevation = PropertyLayer("elevation", (5,5), default_value=0.0) + + # test set_cells + elevation.set_cells(10) + assert np.all(elevation.data==10) + + elevation.set_cells(np.ones((5,5))) + assert np.all(elevation.data==1) + + with pytest.raises(ValueError): + elevation.set_cells(np.ones((6, 6))) + + data = np.random.default_rng(42).random((5, 5)) + layer = PropertyLayer.from_data("some_name", data) + + def condition(x): + return x > 0.5 + + layer.set_cells(1, condition=condition) + assert np.all((layer.data == 1) == (data > 0.5)) + + # modify_cells + layer.data = np.zeros((10, 10)) + layer.modify_cells(lambda x: x + 2) + assert np.all(layer.data == 2) + + layer.data = np.ones((10, 10)) + layer.modify_cells(np.multiply, 3) + assert np.all(layer.data[3, 3] == 3) + + data = np.random.default_rng(42).random((5, 5)) + layer.data = np.random.default_rng(42).random((5, 5)) + layer.modify_cells(np.add, value=3, condition=condition) + assert np.all((layer.data > 3.5) == (data > 0.5)) + + with pytest.raises(ValueError): + layer.modify_cells(np.add) # Missing value for ufunc + + # aggregate + layer.data = np.ones((10, 10)) + assert layer.aggregate(np.sum) == 100 + + def test_property_layer_errors(): """Test error handling for PropertyLayers.""" dimensions = 5, 5 @@ -774,6 +819,9 @@ def test_property_layer_errors(): ): grid.add_property_layer(elevation) + with pytest.warns(UserWarning): + PropertyLayer("elevation", (10, 10), default_value=0, dtype=float) + def test_cell_agent(): # noqa: D103 cell1 = Cell((1,), capacity=None, random=random.Random()) From fcb4a0619bd910d0ca27bb4a2a7ee5b4b0a95a0b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:17:15 +0000 Subject: [PATCH 28/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_cell_space.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 2b7d7330ca3..e7e4290294a 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -755,14 +755,14 @@ def test_select_cells(): def test_property_layer(): """Test various property layer methods.""" - elevation = PropertyLayer("elevation", (5,5), default_value=0.0) + elevation = PropertyLayer("elevation", (5, 5), default_value=0.0) # test set_cells elevation.set_cells(10) - assert np.all(elevation.data==10) + assert np.all(elevation.data == 10) - elevation.set_cells(np.ones((5,5))) - assert np.all(elevation.data==1) + elevation.set_cells(np.ones((5, 5))) + assert np.all(elevation.data == 1) with pytest.raises(ValueError): elevation.set_cells(np.ones((6, 6))) From 26d291b0000ac594631cf651d9b104462a48dedf Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 24 Nov 2024 20:28:46 +0100 Subject: [PATCH 29/55] some extra protection for changing PropertyLayer.data --- .../experimental/cell_space/property_layer.py | 35 ++++++++++++------- tests/test_cell_space.py | 7 ++-- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index ce21f2a1a02..8f0290dcced 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -27,6 +27,17 @@ class PropertyLayer: # Fixme do we need this class at all? # what does it add to just a numpy array? + @property + def data(self): + return self._mesa_data + + @data.setter + def data(self, value): + if value.shape != self._mesa_data.shape: + raise ValueError(f"dimensions of value don't match dimensions of property layer: is {value.shape}, should be {self.dimensions}") + + self._mesa_data = value + propertylayer_experimental_warning_given = False def __init__( @@ -58,7 +69,7 @@ def __init__( ) # fixme why not initialize with empty? - self.data = np.full(self.dimensions, default_value, dtype=dtype) + self._mesa_data = np.full(self.dimensions, default_value, dtype=dtype) if not self.__class__.propertylayer_experimental_warning_given: warnings.warn( @@ -95,11 +106,11 @@ def set_cells(self, value, condition: Callable | None = None): condition: (Optional) A callable that returns a boolean array when applied to the data. """ if condition is None: - np.copyto(self.data, value) # In-place update + np.copyto(self._mesa_data, value) # In-place update else: vectorized_condition = np.vectorize(condition) - condition_result = vectorized_condition(self.data) - np.copyto(self.data, value, where=condition_result) + condition_result = vectorized_condition(self._mesa_data) + np.copyto(self._mesa_data, value, where=condition_result) def modify_cells( self, @@ -117,26 +128,26 @@ def modify_cells( condition: (Optional) A callable that returns a boolean array when applied to the data. """ condition_array = np.ones_like( - self.data, dtype=bool + self._mesa_data, dtype=bool ) # Default condition (all cells) if condition is not None: vectorized_condition = np.vectorize(condition) - condition_array = vectorized_condition(self.data) + condition_array = vectorized_condition(self._mesa_data) # Check if the operation is a lambda function or a NumPy ufunc if isinstance(operation, np.ufunc): if ufunc_requires_additional_input(operation): if value is None: raise ValueError("This ufunc requires an additional input value.") - modified_data = operation(self.data, value) + modified_data = operation(self._mesa_data, value) else: - modified_data = operation(self.data) + modified_data = operation(self._mesa_data) else: # Vectorize non-ufunc operations vectorized_operation = np.vectorize(operation) - modified_data = vectorized_operation(self.data) + modified_data = vectorized_operation(self._mesa_data) - self.data = np.where(condition_array, modified_data, self.data) + self._mesa_data = np.where(condition_array, modified_data, self._mesa_data) def select_cells(self, condition: Callable, return_list=True): """Find cells that meet a specified condition using NumPy's boolean indexing, in-place. @@ -152,7 +163,7 @@ def select_cells(self, condition: Callable, return_list=True): Returns: A list of (x, y) tuples or a boolean array. """ - condition_array = condition(self.data) + condition_array = condition(self._mesa_data) if return_list: return list(zip(*np.where(condition_array))) else: @@ -164,7 +175,7 @@ def aggregate(self, operation: Callable): Args: operation: A function to apply. Can be a lambda function or a NumPy ufunc. """ - return operation(self.data) + return operation(self._mesa_data) class HasPropertyLayers: diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index e7e4290294a..92622d4f18a 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -777,6 +777,9 @@ def condition(x): assert np.all((layer.data == 1) == (data > 0.5)) # modify_cells + data = np.zeros((10, 10)) + layer = PropertyLayer.from_data("some_name", data) + layer.data = np.zeros((10, 10)) layer.modify_cells(lambda x: x + 2) assert np.all(layer.data == 2) @@ -785,8 +788,8 @@ def condition(x): layer.modify_cells(np.multiply, 3) assert np.all(layer.data[3, 3] == 3) - data = np.random.default_rng(42).random((5, 5)) - layer.data = np.random.default_rng(42).random((5, 5)) + data = np.random.default_rng(42).random((10, 10)) + layer.data = np.random.default_rng(42).random((10, 10)) layer.modify_cells(np.add, value=3, condition=condition) assert np.all((layer.data > 3.5) == (data > 0.5)) From dba1137999c737d9df2001caea1eff32315552ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 24 Nov 2024 19:28:54 +0000 Subject: [PATCH 30/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/property_layer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 8f0290dcced..dd1fdf41ed6 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -34,7 +34,9 @@ def data(self): @data.setter def data(self, value): if value.shape != self._mesa_data.shape: - raise ValueError(f"dimensions of value don't match dimensions of property layer: is {value.shape}, should be {self.dimensions}") + raise ValueError( + f"dimensions of value don't match dimensions of property layer: is {value.shape}, should be {self.dimensions}" + ) self._mesa_data = value From ded2d91fad42d370b2c4aaaa8ff31d57546d498e Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 24 Nov 2024 20:37:08 +0100 Subject: [PATCH 31/55] Update property_layer.py --- mesa/experimental/cell_space/property_layer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index dd1fdf41ed6..32b517f3815 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -28,7 +28,7 @@ class PropertyLayer: # what does it add to just a numpy array? @property - def data(self): + def data(self): # noqa: D102 return self._mesa_data @data.setter From 1f56dedf72a77eb4bfa26edb178c70eea510a4a2 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 24 Nov 2024 20:40:02 +0100 Subject: [PATCH 32/55] Update property_layer.py --- mesa/experimental/cell_space/property_layer.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 32b517f3815..9447ff3c53a 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -24,8 +24,10 @@ class PropertyLayer: """ - # Fixme do we need this class at all? - # what does it add to just a numpy array? + # Fixme + # can't we simplify this a lot + # in essence, this is just a numpy array with a name and fixed dimensions + # all other functionality seems redundant to me? @property def data(self): # noqa: D102 @@ -33,12 +35,7 @@ def data(self): # noqa: D102 @data.setter def data(self, value): - if value.shape != self._mesa_data.shape: - raise ValueError( - f"dimensions of value don't match dimensions of property layer: is {value.shape}, should be {self.dimensions}" - ) - - self._mesa_data = value + self.set_cells(value) propertylayer_experimental_warning_given = False From 68356a8b313e030e4b229e3049fa6ae8c4ebba1c Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 25 Nov 2024 10:58:55 +0100 Subject: [PATCH 33/55] raise a value error if name of layer clashes with existing attribute of cell_klass --- mesa/experimental/cell_space/grid.py | 25 +++++++++++++++++-- .../experimental/cell_space/property_layer.py | 23 +++++++++++------ tests/test_cell_space.py | 25 +++++++++++++++++++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 106edf46481..17a7df33112 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -13,6 +13,25 @@ T = TypeVar("T", bound=Cell) +def create_gridclass(*args): + # fixme, we need to add the state info back in here as well from the looks of things + klass = type("GridCell", (Cell,), {"__reduce__": _reduce}) + + + # fixme: I only have the names but not the layers themselves + # so this needs to be handled in __getstate__ / __setstate__ + # also, why did copy initialy work just fine? + for entry in args: + setattr() + + return klass.__new__(klass) + +def _reduce(self): + # fixme this should be changed to the correct parent class + reductor = super(Cell, self).__reduce__() + modified_reductor = (create_gridclass, (self._mesa_properties,), *reductor[2::]) + return modified_reductor + class Grid(DiscreteSpace[T], Generic[T], HasPropertyLayers): """Base class for all grid classes. @@ -62,8 +81,10 @@ def __init__( self._ndims = len(dimensions) self._validate_parameters() self.cell_klass = type( - "GridCell", (self.cell_klass,), {} - ) # fixme name needs to dynamic to support multiple grids in parallel + "GridCell", (self.cell_klass,), + {"_mesa_properties": set()} + # {"__reduce__": _reduce, "_mesa_properties": set()} + ) coordinates = product(*(range(dim) for dim in self.dimensions)) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 9447ff3c53a..b96d26a3712 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -151,10 +151,6 @@ def modify_cells( def select_cells(self, condition: Callable, return_list=True): """Find cells that meet a specified condition using NumPy's boolean indexing, in-place. - # fixme: consider splitting into two separate functions - # select_cells_boolean - # select_cells_index - Args: condition: A callable that returns a boolean array when applied to the data. return_list: (Optional) If True, return a list of (x, y) tuples. Otherwise, return a boolean array. @@ -162,6 +158,10 @@ def select_cells(self, condition: Callable, return_list=True): Returns: A list of (x, y) tuples or a boolean array. """ + # fixme: consider splitting into two separate functions + # select_cells_boolean + # select_cells_index + condition_array = condition(self._mesa_data) if return_list: return list(zip(*np.where(condition_array))) @@ -198,13 +198,16 @@ def create_property_layer( name: The name of the property layer. default_value: The default value of the property layer. dtype: The data type of the property layer. + + Returns: + Property layer instance. + """ - # fixme, do we want to have the ability to add both predefined layers - # as well as just by name? layer = PropertyLayer( name, self.dimensions, default_value=default_value, dtype=dtype ) self.add_property_layer(layer) + return layer def add_property_layer(self, layer: PropertyLayer): """Add a predefined property layer to the grid. @@ -222,11 +225,14 @@ def add_property_layer(self, layer: PropertyLayer): ) if layer.name in self._mesa_property_layers: raise ValueError(f"Property layer {layer.name} already exists.") + if layer.name in self.cell_klass.__slots__ or layer.name in self.cell_klass.__dict__: + raise ValueError(f"Property layer {layer.name} clashes with existing attribute in {self.cell_klass.__name__}") self._mesa_property_layers[layer.name] = layer setattr( self.cell_klass, layer.name, PropertyDescriptor(layer) - ) # fixme: curious to see if this works + ) + self.cell_klass._mesa_properties.add(layer.name) def remove_property_layer(self, property_name: str): """Remove a property layer from the grid. @@ -237,6 +243,7 @@ def remove_property_layer(self, property_name: str): """ del self._mesa_property_layers[property_name] delattr(self.cell_klass, property_name) + self.cell_klass._mesa_properties.remove(property_name) def set_property( self, property_name: str, value, condition: Callable[[T], bool] | None = None @@ -375,7 +382,7 @@ class PropertyDescriptor: """Descriptor for giving cells attribute like access to values defined in property layers.""" def __init__(self, property_layer: PropertyLayer): # noqa: D107 - self.layer: np.ndarray = property_layer + self.layer: PropertyLayer = property_layer self.public_name: str self.private_name: str diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 92622d4f18a..b29f89ce220 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -661,6 +661,29 @@ def test_property_layer_integration(): assert "elevation" not in grid._mesa_property_layers assert not hasattr(cell, "elevation") + # what happens if we add a layer whose name clashes with an existing cell atttribute? + dimensions = (10, 10) + grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) + + with pytest.raises(ValueError): + grid.create_property_layer("capacity", 1, dtype=int) + + +# def test_copy_pickle_with_propertylayers(): +# import copy, pickle +# +# dimensions = (10, 10) +# grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) +# +# grid2 = copy.deepcopy(grid) +# assert grid2._cells[(0,0)].empty +# +# # fixme this currently fails +# dump = pickle.dumps(grid) +# grid2 = pickle.loads( # noqa: S301 +# dump +# ) +# assert grid2._cells[(0, 0)].empty def test_multiple_property_layers(): """Test initialization of DiscrateSpace with PropertyLayers.""" @@ -801,6 +824,8 @@ def condition(x): assert layer.aggregate(np.sum) == 100 + + def test_property_layer_errors(): """Test error handling for PropertyLayers.""" dimensions = 5, 5 From 4de5c98952792a7e54ff80b9a1ff0d79aab834b5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:59:18 +0000 Subject: [PATCH 34/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/grid.py | 8 +++++--- mesa/experimental/cell_space/property_layer.py | 13 ++++++++----- tests/test_cell_space.py | 5 ++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 17a7df33112..3f2d4fb236c 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -17,7 +17,6 @@ def create_gridclass(*args): # fixme, we need to add the state info back in here as well from the looks of things klass = type("GridCell", (Cell,), {"__reduce__": _reduce}) - # fixme: I only have the names but not the layers themselves # so this needs to be handled in __getstate__ / __setstate__ # also, why did copy initialy work just fine? @@ -26,12 +25,14 @@ def create_gridclass(*args): return klass.__new__(klass) + def _reduce(self): # fixme this should be changed to the correct parent class reductor = super(Cell, self).__reduce__() modified_reductor = (create_gridclass, (self._mesa_properties,), *reductor[2::]) return modified_reductor + class Grid(DiscreteSpace[T], Generic[T], HasPropertyLayers): """Base class for all grid classes. @@ -81,8 +82,9 @@ def __init__( self._ndims = len(dimensions) self._validate_parameters() self.cell_klass = type( - "GridCell", (self.cell_klass,), - {"_mesa_properties": set()} + "GridCell", + (self.cell_klass,), + {"_mesa_properties": set()}, # {"__reduce__": _reduce, "_mesa_properties": set()} ) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index b96d26a3712..4f75c1d0b58 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -225,13 +225,16 @@ def add_property_layer(self, layer: PropertyLayer): ) if layer.name in self._mesa_property_layers: raise ValueError(f"Property layer {layer.name} already exists.") - if layer.name in self.cell_klass.__slots__ or layer.name in self.cell_klass.__dict__: - raise ValueError(f"Property layer {layer.name} clashes with existing attribute in {self.cell_klass.__name__}") + if ( + layer.name in self.cell_klass.__slots__ + or layer.name in self.cell_klass.__dict__ + ): + raise ValueError( + f"Property layer {layer.name} clashes with existing attribute in {self.cell_klass.__name__}" + ) self._mesa_property_layers[layer.name] = layer - setattr( - self.cell_klass, layer.name, PropertyDescriptor(layer) - ) + setattr(self.cell_klass, layer.name, PropertyDescriptor(layer)) self.cell_klass._mesa_properties.add(layer.name) def remove_property_layer(self, property_name: str): diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index b29f89ce220..f86e044d128 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -680,11 +680,12 @@ def test_property_layer_integration(): # # # fixme this currently fails # dump = pickle.dumps(grid) -# grid2 = pickle.loads( # noqa: S301 +# grid2 = pickle.loads( # dump # ) # assert grid2._cells[(0, 0)].empty + def test_multiple_property_layers(): """Test initialization of DiscrateSpace with PropertyLayers.""" dimensions = (5, 5) @@ -824,8 +825,6 @@ def condition(x): assert layer.aggregate(np.sum) == 100 - - def test_property_layer_errors(): """Test error handling for PropertyLayers.""" dimensions = 5, 5 From 8baaec5749d8d365dc43a25b84c05cdc92b8ad3a Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 25 Nov 2024 20:02:35 +0100 Subject: [PATCH 35/55] pickle and deepcopy work --- .../experimental/cell_space/discrete_space.py | 2 - mesa/experimental/cell_space/grid.py | 57 +++++++++++++------ .../experimental/cell_space/property_layer.py | 6 +- tests/test_cell_space.py | 47 +++++++++------ 4 files changed, 76 insertions(+), 36 deletions(-) diff --git a/mesa/experimental/cell_space/discrete_space.py b/mesa/experimental/cell_space/discrete_space.py index f7259ebc425..88a6dc5476e 100644 --- a/mesa/experimental/cell_space/discrete_space.py +++ b/mesa/experimental/cell_space/discrete_space.py @@ -59,8 +59,6 @@ def __init__( self.cell_klass = cell_klass self._empties: dict[tuple[int, ...], None] = {} - # self._empties_initialized = False - # self.property_layers: dict[str, PropertyLayer] = {} @property def cutoff_empties(self): # noqa diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 3f2d4fb236c..8e03f41c239 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -5,32 +5,38 @@ from collections.abc import Sequence from itertools import product from random import Random -from typing import Generic, TypeVar +from typing import Generic, TypeVar, Any from mesa.experimental.cell_space import Cell, DiscreteSpace -from mesa.experimental.cell_space.property_layer import HasPropertyLayers +from mesa.experimental.cell_space.property_layer import HasPropertyLayers, PropertyDescriptor T = TypeVar("T", bound=Cell) +import copyreg -def create_gridclass(*args): - # fixme, we need to add the state info back in here as well from the looks of things - klass = type("GridCell", (Cell,), {"__reduce__": _reduce}) +def pickle_gridcell(obj): + """helper function for pickling GridCell instances.""" + # we have the base class, the dict, and the slots + args = obj.__class__.__bases__[0], obj.__getstate__() + return unpickle_gridcell, args - # fixme: I only have the names but not the layers themselves - # so this needs to be handled in __getstate__ / __setstate__ - # also, why did copy initialy work just fine? - for entry in args: - setattr() +def unpickle_gridcell(parent, fields): + """helper function for unpickling GridCell instances.""" + # since the class is dynamically created, we recreate it here + cell_klass = type( + "GridCell", + (parent,), + {"_mesa_properties": set()}, + ) + instance = cell_klass((0,0)) # we use a default coordinate and overwrite it with the correct value next - return klass.__new__(klass) + instance.__dict__ = fields[0] + for k, v in fields[1].items(): + if k != "__dict__": + setattr(instance, k, v) -def _reduce(self): - # fixme this should be changed to the correct parent class - reductor = super(Cell, self).__reduce__() - modified_reductor = (create_gridclass, (self._mesa_properties,), *reductor[2::]) - return modified_reductor + return instance class Grid(DiscreteSpace[T], Generic[T], HasPropertyLayers): @@ -85,9 +91,11 @@ def __init__( "GridCell", (self.cell_klass,), {"_mesa_properties": set()}, - # {"__reduce__": _reduce, "_mesa_properties": set()} ) + # we register the pickle_gridcell helper function + copyreg.pickle(self.cell_klass, pickle_gridcell) + coordinates = product(*(range(dim) for dim in self.dimensions)) self._cells = { @@ -154,6 +162,21 @@ def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> No if 0 <= ni < height and 0 <= nj < width: cell.connect(self._cells[ni, nj], (di, dj)) + def __getstate__(self) -> dict[str, Any]: + """custom __getstate__ for handling dynamic GridCell class and PropertyDescriptors.""" + state = super().__getstate__() + state = {k:v for k, v in state.items() if k!="cell_klass"} + return state + + def __setstate__(self, state: dict[str, Any]) -> None: + """custom __setstate__ for handling dynamic GridCell class and PropertyDescriptors.""" + self.__dict__ = state + self._connect_cells() # using super fails for this for some reason, so we repeat ourselves + + self.cell_klass = type(self._cells[(0,0)]) # the __reduce__ function handles this for us nicely + for layer in self._mesa_property_layers.values(): + setattr(self.cell_klass, layer.name, PropertyDescriptor(layer)) + class OrthogonalMooreGrid(Grid[T]): """Grid where cells are connected to their 8 neighbors. diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 4f75c1d0b58..919a85ea089 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -1,8 +1,10 @@ """This module provides functionality for working with property layers in grids.""" -import warnings +import copyreg from collections.abc import Callable, Sequence from typing import Any, TypeVar +import warnings + import numpy as np @@ -400,3 +402,5 @@ def ufunc_requires_additional_input(ufunc): # noqa: D103 # NumPy ufuncs have a 'nargs' attribute indicating the number of input arguments # For binary ufuncs (like np.add), nargs is 2 return ufunc.nargs > 1 + + diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index f86e044d128..02e7c7f311b 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -433,6 +433,11 @@ def test_networkgrid(): assert connection.coordinate in G.neighbors(i) + import pickle + + pickle.loads(pickle.dumps(grid)) + + def test_voronoigrid(): """Test VoronoiGrid.""" points = [[0, 1], [1, 3], [1.1, 1], [1, 1]] @@ -669,21 +674,28 @@ def test_property_layer_integration(): grid.create_property_layer("capacity", 1, dtype=int) -# def test_copy_pickle_with_propertylayers(): -# import copy, pickle -# -# dimensions = (10, 10) -# grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) -# -# grid2 = copy.deepcopy(grid) -# assert grid2._cells[(0,0)].empty -# -# # fixme this currently fails -# dump = pickle.dumps(grid) -# grid2 = pickle.loads( -# dump -# ) -# assert grid2._cells[(0, 0)].empty +def test_copy_pickle_with_propertylayers(): + import copy, pickle + + dimensions = (10, 10) + grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) + + grid2 = copy.deepcopy(grid) + assert grid2._cells[(0, 0)].empty + + data = grid2._mesa_property_layers["empty"].data + grid2._cells[(0, 0)].empty = False + assert grid2._cells[(0, 0)].empty == data[0, 0] + + + # fixme this currently fails + dimensions = (10, 10) + grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) + dump = pickle.dumps(grid) + grid2 = pickle.loads( + dump + ) + assert grid2._cells[(0, 0)].empty def test_multiple_property_layers(): @@ -931,7 +943,6 @@ def test_copying_discrete_spaces(): # noqa: D103 # inspired by #2373 # we use deepcopy but this also applies to pickle import copy - import networkx as nx grid = OrthogonalMooreGrid((100, 100), random=random.Random(42)) @@ -964,3 +975,7 @@ def test_copying_discrete_spaces(): # noqa: D103 for c1, c2 in zip(grid.all_cells, grid_copy.all_cells): for k, v in c1.connections.items(): assert v.coordinate == c2.connections[k].coordinate + + +if __name__ == "__main__": + test_copy_pickle_with_propertylayers() \ No newline at end of file From e3e5d52692608eae230bf7a2493b9fd9f4e9d903 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:02:44 +0000 Subject: [PATCH 36/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/grid.py | 28 ++++++++++++------- .../experimental/cell_space/property_layer.py | 6 +--- tests/test_cell_space.py | 12 ++++---- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 8e03f41c239..8bb08302129 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -5,37 +5,43 @@ from collections.abc import Sequence from itertools import product from random import Random -from typing import Generic, TypeVar, Any +from typing import Any, Generic, TypeVar from mesa.experimental.cell_space import Cell, DiscreteSpace -from mesa.experimental.cell_space.property_layer import HasPropertyLayers, PropertyDescriptor +from mesa.experimental.cell_space.property_layer import ( + HasPropertyLayers, + PropertyDescriptor, +) T = TypeVar("T", bound=Cell) import copyreg + def pickle_gridcell(obj): - """helper function for pickling GridCell instances.""" + """Helper function for pickling GridCell instances.""" # we have the base class, the dict, and the slots args = obj.__class__.__bases__[0], obj.__getstate__() return unpickle_gridcell, args + def unpickle_gridcell(parent, fields): - """helper function for unpickling GridCell instances.""" + """Helper function for unpickling GridCell instances.""" # since the class is dynamically created, we recreate it here cell_klass = type( "GridCell", (parent,), {"_mesa_properties": set()}, ) - instance = cell_klass((0,0)) # we use a default coordinate and overwrite it with the correct value next + instance = cell_klass( + (0, 0) + ) # we use a default coordinate and overwrite it with the correct value next instance.__dict__ = fields[0] for k, v in fields[1].items(): if k != "__dict__": setattr(instance, k, v) - return instance @@ -163,17 +169,19 @@ def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> No cell.connect(self._cells[ni, nj], (di, dj)) def __getstate__(self) -> dict[str, Any]: - """custom __getstate__ for handling dynamic GridCell class and PropertyDescriptors.""" + """Custom __getstate__ for handling dynamic GridCell class and PropertyDescriptors.""" state = super().__getstate__() - state = {k:v for k, v in state.items() if k!="cell_klass"} + state = {k: v for k, v in state.items() if k != "cell_klass"} return state def __setstate__(self, state: dict[str, Any]) -> None: - """custom __setstate__ for handling dynamic GridCell class and PropertyDescriptors.""" + """Custom __setstate__ for handling dynamic GridCell class and PropertyDescriptors.""" self.__dict__ = state self._connect_cells() # using super fails for this for some reason, so we repeat ourselves - self.cell_klass = type(self._cells[(0,0)]) # the __reduce__ function handles this for us nicely + self.cell_klass = type( + self._cells[(0, 0)] + ) # the __reduce__ function handles this for us nicely for layer in self._mesa_property_layers.values(): setattr(self.cell_klass, layer.name, PropertyDescriptor(layer)) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 919a85ea089..4f75c1d0b58 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -1,10 +1,8 @@ """This module provides functionality for working with property layers in grids.""" -import copyreg +import warnings from collections.abc import Callable, Sequence from typing import Any, TypeVar -import warnings - import numpy as np @@ -402,5 +400,3 @@ def ufunc_requires_additional_input(ufunc): # noqa: D103 # NumPy ufuncs have a 'nargs' attribute indicating the number of input arguments # For binary ufuncs (like np.add), nargs is 2 return ufunc.nargs > 1 - - diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 02e7c7f311b..20350ef73ca 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -432,7 +432,6 @@ def test_networkgrid(): for connection in cell.connections.values(): assert connection.coordinate in G.neighbors(i) - import pickle pickle.loads(pickle.dumps(grid)) @@ -675,7 +674,8 @@ def test_property_layer_integration(): def test_copy_pickle_with_propertylayers(): - import copy, pickle + import copy + import pickle dimensions = (10, 10) grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) @@ -687,14 +687,11 @@ def test_copy_pickle_with_propertylayers(): grid2._cells[(0, 0)].empty = False assert grid2._cells[(0, 0)].empty == data[0, 0] - # fixme this currently fails dimensions = (10, 10) grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) dump = pickle.dumps(grid) - grid2 = pickle.loads( - dump - ) + grid2 = pickle.loads(dump) assert grid2._cells[(0, 0)].empty @@ -943,6 +940,7 @@ def test_copying_discrete_spaces(): # noqa: D103 # inspired by #2373 # we use deepcopy but this also applies to pickle import copy + import networkx as nx grid = OrthogonalMooreGrid((100, 100), random=random.Random(42)) @@ -978,4 +976,4 @@ def test_copying_discrete_spaces(): # noqa: D103 if __name__ == "__main__": - test_copy_pickle_with_propertylayers() \ No newline at end of file + test_copy_pickle_with_propertylayers() From 9211b74c6cab3df946f8437910c84fa4e4406a15 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 25 Nov 2024 20:03:43 +0100 Subject: [PATCH 37/55] ruff --- mesa/experimental/cell_space/grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index 8bb08302129..aac9dca5830 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -2,6 +2,7 @@ from __future__ import annotations +import copyreg from collections.abc import Sequence from itertools import product from random import Random @@ -15,7 +16,6 @@ T = TypeVar("T", bound=Cell) -import copyreg def pickle_gridcell(obj): From 43dda87784466a569bad4e1b0bd390e8e9dd61bc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:03:54 +0000 Subject: [PATCH 38/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/grid.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index aac9dca5830..cf4279b0e7b 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -17,7 +17,6 @@ T = TypeVar("T", bound=Cell) - def pickle_gridcell(obj): """Helper function for pickling GridCell instances.""" # we have the base class, the dict, and the slots From 41f3d6b313db7946603bce1ef62d94fd1ed9587d Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 25 Nov 2024 20:05:13 +0100 Subject: [PATCH 39/55] some explanation --- mesa/experimental/cell_space/grid.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mesa/experimental/cell_space/grid.py b/mesa/experimental/cell_space/grid.py index cf4279b0e7b..7f1d5751f64 100644 --- a/mesa/experimental/cell_space/grid.py +++ b/mesa/experimental/cell_space/grid.py @@ -19,7 +19,7 @@ def pickle_gridcell(obj): """Helper function for pickling GridCell instances.""" - # we have the base class, the dict, and the slots + # we have the base class and the state via __getstate__ args = obj.__class__.__bases__[0], obj.__getstate__() return unpickle_gridcell, args @@ -36,7 +36,8 @@ def unpickle_gridcell(parent, fields): (0, 0) ) # we use a default coordinate and overwrite it with the correct value next - instance.__dict__ = fields[0] + # __gestate__ returns a tuple with dict and slots, but slots contains the dict so we can just use the + # second item only for k, v in fields[1].items(): if k != "__dict__": setattr(instance, k, v) From 353686110ac43292b24c77c73d70fb2fd89e9144 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 25 Nov 2024 20:08:13 +0100 Subject: [PATCH 40/55] Update test_cell_space.py --- tests/test_cell_space.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 20350ef73ca..b6c43be0d2c 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -434,7 +434,7 @@ def test_networkgrid(): import pickle - pickle.loads(pickle.dumps(grid)) + pickle.loads(pickle.dumps(grid)) # noqa: S301 def test_voronoigrid(): @@ -674,6 +674,7 @@ def test_property_layer_integration(): def test_copy_pickle_with_propertylayers(): + """Test deepcopy and pickle with dynamically created GridClass and ProperyLayer descriptors.""" import copy import pickle @@ -687,12 +688,14 @@ def test_copy_pickle_with_propertylayers(): grid2._cells[(0, 0)].empty = False assert grid2._cells[(0, 0)].empty == data[0, 0] - # fixme this currently fails dimensions = (10, 10) grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) dump = pickle.dumps(grid) - grid2 = pickle.loads(dump) + grid2 = pickle.loads(dump) # noqa: S301 assert grid2._cells[(0, 0)].empty + data = grid2._mesa_property_layers["empty"].data + grid2._cells[(0, 0)].empty = False + assert grid2._cells[(0, 0)].empty == data[0, 0] def test_multiple_property_layers(): @@ -974,6 +977,3 @@ def test_copying_discrete_spaces(): # noqa: D103 for k, v in c1.connections.items(): assert v.coordinate == c2.connections[k].coordinate - -if __name__ == "__main__": - test_copy_pickle_with_propertylayers() From 412d5f6b73a80236e141da48fa52213f237c94ef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:08:22 +0000 Subject: [PATCH 41/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_cell_space.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index b6c43be0d2c..37babc2db70 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -691,7 +691,7 @@ def test_copy_pickle_with_propertylayers(): dimensions = (10, 10) grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) dump = pickle.dumps(grid) - grid2 = pickle.loads(dump) # noqa: S301 + grid2 = pickle.loads(dump) # noqa: S301 assert grid2._cells[(0, 0)].empty data = grid2._mesa_property_layers["empty"].data grid2._cells[(0, 0)].empty = False @@ -976,4 +976,3 @@ def test_copying_discrete_spaces(): # noqa: D103 for c1, c2 in zip(grid.all_cells, grid_copy.all_cells): for k, v in c1.connections.items(): assert v.coordinate == c2.connections[k].coordinate - From 455c9a96c9c8f23ef90c6d600fbe3f1b588e2227 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Mon, 25 Nov 2024 20:14:31 +0100 Subject: [PATCH 42/55] ruff --- tests/test_cell_space.py | 63 +++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index 37babc2db70..ef223fed32b 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -163,7 +163,7 @@ def test_orthogonal_grid_moore(): for connection in grid._cells[(5, 5)].connections.values(): # fmt: off assert connection.coordinate in {(4, 4), (4, 5), (4, 6), - (5, 4), (5, 6), + (5, 4), (5, 6), (6, 4), (6, 5), (6, 6)} # fmt: on @@ -175,7 +175,7 @@ def test_orthogonal_grid_moore(): for connection in grid._cells[(0, 0)].connections.values(): # fmt: off assert connection.coordinate in {(9, 9), (9, 0), (9, 1), - (0, 9), (0, 1), + (0, 9), (0, 1), (1, 9), (1, 0), (1, 1)} # fmt: on @@ -206,9 +206,12 @@ def test_orthogonal_grid_moore_3d(): assert len(grid._cells[(5, 5, 5)].connections.values()) == 26 for connection in grid._cells[(5, 5, 5)].connections.values(): # 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)} + 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 @@ -218,9 +221,12 @@ def test_orthogonal_grid_moore_3d(): assert len(grid._cells[(0, 0, 0)].connections.values()) == 26 for connection in grid._cells[(0, 0, 0)].connections.values(): # 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)} + 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 @@ -262,15 +268,24 @@ def test_orthogonal_grid_moore_4d(): assert len(grid._cells[(5, 5, 5, 5)].connections.values()) == 80 for connection in grid._cells[(5, 5, 5, 5)].connections.values(): # 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)} + 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 @@ -379,7 +394,7 @@ def test_hexgrid(): for connection in grid._cells[(1, 0)].connections.values(): # fmt: off assert connection.coordinate in {(0, 0), (0, 1), - (1, 1), + (1, 1), (2, 0), (2, 1)} # middle odd row @@ -387,7 +402,7 @@ def test_hexgrid(): for connection in grid._cells[(5, 5)].connections.values(): # fmt: off assert connection.coordinate in {(4, 5), (4, 6), - (5, 4), (5, 6), + (5, 4), (5, 6), (6, 5), (6, 6)} # fmt: on @@ -397,7 +412,7 @@ def test_hexgrid(): for connection in grid._cells[(4, 4)].connections.values(): # fmt: off assert connection.coordinate in {(3, 3), (3, 4), - (4, 3), (4, 5), + (4, 3), (4, 5), (5, 3), (5, 4)} # fmt: on @@ -410,7 +425,7 @@ def test_hexgrid(): for connection in grid._cells[(0, 0)].connections.values(): # fmt: off assert connection.coordinate in {(9, 9), (9, 0), - (0, 9), (0, 1), + (0, 9), (0, 1), (1, 9), (1, 0)} # fmt: on @@ -665,7 +680,7 @@ def test_property_layer_integration(): assert "elevation" not in grid._mesa_property_layers assert not hasattr(cell, "elevation") - # what happens if we add a layer whose name clashes with an existing cell atttribute? + # what happens if we add a layer whose name clashes with an existing cell attribute? dimensions = (10, 10) grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) @@ -853,8 +868,8 @@ def test_property_layer_errors(): grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) elevation = PropertyLayer("elevation", (10, 10), default_value=0.0) with pytest.raises( - ValueError, - match="Dimensions of property layer do not match the dimensions of the grid", + ValueError, + match="Dimensions of property layer do not match the dimensions of the grid", ): grid.add_property_layer(elevation) From 68885e36d442b1296ce6383d156777e8a9b52e87 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 19:14:47 +0000 Subject: [PATCH 43/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_cell_space.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index ef223fed32b..f449d26c9bf 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -868,8 +868,8 @@ def test_property_layer_errors(): grid = OrthogonalMooreGrid(dimensions, torus=False, random=random.Random(42)) elevation = PropertyLayer("elevation", (10, 10), default_value=0.0) with pytest.raises( - ValueError, - match="Dimensions of property layer do not match the dimensions of the grid", + ValueError, + match="Dimensions of property layer do not match the dimensions of the grid", ): grid.add_property_layer(elevation) From 17bd9308292a091b0d7a785fe83360ca3291f367 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 1 Dec 2024 20:02:09 +0100 Subject: [PATCH 44/55] add attribute like access for layers --- mesa/experimental/cell_space/property_layer.py | 16 +++++++++++++--- tests/test_cell_space.py | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 4f75c1d0b58..4e64ec6a11b 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -178,7 +178,12 @@ def aggregate(self, operation: Callable): class HasPropertyLayers: - """Mixin-like class to add property layer functionality to Grids.""" + """Mixin-like class to add property layer functionality to Grids. + + Property layers can be added to a grid using create_property_layer or add_property_layer. Once created, property + layers can be accessed as attributes if the name used for the layer is a valid python identifier. + + """ # fixme is there a way to indicate that a mixin only works with specific classes? def __init__(self, *args, **kwargs): @@ -237,6 +242,7 @@ def add_property_layer(self, layer: PropertyLayer): setattr(self.cell_klass, layer.name, PropertyDescriptor(layer)) self.cell_klass._mesa_properties.add(layer.name) + def remove_property_layer(self, property_name: str): """Remove a property layer from the grid. @@ -380,14 +386,18 @@ def select_cells( else: return combined_mask + def __getattr__(self, name: str) -> Any: + try: + return self._mesa_property_layers[name] + except KeyError: + raise AttributeError(f"'{type(self).__name__}' object has no property layer called '{name}'") + class PropertyDescriptor: """Descriptor for giving cells attribute like access to values defined in property layers.""" def __init__(self, property_layer: PropertyLayer): # noqa: D107 self.layer: PropertyLayer = property_layer - self.public_name: str - self.private_name: str def __get__(self, instance: Cell, owner): # noqa: D105 return self.layer.data[instance.coordinate] diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index f449d26c9bf..d37cc034c23 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -647,6 +647,7 @@ def test_property_layer_integration(): grid.add_property_layer(elevation) assert "elevation" in grid._mesa_property_layers assert len(grid._mesa_property_layers) == 2 ## empty is always there + assert grid.elevation is elevation # Test accessing PropertyLayer from a cell cell = grid._cells[(0, 0)] From ff5c8a2711457edc14f6e01ae301ff4d426ac1a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:02:46 +0000 Subject: [PATCH 45/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/property_layer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 4e64ec6a11b..8e9251d578a 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -242,7 +242,6 @@ def add_property_layer(self, layer: PropertyLayer): setattr(self.cell_klass, layer.name, PropertyDescriptor(layer)) self.cell_klass._mesa_properties.add(layer.name) - def remove_property_layer(self, property_name: str): """Remove a property layer from the grid. @@ -390,7 +389,9 @@ def __getattr__(self, name: str) -> Any: try: return self._mesa_property_layers[name] except KeyError: - raise AttributeError(f"'{type(self).__name__}' object has no property layer called '{name}'") + raise AttributeError( + f"'{type(self).__name__}' object has no property layer called '{name}'" + ) class PropertyDescriptor: From 1de6580635be48e752e57e3b789cf77c1a75344b Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 1 Dec 2024 20:04:45 +0100 Subject: [PATCH 46/55] precommit related --- mesa/experimental/cell_space/property_layer.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 8e9251d578a..e976a5759c7 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -385,13 +385,11 @@ def select_cells( else: return combined_mask - def __getattr__(self, name: str) -> Any: + def __getattr__(self, name: str) -> Any: # noqa: D105 try: return self._mesa_property_layers[name] - except KeyError: - raise AttributeError( - f"'{type(self).__name__}' object has no property layer called '{name}'" - ) + except KeyError as e: + raise AttributeError(f"'{type(self).__name__}' object has no property layer called '{name}'") from e class PropertyDescriptor: From d6f8e7b1ac6233cdf72505ecad9dd3edeb9ce7c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:05:41 +0000 Subject: [PATCH 47/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/property_layer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index e976a5759c7..9760b05091f 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -389,7 +389,9 @@ def __getattr__(self, name: str) -> Any: # noqa: D105 try: return self._mesa_property_layers[name] except KeyError as e: - raise AttributeError(f"'{type(self).__name__}' object has no property layer called '{name}'") from e + raise AttributeError( + f"'{type(self).__name__}' object has no property layer called '{name}'" + ) from e class PropertyDescriptor: From 85bd5f638c6b96c6d76ff534a2799387afe78f29 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 1 Dec 2024 20:20:39 +0100 Subject: [PATCH 48/55] some additional protection --- mesa/experimental/cell_space/property_layer.py | 12 ++++++++++++ tests/test_cell_space.py | 3 +++ 2 files changed, 15 insertions(+) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 9760b05091f..ab9bd52dbf8 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -394,6 +394,18 @@ def __getattr__(self, name: str) -> Any: # noqa: D105 ) from e + def __setattr__(self, key, value): # noqa: D105 + try: + layers = self.__dict__["_mesa_property_layers"] + except KeyError as e: + super().__setattr__(key, value) + else: + if key in layers: + raise AttributeError(f"'{type(self).__name__}' object already has a property layer with name '{key}'") + else: + super().__setattr__(key, value) + + class PropertyDescriptor: """Descriptor for giving cells attribute like access to values defined in property layers.""" diff --git a/tests/test_cell_space.py b/tests/test_cell_space.py index d37cc034c23..53fb737d5a7 100644 --- a/tests/test_cell_space.py +++ b/tests/test_cell_space.py @@ -649,6 +649,9 @@ def test_property_layer_integration(): assert len(grid._mesa_property_layers) == 2 ## empty is always there assert grid.elevation is elevation + with pytest.raises(AttributeError): + grid.elevation = 0 + # Test accessing PropertyLayer from a cell cell = grid._cells[(0, 0)] assert hasattr(cell, "elevation") From 8284b2da93c9d54ca4ef282c6049742d47ca3365 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Sun, 1 Dec 2024 20:22:05 +0100 Subject: [PATCH 49/55] Update property_layer.py --- mesa/experimental/cell_space/property_layer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index ab9bd52dbf8..2e63b334216 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -395,6 +395,9 @@ def __getattr__(self, name: str) -> Any: # noqa: D105 def __setattr__(self, key, value): # noqa: D105 + # fixme + # this might be done more elegantly, the main problem is that _mesa_property_layers must already be defined to avoid infinte recursion errors from happening + # also, this protection only works if the attribute is added after the layer, not the other way around try: layers = self.__dict__["_mesa_property_layers"] except KeyError as e: From 83b3e1dc0673b1d414e4a38a08fc57b7de07181b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:22:16 +0000 Subject: [PATCH 50/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/property_layer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index 2e63b334216..fa32859972e 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -393,18 +393,19 @@ def __getattr__(self, name: str) -> Any: # noqa: D105 f"'{type(self).__name__}' object has no property layer called '{name}'" ) from e - - def __setattr__(self, key, value): # noqa: D105 + def __setattr__(self, key, value): # noqa: D105 # fixme # this might be done more elegantly, the main problem is that _mesa_property_layers must already be defined to avoid infinte recursion errors from happening # also, this protection only works if the attribute is added after the layer, not the other way around try: layers = self.__dict__["_mesa_property_layers"] - except KeyError as e: + except KeyError: super().__setattr__(key, value) else: if key in layers: - raise AttributeError(f"'{type(self).__name__}' object already has a property layer with name '{key}'") + raise AttributeError( + f"'{type(self).__name__}' object already has a property layer with name '{key}'" + ) else: super().__setattr__(key, value) From 42349b11da238e7c67227110e0ab555375e20737 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 3 Dec 2024 15:50:26 +0100 Subject: [PATCH 51/55] Update property_layer.py --- mesa/experimental/cell_space/property_layer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesa/experimental/cell_space/property_layer.py b/mesa/experimental/cell_space/property_layer.py index fa32859972e..5e2dbf66c94 100644 --- a/mesa/experimental/cell_space/property_layer.py +++ b/mesa/experimental/cell_space/property_layer.py @@ -395,7 +395,7 @@ def __getattr__(self, name: str) -> Any: # noqa: D105 def __setattr__(self, key, value): # noqa: D105 # fixme - # this might be done more elegantly, the main problem is that _mesa_property_layers must already be defined to avoid infinte recursion errors from happening + # this might be done more elegantly, the main problem is that _mesa_property_layers must already be defined to avoid infinite recursion errors from happening # also, this protection only works if the attribute is added after the layer, not the other way around try: layers = self.__dict__["_mesa_property_layers"] From 0c9c25b6a879d9430cbb5aec136790b3d844b05e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:59:36 +0000 Subject: [PATCH 52/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/__init__.py | 2 +- mesa/experimental/cell_space/cell.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mesa/experimental/cell_space/__init__.py b/mesa/experimental/cell_space/__init__.py index 3ba7a92eb0e..22dddcaf30b 100644 --- a/mesa/experimental/cell_space/__init__.py +++ b/mesa/experimental/cell_space/__init__.py @@ -35,6 +35,6 @@ "Network", "OrthogonalMooreGrid", "OrthogonalVonNeumannGrid", - "VoronoiGrid", "PropertyLayer", + "VoronoiGrid", ] diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 6f720f434af..0d600728768 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -32,11 +32,11 @@ class Cell: "_mesa_property_layers", "agents", "capacity", - "random", "connections", "coordinate", "properties", "random", + "random", ] def __init__( From b4355df13849ac589866ec699febcc666534a8a8 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 3 Dec 2024 16:01:40 +0100 Subject: [PATCH 53/55] Update cell.py --- mesa/experimental/cell_space/cell.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 0d600728768..050b9793935 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -29,7 +29,6 @@ class Cell: __slots__ = [ "__dict__", - "_mesa_property_layers", "agents", "capacity", "connections", @@ -64,7 +63,6 @@ def __init__( Coordinate, object ] = {} # fixme still used by voronoi mesh self.random = random - self._mesa_property_layers: dict[str, PropertyLayer] = {} def connect(self, other: Cell, key: Coordinate | None = None) -> None: """Connects this cell to another cell. @@ -189,23 +187,6 @@ def _neighborhood( neighborhood.pop(self, None) return neighborhood - # PropertyLayer methods - def get_property(self, property_name: str) -> Any: - """Get the value of a property.""" - return self._mesa_property_layers[property_name].data[self.coordinate] - - def set_property(self, property_name: str, value: Any): - """Set the value of a property.""" - self._mesa_property_layers[property_name].set_cell(self.coordinate, value) - - def modify_property( - self, property_name: str, operation: Callable, value: Any = None - ): - """Modify the value of a property.""" - self._mesa_property_layers[property_name].modify_cell( - self.coordinate, operation, value - ) - def __getstate__(self): """Return state of the Cell with connections set to empty.""" # fixme, once we shift to 3.11, replace this with super. __getstate__ From c77a8ee3df632fa1e28287c19e04650fa4c1cbf7 Mon Sep 17 00:00:00 2001 From: Jan Kwakkel Date: Tue, 3 Dec 2024 16:02:21 +0100 Subject: [PATCH 54/55] Update cell.py --- mesa/experimental/cell_space/cell.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 050b9793935..3cfe296f6fd 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -35,7 +35,6 @@ class Cell: "coordinate", "properties", "random", - "random", ] def __init__( From 9cea6df39641fcf886d6ef0093c3261240323cae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:03:16 +0000 Subject: [PATCH 55/55] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mesa/experimental/cell_space/cell.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mesa/experimental/cell_space/cell.py b/mesa/experimental/cell_space/cell.py index 3cfe296f6fd..e101b017071 100644 --- a/mesa/experimental/cell_space/cell.py +++ b/mesa/experimental/cell_space/cell.py @@ -2,10 +2,9 @@ from __future__ import annotations -from collections.abc import Callable from functools import cache, cached_property from random import Random -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from mesa.experimental.cell_space.cell_agent import CellAgent from mesa.experimental.cell_space.cell_collection import CellCollection