22__all__ = [
"PiffPsfDeterminerConfig",
"PiffPsfDeterminerTask"]
32from .piffPsf
import PiffPsf
36 def _validateGalsimInterpolant(name: str) -> bool:
37 """A helper function to validate the GalSim interpolant at config time.
42 The name of the interpolant to use from GalSim. Valid options are:
43 Lancsos(N) where n
is a positive integer
54 Whether the provided interpolant name
is valid.
57 for pattern
in (re.compile(
r"Lanczos\(\d+\)"), re.compile(
r"galsim.Lanczos\(\d+\)"),):
58 match = re.match(pattern, name)
61 return match.end() == len(name)
64 names = {
"galsim.{interp}" for interp
in
65 (
"Cubic",
"Delta",
"Linear",
"Nearest",
"Quintic",
"SincInterpolant")
69 spatialOrder = pexConfig.Field(
70 doc=
"specify spatial order for PSF kernel creation",
74 samplingSize = pexConfig.Field(
75 doc=
"Resolution of the internal PSF model relative to the pixel size; "
76 "e.g. 0.5 is equal to 2x oversampling",
80 outlierNSigma = pexConfig.Field(
81 doc=
"n sigma for chisq outlier rejection",
85 outlierMaxRemove = pexConfig.Field(
86 doc=
"Max fraction of stars to remove as outliers each iteration",
90 maxSNR = pexConfig.Field(
91 doc=
"Rescale the weight of bright stars such that their SNR is less "
96 zeroWeightMaskBits = pexConfig.ListField(
97 doc=
"List of mask bits for which to set pixel weights to zero.",
99 default=[
'BAD',
'CR',
'INTRP',
'SAT',
'SUSPECT',
'NO_DATA']
101 minimumUnmaskedFraction = pexConfig.Field(
102 doc=
"Minimum fraction of unmasked pixels required to use star.",
106 interpolant = pexConfig.Field(
107 doc=
"GalSim interpolant name for Piff to use. "
108 "Options include 'Lanczos(N)', where N is an integer, along with "
109 "galsim.Cubic, galsim.Delta, galsim.Linear, galsim.Nearest, "
110 "galsim.Quintic, and galsim.SincInterpolant.",
112 check=_validateGalsimInterpolant,
113 default=
"Lanczos(11)",
127 """Compute an index array indicating good pixels to use.
132 PSF candidate postage stamp
133 zeroWeightMaskBits : `List[str]`
134 List of mask bits for which to set pixel weights to zero.
139 Index array indicating good pixels.
141 imArr = maskedImage.image.array
142 varArr = maskedImage.variance.array
143 bitmask = maskedImage.mask.getPlaneBitMask(zeroWeightMaskBits)
146 & (np.isfinite(varArr))
147 & (np.isfinite(imArr))
148 & ((maskedImage.mask.array & bitmask) == 0)
154 """Derive a weight map without Poisson variance component due to signal.
159 PSF candidate postage stamp
161 Maximum SNR applying variance floor.
163 Index array indicating good pixels.
168 Array to use for weight.
170 imArr = maskedImage.image.array
171 varArr = maskedImage.variance.array
176 fit = np.polyfit(imArr[good], varArr[good], deg=1)
178 weightArr = np.zeros_like(imArr, dtype=float)
179 weightArr[good] = 1./fit[1]
186 """Rescale weight of bright stars to cap the computed SNR.
191 Signal (image) array of stamp.
192 weightArr : `ndarray`
193 Weight map array. May be rescaled in place.
195 Index array of pixels to use when computing SNR.
197 Threshold
for adjusting variance plane implementing maximum SNR.
224 F = np.sum(weightArr[good]*imArr[good]**2, dtype=float)
226 SNR = 0.0
if F < Npix
else (F-Npix)/np.sqrt(F)
229 factor = (maxSNR / SNR)**2
230 weightArr[good] *= factor
233def _computeWeightAlternative(maskedImage, maxSNR):
234 """Alternative algorithm for creating weight map.
236 This version is equivalent to that used by Piff internally. The weight map
237 it produces tends to leave a residual when removing the Poisson component
238 due to the signal. We leave it here
as a reference, but without intending
239 that it be used (
or be maintained).
241 imArr = maskedImage.image.array
242 varArr = maskedImage.variance.array
243 good = (varArr != 0) & np.isfinite(varArr) & np.isfinite(imArr)
245 fit = np.polyfit(imArr[good], varArr[good], deg=1)
248 varArr[good] -= imArr[good] / gain
249 weightArr = np.zeros_like(imArr, dtype=float)
250 weightArr[good] = 1./varArr[good]
257 """A measurePsfTask PSF estimator using Piff as the implementation.
259 ConfigClass = PiffPsfDeterminerConfig
260 _DefaultName = "psfDeterminer.Piff"
263 self, exposure, psfCandidateList, metadata=None, flagKey=None
265 """Determine a Piff PSF model for an exposure given a list of PSF
271 Exposure containing the PSF candidates.
273 A sequence of PSF candidates typically obtained by detecting sources
274 and then running them through a star selector.
275 metadata : `
lsst.daf.base import PropertyList`
or `
None`, optional
276 A home
for interesting tidbits of information.
277 flagKey : `str`
or `
None`, optional
278 Schema key used to mark sources actually used
in PSF determination.
282 psf : `lsst.meas.extensions.piff.PiffPsf`
283 The measured PSF model.
285 Unused by this PsfDeterminer.
287 kernelSize = int(np.clip(
288 self.config.kernelSize,
289 self.config.kernelSizeMin,
290 self.config.kernelSizeMax
295 for candidate
in psfCandidateList:
296 cmi = candidate.getMaskedImage()
298 fracGood = np.sum(good)/good.size
299 if fracGood < self.config.minimumUnmaskedFraction:
304 bds = galsim.BoundsI(
305 galsim.PositionI(*bbox.getMin()),
306 galsim.PositionI(*bbox.getMax())
308 gsImage = galsim.Image(bds, scale=1.0, dtype=float)
309 gsImage.array[:] = cmi.image.array
310 gsWeight = galsim.Image(bds, scale=1.0, dtype=float)
311 gsWeight.array[:] = weight
313 source = candidate.getSource()
314 image_pos = galsim.PositionD(source.getX(), source.getY())
316 data = piff.StarData(
321 stars.append(piff.Star(data,
None))
327 'scale': self.config.samplingSize,
329 'interp': self.config.interpolant
332 'type':
'BasisPolynomial',
333 'order': self.config.spatialOrder
337 'nsigma': self.config.outlierNSigma,
338 'max_remove': self.config.outlierMaxRemove
342 piffResult = piff.PSF.process(piffConfig)
344 wcs = {0: galsim.PixelScale(1.0)}
347 piffResult.fit(stars, wcs, pointing, logger=self.log)
348 drawSize = 2*np.floor(0.5*kernelSize/self.config.samplingSize) + 1
349 psf =
PiffPsf(drawSize, drawSize, piffResult)
351 used_image_pos = [s.image_pos
for s
in piffResult.stars]
353 for candidate
in psfCandidateList:
354 source = candidate.getSource()
355 posd = galsim.PositionD(source.getX(), source.getY())
356 if posd
in used_image_pos:
357 source.set(flagKey,
True)
359 if metadata
is not None:
360 metadata[
"spatialFitChi2"] = piffResult.chisq
361 metadata[
"numAvailStars"] = len(stars)
362 metadata[
"numGoodStars"] = len(piffResult.stars)
363 metadata[
"avgX"] = np.mean([p.x
for p
in piffResult.stars])
364 metadata[
"avgY"] = np.mean([p.y
for p
in piffResult.stars])
368 def _validatePsfCandidates(self, psfCandidateList, kernelSize, samplingSize):
369 """Raise if psfCandidates are smaller than the configured kernelSize.
374 Sequence of psf candidates to check.
376 Size of image model to use in PIFF.
377 samplingSize : `float`
378 Resolution of the internal PSF model relative to the pixel size.
383 Raised
if any psfCandidate has width
or height smaller than
387 candidate = psfCandidateList[0]
388 drawSize =
int(2*np.floor(0.5*kernelSize/samplingSize) + 1)
389 if (candidate.getHeight() < drawSize
390 or candidate.getWidth() < drawSize):
391 raise RuntimeError(
"PSF candidates must be at least config.kernelSize/config.samplingSize="
392 f
"{drawSize} pixels per side; "
393 f
"found {candidate.getWidth()}x{candidate.getHeight()}.")
396measAlg.psfDeterminerRegistry.register(
"piff", PiffPsfDeterminerTask)
A class to contain the data, WCS, and other information needed to describe an image of the sky.
A class to manipulate images, masks, and variance as a single object.
Class stored in SpatialCells for spatial Psf fitting.
def _validatePsfCandidates(self, psfCandidateList, kernelSize, samplingSize)
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def getGoodPixels(maskedImage, zeroWeightMaskBits)
def applyMaxSNR(imArr, weightArr, good, maxSNR)
def computeWeight(maskedImage, maxSNR, good)