# -*- coding: utf-8 -*- # Licensed under a 3-clause BSD style license - see LICENSE.rst import pytest import numpy as np from astropy import units as u from astropy.coordinates.distances import Distance from astropy.coordinates.builtin_frames import ICRS, FK5, Galactic, AltAz, SkyOffsetFrame from astropy.coordinates import SkyCoord, EarthLocation from astropy.time import Time from astropy.tests.helper import assert_quantity_allclose as assert_allclose def test_altaz_attribute_transforms(): """Test transforms between AltAz frames with different attributes.""" el1 = EarthLocation(0*u.deg, 0*u.deg, 0*u.m) origin1 = AltAz(0 * u.deg, 0*u.deg, obstime=Time("2000-01-01T12:00:00"), location=el1) frame1 = SkyOffsetFrame(origin=origin1) coo1 = SkyCoord(1 * u.deg, 1 * u.deg, frame=frame1) el2 = EarthLocation(0*u.deg, 0*u.deg, 0*u.m) origin2 = AltAz(0 * u.deg, 0*u.deg, obstime=Time("2000-01-01T11:00:00"), location=el2) frame2 = SkyOffsetFrame(origin=origin2) coo2 = coo1.transform_to(frame2) coo2_expected = [1.22522446, 0.70624298] * u.deg assert_allclose([coo2.lon.wrap_at(180*u.deg), coo2.lat], coo2_expected, atol=convert_precision) el3 = EarthLocation(0*u.deg, 90*u.deg, 0*u.m) origin3 = AltAz(0 * u.deg, 90*u.deg, obstime=Time("2000-01-01T12:00:00"), location=el3) frame3 = SkyOffsetFrame(origin=origin3) coo3 = coo2.transform_to(frame3) assert_allclose([coo3.lon.wrap_at(180*u.deg), coo3.lat], [1*u.deg, 1*u.deg], atol=convert_precision) @pytest.mark.parametrize("inradec,expectedlatlon, tolsep", [ ((45, 45)*u.deg, (0, 0)*u.deg, .001*u.arcsec), ((45, 0)*u.deg, (0, -45)*u.deg, .001*u.arcsec), ((45, 90)*u.deg, (0, 45)*u.deg, .001*u.arcsec), ((46, 45)*u.deg, (1*np.cos(45*u.deg), 0)*u.deg, 16*u.arcsec), ]) def test_skyoffset(inradec, expectedlatlon, tolsep, originradec=(45, 45)*u.deg): origin = ICRS(*originradec) skyoffset_frame = SkyOffsetFrame(origin=origin) skycoord = SkyCoord(*inradec, frame=ICRS) skycoord_inaf = skycoord.transform_to(skyoffset_frame) assert hasattr(skycoord_inaf, 'lon') assert hasattr(skycoord_inaf, 'lat') expected = SkyCoord(*expectedlatlon, frame=skyoffset_frame) assert skycoord_inaf.separation(expected) < tolsep # Check we can also transform back (regression test for gh-11254). roundtrip = skycoord_inaf.transform_to(ICRS()) assert roundtrip.separation(skycoord) < 1*u.uas def test_skyoffset_functional_ra(): # we do the 12)[1:-1] business because sometimes machine precision issues # lead to results that are either ~0 or ~360, which mucks up the final # comparison and leads to spurious failures. So this just avoids that by # staying away from the edges input_ra = np.linspace(0, 360, 12)[1:-1] input_dec = np.linspace(-90, 90, 12)[1:-1] icrs_coord = ICRS(ra=input_ra*u.deg, dec=input_dec*u.deg, distance=1.*u.kpc) for ra in np.linspace(0, 360, 24): # expected rotation expected = ICRS(ra=np.linspace(0-ra, 360-ra, 12)[1:-1]*u.deg, dec=np.linspace(-90, 90, 12)[1:-1]*u.deg, distance=1.*u.kpc) expected_xyz = expected.cartesian.xyz # actual transformation to the frame skyoffset_frame = SkyOffsetFrame(origin=ICRS(ra*u.deg, 0*u.deg)) actual = icrs_coord.transform_to(skyoffset_frame) actual_xyz = actual.cartesian.xyz # back to ICRS roundtrip = actual.transform_to(ICRS()) roundtrip_xyz = roundtrip.cartesian.xyz # Verify assert_allclose(actual_xyz, expected_xyz, atol=1E-5*u.kpc) assert_allclose(icrs_coord.ra, roundtrip.ra, atol=1E-5*u.deg) assert_allclose(icrs_coord.dec, roundtrip.dec, atol=1E-5*u.deg) assert_allclose(icrs_coord.distance, roundtrip.distance, atol=1E-5*u.kpc) def test_skyoffset_functional_dec(): # we do the 12)[1:-1] business because sometimes machine precision issues # lead to results that are either ~0 or ~360, which mucks up the final # comparison and leads to spurious failures. So this just avoids that by # staying away from the edges input_ra = np.linspace(0, 360, 12)[1:-1] input_dec = np.linspace(-90, 90, 12)[1:-1] input_ra_rad = np.deg2rad(input_ra) input_dec_rad = np.deg2rad(input_dec) icrs_coord = ICRS(ra=input_ra*u.deg, dec=input_dec*u.deg, distance=1.*u.kpc) # Dec rotations # Done in xyz space because dec must be [-90,90] for dec in np.linspace(-90, 90, 13): # expected rotation dec_rad = -np.deg2rad(dec) expected_x = (-np.sin(input_dec_rad) * np.sin(dec_rad) + np.cos(input_ra_rad) * np.cos(input_dec_rad) * np.cos(dec_rad)) expected_y = (np.sin(input_ra_rad) * np.cos(input_dec_rad)) expected_z = (np.sin(input_dec_rad) * np.cos(dec_rad) + np.sin(dec_rad) * np.cos(input_ra_rad) * np.cos(input_dec_rad)) expected = SkyCoord(x=expected_x, y=expected_y, z=expected_z, unit='kpc', representation_type='cartesian') expected_xyz = expected.cartesian.xyz # actual transformation to the frame skyoffset_frame = SkyOffsetFrame(origin=ICRS(0*u.deg, dec*u.deg)) actual = icrs_coord.transform_to(skyoffset_frame) actual_xyz = actual.cartesian.xyz # back to ICRS roundtrip = actual.transform_to(ICRS()) # Verify assert_allclose(actual_xyz, expected_xyz, atol=1E-5*u.kpc) assert_allclose(icrs_coord.ra, roundtrip.ra, atol=1E-5*u.deg) assert_allclose(icrs_coord.dec, roundtrip.dec, atol=1E-5*u.deg) assert_allclose(icrs_coord.distance, roundtrip.distance, atol=1E-5*u.kpc) def test_skyoffset_functional_ra_dec(): # we do the 12)[1:-1] business because sometimes machine precision issues # lead to results that are either ~0 or ~360, which mucks up the final # comparison and leads to spurious failures. So this just avoids that by # staying away from the edges input_ra = np.linspace(0, 360, 12)[1:-1] input_dec = np.linspace(-90, 90, 12)[1:-1] input_ra_rad = np.deg2rad(input_ra) input_dec_rad = np.deg2rad(input_dec) icrs_coord = ICRS(ra=input_ra*u.deg, dec=input_dec*u.deg, distance=1.*u.kpc) for ra in np.linspace(0, 360, 10): for dec in np.linspace(-90, 90, 5): # expected rotation dec_rad = -np.deg2rad(dec) ra_rad = np.deg2rad(ra) expected_x = (-np.sin(input_dec_rad) * np.sin(dec_rad) + np.cos(input_ra_rad) * np.cos(input_dec_rad) * np.cos(dec_rad) * np.cos(ra_rad) + np.sin(input_ra_rad) * np.cos(input_dec_rad) * np.cos(dec_rad) * np.sin(ra_rad)) expected_y = (np.sin(input_ra_rad) * np.cos(input_dec_rad) * np.cos(ra_rad) - np.cos(input_ra_rad) * np.cos(input_dec_rad) * np.sin(ra_rad)) expected_z = (np.sin(input_dec_rad) * np.cos(dec_rad) + np.sin(dec_rad) * np.cos(ra_rad) * np.cos(input_ra_rad) * np.cos(input_dec_rad) + np.sin(dec_rad) * np.sin(ra_rad) * np.sin(input_ra_rad) * np.cos(input_dec_rad)) expected = SkyCoord(x=expected_x, y=expected_y, z=expected_z, unit='kpc', representation_type='cartesian') expected_xyz = expected.cartesian.xyz # actual transformation to the frame skyoffset_frame = SkyOffsetFrame(origin=ICRS(ra*u.deg, dec*u.deg)) actual = icrs_coord.transform_to(skyoffset_frame) actual_xyz = actual.cartesian.xyz # back to ICRS roundtrip = actual.transform_to(ICRS()) # Verify assert_allclose(actual_xyz, expected_xyz, atol=1E-5*u.kpc) assert_allclose(icrs_coord.ra, roundtrip.ra, atol=1E-4*u.deg) assert_allclose(icrs_coord.dec, roundtrip.dec, atol=1E-5*u.deg) assert_allclose(icrs_coord.distance, roundtrip.distance, atol=1E-5*u.kpc) def test_skycoord_skyoffset_frame(): m31 = SkyCoord(10.6847083, 41.26875, frame='icrs', unit=u.deg) m33 = SkyCoord(23.4621, 30.6599417, frame='icrs', unit=u.deg) m31_astro = m31.skyoffset_frame() m31_in_m31 = m31.transform_to(m31_astro) m33_in_m31 = m33.transform_to(m31_astro) assert_allclose([m31_in_m31.lon, m31_in_m31.lat], [0, 0]*u.deg, atol=1e-10*u.deg) assert_allclose([m33_in_m31.lon, m33_in_m31.lat], [11.13135175, -9.79084759]*u.deg) assert_allclose(m33.separation(m31), np.hypot(m33_in_m31.lon, m33_in_m31.lat), atol=.1*u.deg) # used below in the next parametrized test m31_sys = [ICRS, FK5, Galactic] m31_coo = [(10.6847929, 41.2690650), (10.6847929, 41.2690650), (121.1744050, -21.5729360)] m31_dist = Distance(770, u.kpc) convert_precision = 1 * u.arcsec roundtrip_precision = 1e-4 * u.degree dist_precision = 1e-9 * u.kpc m31_params = [] for i in range(len(m31_sys)): for j in range(len(m31_sys)): if i < j: m31_params.append((m31_sys[i], m31_sys[j], m31_coo[i], m31_coo[j])) @pytest.mark.parametrize(('fromsys', 'tosys', 'fromcoo', 'tocoo'), m31_params) def test_m31_coord_transforms(fromsys, tosys, fromcoo, tocoo): """ This tests a variety of coordinate conversions for the Chandra point-source catalog location of M31 from NED, via SkyOffsetFrames """ from_origin = fromsys(fromcoo[0]*u.deg, fromcoo[1]*u.deg, distance=m31_dist) from_pos = SkyOffsetFrame(1*u.deg, 1*u.deg, origin=from_origin) to_origin = tosys(tocoo[0]*u.deg, tocoo[1]*u.deg, distance=m31_dist) to_astroframe = SkyOffsetFrame(origin=to_origin) target_pos = from_pos.transform_to(to_astroframe) assert_allclose(to_origin.separation(target_pos), np.hypot(from_pos.lon, from_pos.lat), atol=convert_precision) roundtrip_pos = target_pos.transform_to(from_pos) assert_allclose([roundtrip_pos.lon.wrap_at(180*u.deg), roundtrip_pos.lat], [1.0*u.deg, 1.0*u.deg], atol=convert_precision) @pytest.mark.parametrize("rotation, expectedlatlon", [ (0*u.deg, [0, 1]*u.deg), (180*u.deg, [0, -1]*u.deg), (90*u.deg, [-1, 0]*u.deg), (-90*u.deg, [1, 0]*u.deg) ]) def test_rotation(rotation, expectedlatlon): origin = ICRS(45*u.deg, 45*u.deg) target = ICRS(45*u.deg, 46*u.deg) aframe = SkyOffsetFrame(origin=origin, rotation=rotation) trans = target.transform_to(aframe) assert_allclose([trans.lon.wrap_at(180*u.deg), trans.lat], expectedlatlon, atol=1e-10*u.deg) @pytest.mark.parametrize("rotation, expectedlatlon", [ (0*u.deg, [0, 1]*u.deg), (180*u.deg, [0, -1]*u.deg), (90*u.deg, [-1, 0]*u.deg), (-90*u.deg, [1, 0]*u.deg) ]) def test_skycoord_skyoffset_frame_rotation(rotation, expectedlatlon): """Test if passing a rotation argument via SkyCoord works""" origin = SkyCoord(45*u.deg, 45*u.deg) target = SkyCoord(45*u.deg, 46*u.deg) aframe = origin.skyoffset_frame(rotation=rotation) trans = target.transform_to(aframe) assert_allclose([trans.lon.wrap_at(180*u.deg), trans.lat], expectedlatlon, atol=1e-10*u.deg) def test_skyoffset_names(): origin1 = ICRS(45*u.deg, 45*u.deg) aframe1 = SkyOffsetFrame(origin=origin1) assert type(aframe1).__name__ == 'SkyOffsetICRS' origin2 = Galactic(45*u.deg, 45*u.deg) aframe2 = SkyOffsetFrame(origin=origin2) assert type(aframe2).__name__ == 'SkyOffsetGalactic' def test_skyoffset_origindata(): origin = ICRS() with pytest.raises(ValueError): SkyOffsetFrame(origin=origin) def test_skyoffset_lonwrap(): origin = ICRS(45*u.deg, 45*u.deg) sc = SkyCoord(190*u.deg, -45*u.deg, frame=SkyOffsetFrame(origin=origin)) assert sc.lon < 180 * u.deg sc2 = SkyCoord(-10*u.deg, -45*u.deg, frame=SkyOffsetFrame(origin=origin)) assert sc2.lon < 180 * u.deg sc3 = sc.realize_frame(sc.represent_as('cartesian')) assert sc3.lon < 180 * u.deg sc4 = sc2.realize_frame(sc2.represent_as('cartesian')) assert sc4.lon < 180 * u.deg def test_skyoffset_velocity(): c = ICRS(ra=170.9*u.deg, dec=-78.4*u.deg, pm_ra_cosdec=74.4134*u.mas/u.yr, pm_dec=-93.2342*u.mas/u.yr) skyoffset_frame = SkyOffsetFrame(origin=c) c_skyoffset = c.transform_to(skyoffset_frame) assert_allclose(c_skyoffset.pm_lon_coslat, c.pm_ra_cosdec) assert_allclose(c_skyoffset.pm_lat, c.pm_dec) @pytest.mark.parametrize("rotation, expectedpmlonlat", [ (0*u.deg, [1, 2]*u.mas/u.yr), (45*u.deg, [-2**-0.5, 3*2**-0.5]*u.mas/u.yr), (90*u.deg, [-2, 1]*u.mas/u.yr), (180*u.deg, [-1, -2]*u.mas/u.yr), (-90*u.deg, [2, -1]*u.mas/u.yr) ]) def test_skyoffset_velocity_rotation(rotation, expectedpmlonlat): sc = SkyCoord(ra=170.9*u.deg, dec=-78.4*u.deg, pm_ra_cosdec=1*u.mas/u.yr, pm_dec=2*u.mas/u.yr) c_skyoffset0 = sc.transform_to(sc.skyoffset_frame(rotation=rotation)) assert_allclose(c_skyoffset0.pm_lon_coslat, expectedpmlonlat[0]) assert_allclose(c_skyoffset0.pm_lat, expectedpmlonlat[1]) def test_skyoffset_two_frames_interfering(): """Regression test for gh-11277, where it turned out that the origin argument validation from one SkyOffsetFrame could interfere with that of another. Note that this example brought out a different bug than that at the top of gh-11277, viz., that an attempt was made to set origin on a SkyCoord when it should just be stay as part of the SkyOffsetFrame. """ # Example adapted from @bmerry's minimal example at # https://github.com/astropy/astropy/issues/11277#issuecomment-825492335 altaz_frame = AltAz(obstime=Time('2020-04-22T13:00:00Z'), location=EarthLocation(18, -30)) target = SkyCoord(alt=70*u.deg, az=150*u.deg, frame=altaz_frame) dirs_altaz_offset = SkyCoord(lon=[-0.02, 0.01, 0.0, 0.0, 0.0] * u.rad, lat=[0.0, 0.2, 0.0, -0.3, 0.1] * u.rad, frame=target.skyoffset_frame()) dirs_altaz = dirs_altaz_offset.transform_to(altaz_frame) dirs_icrs = dirs_altaz.transform_to(ICRS()) target_icrs = target.transform_to(ICRS()) # The line below was almost guaranteed to fail. dirs_icrs.transform_to(target_icrs.skyoffset_frame())