24 __all__ = [
"PcaPsfDeterminerConfig",
"PcaPsfDeterminerTask"]
38 from .psfDeterminer
import BasePsfDeterminerTask, psfDeterminerRegistry
39 from .psfCandidate
import PsfCandidateF
40 from .spatialModelPsf
import createKernelFromPsfCandidates, countPsfCandidates, \
41 fitSpatialKernelFromPsfCandidates, fitKernelParamsToImage
42 from .pcaPsf
import PcaPsf
47 """Return the number of PSF candidates to be rejected. 49 The number of candidates being rejected on each iteration gradually 50 increases, so that on the Nth of M iterations we reject N/M of the bad 55 numBadCandidates : int 56 Number of bad candidates under consideration. 59 The number of the current PSF iteration. 62 The total number of PSF iterations. 67 Number of candidates to reject. 69 return int(numBadCandidates * (numIter + 1) // totalIter + 0.5)
73 nonLinearSpatialFit = pexConfig.Field(
74 doc=
"Use non-linear fitter for spatial variation of Kernel",
78 nEigenComponents = pexConfig.Field(
79 doc=
"number of eigen components for PSF kernel creation",
83 spatialOrder = pexConfig.Field(
84 doc=
"specify spatial order for PSF kernel creation",
88 sizeCellX = pexConfig.Field(
89 doc=
"size of cell used to determine PSF (pixels, column direction)",
93 check=
lambda x: x >= 10,
95 sizeCellY = pexConfig.Field(
96 doc=
"size of cell used to determine PSF (pixels, row direction)",
98 default=sizeCellX.default,
100 check=
lambda x: x >= 10,
102 nStarPerCell = pexConfig.Field(
103 doc=
"number of stars per psf cell for PSF kernel creation",
107 borderWidth = pexConfig.Field(
108 doc=
"Number of pixels to ignore around the edge of PSF candidate postage stamps",
112 nStarPerCellSpatialFit = pexConfig.Field(
113 doc=
"number of stars per psf Cell for spatial fitting",
117 constantWeight = pexConfig.Field(
118 doc=
"Should each PSF candidate be given the same weight, independent of magnitude?",
122 nIterForPsf = pexConfig.Field(
123 doc=
"number of iterations of PSF candidate star list",
127 tolerance = pexConfig.Field(
128 doc=
"tolerance of spatial fitting",
132 lam = pexConfig.Field(
133 doc=
"floor for variance is lam*data",
137 reducedChi2ForPsfCandidates = pexConfig.Field(
138 doc=
"for psf candidate evaluation",
142 spatialReject = pexConfig.Field(
143 doc=
"Rejection threshold (stdev) for candidates based on spatial fit",
147 pixelThreshold = pexConfig.Field(
148 doc=
"Threshold (stdev) for rejecting extraneous pixels around candidate; applied if positive",
152 doRejectBlends = pexConfig.Field(
153 doc=
"Reject candidates that are blended?",
157 doMaskBlends = pexConfig.Field(
158 doc=
"Mask blends in image?",
166 A measurePsfTask psf estimator 168 ConfigClass = PcaPsfDeterminerConfig
170 def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents):
171 PsfCandidateF.setPixelThreshold(self.config.pixelThreshold)
172 PsfCandidateF.setMaskBlends(self.config.doMaskBlends)
176 for nEigen
in range(nEigenComponents, 0, -1):
180 psfCellSet, exposure.getDimensions(), exposure.getXY0(), nEigen,
181 self.config.spatialOrder, kernelSize, self.config.nStarPerCell,
182 bool(self.config.constantWeight))
187 raise IndexError(
"No viable PSF candidates survive")
189 self.log.
warn(
"%s: reducing number of eigen components" % e.what())
194 size = kernelSize + 2*self.config.borderWidth
197 for l
in eigenValues]
201 kernel, psfCellSet, bool(self.config.nonLinearSpatialFit),
202 self.config.nStarPerCellSpatialFit, self.config.tolerance, self.config.lam)
206 return psf, eigenValues, nEigen, chi2
208 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
209 """!Determine a PCA PSF model for an exposure given a list of PSF candidates 211 @param[in] exposure exposure containing the psf candidates (lsst.afw.image.Exposure) 212 @param[in] psfCandidateList a sequence of PSF candidates (each an lsst.meas.algorithms.PsfCandidate); 213 typically obtained by detecting sources and then running them through a star selector 214 @param[in,out] metadata a home for interesting tidbits of information 215 @param[in] flagKey schema key used to mark sources actually used in PSF determination 218 - psf: the measured PSF, an lsst.meas.algorithms.PcaPsf 219 - cellSet: an lsst.afw.math.SpatialCellSet containing the PSF candidates 224 displayPsfCandidates =
lsstDebug.Info(__name__).displayPsfCandidates
226 displayPsfComponents =
lsstDebug.Info(__name__).displayPsfComponents
230 matchKernelAmplitudes =
lsstDebug.Info(__name__).matchKernelAmplitudes
232 keepMatplotlibPlots =
lsstDebug.Info(__name__).keepMatplotlibPlots
233 displayPsfSpatialModel =
lsstDebug.Info(__name__).displayPsfSpatialModel
242 mi = exposure.getMaskedImage()
244 if len(psfCandidateList) == 0:
245 raise RuntimeError(
"No PSF candidates supplied.")
251 for i, psfCandidate
in enumerate(psfCandidateList):
252 if psfCandidate.getSource().getPsfFluxFlag():
256 psfCellSet.insertCandidate(psfCandidate)
257 except Exception
as e:
258 self.log.
debug(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
260 source = psfCandidate.getSource()
263 axes = afwEll.Axes(quad)
264 sizes.append(axes.getA())
266 raise RuntimeError(
"No usable PSF candidates supplied")
267 nEigenComponents = self.config.nEigenComponents
269 if self.config.kernelSize >= 15:
270 self.log.
warn(
"WARNING: NOT scaling kernelSize by stellar quadrupole moment " 271 "because config.kernelSize=%s >= 15; " 272 "using config.kernelSize as as the width, instead",
273 self.config.kernelSize)
274 actualKernelSize =
int(self.config.kernelSize)
276 medSize = numpy.median(sizes)
277 actualKernelSize = 2 *
int(self.config.kernelSize * math.sqrt(medSize) + 0.5) + 1
278 if actualKernelSize < self.config.kernelSizeMin:
279 actualKernelSize = self.config.kernelSizeMin
280 if actualKernelSize > self.config.kernelSizeMax:
281 actualKernelSize = self.config.kernelSizeMax
284 print(
"Median size=%s" % (medSize,))
285 self.log.
trace(
"Kernel size=%s", actualKernelSize)
288 psfCandidateList[0].setHeight(actualKernelSize)
289 psfCandidateList[0].setWidth(actualKernelSize)
291 if self.config.doRejectBlends:
293 blendedCandidates = []
295 if len(cand.getSource().getFootprint().getPeaks()) > 1:
296 blendedCandidates.append((cell, cand))
299 print(
"Removing %d blended Psf candidates" % len(blendedCandidates))
300 for cell, cand
in blendedCandidates:
301 cell.removeCandidate(cand)
303 raise RuntimeError(
"All PSF candidates removed as blends")
308 ds9.mtv(exposure, frame=frame, title=
"psf determination")
309 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell,
310 symb=
"o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW,
317 for iterNum
in range(self.config.nIterForPsf):
318 if display
and displayPsfCandidates:
323 for cell
in psfCellSet.getCellList():
324 for cand
in cell.begin(
not showBadCandidates):
326 im = cand.getMaskedImage()
328 chi2 = cand.getChi2()
332 stamps.append((im,
"%d%s" %
333 (utils.splitId(cand.getSource().getId(),
True)[
"objId"], chi2),
339 print(
"WARNING: No PSF candidates to show; try setting showBadCandidates=True")
341 mos = displayUtils.Mosaic()
342 for im, label, status
in stamps:
343 im =
type(im)(im,
True)
346 except NotImplementedError:
349 mos.append(im, label,
350 ds9.GREEN
if status == afwMath.SpatialCellCandidate.GOOD
else 351 ds9.YELLOW
if status == afwMath.SpatialCellCandidate.UNKNOWN
else ds9.RED)
353 mos.makeMosaic(frame=8, title=
"Psf Candidates")
362 psf, eigenValues, nEigenComponents, fitChi2 = \
363 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
368 for cell
in psfCellSet.getCellList():
370 for cand
in cell.begin(
False):
371 cand.setStatus(afwMath.SpatialCellCandidate.UNKNOWN)
372 rchi2 = cand.getChi2()
373 if not numpy.isfinite(rchi2)
or rchi2 <= 0:
375 awfulCandidates.append(cand)
377 self.log.
debug(
"chi^2=%s; id=%s",
378 cand.getChi2(), cand.getSource().getId())
379 for cand
in awfulCandidates:
381 print(
"Removing bad candidate: id=%d, chi^2=%f" %
382 (cand.getSource().getId(), cand.getChi2()))
383 cell.removeCandidate(cand)
388 badCandidates =
list()
389 for cell
in psfCellSet.getCellList():
390 for cand
in cell.begin(
False):
391 rchi2 = cand.getChi2()
393 if rchi2 > self.config.reducedChi2ForPsfCandidates:
394 badCandidates.append(cand)
396 badCandidates.sort(key=
lambda x: x.getChi2(), reverse=
True)
398 self.config.nIterForPsf)
399 for i, c
in zip(range(numBad), badCandidates):
405 print(
"Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2))
406 c.setStatus(afwMath.SpatialCellCandidate.BAD)
419 kernel = psf.getKernel()
420 noSpatialKernel = psf.getKernel()
421 for cell
in psfCellSet.getCellList():
422 for cand
in cell.begin(
False):
425 im = cand.getMaskedImage(kernel.getWidth(), kernel.getHeight())
433 for p, k
in zip(params, kernels):
434 amp += p * 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 utils.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 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
497 symb=
"o", size=10, frame=frame,
498 ctype=ds9.YELLOW, ctypeBad=ds9.RED)
502 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
503 normalize=normalizeResiduals,
504 showBadCandidates=showBadCandidates)
505 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=5,
506 normalize=normalizeResiduals,
507 showBadCandidates=showBadCandidates,
510 if not showBadCandidates:
511 showBadCandidates =
True 515 if displayPsfComponents:
516 utils.showPsf(psf, eigenValues, frame=6)
518 utils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
519 ds9.scale(
'linear', 0, 1, frame=7)
520 if displayPsfSpatialModel:
521 utils.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 utils.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 utils.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 utils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
581 symb=
"o", ctype=ds9.YELLOW, ctypeBad=ds9.RED,
582 size=10, frame=frame)
584 utils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
585 normalize=normalizeResiduals,
586 showBadCandidates=showBadCandidates)
588 if displayPsfComponents:
589 utils.showPsf(psf, eigenValues, frame=6)
592 utils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
593 ds9.scale(
"linear", 0, 1, frame=7)
594 if displayPsfSpatialModel:
595 utils.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 src = cand.getSource()
615 if flagKey
is not None:
616 src.set(flagKey,
True)
624 if metadata
is not None:
625 metadata.set(
"spatialFitChi2", fitChi2)
626 metadata.set(
"numGoodStars", numGoodStars)
627 metadata.set(
"numAvailStars", numAvailStars)
628 metadata.set(
"avgX", avgX)
629 metadata.set(
"avgY", avgY)
633 return psf, psfCellSet
637 """!Generator for Psf candidates 639 This allows two 'for' loops to be reduced to one. 641 @param psfCellSet SpatialCellSet of PSF candidates 642 @param ignoreBad Ignore candidates flagged as BAD? 643 @return SpatialCell, PsfCandidate 645 for cell
in psfCellSet.getCellList():
646 for cand
in cell.begin(ignoreBad):
650 psfDeterminerRegistry.register(
"pca", PcaPsfDeterminerTask)
An ellipse core with quadrupole moments as parameters.
def numCandidatesToReject(numBadCandidates, numIter, totalIter)
int countPsfCandidates(afw::math::SpatialCellSet const &psfCells, int const nStarPerCell=-1)
Count the number of candidates in use.
A measurePsfTask psf estimator.
Reports attempts to exceed implementation-defined length limits for some classes. ...
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())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
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.
A collection of SpatialCells covering an entire image.
Base class for PSF determiners.
def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents)
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...
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
Determine a PCA PSF model for an exposure given a list of PSF candidates.
daf::base::PropertyList * list
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...