From f8eb35f18b6d699193080013cfe93b08cff82950 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Fri, 30 Sep 2022 10:51:34 +0100 Subject: [PATCH 1/9] added a method to generate break times based on ARIA inputs --- caimira/apps/calculator/model_generator.py | 33 +++++++++++++++++-- .../templates/base/calculator.form.html.j2 | 1 + 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 51d716da..330dc12e 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -32,6 +32,7 @@ class FormData: air_changes: float air_supply: float arve_sensors_option: bool + aria_breaks: list ceiling_height: float exposed_coffee_break_option: str exposed_coffee_duration: int @@ -97,6 +98,7 @@ class FormData: 'air_changes': 0., 'air_supply': 0., 'arve_sensors_option': False, + 'aria_breaks': '[]', 'calculator_version': _NO_DEFAULT, 'ceiling_height': 0., 'exposed_coffee_break_option': 'coffee_break_0', @@ -642,6 +644,25 @@ class FormData: else: return self.exposed_coffee_break_times() + def generate_aria_break_times(self) -> models.BoundarySequence_t: + break_times = [] + for n in self.aria_breaks: + # Input validations. + if type(n) is not dict: + raise TypeError('Each break should be a dictionary.') + dict_keys = list(n.keys()) + if "start_time" not in n: + raise TypeError(f'Unable to fetch "start_time" key. Got "{dict_keys[0]}".') + if "finish_time" not in n: + raise TypeError(f'Unable to fetch "finish_time" key. Got "{dict_keys[1]}".') + for time in n.values(): + if not datetime.datetime.strptime(time, '%H:%M'): return + # Parse break times. + begin = time_string_to_minutes(n["start_time"]) + end = time_string_to_minutes(n["finish_time"]) + break_times.append((begin, end)) + return tuple(break_times) + def present_interval( self, start: int, @@ -735,9 +756,13 @@ class FormData: return models.SpecificInterval(tuple(present_intervals)) def infected_present_interval(self) -> models.Interval: + if len(self.aria_breaks) > 0: # It means the breaks were defined by ARIA interface + breaks = self.generate_aria_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 +771,13 @@ class FormData: return models.SpecificInterval(present_times=((start_time/60, (start_time + duration)/60),)) def exposed_present_interval(self) -> models.Interval: + if len(self.aria_breaks) > 0: # It means the breaks were defined by ARIA interface + breaks = self.generate_aria_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, ) diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index bff35ef8..02fcaa93 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -529,6 +529,7 @@
?
+
From 8cccb0eba9aa951c75163448fdd67291558dbe8b Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 3 Oct 2022 10:02:53 +0100 Subject: [PATCH 2/9] added method and validation to generate precise activities from ARIA --- caimira/apps/calculator/model_generator.py | 96 +++++++++++++++++-- .../templates/base/calculator.form.html.j2 | 3 + 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 330dc12e..3775369f 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -33,6 +33,7 @@ class FormData: air_supply: float arve_sensors_option: bool aria_breaks: list + aria_precise: dict ceiling_height: float exposed_coffee_break_option: str exposed_coffee_duration: int @@ -99,6 +100,7 @@ class FormData: 'air_supply': 0., 'arve_sensors_option': False, 'aria_breaks': '[]', + 'aria_precise': '{}', 'calculator_version': _NO_DEFAULT, 'ceiling_height': 0., 'exposed_coffee_break_option': 'coffee_break_0', @@ -470,6 +472,41 @@ class FormData: mask = models.Mask.types['No mask'] return mask + def generate_aria_activity_expiration(self) -> tuple[str, typing.Any]: + # Input validations. + if type(self.aria_precise) is not dict: + raise TypeError('The precise activities should be in a dictionary.') + + if len(self.aria_precise) == 0: # If no precise activity was defined. + return tuple(result) + + dict_keys = list(self.aria_precise.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]}".') + + result = [] + for physical_activity in self.aria_precise['physical_activity']: + result.append(physical_activity['type']) + + if type(self.aria_precise['respiratory_activity']) is not list: + raise TypeError('The respiratory activities should be in a list.') + + respiratory_dict = {} + for respiratory_activity in self.aria_precise['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]}".') + respiratory_dict[respiratory_activity['type']] = respiratory_activity['percentage'] + + result.append(respiratory_dict) + return tuple(result) + def infected_population(self) -> mc.InfectedPopulation: # Initializes the virus virus = virus_distributions[self.virus_type] @@ -513,8 +550,34 @@ class FormData: #Model 1/2 of time spent speaking in a workshop. {'Speaking': 1, 'Breathing': 1}), 'gym':('Heavy exercise', 'Breathing'), + # ARIA UI activity types + 'household-day': ( + 'Seated', + {'Breathing': 5, 'Speaking': 5} + ), + 'household-night': ( + 'Seated', + {'Breathing': 7, 'Speaking': 3} + ), + 'primary-school': ( + 'Seated', + {'Breathing': 5, 'Speaking': 5} + ), + 'secondary-school': ( + 'Seated', + {'Breathing': 7, 'Speaking': 3} + ), + 'university': ( + 'Seated', + {'Breathing': 9, 'Speaking': 1} + ), + 'restaurant': ( + 'Seated', + {'Breathing': 1, 'Speaking': 9} + ), + 'precise': self.generate_aria_activity_expiration(), } - + [activity_defn, expiration_defn] = scenario_activity_and_expiration[self.activity_type] activity = activity_distributions[activity_defn] expiration = build_expiration(expiration_defn) @@ -546,6 +609,13 @@ class FormData: 'workshop': 'Moderate activity', 'lab':'Light activity', 'gym':'Heavy exercise', + 'household-day': 'Seated', + 'household-night': 'Seated', + 'primary-school': 'Seated', + 'secondary-school': 'Seated', + 'university': 'Seated', + 'restaurant': 'Seated', + 'precise': 'Seated', } activity_defn = scenario_activity[self.activity_type] @@ -857,7 +927,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'} @@ -902,12 +975,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: @@ -944,3 +1025,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 diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 02fcaa93..96dda7bf 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -421,9 +421,12 @@ + + +
From 06eaaf7dd17bb2a70fd65f0ee391c7715003e5c1 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 3 Oct 2022 10:10:42 +0100 Subject: [PATCH 3/9] added additional typing return types --- caimira/apps/calculator/model_generator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 3775369f..7002cf60 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -472,13 +472,13 @@ class FormData: mask = models.Mask.types['No mask'] return mask - def generate_aria_activity_expiration(self) -> tuple[str, typing.Any]: + def generate_aria_activity_expiration(self) -> typing.Tuple[typing.Any, ...]: # Input validations. if type(self.aria_precise) is not dict: raise TypeError('The precise activities should be in a dictionary.') if len(self.aria_precise) == 0: # If no precise activity was defined. - return tuple(result) + return () dict_keys = list(self.aria_precise.keys()) if "physical_activity" not in dict_keys: @@ -726,7 +726,8 @@ class FormData: if "finish_time" not in n: raise TypeError(f'Unable to fetch "finish_time" key. Got "{dict_keys[1]}".') for time in n.values(): - if not datetime.datetime.strptime(time, '%H:%M'): return + if not datetime.datetime.strptime(time, '%H:%M'): + raise TypeError(f'Wrong time format - "HH:MM". Got "{time}".') # Parse break times. begin = time_string_to_minutes(n["start_time"]) end = time_string_to_minutes(n["finish_time"]) From 5cce4956926613cd6d454bb5d9e16445f6e406e3 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Fri, 7 Oct 2022 15:48:48 +0200 Subject: [PATCH 4/9] changed precise activity type object structure --- caimira/apps/calculator/model_generator.py | 26 +++++++++++-------- .../templates/base/calculator.form.html.j2 | 1 - 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 7002cf60..bcc631da 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -486,14 +486,14 @@ class FormData: if "respiratory_activity" not in dict_keys: raise TypeError(f'Unable to fetch "respiratory_activity" key. Got "{dict_keys[1]}".') - result = [] - for physical_activity in self.aria_precise['physical_activity']: - result.append(physical_activity['type']) + if type(self.aria_precise['physical_activity']) is not str: + raise TypeError('The physical activities should be a single string.') if type(self.aria_precise['respiratory_activity']) is not list: raise TypeError('The respiratory activities should be in a list.') respiratory_dict = {} + total_percentage = 0 for respiratory_activity in self.aria_precise['respiratory_activity']: if type(respiratory_activity) is not dict: raise TypeError('Each respiratory activity should be defined in a dictionary.') @@ -503,9 +503,12 @@ class FormData: if "percentage" not in dict_keys: raise TypeError(f'Unable to fetch "percentage" key. Got "{dict_keys[1]}".') respiratory_dict[respiratory_activity['type']] = respiratory_activity['percentage'] + total_percentage += respiratory_activity['percentage'] - result.append(respiratory_dict) - return tuple(result) + if total_percentage != 100: + raise ValueError(f'The sum of all respiratory activities should be 100. Got {total_percentage}.') + + return (self.aria_precise['physical_activity'], respiratory_dict) def infected_population(self) -> mc.InfectedPopulation: # Initializes the virus @@ -552,7 +555,7 @@ class FormData: 'gym':('Heavy exercise', 'Breathing'), # ARIA UI activity types 'household-day': ( - 'Seated', + 'Light activity', {'Breathing': 5, 'Speaking': 5} ), 'household-night': ( @@ -560,11 +563,11 @@ class FormData: {'Breathing': 7, 'Speaking': 3} ), 'primary-school': ( - 'Seated', + 'Light activity', {'Breathing': 5, 'Speaking': 5} ), 'secondary-school': ( - 'Seated', + 'Light activity', {'Breathing': 7, 'Speaking': 3} ), 'university': ( @@ -579,6 +582,7 @@ class FormData: } [activity_defn, expiration_defn] = scenario_activity_and_expiration[self.activity_type] + print(scenario_activity_and_expiration[self.activity_type]) activity = activity_distributions[activity_defn] expiration = build_expiration(expiration_defn) @@ -609,10 +613,10 @@ class FormData: 'workshop': 'Moderate activity', 'lab':'Light activity', 'gym':'Heavy exercise', - 'household-day': 'Seated', + 'household-day': 'Light activity', 'household-night': 'Seated', - 'primary-school': 'Seated', - 'secondary-school': 'Seated', + 'primary-school': 'Light activity', + 'secondary-school': 'Light activity', 'university': 'Seated', 'restaurant': 'Seated', 'precise': 'Seated', diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 96dda7bf..4fb47280 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -421,7 +421,6 @@ -
From 180bf4dae597cb17a85d22f410f89f0595fb64fb Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Fri, 7 Oct 2022 17:32:06 +0200 Subject: [PATCH 5/9] completed validation on ARIA inputs model_generator --- caimira/apps/calculator/model_generator.py | 93 ++++++++++++---------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index bcc631da..0a3ac54a 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -5,6 +5,7 @@ import logging import typing import ast import json +import re import numpy as np @@ -311,6 +312,55 @@ class FormData: raise ValueError("mechanical_ventilation_type cannot be 'not-applicable' if " "ventilation_type is 'mechanical_ventilation'") + # Validate ARIA inputs - breaks + if self.aria_breaks != []: + if type(self.aria_breaks) is not list: + raise TypeError(f'All breaks should be in a list. Got {type(self.aria_breaks)}.') + for input_break in self.aria_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 ARIA inputs - precise activity + if self.aria_precise != {}: + if type(self.aria_precise) is not dict: + raise TypeError('The precise activities should be in a dictionary.') + + dict_keys = list(self.aria_precise.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.aria_precise['physical_activity']) is not str: + raise TypeError('The physical activities should be a single string.') + + if type(self.aria_precise['respiratory_activity']) is not list: + raise TypeError('The respiratory activities should be in a list.') + + total_percentage = 0 + for respiratory_activity in self.aria_precise['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': @@ -473,40 +523,9 @@ class FormData: return mask def generate_aria_activity_expiration(self) -> typing.Tuple[typing.Any, ...]: - # Input validations. - if type(self.aria_precise) is not dict: - raise TypeError('The precise activities should be in a dictionary.') - - if len(self.aria_precise) == 0: # If no precise activity was defined. - return () - - dict_keys = list(self.aria_precise.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.aria_precise['physical_activity']) is not str: - raise TypeError('The physical activities should be a single string.') - - if type(self.aria_precise['respiratory_activity']) is not list: - raise TypeError('The respiratory activities should be in a list.') - respiratory_dict = {} - total_percentage = 0 for respiratory_activity in self.aria_precise['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]}".') respiratory_dict[respiratory_activity['type']] = respiratory_activity['percentage'] - total_percentage += respiratory_activity['percentage'] - - if total_percentage != 100: - raise ValueError(f'The sum of all respiratory activities should be 100. Got {total_percentage}.') return (self.aria_precise['physical_activity'], respiratory_dict) @@ -582,7 +601,6 @@ class FormData: } [activity_defn, expiration_defn] = scenario_activity_and_expiration[self.activity_type] - print(scenario_activity_and_expiration[self.activity_type]) activity = activity_distributions[activity_defn] expiration = build_expiration(expiration_defn) @@ -721,17 +739,6 @@ class FormData: def generate_aria_break_times(self) -> models.BoundarySequence_t: break_times = [] for n in self.aria_breaks: - # Input validations. - if type(n) is not dict: - raise TypeError('Each break should be a dictionary.') - dict_keys = list(n.keys()) - if "start_time" not in n: - raise TypeError(f'Unable to fetch "start_time" key. Got "{dict_keys[0]}".') - if "finish_time" not in n: - raise TypeError(f'Unable to fetch "finish_time" key. Got "{dict_keys[1]}".') - for time in n.values(): - if not datetime.datetime.strptime(time, '%H:%M'): - raise TypeError(f'Wrong time format - "HH:MM". Got "{time}".') # Parse break times. begin = time_string_to_minutes(n["start_time"]) end = time_string_to_minutes(n["finish_time"]) From 0b37aeca7dc800e0cc93d4afbc79443e960e285b Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 10 Oct 2022 09:54:56 +0200 Subject: [PATCH 6/9] added tests for aria breaks and precise activities --- caimira/apps/calculator/model_generator.py | 2 + .../calculator/test_aria_model_generator.py | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 caimira/tests/apps/calculator/test_aria_model_generator.py diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 0a3ac54a..52cca792 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -523,6 +523,8 @@ class FormData: return mask def generate_aria_activity_expiration(self) -> typing.Tuple[typing.Any, ...]: + if self.aria_precise == {}: # It means the precise activity is not defined by ARIA interface. + return () respiratory_dict = {} for respiratory_activity in self.aria_precise['respiratory_activity']: respiratory_dict[respiratory_activity['type']] = respiratory_activity['percentage'] diff --git a/caimira/tests/apps/calculator/test_aria_model_generator.py b/caimira/tests/apps/calculator/test_aria_model_generator.py new file mode 100644 index 00000000..7474d5e8 --- /dev/null +++ b/caimira/tests/apps/calculator/test_aria_model_generator.py @@ -0,0 +1,55 @@ +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 ."], + [[["start_time", "10:00", "finish_time", "11:00"]], "Each break should be a dictionary. Got ."], + [[{"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_aria_break_data_structure(break_input, error, baseline_form: model_generator.FormData): + baseline_form.aria_breaks = break_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": 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_aria_precise_activity_structure(precise_activity_input, error, baseline_form: model_generator.FormData): + baseline_form.aria_precise = 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_aria_sum_precise_activity(precise_activity_input, error, baseline_form: model_generator.FormData): + baseline_form.aria_precise = precise_activity_input + with pytest.raises(ValueError, match=error): + baseline_form.validate() \ No newline at end of file From 388465ba9b33830b22be4a2b3ffd3ab93f18fa85 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 10 Oct 2022 15:17:44 +0200 Subject: [PATCH 7/9] added tests for simulation time start and end with breaks --- caimira/apps/calculator/model_generator.py | 9 +++++++-- .../apps/calculator/test_aria_model_generator.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 52cca792..13bac79f 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -744,6 +744,11 @@ class FormData: # Parse break times. begin = time_string_to_minutes(n["start_time"]) end = time_string_to_minutes(n["finish_time"]) + for time in [begin, end]: + # In ARIA, 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) @@ -840,7 +845,7 @@ class FormData: return models.SpecificInterval(tuple(present_intervals)) def infected_present_interval(self) -> models.Interval: - if len(self.aria_breaks) > 0: # It means the breaks were defined by ARIA interface + if self.aria_breaks != []: # It means the breaks were defined by ARIA interface breaks = self.generate_aria_break_times() else: breaks = self.infected_lunch_break_times() + self.infected_coffee_break_times() @@ -855,7 +860,7 @@ class FormData: return models.SpecificInterval(present_times=((start_time/60, (start_time + duration)/60),)) def exposed_present_interval(self) -> models.Interval: - if len(self.aria_breaks) > 0: # It means the breaks were defined by ARIA interface + if self.aria_breaks != []: # It means the breaks were defined by ARIA interface breaks = self.generate_aria_break_times() else: breaks = self.exposed_lunch_break_times() + self.exposed_coffee_break_times() diff --git a/caimira/tests/apps/calculator/test_aria_model_generator.py b/caimira/tests/apps/calculator/test_aria_model_generator.py index 7474d5e8..bb87bbdd 100644 --- a/caimira/tests/apps/calculator/test_aria_model_generator.py +++ b/caimira/tests/apps/calculator/test_aria_model_generator.py @@ -22,6 +22,21 @@ def test_aria_break_data_structure(break_input, error, baseline_form: model_gene 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_aria_break_time(break_input, error, baseline_form: model_generator.FormData): + baseline_form.aria_breaks = break_input + with pytest.raises(ValueError, match=error): + baseline_form.generate_aria_break_times() + + @pytest.mark.parametrize( ["precise_activity_input", "error"], [ @@ -40,6 +55,7 @@ def test_aria_precise_activity_structure(precise_activity_input, error, baseline with pytest.raises(TypeError, match=error): baseline_form.validate() + @pytest.mark.parametrize( ["precise_activity_input", "error"], [ From 21953de4a22c4eead4d63daf2c017d1d437cd2a1 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 10 Nov 2022 10:17:46 +0100 Subject: [PATCH 8/9] added reference for external dependencies (react) in calculator.form.html.j2 file --- .../apps/templates/base/calculator.form.html.j2 | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 4fb47280..7122dfc4 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -18,6 +18,13 @@ {% block main %} +{% if DEBUG %} +
+{% else %} + +{% endif %} +{{ xsrf_form_html }} + v{{ calculator_version }}
@@ -29,13 +36,6 @@
-{% if DEBUG %} - -{% else %} - -{% endif %} -{{ xsrf_form_html }} -
@@ -731,4 +731,7 @@ +{% block external_dependencies %} +{% endblock external_dependencies %} + {% endblock main %} From d2ea27ce793aa09b2be1662587dd4b9c2f6e456d Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Tue, 6 Dec 2022 09:59:43 +0100 Subject: [PATCH 9/9] removed ARIA references --- caimira/apps/calculator/model_generator.py | 58 +++++++++---------- .../templates/base/calculator.form.html.j2 | 4 +- ...or.py => test_specific_model_generator.py} | 18 +++--- 3 files changed, 40 insertions(+), 40 deletions(-) rename caimira/tests/apps/calculator/{test_aria_model_generator.py => test_specific_model_generator.py} (86%) diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 13bac79f..970cf289 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -33,8 +33,8 @@ class FormData: air_changes: float air_supply: float arve_sensors_option: bool - aria_breaks: list - aria_precise: dict + specific_breaks: list + precise_activity: dict ceiling_height: float exposed_coffee_break_option: str exposed_coffee_duration: int @@ -100,8 +100,8 @@ class FormData: 'air_changes': 0., 'air_supply': 0., 'arve_sensors_option': False, - 'aria_breaks': '[]', - 'aria_precise': '{}', + 'specific_breaks': '[]', + 'precise_activity': '{}', 'calculator_version': _NO_DEFAULT, 'ceiling_height': 0., 'exposed_coffee_break_option': 'coffee_break_0', @@ -312,11 +312,11 @@ class FormData: raise ValueError("mechanical_ventilation_type cannot be 'not-applicable' if " "ventilation_type is 'mechanical_ventilation'") - # Validate ARIA inputs - breaks - if self.aria_breaks != []: - if type(self.aria_breaks) is not list: - raise TypeError(f'All breaks should be in a list. Got {type(self.aria_breaks)}.') - for input_break in self.aria_breaks: + # 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)}.') @@ -329,25 +329,25 @@ class FormData: 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 ARIA inputs - precise activity - if self.aria_precise != {}: - if type(self.aria_precise) is not dict: + # 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.aria_precise.keys()) + 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.aria_precise['physical_activity']) is not str: + if type(self.precise_activity['physical_activity']) is not str: raise TypeError('The physical activities should be a single string.') - if type(self.aria_precise['respiratory_activity']) is not list: + 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.aria_precise['respiratory_activity']: + 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()) @@ -522,14 +522,14 @@ class FormData: mask = models.Mask.types['No mask'] return mask - def generate_aria_activity_expiration(self) -> typing.Tuple[typing.Any, ...]: - if self.aria_precise == {}: # It means the precise activity is not defined by ARIA interface. + 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.aria_precise['respiratory_activity']: + for respiratory_activity in self.precise_activity['respiratory_activity']: respiratory_dict[respiratory_activity['type']] = respiratory_activity['percentage'] - return (self.aria_precise['physical_activity'], respiratory_dict) + return (self.precise_activity['physical_activity'], respiratory_dict) def infected_population(self) -> mc.InfectedPopulation: # Initializes the virus @@ -574,7 +574,7 @@ class FormData: #Model 1/2 of time spent speaking in a workshop. {'Speaking': 1, 'Breathing': 1}), 'gym':('Heavy exercise', 'Breathing'), - # ARIA UI activity types + # Other activity types 'household-day': ( 'Light activity', {'Breathing': 5, 'Speaking': 5} @@ -599,7 +599,7 @@ class FormData: 'Seated', {'Breathing': 1, 'Speaking': 9} ), - 'precise': self.generate_aria_activity_expiration(), + 'precise': self.generate_precise_activity_expiration(), } [activity_defn, expiration_defn] = scenario_activity_and_expiration[self.activity_type] @@ -738,14 +738,14 @@ class FormData: else: return self.exposed_coffee_break_times() - def generate_aria_break_times(self) -> models.BoundarySequence_t: + def generate_specific_break_times(self) -> models.BoundarySequence_t: break_times = [] - for n in self.aria_breaks: + 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]: - # In ARIA, the infected and exposed presence is the same. + # 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)}.') @@ -845,8 +845,8 @@ class FormData: return models.SpecificInterval(tuple(present_intervals)) def infected_present_interval(self) -> models.Interval: - if self.aria_breaks != []: # It means the breaks were defined by ARIA interface - breaks = self.generate_aria_break_times() + 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( @@ -860,8 +860,8 @@ class FormData: return models.SpecificInterval(present_times=((start_time/60, (start_time + duration)/60),)) def exposed_present_interval(self) -> models.Interval: - if self.aria_breaks != []: # It means the breaks were defined by ARIA interface - breaks = self.generate_aria_break_times() + 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( diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 7122dfc4..e6326a12 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -425,7 +425,7 @@ - +
@@ -531,7 +531,7 @@
?
- +
diff --git a/caimira/tests/apps/calculator/test_aria_model_generator.py b/caimira/tests/apps/calculator/test_specific_model_generator.py similarity index 86% rename from caimira/tests/apps/calculator/test_aria_model_generator.py rename to caimira/tests/apps/calculator/test_specific_model_generator.py index bb87bbdd..859af107 100644 --- a/caimira/tests/apps/calculator/test_aria_model_generator.py +++ b/caimira/tests/apps/calculator/test_specific_model_generator.py @@ -16,8 +16,8 @@ from caimira.apps.calculator import model_generator [[{"start_time": "10:00", "finish_time": "11"}], 'Wrong time format - "HH:MM". Got "11".'], ] ) -def test_aria_break_data_structure(break_input, error, baseline_form: model_generator.FormData): - baseline_form.aria_breaks = break_input +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() @@ -31,10 +31,10 @@ def test_aria_break_data_structure(break_input, error, baseline_form: model_gene [[{"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_aria_break_time(break_input, error, baseline_form: model_generator.FormData): - baseline_form.aria_breaks = break_input +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_aria_break_times() + baseline_form.generate_specific_break_times() @pytest.mark.parametrize( @@ -50,8 +50,8 @@ def test_aria_break_time(break_input, error, baseline_form: model_generator.Form [{"physical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentag": 50}, {"type": "Speaking", "percentage": 50}]}, 'Unable to fetch "percentage" key. Got "percentag".'], ] ) -def test_aria_precise_activity_structure(precise_activity_input, error, baseline_form: model_generator.FormData): - baseline_form.aria_precise = precise_activity_input +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() @@ -65,7 +65,7 @@ def test_aria_precise_activity_structure(precise_activity_input, error, baseline [{"physical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentage": 50}]}, 'The sum of all respiratory activities should be 100. Got 50.'], ] ) -def test_aria_sum_precise_activity(precise_activity_input, error, baseline_form: model_generator.FormData): - baseline_form.aria_precise = precise_activity_input +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() \ No newline at end of file