Merge branch 'feature/RH_dependent_virus_halflife' into 'master'

Humidity dependence of virus halflife & central heating option in calculator

Closes #148

See merge request cara/cara!180
This commit is contained in:
Philip James Elson 2021-05-26 08:19:53 +00:00
commit 2a3c3eb138
7 changed files with 55 additions and 26 deletions

View file

@ -53,6 +53,7 @@ class FormData:
calculator_version: str
opening_distance: float
event_month: str
room_heating_option: bool
room_number: str
room_volume: float
simulation_name: str
@ -100,6 +101,7 @@ class FormData:
'mask_wearing_option': 'mask_off',
'mechanical_ventilation_type': 'not-applicable',
'opening_distance': 0.,
'room_heating_option': False,
'room_number': _NO_DEFAULT,
'room_volume': 0.,
'simulation_name': _NO_DEFAULT,
@ -573,7 +575,11 @@ def model_from_form(form: FormData) -> models.ExposureModel:
volume = form.room_volume
else:
volume = form.floor_area * form.ceiling_height
room = models.Room(volume=volume)
if form.room_heating_option:
humidity = 0.3
else:
humidity = 0.5
room = models.Room(volume=volume, humidity=humidity)
# Initializes and returns a model with the attributes defined above
return models.ExposureModel(
@ -618,6 +624,7 @@ def baseline_raw_form_data():
'calculator_version': calculator.__version__,
'opening_distance': '0.2',
'event_month': 'January',
'room_heating_option': '0',
'room_number': '123',
'room_volume': '75',
'simulation_name': 'Test',

View file

@ -46,6 +46,7 @@
{% endif %}
</p></li>
<li><p class="data_text">Room Volume: {{ model.concentration_model.room.volume }} m³</p></li>
<li><p class="data_text">Room Central Heating: {{ "On" if form.room_heating_option else "Off" }}</p></li>
</ul>
<p class="data_title">Ventilation data:</p>

View file

@ -69,9 +69,10 @@ v{{ calculator_version }} <span style="float:right; font-weight:bold">Please sen
<hr width="80%">
<b>Room data:</b>
<div data-tooltip="The area you wish to study (choose one of the 2 options). Use GIS Portal or measure.">
<div data-tooltip="The area you wish to study (choose one of the 2 options). Use GIS Portal or measure. Also indicate if a central (radiator-type) heating system is in use.">
<span class="tooltip_text">?</span>
</div><br>
<div class="row">
<div class="col-xl-3 col-lg-4 col-sm-3">
@ -93,7 +94,13 @@ v{{ calculator_version }} <span style="float:right; font-weight:bold">Please sen
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <label for="room_data_dimensions">Ceiling height:</label> &nbsp;&nbsp;
</div>
<input type="number" step="any" id="ceiling_height" class="non_zero col-xl-3 col-lg-5 col-md-7 col-sm-3 col-3" name="ceiling_height" placeholder="Room ceiling height (m²)" min="0" data-has-radio="#room_data_dimensions">
</div>
</div><br>
Central heating system in use:
<input type="radio" id="heating_no" name="room_heating_option" value=0 checked="checked">
<label for="heating_no">No</label>&nbsp;&nbsp;
<input type="radio" id="heating_yes" name="room_heating_option" value=1>
<label for="heating_yes">Yes</label>&nbsp;&nbsp;
<hr width="80%">

View file

@ -72,6 +72,8 @@ However, this value may be revised in the future as more studies of P.1 transmis
<h3>Room Data</h3>
<p>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 (<a href="https://gis.cern.ch/gisportal/">https://gis.cern.ch/gisportal/</a>).</p>
<h4>Room heating system</h4>
<p>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'.</p>
<h3>Ventilation type</h3>
<p>There are three main options:</p>
<h4>Mechanical ventilation</h4>

View file

@ -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:

View file

@ -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'],
),

View file

@ -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'],
),