Coverage for tests / test_assemble_coadd.py: 26%

171 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-06 08:57 +0000

1# This file is part of drp_tasks. 

2# 

3# LSST Data Management System 

4# This product includes software developed by the 

5# LSST Project (http://www.lsst.org/). 

6# See COPYRIGHT file at the top of the source tree. 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22"""Test AssembleCoaddTask and its variants.""" 

23import unittest 

24 

25import numpy as np 

26from assemble_coadd_test_utils import MockCoaddTestData, makeMockSkyInfo 

27 

28import lsst.pipe.base as pipeBase 

29import lsst.utils.tests 

30from lsst.drp.tasks.assemble_coadd import ( 

31 AssembleCoaddConfig, 

32 AssembleCoaddTask, 

33 CompareWarpAssembleCoaddConfig, 

34 CompareWarpAssembleCoaddTask, 

35) 

36from lsst.drp.tasks.dcr_assemble_coadd import DcrAssembleCoaddConfig, DcrAssembleCoaddTask 

37 

38__all__ = [ 

39 "MockAssembleCoaddConfig", 

40 "MockAssembleCoaddTask", 

41 "MockCompareWarpAssembleCoaddConfig", 

42 "MockCompareWarpAssembleCoaddTask", 

43] 

44 

45 

46class MockAssembleCoaddConfig(AssembleCoaddConfig): 

47 def setDefaults(self): 

48 super().setDefaults() 

49 self.doWrite = False 

50 

51 

52class MockAssembleCoaddTask(AssembleCoaddTask): 

53 """Lightly modified version of `AssembleCoaddTask` for use with unit tests. 

54 

55 The modifications bypass the usual middleware for loading data and setting 

56 up the Task, and instead supply in-memory mock data references to the `run` 

57 method so that the coaddition algorithms can be tested without a Butler. 

58 """ 

59 

60 ConfigClass = MockAssembleCoaddConfig 

61 

62 def __init__(self, **kwargs): 

63 super().__init__(**kwargs) 

64 self.warpType = self.config.warpType 

65 self.makeSubtask("interpImage") 

66 self.makeSubtask("scaleZeroPoint") 

67 

68 def processResults(self, *args, **kwargs): 

69 "This should be tested separately." 

70 pass 

71 

72 def runQuantum(self, mockSkyInfo, warpRefList, psfMatchedWarpRefList=None, **kwargs): 

73 """Modified interface for testing coaddition algorithms without a 

74 Butler. 

75 

76 Parameters 

77 ---------- 

78 mockSkyInfo : `lsst.pipe.base.Struct` 

79 A simple container that supplies a bounding box and WCS in the 

80 same format as the output of 

81 `lsst.pipe.tasks.CoaddBaseTask.getSkyInfo` 

82 warpRefList : `list` of `lsst.pipe.tasks.MockExposureReference` 

83 Data references to the test exposures that will be coadded, 

84 using the Gen 3 API. 

85 

86 Returns 

87 ------- 

88 retStruct : `lsst.pipe.base.Struct` 

89 The coadded exposure and associated metadata. 

90 """ 

91 inputs = self.prepareInputs(warpRefList, mockSkyInfo.bbox, psfMatchedWarpRefList) 

92 retStruct = self.run( 

93 mockSkyInfo, 

94 warpRefList=inputs.warpRefList, 

95 imageScalerList=inputs.imageScalerList, 

96 weightList=inputs.weightList, 

97 psfMatchedWarpRefList=inputs.psfMatchedWarpRefList, 

98 supplementaryData=pipeBase.Struct(), 

99 **kwargs, 

100 ) 

101 return retStruct 

102 

103 

104class MockCompareWarpAssembleCoaddConfig(CompareWarpAssembleCoaddConfig): 

105 def setDefaults(self): 

106 super().setDefaults() 

107 self.assembleStaticSkyModel.retarget(MockAssembleCoaddTask) 

108 self.assembleStaticSkyModel.doWrite = False 

109 self.doWrite = False 

110 

111 

112class MockCompareWarpAssembleCoaddTask(MockAssembleCoaddTask, CompareWarpAssembleCoaddTask): 

113 """Lightly modified version of `CompareWarpAssembleCoaddTask` 

114 for use with unit tests. 

115 

116 The modifications bypass the usual middleware for loading data and setting 

117 up the Task, and instead supply in-memory mock data references to the `run` 

118 method so that the coaddition algorithms can be tested without a Butler. 

119 """ 

120 

121 ConfigClass = MockCompareWarpAssembleCoaddConfig 

122 _DefaultName = "compareWarpAssembleCoadd" 

123 

124 def __init__(self, *args, **kwargs): 

125 CompareWarpAssembleCoaddTask.__init__(self, *args, **kwargs) 

126 

127 def runQuantum(self, mockSkyInfo, warpRefList, psfMatchedWarpRefList=None, *args): 

128 inputs = self.prepareInputs(warpRefList, mockSkyInfo.bbox, psfMatchedWarpRefList) 

129 

130 assembleStaticSkyModel = MockAssembleCoaddTask(config=self.config.assembleStaticSkyModel) 

131 templateCoadd = assembleStaticSkyModel.runQuantum(mockSkyInfo, warpRefList) 

132 

133 supplementaryData = pipeBase.Struct( 

134 templateCoadd=templateCoadd.coaddExposure, 

135 nImage=templateCoadd.nImage, 

136 warpRefList=templateCoadd.warpRefList, 

137 imageScalerList=templateCoadd.imageScalerList, 

138 weightList=templateCoadd.weightList, 

139 ) 

140 

141 retStruct = self.run( 

142 mockSkyInfo, 

143 warpRefList=inputs.warpRefList, 

144 imageScalerList=inputs.imageScalerList, 

145 weightList=inputs.weightList, 

146 psfMatchedWarpRefList=inputs.psfMatchedWarpRefList, 

147 supplementaryData=supplementaryData, 

148 ) 

149 return retStruct 

150 

151 

152class MockDcrAssembleCoaddConfig(DcrAssembleCoaddConfig): 

153 def setDefaults(self): 

154 super().setDefaults() 

155 self.assembleStaticSkyModel.retarget(MockCompareWarpAssembleCoaddTask) 

156 self.assembleStaticSkyModel.doWrite = False 

157 self.doWrite = False 

158 self.effectiveWavelength = 476.31 # Use LSST g band values for the test. 

159 self.bandwidth = 552.0 - 405.0 

160 

161 

162class MockDcrAssembleCoaddTask(MockCompareWarpAssembleCoaddTask, DcrAssembleCoaddTask): 

163 """Lightly modified version of `DcrAssembleCoaddTask` 

164 for use with unit tests. 

165 

166 The modifications bypass the usual middleware for loading data and setting 

167 up the Task, and instead supply in-memory mock data references to the `run` 

168 method so that the coaddition algorithms can be tested without a Butler. 

169 """ 

170 

171 ConfigClass = MockDcrAssembleCoaddConfig 

172 _DefaultName = "dcrAssembleCoadd" 

173 

174 def __init__(self, *args, **kwargs): 

175 DcrAssembleCoaddTask.__init__(self, *args, **kwargs) 

176 

177 

178class MockInputMapAssembleCoaddConfig(MockCompareWarpAssembleCoaddConfig): 

179 def setDefaults(self): 

180 super().setDefaults() 

181 self.doInputMap = True 

182 

183 

184class MockInputMapAssembleCoaddTask(MockCompareWarpAssembleCoaddTask): 

185 """Lightly modified version of `CompareWarpAssembleCoaddTask` 

186 for use with unit tests. 

187 

188 The modifications bypass the usual middleware for loading data and setting 

189 up the Task, and instead supply in-memory mock data references to the `run` 

190 method so that the coaddition algorithms can be tested without a Butler. 

191 """ 

192 

193 ConfigClass = MockInputMapAssembleCoaddConfig 

194 _DefaultName = "inputMapAssembleCoadd" 

195 

196 def __init__(self, *args, **kwargs): 

197 CompareWarpAssembleCoaddTask.__init__(self, *args, **kwargs) 

198 

199 

200class AssembleCoaddTestCase(lsst.utils.tests.TestCase): 

201 """Tests of AssembleCoaddTask and its derived classes. 

202 

203 These tests bypass the middleware used for accessing data and managing Task 

204 execution. 

205 """ 

206 

207 def setUp(self): 

208 patch = 42 

209 tract = 0 

210 testData = MockCoaddTestData(fluxRange=1e4) 

211 exposures = {} 

212 matchedExposures = {} 

213 for expId in range(100, 110): 

214 exposures[expId], matchedExposures[expId] = testData.makeTestImage(expId) 

215 self.handleList = testData.makeDataRefList(exposures, patch=patch, tract=tract) 

216 self.handleListPsfMatched = testData.makeDataRefList(matchedExposures, patch=patch, tract=tract) 

217 self.skyInfo = makeMockSkyInfo(testData.bbox, testData.wcs, patch=patch) 

218 

219 def checkRun(self, assembleTask, warpType="direct"): 

220 """Check that the task runs successfully.""" 

221 handleList = self.handleListPsfMatched if warpType == "psfMatched" else self.handleList 

222 result = assembleTask.runQuantum( 

223 self.skyInfo, 

224 handleList, 

225 psfMatchedWarpRefList=self.handleListPsfMatched, 

226 ) 

227 

228 # Check that we produced an exposure. 

229 self.assertTrue(result.coaddExposure is not None) 

230 self.assertTrue("BUNIT" in result.coaddExposure.metadata) 

231 

232 def testAssembleBasic(self): 

233 config = MockAssembleCoaddConfig() 

234 assembleTask = MockAssembleCoaddTask(config=config) 

235 self.checkRun(assembleTask) 

236 

237 def testAssemblePsfMatched(self): 

238 config = MockAssembleCoaddConfig(warpType="psfMatched") 

239 assembleTask = MockAssembleCoaddTask(config=config) 

240 self.checkRun(assembleTask, warpType="psfMatched") 

241 

242 def testAssembleCompareWarp(self): 

243 config = MockCompareWarpAssembleCoaddConfig() 

244 assembleTask = MockCompareWarpAssembleCoaddTask(config=config) 

245 self.checkRun(assembleTask) 

246 

247 def testAssembleDCR(self): 

248 config = MockDcrAssembleCoaddConfig() 

249 assembleTask = MockDcrAssembleCoaddTask(config=config) 

250 self.checkRun(assembleTask) 

251 

252 def testOnlineCoadd(self): 

253 config = MockInputMapAssembleCoaddConfig() 

254 config.statistic = "MEAN" 

255 assembleTask = MockInputMapAssembleCoaddTask(config=config) 

256 

257 handleList = self.handleList 

258 results = assembleTask.runQuantum( 

259 self.skyInfo, 

260 handleList, 

261 psfMatchedWarpRefList=self.handleListPsfMatched, 

262 ) 

263 coadd = results.coaddExposure 

264 

265 configOnline = MockInputMapAssembleCoaddConfig() 

266 configOnline.statistic = "MEAN" 

267 configOnline.doOnlineForMean = True 

268 configOnline.validate() 

269 assembleTaskOnline = MockInputMapAssembleCoaddTask(config=configOnline) 

270 

271 resultsOnline = assembleTaskOnline.runQuantum( 

272 self.skyInfo, 

273 handleList, 

274 psfMatchedWarpRefList=self.handleListPsfMatched, 

275 ) 

276 coaddOnline = resultsOnline.coaddExposure 

277 

278 self.assertFloatsAlmostEqual(coaddOnline.image.array, coadd.image.array, rtol=1e-3) 

279 self.assertFloatsAlmostEqual(coaddOnline.variance.array, coadd.variance.array, rtol=1e-6) 

280 self.assertMasksEqual(coaddOnline.mask, coadd.mask) 

281 

282 def testInputMap(self): 

283 config = MockInputMapAssembleCoaddConfig() 

284 assembleTask = MockInputMapAssembleCoaddTask(config=config) 

285 

286 # Make exposures where one of them has a bad region. 

287 patch = 42 

288 tract = 0 

289 testData = MockCoaddTestData(fluxRange=1e4) 

290 exposures = {} 

291 matchedExposures = {} 

292 for expId in range(100, 110): 

293 if expId == 105: 

294 badBox = lsst.geom.Box2I( 

295 lsst.geom.Point2I(testData.bbox.beginX + 10, testData.bbox.beginY + 10), 

296 lsst.geom.Extent2I(100, 100), 

297 ) 

298 else: 

299 badBox = None 

300 exposures[expId], matchedExposures[expId] = testData.makeTestImage(expId, badRegionBox=badBox) 

301 handleList = testData.makeDataRefList(exposures, patch=patch, tract=tract) 

302 

303 results = assembleTask.runQuantum( 

304 self.skyInfo, 

305 handleList, 

306 psfMatchedWarpRefList=self.handleListPsfMatched, 

307 ) 

308 

309 inputMap = results.inputMap 

310 validPix, raPix, decPix = inputMap.valid_pixels_pos(return_pixels=True) 

311 

312 # Confirm that all the map pixels are in the bounding box 

313 # Exposure 100 is the first one and they all have the same WCS in the 

314 # tests. 

315 xPix, yPix = exposures[100].getWcs().skyToPixelArray(raPix, decPix, degrees=True) 

316 self.assertGreater(xPix.min(), testData.bbox.beginX) 

317 self.assertGreater(yPix.min(), testData.bbox.beginY) 

318 self.assertLess(xPix.max(), testData.bbox.endX) 

319 self.assertLess(xPix.max(), testData.bbox.endY) 

320 

321 # Confirm that all exposures except 105 are completely covered 

322 # This assumes we have one input per visit in the mock data. 

323 metadata = inputMap.metadata 

324 visitBitDict = {} 

325 for bit in range(inputMap.wide_mask_maxbits): 

326 if f"B{bit:04d}VIS" in metadata: 

327 visitBitDict[metadata[f"B{bit:04d}VIS"]] = bit 

328 for expId in range(100, 110): 

329 if expId == 105: 

330 self.assertFalse(np.all(inputMap.check_bits_pix(validPix, [visitBitDict[expId]]))) 

331 else: 

332 self.assertTrue(np.all(inputMap.check_bits_pix(validPix, [visitBitDict[expId]]))) 

333 

334 @lsst.utils.tests.methodParameters(doOnlineForMean=[False, True]) 

335 def testArtifactMask(self, doOnlineForMean): 

336 """Test that CompareWarp and AssembleCoadd with artifact mask produce 

337 identical results. 

338 """ 

339 config = MockCompareWarpAssembleCoaddConfig() 

340 config.doOnlineForMean = doOnlineForMean 

341 compareWarpTask = MockCompareWarpAssembleCoaddTask(config=config) 

342 compareWarpResult = compareWarpTask.runQuantum( 

343 self.skyInfo, 

344 self.handleList, 

345 psfMatchedWarpRefList=self.handleListPsfMatched, 

346 ) 

347 

348 # Make a new list of handles with artifact masks applied. 

349 handleList = [] 

350 for handle, artifactMask in zip(self.handleList, compareWarpResult.altMaskList): 

351 dataSet = handle.get() 

352 compareWarpTask.applyAltMaskPlanes(dataSet.mask, artifactMask) 

353 # Repackage the dataset into a DataRef. 

354 handleList.append( 

355 pipeBase.InMemoryDatasetHandle( 

356 dataSet, storageClass=handle.storageClass, copy=True, dataId=handle.dataId 

357 ) 

358 ) 

359 

360 config = MockAssembleCoaddConfig() 

361 config.badMaskPlanes = ["NO_DATA", "BAD", "SAT", "CLIPPED", "REJECTED"] 

362 config.statistic = "MEAN" # CompareWarp sets this statistic internally. 

363 config.doOnlineForMean = doOnlineForMean 

364 assembleTask = MockAssembleCoaddTask(config=config) 

365 assembleResult = assembleTask.runQuantum( 

366 self.skyInfo, 

367 handleList, 

368 psfMatchedWarpRefList=self.handleListPsfMatched, 

369 mask=compareWarpResult.coaddExposure.mask.getPlaneBitMask(config.badMaskPlanes), 

370 ) 

371 

372 # Mask planes vary because of removeMaskPlanes calls in AssembleCoadd. 

373 # Instead, just compare the other planes. 

374 self.assertMaskedImagesEqual( 

375 compareWarpResult.coaddExposure, 

376 assembleResult.coaddExposure, 

377 doMask=False, 

378 ) 

379 

380 

381class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase): 

382 pass 

383 

384 

385def setup_module(module): 

386 lsst.utils.tests.init() 

387 

388 

389if __name__ == "__main__": 389 ↛ 390line 389 didn't jump to line 390 because the condition on line 389 was never true

390 lsst.utils.tests.init() 

391 unittest.main()