29from lsst.utils.introspection
import find_outside_stacklevel
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 badSourceFlags = lsst.pex.config.ListField(
198 doc=
"Flags that, if set, the associated source should not "
199 "be used to determine the PSF matching kernel.",
200 default=(
"sky_source",
"slot_Centroid_flag",
201 "slot_ApFlux_flag",
"slot_PsfFlux_flag", ),
203 excludeMaskPlanes = lsst.pex.config.ListField(
205 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE",
"FAKE"),
206 doc=
"Mask planes to exclude when selecting sources for PSF matching."
208 badMaskPlanes = lsst.pex.config.ListField(
210 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE"),
211 doc=
"Mask planes to interpolate over."
213 preserveTemplateMask = lsst.pex.config.ListField(
215 default=(
"NO_DATA",
"BAD",
"SAT",
"FAKE",
"INJECTED",
"INJECTED_CORE"),
216 doc=
"Mask planes from the template to propagate to the image difference."
218 allowKernelSourceDetection = lsst.pex.config.Field(
221 doc=
"Re-run source detection for kernel candidates if an error is"
222 " encountered while calculating the matching kernel."
228 self.
makeKernel.kernel.active.spatialKernelOrder = 1
229 self.
makeKernel.kernel.active.spatialBgOrder = 2
233 pipelineConnections=AlardLuptonSubtractConnections):
234 mode = lsst.pex.config.ChoiceField(
236 default=
"convolveTemplate",
237 allowed={
"auto":
"Choose which image to convolve at runtime.",
238 "convolveScience":
"Only convolve the science image.",
239 "convolveTemplate":
"Only convolve the template image."},
240 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
245 """Compute the image difference of a science and template image using
246 the Alard & Lupton (1998) algorithm.
248 ConfigClass = AlardLuptonSubtractConfig
249 _DefaultName =
"alardLuptonSubtract"
253 self.makeSubtask(
"decorrelate")
254 self.makeSubtask(
"makeKernel")
255 if self.config.doScaleVariance:
256 self.makeSubtask(
"scaleVariance")
265 """Replace calibrations (psf, and ApCorrMap) on this exposure with
270 exposure : `lsst.afw.image.exposure.Exposure`
271 Input exposure to adjust calibrations.
272 visitSummary : `lsst.afw.table.ExposureCatalog`
273 Exposure catalog with external calibrations to be applied. Catalog
274 uses the detector id for the catalog id, sorted on id for fast
279 exposure : `lsst.afw.image.exposure.Exposure`
280 Exposure with adjusted calibrations.
282 detectorId = exposure.info.getDetector().getId()
284 row = visitSummary.find(detectorId)
286 self.
log.
warning(
"Detector id %s not found in external calibrations catalog; "
287 "Using original calibrations.", detectorId)
290 apCorrMap = row.getApCorrMap()
292 self.
log.
warning(
"Detector id %s has None for psf in "
293 "external calibrations catalog; Using original psf and aperture correction.",
295 elif apCorrMap
is None:
296 self.
log.
warning(
"Detector id %s has None for apCorrMap in "
297 "external calibrations catalog; Using original psf and aperture correction.",
301 exposure.info.setApCorrMap(apCorrMap)
306 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None,
308 """PSF match, subtract, and decorrelate two images.
312 template : `lsst.afw.image.ExposureF`
313 Template exposure, warped to match the science exposure.
314 science : `lsst.afw.image.ExposureF`
315 Science exposure to subtract from the template.
316 sources : `lsst.afw.table.SourceCatalog`
317 Identified sources on the science exposure. This catalog is used to
318 select sources in order to perform the AL PSF matching on stamp
320 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
321 Exposure catalog with finalized psf models and aperture correction
322 maps to be applied. Catalog uses the detector id for the catalog
323 id, sorted on id for fast lookup. Deprecated in favor of
324 ``visitSummary``, and will be removed after v26.
325 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
326 Exposure catalog with external calibrations to be applied. Catalog
327 uses the detector id for the catalog id, sorted on id for fast
328 lookup. Ignored (for temporary backwards compatibility) if
329 ``finalizedPsfApCorrCatalog`` is provided.
333 results : `lsst.pipe.base.Struct`
334 ``difference`` : `lsst.afw.image.ExposureF`
335 Result of subtracting template and science.
336 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
337 Warped and PSF-matched template exposure.
338 ``backgroundModel`` : `lsst.afw.math.Function2D`
339 Background model that was fit while solving for the
341 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
342 Kernel used to PSF-match the convolved image.
347 If an unsupported convolution mode is supplied.
349 If there are too few sources to calculate the PSF matching kernel.
350 lsst.pipe.base.NoWorkFound
351 Raised if fraction of good pixels, defined as not having NO_DATA
352 set, is less then the configured requiredTemplateFraction
355 if finalizedPsfApCorrCatalog
is not None:
357 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
358 "argument, and will be removed after v26.",
360 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
362 visitSummary = finalizedPsfApCorrCatalog
367 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
368 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
377 templatePsfSize = getPsfFwhm(template.psf)
378 sciencePsfSize = getPsfFwhm(science.psf)
380 self.
log.
info(
"Unable to evaluate PSF at the average position. "
381 "Evaluting PSF on a grid of points."
383 templatePsfSize = evaluateMeanPsfFwhm(template,
384 fwhmExposureBuffer=fwhmExposureBuffer,
385 fwhmExposureGrid=fwhmExposureGrid
387 sciencePsfSize = evaluateMeanPsfFwhm(science,
388 fwhmExposureBuffer=fwhmExposureBuffer,
389 fwhmExposureGrid=fwhmExposureGrid
391 self.
log.
info(
"Science PSF FWHM: %f pixels", sciencePsfSize)
392 self.
log.
info(
"Template PSF FWHM: %f pixels", templatePsfSize)
393 self.metadata.add(
"sciencePsfSize", sciencePsfSize)
394 self.metadata.add(
"templatePsfSize", templatePsfSize)
396 if self.config.mode ==
"auto":
399 fwhmExposureBuffer=fwhmExposureBuffer,
400 fwhmExposureGrid=fwhmExposureGrid)
402 if sciencePsfSize < templatePsfSize:
403 self.
log.
info(
"Average template PSF size is greater, "
404 "but science PSF greater in one dimension: convolving template image.")
406 self.
log.
info(
"Science PSF size is greater: convolving template image.")
408 self.
log.
info(
"Template PSF size is greater: convolving science image.")
409 elif self.config.mode ==
"convolveTemplate":
410 self.
log.
info(
"`convolveTemplate` is set: convolving template image.")
411 convolveTemplate =
True
412 elif self.config.mode ==
"convolveScience":
413 self.
log.
info(
"`convolveScience` is set: convolving science image.")
414 convolveTemplate =
False
416 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
421 self.metadata.add(
"convolvedExposure",
"Template")
424 self.metadata.add(
"convolvedExposure",
"Science")
428 self.
log.
warning(
"Failed to match template. Checking coverage")
431 self.config.minTemplateFractionForExpectedSuccess,
432 exceptionMessage=
"Template coverage lower than expected to succeed."
433 f
" Failure is tolerable: {e}")
437 return subtractResults
440 """Convolve the template image with a PSF-matching kernel and subtract
441 from the science image.
445 template : `lsst.afw.image.ExposureF`
446 Template exposure, warped to match the science exposure.
447 science : `lsst.afw.image.ExposureF`
448 Science exposure to subtract from the template.
449 selectSources : `lsst.afw.table.SourceCatalog`
450 Identified sources on the science exposure. This catalog is used to
451 select sources in order to perform the AL PSF matching on stamp
456 results : `lsst.pipe.base.Struct`
458 ``difference`` : `lsst.afw.image.ExposureF`
459 Result of subtracting template and science.
460 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
461 Warped and PSF-matched template exposure.
462 ``backgroundModel`` : `lsst.afw.math.Function2D`
463 Background model that was fit while solving for the PSF-matching kernel
464 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
465 Kernel used to PSF-match the template to the science image.
468 kernelSources = self.makeKernel.selectKernelSources(template, science,
469 candidateList=selectSources,
471 kernelResult = self.makeKernel.
run(template, science, kernelSources,
473 except Exception
as e:
474 if self.config.allowKernelSourceDetection:
475 self.
log.
warning(
"Error encountered trying to construct the matching kernel"
476 f
" Running source detection and retrying. {e}")
477 kernelSources = self.makeKernel.selectKernelSources(template, science,
480 kernelResult = self.makeKernel.
run(template, science, kernelSources,
485 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
487 bbox=science.getBBox(),
489 photoCalib=science.photoCalib)
492 backgroundModel=(kernelResult.backgroundModel
493 if self.config.doSubtractBackground
else None))
494 correctedExposure = self.
finalize(template, science, difference,
495 kernelResult.psfMatchingKernel,
496 templateMatched=
True)
498 return lsst.pipe.base.Struct(difference=correctedExposure,
499 matchedTemplate=matchedTemplate,
500 matchedScience=science,
501 backgroundModel=kernelResult.backgroundModel,
502 psfMatchingKernel=kernelResult.psfMatchingKernel)
505 """Convolve the science image with a PSF-matching kernel and subtract
510 template : `lsst.afw.image.ExposureF`
511 Template exposure, warped to match the science exposure.
512 science : `lsst.afw.image.ExposureF`
513 Science exposure to subtract from the template.
514 selectSources : `lsst.afw.table.SourceCatalog`
515 Identified sources on the science exposure. This catalog is used to
516 select sources in order to perform the AL PSF matching on stamp
521 results : `lsst.pipe.base.Struct`
523 ``difference`` : `lsst.afw.image.ExposureF`
524 Result of subtracting template and science.
525 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
526 Warped template exposure. Note that in this case, the template
527 is not PSF-matched to the science image.
528 ``backgroundModel`` : `lsst.afw.math.Function2D`
529 Background model that was fit while solving for the PSF-matching kernel
530 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
531 Kernel used to PSF-match the science image to the template.
533 bbox = science.getBBox()
534 kernelSources = self.makeKernel.selectKernelSources(science, template,
535 candidateList=selectSources,
537 kernelResult = self.makeKernel.
run(science, template, kernelSources,
539 modelParams = kernelResult.backgroundModel.getParameters()
541 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
543 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
544 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
551 matchedScience.maskedImage /= norm
552 matchedTemplate = template.clone()[bbox]
553 matchedTemplate.maskedImage /= norm
554 matchedTemplate.setPhotoCalib(science.photoCalib)
557 backgroundModel=(kernelResult.backgroundModel
558 if self.config.doSubtractBackground
else None))
560 correctedExposure = self.
finalize(template, science, difference,
561 kernelResult.psfMatchingKernel,
562 templateMatched=
False)
564 return lsst.pipe.base.Struct(difference=correctedExposure,
565 matchedTemplate=matchedTemplate,
566 matchedScience=matchedScience,
567 backgroundModel=kernelResult.backgroundModel,
568 psfMatchingKernel=kernelResult.psfMatchingKernel,)
570 def finalize(self, template, science, difference, kernel,
571 templateMatched=True,
574 spatiallyVarying=False):
575 """Decorrelate the difference image to undo the noise correlations
576 caused by convolution.
580 template : `lsst.afw.image.ExposureF`
581 Template exposure, warped to match the science exposure.
582 science : `lsst.afw.image.ExposureF`
583 Science exposure to subtract from the template.
584 difference : `lsst.afw.image.ExposureF`
585 Result of subtracting template and science.
586 kernel : `lsst.afw.math.Kernel`
587 An (optionally spatially-varying) PSF matching kernel
588 templateMatched : `bool`, optional
589 Was the template PSF-matched to the science image?
590 preConvMode : `bool`, optional
591 Was the science image preconvolved with its own PSF
592 before PSF matching the template?
593 preConvKernel : `lsst.afw.detection.Psf`, optional
594 If not `None`, then the science image was pre-convolved with
595 (the reflection of) this kernel. Must be normalized to sum to 1.
596 spatiallyVarying : `bool`, optional
597 Compute the decorrelation kernel spatially varying across the image?
601 correctedExposure : `lsst.afw.image.ExposureF`
602 The decorrelated image difference.
609 if self.config.doDecorrelation:
610 self.
log.
info(
"Decorrelating image difference.")
614 correctedExposure = self.decorrelate.
run(science, template[science.getBBox()], difference, kernel,
615 templateMatched=templateMatched,
616 preConvMode=preConvMode,
617 preConvKernel=preConvKernel,
618 spatiallyVarying=spatiallyVarying).correctedExposure
620 self.
log.
info(
"NOT decorrelating image difference.")
621 correctedExposure = difference
622 return correctedExposure
625 """Update the mask planes on images for finalizing."""
627 bbox = science.getBBox()
628 mask = difference.mask
629 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
631 if "FAKE" in science.mask.getMaskPlaneDict().keys():
636 self.
log.
info(
"Adding injected mask planes")
637 mask.addMaskPlane(
"INJECTED")
638 diffInjectedBitMask = mask.getPlaneBitMask(
"INJECTED")
640 mask.addMaskPlane(
"INJECTED_TEMPLATE")
641 diffInjTmpltBitMask = mask.getPlaneBitMask(
"INJECTED_TEMPLATE")
643 scienceFakeBitMask = science.mask.getPlaneBitMask(
'FAKE')
644 tmpltFakeBitMask = template[bbox].mask.getPlaneBitMask(
'FAKE')
646 injScienceMaskArray = ((science.mask.array & scienceFakeBitMask) > 0) * diffInjectedBitMask
647 injTemplateMaskArray = ((template[bbox].mask.array & tmpltFakeBitMask) > 0) * diffInjTmpltBitMask
649 mask.array |= injScienceMaskArray
650 mask.array |= injTemplateMaskArray
652 template[bbox].mask.array[...] = difference.mask.array[...]
656 """Check that the WCS of the two Exposures match, and the template bbox
657 contains the science bbox.
661 template : `lsst.afw.image.ExposureF`
662 Template exposure, warped to match the science exposure.
663 science : `lsst.afw.image.ExposureF`
664 Science exposure to subtract from the template.
669 Raised if the WCS of the template is not equal to the science WCS,
670 or if the science image is not fully contained in the template
673 assert template.wcs == science.wcs,\
674 "Template and science exposure WCS are not identical."
675 templateBBox = template.getBBox()
676 scienceBBox = science.getBBox()
678 assert templateBBox.contains(scienceBBox),\
679 "Template bbox does not contain all of the science image."
685 interpolateBadMaskPlanes=False,
687 """Convolve an exposure with the given kernel.
691 exposure : `lsst.afw.Exposure`
692 exposure to convolve.
693 kernel : `lsst.afw.math.LinearCombinationKernel`
694 PSF matching kernel computed in the ``makeKernel`` subtask.
695 convolutionControl : `lsst.afw.math.ConvolutionControl`
696 Configuration for convolve algorithm.
697 bbox : `lsst.geom.Box2I`, optional
698 Bounding box to trim the convolved exposure to.
699 psf : `lsst.afw.detection.Psf`, optional
700 Point spread function (PSF) to set for the convolved exposure.
701 photoCalib : `lsst.afw.image.PhotoCalib`, optional
702 Photometric calibration of the convolved exposure.
706 convolvedExp : `lsst.afw.Exposure`
709 convolvedExposure = exposure.clone()
711 convolvedExposure.setPsf(psf)
712 if photoCalib
is not None:
713 convolvedExposure.setPhotoCalib(photoCalib)
714 if interpolateBadMaskPlanes
and self.config.badMaskPlanes
is not None:
716 self.config.badMaskPlanes)
717 self.metadata.add(
"nInterpolated", nInterp)
718 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
720 convolvedExposure.setMaskedImage(convolvedImage)
722 return convolvedExposure
724 return convolvedExposure[bbox]
727 """Select sources from a catalog that meet the selection criteria.
731 sources : `lsst.afw.table.SourceCatalog`
732 Input source catalog to select sources from.
733 mask : `lsst.afw.image.Mask`
734 The image mask plane to use to reject sources
735 based on their location on the ccd.
739 selectSources : `lsst.afw.table.SourceCatalog`
740 The input source catalog, with flagged and low signal-to-noise
746 If there are too few sources to compute the PSF matching kernel
747 remaining after source selection.
749 flags = np.ones(
len(sources), dtype=bool)
750 for flag
in self.config.badSourceFlags:
752 flags *= ~sources[flag]
753 except Exception
as e:
754 self.
log.
warning(
"Could not apply source flag: %s", e)
755 sToNFlag = (sources.getPsfInstFlux()/sources.getPsfInstFluxErr()) > self.config.detectionThreshold
757 flags *= self.
_checkMask(mask, sources, self.config.excludeMaskPlanes)
758 selectSources = sources[flags]
759 self.
log.
info(
"%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
760 len(selectSources),
len(sources), 100*
len(selectSources)/
len(sources))
761 if len(selectSources) < self.config.makeKernel.nStarPerCell:
762 self.
log.error(
"Too few sources to calculate the PSF matching kernel: "
763 "%i selected but %i needed for the calculation.",
764 len(selectSources), self.config.makeKernel.nStarPerCell)
765 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
766 self.metadata.add(
"nPsfSources",
len(selectSources))
768 return selectSources.copy(deep=
True)
772 """Exclude sources that are located on masked pixels.
776 mask : `lsst.afw.image.Mask`
777 The image mask plane to use to reject sources
778 based on the location of their centroid on the ccd.
779 sources : `lsst.afw.table.SourceCatalog`
780 The source catalog to evaluate.
781 excludeMaskPlanes : `list` of `str`
782 List of the names of the mask planes to exclude.
786 flags : `numpy.ndarray` of `bool`
787 Array indicating whether each source in the catalog should be
788 kept (True) or rejected (False) based on the value of the
789 mask plane at its location.
791 setExcludeMaskPlanes = [
792 maskPlane
for maskPlane
in excludeMaskPlanes
if maskPlane
in mask.getMaskPlaneDict()
795 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
797 xv = np.rint(sources.getX() - mask.getX0())
798 yv = np.rint(sources.getY() - mask.getY0())
800 mv = mask.array[yv.astype(int), xv.astype(int)]
801 flags = np.bitwise_and(mv, excludePixelMask) == 0
805 """Perform preparatory calculations common to all Alard&Lupton Tasks.
809 template : `lsst.afw.image.ExposureF`
810 Template exposure, warped to match the science exposure. The
811 variance plane of the template image is modified in place.
812 science : `lsst.afw.image.ExposureF`
813 Science exposure to subtract from the template. The variance plane
814 of the science image is modified in place.
815 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
816 Exposure catalog with external calibrations to be applied. Catalog
817 uses the detector id for the catalog id, sorted on id for fast
821 if visitSummary
is not None:
824 requiredTemplateFraction=self.config.requiredTemplateFraction,
825 exceptionMessage=
"Not attempting subtraction. To force subtraction,"
826 " set config requiredTemplateFraction=0")
828 if self.config.doScaleVariance:
832 templateVarFactor = self.scaleVariance.
run(template.maskedImage)
833 sciVarFactor = self.scaleVariance.
run(science.maskedImage)
834 self.
log.
info(
"Template variance scaling factor: %.2f", templateVarFactor)
835 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
836 self.
log.
info(
"Science variance scaling factor: %.2f", sciVarFactor)
837 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
841 """Clear the mask plane of the template.
845 template : `lsst.afw.image.ExposureF`
846 Template exposure, warped to match the science exposure.
847 The mask plane will be modified in place.
850 clearMaskPlanes = [maskplane
for maskplane
in mask.getMaskPlaneDict().keys()
851 if maskplane
not in self.config.preserveTemplateMask]
853 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
854 mask &= ~bitMaskToClear
858 SubtractScoreOutputConnections):
863 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
868 """Subtract a template from a science image, convolving the science image
869 before computing the kernel, and also convolving the template before
872 ConfigClass = AlardLuptonPreconvolveSubtractConfig
873 _DefaultName =
"alardLuptonPreconvolveSubtract"
875 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None):
876 """Preconvolve the science image with its own PSF,
877 convolve the template image with a PSF-matching kernel and subtract
878 from the preconvolved science image.
882 template : `lsst.afw.image.ExposureF`
883 The template image, which has previously been warped to the science
884 image. The template bbox will be padded by a few pixels compared to
886 science : `lsst.afw.image.ExposureF`
887 The science exposure.
888 sources : `lsst.afw.table.SourceCatalog`
889 Identified sources on the science exposure. This catalog is used to
890 select sources in order to perform the AL PSF matching on stamp
892 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
893 Exposure catalog with finalized psf models and aperture correction
894 maps to be applied. Catalog uses the detector id for the catalog
895 id, sorted on id for fast lookup. Deprecated in favor of
896 ``visitSummary``, and will be removed after v26.
897 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
898 Exposure catalog with complete external calibrations. Catalog uses
899 the detector id for the catalog id, sorted on id for fast lookup.
900 Ignored (for temporary backwards compatibility) if
901 ``finalizedPsfApCorrCatalog`` is provided.
905 results : `lsst.pipe.base.Struct`
906 ``scoreExposure`` : `lsst.afw.image.ExposureF`
907 Result of subtracting the convolved template and science
908 images. Attached PSF is that of the original science image.
909 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
910 Warped and PSF-matched template exposure. Attached PSF is that
911 of the original science image.
912 ``matchedScience`` : `lsst.afw.image.ExposureF`
913 The science exposure after convolving with its own PSF.
914 Attached PSF is that of the original science image.
915 ``backgroundModel`` : `lsst.afw.math.Function2D`
916 Background model that was fit while solving for the
918 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
919 Final kernel used to PSF-match the template to the science
922 if finalizedPsfApCorrCatalog
is not None:
924 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
925 "argument, and will be removed after v26.",
927 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
929 visitSummary = finalizedPsfApCorrCatalog
934 scienceKernel = science.psf.getKernel()
936 interpolateBadMaskPlanes=
True)
937 self.metadata.add(
"convolvedExposure",
"Preconvolution")
940 subtractResults = self.
runPreconvolve(template, science, matchedScience,
941 selectSources, scienceKernel)
944 self.
loglog.
warning(
"Failed to match template. Checking coverage")
947 self.config.minTemplateFractionForExpectedSuccess,
948 exceptionMessage=
"Template coverage lower than expected to succeed."
949 f
" Failure is tolerable: {e}")
953 return subtractResults
955 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
956 """Convolve the science image with its own PSF, then convolve the
957 template with a matching kernel and subtract to form the Score
962 template : `lsst.afw.image.ExposureF`
963 Template exposure, warped to match the science exposure.
964 science : `lsst.afw.image.ExposureF`
965 Science exposure to subtract from the template.
966 matchedScience : `lsst.afw.image.ExposureF`
967 The science exposure, convolved with the reflection of its own PSF.
968 selectSources : `lsst.afw.table.SourceCatalog`
969 Identified sources on the science exposure. This catalog is used to
970 select sources in order to perform the AL PSF matching on stamp
972 preConvKernel : `lsst.afw.math.Kernel`
973 The reflection of the kernel that was used to preconvolve the
974 `science` exposure. Must be normalized to sum to 1.
978 results : `lsst.pipe.base.Struct`
980 ``scoreExposure`` : `lsst.afw.image.ExposureF`
981 Result of subtracting the convolved template and science
982 images. Attached PSF is that of the original science image.
983 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
984 Warped and PSF-matched template exposure. Attached PSF is that
985 of the original science image.
986 ``matchedScience`` : `lsst.afw.image.ExposureF`
987 The science exposure after convolving with its own PSF.
988 Attached PSF is that of the original science image.
989 ``backgroundModel`` : `lsst.afw.math.Function2D`
990 Background model that was fit while solving for the
992 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
993 Final kernel used to PSF-match the template to the science
996 bbox = science.getBBox()
997 innerBBox = preConvKernel.shrinkBBox(bbox)
999 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
1000 candidateList=selectSources,
1002 kernelResult = self.makeKernel.
run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1005 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
1009 interpolateBadMaskPlanes=
True,
1010 photoCalib=science.photoCalib)
1012 backgroundModel=(kernelResult.backgroundModel
1013 if self.config.doSubtractBackground
else None))
1014 correctedScore = self.
finalize(template[bbox], science, score,
1015 kernelResult.psfMatchingKernel,
1016 templateMatched=
True, preConvMode=
True,
1017 preConvKernel=preConvKernel)
1019 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1020 matchedTemplate=matchedTemplate,
1021 matchedScience=matchedScience,
1022 backgroundModel=kernelResult.backgroundModel,
1023 psfMatchingKernel=kernelResult.psfMatchingKernel)
1027 exceptionMessage=""):
1028 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1032 templateExposure : `lsst.afw.image.ExposureF`
1033 The template exposure to check
1034 logger : `lsst.log.Log`
1035 Logger for printing output.
1036 requiredTemplateFraction : `float`, optional
1037 Fraction of pixels of the science image required to have coverage
1039 exceptionMessage : `str`, optional
1040 Message to include in the exception raised if the template coverage
1045 lsst.pipe.base.NoWorkFound
1046 Raised if fraction of good pixels, defined as not having NO_DATA
1047 set, is less than the requiredTemplateFraction
1051 pixNoData = np.count_nonzero(templateExposure.mask.array
1052 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1053 pixGood = templateExposure.getBBox().getArea() - pixNoData
1054 logger.info(
"template has %d good pixels (%.1f%%)", pixGood,
1055 100*pixGood/templateExposure.getBBox().getArea())
1057 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
1058 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1059 100*pixGood/templateExposure.getBBox().getArea(),
1060 100*requiredTemplateFraction))
1061 raise lsst.pipe.base.NoWorkFound(message +
" " + exceptionMessage)
1065 """Subtract template from science, propagating relevant metadata.
1069 science : `lsst.afw.Exposure`
1070 The input science image.
1071 template : `lsst.afw.Exposure`
1072 The template to subtract from the science image.
1073 backgroundModel : `lsst.afw.MaskedImage`, optional
1074 Differential background model
1078 difference : `lsst.afw.Exposure`
1079 The subtracted image.
1081 difference = science.clone()
1082 if backgroundModel
is not None:
1083 difference.maskedImage -= backgroundModel
1084 difference.maskedImage -= template.maskedImage
1089 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1093 exp1 : `~lsst.afw.image.Exposure`
1094 Exposure with the reference point spread function (PSF) to evaluate.
1095 exp2 : `~lsst.afw.image.Exposure`
1096 Exposure with a candidate point spread function (PSF) to evaluate.
1097 fwhmExposureBuffer : `float`
1098 Fractional buffer margin to be left out of all sides of the image
1099 during the construction of the grid to compute mean PSF FWHM in an
1100 exposure, if the PSF is not available at its average position.
1101 fwhmExposureGrid : `int`
1102 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1103 available at its average position.
1107 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1111 shape1 = getPsfFwhm(exp1.psf, average=
False)
1112 shape2 = getPsfFwhm(exp2.psf, average=
False)
1114 shape1 = evaluateMeanPsfFwhm(exp1,
1115 fwhmExposureBuffer=fwhmExposureBuffer,
1116 fwhmExposureGrid=fwhmExposureGrid
1118 shape2 = evaluateMeanPsfFwhm(exp2,
1119 fwhmExposureBuffer=fwhmExposureBuffer,
1120 fwhmExposureGrid=fwhmExposureGrid
1122 return shape1 <= shape2
1125 xTest = shape1[0] <= shape2[0]
1126 yTest = shape1[1] <= shape2[1]
1127 return xTest | yTest
1131 """Replace masked image pixels with interpolated values.
1135 maskedImage : `lsst.afw.image.MaskedImage`
1136 Image on which to perform interpolation.
1137 badMaskPlanes : `list` of `str`
1138 List of mask planes to interpolate over.
1139 fallbackValue : `float`, optional
1140 Value to set when interpolation fails.
1145 The number of masked pixels that were replaced.
1147 imgBadMaskPlanes = [
1148 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in maskedImage.mask.getMaskPlaneDict()
1151 image = maskedImage.image.array
1152 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1153 image[badPixels] = np.nan
1154 if fallbackValue
is None:
1155 fallbackValue = np.nanmedian(image)
1158 image[badPixels] = fallbackValue
1159 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)
_prepareInputs(self, template, science, visitSummary=None)
runConvolveTemplate(self, template, science, selectSources)
_applyExternalCalibrations(self, exposure, visitSummary)
_clearMask(self, template)
_sourceSelector(self, sources, mask)
_convolveExposure(self, exposure, kernel, convolutionControl, bbox=None, psf=None, photoCalib=None, interpolateBadMaskPlanes=False)
runConvolveScience(self, template, science, selectSources)
updateMasks(self, template, science, difference)
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)
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)