Defining more new processes
So now we can build a disease process from scratch. This is the
simplest way of defining a new process, as a direct sub-class of
CompartmentedModel
, but there are two other ways we might use
instead:
By adding compartments to an existing model
By adding state to an existing model
By computing extra results
By composing processes together
Each of these starting points has subtleties, and we’ll explore all but the last here. (The last is the subject of the next chapter.)
(In exceptional circumstances one might need to avoid the process
hierarchy entirely and create new kinds of experiment. This can be
done by sub-classing NetworkExperiment
.)
New compartmented models
A new disease model is easy to build, as we saw,
especially if you have a description of it in terms of
compartments. It’s usually just a matter of overriding
Process.build()
to define the new compartments and add
appropriate rules for each event, and then define a
event function implementing the actions of each.
Adding more compartments
Let’s consider a variant of SIR called SIRS, where someone can become
susceptible again after some time. (This models diseases where the
immunity given by infection is time-bounded.) Again, we’ll define a
slightly simplied version of epydemic
’s built-in SIRS
process.
For SIRS we need to add three things to SIR:
another parameter, the probability of becoming susceptible again;
a locus tracking the R nodes so they can be the target of re-susceptibility events; and
the code for this event.
We can bring these three elements together in a sub-class:
class SIRS(SIR):
# Extra model parameter
P_RESUSCEPT = 'pResuscept' #: Parameter for probability of losing immunity
# Event name
RESUSCEPT = 'RS' #: Compartment/event name for returning to susceptible.
def __init__(self):
super().__init__()
def build(self, params):
super().build(params)
# add components needed for SIRS
pResuscept = params[self.P_RESUSCEPT]
self.trackNodesInCompartment(self.REMOVED)
self.addEventPerElement(self.REMOVED, pResuscept, self.resuscept, self.RESUSCEPT)
def resuscept(self, t, n):
self.changeCompartment(n, self.SUSCEPTIBLE)
From what we’ve seen already this is hopefully quite clear. The new
sub-class builds on SIR
, adding to its SIR.build()
method to track nodes in the removed compartment and add an event with
the probability given by the experimental parameter. The code for this
event simply changes the node’s compartment back to susceptible.
Note
You can see the code for the SIRS process here.
Adding more state
A process over a network will typically want to save state on each
node and/or edge> For a CompartmentedModel
this includes
things like the compartment a node is in
(CompartmentedModel.COMPARTMENT
), which is accessed using the
CompartmenntModel.getCompartment()
method.
Defining new state means providing two things: a name for the state, as an attribute key for the nodes or edges; and getter and setter methods that access the attribute.
Defining a name for the state is slightly more complicated that just
picking a meaningful name, because of the ways epydemic
lets you
combine processes: we have to ensure that two processes running on the
same network at the same time don’t interfere in unwanted ways. The
easiest approach is to make state names unique to a particular process
instance, which is what Process.stateVariable()
does. For
example, the initialisation of CompartmentedModel
defines
some state for compartments as follows:
# Placeholders for model state variables
COMPARTMENT: str = None #: State variable holding a node's compartment.
OCCUPIED: str = None #: State variable that's True for occupied edges.
T_OCCUPIED: str = None #: State variable holding the occupation time of an edge.
T_HITTING: str = None #: State variable holding the infection time of a node.
def __init__(self):
super().__init__()
self._compartments = dict() # compartment -> initial probability
self._effects = dict() # compartment -> event handlers
# state variable tags
self.COMPARTMENT = self.stateVariable('compartment')
self.OCCUPIED = self.stateVariable('occupied')
self.T_OCCUPIED = self.stateVariable('tOccupied')
self.T_HITTING = self.stateVariable('tHitting')
(We define placeholders for the variable names as a place to hang
their documentation.) Code can use COMPARTMENT
as if it was a
constant – which it is, but a constant unique to this instance. The
actual constants can anyway then be hidden by accessor methods, for
example:
def setCompartment(self, n, c):
'''Set the compartment of a node. This assumes that the node doesn't
already have a compartment set, and so should be used only for
initialising new nodes: in all other cases, use
:meth:`changeCompartment`.
:param n: the node
:param c: the new compartment for the node'''
g = self.network()
# set the correct node attribute
g.nodes[n][self.COMPARTMENT] = c
# propagate the change to any other compartments
self._callEnterHandlers(n, c)
def getCompartment(self, n):
'''Return the compartment of a node.
:param n: the node
:returns: its compartment'''
return self.network().nodes[n][self.COMPARTMENT]
The advantage of this approach is that two different compartmented model processes – for example a disease and an opinion model – can be run together over the same network without one model’s state interfering with the other’s. Essentially the model states of different processes live in separate namespaces, but without any additional overhead.