Multiple Jurisdictions¶
With our credible model for dynamic transmission at the contact level and our ability to size environments with approriate contact distributions, we can show the true power of RKnot in modelling complex scenarios.
We have previously shown that contact restrictions and social distancing in any one community should be effective in eliminating sars-cov-2
relatively quickly. Across the world, however, many states, provinces, and countries, have experienced second and third waves spaced out months apart despite various forms of strict quarantines and lockdowns.
So why haven’t they been effective?
In part, this has resulted from the many exceptions made to these restrictions, in particular, schools remaining open in many jurisdictions.
We must also consider the impact of multiple jurisdictions that have unique characteristics, set policy independently, and, importantly, allow travel between them.
We will focus on two jurisdictions to start, building to four, and building in complexity along the way.
Mixed¶
To start, we will simply double the size of the environment used in our dynamic transmission risk models. Thus,
- n = 20,000 and 4 initial infections
- 8 groups, split evenly into two states: “East State” and “West State”
- the states will be exact reflections of each other:
- same size, n = 10,000
- 2 initial infections each
- same demographic mix
- same number of events
- the event structrue for each state is detailed in Sizing (and imported from a pickled object as per below).
- each state will have its own vbox. The
East State
vbox must be reassigned and the events adjusted to point to the correct groups.
from copy import deepcopy
from rknot import Sim, Chart
from rknot.events import Restriction, Quarantine
from rknot.dots.fhutch import tmr
from rknot.sims import us_w_load_18.events
group1 = dict(name='W0-19', n=2700, n_inf=0, ifr=0.00003, mover=.98)
group2 = dict(name='W20-49', n=4100, n_inf=1, ifr=0.0002, mover=.98)
group3 = dict(name='W50-69', n=2300, n_inf=1, ifr=0.005, mover=.98)
group4 = dict(name='W70+', n=900, n_inf=0, ifr=0.054, mover=.98)
wstate = [group1, group2, group3, group4]
wbox = {'label': 'W Main', 'box': 344}
wevents = deepcopy(us_w_load_18.events)
wrsxns = deepcopy(us_w_load_18.rsxns)
group5 = dict(name='E0-19', n=2700, n_inf=0, ifr=0.00003, mover=.98)
group6 = dict(name='E20-49', n=4100, n_inf=1, ifr=0.0002, mover=.98)
group7 = dict(name='E50-69', n=2300, n_inf=1, ifr=0.005, mover=.98)
group8 = dict(name='E70+', n=900, n_inf=0, ifr=0.054, mover=.98)
estate = [group5, group6, group7, group8]
ebox = {'label': 'E Main', 'box': 344}
eevents = deepcopy(us_w_load_18.events)
for e in eevents:
e.groups = [4,5,6,7]
e.vbox = 1
groups = wstate + estate
vboxes = [wbox, ebox]
events = wevents + eevents
params = {
'groups': groups, 'density': 1, 'days': 365, 'tmr_curve': tmr,
'vboxes': vboxes, 'events': events
}
sim = Sim(**params)
sim.run()
Below, we show the arrangement of all of the dots mixed throughout the space. The only difference from the single jurisdiction structure is the presence of two vboxes (instead of one).
On a proportionate basis, this structure is indistinguishable from the n=10,000, 4 group structure used in the viral load simulations. And it should result in the same outbreak curve, on average.
To test, we ran 50 sims for a quick comparison to the base Events structure that we are building from.
Mixed $R_0$>0 | Events w Load $R_0$>0 | |
---|---|---|
n, $R_0$ > 0 | 28 | 68 |
n | 50 | 250 |
Peak | 23.5% | 32.7% |
HIT | 38.0% | 40.6% |
Total | 53.3% | 51.5% |
Fatalities | 0.32% | 0.30% |
%>70 | 79.1% | 6.5% |
IFR | 0.60% | 0.60% |
Days to Peak | 95 | 74 |
As expected, we can see above that, on average, the spread results generated in this structure match those of the Events structure from the viral load analysis. Notice, however, that an outbreak (\(R_0\) > 0) occured much more frequently: 56% vs. 27%. This is likely a result of the greater number of initial infections (4 vs. 2).
Also notice that the average Peak is lower and Days to Peak higher in our new scenario despite all other metrics being the same.
In the distribution below, we can see that, in this scenario, more simulations result in outbreaks, but when they do, they concentrated around the same level of total infection.
We can see this in the sample simulation below:
Above, we can see a “wave” effect occuring with two peaks forming, the first around 70 days and second around 115 days. We can seperate the curve among the constituent states to see how each state’s curve might have added to the aggregate.
From above, we can see that what looked like a single curve and a single outbreak was, in fact, two outbreaks among the two “states”, West and East.
Remarkably, the dots from both states are free to mix in the main grid. The separate waves result simply from isolated event spaces where only the largest events (3+ capacity) occur within the state groups.
Next we will show the impact of further isolating the two states.
Borders¶
Now we split the environment into two halves, separating the subjects of the West and East states by a border.
They cannot interact in any way.
This can be accomplished by simply assign a separate box to the groups in each state, as per below:
for w in wstate:
w['box'] = {'bounds': [1, 72, 1, 144], 'label': 'West State'}
for e in estate:
e['box'] = {'bounds': [73, 144, 1, 144], 'label': 'East State'}
groups = wstate + estate
params['groups'] = groups
sim = Sim(**params)
sim.run(dotlog=True)
This results in the environment shown below:
We ran 50 simulations to compare with the Mixed environment:
Borders $R_0$>0 | Mixed $R_0$>0 | |
---|---|---|
n, $R_0$ > 0 | 26 | 28 |
n | 50 | 50 |
Peak | 19.1% | 23.5% |
HIT | 25.0% | 38.0% |
Total | 31.5% | 53.3% |
Fatalities | 0.20% | 0.32% |
%>70 | 78.9% | 79.1% |
IFR | 0.61% | 0.60% |
Days to Peak | 78 | 95 |
We can see:
- an outbreak occurs with about the same frequency; likely b/c both environments have the same number of initial infections
- the size of the outbreaks is significantly lower in the split environment
- the peaks are significantly lower in the split environment
Contact Mix
Importantly, doubling the number of subjects does not change the number of contacts. Instead, it changes who is contacted. And so when the environment is split and the contact mix becomes more heterogeneous, spread is curtailed.
Again, we see below that the contact distribution of the Borders environment is quite different from the Mixed environment.
Here, the same number of outbreaks occur, but the virus cannot cross the border so at worst it can only spread amongst half the population. As a result, we see two local peaks form, representing when an outbreak occurs in only one state or in both.
In a uniform environment, there are only two outcomes:
- Peak
- No Peak
When we split the space, there are now 4 distinct possible outcomes:
- No Peaks
- One Peak: West
- One Peak: East
- Two Peaks
We can even get a quick sense for how often these different outcomes might occur, as per below.
No Peak | West Peak | East Peak | Double Peak | |
---|---|---|---|---|
Number of Occurences | 24 | 11 | 10 | 5 |
As per the piechart above,only 10% of outbreaks resulted in a Double Peak, while more than 40% resulted in an outbreak in either state. Both states were equally likely to have an outbreak on their own.
Below we see a representative simulation that evidences the border separating the two states. We can also see in the grid on the left that the outbreak was entirely isolated to the West state and did not seep into the East state at all. This resulted in the shorter, more muted peak.
Finally, we also show a typical Double Peak scenario:
Here we see both states develop outbreaks independently and simultaneously, both with very similar spread curves, peaking just after 60 days and reaching ~17% peaks. The aggregate curve is inline with expectations for a well-mixed environment.
Border Crossings¶
Now that we have controlled borders between isolated states, we can explore the impact of border crossings on spread.
To do so, we will create Travel events allowing subjects from each state to cross over into the other state, as follows:
- total of 20 crossings per day
- 10 crossings per day West to East
- 10 crossings per day East to West
- each group will have equal likelihood to participate in the crossings
The crossing amount was roughly determined based on international travel statistics out of the US:
- ~100MM travellers annually *from* the US to other countries
- ~90MM visits *from* other countries annually
- we have assumed 100MM for reach version.
To do this, we will create 8 random locations for each event (within each state’s box), then assign them to our 8 travel events.
from rknot.events import Travel
xs = np.random.randint(19, 120, size=4)
ys = np.random.randint(20, 120, size=4)
w_locs = np.vstack((xs, ys)).T
xs = np.random.randint(74, 120, size=4)
ys = np.random.randint(10, 120, size=4)
e_locs = np.vstack((xs, ys)).T
crosses = []
for i in range(9):
crosses += [Travel(name=f'WtoE_{i}', xy=w_locs[i], start_tick=1,
groups=[0,1,2,3], capacity=1, duration=1, recurring=1
)]
crosses += [Travel(
name=f'EtoW_{i}', xy=e_locs[i], start_tick=1,
groups=[4,5,6,7], capacity=1, duration=1, recurring=1
)]
params['events'] += crosses
The results of 50 simulations are shown below:
Border Crossings $R_0$>0 | Borders $R_0$>0 | |
---|---|---|
n, $R_0$ > 0 | 17 | 26 |
n | 50 | 50 |
Peak | 17.6% | 19.1% |
HIT | 23.5% | 25.0% |
Total | 32.7% | 31.5% |
Fatalities | 0.19% | 0.20% |
%>70 | 80.0% | 78.9% |
IFR | 0.55% | 0.61% |
Days to Peak | 78 | 78 |
We see only a limited impact from border travel as constructed. The structure actually resulted in fewer outbreaks and resulted in similar single and double peak occurences.
No Peak | West Peak | East Peak | Double Peak | |
---|---|---|---|---|
Number of Occurences | 32 | 6 | 7 | 5 |
Below we show a typical Double Peak outcome. The outbreak is almost entirely extinguished and in decline in the East state, but then spread takes hold independently in the West State and creates a second peak.
Crossings with Event Access¶
The above simulation has major omission. Each traveller only visits the other state for a single tick, only ever land on a single location in the other state’s main grid, and so have almost no contact with anyone while travelling.
In reality, travellers often have extended stays of 3/5/7/15/30+ days and, while in the other jurisidiction, they often attend events / have contacts in that jurisdiction like any other local person.
We can mimick this more realistic mixing approach by utilizing the MultiTravel
event object. This object also a travel event greater than a single tick and allows any participants to attend events in their new region.
We must also implementing a few changes to the existing events. Each event will now pull participants from its own main grid as well as any box created to support cross border travel into the state.
We assume:
- 9 visits per day per state, consistent with the prior scenario
- Respective durations of the 9 visits:
- 4 visits are for 3 days
- 3 visits are for 5 days
- 1 visits is for 7 days
- 1 visit is for 11 days
The code looks like this:
from rknot.events import MultiTravel
for e in params['events']:
e.groups = np.arange(8)
if 'W' in e.name:
e.from_boxes = ['West State', 'EtoW']
e.no_events = False
if 'E' in e.name:
e.from_boxes = ['East State', 'WtoE']
e.no_events = False
With the existing events restructured, we assign each event to locations in their box at random.
n_cross = 9
xs = np.random.randint(19, 120, size=n_cross)
ys = np.random.randint(20, 120, size=n_cross)
w_locs = np.vstack((xs, ys)).T
xs = np.random.randint(74, 120, size=n_cross)
ys = np.random.randint(10, 120, size=n_cross)
e_locs = np.vstack((xs, ys)).T
crosses = []
for i, dur in zip(range(n_cross), (3,3,3,3,5,5,5,7,11)):
crosses += [MultiTravel(name=f'WtoE_{dur}', xy=e_locs[i], start_tick=1,
groups=[0,1,2,3], capacity=1, recurring=1, duration=dur
)]
crosses += [MultiTravel(
name=f'EtoW_{dur}', xy=w_locs[i], start_tick=1,
groups=[4,5,6,7], capacity=1, recurring=1, duration=dur
)]
params['events'] += crosses
50 iterations of the above scenario lead to the following:
Crossings w Events $R_0$>0 | Mixed $R_0$>0 | Crossings $R_0$>0 | |
---|---|---|---|
n, $R_0$ > 0 | 21 | 28 | 17 |
n | 52 | 50 | 50 |
Peak | 23.4% | 23.5% | 17.6% |
HIT | 35.6% | 38.0% | 23.5% |
Total | 51.8% | 53.3% | 32.7% |
Fatalities | 0.33% | 0.32% | 0.19% |
%>70 | 75.8% | 79.1% | 80.0% |
IFR | 0.64% | 0.60% | 0.55% |
Days to Peak | 86 | 95 | 78 |
Incredibly, we see that just 10 border crossings per day with travellers mixing normally leads to (almost) the same the results as though there were no border at all.
We can also see that the peak distribution has inverted almost completely. Now a Double Peak event is far more likely than a single peak in either state.
No Peak | Single Peak | Double Peak | |
---|---|---|---|
Number of Occurences | 31 | 1 | 20 |
We can use some additional tools to confirm that mixing between the states was in fact the culprit in increased prevalence of double peaks. The easiest way is to find simulations where a state reaches zero infections, effectively eradicating the virus in prior scenarios, only to have the virus re-emerge.
This can ONLY occur if the virus was transported in from the other state.
{0: 'East', 17: 'East', 21: 'West', 43: 'West', 45: 'East'}
It also tells us the index position of those states in our grouping of simulations. We will isolate simulation # 21, where West State inherited an outbreak.
We can isolate the specific subject that carried the virus.
[26]:
sim = xesims[21]
id_groups = [westids, eastids]
west_inf, east_inf = find_group_infs(sim, id_groups, westids, eastids)
westtrim = np.trim_zeros(west_inf, 'fb')
Above is the number of current infections in the West State at each tick during the simulation. We can clearly see an entire 14-day period during that simulation where the West State had zero infections.
Then, on Tick 44, a single new infection emerges. We can easily isolate which subject was infected.
[27]:
from rknot.dots import MATRIX_COL_LABELS as ML
erads = np.argwhere(westtrim == 0)
i_erad = erads[-1, 0]
dots = sim.dotlog[i_erad + 1]
westmask = np.isin(dots[:, ML['group_id']], westids)
west = dots[westmask]
west[west[:, ML['is_inf']] == 1]
[27]:
array([[ 5210, 1, 1, 0, 0, 1, 1, 15569,
109, 18, 15569, 109, 18, 1, 2, 0,
224729, 5, 98, 100, 20, 44, -1, 75,
441, 1, 1]], dtype=int32)
The array above shows the state of subject 5210 as of Tick 44. We can expand the dot matrix to ticks both before and after to get a sense of this subjects movement.
[28]:
id_new = west[west[:, ML['is_inf']] == 1][0,0]
dottrace = sim.dotlog[i_erad - 4 : i_erad + 4, id_new]
We can take the array above and put it in a table for easier viewing:
[29]:
ticks = np.arange(i_erad - 4, i_erad + 4)
dtrace = np.zeros(shape=(8,28), dtype=np.int32)
dtrace[:, 0] = ticks
dtrace[:, 1:] = dottrace
tick | id | group_id | is_inf | x | y | homex | homey | event_id |
---|---|---|---|---|---|---|---|---|
40 | 5210 | 1 | 0 | 18 | 138 | 54 | 105 | 35205 |
41 | 5210 | 1 | 0 | 54 | 105 | 54 | 105 | -1 |
42 | 5210 | 1 | 0 | 54 | 105 | 54 | 105 | -1 |
43 | 5210 | 1 | 0 | 54 | 105 | 109 | 18 | 224729 |
44 | 5210 | 1 | 0 | 128 | 139 | 109 | 18 | 214892 |
45 | 5210 | 1 | 1 | 109 | 18 | 109 | 18 | 224729 |
46 | 5210 | 1 | 1 | 54 | 105 | 54 | 105 | -1 |
47 | 5210 | 1 | 1 | 54 | 105 | 54 | 105 | -1 |
From the table above we can see the following:
- Subject 5210 was inside the West State box and not infected in the days prior to Tick 44.
- On Tick 42, the subject travelled to East State via 3-day
MultiTravel
event. - On Tick 43, the subject attended an event at location (128, 139), where it was infected.
- On Tick 45, the subject returned to West State and is at least partly responsible for the re-emergence of the virus in that state.
We can even confirm this visually by isolating that specific dot in the animation by passing highlight=[5210]
at the Chart instantiation. We also slowed down the frame rate. You can see at Tick 44, the subject jumps from West to East and back very quickly.
chart = Chart(sim, highlight=[5210], show_intro=True, show_restricted=True)
chart.to_html5_video()
Finally above, we show the full outcome of the sim, which pesents almost as a single prolonged peak.
Care Homes¶
With a few modifications, we can incorporate features of the Care Home to further increase realism.
Again, we simply double the number of groups, double the number of events, and assign a set of cross border events. In this sim, the 70+G
group in each state is restricted from cross border travel.
import numpy as np
from copy import deepcopy
from rknot import Sim, Chart
from rknot.events import Restriction, Quarantine, Travel, MultiTravel
from rknot.dots.fhutch import tmr
from rknot.sims import us_w_load_18
group1 = dict(name='W0-19', n=2700, n_inf=0, ifr=0.00003, mover=0.982)
group2a = dict(name='W20-49', n=4034, n_inf=1, ifr=0.0002, mover=0.982)
group2b = dict(name='WCHW', n=66, n_inf=0, ifr=0.0002, mover=0.982)
group3 = dict(name='W50-69', n=2300, n_inf=1, ifr=0.005, mover=0.982)
group4a = dict(name='W70+', n=600, n_inf=0, ifr=0.042, mover=0.982)
group4b = dict(name='W70+G', n=300, n_inf=0, ifr=0.0683, mover='local')
wstate = [group1, group2a, group2b, group3, group4a, group4b]
group5 = dict(name='E0-19', n=2700, n_inf=0, ifr=0.00003, mover=0.982)
group6a = dict(name='E20-49', n=4034, n_inf=1, ifr=0.0002, mover=0.982)
group6b = dict(name='ECHW', n=66, n_inf=0, ifr=0.0002, mover=0.982)
group7 = dict(name='E50-69', n=2300, n_inf=1, ifr=0.005, mover=0.982)
group8a = dict(name='E70+', n=600, n_inf=0, ifr=0.042, mover=0.982)
group8b = dict(name='E70+G', n=300, n_inf=0, ifr=0.0683, mover='local')
estate = [group5, group6a, group6b, group7, group8a, group8b]
for w in wstate:
w['box'] = {'bounds': [1, 72, 1, 144], 'label': 'West State'}
group4b['box'] = [1,6,1,6]
for e in estate:
e['box'] = {'bounds': [73, 144, 1, 144], 'label': 'East State'}
group8b['box'] = [139,144,1,6]
groups = wstate + estate
wbox = {'label': 'W Main', 'box': 344}
ebox = {'label': 'E Main', 'box': 344}
wevents = deepcopy(us_w_load_18.events_gated)
eevents = deepcopy(us_w_load_18.events_gated)
# Care Home Events
i_ch = -34
for e in wevents[i_ch:]:
w.name = f'W_{w.name}'
for e in eevents[i_ch:]:
e.name = f'E_{e.name}'
# Main Events
for w in wevents[:i_ch]:
w.name = f'W{e.name}'
w.from_boxes = ['West State', 'EtoW']
e.groups = np.arange(8)
e.no_events = False
for e in eevents[:i_ch]:
e.name = f'W{e.name}'
e.from_boxes = ['East State', 'WtoE']
e.vbox = 1
e.groups = np.arange(8)
e.no_events = False
groups = wstate + estate
vboxes = [wbox, ebox]
events = wevents + eevents
# Add border crossings
xs = np.random.randint(19, 120, size=4)
ys = np.random.randint(20, 120, size=4)
w_locs = np.vstack((xs, ys)).T
xs = np.random.randint(74, 120, size=4)
ys = np.random.randint(10, 120, size=4)
e_locs = np.vstack((xs, ys)).T
crosses = []
for i, rec in zip(range(4), (3,3,5,5)):
crosses += [MultiTravel(name=f'WtoE_{i}', xy=e_locs[i], start_tick=1,
groups=[0,1,2,3,4], capacity=1, recurring=rec
)]
crosses += [MultiTravel(
name=f'EtoW_{i}', xy=w_locs[i], start_tick=1,
groups=[6,7,8,9,10], capacity=1, recurring=rec
)]
events_ch += crosses
rsxns = []
quars = []
params_ch = {
'groups': groups, 'density': 1, 'days': 365,
'tmr_curve': tmr, 'vboxes': vboxes,
'events': events_ch, 'rsxns': rsxns, 'quars': quars,
}
sim = Sim(**params_ch)
sim.run(dotlog=True)
The results of 60 simulation show outcomes very similar to the Crossing scenario.
Care Homes $R_0$ > 0 | Crossings $R_0$ > 0 | |
---|---|---|
n, $R_0$ > 0 | 29 | 21 |
n | 60 | 52 |
Peak | 26.1% | 23.4% |
HIT | 37.6% | 35.6% |
Total | 55.3% | 51.8% |
Fatalities | 0.40% | 0.33% |
%>70 | 84.6% | 75.8% |
IFR | 0.73% | 0.64% |
Days to Peak | 84 | 86 |
We can also per below that in these 60 simulations, not a single outbreak was isolated to only one state.
No Peak | Single Peak | Double Peak | |
---|---|---|---|
Number of Occurences | 30 | 1 | 29 |
Finally, we show a sample simulation.
Care Homes - 10Max¶
From the Care Home environment, we will begin to investigate the impact of policy restrictions in the two jurisdictions on spread. The restrictions are derived from those used previously in the SIR and Dynamic Transmission.
In our first scenario, both West State and East State will simultaneously impose a restriction as follows:
- no events with more than 10 subjects
- commences on Day 30
- lasting 120 days
- restriction covers all groups in the respective state
- there is 90% adherence to the restriction
Reminder that there are 10 subjects per day travelling between states.
From our prior implementation, we simply add the following code to the rsxns
keyword of our parameters.
wcap = Restriction(
name='large', start_tick=30, duration=120,
criteria={'capacity': 10, 'groups': [0,1,2,3,4,5]}, adherence=.9
)
ecap = Restriction(
name='large', start_tick=30, duration=120,
criteria={'capacity': 10, 'groups': [6,7,8,9,10,11]}, adherence=.9
)
params_ch['rsxns'] = [wcap, ecap]
sim = Sim(**params_ch)
sim.run(dotlog=True)
The results of 48 simulations are shown below:
Care Homes 10Max $R_0$ > 0 | Care Homes $R_0$ > 0 | |
---|---|---|
n, $R_0$ > 0 | 16 | 29 |
n | 48 | 60 |
Peak | 4.9% | 26.1% |
HIT | 5.4% | 37.6% |
Total | 5.5% | 55.3% |
Fatalities | 0.20% | 0.40% |
%>70 | 93.2% | 84.6% |
IFR | 3.60% | 0.73% |
Days to Peak | 52 | 84 |
We can see from the above that a 10 max daily contact restriction remains highly effective at suppressing spread and eradicating the virus, this despite no restrictions on cross-border travel.
This is because, even if subjects travel cross border, their ability to mix with the population in the visiting state is severly limited.
We can see in the peak distribution below that isolated outbreaks in either state re-emerge somewhat, however, the level of total infections in all scenarios is substantially lower.
No Peak | Single Peak | Double Peak | |
---|---|---|---|
Number of Occurences | 27 | 6 | 15 |
Below we show a sample simulation of a single peak outbreak. Notice that spread is materially curtailed and that fatalities are isolated almost exclusively to the 70+G group in the care homes.
Care Homes - 25Max¶
We will augment the prior scenario only slightly, replacing the 10 max capacity restriction with a 25 max capacity restrction in each state and tuning the adherence down to 75%.
The code is as follows:
```python wcap = Restriction( name=’large’, start_tick=30, duration=120, criteria={‘capacity’: 25, ‘groups’: [0,1,2,3,4,5]}, adherence=.75 ) ecap = Restriction( name=’large’, start_tick=30, duration=120, criteria={‘capacity’: 25, ‘groups’: [6,7,8,9,10,11]}, adherence=.75 ) params_ch[‘rsxns’] = [wcap, ecap]
sim = Sim(**params_ch) sim.run(dotlog=True) ```
The results of 50 simulations are below:
Care Homes 25Max $R_0$ > 0 | Care Homes 10Max $R_0$ > 0 | |
---|---|---|
n, $R_0$ > 0 | 22 | 16 |
n | 50 | 48 |
Peak | 8.1% | 4.9% |
HIT | 9.6% | 5.4% |
Total | 11.3% | 5.5% |
Fatalities | 0.21% | 0.20% |
%>70 | 89.9% | 93.2% |
IFR | 2.11% | 3.60% |
Days to Peak | 67 | 52 |
As expected, and inline with the same restriction in previous environments, spread impact is substantial, though less than a 10max restrction.
Also, note in the distribution above that there was a single simulation with total infections of ~58%. So, despite the best efforts of the populace under this policy, the virus was able to survive the lockdown and being spread unabated thereafter.
We can see below that peaks in both regions was the prevailing outcome when there was spread, although isolated spread did occur.
Our sample simulations will first focus on that single outcome where total infections reached 59%.
We can see in the spread curve that both regions had infections essentially driven down to zero during the restriction period. Even as restrictions are lifted, spread remains very limited, only begins to rip higher in both states almost simultaeously at around 210 days. This is a full 60 days after the restrictions were lifted.
We can see just how close the virus was to be eradicated in the sim logs.
Scipy can help us quickly find local minimums in the curr_inf
log via argrelextrema
.
[50]:
from scipy.signal import argrelextrema
sim = ch25sims[6]
mins, = argrelextrema(sim.log['curr_inf'], np.less)
lowest = sim.log['curr_inf'][mins].min()
i_lowest = np.argwhere(sim.log["curr_inf"] == 44).ravel()[0]
print (f'Low: {lowest}')
print (f'Day of Low: {i_lowest}')
Low: 44.0
Day of Low: 203
[52]:
from rknot.dots import MATRIX_COL_LABELS as ML
infmask = sim.dotlog[i_lowest][:, ML['is_inf']] == 1
infdots = sim.dotlog[i_lowest][infmask]
print (infdots.shape)
(44, 27)
And we can show that all infected subjects were in the West State:
[53]:
np.all(infdots[:, ML['group_id']] <= 5)
[53]:
True
Finally, below we show the more typical outcome from this restriction, which is a quick run up in infections prior to and just after restrictions are put in place. Then, as the restrictions take hold, spread deflates and is eliminated very quickly.
Independent Policy¶
Now, we will explore some more complex structures.
In the dynamic transmission model, we have seen policy restrictions can be successful at supressing spread and ultimately eradicating the virus (see here and here). In fact, these policy measures have proven fairly resilient even in the face of low adherence.
But what happens when there are is mixing between multiple jurisdictions that take independent approaches to policy?
We will build from the Care Homes scenario above.
In this sim, West State will be the more aggressive actor, implementing faster and more imposing policy. The East State will act more slowly and with less imposition. Accordingly, the resrictions will be:
West State:
- Maximum 10 contacts, from Day 30 to Day 60
- Maximum 25 contacts, from Day 60 to Day 105
- Maximum 75 contacts, from Day 105 to Day 150
*This was a common approach for many of the northern US states and Canadian provinces in the Spring of 2020.*
East State:
- Maximum 25 contacts, from Day 75 to Day 105
- Maximum 75 contacts, from Day 105 to Day 135
Under isolated circumstances, the West State would most often experience no outbreak and at worst only a limited one. When bordering with a less rigourous neighbor, the results prove to be very different.
Below is the code adding these restrictions.
wcap1 = Restriction(
name='wcap1', start_tick=30, duration=30,
criteria={'capacity': 10, 'from_boxes': ['West State', 'EtoW']}, adherence=.9
)
wcap2 = Restriction(
name='wcap2', start_tick=30, duration=45,
criteria={'capacity': 25, 'from_boxes': ['West State', 'EtoW']}, adherence=.75
)
wcap3 = Restriction(
name='wcap3', start_tick=75, duration=45,
criteria={'capacity': 75, 'from_boxes': ['West State', 'EtoW']}, adherence=.75
)
ecap1 = Restriction(
name='ecap1', start_tick=75, duration=30,
criteria={'capacity': 25, 'from_boxes': ['East State', 'WtoE']}, adherence=.5
)
ecap2 = Restriction(
name='ecap2', start_tick=105, duration=30,
criteria={'capacity': 75, 'from_boxes': ['East State', 'WtoE']}, adherence=.5
)
params['rsxns'] = [wcap1, wcap2, wcap3, ecap1, ecap2]
sim = Sim(**params)
sim.run(dotlog=True)
The results of 50 simulations are shown below:
Care Homes Uncoordinated $R_0$ > 0 | Care Homes 25Max $R_0$ > 0 | Care Homes $R_0$ > 0 | |
---|---|---|---|
n, $R_0$ > 0 | 22 | 22 | 29 |
n | 50 | 50 | 60 |
Peak | 16.1% | 8.1% | 26.1% |
HIT | 25.9% | 9.6% | 37.6% |
Total | 37.7% | 11.3% | 55.3% |
Fatalities | 0.32% | 0.21% | 0.40% |
%>70 | 85.2% | 89.9% | 84.6% |
IFR | 1.02% | 2.11% | 0.73% |
Days to Peak | 94 | 67 | 84 |
Spread comes back with a vengenance in this scenario, showing very limited impact from the implementations, although the time to peak does increase for reasons we’ll see below.
In the histrogram above, note that two local maxima have once again formed, suggesting outcomes with both single peaks and double peaks, including a maxima around ~55% total infections, which is consistent with full outbreaks across both states (despite the strict implementations int eh West!).
We can the prevalence of double peaks below.
No Peak | Single Peak | Double Peak | |
---|---|---|---|
Number of Occurences | 27 | 2 | 21 |
We can also so the composition of spread amongst the two regions in the bar chart below:
Of the 22 simulations where spread was greater than 1%, the West State accounted for about half of all infections about half the time. So, where its implementations in isolation would be expected to reduce the risk of an outbreak to only 1%, here its risk remains 25%.
Below we show an example of such a double peak.
Spread begins in the East and is eventually carried into the West. The East State’s policies have essentially zero impact on spread, as the peak appears to occur before its 25 max restriction is implemented.
We can see a very modest peak in the West early in the sim and, essentially on queue with the loosening of restrictions, around Day 110 spread takes hold and rips through the West State.
We could map this onto a real world scenario where:
- A new virus is discovered. Both states are unsure how widespread it may be.
- West State locks down as a preventative measure, driven by a focus on public health. East State does not, driven by a focus on personal freedoms / responsibility / etc.
- West State derides East State for its lack of action.
- West State, believeing its outbreak is totally under control and seeing the outbreak on the downside, beings to reopen.
- Because the virus is not fully eradicated, West State inherits the virus from East State and, because West State does not have herd immunity, the virus spreads unabated.
- West State’s policy actions are for not.
Finally, we also show the example of a single peak where the West avoids the 2nd wave of spread.