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",
"SAT",
"FAKE",
"INJECTED",
"INJECTED_CORE"),
237 doc=
"Mask planes from the template to propagate to the image difference."
239 allowKernelSourceDetection = lsst.pex.config.Field(
242 doc=
"Re-run source detection for kernel candidates if an error is"
243 " encountered while calculating the matching kernel."
249 self.
makeKernel.kernel.active.spatialKernelOrder = 1
250 self.
makeKernel.kernel.active.spatialBgOrder = 2
254 pipelineConnections=AlardLuptonSubtractConnections):
255 mode = lsst.pex.config.ChoiceField(
257 default=
"convolveTemplate",
258 allowed={
"auto":
"Choose which image to convolve at runtime.",
259 "convolveScience":
"Only convolve the science image.",
260 "convolveTemplate":
"Only convolve the template image."},
261 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
266 """Compute the image difference of a science and template image using
267 the Alard & Lupton (1998) algorithm.
269 ConfigClass = AlardLuptonSubtractConfig
270 _DefaultName =
"alardLuptonSubtract"
274 self.makeSubtask(
"decorrelate")
275 self.makeSubtask(
"makeKernel")
276 if self.config.doScaleVariance:
277 self.makeSubtask(
"scaleVariance")
286 """Replace calibrations (psf, and ApCorrMap) on this exposure with
291 exposure : `lsst.afw.image.exposure.Exposure`
292 Input exposure to adjust calibrations.
293 visitSummary : `lsst.afw.table.ExposureCatalog`
294 Exposure catalog with external calibrations to be applied. Catalog
295 uses the detector id for the catalog id, sorted on id for fast
300 exposure : `lsst.afw.image.exposure.Exposure`
301 Exposure with adjusted calibrations.
303 detectorId = exposure.info.getDetector().getId()
305 row = visitSummary.find(detectorId)
307 self.
log.
warning(
"Detector id %s not found in external calibrations catalog; "
308 "Using original calibrations.", detectorId)
311 apCorrMap = row.getApCorrMap()
313 self.
log.
warning(
"Detector id %s has None for psf in "
314 "external calibrations catalog; Using original psf and aperture correction.",
316 elif apCorrMap
is None:
317 self.
log.
warning(
"Detector id %s has None for apCorrMap in "
318 "external calibrations catalog; Using original psf and aperture correction.",
322 exposure.info.setApCorrMap(apCorrMap)
327 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None,
329 """PSF match, subtract, and decorrelate two images.
333 template : `lsst.afw.image.ExposureF`
334 Template exposure, warped to match the science exposure.
335 science : `lsst.afw.image.ExposureF`
336 Science exposure to subtract from the template.
337 sources : `lsst.afw.table.SourceCatalog`
338 Identified sources on the science exposure. This catalog is used to
339 select sources in order to perform the AL PSF matching on stamp
341 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
342 Exposure catalog with finalized psf models and aperture correction
343 maps to be applied. Catalog uses the detector id for the catalog
344 id, sorted on id for fast lookup. Deprecated in favor of
345 ``visitSummary``, and will be removed after v26.
346 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
347 Exposure catalog with external calibrations to be applied. Catalog
348 uses the detector id for the catalog id, sorted on id for fast
349 lookup. Ignored (for temporary backwards compatibility) if
350 ``finalizedPsfApCorrCatalog`` is provided.
354 results : `lsst.pipe.base.Struct`
355 ``difference`` : `lsst.afw.image.ExposureF`
356 Result of subtracting template and science.
357 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
358 Warped and PSF-matched template exposure.
359 ``backgroundModel`` : `lsst.afw.math.Function2D`
360 Background model that was fit while solving for the
362 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
363 Kernel used to PSF-match the convolved image.
368 If an unsupported convolution mode is supplied.
370 If there are too few sources to calculate the PSF matching kernel.
371 lsst.pipe.base.NoWorkFound
372 Raised if fraction of good pixels, defined as not having NO_DATA
373 set, is less then the configured requiredTemplateFraction
376 if finalizedPsfApCorrCatalog
is not None:
378 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
379 "argument, and will be removed after v26.",
381 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
383 visitSummary = finalizedPsfApCorrCatalog
388 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
389 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
398 templatePsfSize = getPsfFwhm(template.psf)
399 sciencePsfSize = getPsfFwhm(science.psf)
401 self.
log.
info(
"Unable to evaluate PSF at the average position. "
402 "Evaluting PSF on a grid of points."
404 templatePsfSize = evaluateMeanPsfFwhm(template,
405 fwhmExposureBuffer=fwhmExposureBuffer,
406 fwhmExposureGrid=fwhmExposureGrid
408 sciencePsfSize = evaluateMeanPsfFwhm(science,
409 fwhmExposureBuffer=fwhmExposureBuffer,
410 fwhmExposureGrid=fwhmExposureGrid
412 self.
log.
info(
"Science PSF FWHM: %f pixels", sciencePsfSize)
413 self.
log.
info(
"Template PSF FWHM: %f pixels", templatePsfSize)
414 self.metadata.add(
"sciencePsfSize", sciencePsfSize)
415 self.metadata.add(
"templatePsfSize", templatePsfSize)
417 if self.config.mode ==
"auto":
420 fwhmExposureBuffer=fwhmExposureBuffer,
421 fwhmExposureGrid=fwhmExposureGrid)
423 if sciencePsfSize < templatePsfSize:
424 self.
log.
info(
"Average template PSF size is greater, "
425 "but science PSF greater in one dimension: convolving template image.")
427 self.
log.
info(
"Science PSF size is greater: convolving template image.")
429 self.
log.
info(
"Template PSF size is greater: convolving science image.")
430 elif self.config.mode ==
"convolveTemplate":
431 self.
log.
info(
"`convolveTemplate` is set: convolving template image.")
432 convolveTemplate =
True
433 elif self.config.mode ==
"convolveScience":
434 self.
log.
info(
"`convolveScience` is set: convolving science image.")
435 convolveTemplate =
False
437 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
440 sourceMask = science.mask.clone()
441 sourceMask.array |= template[science.getBBox()].mask.array
444 self.metadata.add(
"convolvedExposure",
"Template")
447 self.metadata.add(
"convolvedExposure",
"Science")
451 self.
log.
warning(
"Failed to match template. Checking coverage")
454 self.config.minTemplateFractionForExpectedSuccess,
455 exceptionMessage=
"Template coverage lower than expected to succeed."
456 f
" Failure is tolerable: {e}")
460 return subtractResults
463 """Convolve the template image with a PSF-matching kernel and subtract
464 from the science image.
468 template : `lsst.afw.image.ExposureF`
469 Template exposure, warped to match the science exposure.
470 science : `lsst.afw.image.ExposureF`
471 Science exposure to subtract from the template.
472 selectSources : `lsst.afw.table.SourceCatalog`
473 Identified sources on the science exposure. This catalog is used to
474 select sources in order to perform the AL PSF matching on stamp
479 results : `lsst.pipe.base.Struct`
481 ``difference`` : `lsst.afw.image.ExposureF`
482 Result of subtracting template and science.
483 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
484 Warped and PSF-matched template exposure.
485 ``backgroundModel`` : `lsst.afw.math.Function2D`
486 Background model that was fit while solving for the PSF-matching kernel
487 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
488 Kernel used to PSF-match the template to the science image.
491 kernelSources = self.makeKernel.selectKernelSources(template, science,
492 candidateList=selectSources,
494 kernelResult = self.makeKernel.
run(template, science, kernelSources,
496 except Exception
as e:
497 if self.config.allowKernelSourceDetection:
498 self.
log.
warning(
"Error encountered trying to construct the matching kernel"
499 f
" Running source detection and retrying. {e}")
500 kernelSources = self.makeKernel.selectKernelSources(template, science,
503 kernelResult = self.makeKernel.
run(template, science, kernelSources,
508 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
510 bbox=science.getBBox(),
512 photoCalib=science.photoCalib)
515 backgroundModel=(kernelResult.backgroundModel
516 if self.config.doSubtractBackground
else None))
517 correctedExposure = self.
finalize(template, science, difference,
518 kernelResult.psfMatchingKernel,
519 templateMatched=
True)
521 return lsst.pipe.base.Struct(difference=correctedExposure,
522 matchedTemplate=matchedTemplate,
523 matchedScience=science,
524 backgroundModel=kernelResult.backgroundModel,
525 psfMatchingKernel=kernelResult.psfMatchingKernel)
528 """Convolve the science image with a PSF-matching kernel and subtract
533 template : `lsst.afw.image.ExposureF`
534 Template exposure, warped to match the science exposure.
535 science : `lsst.afw.image.ExposureF`
536 Science exposure to subtract from the template.
537 selectSources : `lsst.afw.table.SourceCatalog`
538 Identified sources on the science exposure. This catalog is used to
539 select sources in order to perform the AL PSF matching on stamp
544 results : `lsst.pipe.base.Struct`
546 ``difference`` : `lsst.afw.image.ExposureF`
547 Result of subtracting template and science.
548 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
549 Warped template exposure. Note that in this case, the template
550 is not PSF-matched to the science image.
551 ``backgroundModel`` : `lsst.afw.math.Function2D`
552 Background model that was fit while solving for the PSF-matching kernel
553 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
554 Kernel used to PSF-match the science image to the template.
556 bbox = science.getBBox()
557 kernelSources = self.makeKernel.selectKernelSources(science, template,
558 candidateList=selectSources,
560 kernelResult = self.makeKernel.
run(science, template, kernelSources,
562 modelParams = kernelResult.backgroundModel.getParameters()
564 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
566 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
567 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
574 matchedScience.maskedImage /= norm
575 matchedTemplate = template.clone()[bbox]
576 matchedTemplate.maskedImage /= norm
577 matchedTemplate.setPhotoCalib(science.photoCalib)
580 backgroundModel=(kernelResult.backgroundModel
581 if self.config.doSubtractBackground
else None))
583 correctedExposure = self.
finalize(template, science, difference,
584 kernelResult.psfMatchingKernel,
585 templateMatched=
False)
587 return lsst.pipe.base.Struct(difference=correctedExposure,
588 matchedTemplate=matchedTemplate,
589 matchedScience=matchedScience,
590 backgroundModel=kernelResult.backgroundModel,
591 psfMatchingKernel=kernelResult.psfMatchingKernel,)
593 def finalize(self, template, science, difference, kernel,
594 templateMatched=True,
597 spatiallyVarying=False):
598 """Decorrelate the difference image to undo the noise correlations
599 caused by convolution.
603 template : `lsst.afw.image.ExposureF`
604 Template exposure, warped to match the science exposure.
605 science : `lsst.afw.image.ExposureF`
606 Science exposure to subtract from the template.
607 difference : `lsst.afw.image.ExposureF`
608 Result of subtracting template and science.
609 kernel : `lsst.afw.math.Kernel`
610 An (optionally spatially-varying) PSF matching kernel
611 templateMatched : `bool`, optional
612 Was the template PSF-matched to the science image?
613 preConvMode : `bool`, optional
614 Was the science image preconvolved with its own PSF
615 before PSF matching the template?
616 preConvKernel : `lsst.afw.detection.Psf`, optional
617 If not `None`, then the science image was pre-convolved with
618 (the reflection of) this kernel. Must be normalized to sum to 1.
619 spatiallyVarying : `bool`, optional
620 Compute the decorrelation kernel spatially varying across the image?
624 correctedExposure : `lsst.afw.image.ExposureF`
625 The decorrelated image difference.
632 if self.config.doDecorrelation:
633 self.
log.
info(
"Decorrelating image difference.")
637 correctedExposure = self.decorrelate.
run(science, template[science.getBBox()], difference, kernel,
638 templateMatched=templateMatched,
639 preConvMode=preConvMode,
640 preConvKernel=preConvKernel,
641 spatiallyVarying=spatiallyVarying).correctedExposure
643 self.
log.
info(
"NOT decorrelating image difference.")
644 correctedExposure = difference
645 return correctedExposure
648 """Update the mask planes on images for finalizing."""
650 bbox = science.getBBox()
651 mask = difference.mask
652 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
654 self.
log.
info(
"Adding injected mask planes")
655 mask.addMaskPlane(
"INJECTED")
656 mask.addMaskPlane(
"INJECTED_TEMPLATE")
658 if "FAKE" in science.mask.getMaskPlaneDict().keys():
662 diffInjectedBitMask = mask.getPlaneBitMask(
"INJECTED")
663 diffInjTmpltBitMask = mask.getPlaneBitMask(
"INJECTED_TEMPLATE")
665 scienceFakeBitMask = science.mask.getPlaneBitMask(
'FAKE')
666 tmpltFakeBitMask = template[bbox].mask.getPlaneBitMask(
'FAKE')
668 injScienceMaskArray = ((science.mask.array & scienceFakeBitMask) > 0) * diffInjectedBitMask
669 injTemplateMaskArray = ((template[bbox].mask.array & tmpltFakeBitMask) > 0) * diffInjTmpltBitMask
671 mask.array |= injScienceMaskArray
672 mask.array |= injTemplateMaskArray
674 template[bbox].mask.array[...] = difference.mask.array[...]
678 """Check that the WCS of the two Exposures match, and the template bbox
679 contains the science bbox.
683 template : `lsst.afw.image.ExposureF`
684 Template exposure, warped to match the science exposure.
685 science : `lsst.afw.image.ExposureF`
686 Science exposure to subtract from the template.
691 Raised if the WCS of the template is not equal to the science WCS,
692 or if the science image is not fully contained in the template
695 assert template.wcs == science.wcs,\
696 "Template and science exposure WCS are not identical."
697 templateBBox = template.getBBox()
698 scienceBBox = science.getBBox()
700 assert templateBBox.contains(scienceBBox),\
701 "Template bbox does not contain all of the science image."
707 interpolateBadMaskPlanes=False,
709 """Convolve an exposure with the given kernel.
713 exposure : `lsst.afw.Exposure`
714 exposure to convolve.
715 kernel : `lsst.afw.math.LinearCombinationKernel`
716 PSF matching kernel computed in the ``makeKernel`` subtask.
717 convolutionControl : `lsst.afw.math.ConvolutionControl`
718 Configuration for convolve algorithm.
719 bbox : `lsst.geom.Box2I`, optional
720 Bounding box to trim the convolved exposure to.
721 psf : `lsst.afw.detection.Psf`, optional
722 Point spread function (PSF) to set for the convolved exposure.
723 photoCalib : `lsst.afw.image.PhotoCalib`, optional
724 Photometric calibration of the convolved exposure.
728 convolvedExp : `lsst.afw.Exposure`
731 convolvedExposure = exposure.clone()
733 convolvedExposure.setPsf(psf)
734 if photoCalib
is not None:
735 convolvedExposure.setPhotoCalib(photoCalib)
736 if interpolateBadMaskPlanes
and self.config.badMaskPlanes
is not None:
738 self.config.badMaskPlanes)
739 self.metadata.add(
"nInterpolated", nInterp)
740 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
742 convolvedExposure.setMaskedImage(convolvedImage)
744 return convolvedExposure
746 return convolvedExposure[bbox]
749 """Select sources from a catalog that meet the selection criteria.
753 sources : `lsst.afw.table.SourceCatalog`
754 Input source catalog to select sources from.
755 mask : `lsst.afw.image.Mask`
756 The image mask plane to use to reject sources
757 based on their location on the ccd.
761 selectSources : `lsst.afw.table.SourceCatalog`
762 The input source catalog, with flagged and low signal-to-noise
768 If there are too few sources to compute the PSF matching kernel
769 remaining after source selection.
771 flags = np.ones(
len(sources), dtype=bool)
772 for flag
in self.config.badSourceFlags:
774 flags *= ~sources[flag]
775 except Exception
as e:
776 self.
log.
warning(
"Could not apply source flag: %s", e)
777 signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr()
778 sToNFlag = signalToNoise > self.config.detectionThreshold
780 sToNFlagMax = signalToNoise < self.config.detectionThresholdMax
782 flags *= self.
_checkMask(mask, sources, self.config.excludeMaskPlanes)
783 selectSources = sources[flags].copy(deep=
True)
784 if (
len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
785 signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
786 indices = np.argsort(signalToNoise)
787 indices = indices[-self.config.maxKernelSources:]
788 flags = np.zeros(
len(selectSources), dtype=bool)
789 flags[indices] =
True
790 selectSources = selectSources[flags].copy(deep=
True)
792 self.
log.
info(
"%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
793 len(selectSources),
len(sources), 100*
len(selectSources)/
len(sources))
794 if len(selectSources) < self.config.minKernelSources:
795 self.
log.error(
"Too few sources to calculate the PSF matching kernel: "
796 "%i selected but %i needed for the calculation.",
797 len(selectSources), self.config.minKernelSources)
798 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
799 self.metadata.add(
"nPsfSources",
len(selectSources))
805 """Exclude sources that are located on masked pixels.
809 mask : `lsst.afw.image.Mask`
810 The image mask plane to use to reject sources
811 based on the location of their centroid on the ccd.
812 sources : `lsst.afw.table.SourceCatalog`
813 The source catalog to evaluate.
814 excludeMaskPlanes : `list` of `str`
815 List of the names of the mask planes to exclude.
819 flags : `numpy.ndarray` of `bool`
820 Array indicating whether each source in the catalog should be
821 kept (True) or rejected (False) based on the value of the
822 mask plane at its location.
824 setExcludeMaskPlanes = [
825 maskPlane
for maskPlane
in excludeMaskPlanes
if maskPlane
in mask.getMaskPlaneDict()
828 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
830 xv = np.rint(sources.getX() - mask.getX0())
831 yv = np.rint(sources.getY() - mask.getY0())
833 mv = mask.array[yv.astype(int), xv.astype(int)]
834 flags = np.bitwise_and(mv, excludePixelMask) == 0
838 """Perform preparatory calculations common to all Alard&Lupton Tasks.
842 template : `lsst.afw.image.ExposureF`
843 Template exposure, warped to match the science exposure. The
844 variance plane of the template image is modified in place.
845 science : `lsst.afw.image.ExposureF`
846 Science exposure to subtract from the template. The variance plane
847 of the science image is modified in place.
848 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
849 Exposure catalog with external calibrations to be applied. Catalog
850 uses the detector id for the catalog id, sorted on id for fast
854 if visitSummary
is not None:
857 requiredTemplateFraction=self.config.requiredTemplateFraction,
858 exceptionMessage=
"Not attempting subtraction. To force subtraction,"
859 " set config requiredTemplateFraction=0")
861 if self.config.doScaleVariance:
865 templateVarFactor = self.scaleVariance.
run(template.maskedImage)
866 sciVarFactor = self.scaleVariance.
run(science.maskedImage)
867 self.
log.
info(
"Template variance scaling factor: %.2f", templateVarFactor)
868 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
869 self.
log.
info(
"Science variance scaling factor: %.2f", sciVarFactor)
870 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
874 """Clear the mask plane of the template.
878 template : `lsst.afw.image.ExposureF`
879 Template exposure, warped to match the science exposure.
880 The mask plane will be modified in place.
883 clearMaskPlanes = [maskplane
for maskplane
in mask.getMaskPlaneDict().keys()
884 if maskplane
not in self.config.preserveTemplateMask]
886 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
887 mask &= ~bitMaskToClear
891 SubtractScoreOutputConnections):
896 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
901 """Subtract a template from a science image, convolving the science image
902 before computing the kernel, and also convolving the template before
905 ConfigClass = AlardLuptonPreconvolveSubtractConfig
906 _DefaultName =
"alardLuptonPreconvolveSubtract"
908 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None):
909 """Preconvolve the science image with its own PSF,
910 convolve the template image with a PSF-matching kernel and subtract
911 from the preconvolved science image.
915 template : `lsst.afw.image.ExposureF`
916 The template image, which has previously been warped to the science
917 image. The template bbox will be padded by a few pixels compared to
919 science : `lsst.afw.image.ExposureF`
920 The science exposure.
921 sources : `lsst.afw.table.SourceCatalog`
922 Identified sources on the science exposure. This catalog is used to
923 select sources in order to perform the AL PSF matching on stamp
925 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
926 Exposure catalog with finalized psf models and aperture correction
927 maps to be applied. Catalog uses the detector id for the catalog
928 id, sorted on id for fast lookup. Deprecated in favor of
929 ``visitSummary``, and will be removed after v26.
930 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
931 Exposure catalog with complete external calibrations. Catalog uses
932 the detector id for the catalog id, sorted on id for fast lookup.
933 Ignored (for temporary backwards compatibility) if
934 ``finalizedPsfApCorrCatalog`` is provided.
938 results : `lsst.pipe.base.Struct`
939 ``scoreExposure`` : `lsst.afw.image.ExposureF`
940 Result of subtracting the convolved template and science
941 images. Attached PSF is that of the original science image.
942 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
943 Warped and PSF-matched template exposure. Attached PSF is that
944 of the original science image.
945 ``matchedScience`` : `lsst.afw.image.ExposureF`
946 The science exposure after convolving with its own PSF.
947 Attached PSF is that of the original science image.
948 ``backgroundModel`` : `lsst.afw.math.Function2D`
949 Background model that was fit while solving for the
951 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
952 Final kernel used to PSF-match the template to the science
955 if finalizedPsfApCorrCatalog
is not None:
957 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
958 "argument, and will be removed after v26.",
960 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
962 visitSummary = finalizedPsfApCorrCatalog
967 scienceKernel = science.psf.getKernel()
969 interpolateBadMaskPlanes=
True)
970 self.metadata.add(
"convolvedExposure",
"Preconvolution")
973 subtractResults = self.
runPreconvolve(template, science, matchedScience,
974 selectSources, scienceKernel)
977 self.
loglog.
warning(
"Failed to match template. Checking coverage")
980 self.config.minTemplateFractionForExpectedSuccess,
981 exceptionMessage=
"Template coverage lower than expected to succeed."
982 f
" Failure is tolerable: {e}")
986 return subtractResults
988 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
989 """Convolve the science image with its own PSF, then convolve the
990 template with a matching kernel and subtract to form the Score
995 template : `lsst.afw.image.ExposureF`
996 Template exposure, warped to match the science exposure.
997 science : `lsst.afw.image.ExposureF`
998 Science exposure to subtract from the template.
999 matchedScience : `lsst.afw.image.ExposureF`
1000 The science exposure, convolved with the reflection of its own PSF.
1001 selectSources : `lsst.afw.table.SourceCatalog`
1002 Identified sources on the science exposure. This catalog is used to
1003 select sources in order to perform the AL PSF matching on stamp
1005 preConvKernel : `lsst.afw.math.Kernel`
1006 The reflection of the kernel that was used to preconvolve the
1007 `science` exposure. Must be normalized to sum to 1.
1011 results : `lsst.pipe.base.Struct`
1013 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1014 Result of subtracting the convolved template and science
1015 images. Attached PSF is that of the original science image.
1016 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1017 Warped and PSF-matched template exposure. Attached PSF is that
1018 of the original science image.
1019 ``matchedScience`` : `lsst.afw.image.ExposureF`
1020 The science exposure after convolving with its own PSF.
1021 Attached PSF is that of the original science image.
1022 ``backgroundModel`` : `lsst.afw.math.Function2D`
1023 Background model that was fit while solving for the
1025 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1026 Final kernel used to PSF-match the template to the science
1029 bbox = science.getBBox()
1030 innerBBox = preConvKernel.shrinkBBox(bbox)
1032 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
1033 candidateList=selectSources,
1035 kernelResult = self.makeKernel.
run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1038 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
1042 interpolateBadMaskPlanes=
True,
1043 photoCalib=science.photoCalib)
1045 backgroundModel=(kernelResult.backgroundModel
1046 if self.config.doSubtractBackground
else None))
1047 correctedScore = self.
finalize(template[bbox], science, score,
1048 kernelResult.psfMatchingKernel,
1049 templateMatched=
True, preConvMode=
True,
1050 preConvKernel=preConvKernel)
1052 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1053 matchedTemplate=matchedTemplate,
1054 matchedScience=matchedScience,
1055 backgroundModel=kernelResult.backgroundModel,
1056 psfMatchingKernel=kernelResult.psfMatchingKernel)
1060 exceptionMessage=""):
1061 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1065 templateExposure : `lsst.afw.image.ExposureF`
1066 The template exposure to check
1067 logger : `lsst.log.Log`
1068 Logger for printing output.
1069 requiredTemplateFraction : `float`, optional
1070 Fraction of pixels of the science image required to have coverage
1072 exceptionMessage : `str`, optional
1073 Message to include in the exception raised if the template coverage
1078 lsst.pipe.base.NoWorkFound
1079 Raised if fraction of good pixels, defined as not having NO_DATA
1080 set, is less than the requiredTemplateFraction
1084 pixNoData = np.count_nonzero(templateExposure.mask.array
1085 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1086 pixGood = templateExposure.getBBox().getArea() - pixNoData
1087 logger.info(
"template has %d good pixels (%.1f%%)", pixGood,
1088 100*pixGood/templateExposure.getBBox().getArea())
1090 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
1091 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1092 100*pixGood/templateExposure.getBBox().getArea(),
1093 100*requiredTemplateFraction))
1094 raise lsst.pipe.base.NoWorkFound(message +
" " + exceptionMessage)
1098 """Subtract template from science, propagating relevant metadata.
1102 science : `lsst.afw.Exposure`
1103 The input science image.
1104 template : `lsst.afw.Exposure`
1105 The template to subtract from the science image.
1106 backgroundModel : `lsst.afw.MaskedImage`, optional
1107 Differential background model
1111 difference : `lsst.afw.Exposure`
1112 The subtracted image.
1114 difference = science.clone()
1115 if backgroundModel
is not None:
1116 difference.maskedImage -= backgroundModel
1117 difference.maskedImage -= template.maskedImage
1122 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1126 exp1 : `~lsst.afw.image.Exposure`
1127 Exposure with the reference point spread function (PSF) to evaluate.
1128 exp2 : `~lsst.afw.image.Exposure`
1129 Exposure with a candidate point spread function (PSF) to evaluate.
1130 fwhmExposureBuffer : `float`
1131 Fractional buffer margin to be left out of all sides of the image
1132 during the construction of the grid to compute mean PSF FWHM in an
1133 exposure, if the PSF is not available at its average position.
1134 fwhmExposureGrid : `int`
1135 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1136 available at its average position.
1140 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1144 shape1 = getPsfFwhm(exp1.psf, average=
False)
1145 shape2 = getPsfFwhm(exp2.psf, average=
False)
1147 shape1 = evaluateMeanPsfFwhm(exp1,
1148 fwhmExposureBuffer=fwhmExposureBuffer,
1149 fwhmExposureGrid=fwhmExposureGrid
1151 shape2 = evaluateMeanPsfFwhm(exp2,
1152 fwhmExposureBuffer=fwhmExposureBuffer,
1153 fwhmExposureGrid=fwhmExposureGrid
1155 return shape1 <= shape2
1158 xTest = shape1[0] <= shape2[0]
1159 yTest = shape1[1] <= shape2[1]
1160 return xTest | yTest
1164 """Replace masked image pixels with interpolated values.
1168 maskedImage : `lsst.afw.image.MaskedImage`
1169 Image on which to perform interpolation.
1170 badMaskPlanes : `list` of `str`
1171 List of mask planes to interpolate over.
1172 fallbackValue : `float`, optional
1173 Value to set when interpolation fails.
1178 The number of masked pixels that were replaced.
1180 imgBadMaskPlanes = [
1181 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in maskedImage.mask.getMaskPlaneDict()
1184 image = maskedImage.image.array
1185 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1186 image[badPixels] = np.nan
1187 if fallbackValue
is None:
1188 fallbackValue = np.nanmedian(image)
1191 image[badPixels] = fallbackValue
1192 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)