29from lsst.utils.introspection
import find_outside_stacklevel
31from lsst.meas.algorithms
import ScaleVarianceTask
36from .
import MakeKernelTask, DecorrelateALKernelTask
37from lsst.utils.timer
import timeMethod
39__all__ = [
"AlardLuptonSubtractConfig",
"AlardLuptonSubtractTask",
40 "AlardLuptonPreconvolveSubtractConfig",
"AlardLuptonPreconvolveSubtractTask"]
42_dimensions = (
"instrument",
"visit",
"detector")
43_defaultTemplates = {
"coaddName":
"deep",
"fakesType":
""}
47 dimensions=_dimensions,
48 defaultTemplates=_defaultTemplates):
49 template = connectionTypes.Input(
50 doc=
"Input warped template to subtract.",
51 dimensions=(
"instrument",
"visit",
"detector"),
52 storageClass=
"ExposureF",
53 name=
"{fakesType}{coaddName}Diff_templateExp"
55 science = connectionTypes.Input(
56 doc=
"Input science exposure to subtract from.",
57 dimensions=(
"instrument",
"visit",
"detector"),
58 storageClass=
"ExposureF",
59 name=
"{fakesType}calexp"
61 sources = connectionTypes.Input(
62 doc=
"Sources measured on the science exposure; "
63 "used to select sources for making the matching kernel.",
64 dimensions=(
"instrument",
"visit",
"detector"),
65 storageClass=
"SourceCatalog",
68 finalizedPsfApCorrCatalog = connectionTypes.Input(
69 doc=(
"Per-visit finalized psf models and aperture correction maps. "
70 "These catalogs use the detector id for the catalog id, "
71 "sorted on id for fast lookup."),
72 dimensions=(
"instrument",
"visit"),
73 storageClass=
"ExposureCatalog",
74 name=
"finalVisitSummary",
77 "Deprecated in favor of visitSummary. Will be removed after v26."
80 visitSummary = connectionTypes.Input(
81 doc=(
"Per-visit catalog with final calibration objects. "
82 "These catalogs use the detector id for the catalog id, "
83 "sorted on id for fast lookup."),
84 dimensions=(
"instrument",
"visit"),
85 storageClass=
"ExposureCatalog",
86 name=
"finalVisitSummary",
91 if not config.doApplyFinalizedPsf:
92 self.inputs.remove(
"finalizedPsfApCorrCatalog")
93 if not config.doApplyExternalCalibrations
or config.doApplyFinalizedPsf:
98 dimensions=_dimensions,
99 defaultTemplates=_defaultTemplates):
100 difference = connectionTypes.Output(
101 doc=
"Result of subtracting convolved template from science image.",
102 dimensions=(
"instrument",
"visit",
"detector"),
103 storageClass=
"ExposureF",
104 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
106 matchedTemplate = connectionTypes.Output(
107 doc=
"Warped and PSF-matched template used to create `subtractedExposure`.",
108 dimensions=(
"instrument",
"visit",
"detector"),
109 storageClass=
"ExposureF",
110 name=
"{fakesType}{coaddName}Diff_matchedExp",
115 dimensions=_dimensions,
116 defaultTemplates=_defaultTemplates):
117 scoreExposure = connectionTypes.Output(
118 doc=
"The maximum likelihood image, used for the detection of diaSources.",
119 dimensions=(
"instrument",
"visit",
"detector"),
120 storageClass=
"ExposureF",
121 name=
"{fakesType}{coaddName}Diff_scoreExp",
130 makeKernel = lsst.pex.config.ConfigurableField(
131 target=MakeKernelTask,
132 doc=
"Task to construct a matching kernel for convolution.",
134 doDecorrelation = lsst.pex.config.Field(
137 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
138 "kernel convolution? If True, also update the diffim PSF."
140 decorrelate = lsst.pex.config.ConfigurableField(
141 target=DecorrelateALKernelTask,
142 doc=
"Task to decorrelate the image difference.",
144 requiredTemplateFraction = lsst.pex.config.Field(
147 doc=
"Raise NoWorkFound and do not attempt image subtraction if template covers less than this "
148 " fraction of pixels. Setting to 0 will always attempt image subtraction."
150 minTemplateFractionForExpectedSuccess = lsst.pex.config.Field(
153 doc=
"Raise NoWorkFound if PSF-matching fails and template covers less than this fraction of pixels."
154 " If the fraction of pixels covered by the template is less than this value (and greater than"
155 " requiredTemplateFraction) this task is attempted but failure is anticipated and tolerated."
157 doScaleVariance = lsst.pex.config.Field(
160 doc=
"Scale variance of the image difference?"
162 scaleVariance = lsst.pex.config.ConfigurableField(
163 target=ScaleVarianceTask,
164 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
166 doSubtractBackground = lsst.pex.config.Field(
167 doc=
"Subtract the background fit when solving the kernel?",
171 doApplyFinalizedPsf = lsst.pex.config.Field(
172 doc=
"Replace science Exposure's psf and aperture correction map"
173 " with those in finalizedPsfApCorrCatalog.",
178 "Deprecated in favor of doApplyExternalCalibrations. "
179 "Will be removed after v26."
182 doApplyExternalCalibrations = lsst.pex.config.Field(
184 "Replace science Exposure's calibration objects with those"
185 " in visitSummary. Ignored if `doApplyFinalizedPsf is True."
190 detectionThreshold = lsst.pex.config.Field(
193 doc=
"Minimum signal to noise ratio of detected sources "
194 "to use for calculating the PSF matching kernel."
196 badSourceFlags = lsst.pex.config.ListField(
198 doc=
"Flags that, if set, the associated source should not "
199 "be used to determine the PSF matching kernel.",
200 default=(
"sky_source",
"slot_Centroid_flag",
201 "slot_ApFlux_flag",
"slot_PsfFlux_flag", ),
203 badMaskPlanes = lsst.pex.config.ListField(
205 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE",
"FAKE"),
206 doc=
"Mask planes to exclude when selecting sources for PSF matching."
208 preserveTemplateMask = lsst.pex.config.ListField(
210 default=(
"NO_DATA",
"BAD",
"SAT",
"FAKE",
"INJECTED",
"INJECTED_CORE"),
211 doc=
"Mask planes from the template to propagate to the image difference."
213 allowKernelSourceDetection = lsst.pex.config.Field(
216 doc=
"Re-run source detection for kernel candidates if an error is"
217 " encountered while calculating the matching kernel."
223 self.
makeKernel.kernel.active.spatialKernelOrder = 1
224 self.
makeKernel.kernel.active.spatialBgOrder = 2
228 pipelineConnections=AlardLuptonSubtractConnections):
229 mode = lsst.pex.config.ChoiceField(
231 default=
"convolveTemplate",
232 allowed={
"auto":
"Choose which image to convolve at runtime.",
233 "convolveScience":
"Only convolve the science image.",
234 "convolveTemplate":
"Only convolve the template image."},
235 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
240 """Compute the image difference of a science and template image using
241 the Alard & Lupton (1998) algorithm.
243 ConfigClass = AlardLuptonSubtractConfig
244 _DefaultName =
"alardLuptonSubtract"
248 self.makeSubtask(
"decorrelate")
249 self.makeSubtask(
"makeKernel")
250 if self.config.doScaleVariance:
251 self.makeSubtask(
"scaleVariance")
260 """Replace calibrations (psf, and ApCorrMap) on this exposure with
265 exposure : `lsst.afw.image.exposure.Exposure`
266 Input exposure to adjust calibrations.
267 visitSummary : `lsst.afw.table.ExposureCatalog`
268 Exposure catalog with external calibrations to be applied. Catalog
269 uses the detector id for the catalog id, sorted on id for fast
274 exposure : `lsst.afw.image.exposure.Exposure`
275 Exposure with adjusted calibrations.
277 detectorId = exposure.info.getDetector().getId()
279 row = visitSummary.find(detectorId)
281 self.
log.
warning(
"Detector id %s not found in external calibrations catalog; "
282 "Using original calibrations.", detectorId)
285 apCorrMap = row.getApCorrMap()
287 self.
log.
warning(
"Detector id %s has None for psf in "
288 "external calibrations catalog; Using original psf and aperture correction.",
290 elif apCorrMap
is None:
291 self.
log.
warning(
"Detector id %s has None for apCorrMap in "
292 "external calibrations catalog; Using original psf and aperture correction.",
296 exposure.info.setApCorrMap(apCorrMap)
301 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None,
303 """PSF match, subtract, and decorrelate two images.
307 template : `lsst.afw.image.ExposureF`
308 Template exposure, warped to match the science exposure.
309 science : `lsst.afw.image.ExposureF`
310 Science exposure to subtract from the template.
311 sources : `lsst.afw.table.SourceCatalog`
312 Identified sources on the science exposure. This catalog is used to
313 select sources in order to perform the AL PSF matching on stamp
315 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
316 Exposure catalog with finalized psf models and aperture correction
317 maps to be applied. Catalog uses the detector id for the catalog
318 id, sorted on id for fast lookup. Deprecated in favor of
319 ``visitSummary``, and will be removed after v26.
320 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
321 Exposure catalog with external calibrations to be applied. Catalog
322 uses the detector id for the catalog id, sorted on id for fast
323 lookup. Ignored (for temporary backwards compatibility) if
324 ``finalizedPsfApCorrCatalog`` is provided.
328 results : `lsst.pipe.base.Struct`
329 ``difference`` : `lsst.afw.image.ExposureF`
330 Result of subtracting template and science.
331 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
332 Warped and PSF-matched template exposure.
333 ``backgroundModel`` : `lsst.afw.math.Function2D`
334 Background model that was fit while solving for the
336 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
337 Kernel used to PSF-match the convolved image.
342 If an unsupported convolution mode is supplied.
344 If there are too few sources to calculate the PSF matching kernel.
345 lsst.pipe.base.NoWorkFound
346 Raised if fraction of good pixels, defined as not having NO_DATA
347 set, is less then the configured requiredTemplateFraction
350 if finalizedPsfApCorrCatalog
is not None:
352 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
353 "argument, and will be removed after v26.",
355 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
357 visitSummary = finalizedPsfApCorrCatalog
362 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
363 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
372 templatePsfSize = getPsfFwhm(template.psf)
373 sciencePsfSize = getPsfFwhm(science.psf)
375 self.
log.
info(
"Unable to evaluate PSF at the average position. "
376 "Evaluting PSF on a grid of points."
378 templatePsfSize = evaluateMeanPsfFwhm(template,
379 fwhmExposureBuffer=fwhmExposureBuffer,
380 fwhmExposureGrid=fwhmExposureGrid
382 sciencePsfSize = evaluateMeanPsfFwhm(science,
383 fwhmExposureBuffer=fwhmExposureBuffer,
384 fwhmExposureGrid=fwhmExposureGrid
386 self.
log.
info(
"Science PSF FWHM: %f pixels", sciencePsfSize)
387 self.
log.
info(
"Template PSF FWHM: %f pixels", templatePsfSize)
388 self.metadata.add(
"sciencePsfSize", sciencePsfSize)
389 self.metadata.add(
"templatePsfSize", templatePsfSize)
392 if self.config.mode ==
"auto":
395 fwhmExposureBuffer=fwhmExposureBuffer,
396 fwhmExposureGrid=fwhmExposureGrid)
398 if sciencePsfSize < templatePsfSize:
399 self.
log.
info(
"Average template PSF size is greater, "
400 "but science PSF greater in one dimension: convolving template image.")
402 self.
log.
info(
"Science PSF size is greater: convolving template image.")
404 self.
log.
info(
"Template PSF size is greater: convolving science image.")
405 elif self.config.mode ==
"convolveTemplate":
406 self.
log.
info(
"`convolveTemplate` is set: convolving template image.")
407 convolveTemplate =
True
408 elif self.config.mode ==
"convolveScience":
409 self.
log.
info(
"`convolveScience` is set: convolving science image.")
410 convolveTemplate =
False
412 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
416 self.metadata.add(
"convolvedExposure",
"Template")
419 self.metadata.add(
"convolvedExposure",
"Science")
423 self.
log.
warn(
"Failed to match template. Checking coverage")
426 self.config.minTemplateFractionForExpectedSuccess,
427 exceptionMessage=
"Template coverage lower than expected to succeed."
428 f
" Failure is tolerable: {e}")
432 return subtractResults
435 """Convolve the template image with a PSF-matching kernel and subtract
436 from the science image.
440 template : `lsst.afw.image.ExposureF`
441 Template exposure, warped to match the science exposure.
442 science : `lsst.afw.image.ExposureF`
443 Science exposure to subtract from the template.
444 selectSources : `lsst.afw.table.SourceCatalog`
445 Identified sources on the science exposure. This catalog is used to
446 select sources in order to perform the AL PSF matching on stamp
451 results : `lsst.pipe.base.Struct`
453 ``difference`` : `lsst.afw.image.ExposureF`
454 Result of subtracting template and science.
455 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
456 Warped and PSF-matched template exposure.
457 ``backgroundModel`` : `lsst.afw.math.Function2D`
458 Background model that was fit while solving for the PSF-matching kernel
459 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
460 Kernel used to PSF-match the template to the science image.
463 kernelSources = self.makeKernel.selectKernelSources(template, science,
464 candidateList=selectSources,
466 kernelResult = self.makeKernel.
run(template, science, kernelSources,
468 except Exception
as e:
469 if self.config.allowKernelSourceDetection:
470 self.
log.
warning(
"Error encountered trying to construct the matching kernel"
471 f
" Running source detection and retrying. {e}")
472 kernelSources = self.makeKernel.selectKernelSources(template, science,
475 kernelResult = self.makeKernel.
run(template, science, kernelSources,
480 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
482 bbox=science.getBBox(),
484 photoCalib=science.photoCalib)
487 backgroundModel=(kernelResult.backgroundModel
488 if self.config.doSubtractBackground
else None))
489 correctedExposure = self.
finalize(template, science, difference,
490 kernelResult.psfMatchingKernel,
491 templateMatched=
True)
493 return lsst.pipe.base.Struct(difference=correctedExposure,
494 matchedTemplate=matchedTemplate,
495 matchedScience=science,
496 backgroundModel=kernelResult.backgroundModel,
497 psfMatchingKernel=kernelResult.psfMatchingKernel)
500 """Convolve the science image with a PSF-matching kernel and subtract
505 template : `lsst.afw.image.ExposureF`
506 Template exposure, warped to match the science exposure.
507 science : `lsst.afw.image.ExposureF`
508 Science exposure to subtract from the template.
509 selectSources : `lsst.afw.table.SourceCatalog`
510 Identified sources on the science exposure. This catalog is used to
511 select sources in order to perform the AL PSF matching on stamp
516 results : `lsst.pipe.base.Struct`
518 ``difference`` : `lsst.afw.image.ExposureF`
519 Result of subtracting template and science.
520 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
521 Warped template exposure. Note that in this case, the template
522 is not PSF-matched to the science image.
523 ``backgroundModel`` : `lsst.afw.math.Function2D`
524 Background model that was fit while solving for the PSF-matching kernel
525 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
526 Kernel used to PSF-match the science image to the template.
528 bbox = science.getBBox()
529 kernelSources = self.makeKernel.selectKernelSources(science, template,
530 candidateList=selectSources,
532 kernelResult = self.makeKernel.
run(science, template, kernelSources,
534 modelParams = kernelResult.backgroundModel.getParameters()
536 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
538 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
539 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
546 matchedScience.maskedImage /= norm
547 matchedTemplate = template.clone()[bbox]
548 matchedTemplate.maskedImage /= norm
549 matchedTemplate.setPhotoCalib(science.photoCalib)
552 backgroundModel=(kernelResult.backgroundModel
553 if self.config.doSubtractBackground
else None))
555 correctedExposure = self.
finalize(template, science, difference,
556 kernelResult.psfMatchingKernel,
557 templateMatched=
False)
559 return lsst.pipe.base.Struct(difference=correctedExposure,
560 matchedTemplate=matchedTemplate,
561 matchedScience=matchedScience,
562 backgroundModel=kernelResult.backgroundModel,
563 psfMatchingKernel=kernelResult.psfMatchingKernel,)
565 def finalize(self, template, science, difference, kernel,
566 templateMatched=True,
569 spatiallyVarying=False):
570 """Decorrelate the difference image to undo the noise correlations
571 caused by convolution.
575 template : `lsst.afw.image.ExposureF`
576 Template exposure, warped to match the science exposure.
577 science : `lsst.afw.image.ExposureF`
578 Science exposure to subtract from the template.
579 difference : `lsst.afw.image.ExposureF`
580 Result of subtracting template and science.
581 kernel : `lsst.afw.math.Kernel`
582 An (optionally spatially-varying) PSF matching kernel
583 templateMatched : `bool`, optional
584 Was the template PSF-matched to the science image?
585 preConvMode : `bool`, optional
586 Was the science image preconvolved with its own PSF
587 before PSF matching the template?
588 preConvKernel : `lsst.afw.detection.Psf`, optional
589 If not `None`, then the science image was pre-convolved with
590 (the reflection of) this kernel. Must be normalized to sum to 1.
591 spatiallyVarying : `bool`, optional
592 Compute the decorrelation kernel spatially varying across the image?
596 correctedExposure : `lsst.afw.image.ExposureF`
597 The decorrelated image difference.
604 if self.config.doDecorrelation:
605 self.
log.
info(
"Decorrelating image difference.")
609 correctedExposure = self.decorrelate.
run(science, template[science.getBBox()], difference, kernel,
610 templateMatched=templateMatched,
611 preConvMode=preConvMode,
612 preConvKernel=preConvKernel,
613 spatiallyVarying=spatiallyVarying).correctedExposure
615 self.
log.
info(
"NOT decorrelating image difference.")
616 correctedExposure = difference
617 return correctedExposure
620 """Update the mask planes on images for finalizing."""
622 bbox = science.getBBox()
623 mask = difference.mask
624 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
626 if "FAKE" in science.mask.getMaskPlaneDict().keys():
631 self.
log.
info(
"Adding injected mask planes")
632 mask.addMaskPlane(
"INJECTED")
633 diffInjectedBitMask = mask.getPlaneBitMask(
"INJECTED")
635 mask.addMaskPlane(
"INJECTED_TEMPLATE")
636 diffInjTmpltBitMask = mask.getPlaneBitMask(
"INJECTED_TEMPLATE")
638 scienceFakeBitMask = science.mask.getPlaneBitMask(
'FAKE')
639 tmpltFakeBitMask = template[bbox].mask.getPlaneBitMask(
'FAKE')
641 injScienceMaskArray = ((science.mask.array & scienceFakeBitMask) > 0) * diffInjectedBitMask
642 injTemplateMaskArray = ((template[bbox].mask.array & tmpltFakeBitMask) > 0) * diffInjTmpltBitMask
644 mask.array |= injScienceMaskArray
645 mask.array |= injTemplateMaskArray
647 template[bbox].mask.array[...] = difference.mask.array[...]
651 """Check that the WCS of the two Exposures match, and the template bbox
652 contains the science bbox.
656 template : `lsst.afw.image.ExposureF`
657 Template exposure, warped to match the science exposure.
658 science : `lsst.afw.image.ExposureF`
659 Science exposure to subtract from the template.
664 Raised if the WCS of the template is not equal to the science WCS,
665 or if the science image is not fully contained in the template
668 assert template.wcs == science.wcs,\
669 "Template and science exposure WCS are not identical."
670 templateBBox = template.getBBox()
671 scienceBBox = science.getBBox()
673 assert templateBBox.contains(scienceBBox),\
674 "Template bbox does not contain all of the science image."
680 interpolateBadMaskPlanes=False,
682 """Convolve an exposure with the given kernel.
686 exposure : `lsst.afw.Exposure`
687 exposure to convolve.
688 kernel : `lsst.afw.math.LinearCombinationKernel`
689 PSF matching kernel computed in the ``makeKernel`` subtask.
690 convolutionControl : `lsst.afw.math.ConvolutionControl`
691 Configuration for convolve algorithm.
692 bbox : `lsst.geom.Box2I`, optional
693 Bounding box to trim the convolved exposure to.
694 psf : `lsst.afw.detection.Psf`, optional
695 Point spread function (PSF) to set for the convolved exposure.
696 photoCalib : `lsst.afw.image.PhotoCalib`, optional
697 Photometric calibration of the convolved exposure.
701 convolvedExp : `lsst.afw.Exposure`
704 convolvedExposure = exposure.clone()
706 convolvedExposure.setPsf(psf)
707 if photoCalib
is not None:
708 convolvedExposure.setPhotoCalib(photoCalib)
709 if interpolateBadMaskPlanes
and self.config.badMaskPlanes
is not None:
711 self.config.badMaskPlanes)
712 self.metadata.add(
"nInterpolated", nInterp)
713 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
715 convolvedExposure.setMaskedImage(convolvedImage)
717 return convolvedExposure
719 return convolvedExposure[bbox]
722 """Select sources from a catalog that meet the selection criteria.
726 sources : `lsst.afw.table.SourceCatalog`
727 Input source catalog to select sources from.
728 mask : `lsst.afw.image.Mask`
729 The image mask plane to use to reject sources
730 based on their location on the ccd.
734 selectSources : `lsst.afw.table.SourceCatalog`
735 The input source catalog, with flagged and low signal-to-noise
741 If there are too few sources to compute the PSF matching kernel
742 remaining after source selection.
744 flags = np.ones(
len(sources), dtype=bool)
745 for flag
in self.config.badSourceFlags:
747 flags *= ~sources[flag]
748 except Exception
as e:
749 self.
log.
warning(
"Could not apply source flag: %s", e)
750 sToNFlag = (sources.getPsfInstFlux()/sources.getPsfInstFluxErr()) > self.config.detectionThreshold
752 flags *= self.
_checkMask(mask, sources, self.config.badMaskPlanes)
753 selectSources = sources[flags]
754 self.
log.
info(
"%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
755 len(selectSources),
len(sources), 100*
len(selectSources)/
len(sources))
756 if len(selectSources) < self.config.makeKernel.nStarPerCell:
757 self.
log.error(
"Too few sources to calculate the PSF matching kernel: "
758 "%i selected but %i needed for the calculation.",
759 len(selectSources), self.config.makeKernel.nStarPerCell)
760 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
761 self.metadata.add(
"nPsfSources",
len(selectSources))
763 return selectSources.copy(deep=
True)
767 """Exclude sources that are located on masked pixels.
771 mask : `lsst.afw.image.Mask`
772 The image mask plane to use to reject sources
773 based on the location of their centroid on the ccd.
774 sources : `lsst.afw.table.SourceCatalog`
775 The source catalog to evaluate.
776 badMaskPlanes : `list` of `str`
777 List of the names of the mask planes to exclude.
781 flags : `numpy.ndarray` of `bool`
782 Array indicating whether each source in the catalog should be
783 kept (True) or rejected (False) based on the value of the
784 mask plane at its location.
787 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in mask.getMaskPlaneDict()
790 badPixelMask = mask.getPlaneBitMask(setBadMaskPlanes)
792 xv = np.rint(sources.getX() - mask.getX0())
793 yv = np.rint(sources.getY() - mask.getY0())
795 mv = mask.array[yv.astype(int), xv.astype(int)]
796 flags = np.bitwise_and(mv, badPixelMask) == 0
800 """Perform preparatory calculations common to all Alard&Lupton Tasks.
804 template : `lsst.afw.image.ExposureF`
805 Template exposure, warped to match the science exposure. The
806 variance plane of the template image is modified in place.
807 science : `lsst.afw.image.ExposureF`
808 Science exposure to subtract from the template. The variance plane
809 of the science image is modified in place.
810 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
811 Exposure catalog with external calibrations to be applied. Catalog
812 uses the detector id for the catalog id, sorted on id for fast
816 if visitSummary
is not None:
819 requiredTemplateFraction=self.config.requiredTemplateFraction,
820 exceptionMessage=
"Not attempting subtraction. To force subtraction,"
821 " set config requiredTemplateFraction=0")
823 if self.config.doScaleVariance:
827 templateVarFactor = self.scaleVariance.
run(template.maskedImage)
828 sciVarFactor = self.scaleVariance.
run(science.maskedImage)
829 self.
log.
info(
"Template variance scaling factor: %.2f", templateVarFactor)
830 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
831 self.
log.
info(
"Science variance scaling factor: %.2f", sciVarFactor)
832 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
836 """Clear the mask plane of the template.
840 template : `lsst.afw.image.ExposureF`
841 Template exposure, warped to match the science exposure.
842 The mask plane will be modified in place.
845 clearMaskPlanes = [maskplane
for maskplane
in mask.getMaskPlaneDict().keys()
846 if maskplane
not in self.config.preserveTemplateMask]
848 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
849 mask &= ~bitMaskToClear
853 SubtractScoreOutputConnections):
858 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
863 """Subtract a template from a science image, convolving the science image
864 before computing the kernel, and also convolving the template before
867 ConfigClass = AlardLuptonPreconvolveSubtractConfig
868 _DefaultName =
"alardLuptonPreconvolveSubtract"
870 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None):
871 """Preconvolve the science image with its own PSF,
872 convolve the template image with a PSF-matching kernel and subtract
873 from the preconvolved science image.
877 template : `lsst.afw.image.ExposureF`
878 The template image, which has previously been warped to the science
879 image. The template bbox will be padded by a few pixels compared to
881 science : `lsst.afw.image.ExposureF`
882 The science exposure.
883 sources : `lsst.afw.table.SourceCatalog`
884 Identified sources on the science exposure. This catalog is used to
885 select sources in order to perform the AL PSF matching on stamp
887 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
888 Exposure catalog with finalized psf models and aperture correction
889 maps to be applied. Catalog uses the detector id for the catalog
890 id, sorted on id for fast lookup. Deprecated in favor of
891 ``visitSummary``, and will be removed after v26.
892 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
893 Exposure catalog with complete external calibrations. Catalog uses
894 the detector id for the catalog id, sorted on id for fast lookup.
895 Ignored (for temporary backwards compatibility) if
896 ``finalizedPsfApCorrCatalog`` is provided.
900 results : `lsst.pipe.base.Struct`
901 ``scoreExposure`` : `lsst.afw.image.ExposureF`
902 Result of subtracting the convolved template and science
903 images. Attached PSF is that of the original science image.
904 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
905 Warped and PSF-matched template exposure. Attached PSF is that
906 of the original science image.
907 ``matchedScience`` : `lsst.afw.image.ExposureF`
908 The science exposure after convolving with its own PSF.
909 Attached PSF is that of the original science image.
910 ``backgroundModel`` : `lsst.afw.math.Function2D`
911 Background model that was fit while solving for the
913 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
914 Final kernel used to PSF-match the template to the science
917 if finalizedPsfApCorrCatalog
is not None:
919 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
920 "argument, and will be removed after v26.",
922 stacklevel=find_outside_stacklevel(
"lsst.ip.diffim"),
924 visitSummary = finalizedPsfApCorrCatalog
929 scienceKernel = science.psf.getKernel()
931 interpolateBadMaskPlanes=
True)
933 self.metadata.add(
"convolvedExposure",
"Preconvolution")
935 subtractResults = self.
runPreconvolve(template, science, matchedScience, selectSources, scienceKernel)
937 return subtractResults
939 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
940 """Convolve the science image with its own PSF, then convolve the
941 template with a matching kernel and subtract to form the Score
946 template : `lsst.afw.image.ExposureF`
947 Template exposure, warped to match the science exposure.
948 science : `lsst.afw.image.ExposureF`
949 Science exposure to subtract from the template.
950 matchedScience : `lsst.afw.image.ExposureF`
951 The science exposure, convolved with the reflection of its own PSF.
952 selectSources : `lsst.afw.table.SourceCatalog`
953 Identified sources on the science exposure. This catalog is used to
954 select sources in order to perform the AL PSF matching on stamp
956 preConvKernel : `lsst.afw.math.Kernel`
957 The reflection of the kernel that was used to preconvolve the
958 `science` exposure. Must be normalized to sum to 1.
962 results : `lsst.pipe.base.Struct`
964 ``scoreExposure`` : `lsst.afw.image.ExposureF`
965 Result of subtracting the convolved template and science
966 images. Attached PSF is that of the original science image.
967 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
968 Warped and PSF-matched template exposure. Attached PSF is that
969 of the original science image.
970 ``matchedScience`` : `lsst.afw.image.ExposureF`
971 The science exposure after convolving with its own PSF.
972 Attached PSF is that of the original science image.
973 ``backgroundModel`` : `lsst.afw.math.Function2D`
974 Background model that was fit while solving for the
976 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
977 Final kernel used to PSF-match the template to the science
980 bbox = science.getBBox()
981 innerBBox = preConvKernel.shrinkBBox(bbox)
983 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
984 candidateList=selectSources,
986 kernelResult = self.makeKernel.
run(template[innerBBox], matchedScience[innerBBox], kernelSources,
989 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
993 interpolateBadMaskPlanes=
True,
994 photoCalib=science.photoCalib)
996 backgroundModel=(kernelResult.backgroundModel
997 if self.config.doSubtractBackground
else None))
998 correctedScore = self.
finalize(template[bbox], science, score,
999 kernelResult.psfMatchingKernel,
1000 templateMatched=
True, preConvMode=
True,
1001 preConvKernel=preConvKernel)
1003 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1004 matchedTemplate=matchedTemplate,
1005 matchedScience=matchedScience,
1006 backgroundModel=kernelResult.backgroundModel,
1007 psfMatchingKernel=kernelResult.psfMatchingKernel)
1011 exceptionMessage=""):
1012 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1016 templateExposure : `lsst.afw.image.ExposureF`
1017 The template exposure to check
1018 logger : `lsst.log.Log`
1019 Logger for printing output.
1020 requiredTemplateFraction : `float`, optional
1021 Fraction of pixels of the science image required to have coverage
1023 exceptionMessage : `str`, optional
1024 Message to include in the exception raised if the template coverage
1029 lsst.pipe.base.NoWorkFound
1030 Raised if fraction of good pixels, defined as not having NO_DATA
1031 set, is less than the requiredTemplateFraction
1035 pixNoData = np.count_nonzero(templateExposure.mask.array
1036 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1037 pixGood = templateExposure.getBBox().getArea() - pixNoData
1038 logger.info(
"template has %d good pixels (%.1f%%)", pixGood,
1039 100*pixGood/templateExposure.getBBox().getArea())
1041 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
1042 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1043 100*pixGood/templateExposure.getBBox().getArea(),
1044 100*requiredTemplateFraction))
1045 raise lsst.pipe.base.NoWorkFound(message +
" " + exceptionMessage)
1049 """Subtract template from science, propagating relevant metadata.
1053 science : `lsst.afw.Exposure`
1054 The input science image.
1055 template : `lsst.afw.Exposure`
1056 The template to subtract from the science image.
1057 backgroundModel : `lsst.afw.MaskedImage`, optional
1058 Differential background model
1062 difference : `lsst.afw.Exposure`
1063 The subtracted image.
1065 difference = science.clone()
1066 if backgroundModel
is not None:
1067 difference.maskedImage -= backgroundModel
1068 difference.maskedImage -= template.maskedImage
1073 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1077 exp1 : `~lsst.afw.image.Exposure`
1078 Exposure with the reference point spread function (PSF) to evaluate.
1079 exp2 : `~lsst.afw.image.Exposure`
1080 Exposure with a candidate point spread function (PSF) to evaluate.
1081 fwhmExposureBuffer : `float`
1082 Fractional buffer margin to be left out of all sides of the image
1083 during the construction of the grid to compute mean PSF FWHM in an
1084 exposure, if the PSF is not available at its average position.
1085 fwhmExposureGrid : `int`
1086 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1087 available at its average position.
1091 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1095 shape1 = getPsfFwhm(exp1.psf, average=
False)
1096 shape2 = getPsfFwhm(exp2.psf, average=
False)
1098 shape1 = evaluateMeanPsfFwhm(exp1,
1099 fwhmExposureBuffer=fwhmExposureBuffer,
1100 fwhmExposureGrid=fwhmExposureGrid
1102 shape2 = evaluateMeanPsfFwhm(exp2,
1103 fwhmExposureBuffer=fwhmExposureBuffer,
1104 fwhmExposureGrid=fwhmExposureGrid
1106 return shape1 <= shape2
1109 xTest = shape1[0] <= shape2[0]
1110 yTest = shape1[1] <= shape2[1]
1111 return xTest | yTest
1115 """Replace masked image pixels with interpolated values.
1119 maskedImage : `lsst.afw.image.MaskedImage`
1120 Image on which to perform interpolation.
1121 badMaskPlanes : `list` of `str`
1122 List of mask planes to interpolate over.
1123 fallbackValue : `float`, optional
1124 Value to set when interpolation fails.
1129 The number of masked pixels that were replaced.
1131 imgBadMaskPlanes = [
1132 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in maskedImage.mask.getMaskPlaneDict()
1135 image = maskedImage.image.array
1136 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1137 image[badPixels] = np.nan
1138 if fallbackValue
is None:
1139 fallbackValue = np.nanmedian(image)
1142 image[badPixels] = fallbackValue
1143 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)
_checkMask(mask, sources, badMaskPlanes)
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)
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)