Example #1
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)
Example #2
0
class HADec(BaseCoordinateFrame):
    """
    A coordinate or frame in the Hour Angle-Declination system (Equatorial
    coordinates) with respect to the WGS84 ellipsoid.  Hour Angle is oriented
    with respect to upper culmination such that the hour angle is negative to
    the East and positive to the West.

    This frame is assumed to *include* refraction effects if the ``pressure``
    frame attribute is non-zero.

    The frame attributes are listed under **Other Parameters**, which are
    necessary for transforming from HADec to some other system.
    """

    frame_specific_representation_info = {
        r.SphericalRepresentation: [
            RepresentationMapping('lon', 'ha', u.hourangle),
            RepresentationMapping('lat', 'dec')
        ]
    }

    default_representation = r.SphericalRepresentation
    default_differential = r.SphericalCosLatDifferential

    obstime = TimeAttribute(default=None)
    location = EarthLocationAttribute(default=None)
    pressure = QuantityAttribute(default=0, unit=u.hPa)
    temperature = QuantityAttribute(default=0, unit=u.deg_C)
    relative_humidity = QuantityAttribute(default=0,
                                          unit=u.dimensionless_unscaled)
    obswl = QuantityAttribute(default=1 * u.micron, unit=u.micron)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.has_data:
            self._set_data_lon_wrap_angle(self.data)

    @staticmethod
    def _set_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_data_lon_wrap_angle(data)
        return data
Example #3
0
class AltAz(BaseCoordinateFrame):
    """
    A coordinate or frame in the Altitude-Azimuth system (Horizontal
    coordinates) with respect to the WGS84 ellipsoid.  Azimuth is oriented
    East of North (i.e., N=0, E=90 degrees).  Altitude is also known as
    elevation angle, so this frame is also in the Azimuth-Elevation system.

    This frame is assumed to *include* refraction effects if the ``pressure``
    frame attribute is non-zero.

    The frame attributes are listed under **Other Parameters**, which are
    necessary for transforming from AltAz to some other system.
    """

    frame_specific_representation_info = {
        r.SphericalRepresentation: [
            RepresentationMapping('lon', 'az'),
            RepresentationMapping('lat', 'alt')
        ]
    }

    default_representation = r.SphericalRepresentation
    default_differential = r.SphericalCosLatDifferential

    obstime = TimeAttribute(default=None)
    location = EarthLocationAttribute(default=None)
    pressure = QuantityAttribute(default=0, unit=u.hPa)
    temperature = QuantityAttribute(default=0, unit=u.deg_C)
    relative_humidity = QuantityAttribute(default=0,
                                          unit=u.dimensionless_unscaled)
    obswl = QuantityAttribute(default=1 * u.micron, unit=u.micron)

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

    @property
    def secz(self):
        """
        Secant of the zenith angle for this coordinate, a common estimate of
        the airmass.
        """
        return 1 / np.sin(self.alt)

    @property
    def zen(self):
        """
        The zenith angle (or zenith distance / co-altitude) for this coordinate.
        """
        return _90DEG.to(self.alt.unit) - self.alt
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)
Example #5
0
class CustomBarycentricEcliptic(BaseEclipticFrame):
    """
    Barycentric ecliptic coordinates with custom obliquity.
    These origin of the coordinates are the
    barycenter of the solar system, with the x axis pointing in the direction of
    the *mean* (not true) equinox of J2000, and the xy-plane in the plane of the
    ecliptic tilted a custom obliquity angle.

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

    obliquity = QuantityAttribute(default=84381.448 * u.arcsec, unit=u.arcsec)
Example #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)
Example #7
0
class RotatedSunFrame(SunPyBaseCoordinateFrame):
    """
    A frame that applies solar rotation to a base coordinate frame.

    .. note::

        See :ref:`sunpy-coordinates-rotatedsunframe` for how to use this class.

    In essence, the coordinate axes of the frame are distorted by differential solar rotation.
    This allows using a coordinate representation at one time (at the ``obstime`` of the base
    coordinate frame) to point to a location at a different time that has been differentially
    rotated by the time difference (``duration``).

    Parameters
    ----------
    representation : `~astropy.coordinates.BaseRepresentation` or ``None``
        A representation object or ``None`` to have no data.  Alternatively, use coordinate
        component keyword arguments, which depend on the base frame.
    base : `~astropy.coordinates.SkyCoord` or low-level coordinate object.
        The coordinate which specifies the base coordinate frame.  The frame must be a SunPy frame.
    duration : `~astropy.units.Quantity`
        The duration of solar rotation (defaults to zero days).
    rotated_time : {parse_time_types}
        The time to rotate the Sun to.  If provided, ``duration`` will be set to the difference
        between this time and the observation time in ``base``.
    rotation_model : `str`
        Accepted model names are ``'howard'`` (default), ``'snodgrass'``, ``'allen'``, and ``'rigid'``.
        See the documentation for :func:`~sunpy.physics.differential_rotation.diff_rot` for differences
        between these models.

    Notes
    -----
    ``RotatedSunFrame`` is a factory class.  That is, the objects that it
    yields are *not* actually objects of class ``RotatedSunFrame``.  Instead,
    distinct classes are created on-the-fly for whatever the frame class is
    of ``base``.
    """
    # This code reuses significant code from Astropy's implementation of SkyOffsetFrame
    # See licenses/ASTROPY.rst

    # We don't want to inherit the frame attributes of SunPyBaseCoordinateFrame (namely `obstime`)
    # Note that this does not work for Astropy 4.3+, so we need to manually remove it below
    _inherit_descriptors_ = False

    # Even though the frame attribute `base` is a coordinate frame, we use `Attribute` instead of
    # `CoordinateAttribute` because we are preserving the supplied frame rather than converting to
    # a common frame.
    base = Attribute()

    duration = QuantityAttribute(default=0 * u.day)
    rotation_model = Attribute(default='howard')

    def __new__(cls, *args, **kwargs):
        # We don't want to call this method if we've already set up
        # an rotated-Sun frame for this class.
        if not (issubclass(cls, RotatedSunFrame)
                and cls is not RotatedSunFrame):
            # We get the base argument, and handle it here.
            base_frame = kwargs.get('base', None)
            if base_frame is None:
                raise TypeError(
                    "Can't initialize a RotatedSunFrame without a `base` keyword."
                )

            # If a SkyCoord is provided, use the underlying frame
            if hasattr(base_frame, 'frame'):
                base_frame = base_frame.frame

            newcls = _make_rotatedsun_cls(base_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):
        # Validate inputs
        if kwargs['base'].obstime is None:
            raise ValueError(
                "The base coordinate frame must have a defined `obstime`.")

        if 'rotated_time' in kwargs:
            rotated_time = parse_time(kwargs['rotated_time'])
            kwargs['duration'] = (rotated_time -
                                  kwargs['base'].obstime).to('day')
            kwargs.pop('rotated_time')

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

        # Move data out from the base frame
        if self.base.has_data:
            if not self.has_data:
                # If the duration is an array but the data is scalar, upgrade data to an array
                if self.base.data.isscalar and not self.duration.isscalar:
                    self._data = self.base.data._apply('repeat',
                                                       self.duration.shape)
                else:
                    self._data = self.base.data
            self._base = self.base.replicate_without_data()

    def as_base(self):
        """
        Returns a coordinate with the current representation and in the base coordinate frame.

        This method can be thought of as "removing" the
        `~sunpy.coordinates.metaframes.RotatedSunFrame` layer.  Be aware that this method is not
        merely a coordinate transformation, because this method changes the location in inertial
        space that is being pointed to.
        """
        return self.base.realize_frame(self.data)

    @property
    def rotated_time(self):
        """
        Returns the sum of the base frame's observation time and the rotation of duration.
        """
        return self.base.obstime + self.duration
Example #8
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
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
Example #10
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
Example #11
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__)
Example #12
0
class OrbitSkyOffsetFrame(BaseCoordinateFrame):
    """Sky Offset Frame from an Orbit in a Potential.

    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-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. Note that this
        origin is used purely for on-sky location.  It can have a
        ``distance`` but it will not be used by this ``OrbitSkyOffsetFrame``.
    potential : `~galpy.potential.Potential` or list thereof.

    Notes
    -----
    ``OrbitSkyOffsetFrame`` is a factory class.  That is, the objects that it
    yields are *not* actually objects of class ``OrbitSkyOffsetFrame``.
    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_bound_tail = QuantityAttribute(default=-np.inf * u.Myr, unit=u.Myr)
    afn_bound_lead = QuantityAttribute(default=np.inf * u.Myr, unit=u.Myr)

    @property
    def afn_bounds(self):
        """Affine bounds (tail, lead)."""
        return u.Quantity([self.afn_bound_tail, self.afn_bound_lead])

    # /def

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

            try:
                track_fn = kwargs.pop("track_fn")
            except KeyError:
                raise TypeError("Can't initialize an OrbitSkyOffsetFrame "
                                "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", {})

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

            # and the afn arguments
            try:
                afn_bounds = kwargs["afn_bounds"]
            except KeyError:
                raise TypeError("Can't initialize an OrbitSkyOffsetFrame "
                                "without afn_bounds= keyword.")
            else:
                if len(afn_bounds) != 2:
                    raise ValueError("`afn_bounds` must be len= 2.")

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

            newcls = make_orbitskyoffset_cls(
                origin_frame.__class__,
                track_fn=track_fn,
                track_fn_kw=track_fn_kw,
                inverse_track_fn=inverse_track_fn,
                inverse_track_fn_kw=inverse_track_fn_kw,
            )

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

        # /if

        # 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 Sky-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`,
            or any user-provided univariate function like those of "linear"
            and "cubic".

            .. todo::

                minimization set by "tol" parameter for closest point on curve
        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
            - break out into a function that calls OrbitSkyOffsetFrame
              so not a classmethod. Keeps this class general.

        """
        # ----------------------------------------------------
        # 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_sky(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 direction. Most should have _PA
                pa = coords.data._PA
                # Now offset by `sep` in direction `pa`
                out = orbit_pos.directional_offset_by(
                    pa,
                    np.abs(coords.sep)  # need abs() b/c `adj_sep_sgn`
                ).represent_as("spherical")

                return out.lon, out.lat, coords.distance

            # /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 OrbitSkyOffsetFrame."""
        # remove arguments into __new__ that are not supported in __init__.
        kwargs.pop("track_fn", None)
        kwargs.pop("inverse_track_fn", None)
        kwargs.pop("track_fn_kw", None)
        kwargs.pop("inverse_track_fn_kw", None)

        afn_bounds = kwargs.pop("afn_bounds", None)
        if afn_bounds is not None:
            kwargs["afn_bound_tail"], kwargs["afn_bound_lead"] = afn_bounds

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

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