Source code for experimental.scenarios.runner
"""Classes for running parameter sweeps over scenarios."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import pandas as pd
from mesa.exceptions import MesaException
if TYPE_CHECKING:
from mesa.experimental.scenarios import Scenario
from mesa.model import Model
[docs]
class RunConfiguration:
"""Defines how a single Scenario is executed and what is extracted from it.
Can be used as is for simple use cases or subclassed by overriding one or more of the following
methods
- ``instantiate_model`` — construct a Model from a Scenario (default:
``model_class(*model_args, scenario=scenario, **model_kwargs)``).
- ``run_model`` — advance the model. Default delegates to ``model.run_until`` based on the ``until`` attribute.
Override for alternative run control
- ``extract_output`` — return a dict with outcome names as key and dataframes as values
Stopping is the model's responsibility. ``RunConfiguration`` only chooses
which run primitive to call.
"""
def __init__(
self,
model_class: type[Model],
until: float | int,
model_args: None | list[Any] = None,
model_kwargs: None | dict[str, Any] = None,
outcomes: None | str | list[str] = None,
data_recorder_attr_name="data_recorder",
):
"""Initialize a RunConfiguration object.
Args:
model_class: the model class to instantiate
until: until which time to run the model
model_args: any additional model arguments
model_kwargs: any additional model keyword arguments
outcomes: the outcomes to extract. If None, extract all outcomes.
data_recorder_attr_name : the name of the data recorder attribute to use on the model
"""
# we need to avoid circular imports
from mesa.model import Model # noqa: PLC0415
if not (isinstance(model_class, type) and issubclass(model_class, Model)):
raise TypeError("model_class must be a subclass of Model")
if not isinstance(until, (int, float)):
raise TypeError("until must be an int or float")
if until <= 0:
raise ValueError("until must be positive")
self.model_class = model_class
self.model_args = [] if model_args is None else model_args
self.model_kwargs = {} if model_kwargs is None else model_kwargs
self.until = until
# fixme:: this code leaves it to the user to set the attribute to which the recorder is assigned
# this is probably a a convention that needs to be pinned down explicitly on the model class
self.data_recorder_attr_name = data_recorder_attr_name
if isinstance(outcomes, str):
outcomes = [outcomes]
self.outcomes = outcomes
[docs]
def instantiate_model(self, scenario: Scenario) -> Model:
"""Instantiate the model."""
try:
return self.model_class(
*self.model_args, scenario=scenario, **self.model_kwargs
)
except Exception as e:
raise ModelInstantiationError(
f"Failed to instantiate {self.model_class.__name__} "
f"Please check your model_args and model_kwargs.\n"
f" - Passed args: {self.model_args}\n"
f" - Passed kwargs: {self.model_kwargs}\n"
f" - for scenario: {{'scenario': {scenario}}}\n"
) from e
[docs]
def run_model(self, model: Model) -> None:
"""Run the model."""
model.run_until(self.until)
def __call__(self, scenario: Scenario) -> dict[str, pd.DataFrame]:
"""Run the scenario and extract output."""
model = self.instantiate_model(scenario)
self.run_model(model)
output = self.extract_output(model)
return output
[docs]
class ModelInstantiationError(MesaException):
"""Raised when a model cannot be instantiated for a scenario."""