23 from __future__
import print_function
25 __all__ = [
"PcaPsfDeterminerConfig",
"PcaPsfDeterminerTask"]
27 from builtins
import input
28 from builtins
import zip
29 from builtins
import range
41 from .psfDeterminer
import BasePsfDeterminerTask, psfDeterminerRegistry
42 from .psfCandidate
import PsfCandidateF
43 from .spatialModelPsf
import createKernelFromPsfCandidates, countPsfCandidates, \
44 fitSpatialKernelFromPsfCandidates, fitKernelParamsToImage
45 from .pcaPsf
import PcaPsf
46 from .
import utils
as maUtils
50 """Return the number of PSF candidates to be rejected. 52 The number of candidates being rejected on each iteration gradually 53 increases, so that on the Nth of M iterations we reject N/M of the bad 58 numBadCandidates : int 59 Number of bad candidates under consideration. 62 The number of the current PSF iteration. 65 The total number of PSF iterations. 70 Number of candidates to reject. 72 return int(numBadCandidates * (numIter + 1) // totalIter + 0.5)
76 nonLinearSpatialFit = pexConfig.Field(
77 doc=
"Use non-linear fitter for spatial variation of Kernel",
81 nEigenComponents = pexConfig.Field(
82 doc=
"number of eigen components for PSF kernel creation",
86 spatialOrder = pexConfig.Field(
87 doc=
"specify spatial order for PSF kernel creation",
91 sizeCellX = pexConfig.Field(
92 doc=
"size of cell used to determine PSF (pixels, column direction)",
96 check=
lambda x: x >= 10,
98 sizeCellY = pexConfig.Field(
99 doc=
"size of cell used to determine PSF (pixels, row direction)",
101 default=sizeCellX.default,
103 check=
lambda x: x >= 10,
105 nStarPerCell = pexConfig.Field(
106 doc=
"number of stars per psf cell for PSF kernel creation",
110 borderWidth = pexConfig.Field(
111 doc=
"Number of pixels to ignore around the edge of PSF candidate postage stamps",
115 nStarPerCellSpatialFit = pexConfig.Field(
116 doc=
"number of stars per psf Cell for spatial fitting",
120 constantWeight = pexConfig.Field(
121 doc=
"Should each PSF candidate be given the same weight, independent of magnitude?",
125 nIterForPsf = pexConfig.Field(
126 doc=
"number of iterations of PSF candidate star list",
130 tolerance = pexConfig.Field(
131 doc=
"tolerance of spatial fitting",
135 lam = pexConfig.Field(
136 doc=
"floor for variance is lam*data",
140 reducedChi2ForPsfCandidates = pexConfig.Field(
141 doc=
"for psf candidate evaluation",
145 spatialReject = pexConfig.Field(
146 doc=
"Rejection threshold (stdev) for candidates based on spatial fit",
150 pixelThreshold = pexConfig.Field(
151 doc=
"Threshold (stdev) for rejecting extraneous pixels around candidate; applied if positive",
155 doRejectBlends = pexConfig.Field(
156 doc=
"Reject candidates that are blended?",
160 doMaskBlends = pexConfig.Field(
161 doc=
"Mask blends in image?",
169 A measurePsfTask psf estimator 171 ConfigClass = PcaPsfDeterminerConfig
173 def _fitPsf(self, exposure, psfCellSet, kernelSize, nEigenComponents):
174 PsfCandidateF.setPixelThreshold(self.config.pixelThreshold)
175 PsfCandidateF.setMaskBlends(self.config.doMaskBlends)
179 for nEigen
in range(nEigenComponents, 0, -1):
183 psfCellSet, exposure.getDimensions(), exposure.getXY0(), nEigen,
184 self.config.spatialOrder, kernelSize, self.config.nStarPerCell,
185 bool(self.config.constantWeight))
188 except pexExceptions.LengthError
as e:
190 raise IndexError(
"No viable PSF candidates survive")
192 self.log.warn(
"%s: reducing number of eigen components" % e.what())
197 size = kernelSize + 2*self.config.borderWidth
200 for l
in eigenValues]
204 kernel, psfCellSet, bool(self.config.nonLinearSpatialFit),
205 self.config.nStarPerCellSpatialFit, self.config.tolerance, self.config.lam)
209 return psf, eigenValues, nEigen, chi2
211 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None):
212 """!Determine a PCA PSF model for an exposure given a list of PSF candidates 214 \param[in] exposure exposure containing the psf candidates (lsst.afw.image.Exposure) 215 \param[in] psfCandidateList a sequence of PSF candidates (each an lsst.meas.algorithms.PsfCandidate); 216 typically obtained by detecting sources and then running them through a star selector 217 \param[in,out] metadata a home for interesting tidbits of information 218 \param[in] flagKey schema key used to mark sources actually used in PSF determination 221 - psf: the measured PSF, an lsst.meas.algorithms.PcaPsf 222 - cellSet: an lsst.afw.math.SpatialCellSet containing the PSF candidates 227 displayPsfCandidates =
lsstDebug.Info(__name__).displayPsfCandidates
229 displayPsfComponents =
lsstDebug.Info(__name__).displayPsfComponents
233 matchKernelAmplitudes =
lsstDebug.Info(__name__).matchKernelAmplitudes
235 keepMatplotlibPlots =
lsstDebug.Info(__name__).keepMatplotlibPlots
236 displayPsfSpatialModel =
lsstDebug.Info(__name__).displayPsfSpatialModel
245 mi = exposure.getMaskedImage()
247 if len(psfCandidateList) == 0:
248 raise RuntimeError(
"No PSF candidates supplied.")
254 for i, psfCandidate
in enumerate(psfCandidateList):
255 if psfCandidate.getSource().getPsfFluxFlag():
259 psfCellSet.insertCandidate(psfCandidate)
260 except Exception
as e:
261 self.log.debug(
"Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e)
263 source = psfCandidate.getSource()
265 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy())
266 axes = afwEll.Axes(quad)
267 sizes.append(axes.getA())
269 raise RuntimeError(
"No usable PSF candidates supplied")
270 nEigenComponents = self.config.nEigenComponents
272 if self.config.kernelSize >= 15:
273 self.log.warn(
"WARNING: NOT scaling kernelSize by stellar quadrupole moment " +
274 "because config.kernelSize=%s >= 15; " +
275 "using config.kernelSize as as the width, instead",
276 self.config.kernelSize)
277 actualKernelSize = int(self.config.kernelSize)
279 medSize = numpy.median(sizes)
280 actualKernelSize = 2 * int(self.config.kernelSize * math.sqrt(medSize) + 0.5) + 1
281 if actualKernelSize < self.config.kernelSizeMin:
282 actualKernelSize = self.config.kernelSizeMin
283 if actualKernelSize > self.config.kernelSizeMax:
284 actualKernelSize = self.config.kernelSizeMax
287 print(
"Median size=%s" % (medSize,))
288 self.log.trace(
"Kernel size=%s", actualKernelSize)
291 psfCandidateList[0].setHeight(actualKernelSize)
292 psfCandidateList[0].setWidth(actualKernelSize)
294 if self.config.doRejectBlends:
296 blendedCandidates = []
298 if len(cand.getSource().getFootprint().getPeaks()) > 1:
299 blendedCandidates.append((cell, cand))
302 print(
"Removing %d blended Psf candidates" % len(blendedCandidates))
303 for cell, cand
in blendedCandidates:
304 cell.removeCandidate(cand)
306 raise RuntimeError(
"All PSF candidates removed as blends")
311 ds9.mtv(exposure, frame=frame, title=
"psf determination")
312 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell,
313 symb=
"o", ctype=ds9.CYAN, ctypeUnused=ds9.YELLOW,
320 for iterNum
in range(self.config.nIterForPsf):
321 if display
and displayPsfCandidates:
326 for cell
in psfCellSet.getCellList():
327 for cand
in cell.begin(
not showBadCandidates):
329 im = cand.getMaskedImage()
331 chi2 = cand.getChi2()
335 stamps.append((im,
"%d%s" %
336 (maUtils.splitId(cand.getSource().getId(),
True)[
"objId"], chi2),
338 except Exception
as e:
342 print(
"WARNING: No PSF candidates to show; try setting showBadCandidates=True")
344 mos = displayUtils.Mosaic()
345 for im, label, status
in stamps:
346 im = type(im)(im,
True)
349 except NotImplementedError:
352 mos.append(im, label,
353 ds9.GREEN
if status == afwMath.SpatialCellCandidate.GOOD
else 354 ds9.YELLOW
if status == afwMath.SpatialCellCandidate.UNKNOWN
else ds9.RED)
356 mos.makeMosaic(frame=8, title=
"Psf Candidates")
365 psf, eigenValues, nEigenComponents, fitChi2 = \
366 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
371 for cell
in psfCellSet.getCellList():
373 for cand
in cell.begin(
False):
374 cand.setStatus(afwMath.SpatialCellCandidate.UNKNOWN)
375 rchi2 = cand.getChi2()
376 if not numpy.isfinite(rchi2)
or rchi2 <= 0:
378 awfulCandidates.append(cand)
380 self.log.debug(
"chi^2=%s; id=%s",
381 cand.getChi2(), cand.getSource().getId())
382 for cand
in awfulCandidates:
384 print(
"Removing bad candidate: id=%d, chi^2=%f" %
385 (cand.getSource().getId(), cand.getChi2()))
386 cell.removeCandidate(cand)
391 badCandidates = list()
392 for cell
in psfCellSet.getCellList():
393 for cand
in cell.begin(
False):
394 rchi2 = cand.getChi2()
396 if rchi2 > self.config.reducedChi2ForPsfCandidates:
397 badCandidates.append(cand)
399 badCandidates.sort(key=
lambda x: x.getChi2(), reverse=
True)
401 self.config.nIterForPsf)
402 for i, c
in zip(range(numBad), badCandidates):
408 print(
"Chi^2 clipping %-4d %.2g" % (c.getSource().getId(), chi2))
409 c.setStatus(afwMath.SpatialCellCandidate.BAD)
422 kernel = psf.getKernel()
423 noSpatialKernel = psf.getKernel()
424 for cell
in psfCellSet.getCellList():
425 for cand
in cell.begin(
False):
428 im = cand.getMaskedImage(kernel.getWidth(), kernel.getHeight())
429 except Exception
as e:
436 for p, k
in zip(params, kernels):
437 amp += p * k.getSum()
439 predict = [kernel.getSpatialFunction(k)(candCenter.getX(), candCenter.getY())
for 440 k
in range(kernel.getNKernelParameters())]
444 residuals.append([a / amp - p
for a, p
in zip(params, predict)])
445 candidates.append(cand)
447 residuals = numpy.array(residuals)
449 for k
in range(kernel.getNKernelParameters()):
452 mean = residuals[:, k].mean()
453 rms = residuals[:, k].
std()
456 sr = numpy.sort(residuals[:, k])
457 mean = sr[int(0.5*len(sr))]
if len(sr) % 2
else \
458 0.5 * (sr[int(0.5*len(sr))] + sr[int(0.5*len(sr))+1])
459 rms = 0.74 * (sr[int(0.75*len(sr))] - sr[int(0.25*len(sr))])
462 mean = stats.getValue(afwMath.MEANCLIP)
463 rms = stats.getValue(afwMath.STDEVCLIP)
465 rms = max(1.0e-4, rms)
468 print(
"Mean for component %d is %f" % (k, mean))
469 print(
"RMS for component %d is %f" % (k, rms))
470 badCandidates = list()
471 for i, cand
in enumerate(candidates):
472 if numpy.fabs(residuals[i, k] - mean) > self.config.spatialReject * rms:
473 badCandidates.append(i)
475 badCandidates.sort(key=
lambda x: numpy.fabs(residuals[x, k] - mean), reverse=
True)
478 self.config.nIterForPsf)
480 for i, c
in zip(range(min(len(badCandidates), numBad)), badCandidates):
483 print(
"Spatial clipping %d (%f,%f) based on %d: %f vs %f" %
484 (cand.getSource().getId(), cand.getXCenter(), cand.getYCenter(), k,
485 residuals[badCandidates[i], k], self.config.spatialReject * rms))
486 cand.setStatus(afwMath.SpatialCellCandidate.BAD)
491 if display
and displayIterations:
494 ds9.erase(frame=frame)
495 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=
True,
496 symb=
"o", size=8, frame=frame,
497 ctype=ds9.YELLOW, ctypeBad=ds9.RED, ctypeUnused=ds9.MAGENTA)
498 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
499 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
500 symb=
"o", size=10, frame=frame,
501 ctype=ds9.YELLOW, ctypeBad=ds9.RED)
505 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
506 normalize=normalizeResiduals,
507 showBadCandidates=showBadCandidates)
508 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=5,
509 normalize=normalizeResiduals,
510 showBadCandidates=showBadCandidates,
513 if not showBadCandidates:
514 showBadCandidates =
True 518 if displayPsfComponents:
519 maUtils.showPsf(psf, eigenValues, frame=6)
521 maUtils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
522 ds9.scale(
'linear', 0, 1, frame=7)
523 if displayPsfSpatialModel:
524 maUtils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=
True,
525 matchKernelAmplitudes=matchKernelAmplitudes,
526 keepPlots=keepMatplotlibPlots)
531 reply = input(
"Next iteration? [ynchpqQs] ").strip()
535 reply = reply.split()
537 reply, args = reply[0], reply[1:]
541 if reply
in (
"",
"c",
"h",
"n",
"p",
"q",
"Q",
"s",
"y"):
545 print(
"c[ontinue without prompting] h[elp] n[o] p[db] q[uit displaying] " 546 "s[ave fileName] y[es]")
556 fileName = args.pop(0)
558 print(
"Please provide a filename")
561 print(
"Saving to %s" % fileName)
562 maUtils.saveSpatialCellSet(psfCellSet, fileName=fileName)
566 print(
"Unrecognised response: %s" % reply, file=sys.stderr)
572 psf, eigenValues, nEigenComponents, fitChi2 = \
573 self.
_fitPsf(exposure, psfCellSet, actualKernelSize, nEigenComponents)
578 if display
and reply !=
"n":
580 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCell, showChi2=
True,
581 symb=
"o", ctype=ds9.YELLOW, ctypeBad=ds9.RED, size=8, frame=frame)
582 if self.config.nStarPerCellSpatialFit != self.config.nStarPerCell:
583 maUtils.showPsfSpatialCells(exposure, psfCellSet, self.config.nStarPerCellSpatialFit,
584 symb=
"o", ctype=ds9.YELLOW, ctypeBad=ds9.RED,
585 size=10, frame=frame)
587 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, frame=4,
588 normalize=normalizeResiduals,
589 showBadCandidates=showBadCandidates)
591 if displayPsfComponents:
592 maUtils.showPsf(psf, eigenValues, frame=6)
595 maUtils.showPsfMosaic(exposure, psf, frame=7, showFwhm=
True)
596 ds9.scale(
"linear", 0, 1, frame=7)
597 if displayPsfSpatialModel:
598 maUtils.plotPsfSpatialModel(exposure, psf, psfCellSet, showBadCandidates=
True,
599 matchKernelAmplitudes=matchKernelAmplitudes,
600 keepPlots=keepMatplotlibPlots)
612 for cell
in psfCellSet.getCellList():
613 for cand
in cell.begin(
False):
616 for cand
in cell.begin(
True):
617 src = cand.getSource()
618 if flagKey
is not None:
619 src.set(flagKey,
True)
627 if metadata
is not None:
628 metadata.set(
"spatialFitChi2", fitChi2)
629 metadata.set(
"numGoodStars", numGoodStars)
630 metadata.set(
"numAvailStars", numAvailStars)
631 metadata.set(
"avgX", avgX)
632 metadata.set(
"avgY", avgY)
636 return psf, psfCellSet
640 """!Generator for Psf candidates 642 This allows two 'for' loops to be reduced to one. 644 \param psfCellSet SpatialCellSet of PSF candidates 645 \param ignoreBad Ignore candidates flagged as BAD? 646 \return SpatialCell, PsfCandidate 648 for cell
in psfCellSet.getCellList():
649 for cand
in cell.begin(ignoreBad):
653 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.