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]
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", "β"])
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, )
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
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())
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
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), )
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
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, )