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 b570f271378a5cac46828fd1764f681bd078b857..9ba17dcf7505fb65baafe1e7d48c4f1b53c20936 100644 --- a/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py +++ b/SAS/TMSS/services/scheduling/lib/constraints/template_constraints_v1.py @@ -80,8 +80,15 @@ def can_run_within_timewindow_with_daily_constraints(scheduling_unit: models.Sch 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) + duration = timedelta(seconds=scheduling_unit.requirements_doc['tasks']['Observation']['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_daily_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_daily_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool: @@ -94,7 +101,7 @@ def can_run_anywhere_within_timewindow_with_daily_constraints(scheduling_unit: m if constraints['daily']['require_day'] or constraints['daily']['require_night'] or constraints['daily']['avoid_twilight']: if (upper_bound - lower_bound).days >= 1: - logger.info("### SchedulingUnitBlueprint id=%s has daily constraints, but bounds span %s" % (scheduling_unit.id, (upper_bound - lower_bound))) + logger.info("SchedulingUnitBlueprint id=%s has daily constraints, but bounds span %s" % (scheduling_unit.id, (upper_bound - lower_bound))) return False if upper_bound < lower_bound: @@ -107,32 +114,32 @@ def can_run_anywhere_within_timewindow_with_daily_constraints(scheduling_unit: m # get day/night times for bounds # 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) + logger.info("SchedulingUnitBlueprint id=%s cannot meet require_day constraint when starting and ending on different days." % scheduling_unit.id) return False timestamps = [lower_bound, upper_bound] sun_events = timestamps_and_stations_to_sun_rise_and_set(timestamps=tuple(timestamps), stations=(station,))[station] 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("### SchedulingUnitBlueprint id=%s does not meet require_day constraint at timestamp=%s" % (scheduling_unit.id, timestamps[i])) + logger.info("SchedulingUnitBlueprint id=%s does not meet require_day constraint at timestamp=%s" % (scheduling_unit.id, timestamps[i])) return False if constraints['daily']['require_night']: if sun_events['night'][0]['start'].date() != sun_events['night'][1]['start'].date(): - logger.info("### SchedulingUnitBlueprint id=%s cannot meet require_night constraint when starting and ending in different nights." % scheduling_unit.id) + logger.info("SchedulingUnitBlueprint id=%s cannot meet require_night constraint when starting and ending in different nights." % scheduling_unit.id) return False for i in range(len(timestamps)): if timestamps[i] < sun_events['night'][i]['start'] or timestamps[i] > sun_events['night'][i]['end']: - logger.info("### SchedulingUnitBlueprint id=%s does not meet require_night constraint at timestamp=%s" % (scheduling_unit.id, timestamps[i])) + logger.info("SchedulingUnitBlueprint id=%s does not meet require_night constraint at timestamp=%s" % (scheduling_unit.id, timestamps[i])) return False if constraints['daily']['avoid_twilight']: # Note: the same index for sun_events everywhere is not a typo, but to make sure it's the _same_ night or day for both bounds or obs will span over twilight - if not (timestamps[0] > sun_events['day'][0]['start'] and timestamps[0] < sun_events['day'][0]['end'] and - timestamps[1] > sun_events['day'][0]['start'] and timestamps[1] < sun_events['day'][0]['end']) or \ - (timestamps[0] > sun_events['night'][0]['start'] and timestamps[0] < sun_events['night'][0]['end'] and - timestamps[1] > sun_events['night'][0]['start'] and timestamps[1] < sun_events['night'][0]['end']): - logger.info("### SchedulingUnitBlueprint id=%s does not meet avoid_twilight constraint." % scheduling_unit.id) + if not ((timestamps[0] > sun_events['day'][0]['start'] and timestamps[0] < sun_events['day'][0]['end'] and + timestamps[1] > sun_events['day'][0]['start'] and timestamps[1] < sun_events['day'][0]['end']) or + (timestamps[0] > sun_events['night'][0]['start'] and timestamps[0] < sun_events['night'][0]['end'] and + timestamps[1] > sun_events['night'][0]['start'] and timestamps[1] < sun_events['night'][0]['end'])): + logger.info("SchedulingUnitBlueprint id=%s does not meet avoid_twilight constraint." % scheduling_unit.id) return False return True @@ -166,8 +173,14 @@ def can_run_within_timewindow_with_sky_constraints(scheduling_unit: models.Sched 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 sky 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_sky_constraints(scheduling_unit, lower_bound, upper_bound) + duration = timedelta(seconds=scheduling_unit.requirements_doc['tasks']['Observation']['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_sky_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_sky_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool: @@ -176,7 +189,6 @@ def can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit: mod :return: True if all sky constraints are met over the entire time window, else False. """ '''evaluate the time contraint(s)''' - logger.info('### can_run_within_timewindow_with_sky_constraints called with lower_bound=%s upper_bound=%s '% (lower_bound, upper_bound)) constraints = scheduling_unit.draft.scheduling_constraints_doc # TODO: TMSS-245 TMSS-250 (and more?), evaluate the constraints in constraints['sky'] # maybe even split this method into sub methods for the very distinct sky constraints: min_calibrator_elevation, min_target_elevation, transit_offset & min_distance diff --git a/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py b/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py index 54ee94f8b4fd8898bcd508bce97d2b1341611e41..8ecbd54e824b54b8461e399b52d6b2f290b0e13d 100755 --- a/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/services/scheduling/test/t_dynamic_scheduling.py @@ -66,7 +66,7 @@ import lofar.sas.tmss.services.scheduling.constraints.template_constraints_v1 as from lofar.sas.tmss.services.scheduling.dynamic_scheduling import * -@unittest.skip('Disabled until scheduler can deal with failing constraints. (Currently causes infinite loop.)') +#@unittest.skip('Disabled until scheduler can deal with failing constraints. (Currently causes infinite loop.)') class TestDynamicScheduling(unittest.TestCase): ''' Tests for the Dynamic Scheduling @@ -389,7 +389,6 @@ class TestDailyConstraints(unittest.TestCase): self.assertEqual(returned_time, self.sunrise_data['CS001']['day'][1]['start']) def test_can_run_anywhere_within_timewindow_with_daily_constraints_with_daytime_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']['require_day'] = True self.scheduling_unit_blueprint.save() @@ -399,7 +398,6 @@ class TestDailyConstraints(unittest.TestCase): self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) def test_can_run_anywhere_within_timewindow_with_daily_constraints_with_daytime_constraint_returns_false_when_not_daytime(self): - self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {} # remove sky constraint self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['require_day'] = True self.scheduling_unit_blueprint.save() @@ -409,19 +407,36 @@ class TestDailyConstraints(unittest.TestCase): self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) def test_can_run_anywhere_within_timewindow_with_daily_constraints_with_daytime_constraint_returns_false_when_partially_not_daytime(self): - self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {} # remove sky constraint 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_late_night_late_night - lower_bound = datetime(2020, 1, 1, 10, 0, 0) + lower_bound = datetime(2020, 1, 1, 14, 0, 0) upper_bound = datetime(2020, 1, 1, 18, 0, 0) self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) - self.sunrise_mock.return_value = self.sunrise_data_early_night_late_night + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night lower_bound = datetime(2020, 1, 1, 8, 0, 0) - upper_bound = datetime(2020, 1, 1, 10, 0, 0) + upper_bound = datetime(2020, 1, 1, 12, 0, 0) self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) + + def test_can_run_within_timewindow_with_daytime_constraint_returns_correct_value(self): + # todo: for time ranges across dates, consider removing the mock for this because the moving window cannot be easily mocked + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {} # remove sky constraint + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['require_day'] = True + self.scheduling_unit_blueprint.save() + + # can run in day + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night + lower_bound = datetime(2020, 1, 1, 8, 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)) + + # cannot run at night + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night + lower_bound = datetime(2020, 1, 1, 15, 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)) # require_night @@ -466,7 +481,6 @@ class TestDailyConstraints(unittest.TestCase): self.assertEqual(returned_time, self.sunrise_data_early_night['CS001']['night'][1]['start']) def test_can_run_anywhere_within_timewindow_with_daily_constraints_with_nighttime_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']['require_night'] = True self.scheduling_unit_blueprint.save() @@ -489,7 +503,6 @@ class TestDailyConstraints(unittest.TestCase): self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) def test_can_run_anywhere_within_timewindow_with_daily_constraints_with_nighttime_constraint_returns_false_when_not_nighttime(self): - self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {} # remove sky constraint self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['require_night'] = True self.scheduling_unit_blueprint.save() @@ -499,7 +512,6 @@ class TestDailyConstraints(unittest.TestCase): self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) def test_can_run_anywhere_within_timewindow_with_daily_constraints_with_nighttime_constraint_returns_false_when_partially_not_nighttime(self): - self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {} # remove sky constraint self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['require_night'] = True self.scheduling_unit_blueprint.save() @@ -538,6 +550,25 @@ class TestDailyConstraints(unittest.TestCase): lower_bound = datetime(2020, 1, 1, 3, 0, 0) upper_bound = datetime(2020, 1, 1, 23, 0, 0) self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) + + def test_can_run_within_timewindow_with_nighttime_constraint_returns_correct_value(self): + # todo: for time ranges across dates, consider removing the mock for this because the moving window cannot be easily mocked + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky']['min_distance'] = {} # remove sky constraint + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['require_night'] = True + self.scheduling_unit_blueprint.save() + + # cannot run in day + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night + lower_bound = datetime(2020, 1, 1, 8, 0, 0) + upper_bound = datetime(2020, 1, 1, 15, 0, 0) + self.assertFalse(can_run_within_timewindow(self.scheduling_unit_blueprint, lower_bound, upper_bound)) + + # can run at night + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night + lower_bound = datetime(2020, 1, 1, 15, 0, 0) + upper_bound = datetime(2020, 1, 1, 23, 0, 0) + self.assertTrue(can_run_within_timewindow(self.scheduling_unit_blueprint, lower_bound, upper_bound)) + # avoid_twilight @@ -613,8 +644,6 @@ class TestDailyConstraints(unittest.TestCase): self.assertEqual(returned_time, self.sunrise_data['CS001']['day'][0]['start']) def test_can_run_anywhere_within_timewindow_with_daily_constraints_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() @@ -624,19 +653,21 @@ class TestDailyConstraints(unittest.TestCase): self.assertTrue(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) def test_can_run_anywhere_within_timewindow_with_daily_constraints_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) + lower_bound = datetime(2020, 1, 1, 8, 0, 0) + upper_bound = datetime(2020, 1, 1, 9, 0, 0) + self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) + + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night + lower_bound = datetime(2020, 1, 1, 16, 0, 0) + upper_bound = datetime(2020, 1, 1, 17, 0, 0) self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) def test_can_run_anywhere_within_timewindow_with_daily_constraints_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['sky']['min_distance'] = {} # remove sky constraint self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['daily']['avoid_twilight'] = True self.scheduling_unit_blueprint.save() @@ -645,11 +676,29 @@ class TestDailyConstraints(unittest.TestCase): upper_bound = datetime(2020, 1, 1, 18, 0, 0) self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) - self.sunrise_mock.return_value = self.sunrise_data_early_night_late_night + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night lower_bound = datetime(2020, 1, 1, 8, 0, 0) upper_bound = datetime(2020, 1, 1, 10, 0, 0) self.assertFalse(tc1.can_run_anywhere_within_timewindow_with_daily_constraints(self.scheduling_unit_blueprint, lower_bound, upper_bound)) + def test_can_run_within_timewindow_with_twilight_constraint_returns_correct_value(self): + # todo: for time ranges across dates, consider removing the mock for this because the moving window cannot be easily mocked + 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() + + # can run in day + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night + lower_bound = datetime(2020, 1, 1, 8, 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)) + + # can run at night + self.sunrise_mock.return_value = self.sunrise_data_late_night_late_night + lower_bound = datetime(2020, 1, 1, 15, 0, 0) + upper_bound = datetime(2020, 1, 1, 23, 0, 0) + self.assertTrue(can_run_within_timewindow(self.scheduling_unit_blueprint, lower_bound, upper_bound)) + class TestSkyConstraints(unittest.TestCase): '''