Merge branch 'feature/report-scenarios'

* feature/report-scenarios:
  Fix typos in the calculator report regarding alternative scenarios.
  minor tweaks to alternative scenario text
  Refine the logic for the calculation of alternative scenarios in the calculator report.
  updated following diff comments
  moved legend, added dashed cases, changed scenario names a little
  added hyperlink to rules warning
  changed warning to be generic
  added dashed line for FFP2 comparison
  added multiple scenarios to calculator and simple rules violation warning
  Implement alternative scenarios infrastructure for the calculator.
This commit is contained in:
Phil Elson 2020-11-26 09:51:43 +01:00
commit 1934fba291
3 changed files with 134 additions and 6 deletions

View file

@ -403,7 +403,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',

View file

@ -3,6 +3,7 @@ import dataclasses
from datetime import datetime
import io
from pathlib import Path
import typing
import jinja2
import matplotlib
@ -21,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)
@ -99,6 +104,93 @@ 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 = {}
# 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['Base scenario with HEPA and FFP2 masks'] = form.build_model()
elif FFP2_being_worn:
scenarios['Base scenario with FFP2 masks'] = form.build_model()
elif form.hepa_option:
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.
form = dataclasses.replace(form, mask_type='Type I')
if form.hepa_option:
form = dataclasses.replace(form, hepa_option=False)
with_mask = dataclasses.replace(form, mask_wearing='continuous')
without_mask = dataclasses.replace(form, mask_wearing='removed')
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()
elif form.ventilation_type == 'natural':
scenarios['Windows open with Type I masks'] = with_mask.build_model()
scenarios['Windows open without masks'] = without_mask.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
def comparison_plot(scenarios: typing.Dict[str, models.ExposureModel]):
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
resolution = 350
times = None
dash_styled_scenarios = [
'Base scenario with FFP2 masks',
'Base scenario with HEPA filter',
'Base scenario with HEPA and FFP2 masks',
]
for name, model in scenarios.items():
if times is None:
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 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)
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 +204,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"

View file

@ -19,9 +19,12 @@
<p class=subtitle> Created {{ creation_date }} using model version {{ form.model_version }}</p><br>
<p><strong>Applicable rules: <br>
Please ensure that this scenario conforms to current <a href="https://hse.cern/covid-19-information"> CERN HSE rules<a> (minimum ventilation requirements, mask wearing and the maximum number of people permitted in a space).</strong></p>
<p>Simulation Name: {{ form.simulation_name }}</p>
<p>Room Number: {{ form.room_number }}</p>
<p class="data_title">Input data:</p>
<ul>
<li><p class="data_text">Room Volume: {{ model.concentration_model.room.volume }} m³</p></li>
@ -166,7 +169,38 @@
{% endfor %}
</tbody>
</table>
<p>
</p>
<p class="data_title">Alternative scenarios:</p>
<p class="data_text">
<strong> Notes: </strong><br>
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).<br>
For this reason, scenarios with different types of mask will show the same concentration on the graph.<br>
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.<br>
The other alternative scenarios shown for comparison will not include either HEPA filtration or FFP2 masks. <br>
<br>
<img id="scenario_concentration_plot" src="{{ alternative_scenarios.plot }}" align="left" />
<table class="table table-striped w-auto">
<thead class="thead-light">
<tr>
<th>Scenario</th>
<th>P(i)</th>
<th>Expected new cases</th>
</tr>
</thead>
<tbody>
{% for scenario_name, scenario_stats in alternative_scenarios.stats.items() %}
<tr>
<td>{{ scenario_name }}</td>
<td>{{ scenario_stats.probability_of_infection | int_format }}%</td>
<td style="text-align:right">{{ scenario_stats.expected_new_cases | float_format }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</p>
<div style="clear: both;">
<br><br><br>
<div style="border: 2px solid black; padding: 15px;">