diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints.py b/SAS/TMSS/backend/services/scheduling/lib/constraints.py index 39539adaedf90a4db9e5af03b7c447815868af65..a8d134e78d5cb425a76a7d87a51e0713f7d2bdd6 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints.py @@ -1929,12 +1929,16 @@ def get_min_earliest_possible_start_time(scheduling_units: [models.SchedulingUni return min_earliest_possible_start_time -def get_missing_stations_for_scheduling_unit(scheduling_unit: models.SchedulingUnitBlueprint) -> []: +def get_missing_stations_for_scheduling_unit(scheduling_unit: models.SchedulingUnitBlueprint, + proposed_start_time: datetime=None) -> []: observation_subtasks = models.Subtask.independent_subtasks().filter(task_blueprint__scheduling_unit_blueprint_id=scheduling_unit.id).filter(specifications_template__type__value=models.SubtaskType.Choices.OBSERVATION.value).all() missing_stations = set() + lower_bound = proposed_start_time or scheduling_unit.scheduled_start_time + upper_bound = proposed_start_time+scheduling_unit.specified_observation_duration if proposed_start_time else scheduling_unit.scheduled_stop_time + for subtask in observation_subtasks: - for station in get_missing_stations(subtask): + for station in get_missing_stations(subtask, lower_bound, upper_bound): missing_stations.add(station) return sorted((list(set(missing_stations)))) @@ -2000,13 +2004,16 @@ def get_blocking_scheduled_units(scheduling_unit: models.SchedulingUnitBlueprint return scheduled_units.filter(id__in=[s.id for s in observation_overlapping_scheduled_units]).all() -def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime, gridder: Gridder=None, raise_if_interruped: Callable=noop) -> models.SchedulingUnitBlueprint: +def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime, proposed_start_time: datetime=None, gridder: Gridder=None, raise_if_interruped: Callable=noop) -> models.SchedulingUnitBlueprint: try: logger.debug("determine_unschedulable_reason_and_mark_unschedulable_if_needed: scheduling_unit id=%s", scheduling_unit.id) - if not can_run_within_station_reservations(scheduling_unit, lower_bound=lower_bound, upper_bound=upper_bound): - missing_stations = get_missing_stations_for_scheduling_unit(scheduling_unit) - msg = "Stations %s are reserved" % (','.join([str(s) for s in missing_stations]), ) - return mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, msg) + if not can_run_within_station_reservations(scheduling_unit, + lower_bound=proposed_start_time or lower_bound, + upper_bound=proposed_start_time+scheduling_unit.specified_observation_duration if proposed_start_time else upper_bound): + missing_stations = get_missing_stations_for_scheduling_unit(scheduling_unit, proposed_start_time=proposed_start_time) + if missing_stations: + msg = "Stations %s are reserved" % (','.join([str(s) for s in missing_stations]), ) + return mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, msg) blocking_units = get_blocking_scheduled_units(scheduling_unit) if blocking_units.exists(): @@ -2045,7 +2052,7 @@ def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_u between_from = parser.parse(between["from"], ignoretz=True) between_to = parser.parse(between["to"], ignoretz=True) if between_from != lower_bound or between_to != upper_bound: - scheduling_unit = determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_unit, between_from, between_to, gridder) + scheduling_unit = determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_unit, between_from, between_to, proposed_start_time=proposed_start_time, gridder=gridder, raise_if_interruped=raise_if_interruped) if scheduling_unit.status.value == models.SchedulingUnitStatus.Choices.UNSCHEDULABLE.value: return scheduling_unit diff --git a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py index 40b92c9fc569b3b3421650a6fd605dc768b56a5a..18d4876f935c95d3145f3d1e31f6922f022922b2 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py @@ -244,6 +244,7 @@ class Scheduler: runnable_start_time = get_earliest_possible_start_time(schedulable_unit, datetime.utcnow(), gridder=self.search_gridder, raise_if_interruped=self._raise_if_triggered) if runnable_start_time is None: unschedulable_unit = determine_unschedulable_reason_and_mark_unschedulable_if_needed(schedulable_unit, at_timestamp, at_timestamp + schedulable_unit.specified_observation_duration, + proposed_start_time=at_timestamp, gridder=self.search_gridder, raise_if_interruped=self._raise_if_triggered) logger.warning("Cannot schedule fixed_time unit [%s/%s] id=%d at '%s': %s", i, len(schedulable_units), unschedulable_unit.id, at_timestamp, unschedulable_unit.unschedulable_reason) continue @@ -259,6 +260,7 @@ class Scheduler: scheduled_units.extend(scheduled_B_units) else: unschedulable_unit = determine_unschedulable_reason_and_mark_unschedulable_if_needed(schedulable_unit, at_timestamp, at_timestamp + schedulable_unit.specified_observation_duration, + proposed_start_time=at_timestamp, gridder=self.search_gridder, raise_if_interruped=self._raise_if_triggered) logger.warning("Could not schedule fixed_time unit [%s/%s] id=%d at '%s': %s", i, len(schedulable_units), unschedulable_unit.id, at_timestamp, unschedulable_unit.unschedulable_reason) @@ -276,6 +278,7 @@ class Scheduler: if scheduled_unit.status.value != models.SchedulingUnitStatus.Choices.SCHEDULED.value: logger.warning("Fixed_time-scheduled scheduling unit id=%d has subsequently been unscheduled again. Marking it unschedulable.", scheduled_unit.id) unschedulable_unit = determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduled_unit, at_timestamp, at_timestamp + scheduled_unit.specified_observation_duration, + proposed_start_time=at_timestamp, gridder=self.search_gridder, raise_if_interruped=self._raise_if_triggered) assert(unschedulable_unit.status.value == models.SchedulingUnitStatus.Choices.UNSCHEDULABLE.value) return [scheduled_unit for scheduled_unit in scheduled_units if scheduled_unit.status.value == models.SchedulingUnitStatus.Choices.SCHEDULED.value] @@ -533,7 +536,9 @@ class Scheduler: # 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) + determine_unschedulable_reason_and_mark_unschedulable_if_needed(su, lower_bound_start_time, upper_bound_stop_time, + proposed_start_time=None, + gridder=self.search_gridder, raise_if_interruped=self._raise_if_triggered) return None @@ -688,6 +693,7 @@ class Scheduler: logger.warning("Remaining scheduling unit: id=%s '%s'", su.id, su.name) determine_unschedulable_reason_and_mark_unschedulable_if_needed(su, lower_bound_start_time, upper_bound_stop_time, + proposed_start_time=None, gridder=self.search_gridder, raise_if_interruped=self._raise_if_triggered) break logger.info("mid-term schedule: %d units remaining to be estimated", len(scheduling_units)) 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 cb2e71b26db4cd13c957bfca7d476ee8b7b9c430..2a2eb997ec3a08a96a14d93e61b21ab6ef1d2550 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -3418,11 +3418,11 @@ class TestDynamicScheduling(BaseDynamicSchedulingTestCase): scheduling_unit_blueprint.refresh_from_db() self.assertEqual('unschedulable', scheduling_unit_blueprint.status.value) - # check/reproduce the reported bug: - # the unschedulable_reason states that the unit is unschedulable due to station reservations, which is not what we want. + # check that the reported bug is gone. + # the unschedulable_reason used to state that the unit is unschedulable due to station reservations. self.assertNotEqual("Stations are reserved", scheduling_unit_blueprint.unschedulable_reason) - # we want the unschedulable_reason to report on the min_elevation + # we want the unschedulable_reason to report on the min_elevation, if this passes the bug is resolved. self.assertTrue("min_elevation" in scheduling_unit_blueprint.unschedulable_reason) @@ -4680,7 +4680,7 @@ class TestTriggers(BaseDynamicSchedulingTestCase): logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO) # # uncomment to show debug messages for scheduling modules -logging.getLogger('lofar.sas.tmss.services.scheduling').level = logging.DEBUG +# logging.getLogger('lofar.sas.tmss.services.scheduling').level = logging.DEBUG # logging.getLogger('lofar.sas.tmss.tmss.tmssapp.subtasks').level = logging.DEBUG # hide spam of postgreslistener messages diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py index 3d8f3a3f817f7ee0cf55ec0bc716ca507c35b7bc..caaaa399ece969574c4fc497b846ebe2c8c1400b 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py @@ -1785,7 +1785,7 @@ def enough_stations_available(subtask: Subtask, remove_reserved_stations: bool=T logger.error(e) return False -def get_missing_stations(subtask: Subtask) -> []: +def get_missing_stations(subtask: Subtask, lower_bound: datetime=None, upper_bound: datetime=None) -> []: '''Return the list of stations that were requested in the task specification, but which were not available to the scheduled observation.''' # this only makes sense for observations if subtask.specifications_template.type.value != SubtaskType.Choices.OBSERVATION.value: @@ -1796,7 +1796,8 @@ def get_missing_stations(subtask: Subtask) -> []: # fetch the used_stations from the subtask spec, depends on if it's scheduled-and-further if subtask.state.value in (SubtaskState.Choices.DEFINED.value, SubtaskState.Choices.SCHEDULING.value): - used_stations = set(convert_task_station_groups_specification_to_station_list_without_used_and_or_reserved_stations(subtask, remove_reserved_stations=True, remove_used_stations=True, raise_when_too_many_missing=False)) + used_stations = set(convert_task_station_groups_specification_to_station_list_without_used_and_or_reserved_stations(subtask, remove_reserved_stations=True, remove_used_stations=True, raise_when_too_many_missing=False, + lower_bound=lower_bound, upper_bound=upper_bound)) else: used_stations = set(subtask.specifications_doc.get('stations', {}).get('station_list', []))