Compare commits

...

7 commits

9 changed files with 147 additions and 1 deletions

View file

@ -55,6 +55,9 @@ class FormData:
location_name: str
location_latitude: float
location_longitude: float
geographic_population: int
geographic_cases: int
p_recurrent_option: str
mask_type: str
mask_wearing_option: str
mechanical_ventilation_type: str
@ -107,6 +110,9 @@ class FormData:
'infected_start': '08:30',
'location_latitude': _NO_DEFAULT,
'location_longitude': _NO_DEFAULT,
'geographic_population': 0,
'geographic_cases': 0,
'p_recurrent_option': 'p_recurrent_event',
'location_name': _NO_DEFAULT,
'mask_type': 'Type I',
'mask_wearing_option': 'mask_off',
@ -249,6 +255,8 @@ class FormData:
evaporation_factor=0.3,
),
exposed=self.exposed_population(),
geographic_population=self.geographic_population,
geographic_cases=self.geographic_cases
)
def build_model(self, sample_size=_DEFAULT_MC_SAMPLE_SIZE) -> models.ExposureModel:

View file

@ -105,6 +105,7 @@ def calculate_report_data(model: models.ExposureModel):
]
highest_const = max(concentrations)
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()
@ -120,6 +121,7 @@ def calculate_report_data(model: models.ExposureModel):
"concentrations": concentrations,
"highest_const": highest_const,
"prob_inf": prob,
"prob_specific_event": prob_specific_event,
"emission_rate": er,
"exposed_occupants": exposed_occupants,
"expected_new_cases": expected_new_cases,

View file

@ -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;
@ -176,6 +182,11 @@ function require_lunch(id, option) {
}
}
function require_population(option) {
require_input_field("#geographic_population", option);
require_input_field("#geographic_cases", option);
}
function require_mask(option) {
$("#mask_type_1").prop('required', option);
$("#mask_type_ffp2").prop('required', option);
@ -236,6 +247,23 @@ function on_ventilation_type_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) {
@ -572,6 +600,12 @@ $(document).ready(function () {
// Call the function now to handle forward/back button presses in the browser.
on_ventilation_type_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);

View file

@ -309,6 +309,25 @@
<div class="col-sm-6 align-self-center"><input type="number" id="infected_people" class="form-control" name="infected_people" min=1 value=1 required></div>
</div>
<input type="radio" id="p_recurrent_event" name="p_recurrent_option" value="p_recurrent_event" checked="checked">
<label for="p_recurrent_event">Recurrent exposure</label>
<input class="ml-2" type="radio" id="p_specific_event" name="p_recurrent_option" value="p_specific_event" data-enables="#DIVp_specific_event">
<label for="p_specific_event">Specific event</label>
<div data-tooltip="Specific event occurring at a given time (e.g. meeting or conference). Indicate the 7-day average of new reported cases and the population of a given location.">
<span class="tooltip_text">?</span>
</div>
<div id="DIVp_specific_event" style="display: none">
<div class="form-group row">
<div class="col-sm-4"><label class="col-form-label">Population:</label></div>
<div class="col-sm-6 align-self-center"><input type="number" step="any" id="geographic_population" class="non_zero form-control" name="geographic_population" placeholder="Inhabitants (#)" min="0"></div>
</div>
<div class="form-group row">
<div class="col-sm-4"><label class="col-form-label">New confirmed cases:</label></div>
<div class="col-sm-6 align-self-center"><input type="number" step="any" id="geographic_cases" class="non_zero form-control" name="geographic_cases" placeholder="Cases (# 7-day average)" min="0"></div>
</div>
</div>
<span id="training_limit_error" class="red_text" hidden>Conference/Training activities limited to 1 infected<br></span>
<hr width="80%">

View file

@ -77,9 +77,24 @@
</div>
<div class="col-md-8 pr-0 pl-0 d-flex">
{% block report_summary %}
<div class="flex-row align-self-center">
<div class="align-self-center alert alert-dark mb-0" role="alert">
Taking into account the uncertainties tied to the model variables, in this scenario, the <b>probability of one exposed occupant getting infected is {{ prob_inf | non_zero_percentage }}</b> and the <b>expected number of new cases is {{ expected_new_cases | float_format }}</b>*.
</div>
{% block specific_event_probability %}
{% if form.p_recurrent_option == "p_specific_event" %}
<br>
<div class="align-self-center alert alert-dark mb-0" role="alert">
The above result assumes that <b>{{ form.infected_people }}
{{ "occupant is infected" if form.infected_people == 1 else "occupants are infected" }}
</b> in the room.
By taking into account the estimate of cases currently circulating in <b>{{ form.location_name }}</b>,
the probability of on-site transmission, having at least 1 new infection in an <b>event
with {{ form.total_people }} occupants, is {{ prob_specific_event | non_zero_percentage }}</b>.
</div>
{% endif %}
{% endblock %}
</div>
{% endblock report_summary %}
</div>
</div>
@ -285,6 +300,10 @@
<li><p class="data_text">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.</p></li>
{% if form.p_recurrent_option == "p_specific_event" %}
<li><p class="data_text">Population in {{ form.location_name }}: {{ form.geographic_population }}</p></li>
<li><p class="data_text">New reported cases in {{ form.location_name }} (7-day average): {{ form.geographic_cases }}</p></li>
{% endif %}
<li><p class="data_text">
Activity type:
{% if form.activity_type == "office" %}

View file

@ -147,7 +147,15 @@ The recommended airflow rate for the HEPA filter should correspond to a total ai
<p>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.</p>
<p>As an example, for a shared office with 4 people, where one person is infected, we enter 4 occupants and 1 infected person.</p>
<p>As an example, for a shared office with 4 people, where one person is infected, we enter 4 occupants and 1 infected person.</p><br/>
<p>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 a population is infected.
The user will need to select <b>Specific event</b>, input the number of inhabitants and the 7-day average of new reported positive cases of the event location. These two inputs need to the related, i.e. the values of reported new cases shall correspond to the a given population in a given area. For example:</p>
<ul>
<li>Population of Geneva, CH: 508 000 inhabitants</li>
<li>New reported cases in the canton of Geneva: 1000 (7-day avg)</p>
</ul>
<p>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 <b>Recurrent exposure</b> option.</p>
<br>
<h4>Activity type</h4>
<br>

View file

@ -46,6 +46,10 @@
</div>
{% endif %}
{% block specific_event_probability %}
{{ super() }}
{% endblock specific_event_probability %}
{% if (prob_inf > 2) %}
<br>
{% if scale_warning.level == "green-1" %}

View file

@ -38,6 +38,7 @@ import typing
import numpy as np
from scipy.interpolate import interp1d
import scipy.integrate
import scipy.stats as sct
if not typing.TYPE_CHECKING:
from memoization import cached
@ -1074,6 +1075,12 @@ class ExposureModel:
#: The population of non-infected people to be used in the model.
exposed: Population
#: Geographic location population
geographic_population: int = 0
#: Geographic location cases
geographic_cases: int = 0
#: The number of times the exposure event is repeated (default 1).
repeats: int = 1
@ -1147,6 +1154,17 @@ class ExposureModel:
return (dep_exposure_integrated * emission_rate_per_aerosol *
f_inf * self.exposed.activity.inhalation_rate *
(1 - self.exposed.mask.inhale_efficiency()))
def probability_random_individual(self, cases, population, AB) -> _VectorisedFloat:
"""Probability that a randomly selected individual in a focal population is infected."""
return cases*AB/population
def probability_meet_infected_person(self, population, cases, event, x) -> _VectorisedFloat:
"""Probability to meet x infected persons in an event."""
# Ascertainment bias
AB = 5
return sct.binom.pmf(x, event, self.probability_random_individual(cases, population, AB))
def deposited_exposure(self) -> _VectorisedFloat:
"""
@ -1170,6 +1188,23 @@ 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.geographic_population != 0 and self.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)
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.probability_meet_infected_person(self.geographic_population, self.geographic_cases, self.exposed.number, x)
return sum_probability * 100
else:
return 0
def expected_new_cases(self) -> _VectorisedFloat:
prob = self.infection_probability()
exposed_occupants = self.exposed.number

View file

@ -220,3 +220,20 @@ def test_infectious_dose_vectorisation():
inf_probability = model.infection_probability()
assert isinstance(inf_probability, np.ndarray)
assert inf_probability.shape == (3, )
@pytest.mark.parametrize(
"population, cm, pop, cases, specific_event_probability",[
[populations[1], known_concentrations(lambda t: 36.),
100000, 68, 2.24124],
[populations[0], known_concentrations(lambda t: 36.),
100000, 68, 1.875652],
])
def test_specific_event_probability(population, cm,
pop, cases, specific_event_probability):
model = ExposureModel(cm, population, geographic_population=pop,
geographic_cases=cases)
np.testing.assert_allclose(
model.total_probability_rule(), specific_event_probability, rtol=0.05
)