Coverage for tests/test_psf.py: 14%

210 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-21 09:42 +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 

19from lsst.pex.config import FieldValidationError 

20 

21 

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

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

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

25 centered at (x, y) 

26 """ 

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

28 theta = np.radians(30) 

29 ab = 1.0/0.75 # axis ratio 

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

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

32 

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

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

35 

36 

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

38 """A test case for SpatialModelPsf""" 

39 

40 def measure(self, footprintSet, exposure): 

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

42 catalog = afwTable.SourceCatalog(self.schema) 

43 

44 footprintSet.makeSources(catalog) 

45 

46 self.measureSources.run(catalog, exposure) 

47 return catalog 

48 

49 def setUp(self): 

50 config = SingleFrameMeasurementTask.ConfigClass() 

51 config.slots.apFlux = 'base_CircularApertureFlux_12_0' 

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

53 

54 self.measureSources = SingleFrameMeasurementTask( 

55 self.schema, config=config 

56 ) 

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

58 

59 width, height = 110, 301 

60 

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

62 self.mi.set(0) 

63 sd = 3 # standard deviation of image 

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

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

66 

67 self.ksize = 31 # size of desired kernel 

68 

69 sigma1 = 1.75 

70 sigma2 = 2*sigma1 

71 

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

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

74 1.5*sigma1, 1, 0.1)) 

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

76 cdMatrix.shape = (2, 2) 

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

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

79 cdMatrix=cdMatrix) 

80 self.exposure.setWcs(wcs) 

81 

82 # 

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

84 # Useful for debugging 

85 # 

86 basisKernelList = [] 

87 for sigma in (sigma1, sigma2): 

88 basisKernel = afwMath.AnalyticKernel( 

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

90 ) 

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

92 basisKernel.computeImage(basisImage, True) 

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

94 

95 if sigma == sigma1: 

96 basisImage0 = basisImage 

97 else: 

98 basisImage -= basisImage0 

99 

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

101 

102 order = 1 # 1 => up to linear 

103 spFunc = afwMath.PolynomialFunction2D(order) 

104 

105 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc) 

106 exactKernel.setSpatialParameters( 

107 [[1.0, 0, 0], 

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

109 ) 

110 

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

112 

113 im = self.mi.getImage() 

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

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

116 

117 xarr, yarr = [], [] 

118 

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

120 (30, 35), 

121 (50, 50), 

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

123 (50, 120), (70, 80), 

124 (60, 210), (20, 210), 

125 ]: 

126 xarr.append(x) 

127 yarr.append(y) 

128 

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

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

131 dy = rand.uniform() - 0.5 

132 

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

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

135 

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

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

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

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

140 continue 

141 

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

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

144 continue 

145 

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

147 Isample = rand.poisson(II) 

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

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

150 

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

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

153 

154 self.footprintSet = afwDetection.FootprintSet( 

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

156 ) 

157 

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

159 

160 for source in self.catalog: 

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

162 self.cellSet.insertCandidate(cand) 

163 

164 def setupDeterminer(self, stampSize=None): 

165 """Setup the starSelector and psfDeterminer 

166 

167 Parameters 

168 ---------- 

169 stampSize : `int`, optional 

170 Set ``config.stampSize`` to this, if not None. 

171 """ 

172 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"] 

173 starSelectorConfig = starSelectorClass.ConfigClass() 

174 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux" 

175 starSelectorConfig.badFlags = [ 

176 "base_PixelFlags_flag_edge", 

177 "base_PixelFlags_flag_interpolatedCenter", 

178 "base_PixelFlags_flag_saturatedCenter", 

179 "base_PixelFlags_flag_crCenter", 

180 ] 

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

182 starSelectorConfig.widthStdAllowed = 0.5 

183 

184 self.starSelector = starSelectorClass(config=starSelectorConfig) 

185 

186 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

187 if stampSize is not None: 

188 makePsfCandidatesConfig.kernelSize = stampSize 

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

190 

191 psfDeterminerConfig = PiffPsfDeterminerConfig() 

192 psfDeterminerConfig.spatialOrder = 1 

193 if stampSize is not None: 

194 psfDeterminerConfig.stampSize = stampSize 

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, stampSize=None): 

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

223 

224 Parameters 

225 ---------- 

226 stampSize : `int`, optional 

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

228 """ 

229 self.setupDeterminer(stampSize=stampSize) 

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 

255 # Test how well we can subtract the PSF model 

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

257 

258 # Test bboxes 

259 for point in [ 

260 psf.getAveragePosition(), 

261 geom.Point2D(), 

262 geom.Point2D(1, 1) 

263 ]: 

264 self.assertEqual( 

265 psf.computeBBox(point), 

266 psf.computeKernelImage(point).getBBox() 

267 ) 

268 self.assertEqual( 

269 psf.computeKernelBBox(point), 

270 psf.computeKernelImage(point).getBBox() 

271 ) 

272 self.assertEqual( 

273 psf.computeImageBBox(point), 

274 psf.computeImage(point).getBBox() 

275 ) 

276 

277 # Some roundtrips 

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

279 self.exposure.writeFits(tmpFile) 

280 fitsIm = afwImage.ExposureF(tmpFile) 

281 copyIm = copy.deepcopy(self.exposure) 

282 

283 for newIm in [fitsIm, copyIm]: 

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

285 # that some PSF images come out the same. 

286 for point in [ 

287 geom.Point2D(0, 0), 

288 geom.Point2D(10, 100), 

289 geom.Point2D(-200, 30), 

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

291 ]: 

292 self.assertImagesAlmostEqual( 

293 psf.computeImage(point), 

294 newIm.getPsf().computeImage(point) 

295 ) 

296 # Also check average position 

297 newPsf = newIm.getPsf() 

298 self.assertImagesAlmostEqual( 

299 psf.computeImage(psf.getAveragePosition()), 

300 newPsf.computeImage(newPsf.getAveragePosition()) 

301 ) 

302 

303 def testPiffDeterminer_default(self): 

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

305 self.checkPiffDeterminer() 

306 

307 def testPiffDeterminer_kernelSize27(self): 

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

309 self.checkPiffDeterminer(27) 

310 

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

312 def test_validatePsfCandidates(self, samplingSize): 

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

314 

315 This should be independent of the samplingSize parameter. 

316 """ 

317 drawSizeDict = {1.0: 27, 

318 0.9: 31, 

319 1.1: 25, 

320 } 

321 

322 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

323 makePsfCandidatesConfig.kernelSize = 23 

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

325 psfCandidateList = self.makePsfCandidates.run( 

326 self.catalog, 

327 exposure=self.exposure 

328 ).psfCandidates 

329 

330 psfDeterminerConfig = PiffPsfDeterminerConfig() 

331 psfDeterminerConfig.stampSize = drawSizeDict[samplingSize] 

332 psfDeterminerConfig.samplingSize = samplingSize 

333 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

334 

335 with self.assertRaisesRegex(RuntimeError, 

336 "stampSize=27 " 

337 "pixels per side; found 23x23"): 

338 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 27) 

339 

340 # This should not raise. 

341 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 21) 

342 

343 

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

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

346 def testValidateGalsimInterpolant(self): 

347 # Check that random strings are not valid interpolants. 

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

349 # Check that the Lanczos order is an integer 

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

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

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

353 # Check for various valid Lanczos interpolants 

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

355 self.assertTrue(_validateGalsimInterpolant(interp)) 

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

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

358 self.assertTrue(eval(interp)) 

359 # Check that interpolation methods are case sensitive. 

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

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

362 self.assertFalse(_validateGalsimInterpolant(interp)) 

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

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

365 

366 def testKernelSize(self): # TODO: Remove this test in DM-36311. 

367 config = PiffPsfDeterminerConfig() 

368 

369 # Setting both stampSize and kernelSize should raise an error. 

370 config.stampSize = 27 

371 with self.assertWarns(FutureWarning): 

372 config.kernelSize = 25 

373 self.assertRaises(FieldValidationError, config.validate) 

374 

375 # even if they agree with each other 

376 config.stampSize = 31 

377 with self.assertWarns(FutureWarning): 

378 config.kernelSize = 31 

379 self.assertRaises(FieldValidationError, config.validate) 

380 

381 # Setting stampSize and kernelSize should be valid, because if not 

382 # set, stampSize is set to the size of PSF candidates internally. 

383 # This is only a temporary behavior and should go away in DM-36311. 

384 config.stampSize = None 

385 with self.assertWarns(FutureWarning): 

386 config.kernelSize = None 

387 config.validate() 

388 

389 

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

391 pass 

392 

393 

394def setup_module(module): 

395 lsst.utils.tests.init() 

396 

397 

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

399 lsst.utils.tests.init() 

400 unittest.main()