Coverage for python/lsst/ip/diffim/subtractImages.py: 22%
206 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-02 07:21 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-02 07:21 -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"]
38_dimensions = ("instrument", "visit", "detector")
39_defaultTemplates = {"coaddName": "deep", "fakesType": ""}
42class SubtractInputConnections(lsst.pipe.base.PipelineTaskConnections,
43 dimensions=_dimensions,
44 defaultTemplates=_defaultTemplates):
45 template = connectionTypes.Input(
46 doc="Input warped template to subtract.",
47 dimensions=("instrument", "visit", "detector"),
48 storageClass="ExposureF",
49 name="{fakesType}{coaddName}Diff_templateExp"
50 )
51 science = connectionTypes.Input(
52 doc="Input science exposure to subtract from.",
53 dimensions=("instrument", "visit", "detector"),
54 storageClass="ExposureF",
55 name="{fakesType}calexp"
56 )
57 sources = connectionTypes.Input(
58 doc="Sources measured on the science exposure; "
59 "used to select sources for making the matching kernel.",
60 dimensions=("instrument", "visit", "detector"),
61 storageClass="SourceCatalog",
62 name="{fakesType}src"
63 )
64 finalizedPsfApCorrCatalog = connectionTypes.Input(
65 doc=("Per-visit finalized psf models and aperture correction maps. "
66 "These catalogs use the detector id for the catalog id, "
67 "sorted on id for fast lookup."),
68 dimensions=("instrument", "visit"),
69 storageClass="ExposureCatalog",
70 name="finalVisitSummary",
71 )
74class SubtractImageOutputConnections(lsst.pipe.base.PipelineTaskConnections,
75 dimensions=_dimensions,
76 defaultTemplates=_defaultTemplates):
77 difference = connectionTypes.Output(
78 doc="Result of subtracting convolved template from science image.",
79 dimensions=("instrument", "visit", "detector"),
80 storageClass="ExposureF",
81 name="{fakesType}{coaddName}Diff_differenceTempExp",
82 )
83 matchedTemplate = connectionTypes.Output(
84 doc="Warped and PSF-matched template used to create `subtractedExposure`.",
85 dimensions=("instrument", "visit", "detector"),
86 storageClass="ExposureF",
87 name="{fakesType}{coaddName}Diff_matchedExp",
88 )
91class AlardLuptonSubtractConnections(SubtractInputConnections, SubtractImageOutputConnections):
93 def __init__(self, *, config=None):
94 super().__init__(config=config)
95 if not config.doApplyFinalizedPsf:
96 self.inputs.remove("finalizedPsfApCorrCatalog")
99class AlardLuptonSubtractConfig(lsst.pipe.base.PipelineTaskConfig,
100 pipelineConnections=AlardLuptonSubtractConnections):
101 mode = lsst.pex.config.ChoiceField(
102 dtype=str,
103 default="convolveTemplate",
104 allowed={"auto": "Choose which image to convolve at runtime.",
105 "convolveScience": "Only convolve the science image.",
106 "convolveTemplate": "Only convolve the template image."},
107 doc="Choose which image to convolve at runtime, or require that a specific image is convolved."
108 )
109 makeKernel = lsst.pex.config.ConfigurableField(
110 target=MakeKernelTask,
111 doc="Task to construct a matching kernel for convolution.",
112 )
113 doDecorrelation = lsst.pex.config.Field(
114 dtype=bool,
115 default=True,
116 doc="Perform diffim decorrelation to undo pixel correlation due to A&L "
117 "kernel convolution? If True, also update the diffim PSF."
118 )
119 decorrelate = lsst.pex.config.ConfigurableField(
120 target=DecorrelateALKernelTask,
121 doc="Task to decorrelate the image difference.",
122 )
123 requiredTemplateFraction = lsst.pex.config.Field(
124 dtype=float,
125 default=0.1,
126 doc="Abort task if template covers less than this fraction of pixels."
127 " Setting to 0 will always attempt image subtraction."
128 )
129 doScaleVariance = lsst.pex.config.Field(
130 dtype=bool,
131 default=True,
132 doc="Scale variance of the image difference?"
133 )
134 scaleVariance = lsst.pex.config.ConfigurableField(
135 target=ScaleVarianceTask,
136 doc="Subtask to rescale the variance of the template to the statistically expected level."
137 )
138 doSubtractBackground = lsst.pex.config.Field(
139 doc="Subtract the background fit when solving the kernel?",
140 dtype=bool,
141 default=True,
142 )
143 doApplyFinalizedPsf = lsst.pex.config.Field(
144 doc="Replace science Exposure's psf and aperture correction map"
145 " with those in finalizedPsfApCorrCatalog.",
146 dtype=bool,
147 default=False,
148 )
149 detectionThreshold = lsst.pex.config.Field(
150 dtype=float,
151 default=10,
152 doc="Minimum signal to noise ration of detected sources "
153 "to use for calculating the PSF matching kernel."
154 )
155 badSourceFlags = lsst.pex.config.ListField(
156 dtype=str,
157 doc="Flags that, if set, the associated source should not "
158 "be used to determine the PSF matching kernel.",
159 default=("sky_source", "slot_Centroid_flag",
160 "slot_ApFlux_flag", "slot_PsfFlux_flag", ),
161 )
163 def setDefaults(self):
164 self.makeKernel.kernel.name = "AL"
165 self.makeKernel.kernel.active.fitForBackground = self.doSubtractBackground
166 self.makeKernel.kernel.active.spatialKernelOrder = 1
167 self.makeKernel.kernel.active.spatialBgOrder = 2
170class AlardLuptonSubtractTask(lsst.pipe.base.PipelineTask):
171 """Compute the image difference of a science and template image using
172 the Alard & Lupton (1998) algorithm.
173 """
174 ConfigClass = AlardLuptonSubtractConfig
175 _DefaultName = "alardLuptonSubtract"
177 def __init__(self, **kwargs):
178 super().__init__(**kwargs)
179 self.makeSubtask("decorrelate")
180 self.makeSubtask("makeKernel")
181 if self.config.doScaleVariance:
182 self.makeSubtask("scaleVariance")
184 self.convolutionControl = lsst.afw.math.ConvolutionControl()
185 # Normalization is an extra, unnecessary, calculation and will result
186 # in mis-subtraction of the images if there are calibration errors.
187 self.convolutionControl.setDoNormalize(False)
188 self.convolutionControl.setDoCopyEdge(True)
190 def _applyExternalCalibrations(self, exposure, finalizedPsfApCorrCatalog):
191 """Replace calibrations (psf, and ApCorrMap) on this exposure with external ones.".
193 Parameters
194 ----------
195 exposure : `lsst.afw.image.exposure.Exposure`
196 Input exposure to adjust calibrations.
197 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`
198 Exposure catalog with finalized psf models and aperture correction
199 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses
200 the detector id for the catalog id, sorted on id for fast lookup.
202 Returns
203 -------
204 exposure : `lsst.afw.image.exposure.Exposure`
205 Exposure with adjusted calibrations.
206 """
207 detectorId = exposure.info.getDetector().getId()
209 row = finalizedPsfApCorrCatalog.find(detectorId)
210 if row is None:
211 self.log.warning("Detector id %s not found in finalizedPsfApCorrCatalog; "
212 "Using original psf.", detectorId)
213 else:
214 psf = row.getPsf()
215 apCorrMap = row.getApCorrMap()
216 if psf is None:
217 self.log.warning("Detector id %s has None for psf in "
218 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
219 detectorId)
220 elif apCorrMap is None:
221 self.log.warning("Detector id %s has None for apCorrMap in "
222 "finalizedPsfApCorrCatalog; Using original psf and aperture correction.",
223 detectorId)
224 else:
225 exposure.setPsf(psf)
226 exposure.info.setApCorrMap(apCorrMap)
228 return exposure
230 @timeMethod
231 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None):
232 """PSF match, subtract, and decorrelate two images.
234 Parameters
235 ----------
236 template : `lsst.afw.image.ExposureF`
237 Template exposure, warped to match the science exposure.
238 science : `lsst.afw.image.ExposureF`
239 Science exposure to subtract from the template.
240 sources : `lsst.afw.table.SourceCatalog`
241 Identified sources on the science exposure. This catalog is used to
242 select sources in order to perform the AL PSF matching on stamp
243 images around them.
244 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
245 Exposure catalog with finalized psf models and aperture correction
246 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses
247 the detector id for the catalog id, sorted on id for fast lookup.
249 Returns
250 -------
251 results : `lsst.pipe.base.Struct`
252 ``difference`` : `lsst.afw.image.ExposureF`
253 Result of subtracting template and science.
254 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
255 Warped and PSF-matched template exposure.
256 ``backgroundModel`` : `lsst.afw.math.Function2D`
257 Background model that was fit while solving for the PSF-matching kernel
258 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
259 Kernel used to PSF-match the convolved image.
261 Raises
262 ------
263 RuntimeError
264 If an unsupported convolution mode is supplied.
265 RuntimeError
266 If there are too few sources to calculate the PSF matching kernel.
267 lsst.pipe.base.NoWorkFound
268 Raised if fraction of good pixels, defined as not having NO_DATA
269 set, is less then the configured requiredTemplateFraction
270 """
271 self._validateExposures(template, science)
272 if self.config.doApplyFinalizedPsf:
273 self._applyExternalCalibrations(science,
274 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog)
275 checkTemplateIsSufficient(template, self.log,
276 requiredTemplateFraction=self.config.requiredTemplateFraction)
278 # In the event that getPsfFwhm fails, evaluate the PSF on a grid.
279 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
280 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
282 # Calling getPsfFwhm on template.psf fails on some rare occasions when
283 # the template has no input exposures at the average position of the
284 # stars. So we try getPsfFwhm first on template, and if that fails we
285 # evaluate the PSF on a grid specified by fwhmExposure* fields.
286 # To keep consistent definitions for PSF size on the template and
287 # science images, we use the same method for both.
288 try:
289 templatePsfSize = getPsfFwhm(template.psf)
290 sciencePsfSize = getPsfFwhm(science.psf)
291 except InvalidParameterError:
292 self.log.info("Unable to evaluate PSF at the average position. "
293 "Evaluting PSF on a grid of points."
294 )
295 templatePsfSize = evaluateMeanPsfFwhm(template,
296 fwhmExposureBuffer=fwhmExposureBuffer,
297 fwhmExposureGrid=fwhmExposureGrid
298 )
299 sciencePsfSize = evaluateMeanPsfFwhm(science,
300 fwhmExposureBuffer=fwhmExposureBuffer,
301 fwhmExposureGrid=fwhmExposureGrid
302 )
303 self.log.info("Science PSF FWHM: %f pixels", sciencePsfSize)
304 self.log.info("Template PSF FWHM: %f pixels", templatePsfSize)
306 if self.config.mode == "auto":
307 convolveTemplate = _shapeTest(template,
308 science,
309 fwhmExposureBuffer=fwhmExposureBuffer,
310 fwhmExposureGrid=fwhmExposureGrid)
311 if convolveTemplate:
312 if sciencePsfSize < templatePsfSize:
313 self.log.info("Average template PSF size is greater, "
314 "but science PSF greater in one dimension: convolving template image.")
315 else:
316 self.log.info("Science PSF size is greater: convolving template image.")
317 else:
318 self.log.info("Template PSF size is greater: convolving science image.")
319 elif self.config.mode == "convolveTemplate":
320 self.log.info("`convolveTemplate` is set: convolving template image.")
321 convolveTemplate = True
322 elif self.config.mode == "convolveScience":
323 self.log.info("`convolveScience` is set: convolving science image.")
324 convolveTemplate = False
325 else:
326 raise RuntimeError("Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
327 # put the template on the same photometric scale as the science image
328 photoCalib = template.getPhotoCalib()
329 self.log.info("Applying photometric calibration to template: %f", photoCalib.getCalibrationMean())
330 template.maskedImage = photoCalib.calibrateImage(template.maskedImage)
332 if self.config.doScaleVariance:
333 # Scale the variance of the template and science images before
334 # convolution, subtraction, or decorrelation so that they have the
335 # correct ratio.
336 templateVarFactor = self.scaleVariance.run(template.maskedImage)
337 sciVarFactor = self.scaleVariance.run(science.maskedImage)
338 self.log.info("Template variance scaling factor: %.2f", templateVarFactor)
339 self.metadata.add("scaleTemplateVarianceFactor", templateVarFactor)
340 self.log.info("Science variance scaling factor: %.2f", sciVarFactor)
341 self.metadata.add("scaleScienceVarianceFactor", sciVarFactor)
343 selectSources = self._sourceSelector(sources)
344 self.log.info("%i sources used out of %i from the input catalog", len(selectSources), len(sources))
345 if len(selectSources) < self.config.makeKernel.nStarPerCell:
346 self.log.warning("Too few sources to calculate the PSF matching kernel: "
347 "%i selected but %i needed for the calculation.",
348 len(selectSources), self.config.makeKernel.nStarPerCell)
349 raise RuntimeError("Cannot compute PSF matching kernel: too few sources selected.")
350 if convolveTemplate:
351 subtractResults = self.runConvolveTemplate(template, science, selectSources)
352 else:
353 subtractResults = self.runConvolveScience(template, science, selectSources)
355 return subtractResults
357 def runConvolveTemplate(self, template, science, selectSources):
358 """Convolve the template image with a PSF-matching kernel and subtract
359 from the science image.
361 Parameters
362 ----------
363 template : `lsst.afw.image.ExposureF`
364 Template exposure, warped to match the science exposure.
365 science : `lsst.afw.image.ExposureF`
366 Science exposure to subtract from the template.
367 selectSources : `lsst.afw.table.SourceCatalog`
368 Identified sources on the science exposure. This catalog is used to
369 select sources in order to perform the AL PSF matching on stamp
370 images around them.
372 Returns
373 -------
374 results : `lsst.pipe.base.Struct`
376 ``difference`` : `lsst.afw.image.ExposureF`
377 Result of subtracting template and science.
378 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
379 Warped and PSF-matched template exposure.
380 ``backgroundModel`` : `lsst.afw.math.Function2D`
381 Background model that was fit while solving for the PSF-matching kernel
382 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
383 Kernel used to PSF-match the template to the science image.
384 """
385 kernelSources = self.makeKernel.selectKernelSources(template, science,
386 candidateList=selectSources,
387 preconvolved=False)
388 kernelResult = self.makeKernel.run(template, science, kernelSources,
389 preconvolved=False)
391 matchedTemplate = self._convolveExposure(template, kernelResult.psfMatchingKernel,
392 self.convolutionControl,
393 bbox=science.getBBox(),
394 psf=science.psf,
395 photoCalib=science.getPhotoCalib())
396 difference = _subtractImages(science, matchedTemplate,
397 backgroundModel=(kernelResult.backgroundModel
398 if self.config.doSubtractBackground else None))
399 correctedExposure = self.finalize(template, science, difference, kernelResult.psfMatchingKernel,
400 templateMatched=True)
402 return lsst.pipe.base.Struct(difference=correctedExposure,
403 matchedTemplate=matchedTemplate,
404 matchedScience=science,
405 backgroundModel=kernelResult.backgroundModel,
406 psfMatchingKernel=kernelResult.psfMatchingKernel)
408 def runConvolveScience(self, template, science, selectSources):
409 """Convolve the science image with a PSF-matching kernel and subtract the template image.
411 Parameters
412 ----------
413 template : `lsst.afw.image.ExposureF`
414 Template exposure, warped to match the science exposure.
415 science : `lsst.afw.image.ExposureF`
416 Science exposure to subtract from the template.
417 selectSources : `lsst.afw.table.SourceCatalog`
418 Identified sources on the science exposure. This catalog is used to
419 select sources in order to perform the AL PSF matching on stamp
420 images around them.
422 Returns
423 -------
424 results : `lsst.pipe.base.Struct`
426 ``difference`` : `lsst.afw.image.ExposureF`
427 Result of subtracting template and science.
428 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
429 Warped template exposure. Note that in this case, the template
430 is not PSF-matched to the science image.
431 ``backgroundModel`` : `lsst.afw.math.Function2D`
432 Background model that was fit while solving for the PSF-matching kernel
433 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
434 Kernel used to PSF-match the science image to the template.
435 """
436 kernelSources = self.makeKernel.selectKernelSources(science, template,
437 candidateList=selectSources,
438 preconvolved=False)
439 kernelResult = self.makeKernel.run(science, template, kernelSources,
440 preconvolved=False)
441 modelParams = kernelResult.backgroundModel.getParameters()
442 # We must invert the background model if the matching kernel is solved for the science image.
443 kernelResult.backgroundModel.setParameters([-p for p in modelParams])
445 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
446 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=False)
448 matchedScience = self._convolveExposure(science, kernelResult.psfMatchingKernel,
449 self.convolutionControl,
450 psf=template.psf)
452 # Place back on native photometric scale
453 matchedScience.maskedImage /= norm
454 matchedTemplate = template.clone()[science.getBBox()]
455 matchedTemplate.maskedImage /= norm
456 matchedTemplate.setPhotoCalib(science.getPhotoCalib())
458 difference = _subtractImages(matchedScience, matchedTemplate,
459 backgroundModel=(kernelResult.backgroundModel
460 if self.config.doSubtractBackground else None))
462 correctedExposure = self.finalize(template, science, difference, 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):
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.
597 Returns
598 -------
599 `lsst.afw.table.SourceCatalog`
600 The source catalog filtered to include only the selected sources.
601 """
602 flags = [True, ]*len(sources)
603 for flag in self.config.badSourceFlags:
604 try:
605 flags *= ~sources[flag]
606 except Exception as e:
607 self.log.warning("Could not apply source flag: %s", e)
608 sToNFlag = (sources.getPsfInstFlux()/sources.getPsfInstFluxErr()) > self.config.detectionThreshold
609 flags *= sToNFlag
610 selectSources = sources[flags]
612 return selectSources.copy(deep=True)
615def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.):
616 """Raise NoWorkFound if template coverage < requiredTemplateFraction
618 Parameters
619 ----------
620 templateExposure : `lsst.afw.image.ExposureF`
621 The template exposure to check
622 logger : `lsst.log.Log`
623 Logger for printing output.
624 requiredTemplateFraction : `float`, optional
625 Fraction of pixels of the science image required to have coverage
626 in the template.
628 Raises
629 ------
630 lsst.pipe.base.NoWorkFound
631 Raised if fraction of good pixels, defined as not having NO_DATA
632 set, is less then the configured requiredTemplateFraction
633 """
634 # Count the number of pixels with the NO_DATA mask bit set
635 # counting NaN pixels is insufficient because pixels without data are often intepolated over)
636 pixNoData = np.count_nonzero(templateExposure.mask.array
637 & templateExposure.mask.getPlaneBitMask('NO_DATA'))
638 pixGood = templateExposure.getBBox().getArea() - pixNoData
639 logger.info("template has %d good pixels (%.1f%%)", pixGood,
640 100*pixGood/templateExposure.getBBox().getArea())
642 if pixGood/templateExposure.getBBox().getArea() < requiredTemplateFraction:
643 message = ("Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
644 "To force subtraction, set config requiredTemplateFraction=0." % (
645 100*pixGood/templateExposure.getBBox().getArea(),
646 100*requiredTemplateFraction))
647 raise lsst.pipe.base.NoWorkFound(message)
650def _subtractImages(science, template, backgroundModel=None):
651 """Subtract template from science, propagating relevant metadata.
653 Parameters
654 ----------
655 science : `lsst.afw.Exposure`
656 The input science image.
657 template : `lsst.afw.Exposure`
658 The template to subtract from the science image.
659 backgroundModel : `lsst.afw.MaskedImage`, optional
660 Differential background model
662 Returns
663 -------
664 difference : `lsst.afw.Exposure`
665 The subtracted image.
666 """
667 difference = science.clone()
668 if backgroundModel is not None:
669 difference.maskedImage -= backgroundModel
670 difference.maskedImage -= template.maskedImage
671 return difference
674def _shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid):
675 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
677 Parameters
678 ----------
679 exp1 : `~lsst.afw.image.Exposure`
680 Exposure with the reference point spread function (PSF) to evaluate.
681 exp2 : `~lsst.afw.image.Exposure`
682 Exposure with a candidate point spread function (PSF) to evaluate.
683 fwhmExposureBuffer : `float`
684 Fractional buffer margin to be left out of all sides of the image
685 during the construction of the grid to compute mean PSF FWHM in an
686 exposure, if the PSF is not available at its average position.
687 fwhmExposureGrid : `int`
688 Grid size to compute the mean FWHM in an exposure, if the PSF is not
689 available at its average position.
690 Returns
691 -------
692 result : `bool`
693 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
694 either dimension.
695 """
696 try:
697 shape1 = getPsfFwhm(exp1.psf, average=False)
698 shape2 = getPsfFwhm(exp2.psf, average=False)
699 except InvalidParameterError:
700 shape1 = evaluateMeanPsfFwhm(exp1,
701 fwhmExposureBuffer=fwhmExposureBuffer,
702 fwhmExposureGrid=fwhmExposureGrid
703 )
704 shape2 = evaluateMeanPsfFwhm(exp2,
705 fwhmExposureBuffer=fwhmExposureBuffer,
706 fwhmExposureGrid=fwhmExposureGrid
707 )
708 return shape1 <= shape2
710 # Results from getPsfFwhm is a tuple of two values, one for each dimension.
711 xTest = shape1[0] <= shape2[0]
712 yTest = shape1[1] <= shape2[1]
713 return xTest | yTest