22__all__ = [
"PcaPsfDeterminerConfig",
"PcaPsfDeterminerTask"]
36from .psfDeterminer
import BasePsfDeterminerTask, psfDeterminerRegistry
37from .psfCandidate
import PsfCandidateF
38from .spatialModelPsf
import createKernelFromPsfCandidates, countPsfCandidates, \
39 fitSpatialKernelFromPsfCandidates, fitKernelParamsToImage
40from .pcaPsf
import PcaPsf
45 """Return the number of PSF candidates to be rejected.
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
53 numBadCandidates : `int`
54 Number of bad candidates under consideration.
57 The number of the current PSF iteration.
60 The total number of PSF iterations.
65 Number of candidates to reject.
67 return int(numBadCandidates*(numIter + 1)//totalIter + 0.5)
71 nonLinearSpatialFit = pexConfig.Field(
72 doc=
"Use non-linear fitter for spatial variation of Kernel",
76 nEigenComponents = pexConfig.Field(
77 doc=
"number of eigen components for PSF kernel creation",
80 check=
lambda x: x >= 1
82 spatialOrder = pexConfig.Field(
83 doc=
"specify spatial order for PSF kernel creation",
87 sizeCellX = pexConfig.Field(
88 doc=
"size of cell used to determine PSF (pixels, column direction)",
92 check=
lambda x: x >= 10,
94 sizeCellY = pexConfig.Field(
95 doc=
"size of cell used to determine PSF (pixels, row direction)",
97 default=sizeCellX.default,
99 check=
lambda x: x >= 10,
101 nStarPerCell = pexConfig.Field(
102 doc=
"number of stars per psf cell for PSF kernel creation",
106 borderWidth = pexConfig.Field(
107 doc=
"Number of pixels to ignore around the edge of PSF candidate postage stamps",
111 nStarPerCellSpatialFit = pexConfig.Field(
112 doc=
"number of stars per psf Cell for spatial fitting",
116 constantWeight = pexConfig.Field(
117 doc=
"Should each PSF candidate be given the same weight, independent of magnitude?",
121 nIterForPsf = pexConfig.Field(
122 doc=
"number of iterations of PSF candidate star list",
126 tolerance = pexConfig.Field(
127 doc=
"tolerance of spatial fitting",
131 lam = pexConfig.Field(
132 doc=
"floor for variance is lam*data",
136 reducedChi2ForPsfCandidates = pexConfig.Field(
137 doc=
"for psf candidate evaluation",
141 spatialReject = pexConfig.Field(
142 doc=
"Rejection threshold (stdev) for candidates based on spatial fit",
146 pixelThreshold = pexConfig.Field(
147 doc=
"Threshold (stdev) for rejecting extraneous pixels around candidate; applied if positive",
151 doRejectBlends = pexConfig.Field(
152 doc=
"Reject candidates that are blended?",
156 doMaskBlends = pexConfig.Field(
157 doc=
"Mask blends in image?",
164 """A measurePsfTask psf estimator.
166 ConfigClass = PcaPsfDeterminerConfig
168 def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents):
169 PsfCandidateF.setPixelThreshold(self.config.pixelThreshold)
170 PsfCandidateF.setMaskBlends(self.config.doMaskBlends)
174 for nEigen
in range(nEigenComponents, 0, -1):
178 psfCellSet, exposure.getDimensions(), exposure.getXY0(), nEigen,
179 self.config.spatialOrder, kernelSize, self.config.nStarPerCell,
180 bool(self.config.constantWeight))
185 raise IndexError(
"No viable PSF candidates survive")
187 self.log.
warning(
"%s: reducing number of eigen components", e.what())
192 size = kernelSize + 2*self.config.borderWidth
195 for val
in eigenValues]
199 kernel, psfCellSet, bool(self.config.nonLinearSpatialFit),
200 self.config.nStarPerCellSpatialFit, self.config.tolerance, self.config.lam)
204 return psf, eigenValues, nEigen, chi2
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.
212 Exposure containing the psf candidates.
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.
231 displayPsfCandidates =
lsstDebug.Info(__name__).displayPsfCandidates
233 displayPsfComponents =
lsstDebug.Info(__name__).displayPsfComponents
237 matchKernelAmplitudes =
lsstDebug.Info(__name__).matchKernelAmplitudes
239 keepMatplotlibPlots =
lsstDebug.Info(__name__).keepMatplotlibPlots
240 displayPsfSpatialModel =
lsstDebug.Info(__name__).displayPsfSpatialModel
247 afwDisplay.setDefaultMaskTransparency(75)
251 mi = exposure.getMaskedImage()
253 if len(psfCandidateList) == 0:
254 raise RuntimeError(
"No PSF candidates supplied.")
260 for i, psfCandidate
in enumerate(psfCandidateList):
261 if psfCandidate.getSource().getPsfFluxFlag():
265 psfCellSet.insertCandidate(psfCandidate)
266 except Exception
as e:
267 self.log.
debug(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
269 source = psfCandidate.getSource()
272 axes = afwEll.Axes(quad)
273 sizes.append(axes.getA())
275 raise RuntimeError(
"No usable PSF candidates supplied")
276 nEigenComponents = self.config.nEigenComponents
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)
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
293 print(
"Median size=%s" % (medSize,))
294 self.log.
trace(
"Kernel size=%s", actualKernelSize)
297 psfCandidateList[0].setHeight(actualKernelSize)
298 psfCandidateList[0].setWidth(actualKernelSize)
300 if self.config.doRejectBlends:
302 blendedCandidates = []
304 if len(cand.getSource().getFootprint().getPeaks()) > 1:
305 blendedCandidates.append((cell, cand))
308 print(
"Removing %d blended Psf candidates" % len(blendedCandidates))
309 for cell, cand
in blendedCandidates:
310 cell.removeCandidate(cand)
312 raise RuntimeError(
"All PSF candidates removed as blends")
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)
326 for iterNum
in range(self.config.nIterForPsf):
327 if display
and displayPsfCandidates:
330 for cell
in psfCellSet.getCellList():
331 for cand
in cell.begin(
not showBadCandidates):
333 im = cand.getMaskedImage()
335 chi2 = cand.getChi2()
339 stamps.append((im,
"%d%s" %
340 (utils.splitId(cand.getSource().getId(),
True)[
"objId"], chi2),
346 print(
"WARNING: No PSF candidates to show; try setting showBadCandidates=True")
348 mos = afwDisplay.utils.Mosaic()
349 for im, label, status
in stamps:
350 im =
type(im)(im,
True)
353 except NotImplementedError:
356 mos.append(im, label,
357 (afwDisplay.GREEN
if status == afwMath.SpatialCellCandidate.GOOD
else
358 afwDisplay.YELLOW
if status == afwMath.SpatialCellCandidate.UNKNOWN
else
361 disp8 = afwDisplay.Display(frame=8)
362 mos.makeMosaic(display=disp8, title=
"Psf Candidates")
371 psf, eigenValues, nEigenComponents, fitChi2 = \
372 self.
_fitPsf_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
377 for cell
in psfCellSet.getCellList():
379 for cand
in cell.begin(
False):
380 cand.setStatus(afwMath.SpatialCellCandidate.UNKNOWN)
381 rchi2 = cand.getChi2()
382 if not numpy.isfinite(rchi2)
or rchi2 <= 0:
384 awfulCandidates.append(cand)
386 self.log.
debug(
"chi^2=%s; id=%s",
387 cand.getChi2(), cand.getSource().getId())
388 for cand
in awfulCandidates:
390 print(
"Removing bad candidate: id=%d, chi^2=%f" %
391 (cand.getSource().getId(), cand.getChi2()))
392 cell.removeCandidate(cand)
397 badCandidates =
list()
398 for cell
in psfCellSet.getCellList():
399 for cand
in cell.begin(
False):
400 rchi2 = cand.getChi2()
402 if rchi2 > self.config.reducedChi2ForPsfCandidates:
403 badCandidates.append(cand)
405 badCandidates.sort(key=
lambda x: x.getChi2(), reverse=
True)
407 self.config.nIterForPsf)
408 for i, c
in zip(range(numBad), badCandidates):
414 print(
"Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2))
415 c.setStatus(afwMath.SpatialCellCandidate.BAD)
428 kernel = psf.getKernel()
429 noSpatialKernel = psf.getKernel()
430 for cell
in psfCellSet.getCellList():
431 for cand
in cell.begin(
False):
434 im = cand.getMaskedImage(kernel.getWidth(), kernel.getHeight())
442 for p, k
in zip(params, kernels):
445 predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY())
for
446 k
in range(kernel.getNKernelParameters())]
448 residuals.append([a/amp - p
for a, p
in zip(params, predict)])
449 candidates.append(cand)
451 residuals = numpy.array(residuals)
453 for k
in range(kernel.getNKernelParameters()):
456 mean = residuals[:, k].mean()
457 rms = residuals[:, k].
std()
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))])
466 mean = stats.getValue(afwMath.MEANCLIP)
467 rms = stats.getValue(afwMath.STDEVCLIP)
469 rms =
max(1.0e-4, rms)
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)
479 badCandidates.sort(key=
lambda x: numpy.fabs(residuals[x, k] - mean), reverse=
True)
482 self.config.nIterForPsf)
484 for i, c
in zip(range(
min(len(badCandidates), numBad)), badCandidates):
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)
495 if display
and displayIterations:
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)
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,
519 if not showBadCandidates:
520 showBadCandidates =
True
524 if displayPsfComponents:
525 disp6 = afwDisplay.Display(frame=6)
526 utils.showPsf(psf, eigenValues, display=disp6)
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)
539 reply = input(
"Next iteration? [ynchpqQs] ").
strip()
543 reply = reply.split()
545 reply, args = reply[0], reply[1:]
549 if reply
in (
"",
"c",
"h",
"n",
"p",
"q",
"Q",
"s",
"y"):
553 print(
"c[ontinue without prompting] h[elp] n[o] p[db] q[uit displaying] "
554 "s[ave fileName] y[es]")
564 fileName = args.pop(0)
566 print(
"Please provide a filename")
569 print(
"Saving to %s" % fileName)
570 utils.saveSpatialCellSet(psfCellSet, fileName=fileName)
574 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
580 psf, eigenValues, nEigenComponents, fitChi2 = \
581 self.
_fitPsf_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
586 if display
and reply !=
"n":
587 disp = afwDisplay.Display(frame=0)
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)
597 disp4 = afwDisplay.Display(frame=4)
598 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
599 normalize=normalizeResiduals,
600 showBadCandidates=showBadCandidates)
602 if displayPsfComponents:
603 disp6 = afwDisplay.Display(frame=6)
604 utils.showPsf(psf, eigenValues, display=disp6)
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)
625 for cell
in psfCellSet.getCellList():
626 for cand
in cell.begin(
False):
629 for cand
in cell.begin(
True):
630 src = cand.getSource()
631 if flagKey
is not None:
632 src.set(flagKey,
True)
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
649 return psf, psfCellSet
653 """Generator for Psf candidates.
655 This allows two 'for' loops to be reduced to one.
660 SpatialCellSet of PSF candidates.
661 ignoreBad : `bool`, optional
662 Ignore candidates flagged
as BAD?
671 for cell
in psfCellSet.getCellList():
672 for cand
in cell.begin(ignoreBad):
676psfDeterminerRegistry.register(
"pca", PcaPsfDeterminerTask)
An ellipse core with quadrupole moments as parameters.
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Class to ensure constraints for spatial modeling.
A collection of SpatialCells covering an entire image.
Represent a PSF as a linear combination of PCA (== Karhunen-Loeve) basis functions.
Class stored in SpatialCells for spatial Psf fitting.
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.
daf::base::PropertyList * list
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)
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.