Source code for modelly.time.events.event

"""Single simulation events and their priorities.

- Priority: An enumeration defining event priority levels (HIGH, DEFAULT, LOW)
- Event: A class representing individual events with timing and execution details

Events hold a weak reference to their callable, so canceled or orphaned events do
not keep agents alive.
"""

from __future__ import annotations

import itertools
import types
from collections.abc import Callable
from enum import IntEnum
from types import MethodType
from typing import TYPE_CHECKING, Any
from weakref import ReferenceType, WeakMethod, ref

if TYPE_CHECKING:
    from modelly.time.events.event_list import EventList


def _create_callable_reference(
    function: Callable[..., None],
) -> ReferenceType[Any] | WeakMethod:
    """Validate and create a weak-reference wrapper for an event callback."""
    if not callable(function):
        raise TypeError("function must be a callable")

    if isinstance(function, types.FunctionType) and function.__name__ == "<lambda>":
        raise ValueError("function must be alive at Event creation.")

    if isinstance(function, MethodType):
        function_ref = WeakMethod(function)
    else:
        try:
            function_ref = ref(function)
        except TypeError as exc:
            raise TypeError("function must be weak referenceable") from exc

    return function_ref


[docs] class Priority(IntEnum): """Enumeration of priority levels.""" LOW = 10 DEFAULT = 5 HIGH = 1
[docs] class Event: """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. If the callback no longer exists at execution time (e.g., because an agent has been removed), execution will fail silently. Lambda callbacks are rejected at Event creation. """ _ids = itertools.count() @property def CANCELED(self) -> bool: # noqa: D102 return self._canceled def __init__( self, time: int | float, function: Callable[..., None], 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__() self.time = time self.priority = priority.value self._canceled = False weak_ref_fn = _create_callable_reference(function) self.fn = weak_ref_fn self.unique_id = next(self._ids) self.function_args = function_args if function_args else [] self.function_kwargs = function_kwargs if function_kwargs else {} # Set by EventList.add_event, so cancel() can update the list's count. self._owner: ReferenceType[EventList] | None = None
[docs] def execute(self) -> None: """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.""" if self._canceled: return self._canceled = True self.fn = None self.function_args = [] self.function_kwargs = {} if self._owner is not None: owner = self._owner() if owner is not None: owner._on_cancellation()
def __lt__(self, other: Event) -> bool: """Define a total ordering for events to be used by the heapq.""" if self.time != other.time: return self.time < other.time if self.priority != other.priority: return self.priority < other.priority return self.unique_id < other.unique_id def __getstate__(self) -> dict[str, Any]: """Prepare state for pickling.""" state = self.__dict__.copy() # Convert weak reference back to strong reference for pickling fn = self.fn() if self.fn is not None else None state["_fn_strong"] = fn state["fn"] = None # Don't pickle the weak reference state["_owner"] = None # Weak references cannot be pickled return state def __setstate__(self, state: dict[str, Any]) -> None: """Restore state after unpickling.""" fn = state.pop("_fn_strong") self.__dict__.update(state) self._owner = None # Recreate callable reference strategy. if fn is not None: self.fn = _create_callable_reference(fn) else: self.fn = None