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