Merge branch 'develop/multiple_ventilation' into 'master'
Multiple ventilations Closes #44 and #18 See merge request cara/cara!29
This commit is contained in:
commit
adb1f901ef
2 changed files with 189 additions and 15 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue