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