lsst.meas.algorithms  13.0-20-g02a2147
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
pcaPsfDeterminer.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 #
4 # Copyright 2008-2017 AURA/LSST.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <https://www.lsstcorp.org/LegalNotices/>.
22 #
23 from __future__ import print_function
24 
25 __all__ = ["PcaPsfDeterminerConfig", "PcaPsfDeterminerTask"]
26 
27 from builtins import input
28 from builtins import zip
29 from builtins import range
30 import math
31 import sys
32 
33 import numpy
34 
35 import lsst.pex.config as pexConfig
36 import lsst.pex.exceptions as pexExceptions
37 import lsst.afw.geom as afwGeom
38 import lsst.afw.geom.ellipses as afwEll
39 import lsst.afw.display.ds9 as ds9
40 import lsst.afw.math as afwMath
41 from .psfDeterminer import BasePsfDeterminerTask, psfDeterminerRegistry
42 from .psfCandidate import PsfCandidateF
43 from .spatialModelPsf import createKernelFromPsfCandidates, countPsfCandidates, fitSpatialKernelFromPsfCandidates, fitKernelParamsToImage
44 from .pcaPsf import PcaPsf
45 from . import utils as maUtils
46 
47 
48 def numCandidatesToReject(numBadCandidates, numIter, totalIter):
49  """Return the number of PSF candidates to be rejected.
50 
51  The number of candidates being rejected on each iteration gradually
52  increases, so that on the Nth of M iterations we reject N/M of the bad
53  candidates.
54 
55  Parameters
56  ----------
57  numBadCandidates : int
58  Number of bad candidates under consideration.
59 
60  numIter : int
61  The number of the current PSF iteration.
62 
63  totalIter : int
64  The total number of PSF iterations.
65 
66  Returns
67  -------
68  int
69  Number of candidates to reject.
70  """
71  return int(numBadCandidates * (numIter + 1) // totalIter + 0.5)
72 
73 class PcaPsfDeterminerConfig(BasePsfDeterminerTask.ConfigClass):
74  nonLinearSpatialFit = pexConfig.Field(
75  doc="Use non-linear fitter for spatial variation of Kernel",
76  dtype=bool,
77  default=False,
78  )
79  nEigenComponents = pexConfig.Field(
80  doc="number of eigen components for PSF kernel creation",
81  dtype=int,
82  default=4,
83  )
84  spatialOrder = pexConfig.Field(
85  doc="specify spatial order for PSF kernel creation",
86  dtype=int,
87  default=2,
88  )
89  sizeCellX = pexConfig.Field(
90  doc="size of cell used to determine PSF (pixels, column direction)",
91  dtype=int,
92  default=256,
93  # minValue = 10,
94  check=lambda x: x >= 10,
95  )
96  sizeCellY = pexConfig.Field(
97  doc="size of cell used to determine PSF (pixels, row direction)",
98  dtype=int,
99  default=sizeCellX.default,
100  # minValue = 10,
101  check=lambda x: x >= 10,
102  )
103  nStarPerCell = pexConfig.Field(
104  doc="number of stars per psf cell for PSF kernel creation",
105  dtype=int,
106  default=3,
107  )
108  borderWidth = pexConfig.Field(
109  doc="Number of pixels to ignore around the edge of PSF candidate postage stamps",
110  dtype=int,
111  default=0,
112  )
113  nStarPerCellSpatialFit = pexConfig.Field(
114  doc="number of stars per psf Cell for spatial fitting",
115  dtype=int,
116  default=5,
117  )
118  constantWeight = pexConfig.Field(
119  doc="Should each PSF candidate be given the same weight, independent of magnitude?",
120  dtype=bool,
121  default=True,
122  )
123  nIterForPsf = pexConfig.Field(
124  doc="number of iterations of PSF candidate star list",
125  dtype=int,
126  default=3,
127  )
128  tolerance = pexConfig.Field(
129  doc="tolerance of spatial fitting",
130  dtype=float,
131  default=1e-2,
132  )
133  lam = pexConfig.Field(
134  doc="floor for variance is lam*data",
135  dtype=float,
136  default=0.05,
137  )
138  reducedChi2ForPsfCandidates = pexConfig.Field(
139  doc="for psf candidate evaluation",
140  dtype=float,
141  default=2.0,
142  )
143  spatialReject = pexConfig.Field(
144  doc="Rejection threshold (stdev) for candidates based on spatial fit",
145  dtype=float,
146  default=3.0,
147  )
148  pixelThreshold = pexConfig.Field(
149  doc="Threshold (stdev) for rejecting extraneous pixels around candidate; applied if positive",
150  dtype=float,
151  default=0.0,
152  )
153  doRejectBlends = pexConfig.Field(
154  doc="Reject candidates that are blended?",
155  dtype=bool,
156  default=False,
157  )
158  doMaskBlends = pexConfig.Field(
159  doc="Mask blends in image?",
160  dtype=bool,
161  default=True,
162  )
163 
164 
165 class PcaPsfDeterminerTask(BasePsfDeterminerTask):
166  """!
167  A measurePsfTask psf estimator
168  """
169  ConfigClass = PcaPsfDeterminerConfig
170 
171 
172  def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents):
173  PsfCandidateF.setPixelThreshold(self.config.pixelThreshold)
174  PsfCandidateF.setMaskBlends(self.config.doMaskBlends)
175  #
176  # Loop trying to use nEigenComponents, but allowing smaller numbers if necessary
177  #
178  for nEigen in range(nEigenComponents, 0, -1):
179  # Determine KL components
180  try:
181  kernel, eigenValues = createKernelFromPsfCandidates(
182  psfCellSet, exposure.getDimensions(), exposure.getXY0(), nEigen,
183  self.config.spatialOrder, kernelSize, self.config.nStarPerCell,
184  bool(self.config.constantWeight))
185 
186  break # OK, we can get nEigen components
187  except pexExceptions.LengthError as e:
188  if nEigen == 1: # can't go any lower
189  raise IndexError("No viable PSF candidates survive")
190 
191  self.log.warn("%s: reducing number of eigen components" % e.what())
192  #
193  # We got our eigen decomposition so let's use it
194  #
195  # Express eigenValues in units of reduced chi^2 per star
196  size = kernelSize + 2*self.config.borderWidth
197  nu = size*size - 1 # number of degrees of freedom/star for chi^2
198  eigenValues = [l/float(countPsfCandidates(psfCellSet, self.config.nStarPerCell)*nu)
199  for l in eigenValues]
200 
201  # Fit spatial model
202  status, chi2 = fitSpatialKernelFromPsfCandidates(
203  kernel, psfCellSet, bool(self.config.nonLinearSpatialFit),
204  self.config.nStarPerCellSpatialFit, self.config.tolerance, self.config.lam)
205 
206  psf = PcaPsf(kernel)
207 
208  return psf, eigenValues, nEigen, chi2
209 
210  def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
211  """!Determine a PCA PSF model for an exposure given a list of PSF candidates
212 
213  \param[in] exposure exposure containing the psf candidates (lsst.afw.image.Exposure)
214  \param[in] psfCandidateList a sequence of PSF candidates (each an lsst.meas.algorithms.PsfCandidate);
215  typically obtained by detecting sources and then running them through a star selector
216  \param[in,out] metadata a home for interesting tidbits of information
217  \param[in] flagKey schema key used to mark sources actually used in PSF determination
218 
219  \return a list of
220  - psf: the measured PSF, an lsst.meas.algorithms.PcaPsf
221  - cellSet: an lsst.afw.math.SpatialCellSet containing the PSF candidates
222  """
223  import lsstDebug
224  display = lsstDebug.Info(__name__).display
225  displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
226  displayPsfCandidates = lsstDebug.Info(__name__).displayPsfCandidates # show the viable candidates
227  displayIterations = lsstDebug.Info(__name__).displayIterations # display on each PSF iteration
228  displayPsfComponents = lsstDebug.Info(__name__).displayPsfComponents # show the PCA components
229  displayResiduals = lsstDebug.Info(__name__).displayResiduals # show residuals
230  displayPsfMosaic = lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
231  # match Kernel amplitudes for spatial plots
232  matchKernelAmplitudes = lsstDebug.Info(__name__).matchKernelAmplitudes
233  # Keep matplotlib alive post mortem
234  keepMatplotlibPlots = lsstDebug.Info(__name__).keepMatplotlibPlots
235  displayPsfSpatialModel = lsstDebug.Info(__name__).displayPsfSpatialModel # Plot spatial model?
236  showBadCandidates = lsstDebug.Info(__name__).showBadCandidates # Include bad candidates
237  # Normalize residuals by object amplitude
238  normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals
239  pause = lsstDebug.Info(__name__).pause # Prompt user after each iteration?
240 
241  if display > 1:
242  pause = True
243 
244  mi = exposure.getMaskedImage()
245 
246  if len(psfCandidateList) == 0:
247  raise RuntimeError("No PSF candidates supplied.")
248 
249  # construct and populate a spatial cell set
250  bbox = mi.getBBox()
251  psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY)
252  sizes = []
253  for i, psfCandidate in enumerate(psfCandidateList):
254  if psfCandidate.getSource().getPsfFluxFlag(): # bad measurement
255  continue
256 
257  try:
258  psfCellSet.insertCandidate(psfCandidate)
259  except Exception as e:
260  self.log.debug("Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
261  continue
262  source = psfCandidate.getSource()
263 
264  quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
265  axes = afwEll.Axes(quad)
266  sizes.append(axes.getA())
267  if len(sizes) == 0:
268  raise RuntimeError("No usable PSF candidates supplied")
269  nEigenComponents = self.config.nEigenComponents # initial version
270 
271  if self.config.kernelSize >= 15:
272  self.log.warn("WARNING: NOT scaling kernelSize by stellar quadrupole moment " +
273  "because config.kernelSize=%s >= 15; using config.kernelSize as as the width, instead",
274  self.config.kernelSize)
275  actualKernelSize = int(self.config.kernelSize)
276  else:
277  medSize = numpy.median(sizes)
278  actualKernelSize = 2 * int(self.config.kernelSize * math.sqrt(medSize) + 0.5) + 1
279  if actualKernelSize < self.config.kernelSizeMin:
280  actualKernelSize = self.config.kernelSizeMin
281  if actualKernelSize > self.config.kernelSizeMax:
282  actualKernelSize = self.config.kernelSizeMax
283 
284  if display:
285  print("Median size=%s" % (medSize,))
286  self.log.trace("Kernel size=%s", actualKernelSize)
287 
288  # Set size of image returned around candidate
289  psfCandidateList[0].setHeight(actualKernelSize)
290  psfCandidateList[0].setWidth(actualKernelSize)
291 
292  if self.config.doRejectBlends:
293  # Remove blended candidates completely
294  blendedCandidates = [] # Candidates to remove; can't do it while iterating
295  for cell, cand in candidatesIter(psfCellSet, False):
296  if len(cand.getSource().getFootprint().getPeaks()) > 1:
297  blendedCandidates.append((cell, cand))
298  continue
299  if display:
300  print("Removing %d blended Psf candidates" % len(blendedCandidates))
301  for cell, cand in blendedCandidates:
302  cell.removeCandidate(cand)
303  if sum(1 for cand in candidatesIter(psfCellSet, False)) == 0:
304  raise RuntimeError("All PSF candidates removed as blends")
305 
306  if display:
307  frame = 0
308  if displayExposure:
309  ds9.mtv(exposure, frame=frame, title="psf determination")
310  maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell,
311  symb="o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW,
312  size=4, frame=frame)
313 
314  #
315  # Do a PCA decomposition of those PSF candidates
316  #
317  reply = "y" # used in interactive mode
318  for iterNum in range(self.config.nIterForPsf):
319  if display and displayPsfCandidates: # Show a mosaic of usable PSF candidates
320  #
321  import lsst.afw.display.utils as displayUtils
322 
323  stamps = []
324  for cell in psfCellSet.getCellList():
325  for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
326  try:
327  im = cand.getMaskedImage()
328 
329  chi2 = cand.getChi2()
330  if chi2 > 1e100:
331  chi2 = numpy.nan
332 
333  stamps.append((im, "%d%s" %
334  (maUtils.splitId(cand.getSource().getId(), True)["objId"], chi2),
335  cand.getStatus()))
336  except Exception as e:
337  continue
338 
339  if len(stamps) == 0:
340  print("WARNING: No PSF candidates to show; try setting showBadCandidates=True")
341  else:
342  mos = displayUtils.Mosaic()
343  for im, label, status in stamps:
344  im = type(im)(im, True)
345  try:
346  im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
347  except NotImplementedError:
348  pass
349 
350  mos.append(im, label,
351  ds9.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
352  ds9.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else ds9.RED)
353 
354  mos.makeMosaic(frame=8, title="Psf Candidates")
355 
356  # Re-fit until we don't have any candidates with naughty chi^2 values influencing the fit
357  cleanChi2 = False # Any naughty (negative/NAN) chi^2 values?
358  while not cleanChi2:
359  cleanChi2 = True
360  #
361  # First, estimate the PSF
362  #
363  psf, eigenValues, nEigenComponents, fitChi2 = \
364  self._fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
365  #
366  # In clipping, allow all candidates to be innocent until proven guilty on this iteration.
367  # Throw out any prima facie guilty candidates (naughty chi^2 values)
368  #
369  for cell in psfCellSet.getCellList():
370  awfulCandidates = []
371  for cand in cell.begin(False): # include bad candidates
372  cand.setStatus(afwMath.SpatialCellCandidate.UNKNOWN) # until proven guilty
373  rchi2 = cand.getChi2()
374  if not numpy.isfinite(rchi2) or rchi2 <= 0:
375  # Guilty prima facie
376  awfulCandidates.append(cand)
377  cleanChi2 = False
378  self.log.debug("chi^2=%s; id=%s",
379  cand.getChi2(), cand.getSource().getId())
380  for cand in awfulCandidates:
381  if display:
382  print("Removing bad candidate: id=%d, chi^2=%f" % \
383  (cand.getSource().getId(), cand.getChi2()))
384  cell.removeCandidate(cand)
385 
386  #
387  # Clip out bad fits based on reduced chi^2
388  #
389  badCandidates = list()
390  for cell in psfCellSet.getCellList():
391  for cand in cell.begin(False): # include bad candidates
392  rchi2 = cand.getChi2() # reduced chi^2 when fitting PSF to candidate
393  assert rchi2 > 0
394  if rchi2 > self.config.reducedChi2ForPsfCandidates:
395  badCandidates.append(cand)
396 
397  badCandidates.sort(key=lambda x: x.getChi2(), reverse=True)
398  numBad = numCandidatesToReject(len(badCandidates), iterNum,
399  self.config.nIterForPsf)
400  for i, c in zip(range(numBad), badCandidates):
401  if display:
402  chi2 = c.getChi2()
403  if chi2 > 1e100:
404  chi2 = numpy.nan
405 
406  print("Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2))
407  c.setStatus(afwMath.SpatialCellCandidate.BAD)
408 
409  #
410  # Clip out bad fits based on spatial fitting.
411  #
412  # This appears to be better at getting rid of sources that have a single dominant kernel component
413  # (other than the zeroth; e.g., a nearby contaminant) because the surrounding sources (which help
414  # set the spatial model) don't contain that kernel component, and so the spatial modeling
415  # downweights the component.
416  #
417 
418  residuals = list()
419  candidates = list()
420  kernel = psf.getKernel()
421  noSpatialKernel = psf.getKernel()
422  for cell in psfCellSet.getCellList():
423  for cand in cell.begin(False):
424  candCenter = afwGeom.PointD(cand.getXCenter(), cand.getYCenter())
425  try:
426  im = cand.getMaskedImage(kernel.getWidth(), kernel.getHeight())
427  except Exception as e:
428  continue
429 
430  fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter)
431  params = fit[0]
432  kernels = fit[1]
433  amp = 0.0
434  for p, k in zip(params, kernels):
435  amp += p * k.getSum()
436 
437  predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY()) for
438  k in range(kernel.getNKernelParameters())]
439 
440  #print cand.getSource().getId(), [a / amp for a in params], predict
441 
442  residuals.append([a / amp - p for a, p in zip(params, predict)])
443  candidates.append(cand)
444 
445  residuals = numpy.array(residuals)
446 
447  for k in range(kernel.getNKernelParameters()):
448  if False:
449  # Straight standard deviation
450  mean = residuals[:, k].mean()
451  rms = residuals[:, k].std()
452  elif False:
453  # Using interquartile range
454  sr = numpy.sort(residuals[:, k])
455  mean = sr[int(0.5*len(sr))] if len(sr) % 2 else \
456  0.5 * (sr[int(0.5*len(sr))] + sr[int(0.5*len(sr))+1])
457  rms = 0.74 * (sr[int(0.75*len(sr))] - sr[int(0.25*len(sr))])
458  else:
459  stats = afwMath.makeStatistics(residuals[:, k], afwMath.MEANCLIP | afwMath.STDEVCLIP)
460  mean = stats.getValue(afwMath.MEANCLIP)
461  rms = stats.getValue(afwMath.STDEVCLIP)
462 
463  rms = max(1.0e-4, rms) # Don't trust RMS below this due to numerical issues
464 
465  if display:
466  print("Mean for component %d is %f" % (k, mean))
467  print("RMS for component %d is %f" % (k, rms))
468  badCandidates = list()
469  for i, cand in enumerate(candidates):
470  if numpy.fabs(residuals[i, k] - mean) > self.config.spatialReject * rms:
471  badCandidates.append(i)
472 
473  badCandidates.sort(key=lambda x: numpy.fabs(residuals[x, k] - mean), reverse=True)
474 
475  numBad = numCandidatesToReject(len(badCandidates), iterNum,
476  self.config.nIterForPsf)
477 
478  for i, c in zip(range(min(len(badCandidates), numBad)), badCandidates):
479  cand = candidates[c]
480  if display:
481  print("Spatial clipping %d (%f,%f) based on %d: %f vs %f" % \
482  (cand.getSource().getId(), cand.getXCenter(), cand.getYCenter(), k,
483  residuals[badCandidates[i], k], self.config.spatialReject * rms))
484  cand.setStatus(afwMath.SpatialCellCandidate.BAD)
485 
486  #
487  # Display results
488  #
489  if display and displayIterations:
490  if displayExposure:
491  if iterNum > 0:
492  ds9.erase(frame=frame)
493  maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=True,
494  symb="o", size=8, frame=frame,
495  ctype=ds9.YELLOW, ctypeBad=ds9.RED, ctypeUnused=ds9.MAGENTA)
496  if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
497  maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
498  symb="o", size=10, frame=frame,
499  ctype=ds9.YELLOW, ctypeBad=ds9.RED)
500  if displayResiduals:
501  while True:
502  try:
503  maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
504  normalize=normalizeResiduals,
505  showBadCandidates=showBadCandidates)
506  maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=5,
507  normalize=normalizeResiduals,
508  showBadCandidates=showBadCandidates,
509  variance=True)
510  except:
511  if not showBadCandidates:
512  showBadCandidates = True
513  continue
514  break
515 
516  if displayPsfComponents:
517  maUtils.showPsf(psf, eigenValues, frame=6)
518  if displayPsfMosaic:
519  maUtils.showPsfMosaic(exposure, psf, frame=7, showFwhm=True)
520  ds9.scale('linear', 0, 1, frame=7)
521  if displayPsfSpatialModel:
522  maUtils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True,
523  matchKernelAmplitudes=matchKernelAmplitudes,
524  keepPlots=keepMatplotlibPlots)
525 
526  if pause:
527  while True:
528  try:
529  reply = input("Next iteration? [ynchpqQs] ").strip()
530  except EOFError:
531  reply = "n"
532 
533  reply = reply.split()
534  if reply:
535  reply, args = reply[0], reply[1:]
536  else:
537  reply = ""
538 
539  if reply in ("", "c", "h", "n", "p", "q", "Q", "s", "y"):
540  if reply == "c":
541  pause = False
542  elif reply == "h":
543  print("c[ontinue without prompting] h[elp] n[o] p[db] q[uit displaying] " \
544  "s[ave fileName] y[es]")
545  continue
546  elif reply == "p":
547  import pdb
548  pdb.set_trace()
549  elif reply == "q":
550  display = False
551  elif reply == "Q":
552  sys.exit(1)
553  elif reply == "s":
554  fileName = args.pop(0)
555  if not fileName:
556  print("Please provide a filename")
557  continue
558 
559  print("Saving to %s" % fileName)
560  maUtils.saveSpatialCellSet(psfCellSet, fileName=fileName)
561  continue
562  break
563  else:
564  print("Unrecognised response: %s" % reply, file=sys.stderr)
565 
566  if reply == "n":
567  break
568 
569  # One last time, to take advantage of the last iteration
570  psf, eigenValues, nEigenComponents, fitChi2 = \
571  self._fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
572 
573  #
574  # Display code for debugging
575  #
576  if display and reply != "n":
577  if displayExposure:
578  maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=True,
579  symb="o", ctype=ds9.YELLOW, ctypeBad=ds9.RED, size=8, frame=frame)
580  if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
581  maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
582  symb="o", ctype=ds9.YELLOW, ctypeBad=ds9.RED,
583  size=10, frame=frame)
584  if displayResiduals:
585  maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
586  normalize=normalizeResiduals,
587  showBadCandidates=showBadCandidates)
588 
589  if displayPsfComponents:
590  maUtils.showPsf(psf, eigenValues, frame=6)
591 
592  if displayPsfMosaic:
593  maUtils.showPsfMosaic(exposure, psf, frame=7, showFwhm=True)
594  ds9.scale("linear", 0, 1, frame=7)
595  if displayPsfSpatialModel:
596  maUtils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True,
597  matchKernelAmplitudes=matchKernelAmplitudes,
598  keepPlots=keepMatplotlibPlots)
599  #
600  # Generate some QA information
601  #
602  # Count PSF stars
603  #
604  numGoodStars = 0
605  numAvailStars = 0
606 
607  avgX = 0.0
608  avgY = 0.0
609 
610  for cell in psfCellSet.getCellList():
611  for cand in cell.begin(False): # don't ignore BAD stars
612  numAvailStars += 1
613 
614  for cand in cell.begin(True): # do ignore BAD stars
615  src = cand.getSource()
616  if flagKey is not None:
617  src.set(flagKey, True)
618  avgX += src.getX()
619  avgY += src.getY()
620  numGoodStars += 1
621 
622  avgX /= numGoodStars
623  avgY /= numGoodStars
624 
625  if metadata is not None:
626  metadata.set("spatialFitChi2", fitChi2)
627  metadata.set("numGoodStars", numGoodStars)
628  metadata.set("numAvailStars", numAvailStars)
629  metadata.set("avgX", avgX)
630  metadata.set("avgY", avgY)
631 
632  psf = PcaPsf(psf.getKernel(), afwGeom.Point2D(avgX, avgY))
633 
634  return psf, psfCellSet
635 
636 
637 def candidatesIter(psfCellSet, ignoreBad=True):
638  """!Generator for Psf candidates
639 
640  This allows two 'for' loops to be reduced to one.
641 
642  \param psfCellSet SpatialCellSet of PSF candidates
643  \param ignoreBad Ignore candidates flagged as BAD?
644  \return SpatialCell, PsfCandidate
645  """
646  for cell in psfCellSet.getCellList():
647  for cand in cell.begin(ignoreBad):
648  yield (cell, cand)
649 
650 psfDeterminerRegistry.register("pca", PcaPsfDeterminerTask)
int countPsfCandidates(lsst::afw::math::SpatialCellSet const &psfCells, int const nStarPerCell=-1)
STL namespace.
std::pair< std::vector< double >, lsst::afw::math::KernelList > fitKernelParamsToImage(lsst::afw::math::LinearCombinationKernel const &kernel, Image const &image, lsst::afw::geom::Point2D const &pos)
std::pair< bool, double > fitSpatialKernelFromPsfCandidates(lsst::afw::math::Kernel *kernel, lsst::afw::math::SpatialCellSet const &psfCells, int const nStarPerCell=-1, double const tolerance=1e-5, double const lambda=0.0)
std::pair< std::shared_ptr< lsst::afw::math::LinearCombinationKernel >, std::vector< double > > createKernelFromPsfCandidates(lsst::afw::math::SpatialCellSet const &psfCells, lsst::afw::geom::Extent2I const &dims, lsst::afw::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)
def determinePsf
Determine a PCA PSF model for an exposure given a list of PSF candidates.
def candidatesIter
Generator for Psf candidates.