Coverage for tests/test_psf_trampoline.py : 30%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 )
157 def testShape(self):
158 for pgp, gp in zip(self.pgps, self.gps):
159 self.assertAlmostEqual(
160 pgp.computeShape(),
161 gp.computeShape()
162 )
164 def testResized(self):
165 for pgp, gp in zip(self.pgps, self.gps):
166 width, height = pgp.dimensions
167 rpgp = pgp.resized(width+2, height+4)
168 # cppLib.resizedPsf calls Psf::resized, which redirects to
169 # PyGaussianPsf.resized above
170 rpgp2 = cppLib.resizedPsf(pgp, width+2, height+4)
171 rgp = gp.resized(width+2, height+4)
172 self.assertImagesAlmostEqual(
173 rpgp.computeImage(),
174 rgp.computeImage()
175 )
176 self.assertImagesAlmostEqual(
177 rpgp2.computeImage(),
178 rgp.computeImage()
179 )
181 def testClone(self):
182 """Test different ways of invoking PyGaussianPsf.__deepcopy__
183 """
184 for pgp in self.pgps:
185 # directly
186 p1 = deepcopy(pgp)
187 # cppLib::clonedPsf -> Psf::clone
188 p2 = cppLib.clonedPsf(pgp)
189 # cppLib::clonedStorablePsf -> Psf::cloneStorable
190 p3 = cppLib.clonedStorablePsf(pgp)
191 # Psf::clone()
192 p4 = pgp.clone()
194 for p in [p1, p2, p3, p4]:
195 self.assertIsNot(pgp, p)
196 self.assertImagesEqual(
197 pgp.computeImage(),
198 p.computeImage()
199 )
201 def testPersistence(self):
202 for pgp in self.pgps:
203 assert cppLib.isPersistable(pgp)
204 im = ExposureF(10, 10)
205 im.setPsf(pgp)
206 self.assertEqual(im.getPsf(), pgp)
207 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile:
208 im.writeFits(tmpFile)
209 newIm = ExposureF(tmpFile)
210 self.assertEqual(newIm.getPsf(), im.getPsf())
213# Psf with position-dependent image, but nonetheless may use isFixed=True.
214# When isFixed=True, first image returned is cached for all subsequent image
215# queries
216class TestPsf(Psf):
217 def __init__(self, isFixed):
218 Psf.__init__(self, isFixed=isFixed)
220 def _doComputeKernelImage(self, position=None, color=None):
221 bbox = Box2I(Point2I(-3, -3), Extent2I(7, 7))
222 img = Image(bbox, dtype=np.float64)
223 x, y = np.ogrid[bbox.minY:bbox.maxY+1, bbox.minX:bbox.maxX+1]
224 rsqr = x**2 + y**2
225 if position.x >= 0.0:
226 img.array[:] = np.exp(-0.5*rsqr)
227 else:
228 img.array[:] = np.exp(-0.5*rsqr/4)
229 img.array /= np.sum(img.array)
230 return img
233class FixedPsfTestSuite(lsst.utils.tests.TestCase):
234 def setUp(self):
235 self.fixedPsf = TestPsf(isFixed=True)
236 self.floatPsf = TestPsf(isFixed=False)
238 def testFloat(self):
239 pos1 = Point2D(1.0, 1.0)
240 pos2 = Point2D(-1.0, -1.0)
241 img1 = self.floatPsf.computeKernelImage(pos1)
242 img2 = self.floatPsf.computeKernelImage(pos2)
243 self.assertFloatsNotEqual(img1.array, img2.array)
245 def testFixed(self):
246 pos1 = Point2D(1.0, 1.0)
247 pos2 = Point2D(-1.0, -1.0)
248 img1 = self.fixedPsf.computeKernelImage(pos1)
249 # Although _doComputeKernelImage would return a different image here due
250 # do the difference between pos1 and pos2, for the fixed Psf, the
251 # caching mechanism intercepts instead and _doComputeKernelImage is
252 # never called with position=pos2. So img1 == img2.
253 img2 = self.fixedPsf.computeKernelImage(pos2)
254 self.assertFloatsEqual(img1.array, img2.array)
257class MemoryTester(lsst.utils.tests.MemoryTestCase):
258 pass
261def setup_module(module):
262 lsst.utils.tests.init()
265if __name__ == "__main__": 265 ↛ 266line 265 didn't jump to line 266, because the condition on line 265 was never true
266 lsst.utils.tests.init()
267 unittest.main()