LSST Applications g070148d5b3+33e5256705,g0d53e28543+25c8b88941,g0da5cf3356+2dd1178308,g1081da9e2a+62d12e78cb,g17e5ecfddb+7e422d6136,g1c76d35bf8+ede3a706f7,g295839609d+225697d880,g2e2c1a68ba+cc1f6f037e,g2ffcdf413f+853cd4dcde,g38293774b4+62d12e78cb,g3b44f30a73+d953f1ac34,g48ccf36440+885b902d19,g4b2f1765b6+7dedbde6d2,g5320a0a9f6+0c5d6105b6,g56b687f8c9+ede3a706f7,g5c4744a4d9+ef6ac23297,g5ffd174ac0+0c5d6105b6,g6075d09f38+66af417445,g667d525e37+2ced63db88,g670421136f+2ced63db88,g71f27ac40c+2ced63db88,g774830318a+463cbe8d1f,g7876bc68e5+1d137996f1,g7985c39107+62d12e78cb,g7fdac2220c+0fd8241c05,g96f01af41f+368e6903a7,g9ca82378b8+2ced63db88,g9d27549199+ef6ac23297,gabe93b2c52+e3573e3735,gb065e2a02a+3dfbe639da,gbc3249ced9+0c5d6105b6,gbec6a3398f+0c5d6105b6,gc9534b9d65+35b9f25267,gd01420fc67+0c5d6105b6,geee7ff78d7+a14128c129,gf63283c776+ede3a706f7,gfed783d017+0c5d6105b6,w.2022.47
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.ip.diffim import SnapPsfMatchTask
34from lsst.meas.algorithms import SourceDetectionTask
35from lsst.meas.base import SingleFrameMeasurementTask
36import lsst.meas.algorithms as measAlg
37from lsst.utils.timer import timeMethod
38
39from .repair import RepairTask
40
41
42class InitialPsfConfig(pexConfig.Config):
43 """Describes the initial PSF used for detection and measurement before we do PSF determination."""
44
45 model = pexConfig.ChoiceField(
46 dtype=str,
47 doc="PSF model type",
48 default="SingleGaussian",
49 allowed={
50 "SingleGaussian": "Single Gaussian model",
51 "DoubleGaussian": "Double Gaussian model",
52 },
53 )
54 pixelScale = pexConfig.Field(
55 dtype=float,
56 doc="Pixel size (arcsec). Only needed if no Wcs is provided",
57 default=0.25,
58 )
59 fwhm = pexConfig.Field(
60 dtype=float,
61 doc="FWHM of PSF model (arcsec)",
62 default=1.0,
63 )
64 size = pexConfig.Field(
65 dtype=int,
66 doc="Size of PSF model (pixels)",
67 default=15,
68 )
69
70
71class SnapCombineConfig(pexConfig.Config):
72 doRepair = pexConfig.Field(
73 dtype=bool,
74 doc="Repair images (CR reject and interpolate) before combining",
75 default=True,
76 )
77 repairPsfFwhm = pexConfig.Field(
78 dtype=float,
79 doc="Psf FWHM (pixels) used to detect CRs",
80 default=2.5,
81 )
82 doDiffIm = pexConfig.Field(
83 dtype=bool,
84 doc="Perform difference imaging before combining",
85 default=False,
86 )
87 doPsfMatch = pexConfig.Field(
88 dtype=bool,
89 doc="Perform PSF matching for difference imaging (ignored if doDiffIm false)",
90 default=True,
91 )
92 doMeasurement = pexConfig.Field(
93 dtype=bool,
94 doc="Measure difference sources (ignored if doDiffIm false)",
95 default=True,
96 )
97 badMaskPlanes = pexConfig.ListField(
98 dtype=str,
99 doc="Mask planes that, if set, the associated pixels are not included in the combined exposure; "
100 "DETECTED excludes cosmic rays",
101 default=("DETECTED",),
102 )
103 averageKeys = pexConfig.ListField(
104 dtype=str,
105 doc="List of float metadata keys to average when combining snaps, e.g. float positions and dates; "
106 "non-float data must be handled by overriding the fixMetadata method",
107 optional=True,
108
109 )
110 sumKeys = pexConfig.ListField(
111 dtype=str,
112 doc="List of float or int metadata keys to sum when combining snaps, e.g. exposure time; "
113 "non-float, non-int data must be handled by overriding the fixMetadata method",
114 optional=True,
115 )
116
117 repair = pexConfig.ConfigurableField(target=RepairTask, doc="")
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="")
122
123 def setDefaults(self):
124 self.detection.thresholdPolarity = "both"
125
126 def validate(self):
127 if self.detection.thresholdPolarity != "both":
128 raise ValueError("detection.thresholdPolarity must be 'both' for SnapCombineTask")
129
130
131class SnapCombineTask(pipeBase.Task):
132 """Combine two snaps into a single visit image.
133
134 Notes
135 -----
136 Debugging:
137 The `~lsst.base.lsstDebug` variables in SnapCombineTask are:
138
139 display
140 A dictionary containing debug point names as keys with frame number as value. Valid keys are:
141
142 .. code-block:: none
143
144 repair0
145 Display the first snap after repairing.
146 repair1
147 Display the second snap after repairing.
148 """
149
150 ConfigClass = SnapCombineConfig
151 _DefaultName = "snapCombine"
152
153 def __init__(self, *args, **kwargs):
154 pipeBase.Task.__init__(self, *args, **kwargs)
155 self.makeSubtask("repair")
156 self.makeSubtask("diffim")
157 self.schema = afwTable.SourceTable.makeMinimalSchema()
159 self.makeSubtask("detection", schema=self.schema)
160 if self.config.doMeasurement:
161 self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
162
163 @timeMethod
164 def run(self, snap0, snap1, defects=None):
165 """Combine two snaps.
166
167 Parameters
168 ----------
169 snap0 : `Unknown`
170 Snapshot exposure 0.
171 snap1 : `Unknown`
172 Snapshot exposure 1.
173 defects : `list` or `None`, optional
174 Defect list (for repair task).
175
176 Returns
177 -------
178 result : `lsst.pipe.base.Struct`
179 Results as a struct with attributes:
180
181 ``exposure``
182 Snap-combined exposure.
183 ``sources``
184 Detected sources, or `None` if detection not performed.
185 """
186 # initialize optional outputs
187 sources = None
188
189 if self.config.doRepair:
190 self.log.info("snapCombine repair")
191 psf = self.makeInitialPsf(snap0, fwhmPix=self.config.repairPsfFwhm)
192 snap0.setPsf(psf)
193 snap1.setPsf(psf)
194 self.repair.run(snap0, defects=defects, keepCRs=False)
195 self.repair.run(snap1, defects=defects, keepCRs=False)
196
197 repair0frame = getDebugFrame(self._display, "repair0")
198 if repair0frame:
199 getDisplay(repair0frame).mtv(snap0)
200 repair1frame = getDebugFrame(self._display, "repair1")
201 if repair1frame:
202 getDisplay(repair1frame).mtv(snap1)
203
204 if self.config.doDiffIm:
205 if self.config.doPsfMatch:
206 self.log.info("snapCombine psfMatch")
207 diffRet = self.diffim.run(snap0, snap1, "subtractExposures")
208 diffExp = diffRet.subtractedImage
209
210 # Measure centroid and width of kernel; dependent on ticket #1980
211 # Useful diagnostic for the degree of astrometric shift between snaps.
212 diffKern = diffRet.psfMatchingKernel
213 width, height = diffKern.getDimensions()
214
215 else:
216 diffExp = afwImage.ExposureF(snap0, True)
217 diffMi = diffExp.getMaskedImage()
218 diffMi -= snap1.getMaskedImage()
219
220 psf = self.makeInitialPsf(snap0)
221 diffExp.setPsf(psf)
222 table = afwTable.SourceTable.make(self.schema)
223 table.setMetadata(self.algMetadata)
224 detRet = self.detection.run(table, diffExp)
225 sources = detRet.sources
226 fpSets = detRet.fpSets
227 if self.config.doMeasurement:
228 self.measurement.measure(diffExp, sources)
229
230 mask0 = snap0.getMaskedImage().getMask()
231 mask1 = snap1.getMaskedImage().getMask()
232 fpSets.positive.setMask(mask0, "DETECTED")
233 fpSets.negative.setMask(mask1, "DETECTED")
234
235 maskD = diffExp.getMaskedImage().getMask()
236 fpSets.positive.setMask(maskD, "DETECTED")
237 fpSets.negative.setMask(maskD, "DETECTED_NEGATIVE")
238
239 combinedExp = self.addSnaps(snap0, snap1)
240
241 return pipeBase.Struct(
242 exposure=combinedExp,
243 sources=sources,
244 )
245
246 def addSnaps(self, snap0, snap1):
247 """Add two snap exposures together, returning a new exposure.
248
249 Parameters
250 ----------
251 snap0 : `Unknown`
252 Snap exposure 0.
253 snap1 : `Unknown`
254 Snap exposure 1.
255
256 Returns
257 -------
258 combinedExp : `Unknown`
259 Combined exposure.
260 """
261 self.log.info("snapCombine addSnaps")
262
263 combinedExp = snap0.Factory(snap0, True)
264 combinedMi = combinedExp.getMaskedImage()
265 combinedMi.set(0)
266
267 weightMap = combinedMi.getImage().Factory(combinedMi.getBBox())
268 weight = 1.0
269 badPixelMask = afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes)
270 addToCoadd(combinedMi, weightMap, snap0.getMaskedImage(), badPixelMask, weight)
271 addToCoadd(combinedMi, weightMap, snap1.getMaskedImage(), badPixelMask, weight)
272
273 # pre-scaling the weight map instead of post-scaling the combinedMi saves a bit of time
274 # because the weight map is a simple Image instead of a MaskedImage
275 weightMap *= 0.5 # so result is sum of both images, instead of average
276 combinedMi /= weightMap
277 setCoaddEdgeBits(combinedMi.getMask(), weightMap)
278
279 # note: none of the inputs has a valid PhotoCalib object, so that is not touched
280 # Filter was already copied
281
282 combinedMetadata = combinedExp.getMetadata()
283 metadata0 = snap0.getMetadata()
284 metadata1 = snap1.getMetadata()
285 self.fixMetadata(combinedMetadata, metadata0, metadata1)
286
287 return combinedExp
288
289 def fixMetadata(self, combinedMetadata, metadata0, metadata1):
290 """Fix the metadata of the combined exposure (in place).
291
292 This implementation handles items specified by config.averageKeys and config.sumKeys,
293 which have data type restrictions. To handle other data types (such as sexagesimal
294 positions and ISO dates) you must supplement this method with your own code.
295
296 Parameters
297 ----------
298 combinedMetadata : `lsst.daf.base.PropertySet`
299 Metadata of combined exposure;
300 on input this is a deep copy of metadata0 (a PropertySet).
301 metadata0 : `lsst.daf.base.PropertySet`
302 Metadata of snap0 (a PropertySet).
303 metadata1 : `lsst.daf.base.PropertySet`
304 Metadata of snap1 (a PropertySet).
305
306 Notes
307 -----
308 The inputs are presently PropertySets due to ticket #2542. However, in some sense
309 they are just PropertyLists that are missing some methods. In particular: comments and order
310 are preserved if you alter an existing value with set(key, value).
311 """
312 keyDoAvgList = []
313 if self.config.averageKeys:
314 keyDoAvgList += [(key, 1) for key in self.config.averageKeys]
315 if self.config.sumKeys:
316 keyDoAvgList += [(key, 0) for key in self.config.sumKeys]
317 for key, doAvg in keyDoAvgList:
318 opStr = "average" if doAvg else "sum"
319 try:
320 val0 = metadata0.getScalar(key)
321 val1 = metadata1.getScalar(key)
322 except Exception:
323 self.log.warning("Could not %s metadata %r: missing from one or both exposures", opStr, key)
324 continue
325
326 try:
327 combinedVal = val0 + val1
328 if doAvg:
329 combinedVal /= 2.0
330 except Exception:
331 self.log.warning("Could not %s metadata %r: value %r and/or %r not numeric",
332 opStr, key, val0, val1)
333 continue
334
335 combinedMetadata.set(key, combinedVal)
336
337 def makeInitialPsf(self, exposure, fwhmPix=None):
338 """Initialise the detection procedure by setting the PSF and WCS.
339
340 exposure : `lsst.afw.image.Exposure`
341 Exposure to process.
342
343 Returns
344 -------
345 psf : `Unknown`
346 PSF, WCS
347
348 AssertionError
349 Raised if any of the following occur:
350 - No exposure provided.
351 - No wcs in exposure.
352 """
353 assert exposure, "No exposure provided"
354 wcs = exposure.getWcs()
355 assert wcs, "No wcs in exposure"
356
357 if fwhmPix is None:
358 fwhmPix = self.config.initialPsf.fwhm / wcs.getPixelScale().asArcseconds()
359
360 size = self.config.initialPsf.size
361 model = self.config.initialPsf.model
362 self.log.info("installInitialPsf fwhm=%s pixels; size=%s pixels", fwhmPix, size)
363 psfCls = getattr(measAlg, model + "Psf")
364 psf = psfCls(size, size, fwhmPix/(2.0*num.sqrt(2*num.log(2.0))))
365 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.
Definition: PropertyList.h:68
Class for storing generic metadata.
Definition: PropertySet.h:66
def fixMetadata(self, combinedMetadata, metadata0, metadata1)
Definition: snapCombine.py:289
def makeInitialPsf(self, exposure, fwhmPix=None)
Definition: snapCombine.py:337
def __init__(self, *args, **kwargs)
Definition: snapCombine.py:153
daf::base::PropertySet * set
Definition: fits.cc:927