LSSTApplications  1.1.2+25,10.0+13,10.0+132,10.0+133,10.0+224,10.0+41,10.0+8,10.0-1-g0f53050+14,10.0-1-g4b7b172+19,10.0-1-g61a5bae+98,10.0-1-g7408a83+3,10.0-1-gc1e0f5a+19,10.0-1-gdb4482e+14,10.0-11-g3947115+2,10.0-12-g8719d8b+2,10.0-15-ga3f480f+1,10.0-2-g4f67435,10.0-2-gcb4bc6c+26,10.0-28-gf7f57a9+1,10.0-3-g1bbe32c+14,10.0-3-g5b46d21,10.0-4-g027f45f+5,10.0-4-g86f66b5+2,10.0-4-gc4fccf3+24,10.0-40-g4349866+2,10.0-5-g766159b,10.0-5-gca2295e+25,10.0-6-g462a451+1
LSSTDataManagementBasePackage
imageDifference.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # LSST Data Management System
4 # Copyright 2012 LSST Corporation.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <http://www.lsstcorp.org/LegalNotices/>.
22 #
23 import math
24 import random
25 import numpy
26 
27 import lsst.pex.config as pexConfig
28 import lsst.pipe.base as pipeBase
29 import lsst.daf.base as dafBase
30 import lsst.afw.geom as afwGeom
31 import lsst.afw.image as afwImage
32 import lsst.afw.math as afwMath
33 import lsst.afw.table as afwTable
34 import lsst.meas.astrom as measAstrom
35 from lsst.pipe.tasks.registerImage import RegisterTask
36 from lsst.meas.algorithms import SourceDetectionTask, SourceMeasurementTask, \
37  starSelectorRegistry, PsfAttributes, SingleGaussianPsf
38 from lsst.ip.diffim import ImagePsfMatchTask, DipoleMeasurementTask, DipoleAnalysis, \
39  SourceFlagChecker, KernelCandidateF, cast_KernelCandidateF, makeKernelBasisList, \
40  KernelCandidateQa, DiaCatalogSourceSelector, DiaCatalogSourceSelectorConfig
41 import lsst.ip.diffim.diffimTools as diffimTools
42 
43 FwhmPerSigma = 2 * math.sqrt(2 * math.log(2))
44 IqrToSigma = 0.741
45 
46 class ImageDifferenceConfig(pexConfig.Config):
47  """Config for ImageDifferenceTask
48  """
49  doAddCalexpBackground = pexConfig.Field(dtype=bool, default=True,
50  doc="Add background to calexp before processing it. Useful as ipDiffim does background matching.")
51  doUseRegister = pexConfig.Field(dtype=bool, default=True,
52  doc="Use image-to-image registration to align template with science image")
53  doDebugRegister = pexConfig.Field(dtype=bool, default=False,
54  doc="Writing debugging data for doUseRegister")
55  doSelectSources = pexConfig.Field(dtype=bool, default=True,
56  doc="Select stars to use for kernel fitting")
57  doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=False,
58  doc="Select stars of extreme color as part of the control sample")
59  doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=False,
60  doc="Select stars that are variable to be part of the control sample")
61  doSubtract = pexConfig.Field(dtype=bool, default=True, doc="Compute subtracted exposure?")
62  doPreConvolve = pexConfig.Field(dtype=bool, default=True,
63  doc="Convolve science image by its PSF before PSF-matching?")
64  useGaussianForPreConvolution = pexConfig.Field(dtype=bool, default=True,
65  doc="Use a simple gaussian PSF model for pre-convolution (else use fit PSF)? "
66  "Ignored if doPreConvolve false.")
67  doDetection = pexConfig.Field(dtype=bool, default=True, doc="Detect sources?")
68  doMerge = pexConfig.Field(dtype=bool, default=True,
69  doc="Merge positive and negative diaSources with grow radius set by growFootprint")
70  doMatchSources = pexConfig.Field(dtype=bool, default=True,
71  doc="Match diaSources with input calexp sources and ref catalog sources")
72  doMeasurement = pexConfig.Field(dtype=bool, default=True, doc="Measure diaSources?")
73  doWriteSubtractedExp = pexConfig.Field(dtype=bool, default=True, doc="Write difference exposure?")
74  doWriteMatchedExp = pexConfig.Field(dtype=bool, default=False,
75  doc="Write warped and PSF-matched template coadd exposure?")
76  doWriteSources = pexConfig.Field(dtype=bool, default=True, doc="Write sources?")
77  doAddMetrics = pexConfig.Field(dtype=bool, default=True,
78  doc="Add columns to the source table to hold analysis metrics?")
79 
80  coaddName = pexConfig.Field(
81  doc="coadd name: typically one of deep or goodSeeing",
82  dtype=str,
83  default="deep",
84  )
85  convolveTemplate = pexConfig.Field(
86  doc="Which image gets convolved (default = template)",
87  dtype=bool,
88  default=True
89  )
90  sourceSelector = starSelectorRegistry.makeField("Source selection algorithm", default="diacatalog")
91  subtract = pexConfig.ConfigurableField(
92  target=ImagePsfMatchTask,
93  doc="Warp and PSF match template to exposure, then subtract",
94  )
95  detection = pexConfig.ConfigurableField(
96  target=SourceDetectionTask,
97  doc="Low-threshold detection for final measurement",
98  )
99  dipoleMeasurement = pexConfig.ConfigurableField(
100  target=DipoleMeasurementTask,
101  doc="Final source measurement on low-threshold detections; dipole fitting enabled.",
102  )
103  measurement = pexConfig.ConfigurableField(
104  target=SourceMeasurementTask,
105  doc="Final source measurement on low-threshold detections; dipole fitting NOT enabled",
106  )
107  controlStepSize = pexConfig.Field(
108  doc="What step size (every Nth one) to select a control sample from the kernelSources",
109  dtype=int,
110  default=5
111  )
112  controlRandomSeed = pexConfig.Field(
113  doc="Random seed for shuffing the control sample",
114  dtype=int,
115  default=10
116  )
117  register = pexConfig.ConfigurableField(
118  target=RegisterTask,
119  doc="Task to enable image-to-image image registration (warping)",
120  )
121 
122  templateBorderSize = pexConfig.Field(dtype=int, default=10,
123  doc="Number of pixels to grow the requested template image to account for warping")
124 
125  templateSipOrder = pexConfig.Field(dtype=int, default=2,
126  doc="Sip Order for fitting the Template Wcs (default is too high, overfitting)")
127 
128  growFootprint = pexConfig.Field(dtype=int, default=2,
129  doc="Grow positive and negative footprints by this amount before merging")
130 
131  diaSourceMatchRadius = pexConfig.Field(dtype=float, default=0.5,
132  doc="Match radius (in arcseconds) for DiaSource to Source association")
133 
134  maxDiaSourcesToMeasure = pexConfig.Field(dtype=int, default=200,
135  doc="Do not measure more than this many sources with dipoleMeasurement, use measurement")
136 
137  def setDefaults(self):
138  # Set default source selector and configure defaults for that one and some common alternatives
139  self.sourceSelector.name = "diacatalog"
140  self.sourceSelector["secondMoment"].clumpNSigma = 2.0
141  # defaults are OK for catalog and diacatalog
142 
143  # DiaSource Detection
144  self.detection.thresholdPolarity = "both"
145  self.detection.reEstimateBackground = False
146  self.detection.thresholdType = "pixel_stdev"
147 
148  # Add filtered flux measurement, the correct measurement for pre-convolved images.
149  # Enable all measurements, regardless of doPreConvolved, as it makes data harvesting easier.
150  # To change that you must modify algorithms.names in the task's applyOverrides method,
151  # after the user has set doPreConvolved.
152  self.measurement.algorithms.names.add("flux.peakLikelihood")
153  self.dipoleMeasurement.algorithms.names.add("flux.peakLikelihood")
154 
155  # For shuffling the control sample
156  random.seed(self.controlRandomSeed)
157 
158  def validate(self):
159  pexConfig.Config.validate(self)
160  if self.doMeasurement and not self.doDetection:
161  raise ValueError("Cannot run source measurement without source detection.")
162  if self.doMerge and not self.doDetection:
163  raise ValueError("Cannot run source merging without source detection.")
164 
165 class ImageDifferenceTask(pipeBase.CmdLineTask):
166  """Subtract an image from a template coadd and measure the result
167  """
168  ConfigClass = ImageDifferenceConfig
169  _DefaultName = "imageDifference"
170 
171  def __init__(self, **kwargs):
172  pipeBase.CmdLineTask.__init__(self, **kwargs)
173  self.makeSubtask("subtract")
174 
175  if self.config.doUseRegister:
176  self.makeSubtask("register")
177 
178  if self.config.doSelectSources:
179  self.sourceSelector = self.config.sourceSelector.apply()
180  self.schema = afwTable.SourceTable.makeMinimalSchema()
182  if self.config.doDetection:
183  self.makeSubtask("detection", schema=self.schema)
184  if self.config.doMeasurement:
185  self.makeSubtask("dipoleMeasurement", schema=self.schema, algMetadata=self.algMetadata)
186  self.makeSubtask("measurement", schema=afwTable.SourceTable.makeMinimalSchema(),
187  algMetadata=self.algMetadata)
188  self.schema.addField(self.dipoleMeasurement._ClassificationFlag, "F",
189  "probability of being a dipole")
190 
191  if self.config.doMatchSources:
192  self.schema.addField("refMatchId", "L", "unique id of reference catalog match")
193  self.schema.addField("srcMatchId", "L", "unique id of source match")
194 
195  @pipeBase.timeMethod
196  def run(self, sensorRef):
197  """Subtract an image from a template coadd and measure the result
198 
199  Steps include:
200  - warp template coadd to match WCS of image
201  - PSF match image to warped template
202  - subtract image from PSF-matched, warped template
203  - persist difference image
204  - detect sources
205  - measure sources
206 
207  @param sensorRef: sensor-level butler data reference, used for the following data products:
208  Input only:
209  - calexp
210  - psf
211  - ccdExposureId
212  - ccdExposureId_bits
213  - self.config.coaddName + "Coadd_skyMap"
214  - self.config.coaddName + "Coadd"
215  Input or output, depending on config:
216  - self.config.coaddName + "Diff_subtractedExp"
217  Output, depending on config:
218  - self.config.coaddName + "Diff_matchedExp"
219  - self.config.coaddName + "Diff_src"
220 
221  @return pipe_base Struct containing these fields:
222  - subtractedExposure: exposure after subtracting template;
223  the unpersisted version if subtraction not run but detection run
224  None if neither subtraction nor detection run (i.e. nothing useful done)
225  - subtractRes: results of subtraction task; None if subtraction not run
226  - sources: detected and possibly measured sources; None if detection not run
227  """
228  self.log.info("Processing %s" % (sensorRef.dataId))
229 
230  # initialize outputs and some intermediate products
231  subtractedExposure = None
232  subtractRes = None
233  selectSources = None
234  kernelSources = None
235  controlSources = None
236  diaSources = None
237 
238  # We make one IdFactory that will be used by both icSrc and src datasets;
239  # I don't know if this is the way we ultimately want to do things, but at least
240  # this ensures the source IDs are fully unique.
241  expBits = sensorRef.get("ccdExposureId_bits")
242  expId = long(sensorRef.get("ccdExposureId"))
243  idFactory = afwTable.IdFactory.makeSource(expId, 64 - expBits)
244 
245  # Retrieve the science image we wish to analyze
246  exposure = sensorRef.get("calexp", immediate=True)
247  if self.config.doAddCalexpBackground:
248  mi = exposure.getMaskedImage()
249  mi += sensorRef.get("calexpBackground").getImage()
250  if not exposure.hasPsf():
251  raise pipeBase.TaskError("Exposure has no psf")
252  sciencePsf = exposure.getPsf()
253 
254  subtractedExposureName = self.config.coaddName + "Diff_differenceExp"
255  templateExposure = None # Stitched coadd exposure
256  templateSources = None # Sources on the template image
257  if self.config.doSubtract:
258  templateExposure, templateSources = self.getTemplate(exposure, sensorRef)
259 
260  # compute scienceSigmaOrig: sigma of PSF of science image before pre-convolution
261  ctr = afwGeom.Box2D(exposure.getBBox()).getCenter()
262  psfAttr = PsfAttributes(sciencePsf, afwGeom.Point2I(ctr))
263  scienceSigmaOrig = psfAttr.computeGaussianWidth(psfAttr.ADAPTIVE_MOMENT)
264 
265  # sigma of PSF of template image before warping
266  ctr = afwGeom.Box2D(templateExposure.getBBox()).getCenter()
267  psfAttr = PsfAttributes(templateExposure.getPsf(), afwGeom.Point2I(ctr))
268  templateSigma = psfAttr.computeGaussianWidth(psfAttr.ADAPTIVE_MOMENT)
269 
270  # if requested, convolve the science exposure with its PSF
271  # (properly, this should be a cross-correlation, but our code does not yet support that)
272  # compute scienceSigmaPost: sigma of science exposure with pre-convolution, if done,
273  # else sigma of original science exposure
274  if self.config.doPreConvolve:
275  convControl = afwMath.ConvolutionControl()
276  # cannot convolve in place, so make a new MI to receive convolved image
277  srcMI = exposure.getMaskedImage()
278  destMI = srcMI.Factory(srcMI.getDimensions())
279  srcPsf = sciencePsf
280  if self.config.useGaussianForPreConvolution:
281  # convolve with a simplified PSF model: a double Gaussian
282  kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
283  preConvPsf = SingleGaussianPsf(kWidth, kHeight, scienceSigmaOrig)
284  else:
285  # convolve with science exposure's PSF model
286  preConvPsf = srcPsf
287  afwMath.convolve(destMI, srcMI, preConvPsf.getLocalKernel(), convControl)
288  exposure.setMaskedImage(destMI)
289  scienceSigmaPost = scienceSigmaOrig * math.sqrt(2)
290  else:
291  scienceSigmaPost = scienceSigmaOrig
292 
293  # If requested, find sources in the image
294  if self.config.doSelectSources:
295  if not sensorRef.datasetExists("src"):
296  self.log.warn("Src product does not exist; running detection, measurement, selection")
297  # Run own detection and measurement; necessary in nightly processing
298  selectSources = self.subtract.getSelectSources(
299  exposure,
300  sigma = scienceSigmaPost,
301  doSmooth = not self.doPreConvolve,
302  idFactory = idFactory,
303  )
304  else:
305  self.log.info("Source selection via src product")
306  # Sources already exist; for data release processing
307  selectSources = sensorRef.get("src")
308 
309  # Number of basis functions
310  nparam = len(makeKernelBasisList(self.subtract.config.kernel.active,
311  referenceFwhmPix=scienceSigmaPost * FwhmPerSigma,
312  targetFwhmPix=templateSigma * FwhmPerSigma))
313 
314  if self.config.doAddMetrics:
315  # Modify the schema of all Sources
316  kcQa = KernelCandidateQa(nparam)
317  selectSources = kcQa.addToSchema(selectSources)
318 
319  astrometer = measAstrom.Astrometry(measAstrom.MeasAstromConfig())
320  astromRet = astrometer.useKnownWcs(selectSources, exposure=exposure)
321  matches = astromRet.matches
322  kernelSources = self.sourceSelector.selectSources(exposure, selectSources, matches=matches)
323  random.shuffle(kernelSources, random.random)
324  controlSources = kernelSources[::self.config.controlStepSize]
325  kernelSources = [k for i,k in enumerate(kernelSources) if i % self.config.controlStepSize]
326 
327  if self.config.doSelectDcrCatalog:
328  redSelector = DiaCatalogSourceSelector(
329  DiaCatalogSourceSelectorConfig(grMin=self.sourceSelector.config.grMax, grMax=99.999))
330  redSources = redSelector.selectSources(exposure, selectSources, matches=matches)
331  controlSources.extend(redSources)
332 
333  blueSelector = DiaCatalogSourceSelector(
334  DiaCatalogSourceSelectorConfig(grMin=-99.999, grMax=self.sourceSelector.config.grMin))
335  blueSources = blueSelector.selectSources(exposure, selectSources, matches=matches)
336  controlSources.extend(blueSources)
337 
338  if self.config.doSelectVariableCatalog:
339  varSelector = DiaCatalogSourceSelector(
340  DiaCatalogSourceSelectorConfig(includeVariable=True))
341  varSources = varSelector.selectSources(exposure, selectSources, matches=matches)
342  controlSources.extend(varSources)
343 
344  self.log.info("Selected %d / %d sources for Psf matching (%d for control sample)"
345  % (len(kernelSources), len(selectSources), len(controlSources)))
346  allresids = {}
347  if self.config.doUseRegister:
348  self.log.info("Registering images")
349 
350  if templateSources is None:
351  # Run detection on the template, which is
352  # temporarily background-subtracted
353  templateSources = self.subtract.getSelectSources(
354  templateExposure,
355  sigma=templateSigma,
356  doSmooth=True,
357  idFactory=idFactory
358  )
359 
360  # Third step: we need to fit the relative astrometry.
361  #
362  wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
363  warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs,
364  exposure.getWcs(), exposure.getBBox())
365  templateExposure = warpedExp
366 
367  # Create debugging outputs on the astrometric
368  # residuals as a function of position. Persistence
369  # not yet implemented; expected on (I believe) #2636.
370  if self.config.doDebugRegister:
371  # Grab matches to reference catalog
372  srcToMatch = {x.second.getId() : x.first for x in matches}
373 
374  refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
375  inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidKey()
376  sids = [m.first.getId() for m in wcsResults.matches]
377  positions = [m.first.get(refCoordKey) for m in wcsResults.matches]
378  residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
379  m.second.get(inCentroidKey))) for m in wcsResults.matches]
380  allresids = dict(zip(sids, zip(positions, residuals)))
381 
382  cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
383  wcsResults.wcs.pixelToSky(
384  m.second.get(inCentroidKey))) for m in wcsResults.matches]
385  colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get("g"))
386  + 2.5*numpy.log10(srcToMatch[x].get("r"))
387  for x in sids if x in srcToMatch.keys()])
388  dlong = numpy.array([r[0].asArcseconds() for s,r in zip(sids, cresiduals)
389  if s in srcToMatch.keys()])
390  dlat = numpy.array([r[1].asArcseconds() for s,r in zip(sids, cresiduals)
391  if s in srcToMatch.keys()])
392  idx1 = numpy.where(colors<self.sourceSelector.config.grMin)
393  idx2 = numpy.where((colors>=self.sourceSelector.config.grMin)&
394  (colors<=self.sourceSelector.config.grMax))
395  idx3 = numpy.where(colors>self.sourceSelector.config.grMax)
396  rms1Long = IqrToSigma*(numpy.percentile(dlong[idx1],75)-numpy.percentile(dlong[idx1],25))
397  rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1],75)-numpy.percentile(dlat[idx1],25))
398  rms2Long = IqrToSigma*(numpy.percentile(dlong[idx2],75)-numpy.percentile(dlong[idx2],25))
399  rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2],75)-numpy.percentile(dlat[idx2],25))
400  rms3Long = IqrToSigma*(numpy.percentile(dlong[idx3],75)-numpy.percentile(dlong[idx3],25))
401  rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3],75)-numpy.percentile(dlat[idx3],25))
402  self.log.info("Blue star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx1]),
403  rms1Long,
404  numpy.median(dlat[idx1]),
405  rms1Lat))
406  self.log.info("Green star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx2]),
407  rms2Long,
408  numpy.median(dlat[idx2]),
409  rms2Lat))
410  self.log.info("Red star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx3]),
411  rms3Long,
412  numpy.median(dlat[idx3]),
413  rms3Lat))
414 
415  self.metadata.add("RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
416  self.metadata.add("RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
417  self.metadata.add("RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
418  self.metadata.add("RegisterBlueLongOffsetStd", rms1Long)
419  self.metadata.add("RegisterGreenLongOffsetStd", rms2Long)
420  self.metadata.add("RegisterRedLongOffsetStd", rms3Long)
421 
422  self.metadata.add("RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
423  self.metadata.add("RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
424  self.metadata.add("RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
425  self.metadata.add("RegisterBlueLatOffsetStd", rms1Lat)
426  self.metadata.add("RegisterGreenLatOffsetStd", rms2Lat)
427  self.metadata.add("RegisterRedLatOffsetStd", rms3Lat)
428 
429  # warp template exposure to match exposure,
430  # PSF match template exposure to exposure,
431  # then return the difference
432 
433  #Return warped template... Construct sourceKernelCand list after subtract
434  self.log.info("Subtracting images")
435  subtractRes = self.subtract.subtractExposures(
436  templateExposure=templateExposure,
437  scienceExposure=exposure,
438  scienceFwhmPix=scienceSigmaPost * FwhmPerSigma,
439  templateFwhmPix=templateSigma * FwhmPerSigma,
440  candidateList=kernelSources,
441  convolveTemplate=self.config.convolveTemplate,
442  doWarping=not self.config.doUseRegister
443  )
444  subtractedExposure = subtractRes.subtractedExposure
445 
446  if self.config.doWriteMatchedExp:
447  sensorRef.put(subtractRes.matchedExposure, self.config.coaddName + "Diff_matchedExp")
448 
449  if self.config.doDetection:
450  self.log.info("Running diaSource detection")
451  if subtractedExposure is None:
452  subtractedExposure = sensorRef.get(subtractedExposureName)
453 
454  # Get Psf from the appropriate input image if it doesn't exist
455  if not subtractedExposure.hasPsf():
456  if self.config.convolveTemplate:
457  subtractedExposure.setPsf(exposure.getPsf())
458  else:
459  if templateExposure is None:
460  templateExposure, templateSources = self.getTemplate(exposure, sensorRef)
461  subtractedExposure.setPsf(templateExposure.getPsf())
462 
463  # Erase existing detection mask planes
464  mask = subtractedExposure.getMaskedImage().getMask()
465  mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
466 
467  table = afwTable.SourceTable.make(self.schema, idFactory)
468  table.setMetadata(self.algMetadata)
469  results = self.detection.makeSourceCatalog(
470  table=table,
471  exposure=subtractedExposure,
472  doSmooth=not self.config.doPreConvolve
473  )
474 
475  if self.config.doMerge:
476  fpSet = results.fpSets.positive
477  fpSet.merge(results.fpSets.negative, self.config.growFootprint,
478  self.config.growFootprint, False)
479  diaSources = afwTable.SourceCatalog(table)
480  fpSet.makeSources(diaSources)
481  self.log.info("Merging detections into %d sources" % (len(diaSources)))
482  else:
483  diaSources = results.sources
484 
485  if self.config.doMeasurement:
486  if len(diaSources) < self.config.maxDiaSourcesToMeasure:
487  self.log.info("Running diaSource dipole measurement")
488  self.dipoleMeasurement.run(subtractedExposure, diaSources)
489  else:
490  self.log.info("Running diaSource measurement")
491  self.measurement.run(subtractedExposure, diaSources)
492 
493  # Match with the calexp sources if possible
494  if self.config.doMatchSources:
495  if sensorRef.datasetExists("src"):
496  # Create key,val pair where key=diaSourceId and val=sourceId
497  matchRadAsec = self.config.diaSourceMatchRadius
498  matchRadPixel = matchRadAsec / exposure.getWcs().pixelScale().asArcseconds()
499 
500  srcMatches = afwTable.matchXy(sensorRef.get("src"), diaSources, matchRadPixel, True)
501  srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId()) for
502  srcMatch in srcMatches])
503  self.log.info("Matched %d / %d diaSources to sources" % (len(srcMatchDict),
504  len(diaSources)))
505  else:
506  self.log.warn("Src product does not exist; cannot match with diaSources")
507  srcMatchDict = {}
508 
509  # Create key,val pair where key=diaSourceId and val=refId
510  astrometer = measAstrom.Astrometry(measAstrom.MeasAstromConfig(catalogMatchDist=matchRadAsec))
511  astromRet = astrometer.useKnownWcs(diaSources, exposure=exposure)
512  refMatches = astromRet.matches
513  if refMatches is None:
514  self.log.warn("No diaSource matches with reference catalog")
515  refMatchDict = {}
516  else:
517  self.log.info("Matched %d / %d diaSources to reference catalog" % (len(refMatches),
518  len(diaSources)))
519  refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId()) for \
520  refMatch in refMatches])
521 
522  # Assign source Ids
523  for diaSource in diaSources:
524  sid = diaSource.getId()
525  if srcMatchDict.has_key(sid):
526  diaSource.set("srcMatchId", srcMatchDict[sid])
527  if refMatchDict.has_key(sid):
528  diaSource.set("refMatchId", refMatchDict[sid])
529 
530  if diaSources is not None and self.config.doWriteSources:
531  sensorRef.put(diaSources, self.config.coaddName + "Diff_diaSrc")
532 
533  if self.config.doAddMetrics and self.config.doSelectSources:
534  self.log.info("Evaluating metrics and control sample")
535 
536  kernelCandList = []
537  for cell in subtractRes.kernelCellSet.getCellList():
538  for cand in cell.begin(False): # include bad candidates
539  kernelCandList.append(cast_KernelCandidateF(cand))
540 
541  # Get basis list to build control sample kernels
542  basisList = afwMath.cast_LinearCombinationKernel(
543  kernelCandList[0].getKernel(KernelCandidateF.ORIG)).getKernelList()
544 
545  controlCandList = \
546  diffimTools.sourceTableToCandidateList(controlSources,
547  subtractRes.warpedExposure, exposure,
548  self.config.subtract.kernel.active,
549  self.config.subtract.kernel.active.detectionConfig,
550  self.log, doBuild=True, basisList=basisList)
551 
552  kcQa.apply(kernelCandList, subtractRes.psfMatchingKernel, subtractRes.backgroundModel,
553  dof=nparam)
554  kcQa.apply(controlCandList, subtractRes.psfMatchingKernel, subtractRes.backgroundModel)
555 
556  if self.config.doDetection:
557  kcQa.aggregate(selectSources, self.metadata, allresids, diaSources)
558  else:
559  kcQa.aggregate(selectSources, self.metadata, allresids)
560 
561  sensorRef.put(selectSources, self.config.coaddName + "Diff_kernelSrc")
562 
563  if self.config.doWriteSubtractedExp:
564  sensorRef.put(subtractedExposure, subtractedExposureName)
565 
566  self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
567  return pipeBase.Struct(
568  subtractedExposure=subtractedExposure,
569  subtractRes=subtractRes,
570  sources=diaSources,
571  )
572 
573  # One problem is that the SIP fits are w.r.t. CRPIX,
574  # and these coadd patches have the CRPIX of the entire
575  # tract, i.e. off the image. This causes
576  # register.fitWcs to fail. A workaround for now is to
577  # re-fit the Wcs which returns with a CRPIX that is on
578  # the image, and *then* to fit for the relative Wcs.
579  #
580  def fitAstrometry(self, templateSources, templateExposure, selectSources):
581  """Fit the relative astrometry between templateSources and selectSources"""
582  sipOrder = self.config.templateSipOrder
583  astrometer = measAstrom.Astrometry(measAstrom.MeasAstromConfig(sipOrder=sipOrder))
584  newWcs = astrometer.determineWcs(templateSources, templateExposure).getWcs()
585  results = self.register.run(templateSources, newWcs,
586  templateExposure.getBBox(), selectSources)
587  return results
588 
589  def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
590  import lsstDebug
591  import lsst.afw.display.ds9 as ds9
592  display = lsstDebug.Info(__name__).display
593  showSubtracted = lsstDebug.Info(__name__).showSubtracted
594  showPixelResiduals = lsstDebug.Info(__name__).showPixelResiduals
595  showDiaSources = lsstDebug.Info(__name__).showDiaSources
596  showDipoles = lsstDebug.Info(__name__).showDipoles
597  maskTransparency = lsstDebug.Info(__name__).maskTransparency
598  if not maskTransparency:
599  maskTransparency = 0
600  ds9.setMaskTransparency(maskTransparency)
601 
602  if display and showSubtracted:
603  ds9.mtv(subtractRes.subtractedExposure, frame=lsstDebug.frame, title="Subtracted image")
604  mi = subtractRes.subtractedExposure.getMaskedImage()
605  x0, y0 = mi.getX0(), mi.getY0()
606  with ds9.Buffering():
607  for s in diaSources:
608  x, y = s.getX() - x0, s.getY() - y0
609  ctype = "red" if s.get("flags.negative") else "yellow"
610  if (s.get("flags.pixel.interpolated.center") or s.get("flags.pixel.saturated.center") or
611  s.get("flags.pixel.cr.center")):
612  ptype = "x"
613  elif (s.get("flags.pixel.interpolated.any") or s.get("flags.pixel.saturated.any") or
614  s.get("flags.pixel.cr.any")):
615  ptype = "+"
616  else:
617  ptype = "o"
618  ds9.dot(ptype, x, y, size=4, frame=lsstDebug.frame, ctype=ctype)
619  lsstDebug.frame += 1
620 
621  if display and showPixelResiduals and selectSources:
622  import lsst.ip.diffim.utils as diUtils
623  nonKernelSources = []
624  for source in selectSources:
625  if not source in kernelSources:
626  nonKernelSources.append(source)
627 
628  diUtils.plotPixelResiduals(exposure,
629  subtractRes.warpedExposure,
630  subtractRes.subtractedExposure,
631  subtractRes.kernelCellSet,
632  subtractRes.psfMatchingKernel,
633  subtractRes.backgroundModel,
634  nonKernelSources,
635  self.subtract.config.kernel.active.detectionConfig,
636  origVariance=False)
637  diUtils.plotPixelResiduals(exposure,
638  subtractRes.warpedExposure,
639  subtractRes.subtractedExposure,
640  subtractRes.kernelCellSet,
641  subtractRes.psfMatchingKernel,
642  subtractRes.backgroundModel,
643  nonKernelSources,
644  self.subtract.config.kernel.active.detectionConfig,
645  origVariance=True)
646  if display and showDiaSources:
647  import lsst.ip.diffim.utils as diUtils
648  flagChecker = SourceFlagChecker(diaSources)
649  isFlagged = [flagChecker(x) for x in diaSources]
650  isDipole = [x.get("classification.dipole") for x in diaSources]
651  diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
652  frame=lsstDebug.frame)
653  lsstDebug.frame += 1
654 
655  if display and showDipoles:
656  DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
657  frame=lsstDebug.frame)
658  lsstDebug.frame += 1
659 
660  def getTemplate(self, exposure, sensorRef):
661  """Return a template coadd exposure that overlaps the exposure
662 
663  @param[in] exposure: exposure
664  @param[in] sensorRef: a Butler data reference that can be used to obtain coadd data
665 
666  @return coaddExposure: a template coadd exposure assembled out of patches
667  """
668  skyMap = sensorRef.get(datasetType=self.config.coaddName + "Coadd_skyMap")
669  expWcs = exposure.getWcs()
670  expBoxD = afwGeom.Box2D(exposure.getBBox())
671  expBoxD.grow(self.config.templateBorderSize)
672  ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter())
673  tractInfo = skyMap.findTract(ctrSkyPos)
674  self.log.info("Using skyMap tract %s" % (tractInfo.getId(),))
675  skyCorners = [expWcs.pixelToSky(pixPos) for pixPos in expBoxD.getCorners()]
676  patchList = tractInfo.findPatchList(skyCorners)
677 
678  if not patchList:
679  raise RuntimeError("No suitable tract found")
680  self.log.info("Assembling %s coadd patches" % (len(patchList),))
681 
682  # compute coadd bbox
683  coaddWcs = tractInfo.getWcs()
684  coaddBBox = afwGeom.Box2D()
685  for skyPos in skyCorners:
686  coaddBBox.include(coaddWcs.skyToPixel(skyPos))
687  coaddBBox = afwGeom.Box2I(coaddBBox)
688  self.log.info("exposure dimensions=%s; coadd dimensions=%s" % \
689  (exposure.getDimensions(), coaddBBox.getDimensions()))
690 
691  # assemble coadd exposure from subregions of patches
692  coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs)
693  edgeMask = afwImage.MaskU.getPlaneBitMask("EDGE")
694  coaddExposure.getMaskedImage().set(numpy.nan, edgeMask, numpy.nan)
695  nPatchesFound = 0
696  coaddFilter = None
697  coaddPsf = None
698  coaddSources = None
699  for patchInfo in patchList:
700  patchSubBBox = patchInfo.getOuterBBox()
701  patchSubBBox.clip(coaddBBox)
702  patchArgDict = dict(
703  datasetType=self.config.coaddName + "Coadd_sub",
704  bbox=patchSubBBox,
705  tract=tractInfo.getId(),
706  patch="%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]),
707  )
708  if patchSubBBox.isEmpty():
709  self.log.info("skip tract=%(tract)s; no overlapping pixels" % patchArgDict)
710  continue
711  if not sensorRef.datasetExists(**patchArgDict):
712  self.log.warn("%(datasetType)s, tract=%(tract)s, patch=%(patch)s does not exist" \
713  % patchArgDict)
714  continue
715 
716  nPatchesFound += 1
717  self.log.info("Reading patch %s" % patchArgDict)
718  coaddPatch = sensorRef.get(patchArgDict, immediate=True)
719  coaddView = afwImage.MaskedImageF(coaddExposure.getMaskedImage(), coaddPatch.getBBox())
720  coaddView <<= coaddPatch.getMaskedImage()
721  if coaddFilter is None:
722  coaddFilter = coaddPatch.getFilter()
723 
724  # Retrieve the PSF for this coadd tract, if not already retrieved
725  if coaddPsf is None and coaddPatch.hasPsf():
726  coaddPsf = coaddPatch.getPsf()
727 
728  if nPatchesFound == 0:
729  raise RuntimeError("No patches found!")
730 
731  if coaddPsf is None:
732  raise RuntimeError("No coadd Psf found!")
733 
734  coaddExposure.setPsf(coaddPsf)
735  coaddExposure.setFilter(coaddFilter)
736  return coaddExposure, coaddSources
737 
738  def _getConfigName(self):
739  """Return the name of the config dataset
740  """
741  return "%sDiff_config" % (self.config.coaddName,)
742 
743  def _getMetadataName(self):
744  """Return the name of the metadata dataset
745  """
746  return "%sDiff_metadata" % (self.config.coaddName,)
747 
748  def getSchemaCatalogs(self):
749  """Return a dict of empty catalogs for each catalog dataset produced by this task."""
750  diaSrc = afwTable.SourceCatalog(self.schema)
751  diaSrc.getTable().setMetadata(self.algMetadata)
752  return {self.config.coaddName + "Diff_diaSrc": diaSrc}
753 
754  @classmethod
756  """Create an argument parser
757  """
758  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
759  parser.add_id_argument("--id", "calexp", help="data ID, e.g. --id visit=12345 ccd=1,2")
760  return parser
761 
763  winter2013TemplateId = pexConfig.Field(dtype=int, default=88868666,
764  doc="88868666 for sparse data; 22222200 (g) and 11111100 (i) for dense data")
765  winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
766  doc="Shift stars going into RegisterTask by this amount")
767  winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
768  doc="Perturb stars going into RegisterTask by this amount")
769 
771  ConfigClass = Winter2013ImageDifferenceConfig
772  _DefaultName = "winter2013ImageDifference"
773 
774  def __init__(self, **kwargs):
775  ImageDifferenceTask.__init__(self, **kwargs)
776 
777  def getTemplate(self, exposure, sensorRef):
778  """Return a template coadd exposure that overlaps the exposure
779 
780  @param[in] exposure: exposure
781  @param[in] sensorRef: a Butler data reference that can be used to obtain coadd data
782 
783  @return coaddExposure: a template coadd exposure assembled out of patches
784  """
785  # Using a deep simulated calexp instead of a coadd
786  self.log.warn("USING WINTER2013 : DEEP CALEXP AS TEMPLATE")
787  templateId = type(sensorRef.dataId)(sensorRef.dataId)
788  templateId["visit"] = self.config.winter2013TemplateId
789  template = sensorRef.getButler().get(datasetType="calexp", dataId=templateId)
790  if self.config.doAddCalexpBackground:
791  templateBg = sensorRef.getButler().get(datasetType="calexpBackground", dataId=templateId)
792  mi = template.getMaskedImage()
793  mi += templateBg
794  if not template.hasPsf():
795  raise pipeBase.TaskError("Template has no psf")
796  templateSources = sensorRef.getButler().get(datasetType="src", dataId=templateId)
797  return template, templateSources
798 
799 
800  def fitAstrometry(self, templateSources, templateExposure, selectSources):
801  """Fit the relative astrometry between templateSources and selectSources"""
802  if self.config.winter2013WcsShift > 0.0:
803  offset = afwGeom.Extent2D(self.config.winter2013WcsShift,
804  self.config.winter2013WcsShift)
805  cKey = templateSources[0].getTable().getCentroidKey()
806  for source in templateSources:
807  centroid = source.get(cKey)
808  source.set(cKey, centroid+offset)
809  elif self.config.winter2013WcsRms > 0.0:
810  cKey = templateSources[0].getTable().getCentroidKey()
811  for source in templateSources:
812  offset = afwGeom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
813  self.config.winter2013WcsRms*numpy.random.normal())
814  centroid = source.get(cKey)
815  source.set(cKey, centroid+offset)
816 
817  results = self.register.run(templateSources, templateExposure.getWcs(),
818  templateExposure.getBBox(), selectSources)
819  return results
SourceMatchVector matchXy(SourceCatalog const &cat, double radius, bool symmetric=true)
Class for storing ordered metadata with comments.
Definition: PropertyList.h:81
Parameters to control convolution.
Definition: ConvolveImage.h:58
An integer coordinate rectangle.
Definition: Box.h:53
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Definition: fwd.h:55
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
A floating-point coordinate rectangle geometry.
Definition: Box.h:271