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:
commit
2a3c3eb138
7 changed files with 55 additions and 26 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<label for="room_data_dimensions">Ceiling height:</label>
|
||||
</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>
|
||||
<input type="radio" id="heating_yes" name="room_heating_option" value=1>
|
||||
<label for="heating_yes">Yes</label>
|
||||
|
||||
<hr width="80%">
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue