diff --git a/SAS/TMSS/src/tmss/tmssapp/conversions.py b/SAS/TMSS/src/tmss/tmssapp/conversions.py index 037d4b31d57c3d921e04780963e2762997f6a339..2c1e27da04d690bbfc96e07e7e3c14776103f2e6 100644 --- a/SAS/TMSS/src/tmss/tmssapp/conversions.py +++ b/SAS/TMSS/src/tmss/tmssapp/conversions.py @@ -6,7 +6,7 @@ from astropy.coordinates.earth import EarthLocation from astropy.coordinates import Angle, get_body from astroplan.observer import Observer import astropy.time - +from functools import lru_cache def create_astroplan_observer_for_station(station: str) -> Observer: ''' @@ -25,11 +25,12 @@ def create_astroplan_observer_for_station(station: str) -> Observer: SUN_SET_RISE_ANGLE_TO_HORIZON = Angle(10, unit=astropy.units.deg) SUN_SET_RISE_PRECISION = 15 # n_grid_points; higher is more precise but very costly; astropy defaults to 150, 15 seems to cause errors of typically one minute -def timestamps_and_stations_to_sun_rise_and_set(timestamps: [datetime], stations: [str], angle_to_horizon: Angle=SUN_SET_RISE_ANGLE_TO_HORIZON) -> dict: +@lru_cache(maxsize=256, typed=False) # does not like lists, so use sets to allow caching +def timestamps_and_stations_to_sun_rise_and_set(timestamps: tuple, stations: tuple, angle_to_horizon: Angle=SUN_SET_RISE_ANGLE_TO_HORIZON) -> dict: """ compute sunrise, sunset, day and night of the given stations at the given timestamps - :param timestamps: list of datetimes, e.g. [datetime(2020, 1, 1), datetime(2020, 1, 2)] - :param stations: list of station names, e.g. ["CS002"] + :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 nested dict that contains lists of start and end times for sunrise, sunset, etc, on each requested date. E.g. {"CS002": @@ -62,36 +63,35 @@ def timestamps_and_stations_to_sun_rise_and_set(timestamps: [datetime], stations return return_dict -def coordinates_and_timestamps_to_separation_from_bodies(coords: [astropy.coordinates.SkyCoord], timestamps: [datetime], bodies: [str], stations: [str]) -> dict: +@lru_cache(maxsize=256, typed=False) # does not like lists, so use sets to allow caching +def coordinates_and_timestamps_to_separation_from_bodies(angle1: float, angle2: float, direction_type: str, timestamps: tuple, bodies: tuple) -> dict: """ - compute angular distances of the given sky coordinates from the given solar system bodies at the given timestamps and stations - :param coords: list of SkyCoord objects to measure separation - :param timestamps: list of datetimes, e.g. [datetime(2020, 1, 1, 15, 0, 0), datetime(2020, 1, 1, 16, 0, 0)] - :param stations: list of station names, e.g. ["CS002"] - :param bodies: list of solar system bodies, e.g. ['sun', 'moon', 'jupiter'] - :return A dict that maps station names to a list with a dict for each given coordinate, which maps each body body to a list of separation angles for each given timestamp. + compute angular distances of the given sky coordinates from the given solar system bodies at the given timestamps (seen from LOFAR core) + :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, 15, 0, 0), datetime(2020, 1, 1, 16, 0, 0)) + :param bodies: tuple of solar system bodies, e.g. ('sun', 'moon', 'jupiter') + :return A dict that maps each body to a dict that maps the given timestamp to a separation angle from the given coordinate. E.g. - {"CS002": - [ - {"sun": [Angle("0.7rad"), Angle("0.7rad")], "moon": [Angle("0.4rad"), Angle("0.4rad")], "jupiter": [Angle("2.7rad"), Angle("2.7rad")]}, - {"sun": [Angle("0.8rad"), Angle("0.8rad")], "moon": [Angle("0.5rad"), Angle("0.5rad")], "jupiter": [Angle("2.6rad"), Angle("2.6rad")]} - ] + { + "sun": {datetime(2020, 1, 1, 6, 0, 0): Angle("0.7rad"), datetime(2020, 1, 1, 7, 0, 0): Angle("0.7rad")}, + "moon": {datetime(2020, 1, 1, 6, 0, 0): Angle("0.4rad"), datetime(2020, 1, 1, 7, 0, 0): Angle("0.4rad")}, + "jupiter": {datetime(2020, 1, 1, 6, 0, 0): Angle("2.7rad"), datetime(2020, 1, 1, 7, 0, 0): Angle("2.7rad")} } - # todo: not so sure about this data structure. SkyCoord is not hashable, so we cannot use it as a key. - # todo: We could provide coordinates in a string format and use that as keys, or limit to a single pointing to avoid confusion. """ + if direction_type == "J2000": + coord = astropy.coordinates.SkyCoord(ra=angle1, dec=angle2, unit=astropy.units.deg) + else: + raise ValueError("Do not know how to convert direction_type=%s to SkyCoord" % direction_type) return_dict = {} - for station in stations: - for coord in coords: - body_angles = {} - for body in bodies: - location = create_astroplan_observer_for_station(station).location - for timestamp in timestamps: - # get body coords at timestamp - body_coord = get_body(body=body, time=astropy.time.Time(timestamp), location=location) - angle = coord.separation(body_coord) - body_angles.setdefault(body, []).append(angle) - return_dict.setdefault(station, []).append(body_angles) + for body in bodies: + location = create_astroplan_observer_for_station("CS002").location + for timestamp in timestamps: + # get body coords at timestamp + body_coord = get_body(body=body, time=astropy.time.Time(timestamp), location=location) + angle = coord.separation(body_coord) + return_dict.setdefault(body, {})[timestamp] = angle return return_dict diff --git a/SAS/TMSS/src/tmss/tmssapp/views.py b/SAS/TMSS/src/tmss/tmssapp/views.py index 7bee3fd449aea5f42c170720c42cba3160812b82..476bf65e275f0b0367ffdc3618723dec215abbf3 100644 --- a/SAS/TMSS/src/tmss/tmssapp/views.py +++ b/SAS/TMSS/src/tmss/tmssapp/views.py @@ -160,31 +160,31 @@ def get_sun_rise_and_set(request): timestamps = request.GET.get('timestamps', None) stations = request.GET.get('stations', None) if timestamps is None: - timestamps = [datetime.utcnow()] + timestamps = (datetime.utcnow(),) else: timestamps = timestamps.split(',') - timestamps = [dateutil.parser.parse(timestamp) for timestamp in timestamps] # isot to datetime + timestamps = tuple([dateutil.parser.parse(timestamp) for timestamp in timestamps]) # isot to datetime if stations is None: - stations = ["CS002"] + stations = ("CS002",) else: - stations = stations.split(',') + stations = tuple(stations.split(',')) # todo: to improve speed for the frontend, we should probably precompute/cache these and return those (where available), to revisit after constraint table / TMSS-190 is done return JsonResponse(timestamps_and_stations_to_sun_rise_and_set(timestamps, stations)) + @permission_classes([AllowAny]) @authentication_classes([AllowAny]) @swagger_auto_schema(method='GET', - responses={200: 'A JSON object with angular distances of the given sky coordinates from the given solar system bodies at the given timestamps and stations. \n' - 'Outer list contains results per coordinate in given order, inner list per timestamp.'}, - operation_description="Get angular distances of the given sky coordinates from the given solar system bodies at all given timestamps and stations. \n\n" - "Example request: /api/util/sun_rise_and_set?coordinates=,CS005×tamps=2020-05-01,2020-09-09T11-11-00", - manual_parameters=[Parameter(name='ras', required=True, type='string', in_='query', - description="comma-separated list of right ascensions (celestial longitudes)"), - Parameter(name='decs', required=True, type='string', in_='query', - description="comma-separated list of declinations (celestial latitude)"), - Parameter(name='stations', required=False, type='string', in_='query', - description="comma-separated list of station names"), + responses={200: 'A JSON object with angular distances of the given sky coordinates from the given solar system bodies at the given timestamps (seen from LOFAR core)'}, + operation_description="Get angular distances of the given sky coordinates from the given solar system bodies at all given timestamps. \n\n" + "Example request: /api/util/angular_separation_from_bodies?angle1=1&angle2=1×tamps=2020-01-01T15,2020-01-01T16", + manual_parameters=[Parameter(name='angle1', required=True, type='string', in_='query', + description="first angle of celectial coordinates as float, e.g. RA"), + Parameter(name='angle2', required=True, type='string', in_='query', + description="second angle of celectial coordinates as float, e.g. RA"), + Parameter(name='direction_type', required=False, type='string', in_='query', + description="direction_type of celectial coordinates as string, e.g. J2000"), Parameter(name='timestamps', required=False, type='string', in_='query', description="comma-separated list of isoformat timestamps"), Parameter(name='bodies', required=False, type='string', in_='query', @@ -194,40 +194,28 @@ def get_angular_separation_from_bodies(request): ''' returns angular distances of the given sky coordinates from the given astronomical objects at the given timestamps and stations ''' - stations = request.GET.get('stations', None) timestamps = request.GET.get('timestamps', None) - ras = request.GET.get('ras', None) - decs = request.GET.get('decs', None) - bodies = request.GET.get('bodies', None) + angle1 = request.GET.get('angle1') + angle2 = request.GET.get('angle2') + direction_type = request.GET.get("direction_type", "J2000") + bodies = tuple(request.GET.get('bodies', "sun,moon,jupiter").split(',')) + + if angle1 is None or angle2 is None: + raise ValueError("Please provide celestial coordinates in radians/J2000 via the 'angle1', 'angle2' and 'direction_type' properties.") if timestamps is None: - timestamps = [datetime.utcnow()] + timestamps = (datetime.utcnow(),) else: timestamps = timestamps.split(',') - timestamps = [dateutil.parser.parse(timestamp) for timestamp in timestamps] # isot to datetime + timestamps = tuple([dateutil.parser.parse(timestamp) for timestamp in timestamps]) # isot to datetime - if bodies is None: - bodies = ['sun', 'moon', 'jupiter'] - else: - bodies = bodies.split(',') + # calculate + sep_dict = coordinates_and_timestamps_to_separation_from_bodies(angle1=angle1, angle2=angle2, direction_type=direction_type, bodies=bodies, timestamps=timestamps) + new_sep_dict = {} - if stations is None: - stations = ["CS002"] - else: - stations = stations.split(',') - - if ras is None or decs is None: - raise ValueError("Please provide celestial coordinates in radians/J2000 via the 'ras' and 'decs' the properties.") - - coords = [] - longitudes = ras.split(',') - latitudes = decs.split(',') - for ra, dec in zip(longitudes, latitudes): - coords.append(SkyCoord(ra=ra, dec=dec, unit=u.rad, equinox='J2000')) - - sep_dict = coordinates_and_timestamps_to_separation_from_bodies(coords=coords, stations=stations, bodies=bodies, timestamps=timestamps) - for station, v in sep_dict.items(): - for coord in v: - for object, angles in coord.items(): - coord[object] = [angle.rad for angle in angles] - return JsonResponse(sep_dict) + # serialize angles and datetimes for json response + for body, timestamps in sep_dict.items(): + for timestamp, angle in timestamps.items(): + new_sep_dict.setdefault(body, {})[timestamp.isoformat()] = angle.rad + + return JsonResponse(new_sep_dict) diff --git a/SAS/TMSS/test/t_conversions.py b/SAS/TMSS/test/t_conversions.py index 8d987ce104c005598472b0781b1c2cb88cf6941d..141ae89c764abd5a654f344679d166950ed3b2c7 100755 --- a/SAS/TMSS/test/t_conversions.py +++ b/SAS/TMSS/test/t_conversions.py @@ -173,58 +173,62 @@ class UtilREST(unittest.TestCase): self.assertIn("celestial coordinates", r.content.decode('utf-8')) def test_util_angular_separation_from_bodies_returns_json_structure_with_defaults(self): - r = requests.get(BASE_URL + '/util/angular_separation_from_bodies?ras=1&decs=1', auth=AUTH) + r = requests.get(BASE_URL + '/util/angular_separation_from_bodies?angle1=1&angle2=1', auth=AUTH) self.assertEqual(r.status_code, 200) r_dict = json.loads(r.content.decode('utf-8')) - # assert defaults to core and today - self.assertIn('CS002', r_dict.keys()) + # assert default bodies for key in ['sun', 'jupiter', 'moon']: - self.assertIn(key, r_dict['CS002'][0]) - self.assertEqual(type(r_dict['CS002'][0]['jupiter'][0]), float) + self.assertIn(key, r_dict.keys()) - def test_util_angular_separation_from_bodies_considers_stations(self): - stations = ['CS005', 'RS305', 'DE609'] - r = requests.get(BASE_URL + '/util/angular_separation_from_bodies?ras=1&decs=1&stations=%s' % ','.join(stations), auth=AUTH) + # assert timestamp is now and has a value + returned_datetime = dateutil.parser.parse(list(r_dict['jupiter'].keys())[0]) + current_datetime = datetime.datetime.utcnow() + delta = abs((returned_datetime - current_datetime).total_seconds()) + self.assertTrue(delta < 60.0) + self.assertEqual(type(list(r_dict['jupiter'].values())[0]), float) + + def test_util_angular_separation_from_bodies_considers_bodies(self): + bodies = ['sun', 'neptune', 'mercury'] + r = requests.get(BASE_URL + '/util/angular_separation_from_bodies?angle1=1&angle2=1&bodies=%s' % ','.join(bodies), auth=AUTH) self.assertEqual(r.status_code, 200) r_dict = json.loads(r.content.decode('utf-8')) # assert station is included in response and angles differ angle_last = None - for station in stations: - self.assertIn(station, r_dict.keys()) - angle = r_dict[station][0]['jupiter'][0] + for body in bodies: + self.assertIn(body, r_dict.keys()) + angle = list(r_dict[body].values())[0] if angle_last: self.assertNotEqual(angle, angle_last) angle_last = angle def test_util_angular_separation_from_bodies_considers_timestamps(self): timestamps = ['2020-01-01', '2020-02-22T16-00-00', '2020-3-11', '2020-01-01'] - r = requests.get(BASE_URL + '/util/angular_separation_from_bodies?ras=1&decs=1×tamps=%s' % ','.join(timestamps), auth=AUTH) + r = requests.get(BASE_URL + '/util/angular_separation_from_bodies?angle1=1&angle2=1×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 response and angles differ - self.assertEqual(len(timestamps), len(r_dict['CS002'][0]['jupiter'])) angle_last = None - for i in range(len(timestamps)): - angle = r_dict['CS002'][0]['jupiter'][i] + for timestamp in timestamps: + expected_timestamp = dateutil.parser.parse(timestamp).isoformat() + self.assertIn(expected_timestamp, r_dict['jupiter']) + angle = r_dict['jupiter'][expected_timestamp] if angle_last: self.assertNotEqual(angle, angle_last) angle_last = angle def test_util_angular_separation_from_bodies_considers_coordinates(self): - ras = ['1.0', '1.1', '1.2'] - decs = ['1.0', '1.1', '1.2'] - r = requests.get(BASE_URL + '/util/angular_separation_from_bodies?ras=%s&decs=%s' % (','.join(ras), ','.join(decs)), auth=AUTH) - self.assertEqual(r.status_code, 200) - r_dict = json.loads(r.content.decode('utf-8')) + test_coords = [(1, 1,"J2000"), (1.1, 1, "J2000"), (1.1, 1.1, "J2000")] + for coords in test_coords: + r = requests.get(BASE_URL + '/util/angular_separation_from_bodies?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 timestamps yield a response and angles differ - self.assertEqual(len(ras), len(r_dict['CS002'])) - angle_last = None - for i in range(len(ras)): - angle = r_dict['CS002'][i]['jupiter'][0] + # assert all requested timestamps yield a response and angles differ + angle_last = None + angle = list(r_dict['jupiter'].values())[0] if angle_last: self.assertNotEqual(angle, angle_last) angle_last = angle