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