LSSTApplications  16.0-11-g09ed895+2,16.0-11-g12e47bd,16.0-11-g9bb73b2+6,16.0-12-g5c924a4+6,16.0-14-g9a974b3+1,16.0-15-g1417920+1,16.0-15-gdd5ca33+1,16.0-16-gf0259e2,16.0-17-g31abd91+7,16.0-17-g7d7456e+7,16.0-17-ga3d2e9f+13,16.0-18-ga4d4bcb+1,16.0-18-gd06566c+1,16.0-2-g0febb12+21,16.0-2-g9d5294e+69,16.0-2-ga8830df+6,16.0-20-g21842373+7,16.0-24-g3eae5ec,16.0-28-gfc9ea6c+4,16.0-29-ge8801f9,16.0-3-ge00e371+34,16.0-4-g18f3627+13,16.0-4-g5f3a788+20,16.0-4-ga3eb747+10,16.0-4-gabf74b7+29,16.0-4-gb13d127+6,16.0-49-g42e581f7+6,16.0-5-g27fb78a+7,16.0-5-g6a53317+34,16.0-5-gb3f8a4b+87,16.0-6-g9321be7+4,16.0-6-gcbc7b31+42,16.0-6-gf49912c+29,16.0-7-gd2eeba5+51,16.0-71-ge89f8615e,16.0-8-g21fd5fe+29,16.0-8-g3a9f023+20,16.0-8-g4734f7a+1,16.0-8-g5858431+3,16.0-9-gf5c1f43+8,master-gd73dc1d098+1,w.2019.01
LSSTDataManagementBasePackage
fitsUtils.py
Go to the documentation of this file.
1 import re
2 import warnings
3 
4 import lsst.geom
5 import lsst.afw.image as afwImage
6 import lsst.afw.table as afwTable
7 from lsst.afw.fits import readMetadata
8 import lsst.afw.cameraGeom as afwCameraGeom
9 import lsst.afw.geom
10 
11 
12 def getByKey(metadata, key):
13  """Wrapper for getting a value from a metadata object by key.
14  @param[in] metadata metadata object to query for value
15  @param[in] key key to use for value lookup
16  @returns value associated with key, None if key does not exist
17  """
18  mdKeys = metadata.paramNames()
19  if key in mdKeys:
20  return metadata.getScalar(key)
21  else:
22  return None
23 
24 
25 def setByKey(metadata, key, value, clobber):
26  """Wrapper for setting a value in a metadata object. Deals with case
27  where the key already exists.
28  @param[in, out] metadata metadata object ot modify in place.
29  @param[in] key key to associate with value
30  @param[in] value value to assign in the metadata object
31  @param[in] clobber Clobber the value if the key already exisists?
32  """
33  mdKeys = metadata.paramNames()
34  if key not in mdKeys or (key in mdKeys and clobber):
35  metadata.set(key, value)
36 
37 
38 class HeaderMap(dict):
39  """ Class to hold mapping of header cards to attributes"""
40 
41  def addEntry(self, keyname, attribute_name, default=None, transform=lambda x: x):
42  """Adds an entry to the registr
43  @param[in] keyname Key used to retrieve the header record
44  @param[in] attribute_name Name of the attribute to store the value in
45  @param[jn] default Default velue to store if the header card is not available
46  @param[in] transform Transform to apply to the header value before assigning it to the
47  attribute.
48  """
49  self.__setitem__(attribute_name, {'keyName': keyname,
50  'default': default,
51  'transform': transform})
52 
53  def setAttributes(self, obj, metadata, doRaise=True):
54  """Sets the attributes on the give object given a metadata object.
55  @param[in, out] obj Object on which to operate in place
56  @param[in] metadata Metadata object used for applying the mapping
57  @param[in] doRaise Raise exceptions on calling methods on the
58  input object that do not exist?
59  """
60  for key, attrDict in self.items():
61  try:
62  value = getByKey(metadata, attrDict['keyName'])
63  # if attrDict['keyName'] == "RDCRNR" and value == 0:
64  # import ipdb; ipdb.set_trace()
65  if value is not None:
66  self._applyVal(obj, value, key, attrDict['transform'])
67  else:
68  # Only apply transform if the metadata has a value for this key
69  # otherwise assume the default value is transformed.
70  value = attrDict['default']
71  if value is not None:
72  self._applyVal(obj, value, key, lambda x: x)
73  except Exception as e:
74  if doRaise:
75  raise
76  else:
77  warnings.warn('WARNING: Failed to set %s attribute with %s value: %s' %
78  (key, value, str(e)))
79 
80  def _applyVal(self, obj, value, attrName, transform):
81  raise NotImplementedError('Must be implemented in sub-class')
82 
83 
85  """ Class to hold mapping of header cards to AmpInfoTable attributes
86  The amp info is stored using setters, thus calling the attribute as a function.
87  """
88 
89  def _applyVal(self, obj, value, attrName, transform):
90  getattr(obj, attrName)(transform(value))
91 
92 
94  """ Class to hold mapping of header cards to Detector attributes
95  Detector information is stored as attributes on a Config object.
96  """
97 
98  def _applyVal(self, obj, value, attrName, transform):
99  obj.__setattr__(attrName, transform(value))
100 
101 
103  def __init__(self, detectorFileName, ampFileNameList, inAmpCoords=True, plateScale=1.,
104  radialCoeffs=(0., 1.), clobberMetadata=False, doRaise=True):
105  ''' @param[in] detectorFileName FITS file containing the detector description.
106  May use [] notation to specify an extension in an MEF.
107  @param[in] ampFileNameList List of FITS file names to use in building the amps.
108  May contain duplicate entries if the raw data are assembled.
109  @param[in] inAmpCoords Boolean, True if raw data are in amp coordinates, False if raw data
110  are assembled into pseudo detector pixel arrays
111  @param[in] plateScale Nominal platescale (arcsec/mm)
112  @param[in] radialCoeffs Radial distortion coefficients for a radial polynomial in normalized
113  units.
114  @param[in] clobberMetadata Clobber metadata from input files if
115  overridden in the _sanitizeMetadata method
116  @param[in] doRaise Raise exception if not all non-defaulted
117  keywords are defined? Default is True.
118  '''
119  self.inAmpCoords = inAmpCoords
122  self.detectorMetadata = readMetadata(detectorFileName)
124  self.detectorMetadata, clobber=clobberMetadata)
125  self.ampMetadataList = []
126  self.detector = None
127  self.doRaise = doRaise
128  for fileName in ampFileNameList:
129  self.ampMetadataList.append(readMetadata(fileName))
131  self.ampMetadataList[-1], clobber=clobberMetadata)
132  self.plateScale = plateScale
133  self.focalPlaneToField = self._makeRadialTransform(radialCoeffs)
134 
135  def _sanitizeHeaderMetadata(self, metadata, clobber):
136  """This method is called for all metadata and gives an opportunity to add/modify
137  header information for use downstream.
138  Override this method if more than the default is needed.
139  @param[in, out] metadata Metadata to read/modify
140  @param[in] clobber Clobber keys that exist with default keys?
141  """
142  self._defaultSanitization(metadata, clobber)
143 
144  def _defaultSanitization(self, metadata, clobber):
145  """Does the default sanitization of the header metadata.
146  @param[in,out] metadata Header metadata to extend/modify
147  @param[in] clobber Override values in existing header cards?
148  """
149 
150  if self.inAmpCoords:
151  # Deal with DTM to get flipX and flipY for assembly and add as 'FLIPX', 'FLIPY'
152  # The DTM array is a transformation matrix. As I understand it, it transforms between
153  # electronic and assembled coordintates. As such, a negative value in the DTM1_1 spot
154  # corresponds to a flip of the x-axis and a negative value in the DTM2_2 spot
155  # corresponds to a flip of the y-axis.
156  dtm1 = getByKey(metadata, 'DTM1_1')
157  dtm2 = getByKey(metadata, 'DTM2_2')
158  if dtm1 is not None and dtm2 is not None:
159  setByKey(metadata, 'FLIPX', dtm1 < 0, clobber)
160  setByKey(metadata, 'FLIPY', dtm2 < 0, clobber)
161  setByKey(metadata, 'RDCRNR', int(
162  afwTable.ReadoutCorner.LL), clobber)
163  else:
164  setByKey(metadata, 'FLIPX', False, clobber)
165  setByKey(metadata, 'FLIPY', True, clobber)
166  # I don't know how to figure out the read corner if already
167  # assembled
168  setByKey(metadata, 'RDCRNR', None, clobber)
169 
170  # Deal with NAXIS1, NAXIS2 to make rawBBox as 'RAWBBOX'
171  xext = getByKey(metadata, 'NAXIS1')
172  yext = getByKey(metadata, 'NAXIS2')
173  if xext is not None and yext is not None:
174  setByKey(metadata, 'RAWBBOX', '[%i:%i,%i:%i]'%(
175  1, xext, 1, yext), clobber)
176  # Deal with DTV1, DTV2 to make 'XYOFF
177  dtv1 = getByKey(metadata, 'DTV1')
178  dtv2 = getByKey(metadata, 'DTV2')
179  if dtv1 is not None and dtv2 is not None:
180  setByKey(metadata, 'XYOFF', [dtv1, dtv2], clobber)
181  # map biassec[1] to HOSCAN
182  # map biassec[3] to VOSCAN
183  # map biassec[2] to PRESCAN
184  if metadata.isArray('BIASSEC'):
185  keylist = ['HOSCAN', 'PRESCAN', 'VOSCAN']
186  biassecs = getByKey(metadata, 'BIASSEC')
187  for i, biassec in enumerate(biassecs):
188  setByKey(metadata, keylist[i], biassec, clobber)
189  else:
190  biassec = getByKey(metadata, 'BIASSEC')
191  if biassec is not None:
192  setByKey(metadata, 'HOSCAN', biassec, clobber)
193 
194  def _makeDefaultAmpMap(self):
195  """Make the default map from header information to amplifier information
196  @return The HeaderAmpMap object containing the mapping
197  """
198  hMap = HeaderAmpMap()
199  emptyBBox = lsst.geom.BoxI()
200  mapList = [('EXTNAME', 'setName'),
201  ('DETSEC', 'setBBox', None, self._makeBbox),
202  ('GAIN', 'setGain', 1.),
203  ('RDNOISE', 'setReadNoise', 0.),
204  ('SATURATE', 'setSaturation', 2 << 15),
205  ('RDCRNR', 'setReadoutCorner', int(
206  afwTable.ReadoutCorner.LL), afwTable.ReadoutCorner),
207  ('LINCOEFF', 'setLinearityCoeffs', [0., 1.]),
208  ('LINTYPE', 'setLinearityType', 'POLY'),
209  ('RAWBBOX', 'setRawBBox', None, self._makeBbox),
210  ('DATASEC', 'setRawDataBBox', None, self._makeBbox),
211  ('FLIPX', 'setRawFlipX', False),
212  ('FLIPY', 'setRawFlipY', False),
213  ('XYOFF', 'setRawXYOffset', lsst.geom.ExtentI(0, 0), self._makeExt),
214  ('HOSCAN', 'setRawHorizontalOverscanBBox',
215  emptyBBox, self._makeBbox),
216  ('VOSCAN', 'setRawVerticalOverscanBBox',
217  emptyBBox, self._makeBbox),
218  ('PRESCAN', 'setRawPrescanBBox', emptyBBox, self._makeBbox),
219  ]
220  for tup in mapList:
221  hMap.addEntry(*tup)
222  return hMap
223 
224  def _makeDefaultDetectorMap(self):
225  """Make the default map from header information to detector information
226  @return The HeaderDetectorMap object containing the mapping
227  """
228  hMap = HeaderDetectorMap()
229  mapList = [('CCDNAME', 'name', 'ccdName'),
230  ('DETSIZE', 'bbox_x0', 0, self._getBboxX0),
231  ('DETSIZE', 'bbox_y0', 0, self._getBboxY0),
232  ('DETSIZE', 'bbox_x1', 0, self._getBboxX1),
233  ('DETSIZE', 'bbox_y1', 0, self._getBboxY1),
234  ('DETID', 'id', 0),
235  # DetectorConfig.detectorType is of type `int`, not
236  # `DetectorType`
237  ('OBSTYPE', 'detectorType', int(
238  afwCameraGeom.DetectorType.SCIENCE)),
239  ('SERSTR', 'serial', 'none'),
240  ('XPOS', 'offset_x', 0.),
241  ('YPOS', 'offset_y', 0.),
242  ('XPIX', 'refpos_x', 0.),
243  ('YPIX', 'refpos_y', 0.),
244  ('YAWDEG', 'yawDeg', 0.),
245  ('PITCHDEG', 'pitchDeg', 0.),
246  ('ROLLDEG', 'rollDeg', 0.),
247  ('XPIXSIZE', 'pixelSize_x', 1.),
248  ('YPIXSIZE', 'pixelSize_y', 1.),
249  ('TRNSPOSE', 'transposeDetector', False),
250  ]
251  for tup in mapList:
252  hMap.addEntry(*tup)
253  return hMap
254 
255  def _makeExt(self, extArr):
256  """Helper function to make an extent from an array
257  @param[in] extArr Length 2 array to use in creating the Extent object
258  @return Extent2I constructed from the input list
259  """
260  return lsst.geom.ExtentI(*extArr)
261 
262  def _makeBbox(self, boxString):
263  """Helper funtion to make a bounding box from a string representing a FITS style bounding box
264  @param[in] boxString String describing the bounding box
265  @return Box2I for the bounding box
266  """
267  # strip off brackets and split into parts
268  x1, x2, y1, y2 = [int(el) for el in re.split(
269  '[:,]', boxString.strip()[1:-1])]
270  box = lsst.geom.BoxI(lsst.geom.PointI(x1, y1), lsst.geom.PointI(x2, y2))
271  # account for the difference between FITS convention and LSST convention for
272  # index of LLC.
273  box.shift(lsst.geom.Extent2I(-1, -1))
274  return box
275 
276  def _getBboxX0(self, boxString):
277  return self._makeBbox(boxString).getMinX()
278 
279  def _getBboxX1(self, boxString):
280  return self._makeBbox(boxString).getMaxX()
281 
282  def _getBboxY0(self, boxString):
283  return self._makeBbox(boxString).getMinY()
284 
285  def _getBboxY1(self, boxString):
286  return self._makeBbox(boxString).getMaxY()
287 
288  def _makeRadialTransform(self, radialCoeffs):
289  """Helper function to get the radial transform given the radial polynomial coefficients given in
290  the constructor.
291  @param[in] radialCoeffs List of coefficients describing a polynomial radial distortion in
292  normalized units.
293  @return Transform object describing the radial distortion
294  """
295  pScaleRad = lsst.geom.arcsecToRad(self.plateScale)
296  return lsst.afw.geom.makeRadialTransform([el/pScaleRad for el in radialCoeffs])
297 
298  def buildDetector(self):
299  """Take all the information and build a Detector object. The Detector object is necessary for doing
300  things like assembly.
301  @return Detector object
302  """
303  if self.detector is not None:
304  return self.detector
305 
306  schema = afwTable.AmpInfoTable.makeMinimalSchema()
307  ampInfo = afwTable.AmpInfoCatalog(schema)
308  for ampMetadata in self.ampMetadataList:
309  record = ampInfo.addNew()
310  self.defaultAmpMap.setAttributes(record, ampMetadata, self.doRaise)
311  record.setHasRawInfo(True)
312 
313  detConfig = afwCameraGeom.DetectorConfig()
314  self.defaultDetectorMap.setAttributes(
315  detConfig, self.detectorMetadata, self.doRaise)
316  self.detector = afwCameraGeom.makeDetector(
317  detConfig, ampInfo, self.focalPlaneToField)
318  return self.detector
319 
320  def makeCalib(self):
321  """PLaceholder for subclasses to implement construction of a calib to associate with the exposure.
322  @return empty afwImage.Calib object
323  """
324  return afwImage.Calib()
325 
326  def makeExposure(self, im, mask=None, variance=None):
327  """Method for constructing an exposure object from an image and the information contained in this
328  class to construct the Detector and Calib objects.
329  @param[in] im Image used to construct the exposure
330  @param[in] mask Optional mask plane as a <askU
331  @param[in] variance Optional variance plance as an image of the same type as im
332  @param[out] Exposure object
333  """
334  if mask is None:
335  mask = afwImage.Mask(im.getDimensions())
336  if variance is None:
337  variance = im
338  mi = afwImage.makeMaskedImage(im, mask, variance)
339  detector = self.buildDetector()
340 
341  calib = self.makeCalib()
342  exp = afwImage.makeExposure(mi)
343  exp.setCalib(calib)
344  exp.setDetector(detector)
345  return exp
def makeExposure(self, im, mask=None, variance=None)
Definition: fitsUtils.py:326
def _makeRadialTransform(self, radialCoeffs)
Definition: fitsUtils.py:288
def addEntry(self, keyname, attribute_name, default=None, transform=lambda x:x)
Definition: fitsUtils.py:41
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
Describe an exposure&#39;s calibration.
Definition: Calib.h:95
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT >> image, typename std::shared_ptr< Mask< MaskPixelT >> mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT >> variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
Definition: MaskedImage.h:1280
def _defaultSanitization(self, metadata, clobber)
Definition: fitsUtils.py:144
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.
Definition: Exposure.h:446
def _sanitizeHeaderMetadata(self, metadata, clobber)
Definition: fitsUtils.py:135
Represent a 2-dimensional array of bitmask pixels.
Definition: Mask.h:78
def getByKey(metadata, key)
Definition: fitsUtils.py:12
std::shared_ptr< TransformPoint2ToPoint2 > makeRadialTransform(std::vector< double > const &forwardCoeffs, std::vector< double > const &inverseCoeffs)
A purely radial polynomial distortion.
def _applyVal(self, obj, value, attrName, transform)
Definition: fitsUtils.py:80
def setAttributes(self, obj, metadata, doRaise=True)
Definition: fitsUtils.py:53
constexpr double arcsecToRad(double x) noexcept
Definition: Angle.h:55
An integer coordinate rectangle.
Definition: Box.h:54
def __init__(self, detectorFileName, ampFileNameList, inAmpCoords=True, plateScale=1., radialCoeffs=(0., 1.), clobberMetadata=False, doRaise=True)
Definition: fitsUtils.py:104
def setByKey(metadata, key, value, clobber)
Definition: fitsUtils.py:25