LSST Applications 26.0.0,g0265f82a02+6660c170cc,g07994bdeae+30b05a742e,g0a0026dc87+17526d298f,g0a60f58ba1+17526d298f,g0e4bf8285c+96dd2c2ea9,g0ecae5effc+c266a536c8,g1e7d6db67d+6f7cb1f4bb,g26482f50c6+6346c0633c,g2bbee38e9b+6660c170cc,g2cc88a2952+0a4e78cd49,g3273194fdb+f6908454ef,g337abbeb29+6660c170cc,g337c41fc51+9a8f8f0815,g37c6e7c3d5+7bbafe9d37,g44018dc512+6660c170cc,g4a941329ef+4f7594a38e,g4c90b7bd52+5145c320d2,g58be5f913a+bea990ba40,g635b316a6c+8d6b3a3e56,g67924a670a+bfead8c487,g6ae5381d9b+81bc2a20b4,g93c4d6e787+26b17396bd,g98cecbdb62+ed2cb6d659,g98ffbb4407+81bc2a20b4,g9ddcbc5298+7f7571301f,ga1e77700b3+99e9273977,gae46bcf261+6660c170cc,gb2715bf1a1+17526d298f,gc86a011abf+17526d298f,gcf0d15dbbd+96dd2c2ea9,gdaeeff99f8+0d8dbea60f,gdb4ec4c597+6660c170cc,ge23793e450+96dd2c2ea9,gf041782ebf+171108ac67
LSST Data Management Base Package
Loading...
Searching...
No Matches
snapCombine.py
Go to the documentation of this file.
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/>.
21
22__all__ = ["InitialPsfConfig", "SnapCombineConfig", "SnapCombineTask"]
23
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
37
38from .repair import RepairTask
39
40
41class InitialPsfConfig(pexConfig.Config):
42 """Describes the initial PSF used for detection and measurement before we do PSF determination."""
43
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 )
68
69
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,
107 )
108 sumKeys = pexConfig.ListField(
109 dtype=str,
110 doc="List of float or int metadata keys to sum when combining snaps, e.g. exposure time; "
111 "non-float, non-int data must be handled by overriding the fixMetadata method",
112 optional=True,
113 )
114
115 repair = pexConfig.ConfigurableField(target=RepairTask, doc="RepairTask configuration")
116 # Target `SnapPsfMatchTask` removed in DM-38846
117 # diffim = pexConfig.ConfigurableField(target=SnapPsfMatchTask, doc="")
118 detection = pexConfig.ConfigurableField(
119 target=SourceDetectionTask, doc="SourceDetectionTask configuration"
120 )
121 initialPsf = pexConfig.ConfigField(dtype=InitialPsfConfig, doc="InitialPsfConfig configuration")
122 measurement = pexConfig.ConfigurableField(
123 target=SingleFrameMeasurementTask, doc="SingleFrameMeasurementTask configuration"
124 )
125
126 def setDefaults(self):
127 self.detection.thresholdPolarity = "both"
128
129 def validate(self):
130 if self.detection.thresholdPolarity != "both":
131 raise ValueError("detection.thresholdPolarity must be 'both' for SnapCombineTask")
132
133
134class SnapCombineTask(pipeBase.Task):
135 """Combine two snaps into a single visit image.
136
137 Notes
138 -----
139 Debugging:
140 The `~lsst.base.lsstDebug` variables in SnapCombineTask are:
141
142 display
143 A dictionary containing debug point names as keys with frame number as value. Valid keys are:
144
145 .. code-block:: none
146
147 repair0
148 Display the first snap after repairing.
149 repair1
150 Display the second snap after repairing.
151 """
152
153 ConfigClass = SnapCombineConfig
154 _DefaultName = "snapCombine"
155
156 def __init__(self, *args, **kwargs):
157 pipeBase.Task.__init__(self, *args, **kwargs)
158 self.makeSubtask("repair")
159 self.schema = afwTable.SourceTable.makeMinimalSchema()
161 self.makeSubtask("detection", schema=self.schema)
162 if self.config.doMeasurement:
163 self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
164
165 @timeMethod
166 def run(self, snap0, snap1, defects=None):
167 """Combine two snaps.
168
169 Parameters
170 ----------
171 snap0 : `Unknown`
172 Snapshot exposure 0.
173 snap1 : `Unknown`
174 Snapshot exposure 1.
175 defects : `list` or `None`, optional
176 Defect list (for repair task).
177
178 Returns
179 -------
180 result : `lsst.pipe.base.Struct`
181 Results as a struct with attributes:
182
183 ``exposure``
184 Snap-combined exposure.
185 ``sources``
186 Detected sources, or `None` if detection not performed.
187 """
188 # initialize optional outputs
189 sources = None
190
191 if self.config.doRepair:
192 self.log.info("snapCombine repair")
193 psf = self.makeInitialPsf(snap0, fwhmPix=self.config.repairPsfFwhm)
194 snap0.setPsf(psf)
195 snap1.setPsf(psf)
196 self.repair.run(snap0, defects=defects, keepCRs=False)
197 self.repair.run(snap1, defects=defects, keepCRs=False)
198
199 repair0frame = getDebugFrame(self._display, "repair0")
200 if repair0frame:
201 getDisplay(repair0frame).mtv(snap0)
202 repair1frame = getDebugFrame(self._display, "repair1")
203 if repair1frame:
204 getDisplay(repair1frame).mtv(snap1)
205
206 if self.config.doDiffIm:
207 if self.config.doPsfMatch:
208 raise NotImplementedError("PSF-matching of snaps is not yet supported.")
209
210 else:
211 diffExp = afwImage.ExposureF(snap0, True)
212 diffMi = diffExp.getMaskedImage()
213 diffMi -= snap1.getMaskedImage()
214
215 psf = self.makeInitialPsf(snap0)
216 diffExp.setPsf(psf)
217 table = afwTable.SourceTable.make(self.schema)
218 table.setMetadata(self.algMetadata)
219 detRet = self.detection.run(table, diffExp)
220 sources = detRet.sources
221 if self.config.doMeasurement:
222 self.measurement.measure(diffExp, sources)
223
224 mask0 = snap0.getMaskedImage().getMask()
225 mask1 = snap1.getMaskedImage().getMask()
226 detRet.positive.setMask(mask0, "DETECTED")
227 detRet.negative.setMask(mask1, "DETECTED")
228
229 maskD = diffExp.getMaskedImage().getMask()
230 detRet.positive.setMask(maskD, "DETECTED")
231 detRet.negative.setMask(maskD, "DETECTED_NEGATIVE")
232
233 combinedExp = self.addSnaps(snap0, snap1)
234
235 return pipeBase.Struct(
236 exposure=combinedExp,
237 sources=sources,
238 )
239
240 def addSnaps(self, snap0, snap1):
241 """Add two snap exposures together, returning a new exposure.
242
243 Parameters
244 ----------
245 snap0 : `Unknown`
246 Snap exposure 0.
247 snap1 : `Unknown`
248 Snap exposure 1.
249
250 Returns
251 -------
252 combinedExp : `Unknown`
253 Combined exposure.
254 """
255 self.log.info("snapCombine addSnaps")
256
257 combinedExp = snap0.Factory(snap0, True)
258 combinedMi = combinedExp.getMaskedImage()
259 combinedMi.set(0)
260
261 weightMap = combinedMi.getImage().Factory(combinedMi.getBBox())
262 weight = 1.0
263 badPixelMask = afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes)
264 addToCoadd(combinedMi, weightMap, snap0.getMaskedImage(), badPixelMask, weight)
265 addToCoadd(combinedMi, weightMap, snap1.getMaskedImage(), badPixelMask, weight)
266
267 # pre-scaling the weight map instead of post-scaling the combinedMi saves a bit of time
268 # because the weight map is a simple Image instead of a MaskedImage
269 weightMap *= 0.5 # so result is sum of both images, instead of average
270 combinedMi /= weightMap
271 setCoaddEdgeBits(combinedMi.getMask(), weightMap)
272
273 # note: none of the inputs has a valid PhotoCalib object, so that is not touched
274 # Filter was already copied
275
276 combinedMetadata = combinedExp.getMetadata()
277 metadata0 = snap0.getMetadata()
278 metadata1 = snap1.getMetadata()
279 self.fixMetadata(combinedMetadata, metadata0, metadata1)
280
281 return combinedExp
282
283 def fixMetadata(self, combinedMetadata, metadata0, metadata1):
284 """Fix the metadata of the combined exposure (in place).
285
286 This implementation handles items specified by config.averageKeys and config.sumKeys,
287 which have data type restrictions. To handle other data types (such as sexagesimal
288 positions and ISO dates) you must supplement this method with your own code.
289
290 Parameters
291 ----------
292 combinedMetadata : `lsst.daf.base.PropertySet`
293 Metadata of combined exposure;
294 on input this is a deep copy of metadata0 (a PropertySet).
295 metadata0 : `lsst.daf.base.PropertySet`
296 Metadata of snap0 (a PropertySet).
297 metadata1 : `lsst.daf.base.PropertySet`
298 Metadata of snap1 (a PropertySet).
299
300 Notes
301 -----
302 The inputs are presently PropertySets due to ticket #2542. However, in some sense
303 they are just PropertyLists that are missing some methods. In particular: comments and order
304 are preserved if you alter an existing value with set(key, value).
305 """
306 keyDoAvgList = []
307 if self.config.averageKeys:
308 keyDoAvgList += [(key, 1) for key in self.config.averageKeys]
309 if self.config.sumKeys:
310 keyDoAvgList += [(key, 0) for key in self.config.sumKeys]
311 for key, doAvg in keyDoAvgList:
312 opStr = "average" if doAvg else "sum"
313 try:
314 val0 = metadata0.getScalar(key)
315 val1 = metadata1.getScalar(key)
316 except Exception:
317 self.log.warning("Could not %s metadata %r: missing from one or both exposures", opStr, key)
318 continue
319
320 try:
321 combinedVal = val0 + val1
322 if doAvg:
323 combinedVal /= 2.0
324 except Exception:
325 self.log.warning("Could not %s metadata %r: value %r and/or %r not numeric",
326 opStr, key, val0, val1)
327 continue
328
329 combinedMetadata.set(key, combinedVal)
330
331 def makeInitialPsf(self, exposure, fwhmPix=None):
332 """Initialise the detection procedure by setting the PSF and WCS.
333
334 exposure : `lsst.afw.image.Exposure`
335 Exposure to process.
336
337 Returns
338 -------
339 psf : `Unknown`
340 PSF, WCS
341
342 AssertionError
343 Raised if any of the following occur:
344 - No exposure provided.
345 - No wcs in exposure.
346 """
347 assert exposure, "No exposure provided"
348 wcs = exposure.getWcs()
349 assert wcs, "No wcs in exposure"
350
351 if fwhmPix is None:
352 fwhmPix = self.config.initialPsf.fwhm / wcs.getPixelScale().asArcseconds()
353
354 size = self.config.initialPsf.size
355 model = self.config.initialPsf.model
356 self.log.info("installInitialPsf fwhm=%s pixels; size=%s pixels", fwhmPix, size)
357 psfCls = getattr(measAlg, model + "Psf")
358 psf = psfCls(size, size, fwhmPix/(2.0*num.sqrt(2*num.log(2.0))))
359 return psf
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition Exposure.h:72
Class for storing ordered metadata with comments.
Class for storing generic metadata.
Definition PropertySet.h:66
makeInitialPsf(self, exposure, fwhmPix=None)
fixMetadata(self, combinedMetadata, metadata0, metadata1)
daf::base::PropertySet * set
Definition fits.cc:927