Beef up the interval testing for model generator. Improve the validation of the FormData.
This commit is contained in:
parent
4715005b77
commit
9faf7c6134
2 changed files with 147 additions and 52 deletions
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue