From b36e07e0b924cd31302e39f0520982bb53d0bc9d Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 17:22:39 +0100 Subject: [PATCH 01/11] Changing Room volume into float (instead of int), in models.py --- cara/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cara/models.py b/cara/models.py index 805993ac..35ce83f1 100644 --- a/cara/models.py +++ b/cara/models.py @@ -38,7 +38,7 @@ Geneva_hourly_temperatures_celsius_per_hour = { @dataclass(frozen=True) class Room: # The total volume of the room - volume: int + volume: float @dataclass(frozen=True) From f271b29ae10c7e44f93d6014b0fd71a5bd8141ba Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 18:42:05 +0100 Subject: [PATCH 02/11] Adding tests for multiple ventilation --- cara/tests/test_known_quantities.py | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index 923fceac..0877d52c 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -143,6 +143,56 @@ def test_periodic_hepa(baseline_periodic_hepa, baseline_room): npt.assert_allclose(aes, answers, rtol=1e-5) +@pytest.mark.parametrize( + "time, expected_value", + [ + [0, 3.7393925], + [14 / 60, 514.74 / 68 + 3.7393925], + [15 / 60, 514.74 / 68 + 3.7393925], + [16 / 60, 3.7393925], + [1, 5.3842316], + [1.5, 514.74 / 68 + 5.3842316], + [121 / 60, 514.74 / 68 + 5.3842316], + [2.5, 5.3842316], + ], +) +def test_multiple_ventilation_HEPA_window(baseline_periodic_hepa, time, expected_value): + room = models.Room(volume=68.) + tempOutside = models.PiecewiseConstant((0,1,2.5),(273.15,283.15)) + tempInside = models.PiecewiseConstant((0,24),(293.15,)) + window = models.WindowOpening(active=models.SpecificInterval([(0,24)]), + inside_temp=tempInside,outside_temp=tempOutside, + window_height=1.,opening_length=0.6) + vent = models.MultipleVentilation(window, baseline_periodic_hepa) + npt.assert_allclose(vent.air_exchange(room,time), expected_value, rtol=1e-5) + + +@pytest.mark.parametrize( + "volume, expected_value", + [ + [24.5, 500 / 24.5 + 100 / 24.5 + 3.], + [70, 500 / 70 + 100 / 70 + 3.], + ], +) +def test_multiple_ventilation_HEPA_HVAC_AirChange(volume, expected_value): + room = models.Room(volume=volume) + hepa = models.HEPAFilter( + active=models.SpecificInterval([(0,24)]), + q_air_mech=500., + ) + hvac = models.HVACMechanical( + active=models.SpecificInterval([(0,24)]), + q_air_mech=100., + ) + airchange = models.AirChange( + active=models.SpecificInterval([(0,24)]), + air_exch=3., + ) + vent = models.MultipleVentilation(hepa, hvac, airchange) + npt.assert_allclose(vent.air_exchange(room,10.), + expected_value,rtol=1e-5) + + def test_expiration_aerosols(): mask = models.Mask.types['Type I'] exp1 = models.Expiration((0.751, 0.139, 0.0139, 0.059), From dd3f51b21b03eb6d9345dbedc5aa7875ec69d1bd Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 18:46:32 +0100 Subject: [PATCH 03/11] Adding HVAC (mechanical ventilation) and AirChange (manual input of air exchange) in as Ventilation classes, in models.py --- cara/models.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/cara/models.py b/cara/models.py index 35ce83f1..35460810 100644 --- a/cara/models.py +++ b/cara/models.py @@ -216,6 +216,7 @@ class HEPAFilter(Ventilation): active: Interval #: The rate at which the HEPA exchanges air (when switched on) + # in m^3/h q_air_mech: float def air_exchange(self, room: Room, time: float) -> float: @@ -226,6 +227,41 @@ class HEPAFilter(Ventilation): return self.q_air_mech / room.volume +@dataclass(frozen=True) +class HVACMechanical(Ventilation): + #: The interval in which the mechanical ventilation (HVAC) is operating. + active: Interval + + #: The rate at which the HVAC exchanges air (when switched on) + # in m^3/h + q_air_mech: float + + def air_exchange(self, room: Room, time: float) -> float: + # If the HVAC is off, no air is being exchanged. + if not self.active.triggered(time): + return 0. + # Reminder, no dependence on time in the resulting calculation. + return self.q_air_mech / room.volume + + +@dataclass(frozen=True) +class AirChange(Ventilation): + #: The interval in which the ventilation is operating. + active: Interval + + #: The rate (in h^-1) at which the ventilation exchanges all the air + # of the room (when switched on) + air_exch: float + + def air_exchange(self, room: Room, time: float) -> float: + # No dependence on the room volume. + # If off, no air is being exchanged. + if not self.active.triggered(time): + return 0. + # Reminder, no dependence on time in the resulting calculation. + return self.air_exch + + @dataclass(frozen=True) class Virus: #: Biological decay (inactivation of the virus in air) From 3cffcb34af81635e45a38c90d122ea8de76c5bab Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 18:51:01 +0100 Subject: [PATCH 04/11] Correcting multiple ventilation tests --- cara/tests/test_known_quantities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index 0877d52c..6b684282 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -163,7 +163,7 @@ def test_multiple_ventilation_HEPA_window(baseline_periodic_hepa, time, expected window = models.WindowOpening(active=models.SpecificInterval([(0,24)]), inside_temp=tempInside,outside_temp=tempOutside, window_height=1.,opening_length=0.6) - vent = models.MultipleVentilation(window, baseline_periodic_hepa) + vent = models.MultipleVentilation([window, baseline_periodic_hepa]) npt.assert_allclose(vent.air_exchange(room,time), expected_value, rtol=1e-5) @@ -188,7 +188,7 @@ def test_multiple_ventilation_HEPA_HVAC_AirChange(volume, expected_value): active=models.SpecificInterval([(0,24)]), air_exch=3., ) - vent = models.MultipleVentilation(hepa, hvac, airchange) + vent = models.MultipleVentilation([hepa, hvac, airchange]) npt.assert_allclose(vent.air_exchange(room,10.), expected_value,rtol=1e-5) From 7e927490dd1acf5d93e1cea51d955bfdb2cc1555 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 20:49:07 +0100 Subject: [PATCH 05/11] Fixing multiple ventilation tests; adding a test on transition times for multiple ventilations; minor changes to window opening with time-dependent temperature test --- cara/tests/test_known_quantities.py | 51 ++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index 6b684282..5ccb8e14 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -146,27 +146,41 @@ def test_periodic_hepa(baseline_periodic_hepa, baseline_room): @pytest.mark.parametrize( "time, expected_value", [ - [0, 3.7393925], - [14 / 60, 514.74 / 68 + 3.7393925], - [15 / 60, 514.74 / 68 + 3.7393925], - [16 / 60, 3.7393925], - [1, 5.3842316], - [1.5, 514.74 / 68 + 5.3842316], - [121 / 60, 514.74 / 68 + 5.3842316], - [2.5, 5.3842316], + [0., 0.], + [1 / 60, 514.74 / 68], + [14 / 60, 514.74 / 68 + 5.3842316], + [15 / 60, 514.74 / 68 + 5.3842316], + [16 / 60, 5.3842316], + [1., 5.3842316], + [1.5, 3.7393925], + [121 / 60, 514.74 / 68 + 3.7393925], + [2.5, 3.7393925], ], ) def test_multiple_ventilation_HEPA_window(baseline_periodic_hepa, time, expected_value): room = models.Room(volume=68.) - tempOutside = models.PiecewiseConstant((0,1,2.5),(273.15,283.15)) - tempInside = models.PiecewiseConstant((0,24),(293.15,)) - window = models.WindowOpening(active=models.SpecificInterval([(0,24)]), + tempOutside = models.PiecewiseConstant((0., 1., 2.5),(273.15, 283.15)) + tempInside = models.PiecewiseConstant((0., 24.),(293.15,)) + window = models.WindowOpening(active=models.SpecificInterval([(1 / 60, 24.)]), inside_temp=tempInside,outside_temp=tempOutside, window_height=1.,opening_length=0.6) vent = models.MultipleVentilation([window, baseline_periodic_hepa]) npt.assert_allclose(vent.air_exchange(room,time), expected_value, rtol=1e-5) +def test_multiple_ventilation_HEPA_window_transitions(baseline_periodic_hepa): + room = models.Room(volume=68.) + tempOutside = models.PiecewiseConstant((0., 1., 2.5),(273.15, 283.15)) + tempInside = models.PiecewiseConstant((0., 24.),(293.15,)) + window = models.WindowOpening(active=models.SpecificInterval([(1 / 60, 24.)]), + inside_temp=tempInside,outside_temp=tempOutside, + window_height=1.,opening_length=0.6) + vent = models.MultipleVentilation([window, baseline_periodic_hepa]) + assert set(vent.transition_times()) == set([0.0, 1/60, 0.25, 1.0, 2.0, 2.25, + 2.5, 4.0, 4.25, 6.0, 6.25, 8.0, 8.25, 10.0, 10.25, 12.0, 12.25, + 14.0, 14.25, 16.0, 16.25, 18.0, 18.25, 20.0, 20.25, 22.0, 22.25, 24.]) + + @pytest.mark.parametrize( "volume, expected_value", [ @@ -249,16 +263,21 @@ def test_piecewiseconstant_vs_interval(): assert fun.interval().triggered(t) == interval.triggered(t) -def test_windowopening(): +@pytest.mark.parametrize( + "time, expected_value", + [ + [8., 5.3842316], + [16., 3.7393925], + ], +) +def test_windowopening(time, expected_value): tempOutside = models.PiecewiseConstant((0,10,24),(273.15,283.15)) tempInside = models.PiecewiseConstant((0,24),(293.15,)) w = models.WindowOpening(active=models.SpecificInterval([(0,24)]), inside_temp=tempInside,outside_temp=tempOutside, window_height=1.,opening_length=0.6) - npt.assert_allclose(w.air_exchange(models.Room(volume=68),16.), - 3.7393925,rtol=1e-5) - npt.assert_allclose(w.air_exchange(models.Room(volume=68),8.), - 5.3842316,rtol=1e-5) + npt.assert_allclose(w.air_exchange(models.Room(volume=68),time), + expected_value,rtol=1e-5) def build_hourly_dependent_model(month, intervals_open=((7.5, 8.5),)): From e7e54cd1298112ae347f93edb4faeca15a3035ec Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 20:50:36 +0100 Subject: [PATCH 06/11] Fixing docstring of Ventilation class --- cara/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cara/models.py b/cara/models.py index 35460810..6b1be170 100644 --- a/cara/models.py +++ b/cara/models.py @@ -165,8 +165,8 @@ class Ventilation: @abstractmethod def air_exchange(self, room: Room, time: float) -> float: """ - Returns the rate at which air is being exchanged in the given room per - cubic meter at a given time (in hours). + Returns the rate at which air is being exchanged in the given room + at a given time (in hours). Note that whilst the time is known inside this function, it may not be used to vary the result unless the specific time used is declared From 869758ce0db5bb272f4a8803086e89520d221864 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 20:50:58 +0100 Subject: [PATCH 07/11] Adding MultipleVentilation class --- cara/models.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/cara/models.py b/cara/models.py index 6b1be170..8510a492 100644 --- a/cara/models.py +++ b/cara/models.py @@ -176,6 +176,33 @@ class Ventilation: return 0. +@dataclass(frozen=True) +class MultipleVentilation: + """ + Represents a mechanism by which air can be exchanged (replaced/filtered) + in a time dependent manner. + + Group together different sources of ventilations. + + """ + ventilations: typing.Tuple[Ventilation, ...] + + def transition_times(self): + transitions = set() + for ventilation in self.ventilations: + transitions.update(ventilation.transition_times()) + return sorted(transitions) + + @abstractmethod + def air_exchange(self, room: Room, time: float) -> float: + """ + Returns the rate at which air is being exchanged in the given room + at a given time (in hours). + """ + return sum([ventilation.air_exchange(room,time) + for ventilation in self.ventilations]) + + @dataclass(frozen=True) class WindowOpening(Ventilation): #: The interval in which the window is open. From 28427cda52043ae619a409a53142d91e09fa2a20 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 21:46:10 +0100 Subject: [PATCH 08/11] Adding test on transition times for PiecewiseConstant objects; better layout on some other test --- cara/tests/test_known_quantities.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index 5ccb8e14..16cc9301 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -253,14 +253,22 @@ def test_constantfunction(): assert (fun.value(t) == 20) -def test_piecewiseconstant_vs_interval(): +@pytest.mark.parametrize( + "time", + [0,1,8,10,16,20.1,24], +) +def test_piecewiseconstant_vs_interval(time): transition_times = (0,8,16,24) values = (0,1,0) fun = models.PiecewiseConstant(transition_times,values) interval = models.SpecificInterval(present_times=[(8,16)]) assert interval.transition_times() == fun.interval().transition_times() - for t in [0,1,8,10,16,20.1,24]: - assert fun.interval().triggered(t) == interval.triggered(t) + assert fun.interval().triggered(time) == interval.triggered(time) + + +def test_piecewiseconstant_transition_times(): + outside_temp=models.GenevaTemperatures['Jan'] + assert set(outside_temp.transition_times) == outside_temp.interval().transition_times() @pytest.mark.parametrize( From 2790e6b1aa50630cdcd2bbb05e9343e10da94170 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 21:47:44 +0100 Subject: [PATCH 09/11] Simplifying WindowOpening transition_times from temperature (in line with comment on Merge request 19 - see https://gitlab.cern.ch/cara/cara/-/merge_requests/19#note_3928329) --- cara/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cara/models.py b/cara/models.py index 8510a492..cc03020c 100644 --- a/cara/models.py +++ b/cara/models.py @@ -219,8 +219,8 @@ class WindowOpening(Ventilation): def transition_times(self): transitions = super().transition_times() - transitions.update(self.inside_temp.interval().transition_times()) - transitions.update(self.outside_temp.interval().transition_times()) + transitions.update(self.inside_temp.transition_times) + transitions.update(self.outside_temp.transition_times) return transitions def air_exchange(self, room: Room, time: float) -> float: From d437c7e77913e04ad00b54ecba2ed8113cb6bbaf Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 22:15:15 +0100 Subject: [PATCH 10/11] Adding test on the use of the model with a MultipleVentilation object (computing concentration); some fine tuning on other tests (list -> tuple) --- cara/tests/test_known_quantities.py | 40 ++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index 16cc9301..df91b12a 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -191,15 +191,15 @@ def test_multiple_ventilation_HEPA_window_transitions(baseline_periodic_hepa): def test_multiple_ventilation_HEPA_HVAC_AirChange(volume, expected_value): room = models.Room(volume=volume) hepa = models.HEPAFilter( - active=models.SpecificInterval([(0,24)]), + active=models.SpecificInterval(((0,24),)), q_air_mech=500., ) hvac = models.HVACMechanical( - active=models.SpecificInterval([(0,24)]), + active=models.SpecificInterval(((0,24),)), q_air_mech=100., ) airchange = models.AirChange( - active=models.SpecificInterval([(0,24)]), + active=models.SpecificInterval(((0,24),)), air_exch=3., ) vent = models.MultipleVentilation([hepa, hvac, airchange]) @@ -334,6 +334,35 @@ def build_constant_temp_model(outside_temp, intervals_open=((7.5, 8.5),)): return model +def build_hourly_dependent_model_multipleventilation(month, intervals_open=((7.5, 8.5),)): + vent = models.MultipleVentilation(( + models.WindowOpening( + active=models.SpecificInterval(intervals_open), + inside_temp=models.PiecewiseConstant((0,24),(293,)), + outside_temp=models.GenevaTemperatures[month], + cd_b=0.6, window_height=1.6, opening_length=0.6, + ), + models.HEPAFilter( + active=models.SpecificInterval(((0,24),)), + q_air_mech=500., + ))) + model = models.Model( + room=models.Room(volume=75), + ventilation=vent, + infected=models.InfectedPerson( + virus=models.Virus.types['SARS_CoV_2'], + presence=models.SpecificInterval(((0, 4), (5, 7.5))), + mask=models.Mask.types['No mask'], + activity=models.Activity.types['Light exercise'], + expiration=models.Expiration.types['Unmodulated Vocalization'], + ), + infected_occupants=1, + exposed_occupants=10, + exposed_activity=models.Activity.types['Light exercise'], + ) + return model + + @pytest.mark.parametrize( "month, temperatures", models.Geneva_hourly_temperatures_celsius_per_hour.items(), @@ -350,6 +379,11 @@ def test_concentrations_hourly_dep_startup(month, temperatures, time): npt.assert_allclose(m1.concentration(time), m2.concentration(time), rtol=1e-5) +def test_concentrations_hourly_dep_multipleventilation(): + m = build_hourly_dependent_model_multipleventilation('Jan') + m.concentration(12.) + + @pytest.mark.parametrize( "month_temp_item", models.Geneva_hourly_temperatures_celsius_per_hour.items(), From 8c6e01e7b6423163e5c3f60cd49d139e8339509b Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 5 Nov 2020 22:16:03 +0100 Subject: [PATCH 11/11] Make sure to output a set for transition_times (models.py) --- cara/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cara/models.py b/cara/models.py index cc03020c..7737bd2d 100644 --- a/cara/models.py +++ b/cara/models.py @@ -159,7 +159,7 @@ class Ventilation: #: The times at which the air exchange is taking place. active: Interval - def transition_times(self): + def transition_times(self) -> typing.Set[float]: return self.active.transition_times() @abstractmethod @@ -187,11 +187,11 @@ class MultipleVentilation: """ ventilations: typing.Tuple[Ventilation, ...] - def transition_times(self): + def transition_times(self) -> typing.Set[float]: transitions = set() for ventilation in self.ventilations: transitions.update(ventilation.transition_times()) - return sorted(transitions) + return transitions @abstractmethod def air_exchange(self, room: Room, time: float) -> float: @@ -217,7 +217,7 @@ class WindowOpening(Ventilation): cd_b: float = 0.6 #: Discharge coefficient: what portion effective area is used to exchange air (0 <= cd_b <= 1) - def transition_times(self): + def transition_times(self) -> typing.Set[float]: transitions = super().transition_times() transitions.update(self.inside_temp.transition_times) transitions.update(self.outside_temp.transition_times)