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