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 if "FAKE" in science.mask.getMaskPlaneDict().keys():
659 self.
log.
info(
"Adding injected mask planes")
660 mask.addMaskPlane(
"INJECTED")
661 diffInjectedBitMask = mask.getPlaneBitMask(
"INJECTED")
663 mask.addMaskPlane(
"INJECTED_TEMPLATE")
664 diffInjTmpltBitMask = mask.getPlaneBitMask(
"INJECTED_TEMPLATE")
666 scienceFakeBitMask = science.mask.getPlaneBitMask(
'FAKE')
667 tmpltFakeBitMask = template[bbox].mask.getPlaneBitMask(
'FAKE')
669 injScienceMaskArray = ((science.mask.array & scienceFakeBitMask) > 0) * diffInjectedBitMask
670 injTemplateMaskArray = ((template[bbox].mask.array & tmpltFakeBitMask) > 0) * diffInjTmpltBitMask
672 mask.array |= injScienceMaskArray
673 mask.array |= injTemplateMaskArray
675 template[bbox].mask.array[...] = difference.mask.array[...]
679 """Check that the WCS of the two Exposures match, and the template bbox
680 contains the science bbox.
684 template : `lsst.afw.image.ExposureF`
685 Template exposure, warped to match the science exposure.
686 science : `lsst.afw.image.ExposureF`
687 Science exposure to subtract from the template.
692 Raised if the WCS of the template is not equal to the science WCS,
693 or if the science image is not fully contained in the template
696 assert template.wcs == science.wcs,\
697 "Template and science exposure WCS are not identical."
698 templateBBox = template.getBBox()
699 scienceBBox = science.getBBox()
701 assert templateBBox.contains(scienceBBox),\
702 "Template bbox does not contain all of the science image."
708 interpolateBadMaskPlanes=False,
710 """Convolve an exposure with the given kernel.
714 exposure : `lsst.afw.Exposure`
715 exposure to convolve.
716 kernel : `lsst.afw.math.LinearCombinationKernel`
717 PSF matching kernel computed in the ``makeKernel`` subtask.
718 convolutionControl : `lsst.afw.math.ConvolutionControl`
719 Configuration for convolve algorithm.
720 bbox : `lsst.geom.Box2I`, optional
721 Bounding box to trim the convolved exposure to.
722 psf : `lsst.afw.detection.Psf`, optional
723 Point spread function (PSF) to set for the convolved exposure.
724 photoCalib : `lsst.afw.image.PhotoCalib`, optional
725 Photometric calibration of the convolved exposure.
729 convolvedExp : `lsst.afw.Exposure`
732 convolvedExposure = exposure.clone()
734 convolvedExposure.setPsf(psf)
735 if photoCalib
is not None:
736 convolvedExposure.setPhotoCalib(photoCalib)
737 if interpolateBadMaskPlanes
and self.config.badMaskPlanes
is not None:
739 self.config.badMaskPlanes)
740 self.metadata.add(
"nInterpolated", nInterp)
741 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
743 convolvedExposure.setMaskedImage(convolvedImage)
745 return convolvedExposure
747 return convolvedExposure[bbox]
750 """Select sources from a catalog that meet the selection criteria.
754 sources : `lsst.afw.table.SourceCatalog`
755 Input source catalog to select sources from.
756 mask : `lsst.afw.image.Mask`
757 The image mask plane to use to reject sources
758 based on their location on the ccd.
762 selectSources : `lsst.afw.table.SourceCatalog`
763 The input source catalog, with flagged and low signal-to-noise
769 If there are too few sources to compute the PSF matching kernel
770 remaining after source selection.
772 flags = np.ones(
len(sources), dtype=bool)
773 for flag
in self.config.badSourceFlags:
775 flags *= ~sources[flag]
776 except Exception
as e:
777 self.
log.
warning(
"Could not apply source flag: %s", e)
778 signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr()
779 sToNFlag = signalToNoise > self.config.detectionThreshold
781 sToNFlagMax = signalToNoise < self.config.detectionThresholdMax
783 flags *= self.
_checkMask(mask, sources, self.config.excludeMaskPlanes)
784 selectSources = sources[flags].copy(deep=
True)
785 if (
len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
786 signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
787 indices = np.argsort(signalToNoise)
788 indices = indices[-self.config.maxKernelSources:]
789 flags = np.zeros(
len(selectSources), dtype=bool)
790 flags[indices] =
True
791 selectSources = selectSources[flags].copy(deep=
True)
793 self.
log.
info(
"%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
794 len(selectSources),
len(sources), 100*
len(selectSources)/
len(sources))
795 if len(selectSources) < self.config.minKernelSources:
796 self.
log.error(
"Too few sources to calculate the PSF matching kernel: "
797 "%i selected but %i needed for the calculation.",
798 len(selectSources), self.config.minKernelSources)
799 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
800 self.metadata.add(
"nPsfSources",
len(selectSources))
806 """Exclude sources that are located on masked pixels.
810 mask : `lsst.afw.image.Mask`
811 The image mask plane to use to reject sources
812 based on the location of their centroid on the ccd.
813 sources : `lsst.afw.table.SourceCatalog`
814 The source catalog to evaluate.
815 excludeMaskPlanes : `list` of `str`
816 List of the names of the mask planes to exclude.
820 flags : `numpy.ndarray` of `bool`
821 Array indicating whether each source in the catalog should be
822 kept (True) or rejected (False) based on the value of the
823 mask plane at its location.
825 setExcludeMaskPlanes = [
826 maskPlane
for maskPlane
in excludeMaskPlanes
if maskPlane
in mask.getMaskPlaneDict()
829 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
831 xv = np.rint(sources.getX() - mask.getX0())
832 yv = np.rint(sources.getY() - mask.getY0())
834 mv = mask.array[yv.astype(int), xv.astype(int)]
835 flags = np.bitwise_and(mv, excludePixelMask) == 0
839 """Perform preparatory calculations common to all Alard&Lupton Tasks.
843 template : `lsst.afw.image.ExposureF`
844 Template exposure, warped to match the science exposure. The
845 variance plane of the template image is modified in place.
846 science : `lsst.afw.image.ExposureF`
847 Science exposure to subtract from the template. The variance plane
848 of the science image is modified in place.
849 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
850 Exposure catalog with external calibrations to be applied. Catalog
851 uses the detector id for the catalog id, sorted on id for fast
855 if visitSummary
is not None:
858 requiredTemplateFraction=self.config.requiredTemplateFraction,
859 exceptionMessage=
"Not attempting subtraction. To force subtraction,"
860 " set config requiredTemplateFraction=0")
862 if self.config.doScaleVariance:
866 templateVarFactor = self.scaleVariance.
run(template.maskedImage)
867 sciVarFactor = self.scaleVariance.
run(science.maskedImage)
868 self.
log.
info(
"Template variance scaling factor: %.2f", templateVarFactor)
869 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
870 self.
log.
info(
"Science variance scaling factor: %.2f", sciVarFactor)
871 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
875 """Clear the mask plane of the template.
879 template : `lsst.afw.image.ExposureF`
880 Template exposure, warped to match the science exposure.
881 The mask plane will be modified in place.
884 clearMaskPlanes = [maskplane
for maskplane
in mask.getMaskPlaneDict().keys()
885 if maskplane
not in self.config.preserveTemplateMask]
887 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
888 mask &= ~bitMaskToClear
892 SubtractScoreOutputConnections):
897 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
902 """Subtract a template from a science image, convolving the science image
903 before computing the kernel, and also convolving the template before
906 ConfigClass = AlardLuptonPreconvolveSubtractConfig
907 _DefaultName =
"alardLuptonPreconvolveSubtract"
909 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None):
910 """Preconvolve the science image with its own PSF,
911 convolve the template image with a PSF-matching kernel and subtract
912 from the preconvolved science image.
916 template : `lsst.afw.image.ExposureF`
917 The template image, which has previously been warped to the science
918 image. The template bbox will be padded by a few pixels compared to
920 science : `lsst.afw.image.ExposureF`
921 The science exposure.
922 sources : `lsst.afw.table.SourceCatalog`
923 Identified sources on the science exposure. This catalog is used to
924 select sources in order to perform the AL PSF matching on stamp
926 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
927 Exposure catalog with finalized psf models and aperture correction
928 maps to be applied. Catalog uses the detector id for the catalog
929 id, sorted on id for fast lookup. Deprecated in favor of
930 ``visitSummary``, and will be removed after v26.
931 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
932 Exposure catalog with complete external calibrations. Catalog uses
933 the detector id for the catalog id, sorted on id for fast lookup.
934 Ignored (for temporary backwards compatibility) if
935 ``finalizedPsfApCorrCatalog`` is provided.
939 results : `lsst.pipe.base.Struct`
940 ``scoreExposure`` : `lsst.afw.image.ExposureF`
941 Result of subtracting the convolved template and science
942 images. Attached PSF is that of the original science image.
943 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
944 Warped and PSF-matched template exposure. Attached PSF is that
945 of the original science image.
946 ``matchedScience`` : `lsst.afw.image.ExposureF`
947 The science exposure after convolving with its own PSF.
948 Attached PSF is that of the original science image.
949 ``backgroundModel`` : `lsst.afw.math.Function2D`
950 Background model that was fit while solving for the
952 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
953 Final kernel used to PSF-match the template to the science
956 if finalizedPsfApCorrCatalog
is not None:
958 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
959 "argument, and will be removed after v26.",
961 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
963 visitSummary = finalizedPsfApCorrCatalog
968 scienceKernel = science.psf.getKernel()
970 interpolateBadMaskPlanes=
True)
971 self.metadata.add(
"convolvedExposure",
"Preconvolution")
974 subtractResults = self.
runPreconvolve(template, science, matchedScience,
975 selectSources, scienceKernel)
978 self.
loglog.
warning(
"Failed to match template. Checking coverage")
981 self.config.minTemplateFractionForExpectedSuccess,
982 exceptionMessage=
"Template coverage lower than expected to succeed."
983 f
" Failure is tolerable: {e}")
987 return subtractResults
989 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
990 """Convolve the science image with its own PSF, then convolve the
991 template with a matching kernel and subtract to form the Score
996 template : `lsst.afw.image.ExposureF`
997 Template exposure, warped to match the science exposure.
998 science : `lsst.afw.image.ExposureF`
999 Science exposure to subtract from the template.
1000 matchedScience : `lsst.afw.image.ExposureF`
1001 The science exposure, convolved with the reflection of its own PSF.
1002 selectSources : `lsst.afw.table.SourceCatalog`
1003 Identified sources on the science exposure. This catalog is used to
1004 select sources in order to perform the AL PSF matching on stamp
1006 preConvKernel : `lsst.afw.math.Kernel`
1007 The reflection of the kernel that was used to preconvolve the
1008 `science` exposure. Must be normalized to sum to 1.
1012 results : `lsst.pipe.base.Struct`
1014 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1015 Result of subtracting the convolved template and science
1016 images. Attached PSF is that of the original science image.
1017 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1018 Warped and PSF-matched template exposure. Attached PSF is that
1019 of the original science image.
1020 ``matchedScience`` : `lsst.afw.image.ExposureF`
1021 The science exposure after convolving with its own PSF.
1022 Attached PSF is that of the original science image.
1023 ``backgroundModel`` : `lsst.afw.math.Function2D`
1024 Background model that was fit while solving for the
1026 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1027 Final kernel used to PSF-match the template to the science
1030 bbox = science.getBBox()
1031 innerBBox = preConvKernel.shrinkBBox(bbox)
1033 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
1034 candidateList=selectSources,
1036 kernelResult = self.makeKernel.
run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1039 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
1043 interpolateBadMaskPlanes=
True,
1044 photoCalib=science.photoCalib)
1046 backgroundModel=(kernelResult.backgroundModel
1047 if self.config.doSubtractBackground
else None))
1048 correctedScore = self.
finalize(template[bbox], science, score,
1049 kernelResult.psfMatchingKernel,
1050 templateMatched=
True, preConvMode=
True,
1051 preConvKernel=preConvKernel)
1053 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1054 matchedTemplate=matchedTemplate,
1055 matchedScience=matchedScience,
1056 backgroundModel=kernelResult.backgroundModel,
1057 psfMatchingKernel=kernelResult.psfMatchingKernel)
1061 exceptionMessage=""):
1062 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1066 templateExposure : `lsst.afw.image.ExposureF`
1067 The template exposure to check
1068 logger : `lsst.log.Log`
1069 Logger for printing output.
1070 requiredTemplateFraction : `float`, optional
1071 Fraction of pixels of the science image required to have coverage
1073 exceptionMessage : `str`, optional
1074 Message to include in the exception raised if the template coverage
1079 lsst.pipe.base.NoWorkFound
1080 Raised if fraction of good pixels, defined as not having NO_DATA
1081 set, is less than the requiredTemplateFraction
1085 pixNoData = np.count_nonzero(templateExposure.mask.array
1086 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1087 pixGood = templateExposure.getBBox().getArea() - pixNoData
1088 logger.info(
"template has %d good pixels (%.1f%%)", pixGood,
1089 100*pixGood/templateExposure.getBBox().getArea())
1091 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
1092 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1093 100*pixGood/templateExposure.getBBox().getArea(),
1094 100*requiredTemplateFraction))
1095 raise lsst.pipe.base.NoWorkFound(message +
" " + exceptionMessage)
1099 """Subtract template from science, propagating relevant metadata.
1103 science : `lsst.afw.Exposure`
1104 The input science image.
1105 template : `lsst.afw.Exposure`
1106 The template to subtract from the science image.
1107 backgroundModel : `lsst.afw.MaskedImage`, optional
1108 Differential background model
1112 difference : `lsst.afw.Exposure`
1113 The subtracted image.
1115 difference = science.clone()
1116 if backgroundModel
is not None:
1117 difference.maskedImage -= backgroundModel
1118 difference.maskedImage -= template.maskedImage
1123 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1127 exp1 : `~lsst.afw.image.Exposure`
1128 Exposure with the reference point spread function (PSF) to evaluate.
1129 exp2 : `~lsst.afw.image.Exposure`
1130 Exposure with a candidate point spread function (PSF) to evaluate.
1131 fwhmExposureBuffer : `float`
1132 Fractional buffer margin to be left out of all sides of the image
1133 during the construction of the grid to compute mean PSF FWHM in an
1134 exposure, if the PSF is not available at its average position.
1135 fwhmExposureGrid : `int`
1136 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1137 available at its average position.
1141 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1145 shape1 = getPsfFwhm(exp1.psf, average=
False)
1146 shape2 = getPsfFwhm(exp2.psf, average=
False)
1148 shape1 = evaluateMeanPsfFwhm(exp1,
1149 fwhmExposureBuffer=fwhmExposureBuffer,
1150 fwhmExposureGrid=fwhmExposureGrid
1152 shape2 = evaluateMeanPsfFwhm(exp2,
1153 fwhmExposureBuffer=fwhmExposureBuffer,
1154 fwhmExposureGrid=fwhmExposureGrid
1156 return shape1 <= shape2
1159 xTest = shape1[0] <= shape2[0]
1160 yTest = shape1[1] <= shape2[1]
1161 return xTest | yTest
1165 """Replace masked image pixels with interpolated values.
1169 maskedImage : `lsst.afw.image.MaskedImage`
1170 Image on which to perform interpolation.
1171 badMaskPlanes : `list` of `str`
1172 List of mask planes to interpolate over.
1173 fallbackValue : `float`, optional
1174 Value to set when interpolation fails.
1179 The number of masked pixels that were replaced.
1181 imgBadMaskPlanes = [
1182 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in maskedImage.mask.getMaskPlaneDict()
1185 image = maskedImage.image.array
1186 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1187 image[badPixels] = np.nan
1188 if fallbackValue
is None:
1189 fallbackValue = np.nanmedian(image)
1192 image[badPixels] = fallbackValue
1193 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)