Coverage for tests / test_assemble_coadd.py: 26%
171 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 09:31 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 09:31 +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
25import numpy as np
26from assemble_coadd_test_utils import MockCoaddTestData, makeMockSkyInfo
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
38__all__ = [
39 "MockAssembleCoaddConfig",
40 "MockAssembleCoaddTask",
41 "MockCompareWarpAssembleCoaddConfig",
42 "MockCompareWarpAssembleCoaddTask",
43]
46class MockAssembleCoaddConfig(AssembleCoaddConfig):
47 def setDefaults(self):
48 super().setDefaults()
49 self.doWrite = False
52class MockAssembleCoaddTask(AssembleCoaddTask):
53 """Lightly modified version of `AssembleCoaddTask` for use with unit tests.
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 """
60 ConfigClass = MockAssembleCoaddConfig
62 def __init__(self, **kwargs):
63 super().__init__(**kwargs)
64 self.warpType = self.config.warpType
65 self.makeSubtask("interpImage")
66 self.makeSubtask("scaleZeroPoint")
68 def processResults(self, *args, **kwargs):
69 "This should be tested separately."
70 pass
72 def runQuantum(self, mockSkyInfo, warpRefList, psfMatchedWarpRefList=None, **kwargs):
73 """Modified interface for testing coaddition algorithms without a
74 Butler.
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.
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
104class MockCompareWarpAssembleCoaddConfig(CompareWarpAssembleCoaddConfig):
105 def setDefaults(self):
106 super().setDefaults()
107 self.assembleStaticSkyModel.retarget(MockAssembleCoaddTask)
108 self.assembleStaticSkyModel.doWrite = False
109 self.doWrite = False
112class MockCompareWarpAssembleCoaddTask(MockAssembleCoaddTask, CompareWarpAssembleCoaddTask):
113 """Lightly modified version of `CompareWarpAssembleCoaddTask`
114 for use with unit tests.
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 """
121 ConfigClass = MockCompareWarpAssembleCoaddConfig
122 _DefaultName = "compareWarpAssembleCoadd"
124 def __init__(self, *args, **kwargs):
125 CompareWarpAssembleCoaddTask.__init__(self, *args, **kwargs)
127 def runQuantum(self, mockSkyInfo, warpRefList, psfMatchedWarpRefList=None, *args):
128 inputs = self.prepareInputs(warpRefList, mockSkyInfo.bbox, psfMatchedWarpRefList)
130 assembleStaticSkyModel = MockAssembleCoaddTask(config=self.config.assembleStaticSkyModel)
131 templateCoadd = assembleStaticSkyModel.runQuantum(mockSkyInfo, warpRefList)
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 )
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
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
162class MockDcrAssembleCoaddTask(MockCompareWarpAssembleCoaddTask, DcrAssembleCoaddTask):
163 """Lightly modified version of `DcrAssembleCoaddTask`
164 for use with unit tests.
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 """
171 ConfigClass = MockDcrAssembleCoaddConfig
172 _DefaultName = "dcrAssembleCoadd"
174 def __init__(self, *args, **kwargs):
175 DcrAssembleCoaddTask.__init__(self, *args, **kwargs)
178class MockInputMapAssembleCoaddConfig(MockCompareWarpAssembleCoaddConfig):
179 def setDefaults(self):
180 super().setDefaults()
181 self.doInputMap = True
184class MockInputMapAssembleCoaddTask(MockCompareWarpAssembleCoaddTask):
185 """Lightly modified version of `CompareWarpAssembleCoaddTask`
186 for use with unit tests.
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 """
193 ConfigClass = MockInputMapAssembleCoaddConfig
194 _DefaultName = "inputMapAssembleCoadd"
196 def __init__(self, *args, **kwargs):
197 CompareWarpAssembleCoaddTask.__init__(self, *args, **kwargs)
200class AssembleCoaddTestCase(lsst.utils.tests.TestCase):
201 """Tests of AssembleCoaddTask and its derived classes.
203 These tests bypass the middleware used for accessing data and managing Task
204 execution.
205 """
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)
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 )
228 # Check that we produced an exposure.
229 self.assertTrue(result.coaddExposure is not None)
230 self.assertTrue("BUNIT" in result.coaddExposure.metadata)
232 def testAssembleBasic(self):
233 config = MockAssembleCoaddConfig()
234 assembleTask = MockAssembleCoaddTask(config=config)
235 self.checkRun(assembleTask)
237 def testAssemblePsfMatched(self):
238 config = MockAssembleCoaddConfig(warpType="psfMatched")
239 assembleTask = MockAssembleCoaddTask(config=config)
240 self.checkRun(assembleTask, warpType="psfMatched")
242 def testAssembleCompareWarp(self):
243 config = MockCompareWarpAssembleCoaddConfig()
244 assembleTask = MockCompareWarpAssembleCoaddTask(config=config)
245 self.checkRun(assembleTask)
247 def testAssembleDCR(self):
248 config = MockDcrAssembleCoaddConfig()
249 assembleTask = MockDcrAssembleCoaddTask(config=config)
250 self.checkRun(assembleTask)
252 def testOnlineCoadd(self):
253 config = MockInputMapAssembleCoaddConfig()
254 config.statistic = "MEAN"
255 assembleTask = MockInputMapAssembleCoaddTask(config=config)
257 handleList = self.handleList
258 results = assembleTask.runQuantum(
259 self.skyInfo,
260 handleList,
261 psfMatchedWarpRefList=self.handleListPsfMatched,
262 )
263 coadd = results.coaddExposure
265 configOnline = MockInputMapAssembleCoaddConfig()
266 configOnline.statistic = "MEAN"
267 configOnline.doOnlineForMean = True
268 configOnline.validate()
269 assembleTaskOnline = MockInputMapAssembleCoaddTask(config=configOnline)
271 resultsOnline = assembleTaskOnline.runQuantum(
272 self.skyInfo,
273 handleList,
274 psfMatchedWarpRefList=self.handleListPsfMatched,
275 )
276 coaddOnline = resultsOnline.coaddExposure
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)
282 def testInputMap(self):
283 config = MockInputMapAssembleCoaddConfig()
284 assembleTask = MockInputMapAssembleCoaddTask(config=config)
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)
303 results = assembleTask.runQuantum(
304 self.skyInfo,
305 handleList,
306 psfMatchedWarpRefList=self.handleListPsfMatched,
307 )
309 inputMap = results.inputMap
310 validPix, raPix, decPix = inputMap.valid_pixels_pos(return_pixels=True)
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)
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]])))
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 )
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 )
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 )
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 )
381class MyMemoryTestCase(lsst.utils.tests.MemoryTestCase):
382 pass
385def setup_module(module):
386 lsst.utils.tests.init()
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()