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