Coverage for tests/test_psf.py: 14%

219 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-04 02:32 -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 

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]) * 0.2/3600 

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( 

165 self, 

166 stampSize=None, 

167 debugStarData=False, 

168 useCoordinates='pixel' 

169 ): 

170 """Setup the starSelector and psfDeterminer 

171 

172 Parameters 

173 ---------- 

174 stampSize : `int`, optional 

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

176 """ 

177 starSelectorClass = measAlg.sourceSelectorRegistry["objectSize"] 

178 starSelectorConfig = starSelectorClass.ConfigClass() 

179 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux" 

180 starSelectorConfig.badFlags = [ 

181 "base_PixelFlags_flag_edge", 

182 "base_PixelFlags_flag_interpolatedCenter", 

183 "base_PixelFlags_flag_saturatedCenter", 

184 "base_PixelFlags_flag_crCenter", 

185 ] 

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

187 starSelectorConfig.widthStdAllowed = 0.5 

188 

189 self.starSelector = starSelectorClass(config=starSelectorConfig) 

190 

191 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

192 if stampSize is not None: 

193 makePsfCandidatesConfig.kernelSize = stampSize 

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

195 

196 psfDeterminerConfig = PiffPsfDeterminerConfig() 

197 psfDeterminerConfig.spatialOrder = 1 

198 if stampSize is not None: 

199 psfDeterminerConfig.stampSize = stampSize 

200 psfDeterminerConfig.debugStarData = debugStarData 

201 psfDeterminerConfig.useCoordinates = useCoordinates 

202 

203 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

204 

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

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

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

208 

209 subtracted = mi.Factory(mi, True) 

210 for s in catalog: 

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

212 bbox = subtracted.getBBox(afwImage.PARENT) 

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

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

215 chi = subtracted.Factory(subtracted, True) 

216 var = subtracted.getVariance() 

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

218 chi /= var 

219 

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

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

222 print(chi_min, chi_max) 

223 

224 if chi_lim > 0: 

225 self.assertGreater(chi_min, -chi_lim) 

226 self.assertLess(chi_max, chi_lim) 

227 

228 def checkPiffDeterminer(self, **kwargs): 

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

230 

231 Parameters 

232 ---------- 

233 kwargs : `dict`, optional 

234 Additional keyword arguments to pass to setupDeterminer. 

235 """ 

236 self.setupDeterminer(**kwargs) 

237 metadata = dafBase.PropertyList() 

238 

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

240 psfCandidateList = self.makePsfCandidates.run( 

241 stars.sourceCat, 

242 exposure=self.exposure 

243 ).psfCandidates 

244 psf, cellSet = self.psfDeterminer.determinePsf( 

245 self.exposure, 

246 psfCandidateList, 

247 metadata, 

248 flagKey=self.usePsfFlag 

249 ) 

250 self.exposure.setPsf(psf) 

251 

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

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

254 self.assertEqual( 

255 psf.getAveragePosition(), 

256 geom.Point2D( 

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

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

259 ) 

260 ) 

261 if self.psfDeterminer.config.debugStarData: 

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

263 else: 

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

265 

266 # Test how well we can subtract the PSF model 

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

268 

269 # Test bboxes 

270 for point in [ 

271 psf.getAveragePosition(), 

272 geom.Point2D(), 

273 geom.Point2D(1, 1) 

274 ]: 

275 self.assertEqual( 

276 psf.computeBBox(point), 

277 psf.computeKernelImage(point).getBBox() 

278 ) 

279 self.assertEqual( 

280 psf.computeKernelBBox(point), 

281 psf.computeKernelImage(point).getBBox() 

282 ) 

283 self.assertEqual( 

284 psf.computeImageBBox(point), 

285 psf.computeImage(point).getBBox() 

286 ) 

287 

288 # Some roundtrips 

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

290 self.exposure.writeFits(tmpFile) 

291 fitsIm = afwImage.ExposureF(tmpFile) 

292 copyIm = copy.deepcopy(self.exposure) 

293 

294 for newIm in [fitsIm, copyIm]: 

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

296 # that some PSF images come out the same. 

297 for point in [ 

298 geom.Point2D(0, 0), 

299 geom.Point2D(10, 100), 

300 geom.Point2D(-200, 30), 

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

302 ]: 

303 self.assertImagesAlmostEqual( 

304 psf.computeImage(point), 

305 newIm.getPsf().computeImage(point) 

306 ) 

307 # Also check average position 

308 newPsf = newIm.getPsf() 

309 self.assertImagesAlmostEqual( 

310 psf.computeImage(psf.getAveragePosition()), 

311 newPsf.computeImage(newPsf.getAveragePosition()) 

312 ) 

313 

314 def testPiffDeterminer_default(self): 

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

316 self.checkPiffDeterminer() 

317 

318 def testPiffDeterminer_kernelSize27(self): 

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

320 self.checkPiffDeterminer(stampSize=27) 

321 

322 def testPiffDeterminer_debugStarData(self): 

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

324 self.checkPiffDeterminer(debugStarData=True) 

325 

326 def testPiffDeterminer_skyCoords(self): 

327 """Test Piff sky coords.""" 

328 self.checkPiffDeterminer(useCoordinates='sky') 

329 

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

331 def test_validatePsfCandidates(self, samplingSize): 

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

333 

334 This should be independent of the samplingSize parameter. 

335 """ 

336 drawSizeDict = {1.0: 27, 

337 0.9: 31, 

338 1.1: 25, 

339 } 

340 

341 makePsfCandidatesConfig = measAlg.MakePsfCandidatesTask.ConfigClass() 

342 makePsfCandidatesConfig.kernelSize = 23 

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

344 psfCandidateList = self.makePsfCandidates.run( 

345 self.catalog, 

346 exposure=self.exposure 

347 ).psfCandidates 

348 

349 psfDeterminerConfig = PiffPsfDeterminerConfig() 

350 psfDeterminerConfig.stampSize = drawSizeDict[samplingSize] 

351 psfDeterminerConfig.samplingSize = samplingSize 

352 self.psfDeterminer = PiffPsfDeterminerTask(psfDeterminerConfig) 

353 

354 with self.assertRaisesRegex(RuntimeError, 

355 "stampSize=27 " 

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

357 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 27) 

358 

359 # This should not raise. 

360 self.psfDeterminer._validatePsfCandidates(psfCandidateList, 21) 

361 

362 

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

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

365 def testValidateGalsimInterpolant(self): 

366 # Check that random strings are not valid interpolants. 

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

368 # Check that the Lanczos order is an integer 

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

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

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

372 # Check for various valid Lanczos interpolants 

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

374 self.assertTrue(_validateGalsimInterpolant(interp)) 

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

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

377 self.assertTrue(eval(interp)) 

378 # Check that interpolation methods are case sensitive. 

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

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

381 self.assertFalse(_validateGalsimInterpolant(interp)) 

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

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

384 

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

386 config = PiffPsfDeterminerConfig() 

387 

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

389 config.stampSize = 27 

390 with self.assertWarns(FutureWarning): 

391 config.kernelSize = 25 

392 self.assertRaises(FieldValidationError, config.validate) 

393 

394 # even if they agree with each other 

395 config.stampSize = 31 

396 with self.assertWarns(FutureWarning): 

397 config.kernelSize = 31 

398 self.assertRaises(FieldValidationError, config.validate) 

399 

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

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

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

403 config.stampSize = None 

404 with self.assertWarns(FutureWarning): 

405 config.kernelSize = None 

406 config.validate() 

407 

408 

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

410 pass 

411 

412 

413def setup_module(module): 

414 lsst.utils.tests.init() 

415 

416 

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

418 lsst.utils.tests.init() 

419 unittest.main()