32from .
import MakeKernelTask, DecorrelateALKernelTask
33from lsst.utils.timer
import timeMethod
35__all__ = [
"AlardLuptonSubtractConfig",
"AlardLuptonSubtractTask"]
37_dimensions = (
"instrument",
"visit",
"detector")
38_defaultTemplates = {
"coaddName":
"deep",
"fakesType":
""}
42 dimensions=_dimensions,
43 defaultTemplates=_defaultTemplates):
44 template = connectionTypes.Input(
45 doc=
"Input warped template to subtract.",
46 dimensions=(
"instrument",
"visit",
"detector"),
47 storageClass=
"ExposureF",
48 name=
"{fakesType}{coaddName}Diff_templateExp"
50 science = connectionTypes.Input(
51 doc=
"Input science exposure to subtract from.",
52 dimensions=(
"instrument",
"visit",
"detector"),
53 storageClass=
"ExposureF",
54 name=
"{fakesType}calexp"
56 sources = connectionTypes.Input(
57 doc=
"Sources measured on the science exposure; "
58 "used to select sources for making the matching kernel.",
59 dimensions=(
"instrument",
"visit",
"detector"),
60 storageClass=
"SourceCatalog",
63 finalizedPsfApCorrCatalog = connectionTypes.Input(
64 doc=(
"Per-visit finalized psf models and aperture correction maps. "
65 "These catalogs use the detector id for the catalog id, "
66 "sorted on id for fast lookup."),
67 dimensions=(
"instrument",
"visit"),
68 storageClass=
"ExposureCatalog",
69 name=
"finalized_psf_ap_corr_catalog",
74 dimensions=_dimensions,
75 defaultTemplates=_defaultTemplates):
76 difference = connectionTypes.Output(
77 doc=
"Result of subtracting convolved template from science image.",
78 dimensions=(
"instrument",
"visit",
"detector"),
79 storageClass=
"ExposureF",
80 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
82 matchedTemplate = connectionTypes.Output(
83 doc=
"Warped and PSF-matched template used to create `subtractedExposure`.",
84 dimensions=(
"instrument",
"visit",
"detector"),
85 storageClass=
"ExposureF",
86 name=
"{fakesType}{coaddName}Diff_matchedExp",
94 if not config.doApplyFinalizedPsf:
95 self.inputs.remove(
"finalizedPsfApCorrCatalog")
99 pipelineConnections=AlardLuptonSubtractConnections):
102 default=
"convolveTemplate",
103 allowed={
"auto":
"Choose which image to convolve at runtime.",
104 "convolveScience":
"Only convolve the science image.",
105 "convolveTemplate":
"Only convolve the template image."},
106 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
109 target=MakeKernelTask,
110 doc=
"Task to construct a matching kernel for convolution.",
115 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
116 "kernel convolution? If True, also update the diffim PSF."
119 target=DecorrelateALKernelTask,
120 doc=
"Task to decorrelate the image difference.",
125 doc=
"Abort task if template covers less than this fraction of pixels."
126 " Setting to 0 will always attempt image subtraction."
131 doc=
"Scale variance of the image difference?"
134 target=ScaleVarianceTask,
135 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
138 doc=
"Subtract the background fit when solving the kernel?",
143 doc=
"Replace science Exposure's psf and aperture correction map"
144 " with those in finalizedPsfApCorrCatalog.",
151 doc=
"Minimum signal to noise ration of detected sources "
152 "to use for calculating the PSF matching kernel."
156 doc=
"Flags that, if set, the associated source should not "
157 "be used to determine the PSF matching kernel.",
158 default=(
"sky_source",
"slot_Centroid_flag",
159 "slot_ApFlux_flag",
"slot_PsfFlux_flag", ),
165 doc=
"Set up and run diffim using settings that ensure the results"
166 "are compatible with the old version in pipe_tasks.",
167 deprecated=
"This option is only for backwards compatibility purposes"
168 " and will be removed after v24.",
174 self.
makeKernel.kernel.active.spatialKernelOrder = 1
175 self.
makeKernel.kernel.active.spatialBgOrder = 2
179 msg = f
"forceCompatibility=True requires mode='convolveTemplate', but mode was '{self.mode}'."
185 """Compute the image difference of a science and template image using
186 the Alard & Lupton (1998) algorithm.
188 ConfigClass = AlardLuptonSubtractConfig
189 _DefaultName = "alardLuptonSubtract"
193 self.makeSubtask(
"decorrelate")
194 self.makeSubtask(
"makeKernel")
195 if self.config.doScaleVariance:
196 self.makeSubtask(
"scaleVariance")
204 def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog):
205 """Replace calibrations (psf, and ApCorrMap) on this exposure with external ones.".
210 Input exposure to adjust calibrations.
212 Exposure catalog with finalized psf models
and aperture correction
213 maps to be applied
if config.doApplyFinalizedPsf=
True. Catalog uses
214 the detector id
for the catalog id, sorted on id
for fast lookup.
219 Exposure
with adjusted calibrations.
221 detectorId = exposure.info.getDetector().getId()
223 row = finalizedPsfApCorrCatalog.find(detectorId)
225 self.log.warning(
"Detector id %s not found in finalizedPsfApCorrCatalog; "
226 "Using original psf.", detectorId)
229 apCorrMap = row.getApCorrMap()
231 self.log.warning(
"Detector id %s has None for psf in "
232 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
234 elif apCorrMap
is None:
235 self.log.warning(
"Detector id %s has None for apCorrMap in "
236 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
240 exposure.info.setApCorrMap(apCorrMap)
245 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None):
246 """PSF match, subtract, and decorrelate two images.
250 template : `lsst.afw.image.ExposureF`
251 Template exposure, warped to match the science exposure.
252 science : `lsst.afw.image.ExposureF`
253 Science exposure to subtract from the template.
255 Identified sources on the science exposure. This catalog
is used to
256 select sources
in order to perform the AL PSF matching on stamp
259 Exposure catalog
with finalized psf models
and aperture correction
260 maps to be applied
if config.doApplyFinalizedPsf=
True. Catalog uses
261 the detector id
for the catalog id, sorted on id
for fast lookup.
265 results : `lsst.pipe.base.Struct`
266 ``difference`` : `lsst.afw.image.ExposureF`
267 Result of subtracting template
and science.
268 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
269 Warped
and PSF-matched template exposure.
270 ``backgroundModel`` : `lsst.afw.math.Function2D`
271 Background model that was fit
while solving
for the PSF-matching kernel
273 Kernel used to PSF-match the convolved image.
278 If an unsupported convolution mode
is supplied.
280 If there are too few sources to calculate the PSF matching kernel.
281 lsst.pipe.base.NoWorkFound
282 Raised
if fraction of good pixels, defined
as not having NO_DATA
283 set,
is less then the configured requiredTemplateFraction
286 if self.config.doApplyFinalizedPsf:
288 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog)
290 requiredTemplateFraction=self.config.requiredTemplateFraction)
291 if self.config.forceCompatibility:
294 self.log.warning(
"Running with `config.forceCompatibility=True`")
296 sciencePsfSize = getPsfFwhm(science.psf)
297 templatePsfSize = getPsfFwhm(template.psf)
298 self.log.info(
"Science PSF FWHM: %f pixels", sciencePsfSize)
299 self.log.info(
"Template PSF FWHM: %f pixels", templatePsfSize)
300 if self.config.mode ==
"auto":
301 convolveTemplate = _shapeTest(template.psf, science.psf)
303 if sciencePsfSize < templatePsfSize:
304 self.log.info(
"Average template PSF size is greater, "
305 "but science PSF greater in one dimension: convolving template image.")
307 self.log.info(
"Science PSF size is greater: convolving template image.")
309 self.log.info(
"Template PSF size is greater: convolving science image.")
310 elif self.config.mode ==
"convolveTemplate":
311 self.log.info(
"`convolveTemplate` is set: convolving template image.")
312 convolveTemplate =
True
313 elif self.config.mode ==
"convolveScience":
314 self.log.info(
"`convolveScience` is set: convolving science image.")
315 convolveTemplate =
False
317 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
319 photoCalib = template.getPhotoCalib()
320 self.log.info(
"Applying photometric calibration to template: %f", photoCalib.getCalibrationMean())
321 template.maskedImage = photoCalib.calibrateImage(template.maskedImage)
323 if self.config.doScaleVariance
and not self.config.forceCompatibility:
327 templateVarFactor = self.scaleVariance.run(template.maskedImage)
328 sciVarFactor = self.scaleVariance.run(science.maskedImage)
329 self.log.info(
"Template variance scaling factor: %.2f", templateVarFactor)
330 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
331 self.log.info(
"Science variance scaling factor: %.2f", sciVarFactor)
332 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
335 self.log.info(
"%i sources used out of %i from the input catalog", len(selectSources), len(sources))
336 if len(selectSources) < self.config.makeKernel.nStarPerCell:
337 self.log.warning(
"Too few sources to calculate the PSF matching kernel: "
338 "%i selected but %i needed for the calculation.",
339 len(selectSources), self.config.makeKernel.nStarPerCell)
340 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
346 if self.config.doScaleVariance
and self.config.forceCompatibility:
348 diffimVarFactor = self.scaleVariance.run(subtractResults.difference.maskedImage)
349 self.log.info(
"Diffim variance scaling factor: %.2f", diffimVarFactor)
350 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
352 return subtractResults
355 """Convolve the template image with a PSF-matching kernel and subtract
356 from the science image.
360 template : `lsst.afw.image.ExposureF`
361 Template exposure, warped to match the science exposure.
362 science : `lsst.afw.image.ExposureF`
363 Science exposure to subtract
from the template.
365 Identified sources on the science exposure. This catalog
is used to
366 select sources
in order to perform the AL PSF matching on stamp
371 results : `lsst.pipe.base.Struct`
373 ``difference`` : `lsst.afw.image.ExposureF`
374 Result of subtracting template
and science.
375 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
376 Warped
and PSF-matched template exposure.
377 ``backgroundModel`` : `lsst.afw.math.Function2D`
378 Background model that was fit
while solving
for the PSF-matching kernel
380 Kernel used to PSF-match the template to the science image.
382 if self.config.forceCompatibility:
385 template = template[science.getBBox()]
387 kernelSources = self.makeKernel.selectKernelSources(template, science,
388 candidateList=selectSources,
390 kernelResult = self.makeKernel.run(template, science, kernelSources,
393 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
395 bbox=science.getBBox(),
397 photoCalib=science.getPhotoCalib())
398 difference = _subtractImages(science, matchedTemplate,
399 backgroundModel=(kernelResult.backgroundModel
400 if self.config.doSubtractBackground
else None))
401 correctedExposure = self.
finalize(template, science, difference, kernelResult.psfMatchingKernel,
402 templateMatched=
True)
404 return lsst.pipe.base.Struct(difference=correctedExposure,
405 matchedTemplate=matchedTemplate,
406 matchedScience=science,
407 backgroundModel=kernelResult.backgroundModel,
408 psfMatchingKernel=kernelResult.psfMatchingKernel)
411 """Convolve the science image with a PSF-matching kernel and subtract the template image.
415 template : `lsst.afw.image.ExposureF`
416 Template exposure, warped to match the science exposure.
417 science : `lsst.afw.image.ExposureF`
418 Science exposure to subtract from the template.
420 Identified sources on the science exposure. This catalog
is used to
421 select sources
in order to perform the AL PSF matching on stamp
426 results : `lsst.pipe.base.Struct`
428 ``difference`` : `lsst.afw.image.ExposureF`
429 Result of subtracting template
and science.
430 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
431 Warped template exposure. Note that
in this case, the template
432 is not PSF-matched to the science image.
433 ``backgroundModel`` : `lsst.afw.math.Function2D`
434 Background model that was fit
while solving
for the PSF-matching kernel
436 Kernel used to PSF-match the science image to the template.
438 if self.config.forceCompatibility:
441 template = template[science.getBBox()]
442 kernelSources = self.makeKernel.selectKernelSources(science, template,
443 candidateList=selectSources,
445 kernelResult = self.makeKernel.run(science, template, kernelSources,
447 modelParams = kernelResult.backgroundModel.getParameters()
449 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
451 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
452 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
459 matchedScience.maskedImage /= norm
460 matchedTemplate = template.clone()[science.getBBox()]
461 matchedTemplate.maskedImage /= norm
462 matchedTemplate.setPhotoCalib(science.getPhotoCalib())
464 difference = _subtractImages(matchedScience, matchedTemplate,
465 backgroundModel=(kernelResult.backgroundModel
466 if self.config.doSubtractBackground
else None))
468 correctedExposure = self.
finalize(template, science, difference, kernelResult.psfMatchingKernel,
469 templateMatched=
False)
471 return lsst.pipe.base.Struct(difference=correctedExposure,
472 matchedTemplate=matchedTemplate,
473 matchedScience=matchedScience,
474 backgroundModel=kernelResult.backgroundModel,
475 psfMatchingKernel=kernelResult.psfMatchingKernel,)
477 def finalize(self, template, science, difference, kernel,
478 templateMatched=True,
481 spatiallyVarying=False):
482 """Decorrelate the difference image to undo the noise correlations
483 caused by convolution.
487 template : `lsst.afw.image.ExposureF`
488 Template exposure, warped to match the science exposure.
489 science : `lsst.afw.image.ExposureF`
490 Science exposure to subtract from the template.
491 difference : `lsst.afw.image.ExposureF`
492 Result of subtracting template
and science.
494 An (optionally spatially-varying) PSF matching kernel
495 templateMatched : `bool`, optional
496 Was the template PSF-matched to the science image?
497 preConvMode : `bool`, optional
498 Was the science image preconvolved
with its own PSF
499 before PSF matching the template?
501 If
not `
None`, then the science image was pre-convolved
with
502 (the reflection of) this kernel. Must be normalized to sum to 1.
503 spatiallyVarying : `bool`, optional
504 Compute the decorrelation kernel spatially varying across the image?
508 correctedExposure : `lsst.afw.image.ExposureF`
509 The decorrelated image difference.
513 mask = difference.mask
514 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
516 if self.config.doDecorrelation:
517 self.log.info(
"Decorrelating image difference.")
518 correctedExposure = self.decorrelate.run(science, template[science.getBBox()], difference, kernel,
519 templateMatched=templateMatched,
520 preConvMode=preConvMode,
521 preConvKernel=preConvKernel,
522 spatiallyVarying=spatiallyVarying).correctedExposure
524 self.log.info(
"NOT decorrelating image difference.")
525 correctedExposure = difference
526 return correctedExposure
529 def _validateExposures(template, science):
530 """Check that the WCS of the two Exposures match, and the template bbox
531 contains the science bbox.
535 template : `lsst.afw.image.ExposureF`
536 Template exposure, warped to match the science exposure.
537 science : `lsst.afw.image.ExposureF`
538 Science exposure to subtract from the template.
543 Raised
if the WCS of the template
is not equal to the science WCS,
544 or if the science image
is not fully contained
in the template
547 assert template.wcs == science.wcs,\
548 "Template and science exposure WCS are not identical."
549 templateBBox = template.getBBox()
550 scienceBBox = science.getBBox()
552 assert templateBBox.contains(scienceBBox),\
553 "Template bbox does not contain all of the science image."
556 def _convolveExposure(exposure, kernel, convolutionControl,
560 """Convolve an exposure with the given kernel.
564 exposure : `lsst.afw.Exposure`
565 exposure to convolve.
567 PSF matching kernel computed in the ``makeKernel`` subtask.
569 Configuration
for convolve algorithm.
571 Bounding box to trim the convolved exposure to.
573 Point spread function (PSF) to set
for the convolved exposure.
575 Photometric calibration of the convolved exposure.
579 convolvedExp : `lsst.afw.Exposure`
582 convolvedExposure = exposure.clone()
584 convolvedExposure.setPsf(psf)
585 if photoCalib
is not None:
586 convolvedExposure.setPhotoCalib(photoCalib)
587 convolvedImage = lsst.afw.image.MaskedImageF(exposure.getBBox())
589 convolvedExposure.setMaskedImage(convolvedImage)
591 return convolvedExposure
593 return convolvedExposure[bbox]
595 def _sourceSelector(self, sources):
596 """Select sources from a catalog that meet the selection criteria.
601 Input source catalog to select sources from.
606 The source catalog filtered to include only the selected sources.
608 flags = [True, ]*len(sources)
609 for flag
in self.config.badSourceFlags:
611 flags *= ~sources[flag]
612 except Exception
as e:
613 self.log.warning(
"Could not apply source flag: %s", e)
614 sToNFlag = (sources.getPsfInstFlux()/sources.getPsfInstFluxErr()) > self.config.detectionThreshold
616 selectSources = sources[flags]
618 return selectSources.copy(deep=
True)
622 """Raise NoWorkFound if template coverage < requiredTemplateFraction
626 templateExposure : `lsst.afw.image.ExposureF`
627 The template exposure to check
629 Logger for printing output.
630 requiredTemplateFraction : `float`, optional
631 Fraction of pixels of the science image required to have coverage
636 lsst.pipe.base.NoWorkFound
637 Raised
if fraction of good pixels, defined
as not having NO_DATA
638 set,
is less then the configured requiredTemplateFraction
642 pixNoData = np.count_nonzero(templateExposure.mask.array
643 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
644 pixGood = templateExposure.getBBox().getArea() - pixNoData
645 logger.info(
"template has %d good pixels (%.1f%%)", pixGood,
646 100*pixGood/templateExposure.getBBox().getArea())
648 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
649 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
650 "To force subtraction, set config requiredTemplateFraction=0." % (
651 100*pixGood/templateExposure.getBBox().getArea(),
652 100*requiredTemplateFraction))
653 raise lsst.pipe.base.NoWorkFound(message)
656def _subtractImages(science, template, backgroundModel=None):
657 """Subtract template from science, propagating relevant metadata.
661 science : `lsst.afw.Exposure`
662 The input science image.
663 template : `lsst.afw.Exposure`
664 The template to subtract from the science image.
665 backgroundModel : `lsst.afw.MaskedImage`, optional
666 Differential background model
670 difference : `lsst.afw.Exposure`
671 The subtracted image.
673 difference = science.clone()
674 if backgroundModel
is not None:
675 difference.maskedImage -= backgroundModel
676 difference.maskedImage -= template.maskedImage
680def _shapeTest(psf1, psf2):
681 """Determine whether psf1 is narrower in either dimension than psf2.
686 Reference point spread function (PSF) to evaluate.
688 Candidate point spread function (PSF) to evaluate.
693 Returns True if psf1
is narrower than psf2
in either dimension.
695 shape1 = getPsfFwhm(psf1, average=False)
696 shape2 = getPsfFwhm(psf2, average=
False)
697 xTest = shape1[0] < shape2[0]
698 yTest = shape1[1] < shape2[1]
A polymorphic base class for representing an image's Point Spread Function.
The photometric calibration of an exposure.
Parameters to control convolution.
Kernels are used for convolution with MaskedImages and (eventually) Images.
A kernel that is a linear combination of fixed basis kernels.
Custom catalog class for ExposureRecord/Table.
An integer coordinate rectangle.
def __init__(self, *config=None)
def finalize(self, template, science, difference, kernel, templateMatched=True, preConvMode=False, preConvKernel=None, spatiallyVarying=False)
def _convolveExposure(exposure, kernel, convolutionControl, bbox=None, psf=None, photoCalib=None)
def runConvolveTemplate(self, template, science, selectSources)
def _sourceSelector(self, sources)
def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog)
def _validateExposures(template, science)
def __init__(self, **kwargs)
def runConvolveScience(self, template, science, selectSources)
This static class includes a variety of methods for interacting with the the logging module.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.)