diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints.py b/SAS/TMSS/backend/services/scheduling/lib/constraints.py index 879eeb745a98c051500cce2a949674824bcbd6a7..b79648b7a09b4f016eaf283c428bbcf4a839642e 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints.py @@ -1228,11 +1228,11 @@ def evaluate_sky_min_distance_constraint(scheduling_unit: models.SchedulingUnitB actual_distances = distances[body] assert (len(actual_distances) == 1) actual_distance = actual_distances[gridded_timestamp] - logger.debug("min_distance: SUB id=%s task_id=%s task_name='%s' pointing='%s' distance=%.3f[deg] to body=%s %s min_distance=%.3f[deg] at '%s'", - scheduling_unit.id, target_obs_task.id, target_obs_task.name, pointing, actual_distance.degree, body, - '<' if actual_distance.rad < min_distance else '>=', - Angle(min_distance, astropy.units.rad).degree, - gridded_timestamp) + # logger.debug("min_distance: SUB id=%s task_id=%s task_name='%s' pointing='%s' distance=%.3f[deg] to body=%s %s min_distance=%.3f[deg] at '%s'", + # scheduling_unit.id, target_obs_task.id, target_obs_task.name, pointing, actual_distance.degree, body, + # '<' if actual_distance.rad < min_distance else '>=', + # Angle(min_distance, astropy.units.rad).degree, + # gridded_timestamp) if actual_distance.rad < min_distance: # constraint not met. update result, and do early exit. @@ -1862,10 +1862,10 @@ def get_weighted_start_time(scheduling_unit: models.SchedulingUnitBlueprint, low weighted_start_time = round_to_second_precision(weighted_start_time) if can_run_at(scheduling_unit, weighted_start_time, gridder): - logger.debug("get_weighted_start_time: SUB id=%s weighted='%s' - weight=%.3f earliest='%s' optimal='%s' latest='%s' - window=['%s', '%s']", scheduling_unit.id, weighted_start_time, density_vs_optimal.weight, earliest_possible_start_time, optimal_start_time, latest_possible_start_time, lower_bound, upper_bound) + logger.info("get_weighted_start_time: SUB id=%s weighted='%s' - weight=%.3f earliest='%s' optimal='%s' latest='%s' - window=['%s', '%s']", scheduling_unit.id, weighted_start_time, density_vs_optimal.weight, earliest_possible_start_time, optimal_start_time, latest_possible_start_time, lower_bound, upper_bound) return weighted_start_time - logger.debug("get_weighted_start_time: SUB id=%s returning earliest='%s' - window=['%s', '%s']", scheduling_unit.id, earliest_possible_start_time, lower_bound, upper_bound) + logger.info("get_weighted_start_time: SUB id=%s returning earliest='%s' - window=['%s', '%s']", scheduling_unit.id, earliest_possible_start_time, lower_bound, upper_bound) return earliest_possible_start_time logger.warning("get_weighted_start_time: SUB id=%s could not compute weighted_start_time - window=['%s', '%s']", scheduling_unit.id, lower_bound, upper_bound) @@ -2033,28 +2033,36 @@ def can_run_without_used_stations(scheduling_unit: models.SchedulingUnitBlueprin def get_blocking_scheduled_units(scheduling_unit: models.SchedulingUnitBlueprint, proposed_start_time: datetime=None) -> QuerySet: - '''Get a list (tuple) of scheduled scheduling_units overlapping with the scheduled_observation_start/stop_time of the given scheduling_unit''' + return get_blocking_units(scheduling_unit, proposed_start_time, (models.SchedulingUnitStatus.Choices.SCHEDULED.value,)) + +def get_blocking_scheduled_or_observing_units(scheduling_unit: models.SchedulingUnitBlueprint, proposed_start_time: datetime=None) -> QuerySet: + return get_blocking_units(scheduling_unit, proposed_start_time, (models.SchedulingUnitStatus.Choices.SCHEDULED.value, models.SchedulingUnitStatus.Choices.OBSERVING.value)) + +def get_blocking_units(scheduling_unit: models.SchedulingUnitBlueprint, proposed_start_time: datetime = None, blocking_statuses: Iterable[str]=None) -> QuerySet: from .dynamic_scheduling import DEFAULT_INTER_OBSERVATION_GAP - scheduled_units = models.SchedulingUnitBlueprint.objects.filter(status__value=models.SchedulingUnitStatus.Choices.SCHEDULED.value).filter(obsolete_since__isnull=True) + units = models.SchedulingUnitBlueprint.objects.filter(obsolete_since__isnull=True) + + if blocking_statuses: + units = units.filter(status__value__in=blocking_statuses) lower_bound = (proposed_start_time or scheduling_unit.scheduled_observation_start_time) - DEFAULT_INTER_OBSERVATION_GAP upper_bound = (proposed_start_time or scheduling_unit.scheduled_observation_start_time) + scheduling_unit.relative_observation_stop_time + DEFAULT_INTER_OBSERVATION_GAP # do three stage filtering. # First in the db which is fast, but sometimes yields too many overlapping units, because we overlap the entire unit instead of just the observation part... - overlapping_scheduled_units = scheduled_units.filter(scheduled_stop_time__gt=lower_bound) - overlapping_scheduled_units = overlapping_scheduled_units.filter(scheduled_start_time__lt=upper_bound) + overlapping_units = units.filter(scheduled_stop_time__gt=lower_bound) + overlapping_units = overlapping_units.filter(scheduled_start_time__lt=upper_bound) # Second, loop over the small number of overlapping scheduled units, and only keep the ones overlapping in the observation part # (scheduled_observation_start/stop_time properties are not available as db columns, they are evaluated in python) - observation_overlapping_scheduled_units = [s for s in overlapping_scheduled_units.all() + observation_overlapping_units = [s for s in overlapping_units.all() if s.scheduled_observation_stop_time > lower_bound and s.scheduled_observation_start_time <= upper_bound] # Third, loop over the small number of overlapping scheduled units, and only keep the ones sharing one or more stations blocking_scheduled_unit_ids = set() for obs_task in scheduling_unit.observation_tasks.filter(obsolete_since__isnull=True).all(): - for overlapping_scheduled_unit in observation_overlapping_scheduled_units: + for overlapping_scheduled_unit in observation_overlapping_units: for overlapping_scheduled_obs_task in overlapping_scheduled_unit.observation_tasks.all(): if not enough_stations_available_for_task(obs_task, unavailable_stations=overlapping_scheduled_obs_task.used_stations): blocking_scheduled_unit_ids.add(overlapping_scheduled_unit.id) @@ -2065,7 +2073,7 @@ def get_blocking_scheduled_units(scheduling_unit: models.SchedulingUnitBlueprint blocking_scheduled_unit_ids.remove(scheduling_unit.id) # Finally, return the result as a queryset, so the caller can do further queries on it. - return scheduled_units.filter(id__in=[x for x in blocking_scheduled_unit_ids]).all() + return units.filter(id__in=[x for x in blocking_scheduled_unit_ids]).all() 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: @@ -2081,7 +2089,7 @@ def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_u msg = "Stations %s are reserved at start_time='%s'" % (','.join([str(s) for s in missing_stations]), proposed_start_time) return mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable(scheduling_unit, msg) - blocking_units = get_blocking_scheduled_units(scheduling_unit) + blocking_units = get_blocking_scheduled_or_observing_units(scheduling_unit) if blocking_units.exists(): if len(blocking_units) == 1: msg = "Scheduling unit id=%s is blocking this unit from being scheduled" % (blocking_units[0].id, ) diff --git a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py index b9459217c412dc5fdb456e265a62152d4b46540e..86a77a0c2eda0e5421a719b108f5aff219846fdd 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py @@ -323,11 +323,13 @@ class Scheduler: # determine next possible start time for remaining scheduling_units lower_bound_start_time = datetime.utcnow() + DEFAULT_NEXT_STARTTIME_GAP if scheduled_unit: - lower_bound_start_time = scheduled_unit.on_sky_stop_time + DEFAULT_INTER_OBSERVATION_GAP + # this scheduled_unit was the first and the best + # so update the search window to start from scheduled_unit.on_sky_start_time+GAP because we allow parallel observations. + lower_bound_start_time = max(lower_bound_start_time, scheduled_unit.on_sky_start_time + DEFAULT_INTER_OBSERVATION_GAP) else: scheduled_scheduling_units = get_scheduled_scheduling_units(datetime.utcnow(), scheduler='dynamic') if scheduled_scheduling_units: - lower_bound_start_time = max([s.on_sky_stop_time for s in scheduled_scheduling_units]) + DEFAULT_INTER_OBSERVATION_GAP + lower_bound_start_time = max(lower_bound_start_time, min([s.on_sky_start_time for s in scheduled_scheduling_units]) + DEFAULT_INTER_OBSERVATION_GAP) # round up to next nearest minute lower_bound_start_time += timedelta(seconds=60-lower_bound_start_time.second, @@ -452,37 +454,25 @@ class Scheduler: # search in a forward sliding window for the best scheduling_unit that can be scheduled # once found and scheduled, exit. Otherwise slide window forward and try again. # When scheduling 'just in time' we need to allow the other services/controllers/stations/boards some startup time: DEFAULT_NEXT_STARTTIME_GAP - lower_bound_start_time = max(datetime.utcnow() + DEFAULT_NEXT_STARTTIME_GAP, - min([su.earliest_possible_cycle_start_time for su in candidate_units])) - - # for normal A/B units (not triggered units), adjust lower_bound_start_time search window to any running/observing unit - observing_units = get_observing_scheduling_units() - if observing_units.exists() and schedulable_units != schedulable_units_triggered: - last_on_sky_stop_time = observing_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) - - # adjust lower_bound_start_time search window to any observed and ending-later unit - observed_or_beyond_scheduling_units = get_observed_or_beyond_scheduling_units() - if observed_or_beyond_scheduling_units.exists(): - 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) + # scanning from now+DEFAULT_NEXT_STARTTIME_GAP may include/overlap with already running and/or scheduled observations. That's ok. There are rules to handle the precedence who wins. + lower_bound_start_time = round_to_second_precision(datetime.utcnow() + DEFAULT_NEXT_STARTTIME_GAP) # 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])) + upper_bound_stop_time = round_to_second_precision(max(lower_bound_start_time + timedelta(days=1), + max([su.latest_possible_cycle_stop_time for su in candidate_units]))) window_lower_bound_start_time = lower_bound_start_time - while window_lower_bound_start_time < upper_bound_stop_time: + window_upper_bound_stop_time = min(window_lower_bound_start_time + timedelta(hours=12), + upper_bound_stop_time-timedelta(seconds=1)) + while window_upper_bound_stop_time < upper_bound_stop_time: self._raise_if_triggered() # interrupts the scheduling loop for a next round try: # no need to irritate user in log files with sub-second scheduling precision 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(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) + logger.info("schedule_next_scheduling_unit: searching for best scheduling unit to schedule in window ['%s', '%s']", window_lower_bound_start_time, window_upper_bound_stop_time) best_scored_scheduling_unit = self.find_best_next_schedulable_unit(candidate_units, window_lower_bound_start_time, window_upper_bound_stop_time) @@ -500,7 +490,16 @@ class Scheduler: logger.info("schedule_next_scheduling_unit: found best candidate id=%s '%s' weighted_score=%.3f start_time=%s interrupts_telescope=%s", best_scheduling_unit.id, best_scheduling_unit.name, best_scheduling_unit_score, best_start_time, best_scheduling_unit.interrupts_telescope) - return self.try_schedule_unit(best_scored_scheduling_unit.scheduling_unit, best_scored_scheduling_unit.start_time) + scheduled_unit = self.try_schedule_unit(best_scored_scheduling_unit.scheduling_unit, best_scored_scheduling_unit.start_time) + if scheduled_unit is None: + # we had a best_scored_scheduling_unit, but it could not be scheduled here. + # advance the lower bound of the window a bit, so we can find a new best_scored_scheduling_unit, + # or if the same one wins it is scheduled also a bit later, where it might fit. + window_lower_bound_start_time += timedelta(hours=1) + else: + # done, just return the scheduled_unit + return scheduled_unit + else: 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) @@ -532,13 +531,9 @@ class Scheduler: 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, 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: - window_lower_bound_start_time += timedelta(hours=6) - else: - window_lower_bound_start_time = max(min_earliest_possible_start_time, window_lower_bound_start_time + timedelta(hours=1)) - + # advance the window at the upper side only so more candidates fit in + # our sliding window only looks 12 hours ahead + window_upper_bound_stop_time += timedelta(hours=3) # search again... (while loop) with the remaining schedulable_units and new lower_bound_start_time # nothing was found, or an error occurred. @@ -566,8 +561,8 @@ class Scheduler: unschededule_blocking_scheduled_units_if_needed_and_possible(scheduling_unit, start_time, self.fine_gridder) unschededule_previously_scheduled_unit_if_needed_and_possible(scheduling_unit, start_time) - # are there any blocking scheduled scheduling_units still in the way? - blocking_scheduling_units = get_blocking_scheduled_units(scheduling_unit, start_time) + # are there any blocking scheduled or observing scheduling_units still in the way? + blocking_scheduling_units = get_blocking_scheduled_or_observing_units(scheduling_unit, start_time) if blocking_scheduling_units.exists(): logger.warning("cannot schedule scheduling_unit id=%s '%s' at start_time=%s interrupts_telescope=%s because there are %d other units blocking it",