Merge branch 'develop/multiple_ventilation' into 'master'

Multiple ventilations

Closes #44 and #18

See merge request cara/cara!29
This commit is contained in:
Philip James Elson 2020-11-05 22:26:35 +00:00
commit adb1f901ef
2 changed files with 189 additions and 15 deletions

View file

@ -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)
@ -159,14 +159,14 @@ 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
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
@ -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) -> typing.Set[float]:
transitions = set()
for ventilation in self.ventilations:
transitions.update(ventilation.transition_times())
return 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.
@ -190,10 +217,10 @@ 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.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:
@ -216,6 +243,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 +254,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)

View file

@ -143,6 +143,70 @@ def test_periodic_hepa(baseline_periodic_hepa, baseline_room):
npt.assert_allclose(aes, answers, rtol=1e-5)
@pytest.mark.parametrize(
"time, expected_value",
[
[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([(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",
[
[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),
@ -189,26 +253,39 @@ 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_windowopening():
def test_piecewiseconstant_transition_times():
outside_temp=models.GenevaTemperatures['Jan']
assert set(outside_temp.transition_times) == outside_temp.interval().transition_times()
@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),)):
@ -257,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(),
@ -273,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(),