From e4e44bddd54a44de9d059738bb635a755c6d76f2 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Sun, 28 Mar 2021 07:32:42 +0200 Subject: [PATCH] Implement vectorisation support for the ventilation schemes. --- cara/models.py | 31 ++++++++++++++++++++----------- cara/tests/test_ventilation.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/cara/models.py b/cara/models.py index 5337425e..4a12bac5 100644 --- a/cara/models.py +++ b/cara/models.py @@ -44,10 +44,17 @@ else: from .dataclass_utils import nested_replace +# Define types for items supporting vectorisation. In the future this may be replaced +# by ``np.ndarray[]`` once/if that syntax is supported. Note that vectorization +# implies 1d arrays: multi-dimensional arrays are not supported. +_VectorisedFloat = typing.Union[float, np.ndarray] +_VectorisedInt = typing.Union[int, np.ndarray] + + @dataclass(frozen=True) class Room: - # The total volume of the room - volume: float + #: The total volume of the room + volume: _VectorisedFloat Time_t = typing.TypeVar('Time_t', float, int) @@ -185,7 +192,7 @@ class _VentilationBase: def transition_times(self) -> typing.Set[float]: raise NotImplementedError("Subclass must implement") - def air_exchange(self, room: Room, time: float) -> float: + def air_exchange(self, room: Room, time: float) -> _VectorisedFloat: """ Returns the rate at which air is being exchanged in the given room at a given time (in hours). @@ -224,13 +231,15 @@ class MultipleVentilation(_VentilationBase): transitions.update(ventilation.transition_times()) return transitions - def air_exchange(self, room: Room, time: float) -> float: + def air_exchange(self, room: Room, time: float) -> _VectorisedFloat: """ 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]) + return np.array([ + ventilation.air_exchange(room, time) + for ventilation in self.ventilations + ]).sum(axis=0) @dataclass(frozen=True) @@ -271,7 +280,7 @@ class WindowOpening(Ventilation): transitions.update(self.outside_temp.transition_times) return transitions - def air_exchange(self, room: Room, time: float) -> float: + def air_exchange(self, room: Room, time: float) -> _VectorisedFloat: # If the window is shut, no air is being exchanged. if not self.active.triggered(time): return 0. @@ -356,7 +365,7 @@ class HEPAFilter(Ventilation): # in m^3/h q_air_mech: float - def air_exchange(self, room: Room, time: float) -> float: + def air_exchange(self, room: Room, time: float) -> _VectorisedFloat: # If the HEPA is off, no air is being exchanged. if not self.active.triggered(time): return 0. @@ -373,7 +382,7 @@ class HVACMechanical(Ventilation): # in m^3/h q_air_mech: float - def air_exchange(self, room: Room, time: float) -> float: + def air_exchange(self, room: Room, time: float) -> _VectorisedFloat: # If the HVAC is off, no air is being exchanged. if not self.active.triggered(time): return 0. @@ -388,9 +397,9 @@ class AirChange(Ventilation): #: The rate (in h^-1) at which the ventilation exchanges all the air # of the room (when switched on) - air_exch: float + air_exch: _VectorisedFloat - def air_exchange(self, room: Room, time: float) -> float: + def air_exchange(self, room: Room, time: float) -> _VectorisedFloat: # No dependence on the room volume. # If off, no air is being exchanged. if not self.active.triggered(time): diff --git a/cara/tests/test_ventilation.py b/cara/tests/test_ventilation.py index cfb5569c..e77e60a8 100644 --- a/cara/tests/test_ventilation.py +++ b/cara/tests/test_ventilation.py @@ -1,7 +1,8 @@ import dataclasses -import pytest +import numpy as np import numpy.testing as npt +import pytest from cara import models @@ -59,3 +60,32 @@ def test_sliding_window(baseline_slidingwindow): room = models.Room(75) assert baseline_slidingwindow.discharge_coefficient == 0.6 + + +def test_multiple(baseline_slidingwindow, baseline_hingedwindow): + v = models.MultipleVentilation([baseline_hingedwindow, baseline_slidingwindow]) + room = models.Room(75) + t = 1 + assert v.air_exchange(room, t) == ( + baseline_slidingwindow.air_exchange(room, t) + + baseline_hingedwindow.air_exchange(room, t) + ) + + +def test_multiple_vectorisation(): + interval = models.SpecificInterval(((0, 4), (5, 9))) + v1 = models.AirChange(interval, np.arange(10)) + v2 = models.AirChange(interval, np.arange(5)) + v3 = models.AirChange(interval, 10) + + room = models.Room(75) + t_active = 2 + t_inactive = 4.5 + + assert models.MultipleVentilation([v1, v2]).air_exchange(room, t_inactive) == 0 + with pytest.raises(ValueError, match='operands could not be broadcast together'): + models.MultipleVentilation([v1, v2]).air_exchange(room, t_active) + + r = models.MultipleVentilation([v2, v3]).air_exchange(room, t_active) + assert isinstance(r, np.ndarray) + assert r == np.array([10, 11, 12, 13, 14])