22__all__ = [
"PiffPsfDeterminerConfig",
"PiffPsfDeterminerTask"]
33from .piffPsf
import PiffPsf
34from .wcs_wrapper
import CelestialWcsWrapper, UVWcsWrapper
38 """A helper function to validate the GalSim interpolant at config time.
43 The name of the interpolant to use from GalSim. Valid options are:
44 galsim.Lanczos(N)
or Lancsos(N), where N
is a positive integer
50 galsim.SincInterpolant
55 Whether the provided interpolant name
is valid.
58 for pattern
in (re.compile(
r"Lanczos\(\d+\)"), re.compile(
r"galsim.Lanczos\(\d+\)"),):
59 match = re.match(pattern, name)
62 return match.end() == len(name)
65 names = {f
"galsim.{interp}" for interp
in
66 (
"Cubic",
"Delta",
"Linear",
"Nearest",
"Quintic",
"SincInterpolant")
72 spatialOrder = pexConfig.Field[int](
73 doc=
"specify spatial order for PSF kernel creation",
76 samplingSize = pexConfig.Field[float](
77 doc=
"Resolution of the internal PSF model relative to the pixel size; "
78 "e.g. 0.5 is equal to 2x oversampling",
81 outlierNSigma = pexConfig.Field[float](
82 doc=
"n sigma for chisq outlier rejection",
85 outlierMaxRemove = pexConfig.Field[float](
86 doc=
"Max fraction of stars to remove as outliers each iteration",
89 maxSNR = pexConfig.Field[float](
90 doc=
"Rescale the weight of bright stars such that their SNR is less "
94 zeroWeightMaskBits = pexConfig.ListField[str](
95 doc=
"List of mask bits for which to set pixel weights to zero.",
96 default=[
'BAD',
'CR',
'INTRP',
'SAT',
'SUSPECT',
'NO_DATA']
98 minimumUnmaskedFraction = pexConfig.Field[float](
99 doc=
"Minimum fraction of unmasked pixels required to use star.",
102 interpolant = pexConfig.Field[str](
103 doc=
"GalSim interpolant name for Piff to use. "
104 "Options include 'Lanczos(N)', where N is an integer, along with "
105 "galsim.Cubic, galsim.Delta, galsim.Linear, galsim.Nearest, "
106 "galsim.Quintic, and galsim.SincInterpolant.",
107 check=_validateGalsimInterpolant,
108 default=
"Lanczos(11)",
110 debugStarData = pexConfig.Field[bool](
111 doc=
"Include star images used for fitting in PSF model object.",
114 useCoordinates = pexConfig.ChoiceField[str](
115 doc=
"Which spatial coordinates to regress against in PSF modeling.",
117 pixel=
'Regress against pixel coordinates',
118 field=
'Regress against field angles',
119 sky=
'Regress against RA/Dec'
134 """Compute an index array indicating good pixels to use.
139 PSF candidate postage stamp
140 zeroWeightMaskBits : `List[str]`
141 List of mask bits for which to set pixel weights to zero.
146 Index array indicating good pixels.
148 imArr = maskedImage.image.array
149 varArr = maskedImage.variance.array
150 bitmask = maskedImage.mask.getPlaneBitMask(zeroWeightMaskBits)
153 & (np.isfinite(varArr))
154 & (np.isfinite(imArr))
155 & ((maskedImage.mask.array & bitmask) == 0)
161 """Derive a weight map without Poisson variance component due to signal.
166 PSF candidate postage stamp
168 Maximum SNR applying variance floor.
170 Index array indicating good pixels.
175 Array to use for weight.
177 imArr = maskedImage.image.array
178 varArr = maskedImage.variance.array
183 fit = np.polyfit(imArr[good], varArr[good], deg=1)
185 weightArr = np.zeros_like(imArr, dtype=float)
186 weightArr[good] = 1./fit[1]
193 """Rescale weight of bright stars to cap the computed SNR.
198 Signal (image) array of stamp.
199 weightArr : `ndarray`
200 Weight map array. May be rescaled in place.
202 Index array of pixels to use when computing SNR.
204 Threshold
for adjusting variance plane implementing maximum SNR.
231 F = np.sum(weightArr[good]*imArr[good]**2, dtype=float)
233 SNR = 0.0
if F < Npix
else (F-Npix)/np.sqrt(F)
236 factor = (maxSNR / SNR)**2
237 weightArr[good] *= factor
241 """Alternative algorithm for creating weight map.
243 This version is equivalent to that used by Piff internally. The weight map
244 it produces tends to leave a residual when removing the Poisson component
245 due to the signal. We leave it here
as a reference, but without intending
246 that it be used (
or be maintained).
248 imArr = maskedImage.image.array
249 varArr = maskedImage.variance.array
250 good = (varArr != 0) & np.isfinite(varArr) & np.isfinite(imArr)
252 fit = np.polyfit(imArr[good], varArr[good], deg=1)
255 varArr[good] -= imArr[good] / gain
256 weightArr = np.zeros_like(imArr, dtype=float)
257 weightArr[good] = 1./varArr[good]
264 """A measurePsfTask PSF estimator using Piff as the implementation.
266 ConfigClass = PiffPsfDeterminerConfig
267 _DefaultName = "psfDeterminer.Piff"
270 self, exposure, psfCandidateList, metadata=None, flagKey=None
272 """Determine a Piff PSF model for an exposure given a list of PSF
278 Exposure containing the PSF candidates.
280 A sequence of PSF candidates typically obtained by detecting sources
281 and then running them through a star selector.
282 metadata : `
lsst.daf.base import PropertyList`
or `
None`, optional
283 A home
for interesting tidbits of information.
284 flagKey : `str`
or `
None`, optional
285 Schema key used to mark sources actually used
in PSF determination.
289 psf : `lsst.meas.extensions.piff.PiffPsf`
290 The measured PSF model.
292 Unused by this PsfDeterminer.
294 if self.config.stampSize:
295 stampSize = self.config.stampSize
296 if stampSize > psfCandidateList[0].getWidth():
297 self.log.warning(
"stampSize is larger than the PSF candidate size. Using candidate size.")
298 stampSize = psfCandidateList[0].getWidth()
300 self.log.debug(
"stampSize not set. Using candidate size.")
301 stampSize = psfCandidateList[0].getWidth()
303 scale = exposure.getWcs().getPixelScale().asArcseconds()
304 match self.config.useCoordinates:
306 detector = exposure.getDetector()
307 pix_to_field = detector.getTransform(PIXELS, FIELD_ANGLE)
312 skyOrigin = exposure.getWcs().getSkyOrigin()
313 ra = skyOrigin.getLongitude().asDegrees()
314 dec = skyOrigin.getLatitude().asDegrees()
315 pointing = galsim.CelestialCoord(
320 gswcs = galsim.PixelScale(scale)
324 for candidate
in psfCandidateList:
325 cmi = candidate.getMaskedImage(stampSize, stampSize)
327 fracGood = np.sum(good)/good.size
328 if fracGood < self.config.minimumUnmaskedFraction:
333 bds = galsim.BoundsI(
334 galsim.PositionI(*bbox.getMin()),
335 galsim.PositionI(*bbox.getMax())
337 gsImage = galsim.Image(bds, wcs=gswcs, dtype=float)
338 gsImage.array[:] = cmi.image.array
339 gsWeight = galsim.Image(bds, wcs=gswcs, dtype=float)
340 gsWeight.array[:] = weight
342 source = candidate.getSource()
343 image_pos = galsim.PositionD(source.getX(), source.getY())
345 data = piff.StarData(
351 stars.append(piff.Star(data,
None))
357 'scale': scale * self.config.samplingSize,
359 'interp': self.config.interpolant
362 'type':
'BasisPolynomial',
363 'order': self.config.spatialOrder
367 'nsigma': self.config.outlierNSigma,
368 'max_remove': self.config.outlierMaxRemove
372 piffResult = piff.PSF.process(piffConfig)
375 piffResult.fit(stars, wcs, pointing, logger=self.log)
376 drawSize = 2*np.floor(0.5*stampSize/self.config.samplingSize) + 1
378 used_image_pos = [s.image_pos
for s
in piffResult.stars]
380 for candidate
in psfCandidateList:
381 source = candidate.getSource()
382 posd = galsim.PositionD(source.getX(), source.getY())
383 if posd
in used_image_pos:
384 source.set(flagKey,
True)
386 if metadata
is not None:
387 metadata[
"spatialFitChi2"] = piffResult.chisq
388 metadata[
"numAvailStars"] = len(stars)
389 metadata[
"numGoodStars"] = len(piffResult.stars)
390 metadata[
"avgX"] = np.mean([p.x
for p
in piffResult.stars])
391 metadata[
"avgY"] = np.mean([p.y
for p
in piffResult.stars])
393 if not self.config.debugStarData:
394 for star
in piffResult.stars:
397 del star.fit.params_var
402 del star.data.orig_weight
404 return PiffPsf(drawSize, drawSize, piffResult),
None
407measAlg.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.
determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
_computeWeightAlternative(maskedImage, maxSNR)
getGoodPixels(maskedImage, zeroWeightMaskBits)
applyMaxSNR(imArr, weightArr, good, maxSNR)
computeWeight(maskedImage, maxSNR, good)
bool _validateGalsimInterpolant(str name)