diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py b/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py index 9de1309082ee8e2d8d9150460480fc8ae1e05fe7..2b68de96458c95d1cd2e068f7916dc9851ff3a45 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py @@ -151,26 +151,76 @@ def can_run_anywhere_within_timewindow_with_daily_constraints(scheduling_unit: m def can_run_within_timewindow_with_time_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool: - '''evaluate the time contraint(s)''' + """ + Checks whether it is possible to run the scheduling unit /somewhere/ in the given time window, + considering the duration of the involved observation. + :return: True if there is at least one possibility to place the scheduling unit in a way that all time + constraints are met over the runtime of the observation, else False. + """ + main_observation_task_name = get_target_observation_task_name_from_requirements_doc(scheduling_unit) + duration = timedelta( + seconds=scheduling_unit.requirements_doc['tasks'][main_observation_task_name]['specifications_doc']['duration']) + window_lower_bound = lower_bound + while window_lower_bound + duration < upper_bound: + window_upper_bound = window_lower_bound + duration + if can_run_anywhere_within_timewindow_with_time_constraints(scheduling_unit, window_lower_bound, window_upper_bound): + return True + window_lower_bound += min(timedelta(hours=1), upper_bound - window_lower_bound) + + return False + + +def can_run_anywhere_within_timewindow_with_time_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool: + """ + Checks whether it is possible to place the scheduling unit arbitrarily in the given time window, + i.e. the time constraints must be met over the full time window. + :return: True if all time constraints are met over the entire time window, else False. + """ + can_run_at = True + can_run_before = True + can_run_with_after = True + can_run_between = True + can_run_not_between = True constraints = scheduling_unit.draft.scheduling_constraints_doc - # TODO: TMSS-244 (and more?), evaluate the constraints in constraints['time'] + + # TODO TMSS-672 Move to can_run_within and make logic correct if has_manual_scheduler_constraint(scheduling_unit): at = parser.parse(constraints['time']['at'], ignoretz=True) - return at >= lower_bound and at+scheduling_unit.duration <= upper_bound # todo: suggestion: use scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['duration'] + can_run_at = (at >= lower_bound and at+scheduling_unit.duration <= upper_bound) # todo: suggestion: use scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['duration'] if 'before' in constraints['time']: before = parser.parse(constraints['time']['before'], ignoretz=True) - return before <= upper_bound-scheduling_unit.duration # todo: suggestion: use scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['duration'] + can_run_before = (before <= upper_bound-scheduling_unit.duration) # todo: suggestion: use scheduling_unit.requirements_doc['tasks']['Observation']['specifications_doc']['duration'] if 'after' in constraints['time']: after = parser.parse(constraints['time']['after'], ignoretz=True) - return lower_bound >= after - - # if 'between' in constraints['time']: - # betweens = [ dateutil.parser.parse(constraints['time']['between']) - # return lower_bound >= after - - return True # for now, ignore time contraints. + can_run_with_after = (lower_bound >= after) + + # Run within one of these time windows + if 'between' in constraints['time']: + can_run_between = True # empty list is no constraint + for between in constraints['time']['between']: + time_from = parser.parse(between["from"], ignoretz=True) + time_to = parser.parse(between["to"], ignoretz=True) + if time_from >= lower_bound and time_to <= upper_bound: + can_run_between = True + break # something inside the boundary so True and don't look any further + else: + can_run_between = False + + # Do NOT run within any of these time windows + if 'not_between' in constraints['time']: + can_run_not_between = True # empty list is no constraint + for not_between in constraints['time']['not_between']: + time_from = parser.parse(not_between["from"], ignoretz=True) + time_to = parser.parse(not_between["to"], ignoretz=True) + if time_from <= upper_bound and time_to >= lower_bound: + can_run_not_between = False + break # something outside the boundary so False and don't look any further + else: + can_run_not_between = True + + return can_run_at & can_run_before & can_run_with_after & can_run_between & can_run_not_between def can_run_within_timewindow_with_sky_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool: diff --git a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py index 9525e9abba9d40fc006ea3c427854b92b4443df0..45efbf8ae32a51b02d26139010d6f5c125fc2334 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -775,6 +775,250 @@ class TestSkyConstraints(unittest.TestCase): self.assertFalse(returned_value) +class TestTimeConstraints(TestCase): + """ + Tests for the time constraint checkers used in dynamic scheduling with different boundaries + Possible time constraints are + - after + - before + - between (one or more 'from-to') + - not between (one or more 'from-to') + """ + + def add_time_between_constraint(self, from_timestamp, to_timestamp): + lst_between_constraints = self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["between"] + time_constraint_dict = {"from": from_timestamp.isoformat(), "to": to_timestamp.isoformat()} + lst_between_constraints.append(time_constraint_dict) + self.scheduling_unit_blueprint.save() + + def add_time_not_between_constraint(self, from_timestamp, to_timestamp): + lst_between_constraints = self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["not_between"] + time_constraint_dict = {"from": from_timestamp.isoformat(), "to": to_timestamp.isoformat()} + lst_between_constraints.append(time_constraint_dict) + self.scheduling_unit_blueprint.save() + + def setUp(self) -> None: + # scheduling unit + self.obs_duration = 120 * 60 + scheduling_set = models.SchedulingSet.objects.create(**SchedulingSet_test_data()) + scheduling_unit_draft = TestDynamicScheduling.create_simple_observation_scheduling_unit( + "scheduling unit for %s" % self._testMethodName, + scheduling_set=scheduling_set, + obs_duration=self.obs_duration) + self.scheduling_unit_blueprint = create_task_blueprints_and_subtasks_from_scheduling_unit_draft(scheduling_unit_draft) + + def test_can_run_anywhere_after_returns_false(self): + # Set datetime constraints after lower_bound + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 13, 0, 0).isoformat() + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), + datetime(2020, 1, 2, 12, 0, 0))) + + # Set datetime constraints to upper_bound + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 2, 12, 0, 0).isoformat() + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), + datetime(2020, 1, 2, 12, 0, 0))) + + def test_can_run_anywhere_after_returns_true(self): + # Set datetime constraints before lower_bound + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 11, 0, 0).isoformat() + self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), + datetime(2020, 1, 2, 12, 0, 0))) + # Set datetime constraints equal to lower_bound + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["after"] = datetime(2020, 1, 1, 12, 0, 0).isoformat() + self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), + datetime(2020, 1, 2, 12, 0, 0))) + + def test_can_run_anywhere_before_returns_false(self): + # Set datetime constraints after upper_bound + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 2, 13, 0, 0).isoformat() + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), + datetime(2020, 1, 2, 12, 0, 0))) + # Set datetime constraints equal to upper_bound + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 2, 12, 0, 0).isoformat() + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), + datetime(2020, 1, 2, 12, 0, 0))) + # Set datetime constraints equal to upper_bound - duration + 1 sec + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = \ + (datetime(2020, 1, 2, 12, 0, 0) - self.scheduling_unit_blueprint.duration + timedelta(seconds=1)).isoformat() + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), + datetime(2020, 1, 2, 12, 0, 0))) + + def test_can_run_anywhere_before_returns_true(self): + # Set datetime constraints far before upper_bound (lower_bound) + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = datetime(2020, 1, 1, 12, 0, 0).isoformat() + self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), + datetime(2020, 1, 2, 12, 0, 0))) + # Set datetime constraints equal to upper_bound - duration + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["before"] = \ + (datetime(2020, 1, 2, 12, 0, 0) - self.scheduling_unit_blueprint.duration).isoformat() + self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), + datetime(2020, 1, 2, 12, 0, 0))) + + def test_can_run_anywhere_between_returns_false(self): + """ + Test 'between' constraint with start/stop datetime constraints 'outside' upper_bound or lower_bound + """ + # Set datetime constraints start > lower_bound and stop > upper_bound + self.add_time_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 2, 15, 0, 0)) + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0))) + # Set datetime constraints start < lower_bound and stop < upper_bound + self.add_time_between_constraint(datetime(2020, 1, 1, 8, 0, 0), datetime(2020, 1, 2, 8, 0, 0)) + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0))) + # Set datetime constraints start > lower_bound and stop > upper_bound (1 second only) + self.add_time_between_constraint(datetime(2020, 1, 1, 12, 0, 1), datetime(2020, 1, 2, 12, 0, 1)) + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0))) + + def test_can_run_anywhere_between_returns_true(self): + """ + Test 'between' constraint with start/stop datetime constraints 'inside' upper_bound and lower_bound + """ + # Set datetime constraints start > lower_bound and stop < upper_bound -duration + self.add_time_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 15, 0, 0)) + self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 20, 0, 0))) + + # Set datetime constraints start = lower_bound and stop = upper_bound - duration + self.add_time_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 15, 0, 0)) + self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 17, 10, 0))) + + def test_can_run_anywhere_not_between_returns_false(self): + """ + Test 'not_between' constraint with start/stop datetime constraints 'inside' upper_bound or lower_bound + """ + # Set datetime constraints start > lower_bound and stop > upper_bound + self.add_time_not_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 2, 15, 0, 0)) + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0))) + + # Set datetime constraints start < lower_bound and stop > lower_bound and < upper_bound + self.add_time_not_between_constraint(datetime(2020, 1, 1, 8, 0, 0), datetime(2020, 1, 2, 8, 0, 0)) + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0))) + + # Set datetime constraints start > lower_bound and stop < upper_bound + self.add_time_not_between_constraint(datetime(2020, 1, 1, 16, 0, 0), datetime(2020, 1, 2, 8, 0, 0)) + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0))) + + # Set datetime constraints start < lower_bound and stop > upper_bound + self.add_time_not_between_constraint(datetime(2020, 1, 1, 8, 0, 0), datetime(2020, 1, 2, 14, 0, 0)) + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0))) + + def test_can_run_anywhere_not_between_returns_true(self): + """ + Test 'not_between' constraint with start/stop datetime constraints 'outside' upper_bound and lower_bound + """ + # Set datetime constraints start < lower_bound and stop < lower_bound + self.add_time_not_between_constraint(datetime(2020, 1, 1, 3, 0, 0), datetime(2020, 1, 1, 11, 0, 0)) + self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 16, 0, 0))) + + # Set datetime constraints start > upper_bound and stop > upper_bound + self.add_time_not_between_constraint(datetime(2020, 1, 1, 16, 0, 0), datetime(2020, 1, 1, 20, 0, 0)) + self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_time_constraints(self.scheduling_unit_blueprint, + datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 15, 0, 0))) + + def execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary(self): + """ + Just a simple wrapper to call 'can_run_anywhere_within_timewindow_with_time_constraints' function + with a 24 hours boundary 2020-01-01 12:00 - 2020-01-02 12:00 + """ + return (tc1.can_run_within_timewindow_with_time_constraints( + self.scheduling_unit_blueprint, datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0))) + + def test_can_run_within_between_constraints(self): + """ + Test multiple 'between' constraints within 24 boundary and check overall result of + 'can_run_within_timewindow_with_time_constraints' + This function will iterate between the boundary with boundary shift of 1hr and boundary length of + the observation duration, which is in this testcase two hour + i.e. 12-14, 13-15, 14-16,..etc.., 9-11 + """ + # no constraints defined so should be OK + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Add constraints of 1hr, we still 'can_run' + self.add_time_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 14, 0, 0)) + self.add_time_between_constraint(datetime(2020, 1, 1, 16, 0, 0), datetime(2020, 1, 1, 17, 0, 0)) + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Add constraints of 2hr, we still 'can_run' + self.add_time_between_constraint(datetime(2020, 1, 2, 11, 0, 0), datetime(2020, 1, 2, 13, 0, 0)) + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Add constraint of 24hr constraint, we still 'can_run' + self.add_time_between_constraint(datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 12, 0, 0)) + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Add constraint of 2hr, to fill the 'last gap', we 'can run' + self.add_time_between_constraint(datetime(2020, 1, 2, 10, 0, 0), datetime(2020, 1, 2, 12, 0, 0)) + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Clear all between constraints + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["between"] = [] + + # Add constraints 'outside' the 24hr, now we 'can not run' + self.add_time_between_constraint(datetime(2020, 1, 2, 13, 0, 0), datetime(2020, 1, 2, 14, 0, 0)) + self.add_time_between_constraint(datetime(2020, 1, 2, 16, 0, 0), datetime(2020, 1, 2, 17, 0, 0)) + self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Add constraint 'outside' the 24hr, we 'still can not run' + self.add_time_between_constraint(datetime(2020, 1, 1, 9, 0, 0), datetime(2020, 1, 1, 12, 0, 0)) + self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # add one 'inside' constraint, 1 hour within block of 2 hour so overall must be ok + self.add_time_between_constraint(datetime(2020, 1, 1, 13, 30, 0), datetime(2020, 1, 1, 14, 30, 0)) + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + def test_can_run_within_not_between_constraints(self): + """ + Test multiple 'not_between' constraints within 24 boundary and check overall result of + 'can_run_within_timewindow_with_time_constraints' + This function will iterate between the boundary with boundary shift of 1hr and boundary length of + the observation duration, which is in this testcase two hour + i.e. 12-14, 13-15, 14-16,..etc.., 9-11 + """ + # no constraints defined so should be OK + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Add constraints of 1hr, we still 'can_run' + self.add_time_not_between_constraint(datetime(2020, 1, 1, 13, 0, 0), datetime(2020, 1, 1, 14, 0, 0)) + self.add_time_not_between_constraint(datetime(2020, 1, 1, 16, 0, 0), datetime(2020, 1, 1, 17, 0, 0)) + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Add constraints of 2hr, we still 'can_run' + self.add_time_not_between_constraint(datetime(2020, 1, 1, 18, 0, 0), datetime(2020, 1, 1, 20, 0, 0)) + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Add constraint of 20hr constraint, we still 'can_run' + self.add_time_not_between_constraint(datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 2, 8, 0, 0)) + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Add constraint of 2hr, to fill the 'last gap', now we can not run + self.add_time_not_between_constraint(datetime(2020, 1, 2, 10, 0, 0), datetime(2020, 1, 2, 12, 0, 0)) + self.assertFalse(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + # Clear all not_between constraints + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['time']["not_between"] = [] + # Add 4 hr constraints within 24 hours boundary, we can run + self.add_time_not_between_constraint(datetime(2020, 1, 1, 12, 0, 0), datetime(2020, 1, 1, 16, 0, 0)) + self.assertTrue(self.execute_can_run_within_timewindow_with_time_constraints_of_24hour_boundary()) + + class TestReservedStations(unittest.TestCase): """ Tests for the reserved stations used in dynamic scheduling