From f7bce0ef2414197afb3b27368a4bf69e8933dab2 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 1 Aug 2022 09:42:44 +0200 Subject: [PATCH 1/6] added verification for diameter independent parameters --- caimira/models.py | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 4a96be97..738ffa6a 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1310,10 +1310,6 @@ class ShortRangeModel: class ExposureModel: """ Represents the exposure to a concentration of virions in the air. - NOTE: the infection probability formula assumes that if the diameter - is an array, then none of the ventilation parameters, room volume or virus - decay constant, are arrays as well. - TODO: implement a check this is the case, in __post_init__ """ #: The virus concentration model which this exposure model should consider. concentration_model: ConcentrationModel @@ -1330,6 +1326,41 @@ class ExposureModel: #: The number of times the exposure event is repeated (default 1). repeats: int = 1 + def __post_init__(self): + """ + The infection probability formula assumes that if the diameter + is an array, then none of the ventilation parameters, room volume or virus + decay constant, are arrays as well. + """ + infected_population = self.concentration_model.infected + if (isinstance(infected_population, InfectedPopulation) + and not np.isscalar(infected_population.expiration.particle.diameter)): + # Due to the infiltration ventilation (0.25ACH), the ventilation is initialized as MultipleVentilation. + if isinstance(self.concentration_model.ventilation, MultipleVentilation): + for vent in self.concentration_model.ventilation.ventilations: + # Check if any of the ventilation parameters is an array instance. + # Note that most of the ventilation parameters are part of the WindowOpening class (inheritance). + if (isinstance(vent, WindowOpening) and ( + not all(np.isscalar(value) for value in vent.outside_temp.values) or + not np.isscalar(vent.window_height) or + (isinstance(vent, HingedWindow) and not np.isscalar(vent.window_width)))): + raise ValueError("Ventilation parameter(s) and diameter cannot be arrays at the same time.") + # The window_width parameter is only part of the HingedWindow class. + if ((isinstance(vent, HingedWindow)) and not np.isscalar(vent.window_width)): + raise ValueError("Ventilation parameter(s) and diameter cannot be arrays at the same time.") + # The q_air_mech parameter is only part of the HEPAFilter class, and + # the air_exch parameter is only part of the AirChange class. + if (isinstance(vent, HEPAFilter) and not np.isscalar(vent.q_air_mech) or + isinstance(vent, AirChange) and not np.isscalar(vent.air_exch)): + raise ValueError("Ventilation rate and diameter cannot be arrays at the same time.") + # Check if the room volume is an array instance. + if not np.isscalar(self.concentration_model.room.volume): + raise ValueError("Room volume and diameter cannot be arrays at the same time.") + # Virus decay constant depends on the room humidity and inside_temp parameters. + if (not all(np.isscalar(value) for value in self.concentration_model.room.inside_temp.values) or + not np.isscalar(self.concentration_model.room.humidity)): + raise ValueError("Virus decay constant and diameter cannot be arrays at the same time.") + def long_range_fraction_deposited(self) -> _VectorisedFloat: """ The fraction of particles actually deposited in the respiratory From 3314d0146c796d008eee807ef59f29eee04d0430 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 1 Aug 2022 11:55:59 +0200 Subject: [PATCH 2/6] Updated the ExposureModule conditions on post_init --- caimira/models.py | 53 +++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 738ffa6a..82e7a852 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1332,34 +1332,37 @@ class ExposureModel: is an array, then none of the ventilation parameters, room volume or virus decay constant, are arrays as well. """ + def verify_ventilation(vent: Ventilation): + # Check if any of the ventilation parameters is an array instance. + # Note that most of the ventilation parameters are part of the WindowOpening class (inheritance). + if (isinstance(vent, WindowOpening) and ( + not all(np.isscalar(value) for value in vent.outside_temp.values) or + not np.isscalar(vent.window_height) or + not np.isscalar(vent.opening_length) or + (isinstance(vent, HingedWindow) and not np.isscalar(vent.window_width)))): + raise ValueError("Ventilation parameters and diameter cannot be arrays at the same time.") + # The q_air_mech parameter is only part of the HEPAFilter class, and + # the air_exch parameter is only part of the AirChange class. + if (isinstance(vent, HEPAFilter) and not np.isscalar(vent.q_air_mech) or + isinstance(vent, AirChange) and not np.isscalar(vent.air_exch)): + raise ValueError("Ventilation rate and diameter cannot be arrays at the same time.") + infected_population = self.concentration_model.infected - if (isinstance(infected_population, InfectedPopulation) - and not np.isscalar(infected_population.expiration.particle.diameter)): - # Due to the infiltration ventilation (0.25ACH), the ventilation is initialized as MultipleVentilation. + if (isinstance(infected_population, InfectedPopulation) + and not np.isscalar(infected_population.expiration.diameter)): + # Verify if the ventilation is initialized as MultipleVentilation. if isinstance(self.concentration_model.ventilation, MultipleVentilation): for vent in self.concentration_model.ventilation.ventilations: - # Check if any of the ventilation parameters is an array instance. - # Note that most of the ventilation parameters are part of the WindowOpening class (inheritance). - if (isinstance(vent, WindowOpening) and ( - not all(np.isscalar(value) for value in vent.outside_temp.values) or - not np.isscalar(vent.window_height) or - (isinstance(vent, HingedWindow) and not np.isscalar(vent.window_width)))): - raise ValueError("Ventilation parameter(s) and diameter cannot be arrays at the same time.") - # The window_width parameter is only part of the HingedWindow class. - if ((isinstance(vent, HingedWindow)) and not np.isscalar(vent.window_width)): - raise ValueError("Ventilation parameter(s) and diameter cannot be arrays at the same time.") - # The q_air_mech parameter is only part of the HEPAFilter class, and - # the air_exch parameter is only part of the AirChange class. - if (isinstance(vent, HEPAFilter) and not np.isscalar(vent.q_air_mech) or - isinstance(vent, AirChange) and not np.isscalar(vent.air_exch)): - raise ValueError("Ventilation rate and diameter cannot be arrays at the same time.") - # Check if the room volume is an array instance. - if not np.isscalar(self.concentration_model.room.volume): - raise ValueError("Room volume and diameter cannot be arrays at the same time.") - # Virus decay constant depends on the room humidity and inside_temp parameters. - if (not all(np.isscalar(value) for value in self.concentration_model.room.inside_temp.values) or - not np.isscalar(self.concentration_model.room.humidity)): - raise ValueError("Virus decay constant and diameter cannot be arrays at the same time.") + verify_ventilation(vent) + else: + verify_ventilation(self.concentration_model.ventilation) + # Check if the room volume is an array instance. + if not np.isscalar(self.concentration_model.room.volume): + raise ValueError("Room volume and diameter cannot be arrays at the same time.") + # Virus decay constant depends on the room humidity and inside_temp parameters. + if (not all(np.isscalar(value) for value in self.concentration_model.room.inside_temp.values) or + not np.isscalar(self.concentration_model.room.humidity)): + raise ValueError("Virus decay constant and diameter cannot be arrays at the same time.") def long_range_fraction_deposited(self) -> _VectorisedFloat: """ From 5a6f4d54a1907bcb4f3c8ef5867c62509717752b Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 1 Aug 2022 11:56:22 +0200 Subject: [PATCH 3/6] Added tests for ExposureModel __post_init__ conditions --- caimira/tests/models/test_exposure_model.py | 123 +++++++++++++++++--- 1 file changed, 104 insertions(+), 19 deletions(-) diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py index 3168f6e8..ecb95ae0 100644 --- a/caimira/tests/models/test_exposure_model.py +++ b/caimira/tests/models/test_exposure_model.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from caimira import models from caimira.models import ExposureModel from caimira.dataclass_utils import replace - +from caimira.monte_carlo.data import expiration_distributions @dataclass(frozen=True) class KnownNormedconcentration(models.ConcentrationModel): @@ -90,9 +90,8 @@ def known_concentrations(func): np.array([40.91708675, 91.46172332]), np.array([51.6749232285, 80.3196524031])], ]) def test_exposure_model_ndarray(population, cm, - expected_exposure, expected_probability, sr_model): - geographical_data = models.Cases() - model = ExposureModel(cm, sr_model, population, geographical_data) + expected_exposure, expected_probability, sr_model, cases_model): + model = ExposureModel(cm, sr_model, population, cases_model) np.testing.assert_almost_equal( model.deposited_exposure(), expected_exposure ) @@ -111,11 +110,10 @@ def test_exposure_model_ndarray(population, cm, [populations[1], np.array([2.13410688, 1.98167067])], [populations[2], np.array([1.36390289, 1.52436206])], ]) -def test_exposure_model_ndarray_and_float_mix(population, expected_deposited_exposure, sr_model): +def test_exposure_model_ndarray_and_float_mix(population, expected_deposited_exposure, sr_model, cases_model): cm = known_concentrations( lambda t: 0. if np.floor(t) % 2 else np.array([1.2, 1.2])) - geographical_data = models.Cases() - model = ExposureModel(cm, sr_model, population, geographical_data) + model = ExposureModel(cm, sr_model, population, cases_model) np.testing.assert_almost_equal( model.deposited_exposure(), expected_deposited_exposure @@ -130,19 +128,17 @@ def test_exposure_model_ndarray_and_float_mix(population, expected_deposited_exp [populations[1], np.array([2.13410688, 1.98167067])], [populations[2], np.array([1.36390289, 1.52436206])], ]) -def test_exposure_model_vector(population, expected_deposited_exposure, sr_model): +def test_exposure_model_vector(population, expected_deposited_exposure, sr_model, cases_model): cm_array = known_concentrations(lambda t: np.array([1.2, 1.2])) - geographical_data = models.Cases() - model_array = ExposureModel(cm_array, sr_model, population, geographical_data) + model_array = ExposureModel(cm_array, sr_model, population, cases_model) np.testing.assert_almost_equal( model_array.deposited_exposure(), np.array(expected_deposited_exposure) ) -def test_exposure_model_scalar(sr_model): +def test_exposure_model_scalar(sr_model, cases_model): cm_scalar = known_concentrations(lambda t: 1.2) - geographical_data = models.Cases() - model_scalar = ExposureModel(cm_scalar, sr_model, populations[0], geographical_data) + model_scalar = ExposureModel(cm_scalar, sr_model, populations[0], cases_model) expected_deposited_exposure = 1.52436206 np.testing.assert_almost_equal( model_scalar.deposited_exposure(), expected_deposited_exposure @@ -173,9 +169,29 @@ def conc_model(): ) +@pytest.fixture +def diameter_dependent_model(conc_model) -> models.InfectedPopulation: + # Generate a diameter dependent model + return replace(conc_model, + infected = models.InfectedPopulation( + number=1, + presence=halftime, + virus=models.Virus.types['SARS_CoV_2_DELTA'], + mask=models.Mask.types['No mask'], + activity=models.Activity.types['Seated'], + expiration=expiration_distributions['Breathing'], + host_immunity=0., + )) + + @pytest.fixture def sr_model(): return () + + +@pytest.fixture +def cases_model(): + return () # Expected deposited exposure were computed with a trapezoidal integration, using @@ -192,18 +208,17 @@ def sr_model(): ] ) def test_exposure_model_integral_accuracy(exposed_time_interval, - expected_deposited_exposure, conc_model, sr_model): + expected_deposited_exposure, conc_model, sr_model, cases_model): presence_interval = models.SpecificInterval((exposed_time_interval,)) population = models.Population( 10, presence_interval, models.Mask.types['Type I'], models.Activity.types['Standing'], 0., ) - geographical_data = models.Cases() - model = ExposureModel(conc_model, sr_model, population, geographical_data) + model = ExposureModel(conc_model, sr_model, population, cases_model) np.testing.assert_allclose(model.deposited_exposure(), expected_deposited_exposure) -def test_infectious_dose_vectorisation(sr_model): +def test_infectious_dose_vectorisation(sr_model, cases_model): infected_population = models.InfectedPopulation( number=1, presence=halftime, @@ -226,8 +241,7 @@ def test_infectious_dose_vectorisation(sr_model): 10, presence_interval, models.Mask.types['Type I'], models.Activity.types['Standing'], 0., ) - geographical_data = models.Cases() - model = ExposureModel(cm, sr_model, population, geographical_data) + model = ExposureModel(cm, sr_model, population, cases_model) inf_probability = model.infection_probability() assert isinstance(inf_probability, np.ndarray) assert inf_probability.shape == (3, ) @@ -294,3 +308,74 @@ def test_probabilistic_exposure_probability(exposed_population, cm, np.testing.assert_allclose( model.total_probability_rule(), probabilistic_exposure_probability, rtol=0.05 ) + + +@pytest.mark.parametrize( + "active, outside_temp, window_height, opening_length", [ + [models.PeriodicInterval(period=120, duration=120), models.PiecewiseConstant((0., 24.), + (np.array([293., 300.]),)), 1., 1.,], # Verify (ventilation) outside_temp vectorisation. + [models.PeriodicInterval(period=120, duration=120), models.PiecewiseConstant((0., 24.), (293.,)), + np.array([1., 0.5]), 1.], # Verify (ventilation) window_height vectorisation. + [models.PeriodicInterval(period=120, duration=120), models.PiecewiseConstant((0., 24.), (293.,)), + 1., np.array([1., 0.5])], # Verify (ventilation) opening_length vectorisation. + ] +) +def test_diameter_vectorisation_window_opening(diameter_dependent_model, sr_model, active, outside_temp, + window_height, opening_length, cases_model): + concentration = replace(diameter_dependent_model, + ventilation=models.WindowOpening(active=active, + outside_temp=outside_temp, + window_height=window_height, + opening_length=opening_length) + ) + with pytest.raises(ValueError, match="Ventilation parameters and diameter cannot be arrays at the same time."): + models.ExposureModel(concentration, sr_model, populations[0], cases_model) + + +def test_diameter_vectorisation_hinged_window(diameter_dependent_model, sr_model, cases_model): + # Verify (ventilation) window_width vectorisation. + concentration = replace(diameter_dependent_model, + ventilation = models.HingedWindow(active=models.PeriodicInterval(period=120, duration=120), + outside_temp=models.PiecewiseConstant((0., 24.), (293.,)), + window_height=1., + opening_length=1., + window_width=np.array([1., 0.5])) + ) + with pytest.raises(ValueError, match="Ventilation parameters and diameter cannot be arrays at the same time."): + models.ExposureModel(concentration, sr_model, populations[0], cases_model) + + +def test_diameter_vectorisation_HEPA_filter(diameter_dependent_model, sr_model, cases_model): + # Verify (ventilation) q_air_mech vectorisation. + concentration = replace(diameter_dependent_model, + ventilation = models.HEPAFilter(active=models.PeriodicInterval(period=120, duration=120), q_air_mech=np.array([0.5, 1.])) + ) + with pytest.raises(ValueError, match="Ventilation rate and diameter cannot be arrays at the same time."): + models.ExposureModel(concentration, sr_model, populations[1], cases_model) + + +def test_diameter_vectorisation_air_change(diameter_dependent_model, sr_model, cases_model): + # Verify (ventilation) air_exch vectorisation. + concentration = replace(diameter_dependent_model, + ventilation = models.AirChange(active=models.PeriodicInterval(period=120, duration=120), air_exch=np.array([0.5, 1.])) + ) + with pytest.raises(ValueError, match="Ventilation rate and diameter cannot be arrays at the same time."): + models.ExposureModel(concentration, sr_model, populations[2], cases_model) + + +@pytest.mark.parametrize( + "volume, inside_temp, humidity, error", [ + [np.array([50, 100]), models.PiecewiseConstant((0., 24.), (293,)), 0.3, + "Room volume and diameter cannot be arrays at the same time."], # Verify room volume vectorisation + [50, models.PiecewiseConstant((0., 24.), (np.array([293., 300.]),)), 0.3, + "Virus decay constant and diameter cannot be arrays at the same time."], # Verify room inside_temp vectorisation + [50, models.PiecewiseConstant((0., 24.), (293.,)), np.array([0.3, 0.5]), + "Virus decay constant and diameter cannot be arrays at the same time."], # Verify room humidity vectorisation + ] +) +def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_model, volume, inside_temp, humidity, error): + concentration = replace(diameter_dependent_model, + room = models.Room(volume=volume, inside_temp=inside_temp, humidity=humidity)) + with pytest.raises(ValueError, match=error): + models.ExposureModel(concentration, sr_model, populations[0], cases_model) + \ No newline at end of file From 24ef3585719ffe71a9900d1e9fab53ad5c8290be Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 19 Oct 2022 17:55:18 +0200 Subject: [PATCH 4/6] adapted post_init method for diameter-independent elements of the IVRR --- caimira/models.py | 38 +++++---------------- caimira/tests/models/test_exposure_model.py | 29 ++++++++-------- 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 82e7a852..b29a7e9a 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1331,38 +1331,16 @@ class ExposureModel: The infection probability formula assumes that if the diameter is an array, then none of the ventilation parameters, room volume or virus decay constant, are arrays as well. + + The IVRR is the unique term in the exponential of the concentration formula, therefore + there is a check for the diameter-independent elements of the infectious_virus_removal_rate method. """ - def verify_ventilation(vent: Ventilation): - # Check if any of the ventilation parameters is an array instance. - # Note that most of the ventilation parameters are part of the WindowOpening class (inheritance). - if (isinstance(vent, WindowOpening) and ( - not all(np.isscalar(value) for value in vent.outside_temp.values) or - not np.isscalar(vent.window_height) or - not np.isscalar(vent.opening_length) or - (isinstance(vent, HingedWindow) and not np.isscalar(vent.window_width)))): - raise ValueError("Ventilation parameters and diameter cannot be arrays at the same time.") - # The q_air_mech parameter is only part of the HEPAFilter class, and - # the air_exch parameter is only part of the AirChange class. - if (isinstance(vent, HEPAFilter) and not np.isscalar(vent.q_air_mech) or - isinstance(vent, AirChange) and not np.isscalar(vent.air_exch)): - raise ValueError("Ventilation rate and diameter cannot be arrays at the same time.") - infected_population = self.concentration_model.infected - if (isinstance(infected_population, InfectedPopulation) - and not np.isscalar(infected_population.expiration.diameter)): - # Verify if the ventilation is initialized as MultipleVentilation. - if isinstance(self.concentration_model.ventilation, MultipleVentilation): - for vent in self.concentration_model.ventilation.ventilations: - verify_ventilation(vent) - else: - verify_ventilation(self.concentration_model.ventilation) - # Check if the room volume is an array instance. - if not np.isscalar(self.concentration_model.room.volume): - raise ValueError("Room volume and diameter cannot be arrays at the same time.") - # Virus decay constant depends on the room humidity and inside_temp parameters. - if (not all(np.isscalar(value) for value in self.concentration_model.room.inside_temp.values) or - not np.isscalar(self.concentration_model.room.humidity)): - raise ValueError("Virus decay constant and diameter cannot be arrays at the same time.") + if isinstance(infected_population, InfectedPopulation) and not np.isscalar(infected_population.expiration.diameter) and not ( + all(np.isscalar(self.concentration_model.virus.decay_constant(self.concentration_model.room.humidity, self.concentration_model.room.inside_temp.value(time)) + + self.concentration_model.ventilation.air_exchange(self.concentration_model.room, time)) for time in self.concentration_model.state_change_times())): + raise ValueError("If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time.") + def long_range_fraction_deposited(self) -> _VectorisedFloat: """ diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py index ecb95ae0..d199fa96 100644 --- a/caimira/tests/models/test_exposure_model.py +++ b/caimira/tests/models/test_exposure_model.py @@ -323,12 +323,12 @@ def test_probabilistic_exposure_probability(exposed_population, cm, def test_diameter_vectorisation_window_opening(diameter_dependent_model, sr_model, active, outside_temp, window_height, opening_length, cases_model): concentration = replace(diameter_dependent_model, - ventilation=models.WindowOpening(active=active, + ventilation=models.SlidingWindow(active=active, outside_temp=outside_temp, window_height=window_height, opening_length=opening_length) ) - with pytest.raises(ValueError, match="Ventilation parameters and diameter cannot be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[0], cases_model) @@ -341,7 +341,7 @@ def test_diameter_vectorisation_hinged_window(diameter_dependent_model, sr_model opening_length=1., window_width=np.array([1., 0.5])) ) - with pytest.raises(ValueError, match="Ventilation parameters and diameter cannot be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[0], cases_model) @@ -350,7 +350,7 @@ def test_diameter_vectorisation_HEPA_filter(diameter_dependent_model, sr_model, concentration = replace(diameter_dependent_model, ventilation = models.HEPAFilter(active=models.PeriodicInterval(period=120, duration=120), q_air_mech=np.array([0.5, 1.])) ) - with pytest.raises(ValueError, match="Ventilation rate and diameter cannot be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[1], cases_model) @@ -359,23 +359,22 @@ def test_diameter_vectorisation_air_change(diameter_dependent_model, sr_model, c concentration = replace(diameter_dependent_model, ventilation = models.AirChange(active=models.PeriodicInterval(period=120, duration=120), air_exch=np.array([0.5, 1.])) ) - with pytest.raises(ValueError, match="Ventilation rate and diameter cannot be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[2], cases_model) @pytest.mark.parametrize( - "volume, inside_temp, humidity, error", [ - [np.array([50, 100]), models.PiecewiseConstant((0., 24.), (293,)), 0.3, - "Room volume and diameter cannot be arrays at the same time."], # Verify room volume vectorisation - [50, models.PiecewiseConstant((0., 24.), (np.array([293., 300.]),)), 0.3, - "Virus decay constant and diameter cannot be arrays at the same time."], # Verify room inside_temp vectorisation - [50, models.PiecewiseConstant((0., 24.), (293.,)), np.array([0.3, 0.5]), - "Virus decay constant and diameter cannot be arrays at the same time."], # Verify room humidity vectorisation + "inside_temp, humidity, error_message", [ + [models.PiecewiseConstant((0., 24.), (np.array([293., 300.]),)), 0.3, + "If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."], # Verify room inside_temp vectorisation + [models.PiecewiseConstant((0., 24.), (293.,)), np.array([0.3, 0.5]), + "If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."], # Verify room humidity vectorisation ] ) -def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_model, volume, inside_temp, humidity, error): +def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_model, inside_temp, humidity, error_message): concentration = replace(diameter_dependent_model, - room = models.Room(volume=volume, inside_temp=inside_temp, humidity=humidity)) - with pytest.raises(ValueError, match=error): + room = models.Room(volume=50, inside_temp=inside_temp, humidity=humidity)) + # The vectorised volume is not considered in the air_exchange method for the AirChange class. + with pytest.raises(ValueError, match=error_message): models.ExposureModel(concentration, sr_model, populations[0], cases_model) \ No newline at end of file From ecf99b34cccb218c4cb1597b81aae47919a38be5 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 20 Oct 2022 14:19:49 +0200 Subject: [PATCH 5/6] Updated tests and model post_init method --- caimira/models.py | 14 +++++---- caimira/tests/models/test_exposure_model.py | 32 +++++++++++++-------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index b29a7e9a..7dce7b77 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1335,11 +1335,15 @@ class ExposureModel: The IVRR is the unique term in the exponential of the concentration formula, therefore there is a check for the diameter-independent elements of the infectious_virus_removal_rate method. """ - infected_population = self.concentration_model.infected - if isinstance(infected_population, InfectedPopulation) and not np.isscalar(infected_population.expiration.diameter) and not ( - all(np.isscalar(self.concentration_model.virus.decay_constant(self.concentration_model.room.humidity, self.concentration_model.room.inside_temp.value(time)) + - self.concentration_model.ventilation.air_exchange(self.concentration_model.room, time)) for time in self.concentration_model.state_change_times())): - raise ValueError("If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time.") + c_model = self.concentration_model + # Check if the diameter is vectorised. + if (isinstance(c_model.infected, InfectedPopulation) and not np.isscalar(c_model.infected.expiration.diameter) + # Check if the diameter-independent elements of the infectious_virus_removal_rate method are vectorised. + and not ( + all(np.isscalar(c_model.virus.decay_constant(c_model.room.humidity, c_model.room.inside_temp.value(time)) + + 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, " + "room volume or virus decay constant can be arrays at the same time.") def long_range_fraction_deposited(self) -> _VectorisedFloat: diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py index d199fa96..dab571d7 100644 --- a/caimira/tests/models/test_exposure_model.py +++ b/caimira/tests/models/test_exposure_model.py @@ -312,8 +312,8 @@ def test_probabilistic_exposure_probability(exposed_population, cm, @pytest.mark.parametrize( "active, outside_temp, window_height, opening_length", [ - [models.PeriodicInterval(period=120, duration=120), models.PiecewiseConstant((0., 24.), - (np.array([293., 300.]),)), 1., 1.,], # Verify (ventilation) outside_temp vectorisation. + [models.PeriodicInterval(period=120, duration=120), models.PiecewiseConstant((0., 12, 24.), + (np.array([293., 300.]), np.array([305., 310.]),)), 1., 1.,], # Verify (ventilation) outside_temp vectorisation. [models.PeriodicInterval(period=120, duration=120), models.PiecewiseConstant((0., 24.), (293.,)), np.array([1., 0.5]), 1.], # Verify (ventilation) window_height vectorisation. [models.PeriodicInterval(period=120, duration=120), models.PiecewiseConstant((0., 24.), (293.,)), @@ -328,7 +328,8 @@ def test_diameter_vectorisation_window_opening(diameter_dependent_model, sr_mode window_height=window_height, opening_length=opening_length) ) - with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, " + "room volume or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[0], cases_model) @@ -341,40 +342,47 @@ def test_diameter_vectorisation_hinged_window(diameter_dependent_model, sr_model opening_length=1., window_width=np.array([1., 0.5])) ) - with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, " + "room volume or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[0], cases_model) def test_diameter_vectorisation_HEPA_filter(diameter_dependent_model, sr_model, cases_model): # Verify (ventilation) q_air_mech vectorisation. concentration = replace(diameter_dependent_model, - ventilation = models.HEPAFilter(active=models.PeriodicInterval(period=120, duration=120), q_air_mech=np.array([0.5, 1.])) + ventilation = models.HEPAFilter(active=models.PeriodicInterval(period=120, duration=120), + q_air_mech=np.array([0.5, 1.])) ) - with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, " + "room volume or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[1], cases_model) def test_diameter_vectorisation_air_change(diameter_dependent_model, sr_model, cases_model): # Verify (ventilation) air_exch vectorisation. concentration = replace(diameter_dependent_model, - ventilation = models.AirChange(active=models.PeriodicInterval(period=120, duration=120), air_exch=np.array([0.5, 1.])) + ventilation = models.AirChange(active=models.PeriodicInterval(period=120, duration=120), + air_exch=np.array([0.5, 1.])) ) - with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, " + "room volume or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[2], cases_model) @pytest.mark.parametrize( "inside_temp, humidity, error_message", [ - [models.PiecewiseConstant((0., 24.), (np.array([293., 300.]),)), 0.3, - "If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."], # Verify room inside_temp vectorisation + [models.PiecewiseConstant((0., 12, 24.), (np.array([293., 300.]), np.array([305., 310.]))), 0.3, + "If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant " + "can be arrays at the same time."], # Verify room inside_temp vectorisation [models.PiecewiseConstant((0., 24.), (293.,)), np.array([0.3, 0.5]), - "If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant can be arrays at the same time."], # Verify room humidity vectorisation + "If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant " + "can be arrays at the same time."], # Verify room humidity vectorisation ] ) def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_model, inside_temp, humidity, error_message): concentration = replace(diameter_dependent_model, room = models.Room(volume=50, inside_temp=inside_temp, humidity=humidity)) - # The vectorised volume is not considered in the air_exchange method for the AirChange class. + # The Room volume is not considered in the air_exchange method for the AirChange class. with pytest.raises(ValueError, match=error_message): models.ExposureModel(concentration, sr_model, populations[0], cases_model) \ No newline at end of file From 44f016bb1605d272456ed182a346d14fd9ce84b3 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 2 Nov 2022 14:57:43 +0100 Subject: [PATCH 6/6] updated message errors and added room volume vectorisation test when as a ventilation parameter --- caimira/models.py | 17 +++--- caimira/tests/models/test_exposure_model.py | 57 +++++++++++---------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 7dce7b77..4caf3717 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1328,12 +1328,13 @@ class ExposureModel: def __post_init__(self): """ - The infection probability formula assumes that if the diameter - is an array, then none of the ventilation parameters, room volume or virus - decay constant, are arrays as well. - - The IVRR is the unique term in the exponential of the concentration formula, therefore - there is a check for the diameter-independent elements of the infectious_virus_removal_rate method. + When diameters are sampled (given as an array), + the Monte-Carlo integration over the diameters + assumes that all the parameters within the IVRR, + apart from the settling velocity, are NOT arrays. + In other words, the air exchange rate from the + ventilation, and the virus decay constant, must + not be given as arrays. """ c_model = self.concentration_model # Check if the diameter is vectorised. @@ -1342,8 +1343,8 @@ class ExposureModel: and not ( all(np.isscalar(c_model.virus.decay_constant(c_model.room.humidity, c_model.room.inside_temp.value(time)) + 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, " - "room volume or virus decay constant can be arrays at the same time.") + raise ValueError("If the diameter is an array, none of the ventilation parameters " + "or virus decay constant can be arrays at the same time.") def long_range_fraction_deposited(self) -> _VectorisedFloat: diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py index dab571d7..f66943a5 100644 --- a/caimira/tests/models/test_exposure_model.py +++ b/caimira/tests/models/test_exposure_model.py @@ -311,25 +311,27 @@ def test_probabilistic_exposure_probability(exposed_population, cm, @pytest.mark.parametrize( - "active, outside_temp, window_height, opening_length", [ - [models.PeriodicInterval(period=120, duration=120), models.PiecewiseConstant((0., 12, 24.), - (np.array([293., 300.]), np.array([305., 310.]),)), 1., 1.,], # Verify (ventilation) outside_temp vectorisation. - [models.PeriodicInterval(period=120, duration=120), models.PiecewiseConstant((0., 24.), (293.,)), - np.array([1., 0.5]), 1.], # Verify (ventilation) window_height vectorisation. - [models.PeriodicInterval(period=120, duration=120), models.PiecewiseConstant((0., 24.), (293.,)), - 1., np.array([1., 0.5])], # Verify (ventilation) opening_length vectorisation. + "volume, outside_temp, window_height, opening_length", [ + [np.array([50, 100]), models.PiecewiseConstant((0., 24.), (293.,)), 1., 1.,], # Verify (room) volume vectorisation. + [50, models.PiecewiseConstant((0., 12, 24.), + (np.array([293., 300.]), np.array([305., 310.]),)), 1., 1.,], # Verify (ventilation) outside_temp vectorisation. + [50, models.PiecewiseConstant((0., 24.), (293.,)), + np.array([1., 0.5]), 1.], # Verify (ventilation) window_height vectorisation. + [50, models.PiecewiseConstant((0., 24.), (293.,)), + 1., np.array([1., 0.5])], # Verify (ventilation) opening_length vectorisation. ] ) -def test_diameter_vectorisation_window_opening(diameter_dependent_model, sr_model, active, outside_temp, +def test_diameter_vectorisation_window_opening(diameter_dependent_model, sr_model, volume, outside_temp, window_height, opening_length, cases_model): concentration = replace(diameter_dependent_model, - ventilation=models.SlidingWindow(active=active, + room = models.Room(volume=volume, inside_temp=models.PiecewiseConstant((0., 24.), (293.,)), humidity=0.3), + ventilation=models.SlidingWindow(active=models.PeriodicInterval(period=120, duration=120), outside_temp=outside_temp, window_height=window_height, - opening_length=opening_length) + opening_length=opening_length), ) - with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, " - "room volume or virus decay constant can be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " + "or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[0], cases_model) @@ -342,8 +344,8 @@ def test_diameter_vectorisation_hinged_window(diameter_dependent_model, sr_model opening_length=1., window_width=np.array([1., 0.5])) ) - with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, " - "room volume or virus decay constant can be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " + "or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[0], cases_model) @@ -353,8 +355,8 @@ def test_diameter_vectorisation_HEPA_filter(diameter_dependent_model, sr_model, ventilation = models.HEPAFilter(active=models.PeriodicInterval(period=120, duration=120), q_air_mech=np.array([0.5, 1.])) ) - with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, " - "room volume or virus decay constant can be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " + "or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[1], cases_model) @@ -364,25 +366,28 @@ def test_diameter_vectorisation_air_change(diameter_dependent_model, sr_model, c ventilation = models.AirChange(active=models.PeriodicInterval(period=120, duration=120), air_exch=np.array([0.5, 1.])) ) - with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters, " - "room volume or virus decay constant can be arrays at the same time."): + with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " + "or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[2], cases_model) @pytest.mark.parametrize( - "inside_temp, humidity, error_message", [ - [models.PiecewiseConstant((0., 12, 24.), (np.array([293., 300.]), np.array([305., 310.]))), 0.3, - "If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant " + "volume, inside_temp, humidity, error_message", [ + [np.array([50, 100]), models.PiecewiseConstant((0., 24.), (293.,)), 0.3, + "If the diameter is an array, none of the ventilation parameters or virus decay constant " + "can be arrays at the same time."], # Verify room volume vectorisation + [50, models.PiecewiseConstant((0., 12, 24.), (np.array([293., 300.]), np.array([305., 310.]))), 0.3, + "If the diameter is an array, none of the ventilation parameters or virus decay constant " "can be arrays at the same time."], # Verify room inside_temp vectorisation - [models.PiecewiseConstant((0., 24.), (293.,)), np.array([0.3, 0.5]), - "If the diameter is an array, none of the ventilation parameters, room volume or virus decay constant " + [50, models.PiecewiseConstant((0., 24.), (293.,)), np.array([0.3, 0.5]), + "If the diameter is an array, none of the ventilation parameters or virus decay constant " "can be arrays at the same time."], # Verify room humidity vectorisation ] ) -def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_model, inside_temp, humidity, error_message): +def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_model, volume, inside_temp, humidity, error_message): concentration = replace(diameter_dependent_model, - room = models.Room(volume=50, inside_temp=inside_temp, humidity=humidity)) - # The Room volume is not considered in the air_exchange method for the AirChange class. + room = models.Room(volume=volume, inside_temp=inside_temp, humidity=humidity), + ventilation = models.HVACMechanical(active=models.SpecificInterval(((0., 24.), )), q_air_mech=100.)) with pytest.raises(ValueError, match=error_message): models.ExposureModel(concentration, sr_model, populations[0], cases_model) \ No newline at end of file