Coverage for python/lsst/meas/algorithms/pcaPsfDeterminer.py: 9%

344 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-08 10:19 +0000

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 

22__all__ = ["PcaPsfDeterminerConfig", "PcaPsfDeterminerTask"] 

23 

24import math 

25import sys 

26 

27import numpy 

28 

29import lsst.pex.config as pexConfig 

30import lsst.pex.exceptions as pexExceptions 

31import lsst.geom 

32import lsst.afw.geom as afwGeom 

33import lsst.afw.geom.ellipses as afwEll 

34import lsst.afw.display as afwDisplay 

35import lsst.afw.math as afwMath 

36from .psfDeterminer import BasePsfDeterminerTask, psfDeterminerRegistry 

37from .psfCandidate import PsfCandidateF 

38from .spatialModelPsf import createKernelFromPsfCandidates, countPsfCandidates, \ 

39 fitSpatialKernelFromPsfCandidates, fitKernelParamsToImage 

40from .pcaPsf import PcaPsf 

41from . import utils 

42 

43 

44def numCandidatesToReject(numBadCandidates, numIter, totalIter): 

45 """Return the number of PSF candidates to be rejected. 

46 

47 The number of candidates being rejected on each iteration gradually 

48 increases, so that on the Nth of M iterations we reject N/M of the bad 

49 candidates. 

50 

51 Parameters 

52 ---------- 

53 numBadCandidates : `int` 

54 Number of bad candidates under consideration. 

55 

56 numIter : `int` 

57 The number of the current PSF iteration. 

58 

59 totalIter : `int` 

60 The total number of PSF iterations. 

61 

62 Returns 

63 ------- 

64 return : `int` 

65 Number of candidates to reject. 

66 """ 

67 return int(numBadCandidates*(numIter + 1)//totalIter + 0.5) 

68 

69 

70class PcaPsfDeterminerConfig(BasePsfDeterminerTask.ConfigClass): 

71 nonLinearSpatialFit = pexConfig.Field[bool]( 

72 doc="Use non-linear fitter for spatial variation of Kernel", 

73 default=False, 

74 ) 

75 nEigenComponents = pexConfig.Field[int]( 75 ↛ exitline 75 didn't jump to the function exit

76 doc="number of eigen components for PSF kernel creation", 

77 default=4, 

78 check=lambda x: x >= 1 

79 ) 

80 spatialOrder = pexConfig.Field[int]( 

81 doc="specify spatial order for PSF kernel creation", 

82 default=2, 

83 ) 

84 sizeCellX = pexConfig.Field[int]( 84 ↛ exitline 84 didn't jump to the function exit

85 doc="size of cell used to determine PSF (pixels, column direction)", 

86 default=256, 

87 # minValue = 10, 

88 check=lambda x: x >= 10, 

89 ) 

90 sizeCellY = pexConfig.Field[int]( 90 ↛ exitline 90 didn't jump to the function exit

91 doc="size of cell used to determine PSF (pixels, row direction)", 

92 default=sizeCellX.default, 

93 # minValue = 10, 

94 check=lambda x: x >= 10, 

95 ) 

96 nStarPerCell = pexConfig.Field[int]( 

97 doc="number of stars per psf cell for PSF kernel creation", 

98 default=3, 

99 ) 

100 borderWidth = pexConfig.Field[int]( 

101 doc="Number of pixels to ignore around the edge of PSF candidate postage stamps", 

102 default=0, 

103 ) 

104 nStarPerCellSpatialFit = pexConfig.Field[int]( 

105 doc="number of stars per psf Cell for spatial fitting", 

106 default=5, 

107 ) 

108 constantWeight = pexConfig.Field[bool]( 

109 doc="Should each PSF candidate be given the same weight, independent of magnitude?", 

110 default=True, 

111 ) 

112 nIterForPsf = pexConfig.Field[int]( 

113 doc="number of iterations of PSF candidate star list", 

114 default=3, 

115 ) 

116 tolerance = pexConfig.Field[float]( 

117 doc="tolerance of spatial fitting", 

118 default=1e-2, 

119 ) 

120 lam = pexConfig.Field[float]( 

121 doc="floor for variance is lam*data", 

122 default=0.05, 

123 ) 

124 reducedChi2ForPsfCandidates = pexConfig.Field[float]( 

125 doc="for psf candidate evaluation", 

126 default=2.0, 

127 ) 

128 spatialReject = pexConfig.Field[float]( 

129 doc="Rejection threshold (stdev) for candidates based on spatial fit", 

130 default=3.0, 

131 ) 

132 pixelThreshold = pexConfig.Field[float]( 

133 doc="Threshold (stdev) for rejecting extraneous pixels around candidate; applied if positive", 

134 default=0.0, 

135 ) 

136 doRejectBlends = pexConfig.Field[bool]( 

137 doc="Reject candidates that are blended?", 

138 default=False, 

139 ) 

140 doMaskBlends = pexConfig.Field[bool]( 

141 doc="Mask blends in image?", 

142 default=True, 

143 ) 

144 

145 def setDefaults(self): 

146 super().setDefaults() 

147 self.stampSize = 41 

148 

149 

150class PcaPsfDeterminerTask(BasePsfDeterminerTask): 

151 """A measurePsfTask psf estimator. 

152 """ 

153 ConfigClass = PcaPsfDeterminerConfig 

154 

155 def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents): 

156 PsfCandidateF.setPixelThreshold(self.config.pixelThreshold) 

157 PsfCandidateF.setMaskBlends(self.config.doMaskBlends) 

158 # 

159 # Loop trying to use nEigenComponents, but allowing smaller numbers if necessary 

160 # 

161 for nEigen in range(nEigenComponents, 0, -1): 

162 # Determine KL components 

163 try: 

164 kernel, eigenValues = createKernelFromPsfCandidates( 

165 psfCellSet, exposure.getDimensions(), exposure.getXY0(), nEigen, 

166 self.config.spatialOrder, kernelSize, self.config.nStarPerCell, 

167 bool(self.config.constantWeight)) 

168 

169 break # OK, we can get nEigen components 

170 except pexExceptions.LengthError as e: 

171 if nEigen == 1: # can't go any lower 

172 raise IndexError("No viable PSF candidates survive") 

173 

174 self.log.warning("%s: reducing number of eigen components", e.what()) 

175 # 

176 # We got our eigen decomposition so let's use it 

177 # 

178 # Express eigenValues in units of reduced chi^2 per star 

179 size = kernelSize + 2*self.config.borderWidth 

180 nu = size*size - 1 # number of degrees of freedom/star for chi^2 

181 eigenValues = [val/float(countPsfCandidates(psfCellSet, self.config.nStarPerCell)*nu) 

182 for val in eigenValues] 

183 

184 # Fit spatial model 

185 status, chi2 = fitSpatialKernelFromPsfCandidates( 

186 kernel, psfCellSet, bool(self.config.nonLinearSpatialFit), 

187 self.config.nStarPerCellSpatialFit, self.config.tolerance, self.config.lam) 

188 

189 psf = PcaPsf(kernel) 

190 

191 return psf, eigenValues, nEigen, chi2 

192 

193 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None): 

194 """Determine a PCA PSF model for an exposure given a list of PSF candidates. 

195 

196 Parameters 

197 ---------- 

198 exposure : `lsst.afw.image.Exposure` 

199 Exposure containing the psf candidates. 

200 psfCandidateList : `list` of `lsst.meas.algorithms.PsfCandidate` 

201 A sequence of PSF candidates typically obtained by detecting sources 

202 and then running them through a star selector. 

203 metadata : `lsst.daf.base import PropertyList` or `None`, optional 

204 A home for interesting tidbits of information. 

205 flagKey : `str`, optional 

206 Schema key used to mark sources actually used in PSF determination. 

207 

208 Returns 

209 ------- 

210 psf : `lsst.meas.algorithms.PcaPsf` 

211 The measured PSF. 

212 psfCellSet : `lsst.afw.math.SpatialCellSet` 

213 The PSF candidates. 

214 """ 

215 import lsstDebug 

216 display = lsstDebug.Info(__name__).display 

217 displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells 

218 displayPsfCandidates = lsstDebug.Info(__name__).displayPsfCandidates # show the viable candidates 

219 displayIterations = lsstDebug.Info(__name__).displayIterations # display on each PSF iteration 

220 displayPsfComponents = lsstDebug.Info(__name__).displayPsfComponents # show the PCA components 

221 displayResiduals = lsstDebug.Info(__name__).displayResiduals # show residuals 

222 displayPsfMosaic = lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y) 

223 # match Kernel amplitudes for spatial plots 

224 matchKernelAmplitudes = lsstDebug.Info(__name__).matchKernelAmplitudes 

225 # Keep matplotlib alive post mortem 

226 keepMatplotlibPlots = lsstDebug.Info(__name__).keepMatplotlibPlots 

227 displayPsfSpatialModel = lsstDebug.Info(__name__).displayPsfSpatialModel # Plot spatial model? 

228 showBadCandidates = lsstDebug.Info(__name__).showBadCandidates # Include bad candidates 

229 # Normalize residuals by object amplitude 

230 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals 

231 pause = lsstDebug.Info(__name__).pause # Prompt user after each iteration? 

232 

233 if display: 

234 afwDisplay.setDefaultMaskTransparency(75) 

235 if display > 1: 

236 pause = True 

237 

238 mi = exposure.getMaskedImage() 

239 

240 if len(psfCandidateList) == 0: 

241 raise RuntimeError("No PSF candidates supplied.") 

242 

243 # construct and populate a spatial cell set 

244 bbox = mi.getBBox() 

245 psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY) 

246 sizes = [] 

247 for i, psfCandidate in enumerate(psfCandidateList): 

248 if psfCandidate.getSource().getPsfFluxFlag(): # bad measurement 

249 continue 

250 

251 try: 

252 psfCellSet.insertCandidate(psfCandidate) 

253 except Exception as e: 

254 self.log.debug("Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e) 

255 continue 

256 source = psfCandidate.getSource() 

257 

258 quad = afwGeom.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy()) 

259 axes = afwEll.Axes(quad) 

260 sizes.append(axes.getA()) 

261 if len(sizes) == 0: 

262 raise RuntimeError("No usable PSF candidates supplied") 

263 nEigenComponents = self.config.nEigenComponents # initial version 

264 

265 # TODO: DM-36311: Keep only the if block below. 

266 if self.config.stampSize: 

267 actualKernelSize = int(self.config.stampSize) 

268 elif self.config.kernelSize >= 15: 

269 self.log.warning("NOT scaling kernelSize by stellar quadrupole moment " 

270 "because config.kernelSize=%s >= 15; " 

271 "using config.kernelSize as the width, instead", 

272 self.config.kernelSize) 

273 actualKernelSize = int(self.config.kernelSize) 

274 else: 

275 self.log.warning("scaling kernelSize by stellar quadrupole moment " 

276 "because config.kernelSize=%s < 15. This behavior is deprecated.", 

277 self.config.kernelSize) 

278 medSize = numpy.median(sizes) 

279 actualKernelSize = 2*int(self.config.kernelSize*math.sqrt(medSize) + 0.5) + 1 

280 if actualKernelSize < self.config.kernelSizeMin: 

281 actualKernelSize = self.config.kernelSizeMin 

282 if actualKernelSize > self.config.kernelSizeMax: 

283 actualKernelSize = self.config.kernelSizeMax 

284 

285 if display: 

286 print("Median size=%s" % (medSize,)) 

287 self.log.trace("Kernel size=%s", actualKernelSize) 

288 

289 if actualKernelSize > psfCandidateList[0].getWidth(): 

290 self.log.warning("Using a region (%d x %d) larger than kernelSize (%d) set while making PSF " 

291 "candidates. Consider setting a larger value for kernelSize for " 

292 "`makePsfCandidates` to avoid this warning.", 

293 actualKernelSize, actualKernelSize, psfCandidateList[0].getWidth()) 

294 

295 if self.config.doRejectBlends: 

296 # Remove blended candidates completely 

297 blendedCandidates = [] # Candidates to remove; can't do it while iterating 

298 for cell, cand in candidatesIter(psfCellSet, False): 

299 if len(cand.getSource().getFootprint().getPeaks()) > 1: 

300 blendedCandidates.append((cell, cand)) 

301 continue 

302 if display: 

303 print("Removing %d blended Psf candidates" % len(blendedCandidates)) 

304 for cell, cand in blendedCandidates: 

305 cell.removeCandidate(cand) 

306 if sum(1 for cand in candidatesIter(psfCellSet, False)) == 0: 

307 raise RuntimeError("All PSF candidates removed as blends") 

308 

309 if display: 

310 if displayExposure: 

311 disp = afwDisplay.Display(frame=0) 

312 disp.mtv(exposure, title="psf determination") 

313 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, symb="o", 

314 ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW, 

315 size=4, display=disp) 

316 

317 # 

318 # Do a PCA decomposition of those PSF candidates 

319 # 

320 reply = "y" # used in interactive mode 

321 for iterNum in range(self.config.nIterForPsf): 

322 if display and displayPsfCandidates: # Show a mosaic of usable PSF candidates 

323 

324 stamps = [] 

325 for cell in psfCellSet.getCellList(): 

326 for cand in cell.begin(not showBadCandidates): # maybe include bad candidates 

327 try: 

328 im = cand.getMaskedImage() 

329 

330 chi2 = cand.getChi2() 

331 if chi2 > 1e100: 

332 chi2 = numpy.nan 

333 

334 stamps.append((im, "%d%s" % 

335 (utils.splitId(cand.getSource().getId(), True)["objId"], chi2), 

336 cand.getStatus())) 

337 except Exception: 

338 continue 

339 

340 if len(stamps) == 0: 

341 print("WARNING: No PSF candidates to show; try setting showBadCandidates=True") 

342 else: 

343 mos = afwDisplay.utils.Mosaic() 

344 for im, label, status in stamps: 

345 im = type(im)(im, True) 

346 try: 

347 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue() 

348 except NotImplementedError: 

349 pass 

350 

351 mos.append(im, label, 

352 (afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else 

353 afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else 

354 afwDisplay.RED)) 

355 

356 disp8 = afwDisplay.Display(frame=8) 

357 mos.makeMosaic(display=disp8, title="Psf Candidates") 

358 

359 # Re-fit until we don't have any candidates with naughty chi^2 values influencing the fit 

360 cleanChi2 = False # Any naughty (negative/NAN) chi^2 values? 

361 while not cleanChi2: 

362 cleanChi2 = True 

363 # 

364 # First, estimate the PSF 

365 # 

366 psf, eigenValues, nEigenComponents, fitChi2 = \ 

367 self._fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents) 

368 # 

369 # In clipping, allow all candidates to be innocent until proven guilty on this iteration. 

370 # Throw out any prima facie guilty candidates (naughty chi^2 values) 

371 # 

372 for cell in psfCellSet.getCellList(): 

373 awfulCandidates = [] 

374 for cand in cell.begin(False): # include bad candidates 

375 cand.setStatus(afwMath.SpatialCellCandidate.UNKNOWN) # until proven guilty 

376 rchi2 = cand.getChi2() 

377 if not numpy.isfinite(rchi2) or rchi2 <= 0: 

378 # Guilty prima facie 

379 awfulCandidates.append(cand) 

380 cleanChi2 = False 

381 self.log.debug("chi^2=%s; id=%s", 

382 cand.getChi2(), cand.getSource().getId()) 

383 for cand in awfulCandidates: 

384 if display: 

385 print("Removing bad candidate: id=%d, chi^2=%f" % 

386 (cand.getSource().getId(), cand.getChi2())) 

387 cell.removeCandidate(cand) 

388 

389 # 

390 # Clip out bad fits based on reduced chi^2 

391 # 

392 badCandidates = list() 

393 for cell in psfCellSet.getCellList(): 

394 for cand in cell.begin(False): # include bad candidates 

395 rchi2 = cand.getChi2() # reduced chi^2 when fitting PSF to candidate 

396 assert rchi2 > 0 

397 if rchi2 > self.config.reducedChi2ForPsfCandidates: 

398 badCandidates.append(cand) 

399 

400 badCandidates.sort(key=lambda x: x.getChi2(), reverse=True) 

401 numBad = numCandidatesToReject(len(badCandidates), iterNum, 

402 self.config.nIterForPsf) 

403 for i, c in zip(range(numBad), badCandidates): 

404 if display: 

405 chi2 = c.getChi2() 

406 if chi2 > 1e100: 

407 chi2 = numpy.nan 

408 

409 print("Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2)) 

410 c.setStatus(afwMath.SpatialCellCandidate.BAD) 

411 

412 # 

413 # Clip out bad fits based on spatial fitting. 

414 # 

415 # This appears to be better at getting rid of sources that have a single dominant kernel component 

416 # (other than the zeroth; e.g., a nearby contaminant) because the surrounding sources (which help 

417 # set the spatial model) don't contain that kernel component, and so the spatial modeling 

418 # downweights the component. 

419 # 

420 

421 residuals = list() 

422 candidates = list() 

423 kernel = psf.getKernel() 

424 noSpatialKernel = psf.getKernel() 

425 for cell in psfCellSet.getCellList(): 

426 for cand in cell.begin(False): 

427 candCenter = lsst.geom.PointD(cand.getXCenter(), cand.getYCenter()) 

428 try: 

429 im = cand.getMaskedImage(actualKernelSize, actualKernelSize) 

430 except Exception: 

431 continue 

432 

433 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter) 

434 params = fit[0] 

435 kernels = fit[1] 

436 amp = 0.0 

437 for p, k in zip(params, kernels): 

438 amp += p*k.getSum() 

439 

440 predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY()) for 

441 k in range(kernel.getNKernelParameters())] 

442 

443 residuals.append([a/amp - p for a, p in zip(params, predict)]) 

444 candidates.append(cand) 

445 

446 residuals = numpy.array(residuals) 

447 

448 for k in range(kernel.getNKernelParameters()): 

449 if False: 

450 # Straight standard deviation 

451 mean = residuals[:, k].mean() 

452 rms = residuals[:, k].std() 

453 elif False: 

454 # Using interquartile range 

455 sr = numpy.sort(residuals[:, k]) 

456 mean = (sr[int(0.5*len(sr))] if len(sr)%2 else 

457 0.5*(sr[int(0.5*len(sr))] + sr[int(0.5*len(sr)) + 1])) 

458 rms = 0.74*(sr[int(0.75*len(sr))] - sr[int(0.25*len(sr))]) 

459 else: 

460 stats = afwMath.makeStatistics(residuals[:, k], afwMath.MEANCLIP | afwMath.STDEVCLIP) 

461 mean = stats.getValue(afwMath.MEANCLIP) 

462 rms = stats.getValue(afwMath.STDEVCLIP) 

463 

464 rms = max(1.0e-4, rms) # Don't trust RMS below this due to numerical issues 

465 

466 if display: 

467 print("Mean for component %d is %f" % (k, mean)) 

468 print("RMS for component %d is %f" % (k, rms)) 

469 badCandidates = list() 

470 for i, cand in enumerate(candidates): 

471 if numpy.fabs(residuals[i, k] - mean) > self.config.spatialReject*rms: 

472 badCandidates.append(i) 

473 

474 badCandidates.sort(key=lambda x: numpy.fabs(residuals[x, k] - mean), reverse=True) 

475 

476 numBad = numCandidatesToReject(len(badCandidates), iterNum, 

477 self.config.nIterForPsf) 

478 

479 for i, c in zip(range(min(len(badCandidates), numBad)), badCandidates): 

480 cand = candidates[c] 

481 if display: 

482 print("Spatial clipping %d (%f,%f) based on %d: %f vs %f" % 

483 (cand.getSource().getId(), cand.getXCenter(), cand.getYCenter(), k, 

484 residuals[badCandidates[i], k], self.config.spatialReject*rms)) 

485 cand.setStatus(afwMath.SpatialCellCandidate.BAD) 

486 

487 # 

488 # Display results 

489 # 

490 if display and displayIterations: 

491 if displayExposure: 

492 if iterNum > 0: 

493 disp.erase() 

494 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=True, 

495 symb="o", size=8, display=disp, ctype=afwDisplay.YELLOW, 

496 ctypeBad=afwDisplay.RED, ctypeUnused=afwDisplay.MAGENTA) 

497 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell: 

498 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit, 

499 symb="o", size=10, display=disp, 

500 ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED) 

501 if displayResiduals: 

502 while True: 

503 try: 

504 disp4 = afwDisplay.Display(frame=4) 

505 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4, 

506 normalize=normalizeResiduals, 

507 showBadCandidates=showBadCandidates) 

508 disp5 = afwDisplay.Display(frame=5) 

509 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp5, 

510 normalize=normalizeResiduals, 

511 showBadCandidates=showBadCandidates, 

512 variance=True) 

513 except Exception: 

514 if not showBadCandidates: 

515 showBadCandidates = True 

516 continue 

517 break 

518 

519 if displayPsfComponents: 

520 disp6 = afwDisplay.Display(frame=6) 

521 utils.showPsf(psf, eigenValues, display=disp6) 

522 if displayPsfMosaic: 

523 disp7 = afwDisplay.Display(frame=7) 

524 utils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=True) 

525 disp7.scale('linear', 0, 1) 

526 if displayPsfSpatialModel: 

527 utils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True, 

528 matchKernelAmplitudes=matchKernelAmplitudes, 

529 keepPlots=keepMatplotlibPlots) 

530 

531 if pause: 

532 while True: 

533 try: 

534 reply = input("Next iteration? [ynchpqQs] ").strip() 

535 except EOFError: 

536 reply = "n" 

537 

538 reply = reply.split() 

539 if reply: 

540 reply, args = reply[0], reply[1:] 

541 else: 

542 reply = "" 

543 

544 if reply in ("", "c", "h", "n", "p", "q", "Q", "s", "y"): 

545 if reply == "c": 

546 pause = False 

547 elif reply == "h": 

548 print("c[ontinue without prompting] h[elp] n[o] p[db] q[uit displaying] " 

549 "s[ave fileName] y[es]") 

550 continue 

551 elif reply == "p": 

552 import pdb 

553 pdb.set_trace() 

554 elif reply == "q": 

555 display = False 

556 elif reply == "Q": 

557 sys.exit(1) 

558 elif reply == "s": 

559 fileName = args.pop(0) 

560 if not fileName: 

561 print("Please provide a filename") 

562 continue 

563 

564 print("Saving to %s" % fileName) 

565 utils.saveSpatialCellSet(psfCellSet, fileName=fileName) 

566 continue 

567 break 

568 else: 

569 print("Unrecognised response: %s" % reply, file=sys.stderr) 

570 

571 if reply == "n": 

572 break 

573 

574 # One last time, to take advantage of the last iteration 

575 psf, eigenValues, nEigenComponents, fitChi2 = \ 

576 self._fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents) 

577 

578 # 

579 # Display code for debugging 

580 # 

581 if display and reply != "n": 

582 disp = afwDisplay.Display(frame=0) 

583 if displayExposure: 

584 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=True, 

585 symb="o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED, 

586 size=8, display=disp) 

587 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell: 

588 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit, 

589 symb="o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED, 

590 size=10, display=disp) 

591 if displayResiduals: 

592 disp4 = afwDisplay.Display(frame=4) 

593 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4, 

594 normalize=normalizeResiduals, 

595 showBadCandidates=showBadCandidates) 

596 

597 if displayPsfComponents: 

598 disp6 = afwDisplay.Display(frame=6) 

599 utils.showPsf(psf, eigenValues, display=disp6) 

600 

601 if displayPsfMosaic: 

602 disp7 = afwDisplay.Display(frame=7) 

603 utils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=True) 

604 disp7.scale("linear", 0, 1) 

605 if displayPsfSpatialModel: 

606 utils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True, 

607 matchKernelAmplitudes=matchKernelAmplitudes, 

608 keepPlots=keepMatplotlibPlots) 

609 # 

610 # Generate some QA information 

611 # 

612 # Count PSF stars 

613 # 

614 numGoodStars = 0 

615 numAvailStars = 0 

616 

617 avgX = 0.0 

618 avgY = 0.0 

619 

620 for cell in psfCellSet.getCellList(): 

621 for cand in cell.begin(False): # don't ignore BAD stars 

622 numAvailStars += 1 

623 

624 for cand in cell.begin(True): # do ignore BAD stars 

625 src = cand.getSource() 

626 if flagKey is not None: 

627 src.set(flagKey, True) 

628 avgX += src.getX() 

629 avgY += src.getY() 

630 numGoodStars += 1 

631 

632 avgX /= numGoodStars 

633 avgY /= numGoodStars 

634 

635 if metadata is not None: 

636 metadata["spatialFitChi2"] = fitChi2 

637 metadata["numGoodStars"] = numGoodStars 

638 metadata["numAvailStars"] = numAvailStars 

639 metadata["avgX"] = avgX 

640 metadata["avgY"] = avgY 

641 

642 psf = PcaPsf(psf.getKernel(), lsst.geom.Point2D(avgX, avgY)) 

643 

644 return psf, psfCellSet 

645 

646 

647def candidatesIter(psfCellSet, ignoreBad=True): 

648 """Generator for Psf candidates. 

649 

650 This allows two 'for' loops to be reduced to one. 

651 

652 Parameters 

653 ---------- 

654 psfCellSet : `lsst.afw.math.SpatialCellSet` 

655 SpatialCellSet of PSF candidates. 

656 ignoreBad : `bool`, optional 

657 Ignore candidates flagged as BAD? 

658 

659 Yields 

660 ------- 

661 cell : `lsst.afw.math.SpatialCell` 

662 A SpatialCell. 

663 cand : `lsst.meas.algorithms.PsfCandidate` 

664 A PsfCandidate. 

665 """ 

666 for cell in psfCellSet.getCellList(): 

667 for cand in cell.begin(ignoreBad): 

668 yield (cell, cand) 

669 

670 

671psfDeterminerRegistry.register("pca", PcaPsfDeterminerTask)