Esempio n. 1
0
 class TestAttributes(metaclass=OrderedDescriptorContainer):
     attr_none = Attribute()
     attr_2 = Attribute(default=2)
     attr_3_attr2 = Attribute(default=3, secondary_attribute='attr_2')
     attr_none_attr2 = Attribute(default=None, secondary_attribute='attr_2')
     attr_none_nonexist = Attribute(default=None,
                                    secondary_attribute='nonexist')
Esempio n. 2
0
 class MyFK4(FK4):
     # equinox inherited from FK4, obstime overridden, and newattr is new
     obstime = TimeAttribute(default=_EQUINOX_B1980)
     newattr = Attribute(default='newattr')
Esempio n. 3
0
 class TestFrame(BaseCoordinateFrame):
     attrtest = Attribute(default=Spam())
Esempio n. 4
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
Esempio n. 5
0
 class BorkedFrame(BaseCoordinateFrame):
     ra = Attribute(default=150)
     dec = Attribute(default=150)
Esempio n. 6
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.")