LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
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 
24 import math
25 import sys
26 
27 import numpy
28 
29 import lsst.pex.config as pexConfig
30 import lsst.pex.exceptions as pexExceptions
31 import lsst.geom
32 import lsst.afw.geom as afwGeom
33 import lsst.afw.geom.ellipses as afwEll
34 import lsst.afw.display as afwDisplay
35 import lsst.afw.math as afwMath
36 from .psfDeterminer import BasePsfDeterminerTask, psfDeterminerRegistry
37 from .psfCandidate import PsfCandidateF
38 from .spatialModelPsf import createKernelFromPsfCandidates, countPsfCandidates, \
39  fitSpatialKernelFromPsfCandidates, fitKernelParamsToImage
40 from .pcaPsf import PcaPsf
41 from . import utils
42 
43 
44 def 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 
70 class 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
197  status, chi2 = fitSpatialKernelFromPsfCandidates(
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  -------
222  psf : `lsst.meas.algorithms.PcaPsf`
223  The measured PSF.
224  psfCellSet : `lsst.afw.math.SpatialCellSet`
225  The PSF candidates.
226  """
227  import lsstDebug
228  display = lsstDebug.Info(__name__).display
229  displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
230  displayPsfCandidates = lsstDebug.Info(__name__).displayPsfCandidates # show the viable candidates
231  displayIterations = lsstDebug.Info(__name__).displayIterations # display on each PSF iteration
232  displayPsfComponents = lsstDebug.Info(__name__).displayPsfComponents # show the PCA components
233  displayResiduals = lsstDebug.Info(__name__).displayResiduals # show residuals
234  displayPsfMosaic = lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
235  # match Kernel amplitudes for spatial plots
236  matchKernelAmplitudes = lsstDebug.Info(__name__).matchKernelAmplitudes
237  # Keep matplotlib alive post mortem
238  keepMatplotlibPlots = lsstDebug.Info(__name__).keepMatplotlibPlots
239  displayPsfSpatialModel = lsstDebug.Info(__name__).displayPsfSpatialModel # Plot spatial model?
240  showBadCandidates = lsstDebug.Info(__name__).showBadCandidates # Include bad candidates
241  # Normalize residuals by object amplitude
242  normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals
243  pause = lsstDebug.Info(__name__).pause # Prompt user after each iteration?
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 
651 def 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  -------
665  cell : `lsst.afw.math.SpatialCell`
666  A SpatialCell.
667  cand : `lsst.meas.algorithms.PsfCandidate`
668  A PsfCandidate.
669  """
670  for cell in psfCellSet.getCellList():
671  for cand in cell.begin(ignoreBad):
672  yield (cell, cand)
673 
674 
675 psfDeterminerRegistry.register("pca", PcaPsfDeterminerTask)
int min
int max
table::Key< int > type
Definition: Detector.cc:163
An ellipse core with quadrupole moments as parameters.
Definition: Quadrupole.h:47
A collection of SpatialCells covering an entire image.
Definition: SpatialCell.h:383
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents)
Reports attempts to exceed implementation-defined length limits for some classes.
Definition: Runtime.h:76
daf::base::PropertyList * list
Definition: fits.cc:913
bool strip
Definition: fits.cc:911
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition: Statistics.h:359
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.