diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints.py b/SAS/TMSS/backend/services/scheduling/lib/constraints.py index b30f0a2e218943bd43f507b9e6eb253ee895a01e..3445b7d87a95d2cc165b0336a074eb4ee5dd21c1 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints.py @@ -914,7 +914,6 @@ def evaluate_sky_min_elevation_constraint(scheduling_unit: models.SchedulingUnit return result for task in scheduling_unit.observation_tasks.all(): - if get_target_sap_pointings(task) and get_target_sap_pointings(task)[0].direction_type not in ['J2000', 'MOON', 'SUN']: logger.warning('SUB id=%s task id=%s contains a pointing of unsupported direction_type=%s' % (scheduling_unit.id, task.id, get_target_sap_pointings(task)[0].direction_type)) return result @@ -970,17 +969,25 @@ def evaluate_sky_min_elevation_constraint(scheduling_unit: models.SchedulingUnit rise_and_set_time['set'] = round_to_second_precision(rise_and_set_time['set']) - if not rise_and_set_time['always_below_horizon']: + if rise_and_set_time['always_below_horizon']: + # always below horizon -> min_elevation not met. + earliest_possible_start_time = None + elif rise_and_set_time['always_above_horizon']: + # always above horizon -> min_elevation is met + earliest_possible_start_time = proposed_start_time + else: # when crossing the horizon somewhere, # determine earliest_possible_start_time, even when constraint is not met, which may never be earlier than lower_bound - earliest_possible_start_time = rise_and_set_time['rise'] or proposed_start_time - if result.earliest_possible_start_time is None: - result.earliest_possible_start_time = earliest_possible_start_time - else: - result.earliest_possible_start_time = max(earliest_possible_start_time, result.earliest_possible_start_time) + earliest_possible_start_time = rise_and_set_time.get('rise', None) or proposed_start_time + + # determine latest earliest_possible_start_time over all possibilities in this loop + if result.earliest_possible_start_time is None or earliest_possible_start_time is None: + result.earliest_possible_start_time = earliest_possible_start_time + else: + result.earliest_possible_start_time = max(earliest_possible_start_time, result.earliest_possible_start_time) # check if constraint is met... - if rise_and_set_time['always_below_horizon'] or \ + if rise_and_set_time['always_below_horizon'] or rise_and_set_time['always_above_horizon'] or\ (rise_and_set_time['rise'] is not None and gridded_timestamp < gridder.minus_margin(rise_and_set_time['rise'])) or \ (rise_and_set_time['set'] is not None and gridded_timestamp > gridder.plus_margin(rise_and_set_time['set'])): elevation = compute_elevation(pointing, gridded_timestamp, station) @@ -989,7 +996,7 @@ def evaluate_sky_min_elevation_constraint(scheduling_unit: models.SchedulingUnit result.score = 0 result.evaluation_timestamp = gridded_timestamp result.optimal_start_time = None - result.message = "task_id=%s task_name='%s' station=%s target='%s' elevation=%.3f[deg] < min_elevation=%.3f[deg] at '%s'" % (task.id, task.name, station, pointing.target, Angle(elevation, astropy.units.rad).deg, min_elevation.degree, gridded_timestamp) + result.message = "SUB id=%s task_id=%s task_name='%s' station=%s target='%s' elevation=%.3f[deg] < min_elevation=%.3f[deg] at '%s'" % (scheduling_unit.id, task.id, task.name, station, pointing.target, Angle(elevation, astropy.units.rad).deg, min_elevation.degree, gridded_timestamp) logger.debug(result.message) return result @@ -1002,13 +1009,15 @@ def evaluate_sky_min_elevation_constraint(scheduling_unit: models.SchedulingUnit if result.score <= 0.0: # this should hardly ever happen, only on real edge cases due to gridding and rounding. If that's the case, use a finer grid. result.score = 0 - result.message = "lowest_elevation=%.3f[deg] < min_elevation=%.3f[deg] at '%s' over all target observations, stations & pointings" % ( + result.message = "SUB id=%s task id=%s lowest_elevation=%.3f[deg] < min_elevation=%.3f[deg] at '%s' over all target observations, stations & pointings" % ( + scheduling_unit.id, task.id, Angle(lowest_elevation, astropy.units.rad).deg, min_elevation.degree, result.evaluation_timestamp) logger.debug(result.message) return result result.score = max(0.001, min(1.0, result.score)) # 0.001 as lowest score, cause 0 means constraint not met. - logger.debug("lowest_elevation=%.3f[deg] >= min_elevation=%.3f[deg] at '%s' over all target observations, stations & pointings" % ( + logger.debug("SUB id=%s task id=%s lowest_elevation=%.3f[deg] >= min_elevation=%.3f[deg] at '%s' over all target observations, stations & pointings" % ( + scheduling_unit.id, task.id, Angle(lowest_elevation, astropy.units.rad).deg, min_elevation.degree, result.evaluation_timestamp)) return result diff --git a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py index 5ff5a7ec1da827f5a848639cbeb41316518965c8..d2c7d9c5b1210ecf9b2f367d415963364b5f39ee 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py @@ -69,6 +69,7 @@ from lofar.sas.tmss.services.scheduling.constraints import * import logging logger = logging.getLogger(__name__) +from operator import attrgetter # LOFAR needs to have a gap in between observations to (re)initialize hardware. DEFAULT_NEXT_STARTTIME_GAP = timedelta(seconds=180) @@ -349,8 +350,8 @@ class Scheduler: # split the list of units in B-prio and others # because B-prio units cannot unschedule overlapping/blocking A/triggered units, we need to search for gaps between already scheduled units. - scheduling_units_B = [su for su in scheduling_units if su.priority_queue.value == models.PriorityQueueType.Choices.B.value] - scheduling_units_non_B = [su for su in scheduling_units if su.priority_queue.value != models.PriorityQueueType.Choices.B.value] + scheduling_units_B = sorted([su for su in scheduling_units if su.priority_queue.value == models.PriorityQueueType.Choices.B.value], key=attrgetter('id')) + scheduling_units_non_B = sorted([su for su in scheduling_units if su.priority_queue.value != models.PriorityQueueType.Choices.B.value], key=attrgetter('id')) # determine the scheduling search windows, taking into account the gaps between A/triggered units for B, or just the full window for A/triggered scheduling_windows_for_B = get_gaps_between_scheduled_units_in_window(lower_bound_start_time, upper_bound_stop_time, DEFAULT_INTER_OBSERVATION_GAP) if scheduling_units_B else tuple() @@ -363,6 +364,10 @@ class Scheduler: continue for scheduling_window in scheduling_windows: + # check gap size: minimal obs duration of 1 minute, plus an inter_observation_gap at both sides + if (scheduling_window[1] - scheduling_window[0]) <= 2*DEFAULT_INTER_OBSERVATION_GAP + timedelta(minutes=1): + continue # gap is too small + logger.info("find_best_next_schedulable_unit: evaluating constraints for units in window ['%s', '%s']: %s", scheduling_window[0], scheduling_window[1], ','.join([str(su.id) for su in sorted(units, key=lambda x: x.id)]) or 'None') # first, from all given scheduling_units, filter and consider only those that meet their constraints. @@ -579,7 +584,8 @@ class Scheduler: lower_bound_start_time = round_to_second_precision(max(datetime.utcnow()+DEFAULT_NEXT_STARTTIME_GAP, gap[0]+DEFAULT_INTER_OBSERVATION_GAP)) upper_bound_stop_time = round_to_second_precision(min(gap[1] - DEFAULT_INTER_OBSERVATION_GAP, scheduling_unit.latest_possible_cycle_stop_time)) - if (upper_bound_stop_time - lower_bound_start_time) <= 2*DEFAULT_INTER_OBSERVATION_GAP: + # check gap size: minimal obs duration of 1 minute, plus an inter_observation_gap at both sides + if (upper_bound_stop_time - lower_bound_start_time) <= 2*DEFAULT_INTER_OBSERVATION_GAP + timedelta(minutes=1): continue # gap is too small logger.info("place_B_priority_units_in_gaps: evaluating %s B-queue units in %d[min]-wide gap ['%s', '%s') next to unit id=%s", 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 fe74ff356516a099f203d0a8ab9693600745817a..f6fde8d6df229518c2f4b9a828cfe94cef838c43 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -1375,8 +1375,10 @@ class TestFixedTimeScheduling(BaseDynamicSchedulingTestCase): scheduling_unit_spec_B['tasks']['Observation']['specifications_doc']['station_configuration']['SAPs'][0]['digital_pointing']['angle1'] = in_between_lst_longitude.rad scheduling_unit_spec_B['tasks']['Observation']['specifications_doc']['station_configuration']['SAPs'][0]['digital_pointing']['angle2'] = Angle('80 degrees').rad scheduling_unit_spec_B['scheduling_constraints_doc']['scheduler'] = 'dynamic' - scheduling_unit_spec_B['scheduling_constraints_doc']['sky']['transit_offset'] = {'from': -3600, 'to': 3600} scheduling_unit_spec_B['scheduling_constraints_doc']['sky']['min_elevation'] = {'target': Angle('10 degrees').rad} + del scheduling_unit_spec_B['scheduling_constraints_doc']['sky']['min_distance'] + del scheduling_unit_spec_B['scheduling_constraints_doc']['sky']['transit_offset'] + del scheduling_unit_spec_B['scheduling_constraints_doc']['daily'] # the 'before' constraint causes the bug scheduling_unit_spec_B['scheduling_constraints_doc']['time']['before'] = (datetime.utcnow()+timedelta(days=2)).isoformat()