Coverage for tests/test_PsfexPsf.py: 16%

175 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-19 05:18 -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 # NOTE: Warning to those trying to add sources near the edges here: 

148 # self.subtractStars() assumes that every source is able to have the 

149 # psf subtracted. That's not possible for sources on the edge, so the 

150 # chi2 calculation that is asserted on will be off. 

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

152 (30, 35), 

153 (50, 50), 

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

155 (50, 120), (70, 80), 

156 (60, 210), (20, 210), 

157 ]: 

158 xarr.append(x) 

159 yarr.append(y) 

160 

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

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

163 dy = rand.uniform() - 0.5 

164 

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

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

167 

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

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

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

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

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

173 continue 

174 

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

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

177 continue 

178 

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

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

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

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

183 

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

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

186 

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

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

189 

190 for source in self.catalog: 

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

192 self.cellSet.insertCandidate(cand) 

193 

194 def tearDown(self): 

195 del self.cellSet 

196 del self.exposure 

197 del self.mi 

198 del self.footprintSet 

199 del self.catalog 

200 del self.schema 

201 del self.measureSources 

202 

203 def setupDeterminer(self, exposure): 

204 """Setup the starSelector and psfDeterminer""" 

205 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"] 

206 starSelectorConfig = starSelectorClass.ConfigClass() 

207 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux" 

208 starSelectorConfig.badFlags = ["base_PixelFlags_flag_edge", 

209 "base_PixelFlags_flag_interpolatedCenter", 

210 "base_PixelFlags_flag_saturatedCenter", 

211 "base_PixelFlags_flag_crCenter", 

212 ] 

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

214 

215 self.starSelector = starSelectorClass(config=starSelectorConfig) 

216 

217 self.makePsfCandidates = measAlg.MakePsfCandidatesTask() 

218 

219 psfDeterminerClass = measAlg.psfDeterminerRegistry["psfex"] 

220 psfDeterminerConfig = psfDeterminerClass.ConfigClass() 

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

222 psfDeterminerConfig.sizeCellX = width 

223 psfDeterminerConfig.sizeCellY = height//3 

224 psfDeterminerConfig.spatialOrder = 1 

225 

226 self.psfDeterminer = psfDeterminerClass(psfDeterminerConfig) 

227 

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

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

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

231 

232 subtracted = mi.Factory(mi, True) 

233 for s in catalog: 

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

235 bbox = subtracted.getBBox(afwImage.PARENT) 

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

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

238 chi = subtracted.Factory(subtracted, True) 

239 var = subtracted.getVariance() 

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

241 chi /= var 

242 

243 if display: 

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

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

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

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

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

249 

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

251 

252 if chi_lim > 0: 

253 self.assertGreater(chi_min, -chi_lim) 

254 self.assertLess(chi_max, chi_lim) 

255 

256 def testPsfexDeterminer(self): 

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

258 

259 self.setupDeterminer(self.exposure) 

260 metadata = dafBase.PropertyList() 

261 

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

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

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

265 self.exposure.setPsf(psf) 

266 

267 # Test how well we can subtract the PSF model 

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

269 

270 # Test PsfexPsf.computeBBox 

271 pos = psf.getAveragePosition() 

272 self.assertEqual(psf.computeBBox(pos), psf.computeKernelImage(pos).getBBox()) 

273 self.assertEqual(psf.computeBBox(pos), psf.getKernel(pos).getBBox()) 

274 

275 def testPsfexDeterminerTooFewStars(self): 

276 """Test the (Psfex) psfDeterminer with too few stars.""" 

277 self.setupDeterminer(self.exposure) 

278 metadata = dafBase.PropertyList() 

279 

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

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

282 

283 psfCandidateListShort = psfCandidateList[0: 3] 

284 

285 with self.assertRaisesRegex(RuntimeError, "Failed to determine"): 

286 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateListShort, metadata) 

287 

288 

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

290 pass 

291 

292 

293def setup_module(module): 

294 lsst.utils.tests.init() 

295 

296 

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

298 lsst.utils.tests.init() 

299 unittest.main()