From c27f567f3ab5aa936b528eb3460cfb0de5d48ae7 Mon Sep 17 00:00:00 2001
From: Luis Aleixo * The results are based on the parameters and assumptions published in the CERN Open Report CERN-OPEN-2021-004..
return f'data:image/png;base64,{pic_hash}'
-
+
def minutes_to_time(minutes: int) -> str:
minute_string = str(minutes % 60)
@@ -224,18 +211,14 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, mc.Exp
scenarios = {}
# Two special option cases - HEPA and/or FFP2 masks.
- FFP2_being_worn = bool(form.mask_wearing_option ==
- 'mask_on' and form.mask_type == 'FFP2')
+ FFP2_being_worn = bool(form.mask_wearing_option == 'mask_on' and form.mask_type == 'FFP2')
if FFP2_being_worn and form.hepa_option:
- FFP2andHEPAalternative = dataclass_utils.replace(
- form, mask_type='Type I')
+ FFP2andHEPAalternative = dataclass_utils.replace(form, mask_type='Type I')
scenarios['Base scenario with HEPA filter and Type I masks'] = FFP2andHEPAalternative.build_mc_model()
if not FFP2_being_worn and form.hepa_option:
- noHEPAalternative = dataclass_utils.replace(form, mask_type='FFP2')
- noHEPAalternative = dataclass_utils.replace(
- noHEPAalternative, mask_wearing_option='mask_on')
- noHEPAalternative = dataclass_utils.replace(
- noHEPAalternative, hepa_option=False)
+ noHEPAalternative = dataclass_utils.replace(form, mask_type = 'FFP2')
+ noHEPAalternative = dataclass_utils.replace(noHEPAalternative, mask_wearing_option = 'mask_on')
+ noHEPAalternative = dataclass_utils.replace(noHEPAalternative, hepa_option=False)
scenarios['Base scenario without HEPA filter, with FFP2 masks'] = noHEPAalternative.build_mc_model()
# The remaining scenarios are based on Type I masks (possibly not worn)
@@ -245,8 +228,7 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, mc.Exp
form = dataclass_utils.replace(form, hepa_option=False)
with_mask = dataclass_utils.replace(form, mask_wearing_option='mask_on')
- without_mask = dataclass_utils.replace(
- form, mask_wearing_option='mask_off')
+ without_mask = dataclass_utils.replace(form, mask_wearing_option='mask_off')
if form.ventilation_type == 'mechanical_ventilation':
#scenarios['Mechanical ventilation with Type I masks'] = with_mask.build_mc_model()
@@ -257,62 +239,17 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, mc.Exp
scenarios['Windows open without masks'] = without_mask.build_mc_model()
# No matter the ventilation scheme, we include scenarios which don't have any ventilation.
- with_mask_no_vent = dataclass_utils.replace(
- with_mask, ventilation_type='no_ventilation')
- without_mask_or_vent = dataclass_utils.replace(
- without_mask, ventilation_type='no_ventilation')
+ with_mask_no_vent = dataclass_utils.replace(with_mask, ventilation_type='no_ventilation')
+ without_mask_or_vent = dataclass_utils.replace(without_mask, ventilation_type='no_ventilation')
scenarios['No ventilation with Type I masks'] = with_mask_no_vent.build_mc_model()
scenarios['Neither ventilation nor masks'] = without_mask_or_vent.build_mc_model()
return scenarios
-def comparison_plot(scenarios: typing.Dict[str, dict], sample_times: typing.List[float]):
- fig = plt.figure()
- ax = fig.add_subplot(1, 1, 1)
- ax1 = ax.twinx()
-
- dash_styled_scenarios = [
- 'Base scenario with FFP2 masks',
- 'Base scenario with HEPA filter',
- 'Base scenario with HEPA and FFP2 masks',
- ]
-
- datetimes = [datetime(1970, 1, 1) + timedelta(hours=time)
- for time in sample_times]
-
- for name, statistics in scenarios.items():
- model = statistics['model']
- concentrations = statistics['concentrations']
-
- # Plot concentrations
- if name in dash_styled_scenarios:
- ax.plot(datetimes, concentrations, label=name, linestyle='--')
- else:
- ax.plot(datetimes, concentrations,
- label=name, linestyle='-', alpha=0.5)
-
- # Place a legend outside of the axes itself.
- fig.legend(bbox_to_anchor=(1.05, 0.95), loc='upper left')
-
- ax.spines['right'].set_visible(False)
- ax.spines['top'].set_visible(False)
- ax.set_xlabel('Time of day', fontsize=14)
- ax.set_ylabel('Mean viral concentration\n(virion m$^{-3}$)', fontsize=14)
- ax.set_title('Concentration profile')
- ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter("%H:%M"))
-
- ax.set_xlabel('Time of day')
- ax.set_ylabel('Mean concentration ($virions/m^{3}$)')
- ax.set_title('Mean concentration of virions')
-
- return fig
-
-
-def scenario_statistics(mc_model: mc.ExposureModel, sample_times: typing.List[float]):
+def scenario_statistics(mc_model: mc.ExposureModel, sample_times: np.ndarray):
model = mc_model.build_model(size=_DEFAULT_MC_SAMPLE_SIZE)
return {
- 'model': model,
'probability_of_infection': np.mean(model.infection_probability()),
'expected_new_cases': np.mean(model.expected_new_cases()),
'concentrations': [
@@ -339,7 +276,6 @@ def comparison_report(
for (name, model), model_stats in zip(scenarios.items(), results):
statistics[name] = model_stats
return {
- 'plot': img2base64(_figure2bytes(comparison_plot(statistics, sample_times))),
'stats': statistics,
}
@@ -356,8 +292,7 @@ class ReportGenerator:
executor_factory: typing.Callable[[], concurrent.futures.Executor],
) -> str:
model = form.build_model()
- context = self.prepare_context(
- base_url, model, form, executor_factory=executor_factory)
+ context = self.prepare_context(base_url, model, form, executor_factory=executor_factory)
return self.render(context)
def prepare_context(
@@ -383,8 +318,7 @@ class ReportGenerator:
context['alternative_scenarios'] = comparison_report(
alternative_scenarios, scenario_sample_times, executor_factory=executor_factory,
)
- context['qr_code'] = generate_qr_code(
- base_url, self.calculator_prefix, form)
+ context['qr_code'] = generate_qr_code(base_url, self.calculator_prefix, form)
context['calculator_prefix'] = self.calculator_prefix
context['scale_warning'] = {
'level': 'yellow-2',
@@ -404,6 +338,7 @@ class ReportGenerator:
env.filters['minutes_to_time'] = minutes_to_time
env.filters['float_format'] = "{0:.2f}".format
env.filters['int_format'] = "{:0.0f}".format
+ env.filters['JSONify'] = json.dumps
return env
def render(self, context: dict) -> str:
diff --git a/cara/apps/calculator/static/js/report.js b/cara/apps/calculator/static/js/report.js
index 00e85892..6266a09b 100644
--- a/cara/apps/calculator/static/js/report.js
+++ b/cara/apps/calculator/static/js/report.js
@@ -27,70 +27,18 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses
yCumulatedAxis = d3.axisRight(yCumulatedRange);
// Plot tittle.
- vis.append('svg:foreignObject')
- .attr("background-color", "transparent")
- .attr('width', width)
- .attr('height', margins.top)
- .style('text-align', 'center')
- .html('Mean concentration of virions');
+ plot_title(vis, width, margins.top, 'Mean concentration of virions');
// Line representing the mean concentration.
- var lineFunc = d3.line()
- .defined(d => !isNaN(d.concentration))
- .x(d => xTimeRange(d.time))
- .y(d => yRange(d.concentration))
- .curve(d3.curveBasis);
-
- vis.append('svg:path')
- .attr('d', lineFunc(data))
- .attr('stroke', '#1f77b4')
- .attr('stroke-width', 2)
- .attr('fill', 'none');
-
+ plot_scenario_data(vis, data, xTimeRange, yRange, '#1f77b4');
// Line representing the cumulative concentration.
- var lineCumulativeFunc = d3.line()
- .defined(d => !isNaN(d.cumulative_doses))
- .x(d => xTimeRange(d.time))
- .y(d => yCumulatedRange(d.cumulative_doses))
- .curve(d3.curveBasis);
+ plot_cumulative_data(vis, data, xTimeRange, yCumulatedRange, '#1f77b4');
- vis.append('svg:path')
- .attr('d', lineCumulativeFunc(data))
- .attr('stroke', '#1f77b4')
- .attr('stroke-width', 2)
- .style("stroke-dasharray", "5 5")
- .attr('fill', 'none');
+ // X axis.
+ plot_x_axis(vis, height, width, margins, xAxis, 'Time of day');
- // X axis declaration.
- vis.append('svg:g')
- .attr('class', 'x axis')
- .attr('transform', 'translate(0,' + (height - margins.bottom) + ')')
- .call(xAxis);
-
- // X axis label.
- vis.append('text')
- .attr('class', 'x label')
- .attr('fill', 'black')
- .attr('text-anchor', 'middle')
- .attr('x', (width + margins.right) / 2)
- .attr('y', height * 0.97)
- .text('Time of day')
-
- // Y concentration axis declaration.
- vis.append('svg:g')
- .attr('class', 'y axis')
- .attr('transform', 'translate(' + margins.left + ',0)')
- .call(yAxis);
-
- // Y concentration axis label.
- vis.append('svg:text')
- .attr('class', 'y label')
- .attr('fill', 'black')
- .attr('transform', 'rotate(-90, 0,' + height + ')')
- .attr('text-anchor', 'middle')
- .attr('x', (height + margins.bottom) / 2)
- .attr('y', (height + margins.left) * 0.92)
- .text('Mean concentration (virions/m³)');
+ // Y axis
+ plot_y_axis(vis, height, width, margins, yAxis, 'Mean concentration (virions/m³)')
// Y cumulative concentration axis declaration.
vis.append('svg:g')
@@ -230,4 +178,195 @@ function draw_concentration_plot(svg_id, times, concentrations, cumulative_doses
focus.select('#tooltip-time').text('x = ' + time_format(d.hour));
focus.select('#tooltip-concentration').text('y = ' + d.concentration.toFixed(2));
}
+}
+
+// Generate the alternative scenarios plot using d3 library.
+// 'alternative_scenarios' is a dictionary with all the alternative scenarios
+// 'times' is a list of times for all the scenarios
+function draw_alternative_scenarios_plot(svg_id, width, height, alternative_scenarios, times) {
+ // H:M format
+ var time_format = d3.timeFormat('%H:%M');
+ // D3 array of ten categorical colors represented as RGB hexadecimal strings.
+ var colors = d3.schemeAccent;
+
+ // Variable for the highest concentration for all the scenarios
+ var highest_concentration = 0.
+
+ var data_for_scenarios = {}
+ for (scenario in alternative_scenarios) {
+ scenario_concentrations = alternative_scenarios[scenario].concentrations
+
+ highest_concentration = Math.max(highest_concentration, Math.max(...scenario_concentrations))
+
+ var data = []
+ times.map((time, index) => data.push({ 'time': time, 'hour': new Date().setHours(Math.trunc(time), (time - Math.trunc(time)) * 60), 'concentration': scenario_concentrations[index] }))
+
+ // Add data into lines dictionary
+ data_for_scenarios[scenario] = data
+ }
+
+ // We need one scenario to get the time range
+ var first_scenario = Object.values(data_for_scenarios)[0]
+
+ var vis = d3.select(svg_id),
+ width = width,
+ height = height,
+ margins = { top: 30, right: 20, bottom: 50, left: 50 },
+
+ // H:M time format for x axis.
+ xRange = d3.scaleTime().range([margins.left, width - margins.right]).domain([first_scenario[0].hour, first_scenario[first_scenario.length - 1].hour]),
+ xTimeRange = d3.scaleLinear().range([margins.left, width - margins.right]).domain([times[0], times[times.length - 1]]),
+
+ yRange = d3.scaleLinear().range([height - margins.bottom, margins.top]).domain([0., highest_concentration]),
+
+ xAxis = d3.axisBottom(xRange).tickFormat(d => time_format(d)),
+ yAxis = d3.axisLeft(yRange);
+
+ // Plot title.
+ plot_title(vis, width, margins.top, 'Mean concentration of virions');
+
+ // Line representing the mean concentration for each scenario.
+ for (const [scenario_name, data] of Object.entries(data_for_scenarios)) {
+ var scenario_index = Object.keys(data_for_scenarios).indexOf(scenario_name)
+
+ // Line representing the mean concentration.
+ plot_scenario_data(vis, data, xTimeRange, yRange, colors[scenario_index])
+
+ // Legend for the plot elements - lines.
+ var size = 20 * (scenario_index + 1)
+ vis.append('rect')
+ .attr('x', width + 20)
+ .attr('y', margins.top + size)
+ .attr('width', 20)
+ .attr('height', 3)
+ .style('fill', colors[scenario_index]);
+
+ vis.append('text')
+ .attr('x', width + 3 * 20)
+ .attr('y', margins.top + size)
+ .text(scenario_name)
+ .style('font-size', '15px')
+ .attr('alignment-baseline', 'central');
+
+ }
+
+ // X axis.
+ plot_x_axis(vis, height, width, margins, xAxis, "Time of day");
+
+ // Y axis declaration.
+ vis.append('svg:g')
+ .attr('class', 'y axis')
+ .attr('transform', 'translate(' + margins.left + ',0)')
+ .call(yAxis);
+
+ // Y axis label.
+ vis.append('svg:text')
+ .attr('class', 'y label')
+ .attr('fill', 'black')
+ .attr('transform', 'rotate(-90, 0,' + height + ')')
+ .attr('text-anchor', 'middle')
+ .attr('x', (height + margins.bottom) / 2)
+ .attr('y', (height + margins.left) * 0.92)
+ .text('Mean concentration (virions/m³)');
+
+ // Legend bounding box.
+ vis.append('rect')
+ .attr('width', 275)
+ .attr('height', 25 * (Object.keys(data_for_scenarios).length))
+ .attr('x', width * 1.005)
+ .attr('y', margins.top + 5)
+ .attr('stroke', 'lightgrey')
+ .attr('stroke-width', '2')
+ .attr('rx', '5px')
+ .attr('ry', '5px')
+ .attr('stroke-linejoin', 'round')
+ .attr('fill', 'none');
+}
+
+
+// Functions used to build the plots' components
+
+function plot_title(vis, width, margin_top, title) {
+ vis.append('svg:foreignObject')
+ .attr('width', width)
+ .attr('height', margin_top)
+ .attr('fill', 'none')
+ .append('xhtml:div')
+ .style('text-align', 'center')
+ .html(title);
+
+ return vis;
+}
+
+function plot_x_axis(vis, height, width, margins, xAxis, label) {
+ // X axis declaration
+ vis.append('svg:g')
+ .attr('class', 'x axis')
+ .attr('transform', 'translate(0,' + (height - margins.bottom) + ')')
+ .call(xAxis);
+
+ // X axis label.
+ vis.append('text')
+ .attr('class', 'x label')
+ .attr('fill', 'black')
+ .attr('text-anchor', 'middle')
+ .attr('x', (width + margins.right) / 2)
+ .attr('y', height * 0.97)
+ .text(label);
+
+ return vis;
+}
+
+function plot_y_axis(vis, height, width, margins, yAxis, label) {
+ // Y axis declaration.
+ vis.append('svg:g')
+ .attr('class', 'y axis')
+ .attr('transform', 'translate(' + margins.left + ',0)')
+ .call(yAxis);
+
+ // Y axis label.
+ vis.append('svg:text')
+ .attr('class', 'y label')
+ .attr('fill', 'black')
+ .attr('transform', 'rotate(-90, 0,' + height + ')')
+ .attr('text-anchor', 'middle')
+ .attr('x', (height + margins.bottom) / 2)
+ .attr('y', (height + margins.left) * 0.92)
+ .text(label);
+
+ return vis;
+
+}
+
+function plot_scenario_data(vis, data, xTimeRange, yRange, line_color) {
+ var lineFunc = d3.line()
+ .defined(d => !isNaN(d.concentration))
+ .x(d => xTimeRange(d.time))
+ .y(d => yRange(d.concentration))
+ .curve(d3.curveBasis);
+
+ vis.append('svg:path')
+ .attr('d', lineFunc(data))
+ .attr("stroke", line_color)
+ .attr('stroke-width', 2)
+ .attr('fill', 'none');
+
+ return vis;
+}
+
+function plot_cumulative_data(vis, data, xTimeRange, yCumulativeRange, line_color) {
+ var lineCumulativeFunc = d3.line()
+ .defined(d => !isNaN(d.cumulative_doses))
+ .x(d => xTimeRange(d.time))
+ .y(d => yCumulativeRange(d.cumulative_doses))
+ .curve(d3.curveBasis);
+
+ vis.append('svg:path')
+ .attr('d', lineCumulativeFunc(data))
+ .attr('stroke', line_color)
+ .attr('stroke-width', 2)
+ .style("stroke-dasharray", "5 5")
+ .attr('fill', 'none');
+
+ return vis;
}
\ No newline at end of file
diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/calculator/templates/base/calculator.report.html.j2
index 371788c0..108a9a9c 100644
--- a/cara/apps/calculator/templates/base/calculator.report.html.j2
+++ b/cara/apps/calculator/templates/base/calculator.report.html.j2
@@ -86,12 +86,12 @@
{% endblock report_summary_footnote %}