From ee96f81d83b1955f2c18e1d18d330090a2be7a59 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Fri, 20 Jan 2023 16:55:22 +0100 Subject: [PATCH 01/11] d3 implementation for poi bins --- caimira/apps/calculator/report_generator.py | 8 +- caimira/apps/calculator/static/js/report.js | 157 +++++++++++++++++- .../templates/base/calculator.report.html.j2 | 25 ++- 3 files changed, 178 insertions(+), 12 deletions(-) diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index 1dcfa00d..ba987e3c 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -130,7 +130,8 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing for time1, time2 in zip(times[:-1], times[1:]) ]) - prob = np.array(model.infection_probability()).mean() + prob = np.array(model.infection_probability()) + prob_dist_count, prob_dist_bins = np.histogram(prob, bins=100, density=True) prob_probabilistic_exposure = np.array(model.total_probability_rule()).mean() er = np.array(model.concentration_model.infected.emission_rate_when_present()).mean() exposed_occupants = model.exposed.number @@ -147,7 +148,10 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing "highest_const": highest_const, "cumulative_doses": list(cumulative_doses), "long_range_cumulative_doses": list(long_range_cumulative_doses), - "prob_inf": prob, + "prob_inf": prob.mean(), + "prob_dist": list(prob), + "prob_hist_count": list(prob_dist_count), + "prob_hist_bins": list(prob_dist_bins), "prob_probabilistic_exposure": prob_probabilistic_exposure, "emission_rate": er, "exposed_occupants": exposed_occupants, diff --git a/caimira/apps/calculator/static/js/report.js b/caimira/apps/calculator/static/js/report.js index 26e41cc3..d11d8d9e 100644 --- a/caimira/apps/calculator/static/js/report.js +++ b/caimira/apps/calculator/static/js/report.js @@ -537,7 +537,6 @@ function draw_plot(svg_id) { }); } - // 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 @@ -853,6 +852,162 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_ }); } +function draw_histogram(svg_id) { + // Add main SVG element + var plot_div = document.getElementById(svg_id); + var div_width = plot_div.clientWidth; + var div_height = plot_div.clientHeight; + var vis = d3.select(plot_div).append('svg'); + + // set the dimensions and margins of the graph + if (div_width > 900) { + div_width = 900; + var margins = { top: 30, right: 20, bottom: 50, left: 60 }; + var graph_width = div_width * (2/3); + const svg_margins = {'margin-left': '0rem'}; + Object.entries(svg_margins).forEach(([prop,val]) => vis.style(prop,val)); + } + + vis.attr("width", div_width).attr('height', div_height); + + let hist_count = prob_hist_count; + let hist_bins = prob_hist_bins; + + // X axis: scale and draw: + var x = d3.scaleLinear() + .domain([0, d3.max(hist_bins)]) + .range([margins.left, graph_width - margins.right]); + vis.append("svg:g") + .attr("transform", "translate(0," + (graph_height - margins.bottom) + ")") + .call(d3.axisBottom(x)); + + // X axis label. + vis.append('text') + .attr('class', 'x label') + .attr('fill', 'black') + .attr('text-anchor', 'middle') + .text('Probability of Infection') + .attr('x', (graph_width + margins.right) / 2) + .attr('y', graph_height * 0.97); + + // set the parameters for the histogram + var histogram = d3.histogram() + .value(d => d) + .domain(x.domain()) // then the domain of the graphic + .thresholds(x.ticks(100)); // then the numbers of bins + + // And apply this function to data to get the bins + var bins = histogram(prob_dist); + + // Y left axis: scale and draw: + var y_left = d3.scaleLinear() + .range([graph_height - margins.bottom, margins.top]); + y_left.domain([0, d3.max(hist_count)]); // d3.hist has to be called before the Y axis obviously + vis.append("svg:g") + .attr('transform', 'translate(' + margins.left + ',0)') + .call(d3.axisLeft(y_left)); + + // Y left axis label. + vis.append('svg:text') + .attr('class', 'y label') + .attr('fill', 'black') + .attr('text-anchor', 'middle') + .text('Density') + .attr('x', (graph_height * 0.9 + margins.bottom) / 2) + .attr('y', (graph_height + margins.left) * 0.9) + .attr('transform', 'rotate(-90, 0,' + graph_height + ')'); + + // append the bar rectangles to the svg element + vis.selectAll("rect") + .data(bins.slice(0, -1)) + .enter() + .append("rect") + .attr("x", 1) + .attr("transform", function(d, i) { + return "translate(" + x(d.x0) + "," + y_left(hist_count[i]) + ")"; }) + .attr("width", function(d) { return x(d.x1) - x(d.x0) -1 ; }) + .attr("height", function(d, i) { return graph_height - y_left(hist_count[i]) - margins.bottom; }) + .attr('fill', '#1f77b4'); + + // Y right axis: scale and draw: + var y_right = d3.scaleLinear() + .range([graph_height - margins.bottom, margins.top]); + y_right.domain([0, 1]); + vis.append("svg:g") + .attr('transform', 'translate(' + (graph_width - margins.right) + ',0)') + .call(d3.axisRight(y_right)); + + // Y right axis label. + vis.append('svg:text') + .attr('class', 'y label') + .attr('fill', 'black') + .attr('text-anchor', 'middle') + .text('Cumulative Density Function (CDF)') + .attr('transform', 'rotate(-90, 0,' + graph_height + ')') + .attr('x', (graph_height + margins.bottom * 0.55) / 2) + .attr('y', graph_width + 430); + + // CDF Calculation + let count_sum = hist_count.reduce((partialSum, a) => partialSum + a, 0); + let pdf = hist_count.map((el, i) => el/count_sum); + let cdf = pdf.map((sum => value => sum += value)(0)); + // Add the CDF line + vis.append("svg:path") + .datum(cdf) + .attr("fill", "none") + .attr("stroke", "lightblue") + .attr("stroke-width", 1.5) + .attr("d", d3.line() + .x(function(d, i) { return x(hist_bins[i]) }) + .y(function(d) { return y_right(d) }) + ); + + // Legend for the plot elements + const size = 15; + var legend_x_start = 50; + const space_between_text_icon = 30; + const text_height = 6; + // CDF line icon + vis.append('rect') + .attr('width', 20) + .attr('height', 3) + .style('fill', 'lightblue') + .attr('x', graph_width + legend_x_start) + .attr('y', margins.top + size); + // CDF line text + vis.append('text') + .text('CDF') + .style('font-size', '15px') + .attr('x', graph_width + legend_x_start + space_between_text_icon) + .attr('y', margins.top + size + text_height); + // Hist icon + vis.append('rect') + .attr('width', 20) + .attr('height', 15) + .attr('fill', '#1f77b4') + .attr('x', graph_width + legend_x_start) + .attr('y', margins.top + (2 * size)); + // Hist text + vis.append('text') + .text('Histogram') + .style('font-size', '15px') + .attr('x', graph_width + legend_x_start + space_between_text_icon) + .attr('y', margins.top + 2 * size + text_height*2); + + // Legend Bbox + vis.append('rect') + .attr('width', 120) + .attr('height', 50) + .attr('stroke', 'lightgrey') + .attr('stroke-width', '2') + .attr('rx', '5px') + .attr('ry', '5px') + .attr('stroke-linejoin', 'round') + .attr('fill', 'none') + .attr('x', graph_width * 1.07) + .attr('y', margins.top * 1.1); +} + function copy_clipboard(shareable_link) { $("#mobile_link").attr('title', 'Copied!') diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index 7c202f6f..a650c21e 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -171,15 +171,22 @@ {% endif %}
+
+

From d9ac714183ac95b4470e8ed889f93b0d310dc5f7 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 22 Feb 2023 14:02:53 +0100 Subject: [PATCH 02/11] added mean line on histogram plot --- caimira/apps/calculator/report_generator.py | 1 + caimira/apps/calculator/static/js/report.js | 38 ++++++++++++++++++- .../templates/base/calculator.report.html.j2 | 2 +- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index ba987e3c..f1471519 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -149,6 +149,7 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing "cumulative_doses": list(cumulative_doses), "long_range_cumulative_doses": list(long_range_cumulative_doses), "prob_inf": prob.mean(), + "prob_inf_sd": np.std(prob), "prob_dist": list(prob), "prob_hist_count": list(prob_dist_count), "prob_hist_bins": list(prob_dist_bins), diff --git a/caimira/apps/calculator/static/js/report.js b/caimira/apps/calculator/static/js/report.js index d11d8d9e..511958ae 100644 --- a/caimira/apps/calculator/static/js/report.js +++ b/caimira/apps/calculator/static/js/report.js @@ -852,7 +852,7 @@ function draw_alternative_scenarios_plot(concentration_plot_svg_id, alternative_ }); } -function draw_histogram(svg_id) { +function draw_histogram(svg_id, prob, prob_sd) { // Add main SVG element var plot_div = document.getElementById(svg_id); var div_width = plot_div.clientWidth; @@ -962,6 +962,25 @@ function draw_histogram(svg_id) { .y(function(d) { return y_right(d) }) ); + // Add the mean dashed line + vis.append("svg:line") + .attr("fill", "none") + .attr('stroke-width', 2) + .attr('stroke-dasharray', (5, 5)) + .attr("x1", x(prob)) + .attr("y1", y_right(1)) + .attr("x2", x(prob)) + .attr("y2", y_right(0)) + .attr("stroke", "grey"); + + // Plot tile + vis.append("svg:text") + .attr("x", x(50)) + .attr("y", 0 + margins.top) + .attr("text-anchor", "middle") + .style("font-size", "16px") + .text(`P(I) -- Mean(SD) = ${prob.toFixed(2)}(${prob_sd.toFixed(2)}) `); + // Legend for the plot elements const size = 15; var legend_x_start = 50; @@ -993,11 +1012,26 @@ function draw_histogram(svg_id) { .style('font-size', '15px') .attr('x', graph_width + legend_x_start + space_between_text_icon) .attr('y', margins.top + 2 * size + text_height*2); + // Mean text + vis.append('line') + .attr('stroke', 'grey') + .attr('stroke-width', 2) + .attr('stroke-dasharray', (5, 5)) + .attr("x1", graph_width + legend_x_start) + .attr("x2", graph_width + legend_x_start + 20) + .attr("y1", margins.top + 3.85 * size) + .attr("y2", margins.top + 3.85 * size); + // Mean line text + vis.append('text') + .text('Mean') + .style('font-size', '15px') + .attr('x', graph_width + legend_x_start + space_between_text_icon) + .attr('y', margins.top + 3 * size + text_height*3); // Legend Bbox vis.append('rect') .attr('width', 120) - .attr('height', 50) + .attr('height', 65) .attr('stroke', 'lightgrey') .attr('stroke-width', '2') .attr('rx', '5px') diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index a650c21e..1bd08286 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -186,7 +186,7 @@ let prob_dist = {{ prob_dist | JSONify }} let prob_hist_count = {{ prob_hist_count | JSONify }}; let prob_hist_bins = {{ prob_hist_bins | JSONify }}; - draw_histogram("prob_inf_hist"); + draw_histogram("prob_inf_hist", {{ prob_inf }}, {{ prob_inf_sd }});

From 34efdcbdec50c27d8f3ef1d10585ef3a2eeb33f4 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 15 Feb 2023 16:56:40 +0100 Subject: [PATCH 03/11] added method to generate P(I|vl) uncertainties graphs --- caimira/apps/calculator/report_generator.py | 112 ++++++++++++++++++ .../templates/base/calculator.report.html.j2 | 2 + 2 files changed, 114 insertions(+) diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index f1471519..5795e1b1 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -10,6 +10,7 @@ import jinja2 import numpy as np +import matplotlib.pyplot as plt from caimira import models from caimira.apps.calculator import markdown_tools @@ -157,6 +158,7 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing "emission_rate": er, "exposed_occupants": exposed_occupants, "expected_new_cases": expected_new_cases, + "uncertainties_plot_scr": img2base64(_figure2bytes(uncertainties_plot([model]))) } @@ -179,6 +181,109 @@ def generate_permalink(base_url, get_root_url, get_root_calculator_url, form: F } +def uncertainties_plot(exposure_models): + from tqdm import tqdm + fig = plt.figure(figsize=(7, 10)) + viral_loads = np.linspace(2, 10, 600) + + lines, lowers, uppers = [], [], [] + for exposure_mc in exposure_models: + concentration_model = exposure_mc.concentration_model + pi_means = [] + lower_percentiles = [] + upper_percentiles = [] + + for vl in tqdm(viral_loads): + model_vl = dataclass_utils.replace(exposure_mc, + concentration_model = models.ConcentrationModel( + room=concentration_model.room, + ventilation=concentration_model.ventilation, + infected=models.InfectedPopulation( + number=concentration_model.infected.number, + presence=concentration_model.infected.presence, + virus = models.SARSCoV2( + viral_load_in_sputum=10**vl, + infectious_dose=concentration_model.infected.virus.infectious_dose, + viable_to_RNA_ratio=concentration_model.infected.virus.viable_to_RNA_ratio, + transmissibility_factor=0.2, + ), + mask=concentration_model.infected.mask, + activity=concentration_model.infected.activity, + expiration=concentration_model.infected.expiration, + host_immunity=concentration_model.infected.host_immunity, + ) + ), + ) + + pi = model_vl.infection_probability()/100 + pi_means.append(np.mean(pi)) + lower_percentiles.append(np.quantile(pi, 0.05)) + upper_percentiles.append(np.quantile(pi, 0.95)) + + lines.append(pi_means) + uppers.append(upper_percentiles) + lowers.append(lower_percentiles) + + # print(model.concentration_model.infected.virus) + histogram_data = [model.infection_probability() / 100 for model in exposure_models] + + fig, axs = plt.subplots(2, 2 + len(exposure_models), gridspec_kw={'width_ratios': [5, 0.5] + [1] * len(exposure_models), + 'height_ratios': [3, 1], 'wspace': 0}, + sharey='row', sharex='col') + + for y, x in [(0, 1)] + [(1, i + 1) for i in range(len(exposure_models) + 1)]: + axs[y, x].axis('off') + + for x in range(len(exposure_models) - 1): + axs[0, x + 3].tick_params(axis='y', which='both', left='off') + + axs[0, 1].set_visible(False) + + for line, upper, lower in zip(lines, uppers, lowers): + axs[0, 0].plot(viral_loads, line, label='Predictive total probability') + axs[0, 0].fill_between(viral_loads, lower, upper, alpha=0.1, label='5ᵗʰ and 95ᵗʰ percentile') + + for i, data in enumerate(histogram_data): + axs[0, i + 2].hist(data, bins=30, orientation='horizontal') + axs[0, i + 2].set_xticks([]) + axs[0, i + 2].set_xticklabels([]) + # axs[0, i + 2].set_xlabel(f"{np.round(np.mean(data) * 100, 1)}%") + axs[0, i + 2].set_facecolor("lightgrey") + + highest_bar = max(axs[0, i + 2].get_xlim()[1] for i in range(len(histogram_data))) + for i in range(len(histogram_data)): + axs[0, i + 2].set_xlim(0, highest_bar) + + axs[0, i + 2].text(highest_bar * 0.5, 0.5, + rf"$\bf{np.round(np.mean(histogram_data[i]) * 100, 1)}$%", ha='center', va='center') + + axs[1, 0].hist([np.log10(vl) for vl in exposure_models[0].concentration_model.infected.virus.viral_load_in_sputum], + bins=150, range=(2, 10), color='grey') + axs[1, 0].set_facecolor("lightgrey") + axs[1, 0].set_yticks([]) + axs[1, 0].set_yticklabels([]) + axs[1, 0].set_xticks([i for i in range(2, 13, 2)]) + axs[1, 0].set_xticklabels(['$10^{' + str(i) + '}$' for i in range(2, 13, 2)]) + axs[1, 0].set_xlim(2, 10) + axs[1, 0].set_xlabel('Viral load\n(RNA copies)', fontsize=12) + axs[0, 0].set_ylabel('Probability of infection\nfor a given viral load', fontsize=12) + + axs[0, 0].text(9.5, -0.01, '$(i)$') + axs[1, 0].text(9.5, axs[1, 0].get_ylim()[1] * 0.8, '$(ii)$') + #axs[0, 2].text(axs[0, 2].get_xlim()[1] * 0.1, -0.05, '$(iii)$') + axs[0, 2].set_title('$(iii)$', fontsize=10) + + crits = [] + for line in lines: + for i, point in enumerate(line): + if point >= 0.05: + crits.append(viral_loads[i]) + break + + axs[0, 0].legend() + return fig + + def _img2bytes(figure): # Draw the image img_data = io.BytesIO() @@ -186,6 +291,13 @@ def _img2bytes(figure): return img_data +def _figure2bytes(figure): + # Draw the image + img_data = io.BytesIO() + figure.savefig(img_data, format='png', bbox_inches="tight", transparent=True) + return img_data + + def img2base64(img_data) -> str: img_data.seek(0) pic_hash = base64.b64encode(img_data.read()).decode('ascii') diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index 1bd08286..1ba6f720 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -193,6 +193,8 @@ + + {% if form.short_range_option == "short_range_no" %}
Alternative scenarios From a33659dbb6595c0635d13ba8770f0c4a7aa6b2b6 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Wed, 15 Feb 2023 17:10:26 +0100 Subject: [PATCH 04/11] removed tqdm package --- caimira/apps/calculator/report_generator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index 5795e1b1..14531cbd 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -182,7 +182,6 @@ def generate_permalink(base_url, get_root_url, get_root_calculator_url, form: F def uncertainties_plot(exposure_models): - from tqdm import tqdm fig = plt.figure(figsize=(7, 10)) viral_loads = np.linspace(2, 10, 600) @@ -193,7 +192,7 @@ def uncertainties_plot(exposure_models): lower_percentiles = [] upper_percentiles = [] - for vl in tqdm(viral_loads): + for vl in viral_loads: model_vl = dataclass_utils.replace(exposure_mc, concentration_model = models.ConcentrationModel( room=concentration_model.room, From d37691f10bdca9356a5cd9ef973245f83f8501c0 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 2 Mar 2023 15:54:47 +0100 Subject: [PATCH 05/11] added checkbox for conditional probability generation --- caimira/apps/calculator/model_generator.py | 3 +++ caimira/apps/calculator/report_generator.py | 3 ++- caimira/apps/templates/base/calculator.form.html.j2 | 5 +++++ caimira/apps/templates/base/calculator.report.html.j2 | 4 +++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 5931c980..5c0fe232 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -36,6 +36,7 @@ class FormData: specific_breaks: dict precise_activity: dict ceiling_height: float + conditional_probability_plot: bool exposed_coffee_break_option: str exposed_coffee_duration: int exposed_finish: minutes_since_midnight @@ -104,6 +105,7 @@ class FormData: 'precise_activity': '{}', 'calculator_version': _NO_DEFAULT, 'ceiling_height': 0., + 'conditional_probability_plot': False, 'exposed_coffee_break_option': 'coffee_break_0', 'exposed_coffee_duration': 5, 'exposed_finish': '17:30', @@ -900,6 +902,7 @@ def baseline_raw_form_data() -> typing.Dict[str, typing.Union[str, float]]: 'air_changes': '', 'air_supply': '', 'ceiling_height': '', + 'conditional_probability_plot': '0', 'exposed_coffee_break_option': 'coffee_break_4', 'exposed_coffee_duration': '10', 'exposed_finish': '18:00', diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index 14531cbd..8c870a08 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -137,6 +137,7 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing er = np.array(model.concentration_model.infected.emission_rate_when_present()).mean() exposed_occupants = model.exposed.number expected_new_cases = np.array(model.expected_new_cases()).mean() + uncertainties_plot_src = img2base64(_figure2bytes(uncertainties_plot(model))) if form.conditional_probability_plot else None return { "model_repr": repr(model), @@ -158,7 +159,7 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing "emission_rate": er, "exposed_occupants": exposed_occupants, "expected_new_cases": expected_new_cases, - "uncertainties_plot_scr": img2base64(_figure2bytes(uncertainties_plot([model]))) + "uncertainties_plot_src": uncertainties_plot_src, } diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 448350b7..00dd2a37 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -406,6 +406,11 @@
+
+ + +
+
diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index 1ba6f720..ed5c5cc5 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -193,7 +193,9 @@ - + {% if form.conditional_probability_plot %} + + {% endif %} {% if form.short_range_option == "short_range_no" %}
From 09779f3a97eec8a9c445eb710f57238149b7ce20 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 2 Mar 2023 15:54:59 +0100 Subject: [PATCH 06/11] adapted method for uncertainties plot --- caimira/apps/calculator/report_generator.py | 112 +++++++------------- 1 file changed, 36 insertions(+), 76 deletions(-) diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index 8c870a08..4b01c4f7 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -182,82 +182,50 @@ def generate_permalink(base_url, get_root_url, get_root_calculator_url, form: F } -def uncertainties_plot(exposure_models): - fig = plt.figure(figsize=(7, 10)) +def uncertainties_plot(exposure_model: models.ExposureModel): + fig = plt.figure(figsize=(4, 7), dpi=110) + viral_loads = np.linspace(2, 10, 600) + pi_means, lower_percentiles, upper_percentiles = [], [], [] + for vl in viral_loads: + model_vl = dataclass_utils.nested_replace( + exposure_model, { + 'concentration_model.infected.virus.viral_load_in_sputum' : 10**vl, + } + ) + pi = model_vl.infection_probability()/100 - lines, lowers, uppers = [], [], [] - for exposure_mc in exposure_models: - concentration_model = exposure_mc.concentration_model - pi_means = [] - lower_percentiles = [] - upper_percentiles = [] - - for vl in viral_loads: - model_vl = dataclass_utils.replace(exposure_mc, - concentration_model = models.ConcentrationModel( - room=concentration_model.room, - ventilation=concentration_model.ventilation, - infected=models.InfectedPopulation( - number=concentration_model.infected.number, - presence=concentration_model.infected.presence, - virus = models.SARSCoV2( - viral_load_in_sputum=10**vl, - infectious_dose=concentration_model.infected.virus.infectious_dose, - viable_to_RNA_ratio=concentration_model.infected.virus.viable_to_RNA_ratio, - transmissibility_factor=0.2, - ), - mask=concentration_model.infected.mask, - activity=concentration_model.infected.activity, - expiration=concentration_model.infected.expiration, - host_immunity=concentration_model.infected.host_immunity, - ) - ), - ) - - pi = model_vl.infection_probability()/100 - pi_means.append(np.mean(pi)) - lower_percentiles.append(np.quantile(pi, 0.05)) - upper_percentiles.append(np.quantile(pi, 0.95)) - - lines.append(pi_means) - uppers.append(upper_percentiles) - lowers.append(lower_percentiles) - - # print(model.concentration_model.infected.virus) - histogram_data = [model.infection_probability() / 100 for model in exposure_models] - - fig, axs = plt.subplots(2, 2 + len(exposure_models), gridspec_kw={'width_ratios': [5, 0.5] + [1] * len(exposure_models), - 'height_ratios': [3, 1], 'wspace': 0}, - sharey='row', sharex='col') - - for y, x in [(0, 1)] + [(1, i + 1) for i in range(len(exposure_models) + 1)]: - axs[y, x].axis('off') + pi_means.append(np.mean(pi)) + lower_percentiles.append(np.quantile(pi, 0.05)) + upper_percentiles.append(np.quantile(pi, 0.95)) + + histogram_data = exposure_model.infection_probability() / 100 - for x in range(len(exposure_models) - 1): - axs[0, x + 3].tick_params(axis='y', which='both', left='off') + fig, axs = plt.subplots(2, 3, + gridspec_kw={'width_ratios': [5, 0.5] + [1], + 'height_ratios': [3, 1], 'wspace': 0}, + sharey='row', + sharex='col') + + for y, x in [(0, 1)] + [(1, i + 1) for i in range(2)]: + axs[y, x].axis('off') axs[0, 1].set_visible(False) - - for line, upper, lower in zip(lines, uppers, lowers): - axs[0, 0].plot(viral_loads, line, label='Predictive total probability') - axs[0, 0].fill_between(viral_loads, lower, upper, alpha=0.1, label='5ᵗʰ and 95ᵗʰ percentile') - for i, data in enumerate(histogram_data): - axs[0, i + 2].hist(data, bins=30, orientation='horizontal') - axs[0, i + 2].set_xticks([]) - axs[0, i + 2].set_xticklabels([]) - # axs[0, i + 2].set_xlabel(f"{np.round(np.mean(data) * 100, 1)}%") - axs[0, i + 2].set_facecolor("lightgrey") + axs[0, 0].plot(viral_loads, pi_means, label='Predictive total probability') + axs[0, 0].fill_between(viral_loads, lower_percentiles, upper_percentiles, alpha=0.1, label='5ᵗʰ and 95ᵗʰ percentile') - highest_bar = max(axs[0, i + 2].get_xlim()[1] for i in range(len(histogram_data))) - for i in range(len(histogram_data)): - axs[0, i + 2].set_xlim(0, highest_bar) + axs[0, 2].hist(histogram_data, bins=30, orientation='horizontal') + axs[0, 2].set_xticks([]) + axs[0, 2].set_xticklabels([]) + axs[0, 2].set_facecolor("lightgrey") - axs[0, i + 2].text(highest_bar * 0.5, 0.5, - rf"$\bf{np.round(np.mean(histogram_data[i]) * 100, 1)}$%", ha='center', va='center') + highest_bar = axs[0, 2].get_xlim()[1] + axs[0, 2].set_xlim(0, highest_bar) - axs[1, 0].hist([np.log10(vl) for vl in exposure_models[0].concentration_model.infected.virus.viral_load_in_sputum], + axs[0, 2].text(highest_bar * 0.5, 0.5, + rf"$\bf{np.round(np.mean(histogram_data) * 100, 1)}$%", ha='center', va='center') + axs[1, 0].hist(np.log10(exposure_model.concentration_model.infected.virus.viral_load_in_sputum), bins=150, range=(2, 10), color='grey') axs[1, 0].set_facecolor("lightgrey") axs[1, 0].set_yticks([]) @@ -270,16 +238,8 @@ def uncertainties_plot(exposure_models): axs[0, 0].text(9.5, -0.01, '$(i)$') axs[1, 0].text(9.5, axs[1, 0].get_ylim()[1] * 0.8, '$(ii)$') - #axs[0, 2].text(axs[0, 2].get_xlim()[1] * 0.1, -0.05, '$(iii)$') axs[0, 2].set_title('$(iii)$', fontsize=10) - crits = [] - for line in lines: - for i, point in enumerate(line): - if point >= 0.05: - crits.append(viral_loads[i]) - break - axs[0, 0].legend() return fig @@ -294,7 +254,7 @@ def _img2bytes(figure): def _figure2bytes(figure): # Draw the image img_data = io.BytesIO() - figure.savefig(img_data, format='png', bbox_inches="tight", transparent=True) + figure.savefig(img_data, format='png', bbox_inches="tight", transparent=True, dpi=110) return img_data From 74b0563a12cded9712e475f84c031943493efd79 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 6 Mar 2023 15:43:29 +0100 Subject: [PATCH 07/11] added conditional logic to render uncertainties plots --- caimira/apps/calculator/__init__.py | 3 + caimira/apps/calculator/static/js/report.js | 19 ++ .../templates/base/calculator.form.html.j2 | 7 +- .../templates/base/calculator.report.html.j2 | 207 +++++++++--------- 4 files changed, 132 insertions(+), 104 deletions(-) diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index 8399bd9d..54b641fe 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -122,6 +122,9 @@ async def post(self) -> None: max_workers=self.settings['handler_worker_pool_size'], timeout=300, ) + # Re-generate the report with the conditional probability of infection plot + if self.get_cookie('conditional_plot'): + form.conditional_probability_plot = True if self.get_cookie('conditional_plot') == '1' else False report_task = executor.submit( report_generator.build_report, base_url, form, executor_factory=functools.partial( diff --git a/caimira/apps/calculator/static/js/report.js b/caimira/apps/calculator/static/js/report.js index 511958ae..df7cfbb9 100644 --- a/caimira/apps/calculator/static/js/report.js +++ b/caimira/apps/calculator/static/js/report.js @@ -1,3 +1,8 @@ +function on_report_load(conditional_probability_plot) { + // Check/uncheck uncertainties image generation + document.getElementById('conditional_probability_plot').checked = conditional_probability_plot +} + /* Generate the concentration plot using d3 library. */ function draw_plot(svg_id) { @@ -1069,6 +1074,20 @@ function display_rename_column(bool, id) { else document.getElementById(id).style.display = 'none'; } +function conditional_probability_plot(value, is_generated) { + // If the image was previously generated, there is no need to reload the page. + if (value && is_generated == 1) { + document.getElementById('conditional_probability_div').style.display = 'block' + } + else if (value && is_generated == 0) { + document.getElementById('label_conditional_probability_plot').innerHTML = `Loading...`; + document.getElementById('conditional_probability_plot').setAttribute('disabled', true); + window.location.reload(); + } + else document.getElementById('conditional_probability_div').style.display = "none"; + document.cookie = `conditional_plot= ${+value}; path=/`; +} + function export_csv() { // This function generates a CSV file according to the user's input. // It is composed of a list of lists. diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 00dd2a37..576cf535 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -400,15 +400,14 @@
-
- - +
+
diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index ed5c5cc5..5e640393 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -15,7 +15,7 @@ - +