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