Coverage for tests/test_psf.py: 14%

201 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-10-26 16:30 +0000

1import galsim # noqa: F401 

2import unittest 

3import numpy as np 

4import copy 

5from galsim import Lanczos # noqa: F401 

6 

7import lsst.utils.tests 

8import lsst.afw.detection as afwDetection 

9import lsst.afw.geom as afwGeom 

10import lsst.afw.image as afwImage 

11import lsst.afw.math as afwMath 

12import lsst.afw.table as afwTable 

13import lsst.daf.base as dafBase 

14import lsst.geom as geom 

15import lsst.meas.algorithms as measAlg 

16from lsst.meas.base import SingleFrameMeasurementTask 

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

18from lsst.meas.extensions.piff.piffPsfDeterminer import _validateGalsimInterpolant 

19 

20 

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

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

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

24 centered at (x, y) 

25 """ 

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

27 theta = np.radians(30) 

28 ab = 1.0/0.75 # axis ratio 

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

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

31 

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

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

34 

35 

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

37 """A test case for SpatialModelPsf""" 

38 

39 def measure(self, footprintSet, exposure): 

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

41 catalog = afwTable.SourceCatalog(self.schema) 

42 

43 footprintSet.makeSources(catalog) 

44 

45 self.measureSources.run(catalog, exposure) 

46 return catalog 

47 

48 def setUp(self): 

49 config = SingleFrameMeasurementTask.ConfigClass() 

50 config.slots.apFlux = 'base_CircularApertureFlux_12_0' 

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

52 

53 self.measureSources = SingleFrameMeasurementTask( 

54 self.schema, config=config 

55 ) 

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

57 

58 width, height = 110, 301 

59 

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

61 self.mi.set(0) 

62 sd = 3 # standard deviation of image 

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

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

65 

66 self.ksize = 31 # size of desired kernel 

67 

68 sigma1 = 1.75 

69 sigma2 = 2*sigma1 

70 

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

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

73 1.5*sigma1, 1, 0.1)) 

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

75 cdMatrix.shape = (2, 2) 

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

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

78 cdMatrix=cdMatrix) 

79 self.exposure.setWcs(wcs) 

80 

81 # 

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

83 # Useful for debugging 

84 # 

85 basisKernelList = [] 

86 for sigma in (sigma1, sigma2): 

87 basisKernel = afwMath.AnalyticKernel( 

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

89 ) 

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

91 basisKernel.computeImage(basisImage, True) 

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

93 

94 if sigma == sigma1: 

95 basisImage0 = basisImage 

96 else: 

97 basisImage -= basisImage0 

98 

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

100 

101 order = 1 # 1 => up to linear 

102 spFunc = afwMath.PolynomialFunction2D(order) 

103 

104 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc) 

105 exactKernel.setSpatialParameters( 

106 [[1.0, 0, 0], 

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

108 ) 

109 

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

111 

112 im = self.mi.getImage() 

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

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

115 

116 xarr, yarr = [], [] 

117 

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

119 (30, 35), 

120 (50, 50), 

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

122 (50, 120), (70, 80), 

123 (60, 210), (20, 210), 

124 ]: 

125 xarr.append(x) 

126 yarr.append(y) 

127 

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

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

130 dy = rand.uniform() - 0.5 

131 

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

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

134 

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

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

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

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

139 continue 

140 

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

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

143 continue 

144 

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

146 Isample = rand.poisson(II) 

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

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

149 

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

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

152 

153 self.footprintSet = afwDetection.FootprintSet( 

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

155 ) 

156 

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

158 

159 for source in self.catalog: 

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

161 self.cellSet.insertCandidate(cand) 

162 

163 def setupDeterminer(self, kernelSize=None, debugStarData=False): 

164 """Setup the starSelector and psfDeterminer 

165 

166 Parameters 

167 ---------- 

168 kernelSize : `int`, optional 

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

170 """ 

171 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"] 

172 starSelectorConfig = starSelectorClass.ConfigClass() 

173 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux" 

174 starSelectorConfig.badFlags = [ 

175 "base_PixelFlags_flag_edge", 

176 "base_PixelFlags_flag_interpolatedCenter", 

177 "base_PixelFlags_flag_saturatedCenter", 

178 "base_PixelFlags_flag_crCenter", 

179 ] 

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

181 starSelectorConfig.widthStdAllowed = 0.5 

182 

183 self.starSelector = starSelectorClass(config=starSelectorConfig) 

184 

185 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

186 if kernelSize is not None: 

187 makePsfCandidatesConfig.kernelSize = kernelSize 

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

189 

190 psfDeterminerConfig = PiffPsfDeterminerConfig() 

191 psfDeterminerConfig.spatialOrder = 1 

192 if kernelSize is not None: 

193 psfDeterminerConfig.kernelSize = kernelSize 

194 psfDeterminerConfig.debugStarData = debugStarData 

195 

196 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

197 

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

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

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

201 

202 subtracted = mi.Factory(mi, True) 

203 for s in catalog: 

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

205 bbox = subtracted.getBBox(afwImage.PARENT) 

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

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

208 chi = subtracted.Factory(subtracted, True) 

209 var = subtracted.getVariance() 

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

211 chi /= var 

212 

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

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

215 print(chi_min, chi_max) 

216 

217 if chi_lim > 0: 

218 self.assertGreater(chi_min, -chi_lim) 

219 self.assertLess(chi_max, chi_lim) 

220 

221 def checkPiffDeterminer(self, kernelSize=None, debugStarData=False): 

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

223 

224 Parameters 

225 ---------- 

226 kernelSize : `int`, optional 

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

228 """ 

229 self.setupDeterminer(kernelSize=kernelSize, debugStarData=debugStarData) 

230 metadata = dafBase.PropertyList() 

231 

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

233 psfCandidateList = self.makePsfCandidates.run( 

234 stars.sourceCat, 

235 exposure=self.exposure 

236 ).psfCandidates 

237 psf, cellSet = self.psfDeterminer.determinePsf( 

238 self.exposure, 

239 psfCandidateList, 

240 metadata, 

241 flagKey=self.usePsfFlag 

242 ) 

243 self.exposure.setPsf(psf) 

244 

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

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

247 self.assertEqual( 

248 psf.getAveragePosition(), 

249 geom.Point2D( 

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

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

252 ) 

253 ) 

254 if self.psfDeterminer.config.debugStarData: 

255 self.assertIn('image', psf._piffResult.stars[0].data.__dict__) 

256 else: 

257 self.assertNotIn('image', psf._piffResult.stars[0].data.__dict__) 

258 

259 # Test how well we can subtract the PSF model 

260 self.subtractStars(self.exposure, self.catalog, chi_lim=6.1) 

261 

262 # Test bboxes 

263 for point in [ 

264 psf.getAveragePosition(), 

265 geom.Point2D(), 

266 geom.Point2D(1, 1) 

267 ]: 

268 self.assertEqual( 

269 psf.computeBBox(point), 

270 psf.computeKernelImage(point).getBBox() 

271 ) 

272 self.assertEqual( 

273 psf.computeKernelBBox(point), 

274 psf.computeKernelImage(point).getBBox() 

275 ) 

276 self.assertEqual( 

277 psf.computeImageBBox(point), 

278 psf.computeImage(point).getBBox() 

279 ) 

280 

281 # Some roundtrips 

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

283 self.exposure.writeFits(tmpFile) 

284 fitsIm = afwImage.ExposureF(tmpFile) 

285 copyIm = copy.deepcopy(self.exposure) 

286 

287 for newIm in [fitsIm, copyIm]: 

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

289 # that some PSF images come out the same. 

290 for point in [ 

291 geom.Point2D(0, 0), 

292 geom.Point2D(10, 100), 

293 geom.Point2D(-200, 30), 

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

295 ]: 

296 self.assertImagesAlmostEqual( 

297 psf.computeImage(point), 

298 newIm.getPsf().computeImage(point) 

299 ) 

300 # Also check average position 

301 newPsf = newIm.getPsf() 

302 self.assertImagesAlmostEqual( 

303 psf.computeImage(psf.getAveragePosition()), 

304 newPsf.computeImage(newPsf.getAveragePosition()) 

305 ) 

306 

307 def testPiffDeterminer_default(self): 

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

309 self.checkPiffDeterminer() 

310 

311 def testPiffDeterminer_kernelSize27(self): 

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

313 self.checkPiffDeterminer(27) 

314 

315 def testPiffDeterminer_debugStarData(self): 

316 """Test Piff with debugStarData=True.""" 

317 self.checkPiffDeterminer(debugStarData=True) 

318 

319 @lsst.utils.tests.methodParameters(samplingSize=[1.0, 0.9, 1.1]) 

320 def test_validatePsfCandidates(self, samplingSize): 

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

322 """ 

323 drawSizeDict = {1.0: 27, 

324 0.9: 31, 

325 1.1: 25, 

326 } 

327 

328 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

329 makePsfCandidatesConfig.kernelSize = 24 

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

331 psfCandidateList = self.makePsfCandidates.run( 

332 self.catalog, 

333 exposure=self.exposure 

334 ).psfCandidates 

335 

336 psfDeterminerConfig = PiffPsfDeterminerConfig() 

337 psfDeterminerConfig.kernelSize = 27 

338 psfDeterminerConfig.samplingSize = samplingSize 

339 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

340 

341 with self.assertRaisesRegex(RuntimeError, 

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

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

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

345 

346 # This should not raise. 

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

348 

349 

350class PiffConfigTestCase(lsst.utils.tests.TestCase): 

351 """A test case to check for valid Piff config""" 

352 def testValidateGalsimInterpolant(self): 

353 # Check that random strings are not valid interpolants. 

354 self.assertFalse(_validateGalsimInterpolant("foo")) 

355 # Check that the Lanczos order is an integer 

356 self.assertFalse(_validateGalsimInterpolant("Lanczos(3.0")) 

357 self.assertFalse(_validateGalsimInterpolant("Lanczos(-5.0")) 

358 self.assertFalse(_validateGalsimInterpolant("Lanczos(N)")) 

359 # Check for various valid Lanczos interpolants 

360 for interp in ("Lanczos(4)", "galsim.Lanczos(7)"): 

361 self.assertTrue(_validateGalsimInterpolant(interp)) 

362 self.assertFalse(_validateGalsimInterpolant(interp.lower())) 

363 # Evaluating the string should succeed. This is how Piff does it. 

364 self.assertTrue(eval(interp)) 

365 # Check that interpolation methods are case sensitive. 

366 for interp in ("Linear", "Cubic", "Quintic", "Delta", "Nearest", "SincInterpolant"): 

367 self.assertFalse(_validateGalsimInterpolant(f"galsim.{interp.lower()}")) 

368 self.assertFalse(_validateGalsimInterpolant(interp)) 

369 self.assertTrue(_validateGalsimInterpolant(f"galsim.{interp}")) 

370 self.assertTrue(eval(f"galsim.{interp}")) 

371 

372 

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

374 pass 

375 

376 

377def setup_module(module): 

378 lsst.utils.tests.init() 

379 

380 

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

382 lsst.utils.tests.init() 

383 unittest.main()