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 7dcb48aef02d519b0aab7fb8bc620a6eb7028a9d..342b727554e0c3a5ca3212ab4008f8ecd116e752 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 @@ -33,7 +33,7 @@ from astropy.coordinates import Angle import astropy.units from lofar.sas.tmss.tmss.tmssapp import models -from lofar.sas.tmss.tmss.tmssapp.conversions import create_astroplan_observer_for_station, Time, timestamps_and_stations_to_sun_rise_and_set, coordinates_and_timestamps_to_separation_from_bodies, coordinates_timestamps_and_stations_to_target_rise_and_set +from lofar.sas.tmss.tmss.tmssapp.conversions import create_astroplan_observer_for_station, Time, timestamps_and_stations_to_sun_rise_and_set, coordinates_and_timestamps_to_separation_from_bodies, coordinates_timestamps_and_stations_to_target_rise_and_set, coordinates_timestamps_and_stations_to_target_transit, local_sidereal_time_for_utc_and_station from lofar.sas.tmss.tmss.exceptions import TMSSException from . import ScoredSchedulingUnit @@ -265,7 +265,6 @@ def can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit: mod angle1 = beam['angle1'] angle2 = beam['angle2'] direction_type = beam['direction_type'] - if 'min_distance' in constraints['sky']: # currently we only check at bounds, we probably want to add some more samples in between later on distances = coordinates_and_timestamps_to_separation_from_bodies(angle1=angle1, angle2=angle2, direction_type=direction_type, timestamps=(lower_bound, upper_bound), bodies=tuple(constraints['sky']['min_distance'].keys())) @@ -299,6 +298,81 @@ def can_run_anywhere_within_timewindow_with_sky_constraints(scheduling_unit: mod else: logger.info('min_target_elevation=%s constraint is not met at timestamp=%s' % (min_elevation.rad, timestamps[i])) return False + if 'transit_offset' in constraints['sky'] and 'from' in constraints['sky']['transit_offset'] and task['specifications_template'] == 'target observation': + # Check constraint on tile beam for HBA only: + if task['specifications_doc']['antenna_set'].startswith('HBA'): + # since the constraint only applies to the middle of the obs, consider its duration + if 'duration' in task['specifications_doc']: + duration = timedelta(seconds=task['specifications_doc']['duration']) + timestamps = (lower_bound + 0.5 * duration, upper_bound - 0.5 * duration) + else: + timestamps = (lower_bound, upper_bound) + 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)): + offset = (timestamps[i] - times[i]).total_seconds() + offset_from = constraints['sky']['transit_offset']['from'] + offset_to = constraints['sky']['transit_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('transit_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']: + if 'transit_offset' in constraints['sky'] and 'from' in constraints['sky']['transit_offset'] and task['specifications_template'] == 'target observation': + # Check constraint on SAPs for LBA only: + if task['specifications_doc']['antenna_set'].startswith('LBA'): + # since the constraint only applies to the middle of the obs, consider its duration + if 'duration' in task['specifications_doc']: + duration = timedelta(seconds=task['specifications_doc']['duration']) + timestamps = (lower_bound + 0.5 * duration, upper_bound - 0.5 * duration) + else: + timestamps = (lower_bound, upper_bound) + + # 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'] + 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']['transit_offset']['from'] + offset_to = constraints['sky']['transit_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('transit_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 77e62b3c4a2b4a9505563747c6f78c5c9fb1eaa9..59e644b4a882c4add00baa7b495fb95f41a524df 100755 --- a/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py +++ b/SAS/TMSS/backend/services/scheduling/test/t_dynamic_scheduling.py @@ -812,12 +812,19 @@ class TestSkyConstraints(unittest.TestCase): {"rise": datetime(2020, 1, 1, 8, 0, 0), "set": datetime(2020, 1, 1, 12, 30, 0), "always_above_horizon": False, "always_below_horizon": False}]} self.target_rise_and_set_data_always_above = {"CS002": [{"rise": None, "set": None, "always_above_horizon": True, "always_below_horizon": False}]} self.target_rise_and_set_data_always_below = {"CS002": [{"rise": None, "set": None, "always_above_horizon": False, "always_below_horizon": True}]} - self.target_rise_and_set_patcher = mock.patch('lofar.sas.tmss.services.scheduling.constraints.template_constraints_v1.coordinates_timestamps_and_stations_to_target_rise_and_set') self.target_rise_and_set_mock = self.target_rise_and_set_patcher.start() self.target_rise_and_set_mock.return_value = self.target_rise_and_set_data 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 + self.addCleanup(self.target_transit_patcher.stop) + # min_distance def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_min_distance_constraint_returns_true_when_met(self): @@ -850,6 +857,70 @@ class TestSkyConstraints(unittest.TestCase): 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) + # transit_offset + + def test_can_run_anywhere_within_timewindow_with_sky_constraints_with_transit_offset_constraint_returns_true_when_met(self): + # case 1: transits at 14h, obs middle is at 13h, so we have an offset of -3600 seconds + + # big window + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'transit_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) + + # narrow window + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'transit_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'] = {'transit_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_transit_offset_constraint_returns_false_when_not_met(self): + # transits at 14h, obs middle is at 13h, so we have an offset of -3600 seconds + + # window after + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'transit_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) + + # window before + self.scheduling_unit_blueprint.draft.scheduling_constraints_doc['sky'] = {'transit_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_transit_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'] = {'transit_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): """ diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py b/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py index 14b0a38e566666fda10ba8292bb9d4f91525afef..642e7090c070be2033a4af4c8404c137bbe2b771 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/conversions.py @@ -35,7 +35,6 @@ SUN_SET_RISE_ANGLE_TO_HORIZON = Angle(10, unit=astropy.units.deg) # TODO: To be considered, now we store the sunset/sunrise data in advanced, we can increase the number of points!! SUN_SET_RISE_PRECISION = 30 - def timestamps_and_stations_to_sun_rise_and_set(timestamps: tuple, stations: tuple, angle_to_horizon: Angle=SUN_SET_RISE_ANGLE_TO_HORIZON, create_when_not_found=False) -> dict: """ @@ -268,6 +267,34 @@ def coordinates_timestamps_and_stations_to_target_rise_and_set(angle1: float, an return return_dict +# default n_grid_points; higher is more precise but very costly; astropy defaults to 150, note that errors can be in the minutes with a lower values +TARGET_TRANSIT_PRECISION = 150 + +@lru_cache(maxsize=256, typed=False) # does not like lists, so use tuples to allow caching +def coordinates_timestamps_and_stations_to_target_transit(angle1: float, angle2: float, direction_type: str, timestamps: tuple, stations: tuple) -> dict: + """ + Compute nearest meridian transit times of the given coordinates for each given station and timestamp. + :param angle1: first angle of celectial coordinates, e.g. RA + :param angle2: second angle of celectial coordinates, e.g. Dec + :param direction_type: direction_type of celectial coordinates, e.g. 'J2000' + :param timestamps: tuple of datetimes, e.g. (datetime(2020, 1, 1), datetime(2020, 1, 2)) + :param stations: tuple of station names, e.g. ("CS002",) + :return A dict that maps station names to a list of transit times (nearest transit for each requested timestamp). + E.g. + {"CS002": [datetime(2020, 1, 1, 4, 0, 0), datetime(2020, 1, 2, 4, 0, 0)]} + """ + if direction_type == "J2000": + coord = astropy.coordinates.SkyCoord(ra=angle1, dec=angle2, unit=astropy.units.rad) + else: + raise ValueError("Do not know how to convert direction_type=%s to SkyCoord" % direction_type) + return_dict = {} + for station in stations: + for timestamp in timestamps: + # todo: this can probably be made faster by moving the following logic to an own function with single station/timestamp as input and putting the lru_cache on there. + observer = create_astroplan_observer_for_station(station) + target_transit = observer.target_meridian_transit_time(target=coord, time=Time(timestamp), which='nearest', n_grid_points=TARGET_TRANSIT_PRECISION) + return_dict.setdefault(station, []).append(target_transit.to_datetime()) + return return_dict def local_sidereal_time_for_utc_and_station(timestamp: datetime = None, diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/views.py b/SAS/TMSS/backend/src/tmss/tmssapp/views.py index 85bdfe0de03a90428f85f01fb51264e4b4082b49..c043399964b788b809194e49c1c0b6872e57fdfe 100644 --- a/SAS/TMSS/backend/src/tmss/tmssapp/views.py +++ b/SAS/TMSS/backend/src/tmss/tmssapp/views.py @@ -18,7 +18,7 @@ from datetime import datetime import dateutil.parser from astropy.coordinates import Angle import astropy.units -from lofar.sas.tmss.tmss.tmssapp.conversions import local_sidereal_time_for_utc_and_station, local_sidereal_time_for_utc_and_longitude, timestamps_and_stations_to_sun_rise_and_set, coordinates_and_timestamps_to_separation_from_bodies, coordinates_timestamps_and_stations_to_target_rise_and_set +from lofar.sas.tmss.tmss.tmssapp.conversions import local_sidereal_time_for_utc_and_station, local_sidereal_time_for_utc_and_longitude, timestamps_and_stations_to_sun_rise_and_set, coordinates_and_timestamps_to_separation_from_bodies, coordinates_timestamps_and_stations_to_target_rise_and_set, coordinates_timestamps_and_stations_to_target_transit # Note: Decorate with @api_view to get this picked up by Swagger @@ -278,3 +278,27 @@ def get_target_rise_and_set(request): rise_set_dict = coordinates_timestamps_and_stations_to_target_rise_and_set(angle1=angle1, angle2=angle2, direction_type=direction_type, angle_to_horizon=horizon, timestamps=timestamps, stations=stations) return JsonResponse(rise_set_dict) + +@api_view(['GET']) +def get_target_transit(request): + ''' + returns transit times of the given coordinates for each given station and timestamp. + ''' + timestamps = request.GET.get('timestamps', None) + angle1 = request.GET.get('angle1') + angle2 = request.GET.get('angle2') + direction_type = request.GET.get("direction_type", "J2000") + stations = tuple(request.GET.get('stations', "CS002").split(',')) + + if angle1 is None or angle2 is None: + raise ValueError("Please provide celestial coordinates via 'angle1', 'angle2' (and optionally 'direction_type') properties.") + + if timestamps is None: + timestamps = (datetime.utcnow(),) + else: + timestamps = timestamps.split(',') + timestamps = tuple([dateutil.parser.parse(timestamp, ignoretz=True) for timestamp in timestamps]) # isot to datetime + + # calculate + transit_dict = coordinates_timestamps_and_stations_to_target_transit(angle1=angle1, angle2=angle2, direction_type=direction_type, timestamps=timestamps, stations=stations) + return JsonResponse(transit_dict) diff --git a/SAS/TMSS/backend/src/tmss/urls.py b/SAS/TMSS/backend/src/tmss/urls.py index 5306787cb405fa524cbb475cc7d7e76d1fe3c561..c077e51431b29da1484c0653421d54c27a7a5f91 100644 --- a/SAS/TMSS/backend/src/tmss/urls.py +++ b/SAS/TMSS/backend/src/tmss/urls.py @@ -75,6 +75,7 @@ urlpatterns = [ re_path('util/lst/?', views.lst, name="conversion-lst"), re_path('util/angular_separation/?', views.get_angular_separation, name='get_angular_separation'), re_path('util/target_rise_and_set/?', views.get_target_rise_and_set, name='get_target_rise_and_set'), + re_path('util/target_transit/?', views.get_target_transit, name='get_target_transit'), ] if os.environ.get('SHOW_DJANGO_DEBUG_TOOLBAR', False): diff --git a/SAS/TMSS/backend/test/t_conversions.py b/SAS/TMSS/backend/test/t_conversions.py index ad873499f1271b74fd75cf7da3f59b39fc1cbecf..942fb172dd634dd99f95732459ac833b0e37a622 100755 --- a/SAS/TMSS/backend/test/t_conversions.py +++ b/SAS/TMSS/backend/test/t_conversions.py @@ -354,13 +354,13 @@ class UtilREST(unittest.TestCase): def test_util_target_rise_and_set_considers_horizon(self): test_horizons = [0.1, 0.2, 0.3] + rise_last = None for horizon in test_horizons: r = requests.get(BASE_URL + '/util/target_rise_and_set?angle1=0.5&angle2=0.5&horizon=%s' % horizon, auth=AUTH) self.assertEqual(r.status_code, 200) r_dict = json.loads(r.content.decode('utf-8')) # assert all requested horizons yield a response and times differ - rise_last = None rise = r_dict['CS002'][0]['rise'] if rise_last: self.assertNotEqual(rise, rise_last) @@ -395,6 +395,76 @@ class UtilREST(unittest.TestCase): self.assertFalse(r_dict['CS002'][0]['always_above_horizon']) self.assertTrue(r_dict['CS002'][0]['always_below_horizon']) + # target transit + + def test_util_target_transit_returns_json_structure_with_defaults(self): + r = requests.get(BASE_URL + '/util/target_transit?angle1=0.5&angle2=0.5', auth=AUTH) + self.assertEqual(r.status_code, 200) + r_dict = json.loads(r.content.decode('utf-8')) + + # defaults are CS002 and today + self.assertIn('CS002', r_dict.keys()) + + # assert returned timestamp is no further than 12h away from now + expected_time = datetime.datetime.utcnow() + returned_time = dateutil.parser.parse(r_dict['CS002'][0]) + time_diff = abs(expected_time - returned_time) + self.assertTrue(time_diff <= datetime.timedelta(days=0.5)) + + def test_util_target_transit_considers_stations(self): + stations = ['CS005', 'RS305', 'DE609'] + r = requests.get(BASE_URL + '/util/target_transit?angle1=0.5&angle2=0.5&stations=%s' % ','.join(stations), auth=AUTH) + self.assertEqual(r.status_code, 200) + r_dict = json.loads(r.content.decode('utf-8')) + + # assert station is included in response and timestamps differ + target_transit_last = None + for station in stations: + self.assertIn(station, r_dict.keys()) + target_transit = dateutil.parser.parse(r_dict[station][0]) + if target_transit_last: + self.assertNotEqual(target_transit, target_transit_last) + target_transit_last = target_transit + + def test_util_target_transit_considers_timestamps(self): + timestamps = ['2020-01-01', '2020-02-22T16-00-00', '2020-3-11'] + r = requests.get(BASE_URL + '/util/target_transit?angle1=0.5&angle2=0.5×tamps=%s' % ','.join(timestamps), auth=AUTH) + self.assertEqual(r.status_code, 200) + r_dict = json.loads(r.content.decode('utf-8')) + + # assert all requested timestamps yield a different response + transit_last = None + for i in range(len(timestamps)): + transit = r_dict['CS002'][i] + if transit_last: + self.assertNotEqual(transit, transit_last) + transit_last = transit + + def test_util_target_transit_returns_correct_date_of_target_transit(self): + timestamps = ['2020-01-01T02-00-00'] + r = requests.get(BASE_URL + '/util/target_transit?angle1=0.5&angle2=0.5×tamps=%s' % ','.join(timestamps), auth=AUTH) + self.assertEqual(r.status_code, 200) + r_dict = json.loads(r.content.decode('utf-8')) + + # assert transit time is no further than 12h from requested time + requested_time = dateutil.parser.parse(timestamps[0]).replace(tzinfo=None) + returned_time = dateutil.parser.parse(r_dict['CS002'][0]) + time_diff = abs(requested_time - returned_time) + self.assertTrue(time_diff <= datetime.timedelta(days=0.5)) + + def test_util_target_transit_considers_coordinates(self): + test_coords = [(0.5, 0.5, "J2000"), (0.6, 0.5, "J2000"), (0.6, 0.6, "J2000")] + transit_last = None + for coords in test_coords: + r = requests.get(BASE_URL + '/util/target_transit?angle1=%s&angle2=%s&direction_type=%s' % coords, auth=AUTH) + self.assertEqual(r.status_code, 200) + r_dict = json.loads(r.content.decode('utf-8')) + + # assert all requested coordinates yield a response and times differ + transit = r_dict['CS002'][0] + if transit_last: + self.assertNotEqual(transit, transit_last) + transit_last = transit if __name__ == "__main__": os.environ['TZ'] = 'UTC'