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