Refine the form attributes & values in readiness for exposing a public perma-URL

This commit is contained in:
Gabriella Azzopardi 2021-02-18 10:33:37 +00:00 committed by Philip James Elson
parent 182294348f
commit 3b620b0be0
6 changed files with 271 additions and 249 deletions

View file

@ -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:]))

View file

@ -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()

View file

@ -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;

View file

@ -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 @@
<div data-tooltip="The area you wish to study (choose one of the 2 options). Use GIS Portal or measure.">
<span class="tooltip_text">?</span>
</div><br>
<input type="radio" id="room_type_volume" name="volume_type" value="room_volume" onclick="require_fields(this)" tabindex="-1" required>
<label for="room_type_volume">Room volume:</label> &nbsp;&nbsp;
<input type="number" step="any" id="room_volume" class="non_zero" name="room_volume" placeholder="Room volume (m³)" min="0" data-has-radio="#room_type_volume"><br>
<input type="radio" id="room_type_dimensions" name="volume_type" value="room_dimensions" onclick="require_fields(this)" tabindex="-1" required>
<label for="room_type_dimensions">Floor area:</label> &nbsp;&nbsp;
<input type="number" step="any" id="floor_area" class="non_zero" name="floor_area" placeholder="Room floor area (m²)" min="0" data-has-radio="#room_type_dimensions"><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <label for="room_type_dimensions">Ceiling height:</label> &nbsp;&nbsp;
<input type="number" step="any" id="ceiling_height" class="non_zero" name="ceiling_height" placeholder="Room ceiling height (m²)" min="0" data-has-radio="#room_type_dimensions"><br>
<input type="radio" id="room_data_volume" name="volume_type" value="room_volume_explicit" onclick="require_fields(this)" tabindex="-1" required>
<label for="room_data_volume">Room volume:</label> &nbsp;&nbsp;
<input type="number" step="any" id="room_volume" class="non_zero" name="room_volume" placeholder="Room volume (m³)" min="0" data-has-radio="#room_data_volume"><br>
<input type="radio" id="room_data_dimensions" name="volume_type" value="room_volume_from_dimensions" onclick="require_fields(this)" tabindex="-1" required>
<label for="room_data_dimensions">Floor area:</label> &nbsp;&nbsp;
<input type="number" step="any" id="floor_area" class="non_zero" name="floor_area" placeholder="Room floor area (m²)" min="0" data-has-radio="#room_data_dimensions"><br>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <label for="room_data_dimensions">Ceiling height:</label> &nbsp;&nbsp;
<input type="number" step="any" id="ceiling_height" class="non_zero" name="ceiling_height" placeholder="Room ceiling height (m²)" min="0" data-has-radio="#room_data_dimensions"><br>
<hr width="80%">
<!-- Ventilation Options -->
@ -61,39 +61,39 @@
<span class="tooltip_text">?</span>
</div><br>
Ventilation type:
<input type="radio" id="no_ventilation" name="ventilation_type" value="no-ventilation" checked>
<input type="radio" id="no_ventilation" name="ventilation_type" value="no_ventilation" checked>
<label for="no_ventilation">No ventilation</label>&nbsp;&nbsp;
<input type="radio" id="mechanical" name="ventilation_type" value="mechanical" data-enables="#DIVmechanical_ventilation">
<label for="mechanical">Mechanical</label>&nbsp;&nbsp;
<input type="radio" id="natural" name="ventilation_type" value="natural" data-enables="#DIVnatural_ventilation">
<label for="natural">Natural</label><br>
<input type="radio" id="mechanical_ventilation" name="ventilation_type" value="mechanical_ventilation" data-enables="#DIVmechanical_ventilation">
<label for="mechanical_ventilation">Mechanical</label>&nbsp;&nbsp;
<input type="radio" id="natural_ventilation" name="ventilation_type" value="natural_ventilation" data-enables="#DIVnatural_ventilation">
<label for="natural_ventilation">Natural</label><br>
<div id="DIVmechanical_ventilation" class="tabbed" style="display:none">
<input type="radio" id="air_type_supply" name="mechanical_ventilation_type" value="air_supply" onclick="require_fields(this)" tabindex="-1">
<label for="air_type_supply">Air supply flow rate</label> &nbsp;&nbsp;
<input type="number" step="any" id="air_supply" class="non_zero" name="air_supply" min="0" placeholder="(m³ / hour)" data-has-radio="#air_type_supply"><br>
<input type="radio" id="air_type_changes" name="mechanical_ventilation_type" value="air_changes" onclick="require_fields(this)" tabindex="-1">
<label for="air_type_changes">Air changes per hour</label> &nbsp;&nbsp;
<input type="number" step="any" id="air_changes" class="non_zero" name="air_changes" min="0" placeholder="(h⁻¹) only fresh air" data-has-radio="#air_type_changes"><br>
<input type="radio" id="mech_type_air_supply" name="mechanical_ventilation_type" value="mech_type_air_supply" onclick="require_fields(this)" tabindex="-1">
<label for="mech_type_air_supply">Air supply flow rate</label> &nbsp;&nbsp;
<input type="number" step="any" id="air_supply" class="non_zero" name="air_supply" min="0" placeholder="(m³ / hour)" data-has-radio="#mech_type_air_supply"><br>
<input type="radio" id="mech_type_air_changes" name="mechanical_ventilation_type" value="mech_type_air_changes" onclick="require_fields(this)" tabindex="-1">
<label for="mech_type_air_changes">Air changes per hour</label> &nbsp;&nbsp;
<input type="number" step="any" id="air_changes" class="non_zero" name="air_changes" min="0" placeholder="(h⁻¹) only fresh air" data-has-radio="#mech_type_air_changes"><br>
</div>
<div id="DIVnatural_ventilation" class="tabbed" style="display:none">
Number of windows: <input type="number" id="windows_number" class="non_zero" name="windows_number" min="1"><br>
Height of window: <input type="number" step="any" id="window_height" class="non_zero" name="window_height" placeholder="meters" min="0"><br>
Window type:
<input type="radio" id="window_sliding" name="window_type" value="sliding" onclick="require_fields(this)" checked="checked">
Window type:
<input type="radio" id="window_sliding" name="window_type" value="window_sliding" onclick="require_fields(this)" checked="checked">
<label for="window_sliding">Sliding / Side-Hung</label>&nbsp;&nbsp;
<input type="radio" id="window_hinged" name="window_type" value="hinged" onclick="require_fields(this)">
<input type="radio" id="window_hinged" name="window_type" value="window_hinged" onclick="require_fields(this)">
<label for="window_hinged">Top- or Bottom-Hung</label>&nbsp;&nbsp;<br>
Width of window: <input type="number" step="any" id="window_width" class="non_zero disabled" name="window_width" placeholder="meters" min="0" data-has-radio="#window_hinged"><br>
Opening distance: <input type="number" step="any" id="opening_distance" class="non_zero" name="opening_distance" placeholder="meters" min="0"><br>
Windows open:</span><br>
<span class="tabbed"><input type="radio" id="always" name="windows_open" value="always" onclick="require_fields(this)" checked="checked"></span>
<label for="always">Permanently</label><br>
<span class="tabbed"><input type="radio" id="interval" name="windows_open" value="interval" onclick="require_fields(this)"></span>
<label for="interval">Periodically:</label>&nbsp;&nbsp;
<input type="number" step="any" id="windows_duration" class="disabled" name="windows_duration" placeholder="Duration (min)" min="1" data-has-radio="#interval"> /
<input type="number" step="any" id="windows_frequency" class="disabled" name="windows_frequency" placeholder="Frequency (min)" min="1" data-has-radio="#interval">
Windows open:</span><br>
<span class="tabbed"><input type="radio" id="windows_open_permanently" name="window_opening_regime" value="windows_open_permanently" onclick="require_fields(this)" checked="checked"></span>
<label for="windows_open_permanently">Permanently</label><br>
<span class="tabbed"><input type="radio" id="windows_open_periodically" name="window_opening_regime" value="windows_open_periodically" onclick="require_fields(this)"></span>
<label for="windows_open_periodically">Periodically:</label>&nbsp;&nbsp;
<input type="number" step="any" id="windows_duration" class="disabled" name="windows_duration" placeholder="Duration (min)" min="1" data-has-radio="#windows_open_periodically"> /
<input type="number" step="any" id="windows_frequency" class="disabled" name="windows_frequency" placeholder="Frequency (min)" min="1" data-has-radio="#windows_open_periodically">
<br>
</div>
@ -110,15 +110,15 @@
<span class="tooltip_text">?</span>
</div><br>
Are masks worn when occupants are at workstations?
<input type="radio" id="mask_on" name="mask_wearing" value="continuous" required>
<input type="radio" id="mask_on" name="mask_wearing_option" value="mask_on" required>
<label for="mask_on">Yes</label>&nbsp;&nbsp;
<input type="radio" id="mask_off" name="mask_wearing" value="removed" required checked="checked">
<input type="radio" id="mask_off" name="mask_wearing_option" value="mask_off" required checked="checked">
<label for="mask_off">No</label><br>
Type of masks used:
<input type="radio" id="mask_type1" name="mask_type" value="Type I" checked="checked" onclick="require_fields(this)">
<label for="mask_type1">Type 1</label>&nbsp;&nbsp;
<input type="radio" id="mask_ffp2" name="mask_type" value="FFP2" onclick="require_fields(this)">
<label for="mask_ffp2">FFP2</label><br>
<input type="radio" id="mask_type_1" name="mask_type" value="Type I" checked="checked" onclick="require_fields(this)">
<label for="mask_type_1">Type 1</label>&nbsp;&nbsp;
<input type="radio" id="mask_type_ffp2" name="mask_type" value="FFP2" onclick="require_fields(this)">
<label for="mask_type_ffp2">FFP2</label><br>
<hr width="80%">
</div>
@ -194,11 +194,11 @@
<!-- Coffee Options -->
Coffee Breaks:&nbsp;&nbsp;
<input type="radio" id="exposed_coffee_break_0" name="exposed_coffee_breaks" value="0" checked="checked">
<input type="radio" id="exposed_coffee_break_0" name="exposed_coffee_break_option" value="coffee_break_0" checked="checked">
<label for="exposed_coffee_break_0" >No breaks</label>&nbsp;&nbsp;
<input type="radio" id="exposed_coffee_break_2" name="exposed_coffee_breaks" value="2">
<input type="radio" id="exposed_coffee_break_2" name="exposed_coffee_break_option" value="coffee_break_2">
<label for="exposed_coffee_break_2">2</label>&nbsp;&nbsp;
<input type="radio" id="exposed_coffee_break_4" name="exposed_coffee_breaks" value="4">
<input type="radio" id="exposed_coffee_break_4" name="exposed_coffee_break_option" value="coffee_break_4">
<label for="exposed_coffee_break_4">4</label><br>
Duration (minutes):
@ -228,11 +228,11 @@
<!-- Coffee Options -->
Coffee Breaks:&nbsp;&nbsp;
<input type="radio" id="infected_coffee_break_0" name="infected_coffee_breaks" value="0" checked="checked">
<input type="radio" id="infected_coffee_break_0" name="infected_coffee_break_option" value="coffee_break_0" checked="checked">
<label for="infected_coffee_break_0" >No breaks</label>&nbsp;&nbsp;
<input type="radio" id="infected_coffee_break_2" name="infected_coffee_breaks" value="2">
<input type="radio" id="infected_coffee_break_2" name="infected_coffee_break_option" value="coffee_break_2">
<label for="infected_coffee_break_2">2</label>&nbsp;&nbsp;
<input type="radio" id="infected_coffee_break_4" name="infected_coffee_breaks" value="4">
<input type="radio" id="infected_coffee_break_4" name="infected_coffee_break_option" value="coffee_break_4">
<label for="infected_coffee_break_4">4</label><br>
Duration (minutes):
@ -247,7 +247,7 @@
</div>
<br>
</div>
<br style="clear:both;">
<i>Coffee breaks are spread evenly throughout the day.</i><br>
<hr width="80%">

View file

@ -41,13 +41,13 @@
<p class="data_title">Ventilation data:</p>
<ul>
<li><p class="data_text">Mechanical ventilation:
{% if form.ventilation_type == "mechanical" %}
{% if form.ventilation_type == "mechanical_ventilation" %}
Yes </p></li>
<ul>
<li><p class="data_subtext">
{% 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 %}
</p></li>
@ -56,24 +56,24 @@
No </li>
{% endif %}
<li><p class="data_text">Natural ventilation:
{% if form.ventilation_type == "natural"%}
{% if form.ventilation_type == "natural_ventilation"%}
Yes </p></li>
<ul>
<li><p class="data_subtext">Number of windows: {{ form.windows_number }}</p></li>
<li><p class="data_subtext">Height of window: {{ form.window_height }} m</p></li>
<li><p class="data_subtext">Window type:
{% if form.window_type == "hinged" %}
{% if form.window_type == "window_hinged" %}
Top- or Bottom-Hung</p></li>
<li><p class="data_subtext">Width of window: {{ form.window_width }} m</p></li>
{% elif form.window_type == "sliding" %}
{% elif form.window_type == "window_sliding" %}
Sliding / Side-Hung</p></li>
{% endif %}
<li><p class="data_subtext">Opening distance: {{ form.opening_distance }} m</p></li>
<li><p class="data_subtext">Windows open:
{% if form.windows_open == "interval" %}
Periodically for {{ form.windows_duration | readable_minutes}}
<li><p class="data_subtext">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 %}
</p></li>
@ -141,8 +141,8 @@
No
{% endif %}
</p></li>
<li><p class="data_text">Coffee breaks: {{ form.exposed_coffee_breaks }}
{% if form.exposed_coffee_breaks > 0 %}
<li><p class="data_text">Coffee breaks: {{ form.exposed_number_of_coffee_breaks() }}
{% if form.exposed_number_of_coffee_breaks() > 0 %}
each of {{ form.exposed_coffee_duration }} minutes duration
</p></li>
<ul>
@ -166,8 +166,8 @@
No
{% endif %}
</p></li>
<li><p class="data_text">Coffee breaks: {{ form.infected_coffee_breaks }}
{% if form.infected_coffee_breaks > 0 %}
<li><p class="data_text">Coffee breaks: {{ form.infected_number_of_coffee_breaks() }}
{% if form.infected_number_of_coffee_breaks() > 0 %}
each of {{ form.infected_coffee_duration }} minutes duration
</p></li>
<ul>
@ -179,12 +179,13 @@
</ul>
{% endif %}
<p class="data_title">Mask wearing:</p>
<p class="data_title">Mask wearing:</p>
<ul>
<li><p class="data_text">Masks worn at workstations? {{ 'Yes' if form.mask_wearing == "continuous" else 'No' }} </p></li>
{% if form.mask_wearing == "continuous" %}
<li><p class="data_text">Masks worn at workstations? {{ 'Yes' if form.mask_wearing_option == "mask_on" else 'No' }} </p></li>
{% if form.mask_wearing_option == "mask_on" %}
<li><p class="data_text">Mask type: {{ form.mask_type }}</p></li>
{% endif %}
</ul>
<p class="result_title">Results:</p>

View file

@ -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):