Coverage for tests/test_psf_trampoline.py: 33%

Shortcuts 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

142 statements  

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/>. 

21 

22import unittest 

23from copy import deepcopy 

24import pickle 

25 

26import numpy as np 

27 

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 

35 

36 

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") 

50 

51 def __init__(self, width, height, sigma): 

52 Psf.__init__(self, isFixed=True) 

53 self.dimensions = Extent2I(width, height) 

54 self.sigma = sigma 

55 

56 # "public" virtual overrides 

57 def __deepcopy__(self, memo=None): 

58 return PyGaussianPsf(self.dimensions.x, self.dimensions.y, self.sigma) 

59 

60 def resized(self, width, height): 

61 return PyGaussianPsf(width, height, self.sigma) 

62 

63 def isPersistable(self): 

64 return True 

65 

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 

75 

76 def _doComputeBBox(self, position=None, color=None): 

77 return Box2I(Point2I(-self.dimensions/2), self.dimensions) 

78 

79 def _doComputeShape(self, position=None, color=None): 

80 return Quadrupole(self.sigma**2, self.sigma**2, 0.0) 

81 

82 def _doComputeApertureFlux(self, radius, position=None, color=None): 

83 return 1 - np.exp(-0.5*(radius/self.sigma)**2) 

84 

85 def _getPersistenceName(self): 

86 return "PyGaussianPsf" 

87 

88 def _getPythonModule(self): 

89 return __name__ 

90 

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)) 

97 

98 @staticmethod 

99 def _read(pkl): 

100 dimensions, sigma = pickle.loads(pkl) 

101 return PyGaussianPsf(dimensions.x, dimensions.y, sigma) 

102 

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 

110 

111 

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)) 

123 

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 ) 

134 

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 ) 

142 

143 def testPeak(self): 

144 for pgp, gp in zip(self.pgps, self.gps): 

145 self.assertAlmostEqual( 

146 pgp.computePeak(), 

147 gp.computePeak() 

148 ) 

149 

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 ) 

172 

173 def testShape(self): 

174 for pgp, gp in zip(self.pgps, self.gps): 

175 self.assertAlmostEqual( 

176 pgp.computeShape(), 

177 gp.computeShape() 

178 ) 

179 

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 ) 

196 

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() 

209 

210 for p in [p1, p2, p3, p4]: 

211 self.assertIsNot(pgp, p) 

212 self.assertImagesEqual( 

213 pgp.computeImage(), 

214 p.computeImage() 

215 ) 

216 

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()) 

227 

228 

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 def __init__(self, isFixed): 

234 Psf.__init__(self, isFixed=isFixed) 

235 

236 def _doComputeKernelImage(self, position=None, color=None): 

237 bbox = Box2I(Point2I(-3, -3), Extent2I(7, 7)) 

238 img = Image(bbox, dtype=np.float64) 

239 x, y = np.ogrid[bbox.minY:bbox.maxY+1, bbox.minX:bbox.maxX+1] 

240 rsqr = x**2 + y**2 

241 if position.x >= 0.0: 

242 img.array[:] = np.exp(-0.5*rsqr) 

243 else: 

244 img.array[:] = np.exp(-0.5*rsqr/4) 

245 img.array /= np.sum(img.array) 

246 return img 

247 

248 

249class FixedPsfTestSuite(lsst.utils.tests.TestCase): 

250 def setUp(self): 

251 self.fixedPsf = TestPsf(isFixed=True) 

252 self.floatPsf = TestPsf(isFixed=False) 

253 

254 def testFloat(self): 

255 pos1 = Point2D(1.0, 1.0) 

256 pos2 = Point2D(-1.0, -1.0) 

257 img1 = self.floatPsf.computeKernelImage(pos1) 

258 img2 = self.floatPsf.computeKernelImage(pos2) 

259 self.assertFloatsNotEqual(img1.array, img2.array) 

260 

261 def testFixed(self): 

262 pos1 = Point2D(1.0, 1.0) 

263 pos2 = Point2D(-1.0, -1.0) 

264 img1 = self.fixedPsf.computeKernelImage(pos1) 

265 # Although _doComputeKernelImage would return a different image here due 

266 # do the difference between pos1 and pos2, for the fixed Psf, the 

267 # caching mechanism intercepts instead and _doComputeKernelImage is 

268 # never called with position=pos2. So img1 == img2. 

269 img2 = self.fixedPsf.computeKernelImage(pos2) 

270 self.assertFloatsEqual(img1.array, img2.array) 

271 

272 

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

274 pass 

275 

276 

277def setup_module(module): 

278 lsst.utils.tests.init() 

279 

280 

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

282 lsst.utils.tests.init() 

283 unittest.main()