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[bool](
72 doc=
"Use non-linear fitter for spatial variation of Kernel",
75 nEigenComponents = pexConfig.Field[int](
76 doc=
"number of eigen components for PSF kernel creation",
78 check=
lambda x: x >= 1
80 spatialOrder = pexConfig.Field[int](
81 doc=
"specify spatial order for PSF kernel creation",
84 sizeCellX = pexConfig.Field[int](
85 doc=
"size of cell used to determine PSF (pixels, column direction)",
88 check=
lambda x: x >= 10,
90 sizeCellY = pexConfig.Field[int](
91 doc=
"size of cell used to determine PSF (pixels, row direction)",
92 default=sizeCellX.default,
94 check=
lambda x: x >= 10,
96 nStarPerCell = pexConfig.Field[int](
97 doc=
"number of stars per psf cell for PSF kernel creation",
100 borderWidth = pexConfig.Field[int](
101 doc=
"Number of pixels to ignore around the edge of PSF candidate postage stamps",
104 nStarPerCellSpatialFit = pexConfig.Field[int](
105 doc=
"number of stars per psf Cell for spatial fitting",
108 constantWeight = pexConfig.Field[bool](
109 doc=
"Should each PSF candidate be given the same weight, independent of magnitude?",
112 nIterForPsf = pexConfig.Field[int](
113 doc=
"number of iterations of PSF candidate star list",
116 tolerance = pexConfig.Field[float](
117 doc=
"tolerance of spatial fitting",
120 lam = pexConfig.Field[float](
121 doc=
"floor for variance is lam*data",
124 reducedChi2ForPsfCandidates = pexConfig.Field[float](
125 doc=
"for psf candidate evaluation",
128 spatialReject = pexConfig.Field[float](
129 doc=
"Rejection threshold (stdev) for candidates based on spatial fit",
132 pixelThreshold = pexConfig.Field[float](
133 doc=
"Threshold (stdev) for rejecting extraneous pixels around candidate; applied if positive",
136 doRejectBlends = pexConfig.Field[bool](
137 doc=
"Reject candidates that are blended?",
140 doMaskBlends = pexConfig.Field[bool](
141 doc=
"Mask blends in image?",
151 """A measurePsfTask psf estimator.
153 ConfigClass = PcaPsfDeterminerConfig
155 def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents):
156 PsfCandidateF.setPixelThreshold(self.config.pixelThreshold)
157 PsfCandidateF.setMaskBlends(self.config.doMaskBlends)
161 for nEigen
in range(nEigenComponents, 0, -1):
164 kernel, eigenValues = createKernelFromPsfCandidates(
165 psfCellSet, exposure.getDimensions(), exposure.getXY0(), nEigen,
166 self.config.spatialOrder, kernelSize, self.config.nStarPerCell,
167 bool(self.config.constantWeight))
172 raise IndexError(
"No viable PSF candidates survive")
174 self.log.warning(
"%s: reducing number of eigen components", e.what())
179 size = kernelSize + 2*self.config.borderWidth
181 eigenValues = [val/float(countPsfCandidates(psfCellSet, self.config.nStarPerCell)*nu)
182 for val
in eigenValues]
185 status, chi2 = fitSpatialKernelFromPsfCandidates(
186 kernel, psfCellSet, bool(self.config.nonLinearSpatialFit),
187 self.config.nStarPerCellSpatialFit, self.config.tolerance, self.config.lam)
191 return psf, eigenValues, nEigen, chi2
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.
199 Exposure containing the psf candidates.
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.
218 displayPsfCandidates =
lsstDebug.Info(__name__).displayPsfCandidates
220 displayPsfComponents =
lsstDebug.Info(__name__).displayPsfComponents
224 matchKernelAmplitudes =
lsstDebug.Info(__name__).matchKernelAmplitudes
226 keepMatplotlibPlots =
lsstDebug.Info(__name__).keepMatplotlibPlots
227 displayPsfSpatialModel =
lsstDebug.Info(__name__).displayPsfSpatialModel
234 afwDisplay.setDefaultMaskTransparency(75)
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()
259 axes = afwEll.Axes(quad)
260 sizes.append(axes.getA())
262 raise RuntimeError(
"No usable PSF candidates supplied")
263 nEigenComponents = self.config.nEigenComponents
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)
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
286 print(
"Median size=%s" % (medSize,))
287 self.log.trace(
"Kernel size=%s", actualKernelSize)
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())
295 if self.config.doRejectBlends:
297 blendedCandidates = []
299 if len(cand.getSource().getFootprint().getPeaks()) > 1:
300 blendedCandidates.append((cell, cand))
303 print(
"Removing %d blended Psf candidates" % len(blendedCandidates))
304 for cell, cand
in blendedCandidates:
305 cell.removeCandidate(cand)
307 raise RuntimeError(
"All PSF candidates removed as blends")
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)
321 for iterNum
in range(self.config.nIterForPsf):
322 if display
and displayPsfCandidates:
325 for cell
in psfCellSet.getCellList():
326 for cand
in cell.begin(
not showBadCandidates):
328 im = cand.getMaskedImage()
330 chi2 = cand.getChi2()
334 stamps.append((im,
"%d%s" %
335 (utils.splitId(cand.getSource().getId(),
True)[
"objId"], chi2),
341 print(
"WARNING: No PSF candidates to show; try setting showBadCandidates=True")
343 mos = afwDisplay.utils.Mosaic()
344 for im, label, status
in stamps:
345 im =
type(im)(im,
True)
348 except NotImplementedError:
351 mos.append(im, label,
352 (afwDisplay.GREEN
if status == afwMath.SpatialCellCandidate.GOOD
else
353 afwDisplay.YELLOW
if status == afwMath.SpatialCellCandidate.UNKNOWN
else
356 disp8 = afwDisplay.Display(frame=8)
357 mos.makeMosaic(display=disp8, title=
"Psf Candidates")
366 psf, eigenValues, nEigenComponents, fitChi2 = \
367 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
372 for cell
in psfCellSet.getCellList():
374 for cand
in cell.begin(
False):
375 cand.setStatus(afwMath.SpatialCellCandidate.UNKNOWN)
376 rchi2 = cand.getChi2()
377 if not numpy.isfinite(rchi2)
or rchi2 <= 0:
379 awfulCandidates.append(cand)
381 self.log.debug(
"chi^2=%s; id=%s",
382 cand.getChi2(), cand.getSource().getId())
383 for cand
in awfulCandidates:
385 print(
"Removing bad candidate: id=%d, chi^2=%f" %
386 (cand.getSource().getId(), cand.getChi2()))
387 cell.removeCandidate(cand)
392 badCandidates =
list()
393 for cell
in psfCellSet.getCellList():
394 for cand
in cell.begin(
False):
395 rchi2 = cand.getChi2()
397 if rchi2 > self.config.reducedChi2ForPsfCandidates:
398 badCandidates.append(cand)
400 badCandidates.sort(key=
lambda x: x.getChi2(), reverse=
True)
402 self.config.nIterForPsf)
403 for i, c
in zip(range(numBad), badCandidates):
409 print(
"Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2))
410 c.setStatus(afwMath.SpatialCellCandidate.BAD)
423 kernel = psf.getKernel()
424 noSpatialKernel = psf.getKernel()
425 for cell
in psfCellSet.getCellList():
426 for cand
in cell.begin(
False):
429 im = cand.getMaskedImage(actualKernelSize, actualKernelSize)
433 fit = fitKernelParamsToImage(noSpatialKernel, im, candCenter)
437 for p, k
in zip(params, kernels):
440 predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY())
for
441 k
in range(kernel.getNKernelParameters())]
443 residuals.append([a/amp - p
for a, p
in zip(params, predict)])
444 candidates.append(cand)
446 residuals = numpy.array(residuals)
448 for k
in range(kernel.getNKernelParameters()):
451 mean = residuals[:, k].mean()
452 rms = residuals[:, k].
std()
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))])
461 mean = stats.getValue(afwMath.MEANCLIP)
462 rms = stats.getValue(afwMath.STDEVCLIP)
464 rms =
max(1.0e-4, rms)
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)
474 badCandidates.sort(key=
lambda x: numpy.fabs(residuals[x, k] - mean), reverse=
True)
477 self.config.nIterForPsf)
479 for i, c
in zip(range(
min(len(badCandidates), numBad)), badCandidates):
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)
490 if display
and displayIterations:
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)
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,
514 if not showBadCandidates:
515 showBadCandidates =
True
519 if displayPsfComponents:
520 disp6 = afwDisplay.Display(frame=6)
521 utils.showPsf(psf, eigenValues, display=disp6)
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)
534 reply = input(
"Next iteration? [ynchpqQs] ").
strip()
538 reply = reply.split()
540 reply, args = reply[0], reply[1:]
544 if reply
in (
"",
"c",
"h",
"n",
"p",
"q",
"Q",
"s",
"y"):
548 print(
"c[ontinue without prompting] h[elp] n[o] p[db] q[uit displaying] "
549 "s[ave fileName] y[es]")
559 fileName = args.pop(0)
561 print(
"Please provide a filename")
564 print(
"Saving to %s" % fileName)
565 utils.saveSpatialCellSet(psfCellSet, fileName=fileName)
569 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
575 psf, eigenValues, nEigenComponents, fitChi2 = \
576 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
581 if display
and reply !=
"n":
582 disp = afwDisplay.Display(frame=0)
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)
592 disp4 = afwDisplay.Display(frame=4)
593 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4,
594 normalize=normalizeResiduals,
595 showBadCandidates=showBadCandidates)
597 if displayPsfComponents:
598 disp6 = afwDisplay.Display(frame=6)
599 utils.showPsf(psf, eigenValues, display=disp6)
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)
620 for cell
in psfCellSet.getCellList():
621 for cand
in cell.begin(
False):
624 for cand
in cell.begin(
True):
625 src = cand.getSource()
626 if flagKey
is not None:
627 src.set(flagKey,
True)
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
644 return psf, psfCellSet
648 """Generator for Psf candidates.
650 This allows two 'for' loops to be reduced to one.
655 SpatialCellSet of PSF candidates.
656 ignoreBad : `bool`, optional
657 Ignore candidates flagged
as BAD?
666 for cell
in psfCellSet.getCellList():
667 for cand
in cell.begin(ignoreBad):
671psfDeterminerRegistry.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)