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.
  • 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 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 initiation
  • n_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
  • mover
  • tmf
  • 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.

Drawing

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)
and a total of 35 locations.

Currently, a box can only be specified by

  1. passing the box parameter as group keyword
  2. 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 location
  • start_tick, the tick when the event begins
  • groups, an iterable of group ids that are eligible for the event
  • capacity, the number of subjects that should attend
  • recurring, 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,

  1. it goes back to its home location (see Dot Matrix)
  2. its boxes and gates are reset to match its group
  3. 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:

capacity name ticks groups loc_id

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

The dot matrix is essentially RKnot’s canonical form of data structure. The matrix is simply a 2D 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 means False and 1 means True
    • 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