-
Notifications
You must be signed in to change notification settings - Fork 218
TTI Simulation Loop
The SEIRS+ model frameworks offer multiple methods for modeling testing, tracing, and isolation (TTI) scenarios. It is straightforward to approximate simple intervention scenarios using the quarantine states and rate-based testing and tracing that are built into the SEIRS+ compartment models (see SEIRS Network Model Demo for an example). This is sufficient for some explorations, but other modeling efforts may seek more control over the TTI protocol that is to be simulated. More precise and/or elaborate TTI protocols can be implemented by interfacing with model attributes and helper functions that are accessible to custom simulation loop scripts. Practically any TTI protocol can be modeled in this way, as the user has complete control over the implementation of TTI logic within their simulation loop.
Here we document an example of a simulation loop that implements a large number of sophisticated TTI interventions, including options for testing for individual health, random testing, degree-based testing, test turn around time, contact tracing, and isolation of individuals and groups. This simulation loop is flexible and can be used out of the box for a wide range of TTI protocols simply by providing the parameters for your use case of interest. Beyond this, hopefully this simulation loop can serve as an example for how the models can be used in this way.
The simulation loop documented here is implemented by the run_tti_sim()
function in the sim_loops.py
module. The [run_tti_sim()
function arguments](#the-run-tti-sim` function) that parameterize this simulation loop are documented below. This simulation was designed to be used with the Extended SEIRS Network Model, but analagous simulation loops can be used with the Basic SEIRS Network Model as well.
For examples of this simulation loop being used in analyses, refer to the Community TTI and Workplace TTI examples.
The following outline summarizes the TTI protocol that is implemented by this simulation loop. Each component of the protocol is discussed in more detail below.
All of the intervention features that are listed and discussed below are optional, and each can be turned on/off according to the arguments passed to the run_tti_sim()
function call.
-
On each simulation day:
- Introduce exogenous exposures randomly according to a poisson process
- Some fraction of symptomatic individuals self-isolate (without a test)
- Some fraction of groupmates of self-isolating symptomatic individuals may also self-isolate (without a test)
- Some fraction of traced contacts of positive cases self-isolate (without a test)
- Some fraction of groupmates of self-isolating traced individuals may also self-isolate (without a test)
- Administer a fixed allotment of tests:
- A portion of the day's tests are used on some fraction of symptomatic individuals that seek a test on their own
-
On designated testing cadence days:
- A designated portion of the day's tests are used on individuals that have been identified by contact tracing
- The remainder of the day's tests are used on randomly selected individuals from the population
- The aforementioned tests are conducted with sensitivities that depend on each individual's disease state and how long they've been in that state.
- For each individual that tests positive:
- Isolate the positive individual, if compliant (after a designated turnaround time)
- Isolate the positive individual's groupmates, if compliant (after a designated turnaround time)
- Add the positive individual's contacts to the contact tracing queue, if compliant (to be tested after a designated tracing lag time)
The TTI protocol implemented in this simulation loop is meant to be executed on a daily basis. The time unit for the SEIRS+ models is conventionally understood as days, so this simulation loop executes TTI interventions at integer times (t=1.0, 2.0, 3.0, ...
). Rather than calling the ExtSEIRSNetworkModel
run()
function, which runs a simulation for the entire specified duration without interruption, this simulation loop uses the run_iteration()
function, which advances the simulation by a single update (i.e., individual state transition). Model updates typically advance time at decimal intervals that are much shorter than a day. This simulation loop calls the run_iteration()
function repeatedly to update the model dynamics until the next integer time is reached. When an integer time is reached, the exogenous introduction, testing, tracing, and isolation interventions are executed.
It may be of interest to consider the effect of ongoing introductions of the disease from outside the population of interest. For example an employee may become infected at home before and bring the disease into their workplace, or someone may interact with an infectious tourist from another city thereby introducing a new transmission chain into their community. Here, new cases are added to the population according to a random poisson process.
numNewExposures = numpy.random.poisson(lam=average_introductions_per_day)
model.introduce_exposures(num_new_exposures=numNewExposures)
The simulation loop argument average_introductions_per_day
sets the mean of the sampled poisson distribution and thus the expected rate of introductions. The ExtSEIRSNetworkModel
class function introduce_exposures()
selects the given number of individuals randomly from the population and updates each of their states to Exposed (E) if the individual is currently susceptible.
See the rest of the relevant code in run_tti_sim()
.
It may be reasonable to assume that not all individuals in the population will comply with certain interventions. The compliance of each individual for a given intervention is specified by passing a list of True/False
values to the corresponding compliance argument of the run_tti_sim()
function. The run_tti_sim()
has such a compliance argument for each of the interventions discussed here (see Simulation Loop Arguments for details). An individual that is assigned True
in a given compliance argument list is said to be "compliant" and will participate in that intervention at all relevant times throughout the simulation; an individual that is assigned False
is "non-compliant" and will never participate in that intervention. The compliance lists can be generated by any user-defined logic in the script that calls run_tti_sim()
. The default value for all compliance arguments is None
, which turns off the corresponding intervention, so only interventions that have a compliance list provided will be executed.
In this TTI protocol, a set number of tests are available to be used each day. The total number of tests available each day is specified by the pct_tested_per_day
argument to run_tti_sim()
, which defines the overall test allotment in terms of a percentage of the population size. For example, if the population size is N = 10000 and pct_tested_per_day = 0.1
, then a total of 1000 tests will be available each day.
This total test allotment is split between testing self-reporting symptomatic individuals, individuals identified by contact tracing, and randomly selected individuals (each described in more detail below). The fraction of the total tests that are used for testing symptomatic and/or traced individuals can be capped using the max_pct_tests_for_symptomatics
and max_pct_tests_for_traces
arguments, respectively (these arguments have default values of 1.0
, which allows up to the entire daily test allotment to be used for each of these purposes).
Each testing day, pools of symptomatic, traced, and randomly selected individuals are formed as described below. Individuals from the self-reporting symptomatic pool are given highest priority for receiving tests. If tests remain after testing these symptomatic individuals, individuals from the traced pool have the next highest priority for receiving tests. All tests still remaining are then used for random testing.
Testing of self-reporting symptomatic individuals is assumed to happen every simulation day. Testing of traced individuals and random testing take place only on designated proactive testing days. These proactive testing days are defined by a "testing cadence."
The simulation loops internally tracks a repeating 28-day cycle. A testing cadence is specified by a list of the day numbers within this cycle on which proactive testing is to occur. Several common cadences are pre-defined in the cadence_testing_days
dictionary inside the run_tti_sim()
function:
cadence_testing_days = {
'everyday': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27],
'workday': [0, 1, 2, 3, 4, 7, 8, 9, 10, 11, 14, 15, 16, 17, 18, 21, 22, 23, 24, 25],
'semiweekly': [0, 3, 7, 10, 14, 17, 21, 24],
'weekly': [0, 7, 14, 21],
'biweekly': [0, 14],
'monthly': [0]
}
For example, the 'weekly'
cadence specifies that proactive testing will take place every 7 days on the 0th, 7th, 14th, and 21st days of the repeating 28 day cycle. You can define your own cadences by passing a dictionary with the same format as above to the cadence_testing_days
argument of run_tti_sim()
. The testing cadence that will be used is specified by passing the appropriate key (for the pre-defined or user-defined dictionary of cadences) to the testing_cadence
argument of run_tti_sim()
('everyday'
is the default cadence).
Test sensitivity refers to the probability that an individual is truly infected returns a positive test result. In other words, sensitivity is 1 minus the false negative rate. The false negative rate, and thus sensitivity, is specified using the test_falseneg_rate
argument of the run_tti_sim()
function.
Passing a numerical value to the test_falseneg_rate
argument results in that false negative rate being used in all cases.
In general, it is reasonable to assume that test sensitivity varies depending on the disease state (e.g., exposed, pre-symptomatic, symptomatic, asymptomatic) of the individual and the amount of time the individual has spent in a given state. A temporal sensitivity mode can be invoked by passing the string "temporal"
to the test_falseneg_rate
argument. In this mode, the false negative rate is specified by the temporal_falseneg_rates
dictionary, which gives the false negative rate for individuals according to their disease state and time in state. A pre-defined temporal_falseneg_rates
dictionary is defined inside the run_tti_sim()
function:
temporal_falseneg_rates = {
model.E: {0: 1.00, 1: 1.00, 2: 1.00, 3: 1.00},
model.I_pre: {0: 0.25, 1: 0.25, 2: 0.22},
model.I_sym: {0: 0.19, 1: 0.16, 2: 0.16, 3: 0.17, 4: 0.19, 5: 0.22, 6: 0.26, 7: 0.29, 8: 0.34, 9: 0.38, 10: 0.43, 11: 0.48, 12: 0.52, 13: 0.57, 14: 0.62, 15: 0.66, 16: 0.70, 17: 0.76, 18: 0.79, 19: 0.82, 20: 0.85, 21: 0.88, 22: 0.90, 23: 0.92, 24: 0.93, 25: 0.95, 26: 0.96, 27: 0.97, 28: 0.97, 29: 0.98, 30: 0.98, 31: 0.99},
model.I_asym: {0: 0.19, 1: 0.16, 2: 0.16, 3: 0.17, 4: 0.19, 5: 0.22, 6: 0.26, 7: 0.29, 8: 0.34, 9: 0.38, 10: 0.43, 11: 0.48, 12: 0.52, 13: 0.57, 14: 0.62, 15: 0.66, 16: 0.70, 17: 0.76, 18: 0.79, 19: 0.82, 20: 0.85, 21: 0.88, 22: 0.90, 23: 0.92, 24: 0.93, 25: 0.95, 26: 0.96, 27: 0.97, 28: 0.97, 29: 0.98, 30: 0.98, 31: 0.99},
model.Q_E: {0: 1.00, 1: 1.00, 2: 1.00, 3: 1.00},
model.Q_pre: {0: 0.25, 1: 0.25, 2: 0.22},
model.Q_sym: {0: 0.19, 1: 0.16, 2: 0.16, 3: 0.17, 4: 0.19, 5: 0.22, 6: 0.26, 7: 0.29, 8: 0.34, 9: 0.38, 10: 0.43, 11: 0.48, 12: 0.52, 13: 0.57, 14: 0.62, 15: 0.66, 16: 0.70, 17: 0.76, 18: 0.79, 19: 0.82, 20: 0.85, 21: 0.88, 22: 0.90, 23: 0.92, 24: 0.93, 25: 0.95, 26: 0.96, 27: 0.97, 28: 0.97, 29: 0.98, 30: 0.98, 31: 0.99},
model.Q_asym: {0: 0.19, 1: 0.16, 2: 0.16, 3: 0.17, 4: 0.19, 5: 0.22, 6: 0.26, 7: 0.29, 8: 0.34, 9: 0.38, 10: 0.43, 11: 0.48, 12: 0.52, 13: 0.57, 14: 0.62, 15: 0.66, 16: 0.70, 17: 0.76, 18: 0.79, 19: 0.82, 20: 0.85, 21: 0.88, 22: 0.90, 23: 0.92, 24: 0.93, 25: 0.95, 26: 0.96, 27: 0.97, 28: 0.97, 29: 0.98, 30: 0.98, 31: 0.99},
}
You can define your own temporal false negative rates by passing a dictionary with the same format as above to the temporal_falseneg_rates
argument of run_tti_sim()
.
The ExtSEIRSNetworkModel
class includes set_tested()
and set_positive()
convenience functions, which set tested and positive True/False
flags for a given individual. These flags can be useful in tracking which individuals have been previously tested and/or identified as a positive case. As you might expect, this simulation loop sets the tested flag to True
for nodes that receive tests and sets the positive flag to True
for nodes that are deemed positive.
Symptomatic testing refers to individuals seeking a test on their own after experiencing the onset of symptoms (i.e., testing for individual health). Individuals in the symptomatic disease state (Isym) enter the symptomatic testing pool on the first testing day after they enter the symptomatic state, provided they are compliant with symptomatic testing and have not already been tested. Testing of the self-reporting symptomatic pool happens every simulation day regardless of proactive testing cadence. The self-reporting symptomatic pool is given first priority for receiving tests from the total allotment on cadence testing days when contact tracing testing and random testing may also occur.
Contact tracing testing refers to testing individuals that have been identified as contacts of individuals that have previously been identified as positive cases. When an individual is identified as positive, some portion of their network contacts may be placed into a contact tracing queue. See the Contact Tracing section for more information about how contacts are traced and placed into the contact tracing queue. This queue is implemented such that it imposes a time delay between the identification of the primary positive case and the testing of the traced contacts; the number of days constituting this tracing delay is specified by the tracing_lag
argument of run_tti_sim()
. On cadence testing days, the next group of traced individuals is popped off the queue and becomes the tracing testing pool. Individuals are removed from the tracing testing pool if they are non-compliant with tracing testing, have previously tested positive, or are known to be non-infectious (i.e., in the R, QR, H or F disease states). Tracing testing only occurs on designated proactive testing cadence days. The tracing testing pool is given second priority (after the symptomatic testing pool) for receiving tests from the total allotment.
Random testing refers to administering tests to individuals drawn at random from the population. This random testing may represent an explicit random testing program or an approximation of individuals taking advantage of available testing capacity at their own discretion (irrespective of their disease state, as opposed to symptomatic testing). The random testing pool consists of all individuals in the population, excluding individuals that are non-compliant with random testing, have previously tested positive, or are known to be non-infectious (i.e., in the R, QR, H or F disease states). A number of individuals equal to the number of available tests from the total daily allotment (after symptomatic tests and contact tracing tests have been given out) are randomly drawn from the random testing pool and tested.
The figure at right depicts administration of random testing. Infected individuals are depicted by squares, non-infected individuals by circles. Individuals excluded from the testing pool are designated by dashed and unshaded icons. Individuals that are randomly selected for testing are designated by colored icons, with individuals returning positive results being shaded red and negative results green. A true positive case is labeled with '+', and a false negative case is labeled 'FN'.
It may be of interest to explore testing strategies that make use of the population's contact network structure. One network-based testing approach is to preferentially test highly connected individuals. This testing scheme is implemented in this simulation loop with the option to weight individuals in the random testing selection process proportional to a power of their degree. The magnitude of such a degree bias can be specified using the random_testing_degree_bias
argument to run_tti_sim()
. By default the degree bias (the power to which degrees are raised when calculating the random selection probability of individuals) is set to 0, which results in a uniform sampling probability for all individuals in the random testing pool and thus no degree biased selection.
Contact tracing refers to identifying the contacts of positive cases and targeting interventions, such as testing or isolation, to these potentially exposed contacts. In this simulation loop, when an individual tests positive their contacts may be traced. If the primary positive individual is compliant with participating in tracing, then a portion of their close contacts (adjacent nodes in the contact network) are selected for tracing. The portion of contacts that are traced is given by either the num_contacts_to_trace
argument, which specifies a fixed integer number of contacts to trace per primary positive case, or by the pct_contacts_to_trace
argument, which specifies a percentage of of contacts to trace per primary positive case. (By default the num_contacts_to_trace
is set to None, which results in the pct_contacts_to_trace
argument being used, but if a value is passed to num_contacts_to_trace
then this argument overrides pct_contacts_to_trace
.) The contacts constituting the portion of traced contacts are chosen randomly from the primary case's set of close contacts. Some traced contacts may be non-compliant with being tested following being identified via tracing.
Individuals that are identified and selected as traced contacts are placed into a contact tracing queue. This queue is implemented such that it imposes a time delay between the identification of the primary positive case and the actual testing or isolation of the traced contacts. The number of cadence testing days constituting this tracing delay is specified by the tracing_lag
argument of run_tti_sim()
. On cadence testing days, the next group of traced individuals is popped off the queue and becomes the tracing testing pool.
The figure above depicts the contact tracing implementation. Testing identifies positive individuals (3 such individuals are shown). A subset of each primary case's contacts are identified as traced individuals (colored contact nodes). Following a tracing lag, the traced individuals may be tested (positive results depicted as red, negative as green) or isolated without a test (see Isolation section below). Some individuals may be non-compliant with participating in tracing (right primary case) or non-compliant with being test after being identified via tracing (dashed and unshaded contact nodes).
This simulation allows individuals to be isolated individually or as part of groups according to several criteria. In the extended SEIRS model, isolation refers to an individual transitioning into the quarantine compartment that corresponds to their disease state. Individuals in isolation (quarantine compartments) may have different parameters and network connectivity. The amount of time that individuals spend in isolation (in the quarantine states) is set by the isolation_time
parameter of the extended SEIRS model as implemented in the ExtSEIRSNetworkModel
class.
Isolation Groups: Groups of individuals that are to be considered for co-isolation can be specified by passing a list of lists defining groups of node IDs to the isolation_groups
argument of run_tti_sim()
. For example, isolation groups might represent households, employee teams, sub-communities, demographic groups, or other groups of individuals as relevant to the population and scenario of interest. When group-based isolation is used, when one individual is isolated for some reason the entire isolation group may also enter isolation.
There are several criteria that can prompt an individual or group to enter isolation in this simulation loop:
- Individuals that test positive (figure 1st column)
- Groupmates of individuals that test positive (figure 2nd column)
- Traced contacts of individuals that test positive (figure 3rd column)
- Groupmates of traced contacts of individuals that test positive (figure 4th column)
- Individuals that are symptomatic
- Groupmates of individuals that are symptomatic
Note that individuals that enter isolation due to being traced or symptomatic isolate without being tested themselves.
Individuals' compliance with isolation on a group basis is independent of their compliance with isolation on an individual basis and may be different for each triggering criteria.
Individuals that are set to enter isolation are put into an insolation queue. This queue is implemented such that it imposes a time delay between the criteria triggering the isolation and and the actual transition into a quarantine state for the isolating individual(s). The number of days constituting this isolation delay for each isolation criteria are specified by the isolation_lag_symptomatic
, isolation_lag_positive
, and isolation_lag_contact
arguments of run_tti_sim()
. On every simulation days, the next group of isolation individuals is popped off the queue and is transitioned into the appropriate quarantine state by calling the set_isolation()
function of the ExtSEIRSNetworkModel
class.
The simulation loop described here is implemented by the run_tti_sim()
function in the sim_loops.py
module.
The function arguments that parameterize this simulation loop are documented below:
Function Argument | Description | Data Type | Default Value |
---|---|---|---|
model |
the model to be simulated | ExtSEIRSNetworkModel object |
REQUIRED |
T |
total simulation duration | float |
REQUIRED |
intervention_start_pct_infected |
population disease prevalence that triggers the start of the TTI interventions | float |
0.0 |
average_introductions_per_day |
expected rate of exogenous introductions per day (λ parameter of associated poisson distribution) | float |
0.0 |
testing_cadence |
string (dict key) identifying the proactive testing cadence to be used | string |
'everyday' |
pct_tested_per_day |
total daily allotment of tests, defined as a percentage of population size | float |
1.0 |
test_falseneg_rate |
specifies the test sensitivity via the false negative rates to be used; numerical value specifies constant false negative rate, "temporal" specifies that temporal, state-based false negative rates are to be used |
float or string
|
"temporal" |
testing_compliance_symptomatic |
the compliance of each individual with self-reporting symptomatic testing |
list of True/False
|
[None] (disabled) |
max_pct_tests_for_symptomatics |
the maximum portion of the daily test allotment to use for testing self-reporting symptomatic individuals | float |
1.0 |
testing_compliance_traced |
the compliance of each individual with contact tracing testing |
list of True/False
|
[None] (disabled) |
max_pct_tests_for_traces |
the maximum portion of the daily test allotment to use for testing traced individuals | float |
1.0 |
testing_compliance_random |
the compliance of each individual with random testing |
list of True/False
|
[None] (disabled) |
random_testing_degree_bias |
magnitude of node degree bias in random testing selection (sets b for probability of selection ∝ degreeb) | float |
0 (no degree bias) |
tracing_compliance |
the compliance of each individual with contact tracing (providing contacts) |
list of True/False
|
[None] (disabled) |
num_contacts_to_trace |
the fixed absolute number of contacts to trace per positive case (overrides pct_contacts_to_trace when provided) |
float |
None (use pct_contacts_to_trace ) |
pct_contacts_to_trace |
the percent of ones contacts to trace per positive case | float |
1.0 |
tracing_lag |
the number of cadence testing days between identification of a positive case and the testing of their traced contacts | int |
1 |
isolation_compliance_symptomatic_individual |
the compliance of each individual with isolating due to being symptomatic |
list of True/False
|
[None] (disabled) |
isolation_compliance_symptomatic_groupmate |
the compliance of each individual with isolating due to being a groupmate of a symptomatic individual |
list of True/False
|
[None] (disabled) |
isolation_compliance_positive_individual |
the compliance of each individual with isolating due to being a positive case |
list of True/False
|
[None] (disabled) |
isolation_compliance_positive_groupmate |
the compliance of each individual with isolating due to being a groupmate of a positive individual |
list of True/False
|
[None] (disabled) |
isolation_compliance_positive_contact |
the compliance of each individual with isolating due to having a positive contact |
list of True/False
|
[None] (disabled) |
isolation_compliance_positive_contactgroupmate |
the compliance of each individual with isolating due to being a groupmate of an individual with a positive contact |
list of True/False
|
[None] (disabled) |
isolation_lag_symptomatic |
the number of days between being flagged for isolation due to a symptomatic individual and transitioning into a quarantine state | int |
1 |
isolation_lag_positive |
the number of days between being flagged for isolation due to a positive individual and transitioning into a quarantine state | int |
1 |
isolation_lag_contact |
the number of days between being flagged for isolation due to a positive contact and transitioning into a quarantine state | int |
0 |
isolation_groups |
specification of groups of individuals that will co-isolate when one of their members is triggered to isolate |
list of list s of node IDs (ints ) |
None |
cadence_testing_days |
dictionary defining testing cadence days within a 28-day cycle | dict |
None (use pre-defined) |
temporal_falseneg_rates |
dictionary defining false negative rates according to disease state and time in state | dict |
None (use pre-defined) |
Extended SEIRS Model
Basic SEIRS Model
Simulation Demos