24from astropy
import units
as u
25from astropy.stats
import gaussian_fwhm_to_sigma
32from lsst.images
import DifferenceImage
33from lsst.images.convolution_kernels
import ImageBasisConvolutionKernel
35 computeDifferenceImageMetrics, computePSFNoiseEquivalentArea,
36 checkMask, record_from_obs_info, setSourceFootprints,
37 get_difference_image_provenance)
38from lsst.meas.algorithms
import ScaleVarianceTask, ScienceSourceSelectorTask
43from .
import MakeKernelTask, DecorrelateALKernelTask
44from lsst.utils.timer
import timeMethod
46__all__ = [
"AlardLuptonSubtractConfig",
"AlardLuptonSubtractTask",
47 "AlardLuptonPreconvolveSubtractConfig",
"AlardLuptonPreconvolveSubtractTask",
48 "SimplifiedSubtractConfig",
"SimplifiedSubtractTask",
49 "InsufficientKernelSourcesError"]
51_dimensions = (
"instrument",
"visit",
"detector")
52_defaultTemplates = {
"coaddName":
"deep",
"fakesType":
""}
56 """Raised when there are too few sources to calculate the PSF matching
60 msg = (f
"Only {nSources} sources were selected for PSF matching,"
61 f
" but {nRequired} are required.")
74 dimensions=_dimensions,
75 defaultTemplates=_defaultTemplates):
76 template = connectionTypes.Input(
77 doc=
"Input warped template to subtract."
78 " With image_type='future' this is read as a lsst.images.DifferenceImage, so that the"
79 " record of the coadds that went into it can be passed on to the outputs.",
80 dimensions=(
"instrument",
"visit",
"detector"),
81 storageClass=
"ExposureF",
82 name=
"{fakesType}{coaddName}Diff_templateExp"
84 science = connectionTypes.Input(
85 doc=
"Input science exposure to subtract from."
86 " With image_type='future' this is read as a lsst.images.VisitImage, so that its"
87 " photometric scaling can be passed on to the outputs.",
88 dimensions=(
"instrument",
"visit",
"detector"),
89 storageClass=
"ExposureF",
90 name=
"{fakesType}calexp"
92 sources = connectionTypes.Input(
93 doc=
"Sources measured on the science exposure; "
94 "used to select sources for making the matching kernel.",
95 dimensions=(
"instrument",
"visit",
"detector"),
96 storageClass=
"SourceCatalog",
99 visitSummary = connectionTypes.Input(
100 doc=(
"Per-visit catalog with final calibration objects. "
101 "These catalogs use the detector id for the catalog id, "
102 "sorted on id for fast lookup."),
103 dimensions=(
"instrument",
"visit"),
104 storageClass=
"ExposureCatalog",
105 name=
"finalVisitSummary",
110 if not config.doApplyExternalCalibrations:
112 if config.image_type ==
"future":
113 self.
template = dataclasses.replace(self.
template, storageClass=
"DifferenceImage")
114 self.
science = dataclasses.replace(self.
science, storageClass=
"VisitImage")
118 dimensions=_dimensions,
119 defaultTemplates=_defaultTemplates):
120 difference = connectionTypes.Output(
121 doc=
"Result of subtracting convolved template from science image.",
122 dimensions=(
"instrument",
"visit",
"detector"),
123 storageClass=
"ExposureF",
124 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
126 matchedTemplate = connectionTypes.Output(
127 doc=
"Warped and PSF-matched template used to create `subtractedExposure`.",
128 dimensions=(
"instrument",
"visit",
"detector"),
129 storageClass=
"ExposureF",
130 name=
"{fakesType}{coaddName}Diff_matchedExp",
132 psfMatchingKernel = connectionTypes.Output(
133 doc=
"Kernel used to PSF match the science and template images.",
134 dimensions=(
"instrument",
"visit",
"detector"),
135 storageClass=
"MatchingKernel",
136 name=
"{fakesType}{coaddName}Diff_psfMatchKernel",
138 kernelSources = connectionTypes.Output(
139 doc=
"Final selection of sources used for psf matching.",
140 dimensions=(
"instrument",
"visit",
"detector"),
141 storageClass=
"SourceCatalog",
142 name=
"{fakesType}{coaddName}Diff_psfMatchSources"
147 if config.image_type ==
"future":
150 storageClass=
"DifferenceImage")
152 storageClass=
"ConvolutionKernel")
156 dimensions=_dimensions,
157 defaultTemplates=_defaultTemplates):
158 scoreExposure = connectionTypes.Output(
159 doc=
"The maximum likelihood image, used for the detection of diaSources.",
160 dimensions=(
"instrument",
"visit",
"detector"),
161 storageClass=
"ExposureF",
162 name=
"{fakesType}{coaddName}Diff_scoreTempExp",
164 psfMatchingKernel = connectionTypes.Output(
165 doc=
"Kernel used to PSF match the science and template images.",
166 dimensions=(
"instrument",
"visit",
"detector"),
167 storageClass=
"MatchingKernel",
168 name=
"{fakesType}{coaddName}Diff_psfScoreMatchKernel",
170 kernelSources = connectionTypes.Output(
171 doc=
"Final selection of sources used for psf matching.",
172 dimensions=(
"instrument",
"visit",
"detector"),
173 storageClass=
"SourceCatalog",
174 name=
"{fakesType}{coaddName}Diff_psfScoreMatchSources"
179 if config.image_type ==
"future":
182 storageClass=
"ConvolutionKernel")
189class SimplifiedSubtractConnections(SubtractInputConnections, SubtractImageOutputConnections):
190 inputPsfMatchingKernel = connectionTypes.Input(
191 doc=
"Kernel used to PSF match the science and template images.",
192 dimensions=(
"instrument",
"visit",
"detector"),
193 storageClass=
"MatchingKernel",
194 name=
"{fakesType}{coaddName}Diff_psfMatchKernel",
200 if config.useExistingKernel:
203 if config.image_type ==
"future":
205 storageClass=
"ConvolutionKernel")
211 makeKernel = lsst.pex.config.ConfigurableField(
212 target=MakeKernelTask,
213 doc=
"Task to construct a matching kernel for convolution.",
215 doDecorrelation = lsst.pex.config.Field(
218 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
219 "kernel convolution? If True, also update the diffim PSF."
221 decorrelate = lsst.pex.config.ConfigurableField(
222 target=DecorrelateALKernelTask,
223 doc=
"Task to decorrelate the image difference.",
225 requiredTemplateFraction = lsst.pex.config.Field(
228 doc=
"Raise NoWorkFound and do not attempt image subtraction if template covers less than this "
229 " fraction of pixels. Setting to 0 will always attempt image subtraction."
231 minTemplateFractionForExpectedSuccess = lsst.pex.config.Field(
234 doc=
"Raise NoWorkFound if PSF-matching fails and template covers less than this fraction of pixels."
235 " If the fraction of pixels covered by the template is less than this value (and greater than"
236 " requiredTemplateFraction) this task is attempted but failure is anticipated and tolerated."
238 doScaleVariance = lsst.pex.config.Field(
241 doc=
"Scale variance of the science image? Note that the template variance is NOT scaled"
242 " here. The template variance may be scaled independently in ``GetTemplateTask``."
244 scaleVariance = lsst.pex.config.ConfigurableField(
245 target=ScaleVarianceTask,
246 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
248 doSubtractBackground = lsst.pex.config.Field(
249 doc=
"Subtract the background fit when solving the kernel? "
250 "It is generally better to instead subtract the background in detectAndMeasure.",
253 deprecated=
"Subtract the background in detectAndMeasure instead. Will be removed after v31.",
255 doApplyExternalCalibrations = lsst.pex.config.Field(
257 "Replace science Exposure's calibration objects with those"
258 " in visitSummary. Ignored if `doApplyFinalizedPsf is True."
263 sourceSelector = lsst.pex.config.ConfigurableField(
264 target=ScienceSourceSelectorTask,
265 doc=
"Task to select sources to be used for PSF matching.",
267 fallbackSourceSelector = lsst.pex.config.ConfigurableField(
268 target=ScienceSourceSelectorTask,
269 doc=
"Task to select sources to be used for PSF matching."
270 "Used only if the kernel calculation fails and"
271 "`allowKernelSourceDetection` is set. The fallback source detection"
272 " will not include all of the same plugins as the original source "
273 " detection, so not all of the same flags can be used.",
275 detectionThreshold = lsst.pex.config.Field(
278 doc=
"Minimum signal to noise ratio of detected sources "
279 "to use for calculating the PSF matching kernel.",
280 deprecated=
"No longer used. Will be removed after v30"
282 detectionThresholdMax = lsst.pex.config.Field(
285 doc=
"Maximum signal to noise ratio of detected sources "
286 "to use for calculating the PSF matching kernel.",
287 deprecated=
"No longer used. Will be removed after v30"
289 restrictKernelEdgeSources = lsst.pex.config.Field(
292 doc=
"Exclude sources close to the edge from the kernel calculation?"
294 maxKernelSources = lsst.pex.config.Field(
297 doc=
"Maximum number of sources to use for calculating the PSF matching kernel."
298 "Set to -1 to disable."
300 minKernelSources = lsst.pex.config.Field(
303 doc=
"Minimum number of sources needed for calculating the PSF matching kernel."
305 excludeMaskPlanes = lsst.pex.config.ListField(
307 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE",
"FAKE",
"HIGH_VARIANCE"),
308 doc=
"Template mask planes to exclude when selecting sources for PSF matching.",
310 badMaskPlanes = lsst.pex.config.ListField(
312 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE"),
313 doc=
"Mask planes to interpolate over."
315 preserveTemplateMask = lsst.pex.config.ListField(
317 default=(
"NO_DATA",
"BAD",
"HIGH_VARIANCE"),
318 doc=
"Mask planes from the template to propagate to the image difference."
320 renameTemplateMask = lsst.pex.config.ListField(
322 default=(
"SAT",
"INJECTED",
"INJECTED_CORE",),
323 doc=
"Mask planes from the template to propagate to the image difference"
324 "with '_TEMPLATE' appended to the name."
326 preserveMaskPlanes = lsst.pex.config.ListField(
328 default=(
"INJECTED",
"INJECTED_CORE",
"INJECTED_TEMPLATE",
"INJECTED_CORE_TEMPLATE"),
329 doc=
"Mask planes to preserve without dilation when convolving the image.",
331 allowKernelSourceDetection = lsst.pex.config.Field(
334 doc=
"Re-run source detection for kernel candidates if an error is"
335 " encountered while calculating the matching kernel."
337 image_type = lsst.pex.config.ChoiceField[str](
338 doc=
"Image type of the input template, the image outputs of this"
339 " task (difference and matchedTemplate, or scoreExposure), and the PSF"
342 "legacy":
"Read and write lsst.afw.image.ExposureF and"
343 " lsst.afw.math.Kernel.",
344 "future":
"Read and write lsst.images.DifferenceImage and"
345 " lsst.images.convolution_kernels.ConvolutionKernel.",
355 self.
makeKernel.kernel.active.fitForBackground =
True
356 self.
makeKernel.kernel.active.spatialKernelOrder = 1
357 self.
makeKernel.kernel.active.spatialBgOrder = 2
360 doSignalToNoise =
True
362 signalToNoiseMinimum = 10
363 signalToNoiseMaximum = 500
385 raise lsst.pex.config.FieldValidationError(
386 AlardLuptonSubtractBaseConfig.doSubtractBackground, self,
387 "The background subtracted when solving the kernel is not recorded on"
388 " image_type='future' outputs; subtract the background in detectAndMeasure instead."
393 pipelineConnections=AlardLuptonSubtractConnections):
394 mode = lsst.pex.config.ChoiceField(
396 default=
"convolveTemplate",
397 allowed={
"auto":
"Choose which image to convolve at runtime.",
398 "convolveScience":
"Only convolve the science image.",
399 "convolveTemplate":
"Only convolve the template image."},
400 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
405 """Compute the image difference of a science and template image using
406 the Alard & Lupton (1998) algorithm.
408 ConfigClass = AlardLuptonSubtractConfig
409 _DefaultName =
"alardLuptonSubtract"
410 usePreconvolution =
False
411 """Whether this task preconvolves the science image with its own PSF
412 before kernel-matching. Subclasses that preconvolve override this to
415 futureImageOutputs = (
"difference",
"matchedTemplate")
416 """Names of the images in the results struct that are converted in future
417 mode (`tuple` [`str`]). Subclasses with different image outputs override
422 self.makeSubtask(
"decorrelate")
423 self.makeSubtask(
"makeKernel")
424 self.makeSubtask(
"sourceSelector")
425 self.makeSubtask(
"fallbackSourceSelector")
426 if self.config.doScaleVariance:
427 self.makeSubtask(
"scaleVariance")
436 """Replace calibrations (psf, and ApCorrMap) on this exposure with
441 exposure : `lsst.afw.image.exposure.Exposure`
442 Input exposure to adjust calibrations.
443 visitSummary : `lsst.afw.table.ExposureCatalog`
444 Exposure catalog with external calibrations to be applied. Catalog
445 uses the detector id for the catalog id, sorted on id for fast
450 exposure : `lsst.afw.image.exposure.Exposure`
451 Exposure with adjusted calibrations.
453 detectorId = exposure.info.getDetector().getId()
455 row = visitSummary.find(detectorId)
457 self.
log.warning(
"Detector id %s not found in external calibrations catalog; "
458 "Using original calibrations.", detectorId)
461 apCorrMap = row.getApCorrMap()
463 self.
log.warning(
"Detector id %s has None for psf in "
464 "external calibrations catalog; Using original psf and aperture correction.",
466 elif apCorrMap
is None:
467 self.
log.warning(
"Detector id %s has None for apCorrMap in "
468 "external calibrations catalog; Using original psf and aperture correction.",
472 exposure.info.setApCorrMap(apCorrMap)
477 inputs = butlerQC.get(inputRefs)
479 exposureRecord =
None
480 photometricScaling =
None
481 if self.config.image_type ==
"future":
482 template = inputs[
"template"]
483 dataId = butlerQC.quantum.dataId
486 templateInfo = get_difference_image_provenance(template,
"templates",
"template", dataId)
487 exposureRecord = record_from_obs_info(template.obs_info, dataId[
"instrument"],
488 dataId[
"visit"], dataId.universe)
489 inputs[
"template"] = template.to_legacy()
490 photometricScaling = inputs[
"science"].photometric_scaling
491 inputs[
"science"] = inputs[
"science"].to_legacy()
492 if "inputPsfMatchingKernel" in inputs:
493 inputs[
"inputPsfMatchingKernel"] = inputs[
"inputPsfMatchingKernel"].to_legacy()
496 results = self.
run(**inputs)
497 except lsst.pipe.base.AlgorithmError
as e:
498 error = lsst.pipe.base.AnnotatedPartialOutputsError.annotate(e, self, log=self.
log)
502 if self.config.image_type ==
"future":
504 photometricScaling=photometricScaling)
506 butlerQC.put(results, outputRefs)
509 photometricScaling=None):
510 """Convert the image outputs to `lsst.images` types.
512 Each image named in `futureImageOutputs` that is present on
513 ``results`` is replaced by an `lsst.images.DifferenceImage`, with the
514 PSF matching kernel attached to it. ``psfMatchingKernel`` is replaced
515 by the same kernel as an
516 `lsst.images.convolution_kernels.ImageBasisConvolutionKernel`.
520 results : `lsst.pipe.base.Struct`
521 Output struct to read and modify in place.
522 exposureRecord : `lsst.daf.butler.DimensionRecord`
523 The ``exposure`` record of the observation, which supplies the
524 observation metadata recorded on each converted output.
525 templateInfo : `list` [`lsst.images.DifferenceImageTemplateInfo`], \
527 Record of the coadds that went into the template, taken from the
528 template this task subtracted.
529 photometricScaling : `lsst.images.fields.BaseField`, optional
530 Photometric scaling of the science image.
535 Raised if the PSF matching kernel cannot be stored on an
536 `lsst.images.DifferenceImage`.
538 Raised if an output has pixels set in a mask plane that
539 `lsst.images.get_legacy_difference_image_mask_planes` does not
544 This uses the default legacy mask plane map of
545 `lsst.images.get_legacy_difference_image_mask_planes` and adds any
546 planes from `~lsst.images.get_legacy_optional_mask_planes` that are set
547 for any pixels in the image.
551 science = results.matchedScience
553 kernel = ImageBasisConvolutionKernel.from_legacy(results.psfMatchingKernel)
554 except (TypeError, ValueError, AttributeError)
as e:
555 raise TypeError(
"The supplied PSF matching kernel cannot be attached to an"
556 f
" lsst.images.DifferenceImage: {e}")
from e
558 exposure = getattr(results, name,
None)
561 if exposure.getDetector()
is None:
564 exposure.setDetector(science.getDetector())
565 image = DifferenceImage.from_legacy(
568 exposure_record=exposureRecord,
570 if kernel
is not None:
571 image.kernel = kernel
573 image.templates = templateInfo
574 if photometricScaling
is not None:
575 image.photometric_scaling = photometricScaling
576 setattr(results, name, image)
577 results.psfMatchingKernel = kernel
580 def run(self, template, science, sources, visitSummary=None):
581 """PSF match, subtract, and decorrelate two images.
585 template : `lsst.afw.image.ExposureF`
586 Template exposure, warped to match the science exposure.
587 science : `lsst.afw.image.ExposureF`
588 Science exposure to subtract from the template.
589 sources : `lsst.afw.table.SourceCatalog`
590 Identified sources on the science exposure. This catalog is used to
591 select sources in order to perform the AL PSF matching on stamp
593 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
594 Exposure catalog with external calibrations to be applied. Catalog
595 uses the detector id for the catalog id, sorted on id for fast
600 results : `lsst.pipe.base.Struct`
601 ``difference`` : `lsst.afw.image.ExposureF`
602 Result of subtracting template and science.
603 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
604 Warped and PSF-matched template exposure.
605 ``backgroundModel`` : `lsst.afw.math.Function2D`
606 Background model that was fit while solving for the
608 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
609 Kernel used to PSF-match the convolved image.
610 ``kernelSources` : `lsst.afw.table.SourceCatalog`
611 Sources from the input catalog that were used to construct the
619 kernelResult = self.
runMakeKernel(template, science, sources=sources,
620 convolveTemplate=convolveTemplate,
621 runSourceDetection=
False)
623 if self.config.doSubtractBackground:
624 backgroundModel = kernelResult.backgroundModel
626 backgroundModel =
None
628 subtractResults = self.
runConvolveTemplate(template, science, kernelResult.psfMatchingKernel,
629 backgroundModel=backgroundModel)
631 subtractResults = self.
runConvolveScience(template, science, kernelResult.psfMatchingKernel,
632 backgroundModel=backgroundModel)
633 subtractResults.kernelSources = kernelResult.kernelSources
635 metrics = computeDifferenceImageMetrics(science, subtractResults.difference, sources)
637 self.metadata[
"differenceFootprintRatioMean"] = metrics.differenceFootprintRatioMean
638 self.metadata[
"differenceFootprintRatioStdev"] = metrics.differenceFootprintRatioStdev
639 self.metadata[
"differenceFootprintSkyRatioMean"] = metrics.differenceFootprintSkyRatioMean
640 self.metadata[
"differenceFootprintSkyRatioStdev"] = metrics.differenceFootprintSkyRatioStdev
641 self.
log.info(
"Mean, stdev of ratio of difference to science "
642 "pixels in star footprints: %5.4f, %5.4f",
643 self.metadata[
"differenceFootprintRatioMean"],
644 self.metadata[
"differenceFootprintRatioStdev"])
646 return subtractResults
649 """Determine whether the template should be convolved with the PSF
654 template : `lsst.afw.image.ExposureF`
655 Template exposure, warped to match the science exposure.
656 science : `lsst.afw.image.ExposureF`
657 Science exposure to subtract from the template.
661 convolveTemplate : `bool`
662 Convolve the template to match the two images?
667 If an unsupported convolution mode is supplied.
670 raise RuntimeError(
"Choosing a convolution method is incompatible with preconvolution!")
671 if self.config.mode ==
"auto":
674 fwhmExposureBuffer=self.config.makeKernel.fwhmExposureBuffer,
675 fwhmExposureGrid=self.config.makeKernel.fwhmExposureGrid)
678 self.
log.info(
"Average template PSF size is greater, "
679 "but science PSF greater in one dimension: convolving template image.")
681 self.
log.info(
"Science PSF size is greater: convolving template image.")
683 self.
log.info(
"Template PSF size is greater: convolving science image.")
684 elif self.config.mode ==
"convolveTemplate":
685 self.
log.info(
"`convolveTemplate` is set: convolving template image.")
686 convolveTemplate =
True
687 elif self.config.mode ==
"convolveScience":
688 self.
log.info(
"`convolveScience` is set: convolving science image.")
689 convolveTemplate =
False
691 raise RuntimeError(f
"Cannot handle AlardLuptonSubtract mode: {self.config.mode}")
692 return convolveTemplate
694 def runMakeKernel(self, template, science, sources=None, convolveTemplate=True, runSourceDetection=False):
695 """Construct the PSF-matching kernel. Not used for preconvolution.
699 template : `lsst.afw.image.ExposureF`
700 Template exposure, warped to match the science exposure.
701 science : `lsst.afw.image.ExposureF`
702 Science exposure to subtract from the template.
703 sources : `lsst.afw.table.SourceCatalog`
704 Identified sources on the science exposure. This catalog is used to
705 select sources in order to perform the AL PSF matching on stamp
707 Not used if ``runSourceDetection`` is set.
708 convolveTemplate : `bool`, optional
709 Construct the matching kernel to convolve the template?
710 runSourceDetection : `bool`, optional
711 Run a minimal version of source detection to determine kernel
712 candidates? If False, a source list to select kernel candidates
713 from must be supplied.
717 results : `lsst.pipe.base.Struct`
718 ``backgroundModel`` : `lsst.afw.math.Function2D`
719 Background model that was fit while solving for the
721 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
722 Kernel used to PSF-match the convolved image.
723 ``kernelSources` : `lsst.afw.table.SourceCatalog`
724 Sources from the input catalog that were used to construct the
728 raise RuntimeError(
"Incorrect matching kernel calculation configured. "
729 "`runMakeKernel` can't be called if `usePreconvolution` is set.")
741 if runSourceDetection:
745 kernelResult = self.makeKernel.
run(reference, target, kernelSources,
747 templateFwhmPix=referenceFwhmPix,
748 scienceFwhmPix=targetFwhmPix)
750 self.
log.warning(
"Failed to match template. Checking coverage")
753 self.config.minTemplateFractionForExpectedSuccess,
754 exceptionMessage=
"Template coverage lower than expected to succeed."
755 f
" Failure is tolerable: {e}")
759 return lsst.pipe.base.Struct(backgroundModel=kernelResult.backgroundModel,
760 psfMatchingKernel=kernelResult.psfMatchingKernel,
761 kernelSources=kernelSources)
764 """Run detection on the science image and use the template mask plane
765 to reject candidate sources.
769 template : `lsst.afw.image.ExposureF`
770 Template exposure, warped to match the science exposure.
771 science : `lsst.afw.image.ExposureF`
772 Science exposure to subtract from the template.
776 kernelSources : `lsst.afw.table.SourceCatalog`
777 Sources from the input catalog to use to construct the
780 kernelSize = self.makeKernel.makeKernelBasisList(
782 sources = self.makeKernel.makeCandidateList(template, science, kernelSize,
788 """Convolve the template image with a PSF-matching kernel and subtract
789 from the science image.
793 template : `lsst.afw.image.ExposureF`
794 Template exposure, warped to match the science exposure.
795 science : `lsst.afw.image.ExposureF`
796 Science exposure to subtract from the template.
797 psfMatchingKernel : `lsst.afw.math.Kernel`
798 Kernel to be used to PSF-match the science image to the template.
799 backgroundModel : `lsst.afw.math.Function2D`, optional
800 Background model that was fit while solving for the PSF-matching
805 results : `lsst.pipe.base.Struct`
807 ``difference`` : `lsst.afw.image.ExposureF`
808 Result of subtracting template and science.
809 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
810 Warped and PSF-matched template exposure.
811 ``backgroundModel`` : `lsst.afw.math.Function2D`
812 Background model that was fit while solving for the PSF-matching kernel
813 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
814 Kernel used to PSF-match the template to the science image.
816 self.metadata[
"convolvedExposure"] =
"Template"
820 bbox=science.getBBox(),
822 photoCalib=science.photoCalib)
824 difference =
_subtractImages(science, matchedTemplate, backgroundModel=backgroundModel)
825 correctedExposure = self.
finalize(template, science, difference,
827 templateMatched=
True)
829 return lsst.pipe.base.Struct(difference=correctedExposure,
830 matchedTemplate=matchedTemplate,
831 matchedScience=science,
832 backgroundModel=backgroundModel,
833 psfMatchingKernel=psfMatchingKernel)
836 """Convolve the science image with a PSF-matching kernel and subtract
841 template : `lsst.afw.image.ExposureF`
842 Template exposure, warped to match the science exposure.
843 science : `lsst.afw.image.ExposureF`
844 Science exposure to subtract from the template.
845 psfMatchingKernel : `lsst.afw.math.Kernel`
846 Kernel to be used to PSF-match the science image to the template.
847 backgroundModel : `lsst.afw.math.Function2D`, optional
848 Background model that was fit while solving for the PSF-matching
853 results : `lsst.pipe.base.Struct`
855 ``difference`` : `lsst.afw.image.ExposureF`
856 Result of subtracting template and science.
857 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
858 Warped template exposure. Note that in this case, the template
859 is not PSF-matched to the science image.
860 ``backgroundModel`` : `lsst.afw.math.Function2D`
861 Background model that was fit while solving for the PSF-matching kernel
862 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
863 Kernel used to PSF-match the science image to the template.
865 self.metadata[
"convolvedExposure"] =
"Science"
866 bbox = science.getBBox()
868 kernelImage = lsst.afw.image.ImageD(psfMatchingKernel.getDimensions())
869 xcen, ycen = bbox.getCenter()
870 norm = psfMatchingKernel.computeImage(kernelImage, doNormalize=
False, x=xcen, y=ycen)
877 matchedScience.maskedImage /= norm
878 matchedTemplate = template.clone()[bbox]
879 matchedTemplate.setPhotoCalib(science.photoCalib)
881 if backgroundModel
is not None:
883 invertedBackground = backgroundModel.clone()
884 invertedBackground.setParameters([-p
for p
in backgroundModel.getParameters()])
885 backgroundModel = invertedBackground
887 difference =
_subtractImages(matchedScience, matchedTemplate, backgroundModel=backgroundModel)
889 correctedExposure = self.
finalize(template, science, difference,
891 templateMatched=
False)
893 return lsst.pipe.base.Struct(difference=correctedExposure,
894 matchedTemplate=matchedTemplate,
895 matchedScience=matchedScience,
896 backgroundModel=backgroundModel,
897 psfMatchingKernel=psfMatchingKernel)
899 def finalize(self, template, science, difference, kernel,
900 templateMatched=True,
903 spatiallyVarying=False):
904 """Decorrelate the difference image to undo the noise correlations
905 caused by convolution.
909 template : `lsst.afw.image.ExposureF`
910 Template exposure, warped to match the science exposure.
911 science : `lsst.afw.image.ExposureF`
912 Science exposure to subtract from the template.
913 difference : `lsst.afw.image.ExposureF`
914 Result of subtracting template and science.
915 kernel : `lsst.afw.math.Kernel`
916 An (optionally spatially-varying) PSF matching kernel
917 templateMatched : `bool`, optional
918 Was the template PSF-matched to the science image?
919 preConvMode : `bool`, optional
920 Was the science image preconvolved with its own PSF
921 before PSF matching the template?
922 preConvKernel : `lsst.afw.detection.Psf`, optional
923 If not `None`, then the science image was pre-convolved with
924 (the reflection of) this kernel. Must be normalized to sum to 1.
925 spatiallyVarying : `bool`, optional
926 Compute the decorrelation kernel spatially varying across the image?
930 correctedExposure : `lsst.afw.image.ExposureF`
931 The decorrelated image difference.
933 if self.config.doDecorrelation:
934 self.
log.info(
"Decorrelating image difference.")
938 correctedExposure = self.decorrelate.
run(science, template[science.getBBox()], difference, kernel,
939 templateMatched=templateMatched,
940 preConvMode=preConvMode,
941 preConvKernel=preConvKernel,
942 spatiallyVarying=spatiallyVarying).correctedExposure
944 self.
log.info(
"NOT decorrelating image difference.")
945 correctedExposure = difference
946 return correctedExposure
949 """Calculate the point-source limiting magnitude of an exposure.
951 The limit is the flux of a point source detected at ``nsigma`` in
952 the median pixel noise, integrated over the effective area of the
953 PSF, so it does not depend on the units of the pixels.
957 exposure : `lsst.afw.image.Exposure`
958 The exposure to calculate the limiting magnitude for.
959 nsigma : `float`, optional
960 The detection threshold in sigma.
961 fallbackPsfSize : `float`, optional
962 PSF FWHM in pixels to use if the exposure PSF cannot be
968 The limiting AB magnitude, or `numpy.nan` if the exposure has no
969 photometric calibration, no usable pixels, or no PSF.
971 if exposure.photoCalib
is None:
974 psfArea = computePSFNoiseEquivalentArea(exposure.getPsf())
976 afwDetection.InvalidPsfError,
978 if fallbackPsfSize
is None:
979 self.
log.info(
"Unable to evaluate PSF, setting maglim to nan")
981 self.
log.info(
"Unable to evaluate PSF, using fallback FWHM %f", fallbackPsfSize)
983 psfArea = 4*np.pi*(fallbackPsfSize/np.sqrt(8*np.log(2)))**2
985 good = (mask.array & mask.getPlaneBitMask(self.config.badMaskPlanes)) == 0
986 variance = exposure.variance.array[good]
987 variance = variance[np.isfinite(variance) & (variance > 0)]
988 if variance.size == 0:
990 fluxLim = nsigma*np.sqrt(np.median(variance)*psfArea)
991 return exposure.photoCalib.instFluxToMagnitude(fluxLim)
995 """Check that the WCS of the two Exposures match, the template bbox
996 contains the science bbox, and that the bands match.
1000 template : `lsst.afw.image.ExposureF`
1001 Template exposure, warped to match the science exposure.
1002 science : `lsst.afw.image.ExposureF`
1003 Science exposure to subtract from the template.
1008 Raised if the WCS of the template is not equal to the science WCS,
1009 if the science image is not fully contained in the template
1010 bounding box, or if the bands do not match.
1012 assert template.wcs == science.wcs, \
1013 "Template and science exposure WCS are not identical."
1014 templateBBox = template.getBBox()
1015 scienceBBox = science.getBBox()
1016 assert science.filter.bandLabel == template.filter.bandLabel, \
1017 "Science and template exposures have different bands: %s, %s" % \
1018 (science.filter, template.filter)
1020 assert templateBBox.contains(scienceBBox), \
1021 "Template bbox does not contain all of the science image."
1027 interpolateBadMaskPlanes=False,
1029 """Convolve an exposure with the given kernel.
1033 exposure : `lsst.afw.Exposure`
1034 exposure to convolve.
1035 kernel : `lsst.afw.math.LinearCombinationKernel`
1036 PSF matching kernel computed in the ``makeKernel`` subtask.
1037 convolutionControl : `lsst.afw.math.ConvolutionControl`
1038 Configuration for convolve algorithm.
1039 bbox : `lsst.geom.Box2I`, optional
1040 Bounding box to trim the convolved exposure to.
1041 psf : `lsst.afw.detection.Psf`, optional
1042 Point spread function (PSF) to set for the convolved exposure.
1043 photoCalib : `lsst.afw.image.PhotoCalib`, optional
1044 Photometric calibration of the convolved exposure.
1045 interpolateBadMaskPlanes : `bool`, optional
1046 If set, interpolate over mask planes specified in
1047 ``config.badMaskPlanes`` before convolving the image.
1051 convolvedExp : `lsst.afw.Exposure`
1052 The convolved image.
1054 convolvedExposure = exposure.clone()
1056 convolvedExposure.setPsf(psf)
1057 if photoCalib
is not None:
1058 convolvedExposure.setPhotoCalib(photoCalib)
1059 if interpolateBadMaskPlanes
and self.config.badMaskPlanes
is not None:
1061 self.config.badMaskPlanes)
1062 self.metadata[
"nInterpolated"] = nInterp
1066 preservePlanes = [mp
for mp
in self.config.preserveMaskPlanes
1067 if mp
in convolvedExposure.mask.getMaskPlaneDict()]
1069 mp: (convolvedExposure.mask.array
1070 & convolvedExposure.mask.getPlaneBitMask(mp)) > 0
1071 for mp
in preservePlanes
1074 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
1076 convolvedExposure.setMaskedImage(convolvedImage)
1081 self.
_clearMask(convolvedExposure.mask, clearMaskPlanes=preservePlanes)
1082 for maskPlane, maskSetPixels
in maskResetDict.items():
1083 bit = convolvedExposure.mask.getPlaneBitMask(maskPlane)
1084 convolvedExposure.mask.array[maskSetPixels] |= bit
1087 return convolvedExposure
1089 return convolvedExposure[bbox]
1092 """Select sources from a catalog that meet the selection criteria.
1093 The selection criteria include any configured parameters of the
1094 `sourceSelector` subtask, as well as checking the science and template
1099 template : `lsst.afw.image.ExposureF`
1100 Template exposure, warped to match the science exposure.
1101 science : `lsst.afw.image.ExposureF`
1102 Science exposure to subtract from the template.
1103 sources : `lsst.afw.table.SourceCatalog`
1104 Input source catalog to select sources from.
1105 fallback : `bool`, optional
1106 Switch indicating the source selector is being called after
1107 running the fallback source detection subtask, which does not run a
1108 full set of measurement plugins and can't use the same settings for
1109 the source selector.
1113 kernelSources : `lsst.afw.table.SourceCatalog`
1114 The input source catalog, with flagged and low signal-to-noise
1115 sources removed and footprints added.
1119 InsufficientKernelSourcesError
1120 An AlgorithmError that is raised if there are not enough PSF
1121 candidates to construct the PSF matching kernel.
1124 selected = self.fallbackSourceSelector.selectSources(sources).selected
1126 selected = self.sourceSelector.selectSources(sources).selected
1131 selectSources = sources[selected].copy(deep=
True)
1133 kernelSources = setSourceFootprints(selectSources, kernelSize=kSize)
1134 bbox = science.getBBox()
1139 if self.config.restrictKernelEdgeSources:
1142 scienceSelected = checkMask(science.mask[bbox], kernelSources, self.config.excludeMaskPlanes)
1143 templateSelected = checkMask(template.mask[bbox], kernelSources, self.config.excludeMaskPlanes)
1144 maskSelected = scienceSelected & templateSelected
1145 kernelSources = kernelSources[maskSelected].copy(deep=
True)
1148 if (len(kernelSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
1149 signalToNoise = kernelSources.getPsfInstFlux()/kernelSources.getPsfInstFluxErr()
1150 indices = np.argsort(signalToNoise)
1151 indices = indices[-self.config.maxKernelSources:]
1152 selected = np.zeros(len(kernelSources), dtype=bool)
1153 selected[indices] =
True
1154 kernelSources = kernelSources[selected].copy(deep=
True)
1156 self.
log.info(
"%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
1157 len(kernelSources), len(sources), 100*len(kernelSources)/len(sources))
1158 if len(kernelSources) < self.config.minKernelSources:
1159 self.
log.error(
"Too few sources to calculate the PSF matching kernel: "
1160 "%i selected but %i needed for the calculation.",
1161 len(kernelSources), self.config.minKernelSources)
1162 if self.config.allowKernelSourceDetection
and not fallback:
1169 nRequired=self.config.minKernelSources)
1171 self.metadata[
"nPsfSources"] = len(kernelSources)
1173 return kernelSources
1176 """Perform preparatory calculations common to all Alard&Lupton Tasks.
1180 template : `lsst.afw.image.ExposureF`
1181 Template exposure, warped to match the science exposure. The
1182 variance plane of the template image is modified in place.
1183 science : `lsst.afw.image.ExposureF`
1184 Science exposure to subtract from the template. The variance plane
1185 of the science image is modified in place.
1186 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
1187 Exposure catalog with external calibrations to be applied. Catalog
1188 uses the detector id for the catalog id, sorted on id for fast
1192 if visitSummary
is not None:
1195 template[science.getBBox()], science, self.
log,
1196 requiredTemplateFraction=self.config.requiredTemplateFraction,
1197 exceptionMessage=
"Not attempting subtraction. To force subtraction,"
1198 " set config requiredTemplateFraction=0"
1200 self.metadata[
"templateCoveragePercent"] = 100*templateCoverageFraction
1202 if self.config.doScaleVariance:
1207 sciVarFactor = self.scaleVariance.
run(science.maskedImage)
1208 self.
log.info(
"Science variance scaling factor: %.2f", sciVarFactor)
1209 self.metadata[
"scaleScienceVarianceFactor"] = sciVarFactor
1235 self.
log.info(
"Unable to evaluate PSF at the average position. "
1236 "Evaluting PSF on a grid of points."
1240 fwhmExposureBuffer=self.config.makeKernel.fwhmExposureBuffer,
1241 fwhmExposureGrid=self.config.makeKernel.fwhmExposureGrid
1245 fwhmExposureBuffer=self.config.makeKernel.fwhmExposureBuffer,
1246 fwhmExposureGrid=self.config.makeKernel.fwhmExposureGrid
1255 if np.isnan(maglim_science):
1256 self.
log.warning(
"Limiting magnitude of the science image is NaN!")
1257 fluxlim_science = (maglim_science*u.ABmag).to_value(u.nJy)
1259 if np.isnan(maglim_template):
1260 self.
log.info(
"Cannot evaluate template limiting mag; adopting science limiting mag for diffim")
1261 maglim_diffim = maglim_science
1263 fluxlim_template = (maglim_template*u.ABmag).to_value(u.nJy)
1264 maglim_diffim = (np.sqrt(fluxlim_science**2 + fluxlim_template**2)*u.nJy).to(u.ABmag).value
1265 self.metadata[
"scienceLimitingMagnitude"] = maglim_science
1266 self.metadata[
"templateLimitingMagnitude"] = maglim_template
1267 self.metadata[
"diffimLimitingMagnitude"] = maglim_diffim
1270 """Update the science and template mask planes before differencing.
1274 template : `lsst.afw.image.Exposure`
1275 Template exposure, warped to match the science exposure.
1276 The template mask planes will be erased, except for a few specified
1278 science : `lsst.afw.image.Exposure`
1279 Science exposure to subtract from the template.
1280 The DETECTED and DETECTED_NEGATIVE mask planes of the science image
1283 self.
_clearMask(science.mask, clearMaskPlanes=[
"DETECTED",
"DETECTED_NEGATIVE"])
1290 renameMaskPlanes = [mp
for mp
in self.config.renameTemplateMask
1291 if mp
in template.mask.getMaskPlaneDict().keys()]
1295 renamedMaskPlanes = {maskPlane +
"_TEMPLATE" for maskPlane
in renameMaskPlanes}
1296 clearMaskPlanes = [mp
for mp
in template.mask.getMaskPlaneDict().keys()
1297 if mp
not in self.config.preserveTemplateMask
1298 and mp
not in renamedMaskPlanes]
1303 if "FAKE" in science.mask.getMaskPlaneDict().keys():
1304 self.
log.info(
"Adding injected mask plane to science image")
1306 if "FAKE" in template.mask.getMaskPlaneDict().keys():
1307 self.
log.info(
"Adding injected mask plane to template image")
1309 if "INJECTED" in renameMaskPlanes:
1310 renameMaskPlanes.remove(
"INJECTED")
1311 if "INJECTED_TEMPLATE" in clearMaskPlanes:
1312 clearMaskPlanes.remove(
"INJECTED_TEMPLATE")
1314 for maskPlane
in renameMaskPlanes:
1316 self.
_clearMask(template.mask, clearMaskPlanes=clearMaskPlanes)
1320 """Rename a mask plane by adding the new name and copying the data.
1324 mask : `lsst.afw.image.Mask`
1325 The mask image to update in place.
1327 The name of the existing mask plane to copy.
1328 newMaskPlane : `str`
1329 The new name of the mask plane that will be added.
1330 If the mask plane already exists, it will be updated in place.
1332 mask.addMaskPlane(newMaskPlane)
1333 originBitMask = mask.getPlaneBitMask(maskPlane)
1334 destinationBitMask = mask.getPlaneBitMask(newMaskPlane)
1335 mask.array |= ((mask.array & originBitMask) > 0)*destinationBitMask
1338 """Clear the mask plane of an exposure.
1342 mask : `lsst.afw.image.Mask`
1343 The mask plane to erase, which will be modified in place.
1344 clearMaskPlanes : `list` of `str`, optional
1345 Erase the specified mask planes.
1346 If not supplied, the entire mask will be erased.
1348 if clearMaskPlanes
is None:
1349 clearMaskPlanes = list(mask.getMaskPlaneDict().keys())
1351 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
1352 mask &= ~bitMaskToClear
1356 SubtractScoreOutputConnections):
1361 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
1366 """Subtract a template from a science image, convolving the science image
1367 before computing the kernel, and also convolving the template before
1370 ConfigClass = AlardLuptonPreconvolveSubtractConfig
1371 _DefaultName =
"alardLuptonPreconvolveSubtract"
1372 usePreconvolution =
True
1375 futureImageOutputs = (
"scoreExposure",)
1377 def run(self, template, science, sources, visitSummary=None):
1378 """Preconvolve the science image with its own PSF,
1379 convolve the template image with a PSF-matching kernel and subtract
1380 from the preconvolved science image.
1384 template : `lsst.afw.image.ExposureF`
1385 The template image, which has previously been warped to the science
1386 image. The template bbox will be padded by a few pixels compared to
1388 science : `lsst.afw.image.ExposureF`
1389 The science exposure.
1390 sources : `lsst.afw.table.SourceCatalog`
1391 Identified sources on the science exposure. This catalog is used to
1392 select sources in order to perform the AL PSF matching on stamp
1394 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
1395 Exposure catalog with complete external calibrations. Catalog uses
1396 the detector id for the catalog id, sorted on id for fast lookup.
1400 results : `lsst.pipe.base.Struct`
1401 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1402 Result of subtracting the convolved template and science
1403 images. Attached PSF is that of the original science image.
1404 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1405 Warped and PSF-matched template exposure. Attached PSF is that
1406 of the original science image.
1407 ``matchedScience`` : `lsst.afw.image.ExposureF`
1408 The science exposure after convolving with its own PSF.
1409 Attached PSF is that of the original science image.
1410 ``backgroundModel`` : `lsst.afw.math.Function2D`
1411 Background model that was fit while solving for the
1413 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1414 Final kernel used to PSF-match the template to the science
1417 self.
_prepareInputs(template, science, visitSummary=visitSummary)
1421 interpolateBadMaskPlanes=
True)
1422 self.metadata[
"convolvedExposure"] =
"Preconvolution"
1426 self.metadata[
"preconvolvedSciencePsfSize"] = self.
matchedPsfSize
1428 kernelSources = self.
_sourceSelector(template, matchedScience, sources)
1429 subtractResults = self.
runPreconvolve(template, science, matchedScience,
1430 kernelSources, convolutionKernel)
1433 self.
log.warning(
"Failed to match template. Checking coverage")
1436 self.config.minTemplateFractionForExpectedSuccess,
1437 exceptionMessage=
"Template coverage lower than expected to succeed."
1438 f
" Failure is tolerable: {e}")
1442 return subtractResults
1446 """Set the EDGE mask bit on pixels outside a known-valid region.
1450 mask : `~lsst.afw.image.Mask`
1451 Exposure mask that will be modified in place. Must have
1452 an ``EDGE`` mask plane.
1453 innerBBox : `~lsst.geom.Box2I`
1454 The valid inner region. Pixels
1455 outside this bbox will have their ``EDGE`` bit set.
1457 bbox = mask.getBBox()
1458 edgeBit = mask.getPlaneBitMask(
"EDGE")
1459 dx0 = innerBBox.getMinX() - bbox.getMinX()
1460 dx1 = bbox.getMaxX() - innerBBox.getMaxX()
1461 dy0 = innerBBox.getMinY() - bbox.getMinY()
1462 dy1 = bbox.getMaxY() - innerBBox.getMaxY()
1464 mask.array[:dy0, :] |= edgeBit
1466 mask.array[-dy1:, :] |= edgeBit
1468 mask.array[:, :dx0] |= edgeBit
1470 mask.array[:, -dx1:] |= edgeBit
1474 """Build a normalized, reflected matched-filter kernel from a PSF.
1476 Convolving an image with this kernel is equivalent to correlating
1477 the image with the PSF, so peaks in the output align with the PSF's
1478 centroid — even for asymmetric PSFs. The kernel is evaluated at the
1479 PSF's average position and returned as a constant
1480 `~lsst.afw.math.Kernel`.
1484 psf : `~lsst.afw.detection.Psf`
1485 The PSF to derive the preconvolution kernel from.
1489 kernel : `~lsst.afw.math.Kernel`
1490 The PSF reflected about both axes, normalized to sum to one.
1495 Raised if the PSF kernel has an even size along either axis.
1496 It's not possible to center an even-sized kernel.
1498 avgPos = psf.getAveragePosition()
1499 localKernel = psf.getLocalKernel(avgPos)
1500 dims = localKernel.getDimensions()
1501 if dims.x % 2 == 0
or dims.y % 2 == 0:
1503 f
"Preconvolution requires an odd-sized PSF kernel, got {dims.x}x{dims.y}. "
1505 kimg = lsst.afw.image.ImageD(dims)
1506 localKernel.computeImage(kimg, doNormalize=
True)
1509 kimg.array[...] = kimg.array[::-1, ::-1]
1512 def runPreconvolve(self, template, science, matchedScience, kernelSources, preConvKernel):
1513 """Convolve the science image with its own PSF, then convolve the
1514 template with a matching kernel and subtract to form the Score
1519 template : `lsst.afw.image.ExposureF`
1520 Template exposure, warped to match the science exposure.
1521 science : `lsst.afw.image.ExposureF`
1522 Science exposure to subtract from the template.
1523 matchedScience : `lsst.afw.image.ExposureF`
1524 The science exposure, convolved with the reflection of its own PSF.
1525 kernelSources : `lsst.afw.table.SourceCatalog`
1526 Identified sources on the science exposure. This catalog is used to
1527 select sources in order to perform the AL PSF matching on stamp
1529 preConvKernel : `lsst.afw.math.Kernel`
1530 The kernel that was used to preconvolve the ``science``
1531 exposure. Must be normalized to sum to 1.
1535 results : `lsst.pipe.base.Struct`
1537 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1538 Result of subtracting the convolved template and science
1539 images. Attached PSF is that of the original science image.
1540 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1541 Warped and PSF-matched template exposure. Attached PSF is that
1542 of the original science image.
1543 ``matchedScience`` : `lsst.afw.image.ExposureF`
1544 The science exposure after convolving with its own PSF.
1545 Attached PSF is that of the original science image.
1546 ``backgroundModel`` : `lsst.afw.math.Function2D`
1547 Background model that was fit while solving for the
1549 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1550 Final kernel used to PSF-match the template to the science
1553 bbox = science.getBBox()
1554 innerBBox = preConvKernel.shrinkBBox(bbox)
1556 kernelResult = self.makeKernel.
run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1561 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
1565 interpolateBadMaskPlanes=
True,
1566 photoCalib=science.photoCalib)
1568 backgroundModel=(kernelResult.backgroundModel
1569 if self.config.doSubtractBackground
else None))
1570 correctedScore = self.
finalize(template[bbox], science, score,
1571 kernelResult.psfMatchingKernel,
1572 templateMatched=
True, preConvMode=
True,
1573 preConvKernel=preConvKernel)
1578 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1579 matchedTemplate=matchedTemplate,
1580 matchedScience=matchedScience,
1581 backgroundModel=kernelResult.backgroundModel,
1582 psfMatchingKernel=kernelResult.psfMatchingKernel,
1583 kernelSources=kernelSources)
1587 exceptionMessage=""):
1588 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1592 templateExposure : `lsst.afw.image.ExposureF`
1593 The template exposure to check
1594 logger : `logging.Logger`
1595 Logger for printing output.
1596 requiredTemplateFraction : `float`, optional
1597 Fraction of pixels of the science image required to have coverage
1599 exceptionMessage : `str`, optional
1600 Message to include in the exception raised if the template coverage
1605 templateCoverageFraction: `float`
1606 Fraction of pixels in the template with data.
1610 lsst.pipe.base.NoWorkFound
1611 Raised if fraction of good pixels, defined as not having NO_DATA
1612 set, is less than the requiredTemplateFraction
1616 noTemplate = templateExposure.mask.array & templateExposure.mask.getPlaneBitMask(
'NO_DATA')
1619 noScience = scienceExposure.mask.array & scienceExposure.mask.getPlaneBitMask(
'NO_DATA')
1620 pixNoData = np.count_nonzero(noTemplate | noScience)
1621 pixGood = templateExposure.getBBox().getArea() - pixNoData
1622 templateCoverageFraction = pixGood/templateExposure.getBBox().getArea()
1623 logger.info(
"template has %d good pixels (%.1f%%)", pixGood, 100*templateCoverageFraction)
1625 if templateCoverageFraction < requiredTemplateFraction:
1626 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1627 100*templateCoverageFraction,
1628 100*requiredTemplateFraction))
1629 raise lsst.pipe.base.NoWorkFound(message +
" " + exceptionMessage)
1630 return templateCoverageFraction
1634 """Subtract template from science, propagating relevant metadata.
1638 science : `lsst.afw.Exposure`
1639 The input science image.
1640 template : `lsst.afw.Exposure`
1641 The template to subtract from the science image.
1642 backgroundModel : `lsst.afw.MaskedImage`, optional
1643 Differential background model
1647 difference : `lsst.afw.Exposure`
1648 The subtracted image.
1650 difference = science.clone()
1651 if backgroundModel
is not None:
1652 difference.maskedImage -= backgroundModel
1653 difference.maskedImage -= template.maskedImage
1658 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1662 exp1 : `~lsst.afw.image.Exposure`
1663 Exposure with the reference point spread function (PSF) to evaluate.
1664 exp2 : `~lsst.afw.image.Exposure`
1665 Exposure with a candidate point spread function (PSF) to evaluate.
1666 fwhmExposureBuffer : `float`
1667 Fractional buffer margin to be left out of all sides of the image
1668 during the construction of the grid to compute mean PSF FWHM in an
1669 exposure, if the PSF is not available at its average position.
1670 fwhmExposureGrid : `int`
1671 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1672 available at its average position.
1676 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1680 shape1 = getPsfFwhm(exp1.psf, average=
False)
1681 shape2 = getPsfFwhm(exp2.psf, average=
False)
1683 shape1 = evaluateMeanPsfFwhm(exp1,
1684 fwhmExposureBuffer=fwhmExposureBuffer,
1685 fwhmExposureGrid=fwhmExposureGrid
1687 shape2 = evaluateMeanPsfFwhm(exp2,
1688 fwhmExposureBuffer=fwhmExposureBuffer,
1689 fwhmExposureGrid=fwhmExposureGrid
1691 return shape1 <= shape2
1694 xTest = shape1[0] <= shape2[0]
1695 yTest = shape1[1] <= shape2[1]
1696 return xTest | yTest
1700 pipelineConnections=SimplifiedSubtractConnections):
1701 mode = lsst.pex.config.ChoiceField(
1703 default=
"convolveTemplate",
1704 allowed={
"auto":
"Choose which image to convolve at runtime.",
1705 "convolveScience":
"Only convolve the science image.",
1706 "convolveTemplate":
"Only convolve the template image."},
1707 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
1709 useExistingKernel = lsst.pex.config.Field(
1712 doc=
"Use a pre-existing PSF matching kernel?"
1713 "If False, source detection and measurement will be run."
1718 """Compute the image difference of a science and template image using
1719 the Alard & Lupton (1998) algorithm.
1721 ConfigClass = SimplifiedSubtractConfig
1722 _DefaultName =
"simplifiedSubtract"
1725 def run(self, template, science, visitSummary=None, inputPsfMatchingKernel=None):
1726 """PSF match, subtract, and decorrelate two images.
1730 template : `lsst.afw.image.ExposureF`
1731 Template exposure, warped to match the science exposure.
1732 science : `lsst.afw.image.ExposureF`
1733 Science exposure to subtract from the template.
1734 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
1735 Exposure catalog with external calibrations to be applied. Catalog
1736 uses the detector id for the catalog id, sorted on id for fast
1738 inputPsfMatchingKernel : `lsst.afw.math.Kernel`, optional
1739 Pre-existing PSF matching kernel to use for convolution.
1740 Required, and only used, if ``config.useExistingKernel`` is set.
1744 results : `lsst.pipe.base.Struct`
1745 ``difference`` : `lsst.afw.image.ExposureF`
1746 Result of subtracting template and science.
1747 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1748 Warped and PSF-matched template exposure.
1749 ``backgroundModel`` : `lsst.afw.math.Function2D`
1750 Background model that was fit while solving for the
1752 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1753 Kernel used to PSF-match the convolved image.
1754 ``kernelSources` : `lsst.afw.table.SourceCatalog`
1755 Sources detected on the science image that were used to
1756 construct the PSF-matching kernel.
1760 lsst.pipe.base.NoWorkFound
1761 Raised if fraction of good pixels, defined as not having NO_DATA
1762 set, is less then the configured requiredTemplateFraction
1764 self.
_prepareInputs(template, science, visitSummary=visitSummary)
1769 if self.config.useExistingKernel:
1770 psfMatchingKernel = inputPsfMatchingKernel
1771 backgroundModel =
None
1772 kernelSources =
None
1774 kernelResult = self.
runMakeKernel(template, science, convolveTemplate=convolveTemplate,
1775 runSourceDetection=
True)
1776 psfMatchingKernel = kernelResult.psfMatchingKernel
1777 kernelSources = kernelResult.kernelSources
1778 if self.config.doSubtractBackground:
1779 backgroundModel = kernelResult.backgroundModel
1781 backgroundModel =
None
1782 if convolveTemplate:
1784 backgroundModel=backgroundModel)
1787 backgroundModel=backgroundModel)
1788 if kernelSources
is not None:
1789 subtractResults.kernelSources = kernelSources
1790 return subtractResults
1794 """Replace masked image pixels with interpolated values.
1798 maskedImage : `lsst.afw.image.MaskedImage`
1799 Image on which to perform interpolation.
1800 badMaskPlanes : `list` of `str`
1801 List of mask planes to interpolate over.
1802 fallbackValue : `float`, optional
1803 Value to set when interpolation fails.
1808 The number of masked pixels that were replaced.
1810 imgBadMaskPlanes = [
1811 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in maskedImage.mask.getMaskPlaneDict()
1814 image = maskedImage.image.array
1815 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1816 image[badPixels] = np.nan
1817 if fallbackValue
is None:
1818 fallbackValue = np.nanmedian(image)
1821 image[badPixels] = fallbackValue
1822 return np.sum(badPixels)
_flagScoreEdge(mask, innerBBox)
run(self, template, science, sources, visitSummary=None)
runPreconvolve(self, template, science, matchedScience, kernelSources, preConvKernel)
_makePreconvolutionKernel(psf)
_clearMask(self, mask, clearMaskPlanes=None)
_prepareInputs(self, template, science, visitSummary=None)
chooseConvolutionMethod(self, template, science)
runConvolveTemplate(self, template, science, psfMatchingKernel, backgroundModel=None)
runQuantum(self, butlerQC, inputRefs, outputRefs)
run(self, template, science, sources, visitSummary=None)
runConvolveScience(self, template, science, psfMatchingKernel, backgroundModel=None)
_calculateMagLim(self, exposure, nsigma=5.0, fallbackPsfSize=None)
convert_outputs_to_future(self, results, exposureRecord, templateInfo=None, photometricScaling=None)
_applyExternalCalibrations(self, exposure, visitSummary)
updateMasks(self, template, science)
runMakeKernel(self, template, science, sources=None, convolveTemplate=True, runSourceDetection=False)
_convolveExposure(self, exposure, kernel, convolutionControl, bbox=None, psf=None, photoCalib=None, interpolateBadMaskPlanes=False)
_sourceSelector(self, template, science, sources, fallback=False)
runKernelSourceDetection(self, template, science)
finalize(self, template, science, difference, kernel, templateMatched=True, preConvMode=False, preConvKernel=None, spatiallyVarying=False)
_validateExposures(template, science)
_renameMaskPlanes(mask, maskPlane, newMaskPlane)
__init__(self, *, nSources, nRequired)
__init__(self, *, config=None)
run(self, template, science, visitSummary=None, inputPsfMatchingKernel=None)
__init__(self, *, config=None)
__init__(self, *, config=None)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
_interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None)
_subtractImages(science, template, backgroundModel=None)
checkTemplateIsSufficient(templateExposure, scienceExposure, logger, requiredTemplateFraction=0., exceptionMessage="")
_shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid)