r"""Generic versions of the |Representation| in :mod:`astropy`.
These classes assume less about the constituent dimensions than their astropy
counterparts. This can be useful for working with phase-spaces that are not
real-space positions (or derivates thereof). However, care should be taken when
using many of the methods of these generic representations since they inherit
from the astropy real-space representation machinery.
You will probably not be instantiating these classes directly, but encountering
them from :mod:`interpolated_coordinates` classes like
:class:`interpolated_coordinates.InterpolatedSkyCoord`. However, one can get and
use the classes, with all the above-noted caveats:
>>> import astropy.units as u
>>> from interpolated_coordinates.utils.generic_representation import \
... GenericCartesianRepresentation
>>> r = GenericCartesianRepresentation(1, 2, 3)
>>> r
<GenericCartesianRepresentation (x, y, z) [dimensionless]
(1., 2., 3.)>
>>> r.x
<Quantity 1.>
The real convenience lies with differentials, which can go to arbitrary order.
Recalling that many of the methods will give incorrect results, these classes
are primarily useful for consistent and familiar data storage.
>>> from interpolated_coordinates.utils.generic_representation import \
... GenericSpherical2ndDifferential
>>> d2 = GenericSpherical2ndDifferential(1 * u.rad/u.s**2, 2 * u.rad/u.s**2, 3 * u.km/u.s**2)
>>> d2
<GenericSpherical2ndDifferential (d_lon, d_lat, d_distance) in (rad / s2, rad / s2, km / s2)
(1., 2., 3.)>
"""
##############################################################################
# IMPORTS
from __future__ import annotations
import inspect
import sys
from functools import reduce
from typing import Any, ClassVar, cast
import astropy.coordinates as coord
import astropy.units as u
from astropy.coordinates.representation import DIFFERENTIAL_CLASSES
__all__ = [
"GenericRepresentation",
"GenericDifferential",
]
##############################################################################
# PARAMETERS
_GENERIC_REGISTRY: dict[
object | str,
GenericRepresentation | GenericDifferential,
] = {}
##############################################################################
# CODE
##############################################################################
class GenericRepresentationOrDifferential(coord.BaseRepresentationOrDifferential): # type: ignore[misc]
pass
[docs]
class GenericRepresentation(coord.BaseRepresentation, GenericRepresentationOrDifferential): # type: ignore[misc]
"""Generic representation of a point in a 3D coordinate system.
Parameters
----------
q1, q2, q3 : `~astropy.units.Quantity` or subclass
The components of the 3D points. The names are the keys and the
subclasses the values of the ``attr_classes`` attribute.
differentials : dict, `~astropy.coordinates.BaseDifferential`, optional
Any differential classes that should be associated with this
representation. The input must either be a single
`~astropy.coordinates.BaseDifferential` subclass instance, or a
dictionary with keys set to a string representation of the SI unit
with which the differential (derivative) is taken. For example, for a
velocity differential on a positional representation, the key would be
``'s'`` for seconds, indicating that the derivative is a time
derivative.
copy : bool, optional
If `True` (default), arrays will be copied. If `False`, arrays will
be references, though possibly broadcast to ensure matching shapes.
Notes
-----
All representation classes should subclass this base representation class,
and define an ``attr_classes`` attribute, an `~collections.OrderedDict`
which maps component names to the class that creates them. They must also
define a ``to_cartesian`` method and a ``from_cartesian`` class method. By
default, transformations are done via the cartesian system, but classes
that want to define a smarter transformation path can overload the
``represent_as`` method. If one wants to use an associated differential
class, one should also define ``unit_vectors`` and ``scale_factors``
methods (see those methods for details).
"""
attr_classes: ClassVar[dict[str, type]] = {"q1": u.Quantity, "q2": u.Quantity, "q3": u.Quantity}
@staticmethod
def _make_generic_cls(
rep_cls: coord.BaseRepresentation | GenericRepresentation,
) -> GenericRepresentation:
"""Return a generic form of a representation.
Parameters
----------
rep_cls : |Representation| or `GenericRepresentation`
Representation class for which to make generic.
Returns
-------
`GenericRepresentation` subclass
Generic form of `rep_cls`.
If `rep_cls` is already generic, return it unchanged.
Subclasses are cached in a registry.
"""
cls: GenericRepresentation
# 1) Check if it's already generic
if issubclass(rep_cls, GenericRepresentation):
cls = rep_cls
# 2) Check if it's cached
elif rep_cls in _GENERIC_REGISTRY:
cls = _GENERIC_REGISTRY[rep_cls]
# 3) Need to dynamically define the generic class
else:
name = f"Generic{rep_cls.__name__}"
bases = (GenericRepresentation, rep_cls)
# attributes: copies `attr_classes`
attrs_meths = {"attr_classes": rep_cls.attr_classes}
# add link from `qX` to the attr method # TODO!
# for i, k in enumerate(rep_cls.attr_classes.keys()):
# def get_attr(self):
cls = cast(GenericRepresentation, type(name, bases, attrs_meths))
# cache b/c can only define the same Rep/Dif once
_GENERIC_REGISTRY[rep_cls] = cls
# also store in locals
setattr(sys.modules[__name__], cls.__name__, cls)
sys.modules[__name__].__all__.append(cls.__name__)
return cls
# -------------------------------------------------------------------
def _ordinal(n: int) -> str:
"""Return suffix for ordinal.
Credits: https://codegolf.stackexchange.com/a/74047
Parameters
----------
n : int
Must be >= 1
Returns
-------
str
Ordinal form `n`. Ex 1 -> '1st', 2 -> '2nd', 3 -> '3rd'.
"""
i: int = n % 5 * (n % 100 ^ 15 > 4 > n % 10) # noqa: PLR2004
return str(n) + "tsnrhtdd"[i::4]
[docs]
class GenericDifferential(coord.BaseDifferential, GenericRepresentationOrDifferential): # type: ignore[misc]
r"""A base class representing differentials of representations.
These represent differences or derivatives along each component.
E.g., for physics spherical coordinates, these would be
:math:`\delta r, \delta \theta, \delta \phi`.
Parameters
----------
d_q1, d_q2, d_q3 : `~astropy.units.Quantity` or subclass
The components of the 3D differentials. The names are the keys and the
subclasses the values of the ``attr_classes`` attribute.
copy : bool, optional
If `True` (default), arrays will be copied. If `False`, arrays will
be references, though possibly broadcast to ensure matching shapes.
"""
base_representation: coord.BaseRepresentation = GenericRepresentation
@staticmethod
def _make_generic_cls(
dif_cls: coord.BaseDifferential | GenericDifferential,
n: int = 1,
) -> GenericDifferential:
"""Make Generic Differential.
Parameters
----------
dif_cls : |Differential| or `GenericDifferential` class
Differential class for which to make generic.
n : int
The differential level.
Not used if `dif_cls` is `GenericDifferential`
Returns
-------
`GenericDifferential`
Generic form of `dif_cls`.
If `dif_cls` is already generic, return it unchanged.
Subclasses are cached in a registry.
"""
# 1) check if it's already generic
if issubclass(dif_cls, GenericDifferential):
return dif_cls
# 2) check if `n` is too small to make a differential
if n < 1:
msg = "n < 1"
raise ValueError(msg)
# 3) make name for generic.
if n == 1: # a) special case for n=1
name = f"Generic{dif_cls.__name__}"
else: # b) higher ordinal
dif_type = dif_cls.__name__[: -len("Differential")]
name = f"Generic{dif_type}{_ordinal(n)}Differential"
cls: GenericDifferential
# A) check if cached
if n == 1 and dif_cls in _GENERIC_REGISTRY: # i) special case for n=1
cls = _GENERIC_REGISTRY[dif_cls]
elif name in _GENERIC_REGISTRY: # ii) higher ordinal
cls = _GENERIC_REGISTRY[name]
# B) make generic
else:
bases = (GenericDifferential, dif_cls)
# get base representation from differential class.
# and then get the generic form
generic_base = GenericRepresentation._make_generic_cls(
dif_cls.base_representation,
)
# attributes: copies `attr_classes`
attrs_meths = {
"attr_classes": dif_cls.attr_classes,
"base_representation": generic_base,
}
# Make generic differential
cls = cast(GenericDifferential, type(name, bases, attrs_meths))
# cache, either by class or by name
_GENERIC_REGISTRY[dif_cls if n == 1 else name] = cls
# also store in locals
setattr(sys.modules[__name__], cls.__name__, cls)
sys.modules[__name__].__all__.append(cls.__name__)
return cls
@staticmethod
def _make_generic_cls_for_representation(
rep_cls: coord.BaseRepresentation,
n: int = 1,
) -> GenericDifferential:
"""Make generic differential given a representation.
Parameters
----------
rep_cls : |Representation| class
Representation class for which to make generic.
n : int
Must be >= 1
Returns
-------
`GenericDifferential`
Of ordinal `n`
"""
rep_cls_name: str = rep_cls.__name__[: -len("Representation")]
if n == 1:
name = f"Generic{rep_cls_name}Differential"
else:
name = f"Generic{rep_cls_name}{_ordinal(n)}Differential"
cls: GenericDifferential
if name in _GENERIC_REGISTRY:
cls = _GENERIC_REGISTRY[name]
elif dcls := DIFFERENTIAL_CLASSES.get(rep_cls_name.lower()):
cls = GenericDifferential._make_generic_cls(dcls, n=n)
else:
cls = cast(
GenericDifferential,
type(
name,
(GenericDifferential, rep_cls),
{"base_representation": rep_cls},
),
)
_GENERIC_REGISTRY[name] = cls
# also store in locals
setattr(sys.modules[__name__], cls.__name__, cls)
sys.modules[__name__].__all__.append(cls.__name__)
return cls
# ===================================================================
def __getattr__(name: str) -> type:
"""Get a generic direct subclass of an Astropy representation.
Parameters
----------
name : str
Name of the class.
Returns
-------
type
Raises
------
AttributeError
If `name` doesn't start with "Generic", the "Generic"-removed name is
not for a :class:`~astropy.coordinates.BaseRepresentation` or
:class:`~astropy.coordinates.BaseDifferential`.
"""
if name.startswith("Generic"):
name = name.replace("Generic", "")
if name.endswith("Differential"): # strip the ordinal
# Get the type, e.g. Cartesian
i: int = len("Differential")
kind: str = reduce(lambda k, n: k.split(n)[0], "0123456789", name[:-i])
# Get order of the differential
j: int = len(kind)
order: str = reduce(lambda o, s: o.split(s)[0], "tsnrhd", name[j:-i])
n: int = int(order) if order else 1 # 1st derivative is an empty string
name = kind + "Differential"
cls: coord.RepresentationOrDifferential | Any
if cls := getattr(coord, name, False):
generic_cls: GenericRepresentationOrDifferential
if inspect.isclass(cls) and issubclass(cls, coord.BaseRepresentation):
generic_cls = GenericRepresentation._make_generic_cls(cls)
elif inspect.isclass(cls) and issubclass(cls, coord.BaseDifferential):
generic_cls = GenericDifferential._make_generic_cls(cls, n=n)
else:
msg = f"module {__name__!r} has no attribute {name!r}"
raise AttributeError(msg)
return generic_cls
msg = f"module {__name__!r} has no attribute {name!r}"
raise AttributeError(msg)
# def __dir__():
# return sorted(dir_out + __all__ + )