Source code for experimental.devs.simulator

"""Simulator implementations for different time advancement approaches in Mesa.

This module provides simulator classes that control how simulation time advances and how
events are executed. It supports both discrete-time and continuous-time simulations through
three main classes:

- Simulator: Base class defining the core simulation control interface
- ABMSimulator: A simulator for agent-based models that combines fixed time steps with
  event scheduling. Uses integer time units and automatically schedules model.step()
- DEVSimulator: A pure discrete event simulator using floating-point time units for
  continuous time simulation

Key features:
- Flexible time units (integer or float)
- Event scheduling using absolute or relative times
- Priority-based event execution
- Support for running simulations for specific durations or until specific end times

The simulators enable Mesa models to use traditional time-step based approaches, pure
event-driven approaches, or hybrid combinations of both.
"""

from __future__ import annotations

import numbers
from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from .eventlist import EventList, Priority, SimulationEvent

if TYPE_CHECKING:
    from mesa import Model


[docs] class Simulator: """The Simulator controls the time advancement of the model. The simulator uses next event time progression to advance the simulation time, and execute the next event Attributes: event_list (EventList): The list of events to execute time (float | int): The current simulation time time_unit (type) : The unit of the simulation time model (Model): The model to simulate """ # TODO: add replication support # TODO: add experimentation support def __init__(self, time_unit: type, start_time: int | float): """Initialize a Simulator instance. Args: time_unit: type of the smulaiton time start_time: the starttime of the simulator """ # should model run in a separate thread, # and we can then interact with start, stop, run_until, and step? self.event_list = EventList() self.start_time = start_time self.time_unit = time_unit self.time = self.start_time self.model = None def check_time_unit(self, time: int | float) -> bool: ... # noqa: D102
[docs] def setup(self, model: Model) -> None: """Set up the simulator with the model to simulate. Args: model (Model): The model to simulate Raises: Exception if simulator.time is not equal to simulator.starttime Exception if event list is not empty """ if self.time != self.start_time: raise ValueError( "trying to setup model, but current time is not equal to start_time, Has the simulator been reset or freshly initialized?" ) if not self.event_list.is_empty(): raise ValueError( "trying to setup model, but events have already been scheduled. Call simulator.setup before any scheduling" ) self.model = model
[docs] def reset(self): """Reset the simulator by clearing the event list and removing the model to simulate.""" self.event_list.clear() self.model = None self.time = self.start_time
[docs] def run_until(self, end_time: int | float) -> None: """Run the simulator until the end time. Args: end_time (int | float): The end time for stopping the simulator Raises: Exception if simulator.setup() has not yet been called """ if self.model is None: raise Exception( "simulator has not been setup, call simulator.setup(model) first" ) while True: try: event = self.event_list.pop_event() except IndexError: # event list is empty self.time = end_time break if event.time <= end_time: self.time = event.time event.execute() else: self.time = end_time self._schedule_event(event) # reschedule event break
[docs] def run_next_event(self): """Execute the next event. Raises: Exception if simulator.setup() has not yet been called """ if self.model is None: raise Exception( "simulator has not been setup, call simulator.setup(model) first" ) try: event = self.event_list.pop_event() except IndexError: # event list is empty return else: self.time = event.time event.execute()
[docs] def run_for(self, time_delta: int | float): """Run the simulator for the specified time delta. Args: time_delta (float| int): The time delta. The simulator is run from the current time to the current time plus the time delta """ # fixme, raise initialization error or something like it if model.setup has not been called end_time = self.time + time_delta self.run_until(end_time)
[docs] def schedule_event_now( self, function: Callable, priority: Priority = Priority.DEFAULT, function_args: list[Any] | None = None, function_kwargs: dict[str, Any] | None = None, ) -> SimulationEvent: """Schedule event for the current time instant. Args: function (Callable): The callable to execute for this event priority (Priority): the priority of the event, optional function_args (List[Any]): list of arguments for function function_kwargs (Dict[str, Any]): dict of keyword arguments for function Returns: SimulationEvent: the simulation event that is scheduled """ return self.schedule_event_relative( function, 0.0, priority=priority, function_args=function_args, function_kwargs=function_kwargs, )
[docs] def schedule_event_absolute( self, function: Callable, time: int | float, priority: Priority = Priority.DEFAULT, function_args: list[Any] | None = None, function_kwargs: dict[str, Any] | None = None, ) -> SimulationEvent: """Schedule event for the specified time instant. Args: function (Callable): The callable to execute for this event time (int | float): the time for which to schedule the event priority (Priority): the priority of the event, optional function_args (List[Any]): list of arguments for function function_kwargs (Dict[str, Any]): dict of keyword arguments for function Returns: SimulationEvent: the simulation event that is scheduled """ if self.time > time: raise ValueError("trying to schedule an event in the past") event = SimulationEvent( time, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs, ) self._schedule_event(event) return event
[docs] def schedule_event_relative( self, function: Callable, time_delta: int | float, priority: Priority = Priority.DEFAULT, function_args: list[Any] | None = None, function_kwargs: dict[str, Any] | None = None, ) -> SimulationEvent: """Schedule event for the current time plus the time delta. Args: function (Callable): The callable to execute for this event time_delta (int | float): the time delta priority (Priority): the priority of the event, optional function_args (List[Any]): list of arguments for function function_kwargs (Dict[str, Any]): dict of keyword arguments for function Returns: SimulationEvent: the simulation event that is scheduled """ event = SimulationEvent( self.time + time_delta, function, priority=priority, function_args=function_args, function_kwargs=function_kwargs, ) self._schedule_event(event) return event
[docs] def cancel_event(self, event: SimulationEvent) -> None: """Remove the event from the event list. Args: event (SimulationEvent): The simulation event to remove """ self.event_list.remove(event)
def _schedule_event(self, event: SimulationEvent): if not self.check_time_unit(event.time): raise ValueError( f"time unit mismatch {event.time} is not of time unit {self.time_unit}" ) # check timeunit of events self.event_list.add_event(event)
[docs] class ABMSimulator(Simulator): """This simulator uses incremental time progression, while allowing for additional event scheduling. The basic time unit of this simulator is an integer. It schedules `model.step` for each tick with the highest priority. This implies that by default, `model.step` is the first event executed at a specific tick. In addition, discrete event scheduling, using integer as the time unit is fully supported, paving the way for hybrid ABM-DEVS simulations. """ def __init__(self): """Initialize a ABM simulator.""" super().__init__(int, 0)
[docs] def setup(self, model): """Set up the simulator with the model to simulate. Args: model (Model): The model to simulate """ super().setup(model) self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH)
[docs] def check_time_unit(self, time) -> bool: """Check whether the time is of the correct unit. Args: time (int | float): the time Returns: bool: whether the time is of the correct unit """ if isinstance(time, int): return True if isinstance(time, float): return time.is_integer() else: return False
[docs] def schedule_event_next_tick( self, function: Callable, priority: Priority = Priority.DEFAULT, function_args: list[Any] | None = None, function_kwargs: dict[str, Any] | None = None, ) -> SimulationEvent: """Schedule a SimulationEvent for the next tick. Args: function (Callable): the callable to execute priority (Priority): the priority of the event function_args (List[Any]): List of arguments to pass to the callable function_kwargs (Dict[str, Any]): List of keyword arguments to pass to the callable """ return self.schedule_event_relative( function, 1, priority=priority, function_args=function_args, function_kwargs=function_kwargs, )
[docs] def run_until(self, end_time: int) -> None: """Run the simulator up to and included the specified end time. Args: end_time (float| int): The end_time delta. The simulator is until the specified end time Raises: Exception if simulator.setup() has not yet been called """ if self.model is None: raise Exception( "simulator has not been setup, call simulator.setup(model) first" ) while True: try: event = self.event_list.pop_event() except IndexError: self.time = end_time break # fixme: the alternative would be to wrap model.step with an annotation which # handles this scheduling. if event.time <= end_time: self.time = event.time if event.fn() == self.model.step: self.schedule_event_next_tick( self.model.step, priority=Priority.HIGH ) event.execute() else: self.time = end_time self._schedule_event(event) break
[docs] class DEVSimulator(Simulator): """A simulator where the unit of time is a float. Can be used for full-blown discrete event simulating using event scheduling. """ def __init__(self): """Initialize a DEVS simulator.""" super().__init__(float, 0.0)
[docs] def check_time_unit(self, time) -> bool: """Check whether the time is of the correct unit. Args: time (float): the time Returns: bool: whether the time is of the correct unit """ return isinstance(time, numbers.Number)