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 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",
101 dimensions=_dimensions,
102 defaultTemplates=_defaultTemplates):
103 scoreExposure = connectionTypes.Output(
104 doc=
"The maximum likelihood image, used for the detection of diaSources.",
105 dimensions=(
"instrument",
"visit",
"detector"),
106 storageClass=
"ExposureF",
107 name=
"{fakesType}{coaddName}Diff_scoreExp",
116 makeKernel = lsst.pex.config.ConfigurableField(
117 target=MakeKernelTask,
118 doc=
"Task to construct a matching kernel for convolution.",
120 doDecorrelation = lsst.pex.config.Field(
123 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
124 "kernel convolution? If True, also update the diffim PSF."
126 decorrelate = lsst.pex.config.ConfigurableField(
127 target=DecorrelateALKernelTask,
128 doc=
"Task to decorrelate the image difference.",
130 requiredTemplateFraction = lsst.pex.config.Field(
133 doc=
"Raise NoWorkFound and do not attempt image subtraction if template covers less than this "
134 " fraction of pixels. Setting to 0 will always attempt image subtraction."
136 minTemplateFractionForExpectedSuccess = lsst.pex.config.Field(
139 doc=
"Raise NoWorkFound if PSF-matching fails and template covers less than this fraction of pixels."
140 " If the fraction of pixels covered by the template is less than this value (and greater than"
141 " requiredTemplateFraction) this task is attempted but failure is anticipated and tolerated."
143 doScaleVariance = lsst.pex.config.Field(
146 doc=
"Scale variance of the image difference?"
148 scaleVariance = lsst.pex.config.ConfigurableField(
149 target=ScaleVarianceTask,
150 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
152 doSubtractBackground = lsst.pex.config.Field(
153 doc=
"Subtract the background fit when solving the kernel?",
157 doApplyExternalCalibrations = lsst.pex.config.Field(
159 "Replace science Exposure's calibration objects with those"
160 " in visitSummary. Ignored if `doApplyFinalizedPsf is True."
165 detectionThreshold = lsst.pex.config.Field(
168 doc=
"Minimum signal to noise ratio of detected sources "
169 "to use for calculating the PSF matching kernel."
171 detectionThresholdMax = lsst.pex.config.Field(
174 doc=
"Maximum signal to noise ratio of detected sources "
175 "to use for calculating the PSF matching kernel."
177 maxKernelSources = lsst.pex.config.Field(
180 doc=
"Maximum number of sources to use for calculating the PSF matching kernel."
181 "Set to -1 to disable."
183 minKernelSources = lsst.pex.config.Field(
186 doc=
"Minimum number of sources needed for calculating the PSF matching kernel."
188 badSourceFlags = lsst.pex.config.ListField(
190 doc=
"Flags that, if set, the associated source should not "
191 "be used to determine the PSF matching kernel.",
192 default=(
"sky_source",
"slot_Centroid_flag",
193 "slot_ApFlux_flag",
"slot_PsfFlux_flag",
194 "base_PixelFlags_flag_interpolated",
195 "base_PixelFlags_flag_saturated",
196 "base_PixelFlags_flag_bad",
199 excludeMaskPlanes = lsst.pex.config.ListField(
201 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE",
"FAKE"),
202 doc=
"Mask planes to exclude when selecting sources for PSF matching."
204 badMaskPlanes = lsst.pex.config.ListField(
206 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE"),
207 doc=
"Mask planes to interpolate over."
209 preserveTemplateMask = lsst.pex.config.ListField(
211 default=(
"NO_DATA",
"BAD",),
212 doc=
"Mask planes from the template to propagate to the image difference."
214 renameTemplateMask = lsst.pex.config.ListField(
216 default=(
"SAT",
"INJECTED",
"INJECTED_CORE",),
217 doc=
"Mask planes from the template to propagate to the image difference"
218 "with '_TEMPLATE' appended to the name."
220 allowKernelSourceDetection = lsst.pex.config.Field(
223 doc=
"Re-run source detection for kernel candidates if an error is"
224 " encountered while calculating the matching kernel."
230 self.
makeKernel.kernel.active.spatialKernelOrder = 1
231 self.
makeKernel.kernel.active.spatialBgOrder = 2
235 pipelineConnections=AlardLuptonSubtractConnections):
236 mode = lsst.pex.config.ChoiceField(
238 default=
"convolveTemplate",
239 allowed={
"auto":
"Choose which image to convolve at runtime.",
240 "convolveScience":
"Only convolve the science image.",
241 "convolveTemplate":
"Only convolve the template image."},
242 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
247 """Compute the image difference of a science and template image using
248 the Alard & Lupton (1998) algorithm.
250 ConfigClass = AlardLuptonSubtractConfig
251 _DefaultName =
"alardLuptonSubtract"
255 self.makeSubtask(
"decorrelate")
256 self.makeSubtask(
"makeKernel")
257 if self.config.doScaleVariance:
258 self.makeSubtask(
"scaleVariance")
267 """Replace calibrations (psf, and ApCorrMap) on this exposure with
272 exposure : `lsst.afw.image.exposure.Exposure`
273 Input exposure to adjust calibrations.
274 visitSummary : `lsst.afw.table.ExposureCatalog`
275 Exposure catalog with external calibrations to be applied. Catalog
276 uses the detector id for the catalog id, sorted on id for fast
281 exposure : `lsst.afw.image.exposure.Exposure`
282 Exposure with adjusted calibrations.
284 detectorId = exposure.info.getDetector().getId()
286 row = visitSummary.find(detectorId)
288 self.
log.
warning(
"Detector id %s not found in external calibrations catalog; "
289 "Using original calibrations.", detectorId)
292 apCorrMap = row.getApCorrMap()
294 self.
log.
warning(
"Detector id %s has None for psf in "
295 "external calibrations catalog; Using original psf and aperture correction.",
297 elif apCorrMap
is None:
298 self.
log.
warning(
"Detector id %s has None for apCorrMap in "
299 "external calibrations catalog; Using original psf and aperture correction.",
303 exposure.info.setApCorrMap(apCorrMap)
308 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None,
310 """PSF match, subtract, and decorrelate two images.
314 template : `lsst.afw.image.ExposureF`
315 Template exposure, warped to match the science exposure.
316 science : `lsst.afw.image.ExposureF`
317 Science exposure to subtract from the template.
318 sources : `lsst.afw.table.SourceCatalog`
319 Identified sources on the science exposure. This catalog is used to
320 select sources in order to perform the AL PSF matching on stamp
322 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
323 Exposure catalog with finalized psf models and aperture correction
324 maps to be applied. Catalog uses the detector id for the catalog
325 id, sorted on id for fast lookup. Deprecated in favor of
326 ``visitSummary``, and will be removed after v26.
327 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
328 Exposure catalog with external calibrations to be applied. Catalog
329 uses the detector id for the catalog id, sorted on id for fast
330 lookup. Ignored (for temporary backwards compatibility) if
331 ``finalizedPsfApCorrCatalog`` is provided.
335 results : `lsst.pipe.base.Struct`
336 ``difference`` : `lsst.afw.image.ExposureF`
337 Result of subtracting template and science.
338 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
339 Warped and PSF-matched template exposure.
340 ``backgroundModel`` : `lsst.afw.math.Function2D`
341 Background model that was fit while solving for the
343 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
344 Kernel used to PSF-match the convolved image.
349 If an unsupported convolution mode is supplied.
351 If there are too few sources to calculate the PSF matching kernel.
352 lsst.pipe.base.NoWorkFound
353 Raised if fraction of good pixels, defined as not having NO_DATA
354 set, is less then the configured requiredTemplateFraction
357 if finalizedPsfApCorrCatalog
is not None:
359 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
360 "argument, and will be removed after v26.",
362 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
364 visitSummary = finalizedPsfApCorrCatalog
369 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
370 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
379 templatePsfSize = getPsfFwhm(template.psf)
380 sciencePsfSize = getPsfFwhm(science.psf)
382 self.
log.
info(
"Unable to evaluate PSF at the average position. "
383 "Evaluting PSF on a grid of points."
385 templatePsfSize = evaluateMeanPsfFwhm(template,
386 fwhmExposureBuffer=fwhmExposureBuffer,
387 fwhmExposureGrid=fwhmExposureGrid
389 sciencePsfSize = evaluateMeanPsfFwhm(science,
390 fwhmExposureBuffer=fwhmExposureBuffer,
391 fwhmExposureGrid=fwhmExposureGrid
393 self.
log.
info(
"Science PSF FWHM: %f pixels", sciencePsfSize)
394 self.
log.
info(
"Template PSF FWHM: %f pixels", templatePsfSize)
395 self.metadata.add(
"sciencePsfSize", sciencePsfSize)
396 self.metadata.add(
"templatePsfSize", templatePsfSize)
398 if self.config.mode ==
"auto":
401 fwhmExposureBuffer=fwhmExposureBuffer,
402 fwhmExposureGrid=fwhmExposureGrid)
404 if sciencePsfSize < templatePsfSize:
405 self.
log.
info(
"Average template PSF size is greater, "
406 "but science PSF greater in one dimension: convolving template image.")
408 self.
log.
info(
"Science PSF size is greater: convolving template image.")
410 self.
log.
info(
"Template PSF size is greater: convolving science image.")
411 elif self.config.mode ==
"convolveTemplate":
412 self.
log.
info(
"`convolveTemplate` is set: convolving template image.")
413 convolveTemplate =
True
414 elif self.config.mode ==
"convolveScience":
415 self.
log.
info(
"`convolveScience` is set: convolving science image.")
416 convolveTemplate =
False
418 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
421 sourceMask = science.mask.clone()
422 sourceMask.array |= template[science.getBBox()].mask.array
425 self.metadata.add(
"convolvedExposure",
"Template")
428 self.metadata.add(
"convolvedExposure",
"Science")
432 self.
log.
warning(
"Failed to match template. Checking coverage")
435 self.config.minTemplateFractionForExpectedSuccess,
436 exceptionMessage=
"Template coverage lower than expected to succeed."
437 f
" Failure is tolerable: {e}")
441 return subtractResults
444 """Convolve the template image with a PSF-matching kernel and subtract
445 from the science image.
449 template : `lsst.afw.image.ExposureF`
450 Template exposure, warped to match the science exposure.
451 science : `lsst.afw.image.ExposureF`
452 Science exposure to subtract from the template.
453 selectSources : `lsst.afw.table.SourceCatalog`
454 Identified sources on the science exposure. This catalog is used to
455 select sources in order to perform the AL PSF matching on stamp
460 results : `lsst.pipe.base.Struct`
462 ``difference`` : `lsst.afw.image.ExposureF`
463 Result of subtracting template and science.
464 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
465 Warped and PSF-matched template exposure.
466 ``backgroundModel`` : `lsst.afw.math.Function2D`
467 Background model that was fit while solving for the PSF-matching kernel
468 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
469 Kernel used to PSF-match the template to the science image.
472 kernelSources = self.makeKernel.selectKernelSources(template, science,
473 candidateList=selectSources,
475 kernelResult = self.makeKernel.
run(template, science, kernelSources,
477 except Exception
as e:
478 if self.config.allowKernelSourceDetection:
479 self.
log.
warning(
"Error encountered trying to construct the matching kernel"
480 f
" Running source detection and retrying. {e}")
481 kernelSources = self.makeKernel.selectKernelSources(template, science,
484 kernelResult = self.makeKernel.
run(template, science, kernelSources,
489 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
491 bbox=science.getBBox(),
493 photoCalib=science.photoCalib)
496 backgroundModel=(kernelResult.backgroundModel
497 if self.config.doSubtractBackground
else None))
498 correctedExposure = self.
finalize(template, science, difference,
499 kernelResult.psfMatchingKernel,
500 templateMatched=
True)
502 return lsst.pipe.base.Struct(difference=correctedExposure,
503 matchedTemplate=matchedTemplate,
504 matchedScience=science,
505 backgroundModel=kernelResult.backgroundModel,
506 psfMatchingKernel=kernelResult.psfMatchingKernel)
509 """Convolve the science image with a PSF-matching kernel and subtract
514 template : `lsst.afw.image.ExposureF`
515 Template exposure, warped to match the science exposure.
516 science : `lsst.afw.image.ExposureF`
517 Science exposure to subtract from the template.
518 selectSources : `lsst.afw.table.SourceCatalog`
519 Identified sources on the science exposure. This catalog is used to
520 select sources in order to perform the AL PSF matching on stamp
525 results : `lsst.pipe.base.Struct`
527 ``difference`` : `lsst.afw.image.ExposureF`
528 Result of subtracting template and science.
529 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
530 Warped template exposure. Note that in this case, the template
531 is not PSF-matched to the science image.
532 ``backgroundModel`` : `lsst.afw.math.Function2D`
533 Background model that was fit while solving for the PSF-matching kernel
534 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
535 Kernel used to PSF-match the science image to the template.
537 bbox = science.getBBox()
538 kernelSources = self.makeKernel.selectKernelSources(science, template,
539 candidateList=selectSources,
541 kernelResult = self.makeKernel.
run(science, template, kernelSources,
543 modelParams = kernelResult.backgroundModel.getParameters()
545 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
547 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
548 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
555 matchedScience.maskedImage /= norm
556 matchedTemplate = template.clone()[bbox]
557 matchedTemplate.maskedImage /= norm
558 matchedTemplate.setPhotoCalib(science.photoCalib)
561 backgroundModel=(kernelResult.backgroundModel
562 if self.config.doSubtractBackground
else None))
564 correctedExposure = self.
finalize(template, science, difference,
565 kernelResult.psfMatchingKernel,
566 templateMatched=
False)
568 return lsst.pipe.base.Struct(difference=correctedExposure,
569 matchedTemplate=matchedTemplate,
570 matchedScience=matchedScience,
571 backgroundModel=kernelResult.backgroundModel,
572 psfMatchingKernel=kernelResult.psfMatchingKernel,)
574 def finalize(self, template, science, difference, kernel,
575 templateMatched=True,
578 spatiallyVarying=False):
579 """Decorrelate the difference image to undo the noise correlations
580 caused by convolution.
584 template : `lsst.afw.image.ExposureF`
585 Template exposure, warped to match the science exposure.
586 science : `lsst.afw.image.ExposureF`
587 Science exposure to subtract from the template.
588 difference : `lsst.afw.image.ExposureF`
589 Result of subtracting template and science.
590 kernel : `lsst.afw.math.Kernel`
591 An (optionally spatially-varying) PSF matching kernel
592 templateMatched : `bool`, optional
593 Was the template PSF-matched to the science image?
594 preConvMode : `bool`, optional
595 Was the science image preconvolved with its own PSF
596 before PSF matching the template?
597 preConvKernel : `lsst.afw.detection.Psf`, optional
598 If not `None`, then the science image was pre-convolved with
599 (the reflection of) this kernel. Must be normalized to sum to 1.
600 spatiallyVarying : `bool`, optional
601 Compute the decorrelation kernel spatially varying across the image?
605 correctedExposure : `lsst.afw.image.ExposureF`
606 The decorrelated image difference.
608 if self.config.doDecorrelation:
609 self.
log.
info(
"Decorrelating image difference.")
613 correctedExposure = self.decorrelate.
run(science, template[science.getBBox()], difference, kernel,
614 templateMatched=templateMatched,
615 preConvMode=preConvMode,
616 preConvKernel=preConvKernel,
617 spatiallyVarying=spatiallyVarying).correctedExposure
619 self.
log.
info(
"NOT decorrelating image difference.")
620 correctedExposure = difference
621 return correctedExposure
625 """Check that the WCS of the two Exposures match, and the template bbox
626 contains the science bbox.
630 template : `lsst.afw.image.ExposureF`
631 Template exposure, warped to match the science exposure.
632 science : `lsst.afw.image.ExposureF`
633 Science exposure to subtract from the template.
638 Raised if the WCS of the template is not equal to the science WCS,
639 or if the science image is not fully contained in the template
642 assert template.wcs == science.wcs,\
643 "Template and science exposure WCS are not identical."
644 templateBBox = template.getBBox()
645 scienceBBox = science.getBBox()
647 assert templateBBox.contains(scienceBBox),\
648 "Template bbox does not contain all of the science image."
654 interpolateBadMaskPlanes=False,
656 """Convolve an exposure with the given kernel.
660 exposure : `lsst.afw.Exposure`
661 exposure to convolve.
662 kernel : `lsst.afw.math.LinearCombinationKernel`
663 PSF matching kernel computed in the ``makeKernel`` subtask.
664 convolutionControl : `lsst.afw.math.ConvolutionControl`
665 Configuration for convolve algorithm.
666 bbox : `lsst.geom.Box2I`, optional
667 Bounding box to trim the convolved exposure to.
668 psf : `lsst.afw.detection.Psf`, optional
669 Point spread function (PSF) to set for the convolved exposure.
670 photoCalib : `lsst.afw.image.PhotoCalib`, optional
671 Photometric calibration of the convolved exposure.
675 convolvedExp : `lsst.afw.Exposure`
678 convolvedExposure = exposure.clone()
680 convolvedExposure.setPsf(psf)
681 if photoCalib
is not None:
682 convolvedExposure.setPhotoCalib(photoCalib)
683 if interpolateBadMaskPlanes
and self.config.badMaskPlanes
is not None:
685 self.config.badMaskPlanes)
686 self.metadata.add(
"nInterpolated", nInterp)
687 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
689 convolvedExposure.setMaskedImage(convolvedImage)
691 return convolvedExposure
693 return convolvedExposure[bbox]
696 """Select sources from a catalog that meet the selection criteria.
700 sources : `lsst.afw.table.SourceCatalog`
701 Input source catalog to select sources from.
702 mask : `lsst.afw.image.Mask`
703 The image mask plane to use to reject sources
704 based on their location on the ccd.
708 selectSources : `lsst.afw.table.SourceCatalog`
709 The input source catalog, with flagged and low signal-to-noise
715 If there are too few sources to compute the PSF matching kernel
716 remaining after source selection.
718 flags = np.ones(
len(sources), dtype=bool)
719 for flag
in self.config.badSourceFlags:
721 flags *= ~sources[flag]
722 except Exception
as e:
723 self.
log.
warning(
"Could not apply source flag: %s", e)
724 signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr()
725 sToNFlag = signalToNoise > self.config.detectionThreshold
727 sToNFlagMax = signalToNoise < self.config.detectionThresholdMax
729 flags *= self.
_checkMask(mask, sources, self.config.excludeMaskPlanes)
730 selectSources = sources[flags].copy(deep=
True)
731 if (
len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
732 signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
733 indices = np.argsort(signalToNoise)
734 indices = indices[-self.config.maxKernelSources:]
735 flags = np.zeros(
len(selectSources), dtype=bool)
736 flags[indices] =
True
737 selectSources = selectSources[flags].copy(deep=
True)
739 self.
log.
info(
"%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
740 len(selectSources),
len(sources), 100*
len(selectSources)/
len(sources))
741 if len(selectSources) < self.config.minKernelSources:
742 self.
log.error(
"Too few sources to calculate the PSF matching kernel: "
743 "%i selected but %i needed for the calculation.",
744 len(selectSources), self.config.minKernelSources)
745 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
746 self.metadata.add(
"nPsfSources",
len(selectSources))
752 """Exclude sources that are located on masked pixels.
756 mask : `lsst.afw.image.Mask`
757 The image mask plane to use to reject sources
758 based on the location of their centroid on the ccd.
759 sources : `lsst.afw.table.SourceCatalog`
760 The source catalog to evaluate.
761 excludeMaskPlanes : `list` of `str`
762 List of the names of the mask planes to exclude.
766 flags : `numpy.ndarray` of `bool`
767 Array indicating whether each source in the catalog should be
768 kept (True) or rejected (False) based on the value of the
769 mask plane at its location.
771 setExcludeMaskPlanes = [
772 maskPlane
for maskPlane
in excludeMaskPlanes
if maskPlane
in mask.getMaskPlaneDict()
775 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
777 xv = np.rint(sources.getX() - mask.getX0())
778 yv = np.rint(sources.getY() - mask.getY0())
780 mv = mask.array[yv.astype(int), xv.astype(int)]
781 flags = np.bitwise_and(mv, excludePixelMask) == 0
785 """Perform preparatory calculations common to all Alard&Lupton Tasks.
789 template : `lsst.afw.image.ExposureF`
790 Template exposure, warped to match the science exposure. The
791 variance plane of the template image is modified in place.
792 science : `lsst.afw.image.ExposureF`
793 Science exposure to subtract from the template. The variance plane
794 of the science image is modified in place.
795 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
796 Exposure catalog with external calibrations to be applied. Catalog
797 uses the detector id for the catalog id, sorted on id for fast
801 if visitSummary
is not None:
804 template[science.getBBox()], self.
log,
805 requiredTemplateFraction=self.config.requiredTemplateFraction,
806 exceptionMessage=
"Not attempting subtraction. To force subtraction,"
807 " set config requiredTemplateFraction=0"
809 self.metadata.add(
"templateCoveragePercent", 100*templateCoverageFraction)
811 if self.config.doScaleVariance:
815 templateVarFactor = self.scaleVariance.
run(template.maskedImage)
816 sciVarFactor = self.scaleVariance.
run(science.maskedImage)
817 self.
log.
info(
"Template variance scaling factor: %.2f", templateVarFactor)
818 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
819 self.
log.
info(
"Science variance scaling factor: %.2f", sciVarFactor)
820 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
827 """Update the science and template mask planes before differencing.
831 template : `lsst.afw.image.Exposure`
832 Template exposure, warped to match the science exposure.
833 The template mask planes will be erased, except for a few specified
835 science : `lsst.afw.image.Exposure`
836 Science exposure to subtract from the template.
837 The DETECTED and DETECTED_NEGATIVE mask planes of the science image
840 self.
_clearMask(science.mask, clearMaskPlanes=[
"DETECTED",
"DETECTED_NEGATIVE"])
847 clearMaskPlanes = [mp
for mp
in template.mask.getMaskPlaneDict().keys()
848 if mp
not in self.config.preserveTemplateMask]
849 renameMaskPlanes = [mp
for mp
in self.config.renameTemplateMask
850 if mp
in template.mask.getMaskPlaneDict().keys()]
855 if "FAKE" in science.mask.getMaskPlaneDict().keys():
856 self.
log.
info(
"Adding injected mask plane to science image")
858 if "FAKE" in template.mask.getMaskPlaneDict().keys():
859 self.
log.
info(
"Adding injected mask plane to template image")
861 if "INJECTED" in renameMaskPlanes:
862 renameMaskPlanes.remove(
"INJECTED")
863 if "INJECTED_TEMPLATE" in clearMaskPlanes:
864 clearMaskPlanes.remove(
"INJECTED_TEMPLATE")
866 for maskPlane
in renameMaskPlanes:
868 self.
_clearMask(template.mask, clearMaskPlanes=clearMaskPlanes)
872 """Rename a mask plane by adding the new name and copying the data.
876 mask : `lsst.afw.image.Mask`
877 The mask image to update in place.
879 The name of the existing mask plane to copy.
881 The new name of the mask plane that will be added.
882 If the mask plane already exists, it will be updated in place.
884 mask.addMaskPlane(newMaskPlane)
885 originBitMask = mask.getPlaneBitMask(maskPlane)
886 destinationBitMask = mask.getPlaneBitMask(newMaskPlane)
887 mask.array |= ((mask.array & originBitMask) > 0)*destinationBitMask
890 """Clear the mask plane of the template.
894 mask : `lsst.afw.image.Mask`
895 The mask plane to erase, which will be modified in place.
896 clearMaskPlanes : `list` of `str`, optional
897 Erase the specified mask planes.
898 If not supplied, the entire mask will be erased.
900 if clearMaskPlanes
is None:
901 clearMaskPlanes = list(mask.getMaskPlaneDict().keys())
903 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
904 mask &= ~bitMaskToClear
908 SubtractScoreOutputConnections):
913 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
918 """Subtract a template from a science image, convolving the science image
919 before computing the kernel, and also convolving the template before
922 ConfigClass = AlardLuptonPreconvolveSubtractConfig
923 _DefaultName =
"alardLuptonPreconvolveSubtract"
925 def run(self, template, science, sources, visitSummary=None):
926 """Preconvolve the science image with its own PSF,
927 convolve the template image with a PSF-matching kernel and subtract
928 from the preconvolved science image.
932 template : `lsst.afw.image.ExposureF`
933 The template image, which has previously been warped to the science
934 image. The template bbox will be padded by a few pixels compared to
936 science : `lsst.afw.image.ExposureF`
937 The science exposure.
938 sources : `lsst.afw.table.SourceCatalog`
939 Identified sources on the science exposure. This catalog is used to
940 select sources in order to perform the AL PSF matching on stamp
942 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
943 Exposure catalog with complete external calibrations. Catalog uses
944 the detector id for the catalog id, sorted on id for fast lookup.
948 results : `lsst.pipe.base.Struct`
949 ``scoreExposure`` : `lsst.afw.image.ExposureF`
950 Result of subtracting the convolved template and science
951 images. Attached PSF is that of the original science image.
952 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
953 Warped and PSF-matched template exposure. Attached PSF is that
954 of the original science image.
955 ``matchedScience`` : `lsst.afw.image.ExposureF`
956 The science exposure after convolving with its own PSF.
957 Attached PSF is that of the original science image.
958 ``backgroundModel`` : `lsst.afw.math.Function2D`
959 Background model that was fit while solving for the
961 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
962 Final kernel used to PSF-match the template to the science
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 templateCoverageFraction: `float`
1080 Fraction of pixels in the template with data.
1084 lsst.pipe.base.NoWorkFound
1085 Raised if fraction of good pixels, defined as not having NO_DATA
1086 set, is less than the requiredTemplateFraction
1090 pixNoData = np.count_nonzero(templateExposure.mask.array
1091 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1092 pixGood = templateExposure.getBBox().getArea() - pixNoData
1093 templateCoverageFraction = pixGood/templateExposure.getBBox().getArea()
1094 logger.info(
"template has %d good pixels (%.1f%%)", pixGood, 100*templateCoverageFraction)
1096 if templateCoverageFraction < requiredTemplateFraction:
1097 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1098 100*templateCoverageFraction,
1099 100*requiredTemplateFraction))
1100 raise lsst.pipe.base.NoWorkFound(message +
" " + exceptionMessage)
1101 return templateCoverageFraction
1105 """Subtract template from science, propagating relevant metadata.
1109 science : `lsst.afw.Exposure`
1110 The input science image.
1111 template : `lsst.afw.Exposure`
1112 The template to subtract from the science image.
1113 backgroundModel : `lsst.afw.MaskedImage`, optional
1114 Differential background model
1118 difference : `lsst.afw.Exposure`
1119 The subtracted image.
1121 difference = science.clone()
1122 if backgroundModel
is not None:
1123 difference.maskedImage -= backgroundModel
1124 difference.maskedImage -= template.maskedImage
1129 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1133 exp1 : `~lsst.afw.image.Exposure`
1134 Exposure with the reference point spread function (PSF) to evaluate.
1135 exp2 : `~lsst.afw.image.Exposure`
1136 Exposure with a candidate point spread function (PSF) to evaluate.
1137 fwhmExposureBuffer : `float`
1138 Fractional buffer margin to be left out of all sides of the image
1139 during the construction of the grid to compute mean PSF FWHM in an
1140 exposure, if the PSF is not available at its average position.
1141 fwhmExposureGrid : `int`
1142 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1143 available at its average position.
1147 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1151 shape1 = getPsfFwhm(exp1.psf, average=
False)
1152 shape2 = getPsfFwhm(exp2.psf, average=
False)
1154 shape1 = evaluateMeanPsfFwhm(exp1,
1155 fwhmExposureBuffer=fwhmExposureBuffer,
1156 fwhmExposureGrid=fwhmExposureGrid
1158 shape2 = evaluateMeanPsfFwhm(exp2,
1159 fwhmExposureBuffer=fwhmExposureBuffer,
1160 fwhmExposureGrid=fwhmExposureGrid
1162 return shape1 <= shape2
1165 xTest = shape1[0] <= shape2[0]
1166 yTest = shape1[1] <= shape2[1]
1167 return xTest | yTest
1171 """Replace masked image pixels with interpolated values.
1175 maskedImage : `lsst.afw.image.MaskedImage`
1176 Image on which to perform interpolation.
1177 badMaskPlanes : `list` of `str`
1178 List of mask planes to interpolate over.
1179 fallbackValue : `float`, optional
1180 Value to set when interpolation fails.
1185 The number of masked pixels that were replaced.
1187 imgBadMaskPlanes = [
1188 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in maskedImage.mask.getMaskPlaneDict()
1191 image = maskedImage.image.array
1192 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1193 image[badPixels] = np.nan
1194 if fallbackValue
is None:
1195 fallbackValue = np.nanmedian(image)
1198 image[badPixels] = fallbackValue
1199 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, visitSummary=None)
_clearMask(self, mask, clearMaskPlanes=None)
_prepareInputs(self, template, science, visitSummary=None)
runConvolveTemplate(self, template, science, selectSources)
_applyExternalCalibrations(self, exposure, visitSummary)
updateMasks(self, template, science)
_sourceSelector(self, sources, mask)
_convolveExposure(self, exposure, kernel, convolutionControl, bbox=None, psf=None, photoCalib=None, interpolateBadMaskPlanes=False)
runConvolveScience(self, template, science, selectSources)
run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None)
finalize(self, template, science, difference, kernel, templateMatched=True, preConvMode=False, preConvKernel=None, spatiallyVarying=False)
_validateExposures(template, science)
_checkMask(mask, sources, excludeMaskPlanes)
_renameMaskPlanes(mask, maskPlane, newMaskPlane)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
_subtractImages(science, template, backgroundModel=None)
_interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None)
checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0., exceptionMessage="")
_shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid)