From 18c899b9255621f3ae1d66d9b3bb4252b5775bb0 Mon Sep 17 00:00:00 2001 From: Roy de Goei <goei@astron.nl> Date: Thu, 25 Feb 2021 21:17:20 +0100 Subject: [PATCH] TMSS-501 Add check on max_nr_missing per station group at can_run_within_station_reservations --- .../scheduling/lib/constraints/__init__.py | 21 ++- .../scheduling/test/t_dynamic_scheduling.py | 166 ++++++++++++++---- .../src/tmss/tmssapp/models/specification.py | 13 +- 3 files changed, 167 insertions(+), 33 deletions(-) diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints/__init__.py b/SAS/TMSS/backend/services/scheduling/lib/constraints/__init__.py index 0794eb96490..047c1de459d 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints/__init__.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints/__init__.py @@ -241,6 +241,8 @@ def get_min_earliest_possible_start_time(scheduling_units: [models.SchedulingUni def get_active_station_reservations_in_timewindow(lower_bound, upper_bound): """ Retrieve a list of all active stations reservations, which are reserved between a timewindow + TODO: use filter like filter(start_time__lte=upper) filter(stop_time__gte=lower) + BUT can not use filter of property, so find another 'fast' solution (no loop) and move this part to other module """ lst_active_station_reservations = [] reservations = models.Reservation.objects.all() @@ -270,8 +272,23 @@ def can_run_within_station_reservations(scheduling_unit: models.SchedulingUnitBl # Check if the reserved stations are going to be used common_set_stations = set(lst_stations_to_be_used).intersection(lst_reserved_stations) if len(common_set_stations) > 0: - can_run = False - logger.warning("There is/are station(s) reserved (%s) which overlap with timewindow [%s - %s]", + logger.warning("There is/are station(s) reserved %s which overlap with timewindow [%s - %s]", common_set_stations, sub_start_time, sub_stop_time) + # Check which stations are in overlap/common per station group. If more than max_nr_missing stations + # are in overlap then can_run is actually false, otherwise it is still within policy and ok + station_groups = scheduling_unit.station_groups + for sg in station_groups: + nbr_missing = len(set(sg["stations"]) & set(common_set_stations)) + if "max_nr_missing" in sg: + max_nr_missing = sg["max_nr_missing"] + else: + max_nr_missing = 0 + if nbr_missing > max_nr_missing: + logger.info("There are more stations in reservation than the specification is given " + "(%d is larger than %d). The stations that are in conflict are '%s'." + "Can not run scheduling_unit id=%d " % + (nbr_missing, max_nr_missing, common_set_stations, scheduling_unit.pk)) + can_run = False + break return can_run 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 3a442c1b0ea..9525e9abba9 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -790,7 +790,8 @@ class TestReservedStations(unittest.TestCase): 6. | | @.....* can run """ - def create_station_reservation(self, additional_name, lst_stations, start_time=datetime(2100, 1, 1, 0, 0, 0), duration=86400): + @staticmethod + def create_station_reservation(additional_name, lst_stations, start_time=datetime(2100, 1, 1, 0, 0, 0), duration=86400): """ Create a station reservation with given list of stations, start_time and duration (optional) Default duration is 24 hours (defined in seconds) @@ -816,11 +817,9 @@ class TestReservedStations(unittest.TestCase): obs_duration=self.obs_duration) self.scheduling_unit_blueprint = create_task_blueprints_and_subtasks_from_scheduling_unit_draft( scheduling_unit_draft) - # Create reservations in far future to start with - self.reservation_one = self.create_station_reservation("One", ["CS001"]) - self.reservation_two = self.create_station_reservation("Two", ["CS001", "CS002"]) - self.reservation_two_no_overlap = self.create_station_reservation("Two-NoOverlap", ["CS002", "CS003"]) - self.reservation_two_no_duration = self.create_station_reservation("Two-NoDuration", ["CS001", "CS002"], duration=None) + # wipe all reservations in between tests, so the tests don't influence each other + for reservation in models.Reservation.objects.all(): + reservation.delete() def set_1_reservation_start_time_gt_sub_start_time_and_stop_time_gt_sub_stop_time(self, station_reservation): """ @@ -876,34 +875,49 @@ class TestReservedStations(unittest.TestCase): station_reservation.duration = (reservation_stop_time - station_reservation.start_time).total_seconds() station_reservation.save() + def update_station_groups_of_scheduling_unit_blueprint(self): + """ + Use the UC1 strategy template to 'easily' extend the station group of the scheduling_unit + For info, it will have three station groups + - dutch station with max_nr_missing=4 + - international with max_nr_missing=2 + - international required with max_nr_missing=1 + """ + uc1_strategy_template = models.SchedulingUnitObservingStrategyTemplate.objects.get(name="UC1 CTC+pipelines") + scheduling_unit_spec = add_defaults_to_json_object_for_schema(uc1_strategy_template.template, + uc1_strategy_template.scheduling_unit_template.schema) + station_groups = scheduling_unit_spec['tasks']['Target Observation']['specifications_doc']['station_groups'] + self.scheduling_unit_blueprint.requirements_doc['tasks']['Observation']['specifications_doc']['station_groups'] = station_groups + def test_one_station_reserved(self): """ Test station reservation when 1 station (CS001) is reserved and station CS001 is used in scheduling_unit with different reservation start and stop times """ + reservation_one = self.create_station_reservation("One", ["CS001"]) # reservation start_time > SUB start_time and reservation stop_time > SUB stop_time - self.set_1_reservation_start_time_gt_sub_start_time_and_stop_time_gt_sub_stop_time(self.reservation_one) + self.set_1_reservation_start_time_gt_sub_start_time_and_stop_time_gt_sub_stop_time(reservation_one) self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time < SUB start_time and stop_time < SUB stop_time - self.set_2_reservation_start_time_lt_sub_start_time_and_stop_time_lt_sub_stop_time(self.reservation_one) + self.set_2_reservation_start_time_lt_sub_start_time_and_stop_time_lt_sub_stop_time(reservation_one) self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time > SUB start_time and stop_time < SUB stop_time - self.set_3_reservation_start_time_gt_sub_start_time_and_stop_time_lt_sub_stop_time(self.reservation_one) + self.set_3_reservation_start_time_gt_sub_start_time_and_stop_time_lt_sub_stop_time(reservation_one) self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time < SUB start_time and stop_time > SUB stop_time - self.set_4_reservation_start_time_lt_sub_start_time_and_stop_time_gt_sub_stop_time(self.reservation_one) + self.set_4_reservation_start_time_lt_sub_start_time_and_stop_time_gt_sub_stop_time(reservation_one) self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # Reservations outside boundary # start_time and stop_time < SUB start_time - self.set_5_reservation_start_time_and_stop_time_lt_sub_start_time(self.reservation_one) + self.set_5_reservation_start_time_and_stop_time_lt_sub_start_time(reservation_one) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # start_time and stop_time > SUB stop_time - self.set_6_reservation_start_time_and_stop_time_gt_sub_stop_time(self.reservation_one) + self.set_6_reservation_start_time_and_stop_time_gt_sub_stop_time(reservation_one) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) def test_two_stations_reserved(self): @@ -911,29 +925,30 @@ class TestReservedStations(unittest.TestCase): Test station reservation when 2 station (CS001,CS002) are reserved and station CS001 is used in scheduling_unit with different reservation start and stop times """ + reservation_two = self.create_station_reservation("Two", ["CS001", "CS002"]) # reservation start_time > SUB start_time and reservation stop_time > SUB stop_time - self.set_1_reservation_start_time_gt_sub_start_time_and_stop_time_gt_sub_stop_time(self.reservation_two) + self.set_1_reservation_start_time_gt_sub_start_time_and_stop_time_gt_sub_stop_time(reservation_two) self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time < SUB start_time and stop_time < SUB stop_time - self.set_2_reservation_start_time_lt_sub_start_time_and_stop_time_lt_sub_stop_time(self.reservation_two) + self.set_2_reservation_start_time_lt_sub_start_time_and_stop_time_lt_sub_stop_time(reservation_two) self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time > SUB start_time and stop_time < SUB stop_time - self.set_3_reservation_start_time_gt_sub_start_time_and_stop_time_lt_sub_stop_time(self.reservation_two) + self.set_3_reservation_start_time_gt_sub_start_time_and_stop_time_lt_sub_stop_time(reservation_two) self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time < SUB start_time and stop_time > SUB stop_time - self.set_4_reservation_start_time_lt_sub_start_time_and_stop_time_gt_sub_stop_time(self.reservation_two) + self.set_4_reservation_start_time_lt_sub_start_time_and_stop_time_gt_sub_stop_time(reservation_two) self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # Reservations outside boundary # start_time and stop_time < SUB start_time - self.set_5_reservation_start_time_and_stop_time_lt_sub_start_time(self.reservation_two) + self.set_5_reservation_start_time_and_stop_time_lt_sub_start_time(reservation_two) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # start_time and stop_time > SUB stop_time - self.set_6_reservation_start_time_and_stop_time_gt_sub_stop_time(self.reservation_two) + self.set_6_reservation_start_time_and_stop_time_gt_sub_stop_time(reservation_two) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) def test_two_stations_reserved_but_not_used(self): @@ -942,29 +957,30 @@ class TestReservedStations(unittest.TestCase): with different reservation start and stop times All possibilities should result in 'can run' """ + reservation_two_no_overlap = self.create_station_reservation("Two-NoOverlap", ["CS002", "CS003"]) # reservation start_time > SUB start_time and reservation stop_time > SUB stop_time - self.set_1_reservation_start_time_gt_sub_start_time_and_stop_time_gt_sub_stop_time(self.reservation_two_no_overlap) + self.set_1_reservation_start_time_gt_sub_start_time_and_stop_time_gt_sub_stop_time(reservation_two_no_overlap) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time < SUB start_time and stop_time < SUB stop_time - self.set_2_reservation_start_time_lt_sub_start_time_and_stop_time_lt_sub_stop_time(self.reservation_two_no_overlap) + self.set_2_reservation_start_time_lt_sub_start_time_and_stop_time_lt_sub_stop_time(reservation_two_no_overlap) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time > SUB start_time and stop_time < SUB stop_time - self.set_3_reservation_start_time_gt_sub_start_time_and_stop_time_lt_sub_stop_time(self.reservation_two_no_overlap) + self.set_3_reservation_start_time_gt_sub_start_time_and_stop_time_lt_sub_stop_time(reservation_two_no_overlap) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time < SUB start_time and stop_time > SUB stop_time - self.set_4_reservation_start_time_lt_sub_start_time_and_stop_time_gt_sub_stop_time(self.reservation_two_no_overlap) + self.set_4_reservation_start_time_lt_sub_start_time_and_stop_time_gt_sub_stop_time(reservation_two_no_overlap) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # Reservations outside boundary # start_time and stop_time < SUB start_time - self.set_5_reservation_start_time_and_stop_time_lt_sub_start_time(self.reservation_two_no_overlap) + self.set_5_reservation_start_time_and_stop_time_lt_sub_start_time(reservation_two_no_overlap) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # start_time and stop_time > SUB stop_time - self.set_6_reservation_start_time_and_stop_time_gt_sub_stop_time(self.reservation_two_no_overlap) + self.set_6_reservation_start_time_and_stop_time_gt_sub_stop_time(reservation_two_no_overlap) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) def test_two_stations_reserved_with_duration_null(self): @@ -974,19 +990,109 @@ class TestReservedStations(unittest.TestCase): Test with different reservation start time and NO stop_time start_time after SUB stop_time 'can run' all others 'can NOT run' """ + reservation_two_no_duration = self.create_station_reservation("Two-NoDuration", ["CS001", "CS002"], duration=None) # reservation start_time > SUB start_time and < SUB stop_time - self.reservation_two_no_duration.start_time = self.scheduling_unit_blueprint.start_time + timedelta(minutes=5) - self.reservation_two_no_duration.save() + reservation_two_no_duration.start_time = self.scheduling_unit_blueprint.start_time + timedelta(minutes=5) + reservation_two_no_duration.save() self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time < SUB start_time (and < SUB stop_time of course) - self.reservation_two_no_duration.start_time = self.scheduling_unit_blueprint.start_time - timedelta(minutes=5) - self.reservation_two_no_duration.save() + reservation_two_no_duration.start_time = self.scheduling_unit_blueprint.start_time - timedelta(minutes=5) + reservation_two_no_duration.save() self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) # reservation start_time > SUB stop time - self.reservation_two_no_duration.start_time = self.scheduling_unit_blueprint.stop_time + timedelta(minutes=5) - self.reservation_two_no_duration.save() + reservation_two_no_duration.start_time = self.scheduling_unit_blueprint.stop_time + timedelta(minutes=5) + reservation_two_no_duration.save() + self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) + + def test_dutch_stations_conflicts_result_false(self): + """ + Test conflict of 'Dutch' station which have a default of max_nr_missing=4, + Create stations reservation equal to max_nr_missing+1 and check that it can not run + """ + self.update_station_groups_of_scheduling_unit_blueprint() + # Create a reservation within scheduling_unit + self.create_station_reservation("Dutch", ['CS001', 'CS002', 'CS003', 'CS401', 'CS501'], + start_time=self.scheduling_unit_blueprint.start_time) + self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) + + def test_dutch_stations_conflicts_result_true(self): + """ + Test conflict of 'Dutch' station which have a default of max_nr_missing=4, + Create stations reservation equal to max_nr_missing and check that it can run + """ + self.update_station_groups_of_scheduling_unit_blueprint() + # Create a reservation within scheduling_unit + self.create_station_reservation("Dutch", ['CS001', 'CS002', 'CS003', 'CS401'], + start_time=self.scheduling_unit_blueprint.start_time) + self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) + + def test_international_stations_conflicts_result_false(self): + """ + Test conflict of 'International' stations which have a default of max_nr_missing=2, + Create stations reservation equal to max_nr_missing+1 and check that it can not run + """ + self.update_station_groups_of_scheduling_unit_blueprint() + # Create a reservation within scheduling_unit + self.create_station_reservation("International", ['SE607', 'PL610', 'PL612'], + start_time=self.scheduling_unit_blueprint.start_time) + self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) + + def test_international_stations_conflicts_result_true(self): + """ + Test conflict of 'International' stations which are have a default of max_nr_missing=2, + Create stations reservation equal to max_nr_missing and check that it can run + """ + self.update_station_groups_of_scheduling_unit_blueprint() + # Create a reservation within scheduling_unit + self.create_station_reservation("International", ['SE607', 'PL610'], + start_time=self.scheduling_unit_blueprint.start_time) + self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) + + def test_international_required_stations_conflicts_result_false(self): + """ + Test conflict of 'International Required' stations which are have a default of max_nr_missing=1, + Create stations reservation equal to max_nr_missing+1 and check that it can not run + """ + self.update_station_groups_of_scheduling_unit_blueprint() + # Create a reservation within scheduling_unit + self.create_station_reservation("International Required", ['DE601', 'DE605'], + start_time=self.scheduling_unit_blueprint.start_time) + self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) + + def test_international_required_stations_conflicts_result_true(self): + """ + Test conflict of 'International Required' stations which are have a default of max_nr_missing=1, + Create stations reservation equal to max_nr_missing and check that it can run + """ + self.update_station_groups_of_scheduling_unit_blueprint() + # Create a reservation within scheduling_unit + self.create_station_reservation("International Required", ['DE605'], + start_time=self.scheduling_unit_blueprint.start_time) + self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) + + def test_mixed_required_stations_conflicts_result_false(self): + """ + Test conflict of 'mixed' stations which are have a default of max_nr_missing, + Create stations reservation equal to max_nr_missing and one station group max_nr_missing+1 + and check that it can not run + """ + self.update_station_groups_of_scheduling_unit_blueprint() + # Create a reservation within scheduling_unit + self.create_station_reservation("Mixed", ['DE605', 'SE607', 'PL610', 'CS001', 'CS002', 'CS003', 'CS401'], + start_time=self.scheduling_unit_blueprint.start_time) + self.assertFalse(can_run_within_station_reservations(self.scheduling_unit_blueprint)) + + def test_mixed_required_stations_conflicts_result_true(self): + """ + Test conflict of 'mixed' stations which are have a default of max_nr_missing, + Create stations reservation equal to max_nr_missing and check that it can run + """ + self.update_station_groups_of_scheduling_unit_blueprint() + # Create a reservation within scheduling_unit + self.create_station_reservation("Mixed", ['DE605', 'PL610', 'CS001', 'CS002', 'CS003', 'CS401'], + start_time=self.scheduling_unit_blueprint.start_time) self.assertTrue(can_run_within_station_reservations(self.scheduling_unit_blueprint)) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py index b2e3d126303..e186889534f 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py @@ -597,7 +597,7 @@ class SchedulingUnitBlueprint(NamedCommon): @property def flat_station_list(self): """ - Get a flat list of stations of the scheduling unit + Get a flat list of stations of the scheduling unit sorted by name """ lst_stations = [] for sublist in self._get_recursively(self.requirements_doc, "stations"): @@ -605,6 +605,17 @@ class SchedulingUnitBlueprint(NamedCommon): lst_stations.append(item) return list(set(lst_stations)) + @property + def station_groups(self): + """ + Get the station groups of the scheduling unit + """ + lst_station_groups = [] + for sublist in self._get_recursively(self.requirements_doc, "station_groups"): + for item in sublist: + lst_station_groups.append(item) + return lst_station_groups + def _get_recursively(self, search_dict, field): """ Takes a dict with nested lists and dicts, and searches all dicts for a key of the field provided. -- GitLab