diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index 9a3d60f4..de7b9010 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -12,35 +12,34 @@ from cara import data LOG = logging.getLogger(__name__) +minutes_since_midnight = typing.NewType('minutes_since_midnight', int) @dataclass class FormData: - # Number of minutes after 00:00 - exposed_finish: int - exposed_lunch_finish: int - exposed_lunch_start: int - exposed_start: int - infected_finish: int - infected_lunch_finish: int #Used if infected_dont_have_breaks_with_exposed - infected_lunch_start: int #Used if infected_dont_have_breaks_with_exposed - infected_start: int - activity_type: str air_changes: float air_supply: float ceiling_height: float - exposed_coffee_breaks: int + exposed_coffee_break_option: str exposed_coffee_duration: int + exposed_finish: minutes_since_midnight + exposed_lunch_finish: minutes_since_midnight exposed_lunch_option: bool + exposed_lunch_start: minutes_since_midnight + exposed_start: minutes_since_midnight floor_area: float hepa_amount: float hepa_option: bool - infected_coffee_breaks: int #Used if infected_dont_have_breaks_with_exposed + 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_dont_have_breaks_with_exposed: bool + infected_finish: minutes_since_midnight + infected_lunch_finish: 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: minutes_since_midnight #Used if infected_dont_have_breaks_with_exposed infected_people: int + infected_start: minutes_since_midnight mask_type: str - mask_wearing: str + mask_wearing_option: str mechanical_ventilation_type: str model_version: str opening_distance: float @@ -57,14 +56,14 @@ class FormData: window_type: str window_width: float windows_number: int - windows_open: str + window_opening_regime: str @classmethod def from_dict(cls, form_data: typing.Dict) -> "FormData": # Take a copy of the form data so that we can mutate it. form_data = form_data.copy() - valid_na_values = ['windows_open', 'window_type', 'mechanical_ventilation_type', 'infected_dont_have_breaks_with_exposed'] + valid_na_values = ['window_opening_regime', 'window_type', 'mechanical_ventilation_type', 'infected_dont_have_breaks_with_exposed'] for name in valid_na_values: if not form_data.get(name, ''): form_data[name] = 'not-applicable' @@ -101,7 +100,7 @@ class FormData: air_changes=float(form_data['air_changes']), air_supply=float(form_data['air_supply']), ceiling_height=float(form_data['ceiling_height']), - exposed_coffee_breaks=int(form_data['exposed_coffee_breaks']), + exposed_coffee_break_option=form_data['exposed_coffee_break_option'], exposed_coffee_duration=int(form_data['exposed_coffee_duration']), exposed_finish=form_data['exposed_finish'], exposed_lunch_finish=form_data['exposed_lunch_finish'], @@ -111,7 +110,7 @@ class FormData: floor_area=float(form_data['floor_area']), hepa_amount=float(form_data['hepa_amount']), hepa_option=form_data['hepa_option'], - infected_coffee_breaks=int(form_data['infected_coffee_breaks']), + infected_coffee_break_option=form_data['infected_coffee_break_option'], infected_coffee_duration=int(form_data['infected_coffee_duration']), infected_dont_have_breaks_with_exposed=form_data['infected_dont_have_breaks_with_exposed'], infected_finish=form_data['infected_finish'], @@ -121,7 +120,7 @@ class FormData: infected_people=int(form_data['infected_people']), infected_start=form_data['infected_start'], mask_type=form_data['mask_type'], - mask_wearing=form_data['mask_wearing'], + mask_wearing_option=form_data['mask_wearing_option'], mechanical_ventilation_type=form_data['mechanical_ventilation_type'], model_version=form_data['model_version'], opening_distance=float(form_data['opening_distance']), @@ -138,7 +137,7 @@ class FormData: window_type=form_data['window_type'], window_width=float(form_data['window_width']), windows_number=int(form_data['windows_number']), - windows_open=form_data['windows_open'], + window_opening_regime=form_data['window_opening_regime'], ) instance.validate() return instance @@ -161,13 +160,15 @@ class FormData: raise ValueError( f"{start_name} must be less than {end_name}. Got {start} and {end}.") - validation_tuples = [('activity_type', ACTIVITY_TYPES), + validation_tuples = [('activity_type', ACTIVITY_TYPES), + ('exposed_coffee_break_option', COFFEE_OPTIONS_INT), + ('infected_coffee_break_option', COFFEE_OPTIONS_INT), ('mechanical_ventilation_type', MECHANICAL_VENTILATION_TYPES), ('mask_type', MASK_TYPES), - ('mask_wearing', MASK_WEARING), + ('mask_wearing_option', MASK_WEARING_OPTIONS), ('ventilation_type', VENTILATION_TYPES), ('volume_type', VOLUME_TYPES), - ('windows_open', WINDOWS_OPEN), + ('window_opening_regime', WINDOWS_OPENING_REGIMES), ('window_type', WINDOWS_TYPES)] for attr_name, valid_set in validation_tuples: if getattr(self, attr_name) not in valid_set: @@ -185,9 +186,9 @@ class FormData: def ventilation(self) -> models._VentilationBase: always_on = models.PeriodicInterval(period=120, duration=120) - # Initializes a ventilation instance as a window if 'natural' is selected, or as a HEPA-filter otherwise - if self.ventilation_type == 'natural': - if self.windows_open == 'interval': + # Initializes a ventilation instance as a window if 'natural_ventilation' is selected, or as a HEPA-filter otherwise + if self.ventilation_type == 'natural_ventilation': + if self.window_opening_regime == 'windows_open_periodically': window_interval = models.PeriodicInterval(self.windows_frequency, self.windows_duration) else: window_interval = always_on @@ -198,7 +199,7 @@ class FormData: outside_temp = data.GenevaTemperatures[month] ventilation: models.Ventilation - if self.window_type == 'sliding': + if self.window_type == 'window_sliding': ventilation = models.SlidingWindow( active=window_interval, inside_temp=inside_temp, @@ -207,7 +208,7 @@ class FormData: opening_length=self.opening_distance, number_of_windows=self.windows_number, ) - elif self.window_type == 'hinged': + elif self.window_type == 'window_hinged': ventilation = models.HingedWindow( active=window_interval, inside_temp=inside_temp, @@ -218,10 +219,10 @@ class FormData: number_of_windows=self.windows_number, ) - elif self.ventilation_type == "no-ventilation": + elif self.ventilation_type == "no_ventilation": ventilation = models.AirChange(active=always_on, air_exch=0.) else: - if self.mechanical_ventilation_type == 'air_changes': + if self.mechanical_ventilation_type == 'mech_type_air_changes': ventilation = models.AirChange(active=always_on, air_exch=self.air_changes) else: ventilation = models.HVACMechanical( @@ -236,7 +237,7 @@ class FormData: def mask(self) -> models.Mask: # Initializes the mask type if mask wearing is "continuous", otherwise instantiates the mask attribute as # the "No mask"-mask - mask = models.Mask.types[self.mask_type if self.mask_wearing == "continuous" else 'No mask'] + mask = models.Mask.types[self.mask_type if self.mask_wearing_option == "mask_on" else 'No mask'] return mask def infected_population(self) -> models.InfectedPopulation: @@ -337,6 +338,12 @@ class FormData: else: return self.exposed_lunch_break_times() + def exposed_number_of_coffee_breaks(self) -> int: + return COFFEE_OPTIONS_INT[self.exposed_coffee_break_option] + + def infected_number_of_coffee_breaks(self) -> int: + return COFFEE_OPTIONS_INT[self.infected_coffee_break_option] + def _coffee_break_times(self, activity_start, activity_finish, coffee_breaks, coffee_duration, lunch_start, lunch_finish) -> models.BoundarySequence_t: time_before_lunch = lunch_start - activity_start time_after_lunch = activity_finish - lunch_finish @@ -353,22 +360,24 @@ class FormData: return breaks def exposed_coffee_break_times(self) -> models.BoundarySequence_t: - if not self.exposed_coffee_breaks: + exposed_coffee_breaks = self.exposed_number_of_coffee_breaks() + if exposed_coffee_breaks == 0: return () if self.exposed_lunch_option: - breaks = self._coffee_break_times(self.exposed_start, self.exposed_finish, self.exposed_coffee_breaks, self.exposed_coffee_duration, self.exposed_lunch_start, self.exposed_lunch_finish) + breaks = self._coffee_break_times(self.exposed_start, self.exposed_finish, exposed_coffee_breaks, self.exposed_coffee_duration, self.exposed_lunch_start, self.exposed_lunch_finish) else: - breaks = self._compute_breaks_in_interval(self.exposed_start, self.exposed_finish, self.exposed_coffee_breaks, self.exposed_coffee_duration) + breaks = self._compute_breaks_in_interval(self.exposed_start, self.exposed_finish, exposed_coffee_breaks, self.exposed_coffee_duration) return breaks def infected_coffee_break_times(self) -> models.BoundarySequence_t: if self.infected_dont_have_breaks_with_exposed: - if not self.infected_coffee_breaks: + infected_coffee_breaks = self.infected_number_of_coffee_breaks() + if infected_coffee_breaks == 0: return () if self.infected_lunch_option: - breaks = self._coffee_break_times(self.infected_start, self.infected_finish, self.infected_coffee_breaks, self.infected_coffee_duration, self.infected_lunch_start, self.infected_lunch_finish) + breaks = self._coffee_break_times(self.infected_start, self.infected_finish, infected_coffee_breaks, self.infected_coffee_duration, self.infected_lunch_start, self.infected_lunch_finish) else: - breaks = self._compute_breaks_in_interval(self.infected_start, self.infected_finish, self.infected_coffee_breaks, self.infected_coffee_duration) + breaks = self._compute_breaks_in_interval(self.infected_start, self.infected_finish, infected_coffee_breaks, self.infected_coffee_duration) return breaks else: return self.exposed_coffee_break_times() @@ -519,7 +528,7 @@ def expiration_blend(expiration_weights: typing.Dict[models.Expiration, int]) -> def model_from_form(form: FormData) -> models.ExposureModel: # Initializes room with volume either given directly or as product of area and height - if form.volume_type == 'room_volume': + if form.volume_type == 'room_volume_explicit': volume = form.room_volume else: volume = form.floor_area * form.ceiling_height @@ -543,7 +552,7 @@ def baseline_raw_form_data(): 'air_changes': '', 'air_supply': '', 'ceiling_height': '', - 'exposed_coffee_breaks': '4', + 'exposed_coffee_break_option': 'coffee_break_4', 'exposed_coffee_duration': '10', 'exposed_finish': '18:00', 'exposed_lunch_finish': '13:30', @@ -553,7 +562,7 @@ def baseline_raw_form_data(): 'floor_area': '', 'hepa_amount': '250', 'hepa_option': '0', - 'infected_coffee_breaks': '4', + 'infected_coffee_break_option': 'coffee_break_4', 'infected_coffee_duration': '10', 'infected_dont_have_breaks_with_exposed': '1', 'infected_finish': '18:00', @@ -563,7 +572,7 @@ def baseline_raw_form_data(): 'infected_people': '1', 'infected_start': '09:00', 'mask_type': 'Type I', - 'mask_wearing': 'removed', + 'mask_wearing_option': 'mask_off', 'mechanical_ventilation_type': '', 'model_version': 'v1.2.0', 'opening_distance': '0.2', @@ -572,32 +581,34 @@ def baseline_raw_form_data(): 'room_volume': '75', 'simulation_name': 'Test', 'total_people': '10', - 'ventilation_type': 'natural', - 'volume_type': 'room_volume', + 'ventilation_type': 'natural_ventilation', + 'volume_type': 'room_volume_explicit', 'windows_duration': '', 'windows_frequency': '', 'window_height': '2', - 'window_type': 'sliding', + 'window_type': 'window_sliding', 'window_width': '2', 'windows_number': '1', - 'windows_open': 'always' + 'window_opening_regime': 'windows_open_permanently' } ACTIVITY_TYPES = {'office', 'meeting', 'training', 'callcentre', 'library', 'workshop', 'lab', 'gym'} -MECHANICAL_VENTILATION_TYPES = {'air_changes', 'air_supply', 'not-applicable'} +MECHANICAL_VENTILATION_TYPES = {'mech_type_air_changes', 'mech_type_air_supply', 'not-applicable'} MASK_TYPES = {'Type I', 'FFP2'} -MASK_WEARING = {'continuous', 'removed'} -VENTILATION_TYPES = {'natural', 'mechanical', 'no-ventilation'} -VOLUME_TYPES = {'room_volume', 'room_dimensions'} -WINDOWS_OPEN = {'always', 'interval', 'not-applicable'} -WINDOWS_TYPES = {'sliding', 'hinged', 'not-applicable'} +MASK_WEARING_OPTIONS = {'mask_on', 'mask_off'} +VENTILATION_TYPES = {'natural_ventilation', 'mechanical_ventilation', 'no_ventilation'} +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} -def time_string_to_minutes(time: str) -> int: +def time_string_to_minutes(time: str) -> minutes_since_midnight: """ Converts time from string-format to an integer number of minutes after 00:00 :param time: A string of the form "HH:MM" representing a time of day :return: The number of minutes between 'time' and 00:00 """ - return 60 * int(time[:2]) + int(time[3:]) + return minutes_since_midnight(60 * int(time[:2]) + int(time[3:])) diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index 5af72d21..3d39b741 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -142,7 +142,7 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models scenarios = {} # Two special option cases - HEPA and/or FFP2 masks. - FFP2_being_worn = bool(form.mask_wearing == 'continuous' and form.mask_type == 'FFP2') + FFP2_being_worn = bool(form.mask_wearing_option == 'mask_on' and form.mask_type == 'FFP2') if FFP2_being_worn and form.hepa_option: scenarios['Base scenario with HEPA and FFP2 masks'] = form.build_model() elif FFP2_being_worn: @@ -156,20 +156,20 @@ def manufacture_alternative_scenarios(form: FormData) -> typing.Dict[str, models if form.hepa_option: form = dataclasses.replace(form, hepa_option=False) - with_mask = dataclasses.replace(form, mask_wearing='continuous') - without_mask = dataclasses.replace(form, mask_wearing='removed') + with_mask = dataclasses.replace(form, mask_wearing_option='mask_on') + without_mask = dataclasses.replace(form, mask_wearing_option='mask_off') - if form.ventilation_type == 'mechanical': + if form.ventilation_type == 'mechanical_ventilation': scenarios['Mechanical ventilation with Type I masks'] = with_mask.build_model() scenarios['Mechanical ventilation without masks'] = without_mask.build_model() - elif form.ventilation_type == 'natural': + elif form.ventilation_type == 'natural_ventilation': scenarios['Windows open with Type I masks'] = with_mask.build_model() scenarios['Windows open without masks'] = without_mask.build_model() # No matter the ventilation scheme, we include scenarios which don't have any ventilation. - with_mask_no_vent = dataclasses.replace(with_mask, ventilation_type='no-ventilation') - without_mask_or_vent = dataclasses.replace(without_mask, ventilation_type='no-ventilation') + with_mask_no_vent = dataclasses.replace(with_mask, ventilation_type='no_ventilation') + without_mask_or_vent = dataclasses.replace(without_mask, ventilation_type='no_ventilation') scenarios['No ventilation with Type I masks'] = with_mask_no_vent.build_model() scenarios['Neither ventilation nor masks'] = without_mask_or_vent.build_model() diff --git a/cara/apps/calculator/static/js/form.js b/cara/apps/calculator/static/js/form.js index de6814ce..99bdbbd5 100644 --- a/cara/apps/calculator/static/js/form.js +++ b/cara/apps/calculator/static/js/form.js @@ -19,19 +19,19 @@ function removeErrorFor(referenceNode) { /* -------Required fields------- */ function require_fields(obj) { switch ($(obj).attr('id')) { - case "room_type_volume": + case "room_data_volume": require_room_volume(true); require_room_dimensions(false); break; - case "room_type_dimensions": + case "room_data_dimensions": require_room_volume(false); require_room_dimensions(true); break; - case "mechanical": + case "mechanical_ventilation": require_mechanical_ventilation(true); require_natural_ventilation(false); break; - case "natural": + case "natural_ventilation": require_mechanical_ventilation(false); require_natural_ventilation(true); break; @@ -41,18 +41,18 @@ function require_fields(obj) { case "window_hinged": require_window_width(true); break; - case "air_type_changes": + case "mech_type_air_changes": require_air_changes(true); require_air_supply(false); break; - case "air_type_supply": + case "mech_type_air_supply": require_air_changes(false); require_air_supply(true); break; - case "interval": + case "windows_open_periodically": require_venting(true); break; - case "always": + case "windows_open_permanently": require_venting(false); break; case "hepa_yes": @@ -82,10 +82,10 @@ function require_fields(obj) { function unrequire_fields(obj) { switch (obj.id) { - case "mechanical": + case "mechanical_ventilation": require_mechanical_ventilation(false); break; - case "natural": + case "natural_ventilation": require_natural_ventilation(false); break; default: @@ -106,11 +106,11 @@ function require_room_dimensions(option) { } function require_mechanical_ventilation(option) { - $("#air_type_changes").prop('required', option); - $("#air_type_supply").prop('required', option); + $("#mech_type_air_changes").prop('required', option); + $("#mech_type_air_supply").prop('required', option); if (!option) { - removeInvalid("#air_changes"); - removeInvalid("#air_supply"); + require_input_field("#air_changes", option); + require_input_field("#air_supply", option); } } @@ -120,8 +120,13 @@ function require_natural_ventilation(option) { require_input_field("#opening_distance", option); $("#window_sliding").prop('required', option); $("#window_hinged").prop('required', option); - $("#always").prop('required', option); - $("#interval").prop('required', option); + $("#windows_open_permanently").prop('required', option); + $("#windows_open_periodically").prop('required', option); + if (!option) { + require_input_field("#window_width", option); + require_input_field("#windows_duration", option); + require_input_field("#windows_frequency", option); + } } function require_window_width(option) { @@ -172,8 +177,8 @@ function require_lunch(id, option) { } function require_mask(option) { - $("#mask_type1").prop('required', option); - $("#mask_ffp2").prop('required', option); + $("#mask_type_1").prop('required', option); + $("#mask_type_ffp2").prop('required', option); } function require_hepa(option) { @@ -225,8 +230,8 @@ function on_ventilation_type_change() { getChildElement($(this)).hide(); unrequire_fields(this); - // Clear inputs for this newly hidden child element. - getChildElement($(this)).find('input').not('input[type=radio]').val(''); + //Clear invalid inputs for this newly hidden child element + removeInvalid(getChildElement($(this)).find('input').not('input[type=radio]').attr('id')); } }); } @@ -293,14 +298,14 @@ function validate_form(form) { var activityBreaksObj= document.getElementById("activity_breaks"); removeErrorFor(activityBreaksObj); + var lunch_mins = 0; if (document.getElementById(activity+"_lunch_option_yes").checked) { - var lunch_mins = 0; var lunch_start = document.getElementById(activity+"_lunch_start"); var lunch_finish = document.getElementById(activity+"_lunch_finish"); lunch_mins = parseTimeToMins(lunch_finish.value) - parseTimeToMins(lunch_start.value); } - var coffee_breaks = parseInt(document.querySelector('input[name="'+activity+'_coffee_breaks"]:checked').value); + var coffee_breaks = parseInt(document.querySelector('input[name="'+activity+'_coffee_break_option"]:checked').value); var coffee_duration = parseInt(document.getElementById(activity+"_coffee_duration").value); var coffee_mins = coffee_breaks * coffee_duration; diff --git a/cara/apps/calculator/templates/calculator.form.html.j2 b/cara/apps/calculator/templates/calculator.form.html.j2 index b7e0db91..b7ab72f9 100644 --- a/cara/apps/calculator/templates/calculator.form.html.j2 +++ b/cara/apps/calculator/templates/calculator.form.html.j2 @@ -1,6 +1,6 @@ {% extends "layout.html.j2" %} -{% set MODEL_VERSION="v1.3.0" %} +{% set MODEL_VERSION="v1.4.0" %} {% set DEBUG=False %} {% set active_page="calculator/" %} @@ -45,14 +45,14 @@
Ventilation data:
Mechanical ventilation: - {% if form.ventilation_type == "mechanical" %} + {% if form.ventilation_type == "mechanical_ventilation" %} Yes
- {% if form.mechanical_ventilation_type == "air_supply"%} + {% if form.mechanical_ventilation_type == "mech_type_air_supply"%} Air supply flow rate: {{ form.air_supply }} m³ / hour - {% elif form.mechanical_ventilation_type == "air_changes"%} + {% elif form.mechanical_ventilation_type == "mech_type_air_changes"%} Air changes per hour: {{ form.air_changes }} h⁻¹ {% endif %}
Natural ventilation: - {% if form.ventilation_type == "natural"%} + {% if form.ventilation_type == "natural_ventilation"%} Yes
Number of windows: {{ form.windows_number }}
Height of window: {{ form.window_height }} m
Window type: - {% if form.window_type == "hinged" %} + {% if form.window_type == "window_hinged" %} Top- or Bottom-Hung
Width of window: {{ form.window_width }} m
Opening distance: {{ form.opening_distance }} m
Windows open: - {% if form.windows_open == "interval" %} - Periodically for {{ form.windows_duration | readable_minutes}} +
Windows open: + {% if form.window_opening_regime == "windows_open_periodically" %} + Periodically for {{ form.windows_duration | readable_minutes}} every {{ form.windows_frequency | readable_minutes}} - {% elif form.windows_open == "always" %} + {% elif form.window_opening_regime == "windows_open_permanently" %} Permanently {% endif %}
Coffee breaks: {{ form.exposed_coffee_breaks }} - {% if form.exposed_coffee_breaks > 0 %} +
Coffee breaks: {{ form.exposed_number_of_coffee_breaks() }} + {% if form.exposed_number_of_coffee_breaks() > 0 %} each of {{ form.exposed_coffee_duration }} minutes duration
Coffee breaks: {{ form.infected_coffee_breaks }} - {% if form.infected_coffee_breaks > 0 %} +
Coffee breaks: {{ form.infected_number_of_coffee_breaks() }} + {% if form.infected_number_of_coffee_breaks() > 0 %} each of {{ form.infected_coffee_duration }} minutes duration
Mask wearing:
+Mask wearing:
Masks worn at workstations? {{ 'Yes' if form.mask_wearing == "continuous" else 'No' }}
Masks worn at workstations? {{ 'Yes' if form.mask_wearing_option == "mask_on" else 'No' }}
Mask type: {{ form.mask_type }}
Results:
diff --git a/cara/tests/apps/calculator/test_model_generator.py b/cara/tests/apps/calculator/test_model_generator.py index 985a6f38..64ecb0d4 100644 --- a/cara/tests/apps/calculator/test_model_generator.py +++ b/cara/tests/apps/calculator/test_model_generator.py @@ -1,6 +1,7 @@ import pytest from cara.apps.calculator import model_generator +from cara.apps.calculator.model_generator import minutes_since_midnight from cara import models from cara import data import numpy as np @@ -29,7 +30,7 @@ def test_blend_expiration(): assert r == expected -def test_ventilation_slidingwindow(baseline_form): +def test_ventilation_slidingwindow(baseline_form: model_generator.FormData): room = models.Room(75) window = models.SlidingWindow( active=models.PeriodicInterval(period=120, duration=10), @@ -37,11 +38,11 @@ def test_ventilation_slidingwindow(baseline_form): outside_temp=data.GenevaTemperatures['Dec'], window_height=1.6, opening_length=0.6, ) - baseline_form.ventilation_type = 'natural' + baseline_form.ventilation_type = 'natural_ventilation' baseline_form.windows_duration = 10 baseline_form.windows_frequency = 120 - baseline_form.windows_open = 'interval' - baseline_form.window_type = 'sliding' + baseline_form.window_opening_regime = 'windows_open_periodically' + baseline_form.window_type = 'window_sliding' baseline_form.event_month = 'December' baseline_form.window_height = 1.6 baseline_form.opening_distance = 0.6 @@ -51,7 +52,7 @@ def test_ventilation_slidingwindow(baseline_form): [baseline_form.ventilation().air_exchange(room, t) for t in ts]) -def test_ventilation_hingedwindow(baseline_form): +def test_ventilation_hingedwindow(baseline_form: model_generator.FormData): room = models.Room(75) window = models.HingedWindow( active=models.PeriodicInterval(period=120, duration=10), @@ -59,11 +60,11 @@ def test_ventilation_hingedwindow(baseline_form): outside_temp=data.GenevaTemperatures['Dec'], window_height=1.6, window_width=1., opening_length=0.6, ) - baseline_form.ventilation_type = 'natural' + baseline_form.ventilation_type = 'natural_ventilation' baseline_form.windows_duration = 10 baseline_form.windows_frequency = 120 - baseline_form.windows_open = 'interval' - baseline_form.window_type = 'hinged' + baseline_form.window_opening_regime = 'windows_open_periodically' + baseline_form.window_type = 'window_hinged' baseline_form.event_month = 'December' baseline_form.window_height = 1.6 baseline_form.window_width = 1. @@ -74,14 +75,14 @@ def test_ventilation_hingedwindow(baseline_form): [baseline_form.ventilation().air_exchange(room, t) for t in ts]) -def test_ventilation_mechanical(baseline_form): +def test_ventilation_mechanical(baseline_form: model_generator.FormData): room = models.Room(75) mech = models.HVACMechanical( active=models.PeriodicInterval(period=120, duration=120), q_air_mech=500., ) - baseline_form.ventilation_type = 'mechanical' - baseline_form.mechanical_ventilation_type = 'mechanical' + baseline_form.ventilation_type = 'mechanical_ventilation' + baseline_form.mechanical_ventilation_type = 'mech_type_air_supply' baseline_form.air_supply = 500. ts = np.linspace(8, 16, 100) @@ -89,14 +90,14 @@ def test_ventilation_mechanical(baseline_form): [baseline_form.ventilation().air_exchange(room, t) for t in ts]) -def test_ventilation_airchanges(baseline_form): +def test_ventilation_airchanges(baseline_form: model_generator.FormData): room = models.Room(75) airchange = models.AirChange( active=models.PeriodicInterval(period=120, duration=120), air_exch=3., ) - baseline_form.ventilation_type = 'mechanical' - baseline_form.mechanical_ventilation_type = 'air_changes' + baseline_form.ventilation_type = 'mechanical_ventilation' + baseline_form.mechanical_ventilation_type = 'mech_type_air_changes' baseline_form.air_changes = 3. ts = np.linspace(8, 16, 100) @@ -104,7 +105,7 @@ def test_ventilation_airchanges(baseline_form): [baseline_form.ventilation().air_exchange(room, t) for t in ts]) -def test_ventilation_window_hepa(baseline_form): +def test_ventilation_window_hepa(baseline_form: model_generator.FormData): room = models.Room(75) window = models.SlidingWindow( active=models.PeriodicInterval(period=120, duration=10), @@ -118,10 +119,10 @@ def test_ventilation_window_hepa(baseline_form): ) ventilation = models.MultipleVentilation((window,hepa)) - baseline_form.ventilation_type = 'natural' + baseline_form.ventilation_type = 'natural_ventilation' baseline_form.windows_duration = 10 baseline_form.windows_frequency = 120 - baseline_form.windows_open = 'interval' + baseline_form.window_opening_regime = 'windows_open_periodically' baseline_form.event_month = 'December' baseline_form.window_height = 1.6 baseline_form.opening_distance = 0.6 @@ -132,97 +133,101 @@ def test_ventilation_window_hepa(baseline_form): [baseline_form.ventilation().air_exchange(room, t) for t in ts]) -def test_infected_present_intervals(baseline_form): +def present_times(interval: models.Interval) -> models.BoundarySequence_t: + assert isinstance(interval, models.SpecificInterval) + return interval.present_times + + +def test_infected_present_intervals(baseline_form: model_generator.FormData): baseline_form.infected_dont_have_breaks_with_exposed = False baseline_form.exposed_coffee_duration = 15 - baseline_form.exposed_coffee_breaks = 2 - baseline_form.exposed_start = 9 * 60 - baseline_form.exposed_finish = 17 * 60 - baseline_form.exposed_lunch_start = 12 * 60 + 30 - baseline_form.exposed_lunch_finish = 13 * 60 + 30 - baseline_form.infected_start = 10 * 60 - baseline_form.infected_finish = 15 * 60 + baseline_form.exposed_coffee_break_option = 'coffee_break_2' + baseline_form.exposed_start = minutes_since_midnight(9 * 60) + baseline_form.exposed_finish = minutes_since_midnight(17 * 60) + baseline_form.exposed_lunch_start = minutes_since_midnight(12 * 60 + 30) + baseline_form.exposed_lunch_finish = minutes_since_midnight(13 * 60 + 30) + baseline_form.infected_start = minutes_since_midnight(10 * 60) + baseline_form.infected_finish = minutes_since_midnight(15 * 60) correct = ((10, 10+37/60), (10+52/60, 12.5), (13.5, 15.0)) - assert baseline_form.infected_present_interval().present_times == correct + assert present_times(baseline_form.infected_present_interval()) == correct -def test_exposed_present_intervals(baseline_form): +def test_exposed_present_intervals(baseline_form: model_generator.FormData): baseline_form.exposed_coffee_duration = 15 - baseline_form.exposed_coffee_breaks = 2 - baseline_form.exposed_start = 9 * 60 - baseline_form.exposed_finish = 17 * 60 - baseline_form.exposed_lunch_start = 12 * 60 + 30 - baseline_form.exposed_lunch_finish = 13 * 60 + 30 + baseline_form.exposed_coffee_break_option = 'coffee_break_2' + baseline_form.exposed_start = minutes_since_midnight(9 * 60) + baseline_form.exposed_finish = minutes_since_midnight(17 * 60) + baseline_form.exposed_lunch_start = minutes_since_midnight(12 * 60 + 30) + baseline_form.exposed_lunch_finish = minutes_since_midnight(13 * 60 + 30) correct = ((9, 10+37/60), (10+52/60, 12.5), (13.5, 15+7/60), (15+22/60, 17.0)) - assert baseline_form.exposed_present_interval().present_times == correct + assert present_times(baseline_form.exposed_present_interval()) == correct -def test_present_intervals_common_breaks(baseline_form): +def test_present_intervals_common_breaks(baseline_form: model_generator.FormData): baseline_form.infected_dont_have_breaks_with_exposed = False baseline_form.infected_coffee_duration = baseline_form.exposed_coffee_duration = 15 - baseline_form.infected_coffee_breaks = baseline_form.exposed_coffee_breaks = 2 - baseline_form.exposed_lunch_start = baseline_form.infected_lunch_start = 12 * 60 + 30 - baseline_form.exposed_lunch_finish = baseline_form.infected_lunch_finish = 13 * 60 + 30 - baseline_form.exposed_start = 9 * 60 - baseline_form.exposed_finish = 17 * 60 - baseline_form.infected_start = 9 * 60 - baseline_form.infected_finish = 16 * 60 + baseline_form.infected_coffee_break_option = baseline_form.exposed_coffee_break_option = 'coffee_break_2' + baseline_form.exposed_lunch_start = baseline_form.infected_lunch_start = minutes_since_midnight(12 * 60 + 30) + baseline_form.exposed_lunch_finish = baseline_form.infected_lunch_finish = minutes_since_midnight(13 * 60 + 30) + baseline_form.exposed_start = minutes_since_midnight(9 * 60) + baseline_form.exposed_finish = minutes_since_midnight(17 * 60) + baseline_form.infected_start = minutes_since_midnight(9 * 60) + baseline_form.infected_finish = minutes_since_midnight(16 * 60) correct_exposed = ((9, 10+37/60), (10+52/60, 12.5), (13.5, 15+7/60), (15+22/60, 17.0)) correct_infected = ((9, 10+37/60), (10+52/60, 12.5), (13.5, 15+7/60), (15+22/60, 16.0)) - assert baseline_form.exposed_present_interval().present_times == correct_exposed - assert baseline_form.infected_present_interval().present_times == correct_infected + assert present_times(baseline_form.exposed_present_interval()) == correct_exposed + assert present_times(baseline_form.infected_present_interval()) == correct_infected -def test_present_intervals_split_breaks(baseline_form): +def test_present_intervals_split_breaks(baseline_form: model_generator.FormData): baseline_form.infected_dont_have_breaks_with_exposed = True baseline_form.infected_coffee_duration = baseline_form.exposed_coffee_duration = 15 - baseline_form.infected_coffee_breaks = baseline_form.exposed_coffee_breaks = 2 - baseline_form.infected_lunch_start = baseline_form.exposed_lunch_start = 12 * 60 + 30 - baseline_form.infected_lunch_finish = baseline_form.exposed_lunch_finish = 13 * 60 + 30 - baseline_form.exposed_start = 9 * 60 - baseline_form.exposed_finish = 17 * 60 - baseline_form.infected_start = 9 * 60 - baseline_form.infected_finish = 16 * 60 + baseline_form.infected_coffee_break_option = baseline_form.exposed_coffee_break_option = 'coffee_break_2' + baseline_form.infected_lunch_start = baseline_form.exposed_lunch_start = minutes_since_midnight(12 * 60 + 30) + baseline_form.infected_lunch_finish = baseline_form.exposed_lunch_finish = minutes_since_midnight(13 * 60 + 30) + baseline_form.exposed_start = minutes_since_midnight(9 * 60) + baseline_form.exposed_finish = minutes_since_midnight(17 * 60) + baseline_form.infected_start = minutes_since_midnight(9 * 60) + baseline_form.infected_finish = minutes_since_midnight(16 * 60) correct_exposed = ((9, 10+37/60), (10+52/60, 12.5), (13.5, 15+7/60), (15+22/60, 17.0)) correct_infected = ((9, 10+37/60), (10+52/60, 12.5), (13.5, 14+37/60), (14+52/60, 16.0)) - assert baseline_form.exposed_present_interval().present_times == correct_exposed - assert baseline_form.infected_present_interval().present_times == correct_infected + assert present_times(baseline_form.exposed_present_interval()) == correct_exposed + assert present_times(baseline_form.infected_present_interval()) == correct_infected -def test_exposed_present_intervals_starting_with_lunch(baseline_form): - baseline_form.exposed_coffee_breaks = 0 - baseline_form.exposed_start = baseline_form.exposed_lunch_start = 13 * 60 - baseline_form.exposed_finish = 18 * 60 - baseline_form.exposed_lunch_finish = 14 * 60 +def test_exposed_present_intervals_starting_with_lunch(baseline_form: model_generator.FormData): + baseline_form.exposed_coffee_break_option = 'coffee_break_0' + baseline_form.exposed_start = baseline_form.exposed_lunch_start = minutes_since_midnight(13 * 60) + baseline_form.exposed_finish = minutes_since_midnight(18 * 60) + baseline_form.exposed_lunch_finish = minutes_since_midnight(14 * 60) correct = ((14.0, 18.0), ) - assert baseline_form.exposed_present_interval().present_times == correct + assert present_times(baseline_form.exposed_present_interval()) == correct -def test_exposed_present_intervals_ending_with_lunch(baseline_form): - baseline_form.exposed_coffee_breaks = 0 - baseline_form.exposed_start = 11 * 60 - baseline_form.exposed_finish = baseline_form.exposed_lunch_start = 13 * 60 - baseline_form.exposed_lunch_finish = 14 * 60 +def test_exposed_present_intervals_ending_with_lunch(baseline_form: model_generator.FormData): + baseline_form.exposed_coffee_break_option = 'coffee_break_0' + baseline_form.exposed_start = minutes_since_midnight(11 * 60) + baseline_form.exposed_finish = baseline_form.exposed_lunch_start = minutes_since_midnight(13 * 60) + baseline_form.exposed_lunch_finish = minutes_since_midnight(14 * 60) correct = ((11.0, 13.0),) - assert baseline_form.exposed_present_interval().present_times == correct + assert present_times(baseline_form.exposed_present_interval()) == correct -def test_exposed_present_lunch_end_before_beginning(baseline_form): - baseline_form.exposed_coffee_breaks = 0 - baseline_form.exposed_lunch_start = 14 * 60 - baseline_form.exposed_lunch_finish = 13 * 60 +def test_exposed_present_lunch_end_before_beginning(baseline_form: model_generator.FormData): + baseline_form.exposed_coffee_break_option = 'coffee_break_0' + baseline_form.exposed_lunch_start = minutes_since_midnight(14 * 60) + baseline_form.exposed_lunch_finish = minutes_since_midnight(13 * 60) with pytest.raises(ValueError): baseline_form.validate() @pytest.fixture -def coffee_break_between_1045_and_1115(baseline_form): - baseline_form.exposed_coffee_breaks = 1 +def coffee_break_between_1045_and_1115(baseline_form: model_generator.FormData): + baseline_form.exposed_coffee_break_option = 'coffee_break_1' baseline_form.exposed_coffee_duration = 30 - baseline_form.exposed_start = 10 * 60 - baseline_form.exposed_finish = 12 * 60 + baseline_form.exposed_start = minutes_since_midnight(10 * 60) + baseline_form.exposed_finish = minutes_since_midnight(12 * 60) baseline_form.exposed_lunch_option = False - coffee_breaks = baseline_form.exposed_coffee_break_times() assert coffee_breaks == ((10.75 * 60, 11.25 * 60),) return baseline_form @@ -263,9 +268,9 @@ def test_present_only_for_coffee_ends(coffee_break_between_1045_and_1115): assert interval.boundaries() == () -def time2mins(time: str): +def time2mins(time: str) -> minutes_since_midnight: # Convert times like "14:30" to decimal, like 14.5 * 60. - return int(time.split(':')[0]) * 60 + int(time.split(':')[1]) + return minutes_since_midnight(int(time.split(':')[0]) * 60 + int(time.split(':')[1])) def hours2time(hours: float): @@ -280,8 +285,8 @@ def assert_boundaries(interval, boundaries_in_time_string_form): @pytest.fixture -def breaks_every_25_mins_for_20_mins(baseline_form): - baseline_form.exposed_coffee_breaks = 4 +def breaks_every_25_mins_for_20_mins(baseline_form: model_generator.FormData): + baseline_form.exposed_coffee_break_option = 'coffee_break_4' baseline_form.exposed_coffee_duration = 20 baseline_form.exposed_start = time2mins("10:00") baseline_form.exposed_finish = time2mins("14:10") @@ -325,60 +330,60 @@ def test_present_only_during_second_break(breaks_every_25_mins_for_20_mins): assert_boundaries(interval, []) -def test_valid_no_lunch(baseline_form): +def test_valid_no_lunch(baseline_form: model_generator.FormData): # Check that it is valid to have a 0 length lunch if no lunch is selected. baseline_form.exposed_lunch_option = False - baseline_form.exposed_lunch_start = 0 - baseline_form.exposed_lunch_finish = 0 + baseline_form.exposed_lunch_start = minutes_since_midnight(0) + baseline_form.exposed_lunch_finish = minutes_since_midnight(0) assert baseline_form.validate() is None -def test_no_breaks(baseline_form): +def test_no_breaks(baseline_form: model_generator.FormData): # Check that the times are correct in the absence of breaks. baseline_form.infected_dont_have_breaks_with_exposed = False baseline_form.exposed_lunch_option = False - baseline_form.exposed_coffee_breaks = 0 - baseline_form.exposed_start = 9 * 60 - baseline_form.exposed_finish = 17 * 60 - baseline_form.infected_start = 10 * 60 - baseline_form.infected_finish = 15 * 60 + baseline_form.exposed_coffee_break_option = 'coffee_break_0' + baseline_form.exposed_start = minutes_since_midnight(9 * 60) + baseline_form.exposed_finish = minutes_since_midnight(17 * 60) + baseline_form.infected_start = minutes_since_midnight(10 * 60) + baseline_form.infected_finish = minutes_since_midnight(15 * 60) exposed_correct = ((9, 17),) infected_correct = ((10, 15),) - assert baseline_form.exposed_present_interval().present_times == exposed_correct - assert baseline_form.infected_present_interval().present_times == infected_correct + assert present_times(baseline_form.exposed_present_interval()) == exposed_correct + assert present_times(baseline_form.infected_present_interval()) == infected_correct -def test_coffee_lunch_breaks(baseline_form): +def test_coffee_lunch_breaks(baseline_form: model_generator.FormData): baseline_form.exposed_coffee_duration = 30 - baseline_form.exposed_coffee_breaks = 4 - baseline_form.exposed_start = 9 * 60 - baseline_form.exposed_finish = 18 * 60 - baseline_form.exposed_lunch_start = 12 * 60 + 30 - baseline_form.exposed_lunch_finish = 13 * 60 + 30 + baseline_form.exposed_coffee_break_option = 'coffee_break_4' + baseline_form.exposed_start = minutes_since_midnight(9 * 60) + baseline_form.exposed_finish = minutes_since_midnight(18 * 60) + baseline_form.exposed_lunch_start = minutes_since_midnight(12 * 60 + 30) + baseline_form.exposed_lunch_finish = minutes_since_midnight(13 * 60 + 30) correct = ((9, 9+50/60), (10+20/60, 11+10/60), (11+40/60, 12+30/60), (13+30/60, 14+40/60), (15+10/60, 16+20/60), (16+50/60, 18)) - np.testing.assert_allclose(baseline_form.exposed_present_interval().present_times, correct, rtol=1e-14) + np.testing.assert_allclose(present_times(baseline_form.exposed_present_interval()), correct, rtol=1e-14) -def test_coffee_lunch_breaks_unbalance(baseline_form): +def test_coffee_lunch_breaks_unbalance(baseline_form: model_generator.FormData): baseline_form.exposed_coffee_duration = 30 - baseline_form.exposed_coffee_breaks = 2 - baseline_form.exposed_start = 9 * 60 - baseline_form.exposed_finish = 13 * 60 + 30 - baseline_form.exposed_lunch_start = 12 * 60 + 30 - baseline_form.exposed_lunch_finish = 13 * 60 + 30 + baseline_form.exposed_coffee_break_option = 'coffee_break_2' + baseline_form.exposed_start = minutes_since_midnight(9 * 60) + baseline_form.exposed_finish = minutes_since_midnight(13 * 60 + 30) + baseline_form.exposed_lunch_start = minutes_since_midnight(12 * 60 + 30) + baseline_form.exposed_lunch_finish = minutes_since_midnight(13 * 60 + 30) correct = ((9, 9+50/60), (10+20/60, 11+10/60), (11+40/60, 12+30/60)) - np.testing.assert_allclose(baseline_form.exposed_present_interval().present_times, correct, rtol=1e-14) + np.testing.assert_allclose(present_times(baseline_form.exposed_present_interval()), correct, rtol=1e-14) -def test_coffee_breaks(baseline_form): +def test_coffee_breaks(baseline_form: model_generator.FormData): baseline_form.exposed_coffee_duration = 10 - baseline_form.exposed_coffee_breaks = 4 - baseline_form.exposed_start = 9 * 60 - baseline_form.exposed_finish = 10 * 60 + baseline_form.exposed_coffee_break_option = 'coffee_break_4' + baseline_form.exposed_start = minutes_since_midnight(9 * 60) + baseline_form.exposed_finish = minutes_since_midnight(10 * 60) baseline_form.exposed_lunch_option = False correct = ((9, 9+4/60), (9+14/60, 9+18/60), (9+28/60, 9+32/60), (9+42/60, 9+46/60), (9+56/60, 10)) - np.testing.assert_allclose(baseline_form.exposed_present_interval().present_times, correct, rtol=1e-14) + np.testing.assert_allclose(present_times(baseline_form.exposed_present_interval()), correct, rtol=1e-14) def test_key_validation(baseline_form_data):