Coverage for tests/test_PsfexPsf.py: 15%

185 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-22 10:11 +0000

1# This file is part of meas_extensions_psfex. 

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 math 

23import numpy as np 

24import unittest 

25 

26import lsst.utils.tests 

27import lsst.afw.image as afwImage 

28import lsst.afw.detection as afwDetection 

29import lsst.afw.geom as afwGeom 

30import lsst.geom as geom 

31import lsst.afw.math as afwMath 

32import lsst.afw.table as afwTable 

33import lsst.daf.base as dafBase 

34import lsst.meas.algorithms as measAlg 

35from lsst.meas.base import SingleFrameMeasurementTask 

36# register the PSF determiner 

37import lsst.meas.extensions.psfex.psfexPsfDeterminer 

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

39 

40try: 

41 display 

42except NameError: 

43 display = False 

44else: 

45 import lsst.afw.display as afwDisplay 

46 afwDisplay.setDefaultMaskTransparency(75) 

47 

48 

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

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

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

52 centered at (x, y) 

53 """ 

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

55 theta = np.radians(30) 

56 ab = 1.0/0.75 # axis ratio 

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

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

59 

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

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

62 

63 

64class SpatialModelPsfTestCase(unittest.TestCase): 

65 """A test case for SpatialModelPsf""" 

66 

67 def measure(self, footprintSet, exposure): 

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

69 catalog = afwTable.SourceCatalog(self.schema) 

70 if display: 

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

72 

73 footprintSet.makeSources(catalog) 

74 

75 self.measureSources.run(catalog, exposure) 

76 return catalog 

77 

78 def setUp(self): 

79 config = SingleFrameMeasurementTask.ConfigClass() 

80 config.slots.apFlux = 'base_CircularApertureFlux_12_0' 

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

82 

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

84 

85 width, height = 110, 301 

86 

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

88 self.mi.set(0) 

89 sd = 3 # standard deviation of image 

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

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

92 

93 self.ksize = 31 # size of desired kernel 

94 

95 sigma1 = 1.75 

96 sigma2 = 2*sigma1 

97 

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

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

100 1.5*sigma1, 1, 0.1)) 

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

102 cdMatrix.shape = (2, 2) 

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

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

105 cdMatrix=cdMatrix) 

106 self.exposure.setWcs(wcs) 

107 

108 # 

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

110 # Useful for debugging 

111 # 

112 basisKernelList = [] 

113 for sigma in (sigma1, sigma2): 

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

115 afwMath.GaussianFunction2D(sigma, sigma)) 

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

117 basisKernel.computeImage(basisImage, True) 

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

119 

120 if sigma == sigma1: 

121 basisImage0 = basisImage 

122 else: 

123 basisImage -= basisImage0 

124 

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

126 

127 order = 1 # 1 => up to linear 

128 spFunc = afwMath.PolynomialFunction2D(order) 

129 

130 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc) 

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

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

133 

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

135 

136 addNoise = True 

137 

138 if addNoise: 

139 im = self.mi.getImage() 

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

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

142 del im 

143 

144 xarr, yarr = [], [] 

145 

146 # NOTE: Warning to those trying to add sources near the edges here: 

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

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

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

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

151 (30, 35), 

152 (50, 50), 

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

154 (50, 120), (70, 80), 

155 (60, 210), (20, 210), 

156 ]: 

157 xarr.append(x) 

158 yarr.append(y) 

159 

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

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

162 dy = rand.uniform() - 0.5 

163 

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

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

166 

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

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

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

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

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

172 continue 

173 

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

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

176 continue 

177 

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

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

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

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

182 

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

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

185 

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

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

188 

189 for source in self.catalog: 

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

191 self.cellSet.insertCandidate(cand) 

192 

193 def tearDown(self): 

194 del self.cellSet 

195 del self.exposure 

196 del self.mi 

197 del self.footprintSet 

198 del self.catalog 

199 del self.schema 

200 del self.measureSources 

201 

202 def setupDeterminer(self, exposure, fluxField=None): 

203 """Setup the starSelector and psfDeterminer""" 

204 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"] 

205 starSelectorConfig = starSelectorClass.ConfigClass() 

206 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux" 

207 starSelectorConfig.badFlags = ["base_PixelFlags_flag_edge", 

208 "base_PixelFlags_flag_interpolatedCenter", 

209 "base_PixelFlags_flag_saturatedCenter", 

210 "base_PixelFlags_flag_crCenter", 

211 ] 

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

213 

214 self.starSelector = starSelectorClass(config=starSelectorConfig) 

215 

216 self.makePsfCandidates = measAlg.MakePsfCandidatesTask() 

217 

218 psfDeterminerClass = measAlg.psfDeterminerRegistry["psfex"] 

219 psfDeterminerConfig = psfDeterminerClass.ConfigClass() 

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

221 psfDeterminerConfig.sizeCellX = width 

222 psfDeterminerConfig.sizeCellY = height//3 

223 psfDeterminerConfig.spatialOrder = 1 

224 if fluxField is not None: 

225 psfDeterminerConfig.photometricFluxField = fluxField 

226 

227 self.psfDeterminer = psfDeterminerClass(psfDeterminerConfig) 

228 

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

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

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

232 

233 subtracted = mi.Factory(mi, True) 

234 for s in catalog: 

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

236 bbox = subtracted.getBBox(afwImage.PARENT) 

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

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

239 chi = subtracted.Factory(subtracted, True) 

240 var = subtracted.getVariance() 

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

242 chi /= var 

243 

244 if display: 

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

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

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

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

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

250 

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

252 

253 if chi_lim > 0: 

254 self.assertGreater(chi_min, -chi_lim) 

255 self.assertLess(chi_max, chi_lim) 

256 

257 def testPsfexDeterminer(self): 

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

259 

260 self.setupDeterminer(self.exposure) 

261 metadata = dafBase.PropertyList() 

262 

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

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

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

266 self.exposure.setPsf(psf) 

267 

268 # Test how well we can subtract the PSF model 

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

270 

271 # Test PsfexPsf.computeBBox 

272 pos = psf.getAveragePosition() 

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

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

275 

276 def testPsfexDeterminerTooFewStars(self): 

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

278 self.setupDeterminer(self.exposure) 

279 metadata = dafBase.PropertyList() 

280 

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

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

283 

284 psfCandidateListShort = psfCandidateList[0: 3] 

285 

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

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

288 

289 def testPsfDeterminerChangeFluxField(self): 

290 """Test the psfDeterminer with a different flux normalization field.""" 

291 # We test here with an aperture that we would be unlikely to ever use 

292 # as a default. 

293 self.setupDeterminer(self.exposure, fluxField="base_CircularApertureFlux_6_0_instFlux") 

294 metadata = dafBase.PropertyList() 

295 

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

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

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

299 self.exposure.setPsf(psf) 

300 

301 # Test how well we can subtract the PSF model 

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

303 

304 

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

306 pass 

307 

308 

309def setup_module(module): 

310 lsst.utils.tests.init() 

311 

312 

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

314 lsst.utils.tests.init() 

315 unittest.main()