Hide keyboard shortcuts

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

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 

157 def testShape(self): 

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

159 self.assertAlmostEqual( 

160 pgp.computeShape(), 

161 gp.computeShape() 

162 ) 

163 

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 ) 

180 

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

193 

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

195 self.assertIsNot(pgp, p) 

196 self.assertImagesEqual( 

197 pgp.computeImage(), 

198 p.computeImage() 

199 ) 

200 

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

211 

212 

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) 

219 

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 

231 

232 

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

234 def setUp(self): 

235 self.fixedPsf = TestPsf(isFixed=True) 

236 self.floatPsf = TestPsf(isFixed=False) 

237 

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) 

244 

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) 

255 

256 

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

258 pass 

259 

260 

261def setup_module(module): 

262 lsst.utils.tests.init() 

263 

264 

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