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

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

334 statements  

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( 

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

73 dtype=bool, 

74 default=False, 

75 ) 

76 nEigenComponents = pexConfig.Field( 

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

78 dtype=int, 

79 default=4, 

80 ) 

81 spatialOrder = pexConfig.Field( 

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

83 dtype=int, 

84 default=2, 

85 ) 

86 sizeCellX = pexConfig.Field( 86 ↛ exitline 86 didn't jump to the function exit

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

88 dtype=int, 

89 default=256, 

90 # minValue = 10, 

91 check=lambda x: x >= 10, 

92 ) 

93 sizeCellY = pexConfig.Field( 93 ↛ exitline 93 didn't jump to the function exit

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

95 dtype=int, 

96 default=sizeCellX.default, 

97 # minValue = 10, 

98 check=lambda x: x >= 10, 

99 ) 

100 nStarPerCell = pexConfig.Field( 

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

102 dtype=int, 

103 default=3, 

104 ) 

105 borderWidth = pexConfig.Field( 

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

107 dtype=int, 

108 default=0, 

109 ) 

110 nStarPerCellSpatialFit = pexConfig.Field( 

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

112 dtype=int, 

113 default=5, 

114 ) 

115 constantWeight = pexConfig.Field( 

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

117 dtype=bool, 

118 default=True, 

119 ) 

120 nIterForPsf = pexConfig.Field( 

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

122 dtype=int, 

123 default=3, 

124 ) 

125 tolerance = pexConfig.Field( 

126 doc="tolerance of spatial fitting", 

127 dtype=float, 

128 default=1e-2, 

129 ) 

130 lam = pexConfig.Field( 

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

132 dtype=float, 

133 default=0.05, 

134 ) 

135 reducedChi2ForPsfCandidates = pexConfig.Field( 

136 doc="for psf candidate evaluation", 

137 dtype=float, 

138 default=2.0, 

139 ) 

140 spatialReject = pexConfig.Field( 

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

142 dtype=float, 

143 default=3.0, 

144 ) 

145 pixelThreshold = pexConfig.Field( 

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

147 dtype=float, 

148 default=0.0, 

149 ) 

150 doRejectBlends = pexConfig.Field( 

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

152 dtype=bool, 

153 default=False, 

154 ) 

155 doMaskBlends = pexConfig.Field( 

156 doc="Mask blends in image?", 

157 dtype=bool, 

158 default=True, 

159 ) 

160 

161 

162class PcaPsfDeterminerTask(BasePsfDeterminerTask): 

163 """A measurePsfTask psf estimator. 

164 """ 

165 ConfigClass = PcaPsfDeterminerConfig 

166 

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

168 PsfCandidateF.setPixelThreshold(self.config.pixelThreshold) 

169 PsfCandidateF.setMaskBlends(self.config.doMaskBlends) 

170 # 

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

172 # 

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

174 # Determine KL components 

175 try: 

176 kernel, eigenValues = createKernelFromPsfCandidates( 

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

178 self.config.spatialOrder, kernelSize, self.config.nStarPerCell, 

179 bool(self.config.constantWeight)) 

180 

181 break # OK, we can get nEigen components 

182 except pexExceptions.LengthError as e: 

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

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

185 

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

187 # 

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

189 # 

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

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

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

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

194 for val in eigenValues] 

195 

196 # Fit spatial model 

197 status, chi2 = fitSpatialKernelFromPsfCandidates( 

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

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

200 

201 psf = PcaPsf(kernel) 

202 

203 return psf, eigenValues, nEigen, chi2 

204 

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

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

207 

208 Parameters 

209 ---------- 

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

211 Exposure containing the psf candidates. 

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

213 A sequence of PSF candidates typically obtained by detecting sources 

214 and then running them through a star selector. 

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

216 A home for interesting tidbits of information. 

217 flagKey : `str`, optional 

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

219 

220 Returns 

221 ------- 

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

223 The measured PSF. 

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

225 The PSF candidates. 

226 """ 

227 import lsstDebug 

228 display = lsstDebug.Info(__name__).display 

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

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

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

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

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

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

235 # match Kernel amplitudes for spatial plots 

236 matchKernelAmplitudes = lsstDebug.Info(__name__).matchKernelAmplitudes 

237 # Keep matplotlib alive post mortem 

238 keepMatplotlibPlots = lsstDebug.Info(__name__).keepMatplotlibPlots 

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

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

241 # Normalize residuals by object amplitude 

242 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals 

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

244 

245 if display: 

246 afwDisplay.setDefaultMaskTransparency(75) 

247 if display > 1: 

248 pause = True 

249 

250 mi = exposure.getMaskedImage() 

251 

252 if len(psfCandidateList) == 0: 

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

254 

255 # construct and populate a spatial cell set 

256 bbox = mi.getBBox() 

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

258 sizes = [] 

259 for i, psfCandidate in enumerate(psfCandidateList): 

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

261 continue 

262 

263 try: 

264 psfCellSet.insertCandidate(psfCandidate) 

265 except Exception as e: 

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

267 continue 

268 source = psfCandidate.getSource() 

269 

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

271 axes = afwEll.Axes(quad) 

272 sizes.append(axes.getA()) 

273 if len(sizes) == 0: 

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

275 nEigenComponents = self.config.nEigenComponents # initial version 

276 

277 if self.config.kernelSize >= 15: 

278 self.log.warning("WARNING: NOT scaling kernelSize by stellar quadrupole moment " 

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

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

281 self.config.kernelSize) 

282 actualKernelSize = int(self.config.kernelSize) 

283 else: 

284 medSize = numpy.median(sizes) 

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

286 if actualKernelSize < self.config.kernelSizeMin: 

287 actualKernelSize = self.config.kernelSizeMin 

288 if actualKernelSize > self.config.kernelSizeMax: 

289 actualKernelSize = self.config.kernelSizeMax 

290 

291 if display: 

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

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

294 

295 # Set size of image returned around candidate 

296 psfCandidateList[0].setHeight(actualKernelSize) 

297 psfCandidateList[0].setWidth(actualKernelSize) 

298 

299 if self.config.doRejectBlends: 

300 # Remove blended candidates completely 

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

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

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

304 blendedCandidates.append((cell, cand)) 

305 continue 

306 if display: 

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

308 for cell, cand in blendedCandidates: 

309 cell.removeCandidate(cand) 

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

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

312 

313 if display: 

314 if displayExposure: 

315 disp = afwDisplay.Display(frame=0) 

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

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

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

319 size=4, display=disp) 

320 

321 # 

322 # Do a PCA decomposition of those PSF candidates 

323 # 

324 reply = "y" # used in interactive mode 

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

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

327 

328 stamps = [] 

329 for cell in psfCellSet.getCellList(): 

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

331 try: 

332 im = cand.getMaskedImage() 

333 

334 chi2 = cand.getChi2() 

335 if chi2 > 1e100: 

336 chi2 = numpy.nan 

337 

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

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

340 cand.getStatus())) 

341 except Exception: 

342 continue 

343 

344 if len(stamps) == 0: 

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

346 else: 

347 mos = afwDisplay.utils.Mosaic() 

348 for im, label, status in stamps: 

349 im = type(im)(im, True) 

350 try: 

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

352 except NotImplementedError: 

353 pass 

354 

355 mos.append(im, label, 

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

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

358 afwDisplay.RED)) 

359 

360 disp8 = afwDisplay.Display(frame=8) 

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

362 

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

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

365 while not cleanChi2: 

366 cleanChi2 = True 

367 # 

368 # First, estimate the PSF 

369 # 

370 psf, eigenValues, nEigenComponents, fitChi2 = \ 

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

372 # 

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

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

375 # 

376 for cell in psfCellSet.getCellList(): 

377 awfulCandidates = [] 

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

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

380 rchi2 = cand.getChi2() 

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

382 # Guilty prima facie 

383 awfulCandidates.append(cand) 

384 cleanChi2 = False 

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

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

387 for cand in awfulCandidates: 

388 if display: 

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

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

391 cell.removeCandidate(cand) 

392 

393 # 

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

395 # 

396 badCandidates = list() 

397 for cell in psfCellSet.getCellList(): 

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

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

400 assert rchi2 > 0 

401 if rchi2 > self.config.reducedChi2ForPsfCandidates: 

402 badCandidates.append(cand) 

403 

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

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

406 self.config.nIterForPsf) 

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

408 if display: 

409 chi2 = c.getChi2() 

410 if chi2 > 1e100: 

411 chi2 = numpy.nan 

412 

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

414 c.setStatus(afwMath.SpatialCellCandidate.BAD) 

415 

416 # 

417 # Clip out bad fits based on spatial fitting. 

418 # 

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

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

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

422 # downweights the component. 

423 # 

424 

425 residuals = list() 

426 candidates = list() 

427 kernel = psf.getKernel() 

428 noSpatialKernel = psf.getKernel() 

429 for cell in psfCellSet.getCellList(): 

430 for cand in cell.begin(False): 

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

432 try: 

433 im = cand.getMaskedImage(kernel.getWidth(), kernel.getHeight()) 

434 except Exception: 

435 continue 

436 

437 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter) 

438 params = fit[0] 

439 kernels = fit[1] 

440 amp = 0.0 

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

442 amp += p*k.getSum() 

443 

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

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

446 

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

448 candidates.append(cand) 

449 

450 residuals = numpy.array(residuals) 

451 

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

453 if False: 

454 # Straight standard deviation 

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

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

457 elif False: 

458 # Using interquartile range 

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

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

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

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

463 else: 

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

465 mean = stats.getValue(afwMath.MEANCLIP) 

466 rms = stats.getValue(afwMath.STDEVCLIP) 

467 

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

469 

470 if display: 

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

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

473 badCandidates = list() 

474 for i, cand in enumerate(candidates): 

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

476 badCandidates.append(i) 

477 

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

479 

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

481 self.config.nIterForPsf) 

482 

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

484 cand = candidates[c] 

485 if display: 

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

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

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

489 cand.setStatus(afwMath.SpatialCellCandidate.BAD) 

490 

491 # 

492 # Display results 

493 # 

494 if display and displayIterations: 

495 if displayExposure: 

496 if iterNum > 0: 

497 disp.erase() 

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

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

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

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

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

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

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

505 if displayResiduals: 

506 while True: 

507 try: 

508 disp4 = afwDisplay.Display(frame=4) 

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

510 normalize=normalizeResiduals, 

511 showBadCandidates=showBadCandidates) 

512 disp5 = afwDisplay.Display(frame=5) 

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

514 normalize=normalizeResiduals, 

515 showBadCandidates=showBadCandidates, 

516 variance=True) 

517 except Exception: 

518 if not showBadCandidates: 

519 showBadCandidates = True 

520 continue 

521 break 

522 

523 if displayPsfComponents: 

524 disp6 = afwDisplay.Display(frame=6) 

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

526 if displayPsfMosaic: 

527 disp7 = afwDisplay.Display(frame=7) 

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

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

530 if displayPsfSpatialModel: 

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

532 matchKernelAmplitudes=matchKernelAmplitudes, 

533 keepPlots=keepMatplotlibPlots) 

534 

535 if pause: 

536 while True: 

537 try: 

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

539 except EOFError: 

540 reply = "n" 

541 

542 reply = reply.split() 

543 if reply: 

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

545 else: 

546 reply = "" 

547 

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

549 if reply == "c": 

550 pause = False 

551 elif reply == "h": 

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

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

554 continue 

555 elif reply == "p": 

556 import pdb 

557 pdb.set_trace() 

558 elif reply == "q": 

559 display = False 

560 elif reply == "Q": 

561 sys.exit(1) 

562 elif reply == "s": 

563 fileName = args.pop(0) 

564 if not fileName: 

565 print("Please provide a filename") 

566 continue 

567 

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

569 utils.saveSpatialCellSet(psfCellSet, fileName=fileName) 

570 continue 

571 break 

572 else: 

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

574 

575 if reply == "n": 

576 break 

577 

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

579 psf, eigenValues, nEigenComponents, fitChi2 = \ 

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

581 

582 # 

583 # Display code for debugging 

584 # 

585 if display and reply != "n": 

586 disp = afwDisplay.Display(frame=0) 

587 if displayExposure: 

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

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

590 size=8, display=disp) 

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

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

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

594 size=10, display=disp) 

595 if displayResiduals: 

596 disp4 = afwDisplay.Display(frame=4) 

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

598 normalize=normalizeResiduals, 

599 showBadCandidates=showBadCandidates) 

600 

601 if displayPsfComponents: 

602 disp6 = afwDisplay.Display(frame=6) 

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

604 

605 if displayPsfMosaic: 

606 disp7 = afwDisplay.Display(frame=7) 

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

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

609 if displayPsfSpatialModel: 

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

611 matchKernelAmplitudes=matchKernelAmplitudes, 

612 keepPlots=keepMatplotlibPlots) 

613 # 

614 # Generate some QA information 

615 # 

616 # Count PSF stars 

617 # 

618 numGoodStars = 0 

619 numAvailStars = 0 

620 

621 avgX = 0.0 

622 avgY = 0.0 

623 

624 for cell in psfCellSet.getCellList(): 

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

626 numAvailStars += 1 

627 

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

629 src = cand.getSource() 

630 if flagKey is not None: 

631 src.set(flagKey, True) 

632 avgX += src.getX() 

633 avgY += src.getY() 

634 numGoodStars += 1 

635 

636 avgX /= numGoodStars 

637 avgY /= numGoodStars 

638 

639 if metadata is not None: 

640 metadata.set("spatialFitChi2", fitChi2) 

641 metadata.set("numGoodStars", numGoodStars) 

642 metadata.set("numAvailStars", numAvailStars) 

643 metadata.set("avgX", avgX) 

644 metadata.set("avgY", avgY) 

645 

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

647 

648 return psf, psfCellSet 

649 

650 

651def candidatesIter(psfCellSet, ignoreBad=True): 

652 """Generator for Psf candidates. 

653 

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

655 

656 Parameters 

657 ---------- 

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

659 SpatialCellSet of PSF candidates. 

660 ignoreBad : `bool`, optional 

661 Ignore candidates flagged as BAD? 

662 

663 Yields 

664 ------- 

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

666 A SpatialCell. 

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

668 A PsfCandidate. 

669 """ 

670 for cell in psfCellSet.getCellList(): 

671 for cand in cell.begin(ignoreBad): 

672 yield (cell, cand) 

673 

674 

675psfDeterminerRegistry.register("pca", PcaPsfDeterminerTask)