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