Source code for experimental.devs.eventlist

"""Core event management functionality for Mesa's discrete event simulation system.

This module provides the foundational data structures and classes needed for event-based
simulation in Mesa. The EventList class is a priority queue implementation that maintains
simulation events in chronological order while respecting event priorities. Key features:

- Priority-based event ordering
- Weak references to prevent memory leaks from canceled events
- Efficient event insertion and removal using a heap queue
- Support for event cancellation without breaking the heap structure

The module contains three main components:
- Priority: An enumeration defining event priority levels (HIGH, DEFAULT, LOW)
- SimulationEvent: A class representing individual events with timing and execution details
- EventList: A heap-based priority queue managing the chronological ordering of events

The implementation supports both pure discrete event simulation and hybrid approaches
combining agent-based modeling with event scheduling.
"""

from __future__ import annotations

import itertools
from collections.abc import Callable
from enum import IntEnum
from heapq import heapify, heappop, heappush
from types import MethodType
from typing import Any
from weakref import WeakMethod, ref


[docs] class Priority(IntEnum): """Enumeration of priority levels.""" LOW = 10 DEFAULT = 5 HIGH = 1
[docs] class SimulationEvent: """A simulation event. The callable is wrapped using weakref, so there is no need to explicitly cancel event if e.g., an agent is removed from the simulation. Attributes: time (float): The simulation time of the event fn (Callable): The function to execute for this event priority (Priority): The priority of the event unique_id (int) the unique identifier of the event function_args (list[Any]): Argument for the function function_kwargs (Dict[str, Any]): Keyword arguments for the function Notes: simulation events use a weak reference to the callable. Therefore, you cannot pass a lambda function in fn. A simulation event where the callable no longer exists (e.g., because the agent has been removed from the model) will fail silently. """ _ids = itertools.count() @property def CANCELED(self) -> bool: # noqa: D102 return self._canceled def __init__( self, time: int | float, function: Callable, priority: Priority = Priority.DEFAULT, function_args: list[Any] | None = None, function_kwargs: dict[str, Any] | None = None, ) -> None: """Initialize a simulation event. Args: time: the instant of time of the simulation event function: the callable to invoke priority: the priority of the event function_args: arguments for callable function_kwargs: keyword arguments for the callable """ super().__init__() if not callable(function): raise Exception() self.time = time self.priority = priority.value self._canceled = False if isinstance(function, MethodType): function = WeakMethod(function) else: function = ref(function) self.fn = function self.unique_id = next(self._ids) self.function_args = function_args if function_args else [] self.function_kwargs = function_kwargs if function_kwargs else {}
[docs] def execute(self): """Execute this event.""" if not self._canceled: fn = self.fn() if fn is not None: fn(*self.function_args, **self.function_kwargs)
[docs] def cancel(self) -> None: """Cancel this event.""" self._canceled = True self.fn = None self.function_args = [] self.function_kwargs = {}
def __lt__(self, other): # noqa # Define a total ordering for events to be used by the heapq return (self.time, self.priority, self.unique_id) < ( other.time, other.priority, other.unique_id, )
[docs] class EventList: """An event list. This is a heap queue sorted list of events. Events are always removed from the left, so heapq is a performant and appropriate data structure. Events are sorted based on their time stamp, their priority, and their unique_id as a tie-breaker, guaranteeing a complete ordering. """ def __init__(self): """Initialize an event list.""" self._events: list[SimulationEvent] = [] heapify(self._events)
[docs] def add_event(self, event: SimulationEvent): """Add the event to the event list. Args: event (SimulationEvent): The event to be added """ heappush(self._events, event)
[docs] def peak_ahead(self, n: int = 1) -> list[SimulationEvent]: """Look at the first n non-canceled event in the event list. Args: n (int): The number of events to look ahead Returns: list[SimulationEvent] Raises: IndexError: If the eventlist is empty Notes: this method can return a list shorted then n if the number of non-canceled events on the event list is less than n. """ # look n events ahead if self.is_empty(): raise IndexError("event list is empty") peek: list[SimulationEvent] = [] for event in self._events: if not event.CANCELED: peek.append(event) if len(peek) >= n: return peek return peek
[docs] def pop_event(self) -> SimulationEvent: """Pop the first element from the event list.""" while self._events: event = heappop(self._events) if not event.CANCELED: return event raise IndexError("Event list is empty")
[docs] def is_empty(self) -> bool: """Return whether the event list is empty.""" return len(self) == 0
def __contains__(self, event: SimulationEvent) -> bool: # noqa return event in self._events def __len__(self) -> int: # noqa return len(self._events) def __repr__(self) -> str: """Return a string representation of the event list.""" events_str = ", ".join( [ f"Event(time={e.time}, priority={e.priority}, id={e.unique_id})" for e in self._events if not e.CANCELED ] ) return f"EventList([{events_str}])"
[docs] def remove(self, event: SimulationEvent) -> None: """Remove an event from the event list. Args: event (SimulationEvent): The event to be removed """ # we cannot simply remove items from _eventlist because this breaks # heap structure invariant. So, we use a form of lazy deletion. # SimEvents have a CANCELED flag that we set to True, while popping and peak_ahead # silently ignore canceled events event.cancel()
[docs] def clear(self): """Clear the event list.""" self._events.clear()