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"]
45 nonLinearSpatialFit = pexConfig.Field(
46 doc=
"Use non-linear fitter for spatial variation of Kernel",
50 nEigenComponents = pexConfig.Field(
51 doc=
"number of eigen components for PSF kernel creation",
55 spatialOrder = pexConfig.Field(
56 doc=
"specify spatial order for PSF kernel creation",
60 sizeCellX = pexConfig.Field(
61 doc=
"size of cell used to determine PSF (pixels, column direction)",
65 check=
lambda x: x >= 10,
67 sizeCellY = pexConfig.Field(
68 doc=
"size of cell used to determine PSF (pixels, row direction)",
70 default=sizeCellX.default,
72 check=
lambda x: x >= 10,
74 nStarPerCell = pexConfig.Field(
75 doc=
"number of stars per psf cell for PSF kernel creation",
79 borderWidth = pexConfig.Field(
80 doc=
"Number of pixels to ignore around the edge of PSF candidate postage stamps",
84 nStarPerCellSpatialFit = pexConfig.Field(
85 doc=
"number of stars per psf Cell for spatial fitting",
89 constantWeight = pexConfig.Field(
90 doc=
"Should each PSF candidate be given the same weight, independent of magnitude?",
94 nIterForPsf = pexConfig.Field(
95 doc=
"number of iterations of PSF candidate star list",
99 tolerance = pexConfig.Field(
100 doc=
"tolerance of spatial fitting",
104 lam = pexConfig.Field(
105 doc=
"floor for variance is lam*data",
109 reducedChi2ForPsfCandidates = pexConfig.Field(
110 doc=
"for psf candidate evaluation",
114 spatialReject = pexConfig.Field(
115 doc=
"Rejection threshold (stdev) for candidates based on spatial fit",
119 pixelThreshold = pexConfig.Field(
120 doc=
"Threshold (stdev) for rejecting extraneous pixels around candidate; applied if positive",
124 doRejectBlends = pexConfig.Field(
125 doc=
"Reject candidates that are blended?",
129 doMaskBlends = pexConfig.Field(
130 doc=
"Mask blends in image?",
138 A measurePsfTask psf estimator
140 ConfigClass = PcaPsfDeterminerConfig
142 def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents):
143 algorithmsLib.PsfCandidateF.setPixelThreshold(self.config.pixelThreshold)
144 algorithmsLib.PsfCandidateF.setMaskBlends(self.config.doMaskBlends)
148 for nEigen
in range(nEigenComponents, 0, -1):
151 kernel, eigenValues = algorithmsLib.createKernelFromPsfCandidates(
152 psfCellSet, exposure.getDimensions(), exposure.getXY0(), nEigen,
153 self.config.spatialOrder, kernelSize, self.config.nStarPerCell,
154 bool(self.config.constantWeight))
157 except pexExceptions.LengthError
as e:
159 raise IndexError(
"No viable PSF candidates survive")
161 self.log.warn(
"%s: reducing number of eigen components" % e.what())
166 size = kernelSize + 2*self.config.borderWidth
168 eigenValues = [l/float(algorithmsLib.countPsfCandidates(psfCellSet, self.config.nStarPerCell)*nu)
169 for l
in eigenValues]
172 status, chi2 = algorithmsLib.fitSpatialKernelFromPsfCandidates(
173 kernel, psfCellSet, bool(self.config.nonLinearSpatialFit),
174 self.config.nStarPerCellSpatialFit, self.config.tolerance, self.config.lam)
176 psf = algorithmsLib.PcaPsf(kernel)
178 return psf, eigenValues, nEigen, chi2
180 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
181 """!Determine a PCA PSF model for an exposure given a list of PSF candidates
183 \param[in] exposure exposure containing the psf candidates (lsst.afw.image.Exposure)
184 \param[in] psfCandidateList a sequence of PSF candidates (each an lsst.meas.algorithms.PsfCandidate);
185 typically obtained by detecting sources and then running them through a star selector
186 \param[in,out] metadata a home for interesting tidbits of information
187 \param[in] flagKey schema key used to mark sources actually used in PSF determination
190 - psf: the measured PSF, an lsst.meas.algorithms.PcaPsf
191 - cellSet: an lsst.afw.math.SpatialCellSet containing the PSF candidates
196 displayPsfCandidates =
lsstDebug.Info(__name__).displayPsfCandidates
198 displayPsfComponents =
lsstDebug.Info(__name__).displayPsfComponents
202 matchKernelAmplitudes =
lsstDebug.Info(__name__).matchKernelAmplitudes
204 keepMatplotlibPlots =
lsstDebug.Info(__name__).keepMatplotlibPlots
205 displayPsfSpatialModel =
lsstDebug.Info(__name__).displayPsfSpatialModel
214 mi = exposure.getMaskedImage()
216 if len(psfCandidateList) == 0:
217 raise RuntimeError(
"No PSF candidates supplied.")
223 for i, psfCandidate
in enumerate(psfCandidateList):
224 if psfCandidate.getSource().getPsfFluxFlag():
228 psfCellSet.insertCandidate(psfCandidate)
229 except Exception
as e:
230 self.log.debug(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
232 source = psfCandidate.getSource()
234 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
235 axes = afwEll.Axes(quad)
236 sizes.append(axes.getA())
238 raise RuntimeError(
"No usable PSF candidates supplied")
239 nEigenComponents = self.config.nEigenComponents
241 if self.config.kernelSize >= 15:
242 self.log.warn(
"WARNING: NOT scaling kernelSize by stellar quadrupole moment " +
243 "because config.kernelSize=%s >= 15; using config.kernelSize as as the width, instead",
244 self.config.kernelSize)
245 actualKernelSize = int(self.config.kernelSize)
247 medSize = numpy.median(sizes)
248 actualKernelSize = 2 * int(self.config.kernelSize * math.sqrt(medSize) + 0.5) + 1
249 if actualKernelSize < self.config.kernelSizeMin:
250 actualKernelSize = self.config.kernelSizeMin
251 if actualKernelSize > self.config.kernelSizeMax:
252 actualKernelSize = self.config.kernelSizeMax
255 print(
"Median size=%s" % (medSize,))
256 self.log.trace(
"Kernel size=%s", actualKernelSize)
259 psfCandidateList[0].setHeight(actualKernelSize)
260 psfCandidateList[0].setWidth(actualKernelSize)
262 if self.config.doRejectBlends:
264 blendedCandidates = []
266 if len(cand.getSource().getFootprint().getPeaks()) > 1:
267 blendedCandidates.append((cell, cand))
270 print(
"Removing %d blended Psf candidates" % len(blendedCandidates))
271 for cell, cand
in blendedCandidates:
272 cell.removeCandidate(cand)
274 raise RuntimeError(
"All PSF candidates removed as blends")
279 ds9.mtv(exposure, frame=frame, title=
"psf determination")
280 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell,
281 symb=
"o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW,
288 for iter
in range(self.config.nIterForPsf):
289 if display
and displayPsfCandidates:
294 for cell
in psfCellSet.getCellList():
295 for cand
in cell.begin(
not showBadCandidates):
296 cand = algorithmsLib.cast_PsfCandidateF(cand)
299 im = cand.getMaskedImage()
301 chi2 = cand.getChi2()
305 stamps.append((im,
"%d%s" %
306 (maUtils.splitId(cand.getSource().getId(),
True)[
"objId"], chi2),
308 except Exception
as e:
312 print(
"WARNING: No PSF candidates to show; try setting showBadCandidates=True")
314 mos = displayUtils.Mosaic()
315 for im, label, status
in stamps:
316 im = type(im)(im,
True)
319 except NotImplementedError:
322 mos.append(im, label,
323 ds9.GREEN
if status == afwMath.SpatialCellCandidate.GOOD
else
324 ds9.YELLOW
if status == afwMath.SpatialCellCandidate.UNKNOWN
else ds9.RED)
326 mos.makeMosaic(frame=8, title=
"Psf Candidates")
335 psf, eigenValues, nEigenComponents, fitChi2 = \
336 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
341 for cell
in psfCellSet.getCellList():
343 for cand
in cell.begin(
False):
344 cand = algorithmsLib.cast_PsfCandidateF(cand)
345 cand.setStatus(afwMath.SpatialCellCandidate.UNKNOWN)
346 rchi2 = cand.getChi2()
347 if not numpy.isfinite(rchi2)
or rchi2 <= 0:
349 awfulCandidates.append(cand)
351 self.log.debug(
"chi^2=%s; id=%s",
352 cand.getChi2(), cand.getSource().getId())
353 for cand
in awfulCandidates:
355 print(
"Removing bad candidate: id=%d, chi^2=%f" % \
356 (cand.getSource().getId(), cand.getChi2()))
357 cell.removeCandidate(cand)
362 badCandidates = list()
363 for cell
in psfCellSet.getCellList():
364 for cand
in cell.begin(
False):
365 cand = algorithmsLib.cast_PsfCandidateF(cand)
366 rchi2 = cand.getChi2()
368 if rchi2 > self.config.reducedChi2ForPsfCandidates:
369 badCandidates.append(cand)
371 badCandidates.sort(key=
lambda x: x.getChi2(), reverse=
True)
372 numBad = int(len(badCandidates) * (iter + 1) / self.config.nIterForPsf + 0.5)
373 for i, c
in zip(range(numBad), badCandidates):
379 print(
"Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2))
380 c.setStatus(afwMath.SpatialCellCandidate.BAD)
393 kernel = psf.getKernel()
394 noSpatialKernel = afwMath.cast_LinearCombinationKernel(psf.getKernel())
395 for cell
in psfCellSet.getCellList():
396 for cand
in cell.begin(
False):
397 cand = algorithmsLib.cast_PsfCandidateF(cand)
400 im = cand.getMaskedImage(kernel.getWidth(), kernel.getHeight())
401 except Exception
as e:
404 fit = algorithmsLib.fitKernelParamsToImage(noSpatialKernel, im, candCenter)
408 for p, k
in zip(params, kernels):
409 amp += p * afwMath.cast_FixedKernel(k).getSum()
411 predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY())
for
412 k
in range(kernel.getNKernelParameters())]
416 residuals.append([a / amp - p
for a, p
in zip(params, predict)])
417 candidates.append(cand)
419 residuals = numpy.array(residuals)
421 for k
in range(kernel.getNKernelParameters()):
424 mean = residuals[:, k].mean()
425 rms = residuals[:, k].
std()
428 sr = numpy.sort(residuals[:, k])
429 mean = sr[int(0.5*len(sr))]
if len(sr) % 2
else \
430 0.5 * (sr[int(0.5*len(sr))] + sr[int(0.5*len(sr))+1])
431 rms = 0.74 * (sr[int(0.75*len(sr))] - sr[int(0.25*len(sr))])
434 mean = stats.getValue(afwMath.MEANCLIP)
435 rms = stats.getValue(afwMath.STDEVCLIP)
437 rms = max(1.0e-4, rms)
440 print(
"Mean for component %d is %f" % (k, mean))
441 print(
"RMS for component %d is %f" % (k, rms))
442 badCandidates = list()
443 for i, cand
in enumerate(candidates):
444 if numpy.fabs(residuals[i, k] - mean) > self.config.spatialReject * rms:
445 badCandidates.append(i)
447 badCandidates.sort(key=
lambda x: numpy.fabs(residuals[x, k] - mean), reverse=
True)
449 numBad = int(len(badCandidates) * (iter + 1) / self.config.nIterForPsf + 0.5)
451 for i, c
in zip(range(min(len(badCandidates), numBad)), badCandidates):
454 print(
"Spatial clipping %d (%f,%f) based on %d: %f vs %f" % \
455 (cand.getSource().getId(), cand.getXCenter(), cand.getYCenter(), k,
456 residuals[badCandidates[i], k], self.config.spatialReject * rms))
457 cand.setStatus(afwMath.SpatialCellCandidate.BAD)
462 if display
and displayIterations:
465 ds9.erase(frame=frame)
466 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=
True,
467 symb=
"o", size=8, frame=frame,
468 ctype=ds9.YELLOW, ctypeBad=ds9.RED, ctypeUnused=ds9.MAGENTA)
469 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
470 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
471 symb=
"o", size=10, frame=frame,
472 ctype=ds9.YELLOW, ctypeBad=ds9.RED)
476 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
477 normalize=normalizeResiduals,
478 showBadCandidates=showBadCandidates)
479 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=5,
480 normalize=normalizeResiduals,
481 showBadCandidates=showBadCandidates,
484 if not showBadCandidates:
485 showBadCandidates =
True
489 if displayPsfComponents:
490 maUtils.showPsf(psf, eigenValues, frame=6)
492 maUtils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
493 ds9.scale(
'linear', 0, 1, frame=7)
494 if displayPsfSpatialModel:
495 maUtils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=
True,
496 matchKernelAmplitudes=matchKernelAmplitudes,
497 keepPlots=keepMatplotlibPlots)
502 reply = input(
"Next iteration? [ynchpqQs] ").strip()
506 reply = reply.split()
508 reply, args = reply[0], reply[1:]
512 if reply
in (
"",
"c",
"h",
"n",
"p",
"q",
"Q",
"s",
"y"):
516 print(
"c[ontinue without prompting] h[elp] n[o] p[db] q[uit displaying] " \
517 "s[ave fileName] y[es]")
527 fileName = args.pop(0)
529 print(
"Please provide a filename")
532 print(
"Saving to %s" % fileName)
533 maUtils.saveSpatialCellSet(psfCellSet, fileName=fileName)
537 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
543 psf, eigenValues, nEigenComponents, fitChi2 = \
544 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
549 if display
and reply !=
"n":
551 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=
True,
552 symb=
"o", ctype=ds9.YELLOW, ctypeBad=ds9.RED, size=8, frame=frame)
553 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
554 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
555 symb=
"o", ctype=ds9.YELLOW, ctypeBad=ds9.RED,
556 size=10, frame=frame)
558 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
559 normalize=normalizeResiduals,
560 showBadCandidates=showBadCandidates)
562 if displayPsfComponents:
563 maUtils.showPsf(psf, eigenValues, frame=6)
566 maUtils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
567 ds9.scale(
"linear", 0, 1, frame=7)
568 if displayPsfSpatialModel:
569 maUtils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=
True,
570 matchKernelAmplitudes=matchKernelAmplitudes,
571 keepPlots=keepMatplotlibPlots)
583 for cell
in psfCellSet.getCellList():
584 for cand
in cell.begin(
False):
587 for cand
in cell.begin(
True):
588 cand = algorithmsLib.cast_PsfCandidateF(cand)
589 src = cand.getSource()
590 if flagKey
is not None:
591 src.set(flagKey,
True)
599 if metadata
is not None:
600 metadata.set(
"spatialFitChi2", fitChi2)
601 metadata.set(
"numGoodStars", numGoodStars)
602 metadata.set(
"numAvailStars", numAvailStars)
603 metadata.set(
"avgX", avgX)
604 metadata.set(
"avgY", avgY)
606 psf = algorithmsLib.PcaPsf(psf.getKernel(),
afwGeom.Point2D(avgX, avgY))
608 return psf, psfCellSet
612 """!Generator for Psf candidates
614 This allows two 'for' loops to be reduced to one.
616 \param psfCellSet SpatialCellSet of PSF candidates
617 \param ignoreBad Ignore candidates flagged as BAD?
618 \return SpatialCell, PsfCandidate
620 for cell
in psfCellSet.getCellList():
621 for cand
in cell.begin(ignoreBad):
622 yield (cell, algorithmsLib.cast_PsfCandidateF(cand))
624 psfDeterminerRegistry.register(
"pca", PcaPsfDeterminerTask)
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.