def test_stochastic_exit_flows(pop, rtol, deathrate): """ Check that death flows produce outputs that tend towards mean as pop increases. """ model = CompartmentalModel( times=[0, 10], compartments=["S", "I", "R"], infectious_compartments=["I"] ) s_pop = 0.80 * pop i_pop = 0.20 * pop model.set_initial_population(distribution={"S": s_pop, "I": i_pop}) model.add_universal_death_flows("deaths", deathrate) model.run_stochastic(RANDOM_SEED) # No change to recovered compartments assert_array_equal(model.outputs[:, 2], 0) # Calculate births using mean birth rate mean_s = np.zeros_like(model.times) mean_i = np.zeros_like(model.times) mean_s[0] = s_pop mean_i[0] = i_pop for i in range(1, len(model.times)): mean_s[i] = mean_s[i - 1] - deathrate * mean_s[i - 1] mean_i[i] = mean_i[i - 1] - deathrate * mean_i[i - 1] # All S and I compartment sizes are are within the error range # of the mean of the multinomial dist that determines exit. assert_allclose(model.outputs[:, 0], mean_s, rtol=rtol) assert_allclose(model.outputs[:, 1], mean_i, rtol=rtol)
def test_model__with_birth_and_death_rate_replace_deaths__expect_pop_static_overall(): model = CompartmentalModel( times=[0, 5], compartments=["S", "I", "R"], infectious_compartments=["I"] ) model.set_initial_population(distribution={"S": 100, "I": 100}) model.add_replacement_birth_flow("births", "S") # Add some dying at ~2 people / 100 / year. model.add_universal_death_flows("deaths", 0.02) model.run() expected_outputs = np.array( [ [100.0, 100.0, 0], # Initial conditions [102.0, 98.0, 0], [104.0, 96.0, 0], [105.8, 94.2, 0], # Tweaked. [107.7, 92.3, 0], # Tweaked. [109.5, 90.5, 0], # Tweaked. ] ) assert_allclose(model.outputs, expected_outputs, atol=0.1, verbose=True)
def test_model__with_higher_birth_than_and_death_rate__expect_pop_increase(): model = CompartmentalModel( times=[0, 5], compartments=["S", "I", "R"], infectious_compartments=["I"] ) model.set_initial_population(distribution={"S": 100, "I": 100}) # Add some babies at ~10 babies / 100 / year. model.add_crude_birth_flow("births", 0.1, "S") # Add some dying at ~2 people / 100 / year. model.add_universal_death_flows("deaths", 0.02) model.run() expected_outputs = np.array( [ [100.0, 100.0, 0], # Initial conditions [118.6, 98.0, 0], # Tweaked ~0.1 [138.6, 96.1, 0], # Tweaked ~0.4 [160.1, 94.2, 0], # Tweaked ~0.9 [183.1, 92.3, 0], # Tweaked ~1.7 [207.9, 90.5, 0], # Tweaked ~2.7 ] ) assert_allclose(model.outputs, expected_outputs, atol=0.1, verbose=True)
def test_model__with_death_rate__expect_pop_decrease(): """ Ensure that a model with two compartments and only death rate dynamics results in fewer people. """ model = CompartmentalModel( times=[0, 5], compartments=["S", "I", "R"], infectious_compartments=["I"] ) model.set_initial_population(distribution={"S": 100, "I": 100}) # Add some dying at ~2 people / 100 / year. model.add_universal_death_flows("deaths", 0.02) model.run() # Expect that we have fewer people in the population per year expected_outputs = np.array( [ [100.0, 100.0, 0], # Initial conditions [98.0, 98.0, 0], [96.1, 96.1, 0], [94.2, 94.2, 0], [92.3, 92.3, 0], [90.5, 90.5, 0], ] ) assert_allclose(model.outputs, expected_outputs, atol=0.1, verbose=True)
def test_model__with_complex_dynamics__expect_correct_outputs(): """ Ensure that a model with the "full suite" of TB dynamics produces correct results: - 5 compartments - birth rate + universal death rate - standard inter-compartment flows """ model = CompartmentalModel( times=[0, 5], compartments=["S", "EL", "LL", "I", "R"], infectious_compartments=["I"] ) model.set_initial_population(distribution={"S": 900, "I": 100}) model.add_crude_birth_flow("births", 0.02, "S") model.add_universal_death_flows("universal_deaths", 0.02) model.add_infection_frequency_flow("infection", 14, "S", "EL") model.add_infection_frequency_flow("reinfection", 14, "R", "EL") model.add_infection_frequency_flow("regression", 3, "LL", "EL") model.add_transition_flow("early_progression", 2, "EL", "I") model.add_transition_flow("stabilisation", 3, "EL", "LL") model.add_transition_flow("early_progression", 1, "LL", "I") model.add_death_flow("infect_death", 0.4, "I") model.add_transition_flow("recovery", 0.2, "I", "R") model.add_transition_flow("case_detection", 1, "I", "R") model.run() # Expect that the results are consistent, nothing crazy happens. # These results were not independently calculated, so this is more of an "acceptance test". expected_outputs = np.array( [ [900.0, 0.0, 0.0, 100.0, 0.0], [66.1, 203.8, 274.2, 307.2, 75.3], [2.9, 150.9, 220.5, 345.3, 69.4], [2.2, 127.3, 175.6, 297.0, 58.1], [1.8, 106.4, 145.6, 248.8, 48.5], [1.5, 88.8, 121.5, 207.8, 40.5], ] ) assert_allclose(model.outputs, expected_outputs, atol=0.2, verbose=True)
def build_model(params: dict) -> CompartmentalModel: time = params["time"] model = CompartmentalModel( times=[time["start"], time["end"]], compartments=COMPARTMENTS, infectious_compartments=INFECTIOUS_COMPS, timestep=time["step"], ) # Add initial population init_pop = { Compartment.EARLY_LATENT: params["initial_early_latent_population"], Compartment.LATE_LATENT: params["initial_late_latent_population"], Compartment.INFECTIOUS: params["initial_infectious_population"], Compartment.DETECTED: params["initial_detected_population"], Compartment.ON_TREATMENT: params["initial_on_treatment_population"], Compartment.RECOVERED: 0, } sum_init_pop = sum(init_pop.values()) init_pop[Compartment. SUSCEPTIBLE] = params["start_population_size"] - sum_init_pop model.set_initial_population(init_pop) # Add inter-compartmental flows params = _get_derived_params(params) # Entry flows model.add_crude_birth_flow( "birth", params["crude_birth_rate"], Compartment.SUSCEPTIBLE, ) # Infection flows. model.add_infection_frequency_flow( "infection", params["contact_rate"], Compartment.SUSCEPTIBLE, Compartment.EARLY_LATENT, ) model.add_infection_frequency_flow( "infection_from_latent", params["contact_rate_from_latent"], Compartment.LATE_LATENT, Compartment.EARLY_LATENT, ) model.add_infection_frequency_flow( "infection_from_recovered", params["contact_rate_from_recovered"], Compartment.RECOVERED, Compartment.EARLY_LATENT, ) # Transition flows. model.add_fractional_flow( "treatment_early", params["preventive_treatment_rate"], Compartment.EARLY_LATENT, Compartment.RECOVERED, ) model.add_fractional_flow( "treatment_late", params["preventive_treatment_rate"], Compartment.LATE_LATENT, Compartment.RECOVERED, ) model.add_fractional_flow( "stabilisation", params["stabilisation_rate"], Compartment.EARLY_LATENT, Compartment.LATE_LATENT, ) model.add_fractional_flow( "early_activation", params["early_activation_rate"], Compartment.EARLY_LATENT, Compartment.INFECTIOUS, ) model.add_fractional_flow( "late_activation", params["late_activation_rate"], Compartment.LATE_LATENT, Compartment.INFECTIOUS, ) # Post-active-disease flows model.add_fractional_flow( "detection", params["detection_rate"], Compartment.INFECTIOUS, Compartment.DETECTED, ) model.add_fractional_flow( "treatment_commencement", params["treatment_commencement_rate"], Compartment.DETECTED, Compartment.ON_TREATMENT, ) model.add_fractional_flow( "missed_to_active", params["missed_to_active_rate"], Compartment.DETECTED, Compartment.INFECTIOUS, ) model.add_fractional_flow( "self_recovery_infectious", params["self_recovery_rate"], Compartment.INFECTIOUS, Compartment.LATE_LATENT, ) model.add_fractional_flow( "self_recovery_detected", params["self_recovery_rate"], Compartment.DETECTED, Compartment.LATE_LATENT, ) model.add_fractional_flow( "treatment_recovery", params["treatment_recovery_rate"], Compartment.ON_TREATMENT, Compartment.RECOVERED, ) model.add_fractional_flow( "treatment_default", params["treatment_default_rate"], Compartment.ON_TREATMENT, Compartment.INFECTIOUS, ) model.add_fractional_flow( "failure_retreatment", params["failure_retreatment_rate"], Compartment.ON_TREATMENT, Compartment.DETECTED, ) model.add_fractional_flow( "spontaneous_recovery", params["spontaneous_recovery_rate"], Compartment.ON_TREATMENT, Compartment.LATE_LATENT, ) # Death flows # Universal death rate to be overriden by a multiply in age stratification. uni_death_flow_names = model.add_universal_death_flows("universal_death", death_rate=1) model.add_death_flow( "infectious_death", params["infect_death_rate"], Compartment.INFECTIOUS, ) model.add_death_flow( "detected_death", params["infect_death_rate"], Compartment.DETECTED, ) model.add_death_flow( "treatment_death", params["treatment_death_rate"], Compartment.ON_TREATMENT, ) # Apply age-stratification age_strat = _build_age_strat(params, uni_death_flow_names) model.stratify_with(age_strat) # Add vaccination stratification. vac_strat = _build_vac_strat(params) model.stratify_with(vac_strat) # Apply organ stratification organ_strat = _build_organ_strat(params) model.stratify_with(organ_strat) # Apply strain stratification strain_strat = _build_strain_strat(params) model.stratify_with(strain_strat) # Add amplification flow model.add_fractional_flow( name="amplification", fractional_rate=params["amplification_rate"], source=Compartment.ON_TREATMENT, dest=Compartment.ON_TREATMENT, source_strata={"strain": "ds"}, dest_strata={"strain": "mdr"}, expected_flow_count=9, ) # Add cross-strain reinfection flows model.add_infection_frequency_flow( name="reinfection_ds_to_mdr", contact_rate=params["reinfection_rate"], source=Compartment.EARLY_LATENT, dest=Compartment.EARLY_LATENT, source_strata={"strain": "ds"}, dest_strata={"strain": "mdr"}, expected_flow_count=3, ) model.add_infection_frequency_flow( name="reinfection_mdr_to_ds", contact_rate=params["reinfection_rate"], source=Compartment.EARLY_LATENT, dest=Compartment.EARLY_LATENT, source_strata={"strain": "mdr"}, dest_strata={"strain": "ds"}, expected_flow_count=3, ) model.add_infection_frequency_flow( name="reinfection_late_ds_to_mdr", contact_rate=params["reinfection_rate"], source=Compartment.LATE_LATENT, dest=Compartment.EARLY_LATENT, source_strata={"strain": "ds"}, dest_strata={"strain": "mdr"}, expected_flow_count=3, ) model.add_infection_frequency_flow( name="reinfection_late_mdr_to_ds", contact_rate=params["reinfection_rate"], source=Compartment.LATE_LATENT, dest=Compartment.EARLY_LATENT, source_strata={"strain": "mdr"}, dest_strata={"strain": "ds"}, expected_flow_count=3, ) # Apply classification stratification class_strat = _build_class_strat(params) model.stratify_with(class_strat) # Apply retention stratification retention_strat = _build_retention_strat(params) model.stratify_with(retention_strat) # Register derived output functions, which are calculations based on the model's compartment values or flows. # These are calculated after the model is run. model.request_output_for_flow("notifications", flow_name="detection") model.request_output_for_flow("early_activation", flow_name="early_activation") model.request_output_for_flow("late_activation", flow_name="late_activation") model.request_output_for_flow("infectious_deaths", flow_name="infectious_death") model.request_output_for_flow("detected_deaths", flow_name="detected_death") model.request_output_for_flow("treatment_deaths", flow_name="treatment_death") model.request_output_for_flow("progression_early", flow_name="early_activation") model.request_output_for_flow("progression_late", flow_name="late_activation") model.request_aggregate_output("progression", ["progression_early", "progression_late"]) model.request_output_for_compartments("population_size", COMPARTMENTS) model.request_aggregate_output( "_incidence", sources=["early_activation", "late_activation"], save_results=False) model.request_function_output("incidence", sources=["_incidence", "population_size"], func=lambda i, p: 1e5 * i / p) model.request_aggregate_output( "disease_deaths", sources=["infectious_deaths", "detected_deaths", "treatment_deaths"]) cum_start_time = params["cumulative_output_start_time"] model.request_cumulative_output("cumulative_diseased", source="_incidence", start_time=cum_start_time) model.request_cumulative_output("cumulative_deaths", source="disease_deaths", start_time=cum_start_time) model.request_output_for_compartments("_count_infectious", INFECTIOUS_COMPS, save_results=False) model.request_function_output( "prevalence_infectious", sources=["_count_infectious", "population_size"], func=lambda c, p: 1e5 * c / p, ) model.request_output_for_compartments( "_count_latent", [Compartment.EARLY_LATENT, Compartment.LATE_LATENT], save_results=False) model.request_function_output( "percentage_latent", sources=["_count_latent", "population_size"], func=lambda c, p: 100 * c / p, ) return model