def __init__( self, country_iso3: str, region: str, mixing: dict, npi_effectiveness_params: dict, google_mobility_locations: dict, is_periodic_intervention: bool, periodic_int_params: dict, periodic_end_time: float, microdistancing_params: dict, ): """Build the time variant location adjustment functions""" # Load mobility data google_mobility_values, google_mobility_days = get_mobility_data( country_iso3, region, BASE_DATETIME, google_mobility_locations) # Build mixing data timeseries mixing = update_mixing_data( mixing, npi_effectiveness_params, google_mobility_values, google_mobility_days, is_periodic_intervention, periodic_int_params, periodic_end_time, ) # Build the time variant location adjustment functions from mixing timeseries mixing_locations = [loc for loc in LOCATIONS if loc in mixing] self.loc_adj_funcs = {} for loc_key in mixing_locations: loc_times = mixing[loc_key]["times"] loc_vals = mixing[loc_key]["values"] self.loc_adj_funcs[loc_key] = scale_up_function(loc_times, loc_vals, method=4) # Work out microdistancing function to be applied to all non-household locations if not microdistancing_params: self.microdistancing_function = None elif microdistancing_params["function_type"] == "tanh": self.microdistancing_function = tanh_based_scaleup( **microdistancing_params["parameters"]) elif microdistancing_params["function_type"] == "empiric": micro_times = microdistancing_params["parameters"]["times"] micro_vals = microdistancing_params["parameters"]["values"] self.microdistancing_function = scale_up_function(micro_times, micro_vals, method=4) # Load all location-specific mixing info. self.matrix_components = {} for sheet_type in ["all_locations"] + LOCATIONS: # Loads a 16x16 ndarray self.matrix_components[sheet_type] = get_country_mixing_matrix( sheet_type, country_iso3)
def test_microdistancing__with_tanh_func_and_adjuster(): params = { "foo": { "function_type": "tanh", "parameters": { "shape": -0.05, "inflection_time": 275, "lower_asymptote": 0.6, "upper_asymptote": 1, }, "locations": LOCATIONS, }, "foo_adjuster": { "function_type": "empiric", "parameters": { "max_effect": 0.6, "times": [0, 365], "values": [1, 100], }, "locations": LOCATIONS, }, } expect_func = tanh_based_scaleup(**params["foo"]["parameters"]) expect_adj_func = scale_up_function([0, 365], [0.6, 60], method=4) params = {k: MicroDistancingFunc(**v) for k, v in params.items()} funcs = get_microdistancing_funcs(params=params, square_mobility_effect=False) assert funcs["work"](0) == 1 - expect_func(0) * expect_adj_func(0) assert funcs["home"](0) == 1 - expect_func(0) * expect_adj_func(0) assert funcs["work"](300) == 1 - expect_func(300) * expect_adj_func(300) assert funcs["home"](300) == 1 - expect_func(300) * expect_adj_func(300)
def get_pop_mortality_functions( input_database, age_breaks, country_iso_code, emigration_value=0.0, emigration_start_time=1980.0 ): """ use the mortality rate data that can be obtained from find_age_specific_death_rates to fit time-variant mortality functions for each age group being implemented in the model :param age_breaks: list starting ages for each of the age groups :param country_iso_code: str the three digit iso3 code for the country of interest :param emigration_value: float an extra rate of migration to add on to the population-wide mortality rates to simulate net emigration :param emigration_start_time: float the point from which the additional net emigration commences :return: dict keys age breakpoints, values mortality functions """ age_death_dict, data_years = find_age_specific_death_rates( input_database, age_breaks, country_iso_code ) # add an extra fixed value after a particular time point for each mortality estimate for age_group in age_death_dict: for i_year in range(len(age_death_dict[age_group])): if data_years[i_year] > emigration_start_time: age_death_dict[age_group][i_year] += emigration_value # fit the curve functions to the aggregate data of mortality and net emigration return { age_group: scale_up_function( data_years, age_death_dict[age_group], smoothness=0.2, method=5 ) for age_group in age_death_dict }
def build_scale_up_function(values_dict, multiplier=1.0): return scale_up_function( values_dict.keys(), [val * multiplier for val in list(values_dict.values())], smoothness=0.2, method=5, )
def plot_mixing_params_over_time(mixing_params, npi_effectiveness_range): titles = { 'home': 'Household', 'work': 'Workplace', 'school': 'School', 'other_locations': 'Other locations' } y_labs = {'home': 'h', 'work': 'w', 'school': 's', 'other_locations': 'l'} date_ticks = { 32: '1/2', 47: '16/2', 61: '1/3', 76: '16/3', 92: '1/4', 107: '16/4', 122: '1/5', 137: '16/5', 152: '1/6' } # use italics for y_labs for key in y_labs: y_labs[key] = '$\it{' + y_labs[key] + '}$(t)' plt.style.use("ggplot") for i_loc, location in enumerate([ loc for loc in ["home", "other_locations", "school", "work"] if loc + "_times" in mixing_params ]): plt.figure(i_loc) x = list(np.linspace(0.0, 152.0, num=10000)) y = [] for indice_npi_effect_range in [0, 1]: npi_effect = { key: val[indice_npi_effect_range] for key, val in npi_effectiveness_range.items() } modified_mixing_params = apply_npi_effectiveness( copy.deepcopy(mixing_params), npi_effect) location_adjustment = scale_up_function( modified_mixing_params[location + "_times"], modified_mixing_params[location + "_values"], method=4) _y = [location_adjustment(t) for t in x] y.append(_y) plt.plot(x, _y, color='navy') plt.fill_between(x, y[0], y[1], color='cornflowerblue') plt.xlim((30., 152.)) plt.ylim((0, 1.1)) plt.xticks(list(date_ticks.keys()), list(date_ticks.values())) plt.xlabel('Date in 2020') plt.ylabel(y_labs[location]) plt.title(titles[location]) plt.savefig('mixing_adjustment_' + location + '.png')
def __init__(self, age_mixing: Dict[str, TimeSeries]): """Build the time variant age adjustment functions""" self.adjustment_funcs = {} for age_idx, timeseries in age_mixing.items(): func = scale_up_function(timeseries.times, timeseries.values, method=4) self.adjustment_funcs[str(age_idx)] = func
def mixing_matrix_function(time): mixing_matrix = mixing_matrix_components["all_locations"] # Make adjustments by location for location in [ loc for loc in ["home", "other_locations", "school", "work"] if loc + "_times" in mixing_params ]: location_adjustment = scale_up_function( mixing_params[location + "_times"], mixing_params[location + "_values"], method=4) mixing_matrix = np.add( mixing_matrix, (location_adjustment(time) - 1.0) * mixing_matrix_components[location], ) # Make adjustments by age affected_age_indices = [ age_index for age_index in range(16) if 'age_' + str(age_index) + '_times' in mixing_params ] complement_indices = [ age_index for age_index in range(16) if age_index not in affected_age_indices ] for age_index_affected in affected_age_indices: age_adjustment = scale_up_function( mixing_params['age_' + str(age_index_affected) + "_times"], mixing_params['age_' + str(age_index_affected) + "_values"], method=4) for age_index_not_affected in complement_indices: mixing_matrix[age_index_affected, age_index_not_affected] *= age_adjustment(time) mixing_matrix[age_index_not_affected, age_index_affected] *= age_adjustment(time) # FIXME: patch for elderly cocooning in Victoria assuming for age_index_affected_bis in affected_age_indices: mixing_matrix[age_index_affected, age_index_affected_bis] *= 1. - ( 1. - age_adjustment(time)) / 2. return mixing_matrix
def stratify_by_age(model_to_stratify, age_specific_latency_parameters, age_strata): # FIXME: This is Marshall Islands specifc - DO NOT USE outside of Marshall Islands age_breakpoints = [int(i_break) for i_break in age_strata] age_infectiousness = get_parameter_dict_from_function( logistic_scaling_function(10.0), age_breakpoints) age_params = get_adapted_age_parameters(age_breakpoints, age_specific_latency_parameters) age_params.update(split_age_parameter(age_breakpoints, "contact_rate")) death_rates_by_age, death_rate_years = get_death_rates_by_agegroup( age_breakpoints, "FSM") # Add an extra fixed value after a particular time point for each mortality estimate, # to simulate emigration. emigration_value = 0.0075 emigration_start_time = 1990.0 # for age_group in age_breakpoints: # for i_year in range(len(death_rate_years)): # if data_years[i_year] > emigration_start_time: # age_death_dict[age_group][i_year] += emigration_value pop_morts = {} for age_group in age_breakpoints: pop_morts[age_group] = scale_up_function(death_rate_years, death_rates_by_age[age_group], smoothness=0.2, method=5) age_params["universal_death_rate"] = {} for age_break in age_breakpoints: model_to_stratify.time_variants["universal_death_rateXage_" + str(age_break)] = pop_morts[age_break] model_to_stratify.parameters[ "universal_death_rateXage_" + str(age_break)] = "universal_death_rateXage_" + str(age_break) age_params["universal_death_rate"][ str(age_break) + "W"] = "universal_death_rateXage_" + str(age_break) model_to_stratify.parameters["universal_death_rateX"] = 0.0 # Add BCG effect without stratification assuming constant 100% coverage bcg_wane = create_sloping_step_function(15.0, 0.3, 30.0, 1.0) age_bcg_efficacy_dict = get_parameter_dict_from_function( lambda value: bcg_wane(value), age_breakpoints) age_params.update({"contact_rate": age_bcg_efficacy_dict}) model_to_stratify.stratify( "age", deepcopy(age_breakpoints), [], {}, adjustment_requests=age_params, infectiousness_adjustments=age_infectiousness, verbose=False, ) return model_to_stratify
def make_intervention_adjustment_func(time_variant_screening_rate, sensitivity, prop_detected_effectively_moving): acf_detection_func = scale_up_function( list(time_variant_screening_rate.keys()), [ v * sensitivity * prop_detected_effectively_moving for v in list(time_variant_screening_rate.values()) ], method=4, ) return acf_detection_func
def test_scale_up_function(verify, method, func_name, func): """ Acceptance test for scale_up_function.""" case_str = f"scale-up-func-{func_name}-{method}" scaled_func = scale_up_function(x=INPUT_TIMES, y=[func(t) for t in INPUT_TIMES], method=method) outputs = np.zeros_like(TEST_TIMES) for idx in range(66): outputs[idx] = scaled_func(TEST_TIMES[idx]) verify(outputs, case_str)
def build_mongolia_timevariant_tsr(): tsr = { 1950.0: 0.0, 1970.0: 0.2, 1994.0: 0.6, 2000.0: 0.85, 2010.0: 0.87, 2016: 0.9 } return scale_up_function(tsr.keys(), tsr.values(), smoothness=0.2, method=5)
def _build_age_strat(params: dict, uni_death_flow_names: list): # Apply age-stratification age_strat = AgeStratification("age", params["age_breakpoints"], COMPARTMENTS) # Set age demographics pop = get_population_by_agegroup(age_breakpoints=params["age_breakpoints"], country_iso_code=params["iso3"], year=2000) age_split_props = dict( zip(params["age_breakpoints"], [x / sum(pop) for x in pop])) age_strat.set_population_split(age_split_props) # Add age-based heterogeneous mixing mixing_matrix = get_mixing_matrix_specific_agegroups( country_iso_code=params["iso3"], requested_age_breaks=list(map(int, params["age_breakpoints"])), time_unit="years", ) # FIXME: These values break the solver because they are very big. # age_strat.set_mixing_matrix(mixing_matrix) # Add age-based flow adjustments. age_strat.add_flow_adjustments( "stabilisation", _adjust_all_multiply(params["stabilisation_rate_stratified"]["age"])) age_strat.add_flow_adjustments( "early_activation", _adjust_all_multiply( params["early_activation_rate_stratified"]["age"])) age_strat.add_flow_adjustments( "late_activation", _adjust_all_multiply(params["late_activation_rate_stratified"]["age"])) # Add age-specific all-causes mortality rate. death_rates_by_age, death_rate_years = get_death_rates_by_agegroup( params["age_breakpoints"], params["iso3"]) death_rates_by_age = { age: scale_up_function(death_rate_years, death_rates_by_age[int(age)], smoothness=0.2, method=5) for age in params["age_breakpoints"] } for uni_death_flow_name in uni_death_flow_names: age_strat.add_flow_adjustments( uni_death_flow_name, _adjust_all_multiply(death_rates_by_age)) return age_strat
def build_mongolia_timevariant_cdr(cdr_multiplier): cdr = { 1950.0: 0.0, 1980.0: 0.10, 1990.0: 0.15, 2000.0: 0.20, 2010.0: 0.30, 2015: 0.33, } return scale_up_function( cdr.keys(), [c * cdr_multiplier for c in list(cdr.values())], smoothness=0.2, method=5, )
def __init__(self, mixing_age_adjust: dict): """Build the time variant age adjustment functions""" self.age_adjustment_functions = {} affected_age_indices = [ i for i in AGE_INDICES if f"age_{i}" in mixing_age_adjust ] for age_idx in affected_age_indices: key = f"age_{age_idx}" mixing_age_adjust[key]["times"] = [ (time_date - BASE_DATE).days for time_date in mixing_age_adjust[key]["times"] ] age_times = mixing_age_adjust[key]["times"] age_vals = mixing_age_adjust[key]["values"] func = scale_up_function(age_times, age_vals, method=4) self.age_adjustment_functions[age_idx] = func
def get_microdist_func_component(func_params: MicroDistancingFunc): """ Get one function of time using the standard parameter request structure for any microdistancing function or adjustment to a microdistancing function. """ if func_params.function_type == "tanh": return tanh_based_scaleup(**func_params.parameters.dict()) elif func_params.function_type == "empiric": micro_times = func_params.parameters.times multiplier = func_params.parameters.max_effect micro_vals = [ multiplier * value for value in func_params.parameters.values ] return scale_up_function(micro_times, micro_vals, method=4) elif func_params.function_type == "constant": return lambda time: func_params.parameters.effect
def find_cdr_function_from_test_data( assumed_tests_parameter, assumed_cdr_parameter, smoothing_period, country_iso3, total_pops, subregion=None, ): # Get the appropriate testing data if country_iso3 == "AUS": test_dates, test_values = get_dhhs_testing_numbers() elif country_iso3 == "PHL": phl_region = subregion.lower() if subregion else "philippines" test_dates, test_values = get_phl_subregion_testing_numbers(phl_region) elif subregion == "Sabah": test_dates, test_values = get_international_testing_numbers( country_iso3) else: test_dates, test_values = get_international_testing_numbers( country_iso3) # Convert test numbers to per capita testing rates per_capita_tests = [i_tests / sum(total_pops) for i_tests in test_values] # Smooth the testing data if requested smoothed_per_capita_tests = (apply_moving_average(per_capita_tests, smoothing_period) if smoothing_period > 1 else per_capita_tests) # Calculate CDRs and the resulting CDR function over time cdr_from_tests_func = create_cdr_function(assumed_tests_parameter, assumed_cdr_parameter) return scale_up_function( test_dates, [ cdr_from_tests_func(i_test_rate) for i_test_rate in smoothed_per_capita_tests ], smoothness=0.2, method=4, bound_low=0.0, )
def test_microdistancing__with_empiric_func(): params = { "foo": { "function_type": "empiric", "parameters": { "max_effect": 0.6, "times": [0, 365], "values": [1, 100], }, "locations": LOCATIONS, } } expect_func = scale_up_function([0, 365], [0.6, 60], method=4) params = {k: MicroDistancingFunc(**v) for k, v in params.items()} funcs = get_microdistancing_funcs(params=params, square_mobility_effect=False) assert funcs["work"](0) == 1 - expect_func(0) assert funcs["home"](0) == 1 - expect_func(0) assert funcs["work"](300) == 1 - expect_func(300) assert funcs["home"](300) == 1 - expect_func(300)
def get_mobility_funcs( country: Country, region: str, mixing: Dict[str, MixingLocation], google_mobility_locations: List[str], npi_effectiveness_params: Dict[str, float], square_mobility_effect: bool, smooth_google_data: bool, ) -> Dict[str, Callable[[float], float]]: """ Loads google mobility data, combines it with user requested timeseries data and then returns a mobility function for each location. """ google_mobility_values, google_mobility_days = get_mobility_data( country.iso3, region, BASE_DATETIME, google_mobility_locations) if smooth_google_data: for loc in google_mobility_values: google_mobility_values[loc] = apply_moving_average( google_mobility_values[loc], 7) # Build mixing data timeseries mixing = update_mixing_data( {k: v.dict() for k, v in mixing.items()}, npi_effectiveness_params, google_mobility_values, google_mobility_days, ) # Build the time variant location-specific macrodistancing adjustment functions from mixing timeseries mobility_funcs = {} for location, timeseries in mixing.items(): if square_mobility_effect: loc_vals = [v**2 for v in timeseries["values"]] else: loc_vals = timeseries["values"] mobility_funcs[location] = scale_up_function(timeseries["times"], loc_vals, method=4) return mobility_funcs
def get_importation_rate_func_as_birth_rates( importation_times: List[float], importation_n_cases: List[float], detect_prop_func, starting_pops: list, ): """ When imported cases are explicitly simulated as part of the modelled population. They enter the late_infectious compartment through a birth process """ # inflate importation numbers to account for undetected cases (assumed to be asymptomatic or sympt non hospital) for i, time in enumerate(importation_times): importation_n_cases[i] /= detect_prop_func(time) # scale-up curve for importation numbers importation_numbers_scale_up = scale_up_function(importation_times, importation_n_cases, method=4, smoothness=5.0, bound_low=0.0) def recruitment_rate(t): return importation_numbers_scale_up(t) / sum(starting_pops) return recruitment_rate
def build_model(params: dict) -> StratifiedModel: external_params = deepcopy(params) model_parameters = { "contact_rate": external_params["contact_rate"], "contact_rate_recovered": external_params["contact_rate"] * external_params["rr_transmission_recovered"], "contact_rate_late_latent": external_params["contact_rate"] * external_params["rr_transmission_late_latent"], "recovery": external_params["self_recovery_rate"], "infect_death": external_params["tb_mortality_rate"], **external_params, } stratify_by = external_params["stratify_by"] derived_output_types = external_params["derived_outputs"] n_iter = (int( round((external_params["end_time"] - external_params["start_time"]) / external_params["time_step"])) + 1) integration_times = numpy.linspace(external_params["start_time"], external_params["end_time"], n_iter).tolist() model_parameters.update( change_parameter_unit(provide_aggregated_latency_parameters(), 365.251)) # sequentially add groups of flows flows = add_standard_infection_flows([]) flows = add_standard_latency_flows(flows) flows = add_standard_natural_history_flows(flows) # compartments compartments = [ "susceptible", "early_latent", "late_latent", "infectious", "recovered", ] # define model #replace_deaths add_crude_birth_rate init_pop = {"infectious": 1000, "late_latent": 1000000} tb_model = StratifiedModel( integration_times, compartments, init_pop, model_parameters, flows, birth_approach="replace_deaths", starting_population=external_params["start_population"], output_connections={}, derived_output_functions={}, death_output_categories=((), ("age_0", )), ) # add crude birth rate from un estimates birth_rates, years = get_crude_birth_rate("MNG") # Provisional patch to birth rates for i in range(len(birth_rates)): if years[i] > 1990.0: birth_rates[i] = 0.04 tb_model.time_variants["crude_birth_rate"] = scale_up_function( years, birth_rates, smoothness=0.2, method=5) # add case detection process to basic model tb_model.add_transition_flow({ "type": "standard_flows", "parameter": "case_detection", "origin": "infectious", "to": "recovered", }) # Add IPT as a customised flow def ipt_flow_func(model, n_flow, _time, _compartment_values): """ Work out the number of detected individuals from the relevant active TB compartments (with regard to the origin latent compartment of n_flow) multiplied with the proportion of the relevant infected contacts that is from this latent compartment. """ dict_flows = model.transition_flows_dict origin_comp_name = dict_flows["origin"][n_flow] components_latent_comp = find_name_components(origin_comp_name) # find compulsory tags to be found in relevant infectious compartments tags = [] for component in components_latent_comp: if "location_" in component or "strain_" in component: tags.append(component) # loop through all relevant infectious compartments total_tb_detected = 0.0 for comp_ind in model.infectious_indices["all_strains"]: active_components = find_name_components( model.compartment_names[comp_ind]) if all(elem in active_components for elem in tags): infectious_pop = _compartment_values[comp_ind] detection_indices = [ index for index, val in dict_flows["parameter"].items() if "case_detection" in val ] flow_index = [ index for index in detection_indices if dict_flows["origin"][index] == model.compartment_names[comp_ind] ][0] param_name = dict_flows["parameter"][flow_index] detection_tx_rate = model.get_parameter_value( param_name, _time) tsr = mongolia_tsr(_time) + external_params[ "reduction_negative_tx_outcome"] * (1.0 - mongolia_tsr(_time)) if "strain_mdr" in model.compartment_names[comp_ind]: tsr = external_params["mdr_tsr"] * external_params[ "prop_mdr_detected_as_mdr"] if tsr > 0.0: total_tb_detected += infectious_pop * detection_tx_rate / tsr # list all latent compartments relevant to the relevant infectious population relevant_latent_compartments_indices = [ i for i, comp_name in enumerate(model.compartment_names) if find_stem(comp_name) == "early_latent" and all(elem in comp_name for elem in tags) ] total_relevant_latent_size = sum( _compartment_values[i] for i in relevant_latent_compartments_indices) current_latent_size = _compartment_values[ model.compartment_names.index(origin_comp_name)] prop_of_relevant_latent = (current_latent_size / total_relevant_latent_size if total_relevant_latent_size > 0.0 else 0.0) return total_tb_detected * prop_of_relevant_latent tb_model.add_transition_flow({ "type": "customised_flows", "parameter": "ipt_rate", "origin": "early_latent", "to": "recovered", "function": ipt_flow_func, }) # add ACF flow tb_model.add_transition_flow({ "type": "standard_flows", "parameter": "acf_rate", "origin": "infectious", "to": "recovered", }) # load time-variant case detection rate cdr_scaleup_overall = build_mongolia_timevariant_cdr( external_params["cdr_multiplier"]) # targeted TB prevalence proportions by organ prop_smearpos = 0.25 prop_smearneg = 0.40 prop_extrapul = 0.35 # disease duration by organ overall_duration = prop_smearpos * 1.6 + 5.3 * (1 - prop_smearpos) disease_duration = { "smearpos": 1.6, "smearneg": 5.3, "extrapul": 5.3, "overall": overall_duration, } # work out the CDR for smear-positive TB def cdr_smearpos(time): # Had to replace external_params['diagnostic_sensitivity_smearneg'] with its hard-coded value .7 to avoid # cdr_smearpos to be affected when increasing diagnostic_sensitivity_smearneg in interventions (e.g. Xpert) # return (cdr_scaleup_overall(time) / # (prop_smearpos + prop_smearneg * external_params['diagnostic_sensitivity_smearneg'] + # prop_extrapul * external_params['diagnostic_sensitivity_extrapul'])) return cdr_scaleup_overall(time) / ( prop_smearpos + prop_smearneg * 0.7 + prop_extrapul * external_params["diagnostic_sensitivity_extrapul"]) def cdr_smearneg(time): return cdr_smearpos( time) * external_params["diagnostic_sensitivity_smearneg"] def cdr_extrapul(time): return cdr_smearpos( time) * external_params["diagnostic_sensitivity_extrapul"] cdr_by_organ = { "smearpos": cdr_smearpos, "smearneg": cdr_smearneg, "extrapul": cdr_extrapul, "overall": cdr_scaleup_overall, } detect_rate_by_organ = {} for organ in ["smearpos", "smearneg", "extrapul", "overall"]: prop_to_rate = convert_competing_proportion_to_rate( 1.0 / disease_duration[organ]) detect_rate_by_organ[organ] = return_function_of_function( cdr_by_organ[organ], prop_to_rate) # load time-variant treatment success rate mongolia_tsr = build_mongolia_timevariant_tsr() # create a treatment succes rate function adjusted for treatment support intervention tsr_function = lambda t: mongolia_tsr(t) + external_params[ "reduction_negative_tx_outcome"] * (1.0 - mongolia_tsr(t)) # tb control recovery rate (detection and treatment) function set for overall if not organ-specific, smearpos otherwise if "organ" not in stratify_by: tb_control_recovery_rate = lambda t: tsr_function( t) * detect_rate_by_organ["overall"](t) else: tb_control_recovery_rate = lambda t: tsr_function( t) * detect_rate_by_organ["smearpos"](t) # initialise ipt_rate function assuming coverage of 1.0 before age stratification ipt_rate_function = (lambda t: 1.0 * external_params[ "yield_contact_ct_tstpos_per_detected_tb"] * external_params[ "ipt_efficacy"]) # initialise acf_rate function acf_rate_function = ( lambda t: external_params["acf_coverage"] * external_params[ "acf_sensitivity"] * (mongolia_tsr(t) + external_params["reduction_negative_tx_outcome"] * (1.0 - mongolia_tsr(t)))) # assign newly created functions to model parameters tb_model.adaptation_functions["case_detection"] = tb_control_recovery_rate tb_model.parameters["case_detection"] = "case_detection" tb_model.adaptation_functions["ipt_rate"] = ipt_rate_function tb_model.parameters["ipt_rate"] = "ipt_rate" tb_model.adaptation_functions["acf_rate"] = acf_rate_function tb_model.parameters["acf_rate"] = "acf_rate" if "strain" in stratify_by: mdr_adjustment = (external_params["prop_mdr_detected_as_mdr"] * external_params["mdr_tsr"] / 0.9 ) # /.9 for last DS TSR tb_model.stratify( "strain", ["ds", "mdr"], ["early_latent", "late_latent", "infectious"], verbose=False, requested_proportions={"mdr": 0.0}, adjustment_requests={ "contact_rate": { "ds": 1.0, "mdr": 1.0 }, "case_detection": { "mdr": mdr_adjustment }, "ipt_rate": { "ds": 1.0, # external_params['ds_ipt_switch'], "mdr": external_params["mdr_ipt_switch"], }, }, infectiousness_adjustments={ "ds": 1.0, "mdr": external_params["mdr_infectiousness_multiplier"], }, ) tb_model.add_transition_flow({ "type": "standard_flows", "parameter": "dr_amplification", "origin": "infectiousXstrain_ds", "to": "infectiousXstrain_mdr", "implement": len(tb_model.all_stratifications), }) dr_amplification_rate = ( lambda t: detect_rate_by_organ["overall"](t) * (1.0 - mongolia_tsr(t)) * (1.0 - external_params["reduction_negative_tx_outcome"] ) * external_params["dr_amplification_prop_among_nonsuccess"]) tb_model.adaptation_functions[ "dr_amplification"] = dr_amplification_rate tb_model.parameters["dr_amplification"] = "dr_amplification" if "age" in stratify_by: age_breakpoints = [0, 5, 15, 60] age_infectiousness = get_parameter_dict_from_function( logistic_scaling_function(10.0), age_breakpoints) age_params = get_adapted_age_parameters(age_breakpoints) age_params.update(split_age_parameter(age_breakpoints, "contact_rate")) # adjustment of latency parameters for param in ["early_progression", "late_progression"]: for age_break in age_breakpoints: if age_break > 5: age_params[param][ str(age_break) + "W"] *= external_params["adult_latency_adjustment"] death_rates_by_age, death_rate_years = get_death_rates_by_agegroup( age_breakpoints, "MNG") pop_morts = {} for age_group in age_breakpoints: pop_morts[age_group] = scale_up_function( death_rate_years, death_rates_by_age[age_group], smoothness=0.2, method=5) age_params["universal_death_rate"] = {} for age_break in age_breakpoints: tb_model.time_variants["universal_death_rateXage_" + str(age_break)] = pop_morts[age_break] tb_model.parameters[ "universal_death_rateXage_" + str(age_break)] = "universal_death_rateXage_" + str(age_break) age_params["universal_death_rate"][ str(age_break) + "W"] = "universal_death_rateXage_" + str(age_break) tb_model.parameters["universal_death_rateX"] = 0.0 # age-specific IPT ipt_by_age = {"ipt_rate": {}} for age_break in age_breakpoints: ipt_by_age["ipt_rate"][str(age_break)] = external_params[ "ipt_age_" + str(age_break) + "_ct_coverage"] age_params.update(ipt_by_age) # add BCG effect without stratification assuming constant 100% coverage bcg_wane = create_sloping_step_function(15.0, 0.3, 30.0, 1.0) age_bcg_efficacy_dict = get_parameter_dict_from_function( lambda value: bcg_wane(value), age_breakpoints) age_params.update({"contact_rate": age_bcg_efficacy_dict}) tb_model.stratify( "age", deepcopy(age_breakpoints), [], {}, adjustment_requests=age_params, infectiousness_adjustments=age_infectiousness, verbose=False, ) # patch for IPT to overwrite parameters when ds_ipt has been turned off while we still need some coverage at baseline if external_params["ds_ipt_switch"] == 0.0 and external_params[ "mdr_ipt_switch"] == 1.0: tb_model.parameters["ipt_rateXstrain_dsXage_0"] = 0.17 for age_break in [5, 15, 60]: tb_model.parameters["ipt_rateXstrain_dsXage_" + str(age_break)] = 0.0 if "organ" in stratify_by: props_smear = { "smearpos": external_params["prop_smearpos"], "smearneg": 1.0 - (external_params["prop_smearpos"] + 0.20), "extrapul": 0.20, } mortality_adjustments = { "smearpos": 1.0, "smearneg": 0.064, "extrapul": 0.064 } recovery_adjustments = { "smearpos": 1.0, "smearneg": 0.56, "extrapul": 0.56 } # workout the detection rate adjustment by organ status adjustment_smearneg = (detect_rate_by_organ["smearneg"](2015.0) / detect_rate_by_organ["smearpos"](2015.0) if detect_rate_by_organ["smearpos"](2015.0) > 0.0 else 1.0) adjustment_extrapul = (detect_rate_by_organ["extrapul"](2015.0) / detect_rate_by_organ["smearpos"](2015.0) if detect_rate_by_organ["smearpos"](2015.0) > 0.0 else 1.0) tb_model.stratify( "organ", ["smearpos", "smearneg", "extrapul"], ["infectious"], infectiousness_adjustments={ "smearpos": 1.0, "smearneg": 0.25, "extrapul": 0.0, }, verbose=False, requested_proportions=props_smear, adjustment_requests={ "recovery": recovery_adjustments, "infect_death": mortality_adjustments, "case_detection": { "smearpos": 1.0, "smearneg": adjustment_smearneg, "extrapul": adjustment_extrapul, }, "early_progression": props_smear, "late_progression": props_smear, }, ) if "location" in stratify_by: props_location = { "rural_province": 0.48, "urban_nonger": 0.368, "urban_ger": 0.15, "prison": 0.002, } raw_relative_risks_loc = {"rural_province": 1.0} for stratum in ["urban_nonger", "urban_ger", "prison"]: raw_relative_risks_loc[stratum] = external_params[ "rr_transmission_" + stratum] scaled_relative_risks_loc = scale_relative_risks_for_equivalence( props_location, raw_relative_risks_loc) # dummy matrix for mixing by location location_mixing = numpy.array([ 0.899, 0.05, 0.05, 0.001, 0.049, 0.7, 0.25, 0.001, 0.049, 0.25, 0.7, 0.001, 0.1, 0.1, 0.1, 0.7, ]).reshape((4, 4)) location_mixing *= 3.0 # adjusted such that heterogeneous mixing yields similar overall burden as homogeneous location_adjustments = {} for beta_type in ["", "_late_latent", "_recovered"]: location_adjustments["contact_rate" + beta_type] = scaled_relative_risks_loc location_adjustments["acf_rate"] = {} for stratum in [ "rural_province", "urban_nonger", "urban_ger", "prison" ]: location_adjustments["acf_rate"][stratum] = external_params[ "acf_" + stratum + "_switch"] tb_model.stratify( "location", ["rural_province", "urban_nonger", "urban_ger", "prison"], [], requested_proportions=props_location, verbose=False, entry_proportions=props_location, adjustment_requests=location_adjustments, mixing_matrix=location_mixing, ) # tb_model.transition_flows.to_csv("transitions.csv") # tb_model.death_flows.to_csv("deaths.csv") # create some customised derived_outputs if "notifications" in derived_output_types: def notification_function_builder(stratum): """ example of stratum: "Xage_0Xstrain_mdr" """ def calculate_notifications(model, time): total_notifications = 0.0 dict_flows = model.transition_flows_dict comp_ind = model.compartment_names.index("infectious" + stratum) infectious_pop = model.compartment_values[comp_ind] detection_indices = [ index for index, val in dict_flows["parameter"].items() if "case_detection" in val ] flow_index = [ index for index in detection_indices if dict_flows["origin"][index] == model.compartment_names[comp_ind] ][0] param_name = dict_flows["parameter"][flow_index] detection_tx_rate = model.get_parameter_value(param_name, time) tsr = mongolia_tsr(time) + external_params[ "reduction_negative_tx_outcome"] * (1.0 - mongolia_tsr(time)) if "strain_mdr" in model.compartment_names[comp_ind]: tsr = external_params["mdr_tsr"] * external_params[ "prop_mdr_detected_as_mdr"] if tsr > 0.0: total_notifications += infectious_pop * detection_tx_rate / tsr return total_notifications return calculate_notifications for compartment in tb_model.compartment_names: if "infectious" in compartment: stratum = compartment.split("infectious")[1] tb_model.derived_output_functions[ "notifications" + stratum] = notification_function_builder(stratum) # tb_model.derived_output_functions['popsize_treatment_support' + stratum] = notification_function_builder(stratum) if "incidence" in derived_output_types: # add output_connections for all stratum-specific incidence outputs incidence_output_conns = create_output_connections_for_incidence_by_stratum( tb_model.compartment_names) tb_model.output_connections.update(incidence_output_conns) # Create a 'combined incidence' derived output early_names = [ k for k in incidence_output_conns.keys() if k.startswith("incidence_early") ] for early_name in early_names: rootname = early_name[15:] late_name = f"incidence_late{rootname}" combined_name = f"incidence{rootname}" def add_combined_incidence(model, time, e=early_name, l=late_name): time_idx = model.times.index(time) early_incidence = model.derived_outputs[e][time_idx] late_incidence = model.derived_outputs[l][time_idx] return early_incidence + late_incidence tb_model.derived_output_functions[ combined_name] = add_combined_incidence if "mortality" in derived_output_types: # prepare death outputs for all strata tb_model.death_output_categories = list_all_strata_for_mortality( tb_model.compartment_names) ############################################ # population sizes for costing ############################################ if "popsizes" in derived_output_types: # nb of detected individuals by strain: def detected_popsize_function_builder(tag): """ example of tag: "starin_mdr" or "organ_smearpos" """ def calculate_nb_detected(model, time): nb_treated = 0.0 for key, value in model.derived_outputs.items(): if "notifications" in key and tag in key: this_time_index = model.times.index(time) nb_treated += value[this_time_index] return nb_treated return calculate_nb_detected for tag in [ "strain_mdr", "strain_ds", "organ_smearpos", "organ_smearneg", "organ_extrapul", ]: tb_model.derived_output_functions[ "popsizeXnb_detectedX" + tag] = detected_popsize_function_builder(tag) # ACF popsize: number of people screened def popsize_acf(model, time): if external_params["acf_coverage"] == 0.0: return 0.0 pop_urban_ger = sum([ model.compartment_values[i] for i, c_name in enumerate(model.compartment_names) if "location_urban_ger" in c_name ]) return external_params["acf_coverage"] * pop_urban_ger tb_model.derived_output_functions[ "popsizeXnb_screened_acf"] = popsize_acf return tb_model
def time_variant_DST(): return scale_up_function(params['dst'].keys(), params['dst'].values(), smoothness=0.2, method=5)
def build_model(params: dict) -> StratifiedModel: """ Build the master function to run a tuberculosis model """ validate_params(params) # perform validation of parameter format check_param_values(params) # perform validation of some parameter values # Define model times. time = params["time"] integration_times = get_model_times_from_inputs(round(time["start"]), time["end"], time["step"], time["critical_ranges"]) # Define model compartments. compartments = [ Compartment.SUSCEPTIBLE, Compartment.EARLY_LATENT, Compartment.LATE_LATENT, Compartment.INFECTIOUS, Compartment.ON_TREATMENT, Compartment.RECOVERED, ] infectious_comps = [ Compartment.INFECTIOUS, Compartment.ON_TREATMENT, ] # Define initial conditions - 1 infectious person. init_conditions = { Compartment.INFECTIOUS: 1, } # prepare infectiousness adjustment for individuals on treatment treatment_infectiousness_adjustment = [{ "comp_name": Compartment.ON_TREATMENT, "comp_strata": {}, "value": params["on_treatment_infect_multiplier"], }] # Define inter-compartmental flows. flows = deepcopy(preprocess.flows.DEFAULT_FLOWS) # is ACF implemented? implement_acf = len(params["time_variant_acf"]) > 0 if implement_acf: flows.append(preprocess.flows.ACF_FLOW) # is ltbi screening implemented? implement_ltbi_screening = len(params["time_variant_ltbi_screening"]) > 0 if implement_ltbi_screening: flows += preprocess.flows.get_preventive_treatment_flows( params["pt_destination_compartment"]) # Set some parameter values or parameters that require pre-processing ( params, treatment_recovery_func, treatment_death_func, relapse_func, detection_rate_func, acf_detection_rate_func, preventive_treatment_func, contact_rate_functions, ) = preprocess.flows.process_unstratified_parameter_values( params, implement_acf, implement_ltbi_screening) # Create the model. tb_model = StratifiedModel( times=integration_times, compartment_names=compartments, initial_conditions=init_conditions, parameters=params, requested_flows=flows, infectious_compartments=infectious_comps, birth_approach=BirthApproach.ADD_CRUDE, entry_compartment=Compartment.SUSCEPTIBLE, starting_population=int(params["start_population_size"]), ) # register acf_detection_func if acf_detection_rate_func is not None: tb_model.time_variants["acf_detection_rate"] = acf_detection_rate_func # register preventive_treatment_func if preventive_treatment_func is not None: tb_model.time_variants[ "preventive_treatment_rate"] = preventive_treatment_func # register time-variant contact-rate functions: for param_name, func in contact_rate_functions.items(): tb_model.time_variants[param_name] = func # Apply infectiousness adjustment for individuals on treatment tb_model.individual_infectiousness_adjustments = treatment_infectiousness_adjustment # apply age stratification if "age" in params["stratify_by"]: stratify_by_age(tb_model, params, compartments) else: # set time-variant functions for treatment death and relapse rates tb_model.time_variants[ "treatment_recovery_rate"] = treatment_recovery_func tb_model.time_variants["treatment_death_rate"] = treatment_death_func tb_model.time_variants["relapse_rate"] = relapse_func # Load time-variant birth rates birth_rates, years = inputs.get_crude_birth_rate(params["iso3"]) birth_rates = [b / 1000.0 for b in birth_rates ] # birth rates are provided / 1000 population tb_model.time_variants["crude_birth_rate"] = scale_up_function( years, birth_rates, smoothness=0.2, method=5) tb_model.parameters["crude_birth_rate"] = "crude_birth_rate" # apply user-defined stratifications user_defined_stratifications = [ s for s in list(params["user_defined_stratifications"].keys()) if s in params["stratify_by"] ] for stratification in user_defined_stratifications: assert "_" not in stratification, "Stratification name should not include '_'" stratification_details = params["user_defined_stratifications"][ stratification] apply_user_defined_stratification( tb_model, compartments, stratification, stratification_details, implement_acf, implement_ltbi_screening, ) if "organ" in params["stratify_by"]: stratify_by_organ(tb_model, params) else: tb_model.time_variants["detection_rate"] = detection_rate_func # Register derived output functions, which are calculations based on the model's compartment values or flows. # These are calculated after the model is run. outputs.get_all_derived_output_functions(params["calculated_outputs"], params["outputs_stratification"], tb_model) return tb_model
def build_model(params: dict) -> StratifiedModel: """ Build the master function to run the TB model for the Republic of the Marshall Islands :param update_params: dict Any parameters that need to be updated for the current run :return: StratifiedModel The final model with all parameters and stratifications """ # Define compartments and initial conditions. compartments = [ Compartment.SUSCEPTIBLE, Compartment.EARLY_LATENT, Compartment.LATE_LATENT, Compartment.EARLY_INFECTIOUS, Compartment.ON_TREATMENT, Compartment.RECOVERED, # Compartment.LTBI_TREATED, ] init_pop = {Compartment.EARLY_INFECTIOUS: 10, Compartment.LATE_LATENT: 100} model_parameters = params # Update partial immunity/susceptibility parameters model_parameters = update_transmission_parameters( model_parameters, [ Compartment.RECOVERED, Compartment.LATE_LATENT, Compartment.LTBI_TREATED ], ) # Set integration times integration_times = get_model_times_from_inputs( model_parameters["start_time"], model_parameters["end_time"], model_parameters["time_step"], ) # Sequentially add groups of flows to flows list flows = add_standard_infection_flows([]) flows = add_standard_latency_flows(flows) flows = add_standard_natural_history_flows(flows) # flows = add_latency_progression(flows) flows = add_case_detection(flows, compartments) flows = add_treatment_flows(flows) # flows = add_acf(flows, compartments) # flows = add_acf_ltbi(flows) # Make sure incidence and notifications are tracked during integration out_connections = {} out_connections.update( create_request_stratified_incidence( model_parameters["incidence_stratification"], model_parameters["all_stratifications"], )) out_connections.update( create_request_stratified_notifications( model_parameters["notification_stratifications"], model_parameters["all_stratifications"], )) # Define model tb_model = StratifiedModel( integration_times, compartments, init_pop, model_parameters, flows, birth_approach="add_crude_birth_rate", starting_population=model_parameters["start_population"], output_connections=out_connections, death_output_categories=list_all_strata_for_mortality(compartments), ) # Add crude birth rate from UN estimates (using Federated States of Micronesia as a proxy as no data for RMI) birth_rates, years = inputs.get_crude_birth_rate("FSM") tb_model.time_variants["crude_birth_rate"] = scale_up_function( years, birth_rates, smoothness=0.2, method=5) # Find raw case detection rate with multiplier, which is 1 by default, and adjust for differences by organ status cdr_scaleup_raw = build_scale_up_function( model_parameters["cdr"], model_parameters["cdr_multiplier"]) detect_rate_by_organ = find_organ_specific_cdr( cdr_scaleup_raw, model_parameters, model_parameters["all_stratifications"]["organ"], target_organ_props=model_parameters["target_organ_props"], ) # Find base case detection rate and time-variant treatment completion function base_detection_rate = detect_rate_by_organ[ "smearpos" if "organ" in model_parameters["stratify_by"] else "overall"] treatment_success_rate = ( lambda time: build_scale_up_function(model_parameters["tsr"]) (time) / model_parameters["treatment_duration"]) treatment_nonsuccess_rate = ( lambda time: (1.0 - build_scale_up_function(model_parameters["tsr"]) (time)) / model_parameters["treatment_duration"]) # Set acf screening rate using proportion of population reached and duration of intervention # acf_screening_rate = -numpy.log(1 - 0.9) / 0.5 # acf_rate_over_time = progressive_step_function_maker( # 2018.2, 2018.7, acf_screening_rate, scaling_time_fraction=0.3 # ) # Initialise acf_rate function # acf_rate_function = ( # lambda t: model_parameters["acf_coverage"] # * (acf_rate_over_time(t)) # * model_parameters["acf_sensitivity"] # ) # acf_ltbi_rate_function = ( # lambda t: model_parameters["acf_coverage"] # * (acf_rate_over_time(t)) # * model_parameters["acf_ltbi_sensitivity"] # * model_parameters["acf_ltbi_efficacy"] # ) # Assign newly created functions to model parameters add_time_variant_parameter_to_model( tb_model, "case_detection", base_detection_rate, len(model_parameters["stratify_by"]), ) add_time_variant_parameter_to_model( tb_model, "treatment_success", treatment_success_rate, len(model_parameters["stratify_by"]), ) add_time_variant_parameter_to_model( tb_model, "treatment_nonsuccess", treatment_nonsuccess_rate, len(model_parameters["stratify_by"]), ) # add_time_variant_parameter_to_model( # tb_model, 'acf_rate', acf_rate_function, len(model_parameters['stratify_by'])) # add_time_variant_parameter_to_model( # tb_model, 'acf_ltbi_rate', acf_ltbi_rate_function, len(model_parameters['stratify_by'])) # Stratification processes if "age" in model_parameters["stratify_by"]: age_specific_latency_parameters = manually_create_age_specific_latency_parameters( model_parameters) tb_model = stratify_by_age( tb_model, age_specific_latency_parameters, model_parameters["all_stratifications"]["age"], ) if "diabetes" in model_parameters["stratify_by"]: tb_model = stratify_by_diabetes( tb_model, model_parameters, model_parameters["all_stratifications"]["diabetes"], model_parameters["diabetes_target_props"], age_specific_prevalence=False, ) if "organ" in model_parameters["stratify_by"]: tb_model = stratify_by_organ( tb_model, model_parameters, detect_rate_by_organ, model_parameters["all_stratifications"]["organ"], ) if "location" in model_parameters["stratify_by"]: tb_model = stratify_by_location( tb_model, model_parameters, model_parameters["all_stratifications"]["location"], ) # Capture reported prevalence in Majuro assuming over-reporting (needed for calibration) def calculate_reported_majuro_prevalence(model, time): true_prev = 0.0 pop_majuro = 0.0 for i, compartment in enumerate(model.compartment_names): if "majuro" in compartment: pop_majuro += model.compartment_values[i] if "infectious" in compartment: true_prev += model.compartment_values[i] return ( 1.0e5 * true_prev / pop_majuro * (1.0 + model_parameters["over_reporting_prevalence_proportion"])) tb_model.derived_output_functions.update( {"reported_majuro_prevalence": calculate_reported_majuro_prevalence}) return tb_model
def stratify_by_clinical(model, model_parameters, compartments): """ Stratify the infectious compartments of the covid model (not including the pre-symptomatic compartments, which are actually infectious) - notifications are derived from progress from early to late for some strata - the proportion of people moving from presymt to early infectious, conditioned on age group - rate of which people flow through these compartments (reciprocal of time, using within_* which is a rate of ppl / day) - infectiousness levels adjusted by early/late and for clinical strata - we start with an age stratified infection fatality rate - 50% of deaths for each age bracket die in ICU - the other deaths go to hospital, assume no-one else can die from COVID - should we ditch this? """ # General stratification agegroup_strata = model_parameters["all_stratifications"]["agegroup"] all_stratifications = model_parameters["all_stratifications"] clinical_strata = model_parameters["clinical_strata"] # Infection rate multiplication # Importation implement_importation = model_parameters["implement_importation"] traveller_quarantine = model_parameters["traveller_quarantine"] # Time variant case detection prop_detected_among_symptomatic = model_parameters["prop_detected_among_symptomatic"] # FIXME: Make it clear that this for tahn tv_detection_b = model_parameters["tv_detection_b"] tv_detection_c = model_parameters["tv_detection_c"] tv_detection_sigma = model_parameters["tv_detection_sigma"] # ??? within_hospital_early = model_parameters["within_hospital_early"] within_icu_early = model_parameters["within_icu_early"] # Strata entry and infection death proportions icu_prop = model_parameters["icu_prop"] icu_mortality_prop = model_parameters["icu_mortality_prop"] infection_fatality_props_10_year = model_parameters["infection_fatality_props"] hospital_props_10_year = model_parameters["hospital_props"] hospital_props_multiplier = model_parameters["hospital_props_multiplier"] symptomatic_props_10_year = model_parameters["symptomatic_props"] use_raw_mortality_estimates = model_parameters["use_raw_mortality_estimates"] ifr_double_exp_model_params = model_parameters["ifr_double_exp_model_params"] # Define stratification - only stratify infected compartments strata_to_implement = clinical_strata model_parameters["all_stratifications"]["clinical"] = strata_to_implement compartments_to_split = [ comp for comp in compartments if comp.startswith(Compartment.EARLY_INFECTIOUS) or comp.startswith(Compartment.LATE_INFECTIOUS) ] # FIXME: Set params to make comparison happy model_parameters["infection_fatality_props"] = infection_fatality_props_10_year model_parameters["hospital_props"] = hospital_props_10_year # Calculate the proportion of 80+ years old among the 75+ population elderly_populations = get_population_by_agegroup([0, 75, 80], model_parameters["iso3"], model_parameters["region"]) prop_over_80 = elderly_populations[2] / sum(elderly_populations[1:]) # Age dependent proportions of infected people who become symptomatic. # This is defined 8x10 year bands, 0-70+, which we transform into 16x5 year bands 0-75+ symptomatic_props = repeat_list_elements(2, symptomatic_props_10_year) # Age dependent proportions of symptomatic people who become hospitalised. # This is defined 9x10 year bands, 0-80+, which we trransform into 16x5 year bands 0-75+ # Calculate 75+ age bracket as half 75-79 and half 80+ hospital_props = repeat_list_elements_average_last_two( [p * hospital_props_multiplier for p in hospital_props_10_year], prop_over_80) # Infection fatality rate by age group. if use_raw_mortality_estimates: # Data in props used 10 year bands 0-80+, but we want 5 year bands from 0-75+ # Calculate 75+ age bracket as weighted average between 75-79 and half 80+ infection_fatality_props = repeat_list_elements_average_last_two( infection_fatality_props_10_year, prop_over_80 ) else: infection_fatality_props = age_specific_ifrs_from_double_exp_model( ifr_double_exp_model_params['k'], ifr_double_exp_model_params['m'], ifr_double_exp_model_params['last_representative_age'] ) # Find the absolute progression proportions. symptomatic_props_arr = np.array(symptomatic_props) hospital_props_arr = np.array(hospital_props) # Determine the absolute proportion of presymptomatic who become sympt vs non-sympt. sympt, non_sympt = subdivide_props(1, symptomatic_props_arr) # Determine the absolute proportion of sympt who become hospitalized vs non-hospitalized. sympt_hospital, sympt_non_hospital = subdivide_props(sympt, hospital_props_arr) # Determine the absolute proportion of hospitalized who become icu vs non-icu. sympt_hospital_icu, sympt_hospital_non_icu = subdivide_props(sympt_hospital, icu_prop) abs_props = { "sympt": sympt.tolist(), "non_sympt": non_sympt.tolist(), "hospital": sympt_hospital.tolist(), "sympt_non_hospital": sympt_non_hospital.tolist(), # Overidden by a by time-varying proprotion later "icu": sympt_hospital_icu.tolist(), "hospital_non_icu": sympt_hospital_non_icu.tolist(), } # Calculate the absolute proportion of all patients who should eventually reach hospital death or ICU death. # Find IFR that needs to be contributed by ICU and non-ICU hospital deaths hospital_death, icu_death = [], [] for age_idx, agegroup in enumerate(agegroup_strata): # If IFR for age group is greater than absolute proportion hospitalised, increased hospitalised proportion if infection_fatality_props[age_idx] > abs_props["hospital"][age_idx]: abs_props["hospital"][age_idx] = infection_fatality_props[age_idx] # Find the target absolute ICU mortality and the amount left over from IFRs to go to hospital, if any target_icu_abs_mort = abs_props["icu"][age_idx] * icu_mortality_prop left_over_mort = infection_fatality_props[age_idx] - target_icu_abs_mort # If some IFR will be left over for the hospitalised if left_over_mort > 0.0: hospital_death_prop = left_over_mort icu_death_prop = target_icu_abs_mort # Otherwise if all IFR taken up by ICU else: hospital_death_prop = 0.0 icu_death_prop = infection_fatality_props[age_idx] hospital_death.append(hospital_death_prop) icu_death.append(icu_death_prop) abs_props.update({"hospital_death": hospital_death, "icu_death": icu_death}) # FIXME: These depend on static variables which have been made time-variant. # fatality rate for hospitalised patients rel_props = { "hospital_death": element_wise_list_division( abs_props["hospital_death"], abs_props["hospital_non_icu"] ), "icu_death": element_wise_list_division(abs_props["icu_death"], abs_props["icu"]), } # Progression rates into the infectious compartment(s) # Define progresion rates into non-symptomatic compartments using parameter adjustment. stratification_adjustments = {} for age_idx, age in enumerate(agegroup_strata): key = f"to_infectiousXagegroup_{age}" stratification_adjustments[key] = { "non_sympt": non_sympt[age_idx], "icu": sympt_hospital_icu[age_idx], "hospital_non_icu": sympt_hospital_non_icu[age_idx], } # Create a function for the proportion of symptomatic people who are detected at timestep `t`. scale_up_multiplier = \ tanh_based_scaleup(tv_detection_b, tv_detection_c, tv_detection_sigma) # Create function describing the proportion of cases detected over time def prop_detect_among_sympt_func(t): # Raw value without adjustment for any improved detection intervention without_intervention_value = prop_detected_among_symptomatic * scale_up_multiplier(t) # Return value modified for any future intervention that narrows the case detection gap int_detect_gap_reduction = model_parameters['int_detection_gap_reduction'] return without_intervention_value + (1. - without_intervention_value) * int_detect_gap_reduction # Set time-varying isolation proportions for age_idx, agegroup in enumerate(agegroup_strata): # Pass the functions to the model tv_props = TimeVaryingProprotions(age_idx, abs_props, prop_detect_among_sympt_func) time_variants = [ [f"prop_sympt_non_hospital_{agegroup}", tv_props.get_abs_prop_sympt_non_hospital,], [f"prop_sympt_isolate_{agegroup}", tv_props.get_abs_prop_isolated], ] for name, func in time_variants: model.time_variants[name] = func # Tell the model to use these time varying functions for the stratification adjustments. agegroup_adj = stratification_adjustments[f"to_infectiousXagegroup_{agegroup}"] agegroup_adj["sympt_non_hospital"] = f"prop_sympt_non_hospital_{agegroup}" agegroup_adj["sympt_isolate"] = f"prop_sympt_isolate_{agegroup}" # Calculate death rates and progression rates for hospitalised and ICU patients progression_death_rates = {} for stratum in ("hospital", "icu"): ( progression_death_rates[stratum + "_infect_death"], progression_death_rates[stratum + "_within_late"], ) = find_rates_and_complements_from_ifr( rel_props[stratum + "_death"], 1, [model_parameters["within_" + stratum + "_late"]] * 16, ) # Death and non-death progression between infectious compartments towards the recovered compartment for param in ("within_late", "infect_death"): stratification_adjustments.update( adjust_upstream_stratified_parameter( param, strata_to_implement[3:], "agegroup", model_parameters["all_stratifications"]["agegroup"], [ progression_death_rates["hospital_" + param], progression_death_rates["icu_" + param], ], overwrite=True, ) ) # Over-write rate of progression for early compartments for hospital and ICU # FIXME: Ask Romain if he knows why we bother doing this. stratification_adjustments.update( { "within_infectious": { "hospital_non_icuW": model_parameters["within_hospital_early"], "icuW": model_parameters["within_icu_early"], }, } ) # Sort out all infectiousness adjustments for entire model here # Sort out all infectiousness adjustments for all compartments of the model. # Now make adjustment for asymptomatic patients only strata_infectiousness = {} for stratum in strata_to_implement: if stratum + "_infect_multiplier" in model_parameters: strata_infectiousness[stratum] = model_parameters[stratum + "_infect_multiplier"] # Make adjustment for isolation/quarantine for stratum in strata_to_implement: if stratum in model_parameters["late_infect_multiplier"]: model.individual_infectiousness_adjustments.append( [ [Compartment.LATE_INFECTIOUS, "clinical_" + stratum], model_parameters["late_infect_multiplier"][stratum], ] ) # FIXME: Ask Romain about importation # work out time-variant clinical proportions for imported cases accounting for quarantine if model_parameters["implement_importation"]: rep_age_group = ( "35" # the clinical split will be defined according to this representative age-group ) tvs = model.time_variants # to reduce verbosity # create scale-up function for quarantine quarantine_scale_up = scale_up_function( model_parameters["traveller_quarantine"]["times"], model_parameters["traveller_quarantine"]["values"], method=4, ) # set fixed clinical proportions for imported cases (hospital_non_icu and icu) importation_props_by_clinical = { "hospital_non_icu": stratification_adjustments[ "to_infectiousXagegroup_" + rep_age_group ]["hospital_non_icu"], "icu": stratification_adjustments["to_infectiousXagegroup_" + rep_age_group]["icu"], } # create time-variant function for remaining imported clinical proportions tv_prop_imported_non_sympt = lambda t: stratification_adjustments[ "to_infectiousXagegroup_" + rep_age_group ]["non_sympt"] * (1.0 - quarantine_scale_up(t)) tv_prop_imported_sympt_non_hospital = lambda t: tvs[ stratification_adjustments["to_infectiousXagegroup_" + rep_age_group][ "sympt_non_hospital" ] ](t) * (1.0 - quarantine_scale_up(t)) tv_prop_imported_sympt_isolate = lambda t: tvs[ stratification_adjustments["to_infectiousXagegroup_" + rep_age_group]["sympt_isolate"] ](t) + quarantine_scale_up(t) * ( tvs[ stratification_adjustments["to_infectiousXagegroup_" + rep_age_group][ "sympt_non_hospital" ] ](t) + stratification_adjustments["to_infectiousXagegroup_" + rep_age_group]["non_sympt"] ) # Pass time-variant functions to the model object model.time_variants["tv_prop_imported_non_sympt"] = tv_prop_imported_non_sympt model.time_variants[ "tv_prop_imported_sympt_non_hospital" ] = tv_prop_imported_sympt_non_hospital model.time_variants["tv_prop_imported_sympt_isolate"] = tv_prop_imported_sympt_isolate for stratum in ["non_sympt", "sympt_isolate", "sympt_non_hospital"]: importation_props_by_clinical[stratum] = "tv_prop_imported_" + stratum # create absolute time-variant case detection proportion that will be returned to be used to set importation flow def modelled_abs_detection_proportion_imported(t): return ( stratification_adjustments["to_infectiousXagegroup_" + rep_age_group]["icu"] + stratification_adjustments["to_infectiousXagegroup_" + rep_age_group][ "hospital_non_icu" ] + tvs[ stratification_adjustments["to_infectiousXagegroup_" + rep_age_group][ "sympt_isolate" ] ](t) ) else: importation_props_by_clinical = {} modelled_abs_detection_proportion_imported = None # Stratify the model using the SUMMER stratification function model.stratify( "clinical", strata_to_implement, compartments_to_split, infectiousness_adjustments=strata_infectiousness, requested_proportions={ stratum: 1.0 / len(strata_to_implement) for stratum in strata_to_implement }, adjustment_requests=stratification_adjustments, entry_proportions=importation_props_by_clinical, verbose=False, ) return modelled_abs_detection_proportion_imported
def stratify_by_age(model, params, compartments): flow_adjustments = {} # age-specific all-causes mortality rate death_rates_by_age, death_rate_years = get_death_rates_by_agegroup( params["age_breakpoints"], params["iso3"]) flow_adjustments["universal_death_rate"] = {} for age_group in params["age_breakpoints"]: name = "universal_death_rate_" + str(age_group) flow_adjustments["universal_death_rate"][str(age_group)] = name model.time_variants[name] = scale_up_function( death_rate_years, death_rates_by_age[age_group], smoothness=0.2, method=5) model.parameters[name] = name # age-specific latency progression rates flow_adjustments.update( get_adapted_age_parameters(params["age_breakpoints"], params["age_specific_latency"])) if params["inflate_reactivation_for_diabetes"]: flow_adjustments = edit_adjustments_for_diabetes( model, flow_adjustments, params["age_breakpoints"], params["extra_params"]["prop_diabetes"], params["extra_params"]["rr_progression_diabetes"], params["extra_params"]["future_diabetes_multiplier"], ) # age-specific infectiousness strata_infectiousness = calculate_age_specific_infectiousness( params["age_breakpoints"], params["age_infectiousness_switch"]) # age-specific treatment recovery, relapse and treatment death rates time_variant_tsr = scale_up_function( list(params["time_variant_tsr"].keys()), list(params["time_variant_tsr"].values()), method=4, ) factory_functions = { "treatment_recovery_rate": make_treatment_recovery_func, "treatment_death_rate": make_treatment_death_func, "relapse_rate": make_relapse_rate_func, } for param_stem in factory_functions: flow_adjustments[param_stem] = {} for age_group in params["age_breakpoints"]: flow_adjustments[param_stem][str( age_group)] = param_stem + "_" + str(age_group) model.time_variants[ param_stem + "_" + str(age_group)] = factory_functions[param_stem]( age_group, model, params, time_variant_tsr) model.parameters[ param_stem + "_" + str(age_group)] = param_stem + "_" + str(age_group) # get mixing matrix mixing_matrix = get_mixing_matrix_specific_agegroups( params["iso3"], params["age_breakpoints"]) # add BCG effect without stratifying for BCG bcg_wane = create_sloping_step_function(15.0, 0.3, 30.0, 1.0) bcg_susceptibility_multilier_dict = get_parameter_dict_from_function( lambda value: bcg_wane(value), params["age_breakpoints"]) bcg_coverage_func = scale_up_function( list(params["time_variant_bcg_perc"].keys()), list(params["time_variant_bcg_perc"].values()), method=5, bound_low=0, bound_up=100, smoothness=1.5, ) for agegroup, multiplier in bcg_susceptibility_multilier_dict.items(): if multiplier < 1.0: average_age = get_average_age_for_bcg(agegroup, params["age_breakpoints"]) name = "contact_rate_" + agegroup bcg_susceptibility_multilier_dict[agegroup] = name model.time_variants[name] = make_bcg_multiplier_func( bcg_coverage_func, multiplier, average_age) model.parameters[name] = name flow_adjustments.update( {"contact_rate": bcg_susceptibility_multilier_dict}) # trigger model stratification model.stratify( "age", params["age_breakpoints"], compartments, infectiousness_adjustments=strata_infectiousness, flow_adjustments=flow_adjustments, mixing_matrix=mixing_matrix, )
def process_unstratified_parameter_values(params, implement_acf, implement_ltbi_screening): """ This function calculates some unstratified parameter values for parameters that need pre-processing. This usually involves combining multiple input parameters to determine a model parameter :return: """ # Set unstratified detection flow parameter if "organ" in params["stratify_by"]: params["detection_rate"] = 1 detection_rate_func = None else: screening_rate_func = tanh_based_scaleup( params["time_variant_tb_screening_rate"]["shape"], params["time_variant_tb_screening_rate"]["inflection_time"], params["time_variant_tb_screening_rate"]["lower_asymptote"], params["time_variant_tb_screening_rate"]["upper_asymptote"], ) def detection_rate_func(t): return screening_rate_func(t) * params["passive_screening_sensitivity"]["unstratified"] params["detection_rate"] = "detection_rate" # Set unstratified treatment-outcome-related parameters if ( "age" in params["stratify_by"] ): # relapse and treatment death need to be adjusted by age later params["treatment_recovery_rate"] = 1.0 params["treatment_death_rate"] = 1.0 params["relapse_rate"] = 1.0 treatment_recovery_func = None treatment_death_func = None relapse_func = None else: time_variant_tsr = scale_up_function( list(params["time_variant_tsr"].keys()), list(params["time_variant_tsr"].values()), method=4, ) def treatment_recovery_func(t): return max( 1 / params["treatment_duration"], params["universal_death_rate"] / params["prop_death_among_negative_tx_outcome"] * (1.0 / (1.0 - time_variant_tsr(t)) - 1.0), ) def treatment_death_func(t): return ( params["prop_death_among_negative_tx_outcome"] * treatment_recovery_func(t) * (1.0 - time_variant_tsr(t)) / time_variant_tsr(t) - params["universal_death_rate"] ) def relapse_func(t): return (treatment_death_func(t) + params["universal_death_rate"]) * ( 1.0 / params["prop_death_among_negative_tx_outcome"] - 1.0 ) params["treatment_recovery_rate"] = "treatment_recovery_rate" params["treatment_death_rate"] = "treatment_death_rate" params["relapse_rate"] = "relapse_rate" # adjust late reactivation parameters using multiplier for latency_stage in ["early", "late"]: param_name = f"{latency_stage}_activation_rate" for key in params["age_specific_latency"][param_name]: params["age_specific_latency"][param_name][key] *= params["progression_multiplier"] # load unstratified latency parameters params = get_unstratified_parameter_values(params) # set reinfection contact rate parameters for state in ["latent", "recovered"]: params["contact_rate_from_" + state] = ( params["contact_rate"] * params["rr_infection_" + state] ) # assign unstratified parameter values to infection death and self-recovery processes for param_name in ["infect_death_rate", "self_recovery_rate"]: params[param_name] = params[param_name + "_dict"]["unstratified"] # if age-stratification is used, the baseline mortality rate is set to 1 so it can get multiplied by a time-variant if "age" in params["stratify_by"]: params["universal_death_rate"] = 1.0 # PT in household contacts contact_rate_functions = {} if params["hh_contacts_pt"]: scaleup_screening_prop = scale_up_function( x=[params["hh_contacts_pt"]["start_time"], params["hh_contacts_pt"]["start_time"] + 1], y=[0, params["hh_contacts_pt"]["prop_hh_contacts_screened"]], method=4, ) def make_contact_rate_func(raw_contact_rate_value): def contact_rate_func(t): rel_reduction = ( params["hh_contacts_pt"]["prop_smearpos_among_prev_tb"] * params["hh_contacts_pt"]["prop_hh_transmission"] * scaleup_screening_prop(t) * params["ltbi_screening_sensitivity"] * params["hh_contacts_pt"]["prop_pt_completion"] ) return raw_contact_rate_value * (1 - rel_reduction) return contact_rate_func for suffix in ["", "_from_latent", "_from_recovered"]: param_name = f"contact_rate{suffix}" raw_value = params[param_name] params[param_name] = param_name contact_rate_functions[param_name] = make_contact_rate_func(raw_value) # ACF flow parameter acf_detection_func = None if implement_acf: if ( len(params["time_variant_acf"]) == 1 and params["time_variant_acf"][0]["stratum_filter"] is None ): # universal ACF is applied acf_detection_func = scale_up_function( list(params["time_variant_acf"][0]["time_variant_screening_rate"].keys()), [ v * params["acf_screening_sensitivity"] for v in list( params["time_variant_acf"][0]["time_variant_screening_rate"].values() ) ], method=4, ) params["acf_detection_rate"] = "acf_detection_rate" else: params["acf_detection_rate"] = 1.0 # Preventive treatment flow parameters preventive_treatment_func = None if implement_ltbi_screening: if ( len(params["time_variant_ltbi_screening"]) == 1 and params["time_variant_ltbi_screening"][0]["stratum_filter"] is None ): # universal LTBI screening is applied preventive_treatment_func = scale_up_function( list( params["time_variant_ltbi_screening"][0]["time_variant_screening_rate"].keys() ), [ v * params["ltbi_screening_sensitivity" * params["pt_efficacy"]] for v in list( params["time_variant_ltbi_screening"][0][ "time_variant_screening_rate" ].values() ) ], method=4, ) params["preventive_treatment_rate"] = "preventive_treatment_rate" else: params["preventive_treatment_rate"] = 1.0 return ( params, treatment_recovery_func, treatment_death_func, relapse_func, detection_rate_func, acf_detection_func, preventive_treatment_func, contact_rate_functions, )
def stratify_by_clinical(_covid_model, model_parameters, compartments): """ Stratify the infectious compartments of the covid model (not including the pre-symptomatic compartments, which are actually infectious) """ for i_age, h_prop in enumerate(model_parameters['hospital_props']): if h_prop > model_parameters['prop_detected_among_symptomatic']: print("Warning: Hospital proportions had to be reduced for age-group " + str(i_age)) model_parameters['hospital_props'][i_age] = model_parameters['prop_detected_among_symptomatic'] # Define stratification strata_to_implement = \ model_parameters["clinical_strata"] model_parameters["all_stratifications"]["clinical"] = \ strata_to_implement compartments_to_split = [ comp for comp in compartments if comp.startswith(Compartment.EARLY_INFECTIOUS) or comp.startswith(Compartment.LATE_INFECTIOUS) ] # Find unadjusted parameters model_parameters.update(get_raw_clinical_props(model_parameters)) # Find the absolute progression proportions from the requested splits abs_props = split_prop_into_two_subprops([1.0] * 16, "", model_parameters["raw_sympt"], "sympt") abs_props.update( split_prop_into_two_subprops(abs_props["sympt"], "sympt", model_parameters["raw_hospital"], "hospital") ) abs_props.update( split_prop_into_two_subprops(abs_props["hospital"], "hospital", [model_parameters["icu_prop"]] * 16, "icu") ) # Find the absolute proportion dying in hospital and in ICU abs_props.update(find_abs_death_props(model_parameters, abs_props)) # CFR for non-ICU hospitalised patients rel_props = { "hospital_death": element_wise_list_division(abs_props["hospital_death"], abs_props["hospital_non_icu"]), "icu_death": element_wise_list_division(abs_props["icu_death"], abs_props["icu"]) } # Progression rates into the infectious compartment(s) fixed_prop_strata = ["non_sympt"] stratification_adjustments = adjust_upstream_stratified_parameter( "to_infectious", fixed_prop_strata, "agegroup", model_parameters["all_stratifications"]["agegroup"], [abs_props[stratum] for stratum in fixed_prop_strata], ) # Set time-variant proportion of sympt_isolate among all symptomatics # create a scale-up function converging to 1 scale_up_multiplier = tanh_based_scaleup(model_parameters['tv_detection_b'], model_parameters['tv_detection_c'], model_parameters['tv_detection_sigma']) # use the input parameter 'prop_detected_among_symptomatic', specifying the maximum prop of isolates among all sympt _tv_prop_detect_among_sympt = lambda t: model_parameters['prop_detected_among_symptomatic'] * scale_up_multiplier(t) # Set isolation rates as absolute proportions _covid_model, stratification_adjustments = \ set_isolation_props(_covid_model, model_parameters, abs_props, stratification_adjustments, _tv_prop_detect_among_sympt) # Calculate death rates and progression rates for hospitalised and ICU patients progression_death_rates = {} for stratum in ("hospital", "icu"): ( progression_death_rates[stratum + "_infect_death"], progression_death_rates[stratum + "_within_late"], ) = find_rates_and_complements_from_ifr( rel_props[stratum + "_death"], model_parameters["n_compartment_repeats"][Compartment.LATE_INFECTIOUS], [model_parameters["within_" + stratum + "_late"]] * 16, ) # Death and non-death progression between infectious compartments towards the recovered compartment for param in ("within_late", "infect_death"): stratification_adjustments.update( adjust_upstream_stratified_parameter( param, strata_to_implement[3:], "agegroup", model_parameters["all_stratifications"]["agegroup"], [progression_death_rates["hospital_" + param], progression_death_rates["icu_" + param]], overwrite=True, ) ) # Over-write rate of progression for early compartments for hospital and ICU stratification_adjustments.update( {"within_infectious": {"hospital_non_icuW": model_parameters['within_hospital_early'], "icuW": model_parameters['within_icu_early']}, } ) # Sort out all infectiousness adjustments for entire model here _covid_model, stratification_adjustments, strata_infectiousness = \ adjust_infectiousness(_covid_model, model_parameters, strata_to_implement, stratification_adjustments) # work out time-variant clinical proportions for imported cases accounting for quarantine if model_parameters['implement_importation'] and model_parameters['imported_cases_explict']: rep_age_group = '35' # the clinical split will be defined according to this representative age-group tvs = _covid_model.time_variants # to reduce verbosity quarantine_scale_up = scale_up_function(model_parameters['traveller_quarantine']['times'], model_parameters['traveller_quarantine']['values'], method=4 ) tv_prop_imported_non_sympt =\ lambda t: stratification_adjustments['to_infectiousXagegroup_' + rep_age_group]['non_sympt'] *\ (1. - quarantine_scale_up(t)) tv_prop_imported_sympt_non_hospital = \ lambda t: tvs[stratification_adjustments['to_infectiousXagegroup_' + rep_age_group]['sympt_non_hospital']](t) *\ (1. - quarantine_scale_up(t)) tv_prop_imported_sympt_isolate =\ lambda t: tvs[stratification_adjustments['to_infectiousXagegroup_' + rep_age_group]['sympt_isolate']](t) +\ quarantine_scale_up(t) *\ (tvs[stratification_adjustments['to_infectiousXagegroup_' + rep_age_group]['sympt_non_hospital']](t) + stratification_adjustments['to_infectiousXagegroup_' + rep_age_group]['non_sympt']) tv_prop_imported_hospital_non_icu = lambda t: \ tvs[stratification_adjustments['to_infectiousXagegroup_' + rep_age_group]['hospital_non_icu']](t) tv_prop_imported_icu = lambda t: \ tvs[stratification_adjustments['to_infectiousXagegroup_' + rep_age_group]['icu']](t) _covid_model.time_variants['tv_prop_imported_non_sympt'] = tv_prop_imported_non_sympt _covid_model.time_variants['tv_prop_imported_sympt_non_hospital'] = tv_prop_imported_sympt_non_hospital _covid_model.time_variants['tv_prop_imported_sympt_isolate'] = tv_prop_imported_sympt_isolate _covid_model.time_variants['tv_prop_imported_hospital_non_icu'] = tv_prop_imported_hospital_non_icu _covid_model.time_variants['tv_prop_imported_icu'] = tv_prop_imported_icu importation_props_by_clinical = {} for stratum in strata_to_implement: importation_props_by_clinical[stratum] = "tv_prop_imported_" + stratum else: importation_props_by_clinical = {} # Stratify the model using the SUMMER stratification function _covid_model.stratify( "clinical", strata_to_implement, compartments_to_split, infectiousness_adjustments=strata_infectiousness, requested_proportions={ stratum: 1.0 / len(strata_to_implement) for stratum in strata_to_implement }, adjustment_requests=stratification_adjustments, entry_proportions=importation_props_by_clinical, verbose=False, ) return _covid_model, model_parameters