1 from __future__
import print_function
2 from builtins
import input
3 from builtins
import zip
4 from builtins
import range
37 from .psfDeterminer
import BasePsfDeterminerTask, psfDeterminerRegistry
38 from .
import algorithmsLib
39 from .
import utils
as maUtils
41 __all__ = [
"PcaPsfDeterminerConfig",
"PcaPsfDeterminerTask"]
44 """Return the number of PSF candidates to be rejected.
46 The number of candidates being rejected on each iteration gradually
47 increases, so that on the Nth of M iterations we reject N/M of the bad
52 numBadCandidates : int
53 Number of bad candidates under consideration.
56 The number of the current PSF iteration.
59 The total number of PSF iterations.
64 Number of candidates to reject.
66 return int(numBadCandidates * (numIter + 1) // totalIter + 0.5)
69 nonLinearSpatialFit = pexConfig.Field(
70 doc=
"Use non-linear fitter for spatial variation of Kernel",
74 nEigenComponents = pexConfig.Field(
75 doc=
"number of eigen components for PSF kernel creation",
79 spatialOrder = pexConfig.Field(
80 doc=
"specify spatial order for PSF kernel creation",
84 sizeCellX = pexConfig.Field(
85 doc=
"size of cell used to determine PSF (pixels, column direction)",
89 check=
lambda x: x >= 10,
91 sizeCellY = pexConfig.Field(
92 doc=
"size of cell used to determine PSF (pixels, row direction)",
94 default=sizeCellX.default,
96 check=
lambda x: x >= 10,
98 nStarPerCell = pexConfig.Field(
99 doc=
"number of stars per psf cell for PSF kernel creation",
103 borderWidth = pexConfig.Field(
104 doc=
"Number of pixels to ignore around the edge of PSF candidate postage stamps",
108 nStarPerCellSpatialFit = pexConfig.Field(
109 doc=
"number of stars per psf Cell for spatial fitting",
113 constantWeight = pexConfig.Field(
114 doc=
"Should each PSF candidate be given the same weight, independent of magnitude?",
118 nIterForPsf = pexConfig.Field(
119 doc=
"number of iterations of PSF candidate star list",
123 tolerance = pexConfig.Field(
124 doc=
"tolerance of spatial fitting",
128 lam = pexConfig.Field(
129 doc=
"floor for variance is lam*data",
133 reducedChi2ForPsfCandidates = pexConfig.Field(
134 doc=
"for psf candidate evaluation",
138 spatialReject = pexConfig.Field(
139 doc=
"Rejection threshold (stdev) for candidates based on spatial fit",
143 pixelThreshold = pexConfig.Field(
144 doc=
"Threshold (stdev) for rejecting extraneous pixels around candidate; applied if positive",
148 doRejectBlends = pexConfig.Field(
149 doc=
"Reject candidates that are blended?",
153 doMaskBlends = pexConfig.Field(
154 doc=
"Mask blends in image?",
162 A measurePsfTask psf estimator
164 ConfigClass = PcaPsfDeterminerConfig
166 def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents):
167 algorithmsLib.PsfCandidateF.setPixelThreshold(self.config.pixelThreshold)
168 algorithmsLib.PsfCandidateF.setMaskBlends(self.config.doMaskBlends)
172 for nEigen
in range(nEigenComponents, 0, -1):
175 kernel, eigenValues = algorithmsLib.createKernelFromPsfCandidates(
176 psfCellSet, exposure.getDimensions(), exposure.getXY0(), nEigen,
177 self.config.spatialOrder, kernelSize, self.config.nStarPerCell,
178 bool(self.config.constantWeight))
181 except pexExceptions.LengthError
as e:
183 raise IndexError(
"No viable PSF candidates survive")
185 self.log.warn(
"%s: reducing number of eigen components" % e.what())
190 size = kernelSize + 2*self.config.borderWidth
192 eigenValues = [l/float(algorithmsLib.countPsfCandidates(psfCellSet, self.config.nStarPerCell)*nu)
193 for l
in eigenValues]
196 status, chi2 = algorithmsLib.fitSpatialKernelFromPsfCandidates(
197 kernel, psfCellSet, bool(self.config.nonLinearSpatialFit),
198 self.config.nStarPerCellSpatialFit, self.config.tolerance, self.config.lam)
200 psf = algorithmsLib.PcaPsf(kernel)
202 return psf, eigenValues, nEigen, chi2
204 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
205 """!Determine a PCA PSF model for an exposure given a list of PSF candidates
207 \param[in] exposure exposure containing the psf candidates (lsst.afw.image.Exposure)
208 \param[in] psfCandidateList a sequence of PSF candidates (each an lsst.meas.algorithms.PsfCandidate);
209 typically obtained by detecting sources and then running them through a star selector
210 \param[in,out] metadata a home for interesting tidbits of information
211 \param[in] flagKey schema key used to mark sources actually used in PSF determination
214 - psf: the measured PSF, an lsst.meas.algorithms.PcaPsf
215 - cellSet: an lsst.afw.math.SpatialCellSet containing the PSF candidates
220 displayPsfCandidates =
lsstDebug.Info(__name__).displayPsfCandidates
222 displayPsfComponents =
lsstDebug.Info(__name__).displayPsfComponents
226 matchKernelAmplitudes =
lsstDebug.Info(__name__).matchKernelAmplitudes
228 keepMatplotlibPlots =
lsstDebug.Info(__name__).keepMatplotlibPlots
229 displayPsfSpatialModel =
lsstDebug.Info(__name__).displayPsfSpatialModel
238 mi = exposure.getMaskedImage()
240 if len(psfCandidateList) == 0:
241 raise RuntimeError(
"No PSF candidates supplied.")
247 for i, psfCandidate
in enumerate(psfCandidateList):
248 if psfCandidate.getSource().getPsfFluxFlag():
252 psfCellSet.insertCandidate(psfCandidate)
253 except Exception
as e:
254 self.log.debug(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
256 source = psfCandidate.getSource()
258 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
259 axes = afwEll.Axes(quad)
260 sizes.append(axes.getA())
262 raise RuntimeError(
"No usable PSF candidates supplied")
263 nEigenComponents = self.config.nEigenComponents
265 if self.config.kernelSize >= 15:
266 self.log.warn(
"WARNING: NOT scaling kernelSize by stellar quadrupole moment " +
267 "because config.kernelSize=%s >= 15; using config.kernelSize as as the width, instead",
268 self.config.kernelSize)
269 actualKernelSize = int(self.config.kernelSize)
271 medSize = numpy.median(sizes)
272 actualKernelSize = 2 * int(self.config.kernelSize * math.sqrt(medSize) + 0.5) + 1
273 if actualKernelSize < self.config.kernelSizeMin:
274 actualKernelSize = self.config.kernelSizeMin
275 if actualKernelSize > self.config.kernelSizeMax:
276 actualKernelSize = self.config.kernelSizeMax
279 print(
"Median size=%s" % (medSize,))
280 self.log.trace(
"Kernel size=%s", actualKernelSize)
283 psfCandidateList[0].setHeight(actualKernelSize)
284 psfCandidateList[0].setWidth(actualKernelSize)
286 if self.config.doRejectBlends:
288 blendedCandidates = []
290 if len(cand.getSource().getFootprint().getPeaks()) > 1:
291 blendedCandidates.append((cell, cand))
294 print(
"Removing %d blended Psf candidates" % len(blendedCandidates))
295 for cell, cand
in blendedCandidates:
296 cell.removeCandidate(cand)
298 raise RuntimeError(
"All PSF candidates removed as blends")
303 ds9.mtv(exposure, frame=frame, title=
"psf determination")
304 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell,
305 symb=
"o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW,
312 for iterNum
in range(self.config.nIterForPsf):
313 if display
and displayPsfCandidates:
318 for cell
in psfCellSet.getCellList():
319 for cand
in cell.begin(
not showBadCandidates):
320 cand = algorithmsLib.PsfCandidateF.cast(cand)
323 im = cand.getMaskedImage()
325 chi2 = cand.getChi2()
329 stamps.append((im,
"%d%s" %
330 (maUtils.splitId(cand.getSource().getId(),
True)[
"objId"], chi2),
332 except Exception
as e:
336 print(
"WARNING: No PSF candidates to show; try setting showBadCandidates=True")
338 mos = displayUtils.Mosaic()
339 for im, label, status
in stamps:
340 im = type(im)(im,
True)
343 except NotImplementedError:
346 mos.append(im, label,
347 ds9.GREEN
if status == afwMath.SpatialCellCandidate.GOOD
else
348 ds9.YELLOW
if status == afwMath.SpatialCellCandidate.UNKNOWN
else ds9.RED)
350 mos.makeMosaic(frame=8, title=
"Psf Candidates")
359 psf, eigenValues, nEigenComponents, fitChi2 = \
360 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
365 for cell
in psfCellSet.getCellList():
367 for cand
in cell.begin(
False):
368 cand = algorithmsLib.PsfCandidateF.cast(cand)
369 cand.setStatus(afwMath.SpatialCellCandidate.UNKNOWN)
370 rchi2 = cand.getChi2()
371 if not numpy.isfinite(rchi2)
or rchi2 <= 0:
373 awfulCandidates.append(cand)
375 self.log.debug(
"chi^2=%s; id=%s",
376 cand.getChi2(), cand.getSource().getId())
377 for cand
in awfulCandidates:
379 print(
"Removing bad candidate: id=%d, chi^2=%f" % \
380 (cand.getSource().getId(), cand.getChi2()))
381 cell.removeCandidate(cand)
386 badCandidates = list()
387 for cell
in psfCellSet.getCellList():
388 for cand
in cell.begin(
False):
389 cand = algorithmsLib.PsfCandidateF.cast(cand)
390 rchi2 = cand.getChi2()
392 if rchi2 > self.config.reducedChi2ForPsfCandidates:
393 badCandidates.append(cand)
395 badCandidates.sort(key=
lambda x: x.getChi2(), reverse=
True)
397 self.config.nIterForPsf)
398 for i, c
in zip(range(numBad), badCandidates):
404 print(
"Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2))
405 c.setStatus(afwMath.SpatialCellCandidate.BAD)
418 kernel = psf.getKernel()
419 noSpatialKernel = afwMath.cast_LinearCombinationKernel(psf.getKernel())
420 for cell
in psfCellSet.getCellList():
421 for cand
in cell.begin(
False):
422 cand = algorithmsLib.PsfCandidateF.cast(cand)
425 im = cand.getMaskedImage(kernel.getWidth(), kernel.getHeight())
426 except Exception
as e:
429 fit = algorithmsLib.fitKernelParamsToImage(noSpatialKernel, im, candCenter)
433 for p, k
in zip(params, kernels):
434 amp += p * afwMath.cast_FixedKernel(k).getSum()
436 predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY())
for
437 k
in range(kernel.getNKernelParameters())]
441 residuals.append([a / amp - p
for a, p
in zip(params, predict)])
442 candidates.append(cand)
444 residuals = numpy.array(residuals)
446 for k
in range(kernel.getNKernelParameters()):
449 mean = residuals[:, k].mean()
450 rms = residuals[:, k].
std()
453 sr = numpy.sort(residuals[:, k])
454 mean = sr[int(0.5*len(sr))]
if len(sr) % 2
else \
455 0.5 * (sr[int(0.5*len(sr))] + sr[int(0.5*len(sr))+1])
456 rms = 0.74 * (sr[int(0.75*len(sr))] - sr[int(0.25*len(sr))])
459 mean = stats.getValue(afwMath.MEANCLIP)
460 rms = stats.getValue(afwMath.STDEVCLIP)
462 rms = max(1.0e-4, rms)
465 print(
"Mean for component %d is %f" % (k, mean))
466 print(
"RMS for component %d is %f" % (k, rms))
467 badCandidates = list()
468 for i, cand
in enumerate(candidates):
469 if numpy.fabs(residuals[i, k] - mean) > self.config.spatialReject * rms:
470 badCandidates.append(i)
472 badCandidates.sort(key=
lambda x: numpy.fabs(residuals[x, k] - mean), reverse=
True)
475 self.config.nIterForPsf)
477 for i, c
in zip(range(min(len(badCandidates), numBad)), badCandidates):
480 print(
"Spatial clipping %d (%f,%f) based on %d: %f vs %f" % \
481 (cand.getSource().getId(), cand.getXCenter(), cand.getYCenter(), k,
482 residuals[badCandidates[i], k], self.config.spatialReject * rms))
483 cand.setStatus(afwMath.SpatialCellCandidate.BAD)
488 if display
and displayIterations:
491 ds9.erase(frame=frame)
492 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=
True,
493 symb=
"o", size=8, frame=frame,
494 ctype=ds9.YELLOW, ctypeBad=ds9.RED, ctypeUnused=ds9.MAGENTA)
495 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
496 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
497 symb=
"o", size=10, frame=frame,
498 ctype=ds9.YELLOW, ctypeBad=ds9.RED)
502 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
503 normalize=normalizeResiduals,
504 showBadCandidates=showBadCandidates)
505 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=5,
506 normalize=normalizeResiduals,
507 showBadCandidates=showBadCandidates,
510 if not showBadCandidates:
511 showBadCandidates =
True
515 if displayPsfComponents:
516 maUtils.showPsf(psf, eigenValues, frame=6)
518 maUtils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
519 ds9.scale(
'linear', 0, 1, frame=7)
520 if displayPsfSpatialModel:
521 maUtils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=
True,
522 matchKernelAmplitudes=matchKernelAmplitudes,
523 keepPlots=keepMatplotlibPlots)
528 reply =
input(
"Next iteration? [ynchpqQs] ").strip()
532 reply = reply.split()
534 reply, args = reply[0], reply[1:]
538 if reply
in (
"",
"c",
"h",
"n",
"p",
"q",
"Q",
"s",
"y"):
542 print(
"c[ontinue without prompting] h[elp] n[o] p[db] q[uit displaying] " \
543 "s[ave fileName] y[es]")
553 fileName = args.pop(0)
555 print(
"Please provide a filename")
558 print(
"Saving to %s" % fileName)
559 maUtils.saveSpatialCellSet(psfCellSet, fileName=fileName)
563 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
569 psf, eigenValues, nEigenComponents, fitChi2 = \
570 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
575 if display
and reply !=
"n":
577 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=
True,
578 symb=
"o", ctype=ds9.YELLOW, ctypeBad=ds9.RED, size=8, frame=frame)
579 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
580 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
581 symb=
"o", ctype=ds9.YELLOW, ctypeBad=ds9.RED,
582 size=10, frame=frame)
584 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
585 normalize=normalizeResiduals,
586 showBadCandidates=showBadCandidates)
588 if displayPsfComponents:
589 maUtils.showPsf(psf, eigenValues, frame=6)
592 maUtils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
593 ds9.scale(
"linear", 0, 1, frame=7)
594 if displayPsfSpatialModel:
595 maUtils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=
True,
596 matchKernelAmplitudes=matchKernelAmplitudes,
597 keepPlots=keepMatplotlibPlots)
609 for cell
in psfCellSet.getCellList():
610 for cand
in cell.begin(
False):
613 for cand
in cell.begin(
True):
614 cand = algorithmsLib.PsfCandidateF.cast(cand)
615 src = cand.getSource()
616 if flagKey
is not None:
617 src.set(flagKey,
True)
625 if metadata
is not None:
626 metadata.set(
"spatialFitChi2", fitChi2)
627 metadata.set(
"numGoodStars", numGoodStars)
628 metadata.set(
"numAvailStars", numAvailStars)
629 metadata.set(
"avgX", avgX)
630 metadata.set(
"avgY", avgY)
632 psf = algorithmsLib.PcaPsf(psf.getKernel(),
afwGeom.Point2D(avgX, avgY))
634 return psf, psfCellSet
638 """!Generator for Psf candidates
640 This allows two 'for' loops to be reduced to one.
642 \param psfCellSet SpatialCellSet of PSF candidates
643 \param ignoreBad Ignore candidates flagged as BAD?
644 \return SpatialCell, PsfCandidate
646 for cell
in psfCellSet.getCellList():
647 for cand
in cell.begin(ignoreBad):
648 yield (cell, algorithmsLib.PsfCandidateF.cast(cand))
650 psfDeterminerRegistry.register(
"pca", PcaPsfDeterminerTask)
def numCandidatesToReject
A measurePsfTask psf estimator.
A collection of SpatialCells covering an entire image.
def determinePsf
Determine a PCA PSF model for an exposure given a list of PSF candidates.
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
def candidatesIter
Generator for Psf candidates.