@@ -93,7 +94,13 @@ v{{ calculator_version }} Please sen
-
+
+
+ Central heating system in use:
+
+
+
+
diff --git a/cara/apps/calculator/templates/userguide.html.j2 b/cara/apps/calculator/templates/userguide.html.j2
index 6407e503..013a1763 100644
--- a/cara/apps/calculator/templates/userguide.html.j2
+++ b/cara/apps/calculator/templates/userguide.html.j2
@@ -72,6 +72,8 @@ However, this value may be revised in the future as more studies of P.1 transmis
Room Data
Please enter either the room volume (in m³) or both the floor area (m²) and the room height (m).
This information is available via GIS Portal (https://gis.cern.ch/gisportal/).
+
Room heating system
+
The use of central heating (e.g. radiators) reduces relative humidity of the indoor air, which can decrease the decay rate of viral infectivity. If your space is heated with such water radiators, select 'Yes'. If your space does not have such heating, or they are not in use in the period of the simulation (e.g. summer), select 'No'.
Ventilation type
There are three main options:
Mechanical ventilation
diff --git a/cara/models.py b/cara/models.py
index 28841f40..d17b962b 100644
--- a/cara/models.py
+++ b/cara/models.py
@@ -60,6 +60,9 @@ class Room:
#: The total volume of the room
volume: _VectorisedFloat
+ #: The humidity in the room (from 0 to 1 - e.g. 0.5 is 50% humidity)
+ humidity: _VectorisedFloat=0.5
+
Time_t = typing.TypeVar('Time_t', float, int)
BoundaryPair_t = typing.Tuple[Time_t, Time_t]
@@ -413,9 +416,6 @@ class AirChange(Ventilation):
@dataclass(frozen=True)
class Virus:
- #: Biological decay (inactivation of the virus in air)
- halflife: _VectorisedFloat
-
#: RNA copies / mL
viral_load_in_sputum: _VectorisedFloat
@@ -425,29 +425,45 @@ class Virus:
#: Pre-populated examples of Viruses.
types: typing.ClassVar[typing.Dict[str, "Virus"]]
- @property
- def decay_constant(self) -> _VectorisedFloat:
- # Viral inactivation per hour (h^-1)
- return np.log(2) / self.halflife
+ def halflife(self, humidity: _VectorisedFloat) -> _VectorisedFloat:
+ # Biological decay (inactivation of the virus in air) - virus
+ # dependent and function of humidity
+ raise NotImplementedError
+
+ def decay_constant(self, humidity: _VectorisedFloat) -> _VectorisedFloat:
+ # Viral inactivation per hour (h^-1) (function of humidity)
+ return np.log(2) / self.halflife(humidity)
+
+
+@dataclass(frozen=True)
+class SARSCoV2(Virus):
+
+ def halflife(self, humidity: _VectorisedFloat) -> _VectorisedFloat:
+ """
+ Half-life changes with humidity level. Here is implemented a simple
+ piecewise constant model (for more details see A. Henriques et al,
+ CERN-OPEN-2021-004, DOI: 10.17181/CERN.1GDQ.5Y75)
+ """
+ halflife = np.empty_like(humidity)
+ halflife[humidity <= 0.4] = 3.8
+ halflife[humidity > 0.4] = 1.1
+ return halflife
Virus.types = {
- 'SARS_CoV_2': Virus(
- halflife=1.1,
+ 'SARS_CoV_2': SARSCoV2(
viral_load_in_sputum=1e9,
# No data on coefficient for SARS-CoV-2 yet.
# It is somewhere between 1000 or 100 SARS-CoV viruses to have
# a 50% chance to cause infection.
quantum_infectious_dose=50.,
),
- 'SARS_CoV_2_B117': Virus(
+ 'SARS_CoV_2_B117': SARSCoV2(
# also called VOC-202012/01
- halflife=1.1,
viral_load_in_sputum=1e9,
quantum_infectious_dose=30.,
),
- 'SARS_CoV_2_P1': Virus(
- halflife=1.1,
+ 'SARS_CoV_2_P1': SARSCoV2(
viral_load_in_sputum=1e9,
quantum_infectious_dose=1/0.045,
),
@@ -640,9 +656,8 @@ class ConcentrationModel:
# Deposition rate (h^-1)
k = (vg * 3600) / h
- return k + self.virus.decay_constant + self.ventilation.air_exchange(
- self.room, time
- )
+ return k + self.virus.decay_constant(self.room.humidity
+ ) + self.ventilation.air_exchange(self.room, time)
@cached()
def _concentration_limit(self, time: float) -> _VectorisedFloat:
diff --git a/cara/tests/models/test_concentration_model.py b/cara/tests/models/test_concentration_model.py
index 03a042d6..48ef0b66 100644
--- a/cara/tests/models/test_concentration_model.py
+++ b/cara/tests/models/test_concentration_model.py
@@ -9,8 +9,8 @@ from cara import models
@pytest.mark.parametrize(
"override_params", [
{'volume': np.array([100, 120])},
+ {'humidity': np.array([0.5, 0.4])},
{'air_change': np.array([100, 120])},
- {'virus_halflife': np.array([1.1, 1.5])},
{'viral_load_in_sputum': np.array([5e8, 1e9])},
{'quantum_infectious_dose': np.array([50, 20])},
{'η_exhale': np.array([0.92, 0.95])},
@@ -20,8 +20,8 @@ from cara import models
def test_concentration_model_vectorisation(override_params):
defaults = {
'volume': 75,
+ 'humidity': 0.5,
'air_change': 100,
- 'virus_halflife': 1.1,
'viral_load_in_sputum': 1e9,
'quantum_infectious_dose': 50,
'η_exhale': 0.95,
@@ -31,7 +31,7 @@ def test_concentration_model_vectorisation(override_params):
always = models.PeriodicInterval(240, 240) # TODO: This should be a thing on an interval.
c_model = models.ConcentrationModel(
- models.Room(defaults['volume']),
+ models.Room(defaults['volume'], defaults['humidity']),
models.AirChange(always, defaults['air_change']),
models.InfectedPopulation(
number=1,
@@ -45,8 +45,7 @@ def test_concentration_model_vectorisation(override_params):
0.51,
0.75,
),
- virus=models.Virus(
- halflife=defaults['virus_halflife'],
+ virus=models.SARSCoV2(
viral_load_in_sputum=defaults['viral_load_in_sputum'],
quantum_infectious_dose=defaults['quantum_infectious_dose'],
),
diff --git a/cara/tests/test_infected_population.py b/cara/tests/test_infected_population.py
index c237e251..e87ddb20 100644
--- a/cara/tests/test_infected_population.py
+++ b/cara/tests/test_infected_population.py
@@ -15,7 +15,6 @@ import cara.models
)
def test_infected_population_vectorisation(override_params):
defaults = {
- 'virus_halflife': 1.1,
'viral_load_in_sputum': 1e9,
'quantum_infectious_dose': 50,
'η_exhale': 0.95,
@@ -38,7 +37,6 @@ def test_infected_population_vectorisation(override_params):
defaults['exhalation_rate'],
),
virus=cara.models.Virus(
- halflife=defaults['virus_halflife'],
viral_load_in_sputum=defaults['viral_load_in_sputum'],
quantum_infectious_dose=defaults['quantum_infectious_dose'],
),