Having moved from continuous to discrete views of compartmented disease models, we now need to create code so we can simulate such diseases over a network. There are lots of possible compartmented models, so — rather than develop each from scratch — we make use of a Python library the gives us a framework for doing so. This has the advantages that the code we develop can be run under different simulation regimes if required, and can also be run at scale.
(This is a chapter from Complex networks, complex processes.)
Having developed a discrete compartmented model of disease, we now have to turn it into code. Most epidemic processes share a common form and can be simulated using a small set of common techniques. It therefore makes sense to capture the form of an epidemic process in code, and then use that code to drive a simulator. In this way we can focus on the epidemic process rather than on the process of simulation.
We make use of a Python library,
epydemic, written to provide a framework within which to conduct simulations of epidemic processes.
epydemic provides three main elements:
- A base class for describing epidemic processes quickly and cleanly;
- A small library of common epidemic processes that can be used as a starting point for defining additional processes; and
- Implementations of the two most common simulation regimes.
As well as providing the small-scale features we introduce in this chapter,
epydemic has features for performing large-scale simulations on paralle compute clusters, integrating cleanly with the
epyc simulation library. We'll discuss this intregration in more detail later. You can also read the API documentation for a full description of
epydemic and its capabilities.
As we saw earlier, an epidemic simulation consists of two main components:
- A model of the disease process that describes how nodes in the network are infected, recover, and so forth, typically using either probabilities or fixed elapsed times; and
- A dynamics that applies the model to a network over the timespan of the simulation.
The former describes the way nodes evolve as the disease progresses; the latter describes how this evolution occurs in time. For the moment we'll focus on the model, which
epydemic represents by the class
epydemic.CompartmentedModel. We sub-class this class to create different compartmented disease models.
An instance of a sub-class of
epydemic.CompartmentedModel basically encodes exactly the kind of discrete model we developed earlier. Each node in the network resides in a compartment, a box representing the disease state of the node. We are typically interested in how the sizes of the compartments change over time. A locus is a place in the network where an event can occur, where an event typically changes the compartment of one or more nodes around the locus. An example event in SIR would be an infection event, whose locus is the set of SI edges and which causes the S end to become I and any edges to adjacent S nodes to be classified as SI (i.e., be added to the locus for possible future infection).
The significance of loci is that
epydemic keeps track of the nodes and edges in each locus at each stage of the simulation. In our SIR example, after every simulation event
epydemic checks whether any nodes should be removed from the infected locus and whether any edges should be added to the SI locus – and does so automatically in a way that is optimised to only check as little of the network as necessary. This both makes simulation more efficient and simplifies the epidemic process description.
epydemic event is simply a Python function. As such it can do anything Python can do – but typically will perform only some simple transitions of the compartments of nodes.
epydemic.CompartmentedModel provides two methods that perform these operations.
changeCompartment() changes the compartment of a node, making sure that this change is reflected in the process' loci.
markOccupied() marks an edge as having been used to spread the disease, whcih can be useful when exploring how the epidemic spread.
Events might want to do other things, for example keeping track of the simulation time at which the epidemic crossed a particular edge, which might be useful for doing animations. About the only restriction on event code is that it should use
changeCompartment() to change nodes' compartments, as this ensures that the loci are updated.
As an example, let's code-up the SIR model in
epydemic. This isn't actually necessary, as
epydemic already has an implementation of SIR (and indeed other compartmented models). But SIR is conceptually the simplest compartmented model, and demonstrates the approaches we'll use later.
import epydemic import networkx
/Users/sd/programming/cncp/cncp/lib/python2.7/site-packages/matplotlib/__init__.py:878: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter. warnings.warn(self.msg_depr % (key, alt_key))
Let's first define a model for our disease. We know that SIR consists of three compartments: Susceptible, Infected, and Removed. There are two loci for disease and two corresponding events: infected nodes (which can be subject to recovery events), and SI edges (which can undergo infection events). We also know that it requires two dynamical parameters: the probability of infection along an edge, and the probability of recovery. We also require an initial seeding of the network in which nodes become infected with a given probability.
Let's see how this is coded in
class SIR(epydemic.CompartmentedModel): '''The Susceptible-Infected-Removed compartmented model of disease. Susceptible nodes are infected by infected neighbours, and are removed when they are no longer infectious.''' # the model parameters P_INFECTED = 'pInfected' #: Parameter for probability of initially being infected. P_INFECT = 'pInfect' #: Parameter for probability of infection on contact. P_REMOVE = 'pRemove' #: Parameter for probability of removal. # the possible dynamics states of a node for SIR dynamics SUSCEPTIBLE = 'S' #: Compartment for nodes susceptible to infection. INFECTED = 'I' #: Compartment for nodes infected. REMOVED = 'R' #: Compartment for nodes recovered/removed. # the locus for infection events SI = 'SI' #: Edge able to transmit infection. def __init__( self ): super(SIR, self).__init__() def build( self, params ): '''Build the SIR model. :param params: the model parameters''' pInfected = params[self.P_INFECTED] # probability of a node bveing initially infected pInfect = params[self.P_INFECT] # probability of infection pRemove = params[self.P_REMOVE] # probability of recovery self.addCompartment(self.SUSCEPTIBLE, 1 - pInfected) self.addCompartment(self.INFECTED, pInfected) self.addCompartment(self.REMOVED, 0.0) self.addLocus(self.INFECTED) self.addLocus(self.SUSCEPTIBLE, self.INFECTED, name = self.SI) self.addEvent(self.INFECTED, pRemove, lambda d, t, g, e: self.remove(d, t, g, e)) self.addEvent(self.SI, pInfect, lambda d, t, g, e: self.infect(d, t, g, e)) def remove( self, dyn, t, g, n ): '''Perform a removal event. This changes the compartment of the node to :attr:`REMOVED`. :param dyn: the dynamics :param t: the simulation time (unused) :param g: the network :param n: the node''' self.changeCompartment(g, n, self.REMOVED) def infect( self, dyn, t, g, (n, m) ): '''Perform an infection event. This changes the compartment of the susceptible-end node to :attr:`INFECTED`. It also marks the edge traversed as occupied. :param dyn: the dynamics :param t: the simulation time (unused) :param g: the network :param e: the edge transmitting the infection, susceptible-infected''' self.changeCompartment(g, n, self.INFECTED) self.markOccupied(g, (n, m))
Let's look at the
build() method first. This is called to construct the epidemic model. It first extracts the three parameters for the simulation from the hash of parameters. It then declares the three compartments of SIR using the
addCompartment() method. The second parameter is the probability of a ndoe being initially assigned to this compartment. (There are no initially-removed nodes.)
We then add the two loci using
addLocus(). Loci come in two flavours in
epydemic. Node loci capture nodes in a given compartment, while edge loci are edges linking nodes in two particular compartments. In this case, we have a node locus for infected nodes and an edge locus for SI edges (which we name for later).
Finally we bind events to each locus using
addEvent(). Events happen at a given locus with a given probability. An event is a function that takes four parameter: the simulation dynamics, the current simulation time, the
networkx network, and an element from the locus to which the event is bound (either a node or an edge). Since we represent events by methods on the model object, we need to wrap them in lambda expressions (Python closures) so that, when the event is triggered, it calls the correct method on the right model. We then bind these events to the correct loci. A locus may have several events associatd with it if desired, and conversely the same event might occur at several loci.
The above code completely specifies the structure of the epidemic. We now need to specify what happens at each event. For a
remove() event, we are passed a node and change its compartment using
changeCompartment(). For an
infect() event we are passed an SI edge, with the edge being aligned so that the compartments of its endpoints match the way we specified in defining the corresponding locus. We change the susceptible end's compartment to be infected, and mark the edge itself as "occupied", since the infection spread along it.
So far so good, but we still don't have anything to actually run. What we do have is the static description of a disease model thaty describes the probabilities of a node moving between different disease stages – together with code for the events that will occur as we progress through each stage.
What we stil need is a way of deciding when the different progressions happen for the different nodes. This is the issue of simulation dynamics. There are many ways in which we can perform simulations, but the important point is that the model we described can be applied under any of these different models – and that's generally true for most models developed using
epydemic. We next need to explore the simulation under different dynamics to see how they differ.