Пример #1
0
class UVW(BaseCoordinateFrame):
    """
    Written by Joshua G. Albert - [email protected]
    A coordinate or frame in the UVW system.  
    This frame has the following frame attributes, which are necessary for
    transforming from UVW to some other system:
    * ``obstime``
        The time at which the observation is taken.  Used for determining the
        position and orientation of the Earth.
    * ``location``
        The location on the Earth.  This can be specified either as an
        `~astropy.coordinates.EarthLocation` object or as anything that can be
        transformed to an `~astropy.coordinates.ITRS` frame.
    * ``phaseDir``
        The phase tracking center of the frame.  This can be specified either as an
        (ra,dec) `~astropy.units.Qunatity` or as anything that can be
        transformed to an `~astropy.coordinates.ICRS` frame.
    Parameters
    ----------
    representation : `BaseRepresentation` or None
        A representation object or None to have no data (or use the other keywords)
    u : :class:`~astropy.units.Quantity`, optional, must be keyword
        The u coordinate for this object (``v`` and ``w`` must also be given and
        ``representation`` must be None).
    v : :class:`~astropy.units.Quantity`, optional, must be keyword
        The v coordinate for this object (``u`` and ``w`` must also be given and
        ``representation`` must be None).
    w : :class:`~astropy.units.Quantity`, optional, must be keyword
        The w coordinate for this object (``u`` and ``v`` must also be given and
        ``representation`` must be None).
    Notes
    -----
    This is useful for radio astronomy.
    """

    frame_specific_representation_info = {
        'cartesian': [
            RepresentationMapping('x', 'u'),
            RepresentationMapping('y', 'v'),
            RepresentationMapping('z', 'w')
        ],
    }

    default_representation = CartesianRepresentation

    obstime = TimeAttribute(default=None)
    location = EarthLocationAttribute(default=None)
    phase = CoordinateAttribute(ICRS, default=None)

    def __init__(self, *args, **kwargs):
        super(UVW, self).__init__(*args, **kwargs)

    @property
    def elevation(self):
        """
        Elevation above the horizon of the direction
        """
        return self.phase.transform_to(
            AltAz(location=self.location, obstime=self.obstime)).alt
Пример #2
0
class GreatCircleICRSFrame(coord.BaseCoordinateFrame):
    """A frame rotated into great circle coordinates with the pole and longitude
    specified as frame attributes.

    ``GreatCircleICRSFrame``s always have component names for spherical
    coordinates of ``phi1``/``phi2``.
    """

    pole = CoordinateAttribute(default=None, frame=coord.ICRS)
    ra0 = QuantityAttribute(default=np.nan * u.deg, unit=u.deg)
    rotation = QuantityAttribute(default=0, unit=u.deg)

    frame_specific_representation_info = {
        coord.SphericalRepresentation: [
            coord.RepresentationMapping('lon', 'phi1'),
            coord.RepresentationMapping('lat', 'phi2'),
            coord.RepresentationMapping('distance', 'distance')
        ]
    }

    default_representation = coord.SphericalRepresentation
    default_differential = coord.SphericalCosLatDifferential

    _default_wrap_angle = 180 * u.deg

    def __init__(self, *args, **kwargs):
        wrap = kwargs.pop('wrap_longitude', True)
        super().__init__(*args, **kwargs)
        if wrap and isinstance(self._data, (coord.UnitSphericalRepresentation,
                                            coord.SphericalRepresentation)):
            self._data.lon.wrap_angle = self._default_wrap_angle

    @classmethod
    def from_endpoints(cls, coord1, coord2, ra0=None, rotation=None):
        """TODO
        """

        pole = pole_from_endpoints(coord1, coord2)

        kw = dict(pole=pole)
        if ra0 is not None:
            kw['ra0'] = ra0

        if rotation is not None:
            kw['rotation'] = rotation

        if ra0 is None and rotation is None:
            midpt = sph_midpoint(coord1, coord2)
            kw['ra0'] = midpt.ra

        return cls(**kw)
Пример #3
0
        def __new__(cls, name, bases, members):
            # Only 'origin' is needed here, to set the origin frame properly.
            members['origin'] = CoordinateAttribute(frame=framecls, default=None)

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

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

            return super().__new__(cls, newname, bases, members)
Пример #4
0
class ExternalGalaxyFrame(BaseCoordinateFrame):
    """Astropy coordinate frame centered on an external galaxy.

    The frame is defined by sky coordinate, heliocentric distance and velocity, and inclination and position angle.
    Modeled on astropy's Galactocentric class, but without that class's defaults-setting mechanism.
    x, y, and z are defined such that with inclination and position angle both 0, x points along RA,
    y along dec, and z into sky. With nonzero inclination y will still point within the plane of the sky,
    while x and z rotate about the y axis.

    Keyword Arguments:
        gal_coord: Scalar SkyCoord or Frame object
            Contains sky position of galaxy
        gal_distance: Quantity (units of length, e.g. u.kpc)
            Distance to galaxy
        galvel_heliocentric: CartesianDifferential, units of velocity
            3-d heliocentric velocity of galaxy, in the sky-aligned cartesian frame pointing to galaxy
            (x=East, y=North, z=distance)
        inclination: Quantity (units of angle)
        PA: Quantity (units of angle)
    """
    default_representation = r.CartesianRepresentation
    default_differential = r.CartesianDifferential

    # frame attributes
    # notes: apparently you can't call something "distance"?
    # also, ipython reload insufficient to reset attributes - have to quit out and restart python?
    gal_coord = CoordinateAttribute(frame=ICRS)   # PA defined relative to ICRS ra/dec
    gal_distance = QuantityAttribute(unit=u.kpc)
    galvel_heliocentric = DifferentialAttribute(
        allowed_classes=[r.CartesianDifferential])
    inclination = QuantityAttribute(unit=u.deg)
    PA = QuantityAttribute(unit=u.deg)

    def __init__(self, *args, skyalign=False, **kwargs):
        # left some example code for subclasses in here
        default_params = {
            'gal_coord': ICRS(ra=10.68470833 * u.degree, dec=41.26875 * u.degree),
            "gal_distance": 780.0 * u.kpc,
            "galvel_heliocentric": r.CartesianDifferential([125.2, -73.8, -300.] * (u.km / u.s)),
            "inclination": (-77.) * u.degree,
            "PA": 37. * u.degree,
        }
        kwds = dict()
        kwds.update(default_params)
        if skyalign:
            kwds['inclination'] = 0. * u.deg
            kwds['PA'] = 0. * u.deg
        kwds.update(kwargs)
        super().__init__(*args, **kwds)
Пример #5
0
        def __new__(cls, name, bases, members):
            # Only 'origin' is needed here, to set the origin frame properly.
            members["origin"] = CoordinateAttribute(frame=framecls,
                                                    default=None)
            # members["afn_bounds"] = QuantityAttribute(
            #     default=[-np.inf, np.inf] * u.yr, unit=u.Myr, shape=(2,)
            # )

            # This has to be done because FrameMeta will set these attributes
            # to the defaults from BaseCoordinateFrame when it creates the base
            # OrbitOffsetFrame class initially.
            members[
                "_default_representation"] = OrbitOffsetCartesianRepresentation
            members[
                "_default_differential"] = framecls._default_differential  # TODO replace

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

            return super().__new__(cls, newname, bases, members)
Пример #6
0
class GreatCircleICRSFrame(coord.BaseCoordinateFrame):
    """A frame rotated into great circle coordinates with the pole and longitude
    specified as frame attributes.

    ``GreatCircleICRSFrame``s always have component names for spherical
    coordinates of ``phi1``/``phi2``.
    """

    pole = CoordinateAttribute(default=None, frame=coord.ICRS)
    center = CoordinateAttribute(default=None, frame=coord.ICRS)
    ra0 = QuantityAttribute(default=np.nan * u.deg, unit=u.deg)
    rotation = QuantityAttribute(default=0, unit=u.deg)

    frame_specific_representation_info = {
        coord.SphericalRepresentation: [
            coord.RepresentationMapping('lon', 'phi1'),
            coord.RepresentationMapping('lat', 'phi2'),
            coord.RepresentationMapping('distance', 'distance')
        ]
    }

    default_representation = coord.SphericalRepresentation
    default_differential = coord.SphericalCosLatDifferential

    _default_wrap_angle = 180 * u.deg

    def __init__(self, *args, **kwargs):
        wrap = kwargs.pop('wrap_longitude', True)
        super().__init__(*args, **kwargs)
        if wrap and isinstance(self._data, (coord.UnitSphericalRepresentation,
                                            coord.SphericalRepresentation)):
            self._data.lon.wrap_angle = self._default_wrap_angle

        if self.center is not None and np.isfinite(self.ra0):
            raise ValueError(
                "Both `center` and `ra0` were specified for this "
                "{} object: you can only specify one or the other.".format(
                    self.__class__.__name__))

    # TODO: remove this. This is a hack required as of astropy v3.1 in order
    # to have the longitude components wrap at the desired angle
    def represent_as(self, base, s='base', in_frame_units=False):
        r = super().represent_as(base, s=s, in_frame_units=in_frame_units)
        r.lon.wrap_angle = self._default_wrap_angle
        return r

    represent_as.__doc__ = coord.BaseCoordinateFrame.represent_as.__doc__

    @classmethod
    def from_endpoints(cls, coord1, coord2, ra0=None, rotation=None):
        """Compute the great circle frame from two endpoints of an arc on the
        unit sphere.

        Parameters
        ----------
        coord1 : `~astropy.coordinates.SkyCoord`
            One endpoint of the great circle arc.
        coord2 : `~astropy.coordinates.SkyCoord`
            The other endpoint of the great circle arc.
        ra0 : `~astropy.units.Quantity`, `~astropy.coordinates.Angle` (optional)
            If specified, an additional transformation will be applied to make
            this right ascension the longitude zero-point of the resulting
            coordinate frame.
        rotation : `~astropy.units.Quantity`, `~astropy.coordinates.Angle` (optional)
            If specified, a final rotation about the pole (i.e. the resulting z
            axis) applied.
        """

        pole = pole_from_endpoints(coord1, coord2)

        kw = dict(pole=pole)
        if ra0 is not None:
            kw['ra0'] = ra0

        if rotation is not None:
            kw['rotation'] = rotation

        if ra0 is None and rotation is None:
            midpt = sph_midpoint(coord1, coord2)
            kw['ra0'] = midpt.ra

        return cls(**kw)

    @classmethod
    def from_xyz(cls, xnew=None, ynew=None, znew=None):
        """Compute the great circle frame from a specification of the coordinate
        axes in the new system.

        Parameters
        ----------
        xnew : astropy ``Representation`` object
            The x-axis in the new system.
        ynew : astropy ``Representation`` object
            The y-axis in the new system.
        znew : astropy ``Representation`` object
            The z-axis in the new system.
        """
        is_none = [xnew is None, ynew is None, znew is None]
        if np.sum(is_none) > 1:
            raise ValueError("At least 2 axes must be specified.")

        if xnew is not None:
            xnew = xnew.to_cartesian()

        if ynew is not None:
            ynew = ynew.to_cartesian()

        if znew is not None:
            znew = znew.to_cartesian()

        if znew is None:
            znew = xnew.cross(ynew)

        if ynew is None:
            ynew = -xnew.cross(znew)

        if xnew is None:
            xnew = ynew.cross(znew)

        pole = coord.SkyCoord(znew, frame='icrs')
        center = coord.SkyCoord(xnew, frame='icrs')
        return cls(pole=pole, center=center)
Пример #7
0
class Galactocentric(BaseCoordinateFrame):
    r"""
    A coordinate or frame in the Galactocentric system.

    This frame allows specifying the Sun-Galactic center distance, the height of
    the Sun above the Galactic midplane, and the solar motion relative to the
    Galactic center. However, as there is no modern standard definition of a
    Galactocentric reference frame, it is important to pay attention to the
    default values used in this class if precision is important in your code.
    The default values of the parameters of this frame are taken from the
    original definition of the frame in 2014. As such, the defaults are somewhat
    out of date relative to recent measurements made possible by, e.g., Gaia.
    The defaults can, however, be changed at runtime by setting the parameter
    set name in `~astropy.coordinates.galactocentric_frame_defaults`.

    The current default parameter set is ``"pre-v4.0"``, indicating that the
    parameters were adopted before ``astropy`` version 4.0. A regularly-updated
    parameter set can instead be used by setting
    ``galactocentric_frame_defaults.set ('latest')``, and other parameter set
    names may be added in future versions. To find out the scientific papers
    that the current default parameters are derived from, use
    ``galcen.frame_attribute_references`` (where ``galcen`` is an instance of
    this frame), which will update even if the default parameter set is changed.

    The position of the Sun is assumed to be on the x axis of the final,
    right-handed system. That is, the x axis points from the position of
    the Sun projected to the Galactic midplane to the Galactic center --
    roughly towards :math:`(l,b) = (0^\circ,0^\circ)`. For the default
    transformation (:math:`{\rm roll}=0^\circ`), the y axis points roughly
    towards Galactic longitude :math:`l=90^\circ`, and the z axis points
    roughly towards the North Galactic Pole (:math:`b=90^\circ`).

    For a more detailed look at the math behind this transformation, see
    the document :ref:`astropy:coordinates-galactocentric`.

    The frame attributes are listed under **Other Parameters**.
    """

    default_representation = r.CartesianRepresentation
    default_differential = r.CartesianDifferential

    # frame attributes
    galcen_coord = CoordinateAttribute(frame=ICRS)
    galcen_distance = QuantityAttribute(unit=u.kpc)

    galcen_v_sun = DifferentialAttribute(
        allowed_classes=[r.CartesianDifferential])

    z_sun = QuantityAttribute(unit=u.pc)
    roll = QuantityAttribute(unit=u.deg)

    def __init__(self, *args, **kwargs):
        # Set default frame attribute values based on the ScienceState instance
        # for the solar parameters defined above
        default_params = galactocentric_frame_defaults.get()
        self.frame_attribute_references = \
            galactocentric_frame_defaults.references.copy()

        for k in default_params:
            if k in kwargs:
                # If a frame attribute is set by the user, remove its reference
                self.frame_attribute_references.pop(k, None)

            # Keep the frame attribute if it is set by the user, otherwise use
            # the default value
            kwargs[k] = kwargs.get(k, default_params[k])

        super().__init__(*args, **kwargs)

    @classmethod
    def get_roll0(cls):
        """
        The additional roll angle (about the final x axis) necessary to align
        the final z axis to match the Galactic yz-plane.  Setting the ``roll``
        frame attribute to  -this method's return value removes this rotation,
        allowing the use of the `Galactocentric` frame in more general contexts.
        """
        # note that the actual value is defined at the module level.  We make at
        # a property here because this module isn't actually part of the public
        # API, so it's better for it to be accessable from Galactocentric
        return _ROLL0
Пример #8
0
class Galactocentric(BaseCoordinateFrame):
    r"""
    A coordinate or frame in the Galactocentric system. This frame
    requires specifying the Sun-Galactic center distance, and optionally
    the height of the Sun above the Galactic midplane.

    The position of the Sun is assumed to be on the x axis of the final,
    right-handed system. That is, the x axis points from the position of
    the Sun projected to the Galactic midplane to the Galactic center --
    roughly towards :math:`(l,b) = (0^\circ,0^\circ)`. For the default
    transformation (:math:`{\rm roll}=0^\circ`), the y axis points roughly
    towards Galactic longitude :math:`l=90^\circ`, and the z axis points
    roughly towards the North Galactic Pole (:math:`b=90^\circ`).

    The default position of the Galactic Center in ICRS coordinates is
    taken from Reid et al. 2004,
    http://adsabs.harvard.edu/abs/2004ApJ...616..872R.

    .. math::

        {\rm RA} = 17:45:37.224~{\rm hr}\\
        {\rm Dec} = -28:56:10.23~{\rm deg}

    The default distance to the Galactic Center is 8.3 kpc, e.g.,
    Gillessen et al. (2009),
    https://ui.adsabs.harvard.edu/#abs/2009ApJ...692.1075G/abstract

    The default height of the Sun above the Galactic midplane is taken to
    be 27 pc, as measured by Chen et al. (2001),
    https://ui.adsabs.harvard.edu/#abs/2001ApJ...553..184C/abstract

    The default solar motion relative to the Galactic center is taken from a
    combination of Schönrich et al. (2010) [for the peculiar velocity] and
    Bovy (2015) [for the circular velocity at the solar radius],
    https://ui.adsabs.harvard.edu/#abs/2010MNRAS.403.1829S/abstract
    https://ui.adsabs.harvard.edu/#abs/2015ApJS..216...29B/abstract

    For a more detailed look at the math behind this transformation, see
    the document :ref:`coordinates-galactocentric`.

    The frame attributes are listed under **Other Parameters**.
    """

    default_representation = r.CartesianRepresentation
    default_differential = r.CartesianDifferential

    # frame attributes
    galcen_coord = CoordinateAttribute(default=ICRS(ra=266.4051 * u.degree,
                                                    dec=-28.936175 * u.degree),
                                       frame=ICRS)
    galcen_distance = QuantityAttribute(default=8.3 * u.kpc)

    galcen_v_sun = DifferentialAttribute(
        default=r.CartesianDifferential([11.1, 220 + 12.24, 7.25] * u.km /
                                        u.s),
        allowed_classes=[r.CartesianDifferential])

    z_sun = QuantityAttribute(default=27. * u.pc)
    roll = QuantityAttribute(default=0. * u.deg)

    def __init__(self, *args, **kwargs):

        # backwards-compatibility
        if ('galcen_ra' in kwargs or 'galcen_dec' in kwargs):
            warnings.warn(
                "The arguments 'galcen_ra', and 'galcen_dec' are "
                "deprecated in favor of specifying the sky coordinate"
                " as a CoordinateAttribute using the 'galcen_coord' "
                "argument", AstropyDeprecationWarning)

            galcen_kw = dict()
            galcen_kw['ra'] = kwargs.pop('galcen_ra', self.galcen_coord.ra)
            galcen_kw['dec'] = kwargs.pop('galcen_dec', self.galcen_coord.dec)
            kwargs['galcen_coord'] = ICRS(**galcen_kw)

        super().__init__(*args, **kwargs)

    @property
    def galcen_ra(self):
        warnings.warn(
            "The attribute 'galcen_ra' is deprecated. Use "
            "'.galcen_coord.ra' instead.", AstropyDeprecationWarning)
        return self.galcen_coord.ra

    @property
    def galcen_dec(self):
        warnings.warn(
            "The attribute 'galcen_dec' is deprecated. Use "
            "'.galcen_coord.dec' instead.", AstropyDeprecationWarning)
        return self.galcen_coord.dec

    @classmethod
    def get_roll0(cls):
        """
        The additional roll angle (about the final x axis) necessary to align
        the final z axis to match the Galactic yz-plane.  Setting the ``roll``
        frame attribute to  -this method's return value removes this rotation,
        allowing the use of the `Galactocentric` frame in more general contexts.
        """
        # note that the actual value is defined at the module level.  We make at
        # a property here because this module isn't actually part of the public
        # API, so it's better for it to be accessable from Galactocentric
        return _ROLL0
Пример #9
0
class Galactocentric(BaseCoordinateFrame):
    r"""
    A coordinate or frame in the Galactocentric system.

    This frame allows specifying the Sun-Galactic center distance, the height of
    the Sun above the Galactic midplane, and the solar motion relative to the
    Galactic center. However, as there is no modern standard definition of a
    Galactocentric reference frame, it is important to pay attention to the
    default values used in this class if precision is important in your code.
    The default values of the parameters of this frame are taken from the
    original definition of the frame in 2014. As such, the defaults are somewhat
    out of date relative to recent measurements made possible by, e.g., Gaia.
    The defaults can, however, be changed at runtime by setting the parameter
    set name in `~astropy.coordinates.galactocentric_frame_defaults`.

    The current default parameter set is ``"pre-v4.0"``, indicating that the
    parameters were adopted before ``astropy`` version 4.0. A regularly-updated
    parameter set can instead be used by setting
    ``galactocentric_frame_defaults.set ('latest')``, and other parameter set
    names may be added in future versions. To find out the scientific papers
    that the current default parameters are derived from, use
    ``galcen.frame_attribute_references`` (where ``galcen`` is an instance of
    this frame), which will update even if the default parameter set is changed.

    The position of the Sun is assumed to be on the x axis of the final,
    right-handed system. That is, the x axis points from the position of
    the Sun projected to the Galactic midplane to the Galactic center --
    roughly towards :math:`(l,b) = (0^\circ,0^\circ)`. For the default
    transformation (:math:`{\rm roll}=0^\circ`), the y axis points roughly
    towards Galactic longitude :math:`l=90^\circ`, and the z axis points
    roughly towards the North Galactic Pole (:math:`b=90^\circ`).

    For a more detailed look at the math behind this transformation, see
    the document :ref:`coordinates-galactocentric`.

    The frame attributes are listed under **Other Parameters**.
    """

    default_representation = r.CartesianRepresentation
    default_differential = r.CartesianDifferential

    # frame attributes
    galcen_coord = CoordinateAttribute(frame=ICRS)
    galcen_distance = QuantityAttribute(unit=u.kpc)

    galcen_v_sun = DifferentialAttribute(
        allowed_classes=[r.CartesianDifferential])

    z_sun = QuantityAttribute(unit=u.pc)
    roll = QuantityAttribute(unit=u.deg)

    def __init__(self, *args, **kwargs):
        # Set default frame attribute values based on the ScienceState instance
        # for the solar parameters defined above
        default_params = galactocentric_frame_defaults.get()
        self.frame_attribute_references = \
            galactocentric_frame_defaults._references.copy()

        warn = False
        for k in default_params:
            # If a frame attribute is set by the user, remove its reference
            self.frame_attribute_references.pop(k, None)

            # If a parameter is read from the defaults, we might want to warn
            # the user that the defaults will change (see below)
            if k not in kwargs:
                warn = True

            # Keep the frame attribute if it is set by the user, otherwise use
            # the default value
            kwargs[k] = kwargs.get(k, default_params[k])

        # If the frame defaults have not been updated with the ScienceState
        # class, and the user uses any default parameter value, raise a
        # deprecation warning to inform them that the defaults will change in
        # the future:
        if galactocentric_frame_defaults._value == 'pre-v4.0' and warn:
            docs_link = 'http://docs.astropy.org/en/latest/coordinates/galactocentric.html'
            warnings.warn(
                'In v4.1 and later versions, the Galactocentric '
                'frame will adopt default parameters that may update '
                'with time. An updated default parameter set is '
                'already available through the '
                'astropy.coordinates.galactocentric_frame_defaults '
                'ScienceState object, as described in but the '
                'default is currently still set to the pre-v4.0 '
                'parameter defaults. The safest way to guard against '
                'changing default parameters in the future is to '
                'either (1) specify all Galactocentric frame '
                'attributes explicitly when using the frame, '
                'or (2) set the galactocentric_frame_defaults '
                f'parameter set name explicitly. See {docs_link} for more '
                'information.', AstropyDeprecationWarning)

        super().__init__(*args, **kwargs)

    @classmethod
    def get_roll0(cls):
        """
        The additional roll angle (about the final x axis) necessary to align
        the final z axis to match the Galactic yz-plane.  Setting the ``roll``
        frame attribute to  -this method's return value removes this rotation,
        allowing the use of the `Galactocentric` frame in more general contexts.
        """
        # note that the actual value is defined at the module level.  We make at
        # a property here because this module isn't actually part of the public
        # API, so it's better for it to be accessable from Galactocentric
        return _ROLL0
Пример #10
0
class SkyOffsetFrame(BaseCoordinateFrame):
    """
    A frame which is relative to some specific position and oriented to match
    its frame.

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

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

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

    Parameters
    ----------
    representation : `~astropy.coordinates.BaseRepresentation` or None
        A representation object or None to have no data (or use the other keywords)
    origin : coordinate-like
        The coordinate which specifies the origin of this frame. Note that this
        origin is used purely for on-sky location/rotation.  It can have a
        ``distance`` but it will not be used by this ``SkyOffsetFrame``.
    rotation : angle-like
        The final rotation of the frame about the ``origin``. The sign of
        the rotation is the left-hand rule.  That is, an object at a
        particular position angle in the un-rotated system will be sent to
        the positive latitude (z) direction in the final frame.


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

    rotation = QuantityAttribute(default=0, unit=u.deg)
    origin = CoordinateAttribute(default=None, frame=None)

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

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

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.origin is not None and not self.origin.has_data:
            raise ValueError('The origin supplied to SkyOffsetFrame has no '
                             'data.')
        if self.has_data:
            self._set_skyoffset_data_lon_wrap_angle(self.data)

    @staticmethod
    def _set_skyoffset_data_lon_wrap_angle(data):
        if hasattr(data, 'lon'):
            data.lon.wrap_angle = 180. * u.deg
        return data

    def represent_as(self, base, s='base', in_frame_units=False):
        """
        Ensure the wrap angle for any spherical
        representations.
        """
        data = super().represent_as(base, s, in_frame_units=in_frame_units)
        self._set_skyoffset_data_lon_wrap_angle(data)
        return data

    def __reduce__(self):
        return (_skyoffset_reducer, (self.origin, ), self.__dict__)
Пример #11
0
def make_skyoffset_cls(framecls):
    """
    Create a new class that is the sky offset frame for a specific class of
    origin frame. If such a class has already been created for this frame, the
    same class will be returned.

    The new class will always have component names for spherical coordinates of
    ``lon``/``lat``.

    Parameters
    ----------
    framecls : `~astropy.coordinates.BaseCoordinateFrame` subclass
        The class to create the SkyOffsetFrame of.

    Returns
    -------
    skyoffsetframecls : class
        The class for the new skyoffset frame.

    Notes
    -----
    This function is necessary because Astropy's frame transformations depend
    on connection between specific frame *classes*.  So each type of frame
    needs its own distinct skyoffset frame class.  This function generates
    just that class, as well as ensuring that only one example of such a class
    actually gets created in any given python session.
    """

    if framecls in _skyoffset_cache:
        return _skyoffset_cache[framecls]

    # Create a new SkyOffsetFrame subclass for this frame class.
    name = 'SkyOffset' + framecls.__name__
    _SkyOffsetFramecls = type(
        name,
        (SkyOffsetFrame, framecls),
        {
            'origin': CoordinateAttribute(frame=framecls, default=None),
            # The following two have to be done because otherwise we use the
            # defaults of SkyOffsetFrame set by BaseCoordinateFrame.
            '_default_representation': framecls._default_representation,
            '_default_differential': framecls._default_differential,
            '__doc__': SkyOffsetFrame.__doc__,
        })

    @frame_transform_graph.transform(FunctionTransform, _SkyOffsetFramecls,
                                     _SkyOffsetFramecls)
    def skyoffset_to_skyoffset(from_skyoffset_coord, to_skyoffset_frame):
        """Transform between two skyoffset frames."""

        # This transform goes through the parent frames on each side.
        # from_frame -> from_frame.origin -> to_frame.origin -> to_frame
        intermediate_from = from_skyoffset_coord.transform_to(
            from_skyoffset_coord.origin)
        intermediate_to = intermediate_from.transform_to(
            to_skyoffset_frame.origin)
        return intermediate_to.transform_to(to_skyoffset_frame)

    @frame_transform_graph.transform(DynamicMatrixTransform, framecls,
                                     _SkyOffsetFramecls)
    def reference_to_skyoffset(reference_frame, skyoffset_frame):
        """Convert a reference coordinate to an sky offset frame."""

        # Define rotation matrices along the position angle vector, and
        # relative to the origin.
        origin = skyoffset_frame.origin.spherical
        mat1 = rotation_matrix(-skyoffset_frame.rotation, 'x')
        mat2 = rotation_matrix(-origin.lat, 'y')
        mat3 = rotation_matrix(origin.lon, 'z')
        return matrix_product(mat1, mat2, mat3)

    @frame_transform_graph.transform(DynamicMatrixTransform,
                                     _SkyOffsetFramecls, framecls)
    def skyoffset_to_reference(skyoffset_coord, reference_frame):
        """Convert an sky offset frame coordinate to the reference frame"""

        # use the forward transform, but just invert it
        R = reference_to_skyoffset(reference_frame, skyoffset_coord)
        # transpose is the inverse because R is a rotation matrix
        return matrix_transpose(R)

    _skyoffset_cache[framecls] = _SkyOffsetFramecls
    return _SkyOffsetFramecls
Пример #12
0
class OrbitOffsetFrame(BaseCoordinateFrame):
    """Offset Frame from an Orbit in a Potential.

    A frame which is relative to some specific position and oriented to match
    its frame.

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

    Parameters
    ----------
    representation : `~astropy.coordinates.BaseRepresentation` or None
        A representation object or None to have no data,
        or use the other keywords
    origin : `~astropy.coordinates.SkyCoord` or low-level coordinate object.
        The coordinate which specifies the origin of this frame.
    potential : `~galpy.potential.Potential` or list thereof.

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

    """

    origin = CoordinateAttribute(default=None, frame=None)
    potential = PotentialAttribute()
    afn_bounds = Attribute(  # FIXME QuantityAttribute
        # default=[-np.inf, np.inf] * u.yr,
        # unit=u.Myr, shape=(2,)
    )  # [afn_trail, afn_lead]

    # frame_specific_representation_info = {  # note, not in Astropy's Offset
    #     "cartesian": [
    #         RepresentationMapping("z", "afn"),
    #         RepresentationMapping("x", "x"),
    #         RepresentationMapping("y", "y"),
    #     ],
    # }

    def __new__(cls, *args, **kwargs):
        """OrbitOffsetFrame."""
        # We don't want to call this method if we've already set up
        # an orbitoffset frame for this class.
        if not (issubclass(cls, OrbitOffsetFrame)
                and cls is not OrbitOffsetFrame):
            # We get the origin argument, and handle it here.
            # TODO maybe get from orbit function, putting in afn t=0 ?
            try:
                origin_frame = kwargs["origin"]
            except KeyError:
                raise TypeError("Can't initialize an OrbitOffsetFrame "
                                "without origin= keyword.")

            try:  # TODO add to Frame for reverse transformations?
                track_fn = kwargs.pop("track_fn")
            except KeyError:
                raise TypeError("Can't initialize an OrbitOffsetFrame "
                                "without origin= keyword.")

            inverse_track_fn = kwargs.pop("inverse_track_fn", None)

            track_fn_kw = kwargs.pop("track_fn_kw", {})
            inverse_track_fn_kw = kwargs.pop("inverse_track_fn_kw", {})
            # adj_sep_sgn = kwargs.pop("adj_sep_sgn", False)  # has default

            # and the potential argument
            try:
                potential = kwargs["potential"]
            except KeyError:
                raise TypeError("Can't initialize an OrbitOffsetFrame "
                                "without potential= keyword.")

            # and the afn arguments
            # TODO change to afn bounds (tail, lead)
            try:
                afn_bounds = kwargs["afn_bounds"]
            except KeyError:
                raise TypeError("Can't initialize an OrbitOffsetFrame "
                                "without afn_bounds= keyword.")
            else:
                if len(afn_bounds) != 2:
                    raise ValueError("`afn_bounds` must be len= 2.s")

            if hasattr(origin_frame, "frame"):
                origin_frame = origin_frame.frame

            newcls = make_orbitoffset_cls(
                origin_frame.__class__,
                potential=potential,
                track_fn=track_fn,
                track_fn_kw=track_fn_kw,
                inverse_track_fn=inverse_track_fn,
                inverse_track_fn_kw=inverse_track_fn_kw,
                # adj_sep_sgn=adj_sep_sgn,
            )

            return newcls.__new__(newcls, *args, **kwargs)

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

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

    # /def

    @classmethod
    def from_galpy_orbit(
        cls,
        *args,
        orbit,
        orbit_bkw=None,
        frame="galactocentric",
        method: T.Union[Literal["closest"], Literal["linear"],
                        Literal["cubic"],
                        T.Callable[[T.Sequence], T.Sequence], ] = "closest",
        time_unit=None,
        **kwargs,
    ):
        """Create an Orbit Offset Frame from a galpy orbit.

        Parameters
        ----------
        orbit : `~galpy.orbit.Orbit`
            An integrated single orbit., keyword only
            the initial 4-vector and conjugate momenta are taken as the origin.
        orbit_bkw : `~galpy.orbit.Orbit`, optional, keyword only
            An integrated single orbit in the opposite time direction.
            This allows for a leading and trailing orbit from the origin point.
            Must have same origin as `orbit`, but be integrated in reverse.
        frame : str or BaseCoordinateFrame, optional, keyword only
            frame in which to represent the orbit.
            calls ``transform_to(frame)`` on `orbit`'s ICRS SkyCoord output,
            so be careful about things like Galactocentric defaults
        method : {"closest", "linear", "cubic"} or Callable, optional, keyword
            how to construct the affine parameter function mapping time to
            coordinate The orbit integration is precomputed at discrete time
            intervals and the path frame needs to be able to match coordinates
            to the closest point on the orbit. This can be done by treating
            the orbit as an immutable catalog (option "closest", default), a
            linearly interpolatable set of points (option "linear") with
            :class:`~scipy.interpolate.interp1d`, a cubic interpolation
            (option "cubic") with :class:`~scipy.interpolate.CubicSpline`.

            .. todo::

                allow user-provided functions that take the time and
                coordinates and return a function that, given a set of
                coordinates, returns the time of and angular separation from
                "closest" (for some dfn) point on orbit.
        time_unit : Quantity or Nont, optional, keyword only
            preferred time unit. None means no modification.
            Galpy defaults to Gyr.

        Raises
        ------
        ValueError
            if `orbit` is not integrated
            if `orbit_bkw` is not integrated (and not None)
            if the potential in `orbit` and `orbit_bkw` do not match.

        Notes
        -----
        Make sure that the orbit does not wrap and get close to itself.
        There are currently no checks that this is happening and this
        can lead to some very strange coordinate projections.

        .. todo::

            allow affine parameter to be arc length

        """
        # ----------------------------------------------------
        # Checks
        # if orbit_bkw is not None, check has potential and matches orbit
        # check times go in opposite directions

        try:  # check orbit has potential
            orbit._pot
        except AttributeError:  # it does not
            raise ValueError("`orbit` must be integrated.")
        else:  # it does
            # grab the potential, integration time, and origin
            potential = orbit._pot
            t_fwd = orbit.time(use_physical=True)
            origin = orbit.SkyCoord().transform_to(frame)

        if orbit_bkw is not None:
            try:
                orbit._pot
            except AttributeError:
                raise ValueError("`orbit_bkw` must be integrated.")

            if orbit._pot != potential:
                raise ValueError(
                    ("potential in `orbit` and `orbit_bkw` do not match."))

            # check time "directions" are opposite
            # they "fwd" direction can be back in time. That is permitted
            # just not allowed to have the "bkw" direction be the same.
            time_bkw_sgn = np.sign(orbit_bkw.t[1] - orbit_bkw.t[0])
            t_fwd_sgn = np.sign(orbit.t[1] - orbit.t[0])

            if time_bkw_sgn == t_fwd_sgn:
                raise ValueError(("`orbit` and `orbit_bkw` must be integrated"
                                  "in opposite time directions"))

            # get back time, converting to correct units
            t_bkw = orbit_bkw.time(use_physical=True)[::-1] << t_fwd.unit

            # concatenate fwd and bkw orbits into single orbit catalog
            orbit_catalog = concatenate([
                orbit_bkw.SkyCoord(t_bkw).transform_to(frame).frame,
                orbit.SkyCoord(t_fwd).transform_to(frame).frame,
            ])
            orbit_time = np.concatenate((t_bkw, t_fwd))

            # create time bounds
            _u = t_fwd.unit if time_unit is None else time_unit
            if t_fwd[-1] > t_bkw[-1]:
                t_bnds = [t_bkw[0], t_fwd[-1]] << _u
            else:
                t_bnds = [t_fwd[0], t_bkw[-1]] << _u

        else:
            # create orbit catalog
            orbit_catalog = orbit.SkyCoord(t_fwd).transform_to(frame)
            orbit_time = t_fwd

            # create time bounds
            _u = t_fwd.unit if time_unit is None else time_unit
            if t_fwd[-1] > t_fwd[0]:
                t_bnds = [t_fwd[0], t_fwd[-1]] << _u
            else:
                t_bnds = [t_fwd[-1], t_fwd[0]] << _u

        # convert orbit time to `time_unit`, if specified
        if time_unit is not None:
            orbit_time <<= time_unit  # (in-place modification)
        else:  # time unit is not None
            time_unit = orbit_time.unit

        # ----------------------------------------------------
        # construct affine function

        track_fn_kw = kwargs.pop("track_fn_kw", {"afn_name": "time"})
        inverse_track_fn_kw = kwargs.pop("inverse_track_fn_kw", {})

        if isinstance(method, str):
            # now need to check it's one of the supported strings
            if method.lower() == "closest":
                # does a catalog match between the coordinates and the
                # points on the orbit from the orbit integration
                # track_fn = ("closest", orbit_catalog, orbit_time)
                track_fn = catalog_match_track
                inverse_track_fn = catalog_match_inverse_track
                track_fn_kw = {
                    "catalog": orbit_catalog,
                    "affine_param": orbit_time,
                    "adj_sep_sgn": True,
                    "afn_name": "time",
                }
                inverse_track_fn_kw = {
                    "catalog": orbit_catalog,
                    "affine_param": orbit_time,
                }

                _interpolation_flag = False

            else:
                # need to handle interpolated functions separately,
                # because requires a closest point "optimization"
                if method.lower() == "linear":
                    method = interpolate.interp1d
                elif method.lower() == "cubic":
                    method = interpolate.CubicSpline
                else:
                    raise ValueError(f"method {method} not known.")

                _interpolation_flag = True

        elif callable(method):
            _interpolation_flag = True

        else:
            raise ValueError(f"method {method} not known.")

        # /if

        if _interpolation_flag:

            # get affine parameter and data interpolation ready
            affine_param = orbit_time.to_value(time_unit)
            _data = orbit_catalog.data._values
            _data = _data.view(np.float64).reshape(_data.shape + (-1, ))

            # construct interpolation
            _track_array_fn = method(affine_param, _data.T)

            # astropy coordinate object reconstruction information
            _cmpt = [
                (
                    c,
                    orbit_catalog.data._units[c],
                )  # in right order, but dict
                for c in orbit_catalog.data.components  # tuple = order
            ]
            _frame = orbit_catalog.frame.realize_frame(None)
            _rep = orbit_catalog.frame.get_representation_cls()

            # interpolation function as astropy, not numpy
            def _track_fn(affine_param):
                """_track_array_fn converted back into a coordinate object."""
                _oc = _track_array_fn(affine_param)  # evaluate interpolation
                rep = _rep(**{c: _oc[i] * U for i, (c, U) in enumerate(_cmpt)})
                catalog = SkyCoord(  # make catalog (TODO not SkyCoord)
                    _frame.realize_frame(rep))
                return catalog

            # make actual track and inverse functions
            def track_fn(coords,
                         tol=None,
                         init_sampler: T.Union[float, int] = 1e4):
                """Map coordinates to catalog projection.

                .. todo::

                    change defualt `tol` to something else

                Parameters
                ----------
                coords: SkyCoord
                tol : float or None, optional
                    If None (default), does catalog match but no further
                    minimization. The catalog is the evaluation of the "method"
                    function with affine parameter linearly sampled with
                    `init_sampler` points between "afn_bounds"
                init_sampler : int or float, optional
                    the number of points in ``np.linspace`` for an inital
                    sampling of the affine parameter.

                """
                _aff = np.linspace(  # affine parameter
                    *t_bnds, num=int(init_sampler))[1:-2]
                catalog = _track_fn(_aff)

                if tol is None:
                    return catalog_match_track(
                        coords,
                        catalog=catalog,
                        affine_param=_aff,
                        adj_sep_sgn=True,
                    )
                else:  # TODO actual minimization
                    # initial guess
                    idx, sep2d, _, = match_coordinates_3d(coords, catalog)
                    raise ValueError("Not yet implemented")

                return idx

            # /def

            def inverse_track_fn(coords, **kw):
                """Map catalog projection to coordinates.

                .. todo::

                    this is a very generic function. put somewhere else.

                Parameters
                ----------
                coords: SkyCoord
                    in orbit projection frame

                """
                # orbit_pos = _track_fn(coords.afn)

                # need to know offset
                # d_afn = coords.data._d_afn
                # Now offset by `d_afn` in direction `that`
                raise ValueError("TODO")
                # out = orbit_pos.directional_offset_by(
                #     pa, np.abs(coords.sep)  # need abs() b/c `adj_sep_sgn`
                # ).represent_as("spherical")

                # return out.x, out.y, coords.z

            # /def
        # /if

        # TODO correct construction with init to get the Attributes
        self = cls(
            *args,
            origin=origin,
            potential=potential,
            track_fn=track_fn,
            inverse_track_fn=inverse_track_fn,
            track_fn_kw=track_fn_kw,
            inverse_track_fn_kw=inverse_track_fn_kw,
            afn_bounds=t_bnds,
            **kwargs,
        )

        return self

    # /def

    # ---------------------------------------------------------------

    def __init__(self, *args, **kwargs):
        """Initialize OrbitOffsetFrame."""
        # TODO temporary ------->
        kwargs.pop("track_fn", None)
        kwargs.pop("inverse_track_fn", None)
        kwargs.pop("track_fn_kw", None)
        kwargs.pop("inverse_track_fn_kw", None)
        # <-------

        super().__init__(*args, **kwargs)

        if self.origin is not None and not self.origin.has_data:
            raise ValueError(
                "The origin supplied to OrbitOffsetFrame has no data.")