Source code for mpdaf.obj.coords

"""
Copyright (c) 2010-2018 CNRS / Centre de Recherche Astrophysique de Lyon
Copyright (c) 2012-2016 Laure Piqueras <laure.piqueras@univ-lyon1.fr>
Copyright (c) 2014-2019 Simon Conseil <simon.conseil@univ-lyon1.fr>
Copyright (c)      2016 Martin Shepherd <martin.shepherd@univ-lyon1.fr>
Copyright (c)      2019 Yannick Roehlly <yannick.roehlly@univ-lyon1.fr>

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors
   may be used to endorse or promote products derived from this software
   without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

import astropy.units as u
import astropy.wcs as pywcs
import logging
import numbers
import numpy as np

from astropy.io import fits

from .objs import UnitArray
from ..tools import fix_unit_read

__all__ = ('deg2sexa', 'sexa2deg', 'deg2hms', 'hms2deg', 'deg2dms', 'dms2deg',
           'image_angle_from_cd', 'axis_increments_from_cd',
           'WCS', 'WaveCoord', 'determine_refframe')


[docs]def deg2sexa(x): """Transform equatorial coordinates from degrees to sexagesimal strings. Parameters ---------- x : float array Either a single coordinate in a 1D array like ``[dec, ra]``, or a 2D array of multiple (dec,ra) coordinates, ordered like ``[[dec1,ra1], [dec2,ra2], ...]``. All coordinates must be in degrees. Returns ------- out : array of strings The array of dec,ra coordinates as sexagesimal strings, stored in an array of the same dimensions as the input array. Declination values are written like degrees:minutes:seconds. Right-ascension values are written like hours:minutes:seconds. """ x = np.asarray(x) ndim = x.ndim x = np.atleast_2d(x) # FIXME: can be replaced with SkyCoord.to_string('hmsdms', sep=':') result = [] for i in range(np.shape(x)[0]): ra = deg2hms(x[i][1]) dec = deg2dms(x[i][0]) result.append(np.array([dec, ra])) return np.array(result) if ndim > 1 else result[0]
[docs]def sexa2deg(x): """Transform equatorial coordinates from sexagesimal strings to degrees. Parameters ---------- x : string array Either a single pair of coordinate strings in a 1D array like ``[dec, ra]``, or a 2D array of multiple (dec,ra) coordinate strings, ordered like ``[[dec1,ra1], [dec2,ra2], ...]``. In each coordinate pair, the declination string should be written like degrees:minutes:seconds, and the right-ascension string should be written like hours:minutes:seconds. Returns ------- out : array of numbers The array of ra,dec coordinates in degrees, returned in an array of the same dimensions as the input array. """ x = np.asarray(x) ndim = x.ndim x = np.atleast_2d(x) result = [] for i in range(np.shape(x)[0]): ra = hms2deg(x[i][1]) dec = dms2deg(x[i][0]) result.append(np.array([dec, ra])) return np.array(result) if ndim > 1 else result[0]
[docs]def deg2hms(x): """Transform degrees to *hours:minutes:seconds* strings. Parameters ---------- x : float The degree value to be written as a sexagesimal string. Returns ------- out : str The input angle written as a sexagesimal string, in the form, hours:minutes:seconds. """ from astropy.coordinates import Angle ac = Angle(x, unit='degree') hms = ac.to_string(unit='hour', sep=':', pad=True) return str(hms)
[docs]def hms2deg(x): """Transform *hours:minutes:seconds* strings to degrees. Parameters ---------- x : str The input angle, written in the form, hours:minutes:seconds Returns ------- out : float The angle as a number of degrees. """ from astropy.coordinates import Angle ac = Angle(x, unit='hour') deg = float(ac.to_string(unit='degree', decimal=True)) return deg
[docs]def deg2dms(x): """Transform degrees to *degrees:arcminutes:arcseconds* strings. Parameters ---------- x : float The degree value to be converted. Returns ------- out : str The input angle as a string, written as degrees:minutes:seconds. """ from astropy.coordinates import Angle ac = Angle(x, unit='degree') dms = ac.to_string(unit='degree', sep=':', pad=True) return str(dms)
[docs]def dms2deg(x): """Transform *degrees:arcminutes:arcseconds* strings to degrees. Parameters ---------- x : str The input angle written in the form, degrees:arcminutes:arcseconds Returns ------- out : float The input angle as a number of degrees. """ from astropy.coordinates import Angle ac = Angle(x, unit='degree') deg = float(ac.to_string(unit='degree', decimal=True)) return deg
def _wcs_from_header(hdr, naxis=None): if 'CD1_1' in hdr and 'CDELT3' in hdr and 'CD3_3' not in hdr: hdr['CD3_3'] = hdr['CDELT3'] if 'PC1_1' in hdr and 'CDELT3' in hdr and 'PC3_3' not in hdr: hdr['PC3_3'] = 1 # WCS object from data header return pywcs.WCS(hdr, naxis=naxis)
[docs]def image_angle_from_cd(cd, unit=u.deg): """Return the rotation angle of the image. Defined such that a rotation angle of zero aligns north along the positive Y axis, and a positive rotation angle rotates north away from the Y axis, in the sense of a rotation from north to east. Note that the rotation angle is defined in a flat map-projection of the sky. It is what would be seen if the pixels of the image were drawn with their pixel widths scaled by the angular pixel increments returned by the axis_increments_from_cd() method. If the CD matrix was derived from the archaic CROTA and CDELT FITS keywords, then the angle returned by this function is equal to CROTA. Parameters ---------- cd : numpy.ndarray The 2x2 coordinate conversion matrix, with its elements ordered for multiplying a column vector in FITS (x,y) axis order. unit : `astropy.units.Unit` The unit to give the returned angle (degrees by default). Returns ------- out : float The angle between celestial north and the Y axis of the image, in the sense of an eastward rotation of celestial north from the Y-axis. The angle is returned in the range -180 to 180 degrees (or the equivalent for the specified unit). """ # Get the angular increments of pixels along the Y and X axes step = axis_increments_from_cd(cd) # Get the determinant of the coordinate transformation matrix. cddet = np.linalg.det(cd) # The angle of a northward vector from the origin can be calculated by # first using the inverse of the CD matrix to calculate the equivalent # vector in pixel indexes, then calculating the angle of this vector to the # Y axis of the image array. north = np.arctan2(-cd[0, 1] * step[1] / cddet, cd[0, 0] * step[0] / cddet) # Return the angle with the specified units. return (north * u.rad).to(unit).value
[docs]def axis_increments_from_cd(cd): """Return the angular increments of pixels along the Y and X axes of an image array whose coordinates are described by a specified FITS CD matrix. In MPDAF, images are a regular grid of square pixels on a flat projection of the celestial sphere. This function returns the angular width and height of these pixels on the sky, with signs that indicate whether the angle increases or decreases as one steps along the corresponding array axis. To keep plots consistent, regardless of the rotation angle of the image on the sky, the returned height is always positive, but the returned width is negative if a plot of the image with pixel 0,0 at the bottom left would place east anticlockwise of north, and positive otherwise. Parameters ---------- cd : numpy.ndarray The 2x2 coordinate conversion matrix, with its elements ordered for multiplying a column vector in FITS (x,y) axis order. unit : `astropy.units.Unit` The angular units of the returned values. Returns ------- out : numpy.ndarray (dy,dx). These are the angular increments of pixels along the Y and X axes of the image, returned with the same units as the contents of the CD matrix. """ # The pixel dimensions are determined as follows. First note # that the coordinate transformation matrix looks as follows: # # |r| = |M[0,0], M[0,1]| |col - get_crpix1()| # |d| |M[1,0], M[1,1]| |row - get_crpix2()| # # In this equation [col,row] are the indexes of a pixel in the # image array and [r,d] are the coordinates of this pixel on a # flat map-projection of the sky. If the celestial coordinates # of the observation are right ascension and declination, then d # is parallel to declination, and r is perpendicular to this, # pointing east. When the column index is incremented by 1, the # above equation indicates that r and d change by: # # col_dr = M[0,0] col_dd = M[1,0] # # The length of the vector from (0,0) to (col_dr,col_dd) is # the angular width of pixels along the X axis. # # dx = sqrt(M[0,0]^2 + M[1,1]^2) # # Similarly, when the row index is incremented by 1, r and d # change by: # # row_dr = M[0,1] row_dd = M[1,1] # # The length of the vector from (0,0) to (row_dr,row_dd) is # the angular width of pixels along the Y axis. # # dy = sqrt(M[0,1]^2 + M[1,1]^2) # # Calculate the width and height of the pixels as described above. dx = np.sqrt(cd[0, 0]**2 + cd[1, 0]**2) dy = np.sqrt(cd[0, 1]**2 + cd[1, 1]**2) # To decide what sign to give the step in X, we need to know # whether east is clockwise or anticlockwise of north when the # image is plotted with pixel 0,0 at the bottom left of the # plot. The angle of a northward vector from the origin can be # calculated by first using the inverse of the CD matrix to # calculate the equivalent vector in pixel indexes, then # calculating the angle of this vector to the Y axis of the # image array. Start by calculating the determinant of the CD # matrix. cddet = np.linalg.det(cd) # Calculate the rotation angle of a unit northward vector # clockwise of the Y axis. north = np.arctan2(-cd[0, 1] / cddet, cd[0, 0] / cddet) # Calculate the rotation angle of a unit eastward vector # clockwise of the Y axis. east = np.arctan2(cd[1, 1] / cddet, -cd[1, 0] / cddet) # Wrap the difference east-north into the range -pi to pi radians. from astropy.coordinates import Angle delta = Angle((east - north) * u.rad).wrap_at(np.pi * u.rad).value # If east is anticlockwise of north make the X-axis pixel increment # negative. if delta < 0.0: dx *= -1.0 # Return the axis increments in python array-indexing order. return np.array([dy, dx])
[docs]def determine_refframe(phdr): """Determine the reference frame and equinox in standard FITS WCS terms. Parameters ---------- phdr : `astropy.io.fits.Header` Primary Header of an observation Returns ------- out : str, float Reference frame ('ICRS', 'FK5', 'FK4') and equinox """ # MUSE files should have RADECSYS='FK5' and EQUINOX=2000.0 equinox = phdr.get('EQUINOX') radesys = phdr.get('RADESYS') or phdr.get('RADECSYS') if radesys == 'FK5' and equinox == 2000.0: return 'FK5', equinox elif radesys: return radesys, None elif equinox is not None: return 'FK4' if equinox < 1984. else 'FK5', equinox else: return None, None
[docs]class WCS: """The WCS class manages the world coordinates of the spatial axes of MPDAF images, using the pywcs package. Note that MPDAF images are stored in python arrays that are indexed in [y,x] axis order. In general the axes of these arrays are not along celestial axes such as right-ascension and declination. They are cartesian axes of a flat map projection of the sky around the observation center, and they may be rotated away from the celestial axes. When their rotation angle is zero, the Y axis is parallel to the declination axis. However the X axis is only along the right ascension axis for observations at zero declination. Pixels in MPDAF images are not generally square on the sky. To scale index offsets in the image to angular distances in the map projection, the Y-axis and X-axis index offsets must be scaled by different numbers. These numbers can be obtained by calling the get_axis_increments() method, which returns the angular increment per pixel increment along the Y and X axes of the array. The Y-axis increment is always positive, but the X-axis increment is negative if east is anti-clockwise of north when the X-axis pixels are plotted from left to right, The rotation angle of the map projection, relative to the sky, can be obtained by calling the get_rot() method. This returns the angle between celestial north and the Y axis of the image, in the sense of an eastward rotation of celestial north from the Y-axis. When the linearized coordinates of the map projection are insufficient, the celestial coordinates of one or more pixels can be queried by calling the pix2sky() method, which returns coordinates in the [dec,ra] axis order. In the other direction, the [y,x] indexes of the pixel of a given celestial coordinate can be obtained by calling the sky2pix() method. Parameters ---------- hdr : astropy.fits.CardList A FITS header. If the hdr parameter is not None, the WCS object is created from the data header, and the remaining parameters are ignored. crpix : float or (float,float) The FITS array indexes of the reference pixel of the image, given in the order (y,x). Note that the first pixel of the FITS image is [1,1], whereas in the python image array it is [0,0]. Thus to place the reference pixel at [ry,rx] in the python image array would require crpix=(ry+1,rx+1). If both crpix and shape are None, then crpix is given the value (1.0,1.0) and the reference position is at index [0,0] in the python array of the image. If crpix is None and shape is not None, then crpix is set to (shape + 1.0)/2.0, which places the reference point at the center of the image. crval : float or (float,float) The celestial coordinates of the reference pixel (ref_dec,ref_ra). If this parameter is not provided, then (0.0,0.0) is substituted. cdelt : float or (float,float) If the hdr and cd parameters are both None, then this argument can be used to specify the pixel increments along the Y and X axes of the image, respectively. If this parameter is not provided, (1.0,1.0) is substituted. Note that it is conventional for cdelt[1] to be negative, such that east is plotted towards the left when the image rotation angle is zero. deg : bool If True, then cdelt and crval are in decimal degrees (CTYPE1='RA---TAN',CTYPE2='DEC--TAN',CUNIT1=CUNIT2='deg'). If False (the default), the celestial coordinates are linear (CTYPE1=CTYPE2='LINEAR'). rot : float If the hdr and cd parameters are both None, then this argument can be used to specify a value for the rotation angle of the image. This is the angle between celestial north and the Y axis of the image, in the sense of an eastward rotation of celestial north from the Y-axis. Along with the cdelt parameter, the rot parameter is used to construct a FITS CD rotation matrix. This is done as described in equation 189 of Calabretta, M. R., and Greisen, E. W, Astronomy & Astrophysics, 395, 1077-1122, 2002, where it serves as the value of the CROTA term. shape : integer or (integer,integer) The dimensions of the image axes (optional). The dimensions are given in python order (ny,nx). cd : numpy.ndarray This parameter can optionally be used to specify the FITS CD rotation matrix. By default this parameter is None. However if a matrix is provided and hdr is None, then it is used instead of cdelt and rot, which are then ignored. The matrix should be ordered like cd = numpy.array([[CD1_1, CD1_2], [CD2_1, CD2_2]]), where CDj_i are the names of the corresponding FITS keywords. Attributes ---------- wcs : astropy.wcs.WCS The underlying object that performs most of the world coordinate conversions. """ def __init__(self, hdr=None, crpix=None, crval=(1.0, 1.0), cdelt=(1.0, 1.0), deg=False, rot=0, shape=None, cd=None, frame=None, equinox=None): self._logger = logging.getLogger(__name__) if hdr is not None: # Initialize the WCS object from a FITS header? self.wcs = _wcs_from_header(hdr, naxis=2) if frame is not None: self.wcs.wcs.radesys = frame if equinox is not None: self.wcs.wcs.equinox = equinox else: # Initialize the WCS object # Define a function that checks that 2D attributes are # either a 2-element tuple of float or int, or a float or # int scalar which is converted to a 2-element tuple. def check_attrs(val, types=numbers.Number): if isinstance(val, types): return (val, val) elif len(val) > 2: raise ValueError('dimension > 2') else: return val crval = check_attrs(crval) cdelt = check_attrs(cdelt) if crpix is not None: crpix = check_attrs(crpix) if shape is not None: shape = check_attrs(shape, types=int) self.wcs = pywcs.WCS(naxis=2) # Get the FITS array indexes of the reference pixel. Beware that # FITS array indexes are offset by 1 from python array indexes. if crpix is not None: self.wcs.wcs.crpix = np.array([crpix[1], crpix[0]]) elif shape is None: self.wcs.wcs.crpix = np.array([1.0, 1.0]) else: self.wcs.wcs.crpix = (np.array([shape[1], shape[0]]) + 1) / 2. # Get the world coordinate value of reference pixel. self.wcs.wcs.crval = np.array([crval[1], crval[0]]) if deg: # in decimal degree self.wcs.wcs.ctype = ['RA---TAN', 'DEC--TAN'] self.wcs.wcs.cunit = ['deg', 'deg'] if frame is not None: self.wcs.wcs.radesys = frame if equinox is not None: self.wcs.wcs.equinox = equinox else: # in pixel or arcsec self.wcs.wcs.ctype = ['LINEAR', 'LINEAR'] self.wcs.wcs.cunit = ['pixel', 'pixel'] if cd is not None and cd.shape[0] == 2 and cd.shape[1] == 2: # If a CD rotation matrix has been provided by the caller, # install it. self.set_cd(cd) else: # If no CD matrix was provided, construct one from the cdelt # and rot parameters, following the official prescription given # by equation 189 of Calabretta, M. R., and Greisen, E. W, # Astronomy & Astrophysics, 395, 1077-1122, 2002. rho = np.deg2rad(rot) sin_rho = np.sin(rho) cos_rho = np.cos(rho) self.set_cd(np.array([ [cdelt[1] * cos_rho, -cdelt[0] * sin_rho], [cdelt[1] * sin_rho, cdelt[0] * cos_rho]])) # Update the wcs object to accommodate the new value of # the CD matrix. self.wcs.wcs.set() # Set the dimensions of the image array. if shape is not None: self.naxis1 = shape[1] self.naxis2 = shape[0] @property def naxis1(self): try: if self.wcs.pixel_shape is not None: return self.wcs.pixel_shape[0] else: return 0 except AttributeError: # This way to get naxis is deprecated with Astropy 3.1 return self.wcs._naxis1 @naxis1.setter def naxis1(self, value): try: if self.wcs.pixel_shape is not None: self.wcs.pixel_shape = [value, self.wcs.pixel_shape[1]] else: self.wcs.pixel_shape = [value, 0] except AttributeError: self.wcs._naxis1 = value @property def naxis2(self): try: if self.wcs.pixel_shape is not None: return self.wcs.pixel_shape[1] else: return 0 except AttributeError: # This way to get naxis is deprecated with Astropy 3.1 return self.wcs._naxis2 @naxis2.setter def naxis2(self, value): try: if self.wcs.pixel_shape is not None: self.wcs.pixel_shape = [self.wcs.pixel_shape[0], value] else: self.wcs.pixel_shape = [0, value] except AttributeError: self.wcs._naxis2 = value
[docs] def copy(self): """Return a copy of a WCS object.""" out = WCS() out.wcs = self.wcs.deepcopy() return out
def __repr__(self): return repr(self.wcs)
[docs] def info(self): """Print information about a WCS object.""" try: dy, dx = self.get_step(unit=u.arcsec) sizex = dx * self.naxis1 # ra sizey = dy * self.naxis2 # dec # center in sexadecimal xc = (self.naxis1 - 1) / 2. yc = (self.naxis2 - 1) / 2. pixsky = self.pix2sky([yc, xc], unit=u.deg)[0] dec, ra = deg2sexa(pixsky) self._logger.info( 'center:(%s,%s) size:(%0.3f",%0.3f") ' 'step:(%0.3f",%0.3f") rot:%0.1f deg frame:%s', dec, ra, sizey, sizex, dy, dx, self.get_rot(), self.wcs.wcs.radesys) except Exception: # FIXME: when is that useful ? pixcrd = [[0, 0], [self.naxis2 - 1, self.naxis1 - 1]] pixsky = self.pix2sky(pixcrd) dy, dx = self.get_step() self._logger.info( 'spatial coord (%s): min:(%0.1f,%0.1f) max:(%0.1f,%0.1f) ' 'step:(%0.1f,%0.1f) rot:%0.1f deg', self.unit, pixsky[0, 0], pixsky[0, 1], pixsky[1, 0], pixsky[1, 1], dy, dx, self.get_rot())
[docs] def to_header(self): """Generate an astropy.fits header object containing the WCS information. """ has_cd = self.wcs.wcs.has_cd() hdr = self.wcs.to_header() if has_cd: for ci in range(1, 3): cdelt = hdr.pop('CDELT%i' % ci, 1) for cj in range(1, 3): try: val = cdelt * hdr.pop('PC%i_%i' % (ci, cj)) except KeyError: if ci == cj: val = cdelt else: val = 0. hdr['CD%i_%i' % (ci, cj)] = val return hdr
[docs] def sky2pix(self, x, nearest=False, unit=None): """Convert world coordinates (dec,ra) to image pixel indexes (y,x). If nearest=True; returns the nearest integer pixel. Parameters ---------- x : array An (n,2) array of dec- and ra- world coordinates. nearest : bool If nearest is True returns the nearest integer pixel in place of the decimal pixel. unit : `astropy.units.Unit` The units of the world coordinates Returns ------- out : (n,2) array of image pixel indexes. These are python array indexes, ordered like (y,x) and with 0,0 denoting the lower left pixel of the image. """ x = np.asarray(x, dtype=np.float64) if x.shape == (2,): x = x.reshape(1, 2) elif len(x.shape) != 2 or x.shape[1] != 2: raise IOError('invalid input coordinates for sky2pix') if unit is not None: x[:, 1] = UnitArray(x[:, 1], unit, self.unit) x[:, 0] = UnitArray(x[:, 0], unit, self.unit) # Tell world2pix to convert the world coordinates to # zero-relative array indexes. ax, ay = self.wcs.wcs_world2pix(x[:, 1], x[:, 0], 0) res = np.array([ay, ax]).T if nearest: res += 0.5 res = res.astype(int) if self.naxis1 != 0 and self.naxis2 != 0: np.clip(res, (0, 0), (self.naxis2 - 1, self.naxis1 - 1), out=res) return res
[docs] def coord(self, spaxel=False, relative=False, center=None, polar=False, unit=None, reshape=False, mask=None): """Return the full coordinate array of the image Parameters ---------- unit : `astropy.units.Unit` Unit of the spatial coordinates if None return pixels relative : bool If True, the coordinate are given relative to the center center : tuple center coordinate use for relative and radial mode if none, the image center is used must be given in unit polar : bool if True, return polar coordinate (r, theta) unit : `astropy.units.Unit` The units of the world coordinates mask : 2D array of bool If not None, return only non masked spaxels reshape : bool if True, return two 1D array, if False, return two 2D array (meshgrid) Returns ------- out : array of array of float array of [dec,ra], [p,q], [delta_dec, delta_ra], [delta_p, delta_q], [r, theta] """ pixcrd = np.indices((self.naxis2, self.naxis1)) if spaxel: x, y = pixcrd else: coord = self.pix2sky(pixcrd.reshape(2, -1).T).T x, y = coord.reshape(pixcrd.shape) if mask is not None: x = x[~mask] y = y[~mask] if relative or polar: if center is None: if spaxel: center = 0.5 * np.array([self.naxis2 - 1, self.naxis1 - 1]) else: center = self.get_center() x0, y0 = center x = x - x0 y = y - y0 if not spaxel and unit is not None: x = UnitArray(x, self.unit, unit) y = UnitArray(y, self.unit, unit) if polar: theta = np.arctan2(y, x) rho = np.hypot(x, y) x = rho y = theta if reshape: x = np.unique(x) y = np.unique(y) return x, y
[docs] def pix2sky(self, x, unit=None): """Convert image pixel indexes (y,x) to world coordinates (dec,ra). Parameters ---------- x : array An (n,2) array of image pixel indexes. These should be python array indexes, ordered like (y,x) and with 0,0 denoting the lower left pixel of the image. unit : `astropy.units.Unit` The units of the world coordinates. Returns ------- out : (n,2) array of dec- and ra- world coordinates. """ x = np.asarray(x, dtype=np.float64) if x.shape == (2,): x = x.reshape(1, 2) elif len(x.shape) != 2 or x.shape[1] != 2: raise IOError('invalid input coordinates for pix2sky') # Tell world2pix to treat the pixel indexes as zero relative # array indexes. ra, dec = self.wcs.wcs_pix2world(x[:, 1], x[:, 0], 0) if unit is not None: ra = UnitArray(ra, self.unit, unit) dec = UnitArray(dec, self.unit, unit) return np.array([dec, ra]).T
[docs] def isEqual(self, other, start_atol=1e-6, rot_atol=1e-6): """Return True if other and self have the same attributes. Beware that if the two wcs objects have the same world coordinate characteristics, but come from images of different dimensions, the objects will be considered different. Parameters ---------- other : WCS The wcs object to be compared to self. start_atol : float Absolute tolerance for the test with `WCS.get_start`, in degrees. start_rot : float Absolute tolerance for the test with `WCS.get_rot`, in degrees. Returns ------- out : bool True if the two WCS objects have the same attributes. """ if not isinstance(other, WCS): return False if not self.sameStep(other): return False if self.naxis1 != other.naxis1 or self.naxis2 != other.naxis2: return False if not np.allclose(self.get_start(), other.get_start(), atol=start_atol, rtol=0): return False if not np.allclose(self.get_rot(), other.get_rot(), atol=rot_atol, rtol=0): return False return True
[docs] def sameStep(self, other): """Return True if other and self have the same pixel sizes. Parameters ---------- other : WCS The wcs object to compare to self. Returns ------- out : bool True if the two arrays of axis step increments are equal. """ if not isinstance(other, WCS): return False steps1 = self.get_step() steps2 = other.get_step(unit=self.unit) return np.allclose(steps1, steps2, atol=1E-7, rtol=0)
def __getitem__(self, item): """Return a WCS object of a 2D slice""" if not isinstance(item, (tuple, list)) or len(item) != 2: raise ValueError('Invalid index, a 2D slice is expected') if not isinstance(item[1], slice) and not isinstance(item[0], slice): return None # See if a slice object was sent for the X axis. if isinstance(item[1], slice): # If a start index was provided, limit it to the extent of # the x-axis. If no start index was provided, default to # zero. if item[1].start is None: imin = 0 else: imin = int(item[1].start) if imin < 0: imin = self.naxis1 + imin if imin > self.naxis1: imin = self.naxis1 # If a stop index was provided, limit it to the extent of the # X axis. Otherwise substitute the size of the X-axis. if item[1].stop is None: imax = self.naxis1 else: imax = int(item[1].stop) if imax < 0: imax = self.naxis1 + imax if imax > self.naxis1: imax = self.naxis1 # If a step was provided and it isn't 1, complain # because we can't accommodate gaps between pixels. if item[1].step is not None and item[1].step != 1: raise ValueError('Index steps are not supported') else: # If a slice object wasn't sent, then maybe a single index # was passed for the X axis. If so, select the specified # single pixel. imin = int(item[1]) imax = int(item[1] + 1) # See if a slice object was sent for the Y axis. if isinstance(item[0], slice): # If a start index was provided, limit it to the extent of # the y-axis. If no start index was provided, default to # zero. if item[0].start is None: jmin = 0 else: jmin = int(item[0].start) if jmin < 0: jmin = self.naxis2 + jmin if jmin > self.naxis2: jmin = self.naxis2 # If a stop index was provided, limit it to the extent of the # Y axis. Otherwise substitute the size of the Y-axis. if item[0].stop is None: jmax = self.naxis2 else: jmax = int(item[0].stop) if jmax < 0: jmax = self.naxis2 + jmax if jmax > self.naxis2: jmax = self.naxis2 # If an index step was provided and it isn't 1, reject # the call, because we can't accommodate gaps between selected # pixels. if item[0].step is not None and item[0].step != 1: raise ValueError('Index steps are not supported') else: # If a slice object wasn't sent, then maybe a single index # was passed for the Y axis. If so, select the specified # single pixel. jmin = int(item[0]) jmax = int(item[0] + 1) # Compute the array indexes of the coordinate reference # pixel in the sliced array. Note that this can indicate a # pixel outside the slice. crpix = (self.wcs.wcs.crpix[0] - imin, self.wcs.wcs.crpix[1] - jmin) # Get a copy of the original WCS object. res = self.copy() # Record the new coordinate reference pixel index and the # reduced dimensions of the selected sub-image. res.wcs.wcs.crpix = np.array(crpix) res.naxis1 = int(imax - imin) res.naxis2 = int(jmax - jmin) res.wcs.wcs.set() return res
[docs] def get_step(self, unit=None): """Return the angular height and width of a pixel along the Y and X axes of the image array. In MPDAF, images are sampled on a regular grid of square pixels that represent a flat projection of the celestial sphere. The get_step() method returns the angular width and height of these pixels on the sky. See also get_axis_increments(). Parameters ---------- unit : `astropy.units.Unit` The angular units of the returned values. Returns ------- out : numpy.ndarray (dy,dx). These are the angular height and width of pixels along the Y and X axes of the image. The returned values are either in the unit specified by the 'unit' input parameter, or in the unit specified by the self.unit property. """ # The pixel dimensions are determined as follows. First note # that the coordinate transformation matrix looks as follows: # # |r| = |M[0,0], M[0,1]| |col - get_crpix1()| # |d| |M[1,0], M[1,1]| |row - get_crpix2()| # # In this equation [col,row] are the indexes of a pixel in the # image array and [r,d] are the coordinates of this pixel on a # flat map-projection of the sky. If the celestial coordinates # of the observation are right ascension and declination, then d # is parallel to declination, and r is perpendicular to this, # pointing east. When the column index is incremented by 1, the # above equation indicates that r and d change by: # # col_dr = M[0,0] col_dd = M[1,0] # # The length of the vector from (0,0) to (col_dr,col_dd) is # the angular width of pixels along the X axis. # # dx = sqrt(M[0,0]^2 + M[1,1]^2) # # Similarly, when the row index is incremented by 1, r and d # change by: # # row_dr = M[0,1] row_dd = M[1,1] # # The length of the vector from (0,0) to (row_dr,row_dd) is # the angular width of pixels along the Y axis. # # dy = sqrt(M[0,1]^2 + M[1,1]^2) # # Calculate the width and height of the pixels as described above. cd = self.get_cd() dx = np.sqrt(cd[0, 0]**2 + cd[1, 0]**2) dy = np.sqrt(cd[0, 1]**2 + cd[1, 1]**2) steps = np.array([dy, dx]) if unit is not None: steps = (steps * self.unit).to(unit).value return steps
[docs] def get_axis_increments(self, unit=None): """Return the displacements on the sky that result from incrementing the array indexes of the image by one along the Y and X axes, respectively. In MPDAF, images are sampled on a regular grid of square pixels that represent a flat projection of the celestial sphere. The get_axis_increments() method returns the angular width and height of these pixels on the sky, with signs that indicate whether the angle increases or decreases as one increments the array indexes. To keep plots consistent, regardless of the rotation angle of the image on the sky, the returned height is always positive, but the returned width is negative if a plot of the image with pixel 0,0 at the bottom left would place east anticlockwise of north, and positive otherwise. Parameters ---------- unit : `astropy.units.Unit` The angular units of the returned values. Returns ------- out : numpy.ndarray (dy,dx). These are the angular increments of pixels along the Y and X axes of the image. The returned values are either in the unit specified by the 'unit' input parameter, or in the unit specified by the self.unit property. """ # Get the axis increments that are configured by the CD matrix. cd = self.get_cd() increments = axis_increments_from_cd(cd) if unit is not None: increments = (increments * self.unit).to(unit).value return increments
[docs] def get_range(self, unit=None): """Return the minimum and maximum right-ascensions and declinations in the image array. Specifically a list is returned with the following contents:: [dec_min, ra_min, dec_max, ra_max] Note that if the Y axis of the image is not parallel to the declination axis, then the 4 returned values will all come from different corners of the image. In particular, note that this means that the coordinates [dec_min,ra_min] and [dec_max,ra_max] will only coincide with pixels in the image if the Y axis is aligned with the declination axis. Otherwise they will be outside the bounds of the image. Parameters ---------- unit : `astropy.units.Unit` The units of the returned angles. Returns ------- out : numpy.ndarray The range of right ascensions and declinations, arranged as [dec_min, ra_min, dec_max, ra_max]. The returned values are either in the units specified in the 'unit' input parameter, or in the units stored in the self.unit property. """ pixcrd = [[0, 0], [self.naxis2 - 1, 0], [0, self.naxis1 - 1], [self.naxis2 - 1, self.naxis1 - 1]] pixsky = self.pix2sky(pixcrd, unit=unit) return np.hstack([pixsky.min(axis=0), pixsky.max(axis=0)])
[docs] def get_center(self, unit=None): """Return the center (dec,ra) of the image array. Parameters ---------- unit : `astropy.units.Unit` The units of the returned angles. Returns ------- center : numpy.ndarray The center, arranged as [dec, ra]. The returned values are either in the units specified in the 'unit' input parameter, or in the units stored in the self.unit property. """ crange = self.get_range(unit=unit) center = 0.5 * np.array([crange[0] + crange[2], crange[1] + crange[3]]) return center
[docs] def get_start(self, unit=None): """Return the [dec,ra] coordinates of pixel (0,0). Parameters ---------- unit : `astropy.units.Unit` The angular units of the returned coordinates. Returns ------- out : numpy.ndarray The equatorial coordinate of pixel [0,0], ordered as: [dec,ra]. If a value was given to the optional 'unit' argument, the angular unit specified there will be used for the return value. Otherwise the unit stored in the self.unit property will be used. """ return self.pix2sky([0, 0], unit=unit)[0]
[docs] def get_end(self, unit=None): """Return the [dec,ra] coordinates of pixel (-1,-1). Parameters ---------- unit : `astropy.units.Unit` The angular units of the returned coordinates. Returns ------- out : numpy.ndarray The equatorial coordinate of pixel [-1,-1], ordered as, [dec,ra]. If a value was given to the optional 'unit' argument, the angular unit specified there will be used for the return value. Otherwise the unit stored in the self.unit property will be used. """ return self.pix2sky([self.naxis2 - 1, self.naxis1 - 1], unit=unit)[0]
[docs] def get_rot(self, unit=u.deg): """Return the rotation angle of the image. The angle is defined such that a rotation angle of zero aligns north along the positive Y axis, and a positive rotation angle rotates north away from the Y axis, in the sense of a rotation from north to east. Note that the rotation angle is defined in a flat map-projection of the sky. It is what would be seen if the pixels of the image were drawn with their pixel widths scaled by the angular pixel increments returned by the get_axis_increments() method. Parameters ---------- unit : `astropy.units.Unit` The unit to give the returned angle (degrees by default). Returns ------- out : float The angle between celestial north and the Y axis of the image, in the sense of an eastward rotation of celestial north from the Y-axis. """ cd = self.get_cd() return image_angle_from_cd(cd, unit)
[docs] def get_cd(self): """Return the coordinate conversion matrix (CD). This is a 2x2 matrix that can be used to convert from the column and row indexes of a pixel in the image array to a coordinate within a flat map-projection of the celestial sphere. For example, if the celestial coordinates of the observation are right-ascension and declination, and r and d denote their gnonomic TAN projection onto a flat plane, then a pixel at row and column [col,row] has [r,d] coordinates given by:: (r,d) = np.dot(get_cd(), (col - get_crpix1(), row - get_crpix2()) Returns ------- out : nump.ndarray A 2D array containing the coordinate transformation matrix, arranged such that the elements described in the FITS standard are arranged as follows:: [[CD_11, CD_12] [CD_21, CD_22]] """ # The documentation for astropy.wcs.Wcsprm indicates that # get_cdelt() and get_pc() work: # # "even when the header specifies the linear transformation # matrix in one of the alternative CDi_ja or CROTAia # forms. This is useful when you want access to the linear # transformation matrix, but don't care how it was specified # in the header." # # So to ensure that get_cd() always returns the current CD # matrix, get CDELT and PC and convert them to the equivalent # CD matrix. Note that self.wcs.wcs.piximg_matrix is not used # because sometimes pywcs doesn't create it when a WCS object # is not initialized from a FITS header. return np.dot(np.diag(self.wcs.wcs.get_cdelt()), self.wcs.wcs.get_pc())
[docs] def get_crpix1(self): """Return the value of the FITS CRPIX1 parameter. CRPIX1 contains the index of the reference position of the image along the X-axis of the image. Beware that this is a FITS array index, which is 1 greater than the corresponding python array index. For example, a crpix value of 1 denotes a python array index of 0. The reference pixel index is a floating point value that can indicate a position between two pixels. It can also indicate an index that is outside the bounds of the array. Returns ------- out : float The value of the FITS CRPIX1 parameter. """ return self.wcs.wcs.crpix[0]
[docs] def get_crpix2(self): """Return the value of the FITS CRPIX2 parameter. CRPIX2 contains the index of the reference position of the image along the Y-axis of the image. Beware that this is a FITS array index, which is 1 greater than the corresponding python array index. For example, a crpix value of 1 denotes a python array index of 0. The reference pixel index is a floating point value that can indicate a position between two pixels. It can also indicate an index that is outside the bounds of the array. Returns ------- out : float The value of the FITS CRPIX2 parameter. """ return self.wcs.wcs.crpix[1]
[docs] def get_crval1(self, unit=None): """Return the value of the FITS CRVAL1 parameter. CRVAL1 contains the coordinate reference value of the first image axis (eg. right-ascension). Parameters ---------- unit : `astropy.units.Unit` The angular units to give the return value. Returns ------- out : float The value of CRVAL1 in the specified angular units. If the units are not given, then the unit in the self.unit property is used. """ if unit is None: return self.wcs.wcs.crval[0] else: return (self.wcs.wcs.crval[0] * self.unit).to(unit).value
[docs] def get_crval2(self, unit=None): """Return the value of the FITS CRVAL2 parameter. CRVAL2 contains the coordinate reference value of the second image axis (eg. declination). Parameters ---------- unit : `astropy.units.Unit` The angular units to give the return value. Returns ------- out : float The value of CRVAL2 in the specified angular units. If the units are not given, then the unit in the self.unit property is used. """ if unit is None: return self.wcs.wcs.crval[1] else: return (self.wcs.wcs.crval[1] * self.unit).to(unit).value
@property def unit(self): """Return the default angular unit used for sky coordinates. Returns ------- out : `astropy.units.Unit` The unit to use for coordinate angles. """ if self.wcs.wcs.cunit[0] != self.wcs.wcs.cunit[1]: self._logger.warning('different units on x- and y-axes') return self.wcs.wcs.cunit[0]
[docs] def set_cd(self, cd): """Install a new coordinate transform matrix. This is a 2x2 matrix that is used to convert from the row and column indexes of a pixel in the image array to a coordinate within a flat map-projection of the celestial sphere. It is formerly described in the FITS standard. The matrix should be ordered like:: cd = numpy.array([[CD1_1, CD1_2], [CD2_1, CD2_2]]), where CDj_i are the names of the corresponding FITS keywords. Parameters ---------- cd : numpy.ndarray The 2x2 coordinate conversion matrix, with its elements ordered for multiplying a column vector in FITS (x,y) axis order. """ # Wcslib supports three different ways to configure the linear # coordinate transformation. Once one of these has been # established by reading in a FITS header, it can't reliably # be changed by configuring one of the other methods, so # record the specified CD matrix in the currently established format. if self.wcs.wcs.has_pc(): # PC matrix + CDELT self.wcs.wcs.pc = cd self.wcs.wcs.cdelt = np.array([1.0, 1.0]) elif self.wcs.wcs.has_cd(): # CD matrix self.wcs.wcs.cd = cd elif self.wcs.wcs.has_crota(): # CROTA + CDELT self.wcs.wcs.crota = [0., image_angle_from_cd(cd, u.deg)] self.wcs.wcs.cdelt = axis_increments_from_cd(cd)[::-1] self.wcs.wcs.set()
[docs] def set_crpix1(self, x): """Set the value of the FITS CRPIX1 parameter. This sets the reference pixel index along the X-axis of the image. This is a floating point value which can denote a position between pixels. It is specified with the FITS indexing convention, where FITS pixel 1 is equivalent to pixel 0 in python arrays. In general subtract 1 from x to get the corresponding floating-point pixel index along axis 1 of the image array. In cases where x is an integer, the corresponding row in the python data array that contains the image is ``data[:, x-1]``. Parameters ---------- x : float The index of the reference pixel along the X axis. """ self.wcs.wcs.crpix[0] = x self.wcs.wcs.set()
[docs] def set_crpix2(self, y): """Set the value of the FITS CRPIX2 parameter. This sets the reference pixel index along the Y-axis of the image. This is a floating point value which can denote a position between pixels. It is specified with the FITS indexing convention, where FITS pixel 1 is equivalent to pixel 0 in python arrays. In general subtract 1 from y to get the corresponding floating-point pixel index along axis 0 of the image array. In cases where y is an integer, the corresponding column in the python data array that contains the image is ``data[y-1, :]``. Parameters ---------- y : float The index of the reference pixel along the Y axis. """ self.wcs.wcs.crpix[1] = y self.wcs.wcs.set()
[docs] def set_crval1(self, x, unit=None): """Set the value of the CRVAL1 keyword. It indicates the coordinate reference value along the first image axis (eg. right ascension). Parameters ---------- x : float The value of the reference pixel on the first axis. unit : `astropy.units.Unit` The angular units of the world coordinates. """ if unit is None: self.wcs.wcs.crval[0] = x else: self.wcs.wcs.crval[0] = (x * unit).to(self.unit).value self.wcs.wcs.set()
[docs] def set_crval2(self, x, unit=None): """Set the value of the CRVAL2 keyword. It indicates the coordinate reference value along the second image axis (eg. declination). Parameters ---------- x : float The value of the reference pixel on the second axis. unit : `astropy.units.Unit` The angular units of the world coordinates. """ if unit is None: self.wcs.wcs.crval[1] = x else: self.wcs.wcs.crval[1] = (x * unit).to(self.unit).value self.wcs.wcs.set()
[docs] def set_step(self, step, unit=None): """Set the height and width of pixels on the sky. In MPDAF, images are sampled on a regular grid of square pixels that represent a flat projection of the celestial sphere. The set_step() method changes the angular width and height of these pixels on the sky. Parameters ---------- step : array-like (h,w). These are the desired angular height and width of pixels along the Y and X axes of the image. These should either be in the unit specified by the 'unit' input parameter, or, if unit=None, in the unit specified by the self.unit property. unit : `astropy.units.Unit` The angular units of the specified increments. """ step = abs(np.asarray(step)) if unit is not None: step = (step * unit).to(self.unit).value old_step = self.get_step() ratio = step / old_step cd = self.get_cd() # Scaling the 1st column of the CD matrix, scales the X-axis pixel # sizes. Scaling the 2nd column scales the Y-axis pixel sizes. self.set_cd(np.dot(cd, np.array([[ratio[1], 0.0], [0.0, ratio[0]]])))
[docs] def set_axis_increments(self, increments, unit=None): """Set the displacements on the sky that result from incrementing the array indexes of the image by one along the Y and X axes, respectively. In MPDAF, images are sampled on a regular grid of square pixels that represent a flat projection of the celestial sphere. The set_axis_increments() method changes the angular width and height of these pixels on the sky, with signs that indicate whether the angle increases or decreases as one increments the array indexes. To keep plots consistent, regardless of the rotation angle of the image on the sky, the height should always be positive, and the width should be negative if a plot of the image with pixel 0,0 at the bottom left would place east anticlockwise of north, and positive otherwise. Parameters ---------- increments : numpy.ndarray (dy,dx). These are the desired angular increments of pixels along the Y and X axes of the image. These should either be in the unit specified by the 'unit' input parameter, or, if unit=None, in the unit specified by the self.unit property. unit : `astropy.units.Unit` The angular units of the specified increments. """ increments = np.asarray(increments) if unit is not None: increments = (increments * unit).to(self.unit).value cd = self.get_cd() old_increments = axis_increments_from_cd(cd) ratio = increments / old_increments # Scaling the 1st column of the CD matrix, scales the X-axis pixel # sizes. Scaling the 2nd column scales the Y-axis pixel sizes. self.set_cd(np.dot(cd, np.array([[ratio[1], 0.0], [0.0, ratio[0]]])))
[docs] def rotate(self, theta): """Rotate WCS coordinates to new orientation given by theta. Analog to ``astropy.wcs.WCS.rotateCD``, which is deprecated since version 1.3 (see https://github.com/astropy/astropy/issues/5175). Parameters ---------- theta : float Rotation in degree. """ theta = np.deg2rad(theta) sinq = np.sin(theta) cosq = np.cos(theta) mrot = np.array([[cosq, -sinq], [sinq, cosq]]) if self.wcs.wcs.has_cd(): # CD matrix newcd = np.dot(mrot, self.wcs.wcs.cd) self.wcs.wcs.cd = newcd self.wcs.wcs.set() elif self.wcs.wcs.has_pc(): # PC matrix + CDELT newpc = np.dot(mrot, self.wcs.wcs.get_pc()) self.wcs.wcs.pc = newpc self.wcs.wcs.set() else: raise TypeError("Unsupported wcs type (need CD or PC matrix)")
[docs] def rebin(self, factor): """Rebin to a new coordinate system. This is a helper function for the Image and Cube rebin() methods. Parameters ---------- factor : (integer,integer) Factor in y and x. Returns ------- out : WCS """ res = self.copy() # Record the increased pixel sizes. res.set_step(self.get_step() * np.asarray(factor)) # Compute the new coordinate reference pixel, noting that for # the FITS crpix value, the center of the first pixel is # defined to be 1, not 0. The original crpix index denotes a # pixel that is (crpix-0.5) of the original pixel widths from # the start of the first pixel of the original array. This # corresponds to (crpix-0.5)/factor new pixel widths from the # start of the first pixel, so this has a pixel index of # (oldcrpix-0.5)/factor+0.5 in the rebinned array. res.wcs.wcs.crpix[0] = (res.wcs.wcs.crpix[0] - 0.5) / factor[1] + 0.5 res.wcs.wcs.crpix[1] = (res.wcs.wcs.crpix[1] - 0.5) / factor[0] + 0.5 # Record the new dimensions of the image. res.naxis1 = res.naxis1 // factor[1] res.naxis2 = res.naxis2 // factor[0] res.wcs.wcs.set() return res
[docs] def is_deg(self): """Return True if world coordinates are in decimal degrees. (CTYPE1='RA---TAN',CTYPE2='DEC--TAN',CUNIT1=CUNIT2='deg) """ try: return self.wcs.wcs.ctype[0] not in ('LINEAR', 'PIXEL') except Exception: return True
[docs] def to_cube_header(self, wave): """Generate an astropy.fits header object with WCS information and wavelength information.""" hdr = self.to_header() if wave is not None: hdr.update(wave.to_header(naxis=3, use_cd='CD1_1' in hdr)) return hdr
[docs]class WaveCoord: """WaveCoord class manages world coordinates in spectral direction. Parameters ---------- hdr : astropy.fits.CardList A FITS header. If hdr is not None, WaveCoord object is created from this header and other parameters are not used. crpix : float Reference pixel coordinates. 1.0 by default. Note that for crpix definition, the first pixel in the spectrum has pixel coordinates. cdelt : float Step in wavelength (1.0 by default). crval : float Coordinates of the reference pixel (0.0 by default). cunit : u.unit Wavelength unit (Angstrom by default). ctype : string Type of the coordinates. shape : integer or None Size of spectrum (no mandatory). Attributes ---------- shape : integer Size of spectrum. wcs : astropy.wcs.WCS Wavelength coordinates. """ def __init__(self, hdr=None, crpix=1.0, cdelt=1.0, crval=1.0, cunit=u.angstrom, ctype='LINEAR', shape=None): self._logger = logging.getLogger(__name__) self.shape = shape self.unit = cunit if hdr is not None: hdr = hdr.copy() try: n = hdr['NAXIS'] self.shape = hdr['NAXIS%d' % n] except KeyError: n = hdr['WCSAXES'] axis = 1 if n == 1 else 3 # Get the unit and remove it from the header so that wcslib does # not convert the values. self.unit = u.Unit(fix_unit_read(hdr.pop('CUNIT%d' % axis))) self.wcs = _wcs_from_header(hdr).sub([axis]) else: self.unit = u.Unit(cunit) self.wcs = pywcs.WCS(naxis=1) self.wcs.wcs.crpix[0] = crpix self.wcs.wcs.cdelt[0] = cdelt self.wcs.wcs.ctype[0] = ctype self.wcs.wcs.crval[0] = crval self.wcs.wcs.set()
[docs] def copy(self): """Copie WaveCoord object in a new one and returns it.""" out = WaveCoord(shape=self.shape, cunit=self.unit) out.wcs = self.wcs.deepcopy() return out
def __repr__(self): return repr(self.wcs)
[docs] def info(self, unit=None): """Print information.""" unit = unit or self.unit start = self.get_start(unit=unit) step = self.get_step(unit=unit) if self.shape is None: self._logger.info('wavelength: min:%0.2f step:%0.2f %s', start, step, unit) else: end = self.get_end(unit=unit) self._logger.info('wavelength: min:%0.2f max:%0.2f step:%0.2f %s', start, end, step, unit)
[docs] def isEqual(self, other): """Return True if other and self have the same attributes.""" if not isinstance(other, WaveCoord): return False l1 = self.coord(0, unit=self.unit) l2 = other.coord(0, unit=self.unit) return (self.shape == other.shape and np.allclose(l1, l2, atol=1E-2, rtol=0) and np.allclose(self.get_step(), other.get_step(unit=self.unit), atol=1E-2, rtol=0) and self.wcs.wcs.ctype[0] == other.wcs.wcs.ctype[0])
[docs] def coord(self, pixel=None, unit=None, medium=None): """Return the coordinate corresponding to pixel. If pixel is None (default value), the full coordinate array is returned. Parameters ---------- pixel : int, array or None. Pixel value. unit : `astropy.units.Unit` Unit of the wavelength coordinates medium : str or None Medium in which the wavelengths are returned: 'air' or 'vacuum'. If None (default), the wavelength corresponding to the spectrum CTYPE are returned. Returns ------- out : float or array of float """ if pixel is None and self.shape is None: raise IOError("wavelength coordinates without dimension") if pixel is None: pixelarr = np.arange(self.shape, dtype=float) else: pixelarr = np.atleast_1d(pixel) res = self.wcs.wcs_pix2world(pixelarr, 0)[0] if unit is not None: res = (res * self.unit).to(unit).value result = res[0] if np.isscalar(pixel) else res if medium is not None: if medium not in ['air', 'vacuum']: raise ValueError("Unknown 'medium' parameter value.") ctype = self.wcs.wcs.ctype[0] # Wave type if ctype[:4] not in ['WAVE', 'AWAV']: raise ValueError("No method to convert from %s to %s." % (ctype, medium)) from .spectrum import airtovac, vactoair # To avoid circ. import if medium == "air" and ctype.startswith("WAVE"): result = vactoair(result) elif medium == "vacuum" and ctype.startswith("AWAV"): result = airtovac(result) return result
[docs] def pixel(self, lbda, nearest=False, unit=None): """Return the decimal pixel corresponding to the wavelength lbda. If nearest=True; returns the nearest integer pixel. Parameters ---------- lbda : float or array wavelength value. nearest : bool If nearest is True returns the nearest integer pixel in place of the decimal pixel. unit : `astropy.units.Unit` type of the wavelength coordinates Returns ------- out : float or integer """ lbdarr = np.atleast_1d(lbda) if unit is not None: lbdarr = UnitArray(lbdarr, unit, self.unit) pix = self.wcs.wcs_world2pix(lbdarr, 0)[0] if nearest: pix = (pix + 0.5).astype(int) np.maximum(pix, 0, out=pix) if self.shape is not None: np.minimum(pix, self.shape - 1, out=pix) return pix[0] if np.isscalar(lbda) else pix
def __getitem__(self, item): """Return the coordinate corresponding to pixel if item is an integer Return the corresponding WaveCoord object if item is a slice.""" noshape_msg = 'negative index cannot be used without a shape' if item is None: return self elif isinstance(item, int): if item >= 0: lbda = self.coord(pixel=item) else: if self.shape is None: raise ValueError(noshape_msg) else: lbda = self.coord(pixel=self.shape + item) return WaveCoord(crpix=1.0, cdelt=0, crval=lbda, cunit=self.unit, shape=1, ctype=self.wcs.wcs.ctype[0]) elif isinstance(item, slice): if item.start is None: start = 0 elif item.start >= 0: start = item.start else: if self.shape is None: raise ValueError(noshape_msg) else: start = self.shape + item.start if item.stop is None: if self.shape is None: raise ValueError(noshape_msg) else: stop = self.shape elif item.stop >= 0: stop = item.stop else: if self.shape is None: raise ValueError(noshape_msg) else: stop = self.shape + item.stop newlbda = self.coord(pixel=np.arange(start, stop, item.step)) dim = newlbda.shape[0] if dim < 2: raise ValueError('Spectrum with dim < 2') cdelt = newlbda[1] - newlbda[0] return WaveCoord(crpix=1.0, cdelt=cdelt, crval=newlbda[0], cunit=self.unit, shape=dim, ctype=self.wcs.wcs.ctype[0]) else: raise ValueError('Operation forbidden')
[docs] def resample(self, step, start, unit=None): """Resample to a new coordinate system. Parameters ---------- start : float New wavelength for the pixel 0. step : float New step. unit : `astropy.units.Unit` type of the wavelength coordinates Returns ------- out : WaveCoord """ if unit is not None: step = (step * unit).to(self.unit).value if start is not None: start = (start * unit).to(self.unit).value cdelt = self.get_step() if start is None: pix0 = self.coord(0) start = pix0 - 0.5 * cdelt + 0.5 * step old_start = self.get_start() res = self.copy() res.wcs.wcs.crpix[0] = 1.0 res.wcs.wcs.crval[0] = start if self.wcs.wcs.has_cd(): res.wcs.wcs.cd[0][0] = step else: res.wcs.wcs.cdelt[0] = 1.0 res.wcs.wcs.pc[0][0] = step res.wcs.wcs.set() res.shape = int(np.ceil((self.shape * cdelt - start + old_start) / step)) return res
[docs] def rebin(self, factor): """Rebin to a new coordinate system (in place). Parameters ---------- factor : integer Factor. Returns ------- out : WaveCoord """ old_cdelt = self.get_step() if self.wcs.wcs.has_cd(): self.wcs.wcs.cd = self.wcs.wcs.cd * factor else: self.wcs.wcs.cdelt = self.wcs.wcs.cdelt * factor self.wcs.wcs.set() cdelt = self.get_step() crpix = self.wcs.wcs.crpix[0] crpix = (crpix * old_cdelt - old_cdelt / 2.0 + cdelt / 2.0) / cdelt self.wcs.wcs.crpix[0] = crpix self.shape = self.shape // factor self.wcs.wcs.set()
[docs] def get_step(self, unit=None): """Return the step in wavelength. Parameters ---------- unit : `astropy.units.Unit` type of the wavelength coordinates """ if self.wcs.wcs.has_cd(): step = self.wcs.wcs.cd[0][0] else: cdelt = self.wcs.wcs.get_cdelt()[0] pc = self.wcs.wcs.get_pc()[0, 0] step = (cdelt * pc) if unit is not None: step = (step * self.unit).to(unit).value return step
[docs] def set_step(self, x, unit=None): """Return the step in wavelength. Parameters ---------- x : float Step value unit : `astropy.units.Unit` type of the wavelength coordinates """ if unit is not None: step = (x * unit).to(self.unit).value else: step = x if self.wcs.wcs.has_cd(): self.wcs.wcs.cd[0][0] = step else: pc = self.wcs.wcs.get_pc()[0, 0] self.wcs.wcs.cdelt[0] = step / pc self.wcs.wcs.set()
[docs] def get_start(self, unit=None): """Return the value of the first pixel. Parameters ---------- unit : `astropy.units.Unit` type of the wavelength coordinates """ return self.coord(0, unit)
[docs] def get_end(self, unit=None): """Return the value of the last pixel. Parameters ---------- unit : `astropy.units.Unit` type of the wavelength coordinates """ if self.shape is None: raise IOError("wavelength coordinates without dimension") else: return self.coord(self.shape - 1, unit)
[docs] def get_range(self, unit=None): """Return the wavelength range [Lambda_min,Lambda_max]. Parameters ---------- unit : `astropy.units.Unit` type of the wavelength coordinates """ if self.shape is None: raise IOError("wavelength coordinates without dimension") else: return self.coord([0, self.shape - 1], unit)
[docs] def get_crpix(self): """CRPIX getter (reference pixel on the wavelength axis).""" return self.wcs.wcs.crpix[0]
[docs] def set_crpix(self, x): """CRPIX setter (reference pixel on the wavelength axis).""" self.wcs.wcs.crpix[0] = x self.wcs.wcs.set()
[docs] def get_crval(self, unit=None): """CRVAL getter (value of the reference pixel on the wavelength axis). Parameters ---------- unit : `astropy.units.Unit` type of the wavelength coordinates """ if unit is None: return self.wcs.wcs.crval[0] else: return (self.wcs.wcs.crval[0] * self.unit).to(unit).value
[docs] def set_crval(self, x, unit=None): """CRVAL getter (value of the reference pixel on the wavelength axis). Parameters ---------- x : float value of the reference pixel on the wavelength axis unit : `astropy.units.Unit` type of the wavelength coordinates """ if unit is None: self.wcs.wcs.crval[0] = x else: self.wcs.wcs.crval[0] = (x * unit).to(self.unit).value self.wcs.wcs.set()
[docs] def get_ctype(self): """Return the type of wavelength coordinates.""" return self.wcs.wcs.ctype[0]
[docs] def to_header(self, naxis=1, use_cd=False): """Generate a astropy.fits header object with the WCS information.""" hdr = fits.Header() hdr['WCSAXES'] = (naxis, 'Number of coordinate axes') hdr['CRVAL%d' % naxis] = (self.get_crval(), 'Coordinate value at reference point') hdr['CRPIX%d' % naxis] = (self.get_crpix(), 'Pixel coordinate of reference point') hdr['CUNIT%d' % naxis] = (self.unit.to_string('fits'), 'Units of coordinate increment and value') hdr['CTYPE%d' % naxis] = (self.get_ctype(), 'Coordinate type code') if use_cd and naxis == 3: hdr['CD3_3'] = self.get_step() hdr['CD1_3'] = 0. hdr['CD2_3'] = 0. hdr['CD3_1'] = 0. hdr['CD3_2'] = 0. else: hdr['CDELT%d' % naxis] = (self.get_step(), 'Coordinate increment at reference ' 'point') return hdr