lsst.meas.algorithms 22.0.1-35-gcb622b2d+6c0afe47ea
pcaPsfDeterminer.py
Go to the documentation of this file.
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(
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(
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
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
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 -------
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_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_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["spatialFitChi2"] = fitChi2
641 metadata["numGoodStars"] = numGoodStars
642 metadata["numAvailStars"] = numAvailStars
643 metadata["avgX"] = avgX
644 metadata["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 -------
666 A SpatialCell.
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)
Represent a PSF as a linear combination of PCA (== Karhunen-Loeve) basis functions.
Definition: PcaPsf.h:37
Class stored in SpatialCells for spatial Psf fitting.
Definition: PsfCandidate.h:55
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents)
def numCandidatesToReject(numBadCandidates, numIter, totalIter)
def candidatesIter(psfCellSet, ignoreBad=True)
std::pair< std::shared_ptr< afw::math::LinearCombinationKernel >, std::vector< double > > createKernelFromPsfCandidates(afw::math::SpatialCellSet const &psfCells, geom::Extent2I const &dims, geom::Point2I const &xy0, int const nEigenComponents, int const spatialOrder, int const ksize, int const nStarPerCell=-1, bool const constantWeight=true, int const border=3)
Return a Kernel pointer and a list of eigenvalues resulting from analysing the provided SpatialCellSe...
int countPsfCandidates(afw::math::SpatialCellSet const &psfCells, int const nStarPerCell=-1)
Count the number of candidates in use.
std::pair< std::vector< double >, afw::math::KernelList > fitKernelParamsToImage(afw::math::LinearCombinationKernel const &kernel, Image const &image, geom::Point2D const &pos)
Fit a LinearCombinationKernel to an Image, allowing the coefficients of the components to vary.
std::pair< bool, double > fitSpatialKernelFromPsfCandidates(afw::math::Kernel *kernel, afw::math::SpatialCellSet const &psfCells, int const nStarPerCell=-1, double const tolerance=1e-5, double const lambda=0.0)
Fit spatial kernel using full-nonlinear optimization to estimate candidate amplitudes.
STL namespace.