The Event System

vivarium constructs and manages the flow of time through the emission of regularly scheduled events. This event system provides a means of coordinating across various components in a simulation.

What is an Event?

An Event is a simple container for a group of attributes that provide all the necessary information to respond to the event. Events have the following attributes:

Event Attributes

Name

Description

index
An index into the population table that contains all
individuals that may respond to the event.
time
The time at which the event will resolve. The current simulation
time plus the size of the current time step.
step_size
A Timedelta or int representing the
size of the next simulation time step.
user_data
A dict that allows an event emitter to pass arbitrary
information to event listeners.

vivarium manages these events with a publication-subscriber system. The framework maintains several named event channels where it and user components can emit (or publish) events. User components may then register methods or functions as listeners (or subscribers) to events on a particular channel. The listeners will then be called any time an event is emitted on the channel and will receive the event as an argument.

User components can also create new event channels simply by requesting an emitter or listener from the event system. This behavior is discouraged, however, as it makes understanding the structure of a model much more difficult.

Lifecycle Events

The simulation engine itself is responsible for the emission of a dedicated set of events that determine the progression of time in the simulation. Each time step in the simulation corresponds to the emission of a set of time_step events. Components can register themselves as listeners to these events and thus take action on each time step or other key simulation phase.

The following table depicts the events emitted by the simulation engine itself and the lifecycle phase when they are emitted. See the lifecycle concept note for more information about these phases.

Events Emitted by the Simulation Engine

Lifecycle Phase

Events

Post-setup
post_setup
Main Event Loop
time_step_prepare
time_step
time_step__cleanup
collect_metrics
Finalization
simulation_end

Interacting with Events

The EventInterface is available off the Builder and provides two options for interacting with the event system:

1. register_listener to add a listener to a given event to be called on emission

2. get_emitter to retrieve a callable emitter for a given event

Although methods for both getting emitters and registering listeners are provided, it is strongly encouraged that only the registering listeners aspect is used.

Registering Listeners

In order to register a listener to an event to respond when that event is emitted, we can use the register_listener. The listener itself should be a callable function that takes as its only argument the Event that is emitted.

Suppose we wish to track how many simulants are affected each time step. We could do this by creating an observer component with an on_time_step method that we will register as a listener for the time_step event. Our component would look something like the following:

class AffectedObserver:

    def setup(self, builder):
        self.affected_counts = pd.DataFrame(columns=['time_step', 'number_affected])
        builder.event.register_listener('time_step', self.on_time_step)

    def on_time_step(self, event):
        self.affected_counts.append(pd.DataFrame({'time_step': event.time, 'number_affected': len(event.index)}))

On each time step, our on_time_step method will be called and we will add another row to our dataframe tracking the number of affected simulants.

Note

Listeners are stored in priority levels when registered to an event. These levels (0-9) indicate which order listeners should be called when the event is emitted; listeners in lower priority levels will be called earlier. Within a priority level, there is no guarantee of order.

This feature should be avoided if possible. Components should strive to obey the Markov property as they transform the state table: the state of the simulation at the beginning of the next time step should only depend on the current state of the system.

Emitting Events

The get_emitter provides a way to get a callable emitter for a given named event. It can be used as follows:

emitter = builder.event.get_emitter('my_event')

Danger

Do not emit any of the simulation lifecyle events described in the table above. These are events that correspond to particular phases in the simulation and should only be emitted by the engine itself.

Caution

While users may provide their own named events by requesting an emitter, this is not advised. Adding additional events beyond those emitted by the simulation engine essentially creates arbitrary GOTO statements in the simulation flow and makes time much more difficult to think about.