Coverage for tests/test_psfDetermination.py: 14%

302 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-04-27 03:17 -0700

1# This file is part of meas_algorithms. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22import math 

23import logging 

24import numpy as np 

25import unittest 

26 

27import lsst.geom 

28from lsst.afw.cameraGeom.testUtils import DetectorWrapper 

29import lsst.afw.detection as afwDetection 

30import lsst.afw.geom as afwGeom 

31import lsst.afw.image as afwImage 

32import lsst.afw.math as afwMath 

33import lsst.afw.table as afwTable 

34import lsst.daf.base as dafBase 

35import lsst.afw.display as afwDisplay 

36from lsst.log import Log 

37import lsst.meas.algorithms as measAlg 

38from lsst.meas.algorithms.pcaPsfDeterminer import numCandidatesToReject 

39from lsst.meas.algorithms.utils import showPsfMosaic, showPsf 

40import lsst.meas.base as measBase 

41import lsst.utils.tests 

42 

43try: 

44 type(display) 

45except NameError: 

46 display = False 

47else: 

48 afwDisplay.setDefaultMaskTransparency(75) 

49 

50# Change the level to Log.DEBUG or Log.TRACE to see debug messages 

51Log.getLogger("lsst.measurement").setLevel(Log.INFO) 

52Log.getLogger("lsst.psfDeterminer").setLevel(Log.TRACE) 

53 

54 

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

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

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

58 centered at (x, y) 

59 """ 

60 return (math.exp(-0.5*((ix - x)**2 + (iy - y)**2)/sigma1**2) 

61 + b*math.exp(-0.5*((ix - x)**2 + (iy - y)**2)/sigma2**2))/(1 + b) 

62 

63 

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

65 """A test case for SpatialModelPsf""" 

66 

67 def measure(self, footprintSet, exposure): 

68 """Measure a set of Footprints, returning a SourceCatalog.""" 

69 table = afwTable.SourceCatalog(self.schema) 

70 footprintSet.makeSources(table) 

71 

72 # Then run the default SFM task. Results not checked 

73 self.measureTask.run(table, exposure) 

74 

75 if display: 

76 afwDisplay.Display(frame=1).mtv(exposure, title=self._testMethodName + ": image") 

77 

78 return table 

79 

80 def setUp(self): 

81 

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

83 config = measBase.SingleFrameMeasurementConfig() 

84 config.algorithms.names = ["base_PixelFlags", 

85 "base_SdssCentroid", 

86 "base_GaussianFlux", 

87 "base_SdssShape", 

88 "base_CircularApertureFlux", 

89 "base_PsfFlux", 

90 ] 

91 config.algorithms["base_CircularApertureFlux"].radii = [3.0] 

92 config.slots.centroid = "base_SdssCentroid" 

93 config.slots.psfFlux = "base_PsfFlux" 

94 config.slots.apFlux = "base_CircularApertureFlux_3_0" 

95 config.slots.modelFlux = None 

96 config.slots.gaussianFlux = None 

97 config.slots.calibFlux = None 

98 config.slots.shape = "base_SdssShape" 

99 

100 self.measureTask = measBase.SingleFrameMeasurementTask(self.schema, config=config) 

101 

102 width, height = 110, 301 

103 

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

105 self.mi.set(0) 

106 sd = 3 # standard deviation of image 

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

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

109 

110 self.FWHM = 5 

111 self.ksize = 31 # size of desired kernel 

112 

113 sigma1 = 1.75 

114 sigma2 = 2*sigma1 

115 

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

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

118 1.5*sigma1, 1, 0.1)) 

119 self.exposure.setDetector(DetectorWrapper().detector) 

120 

121 # 

122 # Make a kernel with the exactly correct basis functions. Useful for debugging 

123 # 

124 basisKernelList = [] 

125 for sigma in (sigma1, sigma2): 

126 basisKernel = afwMath.AnalyticKernel(self.ksize, self.ksize, 

127 afwMath.GaussianFunction2D(sigma, sigma)) 

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

129 basisKernel.computeImage(basisImage, True) 

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

131 

132 if sigma == sigma1: 

133 basisImage0 = basisImage 

134 else: 

135 basisImage -= basisImage0 

136 

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

138 

139 order = 1 # 1 => up to linear 

140 spFunc = afwMath.PolynomialFunction2D(order) 

141 

142 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc) 

143 exactKernel.setSpatialParameters([[1.0, 0, 0], 

144 [0.0, 0.5*1e-2, 0.2e-2]]) 

145 self.exactPsf = measAlg.PcaPsf(exactKernel) 

146 

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

148 

149 addNoise = True 

150 

151 if addNoise: 

152 im = self.mi.getImage() 

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

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

155 del im 

156 

157 xarr, yarr = [], [] 

158 

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

160 (30, 35), 

161 (50, 50), 

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

163 (50, 120), (70, 80), 

164 (60, 210), (20, 210), 

165 ]: 

166 xarr.append(x) 

167 yarr.append(y) 

168 

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

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

171 dy = rand.uniform() - 0.5 

172 

173 k = exactKernel.getSpatialFunction(1)(x, y) # functional variation of Kernel ... 

174 b = (k*sigma1**2/((1 - k)*sigma2**2)) # ... converted double Gaussian's "b" 

175 

176 # flux = 80000 - 20*x - 10*(y/float(height))**2 

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

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

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

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

181 continue 

182 

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

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

185 continue 

186 

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

188 Isample = rand.poisson(intensity) if addNoise else intensity 

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

190 self.mi.variance[ix, iy, afwImage.LOCAL] += intensity 

191 # 

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

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

194 

195 self.footprintSet = afwDetection.FootprintSet(self.mi, afwDetection.Threshold(100), "DETECTED") 

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

197 

198 for source in self.catalog: 

199 try: 

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

201 self.cellSet.insertCandidate(cand) 

202 

203 except Exception as e: 

204 print(e) 

205 continue 

206 

207 def tearDown(self): 

208 del self.cellSet 

209 del self.exposure 

210 del self.mi 

211 del self.exactPsf 

212 del self.footprintSet 

213 del self.catalog 

214 del self.schema 

215 del self.measureTask 

216 

217 def setupDeterminer(self, exposure=None, nEigenComponents=2, starSelectorAlg="objectSize"): 

218 """Setup the starSelector and psfDeterminer.""" 

219 if exposure is None: 

220 exposure = self.exposure 

221 

222 starSelectorClass = measAlg.sourceSelectorRegistry[starSelectorAlg] 

223 starSelectorConfig = starSelectorClass.ConfigClass() 

224 

225 if starSelectorAlg == "objectSize": 

226 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux" 

227 starSelectorConfig.badFlags = ["base_PixelFlags_flag_edge", 

228 "base_PixelFlags_flag_interpolatedCenter", 

229 "base_PixelFlags_flag_saturatedCenter", 

230 "base_PixelFlags_flag_crCenter", 

231 ] 

232 starSelectorConfig.widthStdAllowed = 0.5 

233 

234 self.starSelector = starSelectorClass(config=starSelectorConfig) 

235 

236 self.makePsfCandidates = measAlg.MakePsfCandidatesTask() 

237 

238 psfDeterminerTask = measAlg.psfDeterminerRegistry["pca"] 

239 psfDeterminerConfig = psfDeterminerTask.ConfigClass() 

240 width, height = exposure.getMaskedImage().getDimensions() 

241 psfDeterminerConfig.sizeCellX = width 

242 psfDeterminerConfig.sizeCellY = height//3 

243 psfDeterminerConfig.nEigenComponents = nEigenComponents 

244 psfDeterminerConfig.spatialOrder = 1 

245 psfDeterminerConfig.stampSize = 31 

246 psfDeterminerConfig.nStarPerCell = 0 

247 psfDeterminerConfig.nStarPerCellSpatialFit = 0 # unlimited 

248 self.psfDeterminer = psfDeterminerTask(psfDeterminerConfig) 

249 

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

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

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

253 

254 subtracted = mi.Factory(mi, True) 

255 

256 for s in catalog: 

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

258 bbox = subtracted.getBBox() 

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

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

261 

262 chi = subtracted.Factory(subtracted, True) 

263 var = subtracted.getVariance() 

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

265 chi /= var 

266 

267 if display: 

268 afwDisplay.Display(frame=0).mtv(subtracted, title=self._testMethodName + ": Subtracted") 

269 afwDisplay.Display(frame=2).mtv(chi, title=self._testMethodName + ": Chi") 

270 afwDisplay.Display(frame=3).mtv(psf.computeImage(lsst.geom.Point2D(xc, yc)), 

271 title=self._testMethodName + ": Psf") 

272 afwDisplay.Display(frame=4).mtv(mi, title=self._testMethodName + ": orig") 

273 kern = psf.getKernel() 

274 kimg = afwImage.ImageD(kern.getWidth(), kern.getHeight()) 

275 kern.computeImage(kimg, True, xc, yc) 

276 afwDisplay.Display(frame=5).mtv(kimg, title=self._testMethodName + ": kernel") 

277 

278 chi_min, chi_max = np.min(chi.getImage().getArray()), np.max(chi.getImage().getArray()) 

279 if False: 

280 print(chi_min, chi_max) 

281 

282 if chi_lim > 0: 

283 self.assertGreater(chi_min, -chi_lim) 

284 self.assertLess(chi_max, chi_lim) 

285 

286 def testPsfDeterminerObjectSize(self): 

287 self._testPsfDeterminer("objectSize") 

288 

289 def _testPsfDeterminer(self, starSelectorAlg): 

290 self.setupDeterminer(starSelectorAlg=starSelectorAlg) 

291 metadata = dafBase.PropertyList() 

292 

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

294 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, self.exposure).psfCandidates 

295 

296 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata) 

297 self.exposure.setPsf(psf) 

298 

299 chi_lim = 5.0 

300 self.subtractStars(self.exposure, self.catalog, chi_lim) 

301 

302 def testPsfDeterminerSubimageObjectSizeStarSelector(self): 

303 """Test the (PCA) psfDeterminer on subImages.""" 

304 w, h = self.exposure.getDimensions() 

305 x0, y0 = int(0.35*w), int(0.45*h) 

306 bbox = lsst.geom.BoxI(lsst.geom.PointI(x0, y0), lsst.geom.ExtentI(w-x0, h-y0)) 

307 subExp = self.exposure.Factory(self.exposure, bbox, afwImage.LOCAL) 

308 

309 self.setupDeterminer(subExp, starSelectorAlg="objectSize") 

310 metadata = dafBase.PropertyList() 

311 # 

312 # Only keep the sources that lie within the subregion (avoiding lots of log messages) 

313 # 

314 

315 def trimCatalogToImage(exp, catalog): 

316 trimmedCatalog = afwTable.SourceCatalog(catalog.table.clone()) 

317 for s in catalog: 

318 if exp.getBBox().contains(lsst.geom.PointI(s.getCentroid())): 

319 trimmedCatalog.append(trimmedCatalog.table.copyRecord(s)) 

320 

321 return trimmedCatalog 

322 

323 stars = self.starSelector.run(trimCatalogToImage(subExp, self.catalog), exposure=subExp) 

324 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, subExp).psfCandidates 

325 

326 psf, cellSet = self.psfDeterminer.determinePsf(subExp, psfCandidateList, metadata) 

327 subExp.setPsf(psf) 

328 

329 # Test how well we can subtract the PSF model. N.b. using self.exposure is an extrapolation 

330 for exp, chi_lim in [(subExp, 4.5), 

331 (self.exposure.Factory(self.exposure, 

332 lsst.geom.BoxI(lsst.geom.PointI(0, 100), 

333 (lsst.geom.PointI(w-1, h-1))), 

334 afwImage.LOCAL), 7.5), 

335 (self.exposure, 19), 

336 ]: 

337 cat = trimCatalogToImage(exp, self.catalog) 

338 exp.setPsf(psf) 

339 self.subtractStars(exp, cat, chi_lim) 

340 

341 def testPsfDeterminerNEigenObjectSizeStarSelector(self): 

342 """Test the (PCA) psfDeterminer when you ask for more components than acceptable stars.""" 

343 self.setupDeterminer(nEigenComponents=3, starSelectorAlg="objectSize") 

344 metadata = dafBase.PropertyList() 

345 

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

347 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, self.exposure).psfCandidates 

348 

349 psfCandidateList, nEigen = psfCandidateList[0:4], 2 # only enough stars for 2 eigen-components 

350 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata) 

351 

352 self.assertEqual(psf.getKernel().getNKernelParameters(), nEigen) 

353 

354 def testCandidateList(self): 

355 self.assertFalse(self.cellSet.getCellList()[0].empty()) 

356 self.assertTrue(self.cellSet.getCellList()[1].empty()) 

357 self.assertFalse(self.cellSet.getCellList()[2].empty()) 

358 self.assertTrue(self.cellSet.getCellList()[3].empty()) 

359 

360 stamps = [] 

361 for cell in self.cellSet.getCellList(): 

362 for cand in cell: 

363 cand = cell[0] 

364 width, height = 29, 25 

365 cand.setWidth(width) 

366 cand.setHeight(height) 

367 

368 im = cand.getMaskedImage() 

369 stamps.append(im) 

370 

371 self.assertEqual(im.getWidth(), width) 

372 self.assertEqual(im.getHeight(), height) 

373 

374 if False and display: 

375 mos = afwDisplay.utils.Mosaic() 

376 mos.makeMosaic(stamps, frame=2) 

377 

378 def testRejectBlends(self): 

379 """Test the PcaPsfDeterminerTask blend removal.""" 

380 """ 

381 We give it a single blended source, asking it to remove blends, 

382 and check that it barfs in the expected way. 

383 """ 

384 

385 psfDeterminerClass = measAlg.psfDeterminerRegistry["pca"] 

386 config = psfDeterminerClass.ConfigClass() 

387 config.doRejectBlends = True 

388 psfDeterminer = psfDeterminerClass(config=config) 

389 

390 schema = afwTable.SourceTable.makeMinimalSchema() 

391 # Use The single frame measurement task to populate the schema with standard keys 

392 measBase.SingleFrameMeasurementTask(schema) 

393 catalog = afwTable.SourceCatalog(schema) 

394 source = catalog.addNew() 

395 

396 # Make the source blended, with necessary information to calculate pca 

397 spanShift = lsst.geom.Point2I(54, 123) 

398 spans = afwGeom.SpanSet.fromShape(6, offset=spanShift) 

399 foot = afwDetection.Footprint(spans, self.exposure.getBBox()) 

400 foot.addPeak(45, 123, 6) 

401 foot.addPeak(47, 126, 5) 

402 source.setFootprint(foot) 

403 centerKey = afwTable.Point2DKey(source.schema['slot_Centroid']) 

404 shapeKey = afwTable.QuadrupoleKey(schema['slot_Shape']) 

405 source.set(centerKey, lsst.geom.Point2D(46, 124)) 

406 source.set(shapeKey, afwGeom.Quadrupole(1.1, 2.2, 1)) 

407 

408 candidates = [measAlg.makePsfCandidate(source, self.exposure)] 

409 metadata = dafBase.PropertyList() 

410 

411 with self.assertRaises(RuntimeError) as cm: 

412 psfDeterminer.determinePsf(self.exposure, candidates, metadata) 

413 self.assertEqual(str(cm.exception), "All PSF candidates removed as blends") 

414 

415 def testShowPsfMosaic(self): 

416 """ Test that the showPsfMosaic function works. 

417 

418 This function is usually called without display=None, which would activate ds9 

419 """ 

420 testDisplay = display if display else afwDisplay.getDisplay(backend="virtualDevice") 

421 mos = showPsfMosaic(self.exposure, showEllipticity=True, showFwhm=True, display=testDisplay) 

422 self.assertTrue(len(mos.images) > 0) 

423 

424 def testShowPsf(self): 

425 """ Test that the showPsfMosaic function works. 

426 

427 This function is usually called without display=None, which would activate ds9 

428 """ 

429 

430 # Measure PSF so we have a real PSF to work with 

431 self.setupDeterminer() 

432 metadata = dafBase.PropertyList() 

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

434 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, self.exposure).psfCandidates 

435 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata) 

436 testDisplay = display if display else afwDisplay.getDisplay(backend="virtualDevice") 

437 mos = showPsf(psf, display=testDisplay) 

438 self.assertTrue(len(mos.images) > 0) 

439 

440 def testDownsampleBase(self): 

441 """Test that the downsampleCandidates function works. 

442 """ 

443 self.setupDeterminer() 

444 

445 # Note that the downsampleCandidates function is designed to work 

446 # with a list of psf candidates, it can work with any list. 

447 # For these tests we use a list of integers which allows easier 

448 # testing that the sort order is maintained. 

449 

450 # Try with no downsampling. 

451 inputList = list(np.arange(100)) 

452 candidateList = self.psfDeterminer.downsampleCandidates(inputList) 

453 np.testing.assert_array_equal(candidateList, inputList) 

454 

455 # And with downsampling. 

456 inputList = list(np.arange(500)) 

457 with self.assertLogs(level=logging.INFO) as cm: 

458 candidateList = self.psfDeterminer.downsampleCandidates(inputList) 

459 self.assertIn("Down-sampling from 500 to 300 psf candidates.", cm[0][0].message) 

460 self.assertEqual(len(candidateList), self.psfDeterminer.config.maxCandidates) 

461 np.testing.assert_array_equal(np.sort(candidateList), candidateList) 

462 self.assertEqual(len(np.unique(candidateList)), len(candidateList)) 

463 

464 def testDownsamplePca(self): 

465 """Test PCA determiner with downsampling. 

466 """ 

467 self.setupDeterminer() 

468 metadata = dafBase.PropertyList() 

469 

470 # Decrease the maximum number of stars. 

471 self.psfDeterminer.config.maxCandidates = 10 

472 

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

474 psfCandidateList = self.makePsfCandidates.run(stars.sourceCat, self.exposure).psfCandidates 

475 psf, cellSet = self.psfDeterminer.determinePsf(self.exposure, psfCandidateList, metadata) 

476 

477 self.assertEqual(metadata['numAvailStars'], self.psfDeterminer.config.maxCandidates) 

478 self.assertLessEqual(metadata['numGoodStars'], self.psfDeterminer.config.maxCandidates) 

479 

480 

481class PsfCandidateTestCase(lsst.utils.tests.TestCase): 

482 def testNumToReject(self): 

483 """Reject the correct number of PSF candidates on each iteration""" 

484 # Numerical values correspond to the problem case identified in 

485 # DM-8030. 

486 

487 numBadCandidates = 5 

488 totalIter = 3 

489 

490 for numIter, value in [(0, 1), (1, 3), (2, 5)]: 

491 self.assertEqual(numCandidatesToReject(numBadCandidates, numIter, 

492 totalIter), value) 

493 

494 

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

496 pass 

497 

498 

499def setup_module(module): 

500 lsst.utils.tests.init() 

501 

502 

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

504 lsst.utils.tests.init() 

505 unittest.main()