Beef up the interval testing for model generator. Improve the validation of the FormData.

This commit is contained in:
Phil Elson 2020-12-14 15:10:50 +01:00
parent 4715005b77
commit 9faf7c6134
2 changed files with 147 additions and 52 deletions

View file

@ -53,6 +53,8 @@ class FormData:
@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']
for name in valid_na_values:
@ -63,26 +65,6 @@ class FormData:
if not form_data.get(name, ''):
form_data[name] = '00:00'
validation_tuples = [('activity_type', ACTIVITY_TYPES),
('event_type', EVENT_TYPES),
('mechanical_ventilation_type', MECHANICAL_VENTILATION_TYPES),
('mask_type', MASK_TYPES),
('mask_wearing', MASK_WEARING),
('ventilation_type', VENTILATION_TYPES),
('volume_type', VOLUME_TYPES),
('windows_open', WINDOWS_OPEN),
('window_type', WINDOWS_TYPES)]
for key, valid_set in validation_tuples:
if key not in form_data:
raise ValueError(f"Missing key {key}")
if form_data[key] not in valid_set:
raise ValueError(f"{form_data[key]} is not a valid value for {key}")
if (form_data['ventilation_type'] == 'natural' and
form_data['window_type'] == 'not-applicable'):
raise ValueError("window_type cannot be ''not-applicable'' if "
"ventilation_type is ''natural''")
# Don't let arbitrary unescaped HTML through the net.
for key, value in form_data.items():
if isinstance(value, str):
@ -93,9 +75,22 @@ class FormData:
if value == "":
form_data[key] = "0"
return cls(
activity_finish=time_string_to_minutes(form_data['activity_finish']),
activity_start=time_string_to_minutes(form_data['activity_start']),
time_attributes = [
'activity_start', 'activity_finish', 'lunch_start',
'lunch_finish', 'infected_start', 'infected_finish',
]
for attr_name in time_attributes:
form_data[attr_name] = time_string_to_minutes(form_data[attr_name])
boolean_attributes = [
'hepa_option', 'lunch_option',
]
for attr_name in boolean_attributes:
form_data[attr_name] = form_data[attr_name] == '1'
instance = cls(
activity_finish=form_data['activity_finish'],
activity_start=form_data['activity_start'],
activity_type=form_data['activity_type'],
air_changes=float(form_data['air_changes']),
air_supply=float(form_data['air_supply']),
@ -105,11 +100,11 @@ class FormData:
event_type=form_data['event_type'],
floor_area=float(form_data['floor_area']),
hepa_amount=float(form_data['hepa_amount']),
hepa_option=(form_data['hepa_option'] == '1'),
hepa_option=form_data['hepa_option'],
infected_people=int(form_data['infected_people']),
lunch_finish=time_string_to_minutes(form_data['lunch_finish']),
lunch_option=(form_data['lunch_option'] == '1'),
lunch_start=time_string_to_minutes(form_data['lunch_start']),
lunch_finish=form_data['lunch_finish'],
lunch_option=form_data['lunch_option'],
lunch_start=form_data['lunch_start'],
mask_type=form_data['mask_type'],
mask_wearing=form_data['mask_wearing'],
mechanical_ventilation_type=form_data['mechanical_ventilation_type'],
@ -130,9 +125,44 @@ class FormData:
window_width=float(form_data['window_width']),
windows_number=int(form_data['windows_number']),
windows_open=form_data['windows_open'],
infected_start=time_string_to_minutes(form_data['infected_start']),
infected_finish=time_string_to_minutes(form_data['infected_finish']),
infected_start=form_data['infected_start'],
infected_finish=form_data['infected_finish'],
)
instance.validate()
return instance
def validate(self):
time_intervals = [
['activity_start', 'activity_finish'],
['lunch_start', 'lunch_finish'],
['infected_start', 'infected_finish'],
]
for start_name, end_name in time_intervals:
start = getattr(self, start_name)
end = getattr(self, end_name)
if start > end:
raise ValueError(
f"{start_name} must be less than {end_name}. Got {start} and {end}.")
validation_tuples = [('activity_type', ACTIVITY_TYPES),
('event_type', EVENT_TYPES),
('mechanical_ventilation_type', MECHANICAL_VENTILATION_TYPES),
('mask_type', MASK_TYPES),
('mask_wearing', MASK_WEARING),
('ventilation_type', VENTILATION_TYPES),
('volume_type', VOLUME_TYPES),
('windows_open', WINDOWS_OPEN),
('window_type', WINDOWS_TYPES)]
for attr_name, valid_set in validation_tuples:
if getattr(self, attr_name) not in valid_set:
raise ValueError(f"{getattr(self, attr_name)} is not a valid value for {attr_name}")
if (
self.ventilation_type == 'natural'
and self.window_type == 'not-applicable'
):
raise ValueError("window_type cannot be 'not-applicable' if "
"ventilation_type is 'natural'")
def build_model(self) -> models.ExposureModel:
return model_from_form(self)
@ -320,30 +350,36 @@ class FormData:
present_intervals = []
time = start
is_present = True
while time < finish:
if is_present:
if not leave_times:
# There are no further leave times, so the final interval
# is from the current time to the end.
present_intervals.append((time / 60, finish / 60))
break
if leave_times[-1] < time:
leave_times.pop()
next_leave_time = leave_times.pop()
if next_leave_time > finish:
next_leave_time = finish
if next_leave_time < time:
# If there is an interval which has been interrupted by the
# end time, we cut it off (e.g. finish happens in the middle
# of a coffee/lunch break).
pass
elif time == next_leave_time:
# If the start time and the end time are the same, then
# ignore this interval.
pass
else:
new_time = leave_times.pop()
if time / 60 != min(new_time, finish) / 60 :
present_intervals.append((time / 60, min(new_time, finish) / 60))
is_present = False
time = new_time
present_intervals.append((time / 60, next_leave_time / 60))
time = next_leave_time
is_present = False
else:
if not enter_times:
break
if enter_times[-1] < time:
enter_times.pop()
else:
is_present = True
time = enter_times.pop()
is_present = True
time = enter_times.pop()
return models.SpecificInterval(tuple(present_intervals))

View file

@ -16,9 +16,8 @@ def baseline_form(baseline_form_data):
def test_model_from_dict(baseline_form_data):
model = model_generator.FormData.from_dict(baseline_form_data)
# TODO:
# assert model.ventilation == cara.models.Ventilation()
form = model_generator.FormData.from_dict(baseline_form_data)
assert isinstance(form.build_model(), models.ExposureModel)
def test_blend_expiration():
@ -156,21 +155,81 @@ def test_exposed_present_intervals(baseline_form):
baseline_form.activity_finish = 17 * 60
baseline_form.lunch_start = 12 * 60 + 30
baseline_form.lunch_finish = 13 * 60 + 30
baseline_form.infected_start = 10 * 60
baseline_form.infected_finish = 15 * 60
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
def test_exposed_present_intervals_starting_with_lunch(baseline_form):
baseline_form.coffee_breaks = 0
baseline_form.activity_start = baseline_form.lunch_start = 13 * 60
baseline_form.activity_finish = 18 * 60
baseline_form.lunch_finish = 14 * 60
baseline_form.infected_start = 9 * 60
baseline_form.infected_finish = 13 * 60
correct = ((14.0, 18.0),)
assert baseline_form.exposed_present_interval().present_times == correct
def test_exposed_present_intervals_ending_with_lunch(baseline_form):
baseline_form.coffee_breaks = 0
baseline_form.activity_start = 11 * 60
baseline_form.activity_finish = baseline_form.lunch_start = 13 * 60
baseline_form.lunch_finish = 14 * 60
correct = ((11.0, 13.0),)
assert baseline_form.exposed_present_interval().present_times == correct
def test_exposed_present_lunch_end_before_beginning(baseline_form):
baseline_form.coffee_breaks = 0
baseline_form.lunch_start = 14 * 60
baseline_form.lunch_finish = 13 * 60
with pytest.raises(ValueError):
baseline_form.validate()
@pytest.fixture
def coffee_break_between_1045_and_1115(baseline_form):
baseline_form.coffee_breaks = 1
baseline_form.coffee_duration = 30
baseline_form.activity_start = 10 * 60
baseline_form.activity_finish = 12 * 60
baseline_form.lunch_option = False
coffee_breaks = baseline_form.coffee_break_times()
assert coffee_breaks == ((10.75 * 60, 11.25 * 60),)
return baseline_form
def test_present_before_coffee(coffee_break_between_1045_and_1115):
interval = coffee_break_between_1045_and_1115.present_interval(10.5 * 60, 11 * 60)
assert interval.boundaries() == ((10.5, 10.75),)
def test_present_after_coffee(coffee_break_between_1045_and_1115):
interval = coffee_break_between_1045_and_1115.present_interval(11 * 60, 11.5 * 60)
assert interval.boundaries() == ((11.25, 11.5),)
def test_present_when_coffee_starts(coffee_break_between_1045_and_1115):
interval = coffee_break_between_1045_and_1115.present_interval(10.75 * 60, 11.5 * 60)
assert interval.boundaries() == ((11.25, 11.5),)
def test_present_when_coffee_ends(coffee_break_between_1045_and_1115):
interval = coffee_break_between_1045_and_1115.present_interval(10.5 * 60, 11.25 * 60)
assert interval.boundaries() == ((10.5, 10.75), )
def test_present_only_for_coffee_ends(coffee_break_between_1045_and_1115):
interval = coffee_break_between_1045_and_1115.present_interval(10.75 * 60, 11.25 * 60)
assert interval.boundaries() == ()
def test_no_lunch(baseline_form):
baseline_form.lunch_option = False
baseline_form.lunch_start = 0
baseline_form.lunch_finish = 0
baseline_form.validate()
def test_coffee_lunch_breaks(baseline_form):
baseline_form.coffee_duration = 30
baseline_form.coffee_breaks = 4
@ -190,7 +249,7 @@ def test_coffee_lunch_breaks_unbalance(baseline_form):
baseline_form.activity_finish = 13 * 60 + 30
baseline_form.lunch_start = 12 * 60 + 30
baseline_form.lunch_finish = 13 * 60 + 30
correct = ((9, 9+50/60), (10+20/60, 11+10/60), (11+40/60, 12+30/60) )
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)