28from lsst.meas.algorithms
import ScaleVarianceTask
32from .
import MakeKernelTask, DecorrelateALKernelTask
34__all__ = [
"AlardLuptonSubtractConfig",
"AlardLuptonSubtractTask"]
36_dimensions = (
"instrument",
"visit",
"detector")
37_defaultTemplates = {
"coaddName":
"deep",
"fakesType":
""}
41 dimensions=_dimensions,
42 defaultTemplates=_defaultTemplates):
43 template = connectionTypes.Input(
44 doc=
"Input warped template to subtract.",
45 dimensions=(
"instrument",
"visit",
"detector"),
46 storageClass=
"ExposureF",
47 name=
"{fakesType}{coaddName}Diff_templateExp"
49 science = connectionTypes.Input(
50 doc=
"Input science exposure to subtract from.",
51 dimensions=(
"instrument",
"visit",
"detector"),
52 storageClass=
"ExposureF",
53 name=
"{fakesType}calexp"
55 sources = connectionTypes.Input(
56 doc=
"Sources measured on the science exposure; "
57 "used to select sources for making the matching kernel.",
58 dimensions=(
"instrument",
"visit",
"detector"),
59 storageClass=
"SourceCatalog",
62 finalizedPsfApCorrCatalog = connectionTypes.Input(
63 doc=(
"Per-visit finalized psf models and aperture correction maps. "
64 "These catalogs use the detector id for the catalog id, "
65 "sorted on id for fast lookup."),
66 dimensions=(
"instrument",
"visit"),
67 storageClass=
"ExposureCatalog",
68 name=
"finalized_psf_ap_corr_catalog",
73 dimensions=_dimensions,
74 defaultTemplates=_defaultTemplates):
75 difference = connectionTypes.Output(
76 doc=
"Result of subtracting convolved template from science image.",
77 dimensions=(
"instrument",
"visit",
"detector"),
78 storageClass=
"ExposureF",
79 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
81 matchedTemplate = connectionTypes.Output(
82 doc=
"Warped and PSF-matched template used to create `subtractedExposure`.",
83 dimensions=(
"instrument",
"visit",
"detector"),
84 storageClass=
"ExposureF",
85 name=
"{fakesType}{coaddName}Diff_matchedExp",
93 if not config.doApplyFinalizedPsf:
94 self.inputs.remove(
"finalizedPsfApCorrCatalog")
98 pipelineConnections=AlardLuptonSubtractConnections):
99 mode = lsst.pex.config.ChoiceField(
102 allowed={
"auto":
"Choose which image to convolve at runtime.",
103 "convolveScience":
"Only convolve the science image.",
104 "convolveTemplate":
"Only convolve the template image."},
105 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
107 makeKernel = lsst.pex.config.ConfigurableField(
108 target=MakeKernelTask,
109 doc=
"Task to construct a matching kernel for convolution.",
111 doDecorrelation = lsst.pex.config.Field(
114 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
115 "kernel convolution? If True, also update the diffim PSF."
117 decorrelate = lsst.pex.config.ConfigurableField(
118 target=DecorrelateALKernelTask,
119 doc=
"Task to decorrelate the image difference.",
121 requiredTemplateFraction = lsst.pex.config.Field(
124 doc=
"Abort task if template covers less than this fraction of pixels."
125 " Setting to 0 will always attempt image subtraction."
127 doScaleVariance = lsst.pex.config.Field(
130 doc=
"Scale variance of the image difference?"
132 scaleVariance = lsst.pex.config.ConfigurableField(
133 target=ScaleVarianceTask,
134 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
136 doSubtractBackground = lsst.pex.config.Field(
137 doc=
"Subtract the background fit when solving the kernel?",
141 doApplyFinalizedPsf = lsst.pex.config.Field(
142 doc=
"Replace science Exposure's psf and aperture correction map"
143 " with those in finalizedPsfApCorrCatalog.",
148 forceCompatibility = lsst.pex.config.Field(
151 doc=
"Set up and run diffim using settings that ensure the results"
152 "are compatible with the old version in pipe_tasks.",
153 deprecated=
"This option is only for backwards compatibility purposes"
154 " and will be removed after v24.",
160 self.
makeKernel.kernel.active.spatialKernelOrder = 1
161 self.
makeKernel.kernel.active.spatialBgOrder = 2
165 self.
mode =
"convolveTemplate"
169 """Compute the image difference of a science and template image using
170 the Alard & Lupton (1998) algorithm.
172 ConfigClass = AlardLuptonSubtractConfig
173 _DefaultName = "alardLuptonSubtract"
177 self.makeSubtask(
"decorrelate")
178 self.makeSubtask(
"makeKernel")
179 if self.config.doScaleVariance:
180 self.makeSubtask(
"scaleVariance")
188 def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog):
189 """Replace calibrations (psf, and ApCorrMap) on this exposure with external ones.".
193 exposure : `lsst.afw.image.exposure.Exposure`
194 Input exposure to adjust calibrations.
196 Exposure catalog with finalized psf models
and aperture correction
197 maps to be applied
if config.doApplyFinalizedPsf=
True. Catalog uses
198 the detector id
for the catalog id, sorted on id
for fast lookup.
202 exposure : `lsst.afw.image.exposure.Exposure`
203 Exposure
with adjusted calibrations.
205 detectorId = exposure.info.getDetector().getId()
207 row = finalizedPsfApCorrCatalog.find(detectorId)
209 self.log.warning(
"Detector id %s not found in finalizedPsfApCorrCatalog; "
210 "Using original psf.", detectorId)
213 apCorrMap = row.getApCorrMap()
215 self.log.warning(
"Detector id %s has None for psf in "
216 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
218 elif apCorrMap
is None:
219 self.log.warning(
"Detector id %s has None for apCorrMap in "
220 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
224 exposure.info.setApCorrMap(apCorrMap)
228 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None):
229 """PSF match, subtract, and decorrelate two images.
233 template : `lsst.afw.image.ExposureF`
234 Template exposure, warped to match the science exposure.
235 science : `lsst.afw.image.ExposureF`
236 Science exposure to subtract from the template.
238 Identified sources on the science exposure. This catalog
is used to
239 select sources
in order to perform the AL PSF matching on stamp
242 Exposure catalog
with finalized psf models
and aperture correction
243 maps to be applied
if config.doApplyFinalizedPsf=
True. Catalog uses
244 the detector id
for the catalog id, sorted on id
for fast lookup.
248 results : `lsst.pipe.base.Struct`
249 ``difference`` : `lsst.afw.image.ExposureF`
250 Result of subtracting template
and science.
251 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
252 Warped
and PSF-matched template exposure.
253 ``backgroundModel`` : `lsst.afw.math.Chebyshev1Function2D`
254 Background model that was fit
while solving
for the PSF-matching kernel
256 Kernel used to PSF-match the convolved image.
261 If an unsupported convolution mode
is supplied.
262 lsst.pipe.base.NoWorkFound
263 Raised
if fraction of good pixels, defined
as not having NO_DATA
264 set,
is less then the configured requiredTemplateFraction
267 if self.config.doApplyFinalizedPsf:
269 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog)
271 requiredTemplateFraction=self.config.requiredTemplateFraction)
272 if self.config.forceCompatibility:
275 self.log.warning(
"Running with `config.forceCompatibility=True`")
279 self.log.info(
"Science PSF size: %f", sciencePsfSize)
280 self.log.info(
"Template PSF size: %f", templatePsfSize)
281 if self.config.mode ==
"auto":
282 if sciencePsfSize < templatePsfSize:
283 self.log.info(
"Template PSF size is greater: convolving science image.")
284 convolveTemplate =
False
286 self.log.info(
"Science PSF size is greater: convolving template image.")
287 convolveTemplate =
True
288 elif self.config.mode ==
"convolveTemplate":
289 self.log.info(
"`convolveTemplate` is set: convolving template image.")
290 convolveTemplate =
True
291 elif self.config.mode ==
"convolveScience":
292 self.log.info(
"`convolveScience` is set: convolving science image.")
293 convolveTemplate =
False
295 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
297 if self.config.doScaleVariance
and ~self.config.forceCompatibility:
301 templateVarFactor = self.scaleVariance.
run(template.maskedImage)
302 sciVarFactor = self.scaleVariance.
run(science.maskedImage)
303 self.log.info(
"Template variance scaling factor: %.2f", templateVarFactor)
304 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
305 self.log.info(
"Science variance scaling factor: %.2f", sciVarFactor)
306 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
308 kernelSources = self.makeKernel.selectKernelSources(template, science,
309 candidateList=sources,
316 if self.config.doScaleVariance
and self.config.forceCompatibility:
318 diffimVarFactor = self.scaleVariance.
run(subtractResults.difference.maskedImage)
319 self.log.info(
"Diffim variance scaling factor: %.2f", diffimVarFactor)
320 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
322 return subtractResults
325 """Convolve the template image with a PSF-matching kernel and subtract
326 from the science image.
330 template : `lsst.afw.image.ExposureF`
331 Template exposure, warped to match the science exposure.
332 science : `lsst.afw.image.ExposureF`
333 Science exposure to subtract
from the template.
335 Identified sources on the science exposure. This catalog
is used to
336 select sources
in order to perform the AL PSF matching on stamp
341 results : `lsst.pipe.base.Struct`
343 ``difference`` : `lsst.afw.image.ExposureF`
344 Result of subtracting template
and science.
345 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
346 Warped
and PSF-matched template exposure.
347 ``backgroundModel`` : `lsst.afw.math.Chebyshev1Function2D`
348 Background model that was fit
while solving
for the PSF-matching kernel
350 Kernel used to PSF-match the template to the science image.
352 if self.config.forceCompatibility:
355 template = template[science.getBBox()]
356 kernelResult = self.makeKernel.
run(template, science, sources, preconvolved=
False)
358 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
360 bbox=science.getBBox(),
362 photoCalib=science.getPhotoCalib())
363 difference = _subtractImages(science, matchedTemplate,
364 backgroundModel=(kernelResult.backgroundModel
365 if self.config.doSubtractBackground
else None))
366 correctedExposure = self.
finalize(template, science, difference, kernelResult.psfMatchingKernel,
367 templateMatched=
True)
369 return lsst.pipe.base.Struct(difference=correctedExposure,
370 matchedTemplate=matchedTemplate,
371 matchedScience=science,
372 backgroundModel=kernelResult.backgroundModel,
373 psfMatchingKernel=kernelResult.psfMatchingKernel)
376 """Convolve the science image with a PSF-matching kernel and subtract the template image.
380 template : `lsst.afw.image.ExposureF`
381 Template exposure, warped to match the science exposure.
382 science : `lsst.afw.image.ExposureF`
383 Science exposure to subtract from the template.
385 Identified sources on the science exposure. This catalog
is used to
386 select sources
in order to perform the AL PSF matching on stamp
391 results : `lsst.pipe.base.Struct`
393 ``difference`` : `lsst.afw.image.ExposureF`
394 Result of subtracting template
and science.
395 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
396 Warped template exposure. Note that
in this case, the template
397 is not PSF-matched to the science image.
398 ``backgroundModel`` : `lsst.afw.math.Chebyshev1Function2D`
399 Background model that was fit
while solving
for the PSF-matching kernel
401 Kernel used to PSF-match the science image to the template.
403 if self.config.forceCompatibility:
406 template = template[science.getBBox()]
407 kernelResult = self.makeKernel.
run(science, template, sources, preconvolved=
False)
408 modelParams = kernelResult.backgroundModel.getParameters()
410 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
412 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
413 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
420 matchedScience.maskedImage /= norm
421 matchedTemplate = template.clone()[science.getBBox()]
422 matchedTemplate.maskedImage /= norm
423 matchedTemplate.setPhotoCalib(science.getPhotoCalib())
425 difference = _subtractImages(matchedScience, matchedTemplate,
426 backgroundModel=(kernelResult.backgroundModel
427 if self.config.doSubtractBackground
else None))
429 correctedExposure = self.
finalize(template, science, difference, kernelResult.psfMatchingKernel,
430 templateMatched=
False)
432 return lsst.pipe.base.Struct(difference=correctedExposure,
433 matchedTemplate=matchedTemplate,
434 matchedScience=matchedScience,
435 backgroundModel=kernelResult.backgroundModel,
436 psfMatchingKernel=kernelResult.psfMatchingKernel,)
438 def finalize(self, template, science, difference, kernel,
439 templateMatched=True,
442 spatiallyVarying=False):
443 """Decorrelate the difference image to undo the noise correlations
444 caused by convolution.
448 template : `lsst.afw.image.ExposureF`
449 Template exposure, warped to match the science exposure.
450 science : `lsst.afw.image.ExposureF`
451 Science exposure to subtract from the template.
452 difference : `lsst.afw.image.ExposureF`
453 Result of subtracting template
and science.
455 An (optionally spatially-varying) PSF matching kernel
456 templateMatched : `bool`, optional
457 Was the template PSF-matched to the science image?
458 preConvMode : `bool`, optional
459 Was the science image preconvolved
with its own PSF
460 before PSF matching the template?
462 If
not `
None`, then the science image was pre-convolved
with
463 (the reflection of) this kernel. Must be normalized to sum to 1.
464 spatiallyVarying : `bool`, optional
465 Compute the decorrelation kernel spatially varying across the image?
469 correctedExposure : `lsst.afw.image.ExposureF`
470 The decorrelated image difference.
474 mask = difference.mask
475 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
477 if self.config.doDecorrelation:
478 self.log.info(
"Decorrelating image difference.")
479 correctedExposure = self.decorrelate.
run(science, template[science.getBBox()], difference, kernel,
480 templateMatched=templateMatched,
481 preConvMode=preConvMode,
482 preConvKernel=preConvKernel,
483 spatiallyVarying=spatiallyVarying).correctedExposure
485 self.log.info(
"NOT decorrelating image difference.")
486 correctedExposure = difference
487 return correctedExposure
490 def _validateExposures(template, science):
491 """Check that the WCS of the two Exposures match, and the template bbox
492 contains the science bbox.
496 template : `lsst.afw.image.ExposureF`
497 Template exposure, warped to match the science exposure.
498 science : `lsst.afw.image.ExposureF`
499 Science exposure to subtract from the template.
504 Raised
if the WCS of the template
is not equal to the science WCS,
505 or if the science image
is not fully contained
in the template
508 assert template.wcs == science.wcs,\
509 "Template and science exposure WCS are not identical."
510 templateBBox = template.getBBox()
511 scienceBBox = science.getBBox()
513 assert templateBBox.contains(scienceBBox),\
514 "Template bbox does not contain all of the science image."
517 def _convolveExposure(exposure, kernel, convolutionControl,
521 """Convolve an exposure with the given kernel.
525 exposure : `lsst.afw.Exposure`
526 exposure to convolve.
528 PSF matching kernel computed in the ``makeKernel`` subtask.
530 Configuration
for convolve algorithm.
532 Bounding box to trim the convolved exposure to.
534 Point spread function (PSF) to set
for the convolved exposure.
536 Photometric calibration of the convolved exposure.
540 convolvedExp : `lsst.afw.Exposure`
543 convolvedExposure = exposure.clone()
545 convolvedExposure.setPsf(psf)
546 if photoCalib
is not None:
547 convolvedExposure.setPhotoCalib(photoCalib)
548 convolvedImage = lsst.afw.image.MaskedImageF(exposure.getBBox())
550 convolvedExposure.setMaskedImage(convolvedImage)
552 return convolvedExposure
554 return convolvedExposure[bbox]
558 """Raise NoWorkFound if template coverage < requiredTemplateFraction
562 templateExposure : `lsst.afw.image.ExposureF`
563 The template exposure to check
565 Logger for printing output.
566 requiredTemplateFraction : `float`, optional
567 Fraction of pixels of the science image required to have coverage
572 lsst.pipe.base.NoWorkFound
573 Raised
if fraction of good pixels, defined
as not having NO_DATA
574 set,
is less then the configured requiredTemplateFraction
578 pixNoData = np.count_nonzero(templateExposure.mask.array
579 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
580 pixGood = templateExposure.getBBox().getArea() - pixNoData
581 logger.info(
"template has %d good pixels (%.1f%%)", pixGood,
582 100*pixGood/templateExposure.getBBox().getArea())
584 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
585 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
586 "To force subtraction, set config requiredTemplateFraction=0." % (
587 100*pixGood/templateExposure.getBBox().getArea(),
588 100*requiredTemplateFraction))
589 raise lsst.pipe.base.NoWorkFound(message)
592def _subtractImages(science, template, backgroundModel=None):
593 """Subtract template from science, propagating relevant metadata.
597 science : `lsst.afw.Exposure`
598 The input science image.
599 template : `lsst.afw.Exposure`
600 The template to subtract from the science image.
601 backgroundModel : `lsst.afw.MaskedImage`, optional
602 Differential background model
606 difference : `lsst.afw.Exposure`
607 The subtracted image.
609 difference = science.clone()
610 if backgroundModel
is not None:
611 difference.maskedImage -= backgroundModel
612 difference.maskedImage -= template.maskedImage
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 runConvolveScience(self, template, science, sources)
def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog)
def _validateExposures(template, science)
def runConvolveTemplate(self, template, science, sources)
def __init__(self, **kwargs)
def run(self, template, science, sources, finalizedPsfApCorrCatalog=None)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.)