Home » cncp » Coding a compartmented model

Coding a compartmented model

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.)

Coding a compartmented model

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:

  1. A base class for describing epidemic processes quickly and cleanly;
  2. A small library of common epidemic processes that can be used as a starting point for defining additional processes; and
  3. 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.

Concepts

As we saw earlier, an epidemic simulation consists of two main components:

  1. 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
  2. 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.

Describing an epidemic model in code

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.

What events can do

An 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.

An example: coding up SIR

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.

In [1]:
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))

Describing a model

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 epydemic:

In [2]:
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.

Running a model

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.


Leave a comment