From dd60c0ef2371acc8f0d92b0100338d0962d4af09 Mon Sep 17 00:00:00 2001
From: Phil Elson
Date: Fri, 20 Nov 2020 14:03:18 +0100
Subject: [PATCH 01/10] 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:
+
+
+
+
+
+
+ | Scenario |
+ P(i) |
+ Expected new cases |
+
+
+
+ {% for scenario_name, scenario_stats in alternative_scenarios.stats.items() %}
+
+ | {{ scenario_name }} |
+ {{ scenario_stats.probability_of_infection | int_format }}% |
+ {{ scenario_stats.expected_new_cases | float_format }} |
+
+ {% endfor %}
+
+
+
+
+
From 3b8c10c8c37d71ee75aa7a7283b313daa3e86ea6 Mon Sep 17 00:00:00 2001
From: jdevine
Date: Mon, 23 Nov 2020 15:43:55 +0100
Subject: [PATCH 02/10] 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:
Room Volume: {{ model.concentration_model.room.volume }} m³
@@ -170,8 +174,10 @@
Alternative scenarios:
+ Note: this graph shows the concentration of viral quanta in the air.
+ The filtration of Type I and FFP2 masks, if worn, applies to indiviudal exposure (inhalation only).
+ For this reason, scenarios with different types of mask will show the same concentration on the graph.
-
From 3cf41c5004188ca1415daa6f43032c264d102de2 Mon Sep 17 00:00:00 2001
From: jdevine
Date: Mon, 23 Nov 2020 18:10:04 +0100
Subject: [PATCH 03/10] added dashed line for FFP2 comparison
---
cara/apps/calculator/report_generator.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py
index 2027f959..cfff23a6 100644
--- a/cara/apps/calculator/report_generator.py
+++ b/cara/apps/calculator/report_generator.py
@@ -107,6 +107,7 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models
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)
@@ -177,8 +178,11 @@ def comparison_plot(scenarios: typing.Dict[str, models.ExposureModel]):
times = np.linspace(t_start, t_end, resolution)
concentrations = [model.concentration_model.concentration(time) for time in times]
- ax.plot(times, concentrations, label=name)
-
+ if name == 'Scenario with FFP2 masks' :
+ ax.plot(times, concentrations, '--', label=name)
+ else :
+ ax.plot(times, concentrations, '-', label=name, alpha=0.5)
+
ax.legend()
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
From 48e7398c7bf65531991fe0d1dfaf5384f20228be Mon Sep 17 00:00:00 2001
From: jdevine
Date: Mon, 23 Nov 2020 18:12:06 +0100
Subject: [PATCH 04/10] changed warning to be generic
---
cara/apps/calculator/templates/report.html.j2 | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/cara/apps/calculator/templates/report.html.j2 b/cara/apps/calculator/templates/report.html.j2
index 43187959..b5b07bbe 100644
--- a/cara/apps/calculator/templates/report.html.j2
+++ b/cara/apps/calculator/templates/report.html.j2
@@ -22,10 +22,8 @@
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 %}
-
+ Applicable rules: Please ensure that this scenario conforms to current CERN HSE rules (ventilation requirements, mask wearing and maximum number of people).
+
Input data:
Room Volume: {{ model.concentration_model.room.volume }} m³
From c82f8e611a5f8b31a3d6ae06142f7f9f1a045311 Mon Sep 17 00:00:00 2001
From: jdevine
Date: Mon, 23 Nov 2020 18:17:10 +0100
Subject: [PATCH 05/10] added hyperlink to rules warning
---
cara/apps/calculator/templates/report.html.j2 | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/cara/apps/calculator/templates/report.html.j2 b/cara/apps/calculator/templates/report.html.j2
index b5b07bbe..651571a6 100644
--- a/cara/apps/calculator/templates/report.html.j2
+++ b/cara/apps/calculator/templates/report.html.j2
@@ -19,10 +19,11 @@
Created {{ creation_date }} using model version {{ form.model_version }}
+ 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).
+
Simulation Name: {{ form.simulation_name }}
Room Number: {{ form.room_number }}
-
- Applicable rules: Please ensure that this scenario conforms to current CERN HSE rules (ventilation requirements, mask wearing and maximum number of people).
Input data:
From 598a845434be27e25e1946e6bde2c7756caec8ec Mon Sep 17 00:00:00 2001
From: jdevine
Date: Tue, 24 Nov 2020 12:09:12 +0100
Subject: [PATCH 06/10] moved legend, added dashed cases, changed scenario
names a little
---
cara/apps/calculator/report_generator.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py
index cfff23a6..6a8d3db6 100644
--- a/cara/apps/calculator/report_generator.py
+++ b/cara/apps/calculator/report_generator.py
@@ -129,7 +129,7 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models
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()
+ scenarios['Neither ventilation nor masks'] = without_mask.build_model()
elif form.ventilation_type == 'mechanical':
with_mask_type1 = dataclasses.replace(form, mask_type = 'Type I', mask_wearing='continuous')
@@ -140,7 +140,7 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models
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()
+ scenarios['Neither ventilation nor 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')
@@ -178,12 +178,12 @@ def comparison_plot(scenarios: typing.Dict[str, models.ExposureModel]):
times = np.linspace(t_start, t_end, resolution)
concentrations = [model.concentration_model.concentration(time) for time in times]
- if name == 'Scenario with FFP2 masks' :
+ if (name == 'Scenario with FFP2 masks') or (name == 'Scenario with HEPA filter') or (name == 'Scenario with HEPA and FFP2 masks'):
ax.plot(times, concentrations, '--', label=name)
else :
ax.plot(times, concentrations, '-', label=name, alpha=0.5)
- ax.legend()
+ ax.legend(bbox_to_anchor=(1.05,1), loc='upper left')
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
From 9fa6c4cd871eaac5105cd4e4a1657ce0a56b2577 Mon Sep 17 00:00:00 2001
From: jdevine
Date: Wed, 25 Nov 2020 23:45:05 +0100
Subject: [PATCH 07/10] updated following diff comments
---
cara/apps/calculator/report_generator.py | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py
index 6a8d3db6..12963c1c 100644
--- a/cara/apps/calculator/report_generator.py
+++ b/cara/apps/calculator/report_generator.py
@@ -103,6 +103,9 @@ def minutes_to_time(minutes: int) -> str:
def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models.ExposureModel]:
scenarios = {}
+ #Setup scenarios for with/without Type 1 masks and fixed variations on selected ventilation options.
+
+ #Two special option cases - HEPA and/or FFP2 masks
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')
@@ -111,19 +114,20 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models
form =dataclasses.replace(form, mask_type = 'Type I')
form =dataclasses.replace(form, hepa_option =0)
- if (form.mask_wearing == 'continuous') and (form.mask_type == 'FFP2'):
+ elif (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:
+ elif 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)
-
+
+ #general cases for with/without Type 1 masks across ventilation types
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')
@@ -152,14 +156,7 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models
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
From df034b71454a49a75b99894e34153fe279a83583 Mon Sep 17 00:00:00 2001
From: Phil Elson
Date: Thu, 26 Nov 2020 08:18:13 +0100
Subject: [PATCH 08/10] Refine the logic for the calculation of alternative
scenarios in the calculator report.
---
cara/apps/calculator/report_generator.py | 109 ++++++++++-------------
1 file changed, 48 insertions(+), 61 deletions(-)
diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py
index 12963c1c..317ced76 100644
--- a/cara/apps/calculator/report_generator.py
+++ b/cara/apps/calculator/report_generator.py
@@ -22,14 +22,18 @@ class RepeatEvents:
expected_new_cases: float
-def calculate_report_data(model: models.ExposureModel):
- resolution = 600
-
+def model_start_end(model: models.ExposureModel):
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])
+ return t_start, t_end
+
+def calculate_report_data(model: models.ExposureModel):
+ resolution = 600
+
+ 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]
highest_const = max(concentrations)
@@ -103,60 +107,38 @@ def minutes_to_time(minutes: int) -> str:
def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models.ExposureModel]:
scenarios = {}
- #Setup scenarios for with/without Type 1 masks and fixed variations on selected ventilation options.
+ # Two special option cases - HEPA and/or FFP2 masks.
+ FFP2_being_worn = bool(form.mask_wearing == 'continuous' and form.mask_type == 'FFP2')
+ if FFP2_being_worn and form.hepa_option:
+ scenarios['Scenario with HEPA and FFP2 masks'] = form.build_model()
+ elif FFP2_being_worn:
+ scenarios['Scenario with FFP2 masks'] = form.build_model()
+ elif form.hepa_option:
+ scenarios['Scenario with HEPA filter'] = form.build_model()
- #Two special option cases - HEPA and/or FFP2 masks
- 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)
+ # The remaining scenarios are based on Type I masks (possibly not worn)
+ # and no HEPA filtration.
+ form = dataclasses.replace(form, mask_type='Type I')
+ if form.hepa_option:
+ form = dataclasses.replace(form, hepa_option=False)
- elif (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')
+ with_mask = dataclasses.replace(form, mask_wearing='continuous')
+ without_mask = dataclasses.replace(form, mask_wearing='removed')
- elif 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)
-
-
- #general cases for with/without Type 1 masks across ventilation types
- 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['Neither ventilation nor 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()
+ if form.ventilation_type == 'mechanical':
+ scenarios['Mechanical ventilation with Type I masks'] = with_mask.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['Neither ventilation nor 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 with Type I masks'] = with_mask.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()
-
+
+ # No matter the ventilation scheme, we include scenarios which don't have any ventilation.
+ with_mask_no_vent = dataclasses.replace(with_mask, ventilation_type='no-ventilation')
+ without_mask_or_vent = dataclasses.replace(without_mask, ventilation_type='no-ventilation')
+ scenarios['No ventilation with Type I masks'] = with_mask_no_vent.build_model()
+ scenarios['Neither ventilation nor masks'] = without_mask_or_vent.build_model()
+
return scenarios
@@ -166,21 +148,26 @@ def comparison_plot(scenarios: typing.Dict[str, models.ExposureModel]):
resolution = 350
times = None
+
+ dash_styled_scenarios = [
+ 'Scenario with FFP2 masks',
+ 'Scenario with HEPA filter',
+ 'Scenario with HEPA and FFP2 masks',
+ ]
+
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])
+ t_start, t_end = model_start_end(model)
times = np.linspace(t_start, t_end, resolution)
concentrations = [model.concentration_model.concentration(time) for time in times]
- if (name == 'Scenario with FFP2 masks') or (name == 'Scenario with HEPA filter') or (name == 'Scenario with HEPA and FFP2 masks'):
- ax.plot(times, concentrations, '--', label=name)
- else :
- ax.plot(times, concentrations, '-', label=name, alpha=0.5)
-
- ax.legend(bbox_to_anchor=(1.05,1), loc='upper left')
+ if name in dash_styled_scenarios:
+ ax.plot(times, concentrations, label=name, linestyle='--')
+ else:
+ ax.plot(times, concentrations, label=name, linestyle='-', alpha=0.5)
+
+ # Place a legend outside of the axes itself.
+ ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
From 0957987740c9da023ceb07f9211d20a003692324 Mon Sep 17 00:00:00 2001
From: jdevine
Date: Thu, 26 Nov 2020 09:29:31 +0100
Subject: [PATCH 09/10] minor tweaks to alternative scenario text
---
cara/apps/calculator/report_generator.py | 12 ++++++------
cara/apps/calculator/templates/report.html.j2 | 7 +++++--
2 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py
index 317ced76..be8f8322 100644
--- a/cara/apps/calculator/report_generator.py
+++ b/cara/apps/calculator/report_generator.py
@@ -110,11 +110,11 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models
# Two special option cases - HEPA and/or FFP2 masks.
FFP2_being_worn = bool(form.mask_wearing == 'continuous' and form.mask_type == 'FFP2')
if FFP2_being_worn and form.hepa_option:
- scenarios['Scenario with HEPA and FFP2 masks'] = form.build_model()
+ scenarios['Base scenario with HEPA and FFP2 masks'] = form.build_model()
elif FFP2_being_worn:
- scenarios['Scenario with FFP2 masks'] = form.build_model()
+ scenarios['Base scenario with FFP2 masks'] = form.build_model()
elif form.hepa_option:
- scenarios['Scenario with HEPA filter'] = form.build_model()
+ scenarios['Base scenario with HEPA filter'] = form.build_model()
# The remaining scenarios are based on Type I masks (possibly not worn)
# and no HEPA filtration.
@@ -150,9 +150,9 @@ def comparison_plot(scenarios: typing.Dict[str, models.ExposureModel]):
times = None
dash_styled_scenarios = [
- 'Scenario with FFP2 masks',
- 'Scenario with HEPA filter',
- 'Scenario with HEPA and FFP2 masks',
+ 'Base scenario with FFP2 masks',
+ 'Base scenario with HEPA filter',
+ 'Base scenario with HEPA and FFP2 masks',
]
for name, model in scenarios.items():
diff --git a/cara/apps/calculator/templates/report.html.j2 b/cara/apps/calculator/templates/report.html.j2
index 651571a6..e3c4f699 100644
--- a/cara/apps/calculator/templates/report.html.j2
+++ b/cara/apps/calculator/templates/report.html.j2
@@ -173,9 +173,12 @@
Alternative scenarios:
- Note: this graph shows the concentration of viral quanta in the air.
- The filtration of Type I and FFP2 masks, if worn, applies to indiviudal exposure (inhalation only).
+ Notes:
+ 1) This graph shows the concentration of viral quanta in the air. The filtration of Type I and FFP2 masks, if worn, applies to indiviudal exposure (inhalation only).
For this reason, scenarios with different types of mask will show the same concentration on the graph.
+ 2) If you have selected more sophisticated options, such as HEPA filtration or FF2 masks, this will be indicated in the plot as the "base scenario", identical to the main results shown above.
+ The other alternative scenarios shown for comparison will not include either HEPA filtration or FFP2 masks.
+
From f5a8c2d477b6275a7a479ba06da93f8cd2b58401 Mon Sep 17 00:00:00 2001
From: Phil Elson
Date: Thu, 26 Nov 2020 09:51:14 +0100
Subject: [PATCH 10/10] Fix typos in the calculator report regarding
alternative scenarios.
---
cara/apps/calculator/templates/report.html.j2 | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/cara/apps/calculator/templates/report.html.j2 b/cara/apps/calculator/templates/report.html.j2
index e3c4f699..da1b0d91 100644
--- a/cara/apps/calculator/templates/report.html.j2
+++ b/cara/apps/calculator/templates/report.html.j2
@@ -174,9 +174,9 @@
Alternative scenarios:
Notes:
- 1) This graph shows the concentration of viral quanta in the air. The filtration of Type I and FFP2 masks, if worn, applies to indiviudal exposure (inhalation only).
+ 1) This graph shows the concentration of viral quanta in the air. The filtration of Type I and FFP2 masks, if worn, applies to individual exposure (inhalation only).
For this reason, scenarios with different types of mask will show the same concentration on the graph.
- 2) If you have selected more sophisticated options, such as HEPA filtration or FF2 masks, this will be indicated in the plot as the "base scenario", identical to the main results shown above.
+ 2) 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", identical to the main results shown above.
The other alternative scenarios shown for comparison will not include either HEPA filtration or FFP2 masks.