Coverage for python/lsst/pipe/tasks/snapCombine.py: 27%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# LSST Data Management System
3# Copyright 2008-2016 AURA/LSST.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22import numpy as num
23import lsst.pex.config as pexConfig
24import lsst.daf.base as dafBase
25import lsst.afw.image as afwImage
26import lsst.afw.table as afwTable
27import lsst.pipe.base as pipeBase
28from lsstDebug import getDebugFrame
29from lsst.afw.display import getDisplay
30from lsst.coadd.utils import addToCoadd, setCoaddEdgeBits
31from lsst.ip.diffim import SnapPsfMatchTask
32from lsst.meas.algorithms import SourceDetectionTask
33from lsst.meas.base import SingleFrameMeasurementTask
34import lsst.meas.algorithms as measAlg
36from .repair import RepairTask
39class InitialPsfConfig(pexConfig.Config):
40 """!Describes the initial PSF used for detection and measurement before we do PSF determination."""
42 model = pexConfig.ChoiceField(
43 dtype=str,
44 doc="PSF model type",
45 default="SingleGaussian",
46 allowed={
47 "SingleGaussian": "Single Gaussian model",
48 "DoubleGaussian": "Double Gaussian model",
49 },
50 )
51 pixelScale = pexConfig.Field(
52 dtype=float,
53 doc="Pixel size (arcsec). Only needed if no Wcs is provided",
54 default=0.25,
55 )
56 fwhm = pexConfig.Field(
57 dtype=float,
58 doc="FWHM of PSF model (arcsec)",
59 default=1.0,
60 )
61 size = pexConfig.Field(
62 dtype=int,
63 doc="Size of PSF model (pixels)",
64 default=15,
65 )
68class SnapCombineConfig(pexConfig.Config):
69 doRepair = pexConfig.Field(
70 dtype=bool,
71 doc="Repair images (CR reject and interpolate) before combining",
72 default=True,
73 )
74 repairPsfFwhm = pexConfig.Field(
75 dtype=float,
76 doc="Psf FWHM (pixels) used to detect CRs",
77 default=2.5,
78 )
79 doDiffIm = pexConfig.Field(
80 dtype=bool,
81 doc="Perform difference imaging before combining",
82 default=False,
83 )
84 doPsfMatch = pexConfig.Field(
85 dtype=bool,
86 doc="Perform PSF matching for difference imaging (ignored if doDiffIm false)",
87 default=True,
88 )
89 doMeasurement = pexConfig.Field(
90 dtype=bool,
91 doc="Measure difference sources (ignored if doDiffIm false)",
92 default=True,
93 )
94 badMaskPlanes = pexConfig.ListField(
95 dtype=str,
96 doc="Mask planes that, if set, the associated pixels are not included in the combined exposure; "
97 "DETECTED excludes cosmic rays",
98 default=("DETECTED",),
99 )
100 averageKeys = pexConfig.ListField(
101 dtype=str,
102 doc="List of float metadata keys to average when combining snaps, e.g. float positions and dates; "
103 "non-float data must be handled by overriding the fixMetadata method",
104 optional=True,
106 )
107 sumKeys = pexConfig.ListField(
108 dtype=str,
109 doc="List of float or int metadata keys to sum when combining snaps, e.g. exposure time; "
110 "non-float, non-int data must be handled by overriding the fixMetadata method",
111 optional=True,
112 )
114 repair = pexConfig.ConfigurableField(target=RepairTask, doc="")
115 diffim = pexConfig.ConfigurableField(target=SnapPsfMatchTask, doc="")
116 detection = pexConfig.ConfigurableField(target=SourceDetectionTask, doc="")
117 initialPsf = pexConfig.ConfigField(dtype=InitialPsfConfig, doc="")
118 measurement = pexConfig.ConfigurableField(target=SingleFrameMeasurementTask, doc="")
120 def setDefaults(self):
121 self.detection.thresholdPolarity = "both"
123 def validate(self):
124 if self.detection.thresholdPolarity != "both":
125 raise ValueError("detection.thresholdPolarity must be 'both' for SnapCombineTask")
127## \addtogroup LSST_task_documentation
128## \{
129## \page SnapCombineTask
130## \ref SnapCombineTask_ "SnapCombineTask"
131## \copybrief SnapCombineTask
132## \}
135class SnapCombineTask(pipeBase.Task):
136 r"""!
137 \anchor SnapCombineTask_
139 \brief Combine snaps.
141 \section pipe_tasks_snapcombine_Contents Contents
143 - \ref pipe_tasks_snapcombine_Debug
145 \section pipe_tasks_snapcombine_Debug Debug variables
147 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
148 flag \c -d to import \b debug.py from your \c PYTHONPATH; see <a
149 href="https://developer.lsst.io/stack/debug.html">Debugging Tasks with lsstDebug</a> for more
150 about \b debug.py files.
152 The available variables in SnapCombineTask are:
153 <DL>
154 <DT> \c display
155 <DD> A dictionary containing debug point names as keys with frame number as value. Valid keys are:
156 <DL>
157 <DT> repair0
158 <DD> Display the first snap after repairing.
159 <DT> repair1
160 <DD> Display the second snap after repairing.
161 </DL>
162 </DD>
163 </DL>
164 """
165 ConfigClass = SnapCombineConfig
166 _DefaultName = "snapCombine"
168 def __init__(self, *args, **kwargs):
169 pipeBase.Task.__init__(self, *args, **kwargs)
170 self.makeSubtask("repair")
171 self.makeSubtask("diffim")
172 self.schema = afwTable.SourceTable.makeMinimalSchema()
173 self.algMetadata = dafBase.PropertyList()
174 self.makeSubtask("detection", schema=self.schema)
175 if self.config.doMeasurement:
176 self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
178 @pipeBase.timeMethod
179 def run(self, snap0, snap1, defects=None):
180 """Combine two snaps
182 @param[in] snap0: snapshot exposure 0
183 @param[in] snap1: snapshot exposure 1
184 @defects[in] defect list (for repair task)
185 @return a pipe_base Struct with fields:
186 - exposure: snap-combined exposure
187 - sources: detected sources, or None if detection not performed
188 """
189 # initialize optional outputs
190 sources = None
192 if self.config.doRepair:
193 self.log.info("snapCombine repair")
194 psf = self.makeInitialPsf(snap0, fwhmPix=self.config.repairPsfFwhm)
195 snap0.setPsf(psf)
196 snap1.setPsf(psf)
197 self.repair.run(snap0, defects=defects, keepCRs=False)
198 self.repair.run(snap1, defects=defects, keepCRs=False)
200 repair0frame = getDebugFrame(self._display, "repair0")
201 if repair0frame:
202 getDisplay(repair0frame).mtv(snap0)
203 repair1frame = getDebugFrame(self._display, "repair1")
204 if repair1frame:
205 getDisplay(repair1frame).mtv(snap1)
207 if self.config.doDiffIm:
208 if self.config.doPsfMatch:
209 self.log.info("snapCombine psfMatch")
210 diffRet = self.diffim.run(snap0, snap1, "subtractExposures")
211 diffExp = diffRet.subtractedImage
213 # Measure centroid and width of kernel; dependent on ticket #1980
214 # Useful diagnostic for the degree of astrometric shift between snaps.
215 diffKern = diffRet.psfMatchingKernel
216 width, height = diffKern.getDimensions()
218 else:
219 diffExp = afwImage.ExposureF(snap0, True)
220 diffMi = diffExp.getMaskedImage()
221 diffMi -= snap1.getMaskedImage()
223 psf = self.makeInitialPsf(snap0)
224 diffExp.setPsf(psf)
225 table = afwTable.SourceTable.make(self.schema)
226 table.setMetadata(self.algMetadata)
227 detRet = self.detection.run(table, diffExp)
228 sources = detRet.sources
229 fpSets = detRet.fpSets
230 if self.config.doMeasurement:
231 self.measurement.measure(diffExp, sources)
233 mask0 = snap0.getMaskedImage().getMask()
234 mask1 = snap1.getMaskedImage().getMask()
235 fpSets.positive.setMask(mask0, "DETECTED")
236 fpSets.negative.setMask(mask1, "DETECTED")
238 maskD = diffExp.getMaskedImage().getMask()
239 fpSets.positive.setMask(maskD, "DETECTED")
240 fpSets.negative.setMask(maskD, "DETECTED_NEGATIVE")
242 combinedExp = self.addSnaps(snap0, snap1)
244 return pipeBase.Struct(
245 exposure=combinedExp,
246 sources=sources,
247 )
249 def addSnaps(self, snap0, snap1):
250 """Add two snap exposures together, returning a new exposure
252 @param[in] snap0 snap exposure 0
253 @param[in] snap1 snap exposure 1
254 @return combined exposure
255 """
256 self.log.info("snapCombine addSnaps")
258 combinedExp = snap0.Factory(snap0, True)
259 combinedMi = combinedExp.getMaskedImage()
260 combinedMi.set(0)
262 weightMap = combinedMi.getImage().Factory(combinedMi.getBBox())
263 weight = 1.0
264 badPixelMask = afwImage.Mask.getPlaneBitMask(self.config.badMaskPlanes)
265 addToCoadd(combinedMi, weightMap, snap0.getMaskedImage(), badPixelMask, weight)
266 addToCoadd(combinedMi, weightMap, snap1.getMaskedImage(), badPixelMask, weight)
268 # pre-scaling the weight map instead of post-scaling the combinedMi saves a bit of time
269 # because the weight map is a simple Image instead of a MaskedImage
270 weightMap *= 0.5 # so result is sum of both images, instead of average
271 combinedMi /= weightMap
272 setCoaddEdgeBits(combinedMi.getMask(), weightMap)
274 # note: none of the inputs has a valid PhotoCalib object, so that is not touched
275 # Filter was already copied
277 combinedMetadata = combinedExp.getMetadata()
278 metadata0 = snap0.getMetadata()
279 metadata1 = snap1.getMetadata()
280 self.fixMetadata(combinedMetadata, metadata0, metadata1)
282 return combinedExp
284 def fixMetadata(self, combinedMetadata, metadata0, metadata1):
285 """Fix the metadata of the combined exposure (in place)
287 This implementation handles items specified by config.averageKeys and config.sumKeys,
288 which have data type restrictions. To handle other data types (such as sexagesimal
289 positions and ISO dates) you must supplement this method with your own code.
291 @param[in,out] combinedMetadata metadata of combined exposure;
292 on input this is a deep copy of metadata0 (a PropertySet)
293 @param[in] metadata0 metadata of snap0 (a PropertySet)
294 @param[in] metadata1 metadata of snap1 (a PropertySet)
296 @note the inputs are presently PropertySets due to ticket #2542. However, in some sense
297 they are just PropertyLists that are missing some methods. In particular: comments and order
298 are preserved if you alter an existing value with set(key, value).
299 """
300 keyDoAvgList = []
301 if self.config.averageKeys:
302 keyDoAvgList += [(key, 1) for key in self.config.averageKeys]
303 if self.config.sumKeys:
304 keyDoAvgList += [(key, 0) for key in self.config.sumKeys]
305 for key, doAvg in keyDoAvgList:
306 opStr = "average" if doAvg else "sum"
307 try:
308 val0 = metadata0.getScalar(key)
309 val1 = metadata1.getScalar(key)
310 except Exception:
311 self.log.warning("Could not %s metadata %r: missing from one or both exposures", opStr, key)
312 continue
314 try:
315 combinedVal = val0 + val1
316 if doAvg:
317 combinedVal /= 2.0
318 except Exception:
319 self.log.warning("Could not %s metadata %r: value %r and/or %r not numeric",
320 opStr, key, val0, val1)
321 continue
323 combinedMetadata.set(key, combinedVal)
325 def makeInitialPsf(self, exposure, fwhmPix=None):
326 """Initialise the detection procedure by setting the PSF and WCS
328 @param exposure Exposure to process
329 @return PSF, WCS
330 """
331 assert exposure, "No exposure provided"
332 wcs = exposure.getWcs()
333 assert wcs, "No wcs in exposure"
335 if fwhmPix is None:
336 fwhmPix = self.config.initialPsf.fwhm / wcs.getPixelScale().asArcseconds()
338 size = self.config.initialPsf.size
339 model = self.config.initialPsf.model
340 self.log.info("installInitialPsf fwhm=%s pixels; size=%s pixels", fwhmPix, size)
341 psfCls = getattr(measAlg, model + "Psf")
342 psf = psfCls(size, size, fwhmPix/(2.0*num.sqrt(2*num.log(2.0))))
343 return psf