Source code for mesa.agent

"""Agent related classes.

Core Objects: Agent.
"""

# Postpone annotation evaluation to avoid NameError from forward references (PEP 563). Remove once Python 3.14+ is required.
from __future__ import annotations

import contextlib
import itertools
from random import Random
from typing import TYPE_CHECKING, ClassVar

import numpy as np
import pandas as pd

if TYPE_CHECKING:
    from mesa.experimental.actions import Action
    from mesa.model import Model

from mesa.agentset import AgentSet


[docs] class Agent[M: Model]: """Base class for a model agent in Mesa. Attributes: model (Model): A reference to the model instance. unique_id (int): A unique identifier for this agent. Notes: Agents must be hashable to be used in an AgentSet. In Python 3, defining `__eq__` without `__hash__` makes an object unhashable, which will break AgentSet usage. unique_id is unique relative to a model instance and starts from 1 """ _datasets: ClassVar = set() def __init_subclass__(cls, **kwargs): """Called when DatasetTrackedAgent is subclassed.""" super().__init_subclass__(**kwargs) # Each subclass gets its own dataset set # we use strings on this to avoid memory leaks # and ensure the retrieved dataset belongs to the same # model instance as the agent cls._datasets = set() def __init__(self, model: M, *args, **kwargs) -> None: """Create a new agent. Args: model (Model): The model instance in which the agent exists. args: Passed on to super. kwargs: Passed on to super. Notes: to make proper use of python's super, in each class remove the arguments and keyword arguments you need and pass on the rest to super """ super().__init__(*args, **kwargs) self.model: M = model self.unique_id = None self.current_action: Action | None = None self.model.register_agent(self) for dataset in self._datasets: self.model.data_registry[dataset].add_agent(self)
[docs] def remove(self) -> None: """Remove and delete the agent from the model. If the agent is currently performing an action, the action's scheduled completion event is cancelled silently. The action's on_interrupt() callback is NOT fired, because the agent is being destroyed — not making a behavioral decision. The action moves to no defined end state; it is simply abandoned. If your action holds external resources (e.g., a Resource slot, a reservation, a lock), override Agent.remove() and call self.cancel_action() before super().remove() to ensure on_interrupt() fires and cleanup logic runs: def remove(self): self.cancel_action() # Fires on_interrupt for cleanup super().remove() Notes: This is a deliberate design choice. The default silent cleanup is safe and avoids callbacks touching agent state during teardown. Models that need cleanup should opt in explicitly. """ if self.current_action is not None: self.current_action._cancel_event() # Silent cleanup, no callback self.current_action = None with contextlib.suppress(KeyError): self.model.deregister_agent(self) # ensures models are also removed from datasets for dataset in self._datasets: self.model.data_registry[dataset].remove_agent(self)
[docs] def step(self) -> None: """A single step of the agent."""
[docs] def advance(self) -> None: # noqa: D102 pass
[docs] @classmethod def create_agents[T: Agent]( cls: type[T], model: Model, n: int, *args, **kwargs ) -> AgentSet[T]: """Create N agents. Args: model: the model to which the agents belong args: arguments to pass onto agent instances each arg is either a single object or a sequence of length n n: the number of agents to create kwargs: keyword arguments to pass onto agent instances each keyword arg is either a single object or a sequence of length n Returns: AgentSet containing the agents created. """ agents = [] if not args and not kwargs: for _ in range(n): agents.append(cls(model)) return AgentSet(agents, random=model.random) # Prepare positional argument iterators arg_iters = [] for arg in args: if isinstance(arg, (list, np.ndarray, tuple, pd.Series)) and len(arg) == n: arg_iters.append(arg) else: arg_iters.append(itertools.repeat(arg, n)) # Prepare keyword argument iterators kw_keys = list(kwargs.keys()) kw_val_iters = [] for v in kwargs.values(): if isinstance(v, (list, np.ndarray, tuple, pd.Series)) and len(v) == n: kw_val_iters.append(v) else: kw_val_iters.append(itertools.repeat(v, n)) # If arg_iters is empty, zip(*[]) returns nothing, so we use repeat(()) pos_iter = zip(*arg_iters) if arg_iters else itertools.repeat(()) kw_iter = zip(*kw_val_iters) if kw_val_iters else itertools.repeat(()) # We rely on range(n) to drive the loop length if kwargs: for _, p_args, k_vals in zip(range(n), pos_iter, kw_iter): agents.append(cls(model, *p_args, **dict(zip(kw_keys, k_vals)))) else: for _, p_args in zip(range(n), pos_iter): agents.append(cls(model, *p_args)) return AgentSet(agents, random=model.random)
[docs] @classmethod def from_dataframe[T: Agent]( cls: type[T], model: Model, df: pd.DataFrame, **kwargs ) -> AgentSet[T]: """Create agents from a pandas DataFrame. Each row of the DataFrame represents one agent. The DataFrame columns are mapped to the agent's constructor as keyword arguments. Additional keyword arguments (`**kwargs`) can be used to set constant attributes for all agents. Args: model: The model instance. df: The pandas DataFrame. Each row represents an agent. **kwargs: Constant values to pass to every agent's constructor. Only non-sequence data is allowed in kwargs to avoid ambiguity with DataFrame columns. Returns: AgentSet containing the agents created. Note: If you need to pass variable data or sequences, add them as columns to the DataFrame before calling this method. """ for key, value in kwargs.items(): if isinstance(value, (list, np.ndarray, tuple, pd.Series)): raise TypeError( f"from_dataframe does not support sequence data in kwargs ('{key}'). " "Please add this data to the DataFrame before calling from_dataframe." ) agents = [ cls(model, **{**record, **kwargs}) for record in df.to_dict(orient="records") ] return AgentSet(agents, random=model.random)
def __str__(self) -> str: """Return a human-readable string representation of the agent.""" return f"{self.__class__.__name__}, agent_id = {self.unique_id}" @property def random(self) -> Random: """Return a seeded stdlib rng.""" return self.model.random @property def rng(self) -> np.random.Generator: """Return a seeded np.random rng.""" return self.model.rng @property def scenario(self): """Return the scenario associated with the model.""" return self.model.scenario # Actions methods
[docs] def start_action(self, action: Action) -> Action: """Start performing an action. The action must be in PENDING or INTERRUPTED state and the agent must not be currently performing another action. Args: action: The Action to perform. Must have been created with this agent as its agent. Returns: The started Action. Raises: ValueError: If the agent is already performing an action, or if the action doesn't belong to this agent. """ if self.current_action is not None: raise ValueError( f"Agent {self.unique_id} is already performing an action " f"({self.current_action!r}). Use interrupt_for() or " f"cancel_action() first." ) if action.agent is not self: raise ValueError( f"Action's agent (id={action.agent.unique_id}) does not match " f"this agent (id={self.unique_id})." ) self.current_action = action action.start() # If the action completed instantly (duration=0), start() already # called _do_complete which cleared current_action via the Action. return action
[docs] def interrupt_for(self, new_action: Action) -> bool: """Interrupt the current action and start a new one. If there is no current action, simply starts the new one. If the current action is non-interruptible, returns False and does nothing. Args: new_action: The Action to perform instead. Returns: True if the new action was started (either no current action, or the current one was successfully interrupted). False if the current action is non-interruptible. """ if self.current_action is not None and not self.current_action.interrupt(): return False # interrupt() already cleared current_action self.start_action(new_action) return True
[docs] def cancel_action(self) -> bool: """Cancel the current action, ignoring interruptible flag. Calls on_interrupt with partial progress. Returns False only if there is no current action. Returns: True if an action was cancelled, False if idle. """ if self.current_action is None: return False self.current_action.cancel() # cancel() already cleared current_action return True
@property def is_busy(self) -> bool: """Whether the agent is currently performing an action.""" return self.current_action is not None