From cdc2b60f65ee8413d239bd3fdd991f0b72084dbd Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Fri, 11 Aug 2023 11:13:46 +0100 Subject: [PATCH] optimised validation method --- .../apps/calculator/co2_model_generator.py | 105 ++++-------------- caimira/apps/calculator/model_generator.py | 32 ++++-- caimira/apps/calculator/report_generator.py | 3 +- caimira/apps/calculator/static/js/co2_form.js | 3 +- 4 files changed, 45 insertions(+), 98 deletions(-) diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index 0187535b..bc55ae4e 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -24,13 +24,13 @@ class CO2FormData(model_generator.FormData): exposed_start: model_generator.minutes_since_midnight fitting_ventilation_states: list fitting_ventilation_type: str - infected_coffee_break_option: str #Used if infected_dont_have_breaks_with_exposed - infected_coffee_duration: int #Used if infected_dont_have_breaks_with_exposed + infected_coffee_break_option: str + infected_coffee_duration: int infected_dont_have_breaks_with_exposed: bool infected_finish: model_generator.minutes_since_midnight - infected_lunch_finish: model_generator.minutes_since_midnight #Used if infected_dont_have_breaks_with_exposed - infected_lunch_option: bool #Used if infected_dont_have_breaks_with_exposed - infected_lunch_start: model_generator.minutes_since_midnight #Used if infected_dont_have_breaks_with_exposed + infected_lunch_finish: model_generator.minutes_since_midnight + infected_lunch_option: bool + infected_lunch_start: model_generator.minutes_since_midnight infected_people: int infected_start: model_generator.minutes_since_midnight room_volume: float @@ -98,73 +98,22 @@ def from_dict(self, form_data: typing.Dict) -> "CO2FormData": raise ValueError(f'Invalid argument "{html.escape(key)}" given') instance = self(**form_data) - instance.validate() + instance.validate_population_parameters() return instance - def validate(self): - # Validate number of infected <= number of total people - if self.infected_people >= self.total_people: - raise ValueError('Number of infected people cannot be more or equal than number of total people.') - - # Validate time intervals selected by user - time_intervals = [ - ['exposed_start', 'exposed_finish'], - ['infected_start', 'infected_finish'], - ] - if self.exposed_lunch_option: - time_intervals.append(['exposed_lunch_start', 'exposed_lunch_finish']) - if self.infected_dont_have_breaks_with_exposed and self.infected_lunch_option: - time_intervals.append(['infected_lunch_start', 'infected_lunch_finish']) - - for start_name, end_name in time_intervals: - start = getattr(self, start_name) - end = getattr(self, end_name) - if start > end: - raise ValueError( - f"{start_name} must be less than {end_name}. Got {start} and {end}.") - - def validate_lunch(start, finish): - lunch_start = getattr(self, f'{population}_lunch_start') - lunch_finish = getattr(self, f'{population}_lunch_finish') - return (start <= lunch_start <= finish and - start <= lunch_finish <= finish) - - def get_lunch_mins(population): - lunch_mins = 0 - if getattr(self, f'{population}_lunch_option'): - lunch_mins = getattr(self, f'{population}_lunch_finish') - getattr(self, f'{population}_lunch_start') - return lunch_mins - - def get_coffee_mins(population): - coffee_mins = 0 - if getattr(self, f'{population}_coffee_break_option') != 'coffee_break_0': - coffee_mins = COFFEE_OPTIONS_INT[getattr(self, f'{population}_coffee_break_option')] * getattr(self, f'{population}_coffee_duration') - return coffee_mins - - def get_activity_mins(population): - return getattr(self, f'{population}_finish') - getattr(self, f'{population}_start') - - populations = ['exposed', 'infected'] if self.infected_dont_have_breaks_with_exposed else ['exposed'] - for population in populations: - # Validate lunch time within the activity times. - if (getattr(self, f'{population}_lunch_option') and - not validate_lunch(getattr(self, f'{population}_start'), getattr(self, f'{population}_finish')) - ): - raise ValueError( - f"{population} lunch break must be within presence times." - ) - - # Length of breaks < length of activity - if (get_lunch_mins(population) + get_coffee_mins(population)) >= get_activity_mins(population): - raise ValueError( - f"Length of breaks >= Length of {population} presence." - ) - - validation_tuples = [('exposed_coffee_break_option', COFFEE_OPTIONS_INT), - ('infected_coffee_break_option', COFFEE_OPTIONS_INT),] - 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}") + def population_present_changes(self) -> typing.List[float]: + state_change_times = set(self.infected_present_interval().transition_times()) + state_change_times.update(self.exposed_present_interval().transition_times()) + return sorted(state_change_times) + + def ventilation_transition_times(self) -> typing.Tuple[float, ...]: + # Check what type of ventilation is considered for the fitting + if self.fitting_ventilation_type == 'fitting_natural_ventilation': + vent_states = self.fitting_ventilation_states + vent_states.append(self.CO2_data['times'][-1]) + return tuple(vent_states) + else: + return tuple((self.CO2_data['times'][0], self.CO2_data['times'][-1])) def build_model(self, size=DEFAULT_MC_SAMPLE_SIZE) -> models.CO2DataModel: # type: ignore infected_population: models.Population = self.infected_population().build_model(size) @@ -180,21 +129,7 @@ def build_model(self, size=DEFAULT_MC_SAMPLE_SIZE) -> models.CO2DataModel: # typ ventilation_transition_times=self.ventilation_transition_times(), times=self.CO2_data['times'], CO2_concentrations=self.CO2_data['CO2'], - ) - - def population_present_changes(self) -> typing.List[float]: - state_change_times = set(self.infected_present_interval().transition_times()) - state_change_times.update(self.exposed_present_interval().transition_times()) - return sorted(state_change_times) - - def ventilation_transition_times(self) -> typing.Tuple[float, ...]: - # Check what type of ventilation is considered for the fitting - if self.fitting_ventilation_type == 'fitting_natural_ventilation': - vent_states = self.fitting_ventilation_states - vent_states.append(self.CO2_data['times'][-1]) - return tuple(vent_states) - else: - return tuple((self.CO2_data['times'][0], self.CO2_data['times'][-1])) + ) for _field in dataclasses.fields(CO2FormData): diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index f659867b..eeae4ece 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -143,14 +143,11 @@ def to_dict(cls, form: "FormData", strip_defaults: bool = False) -> dict: if default is not NO_DEFAULT and value in [default, 'not-applicable']: form_dict.pop(attr) return form_dict - - def validate(self): - # Validate number of infected people == 1 when activity is Conference/Training. - if self.activity_type == 'training' and self.infected_people > 1: - raise ValueError('Conference/Training activities are limited to 1 infected.') + + def validate_population_parameters(self): # Validate number of infected <= number of total people - elif self.infected_people >= self.total_people: - raise ValueError('Number of infected people cannot be more or equal than number of total people.') + if self.infected_people >= self.total_people: + raise ValueError('Number of infected people cannot be more or equal than number of total people.') # Validate time intervals selected by user time_intervals = [ @@ -190,7 +187,7 @@ def get_coffee_mins(population): def get_activity_mins(population): return getattr(self, f'{population}_finish') - getattr(self, f'{population}_start') - populations = ['exposed', 'infected'] if self.infected_dont_have_breaks_with_exposed else ['exposed'] + populations = ['exposed', 'infected'] if self.infected_dont_have_breaks_with_exposed else ['exposed'] for population in populations: # Validate lunch time within the activity times. if (getattr(self, f'{population}_lunch_option') and @@ -205,10 +202,17 @@ def get_activity_mins(population): raise ValueError( f"Length of breaks >= Length of {population} presence." ) + + for attr_name, valid_set in [('exposed_coffee_break_option', COFFEE_OPTIONS_INT), + ('infected_coffee_break_option', COFFEE_OPTIONS_INT)]: + if getattr(self, attr_name) not in valid_set: + raise ValueError(f"{getattr(self, attr_name)} is not a valid value for {attr_name}") - validation_tuples = [('activity_type', ACTIVITY_TYPES), - ('exposed_coffee_break_option', COFFEE_OPTIONS_INT), - ('infected_coffee_break_option', COFFEE_OPTIONS_INT), + def validate(self): + # Validate population parameters + self.validate_population_parameters() + + validation_tuples = [('activity_type', ACTIVITY_TYPES), ('mechanical_ventilation_type', MECHANICAL_VENTILATION_TYPES), ('mask_type', MASK_TYPES), ('mask_wearing_option', MASK_WEARING_OPTIONS), @@ -221,10 +225,16 @@ def get_activity_mins(population): ('ascertainment_bias', CONFIDENCE_LEVEL_OPTIONS), ('vaccine_type', VACCINE_TYPE), ('vaccine_booster_type', VACCINE_BOOSTER_TYPE),] + 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}") + + # Validate number of infected people == 1 when activity is Conference/Training. + if self.activity_type == 'training' and self.infected_people > 1: + raise ValueError('Conference/Training activities are limited to 1 infected.') + # Validate ventilation parameters if self.ventilation_type == 'natural_ventilation': if self.window_type == 'not-applicable': raise ValueError( diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index b76a2590..75d019d9 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -317,11 +317,12 @@ def readable_minutes(minutes: int) -> str: def hour_format(hour: float) -> str: + # Convert float hour to HH:MM format hours = int(hour) minutes = int(hour % 1 * 60) - return f"{hours}:{minutes if minutes != 0 else '00'}" + def percentage(absolute: float) -> float: return absolute * 100 diff --git a/caimira/apps/calculator/static/js/co2_form.js b/caimira/apps/calculator/static/js/co2_form.js index 45f45941..d94fe6a0 100644 --- a/caimira/apps/calculator/static/js/co2_form.js +++ b/caimira/apps/calculator/static/js/co2_form.js @@ -1,3 +1,4 @@ +// Input data for CO2 fitting algorithm const CO2_data_form = [ "CO2_data", "exposed_coffee_break_option", @@ -104,7 +105,7 @@ function uploadFile(endpoint) { } } - // Call function to convert Excel file to JSON and further processing + // Convert Excel file to JSON and further processing try { generateJSONStructure(endpoint, data); // If all validations pass, process the file here or display a success message