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

338 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-16 02:24 -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 

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( 76 ↛ exitline 76 didn't jump to the function exit

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

78 dtype=int, 

79 default=4, 

80 check=lambda x: x >= 1 

81 ) 

82 spatialOrder = pexConfig.Field( 

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

84 dtype=int, 

85 default=2, 

86 ) 

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

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

89 dtype=int, 

90 default=256, 

91 # minValue = 10, 

92 check=lambda x: x >= 10, 

93 ) 

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

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

96 dtype=int, 

97 default=sizeCellX.default, 

98 # minValue = 10, 

99 check=lambda x: x >= 10, 

100 ) 

101 nStarPerCell = pexConfig.Field( 

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

103 dtype=int, 

104 default=3, 

105 ) 

106 borderWidth = pexConfig.Field( 

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

108 dtype=int, 

109 default=0, 

110 ) 

111 nStarPerCellSpatialFit = pexConfig.Field( 

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

113 dtype=int, 

114 default=5, 

115 ) 

116 constantWeight = pexConfig.Field( 

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

118 dtype=bool, 

119 default=True, 

120 ) 

121 nIterForPsf = pexConfig.Field( 

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

123 dtype=int, 

124 default=3, 

125 ) 

126 tolerance = pexConfig.Field( 

127 doc="tolerance of spatial fitting", 

128 dtype=float, 

129 default=1e-2, 

130 ) 

131 lam = pexConfig.Field( 

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

133 dtype=float, 

134 default=0.05, 

135 ) 

136 reducedChi2ForPsfCandidates = pexConfig.Field( 

137 doc="for psf candidate evaluation", 

138 dtype=float, 

139 default=2.0, 

140 ) 

141 spatialReject = pexConfig.Field( 

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

143 dtype=float, 

144 default=3.0, 

145 ) 

146 pixelThreshold = pexConfig.Field( 

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

148 dtype=float, 

149 default=0.0, 

150 ) 

151 doRejectBlends = pexConfig.Field( 

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

153 dtype=bool, 

154 default=False, 

155 ) 

156 doMaskBlends = pexConfig.Field( 

157 doc="Mask blends in image?", 

158 dtype=bool, 

159 default=True, 

160 ) 

161 

162 

163class PcaPsfDeterminerTask(BasePsfDeterminerTask): 

164 """A measurePsfTask psf estimator. 

165 """ 

166 ConfigClass = PcaPsfDeterminerConfig 

167 

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

169 PsfCandidateF.setPixelThreshold(self.config.pixelThreshold) 

170 PsfCandidateF.setMaskBlends(self.config.doMaskBlends) 

171 # 

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

173 # 

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

175 # Determine KL components 

176 try: 

177 kernel, eigenValues = createKernelFromPsfCandidates( 

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

179 self.config.spatialOrder, kernelSize, self.config.nStarPerCell, 

180 bool(self.config.constantWeight)) 

181 

182 break # OK, we can get nEigen components 

183 except pexExceptions.LengthError as e: 

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

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

186 

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

188 # 

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

190 # 

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

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

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

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

195 for val in eigenValues] 

196 

197 # Fit spatial model 

198 status, chi2 = fitSpatialKernelFromPsfCandidates( 

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

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

201 

202 psf = PcaPsf(kernel) 

203 

204 return psf, eigenValues, nEigen, chi2 

205 

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

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

208 

209 Parameters 

210 ---------- 

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

212 Exposure containing the psf candidates. 

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

214 A sequence of PSF candidates typically obtained by detecting sources 

215 and then running them through a star selector. 

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

217 A home for interesting tidbits of information. 

218 flagKey : `str`, optional 

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

220 

221 Returns 

222 ------- 

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

224 The measured PSF. 

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

226 The PSF candidates. 

227 """ 

228 import lsstDebug 

229 display = lsstDebug.Info(__name__).display 

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

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

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

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

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

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

236 # match Kernel amplitudes for spatial plots 

237 matchKernelAmplitudes = lsstDebug.Info(__name__).matchKernelAmplitudes 

238 # Keep matplotlib alive post mortem 

239 keepMatplotlibPlots = lsstDebug.Info(__name__).keepMatplotlibPlots 

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

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

242 # Normalize residuals by object amplitude 

243 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals 

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

245 

246 if display: 

247 afwDisplay.setDefaultMaskTransparency(75) 

248 if display > 1: 

249 pause = True 

250 

251 mi = exposure.getMaskedImage() 

252 

253 if len(psfCandidateList) == 0: 

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

255 

256 # construct and populate a spatial cell set 

257 bbox = mi.getBBox() 

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

259 sizes = [] 

260 for i, psfCandidate in enumerate(psfCandidateList): 

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

262 continue 

263 

264 try: 

265 psfCellSet.insertCandidate(psfCandidate) 

266 except Exception as e: 

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

268 continue 

269 source = psfCandidate.getSource() 

270 

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

272 axes = afwEll.Axes(quad) 

273 sizes.append(axes.getA()) 

274 if len(sizes) == 0: 

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

276 nEigenComponents = self.config.nEigenComponents # initial version 

277 

278 if self.config.kernelSize >= 15: 

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

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

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

282 self.config.kernelSize) 

283 actualKernelSize = int(self.config.kernelSize) 

284 else: 

285 medSize = numpy.median(sizes) 

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

287 if actualKernelSize < self.config.kernelSizeMin: 

288 actualKernelSize = self.config.kernelSizeMin 

289 if actualKernelSize > self.config.kernelSizeMax: 

290 actualKernelSize = self.config.kernelSizeMax 

291 

292 if display: 

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

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

295 

296 # Set size of image returned around candidate 

297 psfCandidateList[0].setHeight(actualKernelSize) 

298 psfCandidateList[0].setWidth(actualKernelSize) 

299 

300 if self.config.doRejectBlends: 

301 # Remove blended candidates completely 

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

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

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

305 blendedCandidates.append((cell, cand)) 

306 continue 

307 if display: 

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

309 for cell, cand in blendedCandidates: 

310 cell.removeCandidate(cand) 

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

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

313 

314 if display: 

315 if displayExposure: 

316 disp = afwDisplay.Display(frame=0) 

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

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

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

320 size=4, display=disp) 

321 

322 # 

323 # Do a PCA decomposition of those PSF candidates 

324 # 

325 reply = "y" # used in interactive mode 

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

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

328 

329 stamps = [] 

330 for cell in psfCellSet.getCellList(): 

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

332 try: 

333 im = cand.getMaskedImage() 

334 

335 chi2 = cand.getChi2() 

336 if chi2 > 1e100: 

337 chi2 = numpy.nan 

338 

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

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

341 cand.getStatus())) 

342 except Exception: 

343 continue 

344 

345 if len(stamps) == 0: 

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

347 else: 

348 mos = afwDisplay.utils.Mosaic() 

349 for im, label, status in stamps: 

350 im = type(im)(im, True) 

351 try: 

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

353 except NotImplementedError: 

354 pass 

355 

356 mos.append(im, label, 

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

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

359 afwDisplay.RED)) 

360 

361 disp8 = afwDisplay.Display(frame=8) 

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

363 

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

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

366 while not cleanChi2: 

367 cleanChi2 = True 

368 # 

369 # First, estimate the PSF 

370 # 

371 psf, eigenValues, nEigenComponents, fitChi2 = \ 

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

373 # 

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

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

376 # 

377 for cell in psfCellSet.getCellList(): 

378 awfulCandidates = [] 

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

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

381 rchi2 = cand.getChi2() 

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

383 # Guilty prima facie 

384 awfulCandidates.append(cand) 

385 cleanChi2 = False 

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

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

388 for cand in awfulCandidates: 

389 if display: 

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

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

392 cell.removeCandidate(cand) 

393 

394 # 

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

396 # 

397 badCandidates = list() 

398 for cell in psfCellSet.getCellList(): 

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

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

401 assert rchi2 > 0 

402 if rchi2 > self.config.reducedChi2ForPsfCandidates: 

403 badCandidates.append(cand) 

404 

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

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

407 self.config.nIterForPsf) 

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

409 if display: 

410 chi2 = c.getChi2() 

411 if chi2 > 1e100: 

412 chi2 = numpy.nan 

413 

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

415 c.setStatus(afwMath.SpatialCellCandidate.BAD) 

416 

417 # 

418 # Clip out bad fits based on spatial fitting. 

419 # 

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

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

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

423 # downweights the component. 

424 # 

425 

426 residuals = list() 

427 candidates = list() 

428 kernel = psf.getKernel() 

429 noSpatialKernel = psf.getKernel() 

430 for cell in psfCellSet.getCellList(): 

431 for cand in cell.begin(False): 

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

433 try: 

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

435 except Exception: 

436 continue 

437 

438 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter) 

439 params = fit[0] 

440 kernels = fit[1] 

441 amp = 0.0 

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

443 amp += p*k.getSum() 

444 

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

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

447 

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

449 candidates.append(cand) 

450 

451 residuals = numpy.array(residuals) 

452 

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

454 if False: 

455 # Straight standard deviation 

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

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

458 elif False: 

459 # Using interquartile range 

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

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

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

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

464 else: 

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

466 mean = stats.getValue(afwMath.MEANCLIP) 

467 rms = stats.getValue(afwMath.STDEVCLIP) 

468 

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

470 

471 if display: 

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

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

474 badCandidates = list() 

475 for i, cand in enumerate(candidates): 

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

477 badCandidates.append(i) 

478 

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

480 

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

482 self.config.nIterForPsf) 

483 

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

485 cand = candidates[c] 

486 if display: 

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

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

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

490 cand.setStatus(afwMath.SpatialCellCandidate.BAD) 

491 

492 # 

493 # Display results 

494 # 

495 if display and displayIterations: 

496 if displayExposure: 

497 if iterNum > 0: 

498 disp.erase() 

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

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

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

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

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

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

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

506 if displayResiduals: 

507 while True: 

508 try: 

509 disp4 = afwDisplay.Display(frame=4) 

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

511 normalize=normalizeResiduals, 

512 showBadCandidates=showBadCandidates) 

513 disp5 = afwDisplay.Display(frame=5) 

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

515 normalize=normalizeResiduals, 

516 showBadCandidates=showBadCandidates, 

517 variance=True) 

518 except Exception: 

519 if not showBadCandidates: 

520 showBadCandidates = True 

521 continue 

522 break 

523 

524 if displayPsfComponents: 

525 disp6 = afwDisplay.Display(frame=6) 

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

527 if displayPsfMosaic: 

528 disp7 = afwDisplay.Display(frame=7) 

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

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

531 if displayPsfSpatialModel: 

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

533 matchKernelAmplitudes=matchKernelAmplitudes, 

534 keepPlots=keepMatplotlibPlots) 

535 

536 if pause: 

537 while True: 

538 try: 

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

540 except EOFError: 

541 reply = "n" 

542 

543 reply = reply.split() 

544 if reply: 

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

546 else: 

547 reply = "" 

548 

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

550 if reply == "c": 

551 pause = False 

552 elif reply == "h": 

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

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

555 continue 

556 elif reply == "p": 

557 import pdb 

558 pdb.set_trace() 

559 elif reply == "q": 

560 display = False 

561 elif reply == "Q": 

562 sys.exit(1) 

563 elif reply == "s": 

564 fileName = args.pop(0) 

565 if not fileName: 

566 print("Please provide a filename") 

567 continue 

568 

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

570 utils.saveSpatialCellSet(psfCellSet, fileName=fileName) 

571 continue 

572 break 

573 else: 

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

575 

576 if reply == "n": 

577 break 

578 

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

580 psf, eigenValues, nEigenComponents, fitChi2 = \ 

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

582 

583 # 

584 # Display code for debugging 

585 # 

586 if display and reply != "n": 

587 disp = afwDisplay.Display(frame=0) 

588 if displayExposure: 

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

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

591 size=8, display=disp) 

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

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

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

595 size=10, display=disp) 

596 if displayResiduals: 

597 disp4 = afwDisplay.Display(frame=4) 

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

599 normalize=normalizeResiduals, 

600 showBadCandidates=showBadCandidates) 

601 

602 if displayPsfComponents: 

603 disp6 = afwDisplay.Display(frame=6) 

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

605 

606 if displayPsfMosaic: 

607 disp7 = afwDisplay.Display(frame=7) 

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

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

610 if displayPsfSpatialModel: 

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

612 matchKernelAmplitudes=matchKernelAmplitudes, 

613 keepPlots=keepMatplotlibPlots) 

614 # 

615 # Generate some QA information 

616 # 

617 # Count PSF stars 

618 # 

619 numGoodStars = 0 

620 numAvailStars = 0 

621 

622 avgX = 0.0 

623 avgY = 0.0 

624 

625 for cell in psfCellSet.getCellList(): 

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

627 numAvailStars += 1 

628 

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

630 src = cand.getSource() 

631 if flagKey is not None: 

632 src.set(flagKey, True) 

633 avgX += src.getX() 

634 avgY += src.getY() 

635 numGoodStars += 1 

636 

637 avgX /= numGoodStars 

638 avgY /= numGoodStars 

639 

640 if metadata is not None: 

641 metadata["spatialFitChi2"] = fitChi2 

642 metadata["numGoodStars"] = numGoodStars 

643 metadata["numAvailStars"] = numAvailStars 

644 metadata["avgX"] = avgX 

645 metadata["avgY"] = avgY 

646 

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

648 

649 return psf, psfCellSet 

650 

651 

652def candidatesIter(psfCellSet, ignoreBad=True): 

653 """Generator for Psf candidates. 

654 

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

656 

657 Parameters 

658 ---------- 

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

660 SpatialCellSet of PSF candidates. 

661 ignoreBad : `bool`, optional 

662 Ignore candidates flagged as BAD? 

663 

664 Yields 

665 ------- 

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

667 A SpatialCell. 

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

669 A PsfCandidate. 

670 """ 

671 for cell in psfCellSet.getCellList(): 

672 for cand in cell.begin(ignoreBad): 

673 yield (cell, cand) 

674 

675 

676psfDeterminerRegistry.register("pca", PcaPsfDeterminerTask)