Modelling vaccination

Let’s now use MoveGenerator and go_ward() to model vaccination.

First, we will create a new version of the lurgy that includes a vaccination stage. To do this in Python open ipython or jupyter and type;

>>> from metawards import Disease
>>> lurgy = Disease("lurgy5")
>>> lurgy.add("E", beta=0.0, progress=1.0)
>>> lurgy.add("I1", beta=0.4, progress=0.2)
>>> lurgy.add("I2", beta=0.5, progress=0.5, too_ill_to_move=0.5)
>>> lurgy.add("I3", beta=0.5, progress=0.8, too_ill_to_move=0.8)
>>> lurgy.add("R")
>>> lurgy.add("V", beta=0.0, progress=0.0, is_infected=False)
>>> lurgy.to_json("lurgy5.json", indent=2, auto_bzip=False)

or, in R/RStudio type;

> library(metawards)
> lurgy <- metawards$Disease("lurgy5")
> lurgy$add("E", beta=0.0, progress=1.0)
> lurgy$add("I1", beta=0.4, progress=0.2)
> lurgy$add("I2", beta=0.5, progress=0.5, too_ill_to_move=0.5)
> lurgy$add("I3", beta=0.5, progress=0.8, too_ill_to_move=0.8)
> lurgy$add("R")
> lurgy$add("V", beta=0.0, progress=0.0, is_infected=FALSE)
> lurgy$to_json("lurgy5.json", indent=2, auto_bzip=FALSE)

or, simply copy the below into the file lurgy5.json;

{
    "name": "lurgy5",
    "stage":           ["E", "I1", "I2", "I3", "R", "V"],
    "mapping":         ["E", "I",  "I",  "I",  "R", "V"],
    "beta":            [0.0, 0.4,  0.5,  0.5,  0.0, 0.0],
    "progress":        [1.0, 0.2,  0.5,  0.8,  0.0, 0.0],
    "too_ill_to_move": [0.0, 0.0,  0.5,  0.8,  0.0, 0.0],
    "contrib_foi":     [1.0, 1.0,  1.0,  1.0,  1.0, 1.0],
    "is_infected": [true, true, true, true, false, false],
    "start_symptom": 2
}

This has added an extra stage, called V, which is used to hold vaccinated individuals. Because vaccinated individuals are not infected, we have to set is_infected to false for this stage (note how this is set automatically for the R stage).

Also, once vaccinated, individuals will remain in the V stage. Thus progress for this stage is set to 0.0, just like it is automatically set for the R stage.

We can run a quick test of this model using;

metawards -d lurgy5.json -m single -a 5

This uses the single-ward model and infects 5 individuals on day 1 with the disease described in lurgy5.json.

You should see that the outbreak progresses as before, with many of the 1000-strong population of the ward infected, and, currently, no-one vaccinated.

Vaccinating using go_ward

We can model vaccination by writing a mover that uses go_ward to move up to 50 individuals per day from S to V (e.g. representing a vaccination capacity of 50 vaccinations per day).

Do this by creating a file called move_vaccinate.py and copying in the below;

from metawards.movers import MoveGenerator, go_ward


def move_vaccinate(**kwargs):
    capacity = 50  # number of vaccinations per day

    def go_vaccinate(**kwargs):
        gen = MoveGenerator(from_stage="S", to_stage="V", number=capacity)
        go_ward(generator=gen, **kwargs)

    return [go_vaccinate]

Run the model using;

metawards -d lurgy5.json -m single -a 5 --mover move_vaccinate.py

You should see that the number of vaccinated individuals climbs by 50 each day. This reduces the number of susceptibles, meaning that the infection is dampened before it can get going.

Demand-driven vaccination

This model started vaccination on the first day of the outbreak. We can instead trigger vaccination based on a threshold of infections, e.g. because maybe the vaccine has a limited supply. Edit move_vaccinate.py to add a trigger where vaccination starts only when the number of infections grows above 100, e.g.

from metawards.movers import MoveGenerator, go_ward
from metawards.utils import Console


def move_vaccinate(**kwargs):
    capacity = 50  # number of vaccinations per day
    trigger = 100  # number of infections to trigger vaccination

    def go_vaccinate(network, population, **kwargs):
        params = network.params

        # Are we vaccinating? If this has not been set then we are not
        is_vaccinating = params.user_params.get("is_vaccinating", False)

        if not is_vaccinating and population.total >= trigger:
            # trigger vaccination
            params.user_params["is_vaccinating"] = True
            is_vaccinating = True
            Console.info("Starting vaccination")

        if is_vaccinating:
            gen = MoveGenerator(from_stage="S", to_stage="V", number=capacity)
            go_ward(generator=gen, network=network,
                    population=population, **kwargs)

    return [go_vaccinate]

Run the model using;

metawards -d lurgy5.json -m single -a 5 --mover move_vaccinate.py

You should see now in the output that vaccination starts when the number of infections rises about 100, which for me happened on day 17. Vaccination of the remaining susceptibles completed by day 27, with the outbreak ending with 528 vaccinated individuals and 472 who contracted the lurgy and recovered.

National vaccination

We can extend the model to vaccinate on a per-ward basis if the number of infections climbs above a threshold in an individual ward. To do this, modify move_vaccinate.py to read;

from metawards import WardID
from metawards.movers import MoveGenerator, go_ward
from metawards.utils import Console


def move_vaccinate(**kwargs):
    capacity = 50  # number of vaccinations per day
    trigger = 20  # number of infections to trigger vaccination
    stop_trigger = 5 # number of infections to stop vaccination

    def go_vaccinate(network, workspace, **kwargs):
        # create ward-local is_vaccinating parameters, that defaults to 0.0
        is_vaccinating = network.nodes.get_custom("is_vaccinating", 0.0)

        vaccinate = []

        I_in_wards = workspace.I_in_wards

        for i in range(1, network.nnodes + 1):
            if not is_vaccinating[i]:
                if I_in_wards[i] >= trigger:
                    is_vaccinating[i] = 1.0

            if is_vaccinating[i]:
                if I_in_wards[i] < stop_trigger:
                    is_vaccinating[i] = 0.0
                else:
                    vaccinate.append(WardID(i, all_commute=True))
                    vaccinate.append(WardID(i))

        nv = int(sum(is_vaccinating))

        if nv > 0:
            print(f"Vaccinating in wards: number = {nv}")
            gen = MoveGenerator(from_ward=vaccinate,
                                from_stage="S", to_stage="V", number=capacity)
            go_ward(generator=gen, network=network,
                    workspace=workspace, **kwargs)

    return [go_vaccinate]

You can run this model using;

metawards -d lurgy5.json -m 2011Data --mover move_vaccinate.py -a ExtraSeedsLondon.dat

In this case, vaccination starts in a ward if the number of infections grows to 50 or above. Then, there is capacity for 20 vaccinations per day per ward and per ward-link. If the number of infections in the ward drops below 5 then vaccination is stopped.

For my run, I see that all wards enter the vaccination program, with eventually ~44 M vaccinations and ~10 M infections. The progress plot produced via metawards-plot is shown below;

Demographic trajectories for the ward-local vaccinations