29from lsst.utils.introspection
import find_outside_stacklevel
30from lsst.ip.diffim.utils
import evaluateMeanPsfFwhm, getPsfFwhm
31from lsst.meas.algorithms
import ScaleVarianceTask
36from .
import MakeKernelTask, DecorrelateALKernelTask
37from lsst.utils.timer
import timeMethod
39__all__ = [
"AlardLuptonSubtractConfig",
"AlardLuptonSubtractTask",
40 "AlardLuptonPreconvolveSubtractConfig",
"AlardLuptonPreconvolveSubtractTask"]
42_dimensions = (
"instrument",
"visit",
"detector")
43_defaultTemplates = {
"coaddName":
"deep",
"fakesType":
""}
47 dimensions=_dimensions,
48 defaultTemplates=_defaultTemplates):
49 template = connectionTypes.Input(
50 doc=
"Input warped template to subtract.",
51 dimensions=(
"instrument",
"visit",
"detector"),
52 storageClass=
"ExposureF",
53 name=
"{fakesType}{coaddName}Diff_templateExp"
55 science = connectionTypes.Input(
56 doc=
"Input science exposure to subtract from.",
57 dimensions=(
"instrument",
"visit",
"detector"),
58 storageClass=
"ExposureF",
59 name=
"{fakesType}calexp"
61 sources = connectionTypes.Input(
62 doc=
"Sources measured on the science exposure; "
63 "used to select sources for making the matching kernel.",
64 dimensions=(
"instrument",
"visit",
"detector"),
65 storageClass=
"SourceCatalog",
68 finalizedPsfApCorrCatalog = connectionTypes.Input(
69 doc=(
"Per-visit finalized psf models and aperture correction maps. "
70 "These catalogs use the detector id for the catalog id, "
71 "sorted on id for fast lookup."),
72 dimensions=(
"instrument",
"visit"),
73 storageClass=
"ExposureCatalog",
74 name=
"finalVisitSummary",
77 "Deprecated in favor of visitSummary. Will be removed after v26."
80 visitSummary = connectionTypes.Input(
81 doc=(
"Per-visit catalog with final calibration objects. "
82 "These catalogs use the detector id for the catalog id, "
83 "sorted on id for fast lookup."),
84 dimensions=(
"instrument",
"visit"),
85 storageClass=
"ExposureCatalog",
86 name=
"finalVisitSummary",
91 if not config.doApplyFinalizedPsf:
92 self.inputs.remove(
"finalizedPsfApCorrCatalog")
93 if not config.doApplyExternalCalibrations
or config.doApplyFinalizedPsf:
98 dimensions=_dimensions,
99 defaultTemplates=_defaultTemplates):
100 difference = connectionTypes.Output(
101 doc=
"Result of subtracting convolved template from science image.",
102 dimensions=(
"instrument",
"visit",
"detector"),
103 storageClass=
"ExposureF",
104 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
106 matchedTemplate = connectionTypes.Output(
107 doc=
"Warped and PSF-matched template used to create `subtractedExposure`.",
108 dimensions=(
"instrument",
"visit",
"detector"),
109 storageClass=
"ExposureF",
110 name=
"{fakesType}{coaddName}Diff_matchedExp",
115 dimensions=_dimensions,
116 defaultTemplates=_defaultTemplates):
117 scoreExposure = connectionTypes.Output(
118 doc=
"The maximum likelihood image, used for the detection of diaSources.",
119 dimensions=(
"instrument",
"visit",
"detector"),
120 storageClass=
"ExposureF",
121 name=
"{fakesType}{coaddName}Diff_scoreExp",
130 makeKernel = lsst.pex.config.ConfigurableField(
131 target=MakeKernelTask,
132 doc=
"Task to construct a matching kernel for convolution.",
134 doDecorrelation = lsst.pex.config.Field(
137 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
138 "kernel convolution? If True, also update the diffim PSF."
140 decorrelate = lsst.pex.config.ConfigurableField(
141 target=DecorrelateALKernelTask,
142 doc=
"Task to decorrelate the image difference.",
144 requiredTemplateFraction = lsst.pex.config.Field(
147 doc=
"Raise NoWorkFound and do not attempt image subtraction if template covers less than this "
148 " fraction of pixels. Setting to 0 will always attempt image subtraction."
150 minTemplateFractionForExpectedSuccess = lsst.pex.config.Field(
153 doc=
"Raise NoWorkFound if PSF-matching fails and template covers less than this fraction of pixels."
154 " If the fraction of pixels covered by the template is less than this value (and greater than"
155 " requiredTemplateFraction) this task is attempted but failure is anticipated and tolerated."
157 doScaleVariance = lsst.pex.config.Field(
160 doc=
"Scale variance of the image difference?"
162 scaleVariance = lsst.pex.config.ConfigurableField(
163 target=ScaleVarianceTask,
164 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
166 doSubtractBackground = lsst.pex.config.Field(
167 doc=
"Subtract the background fit when solving the kernel?",
171 doApplyFinalizedPsf = lsst.pex.config.Field(
172 doc=
"Replace science Exposure's psf and aperture correction map"
173 " with those in finalizedPsfApCorrCatalog.",
178 "Deprecated in favor of doApplyExternalCalibrations. "
179 "Will be removed after v26."
182 doApplyExternalCalibrations = lsst.pex.config.Field(
184 "Replace science Exposure's calibration objects with those"
185 " in visitSummary. Ignored if `doApplyFinalizedPsf is True."
190 detectionThreshold = lsst.pex.config.Field(
193 doc=
"Minimum signal to noise ratio of detected sources "
194 "to use for calculating the PSF matching kernel."
196 detectionThresholdMax = lsst.pex.config.Field(
199 doc=
"Maximum signal to noise ratio of detected sources "
200 "to use for calculating the PSF matching kernel."
202 maxKernelSources = lsst.pex.config.Field(
205 doc=
"Maximum number of sources to use for calculating the PSF matching kernel."
206 "Set to -1 to disable."
208 minKernelSources = lsst.pex.config.Field(
211 doc=
"Minimum number of sources needed for calculating the PSF matching kernel."
213 badSourceFlags = lsst.pex.config.ListField(
215 doc=
"Flags that, if set, the associated source should not "
216 "be used to determine the PSF matching kernel.",
217 default=(
"sky_source",
"slot_Centroid_flag",
218 "slot_ApFlux_flag",
"slot_PsfFlux_flag",
219 "base_PixelFlags_flag_interpolated",
220 "base_PixelFlags_flag_saturated",
221 "base_PixelFlags_flag_bad",
224 excludeMaskPlanes = lsst.pex.config.ListField(
226 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE",
"FAKE"),
227 doc=
"Mask planes to exclude when selecting sources for PSF matching."
229 badMaskPlanes = lsst.pex.config.ListField(
231 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE"),
232 doc=
"Mask planes to interpolate over."
234 preserveTemplateMask = lsst.pex.config.ListField(
236 default=(
"NO_DATA",
"BAD",),
237 doc=
"Mask planes from the template to propagate to the image difference."
239 renameTemplateMask = lsst.pex.config.ListField(
241 default=(
"SAT",
"INJECTED",
"INJECTED_CORE",),
242 doc=
"Mask planes from the template to propagate to the image difference"
243 "with '_TEMPLATE' appended to the name."
245 allowKernelSourceDetection = lsst.pex.config.Field(
248 doc=
"Re-run source detection for kernel candidates if an error is"
249 " encountered while calculating the matching kernel."
255 self.
makeKernel.kernel.active.spatialKernelOrder = 1
256 self.
makeKernel.kernel.active.spatialBgOrder = 2
260 pipelineConnections=AlardLuptonSubtractConnections):
261 mode = lsst.pex.config.ChoiceField(
263 default=
"convolveTemplate",
264 allowed={
"auto":
"Choose which image to convolve at runtime.",
265 "convolveScience":
"Only convolve the science image.",
266 "convolveTemplate":
"Only convolve the template image."},
267 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
272 """Compute the image difference of a science and template image using
273 the Alard & Lupton (1998) algorithm.
275 ConfigClass = AlardLuptonSubtractConfig
276 _DefaultName =
"alardLuptonSubtract"
280 self.makeSubtask(
"decorrelate")
281 self.makeSubtask(
"makeKernel")
282 if self.config.doScaleVariance:
283 self.makeSubtask(
"scaleVariance")
292 """Replace calibrations (psf, and ApCorrMap) on this exposure with
297 exposure : `lsst.afw.image.exposure.Exposure`
298 Input exposure to adjust calibrations.
299 visitSummary : `lsst.afw.table.ExposureCatalog`
300 Exposure catalog with external calibrations to be applied. Catalog
301 uses the detector id for the catalog id, sorted on id for fast
306 exposure : `lsst.afw.image.exposure.Exposure`
307 Exposure with adjusted calibrations.
309 detectorId = exposure.info.getDetector().getId()
311 row = visitSummary.find(detectorId)
313 self.
log.
warning(
"Detector id %s not found in external calibrations catalog; "
314 "Using original calibrations.", detectorId)
317 apCorrMap = row.getApCorrMap()
319 self.
log.
warning(
"Detector id %s has None for psf in "
320 "external calibrations catalog; Using original psf and aperture correction.",
322 elif apCorrMap
is None:
323 self.
log.
warning(
"Detector id %s has None for apCorrMap in "
324 "external calibrations catalog; Using original psf and aperture correction.",
328 exposure.info.setApCorrMap(apCorrMap)
333 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None,
335 """PSF match, subtract, and decorrelate two images.
339 template : `lsst.afw.image.ExposureF`
340 Template exposure, warped to match the science exposure.
341 science : `lsst.afw.image.ExposureF`
342 Science exposure to subtract from the template.
343 sources : `lsst.afw.table.SourceCatalog`
344 Identified sources on the science exposure. This catalog is used to
345 select sources in order to perform the AL PSF matching on stamp
347 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
348 Exposure catalog with finalized psf models and aperture correction
349 maps to be applied. Catalog uses the detector id for the catalog
350 id, sorted on id for fast lookup. Deprecated in favor of
351 ``visitSummary``, and will be removed after v26.
352 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
353 Exposure catalog with external calibrations to be applied. Catalog
354 uses the detector id for the catalog id, sorted on id for fast
355 lookup. Ignored (for temporary backwards compatibility) if
356 ``finalizedPsfApCorrCatalog`` is provided.
360 results : `lsst.pipe.base.Struct`
361 ``difference`` : `lsst.afw.image.ExposureF`
362 Result of subtracting template and science.
363 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
364 Warped and PSF-matched template exposure.
365 ``backgroundModel`` : `lsst.afw.math.Function2D`
366 Background model that was fit while solving for the
368 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
369 Kernel used to PSF-match the convolved image.
374 If an unsupported convolution mode is supplied.
376 If there are too few sources to calculate the PSF matching kernel.
377 lsst.pipe.base.NoWorkFound
378 Raised if fraction of good pixels, defined as not having NO_DATA
379 set, is less then the configured requiredTemplateFraction
382 if finalizedPsfApCorrCatalog
is not None:
384 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
385 "argument, and will be removed after v26.",
387 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
389 visitSummary = finalizedPsfApCorrCatalog
394 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
395 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
404 templatePsfSize = getPsfFwhm(template.psf)
405 sciencePsfSize = getPsfFwhm(science.psf)
407 self.
log.
info(
"Unable to evaluate PSF at the average position. "
408 "Evaluting PSF on a grid of points."
410 templatePsfSize = evaluateMeanPsfFwhm(template,
411 fwhmExposureBuffer=fwhmExposureBuffer,
412 fwhmExposureGrid=fwhmExposureGrid
414 sciencePsfSize = evaluateMeanPsfFwhm(science,
415 fwhmExposureBuffer=fwhmExposureBuffer,
416 fwhmExposureGrid=fwhmExposureGrid
418 self.
log.
info(
"Science PSF FWHM: %f pixels", sciencePsfSize)
419 self.
log.
info(
"Template PSF FWHM: %f pixels", templatePsfSize)
420 self.metadata.add(
"sciencePsfSize", sciencePsfSize)
421 self.metadata.add(
"templatePsfSize", templatePsfSize)
423 if self.config.mode ==
"auto":
426 fwhmExposureBuffer=fwhmExposureBuffer,
427 fwhmExposureGrid=fwhmExposureGrid)
429 if sciencePsfSize < templatePsfSize:
430 self.
log.
info(
"Average template PSF size is greater, "
431 "but science PSF greater in one dimension: convolving template image.")
433 self.
log.
info(
"Science PSF size is greater: convolving template image.")
435 self.
log.
info(
"Template PSF size is greater: convolving science image.")
436 elif self.config.mode ==
"convolveTemplate":
437 self.
log.
info(
"`convolveTemplate` is set: convolving template image.")
438 convolveTemplate =
True
439 elif self.config.mode ==
"convolveScience":
440 self.
log.
info(
"`convolveScience` is set: convolving science image.")
441 convolveTemplate =
False
443 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
446 sourceMask = science.mask.clone()
447 sourceMask.array |= template[science.getBBox()].mask.array
450 self.metadata.add(
"convolvedExposure",
"Template")
453 self.metadata.add(
"convolvedExposure",
"Science")
457 self.
log.
warning(
"Failed to match template. Checking coverage")
460 self.config.minTemplateFractionForExpectedSuccess,
461 exceptionMessage=
"Template coverage lower than expected to succeed."
462 f
" Failure is tolerable: {e}")
466 return subtractResults
469 """Convolve the template image with a PSF-matching kernel and subtract
470 from the science image.
474 template : `lsst.afw.image.ExposureF`
475 Template exposure, warped to match the science exposure.
476 science : `lsst.afw.image.ExposureF`
477 Science exposure to subtract from the template.
478 selectSources : `lsst.afw.table.SourceCatalog`
479 Identified sources on the science exposure. This catalog is used to
480 select sources in order to perform the AL PSF matching on stamp
485 results : `lsst.pipe.base.Struct`
487 ``difference`` : `lsst.afw.image.ExposureF`
488 Result of subtracting template and science.
489 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
490 Warped and PSF-matched template exposure.
491 ``backgroundModel`` : `lsst.afw.math.Function2D`
492 Background model that was fit while solving for the PSF-matching kernel
493 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
494 Kernel used to PSF-match the template to the science image.
497 kernelSources = self.makeKernel.selectKernelSources(template, science,
498 candidateList=selectSources,
500 kernelResult = self.makeKernel.
run(template, science, kernelSources,
502 except Exception
as e:
503 if self.config.allowKernelSourceDetection:
504 self.
log.
warning(
"Error encountered trying to construct the matching kernel"
505 f
" Running source detection and retrying. {e}")
506 kernelSources = self.makeKernel.selectKernelSources(template, science,
509 kernelResult = self.makeKernel.
run(template, science, kernelSources,
514 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
516 bbox=science.getBBox(),
518 photoCalib=science.photoCalib)
521 backgroundModel=(kernelResult.backgroundModel
522 if self.config.doSubtractBackground
else None))
523 correctedExposure = self.
finalize(template, science, difference,
524 kernelResult.psfMatchingKernel,
525 templateMatched=
True)
527 return lsst.pipe.base.Struct(difference=correctedExposure,
528 matchedTemplate=matchedTemplate,
529 matchedScience=science,
530 backgroundModel=kernelResult.backgroundModel,
531 psfMatchingKernel=kernelResult.psfMatchingKernel)
534 """Convolve the science image with a PSF-matching kernel and subtract
539 template : `lsst.afw.image.ExposureF`
540 Template exposure, warped to match the science exposure.
541 science : `lsst.afw.image.ExposureF`
542 Science exposure to subtract from the template.
543 selectSources : `lsst.afw.table.SourceCatalog`
544 Identified sources on the science exposure. This catalog is used to
545 select sources in order to perform the AL PSF matching on stamp
550 results : `lsst.pipe.base.Struct`
552 ``difference`` : `lsst.afw.image.ExposureF`
553 Result of subtracting template and science.
554 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
555 Warped template exposure. Note that in this case, the template
556 is not PSF-matched to the science image.
557 ``backgroundModel`` : `lsst.afw.math.Function2D`
558 Background model that was fit while solving for the PSF-matching kernel
559 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
560 Kernel used to PSF-match the science image to the template.
562 bbox = science.getBBox()
563 kernelSources = self.makeKernel.selectKernelSources(science, template,
564 candidateList=selectSources,
566 kernelResult = self.makeKernel.
run(science, template, kernelSources,
568 modelParams = kernelResult.backgroundModel.getParameters()
570 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
572 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
573 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
580 matchedScience.maskedImage /= norm
581 matchedTemplate = template.clone()[bbox]
582 matchedTemplate.maskedImage /= norm
583 matchedTemplate.setPhotoCalib(science.photoCalib)
586 backgroundModel=(kernelResult.backgroundModel
587 if self.config.doSubtractBackground
else None))
589 correctedExposure = self.
finalize(template, science, difference,
590 kernelResult.psfMatchingKernel,
591 templateMatched=
False)
593 return lsst.pipe.base.Struct(difference=correctedExposure,
594 matchedTemplate=matchedTemplate,
595 matchedScience=matchedScience,
596 backgroundModel=kernelResult.backgroundModel,
597 psfMatchingKernel=kernelResult.psfMatchingKernel,)
599 def finalize(self, template, science, difference, kernel,
600 templateMatched=True,
603 spatiallyVarying=False):
604 """Decorrelate the difference image to undo the noise correlations
605 caused by convolution.
609 template : `lsst.afw.image.ExposureF`
610 Template exposure, warped to match the science exposure.
611 science : `lsst.afw.image.ExposureF`
612 Science exposure to subtract from the template.
613 difference : `lsst.afw.image.ExposureF`
614 Result of subtracting template and science.
615 kernel : `lsst.afw.math.Kernel`
616 An (optionally spatially-varying) PSF matching kernel
617 templateMatched : `bool`, optional
618 Was the template PSF-matched to the science image?
619 preConvMode : `bool`, optional
620 Was the science image preconvolved with its own PSF
621 before PSF matching the template?
622 preConvKernel : `lsst.afw.detection.Psf`, optional
623 If not `None`, then the science image was pre-convolved with
624 (the reflection of) this kernel. Must be normalized to sum to 1.
625 spatiallyVarying : `bool`, optional
626 Compute the decorrelation kernel spatially varying across the image?
630 correctedExposure : `lsst.afw.image.ExposureF`
631 The decorrelated image difference.
633 if self.config.doDecorrelation:
634 self.
log.
info(
"Decorrelating image difference.")
638 correctedExposure = self.decorrelate.
run(science, template[science.getBBox()], difference, kernel,
639 templateMatched=templateMatched,
640 preConvMode=preConvMode,
641 preConvKernel=preConvKernel,
642 spatiallyVarying=spatiallyVarying).correctedExposure
644 self.
log.
info(
"NOT decorrelating image difference.")
645 correctedExposure = difference
646 return correctedExposure
650 """Check that the WCS of the two Exposures match, and the template bbox
651 contains the science bbox.
655 template : `lsst.afw.image.ExposureF`
656 Template exposure, warped to match the science exposure.
657 science : `lsst.afw.image.ExposureF`
658 Science exposure to subtract from the template.
663 Raised if the WCS of the template is not equal to the science WCS,
664 or if the science image is not fully contained in the template
667 assert template.wcs == science.wcs,\
668 "Template and science exposure WCS are not identical."
669 templateBBox = template.getBBox()
670 scienceBBox = science.getBBox()
672 assert templateBBox.contains(scienceBBox),\
673 "Template bbox does not contain all of the science image."
679 interpolateBadMaskPlanes=False,
681 """Convolve an exposure with the given kernel.
685 exposure : `lsst.afw.Exposure`
686 exposure to convolve.
687 kernel : `lsst.afw.math.LinearCombinationKernel`
688 PSF matching kernel computed in the ``makeKernel`` subtask.
689 convolutionControl : `lsst.afw.math.ConvolutionControl`
690 Configuration for convolve algorithm.
691 bbox : `lsst.geom.Box2I`, optional
692 Bounding box to trim the convolved exposure to.
693 psf : `lsst.afw.detection.Psf`, optional
694 Point spread function (PSF) to set for the convolved exposure.
695 photoCalib : `lsst.afw.image.PhotoCalib`, optional
696 Photometric calibration of the convolved exposure.
700 convolvedExp : `lsst.afw.Exposure`
703 convolvedExposure = exposure.clone()
705 convolvedExposure.setPsf(psf)
706 if photoCalib
is not None:
707 convolvedExposure.setPhotoCalib(photoCalib)
708 if interpolateBadMaskPlanes
and self.config.badMaskPlanes
is not None:
710 self.config.badMaskPlanes)
711 self.metadata.add(
"nInterpolated", nInterp)
712 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
714 convolvedExposure.setMaskedImage(convolvedImage)
716 return convolvedExposure
718 return convolvedExposure[bbox]
721 """Select sources from a catalog that meet the selection criteria.
725 sources : `lsst.afw.table.SourceCatalog`
726 Input source catalog to select sources from.
727 mask : `lsst.afw.image.Mask`
728 The image mask plane to use to reject sources
729 based on their location on the ccd.
733 selectSources : `lsst.afw.table.SourceCatalog`
734 The input source catalog, with flagged and low signal-to-noise
740 If there are too few sources to compute the PSF matching kernel
741 remaining after source selection.
743 flags = np.ones(
len(sources), dtype=bool)
744 for flag
in self.config.badSourceFlags:
746 flags *= ~sources[flag]
747 except Exception
as e:
748 self.
log.
warning(
"Could not apply source flag: %s", e)
749 signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr()
750 sToNFlag = signalToNoise > self.config.detectionThreshold
752 sToNFlagMax = signalToNoise < self.config.detectionThresholdMax
754 flags *= self.
_checkMask(mask, sources, self.config.excludeMaskPlanes)
755 selectSources = sources[flags].copy(deep=
True)
756 if (
len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
757 signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
758 indices = np.argsort(signalToNoise)
759 indices = indices[-self.config.maxKernelSources:]
760 flags = np.zeros(
len(selectSources), dtype=bool)
761 flags[indices] =
True
762 selectSources = selectSources[flags].copy(deep=
True)
764 self.
log.
info(
"%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
765 len(selectSources),
len(sources), 100*
len(selectSources)/
len(sources))
766 if len(selectSources) < self.config.minKernelSources:
767 self.
log.error(
"Too few sources to calculate the PSF matching kernel: "
768 "%i selected but %i needed for the calculation.",
769 len(selectSources), self.config.minKernelSources)
770 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
771 self.metadata.add(
"nPsfSources",
len(selectSources))
777 """Exclude sources that are located on masked pixels.
781 mask : `lsst.afw.image.Mask`
782 The image mask plane to use to reject sources
783 based on the location of their centroid on the ccd.
784 sources : `lsst.afw.table.SourceCatalog`
785 The source catalog to evaluate.
786 excludeMaskPlanes : `list` of `str`
787 List of the names of the mask planes to exclude.
791 flags : `numpy.ndarray` of `bool`
792 Array indicating whether each source in the catalog should be
793 kept (True) or rejected (False) based on the value of the
794 mask plane at its location.
796 setExcludeMaskPlanes = [
797 maskPlane
for maskPlane
in excludeMaskPlanes
if maskPlane
in mask.getMaskPlaneDict()
800 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
802 xv = np.rint(sources.getX() - mask.getX0())
803 yv = np.rint(sources.getY() - mask.getY0())
805 mv = mask.array[yv.astype(int), xv.astype(int)]
806 flags = np.bitwise_and(mv, excludePixelMask) == 0
810 """Perform preparatory calculations common to all Alard&Lupton Tasks.
814 template : `lsst.afw.image.ExposureF`
815 Template exposure, warped to match the science exposure. The
816 variance plane of the template image is modified in place.
817 science : `lsst.afw.image.ExposureF`
818 Science exposure to subtract from the template. The variance plane
819 of the science image is modified in place.
820 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
821 Exposure catalog with external calibrations to be applied. Catalog
822 uses the detector id for the catalog id, sorted on id for fast
826 if visitSummary
is not None:
829 requiredTemplateFraction=self.config.requiredTemplateFraction,
830 exceptionMessage=
"Not attempting subtraction. To force subtraction,"
831 " set config requiredTemplateFraction=0")
833 if self.config.doScaleVariance:
837 templateVarFactor = self.scaleVariance.
run(template.maskedImage)
838 sciVarFactor = self.scaleVariance.
run(science.maskedImage)
839 self.
log.
info(
"Template variance scaling factor: %.2f", templateVarFactor)
840 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
841 self.
log.
info(
"Science variance scaling factor: %.2f", sciVarFactor)
842 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
849 """Update the science and template mask planes before differencing.
853 template : `lsst.afw.image.Exposure`
854 Template exposure, warped to match the science exposure.
855 The template mask planes will be erased, except for a few specified
857 science : `lsst.afw.image.Exposure`
858 Science exposure to subtract from the template.
859 The DETECTED and DETECTED_NEGATIVE mask planes of the science image
862 self.
_clearMask(science.mask, clearMaskPlanes=[
"DETECTED",
"DETECTED_NEGATIVE"])
869 clearMaskPlanes = [mp
for mp
in template.mask.getMaskPlaneDict().keys()
870 if mp
not in self.config.preserveTemplateMask]
871 renameMaskPlanes = [mp
for mp
in self.config.renameTemplateMask
872 if mp
in template.mask.getMaskPlaneDict().keys()]
877 if "FAKE" in science.mask.getMaskPlaneDict().keys():
878 self.
log.
info(
"Adding injected mask plane to science image")
880 if "FAKE" in template.mask.getMaskPlaneDict().keys():
881 self.
log.
info(
"Adding injected mask plane to template image")
883 if "INJECTED" in renameMaskPlanes:
884 renameMaskPlanes.remove(
"INJECTED")
885 if "INJECTED_TEMPLATE" in clearMaskPlanes:
886 clearMaskPlanes.remove(
"INJECTED_TEMPLATE")
888 for maskPlane
in renameMaskPlanes:
890 self.
_clearMask(template.mask, clearMaskPlanes=clearMaskPlanes)
894 """Rename a mask plane by adding the new name and copying the data.
898 mask : `lsst.afw.image.Mask`
899 The mask image to update in place.
901 The name of the existing mask plane to copy.
903 The new name of the mask plane that will be added.
904 If the mask plane already exists, it will be updated in place.
906 mask.addMaskPlane(newMaskPlane)
907 originBitMask = mask.getPlaneBitMask(maskPlane)
908 destinationBitMask = mask.getPlaneBitMask(newMaskPlane)
909 mask.array |= ((mask.array & originBitMask) > 0)*destinationBitMask
912 """Clear the mask plane of the template.
916 mask : `lsst.afw.image.Mask`
917 The mask plane to erase, which will be modified in place.
918 clearMaskPlanes : `list` of `str`, optional
919 Erase the specified mask planes.
920 If not supplied, the entire mask will be erased.
922 if clearMaskPlanes
is None:
923 clearMaskPlanes = list(mask.getMaskPlaneDict().keys())
925 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
926 mask &= ~bitMaskToClear
930 SubtractScoreOutputConnections):
935 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
940 """Subtract a template from a science image, convolving the science image
941 before computing the kernel, and also convolving the template before
944 ConfigClass = AlardLuptonPreconvolveSubtractConfig
945 _DefaultName =
"alardLuptonPreconvolveSubtract"
947 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None):
948 """Preconvolve the science image with its own PSF,
949 convolve the template image with a PSF-matching kernel and subtract
950 from the preconvolved science image.
954 template : `lsst.afw.image.ExposureF`
955 The template image, which has previously been warped to the science
956 image. The template bbox will be padded by a few pixels compared to
958 science : `lsst.afw.image.ExposureF`
959 The science exposure.
960 sources : `lsst.afw.table.SourceCatalog`
961 Identified sources on the science exposure. This catalog is used to
962 select sources in order to perform the AL PSF matching on stamp
964 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
965 Exposure catalog with finalized psf models and aperture correction
966 maps to be applied. Catalog uses the detector id for the catalog
967 id, sorted on id for fast lookup. Deprecated in favor of
968 ``visitSummary``, and will be removed after v26.
969 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
970 Exposure catalog with complete external calibrations. Catalog uses
971 the detector id for the catalog id, sorted on id for fast lookup.
972 Ignored (for temporary backwards compatibility) if
973 ``finalizedPsfApCorrCatalog`` is provided.
977 results : `lsst.pipe.base.Struct`
978 ``scoreExposure`` : `lsst.afw.image.ExposureF`
979 Result of subtracting the convolved template and science
980 images. Attached PSF is that of the original science image.
981 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
982 Warped and PSF-matched template exposure. Attached PSF is that
983 of the original science image.
984 ``matchedScience`` : `lsst.afw.image.ExposureF`
985 The science exposure after convolving with its own PSF.
986 Attached PSF is that of the original science image.
987 ``backgroundModel`` : `lsst.afw.math.Function2D`
988 Background model that was fit while solving for the
990 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
991 Final kernel used to PSF-match the template to the science
994 if finalizedPsfApCorrCatalog
is not None:
996 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
997 "argument, and will be removed after v26.",
999 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
1001 visitSummary = finalizedPsfApCorrCatalog
1003 self.
_prepareInputs(template, science, visitSummary=visitSummary)
1006 scienceKernel = science.psf.getKernel()
1008 interpolateBadMaskPlanes=
True)
1009 self.metadata.add(
"convolvedExposure",
"Preconvolution")
1012 subtractResults = self.
runPreconvolve(template, science, matchedScience,
1013 selectSources, scienceKernel)
1016 self.
loglog.
warning(
"Failed to match template. Checking coverage")
1019 self.config.minTemplateFractionForExpectedSuccess,
1020 exceptionMessage=
"Template coverage lower than expected to succeed."
1021 f
" Failure is tolerable: {e}")
1025 return subtractResults
1027 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
1028 """Convolve the science image with its own PSF, then convolve the
1029 template with a matching kernel and subtract to form the Score
1034 template : `lsst.afw.image.ExposureF`
1035 Template exposure, warped to match the science exposure.
1036 science : `lsst.afw.image.ExposureF`
1037 Science exposure to subtract from the template.
1038 matchedScience : `lsst.afw.image.ExposureF`
1039 The science exposure, convolved with the reflection of its own PSF.
1040 selectSources : `lsst.afw.table.SourceCatalog`
1041 Identified sources on the science exposure. This catalog is used to
1042 select sources in order to perform the AL PSF matching on stamp
1044 preConvKernel : `lsst.afw.math.Kernel`
1045 The reflection of the kernel that was used to preconvolve the
1046 `science` exposure. Must be normalized to sum to 1.
1050 results : `lsst.pipe.base.Struct`
1052 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1053 Result of subtracting the convolved template and science
1054 images. Attached PSF is that of the original science image.
1055 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1056 Warped and PSF-matched template exposure. Attached PSF is that
1057 of the original science image.
1058 ``matchedScience`` : `lsst.afw.image.ExposureF`
1059 The science exposure after convolving with its own PSF.
1060 Attached PSF is that of the original science image.
1061 ``backgroundModel`` : `lsst.afw.math.Function2D`
1062 Background model that was fit while solving for the
1064 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1065 Final kernel used to PSF-match the template to the science
1068 bbox = science.getBBox()
1069 innerBBox = preConvKernel.shrinkBBox(bbox)
1071 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
1072 candidateList=selectSources,
1074 kernelResult = self.makeKernel.
run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1077 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
1081 interpolateBadMaskPlanes=
True,
1082 photoCalib=science.photoCalib)
1084 backgroundModel=(kernelResult.backgroundModel
1085 if self.config.doSubtractBackground
else None))
1086 correctedScore = self.
finalize(template[bbox], science, score,
1087 kernelResult.psfMatchingKernel,
1088 templateMatched=
True, preConvMode=
True,
1089 preConvKernel=preConvKernel)
1091 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1092 matchedTemplate=matchedTemplate,
1093 matchedScience=matchedScience,
1094 backgroundModel=kernelResult.backgroundModel,
1095 psfMatchingKernel=kernelResult.psfMatchingKernel)
1099 exceptionMessage=""):
1100 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1104 templateExposure : `lsst.afw.image.ExposureF`
1105 The template exposure to check
1106 logger : `lsst.log.Log`
1107 Logger for printing output.
1108 requiredTemplateFraction : `float`, optional
1109 Fraction of pixels of the science image required to have coverage
1111 exceptionMessage : `str`, optional
1112 Message to include in the exception raised if the template coverage
1117 lsst.pipe.base.NoWorkFound
1118 Raised if fraction of good pixels, defined as not having NO_DATA
1119 set, is less than the requiredTemplateFraction
1123 pixNoData = np.count_nonzero(templateExposure.mask.array
1124 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1125 pixGood = templateExposure.getBBox().getArea() - pixNoData
1126 logger.info(
"template has %d good pixels (%.1f%%)", pixGood,
1127 100*pixGood/templateExposure.getBBox().getArea())
1129 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
1130 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1131 100*pixGood/templateExposure.getBBox().getArea(),
1132 100*requiredTemplateFraction))
1133 raise lsst.pipe.base.NoWorkFound(message +
" " + exceptionMessage)
1137 """Subtract template from science, propagating relevant metadata.
1141 science : `lsst.afw.Exposure`
1142 The input science image.
1143 template : `lsst.afw.Exposure`
1144 The template to subtract from the science image.
1145 backgroundModel : `lsst.afw.MaskedImage`, optional
1146 Differential background model
1150 difference : `lsst.afw.Exposure`
1151 The subtracted image.
1153 difference = science.clone()
1154 if backgroundModel
is not None:
1155 difference.maskedImage -= backgroundModel
1156 difference.maskedImage -= template.maskedImage
1161 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1165 exp1 : `~lsst.afw.image.Exposure`
1166 Exposure with the reference point spread function (PSF) to evaluate.
1167 exp2 : `~lsst.afw.image.Exposure`
1168 Exposure with a candidate point spread function (PSF) to evaluate.
1169 fwhmExposureBuffer : `float`
1170 Fractional buffer margin to be left out of all sides of the image
1171 during the construction of the grid to compute mean PSF FWHM in an
1172 exposure, if the PSF is not available at its average position.
1173 fwhmExposureGrid : `int`
1174 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1175 available at its average position.
1179 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1183 shape1 = getPsfFwhm(exp1.psf, average=
False)
1184 shape2 = getPsfFwhm(exp2.psf, average=
False)
1186 shape1 = evaluateMeanPsfFwhm(exp1,
1187 fwhmExposureBuffer=fwhmExposureBuffer,
1188 fwhmExposureGrid=fwhmExposureGrid
1190 shape2 = evaluateMeanPsfFwhm(exp2,
1191 fwhmExposureBuffer=fwhmExposureBuffer,
1192 fwhmExposureGrid=fwhmExposureGrid
1194 return shape1 <= shape2
1197 xTest = shape1[0] <= shape2[0]
1198 yTest = shape1[1] <= shape2[1]
1199 return xTest | yTest
1203 """Replace masked image pixels with interpolated values.
1207 maskedImage : `lsst.afw.image.MaskedImage`
1208 Image on which to perform interpolation.
1209 badMaskPlanes : `list` of `str`
1210 List of mask planes to interpolate over.
1211 fallbackValue : `float`, optional
1212 Value to set when interpolation fails.
1217 The number of masked pixels that were replaced.
1219 imgBadMaskPlanes = [
1220 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in maskedImage.mask.getMaskPlaneDict()
1223 image = maskedImage.image.array
1224 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1225 image[badPixels] = np.nan
1226 if fallbackValue
is None:
1227 fallbackValue = np.nanmedian(image)
1230 image[badPixels] = fallbackValue
1231 return np.sum(badPixels)
Asseses the quality of a candidate given a spatial kernel and background model.
runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel)
run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None)
_clearMask(self, mask, clearMaskPlanes=None)
_prepareInputs(self, template, science, visitSummary=None)
runConvolveTemplate(self, template, science, selectSources)
_applyExternalCalibrations(self, exposure, visitSummary)
updateMasks(self, template, science)
_sourceSelector(self, sources, mask)
_convolveExposure(self, exposure, kernel, convolutionControl, bbox=None, psf=None, photoCalib=None, interpolateBadMaskPlanes=False)
runConvolveScience(self, template, science, selectSources)
run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None)
finalize(self, template, science, difference, kernel, templateMatched=True, preConvMode=False, preConvKernel=None, spatiallyVarying=False)
_validateExposures(template, science)
_checkMask(mask, sources, excludeMaskPlanes)
_renameMaskPlanes(mask, maskPlane, newMaskPlane)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
_subtractImages(science, template, backgroundModel=None)
_interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None)
checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0., exceptionMessage="")
_shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid)