From c2b5297cca93e137080220c0d973d980711fd7ae Mon Sep 17 00:00:00 2001 From: Matthew Abueg Date: Wed, 27 Jan 2021 15:44:17 -0800 Subject: [PATCH 01/10] Adding lateral flow test intervention code. Will clean up spacing soon, just wanted to get this out for PR. --- .../active_intervention_parameters.md | 9 + .../parameters/parameter_dictionary.md | 9 + src/COVID19/model.py | 24 + src/constant.h | 3 + src/demographics.c | 3 +- src/disease.c | 33 ++ src/individual.c | 3 + src/individual.h | 3 + src/input.c | 51 +- src/interventions.c | 190 +++++++- src/interventions.h | 3 + src/model.c | 3 + src/model.h | 1 + src/params.c | 164 ++++++- src/params.h | 34 +- src/params_utils.i | 171 +++++++ src/utilities.c | 27 ++ src/utilities.h | 1 + tests/data/baseline_parameters.csv | 4 +- tests/data/baseline_parameters_transpose.csv | 9 + tests/test_interventions.py | 436 +++++++++++++++++- 21 files changed, 1105 insertions(+), 76 deletions(-) diff --git a/documentation/parameters/active_intervention_parameters.md b/documentation/parameters/active_intervention_parameters.md index f45c590d6..2b52b37d7 100644 --- a/documentation/parameters/active_intervention_parameters.md +++ b/documentation/parameters/active_intervention_parameters.md @@ -10,6 +10,7 @@ | `quarantine_dropout_positive` | 0.01 | - | Daily probability of drop out for an individual quarantining after a positive test result | - | | `quarantine_compliance_traced_symptoms` | 0.5 | - | Fraction of individuals who initially comply with a quarantine notification after their contact reported symptoms | - | | `quarantine_compliance_traced_positive` | 0.9 | - | Fraction of individuals who initially comply with a quarantine notification after their contact tested positive | - | +| `quarantine_compliance_positive` | 1.0 | - | Fraction of individuals who initially comply with a quarantine after they test positive | - | | `test_on_symptoms` | 0 | - | Test individuals who show symptoms (0=no, 1=yes) | - | | `test_on_traced` | 0 | - | Test individuals who have been contact-traced (0=no, 1=yes) | - | | `test_release_on_negative` | 1 | - | Release individuals following a negative test (0=no, 1=yes) | - | @@ -34,6 +35,14 @@ | `test_specificity` | 0.999 | - | Specificity of test (at any time) | - | | `test_order_wait` | 1 | - | Minimum number of days to wait to take a test | - | | `test_result_wait` | 1 | - | Number of days to wait for a test result | - | +| `lateral_flow_test_order_wait` | 1 | - | Number of days to wait to receive a set of lateral flow tests | - | +| `lateral_flow_test_on_symptoms` | 0 | - | Test individuals with Lateral Flow Assay who show symptoms (0=no, 1=yes) | - | +| `lateral_flow_test_on_traced` | 0 | - | Test individuals with Lateral Flow Assay who have been contact-traced (0=no, 1=yes) | - | +| `lateral_flow_test_repeat_count` | 7 | - | Number of daily Lateral Flow Assay tests to take in a row | - | +| `lateral_flow_test_only` | 0 | - | If an individual takes Lateral Flow tests, do not take PCR or quarantine until they receive a test result | - | +| `lateral_flow_test_fraction` | 0.5 | - | The fraction of individuals who take a Lateral Flow Assay test instead of quarantine if offered | - | +| `lateral_flow_test_sensitivity` | 0.95 | - | Peak sensitivity of Lateral Flow Assay | - | +| `lateral_flow_test_specificity` | 0.999 | - | Specificity of Lateral Flow Assay (at any time) | - | | `app_users_fraction_0_9` | 0.09 | - | Maximum fraction of the population with smartphones aged 0-9 | OFCOM 3-5 year olds | | `app_users_fraction_10_19` | 0.8 | - | Maximum fraction of the population with smartphones aged 10-19 | OFCOM 5-15 year olds | | `app_users_fraction_20_29` | 0.97 | - | Maximum fraction of the population with smartphones aged 20-29 | OFCOM 16-55 year olds | diff --git a/documentation/parameters/parameter_dictionary.md b/documentation/parameters/parameter_dictionary.md index 80bd4ca9b..da85a0753 100644 --- a/documentation/parameters/parameter_dictionary.md +++ b/documentation/parameters/parameter_dictionary.md @@ -136,6 +136,7 @@ | `quarantine_dropout_positive` | 0.01 | - | Daily probability of drop out for an individual quarantining after a positive test result | - | | `quarantine_compliance_traced_symptoms` | 0.5 | - | Fraction of individuals who initially comply with a quarantine notification after their contact reported symptoms | - | | `quarantine_compliance_traced_positive` | 0.9 | - | Fraction of individuals who initially comply with a quarantine notification after their contact tested positive | - | +| `quarantine_compliance_positive` | 1.0 | - | Fraction of individuals who initially comply with a quarantine after they test positive | - | | `test_on_symptoms` | 0 | - | Test individuals who show symptoms (0=no, 1=yes) | - | | `test_on_traced` | 0 | - | Test individuals who have been contact-traced (0=no, 1=yes) | - | | `test_release_on_negative` | 1 | - | Release individuals following a negative test (0=no, 1=yes) | - | @@ -173,6 +174,14 @@ | `priority_test_contacts_60_69` | 1000 | - | Number of contacts that triggers priority test for individuals aged 60-69 years old | - | | `priority_test_contacts_70_79` | 1000 | - | Number of contacts that triggers priority test for individuals aged 70-79 years old | - | | `priority_test_contacts_80` | 1000 | - | Number of contacts that triggers priority test for individuals aged 80+ years old | - | +| `lateral_flow_test_order_wait` | 1 | - | Number of days to wait to receive a set of lateral flow tests | - | +| `lateral_flow_test_on_symptoms` | 0 | - | Test individuals with Lateral Flow Assay who show symptoms (0=no, 1=yes) | - | +| `lateral_flow_test_on_traced` | 0 | - | Test individuals with Lateral Flow Assay who have been contact-traced (0=no, 1=yes) | - | +| `lateral_flow_test_repeat_count` | 7 | - | Number of daily Lateral Flow Assay tests to take in a row | - | +| `lateral_flow_test_only` | 0 | - | If an individual takes Lateral Flow tests, do not take PCR or quarantine until they receive a test result | - | +| `lateral_flow_test_fraction` | 0.5 | - | The fraction of individuals who take a Lateral Flow Assay test instead of quarantine if offered | - | +| `lateral_flow_test_sensitivity` | 0.95 | - | Peak sensitivity of Lateral Flow Assay | - | +| `lateral_flow_test_specificity` | 0.999 | - | Specificity of Lateral Flow Assay (at any time) | - | | `self_quarantine_fraction` | 0 | - | Proportion of people who self-quarantine upon symptoms | - | | `app_users_fraction_0_9` | 0.09 | - | Maximum fraction of the population with smartphones aged 0-9 | OFCOM 3-5 year olds | | `app_users_fraction_10_19` | 0.8 | - | Maximum fraction of the population with smartphones aged 10-19 | OFCOM 5-15 year olds | diff --git a/src/COVID19/model.py b/src/COVID19/model.py index 6af418b44..57f6f75e5 100644 --- a/src/COVID19/model.py +++ b/src/COVID19/model.py @@ -5,6 +5,7 @@ import pandas as pd import pkg_resources import sys, time +import numpy as np import covid19 from COVID19.network import Network @@ -40,12 +41,21 @@ class ModelException(Exception): "test_result_wait", "test_result_wait_priority", "self_quarantine_fraction", + "quarantine_compliance_positive", "lockdown_on", "lockdown_elderly_on", "app_turned_on", "app_users_fraction", "trace_on_symptoms", "trace_on_positive", + "lateral_flow_test_on_symptoms", + "lateral_flow_test_on_traced", + "lateral_flow_test_order_wait", + "lateral_flow_test_sensitivity", + "lateral_flow_test_specificity", + "lateral_flow_test_repeat_count", + "lateral_flow_test_only", + "lateral_flow_test_fraction", "lockdown_house_interaction_multiplier", "lockdown_random_network_multiplier", "lockdown_occupation_multiplier_primary_network", @@ -1139,6 +1149,20 @@ def one_time_step_results(self): results["R_inst_05"] = covid19.calculate_R_instanteous( self.c_model, self.c_model.time, 0.05 ) results["R_inst_95"] = covid19.calculate_R_instanteous( self.c_model, self.c_model.time, 0.95 ) + results["n_lateral_flow_tests"] = covid19.utils_n_total_by_day( + self.c_model, covid19.LATERAL_FLOW_TEST_TAKE, int(self.c_model.time) + ) + if results["n_lateral_flow_tests"] > 0: + results["mean_lfa_sensitivity"] = covid19.calculate_mean_lfa_sensitivity( self.c_model, covid19.NO_EVENT ) + results["mean_lfa_sensitivity_asymptom"] = covid19.calculate_mean_lfa_sensitivity( self.c_model, covid19.ASYMPTOMATIC ) + results["mean_lfa_sensitivity_symptom_mild"] = covid19.calculate_mean_lfa_sensitivity( self.c_model, covid19.PRESYMPTOMATIC_MILD ) + results["mean_lfa_sensitivity_symptom"] = covid19.calculate_mean_lfa_sensitivity( self.c_model, covid19.PRESYMPTOMATIC ) + else: + results["mean_lfa_sensitivity"] = np.nan + results["mean_lfa_sensitivity_asymptom"] = np.nan + results["mean_lfa_sensitivity_symptom_mild"] = np.nan + results["mean_lfa_sensitivity_symptom"] = np.nan + return results def write_output_files(self): diff --git a/src/constant.h b/src/constant.h index 298764cca..3bb3fb164 100644 --- a/src/constant.h +++ b/src/constant.h @@ -30,6 +30,7 @@ enum EVENT_TYPES{ DEATH, QUARANTINED, QUARANTINE_RELEASE, + LATERAL_FLOW_TEST_TAKE, TEST_TAKE, TEST_RESULT, CASE, @@ -161,6 +162,7 @@ enum INTERACTION_TYPE{ HOSPITAL_NURSE_PATIENT_GENERAL, HOSPITAL_DOCTOR_PATIENT_ICU, HOSPITAL_NURSE_PATIENT_ICU, + LATERAL_FLOW_TEST, N_INTERACTION_TYPES }; @@ -231,6 +233,7 @@ enum VACCINE_TYPES{ #define NO_HOSPITAL -1 #define HOSPITAL_WORK_NETWORK -1 #define N_HOSPITAL_INTERACTION_TYPES 5 +#define N_NETWORK_INTERACTION_TYPES N_INTERACTION_TYPES - N_HOSPITAL_INTERACTION_TYPES - 1 extern gsl_rng * rng; diff --git a/src/demographics.c b/src/demographics.c index 9b384e707..ac95d7725 100644 --- a/src/demographics.c +++ b/src/demographics.c @@ -235,7 +235,7 @@ void add_reference_household( double *array, long hdx, int **REFERENCE_HOUSEHOLD ******************************************************************************************/ void generate_household_distribution( model *model ) { - int idx, housesize, age; + int idx, age; long hdx, n_households, pdx, sample; double error, last_error, acceptance; demographic_household_table *demo_house = calloc( 1, sizeof( demographic_household_table ) ); @@ -336,7 +336,6 @@ void generate_household_distribution( model *model ) pdx = 0; for( hdx = 0; hdx < n_households; hdx++ ) { - housesize = 0; for( age = N_AGE_GROUPS - 1; age >= 0; age-- ) { for( idx = 0; idx < model->params->REFERENCE_HOUSEHOLDS[households[hdx]][age]; idx++ ) diff --git a/src/disease.c b/src/disease.c index dae8d160f..40f7f6bae 100644 --- a/src/disease.c +++ b/src/disease.c @@ -162,6 +162,39 @@ void set_up_infectious_curves( model *model ) gamma_rate_curve( model->event_lists[CRITICAL].infectious_curve[type], MAX_INFECTIOUS_PERIOD, params->mean_infectious_period, params->sd_infectious_period, infectious_rate * type_factor ); }; + + + model->event_lists[PRESYMPTOMATIC].infectious_peak_time = curve_peak_time( + model->event_lists[PRESYMPTOMATIC].infectious_curve[LATERAL_FLOW_TEST], + MAX_INFECTIOUS_PERIOD ); + + model->event_lists[PRESYMPTOMATIC_MILD].infectious_peak_time = curve_peak_time( + model->event_lists[PRESYMPTOMATIC_MILD].infectious_curve[LATERAL_FLOW_TEST], + MAX_INFECTIOUS_PERIOD ); + + model->event_lists[ASYMPTOMATIC].infectious_peak_time = curve_peak_time( + model->event_lists[ASYMPTOMATIC].infectious_curve[LATERAL_FLOW_TEST], + MAX_INFECTIOUS_PERIOD ); + + model->event_lists[SYMPTOMATIC].infectious_peak_time = curve_peak_time( + model->event_lists[SYMPTOMATIC].infectious_curve[LATERAL_FLOW_TEST], + MAX_INFECTIOUS_PERIOD ); + + model->event_lists[SYMPTOMATIC_MILD].infectious_peak_time = curve_peak_time( + model->event_lists[SYMPTOMATIC_MILD].infectious_curve[LATERAL_FLOW_TEST], + MAX_INFECTIOUS_PERIOD ); + + model->event_lists[HOSPITALISED].infectious_peak_time = curve_peak_time( + model->event_lists[HOSPITALISED].infectious_curve[LATERAL_FLOW_TEST], + MAX_INFECTIOUS_PERIOD ); + + model->event_lists[HOSPITALISED_RECOVERING].infectious_peak_time = curve_peak_time( + model->event_lists[HOSPITALISED_RECOVERING].infectious_curve[LATERAL_FLOW_TEST], + MAX_INFECTIOUS_PERIOD ); + + model->event_lists[CRITICAL].infectious_peak_time = curve_peak_time( + model->event_lists[CRITICAL].infectious_curve[LATERAL_FLOW_TEST], + MAX_INFECTIOUS_PERIOD ); } /***************************************************************************************** * Name: transmit_virus_by_type diff --git a/src/individual.c b/src/individual.c index 6d0231694..754fc6ba2 100644 --- a/src/individual.c +++ b/src/individual.c @@ -55,6 +55,9 @@ void initialize_individual( indiv->current_disease_event = NULL; indiv->next_disease_event = NULL; indiv->quarantine_test_result = NO_TEST; + indiv->lateral_flow_test_result = NO_TEST; + indiv->lateral_flow_test_capacity = 0; + indiv->lateral_flow_test_sensitivity = NO_TEST; indiv->trace_tokens = NULL; indiv->index_trace_token = NULL; diff --git a/src/individual.h b/src/individual.h index 24f619046..0265c7e18 100644 --- a/src/individual.h +++ b/src/individual.h @@ -45,6 +45,9 @@ struct individual{ event *quarantine_event; event *quarantine_release_event; int quarantine_test_result; + int lateral_flow_test_result; + int lateral_flow_test_capacity; + double lateral_flow_test_sensitivity; trace_token *trace_tokens; trace_token *index_trace_token; diff --git a/src/input.c b/src/input.c index d16c2c134..1f1715d18 100644 --- a/src/input.c +++ b/src/input.c @@ -260,7 +260,7 @@ void read_param_file( parameters *params) if( check < 1){ print_exit("Failed to read parameter relative_susceptibility\n"); }; } - for( i = 0; i < N_INTERACTION_TYPES - N_HOSPITAL_INTERACTION_TYPES; i++ ) + for( i = 0; i < N_NETWORK_INTERACTION_TYPES; i++ ) { check = fscanf(parameter_file, " %lf ,", &(params->relative_transmission[i])); if( check < 1){ print_exit("Failed to read parameter relative_transmission_**\n"); }; @@ -332,6 +332,9 @@ void read_param_file( parameters *params) check = fscanf(parameter_file, " %lf ,", &(params->quarantine_compliance_traced_positive)); if( check < 1){ print_exit("Failed to read parameter quarantine_compliance_traced_positive\n"); }; + check = fscanf(parameter_file, " %lf ,", &(params->quarantine_compliance_positive)); + if( check < 1){ print_exit("Failed to read parameter quarantine_compliance_positive\n"); }; + check = fscanf(parameter_file, " %i ,", &(params->test_on_symptoms)); if( check < 1){ print_exit("Failed to read parameter test_on_symptoms\n"); }; @@ -407,8 +410,8 @@ void read_param_file( parameters *params) check = fscanf(parameter_file, " %i , ", &(params->test_order_wait)); if( check < 1){ print_exit("Failed to read parameter test_order_wait\n"); }; - check = fscanf(parameter_file, " %i , ", &(params->test_order_wait_priority)); - if( check < 1){ print_exit("Failed to read parameter test_order_wait_priority\n"); }; + check = fscanf(parameter_file, " %i , ", &(params->test_order_wait_priority)); + if( check < 1){ print_exit("Failed to read parameter test_order_wait_priority\n"); }; check = fscanf(parameter_file, " %i , ", &(params->test_result_wait)); if( check < 1){ print_exit("Failed to read parameter test_result_wait\n"); }; @@ -417,10 +420,34 @@ void read_param_file( parameters *params) if( check < 1){ print_exit("Failed to read parameter test_result_wait_priority\n"); }; for( i = 0; i < N_AGE_GROUPS; i++ ) - { - check = fscanf(parameter_file, " %i ,", &(params->priority_test_contacts[i])); - if( check < 1){ print_exit("Failed to read parameter priority_test_contacts\n"); }; - } + { + check = fscanf(parameter_file, " %i ,", &(params->priority_test_contacts[i])); + if( check < 1){ print_exit("Failed to read parameter priority_test_contacts\n"); }; + } + + check = fscanf(parameter_file, "%i,", &(params->lateral_flow_test_order_wait)); + if( check < 1){ print_exit("Failed to read parameter lateral_flow_test_order_wait\n"); }; + + check = fscanf(parameter_file, "%i,", &(params->lateral_flow_test_on_symptoms)); + if( check < 1){ print_exit("Failed to read parameter lateral_flow_test_on_symptoms\n"); }; + + check = fscanf(parameter_file, "%i,", &(params->lateral_flow_test_on_traced)); + if( check < 1){ print_exit("Failed to read parameter lateral_flow_test_on_traced\n"); }; + + check = fscanf(parameter_file, "%i,", &(params->lateral_flow_test_repeat_count)); + if( check < 1){ print_exit("Failed to read parameter lateral_flow_test_repeat_count\n"); }; + + check = fscanf(parameter_file, "%i,", &(params->lateral_flow_test_only)); + if( check < 1){ print_exit("Failed to read parameter lateral_flow_test_only\n"); }; + + check = fscanf(parameter_file, "%lf,", &(params->lateral_flow_test_fraction)); + if( check < 1){ print_exit("Failed to read parameter lateral_flow_test_fraction\n"); }; + + check = fscanf(parameter_file, "%lf,", &(params->lateral_flow_test_sensitivity)); + if( check < 1){ print_exit("Failed to read parameter lateral_flow_test_sensitivity\n"); }; + + check = fscanf(parameter_file, "%lf,", &(params->lateral_flow_test_specificity)); + if( check < 1){ print_exit("Failed to read parameter lateral_flow_test_specificity\n"); }; check = fscanf(parameter_file, " %lf ,", &(params->self_quarantine_fraction)); if( check < 1){ print_exit("Failed to read parameter self_quarantine_fraction\n"); }; @@ -549,7 +576,7 @@ void read_hospital_param_file( parameters *params) check = fscanf( hospital_parameter_file, " %lf ,", &( params->critical_waiting_mod ) ); if( check < 1 ){ print_exit( "Failed to read parameter critical_waiting_mod\n" ); }; - for( i = N_INTERACTION_TYPES - N_HOSPITAL_INTERACTION_TYPES; i < N_INTERACTION_TYPES; i++ ) + for( i = N_NETWORK_INTERACTION_TYPES; i < N_NETWORK_INTERACTION_TYPES + N_HOSPITAL_INTERACTION_TYPES; i++ ) { check = fscanf(hospital_parameter_file, " %lf ,", &(params->relative_transmission[i])); if( check < 1){ print_exit("Failed to read parameter relative_transmission_**\n"); }; @@ -756,7 +783,8 @@ void write_individual_file(model *model, parameters *params) fprintf(individual_output_file,"mean_interactions,"); fprintf(individual_output_file,"infection_count,"); fprintf(individual_output_file,"infectiousness_multiplier,"); - fprintf(individual_output_file,"vaccine_status"); + fprintf(individual_output_file,"vaccine_status,"); + fprintf(individual_output_file,"lateral_flow_status"); fprintf(individual_output_file,"\n"); // Loop through all individuals in the simulation @@ -774,7 +802,7 @@ void write_individual_file(model *model, parameters *params) infection_count = count_infection_events( indiv ); fprintf(individual_output_file, - "%li,%d,%d,%d,%d,%d,%li,%d,%d,%d,%d,%d,%d,%0.4f,%d\n", + "%li,%d,%d,%d,%d,%d,%li,%d,%d,%d,%d,%d,%d,%0.4f,%d,%d\n", indiv->idx, indiv->status, indiv->age_group, @@ -789,7 +817,8 @@ void write_individual_file(model *model, parameters *params) indiv->random_interactions, infection_count, indiv->infectiousness_multiplier, - indiv->vaccine_status + indiv->vaccine_status, + indiv->lateral_flow_test_result ); } fclose(individual_output_file); diff --git a/src/interventions.c b/src/interventions.c index cd7fa8678..443491cc2 100644 --- a/src/interventions.c +++ b/src/interventions.c @@ -7,6 +7,7 @@ #include "model.h" #include "individual.h" +#include "gsl/gsl_randist.h" #include "utilities.h" #include "constant.h" #include "params.h" @@ -153,7 +154,7 @@ void set_up_trace_tokens( model *model ) } /***************************************************************************************** -* Name: new_trace_token +* Name: create_trace_token * Description: gets a new trace token * Returns: void ******************************************************************************************/ @@ -334,7 +335,7 @@ void update_intervention_policy( model *model, int time ) if( time == params->lockdown_time_off ) set_model_param_lockdown_on( model, FALSE ); - + if( time == params->lockdown_elderly_time_on ) set_model_param_lockdown_elderly_on( model, TRUE ); @@ -352,6 +353,15 @@ void update_intervention_policy( model *model, int time ) model->manual_trace_interview_quota = params->manual_trace_n_workers * params->manual_trace_interviews_per_worker_day; model->manual_trace_notification_quota = params->manual_trace_n_workers * params->manual_trace_notifications_per_worker_day; } + + for( int idx = 0; idx < model->params->n_total; idx++ ) + { + if( model->population[ idx ].lateral_flow_test_result >= 0 ) + { + model->population[ idx ].lateral_flow_test_result = NO_TEST; + model->population[ idx ].lateral_flow_test_sensitivity = NO_TEST; + } + } } /***************************************************************************************** @@ -736,6 +746,125 @@ void intervention_test_take( model *model, individual *indiv ) add_individual_to_event_list( model, TEST_RESULT, indiv, result_time ); } +/***************************************************************************************** +* Name: intervention_lateral_flow_test_order +* Description: Order a test for either today or a future date +* Returns: void +******************************************************************************************/ +void intervention_lateral_flow_test_order( model *model, individual *indiv, int time ) +{ + if( indiv->lateral_flow_test_result != TEST_ORDERED && !(indiv->infection_events->is_case) ) + { + indiv->lateral_flow_test_capacity = model->params->lateral_flow_test_repeat_count; + add_individual_to_event_list( model, LATERAL_FLOW_TEST_TAKE, indiv, time ); + indiv->lateral_flow_test_result = TEST_ORDERED; + } +} + +/***************************************************************************************** +* Name: intervention_lateral_flow_test_take +* Description: An individual takes a lateral flow test +* +* At the time of testing it will test positive if the individual has +* sufficient viral load, modeled by their infectiousness. +* Returns: void +******************************************************************************************/ +void intervention_lateral_flow_test_take( model *model, individual *indiv ) +{ + if (indiv->lateral_flow_test_capacity <= 0) return; + indiv->lateral_flow_test_capacity--; + + int result_time = model->time; + + int time_infected = time_infected( indiv ); + + if( time_infected != UNKNOWN ) + { + int infection_type = 0; + for(int bucket = 0; bucket < N_NEWLY_INFECTED_STATES; bucket++) + { + infection_type = NEWLY_INFECTED_STATES[bucket]; + if( indiv->infection_events->times[NEWLY_INFECTED_STATES[bucket]] == time_infected ) + break; + } + + time_infected = model->time - time_infected; + + const double infectious_factor = 1/.0802 * exp(1); + + double sensitivity = 0; + double I = 0; + double V = 0; + const int peak_time = model->event_lists[LATERAL_FLOW_TEST].infectious_peak_time; + const double *infectious_curve = model->event_lists[infection_type].infectious_curve[LATERAL_FLOW_TEST]; + const double g = 1.0/8; + + const double b = 1.0/6; + if( time_infected <= peak_time ) + { + I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * infectious_factor; + V = log( I ) / g; + } + else + { + I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * infectious_factor; + V = log( I ) / ( g + b ) + log( infectious_curve[ peak_time ] * + indiv->infectiousness_multiplier * + infectious_factor ) * ( 1/g - 1/( g + b ) ); + } + + if ( V < 0 ) + { + indiv->lateral_flow_test_result = gsl_ran_bernoulli( rng, 1 - model->params->lateral_flow_test_specificity ); + } + else + { + sensitivity = 1 / ( 1 + exp( -V ) ); + sensitivity = max( 0, min( model->params->lateral_flow_test_sensitivity , sensitivity ) ); + indiv->lateral_flow_test_sensitivity = sensitivity; + indiv->lateral_flow_test_result = gsl_ran_bernoulli( rng, sensitivity ); + } + } + else + indiv->lateral_flow_test_result = gsl_ran_bernoulli( rng, 1 - model->params->lateral_flow_test_specificity ); + + if ( indiv->lateral_flow_test_result == POSITIVE_TEST ) + indiv->quarantine_test_result = POSITIVE_TEST; + + if ( indiv->lateral_flow_test_result == POSITIVE_TEST ) + { + indiv->quarantine_test_result = POSITIVE_TEST; + add_individual_to_event_list( model, TEST_RESULT, indiv, result_time ); + } + else if ( indiv->lateral_flow_test_capacity > 0 ) + add_individual_to_event_list( model, LATERAL_FLOW_TEST_TAKE, indiv, model->time + 1 ); + +} + +/***************************************************************************************** +* Name: calculate_mean_lfa_sensitivity +* Description: Calculates the mean sensitivity for Lateral Flow tests for today. +* Returns: double +******************************************************************************************/ +double calculate_mean_lfa_sensitivity( model *model, int type ) +{ + double total = 0; + long cnt = 0; + individual * indiv; + for( int idx = 0; idx < model->params->n_total; idx++ ) + { + indiv = &(model->population[idx]); + if( indiv->lateral_flow_test_sensitivity >= 0 && + (type == NO_EVENT || + indiv->infection_events->times[type] == time_infected( indiv ))) + { + total += indiv->lateral_flow_test_sensitivity; + cnt++; + } + } + return cnt > 0 ? total / cnt : NAN; +} + /***************************************************************************************** * Name: intervention_test_result * Description: An individual gets a test result @@ -921,7 +1050,7 @@ void intervention_quarantine_household( { parameters *params = model->params; individual *contact; - int idx, n, time_event, quarantine, time_test; + int idx, n, time_event, quarantine, time_test, lateral_flow_test; long* members; double *risk_scores = model->params->risk_score_household[ indiv->age_group ]; @@ -938,14 +1067,18 @@ void intervention_quarantine_household( continue; quarantine = intervention_quarantine_until( model, contact, indiv, time_event, TRUE, index_token, contact_time, risk_scores[ contact->age_group ] ); + lateral_flow_test = params->lateral_flow_test_on_traced && ( index_token->index_status == POSITIVE_TEST ) && gsl_ran_bernoulli( rng, params->lateral_flow_test_fraction ); - if( quarantine && params->test_on_traced && ( index_token->index_status == POSITIVE_TEST ) ) + if( lateral_flow_test ) + intervention_lateral_flow_test_order( model, contact, model->time + params->lateral_flow_test_order_wait ); + + if( !(lateral_flow_test && params->lateral_flow_test_only) && quarantine && params->test_on_traced && ( index_token->index_status == POSITIVE_TEST ) ) { time_test = max( model->time + params->test_order_wait, contact_time + params->test_insensitive_period ); intervention_test_order( model, contact, time_test ); } - if( contact_trace && ( params->quarantine_on_traced || params->test_on_traced ) ) + if( contact_trace && ( params->quarantine_on_traced || params->test_on_traced || params->lateral_flow_test_on_traced ) ) intervention_notify_contacts( model, contact, NOT_RECURSIVE, index_token, DIGITAL_TRACE ); } } @@ -1003,6 +1136,7 @@ void intervention_index_case_symptoms_to_positive( trace_token *token = index_token; long house_no = index_token->individual->house_no; int contact_time; + int lateral_flow_test; while( token->next_index != NULL ) { @@ -1015,13 +1149,20 @@ void intervention_index_case_symptoms_to_positive( if( contact->status != DEATH && !is_in_hospital( contact ) && !contact->infection_events->is_case ) { contact_time = token->contact_time; - if( gsl_ran_bernoulli( rng, params->quarantine_compliance_traced_positive ) ) + + lateral_flow_test = params->lateral_flow_test_on_traced == TRUE && gsl_ran_bernoulli( rng, params->lateral_flow_test_fraction ); + if( lateral_flow_test ) + intervention_lateral_flow_test_order( model, contact, model->time + params->lateral_flow_test_order_wait ); + + if( !(params->lateral_flow_test_only && lateral_flow_test) && gsl_ran_bernoulli( rng, params->quarantine_compliance_traced_positive ) ) { time_quarantine = contact_time + sample_transition_time( model, TRACED_QUARANTINE_POSITIVE ); - intervention_quarantine_until( model, contact, index_token->individual, time_quarantine, TRUE, NULL, contact_time, 1 ); + } else { + time_quarantine = contact_time; } + intervention_quarantine_until( model, contact, index_token->individual, time_quarantine, TRUE, NULL, contact_time, 1 ); - if( ( contact->quarantine_release_event != NULL ) & ( params->test_on_traced == TRUE ) ) + if( ( contact->quarantine_release_event != NULL ) && ( params->test_on_traced == TRUE ) ) { time_test = max( model->time + params->test_order_wait, contact_time + params->test_insensitive_period ); intervention_test_order( model, contact, time_test ); @@ -1058,17 +1199,23 @@ void intervention_on_symptoms( model *model, individual *indiv ) if( indiv->index_trace_token != NULL ) return; - int quarantine, time_event; + int quarantine, time_event, lateral_flow_test; parameters *params = model->params; + lateral_flow_test = params->lateral_flow_test_on_symptoms && gsl_ran_bernoulli( rng, params->lateral_flow_test_fraction ); quarantine = indiv->quarantined || gsl_ran_bernoulli( rng, params->self_quarantine_fraction ); + if( lateral_flow_test ) + intervention_lateral_flow_test_order( model, indiv, model->time + params->lateral_flow_test_order_wait ); if( quarantine ) { trace_token *index_token = index_trace_token( model, indiv ); index_token->index_status = SYMPTOMS_ONLY; - time_event = model->time + sample_transition_time( model, SYMPTOMATIC_QUARANTINE ); + if (params->lateral_flow_test_only && lateral_flow_test) + time_event = model->time; + else + time_event = model->time + sample_transition_time( model, SYMPTOMATIC_QUARANTINE ); intervention_quarantine_until( model, indiv, NULL, time_event, TRUE, NULL, model->time, 1 ); indiv->traced_on_this_trace = TRUE; @@ -1079,7 +1226,7 @@ void intervention_on_symptoms( model *model, individual *indiv ) if( params->test_on_symptoms ) intervention_test_order( model, indiv, model->time + params->test_order_wait ); - if( params->trace_on_symptoms && ( params->quarantine_on_traced || params->test_on_traced ) ) + if( params->trace_on_symptoms && ( params->quarantine_on_traced || params->test_on_traced || params->lateral_flow_test_on_traced ) ) intervention_notify_contacts( model, indiv, 1, index_token, DIGITAL_TRACE ); remove_traced_on_this_trace( model, indiv ); @@ -1140,26 +1287,26 @@ void intervention_on_positive_result( model *model, individual *indiv ) index_token->index_status = POSITIVE_TEST; int release_time = index_token->contact_time + params->quarantine_length_traced_positive; - if( !is_in_hospital( indiv ) ) + if( !is_in_hospital( indiv ) && gsl_ran_bernoulli( rng, params->quarantine_compliance_positive ) ) { time_event = index_token->contact_time + sample_transition_time( model, TEST_RESULT_QUARANTINE ); - intervention_quarantine_until( model, indiv, NULL, time_event, TRUE, NULL, model->time, 1 ); } + intervention_quarantine_until( model, indiv, NULL, time_event, TRUE, NULL, model->time, 1 ); indiv->traced_on_this_trace = TRUE; if( params->quarantine_household_on_positive ) intervention_quarantine_household( model, indiv, time_event, params->quarantine_household_contacts_on_positive, index_token, model->time ); if( params->trace_on_positive && - ( !index_already || !params->trace_on_symptoms || params->retrace_on_positive ) && - ( params->quarantine_on_traced || params->test_on_traced ) + ( !index_already || !params->trace_on_symptoms || params->retrace_on_positive ) && + ( params->quarantine_on_traced || params->test_on_traced || params->lateral_flow_test_on_traced ) ) intervention_notify_contacts( model, indiv, 1, index_token, DIGITAL_TRACE ); if( params->manual_trace_on && ( params->manual_trace_on_positive || ( params->manual_trace_on_hospitalization && is_in_hospital( indiv ) ) ) && - ( params->quarantine_on_traced || params->test_on_traced ) + ( params->quarantine_on_traced || params->test_on_traced || params->lateral_flow_test_on_traced ) ) { add_individual_to_event_list( model, MANUAL_CONTACT_TRACING, indiv, model->time + params->manual_trace_delay ); @@ -1219,12 +1366,21 @@ void intervention_on_traced( parameters *params = model->params; + int lateral_flow_test = params->lateral_flow_test_on_traced && gsl_ran_bernoulli( rng, params->lateral_flow_test_fraction ); + + if( lateral_flow_test ) + intervention_lateral_flow_test_order( model, indiv, model->time + params->lateral_flow_test_order_wait ); + if( params->quarantine_on_traced ) { int time_event = model->time; int quarantine; - if( index_token->index_status == SYMPTOMS_ONLY ) + if (params->lateral_flow_test_only && lateral_flow_test) + { + time_event = model->time; + } + else if( index_token->index_status == SYMPTOMS_ONLY ) { if( gsl_ran_bernoulli( rng, params->quarantine_compliance_traced_symptoms ) ) time_event = contact_time + sample_transition_time( model, TRACED_QUARANTINE_SYMPTOMS ); diff --git a/src/interventions.h b/src/interventions.h index f91f14d9b..436ec1f9f 100644 --- a/src/interventions.h +++ b/src/interventions.h @@ -63,6 +63,9 @@ void intervention_on_hospitalised( model*, individual* ); void intervention_on_critical( model*, individual* ); void intervention_on_positive_result( model*, individual* ); void intervention_on_traced( model*, individual*, int, int, trace_token*, double, int ); +void intervention_lateral_flow_test_order( model*, individual*, int ); +void intervention_lateral_flow_test_take( model*, individual* ); +double calculate_mean_lfa_sensitivity( model *, int ); void intervention_smart_release( model* ); int resolve_quarantine_reasons(int *); diff --git a/src/model.c b/src/model.c index 57f7a99b2..2d4f4000a 100644 --- a/src/model.c +++ b/src/model.c @@ -56,6 +56,7 @@ model* new_model( parameters *params ) rng = gsl_rng_alloc ( gsl_rng_default); gsl_rng_set( rng, params->rng_seed ); + set_up_population( model_ptr ); update_intervention_policy( model_ptr, model_ptr->time ); model_ptr->event_lists = calloc( N_EVENT_TYPES, sizeof( event_list ) ); @@ -1355,9 +1356,11 @@ int one_time_step( model *model ) while( ( n_daily( model, TEST_TAKE, model->time ) > 0 ) || ( n_daily( model, TEST_RESULT, model->time ) > 0 ) || + ( n_daily( model, LATERAL_FLOW_TEST_TAKE, model->time ) > 0 ) || ( n_daily( model, MANUAL_CONTACT_TRACING, model->time ) > 0 ) ) { + transition_events( model, LATERAL_FLOW_TEST_TAKE, &intervention_lateral_flow_test_take, TRUE ); transition_events( model, TEST_TAKE, &intervention_test_take, TRUE ); transition_events( model, TEST_RESULT, &intervention_test_result, TRUE ); transition_events( model, MANUAL_CONTACT_TRACING, &intervention_manual_trace, TRUE ); diff --git a/src/model.h b/src/model.h index 96aa4747f..c8d75dcd3 100644 --- a/src/model.h +++ b/src/model.h @@ -33,6 +33,7 @@ struct event_list{ long *n_total_by_age; long n_current; double **infectious_curve; + int infectious_peak_time; }; struct directory{ diff --git a/src/params.c b/src/params.c index d80fd5e1b..427487ae6 100644 --- a/src/params.c +++ b/src/params.c @@ -24,6 +24,8 @@ void initialize_params( parameters *params ) { params->demo_house = NULL; params->occupation_network_table = NULL; + params->relative_transmission[LATERAL_FLOW_TEST] = 1; + params->relative_transmission_used[LATERAL_FLOW_TEST] = 1; } /***************************************************************************************** @@ -386,6 +388,15 @@ double get_model_param_self_quarantine_fraction(model *model) return model->params->self_quarantine_fraction; } +/***************************************************************************************** +* Name: get_model_param_quarantine_compliance_positive +* Description: Gets the value of an int parameter +******************************************************************************************/ +double get_model_param_quarantine_compliance_positive(model *model) +{ + return model->params->quarantine_compliance_positive; +} + /***************************************************************************************** * Name: get_model_param_trace_on_symptoms * Description: Gets the value of an int parameter @@ -567,6 +578,60 @@ int get_model_param_priority_test_contacts(model *model, int idx) return model->params->priority_test_contacts[idx]; } +/***************************************************************************************** +* Name: get_model_param_lateral_flow_test_on_symptoms +* Description: Gets the value of an int parameter +******************************************************************************************/ +int get_model_param_lateral_flow_test_on_symptoms(model *model) +{ + return model->params->lateral_flow_test_on_symptoms; +} + +/***************************************************************************************** +* Name: get_model_param_lateral_flow_test_on_traced +* Description: Gets the value of an int parameter +******************************************************************************************/ +int get_model_param_lateral_flow_test_on_traced(model *model) +{ + return model->params->lateral_flow_test_on_traced; +} + +/***************************************************************************************** +* Name: get_model_param_lateral_flow_test_order_wait +* Description: Gets the value of an int parameter +******************************************************************************************/ +int get_model_param_lateral_flow_test_order_wait(model *model) +{ + return model->params->lateral_flow_test_order_wait; +} + +/***************************************************************************************** +* Name: get_model_param_lateral_flow_test_repeat_count +* Description: Gets the value of an int parameter +******************************************************************************************/ +int get_model_param_lateral_flow_test_repeat_count(model *model) +{ + return model->params->lateral_flow_test_repeat_count; +} + +/***************************************************************************************** +* Name: get_model_param_lateral_flow_test_only +* Description: Gets the value of an int parameter +******************************************************************************************/ +int get_model_param_lateral_flow_test_only(model *model) +{ + return model->params->lateral_flow_test_only; +} + +/***************************************************************************************** +* Name: get_model_param_lateral_flow_test_fraction +* Description: Gets the value of an double parameter +******************************************************************************************/ +double get_model_param_lateral_flow_test_fraction(model *model) +{ + return model->params->lateral_flow_test_fraction; +} + /***************************************************************************************** * Name: get_model_param_app_users_fraction * Description: Gets the value of double parameter @@ -734,6 +799,16 @@ int set_model_param_self_quarantine_fraction(model *model, double value) return TRUE; } +/***************************************************************************************** +* Name: set_model_param_quarantine_compliance_positive +* Description: Sets the value of parameter +******************************************************************************************/ +int set_model_param_quarantine_compliance_positive(model *model, double value) +{ + model->params->quarantine_compliance_positive = value; + return TRUE; +} + /***************************************************************************************** * Name: set_model_param_trace_on_symptoms * Description: Sets the value of parameter @@ -909,17 +984,17 @@ int set_model_param_test_order_wait( model *model, int value ) ******************************************************************************************/ int set_model_param_test_result_wait_priority( model *model, int value ) { - model->params->test_result_wait_priority = value; + model->params->test_result_wait_priority = value; - if( model->params->test_order_wait_priority == NO_PRIORITY_TEST ) - model->params->test_order_wait_priority = model->params->test_order_wait; + if( model->params->test_order_wait_priority == NO_PRIORITY_TEST ) + model->params->test_order_wait_priority = model->params->test_order_wait; - if( value == NO_PRIORITY_TEST ) - model->params->test_order_wait_priority = NO_PRIORITY_TEST; + if( value == NO_PRIORITY_TEST ) + model->params->test_order_wait_priority = NO_PRIORITY_TEST; - check_params( model->params ); + check_params( model->params ); - return TRUE; + return TRUE; } /***************************************************************************************** @@ -928,17 +1003,17 @@ int set_model_param_test_result_wait_priority( model *model, int value ) ******************************************************************************************/ int set_model_param_test_order_wait_priority( model *model, int value ) { - model->params->test_order_wait_priority = value; + model->params->test_order_wait_priority = value; - if( model->params->test_result_wait_priority == NO_PRIORITY_TEST ) - model->params->test_result_wait_priority = model->params->test_result_wait; + if( model->params->test_result_wait_priority == NO_PRIORITY_TEST ) + model->params->test_result_wait_priority = model->params->test_result_wait; - if( value == NO_PRIORITY_TEST ) - model->params->test_result_wait_priority = NO_PRIORITY_TEST; + if( value == NO_PRIORITY_TEST ) + model->params->test_result_wait_priority = NO_PRIORITY_TEST; - check_params( model->params ); + check_params( model->params ); - return TRUE; + return TRUE; } /***************************************************************************************** @@ -951,6 +1026,67 @@ int set_model_param_priority_test_contacts( model *model, int value, int idx ) model->params->priority_test_contacts[idx] = value; return TRUE; } + +/***************************************************************************************** +* Name: set_model_param_lateral_flow_test_on_symptoms +* Description: Sets the value of parameter +******************************************************************************************/ +int set_model_param_lateral_flow_test_on_symptoms(model *model, int value) +{ + model->params->lateral_flow_test_on_symptoms = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_model_param_lateral_flow_test_on_traced +* Description: Sets the value of parameter +******************************************************************************************/ +int set_model_param_lateral_flow_test_on_traced(model *model, int value) +{ + model->params->lateral_flow_test_on_traced = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_model_param_lateral_flow_test_order_wait +* Description: Sets the value of parameter +******************************************************************************************/ +int set_model_param_lateral_flow_test_order_wait(model *model, int value) +{ + model->params->lateral_flow_test_order_wait = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_model_param_lateral_flow_test_repeat_count +* Description: Sets the value of parameter +******************************************************************************************/ +int set_model_param_lateral_flow_test_repeat_count(model *model, int value) +{ + model->params->lateral_flow_test_repeat_count = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_model_param_lateral_flow_test_only +* Description: Sets the value of parameter +******************************************************************************************/ +int set_model_param_lateral_flow_test_only(model *model, int value) +{ + model->params->lateral_flow_test_only = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_model_param_lateral_flow_test_fraction +* Description: Sets the value of parameter +******************************************************************************************/ +int set_model_param_lateral_flow_test_fraction(model *model, double value) +{ + model->params->lateral_flow_test_fraction = value; + return TRUE; +} + /***************************************************************************************** * Name: set_model_param_app_users_fraction * Description: Sets the value of parameter diff --git a/src/params.h b/src/params.h index 492f5c1ba..7aeac453c 100644 --- a/src/params.h +++ b/src/params.h @@ -118,6 +118,7 @@ typedef struct{ double quarantine_compliance_traced_symptoms; // probability that someone complies with a amber quarantine message double quarantine_compliance_traced_positive; // probability that someone complies with a red quarantine message + double quarantine_compliance_positive; // probability that someone complies with a quarantine after testing positive int quarantine_on_traced; // immediately quarantine those who are contact traced int quarantine_smart_release_day; // number of days until smart release on no contacts @@ -134,7 +135,7 @@ typedef struct{ int quarantine_household_contacts_on_symptoms; // quarantine the contacts of other household members when someone gets symptoms int test_on_symptoms; // carry out a test on those with symptoms - int test_on_traced; // carry out a test on those with positive test results + int test_on_traced; // carry out a test on those contacted via contact tracing int test_result_wait; // number of days to wait for a test result int test_order_wait; // minimum number of days to wait for a test to be taken int test_result_wait_priority; // number of days to wait for a priority test result @@ -143,10 +144,19 @@ typedef struct{ int priority_test_contacts[N_AGE_GROUPS]; // number of contacts that triggers priority test - int test_insensitive_period; // number of days until a test is sensitive (delay test of recent contacts) - int test_sensitive_period; // number of days post infection in which the test is sensitive - double test_sensitivity; // sensitivity of test - double test_specificity; // specificity of test + int test_insensitive_period; // number of days until a PCR test is sensitive (delay test of recent contacts) + int test_sensitive_period; // number of days post infection in which the PCR test is sensitive + double test_sensitivity; // sensitivity of PCR test + double test_specificity; // specificity of PCR test + + int lateral_flow_test_on_symptoms; // carry out a lateral flow test on those with symptoms + int lateral_flow_test_on_traced; // carry out a lateral flow test on those contacted via contact tracing + int lateral_flow_test_order_wait; // number of days to wait for a test to be delivered + double lateral_flow_test_sensitivity; // peak sensitivity of lateral flow test + double lateral_flow_test_specificity; // specificity of lateral flow test + int lateral_flow_test_repeat_count; // number of tests to take, one per day, when advised to do so + int lateral_flow_test_only; // if lateral flow testing, do not perform other interventions until you have a positive test result + double lateral_flow_test_fraction; // if offered lateral flow testing, the fraction of people that will choose it double app_users_fraction[N_AGE_GROUPS];// Proportion of the population that use the app by age int app_turned_on; // is the app turned on @@ -206,6 +216,7 @@ int get_model_param_hospital_on(model *pmodel); double get_model_param_daily_fraction_work_used(model *pmodel, int idx); int get_model_param_quarantine_days(model *pmodel); double get_model_param_self_quarantine_fraction(model *pmodel); +double get_model_param_quarantine_compliance_positive(model *pmodel); int get_model_param_trace_on_symptoms(model *pmodel); int get_model_param_trace_on_positive(model *pmodel); int get_model_param_quarantine_on_traced(model *pmodel); @@ -245,9 +256,16 @@ int get_model_param_manual_trace_interviews_per_worker_day( model *pmodel ); int get_model_param_manual_trace_notifications_per_worker_day( model *pmodel ); double get_model_param_manual_traceable_fraction( model *pmodel, int ); double get_model_param_fatality_fraction( model *pmodel, int age_group ); +int get_model_param_lateral_flow_test_on_symptoms(model *pmodel); +int get_model_param_lateral_flow_test_on_traced(model *pmodel); +int get_model_param_lateral_flow_test_order_wait(model *pmodel); +int get_model_param_lateral_flow_test_repeat_count(model *pmodel); +int get_model_param_lateral_flow_test_only(model *pmodel); +double get_model_param_lateral_flow_test_fraction(model *pmodel); int set_model_param_quarantine_days(model *pmodel, int value); int set_model_param_self_quarantine_fraction(model *pmodel, double value); +int set_model_param_quarantine_compliance_positive(model *pmodel, double value); int set_model_param_trace_on_symptoms(model *pmodel, int value); int set_model_param_trace_on_positive(model *pmodel, int value); int set_model_param_quarantine_on_traced(model *pmodel, int value); @@ -285,6 +303,12 @@ int set_model_param_manual_trace_n_workers( model *pmodel, int value ); int set_model_param_manual_trace_interviews_per_worker_day( model *pmodel, int value ); int set_model_param_manual_trace_notifications_per_worker_day( model *pmodel, int value ); int set_model_param_manual_traceable_fraction( model *pmodel, double value, int type ); +int set_model_param_lateral_flow_test_on_symptoms( model *pmodel, int value ); +int set_model_param_lateral_flow_test_on_traced( model *pmodel, int value ); +int set_model_param_lateral_flow_test_order_wait( model *pmodel, int value ); +int set_model_param_lateral_flow_test_repeat_count( model *pmodel, int value ); +int set_model_param_lateral_flow_test_only(model *pmodel, int value ); +int set_model_param_lateral_flow_test_fraction(model *pmodel, double value ); int set_model_param_risk_score( model*, int, int, int, double ); int set_model_param_risk_score_household( model*, int, int, double ); diff --git a/src/params_utils.i b/src/params_utils.i index 46b82df99..d1f8f9116 100644 --- a/src/params_utils.i +++ b/src/params_utils.i @@ -427,6 +427,15 @@ double get_param_self_quarantine_fraction(parameters *params) return params->self_quarantine_fraction; } +/***************************************************************************************** +* Name: get_param_quarantine_compliance_positive +* Description: Gets the value of an int parameter +******************************************************************************************/ +double get_param_quarantine_compliance_positive(parameters *params) +{ + return params->quarantine_compliance_positive; +} + /***************************************************************************************** * Name: get_param_trace_on_symptoms * Description: Gets the value of an int parameter @@ -726,6 +735,78 @@ int get_param_priority_test_contacts(parameters *params, int idx) return params->priority_test_contacts[idx]; } +/***************************************************************************************** +* Name: get_param_lateral_flow_test_on_symptoms +* Description: Gets the value of an int parameter +******************************************************************************************/ +int get_param_lateral_flow_test_on_symptoms(parameters *params) +{ + return params->lateral_flow_test_on_symptoms; +} + +/***************************************************************************************** +* Name: get_param_lateral_flow_test_on_traced +* Description: Gets the value of an int parameter +******************************************************************************************/ +int get_param_lateral_flow_test_on_traced(parameters *params) +{ + return params->lateral_flow_test_on_traced; +} + +/***************************************************************************************** +* Name: get_param_lateral_flow_test_sensitivity +* Description: Gets the value of a double parameter +******************************************************************************************/ +double get_param_lateral_flow_test_sensitivity(parameters *params) +{ + return params->lateral_flow_test_sensitivity; +} + +/***************************************************************************************** +* Name: get_param_lateral_flow_test_specificity +* Description: Gets the value of a double parameter +******************************************************************************************/ +double get_param_lateral_flow_test_specificity(parameters *params) +{ + return params->lateral_flow_test_specificity; +} + +/***************************************************************************************** +* Name: get_param_lateral_flow_test_order_wait +* Description: Gets the value of an int parameter +******************************************************************************************/ +int get_param_lateral_flow_test_order_wait(parameters *params) +{ + return params->lateral_flow_test_order_wait; +} + +/***************************************************************************************** +* Name: get_param_lateral_flow_test_repeat_count +* Description: Gets the value of an int parameter +******************************************************************************************/ +int get_param_lateral_flow_test_repeat_count(parameters *params) +{ + return params->lateral_flow_test_repeat_count; +} + +/***************************************************************************************** +* Name: get_param_lateral_flow_test_only +* Description: Gets the value of an int parameter +******************************************************************************************/ +int get_param_lateral_flow_test_only(parameters *params) +{ + return params->lateral_flow_test_only; +} + +/***************************************************************************************** +* Name: get_param_lateral_flow_test_fraction +* Description: Gets the value of an int parameter +******************************************************************************************/ +double get_param_lateral_flow_test_fraction(parameters *params) +{ + return params->lateral_flow_test_fraction; +} + /***************************************************************************************** * Name: get_param_app_users_fraction * Description: Gets the value of double parameter @@ -1290,6 +1371,16 @@ int set_param_self_quarantine_fraction(parameters *params, double value) return TRUE; } +/***************************************************************************************** +* Name: set_param_quarantine_compliance_positive +* Description: Sets the value of parameter +******************************************************************************************/ +int set_param_quarantine_compliance_positive(parameters *params, double value) +{ + params->quarantine_compliance_positive = value; + return TRUE; +} + /***************************************************************************************** * Name: set_param_trace_on_symptoms * Description: Sets the value of parameter @@ -1621,6 +1712,86 @@ int set_param_priority_test_contacts(parameters *params, int value, int idx) return TRUE; } +/***************************************************************************************** +* Name: set_param_lateral_flow_test_on_symptoms +* Description: Sets the value of parameter +******************************************************************************************/ +int set_param_lateral_flow_test_on_symptoms(parameters *params, int value) +{ + params->lateral_flow_test_on_symptoms = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_param_lateral_flow_test_on_traced +* Description: Sets the value of parameter +******************************************************************************************/ +int set_param_lateral_flow_test_on_traced(parameters *params, int value) +{ + params->lateral_flow_test_on_traced = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_param_lateral_flow_test_sensitivity +* Description: Sets the value of parameter +******************************************************************************************/ +int set_param_lateral_flow_test_sensitivity(parameters *params, double value) +{ + params->lateral_flow_test_sensitivity = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_param_lateral_flow_test_specificity +* Description: Sets the value of parameter +******************************************************************************************/ +int set_param_lateral_flow_test_specificity(parameters *params, double value) +{ + params->lateral_flow_test_specificity = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_param_lateral_flow_test_order_wait +* Description: Sets the value of parameter +******************************************************************************************/ +int set_param_lateral_flow_test_order_wait(parameters *params, int value) +{ + params->lateral_flow_test_order_wait = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_param_lateral_flow_test_repeat_count +* Description: Sets the value of parameter +******************************************************************************************/ +int set_param_lateral_flow_test_repeat_count(parameters *params, int value) +{ + params->lateral_flow_test_repeat_count = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_param_lateral_flow_test_only +* Description: Sets the value of parameter +******************************************************************************************/ +int set_param_lateral_flow_test_only(parameters *params, int value) +{ + params->lateral_flow_test_only = value; + return TRUE; +} + +/***************************************************************************************** +* Name: set_param_lateral_flow_test_fraction +* Description: Sets the value of parameter +******************************************************************************************/ +int set_param_lateral_flow_test_fraction(parameters *params, double value) +{ + params->lateral_flow_test_fraction = value; + return TRUE; +} + /***************************************************************************************** * Name: set_param_app_users_fraction * Description: Sets the value of parameter diff --git a/src/utilities.c b/src/utilities.c index 71735a26e..bd5dc947b 100644 --- a/src/utilities.c +++ b/src/utilities.c @@ -284,6 +284,33 @@ void gamma_rate_curve( list[idx] *= factor / total; } +/***************************************************************************************** +* Name: curve_peak_time +* Description: returns the first time of the peak value of the provided +* curve +* +* Arguments: list: pointer to draw list +* n: length of draw list +******************************************************************************************/ +int curve_peak_time( + double *list, + int n +) +{ + int idx = 0; + int maxt = 0; + double maxv = 0; + for( idx = 0; idx < n; idx++ ) + { + if( list[idx] > maxv ) + { + maxv = list[idx]; + maxt = idx; + } + } + return maxt; +} + /***************************************************************************************** * Name: negative_binomial_draw * Description: Draws from a negative binomial distribution with a given mean diff --git a/src/utilities.h b/src/utilities.h index e2de0189f..7d5dd6710 100644 --- a/src/utilities.h +++ b/src/utilities.h @@ -46,6 +46,7 @@ void geometric_max_draw_list( int*, int, double, int ); void shifted_geometric_draw_list( int*, int, double , int); void geometric_draw_list( int*, int, double ); void gamma_rate_curve( double*, int, double, double, double ); +int curve_peak_time( double*, int ); int negative_binomial_draw( double, double ); int discrete_draw( int, double* ); void normalize_array( double*, int ); diff --git a/tests/data/baseline_parameters.csv b/tests/data/baseline_parameters.csv index a0a1297f5..9cc065e94 100644 --- a/tests/data/baseline_parameters.csv +++ b/tests/data/baseline_parameters.csv @@ -1,2 +1,2 @@ -rng_seed,param_id,n_total,mean_work_interactions_child,mean_work_interactions_adult,mean_work_interactions_elderly,daily_fraction_work,work_network_rewire,mean_random_interactions_child,sd_random_interactions_child,mean_random_interactions_adult,sd_random_interactions_adult,mean_random_interactions_elderly,sd_random_interactions_elderly,random_interaction_distribution,child_network_adults,elderly_network_adults,days_of_interactions,end_time,n_seed_infection,mean_infectious_period,sd_infectious_period,infectious_rate,sd_infectiousness_multiplier,mean_time_to_symptoms,sd_time_to_symptoms,mean_time_to_hospital,mean_time_to_critical,sd_time_to_critical,mean_time_to_recover,sd_time_to_recover,mean_time_to_death,sd_time_to_death,mean_time_to_susceptible_after_shift,time_to_susceptible_shift,fraction_asymptomatic_0_9,fraction_asymptomatic_10_19,fraction_asymptomatic_20_29,fraction_asymptomatic_30_39,fraction_asymptomatic_40_49,fraction_asymptomatic_50_59,fraction_asymptomatic_60_69,fraction_asymptomatic_70_79,fraction_asymptomatic_80,asymptomatic_infectious_factor,mild_fraction_0_9,mild_fraction_10_19,mild_fraction_20_29,mild_fraction_30_39,mild_fraction_40_49,mild_fraction_50_59,mild_fraction_60_69,mild_fraction_70_79,mild_fraction_80,mild_infectious_factor,mean_asymptomatic_to_recovery,sd_asymptomatic_to_recovery,household_size_1,household_size_2,household_size_3,household_size_4,household_size_5,household_size_6,population_0_9,population_10_19,population_20_29,population_30_39,population_40_49,population_50_59,population_60_69,population_70_79,population_80,daily_non_cov_symptoms_rate,relative_susceptibility_0_9,relative_susceptibility_10_19,relative_susceptibility_20_29,relative_susceptibility_30_39,relative_susceptibility_40_49,relative_susceptibility_50_59,relative_susceptibility_60_69,relative_susceptibility_70_79,relative_susceptibility_80,relative_transmission_household,relative_transmission_occupation,relative_transmission_random,hospitalised_fraction_0_9,hospitalised_fraction_10_19,hospitalised_fraction_20_29,hospitalised_fraction_30_39,hospitalised_fraction_40_49,hospitalised_fraction_50_59,hospitalised_fraction_60_69,hospitalised_fraction_70_79,hospitalised_fraction_80,critical_fraction_0_9,critical_fraction_10_19,critical_fraction_20_29,critical_fraction_30_39,critical_fraction_40_49,critical_fraction_50_59,critical_fraction_60_69,critical_fraction_70_79,critical_fraction_80,fatality_fraction_0_9,fatality_fraction_10_19,fatality_fraction_20_29,fatality_fraction_30_39,fatality_fraction_40_49,fatality_fraction_50_59,fatality_fraction_60_69,fatality_fraction_70_79,fatality_fraction_80,mean_time_hospitalised_recovery,sd_time_hospitalised_recovery,mean_time_critical_survive,sd_time_critical_survive,location_death_icu_0_9,location_death_icu_10_19,location_death_icu_20_29,location_death_icu_30_39,location_death_icu_40_49,location_death_icu_50_59,location_death_icu_60_69,location_death_icu_70_79,location_death_icu_80,quarantine_length_self,quarantine_length_traced_symptoms,quarantine_length_traced_positive,quarantine_length_positive,quarantine_dropout_self,quarantine_dropout_traced_symptoms,quarantine_dropout_traced_positive,quarantine_dropout_positive,quarantine_compliance_traced_symptoms,quarantine_compliance_traced_positive,test_on_symptoms,test_on_traced,test_release_on_negative,trace_on_symptoms,trace_on_positive,retrace_on_positive,quarantine_on_traced,traceable_interaction_fraction,tracing_network_depth,allow_clinical_diagnosis,quarantine_household_on_positive,quarantine_household_on_symptoms,quarantine_household_on_traced_positive,quarantine_household_on_traced_symptoms,quarantine_household_contacts_on_positive,quarantine_household_contacts_on_symptoms,quarantined_daily_interactions,quarantine_days,quarantine_smart_release_day,hospitalised_daily_interactions,test_insensitive_period,test_sensitive_period,test_sensitivity,test_specificity,test_order_wait,test_order_wait_priority,test_result_wait,test_result_wait_priority,priority_test_contacts_0_9,priority_test_contacts_10_19,priority_test_contacts_20_29,priority_test_contacts_30_39,priority_test_contacts_40_49,priority_test_contacts_50_59,priority_test_contacts_60_69,priority_test_contacts_70_79,priority_test_contacts_80,self_quarantine_fraction,app_users_fraction_0_9,app_users_fraction_10_19,app_users_fraction_20_29,app_users_fraction_30_39,app_users_fraction_40_49,app_users_fraction_50_59,app_users_fraction_60_69,app_users_fraction_70_79,app_users_fraction_80,app_turn_on_time,lockdown_occupation_multiplier_primary_network,lockdown_occupation_multiplier_secondary_network,lockdown_occupation_multiplier_working_network,lockdown_occupation_multiplier_retired_network,lockdown_occupation_multiplier_elderly_network,lockdown_random_network_multiplier,lockdown_house_interaction_multiplier,lockdown_time_on,lockdown_time_off,lockdown_elderly_time_on,lockdown_elderly_time_off,testing_symptoms_time_on,testing_symptoms_time_off,intervention_start_time,hospital_on,manual_trace_on,manual_trace_time_on,manual_trace_on_hospitalization,manual_trace_on_positive,manual_trace_delay,manual_trace_exclude_app_users,manual_trace_n_workers,manual_trace_interviews_per_worker_day,manual_trace_notifications_per_worker_day,manual_traceable_fraction_household,manual_traceable_fraction_occupation,manual_traceable_fraction_random,relative_susceptibility_by_interaction -1,1,1000000,10,7,3,0.5,0.1,2,2,4,4,3,3,1,0.2,0.2,10,200,5,5.5,2.14,5.18,0,5.42,2.7,5.14,2.27,2.27,12,5,11.74,8.79,180,10000,0.456,0.412,0.370,0.332,0.296,0.265,0.238,0.214,0.192,0.33,0.533,0.569,0.597,0.614,0.616,0.602,0.571,0.523,0.461,0.72,15,5,7452,9936,4416,4140,1104,552,8054000,7528000,8712000,8835000,8500000,8968000,7069000,5488000,3281000,0.002,0.35,0.69,1.03,1.03,1.03,1.03,1.27,1.52,1.52,2,1,1,0.001,0.006,0.015,0.069,0.219,0.279,0.370,0.391,0.379,0.05,0.05,0.05,0.05,0.063,0.122,0.274,0.432,0.709,0.33,0.25,0.5,0.5,0.5,0.69,0.65,0.88,1,8.75,8.75,18.8,12.21,1,1,0.9,0.9,0.8,0.8,0.4,0.4,0.05,7,14,14,14,0.02,0.04,0.03,0.01,0.5,0.9,0,0,1,0,0,0,0,0.8,0,1,0,0,0,0,0,0,0,7,0,0,3,14,0.8,0.999,1,-1,1,-1,1000,1000,1000,1000,1000,1000,1000,1000,1000,0,0.09,0.8,0.97,0.96,0.94,0.86,0.7,0.48,0.32,10000,0.29,0.29,0.29,0.29,0.29,0.29,1.5,10000,10000,10000,10000,10000,10000,0,0,0,10000,1,0,1,0,300,6,12,1,0.8,0.05,1 +rng_seed,param_id,n_total,mean_work_interactions_child,mean_work_interactions_adult,mean_work_interactions_elderly,daily_fraction_work,work_network_rewire,mean_random_interactions_child,sd_random_interactions_child,mean_random_interactions_adult,sd_random_interactions_adult,mean_random_interactions_elderly,sd_random_interactions_elderly,random_interaction_distribution,child_network_adults,elderly_network_adults,days_of_interactions,end_time,n_seed_infection,mean_infectious_period,sd_infectious_period,infectious_rate,sd_infectiousness_multiplier,mean_time_to_symptoms,sd_time_to_symptoms,mean_time_to_hospital,mean_time_to_critical,sd_time_to_critical,mean_time_to_recover,sd_time_to_recover,mean_time_to_death,sd_time_to_death,mean_time_to_susceptible_after_shift,time_to_susceptible_shift,fraction_asymptomatic_0_9,fraction_asymptomatic_10_19,fraction_asymptomatic_20_29,fraction_asymptomatic_30_39,fraction_asymptomatic_40_49,fraction_asymptomatic_50_59,fraction_asymptomatic_60_69,fraction_asymptomatic_70_79,fraction_asymptomatic_80,asymptomatic_infectious_factor,mild_fraction_0_9,mild_fraction_10_19,mild_fraction_20_29,mild_fraction_30_39,mild_fraction_40_49,mild_fraction_50_59,mild_fraction_60_69,mild_fraction_70_79,mild_fraction_80,mild_infectious_factor,mean_asymptomatic_to_recovery,sd_asymptomatic_to_recovery,household_size_1,household_size_2,household_size_3,household_size_4,household_size_5,household_size_6,population_0_9,population_10_19,population_20_29,population_30_39,population_40_49,population_50_59,population_60_69,population_70_79,population_80,daily_non_cov_symptoms_rate,relative_susceptibility_0_9,relative_susceptibility_10_19,relative_susceptibility_20_29,relative_susceptibility_30_39,relative_susceptibility_40_49,relative_susceptibility_50_59,relative_susceptibility_60_69,relative_susceptibility_70_79,relative_susceptibility_80,relative_transmission_household,relative_transmission_occupation,relative_transmission_random,hospitalised_fraction_0_9,hospitalised_fraction_10_19,hospitalised_fraction_20_29,hospitalised_fraction_30_39,hospitalised_fraction_40_49,hospitalised_fraction_50_59,hospitalised_fraction_60_69,hospitalised_fraction_70_79,hospitalised_fraction_80,critical_fraction_0_9,critical_fraction_10_19,critical_fraction_20_29,critical_fraction_30_39,critical_fraction_40_49,critical_fraction_50_59,critical_fraction_60_69,critical_fraction_70_79,critical_fraction_80,fatality_fraction_0_9,fatality_fraction_10_19,fatality_fraction_20_29,fatality_fraction_30_39,fatality_fraction_40_49,fatality_fraction_50_59,fatality_fraction_60_69,fatality_fraction_70_79,fatality_fraction_80,mean_time_hospitalised_recovery,sd_time_hospitalised_recovery,mean_time_critical_survive,sd_time_critical_survive,location_death_icu_0_9,location_death_icu_10_19,location_death_icu_20_29,location_death_icu_30_39,location_death_icu_40_49,location_death_icu_50_59,location_death_icu_60_69,location_death_icu_70_79,location_death_icu_80,quarantine_length_self,quarantine_length_traced_symptoms,quarantine_length_traced_positive,quarantine_length_positive,quarantine_dropout_self,quarantine_dropout_traced_symptoms,quarantine_dropout_traced_positive,quarantine_dropout_positive,quarantine_compliance_traced_symptoms,quarantine_compliance_traced_positive,quarantine_compliance_positive,test_on_symptoms,test_on_traced,test_release_on_negative,trace_on_symptoms,trace_on_positive,retrace_on_positive,quarantine_on_traced,traceable_interaction_fraction,tracing_network_depth,allow_clinical_diagnosis,quarantine_household_on_positive,quarantine_household_on_symptoms,quarantine_household_on_traced_positive,quarantine_household_on_traced_symptoms,quarantine_household_contacts_on_positive,quarantine_household_contacts_on_symptoms,quarantined_daily_interactions,quarantine_days,quarantine_smart_release_day,hospitalised_daily_interactions,test_insensitive_period,test_sensitive_period,test_sensitivity,test_specificity,test_order_wait,test_order_wait_priority,test_result_wait,test_result_wait_priority,priority_test_contacts_0_9,priority_test_contacts_10_19,priority_test_contacts_20_29,priority_test_contacts_30_39,priority_test_contacts_40_49,priority_test_contacts_50_59,priority_test_contacts_60_69,priority_test_contacts_70_79,priority_test_contacts_80,lateral_flow_test_order_wait,lateral_flow_test_on_symptoms,lateral_flow_test_on_traced,lateral_flow_test_repeat_count,lateral_flow_test_only,lateral_flow_test_fraction,lateral_flow_test_sensitivity,lateral_flow_test_specificity,self_quarantine_fraction,app_users_fraction_0_9,app_users_fraction_10_19,app_users_fraction_20_29,app_users_fraction_30_39,app_users_fraction_40_49,app_users_fraction_50_59,app_users_fraction_60_69,app_users_fraction_70_79,app_users_fraction_80,app_turn_on_time,lockdown_occupation_multiplier_primary_network,lockdown_occupation_multiplier_secondary_network,lockdown_occupation_multiplier_working_network,lockdown_occupation_multiplier_retired_network,lockdown_occupation_multiplier_elderly_network,lockdown_random_network_multiplier,lockdown_house_interaction_multiplier,lockdown_time_on,lockdown_time_off,lockdown_elderly_time_on,lockdown_elderly_time_off,testing_symptoms_time_on,testing_symptoms_time_off,intervention_start_time,hospital_on,manual_trace_on,manual_trace_time_on,manual_trace_on_hospitalization,manual_trace_on_positive,manual_trace_delay,manual_trace_exclude_app_users,manual_trace_n_workers,manual_trace_interviews_per_worker_day,manual_trace_notifications_per_worker_day,manual_traceable_fraction_household,manual_traceable_fraction_occupation,manual_traceable_fraction_random,relative_susceptibility_by_interaction +1,1,1000000,10,7,3,0.5,0.1,2,2,4,4,3,3,1,0.2,0.2,10,200,5,5.5,2.14,5.18,0,5.42,2.7,5.14,2.27,2.27,12,5,11.74,8.79,180,10000,0.456,0.412,0.370,0.332,0.296,0.265,0.238,0.214,0.192,0.33,0.533,0.569,0.597,0.614,0.616,0.602,0.571,0.523,0.461,0.72,15,5,7452,9936,4416,4140,1104,552,8054000,7528000,8712000,8835000,8500000,8968000,7069000,5488000,3281000,0.002,0.35,0.69,1.03,1.03,1.03,1.03,1.27,1.52,1.52,2,1,1,0.001,0.006,0.015,0.069,0.219,0.279,0.370,0.391,0.379,0.05,0.05,0.05,0.05,0.063,0.122,0.274,0.432,0.709,0.33,0.25,0.5,0.5,0.5,0.69,0.65,0.88,1,8.75,8.75,18.8,12.21,1,1,0.9,0.9,0.8,0.8,0.4,0.4,0.05,7,14,14,14,0.02,0.04,0.03,0.01,0.5,0.9,1.0,0,0,1,0,0,0,0,0.8,0,1,0,0,0,0,0,0,0,7,0,0,3,14,0.8,0.999,1,-1,1,-1,1000,1000,1000,1000,1000,1000,1000,1000,1000,1,0,0,7,0,0.5,0.95,0.999,0,0.09,0.8,0.97,0.96,0.94,0.86,0.7,0.48,0.32,10000,0.29,0.29,0.29,0.29,0.29,0.29,1.5,10000,10000,10000,10000,10000,10000,0,0,0,10000,1,0,1,0,300,6,12,1,0.8,0.05,1 diff --git a/tests/data/baseline_parameters_transpose.csv b/tests/data/baseline_parameters_transpose.csv index ebac4e0ec..8a21bc177 100644 --- a/tests/data/baseline_parameters_transpose.csv +++ b/tests/data/baseline_parameters_transpose.csv @@ -134,6 +134,7 @@ quarantine_dropout_traced_positive,0.03,,Daily probability of drop out for an in quarantine_dropout_positive,0.01,,Daily probability of drop out for an individual quarantining after a positive test result,,Active intervention parameters quarantine_compliance_traced_symptoms,0.5,,Fraction of individuals who initially comply with a quarantine notification after their contact reported symptoms,,Active intervention parameters quarantine_compliance_traced_positive,0.9,,Fraction of individuals who initially comply with a quarantine notification after their contact tested positive,,Active intervention parameters +quarantine_compliance_positive,1.0,,Fraction of individuals who initially comply with a quarantine after they test positive,,Active intervention parameters test_on_symptoms,0,,"Test individuals who show symptoms (0=no, 1=yes)",,Active intervention parameters test_on_traced,0,,"Test individuals who have been contact-traced (0=no, 1=yes)",,Active intervention parameters test_release_on_negative,1,,"Release individuals following a negative test (0=no, 1=yes)",,Active intervention parameters @@ -171,6 +172,14 @@ priority_test_contacts_50_59,1000,,Number of contacts that triggers priority tes priority_test_contacts_60_69,1000,,Number of contacts that triggers priority test for individuals aged 60-69 years old,, priority_test_contacts_70_79,1000,,Number of contacts that triggers priority test for individuals aged 70-79 years old,, priority_test_contacts_80,1000,,Number of contacts that triggers priority test for individuals aged 80+ years old,, +lateral_flow_test_order_wait,1,,Number of days to wait to receive a set of lateral flow tests,,Active intervention parameters +lateral_flow_test_on_symptoms,0,,"Test individuals with Lateral Flow Assay who show symptoms (0=no, 1=yes)",,Active intervention parameters +lateral_flow_test_on_traced,0,,"Test individuals with Lateral Flow Assay who have been contact-traced (0=no, 1=yes)",,Active intervention parameters +lateral_flow_test_repeat_count,7,,Number of daily Lateral Flow Assay tests to take in a row,,Active intervention parameters +lateral_flow_test_only,0,,"If an individual takes Lateral Flow tests, do not take PCR or quarantine until they receive a test result",,Active intervention parameters +lateral_flow_test_fraction,0.5,,"The fraction of individuals who take a Lateral Flow Assay test instead of quarantine if offered",,Active intervention parameters +lateral_flow_test_sensitivity,0.95,,Peak sensitivity of Lateral Flow Assay,,Active intervention parameters +lateral_flow_test_specificity,0.999,,Specificity of Lateral Flow Assay (at any time),,Active intervention parameters self_quarantine_fraction,0,,Proportion of people who self-quarantine upon symptoms,,Passive intervention parameters app_users_fraction_0_9,0.09,,Maximum fraction of the population with smartphones aged 0-9,OFCOM 3-5 year olds,Active intervention parameters app_users_fraction_10_19,0.8,,Maximum fraction of the population with smartphones aged 10-19,OFCOM 5-15 year olds,Active intervention parameters diff --git a/tests/test_interventions.py b/tests/test_interventions.py index 0520bc553..6178fc2f2 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -110,7 +110,7 @@ class TestClass(object): ], "test_quarantine_household_on_symptoms": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 500, end_time = 20, @@ -122,7 +122,7 @@ class TestClass(object): ], "test_trace_on_symptoms": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 500, end_time = 20, @@ -139,7 +139,7 @@ class TestClass(object): ], "test_lockdown_transmission_rates": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 10000, end_time = 3, @@ -156,7 +156,7 @@ class TestClass(object): ], "test_app_users_fraction": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 500, end_time = 20, @@ -181,7 +181,7 @@ class TestClass(object): ], "test_risk_score_household": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 500, end_time = 10, @@ -193,7 +193,7 @@ class TestClass(object): min_age_sus = 2 ), dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 500, end_time = 10, @@ -207,7 +207,7 @@ class TestClass(object): ], "test_risk_score_age": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 500, end_time = 10, @@ -223,7 +223,7 @@ class TestClass(object): ], "test_risk_score_days_since_contact": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 500, end_time = 10, @@ -236,7 +236,7 @@ class TestClass(object): days_since_contact = 2, ), dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 500, end_time = 10, @@ -251,7 +251,7 @@ class TestClass(object): ], "test_risk_score_multiple_contact": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 500, end_time = 10, @@ -269,7 +269,7 @@ class TestClass(object): required_interactions = 1 ), dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 500, end_time = 10, @@ -289,7 +289,7 @@ class TestClass(object): ], "test_quarantine_household_on_trace_positive_not_symptoms": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 100, end_time = 10, @@ -320,7 +320,7 @@ class TestClass(object): ], "test_traced_on_symptoms_quarantine_on_positive": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 100, end_time = 15, @@ -348,7 +348,7 @@ class TestClass(object): tol_sd = 3 ), dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 100, end_time = 15, @@ -378,7 +378,7 @@ class TestClass(object): ], "test_quarantined_have_trace_token" : [ dict( - test_params = dict( + test_params = dict( n_total = 20000, n_seed_infection = 100, end_time = 20, @@ -505,7 +505,7 @@ class TestClass(object): ], "test_manual_trace_params" : [ dict( - test_params = dict( + test_params = dict( n_total = 20000, n_seed_infection = 100, end_time = 30, @@ -747,7 +747,7 @@ class TestClass(object): ], "test_test_sensitivity": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 4000, end_time = 12, @@ -765,13 +765,232 @@ class TestClass(object): daily_non_cov_symptoms_rate =0.01, test_specificity = 0.9, test_insensitive_period = 3 - + ), + ) + ], + "test_lateral_flow_interventions_has_tests": [ + dict( + test_params = dict( + n_seed_infection = 400, + end_time = 10, + infectious_rate = 6, + sd_infectiousness_multiplier = 0.4, + lateral_flow_test_on_symptoms = True, + ), + ), + dict( + test_params = dict( + n_seed_infection = 400, + end_time = 10, + infectious_rate = 6, + sd_infectiousness_multiplier = 0.4, + app_turn_on_time = 0, + test_on_symptoms = True, + trace_on_positive = True, + quarantine_on_traced = False, + lateral_flow_test_on_symptoms = False, + lateral_flow_test_on_traced = True, + ), + ) + ], + "test_lateral_flow_interventions_no_tests": [ + dict( + test_params = dict( + n_seed_infection = 400, + end_time = 10, + infectious_rate = 6, + sd_infectiousness_multiplier = 0.4, + app_turn_on_time = 0, + test_on_symptoms = True, + quarantine_on_traced = True, + trace_on_symptoms = False, + trace_on_positive = False, + lateral_flow_test_on_symptoms = False, + lateral_flow_test_on_traced = True, + ), + ) + ], + "test_lateral_flow_interventions_vs_quarantine": [ + # Test on Trace + dict( + test_params = dict( + n_seed_infection = 100, + end_time = 10, + infectious_rate = 6, + sd_infectiousness_multiplier = 0.4, + app_turn_on_time = 0, + test_on_symptoms = True, + test_order_wait = 0, + test_result_wait = 0, + trace_on_symptoms = False, + trace_on_positive = True, + lateral_flow_test_order_wait = 0, + lateral_flow_test_on_symptoms = False, + lateral_flow_test_on_traced = True, + self_quarantine_fraction = 1.0, + quarantine_on_traced = True, + quarantine_compliance_positive = 1, + quarantine_compliance_traced_positive = 1, + lateral_flow_test_only = 1, + lateral_flow_test_fraction = 0.5, + ), + quarantine_expected = True, + quarantine_reason = 4, # QR_TRACE_POSITIVE + ), + # LFA only on trace + dict( + test_params = dict( + n_seed_infection = 100, + end_time = 10, + infectious_rate = 6, + sd_infectiousness_multiplier = 0.4, + app_turn_on_time = 0, + test_on_symptoms = True, + trace_on_symptoms = False, + trace_on_positive = True, + lateral_flow_test_order_wait = 0, + lateral_flow_test_on_symptoms = False, + lateral_flow_test_on_traced = True, + quarantine_on_traced = True, + self_quarantine_fraction = 1, + quarantine_compliance_positive = 1, + quarantine_compliance_traced_positive = 0.5, + quarantine_dropout_traced_positive = 0, + lateral_flow_test_sensitivity = 1, + lateral_flow_test_specificity = 1, + lateral_flow_test_repeat_count = 1, + lateral_flow_test_only = 1, + lateral_flow_test_fraction = 1, + ), + quarantine_expected = False, + quarantine_reason = 4, # QR_TRACE_POSITIVE + ), + # Both on trace + dict( + test_params = dict( + n_seed_infection = 100, + end_time = 10, + infectious_rate = 6, + sd_infectiousness_multiplier = 0.4, + app_turn_on_time = 0, + test_on_symptoms = True, + trace_on_symptoms = False, + trace_on_positive = True, + lateral_flow_test_order_wait = 0, + lateral_flow_test_on_symptoms = False, + lateral_flow_test_on_traced = True, + self_quarantine_fraction = 1, + quarantine_on_traced = True, + quarantine_compliance_positive = 1, + quarantine_compliance_traced_positive = 1, + quarantine_dropout_traced_positive = 0, + lateral_flow_test_sensitivity = 1, + lateral_flow_test_specificity = 1, + lateral_flow_test_repeat_count = 1, + lateral_flow_test_fraction = 1.0, + lateral_flow_test_only = 0, + ), + quarantine_expected = True, + quarantine_reason = 4, # QR_TRACE_POSITIVE + ), + # LFA and Quarantine on Symptoms only. + dict( + test_params = dict( + n_seed_infection = 100, + end_time = 10, + infectious_rate = 6, + sd_infectiousness_multiplier = 0.4, + test_on_symptoms = False, + trace_on_symptoms = False, + lateral_flow_test_order_wait = 0, + lateral_flow_test_on_symptoms = True, + quarantine_compliance_positive = 1, + quarantine_dropout_self = 0, + lateral_flow_test_sensitivity = 1, + lateral_flow_test_specificity = 1, + lateral_flow_test_repeat_count = 1, + lateral_flow_test_only = 0, + self_quarantine_fraction = 0.6, + lateral_flow_test_fraction = 0.6, + ), + quarantine_expected = True, + quarantine_reason = 1, # QR_SELF_SYMPTOMS + ), + # LFA only on Symptoms. + dict( + test_params = dict( + n_seed_infection = 100, + end_time = 10, + infectious_rate = 6, + sd_infectiousness_multiplier = 0.4, + test_on_symptoms = False, + trace_on_symptoms = False, + lateral_flow_test_order_wait = 0, + lateral_flow_test_on_symptoms = True, + quarantine_compliance_positive = 1, + quarantine_dropout_self = 0, + lateral_flow_test_sensitivity = 1, + lateral_flow_test_specificity = 1, + lateral_flow_test_repeat_count = 1, + lateral_flow_test_only = 1, + lateral_flow_test_fraction = 1, + self_quarantine_fraction = 1, + ), + quarantine_expected = False, + quarantine_reason = 1, # QR_SELF_SYMPTOMS + ), + ], + "test_lateral_flow_test_sensitivity": [ + dict( + test_params = dict( + n_total = 100000, + n_seed_infection = 5000, + end_time = 15, + infectious_rate = 6, + self_quarantine_fraction = 1.0, + quarantine_household_on_symptoms = False, + test_on_symptoms = False, + test_on_traced = False, + trace_on_symptoms = False, + quarantine_on_traced = False, + app_turn_on_time = 0, + daily_non_cov_symptoms_rate = 0.01, + lateral_flow_test_on_symptoms = True, + lateral_flow_test_on_traced = True, + lateral_flow_test_order_wait = 0, + lateral_flow_test_repeat_count = 7, + lateral_flow_test_sensitivity = 0.7, + lateral_flow_test_specificity = 0.9, + sd_infectiousness_multiplier = 0.4, + ), + ), + dict( + test_params = dict( + n_total = 100000, + n_seed_infection = 5000, + end_time = 15, + infectious_rate = 6, + self_quarantine_fraction = 1.0, + quarantine_household_on_symptoms = False, + test_on_symptoms = False, + test_on_traced = False, + trace_on_symptoms = False, + quarantine_on_traced = False, + app_turn_on_time = 0, + daily_non_cov_symptoms_rate = 0.01, + lateral_flow_test_on_symptoms = True, + lateral_flow_test_on_traced = True, + lateral_flow_test_order_wait = 0, + lateral_flow_test_repeat_count = 7, + lateral_flow_test_sensitivity = 0.7, + lateral_flow_test_specificity = 0.9, + sd_infectiousness_multiplier = 0.0, ), ) ], "test_recursive_testing_indirect_release": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 4000, end_time = 10, @@ -803,7 +1022,7 @@ class TestClass(object): ], "test_recursive_testing": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 4000, end_time = 10, @@ -837,7 +1056,7 @@ class TestClass(object): ], "test_recursive_testing_household_not_released": [ dict( - test_params = dict( + test_params = dict( n_total = 100000, n_seed_infection = 4000, end_time = 10, @@ -2065,7 +2284,7 @@ def test_test_sensitivity(self, test_params ): np.testing.assert_equal( p_val > lower_CI, True, "Too few false positives given the test specificity" ) np.testing.assert_equal( p_val < upper_CI, True, "Too many false positives given the test specificity" ) - # check the sensitivity in the initial period when not sensitive + # check the specificity in the initial period when not sensitive false_neg = sum( ( df_test["infected"] == True ) & ( df_test["test_status"] == 0 ) & ( df_test["test_sensitive"] == False )) true_pos = sum( ( df_test["infected"] == True ) & ( df_test["test_status"] == 1 ) & ( df_test["test_sensitive"] == False )) p_val = binom.cdf( false_neg, ( false_neg + true_pos ), test_params[ "test_specificity"] ) @@ -2074,7 +2293,7 @@ def test_test_sensitivity(self, test_params ): np.testing.assert_equal( p_val > lower_CI, True, "Too true positives in insensitive period given the test specificity" ) np.testing.assert_equal( p_val < upper_CI, True, "Too few true positives in insensitive period the test specificity" ) - # check the sensitivity in the initial period when not sensitive + # check the sensitivity in the sensitive period false_neg = sum( ( df_test["infected"] == True ) & ( df_test["test_status"] == 0 ) & ( df_test["test_sensitive"] == True )) true_pos = sum( ( df_test["infected"] == True ) & ( df_test["test_status"] == 1 ) & ( df_test["test_sensitive"] == True )) p_val = binom.cdf( true_pos, ( false_neg + true_pos ), test_params[ "test_sensitivity"] ) @@ -2084,6 +2303,175 @@ def test_test_sensitivity(self, test_params ): np.testing.assert_equal( p_val < upper_CI, True, "Too many true positives in sensitive period the test sensitivity" ) del( model ) + + def test_lateral_flow_interventions_has_tests(self, test_params): + end_time = test_params[ "end_time" ] + + params = utils.get_params_swig() + for param, value in test_params.items(): + params.set_param( param, value ) + model = utils.get_model_swig( params ) + + for time in range( end_time ): + model.one_time_step() + results = model.one_time_step_results() + + np.testing.assert_equal(results["n_lateral_flow_tests"] > 0, True, "Expected lateral flow tests not found.") + + # write files + model.write_individual_file() + model.write_transmissions() + + # read CSV's + df_indiv = pd.read_csv( constant.TEST_INDIVIDUAL_FILE, comment="#", sep=",", skipinitialspace=True ) + np.testing.assert_equal(sum(df_indiv["lateral_flow_status"] == 0) > 0, True, "No negative Lateral Flow tests found.") + np.testing.assert_equal(sum(df_indiv["lateral_flow_status"] == 1) > 0, True, "No positive Lateral Flow tests found.") + + def test_lateral_flow_interventions_no_tests(self, test_params): + end_time = test_params[ "end_time" ] + + params = utils.get_params_swig() + for param, value in test_params.items(): + params.set_param( param, value ) + model = utils.get_model_swig( params ) + + for time in range( end_time ): + model.one_time_step() + results = model.one_time_step_results() + + np.testing.assert_equal(results["n_lateral_flow_tests"], 0, "Unexpected lateral flow tests found.") + + # write files + model.write_individual_file() + model.write_transmissions() + + # read CSV's + df_indiv = pd.read_csv( constant.TEST_INDIVIDUAL_FILE, comment="#", sep=",", skipinitialspace=True ) + np.testing.assert_equal(sum(df_indiv["lateral_flow_status"] >= 0), 0, "Unexpected Lateral Flow tests found.") + + def test_lateral_flow_interventions_vs_quarantine(self, test_params, quarantine_expected, quarantine_reason): + """ + Test that we have the expected ratio between number of quarantines performed and number of LFA tests performed. + """ + end_time = test_params[ "end_time" ] + max_CI = 0.99 + upper_CI = ( 1 + max_CI ) / 2 + lower_CI = ( 1 - max_CI ) / 2 + + params = utils.get_params_swig() + for param, value in test_params.items(): + params.set_param( param, value ) + model = utils.get_model_swig( params ) + + all_test = [] + for time in range( 1, end_time + 1 ): + model.one_time_step() + + # write files + model.write_individual_file() + model.write_quarantine_reasons() + + # read CSV's + df_indiv = pd.read_csv( constant.TEST_INDIVIDUAL_FILE, comment="#", sep=",", skipinitialspace=True ) + df_indiv["time"] = time + + df_quar = pd.read_csv( constant.TEST_QUARANTINE_REASONS_FILE.substitute(T = time ), comment="#", sep=",", skipinitialspace=True ) + df_quar = df_quar[["ID","quarantine_reason"]] + + df_test = pd.merge( df_indiv, df_quar, on = "ID", how = "left") + all_test.append(df_test) + + del model + + df_test = pd.concat(all_test) + + lfa_test = sum( df_test["lateral_flow_status"] >= 0 ) + quar = sum( (df_test[ "quarantine_reason" ] == quarantine_reason ) ) + np.testing.assert_equal( lfa_test > 0, True, f"Expected existence of LFA tests." ) + np.testing.assert_equal( quar > 0, quarantine_expected, f"Expected existnce of quarantines to be {quarantine_expected}" ) + + def test_lateral_flow_test_sensitivity(self, test_params): + """ + Test that the lateral flow tests results have the required sensitivity and specificity + Make sure there sufficient true/false pos/neg and then check they + lie within the 99% confidence interval + """ + end_time = test_params[ "end_time" ] + max_CI = 0.99 + upper_CI = ( 1 + max_CI ) / 2 + lower_CI = ( 1 - max_CI ) / 2 + + params = utils.get_params_swig() + for param, value in test_params.items(): + params.set_param( param, value ) + model = utils.get_model_swig( params ) + + all_test = [] + for time in range( end_time ): + model.one_time_step() + + # write files + model.write_individual_file() + model.write_transmissions() + + # read CSV's + df_trans = pd.read_csv( constant.TEST_TRANSMISSION_FILE, sep = ",", comment = "#", skipinitialspace = True ) + df_indiv = pd.read_csv( constant.TEST_INDIVIDUAL_FILE, comment="#", sep=",", skipinitialspace=True ) + df_test = df_indiv.loc[:,["ID","lateral_flow_status"]] + df_test = df_test[ df_test["lateral_flow_status"] >= 0 ] + df_trans = df_trans.loc[:,["ID_recipient","time_infected", "time_symptomatic"]] + df_test = pd.merge( df_test, df_trans, left_on = "ID", right_on = "ID_recipient", how = "left") + df_test["time"] = time + df_test.fillna(-1, inplace=True) + all_test.append(df_test) + + # find everyone with a test result + df_test = pd.concat(all_test) + df_test["infected"] = (df_test["time_infected"]>-1) + + # check the specificity of the test + true_neg = sum( ( df_test["infected"] == False ) & ( df_test["lateral_flow_status"] == 0 ) ) + false_pos = sum( ( df_test["infected"] == False ) & ( df_test["lateral_flow_status"] == 1 ) ) + p_val = binom.cdf( true_neg, ( true_neg + false_pos ), test_params[ "lateral_flow_test_specificity"] ) + np.testing.assert_equal( true_neg > 100, True, "In-sufficient true negatives cases to test" ) + np.testing.assert_equal( false_pos > 50, True, "In-sufficient false positives cases to test" ) + np.testing.assert_equal( p_val > lower_CI, True, f"Too few false positives given the test specificity tn={true_neg}, fp={false_pos}, p={p_val}" ) + np.testing.assert_equal( p_val < upper_CI, True, f"Too many false positives given the test specificity tn={true_neg}, fp={false_pos}, p={p_val}" ) + + # check the sensitivity is monotonic on each side of the peak. + df_test[ "test_sensitive_inf" ] = ( ( df_test["time_infected"] != - 1 ) & ( df_test["time_infected"] <= df_test["time"] ) ) + df_test[ "time_since_inf" ] = ( df_test["time"] - df_test["time_infected"] ) + + true_pos = df_test[ ( df_test["infected"] == True ) & ( df_test["lateral_flow_status"] == 1 ) & ( df_test["test_sensitive_inf"] == True ) ].groupby( [ "time_since_inf" ] ).size() + false_neg = df_test[ ( df_test["infected"] == True ) & ( df_test["lateral_flow_status"] == 0 ) & ( df_test["test_sensitive_inf"] == True ) ].groupby( [ "time_since_inf" ] ).size() + sens_ratio = true_pos.divide(false_neg.add(true_pos, fill_value=0), fill_value=0) + sens_peak = sens_ratio.argmax() + + sens_diff = sens_ratio.diff() + is_sens_single_peak = np.all(sens_diff.iloc[2:sens_peak+1] >= 0) & np.all(sens_diff[sens_peak+1:-3] <= 0) + np.testing.assert_equal( is_sens_single_peak, True, f"Sensitivity does not have a single peak: {sens_diff}" ) + + # check the sensitivity at the peak. + df_test[ "symptomatic" ] = ( df_test["time_symptomatic"] > 0 ) + true_pos = df_test[ ( df_test["time_since_inf"] == 3 ) & ( df_test["symptomatic"] == True ) & ( df_test["lateral_flow_status"] == 1 ) & ( df_test["test_sensitive_inf"] == True ) ].groupby( [ "time_since_inf" ] ).size() + false_neg = df_test[ ( df_test["time_since_inf"] == 3 ) & ( df_test["symptomatic"] == True ) & ( df_test["lateral_flow_status"] == 0 ) & ( df_test["test_sensitive_inf"] == True ) ].groupby( [ "time_since_inf" ] ).size() + sens_ratio = true_pos.divide(false_neg.add(true_pos, fill_value=0), fill_value=0) + sens_peak_idx = sens_ratio.idxmax() + + np.testing.assert_equal( false_neg[sens_peak_idx] > 100, True, "In-sufficient false negatives in sensitive period to test" ) + np.testing.assert_equal( true_pos[sens_peak_idx] > 100, True, "In-sufficient true positives in sensitive period to test" ) + + sd_mult = test_params[ "sd_infectiousness_multiplier" ] + sens_exp = test_params[ "lateral_flow_test_sensitivity"] + if sd_mult: + sens_low = sens_exp - 0.675 / 2.0 * sd_mult + sens_peak = sens_ratio[sens_peak_idx] + np.testing.assert_equal( sens_low <= sens_peak <= sens_exp, True, f"Sensitivity outside expected range {sens_low} <= {sens_peak} <= {sens_exp}." ) + else: + p_val = binom.cdf( true_pos[sens_peak_idx], ( false_neg[sens_peak_idx] + true_pos[sens_peak_idx]), sens_exp ) + np.testing.assert_equal( p_val > lower_CI, True, f"Too few true positives in sensitive period given the test sensitivity s={sens_exp}: tn={true_neg}, fp={false_pos}, p={p_val}" ) + np.testing.assert_equal( p_val < upper_CI, True, f"Too many true positives in sensitive period the test sensitivity s={sens_exp}: tn={true_neg}, fp={false_pos}, p={p_val}" ) + del( model ) def test_recursive_testing_indirect_release(self, test_params ): """ @@ -2115,7 +2503,6 @@ def test_recursive_testing_indirect_release(self, test_params ): # now get those indirectly traced (i.e. household members of traced) df_indirect_trace = df_trace[ ( df_trace[ "index_ID" ] != df_trace[ "traced_from_ID" ] ) & ( df_trace[ "index_ID" ] != df_trace[ "traced_ID" ] )] df = pd.merge( df_indirect_trace, df_direct_trace, left_on = ["index_ID", "traced_from_ID"], right_on = ["index_ID", "direct_traced_ID"], how = "left") - df.fillna(-1, inplace=True) # test that everyone who has been indirectly traced, the directly traced person is still on the trace list np.testing.assert_equal( len( df_indirect_trace) > 500, True, "In-sufficient indirect-traced to test" ) @@ -2734,4 +3121,3 @@ def test_vaccinate_schedule( self, test_params, n_to_seed, vaccine_type, fractio else : sd = sqrt( expected * max( 1 - expected / n_age, 0.01 ) ) np.testing.assert_allclose(n_vac[0], expected, atol = 3 * sd, err_msg = "incorrect number vaccinated by age group" ) - \ No newline at end of file From e5fc9cf174651378462c18237cf287d3b8a6a6eb Mon Sep 17 00:00:00 2001 From: Matthew Abueg Date: Wed, 27 Jan 2021 16:28:41 -0800 Subject: [PATCH 02/10] Spacing and comment cleanup --- src/interventions.c | 32 ++++++++--------- src/params.c | 4 +-- tests/test_interventions.py | 72 ++++++++++++++++++++----------------- 3 files changed, 57 insertions(+), 51 deletions(-) diff --git a/src/interventions.c b/src/interventions.c index 443491cc2..c35329a8d 100644 --- a/src/interventions.c +++ b/src/interventions.c @@ -748,12 +748,12 @@ void intervention_test_take( model *model, individual *indiv ) /***************************************************************************************** * Name: intervention_lateral_flow_test_order -* Description: Order a test for either today or a future date +* Description: Order a lateral flow test for either today or a future date * Returns: void ******************************************************************************************/ void intervention_lateral_flow_test_order( model *model, individual *indiv, int time ) { - if( indiv->lateral_flow_test_result != TEST_ORDERED && !(indiv->infection_events->is_case) ) + if( indiv->lateral_flow_test_result != TEST_ORDERED && !indiv->infection_events->is_case ) { indiv->lateral_flow_test_capacity = model->params->lateral_flow_test_repeat_count; add_individual_to_event_list( model, LATERAL_FLOW_TEST_TAKE, indiv, time ); @@ -771,7 +771,7 @@ void intervention_lateral_flow_test_order( model *model, individual *indiv, int ******************************************************************************************/ void intervention_lateral_flow_test_take( model *model, individual *indiv ) { - if (indiv->lateral_flow_test_capacity <= 0) return; + if ( indiv->lateral_flow_test_capacity <= 0 ) return; indiv->lateral_flow_test_capacity--; int result_time = model->time; @@ -781,10 +781,10 @@ void intervention_lateral_flow_test_take( model *model, individual *indiv ) if( time_infected != UNKNOWN ) { int infection_type = 0; - for(int bucket = 0; bucket < N_NEWLY_INFECTED_STATES; bucket++) + for( int bucket = 0; bucket < N_NEWLY_INFECTED_STATES; bucket++ ) { - infection_type = NEWLY_INFECTED_STATES[bucket]; - if( indiv->infection_events->times[NEWLY_INFECTED_STATES[bucket]] == time_infected ) + infection_type = NEWLY_INFECTED_STATES[ bucket ]; + if( indiv->infection_events->times[ NEWLY_INFECTED_STATES[ bucket ] ] == time_infected ) break; } @@ -795,8 +795,8 @@ void intervention_lateral_flow_test_take( model *model, individual *indiv ) double sensitivity = 0; double I = 0; double V = 0; - const int peak_time = model->event_lists[LATERAL_FLOW_TEST].infectious_peak_time; - const double *infectious_curve = model->event_lists[infection_type].infectious_curve[LATERAL_FLOW_TEST]; + const int peak_time = model->event_lists[ LATERAL_FLOW_TEST ].infectious_peak_time; + const double *infectious_curve = model->event_lists[ infection_type ].infectious_curve[ LATERAL_FLOW_TEST ]; const double g = 1.0/8; const double b = 1.0/6; @@ -810,7 +810,7 @@ void intervention_lateral_flow_test_take( model *model, individual *indiv ) I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * infectious_factor; V = log( I ) / ( g + b ) + log( infectious_curve[ peak_time ] * indiv->infectiousness_multiplier * - infectious_factor ) * ( 1/g - 1/( g + b ) ); + infectious_factor ) * ( 1 / g - 1 / ( g + b ) ); } if ( V < 0 ) @@ -853,10 +853,10 @@ double calculate_mean_lfa_sensitivity( model *model, int type ) individual * indiv; for( int idx = 0; idx < model->params->n_total; idx++ ) { - indiv = &(model->population[idx]); + indiv = &( model->population[ idx ] ); if( indiv->lateral_flow_test_sensitivity >= 0 && - (type == NO_EVENT || - indiv->infection_events->times[type] == time_infected( indiv ))) + ( type == NO_EVENT || + indiv->infection_events->times[ type ] == time_infected( indiv ))) { total += indiv->lateral_flow_test_sensitivity; cnt++; @@ -1072,7 +1072,7 @@ void intervention_quarantine_household( if( lateral_flow_test ) intervention_lateral_flow_test_order( model, contact, model->time + params->lateral_flow_test_order_wait ); - if( !(lateral_flow_test && params->lateral_flow_test_only) && quarantine && params->test_on_traced && ( index_token->index_status == POSITIVE_TEST ) ) + if( !( lateral_flow_test && params->lateral_flow_test_only ) && quarantine && params->test_on_traced && ( index_token->index_status == POSITIVE_TEST ) ) { time_test = max( model->time + params->test_order_wait, contact_time + params->test_insensitive_period ); intervention_test_order( model, contact, time_test ); @@ -1154,7 +1154,7 @@ void intervention_index_case_symptoms_to_positive( if( lateral_flow_test ) intervention_lateral_flow_test_order( model, contact, model->time + params->lateral_flow_test_order_wait ); - if( !(params->lateral_flow_test_only && lateral_flow_test) && gsl_ran_bernoulli( rng, params->quarantine_compliance_traced_positive ) ) + if( !( params->lateral_flow_test_only && lateral_flow_test ) && gsl_ran_bernoulli( rng, params->quarantine_compliance_traced_positive ) ) { time_quarantine = contact_time + sample_transition_time( model, TRACED_QUARANTINE_POSITIVE ); } else { @@ -1212,7 +1212,7 @@ void intervention_on_symptoms( model *model, individual *indiv ) trace_token *index_token = index_trace_token( model, indiv ); index_token->index_status = SYMPTOMS_ONLY; - if (params->lateral_flow_test_only && lateral_flow_test) + if ( params->lateral_flow_test_only && lateral_flow_test ) time_event = model->time; else time_event = model->time + sample_transition_time( model, SYMPTOMATIC_QUARANTINE ); @@ -1376,7 +1376,7 @@ void intervention_on_traced( int time_event = model->time; int quarantine; - if (params->lateral_flow_test_only && lateral_flow_test) + if ( params->lateral_flow_test_only && lateral_flow_test ) { time_event = model->time; } diff --git a/src/params.c b/src/params.c index 427487ae6..682e39cbb 100644 --- a/src/params.c +++ b/src/params.c @@ -24,8 +24,8 @@ void initialize_params( parameters *params ) { params->demo_house = NULL; params->occupation_network_table = NULL; - params->relative_transmission[LATERAL_FLOW_TEST] = 1; - params->relative_transmission_used[LATERAL_FLOW_TEST] = 1; + params->relative_transmission[ LATERAL_FLOW_TEST ] = 1; + params->relative_transmission_used[ LATERAL_FLOW_TEST ] = 1; } /***************************************************************************************** diff --git a/tests/test_interventions.py b/tests/test_interventions.py index 6178fc2f2..eb1b8001b 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -2305,6 +2305,9 @@ def test_test_sensitivity(self, test_params ): del( model ) def test_lateral_flow_interventions_has_tests(self, test_params): + """ + Test that we do have lateral flow tests if they are enabled. + """ end_time = test_params[ "end_time" ] params = utils.get_params_swig() @@ -2316,7 +2319,7 @@ def test_lateral_flow_interventions_has_tests(self, test_params): model.one_time_step() results = model.one_time_step_results() - np.testing.assert_equal(results["n_lateral_flow_tests"] > 0, True, "Expected lateral flow tests not found.") + np.testing.assert_equal( results[ "n_lateral_flow_tests" ] > 0, True, "Expected lateral flow tests not found." ) # write files model.write_individual_file() @@ -2324,10 +2327,13 @@ def test_lateral_flow_interventions_has_tests(self, test_params): # read CSV's df_indiv = pd.read_csv( constant.TEST_INDIVIDUAL_FILE, comment="#", sep=",", skipinitialspace=True ) - np.testing.assert_equal(sum(df_indiv["lateral_flow_status"] == 0) > 0, True, "No negative Lateral Flow tests found.") - np.testing.assert_equal(sum(df_indiv["lateral_flow_status"] == 1) > 0, True, "No positive Lateral Flow tests found.") + np.testing.assert_equal( sum( df_indiv[ "lateral_flow_status" ] == 0 ) > 0, True, "No negative Lateral Flow tests found." ) + np.testing.assert_equal( sum( df_indiv[ "lateral_flow_status" ] == 1 ) > 0, True, "No positive Lateral Flow tests found." ) def test_lateral_flow_interventions_no_tests(self, test_params): + """ + Test that we do not have lateral flow tests if they are disabled. + """ end_time = test_params[ "end_time" ] params = utils.get_params_swig() @@ -2339,7 +2345,7 @@ def test_lateral_flow_interventions_no_tests(self, test_params): model.one_time_step() results = model.one_time_step_results() - np.testing.assert_equal(results["n_lateral_flow_tests"], 0, "Unexpected lateral flow tests found.") + np.testing.assert_equal( results[ "n_lateral_flow_tests" ], 0, "Unexpected lateral flow tests found." ) # write files model.write_individual_file() @@ -2347,11 +2353,11 @@ def test_lateral_flow_interventions_no_tests(self, test_params): # read CSV's df_indiv = pd.read_csv( constant.TEST_INDIVIDUAL_FILE, comment="#", sep=",", skipinitialspace=True ) - np.testing.assert_equal(sum(df_indiv["lateral_flow_status"] >= 0), 0, "Unexpected Lateral Flow tests found.") + np.testing.assert_equal( sum( df_indiv[ "lateral_flow_status" ] >= 0 ), 0, "Unexpected Lateral Flow tests found." ) def test_lateral_flow_interventions_vs_quarantine(self, test_params, quarantine_expected, quarantine_reason): """ - Test that we have the expected ratio between number of quarantines performed and number of LFA tests performed. + Test that we have the expected lateral flow tests and quarantines. """ end_time = test_params[ "end_time" ] max_CI = 0.99 @@ -2376,17 +2382,17 @@ def test_lateral_flow_interventions_vs_quarantine(self, test_params, quarantine_ df_indiv["time"] = time df_quar = pd.read_csv( constant.TEST_QUARANTINE_REASONS_FILE.substitute(T = time ), comment="#", sep=",", skipinitialspace=True ) - df_quar = df_quar[["ID","quarantine_reason"]] + df_quar = df_quar[ [ "ID", "quarantine_reason" ] ] - df_test = pd.merge( df_indiv, df_quar, on = "ID", how = "left") - all_test.append(df_test) + df_test = pd.merge( df_indiv, df_quar, on = "ID", how = "left" ) + all_test.append( df_test ) del model - df_test = pd.concat(all_test) + df_test = pd.concat( all_test ) lfa_test = sum( df_test["lateral_flow_status"] >= 0 ) - quar = sum( (df_test[ "quarantine_reason" ] == quarantine_reason ) ) + quar = sum( ( df_test[ "quarantine_reason" ] == quarantine_reason ) ) np.testing.assert_equal( lfa_test > 0, True, f"Expected existence of LFA tests." ) np.testing.assert_equal( quar > 0, quarantine_expected, f"Expected existnce of quarantines to be {quarantine_expected}" ) @@ -2417,21 +2423,21 @@ def test_lateral_flow_test_sensitivity(self, test_params): # read CSV's df_trans = pd.read_csv( constant.TEST_TRANSMISSION_FILE, sep = ",", comment = "#", skipinitialspace = True ) df_indiv = pd.read_csv( constant.TEST_INDIVIDUAL_FILE, comment="#", sep=",", skipinitialspace=True ) - df_test = df_indiv.loc[:,["ID","lateral_flow_status"]] - df_test = df_test[ df_test["lateral_flow_status"] >= 0 ] - df_trans = df_trans.loc[:,["ID_recipient","time_infected", "time_symptomatic"]] + df_test = df_indiv.loc[ :, [ "ID", "lateral_flow_status" ] ] + df_test = df_test[ df_test[ "lateral_flow_status" ] >= 0 ] + df_trans = df_trans.loc[ :, [ "ID_recipient", "time_infected", "time_symptomatic" ] ] df_test = pd.merge( df_test, df_trans, left_on = "ID", right_on = "ID_recipient", how = "left") - df_test["time"] = time - df_test.fillna(-1, inplace=True) - all_test.append(df_test) + df_test[ "time" ] = time + df_test.fillna( -1, inplace=True ) + all_test.append( df_test ) # find everyone with a test result - df_test = pd.concat(all_test) - df_test["infected"] = (df_test["time_infected"]>-1) + df_test = pd.concat( all_test ) + df_test["infected"] = ( df_test["time_infected"] > -1 ) # check the specificity of the test - true_neg = sum( ( df_test["infected"] == False ) & ( df_test["lateral_flow_status"] == 0 ) ) - false_pos = sum( ( df_test["infected"] == False ) & ( df_test["lateral_flow_status"] == 1 ) ) + true_neg = sum( ( df_test[ "infected" ] == False ) & ( df_test[ "lateral_flow_status" ] == 0 ) ) + false_pos = sum( ( df_test[ "infected" ] == False ) & ( df_test[ "lateral_flow_status" ] == 1 ) ) p_val = binom.cdf( true_neg, ( true_neg + false_pos ), test_params[ "lateral_flow_test_specificity"] ) np.testing.assert_equal( true_neg > 100, True, "In-sufficient true negatives cases to test" ) np.testing.assert_equal( false_pos > 50, True, "In-sufficient false positives cases to test" ) @@ -2439,27 +2445,27 @@ def test_lateral_flow_test_sensitivity(self, test_params): np.testing.assert_equal( p_val < upper_CI, True, f"Too many false positives given the test specificity tn={true_neg}, fp={false_pos}, p={p_val}" ) # check the sensitivity is monotonic on each side of the peak. - df_test[ "test_sensitive_inf" ] = ( ( df_test["time_infected"] != - 1 ) & ( df_test["time_infected"] <= df_test["time"] ) ) - df_test[ "time_since_inf" ] = ( df_test["time"] - df_test["time_infected"] ) + df_test[ "test_sensitive_inf" ] = ( ( df_test[ "time_infected" ] != - 1 ) & ( df_test[ "time_infected" ] <= df_test[ "time" ] ) ) + df_test[ "time_since_inf" ] = ( df_test[ "time" ] - df_test[ "time_infected" ] ) - true_pos = df_test[ ( df_test["infected"] == True ) & ( df_test["lateral_flow_status"] == 1 ) & ( df_test["test_sensitive_inf"] == True ) ].groupby( [ "time_since_inf" ] ).size() - false_neg = df_test[ ( df_test["infected"] == True ) & ( df_test["lateral_flow_status"] == 0 ) & ( df_test["test_sensitive_inf"] == True ) ].groupby( [ "time_since_inf" ] ).size() - sens_ratio = true_pos.divide(false_neg.add(true_pos, fill_value=0), fill_value=0) + true_pos = df_test[ ( df_test[ "infected" ] == True ) & ( df_test[ "lateral_flow_status" ] == 1 ) & ( df_test[ "test_sensitive_inf" ] == True ) ].groupby( [ "time_since_inf" ] ).size() + false_neg = df_test[ ( df_test[ "infected" ] == True ) & ( df_test[ "lateral_flow_status" ] == 0 ) & ( df_test[ "test_sensitive_inf" ] == True ) ].groupby( [ "time_since_inf" ] ).size() + sens_ratio = true_pos.divide( false_neg.add( true_pos, fill_value=0 ), fill_value=0 ) sens_peak = sens_ratio.argmax() sens_diff = sens_ratio.diff() - is_sens_single_peak = np.all(sens_diff.iloc[2:sens_peak+1] >= 0) & np.all(sens_diff[sens_peak+1:-3] <= 0) + is_sens_single_peak = np.all( sens_diff.iloc[ 2 : sens_peak + 1 ] >= 0 ) & np.all( sens_diff[ sens_peak + 1 : -3 ] <= 0 ) np.testing.assert_equal( is_sens_single_peak, True, f"Sensitivity does not have a single peak: {sens_diff}" ) # check the sensitivity at the peak. df_test[ "symptomatic" ] = ( df_test["time_symptomatic"] > 0 ) - true_pos = df_test[ ( df_test["time_since_inf"] == 3 ) & ( df_test["symptomatic"] == True ) & ( df_test["lateral_flow_status"] == 1 ) & ( df_test["test_sensitive_inf"] == True ) ].groupby( [ "time_since_inf" ] ).size() - false_neg = df_test[ ( df_test["time_since_inf"] == 3 ) & ( df_test["symptomatic"] == True ) & ( df_test["lateral_flow_status"] == 0 ) & ( df_test["test_sensitive_inf"] == True ) ].groupby( [ "time_since_inf" ] ).size() - sens_ratio = true_pos.divide(false_neg.add(true_pos, fill_value=0), fill_value=0) + true_pos = df_test[ ( df_test[ "time_since_inf" ] == 3 ) & ( df_test[ "symptomatic" ] == True ) & ( df_test[ "lateral_flow_status" ] == 1 ) & ( df_test[ "test_sensitive_inf" ] == True ) ].groupby( [ "time_since_inf" ] ).size() + false_neg = df_test[ ( df_test[ "time_since_inf" ] == 3 ) & ( df_test[ "symptomatic" ] == True ) & ( df_test[ "lateral_flow_status" ] == 0 ) & ( df_test[ "test_sensitive_inf" ] == True ) ].groupby( [ "time_since_inf" ] ).size() + sens_ratio = true_pos.divide( false_neg.add( true_pos, fill_value=0 ), fill_value=0 ) sens_peak_idx = sens_ratio.idxmax() - np.testing.assert_equal( false_neg[sens_peak_idx] > 100, True, "In-sufficient false negatives in sensitive period to test" ) - np.testing.assert_equal( true_pos[sens_peak_idx] > 100, True, "In-sufficient true positives in sensitive period to test" ) + np.testing.assert_equal( false_neg[ sens_peak_idx ] > 100, True, "In-sufficient false negatives in sensitive period to test" ) + np.testing.assert_equal( true_pos[ sens_peak_idx ] > 100, True, "In-sufficient true positives in sensitive period to test" ) sd_mult = test_params[ "sd_infectiousness_multiplier" ] sens_exp = test_params[ "lateral_flow_test_sensitivity"] @@ -2468,7 +2474,7 @@ def test_lateral_flow_test_sensitivity(self, test_params): sens_peak = sens_ratio[sens_peak_idx] np.testing.assert_equal( sens_low <= sens_peak <= sens_exp, True, f"Sensitivity outside expected range {sens_low} <= {sens_peak} <= {sens_exp}." ) else: - p_val = binom.cdf( true_pos[sens_peak_idx], ( false_neg[sens_peak_idx] + true_pos[sens_peak_idx]), sens_exp ) + p_val = binom.cdf( true_pos[ sens_peak_idx ], ( false_neg[ sens_peak_idx ] + true_pos[ sens_peak_idx ] ), sens_exp ) np.testing.assert_equal( p_val > lower_CI, True, f"Too few true positives in sensitive period given the test sensitivity s={sens_exp}: tn={true_neg}, fp={false_pos}, p={p_val}" ) np.testing.assert_equal( p_val < upper_CI, True, f"Too many true positives in sensitive period the test sensitivity s={sens_exp}: tn={true_neg}, fp={false_pos}, p={p_val}" ) del( model ) From 066381c1033773825d5cf28c4d843b118c6732cb Mon Sep 17 00:00:00 2001 From: Matthew Abueg Date: Wed, 27 Jan 2021 17:43:19 -0800 Subject: [PATCH 03/10] fix merge bug --- src/model.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/model.c b/src/model.c index 2d4f4000a..8f020c660 100644 --- a/src/model.c +++ b/src/model.c @@ -64,7 +64,6 @@ model* new_model( parameters *params ) set_up_event_list( model_ptr, params, type ); set_up_counters( model_ptr ); - set_up_population( model_ptr ); set_up_household_distribution( model_ptr ); set_up_allocate_work_places( model_ptr ); if( params->hospital_on ) From 6889d65d779dbb7d2f4a2e0d437cb5bb955e970b Mon Sep 17 00:00:00 2001 From: Matthew Abueg Date: Wed, 27 Jan 2021 18:34:56 -0800 Subject: [PATCH 04/10] updating gsl import syntax --- src/interventions.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interventions.c b/src/interventions.c index c35329a8d..65b10cbfd 100644 --- a/src/interventions.c +++ b/src/interventions.c @@ -7,13 +7,13 @@ #include "model.h" #include "individual.h" -#include "gsl/gsl_randist.h" #include "utilities.h" #include "constant.h" #include "params.h" #include "network.h" #include "disease.h" #include "interventions.h" +#include #include #include #include From 4295d032b001e0037e0fa62ac2ceb5840fdfbdcf Mon Sep 17 00:00:00 2001 From: Matthew Abueg Date: Wed, 10 Feb 2021 10:57:43 -0800 Subject: [PATCH 05/10] clean up model deletions in tests --- tests/test_interventions.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_interventions.py b/tests/test_interventions.py index eb1b8001b..c1e1e50e5 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -2325,6 +2325,8 @@ def test_lateral_flow_interventions_has_tests(self, test_params): model.write_individual_file() model.write_transmissions() + del( model ) + # read CSV's df_indiv = pd.read_csv( constant.TEST_INDIVIDUAL_FILE, comment="#", sep=",", skipinitialspace=True ) np.testing.assert_equal( sum( df_indiv[ "lateral_flow_status" ] == 0 ) > 0, True, "No negative Lateral Flow tests found." ) @@ -2351,6 +2353,8 @@ def test_lateral_flow_interventions_no_tests(self, test_params): model.write_individual_file() model.write_transmissions() + del( model ) + # read CSV's df_indiv = pd.read_csv( constant.TEST_INDIVIDUAL_FILE, comment="#", sep=",", skipinitialspace=True ) np.testing.assert_equal( sum( df_indiv[ "lateral_flow_status" ] >= 0 ), 0, "Unexpected Lateral Flow tests found." ) @@ -2387,7 +2391,7 @@ def test_lateral_flow_interventions_vs_quarantine(self, test_params, quarantine_ df_test = pd.merge( df_indiv, df_quar, on = "ID", how = "left" ) all_test.append( df_test ) - del model + del( model ) df_test = pd.concat( all_test ) @@ -2477,6 +2481,7 @@ def test_lateral_flow_test_sensitivity(self, test_params): p_val = binom.cdf( true_pos[ sens_peak_idx ], ( false_neg[ sens_peak_idx ] + true_pos[ sens_peak_idx ] ), sens_exp ) np.testing.assert_equal( p_val > lower_CI, True, f"Too few true positives in sensitive period given the test sensitivity s={sens_exp}: tn={true_neg}, fp={false_pos}, p={p_val}" ) np.testing.assert_equal( p_val < upper_CI, True, f"Too many true positives in sensitive period the test sensitivity s={sens_exp}: tn={true_neg}, fp={false_pos}, p={p_val}" ) + del( model ) def test_recursive_testing_indirect_release(self, test_params ): @@ -2541,6 +2546,8 @@ def test_manual_trace_params(self, test_params, time_steps_test ): all_pos = all_pos.append(df_inter[ df_inter[ "manual_traceable" ] == 1 ] ) model.write_trace_tokens() + del( model ) + np.testing.assert_equal( len( all_pos ) > 0, True, "expected manual traces do not exist" ) def test_recursive_testing(self, test_params ): From af1a8ecc384492e7954859910906d0255209b6af Mon Sep 17 00:00:00 2001 From: Matthew Abueg Date: Fri, 12 Feb 2021 13:10:20 -0800 Subject: [PATCH 06/10] cleaning up constants and ward map; making hospital test more stable --- src/constant.c | 1 + src/constant.h | 2 +- tests/constant.py | 3 ++- tests/hospital/test_hospital_logic.py | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/constant.c b/src/constant.c index fd70a5058..71efe4ead 100644 --- a/src/constant.c +++ b/src/constant.c @@ -95,6 +95,7 @@ const int EVENT_TYPE_TO_WARD_MAP[N_EVENT_TYPES] = { NOT_IN_HOSPITAL, NOT_IN_HOSPITAL, NOT_IN_HOSPITAL, + NOT_IN_HOSPITAL, NOT_IN_HOSPITAL }; diff --git a/src/constant.h b/src/constant.h index 3bb3fb164..7fb5e813f 100644 --- a/src/constant.h +++ b/src/constant.h @@ -30,7 +30,6 @@ enum EVENT_TYPES{ DEATH, QUARANTINED, QUARANTINE_RELEASE, - LATERAL_FLOW_TEST_TAKE, TEST_TAKE, TEST_RESULT, CASE, @@ -46,6 +45,7 @@ enum EVENT_TYPES{ TRANSITION_TO_CRITICAL, VACCINE_PROTECT, VACCINE_WANE, + LATERAL_FLOW_TEST_TAKE, N_EVENT_TYPES }; #define N_NEWLY_INFECTED_STATES 3 diff --git a/tests/constant.py b/tests/constant.py index 4adf92b81..4eb9bbd69 100755 --- a/tests/constant.py +++ b/tests/constant.py @@ -66,7 +66,8 @@ class EVENT_TYPES(Enum): MANUAL_CONTACT_TRACING = 23 TRANSITION_TO_HOSPITAL = 24 TRANSITION_TO_CRITICAL = 25 - N_EVENT_TYPES = 26 + LATERAL_FLOW_TEST_TAKE = 26 + N_EVENT_TYPES = 27 # Age groups AGE_0_9 = 0 diff --git a/tests/hospital/test_hospital_logic.py b/tests/hospital/test_hospital_logic.py index d573c7b45..ff4c89925 100644 --- a/tests/hospital/test_hospital_logic.py +++ b/tests/hospital/test_hospital_logic.py @@ -182,7 +182,7 @@ def test_interaction_type_representative(self): # Adjust baseline parameter params = ParameterSet(constant.TEST_DATA_FILE, line_number=1) params.set_param("n_total", 20000) - params.set_param("n_seed_infection", 1000) + params.set_param("n_seed_infection", 2000) params.set_param("hospital_on", 1) params.set_param("end_time", 30) params.write_params(constant.TEST_DATA_FILE) @@ -205,4 +205,4 @@ def test_interaction_type_representative(self): list_this_simulation_interaction_types = df_interaction_output.type.unique() list_this_simulation_interaction_types.sort() - np.testing.assert_equal( list_all_interaction_types, list_this_simulation_interaction_types) \ No newline at end of file + np.testing.assert_equal( list_all_interaction_types, list_this_simulation_interaction_types) From 7cd63585c84f92f062051136ea2da1282312f616 Mon Sep 17 00:00:00 2001 From: Matthew Abueg Date: Fri, 26 Feb 2021 15:27:37 -0800 Subject: [PATCH 07/10] LFA Cleanup * Breaking out lfa test helpers. * Gating computation on performing LFA. * Condensing redundancy. --- src/disease.c | 36 ++------------- src/interventions.c | 110 ++++++++++++++++++++++++++++---------------- src/model.c | 6 ++- src/model.h | 2 + 4 files changed, 81 insertions(+), 73 deletions(-) diff --git a/src/disease.c b/src/disease.c index 40f7f6bae..ed4911f87 100644 --- a/src/disease.c +++ b/src/disease.c @@ -163,38 +163,12 @@ void set_up_infectious_curves( model *model ) params->sd_infectious_period, infectious_rate * type_factor ); }; - - model->event_lists[PRESYMPTOMATIC].infectious_peak_time = curve_peak_time( - model->event_lists[PRESYMPTOMATIC].infectious_curve[LATERAL_FLOW_TEST], - MAX_INFECTIOUS_PERIOD ); - - model->event_lists[PRESYMPTOMATIC_MILD].infectious_peak_time = curve_peak_time( - model->event_lists[PRESYMPTOMATIC_MILD].infectious_curve[LATERAL_FLOW_TEST], - MAX_INFECTIOUS_PERIOD ); - - model->event_lists[ASYMPTOMATIC].infectious_peak_time = curve_peak_time( - model->event_lists[ASYMPTOMATIC].infectious_curve[LATERAL_FLOW_TEST], - MAX_INFECTIOUS_PERIOD ); - - model->event_lists[SYMPTOMATIC].infectious_peak_time = curve_peak_time( - model->event_lists[SYMPTOMATIC].infectious_curve[LATERAL_FLOW_TEST], - MAX_INFECTIOUS_PERIOD ); - - model->event_lists[SYMPTOMATIC_MILD].infectious_peak_time = curve_peak_time( - model->event_lists[SYMPTOMATIC_MILD].infectious_curve[LATERAL_FLOW_TEST], - MAX_INFECTIOUS_PERIOD ); - - model->event_lists[HOSPITALISED].infectious_peak_time = curve_peak_time( - model->event_lists[HOSPITALISED].infectious_curve[LATERAL_FLOW_TEST], - MAX_INFECTIOUS_PERIOD ); - - model->event_lists[HOSPITALISED_RECOVERING].infectious_peak_time = curve_peak_time( - model->event_lists[HOSPITALISED_RECOVERING].infectious_curve[LATERAL_FLOW_TEST], - MAX_INFECTIOUS_PERIOD ); - - model->event_lists[CRITICAL].infectious_peak_time = curve_peak_time( - model->event_lists[CRITICAL].infectious_curve[LATERAL_FLOW_TEST], + for( type = 0; type < N_NEWLY_INFECTED_STATES; type++ ) + { + model->event_lists[NEWLY_INFECTED_STATES[type]].infectious_peak_time = curve_peak_time( + model->event_lists[NEWLY_INFECTED_STATES[type]].infectious_curve[LATERAL_FLOW_TEST], MAX_INFECTIOUS_PERIOD ); + } } /***************************************************************************************** * Name: transmit_virus_by_type diff --git a/src/interventions.c b/src/interventions.c index 65b10cbfd..7cf503e1b 100644 --- a/src/interventions.c +++ b/src/interventions.c @@ -354,12 +354,15 @@ void update_intervention_policy( model *model, int time ) model->manual_trace_notification_quota = params->manual_trace_n_workers * params->manual_trace_notifications_per_worker_day; } - for( int idx = 0; idx < model->params->n_total; idx++ ) + if ( model->n_lateral_flow_tests > 0 ) { - if( model->population[ idx ].lateral_flow_test_result >= 0 ) + for( int idx = 0; idx < model->params->n_total; idx++ ) { - model->population[ idx ].lateral_flow_test_result = NO_TEST; - model->population[ idx ].lateral_flow_test_sensitivity = NO_TEST; + if( model->population[ idx ].lateral_flow_test_result >= 0 ) + { + model->population[ idx ].lateral_flow_test_result = NO_TEST; + model->population[ idx ].lateral_flow_test_sensitivity = NO_TEST; + } } } } @@ -761,6 +764,66 @@ void intervention_lateral_flow_test_order( model *model, individual *indiv, int } } +/***************************************************************************************** +* Name: infectious_state +* Description: Get the infectious state for the given time infected. +* Returns: int +******************************************************************************************/ +int infectious_state( individual* indiv, int time_infected ) +{ + for( int bucket = 0; bucket < N_NEWLY_INFECTED_STATES; bucket++ ) + { + if( indiv->infection_events->times[ NEWLY_INFECTED_STATES[ bucket ] ] == time_infected ) + return NEWLY_INFECTED_STATES[ bucket ]; + } + return 0; +} +/***************************************************************************************** +* Name: lfa_sensitivity +* Description: Calculates the lfa sensitivity for a given individual. +* +* Returns the test sensitivity if the individual is in the +* sensitive period, -1 otherwise. +* Returns: double +******************************************************************************************/ +double lfa_sensitivity( model *model, individual *indiv ) +{ + int time_infected = time_infected( indiv ); + int infection_type = infectious_state( indiv, time_infected ); + + time_infected = model->time - time_infected; + + const double infectious_factor = 1/.0802 * exp(1); + + double sensitivity = 0; + double I = 0; + double V = 0; + const int peak_time = model->event_lists[ LATERAL_FLOW_TEST ].infectious_peak_time; + const double *infectious_curve = model->event_lists[ infection_type ].infectious_curve[ LATERAL_FLOW_TEST ]; + const double g = 1.0/8; + const double b = 1.0/6; + + if( time_infected <= peak_time ) + { + I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * infectious_factor; + V = log( I ) / g; + } + else + { + I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * infectious_factor; + V = log( I ) / ( g + b ) + log( infectious_curve[ peak_time ] * + indiv->infectiousness_multiplier * + infectious_factor ) * ( 1 / g - 1 / ( g + b ) ); + } + + if ( V < 0 ) + return -1; + + sensitivity = 1 / ( 1 + exp( -V ) ); + sensitivity = max( 0, min( model->params->lateral_flow_test_sensitivity , sensitivity ) ); + return sensitivity; +} + /***************************************************************************************** * Name: intervention_lateral_flow_test_take * Description: An individual takes a lateral flow test @@ -773,6 +836,7 @@ void intervention_lateral_flow_test_take( model *model, individual *indiv ) { if ( indiv->lateral_flow_test_capacity <= 0 ) return; indiv->lateral_flow_test_capacity--; + model->n_lateral_flow_tests++; int result_time = model->time; @@ -780,47 +844,13 @@ void intervention_lateral_flow_test_take( model *model, individual *indiv ) if( time_infected != UNKNOWN ) { - int infection_type = 0; - for( int bucket = 0; bucket < N_NEWLY_INFECTED_STATES; bucket++ ) - { - infection_type = NEWLY_INFECTED_STATES[ bucket ]; - if( indiv->infection_events->times[ NEWLY_INFECTED_STATES[ bucket ] ] == time_infected ) - break; - } - - time_infected = model->time - time_infected; - - const double infectious_factor = 1/.0802 * exp(1); - - double sensitivity = 0; - double I = 0; - double V = 0; - const int peak_time = model->event_lists[ LATERAL_FLOW_TEST ].infectious_peak_time; - const double *infectious_curve = model->event_lists[ infection_type ].infectious_curve[ LATERAL_FLOW_TEST ]; - const double g = 1.0/8; - - const double b = 1.0/6; - if( time_infected <= peak_time ) - { - I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * infectious_factor; - V = log( I ) / g; - } - else - { - I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * infectious_factor; - V = log( I ) / ( g + b ) + log( infectious_curve[ peak_time ] * - indiv->infectiousness_multiplier * - infectious_factor ) * ( 1 / g - 1 / ( g + b ) ); - } - - if ( V < 0 ) + double sensitivity = lfa_sensitivity( model, indiv ); + if ( sensitivity < 0 ) { indiv->lateral_flow_test_result = gsl_ran_bernoulli( rng, 1 - model->params->lateral_flow_test_specificity ); } else { - sensitivity = 1 / ( 1 + exp( -V ) ); - sensitivity = max( 0, min( model->params->lateral_flow_test_sensitivity , sensitivity ) ); indiv->lateral_flow_test_sensitivity = sensitivity; indiv->lateral_flow_test_result = gsl_ran_bernoulli( rng, sensitivity ); } diff --git a/src/model.c b/src/model.c index 8f020c660..140a9143e 100644 --- a/src/model.c +++ b/src/model.c @@ -56,6 +56,7 @@ model* new_model( parameters *params ) rng = gsl_rng_alloc ( gsl_rng_default); gsl_rng_set( rng, params->rng_seed ); + set_up_counters( model_ptr ); set_up_population( model_ptr ); update_intervention_policy( model_ptr, model_ptr->time ); @@ -63,7 +64,6 @@ model* new_model( parameters *params ) for( type = 0; type < N_EVENT_TYPES; type++ ) set_up_event_list( model_ptr, params, type ); - set_up_counters( model_ptr ); set_up_household_distribution( model_ptr ); set_up_allocate_work_places( model_ptr ); if( params->hospital_on ) @@ -267,6 +267,7 @@ void set_up_counters( model *model ){ model->n_quarantine_events_app_user = 0; model->n_quarantine_release_events = 0; model->n_quarantine_release_events_app_user = 0; + model->n_lateral_flow_tests = 0; model->n_vaccinated_fully = 0; model->n_vaccinated_symptoms = 0; @@ -289,6 +290,7 @@ void reset_counters( model *model ){ model->n_quarantine_events_app_user = 0; model->n_quarantine_release_events = 0; model->n_quarantine_release_events_app_user = 0; + model->n_lateral_flow_tests = 0; } /***************************************************************************************** @@ -1318,8 +1320,8 @@ void return_interactions( model *model ) int one_time_step( model *model ) { (model->time)++; - reset_counters( model ); update_intervention_policy( model, model->time ); + reset_counters( model ); int idx; for( idx = 0; idx < N_EVENT_TYPES; idx++ ) diff --git a/src/model.h b/src/model.h index c8d75dcd3..6b7a30be3 100644 --- a/src/model.h +++ b/src/model.h @@ -92,6 +92,8 @@ struct model{ long n_quarantine_release_events; long n_quarantine_release_events_app_user; + long n_lateral_flow_tests; + long n_population_by_age[ N_AGE_GROUPS ]; long n_vaccinated_fully; long n_vaccinated_symptoms; From 22a1c01d40ac3ac9dbdc07841c6ac25ad5a9d767 Mon Sep 17 00:00:00 2001 From: Matthew Abueg Date: Fri, 26 Feb 2021 15:39:26 -0800 Subject: [PATCH 08/10] Add infectiousness_factor comment --- src/interventions.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/interventions.c b/src/interventions.c index 7cf503e1b..4ede65c04 100644 --- a/src/interventions.c +++ b/src/interventions.c @@ -784,7 +784,7 @@ int infectious_state( individual* indiv, int time_infected ) * * Returns the test sensitivity if the individual is in the * sensitive period, -1 otherwise. -* Returns: double +* ******************************************************************************************/ double lfa_sensitivity( model *model, individual *indiv ) { @@ -793,6 +793,7 @@ double lfa_sensitivity( model *model, individual *indiv ) time_infected = model->time - time_infected; + // Used to normalize the infectiousness curve to V(peak_symptomatic) = 1/g. const double infectious_factor = 1/.0802 * exp(1); double sensitivity = 0; From ea7ded30d543b054600dc55df30853a8077db6d6 Mon Sep 17 00:00:00 2001 From: Matthew Abueg Date: Fri, 5 Mar 2021 18:35:14 -0800 Subject: [PATCH 09/10] Lateral flow cleanup. * Making test cleanup event driven. * Additional tests for number of tests performed. --- .../default_params/baseline_parameters.csv | 4 +- src/constant.h | 1 + src/interventions.c | 55 ++++++------ src/interventions.h | 1 + src/model.c | 7 +- src/model.h | 2 - tests/test_interventions.py | 90 ++++++++++++++++--- 7 files changed, 118 insertions(+), 42 deletions(-) diff --git a/src/COVID19/default_params/baseline_parameters.csv b/src/COVID19/default_params/baseline_parameters.csv index a0a1297f5..9cc065e94 100644 --- a/src/COVID19/default_params/baseline_parameters.csv +++ b/src/COVID19/default_params/baseline_parameters.csv @@ -1,2 +1,2 @@ -rng_seed,param_id,n_total,mean_work_interactions_child,mean_work_interactions_adult,mean_work_interactions_elderly,daily_fraction_work,work_network_rewire,mean_random_interactions_child,sd_random_interactions_child,mean_random_interactions_adult,sd_random_interactions_adult,mean_random_interactions_elderly,sd_random_interactions_elderly,random_interaction_distribution,child_network_adults,elderly_network_adults,days_of_interactions,end_time,n_seed_infection,mean_infectious_period,sd_infectious_period,infectious_rate,sd_infectiousness_multiplier,mean_time_to_symptoms,sd_time_to_symptoms,mean_time_to_hospital,mean_time_to_critical,sd_time_to_critical,mean_time_to_recover,sd_time_to_recover,mean_time_to_death,sd_time_to_death,mean_time_to_susceptible_after_shift,time_to_susceptible_shift,fraction_asymptomatic_0_9,fraction_asymptomatic_10_19,fraction_asymptomatic_20_29,fraction_asymptomatic_30_39,fraction_asymptomatic_40_49,fraction_asymptomatic_50_59,fraction_asymptomatic_60_69,fraction_asymptomatic_70_79,fraction_asymptomatic_80,asymptomatic_infectious_factor,mild_fraction_0_9,mild_fraction_10_19,mild_fraction_20_29,mild_fraction_30_39,mild_fraction_40_49,mild_fraction_50_59,mild_fraction_60_69,mild_fraction_70_79,mild_fraction_80,mild_infectious_factor,mean_asymptomatic_to_recovery,sd_asymptomatic_to_recovery,household_size_1,household_size_2,household_size_3,household_size_4,household_size_5,household_size_6,population_0_9,population_10_19,population_20_29,population_30_39,population_40_49,population_50_59,population_60_69,population_70_79,population_80,daily_non_cov_symptoms_rate,relative_susceptibility_0_9,relative_susceptibility_10_19,relative_susceptibility_20_29,relative_susceptibility_30_39,relative_susceptibility_40_49,relative_susceptibility_50_59,relative_susceptibility_60_69,relative_susceptibility_70_79,relative_susceptibility_80,relative_transmission_household,relative_transmission_occupation,relative_transmission_random,hospitalised_fraction_0_9,hospitalised_fraction_10_19,hospitalised_fraction_20_29,hospitalised_fraction_30_39,hospitalised_fraction_40_49,hospitalised_fraction_50_59,hospitalised_fraction_60_69,hospitalised_fraction_70_79,hospitalised_fraction_80,critical_fraction_0_9,critical_fraction_10_19,critical_fraction_20_29,critical_fraction_30_39,critical_fraction_40_49,critical_fraction_50_59,critical_fraction_60_69,critical_fraction_70_79,critical_fraction_80,fatality_fraction_0_9,fatality_fraction_10_19,fatality_fraction_20_29,fatality_fraction_30_39,fatality_fraction_40_49,fatality_fraction_50_59,fatality_fraction_60_69,fatality_fraction_70_79,fatality_fraction_80,mean_time_hospitalised_recovery,sd_time_hospitalised_recovery,mean_time_critical_survive,sd_time_critical_survive,location_death_icu_0_9,location_death_icu_10_19,location_death_icu_20_29,location_death_icu_30_39,location_death_icu_40_49,location_death_icu_50_59,location_death_icu_60_69,location_death_icu_70_79,location_death_icu_80,quarantine_length_self,quarantine_length_traced_symptoms,quarantine_length_traced_positive,quarantine_length_positive,quarantine_dropout_self,quarantine_dropout_traced_symptoms,quarantine_dropout_traced_positive,quarantine_dropout_positive,quarantine_compliance_traced_symptoms,quarantine_compliance_traced_positive,test_on_symptoms,test_on_traced,test_release_on_negative,trace_on_symptoms,trace_on_positive,retrace_on_positive,quarantine_on_traced,traceable_interaction_fraction,tracing_network_depth,allow_clinical_diagnosis,quarantine_household_on_positive,quarantine_household_on_symptoms,quarantine_household_on_traced_positive,quarantine_household_on_traced_symptoms,quarantine_household_contacts_on_positive,quarantine_household_contacts_on_symptoms,quarantined_daily_interactions,quarantine_days,quarantine_smart_release_day,hospitalised_daily_interactions,test_insensitive_period,test_sensitive_period,test_sensitivity,test_specificity,test_order_wait,test_order_wait_priority,test_result_wait,test_result_wait_priority,priority_test_contacts_0_9,priority_test_contacts_10_19,priority_test_contacts_20_29,priority_test_contacts_30_39,priority_test_contacts_40_49,priority_test_contacts_50_59,priority_test_contacts_60_69,priority_test_contacts_70_79,priority_test_contacts_80,self_quarantine_fraction,app_users_fraction_0_9,app_users_fraction_10_19,app_users_fraction_20_29,app_users_fraction_30_39,app_users_fraction_40_49,app_users_fraction_50_59,app_users_fraction_60_69,app_users_fraction_70_79,app_users_fraction_80,app_turn_on_time,lockdown_occupation_multiplier_primary_network,lockdown_occupation_multiplier_secondary_network,lockdown_occupation_multiplier_working_network,lockdown_occupation_multiplier_retired_network,lockdown_occupation_multiplier_elderly_network,lockdown_random_network_multiplier,lockdown_house_interaction_multiplier,lockdown_time_on,lockdown_time_off,lockdown_elderly_time_on,lockdown_elderly_time_off,testing_symptoms_time_on,testing_symptoms_time_off,intervention_start_time,hospital_on,manual_trace_on,manual_trace_time_on,manual_trace_on_hospitalization,manual_trace_on_positive,manual_trace_delay,manual_trace_exclude_app_users,manual_trace_n_workers,manual_trace_interviews_per_worker_day,manual_trace_notifications_per_worker_day,manual_traceable_fraction_household,manual_traceable_fraction_occupation,manual_traceable_fraction_random,relative_susceptibility_by_interaction -1,1,1000000,10,7,3,0.5,0.1,2,2,4,4,3,3,1,0.2,0.2,10,200,5,5.5,2.14,5.18,0,5.42,2.7,5.14,2.27,2.27,12,5,11.74,8.79,180,10000,0.456,0.412,0.370,0.332,0.296,0.265,0.238,0.214,0.192,0.33,0.533,0.569,0.597,0.614,0.616,0.602,0.571,0.523,0.461,0.72,15,5,7452,9936,4416,4140,1104,552,8054000,7528000,8712000,8835000,8500000,8968000,7069000,5488000,3281000,0.002,0.35,0.69,1.03,1.03,1.03,1.03,1.27,1.52,1.52,2,1,1,0.001,0.006,0.015,0.069,0.219,0.279,0.370,0.391,0.379,0.05,0.05,0.05,0.05,0.063,0.122,0.274,0.432,0.709,0.33,0.25,0.5,0.5,0.5,0.69,0.65,0.88,1,8.75,8.75,18.8,12.21,1,1,0.9,0.9,0.8,0.8,0.4,0.4,0.05,7,14,14,14,0.02,0.04,0.03,0.01,0.5,0.9,0,0,1,0,0,0,0,0.8,0,1,0,0,0,0,0,0,0,7,0,0,3,14,0.8,0.999,1,-1,1,-1,1000,1000,1000,1000,1000,1000,1000,1000,1000,0,0.09,0.8,0.97,0.96,0.94,0.86,0.7,0.48,0.32,10000,0.29,0.29,0.29,0.29,0.29,0.29,1.5,10000,10000,10000,10000,10000,10000,0,0,0,10000,1,0,1,0,300,6,12,1,0.8,0.05,1 +rng_seed,param_id,n_total,mean_work_interactions_child,mean_work_interactions_adult,mean_work_interactions_elderly,daily_fraction_work,work_network_rewire,mean_random_interactions_child,sd_random_interactions_child,mean_random_interactions_adult,sd_random_interactions_adult,mean_random_interactions_elderly,sd_random_interactions_elderly,random_interaction_distribution,child_network_adults,elderly_network_adults,days_of_interactions,end_time,n_seed_infection,mean_infectious_period,sd_infectious_period,infectious_rate,sd_infectiousness_multiplier,mean_time_to_symptoms,sd_time_to_symptoms,mean_time_to_hospital,mean_time_to_critical,sd_time_to_critical,mean_time_to_recover,sd_time_to_recover,mean_time_to_death,sd_time_to_death,mean_time_to_susceptible_after_shift,time_to_susceptible_shift,fraction_asymptomatic_0_9,fraction_asymptomatic_10_19,fraction_asymptomatic_20_29,fraction_asymptomatic_30_39,fraction_asymptomatic_40_49,fraction_asymptomatic_50_59,fraction_asymptomatic_60_69,fraction_asymptomatic_70_79,fraction_asymptomatic_80,asymptomatic_infectious_factor,mild_fraction_0_9,mild_fraction_10_19,mild_fraction_20_29,mild_fraction_30_39,mild_fraction_40_49,mild_fraction_50_59,mild_fraction_60_69,mild_fraction_70_79,mild_fraction_80,mild_infectious_factor,mean_asymptomatic_to_recovery,sd_asymptomatic_to_recovery,household_size_1,household_size_2,household_size_3,household_size_4,household_size_5,household_size_6,population_0_9,population_10_19,population_20_29,population_30_39,population_40_49,population_50_59,population_60_69,population_70_79,population_80,daily_non_cov_symptoms_rate,relative_susceptibility_0_9,relative_susceptibility_10_19,relative_susceptibility_20_29,relative_susceptibility_30_39,relative_susceptibility_40_49,relative_susceptibility_50_59,relative_susceptibility_60_69,relative_susceptibility_70_79,relative_susceptibility_80,relative_transmission_household,relative_transmission_occupation,relative_transmission_random,hospitalised_fraction_0_9,hospitalised_fraction_10_19,hospitalised_fraction_20_29,hospitalised_fraction_30_39,hospitalised_fraction_40_49,hospitalised_fraction_50_59,hospitalised_fraction_60_69,hospitalised_fraction_70_79,hospitalised_fraction_80,critical_fraction_0_9,critical_fraction_10_19,critical_fraction_20_29,critical_fraction_30_39,critical_fraction_40_49,critical_fraction_50_59,critical_fraction_60_69,critical_fraction_70_79,critical_fraction_80,fatality_fraction_0_9,fatality_fraction_10_19,fatality_fraction_20_29,fatality_fraction_30_39,fatality_fraction_40_49,fatality_fraction_50_59,fatality_fraction_60_69,fatality_fraction_70_79,fatality_fraction_80,mean_time_hospitalised_recovery,sd_time_hospitalised_recovery,mean_time_critical_survive,sd_time_critical_survive,location_death_icu_0_9,location_death_icu_10_19,location_death_icu_20_29,location_death_icu_30_39,location_death_icu_40_49,location_death_icu_50_59,location_death_icu_60_69,location_death_icu_70_79,location_death_icu_80,quarantine_length_self,quarantine_length_traced_symptoms,quarantine_length_traced_positive,quarantine_length_positive,quarantine_dropout_self,quarantine_dropout_traced_symptoms,quarantine_dropout_traced_positive,quarantine_dropout_positive,quarantine_compliance_traced_symptoms,quarantine_compliance_traced_positive,quarantine_compliance_positive,test_on_symptoms,test_on_traced,test_release_on_negative,trace_on_symptoms,trace_on_positive,retrace_on_positive,quarantine_on_traced,traceable_interaction_fraction,tracing_network_depth,allow_clinical_diagnosis,quarantine_household_on_positive,quarantine_household_on_symptoms,quarantine_household_on_traced_positive,quarantine_household_on_traced_symptoms,quarantine_household_contacts_on_positive,quarantine_household_contacts_on_symptoms,quarantined_daily_interactions,quarantine_days,quarantine_smart_release_day,hospitalised_daily_interactions,test_insensitive_period,test_sensitive_period,test_sensitivity,test_specificity,test_order_wait,test_order_wait_priority,test_result_wait,test_result_wait_priority,priority_test_contacts_0_9,priority_test_contacts_10_19,priority_test_contacts_20_29,priority_test_contacts_30_39,priority_test_contacts_40_49,priority_test_contacts_50_59,priority_test_contacts_60_69,priority_test_contacts_70_79,priority_test_contacts_80,lateral_flow_test_order_wait,lateral_flow_test_on_symptoms,lateral_flow_test_on_traced,lateral_flow_test_repeat_count,lateral_flow_test_only,lateral_flow_test_fraction,lateral_flow_test_sensitivity,lateral_flow_test_specificity,self_quarantine_fraction,app_users_fraction_0_9,app_users_fraction_10_19,app_users_fraction_20_29,app_users_fraction_30_39,app_users_fraction_40_49,app_users_fraction_50_59,app_users_fraction_60_69,app_users_fraction_70_79,app_users_fraction_80,app_turn_on_time,lockdown_occupation_multiplier_primary_network,lockdown_occupation_multiplier_secondary_network,lockdown_occupation_multiplier_working_network,lockdown_occupation_multiplier_retired_network,lockdown_occupation_multiplier_elderly_network,lockdown_random_network_multiplier,lockdown_house_interaction_multiplier,lockdown_time_on,lockdown_time_off,lockdown_elderly_time_on,lockdown_elderly_time_off,testing_symptoms_time_on,testing_symptoms_time_off,intervention_start_time,hospital_on,manual_trace_on,manual_trace_time_on,manual_trace_on_hospitalization,manual_trace_on_positive,manual_trace_delay,manual_trace_exclude_app_users,manual_trace_n_workers,manual_trace_interviews_per_worker_day,manual_trace_notifications_per_worker_day,manual_traceable_fraction_household,manual_traceable_fraction_occupation,manual_traceable_fraction_random,relative_susceptibility_by_interaction +1,1,1000000,10,7,3,0.5,0.1,2,2,4,4,3,3,1,0.2,0.2,10,200,5,5.5,2.14,5.18,0,5.42,2.7,5.14,2.27,2.27,12,5,11.74,8.79,180,10000,0.456,0.412,0.370,0.332,0.296,0.265,0.238,0.214,0.192,0.33,0.533,0.569,0.597,0.614,0.616,0.602,0.571,0.523,0.461,0.72,15,5,7452,9936,4416,4140,1104,552,8054000,7528000,8712000,8835000,8500000,8968000,7069000,5488000,3281000,0.002,0.35,0.69,1.03,1.03,1.03,1.03,1.27,1.52,1.52,2,1,1,0.001,0.006,0.015,0.069,0.219,0.279,0.370,0.391,0.379,0.05,0.05,0.05,0.05,0.063,0.122,0.274,0.432,0.709,0.33,0.25,0.5,0.5,0.5,0.69,0.65,0.88,1,8.75,8.75,18.8,12.21,1,1,0.9,0.9,0.8,0.8,0.4,0.4,0.05,7,14,14,14,0.02,0.04,0.03,0.01,0.5,0.9,1.0,0,0,1,0,0,0,0,0.8,0,1,0,0,0,0,0,0,0,7,0,0,3,14,0.8,0.999,1,-1,1,-1,1000,1000,1000,1000,1000,1000,1000,1000,1000,1,0,0,7,0,0.5,0.95,0.999,0,0.09,0.8,0.97,0.96,0.94,0.86,0.7,0.48,0.32,10000,0.29,0.29,0.29,0.29,0.29,0.29,1.5,10000,10000,10000,10000,10000,10000,0,0,0,10000,1,0,1,0,300,6,12,1,0.8,0.05,1 diff --git a/src/constant.h b/src/constant.h index 7fb5e813f..745aace0b 100644 --- a/src/constant.h +++ b/src/constant.h @@ -46,6 +46,7 @@ enum EVENT_TYPES{ VACCINE_PROTECT, VACCINE_WANE, LATERAL_FLOW_TEST_TAKE, + LATERAL_FLOW_TEST_CLEAR, N_EVENT_TYPES }; #define N_NEWLY_INFECTED_STATES 3 diff --git a/src/interventions.c b/src/interventions.c index 4ede65c04..ba202d8a7 100644 --- a/src/interventions.c +++ b/src/interventions.c @@ -353,18 +353,6 @@ void update_intervention_policy( model *model, int time ) model->manual_trace_interview_quota = params->manual_trace_n_workers * params->manual_trace_interviews_per_worker_day; model->manual_trace_notification_quota = params->manual_trace_n_workers * params->manual_trace_notifications_per_worker_day; } - - if ( model->n_lateral_flow_tests > 0 ) - { - for( int idx = 0; idx < model->params->n_total; idx++ ) - { - if( model->population[ idx ].lateral_flow_test_result >= 0 ) - { - model->population[ idx ].lateral_flow_test_result = NO_TEST; - model->population[ idx ].lateral_flow_test_sensitivity = NO_TEST; - } - } - } } /***************************************************************************************** @@ -778,6 +766,17 @@ int infectious_state( individual* indiv, int time_infected ) } return 0; } + +// Parameters for the LFA test. +const struct { + // Used to normalize the infectiousness curve to V(peak_symptomatic) = 1/g. + double infectious_factor; + // Inverse rate of growth of the viral load curve. + double g; + // Inverse rate of falloff of the viral load curve. + double b; +} LFA = {1/.0802 * exp(1), 1.0/8, 1.0/6}; + /***************************************************************************************** * Name: lfa_sensitivity * Description: Calculates the lfa sensitivity for a given individual. @@ -793,28 +792,23 @@ double lfa_sensitivity( model *model, individual *indiv ) time_infected = model->time - time_infected; - // Used to normalize the infectiousness curve to V(peak_symptomatic) = 1/g. - const double infectious_factor = 1/.0802 * exp(1); - double sensitivity = 0; double I = 0; double V = 0; - const int peak_time = model->event_lists[ LATERAL_FLOW_TEST ].infectious_peak_time; + const int peak_time = model->event_lists[ infection_type ].infectious_peak_time; const double *infectious_curve = model->event_lists[ infection_type ].infectious_curve[ LATERAL_FLOW_TEST ]; - const double g = 1.0/8; - const double b = 1.0/6; if( time_infected <= peak_time ) { - I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * infectious_factor; - V = log( I ) / g; + I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * LFA.infectious_factor; + V = log( I ) / LFA.g; } else { - I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * infectious_factor; - V = log( I ) / ( g + b ) + log( infectious_curve[ peak_time ] * - indiv->infectiousness_multiplier * - infectious_factor ) * ( 1 / g - 1 / ( g + b ) ); + I = infectious_curve[ time_infected ] * indiv->infectiousness_multiplier * LFA.infectious_factor; + V = log( I ) / ( LFA.g + LFA.b ) + log( infectious_curve[ peak_time ] * + indiv->infectiousness_multiplier * + LFA.infectious_factor ) * ( 1 / LFA.g - 1 / ( LFA.g + LFA.b ) ); } if ( V < 0 ) @@ -837,7 +831,6 @@ void intervention_lateral_flow_test_take( model *model, individual *indiv ) { if ( indiv->lateral_flow_test_capacity <= 0 ) return; indiv->lateral_flow_test_capacity--; - model->n_lateral_flow_tests++; int result_time = model->time; @@ -870,6 +863,18 @@ void intervention_lateral_flow_test_take( model *model, individual *indiv ) else if ( indiv->lateral_flow_test_capacity > 0 ) add_individual_to_event_list( model, LATERAL_FLOW_TEST_TAKE, indiv, model->time + 1 ); + add_individual_to_event_list( model, LATERAL_FLOW_TEST_CLEAR, indiv, model->time + 1 ); +} + +/***************************************************************************************** +* Name: intervention_lateral_flow_test_clear +* Description: Clears the results of previous lateral flow tests. +* Returns: void +******************************************************************************************/ +void intervention_lateral_flow_test_clear( model *model, individual *indiv ) +{ + indiv->lateral_flow_test_result = NO_TEST; + indiv->lateral_flow_test_sensitivity = NO_TEST; } /***************************************************************************************** diff --git a/src/interventions.h b/src/interventions.h index 436ec1f9f..a4f7c0b86 100644 --- a/src/interventions.h +++ b/src/interventions.h @@ -65,6 +65,7 @@ void intervention_on_positive_result( model*, individual* ); void intervention_on_traced( model*, individual*, int, int, trace_token*, double, int ); void intervention_lateral_flow_test_order( model*, individual*, int ); void intervention_lateral_flow_test_take( model*, individual* ); +void intervention_lateral_flow_test_clear( model*, individual* ); double calculate_mean_lfa_sensitivity( model *, int ); void intervention_smart_release( model* ); diff --git a/src/model.c b/src/model.c index 140a9143e..70eadef7b 100644 --- a/src/model.c +++ b/src/model.c @@ -267,7 +267,6 @@ void set_up_counters( model *model ){ model->n_quarantine_events_app_user = 0; model->n_quarantine_release_events = 0; model->n_quarantine_release_events_app_user = 0; - model->n_lateral_flow_tests = 0; model->n_vaccinated_fully = 0; model->n_vaccinated_symptoms = 0; @@ -290,7 +289,6 @@ void reset_counters( model *model ){ model->n_quarantine_events_app_user = 0; model->n_quarantine_release_events = 0; model->n_quarantine_release_events_app_user = 0; - model->n_lateral_flow_tests = 0; } /***************************************************************************************** @@ -1355,6 +1353,11 @@ int one_time_step( model *model ) flu_infections( model ); + if ( n_total_by_day( model, LATERAL_FLOW_TEST_TAKE, model->time - 1 ) > 0 ) + { + transition_events( model, LATERAL_FLOW_TEST_CLEAR, &intervention_lateral_flow_test_clear, TRUE ); + } + while( ( n_daily( model, TEST_TAKE, model->time ) > 0 ) || ( n_daily( model, TEST_RESULT, model->time ) > 0 ) || ( n_daily( model, LATERAL_FLOW_TEST_TAKE, model->time ) > 0 ) || diff --git a/src/model.h b/src/model.h index 6b7a30be3..c8d75dcd3 100644 --- a/src/model.h +++ b/src/model.h @@ -92,8 +92,6 @@ struct model{ long n_quarantine_release_events; long n_quarantine_release_events_app_user; - long n_lateral_flow_tests; - long n_population_by_age[ N_AGE_GROUPS ]; long n_vaccinated_fully; long n_vaccinated_symptoms; diff --git a/tests/test_interventions.py b/tests/test_interventions.py index c1e1e50e5..337d86654 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -10,6 +10,7 @@ Author: p-robot """ +import collections import pytest import subprocess import sys @@ -768,7 +769,7 @@ class TestClass(object): ), ) ], - "test_lateral_flow_interventions_has_tests": [ + "test_lateral_flow_symptoms_has_tests": [ dict( test_params = dict( n_seed_infection = 400, @@ -776,6 +777,12 @@ class TestClass(object): infectious_rate = 6, sd_infectiousness_multiplier = 0.4, lateral_flow_test_on_symptoms = True, + lateral_flow_test_order_wait = 1, + lateral_flow_test_fraction = 1.0, + daily_non_cov_symptoms_rate=0.0, + lateral_flow_test_sensitivity = 0.0, + lateral_flow_test_specificity = 1.0, + lateral_flow_test_repeat_count = 7, ), ), dict( @@ -784,6 +791,32 @@ class TestClass(object): end_time = 10, infectious_rate = 6, sd_infectiousness_multiplier = 0.4, + lateral_flow_test_on_symptoms = True, + lateral_flow_test_order_wait = 1, + lateral_flow_test_fraction = 1.0, + daily_non_cov_symptoms_rate=0.0, + lateral_flow_test_sensitivity = 0.0, + lateral_flow_test_specificity = 1.0, + lateral_flow_test_repeat_count = 3, + ), + ), + ], + "test_lateral_flow_interventions_has_tests": [ + dict( + test_params = dict( + n_seed_infection = 400, + end_time = 10, + infectious_rate = 6, + sd_infectiousness_multiplier = 0.4, + lateral_flow_test_on_symptoms = True, + ), + ), + dict( + test_params = dict( + n_seed_infection = 1000, + end_time = 10, + infectious_rate = 6, + sd_infectiousness_multiplier = 0.4, app_turn_on_time = 0, test_on_symptoms = True, trace_on_positive = True, @@ -791,13 +824,13 @@ class TestClass(object): lateral_flow_test_on_symptoms = False, lateral_flow_test_on_traced = True, ), - ) + ), ], "test_lateral_flow_interventions_no_tests": [ dict( test_params = dict( n_seed_infection = 400, - end_time = 10, + end_time = 8, infectious_rate = 6, sd_infectiousness_multiplier = 0.4, app_turn_on_time = 0, @@ -815,7 +848,7 @@ class TestClass(object): dict( test_params = dict( n_seed_infection = 100, - end_time = 10, + end_time = 8, infectious_rate = 6, sd_infectiousness_multiplier = 0.4, app_turn_on_time = 0, @@ -2304,33 +2337,68 @@ def test_test_sensitivity(self, test_params ): del( model ) - def test_lateral_flow_interventions_has_tests(self, test_params): + def test_lateral_flow_symptoms_has_tests(self, test_params): """ Test that we do have lateral flow tests if they are enabled. """ end_time = test_params[ "end_time" ] + total_delay = test_params[ "lateral_flow_test_order_wait" ] params = utils.get_params_swig() for param, value in test_params.items(): - params.set_param( param, value ) + params.set_param( param, value ) model = utils.get_model_swig( params ) + n_tests = [] for time in range( end_time ): model.one_time_step() - results = model.one_time_step_results() - - np.testing.assert_equal( results[ "n_lateral_flow_tests" ] > 0, True, "Expected lateral flow tests not found." ) + results = model.one_time_step_results() + n_tests.append( results["n_lateral_flow_tests"] ) # write files model.write_individual_file() model.write_transmissions() + df_trans = pd.read_csv( constant.TEST_TRANSMISSION_FILE, sep = ",", comment = "#", skipinitialspace = True ) + df_trans = df_trans[ df_trans[ "time_symptomatic" ] > 0 ].groupby( "time_symptomatic").size().reset_index( name = "n_symptoms") + + symp_hist = collections.deque(maxlen=test_params["lateral_flow_test_repeat_count"]) + for time in range( test_params[ "end_time" ] - total_delay ): + symp = df_trans[ df_trans["time_symptomatic" ] == time + 1 ] + if len( symp.index ) > 0 : + symp = symp.iloc[ 0,1 ] + else : + symp = 0 + symp_hist.append(symp) + np.testing.assert_equal( n_tests[time + total_delay ], sum(symp_hist), f"Number of LFA test results not what expected given prior number of new symptomatic infections: {time} + {total_delay}") del( model ) + def test_lateral_flow_interventions_has_tests(self, test_params): + """ + Test that we do have lateral flow tests if they are enabled. + """ + end_time = test_params[ "end_time" ] + + params = utils.get_params_swig() + for param, value in test_params.items(): + params.set_param( param, value ) + model = utils.get_model_swig( params ) + + for time in range( end_time ): + model.one_time_step() + + # write files + model.write_individual_file() + # read CSV's df_indiv = pd.read_csv( constant.TEST_INDIVIDUAL_FILE, comment="#", sep=",", skipinitialspace=True ) - np.testing.assert_equal( sum( df_indiv[ "lateral_flow_status" ] == 0 ) > 0, True, "No negative Lateral Flow tests found." ) - np.testing.assert_equal( sum( df_indiv[ "lateral_flow_status" ] == 1 ) > 0, True, "No positive Lateral Flow tests found." ) + tot = sum( df_indiv[ "lateral_flow_status" ] >= 0 ) + pos = sum( df_indiv[ "lateral_flow_status" ] == 1 ) + neg = sum( df_indiv[ "lateral_flow_status" ] == 0 ) + np.testing.assert_equal( sum( df_indiv[ "lateral_flow_status" ] == 0 ) > 0, True, f"No negative Lateral Flow tests found. {tot} {pos} {neg}" ) + np.testing.assert_equal( sum( df_indiv[ "lateral_flow_status" ] == 1 ) > 0, True, f"No positive Lateral Flow tests found. {tot} {pos} {neg}" ) + + del( model ) def test_lateral_flow_interventions_no_tests(self, test_params): """ From 41bff6fd9479e4778394d77acf3ae9ae44fa5153 Mon Sep 17 00:00:00 2001 From: Matthew Abueg Date: Mon, 8 Mar 2021 13:04:00 -0800 Subject: [PATCH 10/10] Switch hardcoded sensistivity peak to calculated. --- tests/test_interventions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_interventions.py b/tests/test_interventions.py index 337d86654..f374f035f 100644 --- a/tests/test_interventions.py +++ b/tests/test_interventions.py @@ -2523,16 +2523,16 @@ def test_lateral_flow_test_sensitivity(self, test_params): true_pos = df_test[ ( df_test[ "infected" ] == True ) & ( df_test[ "lateral_flow_status" ] == 1 ) & ( df_test[ "test_sensitive_inf" ] == True ) ].groupby( [ "time_since_inf" ] ).size() false_neg = df_test[ ( df_test[ "infected" ] == True ) & ( df_test[ "lateral_flow_status" ] == 0 ) & ( df_test[ "test_sensitive_inf" ] == True ) ].groupby( [ "time_since_inf" ] ).size() sens_ratio = true_pos.divide( false_neg.add( true_pos, fill_value=0 ), fill_value=0 ) - sens_peak = sens_ratio.argmax() + sens_peak_time = sens_ratio.argmax() sens_diff = sens_ratio.diff() - is_sens_single_peak = np.all( sens_diff.iloc[ 2 : sens_peak + 1 ] >= 0 ) & np.all( sens_diff[ sens_peak + 1 : -3 ] <= 0 ) + is_sens_single_peak = np.all( sens_diff.iloc[ 2 : sens_peak_time + 1 ] >= 0 ) & np.all( sens_diff[ sens_peak_time + 1 : -3 ] <= 0 ) np.testing.assert_equal( is_sens_single_peak, True, f"Sensitivity does not have a single peak: {sens_diff}" ) # check the sensitivity at the peak. df_test[ "symptomatic" ] = ( df_test["time_symptomatic"] > 0 ) - true_pos = df_test[ ( df_test[ "time_since_inf" ] == 3 ) & ( df_test[ "symptomatic" ] == True ) & ( df_test[ "lateral_flow_status" ] == 1 ) & ( df_test[ "test_sensitive_inf" ] == True ) ].groupby( [ "time_since_inf" ] ).size() - false_neg = df_test[ ( df_test[ "time_since_inf" ] == 3 ) & ( df_test[ "symptomatic" ] == True ) & ( df_test[ "lateral_flow_status" ] == 0 ) & ( df_test[ "test_sensitive_inf" ] == True ) ].groupby( [ "time_since_inf" ] ).size() + true_pos = df_test[ ( df_test[ "time_since_inf" ] == sens_peak_time ) & ( df_test[ "symptomatic" ] == True ) & ( df_test[ "lateral_flow_status" ] == 1 ) & ( df_test[ "test_sensitive_inf" ] == True ) ].groupby( [ "time_since_inf" ] ).size() + false_neg = df_test[ ( df_test[ "time_since_inf" ] == sens_peak_time ) & ( df_test[ "symptomatic" ] == True ) & ( df_test[ "lateral_flow_status" ] == 0 ) & ( df_test[ "test_sensitive_inf" ] == True ) ].groupby( [ "time_since_inf" ] ).size() sens_ratio = true_pos.divide( false_neg.add( true_pos, fill_value=0 ), fill_value=0 ) sens_peak_idx = sens_ratio.idxmax() @@ -2542,6 +2542,7 @@ def test_lateral_flow_test_sensitivity(self, test_params): sd_mult = test_params[ "sd_infectiousness_multiplier" ] sens_exp = test_params[ "lateral_flow_test_sensitivity"] if sd_mult: + # Low is half of one standard deviation (67.5% / 2). sens_low = sens_exp - 0.675 / 2.0 * sd_mult sens_peak = sens_ratio[sens_peak_idx] np.testing.assert_equal( sens_low <= sens_peak <= sens_exp, True, f"Sensitivity outside expected range {sens_low} <= {sens_peak} <= {sens_exp}." )