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