Compare commits
7 commits
master
...
feature/p_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3e82620a7 | ||
|
|
0348065515 | ||
|
|
98e7da33d4 | ||
|
|
931a3320b9 | ||
|
|
95d0afc52f | ||
|
|
e487953a61 | ||
|
|
a3a44e5e89 |
9 changed files with 147 additions and 1 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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%">
|
||||
|
||||
|
|
|
|||
|
|
@ -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" %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" %}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
Loading…
Reference in a new issue