Coverage for tests/test_psf.py: 15%

Shortcuts 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

174 statements  

1import unittest 

2import numpy as np 

3import copy 

4 

5import lsst.utils.tests 

6import lsst.afw.detection as afwDetection 

7import lsst.afw.geom as afwGeom 

8import lsst.afw.image as afwImage 

9import lsst.afw.math as afwMath 

10import lsst.afw.table as afwTable 

11import lsst.daf.base as dafBase 

12import lsst.geom as geom 

13import lsst.meas.algorithms as measAlg 

14from lsst.meas.base import SingleFrameMeasurementTask 

15from lsst.meas.extensions.piff.piffPsfDeterminer import PiffPsfDeterminerConfig, PiffPsfDeterminerTask 

16 

17 

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

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

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

21 centered at (x, y) 

22 """ 

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

24 theta = np.radians(30) 

25 ab = 1.0/0.75 # axis ratio 

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

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

28 

29 return (np.exp(-0.5*(u**2 + (v*ab)**2)/sigma1**2) 

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

31 

32 

33class SpatialModelPsfTestCase(lsst.utils.tests.TestCase): 

34 """A test case for SpatialModelPsf""" 

35 

36 def measure(self, footprintSet, exposure): 

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

38 catalog = afwTable.SourceCatalog(self.schema) 

39 

40 footprintSet.makeSources(catalog) 

41 

42 self.measureSources.run(catalog, exposure) 

43 return catalog 

44 

45 def setUp(self): 

46 config = SingleFrameMeasurementTask.ConfigClass() 

47 config.slots.apFlux = 'base_CircularApertureFlux_12_0' 

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

49 

50 self.measureSources = SingleFrameMeasurementTask( 

51 self.schema, config=config 

52 ) 

53 self.usePsfFlag = self.schema.addField("use_psf", type="Flag") 

54 

55 width, height = 110, 301 

56 

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

58 self.mi.set(0) 

59 sd = 3 # standard deviation of image 

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

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

62 

63 self.ksize = 31 # size of desired kernel 

64 

65 sigma1 = 1.75 

66 sigma2 = 2*sigma1 

67 

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

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

70 1.5*sigma1, 1, 0.1)) 

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

72 cdMatrix.shape = (2, 2) 

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

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

75 cdMatrix=cdMatrix) 

76 self.exposure.setWcs(wcs) 

77 

78 # 

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

80 # Useful for debugging 

81 # 

82 basisKernelList = [] 

83 for sigma in (sigma1, sigma2): 

84 basisKernel = afwMath.AnalyticKernel( 

85 self.ksize, self.ksize, afwMath.GaussianFunction2D(sigma, sigma) 

86 ) 

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

88 basisKernel.computeImage(basisImage, True) 

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

90 

91 if sigma == sigma1: 

92 basisImage0 = basisImage 

93 else: 

94 basisImage -= basisImage0 

95 

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

97 

98 order = 1 # 1 => up to linear 

99 spFunc = afwMath.PolynomialFunction2D(order) 

100 

101 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc) 

102 exactKernel.setSpatialParameters( 

103 [[1.0, 0, 0], 

104 [0.0, 0.5*1e-2, 0.2e-2]] 

105 ) 

106 

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

108 

109 im = self.mi.getImage() 

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

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

112 

113 xarr, yarr = [], [] 

114 

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

116 (30, 35), 

117 (50, 50), 

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

119 (50, 120), (70, 80), 

120 (60, 210), (20, 210), 

121 ]: 

122 xarr.append(x) 

123 yarr.append(y) 

124 

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

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

127 dy = rand.uniform() - 0.5 

128 

129 k = exactKernel.getSpatialFunction(1)(x, y) 

130 b = (k*sigma1**2/((1 - k)*sigma2**2)) 

131 

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

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

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

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

136 continue 

137 

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

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

140 continue 

141 

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

143 Isample = rand.poisson(II) 

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

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

146 

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

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

149 

150 self.footprintSet = afwDetection.FootprintSet( 

151 self.mi, afwDetection.Threshold(100), "DETECTED" 

152 ) 

153 

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

155 

156 for source in self.catalog: 

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

158 self.cellSet.insertCandidate(cand) 

159 

160 def setupDeterminer(self, kernelSize=None): 

161 """Setup the starSelector and psfDeterminer 

162 

163 Parameters 

164 ---------- 

165 kernelSize : `int`, optional 

166 Set ``config.kernelSize`` to this, if not None. 

167 """ 

168 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"] 

169 starSelectorConfig = starSelectorClass.ConfigClass() 

170 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux" 

171 starSelectorConfig.badFlags = [ 

172 "base_PixelFlags_flag_edge", 

173 "base_PixelFlags_flag_interpolatedCenter", 

174 "base_PixelFlags_flag_saturatedCenter", 

175 "base_PixelFlags_flag_crCenter", 

176 ] 

177 # Set to match when the tolerance of the test was set 

178 starSelectorConfig.widthStdAllowed = 0.5 

179 

180 self.starSelector = starSelectorClass(config=starSelectorConfig) 

181 

182 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

183 if kernelSize is not None: 

184 makePsfCandidatesConfig.kernelSize = kernelSize 

185 self.makePsfCandidates = measAlg.MakePsfCandidatesTask(config=makePsfCandidatesConfig) 

186 

187 psfDeterminerConfig = PiffPsfDeterminerConfig() 

188 psfDeterminerConfig.spatialOrder = 1 

189 if kernelSize is not None: 

190 psfDeterminerConfig.kernelSize = kernelSize 

191 

192 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

193 

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

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

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

197 

198 subtracted = mi.Factory(mi, True) 

199 for s in catalog: 

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

201 bbox = subtracted.getBBox(afwImage.PARENT) 

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

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

204 chi = subtracted.Factory(subtracted, True) 

205 var = subtracted.getVariance() 

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

207 chi /= var 

208 

209 chi_min = np.min(chi.getImage().getArray()) 

210 chi_max = np.max(chi.getImage().getArray()) 

211 print(chi_min, chi_max) 

212 

213 if chi_lim > 0: 

214 self.assertGreater(chi_min, -chi_lim) 

215 self.assertLess(chi_max, chi_lim) 

216 

217 def checkPiffDeterminer(self, kernelSize=None): 

218 """Configure PiffPsfDeterminerTask and run basic tests on it. 

219 

220 Parameters 

221 ---------- 

222 kernelSize : `int`, optional 

223 Set ``config.kernelSize`` to this, if not None. 

224 """ 

225 self.setupDeterminer(kernelSize=kernelSize) 

226 metadata = dafBase.PropertyList() 

227 

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

229 psfCandidateList = self.makePsfCandidates.run( 

230 stars.sourceCat, 

231 exposure=self.exposure 

232 ).psfCandidates 

233 psf, cellSet = self.psfDeterminer.determinePsf( 

234 self.exposure, 

235 psfCandidateList, 

236 metadata, 

237 flagKey=self.usePsfFlag 

238 ) 

239 self.exposure.setPsf(psf) 

240 

241 self.assertEqual(len(psfCandidateList), metadata['numAvailStars']) 

242 self.assertEqual(sum(self.catalog['use_psf']), metadata['numGoodStars']) 

243 self.assertEqual( 

244 psf.getAveragePosition(), 

245 geom.Point2D( 

246 np.mean([s.x for s in psf._piffResult.stars]), 

247 np.mean([s.y for s in psf._piffResult.stars]) 

248 ) 

249 ) 

250 

251 # Test how well we can subtract the PSF model 

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

253 

254 # Test bboxes 

255 for point in [ 

256 psf.getAveragePosition(), 

257 geom.Point2D(), 

258 geom.Point2D(1, 1) 

259 ]: 

260 self.assertEqual( 

261 psf.computeBBox(point), 

262 psf.computeKernelImage(point).getBBox() 

263 ) 

264 self.assertEqual( 

265 psf.computeKernelBBox(point), 

266 psf.computeKernelImage(point).getBBox() 

267 ) 

268 self.assertEqual( 

269 psf.computeImageBBox(point), 

270 psf.computeImage(point).getBBox() 

271 ) 

272 

273 # Some roundtrips 

274 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

275 self.exposure.writeFits(tmpFile) 

276 fitsIm = afwImage.ExposureF(tmpFile) 

277 copyIm = copy.deepcopy(self.exposure) 

278 

279 for newIm in [fitsIm, copyIm]: 

280 # Piff doesn't enable __eq__ for its results, so we just check 

281 # that some PSF images come out the same. 

282 for point in [ 

283 geom.Point2D(0, 0), 

284 geom.Point2D(10, 100), 

285 geom.Point2D(-200, 30), 

286 geom.Point2D(float("nan")) # "nullPoint" 

287 ]: 

288 self.assertImagesAlmostEqual( 

289 psf.computeImage(point), 

290 newIm.getPsf().computeImage(point) 

291 ) 

292 # Also check average position 

293 newPsf = newIm.getPsf() 

294 self.assertImagesAlmostEqual( 

295 psf.computeImage(psf.getAveragePosition()), 

296 newPsf.computeImage(newPsf.getAveragePosition()) 

297 ) 

298 

299 def testPiffDeterminer_default(self): 

300 """Test piff with the default config.""" 

301 self.checkPiffDeterminer() 

302 

303 def testPiffDeterminer_kernelSize27(self): 

304 """Test Piff with a psf kernelSize of 27.""" 

305 self.checkPiffDeterminer(27) 

306 

307 def test_validatePsfCandidates(self): 

308 """Test that `_validatePsfCandidates` raises for too-small candidates. 

309 """ 

310 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

311 makePsfCandidatesConfig.kernelSize = 24 

312 self.makePsfCandidates = measAlg.MakePsfCandidatesTask(config=makePsfCandidatesConfig) 

313 psfCandidateList = self.makePsfCandidates.run( 

314 self.catalog, 

315 exposure=self.exposure 

316 ).psfCandidates 

317 

318 psfDeterminerConfig = PiffPsfDeterminerConfig() 

319 psfDeterminerConfig.kernelSize = 25 

320 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

321 with self.assertRaisesRegex(RuntimeError, 

322 "config.kernelSize=25 pixels per side; found 24x24"): 

323 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 25) 

324 

325 # This should not raise. 

326 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 23) 

327 

328 

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

330 pass 

331 

332 

333def setup_module(module): 

334 lsst.utils.tests.init() 

335 

336 

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

338 lsst.utils.tests.init() 

339 unittest.main()