Coverage for tests/test_psf.py: 15%

195 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-06 13:26 -0800

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): 

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 

195 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

196 

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

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

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

200 

201 subtracted = mi.Factory(mi, True) 

202 for s in catalog: 

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

204 bbox = subtracted.getBBox(afwImage.PARENT) 

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

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

207 chi = subtracted.Factory(subtracted, True) 

208 var = subtracted.getVariance() 

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

210 chi /= var 

211 

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

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

214 print(chi_min, chi_max) 

215 

216 if chi_lim > 0: 

217 self.assertGreater(chi_min, -chi_lim) 

218 self.assertLess(chi_max, chi_lim) 

219 

220 def checkPiffDeterminer(self, kernelSize=None): 

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

222 

223 Parameters 

224 ---------- 

225 kernelSize : `int`, optional 

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

227 """ 

228 self.setupDeterminer(kernelSize=kernelSize) 

229 metadata = dafBase.PropertyList() 

230 

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

232 psfCandidateList = self.makePsfCandidates.run( 

233 stars.sourceCat, 

234 exposure=self.exposure 

235 ).psfCandidates 

236 psf, cellSet = self.psfDeterminer.determinePsf( 

237 self.exposure, 

238 psfCandidateList, 

239 metadata, 

240 flagKey=self.usePsfFlag 

241 ) 

242 self.exposure.setPsf(psf) 

243 

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

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

246 self.assertEqual( 

247 psf.getAveragePosition(), 

248 geom.Point2D( 

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

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

251 ) 

252 ) 

253 

254 # Test how well we can subtract the PSF model 

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

256 

257 # Test bboxes 

258 for point in [ 

259 psf.getAveragePosition(), 

260 geom.Point2D(), 

261 geom.Point2D(1, 1) 

262 ]: 

263 self.assertEqual( 

264 psf.computeBBox(point), 

265 psf.computeKernelImage(point).getBBox() 

266 ) 

267 self.assertEqual( 

268 psf.computeKernelBBox(point), 

269 psf.computeKernelImage(point).getBBox() 

270 ) 

271 self.assertEqual( 

272 psf.computeImageBBox(point), 

273 psf.computeImage(point).getBBox() 

274 ) 

275 

276 # Some roundtrips 

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

278 self.exposure.writeFits(tmpFile) 

279 fitsIm = afwImage.ExposureF(tmpFile) 

280 copyIm = copy.deepcopy(self.exposure) 

281 

282 for newIm in [fitsIm, copyIm]: 

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

284 # that some PSF images come out the same. 

285 for point in [ 

286 geom.Point2D(0, 0), 

287 geom.Point2D(10, 100), 

288 geom.Point2D(-200, 30), 

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

290 ]: 

291 self.assertImagesAlmostEqual( 

292 psf.computeImage(point), 

293 newIm.getPsf().computeImage(point) 

294 ) 

295 # Also check average position 

296 newPsf = newIm.getPsf() 

297 self.assertImagesAlmostEqual( 

298 psf.computeImage(psf.getAveragePosition()), 

299 newPsf.computeImage(newPsf.getAveragePosition()) 

300 ) 

301 

302 def testPiffDeterminer_default(self): 

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

304 self.checkPiffDeterminer() 

305 

306 def testPiffDeterminer_kernelSize27(self): 

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

308 self.checkPiffDeterminer(27) 

309 

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

311 def test_validatePsfCandidates(self, samplingSize): 

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

313 """ 

314 drawSizeDict = {1.0: 27, 

315 0.9: 31, 

316 1.1: 25, 

317 } 

318 

319 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

320 makePsfCandidatesConfig.kernelSize = 24 

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

322 psfCandidateList = self.makePsfCandidates.run( 

323 self.catalog, 

324 exposure=self.exposure 

325 ).psfCandidates 

326 

327 psfDeterminerConfig = PiffPsfDeterminerConfig() 

328 psfDeterminerConfig.kernelSize = 27 

329 psfDeterminerConfig.samplingSize = samplingSize 

330 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

331 

332 with self.assertRaisesRegex(RuntimeError, 

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

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

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

336 

337 # This should not raise. 

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

339 

340 

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

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

343 def testValidateGalsimInterpolant(self): 

344 # Check that random strings are not valid interpolants. 

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

346 # Check that the Lanczos order is an integer 

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

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

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

350 # Check for various valid Lanczos interpolants 

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

352 self.assertTrue(_validateGalsimInterpolant(interp)) 

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

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

355 self.assertTrue(eval(interp)) 

356 # Check that interpolation methods are case sensitive. 

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

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

359 self.assertFalse(_validateGalsimInterpolant(interp)) 

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

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

362 

363 

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

365 pass 

366 

367 

368def setup_module(module): 

369 lsst.utils.tests.init() 

370 

371 

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

373 lsst.utils.tests.init() 

374 unittest.main()