Coverage for tests / test_assemble_cell_coadd.py: 24%

135 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-16 00:38 -0800

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 

23from __future__ import annotations 

24 

25import unittest 

26import warnings 

27from typing import TYPE_CHECKING, Iterable 

28 

29import galsim 

30import numpy as np 

31from assemble_coadd_test_utils import MockCoaddTestData, makeMockSkyInfo 

32 

33import lsst.afw.image as afwImage 

34import lsst.pipe.base as pipeBase 

35import lsst.utils.tests 

36from lsst.cell_coadds import CommonComponents 

37from lsst.drp.tasks.assemble_cell_coadd import ( 

38 AssembleCellCoaddConfig, 

39 AssembleCellCoaddTask, 

40 WarpInputs, 

41) 

42 

43if TYPE_CHECKING: 

44 from lsst.cell_coadds import ObservationIdentifiers 

45 

46__all__ = ( 

47 "MockAssembleCellCoaddConfig", 

48 "MockAssembleCellCoaddTask", 

49) 

50 

51 

52class MockAssembleCellCoaddConfig(AssembleCellCoaddConfig): 

53 def setDefaults(self): 

54 super().setDefaults() 

55 self.require_artifact_mask = False 

56 

57 

58class MockAssembleCellCoaddTask(AssembleCellCoaddTask): 

59 """Lightly modified version of `AssembleCellCoaddTask` for unit tests. 

60 

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

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

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

64 """ 

65 

66 ConfigClass = MockAssembleCellCoaddConfig 

67 

68 def runQuantum( 

69 self, 

70 mockSkyInfo, 

71 warpRefList, 

72 maskedFractionRefList, 

73 noise0RefList, 

74 visitSummaryList=None, 

75 ): 

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

77 Butler. 

78 

79 Parameters 

80 ---------- 

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

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

83 same format as the output of 

84 `lsst.pipe.tasks.CoaddBaseTask.getSkyInfo` 

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

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

87 using the Gen 3 API. 

88 

89 Returns 

90 ------- 

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

92 The coadded exposure and associated metadata. 

93 """ 

94 

95 self.common = CommonComponents( 

96 units=None, 

97 wcs=mockSkyInfo.wcs, 

98 band="i", 

99 identifiers=pipeBase.Struct(skymap=None, tract=0, patch=42, band="i"), 

100 ) 

101 

102 inputs = {} 

103 for warpInput, maskedFractionInput, noise0Input in zip( 

104 warpRefList, 

105 maskedFractionRefList, 

106 noise0RefList, 

107 ): 

108 inputs[warpInput.dataId] = WarpInputs( 

109 warp=warpInput, 

110 masked_fraction=maskedFractionInput, 

111 noise_warps=[noise0Input], 

112 ) 

113 

114 retStruct = self.run( 

115 inputs=inputs, 

116 skyInfo=mockSkyInfo, 

117 visitSummaryList=visitSummaryList, 

118 ) 

119 

120 return retStruct 

121 

122 

123class AssembleCellCoaddTestCase(lsst.utils.tests.TestCase): 

124 """Tests of AssembleCellCoaddTask. 

125 

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

127 execution. 

128 """ 

129 

130 @classmethod 

131 def setUpClass(cls) -> None: 

132 np.random.seed(42) 

133 rng = np.random.Generator(np.random.MT19937(42)) 

134 patch = 42 

135 tract = 0 

136 testData = MockCoaddTestData(fluxRange=1e4) 

137 exposures = {} 

138 matchedExposures = {} 

139 masked_fraction_images = {} 

140 noise0_masked_images = {} 

141 for expId in range(100, 110): 

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

143 masked_fraction_images[expId] = afwImage.ImageF(bbox=exposures[expId].getBBox()) 

144 masked_fraction_images[expId].array[:, :] = rng.random(masked_fraction_images[expId].array.shape) 

145 noise0_masked_images[expId] = afwImage.MaskedImageF(bbox=exposures[expId].getBBox()) 

146 noise0_masked_images[expId].image.array[:, :] = rng.normal( 

147 0, 1, noise0_masked_images[expId].image.array.shape 

148 ) * (exposures[expId].variance.array**0.5) 

149 

150 cls.handleList = testData.makeDataRefList(exposures, patch=patch, tract=tract) 

151 cls.maskedFractionRefList = testData.makeDataRefList(masked_fraction_images, patch=patch, tract=tract) 

152 cls.noise0RefList = testData.makeDataRefList(noise0_masked_images, patch=patch, tract=tract) 

153 cls.visitSummaryList = [ 

154 testData.makeVisitSummaryTableHandle(warpHandle) for warpHandle in cls.handleList 

155 ] 

156 cls.skyInfo = makeMockSkyInfo(testData.bbox, testData.wcs, patch=patch) 

157 

158 def runTask( 

159 self, 

160 config=None, 

161 warpRefList=None, 

162 maskedFractionRefList=None, 

163 noise0RefList=None, 

164 visitSummaryList=None, 

165 ) -> None: 

166 if config is None: 

167 config = MockAssembleCellCoaddConfig() 

168 assembleTask = MockAssembleCellCoaddTask(config=config) 

169 if warpRefList is None: 

170 warpRefList = self.handleList 

171 if maskedFractionRefList is None: 

172 maskedFractionRefList = self.maskedFractionRefList 

173 if noise0RefList is None: 

174 noise0RefList = self.noise0RefList 

175 if visitSummaryList is None: 

176 visitSummaryList = self.visitSummaryList 

177 

178 self.result = assembleTask.runQuantum( 

179 self.skyInfo, 

180 warpRefList=warpRefList, 

181 maskedFractionRefList=maskedFractionRefList, 

182 noise0RefList=noise0RefList, 

183 visitSummaryList=visitSummaryList, 

184 ) 

185 

186 def checkSortOrder(self, inputs: Iterable[ObservationIdentifiers]) -> None: 

187 """Check that the inputs are sorted. 

188 

189 The inputs must be sorted first by visit, and within the same visit, 

190 by detector. 

191 

192 Parameters 

193 ---------- 

194 inputs : `Iterable` [`ObservationIdentifiers`] 

195 The inputs to be checked. 

196 """ 

197 visit, detector = -np.inf, -np.inf # Previous visit, detector IDs. 

198 for _, obsId in enumerate(inputs): 

199 with self.subTest(input_number=repr(obsId)): 

200 self.assertGreaterEqual(obsId.visit, visit) 

201 if visit == obsId.visit: 

202 with self.subTest(detector_number=repr(obsId.detector)): 

203 self.assertGreaterEqual(obsId.detector, detector) 

204 

205 visit, detector = obsId.visit, obsId.detector 

206 

207 def checkRun(self, assembleTask): 

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

209 result = assembleTask.runQuantum(self.skyInfo, self.handleList) 

210 

211 # Check that we produced an exposure. 

212 self.assertTrue(result.multipleCellCoadd is not None) 

213 # Check that the visit_count method returns a number less than or equal 

214 # to the total number of input exposures available. 

215 max_visit_count = len(self.handleList) 

216 for cellId, singleCellCoadd in result.multipleCellCoadd.cells.items(): 

217 with self.subTest(x=repr(cellId.x), y=repr(cellId.y)): 

218 self.assertLessEqual(singleCellCoadd.visit_count, max_visit_count) 

219 # Check that the aperture correction maps are not None. 

220 with self.subTest(x=repr(cellId.x), y=repr(cellId.y)): 

221 self.assertTrue(singleCellCoadd.aperture_correction_map is not None) 

222 # Check that the inputs are sorted. 

223 with self.subTest(x=repr(cellId.x), y=repr(cellId.y)): 

224 self.checkSortOrder(singleCellCoadd.inputs) 

225 

226 def test_assemble_basic(self): 

227 """Test that AssembleCellCoaddTask runs successfully without errors. 

228 

229 This test does not check the correctness of the coaddition algorithms. 

230 This is intended to prevent the code from bit rotting. 

231 """ 

232 self.runTask() 

233 # Check that we produced an exposure. 

234 self.assertTrue(self.result.multipleCellCoadd is not None) 

235 

236 def test_assemble_empty(self): 

237 """Test that AssembleCellCoaddTask runs successfully without errors 

238 when no input exposures are provided.""" 

239 self.result = None # so tearDown has something. 

240 with self.assertRaises(pipeBase.NoWorkFound, msg="No cells could be populated for the cell coadd."): 

241 self.runTask(warpRefList=[], maskedFractionRefList=[], noise0RefList=[], visitSummaryList=[]) 

242 

243 def test_assemble_without_visitSummary(self): 

244 """Test that AssembleCellCoaddTask calculates detector weights and 

245 runs successfully without errors when no visit summaries are provided. 

246 """ 

247 self.runTask(visitSummaryList=[]) 

248 # Check that we produced an exposure. 

249 self.assertTrue(self.result.multipleCellCoadd is not None) 

250 

251 # TODO: Remove this test in DM-49401 

252 @lsst.utils.tests.methodParameters(do_scale_zero_point=[False, True]) 

253 def test_do_scale_zero_point(self, do_scale_zero_point): 

254 config = MockAssembleCellCoaddConfig() 

255 with warnings.catch_warnings(): 

256 warnings.filterwarnings("ignore", category=FutureWarning) 

257 config.do_scale_zero_point = do_scale_zero_point 

258 self.runTask(config) 

259 # Check that we produced an exposure. 

260 self.assertTrue(self.result.multipleCellCoadd is not None) 

261 

262 @lsst.utils.tests.methodParameters(do_calculate_weight_from_warp=[False, True]) 

263 def test_do_calculate_weight_from_warp(self, do_calculate_weight_from_warp): 

264 config = MockAssembleCellCoaddConfig() 

265 config.do_calculate_weight_from_warp = do_calculate_weight_from_warp 

266 self.runTask(config) 

267 # Check that we produced an exposure. 

268 self.assertTrue(self.result.multipleCellCoadd is not None) 

269 

270 def test_visit_count(self): 

271 """Check that the visit_count method returns a number less than or 

272 equal to the total number of input exposures available. 

273 """ 

274 self.runTask() 

275 max_visit_count = len(self.handleList) 

276 for cellId, singleCellCoadd in self.result.multipleCellCoadd.cells.items(): 

277 with self.subTest(x=repr(cellId.x), y=repr(cellId.y)): 

278 self.assertLessEqual(singleCellCoadd.visit_count, max_visit_count) 

279 

280 def test_inputs_sorted(self): 

281 """Check that the inputs are sorted. 

282 

283 The ordering is that inputs are sorted first by visit, and within the 

284 same visit, they are ordered by detector. 

285 """ 

286 self.runTask() 

287 for _, singleCellCoadd in self.result.multipleCellCoadd.cells.items(): 

288 self.checkSortOrder(singleCellCoadd.inputs) 

289 

290 def test_psf_normalization(self): 

291 """Check that the sum of PSF images is close to 1.""" 

292 self.runTask() 

293 for cellId, singleCellCoadd in self.result.multipleCellCoadd.cells.items(): 

294 with self.subTest(x=repr(cellId.x), y=repr(cellId.y)): 

295 self.assertFloatsAlmostEqual(singleCellCoadd.psf_image.array.sum(), 1.0, rtol=None, atol=1e-7) 

296 

297 def test_psf_centering(self): 

298 """Check that the PSF images are centered.""" 

299 self.runTask() 

300 for cellId, singleCellCoadd in self.result.multipleCellCoadd.cells.items(): 

301 with self.subTest(x=repr(cellId.x), y=repr(cellId.y)): 

302 shape = galsim.hsm.FindAdaptiveMom(galsim.Image(singleCellCoadd.psf_image.array)) 

303 self.assertFloatsAlmostEqual( 

304 shape.moments_centroid.x, round(shape.moments_centroid.x), rtol=None, atol=0.01 

305 ) 

306 self.assertFloatsAlmostEqual( 

307 shape.moments_centroid.y, round(shape.moments_centroid.y), rtol=None, atol=0.01 

308 ) 

309 

310 

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

312 pass 

313 

314 

315def setup_module(module): 

316 lsst.utils.tests.init() 

317 

318 

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

320 lsst.utils.tests.init() 

321 unittest.main()