Skip to content

Commit 307fcf8

Browse files
committed
_PropertyGrid: Add optional neighborhood filtering to spatial methods
- Updated `select_cells_multi_properties`, `move_agent_to_random_cell`, and `move_agent_to_extreme_value_cell` methods in the `_PropertyGrid` class to include an optional neighborhood filtering feature. - Added `only_neighborhood` parameter to these methods to allow for conditional operations within a specified neighborhood around an agent's position. - Introduced `get_neighborhood_mask` as a helper function to create a boolean mask for neighborhood-based selections, enhancing performance and readability. - Modified methods to utilize NumPy for efficient array operations, improving the overall performance of grid-based spatial calculations. - Ensured backward compatibility by setting `only_neighborhood` to `False` by default, allowing existing code to function without modification.
1 parent 784fdd7 commit 307fcf8

File tree

1 file changed

+108
-18
lines changed

1 file changed

+108
-18
lines changed

mesa/space.py

Lines changed: 108 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -647,17 +647,50 @@ def remove_property_layer(self, property_name: str):
647647
raise ValueError(f"Property layer {property_name} does not exist.")
648648
del self.properties[property_name]
649649

650-
def select_cells_multi_properties(self, conditions: dict) -> List[Coordinate]:
650+
def get_neighborhood_mask(
651+
self, pos: Coordinate, moore: bool, include_center: bool, radius: int
652+
) -> np.ndarray:
651653
"""
652-
Select cells based on multiple property conditions using NumPy.
654+
Generate a boolean mask representing the neighborhood.
655+
656+
Args:
657+
pos (Coordinate): Center of the neighborhood.
658+
moore (bool): True for Moore neighborhood, False for Von Neumann.
659+
include_center (bool): Include the central cell in the neighborhood.
660+
radius (int): The radius of the neighborhood.
661+
662+
Returns:
663+
np.ndarray: A boolean mask representing the neighborhood.
664+
"""
665+
neighborhood = self.get_neighborhood(pos, moore, include_center, radius)
666+
mask = np.zeros((self.width, self.height), dtype=bool)
667+
668+
# Convert the neighborhood list to a NumPy array and use advanced indexing
669+
coords = np.array(neighborhood)
670+
mask[coords[:, 0], coords[:, 1]] = True
671+
return mask
672+
673+
def select_cells_multi_properties(
674+
self,
675+
conditions: dict,
676+
only_neighborhood: bool = False,
677+
pos: None | Coordinate = None,
678+
moore: bool = True,
679+
include_center: bool = False,
680+
radius: int = 1,
681+
) -> list[Coordinate]:
682+
"""
683+
Select cells based on multiple property conditions using NumPy, optionally within a neighborhood.
653684
654685
Args:
655686
conditions (dict): A dictionary where keys are property names and values are
656687
callables that take a single argument (the property value)
657688
and return a boolean.
689+
only_neighborhood (bool): If True, restrict selection to the neighborhood.
690+
pos, moore, include_center, radius: Optional neighborhood parameters.
658691
659692
Returns:
660-
List[Coordinate]: A list of coordinates where the conditions are satisfied.
693+
List[Coordinate]: Coordinates where conditions are satisfied.
661694
"""
662695
# Start with a mask of all True values
663696
combined_mask = np.ones((self.width, self.height), dtype=bool)
@@ -669,50 +702,107 @@ def select_cells_multi_properties(self, conditions: dict) -> List[Coordinate]:
669702
# Combine with the existing mask using logical AND
670703
combined_mask = np.logical_and(combined_mask, prop_mask)
671704

705+
if only_neighborhood and pos is not None:
706+
neighborhood_mask = self.get_neighborhood_mask(
707+
pos, moore, include_center, radius
708+
)
709+
combined_mask = np.logical_and(combined_mask, neighborhood_mask)
710+
672711
# Extract coordinates from the combined mask
673712
selected_cells = list(zip(*np.where(combined_mask)))
674713
return selected_cells
675714

676-
def move_agent_to_random_cell(self, agent: Agent, conditions: dict) -> None:
715+
def move_agent_to_random_cell(
716+
self,
717+
agent: Agent,
718+
conditions: dict,
719+
only_neighborhood: bool = False,
720+
moore: bool = True,
721+
include_center: bool = False,
722+
radius: int = 1,
723+
) -> None:
677724
"""
678-
Move an agent to a random cell that meets specified property conditions.
725+
Move an agent to a random cell that meets specified property conditions, optionally within a neighborhood.
679726
If no eligible cells are found, issue a warning and keep the agent in its current position.
680727
681728
Args:
682729
agent (Agent): The agent to move.
683730
conditions (dict): Conditions for selecting the cell.
731+
only_neighborhood, moore, include_center, radius: Optional neighborhood parameters.
684732
"""
685-
eligible_cells = self.select_cells_multi_properties(conditions)
733+
pos = agent.pos if only_neighborhood else None
734+
eligible_cells = self.select_cells_multi_properties(
735+
conditions,
736+
only_neighborhood,
737+
pos,
738+
moore,
739+
include_center,
740+
radius,
741+
)
686742
if not eligible_cells:
687-
warn(f"No eligible cells found. Agent {agent.unique_id} remains in the current position.", RuntimeWarning)
743+
warn(
744+
f"No eligible cells found. Agent {agent.unique_id} remains in the current position.",
745+
RuntimeWarning, stacklevel=2
746+
)
688747
return # Agent stays in the current position
689748

690749
# Randomly choose one of the eligible cells and move the agent
691750
new_pos = agent.random.choice(eligible_cells)
692751
self.move_agent(agent, new_pos)
693752

694-
def move_agent_to_extreme_value_cell(self, agent: Agent, property_name: str, mode: str) -> None:
753+
def move_agent_to_extreme_value_cell(
754+
self,
755+
agent: Agent,
756+
property_name: str,
757+
mode: str,
758+
only_neighborhood: bool = False,
759+
moore: bool = True,
760+
include_center: bool = False,
761+
radius: int = 1,
762+
) -> None:
695763
"""
696-
Move an agent to a cell with the highest, lowest, or closest property value.
764+
Move an agent to a cell with the highest, lowest, or closest property value,
765+
optionally within a neighborhood.
697766
698767
Args:
699768
agent (Agent): The agent to move.
700769
property_name (str): The name of the property layer.
701770
mode (str): 'highest', 'lowest', or 'closest'.
771+
only_neighborhood, moore, include_center, radius: Optional neighborhood parameters.
702772
"""
773+
pos = agent.pos if only_neighborhood else None
703774
prop_values = self.properties[property_name].data
704-
if mode == 'highest':
705-
target_value = np.max(prop_values)
706-
elif mode == 'lowest':
707-
target_value = np.min(prop_values)
708-
elif mode == 'closest':
775+
776+
777+
if pos is not None:
778+
# Mask out cells outside the neighborhood.
779+
neighborhood_mask = self.get_neighborhood_mask(
780+
pos, moore, include_center, radius
781+
)
782+
# Use NaN for out-of-neighborhood cells
783+
masked_prop_values = np.where(neighborhood_mask, prop_values, np.nan)
784+
else:
785+
masked_prop_values = prop_values
786+
787+
# Find the target value
788+
if mode == "highest":
789+
target_value = np.nanmax(masked_prop_values)
790+
elif mode == "lowest":
791+
target_value = np.nanmin(masked_prop_values)
792+
elif mode == "closest":
709793
agent_value = prop_values[agent.pos]
710-
target_value = prop_values[np.abs(prop_values - agent_value).argmin()]
794+
target_value = masked_prop_values[
795+
np.nanargmin(np.abs(masked_prop_values - agent_value))
796+
]
711797
else:
712-
raise ValueError(f"Invalid mode {mode}. Choose from 'highest', 'lowest', or 'closest'.")
798+
raise ValueError(
799+
f"Invalid mode {mode}. Choose from 'highest', 'lowest', or 'closest'."
800+
)
713801

714-
target_cells = list(zip(*np.where(prop_values == target_value)))
715-
new_pos = agent.random.choice(target_cells)
802+
# Find the coordinates of the target value(s)
803+
target_cells = np.column_stack(np.where(masked_prop_values == target_value))
804+
# If there are multiple target cells, randomly choose one
805+
new_pos = tuple(agent.random.choice(target_cells, axis=0))
716806
self.move_agent(agent, new_pos)
717807

718808

0 commit comments

Comments
 (0)