38 from lsst.meas.algorithms import SourceDetectionTask, SingleGaussianPsf, ObjectSizeStarSelectorTask
39 from lsst.ip.diffim import (DipoleAnalysis, SourceFlagChecker, KernelCandidateF, makeKernelBasisList,
40 KernelCandidateQa, DiaCatalogSourceSelectorTask, DiaCatalogSourceSelectorConfig,
41 GetCoaddAsTemplateTask, GetCalexpAsTemplateTask, DipoleFitTask,
42 DecorrelateALKernelSpatialTask, subtractAlgorithmRegistry)
49 __all__ = [
"ImageDifferenceConfig",
"ImageDifferenceTask"]
50 FwhmPerSigma = 2*math.sqrt(2*math.log(2))
55 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
56 defaultTemplates={
"coaddName":
"deep",
61 exposure = pipeBase.connectionTypes.Input(
62 doc=
"Input science exposure to subtract from.",
63 dimensions=(
"instrument",
"visit",
"detector"),
64 storageClass=
"ExposureF",
65 name=
"{fakesType}calexp"
76 skyMap = pipeBase.connectionTypes.Input(
77 doc=
"Input definition of geometry/bbox and projection/wcs for template exposures",
78 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
79 dimensions=(
"skymap", ),
80 storageClass=
"SkyMap",
82 coaddExposures = pipeBase.connectionTypes.Input(
83 doc=
"Input template to match and subtract from the exposure",
84 dimensions=(
"tract",
"patch",
"skymap",
"band"),
85 storageClass=
"ExposureF",
86 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
90 dcrCoadds = pipeBase.connectionTypes.Input(
91 doc=
"Input DCR template to match and subtract from the exposure",
92 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
93 storageClass=
"ExposureF",
94 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
98 outputSchema = pipeBase.connectionTypes.InitOutput(
99 doc=
"Schema (as an example catalog) for output DIASource catalog.",
100 storageClass=
"SourceCatalog",
101 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
103 subtractedExposure = pipeBase.connectionTypes.Output(
104 doc=
"Output difference image",
105 dimensions=(
"instrument",
"visit",
"detector"),
106 storageClass=
"ExposureF",
107 name=
"{fakesType}{coaddName}Diff_differenceExp",
109 warpedExposure = pipeBase.connectionTypes.Output(
110 doc=
"Warped template used to create `subtractedExposure`.",
111 dimensions=(
"instrument",
"visit",
"detector"),
112 storageClass=
"ExposureF",
113 name=
"{fakesType}{coaddName}Diff_warpedExp",
115 diaSources = pipeBase.connectionTypes.Output(
116 doc=
"Output detected diaSources on the difference image",
117 dimensions=(
"instrument",
"visit",
"detector"),
118 storageClass=
"SourceCatalog",
119 name=
"{fakesType}{coaddName}Diff_diaSrc",
122 def __init__(self, *, config=None):
123 super().__init__(config=config)
124 if config.coaddName ==
'dcr':
125 self.inputs.remove(
"coaddExposures")
127 self.inputs.remove(
"dcrCoadds")
133 class ImageDifferenceConfig(pipeBase.PipelineTaskConfig,
134 pipelineConnections=ImageDifferenceTaskConnections):
135 """Config for ImageDifferenceTask
137 doAddCalexpBackground = pexConfig.Field(dtype=bool, default=
False,
138 doc=
"Add background to calexp before processing it. "
139 "Useful as ipDiffim does background matching.")
140 doUseRegister = pexConfig.Field(dtype=bool, default=
True,
141 doc=
"Use image-to-image registration to align template with "
143 doDebugRegister = pexConfig.Field(dtype=bool, default=
False,
144 doc=
"Writing debugging data for doUseRegister")
145 doSelectSources = pexConfig.Field(dtype=bool, default=
True,
146 doc=
"Select stars to use for kernel fitting")
147 doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=
False,
148 doc=
"Select stars of extreme color as part of the control sample")
149 doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=
False,
150 doc=
"Select stars that are variable to be part "
151 "of the control sample")
152 doSubtract = pexConfig.Field(dtype=bool, default=
True, doc=
"Compute subtracted exposure?")
153 doPreConvolve = pexConfig.Field(dtype=bool, default=
True,
154 doc=
"Convolve science image by its PSF before PSF-matching?")
155 doScaleTemplateVariance = pexConfig.Field(dtype=bool, default=
False,
156 doc=
"Scale variance of the template before PSF matching")
157 doScaleDiffimVariance = pexConfig.Field(dtype=bool, default=
False,
158 doc=
"Scale variance of the diffim before PSF matching. "
159 "You may do either this or template variance scaling, "
160 "or neither. (Doing both is a waste of CPU.)")
161 useGaussianForPreConvolution = pexConfig.Field(dtype=bool, default=
True,
162 doc=
"Use a simple gaussian PSF model for pre-convolution "
163 "(else use fit PSF)? Ignored if doPreConvolve false.")
164 doDetection = pexConfig.Field(dtype=bool, default=
True, doc=
"Detect sources?")
165 doDecorrelation = pexConfig.Field(dtype=bool, default=
False,
166 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
167 "kernel convolution? If True, also update the diffim PSF.")
168 doMerge = pexConfig.Field(dtype=bool, default=
True,
169 doc=
"Merge positive and negative diaSources with grow radius "
170 "set by growFootprint")
171 doMatchSources = pexConfig.Field(dtype=bool, default=
True,
172 doc=
"Match diaSources with input calexp sources and ref catalog sources")
173 doMeasurement = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure diaSources?")
174 doDipoleFitting = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure dipoles using new algorithm?")
175 doForcedMeasurement = pexConfig.Field(
178 doc=
"Force photometer diaSource locations on PVI?")
179 doWriteSubtractedExp = pexConfig.Field(dtype=bool, default=
True, doc=
"Write difference exposure?")
180 doWriteWarpedExp = pexConfig.Field(dtype=bool, default=
False,
181 doc=
"Write WCS, warped template coadd exposure?")
182 doWriteMatchedExp = pexConfig.Field(dtype=bool, default=
False,
183 doc=
"Write warped and PSF-matched template coadd exposure?")
184 doWriteSources = pexConfig.Field(dtype=bool, default=
True, doc=
"Write sources?")
185 doAddMetrics = pexConfig.Field(dtype=bool, default=
True,
186 doc=
"Add columns to the source table to hold analysis metrics?")
188 coaddName = pexConfig.Field(
189 doc=
"coadd name: typically one of deep, goodSeeing, or dcr",
193 convolveTemplate = pexConfig.Field(
194 doc=
"Which image gets convolved (default = template)",
198 refObjLoader = pexConfig.ConfigurableField(
199 target=LoadIndexedReferenceObjectsTask,
200 doc=
"reference object loader",
202 astrometer = pexConfig.ConfigurableField(
203 target=AstrometryTask,
204 doc=
"astrometry task; used to match sources to reference objects, but not to fit a WCS",
206 sourceSelector = pexConfig.ConfigurableField(
207 target=ObjectSizeStarSelectorTask,
208 doc=
"Source selection algorithm",
210 subtract = subtractAlgorithmRegistry.makeField(
"Subtraction Algorithm", default=
"al")
211 decorrelate = pexConfig.ConfigurableField(
212 target=DecorrelateALKernelSpatialTask,
213 doc=
"Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
214 "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
217 doSpatiallyVarying = pexConfig.Field(
220 doc=
"If using Zogy or A&L decorrelation, perform these on a grid across the "
221 "image in order to allow for spatial variations"
223 detection = pexConfig.ConfigurableField(
224 target=SourceDetectionTask,
225 doc=
"Low-threshold detection for final measurement",
227 measurement = pexConfig.ConfigurableField(
228 target=DipoleFitTask,
229 doc=
"Enable updated dipole fitting method",
234 doc=
"Run subtask to apply aperture corrections"
237 target=ApplyApCorrTask,
238 doc=
"Subtask to apply aperture corrections"
240 forcedMeasurement = pexConfig.ConfigurableField(
241 target=ForcedMeasurementTask,
242 doc=
"Subtask to force photometer PVI at diaSource location.",
244 getTemplate = pexConfig.ConfigurableField(
245 target=GetCoaddAsTemplateTask,
246 doc=
"Subtask to retrieve template exposure and sources",
248 scaleVariance = pexConfig.ConfigurableField(
249 target=ScaleVarianceTask,
250 doc=
"Subtask to rescale the variance of the template "
251 "to the statistically expected level"
253 controlStepSize = pexConfig.Field(
254 doc=
"What step size (every Nth one) to select a control sample from the kernelSources",
258 controlRandomSeed = pexConfig.Field(
259 doc=
"Random seed for shuffing the control sample",
263 register = pexConfig.ConfigurableField(
265 doc=
"Task to enable image-to-image image registration (warping)",
267 kernelSourcesFromRef = pexConfig.Field(
268 doc=
"Select sources to measure kernel from reference catalog if True, template if false",
272 templateSipOrder = pexConfig.Field(
273 dtype=int, default=2,
274 doc=
"Sip Order for fitting the Template Wcs (default is too high, overfitting)"
276 growFootprint = pexConfig.Field(
277 dtype=int, default=2,
278 doc=
"Grow positive and negative footprints by this amount before merging"
280 diaSourceMatchRadius = pexConfig.Field(
281 dtype=float, default=0.5,
282 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
288 self.subtract[
'al'].kernel.name =
"AL"
289 self.subtract[
'al'].kernel.active.fitForBackground =
True
290 self.subtract[
'al'].kernel.active.spatialKernelOrder = 1
291 self.subtract[
'al'].kernel.active.spatialBgOrder = 2
292 self.doPreConvolve =
False
293 self.doMatchSources =
False
294 self.doAddMetrics =
False
295 self.doUseRegister =
False
298 self.detection.thresholdPolarity =
"both"
299 self.detection.thresholdValue = 5.5
300 self.detection.reEstimateBackground =
False
301 self.detection.thresholdType =
"pixel_stdev"
307 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
308 self.measurement.plugins.names |= [
'base_LocalPhotoCalib',
311 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
312 self.forcedMeasurement.copyColumns = {
313 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
314 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
315 self.forcedMeasurement.slots.shape =
None
318 random.seed(self.controlRandomSeed)
321 pexConfig.Config.validate(self)
322 if self.doAddMetrics
and not self.doSubtract:
323 raise ValueError(
"Subtraction must be enabled for kernel metrics calculation.")
324 if not self.doSubtract
and not self.doDetection:
325 raise ValueError(
"Either doSubtract or doDetection must be enabled.")
326 if self.subtract.name ==
'zogy' and self.doAddMetrics:
327 raise ValueError(
"Kernel metrics does not exist in zogy subtraction.")
328 if self.doMeasurement
and not self.doDetection:
329 raise ValueError(
"Cannot run source measurement without source detection.")
330 if self.doMerge
and not self.doDetection:
331 raise ValueError(
"Cannot run source merging without source detection.")
332 if self.doUseRegister
and not self.doSelectSources:
333 raise ValueError(
"doUseRegister=True and doSelectSources=False. "
334 "Cannot run RegisterTask without selecting sources.")
335 if self.doPreConvolve
and self.doDecorrelation
and not self.convolveTemplate:
336 raise ValueError(
"doPreConvolve=True and doDecorrelation=True and "
337 "convolveTemplate=False is not supported.")
338 if hasattr(self.getTemplate,
"coaddName"):
339 if self.getTemplate.coaddName != self.coaddName:
340 raise ValueError(
"Mis-matched coaddName and getTemplate.coaddName in the config.")
341 if self.doScaleDiffimVariance
and self.doScaleTemplateVariance:
342 raise ValueError(
"Scaling the diffim variance and scaling the template variance "
343 "are both set. Please choose one or the other.")
346 class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
349 def getTargetList(parsedCmd, **kwargs):
350 return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
354 class ImageDifferenceTask(pipeBase.CmdLineTask, pipeBase.PipelineTask):
355 """Subtract an image from a template and measure the result
357 ConfigClass = ImageDifferenceConfig
358 RunnerClass = ImageDifferenceTaskRunner
359 _DefaultName =
"imageDifference"
361 def __init__(self, butler=None, **kwargs):
362 """!Construct an ImageDifference Task
364 @param[in] butler Butler object to use in constructing reference object loaders
366 super().__init__(**kwargs)
367 self.makeSubtask(
"getTemplate")
369 self.makeSubtask(
"subtract")
371 if self.config.subtract.name ==
'al' and self.config.doDecorrelation:
372 self.makeSubtask(
"decorrelate")
374 if self.config.doScaleTemplateVariance
or self.config.doScaleDiffimVariance:
375 self.makeSubtask(
"scaleVariance")
377 if self.config.doUseRegister:
378 self.makeSubtask(
"register")
379 self.schema = afwTable.SourceTable.makeMinimalSchema()
381 if self.config.doSelectSources:
382 self.makeSubtask(
"sourceSelector")
383 if self.config.kernelSourcesFromRef:
384 self.makeSubtask(
'refObjLoader', butler=butler)
385 self.makeSubtask(
"astrometer", refObjLoader=self.refObjLoader)
388 if self.config.doDetection:
389 self.makeSubtask(
"detection", schema=self.schema)
390 if self.config.doMeasurement:
391 self.makeSubtask(
"measurement", schema=self.schema,
392 algMetadata=self.algMetadata)
393 if self.config.doApCorr:
394 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
395 if self.config.doForcedMeasurement:
396 self.schema.addField(
397 "ip_diffim_forced_PsfFlux_instFlux",
"D",
398 "Forced PSF flux measured on the direct image.",
400 self.schema.addField(
401 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
402 "Forced PSF flux error measured on the direct image.",
404 self.schema.addField(
405 "ip_diffim_forced_PsfFlux_area",
"F",
406 "Forced PSF flux effective area of PSF.",
408 self.schema.addField(
409 "ip_diffim_forced_PsfFlux_flag",
"Flag",
410 "Forced PSF flux general failure flag.")
411 self.schema.addField(
412 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
413 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
414 self.schema.addField(
415 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
416 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
417 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
418 if self.config.doMatchSources:
419 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
420 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
424 self.outputSchema.getTable().setMetadata(self.algMetadata)
427 def makeIdFactory(expId, expBits):
428 """Create IdFactory instance for unique 64 bit diaSource id-s.
436 Number of used bits in ``expId``.
440 The diasource id-s consists of the ``expId`` stored fixed in the highest value
441 ``expBits`` of the 64-bit integer plus (bitwise or) a generated sequence number in the
442 low value end of the integer.
446 idFactory: `lsst.afw.table.IdFactory`
448 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
450 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
451 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
452 inputRefs: pipeBase.InputQuantizedConnection,
453 outputRefs: pipeBase.OutputQuantizedConnection):
454 inputs = butlerQC.get(inputRefs)
455 self.log.
info(f
"Processing {butlerQC.quantum.dataId}")
456 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
458 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
459 if self.config.coaddName ==
'dcr':
460 templateExposures = inputRefs.dcrCoadds
462 templateExposures = inputRefs.coaddExposures
463 templateStruct = self.getTemplate.runQuantum(
464 inputs[
'exposure'], butlerQC, inputRefs.skyMap, templateExposures
467 outputs = self.run(exposure=inputs[
'exposure'],
468 templateExposure=templateStruct.exposure,
470 butlerQC.put(outputs, outputRefs)
473 def runDataRef(self, sensorRef, templateIdList=None):
474 """Subtract an image from a template coadd and measure the result.
476 Data I/O wrapper around `run` using the butler in Gen2.
480 sensorRef : `lsst.daf.persistence.ButlerDataRef`
481 Sensor-level butler data reference, used for the following data products:
488 - self.config.coaddName + "Coadd_skyMap"
489 - self.config.coaddName + "Coadd"
490 Input or output, depending on config:
491 - self.config.coaddName + "Diff_subtractedExp"
492 Output, depending on config:
493 - self.config.coaddName + "Diff_matchedExp"
494 - self.config.coaddName + "Diff_src"
498 results : `lsst.pipe.base.Struct`
499 Returns the Struct by `run`.
501 subtractedExposureName = self.config.coaddName +
"Diff_differenceExp"
502 subtractedExposure =
None
504 calexpBackgroundExposure =
None
505 self.log.
info(
"Processing %s" % (sensorRef.dataId))
510 idFactory = self.makeIdFactory(expId=int(sensorRef.get(
"ccdExposureId")),
511 expBits=sensorRef.get(
"ccdExposureId_bits"))
512 if self.config.doAddCalexpBackground:
513 calexpBackgroundExposure = sensorRef.get(
"calexpBackground")
516 exposure = sensorRef.get(
"calexp", immediate=
True)
519 template = self.getTemplate.runDataRef(exposure, sensorRef, templateIdList=templateIdList)
521 if sensorRef.datasetExists(
"src"):
522 self.log.
info(
"Source selection via src product")
524 selectSources = sensorRef.get(
"src")
526 if not self.config.doSubtract
and self.config.doDetection:
528 subtractedExposure = sensorRef.get(subtractedExposureName)
531 results = self.run(exposure=exposure,
532 selectSources=selectSources,
533 templateExposure=template.exposure,
534 templateSources=template.sources,
536 calexpBackgroundExposure=calexpBackgroundExposure,
537 subtractedExposure=subtractedExposure)
539 if self.config.doWriteSources
and results.diaSources
is not None:
540 sensorRef.put(results.diaSources, self.config.coaddName +
"Diff_diaSrc")
541 if self.config.doWriteWarpedExp:
542 sensorRef.put(results.warpedExposure, self.config.coaddName +
"Diff_warpedExp")
543 if self.config.doWriteMatchedExp:
544 sensorRef.put(results.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
545 if self.config.doAddMetrics
and self.config.doSelectSources:
546 sensorRef.put(results.selectSources, self.config.coaddName +
"Diff_kernelSrc")
547 if self.config.doWriteSubtractedExp:
548 sensorRef.put(results.subtractedExposure, subtractedExposureName)
552 def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
553 idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
554 """PSF matches, subtract two images and perform detection on the difference image.
558 exposure : `lsst.afw.image.ExposureF`, optional
559 The science exposure, the minuend in the image subtraction.
560 Can be None only if ``config.doSubtract==False``.
561 selectSources : `lsst.afw.table.SourceCatalog`, optional
562 Identified sources on the science exposure. This catalog is used to
563 select sources in order to perform the AL PSF matching on stamp images
564 around them. The selection steps depend on config options and whether
565 ``templateSources`` and ``matchingSources`` specified.
566 templateExposure : `lsst.afw.image.ExposureF`, optional
567 The template to be subtracted from ``exposure`` in the image subtraction.
568 The template exposure should cover the same sky area as the science exposure.
569 It is either a stich of patches of a coadd skymap image or a calexp
570 of the same pointing as the science exposure. Can be None only
571 if ``config.doSubtract==False`` and ``subtractedExposure`` is not None.
572 templateSources : `lsst.afw.table.SourceCatalog`, optional
573 Identified sources on the template exposure.
574 idFactory : `lsst.afw.table.IdFactory`
575 Generator object to assign ids to detected sources in the difference image.
576 calexpBackgroundExposure : `lsst.afw.image.ExposureF`, optional
577 Background exposure to be added back to the science exposure
578 if ``config.doAddCalexpBackground==True``
579 subtractedExposure : `lsst.afw.image.ExposureF`, optional
580 If ``config.doSubtract==False`` and ``config.doDetection==True``,
581 performs the post subtraction source detection only on this exposure.
582 Otherwise should be None.
586 results : `lsst.pipe.base.Struct`
587 ``subtractedExposure`` : `lsst.afw.image.ExposureF`
589 ``matchedExposure`` : `lsst.afw.image.ExposureF`
590 The matched PSF exposure.
591 ``subtractRes`` : `lsst.pipe.base.Struct`
592 The returned result structure of the ImagePsfMatchTask subtask.
593 ``diaSources`` : `lsst.afw.table.SourceCatalog`
594 The catalog of detected sources.
595 ``selectSources`` : `lsst.afw.table.SourceCatalog`
596 The input source catalog with optionally added Qa information.
600 The following major steps are included:
602 - warp template coadd to match WCS of image
603 - PSF match image to warped template
604 - subtract image from PSF-matched, warped template
608 For details about the image subtraction configuration modes
609 see `lsst.ip.diffim`.
612 controlSources =
None
616 if self.config.doAddCalexpBackground:
617 mi = exposure.getMaskedImage()
618 mi += calexpBackgroundExposure.getImage()
620 if not exposure.hasPsf():
621 raise pipeBase.TaskError(
"Exposure has no psf")
622 sciencePsf = exposure.getPsf()
624 if self.config.doSubtract:
625 if self.config.doScaleTemplateVariance:
626 self.log.
info(
"Rescaling template variance")
627 templateVarFactor = self.scaleVariance.
run(
628 templateExposure.getMaskedImage())
629 self.log.
info(
"Template variance scaling factor: %.2f" % templateVarFactor)
630 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
632 if self.config.subtract.name ==
'zogy':
633 subtractRes = self.subtract.
run(exposure, templateExposure, doWarping=
True)
634 if self.config.doPreConvolve:
635 subtractedExposure = subtractRes.scoreExp
637 subtractedExposure = subtractRes.diffExp
638 subtractRes.subtractedExposure = subtractedExposure
639 subtractRes.matchedExposure =
None
641 elif self.config.subtract.name ==
'al':
643 scienceSigmaOrig = sciencePsf.computeShape().getDeterminantRadius()
644 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
652 if self.config.doPreConvolve:
655 srcMI = exposure.getMaskedImage()
656 exposureOrig = exposure.clone()
657 destMI = srcMI.Factory(srcMI.getDimensions())
659 if self.config.useGaussianForPreConvolution:
661 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
667 exposure.setMaskedImage(destMI)
668 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
670 scienceSigmaPost = scienceSigmaOrig
671 exposureOrig = exposure
676 if self.config.doSelectSources:
677 if selectSources
is None:
678 self.log.
warn(
"Src product does not exist; running detection, measurement, selection")
680 selectSources = self.subtract.getSelectSources(
682 sigma=scienceSigmaPost,
683 doSmooth=
not self.config.doPreConvolve,
687 if self.config.doAddMetrics:
691 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
692 targetFwhmPix=templateSigma*FwhmPerSigma))
700 selectSources = kcQa.addToSchema(selectSources)
701 if self.config.kernelSourcesFromRef:
703 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
704 matches = astromRet.matches
705 elif templateSources:
708 mc.findOnlyClosest =
False
712 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False,"
713 "but template sources not available. Cannot match science "
714 "sources with template sources. Run process* on data from "
715 "which templates are built.")
717 kernelSources = self.sourceSelector.
run(selectSources, exposure=exposure,
718 matches=matches).sourceCat
719 random.shuffle(kernelSources, random.random)
720 controlSources = kernelSources[::self.config.controlStepSize]
721 kernelSources = [k
for i, k
in enumerate(kernelSources)
722 if i % self.config.controlStepSize]
724 if self.config.doSelectDcrCatalog:
728 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
729 controlSources.extend(redSources)
733 grMax=self.sourceSelector.config.grMin))
734 blueSources = blueSelector.selectStars(exposure, selectSources,
735 matches=matches).starCat
736 controlSources.extend(blueSources)
738 if self.config.doSelectVariableCatalog:
741 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
742 controlSources.extend(varSources)
744 self.log.
info(
"Selected %d / %d sources for Psf matching (%d for control sample)"
745 % (len(kernelSources), len(selectSources), len(controlSources)))
749 if self.config.doUseRegister:
750 self.log.
info(
"Registering images")
752 if templateSources
is None:
756 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
757 templateSources = self.subtract.getSelectSources(
766 wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
767 warpedExp = self.register.
warpExposure(templateExposure, wcsResults.wcs,
768 exposure.getWcs(), exposure.getBBox())
769 templateExposure = warpedExp
774 if self.config.doDebugRegister:
776 srcToMatch = {x.second.getId(): x.first
for x
in matches}
778 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
779 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidSlot().getMeasKey()
780 sids = [m.first.getId()
for m
in wcsResults.matches]
781 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
782 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
783 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
784 allresids = dict(zip(sids, zip(positions, residuals)))
786 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
787 wcsResults.wcs.pixelToSky(
788 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
789 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
790 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
791 for x
in sids
if x
in srcToMatch.keys()])
792 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
793 if s
in srcToMatch.keys()])
794 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
795 if s
in srcToMatch.keys()])
796 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
797 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
798 & (colors <= self.sourceSelector.config.grMax))
799 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
800 rms1Long = IqrToSigma*(
801 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
802 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
803 - numpy.percentile(dlat[idx1], 25))
804 rms2Long = IqrToSigma*(
805 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
806 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
807 - numpy.percentile(dlat[idx2], 25))
808 rms3Long = IqrToSigma*(
809 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
810 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
811 - numpy.percentile(dlat[idx3], 25))
812 self.log.
info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f" %
813 (numpy.median(dlong[idx1]), rms1Long,
814 numpy.median(dlat[idx1]), rms1Lat))
815 self.log.
info(
"Green star offsets'': %.3f %.3f, %.3f %.3f" %
816 (numpy.median(dlong[idx2]), rms2Long,
817 numpy.median(dlat[idx2]), rms2Lat))
818 self.log.
info(
"Red star offsets'': %.3f %.3f, %.3f %.3f" %
819 (numpy.median(dlong[idx3]), rms3Long,
820 numpy.median(dlat[idx3]), rms3Lat))
822 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
823 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
824 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
825 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
826 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
827 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
829 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
830 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
831 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
832 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
833 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
834 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
841 self.log.
info(
"Subtracting images")
842 subtractRes = self.subtract.subtractExposures(
843 templateExposure=templateExposure,
844 scienceExposure=exposure,
845 candidateList=kernelSources,
846 convolveTemplate=self.config.convolveTemplate,
847 doWarping=
not self.config.doUseRegister
849 subtractedExposure = subtractRes.subtractedExposure
851 if self.config.doDetection:
852 self.log.
info(
"Computing diffim PSF")
855 if not subtractedExposure.hasPsf():
856 if self.config.convolveTemplate:
857 subtractedExposure.setPsf(exposure.getPsf())
859 subtractedExposure.setPsf(templateExposure.getPsf())
866 if self.config.doDecorrelation
and self.config.doSubtract:
868 if preConvPsf
is not None:
869 preConvKernel = preConvPsf.getLocalKernel()
870 decorrResult = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
872 subtractRes.psfMatchingKernel,
873 spatiallyVarying=self.config.doSpatiallyVarying,
874 preConvKernel=preConvKernel,
875 templateMatched=self.config.convolveTemplate)
876 subtractedExposure = decorrResult.correctedExposure
880 if self.config.doDetection:
881 self.log.
info(
"Running diaSource detection")
884 if self.config.doScaleDiffimVariance:
885 self.log.
info(
"Rescaling diffim variance")
886 diffimVarFactor = self.scaleVariance.
run(subtractedExposure.getMaskedImage())
887 self.log.
info(
"Diffim variance scaling factor: %.2f" % diffimVarFactor)
888 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
891 mask = subtractedExposure.getMaskedImage().getMask()
892 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
894 table = afwTable.SourceTable.make(self.schema, idFactory)
895 table.setMetadata(self.algMetadata)
896 results = self.detection.
run(
898 exposure=subtractedExposure,
899 doSmooth=
not self.config.doPreConvolve
902 if self.config.doMerge:
903 fpSet = results.fpSets.positive
904 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
905 self.config.growFootprint,
False)
907 fpSet.makeSources(diaSources)
908 self.log.
info(
"Merging detections into %d sources" % (len(diaSources)))
910 diaSources = results.sources
912 if self.config.doMeasurement:
913 newDipoleFitting = self.config.doDipoleFitting
914 self.log.
info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
915 if not newDipoleFitting:
917 self.measurement.
run(diaSources, subtractedExposure)
920 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
921 self.measurement.
run(diaSources, subtractedExposure, exposure,
922 subtractRes.matchedExposure)
924 self.measurement.
run(diaSources, subtractedExposure, exposure)
925 if self.config.doApCorr:
926 self.applyApCorr.
run(
928 apCorrMap=subtractedExposure.getInfo().getApCorrMap()
931 if self.config.doForcedMeasurement:
934 forcedSources = self.forcedMeasurement.generateMeasCat(
935 exposure, diaSources, subtractedExposure.getWcs())
936 self.forcedMeasurement.
run(forcedSources, exposure, diaSources, subtractedExposure.getWcs())
938 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
939 "ip_diffim_forced_PsfFlux_instFlux",
True)
940 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
941 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
942 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
943 "ip_diffim_forced_PsfFlux_area",
True)
944 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
945 "ip_diffim_forced_PsfFlux_flag",
True)
946 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
947 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
948 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
949 "ip_diffim_forced_PsfFlux_flag_edge",
True)
950 for diaSource, forcedSource
in zip(diaSources, forcedSources):
951 diaSource.assign(forcedSource, mapper)
954 if self.config.doMatchSources:
955 if selectSources
is not None:
957 matchRadAsec = self.config.diaSourceMatchRadius
958 matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
961 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
962 srcMatch
in srcMatches])
963 self.log.
info(
"Matched %d / %d diaSources to sources" % (len(srcMatchDict),
966 self.log.
warn(
"Src product does not exist; cannot match with diaSources")
971 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
973 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
974 refMatches = astromRet.matches
975 if refMatches
is None:
976 self.log.
warn(
"No diaSource matches with reference catalog")
979 self.log.
info(
"Matched %d / %d diaSources to reference catalog" % (len(refMatches),
981 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for
982 refMatch
in refMatches])
985 for diaSource
in diaSources:
986 sid = diaSource.getId()
987 if sid
in srcMatchDict:
988 diaSource.set(
"srcMatchId", srcMatchDict[sid])
989 if sid
in refMatchDict:
990 diaSource.set(
"refMatchId", refMatchDict[sid])
992 if self.config.doAddMetrics
and self.config.doSelectSources:
993 self.log.
info(
"Evaluating metrics and control sample")
996 for cell
in subtractRes.kernelCellSet.getCellList():
997 for cand
in cell.begin(
False):
998 kernelCandList.append(cand)
1001 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
1002 nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
1005 diffimTools.sourceTableToCandidateList(controlSources,
1006 subtractRes.warpedExposure, exposure,
1007 self.config.subtract.kernel.active,
1008 self.config.subtract.kernel.active.detectionConfig,
1009 self.log, doBuild=
True, basisList=basisList))
1011 KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
1012 subtractRes.backgroundModel, dof=nparam)
1013 KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
1014 subtractRes.backgroundModel)
1016 if self.config.doDetection:
1017 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
1019 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
1021 self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
1022 return pipeBase.Struct(
1023 subtractedExposure=subtractedExposure,
1024 warpedExposure=subtractRes.warpedExposure,
1025 matchedExposure=subtractRes.matchedExposure,
1026 subtractRes=subtractRes,
1027 diaSources=diaSources,
1028 selectSources=selectSources
1031 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1032 """Fit the relative astrometry between templateSources and selectSources
1037 Remove this method. It originally fit a new WCS to the template before calling register.run
1038 because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed.
1039 It remains because a subtask overrides it.
1041 results = self.register.
run(templateSources, templateExposure.getWcs(),
1042 templateExposure.getBBox(), selectSources)
1045 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
1046 """Make debug plots and displays.
1050 Test and update for current debug display and slot names
1060 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
1061 if not maskTransparency:
1062 maskTransparency = 0
1063 disp.setMaskTransparency(maskTransparency)
1065 if display
and showSubtracted:
1066 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
1067 mi = subtractRes.subtractedExposure.getMaskedImage()
1068 x0, y0 = mi.getX0(), mi.getY0()
1069 with disp.Buffering():
1070 for s
in diaSources:
1071 x, y = s.getX() - x0, s.getY() - y0
1072 ctype =
"red" if s.get(
"flags_negative")
else "yellow"
1073 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
1074 or s.get(
"base_PixelFlags_flag_saturatedCenter")
1075 or s.get(
"base_PixelFlags_flag_crCenter")):
1077 elif (s.get(
"base_PixelFlags_flag_interpolated")
1078 or s.get(
"base_PixelFlags_flag_saturated")
1079 or s.get(
"base_PixelFlags_flag_cr")):
1083 disp.dot(ptype, x, y, size=4, ctype=ctype)
1084 lsstDebug.frame += 1
1086 if display
and showPixelResiduals
and selectSources:
1087 nonKernelSources = []
1088 for source
in selectSources:
1089 if source
not in kernelSources:
1090 nonKernelSources.append(source)
1092 diUtils.plotPixelResiduals(exposure,
1093 subtractRes.warpedExposure,
1094 subtractRes.subtractedExposure,
1095 subtractRes.kernelCellSet,
1096 subtractRes.psfMatchingKernel,
1097 subtractRes.backgroundModel,
1099 self.subtract.config.kernel.active.detectionConfig,
1101 diUtils.plotPixelResiduals(exposure,
1102 subtractRes.warpedExposure,
1103 subtractRes.subtractedExposure,
1104 subtractRes.kernelCellSet,
1105 subtractRes.psfMatchingKernel,
1106 subtractRes.backgroundModel,
1108 self.subtract.config.kernel.active.detectionConfig,
1110 if display
and showDiaSources:
1112 isFlagged = [flagChecker(x)
for x
in diaSources]
1113 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
1114 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
1115 frame=lsstDebug.frame)
1116 lsstDebug.frame += 1
1118 if display
and showDipoles:
1119 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
1120 frame=lsstDebug.frame)
1121 lsstDebug.frame += 1
1123 def _getConfigName(self):
1124 """Return the name of the config dataset
1126 return "%sDiff_config" % (self.config.coaddName,)
1128 def _getMetadataName(self):
1129 """Return the name of the metadata dataset
1131 return "%sDiff_metadata" % (self.config.coaddName,)
1134 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
1135 return {self.config.coaddName +
"Diff_diaSrc": self.outputSchema}
1138 def _makeArgumentParser(cls):
1139 """Create an argument parser
1141 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1142 parser.add_id_argument(
"--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
1143 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
1144 help=
"Template data ID in case of calexp template,"
1145 " e.g. --templateId visit=6789")
1149 class Winter2013ImageDifferenceConfig(ImageDifferenceConfig):
1150 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1151 doc=
"Shift stars going into RegisterTask by this amount")
1152 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1153 doc=
"Perturb stars going into RegisterTask by this amount")
1156 ImageDifferenceConfig.setDefaults(self)
1157 self.getTemplate.retarget(GetCalexpAsTemplateTask)
1160 class Winter2013ImageDifferenceTask(ImageDifferenceTask):
1161 """!Image difference Task used in the Winter 2013 data challege.
1162 Enables testing the effects of registration shifts and scatter.
1164 For use with winter 2013 simulated images:
1165 Use --templateId visit=88868666 for sparse data
1166 --templateId visit=22222200 for dense data (g)
1167 --templateId visit=11111100 for dense data (i)
1169 ConfigClass = Winter2013ImageDifferenceConfig
1170 _DefaultName =
"winter2013ImageDifference"
1172 def __init__(self, **kwargs):
1173 ImageDifferenceTask.__init__(self, **kwargs)
1175 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1176 """Fit the relative astrometry between templateSources and selectSources"""
1177 if self.config.winter2013WcsShift > 0.0:
1179 self.config.winter2013WcsShift)
1180 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1181 for source
in templateSources:
1182 centroid = source.get(cKey)
1183 source.set(cKey, centroid + offset)
1184 elif self.config.winter2013WcsRms > 0.0:
1185 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1186 for source
in templateSources:
1187 offset =
geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1188 self.config.winter2013WcsRms*numpy.random.normal())
1189 centroid = source.get(cKey)
1190 source.set(cKey, centroid + offset)
1192 results = self.register.
run(templateSources, templateExposure.getWcs(),
1193 templateExposure.getBBox(), selectSources)
Parameters to control convolution.
Pass parameters to algorithms that match list of sources.
A mapping between the keys of two Schemas, used to copy data between them.
Class for storing ordered metadata with comments.
Represent a PSF as a circularly symmetrical Gaussian.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
int warpExposure(DestExposureT &destExposure, SrcExposureT const &srcExposure, WarpingControl const &control, typename DestExposureT::MaskedImageT::SinglePixel padValue=lsst::afw::math::edgePixel< typename DestExposureT::MaskedImageT >(typename lsst::afw::image::detail::image_traits< typename DestExposureT::MaskedImageT >::image_category()))
Warp (remap) one exposure to another.
SourceMatchVector matchXy(SourceCatalog const &cat1, SourceCatalog const &cat2, double radius, MatchControl const &mc=MatchControl())
Compute all tuples (s1,s2,d) where s1 belings to cat1, s2 belongs to cat2 and d, the distance between...
std::vector< Match< typename Cat1::Record, typename Cat2::Record > > matchRaDec(Cat1 const &cat1, Cat2 const &cat2, lsst::geom::Angle radius, MatchControl const &mc=MatchControl())
Compute all tuples (s1,s2,d) where s1 belings to cat1, s2 belongs to cat2 and d, the distance between...
def makeKernelBasisList(config, targetFwhmPix=None, referenceFwhmPix=None, basisDegGauss=None, metadata=None)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def getSchemaCatalogs(self)
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)