Coverage for tests/test_psf.py: 15%

177 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-29 02:54 -0700

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=6.1) 

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 @lsst.utils.tests.methodParameters(samplingSize=[1.0, 0.9, 1.1]) 

308 def test_validatePsfCandidates(self, samplingSize): 

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

310 """ 

311 drawSizeDict = {1.0: 27, 

312 0.9: 31, 

313 1.1: 25, 

314 } 

315 

316 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

317 makePsfCandidatesConfig.kernelSize = 24 

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

319 psfCandidateList = self.makePsfCandidates.run( 

320 self.catalog, 

321 exposure=self.exposure 

322 ).psfCandidates 

323 

324 psfDeterminerConfig = PiffPsfDeterminerConfig() 

325 psfDeterminerConfig.kernelSize = 27 

326 psfDeterminerConfig.samplingSize = samplingSize 

327 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

328 

329 with self.assertRaisesRegex(RuntimeError, 

330 f"config.kernelSize/config.samplingSize={drawSizeDict[samplingSize]} " 

331 "pixels per side; found 24x24"): 

332 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 27, samplingSize) 

333 

334 # This should not raise. 

335 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 21, samplingSize) 

336 

337 

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

339 pass 

340 

341 

342def setup_module(module): 

343 lsst.utils.tests.init() 

344 

345 

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

347 lsst.utils.tests.init() 

348 unittest.main()