From dd60c0ef2371acc8f0d92b0100338d0962d4af09 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Fri, 20 Nov 2020 14:03:18 +0100 Subject: [PATCH 01/12] Implement alternative scenarios infrastructure for the calculator. --- cara/apps/calculator/model_generator.py | 2 +- cara/apps/calculator/report_generator.py | 56 +++++++++++++++++++ cara/apps/calculator/templates/report.html.j2 | 28 +++++++++- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index bdb1cf80..1ba26cef 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -384,7 +384,7 @@ def baseline_raw_form_data(): 'mask_type': 'Type I', 'mask_wearing': 'removed', 'mechanical_ventilation_type': '', - 'model_version': 'BetaV1.1.0', + 'model_version': 'v1.1.0', 'opening_distance': '0.2', 'recurrent_event_month': 'January', 'room_number': '123', diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index c98355e9..aa2585a5 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -3,6 +3,7 @@ import dataclasses from datetime import datetime import io from pathlib import Path +import typing import jinja2 import matplotlib @@ -99,6 +100,59 @@ def minutes_to_time(minutes: int) -> str: return f"{hour_string}:{minute_string}" +def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models.ExposureModel]: + scenarios = {} + + with_mask = dataclasses.replace(form, mask_wearing='continuous') + without_mask = dataclasses.replace(form, mask_wearing='removed') + + scenarios['With mask'] = with_mask.build_model() + scenarios['Without mask'] = without_mask.build_model() + + return scenarios + + +def comparison_plot(scenarios: typing.Dict[str, models.ExposureModel]): + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + + resolution = 350 + times = None + for name, model in scenarios.items(): + if times is None: + t_start = min(model.exposed.presence.boundaries()[0][0], + model.concentration_model.infected.presence.boundaries()[0][0]) + t_end = max(model.exposed.presence.boundaries()[-1][1], + model.concentration_model.infected.presence.boundaries()[-1][1]) + times = np.linspace(t_start, t_end, resolution) + concentrations = [model.concentration_model.concentration(time) for time in times] + + ax.plot(times, concentrations, label=name) + + ax.legend() + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + + ax.set_xlabel('Time (hour of day)') + ax.set_ylabel('Concentration ($q/m^3$)') + ax.set_title('Concentration of infectious quanta') + + return fig + + +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(), + } + return { + 'plot': embed_figure(comparison_plot(scenarios)), + 'stats': statistics, + } + + def build_report(model: models.ExposureModel, form: FormData): now = datetime.now() time = now.strftime("%d/%m/%Y %H:%M:%S") @@ -112,6 +166,8 @@ def build_report(model: models.ExposureModel, form: FormData): } context.update(calculate_report_data(model)) + alternative_scenarios = manufacture_alternative_scenarios(form) + context['alternative_scenarios'] = comparison_report(alternative_scenarios) cara_templates = Path(__file__).parent.parent / "templates" calculator_templates = Path(__file__).parent / "templates" diff --git a/cara/apps/calculator/templates/report.html.j2 b/cara/apps/calculator/templates/report.html.j2 index 451e60f2..036ccda4 100644 --- a/cara/apps/calculator/templates/report.html.j2 +++ b/cara/apps/calculator/templates/report.html.j2 @@ -166,7 +166,33 @@ {% endfor %} -

+

+ +

Alternative scenarios:

+

+ + + + + + + + + + + + {% for scenario_name, scenario_stats in alternative_scenarios.stats.items() %} + + + + + + {% endfor %} + +
ScenarioP(i)Expected new cases
{{ scenario_name }}{{ scenario_stats.probability_of_infection | int_format }}%{{ scenario_stats.expected_new_cases | float_format }}
+ +

+



From 3b8c10c8c37d71ee75aa7a7283b313daa3e86ea6 Mon Sep 17 00:00:00 2001 From: jdevine Date: Mon, 23 Nov 2020 15:43:55 +0100 Subject: [PATCH 02/12] added multiple scenarios to calculator and simple rules violation warning --- cara/apps/calculator/report_generator.py | 58 +++++++++++++++++-- cara/apps/calculator/templates/report.html.j2 | 8 ++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index aa2585a5..2027f959 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -103,12 +103,62 @@ def minutes_to_time(minutes: int) -> str: def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models.ExposureModel]: scenarios = {} - with_mask = dataclasses.replace(form, mask_wearing='continuous') - without_mask = dataclasses.replace(form, mask_wearing='removed') + if (form.mask_wearing == 'continuous') and (form.mask_type == 'FFP2') and (form.hepa_option == 1) : + hepa_with_mask_ffp2 = dataclasses.replace(form, mask_type = 'FFP2', mask_wearing='continuous') + + scenarios['Scenario with HEPA and FFP2 masks'] = hepa_with_mask_ffp2.build_model() + form =dataclasses.replace(form, mask_type = 'Type I') + form =dataclasses.replace(form, hepa_option =0) - scenarios['With mask'] = with_mask.build_model() - scenarios['Without mask'] = without_mask.build_model() + if (form.mask_wearing == 'continuous') and (form.mask_type == 'FFP2'): + with_mask_ffp2 = dataclasses.replace(form, mask_type = 'FFP2', mask_wearing='continuous') + + scenarios['Scenario with FFP2 masks'] = with_mask_ffp2.build_model() + form =dataclasses.replace(form, mask_type = 'Type I') + if form.hepa_option == 1: + with_hepa = dataclasses.replace(form, hepa_option = 1) + + scenarios['Scenario with HEPA filter'] = with_hepa.build_model() + form =dataclasses.replace(form, hepa_option =0) + + + if form.ventilation_type == 'no-ventilation': + with_mask_type1 = dataclasses.replace(form, mask_type = 'Type I', mask_wearing='continuous') + without_mask = dataclasses.replace(form, mask_wearing='removed') + + scenarios['No ventilation with Type I masks'] = with_mask_type1.build_model() + scenarios['No venilation without masks'] = without_mask.build_model() + + elif form.ventilation_type == 'mechanical': + with_mask_type1 = dataclasses.replace(form, mask_type = 'Type I', mask_wearing='continuous') + without_mask = dataclasses.replace(form, mask_wearing='removed') + with_mask_no_vent = dataclasses.replace(form, mask_type = 'Type I', mask_wearing='continuous', ventilation_type='no-ventilation') + without_mask_or_vent = dataclasses.replace(form, mask_wearing='removed', ventilation_type='no-ventilation') + + scenarios['Mechanical ventilation with Type I masks'] = with_mask_type1.build_model() + scenarios['Mechanical ventilation without masks'] = without_mask.build_model() + scenarios['No ventilation with Type I masks'] = with_mask_no_vent.build_model() + scenarios['No ventilation or masks'] = without_mask_or_vent.build_model() + + elif form.ventilation_type == 'natural': + with_mask_type1 = dataclasses.replace(form, mask_type = 'Type I', mask_wearing='continuous') + without_mask = dataclasses.replace(form, mask_wearing='removed') + with_mask_no_vent = dataclasses.replace(form, mask_wearing='continuous', ventilation_type='no-ventilation') + without_mask_or_vent = dataclasses.replace(form, mask_wearing='removed', ventilation_type='no-ventilation') + + scenarios['Windows open with Type I masks'] = with_mask_type1.build_model() + scenarios['Windows open without masks'] = without_mask.build_model() + scenarios['Windows closed with Type I mask'] = with_mask_no_vent.build_model() + scenarios['Windows closed without masks'] = without_mask_or_vent.build_model() + + else : + with_mask_type1 = dataclasses.replace(form, mask_type = 'Type I', mask_wearing='continuous') + without_mask = dataclasses.replace(form, mask_wearing='removed') + + scenarios['With Type I mask'] = with_mask_type1.build_model() + scenarios['Without mask'] = without_mask.build_model() + return scenarios diff --git a/cara/apps/calculator/templates/report.html.j2 b/cara/apps/calculator/templates/report.html.j2 index 036ccda4..43187959 100644 --- a/cara/apps/calculator/templates/report.html.j2 +++ b/cara/apps/calculator/templates/report.html.j2 @@ -22,6 +22,10 @@

Simulation Name: {{ form.simulation_name }}

Room Number: {{ form.room_number }}

+ {% if (form.total_people > 5) or (form.ventilation_type == "no-ventilation") or (form.mask_wearing == "removed")%} +

Rules violation: This simulation doesn't conform to current CERN HSE rules. Please check your input assumptions and try again.

+ {% endif %} +

Input data: