Key Concepts¶
RKnot builds a simulation across four dimensions of global properties:
- Time
- The fundamental unit of time is a
tick
. - Each iteration of the simulation is one tick. During a tick, the following occurs:
- subjects can move to new locatioccns
- subjects can contact other subjects
- attributes of the subjects can change
- Many of the fundamental properties of a virus are measured in days. RKnot translates daily inputs into ticks.
- Currently, only one
tick
per day is supported. The goal is to support any number of ticks during a day.
- The fundamental unit of time is a
- Space
- Subjects interact in an two-dimensional environment called the Grid.
- The Grid must be a square. The Grid size can be passed manually or it can be determined automatically for a specified density level.
- Each pair of xy coordinates in the Grid is a location.
- A contact occurs when an infected subject and a susceptible subject occupy the same location at the same tick.
- There is no limit to the number of subjects that can occupy a single location at the same time.
- Subjects move through the Grid according to user-specified
mover
functions. These functions typically incorporate a degree of randomness. - A subject can also move by attending an Event.
- Portions of the Grid may be restricted by Boxes and/or Gates.
- Subjects
- subjects (also referred to as “dots”) are the analog of people in the simulation.
- subjects carry many attributes through the life of the simulation that are updated and changed as required see Dot Matrix).
- Virus
- the user may pass several characteristics fundamental to the simulated virus. RKnot may infer others. Virus characteristics include:
- \(R_0\)
- Duration of Infection
- Transmission Risk
- Duration of Immunity
- Infection Fatality Rate
- the user may pass several characteristics fundamental to the simulated virus. RKnot may infer others. Virus characteristics include:
The Sim¶
The Sim
object is the user interface for the RKnot simulation package and acts as a thin wrapper for the Server and Worker classes of Ray actors that form the core of a simulation.
A Sim object is instantiated with pre-defined characteristics of the space, the subjects, and the virus.
For demonstration purposes, a quick default simulation can be run by simply providing a few parameters.
from rknot import Sim, Chart
params = {'square': 4, 'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365}
group = {'n': 2, 'n_inf': 1}
sim = Sim(groups=group, **params)
run
is the main method of the Sim
object. run
iterates through each tick
in the simulation. Currently, one day == one tick.
sim.run()
sim.run()
does not return any values, but it does update various attributes of the sim
object. After calling run
, you can then pass sim
to the Chart
object, which will generate an animation of the simulation across time.
chart = Chart(sim, dotsize=2000, interval=200, show_intro=False, use_init_func=False)
chart.to_html5_video()
The animation is split in 3 sections: * Interactions * the visual representation of subjects in the Grid. Each marker is a subject and each cross-section of gridlines is a point (for larger grids the lines are removed). * Details * provides several on-the-run statistics including Effective Reproduction Number, total fatalities, and fatalities by group. * Infections * shows the change in infection level over each day, showing both current infection level and total penetration (“Ever” in the legend)
The animation is built on AxesSubPlot components that can be arranged in any fashion desired, including a handful of preset layouts. see Chart for more details.
As per the animation above, the default simulation is of a single infected subject, moving across a 4x4 two-dimensional space according to the equal
mover function. The subject is equally likely to move to any location in the Grid on any tick.
Subjects¶
Dots¶
Dots are subjects/people in the simulation space. A subject has many attributes that are adjusted over time, including:
- which Group it belongs to
- if it is alive
- if it is infected
- if it is susceptible
- its location
- any restricted areas that apply to it (see Boxes and Gates)
- if infected, when it will recover (or when it will succumb)
- if recovered, when it will again become susceptible
- its fatality rate
- its
mover
function
see Dot Matrix for a more fulsome discussion.
Groups¶
Dots are the fundamental subjects of the simulation, but dots can only be created via a Group object.
To create our group objects, we can pass a list of dictionaries to the groups
parameter of Sim. The dictionaries correspond to the attributes of the group, which in turn correspond to the attributes of its constituent dots at initiation.
To create a group, you need only provide two parameters:
n
, population size of the group at initiationn_inf
, number of infected subjects in the group at initiation
If only one group is being provided, you can pass a dictionary. With multiple groups, pass an iterable of dicts.
params = {'square': 10, 'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365}
group = dict(n=2, n_inf=1)
sim = Sim(groups=group, **params)
sim.run()
chart = Chart(
sim, figsize=(16,8), dotsize=2000, interval=200,
layout='dots_only', show_intro=False
)
chart.to_html5_video()
Below we see this structure creates a 10x10 Grid with two subjects, only one of which is infected at the outset.
There are many other parameters and customizations that can provided:
name
- if not provided, Sim will create one
box
&box_is_gate
- see Boxes and Gates
mover
- function used to dictate a dot’s movement (see Mover Functions)
tmf
- *transmission factor*, \(T\), applied to each of the dots interactions.
- default: 1
susf
- susceptiblity factor; the fraction of subjects in a group that will be made susceptible to the virus at initiation
- the inverse of
susf
(\(1/{susf}\)) is the number of subjects in a group that already have immunity.
ifr
- infection fatality rate; or the likelihood that an infection will be fatal
These can again be passed as a dictionary of a single group:
group = dict(
name='main', n=2, n_inf=1, mover='equal',
tmf=1.25, susf=.75, ifr=0.005
)
sim = Sim(groups=group, **params)
sim.run()
chart = Chart(
sim, figsize=(16,8), dotsize=2000, interval=200,
layout='dots_only', show_intro=False
)
chart.to_html5_video()
Or as an iterable of dictionaries. Each group is assigned a unique marker in the animation.
group1 = dict(name='1', n=1, n_inf=1, mover='local', tmf=1.25, susf=.75, ifr=0.005)
group2 = dict(name='2', n=1, n_inf=0, mover='equal', tmf=0.75, susf=0.95, ifr=0.05)
group3 = dict(name='3', n=1, n_inf=0, mover='equal', tmf=0.25, susf=0.5, ifr=0.4)
groups = [group1, group2, group3]
sim = Sim(groups=groups, **params)
sim.run()
chart = Chart(
sim, figsize=(16,8), dotsize=2000, interval=200,
layout='dots_only', show_intro=False
)
chart.to_html5_video()
The Grid¶
All interactions in an RKnot simulation take place inside the Grid. The grid is a Grid
object, which in turn is a sub-classed numpy array with some additional features.
The Grid size can be determined by passing the square
or density
parameters. Each density
accepts either a str
or a float
. The float
value is a specific desired subject per location and a str
must be on of the three categories below.
Available str
values for density
and their corresponding densities are:
low: 0.2
med: 1
high: 10
If we set density=med
, the Grid will be set such that the density is 1 subject per location. For a group of 100 subjects, that will result in a 10x10 grid. We can see these attributes by passing details=True
.
[9]:
params = {'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365}
group = dict(name='main', n=100, n_inf=1)
sim = Sim(groups=group, density='med', details=True, **params)
---------------------------------------------------------------------------------
| SIM DETAILS |
|-------------------------------------------------------------------------------|
| Boundary| [ 1 10 1 10]| Locations| 100|
|-------------------|-------------------|-------------------|-------------------|
| Population| 100| Density| 1.0|
|-------------------|-------------------|-------------------|-------------------|
| Contact Rate| 1.01| | |
|-------------------|-------------------|-------------------|-------------------|
sim.run()
chart = Chart(
sim, figsize=(16,8), layout='dots_only',
show_intro=False, use_init_func=False
)
chart.to_html5_video()
For smaller populations, the density level can only be approximated. RKnot defaults to rounding up to the nearest square value.
You can also pass a float value to density
in order to create a specified density. Here, we set density=3.5
[13]:
group1 = dict(name='1', n=1000, n_inf=1)
group2 = dict(name='2', n=20, n_inf=20)
groups = [group1, group2]
sim = Sim(groups=groups, density=3.5, details=True, **params)
---------------------------------------------------------------------------------
| SIM DETAILS |
|-------------------------------------------------------------------------------|
| Boundary| [ 1 18 1 18]| Locations| 324|
|-------------------|-------------------|-------------------|-------------------|
| Population| 1,020| Density| 3.15|
|-------------------|-------------------|-------------------|-------------------|
| Contact Rate| 3.16| | |
|-------------------|-------------------|-------------------|-------------------|
sim.run()
chart = Chart(sim, figsize=(16,8), layout='dots_only', show_intro=False)
chart.to_html5_video()
Mover Functions¶
When a subject changes locations, this is called a ‘move’. A move is completed during a tick and the movement of a subject on any tick is governed by its mover function. Movers select a location according to a pre-defined probability distribution, so the general movement pattern of a dot can be pre-determined, but any one movement occurs randomly.
There are currently 5 mover functions. Their respective definitions, along with examples of their movement are provided below. A float
value is also accepted which is used as the p-value
in a geometric movement pattern.
Equal
The subject is equally likely to move to any location.
Local
The subject has a strong bias towards dots only in its immediate vicinity.
Traveller
The subject commonly moves to locations far across the Grid.
Quarantine
The subject has a strong bias towards not moving, with only some movement occuring.
Social
The subject moves mostly within its vicinty but also to other more medium distance locations.
In addition to specifying a mover function, the user can also simply specify a float
value between 0 and 1.
This value corresponds to a p-value
used in a geometric distrubtion. The relationship between p-value
and movement is shown below.
The higher the p-value
, the greater the bias towards shorter moves. Increasing p-value
, all else equal, should decrease the number of contacts in a sim. This is explored further here.
Below we compare the movement patterns of two subjects with very different p-value
.
params = {'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365}
group1 = dict(n=1, n_inf=1, mover=.25)
group2 = dict(n=1, n_inf=1, mover=.95)
groups = [group1, group2]
sim = Sim(groups=groups, square=10, **params)
sim.run()
chart = Chart(sim, figsize=(16,8), layout='dots_only', show_intro=False)
chart.to_html5_video()
Boxes and Gates¶
The movement of a subject across the Grid can be restricted by two concepts known as Boxes and Gates. These concepts are designed to mimick certain funcitonal or perceived boundaries between groups, such as international borders or closed-access communities like assisted-living facilities.
The distinction between boxes and gates is simple:
- Subjects cannot exit Boxes
- Subjects cannot enter Gates
Boxes¶
A box is a \(m*n\) subset of locations within the Grid that a subject(s) cannot leave.
The locations are specified by passing a four element iterable that specifies the coordinates of the “four corners” of the box according to [\(x_0\), \(x_1\), \(y_0\), \(y_1\)]
So passing:
box = [2,6,3,9]
creates a box with the four corners:
(2,3) (2,9) (6,3) (6,9)
Currently, a box can only be specified by
- passing the
box
parameter as group keyword - by passing a
`vbox
<#VBoxes>`__.
Every dot in the group can only move within the box
, regardless of the size of the Grid.
group1 = dict(name='1', n=2, n_inf=1, box=[1,3,2,4])
sim = Sim(groups=group1, square=10, **params)
sim.run()
chart = Chart(
sim, figsize=(16,8), dotsize=2000,
layout='dots_only', show_intro=False, use_init_func=False
)
chart.to_html5_video()
A group can only have one box and each group can have its own box.
group1 = dict(name='1', n=2, n_inf=1, box=[1,3,2,4])
group2 = dict(name='2', n=2, n_inf=0, box=[6,9,6,10])
groups = [group1, group2]
sim = Sim(groups=groups, square=10, **params)
sim.run()
chart = Chart(
sim, figsize=(16,8), dotsize=2000,
layout='dots_only', show_intro=False, use_init_func=False
)
chart.to_html5_video()
Remember that a box only restricts the subjects in that group from leaving the space. Other dots not assigned to that box can move into the space without restriction.
group1 = dict(name='1', n=5, n_inf=1, box=[1,3,1,3])
group2 = dict(name='2', n=5, n_inf=0)
groups = [group1, group2]
sim = Sim(groups=groups, square=10, **params)
sim.run()
chart = Chart(sim, figsize=(16,8), dotsize=2000, layout='dots_only', show_intro=False)
chart.to_html5_video()
Gates¶
Gates are the inverse of boxes. A gate is an area that subjects cannot enter.
Gates are a Gate object, which are a subclass of the Box class (in turn a subclass of ndarray), and they are created via the same 4 element iterable. For now, a gate can only be created by passing the box_is_gated=True
flag as a keyword in a group dictionary, or by specifying a vbox
.
Using the previous example, we can see that group2
dots can no longer enter the group1
box.
group1 = dict(name='1', n=5, n_inf=1, box=[1,3,1,3], box_is_gated=True)
group2 = dict(name='2', n=5, n_inf=0)
groups = [group1, group2]
sim = Sim(groups=groups, square=10, **params)
sim.run()
chart = Chart(
sim, figsize=(16,8), dotsize=2000, layout='dots_only', show_intro=False,
use_init_func=False
)
chart.to_html5_video()
This structure allows for intricate movement patterns. We show isolated groups below.
We will also provide the show_restricted=True
flag, which will outline the boxes and gates for us. It will also the label the restricted area with the name of the group used to form the it.
group1 = dict(name='1', n=50, n_inf=5, box=[1,5,1,20], box_is_gated=True)
group2 = dict(name='2', n=50, n_inf=5, box=[6,25,3,10], box_is_gated=True)
group3 = dict(name='3', n=50, n_inf=5, box=[10,21,16,22], box_is_gated=True)
group4 = dict(name='4', n=50, n_inf=5, box=[2,15,23,25], box_is_gated=True)
groups = [group1, group2, group3, group4]
sim = Sim(groups=groups, square=25, **params)
sim.run()
chart = Chart(sim,
figsize=(16,8), layout='dots_only', show_intro=False, use_init_func=False,
show_restricted=True,
)
chart.to_html5_video()
And here some isolated and some free moving.
group1 = dict(name='1', n=50, n_inf=5, box=[1,5,1,5], box_is_gated=True)
group2 = dict(name='2', n=50, n_inf=5, box=[14,19,14,19], box_is_gated=True)
group3 = dict(name='4', n=10, n_inf=5)
group4 = dict(name='4', n=10, n_inf=5)
groups = [group1, group2, group3, group4]
sim = Sim(groups=groups, square=25, **params)
sim.run()
chart = Chart(
sim, figsize=(16,8), layout='dots_only',
show_intro=False, use_init_func=False
)
chart.to_html5_video()
VBoxes¶
A VBox is a vacant area of the Grid, meaning there are no subjects inside the box at the initiation of the Sim and that no subjects can enter the VBox except via Travel events.
VBoxes can be used to customize contact patterns, as done in the Dynamic Transmission Risk simulations. They can also be used to mimick areas that people typically only visit, rather than reside in, such as hospitals, sports arenas, office buildings, etc.
VBoxes are simply box objects and can be created by passing the vboxes
parameter to Sim
. VBoxes are always setup with a corresponding gate.
*IMPORANT*: A VBox is \(\underline{\text{not}}\) included in the density calculation of the grid size.
If we pass an integer, Sim
will create a VBox with the value corresponding to the number of locations in the VBox. The VBox will be placed in the top-left corner of the Grid.
from rknot import Sim
vbox = 4
groups = [
dict(n=10, n_inf=1, mover='social'),
dict(n=10, n_inf=1, mover='local')
]
params = {'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365, 'vboxes': vbox}
sim = Sim(groups=groups, **params)
sim.run(dotlog=True)
chart = Chart(
sim, figsize=(16,8), layout='dots_only',
show_intro=False, use_init_func=False
)
chart.to_html5_video()
We can also pass a boundary as a 4 item iterable.
from rknot import Sim
vbox = [1,3,1,3]
groups = [
dict(n=10, n_inf=1, mover='social'),
dict(n=10, n_inf=1, mover='local')
]
params = {'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365, 'vboxes': vbox}
sim = Sim(groups=groups, **params)
sim.run(dotlog=True)
chart = Chart(
sim, figsize=(16,8), layout='dots_only',
show_intro=False, use_init_func=False
)
chart.to_html5_video()
We can pass a dictionary and include the label
keyword to indicate a name for the VBox.
We’ve included a couple Travel
objects to show how the VBox can be accessed. Simply assign the index of the vbox
as a parameter to Travel
and the Sim
will determine the location automatically.
from rknot import Sim
from rknot.events import Travel
vbox = {'box': [1,3,1,3], 'label': 'Hospital'}
groups = [
dict(n=10, n_inf=1, mover='social'),
dict(n=10, n_inf=1, mover='local')
]
params = {'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365, 'vboxes': vbox}
events = [
Travel(
name='vbox_event', start_tick=3, recurring=3,
groups=[0,1], capacity=1, vbox=0,
)
]
sim = Sim(groups=groups, events=events, **params)
sim.run(dotlog=True)
chart = Chart(
sim, figsize=(16,8), layout='dots_only',
show_intro=False, use_init_func=False
)
chart.to_html5_video()
Finally, we can pass multiple VBoxes as a list of dictionaries.
from rknot import Sim
vboxes = [
{'box': [1,3,1,3], 'label': 'Hospital'},
{'box': [5,7,1,3], 'label': 'Arena'}
]
groups = [
dict(n=20, n_inf=1, mover='social'),
dict(n=20, n_inf=1, mover='local')
]
params = {'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365, 'vboxes': vbox}
sim = Sim(groups=groups, **params)
sim.run(dotlog=True)
chart = Chart(
sim, figsize=(16,8), layout='dots_only',
show_intro=False, use_init_func=False
)
chart.to_html5_video()
Events¶
Events impact the attributes of subjects over the course of the simulation.
Events are utilized to better simulate real-world behavior.
For instance, people do not move in consistent, prescribed ways. They move in regular ways most of the time with contacts that are well defined, but sometimes they attend events (perhaps periodically or uniquely) that are not governed by their regular movement patterns.
Event¶
An Event
is an event that occurs at a particular location.
An Event
accepts the following parameters:
xy
, the xy coordinates of the locationstart_tick
, the tick when the event beginsgroups
, an iterable of group ids that are eligible for the eventcapacity
, the number of subjects that should attendrecurring
, how often the event recurs (i.e. every nth tick); if set to 0, the event does not recur
When an Event
concludes, the subject returns to its home
location as specified in the dot matrix.
To schedule an event, you must pass a list of event objects to the events
parameter.
To begin with, we’ll create a single Event
object, called show
, that occurs once on day 5.
from rknot.events import Event
params = {'square': 10, 'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365}
group1 = dict(name='1', n=10, n_inf=5)
show = Event(name='show', xy=(5,5), start_tick=5, groups=[0], capacity=10)
events = [show]
sim = Sim(groups=group1, events=events, **params)
sim.run(dotlog=True)
chart = Chart(sim, figsize=(16,8), layout='dots_only', show_intro=False)
chart.to_html5_video()
If you watch closely, you’ll see on Day 5 that all the dots seemingly disappear, save for one, at location (5,5).
In fact, all 10 dots are actually at that location at the same time.
We can confirm this by inspecting the Dot Matrix on that day via the dotlog
attribute.
[22]:
from rknot.dots import MATRIX_COL_LABELS as ML
sim.dotlog[4][:, ML['x']:ML['y']+1]
[22]:
array([[5, 5],
[5, 5],
[5, 5],
[5, 5],
[5, 5],
[5, 5],
[5, 5],
[5, 5],
[5, 5],
[5, 5]])
We can see the event more clearly if we extend the duration to 10 days. We also significantly reduced the frame rate.
show = Event(name='show', xy=(5,5), start_tick=5, groups=[0], capacity=10, duration=10)
events = [show]
sim = Sim(groups=group1, events=events, **params)
sim.run()
chart = Chart(
sim, figsize=(16,8), dotsize=1000, interval=300,
layout='dots_only', show_intro=False, use_init_func=False
)
chart.to_html5.video()
Many event objects can be specified at once, in various combinations of groups.
group1 = dict(name='1', n=10, n_inf=5)
group2 = dict(name='2', n=10, n_inf=0)
show = Event(name='show', xy=(5,5), start_tick=5, groups=[0,1], capacity=5, recurring=30)
game = Event(name='game', xy=(1,1), start_tick=5, groups=[0], capacity=5, recurring=14)
church = Event(name='church', xy=(1,1), start_tick=5, groups=[1], capacity=10, recurring=7)
groups = [group1, group2]
events = [show, game, church]
sim = Sim(groups=groups, events=events, **params)
sim.run()
chart = Chart(
sim, interval=300, dotsize=1000, layout='dots_only', show_intro=False,
use_init_func=False
)
chart.to_html5_video()
Travel¶
Travel is a special type of event that allows a subject to enter a gate.
When a dot enters a gate via a Travel object, its box and gate attributes are temporarily adjusted to match those of the groups within the gate. The attributes revert when the event ends (determined by duration
parameter).
Once inside the gate, the dot(s) are free to interact with other dots normally.
from rknot.events import Travel
params = {'square': 10, 'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365}
group1 = dict(name='1', n=1, n_inf=1, box=[1,5,1,5], box_is_gated=True)
group2 = dict(name='2', n=10, n_inf=0, box=[6,10,6,10], box_is_gated=True)
visit = Travel(
name='visit', xy=[1,1], start_tick=3, groups=[1], capacity=1, duration=5, recurring=10
)
groups = [group1, group2]
events = [visit]
sim = Sim(groups=groups, events=events, **params)
sim.run()
chart = Chart(
sim, interval=200, dotsize=2000, layout='dots_only', show_intro=False
use_init_func=False
)
chartto_html5_video()
Many unique layouturations can be achieved with this structure. Below, the group1
box will be vacated by the solitary group1
dot (essentially switching places with a dot from group2
).
from rknot.events import Travel
params = {'square': 10, 'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365}
group1 = dict(name='1', n=1, n_inf=1, box=[1,5,1,5], box_is_gated=True)
group2 = dict(name='2', n=10, n_inf=0, box=[6,10,6,10], box_is_gated=True)
visit2 = Travel(
name='visit2', xy=[9,9], start_tick=3, groups=[0], capacity=1, duration=5, recurring=10
)
visit1 = Travel(
name='visit1', xy=[1,1], start_tick=3, groups=[1], capacity=1, duration=5, recurring=10
)
groups = [group1, group2]
events = [visit2, visit1]
sim = Sim(groups=groups, events=events, **params)
sim.run()
chart = Chart(
sim, interval=200, dotsize=2000, layout='dots_only', show_intro=False,
use_init_func=False
)
chart.to_html5_video()
Quarantine¶
A quarantine is an event object that makes several changes to a dots state in order to restrict its movement.
When a dot is quarantined,
- it goes back to its home location (see Dot Matrix)
- its boxes and gates are reset to match its group
- its mover function is changed to ‘quarantine’
In addition, a Quarantine object will create a additional restriction objects that disallow events during the quarantine (see Restrictions below)
from rknot.events import Quarantine
params = {'square': 10, 'R0': 2.5, 'days': 50, 'imndur': 365, 'infdur': 365}
group1 = dict(name='1', n=2, n_inf=1, box=[1,5,1,5], box_is_gated=True)
group2 = dict(name='2', n=2, n_inf=0, box=[6,10,6,10], box_is_gated=True)
quar = Quarantine(name='all', start_tick=5, groups=[0,1], duration=30)
groups = [group1, group2]
events = [quar]
sim = Sim(groups=groups, events=events, **params)
sim.run()
chart = Chart(
sim, interval=200, dotsize=2000, layout='dots_only', show_intro=False,
use_init_func=False
)
chart.to_html5_video()
We can see from above that once in quaratine, the subjects barely move. We can include events in our structure. The events will be restricted during the quarantine period, then will resume when the quarantine ends.
params = {'square': 10, 'R0': 2.5, 'days': 100, 'imndur': 365, 'infdur': 365}
group1 = dict(name='1', n=1, n_inf=1, box=[1,5,1,5], box_is_gated=True)
group2 = dict(name='2', n=10, n_inf=0, box=[6,10,6,10], box_is_gated=True)
show = Event(name='show', xy=(6,6), start_tick=5, groups=[1], capacity=5, recurring=30)
visit2 = Travel(
name='visit2', xy=[9,9], start_tick=3, groups=[0], capacity=1, duration=5, recurring=10
)
visit1 = Travel(
name='visit1', xy=[1,1], start_tick=3, groups=[1], capacity=1, duration=5, recurring=10
)
groups = [group1, group2]
events = [show, visit2, visit1, quar]
sim = Sim(groups=groups, events=events, **params)
sim.run()
chart = Chart(
sim, interval=200, dotsize=2000, layout='dots_only', show_intro=False,
use_init_func=False,
)
chart.to_html5_video()
Social Distancing¶
Event to mimick social distancing practices.
Social distancing is assumed to impact the Transmission Factor, \(\tau\) of each dot. The core hypothesis is that practices such as maintaining 6-feet of distance or mask wearing don’t reduce the number of contacts, but do reduce the likelihood that a contact will result in a new infection (ceterus paribus).
You can see use of this object here.
Vaccination¶
TBD
Restrictions¶
A restriction object restricts attendance to events that fall within the specified criteria. Each event has a restricted
attribute that defaults to False
. A restriction object filters out events from the event schedule by setting restricted=True
for each event that satisfies the criteria.
To clarify, a Restriction is not an event. Events act on dots. Restrictions act on events.
Restrictions have potential as a versatile tool that can be used to investigate the impact of various government and business policy decisions that impact spread.
The Restriction object has a criteria
parameter that accepts a dict
, with keywords related to event object attributes. Acceptable criteria
keys are currently:
The simplest way to restrict an event is by its name:
from rknot.events import Restriction
params = {'square': 10, 'R0': 2.5, 'days': 100, 'imndur': 365, 'infdur': 365}
group1 = dict(name='1', n=10, n_inf=1, box=[1,5,1,5], box_is_gated=True)
group2 = dict(name='2', n=10, n_inf=0, box=[6,10,6,10], box_is_gated=True)
show1 = Event(name='show1', xy=(1,1), start_tick=2, groups=[0], capacity=10, recurring=2)
show2 = Event(name='show2', xy=(10,10), start_tick=2, groups=[1], capacity=10, recurring=2)
criteria = {'name': 'show1'}
res1 = Restriction(name='no_show1', start_tick=10, duration=20, criteria=criteria)
groups = [group1, group2]
events = [show1, show2, res1]
sim = Sim(groups=groups, events=events, **params)
sim.run(dotlog=True)
chart = Chart(
sim, interval=300, dotsize=2000, layout='dots_only', show_intro=False,
use_init_func=False,
)
chart.to_html5_video()
In the above, we can see that every other day both group boxes have events that are attended by all dots in the group.
But on day 10, the group1
dots no longer converge on location (1,1). Instead, they are spread throughout their box. So res1
has successfully restricted attendance to show1
.
Unlike Quarantine
objects, however, the group1
dots have not changed their standard movement patterns.
We can restrict multiple events via the other criteria. Next we will restrict events based on their capacity. Events with more than 5 subjects in attendance will be restricted.
criteria = {'capacity': 5}
res1 = Restriction(name='cap5', start_tick=10, duration=20, criteria=criteria)
groups = [group1, group2]
events = [show1, show2, res1]
sim = Sim(groups=groups, events=events, **params)
sim.run()
chart = Chart(
sim, interval=300, dotsize=2000, layout='dots_only', show_intro=False,
use_init_func=False,
)
chart.to_html5_video()
In the above we see that neither of the groups had events from day 10 onward during the restriction period.
Restrictions can be chained together as desired to form a complex and tailored policy recipe for the population of the sim. See this example.
Dot Matrix¶
numpy
array of shape (n, 23) with each of the n rows representing a dot and each column representing an attribute.More typical Python objects have been eschewed in favor the Dot Matrix because:
- RKnot relies heavily on Ray for parallel processing and Numba for just-in-time compilation and vectorization to improve processing speed.
Numpy
arrays have several advantages in Ray including rapid serialization and ease of batching.- Numba also integrates well with
numpy
, supporting many of its features and leads to major performance improvements.
The dot matrix is created inside a Ray actor at instantiation and is only passed back to the main Sim
object when the simulation is completed.
It can be accessed via the dots
attribute. Below is a sample of 4 dots:
[33]:
sim.dots[:4]
[33]:
array([[ 0, 0, 1, 0, 0, 1, 1, 65, 7, 6, 54, 6, 5,
0, 0, -1, 0, 100, 650, 0, -1, 365, 730],
[ 1, 0, 1, 0, 0, 1, 1, 77, 8, 8, 22, 3, 3,
0, 0, -1, 0, 100, 650, 0, -1, 365, 730],
[ 2, 0, 1, 0, 0, 1, 1, 11, 2, 2, 88, 9, 9,
0, 0, -1, 0, 100, 650, 0, -1, 365, 730],
[ 3, 0, 1, 0, 0, 1, 1, 27, 3, 8, 96, 10, 7,
0, 0, -1, 0, 100, 650, 42, -1, 407, 772]])
The column attributes have corresponding labels:
[11]:
from rknot.dots import MATRIX_LABELS
print (MATRIX_LABELS)
['id', 'group_id', 'is_alive', 'is_vaxxed', 'is_sus', 'is_inf', 'ever_inf', 'loc_id', 'x', 'y', 'home_id', 'homex', 'homey', 'go_home', 'box_id', 'event_id', 'mover', 'mover_p', 'tmf', 'ifr', 'inf_tick', 'depart', 'recover', 'relapse']
With these labels, the 4 dot matrix above can be shown in a table.
id | group_id | is_alive | is_vaxxed | is_sus | is_inf | ever_inf | loc_id | x | y | home_id | homex | homey | go_home | box_id | event_id | mover | mover_p | tmf | ifr | inf_tick | depart | recover | relapse |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | 0 | 1 | 0 | 0 | 46 | 6 | 7 | 36 | 5 | 5 | 0 | 0 | -1 | 4 | -999 | 100 | 0 | -1 | -1 | -1 | -1 |
1 | 0 | 1 | 0 | 1 | 0 | 0 | 61 | 8 | 6 | 27 | 4 | 4 | 0 | 0 | -1 | 4 | -999 | 100 | 0 | -1 | -1 | -1 | -1 |
2 | 0 | 1 | 0 | 1 | 0 | 0 | 52 | 7 | 5 | 58 | 8 | 3 | 0 | 0 | -1 | 4 | -999 | 100 | 0 | -1 | -1 | -1 | -1 |
3 | 0 | 1 | 0 | 1 | 0 | 0 | 20 | 3 | 5 | 3 | 1 | 4 | 0 | 0 | -1 | 4 | -999 | 100 | 0 | -1 | -1 | -1 | -1 |
4 | 0 | 1 | 0 | 1 | 0 | 0 | 15 | 2 | 8 | 24 | 4 | 1 | 0 | 0 | -1 | 4 | -999 | 100 | 0 | -1 | -1 | -1 | -1 |
There are several data types at work:
- categorical integers; used to identify related objects
id
,group_id
,loc_id
,home_id
,box_id
,event_id
,mover
- boolean integers; used to set boolean flags
0
meansFalse
and1
meansTrue
is_alive
,is_vaxxed
,is_sus
,is_inf
,ever_inf
,go_home
- coordinates; used to identify locations
x
,y
,homex
,homey
- event ticks; integers that trigger an event on the given tick
depart
,recover
,relapse
- factors; scaled integers that must be unscaled before being used in multiplicative formulas
tmf
,ifr
The column attributes are defined as follows:
Label | Definition | Label | Definition |
---|---|---|---|
id | the subject’s unique identifier | homey | y coord of the subject’s home location |
group_id | the unique identifier of the subject’s group | go_home | is the subject going home on the next move? |
is_alive | Is the subject alive? | box_id | id of the box the subject belongs to |
is_vaxxed | Has the subject been vaccinated? | event_id | id of the event the subject is attending |
is_sus | Is the subject susceptible to infection? | mover | id of the subject’s mover function |
is_inf | Is the subject infected? | mover_p | p-value of custom mover function |
ever_inf | Has the subject ever been infected? | tmf | the subject’s transmission factor |
loc_id | id of the subject’s current location | ifr | the subject’s infection fatality rate |
x | x coord of the subject’s current location | inf_tick | the tick a subject is infected |
y | y coord of the subject’s curretn location | depart | the tick an infected subject will depart |
home_id | id of the subject’s home location | recover | the tick an infected subject will no longer be infected or susceptible |
homex | x coord of the subject’s home location | relapse | the tick a recovered subject will again become susceptible |