Coverage for tests/test_stitched_psf.py: 20%
115 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-30 03:57 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-30 03:57 -0700
1# This file is part of cell_coadds.
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 pickle
23import unittest
25import lsst.geom as geom
26import lsst.meas.base.tests
27import lsst.utils.tests
28import numpy as np
29from lsst.afw.detection import GaussianPsf
30from lsst.afw.image import ImageD
31from lsst.cell_coadds import GridContainer, StitchedPsf, UniformGrid
32from lsst.meas.algorithms import SingleGaussianPsf
33from lsst.skymap import Index2D
36class StitchedPsfTestCase(lsst.utils.tests.TestCase):
37 """Test the methods of StitchedPsf class."""
39 @classmethod
40 def setUpClass(cls) -> None: # noqa: D102
41 super().setUpClass()
43 shape = Index2D(x=3, y=2)
45 cls.psf_size = 15
46 cls.psf_sigmas = {
47 Index2D(x=0, y=0): 1.2,
48 Index2D(x=0, y=1): 0.7,
49 Index2D(x=1, y=0): 0.9,
50 Index2D(x=1, y=1): 1.1,
51 Index2D(x=2, y=0): 1.3,
52 Index2D(x=2, y=1): 0.8,
53 }
55 gc = GridContainer[ImageD](shape)
56 for key, sigma in cls.psf_sigmas.items():
57 # It does not matter where we compute the kernel image.
58 # Pick any point.
59 gc[key] = GaussianPsf(cls.psf_size, cls.psf_size, sigma).computeKernelImage(geom.Point2D(1, 1))
61 grid = UniformGrid(cell_size=geom.Extent2I(cls.psf_size, cls.psf_size), shape=shape)
63 cls.psf = StitchedPsf(gc, grid)
65 cls.test_positions = (
66 (geom.Point2D(5, 4), Index2D(x=0, y=0)), # inner point in lower left
67 (geom.Point2D(6, 24), Index2D(x=0, y=1)), # inner point in upper left
68 (geom.Point2D(23.2, 7.8), Index2D(x=1, y=0)), # inner point in lower middle
69 (geom.Point2D(21, 22), Index2D(x=1, y=1)), # inner point in upper middle
70 (geom.Point2D(37, 9.4), Index2D(x=2, y=0)), # inner point in lower right
71 (geom.Point2D(42, 24), Index2D(x=2, y=1)), # inner point in upper right
72 # Some points that lie on the border
73 (geom.Point2D(31, 24), Index2D(x=2, y=1)), # inner point in upper right
74 (geom.Point2D(44, 0), Index2D(x=2, y=0)), # inner point in lower right
75 (geom.Point2D(17, 16), Index2D(x=1, y=1)), # inner point in upper middle
76 (geom.Point2D(15, 8), Index2D(x=1, y=0)), # inner point in lower middle
77 (geom.Point2D(0, 29), Index2D(x=0, y=1)), # inner point in upper left
78 (geom.Point2D(0, 0), Index2D(x=0, y=0)), # inner point in lower left
79 )
81 def test_resized(self):
82 """Test that the resized method works as it should."""
83 original_bbox = self.psf.computeBBox(self.psf.getAveragePosition())
84 # Resizing to original size should leave everything unchanged.
85 psf = self.psf.resized(self.psf_size, self.psf_size)
86 self.assertEqual(psf.getAveragePosition(), self.psf.getAveragePosition())
87 self.assertEqual(
88 psf.computeBBox(psf.getAveragePosition()), self.psf.computeBBox(self.psf.getAveragePosition())
89 )
90 self.assertImagesEqual(
91 psf.computeKernelImage(psf.getAveragePosition()),
92 self.psf.computeKernelImage(self.psf.getAveragePosition()),
93 )
94 self.assertEqual(psf, self.psf)
96 # Resize to a rectangular postage stamp
97 psf = self.psf.resized(25, 21)
98 self.assertEqual(self.psf.computeBBox(self.psf.getAveragePosition()), original_bbox)
99 self.assertEqual(psf.computeBBox(psf.getAveragePosition()).getDimensions(), geom.Extent2I(25, 21))
100 self.assertEqual(
101 psf.computeBBox(psf.getAveragePosition()).getCenter(),
102 self.psf.computeBBox(self.psf.getAveragePosition()).getCenter(),
103 )
104 self.assertNotEqual(psf, self.psf)
106 # Test that resizing to even dimensions throws an error
107 with self.assertRaises(ValueError):
108 psf = self.psf.resized(26, 22) # even, even
109 with self.assertRaises(ValueError):
110 psf = self.psf.resized(25, 22) # odd, even
111 with self.assertRaises(ValueError):
112 psf = self.psf.resized(26, 21) # even, odd
114 def test_clone(self):
115 """Test that the clone method works."""
116 psf = self.psf
117 cloned = psf.clone()
118 # Check that the cloned version is an exact replica of the original.
119 # self.assertEqual(psf, cloned) # Should there be an __eq__ method?
120 self.assertEqual(psf.getAveragePosition(), cloned.getAveragePosition())
121 self.assertEqual(
122 psf.computeBBox(psf.getAveragePosition()), cloned.computeBBox(cloned.getAveragePosition())
123 )
124 self.assertImagesEqual(
125 psf.computeKernelImage(psf.getAveragePosition()),
126 cloned.computeKernelImage(cloned.getAveragePosition()),
127 )
128 self.assertEqual(psf, cloned)
129 psf = psf.resized(41, 41)
130 # Now that one of them has been resized, they should not be equal.
131 self.assertNotEqual(
132 psf.computeBBox(psf.getAveragePosition()), cloned.computeBBox(cloned.getAveragePosition())
133 )
134 with self.assertRaises(TypeError):
135 self.assertImagesEqual(
136 psf.computeKernelImage(psf.getAveragePosition()),
137 cloned.computeKernelImage(cloned.getAveragePosition()),
138 )
139 self.assertNotEqual(psf, cloned)
141 def test_computeKernelImage(self):
142 """Test the computeKernelImage method for a StitchedPsf object."""
143 stitched_psf = self.psf
144 psf_bbox = geom.Box2I(
145 geom.Point2I(-(self.psf_size // 2), -(self.psf_size // 2)),
146 geom.Extent2I(self.psf_size, self.psf_size),
147 )
149 for position, cell_index in self.test_positions:
150 image1 = stitched_psf.computeKernelImage(position)
151 image2 = SingleGaussianPsf(
152 self.psf_size, self.psf_size, self.psf_sigmas[cell_index]
153 ).computeKernelImage(position)
154 # Small differences may exist due to differences in evaluating
155 # GaussianPsf vs. SingleGaussianPsf
156 self.assertImagesAlmostEqual(image1, image2, atol=2e-16)
157 self.assertEqual(image1.getBBox(), psf_bbox)
159 def test_computeBBox(self):
160 """Test the computeBBox method for a StitchedPsf object."""
161 psf = self.psf
162 psf_bbox = geom.Box2I(
163 geom.Point2I(-(self.psf_size // 2), -(self.psf_size // 2)),
164 geom.Extent2I(self.psf_size, self.psf_size),
165 )
167 for position, _ in self.test_positions:
168 bbox = psf.computeBBox(position)
169 self.assertEqual(bbox, psf_bbox)
171 def test_computeShape(self):
172 """Test the results from the computeShape method on a StitchedPsf
173 object matches the true input.
174 """
175 stitched_psf = self.psf
176 for position, cell_index in self.test_positions:
177 psf_shape = stitched_psf.computeShape(position) # check we can compute shape
178 self.assertIsNot(psf_shape.getIxx(), np.nan)
179 self.assertIsNot(psf_shape.getIyy(), np.nan)
180 self.assertIsNot(psf_shape.getIxy(), np.nan)
182 # Moments measured from pixellated images are significantly
183 # underestimated for small PSFs.
184 if self.psf_sigmas[cell_index] >= 1.0:
185 self.assertAlmostEqual(psf_shape.getIxx(), self.psf_sigmas[cell_index] ** 2, delta=1e-3)
186 self.assertAlmostEqual(psf_shape.getIyy(), self.psf_sigmas[cell_index] ** 2, delta=1e-3)
187 self.assertAlmostEqual(psf_shape.getIxy(), 0.0)
189 def test_computeApertureFlux(self):
190 """Test that the results from the computeApertureFlux method on a
191 StitchedPsf object returns the analytical results for a Gaussian PSF.
192 """
193 stitched_psf = self.psf
194 for position, cell_index in self.test_positions:
195 flux1sigma = stitched_psf.computeApertureFlux(self.psf_sigmas[cell_index], position=position)
196 self.assertAlmostEqual(flux1sigma, 0.39, delta=5e-2)
198 flux3sigma = stitched_psf.computeApertureFlux(
199 3.0 * self.psf_sigmas[cell_index], position=position
200 )
201 self.assertAlmostEqual(flux3sigma, 0.97, delta=2e-2)
203 def test_computeImage(self):
204 """Test the computeImage method for a StitchedPsf object produces
205 the same result as that on GaussianPsf for Gaussian PSFs.
206 """
207 stitched_psf = self.psf
208 psf_extent = geom.Extent2I(self.psf_size, self.psf_size)
210 for position, cell_index in self.test_positions:
211 image1 = stitched_psf.computeImage(position)
212 image2 = GaussianPsf(self.psf_size, self.psf_size, self.psf_sigmas[cell_index]).computeImage(
213 position
214 )
215 self.assertImagesEqual(image1, image2)
216 self.assertEqual(image1.getBBox().getDimensions(), psf_extent)
218 def test_computeImage_computeKernelImage(self):
219 """Test that computeImage called at integer points gives the same
220 result as calling computeKernelImage.
221 """
222 stitched_psf = self.psf
223 for position, _cell_index in self.test_positions:
224 pos = geom.Point2D(geom.Point2I(position)) # round to integer
225 image1 = stitched_psf.computeKernelImage(pos)
226 image2 = stitched_psf.computeImage(pos)
227 self.assertImagesEqual(image1, image2)
229 def test_pickle(self):
230 """Test that StitchedPsf objects can be pickled and unpickled."""
231 self.assertTrue(self.psf.isPersistable())
232 stream = pickle.dumps(self.psf)
233 psf = pickle.loads(stream)
234 self.assertEqual(psf.getAveragePosition(), self.psf.getAveragePosition())
235 self.assertEqual(
236 psf.computeBBox(psf.getAveragePosition()), self.psf.computeBBox(self.psf.getAveragePosition())
237 )
238 self.assertImagesEqual(
239 psf.computeKernelImage(psf.getAveragePosition()),
240 self.psf.computeKernelImage(self.psf.getAveragePosition()),
241 )
242 self.assertEqual(psf, self.psf)
245class TestMemory(lsst.utils.tests.MemoryTestCase):
246 """Test for memory/resource leaks."""
249def setup_module(module): # noqa: D103
250 lsst.utils.tests.init()
253if __name__ == "__main__": 253 ↛ 254line 253 didn't jump to line 254, because the condition on line 253 was never true
254 lsst.utils.tests.init()
255 unittest.main()