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