diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints.py b/SAS/TMSS/backend/services/scheduling/lib/constraints.py index 91d4bca30a96d47e7250c3d5fe8d1c6ef9cadcef..6f699de78dc9b17cac1b8571c006cce09818f6ec 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints.py @@ -26,9 +26,6 @@ This module defines the 'API' to: These main methods are used in the dynamic_scheduler to pick the next best scheduling unit, and compute the midterm schedule. """ -import logging -logger = logging.getLogger(__name__) - from datetime import datetime, timedelta from dateutil import parser from typing import Callable @@ -46,6 +43,9 @@ from lofar.sas.tmss.tmss.exceptions import * from lofar.sas.tmss.tmss.tmssapp.subtasks import enough_stations_available from lofar.sas.tmss.tmss.tmssapp.tasks import mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable +import logging +logger = logging.getLogger(__name__) + # rough estimate of how many units and timestamp evaluations can be cached CACHE_SIZE = 1000 * 24 * 12 * 365 @@ -730,12 +730,6 @@ def evaluate_sky_transit_constraint(scheduling_unit: models.SchedulingUnitBluepr transit_from_limit_with_margin = transit_from_limit - 60*gridder.grid_minutes transit_to_limit_with_margin = transit_to_limit + 60*gridder.grid_minutes - # TODO: Remove this workaround when fixing bug in TMSS-2017 - logger.info("Applying temporary sky.transit_offset=[-12h,+12h] as workaround to prevent unschedulable bug") - transit_from_limit = -12*60*60 - transit_from_limit_with_margin = -12*60*60 - transit_to_limit = 12*60*60 - transit_to_limit_with_margin = 12*60*60 # transits are only computed for target observations target_obs_tasks = [t for t in scheduling_unit.observation_tasks if t.is_target_observation] @@ -768,23 +762,23 @@ def evaluate_sky_transit_constraint(scheduling_unit: models.SchedulingUnitBluepr # transit minus half duration is by definition the optimal start_time # also take the task relative start time against the su.starttime into account - result.optimal_start_time = transit_timestamp - target_obs_task.relative_start_time - (target_obs_task.specified_duration / 2) + result.optimal_start_time = transit_timestamp - (target_obs_task.specified_duration / 2) - target_obs_task.relative_start_time # earliest_possible_start_time is the transit plus the lower limit (which is usually negative) - # also take the task relative start time against the su.starttime into account - result.earliest_possible_start_time = transit_timestamp + timedelta(seconds=transit_from_limit) - target_obs_task.relative_start_time + result.earliest_possible_start_time = result.optimal_start_time + timedelta(seconds=transit_from_limit) # now check if the constraint is met, and compute/set score # include the margins for gridding effects when checking offset-within-window, # but not when computing score - offset = (transit_timestamp-task_proposed_center_time).total_seconds() + offset = (task_proposed_center_time-transit_timestamp).total_seconds() if offset > transit_from_limit_with_margin and offset < transit_to_limit_with_margin: # constraint is met. compute score. # 1.0 when proposed_center_time==transit_timestamp # 0.0 at either translit offset limit - score_from = min(1.0, max(0.0, (transit_from_limit - offset)/transit_from_limit)) - score_to = min(1.0, max(0.0, (transit_to_limit - offset)/transit_to_limit)) - result.score = 0.5*score_from + 0.5*score_to + if offset <= 0: + result.score = min(1.0, max(0.0, (offset - transit_from_limit_with_margin)/abs(transit_from_limit_with_margin))) + else: + result.score = min(1.0, max(0.0, (transit_to_limit_with_margin - offset)/abs(transit_to_limit_with_margin))) else: result.score = 0 @@ -890,16 +884,24 @@ def evaluate_sky_min_elevation_constraint(scheduling_unit: models.SchedulingUnit def get_transit_timestamp_offset_and_elevation(scheduling_unit: models.SchedulingUnitBlueprint, proposed_start_time: datetime, gridder: Gridder=None) -> (datetime, float): '''Get the transit timestamp, offset (in seconds) and elevation (in radions) for the given scheduling_unit at the given proposed_start_time''' - if gridder is None: - gridder = Gridder(1) - if proposed_start_time is None: proposed_start_time = scheduling_unit.scheduled_start_time + if proposed_start_time is None: # still none? because scheduling_unit.scheduled_start_time is None + return None, None, None + + if gridder is None: + gridder = Gridder(1) + observer = create_astroplan_observer_for_station('CS002') - transit_pointings = get_transit_offset_pointings(scheduling_unit.main_observation_task) - gridded_start_time = gridder.grid_time(proposed_start_time + scheduling_unit.main_observation_task.relative_start_time) - proposed_center_time = proposed_start_time + scheduling_unit.main_observation_task.relative_start_time + scheduling_unit.main_observation_task.specified_duration / 2 + + target_obs_task = scheduling_unit.main_observation_task + transit_pointings = get_transit_offset_pointings(target_obs_task) + + # take along the relative_start_time of this task compared to the scheduling unit's start_time + task_proposed_start_time = proposed_start_time + target_obs_task.relative_start_time + task_proposed_center_time = task_proposed_start_time + target_obs_task.specified_duration / 2 + gridded_start_time = gridder.grid_time(task_proposed_start_time) min_elevation = 1e99 min_elevation_transit = None @@ -915,7 +917,7 @@ def get_transit_timestamp_offset_and_elevation(scheduling_unit: models.Schedulin if elevation < min_elevation: min_elevation = elevation min_elevation_transit = transit_timestamp - min_offset = (proposed_center_time - min_elevation_transit).total_seconds() + min_offset = (task_proposed_center_time - min_elevation_transit).total_seconds() return (min_elevation_transit, min_offset, min_elevation) @@ -1023,6 +1025,8 @@ def get_earliest_possible_start_time_for_sky_min_elevation(scheduling_unit: mode def get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime=None, gridder: Gridder=None) -> datetime: # compute the transit time, and thus the optimal_start_time and earliest_possible_start_time + if gridder is None: + gridder = Gridder() gridded_lower_bound = gridder.grid_time(lower_bound) possible_start_time = gridded_lower_bound @@ -1036,15 +1040,16 @@ def get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit: mod if not result.has_constraint: return None - if result.is_constraint_met: + if result.is_constraint_met and result.earliest_possible_start_time >= lower_bound: return result.earliest_possible_start_time # constraint is not met, advance possible_start_time and evaluate again if result.earliest_possible_start_time is None or result.earliest_possible_start_time < lower_bound: - # advance with a grid step, and evaluate again + # advance, and evaluate again possible_start_time += gridder.as_timedelta() continue + # constraint is not met, advance using estimate of earliest_possible_start_time if result.earliest_possible_start_time is not None: # advance straight to earliest_possible_start_time, and evaluate again to ensure the constraint is met next_possible_start_time = gridder.grid_time(result.earliest_possible_start_time) @@ -1597,6 +1602,7 @@ def determine_unschedulable_reason_and_mark_unschedulable_if_needed(scheduling_u unmet_constraints.append("sky min elevation") if 'transit_offset' in scheduling_unit.scheduling_constraints_doc['sky']: + # TODO: this check ignores the 'at' contraint. Fix that if get_earliest_possible_start_time_for_sky_transit_offset(scheduling_unit, lower_bound, upper_bound, gridder) is None: unmet_constraints.append("sky transit offset") diff --git a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py index 7ecf79bd838f8b97cf4c8c28b31c116b339e663e..1b2fe30f72c31f927e7475ca2f6fa046777e0ece 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/lib/dynamic_scheduling.py @@ -52,16 +52,14 @@ and then triggering a new computation of a full schedule in the Scheduler. import os -import logging import astropy.coordinates -logger = logging.getLogger(__name__) from datetime import datetime, timedelta, time from lofar.sas.tmss.tmss.tmssapp import models from lofar.sas.tmss.tmss.tmssapp.tasks import schedule_independent_subtasks_in_scheduling_unit_blueprint, unschedule_subtasks_in_scheduling_unit_blueprint, mark_independent_subtasks_in_scheduling_unit_blueprint_as_unschedulable, mark_independent_subtasks_in_scheduling_unit_blueprint_as_schedulable, mark_independent_subtasks_in_scheduling_unit_blueprint_as_schedulable, set_scheduling_unit_blueprint_start_times, reschedule_subtasks_in_scheduling_unit_blueprint -from lofar.sas.tmss.tmss.tmssapp.subtasks import update_subtasks_start_times_for_scheduling_unit, clear_defined_subtasks_start_stop_times_for_scheduling_unit, cancel_subtask, mark_subtasks_and_successors_as_defined +from lofar.sas.tmss.tmss.tmssapp.subtasks import update_subtasks_start_times_for_scheduling_unit, cancel_subtask, mark_subtasks_and_successors_as_defined from lofar.sas.tmss.client.tmssbuslistener import * from lofar.common.datetimeutils import round_to_second_precision from threading import Thread, Event @@ -71,6 +69,9 @@ from django.db.models import QuerySet, Q, Max from lofar.sas.tmss.tmss.exceptions import SchedulerInterruptedException from lofar.sas.tmss.services.scheduling.constraints import * +import logging +logger = logging.getLogger(__name__) + # LOFAR needs to have a gap in between observations to (re)initialize hardware. DEFAULT_NEXT_STARTTIME_GAP = timedelta(seconds=180) @@ -309,6 +310,9 @@ class Scheduler: # ensure upper is greater than or equal to lower upper_bound_stop_time = max(lower_bound_start_time, upper_bound_stop_time) + if not scheduling_units: + return None + logger.info("find_best_next_schedulable_unit: evaluating constraints for units in window ['%s', '%s']: %s", lower_bound_start_time, upper_bound_stop_time, ','.join([str(su.id) for su in sorted(scheduling_units, key=lambda x: x.id)]) or 'None') # first, from all given scheduling_units, filter and consider only those that meet their constraints. @@ -400,6 +404,10 @@ class Scheduler: while 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) @@ -575,8 +583,8 @@ class Scheduler: ("'%s'" % (unit.name[:32],)).ljust(34), unit.scheduled_start_time, unit.status.value.ljust(14), - transit_offset / 60, - Angle(elevation, astropy.units.rad).degree) + transit_offset / 60 if transit_offset else None, + Angle(elevation, astropy.units.rad).degree) if elevation else None except Exception as e: logger.warning(e) logger.log(log_level, "-----------------------------------------------------------------") @@ -763,6 +771,7 @@ class TMSSDynamicSchedulingMessageHandler(TMSSEventMessageHandler): weight_factor = models.SchedulingConstraintsWeightFactor.objects.get(id=id) logger.info("weight_factor %s for template %s version %s changed to %s: triggering update of dynamic schedule...", weight_factor.constraint_name, weight_factor.scheduling_constraints_template.name, weight_factor.scheduling_constraints_template.version, weight_factor.weight) + wipe_evaluate_constraints_caches() mark_unschedulable_scheduling_units_for_active_projects_schedulable() self.scheduler.trigger() diff --git a/SAS/TMSS/backend/services/scheduling/lib/subtask_scheduling.py b/SAS/TMSS/backend/services/scheduling/lib/subtask_scheduling.py index 323fe4c4f73d37555c1da63eaae25be276405017..9246fe17d0e3d2ab1d9271ed5f8f03dda0dad9ec 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/subtask_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/lib/subtask_scheduling.py @@ -30,8 +30,6 @@ it schedules (rest action) all successors that are in state 'defined'. import os from optparse import OptionParser -import logging -logger = logging.getLogger(__name__) from lofar.sas.tmss.client.tmssbuslistener import * from lofar.sas.tmss.tmss.tmssapp.models import Subtask, SubtaskState, SubtaskType, SchedulingUnitBlueprint, TaskBlueprint @@ -40,6 +38,9 @@ from lofar.sas.tmss.tmss.exceptions import TMSSException from lofar.common.datetimeutils import round_to_second_precision from datetime import datetime, timedelta +import logging +logger = logging.getLogger(__name__) + class TMSSSubTaskSchedulingEventMessageHandler(TMSSEventMessageHandler): ''' diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py b/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py index 97074592dae60ce05fc40fce4ff34a6e1577fb02..9dedc2de6dbaa577c23702b93056fcdb57bd3d51 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py @@ -389,7 +389,7 @@ def compute_elevation(pointing: Pointing, timestamp: datetime, station: str='CS0 def local_sidereal_time_for_utc_and_station(timestamp: datetime = None, station: str = 'CS002', field: str = 'LBA', - kind: str = "apparent"): + kind: str = "apparent") -> astropy.coordinates.Longitude: """ calculate local sidereal time for given utc time and station :param timestamp: timestamp as datetime object @@ -398,19 +398,15 @@ def local_sidereal_time_for_utc_and_station(timestamp: datetime = None, :param kind: 'mean' or 'apparent' :return: """ - from lofar.lta.sip import station_coordinates - if timestamp is None: timestamp = datetime.utcnow() - station_coords = station_coordinates.parse_station_coordinates() - field_coords = station_coords["%s_%s" % (station, field)] - location = EarthLocation.from_geocentric(x=field_coords['x'], y=field_coords['y'], z=field_coords['z'], unit=astropy.units.m) + location = create_location_for_station(station) return local_sidereal_time_for_utc_and_longitude(timestamp=timestamp, longitude=location.lon.to_string(decimal=True), kind=kind) def local_sidereal_time_for_utc_and_longitude(timestamp: datetime = None, longitude: float = 6.8693028, - kind: str = "apparent"): + kind: str = "apparent") -> astropy.coordinates.Longitude: """ :param timestamp: timestamp as datetime object :param longitude: decimal longitude of observer location (defaults to CS002 LBA center) diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py index 9647e847409c0cd9a230f2f37dc7fe0560a26cee..28f75f91f589c2ccddab103c3a0fe7d18c7fb4d3 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/subtasks.py @@ -28,7 +28,7 @@ from lofar.common.cobaltblocksize import CorrelatorSettings, StokesSettings, Blo from lofar.mac.observation_control_rpc import ObservationControlRPCClient from lofar.mac.pipeline_control_rpc import PipelineControlRPCClient -from lofar.sas.tmss.tmss.tmssapp.conversions import antennafields_for_antennaset_and_station, create_location_for_station, Time +from lofar.sas.tmss.tmss.tmssapp.conversions import antennafields_for_antennaset_and_station, create_location_for_station, Time, local_sidereal_time_for_utc_and_station from lofar.sas.tmss.tmss.exceptions import TMSSException, TooManyStationsUnavailableException from django.db import transaction from django.db.models import Q @@ -1653,13 +1653,14 @@ def compute_scheduled_central_lst(subtask: Subtask) -> Subtask: scheduled_duration = subtask.scheduled_stop_time - subtask.scheduled_start_time scheduled_central_time = subtask.scheduled_start_time + 0.5*scheduled_duration - # convert the UTC timestamp to LST at CS002 - cs002_location = create_location_for_station('CS002') - scheduled_central_lst = Time(scheduled_central_time, scale='utc', location=cs002_location).sidereal_time('apparent') + # convert the UTC timestamp to LST at CS002 (expressed as astropy Longitude) + lst_longitude = local_sidereal_time_for_utc_and_station(scheduled_central_time, 'CS002') - # convert the astropy Longitude LST time back to a native python time via datetime - scheduled_central_lst = datetime.fromordinal(scheduled_central_time.date().toordinal()) + timedelta(hours=scheduled_central_lst.hour) - subtask.scheduled_central_lst = round_to_second_precision(scheduled_central_lst).time() + # convert the astropy Longitude LST time back to a native python time (ignore microsecond) + # we're "forced" to convert via datetime, evan though the date is irrelevant (and discarded) + lst_longitude_as_timedelta = timedelta(hours=lst_longitude.hour) + lst_longitude_as_py_time = (datetime(2022, 1, 1, 0, 0, 0) + lst_longitude_as_timedelta).time().replace(microsecond=0) + subtask.scheduled_central_lst = lst_longitude_as_py_time else: subtask.scheduled_central_lst = None