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