Coverage for tests/test_PsfexPsf.py: 18%

166 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-27 02:27 -0700

1# 

2# LSST Data Management System 

3# 

4# Copyright 2008-2016 AURA/LSST. 

5# 

6# This product includes software developed by the 

7# LSST Project (http://www.lsst.org/). 

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 LSST License Statement and 

20# the GNU General Public License along with this program. If not, 

21# see <https://www.lsstcorp.org/LegalNotices/>. 

22# 

23import math 

24import numpy as np 

25import unittest 

26 

27import lsst.utils.tests 

28import lsst.afw.image as afwImage 

29import lsst.afw.detection as afwDetection 

30import lsst.afw.geom as afwGeom 

31import lsst.geom as geom 

32import lsst.afw.math as afwMath 

33import lsst.afw.table as afwTable 

34import lsst.daf.base as dafBase 

35import lsst.meas.algorithms as measAlg 

36from lsst.meas.base import SingleFrameMeasurementTask 

37# register the PSF determiner 

38import lsst.meas.extensions.psfex.psfexPsfDeterminer 

39assert lsst.meas.extensions.psfex.psfexPsfDeterminer # make pyflakes happy 

40 

41try: 

42 display 

43except NameError: 

44 display = False 

45else: 

46 import lsst.afw.display as afwDisplay 

47 afwDisplay.setDefaultMaskTransparency(75) 

48 

49 

50def psfVal(ix, iy, x, y, sigma1, sigma2, b): 

51 """Return the value at (ix, iy) of a double Gaussian 

52 (N(0, sigma1^2) + b*N(0, sigma2^2))/(1 + b) 

53 centered at (x, y) 

54 """ 

55 dx, dy = x - ix, y - iy 

56 theta = np.radians(30) 

57 ab = 1.0/0.75 # axis ratio 

58 c, s = np.cos(theta), np.sin(theta) 

59 u, v = c*dx - s*dy, s*dx + c*dy 

60 

61 return (math.exp(-0.5*(u**2 + (v*ab)**2)/sigma1**2) 

62 + b*math.exp(-0.5*(u**2 + (v*ab)**2)/sigma2**2))/(1 + b) 

63 

64 

65class SpatialModelPsfTestCase(unittest.TestCase): 

66 """A test case for SpatialModelPsf""" 

67 

68 def measure(self, footprintSet, exposure): 

69 """Measure a set of Footprints, returning a SourceCatalog""" 

70 catalog = afwTable.SourceCatalog(self.schema) 

71 if display: 

72 afwDisplay.Display(frame=0).mtv(exposure, title="Original") 

73 

74 footprintSet.makeSources(catalog) 

75 

76 self.measureSources.run(catalog, exposure) 

77 return catalog 

78 

79 def setUp(self): 

80 config = SingleFrameMeasurementTask.ConfigClass() 

81 config.slots.apFlux = 'base_CircularApertureFlux_12_0' 

82 self.schema = afwTable.SourceTable.makeMinimalSchema() 

83 

84 self.measureSources = SingleFrameMeasurementTask(self.schema, config=config) 

85 

86 width, height = 110, 301 

87 

88 self.mi = afwImage.MaskedImageF(geom.ExtentI(width, height)) 

89 self.mi.set(0) 

90 sd = 3 # standard deviation of image 

91 self.mi.getVariance().set(sd*sd) 

92 self.mi.getMask().addMaskPlane("DETECTED") 

93 

94 self.ksize = 31 # size of desired kernel 

95 

96 sigma1 = 1.75 

97 sigma2 = 2*sigma1 

98 

99 self.exposure = afwImage.makeExposure(self.mi) 

100 self.exposure.setPsf(measAlg.DoubleGaussianPsf(self.ksize, self.ksize, 

101 1.5*sigma1, 1, 0.1)) 

102 cdMatrix = np.array([1.0, 0.0, 0.0, 1.0]) 

103 cdMatrix.shape = (2, 2) 

104 wcs = afwGeom.makeSkyWcs(crpix=geom.PointD(0, 0), 

105 crval=geom.SpherePoint(0.0, 0.0, geom.degrees), 

106 cdMatrix=cdMatrix) 

107 self.exposure.setWcs(wcs) 

108 

109 # 

110 # Make a kernel with the exactly correct basis functions. 

111 # Useful for debugging 

112 # 

113 basisKernelList = [] 

114 for sigma in (sigma1, sigma2): 

115 basisKernel = afwMath.AnalyticKernel(self.ksize, self.ksize, 

116 afwMath.GaussianFunction2D(sigma, sigma)) 

117 basisImage = afwImage.ImageD(basisKernel.getDimensions()) 

118 basisKernel.computeImage(basisImage, True) 

119 basisImage /= np.sum(basisImage.getArray()) 

120 

121 if sigma == sigma1: 

122 basisImage0 = basisImage 

123 else: 

124 basisImage -= basisImage0 

125 

126 basisKernelList.append(afwMath.FixedKernel(basisImage)) 

127 

128 order = 1 # 1 => up to linear 

129 spFunc = afwMath.PolynomialFunction2D(order) 

130 

131 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc) 

132 exactKernel.setSpatialParameters([[1.0, 0, 0], 

133 [0.0, 0.5*1e-2, 0.2e-2]]) 

134 

135 rand = afwMath.Random() # make these tests repeatable by setting seed 

136 

137 addNoise = True 

138 

139 if addNoise: 

140 im = self.mi.getImage() 

141 afwMath.randomGaussianImage(im, rand) # N(0, 1) 

142 im *= sd # N(0, sd^2) 

143 del im 

144 

145 xarr, yarr = [], [] 

146 

147 for x, y in [(20, 20), (60, 20), 

148 (30, 35), 

149 (50, 50), 

150 (20, 90), (70, 160), (25, 265), (75, 275), (85, 30), 

151 (50, 120), (70, 80), 

152 (60, 210), (20, 210), 

153 ]: 

154 xarr.append(x) 

155 yarr.append(y) 

156 

157 for x, y in zip(xarr, yarr): 

158 dx = rand.uniform() - 0.5 # random (centered) offsets 

159 dy = rand.uniform() - 0.5 

160 

161 k = exactKernel.getSpatialFunction(1)(x, y) # functional variation of Kernel ... 

162 b = (k*sigma1**2/((1 - k)*sigma2**2)) # ... converted double Gaussian's "b" 

163 

164 # flux = 80000 - 20*x - 10*(y/float(height))**2 

165 flux = 80000*(1 + 0.1*(rand.uniform() - 0.5)) 

166 I0 = flux*(1 + b)/(2*np.pi*(sigma1**2 + b*sigma2**2)) 

167 for iy in range(y - self.ksize//2, y + self.ksize//2 + 1): 

168 if iy < 0 or iy >= self.mi.getHeight(): 

169 continue 

170 

171 for ix in range(x - self.ksize//2, x + self.ksize//2 + 1): 

172 if ix < 0 or ix >= self.mi.getWidth(): 

173 continue 

174 

175 II = I0*psfVal(ix, iy, x + dx, y + dy, sigma1, sigma2, b) 

176 Isample = rand.poisson(II) if addNoise else II 

177 self.mi.image[ix, iy, afwImage.LOCAL] += Isample 

178 self.mi.variance[ix, iy, afwImage.LOCAL] += II 

179 

180 bbox = geom.BoxI(geom.PointI(0, 0), geom.ExtentI(width, height)) 

181 self.cellSet = afwMath.SpatialCellSet(bbox, 100) 

182 

183 self.footprintSet = afwDetection.FootprintSet(self.mi, afwDetection.Threshold(100), "DETECTED") 

184 

185 self.catalog = self.measure(self.footprintSet, self.exposure) 

186 

187 for source in self.catalog: 

188 cand = measAlg.makePsfCandidate(source, self.exposure) 

189 self.cellSet.insertCandidate(cand) 

190 

191 def tearDown(self): 

192 del self.cellSet 

193 del self.exposure 

194 del self.mi 

195 del self.footprintSet 

196 del self.catalog 

197 del self.schema 

198 del self.measureSources 

199 

200 def setupDeterminer(self, exposure): 

201 """Setup the starSelector and psfDeterminer""" 

202 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"] 

203 starSelectorConfig = starSelectorClass.ConfigClass() 

204 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux" 

205 starSelectorConfig.badFlags = ["base_PixelFlags_flag_edge", 

206 "base_PixelFlags_flag_interpolatedCenter", 

207 "base_PixelFlags_flag_saturatedCenter", 

208 "base_PixelFlags_flag_crCenter", 

209 ] 

210 starSelectorConfig.widthStdAllowed = 0.5 # Set to match when the tolerance of the test was set 

211 

212 self.starSelector = starSelectorClass(config=starSelectorConfig) 

213 

214 self.makePsfCandidates = measAlg.MakePsfCandidatesTask() 

215 

216 psfDeterminerClass = measAlg.psfDeterminerRegistry["psfex"] 

217 psfDeterminerConfig = psfDeterminerClass.ConfigClass() 

218 width, height = exposure.getMaskedImage().getDimensions() 

219 psfDeterminerConfig.sizeCellX = width 

220 psfDeterminerConfig.sizeCellY = height//3 

221 psfDeterminerConfig.spatialOrder = 1 

222 

223 self.psfDeterminer = psfDeterminerClass(psfDeterminerConfig) 

224 

225 def subtractStars(self, exposure, catalog, chi_lim=-1): 

226 """Subtract the exposure's PSF from all the sources in catalog""" 

227 mi, psf = exposure.getMaskedImage(), exposure.getPsf() 

228 

229 subtracted = mi.Factory(mi, True) 

230 for s in catalog: 

231 xc, yc = s.getX(), s.getY() 

232 bbox = subtracted.getBBox(afwImage.PARENT) 

233 if bbox.contains(geom.PointI(int(xc), int(yc))): 

234 measAlg.subtractPsf(psf, subtracted, xc, yc) 

235 chi = subtracted.Factory(subtracted, True) 

236 var = subtracted.getVariance() 

237 np.sqrt(var.getArray(), var.getArray()) # inplace sqrt 

238 chi /= var 

239 

240 if display: 

241 afwDisplay.Display(frame=1).mtv(subtracted, title="Subtracted") 

242 afwDisplay.Display(frame=2).mtv(chi, title="Chi") 

243 xc, yc = exposure.getWidth()//2, exposure.getHeight()//2 

244 afwDisplay.Display(frame=3).mtv(psf.computeImage(geom.Point2D(xc, yc)), 

245 title="Psf %.1f,%.1f" % (xc, yc)) 

246 

247 chi_min, chi_max = np.min(chi.getImage().getArray()), np.max(chi.getImage().getArray()) 

248 

249 if chi_lim > 0: 

250 self.assertGreater(chi_min, -chi_lim) 

251 self.assertLess(chi_max, chi_lim) 

252 

253 def testPsfexDeterminer(self): 

254 """Test the (Psfex) psfDeterminer on subImages""" 

255 

256 self.setupDeterminer(self.exposure) 

257 metadata = dafBase.PropertyList() 

258 

259 stars = self.starSelector.run(self.catalog, exposure=self.exposure) 

260 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, exposure=self.exposure).psfCandidates 

261 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata) 

262 self.exposure.setPsf(psf) 

263 

264 # Test how well we can subtract the PSF model 

265 self.subtractStars(self.exposure, self.catalog, chi_lim=5.6) 

266 

267 # Test PsfexPsf.computeBBox 

268 self.assertEqual(psf.computeBBox(), psf.computeKernelImage().getBBox()) 

269 self.assertEqual(psf.computeBBox(), psf.getKernel().getBBox()) 

270 

271 

272class TestMemory(lsst.utils.tests.MemoryTestCase): 

273 pass 

274 

275 

276def setup_module(module): 

277 lsst.utils.tests.init() 

278 

279 

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

281 lsst.utils.tests.init() 

282 unittest.main()