lsst.ip.diffim gee1b14a8c9+0f30a6b5f9
Loading...
Searching...
No Matches
subtractImages.py
Go to the documentation of this file.
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/>.
21
22import warnings
23
24import numpy as np
25
26import lsst.afw.image
27import lsst.afw.math
28import lsst.geom
29from lsst.utils.introspection import find_outside_stacklevel
30from lsst.ip.diffim.utils import evaluateMeanPsfFwhm, getPsfFwhm
31from lsst.meas.algorithms import ScaleVarianceTask
32import lsst.pex.config
33import lsst.pipe.base
35from lsst.pipe.base import connectionTypes
36from . import MakeKernelTask, DecorrelateALKernelTask
37from lsst.utils.timer import timeMethod
38
39__all__ = ["AlardLuptonSubtractConfig", "AlardLuptonSubtractTask",
40 "AlardLuptonPreconvolveSubtractConfig", "AlardLuptonPreconvolveSubtractTask"]
41
42_dimensions = ("instrument", "visit", "detector")
43_defaultTemplates = {"coaddName": "deep", "fakesType": ""}
44
45
46class SubtractInputConnections(lsst.pipe.base.PipelineTaskConnections,
47 dimensions=_dimensions,
48 defaultTemplates=_defaultTemplates):
49 template = connectionTypes.Input(
50 doc="Input warped template to subtract.",
51 dimensions=("instrument", "visit", "detector"),
52 storageClass="ExposureF",
53 name="{fakesType}{coaddName}Diff_templateExp"
54 )
55 science = connectionTypes.Input(
56 doc="Input science exposure to subtract from.",
57 dimensions=("instrument", "visit", "detector"),
58 storageClass="ExposureF",
59 name="{fakesType}calexp"
60 )
61 sources = connectionTypes.Input(
62 doc="Sources measured on the science exposure; "
63 "used to select sources for making the matching kernel.",
64 dimensions=("instrument", "visit", "detector"),
65 storageClass="SourceCatalog",
66 name="{fakesType}src"
67 )
68 finalizedPsfApCorrCatalog = connectionTypes.Input(
69 doc=("Per-visit finalized psf models and aperture correction maps. "
70 "These catalogs use the detector id for the catalog id, "
71 "sorted on id for fast lookup."),
72 dimensions=("instrument", "visit"),
73 storageClass="ExposureCatalog",
74 name="finalVisitSummary",
75 # TODO: remove on DM-39854.
76 deprecated=(
77 "Deprecated in favor of visitSummary. Will be removed after v26."
78 )
79 )
80 visitSummary = connectionTypes.Input(
81 doc=("Per-visit catalog with final calibration objects. "
82 "These catalogs use the detector id for the catalog id, "
83 "sorted on id for fast lookup."),
84 dimensions=("instrument", "visit"),
85 storageClass="ExposureCatalog",
86 name="finalVisitSummary",
87 )
88
89 def __init__(self, *, config=None):
90 super().__init__(config=config)
91 if not config.doApplyFinalizedPsf:
92 self.inputs.remove("finalizedPsfApCorrCatalog")
93 if not config.doApplyExternalCalibrations or config.doApplyFinalizedPsf:
94 del self.visitSummary
95
96
97class SubtractImageOutputConnections(lsst.pipe.base.PipelineTaskConnections,
98 dimensions=_dimensions,
99 defaultTemplates=_defaultTemplates):
100 difference = connectionTypes.Output(
101 doc="Result of subtracting convolved template from science image.",
102 dimensions=("instrument", "visit", "detector"),
103 storageClass="ExposureF",
104 name="{fakesType}{coaddName}Diff_differenceTempExp",
105 )
106 matchedTemplate = connectionTypes.Output(
107 doc="Warped and PSF-matched template used to create `subtractedExposure`.",
108 dimensions=("instrument", "visit", "detector"),
109 storageClass="ExposureF",
110 name="{fakesType}{coaddName}Diff_matchedExp",
111 )
112
113
114class SubtractScoreOutputConnections(lsst.pipe.base.PipelineTaskConnections,
115 dimensions=_dimensions,
116 defaultTemplates=_defaultTemplates):
117 scoreExposure = connectionTypes.Output(
118 doc="The maximum likelihood image, used for the detection of diaSources.",
119 dimensions=("instrument", "visit", "detector"),
120 storageClass="ExposureF",
121 name="{fakesType}{coaddName}Diff_scoreExp",
122 )
123
124
129class AlardLuptonSubtractBaseConfig(lsst.pex.config.Config):
130 makeKernel = lsst.pex.config.ConfigurableField(
131 target=MakeKernelTask,
132 doc="Task to construct a matching kernel for convolution.",
133 )
134 doDecorrelation = lsst.pex.config.Field(
135 dtype=bool,
136 default=True,
137 doc="Perform diffim decorrelation to undo pixel correlation due to A&L "
138 "kernel convolution? If True, also update the diffim PSF."
139 )
140 decorrelate = lsst.pex.config.ConfigurableField(
141 target=DecorrelateALKernelTask,
142 doc="Task to decorrelate the image difference.",
143 )
144 requiredTemplateFraction = lsst.pex.config.Field(
145 dtype=float,
146 default=0.1,
147 doc="Raise NoWorkFound and do not attempt image subtraction if template covers less than this "
148 " fraction of pixels. Setting to 0 will always attempt image subtraction."
149 )
150 minTemplateFractionForExpectedSuccess = lsst.pex.config.Field(
151 dtype=float,
152 default=0.2,
153 doc="Raise NoWorkFound if PSF-matching fails and template covers less than this fraction of pixels."
154 " If the fraction of pixels covered by the template is less than this value (and greater than"
155 " requiredTemplateFraction) this task is attempted but failure is anticipated and tolerated."
156 )
157 doScaleVariance = lsst.pex.config.Field(
158 dtype=bool,
159 default=True,
160 doc="Scale variance of the image difference?"
161 )
162 scaleVariance = lsst.pex.config.ConfigurableField(
163 target=ScaleVarianceTask,
164 doc="Subtask to rescale the variance of the template to the statistically expected level."
165 )
166 doSubtractBackground = lsst.pex.config.Field(
167 doc="Subtract the background fit when solving the kernel?",
168 dtype=bool,
169 default=True,
170 )
171 doApplyFinalizedPsf = lsst.pex.config.Field(
172 doc="Replace science Exposure's psf and aperture correction map"
173 " with those in finalizedPsfApCorrCatalog.",
174 dtype=bool,
175 default=False,
176 # TODO: remove on DM-39854.
177 deprecated=(
178 "Deprecated in favor of doApplyExternalCalibrations. "
179 "Will be removed after v26."
180 )
181 )
182 doApplyExternalCalibrations = lsst.pex.config.Field(
183 doc=(
184 "Replace science Exposure's calibration objects with those"
185 " in visitSummary. Ignored if `doApplyFinalizedPsf is True."
186 ),
187 dtype=bool,
188 default=False,
189 )
190 detectionThreshold = lsst.pex.config.Field(
191 dtype=float,
192 default=10,
193 doc="Minimum signal to noise ratio of detected sources "
194 "to use for calculating the PSF matching kernel."
195 )
196 detectionThresholdMax = lsst.pex.config.Field(
197 dtype=float,
198 default=500,
199 doc="Maximum signal to noise ratio of detected sources "
200 "to use for calculating the PSF matching kernel."
201 )
202 maxKernelSources = lsst.pex.config.Field(
203 dtype=int,
204 default=1000,
205 doc="Maximum number of sources to use for calculating the PSF matching kernel."
206 "Set to -1 to disable."
207 )
208 minKernelSources = lsst.pex.config.Field(
209 dtype=int,
210 default=3,
211 doc="Minimum number of sources needed for calculating the PSF matching kernel."
212 )
213 badSourceFlags = lsst.pex.config.ListField(
214 dtype=str,
215 doc="Flags that, if set, the associated source should not "
216 "be used to determine the PSF matching kernel.",
217 default=("sky_source", "slot_Centroid_flag",
218 "slot_ApFlux_flag", "slot_PsfFlux_flag",
219 "base_PixelFlags_flag_interpolated",
220 "base_PixelFlags_flag_saturated",
221 "base_PixelFlags_flag_bad",
222 ),
223 )
224 excludeMaskPlanes = lsst.pex.config.ListField(
225 dtype=str,
226 default=("NO_DATA", "BAD", "SAT", "EDGE", "FAKE"),
227 doc="Mask planes to exclude when selecting sources for PSF matching."
228 )
229 badMaskPlanes = lsst.pex.config.ListField(
230 dtype=str,
231 default=("NO_DATA", "BAD", "SAT", "EDGE"),
232 doc="Mask planes to interpolate over."
233 )
234 preserveTemplateMask = lsst.pex.config.ListField(
235 dtype=str,
236 default=("NO_DATA", "BAD",),
237 doc="Mask planes from the template to propagate to the image difference."
238 )
239 renameTemplateMask = lsst.pex.config.ListField(
240 dtype=str,
241 default=("SAT", "INJECTED", "INJECTED_CORE",),
242 doc="Mask planes from the template to propagate to the image difference"
243 "with '_TEMPLATE' appended to the name."
244 )
245 allowKernelSourceDetection = lsst.pex.config.Field(
246 dtype=bool,
247 default=False,
248 doc="Re-run source detection for kernel candidates if an error is"
249 " encountered while calculating the matching kernel."
250 )
251
252 def setDefaults(self):
253 self.makeKernel.kernel.name = "AL"
254 self.makeKernel.kernel.active.fitForBackground = self.doSubtractBackground
255 self.makeKernel.kernel.active.spatialKernelOrder = 1
256 self.makeKernel.kernel.active.spatialBgOrder = 2
257
258
259class AlardLuptonSubtractConfig(AlardLuptonSubtractBaseConfig, lsst.pipe.base.PipelineTaskConfig,
260 pipelineConnections=AlardLuptonSubtractConnections):
261 mode = lsst.pex.config.ChoiceField(
262 dtype=str,
263 default="convolveTemplate",
264 allowed={"auto": "Choose which image to convolve at runtime.",
265 "convolveScience": "Only convolve the science image.",
266 "convolveTemplate": "Only convolve the template image."},
267 doc="Choose which image to convolve at runtime, or require that a specific image is convolved."
268 )
269
270
271class AlardLuptonSubtractTask(lsst.pipe.base.PipelineTask):
272 """Compute the image difference of a science and template image using
273 the Alard & Lupton (1998) algorithm.
274 """
275 ConfigClass = AlardLuptonSubtractConfig
276 _DefaultName = "alardLuptonSubtract"
277
278 def __init__(self, **kwargs):
279 super().__init__(**kwargs)
280 self.makeSubtask("decorrelate")
281 self.makeSubtask("makeKernel")
282 if self.config.doScaleVariance:
283 self.makeSubtask("scaleVariance")
284
286 # Normalization is an extra, unnecessary, calculation and will result
287 # in mis-subtraction of the images if there are calibration errors.
288 self.convolutionControl.setDoNormalize(False)
289 self.convolutionControl.setDoCopyEdge(True)
290
291 def _applyExternalCalibrations(self, exposure, visitSummary):
292 """Replace calibrations (psf, and ApCorrMap) on this exposure with
293 external ones.".
294
295 Parameters
296 ----------
297 exposure : `lsst.afw.image.exposure.Exposure`
298 Input exposure to adjust calibrations.
299 visitSummary : `lsst.afw.table.ExposureCatalog`
300 Exposure catalog with external calibrations to be applied. Catalog
301 uses the detector id for the catalog id, sorted on id for fast
302 lookup.
303
304 Returns
305 -------
306 exposure : `lsst.afw.image.exposure.Exposure`
307 Exposure with adjusted calibrations.
308 """
309 detectorId = exposure.info.getDetector().getId()
310
311 row = visitSummary.find(detectorId)
312 if row is None:
313 self.log.warning("Detector id %s not found in external calibrations catalog; "
314 "Using original calibrations.", detectorId)
315 else:
316 psf = row.getPsf()
317 apCorrMap = row.getApCorrMap()
318 if psf is None:
319 self.log.warning("Detector id %s has None for psf in "
320 "external calibrations catalog; Using original psf and aperture correction.",
321 detectorId)
322 elif apCorrMap is None:
323 self.log.warning("Detector id %s has None for apCorrMap in "
324 "external calibrations catalog; Using original psf and aperture correction.",
325 detectorId)
326 else:
327 exposure.setPsf(psf)
328 exposure.info.setApCorrMap(apCorrMap)
329
330 return exposure
331
332 @timeMethod
333 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None,
334 visitSummary=None):
335 """PSF match, subtract, and decorrelate two images.
336
337 Parameters
338 ----------
339 template : `lsst.afw.image.ExposureF`
340 Template exposure, warped to match the science exposure.
341 science : `lsst.afw.image.ExposureF`
342 Science exposure to subtract from the template.
343 sources : `lsst.afw.table.SourceCatalog`
344 Identified sources on the science exposure. This catalog is used to
345 select sources in order to perform the AL PSF matching on stamp
346 images around them.
347 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
348 Exposure catalog with finalized psf models and aperture correction
349 maps to be applied. Catalog uses the detector id for the catalog
350 id, sorted on id for fast lookup. Deprecated in favor of
351 ``visitSummary``, and will be removed after v26.
352 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
353 Exposure catalog with external calibrations to be applied. Catalog
354 uses the detector id for the catalog id, sorted on id for fast
355 lookup. Ignored (for temporary backwards compatibility) if
356 ``finalizedPsfApCorrCatalog`` is provided.
357
358 Returns
359 -------
360 results : `lsst.pipe.base.Struct`
361 ``difference`` : `lsst.afw.image.ExposureF`
362 Result of subtracting template and science.
363 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
364 Warped and PSF-matched template exposure.
365 ``backgroundModel`` : `lsst.afw.math.Function2D`
366 Background model that was fit while solving for the
367 PSF-matching kernel
368 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
369 Kernel used to PSF-match the convolved image.
370
371 Raises
372 ------
373 RuntimeError
374 If an unsupported convolution mode is supplied.
375 RuntimeError
376 If there are too few sources to calculate the PSF matching kernel.
377 lsst.pipe.base.NoWorkFound
378 Raised if fraction of good pixels, defined as not having NO_DATA
379 set, is less then the configured requiredTemplateFraction
380 """
381
382 if finalizedPsfApCorrCatalog is not None:
383 warnings.warn(
384 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
385 "argument, and will be removed after v26.",
386 FutureWarning,
387 stacklevel=find_outside_stacklevel("lsst.ip.diffim"),
388 )
389 visitSummary = finalizedPsfApCorrCatalog
390
391 self._prepareInputs(template, science, visitSummary=visitSummary)
392
393 # In the event that getPsfFwhm fails, evaluate the PSF on a grid.
394 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
395 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
396
397 # Calling getPsfFwhm on template.psf fails on some rare occasions when
398 # the template has no input exposures at the average position of the
399 # stars. So we try getPsfFwhm first on template, and if that fails we
400 # evaluate the PSF on a grid specified by fwhmExposure* fields.
401 # To keep consistent definitions for PSF size on the template and
402 # science images, we use the same method for both.
403 try:
404 templatePsfSize = getPsfFwhm(template.psf)
405 sciencePsfSize = getPsfFwhm(science.psf)
407 self.log.info("Unable to evaluate PSF at the average position. "
408 "Evaluting PSF on a grid of points."
409 )
410 templatePsfSize = evaluateMeanPsfFwhm(template,
411 fwhmExposureBuffer=fwhmExposureBuffer,
412 fwhmExposureGrid=fwhmExposureGrid
413 )
414 sciencePsfSize = evaluateMeanPsfFwhm(science,
415 fwhmExposureBuffer=fwhmExposureBuffer,
416 fwhmExposureGrid=fwhmExposureGrid
417 )
418 self.log.info("Science PSF FWHM: %f pixels", sciencePsfSize)
419 self.log.info("Template PSF FWHM: %f pixels", templatePsfSize)
420 self.metadata.add("sciencePsfSize", sciencePsfSize)
421 self.metadata.add("templatePsfSize", templatePsfSize)
422
423 if self.config.mode == "auto":
424 convolveTemplate = _shapeTest(template,
425 science,
426 fwhmExposureBuffer=fwhmExposureBuffer,
427 fwhmExposureGrid=fwhmExposureGrid)
428 if convolveTemplate:
429 if sciencePsfSize < templatePsfSize:
430 self.log.info("Average template PSF size is greater, "
431 "but science PSF greater in one dimension: convolving template image.")
432 else:
433 self.log.info("Science PSF size is greater: convolving template image.")
434 else:
435 self.log.info("Template PSF size is greater: convolving science image.")
436 elif self.config.mode == "convolveTemplate":
437 self.log.info("`convolveTemplate` is set: convolving template image.")
438 convolveTemplate = True
439 elif self.config.mode == "convolveScience":
440 self.log.info("`convolveScience` is set: convolving science image.")
441 convolveTemplate = False
442 else:
443 raise RuntimeError("Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
444
445 try:
446 sourceMask = science.mask.clone()
447 sourceMask.array |= template[science.getBBox()].mask.array
448 selectSources = self._sourceSelector(sources, sourceMask)
449 if convolveTemplate:
450 self.metadata.add("convolvedExposure", "Template")
451 subtractResults = self.runConvolveTemplate(template, science, selectSources)
452 else:
453 self.metadata.add("convolvedExposure", "Science")
454 subtractResults = self.runConvolveScience(template, science, selectSources)
455
456 except (RuntimeError, lsst.pex.exceptions.Exception) as e:
457 self.log.warning("Failed to match template. Checking coverage")
458 # Raise NoWorkFound if template fraction is insufficient
459 checkTemplateIsSufficient(template[science.getBBox()], self.log,
460 self.config.minTemplateFractionForExpectedSuccess,
461 exceptionMessage="Template coverage lower than expected to succeed."
462 f" Failure is tolerable: {e}")
463 # checkTemplateIsSufficient did not raise NoWorkFound, so raise original exception
464 raise e
465
466 return subtractResults
467
468 def runConvolveTemplate(self, template, science, selectSources):
469 """Convolve the template image with a PSF-matching kernel and subtract
470 from the science image.
471
472 Parameters
473 ----------
474 template : `lsst.afw.image.ExposureF`
475 Template exposure, warped to match the science exposure.
476 science : `lsst.afw.image.ExposureF`
477 Science exposure to subtract from the template.
478 selectSources : `lsst.afw.table.SourceCatalog`
479 Identified sources on the science exposure. This catalog is used to
480 select sources in order to perform the AL PSF matching on stamp
481 images around them.
482
483 Returns
484 -------
485 results : `lsst.pipe.base.Struct`
486
487 ``difference`` : `lsst.afw.image.ExposureF`
488 Result of subtracting template and science.
489 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
490 Warped and PSF-matched template exposure.
491 ``backgroundModel`` : `lsst.afw.math.Function2D`
492 Background model that was fit while solving for the PSF-matching kernel
493 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
494 Kernel used to PSF-match the template to the science image.
495 """
496 try:
497 kernelSources = self.makeKernel.selectKernelSources(template, science,
498 candidateList=selectSources,
499 preconvolved=False)
500 kernelResult = self.makeKernel.run(template, science, kernelSources,
501 preconvolved=False)
502 except Exception as e:
503 if self.config.allowKernelSourceDetection:
504 self.log.warning("Error encountered trying to construct the matching kernel"
505 f" Running source detection and retrying. {e}")
506 kernelSources = self.makeKernel.selectKernelSources(template, science,
507 candidateList=None,
508 preconvolved=False)
509 kernelResult = self.makeKernel.run(template, science, kernelSources,
510 preconvolved=False)
511 else:
512 raise e
513
514 matchedTemplate = self._convolveExposure(template, kernelResult.psfMatchingKernel,
516 bbox=science.getBBox(),
517 psf=science.psf,
518 photoCalib=science.photoCalib)
519
520 difference = _subtractImages(science, matchedTemplate,
521 backgroundModel=(kernelResult.backgroundModel
522 if self.config.doSubtractBackground else None))
523 correctedExposure = self.finalize(template, science, difference,
524 kernelResult.psfMatchingKernel,
525 templateMatched=True)
526
527 return lsst.pipe.base.Struct(difference=correctedExposure,
528 matchedTemplate=matchedTemplate,
529 matchedScience=science,
530 backgroundModel=kernelResult.backgroundModel,
531 psfMatchingKernel=kernelResult.psfMatchingKernel)
532
533 def runConvolveScience(self, template, science, selectSources):
534 """Convolve the science image with a PSF-matching kernel and subtract
535 the template image.
536
537 Parameters
538 ----------
539 template : `lsst.afw.image.ExposureF`
540 Template exposure, warped to match the science exposure.
541 science : `lsst.afw.image.ExposureF`
542 Science exposure to subtract from the template.
543 selectSources : `lsst.afw.table.SourceCatalog`
544 Identified sources on the science exposure. This catalog is used to
545 select sources in order to perform the AL PSF matching on stamp
546 images around them.
547
548 Returns
549 -------
550 results : `lsst.pipe.base.Struct`
551
552 ``difference`` : `lsst.afw.image.ExposureF`
553 Result of subtracting template and science.
554 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
555 Warped template exposure. Note that in this case, the template
556 is not PSF-matched to the science image.
557 ``backgroundModel`` : `lsst.afw.math.Function2D`
558 Background model that was fit while solving for the PSF-matching kernel
559 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
560 Kernel used to PSF-match the science image to the template.
561 """
562 bbox = science.getBBox()
563 kernelSources = self.makeKernel.selectKernelSources(science, template,
564 candidateList=selectSources,
565 preconvolved=False)
566 kernelResult = self.makeKernel.run(science, template, kernelSources,
567 preconvolved=False)
568 modelParams = kernelResult.backgroundModel.getParameters()
569 # We must invert the background model if the matching kernel is solved for the science image.
570 kernelResult.backgroundModel.setParameters([-p for p in modelParams])
571
572 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
573 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=False)
574
575 matchedScience = self._convolveExposure(science, kernelResult.psfMatchingKernel,
577 psf=template.psf)
578
579 # Place back on native photometric scale
580 matchedScience.maskedImage /= norm
581 matchedTemplate = template.clone()[bbox]
582 matchedTemplate.maskedImage /= norm
583 matchedTemplate.setPhotoCalib(science.photoCalib)
584
585 difference = _subtractImages(matchedScience, matchedTemplate,
586 backgroundModel=(kernelResult.backgroundModel
587 if self.config.doSubtractBackground else None))
588
589 correctedExposure = self.finalize(template, science, difference,
590 kernelResult.psfMatchingKernel,
591 templateMatched=False)
592
593 return lsst.pipe.base.Struct(difference=correctedExposure,
594 matchedTemplate=matchedTemplate,
595 matchedScience=matchedScience,
596 backgroundModel=kernelResult.backgroundModel,
597 psfMatchingKernel=kernelResult.psfMatchingKernel,)
598
599 def finalize(self, template, science, difference, kernel,
600 templateMatched=True,
601 preConvMode=False,
602 preConvKernel=None,
603 spatiallyVarying=False):
604 """Decorrelate the difference image to undo the noise correlations
605 caused by convolution.
606
607 Parameters
608 ----------
609 template : `lsst.afw.image.ExposureF`
610 Template exposure, warped to match the science exposure.
611 science : `lsst.afw.image.ExposureF`
612 Science exposure to subtract from the template.
613 difference : `lsst.afw.image.ExposureF`
614 Result of subtracting template and science.
615 kernel : `lsst.afw.math.Kernel`
616 An (optionally spatially-varying) PSF matching kernel
617 templateMatched : `bool`, optional
618 Was the template PSF-matched to the science image?
619 preConvMode : `bool`, optional
620 Was the science image preconvolved with its own PSF
621 before PSF matching the template?
622 preConvKernel : `lsst.afw.detection.Psf`, optional
623 If not `None`, then the science image was pre-convolved with
624 (the reflection of) this kernel. Must be normalized to sum to 1.
625 spatiallyVarying : `bool`, optional
626 Compute the decorrelation kernel spatially varying across the image?
627
628 Returns
629 -------
630 correctedExposure : `lsst.afw.image.ExposureF`
631 The decorrelated image difference.
632 """
633 if self.config.doDecorrelation:
634 self.log.info("Decorrelating image difference.")
635 # We have cleared the template mask plane, so copy the mask plane of
636 # the image difference so that we can calculate correct statistics
637 # during decorrelation
638 correctedExposure = self.decorrelate.run(science, template[science.getBBox()], difference, kernel,
639 templateMatched=templateMatched,
640 preConvMode=preConvMode,
641 preConvKernel=preConvKernel,
642 spatiallyVarying=spatiallyVarying).correctedExposure
643 else:
644 self.log.info("NOT decorrelating image difference.")
645 correctedExposure = difference
646 return correctedExposure
647
648 @staticmethod
649 def _validateExposures(template, science):
650 """Check that the WCS of the two Exposures match, and the template bbox
651 contains the science bbox.
652
653 Parameters
654 ----------
655 template : `lsst.afw.image.ExposureF`
656 Template exposure, warped to match the science exposure.
657 science : `lsst.afw.image.ExposureF`
658 Science exposure to subtract from the template.
659
660 Raises
661 ------
662 AssertionError
663 Raised if the WCS of the template is not equal to the science WCS,
664 or if the science image is not fully contained in the template
665 bounding box.
666 """
667 assert template.wcs == science.wcs,\
668 "Template and science exposure WCS are not identical."
669 templateBBox = template.getBBox()
670 scienceBBox = science.getBBox()
671
672 assert templateBBox.contains(scienceBBox),\
673 "Template bbox does not contain all of the science image."
674
675 def _convolveExposure(self, exposure, kernel, convolutionControl,
676 bbox=None,
677 psf=None,
678 photoCalib=None,
679 interpolateBadMaskPlanes=False,
680 ):
681 """Convolve an exposure with the given kernel.
682
683 Parameters
684 ----------
685 exposure : `lsst.afw.Exposure`
686 exposure to convolve.
687 kernel : `lsst.afw.math.LinearCombinationKernel`
688 PSF matching kernel computed in the ``makeKernel`` subtask.
689 convolutionControl : `lsst.afw.math.ConvolutionControl`
690 Configuration for convolve algorithm.
691 bbox : `lsst.geom.Box2I`, optional
692 Bounding box to trim the convolved exposure to.
693 psf : `lsst.afw.detection.Psf`, optional
694 Point spread function (PSF) to set for the convolved exposure.
695 photoCalib : `lsst.afw.image.PhotoCalib`, optional
696 Photometric calibration of the convolved exposure.
697
698 Returns
699 -------
700 convolvedExp : `lsst.afw.Exposure`
701 The convolved image.
702 """
703 convolvedExposure = exposure.clone()
704 if psf is not None:
705 convolvedExposure.setPsf(psf)
706 if photoCalib is not None:
707 convolvedExposure.setPhotoCalib(photoCalib)
708 if interpolateBadMaskPlanes and self.config.badMaskPlanes is not None:
709 nInterp = _interpolateImage(convolvedExposure.maskedImage,
710 self.config.badMaskPlanes)
711 self.metadata.add("nInterpolated", nInterp)
712 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
713 lsst.afw.math.convolve(convolvedImage, convolvedExposure.maskedImage, kernel, convolutionControl)
714 convolvedExposure.setMaskedImage(convolvedImage)
715 if bbox is None:
716 return convolvedExposure
717 else:
718 return convolvedExposure[bbox]
719
720 def _sourceSelector(self, sources, mask):
721 """Select sources from a catalog that meet the selection criteria.
722
723 Parameters
724 ----------
725 sources : `lsst.afw.table.SourceCatalog`
726 Input source catalog to select sources from.
727 mask : `lsst.afw.image.Mask`
728 The image mask plane to use to reject sources
729 based on their location on the ccd.
730
731 Returns
732 -------
733 selectSources : `lsst.afw.table.SourceCatalog`
734 The input source catalog, with flagged and low signal-to-noise
735 sources removed.
736
737 Raises
738 ------
739 RuntimeError
740 If there are too few sources to compute the PSF matching kernel
741 remaining after source selection.
742 """
743 flags = np.ones(len(sources), dtype=bool)
744 for flag in self.config.badSourceFlags:
745 try:
746 flags *= ~sources[flag]
747 except Exception as e:
748 self.log.warning("Could not apply source flag: %s", e)
749 signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr()
750 sToNFlag = signalToNoise > self.config.detectionThreshold
751 flags *= sToNFlag
752 sToNFlagMax = signalToNoise < self.config.detectionThresholdMax
753 flags *= sToNFlagMax
754 flags *= self._checkMask(mask, sources, self.config.excludeMaskPlanes)
755 selectSources = sources[flags].copy(deep=True)
756 if (len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
757 signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
758 indices = np.argsort(signalToNoise)
759 indices = indices[-self.config.maxKernelSources:]
760 flags = np.zeros(len(selectSources), dtype=bool)
761 flags[indices] = True
762 selectSources = selectSources[flags].copy(deep=True)
763
764 self.log.info("%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
765 len(selectSources), len(sources), 100*len(selectSources)/len(sources))
766 if len(selectSources) < self.config.minKernelSources:
767 self.log.error("Too few sources to calculate the PSF matching kernel: "
768 "%i selected but %i needed for the calculation.",
769 len(selectSources), self.config.minKernelSources)
770 raise RuntimeError("Cannot compute PSF matching kernel: too few sources selected.")
771 self.metadata.add("nPsfSources", len(selectSources))
772
773 return selectSources
774
775 @staticmethod
776 def _checkMask(mask, sources, excludeMaskPlanes):
777 """Exclude sources that are located on masked pixels.
778
779 Parameters
780 ----------
781 mask : `lsst.afw.image.Mask`
782 The image mask plane to use to reject sources
783 based on the location of their centroid on the ccd.
784 sources : `lsst.afw.table.SourceCatalog`
785 The source catalog to evaluate.
786 excludeMaskPlanes : `list` of `str`
787 List of the names of the mask planes to exclude.
788
789 Returns
790 -------
791 flags : `numpy.ndarray` of `bool`
792 Array indicating whether each source in the catalog should be
793 kept (True) or rejected (False) based on the value of the
794 mask plane at its location.
795 """
796 setExcludeMaskPlanes = [
797 maskPlane for maskPlane in excludeMaskPlanes if maskPlane in mask.getMaskPlaneDict()
798 ]
799
800 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
801
802 xv = np.rint(sources.getX() - mask.getX0())
803 yv = np.rint(sources.getY() - mask.getY0())
804
805 mv = mask.array[yv.astype(int), xv.astype(int)]
806 flags = np.bitwise_and(mv, excludePixelMask) == 0
807 return flags
808
809 def _prepareInputs(self, template, science, visitSummary=None):
810 """Perform preparatory calculations common to all Alard&Lupton Tasks.
811
812 Parameters
813 ----------
814 template : `lsst.afw.image.ExposureF`
815 Template exposure, warped to match the science exposure. The
816 variance plane of the template image is modified in place.
817 science : `lsst.afw.image.ExposureF`
818 Science exposure to subtract from the template. The variance plane
819 of the science image is modified in place.
820 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
821 Exposure catalog with external calibrations to be applied. Catalog
822 uses the detector id for the catalog id, sorted on id for fast
823 lookup.
824 """
825 self._validateExposures(template, science)
826 if visitSummary is not None:
827 self._applyExternalCalibrations(science, visitSummary=visitSummary)
828 templateCoverageFraction = checkTemplateIsSufficient(
829 template[science.getBBox()], self.log,
830 requiredTemplateFraction=self.config.requiredTemplateFraction,
831 exceptionMessage="Not attempting subtraction. To force subtraction,"
832 " set config requiredTemplateFraction=0"
833 )
834 self.metadata.add("templateCoveragePercent", 100*templateCoverageFraction)
835
836 if self.config.doScaleVariance:
837 # Scale the variance of the template and science images before
838 # convolution, subtraction, or decorrelation so that they have the
839 # correct ratio.
840 templateVarFactor = self.scaleVariance.run(template.maskedImage)
841 sciVarFactor = self.scaleVariance.run(science.maskedImage)
842 self.log.info("Template variance scaling factor: %.2f", templateVarFactor)
843 self.metadata.add("scaleTemplateVarianceFactor", templateVarFactor)
844 self.log.info("Science variance scaling factor: %.2f", sciVarFactor)
845 self.metadata.add("scaleScienceVarianceFactor", sciVarFactor)
846
847 # Erase existing detection mask planes.
848 # We don't want the detection mask from the science image
849 self.updateMasks(template, science)
850
851 def updateMasks(self, template, science):
852 """Update the science and template mask planes before differencing.
853
854 Parameters
855 ----------
856 template : `lsst.afw.image.Exposure`
857 Template exposure, warped to match the science exposure.
858 The template mask planes will be erased, except for a few specified
859 in the task config.
860 science : `lsst.afw.image.Exposure`
861 Science exposure to subtract from the template.
862 The DETECTED and DETECTED_NEGATIVE mask planes of the science image
863 will be erased.
864 """
865 self._clearMask(science.mask, clearMaskPlanes=["DETECTED", "DETECTED_NEGATIVE"])
866
867 # We will clear ALL template mask planes, except for those specified
868 # via the `preserveTemplateMask` config. Mask planes specified via
869 # the `renameTemplateMask` config will be copied to new planes with
870 # "_TEMPLATE" appended to their names, and the original mask plane will
871 # be cleared.
872 clearMaskPlanes = [mp for mp in template.mask.getMaskPlaneDict().keys()
873 if mp not in self.config.preserveTemplateMask]
874 renameMaskPlanes = [mp for mp in self.config.renameTemplateMask
875 if mp in template.mask.getMaskPlaneDict().keys()]
876
877 # propagate the mask plane related to Fake source injection
878 # NOTE: the fake source injection sets FAKE plane, but it should be INJECTED
879 # NOTE: This can be removed in DM-40796
880 if "FAKE" in science.mask.getMaskPlaneDict().keys():
881 self.log.info("Adding injected mask plane to science image")
882 self._renameMaskPlanes(science.mask, "FAKE", "INJECTED")
883 if "FAKE" in template.mask.getMaskPlaneDict().keys():
884 self.log.info("Adding injected mask plane to template image")
885 self._renameMaskPlanes(template.mask, "FAKE", "INJECTED_TEMPLATE")
886 if "INJECTED" in renameMaskPlanes:
887 renameMaskPlanes.remove("INJECTED")
888 if "INJECTED_TEMPLATE" in clearMaskPlanes:
889 clearMaskPlanes.remove("INJECTED_TEMPLATE")
890
891 for maskPlane in renameMaskPlanes:
892 self._renameMaskPlanes(template.mask, maskPlane, maskPlane + "_TEMPLATE")
893 self._clearMask(template.mask, clearMaskPlanes=clearMaskPlanes)
894
895 @staticmethod
896 def _renameMaskPlanes(mask, maskPlane, newMaskPlane):
897 """Rename a mask plane by adding the new name and copying the data.
898
899 Parameters
900 ----------
901 mask : `lsst.afw.image.Mask`
902 The mask image to update in place.
903 maskPlane : `str`
904 The name of the existing mask plane to copy.
905 newMaskPlane : `str`
906 The new name of the mask plane that will be added.
907 If the mask plane already exists, it will be updated in place.
908 """
909 mask.addMaskPlane(newMaskPlane)
910 originBitMask = mask.getPlaneBitMask(maskPlane)
911 destinationBitMask = mask.getPlaneBitMask(newMaskPlane)
912 mask.array |= ((mask.array & originBitMask) > 0)*destinationBitMask
913
914 def _clearMask(self, mask, clearMaskPlanes=None):
915 """Clear the mask plane of the template.
916
917 Parameters
918 ----------
919 mask : `lsst.afw.image.Mask`
920 The mask plane to erase, which will be modified in place.
921 clearMaskPlanes : `list` of `str`, optional
922 Erase the specified mask planes.
923 If not supplied, the entire mask will be erased.
924 """
925 if clearMaskPlanes is None:
926 clearMaskPlanes = list(mask.getMaskPlaneDict().keys())
927
928 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
929 mask &= ~bitMaskToClear
930
931
933 SubtractScoreOutputConnections):
934 pass
935
936
938 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
939 pass
940
941
943 """Subtract a template from a science image, convolving the science image
944 before computing the kernel, and also convolving the template before
945 subtraction.
946 """
947 ConfigClass = AlardLuptonPreconvolveSubtractConfig
948 _DefaultName = "alardLuptonPreconvolveSubtract"
949
950 def run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None):
951 """Preconvolve the science image with its own PSF,
952 convolve the template image with a PSF-matching kernel and subtract
953 from the preconvolved science image.
954
955 Parameters
956 ----------
957 template : `lsst.afw.image.ExposureF`
958 The template image, which has previously been warped to the science
959 image. The template bbox will be padded by a few pixels compared to
960 the science bbox.
961 science : `lsst.afw.image.ExposureF`
962 The science exposure.
963 sources : `lsst.afw.table.SourceCatalog`
964 Identified sources on the science exposure. This catalog is used to
965 select sources in order to perform the AL PSF matching on stamp
966 images around them.
967 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional
968 Exposure catalog with finalized psf models and aperture correction
969 maps to be applied. Catalog uses the detector id for the catalog
970 id, sorted on id for fast lookup. Deprecated in favor of
971 ``visitSummary``, and will be removed after v26.
972 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
973 Exposure catalog with complete external calibrations. Catalog uses
974 the detector id for the catalog id, sorted on id for fast lookup.
975 Ignored (for temporary backwards compatibility) if
976 ``finalizedPsfApCorrCatalog`` is provided.
977
978 Returns
979 -------
980 results : `lsst.pipe.base.Struct`
981 ``scoreExposure`` : `lsst.afw.image.ExposureF`
982 Result of subtracting the convolved template and science
983 images. Attached PSF is that of the original science image.
984 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
985 Warped and PSF-matched template exposure. Attached PSF is that
986 of the original science image.
987 ``matchedScience`` : `lsst.afw.image.ExposureF`
988 The science exposure after convolving with its own PSF.
989 Attached PSF is that of the original science image.
990 ``backgroundModel`` : `lsst.afw.math.Function2D`
991 Background model that was fit while solving for the
992 PSF-matching kernel
993 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
994 Final kernel used to PSF-match the template to the science
995 image.
996 """
997 if finalizedPsfApCorrCatalog is not None:
998 warnings.warn(
999 "The finalizedPsfApCorrCatalog argument is deprecated in favor of the visitSummary "
1000 "argument, and will be removed after v26.",
1001 FutureWarning,
1002 stacklevel=find_outside_stacklevel("lsst.ip.diffim"),
1003 )
1004 visitSummary = finalizedPsfApCorrCatalog
1005
1006 self._prepareInputs(template, science, visitSummary=visitSummary)
1007
1008 # TODO: DM-37212 we need to mirror the kernel in order to get correct cross correlation
1009 scienceKernel = science.psf.getKernel()
1010 matchedScience = self._convolveExposure(science, scienceKernel, self.convolutionControlconvolutionControl,
1011 interpolateBadMaskPlanes=True)
1012 self.metadata.add("convolvedExposure", "Preconvolution")
1013 try:
1014 selectSources = self._sourceSelector(sources, matchedScience.mask)
1015 subtractResults = self.runPreconvolve(template, science, matchedScience,
1016 selectSources, scienceKernel)
1017
1018 except (RuntimeError, lsst.pex.exceptions.Exception) as e:
1019 self.loglog.warning("Failed to match template. Checking coverage")
1020 # Raise NoWorkFound if template fraction is insufficient
1021 checkTemplateIsSufficient(template[science.getBBox()], self.loglog,
1022 self.config.minTemplateFractionForExpectedSuccess,
1023 exceptionMessage="Template coverage lower than expected to succeed."
1024 f" Failure is tolerable: {e}")
1025 # checkTemplateIsSufficient did not raise NoWorkFound, so raise original exception
1026 raise e
1027
1028 return subtractResults
1029
1030 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
1031 """Convolve the science image with its own PSF, then convolve the
1032 template with a matching kernel and subtract to form the Score
1033 exposure.
1034
1035 Parameters
1036 ----------
1037 template : `lsst.afw.image.ExposureF`
1038 Template exposure, warped to match the science exposure.
1039 science : `lsst.afw.image.ExposureF`
1040 Science exposure to subtract from the template.
1041 matchedScience : `lsst.afw.image.ExposureF`
1042 The science exposure, convolved with the reflection of its own PSF.
1043 selectSources : `lsst.afw.table.SourceCatalog`
1044 Identified sources on the science exposure. This catalog is used to
1045 select sources in order to perform the AL PSF matching on stamp
1046 images around them.
1047 preConvKernel : `lsst.afw.math.Kernel`
1048 The reflection of the kernel that was used to preconvolve the
1049 `science` exposure. Must be normalized to sum to 1.
1050
1051 Returns
1052 -------
1053 results : `lsst.pipe.base.Struct`
1054
1055 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1056 Result of subtracting the convolved template and science
1057 images. Attached PSF is that of the original science image.
1058 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1059 Warped and PSF-matched template exposure. Attached PSF is that
1060 of the original science image.
1061 ``matchedScience`` : `lsst.afw.image.ExposureF`
1062 The science exposure after convolving with its own PSF.
1063 Attached PSF is that of the original science image.
1064 ``backgroundModel`` : `lsst.afw.math.Function2D`
1065 Background model that was fit while solving for the
1066 PSF-matching kernel
1067 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1068 Final kernel used to PSF-match the template to the science
1069 image.
1070 """
1071 bbox = science.getBBox()
1072 innerBBox = preConvKernel.shrinkBBox(bbox)
1073
1074 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
1075 candidateList=selectSources,
1076 preconvolved=True)
1077 kernelResult = self.makeKernel.run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1078 preconvolved=True)
1079
1080 matchedTemplate = self._convolveExposure(template, kernelResult.psfMatchingKernel,
1082 bbox=bbox,
1083 psf=science.psf,
1084 interpolateBadMaskPlanes=True,
1085 photoCalib=science.photoCalib)
1086 score = _subtractImages(matchedScience, matchedTemplate,
1087 backgroundModel=(kernelResult.backgroundModel
1088 if self.config.doSubtractBackground else None))
1089 correctedScore = self.finalize(template[bbox], science, score,
1090 kernelResult.psfMatchingKernel,
1091 templateMatched=True, preConvMode=True,
1092 preConvKernel=preConvKernel)
1093
1094 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1095 matchedTemplate=matchedTemplate,
1096 matchedScience=matchedScience,
1097 backgroundModel=kernelResult.backgroundModel,
1098 psfMatchingKernel=kernelResult.psfMatchingKernel)
1099
1100
1101def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.,
1102 exceptionMessage=""):
1103 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1104
1105 Parameters
1106 ----------
1107 templateExposure : `lsst.afw.image.ExposureF`
1108 The template exposure to check
1109 logger : `lsst.log.Log`
1110 Logger for printing output.
1111 requiredTemplateFraction : `float`, optional
1112 Fraction of pixels of the science image required to have coverage
1113 in the template.
1114 exceptionMessage : `str`, optional
1115 Message to include in the exception raised if the template coverage
1116 is insufficient.
1117
1118 Returns
1119 -------
1120 templateCoverageFraction: `float`
1121 Fraction of pixels in the template with data.
1122
1123 Raises
1124 ------
1125 lsst.pipe.base.NoWorkFound
1126 Raised if fraction of good pixels, defined as not having NO_DATA
1127 set, is less than the requiredTemplateFraction
1128 """
1129 # Count the number of pixels with the NO_DATA mask bit set
1130 # counting NaN pixels is insufficient because pixels without data are often intepolated over)
1131 pixNoData = np.count_nonzero(templateExposure.mask.array
1132 & templateExposure.mask.getPlaneBitMask('NO_DATA'))
1133 pixGood = templateExposure.getBBox().getArea() - pixNoData
1134 templateCoverageFraction = pixGood/templateExposure.getBBox().getArea()
1135 logger.info("template has %d good pixels (%.1f%%)", pixGood, 100*templateCoverageFraction)
1136
1137 if templateCoverageFraction < requiredTemplateFraction:
1138 message = ("Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1139 100*templateCoverageFraction,
1140 100*requiredTemplateFraction))
1141 raise lsst.pipe.base.NoWorkFound(message + " " + exceptionMessage)
1142 return templateCoverageFraction
1143
1144
1145def _subtractImages(science, template, backgroundModel=None):
1146 """Subtract template from science, propagating relevant metadata.
1147
1148 Parameters
1149 ----------
1150 science : `lsst.afw.Exposure`
1151 The input science image.
1152 template : `lsst.afw.Exposure`
1153 The template to subtract from the science image.
1154 backgroundModel : `lsst.afw.MaskedImage`, optional
1155 Differential background model
1156
1157 Returns
1158 -------
1159 difference : `lsst.afw.Exposure`
1160 The subtracted image.
1161 """
1162 difference = science.clone()
1163 if backgroundModel is not None:
1164 difference.maskedImage -= backgroundModel
1165 difference.maskedImage -= template.maskedImage
1166 return difference
1167
1168
1169def _shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid):
1170 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1171
1172 Parameters
1173 ----------
1174 exp1 : `~lsst.afw.image.Exposure`
1175 Exposure with the reference point spread function (PSF) to evaluate.
1176 exp2 : `~lsst.afw.image.Exposure`
1177 Exposure with a candidate point spread function (PSF) to evaluate.
1178 fwhmExposureBuffer : `float`
1179 Fractional buffer margin to be left out of all sides of the image
1180 during the construction of the grid to compute mean PSF FWHM in an
1181 exposure, if the PSF is not available at its average position.
1182 fwhmExposureGrid : `int`
1183 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1184 available at its average position.
1185 Returns
1186 -------
1187 result : `bool`
1188 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1189 either dimension.
1190 """
1191 try:
1192 shape1 = getPsfFwhm(exp1.psf, average=False)
1193 shape2 = getPsfFwhm(exp2.psf, average=False)
1195 shape1 = evaluateMeanPsfFwhm(exp1,
1196 fwhmExposureBuffer=fwhmExposureBuffer,
1197 fwhmExposureGrid=fwhmExposureGrid
1198 )
1199 shape2 = evaluateMeanPsfFwhm(exp2,
1200 fwhmExposureBuffer=fwhmExposureBuffer,
1201 fwhmExposureGrid=fwhmExposureGrid
1202 )
1203 return shape1 <= shape2
1204
1205 # Results from getPsfFwhm is a tuple of two values, one for each dimension.
1206 xTest = shape1[0] <= shape2[0]
1207 yTest = shape1[1] <= shape2[1]
1208 return xTest | yTest
1209
1210
1211def _interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None):
1212 """Replace masked image pixels with interpolated values.
1213
1214 Parameters
1215 ----------
1216 maskedImage : `lsst.afw.image.MaskedImage`
1217 Image on which to perform interpolation.
1218 badMaskPlanes : `list` of `str`
1219 List of mask planes to interpolate over.
1220 fallbackValue : `float`, optional
1221 Value to set when interpolation fails.
1222
1223 Returns
1224 -------
1225 result: `float`
1226 The number of masked pixels that were replaced.
1227 """
1228 imgBadMaskPlanes = [
1229 maskPlane for maskPlane in badMaskPlanes if maskPlane in maskedImage.mask.getMaskPlaneDict()
1230 ]
1231
1232 image = maskedImage.image.array
1233 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1234 image[badPixels] = np.nan
1235 if fallbackValue is None:
1236 fallbackValue = np.nanmedian(image)
1237 # For this initial implementation, skip the interpolation and just fill with
1238 # the median value.
1239 image[badPixels] = fallbackValue
1240 return np.sum(badPixels)
Asseses the quality of a candidate given a spatial kernel and background model.
runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel)
run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None)
_prepareInputs(self, template, science, visitSummary=None)
runConvolveTemplate(self, template, science, selectSources)
_convolveExposure(self, exposure, kernel, convolutionControl, bbox=None, psf=None, photoCalib=None, interpolateBadMaskPlanes=False)
runConvolveScience(self, template, science, selectSources)
run(self, template, science, sources, finalizedPsfApCorrCatalog=None, visitSummary=None)
finalize(self, template, science, difference, kernel, templateMatched=True, preConvMode=False, preConvKernel=None, spatiallyVarying=False)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
_subtractImages(science, template, backgroundModel=None)
_interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None)
checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0., exceptionMessage="")
_shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid)