Skip to content

Commit 221b4e3

Browse files
committed
space: Implement PropertyLayer and _PropertyGrid
1 parent 838e216 commit 221b4e3

File tree

1 file changed

+149
-2
lines changed

1 file changed

+149
-2
lines changed

mesa/space.py

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from __future__ import annotations
2222

2323
import collections
24+
import inspect
2425
import itertools
2526
import math
2627
from numbers import Real
@@ -485,7 +486,153 @@ def exists_empty_cells(self) -> bool:
485486
return len(self.empties) > 0
486487

487488

488-
class SingleGrid(_Grid):
489+
def is_lambda_function(function):
490+
"""Check if a function is a lambda function."""
491+
return (
492+
inspect.isfunction(function)
493+
and len(inspect.signature(function).parameters) == 1
494+
)
495+
496+
497+
class PropertyLayer:
498+
def __init__(
499+
self, name: str, width: int, height: int, default_value, dtype=np.float32
500+
):
501+
self.name = name
502+
self.width = width
503+
self.height = height
504+
self.data = np.full((width, height), default_value, dtype=dtype)
505+
506+
def set_cell(self, position: Coordinate, value):
507+
"""
508+
Update a single cell's value in-place.
509+
"""
510+
self.data[position] = value
511+
512+
def set_cells(self, value, condition=None):
513+
"""
514+
Perform a batch update either on the entire grid or conditionally, in-place.
515+
516+
Args:
517+
value: The value to be used for the update.
518+
condition: (Optional) A callable that returns a boolean array when applied to the data.
519+
"""
520+
if condition is None:
521+
np.copyto(self.data, value) # In-place update
522+
else:
523+
# Ensure condition is a boolean array of the same shape as self.data
524+
if (
525+
not isinstance(condition, np.ndarray)
526+
or condition.shape != self.data.shape
527+
):
528+
raise ValueError(
529+
"Condition must be a NumPy array with the same shape as the grid."
530+
)
531+
np.copyto(self.data, value, where=condition) # Conditional in-place update
532+
533+
def modify_cell(self, position: Coordinate, operation, value=None):
534+
"""
535+
Modify a single cell using an operation, which can be a lambda function or a NumPy ufunc.
536+
If a NumPy ufunc is used, an additional value should be provided.
537+
538+
Args:
539+
position: The grid coordinates of the cell to modify.
540+
operation: A function to apply. Can be a lambda function or a NumPy ufunc.
541+
value: The value to be used if the operation is a NumPy ufunc. Ignored for lambda functions.
542+
"""
543+
current_value = self.data[position]
544+
545+
# Determine if the operation is a lambda function or a NumPy ufunc
546+
if is_lambda_function(operation):
547+
# Lambda function case
548+
self.data[position] = operation(current_value)
549+
elif value is not None:
550+
# NumPy ufunc case
551+
self.data[position] = operation(current_value, value)
552+
else:
553+
raise ValueError("Invalid operation or missing value for NumPy ufunc.")
554+
555+
def modify_cells(self, operation, value=None, condition_function=None):
556+
"""
557+
Modify cells using an operation, which can be a lambda function or a NumPy ufunc.
558+
If a NumPy ufunc is used, an additional value should be provided.
559+
560+
Args:
561+
operation: A function to apply. Can be a lambda function or a NumPy ufunc.
562+
value: The value to be used if the operation is a NumPy ufunc. Ignored for lambda functions.
563+
condition_function: (Optional) A callable that returns a boolean array when applied to the data.
564+
"""
565+
if condition_function is not None:
566+
condition_array = np.vectorize(condition_function)(self.data)
567+
else:
568+
condition_array = np.ones_like(self.data, dtype=bool) # All cells
569+
570+
# Check if the operation is a lambda function or a NumPy ufunc
571+
if is_lambda_function(operation):
572+
# Lambda function case
573+
modified_data = np.vectorize(operation)(self.data)
574+
elif value is not None:
575+
# NumPy ufunc case
576+
modified_data = operation(self.data, value)
577+
else:
578+
raise ValueError("Invalid operation or missing value for NumPy ufunc.")
579+
580+
self.data = np.where(condition_array, modified_data, self.data)
581+
582+
def select_cells(self, condition, return_list=True):
583+
"""
584+
Find cells that meet a specified condition using NumPy's boolean indexing, in-place.
585+
586+
Args:
587+
condition: A callable that returns a boolean array when applied to the data.
588+
return_list: (Optional) If True, return a list of (x, y) tuples. Otherwise, return a boolean array.
589+
590+
Returns:
591+
A list of (x, y) tuples or a boolean array.
592+
"""
593+
condition_array = condition(self.data)
594+
if return_list:
595+
return list(zip(*np.where(condition_array)))
596+
else:
597+
return condition_array
598+
599+
def aggregate_property(self, operation):
600+
"""Perform an aggregate operation (e.g., sum, mean) on a property across all cells.
601+
602+
Args:
603+
operation: A function to apply. Can be a lambda function or a NumPy ufunc.
604+
"""
605+
606+
# Check if the operation is a lambda function or a NumPy ufunc
607+
if is_lambda_function(operation):
608+
# Lambda function case
609+
return operation(self.data)
610+
else:
611+
# NumPy ufunc case
612+
return operation(self.data)
613+
614+
615+
class _PropertyGrid(_Grid):
616+
def __init__(self, width: int, height: int, torus: bool):
617+
super().__init__(width, height, torus)
618+
self.properties = {}
619+
620+
# Add and remove properties to the grid
621+
def add_property_layer(self, PropertyLayer):
622+
self.properties[PropertyLayer.name] = PropertyLayer
623+
624+
def remove_property_layer(self, property_name: str):
625+
if property_name not in self.properties:
626+
raise ValueError(f"Property layer {property_name} does not exist.")
627+
del self.properties[property_name]
628+
629+
# TODO:
630+
# - Select cells conditionally based on multiple properties
631+
# - Move random cells conditionally based on multiple properties
632+
# - Move to cell with highest/lowest/closest property value
633+
634+
635+
class SingleGrid(_PropertyGrid):
489636
"""Rectangular grid where each cell contains exactly at most one agent.
490637
491638
Grid cells are indexed by [x, y], where [0, 0] is assumed to be the
@@ -528,7 +675,7 @@ def remove_agent(self, agent: Agent) -> None:
528675
agent.pos = None
529676

530677

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

0 commit comments

Comments
 (0)