From 5b578c15eff7061983c8da851b2dd105c36365e2 Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Tue, 20 Sep 2022 16:00:25 +0200
Subject: [PATCH 01/15] backend model changes
---
caimira/apps/calculator/__init__.py | 2 +-
caimira/apps/calculator/model_generator.py | 23 ++++++++--
caimira/apps/expert.py | 1 +
caimira/models.py | 51 ++++++++++++++++++++++
4 files changed, 73 insertions(+), 4 deletions(-)
diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py
index fe1898eb..4072648c 100644
--- a/caimira/apps/calculator/__init__.py
+++ b/caimira/apps/calculator/__init__.py
@@ -33,7 +33,7 @@ from .user import AuthenticatedUser, AnonymousUser
# calculator version. If the calculator needs to make breaking changes (e.g. change
# form attributes) then it can also increase its MAJOR version without needing to
# increase the overall CAiMIRA version (found at ``caimira.__version__``).
-__version__ = "4.3"
+__version__ = "4.4"
class BaseRequestHandler(RequestHandler):
diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py
index 290d22d3..dfac768d 100644
--- a/caimira/apps/calculator/model_generator.py
+++ b/caimira/apps/calculator/model_generator.py
@@ -57,6 +57,10 @@ class FormData:
location_name: str
location_latitude: float
location_longitude: float
+ geographic_population: int
+ geographic_cases: int
+ ascertainment_bias: str
+ p_recurrent_option: str
mask_type: str
mask_wearing_option: str
mechanical_ventilation_type: str
@@ -116,6 +120,10 @@ class FormData:
'location_latitude': _NO_DEFAULT,
'location_longitude': _NO_DEFAULT,
'location_name': _NO_DEFAULT,
+ 'geographic_population': 0,
+ 'geographic_cases': 0,
+ 'ascertainment_bias': 'confidence_low',
+ 'p_recurrent_option': 'p_recurrent_event',
'mask_type': 'Type I',
'mask_wearing_option': 'mask_off',
'mechanical_ventilation_type': 'not-applicable',
@@ -261,7 +269,9 @@ class FormData:
('volume_type', VOLUME_TYPES),
('window_opening_regime', WINDOWS_OPENING_REGIMES),
('window_type', WINDOWS_TYPES),
- ('event_month', MONTH_NAMES)]
+ ('event_month', MONTH_NAMES),
+ ('ascertainment_bias', CONFIDENCE_LEVEL_OPTIONS),]
+
for attr_name, valid_set in validation_tuples:
if getattr(self, attr_name) not in valid_set:
raise ValueError(f"{getattr(self, attr_name)} is not a valid value for {attr_name}")
@@ -329,6 +339,11 @@ class FormData:
),
short_range = tuple(short_range),
exposed=self.exposed_population(),
+ geographical_data=mc.Cases(
+ geographic_population=self.geographic_population,
+ geographic_cases=self.geographic_cases,
+ ascertainment_bias=CONFIDENCE_LEVEL_OPTIONS[self.ascertainment_bias],
+ ),
)
def build_model(self, sample_size=_DEFAULT_MC_SAMPLE_SIZE) -> models.ExposureModel:
@@ -759,6 +774,9 @@ def baseline_raw_form_data() -> typing.Dict[str, typing.Union[str, float]]:
'location_latitude': 46.20833,
'location_longitude': 6.14275,
'location_name': 'Geneva',
+ 'geographic_population': 0,
+ 'geographic_cases': 0,
+ 'ascertainment_bias': 'confidence_low',
'mask_type': 'Type I',
'mask_wearing_option': 'mask_off',
'mechanical_ventilation_type': '',
@@ -794,9 +812,8 @@ VIRUS_TYPES = {'SARS_CoV_2', 'SARS_CoV_2_ALPHA', 'SARS_CoV_2_BETA','SARS_CoV_2_G
VOLUME_TYPES = {'room_volume_explicit', 'room_volume_from_dimensions'}
WINDOWS_OPENING_REGIMES = {'windows_open_permanently', 'windows_open_periodically', 'not-applicable'}
WINDOWS_TYPES = {'window_sliding', 'window_hinged', 'not-applicable'}
-
COFFEE_OPTIONS_INT = {'coffee_break_0': 0, 'coffee_break_1': 1, 'coffee_break_2': 2, 'coffee_break_4': 4}
-
+CONFIDENCE_LEVEL_OPTIONS = {'confidence_low': 10, 'confidence_medium': 5, 'confidence_high': 2}
MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December',
diff --git a/caimira/apps/expert.py b/caimira/apps/expert.py
index d3740bbf..361dc1c4 100644
--- a/caimira/apps/expert.py
+++ b/caimira/apps/expert.py
@@ -847,6 +847,7 @@ baseline_model = models.ExposureModel(
mask=models.Mask.types['No mask'],
host_immunity=0.,
),
+ geographical_data=models.Cases(),
)
diff --git a/caimira/models.py b/caimira/models.py
index 0e6fec4c..f07b715c 100644
--- a/caimira/models.py
+++ b/caimira/models.py
@@ -37,6 +37,7 @@ import typing
import numpy as np
from scipy.interpolate import interp1d
+import scipy.stats as sct
if not typing.TYPE_CHECKING:
from memoization import cached
@@ -911,6 +912,33 @@ class InfectedPopulation(_PopulationWithVirus):
return self.expiration.particle
+@dataclass(frozen=True)
+class Cases:
+ """
+ The geographical data to calculate the probability of having at least 1
+ new infection in a specific event.
+ """
+ #: Geographic location population
+ geographic_population: int = 0
+
+ #: Geographic location new cases
+ geographic_cases: int = 0
+
+ #: Number of new cases confidence level
+ ascertainment_bias: int = 0
+
+ def probability_random_individual(self) -> _VectorisedFloat:
+ """Probability that a randomly selected individual in a focal population is infected."""
+ return self.geographic_cases*self.ascertainment_bias/self.geographic_population
+
+ def probability_meet_infected_person(self, event, x) -> _VectorisedFloat:
+ """
+ Probability to meet x infected persons in an event.
+ From https://doi.org/10.1038/s41562-020-01000-9.
+ """
+ return sct.binom.pmf(x, event, self.probability_random_individual())
+
+
@dataclass(frozen=True)
class ConcentrationModel:
room: Room
@@ -1280,6 +1308,9 @@ class ExposureModel:
#: The population of non-infected people to be used in the model.
exposed: Population
+ #: Geographical data
+ geographical_data: Cases
+
#: The number of times the exposure event is repeated (default 1).
repeats: int = 1
@@ -1435,6 +1466,26 @@ class ExposureModel:
return (1 - np.exp(-((vD * (1 - self.exposed.host_immunity))/(infectious_dose *
self.concentration_model.virus.transmissibility_factor)))) * 100
+ def total_probability_rule(self) -> _VectorisedFloat:
+ if (self.geographical_data.geographic_population != 0 and self.geographical_data.geographic_cases != 0):
+ sum_probability = 0.0
+ # Create an equivalent exposure model but with i infected cases
+ total_people = self.concentration_model.infected.number + self.exposed.number
+ X = (total_people if total_people < 10 else 10)
+ # The influence of a higher number of simultainious infected people (> 4 - 5) yields an almost negligible contirbution to the total probability.
+ # To be on the safe side, a hard coded limit with a safety margin of 2x was set.
+ # Therefore we decided a hard limit of 10 infected people.
+ for x in range(1, X):
+ exposure_model = nested_replace(
+ self, {'concentration_model.infected.number': x}
+ )
+ prob_exposed_occupant = exposure_model.infection_probability().mean() / 100
+ # By means of a Binomial Distribution
+ sum_probability += (prob_exposed_occupant)*self.geographical_data.probability_meet_infected_person(self.exposed.number, x)
+ return sum_probability * 100
+ else:
+ return 0
+
def expected_new_cases(self) -> _VectorisedFloat:
# Create an equivalent exposure model without short-range interactions, if any.
if (len(self.short_range) == 0):
From 8936b0db48aa753d91ba80be889e9d523e684294 Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Wed, 21 Sep 2022 09:14:33 +0200
Subject: [PATCH 02/15] added UI input fields and updated report for the new
P(I)
---
caimira/apps/calculator/report_generator.py | 16 ++++++-
caimira/apps/calculator/static/js/form.js | 47 +++++++++++++++++++
.../templates/base/calculator.form.html.j2 | 30 ++++++++++++
.../templates/base/calculator.report.html.j2 | 47 ++++++++++++++++---
caimira/apps/templates/base/userguide.html.j2 | 17 ++++++-
.../templates/cern/calculator.report.html.j2 | 5 ++
6 files changed, 154 insertions(+), 8 deletions(-)
diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py
index 1a7cdf02..7317f2b0 100644
--- a/caimira/apps/calculator/report_generator.py
+++ b/caimira/apps/calculator/report_generator.py
@@ -131,6 +131,7 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing
])
prob = np.array(model.infection_probability()).mean()
+ prob_specific_event = np.array(model.total_probability_rule()).mean()
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()
@@ -147,6 +148,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,
+ "prob_specific_event": prob_specific_event,
"emission_rate": er,
"exposed_occupants": exposed_occupants,
"expected_new_cases": expected_new_cases,
@@ -272,8 +274,13 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, mc.Exp
return scenarios
-def scenario_statistics(mc_model: mc.ExposureModel, sample_times: typing.List[float]):
+def scenario_statistics(mc_model: mc.ExposureModel, sample_times: typing.List[float], specific_event: bool):
model = mc_model.build_model(size=_DEFAULT_MC_SAMPLE_SIZE)
+ if (specific_event):
+ # It means we have data to calculate the total_probability_rule
+ prob_specific_event = np.array(model.total_probability_rule()).mean()
+ else:
+ prob_specific_event = 0.
return {
'probability_of_infection': np.mean(model.infection_probability()),
@@ -282,6 +289,7 @@ def scenario_statistics(mc_model: mc.ExposureModel, sample_times: typing.List[fl
np.mean(model.concentration(time))
for time in sample_times
],
+ 'prob_specific_event': prob_specific_event,
}
@@ -303,11 +311,17 @@ def comparison_report(
else:
statistics = {}
+ if (form.short_range_option == "short_range_yes" and form.p_recurrent_option == "p_specific_event"):
+ specific_event = True
+ else:
+ specific_event = False
+
with executor_factory() as executor:
results = executor.map(
scenario_statistics,
scenarios.values(),
[sample_times] * len(scenarios),
+ [specific_event] * len(scenarios),
timeout=60,
)
diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js
index 45656c78..5e551b7d 100644
--- a/caimira/apps/calculator/static/js/form.js
+++ b/caimira/apps/calculator/static/js/form.js
@@ -61,6 +61,12 @@ function require_fields(obj) {
case "hepa_no":
require_hepa(false);
break;
+ case "p_specific_event":
+ require_population(true);
+ break;
+ case "p_recurrent_event":
+ require_population(false);
+ break;
case "mask_on":
require_mask(true);
break;
@@ -172,6 +178,12 @@ function require_lunch(id, option) {
}
}
+function require_population(option) {
+ require_input_field("#geographic_population", option);
+ require_input_field("#geographic_cases", option);
+ require_input_field("#ascertainment_bias", option);
+}
+
function require_mask(option) {
$("#mask_type_1").prop('required', option);
$("#mask_type_ffp2").prop('required', option);
@@ -269,6 +281,23 @@ function on_hepa_option_change() {
})
}
+function on_p_recurrent_change() {
+ p_recurrent = $('input[type=radio][name=p_recurrent_option]')
+ p_recurrent.each(function (index) {
+ if (this.checked) {
+ getChildElement($(this)).show();
+ require_fields(this);
+ }
+ else {
+ getChildElement($(this)).hide();
+ unrequire_fields(this);
+
+ //Clear invalid inputs for this newly hidden child element
+ removeInvalid("#"+getChildElement($(this)).find('input').not('input[type=radio]').attr('id'));
+ }
+ })
+}
+
function on_wearing_mask_change() {
wearing_mask = $('input[type=radio][name=mask_wearing_option]')
wearing_mask.each(function (index) {
@@ -538,6 +567,18 @@ function validate_form(form) {
}
}
+ // Validate cases < population
+ if ($("#p_specific_event").prop('checked')) {
+ var geographicPopulationObj = document.getElementById("geographic_population");
+ var geographicCasesObj = document.getElementById("geographic_cases");
+ removeErrorFor(geographicCasesObj);
+
+ if (parseInt(geographicPopulationObj.value) < parseInt(geographicCasesObj.value)) {
+ insertErrorFor(geographicCasesObj, "Cases > Population");
+ submit = false;
+ }
+ }
+
// Generate the short-range interactions list
var short_range_interactions = [];
$(".form_field_outer_row").each(function (index, element){
@@ -870,6 +911,12 @@ $(document).ready(function () {
// Call the function now to handle forward/back button presses in the browser.
on_hepa_option_change();
+ // When the p_recurrent_option changes we want to make its respective
+ // children show/hide.
+ $("input[type=radio][name=p_recurrent_option]").change(on_p_recurrent_change);
+ // Call the function now to handle forward/back button presses in the browser.
+ on_p_recurrent_change();
+
// When the mask_wearing_option changes we want to make its respective
// children show/hide.
$("input[type=radio][name=mask_wearing_option]").change(on_wearing_mask_change);
diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2
index 37d8d932..5278a040 100644
--- a/caimira/apps/templates/base/calculator.form.html.j2
+++ b/caimira/apps/templates/base/calculator.form.html.j2
@@ -317,6 +317,36 @@
+
+
+
+
+
+ ?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 34d12776..1b247015 100644
--- a/caimira/apps/templates/base/calculator.report.html.j2
+++ b/caimira/apps/templates/base/calculator.report.html.j2
@@ -57,6 +57,7 @@
{% if form.short_range_option == "short_range_yes" %}
{% set scenario = alternative_scenarios.stats.values() | first %}
{% set long_range_prob_inf = scenario.probability_of_infection %}
+ {% set long_range_prob_specific_event = scenario.prob_specific_event if form.p_recurrent_option == 'p_specific_event' %}
{% else %}
{% set long_range_prob_inf = prob_inf %}
{% endif %}
@@ -120,19 +121,41 @@
{% endif %}
{% block report_summary %}
-
-
+
+
+ Taking into account the uncertainties tied to the model variables, in this scenario and assuming all occupants are exposed equally (i.e. without short-range interactions), the probability of one exposed occupant getting infected is {{ long_range_prob_inf | non_zero_percentage }} and the expected number of new cases is {{ expected_new_cases | float_format }}*.
+
+ {% if form.short_range_option == "short_range_yes" %}
- Taking into account the uncertainties tied to the model variables, in this scenario and assuming all occupants are exposed equally (i.e. without short-range interactions), the probability of one exposed occupant getting infected is {{ long_range_prob_inf | non_zero_percentage }} and the expected number of new cases is {{ expected_new_cases | float_format }}*.
+ In this scenario, assuming short-range interactions occur, the probability of one exposed occupant getting infected can go as high as {{ prob_inf | non_zero_percentage }}.
- In this scenario, assuming short-range interactions occur, the probability of one exposed occupant getting infected can go as high as {{ prob_inf | non_zero_percentage }}.
+ The above {{ "result assumes" if form.short_range_option == "short_range_no" else "results assume" }} that {{ form.infected_people }}
+ {{ "occupant is infected" if form.infected_people == 1 else "occupants are infected" }}
+ in the room.
+ By taking into account the estimate of cases currently circulating in {{ form.location_name }},
+ the probability of on-site transmission, having at least 1 new infection in an event
+ with {{ form.total_people }} occupants, is
+ {% if form.short_range_option == 'short_range_yes' %}:
+
+
{{ long_range_prob_specific_event | non_zero_percentage }}, assuming all occupants are exposed equally (i.e. without short-range interactions).
The probability of infection is larger than above which signifies that the chosen number of infected occupants in the form was underestimated.
+ {% endif %}
{% endif %}
-
+ {% endblock %}
+
{% endblock report_summary %}
@@ -419,6 +442,18 @@
Number of attendees and infected people: {{ form.total_people }} in attendance, of whom {{ form.infected_people }}
{{ "is" if form.infected_people == 1 else "are" }}
infected.
+ {% if form.p_recurrent_option == "p_specific_event" %}
+ {% if form.ascertainment_bias == "confidence_high" %}
+ {% set conf_level = "High - Mandatory population surveillance." %}
+ {% elif form.ascertainment_bias == "confidence_medium" %}
+ {% set conf_level = "Medium - Recommended population wide surveillance." %}
+ {% else %}
+ {% set conf_level = "Low - Surveillance only for sympotmatic patients." %}
+ {% endif %}
+
Population in {{ form.location_name }}: {{ form.geographic_population }}
+
New reported cases in {{ form.location_name }} (7-day average): {{ form.geographic_cases }}
+
Confidence level: {{ conf_level }}
+ {% endif %}
Activity type:
{% if form.activity_type == "office" %}
diff --git a/caimira/apps/templates/base/userguide.html.j2 b/caimira/apps/templates/base/userguide.html.j2
index 7775a7ec..976ef328 100644
--- a/caimira/apps/templates/base/userguide.html.j2
+++ b/caimira/apps/templates/base/userguide.html.j2
@@ -113,7 +113,22 @@ The recommended airflow rate for the HEPA filter should correspond to a total ai
Here we capture the information about the event being simulated.
First enter the number of occupants in the space, if you have a (small) variation in the number of people, please input the average or consider using the expert tool.
Within the number of people occupying the space, you should specify how many are infected.
-
As an example, for a shared office with 4 people, where one person is infected, we enter 4 occupants and 1 infected person.
+
As an example, for a shared office with 4 people, where one person is infected, we enter 4 occupants and 1 infected person.
+
In case one would like to simulate an event happening at a given time and location, where the epidemiological situation is known, the tool allows for an estimation of the probability of on-site transmission, considering the chances that a given person in the event is infected.
+The user will need to select Specific event, input the number of inhabitants and the cumulative weekly (7-day average) value of new reported positive cases at the event location, as well as the confidence level of the inputs. The first two inputs need to the related, i.e. the values of reported new cases and the number of inhabitants shall correspond to the a same geographical location. For example:
+
+
Population of Geneva, CH: 508 000 inhabitants
+
New reported cases in the canton of Geneva: 1000 (in one week - avg)
+
+
The confidence level has the following options:
+
+
High - mandatory population wide surveillance
+
Medium - recommended population wide surveillance
+
Low - surveillance only for sympotmatic patients
+
+
Depending on the epidemiological situation in the choosen location, the public health surveillance can be more or less active. The confidence level will provide an ascertainment bias to the data collected by the user.
+
The higher the incidence rate (i.e. new cases / population) the higher are the chances of having at least one infected occupant participating to the event.
+For general and recurrent layout simply select the Recurrent exposure option.
Laboratory = light physical activity, talking 50% of the time,
Workshop = moderate physical activity, talking 50% of the time,
-
Conference/Training = speaker/trainer standing and talking, rest seated and talking quietly.
+
Conference/Training (speaker infected) = speaker/trainer standing and talking, rest seated and talking quietly.
Speaker/Trainer assumed infected (worst case scenario),
+
Conference/Training (attendee infected) = someone in the audience is infected, all are seated and breathing.
Gym = heavy exercise, no talking, just breathing.
Activity breaks:
diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2
index 1b247015..22a80dfc 100644
--- a/caimira/apps/templates/base/calculator.report.html.j2
+++ b/caimira/apps/templates/base/calculator.report.html.j2
@@ -473,7 +473,9 @@
{% elif form.activity_type == "workshop" %}
Workshop = assembly workshop environment, all persons doing moderate physical activity, speaking 50% of the time.
{% elif form.activity_type == "training" %}
- Conference/Training – one person (the speaker/trainer) standing, talking, all others seated, talking quietly (whispering). It is assumed the speaker/trainer is the infected person, for the worst case scenario.
+ Conference/Training (speaker infected) – one person (the speaker/trainer) standing, talking, all others seated, talking quietly (whispering). It is assumed the speaker/trainer is the infected person, for the worst case scenario.
+ {% elif form.activity_type == "training_attendee" %}
+ Conference/Training (attendee infected) – the infected person(s) are in the audience. All persons seated and breathing.
{% elif form.activity_type == "lab" %}
Laboratory = Lab or technical environment, all persons doing light physical activity, speaking 50% of the time.
{% elif form.activity_type == "gym" %}
diff --git a/caimira/apps/templates/base/userguide.html.j2 b/caimira/apps/templates/base/userguide.html.j2
index 976ef328..cf9ac568 100644
--- a/caimira/apps/templates/base/userguide.html.j2
+++ b/caimira/apps/templates/base/userguide.html.j2
@@ -143,9 +143,11 @@ For general and recurrent layout simply select the Recurrent exposure opt
Control Room (night shift) = All persons seated, all talking 10% of the time. Everyone (exposed and infected occupants) is treated the same in this model.
Lab = Based on a typical lab or technical working area, all persons are doing light activity and talking 50% of the time. Everyone (exposed and infected occupants) is treated the same in this model.
Workshop = Based on a mechanical assembly workshop or equipment installation scenario, all persons are doing moderate activity and talking 50% of the time. This activity is equally applicable to bicycling, or walking on a gradient, in the LHC tunnels. Everyone (exposed and infected occupants) is treated the same in this model.
-
Conference/Training = Based on a typical conference/training course scenario.
+
Conference/Training (speaker infected) = Based on a typical conference/training course scenario.
One individual (the speaker/trainer) is standing and talking, with all other individuals seated and talking quietly (whispering).
In this case it is assumed that the infected person is the speaker/trainer, because this is the worst case in terms of viral shedding.
+
Conference/Training (attendee infected) = All individuals seated and breathing.
+In this case it is assumed that the infected person is not the speaker/trainer.
Gym = All persons are doing heavy exercise and breathing (not talking). Everyone (exposed and infected occupants) is treated the same in this model.
Timings
@@ -196,7 +198,7 @@ If not, then you can input separate breaks. This is particularly different when
The model allows for a simulation with either a continuous wearing of face masks throughout the duration of the event, or have the removed at all times - i.e. all occupants (infected and exposed alike) wear or not masks for the duration of the simulation.
Please bear in mind the user inputs shall be aligned with the current applicable public health & safety instructions.
Please check what are the applicable rules, before deciding which assumptions are used for the simulation.
-
If you have selected the Conference/Training activity type, this equates to the speakr/trainer and all participants either wearing masks throughout the conference/training (Yes), or removing them when seated/standing at their socially distanced positions within the conference/training room (No).
+
If you have selected the Conference/Training activity type, this equates to the speaker/trainer and all participants either wearing masks throughout the conference/training (Yes), or removing them when seated/standing at their socially distanced positions within the conference/training room (No).
Please confirm what are the applicable rules, before deciding which assumptions are used for the simulation
For the time being only the Type 1 surgical and FFP2 masks can be selected.
From 1b3ef45b006752460fa7e9a4789e013962946184 Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Mon, 3 Oct 2022 14:40:12 +0100
Subject: [PATCH 05/15] changes to user guide
---
caimira/apps/templates/base/userguide.html.j2 | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/caimira/apps/templates/base/userguide.html.j2 b/caimira/apps/templates/base/userguide.html.j2
index cf9ac568..1ca1de8d 100644
--- a/caimira/apps/templates/base/userguide.html.j2
+++ b/caimira/apps/templates/base/userguide.html.j2
@@ -115,12 +115,14 @@ First enter the number of occupants in the space, if you have a (small) variatio
Within the number of people occupying the space, you should specify how many are infected.
As an example, for a shared office with 4 people, where one person is infected, we enter 4 occupants and 1 infected person.
In case one would like to simulate an event happening at a given time and location, where the epidemiological situation is known, the tool allows for an estimation of the probability of on-site transmission, considering the chances that a given person in the event is infected.
-The user will need to select Specific event, input the number of inhabitants and the cumulative weekly (7-day average) value of new reported positive cases at the event location, as well as the confidence level of the inputs. The first two inputs need to the related, i.e. the values of reported new cases and the number of inhabitants shall correspond to the a same geographical location. For example:
+The user will need to select Specific event, input the number of inhabitants and the the weekly (7-day rolling average) value of new reported laboratory - confirmed cases at the event location, as well as the confidence level of these inputs.
+The 7-day rolling average consists in the average of the previous 3 days to subsequent 3 days, generally reported by the different public health authorities (e.g. in Switzerland here).
+These two inputs need to the related, i.e. the values of reported new cases and the number of inhabitants shall correspond to the a same geographical location. For example:
Population of Geneva, CH: 508 000 inhabitants
-
New reported cases in the canton of Geneva: 1000 (in one week - avg)
+
New reported cases in the canton of Geneva: 1000 (7-day rolling avg)
-
The confidence level has the following options:
+
The confidence level allows for an ascertainment bias to the data. The user can add the following options:
High - mandatory population wide surveillance
Medium - recommended population wide surveillance
From 10d743c1497d375c7109c99fa900db71ca991f58 Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Mon, 3 Oct 2022 14:42:37 +0100
Subject: [PATCH 06/15] UI changes for probabilistic approach
---
caimira/apps/calculator/model_generator.py | 4 +--
caimira/apps/calculator/report_generator.py | 2 +-
caimira/apps/calculator/static/js/form.js | 12 ++++-----
.../templates/base/calculator.form.html.j2 | 25 ++++++++++---------
.../templates/base/calculator.report.html.j2 | 6 ++---
5 files changed, 25 insertions(+), 24 deletions(-)
diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py
index 4919b6c2..475ca028 100644
--- a/caimira/apps/calculator/model_generator.py
+++ b/caimira/apps/calculator/model_generator.py
@@ -60,7 +60,7 @@ class FormData:
geographic_population: int
geographic_cases: int
ascertainment_bias: str
- p_recurrent_option: str
+ exposure_option: str
mask_type: str
mask_wearing_option: str
mechanical_ventilation_type: str
@@ -123,7 +123,7 @@ class FormData:
'geographic_population': 0,
'geographic_cases': 0,
'ascertainment_bias': 'confidence_low',
- 'p_recurrent_option': 'p_recurrent_event',
+ 'exposure_option': 'p_deterministic_exposure',
'mask_type': 'Type I',
'mask_wearing_option': 'mask_off',
'mechanical_ventilation_type': 'not-applicable',
diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py
index 7317f2b0..a4cadd35 100644
--- a/caimira/apps/calculator/report_generator.py
+++ b/caimira/apps/calculator/report_generator.py
@@ -311,7 +311,7 @@ def comparison_report(
else:
statistics = {}
- if (form.short_range_option == "short_range_yes" and form.p_recurrent_option == "p_specific_event"):
+ if (form.short_range_option == "short_range_yes" and form.exposure_option == "p_probabilistic_exposure"):
specific_event = True
else:
specific_event = False
diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js
index 5e551b7d..79a0b277 100644
--- a/caimira/apps/calculator/static/js/form.js
+++ b/caimira/apps/calculator/static/js/form.js
@@ -61,10 +61,10 @@ function require_fields(obj) {
case "hepa_no":
require_hepa(false);
break;
- case "p_specific_event":
+ case "p_probabilistic_exposure":
require_population(true);
break;
- case "p_recurrent_event":
+ case "p_deterministic_exposure":
require_population(false);
break;
case "mask_on":
@@ -282,7 +282,7 @@ function on_hepa_option_change() {
}
function on_p_recurrent_change() {
- p_recurrent = $('input[type=radio][name=p_recurrent_option]')
+ p_recurrent = $('input[type=radio][name=exposure_option]')
p_recurrent.each(function (index) {
if (this.checked) {
getChildElement($(this)).show();
@@ -568,7 +568,7 @@ function validate_form(form) {
}
// Validate cases < population
- if ($("#p_specific_event").prop('checked')) {
+ if ($("#p_probabilistic_exposure").prop('checked')) {
var geographicPopulationObj = document.getElementById("geographic_population");
var geographicCasesObj = document.getElementById("geographic_cases");
removeErrorFor(geographicCasesObj);
@@ -911,9 +911,9 @@ $(document).ready(function () {
// Call the function now to handle forward/back button presses in the browser.
on_hepa_option_change();
- // When the p_recurrent_option changes we want to make its respective
+ // When the exposure_option changes we want to make its respective
// children show/hide.
- $("input[type=radio][name=p_recurrent_option]").change(on_p_recurrent_change);
+ $("input[type=radio][name=exposure_option]").change(on_p_recurrent_change);
// Call the function now to handle forward/back button presses in the browser.
on_p_recurrent_change();
diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2
index 35bbd69f..c658a7d3 100644
--- a/caimira/apps/templates/base/calculator.form.html.j2
+++ b/caimira/apps/templates/base/calculator.form.html.j2
@@ -303,7 +303,8 @@
Event data:
-
+
?
@@ -312,20 +313,20 @@
-
-
-
+
+
+
-
-
-
-
-
-
- ?
+
+
+
-
+
+
+
+
+
diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2
index 22a80dfc..19e7732d 100644
--- a/caimira/apps/templates/base/calculator.report.html.j2
+++ b/caimira/apps/templates/base/calculator.report.html.j2
@@ -57,7 +57,7 @@
{% if form.short_range_option == "short_range_yes" %}
{% set scenario = alternative_scenarios.stats.values() | first %}
{% set long_range_prob_inf = scenario.probability_of_infection %}
- {% set long_range_prob_specific_event = scenario.prob_specific_event if form.p_recurrent_option == 'p_specific_event' %}
+ {% set long_range_prob_specific_event = scenario.prob_specific_event if form.exposure_option == 'p_probabilistic_exposure' %}
{% else %}
{% set long_range_prob_inf = prob_inf %}
{% endif %}
@@ -132,7 +132,7 @@
The above {{ "result assumes" if form.short_range_option == "short_range_no" else "results assume" }} that {{ form.infected_people }}
@@ -442,7 +442,7 @@
Number of attendees and infected people: {{ form.total_people }} in attendance, of whom {{ form.infected_people }}
{{ "is" if form.infected_people == 1 else "are" }}
infected.
- {% if form.p_recurrent_option == "p_specific_event" %}
+ {% if form.exposure_option == "p_probabilistic_exposure" %}
{% if form.ascertainment_bias == "confidence_high" %}
{% set conf_level = "High - Mandatory population surveillance." %}
{% elif form.ascertainment_bias == "confidence_medium" %}
From c9134679fd40014dd26c1cde43b45811da83b650 Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Mon, 3 Oct 2022 16:49:54 +0100
Subject: [PATCH 07/15] added default number of infected population and made
few UI changes
---
caimira/apps/calculator/model_generator.py | 2 +-
caimira/apps/calculator/report_generator.py | 20 +++++++++----------
caimira/apps/calculator/static/css/form.css | 4 ++--
caimira/apps/calculator/static/js/form.js | 14 ++++++++++---
.../templates/base/calculator.form.html.j2 | 18 ++++++++++-------
.../templates/base/calculator.report.html.j2 | 15 ++++++--------
caimira/apps/templates/base/userguide.html.j2 | 13 ++++++------
.../templates/cern/calculator.report.html.j2 | 4 ++--
caimira/tests/models/test_exposure_model.py | 8 ++++----
9 files changed, 53 insertions(+), 45 deletions(-)
diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py
index 475ca028..49fb639a 100644
--- a/caimira/apps/calculator/model_generator.py
+++ b/caimira/apps/calculator/model_generator.py
@@ -114,7 +114,7 @@ class FormData:
'infected_lunch_finish': '13:30',
'infected_lunch_option': True,
'infected_lunch_start': '12:30',
- 'infected_people': _NO_DEFAULT,
+ 'infected_people': 1,
'infected_start': '08:30',
'inside_temp': _NO_DEFAULT,
'location_latitude': _NO_DEFAULT,
diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py
index a4cadd35..f171fe01 100644
--- a/caimira/apps/calculator/report_generator.py
+++ b/caimira/apps/calculator/report_generator.py
@@ -131,7 +131,7 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing
])
prob = np.array(model.infection_probability()).mean()
- prob_specific_event = np.array(model.total_probability_rule()).mean()
+ 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
expected_new_cases = np.array(model.expected_new_cases()).mean()
@@ -148,7 +148,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,
- "prob_specific_event": prob_specific_event,
+ "prob_probabilistic_exposure": prob_probabilistic_exposure,
"emission_rate": er,
"exposed_occupants": exposed_occupants,
"expected_new_cases": expected_new_cases,
@@ -274,13 +274,13 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, mc.Exp
return scenarios
-def scenario_statistics(mc_model: mc.ExposureModel, sample_times: typing.List[float], specific_event: bool):
+def scenario_statistics(mc_model: mc.ExposureModel, sample_times: typing.List[float], probabilistic_exposure: bool):
model = mc_model.build_model(size=_DEFAULT_MC_SAMPLE_SIZE)
- if (specific_event):
+ if (probabilistic_exposure):
# It means we have data to calculate the total_probability_rule
- prob_specific_event = np.array(model.total_probability_rule()).mean()
+ prob_probabilistic_exposure = np.array(model.total_probability_rule()).mean()
else:
- prob_specific_event = 0.
+ prob_probabilistic_exposure = 0.
return {
'probability_of_infection': np.mean(model.infection_probability()),
@@ -289,7 +289,7 @@ def scenario_statistics(mc_model: mc.ExposureModel, sample_times: typing.List[fl
np.mean(model.concentration(time))
for time in sample_times
],
- 'prob_specific_event': prob_specific_event,
+ 'prob_probabilistic_exposure': prob_probabilistic_exposure,
}
@@ -312,16 +312,16 @@ def comparison_report(
statistics = {}
if (form.short_range_option == "short_range_yes" and form.exposure_option == "p_probabilistic_exposure"):
- specific_event = True
+ probabilistic_exposure = True
else:
- specific_event = False
+ probabilistic_exposure = False
with executor_factory() as executor:
results = executor.map(
scenario_statistics,
scenarios.values(),
[sample_times] * len(scenarios),
- [specific_event] * len(scenarios),
+ [probabilistic_exposure] * len(scenarios),
timeout=60,
)
diff --git a/caimira/apps/calculator/static/css/form.css b/caimira/apps/calculator/static/css/form.css
index 4543fefc..dd08db56 100644
--- a/caimira/apps/calculator/static/css/form.css
+++ b/caimira/apps/calculator/static/css/form.css
@@ -66,7 +66,7 @@
content: attr(data-tooltip);
padding: 10px 18px;
min-width: 50px;
- max-width: 200px;
+ max-width: 220px;
width: max-content;
width: -moz-max-content;
border-radius: 6px;
@@ -113,4 +113,4 @@
transition-delay: 0.5s; /* Starting after the grow effect */
transition-duration: 0.2s;
transform: translateX(-50%) scaleY(1);
-}
\ No newline at end of file
+}
diff --git a/caimira/apps/calculator/static/js/form.js b/caimira/apps/calculator/static/js/form.js
index 79a0b277..6ed6c0b3 100644
--- a/caimira/apps/calculator/static/js/form.js
+++ b/caimira/apps/calculator/static/js/form.js
@@ -63,9 +63,11 @@ function require_fields(obj) {
break;
case "p_probabilistic_exposure":
require_population(true);
+ require_infected(false);
break;
case "p_deterministic_exposure":
require_population(false);
+ require_infected(true);
break;
case "mask_on":
require_mask(true);
@@ -184,6 +186,10 @@ function require_population(option) {
require_input_field("#ascertainment_bias", option);
}
+function require_infected(option) {
+ require_input_field("#infected_people", option);
+}
+
function require_mask(option) {
$("#mask_type_1").prop('required', option);
$("#mask_type_ffp2").prop('required', option);
@@ -281,7 +287,7 @@ function on_hepa_option_change() {
})
}
-function on_p_recurrent_change() {
+function on_exposure_change() {
p_recurrent = $('input[type=radio][name=exposure_option]')
p_recurrent.each(function (index) {
if (this.checked) {
@@ -569,6 +575,8 @@ function validate_form(form) {
// Validate cases < population
if ($("#p_probabilistic_exposure").prop('checked')) {
+ // Set number of infected people as 1
+ $("#infected_people").val(1);
var geographicPopulationObj = document.getElementById("geographic_population");
var geographicCasesObj = document.getElementById("geographic_cases");
removeErrorFor(geographicCasesObj);
@@ -913,9 +921,9 @@ $(document).ready(function () {
// When the exposure_option changes we want to make its respective
// children show/hide.
- $("input[type=radio][name=exposure_option]").change(on_p_recurrent_change);
+ $("input[type=radio][name=exposure_option]").change(on_exposure_change);
// Call the function now to handle forward/back button presses in the browser.
- on_p_recurrent_change();
+ on_exposure_change();
// When the mask_wearing_option changes we want to make its respective
// children show/hide.
diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2
index c658a7d3..6698b0bf 100644
--- a/caimira/apps/templates/base/calculator.form.html.j2
+++ b/caimira/apps/templates/base/calculator.form.html.j2
@@ -303,8 +303,9 @@
Event data:
-
+
?
@@ -314,26 +315,29 @@
-
+
-
-
+
+
-
+
+
+
+
-
+
diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2
index 19e7732d..c988b594 100644
--- a/caimira/apps/templates/base/calculator.report.html.j2
+++ b/caimira/apps/templates/base/calculator.report.html.j2
@@ -57,7 +57,7 @@
{% if form.short_range_option == "short_range_yes" %}
{% set scenario = alternative_scenarios.stats.values() | first %}
{% set long_range_prob_inf = scenario.probability_of_infection %}
- {% set long_range_prob_specific_event = scenario.prob_specific_event if form.exposure_option == 'p_probabilistic_exposure' %}
+ {% set long_range_prob_probabilistic_exposure = scenario.prob_probabilistic_exposure if form.exposure_option == 'p_probabilistic_exposure' %}
{% else %}
{% set long_range_prob_inf = prob_inf %}
{% endif %}
@@ -131,7 +131,7 @@
In this scenario, assuming short-range interactions occur, the probability of one exposed occupant getting infected can go as high as {{ prob_inf | non_zero_percentage }}.
{{ long_range_prob_probabilistic_exposure | non_zero_percentage }}, assuming all occupants are exposed equally (i.e. without short-range interactions).
diff --git a/caimira/apps/templates/base/userguide.html.j2 b/caimira/apps/templates/base/userguide.html.j2
index 1ca1de8d..14929ff9 100644
--- a/caimira/apps/templates/base/userguide.html.j2
+++ b/caimira/apps/templates/base/userguide.html.j2
@@ -113,24 +113,23 @@ The recommended airflow rate for the HEPA filter should correspond to a total ai
Here we capture the information about the event being simulated.
First enter the number of occupants in the space, if you have a (small) variation in the number of people, please input the average or consider using the expert tool.
Within the number of people occupying the space, you should specify how many are infected.
-
As an example, for a shared office with 4 people, where one person is infected, we enter 4 occupants and 1 infected person.
In case one would like to simulate an event happening at a given time and location, where the epidemiological situation is known, the tool allows for an estimation of the probability of on-site transmission, considering the chances that a given person in the event is infected.
-The user will need to select Specific event, input the number of inhabitants and the the weekly (7-day rolling average) value of new reported laboratory - confirmed cases at the event location, as well as the confidence level of these inputs.
+The user will need to select Probabilistic event, input the number of inhabitants and the the weekly (7-day rolling average) value of new reported laboratory - confirmed cases at the event location, as well as the confidence level of these inputs.
The 7-day rolling average consists in the average of the previous 3 days to subsequent 3 days, generally reported by the different public health authorities (e.g. in Switzerland here).
These two inputs need to the related, i.e. the values of reported new cases and the number of inhabitants shall correspond to the a same geographical location. For example:
Population of Geneva, CH: 508 000 inhabitants
-
New reported cases in the canton of Geneva: 1000 (7-day rolling avg)
+
New lab reported cases in the canton of Geneva: 1000 (7-day rolling avg)
The confidence level allows for an ascertainment bias to the data. The user can add the following options:
High - mandatory population wide surveillance
-
Medium - recommended population wide surveillance
-
Low - surveillance only for sympotmatic patients
+
Medium - recommended population-wide surveillance
+
Low - surveillance only for symptomatic patients
Depending on the epidemiological situation in the choosen location, the public health surveillance can be more or less active. The confidence level will provide an ascertainment bias to the data collected by the user.
-
The higher the incidence rate (i.e. new cases / population) the higher are the chances of having at least one infected occupant participating to the event.
-For general and recurrent layout simply select the Recurrent exposure option.
+
The higher the incidence rate (i.e. new cases / population) the higher are the chances of having at least one infected occupant participating to the event.
+
For general and recurrent layout simply select the Deterministic exposure option. As an example, for a shared office with 4 people, where one person is infected, we enter 4 occupants and 1 infected person.
{% endif %}
- {% block specific_event_probability %}
+ {% block probabilistic_exposure_probability %}
{{ super() }}
- {% endblock specific_event_probability %}
+ {% endblock probabilistic_exposure_probability %}
{% if (prob_inf > 2) %}
diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py
index 16fded63..bc9302d2 100644
--- a/caimira/tests/models/test_exposure_model.py
+++ b/caimira/tests/models/test_exposure_model.py
@@ -266,16 +266,16 @@ def test_prob_meet_infected_person(pop, cases, AB, exposed, infected, prob_meet_
@pytest.mark.parametrize(
- "population, cm, pop, cases, AB, specific_event_probability",[
+ "population, cm, pop, cases, AB, probabilistic_exposure_probability",[
[populations[1], known_concentrations(lambda t: 36.),
100000, 68, 5, 2.24124],
[populations[0], known_concentrations(lambda t: 36.),
100000, 68, 5, 1.875652],
])
-def test_specific_event_probability(population, cm,
- pop, AB, cases, specific_event_probability):
+def test_probabilistic_exposure_probability(population, cm,
+ pop, AB, cases, probabilistic_exposure_probability):
model = ExposureModel(cm, (), population, models.Cases(geographic_population=pop,
geographic_cases=cases, ascertainment_bias=AB))
np.testing.assert_allclose(
- model.total_probability_rule(), specific_event_probability, rtol=0.05
+ model.total_probability_rule(), probabilistic_exposure_probability, rtol=0.05
)
From a61b88ce1b50e9cc630e050d08849a218a4eb970 Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Tue, 4 Oct 2022 11:35:51 +0200
Subject: [PATCH 08/15] Changed variable names
---
caimira/models.py | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/caimira/models.py b/caimira/models.py
index f07b715c..913487e9 100644
--- a/caimira/models.py
+++ b/caimira/models.py
@@ -916,7 +916,7 @@ class InfectedPopulation(_PopulationWithVirus):
class Cases:
"""
The geographical data to calculate the probability of having at least 1
- new infection in a specific event.
+ new infection in a probabilistic exposure.
"""
#: Geographic location population
geographic_population: int = 0
@@ -931,12 +931,12 @@ class Cases:
"""Probability that a randomly selected individual in a focal population is infected."""
return self.geographic_cases*self.ascertainment_bias/self.geographic_population
- def probability_meet_infected_person(self, event, x) -> _VectorisedFloat:
+ def probability_meet_infected_person(self, event_population: int, n_infected: int) -> _VectorisedFloat:
"""
- Probability to meet x infected persons in an event.
+ Probability to meet n_infected persons in an event.
From https://doi.org/10.1038/s41562-020-01000-9.
"""
- return sct.binom.pmf(x, event, self.probability_random_individual())
+ return sct.binom.pmf(n_infected, event_population, self.probability_random_individual())
@dataclass(frozen=True)
@@ -1469,19 +1469,19 @@ class ExposureModel:
def total_probability_rule(self) -> _VectorisedFloat:
if (self.geographical_data.geographic_population != 0 and self.geographical_data.geographic_cases != 0):
sum_probability = 0.0
- # Create an equivalent exposure model but with i infected cases
+ # Create an equivalent exposure model but changing the number of infected cases.
total_people = self.concentration_model.infected.number + self.exposed.number
- X = (total_people if total_people < 10 else 10)
+ max_num_infected = (total_people if total_people < 10 else 10)
# The influence of a higher number of simultainious infected people (> 4 - 5) yields an almost negligible contirbution to the total probability.
# To be on the safe side, a hard coded limit with a safety margin of 2x was set.
# Therefore we decided a hard limit of 10 infected people.
- for x in range(1, X):
+ for num_infected in range(1, max_num_infected):
exposure_model = nested_replace(
- self, {'concentration_model.infected.number': x}
+ self, {'concentration_model.infected.number': num_infected}
)
prob_exposed_occupant = exposure_model.infection_probability().mean() / 100
# By means of a Binomial Distribution
- sum_probability += (prob_exposed_occupant)*self.geographical_data.probability_meet_infected_person(self.exposed.number, x)
+ sum_probability += (prob_exposed_occupant)*self.geographical_data.probability_meet_infected_person(self.exposed.number, num_infected)
return sum_probability * 100
else:
return 0
From 9032a8815d0d5e1a8872fdada9919a97d78f281b Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Tue, 4 Oct 2022 14:17:32 +0200
Subject: [PATCH 09/15] changed variable name in report_generator and added a
retry decorator in test
---
caimira/apps/calculator/report_generator.py | 10 +++++-----
caimira/models.py | 2 +-
caimira/tests/test_full_algorithm.py | 1 +
3 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py
index f171fe01..8f30876c 100644
--- a/caimira/apps/calculator/report_generator.py
+++ b/caimira/apps/calculator/report_generator.py
@@ -274,9 +274,9 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, mc.Exp
return scenarios
-def scenario_statistics(mc_model: mc.ExposureModel, sample_times: typing.List[float], probabilistic_exposure: bool):
+def scenario_statistics(mc_model: mc.ExposureModel, sample_times: typing.List[float], compute_prob_exposure: bool):
model = mc_model.build_model(size=_DEFAULT_MC_SAMPLE_SIZE)
- if (probabilistic_exposure):
+ if (compute_prob_exposure):
# It means we have data to calculate the total_probability_rule
prob_probabilistic_exposure = np.array(model.total_probability_rule()).mean()
else:
@@ -312,16 +312,16 @@ def comparison_report(
statistics = {}
if (form.short_range_option == "short_range_yes" and form.exposure_option == "p_probabilistic_exposure"):
- probabilistic_exposure = True
+ compute_prob_exposure = True
else:
- probabilistic_exposure = False
+ compute_prob_exposure = False
with executor_factory() as executor:
results = executor.map(
scenario_statistics,
scenarios.values(),
[sample_times] * len(scenarios),
- [probabilistic_exposure] * len(scenarios),
+ [compute_prob_exposure] * len(scenarios),
timeout=60,
)
diff --git a/caimira/models.py b/caimira/models.py
index 913487e9..92589a22 100644
--- a/caimira/models.py
+++ b/caimira/models.py
@@ -1475,7 +1475,7 @@ class ExposureModel:
# The influence of a higher number of simultainious infected people (> 4 - 5) yields an almost negligible contirbution to the total probability.
# To be on the safe side, a hard coded limit with a safety margin of 2x was set.
# Therefore we decided a hard limit of 10 infected people.
- for num_infected in range(1, max_num_infected):
+ for num_infected in range(1, max_num_infected + 1):
exposure_model = nested_replace(
self, {'concentration_model.infected.number': num_infected}
)
diff --git a/caimira/tests/test_full_algorithm.py b/caimira/tests/test_full_algorithm.py
index 0ef12fda..aea4151c 100644
--- a/caimira/tests/test_full_algorithm.py
+++ b/caimira/tests/test_full_algorithm.py
@@ -785,6 +785,7 @@ def test_concentration_with_shortrange(expo_sr_model,simple_expo_sr_model,time):
)
+@retry(tries=10)
def test_exposure_with_shortrange(expo_sr_model,simple_expo_sr_model):
npt.assert_allclose(
expo_sr_model.build_model(SAMPLE_SIZE).deposited_exposure().mean(),
From 7a9401a6fae051da9388c447c16f3767fee07b23 Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Wed, 5 Oct 2022 14:28:53 +0200
Subject: [PATCH 10/15] added number of incubation days as property of the
virus and adapted tests
---
caimira/models.py | 14 ++++---
caimira/tests/models/test_exposure_model.py | 42 ++++++++++++---------
2 files changed, 34 insertions(+), 22 deletions(-)
diff --git a/caimira/models.py b/caimira/models.py
index 92589a22..b7867e69 100644
--- a/caimira/models.py
+++ b/caimira/models.py
@@ -439,6 +439,9 @@ class Virus:
#: Pre-populated examples of Viruses.
types: typing.ClassVar[typing.Dict[str, "Virus"]]
+ #: Number of incubation days
+ infectiousness_days: int = 14
+
def halflife(self, humidity: _VectorisedFloat, inside_temp: _VectorisedFloat) -> _VectorisedFloat:
# Biological decay (inactivation of the virus in air) - virus
# dependent and function of humidity
@@ -927,16 +930,16 @@ class Cases:
#: Number of new cases confidence level
ascertainment_bias: int = 0
- def probability_random_individual(self) -> _VectorisedFloat:
+ def probability_random_individual(self, virus: Virus) -> _VectorisedFloat:
"""Probability that a randomly selected individual in a focal population is infected."""
- return self.geographic_cases*self.ascertainment_bias/self.geographic_population
+ return self.geographic_cases*virus.infectiousness_days*self.ascertainment_bias/self.geographic_population
- def probability_meet_infected_person(self, event_population: int, n_infected: int) -> _VectorisedFloat:
+ def probability_meet_infected_person(self, virus: Virus, event_population: int, n_infected: int) -> _VectorisedFloat:
"""
Probability to meet n_infected persons in an event.
From https://doi.org/10.1038/s41562-020-01000-9.
"""
- return sct.binom.pmf(n_infected, event_population, self.probability_random_individual())
+ return sct.binom.pmf(n_infected, event_population, self.probability_random_individual(virus))
@dataclass(frozen=True)
@@ -1481,7 +1484,8 @@ class ExposureModel:
)
prob_exposed_occupant = exposure_model.infection_probability().mean() / 100
# By means of a Binomial Distribution
- sum_probability += (prob_exposed_occupant)*self.geographical_data.probability_meet_infected_person(self.exposed.number, num_infected)
+ sum_probability += (prob_exposed_occupant *
+ self.geographical_data.probability_meet_infected_person(self.concentration_model.infected.virus, self.exposed.number, num_infected))
return sum_probability * 100
else:
return 0
diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py
index bc9302d2..7d4a93b5 100644
--- a/caimira/tests/models/test_exposure_model.py
+++ b/caimira/tests/models/test_exposure_model.py
@@ -234,43 +234,51 @@ def test_infectious_dose_vectorisation(sr_model):
@pytest.mark.parametrize(
- "pop, cases, AB, prob_random_individual", [
- [100_000, 67, 5, 0.00335],
- [200_000, 121, 5, 0.003025],
- [np.array([100_000, 200_000]), 67, 10, np.array([0.0067, 0.00335])],
- [150_000, np.array([67, 121]), 2, np.array([0.00089333, 0.00161333])],
- [np.array([100_000, 200_000]), np.array([67, 121]), 5, np.array([0.00335, 0.003025])]
+ "pop, cases, infectiousness_days, AB, prob_random_individual", [
+ [100_000, 68, 7, 5, 0.02345],
+ [200_000, 121, np.array([7, 14]), 5, np.array([0.021175, 0.042350])],
+ [np.array([100_000, 200_000]), 68, 14, 10, np.array([0.0952, 0.0476])],
+ [150_000, np.array([68, 121]), 14, 2, np.array([0.012693, 0.022587])],
+ [np.array([100_000, 200_000]), np.array([68, 121]), 7, 5, np.array([0.023450, 0.021175])]
]
)
-def test_probability_random_individual(pop, cases, AB, prob_random_individual):
- model = models.Cases(geographic_population=pop, geographic_cases=cases,
+def test_probability_random_individual(pop, cases, infectiousness_days, AB, prob_random_individual):
+ cases = models.Cases(geographic_population=pop, geographic_cases=cases,
ascertainment_bias=AB)
+ virus=models.SARSCoV2(
+ viral_load_in_sputum=1e9,
+ infectious_dose=50.,
+ viable_to_RNA_ratio = 0.5,
+ transmissibility_factor=1,
+ infectiousness_days=infectiousness_days,
+ )
np.testing.assert_allclose(
- model.probability_random_individual(), prob_random_individual, rtol=0.05
+ cases.probability_random_individual(virus), prob_random_individual, rtol=0.05
)
@pytest.mark.parametrize(
"pop, cases, AB, exposed, infected, prob_meet_infected_person", [
- [100_000, 67, 5, 10, 2, 0.00049],
- [200_000, 121, 5, 10, 1, 0.02944],
- [np.array([100_000, 200_000]), 67, 10, 15, 2, np.array([0.00432, 0.00113])],
- [150_000, np.array([67, 121]), 2, np.array([10, 15]), np.array([1, 2]), np.array([0.00886, 0.00027])],
+ [100_000, 68, 5, 10, 1, 0.306889],
+ [200_000, 121, 5, 10, 1, 0.286890],
+ [np.array([100_000, 200_000]), 68, 10, 15, 2, np.array([0.259207, 0.126199])],
+ [150_000, np.array([68, 121]), 2, np.array([10, 15]), np.array([1, 2]), np.array([0.113147, 0.039803])],
]
)
def test_prob_meet_infected_person(pop, cases, AB, exposed, infected, prob_meet_infected_person):
- model = models.Cases(geographic_population=pop, geographic_cases=cases,
+ cases = models.Cases(geographic_population=pop, geographic_cases=cases,
ascertainment_bias=AB)
- np.testing.assert_allclose(model.probability_meet_infected_person(exposed, infected),
+ virus = models.Virus.types['SARS_CoV_2']
+ np.testing.assert_allclose(cases.probability_meet_infected_person(virus, exposed, infected),
prob_meet_infected_person, rtol=0.05)
@pytest.mark.parametrize(
"population, cm, pop, cases, AB, probabilistic_exposure_probability",[
[populations[1], known_concentrations(lambda t: 36.),
- 100000, 68, 5, 2.24124],
+ 100000, 68, 5, 27.537276],
[populations[0], known_concentrations(lambda t: 36.),
- 100000, 68, 5, 1.875652],
+ 100000, 68, 5, 23.540145],
])
def test_probabilistic_exposure_probability(population, cm,
pop, AB, cases, probabilistic_exposure_probability):
From f7b63decc37900b5f1791e91caadcffeb05115d5 Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Wed, 5 Oct 2022 17:33:12 +0200
Subject: [PATCH 11/15] Modified total probability rule formula
---
caimira/models.py | 12 +++++++-----
caimira/tests/models/test_exposure_model.py | 4 ++--
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/caimira/models.py b/caimira/models.py
index b7867e69..aac1380f 100644
--- a/caimira/models.py
+++ b/caimira/models.py
@@ -439,7 +439,7 @@ class Virus:
#: Pre-populated examples of Viruses.
types: typing.ClassVar[typing.Dict[str, "Virus"]]
- #: Number of incubation days
+ #: Number of days the infector is contagious
infectiousness_days: int = 14
def halflife(self, humidity: _VectorisedFloat, inside_temp: _VectorisedFloat) -> _VectorisedFloat:
@@ -1482,10 +1482,12 @@ class ExposureModel:
exposure_model = nested_replace(
self, {'concentration_model.infected.number': num_infected}
)
- prob_exposed_occupant = exposure_model.infection_probability().mean() / 100
- # By means of a Binomial Distribution
- sum_probability += (prob_exposed_occupant *
- self.geographical_data.probability_meet_infected_person(self.concentration_model.infected.virus, self.exposed.number, num_infected))
+ prob_ind = exposure_model.infection_probability().mean() / 100
+ exposed_ind = self.exposed.number
+ # By means of the total probability rule
+ prob_at_least_one_infected = 1 - (1 - prob_ind)**(exposed_ind-1)
+ sum_probability += (prob_at_least_one_infected *
+ self.geographical_data.probability_meet_infected_person(self.concentration_model.infected.virus, exposed_ind, num_infected))
return sum_probability * 100
else:
return 0
diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py
index 7d4a93b5..ae25de05 100644
--- a/caimira/tests/models/test_exposure_model.py
+++ b/caimira/tests/models/test_exposure_model.py
@@ -276,9 +276,9 @@ def test_prob_meet_infected_person(pop, cases, AB, exposed, infected, prob_meet_
@pytest.mark.parametrize(
"population, cm, pop, cases, AB, probabilistic_exposure_probability",[
[populations[1], known_concentrations(lambda t: 36.),
- 100000, 68, 5, 27.537276],
+ 100000, 68, 5, 38.594805],
[populations[0], known_concentrations(lambda t: 36.),
- 100000, 68, 5, 23.540145],
+ 100000, 121, 2, 29.138216],
])
def test_probabilistic_exposure_probability(population, cm,
pop, AB, cases, probabilistic_exposure_probability):
From 52615b1cfba773ae3e6a93d7d69ce22b3a9d89ba Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Fri, 7 Oct 2022 09:03:24 +0200
Subject: [PATCH 12/15] updated based on test excel results
---
caimira/models.py | 9 +++---
caimira/tests/models/test_exposure_model.py | 32 ++++++++++++---------
2 files changed, 24 insertions(+), 17 deletions(-)
diff --git a/caimira/models.py b/caimira/models.py
index aac1380f..a3f3a5a2 100644
--- a/caimira/models.py
+++ b/caimira/models.py
@@ -934,7 +934,7 @@ class Cases:
"""Probability that a randomly selected individual in a focal population is infected."""
return self.geographic_cases*virus.infectiousness_days*self.ascertainment_bias/self.geographic_population
- def probability_meet_infected_person(self, virus: Virus, event_population: int, n_infected: int) -> _VectorisedFloat:
+ def probability_meet_infected_person(self, virus: Virus, n_infected: int, event_population: int) -> _VectorisedFloat:
"""
Probability to meet n_infected persons in an event.
From https://doi.org/10.1038/s41562-020-01000-9.
@@ -1483,11 +1483,12 @@ class ExposureModel:
self, {'concentration_model.infected.number': num_infected}
)
prob_ind = exposure_model.infection_probability().mean() / 100
- exposed_ind = self.exposed.number
+ print(prob_ind)
+ n = total_people - num_infected
# By means of the total probability rule
- prob_at_least_one_infected = 1 - (1 - prob_ind)**(exposed_ind-1)
+ prob_at_least_one_infected = 1 - (1 - prob_ind)**n
sum_probability += (prob_at_least_one_infected *
- self.geographical_data.probability_meet_infected_person(self.concentration_model.infected.virus, exposed_ind, num_infected))
+ self.geographical_data.probability_meet_infected_person(self.concentration_model.infected.virus, num_infected, total_people))
return sum_probability * 100
else:
return 0
diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py
index ae25de05..a67568b0 100644
--- a/caimira/tests/models/test_exposure_model.py
+++ b/caimira/tests/models/test_exposure_model.py
@@ -235,7 +235,7 @@ def test_infectious_dose_vectorisation(sr_model):
@pytest.mark.parametrize(
"pop, cases, infectiousness_days, AB, prob_random_individual", [
- [100_000, 68, 7, 5, 0.02345],
+ [100_000, 68, 7, 5, 0.023800],
[200_000, 121, np.array([7, 14]), 5, np.array([0.021175, 0.042350])],
[np.array([100_000, 200_000]), 68, 14, 10, np.array([0.0952, 0.0476])],
[150_000, np.array([68, 121]), 14, 2, np.array([0.012693, 0.022587])],
@@ -259,31 +259,37 @@ def test_probability_random_individual(pop, cases, infectiousness_days, AB, prob
@pytest.mark.parametrize(
"pop, cases, AB, exposed, infected, prob_meet_infected_person", [
- [100_000, 68, 5, 10, 1, 0.306889],
- [200_000, 121, 5, 10, 1, 0.286890],
- [np.array([100_000, 200_000]), 68, 10, 15, 2, np.array([0.259207, 0.126199])],
- [150_000, np.array([68, 121]), 2, np.array([10, 15]), np.array([1, 2]), np.array([0.113147, 0.039803])],
+ [100000, 68, 5, 10, 1, 0.321509274],
+ [100000, 121, 5, 20, 1, 0.302950694],
+ [100000, np.array([68, 121]), 5, np.array([10, 20]), 1, np.array([0.321509274, 0.302950694])],
]
)
def test_prob_meet_infected_person(pop, cases, AB, exposed, infected, prob_meet_infected_person):
cases = models.Cases(geographic_population=pop, geographic_cases=cases,
ascertainment_bias=AB)
virus = models.Virus.types['SARS_CoV_2']
- np.testing.assert_allclose(cases.probability_meet_infected_person(virus, exposed, infected),
+ np.testing.assert_allclose(cases.probability_meet_infected_person(virus, infected, exposed+infected),
prob_meet_infected_person, rtol=0.05)
@pytest.mark.parametrize(
- "population, cm, pop, cases, AB, probabilistic_exposure_probability",[
- [populations[1], known_concentrations(lambda t: 36.),
- 100000, 68, 5, 38.594805],
- [populations[0], known_concentrations(lambda t: 36.),
- 100000, 121, 2, 29.138216],
+ "exposed_population, cm, pop, cases, AB, probabilistic_exposure_probability",[
+ [10, known_concentrations(lambda t: 36.),
+ 100000, 68, 5, 41.51920685],
+ [20, known_concentrations(lambda t: 72.),
+ 100000, 68, 5, 64.09068488],
+ [30, known_concentrations(lambda t: 1.2),
+ 100000, 68, 5, 55.93154502],
])
-def test_probabilistic_exposure_probability(population, cm,
+def test_probabilistic_exposure_probability(exposed_population, cm,
pop, AB, cases, probabilistic_exposure_probability):
+
+ population = models.Population(
+ exposed_population, models.PeriodicInterval(120, 60), models.Mask.types['Type I'],
+ models.Activity.types['Standing'], host_immunity=0.,)
model = ExposureModel(cm, (), population, models.Cases(geographic_population=pop,
- geographic_cases=cases, ascertainment_bias=AB))
+ geographic_cases=cases, ascertainment_bias=AB),)
+
np.testing.assert_allclose(
model.total_probability_rule(), probabilistic_exposure_probability, rtol=0.05
)
From a91ef8f9533c81703bf7751c4bc8c80bf96ae905 Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Fri, 7 Oct 2022 09:10:12 +0200
Subject: [PATCH 13/15] updated short range message for P(I) one infected
---
caimira/apps/templates/base/calculator.report.html.j2 | 2 +-
caimira/models.py | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2
index c988b594..244e0758 100644
--- a/caimira/apps/templates/base/calculator.report.html.j2
+++ b/caimira/apps/templates/base/calculator.report.html.j2
@@ -144,7 +144,7 @@
{% if form.short_range_option == 'short_range_yes' %}:
{{ long_range_prob_probabilistic_exposure | non_zero_percentage }}, assuming all occupants are exposed equally (i.e. without short-range interactions).