def test_compute_tauT(self):
        # No contact tracing without app
        tauAs_peak = 5 * UNITS_IN_ONE_DAY
        tauAs_t_gs = (
            DiscreteDistributionOnNonNegatives(pmf_values=[0], tau_min=0, improper=True),
            DiscreteDistributionOnNonNegatives(pmf_values=[1], tau_min=tauAs_peak),
        )

        # Uniform distribution from 0 to 6 (excluded),  with total mass 0.6
        tau_Ac_t = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.1 / UNITS_IN_ONE_DAY] * 6 * UNITS_IN_ONE_DAY, tau_min=0, improper=True,
        )
        assert floats_match(tau_Ac_t.total_mass, 0.6)

        DeltaAT_peak = 2 * UNITS_IN_ONE_DAY
        DeltaAT = DiscreteDistributionOnNonNegatives(pmf_values=[1], tau_min=DeltaAT_peak)

        tauT_t_gs = compute_tauT_t(tauAs_t_gs=tauAs_t_gs, tauAc_t=tau_Ac_t, DeltaAT=DeltaAT)

        # Expected results
        tau_A_gs = (
            tau_Ac_t,
            DiscreteDistributionOnNonNegatives(
                pmf_values=[0.1 / UNITS_IN_ONE_DAY] * 5 * UNITS_IN_ONE_DAY + [0.5],
                tau_min=0,
                improper=True,
            ),
        )

        assert all(tauT_t_gs[g] == tau_A_gs[g] + DeltaAT_peak for g in (0, 1))
    def test_b_suppression(self):
        _, b0_gs = make_scenario_parameters_for_asymptomatic_symptomatic_model()
        R0_gs = tuple(b0_gs[g].total_mass for g in [0, 1])
        xi = 0.8  # Any value in [0,1] will do

        tauT_peak = 10 * UNITS_IN_ONE_DAY

        tauT_gs = (
            DiscreteDistributionOnNonNegatives(pmf_values=[0], tau_min=0, improper=True),
            DiscreteDistributionOnNonNegatives(pmf_values=[1], tau_min=tauT_peak),
        )

        b_t_gs = compute_suppressed_b_t(b0_t_gs=b0_gs, tauT_t_gs=tauT_gs, xi_t=xi)

        R_t_gs = tuple(b_t_gs[g].total_mass for g in [0, 1])

        assert b_t_gs[0] == b0_gs[0]  # No suppression for asymptomatics
        assert R_t_gs[0] == R0_gs[0]

        assert all(
            b_t_gs[1].pmf(tau * UNITS_IN_ONE_DAY) == b0_gs[1].pmf(tau * UNITS_IN_ONE_DAY)
            for tau in (0, 3, 6, 9)
        )
        assert all(
            b_t_gs[1].pmf(tau * UNITS_IN_ONE_DAY) == (1 - xi) * b0_gs[1].pmf(tau * UNITS_IN_ONE_DAY)
            for tau in (10, 13, 16, 19)
        )
        assert (1 - xi) * R0_gs[1] <= R_t_gs[1] <= R0_gs[1]
Exemplo n.º 3
0
    def test_check_negative_times(self):
        p_gs = (0.4, 0.6)
        rho_0 = DiscreteDistributionOnNonNegatives(pmf_values=[0.3, 0.4, 0.3],
                                                   tau_min=1,
                                                   improper=True)

        # Case 1

        c_0 = 0.5
        c_1 = (-c_0 * p_gs[0] + 1) / p_gs[1]

        check_b_negative_times(
            p_gs=p_gs,
            b_negative_times=(
                rho_0.rescale_by_factor(scale_factor=c_0),
                rho_0.rescale_by_factor(scale_factor=c_1),
            ),
        )

        # Case 2

        c_0 = 0.8
        c_1 = (-c_0 * p_gs[0] + 1) / p_gs[1]

        check_b_negative_times(
            p_gs=p_gs,
            b_negative_times=(
                rho_0.rescale_by_factor(scale_factor=c_0),
                rho_0.rescale_by_factor(scale_factor=c_1),
            ),
        )

        # Case 3

        c_0 = 0.8
        c_1 = 0.6

        try:
            check_b_negative_times(
                p_gs=p_gs,
                b_negative_times=(
                    rho_0.rescale_by_factor(scale_factor=c_0),
                    rho_0.rescale_by_factor(scale_factor=c_1),
                ),
            )
            raise AssertionError
        except AssertionError:
            pass
def R_suppression_with_fixed_testing_time():
    """
    Example computing the suppressed infectiousness and R, assuming that all individuals are tested
    at a given infectious age tau_s.
    """
    tau_s_in_days = 7

    tauT = DiscreteDistributionOnNonNegatives(
        pmf_values=[1],
        tau_min=tau_s_in_days * UNITS_IN_ONE_DAY,
        improper=True,
    )

    xi = 1.0  # Probability of (immediate) isolation given positive test

    suppressed_b0 = compute_suppressed_b_t(
        b0_t_gs=(b0, ),
        tauT_t_gs=(tauT, ),
        xi_t=xi,
    )[0]
    suppressed_R_0 = suppressed_b0.total_mass

    print("suppressed R_0 =", suppressed_R_0)
    plot_discrete_distributions(ds=[b0, suppressed_b0],
                                custom_labels=["β^0", "β"])
Exemplo n.º 5
0
def make_scenario_parameters_for_asymptomatic_symptomatic_model(
    rho0_discrete: DiscreteDistributionOnNonNegatives = rho0_discrete,
    R0: float = R0,
    p_sym: float = p_sym,
    contribution_of_symptomatics_to_R0: float = contribution_of_symptomatics_to_R0,
) -> Tuple[Tuple[float, ...], Tuple[DiscreteDistributionOnNonNegatives, ...]]:
    """
    Returns the couples p_gs and b0_gs for the "two-components model" for the severity,
    namely for asymptomatic and symptomatic individuals.
    :param rho0: the generation time distribution.
    :param p_sym: the fraction of infected individuals that are symptomatic
    :param contribution_of_symptomatics_to_R0: the fraction of R0 due to symptomatic individuals.
    """
    p_asy = 1 - p_sym  # Fraction of infected individuals who are asymptomatic.

    R0_asy = (  # Component of R0 due to asymptomatic individuals
        (1 - contribution_of_symptomatics_to_R0) / p_asy * R0 if p_asy > 0 else 0
    )
    R0_sym = (  # Component of R0 due to symptomatic individuals
        contribution_of_symptomatics_to_R0 / p_sym * R0 if p_sym > 0 else 0
    )

    assert abs(R0 - p_sym * R0_sym - p_asy * R0_asy) < DISTRIBUTION_NORMALIZATION_TOLERANCE

    p_gs = (1 - p_sym, p_sym)

    b0_gs = (
        rho0_discrete.rescale_by_factor(R0_asy),
        rho0_discrete.rescale_by_factor(R0_sym),
    )

    return p_gs, b0_gs
    def test_linear_combination(self):
        d1 = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.1, 0.3, 0.4, 0.2, 0.5, 0.2, 0.3], tau_min=0, improper=True
        )
        d2 = DiscreteDistributionOnNonNegatives(pmf_values=[0.2, 0.5, 0.3], tau_min=3)

        d3 = linear_combination_discrete_distributions_by_values(
            scalars=[2, 3], seq=[d1, d2], use_cdfs=False, improper=True,
        )
        assert d3 == DiscreteDistributionOnNonNegatives(
            pmf_values=[0.2, 0.6, 0.8, 1, 2.5, 1.3, 0.6], improper=True
        )
        assert d3 == linear_combination_discrete_distributions_by_values(
            scalars=[2, 3], seq=[d1, d2], use_cdfs=True, improper=True,
        )

        linear_combination_discrete_distributions_by_values(
            scalars=[0.4, 0.6], seq=[d1.normalize(), d2], use_cdfs=False, improper=False,
        )
Exemplo n.º 7
0
    def test_tausigma_nu_computation_many_severities(self):
        p_gs = (0.4, 0.6)
        rho_0 = DiscreteDistributionOnNonNegatives(pmf_values=[0.3, 0.4, 0.3],
                                                   tau_min=1,
                                                   improper=True)
        c_0 = 0.5
        c_1 = (-c_0 * p_gs[0] + 1) / p_gs[1]
        assert floats_match(c_1, 4 / 3)
        b_negative_times = (
            rho_0.rescale_by_factor(scale_factor=c_0),
            rho_0.rescale_by_factor(scale_factor=c_1),
        )
        check_b_negative_times(
            p_gs=p_gs,
            b_negative_times=b_negative_times,
        )

        R_ts = [2]
        b = [(
            rho_0.rescale_by_factor(scale_factor=c_0 * R_t),
            rho_0.rescale_by_factor(scale_factor=c_1 * R_t),
        ) for R_t in R_ts]
        nu_0 = 10

        nugs_1, tausigmags_1 = compute_tausigma_and_nu_components_at_time_t(
            t=1,
            b=b,
            nu=[nu_0],
            p_gs=p_gs,
            b_negative_times=b_negative_times,
            nu_negative_times=nu_0,
        )

        expected_nu0_1_addends = [
            nu_0 * p_gs[0] * c_0 * R_ts[0] *
            0.3,  # Infections from infected at t=0
            nu_0 * p_gs[0] * c_0 * 0.4,  # Infections from infected at t=-1
            nu_0 * p_gs[0] * c_0 * 0.3,  # Infections from infected at t=-2
        ]

        expected_nu0_1 = sum(expected_nu0_1_addends)

        expected_nu1_1_addends = [
            nu_0 * p_gs[1] * c_1 * R_ts[0] *
            0.3,  # Infections from infected at t=0
            nu_0 * p_gs[1] * c_1 * 0.4,  # Infections from infected at t=-1
            nu_0 * p_gs[1] * c_1 * 0.3,  # Infections from infected at t=-2
        ]
        expected_nu1_1 = sum(expected_nu1_1_addends)

        assert float_sequences_match(nugs_1, (expected_nu0_1, expected_nu1_1))
        assert tausigmags_1[0] == DiscreteDistributionOnNonNegatives(
            pmf_values=expected_nu0_1_addends, tau_min=1,
            improper=True).rescale_by_factor(scale_factor=1 /
                                             (nugs_1[0] + nugs_1[1]))
        assert tausigmags_1[1] == DiscreteDistributionOnNonNegatives(
            pmf_values=expected_nu1_1_addends, tau_min=1,
            improper=True).rescale_by_factor(scale_factor=1 /
                                             (nugs_1[0] + nugs_1[1]))
def free_evolution_by_severity(
    b: Sequence[Tuple[DiscreteDistributionOnNonNegatives, ...]],
    nu_start: int,
    p_gs: Tuple[float, ...],
    b_negative_times: Optional[Tuple[DiscreteDistributionOnNonNegatives,
                                     ...]] = None,
) -> Tuple[List[int], List[Tuple[float, ...]], List[Tuple[
        DiscreteDistributionOnNonNegatives, ...]], ]:
    nu = []
    R = []
    tausigma = []

    gs = range(len(p_gs))  # Values of severity G

    for t in range(0, len(b)):
        t_in_days = t * TAU_UNIT_IN_DAYS
        b_t_gs = b[t]
        R_t_gs = tuple(b_t_g.total_mass for b_t_g in b_t_gs)
        R.append(R_t_gs)

        if t == 0 and b_negative_times is None:
            nu_t = nu_start
            tausigmags_t = tuple(
                DiscreteDistributionOnNonNegatives(
                    pmf_values=[], tau_min=0, improper=True) for _ in gs)
            nu_t_gs = None
        else:
            nu_t_gs, tausigmags_t = compute_tausigma_and_nu_components_at_time_t(
                t=t,
                b=b,
                nu=nu,
                p_gs=p_gs,
                b_negative_times=b_negative_times,
                nu_negative_times=nu_start,
            )
            nu_t = sum(nu_t_gs)

        nu.append(nu_t)
        tausigma.append(tausigmags_t)

        R_t = sum(p_g * R_t_g for (p_g, R_t_g) in zip(p_gs, R_t_gs))

        if t % UNITS_IN_ONE_DAY == 0:
            print(f"""t = {t_in_days} days
            nu_t_gs = {tuple(nu_t_gs)},   nu_t = {nu[t]}
            R_t_gs = {R_t_gs},    R_t = {round(R_t, 2)}
            Fsigmags_t(∞) = {tuple(tausigmag_t.total_mass for tausigmag_t in tausigmags_t)}
            """)

    return nu, R, tausigma
Exemplo n.º 9
0
    def test_tausigma_nu_computation_one_severity_with_negative_times(self):
        b = [
            DiscreteDistributionOnNonNegatives(pmf_values=[1.5, 2, 1],
                                               tau_min=1,
                                               improper=True),
            DiscreteDistributionOnNonNegatives(pmf_values=[1.5, 2.5, 1],
                                               tau_min=1,
                                               improper=True),
            DiscreteDistributionOnNonNegatives(pmf_values=[1, 1.4, 0.9],
                                               tau_min=1,
                                               improper=True),
        ]

        b_negative_times = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.3, 0.4, 0.3], tau_min=1, improper=True)

        nu_0 = 10

        nu_1, tausigma_1 = compute_tausigma_and_nu_at_time_t(
            t=1,
            b=b,
            nu=[nu_0],
            b_negative_times=b_negative_times,
            nu_negative_times=nu_0,
        )
        assert nu_1 == (0.3 + 0.4 + 1.5) * 10 == 22
        assert (tausigma_1 == DiscreteDistributionOnNonNegatives(
            pmf_values=[15, 4, 3], tau_min=1, improper=True).normalize())

        nu_2, tausigma_2 = compute_tausigma_and_nu_at_time_t(
            t=2,
            b=b,
            nu=[nu_0, nu_1],
            b_negative_times=b_negative_times,
            nu_negative_times=nu_0,
        )
        assert nu_2 == 10 * 0.3 + 10 * 2 + 22 * 1.5 == 56
        assert (tausigma_2 == DiscreteDistributionOnNonNegatives(
            pmf_values=[22 * 1.5, 10 * 2,
                        10 * 0.3], tau_min=1, improper=True).normalize())

        nu_3, tausigma_3 = compute_tausigma_and_nu_at_time_t(
            t=3,
            b=b,
            nu=[nu_0, nu_1, nu_2],
            b_negative_times=b_negative_times,
            nu_negative_times=nu_0,
        )
        assert nu_3 == 10 * 1 + 22 * 2.5 + 56 * 1 == 121
        assert (tausigma_3 == DiscreteDistributionOnNonNegatives(
            pmf_values=[56 * 1, 22 * 2.5,
                        10 * 1], tau_min=1, improper=True).normalize())
Exemplo n.º 10
0
def compute_tauAc_t(
    t: int,
    tauT: List[Tuple[DiscreteDistributionOnNonNegatives, ...]],
    tausigmags_t: Tuple[DiscreteDistributionOnNonNegatives, ...],
    xi: FunctionOfTimeUnit,
    sc_t: float,
) -> DiscreteDistributionOnNonNegatives:
    """
    Implements the time evolution equation, by computing the distribution of the relative time
    tauAc_t at which someone infected at t receives a risk notification. This is computed from the
    testing times tauT_t'_g for t'<t, that are averaged with weights given by the distributions
    tausigmag_t.

    :param t: the absolute time at which tauAc_t is computed
    :param tauT: the list of distributions tauT_t'_g (one tuple for each t'=0,...,t-1)
    :param tausigmags_t: the tuple for distributions of the infection time and severity
     of the source.
    :param xi: the suppression factor, as a function of absolute time t.
    :param sc_t: the contact-tracing sensitivity at absolute time t.
    :return: the distribution of tauAc_t.
    """
    if t == 0:
        return DiscreteDistributionOnNonNegatives(pmf_values=[],
                                                  tau_min=0,
                                                  improper=True)

    gs = range(len(tausigmags_t))

    rho_max = max(max(tauT_t[g].tau_max for g in gs) for tauT_t in tauT)

    # The function rho -> checkFT_t(rho) - checkFT_t(0)
    cut_checkFT_t = compute_cut_checkF_t(t=t,
                                         tauT=tauT,
                                         xi=xi,
                                         measures=tausigmags_t)

    def FAc_t(rho: int) -> float:
        """
        The function
        FAc_t(rho) = sc_t(checkFT_t(rho) - checkFT_t(0))
        """
        return sc_t * cut_checkFT_t(rho)

    tauAc_t = generate_discrete_distribution_from_cdf_function(
        cdf=FAc_t,
        tau_min=1,
        tau_max=rho_max,
    )
    return tauAc_t
def free_evolution_global(
    b: Sequence[DiscreteDistributionOnNonNegatives],
    nu_start: int,
    b_negative_times: Optional[DiscreteDistributionOnNonNegatives] = None,
) -> Tuple[List[int], List[float], List[DiscreteDistributionOnNonNegatives]]:
    nu = []
    R = []
    tausigma = []

    for t in range(0, len(b)):
        t_in_days = t * TAU_UNIT_IN_DAYS

        beta_t = b[t]
        R_t = beta_t.total_mass
        R.append(R_t)

        if t == 0 and b_negative_times is None:
            nu_t = nu_start
            tausigma_t = DiscreteDistributionOnNonNegatives(pmf_values=[],
                                                            tau_min=0,
                                                            improper=True)
        else:
            nu_t, tausigma_t = compute_tausigma_and_nu_at_time_t(
                t=t,
                b=b,
                nu=nu,
                b_negative_times=b_negative_times,
                nu_negative_times=nu_start,
            )

        nu.append(nu_t)
        tausigma.append(tausigma_t)

        if t % UNITS_IN_ONE_DAY == 0:
            print(f"""t = {t_in_days} days
                    nu_t = {nu[t]}
                    R_t = {round(R_t, 2)}
                    Fsigma_t(∞) = {tausigma_t.total_mass}
                    """)

    return nu, R, tausigma
Exemplo n.º 12
0
    def test_tausigma_nu_computation_one_severity_no_negative_times(self):
        b = [
            DiscreteDistributionOnNonNegatives(pmf_values=[1.5, 2, 1],
                                               tau_min=1,
                                               improper=True),
            DiscreteDistributionOnNonNegatives(pmf_values=[1.5, 2.5, 1],
                                               tau_min=1,
                                               improper=True),
            DiscreteDistributionOnNonNegatives(pmf_values=[1, 1.4, 0.9],
                                               tau_min=1,
                                               improper=True),
        ]

        nu_1, tausigma_1 = compute_tausigma_and_nu_at_time_t(
            t=1,
            b=b,
            nu=[10],
        )
        assert nu_1 == 15
        assert tausigma_1 == DiscreteDistributionOnNonNegatives(pmf_values=[1],
                                                                tau_min=1)

        nu_2, tausigma_2 = compute_tausigma_and_nu_at_time_t(
            t=2,
            b=b,
            nu=[10, 15],
        )
        assert nu_2 == 10 * 2 + 15 * 1.5 == 42.5
        assert (tausigma_2 == DiscreteDistributionOnNonNegatives(
            pmf_values=[15 * 1.5,
                        10 * 2], tau_min=1, improper=True).normalize())

        nu_3, tausigma_3 = compute_tausigma_and_nu_at_time_t(
            t=3,
            b=b,
            nu=[10, 15, 42.5],
        )
        assert nu_3 == 10 * 1 + 15 * 2.5 + 42.5 * 1 == 90
        assert (tausigma_3 == DiscreteDistributionOnNonNegatives(
            pmf_values=[42.5 * 1, 15 * 2.5,
                        10 * 1], tau_min=1, improper=True).normalize())
def compute_tausigma_and_nu_components_at_time_t(
    t: int,
    b: Sequence[Tuple[DiscreteDistributionOnNonNegatives, ...]],
    nu: List[int],
    p_gs: Tuple[float, ...],
    b_negative_times: Optional[Tuple[DiscreteDistributionOnNonNegatives,
                                     ...]] = None,
    nu_negative_times: Optional[int] = None,
) -> Tuple[Tuple[float, ...], Optional[Tuple[
        DiscreteDistributionOnNonNegatives, ...]]]:
    """
    Computes, for each g:
    - The number nug_t of people infected at t by someone with severity g
    - The improper distribution tausigmag_t of probabilities to be infected by someone with
      severity g that was infected at a given time prior to t

    :param t: the absolute time at which we want to compute tausigma.
    :param b: the sequence of discretized infectiousnesses from time 0 to at least t-1.
    :param nu: the number of infected people per time step, from time 0 to t-1.
    :param p_gs: the tuple of fractions of infected people with given severity.
    :param b_negative_times: the (optional) tuple of discretized infectiousness distributions (one
     for each severity) at times t<0. They should be normalized in such a way that R
     at negative times is 1, to give meaningful results (as we assume that nu at negative times is
     constantly equal to nu_0).
    :param nu_negative_times: the (constant) number of people infected at a time t<0.
    :return: The tuples (nug_t) and (tausigmag_t).
    """
    if t == 0 and b_negative_times is None:
        raise ValueError(
            "No computation can be done at t=0 without past data.")
    if nu_negative_times and t > 0:
        assert abs(nu[0] -
                   nu_negative_times) < DISTRIBUTION_NORMALIZATION_TOLERANCE
    assert t <= len(b) and t == len(nu)

    gs = range(len(p_gs))

    mgs_t = []
    nugs_t = []

    for g in gs:
        # Let mg_t(tau) be the number of people infected at t by someone infected on t - tau
        # with severity g. We create a list m_t_g_pmf_values of these numbers for tau = 1,2,...
        mg_t_pmf_values = [
            p_gs[g] * b[t - tau][g].pmf(tau) * nu[t - tau]
            for tau in range(1, t + 1)
        ]
        if b_negative_times is not None and nu_negative_times is not None:
            tau_max_beta_negative = b_negative_times[g].tau_max
            mg_t_pmf_values += [
                p_gs[g] * b_negative_times[g].pmf(tau) * nu_negative_times
                for tau in range(t + 1, tau_max_beta_negative + 1)
            ]
        mg_t = DiscreteDistributionOnNonNegatives(pmf_values=mg_t_pmf_values,
                                                  tau_min=1,
                                                  improper=True)
        # Now nug_t (number of people infected at t by someone with severity g) is the sum of
        # mg_t(tau) for all tau:
        nug_t = mg_t.total_mass
        nugs_t.append(nug_t)
        mgs_t.append(mg_t)

    nu_t = sum(nugs_t)  # People infected at t
    if nu_t == 0:
        return tuple(nugs_t), None
    tausigmags_t = [mg_t.rescale_by_factor(1 / nu_t) for mg_t in mgs_t]

    # Check that tausigma_t is correctly normalized (for t = 0, this also checks that nu_t is
    # nu_negative_times):
    discrepancy = abs(
        sum(tausigmag_t.total_mass for tausigmag_t in tausigmags_t) - 1)
    assert discrepancy < DISTRIBUTION_NORMALIZATION_TOLERANCE

    return tuple(nugs_t), tuple(tausigmags_t)
def compute_tausigma_and_nu_components_at_time_t_with_app(
    t: int,
    b_app: Sequence[Tuple[DiscreteDistributionOnNonNegatives, ...]],
    b_noapp: Sequence[Tuple[DiscreteDistributionOnNonNegatives, ...]],
    nu: List[int],
    p_gs: Tuple[float, ...],
    epsilon_app: Callable[[int], float],
    b_negative_times: Optional[Tuple[DiscreteDistributionOnNonNegatives,
                                     ...]] = None,
    nu_negative_times: Optional[int] = None,
) -> Tuple[Tuple[float, ...], Optional[Tuple[
        DiscreteDistributionOnNonNegatives, ...]], Tuple[float, ...],
           Optional[Tuple[DiscreteDistributionOnNonNegatives, ...]], ]:
    """
    Computes, for each g, a:
    - The number nuga_t of people infected at t by someone with severity g and app/no app status a
    - The improper distribution tausigmaga_t of probabilities to be infected by someone with
      severity g app/no app status a,that was infected at a given time prior to t

    :param t: the absolute time at which we want to compute tausigma.
    :param b_app: the sequence of discretized infectiousnesses from time 0 to at least t-1,
      for individuals with the app.
    :param b_noapp: the sequence of discretized infectiousnesses from time 0 to at least t-1,
      for individuals without the app.
    :param nu: the number of infected people per time step, from time 0 to t-1.
    :param p_gs: the tuple of fractions of infected people with given severity.
    :param epsilon_app: the probability that an individual infected at t has the app,
    as a function of t.
    :param b_negative_times: the (optional) tuple of discretized infectiousness distributions (one
     for each severity) at times t<0. They should be normalized in such a way that R
     at negative times is 1, to give meaningful results (as we assume that nu at negative times is
     constantly equal to nu_0).
    :param nu_negative_times: the (constant) number of people infected at a time t<0.
    :return: The tuples (nuga_t) and (tausigmaga_t), for a = app, no app.
    """
    if t == 0 and b_negative_times is None:
        raise ValueError(
            "No computation can be done at t=0 without past data.")
    if nu_negative_times and t > 0:
        assert abs(nu[0] -
                   nu_negative_times) < DISTRIBUTION_NORMALIZATION_TOLERANCE
    assert t <= len(b_app) and t <= len(b_noapp) and t == len(nu)

    gs = range(len(p_gs))

    # app/no app status: a = [0, 1] = [app, noapp]
    p_as = [epsilon_app, lambda t: 1 - epsilon_app(t)]
    b = [b_app, b_noapp]

    mgsas_t = [[], []]
    nugsas_t = [[], []]
    for a in [0, 1]:
        for g in gs:
            # Let mga_t(tau) be the number of people infected at t by someone infected on t - tau
            # with severity g and app usage a. We create a list mga_t_pmf_values of
            # these numbers for tau = 1,2,...
            mga_t_pmf_values = [
                p_as[a](t - tau) * p_gs[g] * nu[t - tau] *
                b[a][t - tau][g].pmf(tau) for tau in range(1, t + 1)
            ]
            if b_negative_times is not None and nu_negative_times is not None:
                tau_max_beta_negative = b_negative_times[g].tau_max
                p_g_a_negative_time = p_gs[g] if a == 1 else 0
                mga_t_pmf_values += [
                    p_g_a_negative_time * nu_negative_times *
                    b_negative_times[g].pmf(tau)
                    for tau in range(t + 1, tau_max_beta_negative + 1)
                ]
            mga_t = DiscreteDistributionOnNonNegatives(
                pmf_values=mga_t_pmf_values, tau_min=1, improper=True)
            # Now nuga_t (number of people infected at t by someone with severity g and app usage a)
            # is the sum of mga_t(tau) for all tau:
            nuga_t = mga_t.total_mass
            nugsas_t[a].append(nuga_t)
            mgsas_t[a].append(mga_t)

    nu_t = sum(nugsas_t[0]) + sum(nugsas_t[1])  # People infected at t
    if nu_t == 0:
        return tuple(nugsas_t[0]), None, tuple(nugsas_t[1]), None
    tausigmagsapp_t = [mg_t.rescale_by_factor(1 / nu_t) for mg_t in mgsas_t[0]]
    tausigmagsnoapp_t = [
        mg_t.rescale_by_factor(1 / nu_t) for mg_t in mgsas_t[1]
    ]

    # Check that tausigma_t is correctly normalized (for t = 0, this also checks that nu_t is
    # nu_negative_times):
    discrepancy = abs(
        sum(tausigmag_t.total_mass for tausigmag_t in tausigmagsapp_t) +
        sum(tausigmag_t.total_mass for tausigmag_t in tausigmagsnoapp_t) - 1)
    assert discrepancy < DISTRIBUTION_NORMALIZATION_TOLERANCE

    return (
        tuple(nugsas_t[0]),
        tuple(tausigmagsapp_t),
        tuple(nugsas_t[1]),
        tuple(tausigmagsnoapp_t),
    )
Exemplo n.º 15
0
    def test_tausigma_nu_computation_with_app(self):

        p_gs = (0.4, 0.6)
        epsilon_app = lambda t: 0.3
        rho_0 = DiscreteDistributionOnNonNegatives(pmf_values=[0.3, 0.4, 0.3],
                                                   tau_min=1,
                                                   improper=True)
        c_0 = 0.5
        c_1 = (-c_0 * p_gs[0] + 1) / p_gs[1]
        assert floats_match(c_1, 4 / 3)
        b_negative_times = (
            rho_0.rescale_by_factor(scale_factor=c_0),
            rho_0.rescale_by_factor(scale_factor=c_1),
        )
        check_b_negative_times(
            p_gs=p_gs,
            b_negative_times=b_negative_times,
        )

        R_0_app = 2
        R_0_noapp = 3
        b_0_app = (
            rho_0.rescale_by_factor(scale_factor=c_0 * R_0_app),
            rho_0.rescale_by_factor(scale_factor=c_1 * R_0_app),
        )

        b_0_noapp = (
            rho_0.rescale_by_factor(scale_factor=c_0 * R_0_noapp),
            rho_0.rescale_by_factor(scale_factor=c_1 * R_0_noapp),
        )
        nu_0 = 10

        (
            nugsapp_1,
            tausigmagsapp_1,
            nugsnoapp_1,
            tausigmagsnoapp_1,
        ) = compute_tausigma_and_nu_components_at_time_t_with_app(
            t=1,
            b_app=[b_0_app],
            b_noapp=[b_0_noapp],
            nu=[nu_0],
            p_gs=p_gs,
            epsilon_app=epsilon_app,
            b_negative_times=b_negative_times,
            nu_negative_times=nu_0,
        )

        expected_nu0app_1_addends = [
            nu_0 * p_gs[0] * epsilon_app(0) * c_0 * R_0_app *
            0.3,  # Infections from infected at t=0
            0,  # Infections from infected at t=-1
            0,  # Infections from infected at t=-2
        ]

        expected_nu0noapp_1_addends = [
            nu_0 * p_gs[0] * (1 - epsilon_app(0)) * c_0 * R_0_noapp *
            0.3,  # Infections from infected at t=0
            nu_0 * p_gs[0] * c_0 * 0.4,  # Infections from infected at t=-1
            nu_0 * p_gs[0] * c_0 * 0.3,  # Infections from infected at t=-2
        ]

        expected_nu1app_1_addends = [
            nu_0 * p_gs[1] * epsilon_app(0) * c_1 * R_0_app *
            0.3,  # Infections from infected at t=0
            0,  # Infections from infected at t=-1
            0,  # Infections from infected at t=-2
        ]

        expected_nu1noapp_1_addends = [
            nu_0 * p_gs[1] * (1 - epsilon_app(0)) * c_1 * R_0_noapp *
            0.3,  # Infections from infected at t=0
            nu_0 * p_gs[1] * c_1 * 0.4,  # Infections from infected at t=-1
            nu_0 * p_gs[1] * c_1 * 0.3,  # Infections from infected at t=-2
        ]

        expected_nu0app_1 = sum(expected_nu0app_1_addends)
        expected_nu0noapp_1 = sum(expected_nu0noapp_1_addends)
        expected_nu1app_1 = sum(expected_nu1app_1_addends)
        expected_nu1noapp_1 = sum(expected_nu1noapp_1_addends)

        expected_nu_1 = (expected_nu0app_1 + expected_nu0noapp_1 +
                         expected_nu1app_1 + expected_nu1noapp_1)

        assert float_sequences_match(nugsapp_1,
                                     (expected_nu0app_1, expected_nu1app_1))
        assert float_sequences_match(
            nugsnoapp_1, (expected_nu0noapp_1, expected_nu1noapp_1))
        assert tausigmagsapp_1[0] == DiscreteDistributionOnNonNegatives(
            pmf_values=expected_nu0app_1_addends, tau_min=1,
            improper=True).rescale_by_factor(scale_factor=1 / expected_nu_1)
        assert tausigmagsnoapp_1[0] == DiscreteDistributionOnNonNegatives(
            pmf_values=expected_nu0noapp_1_addends, tau_min=1,
            improper=True).rescale_by_factor(scale_factor=1 / expected_nu_1)
        assert tausigmagsapp_1[1] == DiscreteDistributionOnNonNegatives(
            pmf_values=expected_nu1app_1_addends, tau_min=1,
            improper=True).rescale_by_factor(scale_factor=1 / expected_nu_1)
        assert tausigmagsnoapp_1[1] == DiscreteDistributionOnNonNegatives(
            pmf_values=expected_nu1noapp_1_addends, tau_min=1,
            improper=True).rescale_by_factor(scale_factor=1 / expected_nu_1)
    def test_compute_tauAc(self):
        """
        Tests the time evolution equation in the homogeneous scenario.
        """

        # 1 - No suppression, one component, one contribution
        sc = 0.5
        tauAc_t = compute_tauAc_t(
            t=1,
            tauT=[
                (
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.1, 0.2, 0.3], tau_min=2, improper=True
                    ),
                )
            ],
            tausigmags_t=(DiscreteDistributionOnNonNegatives(pmf_values=[1], tau_min=1),),
            xi=lambda t: 0,
            sc_t=sc,
        )
        expected_tauAc_t = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.1, 0.2, 0.3], tau_min=1, improper=True
        ).rescale_by_factor(sc)
        assert tauAc_t == expected_tauAc_t

        # 2 - No suppression, one component, one cut contribution
        sc = 0.5
        tauAc_t = compute_tauAc_t(
            t=1,
            tauT=[
                (
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.1, 0.2, 0.3], tau_min=1, improper=True
                    ),
                )
            ],
            tausigmags_t=(DiscreteDistributionOnNonNegatives(pmf_values=[1], tau_min=1),),
            xi=lambda t: 0,
            sc_t=sc,
        )
        expected_tauAc_t = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.2, 0.3], tau_min=1, improper=True
        ).rescale_by_factor(sc)
        assert tauAc_t == expected_tauAc_t

        # 3 - No suppression, one component, two contributions
        sc = 0.5
        tauAc_t = compute_tauAc_t(
            t=3,
            tauT=[
                (
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.1, 0.2, 0.3], tau_min=2, improper=True
                    ),
                )
            ]
            * 3,
            tausigmags_t=(DiscreteDistributionOnNonNegatives(pmf_values=[0.4, 0.6, 0], tau_min=1),),
            xi=lambda t: 0,
            sc_t=sc,
        )
        expected_tauAc_t = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.1 * 0.4 + 0.2 * 0.6, 0.2 * 0.4 + 0.3 * 0.6, 0.3 * 0.4],
            tau_min=1,
            improper=True,
        ).rescale_by_factor(sc)
        assert tauAc_t == expected_tauAc_t

        # 4 - With suppression, one component, two contributions
        sc = 0.5
        xi = 0.6
        tauAc_t = compute_tauAc_t(
            t=3,
            tauT=[
                (
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.1, 0.2, 0.3], tau_min=2, improper=True
                    ),
                )
            ]
            * 3,
            tausigmags_t=(DiscreteDistributionOnNonNegatives(pmf_values=[0.4, 0.6, 0], tau_min=1),),
            xi=lambda t: xi,
            sc_t=sc,
        )
        assert all(tauAc_t.pmf(tau) >= expected_tauAc_t.pmf(tau) for tau in [1, 2, 3])
        assert tauAc_t.total_mass > expected_tauAc_t.total_mass

        # 5 - No suppression, two components, two contributions
        xi = 0
        sc = 0.6
        tauAc_t = compute_tauAc_t(
            t=4,
            tauT=[
                (
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.1, 0.2, 0], tau_min=2, improper=True
                    ),
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.1, 0.2, 0.3], tau_min=2, improper=True
                    ),
                )
            ]
            * 4,
            tausigmags_t=(
                DiscreteDistributionOnNonNegatives(
                    pmf_values=[0.1, 0.2, 0, 0], tau_min=1, improper=True,
                ),
                DiscreteDistributionOnNonNegatives(
                    pmf_values=[0.3, 0.4, 0, 0], tau_min=1, improper=True,
                ),
            ),
            xi=lambda t: xi,
            sc_t=sc,
        )
        expectedchecktauT0_4 = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.1 * 0.1 + 0.2 * 0.2, 0.2 * 0.1], tau_min=1, improper=True,
        )
        expectedchecktauT1_4 = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.1 * 0.3 + 0.2 * 0.4, 0.2 * 0.3 + 0.3 * 0.4, 0.3 * 0.3],
            tau_min=1,
            improper=True,
        )

        expected_tauAc_t = linear_combination_discrete_distributions_by_values(
            scalars=[sc, sc], seq=[expectedchecktauT0_4, expectedchecktauT1_4],
        )
        assert tauAc_t == expected_tauAc_t
def compute_time_evolution_homogeneous_case(
    scenario: HomogeneousScenario,
    t_max_in_days: int,
    nu_start: int,
    b_negative_times: Optional[Tuple[DiscreteDistributionOnNonNegatives,
                                     ...]] = None,
    verbose: bool = True,
    threshold_to_stop: Optional[float] = None,
) -> Tuple[List[int], List[float], List[float], List[float], List[Tuple[
        float, ...]], List[float], ]:
    """
    The main function that implements the algorithm, computing the time evolution in the
    "homogeneous" scenario, in which the same tracing and isolation measures apply to the whole
    population.
    Note that the time step Δ𝜏 is determined by the constant
        math_utilities.config.UNITS_IN_ONE_DAY.
    :param scenario: the object gathering the input parameters (epidemic data and
      suppression measures).
    :param t_max_in_days: the maximum number of days for which the algorithm runs
    :param nu_start: the initial number of infected people per time step.
    :param b_negative_times: the optional infectiousness (by degree of severity) at times t<0.
      These improper distribution must be jointly normalized to have total mass 1,
      to ensure that the number of infections per time step is constantly equal to nu_start at t<0.
      If None, the epidemic is assumed to start at t=0.
    :param verbose: if True, the main KPIs are printed for each day.
    :param threshold_to_stop: an optional float that makes the algorithm stop if the reproduction
      number R_t and total testing probability FT_infty_t have had relative variations below
      threshold_to_stop in the previous iteration.
    :return: Several lists of floats, one per time step:
    - t_in_days_list: the absolute times 0, Δ𝜏, 2Δ𝜏,...
    - nu: the number of infections at each time step.
    - nu0: the number of infections at each time step, if there were no isolation measures.
    - R: the effective reproduction numbers.
    - R_by_severity: the tuples of effective reproduction numbers by degree of severity,
    - FT_infty: the probabilities to be eventually tested positive.
    """
    #
    t_in_days_list = []
    nu = []
    nu0 = []
    b: List[Tuple[DiscreteDistributionOnNonNegatives, ...]] = []
    R_by_severity: List[Tuple[float, ...]] = []
    R: List[float] = []
    tausigma: List[Tuple[DiscreteDistributionOnNonNegatives, ...]] = []
    tauT: List[Tuple[DiscreteDistributionOnNonNegatives, ...]] = []
    FT_infty: List[float, ...] = []

    gs = range(scenario.n_severities)  # Values of severity G

    t_max = t_max_in_days * UNITS_IN_ONE_DAY
    for t in range(0, t_max + 1):
        t_in_days = t / UNITS_IN_ONE_DAY

        # Compute tausigma_t and nu_t from nu_t' and b_t' for t' = 0,...,t-1
        if t == 0 and b_negative_times is None:
            nu_t = nu_start
            nugs_t = tuple(nu_t * p_g for p_g in scenario.p_gs)
            nu0_t = nu_start
            tausigmags_t = tuple(
                DiscreteDistributionOnNonNegatives(
                    pmf_values=[], tau_min=0, improper=True) for _ in gs)
        else:
            nugs_t, tausigmags_t = compute_tausigma_and_nu_components_at_time_t(
                t=t,
                b=b,
                nu=nu,
                p_gs=scenario.p_gs,
                b_negative_times=b_negative_times,
                nu_negative_times=nu_start,
            )
            nugs0_t, _ = compute_tausigma_and_nu_components_at_time_t(
                t=t,
                b=[scenario.b0_gs] * t,
                nu=nu0,
                p_gs=scenario.p_gs,
                b_negative_times=b_negative_times,
                nu_negative_times=nu_start,
            )

            nu_t = sum(nugs_t)  # People infected at t
            nu0_t = sum(
                nugs0_t)  # People infected at t without isolation measures

            if nu_t < 0.5:  # Breaks the loop when nu_t = 0
                break

        # Compute tauAs_t components from tauS
        tauAs_t_gs = tuple(
            scenario.tauS.rescale_by_factor(scenario.ss[g](t)) for g in gs)

        # Time evolution step:
        # Compute tauAc_t from tausigma_t and tauT_t' (for t' = 0,...,t-1) components
        tauAc_t = compute_tauAc_t(
            t=t,
            tauT=tauT,
            tausigmags_t=tausigmags_t,
            xi=scenario.xi,
            sc_t=scenario.sc(t),
        )

        # Compute tauA_t and tauT_t components from tauAs_t, tauAc_t, and DeltaAT
        tauT_t_gs = compute_tauT_t(tauAs_t_gs=tauAs_t_gs,
                                   tauAc_t=tauAc_t,
                                   DeltaAT=scenario.DeltaAT)

        # Compute b and R
        b_t_gs = compute_suppressed_b_t(b0_t_gs=scenario.b0_gs,
                                        tauT_t_gs=tauT_t_gs,
                                        xi_t=scenario.xi(t))
        R_t_gs = tuple(b_t_g.total_mass for b_t_g in b_t_gs)
        R_t = sum(p_g * R_t_g for (p_g, R_t_g) in zip(scenario.p_gs, R_t_gs))
        FT_t_infty = sum(p_g * tauT_t_g.total_mass
                         for (p_g, tauT_t_g) in zip(scenario.p_gs, tauT_t_gs))

        t_in_days_list.append(t_in_days)
        tausigma.append(tausigmags_t)
        nu.append(nu_t)
        nu0.append(nu0_t)
        b.append(b_t_gs)
        R.append(R_t)
        R_by_severity.append(R_t_gs)
        tauT.append(tauT_t_gs)
        FT_infty.append(FT_t_infty)

        if verbose and t % UNITS_IN_ONE_DAY == 0:
            EtauC_t_gs_in_days = [
                b_t_g.normalize().mean() * UNITS_IN_ONE_DAY for b_t_g in b_t_gs
            ]

            print(f"""t = {t_in_days} days
                    nugs_t = {tuple(nugs_t)},   nu_t = {int(round(nu_t, 0))}
                    nu0_t = {int(round(nu0_t, 0))}
                    R_t_gs = {R_t_gs},    R_t = {round(R_t, 2)}
                    EtauC_t_gs = {tuple(EtauC_t_gs_in_days)} days
                    Fsigmags_t(∞) = {tuple(tausigmag_t.total_mass for tausigmag_t in tausigmags_t)}
                    FAs_t_gs(∞) = {tuple(tauAs_t_g.total_mass for tauAs_t_g in tauAs_t_gs)}
                    FAc_t(∞) = {tauAc_t.total_mass}
                    FT_t_gs(∞) = {tuple(tauT_t_g.total_mass for tauT_t_g in tauT_t_gs)},   FT_t(∞) = {round(FT_t_infty, 2)}
                    tauT_t_gs_mean = {tuple(tauT_t_g.normalize().mean() if tauT_t_g.total_mass > 0 else None for tauT_t_g in tauT_t_gs)},
                    """)

        if (threshold_to_stop is not None and t > 10
                and (abs((R[-2] - R[-1]) / R[-2]) < threshold_to_stop
                     and FT_infty[-2] != 0 and abs(
                         (FT_infty[-2] - FT_infty[-1]) / FT_infty[-2]) <
                     threshold_to_stop)):
            break

    return t_in_days_list, nu, nu0, R, R_by_severity, FT_infty
    def test_compute_tauAc_with_app(self):
        """
        Tests the time evolution equation in the scenario with app.
        """

        xi = 0
        scapp = 0.6
        scnoapp = 0.2

        kwargs = dict(
            t=4,
            tauT_app=[
                (
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.1, 0.13, 0], tau_min=2, improper=True
                    ),
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.11, 0.19], tau_min=2, improper=True
                    ),
                )
            ]
            * 3
            + [
                (
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.1, 0.16, 0], tau_min=2, improper=True
                    ),
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.125, 0.19], tau_min=2, improper=True
                    ),
                )
            ],
            tauT_noapp=[
                (
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.08, 0.14, 0], tau_min=2, improper=True
                    ),
                    DiscreteDistributionOnNonNegatives(
                        pmf_values=[0.12, 0.23, 0.18], tau_min=2, improper=True
                    ),
                )
            ]
            * 4,
            tausigmagsapp_t=(
                DiscreteDistributionOnNonNegatives(
                    pmf_values=[0.05, 0.1, 0, 0], tau_min=1, improper=True,
                ),
                DiscreteDistributionOnNonNegatives(
                    pmf_values=[0.1, 0.2, 0, 0], tau_min=1, improper=True,
                ),
            ),
            tausigmagsnoapp_t=(
                DiscreteDistributionOnNonNegatives(
                    pmf_values=[0.1, 0.15, 0, 0], tau_min=1, improper=True,
                ),
                DiscreteDistributionOnNonNegatives(
                    pmf_values=[0.15, 0.25, 0, 0], tau_min=1, improper=True,
                ),
            ),
            xi=lambda t: xi,
            scapp_t=scapp,
            scnoapp_t=scnoapp,
        )

        tauAc_t_app, tauAc_t_noapp = compute_tauAc_t_two_components(**kwargs)

        expectedchecktauT0app_4 = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.1 * 0.05 + 0.13 * 0.1, 0.16 * 0.05], tau_min=1, improper=True,
        )
        expectedchecktauT1app_4 = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.125 * 0.1 + 0.19 * 0.2, 0.19 * 0.1], tau_min=1, improper=True,
        )

        expectedchecktauT0noapp_4 = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.08 * 0.1 + 0.14 * 0.15, 0.14 * 0.1], tau_min=1, improper=True,
        )
        expectedchecktauT1noapp_4 = DiscreteDistributionOnNonNegatives(
            pmf_values=[0.12 * 0.15 + 0.23 * 0.25, 0.23 * 0.15 + 0.18 * 0.25, 0.18 * 0.15,],
            tau_min=1,
            improper=True,
        )

        expected_tauAc_t_app = linear_combination_discrete_distributions_by_values(
            scalars=[scapp, scapp, scnoapp, scnoapp],
            seq=[
                expectedchecktauT0app_4,
                expectedchecktauT1app_4,
                expectedchecktauT0noapp_4,
                expectedchecktauT1noapp_4,
            ],
        )

        expected_tauAc_t_noapp = linear_combination_discrete_distributions_by_values(
            scalars=[scnoapp, scnoapp, scnoapp, scnoapp],
            seq=[
                expectedchecktauT0app_4,
                expectedchecktauT1app_4,
                expectedchecktauT0noapp_4,
                expectedchecktauT1noapp_4,
            ],
        )
        assert tauAc_t_app == expected_tauAc_t_app
        assert tauAc_t_noapp == expected_tauAc_t_noapp

        # Same, but this time with suppression
        kwargs["xi"] = lambda t: 0.6

        tauAc_t_app, tauAc_t_noapp = compute_tauAc_t_two_components(**kwargs)

        assert all(
            tauAc_t_app.pmf(tau) >= expected_tauAc_t_app.pmf(tau) - FLOAT_TOLERANCE_FOR_EQUALITIES
            for tau in [1, 2, 3]
        )
        assert tauAc_t_app.total_mass > expected_tauAc_t_app.total_mass

        assert all(
            tauAc_t_noapp.pmf(tau)
            >= expected_tauAc_t_noapp.pmf(tau) - FLOAT_TOLERANCE_FOR_EQUALITIES
            for tau in [1, 2, 3]
        )
        assert tauAc_t_noapp.total_mass > expected_tauAc_t_noapp.total_mass
Exemplo n.º 19
0
def compute_tauAc_t_two_components(
    t: int,
    tauT_app: List[Tuple[DiscreteDistributionOnNonNegatives, ...]],
    tauT_noapp: List[Tuple[DiscreteDistributionOnNonNegatives, ...]],
    tausigmagsapp_t: Tuple[DiscreteDistributionOnNonNegatives, ...],
    tausigmagsnoapp_t: Tuple[DiscreteDistributionOnNonNegatives, ...],
    xi: FunctionOfTimeUnit,
    scapp_t: float,
    scnoapp_t: float,
) -> Tuple[DiscreteDistributionOnNonNegatives,
           DiscreteDistributionOnNonNegatives]:
    """
    Implements the time evolution equation in the two-components scenario,
    by computing the distribution of the relative time tauAc_t at which someone
    infected at t receives a risk notification. This is computed from the
    testing times tauT_t'_g for t'<t, that are averaged with weights given by the distributions
    tausigmag_t.

    :param t: the absolute time at which tauAc_t is computed
    :param tauT: the list of distributions tauT_t'_g (one tuple for each t'=0,...,t-1)
    :param tausigmags_t: the tuple for distributions of the infection time and severity
     of the source.
    :param xi: the suppression factor, as a function of absolute time t.
    :param sc: the contact-tracing sensitivity, as a function of absolute time t.
    :return: the distribution of tauAc_t.
    """
    if t == 0:
        return (
            DiscreteDistributionOnNonNegatives(pmf_values=[],
                                               tau_min=0,
                                               improper=True),
            DiscreteDistributionOnNonNegatives(pmf_values=[],
                                               tau_min=0,
                                               improper=True),
        )
    gs = range(len(tausigmagsapp_t))

    # The improper distribution with CDF rho -> checkFTapp_t(rho) - checkFTapp_t(0)
    cut_checkFTapp_t = compute_cut_checkF_t(t=t,
                                            tauT=tauT_app,
                                            xi=xi,
                                            measures=tausigmagsapp_t)
    rho_max_app = max(
        max(tauT_t[g].tau_max for g in gs) for tauT_t in tauT_app)
    cut_check_tauTapp_t = generate_discrete_distribution_from_cdf_function(
        cdf=cut_checkFTapp_t, tau_min=1, tau_max=rho_max_app)

    # The improper distribution with CDF rho -> checkFTnoapp_t(rho) - checkFTnoapp_t(0)
    cut_checkFTnoapp_t = compute_cut_checkF_t(t=t,
                                              tauT=tauT_noapp,
                                              xi=xi,
                                              measures=tausigmagsnoapp_t)
    rho_max_noapp = max(
        max(tauT_t[g].tau_max for g in gs) for tauT_t in tauT_noapp)
    cut_check_tauTnoapp_t = generate_discrete_distribution_from_cdf_function(
        cdf=cut_checkFTnoapp_t, tau_min=1, tau_max=rho_max_noapp)

    tauAc_t_app = linear_combination_discrete_distributions_by_values(
        scalars=[scapp_t, scnoapp_t],
        seq=[cut_check_tauTapp_t, cut_check_tauTnoapp_t],
        use_cdfs=True,
    )
    tauAc_t_noapp = linear_combination_discrete_distributions_by_values(
        scalars=[scnoapp_t, scnoapp_t],
        seq=[cut_check_tauTapp_t, cut_check_tauTnoapp_t],
        use_cdfs=True,
    )

    return tauAc_t_app, tauAc_t_noapp
def compute_time_evolution_with_app(
    scenario: ScenarioWithApp,
    t_max_in_days: int,
    nu_start: int,
    b_negative_times: Optional[Tuple[DiscreteDistributionOnNonNegatives, ...]] = None,
    verbose: bool = True,
    threshold_to_stop: Optional[float] = None,
) -> Tuple[
    List[float],
    List[float],
    List[float],
    List[float],
    List[float],
    List[float],
    List[float],
    List[float],
    List[float],
    List[float],
]:
    """
    The main function that implements the algorithm, computing the time evolution in the scenario
    in which an app for epidemic control is used.
    Note that the time step Δ𝜏 is determined by the constant
        math_utilities.config.UNITS_IN_ONE_DAY.
    :param scenario: the object gathering the input parameters (epidemic data and
      suppression measures).
    :param t_max_in_days: the maximum number of days for which the algorithm runs
    :param nu_start: the initial number of infected people per time step.
    :param b_negative_times: the optional infectiousness (by degree of severity) at times t<0.
      These improper distribution must be jointly normalized to have total mass 1,
      to ensure that the number of infections per time step is constantly equal to nu_start at t<0.
      If None, the epidemic is assumed to start at t=0.
    :param verbose: if True, the main KPIs are printed for each day.
    :param threshold_to_stop: an optional float that makes the algorithm stop if the reproduction
      number R_t and total testing probability FT_infty_t have had relative variations below
      threshold_to_stop in the previous iteration.
    :return: Several lists of floats, one per time step:
    - t_in_days_list: the absolute times 0, Δ𝜏, 2Δ𝜏,...
    - nu: the number of infections at each time step.
    - nu0: the number of infections at each time step, if there were no isolation measures.
    - Fsigmaapp_infty: the probabilities that one's infector was using the app.
    - R: the effective reproduction numbers.
    - R_app: the effective reproduction numbers for people with the app.
    - R_noapp: the effective reproduction numbers for people without the app.
    - FT_infty: the probabilities to be eventually tested positive.
    - FT_app_infty: the probabilities to be eventually tested positive, for people with the app.
    - FT_noapp_infty: the probabilities to be eventually tested positive, for people without
      the app.
    """
    #
    t_in_days_list: List[float] = []
    nu = []
    nu_app = []
    nu_noapp = []
    nu0 = []
    tausigma_app: List[Tuple[DiscreteDistributionOnNonNegatives, ...]] = []
    tausigma_noapp: List[Tuple[DiscreteDistributionOnNonNegatives, ...]] = []
    Fsigmaapp_infty: List[float] = []
    b_app: List[Tuple[DiscreteDistributionOnNonNegatives, ...]] = []
    b_noapp: List[Tuple[DiscreteDistributionOnNonNegatives, ...]] = []
    R_app: List[float] = []
    R_noapp: List[float] = []
    R: List[float] = []
    tauT_app: List[Tuple[DiscreteDistributionOnNonNegatives, ...]] = []
    tauT_noapp: List[Tuple[DiscreteDistributionOnNonNegatives, ...]] = []
    FT_infty: List[float, ...] = []
    FT_app_infty: List[float, ...] = []
    FT_noapp_infty: List[float, ...] = []

    gs = range(scenario.n_severities)  # Values of severity G

    t_max = t_max_in_days * UNITS_IN_ONE_DAY
    for t in range(0, t_max + 1):
        t_in_days = t / UNITS_IN_ONE_DAY

        pgs_t_app = tuple(p_g * scenario.epsilon_app(t) for p_g in scenario.p_gs)
        pgs_t_noapp = tuple(p_g * (1 - scenario.epsilon_app(t)) for p_g in scenario.p_gs)

        # Compute tausigma_t and nu_t from nu_t' and b_t' for t' = 0,...,t-1
        if t == 0 and b_negative_times is None:

            nugsapp_t = tuple(nu_start * p_g for p_g in pgs_t_app)
            nugsnoapp_t = tuple(nu_start * p_g for p_g in pgs_t_noapp)
            nu0_t = nu_start
            tausigmagsapp_t = tausigmagsnoapp_t = tuple(
                DiscreteDistributionOnNonNegatives(pmf_values=[], tau_min=0, improper=True)
                for _ in gs
            )
        else:
            (
                nugsapp_t,
                tausigmagsapp_t,
                nugsnoapp_t,
                tausigmagsnoapp_t,
            ) = compute_tausigma_and_nu_components_at_time_t_with_app(
                t=t,
                b_app=b_app,
                b_noapp=b_noapp,
                nu=nu,
                p_gs=scenario.p_gs,
                epsilon_app=scenario.epsilon_app,
                b_negative_times=b_negative_times,
                nu_negative_times=nu_start,
            )

            nu0_t_gs, _ = compute_tausigma_and_nu_components_at_time_t(
                t=t,
                b=[scenario.b0_gs] * t,
                nu=nu0,
                p_gs=scenario.p_gs,
                b_negative_times=b_negative_times,
                nu_negative_times=nu_start,
            )
            nu0_t = sum(nu0_t_gs)  # People infected at t without isolation measures
        # Prob. that infector had the app
        Fsigmaapp_t_infty = sum(tausigmag_t.total_mass for tausigmag_t in tausigmagsapp_t)
        nuapp_t = sum(nugsapp_t)
        nunoapp_t = sum(nugsnoapp_t)

        nu_t = nuapp_t + nunoapp_t  # People infected at t

        if nu_t < 0.5:  # Breaks the loop when nu_t = 0
            break

        # Compute tauAs_t components from tauS
        tauAs_t_gs_app = tuple(scenario.tauS.rescale_by_factor(scenario.ssapp[g](t)) for g in gs)
        tauAs_t_gs_noapp = tuple(
            scenario.tauS.rescale_by_factor(scenario.ssnoapp[g](t)) for g in gs
        )

        # Time evolution step:
        # Compute tauAc_t from tausigma_t and tauT_t' (for t' = 0,...,t-1) components
        tauAc_t_app, tauAc_t_noapp = compute_tauAc_t_two_components(
            t=t,
            tauT_app=tauT_app,
            tauT_noapp=tauT_noapp,
            tausigmagsapp_t=tausigmagsapp_t,
            tausigmagsnoapp_t=tausigmagsnoapp_t,
            xi=scenario.xi,
            scapp_t=scenario.scapp(t),
            scnoapp_t=scenario.scnoapp(t),
        )

        # Compute tauA_t and tauT_t components from tauAs_t, tauAc_t, and DeltaAT
        tauT_t_gs_app = compute_tauT_t(
            tauAs_t_gs=tauAs_t_gs_app, tauAc_t=tauAc_t_app, DeltaAT=scenario.DeltaATapp
        )
        tauT_t_gs_noapp = compute_tauT_t(
            tauAs_t_gs=tauAs_t_gs_noapp, tauAc_t=tauAc_t_noapp, DeltaAT=scenario.DeltaATnoapp,
        )

        # Compute b and R
        b_t_gs_app = compute_suppressed_b_t(
            b0_t_gs=scenario.b0_gs, tauT_t_gs=tauT_t_gs_app, xi_t=scenario.xi(t)
        )
        b_t_gs_noapp = compute_suppressed_b_t(
            b0_t_gs=scenario.b0_gs, tauT_t_gs=tauT_t_gs_noapp, xi_t=scenario.xi(t)
        )
        R_t_gs_app = tuple(b_t_g.total_mass for b_t_g in b_t_gs_app)
        R_t_gs_noapp = tuple(b_t_g.total_mass for b_t_g in b_t_gs_noapp)
        R_t_app = sum(p_g * R_t_g for (p_g, R_t_g) in zip(scenario.p_gs, R_t_gs_app))
        R_t_noapp = sum(p_g * R_t_g for (p_g, R_t_g) in zip(scenario.p_gs, R_t_gs_noapp))
        R_t = scenario.epsilon_app(t) * R_t_app + (1 - scenario.epsilon_app(t)) * R_t_noapp
        FT_t_app_infty = sum(
            p_g * tauT_t_g_app.total_mass
            for (p_g, tauT_t_g_app, tauT_t_g_noapp) in zip(
                scenario.p_gs, tauT_t_gs_app, tauT_t_gs_noapp
            )
        )
        FT_t_noapp_infty = sum(
            p_g * tauT_t_g_noapp.total_mass
            for (p_g, tauT_t_g_app, tauT_t_g_noapp) in zip(
                scenario.p_gs, tauT_t_gs_app, tauT_t_gs_noapp
            )
        )
        FT_t_infty = (
            scenario.epsilon_app(t) * FT_t_app_infty
            + (1 - scenario.epsilon_app(t)) * FT_t_noapp_infty
        )

        t_in_days_list.append(t_in_days)
        tausigma_app.append(tausigmagsapp_t)
        tausigma_noapp.append(tausigmagsnoapp_t)
        Fsigmaapp_infty.append(Fsigmaapp_t_infty)
        nu.append(nu_t)
        nu_app.append(nuapp_t)
        nu_noapp.append(nunoapp_t)
        nu0.append(nu0_t)
        b_app.append(b_t_gs_app)
        b_noapp.append(b_t_gs_noapp)
        R_app.append(R_t_app)
        R_noapp.append(R_t_noapp)
        R.append(R_t)
        tauT_app.append(tauT_t_gs_app)
        tauT_noapp.append(tauT_t_gs_noapp)
        FT_infty.append(FT_t_infty)
        FT_app_infty.append(FT_t_app_infty)
        FT_noapp_infty.append(FT_t_noapp_infty)

        if verbose and t % UNITS_IN_ONE_DAY == 0:
            EtauC_t_gs_app_in_days = [
                b_t_g.normalize().mean() * UNITS_IN_ONE_DAY for b_t_g in b_t_gs_app
            ]
            EtauC_t_gs_noapp_in_days = [
                b_t_g.normalize().mean() * UNITS_IN_ONE_DAY for b_t_g in b_t_gs_noapp
            ]

            print(
                f"""t = {t_in_days} days
                    nugsapp_t = {tuple(nugsapp_t)},   nugsnoapp_t = {tuple(nugsnoapp_t)},   nu_t = {int(round(nu_t, 0))}
                    nu0_t = {int(round(nu0_t, 0))}
                    R_t_gs_app = {R_t_gs_app},    R_t_app = {R_t_app},    
                    R_t_gs_noapp = {R_t_gs_noapp},    R_t_noapp = {R_t_noapp},        
                    R_t = {round(R_t, 2)}
                    EtauC_t_gs_app = {tuple(EtauC_t_gs_app_in_days)} days
                    EtauC_t_gs_noapp = {tuple(EtauC_t_gs_noapp_in_days)} days
                    Fsigmagsapp_t(∞) = {tuple(tausigmag_t.total_mass for tausigmag_t in tausigmagsapp_t)}
                    Fsigmagsnoapp_t(∞) = {tuple(tausigmag_t.total_mass for tausigmag_t in tausigmagsnoapp_t)}
                    Fsigmaapp_t(∞) = {Fsigmaapp_t_infty}
                    FAs_t_gs_app(∞) = {tuple(tauAs_t_g_app.total_mass for tauAs_t_g_app in tauAs_t_gs_app)}
                    FAs_t_gs_noapp(∞) = {tuple(tauAs_t_g_noapp.total_mass for tauAs_t_g_noapp in tauAs_t_gs_noapp)}
                    FAc_t_app(∞) = {tauAc_t_app.total_mass},    FAc_t_noapp(∞) = {tauAc_t_noapp.total_mass}
                    FT_t_gs_app(∞) = {tuple(tauT_t_g.total_mass for tauT_t_g in tauT_t_gs_app)},
                    FT_t_app(∞) = {FT_t_app_infty},
                    FT_t_gs_noapp(∞) = {tuple(tauT_t_g.total_mass for tauT_t_g in tauT_t_gs_noapp)},
                    FT_t_noapp(∞) = {FT_t_noapp_infty}
                    FT_t(∞) = {round(FT_t_infty, 2)}
                    """
            )

        if (
            threshold_to_stop is not None
            and t > 10
            and (
                abs((R[-2] - R[-1]) / R[-2]) < threshold_to_stop
                and FT_infty[-2] != 0
                and abs((FT_infty[-2] - FT_infty[-1]) / FT_infty[-2]) < threshold_to_stop
            )
        ):
            break

    return (
        t_in_days_list,
        nu,
        nu0,
        Fsigmaapp_infty,
        R,
        R_app,
        R_noapp,
        FT_infty,
        FT_app_infty,
        FT_noapp_infty,
    )