Source code for mesa.experimental.cell_space.cell_collection

"""Collection class for managing and querying groups of cells.

The CellCollection class provides a consistent interface for operating on multiple
cells, supporting:
- Filtering and selecting cells based on conditions
- Random cell and agent selection
- Access to contained agents
- Group operations

This is useful for implementing area effects, zones, or any operation that needs
to work with multiple cells as a unit. The collection handles efficient iteration
and agent access across cells. The class is used throughout the cell space
implementation to represent neighborhoods, selections, and other cell groupings.
"""

from __future__ import annotations

import itertools
import warnings
from collections.abc import Callable, Iterable, Mapping
from functools import cached_property
from random import Random
from typing import TYPE_CHECKING, Generic, TypeVar

if TYPE_CHECKING:
    from mesa.experimental.cell_space.cell import Cell
    from mesa.experimental.cell_space.cell_agent import CellAgent

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


[docs] class CellCollection(Generic[T]): """An immutable collection of cells. Attributes: cells (List[Cell]): The list of cells this collection represents agents (List[CellAgent]) : List of agents occupying the cells in this collection random (Random) : The random number generator Notes: A `UserWarning` is issued if `random=None`. You can resolve this warning by explicitly passing a random number generator. In most cases, this will be the seeded random number generator in the model. So, you would do `random=self.random` in a `Model` or `Agent` instance. """ def __init__( self, cells: Mapping[T, list[CellAgent]] | Iterable[T], random: Random | None = None, ) -> None: """Initialize a CellCollection. Args: cells: cells to add to the collection random: a seeded random number generator. """ if isinstance(cells, dict): self._cells = cells else: self._cells = {cell: cell.agents for cell in cells} # Get capacity from first cell if collection is not empty self._capacity: int | None = ( next(iter(self._cells.keys())).capacity if self._cells else None ) if random is None: warnings.warn( "Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly", UserWarning, stacklevel=2, ) random = Random() self.random = random def __iter__(self): # noqa return iter(self._cells) def __getitem__(self, key: T) -> Iterable[CellAgent]: # noqa return self._cells[key] # @cached_property def __len__(self) -> int: # noqa return len(self._cells) def __repr__(self): # noqa return f"CellCollection({self._cells})"
[docs] @cached_property def cells(self) -> list[T]: # noqa return list(self._cells.keys())
@property def agents(self) -> Iterable[CellAgent]: # noqa return itertools.chain.from_iterable(self._cells.values())
[docs] def select_random_cell(self) -> T: """Select a random cell.""" return self.random.choice(self.cells)
[docs] def select_random_agent(self) -> CellAgent: """Select a random agent. Returns: CellAgent instance """ return self.random.choice(list(self.agents))
[docs] def select( self, filter_func: Callable[[T], bool] | None = None, at_most: int | float = float("inf"), ): """Select cells based on filter function. Args: filter_func: filter function at_most: The maximum amount of cells to select. Defaults to infinity. - If an integer, at most the first number of matching cells is selected. - If a float between 0 and 1, at most that fraction of original number of cells Returns: CellCollection """ if filter_func is None and at_most == float("inf"): return self if at_most <= 1.0 and isinstance(at_most, float): at_most = int(len(self) * at_most) # Note that it rounds down (floor) def cell_generator(filter_func, at_most): count = 0 for cell in self: if count >= at_most: break if not filter_func or filter_func(cell): yield cell count += 1 return CellCollection(cell_generator(filter_func, at_most), random=self.random)