Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ New Features
- Solar system and lunar ephemerides accessible via ``get_body``,
``get_body_barycentric`` and ``get_moon`` functions. [#4890]

- Added astrometric frames (i.e., a frame centered on a particular
point/object specified in another frame). [#4909]
- Added astrometric frames (i.e., a frame centered on a particular
point/object specified in another frame). [#4909, #4941]

- Added ``SkyCoord.spherical_offsets_to`` method. [#4338]
- Recent Earth rotation (IERS) data are now auto-downloaded so that AltAz
Expand Down
4 changes: 2 additions & 2 deletions astropy/coordinates/builtin_frames/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from .itrs import ITRS
from .hcrs import HCRS
from .ecliptic import GeocentricTrueEcliptic, BarycentricTrueEcliptic, HeliocentricTrueEcliptic
from .astrometric import AstrometricICRS, AstrometricAltAz, make_astrometric_cls
from .astrometric import AstrometricFrame
# need to import transformations so that they get registered in the graph
from . import icrs_fk5_transforms
from . import fk4_fk5_transforms
Expand All @@ -52,7 +52,7 @@
'Supergalactic', 'AltAz', 'GCRS', 'CIRS', 'ITRS', 'HCRS',
'PrecessedGeocentric', 'GeocentricTrueEcliptic',
'BarycentricTrueEcliptic', 'HeliocentricTrueEcliptic',
'AstrometricICRS', 'AstrometricAltAz', 'make_astrometric_cls']
'AstrometricFrame']


def _make_transform_graph_docs():
Expand Down
165 changes: 110 additions & 55 deletions astropy/coordinates/builtin_frames/astrometric.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,23 @@
# -*- coding: utf-8 -*-

import numpy as np

from ... import units as u
from .icrs import ICRS
from .altaz import AltAz
from ..transformations import DynamicMatrixTransform, FunctionTransform
from ..baseframe import (CoordinateAttribute, QuantityFrameAttribute,
frame_transform_graph, RepresentationMapping)
frame_transform_graph, RepresentationMapping,
BaseCoordinateFrame)
from ..angles import rotation_matrix
from ...utils.compat import namedtuple_asdict
from ...extern import six


_astrometric_cache = {}


def make_astrometric_cls(framecls):
"""
Create a new class that is the Astrometric frame for a specific class of
origin frame. If such a class has already been created for this frame, the
same class will be returned.

The resulting frame class will be subtly different from the base class in
that its spherical component names will be d<lat> and d<lon>. E.g., for
ICRS the astrometric frame had components ``dra`` and ``ddec`` instead of
``ra`` and ``dec``.
The new class will always have component names for spherical coordinates of
``lon``/``lat``.

Parameters
----------
Expand Down Expand Up @@ -59,9 +51,19 @@ class AstrometricMeta(framemeta):
of spherical names
"""
def __new__(cls, name, bases, members):
newname = name + framecls.__name__
res = super(AstrometricMeta, cls).__new__(cls, newname, bases, members)
# Only 'origin' is needed here, to set the origin frame properly.
members['origin'] = CoordinateAttribute(frame=framecls, default=None)

# This has to be done because FrameMeta will set these attributes
# to the defaults from BaseCoordinateFrame when it creates the base
# AstrometricFrame class initially.
members['_frame_specific_representation_info'] = framecls._frame_specific_representation_info
members['_default_representation'] = framecls._default_representation

newname = name[:-5] if name.endswith('Frame') else name
newname += framecls.__name__

res = super(AstrometricMeta, cls).__new__(cls, newname, bases, members)
# now go through all the component names and make any spherical
# lat/lon names be "d<lon>"/"d<lat>"

Expand All @@ -79,54 +81,40 @@ def __new__(cls, name, bases, members):

if comp.reprname in ('lon', 'lat'):
dct = namedtuple_asdict(comp)
dct['framename'] = 'd' + dct['framename']
# this forces the component names to be 'lat' and
# 'lon' regardless of what the actual base frame
# might use
dct['framename'] = comp.reprname
component_list[i] = type(comp)(**dct)
gotlatlon.append(comp.reprname)
if 'lon' not in gotlatlon:
rmlon = RepresentationMapping('lon', 'dlon', 'recommended')
rmlon = RepresentationMapping('lon', 'lon', 'recommended')
component_list.insert(0, rmlon)
if 'lat' not in gotlatlon:
rmlat = RepresentationMapping('lat', 'dlat', 'recommended')
rmlat = RepresentationMapping('lat', 'lat', 'recommended')
component_list.insert(0, rmlat)
lists_done.append(component_list)

return res

class Astrometric(six.with_metaclass(AstrometricMeta, framecls)):
"""
A frame which is relative to some position on the sky. Useful for
calculating offsets and dithers in the frame of the sky.

Parameters
----------
representation : `BaseRepresentation` or None
A representation object or None to have no data (or use the other keywords)
origin : `SkyCoord` or low-level coordinate object.
the coordinate which specifiy the origin of this frame.
rotation : Quantity with angle units
The final rotation of the frame about the ``origin``. The sign of
the rotation is the left-hand rule. That is, an object at a
particular position angle in the un-rotated system will be sent to
the positive latitude (z) direction in the final frame.

"""
origin = CoordinateAttribute(default=None, frame=framecls)
rotation = QuantityFrameAttribute(default=0, unit=u.deg)
# We need this to handle the intermediate metaclass correctly, otherwise we could
# just subclass astrometric.
_Astrometric = AstrometricMeta('AstrometricFrame', (AstrometricFrame, framecls),
{'__doc__': AstrometricFrame.__doc__})

@frame_transform_graph.transform(FunctionTransform, Astrometric, Astrometric)
@frame_transform_graph.transform(FunctionTransform, _Astrometric, _Astrometric)
def astrometric_to_astrometric(from_astrometric_coord, to_astrometric_frame):
"""Transform between two astrometric frames."""

# If both frames have on-sky positions, then the transform should happen relative to both origins.
if (from_astrometric_coord.origin is not None) and (to_astrometric_frame.origin is not None):
return from_astrometric_coord.transform_to(framecls).transform_to(to_astrometric_frame)
# This transform goes through the parent frames on each side.
# from_frame -> from_frame.origin -> to_frame.origin -> to_frame
intermediate_from = from_astrometric_coord.transform_to(from_astrometric_coord.origin)
intermediate_to = intermediate_from.transform_to(to_astrometric_frame.origin)
return intermediate_to.transform_to(to_astrometric_frame)

# Otherwise, the transform occurs just by setting the new origin.
return to_astrometric_frame.realize_frame(from_astrometric_coord.cartesian)

@frame_transform_graph.transform(DynamicMatrixTransform, framecls, Astrometric)
def icrs_to_astrometric(reference_frame, astrometric_frame):
"""Convert an ICRS coordinate to an Astrometric frame."""
@frame_transform_graph.transform(DynamicMatrixTransform, framecls, _Astrometric)
def reference_to_astrometric(reference_frame, astrometric_frame):
"""Convert a reference coordinate to an Astrometric frame."""

# Define rotation matricies along the position angle vector, and
# relative to the origin.
Expand All @@ -137,16 +125,83 @@ def icrs_to_astrometric(reference_frame, astrometric_frame):
R = mat1 * mat2 * mat3
return R

@frame_transform_graph.transform(DynamicMatrixTransform, Astrometric, framecls)
def astrometric_to_icrs(astrometric_coord, reference_frame):
"""Convert an Astrometric frame coordinate to an ICRS"""
@frame_transform_graph.transform(DynamicMatrixTransform, _Astrometric, framecls)
def astrometric_to_reference(astrometric_coord, reference_frame):
"""Convert an Astrometric frame coordinate to the reference frame"""

# use the forward transform, but just invert it
R = icrs_to_astrometric(reference_frame, astrometric_coord)
R = reference_to_astrometric(reference_frame, astrometric_coord)
return R.T # this is the inverse because R is a rotation matrix

_astrometric_cache[framecls] = Astrometric
return Astrometric
_astrometric_cache[framecls] = _Astrometric
return _Astrometric


class AstrometricFrame(BaseCoordinateFrame):
"""
A frame which is relative to some specific position and oriented to match
its frame.

AstrometricFrames always have component names for spherical coordinates
of ``lon``/``lat``, *not* the component names for the frame of ``origin``.

This is useful for calculating offsets and dithers in the frame of the sky
relative to an arbitrary position. Coordinates in this frame are both centered on the position specified by the
``origin`` coordinate, *and* they are oriented in the same manner as the
``origin`` frame. E.g., if ``origin`` is `~astropy.coordinates.ICRS`, this
object's ``lat`` will be pointed in the direction of Dec, while ``lon``
will point in the direction of RA.

For more on astrometric frames, see :ref:`astropy-astrometric-frames`.

Parameters
----------
representation : `BaseRepresentation` or None
A representation object or None to have no data (or use the other keywords)
origin : `SkyCoord` or low-level coordinate object.
the coordinate which specifies the origin of this frame.
rotation : Quantity with angle units
The final rotation of the frame about the ``origin``. The sign of
the rotation is the left-hand rule. That is, an object at a
particular position angle in the un-rotated system will be sent to
the positive latitude (z) direction in the final frame.


Notes
-----
``AstrometricFrame`` is a factory class. That is, the objects that it
yields are *not* actually objects of class ``AstrometricFrame``. Instead,
distinct classes are created on-the-fly for whatever the frame class is
of ``origin``.
"""

AstrometricICRS = make_astrometric_cls(ICRS)
AstrometricAltAz = make_astrometric_cls(AltAz)
rotation = QuantityFrameAttribute(default=0, unit=u.deg)
origin = CoordinateAttribute(default=None, frame=None)

def __new__(cls, *args, **kwargs):
# We don't want to call this method if we've already set up
# an astrometric frame for this class.
if not (issubclass(cls, AstrometricFrame) and cls is not AstrometricFrame):
# We get the origin argument, and handle it here.
try:
origin_frame = kwargs['origin']
except KeyError:
raise TypeError("Can't initialize an AstrometricFrame without origin= keyword.")
if hasattr(origin_frame, 'frame'):
origin_frame = origin_frame.frame
newcls = make_astrometric_cls(origin_frame.__class__)
return newcls.__new__(newcls, *args, **kwargs)

# http://stackoverflow.com/questions/19277399/why-does-object-new-work-differently-in-these-three-cases
# See above for why this is necessary. Basically, because some child
# may override __new__, we must override it here to never pass
# arguments to the object.__new__ method.
if super(AstrometricFrame, cls).__new__ is object.__new__:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this ever happen, given that AstrometricFrame is always a subclass of BaseCoordinateFrame?

@alexrudy alexrudy May 18, 2016 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, because BaseCoordinateFrame doesn't implement __new__, this is always triggered right now. If another frame were to implement a custom __new__, this would be skipped by the parent class when traversing the class hierarchy if AstrometricFrame came before the framecls above, which ideally it would, but that breaks something about ordered descriptor that I don't understand (but maybe @eteq could fix?)

Regardless, right now, this is required to prevent this __new__ method from breaking should an descendant implement __new__ in the future. When this happens, if we didn't put in this statement, arguments would never get passed to a subclass __new__, since we'd need to assume we are calling object.__new__. As this is kind of a subtle bug when that happens, I would advocate for leaving this check in now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, yes, I was sloppy in looking through the chain. Given that object is one up as far as __new__ is concerned, it definitely makes sense to have this in.

return super(AstrometricFrame, cls).__new__(cls)
return super(AstrometricFrame, cls).__new__(cls, *args, **kwargs)

def __init__(self, *args, **kwargs):
super(AstrometricFrame, self).__init__(*args, **kwargs)
if self.origin is not None and not self.origin.has_data:
raise ValueError('The origin supplied to AstrometricFrame has no '
'data.')
9 changes: 4 additions & 5 deletions astropy/coordinates/sky_coordinate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .distances import Distance
from .angles import Angle
from .baseframe import BaseCoordinateFrame, frame_transform_graph, GenericFrame, _get_repr_cls
from .builtin_frames import ICRS, make_astrometric_cls
from .builtin_frames import ICRS, AstrometricFrame
from .representation import (BaseRepresentation, SphericalRepresentation,
UnitSphericalRepresentation)

Expand Down Expand Up @@ -1033,13 +1033,12 @@ def astrometric_frame(self):

Returns
-------
astrframe: AstrometricFrame<subtype set by this object>
astrframe: AstrometricFrame
An astrometric frame of the same type as this `SkyCoord` (e.g., if
this object has an ICRS coordinate, the resulting frame is
AstrometricICRS)
AstrometricFrame with an ICRS origin)
"""
acls = make_astrometric_cls(self.frame.__class__)
return acls(origin=self)
return AstrometricFrame(origin=self)

def get_constellation(self, short_name=False, constellation_list='iau'):
"""
Expand Down
Loading