Movers and go functions
Demographics in metawards
are a powerful concept that enables the
modelling of a wide variety of different scenarios. Just as work
and play have very general meanings in metawards
, so to do
demographics. We use it to mean any group of individuals. It is fluid,
in the sense that an individual can move between different demographics
during a model run, with the constraint that they can only belong
to one demographic at a time.
Demographic for self-isolation
Individuals are moved between demographics during a model run using
mover functions. These are
plugins that return the go functions
that are used to make individuals
go from one demographic to another.
This is best demonstrated by example. In this example we will use demographics to model the effect of self-isolation or quarantine during an outbreak.
First, create a new demographics.json
file that contains;
{
"demographics" : ["home", "isolate"],
"work_ratios" : [ 1.0, 0.0 ],
"play_ratios" : [ 1.0, 0.0 ]
}
This specifies two demographics:
home
- this holds the entire population and represents individuals behaving “normally”, e.g. continuing to work and play.isolate
- this currently has no members. We will use this demographic to represent individuals who are self-isolating or in quarantine, e.g. they will not contribute to the force of infection of any ward.
Moving individuals to isolation
Next, create a custom move function
called move_isolate
by creating a file called move_isolate.py
and copying in the below;
from metawards.movers import go_isolate
def move_isolate(**kwargs):
func = lambda **kwargs: go_isolate(go_from="home",
go_to="isolate",
self_isolate_stage=2,
**kwargs)
return [func]
This defines a custom move function
called move_isolate
. This returns the
go function
go_isolate()
that is
provided in metawards.movers
. This
go_isolate()
function scans through the
demographics idenfied by go_from
to search for individuals who
are showing signs of infection, i.e. individuals in a disease stage
that is greater or equal to self_isolate_stage
.
go_isolate()
moves these infected individuals
from their existing demographic into the new demographic identified
by go_to
.
This go function has several parameters that must be set before it
can be returned by move_isolate
. We set these parameters by using
lambda to create
a new anonymous go function where those arguments are bound to fixed
values.
Note
Here is a good explanation of lambda and argument binding
if you’ve never seen this before. In this case we have bound
go_from
to equal "home"
, go_to
to equal "isolate"
,
and self_isolate_stage
to equal 2
. This means that these values
will be used every time the go_isolate
function returned
from move_isolate
is called.
Mixing without infection
Next, create a mixer in
mix_isolate.py
and copy in the below;
from metawards.mixers import merge_using_matrix
def mix_isolate(network, **kwargs):
matrix = [ [1.0, 0.0],
[0.0, 0.0] ]
network.demographics.interaction_matrix = matrix
return [merge_using_matrix]
This mixer specifies an interaction matrix where the only contribution
to the FOIs comes from the home
demographic (matrix[0][0] == 1
).
The isolate
demographic makes no contribution to the FOI
(matrix[0][1]
, matrix[1][0]
and matrix[1][1]
are all zero).
Running the model
You can run the simulation by passing in your custom mover using the
--mover
command line argument, and your custom mixer using the
--mixer
command line argument. We will seed the infection using
ExtraSeedsBrighton.dat
and will use the parameters from lurgy3.json
which you should copy into this directory. Run the job using;
metawards -d lurgy3 -D demographics.json -a ExtraSeedsBrighton.dat --mover move_isolate --mixer mix_isolate
Note
Note that we are using the lurgy3
parameters that were
optimised earlier. These include the
long-lived asymptomatic but infectious stage 3 of the disease.
You should see a trajectory that looks something like this;
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 0 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 56082077 E: 0 I: 0 R: 0 IW: 0 POPULATION: 56082077
home S: 56082077 E: 0 I: 0 R: 0 IW: 0 POPULATION: 56082077
isolate S: 0 E: 0 I: 0 R: 0 IW: 0 POPULATION: 0
Number of infections: 0
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 1 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
seeding demographic 0 play_infections[0][2124] += 5
S: 56082072 E: 5 I: 0 R: 0 IW: 0 POPULATION: 56082077
home S: 56082072 E: 5 I: 0 R: 0 IW: 0 POPULATION: 56082077
isolate S: 0 E: 0 I: 0 R: 0 IW: 0 POPULATION: 0
Number of infections: 5
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 2 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 56082072 E: 0 I: 5 R: 0 IW: 0 POPULATION: 56082077
home S: 56082072 E: 0 I: 5 R: 0 IW: 0 POPULATION: 56082077
isolate S: 0 E: 0 I: 0 R: 0 IW: 0 POPULATION: 0
Number of infections: 5
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 3 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 56082072 E: 0 I: 5 R: 0 IW: 0 POPULATION: 56082077
home S: 56082072 E: 0 I: 0 R: 0 IW: 0 POPULATION: 56082072
isolate S: 0 E: 0 I: 5 R: 0 IW: 0 POPULATION: 5
Number of infections: 5
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 4 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 56082072 E: 0 I: 5 R: 0 IW: 0 POPULATION: 56082077
home S: 56082072 E: 0 I: 0 R: 0 IW: 0 POPULATION: 56082072
isolate S: 0 E: 0 I: 5 R: 0 IW: 0 POPULATION: 5
Number of infections: 5
...
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 20 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 56082072 E: 0 I: 1 R: 4 IW: 0 POPULATION: 56082077
home S: 56082072 E: 0 I: 0 R: 0 IW: 0 POPULATION: 56082072
isolate S: 0 E: 0 I: 1 R: 4 IW: 0 POPULATION: 5
Number of infections: 1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 21 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 56082072 E: 0 I: 1 R: 4 IW: 0 POPULATION: 56082077
home S: 56082072 E: 0 I: 0 R: 0 IW: 0 POPULATION: 56082072
isolate S: 0 E: 0 I: 1 R: 4 IW: 0 POPULATION: 5
Number of infections: 1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 22 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 56082072 E: 0 I: 0 R: 5 IW: 0 POPULATION: 56082077
home S: 56082072 E: 0 I: 0 R: 0 IW: 0 POPULATION: 56082072
isolate S: 0 E: 0 I: 0 R: 5 IW: 0 POPULATION: 5
Number of infections: 0
Infection died ... Ending on day 22
The infection was seeded with five individuals on day 1. They had a
latent infection for a day (E == 5
), before developing symptoms
on day 2 (I == 5
). At the beginning of day 3 then were moved
into the isolate
demographic, in which they were unable to
infect others, and so progressed through the disease until they had
all recovered by day 22.
The asymptomatic stage
Self-isolation appeared to have worked well. However, we neglected
to account for the asymptomatic stage 3
of the lurgy. We modelled
the disease such that symptoms only appeared in stage 3, but individuals
were infectious from stage 2. We need to update our move_isolate
function
so that individuals only self-isolate at stage 3, when they realise
that they have symptoms. Edit move_isolate.py
and change it to read;
from metawards.movers import go_isolate
def move_isolate(**kwargs):
func = lambda **kwargs: go_isolate(go_from="home",
go_to="isolate",
self_isolate_stage=3,
**kwargs)
return [func]
(we have just changed self_isolate_stage
from 2
to 3
).
Now run metawards
again using;
metawards -d lurgy3 -D demographics.json -a ExtraSeedsBrighton.dat --mover move_isolate --mixer mix_isolate
We now have a completely different outbreak. Asymptomatic (and thus not self-isolating) individuals were able to spread the infection to others before going into isolation. This spread was exponential, and so the epidemic lasted for a long time, with the vast majority of Individuals in the model being infected. For example;
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 370 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 11394138 E: 0 I: 1 R: 44687938 IW: 0 POPULATION: 56082077
home S: 11394138 E: 0 I: 0 R: 0 IW: 0 POPULATION: 11394138
isolate S: 0 E: 0 I: 1 R: 44687938 IW: 0 POPULATION: 44687939
Number of infections: 1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 371 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 11394138 E: 0 I: 1 R: 44687938 IW: 0 POPULATION: 56082077
home S: 11394138 E: 0 I: 0 R: 0 IW: 0 POPULATION: 11394138
isolate S: 0 E: 0 I: 1 R: 44687938 IW: 0 POPULATION: 44687939
Number of infections: 1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 372 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 11394138 E: 0 I: 0 R: 44687939 IW: 0 POPULATION: 56082077
home S: 11394138 E: 0 I: 0 R: 0 IW: 0 POPULATION: 11394138
isolate S: 0 E: 0 I: 0 R: 44687939 IW: 0 POPULATION: 44687939
Number of infections: 0
Infection died ... Ending on day 373
Here, the outbreak lasted for 372 days, with ~45M infections.
Adjusting progress
The issue here is that the amount of time spent in the asymptomatic but
infectious stage was very long (progress[3] == 0.2
) and the
infectiousness of asymptomatic individuals was very high
(beta[3] == 0.4
). During a real outbreak it is likely that individuals
would take actions that would reduce the chance of infection even from
asymptomatic carriers, e.g. by generally being more wary of one another,
washing hands, wearing masks etc. To account for this, we should reduce
the value of beta[3]
to a lower value, e.g. to 0.2
. Copy
lurgy3.json
to lurgy4.json
and update that to read;
{ "name" : "The Lurgy",
"version" : "May 18th 2020",
"author(s)" : "Christopher Woods",
"contact(s)" : "christopher.woods@bristol.ac.uk",
"reference(s)" : "Completely ficticious disease - no references",
"beta" : [0.0, 0.0, 0.2, 0.5, 0.5, 0.0],
"progress" : [1.0, 1.0, 0.2, 0.5, 0.5, 0.0],
"too_ill_to_move" : [0.0, 0.0, 0.0, 0.5, 0.8, 1.0],
"contrib_foi" : [1.0, 1.0, 1.0, 1.0, 1.0, 0.0]
}
(we have just changed beta[3]
from 0.4
to 0.2
)
Note
In a real outbreak you should scan the value of beta[3]
to match
against observations. We are not doing this now as the lurgy is a
fictional disease.
Now run the model using the command;
metawards -d lurgy4 -D demographics.json -a ExtraSeedsBrighton.dat --mover move_isolate --mixer mix_isolate --nsteps 365
Note
We’ve switched to using lurgy4
and have limited the run to modelling
just a single year (365 days)
You should see output similar to;
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 362 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 55673047 E: 4015 I: 35748 R: 369267 IW: 3073 POPULATION: 56082077
home S: 55673047 E: 4015 I: 23938 R: 4094 IW: 3073 POPULATION: 55705094
isolate S: 0 E: 0 I: 11810 R: 365173 IW: 0 POPULATION: 376983
Number of infections: 39763
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 363 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 55668968 E: 4094 I: 35800 R: 373215 IW: 2990 POPULATION: 56082077
home S: 55668968 E: 4094 I: 23987 R: 4079 IW: 2990 POPULATION: 55701128
isolate S: 0 E: 0 I: 11813 R: 369136 IW: 0 POPULATION: 380949
Number of infections: 39894
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Day 364 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
S: 55664887 E: 4079 I: 35966 R: 377145 IW: 3028 POPULATION: 56082077
home S: 55664887 E: 4079 I: 24148 R: 4081 IW: 3028 POPULATION: 55697195
isolate S: 0 E: 0 I: 11818 R: 373064 IW: 0 POPULATION: 384882
Number of infections: 40045
Exiting model run early
Infection died ... Ending on day 365
Note
You may find that the outbreak dies out quite quickly. The number of
infections is low at the start, and the action of low beta
and
the move to self-isolating does quench the outbreak during some runs.
However, once it catches light, the outbreak will spread to approximately
40,000 individuals within one year.
The spread of the infection was significantly reduced by the reduction
in beta[3]
for the asymptomatic stage. This demonstrates how small changes
in beta
, e.g. caused by increased hand-washing, masks etc., can have
a big impact on the spread of the disease in the model.