Loading [MathJax]/extensions/tex2jax.js
LSST Applications  21.0.0+04719a4bac,21.0.0-1-ga51b5d4+f5e6047307,21.0.0-11-g2b59f77+a9c1acf22d,21.0.0-11-ga42c5b2+86977b0b17,21.0.0-12-gf4ce030+76814010d2,21.0.0-13-g1721dae+760e7a6536,21.0.0-13-g3a573fe+768d78a30a,21.0.0-15-g5a7caf0+f21cbc5713,21.0.0-16-g0fb55c1+b60e2d390c,21.0.0-19-g4cded4ca+71a93a33c0,21.0.0-2-g103fe59+bb20972958,21.0.0-2-g45278ab+04719a4bac,21.0.0-2-g5242d73+3ad5d60fb1,21.0.0-2-g7f82c8f+8babb168e8,21.0.0-2-g8f08a60+06509c8b61,21.0.0-2-g8faa9b5+616205b9df,21.0.0-2-ga326454+8babb168e8,21.0.0-2-gde069b7+5e4aea9c2f,21.0.0-2-gecfae73+1d3a86e577,21.0.0-2-gfc62afb+3ad5d60fb1,21.0.0-25-g1d57be3cd+e73869a214,21.0.0-3-g357aad2+ed88757d29,21.0.0-3-g4a4ce7f+3ad5d60fb1,21.0.0-3-g4be5c26+3ad5d60fb1,21.0.0-3-g65f322c+e0b24896a3,21.0.0-3-g7d9da8d+616205b9df,21.0.0-3-ge02ed75+a9c1acf22d,21.0.0-4-g591bb35+a9c1acf22d,21.0.0-4-g65b4814+b60e2d390c,21.0.0-4-gccdca77+0de219a2bc,21.0.0-4-ge8a399c+6c55c39e83,21.0.0-5-gd00fb1e+05fce91b99,21.0.0-6-gc675373+3ad5d60fb1,21.0.0-64-g1122c245+4fb2b8f86e,21.0.0-7-g04766d7+cd19d05db2,21.0.0-7-gdf92d54+04719a4bac,21.0.0-8-g5674e7b+d1bd76f71f,master-gac4afde19b+a9c1acf22d,w.2021.13
LSST Data Management Base Package
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
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 logging
25 
26 import numpy as np
27 import piff
28 import galsim
29 
30 import lsst.log
31 import lsst.pex.config as pexConfig
32 import lsst.meas.algorithms as measAlg
33 from lsst.meas.algorithms.psfDeterminer import BasePsfDeterminerTask
34 from .piffPsf import PiffPsf
35 
36 
37 class PiffPsfDeterminerConfig(BasePsfDeterminerTask.ConfigClass):
38  spatialOrder = pexConfig.Field(
39  doc="specify spatial order for PSF kernel creation",
40  dtype=int,
41  default=2,
42  )
43  samplingSize = pexConfig.Field(
44  doc="Resolution of the internal PSF model relative to the pixel size; "
45  "e.g. 0.5 is equal to 2x oversampling",
46  dtype=float,
47  default=1,
48  )
49  outlierNSigma = pexConfig.Field(
50  doc="n sigma for chisq outlier rejection",
51  dtype=float,
52  default=4.0
53  )
54  outlierMaxRemove = pexConfig.Field(
55  doc="Max fraction of stars to remove as outliers each iteration",
56  dtype=float,
57  default=0.05
58  )
59  maxSNR = pexConfig.Field(
60  doc="Rescale the weight of bright stars such that their SNR is less "
61  "than this value.",
62  dtype=float,
63  default=200.0
64  )
65 
66  def setDefaults(self):
67  self.kernelSizekernelSize = 21
68  self.kernelSizeMinkernelSizeMin = 11
69  self.kernelSizeMaxkernelSizeMax = 35
70 
71 
72 def computeWeight(maskedImage, maxSNR):
73  """Derive a weight map without Poisson variance component due to signal.
74 
75  Parameters
76  ----------
77  maskedImage : `afw.image.MaskedImage`
78  PSF candidate postage stamp
79  maxSNR : `float`
80  Maximum SNR applying variance floor.
81 
82  Returns
83  -------
84  weightArr : `ndarry`
85  Array to use for weight.
86  """
87  imArr = maskedImage.image.array
88  varArr = maskedImage.variance.array
89  good = (varArr != 0) & np.isfinite(varArr) & np.isfinite(imArr)
90 
91  # Fit a straight line to variance vs (sky-subtracted) signal.
92  # The evaluate that line at zero signal to get an estimate of the
93  # signal-free variance.
94  fit = np.polyfit(imArr[good], varArr[good], deg=1)
95  # fit is [1/gain, sky_var]
96  weightArr = np.zeros_like(imArr, dtype=float)
97  weightArr[good] = 1./fit[1]
98 
99  applyMaxSNR(imArr, weightArr, good, maxSNR)
100  return weightArr
101 
102 
103 def applyMaxSNR(imArr, weightArr, good, maxSNR):
104  """Rescale weight of bright stars to cap the computed SNR.
105 
106  Parameters
107  ----------
108  imArr : `ndarray`
109  Signal (image) array of stamp.
110  weightArr : `ndarray`
111  Weight map array. May be rescaled in place.
112  good : `ndarray`
113  Index array of pixels to use when computing SNR.
114  maxSNR : `float`
115  Threshold for adjusting variance plane implementing maximum SNR.
116  """
117  # We define the SNR value following Piff. Here's the comment from that
118  # code base explaining the calculation.
119  #
120  # The S/N value that we use will be the weighted total flux where the
121  # weight function is the star's profile itself. This is the maximum S/N
122  # value that any flux measurement can possibly produce, which will be
123  # closer to an in-practice S/N than using all the pixels equally.
124  #
125  # F = Sum_i w_i I_i^2
126  # var(F) = Sum_i w_i^2 I_i^2 var(I_i)
127  # = Sum_i w_i I_i^2 <--- Assumes var(I_i) = 1/w_i
128  #
129  # S/N = F / sqrt(var(F))
130  #
131  # Note that if the image is pure noise, this will produce a "signal" of
132  #
133  # F_noise = Sum_i w_i 1/w_i = Npix
134  #
135  # So for a more accurate estimate of the S/N of the actual star itself, one
136  # should subtract off Npix from the measured F.
137  #
138  # The final formula then is:
139  #
140  # F = Sum_i w_i I_i^2
141  # S/N = (F-Npix) / sqrt(F)
142  F = np.sum(weightArr[good]*imArr[good]**2, dtype=float)
143  Npix = np.sum(good)
144  SNR = 0.0 if F < Npix else (F-Npix)/np.sqrt(F)
145  # rescale weight of bright stars. Essentially makes an error floor.
146  if SNR > maxSNR:
147  factor = (maxSNR / SNR)**2
148  weightArr[good] *= factor
149 
150 
151 def _computeWeightAlternative(maskedImage, maxSNR):
152  """Alternative algorithm for creating weight map.
153 
154  This version is equivalent to that used by Piff internally. The weight map
155  it produces tends to leave a residual when removing the Poisson component
156  due to the signal. We leave it here as a reference, but without intending
157  that it be used.
158  """
159  imArr = maskedImage.image.array
160  varArr = maskedImage.variance.array
161  good = (varArr != 0) & np.isfinite(varArr) & np.isfinite(imArr)
162 
163  fit = np.polyfit(imArr[good], varArr[good], deg=1)
164  # fit is [1/gain, sky_var]
165  gain = 1./fit[0]
166  varArr[good] -= imArr[good] / gain
167  weightArr = np.zeros_like(imArr, dtype=float)
168  weightArr[good] = 1./varArr[good]
169 
170  applyMaxSNR(imArr, weightArr, good, maxSNR)
171  return weightArr
172 
173 
175  """A measurePsfTask PSF estimator using Piff as the implementation.
176  """
177  ConfigClass = PiffPsfDeterminerConfig
178 
180  self, exposure, psfCandidateList, metadata=None, flagKey=None
181  ):
182  """Determine a Piff PSF model for an exposure given a list of PSF
183  candidates.
184 
185  Parameters
186  ----------
187  exposure : `lsst.afw.image.Exposure`
188  Exposure containing the PSF candidates.
189  psfCandidateList : `list` of `lsst.meas.algorithms.PsfCandidate`
190  A sequence of PSF candidates typically obtained by detecting sources
191  and then running them through a star selector.
192  metadata : `lsst.daf.base import PropertyList` or `None`, optional
193  A home for interesting tidbits of information.
194  flagKey : `str` or `None`, optional
195  Schema key used to mark sources actually used in PSF determination.
196 
197  Returns
198  -------
199  psf : `lsst.meas.extensions.piff.PiffPsf`
200  The measured PSF model.
201  psfCellSet : `None`
202  Unused by this PsfDeterminer.
203  """
204  stars = []
205  for candidate in psfCandidateList:
206  cmi = candidate.getMaskedImage()
207  weight = computeWeight(cmi, self.config.maxSNR)
208 
209  bbox = cmi.getBBox()
210  bds = galsim.BoundsI(
211  galsim.PositionI(*bbox.getMin()),
212  galsim.PositionI(*bbox.getMax())
213  )
214  gsImage = galsim.Image(bds, scale=1.0, dtype=float)
215  gsImage.array[:] = cmi.image.array
216  gsWeight = galsim.Image(bds, scale=1.0, dtype=float)
217  gsWeight.array[:] = weight
218 
219  source = candidate.getSource()
220  image_pos = galsim.PositionD(source.getX(), source.getY())
221 
222  data = piff.StarData(
223  gsImage,
224  image_pos,
225  weight=gsWeight
226  )
227  stars.append(piff.Star(data, None))
228 
229  kernelSize = int(np.clip(
230  self.config.kernelSize,
231  self.config.kernelSizeMin,
232  self.config.kernelSizeMax
233  ))
234 
235  piffConfig = {
236  'type': "Simple",
237  'model': {
238  'type': 'PixelGrid',
239  'scale': self.config.samplingSize,
240  'size': kernelSize
241  },
242  'interp': {
243  'type': 'BasisPolynomial',
244  'order': self.config.spatialOrder
245  },
246  'outliers': {
247  'type': 'Chisq',
248  'nsigma': self.config.outlierNSigma,
249  'max_remove': self.config.outlierMaxRemove
250  }
251  }
252 
253  piffResult = piff.PSF.process(piffConfig)
254  # Run on a single CCD, and in image coords rather than sky coords.
255  wcs = {0: galsim.PixelScale(1.0)}
256  pointing = None
257 
258  logger = logging.getLogger(self.log.getName()+".Piff")
259  logger.addHandler(lsst.log.LogHandler())
260 
261  piffResult.fit(stars, wcs, pointing, logger=logger)
262  psf = PiffPsf(kernelSize, kernelSize, piffResult)
263 
264  used_image_pos = [s.image_pos for s in piffResult.stars]
265  if flagKey:
266  for candidate in psfCandidateList:
267  source = candidate.getSource()
268  posd = galsim.PositionD(source.getX(), source.getY())
269  if posd in used_image_pos:
270  source.set(flagKey, True)
271 
272  if metadata is not None:
273  metadata.set("spatialFitChi2", piffResult.chisq)
274  metadata.set("numAvailStars", len(stars))
275  metadata.set("numGoodStars", len(piffResult.stars))
276  metadata.set("avgX", np.mean([p.x for p in piffResult.stars]))
277  metadata.set("avgY", np.mean([p.y for p in piffResult.stars]))
278 
279  return psf, None
280 
281 
282 measAlg.psfDeterminerRegistry.register("piff", PiffPsfDeterminerTask)
def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None)
std::string const & getName() const noexcept
Return a filter's name.
Definition: Filter.h:78
Definition: Log.h:706
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def applyMaxSNR(imArr, weightArr, good, maxSNR)