Coverage for python/lsst/meas/algorithms/pcaPsfDeterminer.py: 8%
334 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 20:39 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-12-01 20:39 +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/>.
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 .psfCandidate import PsfCandidateF
38from .spatialModelPsf import createKernelFromPsfCandidates, countPsfCandidates, \
39 fitSpatialKernelFromPsfCandidates, fitKernelParamsToImage
40from .pcaPsf 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(
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 )
162class PcaPsfDeterminerTask(BasePsfDeterminerTask):
163 """A measurePsfTask psf estimator.
164 """
165 ConfigClass = PcaPsfDeterminerConfig
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))
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")
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]
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)
201 psf = PcaPsf(kernel)
203 return psf, eigenValues, nEigen, chi2
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.
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.
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?
245 if display:
246 afwDisplay.setDefaultMaskTransparency(75)
247 if display > 1:
248 pause = True
250 mi = exposure.getMaskedImage()
252 if len(psfCandidateList) == 0:
253 raise RuntimeError("No PSF candidates supplied.")
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
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()
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
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
291 if display:
292 print("Median size=%s" % (medSize,))
293 self.log.trace("Kernel size=%s", actualKernelSize)
295 # Set size of image returned around candidate
296 psfCandidateList[0].setHeight(actualKernelSize)
297 psfCandidateList[0].setWidth(actualKernelSize)
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")
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)
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
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()
334 chi2 = cand.getChi2()
335 if chi2 > 1e100:
336 chi2 = numpy.nan
338 stamps.append((im, "%d%s" %
339 (utils.splitId(cand.getSource().getId(), True)["objId"], chi2),
340 cand.getStatus()))
341 except Exception:
342 continue
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
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))
360 disp8 = afwDisplay.Display(frame=8)
361 mos.makeMosaic(display=disp8, title="Psf Candidates")
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)
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)
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
413 print("Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2))
414 c.setStatus(afwMath.SpatialCellCandidate.BAD)
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 #
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
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()
444 predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY()) for
445 k in range(kernel.getNKernelParameters())]
447 residuals.append([a/amp - p for a, p in zip(params, predict)])
448 candidates.append(cand)
450 residuals = numpy.array(residuals)
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)
468 rms = max(1.0e-4, rms) # Don't trust RMS below this due to numerical issues
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)
478 badCandidates.sort(key=lambda x: numpy.fabs(residuals[x, k] - mean), reverse=True)
480 numBad = numCandidatesToReject(len(badCandidates), iterNum,
481 self.config.nIterForPsf)
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)
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
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)
535 if pause:
536 while True:
537 try:
538 reply = input("Next iteration? [ynchpqQs] ").strip()
539 except EOFError:
540 reply = "n"
542 reply = reply.split()
543 if reply:
544 reply, args = reply[0], reply[1:]
545 else:
546 reply = ""
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
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)
575 if reply == "n":
576 break
578 # One last time, to take advantage of the last iteration
579 psf, eigenValues, nEigenComponents, fitChi2 = \
580 self._fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
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)
601 if displayPsfComponents:
602 disp6 = afwDisplay.Display(frame=6)
603 utils.showPsf(psf, eigenValues, display=disp6)
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
621 avgX = 0.0
622 avgY = 0.0
624 for cell in psfCellSet.getCellList():
625 for cand in cell.begin(False): # don't ignore BAD stars
626 numAvailStars += 1
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
636 avgX /= numGoodStars
637 avgY /= numGoodStars
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)
646 psf = PcaPsf(psf.getKernel(), lsst.geom.Point2D(avgX, avgY))
648 return psf, psfCellSet
651def candidatesIter(psfCellSet, ignoreBad=True):
652 """Generator for Psf candidates.
654 This allows two 'for' loops to be reduced to one.
656 Parameters
657 ----------
658 psfCellSet : `lsst.afw.math.SpatialCellSet`
659 SpatialCellSet of PSF candidates.
660 ignoreBad : `bool`, optional
661 Ignore candidates flagged as BAD?
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)
675psfDeterminerRegistry.register("pca", PcaPsfDeterminerTask)