Example #1
0
def build_model(params: dict) -> CompartmentalModel:
    """
    Build the master function to run a simple SIR model
    """
    # Define model compartments.
    compartments = ["S", "I", "R"]

    time = params["time"]
    model = CompartmentalModel(
        times=[time["start"], time["end"]],
        compartments=compartments,
        infectious_compartments=["I"],
        timestep=time["step"],
    )
    model.set_initial_population({
        "I": 1,
        "S": 999999,
    })
    # Add flows
    model.add_infection_frequency_flow("infection", params["contact_rate"],
                                       "S", "I")
    model.add_fractional_flow("recovery", params["recovery_rate"], "I", "R")

    # Request derived outputs
    model.request_output_for_compartments("prevalence_susceptible",
                                          compartments=["S"])
    model.request_output_for_compartments("prevalence_infectious",
                                          compartments=["I"])
    return model
Example #2
0
def test_stochastic_recovery_exitinction(recovery_rate, contact_rate):
    """
    A smokey test to make sure the disease goes extinct sometimes,
    because the infectious person recovers before they can infect someone else.

    Calculations similar to test_stochastic_death_exitinction
    """
    pr_recovery = 1 - np.exp(-recovery_rate)
    pr_infected = 1 - np.exp(-contact_rate / 1000)
    pr_noone_infected = binom.pmf(0, 1000, pr_infected)
    pr_extinction = pr_recovery * pr_noone_infected
    expected_extinctions = _find_num_successes(pr_extinction, TRIALS,
                                               ERROR_RATE)
    count_extinctions = 0
    for _ in range(TRIALS):
        model = CompartmentalModel(
            times=[0, 1],
            compartments=["S", "I", "R"],
            infectious_compartments=["I"],
        )
        model.set_initial_population(distribution={"S": 999, "I": 1})
        model.add_transition_flow("recovery", recovery_rate, "I", "R")
        model.add_infection_frequency_flow("infection", contact_rate, "S", "I")
        model.run_stochastic()
        is_extinct = model.outputs[1, 1] == 0
        if is_extinct:
            count_extinctions += 1

    assert count_extinctions >= expected_extinctions
Example #3
0
def test_stochastic_death_exitinction(death_rate, contact_rate):
    """
    A smokey test to make sure the disease goes extinct around the right amount,
    because the infectious person dies before they can infect someone else.

    See here for how this stuff is calculated
    https://autumn-files.s3-ap-southeast-2.amazonaws.com/Switching_to_stochastic_mode.pdf

    Consider the following flow rates:
    - 0.5 infected deaths timestep
    - 2 people infected per timestep
        - infection frequency force of infection of  1 inf / 1000 pop
        - sus pop of 999
        - contact rate of 2
        - flow rate of 2 * 999 / 1000 = 1.998 ~= 2

    Based on stochastic model (per person)
    - P(infect_death) ~=40%(1 - e^(-0.5/1))
    - P(infected) ~= 0.2% (1 - e^(-2/1000))

    Using a binomial calculator, we get
    - ~86% chance of 1 or more people getting infected
    - ~14% chance of noone getting infected

    Death and infection are independent processes within the model.
    So then we expect a ~6% chance of exctinction (infected person dies, no one infected) (40% * 14%)

    Given this there is a > 0.999999 chance that we see at least 25
    disease exctinctions in 1000 runs (using binomial calculation)
    """
    pr_death = 1 - np.exp(-death_rate)
    pr_infected = 1 - np.exp(-contact_rate / 1000)
    pr_noone_infected = binom.pmf(0, 1000, pr_infected)
    pr_extinction = pr_death * pr_noone_infected
    expected_extinctions = _find_num_successes(pr_extinction, TRIALS,
                                               ERROR_RATE)
    count_extinctions = 0
    for _ in range(TRIALS):
        model = CompartmentalModel(
            times=[0, 1],
            compartments=["S", "I", "R"],
            infectious_compartments=["I"],
        )
        model.set_initial_population(distribution={"S": 999, "I": 1})
        model.add_death_flow("infect_death", death_rate, "I")
        model.add_infection_frequency_flow("infection", contact_rate, "S", "I")
        model.run_stochastic()
        is_extinct = model.outputs[1, 1] == 0
        if is_extinct:
            count_extinctions += 1

    assert count_extinctions >= expected_extinctions
Example #4
0
def test_model__with_two_symmetric_stratifications():
    """
    Adding two strata with the same properties should yield the exact same infection dynamics and outputs as having no strata at all.
    This does not test strains directly, but if this doesn't work then further testing is pointless.
    """
    model = CompartmentalModel(times=[0, 5],
                               compartments=["S", "I", "R"],
                               infectious_compartments=["I"])
    model.set_initial_population(distribution={"S": 900, "I": 100})
    model.add_infection_frequency_flow("infection", 0.2, "S", "I")
    model.add_sojourn_flow("recovery", 10, "I", "R")

    # Do pre-run force of infection calcs.
    model._prepare_to_run()
    model._prepare_time_step(0, model.initial_population)

    # Check infectiousness multipliers
    susceptible = model.compartments[0]
    infectious = model.compartments[1]
    assert model._get_infection_density_multiplier(susceptible,
                                                   infectious) == 100.0
    assert model._get_infection_frequency_multiplier(susceptible,
                                                     infectious) == 0.1
    model.run()

    # Create a stratified model where the two non-strain strata are symmetric
    stratified_model = CompartmentalModel(times=[0, 5],
                                          compartments=["S", "I", "R"],
                                          infectious_compartments=["I"])
    stratified_model.set_initial_population(distribution={"S": 900, "I": 100})
    stratified_model.add_infection_frequency_flow("infection", 0.2, "S", "I")
    stratified_model.add_sojourn_flow("recovery", 10, "I", "R")
    strat = Stratification("clinical", ["home", "hospital"], ["I"])
    stratified_model.stratify_with(strat)
    stratified_model.run()

    # Ensure stratified model has the same results as the unstratified model.
    merged_outputs = np.zeros_like(model.outputs)
    merged_outputs[:, 0] = stratified_model.outputs[:, 0]
    merged_outputs[:,
                   1] = stratified_model.outputs[:,
                                                 1] + stratified_model.outputs[:,
                                                                               2]
    merged_outputs[:, 2] = stratified_model.outputs[:, 3]
    assert_allclose(merged_outputs,
                    model.outputs,
                    atol=0.01,
                    rtol=0.01,
                    verbose=True)
Example #5
0
def test_strains__with_two_symmetric_strains():
    """
    Adding two strains with the same properties should yield the same infection dynamics and outputs as having no strains at all.
    We expect the force of infection for each strain to be 1/2 of the unstratified model,
    but the stratification process will not apply the usual conservation fraction to the fan out flows.
    """
    # Create an unstratified model
    model = CompartmentalModel(times=[0, 5],
                               compartments=["S", "I", "R"],
                               infectious_compartments=["I"])
    model.set_initial_population(distribution={"S": 900, "I": 100})
    model.add_infection_frequency_flow("infection", 0.2, "S", "I")
    model.add_sojourn_flow("recovery", 10, "I", "R")

    # Do pre-run force of infection calcs.
    model._prepare_to_run()
    model._prepare_time_step(0, model.initial_population)
    # Check infectiousness multipliers
    susceptible = model.compartments[0]
    infectious = model.compartments[1]
    assert model._get_infection_density_multiplier(susceptible,
                                                   infectious) == 100.0
    assert model._get_infection_frequency_multiplier(susceptible,
                                                     infectious) == 0.1
    model.run()

    # Create a stratified model where the two strain strata are symmetric
    strain_model = CompartmentalModel(times=[0, 5],
                                      compartments=["S", "I", "R"],
                                      infectious_compartments=["I"])
    strain_model.set_initial_population(distribution={"S": 900, "I": 100})
    strain_model.add_infection_frequency_flow("infection", 0.2, "S", "I")
    strain_model.add_sojourn_flow("recovery", 10, "I", "R")
    strat = StrainStratification("strain", ["a", "b"], ["I"])
    strain_model.stratify_with(strat)
    strain_model.run()

    # Ensure stratified model has the same results as the unstratified model.
    merged_outputs = np.zeros_like(model.outputs)
    merged_outputs[:, 0] = strain_model.outputs[:, 0]
    merged_outputs[:,
                   1] = strain_model.outputs[:, 1] + strain_model.outputs[:, 2]
    merged_outputs[:, 2] = strain_model.outputs[:, 3]
    assert_allclose(merged_outputs,
                    model.outputs,
                    atol=0.01,
                    rtol=0.01,
                    verbose=True)
Example #6
0
def test_stratify_transition_flows__with_source_and_dest_stratified():
    """
    Ensure transition flows are stratified correctly when both the flow source and dest are stratified.
    """
    model = CompartmentalModel(
        times=[0, 5], compartments=["S", "I", "R"], infectious_compartments=["I"]
    )
    model.add_infection_frequency_flow("infection", 0.03, "S", "I")
    model.add_sojourn_flow("recovery", 7, "I", "R")

    expected_flows = [
        InfectionFrequencyFlow(
            "infection", C("S"), C("I"), 0.03, model._get_infection_frequency_multiplier
        ),
        SojournFlow("recovery", C("I"), C("R"), 7),
    ]
    assert len(expected_flows) == len(model._flows)
    assert all([a._is_equal(e) for e, a in zip(expected_flows, model._flows)])

    # Apply stratification
    strat = Stratification("location", ["urban", "rural"], ["S", "I", "R"])
    model.stratify_with(strat)

    expected_flows = [
        InfectionFrequencyFlow(
            "infection",
            C("S", {"location": "urban"}),
            C("I", {"location": "urban"}),
            0.03,
            model._get_infection_frequency_multiplier,
        ),
        InfectionFrequencyFlow(
            "infection",
            C("S", {"location": "rural"}),
            C("I", {"location": "rural"}),
            0.03,
            model._get_infection_frequency_multiplier,
        ),
        SojournFlow("recovery", C("I", {"location": "urban"}), C("R", {"location": "urban"}), 7),
        SojournFlow("recovery", C("I", {"location": "rural"}), C("R", {"location": "rural"}), 7),
    ]
    assert len(expected_flows) == len(model._flows)
    assert all([a._is_equal(e) for e, a in zip(expected_flows, model._flows)])
Example #7
0
def test_model__with_infection_frequency__expect_all_infected():
    """
    Ensure that a model with two compartments and one-way internal dynamics results in all infected.
    """
    # Set up a model with 100 people, all susceptible execept 1 infected.
    model = CompartmentalModel(
        times=[0, 5], compartments=["S", "I", "R"], infectious_compartments=["I"]
    )
    model.set_initial_population(distribution={"S": 99, "I": 1})
    model.add_infection_frequency_flow("infection", 3, "S", "I")
    model.run()
    # Expect that everyone gets infected eventually.
    expected_outputs = np.array(
        [
            [99.00, 1.00, 0],  # Initial conditions
            [83.13, 16.87, 0],
            [19.70, 80.30, 0],
            [1.21, 98.79, 0],
            [0.06, 99.94, 0],
            [0.00, 100.00, 0],
        ]
    )
    assert_allclose(model.outputs, expected_outputs, atol=0.1, verbose=True)
Example #8
0
def test_model__with_no_infected__expect_no_change():
    """
    Ensure that if no one has the disease, then no one gets the disease in the future.
    """
    # Set up a model with 100 people, all susceptible, transmission highly likely, but no one is infected.
    model = CompartmentalModel(
        times=[0, 5], compartments=["S", "I", "R"], infectious_compartments=["I"]
    )
    model.set_initial_population(distribution={"S": 100, "I": 0})
    model.add_infection_frequency_flow("infection", 10, "S", "I")
    model.run()
    # Expect that no one has moved from sucsceptible to infections at any point in time
    expected_outputs = np.array(
        [
            [100.0, 0.0, 0],  # Initial conditions
            [100.0, 0.0, 0],
            [100.0, 0.0, 0],
            [100.0, 0.0, 0],
            [100.0, 0.0, 0],
            [100.0, 0.0, 0],
        ]
    )
    assert_allclose(model.outputs, expected_outputs, atol=0.1, verbose=True)
Example #9
0
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)
Example #10
0
def _get_test_model(timestep=1, times=[0, 150]):
    comps = ["S", "EE", "LE", "EA", "LA", "R"]
    infectious_comps = ["LE", "EA", "LA"]
    model = CompartmentalModel(
        times=times,
        compartments=comps,
        infectious_compartments=infectious_comps,
        timestep=timestep,
    )
    model.set_initial_population({"S": int(20e6), "LA": 100})

    # Add flows
    model.add_infection_frequency_flow(name="infection", contact_rate=0.03, source="S", dest="EE")
    model.add_sojourn_flow(name="infect_onset", sojourn_time=7, source="EE", dest="LE")
    model.add_sojourn_flow(name="incidence", sojourn_time=7, source="LE", dest="EA")
    model.add_sojourn_flow(name="progress", sojourn_time=7, source="EA", dest="LA")
    model.add_sojourn_flow(name="recovery", sojourn_time=7, source="LA", dest="R")
    model.add_death_flow(name="infect_death", death_rate=0.005, source="LA")
    model.add_transition_flow(name="warning_immunity", fractional_rate=0.01, source="R", dest="S")

    # Stratify by age
    age_strat = Stratification("age", AGE_STRATA, comps)
    age_strat.set_population_split(AGE_SPLIT_PROPORTIONS)
    age_strat.set_mixing_matrix(AGE_MIXING_MATRIX)
    age_strat.add_flow_adjustments(
        "infection", {s: Multiply(v) for s, v in AGE_SUSCEPTIBILITY.items()}
    )
    model.stratify_with(age_strat)

    # Stratify by clinical status
    clinical_strat = Stratification("clinical", CLINICAL_STRATA, infectious_comps)
    clinical_strat.add_infectiousness_adjustments("LE", {**ADJ_BASE, "non_sympt": Overwrite(0.25)})
    clinical_strat.add_infectiousness_adjustments("EA", {**ADJ_BASE, "non_sympt": Overwrite(0.25)})
    clinical_strat.add_infectiousness_adjustments(
        "LA",
        {
            **ADJ_BASE,
            "non_sympt": Overwrite(0.25),
            "sympt_isolate": Overwrite(0.2),
            "hospital": Overwrite(0.2),
            "icu": Overwrite(0.2),
        },
    )
    clinical_strat.add_flow_adjustments(
        "infect_onset",
        {
            "non_sympt": Multiply(0.26),
            "icu": Multiply(0.01),
            "hospital": Multiply(0.04),
            "sympt_public": Multiply(0.66),
            "sympt_isolate": Multiply(0.03),
        },
    )
    model.stratify_with(clinical_strat)

    # Request derived outputs.
    model.request_output_for_flow(name="incidence", flow_name="incidence")
    model.request_output_for_flow(name="progress", flow_name="progress")
    for age in AGE_STRATA:
        for clinical in NOTIFICATION_STRATA:
            model.request_output_for_flow(
                name=f"progressXage_{age}Xclinical_{clinical}",
                flow_name="progress",
                dest_strata={"age": age, "clinical": clinical},
            )

    hospital_sources = []
    icu_sources = []
    for age in AGE_STRATA:
        icu_sources.append(f"progressXage_{age}Xclinical_icu")
        hospital_sources += [
            f"progressXage_{age}Xclinical_icu",
            f"progressXage_{age}Xclinical_hospital",
        ]

    model.request_aggregate_output(
        name="new_hospital_admissions",
        sources=hospital_sources,
    )
    model.request_aggregate_output(name="new_icu_admissions", sources=icu_sources)

    # Get notifications, which may included people detected in-country as they progress, or imported cases which are detected.
    notification_sources = [
        f"progressXage_{a}Xclinical_{c}" for a in AGE_STRATA for c in NOTIFICATION_STRATA
    ]
    model.request_aggregate_output(name="notifications", sources=notification_sources)

    # Infection deaths.
    model.request_output_for_flow(name="infection_deaths", flow_name="infect_death")
    model.request_cumulative_output(name="accum_deaths", source="infection_deaths")

    # Track hospital occupancy.
    # We count all ICU and hospital late active compartments and a proportion of early active ICU cases.
    model.request_output_for_compartments(
        "_late_active_hospital",
        compartments=["LA"],
        strata={"clinical": "hospital"},
        save_results=False,
    )
    model.request_output_for_compartments(
        "icu_occupancy",
        compartments=["LA"],
        strata={"clinical": "icu"},
    )
    model.request_output_for_compartments(
        "_early_active_icu",
        compartments=["EA"],
        strata={"clinical": "icu"},
        save_results=False,
    )
    proportion_icu_patients_in_hospital = 0.25
    model.request_function_output(
        name="_early_active_icu_proportion",
        func=lambda patients: patients * proportion_icu_patients_in_hospital,
        sources=["_early_active_icu"],
        save_results=False,
    )
    model.request_aggregate_output(
        name="hospital_occupancy",
        sources=[
            "_late_active_hospital",
            "icu_occupancy",
            "_early_active_icu_proportion",
        ],
    )

    # Proportion seropositive
    model.request_output_for_compartments(
        name="_total_population", compartments=comps, save_results=False
    )
    model.request_output_for_compartments(name="_recovered", compartments=["R"], save_results=False)
    model.request_function_output(
        name="proportion_seropositive",
        sources=["_recovered", "_total_population"],
        func=lambda recovered, total: recovered / total,
    )

    return model
Example #11
0
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
def test_solve_stochastic(monkeypatch):
    """
    Test that _solve_stochastic glue code works.
    Don't test the actual flow rate calculations or stochastic sampling bits.
    """
    model = CompartmentalModel(
        times=[0, 5],
        compartments=["S", "I", "R"],
        infectious_compartments=["I"],
    )
    # Add some people to the model, expect initial conditions of [990, 10, 0]
    model.set_initial_population(distribution={"S": 990, "I": 10})
    # Add flows - the parameters add here will be overidden by  `mock_get_rates`
    # but the flow directions will be used.
    model.add_crude_birth_flow("birth", 8, "S")
    model.add_infection_frequency_flow("infection", 6, "S", "I")
    model.add_death_flow("infect_death", 3, "I")
    model.add_transition_flow("recovery", 2, "I", "R")

    # Mock out flow rate calculation - tested elsewhere and tricky to predict.
    def mock_get_rates(comp_vals, time):
        # Return the flow rates that will be used to solve the model
        return None, np.array([float(f.param) for f in model._flows])

    monkeypatch.setattr(model, "_get_rates", mock_get_rates)

    # Mock out stochastic flow sampling - tested elsewhere.
    def mock_sample_entry_flows(seed, entry_flow_rates, timestep):
        assert not seed
        assert 0 < timestep <= 5
        expected_flow_rates = np.array([8, 0, 0])
        assert_array_equal(entry_flow_rates, expected_flow_rates)
        return np.array([8, 0, 0])

    def mock_sample_transistion_flows(seed, flow_rates, flow_map, comp_vals, timestep):
        assert not seed
        assert 0 < timestep <= 5
        # Flows get re-arranged by setup process
        expected_flow_map = np.array([[0, 1, -1], [2, 0, 1], [3, 1, 2]])
        assert_array_equal(flow_map, expected_flow_map)
        expected_flow_rates = np.array(
            [[0.0, 3.0, 0.0], [0.0, 0.0, 0.0], [6.0, 0.0, 0.0], [0.0, 2.0, 0.0]]
        )
        assert_array_equal(flow_rates, expected_flow_rates)

        return np.array([-6, 1, 2])

    monkeypatch.setattr(stochastic, "sample_entry_flows", mock_sample_entry_flows)
    monkeypatch.setattr(stochastic, "sample_transistion_flows", mock_sample_transistion_flows)

    model.run_stochastic()
    expected_outputs = np.array(
        [
            [990, 10, 0],
            [992, 11, 2],
            [994, 12, 4],
            [996, 13, 6],
            [998, 14, 8],
            [1000, 15, 10],
        ]
    )
    assert_array_equal(model.outputs, expected_outputs)
Example #13
0
def test_strain__with_infectious_multipliers_and_heterogeneous_mixing():
    """
    Test infectious multiplier and flow rate calculations for
    3 strains which have different infectiousness levels plus a seperate
    stratification which has a mixing matrix.
    """
    model = CompartmentalModel(times=[0, 5],
                               compartments=["S", "I", "R"],
                               infectious_compartments=["I"])
    model.set_initial_population(distribution={"S": 900, "I": 100})
    contact_rate = 0.2
    model.add_infection_frequency_flow("infection", contact_rate, "S", "I")

    age_strat = Stratification("age", ["child", "adult"], ["S", "I", "R"])
    age_strat.set_population_split({
        "child": 0.6,  # 600 people
        "adult": 0.4,  # 400 people
    })
    # Higher mixing among adults or children,
    # than between adults or children.
    age_strat.set_mixing_matrix(np.array([[1.5, 0.5], [0.5, 1.5]]))
    model.stratify_with(age_strat)

    strain_strat = StrainStratification("strain", ["a", "b", "c"], ["I"])
    strain_strat.set_population_split({
        "a": 0.7,  # 70 people
        "b": 0.2,  # 20 people
        "c": 0.1,  # 10 people
    })
    strain_strat.add_infectiousness_adjustments(
        "I",
        {
            "a": adjust.Multiply(0.5),  # 0.5x as susceptible
            "b": adjust.Multiply(3),  # 3x as susceptible
            "c": adjust.Multiply(2),  # 2x as susceptible
        },
    )
    model.stratify_with(strain_strat)

    # Do pre-run force of infection calcs.
    model._prepare_to_run()
    assert_array_equal(model._compartment_infectiousness["a"],
                       np.array([0, 0, 0.5, 0, 0, 0.5, 0, 0, 0, 0]))
    assert_array_equal(model._compartment_infectiousness["b"],
                       np.array([0, 0, 0, 3, 0, 0, 3, 0, 0, 0]))
    assert_array_equal(model._compartment_infectiousness["c"],
                       np.array([0, 0, 0, 0, 2, 0, 0, 2, 0, 0]))
    # 0 for child, 1 for adult
    assert model._category_lookup == {
        0: 0,
        1: 1,
        2: 0,
        3: 0,
        4: 0,
        5: 1,
        6: 1,
        7: 1,
        8: 0,
        9: 1
    }
    assert_array_equal(
        model._category_matrix,
        np.array([
            [1, 0, 1, 1, 1, 0, 0, 0, 1, 0],
            [0, 1, 0, 0, 0, 1, 1, 1, 0, 1],
        ]),
    )

    # Do pre-iteration force of infection calcs
    model._prepare_time_step(0, model.initial_population)
    assert_array_equal(model._category_populations, np.array([600, 400]))
    assert_array_equal(
        model._infection_density["a"],
        np.array([0.5 * (42 * 1.5 + 28 * 0.5), 0.5 * (42 * 0.5 + 28 * 1.5)]),
    )
    assert_array_equal(
        model._infection_density["b"],
        np.array([
            3 * (12 * 1.5 + 8 * 0.5),
            3 * (8 * 1.5 + 12 * 0.5),
        ]),
    )
    assert_array_equal(
        model._infection_density["c"],
        np.array([2 * (6 * 1.5 + 4 * 0.5), 2 * (4 * 1.5 + 6 * 0.5)]),
    )
    assert_array_equal(
        model._infection_frequency["a"],
        np.array([
            0.5 * ((42 / 600) * 1.5 + (28 / 400) * 0.5),
            0.5 * ((42 / 600) * 0.5 + (28 / 400) * 1.5),
        ]),
    )
    assert_array_equal(
        model._infection_frequency["b"],
        np.array([
            3 * ((12 / 600) * 1.5 + (8 / 400) * 0.5),
            3 * ((8 / 400) * 1.5 + (12 / 600) * 0.5),
        ]),
    )
    assert_array_equal(
        model._infection_frequency["c"],
        np.array([
            2 * ((6 / 600) * 1.5 + (4 / 400) * 0.5),
            2 * ((4 / 400) * 1.5 + (6 / 600) * 0.5)
        ]),
    )

    # Get multipliers
    sus_child = model.compartments[0]
    sus_adult = model.compartments[1]
    inf_child_a = model.compartments[2]
    inf_child_b = model.compartments[3]
    inf_child_c = model.compartments[4]
    inf_adult_a = model.compartments[5]
    inf_adult_b = model.compartments[6]
    inf_adult_c = model.compartments[7]
    density = model._get_infection_density_multiplier
    freq = model._get_infection_frequency_multiplier
    assert density(sus_child, inf_child_a) == 0.5 * (42 * 1.5 + 28 * 0.5)
    assert density(sus_adult, inf_adult_a) == 0.5 * (42 * 0.5 + 28 * 1.5)
    assert density(sus_child, inf_child_b) == 3 * (12 * 1.5 + 8 * 0.5)
    assert density(sus_adult, inf_adult_b) == 3 * (8 * 1.5 + 12 * 0.5)
    assert density(sus_child, inf_child_c) == 2 * (6 * 1.5 + 4 * 0.5)
    assert density(sus_adult, inf_adult_c) == 2 * (4 * 1.5 + 6 * 0.5)
    assert freq(sus_child,
                inf_child_a) == 0.5 * ((42 / 600) * 1.5 + (28 / 400) * 0.5)
    assert freq(sus_adult,
                inf_adult_a) == 0.5 * ((42 / 600) * 0.5 + (28 / 400) * 1.5)
    assert freq(sus_child,
                inf_child_b) == 3 * ((12 / 600) * 1.5 + (8 / 400) * 0.5)
    assert freq(sus_adult,
                inf_adult_b) == 3 * ((8 / 400) * 1.5 + (12 / 600) * 0.5)
    assert freq(sus_child,
                inf_child_c) == 2 * ((6 / 600) * 1.5 + (4 / 400) * 0.5)
    assert freq(sus_adult,
                inf_adult_c) == 2 * ((4 / 400) * 1.5 + (6 / 600) * 0.5)

    # Get infection flow rates
    flow_to_inf_child_a = 540 * contact_rate * freq(sus_child, inf_child_a)
    flow_to_inf_adult_a = 360 * contact_rate * freq(sus_adult, inf_adult_a)
    flow_to_inf_child_b = 540 * contact_rate * freq(sus_child, inf_child_b)
    flow_to_inf_adult_b = 360 * contact_rate * freq(sus_adult, inf_adult_b)
    flow_to_inf_child_c = 540 * contact_rate * freq(sus_child, inf_child_c)
    flow_to_inf_adult_c = 360 * contact_rate * freq(sus_adult, inf_adult_c)
    expected_flow_rates = np.array([
        -flow_to_inf_child_a - flow_to_inf_child_b - flow_to_inf_child_c,
        -flow_to_inf_adult_a - flow_to_inf_adult_b - flow_to_inf_adult_c,
        flow_to_inf_child_a,
        flow_to_inf_child_b,
        flow_to_inf_child_c,
        flow_to_inf_adult_a,
        flow_to_inf_adult_b,
        flow_to_inf_adult_c,
        0.0,
        0.0,
    ])
    flow_rates = model._get_compartment_rates(model.initial_population, 0)
    assert_allclose(expected_flow_rates, flow_rates, verbose=True)
Example #14
0
def test_strain__with_flow_adjustments():
    """
    Test infectious multiplier and flow rate calculations for
    3 strains which have different flow adjustments.

    These flow adjustments would correspond to some physical process that we're modelling,
    and they should be effectively the same as applying infectiousness multipliers.
    """
    model = CompartmentalModel(times=[0, 5],
                               compartments=["S", "I", "R"],
                               infectious_compartments=["I"])
    model.set_initial_population(distribution={"S": 900, "I": 100})
    contact_rate = 0.2
    model.add_infection_frequency_flow("infection", contact_rate, "S", "I")
    strat = StrainStratification("strain", ["a", "b", "c"], ["I"])
    strat.set_population_split({
        "a": 0.7,  # 70 people
        "b": 0.2,  # 20 people
        "c": 0.1,  # 10 people
    })
    strat.add_flow_adjustments(
        "infection",
        {
            "a": adjust.Multiply(0.5),  # 0.5x as susceptible
            "b": adjust.Multiply(3),  # 3x as susceptible
            "c": adjust.Multiply(2),  # 2x as susceptible
        },
    )
    model.stratify_with(strat)

    # Do pre-run force of infection calcs.
    model._prepare_to_run()
    assert_array_equal(model._compartment_infectiousness["a"],
                       np.array([0, 1, 0, 0, 0]))
    assert_array_equal(model._compartment_infectiousness["b"],
                       np.array([0, 0, 1, 0, 0]))
    assert_array_equal(model._compartment_infectiousness["c"],
                       np.array([0, 0, 0, 1, 0]))
    assert model._category_lookup == {0: 0, 1: 0, 2: 0, 3: 0, 4: 0}
    assert_array_equal(model._category_matrix, np.array([[1, 1, 1, 1, 1]]))

    # Do pre-iteration force of infection calcs
    model._prepare_time_step(0, model.initial_population)
    assert_array_equal(model._category_populations, np.array([1000]))
    assert_array_equal(model._infection_density["a"], np.array([70]))
    assert_array_equal(model._infection_density["b"], np.array([20]))
    assert_array_equal(model._infection_density["c"], np.array([10]))
    assert_array_equal(model._infection_frequency["a"], np.array([70 / 1000]))
    assert_array_equal(model._infection_frequency["b"], np.array([20 / 1000]))
    assert_array_equal(model._infection_frequency["c"], np.array([10 / 1000]))

    # Get multipliers
    susceptible = model.compartments[0]
    infectious_a = model.compartments[1]
    infectious_b = model.compartments[2]
    infectious_c = model.compartments[3]
    assert model._get_infection_density_multiplier(susceptible,
                                                   infectious_a) == 70
    assert model._get_infection_density_multiplier(susceptible,
                                                   infectious_b) == 20
    assert model._get_infection_density_multiplier(susceptible,
                                                   infectious_c) == 10
    assert model._get_infection_frequency_multiplier(susceptible,
                                                     infectious_a) == 70 / 1000
    assert model._get_infection_frequency_multiplier(susceptible,
                                                     infectious_b) == 20 / 1000
    assert model._get_infection_frequency_multiplier(susceptible,
                                                     infectious_c) == 10 / 1000

    # Get infection flow rates
    flow_rates = model._get_compartment_rates(model.initial_population, 0)
    sus_pop = 900
    flow_to_a = sus_pop * contact_rate * (70 * 0.5 / 1000)
    flow_to_b = sus_pop * contact_rate * (20 * 3 / 1000)
    flow_to_c = sus_pop * contact_rate * (10 * 2 / 1000)
    expected_flow_rates = np.array([
        -flow_to_a - flow_to_b - flow_to_c, flow_to_a, flow_to_b, flow_to_c,
        0.0
    ])
    assert_allclose(expected_flow_rates, flow_rates, verbose=True)
Example #15
0
def build_model(params: dict) -> CompartmentalModel:
    """
    Build the compartmental model from the provided parameters.
    """
    params = Parameters(**params)
    model = CompartmentalModel(
        times=[params.time.start, params.time.end],
        compartments=COMPARTMENTS,
        infectious_compartments=INFECTIOUS_COMPARTMENTS,
        timestep=params.time.step,
    )

    # Population distribution
    country = params.country
    pop = params.population
    # Time periods calculated from periods (ie "sojourn times")
    compartment_periods = preprocess.compartments.calc_compartment_periods(
        params.sojourn)
    # Get country population by age-group
    total_pops = inputs.get_population_by_agegroup(AGEGROUP_STRATA,
                                                   country.iso3,
                                                   pop.region,
                                                   year=pop.year)
    # Distribute infectious seed across infectious split sub-compartments
    total_disease_time = sum(
        [compartment_periods[c] for c in DISEASE_COMPARTMENTS])
    init_pop = {
        c: params.infectious_seed * compartment_periods[c] / total_disease_time
        for c in DISEASE_COMPARTMENTS
    }
    # Assign the remainder starting population to the S compartment
    init_pop[Compartment.SUSCEPTIBLE] = sum(total_pops) - sum(
        init_pop.values())
    model.set_initial_population(init_pop)

    # Add intercompartmental flows
    if params.seasonal_force:
        # Use a time-varying, sinusoidal seasonal forcing function for contact rate.
        contact_rate = get_seasonal_forcing(365.0, 173.0,
                                            params.seasonal_force,
                                            params.contact_rate)
    else:
        # Use a static contact rate.
        contact_rate = params.contact_rate

    # Adjust contact rate for Variant of Concerns
    if params.voc_emmergence:
        voc_multiplier = scale_up_function(
            x=[
                params.voc_emmergence.start_time,
                params.voc_emmergence.end_time
            ],
            y=[
                1.0,
                1.0 + params.voc_emmergence.final_proportion *
                (params.voc_emmergence.contact_rate_multiplier - 1.0),
            ],
            method=4,
        )
        raw_contact_rate = contact_rate
        if isinstance(contact_rate, float):

            def contact_rate(t):
                return raw_contact_rate * voc_multiplier(t)

        else:

            def contact_rate(t):
                return raw_contact_rate(t) * voc_multiplier(t)

    model.add_infection_frequency_flow(
        name="infection",
        contact_rate=contact_rate,
        source=Compartment.SUSCEPTIBLE,
        dest=Compartment.EARLY_EXPOSED,
    )
    # Infection progress flows.
    model.add_fractional_flow(
        name="infect_onset",
        fractional_rate=1.0 / compartment_periods[Compartment.EARLY_EXPOSED],
        source=Compartment.EARLY_EXPOSED,
        dest=Compartment.LATE_EXPOSED,
    )
    model.add_fractional_flow(
        name="incidence",
        fractional_rate=1.0 / compartment_periods[Compartment.LATE_EXPOSED],
        source=Compartment.LATE_EXPOSED,
        dest=Compartment.EARLY_ACTIVE,
    )
    model.add_fractional_flow(
        name="progress",
        fractional_rate=1.0 / compartment_periods[Compartment.EARLY_ACTIVE],
        source=Compartment.EARLY_ACTIVE,
        dest=Compartment.LATE_ACTIVE,
    )
    # Recovery flows
    model.add_fractional_flow(
        name="recovery",
        fractional_rate=1.0 / compartment_periods[Compartment.LATE_ACTIVE],
        source=Compartment.LATE_ACTIVE,
        dest=Compartment.RECOVERED,
    )
    # Infection death
    model.add_death_flow(
        name="infect_death",
        death_rate=0,  # Will be overwritten later in clinical stratification.
        source=Compartment.LATE_ACTIVE,
    )

    if params.waning_immunity_duration is not None:
        # Waning immunity (if requested)
        model.add_fractional_flow(
            name="warning_immunity",
            fractional_rate=1.0 / params.waning_immunity_duration,
            source=Compartment.RECOVERED,
            dest=Compartment.SUSCEPTIBLE,
        )

    # Stratify the model by age group.
    age_strat = get_agegroup_strat(params, total_pops)
    model.stratify_with(age_strat)

    # Stratify the model by clinical status
    clinical_strat = get_clinical_strat(params)
    model.stratify_with(clinical_strat)

    # Stratify by immunity - which will include vaccination and infection history
    if params.stratify_by_immunity:
        immunity_strat = get_immunity_strat(params)
        model.stratify_with(immunity_strat)
        if params.vaccination:
            vacc_params = params.vaccination
            for roll_out_component in vacc_params.roll_out_components:
                add_vaccination_flows(model, roll_out_component,
                                      age_strat.strata)

    # Infection history stratification
    if params.stratify_by_infection_history:
        history_strat = get_history_strat(params, compartment_periods)
        model.stratify_with(history_strat)

    # Stratify model by Victorian subregion (used for Victorian cluster model).
    if params.victorian_clusters:
        cluster_strat = get_cluster_strat(params)
        model.stratify_with(cluster_strat)
        apply_post_cluster_strat_hacks(params, model)

    # Set up derived output functions
    if not params.victorian_clusters:
        request_standard_outputs(model, params)
    else:
        request_victorian_outputs(model, params)

    return model