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

99 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-01 14:30 +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 def setDefaults(self): 

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

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

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

95 self.largeScaleBackground.xSize = 122.88 # in mm 

96 self.largeScaleBackground.ySize = 122.88 # in mm 

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

98 self.largeScaleBackground.minFrac = 0.1 

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

100 'EDGE', 'NO_DATA'] 

101 

102 

103class CpSkyImageTask(pipeBase.PipelineTask): 

104 """Mask the detections on the postISRCCD. 

105 

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

107 processed cpSkyIsr images to create cpSkyMaskedIsr products for 

108 all (exposure, detector) values. 

109 """ 

110 

111 ConfigClass = CpSkyImageConfig 

112 _DefaultName = "CpSkyImage" 

113 

114 def __init__(self, **kwargs): 

115 super().__init__(**kwargs) 

116 self.makeSubtask("maskTask") 

117 

118 def run(self, inputExp, camera): 

119 """Mask the detections on the postISRCCD. 

120 

121 Parameters 

122 ---------- 

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

124 An ISR processed exposure that will have detections 

125 masked. 

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

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

128 create the background model. 

129 

130 Returns 

131 ------- 

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

133 The results struct containing: 

134 

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

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

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

138 The partial focal plane background containing only 

139 this exposure/detector's worth of data. 

140 """ 

141 # As constructCalibs.py SkyTask.processSingleBackground() 

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

143 # self.maskTask raising. 

144 currentMask = inputExp.getMask() 

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

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

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

148 else: 

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

150 

151 # As constructCalibs.py SkyTask.measureBackground() 

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

153 bgModel.addCcd(inputExp) 

154 

155 return pipeBase.Struct( 

156 maskedExp=inputExp, 

157 maskedBkg=bgModel, 

158 ) 

159 

160 

161class CpSkyScaleMeasureConnections(pipeBase.PipelineTaskConnections, 

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

163 inputBkgs = cT.Input( 

164 name="cpSkyDetectorBackground", 

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

166 storageClass="FocalPlaneBackground", 

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

168 multiple=True 

169 ) 

170 

171 outputBkg = cT.Output( 

172 name="cpSkyExpBackground", 

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

174 storageClass="FocalPlaneBackground", 

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

176 ) 

177 outputScale = cT.Output( 

178 name="cpSkyExpScale", 

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

180 storageClass="PropertyList", 

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

182 ) 

183 

184 

185class CpSkyScaleMeasureConfig(pipeBase.PipelineTaskConfig, 

186 pipelineConnections=CpSkyScaleMeasureConnections): 

187 # There are no configurable parameters here. 

188 pass 

189 

190 

191class CpSkyScaleMeasureTask(pipeBase.PipelineTask): 

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

193 

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

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

196 from that full background. 

197 """ 

198 

199 ConfigClass = CpSkyScaleMeasureConfig 

200 _DefaultName = "cpSkyScaleMeasure" 

201 

202 def run(self, inputBkgs): 

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

204 

205 Parameters 

206 ---------- 

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

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

209 from each detector in this exposure. 

210 

211 Returns 

212 ------- 

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

214 The results struct containing: 

215 

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

217 The full merged background for the entire focal plane. 

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

219 A metadata containing the median level of the 

220 background, stored in the key 'scale'. 

221 """ 

222 # As constructCalibs.py SkyTask.scatterProcess() 

223 # Merge into the full focal plane. 

224 background = inputBkgs[0] 

225 for bg in inputBkgs[1:]: 

226 background.merge(bg) 

227 

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

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

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

231 np.median(backgroundPixels)) 

232 

233 # A property list is overkill, but FocalPlaneBackground 

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

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

236 scaleMD = PropertyList() 

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

238 

239 return pipeBase.Struct( 

240 outputBkg=background, 

241 outputScale=scaleMD, 

242 ) 

243 

244 

245class CpSkySubtractBackgroundConnections(pipeBase.PipelineTaskConnections, 

246 dimensions=("instrument", "physical_filter", 

247 "exposure", "detector")): 

248 inputExp = cT.Input( 

249 name="cpSkyMaskedIsr", 

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

251 storageClass="Exposure", 

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

253 ) 

254 inputBkg = cT.Input( 

255 name="cpSkyExpBackground", 

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

257 storageClass="FocalPlaneBackground", 

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

259 ) 

260 inputScale = cT.Input( 

261 name="cpSkyExpScale", 

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

263 storageClass="PropertyList", 

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

265 ) 

266 

267 outputBkg = cT.Output( 

268 name="cpExpBackground", 

269 doc="Normalized, static background.", 

270 storageClass="Background", 

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

272 ) 

273 

274 

275class CpSkySubtractBackgroundConfig(pipeBase.PipelineTaskConfig, 

276 pipelineConnections=CpSkySubtractBackgroundConnections): 

277 sky = pexConfig.ConfigurableField( 

278 target=SkyMeasurementTask, 

279 doc="Sky measurement", 

280 ) 

281 

282 

283class CpSkySubtractBackgroundTask(pipeBase.PipelineTask): 

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

285 

286 The cpSkyMaskedIsr images constructed by CpSkyImageTask have the 

287 scaled background constructed by CpSkyScaleMeasureTask subtracted, 

288 and new background models are constructed for the remaining 

289 signal. 

290 

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

292 created here has definition clashes that prevent that from being 

293 reused. 

294 """ 

295 

296 ConfigClass = CpSkySubtractBackgroundConfig 

297 _DefaultName = "cpSkySubtractBkg" 

298 

299 def __init__(self, **kwargs): 

300 super().__init__(**kwargs) 

301 self.makeSubtask("sky") 

302 

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

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

305 images. 

306 

307 Parameters 

308 ---------- 

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

310 The ISR processed, detection masked image. 

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

312 Full focal plane background for this exposure. 

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

314 Metadata containing the scale factor. 

315 

316 Returns 

317 ------- 

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

319 The results struct containing: 

320 

321 ``outputBkg`` 

322 Remnant sky background with the full-exposure 

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

324 """ 

325 # As constructCalibs.py SkyTask.processSingle() 

326 image = inputExp.getMaskedImage() 

327 detector = inputExp.getDetector() 

328 bbox = image.getBBox() 

329 

330 scale = inputScale.get('scale') 

331 background = inputBkg.toCcdBackground(detector, bbox) 

332 image -= background.getImage() 

333 image /= scale 

334 

335 newBackground = self.sky.measureBackground(image) 

336 return pipeBase.Struct( 

337 outputBkg=newBackground 

338 ) 

339 

340 

341class CpSkyCombineConnections(pipeBase.PipelineTaskConnections, 

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

343 inputBkgs = cT.Input( 

344 name="cpExpBackground", 

345 doc="Normalized, static background.", 

346 storageClass="Background", 

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

348 multiple=True, 

349 ) 

350 inputExpHandles = cT.Input( 

351 name="cpSkyMaskedIsr", 

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

353 storageClass="Exposure", 

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

355 multiple=True, 

356 deferLoad=True, 

357 ) 

358 

359 outputCalib = cT.Output( 

360 name="sky", 

361 doc="Averaged static background.", 

362 storageClass="ExposureF", 

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

364 isCalibration=True, 

365 ) 

366 

367 

368class CpSkyCombineConfig(pipeBase.PipelineTaskConfig, 

369 pipelineConnections=CpSkyCombineConnections): 

370 sky = pexConfig.ConfigurableField( 

371 target=SkyMeasurementTask, 

372 doc="Sky measurement", 

373 ) 

374 

375 

376class CpSkyCombineTask(pipeBase.PipelineTask): 

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

378 

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

380 averaged to produce the final SKY calibration. 

381 

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

383 definition classes in gen3. 

384 """ 

385 

386 ConfigClass = CpSkyCombineConfig 

387 _DefaultName = "cpSkyCombine" 

388 

389 def __init__(self, **kwargs): 

390 super().__init__(**kwargs) 

391 self.makeSubtask("sky") 

392 

393 def run(self, inputBkgs, inputExpHandles): 

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

395 

396 Parameters 

397 ---------- 

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

399 Remnant backgrounds from each exposure. 

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

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

402 

403 Returns 

404 ------- 

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

406 The results struct containing: 

407 

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

409 The final sky calibration product. 

410 """ 

411 skyCalib = self.sky.averageBackgrounds(inputBkgs) 

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

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

414 

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

416 

417 return pipeBase.Struct( 

418 outputCalib=skyCalib, 

419 )