1"""
2In this module, we define the coordinate representation classes, which are
3used to represent low-level cartesian, spherical, cylindrical, and other
4coordinates.
5"""
6
7import abc
8import functools
9import operator
10import inspect
11import warnings
12
13import numpy as np
14import astropy.units as u
15from erfa import ufunc as erfa_ufunc
16
17from .angles import Angle, Longitude, Latitude
18from .distances import Distance
19from .matrix_utilities import is_O3
20from astropy.utils import ShapedLikeNDArray, classproperty
21from astropy.utils.data_info import MixinInfo
22from astropy.utils.exceptions import DuplicateRepresentationWarning
23
24
25__all__ = ["BaseRepresentationOrDifferential", "BaseRepresentation",
26           "CartesianRepresentation", "SphericalRepresentation",
27           "UnitSphericalRepresentation", "RadialRepresentation",
28           "PhysicsSphericalRepresentation", "CylindricalRepresentation",
29           "BaseDifferential", "CartesianDifferential",
30           "BaseSphericalDifferential", "BaseSphericalCosLatDifferential",
31           "SphericalDifferential", "SphericalCosLatDifferential",
32           "UnitSphericalDifferential", "UnitSphericalCosLatDifferential",
33           "RadialDifferential", "CylindricalDifferential",
34           "PhysicsSphericalDifferential"]
35
36# Module-level dict mapping representation string alias names to classes.
37# This is populated by __init_subclass__ when called by Representation or
38# Differential classes so that they are all registered automatically.
39REPRESENTATION_CLASSES = {}
40DIFFERENTIAL_CLASSES = {}
41# set for tracking duplicates
42DUPLICATE_REPRESENTATIONS = set()
43
44# a hash for the content of the above two dicts, cached for speed.
45_REPRDIFF_HASH = None
46
47
48def _fqn_class(cls):
49    ''' Get the fully qualified name of a class '''
50    return cls.__module__ + '.' + cls.__qualname__
51
52
53def get_reprdiff_cls_hash():
54    """
55    Returns a hash value that should be invariable if the
56    `REPRESENTATION_CLASSES` and `DIFFERENTIAL_CLASSES` dictionaries have not
57    changed.
58    """
59    global _REPRDIFF_HASH
60    if _REPRDIFF_HASH is None:
61        _REPRDIFF_HASH = (hash(tuple(REPRESENTATION_CLASSES.items())) +
62                          hash(tuple(DIFFERENTIAL_CLASSES.items())))
63    return _REPRDIFF_HASH
64
65
66def _invalidate_reprdiff_cls_hash():
67    global _REPRDIFF_HASH
68    _REPRDIFF_HASH = None
69
70
71def _array2string(values, prefix=''):
72    # Work around version differences for array2string.
73    kwargs = {'separator': ', ', 'prefix': prefix}
74    kwargs['formatter'] = {}
75
76    return np.array2string(values, **kwargs)
77
78
79class BaseRepresentationOrDifferentialInfo(MixinInfo):
80    """
81    Container for meta information like name, description, format.  This is
82    required when the object is used as a mixin column within a table, but can
83    be used as a general way to store meta information.
84    """
85    attrs_from_parent = {'unit'}  # Indicates unit is read-only
86    _supports_indexing = False
87
88    @staticmethod
89    def default_format(val):
90        # Create numpy dtype so that numpy formatting will work.
91        components = val.components
92        values = tuple(getattr(val, component).value for component in components)
93        a = np.empty(getattr(val, 'shape', ()),
94                     [(component, value.dtype) for component, value
95                      in zip(components, values)])
96        for component, value in zip(components, values):
97            a[component] = value
98        return str(a)
99
100    @property
101    def _represent_as_dict_attrs(self):
102        return self._parent.components
103
104    @property
105    def unit(self):
106        if self._parent is None:
107            return None
108
109        unit = self._parent._unitstr
110        return unit[1:-1] if unit.startswith('(') else unit
111
112    def new_like(self, reps, length, metadata_conflicts='warn', name=None):
113        """
114        Return a new instance like ``reps`` with ``length`` rows.
115
116        This is intended for creating an empty column object whose elements can
117        be set in-place for table operations like join or vstack.
118
119        Parameters
120        ----------
121        reps : list
122            List of input representations or differentials.
123        length : int
124            Length of the output column object
125        metadata_conflicts : str ('warn'|'error'|'silent')
126            How to handle metadata conflicts
127        name : str
128            Output column name
129
130        Returns
131        -------
132        col : `BaseRepresentation` or `BaseDifferential` subclass instance
133            Empty instance of this class consistent with ``cols``
134
135        """
136
137        # Get merged info attributes like shape, dtype, format, description, etc.
138        attrs = self.merge_cols_attributes(reps, metadata_conflicts, name,
139                                           ('meta', 'description'))
140        # Make a new representation or differential with the desired length
141        # using the _apply / __getitem__ machinery to effectively return
142        # rep0[[0, 0, ..., 0, 0]]. This will have the right shape, and
143        # include possible differentials.
144        indexes = np.zeros(length, dtype=np.int64)
145        out = reps[0][indexes]
146
147        # Use __setitem__ machinery to check whether all representations
148        # can represent themselves as this one without loss of information.
149        for rep in reps[1:]:
150            try:
151                out[0] = rep[0]
152            except Exception as err:
153                raise ValueError(f'input representations are inconsistent.') from err
154
155        # Set (merged) info attributes.
156        for attr in ('name', 'meta', 'description'):
157            if attr in attrs:
158                setattr(out.info, attr, attrs[attr])
159
160        return out
161
162
163class BaseRepresentationOrDifferential(ShapedLikeNDArray):
164    """3D coordinate representations and differentials.
165
166    Parameters
167    ----------
168    comp1, comp2, comp3 : `~astropy.units.Quantity` or subclass
169        The components of the 3D point or differential.  The names are the
170        keys and the subclasses the values of the ``attr_classes`` attribute.
171    copy : bool, optional
172        If `True` (default), arrays will be copied; if `False`, they will be
173        broadcast together but not use new memory.
174    """
175
176    # Ensure multiplication/division with ndarray or Quantity doesn't lead to
177    # object arrays.
178    __array_priority__ = 50000
179
180    info = BaseRepresentationOrDifferentialInfo()
181
182    def __init__(self, *args, **kwargs):
183        # make argument a list, so we can pop them off.
184        args = list(args)
185        components = self.components
186        if (args and isinstance(args[0], self.__class__)
187                and all(arg is None for arg in args[1:])):
188            rep_or_diff = args[0]
189            copy = kwargs.pop('copy', True)
190            attrs = [getattr(rep_or_diff, component)
191                     for component in components]
192            if 'info' in rep_or_diff.__dict__:
193                self.info = rep_or_diff.info
194
195            if kwargs:
196                raise TypeError(f'unexpected keyword arguments for case '
197                                f'where class instance is passed in: {kwargs}')
198
199        else:
200            attrs = []
201            for component in components:
202                try:
203                    attr = args.pop(0) if args else kwargs.pop(component)
204                except KeyError:
205                    raise TypeError(f'__init__() missing 1 required positional '
206                                    f'argument: {component!r}') from None
207
208                if attr is None:
209                    raise TypeError(f'__init__() missing 1 required positional '
210                                    f'argument: {component!r} (or first '
211                                    f'argument should be an instance of '
212                                    f'{self.__class__.__name__}).')
213
214                attrs.append(attr)
215
216            copy = args.pop(0) if args else kwargs.pop('copy', True)
217
218            if args:
219                raise TypeError(f'unexpected arguments: {args}')
220
221            if kwargs:
222                for component in components:
223                    if component in kwargs:
224                        raise TypeError(f"__init__() got multiple values for "
225                                        f"argument {component!r}")
226
227                raise TypeError(f'unexpected keyword arguments: {kwargs}')
228
229        # Pass attributes through the required initializing classes.
230        attrs = [self.attr_classes[component](attr, copy=copy, subok=True)
231                 for component, attr in zip(components, attrs)]
232        try:
233            bc_attrs = np.broadcast_arrays(*attrs, subok=True)
234        except ValueError  as err:
235            if len(components) <= 2:
236                c_str = ' and '.join(components)
237            else:
238                c_str = ', '.join(components[:2]) + ', and ' + components[2]
239            raise ValueError(f"Input parameters {c_str} cannot be broadcast") from err
240
241        # If inputs have been copied, there is no reason to output a broadcasted array for a
242        # component, so we perform another copy of any component that has been broadcasted
243        # TODO: Look for some way to avoid the double copy in these situations
244        if copy:
245            attrs = [bc_attr.copy() if bc_attr.shape != attr.shape else attr
246                     for attr, bc_attr in zip(attrs, bc_attrs)]
247        else:
248            attrs = bc_attrs
249
250        # Set private attributes for the attributes. (If not defined explicitly
251        # on the class, the metaclass will define properties to access these.)
252        for component, attr in zip(components, attrs):
253            setattr(self, '_' + component, attr)
254
255    @classmethod
256    def get_name(cls):
257        """Name of the representation or differential.
258
259        In lower case, with any trailing 'representation' or 'differential'
260        removed. (E.g., 'spherical' for
261        `~astropy.coordinates.SphericalRepresentation` or
262        `~astropy.coordinates.SphericalDifferential`.)
263        """
264        name = cls.__name__.lower()
265
266        if name.endswith('representation'):
267            name = name[:-14]
268        elif name.endswith('differential'):
269            name = name[:-12]
270
271        return name
272
273    # The two methods that any subclass has to define.
274    @classmethod
275    @abc.abstractmethod
276    def from_cartesian(cls, other):
277        """Create a representation of this class from a supplied Cartesian one.
278
279        Parameters
280        ----------
281        other : `CartesianRepresentation`
282            The representation to turn into this class
283
284        Returns
285        -------
286        representation : `BaseRepresentation` subclass instance
287            A new representation of this class's type.
288        """
289        # Note: the above docstring gets overridden for differentials.
290        raise NotImplementedError()
291
292    @abc.abstractmethod
293    def to_cartesian(self):
294        """Convert the representation to its Cartesian form.
295
296        Note that any differentials get dropped.
297        Also note that orientation information at the origin is *not* preserved by
298        conversions through Cartesian coordinates. For example, transforming
299        an angular position defined at distance=0 through cartesian coordinates
300        and back will lose the original angular coordinates::
301
302            >>> import astropy.units as u
303            >>> import astropy.coordinates as coord
304            >>> rep = coord.SphericalRepresentation(
305            ...     lon=15*u.deg,
306            ...     lat=-11*u.deg,
307            ...     distance=0*u.pc)
308            >>> rep.to_cartesian().represent_as(coord.SphericalRepresentation)
309            <SphericalRepresentation (lon, lat, distance) in (rad, rad, pc)
310                (0., 0., 0.)>
311
312        Returns
313        -------
314        cartrepr : `CartesianRepresentation`
315            The representation in Cartesian form.
316        """
317        # Note: the above docstring gets overridden for differentials.
318        raise NotImplementedError()
319
320    @property
321    def components(self):
322        """A tuple with the in-order names of the coordinate components."""
323        return tuple(self.attr_classes)
324
325    def __eq__(self, value):
326        """Equality operator
327
328        This implements strict equality and requires that the representation
329        classes are identical and that the representation data are exactly equal.
330        """
331        if self.__class__ is not value.__class__:
332            raise TypeError(f'cannot compare: objects must have same class: '
333                            f'{self.__class__.__name__} vs. '
334                            f'{value.__class__.__name__}')
335
336        try:
337            np.broadcast(self, value)
338        except ValueError as exc:
339            raise ValueError(f'cannot compare: {exc}') from exc
340
341        out = True
342        for comp in self.components:
343            out &= (getattr(self, '_' + comp) == getattr(value, '_' + comp))
344
345        return out
346
347    def __ne__(self, value):
348        return np.logical_not(self == value)
349
350    def _apply(self, method, *args, **kwargs):
351        """Create a new representation or differential with ``method`` applied
352        to the component data.
353
354        In typical usage, the method is any of the shape-changing methods for
355        `~numpy.ndarray` (``reshape``, ``swapaxes``, etc.), as well as those
356        picking particular elements (``__getitem__``, ``take``, etc.), which
357        are all defined in `~astropy.utils.shapes.ShapedLikeNDArray`. It will be
358        applied to the underlying arrays (e.g., ``x``, ``y``, and ``z`` for
359        `~astropy.coordinates.CartesianRepresentation`), with the results used
360        to create a new instance.
361
362        Internally, it is also used to apply functions to the components
363        (in particular, `~numpy.broadcast_to`).
364
365        Parameters
366        ----------
367        method : str or callable
368            If str, it is the name of a method that is applied to the internal
369            ``components``. If callable, the function is applied.
370        *args : tuple
371            Any positional arguments for ``method``.
372        **kwargs : dict
373            Any keyword arguments for ``method``.
374        """
375        if callable(method):
376            apply_method = lambda array: method(array, *args, **kwargs)
377        else:
378            apply_method = operator.methodcaller(method, *args, **kwargs)
379
380        new = super().__new__(self.__class__)
381        for component in self.components:
382            setattr(new, '_' + component,
383                    apply_method(getattr(self, component)))
384
385        # Copy other 'info' attr only if it has actually been defined.
386        # See PR #3898 for further explanation and justification, along
387        # with Quantity.__array_finalize__
388        if 'info' in self.__dict__:
389            new.info = self.info
390
391        return new
392
393    def __setitem__(self, item, value):
394        if value.__class__ is not self.__class__:
395            raise TypeError(f'can only set from object of same class: '
396                            f'{self.__class__.__name__} vs. '
397                            f'{value.__class__.__name__}')
398
399        for component in self.components:
400            getattr(self, '_' + component)[item] = getattr(value, '_' + component)
401
402    @property
403    def shape(self):
404        """The shape of the instance and underlying arrays.
405
406        Like `~numpy.ndarray.shape`, can be set to a new shape by assigning a
407        tuple.  Note that if different instances share some but not all
408        underlying data, setting the shape of one instance can make the other
409        instance unusable.  Hence, it is strongly recommended to get new,
410        reshaped instances with the ``reshape`` method.
411
412        Raises
413        ------
414        ValueError
415            If the new shape has the wrong total number of elements.
416        AttributeError
417            If the shape of any of the components cannot be changed without the
418            arrays being copied.  For these cases, use the ``reshape`` method
419            (which copies any arrays that cannot be reshaped in-place).
420        """
421        return getattr(self, self.components[0]).shape
422
423    @shape.setter
424    def shape(self, shape):
425        # We keep track of arrays that were already reshaped since we may have
426        # to return those to their original shape if a later shape-setting
427        # fails. (This can happen since coordinates are broadcast together.)
428        reshaped = []
429        oldshape = self.shape
430        for component in self.components:
431            val = getattr(self, component)
432            if val.size > 1:
433                try:
434                    val.shape = shape
435                except Exception:
436                    for val2 in reshaped:
437                        val2.shape = oldshape
438                    raise
439                else:
440                    reshaped.append(val)
441
442    # Required to support multiplication and division, and defined by the base
443    # representation and differential classes.
444    @abc.abstractmethod
445    def _scale_operation(self, op, *args):
446        raise NotImplementedError()
447
448    def __mul__(self, other):
449        return self._scale_operation(operator.mul, other)
450
451    def __rmul__(self, other):
452        return self.__mul__(other)
453
454    def __truediv__(self, other):
455        return self._scale_operation(operator.truediv, other)
456
457    def __div__(self, other):  # pragma: py2
458        return self._scale_operation(operator.truediv, other)
459
460    def __neg__(self):
461        return self._scale_operation(operator.neg)
462
463    # Follow numpy convention and make an independent copy.
464    def __pos__(self):
465        return self.copy()
466
467    # Required to support addition and subtraction, and defined by the base
468    # representation and differential classes.
469    @abc.abstractmethod
470    def _combine_operation(self, op, other, reverse=False):
471        raise NotImplementedError()
472
473    def __add__(self, other):
474        return self._combine_operation(operator.add, other)
475
476    def __radd__(self, other):
477        return self._combine_operation(operator.add, other, reverse=True)
478
479    def __sub__(self, other):
480        return self._combine_operation(operator.sub, other)
481
482    def __rsub__(self, other):
483        return self._combine_operation(operator.sub, other, reverse=True)
484
485    # The following are used for repr and str
486    @property
487    def _values(self):
488        """Turn the coordinates into a record array with the coordinate values.
489
490        The record array fields will have the component names.
491        """
492        coo_items = [(c, getattr(self, c)) for c in self.components]
493        result = np.empty(self.shape, [(c, coo.dtype) for c, coo in coo_items])
494        for c, coo in coo_items:
495            result[c] = coo.value
496        return result
497
498    @property
499    def _units(self):
500        """Return a dictionary with the units of the coordinate components."""
501        return dict([(component, getattr(self, component).unit)
502                     for component in self.components])
503
504    @property
505    def _unitstr(self):
506        units_set = set(self._units.values())
507        if len(units_set) == 1:
508            unitstr = units_set.pop().to_string()
509        else:
510            unitstr = '({})'.format(
511                ', '.join([self._units[component].to_string()
512                           for component in self.components]))
513        return unitstr
514
515    def __str__(self):
516        return f'{_array2string(self._values)} {self._unitstr:s}'
517
518    def __repr__(self):
519        prefixstr = '    '
520        arrstr = _array2string(self._values, prefix=prefixstr)
521
522        diffstr = ''
523        if getattr(self, 'differentials', None):
524            diffstr = '\n (has differentials w.r.t.: {})'.format(
525                ', '.join([repr(key) for key in self.differentials.keys()]))
526
527        unitstr = ('in ' + self._unitstr) if self._unitstr else '[dimensionless]'
528        return '<{} ({}) {:s}\n{}{}{}>'.format(
529            self.__class__.__name__, ', '.join(self.components),
530            unitstr, prefixstr, arrstr, diffstr)
531
532
533def _make_getter(component):
534    """Make an attribute getter for use in a property.
535
536    Parameters
537    ----------
538    component : str
539        The name of the component that should be accessed.  This assumes the
540        actual value is stored in an attribute of that name prefixed by '_'.
541    """
542    # This has to be done in a function to ensure the reference to component
543    # is not lost/redirected.
544    component = '_' + component
545
546    def get_component(self):
547        return getattr(self, component)
548    return get_component
549
550
551class RepresentationInfo(BaseRepresentationOrDifferentialInfo):
552
553    @property
554    def _represent_as_dict_attrs(self):
555        attrs = super()._represent_as_dict_attrs
556        if self._parent._differentials:
557            attrs += ('differentials',)
558        return attrs
559
560    def _represent_as_dict(self, attrs=None):
561        out = super()._represent_as_dict(attrs)
562        for key, value in out.pop('differentials', {}).items():
563            out[f'differentials.{key}'] = value
564        return out
565
566    def _construct_from_dict(self, map):
567        differentials = {}
568        for key in list(map.keys()):
569            if key.startswith('differentials.'):
570                differentials[key[14:]] = map.pop(key)
571        map['differentials'] = differentials
572        return super()._construct_from_dict(map)
573
574
575class BaseRepresentation(BaseRepresentationOrDifferential):
576    """Base for representing a point in a 3D coordinate system.
577
578    Parameters
579    ----------
580    comp1, comp2, comp3 : `~astropy.units.Quantity` or subclass
581        The components of the 3D points.  The names are the keys and the
582        subclasses the values of the ``attr_classes`` attribute.
583    differentials : dict, `~astropy.coordinates.BaseDifferential`, optional
584        Any differential classes that should be associated with this
585        representation. The input must either be a single `~astropy.coordinates.BaseDifferential`
586        subclass instance, or a dictionary with keys set to a string
587        representation of the SI unit with which the differential (derivative)
588        is taken. For example, for a velocity differential on a positional
589        representation, the key would be ``'s'`` for seconds, indicating that
590        the derivative is a time derivative.
591    copy : bool, optional
592        If `True` (default), arrays will be copied. If `False`, arrays will
593        be references, though possibly broadcast to ensure matching shapes.
594
595    Notes
596    -----
597    All representation classes should subclass this base representation class,
598    and define an ``attr_classes`` attribute, a `dict`
599    which maps component names to the class that creates them. They must also
600    define a ``to_cartesian`` method and a ``from_cartesian`` class method. By
601    default, transformations are done via the cartesian system, but classes
602    that want to define a smarter transformation path can overload the
603    ``represent_as`` method. If one wants to use an associated differential
604    class, one should also define ``unit_vectors`` and ``scale_factors``
605    methods (see those methods for details).
606    """
607
608    info = RepresentationInfo()
609
610    def __init_subclass__(cls, **kwargs):
611        # Register representation name (except for BaseRepresentation)
612        if cls.__name__ == 'BaseRepresentation':
613            return
614
615        if not hasattr(cls, 'attr_classes'):
616            raise NotImplementedError('Representations must have an '
617                                      '"attr_classes" class attribute.')
618
619        repr_name = cls.get_name()
620        # first time a duplicate is added
621        # remove first entry and add both using their qualnames
622        if repr_name in REPRESENTATION_CLASSES:
623            DUPLICATE_REPRESENTATIONS.add(repr_name)
624
625            fqn_cls = _fqn_class(cls)
626            existing = REPRESENTATION_CLASSES[repr_name]
627            fqn_existing = _fqn_class(existing)
628
629            if fqn_cls == fqn_existing:
630                raise ValueError(f'Representation "{fqn_cls}" already defined')
631
632            msg = (
633                f'Representation "{repr_name}" already defined, removing it to avoid confusion.'
634                f'Use qualnames "{fqn_cls}" and "{fqn_existing}" or class instances directly'
635            )
636            warnings.warn(msg, DuplicateRepresentationWarning)
637
638            del REPRESENTATION_CLASSES[repr_name]
639            REPRESENTATION_CLASSES[fqn_existing] = existing
640            repr_name = fqn_cls
641
642        # further definitions with the same name, just add qualname
643        elif repr_name in DUPLICATE_REPRESENTATIONS:
644            fqn_cls = _fqn_class(cls)
645            warnings.warn(f'Representation "{repr_name}" already defined, using qualname '
646                          f'"{fqn_cls}".')
647            repr_name = fqn_cls
648            if repr_name in REPRESENTATION_CLASSES:
649                raise ValueError(
650                    f'Representation "{repr_name}" already defined'
651                )
652
653        REPRESENTATION_CLASSES[repr_name] = cls
654        _invalidate_reprdiff_cls_hash()
655
656        # define getters for any component that does not yet have one.
657        for component in cls.attr_classes:
658            if not hasattr(cls, component):
659                setattr(cls, component,
660                        property(_make_getter(component),
661                                 doc=f"The '{component}' component of the points(s)."))
662
663        super().__init_subclass__(**kwargs)
664
665    def __init__(self, *args, differentials=None, **kwargs):
666        # Handle any differentials passed in.
667        super().__init__(*args, **kwargs)
668        if (differentials is None
669                and args and isinstance(args[0], self.__class__)):
670            differentials = args[0]._differentials
671        self._differentials = self._validate_differentials(differentials)
672
673    def _validate_differentials(self, differentials):
674        """
675        Validate that the provided differentials are appropriate for this
676        representation and recast/reshape as necessary and then return.
677
678        Note that this does *not* set the differentials on
679        ``self._differentials``, but rather leaves that for the caller.
680        """
681
682        # Now handle the actual validation of any specified differential classes
683        if differentials is None:
684            differentials = dict()
685
686        elif isinstance(differentials, BaseDifferential):
687            # We can't handle auto-determining the key for this combo
688            if (isinstance(differentials, RadialDifferential) and
689                    isinstance(self, UnitSphericalRepresentation)):
690                raise ValueError("To attach a RadialDifferential to a "
691                                 "UnitSphericalRepresentation, you must supply "
692                                 "a dictionary with an appropriate key.")
693
694            key = differentials._get_deriv_key(self)
695            differentials = {key: differentials}
696
697        for key in differentials:
698            try:
699                diff = differentials[key]
700            except TypeError as err:
701                raise TypeError("'differentials' argument must be a "
702                                "dictionary-like object") from err
703
704            diff._check_base(self)
705
706            if (isinstance(diff, RadialDifferential) and
707                    isinstance(self, UnitSphericalRepresentation)):
708                # We trust the passing of a key for a RadialDifferential
709                # attached to a UnitSphericalRepresentation because it will not
710                # have a paired component name (UnitSphericalRepresentation has
711                # no .distance) to automatically determine the expected key
712                pass
713
714            else:
715                expected_key = diff._get_deriv_key(self)
716                if key != expected_key:
717                    raise ValueError("For differential object '{}', expected "
718                                     "unit key = '{}' but received key = '{}'"
719                                     .format(repr(diff), expected_key, key))
720
721            # For now, we are very rigid: differentials must have the same shape
722            # as the representation. This makes it easier to handle __getitem__
723            # and any other shape-changing operations on representations that
724            # have associated differentials
725            if diff.shape != self.shape:
726                # TODO: message of IncompatibleShapeError is not customizable,
727                #       so use a valueerror instead?
728                raise ValueError("Shape of differentials must be the same "
729                                 "as the shape of the representation ({} vs "
730                                 "{})".format(diff.shape, self.shape))
731
732        return differentials
733
734    def _raise_if_has_differentials(self, op_name):
735        """
736        Used to raise a consistent exception for any operation that is not
737        supported when a representation has differentials attached.
738        """
739        if self.differentials:
740            raise TypeError("Operation '{}' is not supported when "
741                            "differentials are attached to a {}."
742                            .format(op_name, self.__class__.__name__))
743
744    @classproperty
745    def _compatible_differentials(cls):
746        return [DIFFERENTIAL_CLASSES[cls.get_name()]]
747
748    @property
749    def differentials(self):
750        """A dictionary of differential class instances.
751
752        The keys of this dictionary must be a string representation of the SI
753        unit with which the differential (derivative) is taken. For example, for
754        a velocity differential on a positional representation, the key would be
755        ``'s'`` for seconds, indicating that the derivative is a time
756        derivative.
757        """
758        return self._differentials
759
760    # We do not make unit_vectors and scale_factors abstract methods, since
761    # they are only necessary if one also defines an associated Differential.
762    # Also, doing so would break pre-differential representation subclasses.
763    def unit_vectors(self):
764        r"""Cartesian unit vectors in the direction of each component.
765
766        Given unit vectors :math:`\hat{e}_c` and scale factors :math:`f_c`,
767        a change in one component of :math:`\delta c` corresponds to a change
768        in representation of :math:`\delta c \times f_c \times \hat{e}_c`.
769
770        Returns
771        -------
772        unit_vectors : dict of `CartesianRepresentation`
773            The keys are the component names.
774        """
775        raise NotImplementedError(f"{type(self)} has not implemented unit vectors")
776
777    def scale_factors(self):
778        r"""Scale factors for each component's direction.
779
780        Given unit vectors :math:`\hat{e}_c` and scale factors :math:`f_c`,
781        a change in one component of :math:`\delta c` corresponds to a change
782        in representation of :math:`\delta c \times f_c \times \hat{e}_c`.
783
784        Returns
785        -------
786        scale_factors : dict of `~astropy.units.Quantity`
787            The keys are the component names.
788        """
789        raise NotImplementedError(f"{type(self)} has not implemented scale factors.")
790
791    def _re_represent_differentials(self, new_rep, differential_class):
792        """Re-represent the differentials to the specified classes.
793
794        This returns a new dictionary with the same keys but with the
795        attached differentials converted to the new differential classes.
796        """
797        if differential_class is None:
798            return dict()
799
800        if not self.differentials and differential_class:
801            raise ValueError("No differentials associated with this "
802                             "representation!")
803
804        elif (len(self.differentials) == 1 and
805                inspect.isclass(differential_class) and
806                issubclass(differential_class, BaseDifferential)):
807            # TODO: is there a better way to do this?
808            differential_class = {
809                list(self.differentials.keys())[0]: differential_class
810            }
811
812        elif differential_class.keys() != self.differentials.keys():
813            raise ValueError("Desired differential classes must be passed in "
814                             "as a dictionary with keys equal to a string "
815                             "representation of the unit of the derivative "
816                             "for each differential stored with this "
817                             "representation object ({0})"
818                             .format(self.differentials))
819
820        new_diffs = dict()
821        for k in self.differentials:
822            diff = self.differentials[k]
823            try:
824                new_diffs[k] = diff.represent_as(differential_class[k],
825                                                 base=self)
826            except Exception as err:
827                if (differential_class[k] not in
828                        new_rep._compatible_differentials):
829                    raise TypeError("Desired differential class {} is not "
830                                    "compatible with the desired "
831                                    "representation class {}"
832                                    .format(differential_class[k],
833                                            new_rep.__class__)) from err
834                else:
835                    raise
836
837        return new_diffs
838
839    def represent_as(self, other_class, differential_class=None):
840        """Convert coordinates to another representation.
841
842        If the instance is of the requested class, it is returned unmodified.
843        By default, conversion is done via Cartesian coordinates.
844        Also note that orientation information at the origin is *not* preserved by
845        conversions through Cartesian coordinates. See the docstring for
846        :meth:`~astropy.coordinates.BaseRepresentationOrDifferential.to_cartesian`
847        for an example.
848
849        Parameters
850        ----------
851        other_class : `~astropy.coordinates.BaseRepresentation` subclass
852            The type of representation to turn the coordinates into.
853        differential_class : dict of `~astropy.coordinates.BaseDifferential`, optional
854            Classes in which the differentials should be represented.
855            Can be a single class if only a single differential is attached,
856            otherwise it should be a `dict` keyed by the same keys as the
857            differentials.
858        """
859        if other_class is self.__class__ and not differential_class:
860            return self.without_differentials()
861
862        else:
863            if isinstance(other_class, str):
864                raise ValueError("Input to a representation's represent_as "
865                                 "must be a class, not a string. For "
866                                 "strings, use frame objects")
867
868            if other_class is not self.__class__:
869                # The default is to convert via cartesian coordinates
870                new_rep = other_class.from_cartesian(self.to_cartesian())
871            else:
872                new_rep = self
873
874            new_rep._differentials = self._re_represent_differentials(
875                new_rep, differential_class)
876
877            return new_rep
878
879    def transform(self, matrix):
880        """Transform coordinates using a 3x3 matrix in a Cartesian basis.
881
882        This returns a new representation and does not modify the original one.
883        Any differentials attached to this representation will also be
884        transformed.
885
886        Parameters
887        ----------
888        matrix : (3,3) array-like
889            A 3x3 (or stack thereof) matrix, such as a rotation matrix.
890
891        """
892        # route transformation through Cartesian
893        difs_cls = {k: CartesianDifferential for k in self.differentials.keys()}
894        crep = self.represent_as(CartesianRepresentation,
895                                 differential_class=difs_cls
896                                ).transform(matrix)
897
898        # move back to original representation
899        difs_cls = {k: diff.__class__ for k, diff in self.differentials.items()}
900        rep = crep.represent_as(self.__class__, difs_cls)
901        return rep
902
903    def with_differentials(self, differentials):
904        """
905        Create a new representation with the same positions as this
906        representation, but with these new differentials.
907
908        Differential keys that already exist in this object's differential dict
909        are overwritten.
910
911        Parameters
912        ----------
913        differentials : sequence of `~astropy.coordinates.BaseDifferential` subclass instance
914            The differentials for the new representation to have.
915
916        Returns
917        -------
918        `~astropy.coordinates.BaseRepresentation` subclass instance
919            A copy of this representation, but with the ``differentials`` as
920            its differentials.
921        """
922        if not differentials:
923            return self
924
925        args = [getattr(self, component) for component in self.components]
926
927        # We shallow copy the differentials dictionary so we don't update the
928        # current object's dictionary when adding new keys
929        new_rep = self.__class__(*args, differentials=self.differentials.copy(),
930                                 copy=False)
931        new_rep._differentials.update(
932            new_rep._validate_differentials(differentials))
933
934        return new_rep
935
936    def without_differentials(self):
937        """Return a copy of the representation without attached differentials.
938
939        Returns
940        -------
941        `~astropy.coordinates.BaseRepresentation` subclass instance
942            A shallow copy of this representation, without any differentials.
943            If no differentials were present, no copy is made.
944        """
945
946        if not self._differentials:
947            return self
948
949        args = [getattr(self, component) for component in self.components]
950        return self.__class__(*args, copy=False)
951
952    @classmethod
953    def from_representation(cls, representation):
954        """Create a new instance of this representation from another one.
955
956        Parameters
957        ----------
958        representation : `~astropy.coordinates.BaseRepresentation` instance
959            The presentation that should be converted to this class.
960        """
961        return representation.represent_as(cls)
962
963    def __eq__(self, value):
964        """Equality operator for BaseRepresentation
965
966        This implements strict equality and requires that the representation
967        classes are identical, the differentials are identical, and that the
968        representation data are exactly equal.
969        """
970        # BaseRepresentationOrDifferental (checks classes and compares components)
971        out = super().__eq__(value)
972
973        # super() checks that the class is identical so can this even happen?
974        # (same class, different differentials ?)
975        if self._differentials.keys() != value._differentials.keys():
976            raise ValueError(f'cannot compare: objects must have same differentials')
977
978        for self_diff, value_diff in zip(self._differentials.values(),
979                                         value._differentials.values()):
980            out &= (self_diff == value_diff)
981
982        return out
983
984    def __ne__(self, value):
985        return np.logical_not(self == value)
986
987    def _apply(self, method, *args, **kwargs):
988        """Create a new representation with ``method`` applied to the component
989        data.
990
991        This is not a simple inherit from ``BaseRepresentationOrDifferential``
992        because we need to call ``._apply()`` on any associated differential
993        classes.
994
995        See docstring for `BaseRepresentationOrDifferential._apply`.
996
997        Parameters
998        ----------
999        method : str or callable
1000            If str, it is the name of a method that is applied to the internal
1001            ``components``. If callable, the function is applied.
1002        *args : tuple
1003            Any positional arguments for ``method``.
1004        **kwargs : dict
1005            Any keyword arguments for ``method``.
1006
1007        """
1008        rep = super()._apply(method, *args, **kwargs)
1009
1010        rep._differentials = dict(
1011            [(k, diff._apply(method, *args, **kwargs))
1012             for k, diff in self._differentials.items()])
1013        return rep
1014
1015    def __setitem__(self, item, value):
1016        if not isinstance(value, BaseRepresentation):
1017            raise TypeError(f'value must be a representation instance, '
1018                            f'not {type(value)}.')
1019
1020        if not (isinstance(value, self.__class__)
1021                or len(value.attr_classes) == len(self.attr_classes)):
1022            raise ValueError(
1023                f'value must be representable as {self.__class__.__name__} '
1024                f'without loss of information.')
1025
1026        diff_classes = {}
1027        if self._differentials:
1028            if self._differentials.keys() != value._differentials.keys():
1029                raise ValueError('value must have the same differentials.')
1030
1031            for key, self_diff in self._differentials.items():
1032                diff_classes[key] = self_diff_cls = self_diff.__class__
1033                value_diff_cls = value._differentials[key].__class__
1034                if not (isinstance(value_diff_cls, self_diff_cls)
1035                        or (len(value_diff_cls.attr_classes)
1036                            == len(self_diff_cls.attr_classes))):
1037                    raise ValueError(
1038                        f'value differential {key!r} must be representable as '
1039                        f'{self_diff.__class__.__name__} without loss of information.')
1040
1041        value = value.represent_as(self.__class__, diff_classes)
1042        super().__setitem__(item, value)
1043        for key, differential in self._differentials.items():
1044            differential[item] = value._differentials[key]
1045
1046    def _scale_operation(self, op, *args):
1047        """Scale all non-angular components, leaving angular ones unchanged.
1048
1049        Parameters
1050        ----------
1051        op : `~operator` callable
1052            Operator to apply (e.g., `~operator.mul`, `~operator.neg`, etc.
1053        *args
1054            Any arguments required for the operator (typically, what is to
1055            be multiplied with, divided by).
1056        """
1057        results = []
1058        for component, cls in self.attr_classes.items():
1059            value = getattr(self, component)
1060            if issubclass(cls, Angle):
1061                results.append(value)
1062            else:
1063                results.append(op(value, *args))
1064
1065        # try/except catches anything that cannot initialize the class, such
1066        # as operations that returned NotImplemented or a representation
1067        # instead of a quantity (as would happen for, e.g., rep * rep).
1068        try:
1069            result = self.__class__(*results)
1070        except Exception:
1071            return NotImplemented
1072
1073        for key, differential in self.differentials.items():
1074            diff_result = differential._scale_operation(op, *args, scaled_base=True)
1075            result.differentials[key] = diff_result
1076
1077        return result
1078
1079    def _combine_operation(self, op, other, reverse=False):
1080        """Combine two representation.
1081
1082        By default, operate on the cartesian representations of both.
1083
1084        Parameters
1085        ----------
1086        op : `~operator` callable
1087            Operator to apply (e.g., `~operator.add`, `~operator.sub`, etc.
1088        other : `~astropy.coordinates.BaseRepresentation` subclass instance
1089            The other representation.
1090        reverse : bool
1091            Whether the operands should be reversed (e.g., as we got here via
1092            ``self.__rsub__`` because ``self`` is a subclass of ``other``).
1093        """
1094        self._raise_if_has_differentials(op.__name__)
1095
1096        result = self.to_cartesian()._combine_operation(op, other, reverse)
1097        if result is NotImplemented:
1098            return NotImplemented
1099        else:
1100            return self.from_cartesian(result)
1101
1102    # We need to override this setter to support differentials
1103    @BaseRepresentationOrDifferential.shape.setter
1104    def shape(self, shape):
1105        orig_shape = self.shape
1106
1107        # See: https://stackoverflow.com/questions/3336767/ for an example
1108        BaseRepresentationOrDifferential.shape.fset(self, shape)
1109
1110        # also try to perform shape-setting on any associated differentials
1111        try:
1112            for k in self.differentials:
1113                self.differentials[k].shape = shape
1114        except Exception:
1115            BaseRepresentationOrDifferential.shape.fset(self, orig_shape)
1116            for k in self.differentials:
1117                self.differentials[k].shape = orig_shape
1118
1119            raise
1120
1121    def norm(self):
1122        """Vector norm.
1123
1124        The norm is the standard Frobenius norm, i.e., the square root of the
1125        sum of the squares of all components with non-angular units.
1126
1127        Note that any associated differentials will be dropped during this
1128        operation.
1129
1130        Returns
1131        -------
1132        norm : `astropy.units.Quantity`
1133            Vector norm, with the same shape as the representation.
1134        """
1135        return np.sqrt(functools.reduce(
1136            operator.add, (getattr(self, component)**2
1137                           for component, cls in self.attr_classes.items()
1138                           if not issubclass(cls, Angle))))
1139
1140    def mean(self, *args, **kwargs):
1141        """Vector mean.
1142
1143        Averaging is done by converting the representation to cartesian, and
1144        taking the mean of the x, y, and z components. The result is converted
1145        back to the same representation as the input.
1146
1147        Refer to `~numpy.mean` for full documentation of the arguments, noting
1148        that ``axis`` is the entry in the ``shape`` of the representation, and
1149        that the ``out`` argument cannot be used.
1150
1151        Returns
1152        -------
1153        mean : `~astropy.coordinates.BaseRepresentation` subclass instance
1154            Vector mean, in the same representation as that of the input.
1155        """
1156        self._raise_if_has_differentials('mean')
1157        return self.from_cartesian(self.to_cartesian().mean(*args, **kwargs))
1158
1159    def sum(self, *args, **kwargs):
1160        """Vector sum.
1161
1162        Adding is done by converting the representation to cartesian, and
1163        summing the x, y, and z components. The result is converted back to the
1164        same representation as the input.
1165
1166        Refer to `~numpy.sum` for full documentation of the arguments, noting
1167        that ``axis`` is the entry in the ``shape`` of the representation, and
1168        that the ``out`` argument cannot be used.
1169
1170        Returns
1171        -------
1172        sum : `~astropy.coordinates.BaseRepresentation` subclass instance
1173            Vector sum, in the same representation as that of the input.
1174        """
1175        self._raise_if_has_differentials('sum')
1176        return self.from_cartesian(self.to_cartesian().sum(*args, **kwargs))
1177
1178    def dot(self, other):
1179        """Dot product of two representations.
1180
1181        The calculation is done by converting both ``self`` and ``other``
1182        to `~astropy.coordinates.CartesianRepresentation`.
1183
1184        Note that any associated differentials will be dropped during this
1185        operation.
1186
1187        Parameters
1188        ----------
1189        other : `~astropy.coordinates.BaseRepresentation`
1190            The representation to take the dot product with.
1191
1192        Returns
1193        -------
1194        dot_product : `~astropy.units.Quantity`
1195            The sum of the product of the x, y, and z components of the
1196            cartesian representations of ``self`` and ``other``.
1197        """
1198        return self.to_cartesian().dot(other)
1199
1200    def cross(self, other):
1201        """Vector cross product of two representations.
1202
1203        The calculation is done by converting both ``self`` and ``other``
1204        to `~astropy.coordinates.CartesianRepresentation`, and converting the
1205        result back to the type of representation of ``self``.
1206
1207        Parameters
1208        ----------
1209        other : `~astropy.coordinates.BaseRepresentation` subclass instance
1210            The representation to take the cross product with.
1211
1212        Returns
1213        -------
1214        cross_product : `~astropy.coordinates.BaseRepresentation` subclass instance
1215            With vectors perpendicular to both ``self`` and ``other``, in the
1216            same type of representation as ``self``.
1217        """
1218        self._raise_if_has_differentials('cross')
1219        return self.from_cartesian(self.to_cartesian().cross(other))
1220
1221
1222class CartesianRepresentation(BaseRepresentation):
1223    """
1224    Representation of points in 3D cartesian coordinates.
1225
1226    Parameters
1227    ----------
1228    x, y, z : `~astropy.units.Quantity` or array
1229        The x, y, and z coordinates of the point(s). If ``x``, ``y``, and ``z``
1230        have different shapes, they should be broadcastable. If not quantity,
1231        ``unit`` should be set.  If only ``x`` is given, it is assumed that it
1232        contains an array with the 3 coordinates stored along ``xyz_axis``.
1233    unit : unit-like
1234        If given, the coordinates will be converted to this unit (or taken to
1235        be in this unit if not given.
1236    xyz_axis : int, optional
1237        The axis along which the coordinates are stored when a single array is
1238        provided rather than distinct ``x``, ``y``, and ``z`` (default: 0).
1239
1240    differentials : dict, `CartesianDifferential`, optional
1241        Any differential classes that should be associated with this
1242        representation. The input must either be a single
1243        `CartesianDifferential` instance, or a dictionary of
1244        `CartesianDifferential` s with keys set to a string representation of
1245        the SI unit with which the differential (derivative) is taken. For
1246        example, for a velocity differential on a positional representation, the
1247        key would be ``'s'`` for seconds, indicating that the derivative is a
1248        time derivative.
1249
1250    copy : bool, optional
1251        If `True` (default), arrays will be copied. If `False`, arrays will
1252        be references, though possibly broadcast to ensure matching shapes.
1253    """
1254
1255    attr_classes = {'x': u.Quantity,
1256                    'y': u.Quantity,
1257                    'z': u.Quantity}
1258
1259    _xyz = None
1260
1261    def __init__(self, x, y=None, z=None, unit=None, xyz_axis=None,
1262                 differentials=None, copy=True):
1263
1264        if y is None and z is None:
1265            if isinstance(x, np.ndarray) and x.dtype.kind not in 'OV':
1266                # Short-cut for 3-D array input.
1267                x = u.Quantity(x, unit, copy=copy, subok=True)
1268                # Keep a link to the array with all three coordinates
1269                # so that we can return it quickly if needed in get_xyz.
1270                self._xyz = x
1271                if xyz_axis:
1272                    x = np.moveaxis(x, xyz_axis, 0)
1273                    self._xyz_axis = xyz_axis
1274                else:
1275                    self._xyz_axis = 0
1276
1277                self._x, self._y, self._z = x
1278                self._differentials = self._validate_differentials(differentials)
1279                return
1280
1281            elif (isinstance(x, CartesianRepresentation)
1282                  and unit is None and xyz_axis is None):
1283                if differentials is None:
1284                    differentials = x._differentials
1285
1286                return super().__init__(x, differentials=differentials,
1287                                        copy=copy)
1288
1289            else:
1290                x, y, z = x
1291
1292        if xyz_axis is not None:
1293            raise ValueError("xyz_axis should only be set if x, y, and z are "
1294                             "in a single array passed in through x, "
1295                             "i.e., y and z should not be not given.")
1296
1297        if y is None or z is None:
1298            raise ValueError("x, y, and z are required to instantiate {}"
1299                             .format(self.__class__.__name__))
1300
1301        if unit is not None:
1302            x = u.Quantity(x, unit, copy=copy, subok=True)
1303            y = u.Quantity(y, unit, copy=copy, subok=True)
1304            z = u.Quantity(z, unit, copy=copy, subok=True)
1305            copy = False
1306
1307        super().__init__(x, y, z, copy=copy, differentials=differentials)
1308        if not (self._x.unit.is_equivalent(self._y.unit) and
1309                self._x.unit.is_equivalent(self._z.unit)):
1310            raise u.UnitsError("x, y, and z should have matching physical types")
1311
1312    def unit_vectors(self):
1313        l = np.broadcast_to(1.*u.one, self.shape, subok=True)
1314        o = np.broadcast_to(0.*u.one, self.shape, subok=True)
1315        return {
1316            'x': CartesianRepresentation(l, o, o, copy=False),
1317            'y': CartesianRepresentation(o, l, o, copy=False),
1318            'z': CartesianRepresentation(o, o, l, copy=False)}
1319
1320    def scale_factors(self):
1321        l = np.broadcast_to(1.*u.one, self.shape, subok=True)
1322        return {'x': l, 'y': l, 'z': l}
1323
1324    def get_xyz(self, xyz_axis=0):
1325        """Return a vector array of the x, y, and z coordinates.
1326
1327        Parameters
1328        ----------
1329        xyz_axis : int, optional
1330            The axis in the final array along which the x, y, z components
1331            should be stored (default: 0).
1332
1333        Returns
1334        -------
1335        xyz : `~astropy.units.Quantity`
1336            With dimension 3 along ``xyz_axis``.  Note that, if possible,
1337            this will be a view.
1338        """
1339        if self._xyz is not None:
1340            if self._xyz_axis == xyz_axis:
1341                return self._xyz
1342            else:
1343                return np.moveaxis(self._xyz, self._xyz_axis, xyz_axis)
1344
1345        # Create combined array.  TO DO: keep it in _xyz for repeated use?
1346        # But then in-place changes have to cancel it. Likely best to
1347        # also update components.
1348        return np.stack([self._x, self._y, self._z], axis=xyz_axis)
1349
1350    xyz = property(get_xyz)
1351
1352    @classmethod
1353    def from_cartesian(cls, other):
1354        return other
1355
1356    def to_cartesian(self):
1357        return self
1358
1359    def transform(self, matrix):
1360        """
1361        Transform the cartesian coordinates using a 3x3 matrix.
1362
1363        This returns a new representation and does not modify the original one.
1364        Any differentials attached to this representation will also be
1365        transformed.
1366
1367        Parameters
1368        ----------
1369        matrix : ndarray
1370            A 3x3 transformation matrix, such as a rotation matrix.
1371
1372        Examples
1373        --------
1374
1375        We can start off by creating a cartesian representation object:
1376
1377            >>> from astropy import units as u
1378            >>> from astropy.coordinates import CartesianRepresentation
1379            >>> rep = CartesianRepresentation([1, 2] * u.pc,
1380            ...                               [2, 3] * u.pc,
1381            ...                               [3, 4] * u.pc)
1382
1383        We now create a rotation matrix around the z axis:
1384
1385            >>> from astropy.coordinates.matrix_utilities import rotation_matrix
1386            >>> rotation = rotation_matrix(30 * u.deg, axis='z')
1387
1388        Finally, we can apply this transformation:
1389
1390            >>> rep_new = rep.transform(rotation)
1391            >>> rep_new.xyz  # doctest: +FLOAT_CMP
1392            <Quantity [[ 1.8660254 , 3.23205081],
1393                       [ 1.23205081, 1.59807621],
1394                       [ 3.        , 4.        ]] pc>
1395        """
1396        # erfa rxp: Multiply a p-vector by an r-matrix.
1397        p = erfa_ufunc.rxp(matrix, self.get_xyz(xyz_axis=-1))
1398        # transformed representation
1399        rep = self.__class__(p, xyz_axis=-1, copy=False)
1400        # Handle differentials attached to this representation
1401        new_diffs = dict((k, d.transform(matrix, self, rep))
1402                         for k, d in self.differentials.items())
1403        return rep.with_differentials(new_diffs)
1404
1405    def _combine_operation(self, op, other, reverse=False):
1406        self._raise_if_has_differentials(op.__name__)
1407
1408        try:
1409            other_c = other.to_cartesian()
1410        except Exception:
1411            return NotImplemented
1412
1413        first, second = ((self, other_c) if not reverse else
1414                         (other_c, self))
1415        return self.__class__(*(op(getattr(first, component),
1416                                   getattr(second, component))
1417                                for component in first.components))
1418
1419    def norm(self):
1420        """Vector norm.
1421
1422        The norm is the standard Frobenius norm, i.e., the square root of the
1423        sum of the squares of all components with non-angular units.
1424
1425        Note that any associated differentials will be dropped during this
1426        operation.
1427
1428        Returns
1429        -------
1430        norm : `astropy.units.Quantity`
1431            Vector norm, with the same shape as the representation.
1432        """
1433        # erfa pm: Modulus of p-vector.
1434        return erfa_ufunc.pm(self.get_xyz(xyz_axis=-1))
1435
1436    def mean(self, *args, **kwargs):
1437        """Vector mean.
1438
1439        Returns a new CartesianRepresentation instance with the means of the
1440        x, y, and z components.
1441
1442        Refer to `~numpy.mean` for full documentation of the arguments, noting
1443        that ``axis`` is the entry in the ``shape`` of the representation, and
1444        that the ``out`` argument cannot be used.
1445        """
1446        self._raise_if_has_differentials('mean')
1447        return self._apply('mean', *args, **kwargs)
1448
1449    def sum(self, *args, **kwargs):
1450        """Vector sum.
1451
1452        Returns a new CartesianRepresentation instance with the sums of the
1453        x, y, and z components.
1454
1455        Refer to `~numpy.sum` for full documentation of the arguments, noting
1456        that ``axis`` is the entry in the ``shape`` of the representation, and
1457        that the ``out`` argument cannot be used.
1458        """
1459        self._raise_if_has_differentials('sum')
1460        return self._apply('sum', *args, **kwargs)
1461
1462    def dot(self, other):
1463        """Dot product of two representations.
1464
1465        Note that any associated differentials will be dropped during this
1466        operation.
1467
1468        Parameters
1469        ----------
1470        other : `~astropy.coordinates.BaseRepresentation` subclass instance
1471            If not already cartesian, it is converted.
1472
1473        Returns
1474        -------
1475        dot_product : `~astropy.units.Quantity`
1476            The sum of the product of the x, y, and z components of ``self``
1477            and ``other``.
1478        """
1479        try:
1480            other_c = other.to_cartesian()
1481        except Exception as err:
1482            raise TypeError("cannot only take dot product with another "
1483                            "representation, not a {} instance."
1484                            .format(type(other))) from err
1485        # erfa pdp: p-vector inner (=scalar=dot) product.
1486        return erfa_ufunc.pdp(self.get_xyz(xyz_axis=-1),
1487                              other_c.get_xyz(xyz_axis=-1))
1488
1489    def cross(self, other):
1490        """Cross product of two representations.
1491
1492        Parameters
1493        ----------
1494        other : `~astropy.coordinates.BaseRepresentation` subclass instance
1495            If not already cartesian, it is converted.
1496
1497        Returns
1498        -------
1499        cross_product : `~astropy.coordinates.CartesianRepresentation`
1500            With vectors perpendicular to both ``self`` and ``other``.
1501        """
1502        self._raise_if_has_differentials('cross')
1503        try:
1504            other_c = other.to_cartesian()
1505        except Exception as err:
1506            raise TypeError("cannot only take cross product with another "
1507                            "representation, not a {} instance."
1508                            .format(type(other))) from err
1509        # erfa pxp: p-vector outer (=vector=cross) product.
1510        sxo = erfa_ufunc.pxp(self.get_xyz(xyz_axis=-1),
1511                             other_c.get_xyz(xyz_axis=-1))
1512        return self.__class__(sxo, xyz_axis=-1)
1513
1514
1515class UnitSphericalRepresentation(BaseRepresentation):
1516    """
1517    Representation of points on a unit sphere.
1518
1519    Parameters
1520    ----------
1521    lon, lat : `~astropy.units.Quantity` ['angle'] or str
1522        The longitude and latitude of the point(s), in angular units. The
1523        latitude should be between -90 and 90 degrees, and the longitude will
1524        be wrapped to an angle between 0 and 360 degrees. These can also be
1525        instances of `~astropy.coordinates.Angle`,
1526        `~astropy.coordinates.Longitude`, or `~astropy.coordinates.Latitude`.
1527
1528    differentials : dict, `~astropy.coordinates.BaseDifferential`, optional
1529        Any differential classes that should be associated with this
1530        representation. The input must either be a single `~astropy.coordinates.BaseDifferential`
1531        instance (see `._compatible_differentials` for valid types), or a
1532        dictionary of of differential instances with keys set to a string
1533        representation of the SI unit with which the differential (derivative)
1534        is taken. For example, for a velocity differential on a positional
1535        representation, the key would be ``'s'`` for seconds, indicating that
1536        the derivative is a time derivative.
1537
1538    copy : bool, optional
1539        If `True` (default), arrays will be copied. If `False`, arrays will
1540        be references, though possibly broadcast to ensure matching shapes.
1541    """
1542
1543    attr_classes = {'lon': Longitude,
1544                    'lat': Latitude}
1545
1546    @classproperty
1547    def _dimensional_representation(cls):
1548        return SphericalRepresentation
1549
1550    def __init__(self, lon, lat=None, differentials=None, copy=True):
1551        super().__init__(lon, lat, differentials=differentials, copy=copy)
1552
1553    @classproperty
1554    def _compatible_differentials(cls):
1555        return [UnitSphericalDifferential, UnitSphericalCosLatDifferential,
1556                SphericalDifferential, SphericalCosLatDifferential,
1557                RadialDifferential]
1558
1559    # Could let the metaclass define these automatically, but good to have
1560    # a bit clearer docstrings.
1561    @property
1562    def lon(self):
1563        """
1564        The longitude of the point(s).
1565        """
1566        return self._lon
1567
1568    @property
1569    def lat(self):
1570        """
1571        The latitude of the point(s).
1572        """
1573        return self._lat
1574
1575    def unit_vectors(self):
1576        sinlon, coslon = np.sin(self.lon), np.cos(self.lon)
1577        sinlat, coslat = np.sin(self.lat), np.cos(self.lat)
1578        return {
1579            'lon': CartesianRepresentation(-sinlon, coslon, 0., copy=False),
1580            'lat': CartesianRepresentation(-sinlat*coslon, -sinlat*sinlon,
1581                                           coslat, copy=False)}
1582
1583    def scale_factors(self, omit_coslat=False):
1584        sf_lat = np.broadcast_to(1./u.radian, self.shape, subok=True)
1585        sf_lon = sf_lat if omit_coslat else np.cos(self.lat) / u.radian
1586        return {'lon': sf_lon,
1587                'lat': sf_lat}
1588
1589    def to_cartesian(self):
1590        """
1591        Converts spherical polar coordinates to 3D rectangular cartesian
1592        coordinates.
1593        """
1594        # erfa s2c: Convert [unit]spherical coordinates to Cartesian.
1595        p = erfa_ufunc.s2c(self.lon, self.lat)
1596        return CartesianRepresentation(p, xyz_axis=-1, copy=False)
1597
1598    @classmethod
1599    def from_cartesian(cls, cart):
1600        """
1601        Converts 3D rectangular cartesian coordinates to spherical polar
1602        coordinates.
1603        """
1604        p = cart.get_xyz(xyz_axis=-1)
1605        # erfa c2s: P-vector to [unit]spherical coordinates.
1606        return cls(*erfa_ufunc.c2s(p), copy=False)
1607
1608    def represent_as(self, other_class, differential_class=None):
1609        # Take a short cut if the other class is a spherical representation
1610        # TODO! for differential_class. This cannot (currently) be implemented
1611        # like in the other Representations since `_re_represent_differentials`
1612        # keeps differentials' unit keys, but this can result in a mismatch
1613        # between the UnitSpherical expected key (e.g. "s") and that expected
1614        # in the other class (here "s / m"). For more info, see PR #11467
1615        if inspect.isclass(other_class) and not differential_class:
1616            if issubclass(other_class, PhysicsSphericalRepresentation):
1617                return other_class(phi=self.lon, theta=90 * u.deg - self.lat,
1618                                   r=1.0, copy=False)
1619            elif issubclass(other_class, SphericalRepresentation):
1620                return other_class(lon=self.lon, lat=self.lat, distance=1.0,
1621                                   copy=False)
1622
1623        return super().represent_as(other_class, differential_class)
1624
1625    def transform(self, matrix):
1626        r"""Transform the unit-spherical coordinates using a 3x3 matrix.
1627
1628        This returns a new representation and does not modify the original one.
1629        Any differentials attached to this representation will also be
1630        transformed.
1631
1632        Parameters
1633        ----------
1634        matrix : (3,3) array-like
1635            A 3x3 matrix, such as a rotation matrix (or a stack of matrices).
1636
1637        Returns
1638        -------
1639        `UnitSphericalRepresentation` or `SphericalRepresentation`
1640            If ``matrix`` is O(3) -- :math:`M \dot M^T = I` -- like a rotation,
1641            then the result is a `UnitSphericalRepresentation`.
1642            All other matrices will change the distance, so the dimensional
1643            representation is used instead.
1644
1645        """
1646        # the transformation matrix does not need to be a rotation matrix,
1647        # so the unit-distance is not guaranteed. For speed, we check if the
1648        # matrix is in O(3) and preserves lengths.
1649        if np.all(is_O3(matrix)):  # remain in unit-rep
1650            xyz = erfa_ufunc.s2c(self.lon, self.lat)
1651            p = erfa_ufunc.rxp(matrix, xyz)
1652            lon, lat = erfa_ufunc.c2s(p)
1653            rep = self.__class__(lon=lon, lat=lat)
1654            # handle differentials
1655            new_diffs = dict((k, d.transform(matrix, self, rep))
1656                             for k, d in self.differentials.items())
1657            rep = rep.with_differentials(new_diffs)
1658
1659        else:  # switch to dimensional representation
1660            rep = self._dimensional_representation(
1661                lon=self.lon, lat=self.lat, distance=1,
1662                differentials=self.differentials
1663            ).transform(matrix)
1664
1665        return rep
1666
1667    def _scale_operation(self, op, *args):
1668        return self._dimensional_representation(
1669            lon=self.lon, lat=self.lat, distance=1.,
1670            differentials=self.differentials)._scale_operation(op, *args)
1671
1672    def __neg__(self):
1673        if any(differential.base_representation is not self.__class__
1674               for differential in self.differentials.values()):
1675            return super().__neg__()
1676
1677        result = self.__class__(self.lon + 180. * u.deg, -self.lat, copy=False)
1678        for key, differential in self.differentials.items():
1679            new_comps = (op(getattr(differential, comp))
1680                         for op, comp in zip((operator.pos, operator.neg),
1681                                             differential.components))
1682            result.differentials[key] = differential.__class__(*new_comps, copy=False)
1683        return result
1684
1685    def norm(self):
1686        """Vector norm.
1687
1688        The norm is the standard Frobenius norm, i.e., the square root of the
1689        sum of the squares of all components with non-angular units, which is
1690        always unity for vectors on the unit sphere.
1691
1692        Returns
1693        -------
1694        norm : `~astropy.units.Quantity` ['dimensionless']
1695            Dimensionless ones, with the same shape as the representation.
1696        """
1697        return u.Quantity(np.ones(self.shape), u.dimensionless_unscaled,
1698                          copy=False)
1699
1700    def _combine_operation(self, op, other, reverse=False):
1701        self._raise_if_has_differentials(op.__name__)
1702
1703        result = self.to_cartesian()._combine_operation(op, other, reverse)
1704        if result is NotImplemented:
1705            return NotImplemented
1706        else:
1707            return self._dimensional_representation.from_cartesian(result)
1708
1709    def mean(self, *args, **kwargs):
1710        """Vector mean.
1711
1712        The representation is converted to cartesian, the means of the x, y,
1713        and z components are calculated, and the result is converted to a
1714        `~astropy.coordinates.SphericalRepresentation`.
1715
1716        Refer to `~numpy.mean` for full documentation of the arguments, noting
1717        that ``axis`` is the entry in the ``shape`` of the representation, and
1718        that the ``out`` argument cannot be used.
1719        """
1720        self._raise_if_has_differentials('mean')
1721        return self._dimensional_representation.from_cartesian(
1722            self.to_cartesian().mean(*args, **kwargs))
1723
1724    def sum(self, *args, **kwargs):
1725        """Vector sum.
1726
1727        The representation is converted to cartesian, the sums of the x, y,
1728        and z components are calculated, and the result is converted to a
1729        `~astropy.coordinates.SphericalRepresentation`.
1730
1731        Refer to `~numpy.sum` for full documentation of the arguments, noting
1732        that ``axis`` is the entry in the ``shape`` of the representation, and
1733        that the ``out`` argument cannot be used.
1734        """
1735        self._raise_if_has_differentials('sum')
1736        return self._dimensional_representation.from_cartesian(
1737            self.to_cartesian().sum(*args, **kwargs))
1738
1739    def cross(self, other):
1740        """Cross product of two representations.
1741
1742        The calculation is done by converting both ``self`` and ``other``
1743        to `~astropy.coordinates.CartesianRepresentation`, and converting the
1744        result back to `~astropy.coordinates.SphericalRepresentation`.
1745
1746        Parameters
1747        ----------
1748        other : `~astropy.coordinates.BaseRepresentation` subclass instance
1749            The representation to take the cross product with.
1750
1751        Returns
1752        -------
1753        cross_product : `~astropy.coordinates.SphericalRepresentation`
1754            With vectors perpendicular to both ``self`` and ``other``.
1755        """
1756        self._raise_if_has_differentials('cross')
1757        return self._dimensional_representation.from_cartesian(
1758            self.to_cartesian().cross(other))
1759
1760
1761class RadialRepresentation(BaseRepresentation):
1762    """
1763    Representation of the distance of points from the origin.
1764
1765    Note that this is mostly intended as an internal helper representation.
1766    It can do little else but being used as a scale in multiplication.
1767
1768    Parameters
1769    ----------
1770    distance : `~astropy.units.Quantity` ['length']
1771        The distance of the point(s) from the origin.
1772
1773    differentials : dict, `~astropy.coordinates.BaseDifferential`, optional
1774        Any differential classes that should be associated with this
1775        representation. The input must either be a single `~astropy.coordinates.BaseDifferential`
1776        instance (see `._compatible_differentials` for valid types), or a
1777        dictionary of of differential instances with keys set to a string
1778        representation of the SI unit with which the differential (derivative)
1779        is taken. For example, for a velocity differential on a positional
1780        representation, the key would be ``'s'`` for seconds, indicating that
1781        the derivative is a time derivative.
1782
1783    copy : bool, optional
1784        If `True` (default), arrays will be copied. If `False`, arrays will
1785        be references, though possibly broadcast to ensure matching shapes.
1786    """
1787
1788    attr_classes = {'distance': u.Quantity}
1789
1790    def __init__(self, distance, differentials=None, copy=True):
1791        super().__init__(distance, differentials=differentials, copy=copy)
1792
1793    @property
1794    def distance(self):
1795        """
1796        The distance from the origin to the point(s).
1797        """
1798        return self._distance
1799
1800    def unit_vectors(self):
1801        """Cartesian unit vectors are undefined for radial representation."""
1802        raise NotImplementedError('Cartesian unit vectors are undefined for '
1803                                  '{} instances'.format(self.__class__))
1804
1805    def scale_factors(self):
1806        l = np.broadcast_to(1.*u.one, self.shape, subok=True)
1807        return {'distance': l}
1808
1809    def to_cartesian(self):
1810        """Cannot convert radial representation to cartesian."""
1811        raise NotImplementedError('cannot convert {} instance to cartesian.'
1812                                  .format(self.__class__))
1813
1814    @classmethod
1815    def from_cartesian(cls, cart):
1816        """
1817        Converts 3D rectangular cartesian coordinates to radial coordinate.
1818        """
1819        return cls(distance=cart.norm(), copy=False)
1820
1821    def __mul__(self, other):
1822        if isinstance(other, BaseRepresentation):
1823            return self.distance * other
1824        else:
1825            return super().__mul__(other)
1826
1827    def norm(self):
1828        """Vector norm.
1829
1830        Just the distance itself.
1831
1832        Returns
1833        -------
1834        norm : `~astropy.units.Quantity` ['dimensionless']
1835            Dimensionless ones, with the same shape as the representation.
1836        """
1837        return self.distance
1838
1839    def _combine_operation(self, op, other, reverse=False):
1840        return NotImplemented
1841
1842    def transform(self, matrix):
1843        """Radial representations cannot be transformed by a Cartesian matrix.
1844
1845        Parameters
1846        ----------
1847        matrix : array-like
1848            The transformation matrix in a Cartesian basis.
1849            Must be a multiplication: a diagonal matrix with identical elements.
1850            Must have shape (..., 3, 3), where the last 2 indices are for the
1851            matrix on each other axis. Make sure that the matrix shape is
1852            compatible with the shape of this representation.
1853
1854        Raises
1855        ------
1856        ValueError
1857            If the matrix is not a multiplication.
1858
1859        """
1860        scl = matrix[..., 0, 0]
1861        # check that the matrix is a scaled identity matrix on the last 2 axes.
1862        if np.any(matrix != scl[..., np.newaxis, np.newaxis] * np.identity(3)):
1863            raise ValueError("Radial representations can only be "
1864                             "transformed by a scaled identity matrix")
1865
1866        return self * scl
1867
1868
1869def _spherical_op_funcs(op, *args):
1870    """For given operator, return functions that adjust lon, lat, distance."""
1871    if op is operator.neg:
1872        return lambda x: x+180*u.deg, operator.neg, operator.pos
1873
1874    try:
1875        scale_sign = np.sign(args[0])
1876    except Exception:
1877        # This should always work, even if perhaps we get a negative distance.
1878        return operator.pos, operator.pos, lambda x: op(x, *args)
1879
1880    scale = abs(args[0])
1881    return (lambda x: x + 180*u.deg*np.signbit(scale_sign),
1882            lambda x: x * scale_sign,
1883            lambda x: op(x, scale))
1884
1885
1886class SphericalRepresentation(BaseRepresentation):
1887    """
1888    Representation of points in 3D spherical coordinates.
1889
1890    Parameters
1891    ----------
1892    lon, lat : `~astropy.units.Quantity` ['angle']
1893        The longitude and latitude of the point(s), in angular units. The
1894        latitude should be between -90 and 90 degrees, and the longitude will
1895        be wrapped to an angle between 0 and 360 degrees. These can also be
1896        instances of `~astropy.coordinates.Angle`,
1897        `~astropy.coordinates.Longitude`, or `~astropy.coordinates.Latitude`.
1898
1899    distance : `~astropy.units.Quantity` ['length']
1900        The distance to the point(s). If the distance is a length, it is
1901        passed to the :class:`~astropy.coordinates.Distance` class, otherwise
1902        it is passed to the :class:`~astropy.units.Quantity` class.
1903
1904    differentials : dict, `~astropy.coordinates.BaseDifferential`, optional
1905        Any differential classes that should be associated with this
1906        representation. The input must either be a single `~astropy.coordinates.BaseDifferential`
1907        instance (see `._compatible_differentials` for valid types), or a
1908        dictionary of of differential instances with keys set to a string
1909        representation of the SI unit with which the differential (derivative)
1910        is taken. For example, for a velocity differential on a positional
1911        representation, the key would be ``'s'`` for seconds, indicating that
1912        the derivative is a time derivative.
1913
1914    copy : bool, optional
1915        If `True` (default), arrays will be copied. If `False`, arrays will
1916        be references, though possibly broadcast to ensure matching shapes.
1917    """
1918
1919    attr_classes = {'lon': Longitude,
1920                    'lat': Latitude,
1921                    'distance': u.Quantity}
1922    _unit_representation = UnitSphericalRepresentation
1923
1924    def __init__(self, lon, lat=None, distance=None, differentials=None,
1925                 copy=True):
1926        super().__init__(lon, lat, distance, copy=copy,
1927                         differentials=differentials)
1928        if (not isinstance(self._distance, Distance)
1929                and self._distance.unit.physical_type == 'length'):
1930            try:
1931                self._distance = Distance(self._distance, copy=False)
1932            except ValueError as e:
1933                if e.args[0].startswith('Distance must be >= 0'):
1934                    raise ValueError("Distance must be >= 0. To allow negative "
1935                                     "distance values, you must explicitly pass"
1936                                     " in a `Distance` object with the the "
1937                                     "argument 'allow_negative=True'.") from e
1938                else:
1939                    raise
1940
1941    @classproperty
1942    def _compatible_differentials(cls):
1943        return [UnitSphericalDifferential, UnitSphericalCosLatDifferential,
1944                SphericalDifferential, SphericalCosLatDifferential,
1945                RadialDifferential]
1946
1947    @property
1948    def lon(self):
1949        """
1950        The longitude of the point(s).
1951        """
1952        return self._lon
1953
1954    @property
1955    def lat(self):
1956        """
1957        The latitude of the point(s).
1958        """
1959        return self._lat
1960
1961    @property
1962    def distance(self):
1963        """
1964        The distance from the origin to the point(s).
1965        """
1966        return self._distance
1967
1968    def unit_vectors(self):
1969        sinlon, coslon = np.sin(self.lon), np.cos(self.lon)
1970        sinlat, coslat = np.sin(self.lat), np.cos(self.lat)
1971        return {
1972            'lon': CartesianRepresentation(-sinlon, coslon, 0., copy=False),
1973            'lat': CartesianRepresentation(-sinlat*coslon, -sinlat*sinlon,
1974                                           coslat, copy=False),
1975            'distance': CartesianRepresentation(coslat*coslon, coslat*sinlon,
1976                                                sinlat, copy=False)}
1977
1978    def scale_factors(self, omit_coslat=False):
1979        sf_lat = self.distance / u.radian
1980        sf_lon = sf_lat if omit_coslat else sf_lat * np.cos(self.lat)
1981        sf_distance = np.broadcast_to(1.*u.one, self.shape, subok=True)
1982        return {'lon': sf_lon,
1983                'lat': sf_lat,
1984                'distance': sf_distance}
1985
1986    def represent_as(self, other_class, differential_class=None):
1987        # Take a short cut if the other class is a spherical representation
1988
1989        if inspect.isclass(other_class):
1990            if issubclass(other_class, PhysicsSphericalRepresentation):
1991                diffs = self._re_represent_differentials(other_class,
1992                                                         differential_class)
1993                return other_class(phi=self.lon, theta=90 * u.deg - self.lat,
1994                                   r=self.distance, differentials=diffs,
1995                                   copy=False)
1996
1997            elif issubclass(other_class, UnitSphericalRepresentation):
1998                diffs = self._re_represent_differentials(other_class,
1999                                                         differential_class)
2000                return other_class(lon=self.lon, lat=self.lat,
2001                                   differentials=diffs, copy=False)
2002
2003        return super().represent_as(other_class, differential_class)
2004
2005    def to_cartesian(self):
2006        """
2007        Converts spherical polar coordinates to 3D rectangular cartesian
2008        coordinates.
2009        """
2010
2011        # We need to convert Distance to Quantity to allow negative values.
2012        if isinstance(self.distance, Distance):
2013            d = self.distance.view(u.Quantity)
2014        else:
2015            d = self.distance
2016
2017        # erfa s2p: Convert spherical polar coordinates to p-vector.
2018        p = erfa_ufunc.s2p(self.lon, self.lat, d)
2019
2020        return CartesianRepresentation(p, xyz_axis=-1, copy=False)
2021
2022    @classmethod
2023    def from_cartesian(cls, cart):
2024        """
2025        Converts 3D rectangular cartesian coordinates to spherical polar
2026        coordinates.
2027        """
2028        p = cart.get_xyz(xyz_axis=-1)
2029        # erfa p2s: P-vector to spherical polar coordinates.
2030        return cls(*erfa_ufunc.p2s(p), copy=False)
2031
2032    def transform(self, matrix):
2033        """Transform the spherical coordinates using a 3x3 matrix.
2034
2035        This returns a new representation and does not modify the original one.
2036        Any differentials attached to this representation will also be
2037        transformed.
2038
2039        Parameters
2040        ----------
2041        matrix : (3,3) array-like
2042            A 3x3 matrix, such as a rotation matrix (or a stack of matrices).
2043
2044        """
2045        xyz = erfa_ufunc.s2c(self.lon, self.lat)
2046        p = erfa_ufunc.rxp(matrix, xyz)
2047        lon, lat, ur = erfa_ufunc.p2s(p)
2048        rep = self.__class__(lon=lon, lat=lat, distance=self.distance * ur)
2049
2050        # handle differentials
2051        new_diffs = dict((k, d.transform(matrix, self, rep))
2052                         for k, d in self.differentials.items())
2053        return rep.with_differentials(new_diffs)
2054
2055    def norm(self):
2056        """Vector norm.
2057
2058        The norm is the standard Frobenius norm, i.e., the square root of the
2059        sum of the squares of all components with non-angular units.  For
2060        spherical coordinates, this is just the absolute value of the distance.
2061
2062        Returns
2063        -------
2064        norm : `astropy.units.Quantity`
2065            Vector norm, with the same shape as the representation.
2066        """
2067        return np.abs(self.distance)
2068
2069    def _scale_operation(self, op, *args):
2070        # TODO: expand special-casing to UnitSpherical and RadialDifferential.
2071        if any(differential.base_representation is not self.__class__
2072               for differential in self.differentials.values()):
2073            return super()._scale_operation(op, *args)
2074
2075        lon_op, lat_op, distance_op = _spherical_op_funcs(op, *args)
2076
2077        result = self.__class__(lon_op(self.lon), lat_op(self.lat),
2078                                distance_op(self.distance), copy=False)
2079        for key, differential in self.differentials.items():
2080            new_comps = (op(getattr(differential, comp)) for op, comp in zip(
2081                (operator.pos, lat_op, distance_op),
2082                differential.components))
2083            result.differentials[key] = differential.__class__(*new_comps, copy=False)
2084        return result
2085
2086
2087class PhysicsSphericalRepresentation(BaseRepresentation):
2088    """
2089    Representation of points in 3D spherical coordinates (using the physics
2090    convention of using ``phi`` and ``theta`` for azimuth and inclination
2091    from the pole).
2092
2093    Parameters
2094    ----------
2095    phi, theta : `~astropy.units.Quantity` or str
2096        The azimuth and inclination of the point(s), in angular units. The
2097        inclination should be between 0 and 180 degrees, and the azimuth will
2098        be wrapped to an angle between 0 and 360 degrees. These can also be
2099        instances of `~astropy.coordinates.Angle`.  If ``copy`` is False, `phi`
2100        will be changed inplace if it is not between 0 and 360 degrees.
2101
2102    r : `~astropy.units.Quantity`
2103        The distance to the point(s). If the distance is a length, it is
2104        passed to the :class:`~astropy.coordinates.Distance` class, otherwise
2105        it is passed to the :class:`~astropy.units.Quantity` class.
2106
2107    differentials : dict, `PhysicsSphericalDifferential`, optional
2108        Any differential classes that should be associated with this
2109        representation. The input must either be a single
2110        `PhysicsSphericalDifferential` instance, or a dictionary of of
2111        differential instances with keys set to a string representation of the
2112        SI unit with which the differential (derivative) is taken. For example,
2113        for a velocity differential on a positional representation, the key
2114        would be ``'s'`` for seconds, indicating that the derivative is a time
2115        derivative.
2116
2117    copy : bool, optional
2118        If `True` (default), arrays will be copied. If `False`, arrays will
2119        be references, though possibly broadcast to ensure matching shapes.
2120    """
2121
2122    attr_classes = {'phi': Angle,
2123                    'theta': Angle,
2124                    'r': u.Quantity}
2125
2126    def __init__(self, phi, theta=None, r=None, differentials=None, copy=True):
2127        super().__init__(phi, theta, r, copy=copy, differentials=differentials)
2128
2129        # Wrap/validate phi/theta
2130        # Note that _phi already holds our own copy if copy=True.
2131        self._phi.wrap_at(360 * u.deg, inplace=True)
2132
2133        # This invalid catch block can be removed when the minimum numpy
2134        # version is >= 1.19 (NUMPY_LT_1_19)
2135        with np.errstate(invalid='ignore'):
2136            if np.any(self._theta < 0.*u.deg) or np.any(self._theta > 180.*u.deg):
2137                raise ValueError('Inclination angle(s) must be within '
2138                                 '0 deg <= angle <= 180 deg, '
2139                                 'got {}'.format(theta.to(u.degree)))
2140
2141        if self._r.unit.physical_type == 'length':
2142            self._r = self._r.view(Distance)
2143
2144    @property
2145    def phi(self):
2146        """
2147        The azimuth of the point(s).
2148        """
2149        return self._phi
2150
2151    @property
2152    def theta(self):
2153        """
2154        The elevation of the point(s).
2155        """
2156        return self._theta
2157
2158    @property
2159    def r(self):
2160        """
2161        The distance from the origin to the point(s).
2162        """
2163        return self._r
2164
2165    def unit_vectors(self):
2166        sinphi, cosphi = np.sin(self.phi), np.cos(self.phi)
2167        sintheta, costheta = np.sin(self.theta), np.cos(self.theta)
2168        return {
2169            'phi': CartesianRepresentation(-sinphi, cosphi, 0., copy=False),
2170            'theta': CartesianRepresentation(costheta*cosphi,
2171                                             costheta*sinphi,
2172                                             -sintheta, copy=False),
2173            'r': CartesianRepresentation(sintheta*cosphi, sintheta*sinphi,
2174                                         costheta, copy=False)}
2175
2176    def scale_factors(self):
2177        r = self.r / u.radian
2178        sintheta = np.sin(self.theta)
2179        l = np.broadcast_to(1.*u.one, self.shape, subok=True)
2180        return {'phi': r * sintheta,
2181                'theta': r,
2182                'r': l}
2183
2184    def represent_as(self, other_class, differential_class=None):
2185        # Take a short cut if the other class is a spherical representation
2186
2187        if inspect.isclass(other_class):
2188            if issubclass(other_class, SphericalRepresentation):
2189                diffs = self._re_represent_differentials(other_class,
2190                                                         differential_class)
2191                return other_class(lon=self.phi, lat=90 * u.deg - self.theta,
2192                                   distance=self.r, differentials=diffs,
2193                                   copy=False)
2194            elif issubclass(other_class, UnitSphericalRepresentation):
2195                diffs = self._re_represent_differentials(other_class,
2196                                                         differential_class)
2197                return other_class(lon=self.phi, lat=90 * u.deg - self.theta,
2198                                   differentials=diffs, copy=False)
2199
2200        return super().represent_as(other_class, differential_class)
2201
2202    def to_cartesian(self):
2203        """
2204        Converts spherical polar coordinates to 3D rectangular cartesian
2205        coordinates.
2206        """
2207
2208        # We need to convert Distance to Quantity to allow negative values.
2209        if isinstance(self.r, Distance):
2210            d = self.r.view(u.Quantity)
2211        else:
2212            d = self.r
2213
2214        x = d * np.sin(self.theta) * np.cos(self.phi)
2215        y = d * np.sin(self.theta) * np.sin(self.phi)
2216        z = d * np.cos(self.theta)
2217
2218        return CartesianRepresentation(x=x, y=y, z=z, copy=False)
2219
2220    @classmethod
2221    def from_cartesian(cls, cart):
2222        """
2223        Converts 3D rectangular cartesian coordinates to spherical polar
2224        coordinates.
2225        """
2226
2227        s = np.hypot(cart.x, cart.y)
2228        r = np.hypot(s, cart.z)
2229
2230        phi = np.arctan2(cart.y, cart.x)
2231        theta = np.arctan2(s, cart.z)
2232
2233        return cls(phi=phi, theta=theta, r=r, copy=False)
2234
2235    def transform(self, matrix):
2236        """Transform the spherical coordinates using a 3x3 matrix.
2237
2238        This returns a new representation and does not modify the original one.
2239        Any differentials attached to this representation will also be
2240        transformed.
2241
2242        Parameters
2243        ----------
2244        matrix : (3,3) array-like
2245            A 3x3 matrix, such as a rotation matrix (or a stack of matrices).
2246
2247        """
2248        # apply transformation in unit-spherical coordinates
2249        xyz = erfa_ufunc.s2c(self.phi, 90*u.deg-self.theta)
2250        p = erfa_ufunc.rxp(matrix, xyz)
2251        lon, lat, ur = erfa_ufunc.p2s(p)  # `ur` is transformed unit-`r`
2252        # create transformed physics-spherical representation,
2253        # reapplying the distance scaling
2254        rep = self.__class__(phi=lon, theta=90*u.deg-lat, r=self.r * ur)
2255
2256        new_diffs = dict((k, d.transform(matrix, self, rep))
2257                         for k, d in self.differentials.items())
2258        return rep.with_differentials(new_diffs)
2259
2260    def norm(self):
2261        """Vector norm.
2262
2263        The norm is the standard Frobenius norm, i.e., the square root of the
2264        sum of the squares of all components with non-angular units.  For
2265        spherical coordinates, this is just the absolute value of the radius.
2266
2267        Returns
2268        -------
2269        norm : `astropy.units.Quantity`
2270            Vector norm, with the same shape as the representation.
2271        """
2272        return np.abs(self.r)
2273
2274    def _scale_operation(self, op, *args):
2275        if any(differential.base_representation is not self.__class__
2276               for differential in self.differentials.values()):
2277            return super()._scale_operation(op, *args)
2278
2279        phi_op, adjust_theta_sign, r_op = _spherical_op_funcs(op, *args)
2280        # Also run phi_op on theta to ensure theta remains between 0 and 180:
2281        # any time the scale is negative, we do -theta + 180 degrees.
2282        result = self.__class__(phi_op(self.phi),
2283                                phi_op(adjust_theta_sign(self.theta)),
2284                                r_op(self.r), copy=False)
2285        for key, differential in self.differentials.items():
2286            new_comps = (op(getattr(differential, comp)) for op, comp in zip(
2287                (operator.pos, adjust_theta_sign, r_op),
2288                differential.components))
2289            result.differentials[key] = differential.__class__(*new_comps, copy=False)
2290        return result
2291
2292
2293class CylindricalRepresentation(BaseRepresentation):
2294    """
2295    Representation of points in 3D cylindrical coordinates.
2296
2297    Parameters
2298    ----------
2299    rho : `~astropy.units.Quantity`
2300        The distance from the z axis to the point(s).
2301
2302    phi : `~astropy.units.Quantity` or str
2303        The azimuth of the point(s), in angular units, which will be wrapped
2304        to an angle between 0 and 360 degrees. This can also be instances of
2305        `~astropy.coordinates.Angle`,
2306
2307    z : `~astropy.units.Quantity`
2308        The z coordinate(s) of the point(s)
2309
2310    differentials : dict, `CylindricalDifferential`, optional
2311        Any differential classes that should be associated with this
2312        representation. The input must either be a single
2313        `CylindricalDifferential` instance, or a dictionary of of differential
2314        instances with keys set to a string representation of the SI unit with
2315        which the differential (derivative) is taken. For example, for a
2316        velocity differential on a positional representation, the key would be
2317        ``'s'`` for seconds, indicating that the derivative is a time
2318        derivative.
2319
2320    copy : bool, optional
2321        If `True` (default), arrays will be copied. If `False`, arrays will
2322        be references, though possibly broadcast to ensure matching shapes.
2323    """
2324
2325    attr_classes = {'rho': u.Quantity,
2326                    'phi': Angle,
2327                    'z': u.Quantity}
2328
2329    def __init__(self, rho, phi=None, z=None, differentials=None, copy=True):
2330        super().__init__(rho, phi, z, copy=copy, differentials=differentials)
2331
2332        if not self._rho.unit.is_equivalent(self._z.unit):
2333            raise u.UnitsError("rho and z should have matching physical types")
2334
2335    @property
2336    def rho(self):
2337        """
2338        The distance of the point(s) from the z-axis.
2339        """
2340        return self._rho
2341
2342    @property
2343    def phi(self):
2344        """
2345        The azimuth of the point(s).
2346        """
2347        return self._phi
2348
2349    @property
2350    def z(self):
2351        """
2352        The height of the point(s).
2353        """
2354        return self._z
2355
2356    def unit_vectors(self):
2357        sinphi, cosphi = np.sin(self.phi), np.cos(self.phi)
2358        l = np.broadcast_to(1., self.shape)
2359        return {
2360            'rho': CartesianRepresentation(cosphi, sinphi, 0, copy=False),
2361            'phi': CartesianRepresentation(-sinphi, cosphi, 0, copy=False),
2362            'z': CartesianRepresentation(0, 0, l, unit=u.one, copy=False)}
2363
2364    def scale_factors(self):
2365        rho = self.rho / u.radian
2366        l = np.broadcast_to(1.*u.one, self.shape, subok=True)
2367        return {'rho': l,
2368                'phi': rho,
2369                'z': l}
2370
2371    @classmethod
2372    def from_cartesian(cls, cart):
2373        """
2374        Converts 3D rectangular cartesian coordinates to cylindrical polar
2375        coordinates.
2376        """
2377
2378        rho = np.hypot(cart.x, cart.y)
2379        phi = np.arctan2(cart.y, cart.x)
2380        z = cart.z
2381
2382        return cls(rho=rho, phi=phi, z=z, copy=False)
2383
2384    def to_cartesian(self):
2385        """
2386        Converts cylindrical polar coordinates to 3D rectangular cartesian
2387        coordinates.
2388        """
2389        x = self.rho * np.cos(self.phi)
2390        y = self.rho * np.sin(self.phi)
2391        z = self.z
2392
2393        return CartesianRepresentation(x=x, y=y, z=z, copy=False)
2394
2395    def _scale_operation(self, op, *args):
2396        if any(differential.base_representation is not self.__class__
2397               for differential in self.differentials.values()):
2398            return super()._scale_operation(op, *args)
2399
2400        phi_op, _, rho_op = _spherical_op_funcs(op, *args)
2401        z_op = lambda x: op(x, *args)
2402
2403        result = self.__class__(rho_op(self.rho), phi_op(self.phi),
2404                                z_op(self.z), copy=False)
2405        for key, differential in self.differentials.items():
2406            new_comps = (op(getattr(differential, comp)) for op, comp in zip(
2407                (rho_op, operator.pos, z_op), differential.components))
2408            result.differentials[key] = differential.__class__(*new_comps, copy=False)
2409        return result
2410
2411
2412class BaseDifferential(BaseRepresentationOrDifferential):
2413    r"""A base class representing differentials of representations.
2414
2415    These represent differences or derivatives along each component.
2416    E.g., for physics spherical coordinates, these would be
2417    :math:`\delta r, \delta \theta, \delta \phi`.
2418
2419    Parameters
2420    ----------
2421    d_comp1, d_comp2, d_comp3 : `~astropy.units.Quantity` or subclass
2422        The components of the 3D differentials.  The names are the keys and the
2423        subclasses the values of the ``attr_classes`` attribute.
2424    copy : bool, optional
2425        If `True` (default), arrays will be copied. If `False`, arrays will
2426        be references, though possibly broadcast to ensure matching shapes.
2427
2428    Notes
2429    -----
2430    All differential representation classes should subclass this base class,
2431    and define an ``base_representation`` attribute with the class of the
2432    regular `~astropy.coordinates.BaseRepresentation` for which differential
2433    coordinates are provided. This will set up a default ``attr_classes``
2434    instance with names equal to the base component names prefixed by ``d_``,
2435    and all classes set to `~astropy.units.Quantity`, plus properties to access
2436    those, and a default ``__init__`` for initialization.
2437    """
2438
2439    def __init_subclass__(cls, **kwargs):
2440        """Set default ``attr_classes`` and component getters on a Differential.
2441        class BaseDifferential(BaseRepresentationOrDifferential):
2442
2443        For these, the components are those of the base representation prefixed
2444        by 'd_', and the class is `~astropy.units.Quantity`.
2445        """
2446
2447        # Don't do anything for base helper classes.
2448        if cls.__name__ in ('BaseDifferential', 'BaseSphericalDifferential',
2449                            'BaseSphericalCosLatDifferential'):
2450            return
2451
2452        if not hasattr(cls, 'base_representation'):
2453            raise NotImplementedError('Differential representations must have a'
2454                                      '"base_representation" class attribute.')
2455
2456        # If not defined explicitly, create attr_classes.
2457        if not hasattr(cls, 'attr_classes'):
2458            base_attr_classes = cls.base_representation.attr_classes
2459            cls.attr_classes = {'d_' + c: u.Quantity
2460                                for c in base_attr_classes}
2461
2462        repr_name = cls.get_name()
2463        if repr_name in DIFFERENTIAL_CLASSES:
2464            raise ValueError(f"Differential class {repr_name} already defined")
2465
2466        DIFFERENTIAL_CLASSES[repr_name] = cls
2467        _invalidate_reprdiff_cls_hash()
2468
2469        # If not defined explicitly, create properties for the components.
2470        for component in cls.attr_classes:
2471            if not hasattr(cls, component):
2472                setattr(cls, component,
2473                        property(_make_getter(component),
2474                                 doc=f"Component '{component}' of the Differential."))
2475
2476        super().__init_subclass__(**kwargs)
2477
2478    @classmethod
2479    def _check_base(cls, base):
2480        if cls not in base._compatible_differentials:
2481            raise TypeError(f"Differential class {cls} is not compatible with the "
2482                            f"base (representation) class {base.__class__}")
2483
2484    def _get_deriv_key(self, base):
2485        """Given a base (representation instance), determine the unit of the
2486        derivative by removing the representation unit from the component units
2487        of this differential.
2488        """
2489
2490        # This check is just a last resort so we don't return a strange unit key
2491        # from accidentally passing in the wrong base.
2492        self._check_base(base)
2493
2494        for name in base.components:
2495            comp = getattr(base, name)
2496            d_comp = getattr(self, f'd_{name}', None)
2497            if d_comp is not None:
2498                d_unit = comp.unit / d_comp.unit
2499
2500                # This is quite a bit faster than using to_system() or going
2501                # through Quantity()
2502                d_unit_si = d_unit.decompose(u.si.bases)
2503                d_unit_si._scale = 1  # remove the scale from the unit
2504
2505                return str(d_unit_si)
2506
2507        else:
2508            raise RuntimeError("Invalid representation-differential units! This"
2509                               " likely happened because either the "
2510                               "representation or the associated differential "
2511                               "have non-standard units. Check that the input "
2512                               "positional data have positional units, and the "
2513                               "input velocity data have velocity units, or "
2514                               "are both dimensionless.")
2515
2516    @classmethod
2517    def _get_base_vectors(cls, base):
2518        """Get unit vectors and scale factors from base.
2519
2520        Parameters
2521        ----------
2522        base : instance of ``self.base_representation``
2523            The points for which the unit vectors and scale factors should be
2524            retrieved.
2525
2526        Returns
2527        -------
2528        unit_vectors : dict of `CartesianRepresentation`
2529            In the directions of the coordinates of base.
2530        scale_factors : dict of `~astropy.units.Quantity`
2531            Scale factors for each of the coordinates
2532
2533        Raises
2534        ------
2535        TypeError : if the base is not of the correct type
2536        """
2537        cls._check_base(base)
2538        return base.unit_vectors(), base.scale_factors()
2539
2540    def to_cartesian(self, base):
2541        """Convert the differential to 3D rectangular cartesian coordinates.
2542
2543        Parameters
2544        ----------
2545        base : instance of ``self.base_representation``
2546            The points for which the differentials are to be converted: each of
2547            the components is multiplied by its unit vectors and scale factors.
2548
2549        Returns
2550        -------
2551        `CartesianDifferential`
2552            This object, converted.
2553
2554        """
2555        base_e, base_sf = self._get_base_vectors(base)
2556        return functools.reduce(
2557            operator.add, (getattr(self, d_c) * base_sf[c] * base_e[c]
2558                           for d_c, c in zip(self.components, base.components)))
2559
2560    @classmethod
2561    def from_cartesian(cls, other, base):
2562        """Convert the differential from 3D rectangular cartesian coordinates to
2563        the desired class.
2564
2565        Parameters
2566        ----------
2567        other
2568            The object to convert into this differential.
2569        base : `BaseRepresentation`
2570            The points for which the differentials are to be converted: each of
2571            the components is multiplied by its unit vectors and scale factors.
2572            Will be converted to ``cls.base_representation`` if needed.
2573
2574        Returns
2575        -------
2576        `BaseDifferential` subclass instance
2577            A new differential object that is this class' type.
2578        """
2579        base = base.represent_as(cls.base_representation)
2580        base_e, base_sf = cls._get_base_vectors(base)
2581        return cls(*(other.dot(e / base_sf[component])
2582                     for component, e in base_e.items()), copy=False)
2583
2584    def represent_as(self, other_class, base):
2585        """Convert coordinates to another representation.
2586
2587        If the instance is of the requested class, it is returned unmodified.
2588        By default, conversion is done via cartesian coordinates.
2589
2590        Parameters
2591        ----------
2592        other_class : `~astropy.coordinates.BaseRepresentation` subclass
2593            The type of representation to turn the coordinates into.
2594        base : instance of ``self.base_representation``
2595            Base relative to which the differentials are defined.  If the other
2596            class is a differential representation, the base will be converted
2597            to its ``base_representation``.
2598        """
2599        if other_class is self.__class__:
2600            return self
2601
2602        # The default is to convert via cartesian coordinates.
2603        self_cartesian = self.to_cartesian(base)
2604        if issubclass(other_class, BaseDifferential):
2605            return other_class.from_cartesian(self_cartesian, base)
2606        else:
2607            return other_class.from_cartesian(self_cartesian)
2608
2609    @classmethod
2610    def from_representation(cls, representation, base):
2611        """Create a new instance of this representation from another one.
2612
2613        Parameters
2614        ----------
2615        representation : `~astropy.coordinates.BaseRepresentation` instance
2616            The presentation that should be converted to this class.
2617        base : instance of ``cls.base_representation``
2618            The base relative to which the differentials will be defined. If
2619            the representation is a differential itself, the base will be
2620            converted to its ``base_representation`` to help convert it.
2621        """
2622        if isinstance(representation, BaseDifferential):
2623            cartesian = representation.to_cartesian(
2624                base.represent_as(representation.base_representation))
2625        else:
2626            cartesian = representation.to_cartesian()
2627
2628        return cls.from_cartesian(cartesian, base)
2629
2630    def transform(self, matrix, base, transformed_base):
2631        """Transform differential using a 3x3 matrix in a Cartesian basis.
2632
2633        This returns a new differential and does not modify the original one.
2634
2635        Parameters
2636        ----------
2637        matrix : (3,3) array-like
2638            A 3x3 (or stack thereof) matrix, such as a rotation matrix.
2639        base : instance of ``cls.base_representation``
2640            Base relative to which the differentials are defined.  If the other
2641            class is a differential representation, the base will be converted
2642            to its ``base_representation``.
2643        transformed_base : instance of ``cls.base_representation``
2644            Base relative to which the transformed differentials are defined.
2645            If the other class is a differential representation, the base will
2646            be converted to its ``base_representation``.
2647        """
2648        # route transformation through Cartesian
2649        cdiff = self.represent_as(CartesianDifferential, base=base
2650                                  ).transform(matrix)
2651        # move back to original representation
2652        diff = cdiff.represent_as(self.__class__, transformed_base)
2653        return diff
2654
2655    def _scale_operation(self, op, *args, scaled_base=False):
2656        """Scale all components.
2657
2658        Parameters
2659        ----------
2660        op : `~operator` callable
2661            Operator to apply (e.g., `~operator.mul`, `~operator.neg`, etc.
2662        *args
2663            Any arguments required for the operator (typically, what is to
2664            be multiplied with, divided by).
2665        scaled_base : bool, optional
2666            Whether the base was scaled the same way. This affects whether
2667            differential components should be scaled. For instance, a differential
2668            in longitude should not be scaled if its spherical base is scaled
2669            in radius.
2670        """
2671        scaled_attrs = [op(getattr(self, c), *args) for c in self.components]
2672        return self.__class__(*scaled_attrs, copy=False)
2673
2674    def _combine_operation(self, op, other, reverse=False):
2675        """Combine two differentials, or a differential with a representation.
2676
2677        If ``other`` is of the same differential type as ``self``, the
2678        components will simply be combined.  If ``other`` is a representation,
2679        it will be used as a base for which to evaluate the differential,
2680        and the result is a new representation.
2681
2682        Parameters
2683        ----------
2684        op : `~operator` callable
2685            Operator to apply (e.g., `~operator.add`, `~operator.sub`, etc.
2686        other : `~astropy.coordinates.BaseRepresentation` subclass instance
2687            The other differential or representation.
2688        reverse : bool
2689            Whether the operands should be reversed (e.g., as we got here via
2690            ``self.__rsub__`` because ``self`` is a subclass of ``other``).
2691        """
2692        if isinstance(self, type(other)):
2693            first, second = (self, other) if not reverse else (other, self)
2694            return self.__class__(*[op(getattr(first, c), getattr(second, c))
2695                                    for c in self.components])
2696        else:
2697            try:
2698                self_cartesian = self.to_cartesian(other)
2699            except TypeError:
2700                return NotImplemented
2701
2702            return other._combine_operation(op, self_cartesian, not reverse)
2703
2704    def __sub__(self, other):
2705        # avoid "differential - representation".
2706        if isinstance(other, BaseRepresentation):
2707            return NotImplemented
2708        return super().__sub__(other)
2709
2710    def norm(self, base=None):
2711        """Vector norm.
2712
2713        The norm is the standard Frobenius norm, i.e., the square root of the
2714        sum of the squares of all components with non-angular units.
2715
2716        Parameters
2717        ----------
2718        base : instance of ``self.base_representation``
2719            Base relative to which the differentials are defined. This is
2720            required to calculate the physical size of the differential for
2721            all but Cartesian differentials or radial differentials.
2722
2723        Returns
2724        -------
2725        norm : `astropy.units.Quantity`
2726            Vector norm, with the same shape as the representation.
2727        """
2728        # RadialDifferential overrides this function, so there is no handling here
2729        if not isinstance(self, CartesianDifferential) and base is None:
2730            raise ValueError("`base` must be provided to calculate the norm of a"
2731                             f" {type(self).__name__}")
2732        return self.to_cartesian(base).norm()
2733
2734
2735class CartesianDifferential(BaseDifferential):
2736    """Differentials in of points in 3D cartesian coordinates.
2737
2738    Parameters
2739    ----------
2740    d_x, d_y, d_z : `~astropy.units.Quantity` or array
2741        The x, y, and z coordinates of the differentials. If ``d_x``, ``d_y``,
2742        and ``d_z`` have different shapes, they should be broadcastable. If not
2743        quantities, ``unit`` should be set.  If only ``d_x`` is given, it is
2744        assumed that it contains an array with the 3 coordinates stored along
2745        ``xyz_axis``.
2746    unit : `~astropy.units.Unit` or str
2747        If given, the differentials will be converted to this unit (or taken to
2748        be in this unit if not given.
2749    xyz_axis : int, optional
2750        The axis along which the coordinates are stored when a single array is
2751        provided instead of distinct ``d_x``, ``d_y``, and ``d_z`` (default: 0).
2752    copy : bool, optional
2753        If `True` (default), arrays will be copied. If `False`, arrays will
2754        be references, though possibly broadcast to ensure matching shapes.
2755    """
2756    base_representation = CartesianRepresentation
2757    _d_xyz = None
2758
2759    def __init__(self, d_x, d_y=None, d_z=None, unit=None, xyz_axis=None,
2760                 copy=True):
2761
2762        if d_y is None and d_z is None:
2763            if isinstance(d_x, np.ndarray) and d_x.dtype.kind not in 'OV':
2764                # Short-cut for 3-D array input.
2765                d_x = u.Quantity(d_x, unit, copy=copy, subok=True)
2766                # Keep a link to the array with all three coordinates
2767                # so that we can return it quickly if needed in get_xyz.
2768                self._d_xyz = d_x
2769                if xyz_axis:
2770                    d_x = np.moveaxis(d_x, xyz_axis, 0)
2771                    self._xyz_axis = xyz_axis
2772                else:
2773                    self._xyz_axis = 0
2774
2775                self._d_x, self._d_y, self._d_z = d_x
2776                return
2777
2778            else:
2779                d_x, d_y, d_z = d_x
2780
2781        if xyz_axis is not None:
2782            raise ValueError("xyz_axis should only be set if d_x, d_y, and d_z "
2783                             "are in a single array passed in through d_x, "
2784                             "i.e., d_y and d_z should not be not given.")
2785
2786        if d_y is None or d_z is None:
2787            raise ValueError("d_x, d_y, and d_z are required to instantiate {}"
2788                             .format(self.__class__.__name__))
2789
2790        if unit is not None:
2791            d_x = u.Quantity(d_x, unit, copy=copy, subok=True)
2792            d_y = u.Quantity(d_y, unit, copy=copy, subok=True)
2793            d_z = u.Quantity(d_z, unit, copy=copy, subok=True)
2794            copy = False
2795
2796        super().__init__(d_x, d_y, d_z, copy=copy)
2797        if not (self._d_x.unit.is_equivalent(self._d_y.unit) and
2798                self._d_x.unit.is_equivalent(self._d_z.unit)):
2799            raise u.UnitsError('d_x, d_y and d_z should have equivalent units.')
2800
2801    def to_cartesian(self, base=None):
2802        return CartesianRepresentation(*[getattr(self, c) for c
2803                                         in self.components])
2804
2805    @classmethod
2806    def from_cartesian(cls, other, base=None):
2807        return cls(*[getattr(other, c) for c in other.components])
2808
2809    def transform(self, matrix, base=None, transformed_base=None):
2810        """Transform differentials using a 3x3 matrix in a Cartesian basis.
2811
2812        This returns a new differential and does not modify the original one.
2813
2814        Parameters
2815        ----------
2816        matrix : (3,3) array-like
2817            A 3x3 (or stack thereof) matrix, such as a rotation matrix.
2818        base, transformed_base : `~astropy.coordinates.CartesianRepresentation` or None, optional
2819            Not used in the Cartesian transformation.
2820        """
2821        # erfa rxp: Multiply a p-vector by an r-matrix.
2822        p = erfa_ufunc.rxp(matrix, self.get_d_xyz(xyz_axis=-1))
2823
2824        return self.__class__(p, xyz_axis=-1, copy=False)
2825
2826    def get_d_xyz(self, xyz_axis=0):
2827        """Return a vector array of the x, y, and z coordinates.
2828
2829        Parameters
2830        ----------
2831        xyz_axis : int, optional
2832            The axis in the final array along which the x, y, z components
2833            should be stored (default: 0).
2834
2835        Returns
2836        -------
2837        d_xyz : `~astropy.units.Quantity`
2838            With dimension 3 along ``xyz_axis``.  Note that, if possible,
2839            this will be a view.
2840        """
2841        if self._d_xyz is not None:
2842            if self._xyz_axis == xyz_axis:
2843                return self._d_xyz
2844            else:
2845                return np.moveaxis(self._d_xyz, self._xyz_axis, xyz_axis)
2846
2847        # Create combined array.  TO DO: keep it in _d_xyz for repeated use?
2848        # But then in-place changes have to cancel it. Likely best to
2849        # also update components.
2850        return np.stack([self._d_x, self._d_y, self._d_z], axis=xyz_axis)
2851
2852    d_xyz = property(get_d_xyz)
2853
2854
2855class BaseSphericalDifferential(BaseDifferential):
2856    def _d_lon_coslat(self, base):
2857        """Convert longitude differential d_lon to d_lon_coslat.
2858
2859        Parameters
2860        ----------
2861        base : instance of ``cls.base_representation``
2862            The base from which the latitude will be taken.
2863        """
2864        self._check_base(base)
2865        return self.d_lon * np.cos(base.lat)
2866
2867    @classmethod
2868    def _get_d_lon(cls, d_lon_coslat, base):
2869        """Convert longitude differential d_lon_coslat to d_lon.
2870
2871        Parameters
2872        ----------
2873        d_lon_coslat : `~astropy.units.Quantity`
2874            Longitude differential that includes ``cos(lat)``.
2875        base : instance of ``cls.base_representation``
2876            The base from which the latitude will be taken.
2877        """
2878        cls._check_base(base)
2879        return d_lon_coslat / np.cos(base.lat)
2880
2881    def _combine_operation(self, op, other, reverse=False):
2882        """Combine two differentials, or a differential with a representation.
2883
2884        If ``other`` is of the same differential type as ``self``, the
2885        components will simply be combined.  If both are different parts of
2886        a `~astropy.coordinates.SphericalDifferential` (e.g., a
2887        `~astropy.coordinates.UnitSphericalDifferential` and a
2888        `~astropy.coordinates.RadialDifferential`), they will combined
2889        appropriately.
2890
2891        If ``other`` is a representation, it will be used as a base for which
2892        to evaluate the differential, and the result is a new representation.
2893
2894        Parameters
2895        ----------
2896        op : `~operator` callable
2897            Operator to apply (e.g., `~operator.add`, `~operator.sub`, etc.
2898        other : `~astropy.coordinates.BaseRepresentation` subclass instance
2899            The other differential or representation.
2900        reverse : bool
2901            Whether the operands should be reversed (e.g., as we got here via
2902            ``self.__rsub__`` because ``self`` is a subclass of ``other``).
2903        """
2904        if (isinstance(other, BaseSphericalDifferential) and
2905                not isinstance(self, type(other)) or
2906                isinstance(other, RadialDifferential)):
2907            all_components = set(self.components) | set(other.components)
2908            first, second = (self, other) if not reverse else (other, self)
2909            result_args = {c: op(getattr(first, c, 0.), getattr(second, c, 0.))
2910                           for c in all_components}
2911            return SphericalDifferential(**result_args)
2912
2913        return super()._combine_operation(op, other, reverse)
2914
2915
2916class UnitSphericalDifferential(BaseSphericalDifferential):
2917    """Differential(s) of points on a unit sphere.
2918
2919    Parameters
2920    ----------
2921    d_lon, d_lat : `~astropy.units.Quantity`
2922        The longitude and latitude of the differentials.
2923    copy : bool, optional
2924        If `True` (default), arrays will be copied. If `False`, arrays will
2925        be references, though possibly broadcast to ensure matching shapes.
2926    """
2927    base_representation = UnitSphericalRepresentation
2928
2929    @classproperty
2930    def _dimensional_differential(cls):
2931        return SphericalDifferential
2932
2933    def __init__(self, d_lon, d_lat=None, copy=True):
2934        super().__init__(d_lon, d_lat, copy=copy)
2935        if not self._d_lon.unit.is_equivalent(self._d_lat.unit):
2936            raise u.UnitsError('d_lon and d_lat should have equivalent units.')
2937
2938    @classmethod
2939    def from_cartesian(cls, other, base):
2940        # Go via the dimensional equivalent, so that the longitude and latitude
2941        # differentials correctly take into account the norm of the base.
2942        dimensional = cls._dimensional_differential.from_cartesian(other, base)
2943        return dimensional.represent_as(cls)
2944
2945    def to_cartesian(self, base):
2946        if isinstance(base, SphericalRepresentation):
2947            scale = base.distance
2948        elif isinstance(base, PhysicsSphericalRepresentation):
2949            scale = base.r
2950        else:
2951            return super().to_cartesian(base)
2952
2953        base = base.represent_as(UnitSphericalRepresentation)
2954        return scale * super().to_cartesian(base)
2955
2956    def represent_as(self, other_class, base=None):
2957        # Only have enough information to represent other unit-spherical.
2958        if issubclass(other_class, UnitSphericalCosLatDifferential):
2959            return other_class(self._d_lon_coslat(base), self.d_lat)
2960
2961        return super().represent_as(other_class, base)
2962
2963    @classmethod
2964    def from_representation(cls, representation, base=None):
2965        # All spherical differentials can be done without going to Cartesian,
2966        # though CosLat needs base for the latitude.
2967        if isinstance(representation, SphericalDifferential):
2968            return cls(representation.d_lon, representation.d_lat)
2969        elif isinstance(representation, (SphericalCosLatDifferential,
2970                                         UnitSphericalCosLatDifferential)):
2971            d_lon = cls._get_d_lon(representation.d_lon_coslat, base)
2972            return cls(d_lon, representation.d_lat)
2973        elif isinstance(representation, PhysicsSphericalDifferential):
2974            return cls(representation.d_phi, -representation.d_theta)
2975
2976        return super().from_representation(representation, base)
2977
2978    def transform(self, matrix, base, transformed_base):
2979        """Transform differential using a 3x3 matrix in a Cartesian basis.
2980
2981        This returns a new differential and does not modify the original one.
2982
2983        Parameters
2984        ----------
2985        matrix : (3,3) array-like
2986            A 3x3 (or stack thereof) matrix, such as a rotation matrix.
2987        base : instance of ``cls.base_representation``
2988            Base relative to which the differentials are defined.  If the other
2989            class is a differential representation, the base will be converted
2990            to its ``base_representation``.
2991        transformed_base : instance of ``cls.base_representation``
2992            Base relative to which the transformed differentials are defined.
2993            If the other class is a differential representation, the base will
2994            be converted to its ``base_representation``.
2995        """
2996        # the transformation matrix does not need to be a rotation matrix,
2997        # so the unit-distance is not guaranteed. For speed, we check if the
2998        # matrix is in O(3) and preserves lengths.
2999        if np.all(is_O3(matrix)):  # remain in unit-rep
3000            # TODO! implement without Cartesian intermediate step.
3001            # some of this can be moved to the parent class.
3002            diff = super().transform(matrix, base, transformed_base)
3003
3004        else:  # switch to dimensional representation
3005            du = self.d_lon.unit / base.lon.unit  # derivative unit
3006            diff = self._dimensional_differential(
3007                d_lon=self.d_lon, d_lat=self.d_lat, d_distance=0 * du
3008            ).transform(matrix, base, transformed_base)
3009
3010        return diff
3011
3012    def _scale_operation(self, op, *args, scaled_base=False):
3013        if scaled_base:
3014            return self.copy()
3015        else:
3016            return super()._scale_operation(op, *args)
3017
3018
3019class SphericalDifferential(BaseSphericalDifferential):
3020    """Differential(s) of points in 3D spherical coordinates.
3021
3022    Parameters
3023    ----------
3024    d_lon, d_lat : `~astropy.units.Quantity`
3025        The differential longitude and latitude.
3026    d_distance : `~astropy.units.Quantity`
3027        The differential distance.
3028    copy : bool, optional
3029        If `True` (default), arrays will be copied. If `False`, arrays will
3030        be references, though possibly broadcast to ensure matching shapes.
3031    """
3032    base_representation = SphericalRepresentation
3033    _unit_differential = UnitSphericalDifferential
3034
3035    def __init__(self, d_lon, d_lat=None, d_distance=None, copy=True):
3036        super().__init__(d_lon, d_lat, d_distance, copy=copy)
3037        if not self._d_lon.unit.is_equivalent(self._d_lat.unit):
3038            raise u.UnitsError('d_lon and d_lat should have equivalent units.')
3039
3040    def represent_as(self, other_class, base=None):
3041        # All spherical differentials can be done without going to Cartesian,
3042        # though CosLat needs base for the latitude.
3043        if issubclass(other_class, UnitSphericalDifferential):
3044            return other_class(self.d_lon, self.d_lat)
3045        elif issubclass(other_class, RadialDifferential):
3046            return other_class(self.d_distance)
3047        elif issubclass(other_class, SphericalCosLatDifferential):
3048            return other_class(self._d_lon_coslat(base), self.d_lat,
3049                               self.d_distance)
3050        elif issubclass(other_class, UnitSphericalCosLatDifferential):
3051            return other_class(self._d_lon_coslat(base), self.d_lat)
3052        elif issubclass(other_class, PhysicsSphericalDifferential):
3053            return other_class(self.d_lon, -self.d_lat, self.d_distance)
3054        else:
3055            return super().represent_as(other_class, base)
3056
3057    @classmethod
3058    def from_representation(cls, representation, base=None):
3059        # Other spherical differentials can be done without going to Cartesian,
3060        # though CosLat needs base for the latitude.
3061        if isinstance(representation, SphericalCosLatDifferential):
3062            d_lon = cls._get_d_lon(representation.d_lon_coslat, base)
3063            return cls(d_lon, representation.d_lat, representation.d_distance)
3064        elif isinstance(representation, PhysicsSphericalDifferential):
3065            return cls(representation.d_phi, -representation.d_theta,
3066                       representation.d_r)
3067
3068        return super().from_representation(representation, base)
3069
3070    def _scale_operation(self, op, *args, scaled_base=False):
3071        if scaled_base:
3072            return self.__class__(self.d_lon, self.d_lat, op(self.d_distance, *args))
3073        else:
3074            return super()._scale_operation(op, *args)
3075
3076
3077class BaseSphericalCosLatDifferential(BaseDifferential):
3078    """Differentials from points on a spherical base representation.
3079
3080    With cos(lat) assumed to be included in the longitude differential.
3081    """
3082    @classmethod
3083    def _get_base_vectors(cls, base):
3084        """Get unit vectors and scale factors from (unit)spherical base.
3085
3086        Parameters
3087        ----------
3088        base : instance of ``self.base_representation``
3089            The points for which the unit vectors and scale factors should be
3090            retrieved.
3091
3092        Returns
3093        -------
3094        unit_vectors : dict of `CartesianRepresentation`
3095            In the directions of the coordinates of base.
3096        scale_factors : dict of `~astropy.units.Quantity`
3097            Scale factors for each of the coordinates.  The scale factor for
3098            longitude does not include the cos(lat) factor.
3099
3100        Raises
3101        ------
3102        TypeError : if the base is not of the correct type
3103        """
3104        cls._check_base(base)
3105        return base.unit_vectors(), base.scale_factors(omit_coslat=True)
3106
3107    def _d_lon(self, base):
3108        """Convert longitude differential with cos(lat) to one without.
3109
3110        Parameters
3111        ----------
3112        base : instance of ``cls.base_representation``
3113            The base from which the latitude will be taken.
3114        """
3115        self._check_base(base)
3116        return self.d_lon_coslat / np.cos(base.lat)
3117
3118    @classmethod
3119    def _get_d_lon_coslat(cls, d_lon, base):
3120        """Convert longitude differential d_lon to d_lon_coslat.
3121
3122        Parameters
3123        ----------
3124        d_lon : `~astropy.units.Quantity`
3125            Value of the longitude differential without ``cos(lat)``.
3126        base : instance of ``cls.base_representation``
3127            The base from which the latitude will be taken.
3128        """
3129        cls._check_base(base)
3130        return d_lon * np.cos(base.lat)
3131
3132    def _combine_operation(self, op, other, reverse=False):
3133        """Combine two differentials, or a differential with a representation.
3134
3135        If ``other`` is of the same differential type as ``self``, the
3136        components will simply be combined.  If both are different parts of
3137        a `~astropy.coordinates.SphericalDifferential` (e.g., a
3138        `~astropy.coordinates.UnitSphericalDifferential` and a
3139        `~astropy.coordinates.RadialDifferential`), they will combined
3140        appropriately.
3141
3142        If ``other`` is a representation, it will be used as a base for which
3143        to evaluate the differential, and the result is a new representation.
3144
3145        Parameters
3146        ----------
3147        op : `~operator` callable
3148            Operator to apply (e.g., `~operator.add`, `~operator.sub`, etc.
3149        other : `~astropy.coordinates.BaseRepresentation` subclass instance
3150            The other differential or representation.
3151        reverse : bool
3152            Whether the operands should be reversed (e.g., as we got here via
3153            ``self.__rsub__`` because ``self`` is a subclass of ``other``).
3154        """
3155        if (isinstance(other, BaseSphericalCosLatDifferential) and
3156                not isinstance(self, type(other)) or
3157                isinstance(other, RadialDifferential)):
3158            all_components = set(self.components) | set(other.components)
3159            first, second = (self, other) if not reverse else (other, self)
3160            result_args = {c: op(getattr(first, c, 0.), getattr(second, c, 0.))
3161                           for c in all_components}
3162            return SphericalCosLatDifferential(**result_args)
3163
3164        return super()._combine_operation(op, other, reverse)
3165
3166
3167class UnitSphericalCosLatDifferential(BaseSphericalCosLatDifferential):
3168    """Differential(s) of points on a unit sphere.
3169
3170    Parameters
3171    ----------
3172    d_lon_coslat, d_lat : `~astropy.units.Quantity`
3173        The longitude and latitude of the differentials.
3174    copy : bool, optional
3175        If `True` (default), arrays will be copied. If `False`, arrays will
3176        be references, though possibly broadcast to ensure matching shapes.
3177    """
3178    base_representation = UnitSphericalRepresentation
3179    attr_classes = {'d_lon_coslat': u.Quantity,
3180                    'd_lat': u.Quantity}
3181
3182    @classproperty
3183    def _dimensional_differential(cls):
3184        return SphericalCosLatDifferential
3185
3186    def __init__(self, d_lon_coslat, d_lat=None, copy=True):
3187        super().__init__(d_lon_coslat, d_lat, copy=copy)
3188        if not self._d_lon_coslat.unit.is_equivalent(self._d_lat.unit):
3189            raise u.UnitsError('d_lon_coslat and d_lat should have equivalent '
3190                               'units.')
3191
3192    @classmethod
3193    def from_cartesian(cls, other, base):
3194        # Go via the dimensional equivalent, so that the longitude and latitude
3195        # differentials correctly take into account the norm of the base.
3196        dimensional = cls._dimensional_differential.from_cartesian(other, base)
3197        return dimensional.represent_as(cls)
3198
3199    def to_cartesian(self, base):
3200        if isinstance(base, SphericalRepresentation):
3201            scale = base.distance
3202        elif isinstance(base, PhysicsSphericalRepresentation):
3203            scale = base.r
3204        else:
3205            return super().to_cartesian(base)
3206
3207        base = base.represent_as(UnitSphericalRepresentation)
3208        return scale * super().to_cartesian(base)
3209
3210    def represent_as(self, other_class, base=None):
3211        # Only have enough information to represent other unit-spherical.
3212        if issubclass(other_class, UnitSphericalDifferential):
3213            return other_class(self._d_lon(base), self.d_lat)
3214
3215        return super().represent_as(other_class, base)
3216
3217    @classmethod
3218    def from_representation(cls, representation, base=None):
3219        # All spherical differentials can be done without going to Cartesian,
3220        # though w/o CosLat needs base for the latitude.
3221        if isinstance(representation, SphericalCosLatDifferential):
3222            return cls(representation.d_lon_coslat, representation.d_lat)
3223        elif isinstance(representation, (SphericalDifferential,
3224                                         UnitSphericalDifferential)):
3225            d_lon_coslat = cls._get_d_lon_coslat(representation.d_lon, base)
3226            return cls(d_lon_coslat, representation.d_lat)
3227        elif isinstance(representation, PhysicsSphericalDifferential):
3228            d_lon_coslat = cls._get_d_lon_coslat(representation.d_phi, base)
3229            return cls(d_lon_coslat, -representation.d_theta)
3230
3231        return super().from_representation(representation, base)
3232
3233    def transform(self, matrix, base, transformed_base):
3234        """Transform differential using a 3x3 matrix in a Cartesian basis.
3235
3236        This returns a new differential and does not modify the original one.
3237
3238        Parameters
3239        ----------
3240        matrix : (3,3) array-like
3241            A 3x3 (or stack thereof) matrix, such as a rotation matrix.
3242        base : instance of ``cls.base_representation``
3243            Base relative to which the differentials are defined.  If the other
3244            class is a differential representation, the base will be converted
3245            to its ``base_representation``.
3246        transformed_base : instance of ``cls.base_representation``
3247            Base relative to which the transformed differentials are defined.
3248            If the other class is a differential representation, the base will
3249            be converted to its ``base_representation``.
3250        """
3251        # the transformation matrix does not need to be a rotation matrix,
3252        # so the unit-distance is not guaranteed. For speed, we check if the
3253        # matrix is in O(3) and preserves lengths.
3254        if np.all(is_O3(matrix)):  # remain in unit-rep
3255            # TODO! implement without Cartesian intermediate step.
3256            diff = super().transform(matrix, base, transformed_base)
3257
3258        else:  # switch to dimensional representation
3259            du = self.d_lat.unit / base.lat.unit  # derivative unit
3260            diff = self._dimensional_differential(
3261                d_lon_coslat=self.d_lon_coslat, d_lat=self.d_lat,
3262                d_distance=0 * du
3263            ).transform(matrix, base, transformed_base)
3264
3265        return diff
3266
3267    def _scale_operation(self, op, *args, scaled_base=False):
3268        if scaled_base:
3269            return self.copy()
3270        else:
3271            return super()._scale_operation(op, *args)
3272
3273
3274class SphericalCosLatDifferential(BaseSphericalCosLatDifferential):
3275    """Differential(s) of points in 3D spherical coordinates.
3276
3277    Parameters
3278    ----------
3279    d_lon_coslat, d_lat : `~astropy.units.Quantity`
3280        The differential longitude (with cos(lat) included) and latitude.
3281    d_distance : `~astropy.units.Quantity`
3282        The differential distance.
3283    copy : bool, optional
3284        If `True` (default), arrays will be copied. If `False`, arrays will
3285        be references, though possibly broadcast to ensure matching shapes.
3286    """
3287    base_representation = SphericalRepresentation
3288    _unit_differential = UnitSphericalCosLatDifferential
3289    attr_classes = {'d_lon_coslat': u.Quantity,
3290                    'd_lat': u.Quantity,
3291                    'd_distance': u.Quantity}
3292
3293    def __init__(self, d_lon_coslat, d_lat=None, d_distance=None, copy=True):
3294        super().__init__(d_lon_coslat, d_lat, d_distance, copy=copy)
3295        if not self._d_lon_coslat.unit.is_equivalent(self._d_lat.unit):
3296            raise u.UnitsError('d_lon_coslat and d_lat should have equivalent '
3297                               'units.')
3298
3299    def represent_as(self, other_class, base=None):
3300        # All spherical differentials can be done without going to Cartesian,
3301        # though some need base for the latitude to remove cos(lat).
3302        if issubclass(other_class, UnitSphericalCosLatDifferential):
3303            return other_class(self.d_lon_coslat, self.d_lat)
3304        elif issubclass(other_class, RadialDifferential):
3305            return other_class(self.d_distance)
3306        elif issubclass(other_class, SphericalDifferential):
3307            return other_class(self._d_lon(base), self.d_lat, self.d_distance)
3308        elif issubclass(other_class, UnitSphericalDifferential):
3309            return other_class(self._d_lon(base), self.d_lat)
3310        elif issubclass(other_class, PhysicsSphericalDifferential):
3311            return other_class(self._d_lon(base), -self.d_lat, self.d_distance)
3312
3313        return super().represent_as(other_class, base)
3314
3315    @classmethod
3316    def from_representation(cls, representation, base=None):
3317        # Other spherical differentials can be done without going to Cartesian,
3318        # though we need base for the latitude to remove coslat.
3319        if isinstance(representation, SphericalDifferential):
3320            d_lon_coslat = cls._get_d_lon_coslat(representation.d_lon, base)
3321            return cls(d_lon_coslat, representation.d_lat,
3322                       representation.d_distance)
3323        elif isinstance(representation, PhysicsSphericalDifferential):
3324            d_lon_coslat = cls._get_d_lon_coslat(representation.d_phi, base)
3325            return cls(d_lon_coslat, -representation.d_theta,
3326                       representation.d_r)
3327
3328        return super().from_representation(representation, base)
3329
3330    def _scale_operation(self, op, *args, scaled_base=False):
3331        if scaled_base:
3332            return self.__class__(self.d_lon_coslat, self.d_lat, op(self.d_distance, *args))
3333        else:
3334            return super()._scale_operation(op, *args)
3335
3336
3337class RadialDifferential(BaseDifferential):
3338    """Differential(s) of radial distances.
3339
3340    Parameters
3341    ----------
3342    d_distance : `~astropy.units.Quantity`
3343        The differential distance.
3344    copy : bool, optional
3345        If `True` (default), arrays will be copied. If `False`, arrays will
3346        be references, though possibly broadcast to ensure matching shapes.
3347    """
3348    base_representation = RadialRepresentation
3349
3350    def to_cartesian(self, base):
3351        return self.d_distance * base.represent_as(
3352            UnitSphericalRepresentation).to_cartesian()
3353
3354    def norm(self, base=None):
3355        return self.d_distance
3356
3357    @classmethod
3358    def from_cartesian(cls, other, base):
3359        return cls(other.dot(base.represent_as(UnitSphericalRepresentation)),
3360                   copy=False)
3361
3362    @classmethod
3363    def from_representation(cls, representation, base=None):
3364        if isinstance(representation, (SphericalDifferential,
3365                                       SphericalCosLatDifferential)):
3366            return cls(representation.d_distance)
3367        elif isinstance(representation, PhysicsSphericalDifferential):
3368            return cls(representation.d_r)
3369        else:
3370            return super().from_representation(representation, base)
3371
3372    def _combine_operation(self, op, other, reverse=False):
3373        if isinstance(other, self.base_representation):
3374            if reverse:
3375                first, second = other.distance, self.d_distance
3376            else:
3377                first, second = self.d_distance, other.distance
3378            return other.__class__(op(first, second), copy=False)
3379        elif isinstance(other, (BaseSphericalDifferential,
3380                                BaseSphericalCosLatDifferential)):
3381            all_components = set(self.components) | set(other.components)
3382            first, second = (self, other) if not reverse else (other, self)
3383            result_args = {c: op(getattr(first, c, 0.), getattr(second, c, 0.))
3384                           for c in all_components}
3385            return SphericalDifferential(**result_args)
3386
3387        else:
3388            return super()._combine_operation(op, other, reverse)
3389
3390
3391class PhysicsSphericalDifferential(BaseDifferential):
3392    """Differential(s) of 3D spherical coordinates using physics convention.
3393
3394    Parameters
3395    ----------
3396    d_phi, d_theta : `~astropy.units.Quantity`
3397        The differential azimuth and inclination.
3398    d_r : `~astropy.units.Quantity`
3399        The differential radial distance.
3400    copy : bool, optional
3401        If `True` (default), arrays will be copied. If `False`, arrays will
3402        be references, though possibly broadcast to ensure matching shapes.
3403    """
3404    base_representation = PhysicsSphericalRepresentation
3405
3406    def __init__(self, d_phi, d_theta=None, d_r=None, copy=True):
3407        super().__init__(d_phi, d_theta, d_r, copy=copy)
3408        if not self._d_phi.unit.is_equivalent(self._d_theta.unit):
3409            raise u.UnitsError('d_phi and d_theta should have equivalent '
3410                               'units.')
3411
3412    def represent_as(self, other_class, base=None):
3413        # All spherical differentials can be done without going to Cartesian,
3414        # though CosLat needs base for the latitude. For those, explicitly
3415        # do the equivalent of self._d_lon_coslat in SphericalDifferential.
3416        if issubclass(other_class, SphericalDifferential):
3417            return other_class(self.d_phi, -self.d_theta, self.d_r)
3418        elif issubclass(other_class, UnitSphericalDifferential):
3419            return other_class(self.d_phi, -self.d_theta)
3420        elif issubclass(other_class, SphericalCosLatDifferential):
3421            self._check_base(base)
3422            d_lon_coslat = self.d_phi * np.sin(base.theta)
3423            return other_class(d_lon_coslat, -self.d_theta, self.d_r)
3424        elif issubclass(other_class, UnitSphericalCosLatDifferential):
3425            self._check_base(base)
3426            d_lon_coslat = self.d_phi * np.sin(base.theta)
3427            return other_class(d_lon_coslat, -self.d_theta)
3428        elif issubclass(other_class, RadialDifferential):
3429            return other_class(self.d_r)
3430
3431        return super().represent_as(other_class, base)
3432
3433    @classmethod
3434    def from_representation(cls, representation, base=None):
3435        # Other spherical differentials can be done without going to Cartesian,
3436        # though we need base for the latitude to remove coslat. For that case,
3437        # do the equivalent of cls._d_lon in SphericalDifferential.
3438        if isinstance(representation, SphericalDifferential):
3439            return cls(representation.d_lon, -representation.d_lat,
3440                       representation.d_distance)
3441        elif isinstance(representation, SphericalCosLatDifferential):
3442            cls._check_base(base)
3443            d_phi = representation.d_lon_coslat / np.sin(base.theta)
3444            return cls(d_phi, -representation.d_lat, representation.d_distance)
3445
3446        return super().from_representation(representation, base)
3447
3448    def _scale_operation(self, op, *args, scaled_base=False):
3449        if scaled_base:
3450            return self.__class__(self.d_phi, self.d_theta, op(self.d_r, *args))
3451        else:
3452            return super()._scale_operation(op, *args)
3453
3454
3455class CylindricalDifferential(BaseDifferential):
3456    """Differential(s) of points in cylindrical coordinates.
3457
3458    Parameters
3459    ----------
3460    d_rho : `~astropy.units.Quantity` ['speed']
3461        The differential cylindrical radius.
3462    d_phi : `~astropy.units.Quantity` ['angular speed']
3463        The differential azimuth.
3464    d_z : `~astropy.units.Quantity` ['speed']
3465        The differential height.
3466    copy : bool, optional
3467        If `True` (default), arrays will be copied. If `False`, arrays will
3468        be references, though possibly broadcast to ensure matching shapes.
3469    """
3470    base_representation = CylindricalRepresentation
3471
3472    def __init__(self, d_rho, d_phi=None, d_z=None, copy=False):
3473        super().__init__(d_rho, d_phi, d_z, copy=copy)
3474        if not self._d_rho.unit.is_equivalent(self._d_z.unit):
3475            raise u.UnitsError("d_rho and d_z should have equivalent units.")
3476