LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
LSSTDataManagementBasePackage
imageDifference.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2012 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
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.extensions.astrometryNet import LoadAstrometryNetObjectsTask
35 from lsst.pipe.tasks.registerImage import RegisterTask
36 from lsst.meas.algorithms import SourceDetectionTask, SingleGaussianPsf, \
37  ObjectSizeStarSelectorTask
38 from lsst.ip.diffim import DipoleAnalysis, \
39  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 
46 FwhmPerSigma = 2 * math.sqrt(2 * math.log(2))
47 IqrToSigma = 0.741
48 
49 
50 class ImageDifferenceConfig(pexConfig.Config):
51  """Config for ImageDifferenceTask
52  """
53  doAddCalexpBackground = pexConfig.Field(dtype=bool, default=True,
54  doc="Add background to calexp before processing it. "
55  "Useful as ipDiffim does background matching.")
56  doUseRegister = pexConfig.Field(dtype=bool, default=True,
57  doc="Use image-to-image registration to align template with "
58  "science image")
59  doDebugRegister = pexConfig.Field(dtype=bool, default=False,
60  doc="Writing debugging data for doUseRegister")
61  doSelectSources = pexConfig.Field(dtype=bool, default=True,
62  doc="Select stars to use for kernel fitting")
63  doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=False,
64  doc="Select stars of extreme color as part of the control sample")
65  doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=False,
66  doc="Select stars that are variable to be part "
67  "of the control sample")
68  doSubtract = pexConfig.Field(dtype=bool, default=True, doc="Compute subtracted exposure?")
69  doPreConvolve = pexConfig.Field(dtype=bool, default=True,
70  doc="Convolve science image by its PSF before PSF-matching?")
71  useGaussianForPreConvolution = pexConfig.Field(dtype=bool, default=True,
72  doc="Use a simple gaussian PSF model for pre-convolution "
73  "(else use fit PSF)? Ignored if doPreConvolve false.")
74  doDetection = pexConfig.Field(dtype=bool, default=True, doc="Detect sources?")
75  doDecorrelation = pexConfig.Field(dtype=bool, default=False,
76  doc="Perform diffim decorrelation to undo pixel correlation due to A&L "
77  "kernel convolution? If True, also update the diffim PSF.")
78  doMerge = pexConfig.Field(dtype=bool, default=True,
79  doc="Merge positive and negative diaSources with grow radius "
80  "set by growFootprint")
81  doMatchSources = pexConfig.Field(dtype=bool, default=True,
82  doc="Match diaSources with input calexp sources and ref catalog sources")
83  doMeasurement = pexConfig.Field(dtype=bool, default=True, doc="Measure diaSources?")
84  doDipoleFitting = pexConfig.Field(dtype=bool, default=True, doc="Measure dipoles using new algorithm?")
85  doForcedMeasurement = pexConfig.Field(dtype=bool, default=True,
86  doc="Force photometer diaSource locations on PVI?")
87  doWriteSubtractedExp = pexConfig.Field(dtype=bool, default=True, doc="Write difference exposure?")
88  doWriteMatchedExp = pexConfig.Field(dtype=bool, default=False,
89  doc="Write warped and PSF-matched template coadd exposure?")
90  doWriteSources = pexConfig.Field(dtype=bool, default=True, doc="Write sources?")
91  doAddMetrics = pexConfig.Field(dtype=bool, default=True,
92  doc="Add columns to the source table to hold analysis metrics?")
93 
94  coaddName = pexConfig.Field(
95  doc="coadd name: typically one of deep, goodSeeing, or dcr",
96  dtype=str,
97  default="deep",
98  )
99  convolveTemplate = pexConfig.Field(
100  doc="Which image gets convolved (default = template)",
101  dtype=bool,
102  default=True
103  )
104  refObjLoader = pexConfig.ConfigurableField(
105  target=LoadAstrometryNetObjectsTask,
106  doc="reference object loader",
107  )
108  astrometer = pexConfig.ConfigurableField(
109  target=AstrometryTask,
110  doc="astrometry task; used to match sources to reference objects, but not to fit a WCS",
111  )
112  sourceSelector = pexConfig.ConfigurableField(
113  target=ObjectSizeStarSelectorTask,
114  doc="Source selection algorithm",
115  )
116  subtract = subtractAlgorithmRegistry.makeField("Subtraction Algorithm", default="al")
117  decorrelate = pexConfig.ConfigurableField(
118  target=DecorrelateALKernelSpatialTask,
119  doc="Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
120  "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
121  "default of 5.5).",
122  )
123  doSpatiallyVarying = pexConfig.Field(
124  dtype=bool,
125  default=False,
126  doc="If using Zogy or A&L decorrelation, perform these on a grid across the "
127  "image in order to allow for spatial variations"
128  )
129  detection = pexConfig.ConfigurableField(
130  target=SourceDetectionTask,
131  doc="Low-threshold detection for final measurement",
132  )
133  measurement = pexConfig.ConfigurableField(
134  target=DipoleFitTask,
135  doc="Enable updated dipole fitting method",
136  )
137  forcedMeasurement = pexConfig.ConfigurableField(
138  target=ForcedMeasurementTask,
139  doc="Subtask to force photometer PVI at diaSource location.",
140  )
141  getTemplate = pexConfig.ConfigurableField(
142  target=GetCoaddAsTemplateTask,
143  doc="Subtask to retrieve template exposure and sources",
144  )
145  controlStepSize = pexConfig.Field(
146  doc="What step size (every Nth one) to select a control sample from the kernelSources",
147  dtype=int,
148  default=5
149  )
150  controlRandomSeed = pexConfig.Field(
151  doc="Random seed for shuffing the control sample",
152  dtype=int,
153  default=10
154  )
155  register = pexConfig.ConfigurableField(
156  target=RegisterTask,
157  doc="Task to enable image-to-image image registration (warping)",
158  )
159  kernelSourcesFromRef = pexConfig.Field(
160  doc="Select sources to measure kernel from reference catalog if True, template if false",
161  dtype=bool,
162  default=False
163  )
164  templateSipOrder = pexConfig.Field(dtype=int, default=2,
165  doc="Sip Order for fitting the Template Wcs "
166  "(default is too high, overfitting)")
167 
168  growFootprint = pexConfig.Field(dtype=int, default=2,
169  doc="Grow positive and negative footprints by this amount before merging")
170 
171  diaSourceMatchRadius = pexConfig.Field(dtype=float, default=0.5,
172  doc="Match radius (in arcseconds) "
173  "for DiaSource to Source association")
174 
175  def setDefaults(self):
176  # defaults are OK for catalog and diacatalog
177 
178  self.subtract['al'].kernel.name = "AL"
179  self.subtract['al'].kernel.active.fitForBackground = True
180  self.subtract['al'].kernel.active.spatialKernelOrder = 1
181  self.subtract['al'].kernel.active.spatialBgOrder = 0
182  self.doPreConvolve = False
183  self.doMatchSources = False
184  self.doAddMetrics = False
185  self.doUseRegister = False
186 
187  # DiaSource Detection
188  self.detection.thresholdPolarity = "both"
189  self.detection.thresholdValue = 5.5
190  self.detection.reEstimateBackground = False
191  self.detection.thresholdType = "pixel_stdev"
192 
193  # Add filtered flux measurement, the correct measurement for pre-convolved images.
194  # Enable all measurements, regardless of doPreConvolve, as it makes data harvesting easier.
195  # To change that you must modify algorithms.names in the task's applyOverrides method,
196  # after the user has set doPreConvolve.
197  self.measurement.algorithms.names.add('base_PeakLikelihoodFlux')
198 
199  self.forcedMeasurement.plugins = ["base_TransformedCentroid", "base_PsfFlux"]
200  self.forcedMeasurement.copyColumns = {
201  "id": "objectId", "parent": "parentObjectId", "coord_ra": "coord_ra", "coord_dec": "coord_dec"}
202  self.forcedMeasurement.slots.centroid = "base_TransformedCentroid"
203  self.forcedMeasurement.slots.shape = None
204 
205  # For shuffling the control sample
206  random.seed(self.controlRandomSeed)
207 
208  def validate(self):
209  pexConfig.Config.validate(self)
210  if self.doMeasurement and not self.doDetection:
211  raise ValueError("Cannot run source measurement without source detection.")
212  if self.doMerge and not self.doDetection:
213  raise ValueError("Cannot run source merging without source detection.")
214  if self.doUseRegister and not self.doSelectSources:
215  raise ValueError("doUseRegister=True and doSelectSources=False. "
216  "Cannot run RegisterTask without selecting sources.")
217  if hasattr(self.getTemplate, "coaddName"):
218  if self.getTemplate.coaddName != self.coaddName:
219  raise ValueError("Mis-matched coaddName and getTemplate.coaddName in the config.")
220 
221 
222 class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
223 
224  @staticmethod
225  def getTargetList(parsedCmd, **kwargs):
226  return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
227  **kwargs)
228 
229 
230 class ImageDifferenceTask(pipeBase.CmdLineTask):
231  """Subtract an image from a template and measure the result
232  """
233  ConfigClass = ImageDifferenceConfig
234  RunnerClass = ImageDifferenceTaskRunner
235  _DefaultName = "imageDifference"
236 
237  def __init__(self, butler=None, **kwargs):
238  """!Construct an ImageDifference Task
239 
240  @param[in] butler Butler object to use in constructing reference object loaders
241  """
242  pipeBase.CmdLineTask.__init__(self, **kwargs)
243  self.makeSubtask("getTemplate")
244 
245  self.makeSubtask("subtract")
246 
247  if self.config.subtract.name == 'al' and self.config.doDecorrelation:
248  self.makeSubtask("decorrelate")
249 
250  if self.config.doUseRegister:
251  self.makeSubtask("register")
252  self.schema = afwTable.SourceTable.makeMinimalSchema()
253 
254  if self.config.doSelectSources:
255  self.makeSubtask("sourceSelector")
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  import lsst.afw.display.ds9 as ds9
766  if not maskTransparency:
767  maskTransparency = 0
768  ds9.setMaskTransparency(maskTransparency)
769 
770  if display and showSubtracted:
771  ds9.mtv(subtractRes.subtractedExposure, frame=lsstDebug.frame, title="Subtracted image")
772  mi = subtractRes.subtractedExposure.getMaskedImage()
773  x0, y0 = mi.getX0(), mi.getY0()
774  with ds9.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("flags.pixel.interpolated.center") or s.get("flags.pixel.saturated.center") or
779  s.get("flags.pixel.cr.center")):
780  ptype = "x"
781  elif (s.get("flags.pixel.interpolated.any") or s.get("flags.pixel.saturated.any") or
782  s.get("flags.pixel.cr.any")):
783  ptype = "+"
784  else:
785  ptype = "o"
786  ds9.dot(ptype, x, y, size=4, frame=lsstDebug.frame, ctype=ctype)
787  lsstDebug.frame += 1
788 
789  if display and showPixelResiduals and selectSources:
790  nonKernelSources = []
791  for source in selectSources:
792  if source not in kernelSources:
793  nonKernelSources.append(source)
794 
795  diUtils.plotPixelResiduals(exposure,
796  subtractRes.warpedExposure,
797  subtractRes.subtractedExposure,
798  subtractRes.kernelCellSet,
799  subtractRes.psfMatchingKernel,
800  subtractRes.backgroundModel,
801  nonKernelSources,
802  self.subtract.config.kernel.active.detectionConfig,
803  origVariance=False)
804  diUtils.plotPixelResiduals(exposure,
805  subtractRes.warpedExposure,
806  subtractRes.subtractedExposure,
807  subtractRes.kernelCellSet,
808  subtractRes.psfMatchingKernel,
809  subtractRes.backgroundModel,
810  nonKernelSources,
811  self.subtract.config.kernel.active.detectionConfig,
812  origVariance=True)
813  if display and showDiaSources:
814  flagChecker = SourceFlagChecker(diaSources)
815  isFlagged = [flagChecker(x) for x in diaSources]
816  isDipole = [x.get("classification.dipole") for x in diaSources]
817  diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
818  frame=lsstDebug.frame)
819  lsstDebug.frame += 1
820 
821  if display and showDipoles:
822  DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
823  frame=lsstDebug.frame)
824  lsstDebug.frame += 1
825 
826  def _getConfigName(self):
827  """Return the name of the config dataset
828  """
829  return "%sDiff_config" % (self.config.coaddName,)
830 
831  def _getMetadataName(self):
832  """Return the name of the metadata dataset
833  """
834  return "%sDiff_metadata" % (self.config.coaddName,)
835 
836  def getSchemaCatalogs(self):
837  """Return a dict of empty catalogs for each catalog dataset produced by this task."""
838  diaSrc = afwTable.SourceCatalog(self.schema)
839  diaSrc.getTable().setMetadata(self.algMetadata)
840  return {self.config.coaddName + "Diff_diaSrc": diaSrc}
841 
842  @classmethod
843  def _makeArgumentParser(cls):
844  """Create an argument parser
845  """
846  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
847  parser.add_id_argument("--id", "calexp", help="data ID, e.g. --id visit=12345 ccd=1,2")
848  parser.add_id_argument("--templateId", "calexp", doMakeDataRefList=True,
849  help="Optional template data ID (visit only), e.g. --templateId visit=6789")
850  return parser
851 
852 
854  winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
855  doc="Shift stars going into RegisterTask by this amount")
856  winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
857  doc="Perturb stars going into RegisterTask by this amount")
858 
859  def setDefaults(self):
860  ImageDifferenceConfig.setDefaults(self)
861  self.getTemplate.retarget(GetCalexpAsTemplateTask)
862 
863 
865  """!Image difference Task used in the Winter 2013 data challege.
866  Enables testing the effects of registration shifts and scatter.
867 
868  For use with winter 2013 simulated images:
869  Use --templateId visit=88868666 for sparse data
870  --templateId visit=22222200 for dense data (g)
871  --templateId visit=11111100 for dense data (i)
872  """
873  ConfigClass = Winter2013ImageDifferenceConfig
874  _DefaultName = "winter2013ImageDifference"
875 
876  def __init__(self, **kwargs):
877  ImageDifferenceTask.__init__(self, **kwargs)
878 
879  def fitAstrometry(self, templateSources, templateExposure, selectSources):
880  """Fit the relative astrometry between templateSources and selectSources"""
881  if self.config.winter2013WcsShift > 0.0:
882  offset = afwGeom.Extent2D(self.config.winter2013WcsShift,
883  self.config.winter2013WcsShift)
884  cKey = templateSources[0].getTable().getCentroidKey()
885  for source in templateSources:
886  centroid = source.get(cKey)
887  source.set(cKey, centroid+offset)
888  elif self.config.winter2013WcsRms > 0.0:
889  cKey = templateSources[0].getTable().getCentroidKey()
890  for source in templateSources:
891  offset = afwGeom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
892  self.config.winter2013WcsRms*numpy.random.normal())
893  centroid = source.get(cKey)
894  source.set(cKey, centroid+offset)
895 
896  results = self.register.run(templateSources, templateExposure.getWcs(),
897  templateExposure.getBBox(), selectSources)
898  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)