Merge branch 'feature/expected_reproduction_sr' into 'master'
Expected number of new cases fix See merge request caimira/caimira!493
This commit is contained in:
commit
74849cdee0
11 changed files with 110 additions and 22 deletions
|
|
@ -42,7 +42,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.15.1"
|
||||
__version__ = "4.15.2"
|
||||
|
||||
LOG = logging.getLogger("Calculator")
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ DEFAULTS = {
|
|||
'sensor_in_use': '',
|
||||
'short_range_option': 'short_range_no',
|
||||
'short_range_interactions': '[]',
|
||||
'short_range_occupants': 0,
|
||||
}
|
||||
|
||||
# ------------------ Activities ----------------------
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ class VirusFormData(FormData):
|
|||
sensor_in_use: str
|
||||
short_range_option: str
|
||||
short_range_interactions: list
|
||||
short_range_occupants: int
|
||||
|
||||
_DEFAULTS: typing.ClassVar[typing.Dict[str, typing.Any]] = DEFAULTS
|
||||
|
||||
|
|
@ -182,6 +183,13 @@ class VirusFormData(FormData):
|
|||
|
||||
if total_percentage != 100:
|
||||
raise ValueError(f'The sum of all respiratory activities should be 100. Got {total_percentage}.')
|
||||
|
||||
# Validate number of people with short-range interactions
|
||||
max_occupants_for_sr = self.total_people - self.infected_people
|
||||
if self.short_range_occupants > max_occupants_for_sr:
|
||||
raise ValueError(
|
||||
f'The total number of occupants having short-range interactions ({self.short_range_occupants}) should be lower than the exposed population ({max_occupants_for_sr}).'
|
||||
)
|
||||
|
||||
def initialize_room(self) -> models.Room:
|
||||
# Initializes room with volume either given directly or as product of area and height
|
||||
|
|
@ -206,7 +214,6 @@ class VirusFormData(FormData):
|
|||
room = self.initialize_room()
|
||||
ventilation: models._VentilationBase = self.ventilation()
|
||||
infected_population = self.infected_population()
|
||||
|
||||
short_range = []
|
||||
if self.short_range_option == "short_range_yes":
|
||||
for interaction in self.short_range_interactions:
|
||||
|
|
@ -234,6 +241,7 @@ class VirusFormData(FormData):
|
|||
geographic_cases=self.geographic_cases,
|
||||
ascertainment_bias=CONFIDENCE_LEVEL_OPTIONS[self.ascertainment_bias],
|
||||
),
|
||||
exposed_to_short_range=self.short_range_occupants,
|
||||
)
|
||||
|
||||
def build_model(self, sample_size=None) -> models.ExposureModel:
|
||||
|
|
|
|||
|
|
@ -431,7 +431,7 @@ def manufacture_alternative_scenarios(form: VirusFormData) -> typing.Dict[str, m
|
|||
scenarios['Neither ventilation nor masks'] = without_mask_or_vent.build_mc_model()
|
||||
|
||||
else:
|
||||
no_short_range_alternative = dataclass_utils.replace(form, short_range_interactions=[])
|
||||
no_short_range_alternative = dataclass_utils.replace(form, short_range_interactions=[], total_people=form.total_people - form.short_range_occupants)
|
||||
scenarios['Base scenario without short-range interactions'] = no_short_range_alternative.build_mc_model()
|
||||
|
||||
return scenarios
|
||||
|
|
|
|||
|
|
@ -869,7 +869,7 @@ function validate_sr_parameter(obj, error_message) {
|
|||
if ($(obj).val() == "" || $(obj).val() == null) {
|
||||
if (!$(obj).hasClass("red_border") && !$(obj).prop("disabled")) {
|
||||
var parameter = document.getElementById($(obj).attr('id'));
|
||||
insertErrorFor(parameter, error_message)
|
||||
insertErrorFor(parameter, error_message);
|
||||
$(parameter).addClass("red_border");
|
||||
}
|
||||
return false;
|
||||
|
|
@ -880,6 +880,22 @@ function validate_sr_parameter(obj, error_message) {
|
|||
}
|
||||
}
|
||||
|
||||
function validate_sr_people(obj) {
|
||||
let sr_total_people = document.getElementById($(obj).attr('id'));
|
||||
let max = document.getElementById("total_people").valueAsNumber - document.getElementById("infected_people").valueAsNumber;
|
||||
if ($(obj).val() == "" || $(obj).val() == null || sr_total_people.valueAsNumber > max) {
|
||||
if (!$(obj).hasClass("red_border")) {
|
||||
insertErrorFor(sr_total_people, "Value must be less or equal than the number of exposed people.");
|
||||
$(sr_total_people).addClass("red_border");
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
removeErrorFor($(obj));
|
||||
$(obj).removeClass("red_border");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function parseValToNumber(val) {
|
||||
return parseInt(val.replace(':',''), 10);
|
||||
}
|
||||
|
|
@ -1084,7 +1100,6 @@ $(document).ready(function () {
|
|||
validateMaxInfectedPeople();
|
||||
$("#total_people").change(validateMaxInfectedPeople);
|
||||
$("#activity_type").change(validateMaxInfectedPeople);
|
||||
$("#total_people").change(validateMaxInfectedPeople);
|
||||
$("#infected_people").change(validateMaxInfectedPeople);
|
||||
|
||||
//Validate all non zero values
|
||||
|
|
@ -1253,7 +1268,8 @@ $(document).ready(function () {
|
|||
let activity = validate_sr_parameter('#sr_expiration_no_' + String(index)[0], "Required input.");
|
||||
let start = validate_sr_parameter('#sr_start_no_' + String(index)[0], "Required input.");
|
||||
let duration = validate_sr_parameter('#sr_duration_no_' + String(index)[0], "Required input.");
|
||||
if (activity && start && duration) {
|
||||
let total_people = validate_sr_people('#short_range_occupants');
|
||||
if (activity && start && duration && total_people) {
|
||||
if (validate_sr_time('#sr_start_no_' + String(index)) && validate_sr_time('#sr_duration_no_' + String(index))) {
|
||||
document.getElementById('sr_expiration_no_' + String(index)).disabled = true;
|
||||
document.getElementById('sr_start_no_' + String(index)).disabled = true;
|
||||
|
|
|
|||
|
|
@ -584,6 +584,16 @@
|
|||
<div class="col-md-12 p-0 form-group" id="dialog_sr"></div>
|
||||
<div class="text-center"><button type="button" class="add_node_btn_frm_field btn btn-primary btn-sm">Add row</button></div>
|
||||
<input type="text" class="form-control d-none" name="short_range_interactions">
|
||||
<br />
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-4">
|
||||
<label class="col-form-label col-form-label-sm">Total people with short-range interactions:</label>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" id="short_range_occupants" name="short_range_occupants" class="form-control form-control-sm" min="0" value="1" tabindex="-1" onchange="validate_sr_people(this)" required>
|
||||
</div>
|
||||
<div class="col-sm-6"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
|
|
|
|||
|
|
@ -54,13 +54,20 @@
|
|||
<div class="tab-content" style="border-top: #dee2e6 1px solid; margin-top: -1px" >
|
||||
|
||||
<div class="tab-pane show active" id="results" role="tabpanel" aria-labelledby="results-tab" style="padding: 1%">
|
||||
{% set long_range_prob_inf = prob_inf %}
|
||||
{% set long_range_expected_cases = expected_new_cases %}
|
||||
|
||||
{# Update values if short range option is "short_range_yes" #}
|
||||
{% if form.short_range_option == "short_range_yes" %}
|
||||
{% set scenario = alternative_scenarios.stats.values() | first %}
|
||||
{# Probability of infection values #}
|
||||
{% set long_range_prob_inf = scenario.probability_of_infection %}
|
||||
{% 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 %}
|
||||
{# Expected new case values #}
|
||||
{% set long_range_expected_cases = scenario.expected_new_cases %}
|
||||
|
||||
{% if form.exposure_option == 'p_probabilistic_exposure' %}
|
||||
{% set long_range_prob_probabilistic_exposure = scenario.prob_probabilistic_exposure %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% block report_results %}
|
||||
|
|
@ -97,6 +104,7 @@
|
|||
</div>
|
||||
{% endblock long_range_warning_animation %}
|
||||
</div>
|
||||
<h6><b>Expected new cases:</b> {{ long_range_expected_cases | float_format }}</h6>
|
||||
</div>
|
||||
<br>
|
||||
{% if form.short_range_option == "short_range_yes" %}
|
||||
|
|
@ -118,18 +126,19 @@
|
|||
</div>
|
||||
{% endblock warning_animation %}
|
||||
</div>
|
||||
<h6><b>Expected new cases:</b> {{ expected_new_cases | float_format }}</h6>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="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 and assuming all occupants are exposed equally (i.e. without short-range interactions), the <b>probability of one exposed occupant getting infected is {{ long_range_prob_inf | non_zero_percentage }}</b> and the <b>expected number of new cases is {{ expected_new_cases | float_format }}</b>*.
|
||||
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 <b>probability of one exposed occupant getting infected is {{ long_range_prob_inf | non_zero_percentage }}</b> and the <b>expected number of new cases is {{ long_range_expected_cases | float_format }}</b>*.
|
||||
</div>
|
||||
{% if form.short_range_option == "short_range_yes" %}
|
||||
<br>
|
||||
<div class="align-self-center alert alert-dark mb-0" role="alert">
|
||||
In this scenario, assuming <b>short-range interactions</b> occur, the <b>probability of one exposed occupant getting infected can go as high as {{ prob_inf | non_zero_percentage }}</b>.
|
||||
In this scenario, the <b>probability the occupant(s) exposed to short-range interactions get infected can go as high as {{ prob_inf | non_zero_percentage }}</b> and the <b>expected number of new cases increases to {{ expected_new_cases | float_format }}</b>.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block probabilistic_exposure_probability %}
|
||||
|
|
@ -601,14 +610,16 @@
|
|||
{% endif %}
|
||||
</p></li>
|
||||
{% if form.short_range_option == "short_range_yes" %}
|
||||
<li><p class="data_text">
|
||||
Short-range interactions: {{ form.short_range_interactions|length }}
|
||||
</p></li>
|
||||
<li><p class="data_text">Total number of occupants having short-range interactions: {{ form.short_range_occupants }}</p></li>
|
||||
<ul>
|
||||
{% for interaction in form.short_range_interactions %}
|
||||
<li>Expiratory activity {{ loop.index if form.short_range_interactions|length > 1 }}: {{ "Shouting/Singing" if interaction.expiration == "Shouting" else interaction.expiration }} </li>
|
||||
<li>Start time {{ loop.index if form.short_range_interactions|length > 1 }}: {{ interaction.start_time }} </li>
|
||||
<li>Duration {{ loop.index if form.short_range_interactions|length > 1 }}: {{ interaction.duration }} {{ "minutes" if interaction.duration|float > 1 else "minute" }}</li>
|
||||
<li>Interaction no. {{ loop.index }}:
|
||||
<ul>
|
||||
<li>Expiratory activity: {{ "Shouting/Singing" if interaction.expiration == "Shouting" else interaction.expiration }} </li>
|
||||
<li>Start time: {{ interaction.start_time }} </li>
|
||||
<li>Duration: {{ interaction.duration }} {{ "minutes" if interaction.duration|float > 1 else "minute" }}</li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
<div class="alert alert-success mb-0" role="alert">
|
||||
<strong>Acceptable:</strong>
|
||||
{% endif %}
|
||||
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 <b>probability of one exposed occupant getting infected is {{long_range_prob_inf | non_zero_percentage}}</b> and the <b>expected number of new cases is {{expected_new_cases | float_format}}</b>*.
|
||||
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 <b>probability of one exposed occupant getting infected is {{long_range_prob_inf | non_zero_percentage}}</b> and the <b>expected number of new cases is {{ long_range_expected_cases | float_format }}</b>*.
|
||||
</div>
|
||||
{% if form.short_range_option == "short_range_yes" %}
|
||||
<br>
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
<div class="alert alert-success mb-0" role="alert">
|
||||
<strong>Acceptable:</strong>
|
||||
{% endif %}
|
||||
In this scenario, assuming <b>short-range interactions</b> occur, the <b>probability of one exposed occupant getting infected can go as high as {{prob_inf | non_zero_percentage}}</b>.
|
||||
In this scenario, the <b>probability the occupant(s) exposed to short-range interactions get infected can go as high as {{ prob_inf | non_zero_percentage }}</b> and the <b>expected number of new cases increases to {{ expected_new_cases | float_format }}</b>.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1579,6 +1579,9 @@ class ExposureModel:
|
|||
#: Geographical data
|
||||
geographical_data: Cases
|
||||
|
||||
#: Total people with short-range interactions
|
||||
exposed_to_short_range: int = 0
|
||||
|
||||
#: The number of times the exposure event is repeated (default 1).
|
||||
@property
|
||||
def repeats(self) -> int:
|
||||
|
|
@ -1814,10 +1817,15 @@ class ExposureModel:
|
|||
"with dynamic occupancy")
|
||||
|
||||
"""
|
||||
The expect_new_cases should always take the long-range infection_probability and multiply by the occupants exposed to long-range.
|
||||
The expected_new_cases may provide one or two different outputs:
|
||||
1) Long-range exposure: take the infection_probability and multiply by the occupants exposed to long-range.
|
||||
2) Short- and long-range exposure: take the infection_probability of long-range multiplied by the occupants exposed to long-range only,
|
||||
plus the infection_probability of short- and long-range multiplied by the occupants exposed to short-range only.
|
||||
"""
|
||||
|
||||
if self.short_range != ():
|
||||
return nested_replace(self, {'short_range': ()}).infection_probability() * self.exposed.number / 100
|
||||
new_cases_long_range = nested_replace(self, {'short_range': (),}).infection_probability() * (self.exposed.number - self.exposed_to_short_range)
|
||||
return (new_cases_long_range + (self.infection_probability() * self.exposed_to_short_range)) / 100
|
||||
|
||||
return self.infection_probability() * self.exposed.number / 100
|
||||
|
||||
|
|
|
|||
|
|
@ -11,3 +11,12 @@ def baseline_form_data():
|
|||
@pytest.fixture
|
||||
def baseline_form(baseline_form_data, data_registry):
|
||||
return model_generator.VirusFormData.from_dict(baseline_form_data, data_registry)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def baseline_form_with_sr(baseline_form_data, data_registry):
|
||||
form_data_sr = baseline_form_data
|
||||
form_data_sr['short_range_option'] = 'short_range_yes'
|
||||
form_data_sr['short_range_interactions'] = '[{"expiration": "Shouting", "start_time": "10:30", "duration": "30"}]'
|
||||
form_data_sr['short_range_occupants'] = 5
|
||||
return model_generator.VirusFormData.from_dict(form_data_sr, data_registry)
|
||||
|
|
@ -7,7 +7,9 @@ import numpy as np
|
|||
import pytest
|
||||
|
||||
from caimira.apps.calculator import make_app
|
||||
from caimira.apps.calculator.report_generator import ReportGenerator, readable_minutes
|
||||
from caimira.apps.calculator.model_generator import VirusFormData
|
||||
from caimira.apps.calculator.report_generator import (ReportGenerator, readable_minutes, calculate_report_data,
|
||||
manufacture_alternative_scenarios, interesting_times, comparison_report)
|
||||
import caimira.apps.calculator.report_generator as rep_gen
|
||||
|
||||
|
||||
|
|
@ -90,3 +92,26 @@ def test_interesting_times_w_temp(exposure_model_w_outside_temp_changes):
|
|||
5., 5.4, 5.8, 6.2, 6.6, 7., 7.4, 7.8, 8.
|
||||
]
|
||||
np.testing.assert_allclose(result, expected)
|
||||
|
||||
|
||||
def test_expected_new_cases(baseline_form_with_sr: VirusFormData):
|
||||
model = baseline_form_with_sr.build_model()
|
||||
|
||||
executor_factory = partial(
|
||||
concurrent.futures.ThreadPoolExecutor, 1,
|
||||
)
|
||||
|
||||
# Short- and Long-range contributions
|
||||
report_data = calculate_report_data(baseline_form_with_sr, model, executor_factory)
|
||||
sr_lr_expected_new_cases = report_data['expected_new_cases']
|
||||
sr_lr_prob_inf = report_data['prob_inf']/100
|
||||
|
||||
# Long-range contributions alone
|
||||
scenario_sample_times = interesting_times(model)
|
||||
alternative_scenarios = manufacture_alternative_scenarios(baseline_form_with_sr)
|
||||
alternative_statistics = comparison_report(
|
||||
baseline_form_with_sr, report_data, alternative_scenarios, scenario_sample_times, executor_factory=executor_factory,
|
||||
)
|
||||
|
||||
lr_expected_new_cases = alternative_statistics['stats']['Base scenario without short-range interactions']['expected_new_cases']
|
||||
np.testing.assert_almost_equal(sr_lr_expected_new_cases, lr_expected_new_cases + sr_lr_prob_inf * baseline_form_with_sr.short_range_occupants, 2)
|
||||
|
|
|
|||
Loading…
Reference in a new issue