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

93 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-05 11:40 +0000

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 .cpCombine import CalibCombineTask 

31 

32__all__ = ['CpSkyImageTask', 'CpSkyImageConfig', 

33 'CpSkyScaleMeasureTask', 'CpSkyScaleMeasureConfig', 

34 'CpSkySubtractBackgroundTask', 'CpSkySubtractBackgroundConfig', 

35 'CpSkyCombineTask', 'CpSkyCombineConfig'] 

36 

37 

38class CpSkyImageConnections(pipeBase.PipelineTaskConnections, 

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

40 inputExp = cT.Input( 

41 name="postISRCCD", 

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

43 storageClass="Exposure", 

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

45 ) 

46 camera = cT.PrerequisiteInput( 

47 name="camera", 

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

49 storageClass="Camera", 

50 dimensions=("instrument",), 

51 isCalibration=True, 

52 ) 

53 

54 maskedExp = cT.Output( 

55 name="cpSkyMaskedIsr", 

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

57 storageClass="Exposure", 

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

59 ) 

60 maskedBkg = cT.Output( 

61 name="cpSkyDetectorBackground", 

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

63 storageClass="FocalPlaneBackground", 

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

65 ) 

66 

67 

68class CpSkyImageConfig(pipeBase.PipelineTaskConfig, 

69 pipelineConnections=CpSkyImageConnections): 

70 maskTask = pexConfig.ConfigurableField( 

71 target=MaskObjectsTask, 

72 doc="Object masker to use.", 

73 ) 

74 

75 maskThresh = pexConfig.Field( 

76 dtype=float, 

77 default=3.0, 

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

79 ) 

80 maskList = pexConfig.ListField( 

81 dtype=str, 

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

83 doc="Mask planes to reject.", 

84 ) 

85 

86 largeScaleBackground = pexConfig.ConfigField( 

87 dtype=FocalPlaneBackgroundConfig, 

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

89 ) 

90 

91 

92class CpSkyImageTask(pipeBase.PipelineTask): 

93 """Mask the detections on the postISRCCD. 

94 

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

96 processed cpSkyIsr images to create cpSkyMaskedIsr products for 

97 all (exposure, detector) values. 

98 """ 

99 

100 ConfigClass = CpSkyImageConfig 

101 _DefaultName = "CpSkyImage" 

102 

103 def __init__(self, **kwargs): 

104 super().__init__(**kwargs) 

105 self.makeSubtask("maskTask") 

106 

107 def run(self, inputExp, camera): 

108 """Mask the detections on the postISRCCD. 

109 

110 Parameters 

111 ---------- 

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

113 An ISR processed exposure that will have detections 

114 masked. 

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

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

117 create the background model. 

118 

119 Returns 

120 ------- 

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

122 The results struct containing: 

123 

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

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

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

127 The partial focal plane background containing only 

128 this exposure/detector's worth of data. 

129 """ 

130 # As constructCalibs.py SkyTask.processSingleBackground() 

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

132 # self.maskTask raising. 

133 currentMask = inputExp.getMask() 

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

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

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

137 else: 

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

139 

140 # As constructCalibs.py SkyTask.measureBackground() 

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

142 bgModel.addCcd(inputExp) 

143 

144 return pipeBase.Struct( 

145 maskedExp=inputExp, 

146 maskedBkg=bgModel, 

147 ) 

148 

149 

150class CpSkyScaleMeasureConnections(pipeBase.PipelineTaskConnections, 

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

152 inputBkgs = cT.Input( 

153 name="cpSkyDetectorBackground", 

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

155 storageClass="FocalPlaneBackground", 

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

157 multiple=True 

158 ) 

159 

160 outputBkg = cT.Output( 

161 name="cpSkyExpBackground", 

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

163 storageClass="FocalPlaneBackground", 

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

165 ) 

166 outputScale = cT.Output( 

167 name="cpSkyExpScale", 

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

169 storageClass="PropertyList", 

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

171 ) 

172 

173 

174class CpSkyScaleMeasureConfig(pipeBase.PipelineTaskConfig, 

175 pipelineConnections=CpSkyScaleMeasureConnections): 

176 # There are no configurable parameters here. 

177 pass 

178 

179 

180class CpSkyScaleMeasureTask(pipeBase.PipelineTask): 

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

182 

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

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

185 from that full background. 

186 """ 

187 

188 ConfigClass = CpSkyScaleMeasureConfig 

189 _DefaultName = "cpSkyScaleMeasure" 

190 

191 def run(self, inputBkgs): 

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

193 

194 Parameters 

195 ---------- 

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

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

198 from each detector in this exposure. 

199 

200 Returns 

201 ------- 

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

203 The results struct containing: 

204 

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

206 The full merged background for the entire focal plane. 

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

208 A metadata containing the median level of the 

209 background, stored in the key 'scale'. 

210 """ 

211 # As constructCalibs.py SkyTask.scatterProcess() 

212 # Merge into the full focal plane. 

213 background = inputBkgs[0] 

214 for bg in inputBkgs[1:]: 

215 background.merge(bg) 

216 

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

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

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

220 np.median(backgroundPixels)) 

221 

222 # A property list is overkill, but FocalPlaneBackground 

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

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

225 scaleMD = PropertyList() 

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

227 

228 return pipeBase.Struct( 

229 outputBkg=background, 

230 outputScale=scaleMD, 

231 ) 

232 

233 

234class CpSkySubtractBackgroundConnections(pipeBase.PipelineTaskConnections, 

235 dimensions=("instrument", "physical_filter", 

236 "exposure", "detector")): 

237 inputExp = cT.Input( 

238 name="cpSkyMaskedIsr", 

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

240 storageClass="Exposure", 

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

242 ) 

243 inputBkg = cT.Input( 

244 name="cpSkyExpBackground", 

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

246 storageClass="FocalPlaneBackground", 

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

248 ) 

249 inputScale = cT.Input( 

250 name="cpSkyExpScale", 

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

252 storageClass="PropertyList", 

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

254 ) 

255 

256 outputBkg = cT.Output( 

257 name="cpExpBackground", 

258 doc="Normalized, static background.", 

259 storageClass="Background", 

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

261 ) 

262 

263 

264class CpSkySubtractBackgroundConfig(pipeBase.PipelineTaskConfig, 

265 pipelineConnections=CpSkySubtractBackgroundConnections): 

266 sky = pexConfig.ConfigurableField( 

267 target=SkyMeasurementTask, 

268 doc="Sky measurement", 

269 ) 

270 

271 

272class CpSkySubtractBackgroundTask(pipeBase.PipelineTask): 

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

274 

275 The cpSkyMaskedIsr images constructed by CpSkyImageTask have the 

276 scaled background constructed by CpSkyScaleMeasureTask subtracted, 

277 and new background models are constructed for the remaining 

278 signal. 

279 

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

281 created here has definition clashes that prevent that from being 

282 reused. 

283 """ 

284 

285 ConfigClass = CpSkySubtractBackgroundConfig 

286 _DefaultName = "cpSkySubtractBkg" 

287 

288 def __init__(self, **kwargs): 

289 super().__init__(**kwargs) 

290 self.makeSubtask("sky") 

291 

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

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

294 images. 

295 

296 Parameters 

297 ---------- 

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

299 The ISR processed, detection masked image. 

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

301 Full focal plane background for this exposure. 

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

303 Metadata containing the scale factor. 

304 

305 Returns 

306 ------- 

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

308 The results struct containing: 

309 

310 ``outputBkg`` 

311 Remnant sky background with the full-exposure 

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

313 """ 

314 # As constructCalibs.py SkyTask.processSingle() 

315 image = inputExp.getMaskedImage() 

316 detector = inputExp.getDetector() 

317 bbox = image.getBBox() 

318 

319 scale = inputScale.get('scale') 

320 background = inputBkg.toCcdBackground(detector, bbox) 

321 image -= background.getImage() 

322 image /= scale 

323 

324 newBackground = self.sky.measureBackground(image) 

325 return pipeBase.Struct( 

326 outputBkg=newBackground 

327 ) 

328 

329 

330class CpSkyCombineConnections(pipeBase.PipelineTaskConnections, 

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

332 inputBkgs = cT.Input( 

333 name="cpExpBackground", 

334 doc="Normalized, static background.", 

335 storageClass="Background", 

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

337 multiple=True, 

338 ) 

339 inputExpHandles = cT.Input( 

340 name="cpSkyMaskedIsr", 

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

342 storageClass="Exposure", 

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

344 multiple=True, 

345 deferLoad=True, 

346 ) 

347 

348 outputCalib = cT.Output( 

349 name="sky", 

350 doc="Averaged static background.", 

351 storageClass="ExposureF", 

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

353 isCalibration=True, 

354 ) 

355 

356 

357class CpSkyCombineConfig(pipeBase.PipelineTaskConfig, 

358 pipelineConnections=CpSkyCombineConnections): 

359 sky = pexConfig.ConfigurableField( 

360 target=SkyMeasurementTask, 

361 doc="Sky measurement", 

362 ) 

363 

364 

365class CpSkyCombineTask(pipeBase.PipelineTask): 

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

367 

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

369 averaged to produce the final SKY calibration. 

370 

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

372 definition classes in gen3. 

373 """ 

374 

375 ConfigClass = CpSkyCombineConfig 

376 _DefaultName = "cpSkyCombine" 

377 

378 def __init__(self, **kwargs): 

379 super().__init__(**kwargs) 

380 self.makeSubtask("sky") 

381 

382 def run(self, inputBkgs, inputExpHandles): 

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

384 

385 Parameters 

386 ---------- 

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

388 Remnant backgrounds from each exposure. 

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

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

391 

392 Returns 

393 ------- 

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

395 The results struct containing: 

396 

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

398 The final sky calibration product. 

399 """ 

400 skyCalib = self.sky.averageBackgrounds(inputBkgs) 

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

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

403 

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

405 

406 return pipeBase.Struct( 

407 outputCalib=skyCalib, 

408 )