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
imageDifference.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2012 LSST Corporation.
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 math
23 import random
24 import numpy
25 
26 import lsst.pex.config as pexConfig
27 import lsst.pipe.base as pipeBase
28 import lsst.daf.base as dafBase
29 import lsst.afw.geom as afwGeom
30 import lsst.afw.math as afwMath
31 import lsst.afw.table as afwTable
32 import lsst.meas.astrom as measAstrom
33 from lsst.pipe.tasks.registerImage import RegisterTask
34 from lsst.meas.algorithms import SourceDetectionTask, PsfAttributes, SingleGaussianPsf, \
35  ObjectSizeStarSelectorTask
36 from lsst.ip.diffim import ImagePsfMatchTask, DipoleAnalysis, \
37  SourceFlagChecker, KernelCandidateF, cast_KernelCandidateF, makeKernelBasisList, \
38  KernelCandidateQa, DiaCatalogSourceSelectorTask, DiaCatalogSourceSelectorConfig, \
39  GetCoaddAsTemplateTask, GetCalexpAsTemplateTask, DipoleFitTask, DecorrelateALKernelTask
40 import lsst.ip.diffim.diffimTools as diffimTools
41 import lsst.ip.diffim.utils as diUtils
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  doDecorrelation = pexConfig.Field(dtype=bool, default=False,
69  doc="Perform diffim decorrelation to undo pixel correlation due to A&L kernel convolution? "
70  "If True, also update the diffim PSF.")
71  doMerge = pexConfig.Field(dtype=bool, default=True,
72  doc="Merge positive and negative diaSources with grow radius set by growFootprint")
73  doMatchSources = pexConfig.Field(dtype=bool, default=True,
74  doc="Match diaSources with input calexp sources and ref catalog sources")
75  doMeasurement = pexConfig.Field(dtype=bool, default=True, doc="Measure diaSources?")
76  doDipoleFitting = pexConfig.Field(dtype=bool, default=True, doc="Measure dipoles using new algorithm?")
77  doWriteSubtractedExp = pexConfig.Field(dtype=bool, default=True, doc="Write difference exposure?")
78  doWriteMatchedExp = pexConfig.Field(dtype=bool, default=False,
79  doc="Write warped and PSF-matched template coadd exposure?")
80  doWriteSources = pexConfig.Field(dtype=bool, default=True, doc="Write sources?")
81  doAddMetrics = pexConfig.Field(dtype=bool, default=True,
82  doc="Add columns to the source table to hold analysis metrics?")
83 
84  coaddName = pexConfig.Field(
85  doc="coadd name: typically one of deep or goodSeeing",
86  dtype=str,
87  default="deep",
88  )
89  convolveTemplate = pexConfig.Field(
90  doc="Which image gets convolved (default = template)",
91  dtype=bool,
92  default=True
93  )
94  refObjLoader = pexConfig.ConfigurableField(
95  target = measAstrom.LoadAstrometryNetObjectsTask,
96  doc = "reference object loader",
97  )
98  astrometer = pexConfig.ConfigurableField(
99  target = measAstrom.AstrometryTask,
100  doc = "astrometry task; used to match sources to reference objects, but not to fit a WCS",
101  )
102  sourceSelector = pexConfig.ConfigurableField(
103  target=ObjectSizeStarSelectorTask,
104  doc="Source selection algorithm",
105  )
106  subtract = pexConfig.ConfigurableField(
107  target=ImagePsfMatchTask,
108  doc="Warp and PSF match template to exposure, then subtract",
109  )
110  decorrelate = pexConfig.ConfigurableField(
111  target=DecorrelateALKernelTask,
112  doc="""Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True.
113  If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the
114  default of 5.5).""",
115  )
116  detection = pexConfig.ConfigurableField(
117  target=SourceDetectionTask,
118  doc="Low-threshold detection for final measurement",
119  )
120  #measurement = pexConfig.ConfigurableField(
121  # target=DipoleMeasurementTask,
122  # doc="Final source measurement on low-threshold detections; dipole fitting enabled.",
123  #)
124  measurement = pexConfig.ConfigurableField(
125  target=DipoleFitTask,
126  doc="Enable updated dipole fitting method.",
127  )
128  getTemplate = pexConfig.ConfigurableField(
129  target = GetCoaddAsTemplateTask,
130  doc = "Subtask to retrieve template exposure and sources",
131  )
132  controlStepSize = pexConfig.Field(
133  doc="What step size (every Nth one) to select a control sample from the kernelSources",
134  dtype=int,
135  default=5
136  )
137  controlRandomSeed = pexConfig.Field(
138  doc="Random seed for shuffing the control sample",
139  dtype=int,
140  default=10
141  )
142  register = pexConfig.ConfigurableField(
143  target=RegisterTask,
144  doc="Task to enable image-to-image image registration (warping)",
145  )
146  kernelSourcesFromRef = pexConfig.Field(
147  doc="Select sources to measure kernel from reference catalog if True, template if false",
148  dtype=bool,
149  default=False
150  )
151  templateSipOrder = pexConfig.Field(dtype=int, default=2,
152  doc="Sip Order for fitting the Template Wcs (default is too high, overfitting)")
153 
154  growFootprint = pexConfig.Field(dtype=int, default=2,
155  doc="Grow positive and negative footprints by this amount before merging")
156 
157  diaSourceMatchRadius = pexConfig.Field(dtype=float, default=0.5,
158  doc="Match radius (in arcseconds) for DiaSource to Source association")
159 
160  def setDefaults(self):
161  # defaults are OK for catalog and diacatalog
162 
163  self.subtract.kernel.name = "AL"
164  self.subtract.kernel.active.fitForBackground = True
165  self.subtract.kernel.active.spatialKernelOrder = 1
166  self.subtract.kernel.active.spatialBgOrder = 0
167  self.doPreConvolve = False
168  self.doMatchSources = False
169  self.doAddMetrics = False
170  self.doUseRegister = False
171 
172  # DiaSource Detection
173  self.detection.thresholdPolarity = "both"
174  self.detection.thresholdValue = 5.5
175  self.detection.reEstimateBackground = False
176  self.detection.thresholdType = "pixel_stdev"
177 
178  # Add filtered flux measurement, the correct measurement for pre-convolved images.
179  # Enable all measurements, regardless of doPreConvolved, as it makes data harvesting easier.
180  # To change that you must modify algorithms.names in the task's applyOverrides method,
181  # after the user has set doPreConvolved.
182  self.measurement.algorithms.names.add('base_PeakLikelihoodFlux')
183 
184  # For shuffling the control sample
185  random.seed(self.controlRandomSeed)
186 
187  def validate(self):
188  pexConfig.Config.validate(self)
189  if self.doMeasurement and not self.doDetection:
190  raise ValueError("Cannot run source measurement without source detection.")
191  if self.doMerge and not self.doDetection:
192  raise ValueError("Cannot run source merging without source detection.")
193  if self.doUseRegister and not self.doSelectSources:
194  raise ValueError("doUseRegister=True and doSelectSources=False. " +
195  "Cannot run RegisterTask without selecting sources.")
196 
197 
198 class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
199  @staticmethod
200  def getTargetList(parsedCmd, **kwargs):
201  return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
202  **kwargs)
203 
204 
205 class ImageDifferenceTask(pipeBase.CmdLineTask):
206  """Subtract an image from a template and measure the result
207  """
208  ConfigClass = ImageDifferenceConfig
209  RunnerClass = ImageDifferenceTaskRunner
210  _DefaultName = "imageDifference"
211 
212  def __init__(self, butler=None, **kwargs):
213  """!Construct an ImageDifference Task
214 
215  @param[in] butler Butler object to use in constructing reference object loaders
216  """
217  pipeBase.CmdLineTask.__init__(self, **kwargs)
218  self.makeSubtask("subtract")
219  self.makeSubtask("getTemplate")
220  self.makeSubtask("decorrelate")
221 
222  if self.config.doUseRegister:
223  self.makeSubtask("register")
224  self.schema = afwTable.SourceTable.makeMinimalSchema()
225 
226  if self.config.doSelectSources:
227  self.makeSubtask("sourceSelector", schema=self.schema)
228  self.makeSubtask('refObjLoader', butler=butler)
229  self.makeSubtask("astrometer", refObjLoader=self.refObjLoader)
230 
232  if self.config.doDetection:
233  self.makeSubtask("detection", schema=self.schema)
234  if self.config.doMeasurement:
235  if not self.config.doDipoleFitting:
236  self.makeSubtask("measurement", schema=self.schema,
237  algMetadata=self.algMetadata)
238  else:
239  self.makeSubtask("measurement", schema=self.schema)
240  if self.config.doMatchSources:
241  self.schema.addField("refMatchId", "L", "unique id of reference catalog match")
242  self.schema.addField("srcMatchId", "L", "unique id of source match")
243 
244  @pipeBase.timeMethod
245  def run(self, sensorRef, templateIdList=None):
246  """Subtract an image from a template coadd and measure the result
247 
248  Steps include:
249  - warp template coadd to match WCS of image
250  - PSF match image to warped template
251  - subtract image from PSF-matched, warped template
252  - persist difference image
253  - detect sources
254  - measure sources
255 
256  @param sensorRef: sensor-level butler data reference, used for the following data products:
257  Input only:
258  - calexp
259  - psf
260  - ccdExposureId
261  - ccdExposureId_bits
262  - self.config.coaddName + "Coadd_skyMap"
263  - self.config.coaddName + "Coadd"
264  Input or output, depending on config:
265  - self.config.coaddName + "Diff_subtractedExp"
266  Output, depending on config:
267  - self.config.coaddName + "Diff_matchedExp"
268  - self.config.coaddName + "Diff_src"
269 
270  @return pipe_base Struct containing these fields:
271  - subtractedExposure: exposure after subtracting template;
272  the unpersisted version if subtraction not run but detection run
273  None if neither subtraction nor detection run (i.e. nothing useful done)
274  - subtractRes: results of subtraction task; None if subtraction not run
275  - sources: detected and possibly measured sources; None if detection not run
276  """
277  self.log.info("Processing %s" % (sensorRef.dataId))
278 
279  # initialize outputs and some intermediate products
280  subtractedExposure = None
281  subtractRes = None
282  selectSources = None
283  kernelSources = None
284  controlSources = None
285  diaSources = None
286 
287  # We make one IdFactory that will be used by both icSrc and src datasets;
288  # I don't know if this is the way we ultimately want to do things, but at least
289  # this ensures the source IDs are fully unique.
290  expBits = sensorRef.get("ccdExposureId_bits")
291  expId = long(sensorRef.get("ccdExposureId"))
292  idFactory = afwTable.IdFactory.makeSource(expId, 64 - expBits)
293 
294  # Retrieve the science image we wish to analyze
295  exposure = sensorRef.get("calexp", immediate=True)
296  if self.config.doAddCalexpBackground:
297  mi = exposure.getMaskedImage()
298  mi += sensorRef.get("calexpBackground").getImage()
299  if not exposure.hasPsf():
300  raise pipeBase.TaskError("Exposure has no psf")
301  sciencePsf = exposure.getPsf()
302 
303  subtractedExposureName = self.config.coaddName + "Diff_differenceExp"
304  templateExposure = None # Stitched coadd exposure
305  templateSources = None # Sources on the template image
306  if self.config.doSubtract:
307  template = self.getTemplate.run(exposure, sensorRef, templateIdList=templateIdList)
308  templateExposure = template.exposure
309  templateSources = template.sources
310 
311  # compute scienceSigmaOrig: sigma of PSF of science image before pre-convolution
312  ctr = afwGeom.Box2D(exposure.getBBox()).getCenter()
313  psfAttr = PsfAttributes(sciencePsf, afwGeom.Point2I(ctr))
314  scienceSigmaOrig = psfAttr.computeGaussianWidth(psfAttr.ADAPTIVE_MOMENT)
315 
316  # sigma of PSF of template image before warping
317  ctr = afwGeom.Box2D(templateExposure.getBBox()).getCenter()
318  psfAttr = PsfAttributes(templateExposure.getPsf(), afwGeom.Point2I(ctr))
319  templateSigma = psfAttr.computeGaussianWidth(psfAttr.ADAPTIVE_MOMENT)
320 
321  # if requested, convolve the science exposure with its PSF
322  # (properly, this should be a cross-correlation, but our code does not yet support that)
323  # compute scienceSigmaPost: sigma of science exposure with pre-convolution, if done,
324  # else sigma of original science exposure
325  if self.config.doPreConvolve:
326  convControl = afwMath.ConvolutionControl()
327  # cannot convolve in place, so make a new MI to receive convolved image
328  srcMI = exposure.getMaskedImage()
329  destMI = srcMI.Factory(srcMI.getDimensions())
330  srcPsf = sciencePsf
331  if self.config.useGaussianForPreConvolution:
332  # convolve with a simplified PSF model: a double Gaussian
333  kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
334  preConvPsf = SingleGaussianPsf(kWidth, kHeight, scienceSigmaOrig)
335  else:
336  # convolve with science exposure's PSF model
337  preConvPsf = srcPsf
338  afwMath.convolve(destMI, srcMI, preConvPsf.getLocalKernel(), convControl)
339  exposure.setMaskedImage(destMI)
340  scienceSigmaPost = scienceSigmaOrig * math.sqrt(2)
341  else:
342  scienceSigmaPost = scienceSigmaOrig
343 
344  # If requested, find sources in the image
345  if self.config.doSelectSources:
346  if not sensorRef.datasetExists("src"):
347  self.log.warn("Src product does not exist; running detection, measurement, selection")
348  # Run own detection and measurement; necessary in nightly processing
349  selectSources = self.subtract.getSelectSources(
350  exposure,
351  sigma = scienceSigmaPost,
352  doSmooth = not self.doPreConvolve,
353  idFactory = idFactory,
354  )
355  else:
356  self.log.info("Source selection via src product")
357  # Sources already exist; for data release processing
358  selectSources = sensorRef.get("src")
359 
360  # Number of basis functions
361  nparam = len(makeKernelBasisList(self.subtract.config.kernel.active,
362  referenceFwhmPix=scienceSigmaPost * FwhmPerSigma,
363  targetFwhmPix=templateSigma * FwhmPerSigma))
364 
365  if self.config.doAddMetrics:
366  # Modify the schema of all Sources
367  kcQa = KernelCandidateQa(nparam)
368  selectSources = kcQa.addToSchema(selectSources)
369 
370  if self.config.kernelSourcesFromRef:
371  # match exposure sources to reference catalog
372  astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
373  matches = astromRet.matches
374  elif templateSources:
375  # match exposure sources to template sources
376  matches = afwTable.matchRaDec(templateSources, selectSources, 1.0*afwGeom.arcseconds,
377  False)
378  else:
379  raise RuntimeError("doSelectSources=True and kernelSourcesFromRef=False," +
380  "but template sources not available. Cannot match science " +
381  "sources with template sources. Run process* on data from " +
382  "which templates are built.")
383 
384  kernelSources = self.sourceSelector.selectStars(exposure, selectSources,
385  matches=matches).starCat
386 
387  random.shuffle(kernelSources, random.random)
388  controlSources = kernelSources[::self.config.controlStepSize]
389  kernelSources = [k for i,k in enumerate(kernelSources) if i % self.config.controlStepSize]
390 
391  if self.config.doSelectDcrCatalog:
392  redSelector = DiaCatalogSourceSelectorTask(
393  DiaCatalogSourceSelectorConfig(grMin=self.sourceSelector.config.grMax, grMax=99.999))
394  redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
395  controlSources.extend(redSources)
396 
397  blueSelector = DiaCatalogSourceSelectorTask(
398  DiaCatalogSourceSelectorConfig(grMin=-99.999, grMax=self.sourceSelector.config.grMin))
399  blueSources = blueSelector.selectStars(exposure, selectSources, matches=matches).starCat
400  controlSources.extend(blueSources)
401 
402  if self.config.doSelectVariableCatalog:
403  varSelector = DiaCatalogSourceSelectorTask(
404  DiaCatalogSourceSelectorConfig(includeVariable=True))
405  varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
406  controlSources.extend(varSources)
407 
408  self.log.info("Selected %d / %d sources for Psf matching (%d for control sample)"
409  % (len(kernelSources), len(selectSources), len(controlSources)))
410  allresids = {}
411  if self.config.doUseRegister:
412  self.log.info("Registering images")
413 
414  if templateSources is None:
415  # Run detection on the template, which is
416  # temporarily background-subtracted
417  templateSources = self.subtract.getSelectSources(
418  templateExposure,
419  sigma=templateSigma,
420  doSmooth=True,
421  idFactory=idFactory
422  )
423 
424  # Third step: we need to fit the relative astrometry.
425  #
426  wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
427  warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs,
428  exposure.getWcs(), exposure.getBBox())
429  templateExposure = warpedExp
430 
431  # Create debugging outputs on the astrometric
432  # residuals as a function of position. Persistence
433  # not yet implemented; expected on (I believe) #2636.
434  if self.config.doDebugRegister:
435  # Grab matches to reference catalog
436  srcToMatch = {x.second.getId() : x.first for x in matches}
437 
438  refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
439  inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidKey()
440  sids = [m.first.getId() for m in wcsResults.matches]
441  positions = [m.first.get(refCoordKey) for m in wcsResults.matches]
442  residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
443  m.second.get(inCentroidKey))) for m in wcsResults.matches]
444  allresids = dict(zip(sids, zip(positions, residuals)))
445 
446  cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
447  wcsResults.wcs.pixelToSky(
448  m.second.get(inCentroidKey))) for m in wcsResults.matches]
449  colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get("g"))
450  + 2.5*numpy.log10(srcToMatch[x].get("r"))
451  for x in sids if x in srcToMatch.keys()])
452  dlong = numpy.array([r[0].asArcseconds() for s,r in zip(sids, cresiduals)
453  if s in srcToMatch.keys()])
454  dlat = numpy.array([r[1].asArcseconds() for s,r in zip(sids, cresiduals)
455  if s in srcToMatch.keys()])
456  idx1 = numpy.where(colors<self.sourceSelector.config.grMin)
457  idx2 = numpy.where((colors>=self.sourceSelector.config.grMin)&
458  (colors<=self.sourceSelector.config.grMax))
459  idx3 = numpy.where(colors>self.sourceSelector.config.grMax)
460  rms1Long = IqrToSigma*(numpy.percentile(dlong[idx1],75)-numpy.percentile(dlong[idx1],25))
461  rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1],75)-numpy.percentile(dlat[idx1],25))
462  rms2Long = IqrToSigma*(numpy.percentile(dlong[idx2],75)-numpy.percentile(dlong[idx2],25))
463  rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2],75)-numpy.percentile(dlat[idx2],25))
464  rms3Long = IqrToSigma*(numpy.percentile(dlong[idx3],75)-numpy.percentile(dlong[idx3],25))
465  rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3],75)-numpy.percentile(dlat[idx3],25))
466  self.log.info("Blue star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx1]),
467  rms1Long,
468  numpy.median(dlat[idx1]),
469  rms1Lat))
470  self.log.info("Green star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx2]),
471  rms2Long,
472  numpy.median(dlat[idx2]),
473  rms2Lat))
474  self.log.info("Red star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx3]),
475  rms3Long,
476  numpy.median(dlat[idx3]),
477  rms3Lat))
478 
479  self.metadata.add("RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
480  self.metadata.add("RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
481  self.metadata.add("RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
482  self.metadata.add("RegisterBlueLongOffsetStd", rms1Long)
483  self.metadata.add("RegisterGreenLongOffsetStd", rms2Long)
484  self.metadata.add("RegisterRedLongOffsetStd", rms3Long)
485 
486  self.metadata.add("RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
487  self.metadata.add("RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
488  self.metadata.add("RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
489  self.metadata.add("RegisterBlueLatOffsetStd", rms1Lat)
490  self.metadata.add("RegisterGreenLatOffsetStd", rms2Lat)
491  self.metadata.add("RegisterRedLatOffsetStd", rms3Lat)
492 
493  # warp template exposure to match exposure,
494  # PSF match template exposure to exposure,
495  # then return the difference
496 
497  #Return warped template... Construct sourceKernelCand list after subtract
498  self.log.info("Subtracting images")
499  subtractRes = self.subtract.subtractExposures(
500  templateExposure=templateExposure,
501  scienceExposure=exposure,
502  candidateList=kernelSources,
503  convolveTemplate=self.config.convolveTemplate,
504  doWarping=not self.config.doUseRegister
505  )
506  subtractedExposure = subtractRes.subtractedExposure
507 
508  if self.config.doWriteMatchedExp:
509  sensorRef.put(subtractRes.matchedExposure, self.config.coaddName + "Diff_matchedExp")
510 
511  if self.config.doDetection:
512  self.log.info("Computing diffim PSF")
513  if subtractedExposure is None:
514  subtractedExposure = sensorRef.get(subtractedExposureName)
515 
516  # Get Psf from the appropriate input image if it doesn't exist
517  if not subtractedExposure.hasPsf():
518  if self.config.convolveTemplate:
519  subtractedExposure.setPsf(exposure.getPsf())
520  else:
521  if templateExposure is None:
522  template = self.getTemplate.run(exposure, sensorRef, templateIdList=templateIdList)
523  subtractedExposure.setPsf(template.exposure.getPsf())
524 
525  # If doSubtract is False, then subtractedExposure was fetched from disk (above), thus it may have
526  # already been decorrelated. Thus, we do not do decorrelation if doSubtract is False.
527  if self.config.doDecorrelation and self.config.doSubtract:
528  decorrResult = self.decorrelate.run(exposure, templateExposure,
529  subtractedExposure,
530  subtractRes.psfMatchingKernel)
531  subtractedExposure = decorrResult.correctedExposure
532 
533  if self.config.doDetection:
534  self.log.info("Running diaSource detection")
535  # Erase existing detection mask planes
536  mask = subtractedExposure.getMaskedImage().getMask()
537  mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
538 
539  table = afwTable.SourceTable.make(self.schema, idFactory)
540  table.setMetadata(self.algMetadata)
541  results = self.detection.makeSourceCatalog(
542  table=table,
543  exposure=subtractedExposure,
544  doSmooth=not self.config.doPreConvolve
545  )
546 
547  if self.config.doMerge:
548  fpSet = results.fpSets.positive
549  fpSet.merge(results.fpSets.negative, self.config.growFootprint,
550  self.config.growFootprint, False)
551  diaSources = afwTable.SourceCatalog(table)
552  fpSet.makeSources(diaSources)
553  self.log.info("Merging detections into %d sources" % (len(diaSources)))
554  else:
555  diaSources = results.sources
556 
557  if self.config.doMeasurement:
558  self.log.info("Running diaSource measurement")
559  if not self.config.doDipoleFitting:
560  self.measurement.run(diaSources, subtractedExposure)
561  else:
562  if self.config.doSubtract:
563  self.measurement.run(diaSources, subtractedExposure, exposure,
564  subtractRes.matchedExposure)
565  else:
566  self.measurement.run(diaSources, subtractedExposure, exposure)
567 
568  # Match with the calexp sources if possible
569  if self.config.doMatchSources:
570  if sensorRef.datasetExists("src"):
571  # Create key,val pair where key=diaSourceId and val=sourceId
572  matchRadAsec = self.config.diaSourceMatchRadius
573  matchRadPixel = matchRadAsec / exposure.getWcs().pixelScale().asArcseconds()
574 
575  srcMatches = afwTable.matchXy(sensorRef.get("src"), diaSources, matchRadPixel, True)
576  srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId()) for
577  srcMatch in srcMatches])
578  self.log.info("Matched %d / %d diaSources to sources" % (len(srcMatchDict),
579  len(diaSources)))
580  else:
581  self.log.warn("Src product does not exist; cannot match with diaSources")
582  srcMatchDict = {}
583 
584  # Create key,val pair where key=diaSourceId and val=refId
585  refAstromConfig = measAstrom.AstrometryConfig()
586  refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
587  refAstrometer = measAstrom.AstrometryTask(refAstromConfig)
588  astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
589  refMatches = astromRet.matches
590  if refMatches is None:
591  self.log.warn("No diaSource matches with reference catalog")
592  refMatchDict = {}
593  else:
594  self.log.info("Matched %d / %d diaSources to reference catalog" % (len(refMatches),
595  len(diaSources)))
596  refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId()) for \
597  refMatch in refMatches])
598 
599  # Assign source Ids
600  for diaSource in diaSources:
601  sid = diaSource.getId()
602  if srcMatchDict.has_key(sid):
603  diaSource.set("srcMatchId", srcMatchDict[sid])
604  if refMatchDict.has_key(sid):
605  diaSource.set("refMatchId", refMatchDict[sid])
606 
607  if diaSources is not None and self.config.doWriteSources:
608  sensorRef.put(diaSources, self.config.coaddName + "Diff_diaSrc")
609 
610  if self.config.doAddMetrics and self.config.doSelectSources:
611  self.log.info("Evaluating metrics and control sample")
612 
613  kernelCandList = []
614  for cell in subtractRes.kernelCellSet.getCellList():
615  for cand in cell.begin(False): # include bad candidates
616  kernelCandList.append(cast_KernelCandidateF(cand))
617 
618  # Get basis list to build control sample kernels
619  basisList = afwMath.cast_LinearCombinationKernel(
620  kernelCandList[0].getKernel(KernelCandidateF.ORIG)).getKernelList()
621 
622  controlCandList = \
623  diffimTools.sourceTableToCandidateList(controlSources,
624  subtractRes.warpedExposure, exposure,
625  self.config.subtract.kernel.active,
626  self.config.subtract.kernel.active.detectionConfig,
627  self.log, doBuild=True, basisList=basisList)
628 
629  kcQa.apply(kernelCandList, subtractRes.psfMatchingKernel, subtractRes.backgroundModel,
630  dof=nparam)
631  kcQa.apply(controlCandList, subtractRes.psfMatchingKernel, subtractRes.backgroundModel)
632 
633  if self.config.doDetection:
634  kcQa.aggregate(selectSources, self.metadata, allresids, diaSources)
635  else:
636  kcQa.aggregate(selectSources, self.metadata, allresids)
637 
638  sensorRef.put(selectSources, self.config.coaddName + "Diff_kernelSrc")
639 
640  if self.config.doWriteSubtractedExp:
641  sensorRef.put(subtractedExposure, subtractedExposureName)
642 
643  self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
644  return pipeBase.Struct(
645  subtractedExposure=subtractedExposure,
646  subtractRes=subtractRes,
647  sources=diaSources,
648  )
649 
650  def fitAstrometry(self, templateSources, templateExposure, selectSources):
651  """Fit the relative astrometry between templateSources and selectSources
652 
653  @todo remove this method. It originally fit a new WCS to the template before calling register.run
654  because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed.
655  It remains because a subtask overrides it.
656  """
657  results = self.register.run(templateSources, templateExposure.getWcs(),
658  templateExposure.getBBox(), selectSources)
659  return results
660 
661  def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
662  """@todo Test and update for current debug display and slot names
663  """
664  import lsstDebug
665  display = lsstDebug.Info(__name__).display
666  showSubtracted = lsstDebug.Info(__name__).showSubtracted
667  showPixelResiduals = lsstDebug.Info(__name__).showPixelResiduals
668  showDiaSources = lsstDebug.Info(__name__).showDiaSources
669  showDipoles = lsstDebug.Info(__name__).showDipoles
670  maskTransparency = lsstDebug.Info(__name__).maskTransparency
671  if display:
672  import lsst.afw.display.ds9 as ds9
673  if not maskTransparency:
674  maskTransparency = 0
675  ds9.setMaskTransparency(maskTransparency)
676 
677  if display and showSubtracted:
678  ds9.mtv(subtractRes.subtractedExposure, frame=lsstDebug.frame, title="Subtracted image")
679  mi = subtractRes.subtractedExposure.getMaskedImage()
680  x0, y0 = mi.getX0(), mi.getY0()
681  with ds9.Buffering():
682  for s in diaSources:
683  x, y = s.getX() - x0, s.getY() - y0
684  ctype = "red" if s.get("flags.negative") else "yellow"
685  if (s.get("flags.pixel.interpolated.center") or s.get("flags.pixel.saturated.center") or
686  s.get("flags.pixel.cr.center")):
687  ptype = "x"
688  elif (s.get("flags.pixel.interpolated.any") or s.get("flags.pixel.saturated.any") or
689  s.get("flags.pixel.cr.any")):
690  ptype = "+"
691  else:
692  ptype = "o"
693  ds9.dot(ptype, x, y, size=4, frame=lsstDebug.frame, ctype=ctype)
694  lsstDebug.frame += 1
695 
696  if display and showPixelResiduals and selectSources:
697  nonKernelSources = []
698  for source in selectSources:
699  if not source in kernelSources:
700  nonKernelSources.append(source)
701 
702  diUtils.plotPixelResiduals(exposure,
703  subtractRes.warpedExposure,
704  subtractRes.subtractedExposure,
705  subtractRes.kernelCellSet,
706  subtractRes.psfMatchingKernel,
707  subtractRes.backgroundModel,
708  nonKernelSources,
709  self.subtract.config.kernel.active.detectionConfig,
710  origVariance=False)
711  diUtils.plotPixelResiduals(exposure,
712  subtractRes.warpedExposure,
713  subtractRes.subtractedExposure,
714  subtractRes.kernelCellSet,
715  subtractRes.psfMatchingKernel,
716  subtractRes.backgroundModel,
717  nonKernelSources,
718  self.subtract.config.kernel.active.detectionConfig,
719  origVariance=True)
720  if display and showDiaSources:
721  flagChecker = SourceFlagChecker(diaSources)
722  isFlagged = [flagChecker(x) for x in diaSources]
723  isDipole = [x.get("classification.dipole") for x in diaSources]
724  diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
725  frame=lsstDebug.frame)
726  lsstDebug.frame += 1
727 
728  if display and showDipoles:
729  DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
730  frame=lsstDebug.frame)
731  lsstDebug.frame += 1
732 
733  def _getConfigName(self):
734  """Return the name of the config dataset
735  """
736  return "%sDiff_config" % (self.config.coaddName,)
737 
738  def _getMetadataName(self):
739  """Return the name of the metadata dataset
740  """
741  return "%sDiff_metadata" % (self.config.coaddName,)
742 
743  def getSchemaCatalogs(self):
744  """Return a dict of empty catalogs for each catalog dataset produced by this task."""
745  diaSrc = afwTable.SourceCatalog(self.schema)
746  diaSrc.getTable().setMetadata(self.algMetadata)
747  return {self.config.coaddName + "Diff_diaSrc": diaSrc}
748 
749  @classmethod
751  """Create an argument parser
752  """
753  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
754  parser.add_id_argument("--id", "calexp", help="data ID, e.g. --id visit=12345 ccd=1,2")
755  parser.add_id_argument("--templateId", "calexp", doMakeDataRefList=True,
756  help="Optional template data ID (visit only), e.g. --templateId visit=6789")
757  return parser
758 
760  winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
761  doc="Shift stars going into RegisterTask by this amount")
762  winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
763  doc="Perturb stars going into RegisterTask by this amount")
764 
765  def setDefaults(self):
766  ImageDifferenceConfig.setDefaults(self)
767  self.getTemplate.retarget(GetCalexpAsTemplateTask)
768 
769 
771  """!Image difference Task used in the Winter 2013 data challege.
772  Enables testing the effects of registration shifts and scatter.
773 
774  For use with winter 2013 simulated images:
775  Use --templateId visit=88868666 for sparse data
776  --templateId visit=22222200 for dense data (g)
777  --templateId visit=11111100 for dense data (i)
778  """
779  ConfigClass = Winter2013ImageDifferenceConfig
780  _DefaultName = "winter2013ImageDifference"
781 
782  def __init__(self, **kwargs):
783  ImageDifferenceTask.__init__(self, **kwargs)
784 
785  def fitAstrometry(self, templateSources, templateExposure, selectSources):
786  """Fit the relative astrometry between templateSources and selectSources"""
787  if self.config.winter2013WcsShift > 0.0:
788  offset = afwGeom.Extent2D(self.config.winter2013WcsShift,
789  self.config.winter2013WcsShift)
790  cKey = templateSources[0].getTable().getCentroidKey()
791  for source in templateSources:
792  centroid = source.get(cKey)
793  source.set(cKey, centroid+offset)
794  elif self.config.winter2013WcsRms > 0.0:
795  cKey = templateSources[0].getTable().getCentroidKey()
796  for source in templateSources:
797  offset = afwGeom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
798  self.config.winter2013WcsRms*numpy.random.normal())
799  centroid = source.get(cKey)
800  source.set(cKey, centroid+offset)
801 
802  results = self.register.run(templateSources, templateExposure.getWcs(),
803  templateExposure.getBBox(), selectSources)
804  return results
Class for storing ordered metadata with comments.
Definition: PropertyList.h:82
Parameters to control convolution.
Definition: ConvolveImage.h:58
std::vector< Match< typename Cat::Record, typename Cat::Record > > matchRaDec(Cat const &cat, Angle radius, bool symmetric)
Represent a PSF as a circularly symmetrical Gaussian.
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
Image difference Task used in the Winter 2013 data challege.
def __init__
Construct an ImageDifference Task.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
SourceMatchVector matchXy(SourceCatalog const &cat, double radius, bool symmetric)
A floating-point coordinate rectangle geometry.
Definition: Box.h:271