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

146 statements  

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 

35 

36from .repair import RepairTask 

37 

38 

39class InitialPsfConfig(pexConfig.Config): 

40 """!Describes the initial PSF used for detection and measurement before we do PSF determination.""" 

41 

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 ) 

66 

67 

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, 

105 

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 ) 

113 

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="") 

119 

120 def setDefaults(self): 

121 self.detection.thresholdPolarity = "both" 

122 

123 def validate(self): 

124 if self.detection.thresholdPolarity != "both": 

125 raise ValueError("detection.thresholdPolarity must be 'both' for SnapCombineTask") 

126 

127## \addtogroup LSST_task_documentation 

128## \{ 

129## \page SnapCombineTask 

130## \ref SnapCombineTask_ "SnapCombineTask" 

131## \copybrief SnapCombineTask 

132## \} 

133 

134 

135class SnapCombineTask(pipeBase.Task): 

136 r"""! 

137 \anchor SnapCombineTask_ 

138 

139 \brief Combine snaps. 

140 

141 \section pipe_tasks_snapcombine_Contents Contents 

142 

143 - \ref pipe_tasks_snapcombine_Debug 

144 

145 \section pipe_tasks_snapcombine_Debug Debug variables 

146 

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. 

151 

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" 

167 

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) 

177 

178 @pipeBase.timeMethod 

179 def run(self, snap0, snap1, defects=None): 

180 """Combine two snaps 

181 

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 

191 

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) 

199 

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) 

206 

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 

212 

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() 

217 

218 else: 

219 diffExp = afwImage.ExposureF(snap0, True) 

220 diffMi = diffExp.getMaskedImage() 

221 diffMi -= snap1.getMaskedImage() 

222 

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) 

232 

233 mask0 = snap0.getMaskedImage().getMask() 

234 mask1 = snap1.getMaskedImage().getMask() 

235 fpSets.positive.setMask(mask0, "DETECTED") 

236 fpSets.negative.setMask(mask1, "DETECTED") 

237 

238 maskD = diffExp.getMaskedImage().getMask() 

239 fpSets.positive.setMask(maskD, "DETECTED") 

240 fpSets.negative.setMask(maskD, "DETECTED_NEGATIVE") 

241 

242 combinedExp = self.addSnaps(snap0, snap1) 

243 

244 return pipeBase.Struct( 

245 exposure=combinedExp, 

246 sources=sources, 

247 ) 

248 

249 def addSnaps(self, snap0, snap1): 

250 """Add two snap exposures together, returning a new exposure 

251 

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") 

257 

258 combinedExp = snap0.Factory(snap0, True) 

259 combinedMi = combinedExp.getMaskedImage() 

260 combinedMi.set(0) 

261 

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) 

267 

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) 

273 

274 # note: none of the inputs has a valid PhotoCalib object, so that is not touched 

275 # Filter was already copied 

276 

277 combinedMetadata = combinedExp.getMetadata() 

278 metadata0 = snap0.getMetadata() 

279 metadata1 = snap1.getMetadata() 

280 self.fixMetadata(combinedMetadata, metadata0, metadata1) 

281 

282 return combinedExp 

283 

284 def fixMetadata(self, combinedMetadata, metadata0, metadata1): 

285 """Fix the metadata of the combined exposure (in place) 

286 

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. 

290 

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) 

295 

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 

313 

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 

322 

323 combinedMetadata.set(key, combinedVal) 

324 

325 def makeInitialPsf(self, exposure, fwhmPix=None): 

326 """Initialise the detection procedure by setting the PSF and WCS 

327 

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" 

334 

335 if fwhmPix is None: 

336 fwhmPix = self.config.initialPsf.fwhm / wcs.getPixelScale().asArcseconds() 

337 

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