29from lsst.utils.introspection
import find_outside_stacklevel
30from lsst.ip.diffim.utils
import evaluateMeanPsfFwhm, getPsfFwhm
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 visitSummary = connectionTypes.Input(
69 doc=(
"Per-visit catalog with final calibration objects. "
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",
79 if not config.doApplyExternalCalibrations:
84 dimensions=_dimensions,
85 defaultTemplates=_defaultTemplates):
86 difference = connectionTypes.Output(
87 doc=
"Result of subtracting convolved template from science image.",
88 dimensions=(
"instrument",
"visit",
"detector"),
89 storageClass=
"ExposureF",
90 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
92 matchedTemplate = connectionTypes.Output(
93 doc=
"Warped and PSF-matched template used to create `subtractedExposure`.",
94 dimensions=(
"instrument",
"visit",
"detector"),
95 storageClass=
"ExposureF",
96 name=
"{fakesType}{coaddName}Diff_matchedExp",
98 psfMatchingKernel = connectionTypes.Output(
99 doc=
"Kernel used to PSF match the science and template images.",
100 dimensions=(
"instrument",
"visit",
"detector"),
101 storageClass=
"MatchingKernel",
102 name=
"{fakesType}{coaddName}Diff_psfMatchKernel",
107 dimensions=_dimensions,
108 defaultTemplates=_defaultTemplates):
109 scoreExposure = connectionTypes.Output(
110 doc=
"The maximum likelihood image, used for the detection of diaSources.",
111 dimensions=(
"instrument",
"visit",
"detector"),
112 storageClass=
"ExposureF",
113 name=
"{fakesType}{coaddName}Diff_scoreExp",
115 psfMatchingKernel = connectionTypes.Output(
116 doc=
"Kernel used to PSF match the science and template images.",
117 dimensions=(
"instrument",
"visit",
"detector"),
118 storageClass=
"MatchingKernel",
119 name=
"{fakesType}{coaddName}Diff_psfScoreMatchKernel",
129 target=MakeKernelTask,
130 doc=
"Task to construct a matching kernel for convolution.",
135 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
136 "kernel convolution? If True, also update the diffim PSF."
139 target=DecorrelateALKernelTask,
140 doc=
"Task to decorrelate the image difference.",
145 doc=
"Raise NoWorkFound and do not attempt image subtraction if template covers less than this "
146 " fraction of pixels. Setting to 0 will always attempt image subtraction."
151 doc=
"Raise NoWorkFound if PSF-matching fails and template covers less than this fraction of pixels."
152 " If the fraction of pixels covered by the template is less than this value (and greater than"
153 " requiredTemplateFraction) this task is attempted but failure is anticipated and tolerated."
158 doc=
"Scale variance of the image difference?"
161 target=ScaleVarianceTask,
162 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
165 doc=
"Subtract the background fit when solving the kernel?",
171 "Replace science Exposure's calibration objects with those"
172 " in visitSummary. Ignored if `doApplyFinalizedPsf is True."
180 doc=
"Minimum signal to noise ratio of detected sources "
181 "to use for calculating the PSF matching kernel."
186 doc=
"Maximum signal to noise ratio of detected sources "
187 "to use for calculating the PSF matching kernel."
192 doc=
"Maximum number of sources to use for calculating the PSF matching kernel."
193 "Set to -1 to disable."
198 doc=
"Minimum number of sources needed for calculating the PSF matching kernel."
202 doc=
"Flags that, if set, the associated source should not "
203 "be used to determine the PSF matching kernel.",
204 default=(
"sky_source",
"slot_Centroid_flag",
205 "slot_ApFlux_flag",
"slot_PsfFlux_flag",
206 "base_PixelFlags_flag_interpolated",
207 "base_PixelFlags_flag_saturated",
208 "base_PixelFlags_flag_bad",
213 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE",
"FAKE"),
214 doc=
"Mask planes to exclude when selecting sources for PSF matching."
218 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE"),
219 doc=
"Mask planes to interpolate over."
223 default=(
"NO_DATA",
"BAD",),
224 doc=
"Mask planes from the template to propagate to the image difference."
228 default=(
"SAT",
"INJECTED",
"INJECTED_CORE",),
229 doc=
"Mask planes from the template to propagate to the image difference"
230 "with '_TEMPLATE' appended to the name."
235 doc=
"Re-run source detection for kernel candidates if an error is"
236 " encountered while calculating the matching kernel."
242 self.
makeKernel.kernel.active.spatialKernelOrder = 1
243 self.
makeKernel.kernel.active.spatialBgOrder = 2
247 pipelineConnections=AlardLuptonSubtractConnections):
250 default=
"convolveTemplate",
251 allowed={
"auto":
"Choose which image to convolve at runtime.",
252 "convolveScience":
"Only convolve the science image.",
253 "convolveTemplate":
"Only convolve the template image."},
254 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
259 """Compute the image difference of a science and template image using
260 the Alard & Lupton (1998) algorithm.
262 ConfigClass = AlardLuptonSubtractConfig
263 _DefaultName =
"alardLuptonSubtract"
267 self.makeSubtask(
"decorrelate")
268 self.makeSubtask(
"makeKernel")
269 if self.config.doScaleVariance:
270 self.makeSubtask(
"scaleVariance")
279 """Replace calibrations (psf, and ApCorrMap) on this exposure with
284 exposure : `lsst.afw.image.exposure.Exposure`
285 Input exposure to adjust calibrations.
286 visitSummary : `lsst.afw.table.ExposureCatalog`
287 Exposure catalog with external calibrations to be applied. Catalog
288 uses the detector id for the catalog id, sorted on id for fast
293 exposure : `lsst.afw.image.exposure.Exposure`
294 Exposure with adjusted calibrations.
296 detectorId = exposure.info.getDetector().getId()
298 row = visitSummary.find(detectorId)
300 self.
log.warning(
"Detector id %s not found in external calibrations catalog; "
301 "Using original calibrations.", detectorId)
304 apCorrMap = row.getApCorrMap()
306 self.
log.warning(
"Detector id %s has None for psf in "
307 "external calibrations catalog; Using original psf and aperture correction.",
309 elif apCorrMap
is None:
310 self.
log.warning(
"Detector id %s has None for apCorrMap in "
311 "external calibrations catalog; Using original psf and aperture correction.",
315 exposure.info.setApCorrMap(apCorrMap)
320 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None,
322 """PSF match, subtract, and decorrelate two images.
326 template : `lsst.afw.image.ExposureF`
327 Template exposure, warped to match the science exposure.
328 science : `lsst.afw.image.ExposureF`
329 Science exposure to subtract from the template.
330 sources : `lsst.afw.table.SourceCatalog`
331 Identified sources on the science exposure. This catalog is used to
332 select sources in order to perform the AL PSF matching on stamp
334 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
335 Exposure catalog with finalized psf models and aperture correction
336 maps to be applied. Catalog uses the detector id for the catalog
337 id, sorted on id for fast lookup. Deprecated in favor of
338 ``visitSummary``, and will be removed after v26.
339 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
340 Exposure catalog with external calibrations to be applied. Catalog
341 uses the detector id for the catalog id, sorted on id for fast
342 lookup. Ignored (for temporary backwards compatibility) if
343 ``finalizedPsfApCorrCatalog`` is provided.
347 results : `lsst.pipe.base.Struct`
348 ``difference`` : `lsst.afw.image.ExposureF`
349 Result of subtracting template and science.
350 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
351 Warped and PSF-matched template exposure.
352 ``backgroundModel`` : `lsst.afw.math.Function2D`
353 Background model that was fit while solving for the
355 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
356 Kernel used to PSF-match the convolved image.
361 If an unsupported convolution mode is supplied.
363 If there are too few sources to calculate the PSF matching kernel.
364 lsst.pipe.base.NoWorkFound
365 Raised if fraction of good pixels, defined as not having NO_DATA
366 set, is less then the configured requiredTemplateFraction
369 if finalizedPsfApCorrCatalog
is not None:
371 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
372 "argument, and will be removed after v26.",
374 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
376 visitSummary = finalizedPsfApCorrCatalog
381 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
382 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
391 templatePsfSize = getPsfFwhm(template.psf)
392 sciencePsfSize = getPsfFwhm(science.psf)
394 self.
log.info(
"Unable to evaluate PSF at the average position. "
395 "Evaluting PSF on a grid of points."
397 templatePsfSize = evaluateMeanPsfFwhm(template,
398 fwhmExposureBuffer=fwhmExposureBuffer,
399 fwhmExposureGrid=fwhmExposureGrid
401 sciencePsfSize = evaluateMeanPsfFwhm(science,
402 fwhmExposureBuffer=fwhmExposureBuffer,
403 fwhmExposureGrid=fwhmExposureGrid
405 self.
log.info(
"Science PSF FWHM: %f pixels", sciencePsfSize)
406 self.
log.info(
"Template PSF FWHM: %f pixels", templatePsfSize)
407 self.metadata.add(
"sciencePsfSize", sciencePsfSize)
408 self.metadata.add(
"templatePsfSize", templatePsfSize)
410 if self.config.mode ==
"auto":
413 fwhmExposureBuffer=fwhmExposureBuffer,
414 fwhmExposureGrid=fwhmExposureGrid)
416 if sciencePsfSize < templatePsfSize:
417 self.
log.info(
"Average template PSF size is greater, "
418 "but science PSF greater in one dimension: convolving template image.")
420 self.
log.info(
"Science PSF size is greater: convolving template image.")
422 self.
log.info(
"Template PSF size is greater: convolving science image.")
423 elif self.config.mode ==
"convolveTemplate":
424 self.
log.info(
"`convolveTemplate` is set: convolving template image.")
425 convolveTemplate =
True
426 elif self.config.mode ==
"convolveScience":
427 self.
log.info(
"`convolveScience` is set: convolving science image.")
428 convolveTemplate =
False
430 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
433 sourceMask = science.mask.clone()
434 sourceMask.array |= template[science.getBBox()].mask.array
437 self.metadata.add(
"convolvedExposure",
"Template")
440 self.metadata.add(
"convolvedExposure",
"Science")
444 self.
log.warning(
"Failed to match template. Checking coverage")
447 self.config.minTemplateFractionForExpectedSuccess,
448 exceptionMessage=
"Template coverage lower than expected to succeed."
449 f
" Failure is tolerable: {e}")
453 return subtractResults
456 """Convolve the template image with a PSF-matching kernel and subtract
457 from the science image.
461 template : `lsst.afw.image.ExposureF`
462 Template exposure, warped to match the science exposure.
463 science : `lsst.afw.image.ExposureF`
464 Science exposure to subtract from the template.
465 selectSources : `lsst.afw.table.SourceCatalog`
466 Identified sources on the science exposure. This catalog is used to
467 select sources in order to perform the AL PSF matching on stamp
472 results : `lsst.pipe.base.Struct`
474 ``difference`` : `lsst.afw.image.ExposureF`
475 Result of subtracting template and science.
476 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
477 Warped and PSF-matched template exposure.
478 ``backgroundModel`` : `lsst.afw.math.Function2D`
479 Background model that was fit while solving for the PSF-matching kernel
480 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
481 Kernel used to PSF-match the template to the science image.
484 kernelSources = self.makeKernel.selectKernelSources(template, science,
485 candidateList=selectSources,
487 kernelResult = self.makeKernel.run(template, science, kernelSources,
489 except Exception
as e:
490 if self.config.allowKernelSourceDetection:
491 self.
log.warning(
"Error encountered trying to construct the matching kernel"
492 f
" Running source detection and retrying. {e}")
493 kernelSources = self.makeKernel.selectKernelSources(template, science,
496 kernelResult = self.makeKernel.run(template, science, kernelSources,
501 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
503 bbox=science.getBBox(),
505 photoCalib=science.photoCalib)
508 backgroundModel=(kernelResult.backgroundModel
509 if self.config.doSubtractBackground
else None))
510 correctedExposure = self.
finalize(template, science, difference,
511 kernelResult.psfMatchingKernel,
512 templateMatched=
True)
514 return lsst.pipe.base.Struct(difference=correctedExposure,
515 matchedTemplate=matchedTemplate,
516 matchedScience=science,
517 backgroundModel=kernelResult.backgroundModel,
518 psfMatchingKernel=kernelResult.psfMatchingKernel)
521 """Convolve the science image with a PSF-matching kernel and subtract
526 template : `lsst.afw.image.ExposureF`
527 Template exposure, warped to match the science exposure.
528 science : `lsst.afw.image.ExposureF`
529 Science exposure to subtract from the template.
530 selectSources : `lsst.afw.table.SourceCatalog`
531 Identified sources on the science exposure. This catalog is used to
532 select sources in order to perform the AL PSF matching on stamp
537 results : `lsst.pipe.base.Struct`
539 ``difference`` : `lsst.afw.image.ExposureF`
540 Result of subtracting template and science.
541 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
542 Warped template exposure. Note that in this case, the template
543 is not PSF-matched to the science image.
544 ``backgroundModel`` : `lsst.afw.math.Function2D`
545 Background model that was fit while solving for the PSF-matching kernel
546 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
547 Kernel used to PSF-match the science image to the template.
549 bbox = science.getBBox()
550 kernelSources = self.makeKernel.selectKernelSources(science, template,
551 candidateList=selectSources,
553 kernelResult = self.makeKernel.run(science, template, kernelSources,
555 modelParams = kernelResult.backgroundModel.getParameters()
557 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
559 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
560 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
567 matchedScience.maskedImage /= norm
568 matchedTemplate = template.clone()[bbox]
569 matchedTemplate.maskedImage /= norm
570 matchedTemplate.setPhotoCalib(science.photoCalib)
573 backgroundModel=(kernelResult.backgroundModel
574 if self.config.doSubtractBackground
else None))
576 correctedExposure = self.
finalize(template, science, difference,
577 kernelResult.psfMatchingKernel,
578 templateMatched=
False)
580 return lsst.pipe.base.Struct(difference=correctedExposure,
581 matchedTemplate=matchedTemplate,
582 matchedScience=matchedScience,
583 backgroundModel=kernelResult.backgroundModel,
584 psfMatchingKernel=kernelResult.psfMatchingKernel,)
586 def finalize(self, template, science, difference, kernel,
587 templateMatched=True,
590 spatiallyVarying=False):
591 """Decorrelate the difference image to undo the noise correlations
592 caused by convolution.
596 template : `lsst.afw.image.ExposureF`
597 Template exposure, warped to match the science exposure.
598 science : `lsst.afw.image.ExposureF`
599 Science exposure to subtract from the template.
600 difference : `lsst.afw.image.ExposureF`
601 Result of subtracting template and science.
602 kernel : `lsst.afw.math.Kernel`
603 An (optionally spatially-varying) PSF matching kernel
604 templateMatched : `bool`, optional
605 Was the template PSF-matched to the science image?
606 preConvMode : `bool`, optional
607 Was the science image preconvolved with its own PSF
608 before PSF matching the template?
609 preConvKernel : `lsst.afw.detection.Psf`, optional
610 If not `None`, then the science image was pre-convolved with
611 (the reflection of) this kernel. Must be normalized to sum to 1.
612 spatiallyVarying : `bool`, optional
613 Compute the decorrelation kernel spatially varying across the image?
617 correctedExposure : `lsst.afw.image.ExposureF`
618 The decorrelated image difference.
620 if self.config.doDecorrelation:
621 self.
log.info(
"Decorrelating image difference.")
625 correctedExposure = self.decorrelate.run(science, template[science.getBBox()], difference, kernel,
626 templateMatched=templateMatched,
627 preConvMode=preConvMode,
628 preConvKernel=preConvKernel,
629 spatiallyVarying=spatiallyVarying).correctedExposure
631 self.
log.info(
"NOT decorrelating image difference.")
632 correctedExposure = difference
633 return correctedExposure
637 """Check that the WCS of the two Exposures match, and the template bbox
638 contains the science bbox.
642 template : `lsst.afw.image.ExposureF`
643 Template exposure, warped to match the science exposure.
644 science : `lsst.afw.image.ExposureF`
645 Science exposure to subtract from the template.
650 Raised if the WCS of the template is not equal to the science WCS,
651 or if the science image is not fully contained in the template
654 assert template.wcs == science.wcs,\
655 "Template and science exposure WCS are not identical."
656 templateBBox = template.getBBox()
657 scienceBBox = science.getBBox()
659 assert templateBBox.contains(scienceBBox),\
660 "Template bbox does not contain all of the science image."
666 interpolateBadMaskPlanes=False,
668 """Convolve an exposure with the given kernel.
672 exposure : `lsst.afw.Exposure`
673 exposure to convolve.
674 kernel : `lsst.afw.math.LinearCombinationKernel`
675 PSF matching kernel computed in the ``makeKernel`` subtask.
676 convolutionControl : `lsst.afw.math.ConvolutionControl`
677 Configuration for convolve algorithm.
678 bbox : `lsst.geom.Box2I`, optional
679 Bounding box to trim the convolved exposure to.
680 psf : `lsst.afw.detection.Psf`, optional
681 Point spread function (PSF) to set for the convolved exposure.
682 photoCalib : `lsst.afw.image.PhotoCalib`, optional
683 Photometric calibration of the convolved exposure.
687 convolvedExp : `lsst.afw.Exposure`
690 convolvedExposure = exposure.clone()
692 convolvedExposure.setPsf(psf)
693 if photoCalib
is not None:
694 convolvedExposure.setPhotoCalib(photoCalib)
695 if interpolateBadMaskPlanes
and self.config.badMaskPlanes
is not None:
697 self.config.badMaskPlanes)
698 self.metadata.add(
"nInterpolated", nInterp)
699 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
701 convolvedExposure.setMaskedImage(convolvedImage)
703 return convolvedExposure
705 return convolvedExposure[bbox]
708 """Select sources from a catalog that meet the selection criteria.
712 sources : `lsst.afw.table.SourceCatalog`
713 Input source catalog to select sources from.
714 mask : `lsst.afw.image.Mask`
715 The image mask plane to use to reject sources
716 based on their location on the ccd.
720 selectSources : `lsst.afw.table.SourceCatalog`
721 The input source catalog, with flagged and low signal-to-noise
727 If there are too few sources to compute the PSF matching kernel
728 remaining after source selection.
730 flags = np.ones(len(sources), dtype=bool)
731 for flag
in self.config.badSourceFlags:
733 flags *= ~sources[flag]
734 except Exception
as e:
735 self.
log.warning(
"Could not apply source flag: %s", e)
736 signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr()
737 sToNFlag = signalToNoise > self.config.detectionThreshold
739 sToNFlagMax = signalToNoise < self.config.detectionThresholdMax
741 flags *= self.
_checkMask(mask, sources, self.config.excludeMaskPlanes)
742 selectSources = sources[flags].copy(deep=
True)
743 if (len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
744 signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
745 indices = np.argsort(signalToNoise)
746 indices = indices[-self.config.maxKernelSources:]
747 flags = np.zeros(len(selectSources), dtype=bool)
748 flags[indices] =
True
749 selectSources = selectSources[flags].copy(deep=
True)
751 self.
log.info(
"%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
752 len(selectSources), len(sources), 100*len(selectSources)/len(sources))
753 if len(selectSources) < self.config.minKernelSources:
754 self.
log.error(
"Too few sources to calculate the PSF matching kernel: "
755 "%i selected but %i needed for the calculation.",
756 len(selectSources), self.config.minKernelSources)
757 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
758 self.metadata.add(
"nPsfSources", len(selectSources))
764 """Exclude sources that are located on masked pixels.
768 mask : `lsst.afw.image.Mask`
769 The image mask plane to use to reject sources
770 based on the location of their centroid on the ccd.
771 sources : `lsst.afw.table.SourceCatalog`
772 The source catalog to evaluate.
773 excludeMaskPlanes : `list` of `str`
774 List of the names of the mask planes to exclude.
778 flags : `numpy.ndarray` of `bool`
779 Array indicating whether each source in the catalog should be
780 kept (True) or rejected (False) based on the value of the
781 mask plane at its location.
783 setExcludeMaskPlanes = [
784 maskPlane
for maskPlane
in excludeMaskPlanes
if maskPlane
in mask.getMaskPlaneDict()
787 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
789 xv = np.rint(sources.getX() - mask.getX0())
790 yv = np.rint(sources.getY() - mask.getY0())
792 mv = mask.array[yv.astype(int), xv.astype(int)]
793 flags = np.bitwise_and(mv, excludePixelMask) == 0
797 """Perform preparatory calculations common to all Alard&Lupton Tasks.
801 template : `lsst.afw.image.ExposureF`
802 Template exposure, warped to match the science exposure. The
803 variance plane of the template image is modified in place.
804 science : `lsst.afw.image.ExposureF`
805 Science exposure to subtract from the template. The variance plane
806 of the science image is modified in place.
807 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
808 Exposure catalog with external calibrations to be applied. Catalog
809 uses the detector id for the catalog id, sorted on id for fast
813 if visitSummary
is not None:
816 template[science.getBBox()], self.
log,
817 requiredTemplateFraction=self.config.requiredTemplateFraction,
818 exceptionMessage=
"Not attempting subtraction. To force subtraction,"
819 " set config requiredTemplateFraction=0"
821 self.metadata.add(
"templateCoveragePercent", 100*templateCoverageFraction)
823 if self.config.doScaleVariance:
827 templateVarFactor = self.scaleVariance.run(template.maskedImage)
828 sciVarFactor = self.scaleVariance.run(science.maskedImage)
829 self.
log.info(
"Template variance scaling factor: %.2f", templateVarFactor)
830 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
831 self.
log.info(
"Science variance scaling factor: %.2f", sciVarFactor)
832 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
839 """Update the science and template mask planes before differencing.
843 template : `lsst.afw.image.Exposure`
844 Template exposure, warped to match the science exposure.
845 The template mask planes will be erased, except for a few specified
847 science : `lsst.afw.image.Exposure`
848 Science exposure to subtract from the template.
849 The DETECTED and DETECTED_NEGATIVE mask planes of the science image
852 self.
_clearMask(science.mask, clearMaskPlanes=[
"DETECTED",
"DETECTED_NEGATIVE"])
859 clearMaskPlanes = [mp
for mp
in template.mask.getMaskPlaneDict().keys()
860 if mp
not in self.config.preserveTemplateMask]
861 renameMaskPlanes = [mp
for mp
in self.config.renameTemplateMask
862 if mp
in template.mask.getMaskPlaneDict().keys()]
867 if "FAKE" in science.mask.getMaskPlaneDict().keys():
868 self.
log.info(
"Adding injected mask plane to science image")
870 if "FAKE" in template.mask.getMaskPlaneDict().keys():
871 self.
log.info(
"Adding injected mask plane to template image")
873 if "INJECTED" in renameMaskPlanes:
874 renameMaskPlanes.remove(
"INJECTED")
875 if "INJECTED_TEMPLATE" in clearMaskPlanes:
876 clearMaskPlanes.remove(
"INJECTED_TEMPLATE")
878 for maskPlane
in renameMaskPlanes:
880 self.
_clearMask(template.mask, clearMaskPlanes=clearMaskPlanes)
884 """Rename a mask plane by adding the new name and copying the data.
888 mask : `lsst.afw.image.Mask`
889 The mask image to update in place.
891 The name of the existing mask plane to copy.
893 The new name of the mask plane that will be added.
894 If the mask plane already exists, it will be updated in place.
896 mask.addMaskPlane(newMaskPlane)
897 originBitMask = mask.getPlaneBitMask(maskPlane)
898 destinationBitMask = mask.getPlaneBitMask(newMaskPlane)
899 mask.array |= ((mask.array & originBitMask) > 0)*destinationBitMask
902 """Clear the mask plane of the template.
906 mask : `lsst.afw.image.Mask`
907 The mask plane to erase, which will be modified in place.
908 clearMaskPlanes : `list` of `str`, optional
909 Erase the specified mask planes.
910 If not supplied, the entire mask will be erased.
912 if clearMaskPlanes
is None:
913 clearMaskPlanes = list(mask.getMaskPlaneDict().keys())
915 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
916 mask &= ~bitMaskToClear
920 SubtractScoreOutputConnections):
925 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
930 """Subtract a template from a science image, convolving the science image
931 before computing the kernel, and also convolving the template before
934 ConfigClass = AlardLuptonPreconvolveSubtractConfig
935 _DefaultName =
"alardLuptonPreconvolveSubtract"
937 def run(self, template, science, sources, visitSummary=None):
938 """Preconvolve the science image with its own PSF,
939 convolve the template image with a PSF-matching kernel and subtract
940 from the preconvolved science image.
944 template : `lsst.afw.image.ExposureF`
945 The template image, which has previously been warped to the science
946 image. The template bbox will be padded by a few pixels compared to
948 science : `lsst.afw.image.ExposureF`
949 The science exposure.
950 sources : `lsst.afw.table.SourceCatalog`
951 Identified sources on the science exposure. This catalog is used to
952 select sources in order to perform the AL PSF matching on stamp
954 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
955 Exposure catalog with complete external calibrations. Catalog uses
956 the detector id for the catalog id, sorted on id for fast lookup.
960 results : `lsst.pipe.base.Struct`
961 ``scoreExposure`` : `lsst.afw.image.ExposureF`
962 Result of subtracting the convolved template and science
963 images. Attached PSF is that of the original science image.
964 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
965 Warped and PSF-matched template exposure. Attached PSF is that
966 of the original science image.
967 ``matchedScience`` : `lsst.afw.image.ExposureF`
968 The science exposure after convolving with its own PSF.
969 Attached PSF is that of the original science image.
970 ``backgroundModel`` : `lsst.afw.math.Function2D`
971 Background model that was fit while solving for the
973 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
974 Final kernel used to PSF-match the template to the science
980 scienceKernel = science.psf.getKernel()
982 interpolateBadMaskPlanes=
True)
983 self.metadata.add(
"convolvedExposure",
"Preconvolution")
986 subtractResults = self.
runPreconvolve(template, science, matchedScience,
987 selectSources, scienceKernel)
990 self.
loglog.warning(
"Failed to match template. Checking coverage")
993 self.config.minTemplateFractionForExpectedSuccess,
994 exceptionMessage=
"Template coverage lower than expected to succeed."
995 f
" Failure is tolerable: {e}")
999 return subtractResults
1001 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
1002 """Convolve the science image with its own PSF, then convolve the
1003 template with a matching kernel and subtract to form the Score
1008 template : `lsst.afw.image.ExposureF`
1009 Template exposure, warped to match the science exposure.
1010 science : `lsst.afw.image.ExposureF`
1011 Science exposure to subtract from the template.
1012 matchedScience : `lsst.afw.image.ExposureF`
1013 The science exposure, convolved with the reflection of its own PSF.
1014 selectSources : `lsst.afw.table.SourceCatalog`
1015 Identified sources on the science exposure. This catalog is used to
1016 select sources in order to perform the AL PSF matching on stamp
1018 preConvKernel : `lsst.afw.math.Kernel`
1019 The reflection of the kernel that was used to preconvolve the
1020 `science` exposure. Must be normalized to sum to 1.
1024 results : `lsst.pipe.base.Struct`
1026 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1027 Result of subtracting the convolved template and science
1028 images. Attached PSF is that of the original science image.
1029 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1030 Warped and PSF-matched template exposure. Attached PSF is that
1031 of the original science image.
1032 ``matchedScience`` : `lsst.afw.image.ExposureF`
1033 The science exposure after convolving with its own PSF.
1034 Attached PSF is that of the original science image.
1035 ``backgroundModel`` : `lsst.afw.math.Function2D`
1036 Background model that was fit while solving for the
1038 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1039 Final kernel used to PSF-match the template to the science
1042 bbox = science.getBBox()
1043 innerBBox = preConvKernel.shrinkBBox(bbox)
1045 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
1046 candidateList=selectSources,
1048 kernelResult = self.makeKernel.run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1051 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
1055 interpolateBadMaskPlanes=
True,
1056 photoCalib=science.photoCalib)
1058 backgroundModel=(kernelResult.backgroundModel
1059 if self.config.doSubtractBackground
else None))
1060 correctedScore = self.
finalize(template[bbox], science, score,
1061 kernelResult.psfMatchingKernel,
1062 templateMatched=
True, preConvMode=
True,
1063 preConvKernel=preConvKernel)
1065 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1066 matchedTemplate=matchedTemplate,
1067 matchedScience=matchedScience,
1068 backgroundModel=kernelResult.backgroundModel,
1069 psfMatchingKernel=kernelResult.psfMatchingKernel)
1073 exceptionMessage=""):
1074 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1078 templateExposure : `lsst.afw.image.ExposureF`
1079 The template exposure to check
1080 logger : `lsst.log.Log`
1081 Logger for printing output.
1082 requiredTemplateFraction : `float`, optional
1083 Fraction of pixels of the science image required to have coverage
1085 exceptionMessage : `str`, optional
1086 Message to include in the exception raised if the template coverage
1091 templateCoverageFraction: `float`
1092 Fraction of pixels in the template with data.
1096 lsst.pipe.base.NoWorkFound
1097 Raised if fraction of good pixels, defined as not having NO_DATA
1098 set, is less than the requiredTemplateFraction
1102 pixNoData = np.count_nonzero(templateExposure.mask.array
1103 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1104 pixGood = templateExposure.getBBox().getArea() - pixNoData
1105 templateCoverageFraction = pixGood/templateExposure.getBBox().getArea()
1106 logger.info(
"template has %d good pixels (%.1f%%)", pixGood, 100*templateCoverageFraction)
1108 if templateCoverageFraction < requiredTemplateFraction:
1109 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1110 100*templateCoverageFraction,
1111 100*requiredTemplateFraction))
1112 raise lsst.pipe.base.NoWorkFound(message +
" " + exceptionMessage)
1113 return templateCoverageFraction
1117 """Subtract template from science, propagating relevant metadata.
1121 science : `lsst.afw.Exposure`
1122 The input science image.
1123 template : `lsst.afw.Exposure`
1124 The template to subtract from the science image.
1125 backgroundModel : `lsst.afw.MaskedImage`, optional
1126 Differential background model
1130 difference : `lsst.afw.Exposure`
1131 The subtracted image.
1133 difference = science.clone()
1134 if backgroundModel
is not None:
1135 difference.maskedImage -= backgroundModel
1136 difference.maskedImage -= template.maskedImage
1141 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1145 exp1 : `~lsst.afw.image.Exposure`
1146 Exposure with the reference point spread function (PSF) to evaluate.
1147 exp2 : `~lsst.afw.image.Exposure`
1148 Exposure with a candidate point spread function (PSF) to evaluate.
1149 fwhmExposureBuffer : `float`
1150 Fractional buffer margin to be left out of all sides of the image
1151 during the construction of the grid to compute mean PSF FWHM in an
1152 exposure, if the PSF is not available at its average position.
1153 fwhmExposureGrid : `int`
1154 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1155 available at its average position.
1159 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1163 shape1 = getPsfFwhm(exp1.psf, average=
False)
1164 shape2 = getPsfFwhm(exp2.psf, average=
False)
1166 shape1 = evaluateMeanPsfFwhm(exp1,
1167 fwhmExposureBuffer=fwhmExposureBuffer,
1168 fwhmExposureGrid=fwhmExposureGrid
1170 shape2 = evaluateMeanPsfFwhm(exp2,
1171 fwhmExposureBuffer=fwhmExposureBuffer,
1172 fwhmExposureGrid=fwhmExposureGrid
1174 return shape1 <= shape2
1177 xTest = shape1[0] <= shape2[0]
1178 yTest = shape1[1] <= shape2[1]
1179 return xTest | yTest
1183 """Replace masked image pixels with interpolated values.
1187 maskedImage : `lsst.afw.image.MaskedImage`
1188 Image on which to perform interpolation.
1189 badMaskPlanes : `list` of `str`
1190 List of mask planes to interpolate over.
1191 fallbackValue : `float`, optional
1192 Value to set when interpolation fails.
1197 The number of masked pixels that were replaced.
1199 imgBadMaskPlanes = [
1200 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in maskedImage.mask.getMaskPlaneDict()
1203 image = maskedImage.image.array
1204 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1205 image[badPixels] = np.nan
1206 if fallbackValue
is None:
1207 fallbackValue = np.nanmedian(image)
1210 image[badPixels] = fallbackValue
1211 return np.sum(badPixels)
Parameters to control convolution.
runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel)
run(self, template, science, sources, 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)
Provides consistent interface for LSST exceptions.
Reports invalid arguments.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
_subtractImages(science, template, backgroundModel=None)
_interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None)
checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0., exceptionMessage="")
_shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid)