From 4af2a1fdd7fe85a6c787a82f5df06ef32179fa13 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 15 May 2023 15:53:50 +0200 Subject: [PATCH 01/10] dynamic exposed for incidence rate --- caimira/models.py | 70 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 9ef7eece..ed0767f3 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1494,9 +1494,17 @@ def __post_init__(self): c_model.ventilation.air_exchange(c_model.room, time)) for time in c_model.state_change_times()))): raise ValueError("If the diameter is an array, none of the ventilation parameters " "or virus decay constant can be arrays at the same time.") - if not isinstance(self.exposed.number, int): - raise NotImplementedError("Cannot use dynamic occupancy for" - " the exposed population") + + @method_cache + def population_state_change_times(self) -> typing.List[float]: + """ + All time dependent population entities on this model must provide information + about the times at which their state changes. + """ + state_change_times = set(self.concentration_model.infected.presence_interval().transition_times()) + state_change_times.update(self.exposed.presence_interval().transition_times()) + + return sorted(state_change_times) def long_range_fraction_deposited(self) -> _VectorisedFloat: """ @@ -1681,6 +1689,59 @@ def total_probability_rule(self) -> _VectorisedFloat: else: return 0 + + def dynamic_total_probability_rule(self): + if (self.geographical_data.geographic_population != 0 and self.geographical_data.geographic_cases != 0): + total_probability_rule = [] + + state_change_times = self.population_state_change_times() + for interval in zip(state_change_times[:-1], state_change_times[1:]): + exposed_present = self.exposed.people_present(interval[1]) + infected_present = self.concentration_model.infected.people_present(interval[1]) + + # Create an equivalent exposure model but changing the number of infected cases. + total_people = exposed_present + infected_present + max_num_infected = (total_people if total_people < 10 else 10) + # The influence of a higher number of simultainious infected people (> 4 - 5) yields an almost negligible contirbution to the total probability. + # To be on the safe side, a hard coded limit with a safety margin of 2x was set. + # Therefore we decided a hard limit of 10 infected people. + for num_infected in range(1, max_num_infected + 1): + exposure_model = nested_replace( + self, { + 'concentration_model.infected': InfectedPopulation( + number=num_infected, + presence=SpecificInterval(present_times = (interval, )), + mask=self.concentration_model.infected.mask, + activity=self.concentration_model.infected.activity, + host_immunity=self.concentration_model.infected.host_immunity, + virus=self.concentration_model.infected.virus, + expiration=self.concentration_model.infected.expiration, + ), + 'exposed': Population( + number=exposed_present, + presence=SpecificInterval(present_times=(interval,)), + mask=self.exposed.mask, + activity=self.exposed.activity, + host_immunity=self.exposed.host_immunity, + ), + } + ) + + prob_ind = exposure_model.infection_probability().mean() / 100 + n = total_people - num_infected + # By means of the total probability rule + prob_at_least_one_infected = 1 - (1 - prob_ind)**n + total_probability_rule.append(prob_at_least_one_infected * + self.geographical_data.probability_meet_infected_person( + self.concentration_model.infected.virus, + num_infected, total_people)) + + if (isinstance(self.exposed.number, IntPiecewiseConstant) or + isinstance(self.concentration_model.infected.number, IntPiecewiseConstant)): + return (1 - np.prod([(1 - prob) for prob in total_probability_rule])) * 100 + else: + return 0 + def expected_new_cases(self) -> _VectorisedFloat: # Create an equivalent exposure model without short-range interactions, if any. if (len(self.short_range) == 0): @@ -1689,8 +1750,7 @@ def expected_new_cases(self) -> _VectorisedFloat: else: prob = self.infection_probability() - exposed_occupants = self.exposed.number - return prob * exposed_occupants / 100 + return prob * self.exposed.number / 100 def reproduction_number(self) -> _VectorisedFloat: """ From 27328d960c22cb393b38bad55fc3a992f5e0e012 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 15 May 2023 15:53:57 +0200 Subject: [PATCH 02/10] tests for incidence rate --- .../tests/models/test_dynamic_population.py | 69 +++++++++++++++---- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/caimira/tests/models/test_dynamic_population.py b/caimira/tests/models/test_dynamic_population.py index e5bf9805..2bf4a8ac 100644 --- a/caimira/tests/models/test_dynamic_population.py +++ b/caimira/tests/models/test_dynamic_population.py @@ -27,12 +27,16 @@ def full_exposure_model(): short_range=(), exposed=models.Population( number=10, - presence=models.SpecificInterval(((8, 12), (13, 17), )), + presence=models.SpecificInterval(((8, 12), (13, 17), )), mask=models.Mask.types['No mask'], activity=models.Activity.types['Seated'], host_immunity=0. ), - geographical_data=(), + geographical_data=models.Cases( + geographic_population=50_000, + geographic_cases=52, + ascertainment_bias=1, + ), ) @@ -51,11 +55,37 @@ def baseline_infected_population_number(): @pytest.fixture -def dynamic_single_exposure_model(full_exposure_model, baseline_infected_population_number): +def baseline_exposed_population_number(): + return models.Population( + number=models.IntPiecewiseConstant( + (8, 12, 13, 17), (10, 0, 10)), + presence=None, + mask=models.Mask.types['No mask'], + activity=models.Activity.types['Seated'], + host_immunity=0., + ) + + +@pytest.fixture +def dynamic_infected_single_exposure_model(full_exposure_model, baseline_infected_population_number): return dc_utils.nested_replace(full_exposure_model, {'concentration_model.infected': baseline_infected_population_number, }) +@pytest.fixture +def dynamic_exposed_single_exposure_model(full_exposure_model, baseline_exposed_population_number): + return dc_utils.nested_replace(full_exposure_model, + {'exposed': baseline_exposed_population_number, }) + + +@pytest.fixture +def dynamic_population_exposure_model(full_exposure_model, baseline_infected_population_number ,baseline_exposed_population_number): + return dc_utils.nested_replace(full_exposure_model, { + 'concentration_model.infected': baseline_infected_population_number, + 'exposed': baseline_exposed_population_number, + }) + + @pytest.mark.parametrize( "time", [4., 8., 10., 12., 13., 14., 16., 20., 24.], @@ -91,16 +121,16 @@ def test_population_number(full_exposure_model: models.ExposureModel, [4., 8., 10., 12., 13., 14., 16., 20., 24.], ) def test_concentration_model_dynamic_population(full_exposure_model: models.ExposureModel, - dynamic_single_exposure_model: models.ExposureModel, + dynamic_infected_single_exposure_model: models.ExposureModel, time: float): - assert full_exposure_model.concentration(time) == dynamic_single_exposure_model.concentration(time) + assert full_exposure_model.concentration(time) == dynamic_infected_single_exposure_model.concentration(time) @pytest.mark.parametrize("number_of_infected",[1, 2, 3, 4, 5]) @pytest.mark.parametrize("time",[9., 12.5, 16.]) def test_linearity_with_number_of_infected(full_exposure_model: models.ExposureModel, - dynamic_single_exposure_model: models.ExposureModel, + dynamic_infected_single_exposure_model: models.ExposureModel, time: float, number_of_infected: int): @@ -112,8 +142,8 @@ def test_linearity_with_number_of_infected(full_exposure_model: models.ExposureM } ) - npt.assert_almost_equal(static_multiple_exposure_model.concentration(time), dynamic_single_exposure_model.concentration(time) * number_of_infected) - npt.assert_almost_equal(static_multiple_exposure_model.deposited_exposure(), dynamic_single_exposure_model.deposited_exposure() * number_of_infected) + npt.assert_almost_equal(static_multiple_exposure_model.concentration(time), dynamic_infected_single_exposure_model.concentration(time) * number_of_infected) + npt.assert_almost_equal(static_multiple_exposure_model.deposited_exposure(), dynamic_infected_single_exposure_model.deposited_exposure() * number_of_infected) @pytest.mark.parametrize( @@ -171,10 +201,19 @@ def test_dynamic_dose(full_exposure_model, time): npt.assert_almost_equal(dynamic_exposure, np.sum(static_exposure)) -def test_dynamic_total_probability_rule(dynamic_single_exposure_model: models.ExposureModel): - with pytest.raises( - NotImplementedError, - match=re.escape("Cannot compute total probability " - "(including incidence rate) with dynamic occupancy") - ): - dynamic_single_exposure_model.total_probability_rule() +def test_dynamic_total_probability_rule( + full_exposure_model: models.ExposureModel, + dynamic_infected_single_exposure_model: models.ExposureModel, + dynamic_exposed_single_exposure_model: models.ExposureModel, + dynamic_population_exposure_model: models.ExposureModel): + + full_model_total_prob_rule = full_exposure_model.total_probability_rule() + npt.assert_almost_equal(full_model_total_prob_rule, + dynamic_population_exposure_model.dynamic_total_probability_rule()) + + npt.assert_almost_equal(full_model_total_prob_rule, + dynamic_infected_single_exposure_model.dynamic_total_probability_rule()) + + npt.assert_almost_equal(full_model_total_prob_rule, + dynamic_exposed_single_exposure_model.dynamic_total_probability_rule()) + \ No newline at end of file From 6819b98ffd657dda38f43081ddd6a9f1f8f76d2f Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 17 May 2023 14:55:43 +0200 Subject: [PATCH 03/10] updated dynamic total_prob_rule method --- caimira/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/caimira/models.py b/caimira/models.py index ed0767f3..c2b94ac8 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1696,6 +1696,7 @@ def dynamic_total_probability_rule(self): state_change_times = self.population_state_change_times() for interval in zip(state_change_times[:-1], state_change_times[1:]): + sum_probability = 0.0 exposed_present = self.exposed.people_present(interval[1]) infected_present = self.concentration_model.infected.people_present(interval[1]) @@ -1731,10 +1732,11 @@ def dynamic_total_probability_rule(self): n = total_people - num_infected # By means of the total probability rule prob_at_least_one_infected = 1 - (1 - prob_ind)**n - total_probability_rule.append(prob_at_least_one_infected * + sum_probability += (prob_at_least_one_infected * self.geographical_data.probability_meet_infected_person( self.concentration_model.infected.virus, num_infected, total_people)) + total_probability_rule.append(sum_probability) if (isinstance(self.exposed.number, IntPiecewiseConstant) or isinstance(self.concentration_model.infected.number, IntPiecewiseConstant)): From c644cef278189aa4cbe520eaf608e58aafb336aa Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Fri, 26 May 2023 16:34:05 +0200 Subject: [PATCH 04/10] added probability of infection based on dynamic occupants --- caimira/models.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index c2b94ac8..23c7babf 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1637,29 +1637,44 @@ def deposited_exposure_between_bounds(self, time1: float, time2: float) -> _Vect deposited_exposure += self.long_range_deposited_exposure_between_bounds(time1, time2) return deposited_exposure - + def deposited_exposure(self) -> _VectorisedFloat: """ The number of virus per m^3 deposited on the respiratory tract. """ deposited_exposure: _VectorisedFloat = 0.0 for start, stop in self.exposed.presence_interval().boundaries(): - deposited_exposure += self.deposited_exposure_between_bounds(start, stop) - + deposited_exposure += (self.deposited_exposure_between_bounds(start, stop)) + return deposited_exposure * self.repeats + + def deposited_exposure_list(self): + """ + The number of virus per m^3 deposited on the respiratory tract. + """ + population_change_times = self.population_state_change_times() - @method_cache - def infection_probability(self) -> _VectorisedFloat: + deposited_exposure = [] + for start, stop in zip(population_change_times[:-1], population_change_times[1:]): + deposited_exposure.append(self.deposited_exposure_between_bounds(start, stop)) + + return deposited_exposure * self.repeats + + def infection_probability_list(self): # Viral dose (vD) - vD = self.deposited_exposure() + vD_list = self.deposited_exposure_list() # oneoverln2 multiplied by ID_50 corresponds to ID_63. infectious_dose = oneoverln2 * self.concentration_model.virus.infectious_dose - # Probability of infection. - return (1 - np.exp(-((vD * (1 - self.exposed.host_immunity))/(infectious_dose * - self.concentration_model.virus.transmissibility_factor)))) * 100 - + # Probability of infection. + return [(1 - np.exp(-((vD * (1 - self.exposed.host_immunity))/(infectious_dose * + self.concentration_model.virus.transmissibility_factor)))) for vD in vD_list] + + @method_cache + def infection_probability(self) -> _VectorisedFloat: + return (1 - np.prod([1 - prob for prob in self.infection_probability_list()], axis = 0)) * 100 + def total_probability_rule(self) -> _VectorisedFloat: if (isinstance(self.concentration_model.infected.number, IntPiecewiseConstant) or isinstance(self.exposed.number, IntPiecewiseConstant)): From e4ac733348d8f047e0badca076ec03727b010231 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Fri, 26 May 2023 16:34:20 +0200 Subject: [PATCH 05/10] adapted dynamic total probability rule --- caimira/models.py | 48 ++++++++++++++--------------------------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 23c7babf..7ae8d930 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1704,16 +1704,15 @@ def total_probability_rule(self) -> _VectorisedFloat: else: return 0 - - def dynamic_total_probability_rule(self): + def dynamic_total_probability_rule(self) -> _VectorisedFloat: if (self.geographical_data.geographic_population != 0 and self.geographical_data.geographic_cases != 0): - total_probability_rule = [] - - state_change_times = self.population_state_change_times() - for interval in zip(state_change_times[:-1], state_change_times[1:]): + total_probability_rule_list = [] + population_change_times = self.population_state_change_times() + for start, stop in zip(population_change_times[:-1], population_change_times[1:]): + sum_probability = 0.0 - exposed_present = self.exposed.people_present(interval[1]) - infected_present = self.concentration_model.infected.people_present(interval[1]) + exposed_present = self.exposed.people_present(stop) + infected_present = self.concentration_model.infected.people_present(stop) # Create an equivalent exposure model but changing the number of infected cases. total_people = exposed_present + infected_present @@ -1723,41 +1722,22 @@ def dynamic_total_probability_rule(self): # Therefore we decided a hard limit of 10 infected people. for num_infected in range(1, max_num_infected + 1): exposure_model = nested_replace( - self, { - 'concentration_model.infected': InfectedPopulation( - number=num_infected, - presence=SpecificInterval(present_times = (interval, )), - mask=self.concentration_model.infected.mask, - activity=self.concentration_model.infected.activity, - host_immunity=self.concentration_model.infected.host_immunity, - virus=self.concentration_model.infected.virus, - expiration=self.concentration_model.infected.expiration, - ), - 'exposed': Population( - number=exposed_present, - presence=SpecificInterval(present_times=(interval,)), - mask=self.exposed.mask, - activity=self.exposed.activity, - host_immunity=self.exposed.host_immunity, - ), + self, {'concentration_model.infected.number': + IntPiecewiseConstant((start, stop), (num_infected,)), } ) - prob_ind = exposure_model.infection_probability().mean() / 100 + n = total_people - num_infected # By means of the total probability rule prob_at_least_one_infected = 1 - (1 - prob_ind)**n sum_probability += (prob_at_least_one_infected * - self.geographical_data.probability_meet_infected_person( - self.concentration_model.infected.virus, - num_infected, total_people)) - total_probability_rule.append(sum_probability) - - if (isinstance(self.exposed.number, IntPiecewiseConstant) or - isinstance(self.concentration_model.infected.number, IntPiecewiseConstant)): - return (1 - np.prod([(1 - prob) for prob in total_probability_rule])) * 100 + self.geographical_data.probability_meet_infected_person(self.concentration_model.infected.virus, num_infected, total_people)) + total_probability_rule_list.append(sum_probability) + return (1 - np.prod([(1 - prob) for prob in total_probability_rule_list], axis = 0)) * 100 else: return 0 + def expected_new_cases(self) -> _VectorisedFloat: # Create an equivalent exposure model without short-range interactions, if any. From 37b1d17bb8e456ee080ab18fdc6aad078967a3a4 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Tue, 30 May 2023 16:54:10 +0200 Subject: [PATCH 06/10] updated deposited_exposure method and infection probability tests --- caimira/models.py | 23 ++++++-------- .../tests/models/test_dynamic_population.py | 30 +++++++++++++------ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 7ae8d930..e0c2a50b 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1638,17 +1638,7 @@ def deposited_exposure_between_bounds(self, time1: float, time2: float) -> _Vect return deposited_exposure - def deposited_exposure(self) -> _VectorisedFloat: - """ - The number of virus per m^3 deposited on the respiratory tract. - """ - deposited_exposure: _VectorisedFloat = 0.0 - for start, stop in self.exposed.presence_interval().boundaries(): - deposited_exposure += (self.deposited_exposure_between_bounds(start, stop)) - - return deposited_exposure * self.repeats - - def deposited_exposure_list(self): + def _deposited_exposure_list(self): """ The number of virus per m^3 deposited on the respiratory tract. """ @@ -1658,11 +1648,17 @@ def deposited_exposure_list(self): for start, stop in zip(population_change_times[:-1], population_change_times[1:]): deposited_exposure.append(self.deposited_exposure_between_bounds(start, stop)) - return deposited_exposure * self.repeats + return deposited_exposure + + def deposited_exposure(self) -> _VectorisedFloat: + """ + The number of virus per m^3 deposited on the respiratory tract. + """ + return np.sum(self._deposited_exposure_list(), axis=0) * self.repeats def infection_probability_list(self): # Viral dose (vD) - vD_list = self.deposited_exposure_list() + vD_list = self._deposited_exposure_list() # oneoverln2 multiplied by ID_50 corresponds to ID_63. infectious_dose = oneoverln2 * self.concentration_model.virus.infectious_dose @@ -1709,7 +1705,6 @@ def dynamic_total_probability_rule(self) -> _VectorisedFloat: total_probability_rule_list = [] population_change_times = self.population_state_change_times() for start, stop in zip(population_change_times[:-1], population_change_times[1:]): - sum_probability = 0.0 exposed_present = self.exposed.people_present(stop) infected_present = self.concentration_model.infected.people_present(stop) diff --git a/caimira/tests/models/test_dynamic_population.py b/caimira/tests/models/test_dynamic_population.py index 2bf4a8ac..3a03bebb 100644 --- a/caimira/tests/models/test_dynamic_population.py +++ b/caimira/tests/models/test_dynamic_population.py @@ -149,7 +149,7 @@ def test_linearity_with_number_of_infected(full_exposure_model: models.ExposureM @pytest.mark.parametrize( "time", (8., 9., 10., 11., 12., 13., 14.), ) -def test_dynamic_dose(full_exposure_model, time): +def test_dynamic_dose(full_exposure_model: models.ExposureModel, time: float): dynamic_infected: models.ExposureModel = dc_utils.nested_replace( full_exposure_model, @@ -201,19 +201,31 @@ def test_dynamic_dose(full_exposure_model, time): npt.assert_almost_equal(dynamic_exposure, np.sum(static_exposure)) -def test_dynamic_total_probability_rule( +def test_infection_probability( full_exposure_model: models.ExposureModel, dynamic_infected_single_exposure_model: models.ExposureModel, dynamic_exposed_single_exposure_model: models.ExposureModel, dynamic_population_exposure_model: models.ExposureModel): + + base_infection_probability = full_exposure_model.infection_probability() + npt.assert_almost_equal(base_infection_probability, dynamic_infected_single_exposure_model.infection_probability()) + npt.assert_almost_equal(base_infection_probability, dynamic_exposed_single_exposure_model.infection_probability()) + npt.assert_almost_equal(base_infection_probability, dynamic_population_exposure_model.infection_probability()) + + +# def test_dynamic_total_probability_rule( +# full_exposure_model: models.ExposureModel, +# dynamic_infected_single_exposure_model: models.ExposureModel, +# dynamic_exposed_single_exposure_model: models.ExposureModel, +# dynamic_population_exposure_model: models.ExposureModel): - full_model_total_prob_rule = full_exposure_model.total_probability_rule() - npt.assert_almost_equal(full_model_total_prob_rule, - dynamic_population_exposure_model.dynamic_total_probability_rule()) +# full_model_total_prob_rule = full_exposure_model.total_probability_rule() +# npt.assert_almost_equal(full_model_total_prob_rule, +# dynamic_population_exposure_model.dynamic_total_probability_rule()) - npt.assert_almost_equal(full_model_total_prob_rule, - dynamic_infected_single_exposure_model.dynamic_total_probability_rule()) +# npt.assert_almost_equal(full_model_total_prob_rule, +# dynamic_infected_single_exposure_model.dynamic_total_probability_rule()) - npt.assert_almost_equal(full_model_total_prob_rule, - dynamic_exposed_single_exposure_model.dynamic_total_probability_rule()) +# npt.assert_almost_equal(full_model_total_prob_rule, +# dynamic_exposed_single_exposure_model.dynamic_total_probability_rule()) \ No newline at end of file From 9bfb7f31995a5dda8d1cf47918787d7a69572b65 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 5 Jun 2023 09:46:51 +0200 Subject: [PATCH 07/10] turned infection probability method private --- caimira/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index e0c2a50b..f8ea29e1 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1656,7 +1656,7 @@ def deposited_exposure(self) -> _VectorisedFloat: """ return np.sum(self._deposited_exposure_list(), axis=0) * self.repeats - def infection_probability_list(self): + def _infection_probability_list(self): # Viral dose (vD) vD_list = self._deposited_exposure_list() @@ -1669,7 +1669,7 @@ def infection_probability_list(self): @method_cache def infection_probability(self) -> _VectorisedFloat: - return (1 - np.prod([1 - prob for prob in self.infection_probability_list()], axis = 0)) * 100 + return (1 - np.prod([1 - prob for prob in self._infection_probability_list()], axis = 0)) * 100 def total_probability_rule(self) -> _VectorisedFloat: if (isinstance(self.concentration_model.infected.number, IntPiecewiseConstant) or From 832e902fdefb95a766882c7429a38ae562c219af Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 8 Jun 2023 16:46:00 +0200 Subject: [PATCH 08/10] dose list implementation --- caimira/models.py | 46 +++++------------- .../tests/models/test_dynamic_population.py | 48 ++++++++++++------- 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index f8ea29e1..6df9740d 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1700,41 +1700,11 @@ def total_probability_rule(self) -> _VectorisedFloat: else: return 0 - def dynamic_total_probability_rule(self) -> _VectorisedFloat: - if (self.geographical_data.geographic_population != 0 and self.geographical_data.geographic_cases != 0): - total_probability_rule_list = [] - population_change_times = self.population_state_change_times() - for start, stop in zip(population_change_times[:-1], population_change_times[1:]): - sum_probability = 0.0 - exposed_present = self.exposed.people_present(stop) - infected_present = self.concentration_model.infected.people_present(stop) - - # Create an equivalent exposure model but changing the number of infected cases. - total_people = exposed_present + infected_present - max_num_infected = (total_people if total_people < 10 else 10) - # The influence of a higher number of simultainious infected people (> 4 - 5) yields an almost negligible contirbution to the total probability. - # To be on the safe side, a hard coded limit with a safety margin of 2x was set. - # Therefore we decided a hard limit of 10 infected people. - for num_infected in range(1, max_num_infected + 1): - exposure_model = nested_replace( - self, {'concentration_model.infected.number': - IntPiecewiseConstant((start, stop), (num_infected,)), - } - ) - prob_ind = exposure_model.infection_probability().mean() / 100 - - n = total_people - num_infected - # By means of the total probability rule - prob_at_least_one_infected = 1 - (1 - prob_ind)**n - sum_probability += (prob_at_least_one_infected * - self.geographical_data.probability_meet_infected_person(self.concentration_model.infected.virus, num_infected, total_people)) - total_probability_rule_list.append(sum_probability) - return (1 - np.prod([(1 - prob) for prob in total_probability_rule_list], axis = 0)) * 100 - else: - return 0 - - def expected_new_cases(self) -> _VectorisedFloat: + if (isinstance(self.concentration_model.infected.number, IntPiecewiseConstant) or + isinstance(self.exposed.number, IntPiecewiseConstant)): + raise NotImplementedError("Cannot compute expected new cases " + "with dynamic occupancy") # Create an equivalent exposure model without short-range interactions, if any. if (len(self.short_range) == 0): exposure_model = nested_replace(self, {'short_range': ()}) @@ -1750,13 +1720,19 @@ def reproduction_number(self) -> _VectorisedFloat: cases directly generated by one infected case in a population. """ + if (isinstance(self.concentration_model.infected.number, IntPiecewiseConstant) or + isinstance(self.exposed.number, IntPiecewiseConstant)): + raise NotImplementedError("Cannot compute reproduction number " + "with dynamic occupancy") + if self.concentration_model.infected.number == 1: return self.expected_new_cases() # Create an equivalent exposure model but with precisely # one infected case. single_exposure_model = nested_replace( - self, {'concentration_model.infected.number': 1} + self, { + 'concentration_model.infected.number': 1} ) return single_exposure_model.expected_new_cases() diff --git a/caimira/tests/models/test_dynamic_population.py b/caimira/tests/models/test_dynamic_population.py index 3a03bebb..abc8272c 100644 --- a/caimira/tests/models/test_dynamic_population.py +++ b/caimira/tests/models/test_dynamic_population.py @@ -32,11 +32,7 @@ def full_exposure_model(): activity=models.Activity.types['Seated'], host_immunity=0. ), - geographical_data=models.Cases( - geographic_population=50_000, - geographic_cases=52, - ascertainment_bias=1, - ), + geographical_data=(), ) @@ -213,19 +209,35 @@ def test_infection_probability( npt.assert_almost_equal(base_infection_probability, dynamic_population_exposure_model.infection_probability()) -# def test_dynamic_total_probability_rule( -# full_exposure_model: models.ExposureModel, -# dynamic_infected_single_exposure_model: models.ExposureModel, -# dynamic_exposed_single_exposure_model: models.ExposureModel, -# dynamic_population_exposure_model: models.ExposureModel): +def test_dynamic_total_probability_rule( + dynamic_infected_single_exposure_model: models.ExposureModel, + dynamic_exposed_single_exposure_model: models.ExposureModel, + dynamic_population_exposure_model: models.ExposureModel): -# full_model_total_prob_rule = full_exposure_model.total_probability_rule() -# npt.assert_almost_equal(full_model_total_prob_rule, -# dynamic_population_exposure_model.dynamic_total_probability_rule()) + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute total probability " + "(including incidence rate) with dynamic occupancy")): + dynamic_infected_single_exposure_model.total_probability_rule() + dynamic_exposed_single_exposure_model.total_probability_rule() + dynamic_population_exposure_model.total_probability_rule() + +def test_dynamic_expected_new_cases( + dynamic_infected_single_exposure_model: models.ExposureModel, + dynamic_exposed_single_exposure_model: models.ExposureModel, + dynamic_population_exposure_model: models.ExposureModel): -# npt.assert_almost_equal(full_model_total_prob_rule, -# dynamic_infected_single_exposure_model.dynamic_total_probability_rule()) + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute expected new cases " + "with dynamic occupancy")): + dynamic_infected_single_exposure_model.expected_new_cases() + dynamic_exposed_single_exposure_model.expected_new_cases() + dynamic_population_exposure_model.expected_new_cases() + +def test_dynamic_reproduction_number( + dynamic_infected_single_exposure_model: models.ExposureModel, + dynamic_exposed_single_exposure_model: models.ExposureModel, + dynamic_population_exposure_model: models.ExposureModel): -# npt.assert_almost_equal(full_model_total_prob_rule, -# dynamic_exposed_single_exposure_model.dynamic_total_probability_rule()) - \ No newline at end of file + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute reproduction number " + "with dynamic occupancy")): + dynamic_infected_single_exposure_model.reproduction_number() + dynamic_exposed_single_exposure_model.reproduction_number() + dynamic_population_exposure_model.reproduction_number() From c7ecf17c4bf59a66a8d07a1922c36289084785d2 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 29 Jun 2023 10:01:59 +0100 Subject: [PATCH 09/10] removed unused block of code --- caimira/models.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 6df9740d..4a80307e 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1705,14 +1705,8 @@ def expected_new_cases(self) -> _VectorisedFloat: isinstance(self.exposed.number, IntPiecewiseConstant)): raise NotImplementedError("Cannot compute expected new cases " "with dynamic occupancy") - # Create an equivalent exposure model without short-range interactions, if any. - if (len(self.short_range) == 0): - exposure_model = nested_replace(self, {'short_range': ()}) - prob = exposure_model.infection_probability() - else: - prob = self.infection_probability() - return prob * self.exposed.number / 100 + return self.infection_probability() * self.exposed.number / 100 def reproduction_number(self) -> _VectorisedFloat: """ From 6cbcbfad59ff33aeeb99f188439bbb8d360fc639 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 29 Jun 2023 10:02:26 +0100 Subject: [PATCH 10/10] expanded pytest raises on tests to each single case --- caimira/tests/models/test_dynamic_population.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/caimira/tests/models/test_dynamic_population.py b/caimira/tests/models/test_dynamic_population.py index abc8272c..8fe6b891 100644 --- a/caimira/tests/models/test_dynamic_population.py +++ b/caimira/tests/models/test_dynamic_population.py @@ -217,7 +217,11 @@ def test_dynamic_total_probability_rule( with pytest.raises(NotImplementedError, match=re.escape("Cannot compute total probability " "(including incidence rate) with dynamic occupancy")): dynamic_infected_single_exposure_model.total_probability_rule() + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute total probability " + "(including incidence rate) with dynamic occupancy")): dynamic_exposed_single_exposure_model.total_probability_rule() + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute total probability " + "(including incidence rate) with dynamic occupancy")): dynamic_population_exposure_model.total_probability_rule() def test_dynamic_expected_new_cases( @@ -228,7 +232,11 @@ def test_dynamic_expected_new_cases( with pytest.raises(NotImplementedError, match=re.escape("Cannot compute expected new cases " "with dynamic occupancy")): dynamic_infected_single_exposure_model.expected_new_cases() + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute expected new cases " + "with dynamic occupancy")): dynamic_exposed_single_exposure_model.expected_new_cases() + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute expected new cases " + "with dynamic occupancy")): dynamic_population_exposure_model.expected_new_cases() def test_dynamic_reproduction_number( @@ -239,5 +247,9 @@ def test_dynamic_reproduction_number( with pytest.raises(NotImplementedError, match=re.escape("Cannot compute reproduction number " "with dynamic occupancy")): dynamic_infected_single_exposure_model.reproduction_number() + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute reproduction number " + "with dynamic occupancy")): dynamic_exposed_single_exposure_model.reproduction_number() + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute reproduction number " + "with dynamic occupancy")): dynamic_population_exposure_model.reproduction_number()