Coverage for tests/test_psfDetermination.py: 15%

280 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-17 02:06 -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 numpy as np 

24import unittest 

25 

26import lsst.geom 

27from lsst.afw.cameraGeom.testUtils import DetectorWrapper 

28import lsst.afw.detection as afwDetection 

29import lsst.afw.geom as afwGeom 

30import lsst.afw.image as afwImage 

31import lsst.afw.math as afwMath 

32import lsst.afw.table as afwTable 

33import lsst.daf.base as dafBase 

34import lsst.afw.display as afwDisplay 

35from lsst.log import Log 

36import lsst.meas.algorithms as measAlg 

37from lsst.meas.algorithms.pcaPsfDeterminer import numCandidatesToReject 

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

39import lsst.meas.base as measBase 

40import lsst.utils.tests 

41 

42try: 

43 type(display) 

44except NameError: 

45 display = False 

46else: 

47 afwDisplay.setDefaultMaskTransparency(75) 

48 

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

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

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

52 

53 

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

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

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

57 centered at (x, y) 

58 """ 

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

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

61 

62 

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

64 """A test case for SpatialModelPsf""" 

65 

66 def measure(self, footprintSet, exposure): 

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

68 table = afwTable.SourceCatalog(self.schema) 

69 footprintSet.makeSources(table) 

70 

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

72 self.measureTask.run(table, exposure) 

73 

74 if display: 

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

76 

77 return table 

78 

79 def setUp(self): 

80 

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

82 config = measBase.SingleFrameMeasurementConfig() 

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

84 "base_SdssCentroid", 

85 "base_GaussianFlux", 

86 "base_SdssShape", 

87 "base_CircularApertureFlux", 

88 "base_PsfFlux", 

89 ] 

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

91 config.slots.centroid = "base_SdssCentroid" 

92 config.slots.psfFlux = "base_PsfFlux" 

93 config.slots.apFlux = "base_CircularApertureFlux_3_0" 

94 config.slots.modelFlux = None 

95 config.slots.gaussianFlux = None 

96 config.slots.calibFlux = None 

97 config.slots.shape = "base_SdssShape" 

98 

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

100 

101 width, height = 110, 301 

102 

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

104 self.mi.set(0) 

105 sd = 3 # standard deviation of image 

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

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

108 

109 self.FWHM = 5 

110 self.ksize = 31 # size of desired kernel 

111 

112 sigma1 = 1.75 

113 sigma2 = 2*sigma1 

114 

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

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

117 1.5*sigma1, 1, 0.1)) 

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

119 

120 # 

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

122 # 

123 basisKernelList = [] 

124 for sigma in (sigma1, sigma2): 

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

126 afwMath.GaussianFunction2D(sigma, sigma)) 

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

128 basisKernel.computeImage(basisImage, True) 

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

130 

131 if sigma == sigma1: 

132 basisImage0 = basisImage 

133 else: 

134 basisImage -= basisImage0 

135 

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

137 

138 order = 1 # 1 => up to linear 

139 spFunc = afwMath.PolynomialFunction2D(order) 

140 

141 exactKernel = afwMath.LinearCombinationKernel(basisKernelList, spFunc) 

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

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

144 self.exactPsf = measAlg.PcaPsf(exactKernel) 

145 

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

147 

148 addNoise = True 

149 

150 if addNoise: 

151 im = self.mi.getImage() 

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

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

154 del im 

155 

156 xarr, yarr = [], [] 

157 

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

159 (30, 35), 

160 (50, 50), 

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

162 (50, 120), (70, 80), 

163 (60, 210), (20, 210), 

164 ]: 

165 xarr.append(x) 

166 yarr.append(y) 

167 

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

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

170 dy = rand.uniform() - 0.5 

171 

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

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

174 

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

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

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

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

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

180 continue 

181 

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

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

184 continue 

185 

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

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

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

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

190 # 

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

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

193 

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

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

196 

197 for source in self.catalog: 

198 try: 

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

200 self.cellSet.insertCandidate(cand) 

201 

202 except Exception as e: 

203 print(e) 

204 continue 

205 

206 def tearDown(self): 

207 del self.cellSet 

208 del self.exposure 

209 del self.mi 

210 del self.exactPsf 

211 del self.footprintSet 

212 del self.catalog 

213 del self.schema 

214 del self.measureTask 

215 

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

217 """Setup the starSelector and psfDeterminer.""" 

218 if exposure is None: 

219 exposure = self.exposure 

220 

221 starSelectorClass = measAlg.sourceSelectorRegistry[starSelectorAlg] 

222 starSelectorConfig = starSelectorClass.ConfigClass() 

223 

224 if starSelectorAlg == "objectSize": 

225 starSelectorConfig.sourceFluxField = "base_GaussianFlux_instFlux" 

226 starSelectorConfig.badFlags = ["base_PixelFlags_flag_edge", 

227 "base_PixelFlags_flag_interpolatedCenter", 

228 "base_PixelFlags_flag_saturatedCenter", 

229 "base_PixelFlags_flag_crCenter", 

230 ] 

231 starSelectorConfig.widthStdAllowed = 0.5 

232 

233 self.starSelector = starSelectorClass(config=starSelectorConfig) 

234 

235 self.makePsfCandidates = measAlg.MakePsfCandidatesTask() 

236 

237 psfDeterminerTask = measAlg.psfDeterminerRegistry["pca"] 

238 psfDeterminerConfig = psfDeterminerTask.ConfigClass() 

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

240 psfDeterminerConfig.sizeCellX = width 

241 psfDeterminerConfig.sizeCellY = height//3 

242 psfDeterminerConfig.nEigenComponents = nEigenComponents 

243 psfDeterminerConfig.spatialOrder = 1 

244 psfDeterminerConfig.kernelSizeMin = 31 

245 psfDeterminerConfig.nStarPerCell = 0 

246 psfDeterminerConfig.nStarPerCellSpatialFit = 0 # unlimited 

247 self.psfDeterminer = psfDeterminerTask(psfDeterminerConfig) 

248 

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

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

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

252 

253 subtracted = mi.Factory(mi, True) 

254 

255 for s in catalog: 

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

257 bbox = subtracted.getBBox() 

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

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

260 

261 chi = subtracted.Factory(subtracted, True) 

262 var = subtracted.getVariance() 

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

264 chi /= var 

265 

266 if display: 

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

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

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

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

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

272 kern = psf.getKernel() 

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

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

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

276 

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

278 if False: 

279 print(chi_min, chi_max) 

280 

281 if chi_lim > 0: 

282 self.assertGreater(chi_min, -chi_lim) 

283 self.assertLess(chi_max, chi_lim) 

284 

285 def testPsfDeterminerObjectSize(self): 

286 self._testPsfDeterminer("objectSize") 

287 

288 def _testPsfDeterminer(self, starSelectorAlg): 

289 self.setupDeterminer(starSelectorAlg=starSelectorAlg) 

290 metadata = dafBase.PropertyList() 

291 

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

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

294 

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

296 self.exposure.setPsf(psf) 

297 

298 chi_lim = 5.0 

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

300 

301 def testPsfDeterminerSubimageObjectSizeStarSelector(self): 

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

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

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

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

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

307 

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

309 metadata = dafBase.PropertyList() 

310 # 

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

312 # 

313 

314 def trimCatalogToImage(exp, catalog): 

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

316 for s in catalog: 

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

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

319 

320 return trimmedCatalog 

321 

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

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

324 

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

326 subExp.setPsf(psf) 

327 

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

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

330 (self.exposure.Factory(self.exposure, 

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

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

333 afwImage.LOCAL), 7.5), 

334 (self.exposure, 19), 

335 ]: 

336 cat = trimCatalogToImage(exp, self.catalog) 

337 exp.setPsf(psf) 

338 self.subtractStars(exp, cat, chi_lim) 

339 

340 def testPsfDeterminerNEigenObjectSizeStarSelector(self): 

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

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

343 metadata = dafBase.PropertyList() 

344 

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

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

347 

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

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

350 

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

352 

353 def testCandidateList(self): 

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

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

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

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

358 

359 stamps = [] 

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

361 for cand in cell: 

362 cand = cell[0] 

363 width, height = 29, 25 

364 cand.setWidth(width) 

365 cand.setHeight(height) 

366 

367 im = cand.getMaskedImage() 

368 stamps.append(im) 

369 

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

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

372 

373 if False and display: 

374 mos = afwDisplay.utils.Mosaic() 

375 mos.makeMosaic(stamps, frame=2) 

376 

377 def testRejectBlends(self): 

378 """Test the PcaPsfDeterminerTask blend removal.""" 

379 """ 

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

381 and check that it barfs in the expected way. 

382 """ 

383 

384 psfDeterminerClass = measAlg.psfDeterminerRegistry["pca"] 

385 config = psfDeterminerClass.ConfigClass() 

386 config.doRejectBlends = True 

387 psfDeterminer = psfDeterminerClass(config=config) 

388 

389 schema = afwTable.SourceTable.makeMinimalSchema() 

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

391 measBase.SingleFrameMeasurementTask(schema) 

392 catalog = afwTable.SourceCatalog(schema) 

393 source = catalog.addNew() 

394 

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

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

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

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

399 foot.addPeak(45, 123, 6) 

400 foot.addPeak(47, 126, 5) 

401 source.setFootprint(foot) 

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

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

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

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

406 

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

408 metadata = dafBase.PropertyList() 

409 

410 with self.assertRaises(RuntimeError) as cm: 

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

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

413 

414 def testShowPsfMosaic(self): 

415 """ Test that the showPsfMosaic function works. 

416 

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

418 """ 

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

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

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

422 

423 def testShowPsf(self): 

424 """ Test that the showPsfMosaic function works. 

425 

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

427 """ 

428 

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

430 self.setupDeterminer() 

431 metadata = dafBase.PropertyList() 

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

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

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

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

436 mos = showPsf(psf, display=testDisplay) 

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

438 

439 

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

441 def testNumToReject(self): 

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

443 # Numerical values correspond to the problem case identified in 

444 # DM-8030. 

445 

446 numBadCandidates = 5 

447 totalIter = 3 

448 

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

450 self.assertEqual(numCandidatesToReject(numBadCandidates, numIter, 

451 totalIter), value) 

452 

453 

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

455 pass 

456 

457 

458def setup_module(module): 

459 lsst.utils.tests.init() 

460 

461 

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

463 lsst.utils.tests.init() 

464 unittest.main()