Coverage for python/lsst/pipe/tasks/snapCombine.py: 26%
140 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-08 06:53 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-08 06:53 -0700
1# This file is part of pipe_tasks.
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/>.
22__all__ = ["InitialPsfConfig", "SnapCombineConfig", "SnapCombineTask"]
24import numpy as num
25import lsst.pex.config as pexConfig
26import lsst.daf.base as dafBase
27import lsst.afw.image as afwImage
28import lsst.afw.table as afwTable
29import lsst.pipe.base as pipeBase
30from lsstDebug import getDebugFrame
31from lsst.afw.display import getDisplay
32from lsst.coadd.utils import addToCoadd, setCoaddEdgeBits
33from lsst.meas.algorithms import SourceDetectionTask
34from lsst.meas.base import SingleFrameMeasurementTask
35import lsst.meas.algorithms as measAlg
36from lsst.utils.timer import timeMethod
38from .repair import RepairTask
41class InitialPsfConfig(pexConfig.Config):
42 """Describes the initial PSF used for detection and measurement before we do PSF determination."""
44 model = pexConfig.ChoiceField(
45 dtype=str,
46 doc="PSF model type",
47 default="SingleGaussian",
48 allowed={
49 "SingleGaussian": "Single Gaussian model",
50 "DoubleGaussian": "Double Gaussian model",
51 },
52 )
53 pixelScale = pexConfig.Field(
54 dtype=float,
55 doc="Pixel size (arcsec). Only needed if no Wcs is provided",
56 default=0.25,
57 )
58 fwhm = pexConfig.Field(
59 dtype=float,
60 doc="FWHM of PSF model (arcsec)",
61 default=1.0,
62 )
63 size = pexConfig.Field(
64 dtype=int,
65 doc="Size of PSF model (pixels)",
66 default=15,
67 )
70class SnapCombineConfig(pexConfig.Config):
71 doRepair = pexConfig.Field(
72 dtype=bool,
73 doc="Repair images (CR reject and interpolate) before combining",
74 default=True,
75 )
76 repairPsfFwhm = pexConfig.Field(
77 dtype=float,
78 doc="Psf FWHM (pixels) used to detect CRs",
79 default=2.5,
80 )
81 doDiffIm = pexConfig.Field(
82 dtype=bool,
83 doc="Perform difference imaging before combining",
84 default=False,
85 )
86 doPsfMatch = pexConfig.Field(
87 dtype=bool,
88 doc="Perform PSF matching for difference imaging (ignored if doDiffIm false)",
89 default=True,
90 )
91 doMeasurement = pexConfig.Field(
92 dtype=bool,
93 doc="Measure difference sources (ignored if doDiffIm false)",
94 default=True,
95 )
96 badMaskPlanes = pexConfig.ListField(
97 dtype=str,
98 doc="Mask planes that, if set, the associated pixels are not included in the combined exposure; "
99 "DETECTED excludes cosmic rays",
100 default=("DETECTED",),
101 )
102 averageKeys = pexConfig.ListField(
103 dtype=str,
104 doc="List of float metadata keys to average when combining snaps, e.g. float positions and dates; "
105 "non-float data must be handled by overriding the fixMetadata method",
106 optional=True,
108 )
109 sumKeys = pexConfig.ListField(
110 dtype=str,
111 doc="List of float or int metadata keys to sum when combining snaps, e.g. exposure time; "
112 "non-float, non-int data must be handled by overriding the fixMetadata method",
113 optional=True,
114 )
116 repair = pexConfig.ConfigurableField(target=RepairTask, doc="")
117 # Target `SnapPsfMatchTask` removed in DM-38846
118 # diffim = pexConfig.ConfigurableField(target=SnapPsfMatchTask, doc="")
119 detection = pexConfig.ConfigurableField(target=SourceDetectionTask, doc="")
120 initialPsf = pexConfig.ConfigField(dtype=InitialPsfConfig, doc="")
121 measurement = pexConfig.ConfigurableField(target=SingleFrameMeasurementTask, doc="")
123 def setDefaults(self):
124 self.detection.thresholdPolarity = "both"
126 def validate(self):
127 if self.detection.thresholdPolarity != "both":
128 raise ValueError("detection.thresholdPolarity must be 'both' for SnapCombineTask")
131class SnapCombineTask(pipeBase.Task):
132 """Combine two snaps into a single visit image.
134 Notes
135 -----
136 Debugging:
137 The `~lsst.base.lsstDebug` variables in SnapCombineTask are:
139 display
140 A dictionary containing debug point names as keys with frame number as value. Valid keys are:
142 .. code-block:: none
144 repair0
145 Display the first snap after repairing.
146 repair1
147 Display the second snap after repairing.
148 """
150 ConfigClass = SnapCombineConfig
151 _DefaultName = "snapCombine"
153 def __init__(self, *args, **kwargs):
154 pipeBase.Task.__init__(self, *args, **kwargs)
155 self.makeSubtask("repair")
156 self.schema = afwTable.SourceTable.makeMinimalSchema()
157 self.algMetadata = dafBase.PropertyList()
158 self.makeSubtask("detection", schema=self.schema)
159 if self.config.doMeasurement:
160 self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
162 @timeMethod
163 def run(self, snap0, snap1, defects=None):
164 """Combine two snaps.
166 Parameters
167 ----------
168 snap0 : `Unknown`
169 Snapshot exposure 0.
170 snap1 : `Unknown`
171 Snapshot exposure 1.
172 defects : `list` or `None`, optional
173 Defect list (for repair task).
175 Returns
176 -------
177 result : `lsst.pipe.base.Struct`
178 Results as a struct with attributes:
180 ``exposure``
181 Snap-combined exposure.
182 ``sources``
183 Detected sources, or `None` if detection not performed.
184 """
185 # initialize optional outputs
186 sources = None
188 if self.config.doRepair:
189 self.log.info("snapCombine repair")
190 psf = self.makeInitialPsf(snap0, fwhmPix=self.config.repairPsfFwhm)
191 snap0.setPsf(psf)
192 snap1.setPsf(psf)
193 self.repair.run(snap0, defects=defects, keepCRs=False)
194 self.repair.run(snap1, defects=defects, keepCRs=False)
196 repair0frame = getDebugFrame(self._display, "repair0")
197 if repair0frame:
198 getDisplay(repair0frame).mtv(snap0)
199 repair1frame = getDebugFrame(self._display, "repair1")
200 if repair1frame:
201 getDisplay(repair1frame).mtv(snap1)
203 if self.config.doDiffIm:
204 if self.config.doPsfMatch:
205 raise NotImplementedError("PSF-matching of snaps is not yet supported.")
207 else:
208 diffExp = afwImage.ExposureF(snap0, True)
209 diffMi = diffExp.getMaskedImage()
210 diffMi -= snap1.getMaskedImage()
212 psf = self.makeInitialPsf(snap0)
213 diffExp.setPsf(psf)
214 table = afwTable.SourceTable.make(self.schema)
215 table.setMetadata(self.algMetadata)
216 detRet = self.detection.run(table, diffExp)
217 sources = detRet.sources
218 if self.config.doMeasurement:
219 self.measurement.measure(diffExp, sources)
221 mask0 = snap0.getMaskedImage().getMask()
222 mask1 = snap1.getMaskedImage().getMask()
223 detRet.positive.setMask(mask0, "DETECTED")
224 detRet.negative.setMask(mask1, "DETECTED")
226 maskD = diffExp.getMaskedImage().getMask()
227 detRet.positive.setMask(maskD, "DETECTED")
228 detRet.negative.setMask(maskD, "DETECTED_NEGATIVE")
230 combinedExp = self.addSnaps(snap0, snap1)
232 return pipeBase.Struct(
233 exposure=combinedExp,
234 sources=sources,
235 )
237 def addSnaps(self, snap0, snap1):
238 """Add two snap exposures together, returning a new exposure.
240 Parameters
241 ----------
242 snap0 : `Unknown`
243 Snap exposure 0.
244 snap1 : `Unknown`
245 Snap exposure 1.
247 Returns
248 -------
249 combinedExp : `Unknown`
250 Combined exposure.
251 """
252 self.log.info("snapCombine addSnaps")
254 combinedExp = snap0.Factory(snap0, True)
255 combinedMi = combinedExp.getMaskedImage()
256 combinedMi.set(0)
258 weightMap = combinedMi.getImage().Factory(combinedMi.getBBox())
259 weight = 1.0
260 badPixelMask = afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes)
261 addToCoadd(combinedMi, weightMap, snap0.getMaskedImage(), badPixelMask, weight)
262 addToCoadd(combinedMi, weightMap, snap1.getMaskedImage(), badPixelMask, weight)
264 # pre-scaling the weight map instead of post-scaling the combinedMi saves a bit of time
265 # because the weight map is a simple Image instead of a MaskedImage
266 weightMap *= 0.5 # so result is sum of both images, instead of average
267 combinedMi /= weightMap
268 setCoaddEdgeBits(combinedMi.getMask(), weightMap)
270 # note: none of the inputs has a valid PhotoCalib object, so that is not touched
271 # Filter was already copied
273 combinedMetadata = combinedExp.getMetadata()
274 metadata0 = snap0.getMetadata()
275 metadata1 = snap1.getMetadata()
276 self.fixMetadata(combinedMetadata, metadata0, metadata1)
278 return combinedExp
280 def fixMetadata(self, combinedMetadata, metadata0, metadata1):
281 """Fix the metadata of the combined exposure (in place).
283 This implementation handles items specified by config.averageKeys and config.sumKeys,
284 which have data type restrictions. To handle other data types (such as sexagesimal
285 positions and ISO dates) you must supplement this method with your own code.
287 Parameters
288 ----------
289 combinedMetadata : `lsst.daf.base.PropertySet`
290 Metadata of combined exposure;
291 on input this is a deep copy of metadata0 (a PropertySet).
292 metadata0 : `lsst.daf.base.PropertySet`
293 Metadata of snap0 (a PropertySet).
294 metadata1 : `lsst.daf.base.PropertySet`
295 Metadata of snap1 (a PropertySet).
297 Notes
298 -----
299 The inputs are presently PropertySets due to ticket #2542. However, in some sense
300 they are just PropertyLists that are missing some methods. In particular: comments and order
301 are preserved if you alter an existing value with set(key, value).
302 """
303 keyDoAvgList = []
304 if self.config.averageKeys:
305 keyDoAvgList += [(key, 1) for key in self.config.averageKeys]
306 if self.config.sumKeys:
307 keyDoAvgList += [(key, 0) for key in self.config.sumKeys]
308 for key, doAvg in keyDoAvgList:
309 opStr = "average" if doAvg else "sum"
310 try:
311 val0 = metadata0.getScalar(key)
312 val1 = metadata1.getScalar(key)
313 except Exception:
314 self.log.warning("Could not %s metadata %r: missing from one or both exposures", opStr, key)
315 continue
317 try:
318 combinedVal = val0 + val1
319 if doAvg:
320 combinedVal /= 2.0
321 except Exception:
322 self.log.warning("Could not %s metadata %r: value %r and/or %r not numeric",
323 opStr, key, val0, val1)
324 continue
326 combinedMetadata.set(key, combinedVal)
328 def makeInitialPsf(self, exposure, fwhmPix=None):
329 """Initialise the detection procedure by setting the PSF and WCS.
331 exposure : `lsst.afw.image.Exposure`
332 Exposure to process.
334 Returns
335 -------
336 psf : `Unknown`
337 PSF, WCS
339 AssertionError
340 Raised if any of the following occur:
341 - No exposure provided.
342 - No wcs in exposure.
343 """
344 assert exposure, "No exposure provided"
345 wcs = exposure.getWcs()
346 assert wcs, "No wcs in exposure"
348 if fwhmPix is None:
349 fwhmPix = self.config.initialPsf.fwhm / wcs.getPixelScale().asArcseconds()
351 size = self.config.initialPsf.size
352 model = self.config.initialPsf.model
353 self.log.info("installInitialPsf fwhm=%s pixels; size=%s pixels", fwhmPix, size)
354 psfCls = getattr(measAlg, model + "Psf")
355 psf = psfCls(size, size, fwhmPix/(2.0*num.sqrt(2*num.log(2.0))))
356 return psf