From 84733984031a1b38006caeaa90956cfe001d7f62 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 4 Jun 2021 13:43:35 +0200 Subject: [PATCH 01/21] Using Monte-Carlo approach in calculator; hard-coding the sample size for now --- cara/apps/calculator/model_generator.py | 24 +++++++++++++----------- cara/apps/calculator/report_generator.py | 20 +++++++++++--------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index ad65795e..68b060ab 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -8,7 +8,9 @@ import numpy as np from cara import models from cara import data +import cara.monte_carlo as mc from .. import calculator +from cara.monte_carlo.data import activity_distributions, virus_distributions LOG = logging.getLogger(__name__) @@ -20,7 +22,7 @@ minutes_since_midnight = typing.NewType('minutes_since_midnight', int) # Used to declare when an attribute of a class must have a value provided, and # there should be no default value used. _NO_DEFAULT = object() - +_SAMPLE_SIZE = 50000 @dataclass class FormData: @@ -275,9 +277,9 @@ class FormData: mask = models.Mask.types[self.mask_type if self.mask_wearing_option == "mask_on" else 'No mask'] return mask - def infected_population(self) -> models.InfectedPopulation: + def infected_population(self) -> mc.InfectedPopulation: # Initializes the virus - virus = models.Virus.types[self.virus_type] + virus = virus_distributions[self.virus_type] scenario_activity_and_expiration = { 'office': ( @@ -315,12 +317,12 @@ class FormData: } [activity_defn, expiration_defn] = scenario_activity_and_expiration[self.activity_type] - activity = models.Activity.types[activity_defn] + activity = activity_distributions[activity_defn] expiration = build_expiration(expiration_defn) infected_occupants = self.infected_people - infected = models.InfectedPopulation( + infected = mc.InfectedPopulation( number=infected_occupants, virus=virus, presence=self.infected_present_interval(), @@ -330,7 +332,7 @@ class FormData: ) return infected - def exposed_population(self) -> models.Population: + def exposed_population(self) -> mc.Population: scenario_activity = { 'office': 'Seated', 'controlroom-day': 'Seated', @@ -345,14 +347,14 @@ class FormData: } activity_defn = scenario_activity[self.activity_type] - activity = models.Activity.types[activity_defn] + activity = activity_distributions[activity_defn] infected_occupants = self.infected_people # The number of exposed occupants is the total number of occupants # minus the number of infected occupants. exposed_occupants = self.total_people - infected_occupants - exposed = models.Population( + exposed = mc.Population( number=exposed_occupants, presence=self.exposed_present_interval(), activity=activity, @@ -560,14 +562,14 @@ def model_from_form(form: FormData) -> models.ExposureModel: room = models.Room(volume=volume, humidity=humidity) # Initializes and returns a model with the attributes defined above - return models.ExposureModel( - concentration_model=models.ConcentrationModel( + return mc.ExposureModel( + concentration_model=mc.ConcentrationModel( room=room, ventilation=form.ventilation(), infected=form.infected_population(), ), exposed=form.exposed_population() - ) + ).build_model(size=_SAMPLE_SIZE) def baseline_raw_form_data(): diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index ea22cbb6..6cead28c 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -38,12 +38,13 @@ def calculate_report_data(model: models.ExposureModel): t_start, t_end = model_start_end(model) times = list(np.linspace(t_start, t_end, resolution)) - concentrations = [model.concentration_model.concentration(time) for time in times] + concentrations = [np.mean(model.concentration_model.concentration(time)) + for time in times] highest_const = max(concentrations) - prob = model.infection_probability() - er = model.concentration_model.infected.emission_rate_when_present() + prob = np.mean(model.infection_probability()) + er = np.mean(model.concentration_model.infected.emission_rate_when_present()) exposed_occupants = model.exposed.number - expected_new_cases = model.expected_new_cases() + expected_new_cases = np.mean(model.expected_new_cases()) repeated_events = [] for n in [1, 2, 3, 4, 5]: @@ -52,8 +53,8 @@ def calculate_report_data(model: models.ExposureModel): repeated_events.append( RepeatEvents( repeats=n, - probability_of_infection=repeat_model.infection_probability(), - expected_new_cases=repeat_model.expected_new_cases(), + probability_of_infection=np.mean(repeat_model.infection_probability()), + expected_new_cases=np.mean(repeat_model.expected_new_cases()), ) ) @@ -243,7 +244,8 @@ def comparison_plot(scenarios: typing.Dict[str, models.ExposureModel]): t_start, t_end = model_start_end(model) times = np.linspace(t_start, t_end, resolution) datetimes = [datetime(1970, 1, 1) + timedelta(hours=time) for time in times] - concentrations = [model.concentration_model.concentration(time) for time in times] + concentrations = [np.mean(model.concentration_model.concentration(time)) + for time in times] if name in dash_styled_scenarios: ax.plot(datetimes, concentrations, label=name, linestyle='--') @@ -267,8 +269,8 @@ def comparison_report(scenarios: typing.Dict[str, models.ExposureModel]): statistics = {} for name, model in scenarios.items(): statistics[name] = { - 'probability_of_infection': model.infection_probability(), - 'expected_new_cases': model.expected_new_cases(), + 'probability_of_infection': np.mean(model.infection_probability()), + 'expected_new_cases': np.mean(model.expected_new_cases()), } return { 'plot': img2base64(_figure2bytes(comparison_plot(scenarios))), From 3a8c20a38a840b3a72975cddba0eafacdb9e1756 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 4 Jun 2021 13:45:18 +0200 Subject: [PATCH 02/21] Adding sklearn to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 1bbecfe1..f2133fa2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,6 +62,7 @@ python-dateutil==2.8.1 pyzmq==22.0.3 qrcode==6.1 scipy==1.5.4 +sklearn==0.24.1 Send2Trash==1.5.0 six==1.15.0 sniffio==1.2.0 From 71b59357fad2cd7d1b6eb972f2dc7129c4c9fef2 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 4 Jun 2021 14:16:35 +0200 Subject: [PATCH 03/21] Adding 0.25 ACH to all ventilation schemes in calculator; changing modele_generator tests accordingly --- cara/apps/calculator/model_generator.py | 8 ++++++-- cara/tests/apps/calculator/test_model_generator.py | 10 +++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index 68b060ab..1d4f51ec 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -265,11 +265,15 @@ class FormData: ventilation = models.HVACMechanical( active=always_on, q_air_mech=self.air_supply) + # this is a minimal, always present source of ventilation, due + # to the air infiltration from the outside. + # See CERN-OPEN-2021-004, p. 12. + infiltration_ventilation = models.AirChange(active=always_on, air_exch=0.25) if self.hepa_option: hepa = models.HEPAFilter(active=always_on, q_air_mech=self.hepa_amount) - return models.MultipleVentilation((ventilation, hepa)) + return models.MultipleVentilation((ventilation, hepa, infiltration_ventilation)) else: - return ventilation + return models.MultipleVentilation((ventilation, infiltration_ventilation)) def mask(self) -> models.Mask: # Initializes the mask type if mask wearing is "continuous", otherwise instantiates the mask attribute as diff --git a/cara/tests/apps/calculator/test_model_generator.py b/cara/tests/apps/calculator/test_model_generator.py index eb216727..53045564 100644 --- a/cara/tests/apps/calculator/test_model_generator.py +++ b/cara/tests/apps/calculator/test_model_generator.py @@ -50,7 +50,7 @@ def test_ventilation_slidingwindow(baseline_form: model_generator.FormData): baseline_form.opening_distance = 0.6 ts = np.linspace(8, 16, 100) - np.testing.assert_allclose([window.air_exchange(room, t) for t in ts], + np.testing.assert_allclose([window.air_exchange(room, t)+0.25 for t in ts], [baseline_form.ventilation().air_exchange(room, t) for t in ts]) @@ -73,7 +73,7 @@ def test_ventilation_hingedwindow(baseline_form: model_generator.FormData): baseline_form.opening_distance = 0.6 ts = np.linspace(8, 16, 100) - np.testing.assert_allclose([window.air_exchange(room, t) for t in ts], + np.testing.assert_allclose([window.air_exchange(room, t)+0.25 for t in ts], [baseline_form.ventilation().air_exchange(room, t) for t in ts]) @@ -88,7 +88,7 @@ def test_ventilation_mechanical(baseline_form: model_generator.FormData): baseline_form.air_supply = 500. ts = np.linspace(8, 16, 100) - np.testing.assert_allclose([mech.air_exchange(room, t) for t in ts], + np.testing.assert_allclose([mech.air_exchange(room, t)+0.25 for t in ts], [baseline_form.ventilation().air_exchange(room, t) for t in ts]) @@ -103,7 +103,7 @@ def test_ventilation_airchanges(baseline_form: model_generator.FormData): baseline_form.air_changes = 3. ts = np.linspace(8, 16, 100) - np.testing.assert_allclose([airchange.air_exchange(room, t) for t in ts], + np.testing.assert_allclose([airchange.air_exchange(room, t)+0.25 for t in ts], [baseline_form.ventilation().air_exchange(room, t) for t in ts]) @@ -131,7 +131,7 @@ def test_ventilation_window_hepa(baseline_form: model_generator.FormData): baseline_form.hepa_option = True ts = np.linspace(9, 17, 100) - np.testing.assert_allclose([ventilation.air_exchange(room, t) for t in ts], + np.testing.assert_allclose([ventilation.air_exchange(room, t)+0.25 for t in ts], [baseline_form.ventilation().air_exchange(room, t) for t in ts]) From f672ee4208225b37c9bd8994db1eabf283ee538f Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Fri, 4 Jun 2021 12:25:29 +0000 Subject: [PATCH 04/21] change CERN theme text for COVID measures --- .../calculator/themes/cern/templates/calculator.report.html.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cara/apps/calculator/themes/cern/templates/calculator.report.html.j2 b/cara/apps/calculator/themes/cern/templates/calculator.report.html.j2 index 4d6769f8..85d8860b 100644 --- a/cara/apps/calculator/themes/cern/templates/calculator.report.html.j2 +++ b/cara/apps/calculator/themes/cern/templates/calculator.report.html.j2 @@ -3,7 +3,7 @@ {% block report_preamble %}

Applicable rules:
- Please ensure that this scenario conforms to current CERN HSE rules (minimum ventilation requirements, mask wearing and the maximum number of people permitted in a space).

+ Please ensure that this scenario conforms to current COVID-related Health & Safety requirements, under the applicable COVID Scale and measures in force at the time of the CARA assessment.
The results of this simulation are colour coded according to the risk values authorized at CERN (approved in December 2020):

  • Events with a P(i) less than 5% may go ahead without further mitigation measures.
  • Events with a P(i) between 5% and 15% shall be subject to ALARA principles (see footnote) to minimise the risk before proceeding.
  • From c7c2969bb891d2329df83553554f3bf15b55971f Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Fri, 4 Jun 2021 12:38:36 +0000 Subject: [PATCH 05/21] update of report text --- cara/apps/calculator/templates/base/calculator.report.html.j2 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/calculator/templates/base/calculator.report.html.j2 index 9b618988..2ad82bfa 100644 --- a/cara/apps/calculator/templates/base/calculator.report.html.j2 +++ b/cara/apps/calculator/templates/base/calculator.report.html.j2 @@ -209,7 +209,8 @@

    Results:

    {% block report_summary %} - In this scenario, the estimated probability of one exposed occupant getting infected P(i) is {{ prob_inf | non_zero_percentage }} and the expected number of new cases is {{ expected_new_cases | float_format }}. + Taking into account the uncertainties tied to the model variables, in this scenario, the probability of one exposed occupant getting infected P(i) is {{ prob_inf | non_zero_percentage }}[*] and the expected number of new cases is {{ expected_new_cases | float_format }}. +

    [*] The results are based on the parameters and assumptions published in the CERN Open Report CERN-OPEN-2021-004

    {% endblock report_summary %}

    Exposure graph:

    From 1d34db3a81e7fb99ce08dfaa1ca1efcd1766c0bf Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Fri, 4 Jun 2021 12:40:37 +0000 Subject: [PATCH 06/21] delete repeated events section --- .../templates/base/calculator.report.html.j2 | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/calculator/templates/base/calculator.report.html.j2 index 2ad82bfa..4896ebf0 100644 --- a/cara/apps/calculator/templates/base/calculator.report.html.j2 +++ b/cara/apps/calculator/templates/base/calculator.report.html.j2 @@ -216,30 +216,6 @@

    Exposure graph:

    -

    Repeated events:

    -

    - The P(i) and expected number of new cases if repeating this scenario event - provided the infected person emits the same amount of viruses each day and the exposed person is subject to the same daily exposure time: - - - - - - - - - - - {% for repeat_event in repeated_events %} - - - - - - {% endfor %} - -
    # of repeated eventsP(i)Expected new cases
    {{ repeat_event.repeats }}{{ repeat_event.probability_of_infection | non_zero_percentage }}{{ repeat_event.expected_new_cases | float_format }}
    -

    -

    Alternative scenarios:

    From 8d55eaa632045ddb7c20299197562e551166a9f3 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 4 Jun 2021 14:55:22 +0200 Subject: [PATCH 07/21] Improving the reference values used for the test on the temperature time discretization (using a very fine mesh to compute the reference); decreasing the level of accuracy required for this test --- cara/tests/test_known_quantities.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index ab24bb08..d8597cf8 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -375,12 +375,13 @@ def test_quanta_hourly_dep(month,expected_quanta): npt.assert_allclose(quanta, expected_quanta) # expected quanta were computed with a trapezoidal integration, using -# a mesh of 100'000 pts per exposed presence interval. +# a mesh of 100'000 pts per exposed presence interval and 25 pts per hour +# for the temperature discretization. @pytest.mark.parametrize( "month, expected_quanta", [ - ['Jan', 9.989881], - ['Jun', 39.99636], + ['Jan', 9.993842], + ['Jun', 40.151985], ], ) def test_quanta_hourly_dep_refined(month,expected_quanta): @@ -393,4 +394,4 @@ def test_quanta_hourly_dep_refined(month,expected_quanta): ) ) quanta = m.quanta_exposure() - npt.assert_allclose(quanta, expected_quanta) + npt.assert_allclose(quanta, expected_quanta, rtol=0.02) From 50a36140ab87d76d70376427d80190dde6e2fb18 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 4 Jun 2021 15:00:02 +0200 Subject: [PATCH 08/21] Decreasing (still to an acceptable level) the accuracy of the mode - less points per hour for the temperature, and less sample points in model_generator --- cara/apps/calculator/model_generator.py | 2 +- cara/data.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index 1d4f51ec..4baddaf7 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -22,7 +22,7 @@ minutes_since_midnight = typing.NewType('minutes_since_midnight', int) # Used to declare when an attribute of a class must have a value provided, and # there should be no default value used. _NO_DEFAULT = object() -_SAMPLE_SIZE = 50000 +_SAMPLE_SIZE = 10000 @dataclass class FormData: diff --git a/cara/data.py b/cara/data.py index f84e627e..61f103b9 100644 --- a/cara/data.py +++ b/cara/data.py @@ -38,7 +38,7 @@ GenevaTemperatures_hourly = { } # same temperatures on a finer temperature mesh GenevaTemperatures = { - month: GenevaTemperatures_hourly[month].refine(refine_factor=10) + month: GenevaTemperatures_hourly[month].refine(refine_factor=4) for month,temperatures in Geneva_hourly_temperatures_celsius_per_hour.items() } From 6e223050fbcd4ddd57ebe9137c66ab72970650b1 Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Fri, 4 Jun 2021 13:08:03 +0000 Subject: [PATCH 09/21] update C(t) plot labels --- cara/apps/calculator/report_generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index 6cead28c..8ae90534 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -128,13 +128,13 @@ def plot(times, concentrations, model: models.ExposureModel): fig = plt.figure() ax = fig.add_subplot(1, 1, 1) datetimes = [datetime(1970, 1, 1) + timedelta(hours=time) for time in times] - ax.plot(datetimes, concentrations, lw=2, color='#1f77b4', label='Concentration') + ax.plot(datetimes, concentrations, lw=2, color='#1f77b4', label='Mean concentration') ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) ax.set_xlabel('Time of day') - ax.set_ylabel('Concentration ($q/m^3$)') - ax.set_title('Concentration of infectious quanta') + ax.set_ylabel('Mean concentration ($q/m^3$)') + ax.set_title('Mean concentration of infectious quanta') ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter("%H:%M")) # Plot presence of exposed person From 8dc8e8f42ab64d6bf3e62e8abe35682b4e3c1653 Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Fri, 4 Jun 2021 13:17:45 +0000 Subject: [PATCH 10/21] delete 'Exposure Graph' title --- .../calculator/templates/base/calculator.report.html.j2 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/calculator/templates/base/calculator.report.html.j2 index 4896ebf0..174a85eb 100644 --- a/cara/apps/calculator/templates/base/calculator.report.html.j2 +++ b/cara/apps/calculator/templates/base/calculator.report.html.j2 @@ -210,11 +210,11 @@

    {% block report_summary %} Taking into account the uncertainties tied to the model variables, in this scenario, the probability of one exposed occupant getting infected P(i) is {{ prob_inf | non_zero_percentage }}[*] and the expected number of new cases is {{ expected_new_cases | float_format }}. -

    [*] The results are based on the parameters and assumptions published in the CERN Open Report CERN-OPEN-2021-004

    +

    [*] The results are based on the parameters and assumptions published in the CERN Open Report CERN-OPEN-2021-004

    {% endblock report_summary %}

    -

    Exposure graph:

    - + +

    Alternative scenarios:

    From 30923e490fc4ecf0d0942a1dd8d0f3e1431b1137 Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Fri, 4 Jun 2021 13:25:52 +0000 Subject: [PATCH 11/21] text edit in alternative scenario notes --- cara/apps/calculator/templates/base/calculator.report.html.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/calculator/templates/base/calculator.report.html.j2 index 174a85eb..58a4096e 100644 --- a/cara/apps/calculator/templates/base/calculator.report.html.j2 +++ b/cara/apps/calculator/templates/base/calculator.report.html.j2 @@ -246,7 +246,7 @@

    Notes for alternative scenarios:

    1. This graph shows the concentration of infectious quanta in the air. The filtration of Type I and FFP2 masks, if worn, applies not only to the emission rate but also to the individual exposure (i.e. inhalation). - For this reason, scenarios with different types of mask will show the same concentration on the graph but have different Pi values.
    2. + For this reason, scenarios with different types of mask will show the same concentration on the graph but have different absorbed doses and infection probabilities.
    3. If you have selected more sophisticated options, such as HEPA filtration or FFP2 masks, this will be indicated in the plot as the "base scenario", representing the inputs inserted in the form.
      The other alternative scenarios shown for comparison will not include either HEPA filtration or FFP2 masks.
    From 4241b1c061f4efb7847c43133f7592a9e2c76402 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 4 Jun 2021 17:53:50 +0200 Subject: [PATCH 12/21] Increasing the timeout in tests to 20 seconds; reverting to more accurate sample size & temperature refinement factor (resp. 50000 and 10); changing a few strings in the tests on the report text, that was modified by previous commits --- cara/apps/calculator/model_generator.py | 2 +- cara/data.py | 2 +- .../tests/apps/calculator/test_report_generator.py | 2 +- cara/tests/apps/calculator/test_webapp.py | 14 ++++++++------ cara/tests/test_known_quantities.py | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index 4baddaf7..1d4f51ec 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -22,7 +22,7 @@ minutes_since_midnight = typing.NewType('minutes_since_midnight', int) # Used to declare when an attribute of a class must have a value provided, and # there should be no default value used. _NO_DEFAULT = object() -_SAMPLE_SIZE = 10000 +_SAMPLE_SIZE = 50000 @dataclass class FormData: diff --git a/cara/data.py b/cara/data.py index 61f103b9..f84e627e 100644 --- a/cara/data.py +++ b/cara/data.py @@ -38,7 +38,7 @@ GenevaTemperatures_hourly = { } # same temperatures on a finer temperature mesh GenevaTemperatures = { - month: GenevaTemperatures_hourly[month].refine(refine_factor=4) + month: GenevaTemperatures_hourly[month].refine(refine_factor=10) for month,temperatures in Geneva_hourly_temperatures_celsius_per_hour.items() } diff --git a/cara/tests/apps/calculator/test_report_generator.py b/cara/tests/apps/calculator/test_report_generator.py index da648635..75733d5f 100644 --- a/cara/tests/apps/calculator/test_report_generator.py +++ b/cara/tests/apps/calculator/test_report_generator.py @@ -11,7 +11,7 @@ def test_generate_report(baseline_form): # generate a report for it. Because this is what happens in the cara # calculator, we confirm that the generation happens within a reasonable # time threshold. - time_limit: float = 5.0 # seconds + time_limit: float = 20.0 # seconds start = time.perf_counter() diff --git a/cara/tests/apps/calculator/test_webapp.py b/cara/tests/apps/calculator/test_webapp.py index d8a8d6d7..9ee1a703 100644 --- a/cara/tests/apps/calculator/test_webapp.py +++ b/cara/tests/apps/calculator/test_webapp.py @@ -45,11 +45,12 @@ class TestBasicApp(tornado.testing.AsyncHTTPTestCase): def get_app(self): return cara.apps.calculator.make_app() + @tornado.testing.gen_test(timeout=20) def test_report(self): - response = self.fetch('/calculator/baseline-model/result') + response = yield self.http_client.fetch(self.get_url('/calculator/baseline-model/result')) self.assertEqual(response.code, 200) - assert 'CERN HSE rules' not in response.body.decode() - assert 'the expected number of new cases is' in response.body.decode() + assert 'CERN HSE' not in response.body.decode() + assert 'expected number of new cases is' in response.body.decode() class TestCernApp(tornado.testing.AsyncHTTPTestCase): @@ -57,11 +58,12 @@ class TestCernApp(tornado.testing.AsyncHTTPTestCase): cern_theme = Path(cara.apps.calculator.__file__).parent / 'themes' / 'cern' return cara.apps.calculator.make_app(theme_dir=cern_theme) + @tornado.testing.gen_test(timeout=20) def test_report(self): - response = self.fetch('/calculator/baseline-model/result') + response = yield self.http_client.fetch(self.get_url('/calculator/baseline-model/result')) self.assertEqual(response.code, 200) - assert 'CERN HSE rules' in response.body.decode() - assert 'the expected number of new cases is' in response.body.decode() + assert 'CERN HSE' in response.body.decode() + assert 'expected number of new cases is' in response.body.decode() async def test_qrcode_urls(http_server_client, baseline_form): diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index d8597cf8..2577c837 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -394,4 +394,4 @@ def test_quanta_hourly_dep_refined(month,expected_quanta): ) ) quanta = m.quanta_exposure() - npt.assert_allclose(quanta, expected_quanta, rtol=0.02) + npt.assert_allclose(quanta, expected_quanta, rtol=0.01) From 5b74538fb6c1aed393597bab2f2b12c0d238451b Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Fri, 4 Jun 2021 18:09:37 +0200 Subject: [PATCH 13/21] Setting timeout to 30 seconds --- cara/tests/apps/calculator/test_report_generator.py | 2 +- cara/tests/apps/calculator/test_webapp.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cara/tests/apps/calculator/test_report_generator.py b/cara/tests/apps/calculator/test_report_generator.py index 75733d5f..44131d3a 100644 --- a/cara/tests/apps/calculator/test_report_generator.py +++ b/cara/tests/apps/calculator/test_report_generator.py @@ -11,7 +11,7 @@ def test_generate_report(baseline_form): # generate a report for it. Because this is what happens in the cara # calculator, we confirm that the generation happens within a reasonable # time threshold. - time_limit: float = 20.0 # seconds + time_limit: float = 30.0 # seconds start = time.perf_counter() diff --git a/cara/tests/apps/calculator/test_webapp.py b/cara/tests/apps/calculator/test_webapp.py index 9ee1a703..fcb8c9b4 100644 --- a/cara/tests/apps/calculator/test_webapp.py +++ b/cara/tests/apps/calculator/test_webapp.py @@ -6,6 +6,7 @@ import tornado.testing import cara.apps.calculator from cara.apps.calculator.report_generator import generate_qr_code +_TIMEOUT = 30. @pytest.fixture def app(): @@ -45,7 +46,7 @@ class TestBasicApp(tornado.testing.AsyncHTTPTestCase): def get_app(self): return cara.apps.calculator.make_app() - @tornado.testing.gen_test(timeout=20) + @tornado.testing.gen_test(timeout=_TIMEOUT) def test_report(self): response = yield self.http_client.fetch(self.get_url('/calculator/baseline-model/result')) self.assertEqual(response.code, 200) @@ -58,7 +59,7 @@ class TestCernApp(tornado.testing.AsyncHTTPTestCase): cern_theme = Path(cara.apps.calculator.__file__).parent / 'themes' / 'cern' return cara.apps.calculator.make_app(theme_dir=cern_theme) - @tornado.testing.gen_test(timeout=20) + @tornado.testing.gen_test(timeout=_TIMEOUT) def test_report(self): response = yield self.http_client.fetch(self.get_url('/calculator/baseline-model/result')) self.assertEqual(response.code, 200) From 56ecf150a2cddc7791eff5ac839c75d603b9b2ce Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Sat, 5 Jun 2021 18:36:06 +0200 Subject: [PATCH 14/21] Fixing on Monte-Carlo test (type issue) --- cara/tests/test_monte_carlo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cara/tests/test_monte_carlo.py b/cara/tests/test_monte_carlo.py index c3bfc2fe..23e139fd 100644 --- a/cara/tests/test_monte_carlo.py +++ b/cara/tests/test_monte_carlo.py @@ -76,7 +76,9 @@ def test_build_concentration_model(baseline_mc_model: cara.monte_carlo.Concentra model = baseline_mc_model.build_model(7) assert isinstance(model, cara.models.ConcentrationModel) assert isinstance(model.concentration(time=0), float) - assert model.concentration(time=1).shape == (7, ) + conc = model.concentration(time=1) + assert isinstance(conc, np.ndarray) + assert conc.shape == (7, ) def test_build_exposure_model(baseline_mc_exposure_model: cara.monte_carlo.ExposureModel): From 17956d766daba582ea093d9a9a1f9365b3b0ace4 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Sat, 5 Jun 2021 18:38:57 +0200 Subject: [PATCH 15/21] Reverting to a refinement factor of 4 in the time-dependence of Geneva temperature; optimizing the caching; decreasing the timeout value in tests --- cara/data.py | 2 +- cara/models.py | 8 ++++---- cara/tests/apps/calculator/test_report_generator.py | 2 +- cara/tests/apps/calculator/test_webapp.py | 2 +- cara/tests/test_known_quantities.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cara/data.py b/cara/data.py index f84e627e..61f103b9 100644 --- a/cara/data.py +++ b/cara/data.py @@ -38,7 +38,7 @@ GenevaTemperatures_hourly = { } # same temperatures on a finer temperature mesh GenevaTemperatures = { - month: GenevaTemperatures_hourly[month].refine(refine_factor=10) + month: GenevaTemperatures_hourly[month].refine(refine_factor=4) for month,temperatures in Geneva_hourly_temperatures_celsius_per_hour.items() } diff --git a/cara/models.py b/cara/models.py index 1561b09d..ad4493ba 100644 --- a/cara/models.py +++ b/cara/models.py @@ -711,7 +711,6 @@ class InfectedPopulation(Population): return self.emission_rate_when_present() - @cached() def emission_rate(self, time) -> _VectorisedFloat: """ The emission rate of the entire population. @@ -741,7 +740,6 @@ class ConcentrationModel: return k + self.virus.decay_constant(self.room.humidity ) + self.ventilation.air_exchange(self.room, time) - @cached() def _concentration_limit(self, time: float) -> _VectorisedFloat: """ Provides a constant that represents the theoretical asymptotic @@ -753,7 +751,6 @@ class ConcentrationModel: return (self.infected.emission_rate(time)) / (IVRR * V) - @cached() def state_change_times(self): """ All time dependent entities on this model must provide information about @@ -798,6 +795,9 @@ class ConcentrationModel: return (self.last_state_change(stop) <= start) @cached() + def _concentration_at_state_change(self, time: float) -> _VectorisedFloat: + return self.concentration(time) + def concentration(self, time: float) -> _VectorisedFloat: """ Virus quanta concentration, as a function of time. @@ -816,7 +816,7 @@ class ConcentrationModel: concentration_limit = self._concentration_limit(next_state_change_time) t_last_state_change = self.last_state_change(time) - concentration_at_last_state_change = self.concentration(t_last_state_change) + concentration_at_last_state_change = self._concentration_at_state_change(t_last_state_change) delta_time = time - t_last_state_change fac = np.exp(-IVRR * delta_time) diff --git a/cara/tests/apps/calculator/test_report_generator.py b/cara/tests/apps/calculator/test_report_generator.py index 44131d3a..75733d5f 100644 --- a/cara/tests/apps/calculator/test_report_generator.py +++ b/cara/tests/apps/calculator/test_report_generator.py @@ -11,7 +11,7 @@ def test_generate_report(baseline_form): # generate a report for it. Because this is what happens in the cara # calculator, we confirm that the generation happens within a reasonable # time threshold. - time_limit: float = 30.0 # seconds + time_limit: float = 20.0 # seconds start = time.perf_counter() diff --git a/cara/tests/apps/calculator/test_webapp.py b/cara/tests/apps/calculator/test_webapp.py index fcb8c9b4..0a22fe59 100644 --- a/cara/tests/apps/calculator/test_webapp.py +++ b/cara/tests/apps/calculator/test_webapp.py @@ -6,7 +6,7 @@ import tornado.testing import cara.apps.calculator from cara.apps.calculator.report_generator import generate_qr_code -_TIMEOUT = 30. +_TIMEOUT = 20. @pytest.fixture def app(): diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index 2577c837..d8597cf8 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -394,4 +394,4 @@ def test_quanta_hourly_dep_refined(month,expected_quanta): ) ) quanta = m.quanta_exposure() - npt.assert_allclose(quanta, expected_quanta, rtol=0.01) + npt.assert_allclose(quanta, expected_quanta, rtol=0.02) From cf817d0eb72e5aab05f6cbb1bb2a4d4e9181d07c Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Sat, 5 Jun 2021 19:08:46 +0200 Subject: [PATCH 16/21] Modifying sklearn version in requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f2133fa2..d7f8affc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,7 +62,7 @@ python-dateutil==2.8.1 pyzmq==22.0.3 qrcode==6.1 scipy==1.5.4 -sklearn==0.24.1 +sklearn==0.23.1 Send2Trash==1.5.0 six==1.15.0 sniffio==1.2.0 From 20a288a8876a8a7bf172a80d69b8317b0fa5d215 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Sat, 5 Jun 2021 17:18:14 +0000 Subject: [PATCH 17/21] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d7f8affc..eac04f3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,7 +62,7 @@ python-dateutil==2.8.1 pyzmq==22.0.3 qrcode==6.1 scipy==1.5.4 -sklearn==0.23.1 +scikit_learn==0.23.1 Send2Trash==1.5.0 six==1.15.0 sniffio==1.2.0 From 263330796aa899eeeffde8eea38d1bb39456e946 Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Sat, 5 Jun 2021 19:34:30 +0000 Subject: [PATCH 18/21] update text format in report --- .../calculator/templates/base/calculator.report.html.j2 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/calculator/templates/base/calculator.report.html.j2 index 58a4096e..cb57d045 100644 --- a/cara/apps/calculator/templates/base/calculator.report.html.j2 +++ b/cara/apps/calculator/templates/base/calculator.report.html.j2 @@ -209,8 +209,8 @@

    Results:

    {% block report_summary %} - Taking into account the uncertainties tied to the model variables, in this scenario, the probability of one exposed occupant getting infected P(i) is {{ prob_inf | non_zero_percentage }}[*] and the expected number of new cases is {{ expected_new_cases | float_format }}. -

    [*] The results are based on the parameters and assumptions published in the CERN Open Report CERN-OPEN-2021-004

    + Taking into account the uncertainties tied to the model variables, in this scenario, the probability of one exposed occupant getting infected is {{ prob_inf | non_zero_percentage }}[*] and the expected number of new cases is {{ expected_new_cases | float_format }}. +

    [*] The results are based on the parameters and assumptions published in the CERN Open Report CERN-OPEN-2021-004

    {% endblock report_summary %}

    @@ -225,7 +225,7 @@ Scenario - P(i) + P(I) Expected new cases From 527a6263970b1c7b8e9e48af164d746b46e932a5 Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Sat, 5 Jun 2021 19:36:25 +0000 Subject: [PATCH 19/21] update labels in 'alternative scenarios' plot --- cara/apps/calculator/report_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index 8ae90534..4029e54f 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -259,8 +259,8 @@ def comparison_plot(scenarios: typing.Dict[str, models.ExposureModel]): ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter("%H:%M")) ax.set_xlabel('Time of day') - ax.set_ylabel('Concentration ($q/m^3$)') - ax.set_title('Concentration of infectious quanta') + ax.set_ylabel('Mean concentration ($q/m^3$)') + ax.set_title('Mean concentration of infectious quanta') return fig From a6bb522de9b601c06b5cb7b125ba72f7f3639af1 Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Mon, 7 Jun 2021 12:19:44 +0000 Subject: [PATCH 20/21] Update userguide for mc --- .../apps/calculator/templates/userguide.html.j2 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/cara/apps/calculator/templates/userguide.html.j2 b/cara/apps/calculator/templates/userguide.html.j2 index 013a1763..f519a9bb 100644 --- a/cara/apps/calculator/templates/userguide.html.j2 +++ b/cara/apps/calculator/templates/userguide.html.j2 @@ -8,7 +8,7 @@

    This is a guide to help you use the calculator app. If you are using the expert version of the tool, you should look at the expert notes.

    For more information on the Airborne Transmission of SARS-CoV-2, feel free to check out the HSE Seminar: https://cds.cern.ch/record/2743403

    -

    The methodology, mathematical equations and parameters of the model are described here: https://edms.cern.ch/ui/file/2566402/1/CARA_Deterministic_parameters_2020.pdf

    +

    The methodology, mathematical equations and parameters of the model are described here in the CERN Report: CERN-OPEN-2021-004

    Disclaimer

    @@ -172,26 +172,25 @@ Please check what are the applicable rules, before deciding which assumptions ar Please confirm what are the applicable rules, before deciding which assumptions are used for the simulation

    For the time being only the Type 1 surgical and FFP2 masks can be selected.

    Generate Report

    -

    When you have entered all the necessary information, please click on the Generate Report button to execute the model.

    +

    When you have entered all the necessary information, please click on the Generate Report button to execute the model. With the implementation of Monte Carlo simulations, the browser might take a few secounds to react.

    Report

    The report will open in your web browser. It contains a summary of all the input data, which will allow the simulation to be repeated if required in the future as we improve the model.

    Results

    -

    This part of the report shows the P(i) or probability of one exposed person getting infected. +

    This part of the report shows the P(I) or probability of one exposed person getting infected. It is estimated based on the emission rate of virus into the simulated volume, and the amount which is inhaled by exposed individuals. -This probability is valid for the simulation duration - i.e. if you have simulated one day and plan to work 5 days in these conditions and the infected person emits the same amount of virus each day, the cumulative probability of infection is (1-(1-P(i))^5). +This probability is valid for the simulation duration - i.e. the start and end time. If you are using the natural ventilation option, the simulation is only valid for the selected month, because the following or preceding month will have a different average temperature profile. The expected number of new cases for the simulation is calculated based on the probability of infection, multiplied by the number of exposed occupants.

    -

    Exposure graph

    -

    The graph shows the variation in the concentration of infectious quanta (one quanta is the amount of inhaled virus that can cause infection in 63% of the exposed occupants) within the simulated volume. +

    The graph shows the variation in the concentration of infectious viruses within the simulated volume. It is determined by:

    • The presence of the infected person, who emits airborne viruses in the volume.
    • -
    • The emission rate is related to the type of activity of the infected person (sitting, light exercise), their level of vocalisation (breathing, whispering or talking).
    • +
    • The emission rate is related to the type of activity of the infected person (sitting, light exercise), their level of vocalisation (breathing, talking or shouting).
    • The accumulation of infectious quanta in the volume, which is driven, among other factors, by ventilation (if applicable).
      • In a mechanical ventilation scenario, the removal rate is constant, based on fresh airflow supply in and out of the simulated space.
      • Under natural ventilation conditions, the effectiveness of ventilation relies upon the hourly temperature difference between the inside and outside air temperature.
      • -
      • A HEPA filter removes infectious quanta from the air at a constant rate and is modelled in the same way as mechanical ventilation, however air passed through a HEPA filter is recycled (i.e. it is not fresh air).
      • +
      • A HEPA filter removes infectious virus from the air at a constant rate and is modelled in the same way as mechanical ventilation, however air passed through a HEPA filter is recycled (i.e. it is not fresh air).
    @@ -204,7 +203,7 @@ This allows for:

Conclusion

This tool provides informative comparisons for COVID-19 (long-range) airborne risk only - see Disclaimer -If you have any comments on your experience with the app, or feedback for potential improvements, please share them with the development team at cara-dev@cern.ch.

+If you have any comments on your experience with the app, or feedback for potential improvements, please share them with the development team Send email.

{% endblock contents %} From 6a3751c9e6756bd2bab0724d8168030834be0b01 Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Mon, 7 Jun 2021 12:22:57 +0000 Subject: [PATCH 21/21] Update about page for mc --- cara/apps/templates/about.html.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cara/apps/templates/about.html.j2 b/cara/apps/templates/about.html.j2 index 3ff6222d..56d0a307 100644 --- a/cara/apps/templates/about.html.j2 +++ b/cara/apps/templates/about.html.j2 @@ -17,7 +17,7 @@ CARA stands for COVID Airborne Risk Assessment and was developed in the spring o The mathematical and physical model simulate the long-range airborne spread of SARS-CoV-2 virus in a finite volume, assuming a homogenous mixture, and estimates the risk of COVID-19 infection therein. The results DO NOT include short-range airborne exposure (where the physical distance plays a factor) nor the other known modes of SARS-CoV-2 transmission. Hence, the output from this model is only valid when the other recommended public health & safety instructions are observed, such as adequate physical distancing, good hand hygiene and other barrier measures.
-

The methodology, mathematical equations and parameters of the model are described here: https://edms.cern.ch/ui/file/2566402/1/CARA_Deterministic_parameters_2020.pdf.

+

The methodology, mathematical equations and parameters of the model are described here in the CERN Report: CERN-OPEN-2021-004.

The model used is based on scientific publications relating to airborne transmission of infectious diseases, virology, epidemiology and aerosol science. It can be used to compare the effectiveness of different airborne-related risk mitigation measures.