Skip to content

Commit

Permalink
Merge branch 'feature/p_one_infected' into 'master'
Browse files Browse the repository at this point in the history
Probability that one individual is infected

See merge request cara/caimira!316
  • Loading branch information
andrejhenriques committed Oct 10, 2022
2 parents 70ccfaa + 12b7b57 commit 6c0d334
Show file tree
Hide file tree
Showing 20 changed files with 368 additions and 36 deletions.
2 changes: 1 addition & 1 deletion caimira/apps/calculator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
# 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):
Expand Down
29 changes: 24 additions & 5 deletions caimira/apps/calculator/model_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ class FormData:
location_name: str
location_latitude: float
location_longitude: float
geographic_population: int
geographic_cases: int
ascertainment_bias: str
exposure_option: str
mask_type: str
mask_wearing_option: str
mechanical_ventilation_type: str
Expand Down Expand Up @@ -110,12 +114,16 @@ 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,
'location_longitude': _NO_DEFAULT,
'location_name': _NO_DEFAULT,
'geographic_population': 0,
'geographic_cases': 0,
'ascertainment_bias': 'confidence_low',
'exposure_option': 'p_deterministic_exposure',
'mask_type': 'Type I',
'mask_wearing_option': 'mask_off',
'mechanical_ventilation_type': 'not-applicable',
Expand Down Expand Up @@ -261,7 +269,9 @@ def get_activity_mins(population):
('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}")
Expand Down Expand Up @@ -329,6 +339,11 @@ def build_mc_model(self) -> mc.ExposureModel:
),
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:
Expand Down Expand Up @@ -477,6 +492,7 @@ def infected_population(self) -> mc.InfectedPopulation:
'callcentre': ('Seated', 'Speaking'),
'library': ('Seated', 'Breathing'),
'training': ('Standing', 'Speaking'),
'training_attendee': ('Seated', 'Breathing'),
'lab': (
'Light activity',
#Model 1/2 of time spent speaking in a lab.
Expand Down Expand Up @@ -515,6 +531,7 @@ def exposed_population(self) -> mc.Population:
'callcentre': 'Seated',
'library': 'Seated',
'training': 'Seated',
'training_attendee': 'Seated',
'workshop': 'Moderate activity',
'lab':'Light activity',
'gym':'Heavy exercise',
Expand Down Expand Up @@ -759,6 +776,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': '',
Expand All @@ -785,7 +805,7 @@ def baseline_raw_form_data() -> typing.Dict[str, typing.Union[str, float]]:
}


ACTIVITY_TYPES = {'office', 'smallmeeting', 'largemeeting', 'training', 'callcentre', 'controlroom-day', 'controlroom-night', 'library', 'workshop', 'lab', 'gym'}
ACTIVITY_TYPES = {'office', 'smallmeeting', 'largemeeting', 'training', 'training_attendee', 'callcentre', 'controlroom-day', 'controlroom-night', 'library', 'workshop', 'lab', 'gym'}
MECHANICAL_VENTILATION_TYPES = {'mech_type_air_changes', 'mech_type_air_supply', 'not-applicable'}
MASK_TYPES = {'Type I', 'FFP2'}
MASK_WEARING_OPTIONS = {'mask_on', 'mask_off'}
Expand All @@ -794,9 +814,8 @@ def baseline_raw_form_data() -> typing.Dict[str, typing.Union[str, float]]:
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',
Expand Down
16 changes: 15 additions & 1 deletion caimira/apps/calculator/report_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing
])

prob = np.array(model.infection_probability()).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()
Expand All @@ -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_probabilistic_exposure": prob_probabilistic_exposure,
"emission_rate": er,
"exposed_occupants": exposed_occupants,
"expected_new_cases": expected_new_cases,
Expand Down Expand Up @@ -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], compute_prob_exposure: bool):
model = mc_model.build_model(size=_DEFAULT_MC_SAMPLE_SIZE)
if (compute_prob_exposure):
# It means we have data to calculate the total_probability_rule
prob_probabilistic_exposure = model.total_probability_rule()
else:
prob_probabilistic_exposure = 0.

return {
'probability_of_infection': np.mean(model.infection_probability()),
Expand All @@ -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_probabilistic_exposure': prob_probabilistic_exposure,
}


Expand All @@ -303,11 +311,17 @@ def comparison_report(
else:
statistics = {}

if (form.short_range_option == "short_range_yes" and form.exposure_option == "p_probabilistic_exposure"):
compute_prob_exposure = True
else:
compute_prob_exposure = False

with executor_factory() as executor:
results = executor.map(
scenario_statistics,
scenarios.values(),
[sample_times] * len(scenarios),
[compute_prob_exposure] * len(scenarios),
timeout=60,
)

Expand Down
2 changes: 1 addition & 1 deletion caimira/apps/calculator/static/css/form.css
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,4 @@
transition-delay: 0.5s; /* Starting after the grow effect */
transition-duration: 0.2s;
transform: translateX(-50%) scaleY(1);
}
}
55 changes: 55 additions & 0 deletions caimira/apps/calculator/static/js/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ function require_fields(obj) {
case "hepa_no":
require_hepa(false);
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);
break;
Expand Down Expand Up @@ -172,6 +180,16 @@ 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_infected(option) {
require_input_field("#infected_people", option);
}

function require_mask(option) {
$("#mask_type_1").prop('required', option);
$("#mask_type_ffp2").prop('required', option);
Expand Down Expand Up @@ -269,6 +287,23 @@ function on_hepa_option_change() {
})
}

function on_exposure_change() {
p_recurrent = $('input[type=radio][name=exposure_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) {
Expand Down Expand Up @@ -538,6 +573,20 @@ 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);

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){
Expand Down Expand Up @@ -870,6 +919,12 @@ $(document).ready(function () {
// Call the function now to handle forward/back button presses in the browser.
on_hepa_option_change();

// When the exposure_option changes we want to make its respective
// children show/hide.
$("input[type=radio][name=exposure_option]").change(on_exposure_change);
// Call the function now to handle forward/back button presses in the browser.
on_exposure_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);
Expand Down
1 change: 1 addition & 0 deletions caimira/apps/expert.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@ def present(self):
mask=models.Mask.types['No mask'],
host_immunity=0.,
),
geographical_data=models.Cases(),
)


Expand Down
4 changes: 4 additions & 0 deletions caimira/apps/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ footer img {
display: inline!important;
}

#event_data_tooltip:before{
max-width: 350px!important;
}

}

@media (max-width: 40em) {
Expand Down
49 changes: 43 additions & 6 deletions caimira/apps/templates/base/calculator.form.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,9 @@

<!-- Event Options -->
<b>Event data:</b>
<div data-tooltip="The total no. of occupants in the room and how many of them you assume are infected.">
<div id="event_data_tooltip" data-tooltip="The total no. of occupants in the room.
Deterministic exposure: add no. occupants that are infected.
Probabilistic exposure: event at a given time & location (e.g. meeting or conference), considering the incidence rate in that area.">
<span class="tooltip_text">?</span>
</div><br>

Expand All @@ -312,11 +314,44 @@
<div class="col-sm-6 align-self-center"><input type="number" id="total_people" class="form-control" name="total_people" placeholder="Number" min=1 required></div>
</div>

<div class="form-group row">
<div class="col-sm-4" style="top: -2px"><label>Number of infected people: </label></div>
<div class="col-sm-6"><input type="number" id="infected_people" class="form-control" name="infected_people" min=1 value=1 required></div>
<div class="form-group mb-1">
<input type="radio" id="p_deterministic_exposure" name="exposure_option" value="p_deterministic_exposure" checked="checked" data-enables="#DIVp_deterministic_exposure">
<label for="p_deterministic_exposure">Deterministic exposure</label>
</div>
<div class="form-group">
<input type="radio" id="p_probabilistic_exposure" name="exposure_option" value="p_probabilistic_exposure" data-enables="#DIVp_probabilistic_exposure">
<label for="p_probabilistic_exposure">Probabilistic exposure (incidence rate)</label>
</div>

<div id="DIVp_deterministic_exposure" class="tabbed" style="display: none">
<div class="form-group row">
<div class="col-sm-4"><label class="col-form-label">Number of infected people: </label></div>
<div class="col-sm-6 pl-0 align-self-center"><input type="number" id="infected_people" class="form-control" name="infected_people" min=1 value=1></div>
</div>
</div>

<div id="DIVp_probabilistic_exposure" class="tabbed" 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 pl-0 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 (weekly):</label></div>
<div class="col-sm-6 pl-0 align-self-center"><input type="number" step="any" id="geographic_cases" class="non_zero form-control" name="geographic_cases" placeholder="Cases (#7-day rolling avg)" min="0"></div>
</div>
<div class="form-group row">
<div class="col-sm-4"><label class="col-form-label">Confidence level:</label></div>
<div class="col-sm-6 pl-0 align-self-center">
<select id="ascertainment_bias" name="ascertainment_bias" class="form-control">
<option value="confidence_low">Low - surveillance only for sympotmatic patients</option>
<option value="confidence_medium">Medium - recommended population wide surveillance</option>
<option value="confidence_high">High - mandatory population wide surveillance</option>
</select>
</div>
</div>
</div>

<span id="training_limit_error" class="red_text" hidden>Conference/Training activities limited to 1 infected<br></span>
<hr width="80%">

<div class="form-group row">
Expand All @@ -332,7 +367,8 @@
<option value="library">Library</option>
<option value="lab">Laboratory</option>
<option value="workshop">Workshop</option>
<option value="training">Conference/Training</option>
<option value="training">Conference/Training (speaker infected)</option>
<option value="training_attendee">Conference/Training (attendee infected)</option>
<option value="gym">Gym</option>
</select>
</div>
Expand Down Expand Up @@ -615,8 +651,9 @@
<li>Library = all seated, no talking, just breathing,</li>
<li>Laboratory = light physical activity, talking 50% of the time,</li>
<li>Workshop = moderate physical activity, talking 50% of the time,</li>
<li>Conference/Training = speaker/trainer standing and talking, rest seated and talking quietly.
<li>Conference/Training (speaker infected) = speaker/trainer standing and talking, rest seated and talking quietly.
Speaker/Trainer assumed infected (worst case scenario),</li>
<li>Conference/Training (attendee infected) = someone in the audience is infected, all are seated and breathing.</li>
<li>Gym = heavy exercise, no talking, just breathing.</li>
</ul>
<b>Activity breaks:</b><br>
Expand Down
Loading

0 comments on commit 6c0d334

Please sign in to comment.