Coverage for python/lsst/meas/algorithms/pcaPsfDeterminer.py: 9%
344 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-16 02:19 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2023-03-16 02:19 -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/>.
22__all__ = ["PcaPsfDeterminerConfig", "PcaPsfDeterminerTask"]
24import math
25import sys
27import numpy
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 ._algorithmsLib import PsfCandidateF
38from ._algorithmsLib import createKernelFromPsfCandidates, countPsfCandidates, \
39 fitSpatialKernelFromPsfCandidates, fitKernelParamsToImage
40from ._algorithmsLib import PcaPsf
41from . import utils
44def numCandidatesToReject(numBadCandidates, numIter, totalIter):
45 """Return the number of PSF candidates to be rejected.
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.
51 Parameters
52 ----------
53 numBadCandidates : `int`
54 Number of bad candidates under consideration.
56 numIter : `int`
57 The number of the current PSF iteration.
59 totalIter : `int`
60 The total number of PSF iterations.
62 Returns
63 -------
64 return : `int`
65 Number of candidates to reject.
66 """
67 return int(numBadCandidates*(numIter + 1)//totalIter + 0.5)
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 )
145 def setDefaults(self):
146 super().setDefaults()
147 self.stampSize = 41
150class PcaPsfDeterminerTask(BasePsfDeterminerTask):
151 """A measurePsfTask psf estimator.
152 """
153 ConfigClass = PcaPsfDeterminerConfig
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))
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")
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]
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)
189 psf = PcaPsf(kernel)
191 return psf, eigenValues, nEigen, chi2
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.
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.
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?
233 if display:
234 afwDisplay.setDefaultMaskTransparency(75)
235 if display > 1:
236 pause = True
238 mi = exposure.getMaskedImage()
240 if len(psfCandidateList) == 0:
241 raise RuntimeError("No PSF candidates supplied.")
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
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()
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
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
285 if display:
286 print("Median size=%s" % (medSize,))
287 self.log.trace("Kernel size=%s", actualKernelSize)
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())
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")
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)
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
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()
330 chi2 = cand.getChi2()
331 if chi2 > 1e100:
332 chi2 = numpy.nan
334 stamps.append((im, "%d%s" %
335 (utils.splitId(cand.getSource().getId(), True)["objId"], chi2),
336 cand.getStatus()))
337 except Exception:
338 continue
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
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))
356 disp8 = afwDisplay.Display(frame=8)
357 mos.makeMosaic(display=disp8, title="Psf Candidates")
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)
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)
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
409 print("Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2))
410 c.setStatus(afwMath.SpatialCellCandidate.BAD)
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 #
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
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()
440 predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY()) for
441 k in range(kernel.getNKernelParameters())]
443 residuals.append([a/amp - p for a, p in zip(params, predict)])
444 candidates.append(cand)
446 residuals = numpy.array(residuals)
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)
464 rms = max(1.0e-4, rms) # Don't trust RMS below this due to numerical issues
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)
474 badCandidates.sort(key=lambda x: numpy.fabs(residuals[x, k] - mean), reverse=True)
476 numBad = numCandidatesToReject(len(badCandidates), iterNum,
477 self.config.nIterForPsf)
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)
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
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)
531 if pause:
532 while True:
533 try:
534 reply = input("Next iteration? [ynchpqQs] ").strip()
535 except EOFError:
536 reply = "n"
538 reply = reply.split()
539 if reply:
540 reply, args = reply[0], reply[1:]
541 else:
542 reply = ""
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
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)
571 if reply == "n":
572 break
574 # One last time, to take advantage of the last iteration
575 psf, eigenValues, nEigenComponents, fitChi2 = \
576 self._fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
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)
597 if displayPsfComponents:
598 disp6 = afwDisplay.Display(frame=6)
599 utils.showPsf(psf, eigenValues, display=disp6)
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
617 avgX = 0.0
618 avgY = 0.0
620 for cell in psfCellSet.getCellList():
621 for cand in cell.begin(False): # don't ignore BAD stars
622 numAvailStars += 1
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
632 avgX /= numGoodStars
633 avgY /= numGoodStars
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
642 psf = PcaPsf(psf.getKernel(), lsst.geom.Point2D(avgX, avgY))
644 return psf, psfCellSet
647def candidatesIter(psfCellSet, ignoreBad=True):
648 """Generator for Psf candidates.
650 This allows two 'for' loops to be reduced to one.
652 Parameters
653 ----------
654 psfCellSet : `lsst.afw.math.SpatialCellSet`
655 SpatialCellSet of PSF candidates.
656 ignoreBad : `bool`, optional
657 Ignore candidates flagged as BAD?
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)
671psfDeterminerRegistry.register("pca", PcaPsfDeterminerTask)