Source code for mesa.model
"""The model class for Mesa framework.
Core Objects: Model
"""
# Mypy; for the `|` operator purpose
# Remove this __future__ import once the oldest supported Python is 3.10
from __future__ import annotations
import random
import sys
from collections.abc import Sequence
# mypy
from typing import Any
import numpy as np
from mesa.agent import Agent, AgentSet
from mesa.mesa_logging import create_module_logger, method_logger
SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence
RNGLike = np.random.Generator | np.random.BitGenerator
_mesa_logger = create_module_logger()
[docs]
class Model:
"""Base class for models in the Mesa ABM library.
This class serves as a foundational structure for creating agent-based models.
It includes the basic attributes and methods necessary for initializing and
running a simulation model.
Attributes:
running: A boolean indicating if the model should continue running.
steps: the number of times `model.step()` has been called.
random: a seeded python.random number generator.
rng : a seeded numpy.random.Generator
Notes:
Model.agents returns the AgentSet containing all agents registered with the model. Changing
the content of the AgentSet directly can result in strange behavior. If you want change the
composition of this AgentSet, ensure you operate on a copy.
"""
@method_logger(__name__)
def __init__(
self,
*args: Any,
seed: float | None = None,
rng: RNGLike | SeedLike | None = None,
**kwargs: Any,
) -> None:
"""Create a new model.
Overload this method with the actual code to initialize the model. Always start with super().__init__()
to initialize the model object properly.
Args:
args: arguments to pass onto super
seed: the seed for the random number generator
rng : Pseudorandom number generator state. When `rng` is None, a new `numpy.random.Generator` is created
using entropy from the operating system. Types other than `numpy.random.Generator` are passed to
`numpy.random.default_rng` to instantiate a `Generator`.
kwargs: keyword arguments to pass onto super
Notes:
you have to pass either seed or rng, but not both.
"""
super().__init__(*args, **kwargs)
self.running = True
self.steps: int = 0
if (seed is not None) and (rng is not None):
raise ValueError("you have to pass either rng or seed, not both")
elif seed is None:
self.rng: np.random.Generator = np.random.default_rng(rng)
self._rng = (
self.rng.bit_generator.state
) # this allows for reproducing the rng
try:
self.random = random.Random(rng)
except TypeError:
seed = int(self.rng.integers(np.iinfo(np.int32).max))
self.random = random.Random(seed)
self._seed = seed # this allows for reproducing stdlib.random
elif rng is None:
self.random = random.Random(seed)
self._seed = seed # this allows for reproducing stdlib.random
try:
self.rng: np.random.Generator = np.random.default_rng(seed)
except TypeError:
rng = self.random.randint(0, sys.maxsize)
self.rng: np.random.Generator = np.random.default_rng(rng)
self._rng = self.rng.bit_generator.state
# Wrap the user-defined step method
self._user_step = self.step
self.step = self._wrapped_step
# setup agent registration data structures
self._agents = {} # the hard references to all agents in the model
self._agents_by_type: dict[
type[Agent], AgentSet
] = {} # a dict with an agentset for each class of agents
self._all_agents = AgentSet(
[], random=self.random
) # an agenset with all agents
def _wrapped_step(self, *args: Any, **kwargs: Any) -> None:
"""Automatically increments time and steps after calling the user's step method."""
# Automatically increment time and step counters
self.steps += 1
_mesa_logger.info(f"calling model.step for timestep {self.steps} ")
# Call the original user-defined step method
self._user_step(*args, **kwargs)
@property
def agents(self) -> AgentSet:
"""Provides an AgentSet of all agents in the model, combining agents from all types."""
return self._all_agents
@agents.setter
def agents(self, agents: Any) -> None:
raise AttributeError(
"You are trying to set model.agents. In Mesa 3.0 and higher, this attribute is "
"used by Mesa itself, so you cannot use it directly anymore."
"Please adjust your code to use a different attribute name for custom agent storage."
)
@property
def agent_types(self) -> list[type]:
"""Return a list of all unique agent types registered with the model."""
return list(self._agents_by_type.keys())
@property
def agents_by_type(self) -> dict[type[Agent], AgentSet]:
"""A dictionary where the keys are agent types and the values are the corresponding AgentSets."""
return self._agents_by_type
[docs]
def register_agent(self, agent):
"""Register the agent with the model.
Args:
agent: The agent to register.
Notes:
This method is called automatically by ``Agent.__init__``, so there is no need to use this
if you are subclassing Agent and calling its super in the ``__init__`` method.
"""
self._agents[agent] = None
# because AgentSet requires model, we cannot use defaultdict
# tricks with a function won't work because model then cannot be pickled
try:
self._agents_by_type[type(agent)].add(agent)
except KeyError:
self._agents_by_type[type(agent)] = AgentSet(
[
agent,
],
random=self.random,
)
self._all_agents.add(agent)
_mesa_logger.debug(
f"registered {agent.__class__.__name__} with agent_id {agent.unique_id}"
)
[docs]
def deregister_agent(self, agent):
"""Deregister the agent with the model.
Args:
agent: The agent to deregister.
Notes:
This method is called automatically by ``Agent.remove``
"""
del self._agents[agent]
self._agents_by_type[type(agent)].remove(agent)
self._all_agents.remove(agent)
_mesa_logger.debug(f"deregistered agent with agent_id {agent.unique_id}")
[docs]
def run_model(self) -> None:
"""Run the model until the end condition is reached.
Overload as needed.
"""
while self.running:
self.step()
[docs]
def step(self) -> None:
"""A single step. Fill in here."""
[docs]
def reset_randomizer(self, seed: int | None = None) -> None:
"""Reset the model random number generator.
Args:
seed: A new seed for the RNG; if None, reset using the current seed
"""
if seed is None:
seed = self._seed
self.random.seed(seed)
self._seed = seed
[docs]
def reset_rng(self, rng: RNGLike | SeedLike | None = None) -> None:
"""Reset the model random number generator.
Args:
rng: A new seed for the RNG; if None, reset using the current seed
"""
self.rng = np.random.default_rng(rng)
self._rng = self.rng.bit_generator.state
[docs]
def remove_all_agents(self):
"""Remove all agents from the model.
Notes:
This method calls agent.remove for all agents in the model. If you need to remove agents from
e.g., a SingleGrid, you can either explicitly implement your own agent.remove method or clean this up
near where you are calling this method.
"""
# we need to wrap keys in a list to avoid a RunTimeError: dictionary changed size during iteration
for agent in list(self._agents.keys()):
agent.remove()