Skip to content

TTI Simulation Loop

Ryan Seamus McGee edited this page Aug 9, 2020 · 4 revisions

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 TTI Simulation Loop

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.

TTI Protocol Outline

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.

Protocol:

  • 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)

TTI Protocol Components:

Simulation 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.

Exogenous Introductions

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().

Intervention Compliances

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.

Testing

Test Allotments

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 Cadence

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

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},
                          }
For example, this dictionary specifies that the false negative rate for individuals on their third day of being in a symptomatic state is 0.16 (i.e., `temporal_falseneg_rates['model.I_sym'][2] = 0.16`). The values in this pre-defined `temporal_falseneg_rates` dictionary are approximate consensus false negative rates for PCR-based SARS-CoV-2 tests taken from [Kucirka et al. (2020)](https://www.acpjournals.org/doi/10.7326/M20-1495) and [Wikramaratna et al. (2020)](https://www.medrxiv.org/content/10.1101/2020.04.05.20053355v2). These predefined values are depicted at right as a function of time before/since onset of symptoms.

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().

Testing Flags

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

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

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

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

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).

Isolation

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 run_tti_sim() function

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 lists 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)