Coverage for tests/test_PsfexPsf.py: 16%

175 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-23 08:56 +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): 

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 

225 self.psfDeterminer = psfDeterminerClass(psfDeterminerConfig) 

226 

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

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

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

230 

231 subtracted = mi.Factory(mi, True) 

232 for s in catalog: 

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

234 bbox = subtracted.getBBox(afwImage.PARENT) 

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

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

237 chi = subtracted.Factory(subtracted, True) 

238 var = subtracted.getVariance() 

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

240 chi /= var 

241 

242 if display: 

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

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

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

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

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

248 

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

250 

251 if chi_lim > 0: 

252 self.assertGreater(chi_min, -chi_lim) 

253 self.assertLess(chi_max, chi_lim) 

254 

255 def testPsfexDeterminer(self): 

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

257 

258 self.setupDeterminer(self.exposure) 

259 metadata = dafBase.PropertyList() 

260 

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

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

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

264 self.exposure.setPsf(psf) 

265 

266 # Test how well we can subtract the PSF model 

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

268 

269 # Test PsfexPsf.computeBBox 

270 pos = psf.getAveragePosition() 

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

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

273 

274 def testPsfexDeterminerTooFewStars(self): 

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

276 self.setupDeterminer(self.exposure) 

277 metadata = dafBase.PropertyList() 

278 

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

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

281 

282 psfCandidateListShort = psfCandidateList[0: 3] 

283 

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

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

286 

287 

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

289 pass 

290 

291 

292def setup_module(module): 

293 lsst.utils.tests.init() 

294 

295 

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

297 lsst.utils.tests.init() 

298 unittest.main()