Coverage for tests / test_verifyStats.py: 15%
173 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-21 10:56 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-21 10: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/>.
22import numpy as np
23import unittest
25import lsst.utils.tests
26import lsst.ip.isr.isrMock as isrMock
27import lsst.cp.verify as cpVerify
28import lsst.ip.isr.isrFunctions as isrFunctions
31def updateMockExp(exposure, addCR=True):
32 """Update an exposure with a mask and variance plane.
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
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)
52class ToySubClass(cpVerify.CpVerifyStatsTask):
53 """The CpVerifyStatsTask requires an implentation of verify.
54 """
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
61 return verifiedStats, successValue
64class VerifyStatsTestCase(lsst.utils.tests.TestCase):
65 """Unit test for stats code.
66 """
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 }
77 updateMockExp(self.inputExp)
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)
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)
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)
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)
119 results = task.run(self.inputExp, camera=self.camera, dimensions=self.dimensions)
120 resultStats = results.outputStats
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)
127 self.assertTrue(resultStats['VERIFY']['A REAL TEST'])
128 self.assertFalse(resultStats['VERIFY']['A BAD TEST'])
130 self.assertTrue(resultStats['SUCCESS'])
133class VerifyBiasTestCase(lsst.utils.tests.TestCase):
134 """Unit test for stats code - bias cases."""
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()
143 config.rngSeed = 54321
144 fakeBias = isrMock.BiasMock(config=config).run()
146 self.inputExp = biasExposure.clone()
147 mi = self.inputExp.getMaskedImage()
148 mi.scaledMinus(1.0, fakeBias.getMaskedImage())
149 updateMockExp(self.inputExp)
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 }
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
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
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)
182 self.assertIn(biasStats['SUCCESS'], [True, False])
185class VerifyDarkTestCase(lsst.utils.tests.TestCase):
186 """Unit test for stats code - dark cases.
187 """
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()
196 config.rngSeed = 54321
197 fakeDark = isrMock.DarkMock(config=config).run()
199 self.inputExp = darkExposure.clone()
200 mi = self.inputExp.getMaskedImage()
201 mi.scaledMinus(1.0, fakeDark.getMaskedImage())
202 updateMockExp(self.inputExp)
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 }
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
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
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)
235 self.assertIn(darkStats['SUCCESS'], [True, False])
238class VerifyDefectsTestCase(lsst.utils.tests.TestCase):
239 """Unit test for stats code - defect cases."""
241 defectFlux = 100000 # Flux to use for simulated defect.
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
258 config.skyLevel = 1000
259 config.rngSeed = 12345
260 self.inputExp = isrMock.IsrMock(config=config).run()
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
267 updateMockExp(self.inputExp, addCR=False)
269 self.inputExp.getMask().getArray()[0, 0] = 1
270 self.inputExp.getMask().getArray()[40, 50] = 1
271 self.inputExp.getMask().getArray()[75, 50] = 1
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 }
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.
291 # With defects applied
292 inputCatalogMock = np.arange(1, 100)
293 # Without defects applied
294 uncorrectedCatalogMock = np.arange(1, 200)
296 task = cpVerify.CpVerifyDefectsTask(config=config)
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
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)
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)
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)
323 self.assertIn(defectStats['SUCCESS'], [True, False])
326class MemoryTester(lsst.utils.tests.MemoryTestCase):
327 pass
330def setup_module(module):
331 lsst.utils.tests.init()
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()