diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index e6cd21b7..e50cc67f 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -141,9 +141,9 @@ class FormData: inside_temp = models.PiecewiseConstant((0, 24), (293,)) outside_temp = data.GenevaTemperatures[month] - ventilation = models.WindowOpening( + ventilation = models.SlidingWindow( active=window_interval, - inside_temp=inside_temp, outside_temp=outside_temp, cd_b=0.6, + inside_temp=inside_temp, outside_temp=outside_temp, window_height=self.window_height, opening_length=self.opening_distance, number_of_windows=self.windows_number, diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 809f484a..a14a7274 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -458,7 +458,7 @@ baseline_model = models.ExposureModel( active=models.PeriodicInterval(period=120, duration=15), inside_temp=models.PiecewiseConstant((0,24),(293.15,)), outside_temp=models.PiecewiseConstant((0,24),(283.15,)), - cd_b=0.6, window_height=1.6, opening_length=0.6, + discharge_coefficient=0.6, window_height=1.6, opening_length=0.6, ), infected=models.InfectedPopulation( number=1, diff --git a/cara/models.py b/cara/models.py index 801a38d3..1e918bd8 100644 --- a/cara/models.py +++ b/cara/models.py @@ -189,45 +189,25 @@ class WindowOpening(Ventilation): #: The temperature outside of the window (Kelvin). outside_temp: PiecewiseConstant - #: The height of the window. + #: The height of the window (m). window_height: float - #: The length of the opening-gap when the window is open + #: The length of the opening-gap when the window is open (m). opening_length: float - #: The type of the window ('sliding' or 'hinged') - window_type: str = 'sliding' - - #: The width of the window (used only to get cd_b of a hinged window). - window_width: float = None + #: Discharge coefficient: what portion effective area is + #: used to exchange air (0 <= discharge_coefficient <= 1). + discharge_coefficient: float = None #: The number of windows of the given dimensions. number_of_windows: int = 1 - #: Discharge coefficient: what portion effective area is - #: used to exchange air (0 <= cd_b <= 1). Overrides the value - #: obtained from the window parameters - cd_b: float = None - - #: Minimum difference between inside and outside temperature + #: Minimum difference between inside and outside temperature (K). min_deltaT: float = 0.1 @property - def _cd_b(self): - if self.cd_b is not None: - return self.cd_b - elif self.window_type == 'sliding': - return 0.6 - elif self.window_type == 'hinged': - window_ratio = self.window_width / self.window_height - M = (0.06 if window_ratio < 0.5 else 0.048 if window_ratio < 1 else - 0.04 if window_ratio < 2 else 0.038) - cd_max = (0.612 if window_ratio < 0.5 else 0.589 if window_ratio < 1 - else 0.563 if window_ratio < 2 else 0.548) - window_angle = np.arccos(1-self.opening_length**2/(2.*self.window_height**2)) - return cd_max*(1-np.exp(-M*window_angle)) - else: - raise ValueError("Unknown window type; please specify cd_b") + def cd_b(self) -> float: + return self.discharge_coefficient def transition_times(self) -> typing.Set[float]: transitions = super().transition_times() @@ -252,7 +232,30 @@ class WindowOpening(Ventilation): temp_gradient = (inside_temp - outside_temp) / outside_temp root = np.sqrt(9.81 * self.window_height * temp_gradient) window_area = self.window_height * self.opening_length * self.number_of_windows - return (3600 / (3 * room.volume)) * self._cd_b * window_area * root + return (3600 / (3 * room.volume)) * self.cd_b * window_area * root + + +@dataclass(frozen=True) +class SlidingWindow(WindowOpening): + @property + def cd_b(self) -> float: + return 0.6 + + +@dataclass(frozen=True) +class HingedWindow(WindowOpening): + #: Window width (m). + window_width: float = None + + @property + def cd_b(self) -> float: + window_ratio = self.window_width / self.window_height + M = (0.06 if window_ratio < 0.5 else 0.048 if window_ratio < 1 else + 0.04 if window_ratio < 2 else 0.038) + cd_max = (0.612 if window_ratio < 0.5 else 0.589 if window_ratio < 1 + else 0.563 if window_ratio < 2 else 0.548) + window_angle = np.arccos(1-self.opening_length**2/(2.*self.window_height**2)) + return cd_max*(1-np.exp(-M*window_angle)) @dataclass(frozen=True) diff --git a/cara/tests/apps/calculator/test_model_generator.py b/cara/tests/apps/calculator/test_model_generator.py index 57779c6b..889eca0b 100644 --- a/cara/tests/apps/calculator/test_model_generator.py +++ b/cara/tests/apps/calculator/test_model_generator.py @@ -32,11 +32,11 @@ def test_blend_expiration(): def test_ventilation_window(baseline_form): room = models.Room(75) - window = models.WindowOpening( + window = models.SlidingWindow( active=models.PeriodicInterval(period=120, duration=10), inside_temp=models.PiecewiseConstant((0, 24), (293,)), outside_temp=data.GenevaTemperatures['Dec'], - cd_b=0.6, window_height=1.6, opening_length=0.6, + window_height=1.6, opening_length=0.6, ) baseline_form.ventilation_type = 'natural' baseline_form.windows_open = 'interval' @@ -82,11 +82,11 @@ def test_ventilation_airchanges(baseline_form): def test_ventilation_window_hepa(baseline_form): room = models.Room(75) - window = models.WindowOpening( + window = models.SlidingWindow( active=models.PeriodicInterval(period=120, duration=10), inside_temp=models.PiecewiseConstant((0, 24), (293,)), outside_temp=data.GenevaTemperatures['Dec'], - cd_b=0.6, window_height=1.6, opening_length=0.6, + window_height=1.6, opening_length=0.6, ) hepa = models.HEPAFilter( active=models.PeriodicInterval(period=120, duration=120), diff --git a/cara/tests/conftest.py b/cara/tests/conftest.py index 59dbe826..a7d7a8bc 100644 --- a/cara/tests/conftest.py +++ b/cara/tests/conftest.py @@ -11,7 +11,7 @@ def baseline_model(): active=models.PeriodicInterval(period=120, duration=120), inside_temp=models.PiecewiseConstant((0,24),(293,)), outside_temp=models.PiecewiseConstant((0,24),(283,)), - cd_b=0.6, window_height=1.6, opening_length=0.6, + discharge_coefficient=0.6, window_height=1.6, opening_length=0.6, ), infected=models.InfectedPopulation( number=1, diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index ddceed02..baaaaea7 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -30,7 +30,7 @@ def baseline_periodic_window(): active=models.PeriodicInterval(period=120, duration=15), inside_temp=models.PiecewiseConstant((0,24),(293,)), outside_temp=models.PiecewiseConstant((0,24),(283,)), - cd_b=0.6, window_height=1.6, opening_length=0.6, + discharge_coefficient=0.6, window_height=1.6, opening_length=0.6, ) @@ -136,7 +136,7 @@ def test_multiple_ventilation_HEPA_window(baseline_periodic_hepa, time, expected 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.)]), + window = models.SlidingWindow(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]) @@ -146,7 +146,7 @@ def test_multiple_ventilation_HEPA_window(baseline_periodic_hepa, time, expected def test_multiple_ventilation_HEPA_window_transitions(baseline_periodic_hepa): 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.)]), + window = models.SlidingWindow(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]) @@ -264,7 +264,7 @@ def test_piecewiseconstant_transition_times(): 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)]), + w = models.SlidingWindow(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),time), @@ -293,7 +293,7 @@ def build_hourly_dependent_model(month, intervals_open=((7.5, 8.5),), active=models.SpecificInterval(intervals_open), inside_temp=models.PiecewiseConstant((0,24),(293,)), outside_temp=outside_temp, - cd_b=0.6, window_height=1.6, opening_length=0.6, + discharge_coefficient=0.6, window_height=1.6, opening_length=0.6, ), infected=models.InfectedPopulation( number=1, @@ -314,7 +314,7 @@ def build_constant_temp_model(outside_temp, intervals_open=((7.5, 8.5),)): active=models.SpecificInterval(intervals_open), inside_temp=models.PiecewiseConstant((0,24),(293,)), outside_temp=models.PiecewiseConstant((0,24),(outside_temp,)), - cd_b=0.6, window_height=1.6, opening_length=0.6, + discharge_coefficient=0.6, window_height=1.6, opening_length=0.6, ), infected=models.InfectedPopulation( number=1, @@ -334,7 +334,7 @@ def build_hourly_dependent_model_multipleventilation(month, intervals_open=((7.5 active=models.SpecificInterval(intervals_open), inside_temp=models.PiecewiseConstant((0,24),(293,)), outside_temp=data.GenevaTemperatures[month], - cd_b=0.6, window_height=1.6, opening_length=0.6, + discharge_coefficient=0.6, window_height=1.6, opening_length=0.6, ), models.HEPAFilter( active=models.SpecificInterval(((0,24),)), diff --git a/cara/tests/test_ventilation.py b/cara/tests/test_ventilation.py index bf6a7bb1..7a15e65b 100644 --- a/cara/tests/test_ventilation.py +++ b/cara/tests/test_ventilation.py @@ -7,20 +7,30 @@ from cara import models @pytest.fixture -def baseline_window(): - return models.WindowOpening( +def baseline_slidingwindow(): + return models.SlidingWindow( active=models.SpecificInterval(((0, 4), (5, 9))), inside_temp=models.PiecewiseConstant((0, 24), (293,)), outside_temp=models.PiecewiseConstant((0, 24), (283,)), - cd_b=0.6, window_height=1.6, opening_length=0.6, + window_height=1.6, opening_length=0.6, ) -def test_number_of_windows(baseline_window): - room = models.Room(75) - two_windows = dataclasses.replace(baseline_window, number_of_windows=2) +@pytest.fixture +def baseline_hingedwindow(): + return models.HingedWindow( + active=models.SpecificInterval(((0, 4), (5, 9))), + inside_temp=models.PiecewiseConstant((0, 24), (293,)), + outside_temp=models.PiecewiseConstant((0, 24), (283,)), + window_height=1.6, opening_length=0.6, window_width=1., + ) - one_window_exchange = baseline_window.air_exchange(room, 1) + +def test_number_of_windows(baseline_slidingwindow): + room = models.Room(75) + two_windows = dataclasses.replace(baseline_slidingwindow, number_of_windows=2) + + one_window_exchange = baseline_slidingwindow.air_exchange(room, 1) two_window_exchange = two_windows.air_exchange(room, 1) assert one_window_exchange != 0 assert one_window_exchange * 2 == two_window_exchange @@ -35,17 +45,15 @@ def test_number_of_windows(baseline_window): [4., 0.00779945967], ], ) -def test_hinged_window(baseline_window,window_width,expected_cd_b): +def test_hinged_window(baseline_hingedwindow,window_width,expected_cd_b): room = models.Room(75) - hinged_window = dataclasses.replace(baseline_window, cd_b=None, - window_type='hinged',window_width=window_width) + hinged_window = dataclasses.replace(baseline_hingedwindow, + window_width=window_width) - npt.assert_allclose(hinged_window._cd_b, expected_cd_b, rtol=1e-8) + npt.assert_allclose(hinged_window.cd_b, expected_cd_b, rtol=1e-8) -def test_sliding_window(baseline_window): +def test_sliding_window(baseline_slidingwindow): room = models.Room(75) - sliding_window = dataclasses.replace(baseline_window, cd_b=None, - window_type='sliding') - assert sliding_window._cd_b == 0.6 + assert baseline_slidingwindow.cd_b == 0.6