Coverage for tests/test_psf_trampoline.py: 29%

143 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-22 03:22 -0800

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(self.getAveragePosition()) 

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(pgp.getAveragePosition()), 

128 gp.computeImage(gp.getAveragePosition()) 

129 ) 

130 self.assertImagesAlmostEqual( 

131 pgp.computeKernelImage(pgp.getAveragePosition()), 

132 gp.computeKernelImage(gp.getAveragePosition()) 

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, pgp.getAveragePosition()), 

140 gp.computeApertureFlux(r, gp.getAveragePosition()) 

141 ) 

142 

143 def testPeak(self): 

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

145 self.assertAlmostEqual( 

146 pgp.computePeak(pgp.getAveragePosition()), 

147 gp.computePeak(gp.getAveragePosition()) 

148 ) 

149 

150 def testBBox(self): 

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

152 self.assertEqual( 

153 pgp.computeBBox(pgp.getAveragePosition()), 

154 gp.computeBBox(gp.getAveragePosition()) 

155 ) 

156 self.assertEqual( 

157 pgp.computeKernelBBox(pgp.getAveragePosition()), 

158 gp.computeKernelBBox(gp.getAveragePosition()), 

159 ) 

160 self.assertEqual( 

161 pgp.computeImageBBox(pgp.getAveragePosition()), 

162 gp.computeImageBBox(gp.getAveragePosition()), 

163 ) 

164 self.assertEqual( 

165 pgp.computeImage(pgp.getAveragePosition()).getBBox(), 

166 pgp.computeImageBBox(pgp.getAveragePosition()) 

167 ) 

168 self.assertEqual( 

169 pgp.computeKernelImage(pgp.getAveragePosition()).getBBox(), 

170 pgp.computeKernelBBox(pgp.getAveragePosition()) 

171 ) 

172 

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 ) 

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(rpgp.getAveragePosition()), 

190 rgp.computeImage(rgp.getAveragePosition()) 

191 ) 

192 self.assertImagesAlmostEqual( 

193 rpgp2.computeImage(rpgp2.getAveragePosition()), 

194 rgp.computeImage(rgp.getAveragePosition()) 

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(pgp.getAveragePosition()), 

214 p.computeImage(p.getAveragePosition()) 

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 __test__ = False # Stop Pytest from trying to parse as a TestCase 

234 

235 def __init__(self, isFixed): 

236 Psf.__init__(self, isFixed=isFixed) 

237 

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 

249 

250 

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

252 def setUp(self): 

253 self.fixedPsf = TestPsf(isFixed=True) 

254 self.floatPsf = TestPsf(isFixed=False) 

255 

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) 

262 

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) 

273 

274 

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

276 pass 

277 

278 

279def setup_module(module): 

280 lsst.utils.tests.init() 

281 

282 

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