27 import lsst.pex.config
as pexConfig
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",
"abstract_filter"),
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",
"abstract_filter",
"subfilter"),
96 subtractedExposure = pipeBase.connectionTypes.Output(
97 doc=
"Output difference image",
98 dimensions=(
"instrument",
"visit",
"detector"),
99 storageClass=
"ExposureF",
100 name=
"{fakesType}{coaddName}Diff_differenceExp",
102 diaSources = pipeBase.connectionTypes.Output(
103 doc=
"Output detected diaSources on the difference image",
104 dimensions=(
"instrument",
"visit",
"detector"),
105 storageClass=
"SourceCatalog",
106 name=
"{fakesType}{coaddName}Diff_diaSrc",
109 def __init__(self, *, config=None):
110 super().__init__(config=config)
111 if config.coaddName ==
'dcr':
112 self.inputs.remove(
"coaddExposures")
114 self.inputs.remove(
"dcrCoadds")
120 class ImageDifferenceConfig(pipeBase.PipelineTaskConfig,
121 pipelineConnections=ImageDifferenceTaskConnections):
122 """Config for ImageDifferenceTask
124 doAddCalexpBackground = pexConfig.Field(dtype=bool, default=
False,
125 doc=
"Add background to calexp before processing it. "
126 "Useful as ipDiffim does background matching.")
127 doUseRegister = pexConfig.Field(dtype=bool, default=
True,
128 doc=
"Use image-to-image registration to align template with "
130 doDebugRegister = pexConfig.Field(dtype=bool, default=
False,
131 doc=
"Writing debugging data for doUseRegister")
132 doSelectSources = pexConfig.Field(dtype=bool, default=
True,
133 doc=
"Select stars to use for kernel fitting")
134 doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=
False,
135 doc=
"Select stars of extreme color as part of the control sample")
136 doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=
False,
137 doc=
"Select stars that are variable to be part "
138 "of the control sample")
139 doSubtract = pexConfig.Field(dtype=bool, default=
True, doc=
"Compute subtracted exposure?")
140 doPreConvolve = pexConfig.Field(dtype=bool, default=
True,
141 doc=
"Convolve science image by its PSF before PSF-matching?")
142 doScaleTemplateVariance = pexConfig.Field(dtype=bool, default=
False,
143 doc=
"Scale variance of the template before PSF matching")
144 useGaussianForPreConvolution = pexConfig.Field(dtype=bool, default=
True,
145 doc=
"Use a simple gaussian PSF model for pre-convolution "
146 "(else use fit PSF)? Ignored if doPreConvolve false.")
147 doDetection = pexConfig.Field(dtype=bool, default=
True, doc=
"Detect sources?")
148 doDecorrelation = pexConfig.Field(dtype=bool, default=
False,
149 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
150 "kernel convolution? If True, also update the diffim PSF.")
151 doMerge = pexConfig.Field(dtype=bool, default=
True,
152 doc=
"Merge positive and negative diaSources with grow radius "
153 "set by growFootprint")
154 doMatchSources = pexConfig.Field(dtype=bool, default=
True,
155 doc=
"Match diaSources with input calexp sources and ref catalog sources")
156 doMeasurement = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure diaSources?")
157 doDipoleFitting = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure dipoles using new algorithm?")
158 doForcedMeasurement = pexConfig.Field(
161 doc=
"Force photometer diaSource locations on PVI?")
162 doWriteSubtractedExp = pexConfig.Field(dtype=bool, default=
True, doc=
"Write difference exposure?")
163 doWriteMatchedExp = pexConfig.Field(dtype=bool, default=
False,
164 doc=
"Write warped and PSF-matched template coadd exposure?")
165 doWriteSources = pexConfig.Field(dtype=bool, default=
True, doc=
"Write sources?")
166 doAddMetrics = pexConfig.Field(dtype=bool, default=
True,
167 doc=
"Add columns to the source table to hold analysis metrics?")
169 coaddName = pexConfig.Field(
170 doc=
"coadd name: typically one of deep, goodSeeing, or dcr",
174 convolveTemplate = pexConfig.Field(
175 doc=
"Which image gets convolved (default = template)",
179 refObjLoader = pexConfig.ConfigurableField(
180 target=LoadIndexedReferenceObjectsTask,
181 doc=
"reference object loader",
183 astrometer = pexConfig.ConfigurableField(
184 target=AstrometryTask,
185 doc=
"astrometry task; used to match sources to reference objects, but not to fit a WCS",
187 sourceSelector = pexConfig.ConfigurableField(
188 target=ObjectSizeStarSelectorTask,
189 doc=
"Source selection algorithm",
191 subtract = subtractAlgorithmRegistry.makeField(
"Subtraction Algorithm", default=
"al")
192 decorrelate = pexConfig.ConfigurableField(
193 target=DecorrelateALKernelSpatialTask,
194 doc=
"Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
195 "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
198 doSpatiallyVarying = pexConfig.Field(
201 doc=
"If using Zogy or A&L decorrelation, perform these on a grid across the "
202 "image in order to allow for spatial variations"
204 detection = pexConfig.ConfigurableField(
205 target=SourceDetectionTask,
206 doc=
"Low-threshold detection for final measurement",
208 measurement = pexConfig.ConfigurableField(
209 target=DipoleFitTask,
210 doc=
"Enable updated dipole fitting method",
212 forcedMeasurement = pexConfig.ConfigurableField(
213 target=ForcedMeasurementTask,
214 doc=
"Subtask to force photometer PVI at diaSource location.",
216 getTemplate = pexConfig.ConfigurableField(
217 target=GetCoaddAsTemplateTask,
218 doc=
"Subtask to retrieve template exposure and sources",
220 scaleVariance = pexConfig.ConfigurableField(
221 target=ScaleVarianceTask,
222 doc=
"Subtask to rescale the variance of the template "
223 "to the statistically expected level"
225 controlStepSize = pexConfig.Field(
226 doc=
"What step size (every Nth one) to select a control sample from the kernelSources",
230 controlRandomSeed = pexConfig.Field(
231 doc=
"Random seed for shuffing the control sample",
235 register = pexConfig.ConfigurableField(
237 doc=
"Task to enable image-to-image image registration (warping)",
239 kernelSourcesFromRef = pexConfig.Field(
240 doc=
"Select sources to measure kernel from reference catalog if True, template if false",
244 templateSipOrder = pexConfig.Field(
245 dtype=int, default=2,
246 doc=
"Sip Order for fitting the Template Wcs (default is too high, overfitting)"
248 growFootprint = pexConfig.Field(
249 dtype=int, default=2,
250 doc=
"Grow positive and negative footprints by this amount before merging"
252 diaSourceMatchRadius = pexConfig.Field(
253 dtype=float, default=0.5,
254 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
260 self.subtract[
'al'].kernel.name =
"AL"
261 self.subtract[
'al'].kernel.active.fitForBackground =
True
262 self.subtract[
'al'].kernel.active.spatialKernelOrder = 1
263 self.subtract[
'al'].kernel.active.spatialBgOrder = 2
264 self.doPreConvolve =
False
265 self.doMatchSources =
False
266 self.doAddMetrics =
False
267 self.doUseRegister =
False
270 self.detection.thresholdPolarity =
"both"
271 self.detection.thresholdValue = 5.5
272 self.detection.reEstimateBackground =
False
273 self.detection.thresholdType =
"pixel_stdev"
279 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
280 self.measurement.plugins.names |= [
'base_LocalPhotoCalib',
283 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
284 self.forcedMeasurement.copyColumns = {
285 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
286 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
287 self.forcedMeasurement.slots.shape =
None
290 random.seed(self.controlRandomSeed)
293 pexConfig.Config.validate(self)
294 if self.doAddMetrics
and not self.doSubtract:
295 raise ValueError(
"Subtraction must be enabled for kernel metrics calculation.")
296 if not self.doSubtract
and not self.doDetection:
297 raise ValueError(
"Either doSubtract or doDetection must be enabled.")
298 if self.subtract.name ==
'zogy' and self.doAddMetrics:
299 raise ValueError(
"Kernel metrics does not exist in zogy subtraction.")
300 if self.doMeasurement
and not self.doDetection:
301 raise ValueError(
"Cannot run source measurement without source detection.")
302 if self.doMerge
and not self.doDetection:
303 raise ValueError(
"Cannot run source merging without source detection.")
304 if self.doUseRegister
and not self.doSelectSources:
305 raise ValueError(
"doUseRegister=True and doSelectSources=False. "
306 "Cannot run RegisterTask without selecting sources.")
307 if self.doPreConvolve
and self.doDecorrelation
and not self.convolveTemplate:
308 raise ValueError(
"doPreConvolve=True and doDecorrelation=True and "
309 "convolveTemplate=False is not supported.")
310 if hasattr(self.getTemplate,
"coaddName"):
311 if self.getTemplate.coaddName != self.coaddName:
312 raise ValueError(
"Mis-matched coaddName and getTemplate.coaddName in the config.")
315 class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
318 def getTargetList(parsedCmd, **kwargs):
319 return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
323 class ImageDifferenceTask(pipeBase.CmdLineTask, pipeBase.PipelineTask):
324 """Subtract an image from a template and measure the result
326 ConfigClass = ImageDifferenceConfig
327 RunnerClass = ImageDifferenceTaskRunner
328 _DefaultName =
"imageDifference"
330 def __init__(self, butler=None, **kwargs):
331 """!Construct an ImageDifference Task
333 @param[in] butler Butler object to use in constructing reference object loaders
335 super().__init__(**kwargs)
336 self.makeSubtask(
"getTemplate")
338 self.makeSubtask(
"subtract")
340 if self.config.subtract.name ==
'al' and self.config.doDecorrelation:
341 self.makeSubtask(
"decorrelate")
343 if self.config.doScaleTemplateVariance:
344 self.makeSubtask(
"scaleVariance")
346 if self.config.doUseRegister:
347 self.makeSubtask(
"register")
348 self.schema = afwTable.SourceTable.makeMinimalSchema()
350 if self.config.doSelectSources:
351 self.makeSubtask(
"sourceSelector")
352 if self.config.kernelSourcesFromRef:
353 self.makeSubtask(
'refObjLoader', butler=butler)
354 self.makeSubtask(
"astrometer", refObjLoader=self.refObjLoader)
357 if self.config.doDetection:
358 self.makeSubtask(
"detection", schema=self.schema)
359 if self.config.doMeasurement:
360 self.makeSubtask(
"measurement", schema=self.schema,
361 algMetadata=self.algMetadata)
362 if self.config.doForcedMeasurement:
363 self.schema.addField(
364 "ip_diffim_forced_PsfFlux_instFlux",
"D",
365 "Forced PSF flux measured on the direct image.")
366 self.schema.addField(
367 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
368 "Forced PSF flux error measured on the direct image.")
369 self.schema.addField(
370 "ip_diffim_forced_PsfFlux_area",
"F",
371 "Forced PSF flux effective area of PSF.",
373 self.schema.addField(
374 "ip_diffim_forced_PsfFlux_flag",
"Flag",
375 "Forced PSF flux general failure flag.")
376 self.schema.addField(
377 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
378 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
379 self.schema.addField(
380 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
381 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
382 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
383 if self.config.doMatchSources:
384 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
385 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
388 def makeIdFactory(expId, expBits):
389 """Create IdFactory instance for unique 64 bit diaSource id-s.
397 Number of used bits in ``expId``.
401 The diasource id-s consists of the ``expId`` stored fixed in the highest value
402 ``expBits`` of the 64-bit integer plus (bitwise or) a generated sequence number in the
403 low value end of the integer.
407 idFactory: `lsst.afw.table.IdFactory`
409 return afwTable.IdFactory.makeSource(expId, 64 - expBits)
412 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
413 inputRefs: pipeBase.InputQuantizedConnection,
414 outputRefs: pipeBase.OutputQuantizedConnection):
415 inputs = butlerQC.get(inputRefs)
416 self.log.
info(f
"Processing {butlerQC.quantum.dataId}")
417 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
419 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
420 if self.config.coaddName ==
'dcr':
421 templateExposures = inputRefs.dcrCoadds
423 templateExposures = inputRefs.coaddExposures
424 templateStruct = self.getTemplate.runQuantum(
425 inputs[
'exposure'], butlerQC, inputRefs.skyMap, templateExposures
428 outputs = self.run(exposure=inputs[
'exposure'],
429 templateExposure=templateStruct.exposure,
431 butlerQC.put(outputs, outputRefs)
434 def runDataRef(self, sensorRef, templateIdList=None):
435 """Subtract an image from a template coadd and measure the result.
437 Data I/O wrapper around `run` using the butler in Gen2.
441 sensorRef : `lsst.daf.persistence.ButlerDataRef`
442 Sensor-level butler data reference, used for the following data products:
449 - self.config.coaddName + "Coadd_skyMap"
450 - self.config.coaddName + "Coadd"
451 Input or output, depending on config:
452 - self.config.coaddName + "Diff_subtractedExp"
453 Output, depending on config:
454 - self.config.coaddName + "Diff_matchedExp"
455 - self.config.coaddName + "Diff_src"
459 results : `lsst.pipe.base.Struct`
460 Returns the Struct by `run`.
462 subtractedExposureName = self.config.coaddName +
"Diff_differenceExp"
463 subtractedExposure =
None
465 calexpBackgroundExposure =
None
466 self.log.
info(
"Processing %s" % (sensorRef.dataId))
471 idFactory = self.makeIdFactory(expId=int(sensorRef.get(
"ccdExposureId")),
472 expBits=sensorRef.get(
"ccdExposureId_bits"))
473 if self.config.doAddCalexpBackground:
474 calexpBackgroundExposure = sensorRef.get(
"calexpBackground")
477 exposure = sensorRef.get(
"calexp", immediate=
True)
480 template = self.getTemplate.runDataRef(exposure, sensorRef, templateIdList=templateIdList)
482 if sensorRef.datasetExists(
"src"):
483 self.log.
info(
"Source selection via src product")
485 selectSources = sensorRef.get(
"src")
487 if not self.config.doSubtract
and self.config.doDetection:
489 subtractedExposure = sensorRef.get(subtractedExposureName)
492 results = self.run(exposure=exposure,
493 selectSources=selectSources,
494 templateExposure=template.exposure,
495 templateSources=template.sources,
497 calexpBackgroundExposure=calexpBackgroundExposure,
498 subtractedExposure=subtractedExposure)
500 if self.config.doWriteSources
and results.diaSources
is not None:
501 sensorRef.put(results.diaSources, self.config.coaddName +
"Diff_diaSrc")
502 if self.config.doWriteMatchedExp:
503 sensorRef.put(results.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
504 if self.config.doAddMetrics
and self.config.doSelectSources:
505 sensorRef.put(results.selectSources, self.config.coaddName +
"Diff_kernelSrc")
506 if self.config.doWriteSubtractedExp:
507 sensorRef.put(results.subtractedExposure, subtractedExposureName)
510 def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
511 idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
512 """PSF matches, subtract two images and perform detection on the difference image.
516 exposure : `lsst.afw.image.ExposureF`, optional
517 The science exposure, the minuend in the image subtraction.
518 Can be None only if ``config.doSubtract==False``.
519 selectSources : `lsst.afw.table.SourceCatalog`, optional
520 Identified sources on the science exposure. This catalog is used to
521 select sources in order to perform the AL PSF matching on stamp images
522 around them. The selection steps depend on config options and whether
523 ``templateSources`` and ``matchingSources`` specified.
524 templateExposure : `lsst.afw.image.ExposureF`, optional
525 The template to be subtracted from ``exposure`` in the image subtraction.
526 The template exposure should cover the same sky area as the science exposure.
527 It is either a stich of patches of a coadd skymap image or a calexp
528 of the same pointing as the science exposure. Can be None only
529 if ``config.doSubtract==False`` and ``subtractedExposure`` is not None.
530 templateSources : `lsst.afw.table.SourceCatalog`, optional
531 Identified sources on the template exposure.
532 idFactory : `lsst.afw.table.IdFactory`
533 Generator object to assign ids to detected sources in the difference image.
534 calexpBackgroundExposure : `lsst.afw.image.ExposureF`, optional
535 Background exposure to be added back to the science exposure
536 if ``config.doAddCalexpBackground==True``
537 subtractedExposure : `lsst.afw.image.ExposureF`, optional
538 If ``config.doSubtract==False`` and ``config.doDetection==True``,
539 performs the post subtraction source detection only on this exposure.
540 Otherwise should be None.
544 results : `lsst.pipe.base.Struct`
545 ``subtractedExposure`` : `lsst.afw.image.ExposureF`
547 ``matchedExposure`` : `lsst.afw.image.ExposureF`
548 The matched PSF exposure.
549 ``subtractRes`` : `lsst.pipe.base.Struct`
550 The returned result structure of the ImagePsfMatchTask subtask.
551 ``diaSources`` : `lsst.afw.table.SourceCatalog`
552 The catalog of detected sources.
553 ``selectSources`` : `lsst.afw.table.SourceCatalog`
554 The input source catalog with optionally added Qa information.
558 The following major steps are included:
560 - warp template coadd to match WCS of image
561 - PSF match image to warped template
562 - subtract image from PSF-matched, warped template
566 For details about the image subtraction configuration modes
567 see `lsst.ip.diffim`.
570 controlSources =
None
574 if self.config.doAddCalexpBackground:
575 mi = exposure.getMaskedImage()
576 mi += calexpBackgroundExposure.getImage()
578 if not exposure.hasPsf():
579 raise pipeBase.TaskError(
"Exposure has no psf")
580 sciencePsf = exposure.getPsf()
582 if self.config.doSubtract:
583 if self.config.doScaleTemplateVariance:
584 templateVarFactor = self.scaleVariance.
run(
585 templateExposure.getMaskedImage())
586 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
588 if self.config.subtract.name ==
'zogy':
589 subtractRes = self.subtract.subtractExposures(templateExposure, exposure,
591 spatiallyVarying=self.config.doSpatiallyVarying,
592 doPreConvolve=self.config.doPreConvolve)
593 subtractedExposure = subtractRes.subtractedExposure
595 elif self.config.subtract.name ==
'al':
597 scienceSigmaOrig = sciencePsf.computeShape().getDeterminantRadius()
598 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
606 if self.config.doPreConvolve:
609 srcMI = exposure.getMaskedImage()
610 exposureOrig = exposure.clone()
611 destMI = srcMI.Factory(srcMI.getDimensions())
613 if self.config.useGaussianForPreConvolution:
615 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
621 exposure.setMaskedImage(destMI)
622 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
624 scienceSigmaPost = scienceSigmaOrig
625 exposureOrig = exposure
630 if self.config.doSelectSources:
631 if selectSources
is None:
632 self.log.
warn(
"Src product does not exist; running detection, measurement, selection")
634 selectSources = self.subtract.getSelectSources(
636 sigma=scienceSigmaPost,
637 doSmooth=
not self.config.doPreConvolve,
641 if self.config.doAddMetrics:
645 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
646 targetFwhmPix=templateSigma*FwhmPerSigma))
654 selectSources = kcQa.addToSchema(selectSources)
655 if self.config.kernelSourcesFromRef:
657 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
658 matches = astromRet.matches
659 elif templateSources:
662 mc.findOnlyClosest =
False
666 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False,"
667 "but template sources not available. Cannot match science "
668 "sources with template sources. Run process* on data from "
669 "which templates are built.")
671 kernelSources = self.sourceSelector.
run(selectSources, exposure=exposure,
672 matches=matches).sourceCat
673 random.shuffle(kernelSources, random.random)
674 controlSources = kernelSources[::self.config.controlStepSize]
675 kernelSources = [k
for i, k
in enumerate(kernelSources)
676 if i % self.config.controlStepSize]
678 if self.config.doSelectDcrCatalog:
682 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
683 controlSources.extend(redSources)
687 grMax=self.sourceSelector.config.grMin))
688 blueSources = blueSelector.selectStars(exposure, selectSources,
689 matches=matches).starCat
690 controlSources.extend(blueSources)
692 if self.config.doSelectVariableCatalog:
695 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
696 controlSources.extend(varSources)
698 self.log.
info(
"Selected %d / %d sources for Psf matching (%d for control sample)"
699 % (len(kernelSources), len(selectSources), len(controlSources)))
703 if self.config.doUseRegister:
704 self.log.
info(
"Registering images")
706 if templateSources
is None:
710 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
711 templateSources = self.subtract.getSelectSources(
720 wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
721 warpedExp = self.register.
warpExposure(templateExposure, wcsResults.wcs,
722 exposure.getWcs(), exposure.getBBox())
723 templateExposure = warpedExp
728 if self.config.doDebugRegister:
730 srcToMatch = {x.second.getId(): x.first
for x
in matches}
732 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
733 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidKey()
734 sids = [m.first.getId()
for m
in wcsResults.matches]
735 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
736 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
737 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
738 allresids = dict(zip(sids, zip(positions, residuals)))
740 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
741 wcsResults.wcs.pixelToSky(
742 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
743 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
744 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
745 for x
in sids
if x
in srcToMatch.keys()])
746 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
747 if s
in srcToMatch.keys()])
748 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
749 if s
in srcToMatch.keys()])
750 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
751 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
752 & (colors <= self.sourceSelector.config.grMax))
753 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
754 rms1Long = IqrToSigma*(
755 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
756 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
757 - numpy.percentile(dlat[idx1], 25))
758 rms2Long = IqrToSigma*(
759 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
760 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
761 - numpy.percentile(dlat[idx2], 25))
762 rms3Long = IqrToSigma*(
763 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
764 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
765 - numpy.percentile(dlat[idx3], 25))
766 self.log.
info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f" %
767 (numpy.median(dlong[idx1]), rms1Long,
768 numpy.median(dlat[idx1]), rms1Lat))
769 self.log.
info(
"Green star offsets'': %.3f %.3f, %.3f %.3f" %
770 (numpy.median(dlong[idx2]), rms2Long,
771 numpy.median(dlat[idx2]), rms2Lat))
772 self.log.
info(
"Red star offsets'': %.3f %.3f, %.3f %.3f" %
773 (numpy.median(dlong[idx3]), rms3Long,
774 numpy.median(dlat[idx3]), rms3Lat))
776 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
777 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
778 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
779 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
780 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
781 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
783 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
784 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
785 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
786 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
787 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
788 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
795 self.log.
info(
"Subtracting images")
796 subtractRes = self.subtract.subtractExposures(
797 templateExposure=templateExposure,
798 scienceExposure=exposure,
799 candidateList=kernelSources,
800 convolveTemplate=self.config.convolveTemplate,
801 doWarping=
not self.config.doUseRegister
803 subtractedExposure = subtractRes.subtractedExposure
805 if self.config.doDetection:
806 self.log.
info(
"Computing diffim PSF")
809 if not subtractedExposure.hasPsf():
810 if self.config.convolveTemplate:
811 subtractedExposure.setPsf(exposure.getPsf())
813 subtractedExposure.setPsf(templateExposure.getPsf())
820 if self.config.doDecorrelation
and self.config.doSubtract:
822 if preConvPsf
is not None:
823 preConvKernel = preConvPsf.getLocalKernel()
824 if self.config.convolveTemplate:
825 self.log.
info(
"Decorrelation after template image convolution")
826 decorrResult = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
828 subtractRes.psfMatchingKernel,
829 spatiallyVarying=self.config.doSpatiallyVarying,
830 preConvKernel=preConvKernel)
832 self.log.
info(
"Decorrelation after science image convolution")
833 decorrResult = self.decorrelate.
run(subtractRes.warpedExposure, exposureOrig,
835 subtractRes.psfMatchingKernel,
836 spatiallyVarying=self.config.doSpatiallyVarying,
837 preConvKernel=preConvKernel)
838 subtractedExposure = decorrResult.correctedExposure
842 if self.config.doDetection:
843 self.log.
info(
"Running diaSource detection")
845 mask = subtractedExposure.getMaskedImage().getMask()
846 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
848 table = afwTable.SourceTable.make(self.schema, idFactory)
849 table.setMetadata(self.algMetadata)
850 results = self.detection.
run(
852 exposure=subtractedExposure,
853 doSmooth=
not self.config.doPreConvolve
856 if self.config.doMerge:
857 fpSet = results.fpSets.positive
858 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
859 self.config.growFootprint,
False)
861 fpSet.makeSources(diaSources)
862 self.log.
info(
"Merging detections into %d sources" % (len(diaSources)))
864 diaSources = results.sources
866 if self.config.doMeasurement:
867 newDipoleFitting = self.config.doDipoleFitting
868 self.log.
info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
869 if not newDipoleFitting:
871 self.measurement.
run(diaSources, subtractedExposure)
874 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
875 self.measurement.
run(diaSources, subtractedExposure, exposure,
876 subtractRes.matchedExposure)
878 self.measurement.
run(diaSources, subtractedExposure, exposure)
880 if self.config.doForcedMeasurement:
883 forcedSources = self.forcedMeasurement.generateMeasCat(
884 exposure, diaSources, subtractedExposure.getWcs())
885 self.forcedMeasurement.
run(forcedSources, exposure, diaSources, subtractedExposure.getWcs())
887 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
888 "ip_diffim_forced_PsfFlux_instFlux",
True)
889 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
890 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
891 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
892 "ip_diffim_forced_PsfFlux_area",
True)
893 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
894 "ip_diffim_forced_PsfFlux_flag",
True)
895 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
896 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
897 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
898 "ip_diffim_forced_PsfFlux_flag_edge",
True)
899 for diaSource, forcedSource
in zip(diaSources, forcedSources):
900 diaSource.assign(forcedSource, mapper)
903 if self.config.doMatchSources:
904 if selectSources
is not None:
906 matchRadAsec = self.config.diaSourceMatchRadius
907 matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
910 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
911 srcMatch
in srcMatches])
912 self.log.
info(
"Matched %d / %d diaSources to sources" % (len(srcMatchDict),
915 self.log.
warn(
"Src product does not exist; cannot match with diaSources")
920 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
922 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
923 refMatches = astromRet.matches
924 if refMatches
is None:
925 self.log.
warn(
"No diaSource matches with reference catalog")
928 self.log.
info(
"Matched %d / %d diaSources to reference catalog" % (len(refMatches),
930 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for
931 refMatch
in refMatches])
934 for diaSource
in diaSources:
935 sid = diaSource.getId()
936 if sid
in srcMatchDict:
937 diaSource.set(
"srcMatchId", srcMatchDict[sid])
938 if sid
in refMatchDict:
939 diaSource.set(
"refMatchId", refMatchDict[sid])
941 if self.config.doAddMetrics
and self.config.doSelectSources:
942 self.log.
info(
"Evaluating metrics and control sample")
945 for cell
in subtractRes.kernelCellSet.getCellList():
946 for cand
in cell.begin(
False):
947 kernelCandList.append(cand)
950 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
951 nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
954 diffimTools.sourceTableToCandidateList(controlSources,
955 subtractRes.warpedExposure, exposure,
956 self.config.subtract.kernel.active,
957 self.config.subtract.kernel.active.detectionConfig,
958 self.log, doBuild=
True, basisList=basisList))
960 KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
961 subtractRes.backgroundModel, dof=nparam)
962 KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
963 subtractRes.backgroundModel)
965 if self.config.doDetection:
966 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
968 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
970 self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
971 return pipeBase.Struct(
972 subtractedExposure=subtractedExposure,
973 matchedExposure=subtractRes.matchedExposure,
974 subtractRes=subtractRes,
975 diaSources=diaSources,
976 selectSources=selectSources
979 def fitAstrometry(self, templateSources, templateExposure, selectSources):
980 """Fit the relative astrometry between templateSources and selectSources
985 Remove this method. It originally fit a new WCS to the template before calling register.run
986 because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed.
987 It remains because a subtask overrides it.
989 results = self.register.
run(templateSources, templateExposure.getWcs(),
990 templateExposure.getBBox(), selectSources)
993 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
994 """Make debug plots and displays.
998 Test and update for current debug display and slot names
1008 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
1009 if not maskTransparency:
1010 maskTransparency = 0
1011 disp.setMaskTransparency(maskTransparency)
1013 if display
and showSubtracted:
1014 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
1015 mi = subtractRes.subtractedExposure.getMaskedImage()
1016 x0, y0 = mi.getX0(), mi.getY0()
1017 with disp.Buffering():
1018 for s
in diaSources:
1019 x, y = s.getX() - x0, s.getY() - y0
1020 ctype =
"red" if s.get(
"flags_negative")
else "yellow"
1021 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
1022 or s.get(
"base_PixelFlags_flag_saturatedCenter")
1023 or s.get(
"base_PixelFlags_flag_crCenter")):
1025 elif (s.get(
"base_PixelFlags_flag_interpolated")
1026 or s.get(
"base_PixelFlags_flag_saturated")
1027 or s.get(
"base_PixelFlags_flag_cr")):
1031 disp.dot(ptype, x, y, size=4, ctype=ctype)
1032 lsstDebug.frame += 1
1034 if display
and showPixelResiduals
and selectSources:
1035 nonKernelSources = []
1036 for source
in selectSources:
1037 if source
not in kernelSources:
1038 nonKernelSources.append(source)
1040 diUtils.plotPixelResiduals(exposure,
1041 subtractRes.warpedExposure,
1042 subtractRes.subtractedExposure,
1043 subtractRes.kernelCellSet,
1044 subtractRes.psfMatchingKernel,
1045 subtractRes.backgroundModel,
1047 self.subtract.config.kernel.active.detectionConfig,
1049 diUtils.plotPixelResiduals(exposure,
1050 subtractRes.warpedExposure,
1051 subtractRes.subtractedExposure,
1052 subtractRes.kernelCellSet,
1053 subtractRes.psfMatchingKernel,
1054 subtractRes.backgroundModel,
1056 self.subtract.config.kernel.active.detectionConfig,
1058 if display
and showDiaSources:
1060 isFlagged = [flagChecker(x)
for x
in diaSources]
1061 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
1062 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
1063 frame=lsstDebug.frame)
1064 lsstDebug.frame += 1
1066 if display
and showDipoles:
1067 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
1068 frame=lsstDebug.frame)
1069 lsstDebug.frame += 1
1071 def _getConfigName(self):
1072 """Return the name of the config dataset
1074 return "%sDiff_config" % (self.config.coaddName,)
1076 def _getMetadataName(self):
1077 """Return the name of the metadata dataset
1079 return "%sDiff_metadata" % (self.config.coaddName,)
1081 def getSchemaCatalogs(self):
1082 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
1084 diaSrc.getTable().setMetadata(self.algMetadata)
1085 return {self.config.coaddName +
"Diff_diaSrc": diaSrc}
1088 def _makeArgumentParser(cls):
1089 """Create an argument parser
1091 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1092 parser.add_id_argument(
"--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
1093 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
1094 help=
"Template data ID in case of calexp template,"
1095 " e.g. --templateId visit=6789")
1099 class Winter2013ImageDifferenceConfig(ImageDifferenceConfig):
1100 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1101 doc=
"Shift stars going into RegisterTask by this amount")
1102 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1103 doc=
"Perturb stars going into RegisterTask by this amount")
1106 ImageDifferenceConfig.setDefaults(self)
1107 self.getTemplate.retarget(GetCalexpAsTemplateTask)
1110 class Winter2013ImageDifferenceTask(ImageDifferenceTask):
1111 """!Image difference Task used in the Winter 2013 data challege.
1112 Enables testing the effects of registration shifts and scatter.
1114 For use with winter 2013 simulated images:
1115 Use --templateId visit=88868666 for sparse data
1116 --templateId visit=22222200 for dense data (g)
1117 --templateId visit=11111100 for dense data (i)
1119 ConfigClass = Winter2013ImageDifferenceConfig
1120 _DefaultName =
"winter2013ImageDifference"
1122 def __init__(self, **kwargs):
1123 ImageDifferenceTask.__init__(self, **kwargs)
1125 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1126 """Fit the relative astrometry between templateSources and selectSources"""
1127 if self.config.winter2013WcsShift > 0.0:
1129 self.config.winter2013WcsShift)
1130 cKey = templateSources[0].getTable().getCentroidKey()
1131 for source
in templateSources:
1132 centroid = source.get(cKey)
1133 source.set(cKey, centroid + offset)
1134 elif self.config.winter2013WcsRms > 0.0:
1135 cKey = templateSources[0].getTable().getCentroidKey()
1136 for source
in templateSources:
1137 offset =
geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1138 self.config.winter2013WcsRms*numpy.random.normal())
1139 centroid = source.get(cKey)
1140 source.set(cKey, centroid + offset)
1142 results = self.register.
run(templateSources, templateExposure.getWcs(),
1143 templateExposure.getBBox(), selectSources)