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