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)
47 __all__ = [
"ImageDifferenceConfig",
"ImageDifferenceTask"]
48 FwhmPerSigma = 2*math.sqrt(2*math.log(2))
53 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
54 defaultTemplates={
"coaddName":
"deep",
59 exposure = pipeBase.connectionTypes.Input(
60 doc=
"Input science exposure to subtract from.",
61 dimensions=(
"instrument",
"visit",
"detector"),
62 storageClass=
"ExposureF",
74 skyMap = pipeBase.connectionTypes.Input(
75 doc=
"Input definition of geometry/bbox and projection/wcs for template exposures",
76 name=
"{skyMapName}Coadd_skyMap",
77 dimensions=(
"skymap", ),
78 storageClass=
"SkyMap",
80 coaddExposures = pipeBase.connectionTypes.Input(
81 doc=
"Input template to match and subtract from the exposure",
82 dimensions=(
"tract",
"patch",
"skymap",
"band"),
83 storageClass=
"ExposureF",
84 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
88 dcrCoadds = pipeBase.connectionTypes.Input(
89 doc=
"Input DCR template to match and subtract from the exposure",
90 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
91 storageClass=
"ExposureF",
92 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
96 outputSchema = pipeBase.connectionTypes.InitOutput(
97 doc=
"Schema (as an example catalog) for output DIASource catalog.",
98 storageClass=
"SourceCatalog",
99 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
101 subtractedExposure = pipeBase.connectionTypes.Output(
102 doc=
"Output difference image",
103 dimensions=(
"instrument",
"visit",
"detector"),
104 storageClass=
"ExposureF",
105 name=
"{fakesType}{coaddName}Diff_differenceExp",
107 warpedExposure = pipeBase.connectionTypes.Output(
108 doc=
"Warped template used to create `subtractedExposure`.",
109 dimensions=(
"instrument",
"visit",
"detector"),
110 storageClass=
"ExposureF",
111 name=
"{fakesType}{coaddName}Diff_warpedExp",
113 diaSources = pipeBase.connectionTypes.Output(
114 doc=
"Output detected diaSources on the difference image",
115 dimensions=(
"instrument",
"visit",
"detector"),
116 storageClass=
"SourceCatalog",
117 name=
"{fakesType}{coaddName}Diff_diaSrc",
120 def __init__(self, *, config=None):
121 super().__init__(config=config)
122 if config.coaddName ==
'dcr':
123 self.inputs.remove(
"coaddExposures")
125 self.inputs.remove(
"dcrCoadds")
131 class ImageDifferenceConfig(pipeBase.PipelineTaskConfig,
132 pipelineConnections=ImageDifferenceTaskConnections):
133 """Config for ImageDifferenceTask
135 doAddCalexpBackground = pexConfig.Field(dtype=bool, default=
False,
136 doc=
"Add background to calexp before processing it. "
137 "Useful as ipDiffim does background matching.")
138 doUseRegister = pexConfig.Field(dtype=bool, default=
True,
139 doc=
"Use image-to-image registration to align template with "
141 doDebugRegister = pexConfig.Field(dtype=bool, default=
False,
142 doc=
"Writing debugging data for doUseRegister")
143 doSelectSources = pexConfig.Field(dtype=bool, default=
True,
144 doc=
"Select stars to use for kernel fitting")
145 doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=
False,
146 doc=
"Select stars of extreme color as part of the control sample")
147 doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=
False,
148 doc=
"Select stars that are variable to be part "
149 "of the control sample")
150 doSubtract = pexConfig.Field(dtype=bool, default=
True, doc=
"Compute subtracted exposure?")
151 doPreConvolve = pexConfig.Field(dtype=bool, default=
True,
152 doc=
"Convolve science image by its PSF before PSF-matching?")
153 doScaleTemplateVariance = pexConfig.Field(dtype=bool, default=
False,
154 doc=
"Scale variance of the template before PSF matching")
155 doScaleDiffimVariance = pexConfig.Field(dtype=bool, default=
False,
156 doc=
"Scale variance of the diffim before PSF matching. "
157 "You may do either this or template variance scaling, "
158 "or neither. (Doing both is a waste of CPU.)")
159 useGaussianForPreConvolution = pexConfig.Field(dtype=bool, default=
True,
160 doc=
"Use a simple gaussian PSF model for pre-convolution "
161 "(else use fit PSF)? Ignored if doPreConvolve false.")
162 doDetection = pexConfig.Field(dtype=bool, default=
True, doc=
"Detect sources?")
163 doDecorrelation = pexConfig.Field(dtype=bool, default=
False,
164 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
165 "kernel convolution? If True, also update the diffim PSF.")
166 doMerge = pexConfig.Field(dtype=bool, default=
True,
167 doc=
"Merge positive and negative diaSources with grow radius "
168 "set by growFootprint")
169 doMatchSources = pexConfig.Field(dtype=bool, default=
True,
170 doc=
"Match diaSources with input calexp sources and ref catalog sources")
171 doMeasurement = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure diaSources?")
172 doDipoleFitting = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure dipoles using new algorithm?")
173 doForcedMeasurement = pexConfig.Field(
176 doc=
"Force photometer diaSource locations on PVI?")
177 doWriteSubtractedExp = pexConfig.Field(dtype=bool, default=
True, doc=
"Write difference exposure?")
178 doWriteWarpedExp = pexConfig.Field(dtype=bool, default=
False,
179 doc=
"Write WCS, warped template coadd exposure?")
180 doWriteMatchedExp = pexConfig.Field(dtype=bool, default=
False,
181 doc=
"Write warped and PSF-matched template coadd exposure?")
182 doWriteSources = pexConfig.Field(dtype=bool, default=
True, doc=
"Write sources?")
183 doAddMetrics = pexConfig.Field(dtype=bool, default=
True,
184 doc=
"Add columns to the source table to hold analysis metrics?")
186 coaddName = pexConfig.Field(
187 doc=
"coadd name: typically one of deep, goodSeeing, or dcr",
191 convolveTemplate = pexConfig.Field(
192 doc=
"Which image gets convolved (default = template)",
196 refObjLoader = pexConfig.ConfigurableField(
197 target=LoadIndexedReferenceObjectsTask,
198 doc=
"reference object loader",
200 astrometer = pexConfig.ConfigurableField(
201 target=AstrometryTask,
202 doc=
"astrometry task; used to match sources to reference objects, but not to fit a WCS",
204 sourceSelector = pexConfig.ConfigurableField(
205 target=ObjectSizeStarSelectorTask,
206 doc=
"Source selection algorithm",
208 subtract = subtractAlgorithmRegistry.makeField(
"Subtraction Algorithm", default=
"al")
209 decorrelate = pexConfig.ConfigurableField(
210 target=DecorrelateALKernelSpatialTask,
211 doc=
"Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
212 "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
215 doSpatiallyVarying = pexConfig.Field(
218 doc=
"If using Zogy or A&L decorrelation, perform these on a grid across the "
219 "image in order to allow for spatial variations"
221 detection = pexConfig.ConfigurableField(
222 target=SourceDetectionTask,
223 doc=
"Low-threshold detection for final measurement",
225 measurement = pexConfig.ConfigurableField(
226 target=DipoleFitTask,
227 doc=
"Enable updated dipole fitting method",
229 forcedMeasurement = pexConfig.ConfigurableField(
230 target=ForcedMeasurementTask,
231 doc=
"Subtask to force photometer PVI at diaSource location.",
233 getTemplate = pexConfig.ConfigurableField(
234 target=GetCoaddAsTemplateTask,
235 doc=
"Subtask to retrieve template exposure and sources",
237 scaleVariance = pexConfig.ConfigurableField(
238 target=ScaleVarianceTask,
239 doc=
"Subtask to rescale the variance of the template "
240 "to the statistically expected level"
242 controlStepSize = pexConfig.Field(
243 doc=
"What step size (every Nth one) to select a control sample from the kernelSources",
247 controlRandomSeed = pexConfig.Field(
248 doc=
"Random seed for shuffing the control sample",
252 register = pexConfig.ConfigurableField(
254 doc=
"Task to enable image-to-image image registration (warping)",
256 kernelSourcesFromRef = pexConfig.Field(
257 doc=
"Select sources to measure kernel from reference catalog if True, template if false",
261 templateSipOrder = pexConfig.Field(
262 dtype=int, default=2,
263 doc=
"Sip Order for fitting the Template Wcs (default is too high, overfitting)"
265 growFootprint = pexConfig.Field(
266 dtype=int, default=2,
267 doc=
"Grow positive and negative footprints by this amount before merging"
269 diaSourceMatchRadius = pexConfig.Field(
270 dtype=float, default=0.5,
271 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
277 self.subtract[
'al'].kernel.name =
"AL"
278 self.subtract[
'al'].kernel.active.fitForBackground =
True
279 self.subtract[
'al'].kernel.active.spatialKernelOrder = 1
280 self.subtract[
'al'].kernel.active.spatialBgOrder = 2
281 self.doPreConvolve =
False
282 self.doMatchSources =
False
283 self.doAddMetrics =
False
284 self.doUseRegister =
False
287 self.detection.thresholdPolarity =
"both"
288 self.detection.thresholdValue = 5.5
289 self.detection.reEstimateBackground =
False
290 self.detection.thresholdType =
"pixel_stdev"
296 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
297 self.measurement.plugins.names |= [
'base_LocalPhotoCalib',
300 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
301 self.forcedMeasurement.copyColumns = {
302 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
303 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
304 self.forcedMeasurement.slots.shape =
None
307 random.seed(self.controlRandomSeed)
310 pexConfig.Config.validate(self)
311 if self.doAddMetrics
and not self.doSubtract:
312 raise ValueError(
"Subtraction must be enabled for kernel metrics calculation.")
313 if not self.doSubtract
and not self.doDetection:
314 raise ValueError(
"Either doSubtract or doDetection must be enabled.")
315 if self.subtract.name ==
'zogy' and self.doAddMetrics:
316 raise ValueError(
"Kernel metrics does not exist in zogy subtraction.")
317 if self.doMeasurement
and not self.doDetection:
318 raise ValueError(
"Cannot run source measurement without source detection.")
319 if self.doMerge
and not self.doDetection:
320 raise ValueError(
"Cannot run source merging without source detection.")
321 if self.doUseRegister
and not self.doSelectSources:
322 raise ValueError(
"doUseRegister=True and doSelectSources=False. "
323 "Cannot run RegisterTask without selecting sources.")
324 if self.doPreConvolve
and self.doDecorrelation
and not self.convolveTemplate:
325 raise ValueError(
"doPreConvolve=True and doDecorrelation=True and "
326 "convolveTemplate=False is not supported.")
327 if hasattr(self.getTemplate,
"coaddName"):
328 if self.getTemplate.coaddName != self.coaddName:
329 raise ValueError(
"Mis-matched coaddName and getTemplate.coaddName in the config.")
330 if self.doScaleDiffimVariance
and self.doScaleTemplateVariance:
331 raise ValueError(
"Scaling the diffim variance and scaling the template variance "
332 "are both set. Please choose one or the other.")
335 class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
338 def getTargetList(parsedCmd, **kwargs):
339 return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
343 class ImageDifferenceTask(pipeBase.CmdLineTask, pipeBase.PipelineTask):
344 """Subtract an image from a template and measure the result
346 ConfigClass = ImageDifferenceConfig
347 RunnerClass = ImageDifferenceTaskRunner
348 _DefaultName =
"imageDifference"
350 def __init__(self, butler=None, **kwargs):
351 """!Construct an ImageDifference Task
353 @param[in] butler Butler object to use in constructing reference object loaders
355 super().__init__(**kwargs)
356 self.makeSubtask(
"getTemplate")
358 self.makeSubtask(
"subtract")
360 if self.config.subtract.name ==
'al' and self.config.doDecorrelation:
361 self.makeSubtask(
"decorrelate")
363 if self.config.doScaleTemplateVariance
or self.config.doScaleDiffimVariance:
364 self.makeSubtask(
"scaleVariance")
366 if self.config.doUseRegister:
367 self.makeSubtask(
"register")
368 self.schema = afwTable.SourceTable.makeMinimalSchema()
370 if self.config.doSelectSources:
371 self.makeSubtask(
"sourceSelector")
372 if self.config.kernelSourcesFromRef:
373 self.makeSubtask(
'refObjLoader', butler=butler)
374 self.makeSubtask(
"astrometer", refObjLoader=self.refObjLoader)
377 if self.config.doDetection:
378 self.makeSubtask(
"detection", schema=self.schema)
379 if self.config.doMeasurement:
380 self.makeSubtask(
"measurement", schema=self.schema,
381 algMetadata=self.algMetadata)
382 if self.config.doForcedMeasurement:
383 self.schema.addField(
384 "ip_diffim_forced_PsfFlux_instFlux",
"D",
385 "Forced PSF flux measured on the direct image.")
386 self.schema.addField(
387 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
388 "Forced PSF flux error measured on the direct image.")
389 self.schema.addField(
390 "ip_diffim_forced_PsfFlux_area",
"F",
391 "Forced PSF flux effective area of PSF.",
393 self.schema.addField(
394 "ip_diffim_forced_PsfFlux_flag",
"Flag",
395 "Forced PSF flux general failure flag.")
396 self.schema.addField(
397 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
398 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
399 self.schema.addField(
400 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
401 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
402 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
403 if self.config.doMatchSources:
404 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
405 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
409 self.outputSchema.getTable().setMetadata(self.algMetadata)
412 def makeIdFactory(expId, expBits):
413 """Create IdFactory instance for unique 64 bit diaSource id-s.
421 Number of used bits in ``expId``.
425 The diasource id-s consists of the ``expId`` stored fixed in the highest value
426 ``expBits`` of the 64-bit integer plus (bitwise or) a generated sequence number in the
427 low value end of the integer.
431 idFactory: `lsst.afw.table.IdFactory`
433 return afwTable.IdFactory.makeSource(expId, 64 - expBits)
436 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
437 inputRefs: pipeBase.InputQuantizedConnection,
438 outputRefs: pipeBase.OutputQuantizedConnection):
439 inputs = butlerQC.get(inputRefs)
440 self.log.
info(f
"Processing {butlerQC.quantum.dataId}")
441 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
443 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
444 if self.config.coaddName ==
'dcr':
445 templateExposures = inputRefs.dcrCoadds
447 templateExposures = inputRefs.coaddExposures
448 templateStruct = self.getTemplate.runQuantum(
449 inputs[
'exposure'], butlerQC, inputRefs.skyMap, templateExposures
452 outputs = self.run(exposure=inputs[
'exposure'],
453 templateExposure=templateStruct.exposure,
455 butlerQC.put(outputs, outputRefs)
458 def runDataRef(self, sensorRef, templateIdList=None):
459 """Subtract an image from a template coadd and measure the result.
461 Data I/O wrapper around `run` using the butler in Gen2.
465 sensorRef : `lsst.daf.persistence.ButlerDataRef`
466 Sensor-level butler data reference, used for the following data products:
473 - self.config.coaddName + "Coadd_skyMap"
474 - self.config.coaddName + "Coadd"
475 Input or output, depending on config:
476 - self.config.coaddName + "Diff_subtractedExp"
477 Output, depending on config:
478 - self.config.coaddName + "Diff_matchedExp"
479 - self.config.coaddName + "Diff_src"
483 results : `lsst.pipe.base.Struct`
484 Returns the Struct by `run`.
486 subtractedExposureName = self.config.coaddName +
"Diff_differenceExp"
487 subtractedExposure =
None
489 calexpBackgroundExposure =
None
490 self.log.
info(
"Processing %s" % (sensorRef.dataId))
495 idFactory = self.makeIdFactory(expId=int(sensorRef.get(
"ccdExposureId")),
496 expBits=sensorRef.get(
"ccdExposureId_bits"))
497 if self.config.doAddCalexpBackground:
498 calexpBackgroundExposure = sensorRef.get(
"calexpBackground")
501 exposure = sensorRef.get(
"calexp", immediate=
True)
504 template = self.getTemplate.runDataRef(exposure, sensorRef, templateIdList=templateIdList)
506 if sensorRef.datasetExists(
"src"):
507 self.log.
info(
"Source selection via src product")
509 selectSources = sensorRef.get(
"src")
511 if not self.config.doSubtract
and self.config.doDetection:
513 subtractedExposure = sensorRef.get(subtractedExposureName)
516 results = self.run(exposure=exposure,
517 selectSources=selectSources,
518 templateExposure=template.exposure,
519 templateSources=template.sources,
521 calexpBackgroundExposure=calexpBackgroundExposure,
522 subtractedExposure=subtractedExposure)
524 if self.config.doWriteSources
and results.diaSources
is not None:
525 sensorRef.put(results.diaSources, self.config.coaddName +
"Diff_diaSrc")
526 if self.config.doWriteWarpedExp:
527 sensorRef.put(results.warpedExposure, self.config.coaddName +
"Diff_warpedExp")
528 if self.config.doWriteMatchedExp:
529 sensorRef.put(results.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
530 if self.config.doAddMetrics
and self.config.doSelectSources:
531 sensorRef.put(results.selectSources, self.config.coaddName +
"Diff_kernelSrc")
532 if self.config.doWriteSubtractedExp:
533 sensorRef.put(results.subtractedExposure, subtractedExposureName)
537 def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
538 idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
539 """PSF matches, subtract two images and perform detection on the difference image.
543 exposure : `lsst.afw.image.ExposureF`, optional
544 The science exposure, the minuend in the image subtraction.
545 Can be None only if ``config.doSubtract==False``.
546 selectSources : `lsst.afw.table.SourceCatalog`, optional
547 Identified sources on the science exposure. This catalog is used to
548 select sources in order to perform the AL PSF matching on stamp images
549 around them. The selection steps depend on config options and whether
550 ``templateSources`` and ``matchingSources`` specified.
551 templateExposure : `lsst.afw.image.ExposureF`, optional
552 The template to be subtracted from ``exposure`` in the image subtraction.
553 The template exposure should cover the same sky area as the science exposure.
554 It is either a stich of patches of a coadd skymap image or a calexp
555 of the same pointing as the science exposure. Can be None only
556 if ``config.doSubtract==False`` and ``subtractedExposure`` is not None.
557 templateSources : `lsst.afw.table.SourceCatalog`, optional
558 Identified sources on the template exposure.
559 idFactory : `lsst.afw.table.IdFactory`
560 Generator object to assign ids to detected sources in the difference image.
561 calexpBackgroundExposure : `lsst.afw.image.ExposureF`, optional
562 Background exposure to be added back to the science exposure
563 if ``config.doAddCalexpBackground==True``
564 subtractedExposure : `lsst.afw.image.ExposureF`, optional
565 If ``config.doSubtract==False`` and ``config.doDetection==True``,
566 performs the post subtraction source detection only on this exposure.
567 Otherwise should be None.
571 results : `lsst.pipe.base.Struct`
572 ``subtractedExposure`` : `lsst.afw.image.ExposureF`
574 ``matchedExposure`` : `lsst.afw.image.ExposureF`
575 The matched PSF exposure.
576 ``subtractRes`` : `lsst.pipe.base.Struct`
577 The returned result structure of the ImagePsfMatchTask subtask.
578 ``diaSources`` : `lsst.afw.table.SourceCatalog`
579 The catalog of detected sources.
580 ``selectSources`` : `lsst.afw.table.SourceCatalog`
581 The input source catalog with optionally added Qa information.
585 The following major steps are included:
587 - warp template coadd to match WCS of image
588 - PSF match image to warped template
589 - subtract image from PSF-matched, warped template
593 For details about the image subtraction configuration modes
594 see `lsst.ip.diffim`.
597 controlSources =
None
601 if self.config.doAddCalexpBackground:
602 mi = exposure.getMaskedImage()
603 mi += calexpBackgroundExposure.getImage()
605 if not exposure.hasPsf():
606 raise pipeBase.TaskError(
"Exposure has no psf")
607 sciencePsf = exposure.getPsf()
609 if self.config.doSubtract:
610 if self.config.doScaleTemplateVariance:
611 self.log.
info(
"Rescaling template variance")
612 templateVarFactor = self.scaleVariance.
run(
613 templateExposure.getMaskedImage())
614 self.log.
info(
"Template variance scaling factor: %.2f" % templateVarFactor)
615 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
617 if self.config.subtract.name ==
'zogy':
618 subtractRes = self.subtract.
run(exposure, templateExposure, doWarping=
True)
619 if self.config.doPreConvolve:
620 subtractedExposure = subtractRes.scoreExp
622 subtractedExposure = subtractRes.diffExp
623 subtractRes.subtractedExposure = subtractedExposure
624 subtractRes.matchedExposure =
None
626 elif self.config.subtract.name ==
'al':
628 scienceSigmaOrig = sciencePsf.computeShape().getDeterminantRadius()
629 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
637 if self.config.doPreConvolve:
640 srcMI = exposure.getMaskedImage()
641 exposureOrig = exposure.clone()
642 destMI = srcMI.Factory(srcMI.getDimensions())
644 if self.config.useGaussianForPreConvolution:
646 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
652 exposure.setMaskedImage(destMI)
653 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
655 scienceSigmaPost = scienceSigmaOrig
656 exposureOrig = exposure
661 if self.config.doSelectSources:
662 if selectSources
is None:
663 self.log.
warn(
"Src product does not exist; running detection, measurement, selection")
665 selectSources = self.subtract.getSelectSources(
667 sigma=scienceSigmaPost,
668 doSmooth=
not self.config.doPreConvolve,
672 if self.config.doAddMetrics:
676 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
677 targetFwhmPix=templateSigma*FwhmPerSigma))
685 selectSources = kcQa.addToSchema(selectSources)
686 if self.config.kernelSourcesFromRef:
688 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
689 matches = astromRet.matches
690 elif templateSources:
693 mc.findOnlyClosest =
False
697 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False,"
698 "but template sources not available. Cannot match science "
699 "sources with template sources. Run process* on data from "
700 "which templates are built.")
702 kernelSources = self.sourceSelector.
run(selectSources, exposure=exposure,
703 matches=matches).sourceCat
704 random.shuffle(kernelSources, random.random)
705 controlSources = kernelSources[::self.config.controlStepSize]
706 kernelSources = [k
for i, k
in enumerate(kernelSources)
707 if i % self.config.controlStepSize]
709 if self.config.doSelectDcrCatalog:
713 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
714 controlSources.extend(redSources)
718 grMax=self.sourceSelector.config.grMin))
719 blueSources = blueSelector.selectStars(exposure, selectSources,
720 matches=matches).starCat
721 controlSources.extend(blueSources)
723 if self.config.doSelectVariableCatalog:
726 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
727 controlSources.extend(varSources)
729 self.log.
info(
"Selected %d / %d sources for Psf matching (%d for control sample)"
730 % (len(kernelSources), len(selectSources), len(controlSources)))
734 if self.config.doUseRegister:
735 self.log.
info(
"Registering images")
737 if templateSources
is None:
741 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
742 templateSources = self.subtract.getSelectSources(
751 wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
752 warpedExp = self.register.
warpExposure(templateExposure, wcsResults.wcs,
753 exposure.getWcs(), exposure.getBBox())
754 templateExposure = warpedExp
759 if self.config.doDebugRegister:
761 srcToMatch = {x.second.getId(): x.first
for x
in matches}
763 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
764 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidSlot().getMeasKey()
765 sids = [m.first.getId()
for m
in wcsResults.matches]
766 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
767 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
768 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
769 allresids = dict(zip(sids, zip(positions, residuals)))
771 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
772 wcsResults.wcs.pixelToSky(
773 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
774 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
775 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
776 for x
in sids
if x
in srcToMatch.keys()])
777 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
778 if s
in srcToMatch.keys()])
779 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
780 if s
in srcToMatch.keys()])
781 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
782 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
783 & (colors <= self.sourceSelector.config.grMax))
784 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
785 rms1Long = IqrToSigma*(
786 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
787 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
788 - numpy.percentile(dlat[idx1], 25))
789 rms2Long = IqrToSigma*(
790 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
791 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
792 - numpy.percentile(dlat[idx2], 25))
793 rms3Long = IqrToSigma*(
794 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
795 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
796 - numpy.percentile(dlat[idx3], 25))
797 self.log.
info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f" %
798 (numpy.median(dlong[idx1]), rms1Long,
799 numpy.median(dlat[idx1]), rms1Lat))
800 self.log.
info(
"Green star offsets'': %.3f %.3f, %.3f %.3f" %
801 (numpy.median(dlong[idx2]), rms2Long,
802 numpy.median(dlat[idx2]), rms2Lat))
803 self.log.
info(
"Red star offsets'': %.3f %.3f, %.3f %.3f" %
804 (numpy.median(dlong[idx3]), rms3Long,
805 numpy.median(dlat[idx3]), rms3Lat))
807 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
808 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
809 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
810 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
811 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
812 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
814 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
815 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
816 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
817 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
818 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
819 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
826 self.log.
info(
"Subtracting images")
827 subtractRes = self.subtract.subtractExposures(
828 templateExposure=templateExposure,
829 scienceExposure=exposure,
830 candidateList=kernelSources,
831 convolveTemplate=self.config.convolveTemplate,
832 doWarping=
not self.config.doUseRegister
834 subtractedExposure = subtractRes.subtractedExposure
836 if self.config.doDetection:
837 self.log.
info(
"Computing diffim PSF")
840 if not subtractedExposure.hasPsf():
841 if self.config.convolveTemplate:
842 subtractedExposure.setPsf(exposure.getPsf())
844 subtractedExposure.setPsf(templateExposure.getPsf())
851 if self.config.doDecorrelation
and self.config.doSubtract:
853 if preConvPsf
is not None:
854 preConvKernel = preConvPsf.getLocalKernel()
855 if self.config.convolveTemplate:
856 self.log.
info(
"Decorrelation after template image convolution")
857 decorrResult = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
859 subtractRes.psfMatchingKernel,
860 spatiallyVarying=self.config.doSpatiallyVarying,
861 preConvKernel=preConvKernel)
863 self.log.
info(
"Decorrelation after science image convolution")
864 decorrResult = self.decorrelate.
run(subtractRes.warpedExposure, exposureOrig,
866 subtractRes.psfMatchingKernel,
867 spatiallyVarying=self.config.doSpatiallyVarying,
868 preConvKernel=preConvKernel)
869 subtractedExposure = decorrResult.correctedExposure
873 if self.config.doDetection:
874 self.log.
info(
"Running diaSource detection")
877 if self.config.doScaleDiffimVariance:
878 self.log.
info(
"Rescaling diffim variance")
879 diffimVarFactor = self.scaleVariance.
run(subtractedExposure.getMaskedImage())
880 self.log.
info(
"Diffim variance scaling factor: %.2f" % diffimVarFactor)
881 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
884 mask = subtractedExposure.getMaskedImage().getMask()
885 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
887 table = afwTable.SourceTable.make(self.schema, idFactory)
888 table.setMetadata(self.algMetadata)
889 results = self.detection.
run(
891 exposure=subtractedExposure,
892 doSmooth=
not self.config.doPreConvolve
895 if self.config.doMerge:
896 fpSet = results.fpSets.positive
897 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
898 self.config.growFootprint,
False)
900 fpSet.makeSources(diaSources)
901 self.log.
info(
"Merging detections into %d sources" % (len(diaSources)))
903 diaSources = results.sources
905 if self.config.doMeasurement:
906 newDipoleFitting = self.config.doDipoleFitting
907 self.log.
info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
908 if not newDipoleFitting:
910 self.measurement.
run(diaSources, subtractedExposure)
913 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
914 self.measurement.
run(diaSources, subtractedExposure, exposure,
915 subtractRes.matchedExposure)
917 self.measurement.
run(diaSources, subtractedExposure, exposure)
919 if self.config.doForcedMeasurement:
922 forcedSources = self.forcedMeasurement.generateMeasCat(
923 exposure, diaSources, subtractedExposure.getWcs())
924 self.forcedMeasurement.
run(forcedSources, exposure, diaSources, subtractedExposure.getWcs())
926 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
927 "ip_diffim_forced_PsfFlux_instFlux",
True)
928 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
929 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
930 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
931 "ip_diffim_forced_PsfFlux_area",
True)
932 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
933 "ip_diffim_forced_PsfFlux_flag",
True)
934 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
935 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
936 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
937 "ip_diffim_forced_PsfFlux_flag_edge",
True)
938 for diaSource, forcedSource
in zip(diaSources, forcedSources):
939 diaSource.assign(forcedSource, mapper)
942 if self.config.doMatchSources:
943 if selectSources
is not None:
945 matchRadAsec = self.config.diaSourceMatchRadius
946 matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
949 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
950 srcMatch
in srcMatches])
951 self.log.
info(
"Matched %d / %d diaSources to sources" % (len(srcMatchDict),
954 self.log.
warn(
"Src product does not exist; cannot match with diaSources")
959 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
961 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
962 refMatches = astromRet.matches
963 if refMatches
is None:
964 self.log.
warn(
"No diaSource matches with reference catalog")
967 self.log.
info(
"Matched %d / %d diaSources to reference catalog" % (len(refMatches),
969 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for
970 refMatch
in refMatches])
973 for diaSource
in diaSources:
974 sid = diaSource.getId()
975 if sid
in srcMatchDict:
976 diaSource.set(
"srcMatchId", srcMatchDict[sid])
977 if sid
in refMatchDict:
978 diaSource.set(
"refMatchId", refMatchDict[sid])
980 if self.config.doAddMetrics
and self.config.doSelectSources:
981 self.log.
info(
"Evaluating metrics and control sample")
984 for cell
in subtractRes.kernelCellSet.getCellList():
985 for cand
in cell.begin(
False):
986 kernelCandList.append(cand)
989 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
990 nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
993 diffimTools.sourceTableToCandidateList(controlSources,
994 subtractRes.warpedExposure, exposure,
995 self.config.subtract.kernel.active,
996 self.config.subtract.kernel.active.detectionConfig,
997 self.log, doBuild=
True, basisList=basisList))
999 KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
1000 subtractRes.backgroundModel, dof=nparam)
1001 KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
1002 subtractRes.backgroundModel)
1004 if self.config.doDetection:
1005 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
1007 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
1009 self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
1010 return pipeBase.Struct(
1011 subtractedExposure=subtractedExposure,
1012 warpedExposure=subtractRes.warpedExposure,
1013 matchedExposure=subtractRes.matchedExposure,
1014 subtractRes=subtractRes,
1015 diaSources=diaSources,
1016 selectSources=selectSources
1019 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1020 """Fit the relative astrometry between templateSources and selectSources
1025 Remove this method. It originally fit a new WCS to the template before calling register.run
1026 because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed.
1027 It remains because a subtask overrides it.
1029 results = self.register.
run(templateSources, templateExposure.getWcs(),
1030 templateExposure.getBBox(), selectSources)
1033 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
1034 """Make debug plots and displays.
1038 Test and update for current debug display and slot names
1048 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
1049 if not maskTransparency:
1050 maskTransparency = 0
1051 disp.setMaskTransparency(maskTransparency)
1053 if display
and showSubtracted:
1054 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
1055 mi = subtractRes.subtractedExposure.getMaskedImage()
1056 x0, y0 = mi.getX0(), mi.getY0()
1057 with disp.Buffering():
1058 for s
in diaSources:
1059 x, y = s.getX() - x0, s.getY() - y0
1060 ctype =
"red" if s.get(
"flags_negative")
else "yellow"
1061 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
1062 or s.get(
"base_PixelFlags_flag_saturatedCenter")
1063 or s.get(
"base_PixelFlags_flag_crCenter")):
1065 elif (s.get(
"base_PixelFlags_flag_interpolated")
1066 or s.get(
"base_PixelFlags_flag_saturated")
1067 or s.get(
"base_PixelFlags_flag_cr")):
1071 disp.dot(ptype, x, y, size=4, ctype=ctype)
1072 lsstDebug.frame += 1
1074 if display
and showPixelResiduals
and selectSources:
1075 nonKernelSources = []
1076 for source
in selectSources:
1077 if source
not in kernelSources:
1078 nonKernelSources.append(source)
1080 diUtils.plotPixelResiduals(exposure,
1081 subtractRes.warpedExposure,
1082 subtractRes.subtractedExposure,
1083 subtractRes.kernelCellSet,
1084 subtractRes.psfMatchingKernel,
1085 subtractRes.backgroundModel,
1087 self.subtract.config.kernel.active.detectionConfig,
1089 diUtils.plotPixelResiduals(exposure,
1090 subtractRes.warpedExposure,
1091 subtractRes.subtractedExposure,
1092 subtractRes.kernelCellSet,
1093 subtractRes.psfMatchingKernel,
1094 subtractRes.backgroundModel,
1096 self.subtract.config.kernel.active.detectionConfig,
1098 if display
and showDiaSources:
1100 isFlagged = [flagChecker(x)
for x
in diaSources]
1101 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
1102 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
1103 frame=lsstDebug.frame)
1104 lsstDebug.frame += 1
1106 if display
and showDipoles:
1107 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
1108 frame=lsstDebug.frame)
1109 lsstDebug.frame += 1
1111 def _getConfigName(self):
1112 """Return the name of the config dataset
1114 return "%sDiff_config" % (self.config.coaddName,)
1116 def _getMetadataName(self):
1117 """Return the name of the metadata dataset
1119 return "%sDiff_metadata" % (self.config.coaddName,)
1121 def getSchemaCatalogs(self):
1122 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
1123 return {self.config.coaddName +
"Diff_diaSrc": self.outputSchema}
1126 def _makeArgumentParser(cls):
1127 """Create an argument parser
1129 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1130 parser.add_id_argument(
"--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
1131 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
1132 help=
"Template data ID in case of calexp template,"
1133 " e.g. --templateId visit=6789")
1137 class Winter2013ImageDifferenceConfig(ImageDifferenceConfig):
1138 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1139 doc=
"Shift stars going into RegisterTask by this amount")
1140 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1141 doc=
"Perturb stars going into RegisterTask by this amount")
1144 ImageDifferenceConfig.setDefaults(self)
1145 self.getTemplate.retarget(GetCalexpAsTemplateTask)
1148 class Winter2013ImageDifferenceTask(ImageDifferenceTask):
1149 """!Image difference Task used in the Winter 2013 data challege.
1150 Enables testing the effects of registration shifts and scatter.
1152 For use with winter 2013 simulated images:
1153 Use --templateId visit=88868666 for sparse data
1154 --templateId visit=22222200 for dense data (g)
1155 --templateId visit=11111100 for dense data (i)
1157 ConfigClass = Winter2013ImageDifferenceConfig
1158 _DefaultName =
"winter2013ImageDifference"
1160 def __init__(self, **kwargs):
1161 ImageDifferenceTask.__init__(self, **kwargs)
1163 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1164 """Fit the relative astrometry between templateSources and selectSources"""
1165 if self.config.winter2013WcsShift > 0.0:
1167 self.config.winter2013WcsShift)
1168 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1169 for source
in templateSources:
1170 centroid = source.get(cKey)
1171 source.set(cKey, centroid + offset)
1172 elif self.config.winter2013WcsRms > 0.0:
1173 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1174 for source
in templateSources:
1175 offset =
geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1176 self.config.winter2013WcsRms*numpy.random.normal())
1177 centroid = source.get(cKey)
1178 source.set(cKey, centroid + offset)
1180 results = self.register.
run(templateSources, templateExposure.getWcs(),
1181 templateExposure.getBBox(), selectSources)