Coverage for python/lsst/pipe/tasks/snapCombine.py: 26%

140 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-17 03:36 -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/>. 

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 ) 

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 ) 

115 

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

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

161 

162 @timeMethod 

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

164 """Combine two snaps. 

165 

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

174 

175 Returns 

176 ------- 

177 result : `lsst.pipe.base.Struct` 

178 Results as a struct with attributes: 

179 

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 

187 

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) 

195 

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) 

202 

203 if self.config.doDiffIm: 

204 if self.config.doPsfMatch: 

205 raise NotImplementedError("PSF-matching of snaps is not yet supported.") 

206 

207 else: 

208 diffExp = afwImage.ExposureF(snap0, True) 

209 diffMi = diffExp.getMaskedImage() 

210 diffMi -= snap1.getMaskedImage() 

211 

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) 

220 

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

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

223 detRet.positive.setMask(mask0, "DETECTED") 

224 detRet.negative.setMask(mask1, "DETECTED") 

225 

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

227 detRet.positive.setMask(maskD, "DETECTED") 

228 detRet.negative.setMask(maskD, "DETECTED_NEGATIVE") 

229 

230 combinedExp = self.addSnaps(snap0, snap1) 

231 

232 return pipeBase.Struct( 

233 exposure=combinedExp, 

234 sources=sources, 

235 ) 

236 

237 def addSnaps(self, snap0, snap1): 

238 """Add two snap exposures together, returning a new exposure. 

239 

240 Parameters 

241 ---------- 

242 snap0 : `Unknown` 

243 Snap exposure 0. 

244 snap1 : `Unknown` 

245 Snap exposure 1. 

246 

247 Returns 

248 ------- 

249 combinedExp : `Unknown` 

250 Combined exposure. 

251 """ 

252 self.log.info("snapCombine addSnaps") 

253 

254 combinedExp = snap0.Factory(snap0, True) 

255 combinedMi = combinedExp.getMaskedImage() 

256 combinedMi.set(0) 

257 

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) 

263 

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) 

269 

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

271 # Filter was already copied 

272 

273 combinedMetadata = combinedExp.getMetadata() 

274 metadata0 = snap0.getMetadata() 

275 metadata1 = snap1.getMetadata() 

276 self.fixMetadata(combinedMetadata, metadata0, metadata1) 

277 

278 return combinedExp 

279 

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

281 """Fix the metadata of the combined exposure (in place). 

282 

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. 

286 

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

296 

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 

316 

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 

325 

326 combinedMetadata.set(key, combinedVal) 

327 

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

329 """Initialise the detection procedure by setting the PSF and WCS. 

330 

331 exposure : `lsst.afw.image.Exposure` 

332 Exposure to process. 

333 

334 Returns 

335 ------- 

336 psf : `Unknown` 

337 PSF, WCS 

338 

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" 

347 

348 if fwhmPix is None: 

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

350 

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