summaryrefslogtreecommitdiff
path: root/stwcs/wcsutil/hstwcs.py
diff options
context:
space:
mode:
authorNadia Dencheva <nadia.astropy@gmail.com>2016-08-07 12:23:24 -0400
committerGitHub <noreply@github.com>2016-08-07 12:23:24 -0400
commita2e16e39b0eb8ac0251a6473c60fee0d437c3a5f (patch)
tree7b6771e9c1974852eb8a283507677651078ce32a /stwcs/wcsutil/hstwcs.py
parent86d1bc5a77491770d45b86e5cf18b79ded68fb9b (diff)
parent2dc0676bc00f66a87737e78484876051633b731a (diff)
downloadstwcs_hcf-a2e16e39b0eb8ac0251a6473c60fee0d437c3a5f.tar.gz
Merge pull request #9 from nden/refactor-and-tests
restructure and add stwcs tests
Diffstat (limited to 'stwcs/wcsutil/hstwcs.py')
-rw-r--r--stwcs/wcsutil/hstwcs.py988
1 files changed, 988 insertions, 0 deletions
diff --git a/stwcs/wcsutil/hstwcs.py b/stwcs/wcsutil/hstwcs.py
new file mode 100644
index 0000000..bfebcfc
--- /dev/null
+++ b/stwcs/wcsutil/hstwcs.py
@@ -0,0 +1,988 @@
+from __future__ import absolute_import, division, print_function # confidence high
+
+import os
+from astropy.wcs import WCS
+from astropy.io import fits
+from stwcs.distortion import models, coeff_converter
+import numpy as np
+from stsci.tools import fileutil
+
+from . import altwcs
+from . import getinput
+from . import mappings
+from . import instruments
+from .mappings import inst_mappings, ins_spec_kw
+from .mappings import basic_wcs
+
+__docformat__ = 'restructuredtext'
+
+#
+#### Utility functions copied from 'updatewcs.utils' to avoid circular imports
+#
+def extract_rootname(kwvalue,suffix=""):
+ """ Returns the rootname from a full reference filename
+
+ If a non-valid value (any of ['','N/A','NONE','INDEF',None]) is input,
+ simply return a string value of 'NONE'
+
+ This function will also replace any 'suffix' specified with a blank.
+ """
+ # check to see whether a valid kwvalue has been provided as input
+ if kwvalue.strip() in ['','N/A','NONE','INDEF',None]:
+ return 'NONE' # no valid value, so return 'NONE'
+
+ # for a valid kwvalue, parse out the rootname
+ # strip off any environment variable from input filename, if any are given
+ if '$' in kwvalue:
+ fullval = kwvalue[kwvalue.find('$')+1:]
+ else:
+ fullval = kwvalue
+ # Extract filename without path from kwvalue
+ fname = os.path.basename(fullval).strip()
+
+ # Now, rip out just the rootname from the full filename
+ rootname = fileutil.buildNewRootname(fname)
+
+ # Now, remove any known suffix from rootname
+ rootname = rootname.replace(suffix,'')
+ return rootname.strip()
+
+def build_default_wcsname(idctab):
+
+ idcname = extract_rootname(idctab,suffix='_idc')
+ wcsname = 'IDC_' + idcname
+ return wcsname
+
+
+class NoConvergence(Exception):
+ """
+ An error class used to report non-convergence and/or divergence of
+ numerical methods. It is used to report errors in the iterative solution
+ used by the :py:meth:`~stwcs.hstwcs.HSTWCS.all_world2pix`\ .
+
+ Attributes
+ ----------
+
+ best_solution : numpy.array
+ Best solution achieved by the method.
+
+ accuracy : float
+ Accuracy of the :py:attr:`best_solution`\ .
+
+ niter : int
+ Number of iterations performed by the numerical method to compute
+ :py:attr:`best_solution`\ .
+
+ divergent : None, numpy.array
+ Indices of the points in :py:attr:`best_solution` array for which the
+ solution appears to be divergent. If the solution does not diverge,
+ `divergent` will be set to `None`.
+
+ failed2converge : None, numpy.array
+ Indices of the points in :py:attr:`best_solution` array for which the
+ solution failed to converge within the specified maximum number
+ of iterations. If there are no non-converging poits (i.e., if
+ the required accuracy has been achieved for all points) then
+ `failed2converge` will be set to `None`.
+
+ """
+ def __init__(self, *args, **kwargs):
+ super(NoConvergence, self).__init__(*args)
+
+ self.best_solution = kwargs.pop('best_solution', None)
+ self.accuracy = kwargs.pop('accuracy', None)
+ self.niter = kwargs.pop('niter', None)
+ self.divergent = kwargs.pop('divergent', None)
+ self.failed2converge= kwargs.pop('failed2converge', None)
+
+
+#
+#### HSTWCS Class definition
+#
+class HSTWCS(WCS):
+
+ def __init__(self, fobj=None, ext=None, minerr=0.0, wcskey=" "):
+ """
+ Create a WCS object based on the instrument.
+
+ In addition to basic WCS keywords this class provides
+ instrument specific information needed in distortion computation.
+
+ Parameters
+ ----------
+ fobj : str or `astropy.io.fits.HDUList` object or None
+ file name, e.g j9irw4b1q_flt.fits
+ fully qualified filename[EXTNAME,EXTNUM], e.g. j9irw4b1q_flt.fits[sci,1]
+ `astropy.io.fits` file object, e.g fits.open('j9irw4b1q_flt.fits'), in which case the
+ user is responsible for closing the file object.
+ ext : int, tuple or None
+ extension number
+ if ext is tuple, it must be ("EXTNAME", EXTNUM), e.g. ("SCI", 2)
+ if ext is None, it is assumed the data is in the primary hdu
+ minerr : float
+ minimum value a distortion correction must have in order to be applied.
+ If CPERRja, CQERRja are smaller than minerr, the corersponding
+ distortion is not applied.
+ wcskey : str
+ A one character A-Z or " " used to retrieve and define an
+ alternate WCS description.
+ """
+
+ self.inst_kw = ins_spec_kw
+ self.minerr = minerr
+ self.wcskey = wcskey
+
+ if fobj is not None:
+ filename, hdr0, ehdr, phdu = getinput.parseSingleInput(f=fobj,
+ ext=ext)
+ self.filename = filename
+ instrument_name = hdr0.get('INSTRUME', 'DEFAULT')
+ if instrument_name == 'DEFAULT' or instrument_name not in list(inst_mappings.keys()):
+ #['IRAF/ARTDATA','',' ','N/A']:
+ self.instrument = 'DEFAULT'
+ else:
+ self.instrument = instrument_name
+ # Set the correct reference frame
+ refframe = determine_refframe(hdr0)
+ ehdr['RADESYS'] = refframe
+
+ WCS.__init__(self, ehdr, fobj=phdu, minerr=self.minerr,
+ key=self.wcskey)
+ if self.instrument == 'DEFAULT':
+ self.pc2cd()
+ # If input was a `astropy.io.fits.HDUList` object, it's the user's
+ # responsibility to close it, otherwise, it's closed here.
+ if not isinstance(fobj, fits.HDUList):
+ phdu.close()
+ self.setInstrSpecKw(hdr0, ehdr)
+ self.readIDCCoeffs(ehdr)
+ extname = ehdr.get('EXTNAME', '')
+ extnum = ehdr.get('EXTVER', None)
+ self.extname = (extname, extnum)
+ else:
+ # create a default HSTWCS object
+ self.instrument = 'DEFAULT'
+ WCS.__init__(self, minerr=self.minerr, key=self.wcskey)
+ self.pc2cd()
+ self.setInstrSpecKw()
+ self.setPscale()
+ self.setOrient()
+
+ @property
+ def naxis1(self):
+ return self._naxis1
+
+ @naxis1.setter
+ def naxis1(self, value):
+ self._naxis1 = value
+
+ @property
+ def naxis2(self):
+ return self._naxis2
+
+ @naxis2.setter
+ def naxis2(self, value):
+ self._naxis2 = value
+
+ def readIDCCoeffs(self, header):
+ """
+ Reads in first order IDCTAB coefficients if present in the header
+ """
+ coeffs = ['ocx10', 'ocx11', 'ocy10', 'ocy11', 'idcscale',
+ 'idcv2ref','idcv3ref', 'idctheta']
+ for c in coeffs:
+ self.__setattr__(c, header.get(c, None))
+
+ def setInstrSpecKw(self, prim_hdr=None, ext_hdr=None):
+ """
+ Populate the instrument specific attributes:
+
+ These can be in different headers but each instrument class has knowledge
+ of where to look for them.
+
+ Parameters
+ ----------
+ prim_hdr : `astropy.io.fits.Header`
+ primary header
+ ext_hdr : `astropy.io.fits.Header`
+ extension header
+
+ """
+ if self.instrument in list(inst_mappings.keys()):
+ inst_kl = inst_mappings[self.instrument]
+ inst_kl = instruments.__dict__[inst_kl]
+ insobj = inst_kl(prim_hdr, ext_hdr)
+
+ for key in self.inst_kw:
+ try:
+ self.__setattr__(key, insobj.__getattribute__(key))
+ except AttributeError:
+ # Some of the instrument's attributes are recorded in the primary header and
+ # were already set, (e.g. 'DETECTOR'), the code below is a check for that case.
+ if not self.__getattribute__(key):
+ raise
+ else:
+ pass
+
+ else:
+ raise KeyError("Unsupported instrument - %s" %self.instrument)
+
+ def setPscale(self):
+ """
+ Calculates the plate scale from the CD matrix
+ """
+ try:
+ cd11 = self.wcs.cd[0][0]
+ cd21 = self.wcs.cd[1][0]
+ self.pscale = np.sqrt(np.power(cd11,2)+np.power(cd21,2)) * 3600.
+ except AttributeError:
+ if self.wcs.has_cd():
+ print("This file has a PC matrix. You may want to convert it \n \
+ to a CD matrix, if reasonable, by running pc2.cd() method.\n \
+ The plate scale can be set then by calling setPscale() method.\n")
+ self.pscale = None
+
+ def setOrient(self):
+ """
+ Computes ORIENTAT from the CD matrix
+ """
+ try:
+ cd12 = self.wcs.cd[0][1]
+ cd22 = self.wcs.cd[1][1]
+ self.orientat = np.rad2deg(np.arctan2(cd12,cd22))
+ except AttributeError:
+ if self.wcs.has_cd():
+ print("This file has a PC matrix. You may want to convert it \n \
+ to a CD matrix, if reasonable, by running pc2.cd() method.\n \
+ The orientation can be set then by calling setOrient() method.\n")
+ self.pscale = None
+
+ def updatePscale(self, scale):
+ """
+ Updates the CD matrix with a new plate scale
+ """
+ self.wcs.cd = self.wcs.cd/self.pscale*scale
+ self.setPscale()
+
+ def readModel(self, update=False, header=None):
+ """
+ Reads distortion model from IDCTAB.
+
+ If IDCTAB is not found ('N/A', "", or not found on disk), then
+ if SIP coefficients and first order IDCTAB coefficients are present
+ in the header, restore the idcmodel from the header.
+ If not - assign None to self.idcmodel.
+
+ Parameters
+ ----------
+ header : `astropy.io.fits.Header`
+ fits extension header
+ update : bool (False)
+ if True - record the following IDCTAB quantities as header keywords:
+ CX10, CX11, CY10, CY11, IDCSCALE, IDCTHETA, IDCXREF, IDCYREF,
+ IDCV2REF, IDCV3REF
+ """
+ if self.idctab in [None, '', ' ','N/A']:
+ #Keyword idctab is not present in header - check for sip coefficients
+ if header is not None and 'IDCSCALE' in header:
+ self._readModelFromHeader(header)
+ else:
+ print("Distortion model is not available: IDCTAB=None\n")
+ self.idcmodel = None
+ elif not os.path.exists(fileutil.osfn(self.idctab)):
+ if header is not None and 'IDCSCALE' in header:
+ self._readModelFromHeader(header)
+ else:
+ print('Distortion model is not available: IDCTAB file %s not found\n' % self.idctab)
+ self.idcmodel = None
+ else:
+ self.readModelFromIDCTAB(header=header, update=update)
+
+ def _readModelFromHeader(self, header):
+ # Recreate idc model from SIP coefficients and header kw
+ print('Restoring IDC model from SIP coefficients\n')
+ model = models.GeometryModel()
+ cx, cy = coeff_converter.sip2idc(self)
+ model.cx = cx
+ model.cy = cy
+ model.name = "sip"
+ model.norder = header['A_ORDER']
+
+ refpix = {}
+ refpix['XREF'] = header['IDCXREF']
+ refpix['YREF'] = header['IDCYREF']
+ refpix['PSCALE'] = header['IDCSCALE']
+ refpix['V2REF'] = header['IDCV2REF']
+ refpix['V3REF'] = header['IDCV3REF']
+ refpix['THETA'] = header['IDCTHETA']
+ model.refpix = refpix
+
+ self.idcmodel = model
+
+
+ def readModelFromIDCTAB(self, header=None, update=False):
+ """
+ Read distortion model from idc table.
+
+ Parameters
+ ----------
+ header : `astropy.io.fits.Header`
+ fits extension header
+ update : booln (False)
+ if True - save teh following as header keywords:
+ CX10, CX11, CY10, CY11, IDCSCALE, IDCTHETA, IDCXREF, IDCYREF,
+ IDCV2REF, IDCV3REF
+
+ """
+ if self.date_obs == None:
+ print('date_obs not available\n')
+ self.idcmodel = None
+ return
+ if self.filter1 == None and self.filter2 == None:
+ 'No filter information available\n'
+ self.idcmodel = None
+ return
+
+ self.idcmodel = models.IDCModel(self.idctab,
+ chip=self.chip, direction='forward', date=self.date_obs,
+ filter1=self.filter1, filter2=self.filter2,
+ offtab=self.offtab, binned=self.binned)
+
+ if self.ltv1 != 0. or self.ltv2 != 0.:
+ self.resetLTV()
+
+ if update:
+ if header==None:
+ print('Update header with IDC model kw requested but header was not provided\n.')
+ else:
+ self._updatehdr(header)
+
+ def resetLTV(self):
+ """
+ Reset LTV values for polarizer data
+
+ The polarizer field is smaller than the detector field.
+ The distortion coefficients are defined for the entire
+ polarizer field and the LTV values are set as with subarray
+ data. This may also be true for other special filters.
+ This is a special case when the observation is considered
+ a subarray in terms of detector field but a full frame in
+ terms of distortion model.
+ To avoid shifting the distortion coefficients the LTV values
+ are reset to 0.
+ """
+ if self.naxis1 == self.idcmodel.refpix['XSIZE'] and \
+ self.naxis2 == self.idcmodel.refpix['YSIZE']:
+ self.ltv1 = 0.
+ self.ltv2 = 0.
+
+ def wcs2header(self, sip2hdr=False, idc2hdr=True, wcskey=None, relax=False):
+ """
+ Create a `astropy.io.fits.Header` object from WCS keywords.
+
+ If the original header had a CD matrix, return a CD matrix,
+ otherwise return a PC matrix.
+
+ Parameters
+ ----------
+ sip2hdr : bool
+ If True - include SIP coefficients
+ """
+
+ h = self.to_header(key=wcskey, relax=relax)
+ if not wcskey:
+ wcskey = self.wcs.alt
+ if self.wcs.has_cd():
+ h = altwcs.pc2cd(h, key=wcskey)
+
+ if 'wcsname' not in h:
+ if self.idctab is not None:
+ wname = build_default_wcsname(self.idctab)
+ else:
+ wname = 'DEFAULT'
+ h['wcsname{0}'.format(wcskey)] = wname
+
+ if idc2hdr:
+ for card in self._idc2hdr():
+ h[card.keyword + wcskey] = (card.value, card.comment)
+ try:
+ del h['RESTFRQ']
+ del h['RESTWAV']
+ except KeyError: pass
+
+ if sip2hdr and self.sip:
+ for card in self._sip2hdr('a'):
+ h[card.keyword] = (card.value, card.comment)
+ for card in self._sip2hdr('b'):
+ h[card.keyword] = (card.value, card.comment)
+
+ try:
+ ap = self.sip.ap
+ except AssertionError:
+ ap = None
+ try:
+ bp = self.sip.bp
+ except AssertionError:
+ bp = None
+
+ if ap:
+ for card in self._sip2hdr('ap'):
+ h[card.keyword] = (card.value, card.comment)
+ if bp:
+ for card in self._sip2hdr('bp'):
+ h[card.keyword] = (card.value, card.comment)
+ return h
+
+ def _sip2hdr(self, k):
+ """
+ Get a set of SIP coefficients in the form of an array
+ and turn them into a `astropy.io.fits.Cardlist`.
+ k - one of 'a', 'b', 'ap', 'bp'
+ """
+
+ cards = [] #fits.CardList()
+ korder = self.sip.__getattribute__(k+'_order')
+ cards.append(fits.Card(keyword=k.upper()+'_ORDER', value=korder))
+ coeffs = self.sip.__getattribute__(k)
+ ind = coeffs.nonzero()
+ for i in range(len(ind[0])):
+ card = fits.Card(keyword=k.upper()+'_'+str(ind[0][i])+'_'+str(ind[1][i]),
+ value=coeffs[ind[0][i], ind[1][i]])
+ cards.append(card)
+ return cards
+
+ def _idc2hdr(self):
+ # save some of the idc coefficients
+ coeffs = ['ocx10', 'ocx11', 'ocy10', 'ocy11', 'idcscale']
+ cards = [] #fits.CardList()
+ for c in coeffs:
+ try:
+ val = self.__getattribute__(c)
+ except AttributeError:
+ continue
+ if val:
+ cards.append(fits.Card(keyword=c, value=val))
+ return cards
+
+ def pc2cd(self):
+ if not self.wcs.has_pc():
+ self.wcs.pc = self.wcs.get_pc()
+ self.wcs.cd = self.wcs.pc * self.wcs.cdelt[1]
+
+ def all_world2pix(self, *args, **kwargs):
+ """
+ all_world2pix(*arg, accuracy=1.0e-4, maxiter=20, adaptive=False, \
+detect_divergence=True, quiet=False)
+
+ Performs full inverse transformation using iterative solution
+ on full forward transformation with complete distortion model.
+
+ Parameters
+ ----------
+ accuracy : float, optional (Default = 1.0e-4)
+ Required accuracy of the solution. Iteration terminates when the
+ correction to the solution found during the previous iteration
+ is smaller (in the sence of the L2 norm) than `accuracy`\ .
+
+ maxiter : int, optional (Default = 20)
+ Maximum number of iterations allowed to reach the solution.
+
+ adaptive : bool, optional (Default = False)
+ Specifies whether to adaptively select only points that did not
+ converge to a solution whithin the required accuracy for the
+ next iteration. Default is recommended for HST as well as most
+ other instruments.
+
+ .. note::
+ The :py:meth:`all_world2pix` uses a vectorized implementation
+ of the method of consecutive approximations (see `Notes`
+ section below) in which it iterates over *all* input poits
+ *regardless* until the required accuracy has been reached for
+ *all* input points. In some cases it may be possible that
+ *almost all* points have reached the required accuracy but
+ there are only a few of input data points left for which
+ additional iterations may be needed (this depends mostly on the
+ characteristics of the geometric distortions for a given
+ instrument). In this situation it may be
+ advantageous to set `adaptive` = `True`\ in which
+ case :py:meth:`all_world2pix` will continue iterating *only* over
+ the points that have not yet converged to the required
+ accuracy. However, for the HST's ACS/WFC detector, which has
+ the strongest distortions of all HST instruments, testing has
+ shown that enabling this option would lead to a about 10-30\%
+ penalty in computational time (depending on specifics of the
+ image, geometric distortions, and number of input points to be
+ converted). Therefore, for HST instruments,
+ it is recommended to set `adaptive` = `False`\ . The only
+ danger in getting this setting wrong will be a performance
+ penalty.
+
+ .. note::
+ When `detect_divergence` is `True`\ , :py:meth:`all_world2pix` \
+ will automatically switch to the adaptive algorithm once
+ divergence has been detected.
+
+ detect_divergence : bool, optional (Default = True)
+ Specifies whether to perform a more detailed analysis of the
+ convergence to a solution. Normally :py:meth:`all_world2pix`
+ may not achieve the required accuracy
+ if either the `tolerance` or `maxiter` arguments are too low.
+ However, it may happen that for some geometric distortions
+ the conditions of convergence for the the method of consecutive
+ approximations used by :py:meth:`all_world2pix` may not be
+ satisfied, in which case consecutive approximations to the
+ solution will diverge regardless of the `tolerance` or `maxiter`
+ settings.
+
+ When `detect_divergence` is `False`\ , these divergent points
+ will be detected as not having achieved the required accuracy
+ (without further details). In addition, if `adaptive` is `False`
+ then the algorithm will not know that the solution (for specific
+ points) is diverging and will continue iterating and trying to
+ "improve" diverging solutions. This may result in NaN or Inf
+ values in the return results (in addition to a performance
+ penalties). Even when `detect_divergence` is
+ `False`\ , :py:meth:`all_world2pix`\ , at the end of the iterative
+ process, will identify invalid results (NaN or Inf) as "diverging"
+ solutions and will raise :py:class:`NoConvergence` unless
+ the `quiet` parameter is set to `True`\ .
+
+ When `detect_divergence` is `True`\ , :py:meth:`all_world2pix` will
+ detect points for
+ which current correction to the coordinates is larger than
+ the correction applied during the previous iteration **if** the
+ requested accuracy **has not yet been achieved**\ . In this case,
+ if `adaptive` is `True`, these points will be excluded from
+ further iterations and if `adaptive`
+ is `False`\ , :py:meth:`all_world2pix` will automatically
+ switch to the adaptive algorithm.
+
+ .. note::
+ When accuracy has been achieved, small increases in
+ current corrections may be possible due to rounding errors
+ (when `adaptive` is `False`\ ) and such increases
+ will be ignored.
+
+ .. note::
+ Setting `detect_divergence` to `True` will incurr about 5-10\%
+ performance penalty (in our testing on ACS/WFC images).
+ Because the benefits of enabling this feature outweigh
+ the small performance penalty, it is recommended to set
+ `detect_divergence` to `True`\ , unless extensive testing
+ of the distortion models for images from specific
+ instruments show a good stability of the numerical method
+ for a wide range of coordinates (even outside the image
+ itself).
+
+ .. note::
+ Indices of the diverging inverse solutions will be reported
+ in the `divergent` attribute of the
+ raised :py:class:`NoConvergence` object.
+
+ quiet : bool, optional (Default = False)
+ Do not throw :py:class:`NoConvergence` exceptions when the method
+ does not converge to a solution with the required accuracy
+ within a specified number of maximum iterations set by `maxiter`
+ parameter. Instead, simply return the found solution.
+
+ Raises
+ ------
+ NoConvergence
+ The method does not converge to a
+ solution with the required accuracy within a specified number
+ of maximum iterations set by the `maxiter` parameter.
+
+ Notes
+ -----
+ Inputs can either be (RA, Dec, origin) or (RADec, origin) where RA
+ and Dec are 1-D arrays/lists of coordinates and RADec is an
+ array/list of pairs of coordinates.
+
+ Using the method of consecutive approximations we iterate starting
+ with the initial approximation, which is computed using the
+ non-distorion-aware :py:meth:`wcs_world2pix` (or equivalent).
+
+ The :py:meth:`all_world2pix` function uses a vectorized implementation
+ of the method of consecutive approximations and therefore it is
+ highly efficient (>30x) when *all* data points that need to be
+ converted from sky coordinates to image coordinates are passed at
+ *once*\ . Therefore, it is advisable, whenever possible, to pass
+ as input a long array of all points that need to be converted
+ to :py:meth:`all_world2pix` instead of calling :py:meth:`all_world2pix`
+ for each data point. Also see the note to the `adaptive` parameter.
+
+ Examples
+ --------
+ >>> import stwcs
+ >>> from astropy.io import fits
+ >>> hdulist = fits.open('j94f05bgq_flt.fits')
+ >>> w = stwcs.wcsutil.HSTWCS(hdulist, ext=('sci',1))
+ >>> hdulist.close()
+
+ >>> ra, dec = w.all_pix2world([1,2,3],[1,1,1],1); print(ra); print(dec)
+ [ 5.52645241 5.52649277 5.52653313]
+ [-72.05171776 -72.05171295 -72.05170814]
+ >>> radec = w.all_pix2world([[1,1],[2,1],[3,1]],1); print(radec)
+ [[ 5.52645241 -72.05171776]
+ [ 5.52649277 -72.05171295]
+ [ 5.52653313 -72.05170814]]
+ >>> x, y = w.all_world2pix(ra,dec,1)
+ >>> print(x)
+ [ 1.00000233 2.00000232 3.00000233]
+ >>> print(y)
+ [ 0.99999997 0.99999997 0.99999998]
+ >>> xy = w.all_world2pix(radec,1)
+ >>> print(xy)
+ [[ 1.00000233 0.99999997]
+ [ 2.00000232 0.99999997]
+ [ 3.00000233 0.99999998]]
+ >>> xy = w.all_world2pix(radec,1, maxiter=3, accuracy=1.0e-10, \
+quiet=False)
+ NoConvergence: 'HSTWCS.all_world2pix' failed to converge to requested \
+accuracy after 3 iterations.
+
+ >>>
+ Now try to use some diverging data:
+ >>> divradec = w.all_pix2world([[1.0,1.0],[10000.0,50000.0],\
+[3.0,1.0]],1); print(divradec)
+ [[ 5.52645241 -72.05171776]
+ [ 7.15979392 -70.81405561]
+ [ 5.52653313 -72.05170814]]
+
+ >>> try:
+ >>> xy = w.all_world2pix(divradec,1, maxiter=20, accuracy=1.0e-4, \
+adaptive=False, detect_divergence=True, quiet=False)
+ >>> except stwcs.wcsutil.hstwcs.NoConvergence as e:
+ >>> print("Indices of diverging points: {}".format(e.divergent))
+ >>> print("Indices of poorly converging points: {}".format(e.failed2converge))
+ >>> print("Best solution: {}".format(e.best_solution))
+ >>> print("Achieved accuracy: {}".format(e.accuracy))
+ >>> raise e
+ Indices of diverging points:
+ [1]
+ Indices of poorly converging points:
+ None
+ Best solution:
+ [[ 1.00006219e+00 9.99999288e-01]
+ [ -1.99440907e+06 1.44308548e+06]
+ [ 3.00006257e+00 9.99999316e-01]]
+ Achieved accuracy:
+ [[ 5.98554253e-05 6.79918148e-07]
+ [ 8.59514088e+11 6.61703754e+11]
+ [ 6.02334592e-05 6.59713067e-07]]
+ Traceback (innermost last):
+ File "<console>", line 8, in <module>
+ NoConvergence: 'HSTWCS.all_world2pix' failed to converge to the requested accuracy.
+ After 5 iterations, the solution is diverging at least for one input point.
+
+ >>> try:
+ >>> xy = w.all_world2pix(divradec,1, maxiter=20, accuracy=1.0e-4, \
+adaptive=False, detect_divergence=False, quiet=False)
+ >>> except stwcs.wcsutil.hstwcs.NoConvergence as e:
+ >>> print("Indices of diverging points: {}".format(e.divergent))
+ >>> print("Indices of poorly converging points: {}".format(e.failed2converge))
+ >>> print("Best solution: {}".format(e.best_solution))
+ >>> print("Achieved accuracy: {}".format(e.accuracy))
+ >>> raise e
+ Indices of diverging points:
+ [1]
+ Indices of poorly converging points:
+ None
+ Best solution:
+ [[ 1. 1.]
+ [ nan nan]
+ [ 3. 1.]]
+ Achieved accuracy:
+ [[ 0. 0.]
+ [ nan nan]
+ [ 0. 0.]]
+ Traceback (innermost last):
+ File "<console>", line 8, in <module>
+ NoConvergence: 'HSTWCS.all_world2pix' failed to converge to the requested accuracy.
+ After 20 iterations, the solution is diverging at least for one input point.
+
+ """
+ #####################################################################
+ ## PROCESS ARGUMENTS: ##
+ #####################################################################
+ nargs = len(args)
+
+ if nargs == 3:
+ try:
+ ra = np.asarray(args[0], dtype=np.float64)
+ dec = np.asarray(args[1], dtype=np.float64)
+ #assert( len(ra.shape) == 1 and len(dec.shape) == 1 )
+ origin = int(args[2])
+ vect1D = True
+ except:
+ raise TypeError("When providing three arguments, they must " \
+ "be (Ra, Dec, origin) where Ra and Dec are " \
+ "Nx1 vectors.")
+ elif nargs == 2:
+ try:
+ rd = np.asarray(args[0], dtype=np.float64)
+ #assert( rd.shape[1] == 2 )
+ ra = rd[:,0]
+ dec = rd[:,1]
+ origin = int(args[1])
+ vect1D = False
+ except:
+ raise TypeError("When providing two arguments, they must " \
+ "be (RaDec, origin) where RaDec is a Nx2 array.")
+ else:
+ raise TypeError("Expected 2 or 3 arguments, {:d} given." \
+ .format(nargs))
+
+ # process optional arguments:
+ accuracy = kwargs.pop('accuracy', 1.0e-4)
+ maxiter = kwargs.pop('maxiter', 20)
+ adaptive = kwargs.pop('adaptive', False)
+ detect_divergence = kwargs.pop('detect_divergence', True)
+ quiet = kwargs.pop('quiet', False)
+
+ #####################################################################
+ ## INITIALIZE ITERATIVE PROCESS: ##
+ #####################################################################
+ x0, y0 = self.wcs_world2pix(ra, dec, origin) # <-- initial approximation
+ # (WCS based only)
+
+ # see if an iterative solution is required (when any of the
+ # non-CD-matrix corrections are present). If not required
+ # return initial approximation (x0, y0).
+ if self.sip is None and \
+ self.cpdis1 is None and self.cpdis2 is None and \
+ self.det2im1 is None and self.det2im2 is None:
+ # no non-WCS corrections are detected - return
+ # initial approximation
+ if vect1D:
+ return [x0, y0]
+ else:
+ return np.dstack([x0,y0])[0]
+
+ x = x0.copy() # 0-order solution
+ y = y0.copy() # 0-order solution
+
+ # initial correction:
+ dx, dy = self.pix2foc(x, y, origin)
+ # If pix2foc does not apply all the required distortion
+ # corrections then replace the above line with:
+ #r0, d0 = self.all_pix2world(x, y, origin)
+ #dx, dy = self.wcs_world2pix(r0, d0, origin )
+ dx -= x0
+ dy -= y0
+
+ # update initial solution:
+ x -= dx
+ y -= dy
+
+ # norn (L2) squared of the correction:
+ dn2prev = dx**2+dy**2
+ dn2 = dn2prev
+
+ # prepare for iterative process
+ iterlist = list(range(1, maxiter+1))
+ accuracy2 = accuracy**2
+ ind = None
+ inddiv = None
+
+ npts = x.shape[0]
+
+ # turn off numpy runtime warnings for 'invalid' and 'over':
+ old_invalid = np.geterr()['invalid']
+ old_over = np.geterr()['over']
+ np.seterr(invalid = 'ignore', over = 'ignore')
+
+ #####################################################################
+ ## NON-ADAPTIVE ITERATIONS: ##
+ #####################################################################
+ if not adaptive:
+ for k in iterlist:
+ # check convergence:
+ if np.max(dn2) < accuracy2:
+ break
+
+ # find correction to the previous solution:
+ dx, dy = self.pix2foc(x, y, origin)
+ # If pix2foc does not apply all the required distortion
+ # corrections then replace the above line with:
+ #r0, d0 = self.all_pix2world(x, y, origin)
+ #dx, dy = self.wcs_world2pix(r0, d0, origin )
+ dx -= x0
+ dy -= y0
+
+ # update norn (L2) squared of the correction:
+ dn2 = dx**2+dy**2
+
+ # check for divergence (we do this in two stages
+ # to optimize performance for the most common
+ # scenario when succesive approximations converge):
+ if detect_divergence:
+ ind, = np.where(dn2 <= dn2prev)
+ if ind.shape[0] < npts:
+ inddiv, = np.where(
+ np.logical_and(dn2 > dn2prev, dn2 >= accuracy2))
+ if inddiv.shape[0] > 0:
+ # apply correction only to the converging points:
+ x[ind] -= dx[ind]
+ y[ind] -= dy[ind]
+ # switch to adaptive iterations:
+ ind, = np.where((dn2 >= accuracy2) & \
+ (dn2 <= dn2prev) & np.isfinite(dn2))
+ iterlist = iterlist[k:]
+ adaptive = True
+ break
+ #dn2prev[ind] = dn2[ind]
+ dn2prev = dn2
+
+ # apply correction:
+ x -= dx
+ y -= dy
+
+ #####################################################################
+ ## ADAPTIVE ITERATIONS: ##
+ #####################################################################
+ if adaptive:
+ if ind is None:
+ ind = np.asarray(list(range(npts)), dtype=np.int64)
+
+ for k in iterlist:
+ # check convergence:
+ if ind.shape[0] == 0:
+ break
+
+ # find correction to the previous solution:
+ dx[ind], dy[ind] = self.pix2foc(x[ind], y[ind], origin)
+ # If pix2foc does not apply all the required distortion
+ # corrections then replace the above line with:
+ #r0[ind], d0[ind] = self.all_pix2world(x[ind], y[ind], origin)
+ #dx[ind], dy[ind] = self.wcs_world2pix(r0[ind], d0[ind], origin)
+ dx[ind] -= x0[ind]
+ dy[ind] -= y0[ind]
+
+ # update norn (L2) squared of the correction:
+ dn2 = dx**2+dy**2
+
+ # update indices of elements that still need correction:
+ if detect_divergence:
+ ind, = np.where((dn2 >= accuracy2) & (dn2 <= dn2prev))
+ #ind = ind[np.where((dn2[ind] >= accuracy2) & (dn2[ind] <= dn2prev))]
+ dn2prev[ind] = dn2[ind]
+ else:
+ ind, = np.where(dn2 >= accuracy2)
+ #ind = ind[np.where(dn2[ind] >= accuracy2)]
+
+ # apply correction:
+ x[ind] -= dx[ind]
+ y[ind] -= dy[ind]
+
+ #####################################################################
+ ## FINAL DETECTION OF INVALID, DIVERGING, ##
+ ## AND FAILED-TO-CONVERGE POINTS ##
+ #####################################################################
+ # Identify diverging and/or invalid points:
+ invalid = (((~np.isfinite(y)) | (~np.isfinite(x)) | \
+ (~np.isfinite(dn2))) & \
+ (np.isfinite(ra)) & (np.isfinite(dec)))
+ # When detect_divergence==False, dn2prev is outdated (it is the
+ # norm^2 of the very first correction). Still better than nothing...
+ inddiv, = np.where(((dn2 >= accuracy2) & (dn2 > dn2prev)) | invalid)
+ if inddiv.shape[0] == 0:
+ inddiv = None
+ # identify points that did not converge within
+ # 'maxiter' iterations:
+ if k >= maxiter:
+ ind,= np.where((dn2 >= accuracy2) & (dn2 <= dn2prev) & (~invalid))
+ if ind.shape[0] == 0:
+ ind = None
+ else:
+ ind = None
+
+ #####################################################################
+ ## RAISE EXCEPTION IF DIVERGING OR TOO SLOWLY CONVERGING ##
+ ## DATA POINTS HAVE BEEN DETECTED: ##
+ #####################################################################
+ # raise exception if diverging or too slowly converging
+ if (ind is not None or inddiv is not None) and not quiet:
+ if vect1D:
+ sol = [x, y]
+ err = [np.abs(dx), np.abs(dy)]
+ else:
+ sol = np.dstack( [x, y] )[0]
+ err = np.dstack( [np.abs(dx), np.abs(dy)] )[0]
+
+ # restore previous numpy error settings:
+ np.seterr(invalid = old_invalid, over = old_over)
+
+ if inddiv is None:
+ raise NoConvergence("'HSTWCS.all_world2pix' failed to " \
+ "converge to the requested accuracy after {:d} " \
+ "iterations.".format(k), best_solution = sol, \
+ accuracy = err, niter = k, failed2converge = ind, \
+ divergent = None)
+ else:
+ raise NoConvergence("'HSTWCS.all_world2pix' failed to " \
+ "converge to the requested accuracy.{0:s}" \
+ "After {1:d} iterations, the solution is diverging " \
+ "at least for one input point." \
+ .format(os.linesep, k), best_solution = sol, \
+ accuracy = err, niter = k, failed2converge = ind, \
+ divergent = inddiv)
+
+ #####################################################################
+ ## FINALIZE AND FORMAT DATA FOR RETURN: ##
+ #####################################################################
+ # restore previous numpy error settings:
+ np.seterr(invalid = old_invalid, over = old_over)
+
+ if vect1D:
+ return [x, y]
+ else:
+ return np.dstack( [x, y] )[0]
+
+ def _updatehdr(self, ext_hdr):
+ #kw2add : OCX10, OCX11, OCY10, OCY11
+ # record the model in the header for use by pydrizzle
+ ext_hdr['OCX10'] = self.idcmodel.cx[1,0]
+ ext_hdr['OCX11'] = self.idcmodel.cx[1,1]
+ ext_hdr['OCY10'] = self.idcmodel.cy[1,0]
+ ext_hdr['OCY11'] = self.idcmodel.cy[1,1]
+ ext_hdr['IDCSCALE'] = self.idcmodel.refpix['PSCALE']
+ ext_hdr['IDCTHETA'] = self.idcmodel.refpix['THETA']
+ ext_hdr['IDCXREF'] = self.idcmodel.refpix['XREF']
+ ext_hdr['IDCYREF'] = self.idcmodel.refpix['YREF']
+ ext_hdr['IDCV2REF'] = self.idcmodel.refpix['V2REF']
+ ext_hdr['IDCV3REF'] = self.idcmodel.refpix['V3REF']
+
+ def printwcs(self):
+ """
+ Print the basic WCS keywords.
+ """
+ print('WCS Keywords\n')
+ print('CD_11 CD_12: %r %r' % (self.wcs.cd[0,0], self.wcs.cd[0,1]))
+ print('CD_21 CD_22: %r %r' % (self.wcs.cd[1,0], self.wcs.cd[1,1]))
+ print('CRVAL : %r %r' % (self.wcs.crval[0], self.wcs.crval[1]))
+ print('CRPIX : %r %r' % (self.wcs.crpix[0], self.wcs.crpix[1]))
+ print('NAXIS : %d %d' % (self.naxis1, self.naxis2))
+ print('Plate Scale : %r' % self.pscale)
+ print('ORIENTAT : %r' % self.orientat)
+
+
+def determine_refframe(phdr):
+ """
+ Determine the reference frame in standard FITS WCS terms.
+
+ Parameters
+ ----------
+ phdr : `astropy.io.fits.Header`
+ Primary Header of an HST observation
+
+ In HST images the reference frame is recorded in the primary extension as REFFRAME.
+ Values are "GSC1" which means FK5 or ICRS (for GSC2 observations).
+ """
+ try:
+ refframe = phdr['REFFRAME']
+ except KeyError:
+ refframe = " "
+ if refframe == "GSC1":
+ refframe = "FK5"
+ return refframe