Coverage for tests/test_psf_trampoline.py: 33%
143 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-15 02:45 -0700
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-15 02:45 -0700
1# This file is part of afw.
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 unittest
23from copy import deepcopy
24import pickle
26import numpy as np
28import lsst.utils.tests
29from lsst.afw.typehandling import StorableHelperFactory
30from lsst.afw.detection import Psf, GaussianPsf
31from lsst.afw.image import Image, ExposureF
32from lsst.geom import Box2I, Extent2I, Point2I, Point2D
33from lsst.afw.geom.ellipses import Quadrupole
34import testPsfTrampolineLib as cppLib
37# Subclass Psf in python. Main tests here are that python virtual methods get
38# resolved by trampoline class. The test suite below calls python compute*
39# methods which are implemented in c++ to call the _doCompute* methods defined
40# in the PyGaussianPsf class. We also test persistence and associated
41# overloads.
42class PyGaussianPsf(Psf):
43 # We need to ensure a c++ StorableHelperFactory is constructed and available
44 # before any unpersists of this class. Placing this "private" class
45 # attribute here accomplishes that. Note the unusual use of `__name__` for
46 # the module name, which is appropriate here where the class is defined in
47 # the test suite. In production code, this might be something like
48 # `lsst.meas.extensions.piff`
49 _factory = StorableHelperFactory(__name__, "PyGaussianPsf")
51 def __init__(self, width, height, sigma):
52 Psf.__init__(self, isFixed=True)
53 self.dimensions = Extent2I(width, height)
54 self.sigma = sigma
56 # "public" virtual overrides
57 def __deepcopy__(self, memo=None):
58 return PyGaussianPsf(self.dimensions.x, self.dimensions.y, self.sigma)
60 def resized(self, width, height):
61 return PyGaussianPsf(width, height, self.sigma)
63 def isPersistable(self):
64 return True
66 # "private" virtual overrides are underscored by convention
67 def _doComputeKernelImage(self, position=None, color=None):
68 bbox = self.computeBBox()
69 img = Image(bbox, dtype=np.float64)
70 x, y = np.ogrid[bbox.minY:bbox.maxY+1, bbox.minX:bbox.maxX+1]
71 rsqr = x**2 + y**2
72 img.array[:] = np.exp(-0.5*rsqr/self.sigma**2)
73 img.array /= np.sum(img.array)
74 return img
76 def _doComputeBBox(self, position=None, color=None):
77 return Box2I(Point2I(-self.dimensions/2), self.dimensions)
79 def _doComputeShape(self, position=None, color=None):
80 return Quadrupole(self.sigma**2, self.sigma**2, 0.0)
82 def _doComputeApertureFlux(self, radius, position=None, color=None):
83 return 1 - np.exp(-0.5*(radius/self.sigma)**2)
85 def _getPersistenceName(self):
86 return "PyGaussianPsf"
88 def _getPythonModule(self):
89 return __name__
91 # _write and _read are not ordinary python overrides of the c++ Psf methods,
92 # since the argument types required by the c++ methods are not available in
93 # python. Instead, we create methods that opaquely persist/unpersist
94 # to/from a string via pickle.
95 def _write(self):
96 return pickle.dumps((self.dimensions, self.sigma))
98 @staticmethod
99 def _read(pkl):
100 dimensions, sigma = pickle.loads(pkl)
101 return PyGaussianPsf(dimensions.x, dimensions.y, sigma)
103 def __eq__(self, rhs):
104 if isinstance(rhs, PyGaussianPsf):
105 return (
106 self.dimensions == rhs.dimensions
107 and self.sigma == rhs.sigma
108 )
109 return False
112class PsfTrampolineTestSuite(lsst.utils.tests.TestCase):
113 def setUp(self):
114 self.pgps = []
115 self.gps = []
116 for width, height, sigma in [
117 (5, 5, 1.1),
118 (5, 3, 1.2),
119 (7, 7, 1.3)
120 ]:
121 self.pgps.append(PyGaussianPsf(width, height, sigma))
122 self.gps.append(GaussianPsf(width, height, sigma))
124 def testImages(self):
125 for pgp, gp in zip(self.pgps, self.gps):
126 self.assertImagesAlmostEqual(
127 pgp.computeImage(),
128 gp.computeImage()
129 )
130 self.assertImagesAlmostEqual(
131 pgp.computeKernelImage(),
132 gp.computeKernelImage()
133 )
135 def testApertureFlux(self):
136 for pgp, gp in zip(self.pgps, self.gps):
137 for r in [0.1, 0.2, 0.3]:
138 self.assertAlmostEqual(
139 pgp.computeApertureFlux(r),
140 gp.computeApertureFlux(r)
141 )
143 def testPeak(self):
144 for pgp, gp in zip(self.pgps, self.gps):
145 self.assertAlmostEqual(
146 pgp.computePeak(),
147 gp.computePeak()
148 )
150 def testBBox(self):
151 for pgp, gp in zip(self.pgps, self.gps):
152 self.assertEqual(
153 pgp.computeBBox(),
154 gp.computeBBox()
155 )
156 self.assertEqual(
157 pgp.computeKernelBBox(),
158 gp.computeKernelBBox(),
159 )
160 self.assertEqual(
161 pgp.computeImageBBox(),
162 gp.computeImageBBox(),
163 )
164 self.assertEqual(
165 pgp.computeImage().getBBox(),
166 pgp.computeImageBBox()
167 )
168 self.assertEqual(
169 pgp.computeKernelImage().getBBox(),
170 pgp.computeKernelBBox()
171 )
173 def testShape(self):
174 for pgp, gp in zip(self.pgps, self.gps):
175 self.assertAlmostEqual(
176 pgp.computeShape(pgp.getAveragePosition()),
177 gp.computeShape(gp.getAveragePosition())
178 )
180 def testResized(self):
181 for pgp, gp in zip(self.pgps, self.gps):
182 width, height = pgp.dimensions
183 rpgp = pgp.resized(width+2, height+4)
184 # cppLib.resizedPsf calls Psf::resized, which redirects to
185 # PyGaussianPsf.resized above
186 rpgp2 = cppLib.resizedPsf(pgp, width+2, height+4)
187 rgp = gp.resized(width+2, height+4)
188 self.assertImagesAlmostEqual(
189 rpgp.computeImage(),
190 rgp.computeImage()
191 )
192 self.assertImagesAlmostEqual(
193 rpgp2.computeImage(),
194 rgp.computeImage()
195 )
197 def testClone(self):
198 """Test different ways of invoking PyGaussianPsf.__deepcopy__
199 """
200 for pgp in self.pgps:
201 # directly
202 p1 = deepcopy(pgp)
203 # cppLib::clonedPsf -> Psf::clone
204 p2 = cppLib.clonedPsf(pgp)
205 # cppLib::clonedStorablePsf -> Psf::cloneStorable
206 p3 = cppLib.clonedStorablePsf(pgp)
207 # Psf::clone()
208 p4 = pgp.clone()
210 for p in [p1, p2, p3, p4]:
211 self.assertIsNot(pgp, p)
212 self.assertImagesEqual(
213 pgp.computeImage(),
214 p.computeImage()
215 )
217 def testPersistence(self):
218 for pgp in self.pgps:
219 assert cppLib.isPersistable(pgp)
220 im = ExposureF(10, 10)
221 im.setPsf(pgp)
222 self.assertEqual(im.getPsf(), pgp)
223 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile:
224 im.writeFits(tmpFile)
225 newIm = ExposureF(tmpFile)
226 self.assertEqual(newIm.getPsf(), im.getPsf())
229# Psf with position-dependent image, but nonetheless may use isFixed=True.
230# When isFixed=True, first image returned is cached for all subsequent image
231# queries
232class TestPsf(Psf):
233 __test__ = False # Stop Pytest from trying to parse as a TestCase
235 def __init__(self, isFixed):
236 Psf.__init__(self, isFixed=isFixed)
238 def _doComputeKernelImage(self, position=None, color=None):
239 bbox = Box2I(Point2I(-3, -3), Extent2I(7, 7))
240 img = Image(bbox, dtype=np.float64)
241 x, y = np.ogrid[bbox.minY:bbox.maxY+1, bbox.minX:bbox.maxX+1]
242 rsqr = x**2 + y**2
243 if position.x >= 0.0:
244 img.array[:] = np.exp(-0.5*rsqr)
245 else:
246 img.array[:] = np.exp(-0.5*rsqr/4)
247 img.array /= np.sum(img.array)
248 return img
251class FixedPsfTestSuite(lsst.utils.tests.TestCase):
252 def setUp(self):
253 self.fixedPsf = TestPsf(isFixed=True)
254 self.floatPsf = TestPsf(isFixed=False)
256 def testFloat(self):
257 pos1 = Point2D(1.0, 1.0)
258 pos2 = Point2D(-1.0, -1.0)
259 img1 = self.floatPsf.computeKernelImage(pos1)
260 img2 = self.floatPsf.computeKernelImage(pos2)
261 self.assertFloatsNotEqual(img1.array, img2.array)
263 def testFixed(self):
264 pos1 = Point2D(1.0, 1.0)
265 pos2 = Point2D(-1.0, -1.0)
266 img1 = self.fixedPsf.computeKernelImage(pos1)
267 # Although _doComputeKernelImage would return a different image here due
268 # do the difference between pos1 and pos2, for the fixed Psf, the
269 # caching mechanism intercepts instead and _doComputeKernelImage is
270 # never called with position=pos2. So img1 == img2.
271 img2 = self.fixedPsf.computeKernelImage(pos2)
272 self.assertFloatsEqual(img1.array, img2.array)
275class MemoryTester(lsst.utils.tests.MemoryTestCase):
276 pass
279def setup_module(module):
280 lsst.utils.tests.init()
283if __name__ == "__main__": 283 ↛ 284line 283 didn't jump to line 284, because the condition on line 283 was never true
284 lsst.utils.tests.init()
285 unittest.main()