LSST Applications  21.0.0-172-gfb10e10a+18fedfabac,22.0.0+297cba6710,22.0.0+80564b0ff1,22.0.0+8d77f4f51a,22.0.0+a28f4c53b1,22.0.0+dcf3732eb2,22.0.1-1-g7d6de66+2a20fdde0d,22.0.1-1-g8e32f31+297cba6710,22.0.1-1-geca5380+7fa3b7d9b6,22.0.1-12-g44dc1dc+2a20fdde0d,22.0.1-15-g6a90155+515f58c32b,22.0.1-16-g9282f48+790f5f2caa,22.0.1-2-g92698f7+dcf3732eb2,22.0.1-2-ga9b0f51+7fa3b7d9b6,22.0.1-2-gd1925c9+bf4f0e694f,22.0.1-24-g1ad7a390+a9625a72a8,22.0.1-25-g5bf6245+3ad8ecd50b,22.0.1-25-gb120d7b+8b5510f75f,22.0.1-27-g97737f7+2a20fdde0d,22.0.1-32-gf62ce7b1+aa4237961e,22.0.1-4-g0b3f228+2a20fdde0d,22.0.1-4-g243d05b+871c1b8305,22.0.1-4-g3a563be+32dcf1063f,22.0.1-4-g44f2e3d+9e4ab0f4fa,22.0.1-42-gca6935d93+ba5e5ca3eb,22.0.1-5-g15c806e+85460ae5f3,22.0.1-5-g58711c4+611d128589,22.0.1-5-g75bb458+99c117b92f,22.0.1-6-g1c63a23+7fa3b7d9b6,22.0.1-6-g50866e6+84ff5a128b,22.0.1-6-g8d3140d+720564cf76,22.0.1-6-gd805d02+cc5644f571,22.0.1-8-ge5750ce+85460ae5f3,master-g6e05de7fdc+babf819c66,master-g99da0e417a+8d77f4f51a,w.2021.48
LSST Data Management Base Package
piffPsfDeterminer.py
Go to the documentation of this file.
1 # This file is part of meas_extensions_piff.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 
22 __all__ = ["PiffPsfDeterminerConfig", "PiffPsfDeterminerTask"]
23 
24 import numpy as np
25 import piff
26 import galsim
27 
28 import lsst.pex.config as pexConfig
29 import lsst.meas.algorithms as measAlg
30 from lsst.meas.algorithms.psfDeterminer import BasePsfDeterminerTask
31 from .piffPsf import PiffPsf
32 
33 
34 class PiffPsfDeterminerConfig(BasePsfDeterminerTask.ConfigClass):
35  spatialOrder = pexConfig.Field(
36  doc="specify spatial order for PSF kernel creation",
37  dtype=int,
38  default=2,
39  )
40  samplingSize = pexConfig.Field(
41  doc="Resolution of the internal PSF model relative to the pixel size; "
42  "e.g. 0.5 is equal to 2x oversampling",
43  dtype=float,
44  default=1,
45  )
46  outlierNSigma = pexConfig.Field(
47  doc="n sigma for chisq outlier rejection",
48  dtype=float,
49  default=4.0
50  )
51  outlierMaxRemove = pexConfig.Field(
52  doc="Max fraction of stars to remove as outliers each iteration",
53  dtype=float,
54  default=0.05
55  )
56  maxSNR = pexConfig.Field(
57  doc="Rescale the weight of bright stars such that their SNR is less "
58  "than this value.",
59  dtype=float,
60  default=200.0
61  )
62 
63  def setDefaults(self):
64  self.kernelSizekernelSize = 21
65  self.kernelSizeMinkernelSizeMin = 11
66  self.kernelSizeMaxkernelSizeMax = 35
67 
68 
69 def computeWeight(maskedImage, maxSNR):
70  """Derive a weight map without Poisson variance component due to signal.
71 
72  Parameters
73  ----------
74  maskedImage : `afw.image.MaskedImage`
75  PSF candidate postage stamp
76  maxSNR : `float`
77  Maximum SNR applying variance floor.
78 
79  Returns
80  -------
81  weightArr : `ndarry`
82  Array to use for weight.
83  """
84  imArr = maskedImage.image.array
85  varArr = maskedImage.variance.array
86  good = (varArr != 0) & np.isfinite(varArr) & np.isfinite(imArr)
87 
88  # Fit a straight line to variance vs (sky-subtracted) signal.
89  # The evaluate that line at zero signal to get an estimate of the
90  # signal-free variance.
91  fit = np.polyfit(imArr[good], varArr[good], deg=1)
92  # fit is [1/gain, sky_var]
93  weightArr = np.zeros_like(imArr, dtype=float)
94  weightArr[good] = 1./fit[1]
95 
96  applyMaxSNR(imArr, weightArr, good, maxSNR)
97  return weightArr
98 
99 
100 def applyMaxSNR(imArr, weightArr, good, maxSNR):
101  """Rescale weight of bright stars to cap the computed SNR.
102 
103  Parameters
104  ----------
105  imArr : `ndarray`
106  Signal (image) array of stamp.
107  weightArr : `ndarray`
108  Weight map array. May be rescaled in place.
109  good : `ndarray`
110  Index array of pixels to use when computing SNR.
111  maxSNR : `float`
112  Threshold for adjusting variance plane implementing maximum SNR.
113  """
114  # We define the SNR value following Piff. Here's the comment from that
115  # code base explaining the calculation.
116  #
117  # The S/N value that we use will be the weighted total flux where the
118  # weight function is the star's profile itself. This is the maximum S/N
119  # value that any flux measurement can possibly produce, which will be
120  # closer to an in-practice S/N than using all the pixels equally.
121  #
122  # F = Sum_i w_i I_i^2
123  # var(F) = Sum_i w_i^2 I_i^2 var(I_i)
124  # = Sum_i w_i I_i^2 <--- Assumes var(I_i) = 1/w_i
125  #
126  # S/N = F / sqrt(var(F))
127  #
128  # Note that if the image is pure noise, this will produce a "signal" of
129  #
130  # F_noise = Sum_i w_i 1/w_i = Npix
131  #
132  # So for a more accurate estimate of the S/N of the actual star itself, one
133  # should subtract off Npix from the measured F.
134  #
135  # The final formula then is:
136  #
137  # F = Sum_i w_i I_i^2
138  # S/N = (F-Npix) / sqrt(F)
139  F = np.sum(weightArr[good]*imArr[good]**2, dtype=float)
140  Npix = np.sum(good)
141  SNR = 0.0 if F < Npix else (F-Npix)/np.sqrt(F)
142  # rescale weight of bright stars. Essentially makes an error floor.
143  if SNR > maxSNR:
144  factor = (maxSNR / SNR)**2
145  weightArr[good] *= factor
146 
147 
148 def _computeWeightAlternative(maskedImage, maxSNR):
149  """Alternative algorithm for creating weight map.
150 
151  This version is equivalent to that used by Piff internally. The weight map
152  it produces tends to leave a residual when removing the Poisson component
153  due to the signal. We leave it here as a reference, but without intending
154  that it be used.
155  """
156  imArr = maskedImage.image.array
157  varArr = maskedImage.variance.array
158  good = (varArr != 0) & np.isfinite(varArr) & np.isfinite(imArr)
159 
160  fit = np.polyfit(imArr[good], varArr[good], deg=1)
161  # fit is [1/gain, sky_var]
162  gain = 1./fit[0]
163  varArr[good] -= imArr[good] / gain
164  weightArr = np.zeros_like(imArr, dtype=float)
165  weightArr[good] = 1./varArr[good]
166 
167  applyMaxSNR(imArr, weightArr, good, maxSNR)
168  return weightArr
169 
170 
172  """A measurePsfTask PSF estimator using Piff as the implementation.
173  """
174  ConfigClass = PiffPsfDeterminerConfig
175  _DefaultName = "psfDeterminer.Piff"
176 
178  self, exposure, psfCandidateList, metadata=None, flagKey=None
179  ):
180  """Determine a Piff PSF model for an exposure given a list of PSF
181  candidates.
182 
183  Parameters
184  ----------
185  exposure : `lsst.afw.image.Exposure`
186  Exposure containing the PSF candidates.
187  psfCandidateList : `list` of `lsst.meas.algorithms.PsfCandidate`
188  A sequence of PSF candidates typically obtained by detecting sources
189  and then running them through a star selector.
190  metadata : `lsst.daf.base import PropertyList` or `None`, optional
191  A home for interesting tidbits of information.
192  flagKey : `str` or `None`, optional
193  Schema key used to mark sources actually used in PSF determination.
194 
195  Returns
196  -------
197  psf : `lsst.meas.extensions.piff.PiffPsf`
198  The measured PSF model.
199  psfCellSet : `None`
200  Unused by this PsfDeterminer.
201  """
202  stars = []
203  for candidate in psfCandidateList:
204  cmi = candidate.getMaskedImage()
205  weight = computeWeight(cmi, self.config.maxSNR)
206 
207  bbox = cmi.getBBox()
208  bds = galsim.BoundsI(
209  galsim.PositionI(*bbox.getMin()),
210  galsim.PositionI(*bbox.getMax())
211  )
212  gsImage = galsim.Image(bds, scale=1.0, dtype=float)
213  gsImage.array[:] = cmi.image.array
214  gsWeight = galsim.Image(bds, scale=1.0, dtype=float)
215  gsWeight.array[:] = weight
216 
217  source = candidate.getSource()
218  image_pos = galsim.PositionD(source.getX(), source.getY())
219 
220  data = piff.StarData(
221  gsImage,
222  image_pos,
223  weight=gsWeight
224  )
225  stars.append(piff.Star(data, None))
226 
227  kernelSize = int(np.clip(
228  self.config.kernelSize,
229  self.config.kernelSizeMin,
230  self.config.kernelSizeMax
231  ))
232 
233  piffConfig = {
234  'type': "Simple",
235  'model': {
236  'type': 'PixelGrid',
237  'scale': self.config.samplingSize,
238  'size': kernelSize
239  },
240  'interp': {
241  'type': 'BasisPolynomial',
242  'order': self.config.spatialOrder
243  },
244  'outliers': {
245  'type': 'Chisq',
246  'nsigma': self.config.outlierNSigma,
247  'max_remove': self.config.outlierMaxRemove
248  }
249  }
250 
251  piffResult = piff.PSF.process(piffConfig)
252  # Run on a single CCD, and in image coords rather than sky coords.
253  wcs = {0: galsim.PixelScale(1.0)}
254  pointing = None
255 
256  piffResult.fit(stars, wcs, pointing, logger=self.log)
257  psf = PiffPsf(kernelSize, kernelSize, piffResult)
258 
259  used_image_pos = [s.image_pos for s in piffResult.stars]
260  if flagKey:
261  for candidate in psfCandidateList:
262  source = candidate.getSource()
263  posd = galsim.PositionD(source.getX(), source.getY())
264  if posd in used_image_pos:
265  source.set(flagKey, True)
266 
267  if metadata is not None:
268  metadata["spatialFitChi2"] = piffResult.chisq
269  metadata["numAvailStars"] = len(stars)
270  metadata["numGoodStars"] = len(piffResult.stars)
271  metadata["avgX"] = np.mean([p.x for p in piffResult.stars])
272  metadata["avgY"] = np.mean([p.y for p in piffResult.stars])
273 
274  return psf, None
275 
276 
277 measAlg.psfDeterminerRegistry.register("piff", PiffPsfDeterminerTask)
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 applyMaxSNR(imArr, weightArr, good, maxSNR)