LSSTApplications  11.0-13-gbb96280,12.1.rc1,12.1.rc1+1,12.1.rc1+2,12.1.rc1+5,12.1.rc1+8,12.1.rc1-1-g06d7636+1,12.1.rc1-1-g253890b+5,12.1.rc1-1-g3d31b68+7,12.1.rc1-1-g3db6b75+1,12.1.rc1-1-g5c1385a+3,12.1.rc1-1-g83b2247,12.1.rc1-1-g90cb4cf+6,12.1.rc1-1-g91da24b+3,12.1.rc1-2-g3521f8a,12.1.rc1-2-g39433dd+4,12.1.rc1-2-g486411b+2,12.1.rc1-2-g4c2be76,12.1.rc1-2-gc9c0491,12.1.rc1-2-gda2cd4f+6,12.1.rc1-3-g3391c73+2,12.1.rc1-3-g8c1bd6c+1,12.1.rc1-3-gcf4b6cb+2,12.1.rc1-4-g057223e+1,12.1.rc1-4-g19ed13b+2,12.1.rc1-4-g30492a7
LSSTDataManagementBasePackage
snapCombine.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2016 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 import numpy as num
23 import lsst.pex.config as pexConfig
24 import lsst.daf.base as dafBase
25 import lsst.afw.image as afwImage
26 import lsst.afw.table as afwTable
27 import lsst.pipe.base as pipeBase
28 from lsstDebug import getDebugFrame
29 from lsst.afw.display import getDisplay
30 from lsst.coadd.utils import addToCoadd, setCoaddEdgeBits
31 from lsst.ip.diffim import SnapPsfMatchTask
32 from lsst.meas.algorithms import SourceDetectionTask
33 from lsst.meas.base import SingleFrameMeasurementTask
34 import lsst.meas.algorithms as measAlg
35 
36 from .repair import RepairTask
37 
38 class InitialPsfConfig(pexConfig.Config):
39  """!Describes the initial PSF used for detection and measurement before we do PSF determination."""
40 
41  model = pexConfig.ChoiceField(
42  dtype = str,
43  doc = "PSF model type",
44  default = "SingleGaussian",
45  allowed = {
46  "SingleGaussian": "Single Gaussian model",
47  "DoubleGaussian": "Double Gaussian model",
48  },
49  )
50  pixelScale = pexConfig.Field(
51  dtype = float,
52  doc = "Pixel size (arcsec). Only needed if no Wcs is provided",
53  default = 0.25,
54  )
55  fwhm = pexConfig.Field(
56  dtype = float,
57  doc = "FWHM of PSF model (arcsec)",
58  default = 1.0,
59  )
60  size = pexConfig.Field(
61  dtype = int,
62  doc = "Size of PSF model (pixels)",
63  default = 15,
64  )
65 
66 class SnapCombineConfig(pexConfig.Config):
67  doRepair = pexConfig.Field(
68  dtype = bool,
69  doc = "Repair images (CR reject and interpolate) before combining",
70  default = True,
71  )
72  repairPsfFwhm = pexConfig.Field(
73  dtype = float,
74  doc = "Psf FWHM (pixels) used to detect CRs",
75  default = 2.5,
76  )
77  doDiffIm = pexConfig.Field(
78  dtype = bool,
79  doc = "Perform difference imaging before combining",
80  default = False,
81  )
82  doPsfMatch = pexConfig.Field(
83  dtype = bool,
84  doc = "Perform PSF matching for difference imaging (ignored if doDiffIm false)",
85  default = True,
86  )
87  doMeasurement = pexConfig.Field(
88  dtype = bool,
89  doc = "Measure difference sources (ignored if doDiffIm false)",
90  default = True,
91  )
92  badMaskPlanes = pexConfig.ListField(
93  dtype = str,
94  doc = "Mask planes that, if set, the associated pixels are not included in the combined exposure; "
95  "DETECTED excludes cosmic rays",
96  default = ("DETECTED",),
97  )
98  averageKeys = pexConfig.ListField(
99  dtype = str,
100  doc = "List of float metadata keys to average when combining snaps, e.g. float positions and dates; "
101  "non-float data must be handled by overriding the fixMetadata method",
102  optional = True,
103 
104  )
105  sumKeys = pexConfig.ListField(
106  dtype = str,
107  doc = "List of float or int metadata keys to sum when combining snaps, e.g. exposure time; "
108  "non-float, non-int data must be handled by overriding the fixMetadata method",
109  optional = True,
110  )
111 
112  repair = pexConfig.ConfigurableField(target = RepairTask, doc = "")
113  diffim = pexConfig.ConfigurableField(target = SnapPsfMatchTask, doc = "")
114  detection = pexConfig.ConfigurableField(target = SourceDetectionTask, doc = "")
115  initialPsf = pexConfig.ConfigField(dtype = InitialPsfConfig, doc = "")
116  measurement = pexConfig.ConfigurableField(target = SingleFrameMeasurementTask, doc = "")
117 
118  def setDefaults(self):
119  self.detection.thresholdPolarity = "both"
120 
121  def validate(self):
122  if self.detection.thresholdPolarity != "both":
123  raise ValueError("detection.thresholdPolarity must be 'both' for SnapCombineTask")
124 
125 ## \addtogroup LSST_task_documentation
126 ## \{
127 ## \page SnapCombineTask
128 ## \ref SnapCombineTask_ "SnapCombineTask"
129 ## \copybrief SnapCombineTask
130 ## \}
131 
132 class SnapCombineTask(pipeBase.Task):
133  """!
134  \anchor SnapCombineTask_
135 
136  \brief Combine snaps.
137 
138  \section pipe_tasks_snapcombine_Contents Contents
139 
140  - \ref pipe_tasks_snapcombine_Debug
141 
142  \section pipe_tasks_snapcombine_Debug Debug variables
143 
144  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
145  flag \c -d to import \b debug.py from your \c PYTHONPATH; see <a
146  href="http://lsst-web.ncsa.illinois.edu/~buildbot/doxygen/x_masterDoxyDoc/base_debug.html">
147  Using lsstDebug to control debugging output</a> for more about \b debug.py files.
148 
149  The available variables in SnapCombineTask are:
150  <DL>
151  <DT> \c display
152  <DD> A dictionary containing debug point names as keys with frame number as value. Valid keys are:
153  <DL>
154  <DT> repair0
155  <DD> Display the first snap after repairing.
156  <DT> repair1
157  <DD> Display the second snap after repairing.
158  </DL>
159  </DD>
160  </DL>
161  """
162  ConfigClass = SnapCombineConfig
163  _DefaultName = "snapCombine"
164 
165  def __init__(self, *args, **kwargs):
166  pipeBase.Task.__init__(self, *args, **kwargs)
167  self.makeSubtask("repair")
168  self.makeSubtask("diffim")
169  self.schema = afwTable.SourceTable.makeMinimalSchema()
171  self.makeSubtask("detection", schema=self.schema)
172  if self.config.doMeasurement:
173  self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
174 
175  @pipeBase.timeMethod
176  def run(self, snap0, snap1, defects=None):
177  """Combine two snaps
178 
179  @param[in] snap0: snapshot exposure 0
180  @param[in] snap1: snapshot exposure 1
181  @defects[in] defect list (for repair task)
182  @return a pipe_base Struct with fields:
183  - exposure: snap-combined exposure
184  - sources: detected sources, or None if detection not performed
185  """
186  # initialize optional outputs
187  sources = None
188 
189  if self.config.doRepair:
190  self.log.info("snapCombine repair")
191  psf = self.makeInitialPsf(snap0, fwhmPix=self.config.repairPsfFwhm)
192  snap0.setPsf(psf)
193  snap1.setPsf(psf)
194  self.repair.run(snap0, defects=defects, keepCRs=False)
195  self.repair.run(snap1, defects=defects, keepCRs=False)
196 
197  repair0frame = getDebugFrame(self._display, "repair0")
198  if repair0frame:
199  getDisplay(repair0frame).mtv(snap0)
200  repair1frame = getDebugFrame(self._display, "repair1")
201  if repair1frame:
202  getDisplay(repair1frame).mtv(snap1)
203 
204  if self.config.doDiffIm:
205  if self.config.doPsfMatch:
206  self.log.info("snapCombine psfMatch")
207  diffRet = self.diffim.run(snap0, snap1, "subtractExposures")
208  diffExp = diffRet.subtractedImage
209 
210  # Measure centroid and width of kernel; dependent on ticket #1980
211  # Useful diagnostic for the degree of astrometric shift between snaps.
212  diffKern = diffRet.psfMatchingKernel
213  width, height = diffKern.getDimensions()
214  # TBD...
215  #psfAttr = measAlg.PsfAttributes(diffKern, width//2, height//2)
216 
217  else:
218  diffExp = afwImage.ExposureF(snap0, True)
219  diffMi = diffExp.getMaskedImage()
220  diffMi -= snap1.getMaskedImage()
221 
222  psf = self.makeInitialPsf(snap0)
223  diffExp.setPsf(psf)
224  table = afwTable.SourceTable.make(self.schema)
225  table.setMetadata(self.algMetadata)
226  detRet = self.detection.makeSourceCatalog(table, diffExp)
227  sources = detRet.sources
228  fpSets = detRet.fpSets
229  if self.config.doMeasurement:
230  self.measurement.measure(diffExp, sources)
231 
232  mask0 = snap0.getMaskedImage().getMask()
233  mask1 = snap1.getMaskedImage().getMask()
234  fpSets.positive.setMask(mask0, "DETECTED")
235  fpSets.negative.setMask(mask1, "DETECTED")
236 
237  maskD = diffExp.getMaskedImage().getMask()
238  fpSets.positive.setMask(maskD, "DETECTED")
239  fpSets.negative.setMask(maskD, "DETECTED_NEGATIVE")
240 
241  combinedExp = self.addSnaps(snap0, snap1)
242 
243  return pipeBase.Struct(
244  exposure = combinedExp,
245  sources = sources,
246  )
247 
248  def addSnaps(self, snap0, snap1):
249  """Add two snap exposures together, returning a new exposure
250 
251  @param[in] snap0 snap exposure 0
252  @param[in] snap1 snap exposure 1
253  @return combined exposure
254  """
255  self.log.info("snapCombine addSnaps")
256 
257  combinedExp = snap0.Factory(snap0, True)
258  combinedMi = combinedExp.getMaskedImage()
259  combinedMi.set(0)
260 
261  weightMap = combinedMi.getImage().Factory(combinedMi.getBBox())
262  weight = 1.0
263  badPixelMask = afwImage.MaskU.getPlaneBitMask(self.config.badMaskPlanes)
264  addToCoadd(combinedMi, weightMap, snap0.getMaskedImage(), badPixelMask, weight)
265  addToCoadd(combinedMi, weightMap, snap1.getMaskedImage(), badPixelMask, weight)
266 
267  # pre-scaling the weight map instead of post-scaling the combinedMi saves a bit of time
268  # because the weight map is a simple Image instead of a MaskedImage
269  weightMap *= 0.5 # so result is sum of both images, instead of average
270  combinedMi /= weightMap
271  setCoaddEdgeBits(combinedMi.getMask(), weightMap)
272 
273  # note: none of the inputs has a valid Calib object, so that is not touched
274  # Filter was already copied
275 
276  combinedMetadata = combinedExp.getMetadata()
277  metadata0 = snap0.getMetadata()
278  metadata1 = snap1.getMetadata()
279  self.fixMetadata(combinedMetadata, metadata0, metadata1)
280 
281  return combinedExp
282 
283  def fixMetadata(self, combinedMetadata, metadata0, metadata1):
284  """Fix the metadata of the combined exposure (in place)
285 
286  This implementation handles items specified by config.averageKeys and config.sumKeys,
287  which have data type restrictions. To handle other data types (such as sexagesimal
288  positions and ISO dates) you must supplement this method with your own code.
289 
290  @param[in,out] combinedMetadata metadata of combined exposure;
291  on input this is a deep copy of metadata0 (a PropertySet)
292  @param[in] metadata0 metadata of snap0 (a PropertySet)
293  @param[in] metadata1 metadata of snap1 (a PropertySet)
294 
295  @note the inputs are presently PropertySets due to ticket #2542. However, in some sense
296  they are just PropertyLists that are missing some methods. In particular: comments and order
297  are preserved if you alter an existing value with set(key, value).
298  """
299  keyDoAvgList = []
300  if self.config.averageKeys:
301  keyDoAvgList += [(key, 1) for key in self.config.averageKeys]
302  if self.config.sumKeys:
303  keyDoAvgList += [(key, 0) for key in self.config.sumKeys]
304  for key, doAvg in keyDoAvgList:
305  opStr = "average" if doAvg else "sum"
306  try:
307  val0 = metadata0.get(key)
308  val1 = metadata1.get(key)
309  except Exception:
310  self.log.warn("Could not %s metadata %r: missing from one or both exposures" % (opStr, key,))
311  continue
312 
313  try:
314  combinedVal = val0 + val1
315  if doAvg:
316  combinedVal /= 2.0
317  except Exception:
318  self.log.warn("Could not %s metadata %r: value %r and/or %r not numeric" % \
319  (opStr, key, val0, val1))
320  continue
321 
322  combinedMetadata.set(key, combinedVal)
323 
324  def makeInitialPsf(self, exposure, fwhmPix=None):
325  """Initialise the detection procedure by setting the PSF and WCS
326 
327  @param exposure Exposure to process
328  @return PSF, WCS
329  """
330  assert exposure, "No exposure provided"
331  wcs = exposure.getWcs()
332  assert wcs, "No wcs in exposure"
333 
334  if fwhmPix is None:
335  fwhmPix = self.config.initialPsf.fwhm / wcs.pixelScale().asArcseconds()
336 
337  size = self.config.initialPsf.size
338  model = self.config.initialPsf.model
339  self.log.info("installInitialPsf fwhm=%s pixels; size=%s pixels" % (fwhmPix, size))
340  psfCls = getattr(measAlg, model + "Psf")
341  psf = psfCls(size, size, fwhmPix/(2.0*num.sqrt(2*num.log(2.0))))
342  return psf
Class for storing ordered metadata with comments.
Definition: PropertyList.h:82
void setCoaddEdgeBits(lsst::afw::image::Mask< lsst::afw::image::MaskPixel > &coaddMask, lsst::afw::image::Image< WeightPixelT > const &weightMap)
set edge bits of coadd mask based on weight map
Describes the initial PSF used for detection and measurement before we do PSF determination.
Definition: snapCombine.py:38
lsst::afw::geom::Box2I addToCoadd(lsst::afw::image::Image< CoaddPixelT > &coadd, lsst::afw::image::Image< WeightPixelT > &weightMap, lsst::afw::image::Image< CoaddPixelT > const &image, WeightPixelT weight)
add good pixels from an image to a coadd and associated weight map
Definition: addToCoadd.cc:127
def getDebugFrame
Definition: lsstDebug.py:66