24 __all__ = [
"PcaPsfDeterminerConfig",
"PcaPsfDeterminerTask"]
37 from .psfDeterminer
import BasePsfDeterminerTask, psfDeterminerRegistry
38 from .psfCandidate
import PsfCandidateF
39 from .spatialModelPsf
import createKernelFromPsfCandidates, countPsfCandidates, \
40 fitSpatialKernelFromPsfCandidates, fitKernelParamsToImage
41 from .pcaPsf
import PcaPsf
46 """Return the number of PSF candidates to be rejected. 48 The number of candidates being rejected on each iteration gradually 49 increases, so that on the Nth of M iterations we reject N/M of the bad 54 numBadCandidates : int 55 Number of bad candidates under consideration. 58 The number of the current PSF iteration. 61 The total number of PSF iterations. 66 Number of candidates to reject. 68 return int(numBadCandidates * (numIter + 1) // totalIter + 0.5)
72 nonLinearSpatialFit = pexConfig.Field(
73 doc=
"Use non-linear fitter for spatial variation of Kernel",
77 nEigenComponents = pexConfig.Field(
78 doc=
"number of eigen components for PSF kernel creation",
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?",
165 A measurePsfTask psf estimator 167 ConfigClass = PcaPsfDeterminerConfig
169 def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents):
170 PsfCandidateF.setPixelThreshold(self.config.pixelThreshold)
171 PsfCandidateF.setMaskBlends(self.config.doMaskBlends)
175 for nEigen
in range(nEigenComponents, 0, -1):
179 psfCellSet, exposure.getDimensions(), exposure.getXY0(), nEigen,
180 self.config.spatialOrder, kernelSize, self.config.nStarPerCell,
181 bool(self.config.constantWeight))
184 except pexExceptions.LengthError
as e:
186 raise IndexError(
"No viable PSF candidates survive")
188 self.log.warn(
"%s: reducing number of eigen components" % e.what())
193 size = kernelSize + 2*self.config.borderWidth
196 for l
in eigenValues]
200 kernel, psfCellSet, bool(self.config.nonLinearSpatialFit),
201 self.config.nStarPerCellSpatialFit, self.config.tolerance, self.config.lam)
205 return psf, eigenValues, nEigen, chi2
207 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
208 """!Determine a PCA PSF model for an exposure given a list of PSF candidates 210 @param[in] exposure exposure containing the psf candidates (lsst.afw.image.Exposure) 211 @param[in] psfCandidateList a sequence of PSF candidates (each an lsst.meas.algorithms.PsfCandidate); 212 typically obtained by detecting sources and then running them through a star selector 213 @param[in,out] metadata a home for interesting tidbits of information 214 @param[in] flagKey schema key used to mark sources actually used in PSF determination 217 - psf: the measured PSF, an lsst.meas.algorithms.PcaPsf 218 - cellSet: an lsst.afw.math.SpatialCellSet containing the PSF candidates 223 displayPsfCandidates =
lsstDebug.Info(__name__).displayPsfCandidates
225 displayPsfComponents =
lsstDebug.Info(__name__).displayPsfComponents
229 matchKernelAmplitudes =
lsstDebug.Info(__name__).matchKernelAmplitudes
231 keepMatplotlibPlots =
lsstDebug.Info(__name__).keepMatplotlibPlots
232 displayPsfSpatialModel =
lsstDebug.Info(__name__).displayPsfSpatialModel
241 mi = exposure.getMaskedImage()
243 if len(psfCandidateList) == 0:
244 raise RuntimeError(
"No PSF candidates supplied.")
250 for i, psfCandidate
in enumerate(psfCandidateList):
251 if psfCandidate.getSource().getPsfFluxFlag():
255 psfCellSet.insertCandidate(psfCandidate)
256 except Exception
as e:
257 self.log.debug(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
259 source = psfCandidate.getSource()
261 quad = afwGeom.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
262 axes = afwEll.Axes(quad)
263 sizes.append(axes.getA())
265 raise RuntimeError(
"No usable PSF candidates supplied")
266 nEigenComponents = self.config.nEigenComponents
268 if self.config.kernelSize >= 15:
269 self.log.warn(
"WARNING: NOT scaling kernelSize by stellar quadrupole moment " +
270 "because config.kernelSize=%s >= 15; " +
271 "using config.kernelSize as as the width, instead",
272 self.config.kernelSize)
273 actualKernelSize = int(self.config.kernelSize)
275 medSize = numpy.median(sizes)
276 actualKernelSize = 2 * int(self.config.kernelSize * math.sqrt(medSize) + 0.5) + 1
277 if actualKernelSize < self.config.kernelSizeMin:
278 actualKernelSize = self.config.kernelSizeMin
279 if actualKernelSize > self.config.kernelSizeMax:
280 actualKernelSize = self.config.kernelSizeMax
283 print(
"Median size=%s" % (medSize,))
284 self.log.trace(
"Kernel size=%s", actualKernelSize)
287 psfCandidateList[0].setHeight(actualKernelSize)
288 psfCandidateList[0].setWidth(actualKernelSize)
290 if self.config.doRejectBlends:
292 blendedCandidates = []
294 if len(cand.getSource().getFootprint().getPeaks()) > 1:
295 blendedCandidates.append((cell, cand))
298 print(
"Removing %d blended Psf candidates" % len(blendedCandidates))
299 for cell, cand
in blendedCandidates:
300 cell.removeCandidate(cand)
302 raise RuntimeError(
"All PSF candidates removed as blends")
307 ds9.mtv(exposure, frame=frame, title=
"psf determination")
308 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell,
309 symb=
"o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW,
316 for iterNum
in range(self.config.nIterForPsf):
317 if display
and displayPsfCandidates:
322 for cell
in psfCellSet.getCellList():
323 for cand
in cell.begin(
not showBadCandidates):
325 im = cand.getMaskedImage()
327 chi2 = cand.getChi2()
331 stamps.append((im,
"%d%s" %
332 (utils.splitId(cand.getSource().getId(),
True)[
"objId"], chi2),
334 except Exception
as e:
338 print(
"WARNING: No PSF candidates to show; try setting showBadCandidates=True")
340 mos = displayUtils.Mosaic()
341 for im, label, status
in stamps:
342 im = type(im)(im,
True)
345 except NotImplementedError:
348 mos.append(im, label,
349 ds9.GREEN
if status == afwMath.SpatialCellCandidate.GOOD
else 350 ds9.YELLOW
if status == afwMath.SpatialCellCandidate.UNKNOWN
else ds9.RED)
352 mos.makeMosaic(frame=8, title=
"Psf Candidates")
361 psf, eigenValues, nEigenComponents, fitChi2 = \
362 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
367 for cell
in psfCellSet.getCellList():
369 for cand
in cell.begin(
False):
370 cand.setStatus(afwMath.SpatialCellCandidate.UNKNOWN)
371 rchi2 = cand.getChi2()
372 if not numpy.isfinite(rchi2)
or rchi2 <= 0:
374 awfulCandidates.append(cand)
376 self.log.debug(
"chi^2=%s; id=%s",
377 cand.getChi2(), cand.getSource().getId())
378 for cand
in awfulCandidates:
380 print(
"Removing bad candidate: id=%d, chi^2=%f" %
381 (cand.getSource().getId(), cand.getChi2()))
382 cell.removeCandidate(cand)
387 badCandidates = list()
388 for cell
in psfCellSet.getCellList():
389 for cand
in cell.begin(
False):
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 = psf.getKernel()
420 for cell
in psfCellSet.getCellList():
421 for cand
in cell.begin(
False):
424 im = cand.getMaskedImage(kernel.getWidth(), kernel.getHeight())
425 except Exception
as e:
432 for p, k
in zip(params, kernels):
433 amp += p * k.getSum()
435 predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY())
for 436 k
in range(kernel.getNKernelParameters())]
440 residuals.append([a / amp - p
for a, p
in zip(params, predict)])
441 candidates.append(cand)
443 residuals = numpy.array(residuals)
445 for k
in range(kernel.getNKernelParameters()):
448 mean = residuals[:, k].mean()
449 rms = residuals[:, k].
std()
452 sr = numpy.sort(residuals[:, k])
453 mean = sr[int(0.5*len(sr))]
if len(sr) % 2
else \
454 0.5 * (sr[int(0.5*len(sr))] + sr[int(0.5*len(sr))+1])
455 rms = 0.74 * (sr[int(0.75*len(sr))] - sr[int(0.25*len(sr))])
458 mean = stats.getValue(afwMath.MEANCLIP)
459 rms = stats.getValue(afwMath.STDEVCLIP)
461 rms = max(1.0e-4, rms)
464 print(
"Mean for component %d is %f" % (k, mean))
465 print(
"RMS for component %d is %f" % (k, rms))
466 badCandidates = list()
467 for i, cand
in enumerate(candidates):
468 if numpy.fabs(residuals[i, k] - mean) > self.config.spatialReject * rms:
469 badCandidates.append(i)
471 badCandidates.sort(key=
lambda x: numpy.fabs(residuals[x, k] - mean), reverse=
True)
474 self.config.nIterForPsf)
476 for i, c
in zip(range(min(len(badCandidates), numBad)), badCandidates):
479 print(
"Spatial clipping %d (%f,%f) based on %d: %f vs %f" %
480 (cand.getSource().getId(), cand.getXCenter(), cand.getYCenter(), k,
481 residuals[badCandidates[i], k], self.config.spatialReject * rms))
482 cand.setStatus(afwMath.SpatialCellCandidate.BAD)
487 if display
and displayIterations:
490 ds9.erase(frame=frame)
491 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=
True,
492 symb=
"o", size=8, frame=frame,
493 ctype=ds9.YELLOW, ctypeBad=ds9.RED, ctypeUnused=ds9.MAGENTA)
494 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
495 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
496 symb=
"o", size=10, frame=frame,
497 ctype=ds9.YELLOW, ctypeBad=ds9.RED)
501 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
502 normalize=normalizeResiduals,
503 showBadCandidates=showBadCandidates)
504 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=5,
505 normalize=normalizeResiduals,
506 showBadCandidates=showBadCandidates,
509 if not showBadCandidates:
510 showBadCandidates =
True 514 if displayPsfComponents:
515 utils.showPsf(psf, eigenValues, frame=6)
517 utils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
518 ds9.scale(
'linear', 0, 1, frame=7)
519 if displayPsfSpatialModel:
520 utils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=
True,
521 matchKernelAmplitudes=matchKernelAmplitudes,
522 keepPlots=keepMatplotlibPlots)
527 reply = input(
"Next iteration? [ynchpqQs] ").strip()
531 reply = reply.split()
533 reply, args = reply[0], reply[1:]
537 if reply
in (
"",
"c",
"h",
"n",
"p",
"q",
"Q",
"s",
"y"):
541 print(
"c[ontinue without prompting] h[elp] n[o] p[db] q[uit displaying] " 542 "s[ave fileName] y[es]")
552 fileName = args.pop(0)
554 print(
"Please provide a filename")
557 print(
"Saving to %s" % fileName)
558 utils.saveSpatialCellSet(psfCellSet, fileName=fileName)
562 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
568 psf, eigenValues, nEigenComponents, fitChi2 = \
569 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
574 if display
and reply !=
"n":
576 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=
True,
577 symb=
"o", ctype=ds9.YELLOW, ctypeBad=ds9.RED, size=8, frame=frame)
578 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
579 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
580 symb=
"o", ctype=ds9.YELLOW, ctypeBad=ds9.RED,
581 size=10, frame=frame)
583 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
584 normalize=normalizeResiduals,
585 showBadCandidates=showBadCandidates)
587 if displayPsfComponents:
588 utils.showPsf(psf, eigenValues, frame=6)
591 utils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
592 ds9.scale(
"linear", 0, 1, frame=7)
593 if displayPsfSpatialModel:
594 utils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=
True,
595 matchKernelAmplitudes=matchKernelAmplitudes,
596 keepPlots=keepMatplotlibPlots)
608 for cell
in psfCellSet.getCellList():
609 for cand
in cell.begin(
False):
612 for cand
in cell.begin(
True):
613 src = cand.getSource()
614 if flagKey
is not None:
615 src.set(flagKey,
True)
623 if metadata
is not None:
624 metadata.set(
"spatialFitChi2", fitChi2)
625 metadata.set(
"numGoodStars", numGoodStars)
626 metadata.set(
"numAvailStars", numAvailStars)
627 metadata.set(
"avgX", avgX)
628 metadata.set(
"avgY", avgY)
632 return psf, psfCellSet
636 """!Generator for Psf candidates 638 This allows two 'for' loops to be reduced to one. 640 @param psfCellSet SpatialCellSet of PSF candidates 641 @param ignoreBad Ignore candidates flagged as BAD? 642 @return SpatialCell, PsfCandidate 644 for cell
in psfCellSet.getCellList():
645 for cand
in cell.begin(ignoreBad):
649 psfDeterminerRegistry.register(
"pca", PcaPsfDeterminerTask)
def numCandidatesToReject(numBadCandidates, numIter, totalIter)
std::pair< bool, double > fitSpatialKernelFromPsfCandidates(lsst::afw::math::Kernel *kernel, lsst::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.
A measurePsfTask psf estimator.
std::pair< std::vector< double >, lsst::afw::math::KernelList > fitKernelParamsToImage(lsst::afw::math::LinearCombinationKernel const &kernel, Image const &image, lsst::afw::geom::Point2D const &pos)
Fit a LinearCombinationKernel to an Image, allowing the coefficients of the components to vary...
def candidatesIter(psfCellSet, ignoreBad=True)
Generator for Psf candidates.
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
std::pair< std::shared_ptr< lsst::afw::math::LinearCombinationKernel >, std::vector< double > > createKernelFromPsfCandidates(lsst::afw::math::SpatialCellSet const &psfCells, lsst::afw::geom::Extent2I const &dims, lsst::afw::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...
Base class for PSF determiners.
def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents)
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
Determine a PCA PSF model for an exposure given a list of PSF candidates.
int countPsfCandidates(lsst::afw::math::SpatialCellSet const &psfCells, int const nStarPerCell=-1)
Count the number of candidates in use.