Coverage for tests / test_verifyStats.py: 15%

173 statements  

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

1# This file is part of cp_verify. 

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 numpy as np 

23import unittest 

24 

25import lsst.utils.tests 

26import lsst.ip.isr.isrMock as isrMock 

27import lsst.cp.verify as cpVerify 

28import lsst.ip.isr.isrFunctions as isrFunctions 

29 

30 

31def updateMockExp(exposure, addCR=True): 

32 """Update an exposure with a mask and variance plane. 

33 

34 Parameters 

35 ---------- 

36 exposure : `lsst.afw.image.Exposure` 

37 Exposure to be modified in place. 

38 addCR : `bool` 

39 Whether a known cosmic ray should be added to ``exposure``. 

40 """ 

41 if addCR: 

42 # Add a cosmic ray 

43 image = exposure.getImage() 

44 image.getArray()[50, 50] = 10000.0 

45 

46 # Set the mask and variance planes: 

47 mask = exposure.getMask() 

48 mask.getArray()[:, 10] = 1 

49 isrFunctions.updateVariance(exposure.getMaskedImage(), 1.0, 5.0) 

50 

51 

52class ToySubClass(cpVerify.CpVerifyStatsTask): 

53 """The CpVerifyStatsTask requires an implentation of verify. 

54 """ 

55 

56 def verify(self, inputExp, outputStats): 

57 # Docstring inherited from CpVerifyStatsTask.verify() 

58 verifiedStats = {'A REAL TEST': True, 'A BAD TEST': False} 

59 successValue = True 

60 

61 return verifiedStats, successValue 

62 

63 

64class VerifyStatsTestCase(lsst.utils.tests.TestCase): 

65 """Unit test for stats code. 

66 """ 

67 

68 def setUp(self): 

69 """Generate a mock exposure/camera to test.""" 

70 self.inputExp = isrMock.CalibratedRawMock().run() 

71 self.camera = isrMock.IsrMock().getCamera() 

72 self.dimensions = {'instrument': self.camera.getName(), 

73 'exposure': 1234, 

74 'detector': self.camera[10].getName(), 

75 } 

76 

77 updateMockExp(self.inputExp) 

78 

79 def test_failures(self): 

80 """Test that all the NotImplementedError methods fail correctly.""" 

81 results = None 

82 with self.assertRaises(NotImplementedError): 

83 # We have not implemented a verify method 

84 config = cpVerify.CpVerifyStatsConfig() 

85 config.numSigmaClip = 3.0 

86 task = cpVerify.CpVerifyStatsTask(config=config) 

87 results = task.run(self.inputExp, camera=self.camera, dimensions=self.dimensions) 

88 

89 # Or the catalog stats 

90 config.catalogStatKeywords = {'CAT_MEAN', 'MEDIAN'} 

91 task = cpVerify.CpVerifyStatsTask(config=config) 

92 results = task.run(self.inputExp, camera=self.camera, dimensions=self.dimensions) 

93 

94 # Or the detector stats 

95 config.catalogStatKeywords = {} 

96 config.detectorStatKeywords = {'DET_SIGMA', 'STDEV'} 

97 task = cpVerify.CpVerifyStatsTask(config=config) 

98 results = task.run(self.inputExp, camera=self.camera, dimensions=self.dimensions) 

99 self.assertIsNone(results) 

100 

101 def test_generic(self): 

102 """Test a subset of the output values to identify that the 

103 image stat methods haven't changed. 

104 """ 

105 config = cpVerify.CpVerifyStatsConfig() 

106 config.imageStatKeywords = {'MEAN': 'MEAN', 'MEDIAN': 'MEDIAN', 'CLIPPED': 'MEANCLIP', 

107 'SIGMA': 'STDEV'} 

108 config.unmaskedImageStatKeywords = {'un_MEAN': 'MEAN', 'un_MEDIAN': 'MEDIAN', 

109 'un_CLIPPED': 'MEANCLIP', 

110 'un_SIGMA': 'STDEV'} 

111 config.crImageStatKeywords = {'cr_MEAN': 'MEAN', 'cr_MEDIAN': 'MEDIAN', 'cr_CLIPPED': 'MEANCLIP', 

112 'cr_SIGMA': 'STDEV'} 

113 config.normImageStatKeywords = {'norm_MEAN': 'MEAN', 'norm_MEDIAN': 'MEDIAN', 

114 'norm_CLIPPED': 'MEANCLIP', 

115 'norm_SIGMA': 'STDEV'} 

116 config.numSigmaClip = 3.0 

117 task = ToySubClass(config=config) 

118 

119 results = task.run(self.inputExp, camera=self.camera, dimensions=self.dimensions) 

120 resultStats = results.outputStats 

121 

122 self.assertAlmostEqual(resultStats['AMP']['C:0,0']['MEAN'], 1506.06976, 4) 

123 self.assertAlmostEqual(resultStats['AMP']['C:0,0']['un_MEAN'], 1501.0299, 4) 

124 self.assertAlmostEqual(resultStats['AMP']['C:0,0']['norm_MEAN'], 301.213957, 4) 

125 self.assertAlmostEqual(resultStats['AMP']['C:0,0']['cr_MEAN'], 1504.2776, 4) 

126 

127 self.assertTrue(resultStats['VERIFY']['A REAL TEST']) 

128 self.assertFalse(resultStats['VERIFY']['A BAD TEST']) 

129 

130 self.assertTrue(resultStats['SUCCESS']) 

131 

132 

133class VerifyBiasTestCase(lsst.utils.tests.TestCase): 

134 """Unit test for stats code - bias cases.""" 

135 

136 def setUp(self): 

137 """Generate a mock exposure/camera to test.""" 

138 config = isrMock.IsrMockConfig() 

139 config.isTrimmed = True 

140 config.rngSeed = 12345 

141 biasExposure = isrMock.BiasMock(config=config).run() 

142 

143 config.rngSeed = 54321 

144 fakeBias = isrMock.BiasMock(config=config).run() 

145 

146 self.inputExp = biasExposure.clone() 

147 mi = self.inputExp.getMaskedImage() 

148 mi.scaledMinus(1.0, fakeBias.getMaskedImage()) 

149 updateMockExp(self.inputExp) 

150 

151 self.camera = isrMock.IsrMock().getCamera() 

152 detector = self.camera[20] 

153 self.inputExp.setDetector(detector) 

154 self.dimensions = {'instrument': self.camera.getName(), 

155 'exposure': 1234, 

156 'detector': detector.getName(), 

157 } 

158 

159 # This is here to test accessing metadata info from the 

160 # exposure header. 

161 md = self.inputExp.getMetadata() 

162 for amp in detector.getAmplifiers(): 

163 md[f"LSST ISR OVERSCAN RESIDUAL SERIAL STDEV {amp.getName()}"] = 4.25 

164 

165 def test_bias(self): 

166 """Test a subset of the output values to identify that the 

167 image stat methods haven't changed. 

168 """ 

169 config = cpVerify.CpVerifyBiasConfig() 

170 config.numSigmaClip = 3.0 

171 config.ampCornerBoxSize = 15 

172 task = cpVerify.CpVerifyBiasTask(config=config) 

173 results = task.run(self.inputExp, camera=self.camera, dimensions=self.dimensions) 

174 biasStats = results.outputStats 

175 

176 self.assertAlmostEqual(biasStats['AMP']['C:0,0']['MEAN'], 2.08672, 4) 

177 self.assertAlmostEqual(biasStats['AMP']['C:0,0']['NOISE'], 13.99547, 4) 

178 self.assertAlmostEqual(biasStats['AMP']['C:0,0']['CR_NOISE'], 14.10227, 4) 

179 # This order swap in intended. :sad-panda-emoji: 

180 self.assertAlmostEqual(biasStats['METADATA']['READ_NOISE_ADU']['C:0,0'], 4.25) 

181 

182 self.assertIn(biasStats['SUCCESS'], [True, False]) 

183 

184 

185class VerifyDarkTestCase(lsst.utils.tests.TestCase): 

186 """Unit test for stats code - dark cases. 

187 """ 

188 

189 def setUp(self): 

190 """Generate a mock exposure/camera to test.""" 

191 config = isrMock.IsrMockConfig() 

192 config.isTrimmed = True 

193 config.rngSeed = 12345 

194 darkExposure = isrMock.DarkMock(config=config).run() 

195 

196 config.rngSeed = 54321 

197 fakeDark = isrMock.DarkMock(config=config).run() 

198 

199 self.inputExp = darkExposure.clone() 

200 mi = self.inputExp.getMaskedImage() 

201 mi.scaledMinus(1.0, fakeDark.getMaskedImage()) 

202 updateMockExp(self.inputExp) 

203 

204 self.camera = isrMock.IsrMock().getCamera() 

205 detector = self.camera[20] 

206 self.inputExp.setDetector(detector) 

207 self.dimensions = {'instrument': self.camera.getName(), 

208 'exposure': 1234, 

209 'detector': detector.getName(), 

210 } 

211 

212 # Populate the READ_NOISE (in ADU) into the exposure header 

213 md = self.inputExp.getMetadata() 

214 for amp in detector.getAmplifiers(): 

215 md[f"LSST ISR OVERSCAN RESIDUAL SERIAL STDEV {amp.getName()}"] = 5.24 

216 

217 def test_dark(self): 

218 """Test a subset of the output values to identify that the 

219 image stat methods haven't changed. 

220 """ 

221 config = cpVerify.CpVerifyDarkConfig() 

222 config.numSigmaClip = 3.0 

223 task = cpVerify.CpVerifyDarkTask(config=config) 

224 results = task.run(self.inputExp, 

225 camera=self.camera, 

226 dimensions=self.dimensions) 

227 darkStats = results.outputStats 

228 

229 self.assertAlmostEqual(darkStats['AMP']['C:0,0']['MEAN'], 2.0043, 4) 

230 self.assertAlmostEqual(darkStats['AMP']['C:0,0']['NOISE'], 3.12948, 4) 

231 self.assertAlmostEqual(darkStats['AMP']['C:0,0']['CR_NOISE'], 3.15946, 4) 

232 # This order swap in intended. :sad-panda-emoji: 

233 self.assertAlmostEqual(darkStats['METADATA']['READ_NOISE_ADU']['C:0,0'], 5.24) 

234 

235 self.assertIn(darkStats['SUCCESS'], [True, False]) 

236 

237 

238class VerifyDefectsTestCase(lsst.utils.tests.TestCase): 

239 """Unit test for stats code - defect cases.""" 

240 

241 defectFlux = 100000 # Flux to use for simulated defect. 

242 

243 def setUp(self): 

244 """Generate a mock exposure/camera to test.""" 

245 config = isrMock.IsrMockConfig() 

246 config.isTrimmed = True 

247 config.doGenerateImage = True 

248 config.doAddFringe = False 

249 config.doAddSource = False 

250 config.doAddSky = True 

251 config.doAddOverscan = False 

252 config.doAddCrosstalk = False 

253 config.doAddBias = False 

254 config.doAddDark = False 

255 config.doAddFlat = False 

256 config.doAddFringe = False 

257 

258 config.skyLevel = 1000 

259 config.rngSeed = 12345 

260 self.inputExp = isrMock.IsrMock(config=config).run() 

261 

262 # These are simulated defects 

263 self.inputExp.getImage().getArray()[0, 0] = -1.0 * self.defectFlux 

264 self.inputExp.getImage().getArray()[40, 50] = self.defectFlux 

265 self.inputExp.getImage().getArray()[75, 50] = np.nan 

266 

267 updateMockExp(self.inputExp, addCR=False) 

268 

269 self.inputExp.getMask().getArray()[0, 0] = 1 

270 self.inputExp.getMask().getArray()[40, 50] = 1 

271 self.inputExp.getMask().getArray()[75, 50] = 1 

272 

273 self.camera = isrMock.IsrMock().getCamera() 

274 self.dimensions = {'instrument': self.camera.getName(), 

275 'exposure': 1234, 

276 'visit': 1234, 

277 'detector': self.camera[10].getName(), 

278 } 

279 

280 def test_defects(self): 

281 """Test a subset of the output values to identify that the 

282 image stat methods haven't changed. 

283 """ 

284 config = cpVerify.CpVerifyDefectsConfig() 

285 config.numSigmaClip = 3.0 

286 # The catalog objects are `lsst.afw.table.SourceCatalog` 

287 # but the task catalog tests only check number of 

288 # detections before and after applying defects, so 

289 # arrays will do in this case. 

290 

291 # With defects applied 

292 inputCatalogMock = np.arange(1, 100) 

293 # Without defects applied 

294 uncorrectedCatalogMock = np.arange(1, 200) 

295 

296 task = cpVerify.CpVerifyDefectsTask(config=config) 

297 

298 # Also use the inputExp as uncorrectedExposure. 

299 results = task.run(self.inputExp, 

300 camera=self.camera, 

301 uncorrectedExp=self.inputExp, 

302 inputCatalog=inputCatalogMock, 

303 uncorrectedCatalog=uncorrectedCatalogMock, 

304 dimensions=self.dimensions) 

305 defectStats = results.outputStats 

306 

307 self.assertEqual(defectStats['AMP']['C:0,0']['DEFECT_PIXELS'], 53) 

308 self.assertEqual(defectStats['AMP']['C:0,0']['OUTLIERS'], 17) 

309 self.assertEqual(defectStats['AMP']['C:0,0']['STAT_OUTLIERS'], 3) 

310 self.assertAlmostEqual(defectStats['AMP']['C:0,0']['MEDIAN'], 999.466, 4) 

311 self.assertAlmostEqual(defectStats['AMP']['C:0,0']['STDEV'], 30.96303, 4) 

312 self.assertAlmostEqual(defectStats['AMP']['C:0,0']['MIN'], 881.56146, 4) 

313 self.assertAlmostEqual(defectStats['AMP']['C:0,0']['MAX'], 1124.19934, 4) 

314 

315 self.assertEqual(defectStats['AMP']['C:0,0']['UNMASKED_MIN'], -1.0 * self.defectFlux, 4) 

316 self.assertEqual(defectStats['AMP']['C:0,0']['UNMASKED_MAX'], self.defectFlux, 4) 

317 

318 self.assertEqual(defectStats['CATALOG']['NUM_OBJECTS_BEFORE'], 199) 

319 self.assertEqual(defectStats['CATALOG']['NUM_OBJECTS_AFTER'], 99) 

320 self.assertEqual(defectStats['DET']['NUM_COSMICS_BEFORE'], 0) 

321 self.assertEqual(defectStats['DET']['NUM_COSMICS_AFTER'], 0) 

322 

323 self.assertIn(defectStats['SUCCESS'], [True, False]) 

324 

325 

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

327 pass 

328 

329 

330def setup_module(module): 

331 lsst.utils.tests.init() 

332 

333 

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

335 lsst.utils.tests.init() 

336 unittest.main()