Coverage for python/lsst/ip/diffim/subtractImages.py: 25%
243 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 03:18 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 03:18 -0800
1# This file is part of ip_diffim.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
22import numpy as np
24import lsst.afw.image
25import lsst.afw.math
26import lsst.geom
27from lsst.ip.diffim.utils import evaluateMeanPsfFwhm, getPsfFwhm
28from lsst.meas.algorithms import ScaleVarianceTask
29import lsst.pex.config
30import lsst.pipe.base
31from lsst.pex.exceptions import InvalidParameterError
32from lsst.pipe.base import connectionTypes
33from . import MakeKernelTask, DecorrelateALKernelTask
34from lsst.utils.timer import timeMethod
36__all__ = ["AlardLuptonSubtractConfig", "AlardLuptonSubtractTask",
37 "AlardLuptonPreconvolveSubtractConfig", "AlardLuptonPreconvolveSubtractTask"]
39_dimensions = ("instrument", "visit", "detector")
40_defaultTemplates = {"coaddName": "deep", "fakesType": ""}
43class SubtractInputConnections(lsst.pipe.base.PipelineTaskConnections,
44 dimensions=_dimensions,
45 defaultTemplates=_defaultTemplates):
46 template = connectionTypes.Input(
47 doc="Input warped template to subtract.",
48 dimensions=("instrument", "visit", "detector"),
49 storageClass="ExposureF",
50 name="{fakesType}{coaddName}Diff_templateExp"
51 )
52 science = connectionTypes.Input(
53 doc="Input science exposure to subtract from.",
54 dimensions=("instrument", "visit", "detector"),
55 storageClass="ExposureF",
56 name="{fakesType}calexp"
57 )
58 sources = connectionTypes.Input(
59 doc="Sources measured on the science exposure; "
60 "used to select sources for making the matching kernel.",
61 dimensions=("instrument", "visit", "detector"),
62 storageClass="SourceCatalog",
63 name="{fakesType}src"
64 )
65 finalizedPsfApCorrCatalog = connectionTypes.Input(
66 doc=("Per-visit finalized psf models and aperture correction maps. "
67 "These catalogs use the detector id for the catalog id, "
68 "sorted on id for fast lookup."),
69 dimensions=("instrument", "visit"),
70 storageClass="ExposureCatalog",
71 name="finalVisitSummary",
72 )
74 def __init__(self, *, config=None):
75 super().__init__(config=config)
76 if not config.doApplyFinalizedPsf:
77 self.inputs.remove("finalizedPsfApCorrCatalog")
80class SubtractImageOutputConnections(lsst.pipe.base.PipelineTaskConnections,
81 dimensions=_dimensions,
82 defaultTemplates=_defaultTemplates):
83 difference = connectionTypes.Output(
84 doc="Result of subtracting convolved template from science image.",
85 dimensions=("instrument", "visit", "detector"),
86 storageClass="ExposureF",
87 name="{fakesType}{coaddName}Diff_differenceTempExp",
88 )
89 matchedTemplate = connectionTypes.Output(
90 doc="Warped and PSF-matched template used to create `subtractedExposure`.",
91 dimensions=("instrument", "visit", "detector"),
92 storageClass="ExposureF",
93 name="{fakesType}{coaddName}Diff_matchedExp",
94 )
97class SubtractScoreOutputConnections(lsst.pipe.base.PipelineTaskConnections,
98 dimensions=_dimensions,
99 defaultTemplates=_defaultTemplates):
100 scoreExposure = connectionTypes.Output(
101 doc="The maximum likelihood image, used for the detection of diaSources.",
102 dimensions=("instrument", "visit", "detector"),
103 storageClass="ExposureF",
104 name="{fakesType}{coaddName}Diff_scoreExp",
105 )
108class AlardLuptonSubtractConnections(SubtractInputConnections, SubtractImageOutputConnections):
109 pass
112class AlardLuptonSubtractBaseConfig(lsst.pex.config.Config):
113 makeKernel = lsst.pex.config.ConfigurableField(
114 target=MakeKernelTask,
115 doc="Task to construct a matching kernel for convolution.",
116 )
117 doDecorrelation = lsst.pex.config.Field(
118 dtype=bool,
119 default=True,
120 doc="Perform diffim decorrelation to undo pixel correlation due to A&L "
121 "kernel convolution? If True, also update the diffim PSF."
122 )
123 decorrelate = lsst.pex.config.ConfigurableField(
124 target=DecorrelateALKernelTask,
125 doc="Task to decorrelate the image difference.",
126 )
127 requiredTemplateFraction = lsst.pex.config.Field(
128 dtype=float,
129 default=0.1,
130 doc="Abort task if template covers less than this fraction of pixels."
131 " Setting to 0 will always attempt image subtraction."
132 )
133 doScaleVariance = lsst.pex.config.Field(
134 dtype=bool,
135 default=True,
136 doc="Scale variance of the image difference?"
137 )
138 scaleVariance = lsst.pex.config.ConfigurableField(
139 target=ScaleVarianceTask,
140 doc="Subtask to rescale the variance of the template to the statistically expected level."
141 )
142 doSubtractBackground = lsst.pex.config.Field(
143 doc="Subtract the background fit when solving the kernel?",
144 dtype=bool,
145 default=True,
146 )
147 doApplyFinalizedPsf = lsst.pex.config.Field(
148 doc="Replace science Exposure's psf and aperture correction map"
149 " with those in finalizedPsfApCorrCatalog.",
150 dtype=bool,
151 default=False,
152 )
153 detectionThreshold = lsst.pex.config.Field(
154 dtype=float,
155 default=10,
156 doc="Minimum signal to noise ration of detected sources "
157 "to use for calculating the PSF matching kernel."
158 )
159 badSourceFlags = lsst.pex.config.ListField(
160 dtype=str,
161 doc="Flags that, if set, the associated source should not "
162 "be used to determine the PSF matching kernel.",
163 default=("sky_source", "slot_Centroid_flag",
164 "slot_ApFlux_flag", "slot_PsfFlux_flag", ),
165 )
166 badMaskPlanes = lsst.pex.config.ListField(
167 dtype=str,
168 default=("NO_DATA", "BAD", "SAT", "EDGE"),
169 doc="Mask planes to exclude when selecting sources for PSF matching."
170 )
172 def setDefaults(self):
173 self.makeKernel.kernel.name = "AL"
174 self.makeKernel.kernel.active.fitForBackground = self.doSubtractBackground
175 self.makeKernel.kernel.active.spatialKernelOrder = 1
176 self.makeKernel.kernel.active.spatialBgOrder = 2
179class AlardLuptonSubtractConfig(AlardLuptonSubtractBaseConfig, lsst.pipe.base.PipelineTaskConfig,
180 pipelineConnections=AlardLuptonSubtractConnections):
181 mode = lsst.pex.config.ChoiceField(
182 dtype=str,
183 default="convolveTemplate",
184 allowed={"auto": "Choose which image to convolve at runtime.",
185 "convolveScience": "Only convolve the science image.",
186 "convolveTemplate": "Only convolve the template image."},
187 doc="Choose which image to convolve at runtime, or require that a specific image is convolved."
188 )
191class AlardLuptonSubtractTask(lsst.pipe.base.PipelineTask):
192 """Compute the image difference of a science and template image using
193 the Alard & Lupton (1998) algorithm.
194 """
195 ConfigClass = AlardLuptonSubtractConfig
196 _DefaultName = "alardLuptonSubtract"
198 def __init__(self, **kwargs):
199 super().__init__(**kwargs)
200 self.makeSubtask("decorrelate")
201 self.makeSubtask("makeKernel")
202 if self.config.doScaleVariance:
203 self.makeSubtask("scaleVariance")
205 self.convolutionControl = lsst.afw.math.ConvolutionControl()
206 # Normalization is an extra, unnecessary, calculation and will result
207 # in mis-subtraction of the images if there are calibration errors.
208 self.convolutionControl.setDoNormalize(False)
209 self.convolutionControl.setDoCopyEdge(True)
211 def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog):
212 """Replace calibrations (psf, and ApCorrMap) on this exposure with external ones.".
214 Parameters
215 ----------
216 exposure : `lsst.afw.image.exposure.Exposure`
217 Input exposure to adjust calibrations.
218 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`
219 Exposure catalog with finalized psf models and aperture correction
220 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses
221 the detector id for the catalog id, sorted on id for fast lookup.
223 Returns
224 -------
225 exposure : `lsst.afw.image.exposure.Exposure`
226 Exposure with adjusted calibrations.
227 """
228 detectorId = exposure.info.getDetector().getId()
230 row = finalizedPsfApCorrCatalog.find(detectorId)
231 if row is None:
232 self.log.warning("Detector id %s not found in finalizedPsfApCorrCatalog; "
233 "Using original psf.", detectorId)
234 else:
235 psf = row.getPsf()
236 apCorrMap = row.getApCorrMap()
237 if psf is None:
238 self.log.warning("Detector id %s has None for psf in "
239 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
240 detectorId)
241 elif apCorrMap is None:
242 self.log.warning("Detector id %s has None for apCorrMap in "
243 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
244 detectorId)
245 else:
246 exposure.setPsf(psf)
247 exposure.info.setApCorrMap(apCorrMap)
249 return exposure
251 @timeMethod
252 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None):
253 """PSF match, subtract, and decorrelate two images.
255 Parameters
256 ----------
257 template : `lsst.afw.image.ExposureF`
258 Template exposure, warped to match the science exposure.
259 science : `lsst.afw.image.ExposureF`
260 Science exposure to subtract from the template.
261 sources : `lsst.afw.table.SourceCatalog`
262 Identified sources on the science exposure. This catalog is used to
263 select sources in order to perform the AL PSF matching on stamp
264 images around them.
265 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
266 Exposure catalog with finalized psf models and aperture correction
267 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses
268 the detector id for the catalog id, sorted on id for fast lookup.
270 Returns
271 -------
272 results : `lsst.pipe.base.Struct`
273 ``difference`` : `lsst.afw.image.ExposureF`
274 Result of subtracting template and science.
275 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
276 Warped and PSF-matched template exposure.
277 ``backgroundModel`` : `lsst.afw.math.Function2D`
278 Background model that was fit while solving for the PSF-matching kernel
279 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
280 Kernel used to PSF-match the convolved image.
282 Raises
283 ------
284 RuntimeError
285 If an unsupported convolution mode is supplied.
286 RuntimeError
287 If there are too few sources to calculate the PSF matching kernel.
288 lsst.pipe.base.NoWorkFound
289 Raised if fraction of good pixels, defined as not having NO_DATA
290 set, is less then the configured requiredTemplateFraction
291 """
292 self._prepareInputs(template, science,
293 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog)
295 # In the event that getPsfFwhm fails, evaluate the PSF on a grid.
296 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
297 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
299 # Calling getPsfFwhm on template.psf fails on some rare occasions when
300 # the template has no input exposures at the average position of the
301 # stars. So we try getPsfFwhm first on template, and if that fails we
302 # evaluate the PSF on a grid specified by fwhmExposure* fields.
303 # To keep consistent definitions for PSF size on the template and
304 # science images, we use the same method for both.
305 try:
306 templatePsfSize = getPsfFwhm(template.psf)
307 sciencePsfSize = getPsfFwhm(science.psf)
308 except InvalidParameterError:
309 self.log.info("Unable to evaluate PSF at the average position. "
310 "Evaluting PSF on a grid of points."
311 )
312 templatePsfSize = evaluateMeanPsfFwhm(template,
313 fwhmExposureBuffer=fwhmExposureBuffer,
314 fwhmExposureGrid=fwhmExposureGrid
315 )
316 sciencePsfSize = evaluateMeanPsfFwhm(science,
317 fwhmExposureBuffer=fwhmExposureBuffer,
318 fwhmExposureGrid=fwhmExposureGrid
319 )
320 self.log.info("Science PSF FWHM: %f pixels", sciencePsfSize)
321 self.log.info("Template PSF FWHM: %f pixels", templatePsfSize)
322 selectSources = self._sourceSelector(sources, science.mask)
324 if self.config.mode == "auto":
325 convolveTemplate = _shapeTest(template,
326 science,
327 fwhmExposureBuffer=fwhmExposureBuffer,
328 fwhmExposureGrid=fwhmExposureGrid)
329 if convolveTemplate:
330 if sciencePsfSize < templatePsfSize:
331 self.log.info("Average template PSF size is greater, "
332 "but science PSF greater in one dimension: convolving template image.")
333 else:
334 self.log.info("Science PSF size is greater: convolving template image.")
335 else:
336 self.log.info("Template PSF size is greater: convolving science image.")
337 elif self.config.mode == "convolveTemplate":
338 self.log.info("`convolveTemplate` is set: convolving template image.")
339 convolveTemplate = True
340 elif self.config.mode == "convolveScience":
341 self.log.info("`convolveScience` is set: convolving science image.")
342 convolveTemplate = False
343 else:
344 raise RuntimeError("Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
346 if convolveTemplate:
347 subtractResults = self.runConvolveTemplate(template, science, selectSources)
348 else:
349 subtractResults = self.runConvolveScience(template, science, selectSources)
351 return subtractResults
353 def runConvolveTemplate(self, template, science, selectSources):
354 """Convolve the template image with a PSF-matching kernel and subtract
355 from the science image.
357 Parameters
358 ----------
359 template : `lsst.afw.image.ExposureF`
360 Template exposure, warped to match the science exposure.
361 science : `lsst.afw.image.ExposureF`
362 Science exposure to subtract from the template.
363 selectSources : `lsst.afw.table.SourceCatalog`
364 Identified sources on the science exposure. This catalog is used to
365 select sources in order to perform the AL PSF matching on stamp
366 images around them.
368 Returns
369 -------
370 results : `lsst.pipe.base.Struct`
372 ``difference`` : `lsst.afw.image.ExposureF`
373 Result of subtracting template and science.
374 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
375 Warped and PSF-matched template exposure.
376 ``backgroundModel`` : `lsst.afw.math.Function2D`
377 Background model that was fit while solving for the PSF-matching kernel
378 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
379 Kernel used to PSF-match the template to the science image.
380 """
381 kernelSources = self.makeKernel.selectKernelSources(template, science,
382 candidateList=selectSources,
383 preconvolved=False)
384 kernelResult = self.makeKernel.run(template, science, kernelSources,
385 preconvolved=False)
387 matchedTemplate = self._convolveExposure(template, kernelResult.psfMatchingKernel,
388 self.convolutionControl,
389 bbox=science.getBBox(),
390 psf=science.psf,
391 photoCalib=science.photoCalib)
393 difference = _subtractImages(science, matchedTemplate,
394 backgroundModel=(kernelResult.backgroundModel
395 if self.config.doSubtractBackground else None))
396 correctedExposure = self.finalize(template, science, difference,
397 kernelResult.psfMatchingKernel,
398 templateMatched=True)
400 return lsst.pipe.base.Struct(difference=correctedExposure,
401 matchedTemplate=matchedTemplate,
402 matchedScience=science,
403 backgroundModel=kernelResult.backgroundModel,
404 psfMatchingKernel=kernelResult.psfMatchingKernel)
406 def runConvolveScience(self, template, science, selectSources):
407 """Convolve the science image with a PSF-matching kernel and subtract the template image.
409 Parameters
410 ----------
411 template : `lsst.afw.image.ExposureF`
412 Template exposure, warped to match the science exposure.
413 science : `lsst.afw.image.ExposureF`
414 Science exposure to subtract from the template.
415 selectSources : `lsst.afw.table.SourceCatalog`
416 Identified sources on the science exposure. This catalog is used to
417 select sources in order to perform the AL PSF matching on stamp
418 images around them.
420 Returns
421 -------
422 results : `lsst.pipe.base.Struct`
424 ``difference`` : `lsst.afw.image.ExposureF`
425 Result of subtracting template and science.
426 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
427 Warped template exposure. Note that in this case, the template
428 is not PSF-matched to the science image.
429 ``backgroundModel`` : `lsst.afw.math.Function2D`
430 Background model that was fit while solving for the PSF-matching kernel
431 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
432 Kernel used to PSF-match the science image to the template.
433 """
434 bbox = science.getBBox()
435 kernelSources = self.makeKernel.selectKernelSources(science, template,
436 candidateList=selectSources,
437 preconvolved=False)
438 kernelResult = self.makeKernel.run(science, template, kernelSources,
439 preconvolved=False)
440 modelParams = kernelResult.backgroundModel.getParameters()
441 # We must invert the background model if the matching kernel is solved for the science image.
442 kernelResult.backgroundModel.setParameters([-p for p in modelParams])
444 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
445 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=False)
447 matchedScience = self._convolveExposure(science, kernelResult.psfMatchingKernel,
448 self.convolutionControl,
449 psf=template.psf)
451 # Place back on native photometric scale
452 matchedScience.maskedImage /= norm
453 matchedTemplate = template.clone()[bbox]
454 matchedTemplate.maskedImage /= norm
455 matchedTemplate.setPhotoCalib(science.photoCalib)
457 difference = _subtractImages(matchedScience, matchedTemplate,
458 backgroundModel=(kernelResult.backgroundModel
459 if self.config.doSubtractBackground else None))
461 correctedExposure = self.finalize(template, science, difference,
462 kernelResult.psfMatchingKernel,
463 templateMatched=False)
465 return lsst.pipe.base.Struct(difference=correctedExposure,
466 matchedTemplate=matchedTemplate,
467 matchedScience=matchedScience,
468 backgroundModel=kernelResult.backgroundModel,
469 psfMatchingKernel=kernelResult.psfMatchingKernel,)
471 def finalize(self, template, science, difference, kernel,
472 templateMatched=True,
473 preConvMode=False,
474 preConvKernel=None,
475 spatiallyVarying=False):
476 """Decorrelate the difference image to undo the noise correlations
477 caused by convolution.
479 Parameters
480 ----------
481 template : `lsst.afw.image.ExposureF`
482 Template exposure, warped to match the science exposure.
483 science : `lsst.afw.image.ExposureF`
484 Science exposure to subtract from the template.
485 difference : `lsst.afw.image.ExposureF`
486 Result of subtracting template and science.
487 kernel : `lsst.afw.math.Kernel`
488 An (optionally spatially-varying) PSF matching kernel
489 templateMatched : `bool`, optional
490 Was the template PSF-matched to the science image?
491 preConvMode : `bool`, optional
492 Was the science image preconvolved with its own PSF
493 before PSF matching the template?
494 preConvKernel : `lsst.afw.detection.Psf`, optional
495 If not `None`, then the science image was pre-convolved with
496 (the reflection of) this kernel. Must be normalized to sum to 1.
497 spatiallyVarying : `bool`, optional
498 Compute the decorrelation kernel spatially varying across the image?
500 Returns
501 -------
502 correctedExposure : `lsst.afw.image.ExposureF`
503 The decorrelated image difference.
504 """
505 # Erase existing detection mask planes.
506 # We don't want the detection mask from the science image
507 mask = difference.mask
508 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
510 if self.config.doDecorrelation:
511 self.log.info("Decorrelating image difference.")
512 correctedExposure = self.decorrelate.run(science, template[science.getBBox()], difference, kernel,
513 templateMatched=templateMatched,
514 preConvMode=preConvMode,
515 preConvKernel=preConvKernel,
516 spatiallyVarying=spatiallyVarying).correctedExposure
517 else:
518 self.log.info("NOT decorrelating image difference.")
519 correctedExposure = difference
520 return correctedExposure
522 @staticmethod
523 def _validateExposures(template, science):
524 """Check that the WCS of the two Exposures match, and the template bbox
525 contains the science bbox.
527 Parameters
528 ----------
529 template : `lsst.afw.image.ExposureF`
530 Template exposure, warped to match the science exposure.
531 science : `lsst.afw.image.ExposureF`
532 Science exposure to subtract from the template.
534 Raises
535 ------
536 AssertionError
537 Raised if the WCS of the template is not equal to the science WCS,
538 or if the science image is not fully contained in the template
539 bounding box.
540 """
541 assert template.wcs == science.wcs,\
542 "Template and science exposure WCS are not identical."
543 templateBBox = template.getBBox()
544 scienceBBox = science.getBBox()
546 assert templateBBox.contains(scienceBBox),\
547 "Template bbox does not contain all of the science image."
549 @staticmethod
550 def _convolveExposure(exposure, kernel, convolutionControl,
551 bbox=None,
552 psf=None,
553 photoCalib=None):
554 """Convolve an exposure with the given kernel.
556 Parameters
557 ----------
558 exposure : `lsst.afw.Exposure`
559 exposure to convolve.
560 kernel : `lsst.afw.math.LinearCombinationKernel`
561 PSF matching kernel computed in the ``makeKernel`` subtask.
562 convolutionControl : `lsst.afw.math.ConvolutionControl`
563 Configuration for convolve algorithm.
564 bbox : `lsst.geom.Box2I`, optional
565 Bounding box to trim the convolved exposure to.
566 psf : `lsst.afw.detection.Psf`, optional
567 Point spread function (PSF) to set for the convolved exposure.
568 photoCalib : `lsst.afw.image.PhotoCalib`, optional
569 Photometric calibration of the convolved exposure.
571 Returns
572 -------
573 convolvedExp : `lsst.afw.Exposure`
574 The convolved image.
575 """
576 convolvedExposure = exposure.clone()
577 if psf is not None:
578 convolvedExposure.setPsf(psf)
579 if photoCalib is not None:
580 convolvedExposure.setPhotoCalib(photoCalib)
581 convolvedImage = lsst.afw.image.MaskedImageF(exposure.getBBox())
582 lsst.afw.math.convolve(convolvedImage, exposure.maskedImage, kernel, convolutionControl)
583 convolvedExposure.setMaskedImage(convolvedImage)
584 if bbox is None:
585 return convolvedExposure
586 else:
587 return convolvedExposure[bbox]
589 def _sourceSelector(self, sources, mask):
590 """Select sources from a catalog that meet the selection criteria.
592 Parameters
593 ----------
594 sources : `lsst.afw.table.SourceCatalog`
595 Input source catalog to select sources from.
596 mask : `lsst.afw.image.Mask`
597 The image mask plane to use to reject sources
598 based on their location on the ccd.
600 Returns
601 -------
602 selectSources : `lsst.afw.table.SourceCatalog`
603 The input source catalog, with flagged and low signal-to-noise
604 sources removed.
606 Raises
607 ------
608 RuntimeError
609 If there are too few sources to compute the PSF matching kernel
610 remaining after source selection.
611 """
612 flags = np.ones(len(sources), dtype=bool)
613 for flag in self.config.badSourceFlags:
614 try:
615 flags *= ~sources[flag]
616 except Exception as e:
617 self.log.warning("Could not apply source flag: %s", e)
618 sToNFlag = (sources.getPsfInstFlux()/sources.getPsfInstFluxErr()) > self.config.detectionThreshold
619 flags *= sToNFlag
620 flags *= self._checkMask(mask, sources, self.config.badMaskPlanes)
621 selectSources = sources[flags]
622 self.log.info("%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
623 len(selectSources), len(sources), 100*len(selectSources)/len(sources))
624 if len(selectSources) < self.config.makeKernel.nStarPerCell:
625 self.log.error("Too few sources to calculate the PSF matching kernel: "
626 "%i selected but %i needed for the calculation.",
627 len(selectSources), self.config.makeKernel.nStarPerCell)
628 raise RuntimeError("Cannot compute PSF matching kernel: too few sources selected.")
630 return selectSources.copy(deep=True)
632 @staticmethod
633 def _checkMask(mask, sources, badMaskPlanes):
634 """Exclude sources that are located on masked pixels.
636 Parameters
637 ----------
638 mask : `lsst.afw.image.Mask`
639 The image mask plane to use to reject sources
640 based on the location of their centroid on the ccd.
641 sources : `lsst.afw.table.SourceCatalog`
642 The source catalog to evaluate.
643 badMaskPlanes : `list` of `str`
644 List of the names of the mask planes to exclude.
646 Returns
647 -------
648 flags : `numpy.ndarray` of `bool`
649 Array indicating whether each source in the catalog should be
650 kept (True) or rejected (False) based on the value of the
651 mask plane at its location.
652 """
653 badPixelMask = lsst.afw.image.Mask.getPlaneBitMask(badMaskPlanes)
654 xv = np.rint(sources.getX() - mask.getX0())
655 yv = np.rint(sources.getY() - mask.getY0())
657 mv = mask.array[yv.astype(int), xv.astype(int)]
658 flags = np.bitwise_and(mv, badPixelMask) == 0
659 return flags
661 def _prepareInputs(self, template, science,
662 finalizedPsfApCorrCatalog=None):
663 """Perform preparatory calculations common to all Alard&Lupton Tasks.
665 Parameters
666 ----------
667 template : `lsst.afw.image.ExposureF`
668 Template exposure, warped to match the science exposure.
669 The variance plane of the template image is modified in place.
670 science : `lsst.afw.image.ExposureF`
671 Science exposure to subtract from the template.
672 The variance plane of the science image is modified in place.
673 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
674 Exposure catalog with finalized psf models and aperture correction
675 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses
676 the detector id for the catalog id, sorted on id for fast lookup.
677 """
678 self._validateExposures(template, science)
679 if self.config.doApplyFinalizedPsf:
680 self._applyExternalCalibrations(science,
681 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog)
682 checkTemplateIsSufficient(template, self.log,
683 requiredTemplateFraction=self.config.requiredTemplateFraction)
685 if self.config.doScaleVariance:
686 # Scale the variance of the template and science images before
687 # convolution, subtraction, or decorrelation so that they have the
688 # correct ratio.
689 templateVarFactor = self.scaleVariance.run(template.maskedImage)
690 sciVarFactor = self.scaleVariance.run(science.maskedImage)
691 self.log.info("Template variance scaling factor: %.2f", templateVarFactor)
692 self.metadata.add("scaleTemplateVarianceFactor", templateVarFactor)
693 self.log.info("Science variance scaling factor: %.2f", sciVarFactor)
694 self.metadata.add("scaleScienceVarianceFactor", sciVarFactor)
697class AlardLuptonPreconvolveSubtractConnections(SubtractInputConnections,
698 SubtractScoreOutputConnections):
699 pass
702class AlardLuptonPreconvolveSubtractConfig(AlardLuptonSubtractBaseConfig, lsst.pipe.base.PipelineTaskConfig,
703 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
704 pass
707class AlardLuptonPreconvolveSubtractTask(AlardLuptonSubtractTask):
708 """Subtract a template from a science image, convolving the science image
709 before computing the kernel, and also convolving the template before
710 subtraction.
711 """
712 ConfigClass = AlardLuptonPreconvolveSubtractConfig
713 _DefaultName = "alardLuptonPreconvolveSubtract"
715 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None):
716 """Preconvolve the science image with its own PSF,
717 convolve the template image with a PSF-matching kernel and subtract
718 from the preconvolved science image.
720 Parameters
721 ----------
722 template : `lsst.afw.image.ExposureF`
723 The template image, which has previously been warped to
724 the science image. The template bbox will be padded by a few pixels
725 compared to the science bbox.
726 science : `lsst.afw.image.ExposureF`
727 The science exposure.
728 sources : `lsst.afw.table.SourceCatalog`
729 Identified sources on the science exposure. This catalog is used to
730 select sources in order to perform the AL PSF matching on stamp
731 images around them.
732 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
733 Exposure catalog with finalized psf models and aperture correction
734 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses
735 the detector id for the catalog id, sorted on id for fast lookup.
737 Returns
738 -------
739 results : `lsst.pipe.base.Struct`
740 ``scoreExposure`` : `lsst.afw.image.ExposureF`
741 Result of subtracting the convolved template and science images.
742 Attached PSF is that of the original science image.
743 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
744 Warped and PSF-matched template exposure.
745 Attached PSF is that of the original science image.
746 ``matchedScience`` : `lsst.afw.image.ExposureF`
747 The science exposure after convolving with its own PSF.
748 Attached PSF is that of the original science image.
749 ``backgroundModel`` : `lsst.afw.math.Function2D`
750 Background model that was fit while solving for the PSF-matching kernel
751 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
752 Final kernel used to PSF-match the template to the science image.
753 """
754 self._prepareInputs(template, science,
755 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog)
757 # TODO: DM-37212 we need to mirror the kernel in order to get correct cross correlation
758 scienceKernel = science.psf.getKernel()
759 matchedScience = self._convolveExposure(science, scienceKernel, self.convolutionControl)
760 selectSources = self._sourceSelector(sources, matchedScience.mask)
762 subtractResults = self.runPreconvolve(template, science, matchedScience, selectSources, scienceKernel)
764 return subtractResults
766 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
767 """Convolve the science image with its own PSF, then convolve the
768 template with a matching kernel and subtract to form the Score exposure.
770 Parameters
771 ----------
772 template : `lsst.afw.image.ExposureF`
773 Template exposure, warped to match the science exposure.
774 science : `lsst.afw.image.ExposureF`
775 Science exposure to subtract from the template.
776 matchedScience : `lsst.afw.image.ExposureF`
777 The science exposure, convolved with the reflection of its own PSF.
778 selectSources : `lsst.afw.table.SourceCatalog`
779 Identified sources on the science exposure. This catalog is used to
780 select sources in order to perform the AL PSF matching on stamp
781 images around them.
782 preConvKernel : `lsst.afw.math.Kernel`
783 The reflection of the kernel that was used to preconvolve
784 the `science` exposure.
785 Must be normalized to sum to 1.
787 Returns
788 -------
789 results : `lsst.pipe.base.Struct`
791 ``scoreExposure`` : `lsst.afw.image.ExposureF`
792 Result of subtracting the convolved template and science images.
793 Attached PSF is that of the original science image.
794 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
795 Warped and PSF-matched template exposure.
796 Attached PSF is that of the original science image.
797 ``matchedScience`` : `lsst.afw.image.ExposureF`
798 The science exposure after convolving with its own PSF.
799 Attached PSF is that of the original science image.
800 ``backgroundModel`` : `lsst.afw.math.Function2D`
801 Background model that was fit while solving for the PSF-matching kernel
802 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
803 Final kernel used to PSF-match the template to the science image.
804 """
805 bbox = science.getBBox()
806 innerBBox = preConvKernel.shrinkBBox(bbox)
808 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], science[innerBBox],
809 candidateList=selectSources,
810 preconvolved=True)
811 kernelResult = self.makeKernel.run(template[innerBBox], matchedScience[innerBBox], kernelSources,
812 preconvolved=True)
814 matchedTemplate = self._convolveExposure(template, kernelResult.psfMatchingKernel,
815 self.convolutionControl,
816 bbox=bbox,
817 psf=science.psf,
818 photoCalib=science.photoCalib)
819 score = _subtractImages(matchedScience, matchedTemplate,
820 backgroundModel=(kernelResult.backgroundModel
821 if self.config.doSubtractBackground else None))
822 correctedScore = self.finalize(template[bbox], science, score,
823 kernelResult.psfMatchingKernel,
824 templateMatched=True, preConvMode=True,
825 preConvKernel=preConvKernel)
827 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
828 matchedTemplate=matchedTemplate,
829 matchedScience=matchedScience,
830 backgroundModel=kernelResult.backgroundModel,
831 psfMatchingKernel=kernelResult.psfMatchingKernel)
834def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.):
835 """Raise NoWorkFound if template coverage < requiredTemplateFraction
837 Parameters
838 ----------
839 templateExposure : `lsst.afw.image.ExposureF`
840 The template exposure to check
841 logger : `lsst.log.Log`
842 Logger for printing output.
843 requiredTemplateFraction : `float`, optional
844 Fraction of pixels of the science image required to have coverage
845 in the template.
847 Raises
848 ------
849 lsst.pipe.base.NoWorkFound
850 Raised if fraction of good pixels, defined as not having NO_DATA
851 set, is less then the configured requiredTemplateFraction
852 """
853 # Count the number of pixels with the NO_DATA mask bit set
854 # counting NaN pixels is insufficient because pixels without data are often intepolated over)
855 pixNoData = np.count_nonzero(templateExposure.mask.array
856 & templateExposure.mask.getPlaneBitMask('NO_DATA'))
857 pixGood = templateExposure.getBBox().getArea() - pixNoData
858 logger.info("template has %d good pixels (%.1f%%)", pixGood,
859 100*pixGood/templateExposure.getBBox().getArea())
861 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
862 message = ("Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
863 "To force subtraction, set config requiredTemplateFraction=0." % (
864 100*pixGood/templateExposure.getBBox().getArea(),
865 100*requiredTemplateFraction))
866 raise lsst.pipe.base.NoWorkFound(message)
869def _subtractImages(science, template, backgroundModel=None):
870 """Subtract template from science, propagating relevant metadata.
872 Parameters
873 ----------
874 science : `lsst.afw.Exposure`
875 The input science image.
876 template : `lsst.afw.Exposure`
877 The template to subtract from the science image.
878 backgroundModel : `lsst.afw.MaskedImage`, optional
879 Differential background model
881 Returns
882 -------
883 difference : `lsst.afw.Exposure`
884 The subtracted image.
885 """
886 difference = science.clone()
887 if backgroundModel is not None:
888 difference.maskedImage -= backgroundModel
889 difference.maskedImage -= template.maskedImage
890 return difference
893def _shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid):
894 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
896 Parameters
897 ----------
898 exp1 : `~lsst.afw.image.Exposure`
899 Exposure with the reference point spread function (PSF) to evaluate.
900 exp2 : `~lsst.afw.image.Exposure`
901 Exposure with a candidate point spread function (PSF) to evaluate.
902 fwhmExposureBuffer : `float`
903 Fractional buffer margin to be left out of all sides of the image
904 during the construction of the grid to compute mean PSF FWHM in an
905 exposure, if the PSF is not available at its average position.
906 fwhmExposureGrid : `int`
907 Grid size to compute the mean FWHM in an exposure, if the PSF is not
908 available at its average position.
909 Returns
910 -------
911 result : `bool`
912 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
913 either dimension.
914 """
915 try:
916 shape1 = getPsfFwhm(exp1.psf, average=False)
917 shape2 = getPsfFwhm(exp2.psf, average=False)
918 except InvalidParameterError:
919 shape1 = evaluateMeanPsfFwhm(exp1,
920 fwhmExposureBuffer=fwhmExposureBuffer,
921 fwhmExposureGrid=fwhmExposureGrid
922 )
923 shape2 = evaluateMeanPsfFwhm(exp2,
924 fwhmExposureBuffer=fwhmExposureBuffer,
925 fwhmExposureGrid=fwhmExposureGrid
926 )
927 return shape1 <= shape2
929 # Results from getPsfFwhm is a tuple of two values, one for each dimension.
930 xTest = shape1[0] <= shape2[0]
931 yTest = shape1[1] <= shape2[1]
932 return xTest | yTest