Source code for experimental.scenarios.scenario

"""Base Scenario class."""

from collections import defaultdict
from collections.abc import Sequence
from functools import partial
from itertools import count
from typing import TYPE_CHECKING, Any, ClassVar

import numpy as np

SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence
RNGLike = np.random.Generator | np.random.BitGenerator


if TYPE_CHECKING:
    from mesa.model import Model


[docs] class Scenario[M: Model]: """A Scenario class for defining model parameters and experiments. Supports both simple instantiation and type-hinted subclassing: # Simple usage scenario = Scenario(rng=42, density=0.8, minority_pc=0.5) # Type-hinted subclass (recommended for complex models) class MyScenario(Scenario): citizen_density: float = 0.7 cop_vision: int = 7 movement: bool = True scenario = MyScenario(rng=42, cop_vision=10) # Override defaults Attributes: model: The model instance to which this scenario belongs scenario_id: A unique identifier for this scenario, auto-generated starting from 0 rng: Random number generator or seed value Notes: All parameters are accessible via attribute access (scenario.param). Class-level attributes in subclasses serve as default values. Scenario parameters cannot be modified during model execution. """ _ids: ClassVar[defaultdict] = defaultdict(partial(count, 0)) _scenario_defaults: ClassVar[dict[str, Any]] = {} __slots__ = ("__dict__", "_scenario_id", "model") @classmethod def __init_subclass__(cls): """Called once when a subclass is created.""" # Collect defaults once and cache on the class defaults = {} for base in reversed(cls.__mro__): if base is Scenario or base is object: continue annotations = getattr(base, "__annotations__", {}) for key in annotations: if hasattr(base, key) and not key.startswith("_"): defaults[key] = getattr(base, key) # Cache on the class itself cls._scenario_defaults = defaults @classmethod def _reset_counter(cls): """Reset the scenario counter for this class.""" cls._ids[cls] = count(0) def __init__(self, *, rng: RNGLike | SeedLike | None = None, **kwargs): """Initialize a Scenario. Args: rng: Random number generator or valid seed value **kwargs: All other scenario parameters (override class-level defaults) """ self.model: M | None = None self._scenario_id: int = ( next(self._ids[self.__class__]) if "_scenario_id" not in kwargs else kwargs.pop("_scenario_id") ) self.__dict__.update(self._scenario_defaults) self.__dict__.update(kwargs) self.__dict__["rng"] = rng def __iter__(self): """Iterate over (key, value) pairs.""" return iter(self.__dict__.items()) def __len__(self): """Return number of parameters.""" return len(self.__dict__) def __setattr__(self, name: str, value: object) -> None: """Prevent modification during model execution.""" try: if self.model and self.model.running: raise ValueError( f"Cannot change scenario parameter '{name}' during model run." ) except AttributeError: # During initialization when self.model doesn't exist yet pass super().__setattr__(name, value)
[docs] def to_dict(self) -> dict[str, Any]: """Return dict representation of the scenario.""" return {**self.__dict__, "model": self.model, "_scenario_id": self._scenario_id}
# def scenarios_from_dataframe( # experiments: pd.DataFrame, rng: int | Iterable[SeedLike] # ) -> list[Scenario]: # """Turn a dataframe into a list of scenarios. # # Args: # experiments: Dataframe containing the parameters for the scenarios. # rng: the number of random seeds to use or a list of seeds. # # Returns: # a list of scenario instances # # If rng is an integer, numpy will be used to generate that many seed values. # # """ # if not isinstance(rng, Iterable): # rng = np.random.default_rng(42).integers(0, high=sys.maxsize, size=(rng,)) # # scenarios = [] # for i, entry in enumerate(experiments.to_dict(orient="records")): # for seed in rng: # scenarios.append(Scenario(rng=seed, _experiment_id=i, **entry)) # # return scenarios # def scenarios_from_numpy( # experiments: np.ndarray, parameter_names: list[str], rng: int | Iterable[SeedLike] # ) -> list[Scenario]: # """Turn a numpy array into a list of scenarios. # # Args: # experiments: Dataframe containing the parameters for the scenarios. # parameter_names: the names of the parameters # rng: the number of random seeds to use or a list of seeds. # # Returns: # a list of scenario instances # # If rng is an integer, numpy will be used to generate that many seed values. # # """ # if len(parameter_names) != experiments.shape[1]: # raise ValueError( # "The number of parameter names does not match the number of columns in the numpy array." # ) # # return scenarios_from_dataframe( # pd.DataFrame(experiments, columns=parameter_names), rng # )