Time#
modelly.time holds the event list, events and event generators, the scheduling methods of the
model, and signals (observables that notify subscribers when they change).
Module |
Contents |
|---|---|
|
|
|
|
|
|
Events#
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.
- class Event(time: int | float, function: Callable[[...], None], priority: Priority = Priority.DEFAULT, function_args: list[Any] | None = None, function_kwargs: dict[str, Any] | None = None)[source]#
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.
- Variables:
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.
Initialize a simulation event.
- Parameters:
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
The event list: a heap-based priority queue of simulation events.
Events are kept in chronological order, respecting priorities. Cancellation is lazy, so the heap invariant is never broken, and the heap compacts itself once canceled events dominate it.
- class EventList[source]#
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.
Canceling an event leaves it on the heap so the heap invariant is not broken. Models that repeatedly cancel and reschedule an event would therefore grow the heap without bound, so the list counts the canceled events it holds and rebuilds itself once they dominate. That count also makes len() an O(1) operation.
- Variables:
COMPACTION_RATIO (float) – Fraction of canceled events above which the heap is rebuilt, which also bounds the peak heap at L / (1 - COMPACTION_RATIO)
Notes
An event belongs to one event list at a time. Adding the same event to a second list hands ownership over, and the first list’s count goes stale.
Initialize an event list.
- add_event(event: Event)[source]#
Add the event to the event list.
- Parameters:
event (Event) – The event to be added
- Raises:
ValueError – If the event has already been canceled. Cancellation is terminal, so such an event could never execute.
- peek_ahead(n: int = 1) list[Event][source]#
Look at the first n non-canceled event in the event list.
- Parameters:
n (int) – The number of events to look ahead
- Returns:
list[Event]
- 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.
- compact() None[source]#
Remove all canceled events from the heap and rebuild it.
If there are many canceled events, compaction can speed up performance substantially.
Recurring events: a Schedule describes when, an EventGenerator makes it happen.
- class Schedule(interval: float | int | Callable[[Model], float | int] = 1.0, start: float | None = None, end: float | None = None, count: int | None = None)[source]#
Defines when something should happen repeatedly.
- Variables:
interval (float | int | Callable[[Model], float | int]) – Time between executions (fixed value or callable returning value)
start (float | None) – Absolute time to begin (None = use current model time + interval)
end (float | None) – Absolute time to stop (None = no end)
count (int | None) – Maximum executions (None = unlimited)
- class EventGenerator(model: Model, function: Callable[..., None], schedule: Schedule, priority: Priority = Priority.DEFAULT)[source]#
A generator that creates recurring events based on a Schedule.
Unlike a single Event, an EventGenerator is persistent and can be stopped or configured with stop conditions.
- Variables:
model – The model this generator belongs to
function – The callable to execute for each generated event
schedule – The Schedule defining when events occur
priority – Priority level for generated events
Notes
Event generators 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. If you want to use functools.partial, please assign the partial function to a variable prior to creating the generator.
Initialize an EventGenerator.
- Parameters:
model – The model this generator belongs to
function – The callable to execute for each generated event. Use functools.partial to bind arguments.
schedule – The Schedule defining timing
priority – Priority level for generated events
- property next_scheduled_time: float | None#
Return the time of the next scheduled execution, or None if not scheduled.
- start() EventGenerator[source]#
Start the event generator.
- Returns:
Self for method chaining
- Raises:
ValueError – If schedule.start is in the past, e.g. when restarting a stopped generator after its start time has passed.
Scheduling on the model#
Model inherits these methods; they are also listed on the Core page.
Event scheduling and time advance for modelly.core.model.Model.
A mixin so the model file stays readable; every method here needs the
model’s _event_list, _event_generators, time and signals.
- class HasSchedule[source]#
Scheduling, running and time advance, mixed into
Model.- schedule_event(function: Callable, *, at: float | None = None, after: float | None = None, priority: Priority = Priority.DEFAULT) Event[source]#
Schedule a one-off event.
- Parameters:
function – The callable to execute
at – Absolute time to execute (mutually exclusive with after)
after – Relative time from now to execute (mutually exclusive with at)
priority – Priority level for the event
- Returns:
The scheduled Event (can be used to cancel)
- Raises:
ValueError – If both or neither of at/after are specified
ValueError – If both or neither of at/after are specified, or if the scheduled time is in the past.
- schedule_recurring(function: Callable, schedule: Schedule, priority: Priority = Priority.DEFAULT) EventGenerator[source]#
Schedule a recurring event based on a Schedule.
- Parameters:
function – The callable to execute repeatedly
schedule – The Schedule defining when events occur
priority – Priority level for generated events
- Returns:
The EventGenerator (can be used to stop)
- Raises:
ValueError – If the schedule start time is in the past.
Signals#
HasEmitters takes its subscription methods (observe, unobserve, observe_class, …)
from modelly.time.signals.subscriptions; they are listed under HasEmitters.
Core implementation of Modelly’s reactive programming system.
This module provides the foundational classes for Modelly’s observable/reactive programming functionality:
BaseObservable: Abstract base class defining the interface for all observables
Observable: Main class for creating observable properties that emit change signals
computed_property: Decorator for creating properties that automatically update based on dependencies
HasEmitters: Mixin class that enables an object to contain and manage observables
emit: Decorator for methods that emit signals
The module implements a robust reactive system where changes to observable properties automatically trigger updates to dependent computed values and notify subscribed observers. This enables building models with complex interdependencies while maintaining clean separation of concerns.
It also owns the dependency-tracking state (CURRENT_COMPUTED, PROCESSING_SIGNALS), so everything that reads or rebinds that state is defined here.
- class BaseObservable(fallback_value=None)[source]#
Bases:
objectBase class for all Observables.
Initialize a BaseObservable.
- signal_types#
alias of
SignalType
- class Observable(fallback_value=None)[source]#
Bases:
BaseObservableObservable descriptor.
An observable is an attribute that emits ObservableSignals.CHANGED whenever it is changed to a different value.
Initialize a BaseObservable.
- signal_types#
alias of
ObservableSignals
- computed_property(func: Callable | None = None, *, dependencies: Iterable[tuple[str, str | SignalType]] | None = None) property[source]#
Decorator to create a computed property.
Acts like @property, but automatically tracks dependencies (Observables) accessed during the function execution and updates them using a diffing engine.
- Parameters:
func – The function to be decorated.
dependencies – Optional iterable of (observable_name, signal_type) tuples to explicitly track.
- class ComputedState(owner: HasEmitters, name: str, func: Callable, dependencies: Iterable[tuple[str, str | SignalType]] | None = None)[source]#
Bases:
ComputedStateBaseInternal class to hold the state of a computed property for a specific instance.
Initialize a ComputedState.
- Parameters:
owner – the HasEmitters instance on which this state is defined
name – the name of the computed property
func – the computation function
dependencies – optional iterable of explicit dependencies to track
- evaluate() Any[source]#
Recompute the value by calling self.func, tracking dependencies as it runs.
Any Observable read during the call is recorded as a dependency, and subscriptions are updated so this state is marked dirty again when one of them changes. On success, value is updated and is_dirty is cleared.
- Raises:
Exception – Any exception raised by self.func propagates unchanged. parents is restored to its prior state and is_dirty is left set, so the caller may retry later.
- Returns:
The newly computed value.
Objects that emit signals: the HasEmitters mixin and the emit decorator.
HasEmitters discovers the observables defined on a class and sends their signals to weakly referenced subscribers. Registering subscribers is inherited from SubscriptionMixin. The emit decorator turns a method into a signal source.
- class HasEmitters(*args, **kwargs)[source]#
HasEmitters class.
- Variables:
subscribers (dict[tuple[str, modelly.time.signals.util.SignalType], list[weakref.ReferenceType]]) – mapping of observables/emitters and signal type to subscribers
observables (dict[str, type[modelly.time.signals.util.SignalType] | frozenset[modelly.time.signals.util.SignalType]]) – mapping of observables/emitters to their available signal types
HasEmitters automatically discovers the observables/emitters defined on the class.
Initialize a HasEmitters.
- notify(observable: str, signal_type: str | SignalType, **kwargs)[source]#
Emit a signal.
Inside a batch the signal is captured by the batch. Otherwise it is only built and sent when the observable has live subscribers, and sending it prunes subscribers that are no longer alive.
- Parameters:
observable – the public name of the observable emitting the signal
signal_type – the type of signal to emit
kwargs – additional keyword arguments to include in the signal
- batch()[source]#
Return a context manager that batches signals.
Signals emitted during the batch are buffered and aggregated on exit. Nested batches merge into the outer batch; only the outermost dispatches.
Note
Computed properties may return stale cached values during the batch. They will be updated when aggregated signals are dispatched on exit.
- suppress()[source]#
Return a context manager that suppresses all signals.
No signals are emitted, buffered, or dispatched during suppression.
Note
Computed properties may become permanently stale because their triggering signals are dropped entirely.
- peek(name: str, default: Any = None) Any[source]#
Retrieve an attribute without registering it in the dependency graph.
- classmethod clear_all_class_subscriptions(name: str | _AllSentinel | Iterable[str])#
Clears all class-level subscriptions for the observable <name>.
if name is ALL, all subscriptions are removed
- Parameters:
name – name of the Observable to unsubscribe for all signal types
- clear_all_subscriptions(name: str | _AllSentinel | Iterable[str])#
Clears all instance-level subscriptions for the observable <name>.
if name is ALL, all subscriptions are removed
- Parameters:
name – name of the Observable to unsubscribe for all signal types
- observe(observable_name: str | _AllSentinel | Iterable[str], signal_type: str | SignalType | _AllSentinel | Iterable[str | SignalType], handler: Callable)#
Subscribe to the Observable <name> for signal_type.
- Parameters:
observable_name – name of the Observable to subscribe to
signal_type – the type of signal on the Observable to subscribe to
handler – the handler to call
- Raises:
ValueError – if the Observable <name> is not registered or if the Observable
does not emit the given signal_type –
- classmethod observe_class(observable_name: str | _AllSentinel | Iterable[str], signal_type: str | SignalType | _AllSentinel | Iterable[str | SignalType], handler: Callable)#
Subscribe at the class level to the Observable <name> for signal_type.
All instances of this class will trigger the handler when they emit this signal. Handlers are stored as weak references to prevent memory leaks during experiments.
- Parameters:
observable_name – name of the Observable to subscribe to
signal_type – the type of signal on the Observable to subscribe to
handler – the handler to call
- unobserve(observable_name: str | _AllSentinel | Iterable[str], signal_type: str | SignalType | _AllSentinel | Iterable[str | SignalType], handler: Callable)#
Unsubscribe to the Observable <name> for signal_type.
- Parameters:
observable_name – name of the Observable to unsubscribe from
signal_type – the type of signal on the Observable to unsubscribe to
handler – the handler that is unsubscribing
- classmethod unobserve_class(observable_name: str | _AllSentinel | Iterable[str], signal_type: str | SignalType | _AllSentinel | Iterable[str | SignalType], handler: Callable)#
Unsubscribe at the class level to the Observable <name> for signal_type.
- Parameters:
observable_name – name of the Observable to unsubscribe from
signal_type – the type of signal on the Observable to unsubscribe to
handler – the handler that is unsubscribing
- descriptor_generator(cls) Generator[tuple[str, type[SignalType] | frozenset[SignalType]]][source]#
Yield the name and signal_types for each Observable defined on cls.
This handles both legacy BaseObservable descriptors and new @computed_properties.
- emit(observable_name, signal_to_emit, when: Literal['before', 'after'] = 'after')[source]#
Decorator to emit a signal before or after the call to method (HasEmitters only).
- Parameters:
observable_name – the name of the observable that emits the signal
signal_to_emit – the signal to emit
when – whether to emit the signal before or after the function call.
Dependency bookkeeping for computed properties.
These helpers keep a ComputedState’s subscriptions in step with the observables
it reads. They hold no tracking state themselves: the currently evaluating
computed lives in modelly.time.signals.core.
- class ComputedProperty(fget=None, fset=None, fdel=None, doc=None)[source]#
A custom property class to identify computed properties.
- signal_types#
alias of
ObservableSignals
- class ComputedStateBase(owner: HasEmitters, name: str, func: Callable, dependencies: Iterable[tuple[str, str | SignalType]] | None = None)[source]#
The per-instance data of a computed property; core.ComputedState adds evaluation.
Initialize a ComputedState.
- Parameters:
owner – the HasEmitters instance on which this state is defined
name – the name of the computed property
func – the computation function
dependencies – optional iterable of explicit dependencies to track
Observable collection types that emit signals when modified.
This module extends Modelly’s reactive programming capabilities to collection types like lists. Observable collections emit signals when items are added, removed, or modified, allowing other components to react to changes in the collection’s contents.
The module provides: - ListSignals: Enum defining signal types for list collections - ObservableList: A list descriptor that emits signals on modifications - SignalingList: The underlying list implementation that manages signal emission
These classes enable building models where components need to track and react to changes in collections of agents, resources, or other model elements.
- class ObservableList[source]#
Bases:
BaseObservableAn ObservableList that emits signals on changes to the underlying list.
Initialize the ObservableList.
- signal_types#
alias of
ListSignals
Batching and suppression of signals for modelly signals.
This module provides context managers for controlling signal dispatch:
batch(): Buffers signals and dispatches aggregated results on exit
suppress(): Silently drops all signals during the context
Both batch() and suppress() are used as context managers within HasEmitters. They only batch or suppress signals emitted by the instance within which they are invoked.
It also provides a functools.singledispatch aggregation system for different signal types, making it easy for users to add aggregations for their own custom signal types:
@aggregate.register(MySignalType)
def _my_aggregate(signal_type, signals, value=None):
...
Notes
During a batch, computed properties may return stale cached values because their triggering signals are deferred. At flush, aggregated signals dispatch normally and mark computed properties dirty.
During suppress, computed properties may become permanently stale because their triggering signals are dropped entirely.]]
@aggregate.register is global to the python process, and it’s non-trivial to remove it, although you can overwrite it. See also functools.singledispatch for more details.
- aggregate(signal_type: SignalType, signals: list[Message], value: Any = None) list[Message][source]#
- aggregate(signal_type: ObservableSignals, signals: list[Message], value: Any = None) list[Message]
- aggregate(signal_type: ListSignals, signals: list[Message], value: Any = None) list[Message]
Default aggregation: keep all signals as-is.
Users can register custom aggregators for their own signal types:
@aggregate.register(MySignalType) def _my_aggregate(signal_type, signals, value=None): ...
Notes
we need signal_type as the first argument because that is how singledispatch can determine which handler to call.
- Parameters:
signal_type – the type of signal (used for dispatch)
signals – list of buffered Message objects
value – the attribute value captured at the time of the first signal, or None if not captured
- Returns:
list of Message objects to dispatch
Signal types.
- class ObservableSignals(*values)[source]#
Bases:
SignalTypeEnumeration of signal types that observables can emit.
This enum provides type-safe signal type definitions with IDE autocomplete support. Inherits from str for backward compatibility with existing string-based code.
- Variables:
CHANGED – Emitted when an observable’s value changes.
Examples
>>> from modelly.time.signals import Observable, HasEmitters, ObservableSignals >>> class MyModel(HasEmitters): ... value = Observable() ... def __init__(self): ... super().__init__() ... self._value = 0 >>> model = MyModel() >>> def handler(signal): ... print(signal.additional_kwargs["new"]) >>> model.observe("value", ObservableSignals.CHANGED, handler) >>> model.value = 10 10
Note
String-based signal types are still supported for backward compatibility: >>> model.observe(“value”, “changed”, handler) # Still works
- class ListSignals(*values)[source]#
Bases:
SignalTypeEnumeration of signal types that observable lists can emit.
Provides list-specific signal types with IDE autocomplete and type safety. Inherits from str for backward compatibility with existing string-based code. Includes all list-specific signals (INSERTED, APPENDED, REMOVED, REPLACED) plus a SET signal for when the entire list is modified.
- Variables:
SET – Emitted when the list itself is replaced/assigned.
INSERTED – Emitted when an item is inserted into the list.
APPENDED – Emitted when an item is appended to the list.
REMOVED – Emitted when an item is removed from the list.
REPLACED – Emitted when an item is replaced/modified in the list.
Examples
>>> from modelly.time.signals import ObservableList, HasEmitters, ListSignals >>> class MyModel(HasEmitters): ... items = ObservableList() ... def __init__(self): ... super().__init__() ... self.items = [] >>> model = MyModel() >>> def handler(message): ... print(f"Inserted {message.additional_kwargs['new']}") >>> model.observe("items", ListSignals.INSERTED, handler) >>> model.items.insert(0, "first") Inserted first
Note
String-based signal types are still supported for backward compatibility: >>> model.observe(“items”, “inserted”, handler) # Still works
- class ModelSignals(*values)[source]#
Bases:
SignalTypeSignal types for model-level events.
Utility functions and classes for Modelly’s signals implementation.
This module provides helper functionality used by Modelly’s reactive programming system:
Message: a dataclass containing information about a signal change
SignalType: root enum defining the types of signals that can be emitted
create_weakref: Helper function to properly create weak references to different types
_AllSentinel class for subscribing to all signals or all observables
These utilities support the core signals implementation by providing reference management and convenient data structures used throughout the reactive system.
- class Message(name: str, owner: Any, signal_type: SignalType, additional_kwargs: dict)[source]#
A message class containing information about a signal change.