Source code for sunpy.coordinates.metaframes

"""
Coordinate frames that are defined relative to other frames
"""

import astropy.units as u
from astropy.coordinates.attributes import Attribute, QuantityAttribute
from astropy.coordinates.baseframe import frame_transform_graph
from astropy.coordinates.transformations import FunctionTransform

from sunpy.time import parse_time
from sunpy.time.time import _variables_for_parse_time_docstring
from sunpy.util.decorators import add_common_docstring
from .frames import HeliographicStonyhurst, SunPyBaseCoordinateFrame
from .offset_frame import NorthOffsetFrame
from .transformations import _transformation_debug

__all__ = ['NorthOffsetFrame', 'RotatedSunFrame']


# The code for NorthOffsetFrame currently lives in `offset_frame.py`
# This changes its module to this file so that the docs think that the class is local
NorthOffsetFrame.__module__ = __name__


_rotatedsun_cache = {}


def _make_rotatedsun_cls(framecls):
    """
    Create a new class that is the rotated-Sun frame for a specific class of
    base frame. If such a class has already been created for this frame, the
    same class will be returned.

    This function is necessary because frame transformations depend
    on connection between specific frame *classes*.  So each type of frame
    needs its own distinct rotated-Sun 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.
    """
    # This code reuses significant code from Astropy's implementation of SkyOffsetFrame
    # See licenses/ASTROPY.rst

    if framecls in _rotatedsun_cache:
        return _rotatedsun_cache[framecls]

    members = {'__doc__': f'{RotatedSunFrame.__doc__}\n{framecls.__doc__}'}

    # Copy over the defaults from the input frame class
    attrs_to_copy = ['_default_representation',
                     '_default_differential',
                     '_frame_specific_representation_info']
    for attr in attrs_to_copy:
        members[attr] = getattr(framecls, attr)

    _RotatedSunFramecls = type(f'RotatedSun{framecls.__name__}', (RotatedSunFrame,), members)

    @frame_transform_graph.transform(FunctionTransform, _RotatedSunFramecls, _RotatedSunFramecls)
    @_transformation_debug(f"{_RotatedSunFramecls.__name__}->{_RotatedSunFramecls.__name__}")
    def rotatedsun_to_rotatedsun(from_rotatedsun_coord, to_rotatedsun_frame):
        """Transform between two rotated-Sun frames."""
        # This transform goes through the parent frames on each side.
        # from_frame -> HGS -> to_frame
        int_frame = HeliographicStonyhurst(obstime=from_rotatedsun_coord.base.obstime)
        int_coord = from_rotatedsun_coord.transform_to(int_frame)
        return int_coord.transform_to(to_rotatedsun_frame)

    @frame_transform_graph.transform(FunctionTransform, HeliographicStonyhurst, _RotatedSunFramecls)
    @_transformation_debug(f"HGS->{_RotatedSunFramecls.__name__}")
    def reference_to_rotatedsun(hgs_coord, rotatedsun_frame):
        int_frame = HeliographicStonyhurst(obstime=rotatedsun_frame.base.obstime)
        int_coord = hgs_coord.make_3d().transform_to(int_frame)  # obstime change handled here

        # Rotate the coordinate in HGS
        int_coord = int_coord._apply_diffrot(-rotatedsun_frame.duration,
                                             rotatedsun_frame.rotation_model)

        # Transform from HGS
        new_coord = int_coord.transform_to(rotatedsun_frame.base)
        return rotatedsun_frame.realize_frame(new_coord.data)

    @frame_transform_graph.transform(FunctionTransform, _RotatedSunFramecls, HeliographicStonyhurst)
    @_transformation_debug(f"{_RotatedSunFramecls.__name__}->HGS")
    def rotatedsun_to_reference(rotatedsun_coord, hgs_frame):
        # Transform to HGS
        from_coord = rotatedsun_coord.base.realize_frame(rotatedsun_coord.data)
        if hasattr(from_coord, 'make_3d'):
            from_coord = from_coord.make_3d()
        int_frame = HeliographicStonyhurst(obstime=rotatedsun_coord.base.obstime)
        int_coord = from_coord.transform_to(int_frame)

        # Rotate the coordinate in HGS
        int_coord = int_coord._apply_diffrot(rotatedsun_coord.duration,
                                             rotatedsun_coord.rotation_model)

        # Transform from HGS
        return int_coord.transform_to(hgs_frame)  # obstime change handled here

    _rotatedsun_cache[framecls] = _RotatedSunFramecls
    return _RotatedSunFramecls


[docs]@add_common_docstring(**_variables_for_parse_time_docstring()) 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()
[docs] 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
# For Astropy 4.3+, we need to manually remove the `obstime` frame attribute from RotatedSunFrame if 'obstime' in RotatedSunFrame.frame_attributes: del RotatedSunFrame.frame_attributes['obstime']