def test_celestial_frame_to_wcs_extend(): class OffsetFrame: pass frame = OffsetFrame() with pytest.raises(ValueError): celestial_frame_to_wcs(frame) def identify_offset(frame, projection=None): if isinstance(frame, OffsetFrame): wcs = WCS(naxis=2) wcs.wcs.ctype = ['XOFFSET', 'YOFFSET'] return wcs with custom_frame_to_wcs_mappings(identify_offset): mywcs = celestial_frame_to_wcs(frame) assert tuple(mywcs.wcs.ctype) == ('XOFFSET', 'YOFFSET') # Check that things are back to normal after the context manager with pytest.raises(ValueError): celestial_frame_to_wcs(frame)
def test_celestial_frame_to_wcs(): # Import astropy.coordinates here to avoid circular imports from astropy.coordinates import ICRS, ITRS, FK5, FK4, FK4NoETerms, Galactic, BaseCoordinateFrame class FakeFrame(BaseCoordinateFrame): pass frame = FakeFrame() with pytest.raises(ValueError) as exc: celestial_frame_to_wcs(frame) assert exc.value.args[0] == ("Could not determine WCS corresponding to " "the specified coordinate frame.") frame = ICRS() mywcs = celestial_frame_to_wcs(frame) mywcs.wcs.set() assert tuple(mywcs.wcs.ctype) == ('RA---TAN', 'DEC--TAN') assert mywcs.wcs.radesys == 'ICRS' assert np.isnan(mywcs.wcs.equinox) assert mywcs.wcs.lonpole == 180 assert mywcs.wcs.latpole == 0 frame = FK5(equinox='J1987') mywcs = celestial_frame_to_wcs(frame) assert tuple(mywcs.wcs.ctype) == ('RA---TAN', 'DEC--TAN') assert mywcs.wcs.radesys == 'FK5' assert mywcs.wcs.equinox == 1987. frame = FK4(equinox='B1982') mywcs = celestial_frame_to_wcs(frame) assert tuple(mywcs.wcs.ctype) == ('RA---TAN', 'DEC--TAN') assert mywcs.wcs.radesys == 'FK4' assert mywcs.wcs.equinox == 1982. frame = FK4NoETerms(equinox='B1982') mywcs = celestial_frame_to_wcs(frame) assert tuple(mywcs.wcs.ctype) == ('RA---TAN', 'DEC--TAN') assert mywcs.wcs.radesys == 'FK4-NO-E' assert mywcs.wcs.equinox == 1982. frame = Galactic() mywcs = celestial_frame_to_wcs(frame) assert tuple(mywcs.wcs.ctype) == ('GLON-TAN', 'GLAT-TAN') assert mywcs.wcs.radesys == '' assert np.isnan(mywcs.wcs.equinox) frame = Galactic() mywcs = celestial_frame_to_wcs(frame, projection='CAR') assert tuple(mywcs.wcs.ctype) == ('GLON-CAR', 'GLAT-CAR') assert mywcs.wcs.radesys == '' assert np.isnan(mywcs.wcs.equinox) frame = Galactic() mywcs = celestial_frame_to_wcs(frame, projection='CAR') mywcs.wcs.crval = [100, -30] mywcs.wcs.set() assert_allclose((mywcs.wcs.lonpole, mywcs.wcs.latpole), (180, 60)) frame = ITRS(obstime=Time('2017-08-17T12:41:04.43')) mywcs = celestial_frame_to_wcs(frame, projection='CAR') assert tuple(mywcs.wcs.ctype) == ('TLON-CAR', 'TLAT-CAR') assert mywcs.wcs.radesys == 'ITRS' assert mywcs.wcs.dateobs == '2017-08-17T12:41:04.430' frame = ITRS() mywcs = celestial_frame_to_wcs(frame, projection='CAR') assert tuple(mywcs.wcs.ctype) == ('TLON-CAR', 'TLAT-CAR') assert mywcs.wcs.radesys == 'ITRS' assert mywcs.wcs.dateobs == Time('J2000').utc.fits
def find_optimal_celestial_wcs(input_data, frame=None, auto_rotate=False, projection='TAN', resolution=None, reference=None): """ Given one or more images, return an optimal WCS projection object and shape. This currently only works with 2-d images with celestial WCS. Parameters ---------- input_data : iterable One or more input data specifications to include in the calculation of the final WCS. This should be an iterable containing one entry for each specification, where a single data specification is one of: * The name of a FITS file * An `~astropy.io.fits.HDUList` object * An image HDU object such as a `~astropy.io.fits.PrimaryHDU`, `~astropy.io.fits.ImageHDU`, or `~astropy.io.fits.CompImageHDU` instance * A tuple where the first element is an Numpy array shape tuple the second element is either a `~astropy.wcs.WCS` or a `~astropy.io.fits.Header` object * A tuple where the first element is a `~numpy.ndarray` and the second element is either a `~astropy.wcs.WCS` or a `~astropy.io.fits.Header` object frame : str or `~astropy.coordinates.BaseCoordinateFrame` The coordinate system for the final image (defaults to the frame of the first image specified) auto_rotate : bool Whether to rotate the header to minimize the final image area (if `True`, requires shapely>=1.6 to be installed) projection : str Three-letter code for the WCS projection resolution : `~astropy.units.Quantity` The resolution of the final image. If not specified, this is the smallest resolution of the input images. reference : `~astropy.coordinates.SkyCoord` The reference coordinate for the final header. If not specified, this is determined automatically from the input images. Returns ------- wcs : :class:`~astropy.wcs.WCS` The optimal WCS determined from the input images. shape : tuple The optimal shape required to cover all the output. """ # TODO: support higher-dimensional datasets in future # TODO: take into account NaN values when determining the extent of the # final WCS if isinstance(frame, str): frame = frame_transform_graph.lookup_name(frame)() input_shapes = [parse_input_shape(shape) for shape in input_data] # We start off by looping over images, checking that they are indeed # celestial images, and building up a list of all corners and all reference # coordinates in celestial (ICRS) coordinates. corners = [] references = [] resolutions = [] for shape, wcs in input_shapes: if len(shape) != 2: raise ValueError( "Input data is not 2-dimensional (got shape {!r})".format( shape)) if wcs.naxis != 2: raise ValueError("Input WCS is not 2-dimensional") if not wcs.has_celestial: raise TypeError("WCS does not have celestial components") # Determine frame if it wasn't specified if frame is None: frame = wcs_to_celestial_frame(wcs) # Find pixel coordinates of corners. In future if we are worried about # significant distortions of the edges in the reprojection process we # could simply add arbitrary numbers of midpoints to this list. ny, nx = shape xc = np.array([-0.5, nx - 0.5, nx - 0.5, -0.5]) yc = np.array([-0.5, -0.5, ny - 0.5, ny - 0.5]) # We have to do .frame here to make sure that we get an ICRS object # without any 'hidden' attributes, otherwise the stacking below won't # work. TODO: check if we need to enable distortions here. corners.append(pixel_to_skycoord(xc, yc, wcs, origin=0).icrs.frame) # We now figure out the reference coordinate for the image in ICRS. The # easiest way to do this is actually to use pixel_to_skycoord with the # reference position in pixel coordinates. We have to set origin=1 # because crpix values are 1-based. xp, yp = wcs.wcs.crpix references.append(pixel_to_skycoord(xp, yp, wcs, origin=1).icrs.frame) # Find the pixel scale at the reference position - we take the minimum # since we are going to set up a header with 'square' pixels with the # smallest resolution specified. scales = proj_plane_pixel_scales(wcs) resolutions.append(np.min(np.abs(scales))) # We now stack the coordinates - however the ICRS class can't do this # so we have to use the high-level SkyCoord class. corners = SkyCoord(corners) references = SkyCoord(references) # If no reference coordinate has been passed in for the final header, we # determine the reference coordinate as the mean of all the reference # positions. This choice is as good as any and if the user really cares, # they can set it manually. if reference is None: reference = SkyCoord(references.data.mean(), frame=references.frame) # In any case, we need to convert the reference coordinate (either # specified or automatically determined) to the requested final frame. reference = reference.transform_to(frame) # Determine resolution if not specified if resolution is None: resolution = np.min(resolutions) * u.deg # Determine the resolution in degrees cdelt = resolution.to(u.deg).value # Construct WCS object centered on position wcs_final = celestial_frame_to_wcs(frame, projection=projection) rep = reference.represent_as('unitspherical') wcs_final.wcs.crval = rep.lon.degree, rep.lat.degree wcs_final.wcs.cdelt = -cdelt, cdelt # For now, set crpix to (1, 1) and we'll then figure out where all the # images fall in this projection, then we'll adjust crpix. wcs_final.wcs.crpix = (1, 1) # Find pixel coordinates of all corners in the final WCS projection. We use # origin=1 since we are trying to determine crpix values. xp, yp = skycoord_to_pixel(corners, wcs_final, origin=1) if auto_rotate: # Use shapely to represent the points and find the minimum rotated # rectangle from shapely.geometry import MultiPoint mp = MultiPoint(list(zip(xp, yp))) # The following returns a list of rectangle vertices - in fact there # are 5 coordinates because shapely represents it as a closed polygon # with the same first/last vertex. xr, yr = mp.minimum_rotated_rectangle.exterior.coords.xy xr, yr = xr[:4], yr[:4] # The order of the vertices is not guaranteed to be constant so we # take the vertices with the two smallest y values (which, for a # rectangle, guarantees that the vertices are neighboring) order = np.argsort(yr) x1, y1, x2, y2 = xr[order[0]], yr[order[0]], xr[order[1]], yr[order[1]] # Determine angle between two of the vertices. It doesn't matter which # ones they are, we just want to know how far from being straight the # rectangle is. angle = np.arctan2(y2 - y1, x2 - x1) # Determine the smallest angle that would cause the rectangle to be # lined up with the axes. angle = angle % (np.pi / 2) if angle > np.pi / 4: angle -= np.pi / 2 # Set rotation matrix (use PC instead of CROTA2 since PC is the # recommended approach) pc = np.array([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]) wcs_final.wcs.pc = pc # Recompute pixel coordinates (more accurate than simply rotating xp, yp) xp, yp = skycoord_to_pixel(corners, wcs_final, origin=1) # Find the full range of values xmin = xp.min() xmax = xp.max() ymin = yp.min() ymax = yp.max() # Update crpix so that the lower range falls on the bottom and left. We add # 0.5 because in the final image the bottom left corner should be at (0.5, # 0.5) not (1, 1). wcs_final.wcs.crpix = (1 - xmin) + 0.5, (1 - ymin) + 0.5 # Return the final image shape too naxis1 = int(round(xmax - xmin)) naxis2 = int(round(ymax - ymin)) return wcs_final, (naxis2, naxis1)
def create( cls, npix=None, binsz=0.5, proj="CAR", frame="icrs", refpix=None, axes=None, skydir=None, width=None, ): """Create a WCS geometry object. Pixelization of the map is set with ``binsz`` and one of either ``npix`` or ``width`` arguments. For maps with non-spatial dimensions a different pixelization can be used for each image plane by passing a list or array argument for any of the pixelization parameters. If both npix and width are None then an all-sky geometry will be created. Parameters ---------- npix : int or tuple or list Width of the map in pixels. A tuple will be interpreted as parameters for longitude and latitude axes. For maps with non-spatial dimensions, list input can be used to define a different map width in each image plane. This option supersedes width. width : float or tuple or list Width of the map in degrees. A tuple will be interpreted as parameters for longitude and latitude axes. For maps with non-spatial dimensions, list input can be used to define a different map width in each image plane. binsz : float or tuple or list Map pixel size in degrees. A tuple will be interpreted as parameters for longitude and latitude axes. For maps with non-spatial dimensions, list input can be used to define a different bin size in each image plane. skydir : tuple or `~astropy.coordinates.SkyCoord` Sky position of map center. Can be either a SkyCoord object or a tuple of longitude and latitude in deg in the coordinate system of the map. frame : {"icrs", "galactic"}, optional Coordinate system, either Galactic ("galactic") or Equatorial ("icrs"). axes : list List of non-spatial axes. proj : string, optional Any valid WCS projection type. Default is 'CAR' (cartesian). refpix : tuple Reference pixel of the projection. If None this will be set to the center of the map. Returns ------- geom : `~WcsGeom` A WCS geometry object. Examples -------- >>> from gammapy.maps import WcsGeom >>> from gammapy.maps import MapAxis >>> axis = MapAxis.from_bounds(0,1,2) >>> geom = WcsGeom.create(npix=(100,100), binsz=0.1) >>> geom = WcsGeom.create(npix=[100,200], binsz=[0.1,0.05], axes=[axis]) >>> geom = WcsGeom.create(width=[5.0,8.0], binsz=[0.1,0.05], axes=[axis]) >>> geom = WcsGeom.create(npix=([100,200],[100,200]), binsz=0.1, axes=[axis]) """ if skydir is None: crval = (0.0, 0.0) elif isinstance(skydir, tuple): crval = skydir elif isinstance(skydir, SkyCoord): xref, yref, frame = skycoord_to_lonlat(skydir, frame=frame) crval = (xref, yref) else: raise ValueError(f"Invalid type for skydir: {type(skydir)!r}") if width is not None: width = _check_width(width) shape = max([get_shape(t) for t in [npix, binsz, width]]) binsz = cast_to_shape(binsz, shape, float) # If both npix and width are None then create an all-sky geometry if npix is None and width is None: width = (360.0, 180.0) if npix is None: width = cast_to_shape(width, shape, float) npix = ( np.rint(width[0] / binsz[0]).astype(int), np.rint(width[1] / binsz[1]).astype(int), ) else: npix = cast_to_shape(npix, shape, int) if refpix is None: nxpix = int(npix[0].flat[0]) nypix = int(npix[1].flat[0]) refpix = ((nxpix + 1) / 2.0, (nypix + 1) / 2.0) # get frame class frame = SkyCoord(np.nan, np.nan, frame=frame, unit="deg").frame wcs = celestial_frame_to_wcs(frame, projection=proj) wcs.wcs.crpix = refpix wcs.wcs.crval = crval cdelt = (-binsz[0].flat[0], binsz[1].flat[0]) wcs.wcs.cdelt = cdelt wcs.array_shape = npix[0].flat[0], npix[1].flat[0] wcs.wcs.datfix() return cls(wcs, npix, cdelt=binsz, axes=axes)
def fit_wcs_from_points(xy, world_coords, proj_point='center', projection='TAN', sip_degree=None): # pragma: no cover """ Given two matching sets of coordinates on detector and sky, compute the WCS. Fits a WCS object to matched set of input detector and sky coordinates. Optionally, a SIP can be fit to account for geometric distortion. Returns an `~astropy.wcs.WCS` object with the best fit parameters for mapping between input pixel and sky coordinates. The projection type (default 'TAN') can passed in as a string, one of the valid three-letter projection codes - or as a WCS object with projection keywords already set. Note that if an input WCS has any non-polynomial distortion, this will be applied and reflected in the fit terms and coefficients. Passing in a WCS object in this way essentially allows it to be refit based on the matched input coordinates and projection point, but take care when using this option as non-projection related keywords in the input might cause unexpected behavior. Notes ------ - The fiducial point for the spherical projection can be set to 'center' to use the mean position of input sky coordinates, or as an `~astropy.coordinates.SkyCoord` object. - Units in all output WCS objects will always be in degrees. - If the coordinate frame differs between `~astropy.coordinates.SkyCoord` objects passed in for ``world_coords`` and ``proj_point``, the frame for ``world_coords`` will override as the frame for the output WCS. - If a WCS object is passed in to ``projection`` the CD/PC matrix will be used as an initial guess for the fit. If this is known to be significantly off and may throw off the fit, set to the identity matrix (for example, by doing wcs.wcs.pc = [(1., 0.,), (0., 1.)]) Parameters ---------- xy : tuple of two `numpy.ndarray` x & y pixel coordinates. world_coords : `~astropy.coordinates.SkyCoord` Skycoord object with world coordinates. proj_point : 'center' or ~astropy.coordinates.SkyCoord` Defaults to 'center', in which the geometric center of input world coordinates will be used as the projection point. To specify an exact point for the projection, a Skycoord object with a coordinate pair can be passed in. For consistency, the units and frame of these coordinates will be transformed to match ``world_coords`` if they don't. projection : str or `~astropy.wcs.WCS` Three letter projection code, of any of standard projections defined in the FITS WCS standard. Optionally, a WCS object with projection keywords set may be passed in. sip_degree : None or int If set to a non-zero integer value, will fit SIP of degree ``sip_degree`` to model geometric distortion. Defaults to None, meaning no distortion corrections will be fit. Returns ------- wcs : `~astropy.wcs.WCS` The best-fit WCS to the points given. """ from astropy.coordinates import SkyCoord # here to avoid circular import import astropy.units as u from astropy.wcs import Sip from scipy.optimize import least_squares xp, yp = xy try: lon, lat = world_coords.data.lon.deg, world_coords.data.lat.deg except AttributeError: unit_sph = world_coords.unit_spherical lon, lat = unit_sph.lon.deg, unit_sph.lat.deg # verify input if (proj_point != 'center') and (type(proj_point) != type(world_coords)): raise ValueError("proj_point must be set to 'center', or an" + "`~astropy.coordinates.SkyCoord` object with " + "a pair of points.") if proj_point != 'center': assert proj_point.size == 1 proj_codes = [ 'AZP', 'SZP', 'TAN', 'STG', 'SIN', 'ARC', 'ZEA', 'AIR', 'CYP', 'CEA', 'CAR', 'MER', 'SFL', 'PAR', 'MOL', 'AIT', 'COP', 'COE', 'COD', 'COO', 'BON', 'PCO', 'TSC', 'CSC', 'QSC', 'HPX', 'XPH' ] if type(projection) == str: if projection not in proj_codes: raise ValueError( "Must specify valid projection code from list of " + "supported types: ", ', '.join(proj_codes)) # empty wcs to fill in with fit values wcs = celestial_frame_to_wcs(frame=world_coords.frame, projection=projection) else: #if projection is not string, should be wcs object. use as template. wcs = copy.deepcopy(projection) wcs.cdelt = (1., 1.) # make sure cdelt is 1 wcs.sip = None # Change PC to CD, since cdelt will be set to 1 if wcs.wcs.has_pc(): wcs.wcs.cd = wcs.wcs.pc wcs.wcs.__delattr__('pc') if (type(sip_degree) != type(None)) and (type(sip_degree) != int): raise ValueError("sip_degree must be None, or integer.") # set pixel_shape to span of input points wcs.pixel_shape = (xp.max() + 1 - xp.min(), yp.max() + 1 - yp.min()) # determine CRVAL from input close = lambda l, p: p[np.argmin(np.abs(l))] if str(proj_point) == 'center': # use center of input points sc1 = SkyCoord(lon.min() * u.deg, lat.max() * u.deg) sc2 = SkyCoord(lon.max() * u.deg, lat.min() * u.deg) pa = sc1.position_angle(sc2) sep = sc1.separation(sc2) midpoint_sc = directional_offset_by(sc1, pa, sep / 2) wcs.wcs.crval = ((midpoint_sc.data.lon.deg, midpoint_sc.data.lat.deg)) wcs.wcs.crpix = ((xp.max() + xp.min()) / 2., (yp.max() + yp.min()) / 2.) elif proj_point is not None: # convert units, initial guess for crpix proj_point.transform_to(world_coords) wcs.wcs.crval = (proj_point.data.lon.deg, proj_point.data.lat.deg) wcs.wcs.crpix = (close(lon - wcs.wcs.crval[0], xp), close(lon - wcs.wcs.crval[1], yp)) # fit linear terms, assign to wcs # use (1, 0, 0, 1) as initial guess, in case input wcs was passed in # and cd terms are way off. p0 = np.concatenate([wcs.wcs.cd.flatten(), wcs.wcs.crpix.flatten()]) xpmin, xpmax, ypmin, ypmax = xp.min(), xp.max(), yp.min(), yp.max() if xpmin == xpmax: xpmin, xpmax = xpmin - 0.5, xpmax + 0.5 if ypmin == ypmax: ypmin, ypmax = ypmin - 0.5, ypmax + 0.5 fit = least_squares( _linear_wcs_fit, p0, args=(lon, lat, xp, yp, wcs), bounds=[[-np.inf, -np.inf, -np.inf, -np.inf, xpmin, ypmin], [np.inf, np.inf, np.inf, np.inf, xpmax, ypmax]]) wcs.wcs.crpix = np.array(fit.x[4:6]) wcs.wcs.cd = np.array(fit.x[0:4].reshape((2, 2))) # fit SIP, if specified. Only fit forward coefficients if sip_degree: degree = sip_degree if '-SIP' not in wcs.wcs.ctype[0]: wcs.wcs.ctype = [x + '-SIP' for x in wcs.wcs.ctype] coef_names = [ '{0}_{1}'.format(i, j) for i in range(degree + 1) for j in range(degree + 1) if (i + j) < (degree + 1) and (i + j) > 1 ] p0 = np.concatenate((np.array(wcs.wcs.crpix), wcs.wcs.cd.flatten(), np.zeros(2 * len(coef_names)))) fit = least_squares(_sip_fit, p0, args=(lon, lat, xp, yp, wcs, degree, coef_names)) coef_fit = (list(fit.x[6:6 + len(coef_names)]), list(fit.x[6 + len(coef_names):])) # put fit values in wcs wcs.wcs.cd = fit.x[2:6].reshape((2, 2)) wcs.wcs.crpix = fit.x[0:2] a_vals = np.zeros((degree + 1, degree + 1)) b_vals = np.zeros((degree + 1, degree + 1)) for coef_name in coef_names: a_vals[int(coef_name[0])][int(coef_name[2])] = coef_fit[0].pop(0) b_vals[int(coef_name[0])][int(coef_name[2])] = coef_fit[1].pop(0) wcs.sip = Sip(a_vals, b_vals, np.zeros((degree + 1, degree + 1)), np.zeros((degree + 1, degree + 1)), wcs.wcs.crpix) return wcs
def test_celestial_frame_to_wcs(): # Import astropy.coordinates here to avoid circular imports from astropy.coordinates import ICRS, ITRS, FK5, FK4, FK4NoETerms, Galactic, BaseCoordinateFrame class FakeFrame(BaseCoordinateFrame): pass frame = FakeFrame() with pytest.raises(ValueError) as exc: celestial_frame_to_wcs(frame) assert exc.value.args[0] == ("Could not determine WCS corresponding to " "the specified coordinate frame.") frame = ICRS() mywcs = celestial_frame_to_wcs(frame) mywcs.wcs.set() assert tuple(mywcs.wcs.ctype) == ('RA---TAN', 'DEC--TAN') assert mywcs.wcs.radesys == 'ICRS' assert np.isnan(mywcs.wcs.equinox) assert mywcs.wcs.lonpole == 180 assert mywcs.wcs.latpole == 0 frame = FK5(equinox='J1987') mywcs = celestial_frame_to_wcs(frame) assert tuple(mywcs.wcs.ctype) == ('RA---TAN', 'DEC--TAN') assert mywcs.wcs.radesys == 'FK5' assert mywcs.wcs.equinox == 1987. frame = FK4(equinox='B1982') mywcs = celestial_frame_to_wcs(frame) assert tuple(mywcs.wcs.ctype) == ('RA---TAN', 'DEC--TAN') assert mywcs.wcs.radesys == 'FK4' assert mywcs.wcs.equinox == 1982. frame = FK4NoETerms(equinox='B1982') mywcs = celestial_frame_to_wcs(frame) assert tuple(mywcs.wcs.ctype) == ('RA---TAN', 'DEC--TAN') assert mywcs.wcs.radesys == 'FK4-NO-E' assert mywcs.wcs.equinox == 1982. frame = Galactic() mywcs = celestial_frame_to_wcs(frame) assert tuple(mywcs.wcs.ctype) == ('GLON-TAN', 'GLAT-TAN') assert mywcs.wcs.radesys == '' assert np.isnan(mywcs.wcs.equinox) frame = Galactic() mywcs = celestial_frame_to_wcs(frame, projection='CAR') assert tuple(mywcs.wcs.ctype) == ('GLON-CAR', 'GLAT-CAR') assert mywcs.wcs.radesys == '' assert np.isnan(mywcs.wcs.equinox) frame = Galactic() mywcs = celestial_frame_to_wcs(frame, projection='CAR') mywcs.wcs.crval = [100, -30] mywcs.wcs.set() assert_allclose((mywcs.wcs.lonpole, mywcs.wcs.latpole), (180, 60)) frame = ITRS(obstime=Time('2017-08-17T12:41:04.43')) mywcs = celestial_frame_to_wcs(frame, projection='CAR') assert tuple(mywcs.wcs.ctype) == ('TLON-CAR', 'TLAT-CAR') assert mywcs.wcs.radesys == 'ITRS' assert mywcs.wcs.dateobs == '2017-08-17T12:41:04.430' frame = ITRS() mywcs = celestial_frame_to_wcs(frame, projection='CAR') assert tuple(mywcs.wcs.ctype) == ('TLON-CAR', 'TLAT-CAR') assert mywcs.wcs.radesys == 'ITRS' assert mywcs.wcs.dateobs == Time('J2000').utc.isot