Merge branch 'feature/ARIA_new_inputs' into 'master'
New precise activity and break inputs See merge request cara/caimira!404
This commit is contained in:
commit
295d975bf3
3 changed files with 224 additions and 15 deletions
|
|
@ -5,6 +5,7 @@ import logging
|
|||
import typing
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -32,6 +33,8 @@ class FormData:
|
|||
air_changes: float
|
||||
air_supply: float
|
||||
arve_sensors_option: bool
|
||||
specific_breaks: list
|
||||
precise_activity: dict
|
||||
ceiling_height: float
|
||||
exposed_coffee_break_option: str
|
||||
exposed_coffee_duration: int
|
||||
|
|
@ -97,6 +100,8 @@ class FormData:
|
|||
'air_changes': 0.,
|
||||
'air_supply': 0.,
|
||||
'arve_sensors_option': False,
|
||||
'specific_breaks': '[]',
|
||||
'precise_activity': '{}',
|
||||
'calculator_version': _NO_DEFAULT,
|
||||
'ceiling_height': 0.,
|
||||
'exposed_coffee_break_option': 'coffee_break_0',
|
||||
|
|
@ -307,6 +312,55 @@ class FormData:
|
|||
raise ValueError("mechanical_ventilation_type cannot be 'not-applicable' if "
|
||||
"ventilation_type is 'mechanical_ventilation'")
|
||||
|
||||
# Validate specific inputs - breaks
|
||||
if self.specific_breaks != []:
|
||||
if type(self.specific_breaks) is not list:
|
||||
raise TypeError(f'All breaks should be in a list. Got {type(self.specific_breaks)}.')
|
||||
for input_break in self.specific_breaks:
|
||||
# Input validations.
|
||||
if type(input_break) is not dict:
|
||||
raise TypeError(f'Each break should be a dictionary. Got {type(input_break)}.')
|
||||
dict_keys = list(input_break.keys())
|
||||
if "start_time" not in input_break:
|
||||
raise TypeError(f'Unable to fetch "start_time" key. Got "{dict_keys[0]}".')
|
||||
if "finish_time" not in input_break:
|
||||
raise TypeError(f'Unable to fetch "finish_time" key. Got "{dict_keys[1]}".')
|
||||
for time in input_break.values():
|
||||
if not re.compile("^(2[0-3]|[01]?[0-9]):([0-5]?[0-9])$").match(time):
|
||||
raise TypeError(f'Wrong time format - "HH:MM". Got "{time}".')
|
||||
|
||||
# Validate specific inputs - precise activity
|
||||
if self.precise_activity != {}:
|
||||
if type(self.precise_activity) is not dict:
|
||||
raise TypeError('The precise activities should be in a dictionary.')
|
||||
|
||||
dict_keys = list(self.precise_activity.keys())
|
||||
if "physical_activity" not in dict_keys:
|
||||
raise TypeError(f'Unable to fetch "physical_activity" key. Got "{dict_keys[0]}".')
|
||||
if "respiratory_activity" not in dict_keys:
|
||||
raise TypeError(f'Unable to fetch "respiratory_activity" key. Got "{dict_keys[1]}".')
|
||||
|
||||
if type(self.precise_activity['physical_activity']) is not str:
|
||||
raise TypeError('The physical activities should be a single string.')
|
||||
|
||||
if type(self.precise_activity['respiratory_activity']) is not list:
|
||||
raise TypeError('The respiratory activities should be in a list.')
|
||||
|
||||
total_percentage = 0
|
||||
for respiratory_activity in self.precise_activity['respiratory_activity']:
|
||||
if type(respiratory_activity) is not dict:
|
||||
raise TypeError('Each respiratory activity should be defined in a dictionary.')
|
||||
dict_keys = list(respiratory_activity.keys())
|
||||
if "type" not in dict_keys:
|
||||
raise TypeError(f'Unable to fetch "type" key. Got "{dict_keys[0]}".')
|
||||
if "percentage" not in dict_keys:
|
||||
raise TypeError(f'Unable to fetch "percentage" key. Got "{dict_keys[1]}".')
|
||||
total_percentage += respiratory_activity['percentage']
|
||||
|
||||
if total_percentage != 100:
|
||||
raise ValueError(f'The sum of all respiratory activities should be 100. Got {total_percentage}.')
|
||||
|
||||
|
||||
def build_mc_model(self) -> mc.ExposureModel:
|
||||
# Initializes room with volume either given directly or as product of area and height
|
||||
if self.volume_type == 'room_volume_explicit':
|
||||
|
|
@ -468,6 +522,15 @@ class FormData:
|
|||
mask = models.Mask.types['No mask']
|
||||
return mask
|
||||
|
||||
def generate_precise_activity_expiration(self) -> typing.Tuple[typing.Any, ...]:
|
||||
if self.precise_activity == {}: # It means the precise activity is not defined by a specific input.
|
||||
return ()
|
||||
respiratory_dict = {}
|
||||
for respiratory_activity in self.precise_activity['respiratory_activity']:
|
||||
respiratory_dict[respiratory_activity['type']] = respiratory_activity['percentage']
|
||||
|
||||
return (self.precise_activity['physical_activity'], respiratory_dict)
|
||||
|
||||
def infected_population(self) -> mc.InfectedPopulation:
|
||||
# Initializes the virus
|
||||
virus = virus_distributions[self.virus_type]
|
||||
|
|
@ -511,8 +574,34 @@ class FormData:
|
|||
#Model 1/2 of time spent speaking in a workshop.
|
||||
{'Speaking': 1, 'Breathing': 1}),
|
||||
'gym':('Heavy exercise', 'Breathing'),
|
||||
# Other activity types
|
||||
'household-day': (
|
||||
'Light activity',
|
||||
{'Breathing': 5, 'Speaking': 5}
|
||||
),
|
||||
'household-night': (
|
||||
'Seated',
|
||||
{'Breathing': 7, 'Speaking': 3}
|
||||
),
|
||||
'primary-school': (
|
||||
'Light activity',
|
||||
{'Breathing': 5, 'Speaking': 5}
|
||||
),
|
||||
'secondary-school': (
|
||||
'Light activity',
|
||||
{'Breathing': 7, 'Speaking': 3}
|
||||
),
|
||||
'university': (
|
||||
'Seated',
|
||||
{'Breathing': 9, 'Speaking': 1}
|
||||
),
|
||||
'restaurant': (
|
||||
'Seated',
|
||||
{'Breathing': 1, 'Speaking': 9}
|
||||
),
|
||||
'precise': self.generate_precise_activity_expiration(),
|
||||
}
|
||||
|
||||
|
||||
[activity_defn, expiration_defn] = scenario_activity_and_expiration[self.activity_type]
|
||||
activity = activity_distributions[activity_defn]
|
||||
expiration = build_expiration(expiration_defn)
|
||||
|
|
@ -544,6 +633,13 @@ class FormData:
|
|||
'workshop': 'Moderate activity',
|
||||
'lab':'Light activity',
|
||||
'gym':'Heavy exercise',
|
||||
'household-day': 'Light activity',
|
||||
'household-night': 'Seated',
|
||||
'primary-school': 'Light activity',
|
||||
'secondary-school': 'Light activity',
|
||||
'university': 'Seated',
|
||||
'restaurant': 'Seated',
|
||||
'precise': 'Seated',
|
||||
}
|
||||
|
||||
activity_defn = scenario_activity[self.activity_type]
|
||||
|
|
@ -642,6 +738,20 @@ class FormData:
|
|||
else:
|
||||
return self.exposed_coffee_break_times()
|
||||
|
||||
def generate_specific_break_times(self) -> models.BoundarySequence_t:
|
||||
break_times = []
|
||||
for n in self.specific_breaks:
|
||||
# Parse break times.
|
||||
begin = time_string_to_minutes(n["start_time"])
|
||||
end = time_string_to_minutes(n["finish_time"])
|
||||
for time in [begin, end]:
|
||||
# For a specific break, the infected and exposed presence is the same.
|
||||
if not getattr(self, 'infected_start') < time < getattr(self, 'infected_finish'):
|
||||
raise ValueError(f'All breaks should be within the simulation time. Got {time_minutes_to_string(time)}.')
|
||||
|
||||
break_times.append((begin, end))
|
||||
return tuple(break_times)
|
||||
|
||||
def present_interval(
|
||||
self,
|
||||
start: int,
|
||||
|
|
@ -735,9 +845,13 @@ class FormData:
|
|||
return models.SpecificInterval(tuple(present_intervals))
|
||||
|
||||
def infected_present_interval(self) -> models.Interval:
|
||||
if self.specific_breaks != []: # It means the breaks are specific and not predefined
|
||||
breaks = self.generate_specific_break_times()
|
||||
else:
|
||||
breaks = self.infected_lunch_break_times() + self.infected_coffee_break_times()
|
||||
return self.present_interval(
|
||||
self.infected_start, self.infected_finish,
|
||||
breaks=self.infected_lunch_break_times() + self.infected_coffee_break_times(),
|
||||
breaks=breaks,
|
||||
)
|
||||
|
||||
def short_range_interval(self, interaction) -> models.SpecificInterval:
|
||||
|
|
@ -746,9 +860,13 @@ class FormData:
|
|||
return models.SpecificInterval(present_times=((start_time/60, (start_time + duration)/60),))
|
||||
|
||||
def exposed_present_interval(self) -> models.Interval:
|
||||
if self.specific_breaks != []: # It means the breaks are specific and not predefined
|
||||
breaks = self.generate_specific_break_times()
|
||||
else:
|
||||
breaks = self.exposed_lunch_break_times() + self.exposed_coffee_break_times()
|
||||
return self.present_interval(
|
||||
self.exposed_start, self.exposed_finish,
|
||||
breaks=self.exposed_lunch_break_times() + self.exposed_coffee_break_times(),
|
||||
breaks=breaks,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -828,7 +946,10 @@ def baseline_raw_form_data() -> typing.Dict[str, typing.Union[str, float]]:
|
|||
}
|
||||
|
||||
|
||||
ACTIVITY_TYPES = {'office', 'smallmeeting', 'largemeeting', 'training', 'training_attendee', 'callcentre', 'controlroom-day', 'controlroom-night', 'library', 'workshop', 'lab', 'gym'}
|
||||
ACTIVITY_TYPES = {
|
||||
'office', 'smallmeeting', 'largemeeting', 'training', 'callcentre', 'controlroom-day', 'controlroom-night', 'library', 'workshop', 'lab', 'gym',
|
||||
'household-day', 'household-night', 'primary-school', 'secondary-school', 'university', 'restaurant', 'precise',
|
||||
}
|
||||
MECHANICAL_VENTILATION_TYPES = {'mech_type_air_changes', 'mech_type_air_supply', 'not-applicable'}
|
||||
MASK_TYPES = {'Type I', 'FFP2', 'Cloth'}
|
||||
MASK_WEARING_OPTIONS = {'mask_on', 'mask_off'}
|
||||
|
|
@ -873,12 +994,20 @@ def time_minutes_to_string(time: int) -> str:
|
|||
return "{0:0=2d}".format(int(time/60)) + ":" + "{0:0=2d}".format(time%60)
|
||||
|
||||
|
||||
def string_to_list(l: str) -> list:
|
||||
return list(ast.literal_eval(l.replace(""", "\"")))
|
||||
def string_to_list(s: str) -> list:
|
||||
return list(ast.literal_eval(s.replace(""", "\"")))
|
||||
|
||||
|
||||
def list_to_string(s: list) -> str:
|
||||
return json.dumps(s)
|
||||
def list_to_string(l: list) -> str:
|
||||
return json.dumps(l)
|
||||
|
||||
|
||||
def string_to_dict(s: str) -> dict:
|
||||
return dict(ast.literal_eval(s.replace(""", "\"")))
|
||||
|
||||
|
||||
def dict_to_string(d: dict) -> str:
|
||||
return json.dumps(d)
|
||||
|
||||
|
||||
def _safe_int_cast(value) -> int:
|
||||
|
|
@ -915,3 +1044,6 @@ for _field in dataclasses.fields(FormData):
|
|||
elif _field.type is list:
|
||||
_CAST_RULES_FORM_ARG_TO_NATIVE[_field.name] = string_to_list
|
||||
_CAST_RULES_NATIVE_TO_FORM_ARG[_field.name] = list_to_string
|
||||
elif _field.type is dict:
|
||||
_CAST_RULES_FORM_ARG_TO_NATIVE[_field.name] = string_to_dict
|
||||
_CAST_RULES_NATIVE_TO_FORM_ARG[_field.name] = dict_to_string
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@
|
|||
|
||||
{% block main %}
|
||||
|
||||
{% if DEBUG %}
|
||||
<form id="covid_calculator" name="covid_calculator" onsubmit="return debug_submit(this)" class="form-inline">
|
||||
{% else %}
|
||||
<form id="covid_calculator" name="covid_calculator" action="{{ calculator_prefix }}/report" onsubmit="return validate_form(this)" method="POST">
|
||||
{% endif %}
|
||||
{{ xsrf_form_html }}
|
||||
|
||||
<span class="caimira_version">v{{ calculator_version }}</span>
|
||||
<span class="feedback">Please send feedback to <a href="mailto:CAiMIRA-dev@cern.ch">CAiMIRA-dev@cern.ch</a></span>
|
||||
<header class= "bg-light">
|
||||
|
|
@ -29,13 +36,6 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
{% if DEBUG %}
|
||||
<form id="covid_calculator" name="covid_calculator" onsubmit="return debug_submit(this)" class="form-inline">
|
||||
{% else %}
|
||||
<form id="covid_calculator" name="covid_calculator" action="{{ calculator_prefix }}/report" onsubmit="return validate_form(this)" method="POST">
|
||||
{% endif %}
|
||||
{{ xsrf_form_html }}
|
||||
|
||||
<input type="hidden" name="calculator_version" value="{{ calculator_version }}">
|
||||
|
||||
<section>
|
||||
|
|
@ -424,6 +424,8 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="text" class="form-control d-none" name="precise_activity" value='{}'>
|
||||
|
||||
<div style=" margin-right:2rem;">
|
||||
<div class="boxMargin pb-0">
|
||||
|
|
@ -529,6 +531,7 @@
|
|||
<div data-tooltip="Input breaks that, by default, are the same for infected/exposed person(s) unless specified otherwise.">
|
||||
<span class="tooltip_text">?</span>
|
||||
</div>
|
||||
<input type="text" class="form-control d-none" name="specific_breaks" value='[]'>
|
||||
</span><br>
|
||||
|
||||
<!-- Lunch Options -->
|
||||
|
|
@ -728,4 +731,7 @@
|
|||
|
||||
</form>
|
||||
|
||||
{% block external_dependencies %}
|
||||
{% endblock external_dependencies %}
|
||||
|
||||
{% endblock main %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
from typing import Type
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from caimira.apps.calculator import model_generator
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["break_input", "error"],
|
||||
[
|
||||
[{"start_time": "10:00", "finish_time": "11:00"}, "All breaks should be in a list. Got <class 'dict'>."],
|
||||
[[["start_time", "10:00", "finish_time", "11:00"]], "Each break should be a dictionary. Got <class 'list'>."],
|
||||
[[{"art_time": "10:00", "finish_time": "11:00"}], 'Unable to fetch "start_time" key. Got "art_time".'],
|
||||
[[{"start_time": "10:00", "ish_time": "11:00"}], 'Unable to fetch "finish_time" key. Got "ish_time".'],
|
||||
[[{"start_time": "10", "finish_time": "11:00"}], 'Wrong time format - "HH:MM". Got "10".'],
|
||||
[[{"start_time": "10:00", "finish_time": "11"}], 'Wrong time format - "HH:MM". Got "11".'],
|
||||
]
|
||||
)
|
||||
def test_specific_break_data_structure(break_input, error, baseline_form: model_generator.FormData):
|
||||
baseline_form.specific_breaks = break_input
|
||||
with pytest.raises(TypeError, match=error):
|
||||
baseline_form.validate()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["break_input", "error"],
|
||||
[
|
||||
[[{"start_time": "07:00", "finish_time": "11:00"}, ], "All breaks should be within the simulation time. Got 07:00."],
|
||||
[[{"start_time": "17:00", "finish_time": "18:00"}, ], "All breaks should be within the simulation time. Got 18:00."],
|
||||
[[{"start_time": "10:00", "finish_time": "11:00"}, {"start_time": "17:00", "finish_time": "20:00"}, ], "All breaks should be within the simulation time. Got 20:00."],
|
||||
[[{"start_time": "08:00", "finish_time": "11:00"}, {"start_time": "14:00", "finish_time": "15:00"}, ], "All breaks should be within the simulation time. Got 08:00."],
|
||||
]
|
||||
)
|
||||
def test_specific_break_time(break_input, error, baseline_form: model_generator.FormData):
|
||||
baseline_form.specific_breaks = break_input
|
||||
with pytest.raises(ValueError, match=error):
|
||||
baseline_form.generate_specific_break_times()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["precise_activity_input", "error"],
|
||||
[
|
||||
[["physical_activity", "Light activity", "respiratory_activity", [{"type": "Breathing", "percentage": 50}, {"type": "Speaking", "percentage": 50}]], "The precise activities should be in a dictionary."],
|
||||
[{"pysical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentage": 50}, {"type": "Speaking", "percentage": 50}]}, 'Unable to fetch "physical_activity" key. Got "pysical_activity".'],
|
||||
[{"physical_activity": "Light activity", "rspiratory_activity": [{"type": "Breathing", "percentage": 50}, {"type": "Speaking", "percentage": 50}]}, 'Unable to fetch "respiratory_activity" key. Got "rspiratory_activity".'],
|
||||
[{"physical_activity": ["Light activity"], "respiratory_activity": [{"type": "Breathing", "percentage": 50}, {"type": "Speaking", "percentage": 50}]}, "The physical activities should be a single string."],
|
||||
[{"physical_activity": "Light activity", "respiratory_activity": {"type": "Breathing", "percentage": 100}}, 'The respiratory activities should be in a list.'],
|
||||
[{"physical_activity": "Light activity", "respiratory_activity": [["type", "Speaking", "percentage", 100]]}, 'Each respiratory activity should be defined in a dictionary.'],
|
||||
[{"physical_activity": "Light activity", "respiratory_activity": [{"tpe": "Breathing", "percentage": 50}, {"type": "Speaking", "percentage": 50}]}, 'Unable to fetch "type" key. Got "tpe".'],
|
||||
[{"physical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentag": 50}, {"type": "Speaking", "percentage": 50}]}, 'Unable to fetch "percentage" key. Got "percentag".'],
|
||||
]
|
||||
)
|
||||
def test_precise_activity_structure(precise_activity_input, error, baseline_form: model_generator.FormData):
|
||||
baseline_form.precise_activity = precise_activity_input
|
||||
with pytest.raises(TypeError, match=error):
|
||||
baseline_form.validate()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["precise_activity_input", "error"],
|
||||
[
|
||||
[{"physical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentage": 10}, {"type": "Speaking", "percentage": 50}]}, 'The sum of all respiratory activities should be 100. Got 60.'],
|
||||
[{"physical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentage": 50}, {"type": "Speaking", "percentage": 10}]}, 'The sum of all respiratory activities should be 100. Got 60.'],
|
||||
[{"physical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentage": 10}, {"type": "Speaking", "percentage": 50}, {"type": "Shouting", "percentage": 50}]}, 'The sum of all respiratory activities should be 100. Got 110.'],
|
||||
[{"physical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentage": 50}]}, 'The sum of all respiratory activities should be 100. Got 50.'],
|
||||
]
|
||||
)
|
||||
def test_sum_precise_activity(precise_activity_input, error, baseline_form: model_generator.FormData):
|
||||
baseline_form.precise_activity = precise_activity_input
|
||||
with pytest.raises(ValueError, match=error):
|
||||
baseline_form.validate()
|
||||
Loading…
Reference in a new issue