Responding to changing conditions

We’ve now created a lock-down advance function, but are currently triggering this function in an iterator based on a fixed number of days since the outbreak started.

A better approach would be to trigger the lock-down based on the number of individuals who are detected as infected in the model.

To do this, edit your lockdown.py script and copy in the following;

from metawards.iterators import iterate_working_week, \
                                advance_infprob, \
                                advance_fixed, \
                                advance_play

def advance_lockdown(**kwargs):
    print("We are on lockdown")
    advance_infprob(scale_rate=0.25, **kwargs)
    advance_play(**kwargs)

def iterate_lockdown(population, **kwargs):
    if not hasattr(population, "lockdown_state"):
        population.lockdown_state = "before"
        population.is_locked_down = False

    if population.lockdown_state == "before":
        if population.total > 5000:
            population.lockdown_state = "lockdown"
            population.lockdown_started = population.day
            population.is_locked_down = True

    if population.is_locked_down:
        return [advance_lockdown]
    else:
        return iterate_working_week(population=population,
                                    **kwargs)

The first thing we do here is see if the population has a lockdown_state variable using the standard Python hasattr function. This variable won’t exist on the first call to to iterate_lockdown, and so here we set the lockdown_state to before and set the flag population.is_locked_down to False.

Next, we check if the lockdown is in the before state. If it is, then if the total infected population is greater than 5000 we change the lockdown_state to lockdown, save the day the lockdown started to population.lockdown_started, and set the flag population.is_locked_down to True.

Finally, we either return our advance_lockdown advance function, or the standard advance functions for a working week depending on the value of the population.is_locked_down flag.

Run the model and draw the overview graph using;

metawards -d lurgy3 --additional ExtraSeedsLondon.dat --iterator lockdown
metawards-plot -i output/results.csv.bz2 --format jpg --dpi 150

You should now see that the lockdown takes effect some time after the infected population grows above 5000. This tips the curve and reduces the spread of the disease. You can see what my graphs looked like here;

Overview image of a automatic lockdown

Releasing lockdown

We can use the data in population to decide when to release the lockdown as well. For example, we could release when the size of the infected population drops below 2000. To do this, edit your lockdown.py file to read;

from metawards.iterators import iterate_working_week, \
                                advance_infprob, \
                                advance_fixed, \
                                advance_play

def advance_lockdown(**kwargs):
    print("We are on lockdown")
    advance_infprob(scale_rate=0.25, **kwargs)
    advance_play(**kwargs)

def iterate_lockdown(population, **kwargs):
    if not hasattr(population, "lockdown_state"):
        population.lockdown_state = "before"
        population.is_locked_down = False

    if population.lockdown_state == "before":
        if population.total > 5000:
            population.lockdown_state = "lockdown"
            population.lockdown_started = population.day
            population.is_locked_down = True

    elif population.lockdown_state == "lockdown":
        if population.total < 2000:
            population.lockdown_state = "after"
            population.lockdown_ended = population.day
            population.is_locked_down = False

    if population.is_locked_down:
        return [advance_lockdown]
    else:
        return iterate_working_week(population=population,
                                    **kwargs)

Run the model as before and see what happens…

To start, the lockdown has worked and the number of infections has fallen, with the number falling below 2000 on day 78. However, releasing the lockdown completely leads to a rapid growth in the infection, with over 85,000 infected three weeks after lockdown ended. This is unsurprising, as there was still a lot of infected individuals remaining once lockdown ended, and a large population that was still susceptible to infection (as you can see from the print of my run and the overview graph below).

S: 56057097    E: 213    I: 2189    R: 22578    IW: 197   TOTAL POPULATION 56081864
We are on lockdown

76 2402
S: 56056905    E: 200    I: 2133    R: 22839    IW: 184   TOTAL POPULATION 56081877
We are on lockdown

77 2333
S: 56056746    E: 192    I: 2101    R: 23038    IW: 157   TOTAL POPULATION 56081885
We are on lockdown

78 2293
S: 56056568    E: 159    I: 2061    R: 23289    IW: 176   TOTAL POPULATION 56081918
We are on lockdown

79 2220
S: 56056399    E: 178    I: 1983    R: 23517    IW: 162   TOTAL POPULATION 56081899

80 2161
S: 56055290    E: 169    I: 1904    R: 24714    IW: 942   TOTAL POPULATION 56081908

81 2073
S: 56054625    E: 1109    I: 1853    R: 24490    IW: 596   TOTAL POPULATION 56080968

82 2962
S: 56053957    E: 665    I: 2728    R: 24727    IW: 602   TOTAL POPULATION 56081412

83 3393
S: 56052602    E: 668    I: 3162    R: 25645    IW: 1151   TOTAL POPULATION 56081409

84 3830
S: 56051035    E: 1355    I: 3601    R: 26086    IW: 1304   TOTAL POPULATION 56080722

85 4956
S: 56049197    E: 1567    I: 4689    R: 26624    IW: 1492   TOTAL POPULATION 56080510

86 6256
S: 56046897    E: 1838    I: 5977    R: 27365    IW: 1811   TOTAL POPULATION 56080239

87 7815
S: 56044064    E: 2300    I: 7445    R: 28268    IW: 2140   TOTAL POPULATION 56079777

88 9745
S: 56041777    E: 2833    I: 9318    R: 28149    IW: 1808   TOTAL POPULATION 56079244

89 12151
S: 56039009    E: 2287    I: 11558    R: 29223    IW: 2106   TOTAL POPULATION 56079790

90 13845
S: 56033706    E: 2768    I: 13086    R: 32517    IW: 3391   TOTAL POPULATION 56079309

91 15854
S: 56027735    E: 5303    I: 14968    R: 34071    IW: 3622   TOTAL POPULATION 56076774

92 20271
S: 56020915    E: 5971    I: 19179    R: 36012    IW: 3947   TOTAL POPULATION 56076106

93 25150
S: 56012334    E: 6820    I: 23835    R: 39088    IW: 4518   TOTAL POPULATION 56075257

94 30655
S: 56001786    E: 8581    I: 29059    R: 42651    IW: 4947   TOTAL POPULATION 56073496

95 37640
S: 55994025    E: 10548    I: 35825    R: 41679    IW: 4377   TOTAL POPULATION 56071529

96 46373
S: 55984596    E: 7761    I: 44092    R: 45628    IW: 4847   TOTAL POPULATION 56074316

97 51853
S: 55965009    E: 9429    I: 49023    R: 58616    IW: 6443   TOTAL POPULATION 56072648

98 58452
S: 55943662    E: 19587    I: 55002    R: 63826    IW: 6618   TOTAL POPULATION 56062490

99 74589
S: 55919882    E: 21347    I: 70325    R: 70523    IW: 6883   TOTAL POPULATION 56060730

100 91672
S: 55889466    E: 23780    I: 86623    R: 82208    IW: 7276   TOTAL POPULATION 56058297
Overview image of a automatically released lockdown

Note how the second wave of infection makes the initial wave almost invisible in this graph. The only visible evidence is the small peak in the number of infected wards (IW) plot.

Relaxing, not removing lockdown

The problem is that we treated lockdown like a binary switch, and immediately went back to normal once it was lifted.

Instead, we need to release the lockdown in stages. To model this, edit your lockdown.py to contain the following.

from metawards.iterators import iterate_working_week, \
                                advance_infprob, \
                                advance_fixed, \
                                advance_play

def advance_lockdown(population, **kwargs):
    print(f"We are on lockdown ({population.lockdown_scale_rate})")
    advance_infprob(scale_rate=population.lockdown_scale_rate,
                    **kwargs)
    advance_play(population=population, **kwargs)

def iterate_lockdown(population, **kwargs):
    try:
        population.lockdown_state
    except Exception:
        population.lockdown_state = "before"
        population.is_locked_down = False
        population.lockdown_scale_rate = 0.05

    if population.lockdown_state == "before":
        if population.total > 5000:
            population.lockdown_state = "lockdown"
            population.lockdown_started = population.day
            population.is_locked_down = True

    elif population.lockdown_state == "lockdown":
        if population.total < 2000:
            population.lockdown_state = "relaxed_lockdown"
            population.lockdown_ended = population.day
            population.lockdown_scale_rate = 0.10
            population.is_locked_down = True

    elif population.lockdown_state == "relaxed_lockdown":
        if population.total < 1000:
            population.lockdown_scale_rate = 0.20
        else:
            population.lockdown_scale_rate = 0.10

    if population.is_locked_down:
        return [advance_lockdown]
    else:
        return iterate_working_week(population=population,
                                    **kwargs)

In this code we have created a new lockdown state that we’ve called relaxed_lockdown. This is entered when the number of infections drops below 2000. In this state controls can be released that correspond to now only halving the infection rate (scale_rate is increased to 0.10 from 0.05 during the strong lockdown). In the relaxed_lockdown state the infected population is always checked. If it is below 1000 then the lockdown can be relaxed even more, with the scale_rate increasing from 0.10 to 0.20. However, if the infected population rises above 1000, then the lockdown is tightened and the scale_rate is lowered again to 0.10.

Have a go at running using this iterator. What do you see? In my case I see the model moving from lockdown (scale_factor==0.05), through relaxed lockdown (scale_factor==0.1) to light lockdown (scale_factor==0.2) during the outbreak, which is brought under control. The overview plots are here;

Overview image of a automatically relaxing lockdown

There is a small second peak as the lockdown is relaxed, but this seems to be under control.

Warning

Remember, we cannot read too much into single model runs as these are very stochastic simulations. We would need to run models many times and average before we could gain real insight.

Returning to work

Because Python is dynamically typed, we can set whatever flags or add whatever data we want to the population object that we need (or indeed to any Python object).

Let’s now add an extra flag that will be used by advance_lockdown to call advance_fixed if the lockdown has been lifted sufficiently for people to return to work. Copy the below into your lockdown.py file;

from metawards.iterators import iterate_working_week, \
                                advance_infprob, \
                                advance_fixed, \
                                advance_play

def advance_lockdown(population, **kwargs):
    print(f"We are on lockdown ({population.lockdown_scale_rate})")
    print(f"is_work_locked_down == {population.is_work_locked_down}")
    advance_infprob(scale_rate=population.lockdown_scale_rate,
                    **kwargs)

    advance_play(population=population, **kwargs)

    if not population.is_work_locked_down:
        advance_fixed(population=population, **kwargs)

def iterate_lockdown(population, **kwargs):
    if not hasattr(population, "lockdown_state"):
        population.lockdown_state = "before"
        population.is_locked_down = False

    if population.lockdown_state == "before":
        if population.total > 5000:
            population.lockdown_state = "lockdown"
            population.lockdown_started = population.day
            population.is_locked_down = True
            population.lockdown_scale_rate = 0.05
            population.is_work_locked_down = True

    elif population.lockdown_state == "lockdown":
        if population.total < 2000:
            population.lockdown_state = "relaxed_lockdown"
            population.lockdown_ended = population.day
            population.lockdown_scale_rate = 0.05
            population.is_locked_down = True
            population.is_work_locked_down = False

    elif population.lockdown_state == "relaxed_lockdown":
        population.is_work_locked_down = False

        if population.total > 5000:
            population.lockdown_state = "lockdown"
            population.lockdown_started = population.day
            population.is_locked_down = True
            population.lockdown_scale_rate = 0.05
            population.is_work_locked_down = True
        elif population.total < 1000:
            population.lockdown_scale_rate = 0.1
        else:
            population.lockdown_scale_rate = 0.05

    if population.is_locked_down:
        return [advance_lockdown]
    else:
        return iterate_working_week(population=population,
                                    **kwargs)

This is getting longer, but I hope you can see that all we have added is a population.is_work_locked_down flag, plus some extra code to flip this between True and False. This flag is read by advance_lockdown, which calls advance_fixed if the flag is False. We’ve also added a check to see if the infected population rises above 5000 while in “relaxed lockdown”, and if it does, to re-enter full lockdown.

Run the model and plot the graphs. What do you see? Do you get a graph similar to below?

Overview image of a automatically relaxing lockdown