diff --git a/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py b/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py index 700556966058fb2fb0046e8409b65f5f85b1d131..b9b7bfe1a72bfe00c5a42f21318289e66a3ef418 100644 --- a/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py +++ b/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py @@ -76,6 +76,19 @@ def has_manual_scheduler_constraint(scheduling_unit: models.SchedulingUnitBluepr def can_run_within_timewindow_with_daily_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool: + """ + 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 daily constraints are met over the runtime of the observation, else False. + """ + # todo: use moving window lower_bound to lower_bound + obs duration, return true once window returned true + return can_run_anywhere_within_timewindow_with_daily_constraints(scheduling_unit, lower_bound, upper_bound) + + +def can_run_anywhere_within_timewindow_with_daily_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 daily constraints must be met over the full time window. + :return: True if all daily constraints are met over the entire time window, else False. + """ '''evaluate the daily contraint''' constraints = scheduling_unit.draft.scheduling_constraints_doc if constraints['daily']['require_day'] or constraints['daily']['require_night'] or constraints['daily']['avoid_twilight']: @@ -92,7 +105,7 @@ def can_run_within_timewindow_with_daily_constraints(scheduling_unit: models.Sch # check contraint and return false on first failure for station in stations: # get day/night times for bounds - # we could sample in between bounds, but will instead do some checks + # we could sample in between bounds, but will instead do some checks so that bounds are sufficient if constraints['daily']['require_day'] and lower_bound.date() != upper_bound.date(): logger.info("### SchedulingUnitBlueprint id=%s cannot meet require_day constraint when starting and ending on different days." % scheduling_unit.id) return False @@ -101,11 +114,8 @@ def can_run_within_timewindow_with_daily_constraints(scheduling_unit: models.Sch if constraints['daily']['require_day']: for i in range(len(timestamps)): if timestamps[i] < sun_events['day'][i]['start'] or timestamps[i] > sun_events['day'][i]['end']: - logger.info("### %s not between %s and %s" % (timestamps[i], sun_events['day'][i]['start'], sun_events['day'][i]['end'])) logger.info("### SchedulingUnitBlueprint id=%s does not meet require_day constraint at timestamp=%s" % (scheduling_unit.id, timestamps[i])) return False - else: - logger.info("### %s between %s and %s" % (timestamps[i], sun_events['day'][i]['start'], sun_events['day'][i]['end'] )) if constraints['daily']['require_night']: if sun_events['night'][0]['start'].date() != sun_events['night'][1]['start'].date(): @@ -125,12 +135,23 @@ def can_run_within_timewindow_with_daily_constraints(scheduling_unit: models.Sch logger.info("### SchedulingUnitBlueprint id=%s does not meet avoid_twilight constraint." % scheduling_unit.id) return False - - logger.info('### SchedulingUnitBlueprint id=%s meets all daily constraints. Returning True.') return True def can_run_within_timewindow_with_time_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool: + """ + 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. + """ + # todo: use moving window lower_bound to lower_bound + obs duration, return true once window returned true + return can_run_anywhere_within_timewindow_with_time_constraints(scheduling_unit, lower_bound, upper_bound) + + +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. + """ '''evaluate the time contraint(s)''' constraints = scheduling_unit.draft.scheduling_constraints_doc # TODO: TMSS-244 (and more?), evaluate the constraints in constraints['time'] @@ -200,7 +221,7 @@ def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBluep next_day = sun_events['day'][1] next_night = sun_events['night'][1] if constraints['daily']['require_day']: - # TODO: Do we need to check for observations that are too long and can e.g. only be run in summer? + # TODO: Do we need to check for observations that are too long and can e.g. only be run in summer? -> recursively traverse through days or sth? if lower_bound + duration > day['end']: start_time_per_station[station] = next_day['start'] continue @@ -221,19 +242,20 @@ def get_earliest_possible_start_time(scheduling_unit: models.SchedulingUnitBluep continue if constraints['daily']['avoid_twilight']: - if lower_bound + duration < day['end']: - if lower_bound >= day['start']: - start_time_per_station[station] = lower_bound - continue - start_time_per_station[station] = day['start'] + if lower_bound >= day['start'] and lower_bound + duration < day['end']: + # starts and ends in daytime + start_time_per_station[station] = lower_bound continue - if lower_bound + duration < night['end']: - if lower_bound >= night['start']: - start_time_per_station[station] = lower_bound - continue - start_time_per_station[station] = night['start'] + if lower_bound >= night['start'] and lower_bound + duration < night['end']: + # starts and ends in nighttime + start_time_per_station[station] = lower_bound continue - start_time_per_station[station] = next_day['start'] + if lower_bound < day['start'] and lower_bound + duration >= night['end']: + # ends in morning twilight + start_time_per_station[station] = day['start'] + continue + # ends in evening twilight + start_time_per_station[station] = night['start'] continue return max(start_time_per_station.values()) except Exception as e: diff --git a/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py b/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py index 799f40bfe73c33fdfbf52fede5effd4d2bed92ab..1a391f7b125cff80f7e9581b3359999b30558f8c 100755 --- a/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py @@ -302,7 +302,11 @@ class TestDailyConstraints(unittest.TestCase): 'CS001': {"sunrise": [{"start": datetime(2020, 1, 1, 7, 30, 0), "end": datetime(2020, 1, 1, 9, 30, 0)}, {"start": datetime(2020, 1, 2, 7, 30, 0), "end": datetime(2020, 1, 2, 9, 30, 0)}], "day": [{"start": datetime(2020, 1, 1, 9, 30, 0), "end": datetime(2020, 1, 1, 15, 30, 0)}, {"start": datetime(2020, 1, 2, 9, 30, 0), "end": datetime(2020, 1, 2, 15, 30, 0)}], "sunset": [{"start": datetime(2020, 1, 1, 15, 30, 0), "end": datetime(2020, 1, 1, 17, 30, 0)},{"start": datetime(2020, 1, 2, 15, 30, 0), "end": datetime(2020, 1, 2, 17, 30, 0)}], - "night": [{"start": datetime(2019, 12, 31, 17, 30, 0), "end": datetime(2020, 1, 1, 7, 30, 0)}, {"start": datetime(2020, 1, 1, 17, 30, 0), "end": datetime(2020, 1, 2, 7, 30, 0)}]}} + "night": [{"start": datetime(2019, 12, 31, 17, 30, 0), "end": datetime(2020, 1, 1, 7, 30, 0)}, {"start": datetime(2020, 1, 1, 17, 30, 0), "end": datetime(2020, 1, 2, 7, 30, 0)}]}, + 'DE601': {"sunrise": [{"start": datetime(2020, 1, 1, 7, 45, 0), "end": datetime(2020, 1, 1, 9, 45, 0)}, {"start": datetime(2020, 1, 2, 7, 45, 0), "end": datetime(2020, 1, 2, 9, 45, 0)}], + "day": [{"start": datetime(2020, 1, 1, 9, 45, 0), "end": datetime(2020, 1, 1, 15, 45, 0)}, {"start": datetime(2020, 1, 2, 9, 45, 0), "end": datetime(2020, 1, 2, 15, 45, 0)}], + "sunset": [{"start": datetime(2020, 1, 1, 15, 45, 0), "end": datetime(2020, 1, 1, 17, 45, 0)},{"start": datetime(2020, 1, 2, 15, 45, 0), "end": datetime(2020, 1, 2, 17, 45, 0)}], + "night": [{"start": datetime(2019, 12, 31, 17, 45, 0), "end": datetime(2020, 1, 1, 7, 45, 0)}, {"start": datetime(2020, 1, 1, 17, 45, 0), "end": datetime(2020, 1, 2, 7, 45, 0)}]}} # constraint checker requests lower and upper bound, so we need some variants for various cases @@ -347,6 +351,7 @@ class TestDailyConstraints(unittest.TestCase): def test_get_earliest_possible_start_time_with_daytime_constraint_returns_day_start(self): self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['require_day'] = True self.scheduling_unit_blueprint.save() + self.sunrise_mock.return_value = self.sunrise_data_early_night timestamp = datetime(2020, 1, 1, 4, 0, 0) returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) self.assertEqual(returned_time, self.sunrise_data['CS001']['day'][0]['start']) @@ -355,6 +360,7 @@ class TestDailyConstraints(unittest.TestCase): self.scheduling_unit_blueprint.requirements_doc['tasks']['Observation']['specifications_doc']['stations'] = ['CS001', 'DE601'] self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['require_day'] = True self.scheduling_unit_blueprint.save() + self.sunrise_mock.return_value = self.sunrise_data_early_night timestamp = datetime(2020, 1, 1, 4, 0, 0) returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) self.assertEqual(returned_time, self.sunrise_data['DE601']['day'][0]['start']) @@ -531,7 +537,116 @@ class TestDailyConstraints(unittest.TestCase): upper_bound = datetime(2020, 1, 1, 23, 0, 0) self.assertFalse(can_run_within_timewindow(self.scheduling_unit_blueprint, lower_bound, upper_bound)) - # todo: avoid_twilight checks / TMSS-256 + # avoid_twilight + + def test_get_earliest_possible_start_time_with_twilight_constraint_returns_day_start(self): + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True + self.scheduling_unit_blueprint.save() + + self.sunrise_mock.return_value = self.sunrise_data_early_night + timestamp = datetime(2020, 1, 1, 9, 0, 0) + returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) + self.assertEqual(returned_time, self.sunrise_data['CS001']['day'][0]['start']) + + def test_get_earliest_possible_start_time_with_twilight_constraint_returns_day_start_of_latest_station(self): + self.scheduling_unit_blueprint.requirements_doc['tasks']['Observation']['specifications_doc']['stations'] = ['CS001', 'DE601'] + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True + self.scheduling_unit_blueprint.save() + + self.sunrise_mock.return_value = self.sunrise_data_early_night + timestamp = datetime(2020, 1, 1, 9, 0, 0) + returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) + self.assertEqual(returned_time, self.sunrise_data['DE601']['day'][0]['start']) + + def test_get_earliest_possible_start_time_with_twilight_constraint_returns_night_start(self): + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True + self.scheduling_unit_blueprint.save() + + self.sunrise_mock.return_value = self.sunrise_data + timestamp = datetime(2020, 1, 1, 17, 0, 0) + returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) + self.assertEqual(returned_time, self.sunrise_data['CS001']['night'][0]['start']) + + def test_get_earliest_possible_start_time_with_twilight_constraint_returns_night_start_of_latest_station(self): + self.scheduling_unit_blueprint.requirements_doc['tasks']['Observation']['specifications_doc']['stations'] = ['CS001', 'DE601'] + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True + self.scheduling_unit_blueprint.save() + + self.sunrise_mock.return_value = self.sunrise_data + timestamp = datetime(2020, 1, 1, 17, 0, 0) + returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) + self.assertEqual(returned_time, self.sunrise_data['DE601']['night'][0]['start']) + + def test_get_earliest_possible_start_time_with_twilight_constraint_returns_timestamp(self): + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True + self.scheduling_unit_blueprint.save() + + # daytime + timestamp = datetime(2020, 1, 1, 10, 0, 0) + returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) + self.assertEqual(returned_time, timestamp) + + # late time + timestamp = datetime(2020, 1, 1, 20, 0, 0) + returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) + self.assertEqual(returned_time, timestamp) + + # early night + self.sunrise_mock.return_value = self.sunrise_data_early_night + timestamp = datetime(2020, 1, 1, 3, 0, 0) + returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) + self.assertEqual(returned_time, timestamp) + + def test_get_earliest_possible_start_time_with_twilight_constraint_returns_day_or_night_start_when_obs_does_not_fit(self): + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True + self.scheduling_unit_blueprint.save() + + timestamp = datetime(2020, 1, 1, 15, 0, 0) + returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) + self.assertEqual(returned_time, self.sunrise_data['CS001']['night'][0]['start']) + + self.sunrise_mock.return_value = self.sunrise_data_early_night + timestamp = datetime(2020, 1, 1, 7, 0, 0) + returned_time = get_earliest_possible_start_time(self.scheduling_unit_blueprint, timestamp) + self.assertEqual(returned_time, self.sunrise_data['CS001']['day'][0]['start']) + + def test_can_run_within_timewindow_with_twilight_constraint_returns_true(self): + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'][ + 'min_distance'] = {} # remove sky constraint + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True + self.scheduling_unit_blueprint.save() + + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night + lower_bound = datetime(2020, 1, 1, 10, 0, 0) + upper_bound = datetime(2020, 1, 1, 15, 0, 0) + self.assertTrue(can_run_within_timewindow(self.scheduling_unit_blueprint, lower_bound, upper_bound)) + + def test_can_run_within_timewindow_with_twilight_constraint_returns_false_when_in_twilight(self): + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'][ + 'min_distance'] = {} # remove sky constraint + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True + self.scheduling_unit_blueprint.save() + + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night + lower_bound = datetime(2020, 1, 1, 20, 0, 0) + upper_bound = datetime(2020, 1, 1, 23, 0, 0) + self.assertFalse(can_run_within_timewindow(self.scheduling_unit_blueprint, lower_bound, upper_bound)) + + def test_can_run_within_timewindow_with_twilight_constraint_returns_false_when_partially_in_twilight(self): + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'][ + 'min_distance'] = {} # remove sky constraint + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True + self.scheduling_unit_blueprint.save() + + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night + lower_bound = datetime(2020, 1, 1, 10, 0, 0) + upper_bound = datetime(2020, 1, 1, 18, 0, 0) + self.assertFalse(can_run_within_timewindow(self.scheduling_unit_blueprint, lower_bound, upper_bound)) + + self.sunrise_mock.return_value = self.sunrise_data_early_night_late_night + lower_bound = datetime(2020, 1, 1, 8, 0, 0) + upper_bound = datetime(2020, 1, 1, 10, 0, 0) + self.assertFalse(can_run_within_timewindow(self.scheduling_unit_blueprint, lower_bound, upper_bound)) class TestSkyConstraints(unittest.TestCase):