Simulation dynamics

The events of a simulation are only half the story: they need to be generated and executed in the correct order. This is the job of the dynamics classes, the sub-classes of Dynamics.

epydemic has two simulation dynamics, for continuous-time and discrete-time simulation. In most cases continuous time (stochastic) dynamics is the correct choice, but we explain below in detail how they differ.

Continuous-time (stochastic or Gillespie) dynamics

A StochasticDynamics is a continuous time simulator. It maintains a joint probability distribution defining the probability of a given event occurring in a given time interval into the future. It draws from this distribution, advances the simulation time by the chosen interval, runs any posted events that should have occurred before this time, and then chooses an element on which to run the event. This technique is known as Gillespie simulation.

Let’s define a variant of SIR extended to capture the contents of all its loci:

class MonitoredSIR(SIR):
    '''An SIR model that tracks all its compartments.'''

    def build(self, params):
        super().build(params)
        self.trackNodesInCompartment(SIR.SUSCEPTIBLE)
        self.trackNodesInCompartment(SIR.REMOVED)

We can set up a single simulation of this model under stochastic dynamics very easily:

# network topological parameters (for an ER network)
N = int(1e5)
kmean = 50

# set up the simulation
param = dict()
param[ERNetwork.N] = N
param[ERNetwork.KMEAN] = kmean
param[SIR.P_INFECTED] = 0.001
param[SIR.P_INFECT] = 0.0001
param[SIR.P_REMOVE] = 0.001
param[Monitor.DELTA] = 1

e = StochasticDynamics(ProcessSequence([MonitoredaSIR(), Monitor()]),
                       ERNetwork())
rc = e.set(param).run()

At each event the dynamics will choose what event should happen next, and after how much time, based on the rates at which events can occur. A rate is simply the probability of an event of a given type happening (taken from the experimental parameters) multiplied by the number of possible sites for the event to happen (taken from the size of the locus at which the event occurs). Plotting the results we get:

../_images/sir-stochastic.png

This approach to simulation is both efficient and statistically exact, which make it generally the default choice for simulations inn epydemic. Occasionally, though, we want to run experiments differently.

Discrete-time (synchronous) dynamics

A SynchronousDynamics is a discrete time simulator. At each timestep it runs all the events posted before this time that remain to run, and then checks all the elements that might have an event run on them and selects those that do undergo events using the given event probability.

We can run the same model with the same parameters, and just change the simulation dynamics:

e = SynchronousDynamics(ProcessSequence([MonitoredaSIR(), Monitor()]),
                        ERNetwork())
rc = e.set(param).run()

Running this simulation will look at the important loci every unit of time and, for each element decide whether to run an infection event (for an SI edge) or a removal event (for an I node) depending on the values of the experimental parameters. Plotting the results we get:

../_images/sir-synchronous.png

The stochastic case is generally faster than synchronous case – often massively faster. This is unsurprising: the probability of an SI edge passing an infection (for example) is usually low in any timestep, so most of the checks that the simulation performs will fail. By contrast the stochastic dynamics “steps over” time when nothing is happening. It is also statistically exact, in that the next event (and the time until it) is chosen using all the available information relating to all previous events, rather than all the one in the previous timestep as happens in the synchronous case. This can sometimes lead to different curves even for the same parameters.

Posted events

Both dynamics can have “normal” (stochastic) and “posted” events. The former happen at a time determined by their distribution; the latter happen at set simulation times.

Stochastic events by their nature aren’t known to happen ahead of time. Posted events can however be posted, and then at some later point (but before the event has fired) can be determined to not be needed any more and be un-posted to remove them from the event queue.

(To un-post an event you need its event identifier, which is returned by Dynamics.postEvent(). This implies that you need to remember this information for any posted event that you might later want to un-post. This isn’t an issue in practice, as it’s usually very clear which events in a simulation might need to be un-posted, and therefore need to have their ids recorded.)

The event queue is represented as a priority queue (or priqueue) held in simulation time order. This is efficient for everything except deleting (un-posting) events, so instead when un-posting we simply set the event function to None. Accessing the queue through any of the posted event methods automatically discards any leading un-posted events, so the un-posting mechanism is transparent to client code.

Event tapping

For both dynamics, the processing is wrapped using the functions from the event taps interface. These include marking the start and end of the simulation, and every event fired (whether per-event, fixed-rate, or posted).