Skip to content

Commit

Permalink
space: Implement PropertyLayer and _PropertyGrid
Browse files Browse the repository at this point in the history
  • Loading branch information
EwoutH committed Dec 4, 2023
1 parent 838e216 commit e4be7d8
Showing 1 changed file with 149 additions and 2 deletions.
151 changes: 149 additions & 2 deletions mesa/space.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from __future__ import annotations

import collections
import inspect
import itertools
import math
from numbers import Real
Expand Down Expand Up @@ -485,7 +486,153 @@ def exists_empty_cells(self) -> bool:
return len(self.empties) > 0


class SingleGrid(_Grid):
def is_lambda_function(function):
"""Check if a function is a lambda function."""
return (

Check warning on line 491 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L491

Added line #L491 was not covered by tests
inspect.isfunction(function)
and len(inspect.signature(function).parameters) == 1
)


class PropertyLayer:
def __init__(
self, name: str, width: int, height: int, default_value, dtype=np.float32
):
self.name = name
self.width = width
self.height = height
self.data = np.full((width, height), default_value, dtype=dtype)

Check warning on line 504 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L501-L504

Added lines #L501 - L504 were not covered by tests

def set_cell(self, position: Coordinate, value):
"""
Update a single cell's value in-place.
"""
self.data[position] = value

Check warning on line 510 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L510

Added line #L510 was not covered by tests

def set_cells(self, value, condition=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 that returns a boolean array when applied to the data.
"""
if condition is None:
np.copyto(self.data, value) # In-place update

Check warning on line 521 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L521

Added line #L521 was not covered by tests
else:
# Ensure condition is a boolean array of the same shape as self.data
if (
not isinstance(condition, np.ndarray)
or condition.shape != self.data.shape
):
raise ValueError(

Check warning on line 528 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L528

Added line #L528 was not covered by tests
"Condition must be a NumPy array with the same shape as the grid."
)
np.copyto(self.data, value, where=condition) # Conditional in-place update

Check warning on line 531 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L531

Added line #L531 was not covered by tests

def modify_cell(self, position: Coordinate, operation, 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]

Check warning on line 543 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L543

Added line #L543 was not covered by tests

# Determine if the operation is a lambda function or a NumPy ufunc
if is_lambda_function(operation):
# Lambda function case
self.data[position] = operation(current_value)

Check warning on line 548 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L548

Added line #L548 was not covered by tests
elif value is not None:
# NumPy ufunc case
self.data[position] = operation(current_value, value)

Check warning on line 551 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L551

Added line #L551 was not covered by tests
else:
raise ValueError("Invalid operation or missing value for NumPy ufunc.")

Check warning on line 553 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L553

Added line #L553 was not covered by tests

def modify_cells(self, operation, value=None, condition_function=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.
"""
if condition_function is not None:
condition_array = np.vectorize(condition_function)(self.data)

Check warning on line 566 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L566

Added line #L566 was not covered by tests
else:
condition_array = np.ones_like(self.data, dtype=bool) # All cells

Check warning on line 568 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L568

Added line #L568 was not covered by tests

# Check if the operation is a lambda function or a NumPy ufunc
if is_lambda_function(operation):
# Lambda function case
modified_data = np.vectorize(operation)(self.data)

Check warning on line 573 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L573

Added line #L573 was not covered by tests
elif value is not None:
# NumPy ufunc case
modified_data = operation(self.data, value)

Check warning on line 576 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L576

Added line #L576 was not covered by tests
else:
raise ValueError("Invalid operation or missing value for NumPy ufunc.")

Check warning on line 578 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L578

Added line #L578 was not covered by tests

self.data = np.where(condition_array, modified_data, self.data)

Check warning on line 580 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L580

Added line #L580 was not covered by tests

def select_cells(self, condition, return_list=True):
"""
Find cells that meet a specified condition using NumPy's boolean indexing, in-place.
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)

Check warning on line 593 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L593

Added line #L593 was not covered by tests
if return_list:
return list(zip(*np.where(condition_array)))

Check warning on line 595 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L595

Added line #L595 was not covered by tests
else:
return condition_array

Check warning on line 597 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L597

Added line #L597 was not covered by tests

def aggregate_property(self, operation):
"""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.
"""

# Check if the operation is a lambda function or a NumPy ufunc
if is_lambda_function(operation):
# Lambda function case
return operation(self.data)

Check warning on line 609 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L609

Added line #L609 was not covered by tests
else:
# NumPy ufunc case
return operation(self.data)

Check warning on line 612 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L612

Added line #L612 was not covered by tests


class _PropertyGrid(_Grid):
def __init__(self, width: int, height: int, torus: bool):
super().__init__(width, height, torus)
self.properties = {}

# Add and remove properties to the grid
def add_property_layer(self, property_layer: PropertyLayer):
self.properties[property_layer.name] = property_layer

Check warning on line 622 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L622

Added line #L622 was not covered by tests

def remove_property_layer(self, property_name: str):
if property_name not in self.properties:
raise ValueError(f"Property layer {property_name} does not exist.")
del self.properties[property_name]

Check warning on line 627 in mesa/space.py

View check run for this annotation

Codecov / codecov/patch

mesa/space.py#L626-L627

Added lines #L626 - L627 were not covered by tests

# TODO:
# - Select cells conditionally based on multiple properties
# - Move random cells conditionally based on multiple properties
# - Move to cell with highest/lowest/closest property value


class SingleGrid(_PropertyGrid):
"""Rectangular grid where each cell contains exactly at most one agent.
Grid cells are indexed by [x, y], where [0, 0] is assumed to be the
Expand Down Expand Up @@ -528,7 +675,7 @@ def remove_agent(self, agent: Agent) -> None:
agent.pos = None


class MultiGrid(_Grid):
class MultiGrid(_PropertyGrid):
"""Rectangular grid where each cell can contain more than one agent.
Grid cells are indexed by [x, y], where [0, 0] is assumed to be at
Expand Down

0 comments on commit e4be7d8

Please sign in to comment.