diff --git a/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py b/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py index 35749faf6a030d9b462324861e6f23183bdca43a..ce7eabfebb7bd9aed5a0c0b7622a3957eaf2532d 100644 --- a/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py +++ b/SAS/TMSS/backend/services/scheduling/lib/constraints/template_constraints_v1.py @@ -246,21 +246,6 @@ def can_run_within_timewindow_with_sky_constraints(scheduling_unit: models.Sched return False -def _check_utc_time_offset_within_lst_bounds(utc_time: datetime, utc_ref_time: datetime, lower_bound_seconds: int, upper_bound_seconds: int, station: str) -> bool: - """ - calculate the offset in local sidereal time between utc timestamps, and check whether it is within the provided - min and max offsets in seconds. - :return: True if the lst offset is within bounds, otherwise False - - todo: LST is an hour angle, hence a time constraint is non-sensical. Clarify, and then either stay in UTC domain or constrain LST with angles. - """ - #lst_time = local_sidereal_time_for_utc_and_station(timestamp=utc_time, station=station) - #lst_ref_time = local_sidereal_time_for_utc_and_station(timestamp=utc_ref_time, station=station) - #diff = (lst_time - lst_ref_time).total_seconds() - diff = (utc_ref_time - utc_time).total_seconds() - return diff > lower_bound_seconds and diff < upper_bound_seconds - - def can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit: models.SchedulingUnitBlueprint, lower_bound: datetime, upper_bound: datetime) -> bool: """ Checks whether it is possible to place the scheduling unit arbitrarily in the given time window, i.e. the sky constraints must be met over the full time window. @@ -324,12 +309,17 @@ def can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit: mod transit_times = coordinates_timestamps_and_stations_to_target_transit(angle1=angle1, angle2=angle2, direction_type=direction_type, timestamps=timestamps, stations=tuple(stations)) for station, times in transit_times.items(): for i in range(len(timestamps)): - if not _check_utc_time_offset_within_lst_bounds(timestamps[i], - times[i], - constraints['sky']['lst_offset']['from'], - constraints['sky']['lst_offset']['to'], - station): - logger.info('lst_offset constraint is not met at timestamp=%s' % (timestamps[i])) + offset = (timestamps[i] - times[i]).total_seconds() + offset_from = constraints['sky']['lst_offset']['from'] + offset_to = constraints['sky']['lst_offset']['to'] + # because the constraint allows specifying a window that reaches past 12h from transit, + # the transit that it refers to may not be the nearest transit to the observation time. + # Hence we also check if the constraint is met with 24h shift (which is approximately + # equivalent to checking the constraint for the previous or next transit) + if not ((offset_from < offset < offset_to) or + (offset_from+86400 < offset < offset_to+86400) or + (offset_from-86400 < offset < offset_to-86400)): + logger.info('lst_offset constraint from=%s to=%s is not met by offset=%s at timestamp=%s' % (offset_from, offset_to, offset, timestamps[i])) return False if 'SAPs' in task['specifications_doc']: @@ -342,24 +332,45 @@ def can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit: mod timestamps = (lower_bound + 0.5 * duration, upper_bound - 0.5 * duration) else: timestamps = (lower_bound, upper_bound) - # for LBA check all SAPs: + + # for LBA get transit times for all SAPs... + sap_transit_times = [] + station_groups = task['specifications_doc']['station_groups'] + stations = list(set(sum([group['stations'] for group in station_groups], []))) # flatten all station_groups to single list for sap in task['specifications_doc']['SAPs']: angle1 = sap['digital_pointing']['angle1'] angle2 = sap['digital_pointing']['angle2'] direction_type = sap['digital_pointing']['direction_type'] - station_groups = task['specifications_doc']['station_groups'] - stations = list(set(sum([group['stations'] for group in station_groups], []))) # flatten all station_groups to single list - transit_times = coordinates_timestamps_and_stations_to_target_transit(angle1=angle1, angle2=angle2, direction_type=direction_type, timestamps=timestamps, stations=tuple(stations)) - - for station, times in transit_times.items(): - for i in range(len(timestamps)): - if not _check_utc_time_offset_within_lst_bounds(timestamps[i], - times[i], - constraints['sky']['lst_offset']['from'], - constraints['sky']['lst_offset']['to'], - station): - logger.info('lst_offset constraint is not met at timestamp=%s' % (timestamps[i])) - return False + sap_transit_times.append(coordinates_timestamps_and_stations_to_target_transit(angle1=angle1, angle2=angle2, direction_type=direction_type, timestamps=timestamps, stations=tuple(stations))) + + # ...then for each station and timestamp, average the transit times we got for the different SAPs + transit_times = {} + _reference_date = datetime(1900, 1, 1) + for station in stations: + for j in range(len(timestamps)): + sap_datetime_list = [sap_transit_times[i][station][j] for i in range(len(task['specifications_doc']['SAPs']))] + average_transit_time = _reference_date + sum([date - _reference_date for date in sap_datetime_list], timedelta()) / len(sap_datetime_list) + transit_times.get(station, []).append(average_transit_time) + + logger.warning('##### %s' % transit_times) + + for station, times in transit_times.items(): + for i in range(len(timestamps)): + offset = (timestamps[i] - times[i]).total_seconds() + offset_from = constraints['sky']['lst_offset']['from'] + offset_to = constraints['sky']['lst_offset']['to'] + # because the constraint allows specifying a window that reaches past 12h from transit, + # the transit that it refers to may not be the nearest transit to the observation time. + # Hence we also check if the constraint is met with 24h shift (which is approximately + # equivalent to checking the constraint for the previous or next transit) + if not ((offset_from < offset < offset_to) or + (offset_from+86400 < offset < offset_to+86400) or + (offset_from-86400 < offset < offset_to-86400)): + logger.info('lst_offset constraint from=%s to=%s is not met by offset=%s at timestamp=%s' % (offset_from, offset_to, offset, timestamps[i])) + return False + + + return True 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 07d671c6f7cb870f4a8f3a7f7f5102f8678d83f6..8963ff58db555c4e596afd246d39dff3b87828bb 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -815,6 +815,8 @@ class TestSkyConstraints(unittest.TestCase): self.addCleanup(self.target_rise_and_set_patcher.stop) self.target_transit_data = {"CS002": [datetime(2020, 1, 1, 14, 0, 0), datetime(2020, 1, 1, 14, 0, 0)]} + self.target_transit_data_previous = {"CS002": [datetime(2019, 12, 31, 14, 0, 0), datetime(2020, 1, 1, 14, 0, 0)]} + self.target_transit_data_saps = [{"CS001": [datetime(2020, 1, 1, 14, 0, 0), datetime(2020, 1, 1, 14, 0, 0)]}, {"CS001": [datetime(2020, 1, 1, 16, 0, 0), datetime(2020, 1, 1, 16, 0, 0)]}] self.target_transit_patcher = mock.patch('lofar.sas.tmss.services.scheduling.constraints.template_constraints_v1.coordinates_timestamps_and_stations_to_target_transit') self.target_transit_mock = self.target_transit_patcher.start() self.target_transit_mock.return_value = self.target_transit_data @@ -855,36 +857,68 @@ class TestSkyConstraints(unittest.TestCase): # lst_offset def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_lst_offset_constraint_returns_true_when_met(self): - # transits at 14h, obs middle is 13h, so we have an offset of 1h + # case 1: transits at 14h, obs middle is at 13h, so we have an offset of -3600 seconds - self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'lst_offset': {'from': -7200, 'to': 7200}} + # big window + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'lst_offset': {'from': -43200, 'to': 43200}} # todo: use blueprint contraints after TMSS-697 was merged self.scheduling_unit_blueprint.save() timestamp = datetime(2020, 1, 1, 12, 0, 0) returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration)) self.assertTrue(returned_value) - self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'lst_offset': {'from': 3500, 'to': 3700}} + # narrow window + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'lst_offset': {'from': -3601, 'to': -3599}} # todo: use blueprint contraints after TMSS-697 was merged self.scheduling_unit_blueprint.save() timestamp = datetime(2020, 1, 1, 12, 0, 0) returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration)) self.assertTrue(returned_value) + # case 2: transits at 14h, obs middle is at 2h, so we have an offset of -43200 seconds + + # window spans past 12h, so reference transit is not nearest transit to obs time + self.target_transit_mock.return_value = self.target_transit_data_previous + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'lst_offset': {'from': -43300, 'to': -43100}} # todo: use blueprint contraints after TMSS-697 was merged + self.scheduling_unit_blueprint.save() + timestamp = datetime(2020, 1, 1, 1, 0, 0) + returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration)) + self.assertTrue(returned_value) + self.target_transit_mock.return_value = self.target_transit_data + def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_lst_offset_constraint_returns_false_when_not_met(self): - # transits at 14h, obs middle is 13h, so we have an offset of 1h + # transits at 14h, obs middle is at 13h, so we have an offset of -3600 seconds - self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'lst_offset': {'from': -7200, 'to': 3500}} + # window after + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'lst_offset': {'from': -3599, 'to': 43200}} # todo: use blueprint contraints after TMSS-697 was merged self.scheduling_unit_blueprint.save() timestamp = datetime(2020, 1, 1, 12, 0, 0) returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration)) self.assertFalse(returned_value) - self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'lst_offset': {'from': 3700, 'to': 7200}} + # window before + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'lst_offset': {'from': -43200, 'to': -3601}} # todo: use blueprint contraints after TMSS-697 was merged self.scheduling_unit_blueprint.save() timestamp = datetime(2020, 1, 1, 12, 0, 0) returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration)) self.assertFalse(returned_value) + def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_lst_offset_constraint_averages_SAPs_for_LBA(self): + # sap1 transits at 14h, sap2 transits at 16h, so average transit is at 15h + # obs middle is 13h, so we have an offset of -7200 seconds + + self.target_transit_mock.side_effect = self.target_transit_data_saps + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'lst_offset': {'from': -7201, 'to': -7199}} # todo: use blueprint contraints after TMSS-697 was merged + self.scheduling_unit_blueprint.requirements_doc['tasks']['Observation']['specifications_doc']['antenna_set'] = 'LBA_INNER' + self.scheduling_unit_blueprint.requirements_doc['tasks']['Observation']['specifications_doc']['SAPs'] = \ + [{'name': 'CygA', 'target': 'CygA', 'subbands': [0, 1], 'digital_pointing': {'angle1': 5.233660650313663, 'angle2': 0.7109404782526458, 'direction_type': 'J2000'}}, + {'name': 'CasA', 'target': 'CasA', 'subbands': [2, 3], 'digital_pointing': {'angle1': 6.233660650313663, 'angle2': 0.6109404782526458, 'direction_type': 'J2000'}}] + self.scheduling_unit_blueprint.save() + timestamp = datetime(2020, 1, 1, 12, 0, 0) + returned_value = tc1.can_run_anywhere_within_timewindow_with_sky_constraints(self.scheduling_unit_blueprint, timestamp, timestamp + timedelta(seconds=self.obs_duration)) + self.assertTrue(returned_value) + self.target_transit_mock.side_effect = None + + class TestTimeConstraints(TestCase): """ Tests for the time constraint checkers used in dynamic scheduling with different boundaries