lsst.pipe.tasks  19.0.0-32-gedca71e0+2
imageDifference.py
Go to the documentation of this file.
1 # This file is part of pipe_tasks.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
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 GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
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.geom as geom
30 import lsst.afw.math as afwMath
31 import lsst.afw.table as afwTable
32 from lsst.meas.astrom import AstrometryConfig, AstrometryTask
33 from lsst.meas.base import ForcedMeasurementTask
34 from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask
35 from lsst.pipe.tasks.registerImage import RegisterTask
36 from lsst.pipe.tasks.scaleVariance import ScaleVarianceTask
37 from lsst.meas.algorithms import SourceDetectionTask, SingleGaussianPsf, ObjectSizeStarSelectorTask
38 from lsst.ip.diffim import (DipoleAnalysis, SourceFlagChecker, KernelCandidateF, makeKernelBasisList,
39  KernelCandidateQa, DiaCatalogSourceSelectorTask, DiaCatalogSourceSelectorConfig,
40  GetCoaddAsTemplateTask, GetCalexpAsTemplateTask, DipoleFitTask,
41  DecorrelateALKernelSpatialTask, subtractAlgorithmRegistry)
42 import lsst.ip.diffim.diffimTools as diffimTools
43 import lsst.ip.diffim.utils as diUtils
44 import lsst.afw.display as afwDisplay
45 
46 __all__ = ["ImageDifferenceConfig", "ImageDifferenceTask"]
47 FwhmPerSigma = 2*math.sqrt(2*math.log(2))
48 IqrToSigma = 0.741
49 
50 
51 class ImageDifferenceConfig(pexConfig.Config):
52  """Config for ImageDifferenceTask
53  """
54  doAddCalexpBackground = pexConfig.Field(dtype=bool, default=False,
55  doc="Add background to calexp before processing it. "
56  "Useful as ipDiffim does background matching.")
57  doUseRegister = pexConfig.Field(dtype=bool, default=True,
58  doc="Use image-to-image registration to align template with "
59  "science image")
60  doDebugRegister = pexConfig.Field(dtype=bool, default=False,
61  doc="Writing debugging data for doUseRegister")
62  doSelectSources = pexConfig.Field(dtype=bool, default=True,
63  doc="Select stars to use for kernel fitting")
64  doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=False,
65  doc="Select stars of extreme color as part of the control sample")
66  doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=False,
67  doc="Select stars that are variable to be part "
68  "of the control sample")
69  doSubtract = pexConfig.Field(dtype=bool, default=True, doc="Compute subtracted exposure?")
70  doPreConvolve = pexConfig.Field(dtype=bool, default=True,
71  doc="Convolve science image by its PSF before PSF-matching?")
72  doScaleTemplateVariance = pexConfig.Field(dtype=bool, default=False,
73  doc="Scale variance of the template before PSF matching")
74  useGaussianForPreConvolution = pexConfig.Field(dtype=bool, default=True,
75  doc="Use a simple gaussian PSF model for pre-convolution "
76  "(else use fit PSF)? Ignored if doPreConvolve false.")
77  doDetection = pexConfig.Field(dtype=bool, default=True, doc="Detect sources?")
78  doDecorrelation = pexConfig.Field(dtype=bool, default=False,
79  doc="Perform diffim decorrelation to undo pixel correlation due to A&L "
80  "kernel convolution? If True, also update the diffim PSF.")
81  doMerge = pexConfig.Field(dtype=bool, default=True,
82  doc="Merge positive and negative diaSources with grow radius "
83  "set by growFootprint")
84  doMatchSources = pexConfig.Field(dtype=bool, default=True,
85  doc="Match diaSources with input calexp sources and ref catalog sources")
86  doMeasurement = pexConfig.Field(dtype=bool, default=True, doc="Measure diaSources?")
87  doDipoleFitting = pexConfig.Field(dtype=bool, default=True, doc="Measure dipoles using new algorithm?")
88  doForcedMeasurement = pexConfig.Field(
89  dtype=bool,
90  default=True,
91  doc="Force photometer diaSource locations on PVI?")
92  doWriteSubtractedExp = pexConfig.Field(dtype=bool, default=True, doc="Write difference exposure?")
93  doWriteMatchedExp = pexConfig.Field(dtype=bool, default=False,
94  doc="Write warped and PSF-matched template coadd exposure?")
95  doWriteSources = pexConfig.Field(dtype=bool, default=True, doc="Write sources?")
96  doAddMetrics = pexConfig.Field(dtype=bool, default=True,
97  doc="Add columns to the source table to hold analysis metrics?")
98 
99  coaddName = pexConfig.Field(
100  doc="coadd name: typically one of deep, goodSeeing, or dcr",
101  dtype=str,
102  default="deep",
103  )
104  convolveTemplate = pexConfig.Field(
105  doc="Which image gets convolved (default = template)",
106  dtype=bool,
107  default=True
108  )
109  refObjLoader = pexConfig.ConfigurableField(
110  target=LoadIndexedReferenceObjectsTask,
111  doc="reference object loader",
112  )
113  astrometer = pexConfig.ConfigurableField(
114  target=AstrometryTask,
115  doc="astrometry task; used to match sources to reference objects, but not to fit a WCS",
116  )
117  sourceSelector = pexConfig.ConfigurableField(
118  target=ObjectSizeStarSelectorTask,
119  doc="Source selection algorithm",
120  )
121  subtract = subtractAlgorithmRegistry.makeField("Subtraction Algorithm", default="al")
122  decorrelate = pexConfig.ConfigurableField(
123  target=DecorrelateALKernelSpatialTask,
124  doc="Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
125  "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
126  "default of 5.5).",
127  )
128  doSpatiallyVarying = pexConfig.Field(
129  dtype=bool,
130  default=False,
131  doc="If using Zogy or A&L decorrelation, perform these on a grid across the "
132  "image in order to allow for spatial variations"
133  )
134  detection = pexConfig.ConfigurableField(
135  target=SourceDetectionTask,
136  doc="Low-threshold detection for final measurement",
137  )
138  measurement = pexConfig.ConfigurableField(
139  target=DipoleFitTask,
140  doc="Enable updated dipole fitting method",
141  )
142  forcedMeasurement = pexConfig.ConfigurableField(
143  target=ForcedMeasurementTask,
144  doc="Subtask to force photometer PVI at diaSource location.",
145  )
146  getTemplate = pexConfig.ConfigurableField(
147  target=GetCoaddAsTemplateTask,
148  doc="Subtask to retrieve template exposure and sources",
149  )
150  scaleVariance = pexConfig.ConfigurableField(
151  target=ScaleVarianceTask,
152  doc="Subtask to rescale the variance of the template "
153  "to the statistically expected level"
154  )
155  controlStepSize = pexConfig.Field(
156  doc="What step size (every Nth one) to select a control sample from the kernelSources",
157  dtype=int,
158  default=5
159  )
160  controlRandomSeed = pexConfig.Field(
161  doc="Random seed for shuffing the control sample",
162  dtype=int,
163  default=10
164  )
165  register = pexConfig.ConfigurableField(
166  target=RegisterTask,
167  doc="Task to enable image-to-image image registration (warping)",
168  )
169  kernelSourcesFromRef = pexConfig.Field(
170  doc="Select sources to measure kernel from reference catalog if True, template if false",
171  dtype=bool,
172  default=False
173  )
174  templateSipOrder = pexConfig.Field(
175  dtype=int, default=2,
176  doc="Sip Order for fitting the Template Wcs (default is too high, overfitting)"
177  )
178  growFootprint = pexConfig.Field(
179  dtype=int, default=2,
180  doc="Grow positive and negative footprints by this amount before merging"
181  )
182  diaSourceMatchRadius = pexConfig.Field(
183  dtype=float, default=0.5,
184  doc="Match radius (in arcseconds) for DiaSource to Source association"
185  )
186 
187  def setDefaults(self):
188  # defaults are OK for catalog and diacatalog
189 
190  self.subtract['al'].kernel.name = "AL"
191  self.subtract['al'].kernel.active.fitForBackground = True
192  self.subtract['al'].kernel.active.spatialKernelOrder = 1
193  self.subtract['al'].kernel.active.spatialBgOrder = 2
194  self.doPreConvolve = False
195  self.doMatchSources = False
196  self.doAddMetrics = False
197  self.doUseRegister = False
198 
199  # DiaSource Detection
200  self.detection.thresholdPolarity = "both"
201  self.detection.thresholdValue = 5.5
202  self.detection.reEstimateBackground = False
203  self.detection.thresholdType = "pixel_stdev"
204 
205  # Add filtered flux measurement, the correct measurement for pre-convolved images.
206  # Enable all measurements, regardless of doPreConvolve, as it makes data harvesting easier.
207  # To change that you must modify algorithms.names in the task's applyOverrides method,
208  # after the user has set doPreConvolve.
209  self.measurement.algorithms.names.add('base_PeakLikelihoodFlux')
210  self.measurement.plugins.names |= ['base_LocalPhotoCalib',
211  'base_LocalWcs']
212 
213  self.forcedMeasurement.plugins = ["base_TransformedCentroid", "base_PsfFlux"]
214  self.forcedMeasurement.copyColumns = {
215  "id": "objectId", "parent": "parentObjectId", "coord_ra": "coord_ra", "coord_dec": "coord_dec"}
216  self.forcedMeasurement.slots.centroid = "base_TransformedCentroid"
217  self.forcedMeasurement.slots.shape = None
218 
219  # For shuffling the control sample
220  random.seed(self.controlRandomSeed)
221 
222  def validate(self):
223  pexConfig.Config.validate(self)
224  if self.doAddMetrics and not self.doSubtract:
225  raise ValueError("Subtraction must be enabled for kernel metrics calculation.")
226  if not self.doSubtract and not self.doDetection:
227  raise ValueError("Either doSubtract or doDetection must be enabled.")
228  if self.subtract.name == 'zogy' and self.doAddMetrics:
229  raise ValueError("Kernel metrics does not exist in zogy subtraction.")
230  if self.doMeasurement and not self.doDetection:
231  raise ValueError("Cannot run source measurement without source detection.")
232  if self.doMerge and not self.doDetection:
233  raise ValueError("Cannot run source merging without source detection.")
234  if self.doUseRegister and not self.doSelectSources:
235  raise ValueError("doUseRegister=True and doSelectSources=False. "
236  "Cannot run RegisterTask without selecting sources.")
237  if self.doPreConvolve and self.doDecorrelation and not self.convolveTemplate:
238  raise ValueError("doPreConvolve=True and doDecorrelation=True and "
239  "convolveTemplate=False is not supported.")
240  if hasattr(self.getTemplate, "coaddName"):
241  if self.getTemplate.coaddName != self.coaddName:
242  raise ValueError("Mis-matched coaddName and getTemplate.coaddName in the config.")
243 
244 
245 class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
246 
247  @staticmethod
248  def getTargetList(parsedCmd, **kwargs):
249  return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
250  **kwargs)
251 
252 
253 class ImageDifferenceTask(pipeBase.CmdLineTask):
254  """Subtract an image from a template and measure the result
255  """
256  ConfigClass = ImageDifferenceConfig
257  RunnerClass = ImageDifferenceTaskRunner
258  _DefaultName = "imageDifference"
259 
260  def __init__(self, butler=None, **kwargs):
261  """!Construct an ImageDifference Task
262 
263  @param[in] butler Butler object to use in constructing reference object loaders
264  """
265  pipeBase.CmdLineTask.__init__(self, **kwargs)
266  self.makeSubtask("getTemplate")
267 
268  self.makeSubtask("subtract")
269 
270  if self.config.subtract.name == 'al' and self.config.doDecorrelation:
271  self.makeSubtask("decorrelate")
272 
273  if self.config.doScaleTemplateVariance:
274  self.makeSubtask("scaleVariance")
275 
276  if self.config.doUseRegister:
277  self.makeSubtask("register")
278  self.schema = afwTable.SourceTable.makeMinimalSchema()
279 
280  if self.config.doSelectSources:
281  self.makeSubtask("sourceSelector")
282  if self.config.kernelSourcesFromRef:
283  self.makeSubtask('refObjLoader', butler=butler)
284  self.makeSubtask("astrometer", refObjLoader=self.refObjLoader)
285 
286  self.algMetadata = dafBase.PropertyList()
287  if self.config.doDetection:
288  self.makeSubtask("detection", schema=self.schema)
289  if self.config.doMeasurement:
290  self.makeSubtask("measurement", schema=self.schema,
291  algMetadata=self.algMetadata)
292  if self.config.doForcedMeasurement:
293  self.schema.addField(
294  "ip_diffim_forced_PsfFlux_instFlux", "D",
295  "Forced PSF flux measured on the direct image.")
296  self.schema.addField(
297  "ip_diffim_forced_PsfFlux_instFluxErr", "D",
298  "Forced PSF flux error measured on the direct image.")
299  self.schema.addField(
300  "ip_diffim_forced_PsfFlux_area", "F",
301  "Forced PSF flux effective area of PSF.",
302  units="pixel")
303  self.schema.addField(
304  "ip_diffim_forced_PsfFlux_flag", "Flag",
305  "Forced PSF flux general failure flag.")
306  self.schema.addField(
307  "ip_diffim_forced_PsfFlux_flag_noGoodPixels", "Flag",
308  "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
309  self.schema.addField(
310  "ip_diffim_forced_PsfFlux_flag_edge", "Flag",
311  "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
312  self.makeSubtask("forcedMeasurement", refSchema=self.schema)
313  if self.config.doMatchSources:
314  self.schema.addField("refMatchId", "L", "unique id of reference catalog match")
315  self.schema.addField("srcMatchId", "L", "unique id of source match")
316 
317  @staticmethod
318  def makeIdFactory(expId, expBits):
319  """Create IdFactory instance for unique 64 bit diaSource id-s.
320 
321  Parameters
322  ----------
323  expId : `int`
324  Exposure id.
325 
326  expBits: `int`
327  Number of used bits in ``expId``.
328 
329  Note
330  ----
331  The diasource id-s consists of the ``expId`` stored fixed in the highest value
332  ``expBits`` of the 64-bit integer plus (bitwise or) a generated sequence number in the
333  low value end of the integer.
334 
335  Returns
336  -------
337  idFactory: `lsst.afw.table.IdFactory`
338  """
339  return afwTable.IdFactory.makeSource(expId, 64 - expBits)
340 
341  @pipeBase.timeMethod
342  def runDataRef(self, sensorRef, templateIdList=None):
343  """Subtract an image from a template coadd and measure the result.
344 
345  Data I/O wrapper around `run` using the butler in Gen2.
346 
347  Parameters
348  ----------
349  sensorRef : `lsst.daf.persistence.ButlerDataRef`
350  Sensor-level butler data reference, used for the following data products:
351 
352  Input only:
353  - calexp
354  - psf
355  - ccdExposureId
356  - ccdExposureId_bits
357  - self.config.coaddName + "Coadd_skyMap"
358  - self.config.coaddName + "Coadd"
359  Input or output, depending on config:
360  - self.config.coaddName + "Diff_subtractedExp"
361  Output, depending on config:
362  - self.config.coaddName + "Diff_matchedExp"
363  - self.config.coaddName + "Diff_src"
364 
365  Returns
366  -------
367  results : `lsst.pipe.base.Struct`
368  Returns the Struct by `run`.
369  """
370  subtractedExposureName = self.config.coaddName + "Diff_differenceExp"
371  subtractedExposure = None
372  selectSources = None
373  calexpBackgroundExposure = None
374  self.log.info("Processing %s" % (sensorRef.dataId))
375 
376  # We make one IdFactory that will be used by both icSrc and src datasets;
377  # I don't know if this is the way we ultimately want to do things, but at least
378  # this ensures the source IDs are fully unique.
379  idFactory = self.makeIdFactory(expId=int(sensorRef.get("ccdExposureId")),
380  expBits=sensorRef.get("ccdExposureId_bits"))
381  if self.config.doAddCalexpBackground:
382  calexpBackgroundExposure = sensorRef.get("calexpBackground")
383 
384  # Retrieve the science image we wish to analyze
385  exposure = sensorRef.get("calexp", immediate=True)
386 
387  # Retrieve the template image
388  template = self.getTemplate.run(exposure, sensorRef, templateIdList=templateIdList)
389 
390  if sensorRef.datasetExists("src"):
391  self.log.info("Source selection via src product")
392  # Sources already exist; for data release processing
393  selectSources = sensorRef.get("src")
394 
395  if not self.config.doSubtract and self.config.doDetection:
396  # If we don't do subtraction, we need the subtracted exposure from the repo
397  subtractedExposure = sensorRef.get(subtractedExposureName)
398  # Both doSubtract and doDetection cannot be False
399 
400  results = self.run(exposure=exposure,
401  selectSources=selectSources,
402  templateExposure=template.exposure,
403  templateSources=template.sources,
404  idFactory=idFactory,
405  calexpBackgroundExposure=calexpBackgroundExposure,
406  subtractedExposure=subtractedExposure)
407 
408  if self.config.doWriteSources and results.diaSources is not None:
409  sensorRef.put(results.diaSources, self.config.coaddName + "Diff_diaSrc")
410  if self.config.doWriteMatchedExp:
411  sensorRef.put(results.matchedExposure, self.config.coaddName + "Diff_matchedExp")
412  if self.config.doAddMetrics and self.config.doSelectSources:
413  sensorRef.put(results.selectSources, self.config.coaddName + "Diff_kernelSrc")
414  if self.config.doWriteSubtractedExp:
415  sensorRef.put(results.subtractedExposure, subtractedExposureName)
416  return results
417 
418  def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
419  idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
420  """PSF matches, subtract two images and perform detection on the difference image.
421 
422  Parameters
423  ----------
424  exposure : `lsst.afw.image.ExposureF`, optional
425  The science exposure, the minuend in the image subtraction.
426  Can be None only if ``config.doSubtract==False``.
427  selectSources : `lsst.afw.table.SourceCatalog`, optional
428  Identified sources on the science exposure. This catalog is used to
429  select sources in order to perform the AL PSF matching on stamp images
430  around them. The selection steps depend on config options and whether
431  ``templateSources`` and ``matchingSources`` specified.
432  templateExposure : `lsst.afw.image.ExposureF`, optional
433  The template to be subtracted from ``exposure`` in the image subtraction.
434  The template exposure should cover the same sky area as the science exposure.
435  It is either a stich of patches of a coadd skymap image or a calexp
436  of the same pointing as the science exposure. Can be None only
437  if ``config.doSubtract==False`` and ``subtractedExposure`` is not None.
438  templateSources : `lsst.afw.table.SourceCatalog`, optional
439  Identified sources on the template exposure.
440  idFactory : `lsst.afw.table.IdFactory`
441  Generator object to assign ids to detected sources in the difference image.
442  calexpBackgroundExposure : `lsst.afw.image.ExposureF`, optional
443  Background exposure to be added back to the science exposure
444  if ``config.doAddCalexpBackground==True``
445  subtractedExposure : `lsst.afw.image.ExposureF`, optional
446  If ``config.doSubtract==False`` and ``config.doDetection==True``,
447  performs the post subtraction source detection only on this exposure.
448  Otherwise should be None.
449 
450  Returns
451  -------
452  results : `lsst.pipe.base.Struct`
453  ``subtractedExposure`` : `lsst.afw.image.ExposureF`
454  Difference image.
455  ``matchedExposure`` : `lsst.afw.image.ExposureF`
456  The matched PSF exposure.
457  ``subtractRes`` : `lsst.pipe.base.Struct`
458  The returned result structure of the ImagePsfMatchTask subtask.
459  ``diaSources`` : `lsst.afw.table.SourceCatalog`
460  The catalog of detected sources.
461  ``selectSources`` : `lsst.afw.table.SourceCatalog`
462  The input source catalog with optionally added Qa information.
463 
464  Notes
465  -----
466  The following major steps are included:
467 
468  - warp template coadd to match WCS of image
469  - PSF match image to warped template
470  - subtract image from PSF-matched, warped template
471  - detect sources
472  - measure sources
473 
474  For details about the image subtraction configuration modes
475  see `lsst.ip.diffim`.
476  """
477  subtractRes = None
478  controlSources = None
479  diaSources = None
480  kernelSources = None
481 
482  if self.config.doAddCalexpBackground:
483  mi = exposure.getMaskedImage()
484  mi += calexpBackgroundExposure.getImage()
485 
486  if not exposure.hasPsf():
487  raise pipeBase.TaskError("Exposure has no psf")
488  sciencePsf = exposure.getPsf()
489 
490  if self.config.doSubtract:
491  if self.config.doScaleTemplateVariance:
492  templateVarFactor = self.scaleVariance.run(
493  templateExposure.getMaskedImage())
494  self.metadata.add("scaleTemplateVarianceFactor", templateVarFactor)
495 
496  if self.config.subtract.name == 'zogy':
497  subtractRes = self.subtract.subtractExposures(templateExposure, exposure,
498  doWarping=True,
499  spatiallyVarying=self.config.doSpatiallyVarying,
500  doPreConvolve=self.config.doPreConvolve)
501  subtractedExposure = subtractRes.subtractedExposure
502 
503  elif self.config.subtract.name == 'al':
504  # compute scienceSigmaOrig: sigma of PSF of science image before pre-convolution
505  scienceSigmaOrig = sciencePsf.computeShape().getDeterminantRadius()
506  templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
507 
508  # if requested, convolve the science exposure with its PSF
509  # (properly, this should be a cross-correlation, but our code does not yet support that)
510  # compute scienceSigmaPost: sigma of science exposure with pre-convolution, if done,
511  # else sigma of original science exposure
512  # TODO: DM-22762 This functional block should be moved into its own method
513  preConvPsf = None
514  if self.config.doPreConvolve:
515  convControl = afwMath.ConvolutionControl()
516  # cannot convolve in place, so make a new MI to receive convolved image
517  srcMI = exposure.getMaskedImage()
518  destMI = srcMI.Factory(srcMI.getDimensions())
519  srcPsf = sciencePsf
520  if self.config.useGaussianForPreConvolution:
521  # convolve with a simplified PSF model: a double Gaussian
522  kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
523  preConvPsf = SingleGaussianPsf(kWidth, kHeight, scienceSigmaOrig)
524  else:
525  # convolve with science exposure's PSF model
526  preConvPsf = srcPsf
527  afwMath.convolve(destMI, srcMI, preConvPsf.getLocalKernel(), convControl)
528  exposure.setMaskedImage(destMI)
529  scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
530  else:
531  scienceSigmaPost = scienceSigmaOrig
532 
533  # If requested, find and select sources from the image
534  # else, AL subtraction will do its own source detection
535  # TODO: DM-22762 This functional block should be moved into its own method
536  if self.config.doSelectSources:
537  if selectSources is None:
538  self.log.warn("Src product does not exist; running detection, measurement, selection")
539  # Run own detection and measurement; necessary in nightly processing
540  selectSources = self.subtract.getSelectSources(
541  exposure,
542  sigma=scienceSigmaPost,
543  doSmooth=not self.doPreConvolve,
544  idFactory=idFactory,
545  )
546 
547  if self.config.doAddMetrics:
548  # Number of basis functions
549 
550  nparam = len(makeKernelBasisList(self.subtract.config.kernel.active,
551  referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
552  targetFwhmPix=templateSigma*FwhmPerSigma))
553  # Modify the schema of all Sources
554  # DEPRECATED: This is a data dependent (nparam) output product schema
555  # outside the task constructor.
556  # NOTE: The pre-determination of nparam at this point
557  # may be incorrect as the template psf is warped later in
558  # ImagePsfMatchTask.matchExposures()
559  kcQa = KernelCandidateQa(nparam)
560  selectSources = kcQa.addToSchema(selectSources)
561  if self.config.kernelSourcesFromRef:
562  # match exposure sources to reference catalog
563  astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
564  matches = astromRet.matches
565  elif templateSources:
566  # match exposure sources to template sources
567  mc = afwTable.MatchControl()
568  mc.findOnlyClosest = False
569  matches = afwTable.matchRaDec(templateSources, selectSources, 1.0*geom.arcseconds,
570  mc)
571  else:
572  raise RuntimeError("doSelectSources=True and kernelSourcesFromRef=False,"
573  "but template sources not available. Cannot match science "
574  "sources with template sources. Run process* on data from "
575  "which templates are built.")
576 
577  kernelSources = self.sourceSelector.run(selectSources, exposure=exposure,
578  matches=matches).sourceCat
579  random.shuffle(kernelSources, random.random)
580  controlSources = kernelSources[::self.config.controlStepSize]
581  kernelSources = [k for i, k in enumerate(kernelSources)
582  if i % self.config.controlStepSize]
583 
584  if self.config.doSelectDcrCatalog:
585  redSelector = DiaCatalogSourceSelectorTask(
586  DiaCatalogSourceSelectorConfig(grMin=self.sourceSelector.config.grMax,
587  grMax=99.999))
588  redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
589  controlSources.extend(redSources)
590 
591  blueSelector = DiaCatalogSourceSelectorTask(
592  DiaCatalogSourceSelectorConfig(grMin=-99.999,
593  grMax=self.sourceSelector.config.grMin))
594  blueSources = blueSelector.selectStars(exposure, selectSources,
595  matches=matches).starCat
596  controlSources.extend(blueSources)
597 
598  if self.config.doSelectVariableCatalog:
599  varSelector = DiaCatalogSourceSelectorTask(
600  DiaCatalogSourceSelectorConfig(includeVariable=True))
601  varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
602  controlSources.extend(varSources)
603 
604  self.log.info("Selected %d / %d sources for Psf matching (%d for control sample)"
605  % (len(kernelSources), len(selectSources), len(controlSources)))
606 
607  allresids = {}
608  # TODO: DM-22762 This functional block should be moved into its own method
609  if self.config.doUseRegister:
610  self.log.info("Registering images")
611 
612  if templateSources is None:
613  # Run detection on the template, which is
614  # temporarily background-subtracted
615  # sigma of PSF of template image before warping
616  templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
617  templateSources = self.subtract.getSelectSources(
618  templateExposure,
619  sigma=templateSigma,
620  doSmooth=True,
621  idFactory=idFactory
622  )
623 
624  # Third step: we need to fit the relative astrometry.
625  #
626  wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
627  warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs,
628  exposure.getWcs(), exposure.getBBox())
629  templateExposure = warpedExp
630 
631  # Create debugging outputs on the astrometric
632  # residuals as a function of position. Persistence
633  # not yet implemented; expected on (I believe) #2636.
634  if self.config.doDebugRegister:
635  # Grab matches to reference catalog
636  srcToMatch = {x.second.getId(): x.first for x in matches}
637 
638  refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
639  inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidKey()
640  sids = [m.first.getId() for m in wcsResults.matches]
641  positions = [m.first.get(refCoordKey) for m in wcsResults.matches]
642  residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
643  m.second.get(inCentroidKey))) for m in wcsResults.matches]
644  allresids = dict(zip(sids, zip(positions, residuals)))
645 
646  cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
647  wcsResults.wcs.pixelToSky(
648  m.second.get(inCentroidKey))) for m in wcsResults.matches]
649  colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get("g")) +
650  2.5*numpy.log10(srcToMatch[x].get("r"))
651  for x in sids if x in srcToMatch.keys()])
652  dlong = numpy.array([r[0].asArcseconds() for s, r in zip(sids, cresiduals)
653  if s in srcToMatch.keys()])
654  dlat = numpy.array([r[1].asArcseconds() for s, r in zip(sids, cresiduals)
655  if s in srcToMatch.keys()])
656  idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
657  idx2 = numpy.where((colors >= self.sourceSelector.config.grMin) &
658  (colors <= self.sourceSelector.config.grMax))
659  idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
660  rms1Long = IqrToSigma*(
661  (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
662  rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75) -
663  numpy.percentile(dlat[idx1], 25))
664  rms2Long = IqrToSigma*(
665  (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
666  rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75) -
667  numpy.percentile(dlat[idx2], 25))
668  rms3Long = IqrToSigma*(
669  (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
670  rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75) -
671  numpy.percentile(dlat[idx3], 25))
672  self.log.info("Blue star offsets'': %.3f %.3f, %.3f %.3f" %
673  (numpy.median(dlong[idx1]), rms1Long,
674  numpy.median(dlat[idx1]), rms1Lat))
675  self.log.info("Green star offsets'': %.3f %.3f, %.3f %.3f" %
676  (numpy.median(dlong[idx2]), rms2Long,
677  numpy.median(dlat[idx2]), rms2Lat))
678  self.log.info("Red star offsets'': %.3f %.3f, %.3f %.3f" %
679  (numpy.median(dlong[idx3]), rms3Long,
680  numpy.median(dlat[idx3]), rms3Lat))
681 
682  self.metadata.add("RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
683  self.metadata.add("RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
684  self.metadata.add("RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
685  self.metadata.add("RegisterBlueLongOffsetStd", rms1Long)
686  self.metadata.add("RegisterGreenLongOffsetStd", rms2Long)
687  self.metadata.add("RegisterRedLongOffsetStd", rms3Long)
688 
689  self.metadata.add("RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
690  self.metadata.add("RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
691  self.metadata.add("RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
692  self.metadata.add("RegisterBlueLatOffsetStd", rms1Lat)
693  self.metadata.add("RegisterGreenLatOffsetStd", rms2Lat)
694  self.metadata.add("RegisterRedLatOffsetStd", rms3Lat)
695 
696  # warp template exposure to match exposure,
697  # PSF match template exposure to exposure,
698  # then return the difference
699 
700  # Return warped template... Construct sourceKernelCand list after subtract
701  self.log.info("Subtracting images")
702  subtractRes = self.subtract.subtractExposures(
703  templateExposure=templateExposure,
704  scienceExposure=exposure,
705  candidateList=kernelSources,
706  convolveTemplate=self.config.convolveTemplate,
707  doWarping=not self.config.doUseRegister
708  )
709  subtractedExposure = subtractRes.subtractedExposure
710 
711  if self.config.doDetection:
712  self.log.info("Computing diffim PSF")
713 
714  # Get Psf from the appropriate input image if it doesn't exist
715  if not subtractedExposure.hasPsf():
716  if self.config.convolveTemplate:
717  subtractedExposure.setPsf(exposure.getPsf())
718  else:
719  subtractedExposure.setPsf(templateExposure.getPsf())
720 
721  # If doSubtract is False, then subtractedExposure was fetched from disk (above),
722  # thus it may have already been decorrelated. Thus, we do not decorrelate if
723  # doSubtract is False.
724 
725  # NOTE: At this point doSubtract == True
726  if self.config.doDecorrelation and self.config.doSubtract:
727  preConvKernel = None
728  if preConvPsf is not None:
729  preConvKernel = preConvPsf.getLocalKernel()
730  if self.config.convolveTemplate:
731  self.log.info("Decorrelation after template image convolution")
732  decorrResult = self.decorrelate.run(exposure, templateExposure,
733  subtractedExposure,
734  subtractRes.psfMatchingKernel,
735  spatiallyVarying=self.config.doSpatiallyVarying,
736  preConvKernel=preConvKernel)
737  else:
738  self.log.info("Decorrelation after science image convolution")
739  decorrResult = self.decorrelate.run(templateExposure, exposure,
740  subtractedExposure,
741  subtractRes.psfMatchingKernel,
742  spatiallyVarying=self.config.doSpatiallyVarying,
743  preConvKernel=preConvKernel)
744  subtractedExposure = decorrResult.correctedExposure
745 
746  # END (if subtractAlgorithm == 'AL')
747  # END (if self.config.doSubtract)
748  if self.config.doDetection:
749  self.log.info("Running diaSource detection")
750  # Erase existing detection mask planes
751  mask = subtractedExposure.getMaskedImage().getMask()
752  mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
753 
754  table = afwTable.SourceTable.make(self.schema, idFactory)
755  table.setMetadata(self.algMetadata)
756  results = self.detection.run(
757  table=table,
758  exposure=subtractedExposure,
759  doSmooth=not self.config.doPreConvolve
760  )
761 
762  if self.config.doMerge:
763  fpSet = results.fpSets.positive
764  fpSet.merge(results.fpSets.negative, self.config.growFootprint,
765  self.config.growFootprint, False)
766  diaSources = afwTable.SourceCatalog(table)
767  fpSet.makeSources(diaSources)
768  self.log.info("Merging detections into %d sources" % (len(diaSources)))
769  else:
770  diaSources = results.sources
771 
772  if self.config.doMeasurement:
773  newDipoleFitting = self.config.doDipoleFitting
774  self.log.info("Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
775  if not newDipoleFitting:
776  # Just fit dipole in diffim
777  self.measurement.run(diaSources, subtractedExposure)
778  else:
779  # Use (matched) template and science image (if avail.) to constrain dipole fitting
780  if self.config.doSubtract and 'matchedExposure' in subtractRes.getDict():
781  self.measurement.run(diaSources, subtractedExposure, exposure,
782  subtractRes.matchedExposure)
783  else:
784  self.measurement.run(diaSources, subtractedExposure, exposure)
785 
786  if self.config.doForcedMeasurement:
787  # Run forced psf photometry on the PVI at the diaSource locations.
788  # Copy the measured flux and error into the diaSource.
789  forcedSources = self.forcedMeasurement.generateMeasCat(
790  exposure, diaSources, subtractedExposure.getWcs())
791  self.forcedMeasurement.run(forcedSources, exposure, diaSources, subtractedExposure.getWcs())
792  mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
793  mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFlux")[0],
794  "ip_diffim_forced_PsfFlux_instFlux", True)
795  mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFluxErr")[0],
796  "ip_diffim_forced_PsfFlux_instFluxErr", True)
797  mapper.addMapping(forcedSources.schema.find("base_PsfFlux_area")[0],
798  "ip_diffim_forced_PsfFlux_area", True)
799  mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag")[0],
800  "ip_diffim_forced_PsfFlux_flag", True)
801  mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_noGoodPixels")[0],
802  "ip_diffim_forced_PsfFlux_flag_noGoodPixels", True)
803  mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_edge")[0],
804  "ip_diffim_forced_PsfFlux_flag_edge", True)
805  for diaSource, forcedSource in zip(diaSources, forcedSources):
806  diaSource.assign(forcedSource, mapper)
807 
808  # Match with the calexp sources if possible
809  if self.config.doMatchSources:
810  if selectSources is not None:
811  # Create key,val pair where key=diaSourceId and val=sourceId
812  matchRadAsec = self.config.diaSourceMatchRadius
813  matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
814 
815  srcMatches = afwTable.matchXy(selectSources, diaSources, matchRadPixel)
816  srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId()) for
817  srcMatch in srcMatches])
818  self.log.info("Matched %d / %d diaSources to sources" % (len(srcMatchDict),
819  len(diaSources)))
820  else:
821  self.log.warn("Src product does not exist; cannot match with diaSources")
822  srcMatchDict = {}
823 
824  # Create key,val pair where key=diaSourceId and val=refId
825  refAstromConfig = AstrometryConfig()
826  refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
827  refAstrometer = AstrometryTask(refAstromConfig)
828  astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
829  refMatches = astromRet.matches
830  if refMatches is None:
831  self.log.warn("No diaSource matches with reference catalog")
832  refMatchDict = {}
833  else:
834  self.log.info("Matched %d / %d diaSources to reference catalog" % (len(refMatches),
835  len(diaSources)))
836  refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId()) for
837  refMatch in refMatches])
838 
839  # Assign source Ids
840  for diaSource in diaSources:
841  sid = diaSource.getId()
842  if sid in srcMatchDict:
843  diaSource.set("srcMatchId", srcMatchDict[sid])
844  if sid in refMatchDict:
845  diaSource.set("refMatchId", refMatchDict[sid])
846 
847  if self.config.doAddMetrics and self.config.doSelectSources:
848  self.log.info("Evaluating metrics and control sample")
849 
850  kernelCandList = []
851  for cell in subtractRes.kernelCellSet.getCellList():
852  for cand in cell.begin(False): # include bad candidates
853  kernelCandList.append(cand)
854 
855  # Get basis list to build control sample kernels
856  basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
857  nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
858 
859  controlCandList = (
860  diffimTools.sourceTableToCandidateList(controlSources,
861  subtractRes.warpedExposure, exposure,
862  self.config.subtract.kernel.active,
863  self.config.subtract.kernel.active.detectionConfig,
864  self.log, doBuild=True, basisList=basisList))
865 
866  KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
867  subtractRes.backgroundModel, dof=nparam)
868  KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
869  subtractRes.backgroundModel)
870 
871  if self.config.doDetection:
872  KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
873  else:
874  KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
875 
876  self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
877  return pipeBase.Struct(
878  subtractedExposure=subtractedExposure,
879  matchedExposure=subtractRes.matchedExposure,
880  subtractRes=subtractRes,
881  diaSources=diaSources,
882  selectSources=selectSources
883  )
884 
885  def fitAstrometry(self, templateSources, templateExposure, selectSources):
886  """Fit the relative astrometry between templateSources and selectSources
887 
888  Todo
889  ----
890 
891  Remove this method. It originally fit a new WCS to the template before calling register.run
892  because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed.
893  It remains because a subtask overrides it.
894  """
895  results = self.register.run(templateSources, templateExposure.getWcs(),
896  templateExposure.getBBox(), selectSources)
897  return results
898 
899  def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
900  """Make debug plots and displays.
901 
902  Todo
903  ----
904  Test and update for current debug display and slot names
905  """
906  import lsstDebug
907  display = lsstDebug.Info(__name__).display
908  showSubtracted = lsstDebug.Info(__name__).showSubtracted
909  showPixelResiduals = lsstDebug.Info(__name__).showPixelResiduals
910  showDiaSources = lsstDebug.Info(__name__).showDiaSources
911  showDipoles = lsstDebug.Info(__name__).showDipoles
912  maskTransparency = lsstDebug.Info(__name__).maskTransparency
913  if display:
914  disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
915  if not maskTransparency:
916  maskTransparency = 0
917  disp.setMaskTransparency(maskTransparency)
918 
919  if display and showSubtracted:
920  disp.mtv(subtractRes.subtractedExposure, title="Subtracted image")
921  mi = subtractRes.subtractedExposure.getMaskedImage()
922  x0, y0 = mi.getX0(), mi.getY0()
923  with disp.Buffering():
924  for s in diaSources:
925  x, y = s.getX() - x0, s.getY() - y0
926  ctype = "red" if s.get("flags_negative") else "yellow"
927  if (s.get("base_PixelFlags_flag_interpolatedCenter") or
928  s.get("base_PixelFlags_flag_saturatedCenter") or
929  s.get("base_PixelFlags_flag_crCenter")):
930  ptype = "x"
931  elif (s.get("base_PixelFlags_flag_interpolated") or
932  s.get("base_PixelFlags_flag_saturated") or
933  s.get("base_PixelFlags_flag_cr")):
934  ptype = "+"
935  else:
936  ptype = "o"
937  disp.dot(ptype, x, y, size=4, ctype=ctype)
938  lsstDebug.frame += 1
939 
940  if display and showPixelResiduals and selectSources:
941  nonKernelSources = []
942  for source in selectSources:
943  if source not in kernelSources:
944  nonKernelSources.append(source)
945 
946  diUtils.plotPixelResiduals(exposure,
947  subtractRes.warpedExposure,
948  subtractRes.subtractedExposure,
949  subtractRes.kernelCellSet,
950  subtractRes.psfMatchingKernel,
951  subtractRes.backgroundModel,
952  nonKernelSources,
953  self.subtract.config.kernel.active.detectionConfig,
954  origVariance=False)
955  diUtils.plotPixelResiduals(exposure,
956  subtractRes.warpedExposure,
957  subtractRes.subtractedExposure,
958  subtractRes.kernelCellSet,
959  subtractRes.psfMatchingKernel,
960  subtractRes.backgroundModel,
961  nonKernelSources,
962  self.subtract.config.kernel.active.detectionConfig,
963  origVariance=True)
964  if display and showDiaSources:
965  flagChecker = SourceFlagChecker(diaSources)
966  isFlagged = [flagChecker(x) for x in diaSources]
967  isDipole = [x.get("ip_diffim_ClassificationDipole_value") for x in diaSources]
968  diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
969  frame=lsstDebug.frame)
970  lsstDebug.frame += 1
971 
972  if display and showDipoles:
973  DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
974  frame=lsstDebug.frame)
975  lsstDebug.frame += 1
976 
977  def _getConfigName(self):
978  """Return the name of the config dataset
979  """
980  return "%sDiff_config" % (self.config.coaddName,)
981 
982  def _getMetadataName(self):
983  """Return the name of the metadata dataset
984  """
985  return "%sDiff_metadata" % (self.config.coaddName,)
986 
987  def getSchemaCatalogs(self):
988  """Return a dict of empty catalogs for each catalog dataset produced by this task."""
989  diaSrc = afwTable.SourceCatalog(self.schema)
990  diaSrc.getTable().setMetadata(self.algMetadata)
991  return {self.config.coaddName + "Diff_diaSrc": diaSrc}
992 
993  @classmethod
994  def _makeArgumentParser(cls):
995  """Create an argument parser
996  """
997  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
998  parser.add_id_argument("--id", "calexp", help="data ID, e.g. --id visit=12345 ccd=1,2")
999  parser.add_id_argument("--templateId", "calexp", doMakeDataRefList=True,
1000  help="Template data ID in case of calexp template,"
1001  " e.g. --templateId visit=6789")
1002  return parser
1003 
1004 
1006  winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1007  doc="Shift stars going into RegisterTask by this amount")
1008  winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1009  doc="Perturb stars going into RegisterTask by this amount")
1010 
1011  def setDefaults(self):
1012  ImageDifferenceConfig.setDefaults(self)
1013  self.getTemplate.retarget(GetCalexpAsTemplateTask)
1014 
1015 
1017  """!Image difference Task used in the Winter 2013 data challege.
1018  Enables testing the effects of registration shifts and scatter.
1019 
1020  For use with winter 2013 simulated images:
1021  Use --templateId visit=88868666 for sparse data
1022  --templateId visit=22222200 for dense data (g)
1023  --templateId visit=11111100 for dense data (i)
1024  """
1025  ConfigClass = Winter2013ImageDifferenceConfig
1026  _DefaultName = "winter2013ImageDifference"
1027 
1028  def __init__(self, **kwargs):
1029  ImageDifferenceTask.__init__(self, **kwargs)
1030 
1031  def fitAstrometry(self, templateSources, templateExposure, selectSources):
1032  """Fit the relative astrometry between templateSources and selectSources"""
1033  if self.config.winter2013WcsShift > 0.0:
1034  offset = geom.Extent2D(self.config.winter2013WcsShift,
1035  self.config.winter2013WcsShift)
1036  cKey = templateSources[0].getTable().getCentroidKey()
1037  for source in templateSources:
1038  centroid = source.get(cKey)
1039  source.set(cKey, centroid + offset)
1040  elif self.config.winter2013WcsRms > 0.0:
1041  cKey = templateSources[0].getTable().getCentroidKey()
1042  for source in templateSources:
1043  offset = geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1044  self.config.winter2013WcsRms*numpy.random.normal())
1045  centroid = source.get(cKey)
1046  source.set(cKey, centroid + offset)
1047 
1048  results = self.register.run(templateSources, templateExposure.getWcs(),
1049  templateExposure.getBBox(), selectSources)
1050  return results
def __init__(self, butler=None, kwargs)
Construct an ImageDifference Task.
def fitAstrometry(self, templateSources, templateExposure, selectSources)
def runDataRef(self, sensorRef, templateIdList=None)
def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources)
Image difference Task used in the Winter 2013 data challege.
def fitAstrometry(self, templateSources, templateExposure, selectSources)
def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None, idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None)