1# -*- coding: utf-8 -*-
2# Licensed under a 3-clause BSD style license - see LICENSE.rst
3"""
4Framework and base classes for coordinate frames/"low-level" coordinate
5classes.
6"""
7
8
9# Standard library
10import copy
11import inspect
12from collections import namedtuple, defaultdict
13import warnings
14
15# Dependencies
16import numpy as np
17
18# Project
19from astropy.utils.compat.misc import override__dir__
20from astropy.utils.decorators import lazyproperty, format_doc
21from astropy.utils.exceptions import AstropyWarning, AstropyDeprecationWarning
22from astropy import units as u
23from astropy.utils import ShapedLikeNDArray, check_broadcast
24from .transformations import TransformGraph
25from . import representation as r
26from .angles import Angle
27from .attributes import Attribute
28
29
30__all__ = ['BaseCoordinateFrame', 'frame_transform_graph',
31           'GenericFrame', 'RepresentationMapping']
32
33
34# the graph used for all transformations between frames
35frame_transform_graph = TransformGraph()
36
37
38def _get_repr_cls(value):
39    """
40    Return a valid representation class from ``value`` or raise exception.
41    """
42
43    if value in r.REPRESENTATION_CLASSES:
44        value = r.REPRESENTATION_CLASSES[value]
45    elif (not isinstance(value, type) or
46          not issubclass(value, r.BaseRepresentation)):
47        raise ValueError(
48            'Representation is {!r} but must be a BaseRepresentation class '
49            'or one of the string aliases {}'.format(
50                value, list(r.REPRESENTATION_CLASSES)))
51    return value
52
53
54def _get_diff_cls(value):
55    """
56    Return a valid differential class from ``value`` or raise exception.
57
58    As originally created, this is only used in the SkyCoord initializer, so if
59    that is refactored, this function my no longer be necessary.
60    """
61
62    if value in r.DIFFERENTIAL_CLASSES:
63        value = r.DIFFERENTIAL_CLASSES[value]
64    elif (not isinstance(value, type) or
65          not issubclass(value, r.BaseDifferential)):
66        raise ValueError(
67            'Differential is {!r} but must be a BaseDifferential class '
68            'or one of the string aliases {}'.format(
69                value, list(r.DIFFERENTIAL_CLASSES)))
70    return value
71
72
73def _get_repr_classes(base, **differentials):
74    """Get valid representation and differential classes.
75
76    Parameters
77    ----------
78    base : str or `~astropy.coordinates.BaseRepresentation` subclass
79        class for the representation of the base coordinates.  If a string,
80        it is looked up among the known representation classes.
81    **differentials : dict of str or `~astropy.coordinates.BaseDifferentials`
82        Keys are like for normal differentials, i.e., 's' for a first
83        derivative in time, etc.  If an item is set to `None`, it will be
84        guessed from the base class.
85
86    Returns
87    -------
88    repr_classes : dict of subclasses
89        The base class is keyed by 'base'; the others by the keys of
90        ``diffferentials``.
91    """
92    base = _get_repr_cls(base)
93    repr_classes = {'base': base}
94
95    for name, differential_type in differentials.items():
96        if differential_type == 'base':
97            # We don't want to fail for this case.
98            differential_type = r.DIFFERENTIAL_CLASSES.get(base.get_name(), None)
99
100        elif differential_type in r.DIFFERENTIAL_CLASSES:
101            differential_type = r.DIFFERENTIAL_CLASSES[differential_type]
102
103        elif (differential_type is not None
104              and (not isinstance(differential_type, type)
105                   or not issubclass(differential_type, r.BaseDifferential))):
106            raise ValueError(
107                'Differential is {!r} but must be a BaseDifferential class '
108                'or one of the string aliases {}'.format(
109                    differential_type, list(r.DIFFERENTIAL_CLASSES)))
110        repr_classes[name] = differential_type
111    return repr_classes
112
113
114_RepresentationMappingBase = \
115    namedtuple('RepresentationMapping',
116               ('reprname', 'framename', 'defaultunit'))
117
118
119class RepresentationMapping(_RepresentationMappingBase):
120    """
121    This `~collections.namedtuple` is used with the
122    ``frame_specific_representation_info`` attribute to tell frames what
123    attribute names (and default units) to use for a particular representation.
124    ``reprname`` and ``framename`` should be strings, while ``defaultunit`` can
125    be either an astropy unit, the string ``'recommended'`` (which is degrees
126    for Angles, nothing otherwise), or None (to indicate that no unit mapping
127    should be done).
128    """
129
130    def __new__(cls, reprname, framename, defaultunit='recommended'):
131        # this trick just provides some defaults
132        return super().__new__(cls, reprname, framename, defaultunit)
133
134
135base_doc = """{__doc__}
136    Parameters
137    ----------
138    data : `~astropy.coordinates.BaseRepresentation` subclass instance
139        A representation object or ``None`` to have no data (or use the
140        coordinate component arguments, see below).
141    {components}
142    representation_type : `~astropy.coordinates.BaseRepresentation` subclass, str, optional
143        A representation class or string name of a representation class. This
144        sets the expected input representation class, thereby changing the
145        expected keyword arguments for the data passed in. For example, passing
146        ``representation_type='cartesian'`` will make the classes expect
147        position data with cartesian names, i.e. ``x, y, z`` in most cases
148        unless overridden via ``frame_specific_representation_info``. To see this
149        frame's names, check out ``<this frame>().representation_info``.
150    differential_type : `~astropy.coordinates.BaseDifferential` subclass, str, dict, optional
151        A differential class or dictionary of differential classes (currently
152        only a velocity differential with key 's' is supported). This sets the
153        expected input differential class, thereby changing the expected keyword
154        arguments of the data passed in. For example, passing
155        ``differential_type='cartesian'`` will make the classes expect velocity
156        data with the argument names ``v_x, v_y, v_z`` unless overridden via
157        ``frame_specific_representation_info``. To see this frame's names,
158        check out ``<this frame>().representation_info``.
159    copy : bool, optional
160        If `True` (default), make copies of the input coordinate arrays.
161        Can only be passed in as a keyword argument.
162    {footer}
163"""
164
165_components = """
166    *args, **kwargs
167        Coordinate components, with names that depend on the subclass.
168"""
169
170
171@format_doc(base_doc, components=_components, footer="")
172class BaseCoordinateFrame(ShapedLikeNDArray):
173    """
174    The base class for coordinate frames.
175
176    This class is intended to be subclassed to create instances of specific
177    systems.  Subclasses can implement the following attributes:
178
179    * `default_representation`
180        A subclass of `~astropy.coordinates.BaseRepresentation` that will be
181        treated as the default representation of this frame.  This is the
182        representation assumed by default when the frame is created.
183
184    * `default_differential`
185        A subclass of `~astropy.coordinates.BaseDifferential` that will be
186        treated as the default differential class of this frame.  This is the
187        differential class assumed by default when the frame is created.
188
189    * `~astropy.coordinates.Attribute` class attributes
190       Frame attributes such as ``FK4.equinox`` or ``FK4.obstime`` are defined
191       using a descriptor class.  See the narrative documentation or
192       built-in classes code for details.
193
194    * `frame_specific_representation_info`
195        A dictionary mapping the name or class of a representation to a list of
196        `~astropy.coordinates.RepresentationMapping` objects that tell what
197        names and default units should be used on this frame for the components
198        of that representation.
199
200    Unless overridden via `frame_specific_representation_info`, velocity name
201    defaults are:
202
203      * ``pm_{lon}_cos{lat}``, ``pm_{lat}`` for `SphericalCosLatDifferential`
204        proper motion components
205      * ``pm_{lon}``, ``pm_{lat}`` for `SphericalDifferential` proper motion
206        components
207      * ``radial_velocity`` for any ``d_distance`` component
208      * ``v_{x,y,z}`` for `CartesianDifferential` velocity components
209
210    where ``{lon}`` and ``{lat}`` are the frame names of the angular components.
211    """
212
213    default_representation = None
214    default_differential = None
215
216    # Specifies special names and units for representation and differential
217    # attributes.
218    frame_specific_representation_info = {}
219
220    frame_attributes = {}
221    # Default empty frame_attributes dict
222
223    def __init_subclass__(cls, **kwargs):
224
225        # We first check for explicitly set values for these:
226        default_repr = getattr(cls, 'default_representation', None)
227        default_diff = getattr(cls, 'default_differential', None)
228        repr_info = getattr(cls, 'frame_specific_representation_info', None)
229        # Then, to make sure this works for subclasses-of-subclasses, we also
230        # have to check for cases where the attribute names have already been
231        # replaced by underscore-prefaced equivalents by the logic below:
232        if default_repr is None or isinstance(default_repr, property):
233            default_repr = getattr(cls, '_default_representation', None)
234
235        if default_diff is None or isinstance(default_diff, property):
236            default_diff = getattr(cls, '_default_differential', None)
237
238        if repr_info is None or isinstance(repr_info, property):
239            repr_info = getattr(cls, '_frame_specific_representation_info', None)
240
241        repr_info = cls._infer_repr_info(repr_info)
242
243        # Make read-only properties for the frame class attributes that should
244        # be read-only to make them immutable after creation.
245        # We copy attributes instead of linking to make sure there's no
246        # accidental cross-talk between classes
247        cls._create_readonly_property('default_representation', default_repr,
248                                      'Default representation for position data')
249        cls._create_readonly_property('default_differential', default_diff,
250                                      'Default representation for differential data '
251                                      '(e.g., velocity)')
252        cls._create_readonly_property('frame_specific_representation_info',
253                                      copy.deepcopy(repr_info),
254                                      'Mapping for frame-specific component names')
255
256        # Set the frame attributes. We first construct the attributes from
257        # superclasses, going in reverse order to keep insertion order,
258        # and then add any attributes from the frame now being defined
259        # (if any old definitions are overridden, this keeps the order).
260        # Note that we cannot simply start with the inherited frame_attributes
261        # since we could be a mixin between multiple coordinate frames.
262        # TODO: Should this be made to use readonly_prop_factory as well or
263        # would it be inconvenient for getting the frame_attributes from
264        # classes?
265        frame_attrs = {}
266        for basecls in reversed(cls.__bases__):
267            if issubclass(basecls, BaseCoordinateFrame):
268                frame_attrs.update(basecls.frame_attributes)
269
270        for k, v in cls.__dict__.items():
271            if isinstance(v, Attribute):
272                frame_attrs[k] = v
273
274        cls.frame_attributes = frame_attrs
275
276        # Deal with setting the name of the frame:
277        if not hasattr(cls, 'name'):
278            cls.name = cls.__name__.lower()
279        elif (BaseCoordinateFrame not in cls.__bases__ and
280                cls.name in [getattr(base, 'name', None)
281                             for base in cls.__bases__]):
282            # This may be a subclass of a subclass of BaseCoordinateFrame,
283            # like ICRS(BaseRADecFrame). In this case, cls.name will have been
284            # set by init_subclass
285            cls.name = cls.__name__.lower()
286
287        # A cache that *must be unique to each frame class* - it is
288        # insufficient to share them with superclasses, hence the need to put
289        # them in the meta
290        cls._frame_class_cache = {}
291
292        super().__init_subclass__(**kwargs)
293
294    def __init__(self, *args, copy=True, representation_type=None,
295                 differential_type=None, **kwargs):
296        self._attr_names_with_defaults = []
297
298        self._representation = self._infer_representation(representation_type, differential_type)
299        self._data = self._infer_data(args, copy, kwargs)  # possibly None.
300
301        # Set frame attributes, if any
302
303        values = {}
304        for fnm, fdefault in self.get_frame_attr_names().items():
305            # Read-only frame attributes are defined as FrameAttribute
306            # descriptors which are not settable, so set 'real' attributes as
307            # the name prefaced with an underscore.
308
309            if fnm in kwargs:
310                value = kwargs.pop(fnm)
311                setattr(self, '_' + fnm, value)
312                # Validate attribute by getting it. If the instance has data,
313                # this also checks its shape is OK. If not, we do it below.
314                values[fnm] = getattr(self, fnm)
315            else:
316                setattr(self, '_' + fnm, fdefault)
317                self._attr_names_with_defaults.append(fnm)
318
319        if kwargs:
320            raise TypeError(
321                f'Coordinate frame {self.__class__.__name__} got unexpected '
322                f'keywords: {list(kwargs)}')
323
324        # We do ``is None`` because self._data might evaluate to false for
325        # empty arrays or data == 0
326        if self._data is None:
327            # No data: we still need to check that any non-scalar attributes
328            # have consistent shapes. Collect them for all attributes with
329            # size > 1 (which should be array-like and thus have a shape).
330            shapes = {fnm: value.shape for fnm, value in values.items()
331                      if getattr(value, 'shape', ())}
332            if shapes:
333                if len(shapes) > 1:
334                    try:
335                        self._no_data_shape = check_broadcast(*shapes.values())
336                    except ValueError as err:
337                        raise ValueError(
338                            f"non-scalar attributes with inconsistent shapes: {shapes}") from err
339
340                    # Above, we checked that it is possible to broadcast all
341                    # shapes.  By getting and thus validating the attributes,
342                    # we verify that the attributes can in fact be broadcast.
343                    for fnm in shapes:
344                        getattr(self, fnm)
345                else:
346                    self._no_data_shape = shapes.popitem()[1]
347
348            else:
349                self._no_data_shape = ()
350
351        # The logic of this block is not related to the previous one
352        if self._data is not None:
353            # This makes the cache keys backwards-compatible, but also adds
354            # support for having differentials attached to the frame data
355            # representation object.
356            if 's' in self._data.differentials:
357                # TODO: assumes a velocity unit differential
358                key = (self._data.__class__.__name__,
359                       self._data.differentials['s'].__class__.__name__,
360                       False)
361            else:
362                key = (self._data.__class__.__name__, False)
363
364            # Set up representation cache.
365            self.cache['representation'][key] = self._data
366
367    def _infer_representation(self, representation_type, differential_type):
368        if representation_type is None and differential_type is None:
369            return {'base': self.default_representation, 's': self.default_differential}
370
371        if representation_type is None:
372            representation_type = self.default_representation
373
374        if (inspect.isclass(differential_type)
375                and issubclass(differential_type, r.BaseDifferential)):
376            # TODO: assumes the differential class is for the velocity
377            # differential
378            differential_type = {'s': differential_type}
379
380        elif isinstance(differential_type, str):
381            # TODO: assumes the differential class is for the velocity
382            # differential
383            diff_cls = r.DIFFERENTIAL_CLASSES[differential_type]
384            differential_type = {'s': diff_cls}
385
386        elif differential_type is None:
387            if representation_type == self.default_representation:
388                differential_type = {'s': self.default_differential}
389            else:
390                differential_type = {'s': 'base'}  # see set_representation_cls()
391
392        return _get_repr_classes(representation_type, **differential_type)
393
394    def _infer_data(self, args, copy, kwargs):
395        # if not set below, this is a frame with no data
396        representation_data = None
397        differential_data = None
398
399        args = list(args)  # need to be able to pop them
400        if (len(args) > 0) and (isinstance(args[0], r.BaseRepresentation) or
401                                args[0] is None):
402            representation_data = args.pop(0)  # This can still be None
403            if len(args) > 0:
404                raise TypeError(
405                    'Cannot create a frame with both a representation object '
406                    'and other positional arguments')
407
408            if representation_data is not None:
409                diffs = representation_data.differentials
410                differential_data = diffs.get('s', None)
411                if ((differential_data is None and len(diffs) > 0) or
412                        (differential_data is not None and len(diffs) > 1)):
413                    raise ValueError('Multiple differentials are associated '
414                                     'with the representation object passed in '
415                                     'to the frame initializer. Only a single '
416                                     'velocity differential is supported. Got: '
417                                     '{}'.format(diffs))
418
419        else:
420            representation_cls = self.get_representation_cls()
421            # Get any representation data passed in to the frame initializer
422            # using keyword or positional arguments for the component names
423            repr_kwargs = {}
424            for nmkw, nmrep in self.representation_component_names.items():
425                if len(args) > 0:
426                    # first gather up positional args
427                    repr_kwargs[nmrep] = args.pop(0)
428                elif nmkw in kwargs:
429                    repr_kwargs[nmrep] = kwargs.pop(nmkw)
430
431            # special-case the Spherical->UnitSpherical if no `distance`
432
433            if repr_kwargs:
434                # TODO: determine how to get rid of the part before the "try" -
435                # currently removing it has a performance regression for
436                # unitspherical because of the try-related overhead.
437                # Also frames have no way to indicate what the "distance" is
438                if repr_kwargs.get('distance', True) is None:
439                    del repr_kwargs['distance']
440
441                if (issubclass(representation_cls,
442                               r.SphericalRepresentation)
443                        and 'distance' not in repr_kwargs):
444                    representation_cls = representation_cls._unit_representation
445
446                try:
447                    representation_data = representation_cls(copy=copy,
448                                                             **repr_kwargs)
449                except TypeError as e:
450                    # this except clause is here to make the names of the
451                    # attributes more human-readable.  Without this the names
452                    # come from the representation instead of the frame's
453                    # attribute names.
454                    try:
455                        representation_data = (
456                            representation_cls._unit_representation(
457                                copy=copy, **repr_kwargs))
458                    except Exception:
459                        msg = str(e)
460                        names = self.get_representation_component_names()
461                        for frame_name, repr_name in names.items():
462                            msg = msg.replace(repr_name, frame_name)
463                        msg = msg.replace('__init__()',
464                                          f'{self.__class__.__name__}()')
465                        e.args = (msg,)
466                        raise e
467
468            # Now we handle the Differential data:
469            # Get any differential data passed in to the frame initializer
470            # using keyword or positional arguments for the component names
471            differential_cls = self.get_representation_cls('s')
472            diff_component_names = self.get_representation_component_names('s')
473            diff_kwargs = {}
474            for nmkw, nmrep in diff_component_names.items():
475                if len(args) > 0:
476                    # first gather up positional args
477                    diff_kwargs[nmrep] = args.pop(0)
478                elif nmkw in kwargs:
479                    diff_kwargs[nmrep] = kwargs.pop(nmkw)
480
481            if diff_kwargs:
482                if (hasattr(differential_cls, '_unit_differential')
483                        and 'd_distance' not in diff_kwargs):
484                    differential_cls = differential_cls._unit_differential
485
486                elif len(diff_kwargs) == 1 and 'd_distance' in diff_kwargs:
487                    differential_cls = r.RadialDifferential
488
489                try:
490                    differential_data = differential_cls(copy=copy,
491                                                         **diff_kwargs)
492                except TypeError as e:
493                    # this except clause is here to make the names of the
494                    # attributes more human-readable.  Without this the names
495                    # come from the representation instead of the frame's
496                    # attribute names.
497                    msg = str(e)
498                    names = self.get_representation_component_names('s')
499                    for frame_name, repr_name in names.items():
500                        msg = msg.replace(repr_name, frame_name)
501                    msg = msg.replace('__init__()',
502                                      f'{self.__class__.__name__}()')
503                    e.args = (msg,)
504                    raise
505
506        if len(args) > 0:
507            raise TypeError(
508                '{}.__init__ had {} remaining unhandled arguments'.format(
509                    self.__class__.__name__, len(args)))
510
511        if representation_data is None and differential_data is not None:
512            raise ValueError("Cannot pass in differential component data "
513                             "without positional (representation) data.")
514
515        if differential_data:
516            # Check that differential data provided has units compatible
517            # with time-derivative of representation data.
518            # NOTE: there is no dimensionless time while lengths can be
519            # dimensionless (u.dimensionless_unscaled).
520            for comp in representation_data.components:
521                if (diff_comp := f'd_{comp}') in differential_data.components:
522                    current_repr_unit = representation_data._units[comp]
523                    current_diff_unit = differential_data._units[diff_comp]
524                    expected_unit = current_repr_unit / u.s
525                    if not current_diff_unit.is_equivalent(expected_unit):
526                        for key, val in self.get_representation_component_names().items():
527                            if val == comp:
528                                current_repr_name = key
529                                break
530                        for key, val in self.get_representation_component_names('s').items():
531                            if val == diff_comp:
532                                current_diff_name = key
533                                break
534                        raise ValueError(
535                            f'{current_repr_name} has unit "{current_repr_unit}" with physical '
536                            f'type "{current_repr_unit.physical_type}", but {current_diff_name} '
537                            f'has incompatible unit "{current_diff_unit}" with physical type '
538                            f'"{current_diff_unit.physical_type}" instead of the expected '
539                            f'"{(expected_unit).physical_type}".')
540
541            representation_data = representation_data.with_differentials({'s': differential_data})
542
543        return representation_data
544
545    @classmethod
546    def _infer_repr_info(cls, repr_info):
547        # Unless overridden via `frame_specific_representation_info`, velocity
548        # name defaults are (see also docstring for BaseCoordinateFrame):
549        #   * ``pm_{lon}_cos{lat}``, ``pm_{lat}`` for
550        #     `SphericalCosLatDifferential` proper motion components
551        #   * ``pm_{lon}``, ``pm_{lat}`` for `SphericalDifferential` proper
552        #     motion components
553        #   * ``radial_velocity`` for any `d_distance` component
554        #   * ``v_{x,y,z}`` for `CartesianDifferential` velocity components
555        # where `{lon}` and `{lat}` are the frame names of the angular
556        # components.
557        if repr_info is None:
558            repr_info = {}
559
560        # the tuple() call below is necessary because if it is not there,
561        # the iteration proceeds in a difficult-to-predict manner in the
562        # case that one of the class objects hash is such that it gets
563        # revisited by the iteration.  The tuple() call prevents this by
564        # making the items iterated over fixed regardless of how the dict
565        # changes
566        for cls_or_name in tuple(repr_info.keys()):
567            if isinstance(cls_or_name, str):
568                # TODO: this provides a layer of backwards compatibility in
569                # case the key is a string, but now we want explicit classes.
570                _cls = _get_repr_cls(cls_or_name)
571                repr_info[_cls] = repr_info.pop(cls_or_name)
572
573        # The default spherical names are 'lon' and 'lat'
574        repr_info.setdefault(r.SphericalRepresentation,
575                             [RepresentationMapping('lon', 'lon'),
576                              RepresentationMapping('lat', 'lat')])
577
578        sph_component_map = {m.reprname: m.framename
579                             for m in repr_info[r.SphericalRepresentation]}
580
581        repr_info.setdefault(r.SphericalCosLatDifferential, [
582            RepresentationMapping(
583                'd_lon_coslat',
584                'pm_{lon}_cos{lat}'.format(**sph_component_map),
585                u.mas/u.yr),
586            RepresentationMapping('d_lat',
587                                  'pm_{lat}'.format(**sph_component_map),
588                                  u.mas/u.yr),
589            RepresentationMapping('d_distance', 'radial_velocity',
590                                  u.km/u.s)
591        ])
592
593        repr_info.setdefault(r.SphericalDifferential, [
594            RepresentationMapping('d_lon',
595                                  'pm_{lon}'.format(**sph_component_map),
596                                  u.mas/u.yr),
597            RepresentationMapping('d_lat',
598                                  'pm_{lat}'.format(**sph_component_map),
599                                  u.mas/u.yr),
600            RepresentationMapping('d_distance', 'radial_velocity',
601                                  u.km/u.s)
602        ])
603
604        repr_info.setdefault(r.CartesianDifferential, [
605            RepresentationMapping('d_x', 'v_x', u.km/u.s),
606            RepresentationMapping('d_y', 'v_y', u.km/u.s),
607            RepresentationMapping('d_z', 'v_z', u.km/u.s)])
608
609        # Unit* classes should follow the same naming conventions
610        # TODO: this adds some unnecessary mappings for the Unit classes, so
611        # this could be cleaned up, but in practice doesn't seem to have any
612        # negative side effects
613        repr_info.setdefault(r.UnitSphericalRepresentation,
614                             repr_info[r.SphericalRepresentation])
615
616        repr_info.setdefault(r.UnitSphericalCosLatDifferential,
617                             repr_info[r.SphericalCosLatDifferential])
618
619        repr_info.setdefault(r.UnitSphericalDifferential,
620                             repr_info[r.SphericalDifferential])
621
622        return repr_info
623
624    @classmethod
625    def _create_readonly_property(cls, attr_name, value, doc=None):
626        private_attr = '_' + attr_name
627
628        def getter(self):
629            return getattr(self, private_attr)
630
631        setattr(cls, private_attr, value)
632        setattr(cls, attr_name, property(getter, doc=doc))
633
634    @lazyproperty
635    def cache(self):
636        """
637        Cache for this frame, a dict.  It stores anything that should be
638        computed from the coordinate data (*not* from the frame attributes).
639        This can be used in functions to store anything that might be
640        expensive to compute but might be re-used by some other function.
641        E.g.::
642
643            if 'user_data' in myframe.cache:
644                data = myframe.cache['user_data']
645            else:
646                myframe.cache['user_data'] = data = expensive_func(myframe.lat)
647
648        If in-place modifications are made to the frame data, the cache should
649        be cleared::
650
651            myframe.cache.clear()
652
653        """
654        return defaultdict(dict)
655
656    @property
657    def data(self):
658        """
659        The coordinate data for this object.  If this frame has no data, an
660        `ValueError` will be raised.  Use `has_data` to
661        check if data is present on this frame object.
662        """
663        if self._data is None:
664            raise ValueError('The frame object "{!r}" does not have '
665                             'associated data'.format(self))
666        return self._data
667
668    @property
669    def has_data(self):
670        """
671        True if this frame has `data`, False otherwise.
672        """
673        return self._data is not None
674
675    @property
676    def shape(self):
677        return self.data.shape if self.has_data else self._no_data_shape
678
679    # We have to override the ShapedLikeNDArray definitions, since our shape
680    # does not have to be that of the data.
681    def __len__(self):
682        return len(self.data)
683
684    def __bool__(self):
685        return self.has_data and self.size > 0
686
687    @property
688    def size(self):
689        return self.data.size
690
691    @property
692    def isscalar(self):
693        return self.has_data and self.data.isscalar
694
695    @classmethod
696    def get_frame_attr_names(cls):
697        return {name: getattr(cls, name)
698                for name in cls.frame_attributes}
699
700    def get_representation_cls(self, which='base'):
701        """The class used for part of this frame's data.
702
703        Parameters
704        ----------
705        which : ('base', 's', `None`)
706            The class of which part to return.  'base' means the class used to
707            represent the coordinates; 's' the first derivative to time, i.e.,
708            the class representing the proper motion and/or radial velocity.
709            If `None`, return a dict with both.
710
711        Returns
712        -------
713        representation : `~astropy.coordinates.BaseRepresentation` or `~astropy.coordinates.BaseDifferential`.
714        """
715        if which is not None:
716            return self._representation[which]
717        else:
718            return self._representation
719
720    def set_representation_cls(self, base=None, s='base'):
721        """Set representation and/or differential class for this frame's data.
722
723        Parameters
724        ----------
725        base : str, `~astropy.coordinates.BaseRepresentation` subclass, optional
726            The name or subclass to use to represent the coordinate data.
727        s : `~astropy.coordinates.BaseDifferential` subclass, optional
728            The differential subclass to use to represent any velocities,
729            such as proper motion and radial velocity.  If equal to 'base',
730            which is the default, it will be inferred from the representation.
731            If `None`, the representation will drop any differentials.
732        """
733        if base is None:
734            base = self._representation['base']
735        self._representation = _get_repr_classes(base=base, s=s)
736
737    representation_type = property(
738        fget=get_representation_cls, fset=set_representation_cls,
739        doc="""The representation class used for this frame's data.
740
741        This will be a subclass from `~astropy.coordinates.BaseRepresentation`.
742        Can also be *set* using the string name of the representation. If you
743        wish to set an explicit differential class (rather than have it be
744        inferred), use the ``set_representation_cls`` method.
745        """)
746
747    @property
748    def differential_type(self):
749        """
750        The differential used for this frame's data.
751
752        This will be a subclass from `~astropy.coordinates.BaseDifferential`.
753        For simultaneous setting of representation and differentials, see the
754        ``set_representation_cls`` method.
755        """
756        return self.get_representation_cls('s')
757
758    @differential_type.setter
759    def differential_type(self, value):
760        self.set_representation_cls(s=value)
761
762    @classmethod
763    def _get_representation_info(cls):
764        # This exists as a class method only to support handling frame inputs
765        # without units, which are deprecated and will be removed.  This can be
766        # moved into the representation_info property at that time.
767        # note that if so moved, the cache should be acceessed as
768        # self.__class__._frame_class_cache
769
770        if cls._frame_class_cache.get('last_reprdiff_hash', None) != r.get_reprdiff_cls_hash():
771            repr_attrs = {}
772            for repr_diff_cls in (list(r.REPRESENTATION_CLASSES.values()) +
773                                  list(r.DIFFERENTIAL_CLASSES.values())):
774                repr_attrs[repr_diff_cls] = {'names': [], 'units': []}
775                for c, c_cls in repr_diff_cls.attr_classes.items():
776                    repr_attrs[repr_diff_cls]['names'].append(c)
777                    rec_unit = u.deg if issubclass(c_cls, Angle) else None
778                    repr_attrs[repr_diff_cls]['units'].append(rec_unit)
779
780            for repr_diff_cls, mappings in cls._frame_specific_representation_info.items():
781
782                # take the 'names' and 'units' tuples from repr_attrs,
783                # and then use the RepresentationMapping objects
784                # to update as needed for this frame.
785                nms = repr_attrs[repr_diff_cls]['names']
786                uns = repr_attrs[repr_diff_cls]['units']
787                comptomap = dict([(m.reprname, m) for m in mappings])
788                for i, c in enumerate(repr_diff_cls.attr_classes.keys()):
789                    if c in comptomap:
790                        mapp = comptomap[c]
791                        nms[i] = mapp.framename
792
793                        # need the isinstance because otherwise if it's a unit it
794                        # will try to compare to the unit string representation
795                        if not (isinstance(mapp.defaultunit, str)
796                                and mapp.defaultunit == 'recommended'):
797                            uns[i] = mapp.defaultunit
798                            # else we just leave it as recommended_units says above
799
800                # Convert to tuples so that this can't mess with frame internals
801                repr_attrs[repr_diff_cls]['names'] = tuple(nms)
802                repr_attrs[repr_diff_cls]['units'] = tuple(uns)
803
804            cls._frame_class_cache['representation_info'] = repr_attrs
805            cls._frame_class_cache['last_reprdiff_hash'] = r.get_reprdiff_cls_hash()
806        return cls._frame_class_cache['representation_info']
807
808    @lazyproperty
809    def representation_info(self):
810        """
811        A dictionary with the information of what attribute names for this frame
812        apply to particular representations.
813        """
814        return self._get_representation_info()
815
816    def get_representation_component_names(self, which='base'):
817        out = {}
818        repr_or_diff_cls = self.get_representation_cls(which)
819        if repr_or_diff_cls is None:
820            return out
821        data_names = repr_or_diff_cls.attr_classes.keys()
822        repr_names = self.representation_info[repr_or_diff_cls]['names']
823        for repr_name, data_name in zip(repr_names, data_names):
824            out[repr_name] = data_name
825        return out
826
827    def get_representation_component_units(self, which='base'):
828        out = {}
829        repr_or_diff_cls = self.get_representation_cls(which)
830        if repr_or_diff_cls is None:
831            return out
832        repr_attrs = self.representation_info[repr_or_diff_cls]
833        repr_names = repr_attrs['names']
834        repr_units = repr_attrs['units']
835        for repr_name, repr_unit in zip(repr_names, repr_units):
836            if repr_unit:
837                out[repr_name] = repr_unit
838        return out
839
840    representation_component_names = property(get_representation_component_names)
841
842    representation_component_units = property(get_representation_component_units)
843
844    def _replicate(self, data, copy=False, **kwargs):
845        """Base for replicating a frame, with possibly different attributes.
846
847        Produces a new instance of the frame using the attributes of the old
848        frame (unless overridden) and with the data given.
849
850        Parameters
851        ----------
852        data : `~astropy.coordinates.BaseRepresentation` or None
853            Data to use in the new frame instance.  If `None`, it will be
854            a data-less frame.
855        copy : bool, optional
856            Whether data and the attributes on the old frame should be copied
857            (default), or passed on by reference.
858        **kwargs
859            Any attributes that should be overridden.
860        """
861        # This is to provide a slightly nicer error message if the user tries
862        # to use frame_obj.representation instead of frame_obj.data to get the
863        # underlying representation object [e.g., #2890]
864        if inspect.isclass(data):
865            raise TypeError('Class passed as data instead of a representation '
866                            'instance. If you called frame.representation, this'
867                            ' returns the representation class. frame.data '
868                            'returns the instantiated object - you may want to '
869                            ' use this instead.')
870        if copy and data is not None:
871            data = data.copy()
872
873        for attr in self.get_frame_attr_names():
874            if (attr not in self._attr_names_with_defaults
875                    and attr not in kwargs):
876                value = getattr(self, attr)
877                if copy:
878                    value = value.copy()
879
880                kwargs[attr] = value
881
882        return self.__class__(data, copy=False, **kwargs)
883
884    def replicate(self, copy=False, **kwargs):
885        """
886        Return a replica of the frame, optionally with new frame attributes.
887
888        The replica is a new frame object that has the same data as this frame
889        object and with frame attributes overridden if they are provided as extra
890        keyword arguments to this method. If ``copy`` is set to `True` then a
891        copy of the internal arrays will be made.  Otherwise the replica will
892        use a reference to the original arrays when possible to save memory. The
893        internal arrays are normally not changeable by the user so in most cases
894        it should not be necessary to set ``copy`` to `True`.
895
896        Parameters
897        ----------
898        copy : bool, optional
899            If True, the resulting object is a copy of the data.  When False,
900            references are used where  possible. This rule also applies to the
901            frame attributes.
902
903        Any additional keywords are treated as frame attributes to be set on the
904        new frame object.
905
906        Returns
907        -------
908        frameobj : `BaseCoordinateFrame` subclass instance
909            Replica of this object, but possibly with new frame attributes.
910        """
911        return self._replicate(self.data, copy=copy, **kwargs)
912
913    def replicate_without_data(self, copy=False, **kwargs):
914        """
915        Return a replica without data, optionally with new frame attributes.
916
917        The replica is a new frame object without data but with the same frame
918        attributes as this object, except where overridden by extra keyword
919        arguments to this method.  The ``copy`` keyword determines if the frame
920        attributes are truly copied vs being references (which saves memory for
921        cases where frame attributes are large).
922
923        This method is essentially the converse of `realize_frame`.
924
925        Parameters
926        ----------
927        copy : bool, optional
928            If True, the resulting object has copies of the frame attributes.
929            When False, references are used where  possible.
930
931        Any additional keywords are treated as frame attributes to be set on the
932        new frame object.
933
934        Returns
935        -------
936        frameobj : `BaseCoordinateFrame` subclass instance
937            Replica of this object, but without data and possibly with new frame
938            attributes.
939        """
940        return self._replicate(None, copy=copy, **kwargs)
941
942    def realize_frame(self, data, **kwargs):
943        """
944        Generates a new frame with new data from another frame (which may or
945        may not have data). Roughly speaking, the converse of
946        `replicate_without_data`.
947
948        Parameters
949        ----------
950        data : `~astropy.coordinates.BaseRepresentation`
951            The representation to use as the data for the new frame.
952
953        Any additional keywords are treated as frame attributes to be set on the
954        new frame object. In particular, `representation_type` can be specified.
955
956        Returns
957        -------
958        frameobj : `BaseCoordinateFrame` subclass instance
959            A new object in *this* frame, with the same frame attributes as
960            this one, but with the ``data`` as the coordinate data.
961
962        """
963        return self._replicate(data, **kwargs)
964
965    def represent_as(self, base, s='base', in_frame_units=False):
966        """
967        Generate and return a new representation of this frame's `data`
968        as a Representation object.
969
970        Note: In order to make an in-place change of the representation
971        of a Frame or SkyCoord object, set the ``representation``
972        attribute of that object to the desired new representation, or
973        use the ``set_representation_cls`` method to also set the differential.
974
975        Parameters
976        ----------
977        base : subclass of BaseRepresentation or string
978            The type of representation to generate.  Must be a *class*
979            (not an instance), or the string name of the representation
980            class.
981        s : subclass of `~astropy.coordinates.BaseDifferential`, str, optional
982            Class in which any velocities should be represented. Must be
983            a *class* (not an instance), or the string name of the
984            differential class.  If equal to 'base' (default), inferred from
985            the base class.  If `None`, all velocity information is dropped.
986        in_frame_units : bool, keyword-only
987            Force the representation units to match the specified units
988            particular to this frame
989
990        Returns
991        -------
992        newrep : BaseRepresentation-derived object
993            A new representation object of this frame's `data`.
994
995        Raises
996        ------
997        AttributeError
998            If this object had no `data`
999
1000        Examples
1001        --------
1002        >>> from astropy import units as u
1003        >>> from astropy.coordinates import SkyCoord, CartesianRepresentation
1004        >>> coord = SkyCoord(0*u.deg, 0*u.deg)
1005        >>> coord.represent_as(CartesianRepresentation)  # doctest: +FLOAT_CMP
1006        <CartesianRepresentation (x, y, z) [dimensionless]
1007                (1., 0., 0.)>
1008
1009        >>> coord.representation_type = CartesianRepresentation
1010        >>> coord  # doctest: +FLOAT_CMP
1011        <SkyCoord (ICRS): (x, y, z) [dimensionless]
1012            (1., 0., 0.)>
1013        """
1014
1015        # For backwards compatibility (because in_frame_units used to be the
1016        # 2nd argument), we check to see if `new_differential` is a boolean. If
1017        # it is, we ignore the value of `new_differential` and warn about the
1018        # position change
1019        if isinstance(s, bool):
1020            warnings.warn("The argument position for `in_frame_units` in "
1021                          "`represent_as` has changed. Use as a keyword "
1022                          "argument if needed.", AstropyWarning)
1023            in_frame_units = s
1024            s = 'base'
1025
1026        # In the future, we may want to support more differentials, in which
1027        # case one probably needs to define **kwargs above and use it here.
1028        # But for now, we only care about the velocity.
1029        repr_classes = _get_repr_classes(base=base, s=s)
1030        representation_cls = repr_classes['base']
1031        # We only keep velocity information
1032        if 's' in self.data.differentials:
1033            # For the default 'base' option in which _get_repr_classes has
1034            # given us a best guess based on the representation class, we only
1035            # use it if the class we had already is incompatible.
1036            if (s == 'base'
1037                and (self.data.differentials['s'].__class__
1038                     in representation_cls._compatible_differentials)):
1039                differential_cls = self.data.differentials['s'].__class__
1040            else:
1041                differential_cls = repr_classes['s']
1042        elif s is None or s == 'base':
1043            differential_cls = None
1044        else:
1045            raise TypeError('Frame data has no associated differentials '
1046                            '(i.e. the frame has no velocity data) - '
1047                            'represent_as() only accepts a new '
1048                            'representation.')
1049
1050        if differential_cls:
1051            cache_key = (representation_cls.__name__,
1052                         differential_cls.__name__, in_frame_units)
1053        else:
1054            cache_key = (representation_cls.__name__, in_frame_units)
1055
1056        cached_repr = self.cache['representation'].get(cache_key)
1057        if not cached_repr:
1058            if differential_cls:
1059                # Sanity check to ensure we do not just drop radial
1060                # velocity.  TODO: should Representation.represent_as
1061                # allow this transformation in the first place?
1062                if (isinstance(self.data, r.UnitSphericalRepresentation)
1063                    and issubclass(representation_cls, r.CartesianRepresentation)
1064                    and not isinstance(self.data.differentials['s'],
1065                                       (r.UnitSphericalDifferential,
1066                                        r.UnitSphericalCosLatDifferential,
1067                                        r.RadialDifferential))):
1068                    raise u.UnitConversionError(
1069                        'need a distance to retrieve a cartesian representation '
1070                        'when both radial velocity and proper motion are present, '
1071                        'since otherwise the units cannot match.')
1072
1073                # TODO NOTE: only supports a single differential
1074                data = self.data.represent_as(representation_cls,
1075                                              differential_cls)
1076                diff = data.differentials['s']  # TODO: assumes velocity
1077            else:
1078                data = self.data.represent_as(representation_cls)
1079
1080            # If the new representation is known to this frame and has a defined
1081            # set of names and units, then use that.
1082            new_attrs = self.representation_info.get(representation_cls)
1083            if new_attrs and in_frame_units:
1084                datakwargs = dict((comp, getattr(data, comp))
1085                                  for comp in data.components)
1086                for comp, new_attr_unit in zip(data.components, new_attrs['units']):
1087                    if new_attr_unit:
1088                        datakwargs[comp] = datakwargs[comp].to(new_attr_unit)
1089                data = data.__class__(copy=False, **datakwargs)
1090
1091            if differential_cls:
1092                # the original differential
1093                data_diff = self.data.differentials['s']
1094
1095                # If the new differential is known to this frame and has a
1096                # defined set of names and units, then use that.
1097                new_attrs = self.representation_info.get(differential_cls)
1098                if new_attrs and in_frame_units:
1099                    diffkwargs = dict((comp, getattr(diff, comp))
1100                                      for comp in diff.components)
1101                    for comp, new_attr_unit in zip(diff.components,
1102                                                   new_attrs['units']):
1103                        # Some special-casing to treat a situation where the
1104                        # input data has a UnitSphericalDifferential or a
1105                        # RadialDifferential. It is re-represented to the
1106                        # frame's differential class (which might be, e.g., a
1107                        # dimensional Differential), so we don't want to try to
1108                        # convert the empty component units
1109                        if (isinstance(data_diff,
1110                                       (r.UnitSphericalDifferential,
1111                                        r.UnitSphericalCosLatDifferential))
1112                                and comp not in data_diff.__class__.attr_classes):
1113                            continue
1114
1115                        elif (isinstance(data_diff, r.RadialDifferential)
1116                              and comp not in data_diff.__class__.attr_classes):
1117                            continue
1118
1119                        # Try to convert to requested units. Since that might
1120                        # not be possible (e.g., for a coordinate with proper
1121                        # motion but without distance, one cannot convert to a
1122                        # cartesian differential in km/s), we allow the unit
1123                        # conversion to fail.  See gh-7028 for discussion.
1124                        if new_attr_unit and hasattr(diff, comp):
1125                            try:
1126                                diffkwargs[comp] = diffkwargs[comp].to(new_attr_unit)
1127                            except Exception:
1128                                pass
1129
1130                    diff = diff.__class__(copy=False, **diffkwargs)
1131
1132                    # Here we have to bypass using with_differentials() because
1133                    # it has a validation check. But because
1134                    # .representation_type and .differential_type don't point to
1135                    # the original classes, if the input differential is a
1136                    # RadialDifferential, it usually gets turned into a
1137                    # SphericalCosLatDifferential (or whatever the default is)
1138                    # with strange units for the d_lon and d_lat attributes.
1139                    # This then causes the dictionary key check to fail (i.e.
1140                    # comparison against `diff._get_deriv_key()`)
1141                    data._differentials.update({'s': diff})
1142
1143            self.cache['representation'][cache_key] = data
1144
1145        return self.cache['representation'][cache_key]
1146
1147    def transform_to(self, new_frame):
1148        """
1149        Transform this object's coordinate data to a new frame.
1150
1151        Parameters
1152        ----------
1153        new_frame : coordinate-like or `BaseCoordinateFrame` subclass instance
1154            The frame to transform this coordinate frame into.
1155            The frame class option is deprecated.
1156
1157        Returns
1158        -------
1159        transframe : coordinate-like
1160            A new object with the coordinate data represented in the
1161            ``newframe`` system.
1162
1163        Raises
1164        ------
1165        ValueError
1166            If there is no possible transformation route.
1167        """
1168        from .errors import ConvertError
1169
1170        if self._data is None:
1171            raise ValueError('Cannot transform a frame with no data')
1172
1173        if (getattr(self.data, 'differentials', None)
1174                and hasattr(self, 'obstime') and hasattr(new_frame, 'obstime')
1175                and np.any(self.obstime != new_frame.obstime)):
1176            raise NotImplementedError('You cannot transform a frame that has '
1177                                      'velocities to another frame at a '
1178                                      'different obstime. If you think this '
1179                                      'should (or should not) be possible, '
1180                                      'please comment at https://github.com/astropy/astropy/issues/6280')
1181
1182        if inspect.isclass(new_frame):
1183            warnings.warn("Transforming a frame instance to a frame class (as opposed to another "
1184                          "frame instance) will not be supported in the future.  Either "
1185                          "explicitly instantiate the target frame, or first convert the source "
1186                          "frame instance to a `astropy.coordinates.SkyCoord` and use its "
1187                          "`transform_to()` method.",
1188                          AstropyDeprecationWarning)
1189            # Use the default frame attributes for this class
1190            new_frame = new_frame()
1191
1192        if hasattr(new_frame, '_sky_coord_frame'):
1193            # Input new_frame is not a frame instance or class and is most
1194            # likely a SkyCoord object.
1195            new_frame = new_frame._sky_coord_frame
1196
1197        trans = frame_transform_graph.get_transform(self.__class__,
1198                                                    new_frame.__class__)
1199        if trans is None:
1200            if new_frame is self.__class__:
1201                # no special transform needed, but should update frame info
1202                return new_frame.realize_frame(self.data)
1203            msg = 'Cannot transform from {0} to {1}'
1204            raise ConvertError(msg.format(self.__class__, new_frame.__class__))
1205        return trans(self, new_frame)
1206
1207    def is_transformable_to(self, new_frame):
1208        """
1209        Determines if this coordinate frame can be transformed to another
1210        given frame.
1211
1212        Parameters
1213        ----------
1214        new_frame : `BaseCoordinateFrame` subclass or instance
1215            The proposed frame to transform into.
1216
1217        Returns
1218        -------
1219        transformable : bool or str
1220            `True` if this can be transformed to ``new_frame``, `False` if
1221            not, or the string 'same' if ``new_frame`` is the same system as
1222            this object but no transformation is defined.
1223
1224        Notes
1225        -----
1226        A return value of 'same' means the transformation will work, but it will
1227        just give back a copy of this object.  The intended usage is::
1228
1229            if coord.is_transformable_to(some_unknown_frame):
1230                coord2 = coord.transform_to(some_unknown_frame)
1231
1232        This will work even if ``some_unknown_frame``  turns out to be the same
1233        frame class as ``coord``.  This is intended for cases where the frame
1234        is the same regardless of the frame attributes (e.g. ICRS), but be
1235        aware that it *might* also indicate that someone forgot to define the
1236        transformation between two objects of the same frame class but with
1237        different attributes.
1238        """
1239        new_frame_cls = new_frame if inspect.isclass(new_frame) else new_frame.__class__
1240        trans = frame_transform_graph.get_transform(self.__class__, new_frame_cls)
1241
1242        if trans is None:
1243            if new_frame_cls is self.__class__:
1244                return 'same'
1245            else:
1246                return False
1247        else:
1248            return True
1249
1250    def is_frame_attr_default(self, attrnm):
1251        """
1252        Determine whether or not a frame attribute has its value because it's
1253        the default value, or because this frame was created with that value
1254        explicitly requested.
1255
1256        Parameters
1257        ----------
1258        attrnm : str
1259            The name of the attribute to check.
1260
1261        Returns
1262        -------
1263        isdefault : bool
1264            True if the attribute ``attrnm`` has its value by default, False if
1265            it was specified at creation of this frame.
1266        """
1267        return attrnm in self._attr_names_with_defaults
1268
1269    @staticmethod
1270    def _frameattr_equiv(left_fattr, right_fattr):
1271        """
1272        Determine if two frame attributes are equivalent.  Implemented as a
1273        staticmethod mainly as a convenient location, although conceivable it
1274        might be desirable for subclasses to override this behavior.
1275
1276        Primary purpose is to check for equality of representations.  This
1277        aspect can actually be simplified/removed now that representations have
1278        equality defined.
1279
1280        Secondary purpose is to check for equality of coordinate attributes,
1281        which first checks whether they themselves are in equivalent frames
1282        before checking for equality in the normal fashion.  This is because
1283        checking for equality with non-equivalent frames raises an error.
1284        """
1285        if left_fattr is right_fattr:
1286            # shortcut if it's exactly the same object
1287            return True
1288        elif left_fattr is None or right_fattr is None:
1289            # shortcut if one attribute is unspecified and the other isn't
1290            return False
1291
1292        left_is_repr = isinstance(left_fattr, r.BaseRepresentationOrDifferential)
1293        right_is_repr = isinstance(right_fattr, r.BaseRepresentationOrDifferential)
1294        if left_is_repr and right_is_repr:
1295            # both are representations.
1296            if (getattr(left_fattr, 'differentials', False) or
1297                    getattr(right_fattr, 'differentials', False)):
1298                warnings.warn('Two representation frame attributes were '
1299                              'checked for equivalence when at least one of'
1300                              ' them has differentials.  This yields False '
1301                              'even if the underlying representations are '
1302                              'equivalent (although this may change in '
1303                              'future versions of Astropy)', AstropyWarning)
1304                return False
1305            if isinstance(right_fattr, left_fattr.__class__):
1306                # if same representation type, compare components.
1307                return np.all([(getattr(left_fattr, comp) ==
1308                                getattr(right_fattr, comp))
1309                               for comp in left_fattr.components])
1310            else:
1311                # convert to cartesian and see if they match
1312                return np.all(left_fattr.to_cartesian().xyz ==
1313                              right_fattr.to_cartesian().xyz)
1314        elif left_is_repr or right_is_repr:
1315            return False
1316
1317        left_is_coord = isinstance(left_fattr, BaseCoordinateFrame)
1318        right_is_coord = isinstance(right_fattr, BaseCoordinateFrame)
1319        if left_is_coord and right_is_coord:
1320            # both are coordinates
1321            if left_fattr.is_equivalent_frame(right_fattr):
1322                return np.all(left_fattr == right_fattr)
1323            else:
1324                return False
1325        elif left_is_coord or right_is_coord:
1326            return False
1327
1328        return np.all(left_fattr == right_fattr)
1329
1330    def is_equivalent_frame(self, other):
1331        """
1332        Checks if this object is the same frame as the ``other`` object.
1333
1334        To be the same frame, two objects must be the same frame class and have
1335        the same frame attributes.  Note that it does *not* matter what, if any,
1336        data either object has.
1337
1338        Parameters
1339        ----------
1340        other : :class:`~astropy.coordinates.BaseCoordinateFrame`
1341            the other frame to check
1342
1343        Returns
1344        -------
1345        isequiv : bool
1346            True if the frames are the same, False if not.
1347
1348        Raises
1349        ------
1350        TypeError
1351            If ``other`` isn't a `BaseCoordinateFrame` or subclass.
1352        """
1353        if self.__class__ == other.__class__:
1354            for frame_attr_name in self.get_frame_attr_names():
1355                if not self._frameattr_equiv(getattr(self, frame_attr_name),
1356                                             getattr(other, frame_attr_name)):
1357                    return False
1358            return True
1359        elif not isinstance(other, BaseCoordinateFrame):
1360            raise TypeError("Tried to do is_equivalent_frame on something that "
1361                            "isn't a frame")
1362        else:
1363            return False
1364
1365    def __repr__(self):
1366        frameattrs = self._frame_attrs_repr()
1367        data_repr = self._data_repr()
1368
1369        if frameattrs:
1370            frameattrs = f' ({frameattrs})'
1371
1372        if data_repr:
1373            return f'<{self.__class__.__name__} Coordinate{frameattrs}: {data_repr}>'
1374        else:
1375            return f'<{self.__class__.__name__} Frame{frameattrs}>'
1376
1377    def _data_repr(self):
1378        """Returns a string representation of the coordinate data."""
1379
1380        if not self.has_data:
1381            return ''
1382
1383        if self.representation_type:
1384            if (hasattr(self.representation_type, '_unit_representation')
1385                    and isinstance(self.data,
1386                                   self.representation_type._unit_representation)):
1387                rep_cls = self.data.__class__
1388            else:
1389                rep_cls = self.representation_type
1390
1391            if 's' in self.data.differentials:
1392                dif_cls = self.get_representation_cls('s')
1393                dif_data = self.data.differentials['s']
1394                if isinstance(dif_data, (r.UnitSphericalDifferential,
1395                                         r.UnitSphericalCosLatDifferential,
1396                                         r.RadialDifferential)):
1397                    dif_cls = dif_data.__class__
1398
1399            else:
1400                dif_cls = None
1401
1402            data = self.represent_as(rep_cls, dif_cls, in_frame_units=True)
1403
1404            data_repr = repr(data)
1405            # Generate the list of component names out of the repr string
1406            part1, _, remainder = data_repr.partition('(')
1407            if remainder != '':
1408                comp_str, _, part2 = remainder.partition(')')
1409                comp_names = comp_str.split(', ')
1410                # Swap in frame-specific component names
1411                invnames = dict([(nmrepr, nmpref) for nmpref, nmrepr
1412                                 in self.representation_component_names.items()])
1413                for i, name in enumerate(comp_names):
1414                    comp_names[i] = invnames.get(name, name)
1415                # Reassemble the repr string
1416                data_repr = part1 + '(' + ', '.join(comp_names) + ')' + part2
1417
1418        else:
1419            data = self.data
1420            data_repr = repr(self.data)
1421
1422        if data_repr.startswith('<' + data.__class__.__name__):
1423            # remove both the leading "<" and the space after the name, as well
1424            # as the trailing ">"
1425            data_repr = data_repr[(len(data.__class__.__name__) + 2):-1]
1426        else:
1427            data_repr = 'Data:\n' + data_repr
1428
1429        if 's' in self.data.differentials:
1430            data_repr_spl = data_repr.split('\n')
1431            if 'has differentials' in data_repr_spl[-1]:
1432                diffrepr = repr(data.differentials['s']).split('\n')
1433                if diffrepr[0].startswith('<'):
1434                    diffrepr[0] = ' ' + ' '.join(diffrepr[0].split(' ')[1:])
1435                for frm_nm, rep_nm in self.get_representation_component_names('s').items():
1436                    diffrepr[0] = diffrepr[0].replace(rep_nm, frm_nm)
1437                if diffrepr[-1].endswith('>'):
1438                    diffrepr[-1] = diffrepr[-1][:-1]
1439                data_repr_spl[-1] = '\n'.join(diffrepr)
1440
1441            data_repr = '\n'.join(data_repr_spl)
1442
1443        return data_repr
1444
1445    def _frame_attrs_repr(self):
1446        """
1447        Returns a string representation of the frame's attributes, if any.
1448        """
1449        attr_strs = []
1450        for attribute_name in self.get_frame_attr_names():
1451            attr = getattr(self, attribute_name)
1452            # Check to see if this object has a way of representing itself
1453            # specific to being an attribute of a frame. (Note, this is not the
1454            # Attribute class, it's the actual object).
1455            if hasattr(attr, "_astropy_repr_in_frame"):
1456                attrstr = attr._astropy_repr_in_frame()
1457            else:
1458                attrstr = str(attr)
1459            attr_strs.append(f"{attribute_name}={attrstr}")
1460
1461        return ', '.join(attr_strs)
1462
1463    def _apply(self, method, *args, **kwargs):
1464        """Create a new instance, applying a method to the underlying data.
1465
1466        In typical usage, the method is any of the shape-changing methods for
1467        `~numpy.ndarray` (``reshape``, ``swapaxes``, etc.), as well as those
1468        picking particular elements (``__getitem__``, ``take``, etc.), which
1469        are all defined in `~astropy.utils.shapes.ShapedLikeNDArray`. It will be
1470        applied to the underlying arrays in the representation (e.g., ``x``,
1471        ``y``, and ``z`` for `~astropy.coordinates.CartesianRepresentation`),
1472        as well as to any frame attributes that have a shape, with the results
1473        used to create a new instance.
1474
1475        Internally, it is also used to apply functions to the above parts
1476        (in particular, `~numpy.broadcast_to`).
1477
1478        Parameters
1479        ----------
1480        method : str or callable
1481            If str, it is the name of a method that is applied to the internal
1482            ``components``. If callable, the function is applied.
1483        *args : tuple
1484            Any positional arguments for ``method``.
1485        **kwargs : dict
1486            Any keyword arguments for ``method``.
1487        """
1488        def apply_method(value):
1489            if isinstance(value, ShapedLikeNDArray):
1490                return value._apply(method, *args, **kwargs)
1491            else:
1492                if callable(method):
1493                    return method(value, *args, **kwargs)
1494                else:
1495                    return getattr(value, method)(*args, **kwargs)
1496
1497        new = super().__new__(self.__class__)
1498        if hasattr(self, '_representation'):
1499            new._representation = self._representation.copy()
1500        new._attr_names_with_defaults = self._attr_names_with_defaults.copy()
1501
1502        for attr in self.frame_attributes:
1503            _attr = '_' + attr
1504            if attr in self._attr_names_with_defaults:
1505                setattr(new, _attr, getattr(self, _attr))
1506            else:
1507                value = getattr(self, _attr)
1508                if getattr(value, 'shape', ()):
1509                    value = apply_method(value)
1510                elif method == 'copy' or method == 'flatten':
1511                    # flatten should copy also for a single element array, but
1512                    # we cannot use it directly for array scalars, since it
1513                    # always returns a one-dimensional array. So, just copy.
1514                    value = copy.copy(value)
1515
1516                setattr(new, _attr, value)
1517
1518        if self.has_data:
1519            new._data = apply_method(self.data)
1520        else:
1521            new._data = None
1522            shapes = [getattr(new, '_' + attr).shape
1523                      for attr in new.frame_attributes
1524                      if (attr not in new._attr_names_with_defaults
1525                          and getattr(getattr(new, '_' + attr), 'shape', ()))]
1526            if shapes:
1527                new._no_data_shape = (check_broadcast(*shapes)
1528                                      if len(shapes) > 1 else shapes[0])
1529            else:
1530                new._no_data_shape = ()
1531
1532        return new
1533
1534    def __setitem__(self, item, value):
1535        if self.__class__ is not value.__class__:
1536            raise TypeError(f'can only set from object of same class: '
1537                            f'{self.__class__.__name__} vs. '
1538                            f'{value.__class__.__name__}')
1539
1540        if not self.is_equivalent_frame(value):
1541            raise ValueError('can only set frame item from an equivalent frame')
1542
1543        if value._data is None:
1544            raise ValueError('can only set frame with value that has data')
1545
1546        if self._data is None:
1547            raise ValueError('cannot set frame which has no data')
1548
1549        if self.shape == ():
1550            raise TypeError(f"scalar '{self.__class__.__name__}' frame object "
1551                            f"does not support item assignment")
1552
1553        if self._data is None:
1554            raise ValueError('can only set frame if it has data')
1555
1556        if self._data.__class__ is not value._data.__class__:
1557            raise TypeError(f'can only set from object of same class: '
1558                            f'{self._data.__class__.__name__} vs. '
1559                            f'{value._data.__class__.__name__}')
1560
1561        if self._data._differentials:
1562            # Can this ever occur? (Same class but different differential keys).
1563            # This exception is not tested since it is not clear how to generate it.
1564            if self._data._differentials.keys() != value._data._differentials.keys():
1565                raise ValueError(f'setitem value must have same differentials')
1566
1567            for key, self_diff in self._data._differentials.items():
1568                if self_diff.__class__ is not value._data._differentials[key].__class__:
1569                    raise TypeError(f'can only set from object of same class: '
1570                                    f'{self_diff.__class__.__name__} vs. '
1571                                    f'{value._data._differentials[key].__class__.__name__}')
1572
1573        # Set representation data
1574        self._data[item] = value._data
1575
1576        # Frame attributes required to be identical by is_equivalent_frame,
1577        # no need to set them here.
1578
1579        self.cache.clear()
1580
1581    @override__dir__
1582    def __dir__(self):
1583        """
1584        Override the builtin `dir` behavior to include representation
1585        names.
1586
1587        TODO: dynamic representation transforms (i.e. include cylindrical et al.).
1588        """
1589        dir_values = set(self.representation_component_names)
1590        dir_values |= set(self.get_representation_component_names('s'))
1591
1592        return dir_values
1593
1594    def __getattr__(self, attr):
1595        """
1596        Allow access to attributes on the representation and differential as
1597        found via ``self.get_representation_component_names``.
1598
1599        TODO: We should handle dynamic representation transforms here (e.g.,
1600        `.cylindrical`) instead of defining properties as below.
1601        """
1602
1603        # attr == '_representation' is likely from the hasattr() test in the
1604        # representation property which is used for
1605        # self.representation_component_names.
1606        #
1607        # Prevent infinite recursion here.
1608        if attr.startswith('_'):
1609            return self.__getattribute__(attr)  # Raise AttributeError.
1610
1611        repr_names = self.representation_component_names
1612        if attr in repr_names:
1613            if self._data is None:
1614                self.data  # this raises the "no data" error by design - doing it
1615                # this way means we don't have to replicate the error message here
1616
1617            rep = self.represent_as(self.representation_type,
1618                                    in_frame_units=True)
1619            val = getattr(rep, repr_names[attr])
1620            return val
1621
1622        diff_names = self.get_representation_component_names('s')
1623        if attr in diff_names:
1624            if self._data is None:
1625                self.data  # see above.
1626            # TODO: this doesn't work for the case when there is only
1627            # unitspherical information. The differential_type gets set to the
1628            # default_differential, which expects full information, so the
1629            # units don't work out
1630            rep = self.represent_as(in_frame_units=True,
1631                                    **self.get_representation_cls(None))
1632            val = getattr(rep.differentials['s'], diff_names[attr])
1633            return val
1634
1635        return self.__getattribute__(attr)  # Raise AttributeError.
1636
1637    def __setattr__(self, attr, value):
1638        # Don't slow down access of private attributes!
1639        if not attr.startswith('_'):
1640            if hasattr(self, 'representation_info'):
1641                repr_attr_names = set()
1642                for representation_attr in self.representation_info.values():
1643                    repr_attr_names.update(representation_attr['names'])
1644
1645                if attr in repr_attr_names:
1646                    raise AttributeError(
1647                        f'Cannot set any frame attribute {attr}')
1648
1649        super().__setattr__(attr, value)
1650
1651    def __eq__(self, value):
1652        """Equality operator for frame.
1653
1654        This implements strict equality and requires that the frames are
1655        equivalent and that the representation data are exactly equal.
1656        """
1657        is_equiv = self.is_equivalent_frame(value)
1658
1659        if self._data is None and value._data is None:
1660            # For Frame with no data, == compare is same as is_equivalent_frame()
1661            return is_equiv
1662
1663        if not is_equiv:
1664            raise TypeError(f'cannot compare: objects must have equivalent frames: '
1665                            f'{self.replicate_without_data()} vs. '
1666                            f'{value.replicate_without_data()}')
1667
1668        if ((value._data is None and self._data is not None)
1669                or (self._data is None and value._data is not None)):
1670            raise ValueError('cannot compare: one frame has data and the other '
1671                             'does not')
1672
1673        return self._data == value._data
1674
1675    def __ne__(self, value):
1676        return np.logical_not(self == value)
1677
1678    def separation(self, other):
1679        """
1680        Computes on-sky separation between this coordinate and another.
1681
1682        .. note::
1683
1684            If the ``other`` coordinate object is in a different frame, it is
1685            first transformed to the frame of this object. This can lead to
1686            unintuitive behavior if not accounted for. Particularly of note is
1687            that ``self.separation(other)`` and ``other.separation(self)`` may
1688            not give the same answer in this case.
1689
1690        Parameters
1691        ----------
1692        other : `~astropy.coordinates.BaseCoordinateFrame`
1693            The coordinate to get the separation to.
1694
1695        Returns
1696        -------
1697        sep : `~astropy.coordinates.Angle`
1698            The on-sky separation between this and the ``other`` coordinate.
1699
1700        Notes
1701        -----
1702        The separation is calculated using the Vincenty formula, which
1703        is stable at all locations, including poles and antipodes [1]_.
1704
1705        .. [1] https://en.wikipedia.org/wiki/Great-circle_distance
1706
1707        """
1708        from .angle_utilities import angular_separation
1709        from .angles import Angle
1710
1711        self_unit_sph = self.represent_as(r.UnitSphericalRepresentation)
1712        other_transformed = other.transform_to(self)
1713        other_unit_sph = other_transformed.represent_as(r.UnitSphericalRepresentation)
1714
1715        # Get the separation as a Quantity, convert to Angle in degrees
1716        sep = angular_separation(self_unit_sph.lon, self_unit_sph.lat,
1717                                 other_unit_sph.lon, other_unit_sph.lat)
1718        return Angle(sep, unit=u.degree)
1719
1720    def separation_3d(self, other):
1721        """
1722        Computes three dimensional separation between this coordinate
1723        and another.
1724
1725        Parameters
1726        ----------
1727        other : `~astropy.coordinates.BaseCoordinateFrame`
1728            The coordinate system to get the distance to.
1729
1730        Returns
1731        -------
1732        sep : `~astropy.coordinates.Distance`
1733            The real-space distance between these two coordinates.
1734
1735        Raises
1736        ------
1737        ValueError
1738            If this or the other coordinate do not have distances.
1739        """
1740
1741        from .distances import Distance
1742
1743        if issubclass(self.data.__class__, r.UnitSphericalRepresentation):
1744            raise ValueError('This object does not have a distance; cannot '
1745                             'compute 3d separation.')
1746
1747        # do this first just in case the conversion somehow creates a distance
1748        other_in_self_system = other.transform_to(self)
1749
1750        if issubclass(other_in_self_system.__class__, r.UnitSphericalRepresentation):
1751            raise ValueError('The other object does not have a distance; '
1752                             'cannot compute 3d separation.')
1753
1754        # drop the differentials to ensure they don't do anything odd in the
1755        # subtraction
1756        self_car = self.data.without_differentials().represent_as(r.CartesianRepresentation)
1757        other_car = other_in_self_system.data.without_differentials().represent_as(r.CartesianRepresentation)
1758        dist = (self_car - other_car).norm()
1759        if dist.unit == u.one:
1760            return dist
1761        else:
1762            return Distance(dist)
1763
1764    @property
1765    def cartesian(self):
1766        """
1767        Shorthand for a cartesian representation of the coordinates in this
1768        object.
1769        """
1770
1771        # TODO: if representations are updated to use a full transform graph,
1772        #       the representation aliases should not be hard-coded like this
1773        return self.represent_as('cartesian', in_frame_units=True)
1774
1775    @property
1776    def cylindrical(self):
1777        """
1778        Shorthand for a cylindrical representation of the coordinates in this
1779        object.
1780        """
1781
1782        # TODO: if representations are updated to use a full transform graph,
1783        #       the representation aliases should not be hard-coded like this
1784        return self.represent_as('cylindrical', in_frame_units=True)
1785
1786    @property
1787    def spherical(self):
1788        """
1789        Shorthand for a spherical representation of the coordinates in this
1790        object.
1791        """
1792
1793        # TODO: if representations are updated to use a full transform graph,
1794        #       the representation aliases should not be hard-coded like this
1795        return self.represent_as('spherical', in_frame_units=True)
1796
1797    @property
1798    def sphericalcoslat(self):
1799        """
1800        Shorthand for a spherical representation of the positional data and a
1801        `SphericalCosLatDifferential` for the velocity data in this object.
1802        """
1803
1804        # TODO: if representations are updated to use a full transform graph,
1805        #       the representation aliases should not be hard-coded like this
1806        return self.represent_as('spherical', 'sphericalcoslat',
1807                                 in_frame_units=True)
1808
1809    @property
1810    def velocity(self):
1811        """
1812        Shorthand for retrieving the Cartesian space-motion as a
1813        `CartesianDifferential` object. This is equivalent to calling
1814        ``self.cartesian.differentials['s']``.
1815        """
1816        if 's' not in self.data.differentials:
1817            raise ValueError('Frame has no associated velocity (Differential) '
1818                             'data information.')
1819
1820        return self.cartesian.differentials['s']
1821
1822    @property
1823    def proper_motion(self):
1824        """
1825        Shorthand for the two-dimensional proper motion as a
1826        `~astropy.units.Quantity` object with angular velocity units. In the
1827        returned `~astropy.units.Quantity`, ``axis=0`` is the longitude/latitude
1828        dimension so that ``.proper_motion[0]`` is the longitudinal proper
1829        motion and ``.proper_motion[1]`` is latitudinal. The longitudinal proper
1830        motion already includes the cos(latitude) term.
1831        """
1832        if 's' not in self.data.differentials:
1833            raise ValueError('Frame has no associated velocity (Differential) '
1834                             'data information.')
1835
1836        sph = self.represent_as('spherical', 'sphericalcoslat',
1837                                in_frame_units=True)
1838        pm_lon = sph.differentials['s'].d_lon_coslat
1839        pm_lat = sph.differentials['s'].d_lat
1840        return np.stack((pm_lon.value,
1841                         pm_lat.to(pm_lon.unit).value), axis=0) * pm_lon.unit
1842
1843    @property
1844    def radial_velocity(self):
1845        """
1846        Shorthand for the radial or line-of-sight velocity as a
1847        `~astropy.units.Quantity` object.
1848        """
1849        if 's' not in self.data.differentials:
1850            raise ValueError('Frame has no associated velocity (Differential) '
1851                             'data information.')
1852
1853        sph = self.represent_as('spherical', in_frame_units=True)
1854        return sph.differentials['s'].d_distance
1855
1856
1857class GenericFrame(BaseCoordinateFrame):
1858    """
1859    A frame object that can't store data but can hold any arbitrary frame
1860    attributes. Mostly useful as a utility for the high-level class to store
1861    intermediate frame attributes.
1862
1863    Parameters
1864    ----------
1865    frame_attrs : dict
1866        A dictionary of attributes to be used as the frame attributes for this
1867        frame.
1868    """
1869
1870    name = None  # it's not a "real" frame so it doesn't have a name
1871
1872    def __init__(self, frame_attrs):
1873        self.frame_attributes = {}
1874        for name, default in frame_attrs.items():
1875            self.frame_attributes[name] = Attribute(default)
1876            setattr(self, '_' + name, default)
1877
1878        super().__init__(None)
1879
1880    def __getattr__(self, name):
1881        if '_' + name in self.__dict__:
1882            return getattr(self, '_' + name)
1883        else:
1884            raise AttributeError(f'no {name}')
1885
1886    def __setattr__(self, name, value):
1887        if name in self.get_frame_attr_names():
1888            raise AttributeError(f"can't set frame attribute '{name}'")
1889        else:
1890            super().__setattr__(name, value)
1891