Coverage for tests/test_postprocess.py: 98%

134 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-14 19:17 +0000

1# This file is part of pipe_tasks. 

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 <https://www.gnu.org/licenses/>. 

21 

22import pytest 

23import tempfile 

24import unittest 

25 

26import astropy.units as u 

27from astropy.table import Table 

28import numpy as np 

29 

30import lsst.afw.geom as afwGeom 

31import lsst.afw.image as afwImage 

32from lsst.afw.cameraGeom.testUtils import CameraWrapper 

33import lsst.daf.butler 

34import lsst.daf.butler.tests as butlerTests 

35import lsst.geom 

36import lsst.images 

37import lsst.images.psfs 

38import lsst.meas.base.tests 

39import lsst.utils.tests 

40from lsst.pipe.tasks.postprocess import ConsolidateVisitSummaryTask, ModelExtendednessColumnAction 

41 

42from utils import makeTestVisitInfo, make_exposure_record 

43 

44 

45class ModelExtendednessColumnActionTestCase(lsst.utils.tests.TestCase): 

46 """Demo test case.""" 

47 

48 def setUp(self): 

49 self.bands = ("g", "r", "i") 

50 action = ModelExtendednessColumnAction(bands=self.bands, min_n_good_to_shift_flux_ratio=1) 

51 self.action = action 

52 data = { 

53 action.size_column.format(axis="x"): [1., 3., 0.01, 0.4, 0.02], 

54 action.size_column.format(axis="y"): [0.2, 6, 0.01, 0.2, 0.01], 

55 } 

56 model = action.model_flux_name 

57 factors = np.array([1.01, 1.12, 0.995, 0.998, 1.6]) 

58 fluxes = np.array([1.5e3, 2.5e3, 6.8e3, 3.4e3, 5.5e3]) 

59 for column_flux, column_flux_err, factors in ( 

60 (action.model_column_flux, action.model_column_flux_err, factors), 

61 (action.psf_column_flux, action.psf_column_flux_err, None), 

62 ): 

63 for idx, band in enumerate(action.bands): 

64 flux = np.sqrt(idx + 1.)*fluxes 

65 if factors is not None: 

66 flux *= factors 

67 data[column_flux.format(band=band, model=model)] = flux 

68 data[column_flux_err.format(band=band, model=model)] = np.sqrt(flux) 

69 self.data = Table(data) 

70 

71 def testExtendednessColumnAction(self): 

72 action = self.action 

73 with pytest.raises(ValueError): 

74 action.validate() 

75 action.bands_combined = {"gri": "g,r,i"} 

76 action.validate() 

77 schema = action.getInputSchema() 

78 n_values = len(self.data[schema[0][0]]) 

79 assert all([len(self.data[col]) == n_values for col, _ in schema[1:]]) 

80 

81 result = self.action(self.data) 

82 columns_expected = [ 

83 action.output_column.format(band=band) 

84 for band in list(action.bands) + list(action.bands_combined.keys()) 

85 ] 

86 assert list(result.keys()) == columns_expected 

87 

88 for column, values in result.items(): 

89 assert len(values) == n_values 

90 assert all((values >= 0) & (values <= 1)) 

91 

92 

93class ConsolidateVisitSummaryTestCase(lsst.utils.tests.TestCase): 

94 """Test that ConsolidateVisitSummaryTask makes the same row from a legacy 

95 Exposure and from the equivalent lsst.images.VisitImage. 

96 """ 

97 

98 def setUp(self): 

99 instrument = "testCam" 

100 self.visit = 1 

101 self.band = "r" 

102 physical_filter = "r_test" 

103 

104 # lsst.images needs raw amplifier geometry and a FIELD_ANGLE 

105 # transform, which the TestDataset detector does not have. 

106 detector = list(CameraWrapper().camera)[0] 

107 dataset = lsst.meas.base.tests.TestDataset(detector.getBBox()) 

108 dataset.addSource(instFlux=1e4, centroid=lsst.geom.Point2D(50, 60)) 

109 exposure, _ = dataset.realize(noise=10.0, schema=dataset.makeMinimalSchema()) 

110 exposure.setDetector(detector) 

111 exposure.info.setVisitInfo(makeTestVisitInfo(id=self.visit).copyWith(exposureTime=30.0)) 

112 exposure.setFilter(afwImage.FilterLabel(band=self.band, physical=physical_filter)) 

113 summaryStats = afwImage.ExposureSummaryStats() 

114 summaryStats.psfSigma = 2.5 

115 summaryStats.zeroPoint = 31.4 

116 exposure.info.setSummaryStats(summaryStats) 

117 bbox = lsst.geom.Box2D(exposure.getBBox()) 

118 exposure.info.setValidPolygon(afwGeom.Polygon( 

119 [bbox.getMin(), lsst.geom.Point2D(bbox.maxX, bbox.minY), lsst.geom.Point2D(bbox.minX, bbox.maxY)] 

120 )) 

121 self.exposure = exposure 

122 

123 self.repo_path = tempfile.TemporaryDirectory(ignore_cleanup_errors=True) 

124 self.addCleanup(self.repo_path.cleanup) 

125 # A file datastore, so that reads go through the lsst.images 

126 # formatter's component support. 

127 config = lsst.daf.butler.Config() 

128 config["datastore", "cls"] = "lsst.daf.butler.datastores.fileDatastore.FileDatastore" 

129 self.repo = butlerTests.makeTestRepo(self.repo_path.name, config=config) 

130 self.enterContext(self.repo) 

131 instrumentRecord = self.repo.dimensions["instrument"].RecordClass( 

132 name=instrument, visit_max=1e6, exposure_max=1e6, detector_max=128, 

133 class_name="lsst.obs.base.instrument_tests.DummyCam", 

134 ) 

135 self.repo.registry.syncDimensionData("instrument", instrumentRecord) 

136 butlerTests.addDataIdValue(self.repo, "physical_filter", physical_filter, band=self.band) 

137 butlerTests.addDataIdValue(self.repo, "detector", detector.getId()) 

138 butlerTests.addDataIdValue(self.repo, "visit", self.visit, physical_filter=physical_filter) 

139 self.dataId = {"instrument": instrument, "visit": self.visit, "detector": detector.getId()} 

140 dimensions = {"instrument", "visit", "detector"} 

141 butlerTests.addDatasetType(self.repo, "legacy_image", dimensions, "ExposureF") 

142 butlerTests.addDatasetType(self.repo, "future_image", dimensions, "VisitImage") 

143 self.butler = butlerTests.makeTestCollection(self.repo, uniqueId=self.id()) 

144 self.enterContext(self.butler) 

145 

146 def _run(self, datasetType, image, input_image_type): 

147 """Put one image in the butler and consolidate it into a visit 

148 summary. 

149 """ 

150 self.butler.put(image, datasetType, self.dataId) 

151 ref = self.butler.find_dataset(datasetType, self.dataId, dimension_records=True) 

152 config = ConsolidateVisitSummaryTask.ConfigClass() 

153 config.do_refit_pointing = False 

154 config.do_write_visit_geometry = False 

155 config.input_image_type = input_image_type 

156 task = ConsolidateVisitSummaryTask(config=config) 

157 return task.run(visit=self.visit, handles=[self.butler.getDeferred(ref)]).visitSummary 

158 

159 def _checkFutureMatchesLegacy(self, unit): 

160 """Check the future-mode row against the legacy-mode row. 

161 

162 Parameters 

163 ---------- 

164 unit : `astropy.units.Unit` 

165 Units of the image pixels. 

166 

167 Returns 

168 ------- 

169 visitImage : `lsst.images.VisitImage` 

170 The converted image the future-mode row was made from. 

171 """ 

172 visitImage = lsst.images.VisitImage.from_legacy( 

173 self.exposure, exposure_record=make_exposure_record(self.exposure), unit=unit 

174 ) 

175 # The afw GaussianPsf from TestDataset cannot be serialized, and this 

176 # task does not read the PSF. 

177 visitImage.psf = lsst.images.psfs.GaussianPointSpreadFunction( 

178 2.0, stamp_size=21, bounds=visitImage.bbox 

179 ) 

180 legacy = self._run("legacy_image", self.exposure, "legacy")[0] 

181 future = self._run("future_image", visitImage, "future")[0] 

182 

183 self.assertEqual(future.getId(), legacy.getId()) 

184 self.assertEqual(future["visit"], legacy["visit"]) 

185 self.assertEqual(future["physical_filter"], legacy["physical_filter"]) 

186 self.assertEqual(future["band"], legacy["band"]) 

187 self.assertEqual(future.getBBox(), legacy.getBBox()) 

188 self.assertEqual(future.getValidPolygon(), legacy.getValidPolygon()) 

189 self.assertWcsAlmostEqualOverBBox(future.getWcs(), legacy.getWcs(), legacy.getBBox()) 

190 self.assertFloatsAlmostEqual( 

191 future.getPhotoCalib().getCalibrationMean(), 

192 legacy.getPhotoCalib().getCalibrationMean(), 

193 rtol=1e-5, 

194 ) 

195 self.assertEqual(future.getVisitInfo().id, legacy.getVisitInfo().id) 

196 self.assertEqual(future.getVisitInfo().exposureTime, legacy.getVisitInfo().exposureTime) 

197 self.assertEqual(future["psfSigma"], legacy["psfSigma"]) 

198 self.assertEqual(future["zeroPoint"], legacy["zeroPoint"]) 

199 return visitImage 

200 

201 def testCalibratedImage(self): 

202 """Test an image calibrated to nJy, as calibrateImage writes.""" 

203 self.exposure.setPhotoCalib(afwImage.PhotoCalib(1.0)) 

204 visitImage = self._checkFutureMatchesLegacy(u.nJy) 

205 self.assertIsNone(visitImage.photometric_scaling) 

206 

207 def testUncalibratedImage(self): 

208 """Test an image in instrumental units with a photometric scaling.""" 

209 visitImage = self._checkFutureMatchesLegacy(u.electron) 

210 self.assertIsNotNone(visitImage.photometric_scaling) 

211 

212 def testFullRequiresLegacy(self): 

213 """Test that full=True is rejected for VisitImage inputs.""" 

214 config = ConsolidateVisitSummaryTask.ConfigClass() 

215 config.input_image_type = "future" 

216 config.full = True 

217 with self.assertRaises(ValueError): 

218 config.validate() 

219 

220 

221class MemoryTester(lsst.utils.tests.MemoryTestCase): 

222 pass 

223 

224 

225def setup_module(module): 

226 lsst.utils.tests.init() 

227 

228 

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

230 lsst.utils.tests.init() 

231 unittest.main()