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 @@
+ + + + +The probability of infection is larger than above which signifies that the chosen number of infected occupants in the form was underestimated.
+ {% endif %}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.
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 }}
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:
+The confidence level has the following options:
+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.