diff --git a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py index cbcd8186e18f46cb36f51e6d87759782a31a4fa4..f0ce1ced09b016db31e41924bc17a171c49404a0 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py @@ -417,27 +417,24 @@ class Scheduler: last_on_sky_stop_time = observed_or_beyond_scheduling_units.aggregate(Max('on_sky_stop_time'))['on_sky_stop_time__max'] lower_bound_start_time = max(lower_bound_start_time, last_on_sky_stop_time + DEFAULT_NEXT_STARTTIME_GAP) - # upper bound of search window is at least a week later, or up unit latest cycle end time - upper_bound_stop_time = max(lower_bound_start_time + timedelta(days=7), - max([su.latest_possible_cycle_start_time for su in candidate_units])) + # upper bound of search window is at least a 24h later, or up unit latest cycle end time + upper_bound_stop_time = max(lower_bound_start_time + timedelta(days=1), + max([su.latest_possible_cycle_stop_time for su in candidate_units])) - while lower_bound_start_time < upper_bound_stop_time: + window_lower_bound_start_time = lower_bound_start_time + while window_lower_bound_start_time < upper_bound_stop_time: self._raise_if_triggered() # interrupts the scheduling loop for a next round - if not candidate_units: - logger.info("schedule_next_scheduling_unit: no more candidate units...") - break - try: # no need to irritate user in log files with sub-second scheduling precision - lower_bound_start_time = round_to_second_precision(lower_bound_start_time) + window_lower_bound_start_time = round_to_second_precision(window_lower_bound_start_time) # our sliding window only looks 12 hours ahead - window_upper_bound_stop_time = round_to_second_precision(lower_bound_start_time + timedelta(hours=12)) + window_upper_bound_stop_time = round_to_second_precision(window_lower_bound_start_time + timedelta(hours=12)) # try to find the best next scheduling_unit logger.info("schedule_next_scheduling_unit: searching for best scheduling unit to schedule in window ['%s', '%s']", lower_bound_start_time, window_upper_bound_stop_time) best_scored_scheduling_unit = self.find_best_next_schedulable_unit(candidate_units, - lower_bound_start_time, + window_lower_bound_start_time, window_upper_bound_stop_time) if best_scored_scheduling_unit: @@ -446,7 +443,7 @@ class Scheduler: best_start_time = best_scored_scheduling_unit.start_time # make sure we don't start earlier than allowed - assert best_start_time >= lower_bound_start_time, "The computed start_time='%s' should be larger than the search_window's lower_bound='%s', but it's not."%(best_start_time, lower_bound_start_time) + assert best_start_time >= window_lower_bound_start_time, "The computed start_time='%s' should be larger than the search_window's lower_bound='%s', but it's not."%(best_start_time, lower_bound_start_time) # make start_time "look nice" for us humans best_start_time = round_to_second_precision(best_start_time) @@ -455,7 +452,7 @@ class Scheduler: return self.try_schedule_unit(best_scored_scheduling_unit.scheduling_unit, best_scored_scheduling_unit.start_time) else: - logger.info("schedule_next_scheduling_unit: no scheduling unit found which could be scheduled in window ['%s', '%s']", lower_bound_start_time, window_upper_bound_stop_time) + logger.info("schedule_next_scheduling_unit: no scheduling unit found which could be scheduled in window ['%s', '%s']", window_lower_bound_start_time, window_upper_bound_stop_time) except SubtaskSchedulingException as e: logger.error("Could not schedule scheduling_unit id=%s name='%s'. Error: %s", best_scheduling_unit.id, best_scheduling_unit.name, e) @@ -475,24 +472,32 @@ class Scheduler: # yep, can run later, so mark it as schedulable again, and let it be handled in a new scheduler-round mark_independent_subtasks_in_scheduling_unit_blueprint_as_schedulable(best_scheduling_unit) - # nothing was found, or an error occurred. - # it may be that in the mean time some scheduling_units are not (dynamically) schedulable anymore, filter those out. - for su in candidate_units: - determine_unschedulable_reason_and_mark_unschedulable_if_needed(su, lower_bound_start_time, upper_bound_stop_time, self.search_gridder, raise_if_interruped=self._raise_if_triggered) - # all units are refreshed and either schedulable or unschedulable. # refresh list of schedulable_units to be considered in next round (only schedulable) + for su in candidate_units: + su.refresh_from_db() candidate_units = [su for su in candidate_units if su.status.value==models.SchedulingUnitStatus.Choices.SCHEDULABLE.value] + if not candidate_units: + logger.info("schedule_next_scheduling_unit: no more candidate units...") + break + # advance the window - min_earliest_possible_start_time = get_min_earliest_possible_start_time(candidate_units, lower_bound_start_time+timedelta(hours=1), lower_bound_start_time+timedelta(hours=25), gridder=self.search_gridder, raise_if_interruped=self._raise_if_triggered) + min_earliest_possible_start_time = get_min_earliest_possible_start_time(candidate_units, window_lower_bound_start_time+timedelta(hours=1), window_lower_bound_start_time+timedelta(hours=25), gridder=self.search_gridder, raise_if_interruped=self._raise_if_triggered) if min_earliest_possible_start_time is None: - lower_bound_start_time += timedelta(hours=6) + window_lower_bound_start_time += timedelta(hours=6) else: - lower_bound_start_time = max(min_earliest_possible_start_time, lower_bound_start_time + timedelta(hours=1)) + window_lower_bound_start_time = max(min_earliest_possible_start_time, window_lower_bound_start_time + timedelta(hours=1)) # search again... (while loop) with the remaining schedulable_units and new lower_bound_start_time + # nothing was found, or an error occurred. + # loop over all (remaining) schedulable units, and make them unschedulable for the big search window (if they are unschedulable) + # so they are ignored next time. + # It's up to the user/operator to tweak their constraints which makes them schedulable again, for a next try. + for su in get_dynamically_schedulable_scheduling_units(): + determine_unschedulable_reason_and_mark_unschedulable_if_needed(su, lower_bound_start_time, upper_bound_stop_time, self.search_gridder, raise_if_interruped=self._raise_if_triggered) + return None def try_schedule_unit(self, scheduling_unit: models.SchedulingUnitBlueprint, start_time: datetime) -> models.SchedulingUnitBlueprint: @@ -660,6 +665,10 @@ class Scheduler: logger.log(log_level, "Schedule:") for unit in units_in_schedule: try: + if unit.status.value==models.SchedulingUnitStatus.Choices.SCHEDULABLE.value and not unit.placed: + # skip non-placed schedulable units + continue + task_center_time, transit_time, offset_to_transit, lowest_elevation, elevation_at_center, elevation_at_transit = get_timestamps_elevations_and_offset_to_transit(unit, unit.scheduled_start_time) logger.log(log_level, 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 645d169208974ad0a0b047fe34d9fb17e91dd641..2445157e0b84332c3b126e4b7458b91ad9966444 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -74,10 +74,16 @@ class BaseDynamicSchedulingTestCase(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() + cls.cycle = models.Cycle.objects.create(**Cycle_test_data(start=datetime.utcnow()-timedelta(days=1), + stop=datetime.utcnow() + timedelta(days=7))) + # make some re-usable projects with high/low priority cls.project_low = models.Project.objects.create(**Project_test_data("dynsched_project_%s rank=%s" % (uuid.uuid4(), models.ProjectRank.LOWEST.value,), rank=models.ProjectRank.LOWEST.value)) cls.project_medium = models.Project.objects.create(**Project_test_data("dynsched_project_%s rank=%s" % (uuid.uuid4(), models.ProjectRank.DEFAULT.value,), rank=models.ProjectRank.DEFAULT.value)) cls.project_high = models.Project.objects.create(**Project_test_data("dynsched_project_%s rank=%s"% (uuid.uuid4(), models.ProjectRank.HIGHEST.value,), rank=models.ProjectRank.HIGHEST.value)) + cls.cycle.projects.set([cls.project_low, cls.project_medium, cls.project_high]) + cls.cycle.save() + cls.scheduling_set_low = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=cls.project_low)) cls.scheduling_set_medium = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=cls.project_medium)) cls.scheduling_set_high = models.SchedulingSet.objects.create(**SchedulingSet_test_data(project=cls.project_high)) @@ -1796,11 +1802,11 @@ class TestDynamicScheduling(BaseDynamicSchedulingTestCase): for expected_reason_start, expected_specific_reason_part, constraints in ( ("constraint time.at='2029-12-31 00:00:00' falls outside of cycle bounds ['2030-01-01 00:00:00', '2030-01-07 00:00:00']", '', {'scheduler': 'fixed_time', 'time': {'at': (cycle.start - timedelta(hours=24)).isoformat()}}), ("constraint time.at='2029-12-31 00:00:00' falls outside of cycle bounds ['2030-01-01 00:00:00', '2030-01-07 00:00:00']", '', {'scheduler': 'dynamic', 'time': {'at': (cycle.start - timedelta(hours=24)).isoformat()}}), - ("time constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", '', {'scheduler': 'dynamic', 'time': {'before': (cycle.start - timedelta(hours=24)).isoformat()}}), - ("time constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", '', {'scheduler': 'dynamic', 'time': {'after': (cycle.stop + timedelta(hours=24)).isoformat()}}), - ("sky min_elevation constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", "task_name='Observation' station=CS002 target='_target_name_' elevation=", {'scheduler': 'dynamic', 'sky': {'min_elevation': {'target': 1.57}}}), + ("time constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-07 00:00:00", '', {'scheduler': 'dynamic', 'time': {'before': (cycle.start - timedelta(hours=24)).isoformat()}}), + ("time constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-07 00:00:00", '', {'scheduler': 'dynamic', 'time': {'after': (cycle.stop + timedelta(hours=24)).isoformat()}}), + ("sky min_elevation constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-07 00:00:00", "task_name='Observation' station=CS002 target='_target_name_' elevation=", {'scheduler': 'dynamic', 'sky': {'min_elevation': {'target': 1.57}}}), ("sky min_elevation constraint is not met at 2030-01-01 00:00:00", "task_name='Observation' station=CS002 target='_target_name_' elevation=", {'scheduler': 'fixed_time', 'sky': {'min_elevation': {'target': 1.57}}, 'time': {'at': '2030-01-01T00:00:00Z'}}), - ("sky min_distance constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-08 00:00:00", 'to body=sun < min_distance=149.9', {'scheduler': 'dynamic', 'sky': {'min_distance': {'sun': 150 * 3.1415/180.0}}}), + ("sky min_distance constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-07 00:00:00", 'to body=sun < min_distance=149.9', {'scheduler': 'dynamic', 'sky': {'min_distance': {'sun': 150 * 3.1415/180.0}}}), ("sky min_distance constraint is not met at 2030-01-01 00:00:00", 'to body=sun < min_distance=149.9', {'scheduler': 'fixed_time', 'sky': {'min_distance': {'sun': 150 * 3.1415/180.0}}, 'time': {'at': '2030-01-01T00:00:00Z'}}), ("sky transit_offset constraint is not met anywhere between 2030-01-01 00:00:00 and 2030-01-01 07:00:00", '', {'scheduler': 'dynamic', 'sky': {'transit_offset': {'from': -900, 'to': 900}}, 'time': {'between': [{'from': '2030-01-01T00:00:00Z', 'to': '2030-01-01T07:00:00Z'}]}}), ("sky transit_offset constraint is not met at 2030-01-01 00:00:00", '', {'scheduler': 'fixed_time', 'sky': {'transit_offset': {'from': -900, 'to': 900}}, 'time': {'at': '2030-01-01T00:00:00Z'}}), @@ -1810,6 +1816,9 @@ class TestDynamicScheduling(BaseDynamicSchedulingTestCase): ("daily constraint is not met at 2030-01-01 11:00:00", '', {'scheduler': 'fixed_time', 'daily': {'require_night': True }, 'time': {'at': '2030-01-01T11:00:00Z'}}), ("daily constraint is not met anywhere between 2030-01-01 07:00:00 and 2030-01-01 09:00:00", '', {'scheduler': 'dynamic', 'daily': {'avoid_twilight': True }, 'time': {'between': [{'from': '2030-01-01T07:00:00Z', 'to': '2030-01-01T09:00:00Z'}]}}), ("daily constraint is not met at 2030-01-01 08:00:00", '', {'scheduler': 'fixed_time', 'daily': {'avoid_twilight': True}, 'time': {'at': '2030-01-01T08:00:00Z'}}) ): + + logger.info('---------------------------------------------------------------------\nchecking for expected reason: %s', expected_reason_start) + # reset unit... mark_independent_subtasks_in_scheduling_unit_blueprint_as_schedulable(scheduling_unit_blueprint) self.assertEqual('', scheduling_unit_blueprint.unschedulable_reason) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py index 626be79c34a3d8fcc1948f36619a731dfe9b3c0f..f69678410561a5e8a2295d333cecce3114aa8f76 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/specification.py @@ -813,7 +813,7 @@ class Project(ProjectPropertyMixin, NamedCommonPK): max_cycle_stop = self.cycles.all().aggregate(Max('stop'))['stop__max'] if max_cycle_stop: return max_cycle_stop - return datetime.datetime.utcnow().replace(year=datetime.datetime.today().year+1, + return datetime.datetime.utcnow().replace(day=datetime.datetime.today().day+7, hour=0, minute=0, second=0, microsecond=0) class ProjectQuota(Model): @@ -960,7 +960,7 @@ class SchedulingUnitCommonPropertiesMixin: @cached_property def latest_possible_cycle_start_time(self) -> datetime.datetime: '''return the latest possible start time for this unit's project and cycle(s)''' - return self.latest_possible_cycle_stop_time - self.specified_main_observation_duration + return self.latest_possible_cycle_stop_time - self.specified_observation_duration @cached_property def latest_possible_cycle_stop_time(self) -> datetime.datetime: