LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
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
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(
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 check=lambda x: x >= 1
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 """A measurePsfTask psf estimator.
165 """
166 ConfigClass = PcaPsfDeterminerConfig
167
168 def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents):
169 PsfCandidateF.setPixelThreshold(self.config.pixelThreshold)
170 PsfCandidateF.setMaskBlends(self.config.doMaskBlends)
171 #
172 # Loop trying to use nEigenComponents, but allowing smaller numbers if necessary
173 #
174 for nEigen in range(nEigenComponents, 0, -1):
175 # Determine KL components
176 try:
177 kernel, eigenValues = createKernelFromPsfCandidates(
178 psfCellSet, exposure.getDimensions(), exposure.getXY0(), nEigen,
179 self.config.spatialOrder, kernelSize, self.config.nStarPerCell,
180 bool(self.config.constantWeight))
181
182 break # OK, we can get nEigen components
183 except pexExceptions.LengthError as e:
184 if nEigen == 1: # can't go any lower
185 raise IndexError("No viable PSF candidates survive")
186
187 self.log.warning("%s: reducing number of eigen components", e.what())
188 #
189 # We got our eigen decomposition so let's use it
190 #
191 # Express eigenValues in units of reduced chi^2 per star
192 size = kernelSize + 2*self.config.borderWidth
193 nu = size*size - 1 # number of degrees of freedom/star for chi^2
194 eigenValues = [val/float(countPsfCandidates(psfCellSet, self.config.nStarPerCell)*nu)
195 for val in eigenValues]
196
197 # Fit spatial model
199 kernel, psfCellSet, bool(self.config.nonLinearSpatialFit),
200 self.config.nStarPerCellSpatialFit, self.config.tolerance, self.config.lam)
201
202 psf = PcaPsf(kernel)
203
204 return psf, eigenValues, nEigen, chi2
205
206 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
207 """Determine a PCA PSF model for an exposure given a list of PSF candidates.
208
209 Parameters
210 ----------
211 exposure : `lsst.afw.image.Exposure`
212 Exposure containing the psf candidates.
213 psfCandidateList : `list` of `lsst.meas.algorithms.PsfCandidate`
214 A sequence of PSF candidates typically obtained by detecting sources
215 and then running them through a star selector.
216 metadata : `lsst.daf.base import PropertyList` or `None`, optional
217 A home for interesting tidbits of information.
218 flagKey : `str`, optional
219 Schema key used to mark sources actually used in PSF determination.
220
221 Returns
222 -------
224 The measured PSF.
225 psfCellSet : `lsst.afw.math.SpatialCellSet`
226 The PSF candidates.
227 """
228 import lsstDebug
229 display = lsstDebug.Info(__name__).display
230 displayExposure = lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells
231 displayPsfCandidates = lsstDebug.Info(__name__).displayPsfCandidates # show the viable candidates
232 displayIterations = lsstDebug.Info(__name__).displayIterations # display on each PSF iteration
233 displayPsfComponents = lsstDebug.Info(__name__).displayPsfComponents # show the PCA components
234 displayResiduals = lsstDebug.Info(__name__).displayResiduals # show residuals
235 displayPsfMosaic = lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y)
236 # match Kernel amplitudes for spatial plots
237 matchKernelAmplitudes = lsstDebug.Info(__name__).matchKernelAmplitudes
238 # Keep matplotlib alive post mortem
239 keepMatplotlibPlots = lsstDebug.Info(__name__).keepMatplotlibPlots
240 displayPsfSpatialModel = lsstDebug.Info(__name__).displayPsfSpatialModel # Plot spatial model?
241 showBadCandidates = lsstDebug.Info(__name__).showBadCandidates # Include bad candidates
242 # Normalize residuals by object amplitude
243 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals
244 pause = lsstDebug.Info(__name__).pause # Prompt user after each iteration?
245
246 if display:
247 afwDisplay.setDefaultMaskTransparency(75)
248 if display > 1:
249 pause = True
250
251 mi = exposure.getMaskedImage()
252
253 if len(psfCandidateList) == 0:
254 raise RuntimeError("No PSF candidates supplied.")
255
256 # construct and populate a spatial cell set
257 bbox = mi.getBBox()
258 psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY)
259 sizes = []
260 for i, psfCandidate in enumerate(psfCandidateList):
261 if psfCandidate.getSource().getPsfFluxFlag(): # bad measurement
262 continue
263
264 try:
265 psfCellSet.insertCandidate(psfCandidate)
266 except Exception as e:
267 self.log.debug("Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
268 continue
269 source = psfCandidate.getSource()
270
271 quad = afwGeom.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
272 axes = afwEll.Axes(quad)
273 sizes.append(axes.getA())
274 if len(sizes) == 0:
275 raise RuntimeError("No usable PSF candidates supplied")
276 nEigenComponents = self.config.nEigenComponents # initial version
277
278 if self.config.kernelSize >= 15:
279 self.log.warning("WARNING: NOT scaling kernelSize by stellar quadrupole moment "
280 "because config.kernelSize=%s >= 15; "
281 "using config.kernelSize as the width, instead",
282 self.config.kernelSize)
283 actualKernelSize = int(self.config.kernelSize)
284 else:
285 medSize = numpy.median(sizes)
286 actualKernelSize = 2*int(self.config.kernelSize*math.sqrt(medSize) + 0.5) + 1
287 if actualKernelSize < self.config.kernelSizeMin:
288 actualKernelSize = self.config.kernelSizeMin
289 if actualKernelSize > self.config.kernelSizeMax:
290 actualKernelSize = self.config.kernelSizeMax
291
292 if display:
293 print("Median size=%s" % (medSize,))
294 self.log.trace("Kernel size=%s", actualKernelSize)
295
296 # Set size of image returned around candidate
297 psfCandidateList[0].setHeight(actualKernelSize)
298 psfCandidateList[0].setWidth(actualKernelSize)
299
300 if self.config.doRejectBlends:
301 # Remove blended candidates completely
302 blendedCandidates = [] # Candidates to remove; can't do it while iterating
303 for cell, cand in candidatesIter(psfCellSet, False):
304 if len(cand.getSource().getFootprint().getPeaks()) > 1:
305 blendedCandidates.append((cell, cand))
306 continue
307 if display:
308 print("Removing %d blended Psf candidates" % len(blendedCandidates))
309 for cell, cand in blendedCandidates:
310 cell.removeCandidate(cand)
311 if sum(1 for cand in candidatesIter(psfCellSet, False)) == 0:
312 raise RuntimeError("All PSF candidates removed as blends")
313
314 if display:
315 if displayExposure:
316 disp = afwDisplay.Display(frame=0)
317 disp.mtv(exposure, title="psf determination")
318 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, symb="o",
319 ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW,
320 size=4, display=disp)
321
322 #
323 # Do a PCA decomposition of those PSF candidates
324 #
325 reply = "y" # used in interactive mode
326 for iterNum in range(self.config.nIterForPsf):
327 if display and displayPsfCandidates: # Show a mosaic of usable PSF candidates
328
329 stamps = []
330 for cell in psfCellSet.getCellList():
331 for cand in cell.begin(not showBadCandidates): # maybe include bad candidates
332 try:
333 im = cand.getMaskedImage()
334
335 chi2 = cand.getChi2()
336 if chi2 > 1e100:
337 chi2 = numpy.nan
338
339 stamps.append((im, "%d%s" %
340 (utils.splitId(cand.getSource().getId(), True)["objId"], chi2),
341 cand.getStatus()))
342 except Exception:
343 continue
344
345 if len(stamps) == 0:
346 print("WARNING: No PSF candidates to show; try setting showBadCandidates=True")
347 else:
348 mos = afwDisplay.utils.Mosaic()
349 for im, label, status in stamps:
350 im = type(im)(im, True)
351 try:
352 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue()
353 except NotImplementedError:
354 pass
355
356 mos.append(im, label,
357 (afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else
358 afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else
359 afwDisplay.RED))
360
361 disp8 = afwDisplay.Display(frame=8)
362 mos.makeMosaic(display=disp8, title="Psf Candidates")
363
364 # Re-fit until we don't have any candidates with naughty chi^2 values influencing the fit
365 cleanChi2 = False # Any naughty (negative/NAN) chi^2 values?
366 while not cleanChi2:
367 cleanChi2 = True
368 #
369 # First, estimate the PSF
370 #
371 psf, eigenValues, nEigenComponents, fitChi2 = \
372 self._fitPsf_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
373 #
374 # In clipping, allow all candidates to be innocent until proven guilty on this iteration.
375 # Throw out any prima facie guilty candidates (naughty chi^2 values)
376 #
377 for cell in psfCellSet.getCellList():
378 awfulCandidates = []
379 for cand in cell.begin(False): # include bad candidates
380 cand.setStatus(afwMath.SpatialCellCandidate.UNKNOWN) # until proven guilty
381 rchi2 = cand.getChi2()
382 if not numpy.isfinite(rchi2) or rchi2 <= 0:
383 # Guilty prima facie
384 awfulCandidates.append(cand)
385 cleanChi2 = False
386 self.log.debug("chi^2=%s; id=%s",
387 cand.getChi2(), cand.getSource().getId())
388 for cand in awfulCandidates:
389 if display:
390 print("Removing bad candidate: id=%d, chi^2=%f" %
391 (cand.getSource().getId(), cand.getChi2()))
392 cell.removeCandidate(cand)
393
394 #
395 # Clip out bad fits based on reduced chi^2
396 #
397 badCandidates = list()
398 for cell in psfCellSet.getCellList():
399 for cand in cell.begin(False): # include bad candidates
400 rchi2 = cand.getChi2() # reduced chi^2 when fitting PSF to candidate
401 assert rchi2 > 0
402 if rchi2 > self.config.reducedChi2ForPsfCandidates:
403 badCandidates.append(cand)
404
405 badCandidates.sort(key=lambda x: x.getChi2(), reverse=True)
406 numBad = numCandidatesToReject(len(badCandidates), iterNum,
407 self.config.nIterForPsf)
408 for i, c in zip(range(numBad), badCandidates):
409 if display:
410 chi2 = c.getChi2()
411 if chi2 > 1e100:
412 chi2 = numpy.nan
413
414 print("Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2))
415 c.setStatus(afwMath.SpatialCellCandidate.BAD)
416
417 #
418 # Clip out bad fits based on spatial fitting.
419 #
420 # This appears to be better at getting rid of sources that have a single dominant kernel component
421 # (other than the zeroth; e.g., a nearby contaminant) because the surrounding sources (which help
422 # set the spatial model) don't contain that kernel component, and so the spatial modeling
423 # downweights the component.
424 #
425
426 residuals = list()
427 candidates = list()
428 kernel = psf.getKernel()
429 noSpatialKernel = psf.getKernel()
430 for cell in psfCellSet.getCellList():
431 for cand in cell.begin(False):
432 candCenter = lsst.geom.PointD(cand.getXCenter(), cand.getYCenter())
433 try:
434 im = cand.getMaskedImage(kernel.getWidth(), kernel.getHeight())
435 except Exception:
436 continue
437
438 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter)
439 params = fit[0]
440 kernels = fit[1]
441 amp = 0.0
442 for p, k in zip(params, kernels):
443 amp += p*k.getSum()
444
445 predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY()) for
446 k in range(kernel.getNKernelParameters())]
447
448 residuals.append([a/amp - p for a, p in zip(params, predict)])
449 candidates.append(cand)
450
451 residuals = numpy.array(residuals)
452
453 for k in range(kernel.getNKernelParameters()):
454 if False:
455 # Straight standard deviation
456 mean = residuals[:, k].mean()
457 rms = residuals[:, k].std()
458 elif False:
459 # Using interquartile range
460 sr = numpy.sort(residuals[:, k])
461 mean = (sr[int(0.5*len(sr))] if len(sr)%2 else
462 0.5*(sr[int(0.5*len(sr))] + sr[int(0.5*len(sr)) + 1]))
463 rms = 0.74*(sr[int(0.75*len(sr))] - sr[int(0.25*len(sr))])
464 else:
465 stats = afwMath.makeStatistics(residuals[:, k], afwMath.MEANCLIP | afwMath.STDEVCLIP)
466 mean = stats.getValue(afwMath.MEANCLIP)
467 rms = stats.getValue(afwMath.STDEVCLIP)
468
469 rms = max(1.0e-4, rms) # Don't trust RMS below this due to numerical issues
470
471 if display:
472 print("Mean for component %d is %f" % (k, mean))
473 print("RMS for component %d is %f" % (k, rms))
474 badCandidates = list()
475 for i, cand in enumerate(candidates):
476 if numpy.fabs(residuals[i, k] - mean) > self.config.spatialReject*rms:
477 badCandidates.append(i)
478
479 badCandidates.sort(key=lambda x: numpy.fabs(residuals[x, k] - mean), reverse=True)
480
481 numBad = numCandidatesToReject(len(badCandidates), iterNum,
482 self.config.nIterForPsf)
483
484 for i, c in zip(range(min(len(badCandidates), numBad)), badCandidates):
485 cand = candidates[c]
486 if display:
487 print("Spatial clipping %d (%f,%f) based on %d: %f vs %f" %
488 (cand.getSource().getId(), cand.getXCenter(), cand.getYCenter(), k,
489 residuals[badCandidates[i], k], self.config.spatialReject*rms))
490 cand.setStatus(afwMath.SpatialCellCandidate.BAD)
491
492 #
493 # Display results
494 #
495 if display and displayIterations:
496 if displayExposure:
497 if iterNum > 0:
498 disp.erase()
499 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=True,
500 symb="o", size=8, display=disp, ctype=afwDisplay.YELLOW,
501 ctypeBad=afwDisplay.RED, ctypeUnused=afwDisplay.MAGENTA)
502 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
503 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
504 symb="o", size=10, display=disp,
505 ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED)
506 if displayResiduals:
507 while True:
508 try:
509 disp4 = afwDisplay.Display(frame=4)
510 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
511 normalize=normalizeResiduals,
512 showBadCandidates=showBadCandidates)
513 disp5 = afwDisplay.Display(frame=5)
514 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp5,
515 normalize=normalizeResiduals,
516 showBadCandidates=showBadCandidates,
517 variance=True)
518 except Exception:
519 if not showBadCandidates:
520 showBadCandidates = True
521 continue
522 break
523
524 if displayPsfComponents:
525 disp6 = afwDisplay.Display(frame=6)
526 utils.showPsf(psf, eigenValues, display=disp6)
527 if displayPsfMosaic:
528 disp7 = afwDisplay.Display(frame=7)
529 utils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=True)
530 disp7.scale('linear', 0, 1)
531 if displayPsfSpatialModel:
532 utils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True,
533 matchKernelAmplitudes=matchKernelAmplitudes,
534 keepPlots=keepMatplotlibPlots)
535
536 if pause:
537 while True:
538 try:
539 reply = input("Next iteration? [ynchpqQs] ").strip()
540 except EOFError:
541 reply = "n"
542
543 reply = reply.split()
544 if reply:
545 reply, args = reply[0], reply[1:]
546 else:
547 reply = ""
548
549 if reply in ("", "c", "h", "n", "p", "q", "Q", "s", "y"):
550 if reply == "c":
551 pause = False
552 elif reply == "h":
553 print("c[ontinue without prompting] h[elp] n[o] p[db] q[uit displaying] "
554 "s[ave fileName] y[es]")
555 continue
556 elif reply == "p":
557 import pdb
558 pdb.set_trace()
559 elif reply == "q":
560 display = False
561 elif reply == "Q":
562 sys.exit(1)
563 elif reply == "s":
564 fileName = args.pop(0)
565 if not fileName:
566 print("Please provide a filename")
567 continue
568
569 print("Saving to %s" % fileName)
570 utils.saveSpatialCellSet(psfCellSet, fileName=fileName)
571 continue
572 break
573 else:
574 print("Unrecognised response: %s" % reply, file=sys.stderr)
575
576 if reply == "n":
577 break
578
579 # One last time, to take advantage of the last iteration
580 psf, eigenValues, nEigenComponents, fitChi2 = \
581 self._fitPsf_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
582
583 #
584 # Display code for debugging
585 #
586 if display and reply != "n":
587 disp = afwDisplay.Display(frame=0)
588 if displayExposure:
589 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=True,
590 symb="o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
591 size=8, display=disp)
592 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
593 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
594 symb="o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED,
595 size=10, display=disp)
596 if displayResiduals:
597 disp4 = afwDisplay.Display(frame=4)
598 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
599 normalize=normalizeResiduals,
600 showBadCandidates=showBadCandidates)
601
602 if displayPsfComponents:
603 disp6 = afwDisplay.Display(frame=6)
604 utils.showPsf(psf, eigenValues, display=disp6)
605
606 if displayPsfMosaic:
607 disp7 = afwDisplay.Display(frame=7)
608 utils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=True)
609 disp7.scale("linear", 0, 1)
610 if displayPsfSpatialModel:
611 utils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=True,
612 matchKernelAmplitudes=matchKernelAmplitudes,
613 keepPlots=keepMatplotlibPlots)
614 #
615 # Generate some QA information
616 #
617 # Count PSF stars
618 #
619 numGoodStars = 0
620 numAvailStars = 0
621
622 avgX = 0.0
623 avgY = 0.0
624
625 for cell in psfCellSet.getCellList():
626 for cand in cell.begin(False): # don't ignore BAD stars
627 numAvailStars += 1
628
629 for cand in cell.begin(True): # do ignore BAD stars
630 src = cand.getSource()
631 if flagKey is not None:
632 src.set(flagKey, True)
633 avgX += src.getX()
634 avgY += src.getY()
635 numGoodStars += 1
636
637 avgX /= numGoodStars
638 avgY /= numGoodStars
639
640 if metadata is not None:
641 metadata["spatialFitChi2"] = fitChi2
642 metadata["numGoodStars"] = numGoodStars
643 metadata["numAvailStars"] = numAvailStars
644 metadata["avgX"] = avgX
645 metadata["avgY"] = avgY
646
647 psf = PcaPsf(psf.getKernel(), lsst.geom.Point2D(avgX, avgY))
648
649 return psf, psfCellSet
650
651
652def candidatesIter(psfCellSet, ignoreBad=True):
653 """Generator for Psf candidates.
654
655 This allows two 'for' loops to be reduced to one.
656
657 Parameters
658 ----------
659 psfCellSet : `lsst.afw.math.SpatialCellSet`
660 SpatialCellSet of PSF candidates.
661 ignoreBad : `bool`, optional
662 Ignore candidates flagged as BAD?
663
664 Yields
665 -------
667 A SpatialCell.
669 A PsfCandidate.
670 """
671 for cell in psfCellSet.getCellList():
672 for cand in cell.begin(ignoreBad):
673 yield (cell, cand)
674
675
676psfDeterminerRegistry.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: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.