Coverage for python/lsst/cp/pipe/cpSkyTask.py: 56%

100 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-03 02:35 -0800

1# This file is part of cp_pipe. 

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 <http://www.gnu.org/licenses/>. 

21import numpy as np 

22 

23import lsst.pex.config as pexConfig 

24import lsst.pipe.base as pipeBase 

25import lsst.pipe.base.connectionTypes as cT 

26 

27from lsst.pipe.tasks.background import (FocalPlaneBackground, MaskObjectsTask, SkyMeasurementTask, 

28 FocalPlaneBackgroundConfig) 

29from lsst.daf.base import PropertyList 

30from ._lookupStaticCalibration import lookupStaticCalibration 

31from .cpCombine import CalibCombineTask 

32 

33__all__ = ['CpSkyImageTask', 'CpSkyImageConfig', 

34 'CpSkyScaleMeasureTask', 'CpSkyScaleMeasureConfig', 

35 'CpSkySubtractBackgroundTask', 'CpSkySubtractBackgroundConfig', 

36 'CpSkyCombineTask', 'CpSkyCombineConfig'] 

37 

38 

39class CpSkyImageConnections(pipeBase.PipelineTaskConnections, 

40 dimensions=("instrument", "physical_filter", "exposure", "detector")): 

41 inputExp = cT.Input( 

42 name="postISRCCD", 

43 doc="Input pre-processed exposures to combine.", 

44 storageClass="Exposure", 

45 dimensions=("instrument", "exposure", "detector"), 

46 ) 

47 camera = cT.PrerequisiteInput( 

48 name="camera", 

49 doc="Input camera to use for geometry.", 

50 storageClass="Camera", 

51 dimensions=("instrument",), 

52 lookupFunction=lookupStaticCalibration, 

53 isCalibration=True, 

54 ) 

55 

56 maskedExp = cT.Output( 

57 name="cpSkyMaskedIsr", 

58 doc="Output masked post-ISR exposure.", 

59 storageClass="Exposure", 

60 dimensions=("instrument", "exposure", "detector"), 

61 ) 

62 maskedBkg = cT.Output( 

63 name="cpSkyDetectorBackground", 

64 doc="Initial background model from one image.", 

65 storageClass="FocalPlaneBackground", 

66 dimensions=("instrument", "exposure", "detector"), 

67 ) 

68 

69 

70class CpSkyImageConfig(pipeBase.PipelineTaskConfig, 

71 pipelineConnections=CpSkyImageConnections): 

72 maskTask = pexConfig.ConfigurableField( 

73 target=MaskObjectsTask, 

74 doc="Object masker to use.", 

75 ) 

76 

77 maskThresh = pexConfig.Field( 

78 dtype=float, 

79 default=3.0, 

80 doc="k-sigma threshold for masking pixels.", 

81 ) 

82 maskList = pexConfig.ListField( 

83 dtype=str, 

84 default=["DETECTED", "BAD", "NO_DATA", "SAT"], 

85 doc="Mask planes to reject.", 

86 ) 

87 

88 largeScaleBackground = pexConfig.ConfigField( 

89 dtype=FocalPlaneBackgroundConfig, 

90 doc="Large-scale background configuration.", 

91 ) 

92 

93 def setDefaults(self): 

94 # These values correspond to the HSC recommendation. As noted 

95 # below, the sizes are in millimeters, and correspond to an 

96 # background image 8192x8192 pixels (8192*0.015=122.88). 

97 self.largeScaleBackground.xSize = 122.88 # in mm 

98 self.largeScaleBackground.ySize = 122.88 # in mm 

99 self.largeScaleBackground.pixelSize = 0.015 # in mm per pixel 

100 self.largeScaleBackground.minFrac = 0.1 

101 self.largeScaleBackground.mask = ['BAD', 'SAT', 'INTRP', 'DETECTED', 'DETECTED_NEGATIVE', 

102 'EDGE', 'NO_DATA'] 

103 

104 

105class CpSkyImageTask(pipeBase.PipelineTask): 

106 """Mask the detections on the postISRCCD. 

107 

108 This task maps the MaskObjectsTask across all of the initial ISR 

109 processed cpSkyIsr images to create cpSkyMaskedIsr products for 

110 all (exposure, detector) values. 

111 """ 

112 

113 ConfigClass = CpSkyImageConfig 

114 _DefaultName = "CpSkyImage" 

115 

116 def __init__(self, **kwargs): 

117 super().__init__(**kwargs) 

118 self.makeSubtask("maskTask") 

119 

120 def run(self, inputExp, camera): 

121 """Mask the detections on the postISRCCD. 

122 

123 Parameters 

124 ---------- 

125 inputExp : `lsst.afw.image.Exposure` 

126 An ISR processed exposure that will have detections 

127 masked. 

128 camera : `lsst.afw.cameraGeom.Camera` 

129 The camera geometry for this exposure. This is needed to 

130 create the background model. 

131 

132 Returns 

133 ------- 

134 results : `lsst.pipe.base.Struct` 

135 The results struct containing: 

136 

137 ``maskedExp`` : `lsst.afw.image.Exposure` 

138 The detection-masked version of the ``inputExp``. 

139 ``maskedBkg`` : `lsst.pipe.tasks.background.FocalPlaneBackground` 

140 The partial focal plane background containing only 

141 this exposure/detector's worth of data. 

142 """ 

143 # As constructCalibs.py SkyTask.processSingleBackground() 

144 # Except: check if a detector is fully masked to avoid 

145 # self.maskTask raising. 

146 currentMask = inputExp.getMask() 

147 badMask = currentMask.getPlaneBitMask(self.config.maskList) 

148 if (currentMask.getArray() & badMask).all(): 

149 self.log.warning("All pixels are masked!") 

150 else: 

151 self.maskTask.run(inputExp, self.config.maskList) 

152 

153 # As constructCalibs.py SkyTask.measureBackground() 

154 bgModel = FocalPlaneBackground.fromCamera(self.config.largeScaleBackground, camera) 

155 bgModel.addCcd(inputExp) 

156 

157 return pipeBase.Struct( 

158 maskedExp=inputExp, 

159 maskedBkg=bgModel, 

160 ) 

161 

162 

163class CpSkyScaleMeasureConnections(pipeBase.PipelineTaskConnections, 

164 dimensions=("instrument", "physical_filter", "exposure")): 

165 inputBkgs = cT.Input( 

166 name="cpSkyDetectorBackground", 

167 doc="Initial background model from one exposure/detector", 

168 storageClass="FocalPlaneBackground", 

169 dimensions=("instrument", "exposure", "detector"), 

170 multiple=True 

171 ) 

172 

173 outputBkg = cT.Output( 

174 name="cpSkyExpBackground", 

175 doc="Background model for a full exposure.", 

176 storageClass="FocalPlaneBackground", 

177 dimensions=("instrument", "exposure"), 

178 ) 

179 outputScale = cT.Output( 

180 name="cpSkyExpScale", 

181 doc="Scale for the full exposure.", 

182 storageClass="PropertyList", 

183 dimensions=("instrument", "exposure"), 

184 ) 

185 

186 

187class CpSkyScaleMeasureConfig(pipeBase.PipelineTaskConfig, 

188 pipelineConnections=CpSkyScaleMeasureConnections): 

189 # There are no configurable parameters here. 

190 pass 

191 

192 

193class CpSkyScaleMeasureTask(pipeBase.PipelineTask): 

194 """Measure per-exposure scale factors and merge focal plane backgrounds. 

195 

196 Merge all the per-detector partial backgrounds to a full focal 

197 plane background for each exposure, and measure the scale factor 

198 from that full background. 

199 """ 

200 

201 ConfigClass = CpSkyScaleMeasureConfig 

202 _DefaultName = "cpSkyScaleMeasure" 

203 

204 def run(self, inputBkgs): 

205 """Merge focal plane backgrounds and measure the scale factor. 

206 

207 Parameters 

208 ---------- 

209 inputBkgs : `list` [`lsst.pipe.tasks.background.FocalPlaneBackground`] 

210 A list of all of the partial focal plane backgrounds, one 

211 from each detector in this exposure. 

212 

213 Returns 

214 ------- 

215 results : `lsst.pipe.base.Struct` 

216 The results struct containing: 

217 

218 ``outputBkg`` : `lsst.pipe.tasks.background.FocalPlaneBackground` 

219 The full merged background for the entire focal plane. 

220 ``outputScale`` : `lsst.daf.base.PropertyList` 

221 A metadata containing the median level of the 

222 background, stored in the key 'scale'. 

223 """ 

224 # As constructCalibs.py SkyTask.scatterProcess() 

225 # Merge into the full focal plane. 

226 background = inputBkgs[0] 

227 for bg in inputBkgs[1:]: 

228 background.merge(bg) 

229 

230 backgroundPixels = background.getStatsImage().getArray() 

231 self.log.info("Background model min/max: %f %f. Scale %f", 

232 np.min(backgroundPixels), np.max(backgroundPixels), 

233 np.median(backgroundPixels)) 

234 

235 # A property list is overkill, but FocalPlaneBackground 

236 # doesn't have a metadata object that this can be stored in. 

237 scale = np.median(background.getStatsImage().getArray()) 

238 scaleMD = PropertyList() 

239 scaleMD.set("scale", float(scale)) 

240 

241 return pipeBase.Struct( 

242 outputBkg=background, 

243 outputScale=scaleMD, 

244 ) 

245 

246 

247class CpSkySubtractBackgroundConnections(pipeBase.PipelineTaskConnections, 

248 dimensions=("instrument", "physical_filter", 

249 "exposure", "detector")): 

250 inputExp = cT.Input( 

251 name="cpSkyMaskedIsr", 

252 doc="Masked post-ISR image.", 

253 storageClass="Exposure", 

254 dimensions=("instrument", "exposure", "detector"), 

255 ) 

256 inputBkg = cT.Input( 

257 name="cpSkyExpBackground", 

258 doc="Background model for the full exposure.", 

259 storageClass="FocalPlaneBackground", 

260 dimensions=("instrument", "exposure"), 

261 ) 

262 inputScale = cT.Input( 

263 name="cpSkyExpScale", 

264 doc="Scale for the full exposure.", 

265 storageClass="PropertyList", 

266 dimensions=("instrument", "exposure"), 

267 ) 

268 

269 outputBkg = cT.Output( 

270 name="cpExpBackground", 

271 doc="Normalized, static background.", 

272 storageClass="Background", 

273 dimensions=("instrument", "exposure", "detector"), 

274 ) 

275 

276 

277class CpSkySubtractBackgroundConfig(pipeBase.PipelineTaskConfig, 

278 pipelineConnections=CpSkySubtractBackgroundConnections): 

279 sky = pexConfig.ConfigurableField( 

280 target=SkyMeasurementTask, 

281 doc="Sky measurement", 

282 ) 

283 

284 

285class CpSkySubtractBackgroundTask(pipeBase.PipelineTask): 

286 """Subtract per-exposure background from individual detector masked images. 

287 

288 The cpSkyMaskedIsr images constructed by CpSkyImageTask have the 

289 scaled background constructed by CpSkyScaleMeasureTask subtracted, 

290 and new background models are constructed for the remaining 

291 signal. 

292 

293 The output was called `icExpBackground` in gen2, but the product 

294 created here has definition clashes that prevent that from being 

295 reused. 

296 """ 

297 

298 ConfigClass = CpSkySubtractBackgroundConfig 

299 _DefaultName = "cpSkySubtractBkg" 

300 

301 def __init__(self, **kwargs): 

302 super().__init__(**kwargs) 

303 self.makeSubtask("sky") 

304 

305 def run(self, inputExp, inputBkg, inputScale): 

306 """Subtract per-exposure background from individual detector masked 

307 images. 

308 

309 Parameters 

310 ---------- 

311 inputExp : `lsst.afw.image.Exposure` 

312 The ISR processed, detection masked image. 

313 inputBkg : `lsst.pipe.tasks.background.FocalPlaneBackground. 

314 Full focal plane background for this exposure. 

315 inputScale : `lsst.daf.base.PropertyList` 

316 Metadata containing the scale factor. 

317 

318 Returns 

319 ------- 

320 results : `lsst.pipe.base.Struct` 

321 The results struct containing: 

322 

323 ``outputBkg`` 

324 Remnant sky background with the full-exposure 

325 component removed. (`lsst.afw.math.BackgroundList`) 

326 """ 

327 # As constructCalibs.py SkyTask.processSingle() 

328 image = inputExp.getMaskedImage() 

329 detector = inputExp.getDetector() 

330 bbox = image.getBBox() 

331 

332 scale = inputScale.get('scale') 

333 background = inputBkg.toCcdBackground(detector, bbox) 

334 image -= background.getImage() 

335 image /= scale 

336 

337 newBackground = self.sky.measureBackground(image) 

338 return pipeBase.Struct( 

339 outputBkg=newBackground 

340 ) 

341 

342 

343class CpSkyCombineConnections(pipeBase.PipelineTaskConnections, 

344 dimensions=("instrument", "physical_filter", "detector")): 

345 inputBkgs = cT.Input( 

346 name="cpExpBackground", 

347 doc="Normalized, static background.", 

348 storageClass="Background", 

349 dimensions=("instrument", "exposure", "detector"), 

350 multiple=True, 

351 ) 

352 inputExpHandles = cT.Input( 

353 name="cpSkyMaskedIsr", 

354 doc="Masked post-ISR image.", 

355 storageClass="Exposure", 

356 dimensions=("instrument", "exposure", "detector"), 

357 multiple=True, 

358 deferLoad=True, 

359 ) 

360 

361 outputCalib = cT.Output( 

362 name="sky", 

363 doc="Averaged static background.", 

364 storageClass="ExposureF", 

365 dimensions=("instrument", "detector", "physical_filter"), 

366 isCalibration=True, 

367 ) 

368 

369 

370class CpSkyCombineConfig(pipeBase.PipelineTaskConfig, 

371 pipelineConnections=CpSkyCombineConnections): 

372 sky = pexConfig.ConfigurableField( 

373 target=SkyMeasurementTask, 

374 doc="Sky measurement", 

375 ) 

376 

377 

378class CpSkyCombineTask(pipeBase.PipelineTask): 

379 """Merge per-exposure measurements into a detector level calibration. 

380 

381 Each of the per-detector results from all input exposures are 

382 averaged to produce the final SKY calibration. 

383 

384 As before, this is written to a skyCalib instead of a SKY to avoid 

385 definition classes in gen3. 

386 """ 

387 

388 ConfigClass = CpSkyCombineConfig 

389 _DefaultName = "cpSkyCombine" 

390 

391 def __init__(self, **kwargs): 

392 super().__init__(**kwargs) 

393 self.makeSubtask("sky") 

394 

395 def run(self, inputBkgs, inputExpHandles): 

396 """Merge per-exposure measurements into a detector level calibration. 

397 

398 Parameters 

399 ---------- 

400 inputBkgs : `list` [`lsst.afw.math.BackgroundList`] 

401 Remnant backgrounds from each exposure. 

402 inputHandles : `list` [`lsst.daf.butler.DeferredDatasetHandles`] 

403 The Butler handles to the ISR processed, detection masked images. 

404 

405 Returns 

406 ------- 

407 results : `lsst.pipe.base.Struct` 

408 The results struct containing: 

409 

410 `outputCalib` : `lsst.afw.image.Exposure` 

411 The final sky calibration product. 

412 """ 

413 skyCalib = self.sky.averageBackgrounds(inputBkgs) 

414 skyCalib.setDetector(inputExpHandles[0].get(component='detector')) 

415 skyCalib.setFilter(inputExpHandles[0].get(component='filter')) 

416 

417 CalibCombineTask().combineHeaders(inputExpHandles, skyCalib, calibType='SKY') 

418 

419 return pipeBase.Struct( 

420 outputCalib=skyCalib, 

421 )