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