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)
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
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)
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)
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)
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
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
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
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__)
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.")