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=
"finalVisitSummary",
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(
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."
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.",
148 detectionThreshold = lsst.pex.config.Field(
151 doc=
"Minimum signal to noise ration of detected sources "
152 "to use for calculating the PSF matching kernel."
154 badSourceFlags = lsst.pex.config.ListField(
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 self.
makeKernel.kernel.active.spatialKernelOrder = 1
166 self.
makeKernel.kernel.active.spatialBgOrder = 2
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.Function2D`
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.
265 If there are too few sources to calculate the PSF matching kernel.
266 lsst.pipe.base.NoWorkFound
267 Raised
if fraction of good pixels, defined
as not having NO_DATA
268 set,
is less then the configured requiredTemplateFraction
271 if self.config.doApplyFinalizedPsf:
273 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog)
275 requiredTemplateFraction=self.config.requiredTemplateFraction)
276 sciencePsfSize = getPsfFwhm(science.psf)
277 templatePsfSize = getPsfFwhm(template.psf)
278 self.log.info(
"Science PSF FWHM: %f pixels", sciencePsfSize)
279 self.log.info(
"Template PSF FWHM: %f pixels", templatePsfSize)
280 if self.config.mode ==
"auto":
281 convolveTemplate = _shapeTest(template.psf, science.psf)
283 if sciencePsfSize < templatePsfSize:
284 self.log.info(
"Average template PSF size is greater, "
285 "but science PSF greater in one dimension: convolving template image.")
287 self.log.info(
"Science PSF size is greater: convolving template image.")
289 self.log.info(
"Template PSF size is greater: convolving science image.")
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 photoCalib = template.getPhotoCalib()
300 self.log.info(
"Applying photometric calibration to template: %f", photoCalib.getCalibrationMean())
301 template.maskedImage = photoCalib.calibrateImage(template.maskedImage)
303 if self.config.doScaleVariance:
307 templateVarFactor = self.scaleVariance.
run(template.maskedImage)
308 sciVarFactor = self.scaleVariance.
run(science.maskedImage)
309 self.log.info(
"Template variance scaling factor: %.2f", templateVarFactor)
310 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
311 self.log.info(
"Science variance scaling factor: %.2f", sciVarFactor)
312 self.metadata.add(
"scaleScienceVarianceFactor", sciVarFactor)
315 self.log.info(
"%i sources used out of %i from the input catalog", len(selectSources), len(sources))
316 if len(selectSources) < self.config.makeKernel.nStarPerCell:
317 self.log.warning(
"Too few sources to calculate the PSF matching kernel: "
318 "%i selected but %i needed for the calculation.",
319 len(selectSources), self.config.makeKernel.nStarPerCell)
320 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
326 return subtractResults
329 """Convolve the template image with a PSF-matching kernel and subtract
330 from the science image.
334 template : `lsst.afw.image.ExposureF`
335 Template exposure, warped to match the science exposure.
336 science : `lsst.afw.image.ExposureF`
337 Science exposure to subtract
from the template.
339 Identified sources on the science exposure. This catalog
is used to
340 select sources
in order to perform the AL PSF matching on stamp
345 results : `lsst.pipe.base.Struct`
347 ``difference`` : `lsst.afw.image.ExposureF`
348 Result of subtracting template
and science.
349 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
350 Warped
and PSF-matched template exposure.
351 ``backgroundModel`` : `lsst.afw.math.Function2D`
352 Background model that was fit
while solving
for the PSF-matching kernel
354 Kernel used to PSF-match the template to the science image.
356 kernelSources = self.makeKernel.selectKernelSources(template, science,
357 candidateList=selectSources,
359 kernelResult = self.makeKernel.
run(template, science, kernelSources,
362 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
364 bbox=science.getBBox(),
366 photoCalib=science.getPhotoCalib())
367 difference = _subtractImages(science, matchedTemplate,
368 backgroundModel=(kernelResult.backgroundModel
369 if self.config.doSubtractBackground
else None))
370 correctedExposure = self.
finalize(template, science, difference, kernelResult.psfMatchingKernel,
371 templateMatched=
True)
373 return lsst.pipe.base.Struct(difference=correctedExposure,
374 matchedTemplate=matchedTemplate,
375 matchedScience=science,
376 backgroundModel=kernelResult.backgroundModel,
377 psfMatchingKernel=kernelResult.psfMatchingKernel)
380 """Convolve the science image with a PSF-matching kernel and subtract the template image.
384 template : `lsst.afw.image.ExposureF`
385 Template exposure, warped to match the science exposure.
386 science : `lsst.afw.image.ExposureF`
387 Science exposure to subtract from the template.
389 Identified sources on the science exposure. This catalog
is used to
390 select sources
in order to perform the AL PSF matching on stamp
395 results : `lsst.pipe.base.Struct`
397 ``difference`` : `lsst.afw.image.ExposureF`
398 Result of subtracting template
and science.
399 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
400 Warped template exposure. Note that
in this case, the template
401 is not PSF-matched to the science image.
402 ``backgroundModel`` : `lsst.afw.math.Function2D`
403 Background model that was fit
while solving
for the PSF-matching kernel
405 Kernel used to PSF-match the science image to the template.
407 kernelSources = self.makeKernel.selectKernelSources(science, template,
408 candidateList=selectSources,
410 kernelResult = self.makeKernel.
run(science, template, kernelSources,
412 modelParams = kernelResult.backgroundModel.getParameters()
414 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
416 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
417 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
424 matchedScience.maskedImage /= norm
425 matchedTemplate = template.clone()[science.getBBox()]
426 matchedTemplate.maskedImage /= norm
427 matchedTemplate.setPhotoCalib(science.getPhotoCalib())
429 difference = _subtractImages(matchedScience, matchedTemplate,
430 backgroundModel=(kernelResult.backgroundModel
431 if self.config.doSubtractBackground
else None))
433 correctedExposure = self.
finalize(template, science, difference, kernelResult.psfMatchingKernel,
434 templateMatched=
False)
436 return lsst.pipe.base.Struct(difference=correctedExposure,
437 matchedTemplate=matchedTemplate,
438 matchedScience=matchedScience,
439 backgroundModel=kernelResult.backgroundModel,
440 psfMatchingKernel=kernelResult.psfMatchingKernel,)
442 def finalize(self, template, science, difference, kernel,
443 templateMatched=True,
446 spatiallyVarying=False):
447 """Decorrelate the difference image to undo the noise correlations
448 caused by convolution.
452 template : `lsst.afw.image.ExposureF`
453 Template exposure, warped to match the science exposure.
454 science : `lsst.afw.image.ExposureF`
455 Science exposure to subtract from the template.
456 difference : `lsst.afw.image.ExposureF`
457 Result of subtracting template
and science.
459 An (optionally spatially-varying) PSF matching kernel
460 templateMatched : `bool`, optional
461 Was the template PSF-matched to the science image?
462 preConvMode : `bool`, optional
463 Was the science image preconvolved
with its own PSF
464 before PSF matching the template?
466 If
not `
None`, then the science image was pre-convolved
with
467 (the reflection of) this kernel. Must be normalized to sum to 1.
468 spatiallyVarying : `bool`, optional
469 Compute the decorrelation kernel spatially varying across the image?
473 correctedExposure : `lsst.afw.image.ExposureF`
474 The decorrelated image difference.
478 mask = difference.mask
479 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
481 if self.config.doDecorrelation:
482 self.log.info(
"Decorrelating image difference.")
483 correctedExposure = self.decorrelate.
run(science, template[science.getBBox()], difference, kernel,
484 templateMatched=templateMatched,
485 preConvMode=preConvMode,
486 preConvKernel=preConvKernel,
487 spatiallyVarying=spatiallyVarying).correctedExposure
489 self.log.info(
"NOT decorrelating image difference.")
490 correctedExposure = difference
491 return correctedExposure
494 def _validateExposures(template, science):
495 """Check that the WCS of the two Exposures match, and the template bbox
496 contains the science bbox.
500 template : `lsst.afw.image.ExposureF`
501 Template exposure, warped to match the science exposure.
502 science : `lsst.afw.image.ExposureF`
503 Science exposure to subtract from the template.
508 Raised
if the WCS of the template
is not equal to the science WCS,
509 or if the science image
is not fully contained
in the template
512 assert template.wcs == science.wcs,\
513 "Template and science exposure WCS are not identical."
514 templateBBox = template.getBBox()
515 scienceBBox = science.getBBox()
517 assert templateBBox.contains(scienceBBox),\
518 "Template bbox does not contain all of the science image."
521 def _convolveExposure(exposure, kernel, convolutionControl,
525 """Convolve an exposure with the given kernel.
529 exposure : `lsst.afw.Exposure`
530 exposure to convolve.
532 PSF matching kernel computed in the ``makeKernel`` subtask.
534 Configuration
for convolve algorithm.
536 Bounding box to trim the convolved exposure to.
538 Point spread function (PSF) to set
for the convolved exposure.
540 Photometric calibration of the convolved exposure.
544 convolvedExp : `lsst.afw.Exposure`
547 convolvedExposure = exposure.clone()
549 convolvedExposure.setPsf(psf)
550 if photoCalib
is not None:
551 convolvedExposure.setPhotoCalib(photoCalib)
552 convolvedImage = lsst.afw.image.MaskedImageF(exposure.getBBox())
554 convolvedExposure.setMaskedImage(convolvedImage)
556 return convolvedExposure
558 return convolvedExposure[bbox]
560 def _sourceSelector(self, sources):
561 """Select sources from a catalog that meet the selection criteria.
566 Input source catalog to select sources from.
571 The source catalog filtered to include only the selected sources.
573 flags = [True, ]*len(sources)
574 for flag
in self.config.badSourceFlags:
576 flags *= ~sources[flag]
577 except Exception
as e:
578 self.log.warning(
"Could not apply source flag: %s", e)
579 sToNFlag = (sources.getPsfInstFlux()/sources.getPsfInstFluxErr()) > self.config.detectionThreshold
581 selectSources = sources[flags]
583 return selectSources.copy(deep=
True)
587 """Raise NoWorkFound if template coverage < requiredTemplateFraction
591 templateExposure : `lsst.afw.image.ExposureF`
592 The template exposure to check
594 Logger for printing output.
595 requiredTemplateFraction : `float`, optional
596 Fraction of pixels of the science image required to have coverage
601 lsst.pipe.base.NoWorkFound
602 Raised
if fraction of good pixels, defined
as not having NO_DATA
603 set,
is less then the configured requiredTemplateFraction
607 pixNoData = np.count_nonzero(templateExposure.mask.array
608 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
609 pixGood = templateExposure.getBBox().getArea() - pixNoData
610 logger.info(
"template has %d good pixels (%.1f%%)", pixGood,
611 100*pixGood/templateExposure.getBBox().getArea())
613 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
614 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
615 "To force subtraction, set config requiredTemplateFraction=0." % (
616 100*pixGood/templateExposure.getBBox().getArea(),
617 100*requiredTemplateFraction))
618 raise lsst.pipe.base.NoWorkFound(message)
621def _subtractImages(science, template, backgroundModel=None):
622 """Subtract template from science, propagating relevant metadata.
626 science : `lsst.afw.Exposure`
627 The input science image.
628 template : `lsst.afw.Exposure`
629 The template to subtract from the science image.
630 backgroundModel : `lsst.afw.MaskedImage`, optional
631 Differential background model
635 difference : `lsst.afw.Exposure`
636 The subtracted image.
638 difference = science.clone()
639 if backgroundModel
is not None:
640 difference.maskedImage -= backgroundModel
641 difference.maskedImage -= template.maskedImage
645def _shapeTest(psf1, psf2):
646 """Determine whether psf1 is narrower in either dimension than psf2.
651 Reference point spread function (PSF) to evaluate.
653 Candidate point spread function (PSF) to evaluate.
658 Returns True if psf1
is narrower than psf2
in either dimension.
660 shape1 = getPsfFwhm(psf1, average=False)
661 shape2 = getPsfFwhm(psf2, average=
False)
662 xTest = shape1[0] < shape2[0]
663 yTest = shape1[1] < shape2[1]
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 run(self, template, science, sources, finalizedPsfApCorrCatalog=None)
def runConvolveScience(self, template, science, selectSources)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.)