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" %}
+
+
+
+
+
Conference/Training activities limited to 1 infected
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 @@
-