Merge branch 'master' of https://gitlab.cern.ch/cara/cara into develop/hinged_window

This commit is contained in:
Nicolas Mounet 2020-11-30 09:14:07 +01:00
commit 7e0e8a4e20
4 changed files with 143 additions and 9 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

@ -39,11 +39,15 @@ p.result_title {
font-size: 15pt;
}
.image {
p.image {
text-align: center;
font-size: 13pt;
}
.discalimer {
p.disclaimer {
font-size: 12pt;
}
p.notes {
font-size: 10pt;
}

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,12 +169,45 @@
{% endfor %}
</tbody>
</table>
<p>
</p>
<p class="data_title">Alternative scenarios:</p>
<p class="data_text">
<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;">
<p class="notes"> <strong> Notes for alternative scenarios: </strong><br>
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 ).<br>
For this reason, scenarios with different types of mask will show the same concentration on the graph but have different Pi values.<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", representing the inputs inserted in the form.<br>
The other alternative scenarios shown for comparison will not include either HEPA filtration or FFP2 masks. <br>
<br>
</p>
<br><br><br>
<div style="border: 2px solid black; padding: 15px;">
<p class="image"> <img <img align="middle" src="/calculator/static/images/disclaimer.jpg" width="40" height="40"><b>Disclaimer:</b><br><br></p>
<p class="discalimer">The risk assessment tool simulates the long range airborne spread SARS-CoV-2 virus in a finite volume, assuming a homogenous mixture, and estimates the risk of COVID-19 infection thereto. The results DO NOT include short-range airborne exposure (where the physical distance plays a factor) nor the other know modes of transmission of SARS-CoV-2. Hence, this model implies that proper physical distancing, good hand hygiene and other barrier measures are ensured.<br><br>
<p class="disclaimer">The risk assessment tool simulates the long range airborne spread SARS-CoV-2 virus in a finite volume, assuming a homogenous mixture, and estimates the risk of COVID-19 infection thereto. The results DO NOT include short-range airborne exposure (where the physical distance plays a factor) nor the other know modes of transmission of SARS-CoV-2. Hence, this model implies that proper physical distancing, good hand hygiene and other barrier measures are ensured.<br><br>
It is based on current scientific data and can be used to measures the effectiveness of different mitigation measures.<br><br>
Note that this model is based on a deterministic approach, i.e., at least one person is infected and shedding viruses into the volume. Nonetheless, it is also important to understand that the absolute risk of infection is uncertain as it will depend on the probability that someone infected attends the event. The model is mostly useful to compare the impact and effectiveness of mitigation measures such as ventilation, filtration, exposure time, activity and the size of the room on long-range airborne transmission of COVID-19 in indoor settings.<br><br>
This application is meant for informative and educational purposes. The user can be able to adapt different settings and measure the relative impact on the estimated infection probabilities to allow for a targeted decision making and investment. The user should acknowledge that until the virus is in circulation among the population, the notion of 'zero risk' or a 'completely safe scenario' does not exist. Each event is unique and the results are as accurate as the inputs. The app is based on our scientific understanding of infectious diseases transmission, exposure and aerosol science as of November 2020.<br><br>