28from lsst.meas.algorithms
import ScaleVarianceTask
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):
100 mode = lsst.pex.config.ChoiceField(
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."
108 makeKernel = lsst.pex.config.ConfigurableField(
109 target=MakeKernelTask,
110 doc=
"Task to construct a matching kernel for convolution.",
112 doDecorrelation = lsst.pex.config.Field(
115 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
116 "kernel convolution? If True, also update the diffim PSF."
118 decorrelate = lsst.pex.config.ConfigurableField(
119 target=DecorrelateALKernelTask,
120 doc=
"Task to decorrelate the image difference.",
122 requiredTemplateFraction = lsst.pex.config.Field(
125 doc=
"Abort task if template covers less than this fraction of pixels."
126 " Setting to 0 will always attempt image subtraction."
128 doScaleVariance = lsst.pex.config.Field(
131 doc=
"Scale variance of the image difference?"
133 scaleVariance = lsst.pex.config.ConfigurableField(
134 target=ScaleVarianceTask,
135 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
137 doSubtractBackground = lsst.pex.config.Field(
138 doc=
"Subtract the background fit when solving the kernel?",
142 doApplyFinalizedPsf = lsst.pex.config.Field(
143 doc=
"Replace science Exposure's psf and aperture correction map"
144 " with those in finalizedPsfApCorrCatalog.",
149 forceCompatibility = lsst.pex.config.Field(
152 doc=
"Set up and run diffim using settings that ensure the results"
153 "are compatible with the old version in pipe_tasks.",
154 deprecated=
"This option is only for backwards compatibility purposes"
155 " and will be removed after v24.",
161 self.
makeKernel.kernel.active.spatialKernelOrder = 1
162 self.
makeKernel.kernel.active.spatialBgOrder = 2
166 self.
mode =
"convolveTemplate"
170 """Compute the image difference of a science and template image using
171 the Alard & Lupton (1998) algorithm.
173 ConfigClass = AlardLuptonSubtractConfig
174 _DefaultName = "alardLuptonSubtract"
178 self.makeSubtask(
"decorrelate")
179 self.makeSubtask(
"makeKernel")
180 if self.config.doScaleVariance:
181 self.makeSubtask(
"scaleVariance")
189 def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog):
190 """Replace calibrations (psf, and ApCorrMap) on this exposure with external ones.".
194 exposure : `lsst.afw.image.exposure.Exposure`
195 Input exposure to adjust calibrations.
197 Exposure catalog with finalized psf models
and aperture correction
198 maps to be applied
if config.doApplyFinalizedPsf=
True. Catalog uses
199 the detector id
for the catalog id, sorted on id
for fast lookup.
203 exposure : `lsst.afw.image.exposure.Exposure`
204 Exposure
with adjusted calibrations.
206 detectorId = exposure.info.getDetector().getId()
208 row = finalizedPsfApCorrCatalog.find(detectorId)
210 self.log.warning(
"Detector id %s not found in finalizedPsfApCorrCatalog; "
211 "Using original psf.", detectorId)
214 apCorrMap = row.getApCorrMap()
216 self.log.warning(
"Detector id %s has None for psf in "
217 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
219 elif apCorrMap
is None:
220 self.log.warning(
"Detector id %s has None for apCorrMap in "
221 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
225 exposure.info.setApCorrMap(apCorrMap)
230 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None):
231 """PSF match, subtract, and decorrelate two images.
235 template : `lsst.afw.image.ExposureF`
236 Template exposure, warped to match the science exposure.
237 science : `lsst.afw.image.ExposureF`
238 Science exposure to subtract from the template.
240 Identified sources on the science exposure. This catalog
is used to
241 select sources
in order to perform the AL PSF matching on stamp
244 Exposure catalog
with finalized psf models
and aperture correction
245 maps to be applied
if config.doApplyFinalizedPsf=
True. Catalog uses
246 the detector id
for the catalog id, sorted on id
for fast lookup.
250 results : `lsst.pipe.base.Struct`
251 ``difference`` : `lsst.afw.image.ExposureF`
252 Result of subtracting template
and science.
253 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
254 Warped
and PSF-matched template exposure.
255 ``backgroundModel`` : `lsst.afw.math.Chebyshev1Function2D`
256 Background model that was fit
while solving
for the PSF-matching kernel
258 Kernel used to PSF-match the convolved image.
263 If an unsupported convolution mode
is supplied.
264 lsst.pipe.base.NoWorkFound
265 Raised
if fraction of good pixels, defined
as not having NO_DATA
266 set,
is less then the configured requiredTemplateFraction
269 if self.config.doApplyFinalizedPsf:
271 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog)
273 requiredTemplateFraction=self.config.requiredTemplateFraction)
274 if self.config.forceCompatibility:
277 self.log.warning(
"Running with `config.forceCompatibility=True`")
281 self.log.info(
"Science PSF size: %f", sciencePsfSize)
282 self.log.info(
"Template PSF size: %f", templatePsfSize)
283 if self.config.mode ==
"auto":
284 if sciencePsfSize < templatePsfSize:
285 self.log.info(
"Template PSF size is greater: convolving science image.")
286 convolveTemplate =
False
288 self.log.info(
"Science PSF size is greater: convolving template image.")
289 convolveTemplate =
True
290 elif self.config.mode ==
"convolveTemplate":
291 self.log.info(
"`convolveTemplate` is set: convolving template image.")
292 convolveTemplate =
True
293 elif self.config.mode ==
"convolveScience":
294 self.log.info(
"`convolveScience` is set: convolving science image.")
295 convolveTemplate =
False
297 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
299 if self.config.doScaleVariance
and not self.config.forceCompatibility:
303 templateVarFactor = self.scaleVariance.
run(template.maskedImage)
304 sciVarFactor = self.scaleVariance.
run(science.maskedImage)
305 self.log.info(
"Template variance scaling factor: %.2f", templateVarFactor)
306 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
307 self.log.info(
"Science variance scaling factor: %.2f", sciVarFactor)
308 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
310 kernelSources = self.makeKernel.selectKernelSources(template, science,
311 candidateList=sources,
318 if self.config.doScaleVariance
and self.config.forceCompatibility:
320 diffimVarFactor = self.scaleVariance.
run(subtractResults.difference.maskedImage)
321 self.log.info(
"Diffim variance scaling factor: %.2f", diffimVarFactor)
322 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
324 return subtractResults
327 """Convolve the template image with a PSF-matching kernel and subtract
328 from the science image.
332 template : `lsst.afw.image.ExposureF`
333 Template exposure, warped to match the science exposure.
334 science : `lsst.afw.image.ExposureF`
335 Science exposure to subtract
from the template.
337 Identified sources on the science exposure. This catalog
is used to
338 select sources
in order to perform the AL PSF matching on stamp
343 results : `lsst.pipe.base.Struct`
345 ``difference`` : `lsst.afw.image.ExposureF`
346 Result of subtracting template
and science.
347 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
348 Warped
and PSF-matched template exposure.
349 ``backgroundModel`` : `lsst.afw.math.Chebyshev1Function2D`
350 Background model that was fit
while solving
for the PSF-matching kernel
352 Kernel used to PSF-match the template to the science image.
354 if self.config.forceCompatibility:
357 template = template[science.getBBox()]
358 kernelResult = self.makeKernel.
run(template, science, sources, preconvolved=
False)
360 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
362 bbox=science.getBBox(),
364 photoCalib=science.getPhotoCalib())
365 difference = _subtractImages(science, matchedTemplate,
366 backgroundModel=(kernelResult.backgroundModel
367 if self.config.doSubtractBackground
else None))
368 correctedExposure = self.
finalize(template, science, difference, kernelResult.psfMatchingKernel,
369 templateMatched=
True)
371 return lsst.pipe.base.Struct(difference=correctedExposure,
372 matchedTemplate=matchedTemplate,
373 matchedScience=science,
374 backgroundModel=kernelResult.backgroundModel,
375 psfMatchingKernel=kernelResult.psfMatchingKernel)
378 """Convolve the science image with a PSF-matching kernel and subtract the template image.
382 template : `lsst.afw.image.ExposureF`
383 Template exposure, warped to match the science exposure.
384 science : `lsst.afw.image.ExposureF`
385 Science exposure to subtract from the template.
387 Identified sources on the science exposure. This catalog
is used to
388 select sources
in order to perform the AL PSF matching on stamp
393 results : `lsst.pipe.base.Struct`
395 ``difference`` : `lsst.afw.image.ExposureF`
396 Result of subtracting template
and science.
397 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
398 Warped template exposure. Note that
in this case, the template
399 is not PSF-matched to the science image.
400 ``backgroundModel`` : `lsst.afw.math.Chebyshev1Function2D`
401 Background model that was fit
while solving
for the PSF-matching kernel
403 Kernel used to PSF-match the science image to the template.
405 if self.config.forceCompatibility:
408 template = template[science.getBBox()]
409 kernelResult = self.makeKernel.
run(science, template, sources, preconvolved=
False)
410 modelParams = kernelResult.backgroundModel.getParameters()
412 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
414 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
415 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
422 matchedScience.maskedImage /= norm
423 matchedTemplate = template.clone()[science.getBBox()]
424 matchedTemplate.maskedImage /= norm
425 matchedTemplate.setPhotoCalib(science.getPhotoCalib())
427 difference = _subtractImages(matchedScience, matchedTemplate,
428 backgroundModel=(kernelResult.backgroundModel
429 if self.config.doSubtractBackground
else None))
431 correctedExposure = self.
finalize(template, science, difference, kernelResult.psfMatchingKernel,
432 templateMatched=
False)
434 return lsst.pipe.base.Struct(difference=correctedExposure,
435 matchedTemplate=matchedTemplate,
436 matchedScience=matchedScience,
437 backgroundModel=kernelResult.backgroundModel,
438 psfMatchingKernel=kernelResult.psfMatchingKernel,)
440 def finalize(self, template, science, difference, kernel,
441 templateMatched=True,
444 spatiallyVarying=False):
445 """Decorrelate the difference image to undo the noise correlations
446 caused by convolution.
450 template : `lsst.afw.image.ExposureF`
451 Template exposure, warped to match the science exposure.
452 science : `lsst.afw.image.ExposureF`
453 Science exposure to subtract from the template.
454 difference : `lsst.afw.image.ExposureF`
455 Result of subtracting template
and science.
457 An (optionally spatially-varying) PSF matching kernel
458 templateMatched : `bool`, optional
459 Was the template PSF-matched to the science image?
460 preConvMode : `bool`, optional
461 Was the science image preconvolved
with its own PSF
462 before PSF matching the template?
464 If
not `
None`, then the science image was pre-convolved
with
465 (the reflection of) this kernel. Must be normalized to sum to 1.
466 spatiallyVarying : `bool`, optional
467 Compute the decorrelation kernel spatially varying across the image?
471 correctedExposure : `lsst.afw.image.ExposureF`
472 The decorrelated image difference.
476 mask = difference.mask
477 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
479 if self.config.doDecorrelation:
480 self.log.info(
"Decorrelating image difference.")
481 correctedExposure = self.decorrelate.
run(science, template[science.getBBox()], difference, kernel,
482 templateMatched=templateMatched,
483 preConvMode=preConvMode,
484 preConvKernel=preConvKernel,
485 spatiallyVarying=spatiallyVarying).correctedExposure
487 self.log.info(
"NOT decorrelating image difference.")
488 correctedExposure = difference
489 return correctedExposure
492 def _validateExposures(template, science):
493 """Check that the WCS of the two Exposures match, and the template bbox
494 contains the science bbox.
498 template : `lsst.afw.image.ExposureF`
499 Template exposure, warped to match the science exposure.
500 science : `lsst.afw.image.ExposureF`
501 Science exposure to subtract from the template.
506 Raised
if the WCS of the template
is not equal to the science WCS,
507 or if the science image
is not fully contained
in the template
510 assert template.wcs == science.wcs,\
511 "Template and science exposure WCS are not identical."
512 templateBBox = template.getBBox()
513 scienceBBox = science.getBBox()
515 assert templateBBox.contains(scienceBBox),\
516 "Template bbox does not contain all of the science image."
519 def _convolveExposure(exposure, kernel, convolutionControl,
523 """Convolve an exposure with the given kernel.
527 exposure : `lsst.afw.Exposure`
528 exposure to convolve.
530 PSF matching kernel computed in the ``makeKernel`` subtask.
532 Configuration
for convolve algorithm.
534 Bounding box to trim the convolved exposure to.
536 Point spread function (PSF) to set
for the convolved exposure.
538 Photometric calibration of the convolved exposure.
542 convolvedExp : `lsst.afw.Exposure`
545 convolvedExposure = exposure.clone()
547 convolvedExposure.setPsf(psf)
548 if photoCalib
is not None:
549 convolvedExposure.setPhotoCalib(photoCalib)
550 convolvedImage = lsst.afw.image.MaskedImageF(exposure.getBBox())
552 convolvedExposure.setMaskedImage(convolvedImage)
554 return convolvedExposure
556 return convolvedExposure[bbox]
560 """Raise NoWorkFound if template coverage < requiredTemplateFraction
564 templateExposure : `lsst.afw.image.ExposureF`
565 The template exposure to check
567 Logger for printing output.
568 requiredTemplateFraction : `float`, optional
569 Fraction of pixels of the science image required to have coverage
574 lsst.pipe.base.NoWorkFound
575 Raised
if fraction of good pixels, defined
as not having NO_DATA
576 set,
is less then the configured requiredTemplateFraction
580 pixNoData = np.count_nonzero(templateExposure.mask.array
581 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
582 pixGood = templateExposure.getBBox().getArea() - pixNoData
583 logger.info(
"template has %d good pixels (%.1f%%)", pixGood,
584 100*pixGood/templateExposure.getBBox().getArea())
586 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
587 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
588 "To force subtraction, set config requiredTemplateFraction=0." % (
589 100*pixGood/templateExposure.getBBox().getArea(),
590 100*requiredTemplateFraction))
591 raise lsst.pipe.base.NoWorkFound(message)
594def _subtractImages(science, template, backgroundModel=None):
595 """Subtract template from science, propagating relevant metadata.
599 science : `lsst.afw.Exposure`
600 The input science image.
601 template : `lsst.afw.Exposure`
602 The template to subtract from the science image.
603 backgroundModel : `lsst.afw.MaskedImage`, optional
604 Differential background model
608 difference : `lsst.afw.Exposure`
609 The subtracted image.
611 difference = science.clone()
612 if backgroundModel
is not None:
613 difference.maskedImage -= backgroundModel
614 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.)