diff options
author | Nadia Dencheva <nadia.dencheva@gmail.com> | 2016-08-04 17:42:01 -0400 |
---|---|---|
committer | Nadia Dencheva <nadia.dencheva@gmail.com> | 2016-08-04 17:42:01 -0400 |
commit | 4b65b226085ccb9e665a5866023d8114f7438188 (patch) | |
tree | 7183ed93c0624164930069bf5fedcd582dce84b6 /stwcs | |
parent | 86d1bc5a77491770d45b86e5cf18b79ded68fb9b (diff) | |
download | stwcs_hcf-4b65b226085ccb9e665a5866023d8114f7438188.tar.gz |
restructure and add stwcs tests
Diffstat (limited to 'stwcs')
65 files changed, 11540 insertions, 0 deletions
diff --git a/stwcs/__init__.py b/stwcs/__init__.py new file mode 100644 index 0000000..cd8b0e6 --- /dev/null +++ b/stwcs/__init__.py @@ -0,0 +1,32 @@ +""" STWCS + +This package provides support for WCS based distortion models and coordinate +transformation. It relies on PyWCS (based on WCSLIB). It consists of two +subpackages: updatewcs and wcsutil. + +updatewcs performs corrections to the +basic WCS and includes other distortion infomation in the science files as +header keywords or file extensions. + +Wcsutil provides an HSTWCS object which extends pywcs.WCS object and provides +HST instrument specific information as well as methods for coordinate +transformation. wcsutil also provides functions for manipulating alternate WCS +descriptions in the headers. + +""" +from __future__ import absolute_import, print_function # confidence high +import os + +from . import distortion +from stsci.tools import fileutil +from stsci.tools import teal + + +from .version import * + +try: + from . import gui + teal.print_tasknames(gui.__name__, os.path.dirname(gui.__file__)) + print('\n') +except: + print('No TEAL-based tasks available for this package!') diff --git a/stwcs/distortion/__init__.py b/stwcs/distortion/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/stwcs/distortion/__init__.py diff --git a/stwcs/distortion/coeff_converter.py b/stwcs/distortion/coeff_converter.py new file mode 100644 index 0000000..415b512 --- /dev/null +++ b/stwcs/distortion/coeff_converter.py @@ -0,0 +1,142 @@ +from __future__ import division, print_function # confidence high + +import numpy as np +from astropy.io import fits +from astropy import wcs as pywcs + +def sip2idc(wcs): + """ + Converts SIP style coefficients to IDCTAB coefficients. + + Parameters + ---------- + wcs : `astropy.io.fits.Header` or `astropy.wcs.WCS` object + """ + if isinstance(wcs, fits.Header): + ocx10 = wcs.get('OCX10', None) + ocx11 = wcs.get('OCX11', None) + ocy10 = wcs.get('OCY10', None) + ocy11 = wcs.get('OCY11', None) + order = wcs.get('A_ORDER', None) + sipa, sipb = _read_sip_kw(wcs) + if None in [ocx10, ocx11, ocy10, ocy11, sipa, sipb]: + print('Cannot convert SIP to IDC coefficients.\n') + return None, None + elif isinstance(wcs, pywcs.WCS): + try: + ocx10 = wcs.ocx10 + ocx11 = wcs.ocx11 + ocy10 = wcs.ocy10 + ocy11 = wcs.ocy11 + except AttributeError: + print('First order IDCTAB coefficients are not available.\n') + print('Cannot convert SIP to IDC coefficients.\n') + return None, None + try: + sipa = wcs.sip.a + sipb = wcs.sip.b + except AttributeError: + print('SIP coefficients are not available.') + print('Cannot convert SIP to IDC coefficients.\n') + return None, None + try: + order = wcs.sip.a_order + except AttributeError: + print('SIP model order unknown, exiting ...\n') + return None, None + + else: + print('Input to sip2idc must be a PyFITS header or a wcsutil.HSTWCS object\n') + return + + + if None in [ocx10, ocx11, ocy10, ocy11]: + print('First order IDC coefficients not found, exiting ...\n') + return None, None + idc_coeff = np.array([[ocx11, ocx10], [ocy11, ocy10]]) + cx = np.zeros((order+1,order+1), dtype=np.double) + cy = np.zeros((order+1,order+1), dtype=np.double) + for n in range(order+1): + for m in range(order+1): + if n >= m and n>=2: + sipval = np.array([[sipa[m,n-m]],[sipb[m,n-m]]]) + idcval = np.dot(idc_coeff, sipval) + cx[n,m] = idcval[0] + cy[n,m] = idcval[1] + + cx[1,0] = ocx10 + cx[1,1] = ocx11 + cy[1,0] = ocy10 + cy[1,1] = ocy11 + + return cx, cy + +def _read_sip_kw(header): + """ + Reads SIP header keywords and returns an array of coefficients. + + If no SIP header keywords are found, None is returned. + """ + if "A_ORDER" in header: + if "B_ORDER" not in header: + raise ValueError( + "A_ORDER provided without corresponding B_ORDER " + "keyword for SIP distortion") + + m = int(header["A_ORDER"]) + a = np.zeros((m+1, m+1), np.double) + for i in range(m+1): + for j in range(m-i+1): + a[i, j] = header.get("A_%d_%d" % (i, j), 0.0) + + m = int(header["B_ORDER"]) + b = np.zeros((m+1, m+1), np.double) + for i in range(m+1): + for j in range(m-i+1): + b[i, j] = header.get("B_%d_%d" % (i, j), 0.0) + elif "B_ORDER" in header: + raise ValueError( + "B_ORDER provided without corresponding A_ORDER " + "keyword for SIP distortion") + else: + a = None + b = None + + return a , b + + +""" +def idc2sip(wcsobj, idctab = None): + if isinstance(wcs,pywcs.WCS): + try: + cx10 = wcsobj.ocx10 + cx11 = wcsobj.cx11 + cy10 = wcsobj.cy10 + cy11 = wcsobj.cy11 + except AttributeError: + print + try: + order = wcs.sip.a_order + except AttributeError: + print 'SIP model order unknown, exiting ...\n' + return + else: + print 'Input to sip2idc must be a PyFITS header or a wcsutil.HSTWCS object\n' + return + + if None in [ocx10, ocx11, ocy10, ocy11]: + print 'First order IDC coefficients not found, exiting ...\n' + return + idc_coeff = np.array([[wcsobj.cx11, wcsobj.cx10], [wcsobj.cy11, wcsobj.cy10]]) + cx = numpy.zeros((order+1,order+1), dtype=numpy.double) + cy = numpy.zeros((order+1,order+1), dtype=numpy.double) + for n in range(order+1): + for m in range(order+1): + if n >= m and n>=2: + sipval = numpy.array([[wcsobj.sip.a[n,m]],[wcsobj.sip.b[n,m]]]) + idcval = numpy.dot(idc_coeff, sipval) + cx[m,n-m] = idcval[0] + cy[m,n-m] = idcval[1] + + return cx, cy +""" diff --git a/stwcs/distortion/models.py b/stwcs/distortion/models.py new file mode 100644 index 0000000..231a9f1 --- /dev/null +++ b/stwcs/distortion/models.py @@ -0,0 +1,362 @@ +from __future__ import absolute_import, division, print_function # confidence high + + +import numpy as np + +# Import PyDrizzle utility modules +from . import mutil +from .mutil import combin + +yes = True +no = False + +################# +# +# +# Geometry/Distortion Classes +# +# +################# + +class GeometryModel: + """ + Base class for Distortion model. + There will be a separate class for each type of + model/filetype used with drizzle, i.e., IDCModel and + DrizzleModel. + + Each class will know how to apply the distortion to a + single point and how to convert coefficients to an input table + suitable for the drizzle task. + + Coefficients will be stored in CX,CY arrays. + """ + # + # + # + # + # + # + # + NORDER = 3 + + def __init__(self): + " This will open the given file and determine its type and norder." + + # Method to read in coefficients from given table and + # populate the n arrays 'cx' and 'cy'. + # This will be different for each type of input file, + # IDCTAB vs. drizzle table. + + # Set these up here for all sub-classes to use... + # But, calculate norder and cx,cy arrays in detector specific classes. + self.cx = None + self.cy = None + self.refpix = None + self.norder = self.NORDER + # Keep track of computed zero-point for distortion coeffs + self.x0 = None + self.y0 = None + + # default values for these attributes + self.direction = 'forward' + + self.pscale = 1.0 + + def shift(self, xs, ys): + """ + Shift reference position of coefficients to new center + where (xs,ys) = old-reference-position - subarray/image center. + This will support creating coeffs files for drizzle which will + be applied relative to the center of the image, rather than relative + to the reference position of the chip. + """ + + _cxs = np.zeros(shape=self.cx.shape,dtype=self.cx.dtype) + _cys = np.zeros(shape=self.cy.shape,dtype=self.cy.dtype) + _k = self.norder + 1 + # loop over each input coefficient + for m in range(_k): + for n in range(_k): + if m >= n: + # For this coefficient, shift by xs/ys. + _ilist = list(range(m, _k)) + # sum from m to k + for i in _ilist: + _jlist = list(range(n, i - (m-n)+1)) + # sum from n to i-(m-n) + for j in _jlist: + _cxs[m,n] += self.cx[i,j]*combin(j,n)*combin((i-j),(m-n))*pow(xs,(j-n))*pow(ys,((i-j)-(m-n))) + _cys[m,n] += self.cy[i,j]*combin(j,n)*combin((i-j),(m-n))*pow(xs,(j-n))*pow(ys,((i-j)-(m-n))) + self.cx = _cxs.copy() + self.cy = _cys.copy() + + def convert(self, tmpname, xref=None,yref=None,delta=yes): + """ + Open up an ASCII file, output coefficients in drizzle + format after converting them as necessary. + First, normalize these coefficients to what drizzle expects + Normalize the coefficients by the MODEL/output plate scale. + + 16-May-2002: + Revised to work with higher order polynomials by John Blakeslee. + 27-June-2002: + Added ability to shift coefficients to new center for support + of subarrays. + """ + cx = self.cx/self.pscale + cy = self.cy/self.pscale + x0 = self.refpix['XDELTA'] + cx[0,0] + y0 = self.refpix['YDELTA'] + cy[0,0] + #xr = self.refpix['XREF'] + #yr = self.refpix['YREF'] + xr = self.refpix['CHIP_XREF'] + yr = self.refpix['CHIP_YREF'] + + + + ''' + if xref != None: + # Shift coefficients for use with drizzle + _xs = xref - self.refpix['XREF'] + 1.0 + _ys = yref - self.refpix['YREF'] + 1.0 + + + if _xs != 0 or _ys != 0: + cxs,cys= self.shift(cx, cy, _xs, _ys) + cx = cxs + cy = cys + + # We only want to apply this shift to coeffs + # for subarray images. + if delta == no: + cxs[0,0] = cxs[0,0] - _xs + cys[0,0] = cys[0,0] - _ys + + # Now, apply only the difference introduced by the distortion.. + # i.e., (undistorted - original) shift. + x0 += cxs[0,0] + y0 += cys[0,0] + ''' + self.x0 = x0 #+ 1.0 + self.y0 = y0 #+ 1.0 + + # Now, write out the coefficients into an ASCII + # file in 'drizzle' format. + lines = [] + + + lines.append('# Polynomial distortion coefficients\n') + lines.append('# Extracted from "%s" \n'%self.name) + lines.append('refpix %f %f \n'%(xr,yr)) + if self.norder==3: + lines.append('cubic\n') + elif self.norder==4: + lines.append('quartic\n') + elif self.norder==5: + lines.append('quintic\n') + else: + raise ValueError("Drizzle cannot handle poly distortions of order %d" % self.norder) + + str = '%16.8f %16.8g %16.8g %16.8g %16.8g \n'% (x0,cx[1,1],cx[1,0],cx[2,2],cx[2,1]) + lines.append(str) + str = '%16.8g %16.8g %16.8g %16.8g %16.8g \n'% (cx[2,0],cx[3,3],cx[3,2],cx[3,1],cx[3,0]) + lines.append(str) + if self.norder>3: + str = '%16.8g %16.8g %16.8g %16.8g %16.8g \n'% (cx[4,4],cx[4,3],cx[4,2],cx[4,1],cx[4,0]) + lines.append(str) + if self.norder>4: + str = '%16.8g %16.8g %16.8g %16.8g %16.8g %16.8g \n'% (cx[5,5],cx[5,4],cx[5,3],cx[5,2],cx[5,1],cx[5,0]) + lines.append(str) + lines.append("\n") + + str = '%16.8f %16.8g %16.8g %16.8g %16.8g \n'% (y0,cy[1,1],cy[1,0],cy[2,2],cy[2,1]) + lines.append(str) + str = '%16.8g %16.8g %16.8g %16.8g %16.8g \n'% (cy[2,0],cy[3,3],cy[3,2],cy[3,1],cy[3,0]) + lines.append(str) + if self.norder>3: + str = '%16.8g %16.8g %16.8g %16.8g %16.8g \n'% (cy[4,4],cy[4,3],cy[4,2],cy[4,1],cy[4,0]) + lines.append(str) + if self.norder>4: + str = '%16.8g %16.8g %16.8g %16.8g %16.8g %16.8g \n'% (cy[5,5],cy[5,4],cy[5,3],cy[5,2],cy[5,1],cy[5,0]) + lines.append(str) + + output = open(tmpname,'w') + output.writelines(lines) + output.close() + + + def apply(self, pixpos,scale=1.0,order=None): + """ + Apply coefficients to a pixel position or a list of positions. + This should be the same for all coefficients tables. + Return the geometrically-adjusted position + in arcseconds from the reference position as a tuple (x,y). + + Compute delta from reference position + """ + + """ + scale actually is a ratio of pscale/self.model.pscale + what is pscale? + """ + if self.cx == None: + return pixpos[:,0],pixpos[:,1] + + if order is None: + order = self.norder + + # Apply in the same way that 'drizzle' would... + _cx = self.cx / (self.pscale * scale) + _cy = self.cy / (self.pscale * scale) + _convert = no + _p = pixpos + + # Do NOT include any zero-point terms in CX,CY here + # as they should not be scaled by plate-scale like rest + # of coeffs... This makes the computations consistent + # with 'drizzle'. WJH 17-Feb-2004 + _cx[0,0] = 0. + _cy[0,0] = 0. + + if isinstance(_p, list) or isinstance(_p, tuple): + _p = np.array(_p,dtype=np.float64) + _convert = yes + + dxy = _p - (self.refpix['XREF'],self.refpix['YREF']) + # Apply coefficients from distortion model here... + c = _p * 0. + for i in range(order+1): + for j in range(i+1): + c[:,0] = c[:,0] + _cx[i][j] * pow(dxy[:,0],j) * pow(dxy[:,1],(i-j)) + c[:,1] = c[:,1] + _cy[i][j] * pow(dxy[:,0],j) * pow(dxy[:,1],(i-j)) + xc = c[:,0] + yc = c[:,1] + + # Convert results back to same form as original input + if _convert: + xc = xc.tolist() + yc = yc.tolist() + # If a single tuple was input, return just a single tuple + if len(xc) == 1: + xc = xc[0] + yc = yc[0] + + return xc,yc + + def setPScaleCoeffs(self,pscale): + self.cx[1,1] = pscale + self.cy[1,0] = pscale + + self.refpix['PSCALE'] = pscale + self.pscale = pscale + + +class IDCModel(GeometryModel): + """ + This class will open the IDCTAB, select proper row based on + chip/direction and populate cx,cy arrays. + We also need to read in SCALE, XCOM,YCOM, XREF,YREF as well. + """ + def __init__(self, idcfile, date=None, chip=1, direction='forward', + filter1='CLEAR1',filter2='CLEAR2',offtab=None, binned=1): + GeometryModel.__init__(self) + # + # Norder must be derived from the coeffs file itself, + # then the arrays can be setup. Thus, it needs to be + # done in the sub-class, not in the base class. + # Read in table. + # Populate cx,cy,scale, and other variables here. + # + self.name = idcfile + self.cx,self.cy,self.refpix,self.norder = mutil.readIDCtab(idcfile, + chip=chip,direction=direction,filter1=filter1,filter2=filter2, + date=date, offtab=offtab) + + if 'empty_model' in self.refpix and self.refpix['empty_model']: + pass + else: + self.refpix['PSCALE'] = self.refpix['PSCALE'] * binned + self.cx = self.cx * binned + self.cy = self.cy * binned + self.refpix['XREF'] = self.refpix['XREF'] / binned + self.refpix['YREF'] = self.refpix['YREF'] / binned + self.refpix['XSIZE'] = self.refpix['XSIZE'] / binned + self.refpix['YSIZE'] = self.refpix['YSIZE'] / binned + + self.pscale = self.refpix['PSCALE'] + + +class WCSModel(GeometryModel): + """ + This class sets up a distortion model based on coefficients + found in the image header. + """ + def __init__(self,header,rootname): + GeometryModel.__init__(self) + + + if 'rootname' in header: + self.name = header['rootname'] + else: + self.name = rootname + # Initialize all necessary distortion arrays with + # default model... + #self.cx,self.cy,self.refpix,self.order = mutil.defaultModel() + + # Read in values from header, and update distortion arrays. + self.cx,self.cy,self.refpix,self.norder = mutil.readWCSCoeffs(header) + + self.pscale = self.refpix['PSCALE'] + + + +class DrizzleModel(GeometryModel): + """ + This class will read in an ASCII Cubic + drizzle coeffs file and populate the cx,cy arrays. + """ + + def __init__(self, idcfile, scale = None): + GeometryModel.__init__(self) + # + # We now need to read in the file, populate cx,cy, and + # other variables as necessary. + # + self.name = idcfile + self.cx,self.cy,self.refpix,self.norder = mutil.readCubicTable(idcfile) + + # scale is the ratio wcs.pscale/model.pscale. + # model.pscale for WFPC2 is passed from REFDATA. + # This is needed for WFPC2 binned data. + + if scale != None: + self.pscale = scale + else: + self.pscale = self.refpix['PSCALE'] + + """ + The above definition looks wrong. + In one case it's a ratio in the other it's pscale. + + """ + +class TraugerModel(GeometryModel): + """ + This class will read in the ASCII Trauger coeffs + file, convert them to SIAF coefficients, then populate + the cx,cy arrays. + """ + NORDER = 3 + + def __init__(self, idcfile,lam): + GeometryModel.__init__(self) + self.name = idcfile + self.cx,self.cy,self.refpix,self.norder = mutil.readTraugerTable(idcfile,lam) + self.pscale = self.refpix['PSCALE'] + # + # Read in file here. + # Populate cx,cy, and other variables. + # + + diff --git a/stwcs/distortion/mutil.py b/stwcs/distortion/mutil.py new file mode 100644 index 0000000..ed6a1ea --- /dev/null +++ b/stwcs/distortion/mutil.py @@ -0,0 +1,703 @@ +from __future__ import division, print_function # confidence high + +from stsci.tools import fileutil +import numpy as np +import calendar + +# Set up IRAF-compatible Boolean values +yes = True +no = False + +# This function read the IDC table and generates the two matrices with +# the geometric correction coefficients. +# +# INPUT: FITS object of open IDC table +# OUTPUT: coefficient matrices for Fx and Fy +# +#### If 'tabname' == None: This should return a default, undistorted +#### solution. +# + +def readIDCtab (tabname, chip=1, date=None, direction='forward', + filter1=None,filter2=None, offtab=None): + + """ + Read IDCTAB, and optional OFFTAB if sepcified, and generate + the two matrices with the geometric correction coefficients. + + If tabname == None, then return a default, undistorted solution. + If offtab is specified, dateobs also needs to be given. + + """ + + # Return a default geometry model if no IDCTAB filename + # is given. This model will not distort the data in any way. + if tabname == None: + print('Warning: No IDCTAB specified! No distortion correction will be applied.') + return defaultModel() + + # Implement default values for filters here to avoid the default + # being overwritten by values of None passed by user. + if filter1 == None or filter1.find('CLEAR') == 0 or filter1.strip() == '': + filter1 = 'CLEAR' + if filter2 == None or filter2.find('CLEAR') == 0 or filter2.strip() == '': + filter2 = 'CLEAR' + + # Insure that tabname is full filename with fully expanded + # IRAF variables; i.e. 'jref$mc41442gj_idc.fits' should get + # expanded to '/data/cdbs7/jref/mc41442gj_idc.fits' before + # being used here. + # Open up IDC table now... + try: + ftab = fileutil.openImage(tabname) + except: + err_str = "------------------------------------------------------------------------ \n" + err_str += "WARNING: the IDCTAB geometric distortion file specified in the image \n" + err_str += "header was not found on disk. Please verify that your environment \n" + err_str += "variable ('jref'/'uref'/'oref'/'nref') has been correctly defined. If \n" + err_str += "you do not have the IDCTAB file, you may obtain the latest version \n" + err_str += "of it from the relevant instrument page on the STScI HST website: \n" + err_str += "http://www.stsci.edu/hst/ For WFPC2, STIS and NICMOS data, the \n" + err_str += "present run will continue using the old coefficients provided in \n" + err_str += "the Dither Package (ca. 1995-1998). \n" + err_str += "------------------------------------------------------------------------ \n" + raise IOError(err_str) + + #First thing we need, is to read in the coefficients from the IDC + # table and populate the Fx and Fy matrices. + + if 'DETECTOR' in ftab['PRIMARY'].header: + detector = ftab['PRIMARY'].header['DETECTOR'] + else: + if 'CAMERA' in ftab['PRIMARY'].header: + detector = str(ftab['PRIMARY'].header['CAMERA']) + else: + detector = 1 + # First, read in TDD coeffs if present + phdr = ftab['PRIMARY'].header + instrument = phdr['INSTRUME'] + if instrument == 'ACS' and detector == 'WFC': + skew_coeffs = read_tdd_coeffs(phdr, chip=chip) + else: + skew_coeffs = None + + # Set default filters for SBC + if detector == 'SBC': + if filter1 == 'CLEAR': + filter1 = 'F115LP' + filter2 = 'N/A' + if filter2 == 'CLEAR': + filter2 = 'N/A' + + # Read FITS header to determine order of fit, i.e. k + norder = ftab['PRIMARY'].header['NORDER'] + if norder < 3: + order = 3 + else: + order = norder + + fx = np.zeros(shape=(order+1,order+1),dtype=np.float64) + fy = np.zeros(shape=(order+1,order+1),dtype=np.float64) + + #Determine row from which to get the coefficients. + # How many rows do we have in the table... + fshape = ftab[1].data.shape + colnames = ftab[1].data.names + row = -1 + + # Loop over all the rows looking for the one which corresponds + # to the value of CCDCHIP we are working on... + for i in range(fshape[0]): + + try: + # Match FILTER combo to appropriate row, + #if there is a filter column in the IDCTAB... + if 'FILTER1' in colnames and 'FILTER2' in colnames: + + filt1 = ftab[1].data.field('FILTER1')[i] + if filt1.find('CLEAR') > -1: filt1 = filt1[:5] + + filt2 = ftab[1].data.field('FILTER2')[i] + if filt2.find('CLEAR') > -1: filt2 = filt2[:5] + else: + if 'OPT_ELEM' in colnames: + filt1 = ftab[1].data.field('OPT_ELEM') + if filt1.find('CLEAR') > -1: filt1 = filt1[:5] + else: + filt1 = filter1 + + if 'FILTER' in colnames: + _filt = ftab[1].data.field('FILTER')[i] + if _filt.find('CLEAR') > -1: _filt = _filt[:5] + if 'OPT_ELEM' in colnames: + filt2 = _filt + else: + filt1 = _filt + filt2 = 'CLEAR' + else: + filt2 = filter2 + except: + # Otherwise assume all rows apply and compare to input filters... + filt1 = filter1 + filt2 = filter2 + + if 'DETCHIP' in colnames: + detchip = ftab[1].data.field('DETCHIP')[i] + if not str(detchip).isdigit(): + detchip = 1 + else: + detchip = 1 + + if 'DIRECTION' in colnames: + direct = ftab[1].data.field('DIRECTION')[i].lower().strip() + else: + direct = 'forward' + + if filt1 == filter1.strip() and filt2 == filter2.strip(): + if direct == direction.strip(): + if int(detchip) == int(chip) or int(detchip) == -999: + row = i + break + + joinstr = ',' + if 'CLEAR' in filter1: + f1str = '' + joinstr = '' + else: + f1str = filter1.strip() + if 'CLEAR' in filter2: + f2str = '' + joinstr = '' + else: + f2str = filter2.strip() + filtstr = (joinstr.join([f1str,f2str])).strip() + if row < 0: + err_str = '\nProblem finding row in IDCTAB! Could not find row matching:\n' + err_str += ' CHIP: '+str(detchip)+'\n' + err_str += ' FILTERS: '+filtstr+'\n' + ftab.close() + del ftab + raise LookupError(err_str) + else: + print('- IDCTAB: Distortion model from row',str(row+1),'for chip',detchip,':',filtstr) + + # Read in V2REF and V3REF: this can either come from current table, + # or from an OFFTAB if time-dependent (i.e., for WFPC2) + theta = None + if 'V2REF' in colnames: + v2ref = ftab[1].data.field('V2REF')[row] + v3ref = ftab[1].data.field('V3REF')[row] + else: + # Read V2REF/V3REF from offset table (OFFTAB) + if offtab: + v2ref,v3ref,theta = readOfftab(offtab, date, chip=detchip) + else: + v2ref = 0.0 + v3ref = 0.0 + + if theta == None: + if 'THETA' in colnames: + theta = ftab[1].data.field('THETA')[row] + else: + theta = 0.0 + + refpix = {} + refpix['XREF'] = ftab[1].data.field('XREF')[row] + refpix['YREF'] = ftab[1].data.field('YREF')[row] + refpix['XSIZE'] = ftab[1].data.field('XSIZE')[row] + refpix['YSIZE'] = ftab[1].data.field('YSIZE')[row] + refpix['PSCALE'] = round(ftab[1].data.field('SCALE')[row],8) + refpix['V2REF'] = v2ref + refpix['V3REF'] = v3ref + refpix['THETA'] = theta + refpix['XDELTA'] = 0.0 + refpix['YDELTA'] = 0.0 + refpix['DEFAULT_SCALE'] = yes + refpix['centered'] = no + refpix['skew_coeffs'] = skew_coeffs + # Now that we know which row to look at, read coefficients into the + # numeric arrays we have set up... + # Setup which column name convention the IDCTAB follows + # either: A,B or CX,CY + if 'CX10' in ftab[1].data.names: + cxstr = 'CX' + cystr = 'CY' + else: + cxstr = 'A' + cystr = 'B' + + for i in range(norder+1): + if i > 0: + for j in range(i+1): + xcname = cxstr+str(i)+str(j) + ycname = cystr+str(i)+str(j) + fx[i,j] = ftab[1].data.field(xcname)[row] + fy[i,j] = ftab[1].data.field(ycname)[row] + + ftab.close() + del ftab + + # If CX11 is 1.0 and not equal to the PSCALE, then the + # coeffs need to be scaled + + if fx[1,1] == 1.0 and abs(fx[1,1]) != refpix['PSCALE']: + fx *= refpix['PSCALE'] + fy *= refpix['PSCALE'] + + # Return arrays and polynomial order read in from table. + # NOTE: XREF and YREF are stored in Fx,Fy arrays respectively. + return fx,fy,refpix,order +# +# +# Time-dependent skew correction coefficients (only ACS/WFC) +# +# +def read_tdd_coeffs(phdr, chip=1): + ''' Read in the TDD related keywords from the PRIMARY header of the IDCTAB + ''' + # Insure we have an integer form of chip + ic = int(chip) + + skew_coeffs = {} + skew_coeffs['TDDORDER'] = 0 + skew_coeffs['TDD_DATE'] = "" + skew_coeffs['TDD_A'] = None + skew_coeffs['TDD_B'] = None + skew_coeffs['TDD_CY_BETA'] = None + skew_coeffs['TDD_CY_ALPHA'] = None + skew_coeffs['TDD_CX_BETA'] = None + skew_coeffs['TDD_CX_ALPHA'] = None + + # Skew-based TDD coefficients + skew_terms = ['TDD_CTB','TDD_CTA','TDD_CYA','TDD_CYB','TDD_CXA','TDD_CXB'] + for s in skew_terms: + skew_coeffs[s] = None + + if "TDD_CTB1" in phdr: + # We have the 2015-calibrated TDD correction to apply + # This correction is based on correcting the skew in the linear terms + # not just set polynomial terms + print("Using 2015-calibrated VAFACTOR-corrected TDD correction...") + skew_coeffs['TDD_DATE'] = phdr['TDD_DATE'] + for s in skew_terms: + skew_coeffs[s] = phdr.get('{0}{1}'.format(s,ic),None) + + elif "TDD_CYB1" in phdr: + # We have 2014-calibrated TDD correction to apply, not J.A.-derived values + print("Using 2014-calibrated TDD correction...") + skew_coeffs['TDD_DATE'] = phdr['TDD_DATE'] + # Read coefficients for TDD Y coefficient + cyb_kw = 'TDD_CYB{0}'.format(int(chip)) + skew_coeffs['TDD_CY_BETA'] = phdr.get(cyb_kw,None) + cya_kw = 'TDD_CYA{0}'.format(int(chip)) + tdd_cya = phdr.get(cya_kw,None) + if tdd_cya == 0 or tdd_cya == 'N/A': tdd_cya = None + skew_coeffs['TDD_CY_ALPHA'] = tdd_cya + + # Read coefficients for TDD X coefficient + cxb_kw = 'TDD_CXB{0}'.format(int(chip)) + skew_coeffs['TDD_CX_BETA'] = phdr.get(cxb_kw,None) + cxa_kw = 'TDD_CXA{0}'.format(int(chip)) + tdd_cxa = phdr.get(cxa_kw,None) + if tdd_cxa == 0 or tdd_cxa == 'N/A': tdd_cxa = None + skew_coeffs['TDD_CX_ALPHA'] = tdd_cxa + + else: + if "TDDORDER" in phdr: + n = int(phdr["TDDORDER"]) + else: + print('TDDORDER kw not present, using default TDD correction') + return None + + a = np.zeros((n+1,), np.float64) + b = np.zeros((n+1,), np.float64) + for i in range(n+1): + a[i] = phdr.get(("TDD_A%d" % i), 0.0) + b[i] = phdr.get(("TDD_B%d" % i), 0.0) + if (a==0).all() and (b==0).all(): + print('Warning: TDD_A and TDD_B coeffiecients have values of 0, \n \ + but TDDORDER is %d.' % TDDORDER) + + skew_coeffs['TDDORDER'] = n + skew_coeffs['TDD_DATE'] = phdr['TDD_DATE'] + skew_coeffs['TDD_A'] = a + skew_coeffs['TDD_B'] = b + + return skew_coeffs + +def readOfftab(offtab, date, chip=None): + + +#Read V2REF,V3REF from a specified offset table (OFFTAB). +# Return a default geometry model if no IDCTAB filenam e +# is given. This model will not distort the data in any way. + + if offtab == None: + return 0.,0. + + # Provide a default value for chip + if chip: + detchip = chip + else: + detchip = 1 + + # Open up IDC table now... + try: + ftab = fileutil.openImage(offtab) + except: + raise IOError("Offset table '%s' not valid as specified!" % offtab) + + #Determine row from which to get the coefficients. + # How many rows do we have in the table... + fshape = ftab[1].data.shape + colnames = ftab[1].data.names + row = -1 + + row_start = None + row_end = None + + v2end = None + v3end = None + date_end = None + theta_end = None + + num_date = convertDate(date) + # Loop over all the rows looking for the one which corresponds + # to the value of CCDCHIP we are working on... + for ri in range(fshape[0]): + i = fshape[0] - ri - 1 + if 'DETCHIP' in colnames: + detchip = ftab[1].data.field('DETCHIP')[i] + else: + detchip = 1 + + obsdate = convertDate(ftab[1].data.field('OBSDATE')[i]) + + # If the row is appropriate for the chip... + # Interpolate between dates + if int(detchip) == int(chip) or int(detchip) == -999: + if num_date <= obsdate: + date_end = obsdate + v2end = ftab[1].data.field('V2REF')[i] + v3end = ftab[1].data.field('V3REF')[i] + theta_end = ftab[1].data.field('THETA')[i] + row_end = i + continue + + if row_end == None and (num_date > obsdate): + date_end = obsdate + v2end = ftab[1].data.field('V2REF')[i] + v3end = ftab[1].data.field('V3REF')[i] + theta_end = ftab[1].data.field('THETA')[i] + row_end = i + continue + + if num_date > obsdate: + date_start = obsdate + v2start = ftab[1].data.field('V2REF')[i] + v3start = ftab[1].data.field('V3REF')[i] + theta_start = ftab[1].data.field('THETA')[i] + row_start = i + break + + ftab.close() + del ftab + + if row_start == None and row_end == None: + print('Row corresponding to DETCHIP of ',detchip,' was not found!') + raise LookupError + elif row_start == None: + print('- OFFTAB: Offset defined by row',str(row_end+1)) + else: + print('- OFFTAB: Offset interpolated from rows',str(row_start+1),'and',str(row_end+1)) + + # Now, do the interpolation for v2ref, v3ref, and theta + if row_start == None or row_end == row_start: + # We are processing an observation taken after the last calibration + date_start = date_end + v2start = v2end + v3start = v3end + _fraction = 0. + theta_start = theta_end + else: + _fraction = float((num_date - date_start)) / float((date_end - date_start)) + + v2ref = _fraction * (v2end - v2start) + v2start + v3ref = _fraction * (v3end - v3start) + v3start + theta = _fraction * (theta_end - theta_start) + theta_start + + return v2ref,v3ref,theta + +def readWCSCoeffs(header): + + #Read distortion coeffs from WCS header keywords and + #populate distortion coeffs arrays. + + # Read in order for polynomials + _xorder = header['a_order'] + _yorder = header['b_order'] + order = max(max(_xorder,_yorder),3) + + fx = np.zeros(shape=(order+1,order+1),dtype=np.float64) + fy = np.zeros(shape=(order+1,order+1),dtype=np.float64) + + # Read in CD matrix + _cd11 = header['cd1_1'] + _cd12 = header['cd1_2'] + _cd21 = header['cd2_1'] + _cd22 = header['cd2_2'] + _cdmat = np.array([[_cd11,_cd12],[_cd21,_cd22]]) + _theta = np.arctan2(-_cd12,_cd22) + _rotmat = np.array([[np.cos(_theta),np.sin(_theta)], + [-np.sin(_theta),np.cos(_theta)]]) + _rCD = np.dot(_rotmat,_cdmat) + _skew = np.arcsin(-_rCD[1][0] / _rCD[0][0]) + _scale = _rCD[0][0] * np.cos(_skew) * 3600. + _scale2 = _rCD[1][1] * 3600. + + # Set up refpix + refpix = {} + refpix['XREF'] = header['crpix1'] + refpix['YREF'] = header['crpix2'] + refpix['XSIZE'] = header['naxis1'] + refpix['YSIZE'] = header['naxis2'] + refpix['PSCALE'] = _scale + refpix['V2REF'] = 0. + refpix['V3REF'] = 0. + refpix['THETA'] = np.rad2deg(_theta) + refpix['XDELTA'] = 0.0 + refpix['YDELTA'] = 0.0 + refpix['DEFAULT_SCALE'] = yes + refpix['centered'] = yes + + + # Set up template for coeffs keyword names + cxstr = 'A_' + cystr = 'B_' + # Read coeffs into their own matrix + for i in range(_xorder+1): + for j in range(i+1): + xcname = cxstr+str(j)+'_'+str(i-j) + if xcname in header: + fx[i,j] = header[xcname] + + # Extract Y coeffs separately as a different order may + # have been used to fit it. + for i in range(_yorder+1): + for j in range(i+1): + ycname = cystr+str(j)+'_'+str(i-j) + if ycname in header: + fy[i,j] = header[ycname] + + # Now set the linear terms + fx[0][0] = 1.0 + fy[0][0] = 1.0 + + return fx,fy,refpix,order + + +def readTraugerTable(idcfile,wavelength): + + # Return a default geometry model if no coefficients filename + # is given. This model will not distort the data in any way. + if idcfile == None: + return fileutil.defaultModel() + + # Trauger coefficients only result in a cubic file... + order = 3 + numco = 10 + a_coeffs = [0] * numco + b_coeffs = [0] * numco + indx = _MgF2(wavelength) + + ifile = open(idcfile,'r') + # Search for the first line of the coefficients + _line = fileutil.rAsciiLine(ifile) + while _line[:7].lower() != 'trauger': + _line = fileutil.rAsciiLine(ifile) + # Read in each row of coefficients,split them into their values, + # and convert them into cubic coefficients based on + # index of refraction value for the given wavelength + # Build X coefficients from first 10 rows of Trauger coefficients + j = 0 + while j < 20: + _line = fileutil.rAsciiLine(ifile) + if _line == '': continue + _lc = _line.split() + if j < 10: + a_coeffs[j] = float(_lc[0])+float(_lc[1])*(indx-1.5)+float(_lc[2])*(indx-1.5)**2 + else: + b_coeffs[j-10] = float(_lc[0])+float(_lc[1])*(indx-1.5)+float(_lc[2])*(indx-1.5)**2 + j = j + 1 + + ifile.close() + del ifile + + # Now, convert the coefficients into a Numeric array + # with the right coefficients in the right place. + # Populate output values now... + fx = np.zeros(shape=(order+1,order+1),dtype=np.float64) + fy = np.zeros(shape=(order+1,order+1),dtype=np.float64) + # Assign the coefficients to their array positions + fx[0,0] = 0. + fx[1] = np.array([a_coeffs[2],a_coeffs[1],0.,0.],dtype=np.float64) + fx[2] = np.array([a_coeffs[5],a_coeffs[4],a_coeffs[3],0.],dtype=np.float64) + fx[3] = np.array([a_coeffs[9],a_coeffs[8],a_coeffs[7],a_coeffs[6]],dtype=np.float64) + fy[0,0] = 0. + fy[1] = np.array([b_coeffs[2],b_coeffs[1],0.,0.],dtype=np.float64) + fy[2] = np.array([b_coeffs[5],b_coeffs[4],b_coeffs[3],0.],dtype=np.float64) + fy[3] = np.array([b_coeffs[9],b_coeffs[8],b_coeffs[7],b_coeffs[6]],dtype=np.float64) + + # Used in Pattern.computeOffsets() + refpix = {} + refpix['XREF'] = None + refpix['YREF'] = None + refpix['V2REF'] = None + refpix['V3REF'] = None + refpix['XDELTA'] = 0. + refpix['YDELTA'] = 0. + refpix['PSCALE'] = None + refpix['DEFAULT_SCALE'] = no + refpix['centered'] = yes + + return fx,fy,refpix,order + + +def readCubicTable(idcfile): + # Assumption: this will only be used for cubic file... + order = 3 + # Also, this function does NOT perform any scaling on + # the coefficients, it simply passes along what is found + # in the file as is... + + # Return a default geometry model if no coefficients filename + # is given. This model will not distort the data in any way. + if idcfile == None: + return fileutil.defaultModel() + + ifile = open(idcfile,'r') + # Search for the first line of the coefficients + _line = fileutil.rAsciiLine(ifile) + + _found = no + while _found == no: + if _line[:7] in ['cubic','quartic','quintic'] or _line[:4] == 'poly': + found = yes + break + _line = fileutil.rAsciiLine(ifile) + + # Read in each row of coefficients, without line breaks or newlines + # split them into their values, and create a list for A coefficients + # and another list for the B coefficients + _line = fileutil.rAsciiLine(ifile) + a_coeffs = _line.split() + + x0 = float(a_coeffs[0]) + _line = fileutil.rAsciiLine(ifile) + a_coeffs[len(a_coeffs):] = _line.split() + # Scale coefficients for use within PyDrizzle + for i in range(len(a_coeffs)): + a_coeffs[i] = float(a_coeffs[i]) + + _line = fileutil.rAsciiLine(ifile) + b_coeffs = _line.split() + y0 = float(b_coeffs[0]) + _line = fileutil.rAsciiLine(ifile) + b_coeffs[len(b_coeffs):] = _line.split() + # Scale coefficients for use within PyDrizzle + for i in range(len(b_coeffs)): + b_coeffs[i] = float(b_coeffs[i]) + + ifile.close() + del ifile + # Now, convert the coefficients into a Numeric array + # with the right coefficients in the right place. + # Populate output values now... + fx = np.zeros(shape=(order+1,order+1),dtype=np.float64) + fy = np.zeros(shape=(order+1,order+1),dtype=np.float64) + # Assign the coefficients to their array positions + fx[0,0] = 0. + fx[1] = np.array([a_coeffs[2],a_coeffs[1],0.,0.],dtype=np.float64) + fx[2] = np.array([a_coeffs[5],a_coeffs[4],a_coeffs[3],0.],dtype=np.float64) + fx[3] = np.array([a_coeffs[9],a_coeffs[8],a_coeffs[7],a_coeffs[6]],dtype=np.float64) + fy[0,0] = 0. + fy[1] = np.array([b_coeffs[2],b_coeffs[1],0.,0.],dtype=np.float64) + fy[2] = np.array([b_coeffs[5],b_coeffs[4],b_coeffs[3],0.],dtype=np.float64) + fy[3] = np.array([b_coeffs[9],b_coeffs[8],b_coeffs[7],b_coeffs[6]],dtype=np.float64) + + # Used in Pattern.computeOffsets() + refpix = {} + refpix['XREF'] = None + refpix['YREF'] = None + refpix['V2REF'] = x0 + refpix['V3REF'] = y0 + refpix['XDELTA'] = 0. + refpix['YDELTA'] = 0. + refpix['PSCALE'] = None + refpix['DEFAULT_SCALE'] = no + refpix['centered'] = yes + + return fx,fy,refpix,order + +def factorial(n): + """ Compute a factorial for integer n. """ + m = 1 + for i in range(int(n)): + m = m * (i+1) + return m + +def combin(j,n): + """ Return the combinatorial factor for j in n.""" + return (factorial(j) / (factorial(n) * factorial( (j-n) ) ) ) + + +def defaultModel(): + """ This function returns a default, non-distorting model + that can be used with the data. + """ + order = 3 + + fx = np.zeros(shape=(order+1,order+1),dtype=np.float64) + fy = np.zeros(shape=(order+1,order+1),dtype=np.float64) + + fx[1,1] = 1. + fy[1,0] = 1. + + # Used in Pattern.computeOffsets() + refpix = {} + refpix['empty_model'] = yes + refpix['XREF'] = None + refpix['YREF'] = None + refpix['V2REF'] = 0. + refpix['XSIZE'] = 0. + refpix['YSIZE'] = 0. + refpix['V3REF'] = 0. + refpix['XDELTA'] = 0. + refpix['YDELTA'] = 0. + refpix['PSCALE'] = None + refpix['DEFAULT_SCALE'] = no + refpix['THETA'] = 0. + refpix['centered'] = yes + return fx,fy,refpix,order + +# Function to compute the index of refraction for MgF2 at +# the specified wavelength for use with Trauger coefficients +def _MgF2(lam): + _sig = pow((1.0e7/lam),2) + return np.sqrt(1.0 + 2.590355e10/(5.312993e10-_sig) + + 4.4543708e9/(11.17083e9-_sig) + 4.0838897e5/(1.766361e5-_sig)) + + +def convertDate(date): + """ Converts the DATE-OBS date string into an integer of the + number of seconds since 1970.0 using calendar.timegm(). + + INPUT: DATE-OBS in format of 'YYYY-MM-DD'. + OUTPUT: Date (integer) in seconds. + """ + + _dates = date.split('-') + _val = 0 + _date_tuple = (int(_dates[0]), int(_dates[1]), int(_dates[2]), 0, 0, 0, 0, 0, 0) + + return calendar.timegm(_date_tuple) diff --git a/stwcs/distortion/utils.py b/stwcs/distortion/utils.py new file mode 100644 index 0000000..4228f62 --- /dev/null +++ b/stwcs/distortion/utils.py @@ -0,0 +1,270 @@ +from __future__ import division, print_function # confidence high + +import os + +import numpy as np +from numpy import linalg +from astropy import wcs as pywcs + +from stwcs import wcsutil +from stwcs import updatewcs +from numpy import sqrt, arctan2 +from stsci.tools import fileutil + +def output_wcs(list_of_wcsobj, ref_wcs=None, owcs=None, undistort=True): + """ + Create an output WCS. + + Parameters + ---------- + list_of_wcsobj: Python list + a list of HSTWCS objects + ref_wcs: an HSTWCS object + to be used as a reference WCS, in case outwcs is None. + if ref_wcs is None (default), the first member of the list + is used as a reference + outwcs: an HSTWCS object + the tangent plane defined by this object is used as a reference + undistort: boolean (default-True) + a flag whether to create an undistorted output WCS + """ + fra_dec = np.vstack([w.calc_footprint() for w in list_of_wcsobj]) + wcsname = list_of_wcsobj[0].wcs.name + + # This new algorithm may not be strictly necessary, but it may be more + # robust in handling regions near the poles or at 0h RA. + crval1,crval2 = computeFootprintCenter(fra_dec) + + crval = np.array([crval1,crval2], dtype=np.float64) # this value is now zero-based + if owcs is None: + if ref_wcs is None: + ref_wcs = list_of_wcsobj[0].deepcopy() + if undistort: + #outwcs = undistortWCS(ref_wcs) + outwcs = make_orthogonal_cd(ref_wcs) + else: + outwcs = ref_wcs.deepcopy() + outwcs.wcs.crval = crval + outwcs.wcs.set() + outwcs.pscale = sqrt(outwcs.wcs.cd[0,0]**2 + outwcs.wcs.cd[1,0]**2)*3600. + outwcs.orientat = arctan2(outwcs.wcs.cd[0,1],outwcs.wcs.cd[1,1]) * 180./np.pi + else: + outwcs = owcs.deepcopy() + outwcs.pscale = sqrt(outwcs.wcs.cd[0,0]**2 + outwcs.wcs.cd[1,0]**2)*3600. + outwcs.orientat = arctan2(outwcs.wcs.cd[0,1],outwcs.wcs.cd[1,1]) * 180./np.pi + + tanpix = outwcs.wcs.s2p(fra_dec, 0)['pixcrd'] + + outwcs._naxis1 = int(np.ceil(tanpix[:,0].max() - tanpix[:,0].min())) + outwcs._naxis2 = int(np.ceil(tanpix[:,1].max() - tanpix[:,1].min())) + crpix = np.array([outwcs._naxis1/2., outwcs._naxis2/2.], dtype=np.float64) + outwcs.wcs.crpix = crpix + outwcs.wcs.set() + tanpix = outwcs.wcs.s2p(fra_dec, 0)['pixcrd'] + + # shift crpix to take into account (floating-point value of) position of + # corner pixel relative to output frame size: no rounding necessary... + newcrpix = np.array([crpix[0]+tanpix[:,0].min(), crpix[1]+ + tanpix[:,1].min()]) + + newcrval = outwcs.wcs.p2s([newcrpix], 1)['world'][0] + outwcs.wcs.crval = newcrval + outwcs.wcs.set() + outwcs.wcs.name = wcsname # keep track of label for this solution + return outwcs + +def computeFootprintCenter(edges): + """ Geographic midpoint in spherical coords for points defined by footprints. + Algorithm derived from: http://www.geomidpoint.com/calculation.html + + This algorithm should be more robust against discontinuities at the poles. + """ + alpha = np.deg2rad(edges[:,0]) + dec = np.deg2rad(edges[:,1]) + + xmean = np.mean(np.cos(dec)*np.cos(alpha)) + ymean = np.mean(np.cos(dec)*np.sin(alpha)) + zmean = np.mean(np.sin(dec)) + + crval1 = np.rad2deg(np.arctan2(ymean,xmean))%360.0 + crval2 = np.rad2deg(np.arctan2(zmean,np.sqrt(xmean*xmean+ymean*ymean))) + + return crval1,crval2 + +def make_orthogonal_cd(wcs): + """ Create a perfect (square, orthogonal, undistorted) CD matrix from the + input WCS. + """ + # get determinant of the CD matrix: + cd = wcs.celestial.pixel_scale_matrix + + + if hasattr(wcs, 'idcv2ref') and wcs.idcv2ref is not None: + # Convert the PA_V3 orientation to the orientation at the aperture + # This is for the reference chip only - we use this for the + # reference tangent plane definition + # It has the same orientation as the reference chip + pv = updatewcs.makewcs.troll(wcs.pav3,wcs.wcs.crval[1],wcs.idcv2ref,wcs.idcv3ref) + # Add the chip rotation angle + if wcs.idctheta: + pv += wcs.idctheta + cs = np.cos(np.deg2rad(pv)) + sn = np.sin(np.deg2rad(pv)) + pvmat = np.dot(np.array([[cs,sn],[-sn,cs]]),wcs.parity) + rot = np.arctan2(pvmat[0,1],pvmat[1,1]) + scale = wcs.idcscale/3600. + + det = linalg.det(wcs.parity) + + else: + + det = linalg.det(cd) + + # find pixel scale: + if hasattr(wcs, 'idcscale'): + scale = (wcs.idcscale) / 3600. # HST pixel scale provided + else: + scale = np.sqrt(np.abs(det)) # find as sqrt(pixel area) + + # find Y-axis orientation: + if hasattr(wcs, 'orientat') and not ignoreHST: + rot = np.deg2rad(wcs.orientat) # use HST ORIENTAT + else: + rot = np.arctan2(wcs.wcs.cd[0,1], wcs.wcs.cd[1,1]) # angle of the Y-axis + + par = -1 if det < 0.0 else 1 + + # create a perfectly square, orthogonal WCS + sn = np.sin(rot) + cs = np.cos(rot) + orthogonal_cd = scale * np.array([[par*cs, sn], [-par*sn, cs]]) + + lin_wcsobj = pywcs.WCS() + lin_wcsobj.wcs.cd = orthogonal_cd + lin_wcsobj.wcs.set() + lin_wcsobj.orientat = arctan2(lin_wcsobj.wcs.cd[0,1],lin_wcsobj.wcs.cd[1,1]) * 180./np.pi + lin_wcsobj.pscale = sqrt(lin_wcsobj.wcs.cd[0,0]**2 + lin_wcsobj.wcs.cd[1,0]**2)*3600. + lin_wcsobj.wcs.crval = np.array([0.,0.]) + lin_wcsobj.wcs.crpix = np.array([0.,0.]) + lin_wcsobj.wcs.ctype = ['RA---TAN', 'DEC--TAN'] + lin_wcsobj.wcs.set() + + return lin_wcsobj + +def undistortWCS(wcsobj): + """ + Creates an undistorted linear WCS by applying the IDCTAB distortion model + to a 3-point square. The new ORIENTAT angle is calculated as well as the + plate scale in the undistorted frame. + """ + assert isinstance(wcsobj, pywcs.WCS) + from . import coeff_converter + + cx, cy = coeff_converter.sip2idc(wcsobj) + # cx, cy can be None because either there is no model available + # or updatewcs was not run. + if cx is None or cy is None: + if foundIDCTAB(wcsobj.idctab): + m = """IDCTAB is present but distortion model is missing. + Run updatewcs() to update the headers or + pass 'undistort=False' keyword to output_wcs().\n + """ + raise RuntimeError(m) + else: + print('Distortion model is not available, using input reference image for output WCS.\n') + return wcsobj.copy() + crpix1 = wcsobj.wcs.crpix[0] + crpix2 = wcsobj.wcs.crpix[1] + xy = np.array([(crpix1,crpix2),(crpix1+1.,crpix2),(crpix1,crpix2+1.)],dtype=np.double) + offsets = np.array([wcsobj.ltv1, wcsobj.ltv2]) + px = xy + offsets + #order = wcsobj.sip.a_order + pscale = wcsobj.idcscale + #pixref = np.array([wcsobj.sip.SIPREF1, wcsobj.sip.SIPREF2]) + + tan_pix = apply_idc(px, cx, cy, wcsobj.wcs.crpix, pscale, order=1) + xc = tan_pix[:,0] + yc = tan_pix[:,1] + am = xc[1] - xc[0] + bm = xc[2] - xc[0] + cm = yc[1] - yc[0] + dm = yc[2] - yc[0] + cd_mat = np.array([[am,bm],[cm,dm]],dtype=np.double) + + # Check the determinant for singularity + _det = (am * dm) - (bm * cm) + if ( _det == 0.0): + print('Singular matrix in updateWCS, aborting ...') + return + + lin_wcsobj = pywcs.WCS() + cd_inv = np.linalg.inv(cd_mat) + cd = np.dot(wcsobj.wcs.cd, cd_inv).astype(np.float64) + lin_wcsobj.wcs.cd = cd + lin_wcsobj.wcs.set() + lin_wcsobj.orientat = arctan2(lin_wcsobj.wcs.cd[0,1],lin_wcsobj.wcs.cd[1,1]) * 180./np.pi + lin_wcsobj.pscale = sqrt(lin_wcsobj.wcs.cd[0,0]**2 + lin_wcsobj.wcs.cd[1,0]**2)*3600. + lin_wcsobj.wcs.crval = np.array([0.,0.]) + lin_wcsobj.wcs.crpix = np.array([0.,0.]) + lin_wcsobj.wcs.ctype = ['RA---TAN', 'DEC--TAN'] + lin_wcsobj.wcs.set() + return lin_wcsobj + +def apply_idc(pixpos, cx, cy, pixref, pscale= None, order=None): + """ + Apply the IDCTAB polynomial distortion model to pixel positions. + pixpos must be already corrected for ltv1/2. + + Parameters + ---------- + pixpos: a 2D numpy array of (x,y) pixel positions to be distortion corrected + cx, cy: IDC model distortion coefficients + pixref: reference opixel position + + """ + if cx is None: + return pixpos + + if order is None: + print('Unknown order of distortion model \n') + return pixpos + if pscale is None: + print('Unknown model plate scale\n') + return pixpos + + # Apply in the same way that 'drizzle' would... + _cx = cx/pscale + _cy = cy/ pscale + _p = pixpos + + # Do NOT include any zero-point terms in CX,CY here + # as they should not be scaled by plate-scale like rest + # of coeffs... This makes the computations consistent + # with 'drizzle'. WJH 17-Feb-2004 + _cx[0,0] = 0. + _cy[0,0] = 0. + + dxy = _p - pixref + # Apply coefficients from distortion model here... + + c = _p * 0. + for i in range(order+1): + for j in range(i+1): + c[:,0] = c[:,0] + _cx[i][j] * pow(dxy[:,0],j) * pow(dxy[:,1],(i-j)) + c[:,1] = c[:,1] + _cy[i][j] * pow(dxy[:,0],j) * pow(dxy[:,1],(i-j)) + + return c + +def foundIDCTAB(idctab): + idctab_found = True + try: + idctab = fileutil.osfn(idctab) + if idctab == 'N/A' or idctab == "": + idctab_found = False + if os.path.exists(idctab): + idctab_found = True + else: + idctab_found = False + except KeyError: + idctab_found = False + return idctab_found diff --git a/stwcs/gui/__init__.py b/stwcs/gui/__init__.py new file mode 100644 index 0000000..cd21bf6 --- /dev/null +++ b/stwcs/gui/__init__.py @@ -0,0 +1,19 @@ +""" STWCS.GUI + +This package defines the TEAL interfaces for public, file-based operations +provided by the STWCS package. + +""" +from __future__ import absolute_import # confidence high +__docformat__ = 'restructuredtext' + +# import modules which define the TEAL interfaces +from . import write_headerlet +from . import extract_headerlet +from . import attach_headerlet +from . import delete_headerlet +from . import headerlet_summary +from . import archive_headerlet +from . import restore_headerlet +from . import apply_headerlet +from . import updatewcs diff --git a/stwcs/gui/apply_headerlet.help b/stwcs/gui/apply_headerlet.help new file mode 100644 index 0000000..f701b52 --- /dev/null +++ b/stwcs/gui/apply_headerlet.help @@ -0,0 +1,38 @@ +This task applies a headerlet to a science observation to update either the +PRIMARY WCS or to add it as an alternate WCS. + +filename = "" +hdrlet = "" +attach = True +primary = True +archive = True +force = False +wcskey = "" +wcsname = "" +verbose = False + +Parameters +---------- +filename: string, @-file or wild-card name + File name(s) of science observation whose WCS solution will be updated +hdrlet: string, @-file or wild-card name + Headerlet file(s), must match input filenames 1-to-1 +attach: boolean + True (default): append headerlet to FITS file as a new extension. +primary: boolean + Specify whether or not to replace PRIMARY WCS with WCS from headerlet. +archive: boolean + True (default): before updating, create a headerlet with the + WCS old solution. +force: boolean + If True, this will cause the headerlet to replace the current PRIMARY + WCS even if it has a different distortion model. [Default: False] +wcskey: string + Key value (A-Z, except O) for this alternate WCS + If None, the next available key will be used +wcsname: string + Name to be assigned to this alternate WCS + WCSNAME is a required keyword in a Headerlet but this allows the + user to change it as desired. +logging: boolean + enable file logging diff --git a/stwcs/gui/apply_headerlet.py b/stwcs/gui/apply_headerlet.py new file mode 100644 index 0000000..d517e9f --- /dev/null +++ b/stwcs/gui/apply_headerlet.py @@ -0,0 +1,54 @@ +import os +from stsci.tools import teal, parseinput + +import stwcs +from stwcs.wcsutil import headerlet + +__taskname__ = __name__.split('.')[-1] # needed for help string +__package__ = headerlet.__name__ +__version__ = stwcs.__version__ +# +#### Interfaces used by TEAL +# +def getHelpAsString(docstring=False): + """ + return useful help from a file in the script directory called __taskname__.help + """ + install_dir = os.path.dirname(__file__) + htmlfile = os.path.join(install_dir,'htmlhelp',__taskname__+'.html') + helpfile = os.path.join(install_dir,__taskname__+'.help') + if docstring or (not docstring and not os.path.exists(htmlfile)): + helpString = __taskname__+' Version '+__version__+'\n\n' + if os.path.exists(helpfile): + helpString += teal.getHelpFileAsString(__taskname__,__file__) + else: + helpString = 'file://'+htmlfile + + return helpString + +def run(configObj=None): + + # start by interpreting filename and hdrlet inputs + filename = parseinput.parseinput(configObj['filename'])[0] + hdrlet = parseinput.parseinput(configObj['hdrlet'])[0] + + if configObj['primary']: + # Call function with properly interpreted input parameters + # Syntax: apply_headerlet_as_primary(filename, hdrlet, attach=True, + # archive=True, force=False, verbose=False) + headerlet.apply_headerlet_as_primary(filename, + hdrlet,attach=configObj['attach'], + archive=configObj['archive'],force=configObj['force'], + logging=configObj['logging']) + else: + wcsname = configObj['wcsname'] + if wcsname in ['',' ','INDEF']: wcsname = None + wcskey = configObj['wcskey'] + if wcskey == '': wcskey = None + # Call function with properly interpreted input parameters + # apply_headerlet_as_alternate(filename, hdrlet, attach=True, + # wcskey=None, wcsname=None, verbose=False) + headerlet.apply_headerlet_as_alternate(filename, + hdrlet, attach=configObj['attach'], + wcsname=wcsname, wcskey=wcskey, + logging=configObj['logging']) diff --git a/stwcs/gui/archive_headerlet.py b/stwcs/gui/archive_headerlet.py new file mode 100644 index 0000000..7ad3d4d --- /dev/null +++ b/stwcs/gui/archive_headerlet.py @@ -0,0 +1,69 @@ +from __future__ import print_function +import os + +from astropy.io import fits +from stsci.tools import teal + +import stwcs +from stwcs.wcsutil import headerlet + +__taskname__ = __name__.split('.')[-1] # needed for help string +__package__ = headerlet.__name__ +__version__ = stwcs.__version__ +# +#### Interfaces used by TEAL +# +def getHelpAsString(docstring=False): + """ + return useful help from a file in the script directory called __taskname__.help + """ + install_dir = os.path.dirname(__file__) + htmlfile = os.path.join(install_dir,'htmlhelp',__taskname__+'.html') + helpfile = os.path.join(install_dir,__taskname__+'.help') + if docstring or (not docstring and not os.path.exists(htmlfile)): + helpString = __taskname__+' Version '+__version__+'\n\n' + if os.path.exists(helpfile): + helpString += teal.getHelpFileAsString(__taskname__,__file__) + else: + helpString += headerlet.archive_as_headerlet.__doc__ + + else: + helpString = 'file://'+htmlfile + + return helpString + +def run(configObj=None): + + if configObj['hdrname'] in ['',' ','INDEF']: + print('='*60) + print('ERROR:') + print(' No valid "hdrname" parameter value provided!') + print(' Please restart this task and provide a value for this parameter.') + print('='*60) + return + + str_kw = ['wcsname','destim','sipname','npolfile','d2imfile', + 'descrip','history','author'] + + # create dictionary of remaining parameters, deleting extraneous ones + # such as those above + cdict = configObj.dict() + # remove any rules defined for the TEAL interface + if "_RULES_" in cdict: del cdict['_RULES_'] + del cdict['_task_name_'] + del cdict['filename'] + del cdict['hdrname'] + + # Convert blank string input as None + for kw in str_kw: + if cdict[kw] == '': cdict[kw] = None + if cdict['wcskey'].lower() == 'primary': cdict['wcskey'] = ' ' + + # Call function with properly interpreted input parameters + # Syntax: archive_as_headerlet(filename, sciext='SCI', wcsname=None, wcskey=None, + # hdrname=None, destim=None, + # sipname=None, npolfile=None, d2imfile=None, + # author=None, descrip=None, history=None, + # hdrlet=None, clobber=False) + headerlet.archive_as_headerlet(configObj['filename'], configObj['hdrname'], + **cdict) diff --git a/stwcs/gui/attach_headerlet.py b/stwcs/gui/attach_headerlet.py new file mode 100644 index 0000000..873c549 --- /dev/null +++ b/stwcs/gui/attach_headerlet.py @@ -0,0 +1,36 @@ +import os + +from stsci.tools import teal + +import stwcs +from stwcs.wcsutil import headerlet + +__taskname__ = __name__.split('.')[-1] # needed for help string +__package__ = headerlet.__name__ +__version__ = stwcs.__version__ +# +#### Interfaces used by TEAL +# +def getHelpAsString(docstring=False): + """ + return useful help from a file in the script directory called __taskname__.help + """ + install_dir = os.path.dirname(__file__) + htmlfile = os.path.join(install_dir,'htmlhelp',__taskname__+'.html') + helpfile = os.path.join(install_dir,__taskname__+'.help') + if docstring or (not docstring and not os.path.exists(htmlfile)): + helpString = __taskname__+' Version '+__version__+'\n\n' + if os.path.exists(helpfile): + helpString += teal.getHelpFileAsString(__taskname__,__file__) + else: + helpString += eval('.'.join([__package__,__taskname__,'__doc__'])) + else: + helpString = 'file://'+htmlfile + + return helpString + +def run(configObj=None): + + headerlet.attach_headerlet(configObj['filename'],configObj['hdrlet'], + configObj['logging']) + diff --git a/stwcs/gui/delete_headerlet.py b/stwcs/gui/delete_headerlet.py new file mode 100644 index 0000000..b3df5a7 --- /dev/null +++ b/stwcs/gui/delete_headerlet.py @@ -0,0 +1,52 @@ +from __future__ import print_function +import os + +from stsci.tools import teal +from stsci.tools import parseinput + +import stwcs +from stwcs.wcsutil import headerlet + +__taskname__ = __name__.split('.')[-1] # needed for help string +__package__ = headerlet.__name__ +__version__ = stwcs.__version__ +# +#### Interfaces used by TEAL +# +def getHelpAsString(docstring=False): + """ + return useful help from a file in the script directory called __taskname__.help + """ + install_dir = os.path.dirname(__file__) + htmlfile = os.path.join(install_dir,'htmlhelp',__taskname__+'.html') + helpfile = os.path.join(install_dir,__taskname__+'.help') + if docstring or (not docstring and not os.path.exists(htmlfile)): + helpString = __taskname__+' Version '+__version__+'\n\n' + if os.path.exists(helpfile): + helpString += teal.getHelpFileAsString(__taskname__,__file__) + else: + helpString += eval('.'.join([__package__,__taskname__,'__doc__'])) + else: + helpString = 'file://'+htmlfile + + return helpString + +def run(configObj=None): + + if configObj['hdrname'] == '' and configObj['hdrext'] is None and \ + configObj['distname'] == '': + print('='*60) + print('ERROR:') + print(' No valid "hdrname", "hdrext" or "distname" parameter value provided!') + print(' Please restart this task and provide a value for one of these parameters.') + print('='*60) + return + filename = parseinput.parseinput(configObj['filename'])[0] + # Call function with properly interpreted input parameters + # Syntax: delete_headerlet(filename, hdrname=None, hdrext=None, distname=None) + headerlet.delete_headerlet(filename, + hdrname = configObj['hdrname'], + hdrext = configObj['hdrext'], + distname = configObj['distname'], + logging = configObj['logging']) + diff --git a/stwcs/gui/extract_headerlet.py b/stwcs/gui/extract_headerlet.py new file mode 100644 index 0000000..02ecd7a --- /dev/null +++ b/stwcs/gui/extract_headerlet.py @@ -0,0 +1,58 @@ +from __future__ import print_function +import os + +from stsci.tools import teal + +import stwcs +from stwcs.wcsutil import headerlet + +__taskname__ = __name__.split('.')[-1] # needed for help string +__package__ = headerlet.__name__ +__version__ = stwcs.__version__ +# +#### Interfaces used by TEAL +# +def getHelpAsString(docstring=False): + """ + return useful help from a file in the script directory called __taskname__.help + """ + install_dir = os.path.dirname(__file__) + htmlfile = os.path.join(install_dir,'htmlhelp',__taskname__+'.html') + helpfile = os.path.join(install_dir,__taskname__+'.help') + if docstring or (not docstring and not os.path.exists(htmlfile)): + helpString = __taskname__+' Version '+__version__+'\n\n' + if os.path.exists(helpfile): + helpString += teal.getHelpFileAsString(__taskname__,__file__) + else: + helpString += eval('.'.join([__package__,__taskname__,'__doc__'])) + + else: + helpString = 'file://'+htmlfile + + return helpString + +def run(configObj=None): + + if configObj['output'] in ['',' ',None]: + print('='*60) + print('ERROR:') + print(' No valid "output" parameter value provided!') + print(' Please restart this task and provide a value for this parameter.') + print('='*60) + return + + # create dictionary of remaining parameters, deleting extraneous ones + # such as those above + cdict = configObj.dict() + # remove any rules defined for the TEAL interface + if "_RULES_" in cdict: del cdict['_RULES_'] + del cdict['_task_name_'] + del cdict['filename'] + del cdict['output'] + + # Call function with properly interpreted input parameters + # Syntax: extract_headerlet(filename, output, extnum=None, hdrname=None, + # clobber=False, verbose=100) + headerlet.extract_headerlet(configObj['filename'], configObj['output'], + **cdict) + diff --git a/stwcs/gui/headerlet_summary.py b/stwcs/gui/headerlet_summary.py new file mode 100644 index 0000000..82a3e0c --- /dev/null +++ b/stwcs/gui/headerlet_summary.py @@ -0,0 +1,47 @@ +import os +from stsci.tools import teal + +import stwcs +from stwcs.wcsutil import headerlet + +__taskname__ = __name__.split('.')[-1] # needed for help string +__package__ = headerlet.__name__ +__version__ = stwcs.__version__ +# +#### Interfaces used by TEAL +# +def getHelpAsString(docstring=False): + """ + return useful help from a file in the script directory called __taskname__.help + """ + install_dir = os.path.dirname(__file__) + htmlfile = os.path.join(install_dir,'htmlhelp',__taskname__+'.html') + helpfile = os.path.join(install_dir,__taskname__+'.help') + if docstring or (not docstring and not os.path.exists(htmlfile)): + helpString = __taskname__+' Version '+__version__+'\n\n' + if os.path.exists(helpfile): + helpString += teal.getHelpFileAsString(__taskname__,__file__) + else: + helpString += eval('.'.join([__package__,__taskname__,'__doc__'])) + else: + helpString = 'file://'+htmlfile + + return helpString + +def run(configObj=None): + + # create dictionary of remaining parameters, deleting extraneous ones + # such as those above + cdict = configObj.dict() + # remove any rules defined for the TEAL interface + if "_RULES_" in cdict: del cdict['_RULES_'] + del cdict['_task_name_'] + del cdict['filename'] + if headerlet.is_par_blank(cdict['columns']): + cdict['columns'] = None + # Call function with properly interpreted input parameters + + # Syntax: headerlet_summary(filename,columns=None,pad=2,maxwidth=None, + # output=None,clobber=True,quiet=False) + headerlet.headerlet_summary(configObj['filename'],**cdict) + diff --git a/stwcs/gui/pars/apply_headerlet.cfg b/stwcs/gui/pars/apply_headerlet.cfg new file mode 100644 index 0000000..06d6daf --- /dev/null +++ b/stwcs/gui/pars/apply_headerlet.cfg @@ -0,0 +1,10 @@ +_task_name_ = apply_headerlet +filename = "" +hdrlet = "" +attach = True +primary = True +archive = True +force = False +wcskey = "" +wcsname = "" +logging = False diff --git a/stwcs/gui/pars/apply_headerlet.cfgspc b/stwcs/gui/pars/apply_headerlet.cfgspc new file mode 100644 index 0000000..648e562 --- /dev/null +++ b/stwcs/gui/pars/apply_headerlet.cfgspc @@ -0,0 +1,12 @@ +_task_name_ = string_kw(default="apply_headerlet") +filename = string_kw(default="", comment="Input file name") +hdrlet = string_kw(default="", comment="Headerlet FITS filename") +attach = boolean_kw(default=True, comment= "Append headerlet to FITS file as new extension?") +primary = boolean_kw(default=True, triggers="_rule1_", comment="Replace PRIMARY WCS with headerlet WCS?") +archive = boolean_kw(default=True, active_if="_rule1_", comment="Save PRIMARY WCS as new headerlet extension?") +force = boolean_kw(default=False, active_if="_rule1_", comment="If distortions do not match, force update anyway?") +wcskey = option_kw("A","B","C","D","E","F","G","H","I","J","K","L","M","N","P","Q","R","S","T","U","V","W","X","Y","Z","", default="", inactive_if="_rule1_", comment="Apply headerlet as alternate WCS with this letter") +wcsname = string_kw(default="", inactive_if="_rule1_", comment="Apply headerlet as alternate WCS with this name") +logging = boolean_kw(default=False, comment= "Enable logging to a file") +[ _RULES_ ] +_rule1_ = string_kw(default=True, code='tyfn={"yes":True, "no":False}; OUT = tyfn[VAL]') diff --git a/stwcs/gui/pars/archive_headerlet.cfg b/stwcs/gui/pars/archive_headerlet.cfg new file mode 100644 index 0000000..97cd7ef --- /dev/null +++ b/stwcs/gui/pars/archive_headerlet.cfg @@ -0,0 +1,14 @@ +_task_name_ = archive_headerlet +filename = "" +hdrname = "" +sciext = "SCI" +wcsname = "" +wcskey = "PRIMARY" +destim = "" +sipname = "" +npolfile = "" +d2imfile = "" +author = "" +descrip = "" +history = "" +logging = False
\ No newline at end of file diff --git a/stwcs/gui/pars/archive_headerlet.cfgspc b/stwcs/gui/pars/archive_headerlet.cfgspc new file mode 100644 index 0000000..03e67f0 --- /dev/null +++ b/stwcs/gui/pars/archive_headerlet.cfgspc @@ -0,0 +1,14 @@ +_task_name_ = string_kw(default="archive_headerlet") +filename = string_kw(default="", comment="Input file name") +hdrname = string_kw(default="", comment="Unique name(HDRNAME) for headerlet") +sciext = string_kw(default="SCI", comment="EXTNAME of extension with WCS") +wcsname = string_kw(default="", comment="Name of WCS to be archived") +wcskey = option_kw("A","B","C","D","E","F","G","H","I","J","K","L","M","N","P","Q","R","S","T","U","V","W","X","Y","Z","PRIMARY", default="PRIMARY", comment="Archive the WCS with this letter") +destim = string_kw(default="", comment="Rootname of image to which this headerlet applies ") +sipname = string_kw(default="", comment="Name for source of polynomial distortion keywords") +npolfile = string_kw(default="", comment="Name for source of non-polynomial residuals") +d2imfile = string_kw(default="", comment="Name for source of detector correction table") +author = string_kw(default="", comment="Author name for creator of headerlet") +descrip = string_kw(default="", comment="Short description of headerlet solution") +history = string_kw(default="", comment="Name of ASCII file containing history for headerlet") +logging = boolean_kw(default=False, comment= "Enable logging to a file")
\ No newline at end of file diff --git a/stwcs/gui/pars/attach_headerlet.cfg b/stwcs/gui/pars/attach_headerlet.cfg new file mode 100644 index 0000000..d131a70 --- /dev/null +++ b/stwcs/gui/pars/attach_headerlet.cfg @@ -0,0 +1,4 @@ +_task_name_ = attach_headerlet +filename = "" +hdrlet = "" +logging = False diff --git a/stwcs/gui/pars/attach_headerlet.cfgspc b/stwcs/gui/pars/attach_headerlet.cfgspc new file mode 100644 index 0000000..bc91bca --- /dev/null +++ b/stwcs/gui/pars/attach_headerlet.cfgspc @@ -0,0 +1,4 @@ +_task_name_ = string_kw(default="attach_headerlet") +filename = string_kw(default="", comment="FITS image file name") +hdrlet = string_kw(default="", comment="Headerlet FITS filename") +logging = boolean_kw(default=False, comment= "Enable logging to a file")
\ No newline at end of file diff --git a/stwcs/gui/pars/delete_headerlet.cfg b/stwcs/gui/pars/delete_headerlet.cfg new file mode 100644 index 0000000..d156937 --- /dev/null +++ b/stwcs/gui/pars/delete_headerlet.cfg @@ -0,0 +1,6 @@ +_task_name_ = delete_headerlet +filename = "" +hdrname = "" +hdrext = None +distname = "" +logging = False
\ No newline at end of file diff --git a/stwcs/gui/pars/delete_headerlet.cfgspc b/stwcs/gui/pars/delete_headerlet.cfgspc new file mode 100644 index 0000000..43790b1 --- /dev/null +++ b/stwcs/gui/pars/delete_headerlet.cfgspc @@ -0,0 +1,6 @@ +_task_name_ = string_kw(default="delete_headerlet") +filename = string_kw(default="", comment="FITS image file name(s), list or wild-card") +hdrname = string_kw(default="", comment="Delete headerlet with this HDRNAME") +hdrext = integer_or_none_kw(default=None, comment="Delete headerlet from this extension") +distname = string_kw(default="", comment="Delete *ALL* with this DISTNAME") +logging = boolean_kw(default=False, comment= "Enable logging to a file") diff --git a/stwcs/gui/pars/extract_headerlet.cfg b/stwcs/gui/pars/extract_headerlet.cfg new file mode 100644 index 0000000..3dd0a7a --- /dev/null +++ b/stwcs/gui/pars/extract_headerlet.cfg @@ -0,0 +1,7 @@ +_task_name_ = extract_headerlet +filename = "" +output = "" +extnum = None +hdrname = "" +clobber = True +logging = False diff --git a/stwcs/gui/pars/extract_headerlet.cfgspc b/stwcs/gui/pars/extract_headerlet.cfgspc new file mode 100644 index 0000000..b50d4bf --- /dev/null +++ b/stwcs/gui/pars/extract_headerlet.cfgspc @@ -0,0 +1,7 @@ +_task_name_ = string_kw(default="extract_headerlet") +filename = string_kw(default="", comment="Input file name") +output = string_kw(default="", comment="Output headerlet FITS filename") +extnum = integer_or_none_kw(default=None, comment="FITS extension number of headerlet") +hdrname = string_kw(default="", comment="Unique name(HDRNAME) for headerlet") +clobber = boolean_kw(default=True, comment= "Overwrite existing headerlet FITS file?") +logging = boolean_kw(default=False, comment= "Enable logging to a file") diff --git a/stwcs/gui/pars/headerlet_summary.cfg b/stwcs/gui/pars/headerlet_summary.cfg new file mode 100644 index 0000000..7203552 --- /dev/null +++ b/stwcs/gui/pars/headerlet_summary.cfg @@ -0,0 +1,8 @@ +_task_name_ = headerlet_summary +filename = "" +columns = None +pad = 2 +maxwidth = None +output = "" +clobber = True +quiet = False diff --git a/stwcs/gui/pars/headerlet_summary.cfgspc b/stwcs/gui/pars/headerlet_summary.cfgspc new file mode 100644 index 0000000..ce65930 --- /dev/null +++ b/stwcs/gui/pars/headerlet_summary.cfgspc @@ -0,0 +1,8 @@ +_task_name_ = string_kw(default="headerlet_summary") +filename = string_kw(default="", comment="FITS image file name") +columns = string_kw(default="", comment="Headerlet keyword(s) to be reported") +pad = integer_kw(default=2, comment="Number of spaces between output columns") +maxwidth = integer_or_none_kw(default=None, comment="Max width for each column") +output = string_kw(default="", comment="Name of output file for summary") +clobber = boolean_kw(default=True, comment="Overwrite previously written summary?") +quiet = boolean_kw(default=False, comment="Suppress output of summary to STDOUT?") diff --git a/stwcs/gui/pars/restore_headerlet.cfg b/stwcs/gui/pars/restore_headerlet.cfg new file mode 100644 index 0000000..b0ec427 --- /dev/null +++ b/stwcs/gui/pars/restore_headerlet.cfg @@ -0,0 +1,10 @@ +_task_name_ = restore_headerlet +filename = "" +archive = True +force = False +distname = "" +primary = None +sciext = "SCI" +hdrname = "" +hdrext = None +logging = False
\ No newline at end of file diff --git a/stwcs/gui/pars/restore_headerlet.cfgspc b/stwcs/gui/pars/restore_headerlet.cfgspc new file mode 100644 index 0000000..df47e3f --- /dev/null +++ b/stwcs/gui/pars/restore_headerlet.cfgspc @@ -0,0 +1,12 @@ +_task_name_ = string_kw(default="restore_headerlet") +filename = string_kw(default="", comment="Input file name") +archive = boolean_kw(default=True, comment= "Create headerlets from WCSs being replaced?") +force = boolean_kw(default=False, comment="If distortions do not match, force update anyway?") +distname = string_kw(default="", triggers="_rule1_", comment="Restore ALL headerlet extensions with this DISTNAME") +primary = integer_or_none_kw(default=None, inactive_if="_rule1_", comment="Headerlet extension to restore as new primary WCS") +sciext = string_kw(default="SCI", inactive_if="_rule1_", comment="EXTNAME of extension with WCS") +hdrname = string_kw(default="", active_if="_rule1_", comment="HDRNAME of headerlet extension to be restored") +hdrext = integer_or_none_kw(default=None, active_if="_rule1_", comment="Extension number for headerlet to be restored") +logging = boolean_kw(default=False, comment= "Enable logging to a file") +[ _RULES_ ] +_rule1_ = string_kw(default=True, code='from stwcs import wcsutil;from stwcs.wcsutil import headerlet;OUT = headerlet.is_par_blank(VAL)') diff --git a/stwcs/gui/pars/updatewcs.cfg b/stwcs/gui/pars/updatewcs.cfg new file mode 100644 index 0000000..35360f2 --- /dev/null +++ b/stwcs/gui/pars/updatewcs.cfg @@ -0,0 +1,8 @@ +_task_name_ = updatewcs +input = "*flt.fits" +extname = "SCI" +vacorr = True +tddcorr = True +npolcorr = True +d2imcorr = True +checkfiles = True diff --git a/stwcs/gui/pars/updatewcs.cfgspc b/stwcs/gui/pars/updatewcs.cfgspc new file mode 100644 index 0000000..a3a3fb5 --- /dev/null +++ b/stwcs/gui/pars/updatewcs.cfgspc @@ -0,0 +1,8 @@ +_task_name_ = string_kw(default="updatewcs") +input = string_kw(default="", comment="Input files (name, suffix, or @list)") +extname = string_kw(default="SCI", comment="EXTNAME of extensions to be archived") +vacorr = boolean_kw(default=True, comment= "Apply velocity aberration correction?") +tddcorr = boolean_kw(default=True, comment= "Apply time dependent distortion correction?") +npolcorr = boolean_kw(default=True, comment= "Apply lookup table distortion?") +d2imcorr = boolean_kw(default=True, comment= "Apply detector to image correction?") +checkfiles = boolean_kw(default=True, comment= "Check format of input files?") diff --git a/stwcs/gui/pars/write_headerlet.cfg b/stwcs/gui/pars/write_headerlet.cfg new file mode 100644 index 0000000..9eb4592 --- /dev/null +++ b/stwcs/gui/pars/write_headerlet.cfg @@ -0,0 +1,18 @@ +_task_name_ = write_headerlet +filename = "" +output = "" +clobber = True +hdrname = "" +wcskey = "PRIMARY" +wcsname = "" +author = "" +descrip = "" +catalog = "" +history = "" +sciext = "SCI" +destim = "" +sipname = "" +npolfile = "" +d2imfile = "" +attach = True +logging = False
\ No newline at end of file diff --git a/stwcs/gui/pars/write_headerlet.cfgspc b/stwcs/gui/pars/write_headerlet.cfgspc new file mode 100644 index 0000000..ba28b05 --- /dev/null +++ b/stwcs/gui/pars/write_headerlet.cfgspc @@ -0,0 +1,18 @@ +_task_name_ = string_kw(default="write_headerlet") +filename = string_kw(default="", comment="Input file name") +output = string_kw(default="", comment="Filename for headerlet FITS file") +clobber = boolean_kw(default=True, comment= "Overwrite existing headerlet FITS file?") +hdrname = string_kw(default="", comment="Unique name(HDRNAME) for headerlet[REQUIRED]") +wcskey = option_kw("A","B","C","D","E","F","G","H","I","J","K","L","M","N","P","Q","R","S","T","U","V","W","X","Y","Z","PRIMARY", default="PRIMARY", comment="Create headerlet from WCS with this letter") +wcsname = string_kw(default="", comment="Create headerlet from WCS with this name") +author = string_kw(default="", comment="Author name for creator of headerlet") +descrip = string_kw(default="", comment="Short description of headerlet solution") +catalog = string_kw(default="", comment="Reference frame for headerlet solution") +history = string_kw(default="", comment="Name of ASCII file containing history for headerlet") +sciext = string_kw(default="SCI", comment="EXTNAME of extension with WCS") +destim = string_kw(default="", comment="Rootname of image to which this headerlet applies ") +sipname = string_kw(default="", comment="Name for source of polynomial distortion keywords") +npolfile = string_kw(default="", comment="Name for source of non-polynomial residuals") +d2imfile = string_kw(default="", comment="Name for source of detector correction table") +attach = boolean_kw(default=True, comment="Create headerlet FITS extension?") +logging = boolean_kw(default=False, comment= "Enable logging to a file")
\ No newline at end of file diff --git a/stwcs/gui/restore_headerlet.help b/stwcs/gui/restore_headerlet.help new file mode 100644 index 0000000..fe07a15 --- /dev/null +++ b/stwcs/gui/restore_headerlet.help @@ -0,0 +1,43 @@ +Restore headerlet extension(s) as either a primary WCS or as alternate WCSs + +This task can restore a WCS solution stored in a headerlet extension or +restore all WCS solutions from all headerlet extensions with the same +distortion model. + +Parameters +---------- +filename: string or HDUList + Either a filename or PyFITS HDUList object for the input science file + An input filename (str) will be expanded as necessary to interpret + any environmental variables included in the filename. + +archive: boolean (default True) + flag indicating if HeaderletHDUs should be created from the + primary and alternate WCSs in fname before restoring all matching + headerlet extensions + +force: boolean (default:False) + When the distortion models of the headerlet and the primary do + not match, and archive is False, this flag forces an update of + the primary. + +distname: string + distortion model as represented by a DISTNAME keyword + +primary: int or string or None + HeaderletHDU to be restored as primary + if int - a fits extension + if string - HDRNAME + if None - use first HeaderletHDU + +sciext: string (default: SCI) + EXTNAME value of FITS extensions with WCS keywords + +hdrname: string + HDRNAME keyword of HeaderletHDU + +hdrext: int or tuple + Headerlet extension number of tuple ('HDRLET',2) + +logging: boolean + enable file logging
\ No newline at end of file diff --git a/stwcs/gui/restore_headerlet.py b/stwcs/gui/restore_headerlet.py new file mode 100644 index 0000000..7570d76 --- /dev/null +++ b/stwcs/gui/restore_headerlet.py @@ -0,0 +1,48 @@ +import os + +from stsci.tools import teal + +import stwcs +from stwcs.wcsutil import headerlet + +__taskname__ = __name__.split('.')[-1] # needed for help string +__package__ = headerlet.__name__ +__version__ = stwcs.__version__ +# +#### Interfaces used by TEAL +# +def getHelpAsString(docstring=False): + """ + return useful help from a file in the script directory called __taskname__.help + """ + install_dir = os.path.dirname(__file__) + htmlfile = os.path.join(install_dir,'htmlhelp',__taskname__+'.html') + helpfile = os.path.join(install_dir,__taskname__+'.help') + if docstring or (not docstring and not os.path.exists(htmlfile)): + helpString = __taskname__+' Version '+__version__+'\n\n' + if os.path.exists(helpfile): + helpString += teal.getHelpFileAsString(__taskname__,__file__) + else: + helpString = 'file://'+htmlfile + + return helpString + +def run(configObj=None): + + if configObj['distname'] not in ['',' ','INDEF']: + # Call function with properly interpreted input parameters + # Syntax: restore_all_with_distname(filename, distname, primary, + # archive=True, sciext='SCI', verbose=False) + headerlet.restore_all_with_distname(configObj['filename'], + configObj['distname'],configObj['primary'], + archive=configObj['archive'],sciext=configObj['sciext'], + logging=configObj['logging']) + else: + # Call function with properly interpreted input parameters + # restore_from_headerlet(filename, hdrname=None, hdrext=None, + # archive=True, force=False) + headerlet.restore_from_headerlet(configObj['filename'], + hdrname=configObj['hdrname'],hdrext=configObj['hdrext'], + archive=configObj['archive'], force=configObj['force'], + logging=configObj['logging']) + diff --git a/stwcs/gui/updatewcs.py b/stwcs/gui/updatewcs.py new file mode 100644 index 0000000..3dacb67 --- /dev/null +++ b/stwcs/gui/updatewcs.py @@ -0,0 +1,90 @@ +from __future__ import print_function +import os + +from astropy.io import fits +from stsci.tools import parseinput +from stsci.tools import fileutil +from stsci.tools import teal +import stwcs +from stwcs import updatewcs +from stwcs.wcsutil import convertwcs + +allowed_corr_dict = {'vacorr':'VACorr','tddcorr':'TDDCorr','npolcorr':'NPOLCorr','d2imcorr':'DET2IMCorr'} + +__taskname__ = __name__.split('.')[-1] # needed for help string +__package__ = updatewcs.__name__ +__version__ = stwcs.__version__ + +# +#### Interfaces used by TEAL +# +def getHelpAsString(docstring=False): + """ + return useful help from a file in the script directory called __taskname__.help + """ + install_dir = os.path.dirname(__file__) + htmlfile = os.path.join(install_dir,'htmlhelp',__taskname__+'.html') + helpfile = os.path.join(install_dir,__taskname__+'.help') + if docstring or (not docstring and not os.path.exists(htmlfile)): + helpString = __taskname__+' Version '+__version__+'\n\n' + if os.path.exists(helpfile): + helpString += teal.getHelpFileAsString(__taskname__,__file__) + else: + helpString += eval('.'.join([__package__,__taskname__,'__doc__'])) + + else: + helpString = 'file://'+htmlfile + + return helpString + +def run(configObj=None): + + # Interpret primary parameters from configObj instance + extname = configObj['extname'] + input = configObj['input'] + + # create dictionary of remaining parameters, deleting extraneous ones + # such as those above + cdict = configObj.dict() + # remove any rules defined for the TEAL interface + if "_RULES_" in cdict: del cdict['_RULES_'] + del cdict['_task_name_'] + del cdict['input'] + del cdict['extname'] + + # parse input + input,altfiles = parseinput.parseinput(configObj['input']) + + # Insure that all input files have a correctly archived + # set of OPUS WCS keywords + # Legacy files from OTFR, like all WFPC2 data from OTFR, will only + # have the OPUS WCS keywords archived using a prefix of 'O' + # These keywords need to be converted to the Paper I alternate WCS + # standard using a wcskey (suffix) of 'O' + # If an alternate WCS with wcskey='O' already exists, this will copy + # the values from the old prefix-'O' WCS keywords to insure the correct + # OPUS keyword values get archived for use with updatewcs. + # + for file in input: + # Check to insure that there is a valid reference file to be used + idctab = fits.getval(file, 'idctab') + if not os.path.exists(fileutil.osfn(idctab)): + print('No valid distortion reference file ',idctab,' found in ',file,'!') + raise ValueError + + # Re-define 'cdict' to only have switches for steps supported by that instrument + # the set of supported steps are defined by the dictionary + # updatewcs.apply_corrections.allowed_corrections + # + for file in input: + # get instrument name from input file + instr = fits.getval(file,'INSTRUME') + # make copy of input parameters dict for this file + fdict = cdict.copy() + # Remove any parameter that is not part of this instrument's allowed corrections + for step in allowed_corr_dict: + if allowed_corr_dict[step] not in updatewcs.apply_corrections.allowed_corrections[instr]: + fdict[step] + # Call 'updatewcs' on correctly archived file + updatewcs.updatewcs(file,**fdict) + diff --git a/stwcs/gui/write_headerlet.py b/stwcs/gui/write_headerlet.py new file mode 100644 index 0000000..e18bed8 --- /dev/null +++ b/stwcs/gui/write_headerlet.py @@ -0,0 +1,80 @@ +from __future__ import print_function +import os + +from stsci.tools import teal +from stsci.tools import parseinput + +import stwcs +from stwcs.wcsutil import headerlet + +__taskname__ = __name__.split('.')[-1] # needed for help string +__package__ = headerlet.__name__ +__version__ = stwcs.__version__ +# +#### Interfaces used by TEAL +# +def getHelpAsString(docstring=False): + """ + return useful help from a file in the script directory called __taskname__.help + """ + install_dir = os.path.dirname(__file__) + htmlfile = os.path.join(install_dir,'htmlhelp',__taskname__+'.html') + helpfile = os.path.join(install_dir,__taskname__+'.help') + if docstring or (not docstring and not os.path.exists(htmlfile)): + helpString = __taskname__+' Version '+__version__+'\n\n' + if os.path.exists(helpfile): + helpString += teal.getHelpFileAsString(__taskname__,__file__) + else: + helpString += headerlet.write_headerlet.__doc__ + + else: + helpString = 'file://'+htmlfile + + return helpString + +def run(configObj=None): + flist,oname = parseinput.parseinput(configObj['filename']) + if len(flist) == 0: + print('='*60) + print('ERROR:') + print(' No valid "filename" parameter value provided!') + print(' Please check the working directory and restart this task.') + print('='*60) + return + + if configObj['hdrname'] in ['',' ','INDEF']: + print('='*60) + print('ERROR:') + print(' No valid "hdrname" parameter value provided!') + print(' Please restart this task and provide a value for this parameter.') + print('='*60) + return + + if configObj['output'] in ['',' ','INDEF']: + configObj['output'] = None + + str_kw = ['wcsname','destim','sipname','npolfile','d2imfile', + 'descrip','history','author','output','catalog'] + + # create dictionary of remaining parameters, deleting extraneous ones + # such as those above + cdict = configObj.dict() + # remove any rules defined for the TEAL interface + if "_RULES_" in cdict: del cdict['_RULES_'] + del cdict['_task_name_'] + del cdict['filename'] + del cdict['hdrname'] + + # Convert blank string input as None + for kw in str_kw: + if cdict[kw] == '': cdict[kw] = None + if cdict['wcskey'].lower() == 'primary': cdict['wcskey'] = ' ' + + # Call function with properly interpreted input parameters + # Syntax: write_headerlet(filename, hdrname, output, sciext='SCI', + # wcsname=None, wcskey=None, destim=None, + # sipname=None, npolfile=None, d2imfile=None, + # author=None, descrip=None, history=None, + # attach=True, clobber=False) + headerlet.write_headerlet(flist, configObj['hdrname'], + **cdict) diff --git a/stwcs/tests/__init__.py b/stwcs/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/stwcs/tests/__init__.py diff --git a/stwcs/tests/data/__init__.py b/stwcs/tests/data/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/stwcs/tests/data/__init__.py diff --git a/stwcs/tests/data/j94f05bgq_flt.fits b/stwcs/tests/data/j94f05bgq_flt.fits new file mode 100644 index 0000000..61261d6 --- /dev/null +++ b/stwcs/tests/data/j94f05bgq_flt.fits @@ -0,0 +1 @@ +SIMPLE = T / Fits standard BITPIX = 16 / Bits per pixel NAXIS = 0 / Number of axes EXTEND = T / File may contain extensions IRAF-TLM= '2010-07-22T17:49:08' / Time of last modification NEXTEND = 6 / Number of standard extensions DATE = '2007-02-08T21:38:46' / date this file was written (yyyy-mm-dd) FILENAME= 'j94f05bgq_flt.fits' / name of file FILETYPE= 'SCI ' / type of data found in data file TELESCOP= 'HST' / telescope used to acquire data INSTRUME= 'ACS ' / identifier for instrument used to acquire data EQUINOX = 2000.0 / equinox of celestial coord. system / DATA DESCRIPTION KEYWORDS ROOTNAME= 'j94f05bgq ' / rootname of the observation set / TARGET INFORMATION RA_TARG = 5.655000000000E+00 / right ascension of the target (deg) (J2000) DEC_TARG= -7.207055555556E+01 / declination of the target (deg) (J2000) / PROPOSAL INFORMATION / EXPOSURE INFORMATION REFFRAME= 'GSC1 ' / guide star catalog version DATE-OBS= '2005-03-07' / UT date of start of observation (yyyy-mm-dd) TIME-OBS= '06:51:26' / UT time of start of observation (hh:mm:ss) EXPSTART= 5.343628571938E+04 / exposure start time (Modified Julian Date) EXPEND = 5.343629036114E+04 / exposure end time (Modified Julian Date) EXPTIME = 400.000000 / exposure duration (seconds)--calculated / POINTING INFORMATION PA_V3 = 337.125305 / position angle of V3-axis of HST (deg) / TARGET OFFSETS (POSTARGS) POSTARG1= 0.000000 / POSTARG in axis 1 direction POSTARG2= 0.000000 / POSTARG in axis 2 direction / DIAGNOSTIC KEYWORDS / SCIENCE INSTRUMENT CONFIGURATION SUBARRAY= F / data from a subarray (T) or full frame (F) DETECTOR= 'WFC' / detector in use: WFC, HRC, or SBC FILTER1 = 'F606W ' / element selected from filter wheel 1 FILTER2 = 'CLEAR2L ' / element selected from filter wheel 2 / CALIBRATION SWITCHES: PERFORM, OMIT, COMPLETE / CALIBRATION REFERENCE FILES IDCTAB = 'postsm4_idc.fits' / image distortion correction table DGEOFILE= 'jref$qbu16424j_dxy.fits' / Distortion correction image / COSMIC RAY REJECTION ALGORITHM PARAMETERS / OTFR KEYWORDS / PATTERN KEYWORDS / POST FLASH PARAMETERS / ENGINEERING PARAMETERS / CALIBRATED ENGINEERING PARAMETERS / ASSOCIATION KEYWORDS TDDCORR = 'PERFORM ' NPOLFILE= 'qbu16424j_npl.fits' D2IMFILE= 'new_wfc_d2i.fits' END XTENSION= 'IMAGE ' / Image extension BITPIX = 16 / Bits per pixel NAXIS = 2 / Number of axes NAXIS1 = 4096 / Axis length NAXIS2 = 2048 / Axis length PCOUNT = 0 / No 'random' parameters GCOUNT = 1 / Only one group EXTNAME = 'SCI ' / Extension name EXTVER = 1 / Extension version DATE = '2007-02-08T21:38:47' / Date FITS file was generated IRAF-TLM= '13:38:23 (20/08/2008)' / Time of last modification INHERIT = T / inherit the primary header EXPNAME = 'j94f05bgq ' / exposure identifier BUNIT = 'ELECTRONS' / brightness units / WFC CCD CHIP IDENTIFICATION CCDCHIP = 2 / CCD chip (1 or 2) / World Coordinate System and Related Parameters WCSAXES = 2 / number of World Coordinate System axes CRPIX1 = 2048.0 / x-coordinate of reference pixel CRPIX2 = 1024.0 / y-coordinate of reference pixel CRVAL1 = 5.63056810618 / first axis value at reference pixel CRVAL2 = -72.05457184279 / second axis value at reference pixel CTYPE1 = 'RA---TAN-SIP' / the coordinate type for the first axis CTYPE2 = 'DEC--TAN-SIP' / the coordinate type for the second axis CD1_1 = 1.290562563339972E-05 / partial of first axis coordinate w.r.t. x CD1_2 = 5.953091234198029E-06 / partial of first axis coordinate w.r.t. y CD2_1 = 5.0220581265601E-06 / partial of second axis coordinate w.r.t. x CD2_2 = -1.26447741482017E-05 / partial of second axis coordinate w.r.t. y LTV1 = 0.0000000E+00 / offset in X to subsection start LTV2 = 0.0000000E+00 / offset in Y to subsection start LTM1_1 = 1.0 / reciprocal of sampling rate in X LTM2_2 = 1.0 / reciprocal of sampling rate in Y RA_APER = 5.655000000000E+00 / RA of aperture reference position PA_APER = 154.533 / Position Angle of reference aperture center (deVAFACTOR= 1.000018683511E+00 / velocity aberration plate scale factor / READOUT DEFINITION PARAMETERS SIZAXIS1= 4096 / subarray axis1 size in unbinned detector pixelsSIZAXIS2= 2048 / subarray axis2 size in unbinned detector pixelsBINAXIS1= 1 / axis1 data bin size in unbinned detector pixelsBINAXIS2= 1 / axis2 data bin size in unbinned detector pixels / PHOTOMETRY KEYWORDS / REPEATED EXPOSURES INFO NCOMBINE= 1 / number of image sets combined during CR rejecti / DATA PACKET INFORMATION / ON-BOARD COMPRESSION INFORMATION WFCMPRSD= F / was WFC data compressed? (T/F) / IMAGE STATISTICS AND DATA QUALITY FLAGS NGOODPIX= 7822781 / number of good pixels OORIENTA= 154.7886863186197 / position angle of image y axis (deg. e of n) WCSCDATE= '21:39:44 (08/02/2007)' / Time WCS keywords were copied. TDDALPHA= 0.1195051334702275 TDDBETA = -0.03716837782340918 END diff --git a/stwcs/tests/data/new_wfc_d2i.fits b/stwcs/tests/data/new_wfc_d2i.fits Binary files differnew file mode 100755 index 0000000..f72e20e --- /dev/null +++ b/stwcs/tests/data/new_wfc_d2i.fits diff --git a/stwcs/tests/data/postsm4_idc.fits b/stwcs/tests/data/postsm4_idc.fits new file mode 100644 index 0000000..a03c08a --- /dev/null +++ b/stwcs/tests/data/postsm4_idc.fits @@ -0,0 +1,60 @@ +SIMPLE = T / Fits standard BITPIX = 16 / Bits per pixel NAXIS = 0 / Number of axes EXTEND = T / File may contain extensions ORIGIN = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator DATE = '2006-11-30T15:04:40' IRAF-TLM= '2010-05-21T13:29:07' COMMENT FITS (Flexible Image Transport System) format is defined in 'AstronomyCOMMENT and Astrophysics', volume 376, page 359; bibcode: 2001A&A...376..359H FILENAME= 'postsm4_idc.fits' / name of file NEXTEND = 1 / number of extensions in file USEAFTER= 'Jan 1 2009' PEDIGREE= 'INFLIGHT 01/06/2002 01/05/2006' INSTRUME= 'ACS ' NORDER = 4 FILETYPE= 'DISTORTION COEFFICIENTS' DETECTOR= 'WFC ' COMMENT = 'File created by C. Cox' DESCRIP = 'Supports all filter combinations' HISTORY File created June 9th by Colin Cox HISTORY Expanded version (using texpand) of previous file to HISTORY cover all possible filter combinations. HISTORY Duplicate rows removed plus a few rows that referred HISTORY to HRC-only filters and erroneously included in HISTORY previous tables. HISTORY trim_wfcallmodes_idc.fits renamed to q692007cj_idc.fits on Jun 9 2006 HISTORY Incorporate May 2006 distortion solution HISTORY File created by Colin Cox, Nov 30 2006 HISTORY Latest expansion includes all combinations HISTORY including the previously removed HRC-only ones. HISTORY Probably includes some duplicates but these HISTORY do not cause a problem HISTORY nov30_idc.fits renamed to qbu1641sj_idc.fits on Nov 30 2006 TDD_A0 = 0.095 TDD_A1 = 0.0360 TDD_B0 = -0.029 TDD_B1 = -0.012 TDD_D0 = 0. TDD_DATE= 2006.798432109 TDDORDER= 1 END XTENSION= 'BINTABLE' / binary table extension BITPIX = 8 / 8-bit bytes NAXIS = 2 / 2-dimensional binary table NAXIS1 = 166 / width of table in bytes NAXIS2 = 736 PCOUNT = 0 / size of special data area GCOUNT = 1 / one data group (required keyword) TFIELDS = 39 TTYPE1 = 'DETCHIP ' / label for field 1 TFORM1 = '1I ' / data format of field: 2-byte INTEGER TUNIT1 = 'chip ' / physical unit of field TTYPE2 = 'DIRECTION' / label for field 2 TFORM2 = '8A ' / data format of field: ASCII Character TTYPE3 = 'FILTER1 ' / label for field 3 TFORM3 = '8A ' / data format of field: ASCII Character TTYPE4 = 'FILTER2 ' / label for field 4 TFORM4 = '8A ' / data format of field: ASCII Character TTYPE5 = 'XSIZE ' / label for field 5 TFORM5 = '1J ' / data format of field: 4-byte INTEGER TTYPE6 = 'YSIZE ' / label for field 6 TFORM6 = '1J ' / data format of field: 4-byte INTEGER TTYPE7 = 'XREF ' / label for field 7 TFORM7 = '1E ' / data format of field: 4-byte REAL TTYPE8 = 'YREF ' / label for field 8 TFORM8 = '1E ' / data format of field: 4-byte REAL TTYPE9 = 'V2REF ' / label for field 9 TFORM9 = '1E ' / data format of field: 4-byte REAL TTYPE10 = 'V3REF ' / label for field 10 TFORM10 = '1E ' / data format of field: 4-byte REAL TTYPE11 = 'SCALE ' / label for field 11 TFORM11 = '1E ' / data format of field: 4-byte REAL TUNIT11 = 'arcsec ' / physical unit of field TTYPE12 = 'CX10 ' / label for field 12 TFORM12 = '1E ' / data format of field: 4-byte REAL TTYPE13 = 'CX11 ' / label for field 13 TFORM13 = '1E ' / data format of field: 4-byte REAL TTYPE14 = 'CX20 ' / label for field 14 TFORM14 = '1E ' / data format of field: 4-byte REAL TTYPE15 = 'CX21 ' / label for field 15 TFORM15 = '1E ' / data format of field: 4-byte REAL TTYPE16 = 'CX22 ' / label for field 16 TFORM16 = '1E ' / data format of field: 4-byte REAL TTYPE17 = 'CX30 ' / label for field 17 TFORM17 = '1E ' / data format of field: 4-byte REAL TTYPE18 = 'CX31 ' / label for field 18 TFORM18 = '1E ' / data format of field: 4-byte REAL TTYPE19 = 'CX32 ' / label for field 19 TFORM19 = '1E ' / data format of field: 4-byte REAL TTYPE20 = 'CX33 ' / label for field 20 TFORM20 = '1E ' / data format of field: 4-byte REAL TTYPE21 = 'CX40 ' / label for field 21 TFORM21 = '1E ' / data format of field: 4-byte REAL TTYPE22 = 'CX41 ' / label for field 22 TFORM22 = '1E ' / data format of field: 4-byte REAL TTYPE23 = 'CX42 ' / label for field 23 TFORM23 = '1E ' / data format of field: 4-byte REAL TTYPE24 = 'CX43 ' / label for field 24 TFORM24 = '1E ' / data format of field: 4-byte REAL TTYPE25 = 'CX44 ' / label for field 25 TFORM25 = '1E ' / data format of field: 4-byte REAL TTYPE26 = 'CY10 ' / label for field 26 TFORM26 = '1E ' / data format of field: 4-byte REAL TTYPE27 = 'CY11 ' / label for field 27 TFORM27 = '1E ' / data format of field: 4-byte REAL TTYPE28 = 'CY20 ' / label for field 28 TFORM28 = '1E ' / data format of field: 4-byte REAL TTYPE29 = 'CY21 ' / label for field 29 TFORM29 = '1E ' / data format of field: 4-byte REAL TTYPE30 = 'CY22 ' / label for field 30 TFORM30 = '1E ' / data format of field: 4-byte REAL TTYPE31 = 'CY30 ' / label for field 31 TFORM31 = '1E ' / data format of field: 4-byte REAL TTYPE32 = 'CY31 ' / label for field 32 TFORM32 = '1E ' / data format of field: 4-byte REAL TTYPE33 = 'CY32 ' / label for field 33 TFORM33 = '1E ' / data format of field: 4-byte REAL TTYPE34 = 'CY33 ' / label for field 34 TFORM34 = '1E ' / data format of field: 4-byte REAL TTYPE35 = 'CY40 ' / label for field 35 TFORM35 = '1E ' / data format of field: 4-byte REAL TTYPE36 = 'CY41 ' / label for field 36 TFORM36 = '1E ' / data format of field: 4-byte REAL TTYPE37 = 'CY42 ' / label for field 37 TFORM37 = '1E ' / data format of field: 4-byte REAL TTYPE38 = 'CY43 ' / label for field 38 TFORM38 = '1E ' / data format of field: 4-byte REAL TTYPE39 = 'CY44 ' / label for field 39 TFORM39 = '1E ' / data format of field: 4-byte REAL TDISP1 = 'I4 ' / display format TNULL1 = -32767 / undefined value for column TDISP2 = 'A8 ' / display format TDISP3 = 'A8 ' / display format TDISP4 = 'A8 ' / display format TDISP5 = 'I8 ' / display format TNULL5 = -2147483647 / undefined value for column TDISP6 = 'I8 ' / display format TNULL6 = -2147483647 / undefined value for column TDISP7 = 'F10.2 ' / display format TDISP8 = 'F10.2 ' / display format TDISP9 = 'F12.6 ' / display format TDISP10 = 'F12.6 ' / display format TDISP11 = 'F12.4 ' / display format TDISP12 = 'E20.7 ' / display format TDISP13 = 'E20.7 ' / display format TDISP14 = 'E20.7 ' / display format TDISP15 = 'E20.7 ' / display format TDISP16 = 'E20.7 ' / display format TDISP17 = 'E20.7 ' / display format TDISP18 = 'E20.7 ' / display format TDISP19 = 'E20.7 ' / display format TDISP20 = 'E20.7 ' / display format TDISP21 = 'E20.7 ' / display format TDISP22 = 'E20.7 ' / display format TDISP23 = 'E20.7 ' / display format TDISP24 = 'E20.7 ' / display format TDISP25 = 'E20.7 ' / display format TDISP26 = 'E20.7 ' / display format TDISP27 = 'E20.7 ' / display format TDISP28 = 'E20.7 ' / display format TDISP29 = 'E20.7 ' / display format TDISP30 = 'E20.7 ' / display format TDISP31 = 'E20.7 ' / display format TDISP32 = 'E20.7 ' / display format TDISP33 = 'E20.7 ' / display format TDISP34 = 'E20.7 ' / display format TDISP35 = 'E20.7 ' / display format TDISP36 = 'E20.7 ' / display format TDISP37 = 'E20.7 ' / display format TDISP38 = 'E20.7 ' / display format TDISP39 = 'E20.7 ' / display format EXTNAME = 'IDCall.fits' / name of this binary table extension HISTORY Created Tue 14:31:35 28-Nov-2006 END +ƒ¥ +AѦYŒ +¬=Ë•¾Ë‚,wƒ%¨i{&–8ù§´Ï¦ûùU¦Sv +´üúA4ž¦%´Îª§¸¡¬,âZ¼¢d,tú›¨ÀÆ&Üô §©ðä¦ïQr¦K›½ +ç°Å¥8Ô2¦;{ +4ž‚ˆ´.›ªE—¬@OÁÁ:*,€v¨ +&Õƒ§¸&¬¦ß?¦V/à +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +C—÷m=LÌÍ;ö=Ihå4öT´‚l5 +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +?'‰Ê%ê«p&®ºÆ½M–ºØ+¦4¿÷é´Â
ÿ3ÈÍö-«3u+»Zú-ã>“¬þå`'†y¯&ãÌ'ùÄ%Sã+&f,• +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ +:øcTµDv4„ê¹´åè–Ýøe®ãCª…p}¨À'D…ý§Í·§1R¦—]™ diff --git a/stwcs/tests/data/qbu16424j_npl.fits b/stwcs/tests/data/qbu16424j_npl.fits new file mode 100644 index 0000000..6084bd3 --- /dev/null +++ b/stwcs/tests/data/qbu16424j_npl.fits @@ -0,0 +1,94 @@ +SIMPLE = T / Fits standard BITPIX = 16 / Bits per pixel NAXIS = 0 / Number of axes EXTEND = T / File may contain extensions ORIGIN = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator OBJECT = 'ACS F606W DGEOFILE' / Name of the object observed IRAF-TLM= '16:42:08 (30/11/2006)' NEXTEND = 4 / Number of standard extensions DATE = '2010-04-02T20:32:40' FILENAME= 'qbu16424j_npl.fits' / name of file FILETYPE= 'DXY GRID' / type of data found in data file TELESCOP= 'HST' / telescope used to acquire data INSTRUME= 'ACS ' / identifier for instrument used to acquire data EQUINOX = 2000.0 / equinox of celestial coord. system DETECTOR= 'WFC' / detector in use: WFC, HRC, or SBC FILTER1 = 'F606W ' / element selected from filter wheel 1 FILTER2 = 'CLEAR2L ' / element selected from filter wheel 2 HISTORY File generated from DGEOFILE: jref$qbu16424j_dxy.fits HISTORY wfc_f606w_dxy.fits renamed to o7f1140rj_dxy.fits on Jul 15 2004 HISTORY o7f1140rj_dxy.fits renamed to o8u22156j_dxy.fits on Aug 30 2004 COMMENT Accuracy to 0.01 pixels when dxy corrections included HISTORY wfc_f606w_dxy.fits renamed to qbu16424j_dxy.fits on Nov 30 2006 HISTORY Improved solution as reported in 2005 Cal Workshop HISTORY Average of 64x64 blocks from full DXY image jref$qbu16424j_dxy.fits END XTENSION= 'IMAGE ' / Image extension BITPIX = -32 / Bits per pixel NAXIS = 2 / Number of axes NAXIS1 = 65 / Axis length NAXIS2 = 33 / Axis length PCOUNT = 0 / No 'random' parameters GCOUNT = 1 / Only one group EXTNAME = 'DX ' / Extension name EXTVER = 1 / Extension version ORIGIN = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator INHERIT = F / Inherits global header DATE = '2004-04-28T16:44:21' IRAF-TLM= '16:42:08 (30/11/2006)' WCSDIM = 2 LTM1_1 = 1. LTM2_2 = 1. WAT0_001= 'system=physical' WAT1_001= 'wtype=linear' WAT2_001= 'wtype=linear' CCDCHIP = 2 / CCDCHIP from full size dgeo file LTV1 = 0 LTV2 = 0 ONAXIS1 = 4096 / NAXIS1 of full size dgeo file ONAXIS2 = 2048 / NAXIS2 of full size dgeo file CDELT1 = 64 / Coordinate increment along axis CDELT2 = 64 / Coordinate increment along axis END ¼ñ½¹¼‡QT»Èþ{<Hù’<ÜVd=N¿=&®=3)=ð÷<úî‰<´D<PCÓ;~ ºv•t»9•Ö»d~¼1õ0¼jä–¼‚B¼¥ÇŸ¼°'B¼˜ëJ¼@ À¼í¼.ª¥»Ò© +;Ãó¨;ñHd<@Ÿš<ZIo<Sµº<a3;£ÉÁ;Gq;äåþ<?<«J<ÍNR<¡àÖ<…AŸ<s+a<J Ö<1=Î<ë;&Ã=»¢°¼-O†¼“¼§âp¼ kÏ¼Š¢+¼•‚L¼¢y¼o&¼@»íe»–é¼*þ¼@_5¼6øø¼Aéù¼o¹¼ŽåÔ¼Èò#¼Ac:’tí<xLv<ж== +¼‘q¼«×p¼¶ìí¼È§3¼ÂK^¼±†ª¼o(ˆ¼;Ûk¼Fû0»½
*;j¿;¸7©;ÿv<'>l<pÜh<…±8<н<mÇ<EE_<,«H<G¢<‰U<´Ü¾<Ç´<³<—dç<šË[<|C<sÕ5<I"c;Ú´.ºd8°»Ô¶û¼zp¼C%¼œð5¼…ò¼—Q¼‘‡/¼|쀼rN»à‡àºó-(»;¡»~±ƒ»O¦ºÕ1R»¦¼É2¼°¸Ê¼à;«wº<’´:<Ѻ™<Ùµp<º#!<m®Ü<É;+”a»…ð<¼,(h¼Š-¼¦Bí¼±~¼º¥w¼Ôk¼íÍ¥¼ï?H¼ðvù¼à„¼Æ*м’7¼_ì¼HU©»Ÿ¦ö;cú‘;þT<"<N|<“¡È<¡)”<¦í<ª0<žù<Š…˜<&b<³øo<Ç¿9<Ñ×Ë<¶
<±·ß<<˜ï›<ŒRh<}¿ß<!h;.Ÿ»/ç¼eh¼¡®l¼¨0¦¼ªG¼CV¼,E¼zC¼Üä»Ñ,î:å§Ü;{XV;Iyr;¤ + ¼8ô¼YõF¼n°V¼¸>¹¼îó•¼þ½/5½]н"U3½$Øê½{`½'%½ +ð;ýo"<Gå|<ˆ›<½Ð…<ê†ù<ýú?<ðzí<ÓȨ<Ð>ñ<Õ<Ù'–<ÔÓÜ<Øl½<ÄYÝ<®~×<±ÏÕ<µ-Œ<€GÂ<ÉÙ;·<»IWD»ùͼ9»Ï¼fU¼r¾ˆ¼] ¼{½¼–Ǫ¼ ¯þ¼A*J»‡ G;zœ< +E¼Ø9¼£A¼Œä«¼@#¿»ËxL:¡î¬<¡f<DaÚ<}6•<£Gè<àp=Î<ýÏ<àüB<Üc <ñ:^<ôú—<øzÃ<ó.<Ïhž<¢ôÏ<®Ô¹<±ÝU<xž^;¼Td:ŒÑ¢»•$ˆ»êú¼0þÓ¼/cA¼k¼vú‡¼ 唼±¼ª +Ù¼o]N»Ü&:C4;}<_ͺ<¤3j<¸`<Ü0ô=r;Ò™k<TÖ<#V<¿u·<×;A<à<°XC<p»†Þ/¼Y‚x¼²+ܽ´½²w½"°>½ +O½#¢½8õ½6Q»½#Nê½%R½ù•¼ä⼘†Ž¼‰ÊD¼Hšó»{Íë;Ðíè< «<9ÌY<‡ú~<œ†g<ØÎ¹<üM=€×<ñ_n<åŒ=…=^Î=Ð=~A<×ã<€ê<¶· +<.¿å<•®<»Jb<é‚è=º<Ù¾<„«Ë<®¶%<ÙÞ<ÍM<¹ú€<•ݼ<:¡6ù¼
ŽÙ¼¡a°¼óÃ}½„%½&Àˆ½ø½X„½";·½/}½/vb½(Z½ eª¼úw›¼¶ o¼ˆÖ¼V1b»w¼×;°Ïv<ÑJ<9Æè<oÿa<«-í<Ö <ñ9ê<ó†M<Þ‘u<â7= =OÒ<ûö<ÿc<ÚdÞ<Í"¹<Ðé½<Àî<\šT<膺.>X¼5꼎þ?¼›èǼ²a›¼Èú”¼Ö…¼ÛB²¼Âm뼩3Z¼Jcü»Ò#ü»±¦›»`îò;Ϙ¼<yÕ(<´M!<ó›L=ä<Uíá<©z*<ϨK<Ö'<Æ[«<½¿ê<–°</Z;yAt»¤_ô¼rš>¼Ùê½5@½z¬½µS½»½äÉ½àÆ½hî½ ¾÷½ƒ©¼ûN¼¿‹¼Šq5¼J$×»¿®:9ëö@;þ:"<8•–<^7r<¡g<¼!7<µì»<°{<» +’<żÚ<ØÈ\<úä%<îgá<ðô¿<Ûdï<Å$d<À”U<³ã<M2;Äm:ºÑ¾Ø¼2°¼•ôq¼Âô¼á>¼ï€K¼ôÙò¼ñ‹N¼èD;¼Öµ¿¼‘ÿö¼!uw¼ +żѣ³¼È3·¼Î>„¼Ô‰,¼Û©‚¼Óéþ¼ó¸g½ õR½B +¼Ö¡<¼²”¼•L¼Œ»‰ò>9‚a +¼†Æ!¼„Iò¼Š©ç¼©n”¼¨}³¼°¼¼è‚J¼ð·¼ÊxS¼¥O•¼†Áu¼ã»fÛ;;CŸ<<n°·<šSÑ<¤øÃ<ž5<ˆT¹<ˆÀ‘<ŠaÉ<¤T1<½+Ó<ÆÚ<ÇM<Èã<¸ÜÚ<©¶<›3<¿(<Nè”;æ–9û¬4¼!¼¥ð¼â«I½–½Tó½%#½')û½,›B½I¼ù,ß¼•‹¼¤ÖºÓ%d;ÞE<iŸ„<º)Û=œæ=1ìÍ»‚Hé;¬<[Ÿ<¯ê<ò=x= +£Áº0 +P;Ñ™à<: <np`<‘¨@<’ÒF<I Z<©–<,i[<JǶ<YÀ<eò<d¤<t@¯<Š_˜<‘.<rJ·<$<˜;« +m~:0ôÖ»ð¬à¼-*}¼¼ +d +9[³»zk»©Í¼È*¼Œš¼Ë¢}¼áoÖ¼í»N¼éiÙ¼ÛL?¼Äd‚¼Òâ|¼éÉ£¼íê¼ÂÁ¯¼z³â9-Æ€<¯@<Éu^=6Ö=sŸ=+ŽÊ¼Ë +;ˆíF;w¢ºåcA»{¸º%3é»^»—–ºãCì;2y=;á!;_x|;õ¦ê<[F<YÝ5<HT]<Dwi<MyB<3ë3<ûü<fñ;ÇÞV;â„;ñ!Ð<)¶~<í;þk +<,-< Þ;\ÅØ:(ÀÜ;}l <ói;Öh^9Âåл¹nª¼M䜼ž¡ +»€’; +‚^;ªöª<XV<¦ä<êç<4N<eÑ<‚k~<dù<CØö<"»6<0iÒ<`[˜<‡é©<_Ò<
P;¼ˆ;ŸÉ¡;Ǫà<À9<Ïž;¡Æ»tç¼NÉh¼"¼´ú$¼¹zû¼ÓŸ&¼Ìž¼£±Î¼0œ@»cÝ;‹h<‘p;и°;¾/À<:Û<häÖ<•sä<VH<}½Ÿ½]íY½?ª!½O°¼…üš».Ú¼;õßâ<K¯|<C]y<f”0<˜aÐ<£‰·<”˜Ø<ƒª3<€ <ƒi<D;š5»5¼d¼€º¼Mÿé¼P’¼7¿»ãp8:VO;wü;ÏØ;¶u;™æ;Û<<¶ö<I<öÄ< ><zú<ëZ<rªÊ< æ<ˆ,Y<1¦§<!ð<¯ü<<ª;îPr<V;Qì¡»Y¼B…¼ˆws¼ì¼¸ï¼»Îe¼³Ò¼š$¼T`:Fá<b<\šì<qp2<JîB<9b<NA&<eÎ<Ù;GßÒ½}›Ñ½Wüý½82º½Ä¤¼Z +:/Š€<ER,<z=ª<‰Ð<›Ý’<ºB€<Æ£<¸*®<—Ü<ŒãŠ<~òT<[z;ä‹»ãb¼/Øx¼O® +¼R®:¼™¼ +ŸI»70:Uh9T°»+?±»“±ˆ»eÍ:´lÖ;Sœx;EÝØ;9<6;ÖO“<&ûò<’ß<ŸÙÅ<—¥<Î<^á <.Œ;à•É;ÇB<mº;•F»S¥U¼Ð¼6~ ¼p“i¼›\{¼p˼ŒÎD¼th¼ +¢Í;ª<a!‚<‰)&<ƒåÌ<Tíê<FÄÌ< & <Ö ; »™ÇN½g4w½?»û½€1¼ÎR»¶4 <]ç<”º><¯¨<¼¥1<¸´Ø<ɧž<Öwy<Ñ·ø<‹<âˆ<Wø<$¸ú¹ûˆ¼$¼7¦`¼'ô¼<þü¼8K¸¼(² +K<{èY< +~;æ¡å;ØÀŠ;°7Vºš»~z»£Ž¼(‹¼iqt¼ª»½ªº‡Õ;Ý”t<€ƒÒ<›ºc<š‹<ƒ.é;¾ñ<8„Œ »!Aë¼w?¼ä^o½AUŸ½Ø.¼–6B»`~<16š<·ÇÐ=`™=Õ=¾Û=5þ=Ô”=4Æ=œ[<ú˜=<Ésr<‹íè<çaºÉÏĻℼ2/¼>è ¼v¼¡°³¼ÄºÚ¼úgƽr"½È½¾U¼ëŽ:¼ÁN¯Z¼WÉë¼=p¼UÊ€¼/E»Õ]Ã;fË+<^P¤<»H¦<ù”F= <þ`<½ºÃ<m©Ð<" ç<+u©<9ø;N®¹³¦ºw=ˆ»Ë²‘¼œ"»‡„º!2æ;‰§(<V7<žu<«wW<³kå<†‡C;´o}»º»û¼1_ݼÅsÿ½b½8ʼâR¼@m;ËîÕ<©×<ûl= !ÿ=0¾Â=1ù#=5ªà=+Î += þ=˜Ô=ßÄ<ιl<eÊÝ;Óퟹó¾Í»že¼Ñj¼-Ê¢¼‹Ù»¼¼¬{½r^½*çî½BX½P×ê½;˜½L¼Ù†%¼°i<¼¡ä¿¼ž¨ß¼Ìb¼Òö5¼ªwë¼2‰?;HŽ<ƒ]= +b"<ã“Ý<£™r<k€Ë<c‰<?“_; ¨D;©s%;¶Çh:«,*»(v:=5P;^³<äk<•*Û<ÂŽ<¨A<‘Ã^<~šÈ;€x€¼T¼‡ûL½è ½FAÔ½6½Ç¼ÄÄF»¥`÷<X7"<õŽ=Vì=/£±=A/=O#Ù=K²$=:pÝ='0 +=Æv=63<ÀÅ<M ;¿‚:UÙQ»"ÖŠ»ªjݼh¼£ŽR¼ô¼ ½(jȽc‘½1{½{û_½U6:½#Ɉ¼õ3G¼Ãem¼Í÷9¼ó´¤½Øv½)äó½"ƒ¼ãZ ¼#H<1’Z<ô^Ê=
=Á=Ó1<ÍÀ<—*<’åÉ<>®<B@®<W*R<nd^<*Ï;Ù%<B8<1„"<TÔÓ<µ“<Ãy“<£Z8<nà"<.SÞ9R P¼9´œ¼ L‹½E”½gd½TÆ—¼÷,·¼#?m<L‚<ðöê=,y='*i=61=IR—=O=F!=+¯Z=#§~= µ~<Óü”<‰£<¸·º7ź’Xû»!¼Sx%¼ß¢Ð½>(½[=c½”¾½£W½•çs½m +†<ÅQ<¶'=<²is<²ôè<Æ]–<òšf<ò‡9<¼Çq<”~;<LÌpºÇm¶¼ˆ§E¼è½½Iª«½‰ÁQ½hóm½¦ˆ¼jÜî<8wL<èÖ]==%(=DÀ±=\ü=c\t=[8ë=E#˜=9€É=#±= +û= †=ž=à=‚?<öU<øGˆ=®ù=îH=‘¥<ÑÈ&<“ÆZ< ìÌ»¿bº¼Èùz½´c½jmñ½›Ô½€±½%h%¼´X<¶<à=È=&Ù¹=OLo=p d=omf=gúÇ=R =Jæ =9¿/=<ÒŸê<x‚ï;Ž:8êл’l¼\QļíD@½?^j½†j˜½®ÿt½¿½«J½‚F]½8
½34¼ÛϹ¼ú-7½!½r½_*>½‚^˽Œ>m½g-½©šºûƒA<ô!=G?"=\E=NÄ:=Fªl=?6Ö=L§=>·ý=/„¹=.Ê=94|=5aÎ=&÷="=b=E='E=;Š=%*<éú‚<—3<;Ù_a¼^1½E½:—0½„(`½©7K +¹.;§ë¸ËÞFº®0`ºÔT»˜Ä6¼¬¼…¼1ͼ&õ/¼USì¼^ż‰H²¼–1¼¥gÚ¼»¼®b¼š
¼Œ«¼kS®¼)³I»Ð®É:Ëw:æŽk;¼öë<a˜<¢Ñ<Sþ<o ¸<676<
Rø;Œû:Ë~é¹ò»™I\¼@mϼ‡êÕ¼œkn=º<Æ5½<k]ºÔåc¼]¢¼L¯–¼IEJ¼¦/»– ;Å;¨ÆÕ<.r<”¯Z<¹Wo<§Ž<Ÿ*<ÀÊX<ÑÖ\<ÓI<ÅV<¶É)<°˜Y<»åÞ<ÎN•<Ê‘Y<Y<oÑ•<7ˆ<B;;¶è;‡ðµ:^É»‹Ñ„¼'=ä¼e˜ú¼u¸Ã¼fzr¼‹i¿¼¢¢¬¼šü×¼Ü]¼§‡¼µ’â¼£7>¼}ºh¼cmM¼I¨I¼
绢$2»T}õ»NÅ:»¸ì<&›'<ƒü×<j‘<<º*</¯†<2‘5;îü:ýZ»Èùý¼Rºõ¼–¨ì¼Ñ„>¼úÀ=IÒ<·ª;íaö»j¿¼#;¼EÞļL(ؼÂâ»M·ï;ƒ·d<Ÿ +<‰S <J÷½<$º;¿D;&O®ºX†Ü»¦lt¼D°]¼‚O'¼¢©~¼¤ +Á¼´Ž£¼Ä\¼¼û²¼¢º{¼ Èμ/ì¼}¤!¼7 h¼€»êó»·ás»œº%ɹºŽ1Æ;+Ëî<UQ<ƒ–À<f<D“<d¹¬<ˆ÷Ú<.ˆ9㚉¼àe¼„q‘¼Ãóœ¼ú¨½Ìu<ô¨M<€Ÿ—;»ócª¼[óE¼ht¾¼{,ú¼Fì¿»³Ž;†S<§t<'V<©®Ô<²Hµ<¢/)<¤Ê[<©ö¦<¾+¶<éå<=Iý=
|ƒ<õ®<ØpÈ<äf¦<Ù2A<£|<\òÛ<(e:;ç.[:V6»ŽÚ»ü +¬<?Š<úÙ<?BË<ƒ-<ˆ=n<'ó<©<›ûÁ<|W2<nWþ<uœ”<j¥<,Ú¦;œ5}ºÖ•»0àлaJ-»GÆ~»fB„»È{¼úš¼B¼’a3¼Âar¼Ù“¼¼Ó6ë¼ÚzO¼ûeܼùWʼ¿µš¼–Bå¼fÃÆ»à¾B:º‚œ< +Ÿ>»aôV¼„¿˜¼ó"´½+É<®½;¦ý¼5I¼•±‚¼ÉÕϽ®.½ +Ãýة½
o½ +½Ö-½&AÙ½6Ž +¯}< +ðP;Î}ö;Þg;¸i<ƒœ<Q.J<zÇ<dpý;ø7^;ÿ1ºA‰9ò!e;JXD;ºI;«ž;jк‹¼ +¾;¼ØØ;Û\¯;üNP;ÙE·;gf:£W»Á¡»Uô»µ‡ù»Å%r¼^ì¼Dļ{*¼Œ•¼“éÒ¼©©¼¥f¼‘Š„¼mŸÌ¼FøZ¼iC»FU:°e:<Eð<–<Ò@|<þÙ<=X*=-MÎ=/Å=57=YF<æ¿ +<¡›V< öX»®9μ“Yö¼ÿÄ‹½86O<)ïº4ž¼é¼L–-¼ ©„¼Ò-Û¼ñ2¼éϨ¼ì8мÖÀœ¼Çžý¼¾ü|¼´dx¼‡y¼$ï<»ÓI»EnY9‡|;jÃ;Ä1Ê;뜃;ñb•< +–ã<¢<B1;[£;$ v;ØÝ_<Kº<Ø;Ù·:ËP»“^2»û‡F¼ +l»Ë:Ú;×J~<<#³<…Ü1<§_<¼Ÿl<ºïß<À3•<µ?d<¥”™<ŽJ-<’(ß<=¥À:¸æ»ú + •¼\Äd¼Žý;¼§L¼Çè¼ê¯ ½ +Ûª½¿m½M +½50½í¼é0Ó¼ËR¼´^¼®ã¼±Ñ ¼u,¼$JË»©EÒº§µ[;Ø&¬<}]<–ô<µÙ‰<͸O<Ô0ª<¸ÙÓ<—^0<P“œ;³n,:J¦»‘~Ǽ$/Ù¼–Ÿ)¼¸O¼ª’ý=ëÐ=¡=K=¸¦<Å <tj-<#
’º;\½¼ £#¼‚k¼“áY¼³c̼©œó¼kDR¼K +~»Ë“ºª=Ô;P+";ß¶;ØøA;¨HÅ;”f:辨»òÊ»‹ªU»Û9d»:º¥:—íá< +&=i¾b=<†ÃŠ»l„Ÿ¼”…š¼åZ0½Õ½?R½U&o½YÒX½Y@Ž_fc½U’ ½:f™½‘Ò¼û~3¼ÉÁ¿¼—Sn¼í:·_;<;>×<s<žÝè<ËžÅ=û
=S_ó=€áŸ=ƒX=ƒf5=ŒZv=‘ªã=‘Î=‰6=p=@£=jC<ï@À<WO»‡”S¼Åš¼øÏã½&aO½5í¢½5€½A[9½YÁ½S
Û½8Kï½ÆÞ½'¼ð‘x¼Ø`~¼¿—"¼Jâ¼Uì¼£#¼OfºÎžæ;â\À<0Š<ZWô<µO<õ:T= ýç=<FÄ +½JUg½iR4½j.½t«ã½Ù'Y½cêp»¢³®=(~û= v¾=ÜÜÆ=û³ƒ=üŠ=ä¢?=± =hÓ|=l¢<EiÔ¼FÛÔ½];½a½‹³'½œZ̽£¡½¨G޽»ÞǽÕ|ö½ç½ô4½òR½ÖO½£WѽM†)¼ºÉ +¢Ž½mˆ½÷½!b½Ëx·½\»ý¿Þ=¡=•£ô=ÊQ¦=éƒ{=êI=ÖÛS=§®@=f5=
ÌN<zó×»¼Œ1¼Ó±x½3¶J½eT\½ƒIx½Œþ½‘.>½ ~ƽ·óÙ½Í8}½Ö™h½ÔÌe½Á$–½›æª½Z¡¼ôfF¼ +=3<20<§;Í<ÐçT<Óræ<Ç20<ì¾Ú=- +=ox9=˜Ç=¹h=Êy%=ÅST=²Û=šæ1=Ù=Bþí=Úð<ÍŠ<X·Ž;°@¼a¼‡œM¼•£G¼£Üv¼™>r¼k<¼?–p¼D)¼Bl^¼jŽö¼Ÿƒi¼¾MW¼šÓ¼·L¼,½É4~½j²¼u‘ = ý=€èF=«ëÜ=ÏlJ=Ø,›=À=šlÂ=cûÑ=±8<‘¡ºÌ ¼°xj½Ü†½Rï½lfd½ƒ-|½‹ñÕ½“.Ô½¤™¶½·òd½Á$4½¿<ؽ¯J½Þ½b†½Mܼw›ˆ;E’B<föy<¬ÿ<Ø·<í¡=
°=@@Ñ=z¥W=œ›=¯ìX=·Xø=÷<=œv¡=‚ý@=LäP=p(<À=<=^$¹¾=¼XP=¼Ó ó¼é©/¼ø”:½¹l½ +´X¼ò¢÷¼Ö¥Ô¼Áµh¼’·a¼®@¼–É;¼t»àÎ;+K°<9H|½¼Ø°½ae¼†•H<Îé9=\œ‡=“‰;=·bg=È[a=³ý†=’æ6=hÑr= ùœ<£Õ:Sä¼… +<»Nk=G¯¦=‡:‡=§IS=¸g„=¬0å=d€=u¡=9K2<ÅOá;Ç
×¼!ŽÖ¼ËQ½F*½L(ƒ½mð½~Œ?½ˆ:ª½$Û½’LJ½™WϽ˜Û=½Ž_\½pν6C̼ùˆ:¼™¼$ûL:ºzÖ<l<æ’¹=Å=8V‰=j]Ü=‡Œ¹=R5=¥C=…šl=haž=AßÂ=q<Ö$.<Hk:¤}ö¼/õN¼Ê&«ä½R¶²½^3S½j½uˆÍ½xFX½_£½2ä½7Ǽ踼ª–4¼WìY»VZj<n\ê<ìŸ=.R½‘o½$‰"¼/m<ÇR!=EÞ=‚©þ=œô=¬\?=¢WŒ=‘p=y“æ=A/Ô<æËÜ<W%ºmKp¼‘äq½
(½1ÆŸ½K«½lš½€`ͽvª§½xÊë½zÙþ½y³˜½dt6½;ûf½Á’¼Ö+F¼Œ_¼7˜:É=®<vnœ<øL=&C=D‰I=j#==.=€•Ð=˜=e‚ +=?Z=ÈÀ<à¾È<lí<:Ðf¼+‘мÉä˽.!–½j +½‰½… ½ˆ¦½ˆ•½„@ô½dœh½@½$Q¼þ©Í¼£/I¼Né;¥ó:<ÈLÑ=Ø-=``½Šöy½+¶ÿ¼` <ªe!=;x°={t +=pj=ž=¹=˜÷=~Jø=U¢=)%<íÙ=<eÃ;¯¼P
`¼×yœ½R}½1]нY¼ ½bjà½\P½Qâ×½C¡þ½B+¯½,!ݼîù¼ŸÒ較Ño¼!€9Et`<Y<˜Ò¾<ýôw=%HG=6ïô=P_=_È*=fn6=XæM=;K£= Y<é¾<îË;;H@»È<¼ž½$º½]$7½‚˜º½Ž”N½Žq齆`¦½„W½|§Ù½e#½A¬Š½%ò~¼û$³¼‰³|ºrÅÄ<–+=%y=Uô=‹Uǽ€ì'½‡‘¼Lë<””b=$ÿ=e'í=‰fØ=‘Vå=…j…=]A&=4§;=ª<½Ò×<6–:§ôY»µ“#¼|ͼ߶½
s½@kr½Qí½JR`½7¤w½#P½ðǼÑ:^¼?› +ï=B¿Ø=5†!=C<ùÝ<¯,‘;ÛÙ»ƒ´¼f¼ö˜ ½H#½p_F½†|Q½Uº½‹ý@½…ÑÆ½xhœ½e-û½Uö½6I¦½‚…¼¸zI¼1 +Ò»4«s¼Ÿÿ½,C½GK0½oç©½Šæ½ôÀ½Á*½‡—m½H̽Xf´½*Wz¼ù˜=¼HK¼óV;Ùj<—Ì=DQ=[%¯=’¡=¨^W=¿ny½„k;½2i¼«Û87à<¯R˜=¥w=6‘9=3û}=5Z=$§D<ò‰å<–WÏ<HØò<cM¤<o§<{”<çq»&C¼l¼¾¼õKä¼öò¼Ð ȼ™N¼ +/;—öT<Žž¾<ůÜ<ëÅy=z–=*¬j='ô=(«=82Î=6_{=ea=1Y<ïmm<²V±<k÷‰<B&:» Ø»¼ƒJ.¼ó\•½)Ž]½]¯½„b뽌fÕ½ŽS½†Cº½{õ$½XQ¿½20¼Ék[¼}&r»DM;׫R<Ó_<öÃX=EÆ==„‡V= j
=²äÆ=à >½€!½)¤.¼º«×».u<‰W»<ú×&=ï³=
SN=š²=5[<ÆÝ<’”<‡½U<”¦<’P<Üh<K¡e:ÂL–¼8Ü¡¼”‚a¼¼Øz¼Üm¼Ôš¼•ˆá¼¿™;Í`<´ÂF=V =Á=Hc=Yíò=O……=L;ä=CÍÎ=.sZ=Ï2<õÌP<ª±è<_ª”;ñ<ìºp4¼òâ¼’ð$¼é¿$½ wå½@kú½c³:½u’½‰B½‹I˽…R®½n‚¡½71ɼë1ô¼SJÓ»wX%;Ì&o<”t´<ö£½=)}=c]°=¯=¦Õ =¯
€=¹¯E½x)é½%Ã
m»tp'<o8Ý<Ñÿ$<ïÉs<ê¶Ò<å•4<Üúw<¶ÙW<«Hž<·d<»Ò®<¹®<¨†><zϤ;_>!»ò9ð¼iqX¼´+Ö¼Ó¶œ¼Ós~¼›…ã»ØQÑ<¡þ<Ñ:Z= ¬…=LU¥=nd?=|::=q±p=fÌŠ=Jè¸=&Y<ù=<–®;ö_}9ù;Ø»ºã’¼sœÛ¼¾¡½ +H½)Õ½KÎ7½bñO½mý ½y5B½ŒsN½”o°½Œ·Ò½iÝ»½#Mɼ´hÈ»ìžh;Õ>X<‡õc<ñ§=(ø¾=Vƒ=‚
H=•\=¤¨Ž=¦Ó7=©k½ôÙ½=Õñ¼ó”ϼ^$;º˜@<¬<Æô£<ɘ®<¥§<<¶®þ<ØÆË<Õ=ù<ñu×<ö¶Ä<ëˆÓ<š"¬<æC»wÃå¼?|¼µù¼ÔÊ‘¼Ó˜©¼šò&»³1’<Q‹<ûr=6Ÿ=e™‚=ÖÍ=†¯W=’ä=pw©=U-P=$–6<°š;½a8»áõ6¼˜ÞK¼Ò¼ýX½Îî½B¯>½\×¼½oh½yš½|Û½ƒîh½˜™½¤!´½˜9ê½e¥½ú)¼¦¾:»™S<$–e<ÚëÞ=%z=Mm!=nNh=…Û}=ŒÇ×=–t±=˜}=™1A½ŽîB½\°–½!3+¼™»†”ú;¤7O<[—²<r¹<zç<„ß<¬8<Þc_= +<iŒ<n<½_$<¨%Æ<yc<F~»˜Â ¼Œ ¼È™¼Ä®p¼«D༈Öy»¸ßÌ<Aeü<ý8r=J8Ÿ=u©Ž=†y=”l=†›å=shÝ=Z:Ž=-*À<°¡:ོX)˜¼Çg ½[½<[L½TF½d“û½tÆø½z¸½|!a½ré5½g½_~^½dN½d½4̼Û1¼R¸¼»J!g<<ª<ÜH7=U,=h´=û<Úu<Aj<HÓB<lD—<~€D<uy½†¤‰½u¯^½WÄH½+½½½¼ÐW¼¶Îû¼ž©¼lP¼Üwº3p;̈<C!<2ù<f5;ü๟i”¼Cî¼r:!¼¥_6¼’üR»Ÿ8;Ç£¦<¯q†=A˜=QGg=€F =‡÷[=‡Np=}>Ö=h`Ê=Og=ºU<£ùÁ;RA>¼/¸O¼¹¥ƒ½O)½,Þü½?àz½OðƽPè½V7̽[åî½Z逽Nô«½K*½KÖ°½4^H½¿5¼Œãù»ŒvÂ;´ÀX<…'<ÜßÑ<ûÝ<Ù#¾<¡¶><HbR;¢rª»A`ºå´»—CŒ»«Ù‡½vºa½[úW½FÖ“½+ +¤¼ýµ`¼Úzv¼Ú0ô¼Ìz|¼Íä¼t[œ¼:Ï:€ˆŽ<
,6<[¶ø<.öè;ªÐá8õ»»@¿P¼
T´¼("Œ»Æ|X:ô¨Ò<,Ê&<®?= Æ=:RŽ=bËX="ñ=„Á+=}Xr=d–í=SÉk=3’ÿ=qÆ<ŽŒ‚;Îm_»Ø[̼§’l½ +ê¹¼Æ
Ÿ»ä4¼;åJÞ<ƒ®<£)º<à +<-ç<7¤Š:¬ÜÊ»p>=¼!‚¼„Y7¼¸‡ª¼×Ü ¼Øü¿¼ã1 ¼î—w¼úòç½Ü¬½æs½¼ÿ¦œ¼¾k¼LT9;;%ò<˜›–<ØÚì<Ò[ü<Éí<»Å€<K¡l¸ŠÄ +Å3=B*<çÙÁ< kŠ<(V:ˆ-¼µ¼{F´¼¯z]¼ØÜ¼°ŸŽ¼Úm„¼úTa=jwT=5^<þ +<®Ùª<fC5;Ï6ºP&ž»ÈÝ\¼<-¼ùD¼²:+¼xyF»¸”¶; `J<K<[÷=<?¬P;ÑÙ;Ì +%Ã=†<ý±D<®3m<EÆ´;|ÃQ»c¢X¼n¼ˆJ²¼¹q¶¼àÛ¼Ä]¼óÿ7=‡R‡=PFë=G¨<Îpè<š;úîæ9©ÑP¹ÆÆð»|¹¼;Ü£¼””¤¼b¿š»·É¹.?P;spo;Þÿ¸;ƒ«»]²È¼
¨¼¸¼Qˆm¼-ð¼Ê¼O$¼íõ¼:ê[¼7]*¼ó¼€/¼$„M¼5ñr¼ö¼mö¼!¨¼648»Úzº»dÌẢE»Õz»@M7:fhÂ;³üJ;§†;µ¹;´(<™ø<&kÍ<y(v<Ääã='= +Ã=á=sC=
Ú<ãÀ<¥óí<r—f;¸ lºßaÀ»å\?¼F¼ŒÜ¹¼:À¤¼ et¼×܉=Ž’ +úŸ=‡+=Oç9= t<Ωî<SÚ\;E²»IÌ»¼cè»ô*•¼;@I¼ƒMp¼Ôp¼†U¼Œ£¼Q•¼:lc¼ˆ.¼×½ˆ½3i½qö½3OU½Dн5R*½¾c½Ù~½e´½ +e´¼Cž´¼N8¥¼‡eY¼¢ÐB¼wº¼Z¼"{<¼™¼+t¼®Zˆ½
^F½:]¾½Vx½rE½|Ì8½c;½BOɽ2Ïî½5g„½ y¿½K½ÛÕ½À@½ ¯¼¹ô¯¼C—»Šà¹Í–h;\00< ³<\¼ú<†–¼<Ææ/={=u=ß„=;Ðñ=K‰–=Jh[=Lü=Rê¡=GòQ=I«l=K2“=>þS=+= +"<ó8ø<¾(<¢V¬<7\î;'ši¼=I²¼Ú!]½)Ñe½:Pν{i½• ž +†›½ð¬Ñ½Ê¡‡½˜ú8½B¡å¼·äû:Ç`<¶X¿=*ö‰=q—=˜ð=³¥d=Ϙà=ïSÑ>÷M>C_=÷ì/=àÒ[=¿Mƒ=—¢=Xb <Ì·¼»w¼·ë½Ké½77ì½F¨ˆ½Vêy½cm½nÓ½¤%½’kM½¤•-½µrý½¹Æ{½µD"½¤
½ŠÇ=½cV¨½8Cû½Â©¼™·j»Î™Ÿ:*h;•0Ø;¤tò;ËlM<šT;çA;û‚<YM¿<š:<¯28<©Q|<ÜD=S&>(>=Æçö=-Ú¼þ+½C$Ƚ¨êí½èŒ¾'›¾ F½þ`½Þq¹½º\½ŒØ½A¿ˆ¼Ä¦+ºjx$<«•Û=!k=YQ$=Œ-=¥–6=½Z=ÕCü=ê=íÝ=ç9=Øf€=»Ò +€Ç=´¢ò=*Ø/»Ä² ½*¶½šB½Ô¼û½ù!½ý-‘½ñU²½Ù^½µG½‹Kæ½BZ‡¼Íì ºƒ®X<šÕç=P‚=Ns’=‚óÀ=š~˜=¬MÞ=Â
=Ó°=Þu±=à¦{=Ô‡Ÿ=À+R=¹„=r°9=ó<j”I»¹5þ¼ˆ_ï¼Á½ +=Ã=$<w== +í»×<Á½.½Œ)½Âg4½èsA½ì¯É½äÓØ½Ó.µ½·.뽎¿º½A –¼ËœX»0¾÷<~Z= ç=IÉû=€Â=”Œ=¦
é=·ˆx=Æ•»=Òê=Ö‘==ÐÌ“=¾ÞÓ=¨´ü=Šò~=G<ø¢¹<fY`¸gFi¼Æ¼‘o‹¼ÁÆ+½ +ɽ-Ì‹½[Ñ·½e½ŽŠ{½×—½„ÉܽdkÛ½6„á½”µ¼«R»ëâr<)?Î<Éñc=!š=U%=zÔš=‹î(=[¾=‡)=y<+=Wøm=.6<öa)<oJ +½•Œ½oh +½ó½[iʽƒî¡½’NsÜq½ŽÏ½s Ö½9þý½ý8¼Âðj¼s²ÿ»€¦í;ð<§:j=
e£=BÁ=rão=€Í}=…h'=~“=fò|=J*Ì=%üª<æFØ<‘—_<+Wé;ˆú<» +Ki=JŸ_=…O=¤ù=¶$5=´Œ;=¦Íš=–ª=‚á"=Tä‘=Wb<\—ˆ»½Áº¼ì9¨½\°R½™fH½Ãû„=wU%="q¿<èÍ»aï¼ÐÕd½)0Ô½\åj½mAñ½pÚ…½dâ§½7á轑°¼Âjí¼ÛܼOyü»œ¶d;®[K<†Ø]<ÚN¼=G=F2Ÿ=]z=bè©=^Á@=Go=î<ÚéV<x>W;.’ؼÏ<¼ws@¼µG¦¼þyȽ(Zv½XO̽m…½€Á½—\½™*½‘G½}a=½`gÚ½:Þj½Îb¼À2¼ca<#)<Úݬ=)y|=dî!=ÙP=¬Ù =µ%T=¨Â”=–k¨=…ÿ=Wt= <š0µ:Gòͼ°9~½2ûj½†¼-½ë®½Ø‹Ù=f¿0=j/<š5z9ñ›¼”|Ù½ +ÅÝ¼Êæ¼]%?¹½Š{<C~Æ<Þl¶==¹==„}x=¡ð=«Ç÷='N=£¹ò=Š%ø=U‡î=(Í<°*g;G¡Ü¼”I0½ë½`ù½™h‹½ºÊã½Ùë4½ú·Q=K%==.< +<Ž'²= +k½ƒÝ½šè"½¨’½¸>—½ÆQ=SÒÎ=+Y<ú¶s<’f< +û<Âý=± =6zÐ=<=:}ß=‰-<Ÿ¬™»Bø¼Ü®]½3Õb½n8½Dï½¶Ú½¶É½¦ëo½÷½f@ʽ.1¼ÄÉ"»ø¡;³oš<‚Ìž<çc¿="Ëü=Pë¤=rñs=|§1==Œ™Q=œ=«ði=²oÝ=«äÙ=“R*=]Sé<þø~<!L÷¼2Ž\¼êh½(‰½Rðõ½uß½…‹:½‰„Q½‘w¾½™¶@=Zz[=;y=!é/<åZ$<›w-<0ðz;Ï%ð;® +;Ñ„„;…`@¹åt껨³¦¼V½¼¢êƼ±Hï¼–ªK¼(zºüRò<Hʵ<ål¡=ˆ=*©þ=7ˆ™=1™!=Sü<•œ…¼"¼ý†½Kà•½‰èG½§-0½»¡7½»é½¦â ½‹Ÿ½V³í½~”¼ˆI:àåw<€”k<Ö2-=Ê=N«M=x=9=™Ì=šgf=Ÿ:Œ=¦aœ=«W)=¯@s=¤aâ=‰}“=E’½<ßB<¦t¼.‰ß¼×÷н#½5h½K”ƒ½M̽FC›½S„h½`À=Y#Õ=F¦Q=6S=#‰Ý<ö¸<¤@<íE<‰þ!<gBË<PÈv<Ì#;³>†»Îª,¼eö=¼o¼M¨„»¯Ý„;4\¬<€–Á<ð?‰=r=&…=òá=¢<Ìš<;´Õ}¼de½"Ÿ½fÞq½ñ%½¨A½²Ÿµ½®¢ö½›Ú˽€E½J™½¥¼2ßR<¹7<¦3Š=õ=5¿e=R–o=xAå=’©û=™F=›=ž,=¢ææ=Ÿä=œ°€=¶7=ifN=Ý<¼‰o<¡o»äjʼ¨ ¼åV3½2½¼ý'l¼Ø;æ½uƒ½P¿=G)˜=@ø‚=9¬L=7ào=Ý<߇<½á–<—ÛÃ<‘á‹<—¤k<wQø<*¨¹é´Ä»Ë¹#¼T%»ûÀ¿»*™¬;Ž +Äl;q¥:õ¤8›¯:‡0H;Àð¹<`ÃŽ<¤¬K<§ƒ<¯…þ<…ßÑ;Ô‡¼ch¼å©Ã½+ؽg„ª½‰\2½–š½¡mõ½·!½‘˜P½s:½R*½)d†¼ßêW¼‚ã;·áX<•±<Þª=õ =ùU=2ñy=Gf¼=T!=T~¢=YxR=b+h=i|«=d>N=Døð=
û=<Çi…<J§Q;ÃÚºá—è»+³»¬õڻÉøºì;œ:—=ƒ;Qº´Æ¤»¨¢ÿ<Ѧ<áîé<öï= +:1…³;~r½;äÄ<L~G<‚¼<ª³Ò<“5<‚á|¼Cêÿ»ñ
;¹iå;±<7< <8Y<C¶,<‚¸†<ÄCØ= +vé½1õ½Cí½O«½^Eç½d††½]’u½RÜd½@M ½%N½fH¼ÿi¼èÞ‹¼¢ æ¼y"»¹¡» [Z;¿'J<ZÒÆ<Vò‡<u›†<š[d<¡œ<¹µì<ê-Ì=‚.=
œ= +V<ÍÎ<M3m;X]»kˇ»¼ºØ6Ê;Qã6;좆<D-<†`<™}<¨Ç<—×Å<{áܽ Mº¼Ñ½…¼„èQ¼x3»A~y;©‰;Ç'><Xãj<¯k~<í_b=éª<õb[<¨<Šä€<6ü¼:²Ž»¬>¬»çˆ¼$ÁѼ*Ò›¼oÚ±¼ƒPò¼¸Œ±¼ÿe0½Lé½ ½*—½0×W½+t½$¤½
½Óμú6ݼåQ¼¿(ʼ™³â¼{‰>¼?q ¼<‚»kW›9?±½;6z;mbA;¯6·<%žA<X™õ<¤<âVº<ï÷~==æ<òÿ <“þé;uÒh»hÉ*»÷›I¼/M(»·¥,;};;Á><LÒ<š#¡<¤ªC<²);<¿1<MM‘½s—D½=ªâ½
j +¼·à—¼€¹¼)[:+Y<9ä<„QÉ<´b<ê_è<ùè×<½£ˆ<‰Zé<Gó8< +’¤8Ryó»døj»ªòF»ð¦¼ËP¼?¼‹7¼¹,ͼÔ/Ò¼àU$¼ì:Ú¼ç¯ß¼ÕͼȻ¼ÛÝ?¼Öïd¼Â¶¼µ°z¼—³™¼vfϼ‡¸¢¼‚ei¼8Pʻ㗻ÍÉÉ»Ú\=»:ng:ñÓ¤;™[v<#Ö<†°ê<Ëk=<ÐA|<Ê™<£†…<Ê£ºÜ@¼¼VÄ[¼Œr¢¼&äºÓ¤²;©¯<Ý<‰ <‹Â<§Øó<~9:<J°½£ ½‚_½Jê,½´¼ÍíM¼\”õ»vö;º…O<Kd<zÕ‘<½óp<×Õ«<ŸË<w×–<Aÿ<)Žâ;æMÄ;›mœ;‹wÃ;°ÖO;“PH:½#»¸ã¼(-żMÞØ¼UC¼jjJ¼QÁ¼;n ¼Q©˜¼„eÚ¼ˆÖw¼“Öj¼7E¼T¼}¦‰¼¢ÛM¼Žmd¼ˆâ6¼z(e¼nþ¼g’h¼8ÌQ»_(›;MÔ$;à²)<]a±<waá<~J2<c¿>;ÚPÿºÚ!¼!èî¼fåô¼wÜß¼{Ö¼+„º{zk;np)<þ.<hÂ<})<¢°â<‚$ì<o¡—½²àh½±—½c7d½"æy¼Ø»©¼Jæ¹Øö;<®!;õÓ*<Q©=<ªåµ<Èy<ºBž<£×<šþ<§Í<‡¨|<Žbá<œÑµ<·åÞ<ºû{<›ï#<v<*UY;Î?Ó;Ã.;Ô ;Åú;0Æ+¹âëm» Ö¼M¼ ¢¼Uš/¼;X»¼U?›¼—Sм›Zœ¼¬ïl¼©jß¼›ý%¼—¹¸¼€ ù¼"^{»‘ºÕ;¢:…¥ :,ï_ºéê»OLQ¼¾Š¼€º¼º§•¼ÛH¤¼»*L¼•^¼:·Q»ç:d\<ÇÆ<;œ¾<qG<¤¨Â<¥Ü¿<¤4ཱིê0½‘.½_!½˜-¼Ä8¼6!:Ê‘P;ïö<G48<‡–<» “<Úç]<ÞÉÓ<òÔ<êȽ<Ò§<Ôr¼<þ?-=ã-=<¦=,ÄÒ=.~û=*6×=±Å<öó<äpò<ÙuB<ë£<vÒë<6·`;ßɽ;-»†ž¼ .·»ûÓ¼%7ؼ‡SW¼²k1¼£G¼€W¼šÄм¥¢¼…ýd¼Nê–¼Cù¥¼%ïè¼A¼:żdY༓žš¼¦Œ¢¼ÑQ(½„ƒ½ ¹0¼òñ®¼´ç*¼Z>"¼šsºÿ“ <9u<RE<¬¼J<µÇ×=g½= ½®0p½Šô0½MA¾½¼3)»ü*æ;Qس<'2<k´ˆ<›Ô9<áZn= diff --git a/stwcs/tests/data/simple.fits b/stwcs/tests/data/simple.fits new file mode 100644 index 0000000..6bb9fd1 --- /dev/null +++ b/stwcs/tests/data/simple.fits @@ -0,0 +1 @@ +SIMPLE = T / Fits standard BITPIX = -64 / Bits per pixel NAXIS = 2 / Number of axes NAXIS1 = 2 / Axis length NAXIS2 = 2 / Axis length EXTEND = T / File may contain extensions ORIGIN = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator EXTVER = 1 / Extension version DATE = '2007-02-08T21:38:47' / Date FITS file was generated IRAF-TLM= '2011-08-05T18:56:26' / Time of last modification EXPNAME = 'j94f05bgq ' / exposure identifier BUNIT = 'ELECTRONS' / brightness units / WFC CCD CHIP IDENTIFICATION CCDCHIP = 2 / CCD chip (1 or 2) / World Coordinate System and Related Parameters WCSAXES = 2 / number of World Coordinate System axes CRPIX1 = 2048 / x-coordinate of reference pixel CRPIX2 = 1024 / y-coordinate of reference pixel CRVAL1 = 5.63056810618 / first axis value at reference pixel CRVAL2 = -72.0545718428 / second axis value at reference pixel CTYPE1 = 'RA---TAN-SIP' / the coordinate type for the first axis CTYPE2 = 'DEC--TAN-SIP' / the coordinate type for the second axis CD1_1 = 1.29055157671E-05 / partial of first axis coordinate w.r.t. x CD1_2 = 5.95250061078E-06 / partial of first axis coordinate w.r.t. y CD2_1 = 5.02263802553E-06 / partial of second axis coordinate w.r.t. x CD2_2 = -1.26448442232E-05 / partial of second axis coordinate w.r.t. y LTV1 = 0.0000000E+00 / offset in X to subsection start LTV2 = 0.0000000E+00 / offset in Y to subsection start LTM1_1 = 1.0 / reciprocal of sampling rate in X LTM2_2 = 1.0 / reciprocal of sampling rate in Y ORIENTAT= 154.7915106251955 / position angle of image y axis (deg. e of n) RA_APER = 5.655000000000E+00 / RA of aperture reference position DEC_APER= -7.207055555556E+01 / Declination of aperture reference position PA_APER = 154.533 / Position Angle of reference aperture center (deVAFACTOR= 1.000018683511E+00 / velocity aberration plate scale factor / READOUT DEFINITION PARAMETERS CENTERA1= 2073 / subarray axis1 center pt in unbinned dect. pix CENTERA2= 1035 / subarray axis2 center pt in unbinned dect. pix SIZAXIS1= 4096 / subarray axis1 size in unbinned detector pixelsSIZAXIS2= 2048 / subarray axis2 size in unbinned detector pixelsBINAXIS1= 1 / axis1 data bin size in unbinned detector pixelsBINAXIS2= 1 / axis2 data bin size in unbinned detector pixels / PHOTOMETRY KEYWORDS PHOTMODE= 'ACS WFC1 F606W' / observation con PHOTFLAM= 7.9064521E-20 / inverse sensitivity, ergs/cm2/Ang/electron PHOTZPT = -2.1100000E+01 / ST magnitude zero point PHOTPLAM= 5.9176797E+03 / Pivot wavelength (Angstroms) PHOTBW = 6.7231146E+02 / RMS bandwidth of filter plus detector / REPEATED EXPOSURES INFO NCOMBINE= 1 / number of image sets combined during CR rejecti / DATA PACKET INFORMATION FILLCNT = 0 / number of segments containing fill ERRCNT = 0 / number of segments containing errors PODPSFF = F / podps fill present (T/F) STDCFFF = F / ST DDF fill present (T/F) STDCFFP = 'x5569 ' / ST DDF fill pattern (hex) / ON-BOARD COMPRESSION INFORMATION WFCMPRSD= F / was WFC data compressed? (T/F) CBLKSIZ = 0 / size of compression block in 2-byte words LOSTPIX = 0 / #pixels lost due to buffer overflow COMPTYP = 'None ' / compression type performed (Partial/Full/None) / IMAGE STATISTICS AND DATA QUALITY FLAGS NGOODPIX= 7822781 / number of good pixels SDQFLAGS= 31743 / serious data quality flags GOODMIN = -2.5959351E+02 / minimum value of good pixels GOODMAX = 6.5220551E+04 / maximum value of good pixels GOODMEAN= 2.0491536E+02 / mean value of good pixels SOFTERRS= 0 / number of soft error pixels (DQF=1) SNRMIN = -8.0327058E-01 / minimum signal to noise of good pixels SNRMAX = 2.1379723E+02 / maximum signal to noise of good pixels SNRMEAN = 1.0889255E+01 / mean value of signal to noise of good pixels MEANDARK= 1.5474443E+00 / average of the dark values subtracted MEANBLEV= 2.4558604E+03 / average of all bias levels subtracted MEANFLSH= 0.000000 / Mean number of counts in post flash exposure OCRVAL1 = 5.63056810618 / first axis value at reference pixel OCRVAL2 = -72.05457184279 / second axis value at reference pixel OCRPIX2 = 1024.0 / y-coordinate of reference pixel OCRPIX1 = 2048.0 / x-coordinate of reference pixel ONAXIS2 = 2048 / Axis length ONAXIS1 = 4096 / Axis length OCD2_2 = -1.26445E-05 / partial of second axis coordinate w.r.t. y OCD2_1 = 5.02243E-06 / partial of second axis coordinate w.r.t. x OORIENTA= 154.7886863186197 / position angle of image y axis (deg. e of n) OCTYPE1 = 'RA---TAN' / the coordinate type for the first axis OCD1_1 = 1.29046E-05 / partial of first axis coordinate w.r.t. x OCD1_2 = 5.9531E-06 / partial of first axis coordinate w.r.t. y OCTYPE2 = 'DEC--TAN' / the coordinate type for the second axis WCSCDATE= '21:39:44 (08/02/2007)' / Time WCS keywords were copied. A_0_2 = 2.16615952976212E-06 B_0_2 = -7.2168814507744E-06 A_1_1 = -5.1974576466834E-06 B_1_1 = 6.18443235774478E-06 A_2_0 = 8.55127758255650E-06 B_2_0 = -1.7464918770586E-06 A_0_3 = 1.08193519820265E-11 B_0_3 = -4.1754720492749E-10 A_1_2 = -5.2348707436924E-10 B_1_2 = -6.1692652686813E-11 A_2_1 = -3.9771547747287E-11 B_2_1 = -5.0857161673862E-10 A_3_0 = -4.7304448292227E-10 B_3_0 = 8.56763542781631E-11 A_0_4 = 1.49356171166049E-14 B_0_4 = -9.9570490655478E-15 A_1_3 = -2.4569975537746E-14 B_1_3 = 1.21743011568848E-14 A_2_2 = 3.46791267104378E-14 B_2_2 = -3.6614325928657E-14 A_3_1 = 1.97102297166030E-15 B_3_1 = -3.7795068054874E-15 A_4_0 = 2.37430106240231E-14 B_4_0 = -1.7687653826004E-14 A_ORDER = 4 B_ORDER = 4 CPERROR1= 0.0 / Maximum error of NPOL correction for axis 1 CPERROR2= 0.0 / Maximum error of NPOL correction for axis 2 HISTORY The following throughput tables were used: crotacomp$hst_ota_007_syn.fitHISTORY s, cracscomp$acs_wfc_im123_004_syn.fits, cracscomp$acs_f606w_005_syn.fitHISTORY s, cracscomp$acs_wfc_ebe_win12f_005_syn.fits, cracscomp$acs_wfc_ccd1_017HISTORY _syn.fits TDDALPHA= 0.03676157754622637 TDDBETA = -0.00958719251540879 IDCSCALE= 0.05 IDCV2REF= 256.6222229003906 IDCV3REF= 302.2264099121094 IDCTHETA= 0.0 OCX10 = 0.001959713482071437 OCX11 = 0.04983122487595928 OCY10 = 0.05027393143048926 OCY11 = 0.00148847536166365 SORIENTA= 154.7925383197021 / position angle of image y axis (deg. e of n) SCRVAL1 = 5.63056810618 / first axis value at reference pixel SNAXIS2 = 2048 / Axis length SNAXIS1 = 4096 / Axis length SCRVAL2 = -72.05457184279 / second axis value at reference pixel SCTYPE1 = 'RA---TAN-SIP' / the coordinate type for the first axis SCTYPE2 = 'DEC--TAN-SIP' / the coordinate type for the second axis SCD2_2 = -1.264489181627715E-05 / partial of second axis coordinate w.r.t. y SCD2_1 = 5.022886862247075E-06 / partial of second axis coordinate w.r.t. x SCD1_2 = 5.952245949610081E-06 / partial of first axis coordinate w.r.t. y SCRPIX2 = 1024.0 / y-coordinate of reference pixel SCRPIX1 = 2048.0 / x-coordinate of reference pixel SCD1_1 = 1.290545120875315E-05 / partial of first axis coordinate w.r.t. x IDCXREF = 2048.0 IDCYREF = 1024.0 WCSNAMEO= 'OPUS ' WCSAXESO= 2 CRPIX1O = 2048 CRPIX2O = 1024 CDELT1O = 1 CDELT2O = 1 CUNIT1O = 'deg ' CUNIT2O = 'deg ' CTYPE1O = 'RA---TAN-SIP' CTYPE2O = 'DEC--TAN-SIP' CRVAL1O = 5.63056810618 CRVAL2O = -72.0545718428 LONPOLEO= 180 LATPOLEO= -72.0545718428 RESTFRQO= 0 RESTWAVO= 0 CD1_1O = 1.29055157671E-05 CD1_2O = 5.95250061078E-06 CD2_1O = 5.02263802553E-06 CD2_2O = -1.26448442232E-05 D2IMEXT = 'wfc_ref68col_d2i.fits' D2IMERR = 0.002770500956103206 IDCTAB = 'postsm4_idc.fits' WCSNAMEA= 'IDC_postsm4' WCSAXESA= 2 CRPIX1A = 2048 CRPIX2A = 1024 CDELT1A = 1 CDELT2A = 1 CUNIT1A = 'deg ' CUNIT2A = 'deg ' CTYPE1A = 'RA---TAN-SIP' CTYPE2A = 'DEC--TAN-SIP' CRVAL1A = 5.63056810618 CRVAL2A = -72.0545718428 LONPOLEA= 180 LATPOLEA= -72.0545718428 RESTFRQA= 0 RESTWAVA= 0 CD1_1A = 1.29055157671E-05 CD1_2A = 5.95250061078E-06 CD2_1A = 5.02263802553E-06 CD2_2A = -1.26448442232E-05 NPOLEXT = 'qbu16424j_npl.fits' END diff --git a/stwcs/tests/test_altwcs.py b/stwcs/tests/test_altwcs.py new file mode 100644 index 0000000..86d100f --- /dev/null +++ b/stwcs/tests/test_altwcs.py @@ -0,0 +1,170 @@ +import shutil +import os +from astropy.io import fits as pyfits +from stwcs.wcsutil import altwcs +from stwcs import updatewcs +from stwcs.wcsutil import HSTWCS +import numpy as np +from numpy.testing import utils + +from . import data +data_path = os.path.split(os.path.abspath(data.__file__))[0] + + +def get_filepath(filename, directory=data_path): + return os.path.join(directory, filename) + + +def compare_wcs(w1, w2, exclude_keywords=None): + """ + Compare two WCSs. + + Parameters + ---------- + w1, w2 : `astropy.wcs.WCS` objects + exclude_keywords : list + List of keywords to excude from comparison. + """ + exclude_ctype = False + keywords = ['crval', 'crpix', 'cd'] + if exclude_keywords is not None: + exclude_keywords = [kw.lower() for kw in exclude_keywords] + if 'ctype' in exclude_keywords: + exclude_ctype = True + exclude_keywords.remove('ctype') + for kw in exclude_keywords: + keywords.remove(kw) + for kw in keywords: + kw1 = getattr(w1.wcs, kw) + kw2 = getattr(w2.wcs, kw) + utils.assert_allclose(kw1, kw2, 1e-10) + #utils.assert_allclose(w1.wcs.crpix, w2.wcs.crpix, 1e-10) + #utils.assert_allclose(w1.wcs.cd, w2.wcs.cd, 1e-10) + if not exclude_ctype: + utils.assert_array_equal(np.array(w1.wcs.ctype), np.array(w2.wcs.ctype)) + +class TestAltWCS(object): + + def setup_class(self): + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + simple_orig_file = get_filepath('simple.fits') + current_dir = os.path.abspath(os.path.curdir) + simple_file = get_filepath('simple.fits', current_dir) + acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + + try: + os.remove(acs_file) + os.remove(simple_file) + except OSError: + pass + idctab = get_filepath('postsm4_idc.fits') + npol_file = get_filepath('qbu16424j_npl.fits') + d2imfile = get_filepath('new_wfc_d2i.fits ') + + shutil.copyfile(acs_orig_file, acs_file) + shutil.copyfile(simple_orig_file, simple_file) + pyfits.setval(acs_file, ext=0, keyword="IDCTAB", value=idctab) + pyfits.setval(acs_file, ext=0, keyword="NPOLFILE", value=npol_file) + pyfits.setval(acs_file, ext=0, keyword="D2IMFILE", value=d2imfile) + + updatewcs.updatewcs(acs_file) + self.acs_file = acs_file + self.simplefits = simple_file + self.ww = HSTWCS(self.acs_file, ext=1) + + def test_archive(self): + altwcs.archiveWCS(self.acs_file, ext=1, wcskey='Z', wcsname='ZTEST', reusekey=False) + w1 = HSTWCS(self.acs_file, ext=1) + w1z = HSTWCS(self.acs_file, ext=1, wcskey='Z') + compare_wcs(w1, w1z) + + def test_archive_clobber(self): + altwcs.archiveWCS(self.acs_file, ext=1, wcskey='Z', wcsname='ZTEST', reusekey=True) + w1 = HSTWCS(self.acs_file, ext=1) + w1z = HSTWCS(self.acs_file, ext=1, wcskey='Z') + compare_wcs(w1, w1z) + + def test_restore_wcs(self): + # test restore on a file + altwcs.restoreWCS(self.acs_file, ext=1, wcskey='O') + w1o = HSTWCS(self.acs_file, ext=1, wcskey='O') + w1 = HSTWCS(self.acs_file, ext=1) + compare_wcs(w1, w1o, exclude_keywords=['ctype']) + + def test_restore_wcs_mem(self): + # test restore on an HDUList object + altwcs.archiveWCS(self.acs_file, ext=[('SCI', 1), ('SCI', 2)], wcskey='T') + pyfits.setval(self.acs_file, ext=('SCI', 1), keyword='CRVAL1', value=1) + pyfits.setval(self.acs_file, ext=('SCI', 2), keyword='CRVAL1', value=1) + f = pyfits.open(self.acs_file, mode='update') + altwcs.restoreWCS(f, ext=1, wcskey='T') + f.close() + w1o = HSTWCS(self.acs_file, ext=1, wcskey='T') + w1 = HSTWCS(self.acs_file, ext=1) + compare_wcs(w1, w1o) + + def test_restore_simple(self): + # test restore on simple fits format + altwcs.archiveWCS(self.simplefits, ext=0, wcskey='R') + pyfits.setval(self.simplefits, ext=0, keyword='CRVAL1R', value=1) + altwcs.restoreWCS(self.simplefits, ext=0, wcskey='R') + wo = HSTWCS(self.simplefits, ext=0, wcskey='R') + ws = HSTWCS(self.simplefits, ext=0) + compare_wcs(ws, wo) + + def test_restore_wcs_from_to(self): + # test restore from ... to ... + #altwcs.archiveWCS(self.acs_file, ext=[('SCI',1), ('SCI',2)], wcskey='T') + pyfits.setval(self.acs_file, ext=('SCI', 1), keyword='CRVAL1', value=1) + pyfits.setval(self.acs_file, ext=('SCI', 2), keyword='CRVAL1', value=1) + f = pyfits.open(self.acs_file, mode='update') + altwcs.restore_from_to(f, fromext='SCI', toext=['SCI', 'ERR', 'DQ'], + wcskey='T') + f.close() + w1o = HSTWCS(self.acs_file, ext=('SCI', 1), wcskey='T') + w1 = HSTWCS(self.acs_file, ext=('SCI', 1)) + compare_wcs(w1, w1o) + w2 = HSTWCS(self.acs_file, ext=('ERR', 1)) + compare_wcs(w2, w1o, exclude_keywords=['ctype']) + w3 = HSTWCS(self.acs_file, ext=('DQ', 1)) + compare_wcs(w3, w1o, exclude_keywords=['ctype']) + w4o = HSTWCS(self.acs_file, ext=4, wcskey='T') + w4 = HSTWCS(self.acs_file, ext=('SCI', 2)) + compare_wcs(w4, w4o) + w5 = HSTWCS(self.acs_file, ext=('ERR', 2)) + compare_wcs(w5, w4o, exclude_keywords=['ctype']) + w6 = HSTWCS(self.acs_file, ext=('DQ', 2)) + compare_wcs(w3, w1o, exclude_keywords=['ctype']) + + def test_delete_wcs(self): + #altwcs.archiveWCS(self.acs_file, ext=1, wcskey='Z') + altwcs.deleteWCS(self.acs_file, ext=1, wcskey='Z') + utils.assert_raises(KeyError, HSTWCS, self.acs_file, ext=1, wcskey='Z') + + def test_pars_file_mode1(self): + assert(not altwcs._parpasscheck(self.acs_file, ext=1, wcskey='Z')) + + def test_pars_file_mode2(self): + f = pyfits.open(self.acs_file) + assert(not altwcs._parpasscheck(f, ext=1, wcskey='Z')) + f.close() + + def test_pars_ext(self): + f = pyfits.open(self.acs_file, mode='update') + assert(altwcs._parpasscheck(f, ext=1, wcskey='Z')) + assert(altwcs._parpasscheck(f, ext=[('sci', 1), ('sci', 2)], wcskey='Z')) + assert(altwcs._parpasscheck(f, ext=('sci', 1), wcskey='Z')) + f.close() + + def test_pars_wcskey_not1char(self): + f = pyfits.open(self.acs_file, mode='update') + assert(not altwcs._parpasscheck(f, ext=1, wcskey='ZZ')) + f.close() + + def test_pars_wcskey(self): + f = pyfits.open(self.acs_file, mode='update') + assert(altwcs._parpasscheck(f, ext=1, wcskey=' ')) + #assert(not altwcs._parpasscheck(f, ext=1, wcskey=' ', reusekey=False)) + #assert(altwcs._parpasscheck(f, ext=1, wcskey='O')) + #assert(not altwcs._parpasscheck(f, ext=1, wcskey='O', reusekey=False)) + f.close() diff --git a/stwcs/tests/test_headerlet.py b/stwcs/tests/test_headerlet.py new file mode 100644 index 0000000..48fb470 --- /dev/null +++ b/stwcs/tests/test_headerlet.py @@ -0,0 +1,276 @@ +import shutil +import os +from astropy.io import fits +from stwcs import updatewcs +from stwcs.wcsutil import headerlet, wcsdiff +from stwcs.wcsutil import HSTWCS +import numpy as np +from numpy.testing import utils +from nose.tools import * + +from . import data +data_path = os.path.split(os.path.abspath(data.__file__))[0] + + +def get_filepath(filename, directory=data_path): + return os.path.join(directory, filename) + + +class TestCreateHeaderlet(object): + + def setup_class(self): + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + simple_orig_file = get_filepath('simple.fits') + current_dir = os.path.abspath(os.path.curdir) + simple_file = get_filepath('simple.fits', current_dir) + acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + comp_file = get_filepath('comp.fits', current_dir) + self.headerlet_name = get_filepath('acs_hlet.fits', current_dir) + + try: + os.remove(acs_file) + os.remove('comp.fits') + os.remove(simple_file) + except OSError: + pass + idctab = get_filepath('postsm4_idc.fits') + npol_file = get_filepath('qbu16424j_npl.fits') + d2imfile = get_filepath('new_wfc_d2i.fits ') + + shutil.copyfile(acs_orig_file, acs_file) + shutil.copyfile(simple_orig_file, simple_file) + fits.setval(acs_file, ext=0, keyword="IDCTAB", value=idctab) + fits.setval(acs_file, ext=0, keyword="NPOLFILE", value=npol_file) + fits.setval(acs_file, ext=0, keyword="D2IMFILE", value=d2imfile) + + updatewcs.updatewcs(acs_file) + + shutil.copyfile(acs_file, comp_file) + self.comp_file = comp_file + self.simple_file = simple_file + + def testAllExt(self): + """ + Test create_headerlet stepping through all + extensions in the science file + """ + hlet = headerlet.create_headerlet(self.comp_file, hdrname='hdr1') + hlet.writeto(self.headerlet_name, clobber=True) + assert(wcsdiff.is_wcs_identical(self.comp_file, self.headerlet_name, + [1, 4], [("SIPWCS", 1), ("SIPWCS", 2)], + verbose=True)[0]) + + def testSciExtList(self): + """ + Test create_headerlet using a list of (EXTNAME, EXTNUM) + extensions in the science file + """ + hlet = headerlet.create_headerlet(self.comp_file, + sciext=[('SCI', 1), ('SCI', 2)], + hdrname='hdr1') + hlet.writeto(self.headerlet_name, clobber=True) + assert(wcsdiff.is_wcs_identical(self.comp_file, self.headerlet_name, + [1, 4], [("SIPWCS", 1), ("SIPWCS", 2)], + verbose=True)[0]) + + def testSciExtNumList(self): + """ + Test create_headerlet using a list of EXTNUM + extensions in the science file + """ + hlet = headerlet.create_headerlet(self.comp_file, + sciext=[1, 4], + hdrname='hdr1') + hlet.writeto(self.headerlet_name, clobber=True) + assert(wcsdiff.is_wcs_identical(self.comp_file, self.headerlet_name, + [1, 4], [("SIPWCS", 1), ("SIPWCS", 2)], + verbose=True)[0]) + + def testHletFromSimpleFITS(self): + """ + Test create_headerlet using a FITS HDUList extension + number in the science file + """ + hlet = headerlet.create_headerlet(self.simple_file, + sciext=0, + hdrname='hdr1') + hlet.writeto(self.headerlet_name, clobber=True) + assert(wcsdiff.is_wcs_identical(self.simple_file, self.headerlet_name, + [0], [1], verbose=True)[0]) + + @raises(KeyError) + def test_no_HDRNAME_no_WCSNAME(self): + """ + Test create_headerlet stepping through all + extensions in the science file + """ + newf = get_filepath('ncomp.fits', os.path.abspath(os.path.curdir)) + shutil.copyfile(self.comp_file, newf) + fits.delval(newf, 'HDRNAME', ext=1) + fits.delval(newf, 'WCSNAME', ext=1) + hlet = headerlet.create_headerlet(newf) + + def test1SciExt(self): + """ + Create a headerlet from only 1 SCI ext + """ + hlet = headerlet.create_headerlet(self.comp_file, + sciext=4, + hdrname='hdr1') + hlet.writeto(self.headerlet_name, clobber=True) + assert(wcsdiff.is_wcs_identical(self.comp_file, self.headerlet_name, + [4], [1], verbose=True)[0]) + + +class TestApplyHeaderlet: + + def setup_class(self): + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + simple_orig_file = get_filepath('simple.fits') + current_dir = os.path.abspath(os.path.curdir) + simple_file = get_filepath('simple.fits', current_dir) + acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + comp_file = get_filepath('comp.fits', current_dir) + self.headerlet_name = get_filepath('acs_hlet.fits', current_dir) + + try: + os.remove(acs_file) + os.remove('comp.fits') + os.remove(simple_file) + except OSError: + pass + idctab = get_filepath('postsm4_idc.fits') + npol_file = get_filepath('qbu16424j_npl.fits') + d2imfile = get_filepath('new_wfc_d2i.fits ') + + shutil.copyfile(acs_orig_file, acs_file) + shutil.copyfile(simple_orig_file, simple_file) + fits.setval(acs_file, ext=0, keyword="IDCTAB", value=idctab) + fits.setval(acs_file, ext=0, keyword="NPOLFILE", value=npol_file) + fits.setval(acs_file, ext=0, keyword="D2IMFILE", value=d2imfile) + + updatewcs.updatewcs(acs_file) + + shutil.copyfile(acs_file, comp_file) + self.comp_file = comp_file + + ''' + def setUp(self): + try: + os.remove('j94f05bgq_flt.fits') + os.remove('comp.fits') + except OSError: + pass + shutil.copyfile('orig/j94f05bgq_flt.fits', './j94f05bgq_flt.fits') + updatewcs.updatewcs('j94f05bgq_flt.fits') + shutil.copyfile('j94f05bgq_flt.fits', './comp.fits') + ''' + """ + Does not raise an error currently, should it? + @raises(TypeError) + def testWrongDestim(self): + hlet = headerlet.create_headerlet('comp.fits', sciext=4, + hdrname='hdr1', destim='WRONG') + hlet.apply_as_primary('comp.fits') + """ + + @raises(ValueError) + def testWrongSIPModel(self): + hlet = headerlet.create_headerlet(self.comp_file, hdrname='test1', + sipname='WRONG') + hlet.apply_as_primary(self.comp_file) + + @raises(ValueError) + def testWrongNPOLModel(self): + hlet = headerlet.create_headerlet(self.comp_file, hdrname='test1', + npolfile='WRONG') + hlet.apply_as_primary(self.comp_file) + + @raises(ValueError) + def testWrongD2IMModel(self): + hlet = headerlet.create_headerlet(self.comp_file, hdrname='test1', + d2imfile='WRONG') + hlet.apply_as_primary(self.comp_file) + + def test_apply_as_primary_method(self): + hlet = headerlet.create_headerlet(self.comp_file, hdrname='test2') + hlet['SIPWCS', 1].header['CRPIX1'] = 1 + hlet['SIPWCS', 1].header['CRPIX2'] = 1 + hlet['SIPWCS', 2].header['CRPIX1'] = 2 + hlet['SIPWCS', 2].header['CRPIX2'] = 2 + hlet.apply_as_primary(self.comp_file) + hlet.writeto(self.headerlet_name, clobber=True) + assert(wcsdiff.is_wcs_identical(self.comp_file, self.headerlet_name, + [('SCI', 1), ('SCI', 2)], + [("SIPWCS", 1), ("SIPWCS", 2)], + verbose=True)[0]) + # repeated hlet should not change sci file + try: + headerlet.apply_headerlet_as_primary(self.comp_file, 'hdr1_hlet.fits') + except: + pass + assert(wcsdiff.is_wcs_identical(self.comp_file, self.headerlet_name, + [('SCI', 1), ('SCI', 2)], + [("SIPWCS", 1), ("SIPWCS", 2)], + verbose=True)[0]) + + def test_apply_as_alternate_method(self): + hlet = headerlet.create_headerlet(self.comp_file, hdrname='test1') + hlet.apply_as_alternate(self.comp_file, wcskey='K', wcsname='KK') + hlet.writeto(self.headerlet_name, clobber=True) + assert(wcsdiff.is_wcs_identical(self.comp_file, self.headerlet_name, + [('SCI', 1), ('SCI', 2)], + [("SIPWCS", 1), ("SIPWCS", 2)], + scikey='K', verbose=True)[0]) + headerlet.apply_headerlet_as_alternate(self.comp_file, + self.headerlet_name, wcskey='P') + assert(wcsdiff.is_wcs_identical(self.comp_file, self.headerlet_name, + [('SCI', 1), ('SCI', 2)], + [("SIPWCS", 1), ("SIPWCS", 2)], + scikey='P', verbose=True)[0]) + + +class TestLegacyFiles: + + def setup_class(self): + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + current_dir = os.path.abspath(os.path.curdir) + self.acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + self.legacy_file = get_filepath('jlegacy.fits', current_dir) + + try: + os.remove(self.acs_file) + os.remove(self.legacy_file) + except OSError: + pass + idctab = get_filepath('postsm4_idc.fits') + npol_file = get_filepath('qbu16424j_npl.fits') + d2imfile = get_filepath('new_wfc_d2i.fits ') + + shutil.copyfile(acs_orig_file, self.acs_file) + shutil.copyfile(acs_orig_file, self.legacy_file) + + fits.setval(self.acs_file, ext=0, keyword="IDCTAB", value=idctab) + fits.setval(self.acs_file, ext=0, keyword="NPOLFILE", value=npol_file) + fits.setval(self.acs_file, ext=0, keyword="D2IMFILE", value=d2imfile) + updatewcs.updatewcs(self.acs_file) + + ''' + def setUp(self): + try: + os.remove('j94f05bgq_flt.fits') + os.remove('jlegacy.fits') + except OSError: + pass + shutil.copyfile('orig/j94f05bgq_flt.fits', './j94f05bgq_flt.fits') + shutil.copyfile('j94f05bgq_flt.fits', './jlegacy.fits') + updatewcs.updatewcs('j94f05bgq_flt.fits') + ''' + + def test_update_legacy_file(self): + hlet = headerlet.create_headerlet(self.acs_file) + hlet.apply_as_primary(self.legacy_file, archive=False, attach=False) + assert(wcsdiff.is_wcs_identical(self.acs_file, self.legacy_file, + [('SCI', 1), ('SCI', 2)], + [("SCI", 1), ("SCI", 2)], + verbose=True)[0]) diff --git a/stwcs/tests/test_updatewcs.py b/stwcs/tests/test_updatewcs.py new file mode 100644 index 0000000..dc5a34f --- /dev/null +++ b/stwcs/tests/test_updatewcs.py @@ -0,0 +1,305 @@ +import shutil +import os + +from astropy import wcs +from astropy.io import fits +from stwcs import updatewcs +from stwcs.updatewcs import apply_corrections +from stwcs.distortion import utils as dutils +from stwcs.wcsutil import HSTWCS +import numpy as np +from numpy.testing import utils +import pytest + + +from . import data +data_path = os.path.split(os.path.abspath(data.__file__))[0] + + +def get_filepath(filename, directory=data_path): + return os.path.join(directory, filename) + + +class TestStwcs(object): + + def setup_class(self): + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + current_dir = os.path.abspath(os.path.curdir) + self.ref_file = get_filepath('j94f05bgq_flt_r.fits', current_dir) + self.acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + + try: + os.remove(self.acs_file) + os.remove(self.ref_file) + except OSError: + pass + idctab = get_filepath('postsm4_idc.fits') + npol_file = get_filepath('qbu16424j_npl.fits') + d2imfile = get_filepath('new_wfc_d2i.fits ') + + shutil.copyfile(acs_orig_file, self.acs_file) + + fits.setval(self.acs_file, ext=0, keyword="IDCTAB", value=idctab) + fits.setval(self.acs_file, ext=0, keyword="NPOLFILE", value=npol_file) + fits.setval(self.acs_file, ext=0, keyword="D2IMFILE", value=d2imfile) + updatewcs.updatewcs(self.acs_file) + #self.ref_file = ref_file + shutil.copyfile(self.acs_file, self.ref_file) + + self.w1 = HSTWCS(self.acs_file, ext=1) + self.w4 = HSTWCS(self.acs_file, ext=4) + self.w = wcs.WCS() + + def test_default(self): + crval = np.array([0., 0.]) + crpix = np.array([0., 0.]) + cdelt = np.array([1., 1.]) + pc = np.array([[1., 0], [0., 1.]]) + ctype = np.array(['', '']) + utils.assert_almost_equal(self.w.wcs.crval, crval) + utils.assert_almost_equal(self.w.wcs.crpix, crpix) + utils.assert_almost_equal(self.w.wcs.cdelt, cdelt) + utils.assert_almost_equal(self.w.wcs.pc, pc) + assert((self.w.wcs.ctype == np.array(['', ''])).all()) + + def test_simple_sci1(self): + """ + A simple sanity check that CRPIX corresponds to CRVAL within wcs + """ + px1 = np.array([self.w1.wcs.crpix]) + rd1 = np.array([self.w1.wcs.crval]) + assert(((self.w1.all_pix2world(px1, 1) - rd1) < 5E-7).all()) + + def test_simple_sci2(self): + """ + A simple sanity check that CRPIX corresponds to CRVAL within wcs + """ + px4 = np.array([self.w4.wcs.crpix]) + rd4 = np.array([self.w4.wcs.crval]) + assert(((self.w4.all_pix2world(px4, 1) - rd4) < 2E-6).all()) + + def test_pipeline_sci1(self): + """ + Internal consistency check of the wcs pipeline + """ + px = np.array([[100, 125]]) + sky1 = self.w1.all_pix2world(px, 1) + dpx1 = self.w1.det2im(px, 1) + #fpx1 = dpx1 + (self.w1.sip_pix2foc(dpx1,1)-dpx1) + (self.w1.p4_pix2foc(dpx1,1)-dpx1) + fpx1 = dpx1 + (self.w1.sip_pix2foc(dpx1, 1)-dpx1+self.w1.wcs.crpix) + \ + (self.w1.p4_pix2foc(dpx1, 1)-dpx1) + pipelinepx1 = self.w1.wcs_pix2world(fpx1, 1) + utils.assert_almost_equal(pipelinepx1, sky1) + + def test_pipeline_sci2(self): + """ + Internal consistency check of the wcs pipeline + """ + px = np.array([[100, 125]]) + sky4 = self.w4.all_pix2world(px, 1) + dpx4 = self.w4.det2im(px, 1) + fpx4 = dpx4 + (self.w4.sip_pix2foc(dpx4, 1)-dpx4 + self.w4.wcs.crpix) + \ + (self.w4.p4_pix2foc(dpx4, 1)-dpx4) + pipelinepx4 = self.w4.wcs_pix2world(fpx4, 1) + utils.assert_almost_equal(pipelinepx4, sky4) + + def test_outwcs(self): + """ + Test the WCS of the output image + """ + outwcs = dutils.output_wcs([self.w1, self.w4]) + + #print('outwcs.wcs.crval = {0}'.format(outwcs.wcs.crval)) + utils.assert_allclose( + outwcs.wcs.crval, np.array([5.65109952, -72.0674181]), rtol=1e-7) + + utils.assert_almost_equal(outwcs.wcs.crpix, np.array([2107.0, 2118.5])) + utils.assert_almost_equal( + outwcs.wcs.cd, + np.array([[1.2787045268089949e-05, 5.4215042082174661e-06], + [5.4215042082174661e-06, -1.2787045268089949e-05]])) + assert(outwcs._naxis1 == 4214) + assert(outwcs._naxis2 == 4237) + + def test_repeate(self): + # make sure repeated runs of updatewcs do not change the WCS. + updatewcs.updatewcs(self.acs_file) + w1 = HSTWCS(self.acs_file, ext=('SCI', 1)) + w4 = HSTWCS(self.acs_file, ext=('SCI', 2)) + w1r = HSTWCS(self.ref_file, ext=('SCI', 1)) + w4r = HSTWCS(self.ref_file, ext=('SCI', 2)) + utils.assert_almost_equal(w1.wcs.crval, w1r.wcs.crval) + utils.assert_almost_equal(w1.wcs.crpix, w1r.wcs.crpix) + utils.assert_almost_equal(w1.wcs.cdelt, w1r.wcs.cdelt) + utils.assert_almost_equal(w1.wcs.cd, w1r.wcs.cd) + assert((np.array(w1.wcs.ctype) == np.array(w1r.wcs.ctype)).all()) + utils.assert_almost_equal(w1.sip.a, w1r.sip.a) + utils.assert_almost_equal(w1.sip.b, w1r.sip.b) + utils.assert_almost_equal(w4.wcs.crval, w4r.wcs.crval) + utils.assert_almost_equal(w4.wcs.crpix, w4r.wcs.crpix) + utils.assert_almost_equal(w4.wcs.cdelt, w4r.wcs.cdelt) + utils.assert_almost_equal(w4.wcs.cd, w4r.wcs.cd) + assert((np.array(self.w4.wcs.ctype) == np.array(w4r.wcs.ctype)).all()) + utils.assert_almost_equal(w4.sip.a, w4r.sip.a) + utils.assert_almost_equal(w4.sip.b, w4r.sip.b) + + +def test_remove_npol_distortion(): + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + current_dir = os.path.abspath(os.path.curdir) + acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + idctab = get_filepath('postsm4_idc.fits') + npol_file = get_filepath('qbu16424j_npl.fits') + d2imfile = get_filepath('new_wfc_d2i.fits ') + + try: + os.remove(acs_file) + except OSError: + pass + + shutil.copyfile(acs_orig_file, acs_file) + fits.setval(acs_file, ext=0, keyword="IDCTAB", value=idctab) + fits.setval(acs_file, ext=0, keyword="NPOLFILE", value=npol_file) + fits.setval(acs_file, ext=0, keyword="D2IMFILE", value=d2imfile) + + updatewcs.updatewcs(acs_file) + fits.setval(acs_file, keyword="NPOLFILE", value="N/A") + updatewcs.updatewcs(acs_file) + w1 = HSTWCS(acs_file, ext=("SCI", 1)) + w4 = HSTWCS(acs_file, ext=("SCI", 2)) + assert w1.cpdis1 is None + assert w4.cpdis2 is None + + +def test_remove_d2im_distortion(): + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + current_dir = os.path.abspath(os.path.curdir) + acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + + idctab = get_filepath('postsm4_idc.fits') + npol_file = get_filepath('qbu16424j_npl.fits') + d2imfile = get_filepath('new_wfc_d2i.fits ') + + try: + os.remove(acs_file) + except OSError: + pass + shutil.copyfile(acs_orig_file, acs_file) + fits.setval(acs_file, ext=0, keyword="IDCTAB", value=idctab) + fits.setval(acs_file, ext=0, keyword="NPOLFILE", value=npol_file) + fits.setval(acs_file, ext=0, keyword="D2IMFILE", value=d2imfile) + + updatewcs.updatewcs(acs_file) + fits.setval(acs_file, keyword="D2IMFILE", value="N/A") + updatewcs.updatewcs(acs_file) + w1 = HSTWCS(acs_file, ext=("SCI", 1)) + w4 = HSTWCS(acs_file, ext=("SCI", 2)) + assert w1.det2im1 is None + assert w4.det2im2 is None + + +def test_missing_idctab(): + """ Tests that an IOError is raised if an idctab file is not found on disk.""" + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + current_dir = os.path.abspath(os.path.curdir) + acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + + try: + os.remove(acs_file) + except OSError: + pass + shutil.copyfile(acs_orig_file, acs_file) + + fits.setval(acs_file, keyword="IDCTAB", value="my_missing_idctab.fits") + with pytest.raises(IOError): + updatewcs.updatewcs(acs_file) + + +def test_missing_npolfile(): + """ Tests that an IOError is raised if an NPOLFILE file is not found on disk.""" + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + current_dir = os.path.abspath(os.path.curdir) + acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + + try: + os.remove(acs_file) + except OSError: + pass + + shutil.copyfile(acs_orig_file, acs_file) + + fits.setval(acs_file, keyword="NPOLFILE", value="missing_npl.fits") + with pytest.raises(IOError): + updatewcs.updatewcs(acs_file) + + +def test_missing_d2imfile(): + """ Tests that an IOError is raised if a D2IMFILE file is not found on disk.""" + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + current_dir = os.path.abspath(os.path.curdir) + acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + + try: + os.remove(acs_file) + except OSError: + pass + + shutil.copyfile(acs_orig_file, acs_file) + + fits.setval(acs_file, keyword="D2IMFILE", value="missing_d2i.fits") + with pytest.raises(IOError): + updatewcs.updatewcs(acs_file) + + +def test_found_idctab(): + """ Tests the return value of apply_corrections.foundIDCTAB().""" + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + current_dir = os.path.abspath(os.path.curdir) + acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + npol_file = get_filepath('qbu16424j_npl.fits') + d2imfile = get_filepath('new_wfc_d2i.fits ') + + try: + os.remove(acs_file) + except OSError: + pass + + shutil.copyfile(acs_orig_file, acs_file) + fits.setval(acs_file, ext=0, keyword="NPOLFILE", value=npol_file) + fits.setval(acs_file, ext=0, keyword="D2IMFILE", value=d2imfile) + fits.setval(acs_file, keyword="IDCTAB", value="N/A") + corrections = apply_corrections.setCorrections(acs_file) + assert('MakeWCS' not in corrections) + assert('TDDCor' not in corrections) + assert('CompSIP' not in corrections) + + +def test_add_radesys(): + """ test that RADESYS was successfully added to headers.""" + acs_orig_file = get_filepath('j94f05bgq_flt.fits') + current_dir = os.path.abspath(os.path.curdir) + acs_file = get_filepath('j94f05bgq_flt.fits', current_dir) + + idctab = get_filepath('postsm4_idc.fits') + npol_file = get_filepath('qbu16424j_npl.fits') + d2imfile = get_filepath('new_wfc_d2i.fits ') + + try: + os.remove(acs_file) + except OSError: + pass + + shutil.copyfile(acs_orig_file, acs_file) + fits.setval(acs_file, ext=0, keyword="IDCTAB", value=idctab) + fits.setval(acs_file, ext=0, keyword="NPOLFILE", value=npol_file) + fits.setval(acs_file, ext=0, keyword="D2IMFILE", value=d2imfile) + + #shutil.copyfile('orig/ibof01ahq_flt.fits', './ibof01ahq_flt.fits') + updatewcs.updatewcs(acs_file) + # updatewcs.updatewcs('ibof01ahq_flt.fits') + for ext in [('SCI', 1), ('SCI', 2)]: + hdr = fits.getheader(acs_file, ext) + assert hdr['RADESYS'] == 'FK5' + + #hdr = fits.getheader('ibof01ahq_flt.fits', ext=('SCI', 1)) + #assert hdr['RADESYS'] == 'ICRS' diff --git a/stwcs/updatewcs/__init__.py b/stwcs/updatewcs/__init__.py new file mode 100644 index 0000000..eb5e850 --- /dev/null +++ b/stwcs/updatewcs/__init__.py @@ -0,0 +1,376 @@ +from __future__ import absolute_import, division, print_function # confidence high + +from astropy.io import fits +from stwcs import wcsutil +from stwcs.wcsutil import HSTWCS +import stwcs + +from astropy import wcs as pywcs +import astropy + +from . import utils, corrections, makewcs +from . import npol, det2im +from stsci.tools import parseinput, fileutil +from . import apply_corrections + +import time +import logging +logger = logging.getLogger('stwcs.updatewcs') + +import atexit +atexit.register(logging.shutdown) + +#Note: The order of corrections is important + +def updatewcs(input, vacorr=True, tddcorr=True, npolcorr=True, d2imcorr=True, + checkfiles=True, verbose=False): + """ + + Updates HST science files with the best available calibration information. + This allows users to retrieve from the archive self contained science files + which do not require additional reference files. + + Basic WCS keywords are updated in the process and new keywords (following WCS + Paper IV and the SIP convention) as well as new extensions are added to the science files. + + + Example + ------- + >>>from stwcs import updatewcs + >>>updatewcs.updatewcs(filename) + + Dependencies + ------------ + `stsci.tools` + `astropy.io.fits` + `astropy.wcs` + + Parameters + ---------- + input: a python list of file names or a string (wild card characters allowed) + input files may be in fits, geis or waiver fits format + vacorr: boolean + If True, vecocity aberration correction will be applied + tddcorr: boolean + If True, time dependent distortion correction will be applied + npolcorr: boolean + If True, a Lookup table distortion will be applied + d2imcorr: boolean + If True, detector to image correction will be applied + checkfiles: boolean + If True, the format of the input files will be checked, + geis and waiver fits files will be converted to MEF format. + Default value is True for standalone mode. + """ + if verbose == False: + logger.setLevel(100) + else: + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + log_filename = 'stwcs.log' + fh = logging.FileHandler(log_filename, mode='w') + fh.setLevel(logging.DEBUG) + fh.setFormatter(formatter) + logger.addHandler(fh) + logger.setLevel(verbose) + args = "vacorr=%s, tddcorr=%s, npolcorr=%s, d2imcorr=%s, checkfiles=%s, \ + " % (str(vacorr), str(tddcorr), str(npolcorr), + str(d2imcorr), str(checkfiles)) + logger.info('\n\tStarting UPDATEWCS: %s', time.asctime()) + + files = parseinput.parseinput(input)[0] + logger.info("\n\tInput files: %s, " % [i for i in files]) + logger.info("\n\tInput arguments: %s" %args) + if checkfiles: + files = checkFiles(files) + if not files: + print('No valid input, quitting ...\n') + return + + for f in files: + acorr = apply_corrections.setCorrections(f, vacorr=vacorr, \ + tddcorr=tddcorr,npolcorr=npolcorr, d2imcorr=d2imcorr) + if 'MakeWCS' in acorr and newIDCTAB(f): + logger.warning("\n\tNew IDCTAB file detected. All current WCSs will be deleted") + cleanWCS(f) + + makecorr(f, acorr) + + return files + +def makecorr(fname, allowed_corr): + """ + Purpose + ======= + Applies corrections to the WCS of a single file + + :Parameters: + `fname`: string + file name + `acorr`: list + list of corrections to be applied + """ + logger.info("Allowed corrections: {0}".format(allowed_corr)) + f = fits.open(fname, mode='update') + #Determine the reference chip and create the reference HSTWCS object + nrefchip, nrefext = getNrefchip(f) + wcsutil.restoreWCS(f, nrefext, wcskey='O') + rwcs = HSTWCS(fobj=f, ext=nrefext) + rwcs.readModel(update=True,header=f[nrefext].header) + + if 'DET2IMCorr' in allowed_corr: + kw2update = det2im.DET2IMCorr.updateWCS(f) + for kw in kw2update: + f[1].header[kw] = kw2update[kw] + + for i in range(len(f))[1:]: + extn = f[i] + + if 'extname' in extn.header: + extname = extn.header['extname'].lower() + if extname == 'sci': + wcsutil.restoreWCS(f, ext=i, wcskey='O') + sciextver = extn.header['extver'] + ref_wcs = rwcs.deepcopy() + hdr = extn.header + ext_wcs = HSTWCS(fobj=f, ext=i) + ### check if it exists first!!! + # 'O ' can be safely archived again because it has been restored first. + wcsutil.archiveWCS(f, ext=i, wcskey="O", wcsname="OPUS", reusekey=True) + ext_wcs.readModel(update=True,header=hdr) + for c in allowed_corr: + if c != 'NPOLCorr' and c != 'DET2IMCorr': + corr_klass = corrections.__getattribute__(c) + kw2update = corr_klass.updateWCS(ext_wcs, ref_wcs) + for kw in kw2update: + hdr[kw] = kw2update[kw] + # give the primary WCS a WCSNAME value + idcname = f[0].header.get('IDCTAB', " ") + if idcname.strip() and 'idc.fits' in idcname: + wname = ''.join(['IDC_', + utils.extract_rootname(idcname,suffix='_idc')]) + else: wname = " " + hdr['WCSNAME'] = wname + + elif extname in ['err', 'dq', 'sdq', 'samp', 'time']: + cextver = extn.header['extver'] + if cextver == sciextver: + hdr = f[('SCI',sciextver)].header + w = pywcs.WCS(hdr, f) + copyWCS(w, extn.header) + + else: + continue + + if 'NPOLCorr' in allowed_corr: + kw2update = npol.NPOLCorr.updateWCS(f) + for kw in kw2update: + f[1].header[kw] = kw2update[kw] + # Finally record the version of the software which updated the WCS + if 'HISTORY' in f[0].header: + f[0].header.set('UPWCSVER', value=stwcs.__version__, + comment="Version of STWCS used to updated the WCS", + before='HISTORY') + f[0].header.set('PYWCSVER', value=astropy.__version__, + comment="Version of PYWCS used to updated the WCS", + before='HISTORY') + elif 'ASN_MTYP' in f[0].header: + f[0].header.set('UPWCSVER', value=stwcs.__version__, + comment="Version of STWCS used to updated the WCS", + after='ASN_MTYP') + f[0].header.set('PYWCSVER', value=astropy.__version__, + comment="Version of PYWCS used to updated the WCS", + after='ASN_MTYP') + else: + # Find index of last non-blank card, and insert this new keyword after that card + for i in range(len(f[0].header) - 1, 0, -1): + if f[0].header[i].strip() != '': + break + f[0].header.set('UPWCSVER', stwcs.__version__, + "Version of STWCS used to updated the WCS", + after=i) + f[0].header.set('PYWCSVER', astropy.__version__, + "Version of PYWCS used to updated the WCS", + after=i) + # add additional keywords to be used by headerlets + distdict = utils.construct_distname(f,rwcs) + f[0].header['DISTNAME'] = distdict['DISTNAME'] + f[0].header['SIPNAME'] = distdict['SIPNAME'] + # Make sure NEXTEND keyword remains accurate + f[0].header['NEXTEND'] = len(f)-1 + f.close() + +def copyWCS(w, ehdr): + """ + This is a convenience function to copy a WCS object + to a header as a primary WCS. It is used only to copy the + WCS of the 'SCI' extension to the headers of 'ERR', 'DQ', 'SDQ', + 'TIME' or 'SAMP' extensions. + """ + hwcs = w.to_header() + + if w.wcs.has_cd(): + wcsutil.pc2cd(hwcs) + for k in hwcs.keys(): + key = k[:7] + ehdr[key] = hwcs[k] + +def getNrefchip(fobj): + """ + + Finds which FITS extension holds the reference chip. + + The reference chip has EXTNAME='SCI', can be in any extension and + is instrument specific. This functions provides mappings between + sci extensions, chips and fits extensions. + In the case of a subarray when the reference chip is missing, the + first 'SCI' extension is the reference chip. + + Parameters + ---------- + fobj: `astropy.io.fits.HDUList` object + """ + nrefext = 1 + nrefchip = 1 + instrument = fobj[0].header['INSTRUME'] + + if instrument == 'WFPC2': + chipkw = 'DETECTOR' + extvers = [("SCI",img.header['EXTVER']) for img in + fobj[1:] if img.header['EXTNAME'].lower()=='sci'] + detectors = [img.header[chipkw] for img in fobj[1:] if + img.header['EXTNAME'].lower()=='sci'] + fitsext = [i for i in range(len(fobj))[1:] if + fobj[i].header['EXTNAME'].lower()=='sci'] + det2ext=dict(list(zip(detectors, extvers))) + ext2det=dict(list(zip(extvers, detectors))) + ext2fitsext=dict(list(zip(extvers, fitsext))) + + if 3 not in detectors: + nrefchip = ext2det.pop(extvers[0]) + nrefext = ext2fitsext.pop(extvers[0]) + else: + nrefchip = 3 + extname = det2ext.pop(nrefchip) + nrefext = ext2fitsext.pop(extname) + + elif (instrument == 'ACS' and fobj[0].header['DETECTOR'] == 'WFC') or \ + (instrument == 'WFC3' and fobj[0].header['DETECTOR'] == 'UVIS'): + chipkw = 'CCDCHIP' + extvers = [("SCI",img.header['EXTVER']) for img in + fobj[1:] if img.header['EXTNAME'].lower()=='sci'] + detectors = [img.header[chipkw] for img in fobj[1:] if + img.header['EXTNAME'].lower()=='sci'] + fitsext = [i for i in range(len(fobj))[1:] if + fobj[i].header['EXTNAME'].lower()=='sci'] + det2ext=dict(list(zip(detectors, extvers))) + ext2det=dict(list(zip(extvers, detectors))) + ext2fitsext=dict(list(zip(extvers, fitsext))) + + if 2 not in detectors: + nrefchip = ext2det.pop(extvers[0]) + nrefext = ext2fitsext.pop(extvers[0]) + else: + nrefchip = 2 + extname = det2ext.pop(nrefchip) + nrefext = ext2fitsext.pop(extname) + else: + for i in range(len(fobj)): + extname = fobj[i].header.get('EXTNAME', None) + if extname != None and extname.lower == 'sci': + nrefext = i + break + + return nrefchip, nrefext + +def checkFiles(input): + """ + Checks that input files are in the correct format. + Converts geis and waiver fits files to multiextension fits. + """ + from stsci.tools.check_files import geis2mef, waiver2mef, checkFiles + logger.info("\n\tChecking files %s" % input) + removed_files = [] + newfiles = [] + if not isinstance(input, list): + input=[input] + + for file in input: + try: + imgfits,imgtype = fileutil.isFits(file) + except IOError: + logger.warning( "\n\tFile %s could not be found, removing it from the input list.\n" %file) + removed_files.append(file) + continue + # Check for existence of waiver FITS input, and quit if found. + # Or should we print a warning and continue but not use that file + if imgfits: + if imgtype == 'waiver': + newfilename = waiver2mef(file, convert_dq=True) + if newfilename == None: + logger.warning("\n\tRemoving file %s from input list - could not convert waiver to mef" %file) + removed_files.append(file) + else: + newfiles.append(newfilename) + else: + newfiles.append(file) + + # If a GEIS image is provided as input, create a new MEF file with + # a name generated using 'buildFITSName()' + # Convert the corresponding data quality file if present + if not imgfits: + newfilename = geis2mef(file, convert_dq=True) + if newfilename == None: + logger.warning("\n\tRemoving file %s from input list - could not convert geis to mef" %file) + removed_files.append(file) + else: + newfiles.append(newfilename) + if removed_files: + logger.warning('\n\tThe following files will be removed from the list of files to be processed %s' % removed_files) + + newfiles = checkFiles(newfiles)[0] + logger.info("\n\tThese files passed the input check and will be processed: %s" % newfiles) + return newfiles + +def newIDCTAB(fname): + #When this is called we know there's a kw IDCTAB in the header + hdul = fits.open(fname) + idctab = fileutil.osfn(hdul[0].header['IDCTAB']) + try: + #check for the presence of IDCTAB in the first extension + oldidctab = fileutil.osfn(hdul[1].header['IDCTAB']) + except KeyError: + return False + if idctab == oldidctab: + return False + else: + return True + +def cleanWCS(fname): + # A new IDCTAB means all previously computed WCS's are invalid + # We are deleting all of them except the original OPUS WCS. + f = fits.open(fname, mode='update') + keys = wcsutil.wcskeys(f[1].header) + # Remove the primary WCS from the list + try: + keys.remove(' ') + except ValueError: + pass + fext = list(range(1, len(f))) + for key in keys: + try: + wcsutil.deleteWCS(fname, ext=fext, wcskey=key) + except KeyError: + # Some extensions don't have the alternate (or any) WCS keywords + continue + +def getCorrections(instrument): + """ + Print corrections available for an instrument + + :Parameters: + `instrument`: string, one of 'WFPC2', 'NICMOS', 'STIS', 'ACS', 'WFC3' + """ + acorr = apply_corrections.allowed_corrections[instrument] + + print("The following corrections will be performed for instrument %s\n" % instrument) + for c in acorr: print(c,': ' , apply_corrections.cnames[c]) diff --git a/stwcs/updatewcs/apply_corrections.py b/stwcs/updatewcs/apply_corrections.py new file mode 100644 index 0000000..86b623a --- /dev/null +++ b/stwcs/updatewcs/apply_corrections.py @@ -0,0 +1,248 @@ +from __future__ import division, print_function # confidence high + +import os +from astropy.io import fits +import time +from stsci.tools import fileutil +import os.path +from stwcs.wcsutil import altwcs +from . import utils +from . import wfpc2_dgeo + +import logging +logger = logging.getLogger("stwcs.updatewcs.apply_corrections") + +#Note: The order of corrections is important + + +# A dictionary which lists the allowed corrections for each instrument. +# These are the default corrections applied also in the pipeline. + +allowed_corrections={'WFPC2': ['DET2IMCorr', 'MakeWCS','CompSIP', 'VACorr'], + 'ACS': ['DET2IMCorr', 'TDDCorr', 'MakeWCS', 'CompSIP','VACorr', 'NPOLCorr'], + 'STIS': ['MakeWCS', 'CompSIP','VACorr'], + 'NICMOS': ['MakeWCS', 'CompSIP','VACorr'], + 'WFC3': ['DET2IMCorr', 'MakeWCS', 'CompSIP','VACorr', 'NPOLCorr'], + } + +cnames = {'DET2IMCorr': 'Detector to Image Correction', + 'TDDCorr': 'Time Dependent Distortion Correction', + 'MakeWCS': 'Recalculate basic WCS keywords based on the distortion model', + 'CompSIP': 'Given IDCTAB distortion model calculate the SIP coefficients', + 'VACorr': 'Velocity Aberration Correction', + 'NPOLCorr': 'Lookup Table Distortion' + } + +def setCorrections(fname, vacorr=True, tddcorr=True, npolcorr=True, d2imcorr=True): + """ + Creates a list of corrections to be applied to a file + based on user input paramters and allowed corrections + for the instrument. + """ + instrument = fits.getval(fname, 'INSTRUME') + # make a copy of this list ! + acorr = allowed_corrections[instrument][:] + + # For WFPC2 images, the old-style DGEOFILE needs to be + # converted on-the-fly into a proper D2IMFILE here... + if instrument == 'WFPC2': + # check for DGEOFILE, and convert it to D2IMFILE if found + d2imfile = wfpc2_dgeo.update_wfpc2_d2geofile(fname) + # Check if idctab is present on disk + # If kw IDCTAB is present in the header but the file is + # not found on disk, do not run TDDCorr, MakeCWS and CompSIP + if not foundIDCTAB(fname): + if 'TDDCorr' in acorr: acorr.remove('TDDCorr') + if 'MakeWCS' in acorr: acorr.remove('MakeWCS') + if 'CompSIP' in acorr: acorr.remove('CompSIP') + + if 'VACorr' in acorr and vacorr==False: acorr.remove('VACorr') + if 'TDDCorr' in acorr: + tddcorr = applyTDDCorr(fname, tddcorr) + if tddcorr == False: acorr.remove('TDDCorr') + + if 'NPOLCorr' in acorr: + npolcorr = applyNpolCorr(fname, npolcorr) + if npolcorr == False: acorr.remove('NPOLCorr') + if 'DET2IMCorr' in acorr: + d2imcorr = applyD2ImCorr(fname, d2imcorr) + if d2imcorr == False: acorr.remove('DET2IMCorr') + logger.info("\n\tCorrections to be applied to %s: %s " % (fname, str(acorr))) + return acorr + + +def foundIDCTAB(fname): + """ + This functions looks for an "IDCTAB" keyword in the primary header. + + Returns + ------- + status : bool + If False : MakeWCS, CompSIP and TDDCorr should not be applied. + If True : there's no restriction on corrections, they all should be applied. + + Raises + ------ + IOError : If IDCTAB file not found on disk. + """ + + try: + idctab = fits.getval(fname, 'IDCTAB').strip() + if idctab == 'N/A' or idctab == "": + return False + except KeyError: + return False + idctab = fileutil.osfn(idctab) + if os.path.exists(idctab): + return True + else: + raise IOError("IDCTAB file {0} not found".format(idctab)) + + +def applyTDDCorr(fname, utddcorr): + """ + The default value of tddcorr for all ACS images is True. + This correction will be performed if all conditions below are True: + - the user did not turn it off on the command line + - the detector is WFC + - the idc table specified in the primary header is available. + """ + + phdr = fits.getheader(fname) + instrument = phdr['INSTRUME'] + try: + detector = phdr['DETECTOR'] + except KeyError: + detector = None + try: + tddswitch = phdr['TDDCORR'] + except KeyError: + tddswitch = 'PERFORM' + + if instrument == 'ACS' and detector == 'WFC' and utddcorr == True and tddswitch == 'PERFORM': + tddcorr = True + try: + idctab = phdr['IDCTAB'] + except KeyError: + tddcorr = False + #print "***IDCTAB keyword not found - not applying TDD correction***\n" + if os.path.exists(fileutil.osfn(idctab)): + tddcorr = True + else: + tddcorr = False + #print "***IDCTAB file not found - not applying TDD correction***\n" + else: + tddcorr = False + return tddcorr + +def applyNpolCorr(fname, unpolcorr): + """ + Determines whether non-polynomial distortion lookup tables should be added + as extensions to the science file based on the 'NPOLFILE' keyword in the + primary header and NPOLEXT kw in the first extension. + This is a default correction and will always run in the pipeline. + The file used to generate the extensions is + recorded in the NPOLEXT keyword in the first science extension. + If 'NPOLFILE' in the primary header is different from 'NPOLEXT' in the + extension header and the file exists on disk and is a 'new type' npolfile, + then the lookup tables will be updated as 'WCSDVARR' extensions. + """ + applyNPOLCorr = True + try: + # get NPOLFILE kw from primary header + fnpol0 = fits.getval(fname, 'NPOLFILE') + if fnpol0 == 'N/A': + utils.remove_distortion(fname, "NPOLFILE") + return False + fnpol0 = fileutil.osfn(fnpol0) + if not fileutil.findFile(fnpol0): + msg = """\n\tKw "NPOLFILE" exists in primary header but file %s not found + Non-polynomial distortion correction will not be applied\n + """ % fnpol0 + logger.critical(msg) + raise IOError("NPOLFILE {0} not found".format(fnpol0)) + try: + # get NPOLEXT kw from first extension header + fnpol1 = fits.getval(fname, 'NPOLEXT', ext=1) + fnpol1 = fileutil.osfn(fnpol1) + if fnpol1 and fileutil.findFile(fnpol1): + if fnpol0 != fnpol1: + applyNPOLCorr = True + else: + msg = """\n\tNPOLEXT with the same value as NPOLFILE found in first extension. + NPOL correction will not be applied.""" + logger.info(msg) + applyNPOLCorr = False + else: + # npl file defined in first extension may not be found + # but if a valid kw exists in the primary header, non-polynomial + #distortion correction should be applied. + applyNPOLCorr = True + except KeyError: + # the case of "NPOLFILE" kw present in primary header but "NPOLEXT" missing + # in first extension header + applyNPOLCorr = True + except KeyError: + logger.info('\n\t"NPOLFILE" keyword not found in primary header') + applyNPOLCorr = False + return applyNPOLCorr + + if isOldStyleDGEO(fname, fnpol0): + applyNPOLCorr = False + return (applyNPOLCorr and unpolcorr) + +def isOldStyleDGEO(fname, dgname): + # checks if the file defined in a NPOLFILE kw is a full size + # (old style) image + + sci_hdr = fits.getheader(fname, ext=1) + dgeo_hdr = fits.getheader(dgname, ext=1) + sci_naxis1 = sci_hdr['NAXIS1'] + sci_naxis2 = sci_hdr['NAXIS2'] + dg_naxis1 = dgeo_hdr['NAXIS1'] + dg_naxis2 = dgeo_hdr['NAXIS2'] + if sci_naxis1 <= dg_naxis1 or sci_naxis2 <= dg_naxis2: + msg = """\n\tOnly full size (old style) DGEO file was found.\n + Non-polynomial distortion correction will not be applied.""" + logger.critical(msg) + return True + else: + return False + +def applyD2ImCorr(fname, d2imcorr): + applyD2IMCorr = True + try: + # get D2IMFILE kw from primary header + fd2im0 = fits.getval(fname, 'D2IMFILE') + if fd2im0 == 'N/A': + utils.remove_distortion(fname, "D2IMFILE") + return False + fd2im0 = fileutil.osfn(fd2im0) + if not fileutil.findFile(fd2im0): + msg = """\n\tKw D2IMFILE exists in primary header but file %s not found\n + Detector to image correction will not be applied\n""" % fd2im0 + logger.critical(msg) + print(msg) + raise IOError("D2IMFILE {0} not found".format(fd2im0)) + try: + # get D2IMEXT kw from first extension header + fd2imext = fits.getval(fname, 'D2IMEXT', ext=1) + fd2imext = fileutil.osfn(fd2imext) + if fd2imext and fileutil.findFile(fd2imext): + if fd2im0 != fd2imext: + applyD2IMCorr = True + else: + applyD2IMCorr = False + else: + # D2IM file defined in first extension may not be found + # but if a valid kw exists in the primary header, + # detector to image correction should be applied. + applyD2IMCorr = True + except KeyError: + # the case of D2IMFILE kw present in primary header but D2IMEXT missing + # in first extension header + applyD2IMCorr = True + except KeyError: + print('D2IMFILE keyword not found in primary header') + applyD2IMCorr = False + return applyD2IMCorr diff --git a/stwcs/updatewcs/corrections.py b/stwcs/updatewcs/corrections.py new file mode 100644 index 0000000..d3641eb --- /dev/null +++ b/stwcs/updatewcs/corrections.py @@ -0,0 +1,326 @@ +from __future__ import division, print_function # confidence high + +import copy +import datetime +import logging, time +import numpy as np +from numpy import linalg +from stsci.tools import fileutil + +from . import npol +from . import makewcs +from .utils import diff_angles + +logger=logging.getLogger('stwcs.updatewcs.corrections') + +MakeWCS = makewcs.MakeWCS +NPOLCorr = npol.NPOLCorr + +class TDDCorr(object): + """ + Apply time dependent distortion correction to distortion coefficients and basic + WCS keywords. This correction **must** be done before any other WCS correction. + + Parameters + ---------- + ext_wcs: HSTWCS object + An HSTWCS object to be modified + ref_wcs: HSTWCS object + A reference HSTWCS object + + Notes + ----- + Compute the ACS/WFC time dependent distortion terms as described + in [1]_ and apply the correction to the WCS of the observation. + + The model coefficients are stored in the primary header of the IDCTAB. + :math:`D_{ref}` is the reference date. The computed corrections are saved + in the science extension header as TDDALPHA and TDDBETA keywords. + + .. math:: TDDALPHA = A_{0} + {A_{1}*(obsdate - D_{ref})} + + .. math:: TDDBETA = B_{0} + B_{1}*(obsdate - D_{ref}) + + + The time dependent distortion affects the IDCTAB coefficients, and the + relative location of the two chips. Because the linear order IDCTAB + coefficients ar eused in the computatuion of the NPOL extensions, + the TDD correction affects all components of the distortion model. + + Application of TDD to the IDCTAB polynomial coefficients: + The TDD model is computed in Jay's frame, while the IDCTAB coefficients + are in the HST V2/V3 frame. The coefficients are transformed to Jay's frame, + TDD is applied and they are transformed back to the V2/V3 frame. This + correction is performed in this class. + + Application of TDD to the relative location of the two chips is + done in `makewcs`. + + References + ---------- + .. [1] Jay Anderson, "Variation of the Distortion Solution of the WFC", ACS ISR 2007-08. + + """ + def updateWCS(cls, ext_wcs, ref_wcs): + """ + - Calculates alpha and beta for ACS/WFC data. + - Writes 2 new kw to the extension header: TDDALPHA and TDDBETA + """ + logger.info("\n\tStarting TDDCorr: %s" % time.asctime()) + ext_wcs.idcmodel.ocx = copy.deepcopy(ext_wcs.idcmodel.cx) + ext_wcs.idcmodel.ocy = copy.deepcopy(ext_wcs.idcmodel.cy) + + newkw = {'TDDALPHA': None, 'TDDBETA':None, + 'OCX10':ext_wcs.idcmodel.ocx[1,0], + 'OCX11':ext_wcs.idcmodel.ocx[1,1], + 'OCY10':ext_wcs.idcmodel.ocy[1,0], + 'OCY11':ext_wcs.idcmodel.ocy[1,1], + 'TDD_CTA':None, 'TDD_CTB':None, + 'TDD_CYA':None, 'TDD_CYB':None, + 'TDD_CXA':None, 'TDD_CXB':None} + + if ext_wcs.idcmodel.refpix['skew_coeffs']is not None and \ + ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CTB'] is not None: + cls.apply_tdd2idc2015(ref_wcs) + cls.apply_tdd2idc2015(ext_wcs) + + newkw.update({ + 'TDD_CTA':ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CTA'], + 'TDD_CYA':ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CYA'], + 'TDD_CXA':ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CXA'], + 'TDD_CTB':ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CTB'], + 'TDD_CYB':ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CYB'], + 'TDD_CXB':ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CXB']}) + + elif ext_wcs.idcmodel.refpix['skew_coeffs']is not None and \ + ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CY_BETA'] is not None: + logger.info("\n\t Applying 2014-calibrated TDD: %s" % time.asctime()) + # We have 2014-calibrated TDD, not J.A.-style TDD + cls.apply_tdd2idc2(ref_wcs) + cls.apply_tdd2idc2(ext_wcs) + newkw.update({'TDD_CYA':ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CY_ALPHA'], + 'TDD_CYB':ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CY_BETA'], + 'TDD_CXA':ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CX_ALPHA'], + 'TDD_CXB':ext_wcs.idcmodel.refpix['skew_coeffs']['TDD_CX_BETA']}) + + else: + alpha, beta = cls.compute_alpha_beta(ext_wcs) + cls.apply_tdd2idc(ref_wcs, alpha, beta) + cls.apply_tdd2idc(ext_wcs, alpha, beta) + ext_wcs.idcmodel.refpix['TDDALPHA'] = alpha + ext_wcs.idcmodel.refpix['TDDBETA'] = beta + ref_wcs.idcmodel.refpix['TDDALPHA'] = alpha + ref_wcs.idcmodel.refpix['TDDBETA'] = beta + + newkw.update( {'TDDALPHA': alpha, 'TDDBETA':beta} ) + + return newkw + updateWCS = classmethod(updateWCS) + + def apply_tdd2idc2015(cls, hwcs): + """ Applies 2015-calibrated TDD correction to a couple of IDCTAB + coefficients for ACS/WFC observations. + """ + if not isinstance(hwcs.date_obs,float): + year,month,day = hwcs.date_obs.split('-') + rdate = datetime.datetime(int(year),int(month),int(day)) + rday = float(rdate.strftime("%j"))/365.25 + rdate.year + else: + rday = hwcs.date_obs + + skew_coeffs = hwcs.idcmodel.refpix['skew_coeffs'] + delta_date = rday - skew_coeffs['TDD_DATE'] + + if skew_coeffs['TDD_CXB'] is not None: + hwcs.idcmodel.cx[1,1] += skew_coeffs['TDD_CXB']*delta_date + if skew_coeffs['TDD_CTB'] is not None: + hwcs.idcmodel.cy[1,1] += skew_coeffs['TDD_CTB']*delta_date + if skew_coeffs['TDD_CYB'] is not None: + hwcs.idcmodel.cy[1,0] += skew_coeffs['TDD_CYB']*delta_date + #print("CX[1,1]_TDD={}, CY[1,1]_TDD={}, CY[1,0]_TDD={}".format(hwcs.idcmodel.cx[1,1],hwcs.idcmodel.cy[1,1],hwcs.idcmodel.cy[1,0])) + + apply_tdd2idc2015 = classmethod(apply_tdd2idc2015) + + def apply_tdd2idc2(cls, hwcs): + """ Applies 2014-calibrated TDD correction to single IDCTAB coefficient + of an ACS/WFC observation. + """ + if not isinstance(hwcs.date_obs,float): + year,month,day = hwcs.date_obs.split('-') + rdate = datetime.datetime(int(year),int(month),int(day)) + rday = float(rdate.strftime("%j"))/365.25 + rdate.year + else: + rday = hwcs.date_obs + + skew_coeffs = hwcs.idcmodel.refpix['skew_coeffs'] + cy_beta = skew_coeffs['TDD_CY_BETA'] + cy_alpha = skew_coeffs['TDD_CY_ALPHA'] + delta_date = rday - skew_coeffs['TDD_DATE'] + print("DELTA_DATE: {0} based on rday: {1}, TDD_DATE: {2}".format(delta_date,rday,skew_coeffs['TDD_DATE'])) + + if cy_alpha is None: + hwcs.idcmodel.cy[1,1] += cy_beta*delta_date + else: + new_beta = cy_alpha + cy_beta*delta_date + hwcs.idcmodel.cy[1,1] = new_beta + print("CY11: {0} based on alpha: {1}, beta: {2}".format(hwcs.idcmodel.cy[1,1],cy_alpha,cy_beta)) + + cx_beta = skew_coeffs['TDD_CX_BETA'] + cx_alpha = skew_coeffs['TDD_CX_ALPHA'] + if cx_alpha is not None: + new_beta = cx_alpha + cx_beta*delta_date + hwcs.idcmodel.cx[1,1] = new_beta + print("CX11: {0} based on alpha: {1}, beta: {2}".format(new_beta,cx_alpha,cx_beta)) + + apply_tdd2idc2 = classmethod(apply_tdd2idc2) + + def apply_tdd2idc(cls, hwcs, alpha, beta): + """ + Applies TDD to the idctab coefficients of a ACS/WFC observation. + This should be always the first correction. + """ + theta_v2v3 = 2.234529 + mrotp = fileutil.buildRotMatrix(theta_v2v3) + mrotn = fileutil.buildRotMatrix(-theta_v2v3) + tdd_mat = np.array([[1+(beta/2048.), alpha/2048.],[alpha/2048.,1-(beta/2048.)]],np.float64) + abmat1 = np.dot(tdd_mat, mrotn) + abmat2 = np.dot(mrotp,abmat1) + xshape, yshape = hwcs.idcmodel.cx.shape, hwcs.idcmodel.cy.shape + icxy = np.dot(abmat2,[hwcs.idcmodel.cx.ravel(), hwcs.idcmodel.cy.ravel()]) + hwcs.idcmodel.cx = icxy[0] + hwcs.idcmodel.cy = icxy[1] + hwcs.idcmodel.cx.shape = xshape + hwcs.idcmodel.cy.shape = yshape + + apply_tdd2idc = classmethod(apply_tdd2idc) + + def compute_alpha_beta(cls, ext_wcs): + """ + Compute the ACS time dependent distortion skew terms + as described in ACS ISR 07-08 by J. Anderson. + + Jay's code only computes the alpha/beta values based on a decimal year + with only 3 digits, so this line reproduces that when needed for comparison + with his results. + rday = float(('%0.3f')%rday) + + The zero-point terms account for the skew accumulated between + 2002.0 and 2004.5, when the latest IDCTAB was delivered. + alpha = 0.095 + 0.090*(rday-dday)/2.5 + beta = -0.029 - 0.030*(rday-dday)/2.5 + """ + if not isinstance(ext_wcs.date_obs,float): + year,month,day = ext_wcs.date_obs.split('-') + rdate = datetime.datetime(int(year),int(month),int(day)) + rday = float(rdate.strftime("%j"))/365.25 + rdate.year + else: + rday = ext_wcs.date_obs + + skew_coeffs = ext_wcs.idcmodel.refpix['skew_coeffs'] + if skew_coeffs is None: + # Only print out warning for post-SM4 data where this may matter + if rday > 2009.0: + err_str = "------------------------------------------------------------------------ \n" + err_str += "WARNING: the IDCTAB geometric distortion file specified in the image \n" + err_str += " header did not have the time-dependent distortion coefficients. \n" + err_str += " The pre-SM4 time-dependent skew solution will be used by default.\n" + err_str += " Please update IDCTAB with new reference file from HST archive. \n" + err_str += "------------------------------------------------------------------------ \n" + print(err_str) + # Using default pre-SM4 coefficients + skew_coeffs = {'TDD_A':[0.095,0.090/2.5], + 'TDD_B':[-0.029,-0.030/2.5], + 'TDD_DATE':2004.5,'TDDORDER':1} + + alpha = 0 + beta = 0 + # Compute skew terms, allowing for non-linear coefficients as well + for c in range(skew_coeffs['TDDORDER']+1): + alpha += skew_coeffs['TDD_A'][c]* np.power((rday-skew_coeffs['TDD_DATE']),c) + beta += skew_coeffs['TDD_B'][c]*np.power((rday-skew_coeffs['TDD_DATE']),c) + + return alpha,beta + compute_alpha_beta = classmethod(compute_alpha_beta) + + +class VACorr(object): + """ + Apply velocity aberation correction to WCS keywords. + + Notes + ----- + Velocity Aberration is stored in the extension header keyword 'VAFACTOR'. + The correction is applied to the CD matrix and CRVALs. + + """ + def updateWCS(cls, ext_wcs, ref_wcs): + logger.info("\n\tStarting VACorr: %s" % time.asctime()) + if ext_wcs.vafactor != 1: + ext_wcs.wcs.cd = ext_wcs.wcs.cd * ext_wcs.vafactor + crval0 = ref_wcs.wcs.crval[0] + ext_wcs.vafactor*diff_angles(ext_wcs.wcs.crval[0], + ref_wcs.wcs.crval[0]) + crval1 = ref_wcs.wcs.crval[1] + ext_wcs.vafactor*diff_angles(ext_wcs.wcs.crval[1], + ref_wcs.wcs.crval[1]) + crval = np.array([crval0,crval1]) + ext_wcs.wcs.crval = crval + ext_wcs.wcs.set() + else: + pass + kw2update={'CD1_1': ext_wcs.wcs.cd[0,0], 'CD1_2':ext_wcs.wcs.cd[0,1], + 'CD2_1':ext_wcs.wcs.cd[1,0], 'CD2_2':ext_wcs.wcs.cd[1,1], + 'CRVAL1':ext_wcs.wcs.crval[0], 'CRVAL2':ext_wcs.wcs.crval[1]} + return kw2update + + updateWCS = classmethod(updateWCS) + + +class CompSIP(object): + """ + Compute Simple Imaging Polynomial (SIP) coefficients as defined in [2]_ + from IDC table coefficients. + + This class transforms the TDD corrected IDCTAB coefficients into SIP format. + It also applies a binning factor to the coefficients if the observation was + binned. + + References + ---------- + .. [2] David Shupe, et al, "The SIP Convention of representing Distortion + in FITS Image headers", Astronomical Data Analysis Software And Systems, ASP + Conference Series, Vol. 347, 2005 + + """ + def updateWCS(cls, ext_wcs, ref_wcs): + logger.info("\n\tStarting CompSIP: %s" %time.asctime()) + kw2update = {} + if not ext_wcs.idcmodel: + logger.info("IDC model not found, SIP coefficient will not be computed") + return kw2update + order = ext_wcs.idcmodel.norder + kw2update['A_ORDER'] = order + kw2update['B_ORDER'] = order + pscale = ext_wcs.idcmodel.refpix['PSCALE'] + + cx = ext_wcs.idcmodel.cx + cy = ext_wcs.idcmodel.cy + + matr = np.array([[cx[1,1],cx[1,0]], [cy[1,1],cy[1,0]]], dtype=np.float64) + imatr = linalg.inv(matr) + akeys1 = np.zeros((order+1,order+1), dtype=np.float64) + bkeys1 = np.zeros((order+1,order+1), dtype=np.float64) + for n in range(order+1): + for m in range(order+1): + if n >= m and n>=2: + idcval = np.array([[cx[n,m]],[cy[n,m]]]) + sipval = np.dot(imatr, idcval) + akeys1[m,n-m] = sipval[0] + bkeys1[m,n-m] = sipval[1] + Akey="A_%d_%d" % (m,n-m) + Bkey="B_%d_%d" % (m,n-m) + kw2update[Akey] = sipval[0,0] * ext_wcs.binned + kw2update[Bkey] = sipval[1,0] * ext_wcs.binned + kw2update['CTYPE1'] = 'RA---TAN-SIP' + kw2update['CTYPE2'] = 'DEC--TAN-SIP' + return kw2update + + updateWCS = classmethod(updateWCS) diff --git a/stwcs/updatewcs/det2im.py b/stwcs/updatewcs/det2im.py new file mode 100644 index 0000000..bddb37b --- /dev/null +++ b/stwcs/updatewcs/det2im.py @@ -0,0 +1,299 @@ +from __future__ import absolute_import, division # confidence high + +import numpy as np +from astropy.io import fits +from stsci.tools import fileutil + +from . import utils + +import logging, time +logger = logging.getLogger('stwcs.updatewcs.d2im') + +class DET2IMCorr(object): + """ + Defines a Lookup table prior distortion correction as per WCS paper IV. + It uses a reference file defined by the D2IMFILE (suffix 'd2im') keyword + in the primary header. + + Notes + ----- + - Using extensions in the reference file create a WCSDVARR extensions + and add them to the science file. + - Add record-valued keywords to the science extension header to describe + the lookup tables. + - Add a keyword 'D2IMEXT' to the science extension header to store + the name of the reference file used to create the WCSDVARR extensions. + + If WCSDVARR extensions exist and `D2IMFILE` is different from `D2IMEXT`, + a subsequent update will overwrite the existing extensions. + If WCSDVARR extensions were not found in the science file, they will be added. + + """ + + def updateWCS(cls, fobj): + """ + Parameters + ---------- + fobj: `astropy.io.fits.HDUList` object + Science file, for which a distortion correction in a NPOLFILE is available + + """ + logger.info("\n\tStarting DET2IM: %s" %time.asctime()) + try: + assert isinstance(fobj, fits.HDUList) + except AssertionError: + logger.exception('\n\tInput must be a fits.HDUList object') + raise + + cls.applyDet2ImCorr(fobj) + d2imfile = fobj[0].header['D2IMFILE'] + + new_kw = {'D2IMEXT': d2imfile} + return new_kw + + updateWCS = classmethod(updateWCS) + + def applyDet2ImCorr(cls, fobj): + """ + For each science extension in a fits file object: + - create a WCSDVARR extension + - update science header + - add/update D2IMEXT keyword + """ + d2imfile = fileutil.osfn(fobj[0].header['D2IMFILE']) + # Map D2IMARR EXTVER numbers to FITS extension numbers + wcsdvarr_ind = cls.getWCSIndex(fobj) + d2im_num_ext = 1 + for ext in fobj: + try: + extname = ext.header['EXTNAME'].lower() + except KeyError: + continue + if extname == 'sci': + extversion = ext.header['EXTVER'] + ccdchip = cls.get_ccdchip(fobj, extname='SCI', extver=extversion) + header = ext.header + # get the data arrays from the reference file + dx, dy = cls.getData(d2imfile, ccdchip) + # Determine EXTVER for the D2IMARR extension from the D2I file (EXTNAME, EXTVER) kw. + # This is used to populate DPj.EXTVER kw + for ename in zip(['DX', 'DY'], [dx, dy]): + if ename[1] is not None: + error_val = ename[1].max() + cls.addSciExtKw(header, wdvarr_ver=d2im_num_ext, d2im_extname=ename[0], error_val=error_val) + hdu = cls.createD2ImHDU(header, d2imfile=d2imfile, + wdvarr_ver=d2im_num_ext, + d2im_extname=ename[0], + data=ename[1],ccdchip=ccdchip) + if wcsdvarr_ind and d2im_num_ext in wcsdvarr_ind: + fobj[wcsdvarr_ind[d2im_num_ext]] = hdu + else: + fobj.append(hdu) + d2im_num_ext = d2im_num_ext + 1 + applyDet2ImCorr = classmethod(applyDet2ImCorr) + + def getWCSIndex(cls, fobj): + + """ + If fobj has WCSDVARR extensions: + returns a mapping of their EXTVER kw to file object extension numbers + if fobj does not have WCSDVARR extensions: + an empty dictionary is returned + """ + wcsd = {} + for e in range(len(fobj)): + try: + ename = fobj[e].header['EXTNAME'] + except KeyError: + continue + if ename == 'D2IMARR': + wcsd[fobj[e].header['EXTVER']] = e + logger.debug("A map of D2IMARR extensions %s" % wcsd) + return wcsd + + getWCSIndex = classmethod(getWCSIndex) + + def addSciExtKw(cls, hdr, wdvarr_ver=None, d2im_extname=None, error_val=0.0): + """ + Adds kw to sci extension to define WCSDVARR lookup table extensions + + """ + if d2im_extname =='DX': + j=1 + else: + j=2 + + d2imerror = 'D2IMERR%s' %j + d2imdis = 'D2IMDIS%s' %j + d2imext = 'D2IM%s.' %j + 'EXTVER' + d2imnaxes = 'D2IM%s.' %j +'NAXES' + d2imaxis1 = 'D2IM%s.' %j+'AXIS.1' + d2imaxis2 = 'D2IM%s.' %j+'AXIS.2' + keys = [d2imerror, d2imdis, d2imext, d2imnaxes, d2imaxis1, d2imaxis2] + values = {d2imerror: error_val, + d2imdis: 'Lookup', + d2imext: wdvarr_ver, + d2imnaxes: 2, + d2imaxis1: 1, + d2imaxis2: 2} + + comments = {d2imerror: 'Maximum error of NPOL correction for axis %s' % j, + d2imdis: 'Detector to image correction type', + d2imext: 'Version number of WCSDVARR extension containing d2im lookup table', + d2imnaxes: 'Number of independent variables in d2im function', + d2imaxis1: 'Axis number of the jth independent variable in a d2im function', + d2imaxis2: 'Axis number of the jth independent variable in a d2im function' + } + # Look for HISTORY keywords. If present, insert new keywords before them + before_key = 'HISTORY' + if before_key not in hdr: + before_key = None + + for key in keys: + hdr.set(key, value=values[key], comment=comments[key], before=before_key) + + addSciExtKw = classmethod(addSciExtKw) + + def getData(cls,d2imfile, ccdchip): + """ + Get the data arrays from the reference D2I files + Make sure 'CCDCHIP' in the npolfile matches "CCDCHIP' in the science file. + """ + xdata, ydata = (None, None) + d2im = fits.open(d2imfile) + for ext in d2im: + d2imextname = ext.header.get('EXTNAME',"") + d2imccdchip = ext.header.get('CCDCHIP',1) + if d2imextname == 'DX' and d2imccdchip == ccdchip: + xdata = ext.data.copy() + continue + elif d2imextname == 'DY' and d2imccdchip == ccdchip: + ydata = ext.data.copy() + continue + else: + continue + d2im.close() + return xdata, ydata + getData = classmethod(getData) + + def createD2ImHDU(cls, sciheader, d2imfile=None, wdvarr_ver=1, + d2im_extname=None,data = None, ccdchip=1): + """ + Creates an HDU to be added to the file object. + """ + hdr = cls.createD2ImHdr(sciheader, d2imfile=d2imfile, + wdvarr_ver=wdvarr_ver, d2im_extname=d2im_extname, + ccdchip=ccdchip) + hdu = fits.ImageHDU(header=hdr, data=data) + return hdu + + createD2ImHDU = classmethod(createD2ImHDU) + + def createD2ImHdr(cls, sciheader, d2imfile, wdvarr_ver, d2im_extname, ccdchip): + """ + Creates a header for the D2IMARR extension based on the D2I reference file + and sci extension header. The goal is to always work in image coordinates + (also for subarrays and binned images). The WCS for the D2IMARR extension + is such that a full size d2im table is created and then shifted or scaled + if the science image is a subarray or binned image. + """ + d2im = fits.open(d2imfile) + d2im_phdr = d2im[0].header + for ext in d2im: + try: + d2imextname = ext.header['EXTNAME'] + d2imextver = ext.header['EXTVER'] + except KeyError: + continue + d2imccdchip = cls.get_ccdchip(d2im, extname=d2imextname, extver=d2imextver) + if d2imextname == d2im_extname and d2imccdchip == ccdchip: + d2im_header = ext.header + break + else: + continue + d2im.close() + + naxis = d2im[1].header['NAXIS'] + ccdchip = d2imextname + + kw = { 'NAXIS': 'Size of the axis', + 'CDELT': 'Coordinate increment along axis', + 'CRPIX': 'Coordinate system reference pixel', + 'CRVAL': 'Coordinate system value at reference pixel', + } + + kw_comm1 = {} + kw_val1 = {} + for key in kw.keys(): + for i in range(1, naxis+1): + si = str(i) + kw_comm1[key+si] = kw[key] + + for i in range(1, naxis+1): + si = str(i) + kw_val1['NAXIS'+si] = d2im_header.get('NAXIS'+si) + kw_val1['CDELT'+si] = d2im_header.get('CDELT'+si, 1.0) * sciheader.get('LTM'+si+'_'+si, 1) + kw_val1['CRPIX'+si] = d2im_header.get('CRPIX'+si, 0.0) + kw_val1['CRVAL'+si] = (d2im_header.get('CRVAL'+si, 0.0) - \ + sciheader.get('LTV'+str(i), 0)) + kw_comm0 = {'XTENSION': 'Image extension', + 'BITPIX': 'IEEE floating point', + 'NAXIS': 'Number of axes', + 'EXTNAME': 'WCS distortion array', + 'EXTVER': 'Distortion array version number', + 'PCOUNT': 'Special data area of size 0', + 'GCOUNT': 'One data group', + } + + kw_val0 = { 'XTENSION': 'IMAGE', + 'BITPIX': -32, + 'NAXIS': naxis, + 'EXTNAME': 'D2IMARR', + 'EXTVER': wdvarr_ver, + 'PCOUNT': 0, + 'GCOUNT': 1, + 'CCDCHIP': ccdchip, + } + cdl = [] + for key in kw_comm0.keys(): + cdl.append((key, kw_val0[key], kw_comm0[key])) + for key in kw_comm1.keys(): + cdl.append((key, kw_val1[key], kw_comm1[key])) + # Now add keywords from NPOLFILE header to document source of calibration + # include all keywords after and including 'FILENAME' from header + start_indx = -1 + end_indx = 0 + for i, c in enumerate(d2im_phdr): + if c == 'FILENAME': + start_indx = i + if c == '': # remove blanks from end of header + end_indx = i+1 + break + if start_indx >= 0: + for card in d2im_phdr.cards[start_indx:end_indx]: + cdl.append(card) + + hdr = fits.Header(cards=cdl) + + return hdr + + createD2ImHdr = classmethod(createD2ImHdr) + + def get_ccdchip(cls, fobj, extname, extver): + """ + Given a science file or npol file determine CCDCHIP + """ + ccdchip = 1 + if fobj[0].header['INSTRUME'] == 'ACS' and fobj[0].header['DETECTOR'] == 'WFC': + ccdchip = fobj[extname, extver].header['CCDCHIP'] + elif fobj[0].header['INSTRUME'] == 'WFC3' and fobj[0].header['DETECTOR'] == 'UVIS': + ccdchip = fobj[extname, extver].header['CCDCHIP'] + elif fobj[0].header['INSTRUME'] == 'WFPC2': + ccdchip = fobj[extname, extver].header['DETECTOR'] + elif fobj[0].header['INSTRUME'] == 'STIS': + ccdchip = fobj[extname, extver].header['DETECTOR'] + elif fobj[0].header['INSTRUME'] == 'NICMOS': + ccdchip = fobj[extname, extver].header['CAMERA'] + return ccdchip + + get_ccdchip = classmethod(get_ccdchip) diff --git a/stwcs/updatewcs/makewcs.py b/stwcs/updatewcs/makewcs.py new file mode 100644 index 0000000..06c6f9c --- /dev/null +++ b/stwcs/updatewcs/makewcs.py @@ -0,0 +1,273 @@ +from __future__ import absolute_import, division # confidence high + +import datetime + +import numpy as np +import logging, time +from math import sin, sqrt, pow, cos, asin, atan2,pi + +from stsci.tools import fileutil +from . import utils + +logger = logging.getLogger(__name__) + +class MakeWCS(object): + """ + Recompute basic WCS keywords based on PA_V3 and distortion model. + + Notes + ----- + - Compute the reference chip WCS: + + -- CRVAL: transform the model XREF/YREF to the sky + -- PA_V3 is calculated at the target position and adjusted + for each chip orientation + -- CD: PA_V3 and the model scale are used to cnstruct a CD matrix + + - Compute the second chip WCS: + + -- CRVAL: - the distance between the zero points of the two + chip models on the sky + -- CD matrix: first order coefficients are added to the components + of this distance and transfered on the sky. The difference + between CRVAL and these vectors is the new CD matrix for each chip. + -- CRPIX: chip's model zero point in pixel space (XREF/YREF) + + - Time dependent distortion correction is applied to both chips' V2/V3 values. + + """ + tdd_xyref = {1: [2048, 3072], 2:[2048, 1024]} + def updateWCS(cls, ext_wcs, ref_wcs): + """ + recomputes the basic WCS kw + """ + logger.info("\n\tStarting MakeWCS: %s" % time.asctime()) + if not ext_wcs.idcmodel: + logger.info("IDC model not found, turning off Makewcs") + return {} + ltvoff, offshift = cls.getOffsets(ext_wcs) + + v23_corr = cls.zero_point_corr(ext_wcs) + rv23_corr = cls.zero_point_corr(ref_wcs) + + cls.uprefwcs(ext_wcs, ref_wcs, rv23_corr, ltvoff, offshift) + cls.upextwcs(ext_wcs, ref_wcs, v23_corr, rv23_corr, ltvoff, offshift) + + kw2update = {'CD1_1': ext_wcs.wcs.cd[0,0], + 'CD1_2': ext_wcs.wcs.cd[0,1], + 'CD2_1': ext_wcs.wcs.cd[1,0], + 'CD2_2': ext_wcs.wcs.cd[1,1], + 'CRVAL1': ext_wcs.wcs.crval[0], + 'CRVAL2': ext_wcs.wcs.crval[1], + 'CRPIX1': ext_wcs.wcs.crpix[0], + 'CRPIX2': ext_wcs.wcs.crpix[1], + 'IDCTAB': ext_wcs.idctab, + 'OCX10' : ext_wcs.idcmodel.cx[1,0], + 'OCX11' : ext_wcs.idcmodel.cx[1,1], + 'OCY10' : ext_wcs.idcmodel.cy[1,0], + 'OCY11' : ext_wcs.idcmodel.cy[1,1] + } + return kw2update + + updateWCS = classmethod(updateWCS) + + def upextwcs(cls, ext_wcs, ref_wcs, v23_corr, rv23_corr, loff, offsh): + """ + updates an extension wcs + """ + ltvoffx, ltvoffy = loff[0], loff[1] + offshiftx, offshifty = offsh[0], offsh[1] + ltv1 = ext_wcs.ltv1 + ltv2 = ext_wcs.ltv2 + if ltv1 != 0. or ltv2 != 0.: + offsetx = ext_wcs.wcs.crpix[0] - ltv1 - ext_wcs.idcmodel.refpix['XREF'] + offsety = ext_wcs.wcs.crpix[1] - ltv2 - ext_wcs.idcmodel.refpix['YREF'] + ext_wcs.idcmodel.shift(offsetx,offsety) + + fx, fy = ext_wcs.idcmodel.cx, ext_wcs.idcmodel.cy + + ext_wcs.setPscale() + tddscale = (ref_wcs.pscale/fx[1,1]) + v2 = ext_wcs.idcmodel.refpix['V2REF'] + v23_corr[0,0] * tddscale + v3 = ext_wcs.idcmodel.refpix['V3REF'] - v23_corr[1,0] * tddscale + v2ref = ref_wcs.idcmodel.refpix['V2REF'] + rv23_corr[0,0] * tddscale + v3ref = ref_wcs.idcmodel.refpix['V3REF'] - rv23_corr[1,0] * tddscale + + R_scale = ref_wcs.idcmodel.refpix['PSCALE']/3600.0 + off = sqrt((v2-v2ref)**2 + (v3-v3ref)**2)/(R_scale*3600.0) + + if v3 == v3ref: + theta=0.0 + else: + theta = atan2(ext_wcs.parity[0][0]*(v2-v2ref), ext_wcs.parity[1][1]*(v3-v3ref)) + + if ref_wcs.idcmodel.refpix['THETA']: theta += ref_wcs.idcmodel.refpix['THETA']*pi/180.0 + + dX=(off*sin(theta)) + offshiftx + dY=(off*cos(theta)) + offshifty + + px = np.array([[dX,dY]]) + newcrval = ref_wcs.wcs.p2s(px, 1)['world'][0] + newcrpix = np.array([ext_wcs.idcmodel.refpix['XREF'] + ltvoffx, + ext_wcs.idcmodel.refpix['YREF'] + ltvoffy]) + ext_wcs.wcs.crval = newcrval + ext_wcs.wcs.crpix = newcrpix + ext_wcs.wcs.set() + + # Create a small vector, in reference image pixel scale + delmat = np.array([[fx[1,1], fy[1,1]], \ + [fx[1,0], fy[1,0]]]) / R_scale/3600. + + # Account for subarray offset + # Angle of chip relative to chip + if ext_wcs.idcmodel.refpix['THETA']: + dtheta = ext_wcs.idcmodel.refpix['THETA'] - ref_wcs.idcmodel.refpix['THETA'] + else: + dtheta = 0.0 + + rrmat = fileutil.buildRotMatrix(dtheta) + # Rotate the vectors + dxy = np.dot(delmat, rrmat) + wc = ref_wcs.wcs.p2s((px + dxy), 1)['world'] + + # Calculate the new CDs and convert to degrees + cd11 = utils.diff_angles(wc[0,0],newcrval[0])*cos(newcrval[1]*pi/180.0) + cd12 = utils.diff_angles(wc[1,0],newcrval[0])*cos(newcrval[1]*pi/180.0) + cd21 = utils.diff_angles(wc[0,1],newcrval[1]) + cd22 = utils.diff_angles(wc[1,1],newcrval[1]) + cd = np.array([[cd11, cd12], [cd21, cd22]]) + ext_wcs.wcs.cd = cd + ext_wcs.wcs.set() + + upextwcs = classmethod(upextwcs) + + def uprefwcs(cls, ext_wcs, ref_wcs, rv23_corr_tdd, loff, offsh): + """ + Updates the reference chip + """ + ltvoffx, ltvoffy = loff[0], loff[1] + ltv1 = ref_wcs.ltv1 + ltv2 = ref_wcs.ltv2 + if ref_wcs.ltv1 != 0. or ref_wcs.ltv2 != 0.: + offsetx = ref_wcs.wcs.crpix[0] - ltv1 - ref_wcs.idcmodel.refpix['XREF'] + offsety = ref_wcs.wcs.crpix[1] - ltv2 - ref_wcs.idcmodel.refpix['YREF'] + ref_wcs.idcmodel.shift(offsetx,offsety) + + rfx, rfy = ref_wcs.idcmodel.cx, ref_wcs.idcmodel.cy + + offshift = offsh + dec = ref_wcs.wcs.crval[1] + tddscale = (ref_wcs.pscale/ref_wcs.idcmodel.cx[1,1]) + rv23 = [ref_wcs.idcmodel.refpix['V2REF'] + (rv23_corr_tdd[0,0] *tddscale), + ref_wcs.idcmodel.refpix['V3REF'] - (rv23_corr_tdd[1,0] * tddscale)] + # Get an approximate reference position on the sky + rref = np.array([[ref_wcs.idcmodel.refpix['XREF']+ltvoffx , + ref_wcs.idcmodel.refpix['YREF']+ltvoffy]]) + + crval = ref_wcs.wcs.p2s(rref, 1)['world'][0] + # Convert the PA_V3 orientation to the orientation at the aperture + # This is for the reference chip only - we use this for the + # reference tangent plane definition + # It has the same orientation as the reference chip + pv = troll(ext_wcs.pav3,dec,rv23[0], rv23[1]) + # Add the chip rotation angle + if ref_wcs.idcmodel.refpix['THETA']: + pv += ref_wcs.idcmodel.refpix['THETA'] + + + # Set values for the rest of the reference WCS + ref_wcs.wcs.crval = crval + ref_wcs.wcs.crpix = np.array([0.0,0.0])+offsh + parity = ref_wcs.parity + R_scale = ref_wcs.idcmodel.refpix['PSCALE']/3600.0 + cd11 = parity[0][0] * cos(pv*pi/180.0)*R_scale + cd12 = parity[0][0] * -sin(pv*pi/180.0)*R_scale + cd21 = parity[1][1] * sin(pv*pi/180.0)*R_scale + cd22 = parity[1][1] * cos(pv*pi/180.0)*R_scale + + rcd = np.array([[cd11, cd12], [cd21, cd22]]) + ref_wcs.wcs.cd = rcd + ref_wcs.wcs.set() + + uprefwcs = classmethod(uprefwcs) + + def zero_point_corr(cls,hwcs): + if hwcs.idcmodel.refpix['skew_coeffs'] is not None and 'TDD_CY_BETA' in hwcs.idcmodel.refpix['skew_coeffs']: + v23_corr = np.array([[0.],[0.]]) + return v23_corr + else: + try: + alpha = hwcs.idcmodel.refpix['TDDALPHA'] + beta = hwcs.idcmodel.refpix['TDDBETA'] + except KeyError: + alpha = 0.0 + beta = 0.0 + v23_corr = np.array([[0.],[0.]]) + logger.debug("\n\tTDD Zero point correction for chip %s defaulted to: %s" % (hwcs.chip, v23_corr)) + return v23_corr + + tdd = np.array([[beta, alpha], [alpha, -beta]]) + mrotp = fileutil.buildRotMatrix(2.234529)/2048. + xy0 = np.array([[cls.tdd_xyref[hwcs.chip][0]-2048.], [cls.tdd_xyref[hwcs.chip][1]-2048.]]) + v23_corr = np.dot(mrotp,np.dot(tdd,xy0)) * 0.05 + logger.debug("\n\tTDD Zero point correction for chip %s: %s" % (hwcs.chip, v23_corr)) + return v23_corr + + zero_point_corr = classmethod(zero_point_corr) + + def getOffsets(cls, ext_wcs): + ltv1 = ext_wcs.ltv1 + ltv2 = ext_wcs.ltv2 + + offsetx = ext_wcs.wcs.crpix[0] - ltv1 - ext_wcs.idcmodel.refpix['XREF'] + offsety = ext_wcs.wcs.crpix[1] - ltv2 - ext_wcs.idcmodel.refpix['YREF'] + + shiftx = ext_wcs.idcmodel.refpix['XREF'] + ltv1 + shifty = ext_wcs.idcmodel.refpix['YREF'] + ltv2 + if ltv1 != 0. or ltv2 != 0.: + ltvoffx = ltv1 + offsetx + ltvoffy = ltv2 + offsety + offshiftx = offsetx + shiftx + offshifty = offsety + shifty + else: + ltvoffx = 0. + ltvoffy = 0. + offshiftx = 0. + offshifty = 0. + + ltvoff = np.array([ltvoffx, ltvoffy]) + offshift = np.array([offshiftx, offshifty]) + return ltvoff, offshift + + getOffsets = classmethod(getOffsets) + + +def troll(roll, dec, v2, v3): + """ Computes the roll angle at the target position based on: + the roll angle at the V1 axis(roll), + the dec of the target(dec), and + the V2/V3 position of the aperture (v2,v3) in arcseconds. + + Based on algorithm provided by Colin Cox and used in + Generic Conversion at STScI. + """ + # Convert all angles to radians + _roll = np.deg2rad(roll) + _dec = np.deg2rad(dec) + _v2 = np.deg2rad(v2 / 3600.) + _v3 = np.deg2rad(v3 / 3600.) + + # compute components + sin_rho = sqrt((pow(sin(_v2),2)+pow(sin(_v3),2)) - (pow(sin(_v2),2)*pow(sin(_v3),2))) + rho = asin(sin_rho) + beta = asin(sin(_v3)/sin_rho) + if _v2 < 0: beta = pi - beta + gamma = asin(sin(_v2)/sin_rho) + if _v3 < 0: gamma = pi - gamma + A = pi/2. + _roll - beta + B = atan2( sin(A)*cos(_dec), (sin(_dec)*sin_rho - cos(_dec)*cos(rho)*cos(A))) + + # compute final value + troll = np.rad2deg(pi - (gamma+B)) + + return troll diff --git a/stwcs/updatewcs/npol.py b/stwcs/updatewcs/npol.py new file mode 100644 index 0000000..33579f0 --- /dev/null +++ b/stwcs/updatewcs/npol.py @@ -0,0 +1,343 @@ +from __future__ import absolute_import, division # confidence high + +import logging, time + +import numpy as np +from astropy.io import fits + +from stsci.tools import fileutil +from . import utils + +logger = logging.getLogger('stwcs.updatewcs.npol') + +class NPOLCorr(object): + """ + Defines a Lookup table prior distortion correction as per WCS paper IV. + It uses a reference file defined by the NPOLFILE (suffix 'NPL') keyword + in the primary header. + + Notes + ----- + - Using extensions in the reference file create a WCSDVARR extensions + and add them to the science file. + - Add record-valued keywords to the science extension header to describe + the lookup tables. + - Add a keyword 'NPOLEXT' to the science extension header to store + the name of the reference file used to create the WCSDVARR extensions. + + If WCSDVARR extensions exist and `NPOLFILE` is different from `NPOLEXT`, + a subsequent update will overwrite the existing extensions. + If WCSDVARR extensions were not found in the science file, they will be added. + + It is assumed that the NPL reference files were created to work with IDC tables + but will be applied with SIP coefficients. A transformation is applied to correct + for the fact that the lookup tables will be applied before the first order coefficients + which are in the CD matrix when the SIP convention is used. + """ + + def updateWCS(cls, fobj): + """ + Parameters + ---------- + fobj : `astropy.io.fits.HDUList` object + Science file, for which a distortion correction in a NPOLFILE is available + + """ + logger.info("\n\tStarting NPOL: %s" %time.asctime()) + try: + assert isinstance(fobj, fits.HDUList) + except AssertionError: + logger.exception('\n\tInput must be a fits.HDUList object') + raise + + cls.applyNPOLCorr(fobj) + nplfile = fobj[0].header['NPOLFILE'] + + new_kw = {'NPOLEXT': nplfile} + return new_kw + + updateWCS = classmethod(updateWCS) + + def applyNPOLCorr(cls, fobj): + """ + For each science extension in a fits file object: + - create a WCSDVARR extension + - update science header + - add/update NPOLEXT keyword + """ + nplfile = fileutil.osfn(fobj[0].header['NPOLFILE']) + # Map WCSDVARR EXTVER numbers to extension numbers + wcsdvarr_ind = cls.getWCSIndex(fobj) + for ext in fobj: + try: + extname = ext.header['EXTNAME'].lower() + except KeyError: + continue + if extname == 'sci': + extversion = ext.header['EXTVER'] + ccdchip = cls.get_ccdchip(fobj, extname='SCI', extver=extversion) + header = ext.header + # get the data arrays from the reference file and transform + # them for use with SIP + dx,dy = cls.getData(nplfile, ccdchip) + idccoeffs = cls.getIDCCoeffs(header) + + if idccoeffs is not None: + dx, dy = cls.transformData(dx,dy, idccoeffs) + + # Determine EXTVER for the WCSDVARR extension from the + # NPL file (EXTNAME, EXTVER) kw. + # This is used to populate DPj.EXTVER kw + wcsdvarr_x_version = 2 * extversion -1 + wcsdvarr_y_version = 2 * extversion + for ename in zip(['DX', 'DY'], [wcsdvarr_x_version,wcsdvarr_y_version],[dx, dy]): + error_val = ename[2].max() + cls.addSciExtKw(header, wdvarr_ver=ename[1], npol_extname=ename[0], error_val=error_val) + hdu = cls.createNpolHDU(header, npolfile=nplfile, \ + wdvarr_ver=ename[1], npl_extname=ename[0], data=ename[2],ccdchip=ccdchip) + if wcsdvarr_ind: + fobj[wcsdvarr_ind[ename[1]]] = hdu + else: + fobj.append(hdu) + + + applyNPOLCorr = classmethod(applyNPOLCorr) + + def getWCSIndex(cls, fobj): + + """ + If fobj has WCSDVARR extensions: + returns a mapping of their EXTVER kw to file object extension numbers + if fobj does not have WCSDVARR extensions: + an empty dictionary is returned + """ + wcsd = {} + for e in range(len(fobj)): + try: + ename = fobj[e].header['EXTNAME'] + except KeyError: + continue + if ename == 'WCSDVARR': + wcsd[fobj[e].header['EXTVER']] = e + logger.debug("A map of WSCDVARR externsions %s" % wcsd) + return wcsd + + getWCSIndex = classmethod(getWCSIndex) + + def addSciExtKw(cls, hdr, wdvarr_ver=None, npol_extname=None, error_val=0.0): + """ + Adds kw to sci extension to define WCSDVARR lookup table extensions + + """ + if npol_extname =='DX': + j=1 + else: + j=2 + + cperror = 'CPERR%s' %j + cpdis = 'CPDIS%s' %j + dpext = 'DP%s.' %j + 'EXTVER' + dpnaxes = 'DP%s.' %j +'NAXES' + dpaxis1 = 'DP%s.' %j+'AXIS.1' + dpaxis2 = 'DP%s.' %j+'AXIS.2' + keys = [cperror, cpdis, dpext, dpnaxes, dpaxis1, dpaxis2] + values = {cperror: error_val, + cpdis: 'Lookup', + dpext: wdvarr_ver, + dpnaxes: 2, + dpaxis1: 1, + dpaxis2: 2} + + comments = {cperror: 'Maximum error of NPOL correction for axis %s' % j, + cpdis: 'Prior distortion function type', + dpext: 'Version number of WCSDVARR extension containing lookup distortion table', + dpnaxes: 'Number of independent variables in distortion function', + dpaxis1: 'Axis number of the jth independent variable in a distortion function', + dpaxis2: 'Axis number of the jth independent variable in a distortion function' + } + # Look for HISTORY keywords. If present, insert new keywords before them + before_key = 'HISTORY' + if before_key not in hdr: + before_key = None + + for key in keys: + hdr.set(key, value=values[key], comment=comments[key], before=before_key) + + addSciExtKw = classmethod(addSciExtKw) + + def getData(cls,nplfile, ccdchip): + """ + Get the data arrays from the reference NPOL files + Make sure 'CCDCHIP' in the npolfile matches "CCDCHIP' in the science file. + """ + npl = fits.open(nplfile) + for ext in npl: + nplextname = ext.header.get('EXTNAME',"") + nplccdchip = ext.header.get('CCDCHIP',1) + if nplextname == 'DX' and nplccdchip == ccdchip: + xdata = ext.data.copy() + continue + elif nplextname == 'DY' and nplccdchip == ccdchip: + ydata = ext.data.copy() + continue + else: + continue + npl.close() + return xdata, ydata + getData = classmethod(getData) + + def transformData(cls, dx, dy, coeffs): + """ + Transform the NPOL data arrays for use with SIP + """ + ndx, ndy = np.dot(coeffs, [dx.ravel(), dy.ravel()]).astype(np.float32) + ndx.shape = dx.shape + ndy.shape=dy.shape + return ndx, ndy + + transformData = classmethod(transformData) + + def getIDCCoeffs(cls, header): + """ + Return a matrix of the scaled first order IDC coefficients. + """ + try: + ocx10 = header['OCX10'] + ocx11 = header['OCX11'] + ocy10 = header['OCY10'] + ocy11 = header['OCY11'] + coeffs = np.array([[ocx11, ocx10], [ocy11,ocy10]], dtype=np.float32) + except KeyError: + logger.exception('\n\tFirst order IDCTAB coefficients are not available. \n\ + Cannot convert SIP to IDC coefficients.') + return None + try: + idcscale = header['IDCSCALE'] + except KeyError: + logger.exception("IDCSCALE not found in header - setting it to 1.") + idcscale = 1 + + return np.linalg.inv(coeffs/idcscale) + + getIDCCoeffs = classmethod(getIDCCoeffs) + + def createNpolHDU(cls, sciheader, npolfile=None, wdvarr_ver=1, npl_extname=None,data = None, ccdchip=1): + """ + Creates an HDU to be added to the file object. + """ + hdr = cls.createNpolHdr(sciheader, npolfile=npolfile, wdvarr_ver=wdvarr_ver, npl_extname=npl_extname, ccdchip=ccdchip) + hdu = fits.ImageHDU(header=hdr, data=data) + return hdu + + createNpolHDU = classmethod(createNpolHDU) + + def createNpolHdr(cls, sciheader, npolfile, wdvarr_ver, npl_extname, ccdchip): + """ + Creates a header for the WCSDVARR extension based on the NPOL reference file + and sci extension header. The goal is to always work in image coordinates + (also for subarrays and binned images. The WCS for the WCSDVARR extension + i ssuch that a full size npol table is created and then shifted or scaled + if the science image is a subarray or binned image. + """ + npl = fits.open(npolfile) + npol_phdr = npl[0].header + for ext in npl: + try: + nplextname = ext.header['EXTNAME'] + nplextver = ext.header['EXTVER'] + except KeyError: + continue + nplccdchip = cls.get_ccdchip(npl, extname=nplextname, extver=nplextver) + if nplextname == npl_extname and nplccdchip == ccdchip: + npol_header = ext.header + break + else: + continue + npl.close() + + naxis = npl[1].header['NAXIS'] + ccdchip = nplextname #npol_header['CCDCHIP'] + + kw = { 'NAXIS': 'Size of the axis', + 'CDELT': 'Coordinate increment along axis', + 'CRPIX': 'Coordinate system reference pixel', + 'CRVAL': 'Coordinate system value at reference pixel', + } + + kw_comm1 = {} + kw_val1 = {} + for key in kw.keys(): + for i in range(1, naxis+1): + si = str(i) + kw_comm1[key+si] = kw[key] + + for i in range(1, naxis+1): + si = str(i) + kw_val1['NAXIS'+si] = npol_header.get('NAXIS'+si) + kw_val1['CDELT'+si] = npol_header.get('CDELT'+si, 1.0) * \ + sciheader.get('LTM'+si+'_'+si, 1) + kw_val1['CRPIX'+si] = npol_header.get('CRPIX'+si, 0.0) + kw_val1['CRVAL'+si] = (npol_header.get('CRVAL'+si, 0.0) - \ + sciheader.get('LTV'+str(i), 0)) + + kw_comm0 = {'XTENSION': 'Image extension', + 'BITPIX': 'IEEE floating point', + 'NAXIS': 'Number of axes', + 'EXTNAME': 'WCS distortion array', + 'EXTVER': 'Distortion array version number', + 'PCOUNT': 'Special data area of size 0', + 'GCOUNT': 'One data group', + } + + kw_val0 = { 'XTENSION': 'IMAGE', + 'BITPIX': -32, + 'NAXIS': naxis, + 'EXTNAME': 'WCSDVARR', + 'EXTVER': wdvarr_ver, + 'PCOUNT': 0, + 'GCOUNT': 1, + 'CCDCHIP': ccdchip, + } + cdl = [] + for key in kw_comm0.keys(): + cdl.append((key, kw_val0[key], kw_comm0[key])) + for key in kw_comm1.keys(): + cdl.append((key, kw_val1[key], kw_comm1[key])) + # Now add keywords from NPOLFILE header to document source of calibration + # include all keywords after and including 'FILENAME' from header + start_indx = -1 + end_indx = 0 + for i, c in enumerate(npol_phdr): + if c == 'FILENAME': + start_indx = i + if c == '': # remove blanks from end of header + end_indx = i+1 + break + if start_indx >= 0: + for card in npol_phdr.cards[start_indx:end_indx]: + cdl.append(card) + + hdr = fits.Header(cards=cdl) + + return hdr + + createNpolHdr = classmethod(createNpolHdr) + + def get_ccdchip(cls, fobj, extname, extver): + """ + Given a science file or npol file determine CCDCHIP + """ + ccdchip = 1 + if fobj[0].header['INSTRUME'] == 'ACS' and fobj[0].header['DETECTOR'] == 'WFC': + ccdchip = fobj[extname, extver].header['CCDCHIP'] + elif fobj[0].header['INSTRUME'] == 'WFC3' and fobj[0].header['DETECTOR'] == 'UVIS': + ccdchip = fobj[extname, extver].header['CCDCHIP'] + elif fobj[0].header['INSTRUME'] == 'WFPC2': + ccdchip = fobj[extname, extver].header['DETECTOR'] + elif fobj[0].header['INSTRUME'] == 'STIS': + ccdchip = fobj[extname, extver].header['DETECTOR'] + elif fobj[0].header['INSTRUME'] == 'NICMOS': + ccdchip = fobj[extname, extver].header['CAMERA'] + return ccdchip + + get_ccdchip = classmethod(get_ccdchip) diff --git a/stwcs/updatewcs/utils.py b/stwcs/updatewcs/utils.py new file mode 100644 index 0000000..1214199 --- /dev/null +++ b/stwcs/updatewcs/utils.py @@ -0,0 +1,264 @@ +from __future__ import division # confidence high +import os +from astropy.io import fits +from stsci.tools import fileutil + +import logging +logger = logging.getLogger("stwcs.updatewcs.utils") + + +def diff_angles(a,b): + """ + Perform angle subtraction a-b taking into account + small-angle differences across 360degree line. + """ + + diff = a - b + + if diff > 180.0: + diff -= 360.0 + + if diff < -180.0: + diff += 360.0 + + return diff + +def getBinning(fobj, extver=1): + # Return the binning factor + binned = 1 + if fobj[0].header['INSTRUME'] == 'WFPC2': + mode = fobj[0].header.get('MODE', "") + if mode == 'AREA': binned = 2 + else: + binned = fobj['SCI', extver].header.get('BINAXIS',1) + return binned + +def updateNEXTENDKw(fobj): + """ + Updates PRIMARY header with correct value for NEXTEND, if present. + + Parameters + ----------- + fobj : `astropy.io.fits.HDUList` + The FITS object for file opened in `update` mode + + """ + if 'nextend' in fobj[0].header: + fobj[0].header['nextend'] = len(fobj) -1 + +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 construct_distname(fobj,wcsobj): + """ + This function constructs the value for the keyword 'DISTNAME'. + It relies on the reference files specified by the keywords 'IDCTAB', + 'NPOLFILE', and 'D2IMFILE'. + + The final constructed value will be of the form: + <idctab rootname>-<npolfile rootname>-<d2imfile rootname> + and have a value of 'NONE' if no reference files are specified. + """ + idcname = extract_rootname(fobj[0].header.get('IDCTAB', "NONE"), + suffix='_idc') + if (idcname is None or idcname=='NONE') and wcsobj.sip is not None: + idcname = 'UNKNOWN' + + npolname, npolfile = build_npolname(fobj) + if npolname is None and wcsobj.cpdis1 is not None: + npolname = 'UNKNOWN' + + d2imname, d2imfile = build_d2imname(fobj) + if d2imname is None and wcsobj.det2im is not None: + d2imname = 'UNKNOWN' + + sipname, idctab = build_sipname(fobj) + + distname = build_distname(sipname,npolname,d2imname) + return {'DISTNAME':distname,'SIPNAME':sipname} + +def build_distname(sipname,npolname,d2imname): + """ + Core function to build DISTNAME keyword value without the HSTWCS input. + """ + + distname = sipname.strip() + if npolname != 'NONE' or d2imname != 'NONE': + if d2imname != 'NONE': + distname+= '-'+npolname.strip() + '-'+d2imname.strip() + else: + distname+='-'+npolname.strip() + + return distname + +def build_default_wcsname(idctab): + + idcname = extract_rootname(idctab,suffix='_idc') + wcsname = 'IDC_' + idcname + return wcsname + +def build_sipname(fobj, fname=None, sipname=None): + """ + Build a SIPNAME from IDCTAB + + Parameters + ---------- + fobj : `astropy.io.fits.HDUList` + file object + fname : string + science file name (to be used if ROOTNAME is not present + sipname : string + user supplied SIPNAME keyword + + Returns + ------- + sipname, idctab + """ + try: + idctab = fobj[0].header['IDCTAB'] + idcname = extract_rootname(idctab,suffix='_idc') + except KeyError: + idctab = 'N/A' + idcname= 'N/A' + if not fname: + try: + fname = fobj.filename() + except: + fname = " " + if not sipname: + try: + sipname = fobj[0].header["SIPNAME"] + if idcname == 'N/A' or idcname not in sipname: + raise KeyError + except KeyError: + if idcname != 'N/A': + try: + rootname = fobj[0].header['rootname'] + except KeyError: + rootname = fname + sipname = rootname +'_'+ idcname + else: + if 'A_ORDER' in fobj[1].header or 'B_ORDER' in fobj[1].header: + sipname = 'UNKNOWN' + else: + idcname = 'NOMODEL' + + return sipname, idctab + +def build_npolname(fobj, npolfile=None): + """ + Build a NPOLNAME from NPOLFILE + + Parameters + ---------- + fobj : `astropy.io.fits.HDUList` + file object + npolfile : string + user supplied NPOLFILE keyword + + Returns + ------- + npolname, npolfile + """ + if not npolfile: + try: + npolfile = fobj[0].header["NPOLFILE"] + except KeyError: + npolfile = ' ' + if fileutil.countExtn(fobj, 'WCSDVARR'): + npolname = 'UNKNOWN' + else: + npolname = 'NOMODEL' + npolname = extract_rootname(npolfile, suffix='_npl') + if npolname == 'NONE': + npolname = 'NOMODEL' + else: + npolname = extract_rootname(npolfile, suffix='_npl') + if npolname == 'NONE': + npolname = 'NOMODEL' + return npolname, npolfile + +def build_d2imname(fobj, d2imfile=None): + """ + Build a D2IMNAME from D2IMFILE + + Parameters + ---------- + fobj : `astropy.io.fits.HDUList` + file object + d2imfile : string + user supplied NPOLFILE keyword + + Returns + ------- + d2imname, d2imfile + """ + if not d2imfile: + try: + d2imfile = fobj[0].header["D2IMFILE"] + except KeyError: + d2imfile = 'N/A' + if fileutil.countExtn(fobj, 'D2IMARR'): + d2imname = 'UNKNOWN' + else: + d2imname = 'NOMODEL' + d2imname = extract_rootname(d2imfile,suffix='_d2i') + if d2imname == 'NONE': + d2imname = 'NOMODEL' + else: + d2imname = extract_rootname(d2imfile,suffix='_d2i') + if d2imname == 'NONE': + d2imname = 'NOMODEL' + return d2imname, d2imfile + + +def remove_distortion(fname, dist_keyword): + logger.info("Removing distortion {0} from file {0}".format(dist_keyword, fname)) + from ..wcsutil import altwcs + if dist_keyword == "NPOLFILE": + extname = "WCSDVARR" + keywords = ["CPERR*", "DP1.*", "DP2.*", "CPDIS*", "NPOLEXT"] + elif dist_keyword == "D2IMFILE": + extname = "D2IMARR" + keywords = ["D2IMERR*", "D2IM1.*", "D2IM2.*", "D2IMDIS*", "D2IMEXT"] + else: + raise AttributeError("Unrecognized distortion keyword " + "{0} when attempting to remove distortion".format(dist_keyword)) + ext_mapping = altwcs.mapFitsExt2HDUListInd(fname, "SCI").values() + f = fits.open(fname, mode="update") + for hdu in ext_mapping: + for kw in keywords: + try: + del f[hdu].header[kw] + except KeyError: + pass + ext_mapping = altwcs.mapFitsExt2HDUListInd(fname, extname).values() + ext_mapping.sort() + for hdu in ext_mapping[::-1]: + del f[hdu] + f.close() diff --git a/stwcs/updatewcs/wfpc2_dgeo.py b/stwcs/updatewcs/wfpc2_dgeo.py new file mode 100644 index 0000000..e57bb5c --- /dev/null +++ b/stwcs/updatewcs/wfpc2_dgeo.py @@ -0,0 +1,123 @@ +""" wfpc2_dgeo - Functions to convert WFPC2 DGEOFILE into D2IMFILE + +""" +import os +import datetime + +import astropy +from astropy.io import fits +import numpy as np + +from stsci.tools import fileutil + +import logging +logger = logging.getLogger("stwcs.updatewcs.apply_corrections") + +def update_wfpc2_d2geofile(filename, fhdu=None): + """ + Creates a D2IMFILE from the DGEOFILE for a WFPC2 image (input), and + modifies the header to reflect the new usage. + + Parameters + ---------- + filename: string + Name of WFPC2 file to be processed. This file will be updated + to delete any reference to a DGEOFILE and add a D2IMFILE to replace + that correction when running updatewcs. + fhdu: object + FITS object for WFPC2 image. If user has already opened the WFPC2 + file, they can simply pass that FITS object in for direct processing. + + Returns + ------- + d2imfile: string + Name of D2IMFILE created from DGEOFILE. The D2IMFILE keyword in the + image header will be updated/added to point to this newly created file. + + """ + + close_fhdu = False + if fhdu is None: + fhdu = fileutil.openImage(filename, mode='update') + close_fhdu = True + + dgeofile = fhdu['PRIMARY'].header.get('DGEOFILE', None) + already_converted = dgeofile not in [None, "N/A", "", " "] + if already_converted or 'ODGEOFIL' in fhdu['PRIMARY'].header: + if not already_converted: + dgeofile = fhdu['PRIMARY'].header.get('ODGEOFIL', None) + logger.info('Converting DGEOFILE %s into D2IMFILE...' % dgeofile) + rootname = filename[:filename.find('.fits')] + d2imfile = convert_dgeo_to_d2im(dgeofile,rootname) + fhdu['PRIMARY'].header['ODGEOFIL'] = dgeofile + fhdu['PRIMARY'].header['DGEOFILE'] = 'N/A' + fhdu['PRIMARY'].header['D2IMFILE'] = d2imfile + else: + d2imfile = None + fhdu['PRIMARY'].header['DGEOFILE'] = 'N/A' + if 'D2IMFILE' not in fhdu['PRIMARY'].header: + fhdu['PRIMARY'].header['D2IMFILE'] = 'N/A' + + # Only close the file handle if opened in this function + if close_fhdu: + fhdu.close() + + # return the d2imfile name so that calling routine can keep + # track of the new file created and delete it later if necessary + # (multidrizzle clean=True mode of operation) + return d2imfile + +def convert_dgeo_to_d2im(dgeofile,output,clobber=True): + """ Routine that converts the WFPC2 DGEOFILE into a D2IMFILE. + """ + dgeo = fileutil.openImage(dgeofile) + outname = output+'_d2im.fits' + + removeFileSafely(outname) + data = np.array([dgeo['dy',1].data[:,0]]) + scihdu = fits.ImageHDU(data=data) + dgeo.close() + # add required keywords for D2IM header + scihdu.header['EXTNAME'] = ('DY', 'Extension name') + scihdu.header['EXTVER'] = (1, 'Extension version') + fits_str = 'PYFITS Version '+str(astropy.__version__) + scihdu.header['ORIGIN'] = (fits_str, 'FITS file originator') + scihdu.header['INHERIT'] = (False, 'Inherits global header') + + dnow = datetime.datetime.now() + scihdu.header['DATE'] = (str(dnow).replace(' ','T'), + 'Date FITS file was generated') + + scihdu.header['CRPIX1'] = (0, 'Distortion array reference pixel') + scihdu.header['CDELT1'] = (1, 'Grid step size in first coordinate') + scihdu.header['CRVAL1'] = (0, 'Image array pixel coordinate') + scihdu.header['CRPIX2'] = (0, 'Distortion array reference pixel') + scihdu.header['CDELT2'] = (1, 'Grid step size in second coordinate') + scihdu.header['CRVAL2'] = (0, 'Image array pixel coordinate') + + phdu = fits.PrimaryHDU() + phdu.header['INSTRUME'] = 'WFPC2' + d2imhdu = fits.HDUList() + d2imhdu.append(phdu) + scihdu.header['DETECTOR'] = (1, 'CCD number of the detector: PC 1, WFC 2-4 ') + d2imhdu.append(scihdu.copy()) + scihdu.header['EXTVER'] = (2, 'Extension version') + scihdu.header['DETECTOR'] = (2, 'CCD number of the detector: PC 1, WFC 2-4 ') + d2imhdu.append(scihdu.copy()) + scihdu.header['EXTVER'] = (3, 'Extension version') + scihdu.header['DETECTOR'] = (3, 'CCD number of the detector: PC 1, WFC 2-4 ') + d2imhdu.append(scihdu.copy()) + scihdu.header['EXTVER'] = (4, 'Extension version') + scihdu.header['DETECTOR'] = (4, 'CCD number of the detector: PC 1, WFC 2-4 ') + d2imhdu.append(scihdu.copy()) + d2imhdu.writeto(outname) + d2imhdu.close() + + return outname + + +def removeFileSafely(filename,clobber=True): + """ Delete the file specified, but only if it exists and clobber is True. + """ + if filename is not None and filename.strip() != '': + if os.path.exists(filename) and clobber: os.remove(filename) diff --git a/stwcs/wcsutil/__init__.py b/stwcs/wcsutil/__init__.py new file mode 100644 index 0000000..65280be --- /dev/null +++ b/stwcs/wcsutil/__init__.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import, print_function # confidence high + +from .altwcs import * +from .hstwcs import HSTWCS + + +def help(): + doc = """ \ + 1. Using a `astropy.io.fits.HDUList` object and an extension number \n + Example:\n + from astropy.io improt fits + fobj = fits.open('some_file.fits')\n + w = wcsutil.HSTWCS(fobj, 3)\n\n + + 2. Create an HSTWCS object using a qualified file name. \n + Example:\n + w = wcsutil.HSTWCS('j9irw4b1q_flt.fits[sci,1]')\n\n + + 3. Create an HSTWCS object using a file name and an extension number. \n + Example:\n + w = wcsutil.HSTWCS('j9irw4b1q_flt.fits', ext=2)\n\n + + 4. Create an HSTWCS object from WCS with key 'O'.\n + Example:\n + + w = wcsutil.HSTWCS('j9irw4b1q_flt.fits', ext=2, wcskey='O')\n\n + + 5. Create a template HSTWCS object for a DEFAULT object.\n + Example:\n + w = wcsutil.HSTWCS(instrument='DEFAULT')\n\n + """ + + print('How to create an HSTWCS object:\n\n') + print(doc) diff --git a/stwcs/wcsutil/altwcs.py b/stwcs/wcsutil/altwcs.py new file mode 100644 index 0000000..aae1a9f --- /dev/null +++ b/stwcs/wcsutil/altwcs.py @@ -0,0 +1,758 @@ +from __future__ import division, print_function # confidence high +import os +import string + +import numpy as np +from astropy import wcs as pywcs +from astropy.io import fits +from stsci.tools import fileutil as fu + +altwcskw = ['WCSAXES', 'CRVAL', 'CRPIX', 'PC', 'CDELT', 'CD', 'CTYPE', 'CUNIT', + 'PV', 'PS'] +altwcskw_extra = ['LATPOLE','LONPOLE','RESTWAV','RESTFRQ'] + +# file operations +def archiveWCS(fname, ext, wcskey=" ", wcsname=" ", reusekey=False): + """ + Copy the primary WCS to the header as an alternate WCS + with wcskey and name WCSNAME. It loops over all extensions in 'ext' + + Parameters + ---------- + fname : string or `astropy.io.fits.HDUList` + file name or a file object + ext : int, tuple, str, or list of integers or tuples (e.g.('sci',1)) + fits extensions to work with + If a string is provided, it should specify the EXTNAME of extensions + with WCSs to be archived + wcskey : string "A"-"Z" or " " + if " ": get next available key if wcsname is also " " or try + to get a key from WCSNAME value + wcsname : string + Name of alternate WCS description + reusekey : boolean + if True - overwrites a WCS with the same key + + Examples + -------- + Copy the primary WCS of an in memory headrlet object to an + alternate WCS with key 'T' + + >>> hlet=headerlet.createHeaderlet('junk.fits', 'hdr1.fits') + >>> altwcs.wcskeys(hlet[1].header) + ['A'] + >>> altwcs.archiveWCS(hlet, ext=[('SIPWCS',1),('SIPWCS',2)], wcskey='T') + >>> altwcs.wcskeys(hlet[1].header) + ['A', 'T'] + + + See Also + -------- + wcsutil.restoreWCS: Copy an alternate WCS to the primary WCS + + """ + + if isinstance(fname, str): + f = fits.open(fname, mode='update') + else: + f = fname + + if not _parpasscheck(f, ext, wcskey, wcsname): + closefobj(fname, f) + raise ValueError("Input parameters problem") + + # Interpret input 'ext' value to get list of extensions to process + ext = _buildExtlist(f, ext) + + if not wcskey and not wcsname: + raise KeyError("Either wcskey or wcsname should be specified") + + if wcsname.strip() == "": + try: + wcsname = readAltWCS(f, ext[0], wcskey=" ")['WCSNAME'] + except KeyError: + pass + wcsext = ext[0] + if wcskey != " " and wcskey in wcskeys(f[wcsext].header) and not reusekey: + closefobj(fname, f) + raise KeyError("Wcskey %s is aready used. \ + Run archiveWCS() with reusekey=True to overwrite this alternate WCS. \ + Alternatively choose another wcskey with altwcs.available_wcskeys()." %wcskey) + elif wcskey == " ": + # wcsname exists, overwrite it if reuse is True or get the next key + if wcsname.strip() in wcsnames(f[wcsext].header).values(): + if reusekey: + # try getting the key from an existing WCS with WCSNAME + wkey = getKeyFromName(f[wcsext].header, wcsname) + wname = wcsname + if wkey == ' ': + wkey = next_wcskey(f[wcsext].header) + elif wkey is None: + closefobj(fname, f) + raise KeyError("Could not get a valid wcskey from wcsname %s" %wcsname) + else: + closefobj(fname, f) + raise KeyError("Wcsname %s is aready used. \ + Run archiveWCS() with reusekey=True to overwrite this alternate WCS. \ + Alternatively choose another wcskey with altwcs.available_wcskeys() or\ + choose another wcsname." %wcsname) + else: + wkey = next_wcskey(f[wcsext].header) + if wcsname.strip(): + wname = wcsname + else: + # determine which WCSNAME needs to be replicated in archived WCS + wnames = wcsnames(f[wcsext].header) + if 'O' in wnames: del wnames['O'] # we don't want OPUS/original + if len(wnames) > 0: + if ' ' in wnames: + wname = wnames[' '] + else: + akeys = string.uppercase + wname = "DEFAULT" + for key in akeys[-1::]: + if key in wnames: + wname = wnames + break + else: + wname = "DEFAULT" + else: + wkey = wcskey + wname = wcsname + + for e in ext: + hwcs = readAltWCS(f,e,wcskey=' ') + if hwcs is None: + continue + + wcsnamekey = 'WCSNAME' + wkey + f[e].header[wcsnamekey] = wname + + try: + old_wcsname=hwcs.pop('WCSNAME') + except: + pass + + for k in hwcs.keys(): + key = k[:7] + wkey + f[e].header[key] = hwcs[k] + closefobj(fname, f) + +def restore_from_to(f, fromext=None, toext=None, wcskey=" ", wcsname=" "): + """ + Copy an alternate WCS from one extension as a primary WCS of another extension + + Reads in a WCS defined with wcskey and saves it as the primary WCS. + Goes sequentially through the list of extensions in ext. + Alternatively uses 'fromext' and 'toext'. + + + Parameters + ---------- + f: string or `astropy.io.fits.HDUList` + a file name or a file object + fromext: string + extname from which to read in the alternate WCS, for example 'SCI' + toext: string or python list + extname or a list of extnames to which the WCS will be copied as + primary, for example ['SCI', 'ERR', 'DQ'] + wcskey: a charater + "A"-"Z" - Used for one of 26 alternate WCS definitions. + or " " - find a key from WCSNAMe value + wcsname: string (optional) + if given and wcskey is " ", will try to restore by WCSNAME value + + See Also + -------- + archiveWCS - copy the primary WCS as an alternate WCS + restoreWCS - Copy a WCS with key "WCSKEY" to the primary WCS + + """ + if isinstance(f, str): + fobj = fits.open(f, mode='update') + else: + fobj = f + + if not _parpasscheck(fobj, ext=None, wcskey=wcskey, fromext=fromext, toext=toext): + closefobj(f, fobj) + raise ValueError("Input parameters problem") + + # Interpret input 'ext' value to get list of extensions to process + #ext = _buildExtlist(fobj, ext) + + if isinstance(toext, str): + toext = [toext] + + # the case of an HDUList object in memory without an associated file + + #if fobj.filename() is not None: + # name = fobj.filename() + + simplefits = fu.isFits(fobj)[1] is 'simple' + if simplefits: + wcskeyext = 0 + else: + wcskeyext = 1 + + if wcskey == " ": + if wcsname.strip(): + wkey = getKeyFromName(fobj[wcskeyext].header, wcsname) + if not wkey: + closefobj(f, fobj) + raise KeyError("Could not get a key from wcsname %s ." % wcsname) + else: + if wcskey not in wcskeys(fobj, ext=wcskeyext): + print("Could not find alternate WCS with key %s in this file" % wcskey) + closefobj(f, fobj) + return + wkey = wcskey + + countext = fu.countExtn(fobj, fromext) + if not countext: + raise KeyError("File does not have extension with extname %s", fromext) + else: + for i in range(1, countext+1): + for toe in toext: + _restore(fobj, fromextnum=i, fromextnam=fromext, toextnum=i, toextnam=toe, ukey=wkey) + + if fobj.filename() is not None: + #fobj.writeto(name) + closefobj(f, fobj) + +def restoreWCS(f, ext, wcskey=" ", wcsname=" "): + """ + Copy a WCS with key "WCSKEY" to the primary WCS + + Reads in a WCS defined with wcskey and saves it as the primary WCS. + Goes sequentially through the list of extensions in ext. + Alternatively uses 'fromext' and 'toext'. + + + Parameters + ---------- + f : str or `astropy.io.fits.HDUList` + file name or a file object + ext : int, tuple, str, or list of integers or tuples (e.g.('sci',1)) + fits extensions to work with + If a string is provided, it should specify the EXTNAME of extensions + with WCSs to be archived + wcskey : str + "A"-"Z" - Used for one of 26 alternate WCS definitions. + or " " - find a key from WCSNAMe value + wcsname : str + (optional) if given and wcskey is " ", will try to restore by WCSNAME value + + See Also + -------- + archiveWCS - copy the primary WCS as an alternate WCS + restore_from_to + + """ + if isinstance(f, str): + fobj = fits.open(f, mode='update') + else: + fobj = f + + if not _parpasscheck(fobj, ext=ext, wcskey=wcskey): + closefobj(f, fobj) + raise ValueError("Input parameters problem") + + # Interpret input 'ext' value to get list of extensions to process + ext = _buildExtlist(fobj, ext) + + + # the case of an HDUList object in memory without an associated file + + #if fobj.filename() is not None: + # name = fobj.filename() + + simplefits = fu.isFits(fobj)[1] is 'simple' + if simplefits: + wcskeyext = 0 + else: + wcskeyext = 1 + + if wcskey == " ": + if wcsname.strip(): + wkey = getKeyFromName(fobj[wcskeyext].header, wcsname) + if not wkey: + closefobj(f, fobj) + raise KeyError("Could not get a key from wcsname %s ." % wcsname) + else: + if wcskey not in wcskeys(fobj, ext=wcskeyext): + #print "Could not find alternate WCS with key %s in this file" % wcskey + closefobj(f, fobj) + return + wkey = wcskey + + for e in ext: + _restore(fobj, wkey, fromextnum=e, verbose=False) + + if fobj.filename() is not None: + #fobj.writeto(name) + closefobj(f, fobj) + +def deleteWCS(fname, ext, wcskey=" ", wcsname=" "): + """ + Delete an alternate WCS defined with wcskey. + If wcskey is " " try to get a key from WCSNAME. + + Parameters + ---------- + fname : str or a `astropy.io.fits.HDUList` + ext : int, tuple, str, or list of integers or tuples (e.g.('sci',1)) + fits extensions to work with + If a string is provided, it should specify the EXTNAME of extensions + with WCSs to be archived + wcskey : str + one of 'A'-'Z' or " " + wcsname : str + Name of alternate WCS description + """ + if isinstance(fname, str): + fobj = fits.open(fname, mode='update') + else: + fobj = fname + + if not _parpasscheck(fobj, ext, wcskey, wcsname): + closefobj(fname, fobj) + raise ValueError("Input parameters problem") + + # Interpret input 'ext' value to get list of extensions to process + ext = _buildExtlist(fobj, ext) + # Do not allow deleting the original WCS. + if wcskey == 'O': + print("Wcskey 'O' is reserved for the original WCS and should not be deleted.") + closefobj(fname, fobj) + return + + wcskeyext = ext[0] + + if not wcskeys and not wcsname: + raise KeyError("Either wcskey or wcsname should be specified") + + if wcskey == " ": + # try getting the key from WCSNAME + wkey = getKeyFromName(fobj[wcskeyext].header, wcsname) + if not wkey: + closefobj(fname, fobj) + raise KeyError("Could not get a key: wcsname '%s' not found in header." % wcsname) + else: + if wcskey not in wcskeys(fobj[wcskeyext].header): + closefobj(fname, fobj) + raise KeyError("Could not find alternate WCS with key %s in this file" % wcskey) + wkey = wcskey + + prexts = [] + for i in ext: + hdr = fobj[i].header + hwcs = readAltWCS(fobj,i,wcskey=wkey) + if hwcs is None: + continue + for k in hwcs[::-1]: + del hdr[k] + #del hdr['ORIENT'+wkey] + prexts.append(i) + if prexts != []: + print('Deleted all instances of WCS with key %s in extensions' % wkey, prexts) + else: + print("Did not find WCS with key %s in any of the extensions" % wkey) + closefobj(fname, fobj) + +def _buildExtlist(fobj, ext): + """ + Utility function to interpret the provided value of 'ext' and return a list + of 'valid' values which can then be used by the rest of the functions in + this module. + + Parameters + ---------- + fobj: HDUList + file to be examined + ext: an int, a tuple, string, list of integers or tuples (e.g.('sci',1)) + fits extensions to work with + If a string is provided, it should specify the EXTNAME of extensions + with WCSs to be archived + """ + if not isinstance(ext,list): + if isinstance(ext,str): + extstr = ext + ext = [] + for extn in range(1, len(fobj)): + if 'extname' in fobj[extn].header and fobj[extn].header['extname'] == extstr: + ext.append(extn) + elif isinstance(ext, int) or isinstance(ext, tuple): + ext = [ext] + else: + raise KeyError("Valid extensions in 'ext' parameter need to be specified.") + return ext + +def _restore(fobj, ukey, fromextnum, + toextnum=None, fromextnam=None, toextnam=None, verbose=True): + """ + fobj: string of HDUList + ukey: string 'A'-'Z' + wcs key + fromextnum: int + extver of extension from which to copy WCS + fromextnam: string + extname of extension from which to copy WCS + toextnum: int + extver of extension to which to copy WCS + toextnam: string + extname of extension to which to copy WCS + """ + # create an extension tuple, e.g. ('SCI', 2) + if fromextnam: + fromextension = (fromextnam, fromextnum) + else: + fromextension = fromextnum + if toextnum: + if toextnam: + toextension = (toextnam, toextnum) + else: + toextension =toextnum + else: + toextension = fromextension + + hwcs = readAltWCS(fobj,fromextension,wcskey=ukey,verbose=verbose) + if hwcs is None: + return + + for k in hwcs.keys(): + key = k[:-1] + if key in fobj[toextension].header: + #fobj[toextension].header.update(key=key, value = hwcs[k]) + fobj[toextension].header[key] = hwcs[k] + else: + continue + if key == 'O' and 'TDDALPHA' in fobj[toextension].header: + fobj[toextension].header['TDDALPHA'] = 0.0 + fobj[toextension].header['TDDBETA'] = 0.0 + if 'ORIENTAT' in fobj[toextension].header: + norient = np.rad2deg(np.arctan2(hwcs['CD1_2'+'%s' %ukey], hwcs['CD2_2'+'%s' %ukey])) + fobj[toextension].header['ORIENTAT'] = norient + # Reset 2014 TDD keywords prior to computing new values (if any are computed) + for kw in ['TDD_CYA','TDD_CYB','TDD_CXA','TDD_CXB']: + if kw in fobj[toextension].header: + fobj[toextension].header[kw] = 0.0 + +#header operations +def _check_headerpars(fobj, ext): + if not isinstance(fobj, fits.Header) and not isinstance(fobj, fits.HDUList) \ + and not isinstance(fobj, str): + raise ValueError("Expected a file name, a file object or a header\n") + + if not isinstance(fobj, fits.Header): + #raise ValueError("Expected a valid ext parameter when input is a file") + if not isinstance(ext, int) and not isinstance(ext, tuple): + raise ValueError("Expected ext to be a number or a tuple, e.g. ('SCI', 1)\n") + +def _getheader(fobj, ext): + if isinstance(fobj, str): + hdr = fits.getheader(fobj,ext) + elif isinstance(fobj, fits.Header): + hdr = fobj + else: + hdr = fobj[ext].header + return hdr + +def readAltWCS(fobj, ext, wcskey=' ',verbose=False): + """ Reads in alternate WCS from specified extension + + Parameters + ---------- + fobj : str, `astropy.io.fits.HDUList` + fits filename or fits file object + containing alternate/primary WCS(s) to be converted + wcskey : str + [" ",A-Z] + alternate/primary WCS key that will be replaced by the new key + ext : int + fits extension number + + Returns + ------- + hdr: fits.Header + header object with ONLY the keywords for specified alternate WCS + """ + if isinstance(fobj, str): + fobj = fits.open(fobj) + + hdr = _getheader(fobj,ext) + try: + nwcs = pywcs.WCS(hdr, fobj=fobj, key=wcskey) + except KeyError: + if verbose: + print('readAltWCS: Could not read WCS with key %s' %wcskey) + print(' Skipping %s[%s]' % (fobj.filename(), str(ext))) + return None + hwcs = nwcs.to_header() + + if nwcs.wcs.has_cd(): + hwcs = pc2cd(hwcs, key=wcskey) + + return hwcs + +def convertAltWCS(fobj,ext,oldkey=" ",newkey=' '): + """ + Translates the alternate/primary WCS with one key to an alternate/primary WCS with + another key. + + Parameters + ---------- + fobj : str, `astropy.io.fits.HDUList`, or `astropy.io.fits.Header` + fits filename, fits file object or fits header + containing alternate/primary WCS(s) to be converted + ext : int + extension number + oldkey : str + [" ",A-Z] + alternate/primary WCS key that will be replaced by the new key + newkey : str + [" ",A-Z] + new alternate/primary WCS key + + Returns + ------- + hdr: `astropy.io.fits.Header` + header object with keywords renamed from oldkey to newkey + """ + hdr = readAltWCS(fobj,ext,wcskey=oldkey) + if hdr is None: + return None + # Converting WCS to new key + for card in hdr: + if oldkey == ' ' or oldkey == '': + cname = card + else: + cname = card.rstrip(oldkey) + hdr.rename_key(card,cname+newkey,force=True) + + return hdr + +def wcskeys(fobj, ext=None): + """ + Returns a list of characters used in the header for alternate + WCS description with WCSNAME keyword + + Parameters + ---------- + fobj : str, `astropy.io.fits.HDUList` or `astropy.io.fits.Header` + fits file name, fits file object or fits header + ext : int or None + extension number + if None, fobj must be a header + """ + _check_headerpars(fobj, ext) + hdr = _getheader(fobj, ext) + names = hdr["WCSNAME*"] + d = [] + for key in names: + wkey = key.replace('WCSNAME','') + if wkey == '': wkey = ' ' + d.append(wkey) + return d + +def wcsnames(fobj, ext=None): + """ + Returns a dictionary of wcskey: WCSNAME pairs + + Parameters + ---------- + fobj : stri, `astropy.io.fits.HDUList` or `astropy.io.fits.Header` + fits file name, fits file object or fits header + ext : int or None + extension number + if None, fobj must be a header + + """ + _check_headerpars(fobj, ext) + hdr = _getheader(fobj, ext) + names = hdr["WCSNAME*"] + d = {} + for keyword, value in names.items(): + wkey = keyword.replace('WCSNAME','') + if wkey == '': wkey = ' ' + d[wkey] = value + return d + +def available_wcskeys(fobj, ext=None): + """ + Returns a list of characters which are not used in the header + with WCSNAME keyword. Any of them can be used to save a new + WCS. + + Parameters + ---------- + fobj : str, `astropy.io.fits.HDUList` or `astropy.io.fits.Header` + fits file name, fits file object or fits header + ext : int or None + extension number + if None, fobj must be a header + """ + _check_headerpars(fobj, ext) + hdr = _getheader(fobj, ext) + all_keys = list(string.ascii_uppercase) + used_keys = wcskeys(hdr) + try: + used_keys.remove(" ") + except ValueError: + pass + [all_keys.remove(key) for key in used_keys] + return all_keys + +def next_wcskey(fobj, ext=None): + """ + Returns next available character to be used for an alternate WCS + + Parameters + ---------- + fobj : str, `astropy.io.fits.HDUList` or `astropy.io.fits.Header` + fits file name, fits file object or fits header + ext : int or None + extension number + if None, fobj must be a header + """ + _check_headerpars(fobj, ext) + hdr = _getheader(fobj, ext) + allkeys = available_wcskeys(hdr) + if allkeys != []: + return allkeys[0] + else: + return None + +def getKeyFromName(header, wcsname): + """ + If WCSNAME is found in header, return its key, else return + None. This is used to update an alternate WCS + repeatedly and not generate new keys every time. + + Parameters + ---------- + header : `astropy.io.fits.Header` + wcsname : str + value of WCSNAME + """ + wkey = None + names = wcsnames(header) + wkeys = [] + for item in names.items(): + if item[1].lower() == wcsname.lower(): + wkeys.append(item[0]) + wkeys.sort() + if len(wkeys) > 0: + wkey = wkeys[-1] + else: + wkey = None + return wkey + +def pc2cd(hdr, key=' '): + """ + Convert a CD PC matrix to a CD matrix. + + WCSLIB (and PyWCS) recognizes CD keywords as input + but converts them and works internally with the PC matrix. + to_header() returns the PC matrix even if the i nput was a + CD matrix. To keep input and output consistent we check + for has_cd and convert the PC back to CD. + + Parameters + ---------- + hdr: `astropy.io.fits.Header` + + """ + for c in ['1_1', '1_2', '2_1', '2_2']: + try: + val = hdr['PC'+c+'%s' % key] + del hdr['PC'+c+ '%s' % key] + except KeyError: + if c=='1_1' or c == '2_2': + val = 1. + else: + val = 0. + #hdr.update(key='CD'+c+'%s' %key, value=val) + hdr['CD{0}{1}'.format(c, key)] = val + return hdr + +def _parpasscheck(fobj, ext, wcskey, fromext=None, toext=None, reusekey=False): + """ + Check input parameters to altwcs functions + + fobj : str or `astropy.io.fits.HDUList` object + a file name or a file object + ext : int, a tuple, a python list of integers or a python list + of tuples (e.g.('sci',1)) + fits extensions to work with + wcskey : str + "A"-"Z" or " "- Used for one of 26 alternate WCS definitions + wcsname : str + (optional) + if given and wcskey is " ", will try to restore by WCSNAME value + reusekey : bool + A flag which indicates whether to reuse a wcskey in the header + """ + if not isinstance(fobj, fits.HDUList): + print("First parameter must be a fits file object or a file name.") + return False + + # first one covers the case of an object created in memory + # (e.g. headerlet) for which fileinfo returns None + if fobj.fileinfo(0) is None: + pass + else: + # an HDUList object with associated file + if fobj.fileinfo(0)['filemode'] is not 'update': + print("First parameter must be a file name or a file object opened in 'update' mode.") + return False + + if not isinstance(ext, int) and not isinstance(ext, tuple) \ + and not isinstance(ext,str) \ + and not isinstance(ext, list) and ext is not None: + print("Ext must be integer, tuple, string,a list of int extension numbers, \n\ + or a list of tuples representing a fits extension, for example ('sci', 1).") + return False + + if not isinstance(fromext, str) and fromext is not None: + print("fromext must be a string representing a valid extname") + return False + + if not isinstance(toext, list) and not isinstance(toext, str) and \ + toext is not None : + print("toext must be a string or a list of strings representing extname") + return False + + if len(wcskey) != 1: + print('Parameter wcskey must be a character - one of "A"-"Z" or " "') + return False + + return True + +def closefobj(fname, f): + """ + Functions in this module accept as input a file name or a file object. + If the input was a file name (string) we close the object. If the user + passed a file object we leave it to the user to close it. + """ + if isinstance(fname, str): + f.close() + +def mapFitsExt2HDUListInd(fname, extname): + """ + Map FITS extensions with 'EXTNAME' to HDUList indexes. + """ + + if not isinstance(fname, fits.HDUList): + f = fits.open(fname) + close_file = True + else: + f = fname + close_file = False + d = {} + for hdu in f: + if 'EXTNAME' in hdu.header and hdu.header['EXTNAME'] == extname: + extver = hdu.header['EXTVER'] + d[(extname, extver)] = f.index_of((extname, extver)) + if close_file: + f.close() + return d diff --git a/stwcs/wcsutil/convertwcs.py b/stwcs/wcsutil/convertwcs.py new file mode 100644 index 0000000..a384eb1 --- /dev/null +++ b/stwcs/wcsutil/convertwcs.py @@ -0,0 +1,118 @@ +from __future__ import print_function +try: + import stwcs + from stwcs import wcsutil +except: + stwcs = None + +from stsci.tools import fileutil + +OPUS_WCSKEYS = ['OCRVAL1','OCRVAL2','OCRPIX1','OCRPIX2', + 'OCD1_1','OCD1_2','OCD2_1','OCD2_2', + 'OCTYPE1','OCTYPE2'] + + +def archive_prefix_OPUS_WCS(fobj,extname='SCI'): + """ Identifies WCS keywords which were generated by OPUS and archived + using a prefix of 'O' for all 'SCI' extensions in the file + + Parameters + ---------- + fobj : str or `astropy.io.fits.HDUList` + Filename or fits object of a file + + """ + if stwcs is None: + print('=====================') + print('The STWCS package is needed to convert an old-style OPUS WCS to an alternate WCS') + print('=====================') + raise ImportError + + + closefits = False + if isinstance(fobj,str): + # A filename was provided as input + fobj = fits.open(fobj,mode='update') + closefits=True + + # Define the header + ext = ('sci',1) + hdr = fobj[ext].header + + numextn = fileutil.countExtn(fobj) + extlist = [] + for e in range(1,numextn+1): + extlist.append(('sci',e)) + + # Insure that the 'O' alternate WCS is present + if 'O' not in wcsutil.wcskeys(hdr): + # if not, archive the Primary WCS as the default OPUS WCS + wcsutil.archiveWCS(fobj,extlist, wcskey='O', wcsname='OPUS') + + # find out how many SCI extensions are in the image + numextn = fileutil.countExtn(fobj,extname=extname) + if numextn == 0: + extname = 'PRIMARY' + + # create HSTWCS object from PRIMARY WCS + wcsobj = wcsutil.HSTWCS(fobj,ext=ext,wcskey='O') + # get list of WCS keywords + wcskeys = list(wcsobj.wcs2header().keys()) + + # For each SCI extension... + for e in range(1,numextn+1): + # Now, look for any WCS keywords with a prefix of 'O' + for key in wcskeys: + okey = 'O'+key[:7] + hdr = fobj[(extname,e)].header + if okey in hdr: + # Update alternate WCS keyword with prefix-O OPUS keyword value + hdr[key] = hdr[okey] + + if closefits: + fobj.close() + +def create_prefix_OPUS_WCS(fobj,extname='SCI'): + """ Creates alternate WCS with a prefix of 'O' for OPUS generated WCS values + to work with old MultiDrizzle. + + Parameters + ---------- + fobj : str or `astropy.io.fits.HDUList` + Filename or fits object of a file + + Raises + ------ + IOError: + if input FITS object was not opened in 'update' mode + + """ + # List of O-prefix keywords to create + owcskeys = OPUS_WCSKEYS + + closefits = False + if isinstance(fobj,str): + # A filename was provided as input + fobj = fits.open(fobj, mode='update') + closefits=True + else: + # check to make sure this FITS obj has been opened in update mode + if fobj.fileinfo(0)['filemode'] != 'update': + print('File not opened with "mode=update". Quitting...') + raise IOError + + # check for existance of O-prefix WCS + if owcskeys[0] not in fobj['sci',1].header: + + # find out how many SCI extensions are in the image + numextn = fileutil.countExtn(fobj,extname=extname) + if numextn == 0: + extname = '' + for extn in range(1,numextn+1): + hdr = fobj[(extname,extn)].header + for okey in owcskeys: + hdr[okey] = hdr[okey[1:]+'O'] + + # Close FITS image if we had to open it... + if closefits: + fobj.close() diff --git a/stwcs/wcsutil/getinput.py b/stwcs/wcsutil/getinput.py new file mode 100644 index 0000000..8ee1123 --- /dev/null +++ b/stwcs/wcsutil/getinput.py @@ -0,0 +1,62 @@ +from astropy.io import fits +from stsci.tools import irafglob, fileutil, parseinput + +def parseSingleInput(f=None, ext=None): + if isinstance(f, str): + # create an HSTWCS object from a filename + if ext != None: + filename = f + if isinstance(ext,tuple): + if ext[0] == '': + extnum = ext[1] # handle ext=('',1) + else: + extnum = ext + else: + extnum = int(ext) + elif ext == None: + filename, ext = fileutil.parseFilename(f) + ext = fileutil.parseExtn(ext) + if ext[0] == '': + extnum = int(ext[1]) #handle ext=('',extnum) + else: + extnum = ext + phdu = fits.open(filename) + hdr0 = phdu[0].header + try: + ehdr = phdu[extnum].header + except (IndexError, KeyError) as e: + raise e.__class__('Unable to get extension %s.' % extnum) + + elif isinstance(f, fits.HDUList): + phdu = f + if ext == None: + extnum = 0 + else: + extnum = ext + ehdr = f[extnum].header + hdr0 = f[0].header + filename = hdr0.get('FILENAME', "") + + else: + raise ValueError('Input must be a file name string or a' + '`astropy.io.fits.HDUList` object') + + return filename, hdr0, ehdr, phdu + + +def parseMultipleInput(input): + if isinstance(input, str): + if input[0] == '@': + # input is an @ file + filelist = irafglob.irafglob(input) + else: + try: + filelist, output = parseinput.parseinput(input) + except IOError: raise + elif isinstance(input, list): + if isinstance(input[0], wcsutil.HSTWCS): + # a list of HSTWCS objects + return input + else: + filelist = input[:] + return filelist diff --git a/stwcs/wcsutil/headerlet.py b/stwcs/wcsutil/headerlet.py new file mode 100644 index 0000000..c0dd9b0 --- /dev/null +++ b/stwcs/wcsutil/headerlet.py @@ -0,0 +1,2754 @@ +""" +This module implements headerlets. + +A headerlet serves as a mechanism for encapsulating WCS information +which can be used to update the WCS solution of an image. The idea +came up first from the desire for passing improved astrometric +solutions for HST data and provide those solutions in a manner +that would not require getting entirely new images from the archive +when only the WCS information has been updated. + +""" + +from __future__ import absolute_import, division, print_function +import os +import sys +import functools +import logging +import textwrap +import copy +import time + +import numpy as np +from astropy.io import fits +#import pywcs +from astropy import wcs as pywcs +from astropy.utils import lazyproperty + +from stsci.tools.fileutil import countExtn +from stsci.tools import fileutil as fu +from stsci.tools import parseinput + +from stwcs.updatewcs import utils +from . import altwcs +from . import wcscorr +from .hstwcs import HSTWCS +from .mappings import basic_wcs + +#### Logging support functions +class FuncNameLoggingFormatter(logging.Formatter): + def __init__(self, fmt=None, datefmt=None): + if '%(funcName)s' not in fmt: + fmt = '%(funcName)s' + fmt + logging.Formatter.__init__(self, fmt=fmt, datefmt=datefmt) + + def format(self, record): + record = copy.copy(record) + if hasattr(record, 'funcName') and record.funcName == 'init_logging': + record.funcName = '' + else: + record.funcName += ' ' + return logging.Formatter.format(self, record) + + +logger = logging.getLogger(__name__) +formatter = FuncNameLoggingFormatter("%(levelname)s: %(message)s") +ch = logging.StreamHandler() +ch.setFormatter(formatter) +ch.setLevel(logging.CRITICAL) +logger.addHandler(ch) +logger.setLevel(logging.DEBUG) + +FITS_STD_KW = ['XTENSION', 'BITPIX', 'NAXIS', 'PCOUNT', + 'GCOUNT', 'EXTNAME', 'EXTVER', 'ORIGIN', + 'INHERIT', 'DATE', 'IRAF-TLM'] + +DEFAULT_SUMMARY_COLS = ['HDRNAME', 'WCSNAME', 'DISTNAME', 'AUTHOR', 'DATE', + 'SIPNAME', 'NPOLFILE', 'D2IMFILE', 'DESCRIP'] +COLUMN_DICT = {'vals': [], 'width': []} +COLUMN_FMT = '{:<{width}}' + + +def init_logging(funcname=None, level=100, mode='w', **kwargs): + """ + + Initialize logging for a function + + Parameters + ---------- + funcname: string + Name of function which will be recorded in log + level: int, or bool, or string + int or string : Logging level + bool: False - switch off logging + Text logging level for the message ("DEBUG", "INFO", + "WARNING", "ERROR", "CRITICAL") + mode: 'w' or 'a' + attach to logfile ('a' or start a new logfile ('w') + + """ + for hndl in logger.handlers: + if isinstance(hndl, logging.FileHandler): + has_file_handler = True + else: + has_file_handler = False + if level: + if not has_file_handler: + logname = 'headerlet.log' + fh = logging.FileHandler(logname, mode=mode) + fh.setFormatter(formatter) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + logger.info("%s: Starting %s with arguments:\n\t %s" % + (time.asctime(), funcname, kwargs)) + + +def with_logging(func): + @functools.wraps(func) + def wrapped(*args, **kw): + level = kw.get('logging', 100) + mode = kw.get('logmode', 'w') + func_args = kw.copy() + if sys.version_info[0] >= 3: + argnames = func.__code__.co_varnames + else: + argnames = func.func_code.co_varnames + + for argname, arg in zip(argnames, args): + func_args[argname] = arg + + init_logging(func.__name__, level, mode, **func_args) + return func(*args, **kw) + return wrapped + +#### Utility functions +def is_par_blank(par): + return par in ['', ' ', 'INDEF', "None", None] + +def parse_filename(fname, mode='readonly'): + """ + Interprets the input as either a filename of a file that needs to be opened + or a PyFITS object. + + Parameters + ---------- + fname : str, `astropy.io.fits.HDUList` + Input pointing to a file or `astropy.io.fits.HDUList` object. + An input filename (str) will be expanded as necessary to + interpret any environmental variables + included in the filename. + + mode : string + Specifies what mode to use when opening the file, if it needs + to open the file at all [Default: 'readonly'] + + Returns + ------- + fobj : `astropy.io.fits.HDUList` + FITS file handle for input + + fname : str + Name of input file + + close_fobj : bool + Flag specifying whether or not fobj needs to be closed since it was + opened by this function. This allows a program to know whether they + need to worry about closing the FITS object as opposed to letting + the higher level interface close the object. + + """ + close_fobj = False + if not isinstance(fname, list): + if sys.version_info[0] >= 3: + is_string = isinstance(fname, str) + else: + is_string = isinstance(fname, basestring) + if is_string: + fname = fu.osfn(fname) + fobj = fits.open(fname, mode=mode) + close_fobj = True + else: + fobj = fname + if hasattr(fobj, 'filename'): + fname = fobj.filename() + else: + fname = '' + return fobj, fname, close_fobj + +def get_headerlet_kw_names(fobj, kw='HDRNAME'): + """ + Returns a list of specified keywords from all HeaderletHDU + extensions in a science file. + + Parameters + ---------- + fobj : str, `astropy.io.fits.HDUList` + kw : str + Name of keyword to be read and reported + """ + + fobj, fname, open_fobj = parse_filename(fobj) + + hdrnames = [] + for ext in fobj: + if isinstance(ext, fits.hdu.base.NonstandardExtHDU): + hdrnames.append(ext.header[kw]) + + if open_fobj: + fobj.close() + + return hdrnames + +def get_header_kw_vals(hdr, kwname, kwval, default=0): + if kwval is None: + if kwname in hdr: + kwval = hdr[kwname] + else: + kwval = default + return kwval + +@with_logging +def find_headerlet_HDUs(fobj, hdrext=None, hdrname=None, distname=None, + strict=True, logging=False, logmode='w'): + """ + Returns all HeaderletHDU extensions in a science file that matches + the inputs specified by the user. If no hdrext, hdrname or distname are + specified, this function will return a list of all HeaderletHDU objects. + + Parameters + ---------- + fobj : str, `astropy.io.fits.HDUList` + Name of FITS file or open fits object (`astropy.io.fits.HDUList` instance) + hdrext : int, tuple or None + index number(EXTVER) or extension tuple of HeaderletHDU to be returned + hdrname : string + value of HDRNAME for HeaderletHDU to be returned + distname : string + value of DISTNAME for HeaderletHDUs to be returned + strict : bool [Default: True] + Specifies whether or not at least one parameter needs to be provided + If False, all extension indices returned if hdrext, hdrname and distname + are all None. If True and hdrext, hdrname, and distname are all None, + raise an Exception requiring one to be specified. + logging : boolean + enable logging to a file called headerlet.log + logmode : 'w' or 'a' + log file open mode + + Returns + ------- + hdrlets : list + A list of all matching HeaderletHDU extension indices (could be just one) + + """ + + get_all = False + if hdrext is None and hdrname is None and distname is None: + if not strict: + get_all = True + else: + mess = """\n + ===================================================== + No valid Headerlet extension specified. + Either "hdrname", "hdrext", or "distname" needs to be specified. + ===================================================== + """ + logger.critical(mess) + raise ValueError + + fobj, fname, open_fobj = parse_filename(fobj) + + hdrlets = [] + if hdrext is not None and isinstance(hdrext, int): + if hdrext in range(len(fobj)): # insure specified hdrext is in fobj + if isinstance(fobj[hdrext], fits.hdu.base.NonstandardExtHDU) and \ + fobj[hdrext].header['EXTNAME'] == 'HDRLET': + hdrlets.append(hdrext) + else: + for ext in fobj: + if isinstance(ext, fits.hdu.base.NonstandardExtHDU): + if get_all: + hdrlets.append(fobj.index(ext)) + else: + if hdrext is not None: + if isinstance(hdrext, tuple): + hdrextname = hdrext[0] + hdrextnum = hdrext[1] + else: + hdrextname = 'HDRLET' + hdrextnum = hdrext + hdrext_match = ((hdrext is not None) and + (hdrextnum == ext.header['EXTVER']) and + (hdrextname == ext.header['EXTNAME'])) + hdrname_match = ((hdrname is not None) and + (hdrname == ext.header['HDRNAME'])) + distname_match = ((distname is not None) and + (distname == ext.header['DISTNAME'])) + if hdrext_match or hdrname_match or distname_match: + hdrlets.append(fobj.index(ext)) + + if open_fobj: + fobj.close() + + if len(hdrlets) == 0: + if hdrname: + kwerr = 'hdrname' + kwval = hdrname + elif hdrext: + kwerr = 'hdrext' + kwval = hdrext + else: + kwerr = 'distname' + kwval = distname + message = """\n + ===================================================== + No valid Headerlet extension found!' + "%s" = %s not found in %s.' % (kwerr, kwval, fname) + ===================================================== + """ + logger.critical(message) + raise ValueError + + return hdrlets + +def verify_hdrname_is_unique(fobj, hdrname): + """ + Verifies that no other HeaderletHDU extension has the specified hdrname. + + Parameters + ---------- + fobj : str, `astropy.io.fits.HDUList` + Name of FITS file or open fits file object + hdrname : str + value of HDRNAME for HeaderletHDU to be compared as unique + + Returns + ------- + unique: bool + If True, no other HeaderletHDU has the specified HDRNAME value + """ + hdrnames_list = get_headerlet_kw_names(fobj) + unique = not(hdrname in hdrnames_list) + + return unique + +def update_versions(sourcehdr, desthdr): + """ + Update keywords which store version numbers + """ + phdukw = {'PYWCSVER': 'Version of PYWCS used to updated the WCS', + 'UPWCSVER': 'Version of STWCS used to updated the WCS' + } + for key in phdukw: + try: + desthdr[key] = (sourcehdr[key], sourcehdr.comments[key]) + except KeyError: + desthdr[key] = (" ", phdukw[key]) + +def update_ref_files(source, dest): + """ + Update the reference files name in the primary header of 'dest' + using values from 'source' + + Parameters + ---------- + source : `astropy.io.fits.Header` + dest : `astropy.io.fits.Header` + """ + logger.info("Updating reference files") + phdukw = {'NPOLFILE': True, + 'IDCTAB': True, + 'D2IMFILE': True, + 'SIPNAME': True, + 'DISTNAME': True} + + for key in phdukw: + try: + try: + del dest[key] + except: + pass + dest.set(key, source[key], source.comments[key]) + except KeyError: + phdukw[key] = False + return phdukw + +def print_summary(summary_cols, summary_dict, pad=2, maxwidth=None, idcol=None, + output=None, clobber=True, quiet=False ): + """ + Print out summary dictionary to STDOUT, and possibly an output file + + """ + nrows = None + if idcol: + nrows = len(idcol['vals']) + + # Find max width of each column + column_widths = {} + for kw in summary_dict: + colwidth = np.array(summary_dict[kw]['width']).max() + if maxwidth: + colwidth = min(colwidth, maxwidth) + column_widths[kw] = colwidth + pad + if nrows is None: + nrows = len(summary_dict[kw]['vals']) + + # print rows now + outstr = '' + # Start with column names + if idcol: + outstr += COLUMN_FMT.format(idcol['name'], width=idcol['width'] + pad) + for kw in summary_cols: + outstr += COLUMN_FMT.format(kw, width=column_widths[kw]) + outstr += '\n' + # Now, add a row for each headerlet + for row in range(nrows): + if idcol: + outstr += COLUMN_FMT.format(idcol['vals'][row], + width=idcol['width']+pad) + for kw in summary_cols: + val = summary_dict[kw]['vals'][row][:(column_widths[kw]-pad)] + outstr += COLUMN_FMT.format(val, width=column_widths[kw]) + outstr += '\n' + if not quiet: + print(outstr) + + # If specified, write info to separate text file + write_file = False + if output: + output = fu.osfn(output) # Expand any environment variables in filename + write_file = True + if os.path.exists(output): + if clobber: + os.remove(output) + else: + print('WARNING: Not writing results to file!') + print(' Output text file ', output, ' already exists.') + print(' Set "clobber" to True or move file before trying again.') + write_file = False + if write_file: + fout = open(output, mode='w') + fout.write(outstr) + fout.close() + +#### Private utility functions +def _create_primary_HDU(fobj, fname, wcsext, destim, hdrname, wcsname, + sipname, npolfile, d2imfile, + nmatch,catalog, wcskey, + author, descrip, history): + # convert input values into valid FITS kw values + if author is None: + author = '' + if descrip is None: + descrip = '' + + sipname, idctab = utils.build_sipname(fobj, fname, sipname) + logger.info("Setting sipname value to %s" % sipname) + + npolname, npolfile = utils.build_npolname(fobj, npolfile) + logger.info("Setting npolfile value to %s" % npolname) + + d2imname, d2imfile = utils.build_d2imname(fobj,d2imfile) + logger.info("Setting d2imfile value to %s" % d2imname) + + distname = utils.build_distname(sipname, npolname, d2imname) + logger.info("Setting distname to %s" % distname) + + # open file and parse comments + if history not in ['', ' ', None, 'INDEF'] and os.path.isfile(history): + f = open(fu.osfn(history)) + history = f.readlines() + f.close() + else: + history = '' + + rms_ra = fobj[wcsext].header.get("CRDER1"+wcskey, 0) + rms_dec = fobj[wcsext].header.get("CRDER2"+wcskey, 0) + if not nmatch: + nmatch = fobj[wcsext].header.get("NMATCH"+wcskey, 0) + if not catalog: + catalog = fobj[wcsext].header.get('CATALOG'+wcskey, "") + # get the version of STWCS used to create the WCS of the science file. + #try: + #upwcsver = fobj[0].header.cards[fobj[0].header.index('UPWCSVER')] + #except KeyError: + #upwcsver = pyfits.Card("UPWCSVER", " ", + #"Version of STWCS used to update the WCS") + #try: + #pywcsver = fobj[0].header.cards[fobj[0].header.index('PYWCSVER')] + #except KeyError: + #pywcsver = pyfits.Card("PYWCSVER", " ", + #"Version of PYWCS used to update the WCS") + upwcsver = fobj[0].header.get('UPWCSVER', "") + pywcsver = fobj[0].header.get('PYWCSVER', "") + # build Primary HDU + phdu = fits.PrimaryHDU() + phdu.header['DESTIM'] = (destim, 'Destination observation root name') + phdu.header['HDRNAME'] = (hdrname, 'Headerlet name') + fmt = "%Y-%m-%dT%H:%M:%S" + phdu.header['DATE'] = (time.strftime(fmt), 'Date FITS file was generated') + phdu.header['WCSNAME'] = (wcsname, 'WCS name') + phdu.header['DISTNAME'] = (distname, 'Distortion model name') + phdu.header['SIPNAME'] = (sipname, + 'origin of SIP polynomial distortion model') + phdu.header['NPOLFILE'] = (npolfile, + 'origin of non-polynmial distortion model') + phdu.header['D2IMFILE'] = (d2imfile, + 'origin of detector to image correction') + phdu.header['IDCTAB'] = (idctab, + 'origin of Polynomial Distortion') + phdu.header['AUTHOR'] = (author, 'headerlet created by this user') + phdu.header['DESCRIP'] = (descrip, + 'Short description of headerlet solution') + phdu.header['RMS_RA'] = (rms_ra, + 'RMS in RA at ref pix of headerlet solution') + phdu.header['RMS_DEC'] = (rms_dec, + 'RMS in Dec at ref pix of headerlet solution') + phdu.header['NMATCH'] = (nmatch, + 'Number of sources used for headerlet solution') + phdu.header['CATALOG'] = (catalog, + 'Astrometric catalog used for headerlet ' + 'solution') + phdu.header['UPWCSVER'] = (upwcsver, "Version of STWCS used to update the WCS") + phdu.header['PYWCSVER'] = (pywcsver, "Version of PYWCS used to update the WCS") + + # clean up history string in order to remove whitespace characters that + # would cause problems with FITS + if isinstance(history, list): + history_str = '' + for line in history: + history_str += line + else: + history_str = history + history_lines = textwrap.wrap(history_str, width=70) + for hline in history_lines: + phdu.header.add_history(hline) + + return phdu + + +#### Public Interface functions +@with_logging +def extract_headerlet(filename, output, extnum=None, hdrname=None, + clobber=False, logging=True): + """ + Finds a headerlet extension in a science file and writes it out as + a headerlet FITS file. + + If both hdrname and extnum are given they should match, if not + raise an Exception + + Parameters + ---------- + filename: string or HDUList or Python list + This specifies the name(s) of science file(s) from which headerlets + will be extracted. + + String input formats supported include use of wild-cards, IRAF-style + '@'-files (given as '@<filename>') and comma-separated list of names. + An input filename (str) will be expanded as necessary to interpret + any environmental variables included in the filename. + If a list of filenames has been specified, it will extract a + headerlet from the same extnum from all filenames. + output: string + Filename or just rootname of output headerlet FITS file + If string does not contain '.fits', it will create a filename with + '_hlet.fits' suffix + extnum: int + Extension number which contains the headerlet to be written out + hdrname: string + Unique name for headerlet, stored as the HDRNAME keyword + It stops if a value is not provided and no extnum has been specified + clobber: bool + If output file already exists, this parameter specifies whether or not + to overwrite that file [Default: False] + logging: boolean + enable logging to a file + + """ + + if isinstance(filename, fits.HDUList): + filename = [filename] + else: + filename, oname = parseinput.parseinput(filename) + + for f in filename: + fobj, fname, close_fobj = parse_filename(f) + frootname = fu.buildNewRootname(fname) + if hdrname in ['', ' ', None, 'INDEF'] and extnum is None: + if close_fobj: + fobj.close() + logger.critical("Expected a valid extnum or hdrname parameter") + raise ValueError + if hdrname is not None: + extn_from_hdrname = find_headerlet_HDUs(fobj, hdrname=hdrname)[0] + if extn_from_hdrname != extnum: + logger.critical("hdrname and extnmu should refer to the same FITS extension") + raise ValueError + else: + hdrhdu = fobj[extn_from_hdrname] + else: + hdrhdu = fobj[extnum] + + if not isinstance(hdrhdu, HeaderletHDU): + logger.critical("Specified extension is not a headerlet") + raise ValueError + + hdrlet = hdrhdu.headerlet + + if output is None: + output = frootname + + if '.fits' in output: + outname = output + else: + outname = '%s_hlet.fits' % output + + hdrlet.tofile(outname, clobber=clobber) + + if close_fobj: + fobj.close() + + +@with_logging +def write_headerlet(filename, hdrname, output=None, sciext='SCI', + wcsname=None, wcskey=None, destim=None, + sipname=None, npolfile=None, d2imfile=None, + author=None, descrip=None, history=None, + nmatch=None, catalog=None, + attach=True, clobber=False, logging=False): + + """ + Save a WCS as a headerlet FITS file. + + This function will create a headerlet, write out the headerlet to a + separate headerlet file, then, optionally, attach it as an extension + to the science image (if it has not already been archived) + + Either wcsname or wcskey must be provided; if both are given, they must + match a valid WCS. + + Updates wcscorr if necessary. + + Parameters + ---------- + filename: string or HDUList or Python list + This specifies the name(s) of science file(s) from which headerlets + will be created and written out. + String input formats supported include use of wild-cards, IRAF-style + '@'-files (given as '@<filename>') and comma-separated list of names. + An input filename (str) will be expanded as necessary to interpret + any environmental variables included in the filename. + hdrname: string + Unique name for this headerlet, stored as HDRNAME keyword + output: string or None + Filename or just rootname of output headerlet FITS file + If string does not contain '.fits', it will create a filename + starting with the science filename and ending with '_hlet.fits'. + If None, a default filename based on the input filename will be + generated for the headerlet FITS filename + sciext: string + name (EXTNAME) of extension that contains WCS to be saved + wcsname: string + name of WCS to be archived, if " ": stop + wcskey: one of A...Z or " " or "PRIMARY" + if " " or "PRIMARY" - archive the primary WCS + destim: string + DESTIM keyword + if NOne, use ROOTNAME or science file name + sipname: string or None (default) + Name of unique file where the polynomial distortion coefficients were + read from. If None, the behavior is: + The code looks for a keyword 'SIPNAME' in the science header + If not found, for HST it defaults to 'IDCTAB' + If there is no SIP model the value is 'NOMODEL' + If there is a SIP model but no SIPNAME, it is set to 'UNKNOWN' + npolfile: string or None (default) + Name of a unique file where the non-polynomial distortion was stored. + If None: + The code looks for 'NPOLFILE' in science header. + If 'NPOLFILE' was not found and there is no npol model, it is set to 'NOMODEL' + If npol model exists, it is set to 'UNKNOWN' + d2imfile: string + Name of a unique file where the detector to image correction was + stored. If None: + The code looks for 'D2IMFILE' in the science header. + If 'D2IMFILE' is not found and there is no d2im correction, + it is set to 'NOMODEL' + If d2im correction exists, but 'D2IMFILE' is missing from science + header, it is set to 'UNKNOWN' + author: string + Name of user who created the headerlet, added as 'AUTHOR' keyword + to headerlet PRIMARY header + descrip: string + Short description of the solution provided by the headerlet + This description will be added as the single 'DESCRIP' keyword + to the headerlet PRIMARY header + history: filename, string or list of strings + Long (possibly multi-line) description of the solution provided + by the headerlet. These comments will be added as 'HISTORY' cards + to the headerlet PRIMARY header + If filename is specified, it will format and attach all text from + that file as the history. + attach: bool + Specify whether or not to attach this headerlet as a new extension + It will verify that no other headerlet extension has been created with + the same 'hdrname' value. + clobber: bool + If output file already exists, this parameter specifies whether or not + to overwrite that file [Default: False] + logging: boolean + enable file logging + """ + + if isinstance(filename, fits.HDUList): + filename = [filename] + else: + filename, oname = parseinput.parseinput(filename) + + for f in filename: + if isinstance(f, str): + fname = f + else: + fname = f.filename() + + if wcsname in [None, ' ', '', 'INDEF'] and wcskey is None: + message = """\n + No valid WCS found found in %s. + A valid value for either "wcsname" or "wcskey" + needs to be specified. + """ % fname + logger.critical(message) + raise ValueError + + # Translate 'wcskey' value for PRIMARY WCS to valid altwcs value of ' ' + if wcskey == 'PRIMARY': + wcskey = ' ' + + if attach: + umode = 'update' + else: + umode = 'readonly' + + fobj, fname, close_fobj = parse_filename(f, mode=umode) + + # Interpret sciext input for this file + if isinstance(sciext, int): + sciextlist = [sciext] # allow for specification of simple FITS header + elif isinstance(sciext, str): + numsciext = countExtn(fobj, sciext) + if numsciext > 0: + sciextlist = [tuple((sciext,i)) for i in range(1, numsciext+1)] + else: + sciextlist = [0] + elif isinstance(sciext, list): + sciextlist = sciext + else: + errstr = "Expected sciext to be a list of FITS extensions with science data\n"+\ + " a valid EXTNAME string, or an integer." + logger.critical(errstr) + raise ValueError + + wnames = altwcs.wcsnames(fobj,ext=sciextlist[0]) + + # Insure that WCSCORR table has been created with all original + # WCS's recorded prior to adding the headerlet WCS + wcscorr.init_wcscorr(fobj) + + if wcsname is None: + scihdr = fobj[sciextlist[0]].header + wname = scihdr['wcsname'+wcskey] + else: + wname = wcsname + if hdrname in [None, ' ', '']: + hdrname = wcsname + + logger.critical('Creating the headerlet from image %s' % fname) + hdrletobj = create_headerlet(fobj, sciext=sciextlist, + wcsname=wname, wcskey=wcskey, + hdrname=hdrname, + sipname=sipname, npolfile=npolfile, + d2imfile=d2imfile, author=author, + descrip=descrip, history=history, + nmatch=nmatch, catalog=catalog, + logging=False) + + if attach: + # Check to see whether or not a HeaderletHDU with + #this hdrname already exists + hdrnames = get_headerlet_kw_names(fobj) + if hdrname not in hdrnames: + hdrlet_hdu = HeaderletHDU.fromheaderlet(hdrletobj) + + if destim is not None: + hdrlet_hdu.header['destim'] = destim + + fobj.append(hdrlet_hdu) + + # Update the WCSCORR table with new rows from the headerlet's WCSs + wcscorr.update_wcscorr(fobj, source=hdrletobj, + extname='SIPWCS', wcs_id=wname) + + utils.updateNEXTENDKw(fobj) + fobj.flush() + else: + message = """ + Headerlet with hdrname %s already archived for WCS %s. + No new headerlet appended to %s. + """ % (hdrname, wname, fname) + logger.critical(message) + + if close_fobj: + logger.info('Closing image in write_headerlet()...') + fobj.close() + + frootname = fu.buildNewRootname(fname) + + if output is None: + # Generate default filename for headerlet FITS file + outname = '{0}_hlet.fits'.format(frootname) + else: + outname = output + + if not outname.endswith('.fits'): + outname = '{0}_{1}_hlet.fits'.format(frootname,outname) + + # If user specifies an output filename for headerlet, write it out + hdrletobj.tofile(outname, clobber=clobber) + logger.critical( 'Created Headerlet file %s ' % outname) + + del hdrletobj + +@with_logging +def create_headerlet(filename, sciext='SCI', hdrname=None, destim=None, + wcskey=" ", wcsname=None, + sipname=None, npolfile=None, d2imfile=None, + author=None, descrip=None, history=None, + nmatch=None, catalog=None, + logging=False, logmode='w'): + """ + Create a headerlet from a WCS in a science file + If both wcskey and wcsname are given they should match, if not + raise an Exception + + Parameters + ---------- + filename: string or HDUList + Either a filename or PyFITS HDUList object for the input science file + An input filename (str) will be expanded as necessary to interpret + any environmental variables included in the filename. + sciext: string or python list (default: 'SCI') + Extension in which the science data with the linear WCS is. + The headerlet will be created from these extensions. + If string - a valid EXTNAME is expected + If int - specifies an extension with a valid WCS, such as 0 for a + simple FITS file + If list - a list of FITS extension numbers or strings representing + extension tuples, e.g. ('SCI, 1') is expected. + hdrname: string + value of HDRNAME keyword + Takes the value from the HDRNAME<wcskey> keyword, if not available from WCSNAME<wcskey> + It stops if neither is found in the science file and a value is not provided + destim: string or None + name of file this headerlet can be applied to + if None, use ROOTNAME keyword + wcskey: char (A...Z) or " " or "PRIMARY" or None + a char representing an alternate WCS to be used for the headerlet + if " ", use the primary (default) + if None use wcsname + wcsname: string or None + if wcskey is None use wcsname specified here to choose an alternate WCS for the headerlet + sipname: string or None (default) + Name of unique file where the polynomial distortion coefficients were + read from. If None, the behavior is: + The code looks for a keyword 'SIPNAME' in the science header + If not found, for HST it defaults to 'IDCTAB' + If there is no SIP model the value is 'NOMODEL' + If there is a SIP model but no SIPNAME, it is set to 'UNKNOWN' + npolfile: string or None (default) + Name of a unique file where the non-polynomial distortion was stored. + If None: + The code looks for 'NPOLFILE' in science header. + If 'NPOLFILE' was not found and there is no npol model, it is set to 'NOMODEL' + If npol model exists, it is set to 'UNKNOWN' + d2imfile: string + Name of a unique file where the detector to image correction was + If None: + The code looks for 'D2IMFILE' in the science header. + If 'D2IMFILE' is not found and there is no d2im correction, + it is set to 'NOMODEL' + If d2im correction exists, but 'D2IMFILE' is missing from science + header, it is set to 'UNKNOWN' + author: string + Name of user who created the headerlet, added as 'AUTHOR' keyword + to headerlet PRIMARY header + descrip: string + Short description of the solution provided by the headerlet + This description will be added as the single 'DESCRIP' keyword + to the headerlet PRIMARY header + history: filename, string or list of strings + Long (possibly multi-line) description of the solution provided + by the headerlet. These comments will be added as 'HISTORY' cards + to the headerlet PRIMARY header + If filename is specified, it will format and attach all text from + that file as the history. + nmatch: int (optional) + Number of sources used in the new solution fit + catalog: string (optional) + Astrometric catalog used for headerlet solution + logging: boolean + enable file logging + logmode: 'w' or 'a' + log file open mode + + Returns + ------- + Headerlet object + + """ + if wcskey == 'O': + message = "Warning: 'O' is a reserved key for the original WCS. Quitting..." + logger.info(message) + return None + + fobj, fname, close_file = parse_filename(filename) + # based on `sciext` create a list of (EXTNAME, EXTVER) tuples + # of extensions with WCS to be saved in a headerlet + sciext = get_extname_extver_list(fobj, sciext) + logger.debug("Data extensions from which to create headerlet:\n\t %s" + % (str(sciext))) + if not sciext: + logger.critical("No valid target extensions found in file %s" % fname) + raise ValueError + + # Define extension to evaluate for verification of input parameters + wcsext = sciext[0] + logger.debug("sciext in create_headerlet is %s" % str(sciext)) + # Translate 'wcskey' value for PRIMARY WCS to valid altwcs value of ' ' + if wcskey == 'PRIMARY': + wcskey = ' ' + logger.info("wcskey reset from 'PRIMARY' to ' '") + wcskey = wcskey.upper() + wcsnamekw = "".join(["WCSNAME", wcskey.upper()]).rstrip() + hdrnamekw = "".join(["HDRNAME", wcskey.upper()]).rstrip() + + wnames = altwcs.wcsnames(fobj, ext=wcsext) + if not wcsname: + # User did not specify a value for 'wcsname' + if wcsnamekw in fobj[wcsext].header: + #check if there's a WCSNAME for this wcskey in the header + wcsname = fobj[wcsext].header[wcsnamekw] + logger.info("Setting wcsname from header[%s] to %s" % (wcsnamekw, wcsname)) + else: + if hdrname not in ['', ' ', None, "INDEF"]: + """ + If wcsname for this wcskey was not provided + and WCSNAME<wcskey> does not exist in the header + and hdrname is provided, then + use hdrname as WCSNAME for the headerlet. + """ + wcsname = hdrname + logger.debug("Setting wcsname from hdrname to %s" % hdrname) + else: + if hdrnamekw in fobj[wcsext].header: + wcsname = fobj[wcsext].header[hdrnamekw] + logger.debug("Setting wcsname from header[%s] to %s" % (hdrnamekw, wcsname)) + else: + message = """ + Required keywords 'HDRNAME' or 'WCSNAME' not found! + Please specify a value for parameter 'hdrname' + or update header with 'WCSNAME' keyword. + """ + logger.critical(message) + raise KeyError + else: + # Verify that 'wcsname' and 'wcskey' values specified by user reference + # the same WCS + wname = fobj[wcsext].header[wcsnamekw] + if wcsname != wname: + message = "\tInconsistent values for 'wcskey' and 'wcsname' specified!\n" + message += " 'wcskey' = %s and 'wcsname' = %s. \n" % (wcskey, wcsname) + message += "Actual value of %s found to be %s. \n" % (wcsnamekw, wname) + logger.critical(message) + raise KeyError + wkeys = altwcs.wcskeys(fobj, ext=wcsext) + if wcskey != ' ': + if wcskey not in wkeys: + logger.critical('No WCS with wcskey=%s found in extension %s. Skipping...' % (wcskey, str(wcsext))) + raise ValueError("No WCS with wcskey=%s found in extension %s. Skipping...' % (wcskey, str(wcsext))") + + # get remaining required keywords + if destim is None: + if 'ROOTNAME' in fobj[0].header: + destim = fobj[0].header['ROOTNAME'] + logger.info("Setting destim to rootname of the science file") + else: + destim = fname + logger.info('DESTIM not provided') + logger.info('Keyword "ROOTNAME" not found') + logger.info('Using file name as DESTIM') + + if not hdrname: + # check if HDRNAME<wcskey> is in header + if hdrnamekw in fobj[wcsext].header: + hdrname = fobj[wcsext].header[hdrnamekw] + else: + if wcsnamekw in fobj[wcsext].header: + hdrname = fobj[wcsext].header[wcsnamekw] + message = """ + Using default value for HDRNAME of "%s" derived from %s. + """ % (hdrname, wcsnamekw) + logger.info(message) + logger.info("Setting hdrname to %s from header[%s]" + % (hdrname, wcsnamekw)) + else: + message = "Required keywords 'HDRNAME' or 'WCSNAME' not found" + logger.critical(message) + raise KeyError + + + + hdul = [] + phdu = _create_primary_HDU(fobj, fname, wcsext, destim, hdrname, wcsname, + sipname, npolfile, d2imfile, + nmatch, catalog, wcskey, + author, descrip, history) + hdul.append(phdu) + wcsdvarr_extns = [] + """ + nd2i is a counter for d2i extensions to be used when the science file + has an old d2i correction format. The old format did not write EXTVER + kw for the d2i correction in the science header bu tthe new format expects + them. + """ + nd2i_extver = 1 + for ext in sciext: + wkeys = altwcs.wcskeys(fobj, ext=ext) + if wcskey != ' ': + if wcskey not in wkeys: + logger.debug( + 'No WCS with wcskey=%s found in extension %s. ' + 'Skipping...' % (wcskey, str(ext))) + raise ValueError("") + + hwcs = HSTWCS(fobj, ext=ext, wcskey=wcskey) + + whdul = hwcs.to_fits(relax=True, key=" ") + if hasattr(hwcs, 'orientat'): + orient_comment = "positions angle of image y axis (deg. e of n)" + whdul[0].header['ORIENTAT'] = (hwcs.orientat, orient_comment) + + whdul[0].header.append(('TG_ENAME', ext[0], 'Target science data extname')) + whdul[0].header.append(('TG_EVER', ext[1], 'Target science data extver')) + + if hwcs.wcs.has_cd(): + whdul[0].header = altwcs.pc2cd(whdul[0].header) + + idckw = hwcs._idc2hdr() + whdul[0].header.extend(idckw) + + if hwcs.det2im1 or hwcs.det2im2: + try: + whdul[0].header.append(fobj[ext].header.cards['D2IMEXT']) + except KeyError: + pass + whdul[0].header.extend(fobj[ext].header.cards['D2IMERR*']) + if 'D2IM1.EXTVER' in whdul[0].header: + try: + whdul[0].header['D2IM1.EXTVER'] = fobj[ext].header['D2IM1.EXTVER'] + except KeyError: + whdul[0].header['D2IM1.EXTVER'] = nd2i_extver + nd2i_extver += 1 + if 'D2IM2.EXTVER' in whdul[0].header: + try: + whdul[0].header['D2IM2.EXTVER'] = fobj[ext].header['D2IM2.EXTVER'] + except KeyError: + whdul[0].header['D2IM2.EXTVER'] = nd2i_extver + nd2i_extver += 1 + + if hwcs.cpdis1 or hwcs.cpdis2: + whdul[0].header.extend(fobj[ext].header.cards['CPERR*']) + try: + whdul[0].header.append(fobj[ext].header.cards['NPOLEXT']) + except KeyError: + pass + if 'DP1.EXTVER' in whdul[0].header: + whdul[0].header['DP1.EXTVER'] = fobj[ext].header['DP1.EXTVER'] + if 'DP2.EXTVER' in whdul[0].header: + whdul[0].header['DP2.EXTVER'] = fobj[ext].header['DP2.EXTVER'] + ihdu = fits.ImageHDU(header=whdul[0].header, name='SIPWCS') + + if ext[0] != "PRIMARY": + ihdu.update_ext_version(fobj[ext].header['EXTVER'], comment='Extension version') + + hdul.append(ihdu) + + if hwcs.cpdis1: + whdu = whdul[('WCSDVARR', 1)].copy() + whdu.update_ext_version(fobj[ext].header['DP1.EXTVER']) + hdul.append(whdu) + if hwcs.cpdis2: + whdu = whdul[('WCSDVARR', 2)].copy() + whdu.update_ext_version(fobj[ext].header['DP2.EXTVER']) + hdul.append(whdu) + + if hwcs.det2im1: + whdu = whdul[('D2IMARR', 1)].copy() + whdu.update_ext_version(ihdu.header['D2IM1.EXTVER']) + hdul.append(whdu) + if hwcs.det2im2: + whdu = whdul[('D2IMARR', 2)].copy() + whdu.update_ext_version(ihdu.header['D2IM2.EXTVER']) + hdul.append(whdu) + + + #if hwcs.det2im1 or hwcs.det2im2: + #try: + #darr = hdul[('D2IMARR', 1)] + #except KeyError: + #whdu = whdul[('D2IMARR')] + #whdu.update_ext_version(1) + #hdul.append(whdu) + if close_file: + fobj.close() + + hlet = Headerlet(hdul, logging=logging, logmode='a') + hlet.init_attrs() + return hlet + +@with_logging +def apply_headerlet_as_primary(filename, hdrlet, attach=True, archive=True, + force=False, logging=False, logmode='a'): + """ + Apply headerlet 'hdrfile' to a science observation 'destfile' as the primary WCS + + Parameters + ---------- + filename: string or list of strings + File name(s) of science observation whose WCS solution will be updated + hdrlet: string or list of strings + Headerlet file(s), must match 1-to-1 with input filename(s) + attach: boolean + True (default): append headerlet to FITS file as a new extension. + archive: boolean + True (default): before updating, create a headerlet with the + WCS old solution. + force: boolean + If True, this will cause the headerlet to replace the current PRIMARY + WCS even if it has a different distortion model. [Default: False] + logging: boolean + enable file logging + logmode: 'w' or 'a' + log file open mode + """ + if not isinstance(filename, list): + filename = [filename] + if not isinstance(hdrlet, list): + hdrlet = [hdrlet] + if len(hdrlet) != len(filename): + logger.critical("Filenames must have matching headerlets. " + "{0:d} filenames and {1:d} headerlets specified".format(len(filename),len(hdrlet))) + + for fname,h in zip(filename,hdrlet): + print("Applying {0} as Primary WCS to {1}".format(h,fname)) + hlet = Headerlet.fromfile(h, logging=logging, logmode=logmode) + hlet.apply_as_primary(fname, attach=attach, archive=archive, + force=force) + + +@with_logging +def apply_headerlet_as_alternate(filename, hdrlet, attach=True, wcskey=None, + wcsname=None, logging=False, logmode='w'): + """ + Apply headerlet to a science observation as an alternate WCS + + Parameters + ---------- + filename: string or list of strings + File name(s) of science observation whose WCS solution will be updated + hdrlet: string or list of strings + Headerlet file(s), must match 1-to-1 with input filename(s) + attach: boolean + flag indicating if the headerlet should be attached as a + HeaderletHDU to fobj. If True checks that HDRNAME is unique + in the fobj and stops if not. + wcskey: string + Key value (A-Z, except O) for this alternate WCS + If None, the next available key will be used + wcsname: string + Name to be assigned to this alternate WCS + WCSNAME is a required keyword in a Headerlet but this allows the + user to change it as desired. + logging: boolean + enable file logging + logmode: 'a' or 'w' + """ + if not isinstance(filename, list): + filename = [filename] + if not isinstance(hdrlet, list): + hdrlet = [hdrlet] + if len(hdrlet) != len(filename): + logger.critical("Filenames must have matching headerlets. " + "{0:d} filenames and {1:d} headerlets specified".format(len(filename),len(hdrlet))) + + for fname,h in zip(filename,hdrlet): + print('Applying {0} as an alternate WCS to {1}'.format(h,fname)) + hlet = Headerlet.fromfile(h, logging=logging, logmode=logmode) + hlet.apply_as_alternate(fname, attach=attach, + wcsname=wcsname, wcskey=wcskey) + + +@with_logging +def attach_headerlet(filename, hdrlet, logging=False, logmode='a'): + """ + Attach Headerlet as an HeaderletHDU to a science file + + Parameters + ---------- + filename: HDUList or list of HDULists + science file(s) to which the headerlet should be applied + hdrlet: string, Headerlet object or list of strings or Headerlet objects + string representing a headerlet file(s), must match 1-to-1 input filename(s) + logging: boolean + enable file logging + logmode: 'a' or 'w' + """ + if not isinstance(filename, list): + filename = [filename] + if not isinstance(hdrlet, list): + hdrlet = [hdrlet] + if len(hdrlet) != len(filename): + logger.critical("Filenames must have matching headerlets. " + "{0:d} filenames and {1:d} headerlets specified".format(len(filename),len(hdrlet))) + + for fname,h in zip(filename,hdrlet): + print('Attaching {0} as Headerlet extension to {1}'.format(h,fname)) + hlet = Headerlet.fromfile(h, logging=logging, logmode=logmode) + hlet.attach_to_file(fname,archive=True) + + +@with_logging +def delete_headerlet(filename, hdrname=None, hdrext=None, distname=None, + logging=False, logmode='w'): + """ + Deletes HeaderletHDU(s) with same HDRNAME from science files + + Notes + ----- + One of hdrname, hdrext or distname should be given. + If hdrname is given - delete a HeaderletHDU with a name HDRNAME from fobj. + If hdrext is given - delete HeaderletHDU in extension. + If distname is given - deletes all HeaderletHDUs with a specific distortion model from fobj. + Updates wcscorr + + Parameters + ---------- + filename: string, HDUList or list of strings + Filename can be specified as a single filename or HDUList, or + a list of filenames + Each input filename (str) will be expanded as necessary to interpret + any environmental variables included in the filename. + hdrname: string or None + HeaderletHDU primary header keyword HDRNAME + hdrext: int, tuple or None + HeaderletHDU FITS extension number + tuple has the form ('HDRLET', 1) + distname: string or None + distortion model as specified in the DISTNAME keyword + logging: boolean + enable file logging + logmode: 'a' or 'w' + """ + if not isinstance(filename, list): + filename = [filename] + + for f in filename: + print("Deleting Headerlet from ",f) + _delete_single_headerlet(f, hdrname=hdrname, hdrext=hdrext, + distname=distname, logging=logging, logmode='a') + +def _delete_single_headerlet(filename, hdrname=None, hdrext=None, distname=None, + logging=False, logmode='w'): + """ + Deletes HeaderletHDU(s) from a SINGLE science file + + Notes + ----- + One of hdrname, hdrext or distname should be given. + If hdrname is given - delete a HeaderletHDU with a name HDRNAME from fobj. + If hdrext is given - delete HeaderletHDU in extension. + If distname is given - deletes all HeaderletHDUs with a specific distortion model from fobj. + Updates wcscorr + + Parameters + ---------- + filename: string or HDUList + Either a filename or PyFITS HDUList object for the input science file + An input filename (str) will be expanded as necessary to interpret + any environmental variables included in the filename. + hdrname: string or None + HeaderletHDU primary header keyword HDRNAME + hdrext: int, tuple or None + HeaderletHDU FITS extension number + tuple has the form ('HDRLET', 1) + distname: string or None + distortion model as specified in the DISTNAME keyword + logging: boolean + enable file logging + logmode: 'a' or 'w' + """ + hdrlet_ind = find_headerlet_HDUs(filename, hdrname=hdrname, hdrext=hdrext, + distname=distname, logging=logging, logmode='a') + if len(hdrlet_ind) == 0: + message = """ + No HDUs deleted... No Headerlet HDUs found with ' + hdrname = %s + hdrext = %s + distname = %s + Please review input parameters and try again. + """ % (hdrname, str(hdrext), distname) + logger.critical(message) + return + + fobj, fname, close_fobj = parse_filename(filename, mode='update') + + # delete row(s) from WCSCORR table now... + # + # + if hdrname not in ['', ' ', None, 'INDEF']: + selections = {'hdrname': hdrname} + elif hdrname in ['', ' ', None, 'INDEF'] and hdrext is not None: + selections = {'hdrname': fobj[hdrext].header['hdrname']} + else: + selections = {'distname': distname} + wcscorr.delete_wcscorr_row(fobj['WCSCORR'].data, selections) + + # delete the headerlet extension now + for hdrind in hdrlet_ind: + del fobj[hdrind] + + utils.updateNEXTENDKw(fobj) + # Update file object with changes + fobj.flush() + # close file, if was opened by this function + if close_fobj: + fobj.close() + logger.critical('Deleted headerlet from extension(s) %s ' % str(hdrlet_ind)) + + +def headerlet_summary(filename, columns=None, pad=2, maxwidth=None, + output=None, clobber=True, quiet=False): + """ + + Print a summary of all HeaderletHDUs in a science file to STDOUT, and + optionally to a text file + The summary includes: + HDRLET_ext_number HDRNAME WCSNAME DISTNAME SIPNAME NPOLFILE D2IMFILE + + Parameters + ---------- + filename: string or HDUList + Either a filename or PyFITS HDUList object for the input science file + An input filename (str) will be expanded as necessary to interpret + any environmental variables included in the filename. + columns: list + List of headerlet PRIMARY header keywords to report in summary + By default (set to None), it will use the default set of keywords + defined as the global list DEFAULT_SUMMARY_COLS + pad: int + Number of padding spaces to put between printed columns + [Default: 2] + maxwidth: int + Maximum column width(not counting padding) for any column in summary + By default (set to None), each column's full width will be used + output: string (optional) + Name of optional output file to record summary. This filename + can contain environment variables. + [Default: None] + clobber: bool + If True, will overwrite any previous output file of same name + quiet: bool + If True, will NOT report info to STDOUT + + """ + if columns is None: + summary_cols = DEFAULT_SUMMARY_COLS + else: + summary_cols = columns + + summary_dict = {} + for kw in summary_cols: + summary_dict[kw] = copy.deepcopy(COLUMN_DICT) + + # Define Extension number column + extnums_col = copy.deepcopy(COLUMN_DICT) + extnums_col['name'] = 'EXTN' + extnums_col['width'] = 6 + + fobj, fname, close_fobj = parse_filename(filename) + # find all HDRLET extensions and combine info into a single summary + for extn in fobj: + if 'extname' in extn.header and extn.header['extname'] == 'HDRLET': + hdrlet_indx = fobj.index_of(('hdrlet', extn.header['extver'])) + try: + ext_cols, ext_summary = extn.headerlet.summary(columns=summary_cols) + extnums_col['vals'].append(hdrlet_indx) + for kw in summary_cols: + for key in COLUMN_DICT: + summary_dict[kw][key].extend(ext_summary[kw][key]) + except: + print("Skipping headerlet") + print("Could not read Headerlet from extension ", hdrlet_indx) + + if close_fobj: + fobj.close() + + # Print out the summary dictionary + print_summary(summary_cols, summary_dict, pad=pad, maxwidth=maxwidth, + idcol=extnums_col, output=output, + clobber=clobber, quiet=quiet) + + +@with_logging +def restore_from_headerlet(filename, hdrname=None, hdrext=None, archive=True, + force=False, logging=False, logmode='w'): + """ + Restores a headerlet as a primary WCS + + Parameters + ---------- + filename: string or HDUList + Either a filename or PyFITS HDUList object for the input science file + An input filename (str) will be expanded as necessary to interpret + any environmental variables included in the filename. + hdrname: string + HDRNAME keyword of HeaderletHDU + hdrext: int or tuple + Headerlet extension number of tuple ('HDRLET',2) + archive: boolean (default: True) + When the distortion model in the headerlet is the same as the distortion model of + the science file, this flag indicates if the primary WCS should be saved as an alternate + nd a headerlet extension. + When the distortion models do not match this flag indicates if the current primary and + alternate WCSs should be archived as headerlet extensions and alternate WCS. + force: boolean (default:False) + When the distortion models of the headerlet and the primary do not match, and archive + is False, this flag forces an update of the primary. + logging: boolean + enable file logging + logmode: 'a' or 'w' + """ + + hdrlet_ind = find_headerlet_HDUs(filename, hdrext=hdrext, hdrname=hdrname) + + fobj, fname, close_fobj = parse_filename(filename, mode='update') + + if len(hdrlet_ind) > 1: + if hdrext: + kwerr = 'hdrext' + kwval = hdrext + else: + kwerr = 'hdrname' + kwval = hdrname + message = """ + Multiple Headerlet extensions found with the same name. + %d Headerlets with "%s" = %s found in %s. + """% (len(hdrlet_ind), kwerr, kwval, fname) + if close_fobj: + fobj.close() + logger.critical(message) + raise ValueError + + hdrlet_indx = hdrlet_ind[0] + + # read headerlet from HeaderletHDU into memory + if hasattr(fobj[hdrlet_ind[0]], 'hdulist'): + hdrlet = fobj[hdrlet_indx].hdulist + else: + hdrlet = fobj[hdrlet_indx].headerlet # older convention in PyFITS + + # read in the names of the extensions which HeaderletHDU updates + extlist = [] + for ext in hdrlet: + if 'extname' in ext.header and ext.header['extname'] == 'SIPWCS': + # convert from string to tuple or int + sciext = eval(ext.header['sciext']) + extlist.append(fobj[sciext]) + # determine whether distortion is the same + current_distname = hdrlet[0].header['distname'] + same_dist = True + if current_distname != fobj[0].header['distname']: + same_dist = False + if not archive and not force: + if close_fobj: + fobj.close() + message = """ + Headerlet does not have the same distortion as image! + Set "archive"=True to save old distortion model, or + set "force"=True to overwrite old model with new. + """ + logger.critical(message) + raise ValueError + + # check whether primary WCS has been archived already + # Use information from first 'SCI' extension + priwcs_name = None + + scihdr = extlist[0].header + sci_wcsnames = altwcs.wcsnames(scihdr).values() + if 'hdrname' in scihdr: + priwcs_hdrname = scihdr['hdrname'] + else: + if 'wcsname' in scihdr: + priwcs_hdrname = priwcs_name = scihdr['wcsname'] + else: + if 'idctab' in scihdr: + priwcs_hdrname = ''.join(['IDC_', + utils.extract_rootname(scihdr['idctab'], suffix='_idc')]) + else: + priwcs_hdrname = 'UNKNOWN' + priwcs_name = priwcs_hdrname + scihdr['WCSNAME'] = priwcs_name + + priwcs_unique = verify_hdrname_is_unique(fobj, priwcs_hdrname) + if archive and priwcs_unique: + if priwcs_unique: + newhdrlet = create_headerlet(fobj, sciext=scihdr['extname'], + hdrname=priwcs_hdrname) + newhdrlet.attach_to_file(fobj) + # + # copy hdrlet as a primary + # + hdrlet.apply_as_primary(fobj, attach=False, archive=archive, force=force) + + utils.updateNEXTENDKw(fobj) + fobj.flush() + if close_fobj: + fobj.close() + + +@with_logging +def restore_all_with_distname(filename, distname, primary, archive=True, + sciext='SCI', logging=False, logmode='w'): + """ + Restores all HeaderletHDUs with a given distortion model as alternate WCSs and a primary + + Parameters + -------------- + filename: string or HDUList + Either a filename or PyFITS HDUList object for the input science file + An input filename (str) will be expanded as necessary to interpret + any environmental variables included in the filename. + distname: string + distortion model as represented by a DISTNAME keyword + primary: int or string or None + HeaderletHDU to be restored as primary + if int - a fits extension + if string - HDRNAME + if None - use first HeaderletHDU + archive: boolean (default True) + flag indicating if HeaderletHDUs should be created from the + primary and alternate WCSs in fname before restoring all matching + headerlet extensions + logging: boolean + enable file logging + logmode: 'a' or 'w' + """ + + fobj, fname, close_fobj = parse_filename(filename, mode='update') + + hdrlet_ind = find_headerlet_HDUs(fobj, distname=distname) + if len(hdrlet_ind) == 0: + message = """ + No Headerlet extensions found with + + DISTNAME = %s in %s. + + For a full list of DISTNAMEs found in all headerlet extensions: + + get_headerlet_kw_names(fobj, kw='DISTNAME') + """ % (distname, fname) + if close_fobj: + fobj.close() + logger.critical(message) + raise ValueError + + # Interpret 'primary' parameter input into extension number + if primary is None: + primary_ind = hdrlet_ind[0] + elif isinstance(primary, int): + primary_ind = primary + else: + primary_ind = None + for ind in hdrlet_ind: + if fobj[ind].header['hdrname'] == primary: + primary_ind = ind + break + if primary_ind is None: + if close_fobj: + fobj.close() + message = """ + No Headerlet extensions found with DISTNAME = %s in %s. + """ % (primary, fname) + logger.critical(message) + raise ValueError + # Check to see whether 'primary' HeaderletHDU has same distname as user + # specified on input + + # read headerlet from HeaderletHDU into memory + if hasattr(fobj[primary_ind], 'hdulist'): + primary_hdrlet = fobj[primary_ind].hdulist + else: + primary_hdrlet = fobj[primary_ind].headerlet # older convention in PyFITS + pri_distname = primary_hdrlet[0].header['distname'] + if pri_distname != distname: + if close_fobj: + fobj.close() + message = """ + Headerlet extension to be used as PRIMARY WCS + has "DISTNAME" = %s + "DISTNAME" = %s was specified on input. + All updated WCSs must have same DISTNAME. Quitting...' + """ % (pri_distname, distname) + logger.critical(message) + raise ValueError + + # read in the names of the WCSs which the HeaderletHDUs will update + wnames = altwcs.wcsnames(fobj[sciext, 1].header) + + # work out how many HeaderletHDUs will be used to update the WCSs + numhlt = len(hdrlet_ind) + hdrnames = get_headerlet_kw_names(fobj, kw='wcsname') + + # read in headerletHDUs and update WCS keywords + for hlet in hdrlet_ind: + if fobj[hlet].header['distname'] == distname: + if hasattr(fobj[hlet], 'hdulist'): + hdrlet = fobj[hlet].hdulist + else: + hdrlet = fobj[hlet].headerlet # older convention in PyFITS + if hlet == primary_ind: + hdrlet.apply_as_primary(fobj, attach=False, + archive=archive, force=True) + else: + hdrlet.apply_as_alternate(fobj, attach=False, + wcsname=hdrlet[0].header['wcsname']) + + utils.updateNEXTENDKw(fobj) + fobj.flush() + if close_fobj: + fobj.close() + + +@with_logging +def archive_as_headerlet(filename, hdrname, sciext='SCI', + wcsname=None, wcskey=None, destim=None, + sipname=None, npolfile=None, d2imfile=None, + author=None, descrip=None, history=None, + nmatch=None, catalog=None, + logging=False, logmode='w'): + """ + Save a WCS as a headerlet extension and write it out to a file. + + This function will create a headerlet, attach it as an extension to the + science image (if it has not already been archived) then, optionally, + write out the headerlet to a separate headerlet file. + + Either wcsname or wcskey must be provided, if both are given, they must match a valid WCS + Updates wcscorr if necessary. + + Parameters + ---------- + filename: string or HDUList + Either a filename or PyFITS HDUList object for the input science file + An input filename (str) will be expanded as necessary to interpret + any environmental variables included in the filename. + hdrname: string + Unique name for this headerlet, stored as HDRNAME keyword + sciext: string + name (EXTNAME) of extension that contains WCS to be saved + wcsname: string + name of WCS to be archived, if " ": stop + wcskey: one of A...Z or " " or "PRIMARY" + if " " or "PRIMARY" - archive the primary WCS + destim: string + DESTIM keyword + if NOne, use ROOTNAME or science file name + sipname: string or None (default) + Name of unique file where the polynomial distortion coefficients were + read from. If None, the behavior is: + The code looks for a keyword 'SIPNAME' in the science header + If not found, for HST it defaults to 'IDCTAB' + If there is no SIP model the value is 'NOMODEL' + If there is a SIP model but no SIPNAME, it is set to 'UNKNOWN' + npolfile: string or None (default) + Name of a unique file where the non-polynomial distortion was stored. + If None: + The code looks for 'NPOLFILE' in science header. + If 'NPOLFILE' was not found and there is no npol model, it is set to 'NOMODEL' + If npol model exists, it is set to 'UNKNOWN' + d2imfile: string + Name of a unique file where the detector to image correction was + stored. If None: + The code looks for 'D2IMFILE' in the science header. + If 'D2IMFILE' is not found and there is no d2im correction, + it is set to 'NOMODEL' + If d2im correction exists, but 'D2IMFILE' is missing from science + header, it is set to 'UNKNOWN' + author: string + Name of user who created the headerlet, added as 'AUTHOR' keyword + to headerlet PRIMARY header + descrip: string + Short description of the solution provided by the headerlet + This description will be added as the single 'DESCRIP' keyword + to the headerlet PRIMARY header + history: filename, string or list of strings + Long (possibly multi-line) description of the solution provided + by the headerlet. These comments will be added as 'HISTORY' cards + to the headerlet PRIMARY header + If filename is specified, it will format and attach all text from + that file as the history. + logging: boolean + enable file folling + logmode: 'w' or 'a' + log file open mode + """ + + fobj, fname, close_fobj = parse_filename(filename, mode='update') + + if wcsname in [None, ' ', '', 'INDEF'] and wcskey is None: + message = """ + No valid WCS found found in %s. + A valid value for either "wcsname" or "wcskey" + needs to be specified. + """ % fname + if close_fobj: + fobj.close() + logger.critical(message) + raise ValueError + + # Translate 'wcskey' value for PRIMARY WCS to valid altwcs value of ' ' + if wcskey == 'PRIMARY': + wcskey = ' ' + wcskey = wcskey.upper() + + numhlt = countExtn(fobj, 'HDRLET') + + if wcsname is None: + scihdr = fobj[sciext, 1].header + wcsname = scihdr['wcsname'+wcskey] + + if hdrname in [None, ' ', '']: + hdrname = wcsname + + # Check to see whether or not a HeaderletHDU with this hdrname already + # exists + hdrnames = get_headerlet_kw_names(fobj) + if hdrname not in hdrnames: + hdrletobj = create_headerlet(fobj, sciext=sciext, + wcsname=wcsname, wcskey=wcskey, + hdrname=hdrname, + sipname=sipname, npolfile=npolfile, + d2imfile=d2imfile, author=author, + descrip=descrip, history=history, + nmatch=nmatch, catalog=catalog, + logging=False) + hlt_hdu = HeaderletHDU.fromheaderlet(hdrletobj) + + if destim is not None: + hlt_hdu[0].header['destim'] = destim + + fobj.append(hlt_hdu) + + utils.updateNEXTENDKw(fobj) + fobj.flush() + else: + message = """ + Headerlet with hdrname %s already archived for WCS %s + No new headerlet appended to %s . + """ % (hdrname, wcsname, fname) + logger.critical(message) + + if close_fobj: + fobj.close() + +#### Headerlet Class definitions +class Headerlet(fits.HDUList): + """ + A Headerlet class + Ref: http://mediawiki.stsci.edu/mediawiki/index.php/Telescopedia:Headerlets + """ + + def __init__(self, hdus=[], file=None, logging=False, logmode='w'): + """ + Parameters + ---------- + hdus : list + List of HDUs to be used to create the headerlet object itself + file: string + File-like object from which HDUs should be read + logging: boolean + enable file logging + logmode: 'w' or 'a' + for internal use only, indicates whether the log file + should be open in attach or write mode + """ + self.logging = logging + init_logging('class Headerlet', level=logging, mode=logmode) + + super(Headerlet, self).__init__(hdus, file=file) + + def init_attrs(self): + self.fname = self.filename() + self.hdrname = self[0].header["HDRNAME"] + self.wcsname = self[0].header["WCSNAME"] + self.upwcsver = self[0].header.get("UPWCSVER", "") + self.pywcsver = self[0].header.get("PYWCSVER", "") + self.destim = self[0].header["DESTIM"] + self.sipname = self[0].header["SIPNAME"] + self.idctab = self[0].header["IDCTAB"] + self.npolfile = self[0].header["NPOLFILE"] + self.d2imfile = self[0].header["D2IMFILE"] + self.distname = self[0].header["DISTNAME"] + + try: + self.vafactor = self[("SIPWCS", 1)].header.get("VAFACTOR", 1) #None instead of 1? + except (IndexError, KeyError): + self.vafactor = self[0].header.get("VAFACTOR", 1) #None instead of 1? + self.author = self[0].header["AUTHOR"] + self.descrip = self[0].header["DESCRIP"] + + self.fit_kws = ['HDRNAME', 'NMATCH', 'CATALOG'] + self.history = '' + # header['HISTORY'] returns an iterable of all HISTORY values + if 'HISTORY' in self[0].header: + for hist in self[0].header['HISTORY']: + self.history += hist + '\n' + + self.d2imerr = 0 + self.axiscorr = 1 + + # Overridden to support the Headerlet logging features + @classmethod + def fromfile(cls, fileobj, mode='readonly', memmap=False, + save_backup=False, logging=False, logmode='w', **kwargs): + hlet = super(cls, cls).fromfile(fileobj, mode, memmap, save_backup, + **kwargs) + if len(hlet) > 0: + hlet.init_attrs() + hlet.logging = logging + init_logging('class Headerlet', level=logging, mode=logmode) + return hlet + + @classmethod + def fromstring(cls, data, **kwargs): + hlet = super(cls, cls).fromstring(data, **kwargs) + hlet.logging = logging + init_logging('class Headerlet', level=logging, mode=logmode) + return hlet + + def apply_as_primary(self, fobj, attach=True, archive=True, force=False): + """ + Copy this headerlet as a primary WCS to fobj + + Parameters + ---------- + fobj: string, HDUList + science file to which the headerlet should be applied + attach: boolean + flag indicating if the headerlet should be attached as a + HeaderletHDU to fobj. If True checks that HDRNAME is unique + in the fobj and stops if not. + archive: boolean (default is True) + When the distortion model in the headerlet is the same as the + distortion model of the science file, this flag indicates if + the primary WCS should be saved as an alternate and a headerlet + extension. + When the distortion models do not match this flag indicates if + the current primary and alternate WCSs should be archived as + headerlet extensions and alternate WCS. + force: boolean (default is False) + When the distortion models of the headerlet and the primary do + not match, and archive is False this flag forces an update + of the primary + """ + self.hverify() + fobj, fname, close_dest = parse_filename(fobj, mode='update') + if not self.verify_dest(fobj, fname): + if close_dest: + fobj.close() + raise ValueError("Destination name does not match headerlet" + "Observation %s cannot be updated with headerlet %s" % (fname, self.hdrname)) + + # Check to see whether the distortion model in the destination + # matches the distortion model in the headerlet being applied + + dname = self.get_destination_model(fobj) + dist_models_equal = self.equal_distmodel(dname) + if not dist_models_equal and not force: + raise ValueError("Distortion models do not match" + " To overwrite the distortion model, set force=True") + + orig_hlt_hdu = None + numhlt = countExtn(fobj, 'HDRLET') + hdrlet_extnames = get_headerlet_kw_names(fobj) + + # Insure that WCSCORR table has been created with all original + # WCS's recorded prior to adding the headerlet WCS + wcscorr.init_wcscorr(fobj) + + + ### start archive + # If archive has been specified + # regardless of whether or not the distortion models are equal... + + numsip = countExtn(self, 'SIPWCS') + sciext_list = [] + alt_hlethdu = [] + for i in range(1, numsip+1): + sipheader = self[('SIPWCS', i)].header + sciext_list.append((sipheader['TG_ENAME'], sipheader['TG_EVER'])) + target_ext = sciext_list[0] + if archive: + if 'wcsname' in fobj[target_ext].header: + hdrname = fobj[target_ext].header['WCSNAME'] + wcsname = hdrname + else: + hdrname = fobj[0].header['ROOTNAME'] + '_orig' + wcsname = None + if hdrname not in hdrlet_extnames: + # - if WCS has not been saved, write out WCS as headerlet extension + # Create a headerlet for the original Primary WCS data in the file, + # create an HDU from the original headerlet, and append it to + # the file + orig_hlt = create_headerlet(fobj, sciext=sciext_list, #[target_ext], + wcsname=wcsname, + hdrname=hdrname, + logging=self.logging) + orig_hlt_hdu = HeaderletHDU.fromheaderlet(orig_hlt) + numhlt += 1 + orig_hlt_hdu.header['EXTVER'] = numhlt + logger.info("Created headerlet %s to be attached to file" % hdrname) + else: + logger.info("Headerlet with name %s is already attached" % hdrname) + + + if dist_models_equal: + # Use the WCSNAME to determine whether or not to archive + # Primary WCS as altwcs + # wcsname = hwcs.wcs.name + scihdr = fobj[target_ext].header + if 'hdrname' in scihdr: + priwcs_name = scihdr['hdrname'] + else: + if 'wcsname' in scihdr: + priwcs_name = scihdr['wcsname'] + else: + if 'idctab' in scihdr: + priwcs_name = ''.join(['IDC_', + utils.extract_rootname(scihdr['idctab'], + suffix='_idc')]) + else: + priwcs_name = 'UNKNOWN' + nextkey = altwcs.next_wcskey(fobj, ext=target_ext) + altwcs.archiveWCS(fobj, ext=sciext_list, wcskey=nextkey, + wcsname=priwcs_name) + else: + + for hname in altwcs.wcsnames(fobj, ext=target_ext).values(): + if hname != 'OPUS' and hname not in hdrlet_extnames: + # get HeaderletHDU for alternate WCS as well + alt_hlet = create_headerlet(fobj, sciext=sciext_list, + wcsname=hname, wcskey=wcskey, + hdrname=hname, sipname=None, + npolfile=None, d2imfile=None, + author=None, descrip=None, history=None, + logging=self.logging) + numhlt += 1 + alt_hlet_hdu = HeaderletHDU.fromheaderlet(alt_hlet) + alt_hlet_hdu.header['EXTVER'] = numhlt + alt_hlethdu.append(alt_hlet_hdu) + hdrlet_extnames.append(hname) + + self._del_dest_WCS_ext(fobj) + for i in range(1, numsip+1): + target_ext = sciext_list[i-1] + self._del_dest_WCS(fobj, target_ext) + sipwcs = HSTWCS(self, ('SIPWCS', i)) + idckw = sipwcs._idc2hdr() + priwcs = sipwcs.to_fits(relax=True) + numnpol = 1 + numd2im = 1 + if sipwcs.wcs.has_cd(): + priwcs[0].header = altwcs.pc2cd(priwcs[0].header) + priwcs[0].header.extend(idckw) + if 'crder1' in sipheader: + for card in sipheader['crder*'].cards: + priwcs[0].header.set(card.keyword, card.value, card.comment, + after='WCSNAME') + # Update WCS with HDRNAME as well + + for kw in ['SIMPLE', 'BITPIX', 'NAXIS', 'EXTEND']: + try: + priwcs[0].header.remove(kw) + except ValueError: + pass + + priwcs[0].header.set('WCSNAME', self[0].header['WCSNAME'], "") + priwcs[0].header.set('WCSAXES', self[('SIPWCS', i)].header['WCSAXES'], "") + priwcs[0].header.set('HDRNAME', self[0].header['HDRNAME'], "") + if sipwcs.det2im1 or sipwcs.det2im2: + try: + d2imerr = self[('SIPWCS', i)].header['D2IMERR*'] + priwcs[0].header.extend(d2imerr) + except KeyError: + pass + try: + priwcs[0].header.append(self[('SIPWCS', i)].header.cards['D2IMEXT']) + except KeyError: + pass + if 'D2IM1.EXTVER' in priwcs[0].header: + priwcs[0].header['D2IM1.EXTVER'] = self[('SIPWCS', i)].header['D2IM1.EXTVER'] + priwcs[('D2IMARR', 1)].header['EXTVER'] = self[('SIPWCS', i)].header['D2IM1.EXTVER'] + if 'D2IM2.EXTVER' in priwcs[0].header: + priwcs[0].header['D2IM2.EXTVER'] = self[('SIPWCS', i)].header['D2IM2.EXTVER'] + priwcs[('D2IMARR', 2)].header['EXTVER'] = self[('SIPWCS', i)].header['D2IM2.EXTVER'] + # D2IM1 will NOT exist for WFPC2 data... + if 'D2IM1.EXTVER' in priwcs[0].header: + # only set number of D2IM extensions to 2 if D2IM1 exists + numd2im = 2 + + if sipwcs.cpdis1 or sipwcs.cpdis2: + try: + cperr = self[('SIPWCS', i)].header['CPERR*'] + priwcs[0].header.extend(cperr) + except KeyError: + pass + try: + priwcs[0].header.append(self[('SIPWCS', i)].header.cards['NPOLEXT']) + except KeyError: + pass + if 'DP1.EXTVER' in priwcs[0].header: + priwcs[0].header['DP1.EXTVER'] = self[('SIPWCS', i)].header['DP1.EXTVER'] + priwcs[('WCSDVARR', 1)].header['EXTVER'] = self[('SIPWCS', i)].header['DP1.EXTVER'] + if 'DP2.EXTVER' in priwcs[0].header: + priwcs[0].header['DP2.EXTVER'] = self[('SIPWCS', i)].header['DP2.EXTVER'] + priwcs[('WCSDVARR', 2)].header['EXTVER'] = self[('SIPWCS', i)].header['DP2.EXTVER'] + numnpol = 2 + + fobj[target_ext].header.extend(priwcs[0].header) + if sipwcs.cpdis1: + whdu = priwcs[('WCSDVARR', (i-1)*numnpol+1)].copy() + whdu.update_ext_version(self[('SIPWCS', i)].header['DP1.EXTVER']) + fobj.append(whdu) + if sipwcs.cpdis2: + whdu = priwcs[('WCSDVARR', i*numnpol)].copy() + whdu.update_ext_version(self[('SIPWCS', i)].header['DP2.EXTVER']) + fobj.append(whdu) + if sipwcs.det2im1: #or sipwcs.det2im2: + whdu = priwcs[('D2IMARR', (i-1)*numd2im+1)].copy() + whdu.update_ext_version(self[('SIPWCS', i)].header['D2IM1.EXTVER']) + fobj.append(whdu) + if sipwcs.det2im2: + whdu = priwcs[('D2IMARR', i*numd2im)].copy() + whdu.update_ext_version(self[('SIPWCS', i)].header['D2IM2.EXTVER']) + fobj.append(whdu) + + update_versions(self[0].header, fobj[0].header) + refs = update_ref_files(self[0].header, fobj[0].header) + # Update the WCSCORR table with new rows from the headerlet's WCSs + wcscorr.update_wcscorr(fobj, self, 'SIPWCS') + + # Append the original headerlet + if archive and orig_hlt_hdu: + fobj.append(orig_hlt_hdu) + # Append any alternate WCS Headerlets + if len(alt_hlethdu) > 0: + for ahdu in alt_hlethdu: + fobj.append(ahdu) + if attach: + # Finally, append an HDU for this headerlet + self.attach_to_file(fobj) + utils.updateNEXTENDKw(fobj) + if close_dest: + fobj.close() + + + def apply_as_alternate(self, fobj, attach=True, wcskey=None, wcsname=None): + """ + Copy this headerlet as an alternate WCS to fobj + + Parameters + ---------- + fobj: string, HDUList + science file/HDUList to which the headerlet should be applied + attach: boolean + flag indicating if the headerlet should be attached as a + HeaderletHDU to fobj. If True checks that HDRNAME is unique + in the fobj and stops if not. + wcskey: string + Key value (A-Z, except O) for this alternate WCS + If None, the next available key will be used + wcsname: string + Name to be assigned to this alternate WCS + WCSNAME is a required keyword in a Headerlet but this allows the + user to change it as desired. + + """ + self.hverify() + fobj, fname, close_dest = parse_filename(fobj, mode='update') + if not self.verify_dest(fobj, fname): + if close_dest: + fobj.close() + raise ValueError("Destination name does not match headerlet" + "Observation %s cannot be updated with headerlet %s" % (fname, self.hdrname)) + + # Verify whether this headerlet has the same distortion + #found in the image being updated + dname = self.get_destination_model(fobj) + dist_models_equal = self.equal_distmodel(dname) + if not dist_models_equal: + raise ValueError("Distortion models do not match \n" + "Headerlet: %s \n" + "Destination file: %s\n" + "attach_to_file() can be used to append this headerlet" %(self.distname, dname)) + + # Insure that WCSCORR table has been created with all original + # WCS's recorded prior to adding the headerlet WCS + wcscorr.init_wcscorr(fobj) + + # determine value of WCSNAME to be used + if wcsname is not None: + wname = wcsname + else: + wname = self[0].header['WCSNAME'] + tg_ename = self[('SIPWCS', 1)].header['TG_ENAME'] + tg_ever = self[('SIPWCS', 1)].header['TG_EVER'] + # determine what alternate WCS this headerlet will be assigned to + if wcskey is None: + wkey = altwcs.next_wcskey(fobj[(tg_ename, tg_ever)].header) + else: + wcskey = wcskey.upper() + available_keys = altwcs.available_wcskeys(fobj[(tg_ename, tg_ever)].header) + if wcskey in available_keys: + wkey = wcskey + else: + mess = "Observation %s already contains alternate WCS with key %s" % (fname, wcskey) + logger.critical(mess) + if close_dest: + fobj.close() + raise ValueError(mess) + numsip = countExtn(self, 'SIPWCS') + for idx in range(1, numsip + 1): + siphdr = self[('SIPWCS', idx)].header + tg_ext = (siphdr['TG_ENAME'], siphdr['TG_EVER']) + + fhdr = fobj[tg_ext].header + hwcs = pywcs.WCS(siphdr, self) + hwcs_header = hwcs.to_header(key=wkey) + _idc2hdr(siphdr, fhdr, towkey=wkey) + if hwcs.wcs.has_cd(): + hwcs_header = altwcs.pc2cd(hwcs_header, key=wkey) + + fhdr.extend(hwcs_header) + fhdr['WCSNAME' + wkey] = wname + # also update with HDRNAME (a non-WCS-standard kw) + for kw in self.fit_kws: + #fhdr.insert(wind, pyfits.Card(kw + wkey, + # self[0].header[kw])) + fhdr.append(fits.Card(kw + wkey, self[0].header[kw])) + # Update the WCSCORR table with new rows from the headerlet's WCSs + wcscorr.update_wcscorr(fobj, self, 'SIPWCS') + + if attach: + self.attach_to_file(fobj) + utils.updateNEXTENDKw(fobj) + + if close_dest: + fobj.close() + + def attach_to_file(self, fobj, archive=False): + """ + Attach Headerlet as an HeaderletHDU to a science file + + Parameters + ---------- + fobj: string, HDUList + science file/HDUList to which the headerlet should be applied + archive: string + Specifies whether or not to update WCSCORR table when attaching + + Notes + ----- + The algorithm used by this method: + - verify headerlet can be applied to this file (based on DESTIM) + - verify that HDRNAME is unique for this file + - attach as HeaderletHDU to fobj + + """ + self.hverify() + fobj, fname, close_dest = parse_filename(fobj, mode='update') + destver = self.verify_dest(fobj, fname) + hdrver = self.verify_hdrname(fobj) + if destver and hdrver: + + numhlt = countExtn(fobj, 'HDRLET') + new_hlt = HeaderletHDU.fromheaderlet(self) + new_hlt.header['extver'] = numhlt + 1 + fobj.append(new_hlt) + utils.updateNEXTENDKw(fobj) + else: + message = "Headerlet %s cannot be attached to" % (self.hdrname) + message += "observation %s" % (fname) + if not destver: + message += " * Image %s keyword ROOTNAME not equal to " % (fname) + message += " DESTIM = '%s'\n" % (self.destim) + if not hdrver: + message += " * Image %s already has headerlet " % (fname) + message += "with HDRNAME='%s'\n" % (self.hdrname) + logger.critical(message) + if close_dest: + fobj.close() + + def info(self, columns=None, pad=2, maxwidth=None, + output=None, clobber=True, quiet=False): + """ + Prints a summary of this headerlet + The summary includes: + HDRNAME WCSNAME DISTNAME SIPNAME NPOLFILE D2IMFILE + + Parameters + ---------- + columns: list + List of headerlet PRIMARY header keywords to report in summary + By default (set to None), it will use the default set of keywords + defined as the global list DEFAULT_SUMMARY_COLS + pad: int + Number of padding spaces to put between printed columns + [Default: 2] + maxwidth: int + Maximum column width(not counting padding) for any column in summary + By default (set to None), each column's full width will be used + output: string (optional) + Name of optional output file to record summary. This filename + can contain environment variables. + [Default: None] + clobber: bool + If True, will overwrite any previous output file of same name + quiet: bool + If True, will NOT report info to STDOUT + + """ + summary_cols, summary_dict = self.summary(columns=columns) + print_summary(summary_cols, summary_dict, pad=pad, maxwidth=maxwidth, + idcol=None, output=output, clobber=clobber, quiet=quiet) + + def summary(self, columns=None): + """ + Returns a summary of this headerlet as a dictionary + + The summary includes a summary of the distortion model as : + HDRNAME WCSNAME DISTNAME SIPNAME NPOLFILE D2IMFILE + + Parameters + ---------- + columns: list + List of headerlet PRIMARY header keywords to report in summary + By default(set to None), it will use the default set of keywords + defined as the global list DEFAULT_SUMMARY_COLS + + Returns + ------- + summary: dict + Dictionary of values for summary + """ + if columns is None: + summary_cols = DEFAULT_SUMMARY_COLS + else: + summary_cols = columns + + # Initialize summary dict based on requested columns + summary = {} + for kw in summary_cols: + summary[kw] = copy.deepcopy(COLUMN_DICT) + + # Populate the summary with headerlet values + for kw in summary_cols: + if kw in self[0].header: + val = self[0].header[kw] + else: + val = 'INDEF' + summary[kw]['vals'].append(val) + summary[kw]['width'].append(max(len(val), len(kw))) + + return summary_cols, summary + + def hverify(self): + """ + Verify the headerlet file is a valid fits file and has + the required Primary Header keywords + """ + self.verify() + header = self[0].header + assert('DESTIM' in header and header['DESTIM'].strip()) + assert('HDRNAME' in header and header['HDRNAME'].strip()) + assert('UPWCSVER' in header) + + def verify_hdrname(self, dest): + """ + Verifies that the headerlet can be applied to the observation + + Reports whether or not this file already has a headerlet with this + HDRNAME. + """ + unique = verify_hdrname_is_unique(dest, self.hdrname) + logger.debug("verify_hdrname() returned %s"%unique) + return unique + + def get_destination_model(self, dest): + """ + Verifies that the headerlet can be applied to the observation + + Determines whether or not the file specifies the same distortion + model/reference files. + """ + destim_opened = False + if not isinstance(dest, fits.HDUList): + destim = fits.open(dest) + destim_opened = True + else: + destim = dest + dname = destim[0].header['DISTNAME'] if 'distname' in destim[0].header \ + else self.build_distname(dest) + if destim_opened: + destim.close() + return dname + + def equal_distmodel(self, dmodel): + if dmodel != self[0].header['DISTNAME']: + if self.logging: + message = """ + Distortion model in headerlet not the same as destination model + Headerlet model : %s + Destination model: %s + """ % (self[0].header['DISTNAME'], dmodel) + logger.critical(message) + return False + else: + return True + + def verify_dest(self, dest, fname): + """ + verifies that the headerlet can be applied to the observation + + DESTIM in the primary header of the headerlet must match ROOTNAME + of the science file (or the name of the destination file) + """ + try: + if not isinstance(dest, fits.HDUList): + droot = fits.getval(dest, 'ROOTNAME') + else: + droot = dest[0].header['ROOTNAME'] + except KeyError: + logger.debug("Keyword 'ROOTNAME' not found in destination file") + droot = dest.split('.fits')[0] + if droot == self.destim: + logger.debug("verify_destim() returned True") + return True + else: + logger.debug("verify_destim() returned False") + logger.critical("Destination name does not match headerlet. " + "Observation %s cannot be updated with headerlet %s" % (fname, self.hdrname)) + return False + + def build_distname(self, dest): + """ + Builds the DISTNAME for dest based on reference file names. + """ + + try: + npolfile = dest[0].header['NPOLFILE'] + except KeyError: + npolfile = None + try: + d2imfile = dest[0].header['D2IMFILE'] + except KeyError: + d2imfile = None + + sipname, idctab = utils.build_sipname(dest, dest, None) + npolname, npolfile = utils.build_npolname(dest, npolfile) + d2imname, d2imfile = utils.build_d2imname(dest, d2imfile) + dname = utils.build_distname(sipname,npolname,d2imname) + return dname + + def tofile(self, fname, destim=None, hdrname=None, clobber=False): + """ + Write this headerlet to a file + + Parameters + ---------- + fname: string + file name + destim: string (optional) + provide a value for DESTIM keyword + hdrname: string (optional) + provide a value for HDRNAME keyword + clobber: boolean + a flag which allows to overwrte an existing file + """ + if not destim or not hdrname: + self.hverify() + self.writeto(fname, clobber=clobber) + + def _del_dest_WCS(self, dest, ext=None): + """ + Delete the WCS of a science file extension + """ + + logger.info("Deleting all WCSs of file %s" % dest.filename()) + numext = len(dest) + + if ext: + fext = dest[ext] + self._remove_d2im(fext) + self._remove_sip(fext) + self._remove_lut(fext) + self._remove_primary_WCS(fext) + self._remove_idc_coeffs(fext) + self._remove_fit_values(fext) + else: + for idx in range(numext): + # Only delete WCS from extensions which may have WCS keywords + if ('XTENSION' in dest[idx].header and + dest[idx].header['XTENSION'] == 'IMAGE'): + self._remove_d2im(dest[idx]) + self._remove_sip(dest[idx]) + self._remove_lut(dest[idx]) + self._remove_primary_WCS(dest[idx]) + self._remove_idc_coeffs(dest[idx]) + self._remove_fit_values(dest[idx]) + self._remove_ref_files(dest[0]) + """ + if not ext: + self._remove_alt_WCS(dest, ext=range(numext)) + else: + self._remove_alt_WCS(dest, ext=ext) + """ + def _del_dest_WCS_ext(self, dest): + numwdvarr = countExtn(dest, 'WCSDVARR') + numd2im = countExtn(dest, 'D2IMARR') + if numwdvarr > 0: + for idx in range(1, numwdvarr + 1): + del dest[('WCSDVARR', idx)] + if numd2im > 0: + for idx in range(1, numd2im + 1): + del dest[('D2IMARR', idx)] + + def _remove_ref_files(self, phdu): + """ + phdu: Primary HDU + """ + refkw = ['IDCTAB', 'NPOLFILE', 'D2IMFILE', 'SIPNAME', 'DISTNAME'] + for kw in refkw: + try: + del phdu.header[kw] + except KeyError: + pass + + def _remove_fit_values(self, ext): + """ + Remove the any existing astrometric fit values from a FITS extension + """ + + logger.debug("Removing astrometric fit values from (%s, %s)"% + (ext.name, ext.ver)) + dkeys = altwcs.wcskeys(ext.header) + if 'O' in dkeys: dkeys.remove('O') # Do not remove wcskey='O' values + for fitkw in ['NMATCH', 'CATALOG']: + for k in dkeys: + fkw = (fitkw+k).rstrip() + if fkw in ext.header: + del ext.header[fkw] + + def _remove_sip(self, ext): + """ + Remove the SIP distortion of a FITS extension + """ + + logger.debug("Removing SIP distortion from (%s, %s)" + % (ext.name, ext.ver)) + for prefix in ['A', 'B', 'AP', 'BP']: + try: + order = ext.header[prefix + '_ORDER'] + del ext.header[prefix + '_ORDER'] + except KeyError: + continue + for i in range(order + 1): + for j in range(order + 1): + key = prefix + '_%d_%d' % (i, j) + try: + del ext.header[key] + except KeyError: + pass + try: + del ext.header['IDCTAB'] + except KeyError: + pass + + def _remove_lut(self, ext): + """ + Remove the Lookup Table distortion of a FITS extension + """ + + logger.debug("Removing LUT distortion from (%s, %s)" + % (ext.name, ext.ver)) + try: + cpdis = ext.header['CPDIS*'] + except KeyError: + return + try: + for c in range(1, len(cpdis) + 1): + del ext.header['DP%s*...' % c] + del ext.header[cpdis.cards[c - 1].keyword] + del ext.header['CPERR*'] + del ext.header['NPOLFILE'] + del ext.header['NPOLEXT'] + except KeyError: + pass + + def _remove_d2im(self, ext): + """ + Remove the Detector to Image correction of a FITS extension + """ + + logger.debug("Removing D2IM correction from (%s, %s)" + % (ext.name, ext.ver)) + try: + d2imdis = ext.header['D2IMDIS*'] + except KeyError: + return + try: + for c in range(1, len(d2imdis) + 1): + del ext.header['D2IM%s*...' % c] + del ext.header[d2imdis.cards[c - 1].keyword] + del ext.header['D2IMERR*'] + del ext.header['D2IMFILE'] + del ext.header['D2IMEXT'] + except KeyError: + pass + + def _remove_alt_WCS(self, dest, ext): + """ + Remove Alternate WCSs of a FITS extension. + A WCS with wcskey 'O' is never deleted. + """ + dkeys = altwcs.wcskeys(dest[('SCI', 1)].header) + for val in ['O', '', ' ']: + if val in dkeys: + dkeys.remove(val) # Never delete WCS with wcskey='O' + + logger.debug("Removing alternate WCSs with keys %s from %s" + % (dkeys, dest.filename())) + for k in dkeys: + altwcs.deleteWCS(dest, ext=ext, wcskey=k) + + def _remove_primary_WCS(self, ext): + """ + Remove the primary WCS of a FITS extension + """ + + logger.debug("Removing Primary WCS from (%s, %s)" + % (ext.name, ext.ver)) + naxis = ext.header['NAXIS'] + for key in basic_wcs: + for i in range(1, naxis + 1): + try: + del ext.header[key + str(i)] + except KeyError: + pass + try: + del ext.header['WCSAXES'] + except KeyError: + pass + try: + del ext.header['WCSNAME'] + except KeyError: + pass + + def _remove_idc_coeffs(self, ext): + """ + Remove IDC coefficients of a FITS extension + """ + + logger.debug("Removing IDC coefficient from (%s, %s)" + % (ext.name, ext.ver)) + coeffs = ['OCX10', 'OCX11', 'OCY10', 'OCY11', 'IDCSCALE'] + for k in coeffs: + try: + del ext.header[k] + except KeyError: + pass + +@with_logging +def _idc2hdr(fromhdr, tohdr, towkey=' '): + """ + Copy the IDC (HST specific) keywords from one header to another + + """ + # save some of the idc coefficients + coeffs = ['OCX10', 'OCX11', 'OCY10', 'OCY11', 'IDCSCALE'] + for c in coeffs: + try: + tohdr[c+towkey] = fromhdr[c] + logger.debug("Copied %s to header") + except KeyError: + continue + + +def get_extname_extver_list(fobj, sciext): + """ + Create a list of (EXTNAME, EXTVER) tuples + + Based on sciext keyword (see docstring for create_headerlet) + walk throughh the file and convert extensions in `sciext` to + valid (EXTNAME, EXTVER) tuples. + """ + extlist = [] + if isinstance(sciext, int): + if sciext == 0: + extname = 'PRIMARY' + extver = 1 + else: + try: + extname = fobj[sciext].header['EXTNAME'] + except KeyError: + extname = "" + try: + extver = fobj[sciext].header['EXTVER'] + except KeyError: + extver = 1 + extlist.append((extname, extver)) + elif isinstance(sciext, str): + if sciext == 'PRIMARY': + extname = "PRIMARY" + extver = 1 + extlist.append((extname, extver)) + else: + for ext in fobj: + try: + extname = ext.header['EXTNAME'] + except KeyError: + continue + if extname.upper() == sciext.upper(): + try: + extver = ext.header['EXTVER'] + except KeyError: + extver = 1 + extlist.append((extname, extver)) + elif isinstance(sciext, list): + if isinstance(sciext[0], int): + for i in sciext: + try: + extname = fobj[i].header['EXTNAME'] + except KeyError: + if i == 0: + extname = "PRIMARY" + extver = 1 + else: + extname = "" + try: + extver = fobj[i].header['EXTVER'] + except KeyError: + extver = 1 + extlist.append((extname, extver)) + else: + extlist = sciext[:] + else: + errstr = "Expected sciext to be a list of FITS extensions with science data\n"+\ + " a valid EXTNAME string, or an integer." + logger.critical(errstr) + raise ValueError + return extlist + + +class HeaderletHDU(fits.hdu.nonstandard.FitsHDU): + """ + A non-standard extension HDU for encapsulating Headerlets in a file. These + HDUs have an extension type of HDRLET and their EXTNAME is derived from the + Headerlet's HDRNAME. + + The data itself is a FITS file embedded within the HDU data. The file name + is derived from the HDRNAME keyword, and should be in the form + `<HDRNAME>_hdr.fits`. If the COMPRESS keyword evaluates to `True`, the tar + file is compressed with gzip compression. + + The structure of this HDU is the same as that proposed for the 'FITS' + extension type proposed here: + http://listmgr.cv.nrao.edu/pipermail/fitsbits/2002-April/thread.html + + The Headerlet contained in the HDU's data can be accessed by the + `headerlet` attribute. + """ + + _extension = 'HDRLET' + + @lazyproperty + def headerlet(self): + """Return the encapsulated headerlet as a Headerlet object. + + This is similar to the hdulist property inherited from the FitsHDU + class, though the hdulist property returns a normal HDUList object. + """ + + return Headerlet(self.hdulist) + + @classmethod + def fromheaderlet(cls, headerlet, compress=False): + """ + Creates a new HeaderletHDU from a given Headerlet object. + + Parameters + ---------- + headerlet : `Headerlet` + A valid Headerlet object. + + compress : bool, optional + Gzip compress the headerlet data. + + Returns + ------- + hlet : `HeaderletHDU` + A `HeaderletHDU` object for the given `Headerlet` that can be + attached as an extension to an existing `HDUList`. + """ + + # TODO: Perhaps check that the given object is in fact a valid + # Headerlet + hlet = cls.fromhdulist(headerlet, compress) + + # Add some more headerlet-specific keywords to the header + phdu = headerlet[0] + + if 'SIPNAME' in phdu.header: + sipname = phdu.header['SIPNAME'] + else: + sipname = phdu.header['WCSNAME'] + + hlet.header['HDRNAME'] = (phdu.header['HDRNAME'], + phdu.header.comments['HDRNAME']) + hlet.header['DATE'] = (phdu.header['DATE'], + phdu.header.comments['DATE']) + hlet.header['SIPNAME'] = (sipname, 'SIP distortion model name') + hlet.header['WCSNAME'] = (phdu.header['WCSNAME'], 'WCS name') + hlet.header['DISTNAME'] = (phdu.header['DISTNAME'], + 'Distortion model name') + hlet.header['NPOLFILE'] = (phdu.header['NPOLFILE'], + phdu.header.comments['NPOLFILE']) + hlet.header['D2IMFILE'] = (phdu.header['D2IMFILE'], + phdu.header.comments['D2IMFILE']) + hlet.header['EXTNAME'] = (cls._extension, 'Extension name') + + return hlet + + +fits.register_hdu(HeaderletHDU) 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 diff --git a/stwcs/wcsutil/instruments.py b/stwcs/wcsutil/instruments.py new file mode 100644 index 0000000..f662513 --- /dev/null +++ b/stwcs/wcsutil/instruments.py @@ -0,0 +1,320 @@ +from __future__ import absolute_import, division, print_function # confidence high + +from .mappings import ins_spec_kw + +class InstrWCS(object): + """ + A base class for instrument specific keyword definition. + It prvides a default implementation (modeled by ACS) for + all set_kw methods. + """ + def __init__(self, hdr0=None, hdr=None): + self.exthdr = hdr + self.primhdr = hdr0 + self.set_ins_spec_kw() + + def set_ins_spec_kw(self): + """ + This method MUST call all set_kw methods. + There should be a set_kw method for all kw listed in + mappings.ins_spec_kw. TypeError handles the case when + fobj='DEFAULT'. + """ + self.set_idctab() + self.set_offtab() + self.set_date_obs() + self.set_ra_targ() + self.set_dec_targ() + self.set_pav3() + self.set_detector() + self.set_filter1() + self.set_filter2() + self.set_vafactor() + self.set_naxis1() + self.set_naxis2() + self.set_ltv1() + self.set_ltv2() + self.set_binned() + self.set_chip() + self.set_parity() + + def set_idctab(self): + try: + self.idctab = self.primhdr['IDCTAB'] + except (KeyError, TypeError): + self.idctab = None + + def set_offtab(self): + try: + self.offtab = self.primhdr['OFFTAB'] + except (KeyError, TypeError): + self.offtab = None + + def set_date_obs(self): + try: + self.date_obs = self.primhdr['DATE-OBS'] + except (KeyError, TypeError): + self.date_obs = None + + def set_ra_targ(self): + try: + self.ra_targ = self.primhdr['RA-TARG'] + except (KeyError, TypeError): + self.ra_targ = None + + def set_dec_targ(self): + try: + self.dec_targ = self.primhdr['DEC-TARG'] + except (KeyError, TypeError): + self.dec_targ = None + + def set_pav3(self): + try: + self.pav3 = self.primhdr['PA_V3'] + except (KeyError, TypeError): + self.pav3 = None + + def set_filter1(self): + try: + self.filter1 = self.primhdr['FILTER1'] + except (KeyError, TypeError): + self.filter1 = None + + def set_filter2(self): + try: + self.filter2 = self.primhdr['FILTER2'] + except (KeyError, TypeError): + self.filter2 = None + + def set_vafactor(self): + try: + self.vafactor = self.exthdr['VAFACTOR'] + except (KeyError, TypeError): + self.vafactor = 1 + + def set_naxis1(self): + try: + self.naxis1 = self.exthdr['naxis1'] + except (KeyError, TypeError): + try: + self.naxis1 = self.exthdr['npix1'] + except (KeyError, TypeError): + self.naxis1 = None + + def set_naxis2(self): + try: + self.naxis2 = self.exthdr['naxis2'] + except (KeyError, TypeError): + try: + self.naxis2 = self.exthdr['npix2'] + except (KeyError, TypeError): + self.naxis2 = None + + def set_ltv1(self): + try: + self.ltv1 = self.exthdr['LTV1'] + except (KeyError, TypeError): + self.ltv1 = 0.0 + + def set_ltv2(self): + try: + self.ltv2 = self.exthdr['LTV2'] + except (KeyError, TypeError): + self.ltv2 = 0.0 + + def set_binned(self): + try: + self.binned = self.exthdr['BINAXIS1'] + except (KeyError, TypeError): + self.binned = 1 + + def set_chip(self): + try: + self.chip = self.exthdr['CCDCHIP'] + except (KeyError, TypeError): + self.chip = 1 + + def set_parity(self): + self.parity = [[1.0,0.0],[0.0,-1.0]] + + def set_detector(self): + # each instrument has a different kw for detector and it can be + # in a different header, so this is to be handled by the instrument classes + self.detector = 'DEFAULT' + +class ACSWCS(InstrWCS): + """ + get instrument specific kw + """ + + def __init__(self, hdr0, hdr): + self.primhdr = hdr0 + self.exthdr = hdr + InstrWCS.__init__(self,hdr0, hdr) + self.set_ins_spec_kw() + + def set_detector(self): + try: + self.detector = self.primhdr['DETECTOR'] + except KeyError: + print('ERROR: Detector kw not found.\n') + raise + + def set_parity(self): + parity = {'WFC':[[1.0,0.0],[0.0,-1.0]], + 'HRC':[[-1.0,0.0],[0.0,1.0]], + 'SBC':[[-1.0,0.0],[0.0,1.0]]} + + if self.detector not in list(parity.keys()): + parity = InstrWCS.set_parity(self) + else: + self.parity = parity[self.detector] + + +class WFPC2WCS(InstrWCS): + + + def __init__(self, hdr0, hdr): + self.primhdr = hdr0 + self.exthdr = hdr + InstrWCS.__init__(self,hdr0, hdr) + self.set_ins_spec_kw() + + def set_filter1(self): + self.filter1 = self.primhdr.get('FILTNAM1', None) + if self.filter1 == " " or self.filter1 == None: + self.filter1 = 'CLEAR1' + + def set_filter2(self): + self.filter2 = self.primhdr.get('FILTNAM2', None) + if self.filter2 == " " or self.filter2 == None: + self.filter2 = 'CLEAR2' + + + def set_binned(self): + mode = self.primhdr.get('MODE', 1) + if mode == 'FULL': + self.binned = 1 + elif mode == 'AREA': + self.binned = 2 + + def set_chip(self): + self.chip = self.exthdr.get('DETECTOR', 1) + + def set_parity(self): + self.parity = [[-1.0,0.],[0.,1.0]] + + def set_detector(self): + try: + self.detector = self.exthdr['DETECTOR'] + except KeyError: + print('ERROR: Detector kw not found.\n') + raise + + +class WFC3WCS(InstrWCS): + """ + Create a WFC3 detector specific class + """ + + def __init__(self, hdr0, hdr): + self.primhdr = hdr0 + self.exthdr = hdr + InstrWCS.__init__(self,hdr0, hdr) + self.set_ins_spec_kw() + + def set_detector(self): + try: + self.detector = self.primhdr['DETECTOR'] + except KeyError: + print('ERROR: Detector kw not found.\n') + raise + + def set_filter1(self): + self.filter1 = self.primhdr.get('FILTER', None) + if self.filter1 == " " or self.filter1 == None: + self.filter1 = 'CLEAR' + + def set_filter2(self): + #Nicmos idc tables do not allow 2 filters. + self.filter2 = 'CLEAR' + + def set_parity(self): + parity = {'UVIS':[[-1.0,0.0],[0.0,1.0]], + 'IR':[[-1.0,0.0],[0.0,1.0]]} + + if self.detector not in list(parity.keys()): + parity = InstrWCS.set_parity(self) + else: + self.parity = parity[self.detector] + +class NICMOSWCS(InstrWCS): + """ + Create a NICMOS specific class + """ + + def __init__(self, hdr0, hdr): + self.primhdr = hdr0 + self.exthdr = hdr + InstrWCS.__init__(self,hdr0, hdr) + self.set_ins_spec_kw() + + def set_parity(self): + self.parity = [[-1.0,0.],[0.,1.0]] + + def set_filter1(self): + self.filter1 = self.primhdr.get('FILTER', None) + if self.filter1 == " " or self.filter1 == None: + self.filter1 = 'CLEAR' + + def set_filter2(self): + #Nicmos idc tables do not allow 2 filters. + self.filter2 = 'CLEAR' + + def set_chip(self): + self.chip = self.detector + + def set_detector(self): + try: + self.detector = self.primhdr['CAMERA'] + except KeyError: + print('ERROR: Detector kw not found.\n') + raise + +class STISWCS(InstrWCS): + """ + A STIS specific class + """ + + def __init__(self, hdr0, hdr): + self.primhdr = hdr0 + self.exthdr = hdr + InstrWCS.__init__(self,hdr0, hdr) + self.set_ins_spec_kw() + + def set_parity(self): + self.parity = [[-1.0,0.],[0.,1.0]] + + def set_filter1(self): + self.filter1 = self.exthdr.get('OPT_ELEM', None) + if self.filter1 == " " or self.filter1 == None: + self.filter1 = 'CLEAR1' + + def set_filter2(self): + self.filter2 = self.exthdr.get('FILTER', None) + if self.filter2 == " " or self.filter2 == None: + self.filter2 = 'CLEAR2' + + def set_detector(self): + try: + self.detector = self.primhdr['DETECTOR'] + except KeyError: + print('ERROR: Detector kw not found.\n') + raise + + def set_date_obs(self): + try: + self.date_obs = self.exthdr['DATE-OBS'] + except (KeyError, TypeError): + self.date_obs = None + diff --git a/stwcs/wcsutil/mappings.py b/stwcs/wcsutil/mappings.py new file mode 100644 index 0000000..24038bf --- /dev/null +++ b/stwcs/wcsutil/mappings.py @@ -0,0 +1,29 @@ +from __future__ import division # confidence high + +# This dictionary maps an instrument into an instrument class +# The instrument class handles instrument specific keywords + +inst_mappings={'WFPC2': 'WFPC2WCS', + 'ACS': 'ACSWCS', + 'NICMOS': 'NICMOSWCS', + 'STIS': 'STISWCS', + 'WFC3': 'WFC3WCS', + 'DEFAULT': 'InstrWCS' + } + + +# A list of instrument specific keywords +# Every instrument class must have methods which define each of these +# as class attributes. +ins_spec_kw = [ 'idctab', 'offtab', 'date_obs', 'ra_targ', 'dec_targ', 'pav3', \ + 'detector', 'ltv1', 'ltv2', 'parity', 'binned','vafactor', \ + 'chip', 'naxis1', 'naxis2', 'filter1', 'filter2'] + +# A list of keywords defined in the primary header. +# The HSTWCS class sets this as attributes +prim_hdr_kw = ['detector', 'offtab', 'idctab', 'date-obs', + 'pa_v3', 'ra_targ', 'dec_targ'] + +# These are the keywords which are archived before MakeWCS is run +basic_wcs = ['CD1_', 'CD2_', 'CRVAL', 'CTYPE', 'CRPIX', 'CTYPE', 'CDELT', 'CUNIT'] + diff --git a/stwcs/wcsutil/mosaic.py b/stwcs/wcsutil/mosaic.py new file mode 100644 index 0000000..9d2d0a3 --- /dev/null +++ b/stwcs/wcsutil/mosaic.py @@ -0,0 +1,183 @@ +from __future__ import division, print_function +import numpy as np +from matplotlib import pyplot as plt +from astropy.io import fits +import string + +from stsci.tools import parseinput, irafglob +from stwcs.distortion import utils +from stwcs import updatewcs, wcsutil +from stwcs.wcsutil import altwcs + +def vmosaic(fnames, outwcs=None, ref_wcs=None, ext=None, extname=None, undistort=True, wkey='V', wname='VirtualMosaic', plot=False, clobber=False): + """ + Create a virtual mosaic using the WCS of the input images. + + Parameters + ---------- + fnames: a string or a list + a string or a list of filenames, or a list of wcsutil.HSTWCS objects + outwcs: an HSTWCS object + if given, represents the output tangent plane + if None, the output WCS is calculated from the input observations. + ref_wcs: an HSTWCS object + if output wcs is not given, this will be used as a reference for the + calculation of the output WCS. If ref_wcs is None and outwcs is None, + then the first observation in th einput list is used as reference. + ext: an int, a tuple or a list + an int - represents a FITS extension, e.g. 0 is the primary HDU + a tuple - uses the notation (extname, extver), e.g. ('sci',1) + Can be a list of integers or tuples representing FITS extensions + extname: string + the value of the EXTNAME keyword for the extensions to be used in the mosaic + undistort: boolean (default: True) + undistort (or not) the output WCS + wkey: string + default: 'V' + one character A-Z to be used to record the virtual mosaic WCS as + an alternate WCS in the headers of the input files. + wname: string + default: 'VirtualMosaic + a string to be used as a WCSNAME value for the alternate WCS representign + the virtual mosaic + plot: boolean + if True and matplotlib is installed will make a plot of the tangent plane + and the location of the input observations. + clobber: boolean + This covers the case when an alternate WCS with the requested key + already exists in the header of the input files. + if clobber is True, it will be overwritten + if False, it will compute the new one but will not write it to the headers. + + Notes + ----- + The algorithm is: + 1. If output WCS is not given it is calculated from the input WCSes. + The first image is used as a reference, if no reference is given. + This represents the virtual mosaic WCS. + 2. For each input observation/chip, an HSTWCS object is created + and its footprint on the sky is calculated (using only the four corners). + 3. For each input observation the footprint is projected on the output + tangent plane and the virtual WCS is recorded in the header. + """ + wcsobjects = readWCS(fnames, ext, extname) + if outwcs != None: + outwcs = outwcs.deepcopy() + else: + if ref_wcs != None: + outwcs = utils.output_wcs(wcsobjects, ref_wcs=ref_wcs, undistort=undistort) + else: + outwcs = utils.output_wcs(wcsobjects, undistort=undistort) + if plot: + outc=np.array([[0.,0], [outwcs._naxis1, 0], + [outwcs._naxis1, outwcs._naxis2], + [0, outwcs._naxis2], [0, 0]]) + plt.plot(outc[:,0], outc[:,1]) + for wobj in wcsobjects: + outcorners = outwcs.wcs_world2pix(wobj.calc_footprint(),1) + if plot: + plt.plot(outcorners[:,0], outcorners[:,1]) + objwcs = outwcs.deepcopy() + objwcs.wcs.crpix = objwcs.wcs.crpix - (outcorners[0]) + updatehdr(wobj.filename, objwcs,wkey=wkey, wcsname=wname, ext=wobj.extname, clobber=clobber) + return outwcs + +def updatehdr(fname, wcsobj, wkey, wcsname, ext=1, clobber=False): + hdr = fits.getheader(fname, ext=ext) + all_keys = list(string.ascii_uppercase) + if wkey.upper() not in all_keys: + raise KeyError("wkey must be one character: A-Z") + if wkey not in altwcs.available_wcskeys(hdr): + if not clobber: + raise ValueError("wkey %s is already in use. Use clobber=True to overwrite it or specify a different key." %wkey) + else: + altwcs.deleteWCS(fname, ext=ext, wcskey='V') + f = fits.open(fname, mode='update') + + hwcs = wcs2header(wcsobj) + wcsnamekey = 'WCSNAME' + wkey + f[ext].header[wcsnamekey] = wcsname + for k in hwcs: + f[ext].header[k[:7]+wkey] = hwcs[k] + + f.close() + +def wcs2header(wcsobj): + + h = wcsobj.to_header() + + if wcsobj.wcs.has_cd(): + altwcs.pc2cd(h) + h['CTYPE1'] = 'RA---TAN' + h['CTYPE2'] = 'DEC--TAN' + norient = np.rad2deg(np.arctan2(h['CD1_2'],h['CD2_2'])) + #okey = 'ORIENT%s' % wkey + okey = 'ORIENT' + h[okey] = norient + return h + +def readWCS(input, exts=None, extname=None): + if isinstance(input, str): + if input[0] == '@': + # input is an @ file + filelist = irafglob.irafglob(input) + else: + try: + filelist, output = parseinput.parseinput(input) + except IOError: raise + elif isinstance(input, list): + if isinstance(input[0], wcsutil.HSTWCS): + # a list of HSTWCS objects + return input + else: + filelist = input[:] + wcso = [] + fomited = [] + # figure out which FITS extension(s) to use + if exts == None and extname == None: + #Assume it's simple FITS and the data is in the primary HDU + for f in filelist: + try: + wcso = wcsutil.HSTWCS(f) + except AttributeError: + fomited.append(f) + continue + elif exts != None and validateExt(exts): + exts = [exts] + for f in filelist: + try: + wcso.extend([wcsutil.HSTWCS(f, ext=e) for e in exts]) + except KeyError: + fomited.append(f) + continue + elif extname != None: + for f in filelist: + fobj = fits.open(f) + for i in range(len(fobj)): + try: + ename = fobj[i].header['EXTNAME'] + except KeyError: + continue + if ename.lower() == extname.lower(): + wcso.append(wcsutil.HSTWCS(f,ext=i)) + else: + continue + fobj.close() + if fomited != []: + print("These files were skipped:") + for f in fomited: + print(f) + return wcso + + +def validateExt(ext): + if not isinstance(ext, int) and not isinstance(ext, tuple) \ + and not isinstance(ext, list): + print("Ext must be integer, tuple, a list of int extension numbers, \ + or a list of tuples representing a fits extension, for example ('sci', 1).") + return False + else: + return True + + + diff --git a/stwcs/wcsutil/wcscorr.py b/stwcs/wcsutil/wcscorr.py new file mode 100644 index 0000000..3f9b7d5 --- /dev/null +++ b/stwcs/wcsutil/wcscorr.py @@ -0,0 +1,668 @@ +from __future__ import absolute_import, division, print_function + +import os,copy +import numpy as np +from astropy.io import fits + +import stwcs +from stwcs.wcsutil import altwcs +from stwcs.updatewcs import utils +from stsci.tools import fileutil +from . import convertwcs + +DEFAULT_WCS_KEYS = ['CRVAL1','CRVAL2','CRPIX1','CRPIX2', + 'CD1_1','CD1_2','CD2_1','CD2_2', + 'CTYPE1','CTYPE2','ORIENTAT'] +DEFAULT_PRI_KEYS = ['HDRNAME','SIPNAME','NPOLNAME','D2IMNAME','DESCRIP'] +COL_FITSKW_DICT = {'RMS_RA':'sci.crder1','RMS_DEC':'sci.crder2', + 'NMatch':'sci.nmatch','Catalog':'sci.catalog'} + +### +### WCSEXT table related keyword archive functions +### +def init_wcscorr(input, force=False): + """ + This function will initialize the WCSCORR table if it is not already present, + and look for WCS keywords with a prefix of 'O' as the original OPUS + generated WCS as the initial row for the table or use the current WCS + keywords as initial row if no 'O' prefix keywords are found. + + This function will NOT overwrite any rows already present. + + This function works on all SCI extensions at one time. + """ + # TODO: Create some sort of decorator or (for Python2.5) context for + # opening a FITS file and closing it when done, if necessary + if not isinstance(input, fits.HDUList): + # input must be a filename, so open as `astropy.io.fits.HDUList` object + fimg = fits.open(input, mode='update') + need_to_close = True + else: + fimg = input + need_to_close = False + + # Do not try to generate a WCSCORR table for a simple FITS file + numsci = fileutil.countExtn(fimg) + if len(fimg) == 1 or numsci == 0: + return + + enames = [] + for e in fimg: enames.append(e.name) + if 'WCSCORR' in enames: + if not force: + return + else: + del fimg['wcscorr'] + print('Initializing new WCSCORR table for ',fimg.filename()) + + used_wcskeys = altwcs.wcskeys(fimg['SCI', 1].header) + + # define the primary columns of the WCSEXT table with initial rows for each + # SCI extension for the original OPUS solution + numwcs = len(used_wcskeys) + if numwcs == 0: numwcs = 1 + + # create new table with more rows than needed initially to make it easier to + # add new rows later + wcsext = create_wcscorr(descrip=True,numrows=numsci, padding=(numsci*numwcs) + numsci * 4) + # Assign the correct EXTNAME value to this table extension + wcsext.header['TROWS'] = (numsci * 2, 'Number of updated rows in table') + wcsext.header['EXTNAME'] = ('WCSCORR', 'Table with WCS Update history') + wcsext.header['EXTVER'] = 1 + + # define set of WCS keywords which need to be managed and copied to the table + wcs1 = stwcs.wcsutil.HSTWCS(fimg,ext=('SCI',1)) + idc2header = True + if wcs1.idcscale is None: + idc2header = False + wcs_keywords = list(wcs1.wcs2header(idc2hdr=idc2header).keys()) + + prihdr = fimg[0].header + prihdr_keys = DEFAULT_PRI_KEYS + pri_funcs = {'SIPNAME':stwcs.updatewcs.utils.build_sipname, + 'NPOLNAME':stwcs.updatewcs.utils.build_npolname, + 'D2IMNAME':stwcs.updatewcs.utils.build_d2imname} + + # Now copy original OPUS values into table + for extver in range(1, numsci + 1): + rowind = find_wcscorr_row(wcsext.data, + {'WCS_ID': 'OPUS', 'EXTVER': extver, + 'WCS_key':'O'}) + # There should only EVER be a single row for each extension with OPUS values + rownum = np.where(rowind)[0][0] + #print 'Archiving OPUS WCS in row number ',rownum,' in WCSCORR table for SCI,',extver + + hdr = fimg['SCI', extver].header + # define set of WCS keywords which need to be managed and copied to the table + if used_wcskeys is None: + used_wcskeys = altwcs.wcskeys(hdr) + # Check to see whether or not there is an OPUS alternate WCS present, + # if so, get its values directly, otherwise, archive the PRIMARY WCS + # as the OPUS values in the WCSCORR table + if 'O' not in used_wcskeys: + altwcs.archiveWCS(fimg,('SCI',extver),wcskey='O', wcsname='OPUS') + wkey = 'O' + + wcs = stwcs.wcsutil.HSTWCS(fimg, ext=('SCI', extver), wcskey=wkey) + wcshdr = wcs.wcs2header(idc2hdr=idc2header) + + if wcsext.data.field('CRVAL1')[rownum] != 0: + # If we find values for these keywords already in the table, do not + # overwrite them again + print('WCS keywords already updated...') + break + for key in wcs_keywords: + if key in wcsext.data.names: + wcsext.data.field(key)[rownum] = wcshdr[(key+wkey)[:8]] + # Now get any keywords from PRIMARY header needed for WCS updates + for key in prihdr_keys: + if key in prihdr: + val = prihdr[key] + else: + val = '' + wcsext.data.field(key)[rownum] = val + + # Now that we have archived the OPUS alternate WCS, remove it from the list + # of used_wcskeys + if 'O' in used_wcskeys: + used_wcskeys.remove('O') + + # Now copy remaining alternate WCSs into table + # TODO: Much of this appears to be redundant with update_wcscorr; consider + # merging them... + for uwkey in used_wcskeys: + for extver in range(1, numsci + 1): + hdr = fimg['SCI', extver].header + wcs = stwcs.wcsutil.HSTWCS(fimg, ext=('SCI', extver), + wcskey=uwkey) + wcshdr = wcs.wcs2header() + if 'WCSNAME' + uwkey not in wcshdr: + wcsid = utils.build_default_wcsname(fimg[0].header['idctab']) + else: + wcsid = wcshdr['WCSNAME' + uwkey] + + # identify next empty row + rowind = find_wcscorr_row(wcsext.data, + selections={'wcs_id':['','0.0']}) + rows = np.where(rowind) + if len(rows[0]) > 0: + rownum = np.where(rowind)[0][0] + else: + print('No available rows found for updating. ') + + # Update selection columns for this row with relevant values + wcsext.data.field('WCS_ID')[rownum] = wcsid + wcsext.data.field('EXTVER')[rownum] = extver + wcsext.data.field('WCS_key')[rownum] = uwkey + + # Look for standard WCS keyword values + for key in wcs_keywords: + if key in wcsext.data.names: + wcsext.data.field(key)[rownum] = wcshdr[key + uwkey] + # Now get any keywords from PRIMARY header needed for WCS updates + for key in prihdr_keys: + if key in pri_funcs: + val = pri_funcs[key](fimg)[0] + else: + if key in prihdr: + val = prihdr[key] + else: + val = '' + wcsext.data.field(key)[rownum] = val + + # Append this table to the image FITS file + fimg.append(wcsext) + # force an update now + # set the verify flag to 'warn' so that it will always succeed, but still + # tell the user if PyFITS detects any problems with the file as a whole + utils.updateNEXTENDKw(fimg) + + fimg.flush('warn') + + if need_to_close: + fimg.close() + + +def find_wcscorr_row(wcstab, selections): + """ + Return an array of indices from the table (NOT HDU) 'wcstab' that matches the + selections specified by the user. + + The row selection criteria must be specified as a dictionary with + column name as key and value(s) representing the valid desired row values. + For example, {'wcs_id':'OPUS','extver':2}. + """ + + mask = None + for i in selections: + icol = wcstab.field(i) + if isinstance(icol,np.chararray): icol = icol.rstrip() + selecti = selections[i] + if not isinstance(selecti,list): + if isinstance(selecti,str): + selecti = selecti.rstrip() + bmask = (icol == selecti) + if mask is None: + mask = bmask.copy() + else: + mask = np.logical_and(mask,bmask) + del bmask + else: + for si in selecti: + if isinstance(si,str): + si = si.rstrip() + bmask = (icol == si) + if mask is None: + mask = bmask.copy() + else: + mask = np.logical_or(mask,bmask) + del bmask + + return mask + + +def archive_wcs_file(image, wcs_id=None): + """ + Update WCSCORR table with rows for each SCI extension to record the + newly updated WCS keyword values. + """ + + if not isinstance(image, fits.HDUList): + fimg = fits.open(image, mode='update') + close_image = True + else: + fimg = image + close_image = False + + update_wcscorr(fimg, wcs_id=wcs_id) + + if close_image: + fimg.close() + + +def update_wcscorr(dest, source=None, extname='SCI', wcs_id=None, active=True): + """ + Update WCSCORR table with a new row or rows for this extension header. It + copies the current set of WCS keywords as a new row of the table based on + keyed WCSs as per Paper I Multiple WCS standard). + + Parameters + ---------- + dest : HDUList + The HDU list whose WCSCORR table should be appended to (the WCSCORR HDU + must already exist) + source : HDUList, optional + The HDU list containing the extension from which to extract the WCS + keywords to add to the WCSCORR table. If None, the dest is also used + as the source. + extname : str, optional + The extension name from which to take new WCS keywords. If there are + multiple extensions with that name, rows are added for each extension + version. + wcs_id : str, optional + The name of the WCS to add, as in the WCSNAMEa keyword. If + unspecified, all the WCSs in the specified extensions are added. + active: bool, optional + When True, indicates that the update should reflect an update of the + active WCS information, not just appending the WCS to the file as a + headerlet + """ + if not isinstance(dest, fits.HDUList): + dest = fits.open(dest,mode='update') + fname = dest.filename() + + if source is None: + source = dest + + if extname == 'PRIMARY': + return + + numext = fileutil.countExtn(source, extname) + if numext == 0: + raise ValueError('No %s extensions found in the source HDU list.' + % extname) + # Initialize the WCSCORR table extension in dest if not already present + init_wcscorr(dest) + try: + dest.index_of('WCSCORR') + except KeyError: + return + + # check to see whether or not this is an up-to-date table + # replace with newly initialized table with current format + old_table = dest['WCSCORR'] + wcscorr_cols = ['WCS_ID','EXTVER', 'SIPNAME', + 'HDRNAME', 'NPOLNAME', 'D2IMNAME'] + + for colname in wcscorr_cols: + if colname not in old_table.data.columns.names: + print("WARNING: Replacing outdated WCSCORR table...") + outdated_table = old_table.copy() + del dest['WCSCORR'] + init_wcscorr(dest) + old_table = dest['WCSCORR'] + break + + # Current implementation assumes the same WCS keywords are in each + # extension version; if this should not be assumed then this can be + # modified... + wcs_keys = altwcs.wcskeys(source[(extname, 1)].header) + wcs_keys = [kk for kk in wcs_keys if kk] + if ' ' not in wcs_keys: wcs_keys.append(' ') # Insure that primary WCS gets used + # apply logic for only updating WCSCORR table with specified keywords + # corresponding to the WCS with WCSNAME=wcs_id + if wcs_id is not None: + wnames = altwcs.wcsnames(source[(extname, 1)].header) + wkeys = [] + for letter in wnames: + if wnames[letter] == wcs_id: + wkeys.append(letter) + if len(wkeys) > 1 and ' ' in wkeys: + wkeys.remove(' ') + wcs_keys = wkeys + wcshdr = stwcs.wcsutil.HSTWCS(source, ext=(extname, 1)).wcs2header() + wcs_keywords = list(wcshdr.keys()) + + if 'O' in wcs_keys: + wcs_keys.remove('O') # 'O' is reserved for original OPUS WCS + + # create new table for hdr and populate it with the newly updated values + new_table = create_wcscorr(descrip=True,numrows=0, padding=len(wcs_keys)*numext) + prihdr = source[0].header + + # Get headerlet related keywords here + sipname, idctab = utils.build_sipname(source, fname, "None") + npolname, npolfile = utils.build_npolname(source, None) + d2imname, d2imfile = utils.build_d2imname(source, None) + if 'hdrname' in prihdr: + hdrname = prihdr['hdrname'] + else: + hdrname = '' + + idx = -1 + for wcs_key in wcs_keys: + for extver in range(1, numext + 1): + extn = (extname, extver) + if 'SIPWCS' in extname and not active: + tab_extver = 0 # Since it has not been added to the SCI header yet + else: + tab_extver = extver + hdr = source[extn].header + if 'WCSNAME'+wcs_key in hdr: + wcsname = hdr['WCSNAME' + wcs_key] + else: + wcsname = utils.build_default_wcsname(hdr['idctab']) + + selection = {'WCS_ID': wcsname, 'EXTVER': tab_extver, + 'SIPNAME':sipname, 'HDRNAME': hdrname, + 'NPOLNAME': npolname, 'D2IMNAME':d2imname + } + + # Ensure that an entry for this WCS is not already in the dest + # table; if so just skip it + rowind = find_wcscorr_row(old_table.data, selection) + if np.any(rowind): + continue + + idx += 1 + + wcs = stwcs.wcsutil.HSTWCS(source, ext=extn, wcskey=wcs_key) + wcshdr = wcs.wcs2header() + + # Update selection column values + for key, val in selection.items(): + if key in new_table.data.names: + new_table.data.field(key)[idx] = val + + for key in wcs_keywords: + if key in new_table.data.names: + new_table.data.field(key)[idx] = wcshdr[key + wcs_key] + + for key in DEFAULT_PRI_KEYS: + if key in new_table.data.names and key in prihdr: + new_table.data.field(key)[idx] = prihdr[key] + # Now look for additional, non-WCS-keyword table column data + for key in COL_FITSKW_DICT: + fitkw = COL_FITSKW_DICT[key] + # Interpret any 'pri.hdrname' or + # 'sci.crpix1' formatted keyword names + if '.' in fitkw: + srchdr,fitkw = fitkw.split('.') + if 'pri' in srchdr.lower(): srchdr = prihdr + else: srchdr = source[extn].header + else: + srchdr = source[extn].header + + if fitkw+wcs_key in srchdr: + new_table.data.field(key)[idx] = srchdr[fitkw+wcs_key] + + + # If idx was never incremented, no rows were added, so there's nothing else + # to do... + if idx < 0: + return + + # Now, we need to merge this into the existing table + rowind = find_wcscorr_row(old_table.data, {'wcs_id':['','0.0']}) + old_nrows = np.where(rowind)[0][0] + new_nrows = new_table.data.shape[0] + + # check to see if there is room for the new row + if (old_nrows + new_nrows) > old_table.data.shape[0]-1: + pad_rows = 2 * new_nrows + # if not, create a new table with 'pad_rows' new empty rows + upd_table = fits.new_table(old_table.columns,header=old_table.header, + nrows=old_table.data.shape[0]+pad_rows) + else: + upd_table = old_table + pad_rows = 0 + # Now, add + for name in old_table.columns.names: + if name in new_table.data.names: + # reset the default values to ones specific to the row definitions + for i in range(pad_rows): + upd_table.data.field(name)[old_nrows+i] = old_table.data.field(name)[-1] + # Now populate with values from new table + upd_table.data.field(name)[old_nrows:old_nrows + new_nrows] = \ + new_table.data.field(name) + upd_table.header['TROWS'] = old_nrows + new_nrows + + # replace old extension with newly updated table extension + dest['WCSCORR'] = upd_table + + +def restore_file_from_wcscorr(image, id='OPUS', wcskey=''): + """ Copies the values of the WCS from the WCSCORR based on ID specified by user. + The default will be to restore the original OPUS-derived values to the Primary WCS. + If wcskey is specified, the WCS with that key will be updated instead. + """ + + if not isinstance(image, fits.HDUList): + fimg = fits.open(image, mode='update') + close_image = True + else: + fimg = image + close_image = False + numsci = fileutil.countExtn(fimg) + wcs_table = fimg['WCSCORR'] + orig_rows = (wcs_table.data.field('WCS_ID') == 'OPUS') + # create an HSTWCS object to figure out what WCS keywords need to be updated + wcsobj = stwcs.wcsutil.HSTWCS(fimg,ext=('sci',1)) + wcshdr = wcsobj.wcs2header() + for extn in range(1,numsci+1): + # find corresponding row from table + ext_rows = (wcs_table.data.field('EXTVER') == extn) + erow = np.where(np.logical_and(ext_rows,orig_rows))[0][0] + for key in wcshdr: + if key in wcs_table.data.names: # insure that keyword is column in table + tkey = key + + if 'orient' in key.lower(): + key = 'ORIENT' + if wcskey == '': + skey = key + else: + skey = key[:7]+wcskey + fimg['sci',extn].header[skey] = wcs_table.data.field(tkey)[erow] + for key in DEFAULT_PRI_KEYS: + if key in wcs_table.data.names: + if wcskey == '': + pkey = key + else: + pkey = key[:7]+wcskey + fimg[0].header[pkey] = wcs_table.data.field(key)[erow] + + utils.updateNEXTENDKw(fimg) + + # close the image now that the update has been completed. + if close_image: + fimg.close() + + +def create_wcscorr(descrip=False, numrows=1, padding=0): + """ + Return the basic definitions for a WCSCORR table. + The dtype definitions for the string columns are set to the maximum allowed so + that all new elements will have the same max size which will be automatically + truncated to this limit upon updating (if needed). + + The table is initialized with rows corresponding to the OPUS solution + for all the 'SCI' extensions. + """ + + trows = numrows + padding + # define initialized arrays as placeholders for column data + # TODO: I'm certain there's an easier way to do this... for example, simply + # define the column names and formats, then create an empty array using + # them as a dtype, then create the new table from that array. + def_float64_zeros = np.array([0.0] * trows, dtype=np.float64) + def_float64_ones = def_float64_zeros + 1.0 + def_float_col = {'format': 'D', 'array': def_float64_zeros.copy()} + def_float1_col = {'format': 'D', 'array':def_float64_ones.copy()} + def_str40_col = {'format': '40A', + 'array': np.array([''] * trows, dtype='S40')} + def_str24_col = {'format': '24A', + 'array': np.array([''] * trows, dtype='S24')} + def_int32_col = {'format': 'J', + 'array': np.array([0]*trows,dtype=np.int32)} + + # If more columns are needed, simply add their definitions to this list + col_names = [('HDRNAME', def_str24_col), ('SIPNAME', def_str24_col), + ('NPOLNAME', def_str24_col), ('D2IMNAME', def_str24_col), + ('CRVAL1', def_float_col), ('CRVAL2', def_float_col), + ('CRPIX1', def_float_col), ('CRPIX2', def_float_col), + ('CD1_1', def_float_col), ('CD1_2', def_float_col), + ('CD2_1', def_float_col), ('CD2_2', def_float_col), + ('CTYPE1', def_str24_col), ('CTYPE2', def_str24_col), + ('ORIENTAT', def_float_col), ('PA_V3', def_float_col), + ('RMS_RA', def_float_col), ('RMS_Dec', def_float_col), + ('NMatch', def_int32_col), ('Catalog', def_str40_col)] + + # Define selector columns + id_col = fits.Column(name='WCS_ID', format='40A', + array=np.array(['OPUS'] * numrows + [''] * padding, + dtype='S24')) + extver_col = fits.Column(name='EXTVER', format='I', + array=np.array(list(range(1, numrows + 1)), + dtype=np.int16)) + wcskey_col = fits.Column(name='WCS_key', format='A', + array=np.array(['O'] * numrows + [''] * padding, + dtype='S')) + # create list of remaining columns to be added to table + col_list = [id_col, extver_col, wcskey_col] # start with selector columns + + for c in col_names: + cdef = copy.deepcopy(c[1]) + col_list.append(fits.Column(name=c[0], format=cdef['format'], + array=cdef['array'])) + + if descrip: + col_list.append( + fits.Column(name='DESCRIP', format='128A', + array=np.array( + ['Original WCS computed by OPUS'] * numrows, + dtype='S128'))) + + # Now create the new table from the column definitions + newtab = fits.new_table(fits.ColDefs(col_list), nrows=trows) + # The fact that setting .name is necessary should be considered a bug in + # pyfits. + # TODO: Make sure this is fixed in pyfits, then remove this + newtab.name = 'WCSCORR' + + return newtab + +def delete_wcscorr_row(wcstab,selections=None,rows=None): + """ + Sets all values in a specified row or set of rows to default values + + This function will essentially erase the specified row from the table + without actually removing the row from the table. This avoids the problems + with trying to resize the number of rows in the table while preserving the + ability to update the table with new rows again without resizing the table. + + Parameters + ---------- + wcstab: object + PyFITS binTable object for WCSCORR table + selections: dict + Dictionary of wcscorr column names and values to be used to select + the row or set of rows to erase + rows: int, list + If specified, will specify what rows from the table to erase regardless + of the value of 'selections' + """ + + if selections is None and rows is None: + print('ERROR: Some row selection information must be provided!') + print(' Either a row numbers or "selections" must be provided.') + raise ValueError + + delete_rows = None + if rows is None: + if 'wcs_id' in selections and selections['wcs_id'] == 'OPUS': + delete_rows = None + print('WARNING: OPUS WCS information can not be deleted from WCSCORR table.') + print(' This row will not be deleted!') + else: + rowind = find_wcscorr_row(wcstab, selections=selections) + delete_rows = np.where(rowind)[0].tolist() + else: + if not isinstance(rows,list): + rows = [rows] + delete_rows = rows + + # Insure that rows pointing to OPUS WCS do not get deleted, even by accident + for row in delete_rows: + if wcstab['WCS_key'][row] == 'O' or wcstab['WCS_ID'][row] == 'OPUS': + del delete_rows[delete_rows.index(row)] + + if delete_rows is None: + return + + # identify next empty row + rowind = find_wcscorr_row(wcstab, selections={'wcs_id':['','0.0']}) + last_blank_row = np.where(rowind)[0][-1] + + # copy values from blank row into user-specified rows + for colname in wcstab.names: + wcstab[colname][delete_rows] = wcstab[colname][last_blank_row] + +def update_wcscorr_column(wcstab, column, values, selections=None, rows=None): + """ + Update the values in 'column' with 'values' for selected rows + + Parameters + ---------- + wcstab: object + PyFITS binTable object for WCSCORR table + column: string + Name of table column with values that need to be updated + values: string, int, or list + Value or set of values to copy into the selected rows for the column + selections: dict + Dictionary of wcscorr column names and values to be used to select + the row or set of rows to erase + rows: int, list + If specified, will specify what rows from the table to erase regardless + of the value of 'selections' + """ + if selections is None and rows is None: + print('ERROR: Some row selection information must be provided!') + print(' Either a row numbers or "selections" must be provided.') + raise ValueError + + if not isinstance(values, list): + values = [values] + + update_rows = None + if rows is None: + if 'wcs_id' in selections and selections['wcs_id'] == 'OPUS': + update_rows = None + print('WARNING: OPUS WCS information can not be deleted from WCSCORR table.') + print(' This row will not be deleted!') + else: + rowind = find_wcscorr_row(wcstab, selections=selections) + update_rows = np.where(rowind)[0].tolist() + else: + if not isinstance(rows,list): + rows = [rows] + update_rows = rows + + if update_rows is None: + return + + # Expand single input value to apply to all selected rows + if len(values) > 1 and len(values) < len(update_rows): + print('ERROR: Number of new values',len(values)) + print(' does not match number of rows',len(update_rows),' to be updated!') + print(' Please enter either 1 value or the same number of values') + print(' as there are rows to be updated.') + print(' Table will not be updated...') + raise ValueError + + if len(values) == 1 and len(values) < len(update_rows): + values = values * len(update_rows) + # copy values from blank row into user-specified rows + for row in update_rows: + wcstab[column][row] = values[row] diff --git a/stwcs/wcsutil/wcsdiff.py b/stwcs/wcsutil/wcsdiff.py new file mode 100644 index 0000000..cfc2d66 --- /dev/null +++ b/stwcs/wcsutil/wcsdiff.py @@ -0,0 +1,150 @@ +from __future__ import print_function +from astropy import wcs as pywcs +from collections import OrderedDict +from astropy.io import fits +from .headerlet import parse_filename +import numpy as np + +def is_wcs_identical(scifile, file2, sciextlist, fextlist, scikey=" ", + file2key=" ", verbose=False): + """ + Compares the WCS solution of 2 files. + + Parameters + ---------- + scifile: string + name of file1 (usually science file) + IRAF style extension syntax is accepted as well + for example scifile[1] or scifile[sci,1] + file2: string + name of second file (for example headerlet) + sciextlist - list + a list of int or tuple ('SCI', 1), extensions in the first file + fextlist - list + a list of int or tuple ('SIPWCS', 1), extensions in the second file + scikey: string + alternate WCS key in scifile + file2key: string + alternate WCS key in file2 + verbose: boolean + True: print to stdout + + Notes + ----- + These can be 2 science observations or 2 headerlets + or a science observation and a headerlet. The two files + have the same WCS solution if the following are the same: + + - rootname/destim + - primary WCS + - SIP coefficients + - NPOL distortion + - D2IM correction + + """ + result = True + diff = OrderedDict() + fobj, fname, close_file = parse_filename(file2) + sciobj, sciname, close_scifile = parse_filename(scifile) + diff['file_names'] = [scifile, file2] + if get_rootname(scifile) != get_rootname(file2): + #logger.info('Rootnames do not match.') + diff['rootname'] = ("%s: %s", "%s: %s") % (sciname, get_rootname(scifile), file2, get_rootname(file2)) + result = False + for i, j in zip(sciextlist, fextlist): + w1 = pywcs.WCS(sciobj[i].header, sciobj, key=scikey) + w2 = pywcs.WCS(fobj[j].header, fobj, key=file2key) + diff['extension'] = [get_extname_extnum(sciobj[i]), get_extname_extnum(fobj[j])] + if not np.allclose(w1.wcs.crval, w2.wcs.crval, rtol=10**(-7)): + #logger.info('CRVALs do not match') + diff['CRVAL'] = w1.wcs.crval, w2.wcs.crval + result = False + if not np.allclose(w1.wcs.crpix, w2.wcs.crpix, rtol=10**(-7)): + #logger.info('CRPIX do not match') + diff ['CRPIX'] = w1.wcs.crpix, w2.wcs.crpix + result = False + if not np.allclose(w1.wcs.cd, w2.wcs.cd, rtol=10**(-7)): + #logger.info('CDs do not match') + diff ['CD'] = w1.wcs.cd, w2.wcs.cd + result = False + if not (np.array(w1.wcs.ctype) == np.array(w2.wcs.ctype)).all(): + #logger.info('CTYPEs do not match') + diff ['CTYPE'] = w1.wcs.ctype, w2.wcs.ctype + result = False + if w1.sip or w2.sip: + if (w2.sip and not w1.sip) or (w1.sip and not w2.sip): + diff['sip'] = 'one sip extension is missing' + result = False + if not np.allclose(w1.sip.a, w2.sip.a, rtol=10**(-7)): + diff['SIP_A'] = 'SIP_A differ' + result = False + if not np.allclose(w1.sip.b, w2.sip.b, rtol=10**(-7)): + #logger.info('SIP coefficients do not match') + diff ['SIP_B'] = (w1.sip.b, w2.sip.b) + result = False + if w1.cpdis1 or w2.cpdis1: + if w1.cpdis1 and not w2.cpdis1 or w2.cpdis1 and not w1.cpdis1: + diff['CPDIS1'] = "CPDIS1 missing" + result=False + if w1.cpdis2 and not w2.cpdis2 or w2.cpdis2 and not w1.cpdis2: + diff['CPDIS2'] = "CPDIS2 missing" + result = False + if not np.allclose(w1.cpdis1.data, w2.cpdis1.data, rtol=10**(-7)): + #logger.info('NPOL distortions do not match') + diff ['CPDIS1_data'] = (w1.cpdis1.data, w2.cpdis1.data) + result = False + if not np.allclose(w1.cpdis2.data, w2.cpdis2.data, rtol=10**(-7)): + #logger.info('NPOL distortions do not match') + diff ['CPDIS2_data'] = (w1.cpdis2.data, w2.cpdis2.data) + result = False + if w1.det2im1 or w2.det2im1: + if w1.det2im1 and not w2.det2im1 or \ + w2.det2im1 and not w1.det2im1: + diff['DET2IM'] = "Det2im1 missing" + result = False + if not np.allclose(w1.det2im1.data, w2.det2im1.data, rtol=10**(-7)): + #logger.info('Det2Im corrections do not match') + diff ['D2IM1_data'] = (w1.det2im1.data, w2.det2im1.data) + result = False + if w1.det2im2 or w2.det2im2: + if w1.det2im2 and not w2.det2im2 or \ + w2.det2im2 and not w1.det2im2: + diff['DET2IM2'] = "Det2im2 missing" + result = False + if not np.allclose(w1.det2im2.data, w2.det2im2.data, rtol=10**(-7)): + #logger.info('Det2Im corrections do not match') + diff ['D2IM2_data'] = (w1.det2im2.data, w2.det2im2.data) + result = False + if not result and verbose: + for key in diff: + print(key, ":\t", diff[key][0], "\t", diff[key][1]) + if close_file: + fobj.close() + if close_scifile: + sciobj.close() + return result, diff + +def get_rootname(fname): + """ + Returns the value of ROOTNAME or DESTIM + """ + + hdr = fits.getheader(fname) + try: + rootname = hdr['ROOTNAME'] + except KeyError: + try: + rootname = hdr['DESTIM'] + except KeyError: + rootname = fname + return rootname + +def get_extname_extnum(ext): + """ + Return (EXTNAME, EXTNUM) of a FITS extension + """ + extname = "" + extnum=1 + extname = ext.header.get('EXTNAME', extname) + extnum = ext.header.get('EXTVER', extnum) + return (extname, extnum) |