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