From 6ec0c48fa702ae20aa858602c91d313f721c3ae7 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Wed, 26 May 2021 08:19:52 +0000 Subject: [PATCH] Adding central heating option in calculator (including tooltip and user guide - thanks to J. Devine) --- cara/apps/calculator/model_generator.py | 9 +++- .../templates/base/calculator.report.html.j2 | 1 + .../templates/calculator.form.html.j2 | 11 ++++- .../calculator/templates/userguide.html.j2 | 2 + cara/models.py | 47 ++++++++++++------- cara/tests/models/test_concentration_model.py | 9 ++-- cara/tests/test_infected_population.py | 2 - 7 files changed, 55 insertions(+), 26 deletions(-) diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index 006ec25c..fb34392d 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -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', diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/calculator/templates/base/calculator.report.html.j2 index 27bdacf9..9b618988 100644 --- a/cara/apps/calculator/templates/base/calculator.report.html.j2 +++ b/cara/apps/calculator/templates/base/calculator.report.html.j2 @@ -46,6 +46,7 @@ {% endif %}

  • Room Volume: {{ model.concentration_model.room.volume }} m³

  • +
  • Room Central Heating: {{ "On" if form.room_heating_option else "Off" }}

  • Ventilation data:

    diff --git a/cara/apps/calculator/templates/calculator.form.html.j2 b/cara/apps/calculator/templates/calculator.form.html.j2 index 254ea53c..b03dc47d 100644 --- a/cara/apps/calculator/templates/calculator.form.html.j2 +++ b/cara/apps/calculator/templates/calculator.form.html.j2 @@ -69,9 +69,10 @@ v{{ calculator_version }} Please sen
    Room data: -
    +
    ?

    +
    @@ -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 291dcfc1..4da4478c 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 0.001 and 0.01 to have a 50% chance # to cause infection. i.e. 1000 or 100 SARS-CoV viruses to cause infection. coefficient_of_infectivity=0.02, ), - 'SARS_CoV_2_B117': Virus( + 'SARS_CoV_2_B117': SARSCoV2( # also called VOC-202012/01 - halflife=1.1, viral_load_in_sputum=1e9, coefficient_of_infectivity=1/30., ), - 'SARS_CoV_2_P1': Virus( - halflife=1.1, + 'SARS_CoV_2_P1': SARSCoV2( viral_load_in_sputum=1e9, coefficient_of_infectivity=0.045, ), @@ -643,9 +659,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 52fb0546..4d29169d 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])}, {'coefficient_of_infectivity': np.array([0.02, 0.05])}, {'η_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, 'coefficient_of_infectivity': 0.02, 'η_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'], coefficient_of_infectivity=defaults['coefficient_of_infectivity'], ), diff --git a/cara/tests/test_infected_population.py b/cara/tests/test_infected_population.py index 39fbc039..e0ed35e3 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, 'coefficient_of_infectivity': 0.02, 'η_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'], coefficient_of_infectivity=defaults['coefficient_of_infectivity'], ),