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