def _rotate_polygon(lon, lat, lon0, lat0): """ Given a polygon with vertices defined by (lon, lat), rotate the polygon such that the North pole of the spherical coordinates is now at (lon0, lat0). Therefore, to end up with a polygon centered on (lon0, lat0), the polygon should initially be drawn around the North pole. """ # Create a representation object polygon = UnitSphericalRepresentation(lon=lon, lat=lat) # Determine rotation matrix to make it so that the circle is centered # on the correct longitude/latitude. m1 = rotation_matrix(-(0.5 * np.pi * u.radian - lat0), axis='y') m2 = rotation_matrix(-lon0, axis='z') transform_matrix = m2 * m1 # Apply 3D rotation polygon = polygon.to_cartesian() try: polygon = polygon.transform(transform_matrix) except: # TODO: remove once Astropy 1.1 is no longer supported polygon = _transform_cartesian(polygon, transform_matrix) polygon = UnitSphericalRepresentation.from_cartesian(polygon) return polygon.lon, polygon.lat
def _rotate_polygon(lon, lat, lon0, lat0): """ Given a polygon with vertices defined by (lon, lat), rotate the polygon such that the North pole of the spherical coordinates is now at (lon0, lat0). Therefore, to end up with a polygon centered on (lon0, lat0), the polygon should initially be drawn around the North pole. """ # Create a representation object polygon = UnitSphericalRepresentation(lon=lon, lat=lat) # Determine rotation matrix to make it so that the circle is centered # on the correct longitude/latitude. m1 = rotation_matrix(-(0.5 * np.pi * u.radian - lat0), axis='y') m2 = rotation_matrix(-lon0, axis='z') transform_matrix = m2 * m1 # Apply 3D rotation polygon = polygon.to_cartesian() try: polygon = polygon.transform(transform_matrix) except: # TODO: remove once Astropy 1.1 is no longer supported polygon = _transform_cartesian(polygon, transform_matrix) polygon = UnitSphericalRepresentation.from_cartesian(polygon) return polygon.lon, polygon.lat
def remove_camera_tilt( self, coord: UnitSphericalRepresentation, meridian_side: MeridianSide, ) -> UnitSphericalRepresentation: """Removes the effect of camera tilt. This method takes a coordinate in the mount-relative spherical coordinate system and transforms it to where the camera would be pointed in that coordinate system if the camera had no tilt. This transformation is one of the steps towards determining what encoder positions correspond to a particular camera position. Note that camera tilt results in regions near the poles of the coordinate system that are unreachable by the mount. Specifically, input coordinates with latitudes that are within the camera tilt angle of either pole are unreachable. When such a coordinate is encountered this method will return the pole location which is the nearest reachable position. The formula for the longitude of the reference point was found by noting that the reference point has latitude 0 and its separation from the input coordinate is equal to 90 degrees minus the camera tilt angle. With this information and the equation for the great circle distance between two points on a sphere, it is possible to solve for the difference in longitude between the input coordinate and the reference point. Args: coord: Coordinate in the mount-relative spherical coordinate system to be transformed, representing the direction the camera is pointed. meridian_side: Side of mount meridian. Returns: Coordinate corresponding to the position the camera would be pointed if it had no tilt, or the nearest position that is reachable. """ tilt = self.model_params.camera_tilt # Positions meeting these criteria are not reachable; return nearest reachable position if coord.lat >= 90 * u.deg - np.abs(tilt): return UnitSphericalRepresentation(coord.lon, 90 * u.deg) if coord.lat <= -(90 * u.deg - np.abs(tilt)): return UnitSphericalRepresentation(coord.lon, -90 * u.deg) # difference in longitude of coord and reference position longitude lon_diff = np.arccos(np.cos(90 * u.deg - tilt) / np.cos(coord.lat)) # the tilt is in the direction of this reference position reference_coord = SkyCoord( coord.lon + lon_diff if meridian_side == MeridianSide.WEST else coord.lon - lon_diff, 0 * u.deg, ) sc = SkyCoord(coord) return sc.directional_offset_by( position_angle=sc.position_angle(reference_coord), separation=-self.model_params.camera_tilt).represent_as( UnitSphericalRepresentation)
def encoders_to_spherical( self, encoder_positions: MountEncoderPositions ) -> Tuple[UnitSphericalRepresentation, MeridianSide]: """Convert from mount encoder positions to mount-relative spherical coordinates. The mount-relative spherical coordinate system is a defined such that the positive Z-axis, corresponding to a latitude angle of +90 degrees, is aligned with the physical pole of the mount, wherever it may be. Special cases: For an equatorial mount with the pole tipped up to be aligned with zenith the spherical coordinates returned by this function can be interpreted as topocentric azimuth and altitude. Alternatively, they can be interpreted as local hour angle and declination if used with an equatorial mount where the pole of the mount is perfectly aligned with the celestial pole after applying a 180-degree shift to the longitude coordinate. The details of the transformation applied here follow the conventions used by the Losmandy G11 mount's "physical" encoder position "pra" and "pdec". In particular, the default starting values of the encoders in the "counterweight down" startup position and the direction of axis motion corresponding to increasing encoder reading. This should work with other mounts as long as the "handedness" of the encoders is the same. The encoder zero point offsets in the mount model should take care of any difference in the startup positions. Args: encoder_positions: Set of mount encoder positions to be converted. Returns: Tuple where first element is the spherical coordinate, where longitude corresponds to mount axis 0 and latitude corresponds to mount axis 1, and the second element is the meridian side this position lies on. """ # apply encoder offsets encoder_positions = MountEncoderPositions( Longitude(encoder_positions[0] - self.model_params.axis_0_offset), Longitude(encoder_positions[1] - self.model_params.axis_1_offset), ) # This transformation is only correct if the mount axes are exactly orthogonal. If better # fidelity is required this could be replaced with a more general transformation that can # handle non-orthogonal axes. if encoder_positions[1] < 180 * u.deg: meridian_side = MeridianSide.EAST spherical_coord = UnitSphericalRepresentation( lon=(270 * u.deg - encoder_positions[0]), lat=(encoder_positions[1] - 90 * u.deg)) else: meridian_side = MeridianSide.WEST spherical_coord = UnitSphericalRepresentation( lon=(90 * u.deg - encoder_positions[0]), lat=(270 * u.deg - encoder_positions[1])) return self.apply_camera_tilt(spherical_coord, meridian_side), meridian_side
def observed_to_icrs(observed_coo, icrs_frame): # if the data are UnitSphericalRepresentation, we can skip the distance calculations is_unitspherical = (isinstance(observed_coo.data, UnitSphericalRepresentation) or observed_coo.cartesian.x.unit == u.one) usrepr = observed_coo.represent_as(UnitSphericalRepresentation) lon = usrepr.lon.to_value(u.radian) lat = usrepr.lat.to_value(u.radian) if isinstance(observed_coo, AltAz): # the 'A' indicates zen/az inputs coord_type = 'A' lat = PIOVER2 - lat else: coord_type = 'H' # first set up the astrometry context for ICRS<->CIRS at the observed_coo time astrom = erfa_astrom.get().apco(observed_coo) # Topocentric CIRS cirs_ra, cirs_dec = erfa.atoiq(coord_type, lon, lat, astrom) << u.radian if is_unitspherical: srepr = SphericalRepresentation(cirs_ra, cirs_dec, 1, copy=False) else: srepr = SphericalRepresentation(lon=cirs_ra, lat=cirs_dec, distance=observed_coo.distance, copy=False) # BCRS (Astrometric) direction to source bcrs_ra, bcrs_dec = aticq(srepr, astrom) << u.radian # Correct for parallax to get ICRS representation if is_unitspherical: icrs_srepr = UnitSphericalRepresentation(bcrs_ra, bcrs_dec, copy=False) else: icrs_srepr = SphericalRepresentation(lon=bcrs_ra, lat=bcrs_dec, distance=observed_coo.distance, copy=False) observer_icrs = CartesianRepresentation(astrom['eb'], unit=u.au, xyz_axis=-1, copy=False) newrepr = icrs_srepr.to_cartesian() + observer_icrs icrs_srepr = newrepr.represent_as(SphericalRepresentation) return icrs_frame.realize_frame(icrs_srepr)
def center_fov(lon, lat): # We assume here that any non-finite and non-sensible values have already # been filtered out lon = u.Quantity(lon, u.deg, copy=False) lat = u.Quantity(lat, u.deg, copy=False) unit_sph = UnitSphericalRepresentation(lon, lat, copy=False) cen = unit_sph.mean() sep = angular_separation(lon, lat, cen.lon, cen.lat).to(u.deg).value.max() return cen.lon.to(u.deg).value, cen.lat.to(u.deg).value, sep
def center_fov(lon, lat): # We need to filter out any non-finite values keep = np.isfinite(lon) & np.isfinite(lat) lon, lat = lon[keep], lat[keep] lon = u.Quantity(lon, u.deg, copy=False) lat = u.Quantity(lat, u.deg, copy=False) unit_sph = UnitSphericalRepresentation(lon, lat, copy=False) cen = unit_sph.mean() sep = angular_separation(lon, lat, cen.lon, cen.lat).to(u.deg).value.max() return cen.lon.to(u.deg).value, cen.lat.to(u.deg).value, sep
def cirs_to_observed(cirs_coo, observed_frame): if (np.any(observed_frame.location != cirs_coo.location) or np.any(cirs_coo.obstime != observed_frame.obstime)): cirs_coo = cirs_coo.transform_to(CIRS(obstime=observed_frame.obstime, location=observed_frame.location)) # if the data are UnitSphericalRepresentation, we can skip the distance calculations is_unitspherical = (isinstance(cirs_coo.data, UnitSphericalRepresentation) or cirs_coo.cartesian.x.unit == u.one) # We used to do "astrometric" corrections here, but these are no longer necesssary # CIRS has proper topocentric behaviour usrepr = cirs_coo.represent_as(UnitSphericalRepresentation) cirs_ra = usrepr.lon.to_value(u.radian) cirs_dec = usrepr.lat.to_value(u.radian) # first set up the astrometry context for CIRS<->observed astrom = erfa_astrom.get().apio(observed_frame) if isinstance(observed_frame, AltAz): lon, zen, _, _, _ = erfa.atioq(cirs_ra, cirs_dec, astrom) lat = PIOVER2 - zen else: _, _, lon, lat, _ = erfa.atioq(cirs_ra, cirs_dec, astrom) if is_unitspherical: rep = UnitSphericalRepresentation(lat=u.Quantity(lat, u.radian, copy=False), lon=u.Quantity(lon, u.radian, copy=False), copy=False) else: # since we've transformed to CIRS at the observatory location, just use CIRS distance rep = SphericalRepresentation(lat=u.Quantity(lat, u.radian, copy=False), lon=u.Quantity(lon, u.radian, copy=False), distance=cirs_coo.distance, copy=False) return observed_frame.realize_frame(rep)
def icrs_to_altaz(icrs_coo, altaz_frame): # if the data are UnitSphericalRepresentation, we can skip the distance calculations is_unitspherical = (isinstance(icrs_coo.data, UnitSphericalRepresentation) or icrs_coo.cartesian.x.unit == u.one) # first set up the astrometry context for ICRS<->AltAz astrom = erfa_astrom.get().apco(altaz_frame) # correct for parallax to find BCRS direction from observer (as in erfa.pmpx) if is_unitspherical: srepr = icrs_coo.spherical else: observer_icrs = CartesianRepresentation(astrom['eb'], unit=u.au, xyz_axis=-1, copy=False) srepr = (icrs_coo.cartesian - observer_icrs).represent_as(SphericalRepresentation) # convert to topocentric CIRS cirs_ra, cirs_dec = atciqz(srepr, astrom) # now perform AltAz conversion az, zen, ha, odec, ora = erfa.atioq(cirs_ra, cirs_dec, astrom) alt = PIOVER2 - zen if is_unitspherical: aa_srepr = UnitSphericalRepresentation(az << u.radian, alt << u.radian, copy=False) else: aa_srepr = SphericalRepresentation(az << u.radian, alt << u.radian, srepr.distance, copy=False) return altaz_frame.realize_frame(aa_srepr)
def golden_spiral_grid(size): """Generate a grid of points on the surface of the unit sphere using the Fibonacci or Golden Spiral method. .. seealso:: `Evenly distributing points on a sphere <https://stackoverflow.com/questions/9600801/evenly-distributing-n-points-on-a-sphere>`_ Parameters ---------- size : int The number of points to generate. Returns ------- rep : `~astropy.coordinates.UnitSphericalRepresentation` The grid of points. """ golden_r = (1 + 5**0.5) / 2 grid = np.arange(0, size, dtype=float) + 0.5 lon = 2*np.pi / golden_r * grid * u.rad lat = np.arcsin(1 - 2 * grid / size) * u.rad return UnitSphericalRepresentation(lon, lat)
def cirs_to_icrs(cirs_coo, icrs_frame): srepr = cirs_coo.represent_as(SphericalRepresentation) cirs_ra = srepr.lon.to_value(u.radian) cirs_dec = srepr.lat.to_value(u.radian) # set up the astrometry context for ICRS<->cirs and then convert to # astrometric coordinate direction astrom = erfa_astrom.get().apci(cirs_coo) i_ra, i_dec = aticq(cirs_ra, cirs_dec, astrom) if cirs_coo.data.get_name() == 'unitspherical' or cirs_coo.data.to_cartesian().x.unit == u.one: # if no distance, just use the coordinate direction to yield the # infinite-distance/no parallax answer newrep = UnitSphericalRepresentation(lat=u.Quantity(i_dec, u.radian, copy=False), lon=u.Quantity(i_ra, u.radian, copy=False), copy=False) else: # When there is a distance, apply the parallax/offset to the SSB as the # last step - ensures round-tripping with the icrs_to_cirs transform # the distance in intermedrep is *not* a real distance as it does not # include the offset back to the SSB intermedrep = SphericalRepresentation(lat=u.Quantity(i_dec, u.radian, copy=False), lon=u.Quantity(i_ra, u.radian, copy=False), distance=srepr.distance, copy=False) astrom_eb = CartesianRepresentation(astrom['eb'], unit=u.au, xyz_axis=-1, copy=False) newrep = intermedrep + astrom_eb return icrs_frame.realize_frame(newrep)
def tip_axis(coord: UnitSphericalRepresentation, axis_lon: Angle, rot_angle: Angle) -> UnitSphericalRepresentation: """Perform a rotation about an axis perpendicular to the Z-axis. The purpose of this rotation is to move the pole of the coordinate system from one place to another. For example, transforming from a coordinate system where the pole is aligned with the physical pole of a mount to a topocentric coordinate system where the pole is aligned with zenith. Note that this is a true rotation, and the same rotation is applied to all coordinates in the originating coordinate system equally. It is not equivalent to using SkyCoord directional_offset_by() with a fixed position angle and separation since the direction and magnitude of the offset depend on the value of coord. Args: coord: Coordinate to be transformed. axis_lon: Longitude angle of the axis of rotation. rot_angle: Angle of rotation. Returns: Coordinate after transformation. """ rot = rotation_matrix(rot_angle, axis=SkyCoord(axis_lon, 0 * u.deg).represent_as('cartesian').xyz) coord_cart = coord.represent_as(CartesianRepresentation) coord_rot_cart = coord_cart.transform(rot) return coord_rot_cart.represent_as(UnitSphericalRepresentation)
def boundaries_skycoord(self, healpix_index, step): """ Return the celestial coordinates of the edges of HEALPix pixels This returns the celestial coordinates of points along the edge of each HEALPIX pixel. The number of points returned for each pixel is ``4 * step``, so setting ``step`` to 1 returns just the corners. This method requires that a celestial frame was specified when initializing HEALPix. If you don't know or need the celestial frame, you can instead use :meth:`~astropy_healpix.HEALPix.boundaries_lonlat`. Parameters ---------- healpix_index : `~numpy.ndarray` 1-D array of HEALPix pixels step : int The number of steps to take along each edge. Returns ------- skycoord : :class:`~astropy.coordinates.SkyCoord` The celestial coordinates of the HEALPix pixel boundaries """ if self.frame is None: raise NoFrameError("boundaries_skycoord") lon, lat = self.boundaries_lonlat(healpix_index, step) representation = UnitSphericalRepresentation(lon, lat, copy=False) return SkyCoord(self.frame.realize_frame(representation))
def healpix_to_skycoord(self, healpix_index, dx=None, dy=None): """ Convert HEALPix indices (optionally with offsets) to celestial coordinates. Note that this method requires that a celestial frame was specified when initializing HEALPix. If you don't know or need the celestial frame, you can instead use :meth:`~astropy_healpix.HEALPix.healpix_to_lonlat`. Parameters ---------- healpix_index : `~numpy.ndarray` 1-D array of HEALPix indices dx, dy : `~numpy.ndarray`, optional 1-D arrays of offsets inside the HEALPix pixel, which must be in the range [0:1] (0.5 is the center of the HEALPix pixels). If not specified, the position at the center of the pixel is used. Returns ------- coord : :class:`~astropy.coordinates.SkyCoord` The resulting celestial coordinates """ if self.frame is None: raise NoFrameError("healpix_to_skycoord") lon, lat = self.healpix_to_lonlat(healpix_index, dx=dx, dy=dy) representation = UnitSphericalRepresentation(lon, lat, copy=False) return SkyCoord(self.frame.realize_frame(representation))
def test_horizons_consistency_with_precision(): """ A test to compare at high precision against output of JPL horizons. Tests ephemerides, and conversions from ICRS to GCRS to TETE. We are aiming for better than 2 milli-arcsecond precision. We use the Moon since it is nearby, and moves fast in the sky so we are testing for parallax, proper handling of light deflection and aberration. """ # JPL Horizon values for 2020_04_06 00:00 to 23:00 in 1 hour steps # JPL Horizons has a known offset (frame bias) of 51.02 mas in RA. We correct that here ra_apparent_horizons = [ 170.167332531, 170.560688674, 170.923834838, 171.271663481, 171.620188972, 171.985340827, 172.381766539, 172.821772139, 173.314502650, 173.865422398, 174.476108551, 175.144332386, 175.864375310, 176.627519827, 177.422655853, 178.236955730, 179.056584831, 179.867427392, 180.655815385, 181.409252074, 182.117113814, 182.771311578, 183.366872837, 183.902395443 ] * u.deg + 51.02376467 * u.mas dec_apparent_horizons = [ 10.269112037, 10.058820647, 9.837152044, 9.603724551, 9.358956528, 9.104012390, 8.840674927, 8.571162442, 8.297917326, 8.023394488, 7.749873882, 7.479312991, 7.213246666, 6.952732614, 6.698336823, 6.450150213, 6.207828142, 5.970645962, 5.737565957, 5.507313851, 5.278462034, 5.049521497, 4.819038911, 4.585696512 ] * u.deg with solar_system_ephemeris.set('de430'): loc = EarthLocation.from_geodetic(-67.787260*u.deg, -22.959748*u.deg, 5186*u.m) times = Time('2020-04-06 00:00') + np.arange(0, 24, 1)*u.hour astropy = get_body('moon', times, loc) apparent_frame = TETE(obstime=times, location=loc) astropy = astropy.transform_to(apparent_frame) usrepr = UnitSphericalRepresentation(ra_apparent_horizons, dec_apparent_horizons) horizons = apparent_frame.realize_frame(usrepr) assert_quantity_allclose(astropy.separation(horizons), 0*u.mas, atol=1.5*u.mas)
def boundaries(nside, pix, step=1, nest=False): """Drop-in replacement for healpy `~healpy.boundaries`.""" pix = np.asarray(pix) if pix.ndim > 1: # For consistency with healpy we only support scalars or 1D arrays raise ValueError("Array has to be one dimensional") lon, lat = boundaries_lonlat(pix, step, nside, order='nested' if nest else 'ring') rep_sph = UnitSphericalRepresentation(lon, lat) rep_car = rep_sph.to_cartesian().xyz.value.swapaxes(0, 1) if rep_car.shape[0] == 1: return rep_car[0] else: return rep_car
def icrs_to_cirs(icrs_coo, cirs_frame): # first set up the astrometry context for ICRS<->CIRS astrom = erfa_astrom.get().apci(cirs_frame) if icrs_coo.data.get_name() == 'unitspherical' or icrs_coo.data.to_cartesian().x.unit == u.one: # if no distance, just do the infinite-distance/no parallax calculation usrepr = icrs_coo.represent_as(UnitSphericalRepresentation) i_ra = usrepr.lon.to_value(u.radian) i_dec = usrepr.lat.to_value(u.radian) cirs_ra, cirs_dec = atciqz(i_ra, i_dec, astrom) newrep = UnitSphericalRepresentation(lat=u.Quantity(cirs_dec, u.radian, copy=False), lon=u.Quantity(cirs_ra, u.radian, copy=False), copy=False) else: # When there is a distance, we first offset for parallax to get the # astrometric coordinate direction and *then* run the ERFA transform for # no parallax/PM. This ensures reversibility and is more sensible for # inside solar system objects astrom_eb = CartesianRepresentation(astrom['eb'], unit=u.au, xyz_axis=-1, copy=False) newcart = icrs_coo.cartesian - astrom_eb srepr = newcart.represent_as(SphericalRepresentation) i_ra = srepr.lon.to_value(u.radian) i_dec = srepr.lat.to_value(u.radian) cirs_ra, cirs_dec = atciqz(i_ra, i_dec, astrom) newrep = SphericalRepresentation(lat=u.Quantity(cirs_dec, u.radian, copy=False), lon=u.Quantity(cirs_ra, u.radian, copy=False), distance=srepr.distance, copy=False) return cirs_frame.realize_frame(newrep)
def icrs_to_gcrs(icrs_coo, gcrs_frame): # first set up the astrometry context for ICRS<->GCRS. There are a few steps... # get the position and velocity arrays for the observatory. Need to # have xyz in last dimension, and pos/vel in one-but-last. # (Note could use np.stack once our minimum numpy version is >=1.10.) obs_pv = pav2pv( gcrs_frame.obsgeoloc.get_xyz(xyz_axis=-1).to_value(u.m), gcrs_frame.obsgeovel.get_xyz(xyz_axis=-1).to_value(u.m / u.s)) # find the position and velocity of earth jd1, jd2 = get_jd12(gcrs_frame.obstime, 'tdb') earth_pv, earth_heliocentric = prepare_earth_position_vel( gcrs_frame.obstime) # get astrometry context object, astrom. astrom = erfa.apcs(jd1, jd2, obs_pv, earth_pv, earth_heliocentric) if icrs_coo.data.get_name( ) == 'unitspherical' or icrs_coo.data.to_cartesian().x.unit == u.one: # if no distance, just do the infinite-distance/no parallax calculation usrepr = icrs_coo.represent_as(UnitSphericalRepresentation) i_ra = usrepr.lon.to_value(u.radian) i_dec = usrepr.lat.to_value(u.radian) gcrs_ra, gcrs_dec = atciqz(i_ra, i_dec, astrom) newrep = UnitSphericalRepresentation(lat=u.Quantity(gcrs_dec, u.radian, copy=False), lon=u.Quantity(gcrs_ra, u.radian, copy=False), copy=False) else: # When there is a distance, we first offset for parallax to get the # BCRS coordinate direction and *then* run the ERFA transform for no # parallax/PM. This ensures reversibility and is more sensible for # inside solar system objects astrom_eb = CartesianRepresentation(astrom['eb'], unit=u.au, xyz_axis=-1, copy=False) newcart = icrs_coo.cartesian - astrom_eb srepr = newcart.represent_as(SphericalRepresentation) i_ra = srepr.lon.to_value(u.radian) i_dec = srepr.lat.to_value(u.radian) gcrs_ra, gcrs_dec = atciqz(i_ra, i_dec, astrom) newrep = SphericalRepresentation(lat=u.Quantity(gcrs_dec, u.radian, copy=False), lon=u.Quantity(gcrs_ra, u.radian, copy=False), distance=srepr.distance, copy=False) return gcrs_frame.realize_frame(newrep)
def gcrs_to_hcrs(gcrs_coo, hcrs_frame): if np.any(gcrs_coo.obstime != hcrs_frame.obstime): # if they GCRS obstime and HCRS obstime are not the same, we first # have to move to a GCRS where they are. frameattrs = gcrs_coo.get_frame_attr_names() frameattrs['obstime'] = hcrs_frame.obstime gcrs_coo = gcrs_coo.transform_to(GCRS(**frameattrs)) srepr = gcrs_coo.represent_as(SphericalRepresentation) gcrs_ra = srepr.lon.to_value(u.radian) gcrs_dec = srepr.lat.to_value(u.radian) # set up the astrometry context for ICRS<->GCRS and then convert to ICRS # coordinate direction obs_pv = pav2pv( gcrs_coo.obsgeoloc.get_xyz(xyz_axis=-1).to_value(u.m), gcrs_coo.obsgeovel.get_xyz(xyz_axis=-1).to_value(u.m / u.s)) jd1, jd2 = get_jd12(hcrs_frame.obstime, 'tdb') earth_pv, earth_heliocentric = prepare_earth_position_vel(gcrs_coo.obstime) astrom = erfa.apcs(jd1, jd2, obs_pv, earth_pv, earth_heliocentric) i_ra, i_dec = aticq(gcrs_ra, gcrs_dec, astrom) # convert to Quantity objects i_ra = u.Quantity(i_ra, u.radian, copy=False) i_dec = u.Quantity(i_dec, u.radian, copy=False) if gcrs_coo.data.get_name( ) == 'unitspherical' or gcrs_coo.data.to_cartesian().x.unit == u.one: # if no distance, just use the coordinate direction to yield the # infinite-distance/no parallax answer newrep = UnitSphericalRepresentation(lat=i_dec, lon=i_ra, copy=False) else: # When there is a distance, apply the parallax/offset to the # Heliocentre as the last step to ensure round-tripping with the # hcrs_to_gcrs transform # Note that the distance in intermedrep is *not* a real distance as it # does not include the offset back to the Heliocentre intermedrep = SphericalRepresentation(lat=i_dec, lon=i_ra, distance=srepr.distance, copy=False) # astrom['eh'] and astrom['em'] contain Sun to observer unit vector, # and distance, respectively. Shapes are (X) and (X,3), where (X) is the # shape resulting from broadcasting the shape of the times object # against the shape of the pv array. # broadcast em to eh and scale eh eh = astrom['eh'] * astrom['em'][..., np.newaxis] eh = CartesianRepresentation(eh, unit=u.au, xyz_axis=-1, copy=False) newrep = intermedrep.to_cartesian() + eh return hcrs_frame.realize_frame(newrep)
def cirs_to_altaz(cirs_coo, altaz_frame): if np.any(cirs_coo.obstime != altaz_frame.obstime): # the only frame attribute for the current CIRS is the obstime, but this # would need to be updated if a future change allowed specifying an # Earth location algorithm or something cirs_coo = cirs_coo.transform_to(CIRS(obstime=altaz_frame.obstime)) # we use the same obstime everywhere now that we know they're the same obstime = cirs_coo.obstime # if the data are UnitSphericalRepresentation, we can skip the distance calculations is_unitspherical = (isinstance(cirs_coo.data, UnitSphericalRepresentation) or cirs_coo.cartesian.x.unit == u.one) if is_unitspherical: usrepr = cirs_coo.represent_as(UnitSphericalRepresentation) cirs_ra = usrepr.lon.to_value(u.radian) cirs_dec = usrepr.lat.to_value(u.radian) else: # compute an "astrometric" ra/dec -i.e., the direction of the # displacement vector from the observer to the target in CIRS loccirs = altaz_frame.location.get_itrs( cirs_coo.obstime).transform_to(cirs_coo) diffrepr = ( cirs_coo.cartesian - loccirs.cartesian).represent_as(UnitSphericalRepresentation) cirs_ra = diffrepr.lon.to_value(u.radian) cirs_dec = diffrepr.lat.to_value(u.radian) # first set up the astrometry context for CIRS<->AltAz astrom = erfa_astrom.get().apio13(altaz_frame) az, zen, _, _, _ = erfa.atioq(cirs_ra, cirs_dec, astrom) if is_unitspherical: rep = UnitSphericalRepresentation(lat=u.Quantity(PIOVER2 - zen, u.radian, copy=False), lon=u.Quantity(az, u.radian, copy=False), copy=False) else: # now we get the distance as the cartesian distance from the earth # location to the coordinate location locitrs = altaz_frame.location.get_itrs(obstime) distance = locitrs.separation_3d(cirs_coo) rep = SphericalRepresentation(lat=u.Quantity(PIOVER2 - zen, u.radian, copy=False), lon=u.Quantity(az, u.radian, copy=False), distance=distance, copy=False) return altaz_frame.realize_frame(rep)
def gcrs_to_icrs(gcrs_coo, icrs_frame): srepr = gcrs_coo.represent_as(SphericalRepresentation) gcrs_ra = srepr.lon.to_value(u.radian) gcrs_dec = srepr.lat.to_value(u.radian) # set up the astrometry context for ICRS<->GCRS and then convert to BCRS # coordinate direction obs_pv = pav2pv( gcrs_coo.obsgeoloc.get_xyz(xyz_axis=-1).to_value(u.m), gcrs_coo.obsgeovel.get_xyz(xyz_axis=-1).to_value(u.m / u.s)) jd1, jd2 = get_jd12(gcrs_coo.obstime, 'tdb') earth_pv, earth_heliocentric = prepare_earth_position_vel(gcrs_coo.obstime) astrom = erfa.apcs(jd1, jd2, obs_pv, earth_pv, earth_heliocentric) i_ra, i_dec = aticq(gcrs_ra, gcrs_dec, astrom) if gcrs_coo.data.get_name( ) == 'unitspherical' or gcrs_coo.data.to_cartesian().x.unit == u.one: # if no distance, just use the coordinate direction to yield the # infinite-distance/no parallax answer newrep = UnitSphericalRepresentation(lat=u.Quantity(i_dec, u.radian, copy=False), lon=u.Quantity(i_ra, u.radian, copy=False), copy=False) else: # When there is a distance, apply the parallax/offset to the SSB as the # last step - ensures round-tripping with the icrs_to_gcrs transform # the distance in intermedrep is *not* a real distance as it does not # include the offset back to the SSB intermedrep = SphericalRepresentation(lat=u.Quantity(i_dec, u.radian, copy=False), lon=u.Quantity(i_ra, u.radian, copy=False), distance=srepr.distance, copy=False) astrom_eb = CartesianRepresentation(astrom['eb'], unit=u.au, xyz_axis=-1, copy=False) newrep = intermedrep + astrom_eb return icrs_frame.realize_frame(newrep)
def icrs_to_cirs(icrs_coo, cirs_frame): # first set up the astrometry context for ICRS<->CIRS jd1, jd2 = get_jd12(cirs_frame.obstime, 'tt') x, y, s = get_cip(jd1, jd2) earth_pv, earth_heliocentric = prepare_earth_position_vel( cirs_frame.obstime) # erfa.apci requests TDB but TT can be used instead of TDB without any significant impact on accuracy astrom = erfa.apci(jd1, jd2, earth_pv, earth_heliocentric, x, y, s) if icrs_coo.data.get_name( ) == 'unitspherical' or icrs_coo.data.to_cartesian().x.unit == u.one: # if no distance, just do the infinite-distance/no parallax calculation usrepr = icrs_coo.represent_as(UnitSphericalRepresentation) i_ra = usrepr.lon.to_value(u.radian) i_dec = usrepr.lat.to_value(u.radian) cirs_ra, cirs_dec = atciqz(i_ra, i_dec, astrom) newrep = UnitSphericalRepresentation(lat=u.Quantity(cirs_dec, u.radian, copy=False), lon=u.Quantity(cirs_ra, u.radian, copy=False), copy=False) else: # When there is a distance, we first offset for parallax to get the # astrometric coordinate direction and *then* run the ERFA transform for # no parallax/PM. This ensures reversibility and is more sensible for # inside solar system objects astrom_eb = CartesianRepresentation(astrom['eb'], unit=u.au, xyz_axis=-1, copy=False) newcart = icrs_coo.cartesian - astrom_eb srepr = newcart.represent_as(SphericalRepresentation) i_ra = srepr.lon.to_value(u.radian) i_dec = srepr.lat.to_value(u.radian) cirs_ra, cirs_dec = atciqz(i_ra, i_dec, astrom) newrep = SphericalRepresentation(lat=u.Quantity(cirs_dec, u.radian, copy=False), lon=u.Quantity(cirs_ra, u.radian, copy=False), distance=srepr.distance, copy=False) return cirs_frame.realize_frame(newrep)
def cirs_to_icrs(cirs_coo, icrs_frame): srepr = cirs_coo.represent_as(SphericalRepresentation) cirs_ra = srepr.lon.to_value(u.radian) cirs_dec = srepr.lat.to_value(u.radian) # set up the astrometry context for ICRS<->cirs and then convert to # astrometric coordinate direction jd1, jd2 = get_jd12(cirs_coo.obstime, 'tt') x, y, s = get_cip(jd1, jd2) earth_pv, earth_heliocentric = prepare_earth_position_vel(cirs_coo.obstime) # erfa.apci requests TDB but TT can be used instead of TDB without any significant impact on accuracy astrom = erfa.apci(jd1, jd2, earth_pv, earth_heliocentric, x, y, s) i_ra, i_dec = aticq(cirs_ra, cirs_dec, astrom) if cirs_coo.data.get_name( ) == 'unitspherical' or cirs_coo.data.to_cartesian().x.unit == u.one: # if no distance, just use the coordinate direction to yield the # infinite-distance/no parallax answer newrep = UnitSphericalRepresentation(lat=u.Quantity(i_dec, u.radian, copy=False), lon=u.Quantity(i_ra, u.radian, copy=False), copy=False) else: # When there is a distance, apply the parallax/offset to the SSB as the # last step - ensures round-tripping with the icrs_to_cirs transform # the distance in intermedrep is *not* a real distance as it does not # include the offset back to the SSB intermedrep = SphericalRepresentation(lat=u.Quantity(i_dec, u.radian, copy=False), lon=u.Quantity(i_ra, u.radian, copy=False), distance=srepr.distance, copy=False) astrom_eb = CartesianRepresentation(astrom['eb'], unit=u.au, xyz_axis=-1, copy=False) newrep = intermedrep + astrom_eb return icrs_frame.realize_frame(newrep)
def uniform_spherical_random_surface(size=1): """Generate a random sampling of points on the surface of the unit sphere. Parameters ---------- size : int The number of points to generate. Returns ------- rep : `~astropy.coordinates.UnitSphericalRepresentation` The random points. """ rng = np.random # can maybe switch to this being an input later - see #11628 lon = rng.uniform(0, 2*np.pi, size) * u.rad lat = np.arcsin(rng.uniform(-1, 1, size=size)) * u.rad return UnitSphericalRepresentation(lon, lat)
def ang2vec(theta, phi, lonlat=False): """Drop-in replacement for healpy `~healpy.pixelfunc.ang2vec`.""" lon, lat = _healpy_to_lonlat(theta, phi, lonlat=lonlat) rep_sph = UnitSphericalRepresentation(lon, lat) rep_car = rep_sph.represent_as(CartesianRepresentation) return rep_car.xyz.value
def _sun_earth_icrf(time): """ Return the Sun-Earth vector for ICRF-based frames. """ sun_pos_icrs = get_body_barycentric('sun', time) earth_pos_icrs = get_body_barycentric('earth', time) return earth_pos_icrs - sun_pos_icrs # The Sun's north pole is oriented RA=286.13 deg, dec=63.87 deg in ICRS, and thus HCRS as well # (See Archinal et al. 2011, # "Report of the IAU Working Group on Cartographic Coordinates and Rotational Elements: 2009") # The orientation of the north pole in ICRS/HCRS is assumed to be constant in time _SOLAR_NORTH_POLE_HCRS = UnitSphericalRepresentation(lon=constants.get('alpha_0'), lat=constants.get('delta_0')) # Calculate the rotation matrix to de-tilt the Sun's rotation axis to be parallel to the Z axis _SUN_DETILT_MATRIX = _rotation_matrix_reprs_to_reprs(_SOLAR_NORTH_POLE_HCRS, CartesianRepresentation(0, 0, 1)) def _affine_params_hcrs_to_hgs(hcrs_time, hgs_time): """ Return the affine parameters (matrix and offset) from HCRS to HGS HGS shares the same origin (the Sun) as HCRS, but has its Z axis aligned with the Sun's rotation axis and its X axis aligned with the projection of the Sun-Earth vector onto the Sun's equatorial plane (i.e., the component of the Sun-Earth vector perpendicular to the Z axis). Thus, the transformation matrix is the product of the matrix to align the Z axis (by de-tilting
def test_frame_api(): from astropy.coordinates.representation import SphericalRepresentation, \ UnitSphericalRepresentation from astropy.coordinates.builtin_frames import ICRS, FK5 # <--------------------Reference Frame/"Low-level" classes---------------------> # The low-level classes have a dual role: they act as specifiers of coordinate # frames and they *may* also contain data as one of the representation objects, # in which case they are the actual coordinate objects themselves. # They can always accept a representation as a first argument icrs = ICRS(UnitSphericalRepresentation(lon=8 * u.hour, lat=5 * u.deg)) # which is stored as the `data` attribute assert icrs.data.lat == 5 * u.deg assert icrs.data.lon == 8 * u.hourangle # Frames that require additional information like equinoxs or obstimes get them # as keyword parameters to the frame constructor. Where sensible, defaults are # used. E.g., FK5 is almost always J2000 equinox fk5 = FK5(UnitSphericalRepresentation(lon=8 * u.hour, lat=5 * u.deg)) J2000 = time.Time('J2000') fk5_2000 = FK5(UnitSphericalRepresentation(lon=8 * u.hour, lat=5 * u.deg), equinox=J2000) assert fk5.equinox == fk5_2000.equinox # the information required to specify the frame is immutable J2001 = time.Time('J2001') with pytest.raises(AttributeError): fk5.equinox = J2001 # Similar for the representation data. with pytest.raises(AttributeError): fk5.data = UnitSphericalRepresentation(lon=8 * u.hour, lat=5 * u.deg) # There is also a class-level attribute that lists the attributes needed to # identify the frame. These include attributes like `equinox` shown above. assert all(nm in ('equinox', 'obstime') for nm in fk5.get_frame_attr_names()) # the result of `get_frame_attr_names` is called for particularly in the # high-level class (discussed below) to allow round-tripping between various # frames. It is also part of the public API for other similar developer / # advanced users' use. # The actual position information is accessed via the representation objects assert_allclose(icrs.represent_as(SphericalRepresentation).lat, 5 * u.deg) # shorthand for the above assert_allclose(icrs.spherical.lat, 5 * u.deg) assert icrs.cartesian.z.value > 0 # Many frames have a "default" representation, the one in which they are # conventionally described, often with a special name for some of the # coordinates. E.g., most equatorial coordinate systems are spherical with RA and # Dec. This works simply as a shorthand for the longer form above assert_allclose(icrs.dec, 5 * u.deg) assert_allclose(fk5.ra, 8 * u.hourangle) assert icrs.representation_type == SphericalRepresentation # low-level classes can also be initialized with names valid for that representation # and frame: icrs_2 = ICRS(ra=8 * u.hour, dec=5 * u.deg, distance=1 * u.kpc) assert_allclose(icrs.ra, icrs_2.ra) # and these are taken as the default if keywords are not given: # icrs_nokwarg = ICRS(8*u.hour, 5*u.deg, distance=1*u.kpc) # assert icrs_nokwarg.ra == icrs_2.ra and icrs_nokwarg.dec == icrs_2.dec # they also are capable of computing on-sky or 3d separations from each other, # which will be a direct port of the existing methods: coo1 = ICRS(ra=0 * u.hour, dec=0 * u.deg) coo2 = ICRS(ra=0 * u.hour, dec=1 * u.deg) # `separation` is the on-sky separation assert coo1.separation(coo2).degree == 1.0 # while `separation_3d` includes the 3D distance information coo3 = ICRS(ra=0 * u.hour, dec=0 * u.deg, distance=1 * u.kpc) coo4 = ICRS(ra=0 * u.hour, dec=0 * u.deg, distance=2 * u.kpc) assert coo3.separation_3d(coo4).kpc == 1.0 # The next example fails because `coo1` and `coo2` don't have distances with pytest.raises(ValueError): assert coo1.separation_3d(coo2).kpc == 1.0
def cirs_to_altaz(cirs_coo, altaz_frame): if np.any(cirs_coo.obstime != altaz_frame.obstime): # the only frame attribute for the current CIRS is the obstime, but this # would need to be updated if a future change allowed specifying an # Earth location algorithm or something cirs_coo = cirs_coo.transform_to(CIRS(obstime=altaz_frame.obstime)) # we use the same obstime everywhere now that we know they're the same obstime = cirs_coo.obstime # if the data are UnitSphericalRepresentation, we can skip the distance calculations is_unitspherical = (isinstance(cirs_coo.data, UnitSphericalRepresentation) or cirs_coo.cartesian.x.unit == u.one) if is_unitspherical: usrepr = cirs_coo.represent_as(UnitSphericalRepresentation) cirs_ra = usrepr.lon.to_value(u.radian) cirs_dec = usrepr.lat.to_value(u.radian) else: # compute an "astrometric" ra/dec -i.e., the direction of the # displacement vector from the observer to the target in CIRS loccirs = altaz_frame.location.get_itrs( cirs_coo.obstime).transform_to(cirs_coo) diffrepr = ( cirs_coo.cartesian - loccirs.cartesian).represent_as(UnitSphericalRepresentation) cirs_ra = diffrepr.lon.to_value(u.radian) cirs_dec = diffrepr.lat.to_value(u.radian) lon, lat, height = altaz_frame.location.to_geodetic('WGS84') xp, yp = get_polar_motion(obstime) # first set up the astrometry context for CIRS<->AltAz jd1, jd2 = get_jd12(obstime, 'utc') astrom = erfa.apio13( jd1, jd2, get_dut1utc(obstime), lon.to_value(u.radian), lat.to_value(u.radian), height.to_value(u.m), xp, yp, # polar motion # all below are already in correct units because they are QuantityFrameAttribues altaz_frame.pressure.value, altaz_frame.temperature.value, altaz_frame.relative_humidity.value, altaz_frame.obswl.value) az, zen, _, _, _ = erfa.atioq(cirs_ra, cirs_dec, astrom) if is_unitspherical: rep = UnitSphericalRepresentation(lat=u.Quantity(PIOVER2 - zen, u.radian, copy=False), lon=u.Quantity(az, u.radian, copy=False), copy=False) else: # now we get the distance as the cartesian distance from the earth # location to the coordinate location locitrs = altaz_frame.location.get_itrs(obstime) distance = locitrs.separation_3d(cirs_coo) rep = SphericalRepresentation(lat=u.Quantity(PIOVER2 - zen, u.radian, copy=False), lon=u.Quantity(az, u.radian, copy=False), distance=distance, copy=False) return altaz_frame.realize_frame(rep)
B = end_representation.to_cartesian() rotation_axis = A.cross(B) rotation_angle = -np.arccos(A.dot(B) / (A.norm() * B.norm())) # negation is required # This line works around some input/output quirks of Astropy's rotation_matrix() matrix = np.array( rotation_matrix(rotation_angle, rotation_axis.xyz.value.tolist())) return matrix # The Sun's north pole is oriented RA=286.13 deg, dec=63.87 deg in ICRS, and thus HCRS as well # (See Archinal et al. 2011, # "Report of the IAU Working Group on Cartographic Coordinates and Rotational Elements: 2009") # The orientation of the north pole in ICRS/HCRS is assumed to be constant in time _SOLAR_NORTH_POLE_HCRS = UnitSphericalRepresentation(lon=286.13 * u.deg, lat=63.87 * u.deg) # Calculate the rotation matrix to de-tilt the Sun's rotation axis to be parallel to the Z axis _SUN_DETILT_MATRIX = _make_rotation_matrix_from_reprs( _SOLAR_NORTH_POLE_HCRS, CartesianRepresentation(0, 0, 1)) @frame_transform_graph.transform(DynamicMatrixTransform, HCRS, HeliographicStonyhurst) def hcrs_to_hgs(hcrscoord, hgsframe): """ Convert from HCRS to Heliographic Stonyhurst (HGS). HGS shares the same origin (the Sun) as HCRS, but has its Z axis aligned with the Sun's rotation axis and its X axis aligned with the projection of the Sun-Earth vector onto the Sun's equatorial plane (i.e., the component of the Sun-Earth vector perpendicular to the Z axis).
def test_representations_api(): from astropy.coordinates.representation import SphericalRepresentation, \ UnitSphericalRepresentation, PhysicsSphericalRepresentation, \ CartesianRepresentation from astropy.coordinates import Angle, Longitude, Latitude, Distance # <-----------------Classes for representation of coordinate data--------------> # These classes inherit from a common base class and internally contain Quantity # objects, which are arrays (although they may act as scalars, like numpy's # length-0 "arrays") # They can be initialized with a variety of ways that make intuitive sense. # Distance is optional. UnitSphericalRepresentation(lon=8 * u.hour, lat=5 * u.deg) UnitSphericalRepresentation(lon=8 * u.hourangle, lat=5 * u.deg) SphericalRepresentation(lon=8 * u.hourangle, lat=5 * u.deg, distance=10 * u.kpc) # In the initial implementation, the lat/lon/distance arguments to the # initializer must be in order. A *possible* future change will be to allow # smarter guessing of the order. E.g. `Latitude` and `Longitude` objects can be # given in any order. UnitSphericalRepresentation(Longitude(8, u.hour), Latitude(5, u.deg)) SphericalRepresentation(Longitude(8, u.hour), Latitude(5, u.deg), Distance(10, u.kpc)) # Arrays of any of the inputs are fine UnitSphericalRepresentation(lon=[8, 9] * u.hourangle, lat=[5, 6] * u.deg) # Default is to copy arrays, but optionally, it can be a reference UnitSphericalRepresentation(lon=[8, 9] * u.hourangle, lat=[5, 6] * u.deg, copy=False) # strings are parsed by `Latitude` and `Longitude` constructors, so no need to # implement parsing in the Representation classes UnitSphericalRepresentation(lon=Angle('2h6m3.3s'), lat=Angle('0.1rad')) # Or, you can give `Quantity`s with keywords, and they will be internally # converted to Angle/Distance c1 = SphericalRepresentation(lon=8 * u.hourangle, lat=5 * u.deg, distance=10 * u.kpc) # Can also give another representation object with the `reprobj` keyword. c2 = SphericalRepresentation.from_representation(c1) # distance, lat, and lon typically will just match in shape SphericalRepresentation(lon=[8, 9] * u.hourangle, lat=[5, 6] * u.deg, distance=[10, 11] * u.kpc) # if the inputs are not the same, if possible they will be broadcast following # numpy's standard broadcasting rules. c2 = SphericalRepresentation(lon=[8, 9] * u.hourangle, lat=[5, 6] * u.deg, distance=10 * u.kpc) assert len(c2.distance) == 2 # when they can't be broadcast, it is a ValueError (same as Numpy) with pytest.raises(ValueError): c2 = UnitSphericalRepresentation(lon=[8, 9, 10] * u.hourangle, lat=[5, 6] * u.deg) # It's also possible to pass in scalar quantity lists with mixed units. These # are converted to array quantities following the same rule as `Quantity`: all # elements are converted to match the first element's units. c2 = UnitSphericalRepresentation( lon=Angle([8 * u.hourangle, 135 * u.deg]), lat=Angle([5 * u.deg, (6 * np.pi / 180) * u.rad])) assert c2.lat.unit == u.deg and c2.lon.unit == u.hourangle npt.assert_almost_equal(c2.lon[1].value, 9) # The Quantity initializer itself can also be used to force the unit even if the # first element doesn't have the right unit lon = u.Quantity([120 * u.deg, 135 * u.deg], u.hourangle) lat = u.Quantity([(5 * np.pi / 180) * u.rad, 0.4 * u.hourangle], u.deg) c2 = UnitSphericalRepresentation(lon, lat) # regardless of how input, the `lat` and `lon` come out as angle/distance assert isinstance(c1.lat, Angle) assert isinstance( c1.lat, Latitude) # `Latitude` is an `~astropy.coordinates.Angle` subclass assert isinstance(c1.distance, Distance) # but they are read-only, as representations are immutable once created with pytest.raises(AttributeError): c1.lat = Latitude(5, u.deg) # Note that it is still possible to modify the array in-place, but this is not # sanctioned by the API, as this would prevent things like caching. c2.lat[:] = [0] * u.deg # possible, but NOT SUPPORTED # To address the fact that there are various other conventions for how spherical # coordinates are defined, other conventions can be included as new classes. # Later there may be other conventions that we implement - for now just the # physics convention, as it is one of the most common cases. _ = PhysicsSphericalRepresentation(phi=120 * u.deg, theta=85 * u.deg, r=3 * u.kpc) # first dimension must be length-3 if a lone `Quantity` is passed in. c1 = CartesianRepresentation(np.random.randn(3, 100) * u.kpc) assert c1.xyz.shape[0] == 3 assert c1.xyz.unit == u.kpc assert c1.x.shape[0] == 100 assert c1.y.shape[0] == 100 assert c1.z.shape[0] == 100 # can also give each as separate keywords CartesianRepresentation(x=np.random.randn(100) * u.kpc, y=np.random.randn(100) * u.kpc, z=np.random.randn(100) * u.kpc) # if the units don't match but are all distances, they will automatically be # converted to match `x` xarr, yarr, zarr = np.random.randn(3, 100) c1 = CartesianRepresentation(x=xarr * u.kpc, y=yarr * u.kpc, z=zarr * u.kpc) c2 = CartesianRepresentation(x=xarr * u.kpc, y=yarr * u.kpc, z=zarr * u.pc) assert c1.xyz.unit == c2.xyz.unit == u.kpc assert_allclose((c1.z / 1000) - c2.z, 0 * u.kpc, atol=1e-10 * u.kpc) # representations convert into other representations via `represent_as` srep = SphericalRepresentation(lon=90 * u.deg, lat=0 * u.deg, distance=1 * u.pc) crep = srep.represent_as(CartesianRepresentation) assert_allclose(crep.x, 0 * u.pc, atol=1e-10 * u.pc) assert_allclose(crep.y, 1 * u.pc, atol=1e-10 * u.pc) assert_allclose(crep.z, 0 * u.pc, atol=1e-10 * u.pc)
def test_transform_api(): from astropy.coordinates.representation import UnitSphericalRepresentation from astropy.coordinates.builtin_frames import ICRS, FK5 from astropy.coordinates.baseframe import frame_transform_graph, BaseCoordinateFrame from astropy.coordinates.transformations import DynamicMatrixTransform # <------------------------Transformations-------------------------------------> # Transformation functionality is the key to the whole scheme: they transform # low-level classes from one frame to another. # (used below but defined above in the API) fk5 = FK5(ra=8 * u.hour, dec=5 * u.deg) # If no data (or `None`) is given, the class acts as a specifier of a frame, but # without any stored data. J2001 = time.Time('J2001') fk5_J2001_frame = FK5(equinox=J2001) # if they do not have data, the string instead is the frame specification assert repr(fk5_J2001_frame) == "<FK5 Frame (equinox=J2001.000)>" # Note that, although a frame object is immutable and can't have data added, it # can be used to create a new object that does have data by giving the # `realize_frame` method a representation: srep = UnitSphericalRepresentation(lon=8 * u.hour, lat=5 * u.deg) fk5_j2001_with_data = fk5_J2001_frame.realize_frame(srep) assert fk5_j2001_with_data.data is not None # Now `fk5_j2001_with_data` is in the same frame as `fk5_J2001_frame`, but it # is an actual low-level coordinate, rather than a frame without data. # These frames are primarily useful for specifying what a coordinate should be # transformed *into*, as they are used by the `transform_to` method # E.g., this snippet precesses the point to the new equinox newfk5 = fk5.transform_to(fk5_J2001_frame) assert newfk5.equinox == J2001 # classes can also be given to `transform_to`, which then uses the defaults for # the frame information: samefk5 = fk5.transform_to(FK5) # `fk5` was initialized using default `obstime` and `equinox`, so: assert_allclose(samefk5.ra, fk5.ra, atol=1e-10 * u.deg) assert_allclose(samefk5.dec, fk5.dec, atol=1e-10 * u.deg) # transforming to a new frame necessarily loses framespec information if that # information is not applicable to the new frame. This means transforms are not # always round-trippable: fk5_2 = FK5(ra=8 * u.hour, dec=5 * u.deg, equinox=J2001) ic_trans = fk5_2.transform_to(ICRS) # `ic_trans` does not have an `equinox`, so now when we transform back to FK5, # it's a *different* RA and Dec fk5_trans = ic_trans.transform_to(FK5) assert not allclose(fk5_2.ra, fk5_trans.ra, rtol=0, atol=1e-10 * u.deg) # But if you explicitly give the right equinox, all is fine fk5_trans_2 = fk5_2.transform_to(FK5(equinox=J2001)) assert_allclose(fk5_2.ra, fk5_trans_2.ra, rtol=0, atol=1e-10 * u.deg) # Trying to transforming a frame with no data is of course an error: with pytest.raises(ValueError): FK5(equinox=J2001).transform_to(ICRS) # To actually define a new transformation, the same scheme as in the # 0.2/0.3 coordinates framework can be re-used - a graph of transform functions # connecting various coordinate classes together. The main changes are: # 1) The transform functions now get the frame object they are transforming the # current data into. # 2) Frames with additional information need to have a way to transform between # objects of the same class, but with different framespecinfo values # An example transform function: class SomeNewSystem(BaseCoordinateFrame): pass @frame_transform_graph.transform(DynamicMatrixTransform, SomeNewSystem, FK5) def new_to_fk5(newobj, fk5frame): _ = newobj.obstime _ = fk5frame.equinox # ... build a *cartesian* transform matrix using `eq` that transforms from # the `newobj` frame as observed at `ot` to FK5 an equinox `eq` matrix = np.eye(3) return matrix