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