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

140 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-23 10:59 +0000

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

160 self.algMetadata = dafBase.PropertyList() 

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