Skip to content

Commit ff653f3

Browse files
committed
Merge pull request #5591 from eteq/fix-5133
Correctly account for height in transformations between intermediate and AltAz frames
1 parent 950eb26 commit ff653f3

4 files changed

Lines changed: 123 additions & 34 deletions

File tree

‎CHANGES.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ Bug Fixes
309309

310310
- ``astropy.coordinates``
311311

312+
- Transformations between CIRS and AltAz now correctly account for the
313+
location of the observer. [#5591]
314+
312315
- GCRS frames representing a location on Earth with multiple obstimes are now
313316
allowed. This means that the solar system routines ``get_body``,
314317
``get_moon`` and ``get_sun`` now work with non-scalar times and a

‎astropy/coordinates/builtin_frames/cirs_observed_transforms.py‎

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,22 @@ def cirs_to_altaz(cirs_coo, altaz_frame):
3232
# we use the same obstime everywhere now that we know they're the same
3333
obstime = cirs_coo.obstime
3434

35-
usrepr = cirs_coo.represent_as(UnitSphericalRepresentation)
36-
cirs_ra = usrepr.lon.to(u.radian).value
37-
cirs_dec = usrepr.lat.to(u.radian).value
35+
# if the data are UnitSphericalRepresentation, we can skip the distance calculations
36+
is_unitspherical = (isinstance(cirs_coo.data, UnitSphericalRepresentation) or
37+
cirs_coo.cartesian.x.unit == u.one)
38+
39+
if is_unitspherical:
40+
usrepr = cirs_coo.represent_as(UnitSphericalRepresentation)
41+
cirs_ra = usrepr.lon.to(u.radian).value
42+
cirs_dec = usrepr.lat.to(u.radian).value
43+
else:
44+
# compute an "astrometric" ra/dec -i.e., the direction of the
45+
# displacement vector from the observer to the target in CIRS
46+
loccirs = altaz_frame.location.get_itrs(cirs_coo.obstime).transform_to(cirs_coo)
47+
diffrepr = (cirs_coo.cartesian - loccirs.cartesian).represent_as(UnitSphericalRepresentation)
48+
49+
cirs_ra = diffrepr.lon.to(u.radian).value
50+
cirs_dec = diffrepr.lat.to(u.radian).value
3851

3952
lon, lat, height = altaz_frame.location.to_geodetic('WGS84')
4053
xp, yp = get_polar_motion(obstime)
@@ -52,10 +65,9 @@ def cirs_to_altaz(cirs_coo, altaz_frame):
5265
altaz_frame.relative_humidity,
5366
altaz_frame.obswl.value)
5467

55-
az, zen, ha, obs_dec, obs_ra = erfa.atioq(cirs_ra, cirs_dec, astrom)
68+
az, zen, _, _, _ = erfa.atioq(cirs_ra, cirs_dec, astrom)
5669

57-
dat = cirs_coo.data
58-
if dat.get_name() == 'unitspherical' or dat.to_cartesian().x.unit == u.one:
70+
if is_unitspherical:
5971
rep = UnitSphericalRepresentation(lat=u.Quantity(PIOVER2 - zen, u.radian, copy=False),
6072
lon=u.Quantity(az, u.radian, copy=False),
6173
copy=False)
@@ -95,32 +107,23 @@ def altaz_to_cirs(altaz_coo, cirs_frame):
95107

96108
# the 'A' indicates zen/az inputs
97109
cirs_ra, cirs_dec = erfa.atoiq('A', az, zen, astrom)*u.radian
98-
99-
if isinstance(altaz_coo.data, UnitSphericalRepresentation):
100-
distance = None
110+
if isinstance(altaz_coo.data, UnitSphericalRepresentation) or altaz_coo.cartesian.x.unit == u.one:
111+
cirs_at_aa_time = CIRS(ra=cirs_ra, dec=cirs_dec, distance=None,
112+
obstime=altaz_coo.obstime)
101113
else:
102-
locitrs = altaz_coo.location.get_itrs(altaz_coo.obstime)
103-
104-
# To compute the distance in a way that is reversible with cirs_to_altaz
105-
# we use basic trigonometry. The altaz_coo's distance ("d") is one leg
106-
# of a triangle, and the earth center -> EarthLocation distance ("r")
107-
# is a neighboring leg. We can also easily get the angle between the
108-
# earth center->target (calculated above by apio13), and
109-
# earth center->EarthLocation vectors. This is a Side-Side-Angle
110-
# situation, and the formula below is the trig formula to solve for
111-
# the remaining side
112-
113-
ucirs = cirs_frame.realize_frame(UnitSphericalRepresentation(lon=cirs_ra, lat=cirs_dec))
114-
delta = ucirs.separation(locitrs)
115-
r = locitrs.spherical.distance
116-
d = altaz_coo.distance
117-
118-
sindoverd = np.sin(delta) / d
119-
distance = np.sin(delta + np.arcsin(r*sindoverd))/sindoverd
120-
121-
#the final transform may be a no-op if the obstimes are the same
122-
return CIRS(ra=cirs_ra, dec=cirs_dec, distance=distance,
123-
obstime=altaz_coo.obstime).transform_to(cirs_frame)
114+
# treat the output of atoiq as an "astrometric" RA/DEC, so to get the
115+
# actual RA/Dec from the observers vantage point, we have to reverse
116+
# the vector operation of cirs_to_altaz (see there for more detail)
117+
118+
loccirs = altaz_coo.location.get_itrs(altaz_coo.obstime).transform_to(cirs_frame)
119+
120+
astrometric_rep = SphericalRepresentation(lon=cirs_ra, lat=cirs_dec,
121+
distance=altaz_coo.distance)
122+
newrepr = astrometric_rep + loccirs.cartesian
123+
cirs_at_aa_time = CIRS(newrepr, obstime=altaz_coo.obstime)
124+
125+
#this final transform may be a no-op if the obstimes are the same
126+
return cirs_at_aa_time.transform_to(cirs_frame)
124127

125128

126129
@frame_transform_graph.transform(FunctionTransform, AltAz, AltAz)

‎astropy/coordinates/tests/test_intermediate_transformations.py‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
from ...time import Time
1515
from .. import (EarthLocation, get_sun, ICRS, GCRS, CIRS, ITRS, AltAz,
1616
PrecessedGeocentric, CartesianRepresentation, SkyCoord,
17-
SphericalRepresentation, HCRS, HeliocentricTrueEcliptic)
17+
SphericalRepresentation, UnitSphericalRepresentation,
18+
HCRS, HeliocentricTrueEcliptic)
1819

1920

2021
from ..._erfa import epv00
@@ -331,6 +332,10 @@ def test_gcrs_altaz_moonish(testframe):
331332
# now check that the distance change is similar to earth radius
332333
assert 1000*u.km < np.abs(moonaa.distance - moon.distance).to(u.au) < 7000*u.km
333334

335+
# now check that it round-trips
336+
moon2 = moonaa.transform_to(moon)
337+
assert_allclose(moon.cartesian.xyz, moon2.cartesian.xyz)
338+
334339
# also should add checks that the alt/az are different for different earth locations
335340

336341

@@ -356,7 +361,7 @@ def test_gcrs_altaz_bothroutes(testframe):
356361
def test_cirs_altaz_moonish(testframe):
357362
"""
358363
Sanity-check that an object resembling the moon goes to the right place with
359-
a CIRS->AltAz transformation
364+
a CIRS<->AltAz transformation
360365
"""
361366
moon = CIRS(MOONDIST_CART, obstime=testframe.obstime)
362367

@@ -368,6 +373,19 @@ def test_cirs_altaz_moonish(testframe):
368373
assert_allclose(moon.cartesian.xyz, moon2.cartesian.xyz)
369374

370375

376+
@pytest.mark.parametrize('testframe', totest_frames)
377+
def test_cirs_altaz_nodist(testframe):
378+
"""
379+
Check that a UnitSphericalRepresentation coordinate round-trips for the
380+
CIRS<->AltAz transformation.
381+
"""
382+
coo0 = CIRS(UnitSphericalRepresentation(10*u.deg, 20*u.deg), obstime=testframe.obstime)
383+
384+
# check that it round-trips
385+
coo1 = coo0.transform_to(testframe).transform_to(coo0)
386+
assert_allclose(coo0.cartesian.xyz, coo1.cartesian.xyz)
387+
388+
371389
@pytest.mark.parametrize('testframe', totest_frames)
372390
def test_cirs_icrs_moonish(testframe):
373391
"""

‎astropy/coordinates/tests/test_regression.py‎

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from ...time import Time
2020
from ...utils import iers
2121

22-
from ...tests.helper import pytest, assert_quantity_allclose, catch_warnings
22+
from ...tests.helper import pytest, assert_quantity_allclose, catch_warnings, quantity_allclose
2323
from .test_matching import HAS_SCIPY, OLDER_SCIPY
2424

2525

@@ -283,3 +283,68 @@ def test_regression_5209():
283283
moon = get_moon(time)
284284
new_coord = SkyCoord([moon])
285285
assert_quantity_allclose(new_coord[0].distance, moon.distance)
286+
287+
288+
def test_regression_5133():
289+
N = 1000
290+
np.random.seed(12345)
291+
lon = np.random.uniform(-10, 10, N) * u.deg
292+
lat = np.random.uniform(50, 52, N) * u.deg
293+
alt = np.random.uniform(0, 10., N) * u.km
294+
295+
time = Time('2010-1-1')
296+
297+
objects = EarthLocation.from_geodetic(lon, lat, height=alt)
298+
itrs_coo = objects.get_itrs(time)
299+
300+
homes = [EarthLocation.from_geodetic(lon=-1 * u.deg, lat=52 * u.deg, height=h)
301+
for h in (0, 1000, 10000)*u.km]
302+
303+
altaz_frames = [AltAz(obstime=time, location=h) for h in homes]
304+
altaz_coos = [itrs_coo.transform_to(f) for f in altaz_frames]
305+
306+
# they should all be different
307+
for coo in altaz_coos[1:]:
308+
assert not quantity_allclose(coo.az, coo.az[0])
309+
assert not quantity_allclose(coo.alt, coo.alt[0])
310+
311+
312+
def test_itrs_vals_5133():
313+
time = Time('2010-1-1')
314+
el = EarthLocation.from_geodetic(lon=20*u.deg, lat=45*u.deg, height=0*u.km)
315+
316+
lons = [20, 30, 20]*u.deg
317+
lats = [44, 45, 45]*u.deg
318+
alts = [0, 0, 10]*u.km
319+
coos = [EarthLocation.from_geodetic(lon, lat, height=alt).get_itrs(time)
320+
for lon, lat, alt in zip(lons, lats, alts)]
321+
322+
aaf = AltAz(obstime=time, location=el)
323+
aacs = [coo.transform_to(aaf) for coo in coos]
324+
325+
assert all([coo.isscalar for coo in aacs])
326+
327+
# the ~1 arcsec tolerance is b/c aberration makes it not exact
328+
assert_quantity_allclose(aacs[0].az, 180*u.deg, atol=1*u.arcsec)
329+
assert aacs[0].alt < 0*u.deg
330+
assert aacs[0].distance > 50*u.km
331+
332+
# it should *not* actually be 90 degrees, b/c constant latitude is not
333+
# straight east anywhere except the equator... but should be close-ish
334+
assert_quantity_allclose(aacs[1].az, 90*u.deg, atol=5*u.deg)
335+
assert aacs[1].alt < 0*u.deg
336+
assert aacs[1].distance > 50*u.km
337+
338+
assert_quantity_allclose(aacs[2].alt, 90*u.deg, atol=1*u.arcsec)
339+
assert_quantity_allclose(aacs[2].distance, 10*u.km)
340+
341+
342+
def test_regression_simple_5133():
343+
t = Time('J2010')
344+
obj = EarthLocation(-1*u.deg, 52*u.deg, height=[100., 0.]*u.km)
345+
home = EarthLocation(-1*u.deg, 52*u.deg, height=10.*u.km)
346+
aa = obj.get_itrs(t).transform_to(AltAz(obstime=t, location=home))
347+
348+
# az is more-or-less undefined for straight up or down
349+
assert_quantity_allclose(aa.alt, [90, -90]*u.deg, rtol=1e-5)
350+
assert_quantity_allclose(aa.distance, [90, 10]*u.km)

0 commit comments

Comments
 (0)