Coverage for python/lsst/meas/extensions/piff/piffPsfDeterminer.py: 26%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

87 statements  

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 

24import numpy as np 

25import piff 

26import galsim 

27 

28import lsst.pex.config as pexConfig 

29import lsst.meas.algorithms as measAlg 

30from lsst.meas.algorithms.psfDeterminer import BasePsfDeterminerTask 

31from .piffPsf import PiffPsf 

32 

33 

34class 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 zeroWeightMaskBits = pexConfig.ListField( 

63 doc="List of mask bits for which to set pixel weights to zero.", 

64 dtype=str, 

65 default=['BAD', 'CR', 'INTRP', 'SAT', 'SUSPECT', 'NO_DATA'] 

66 ) 

67 

68 def setDefaults(self): 

69 self.kernelSize = 21 

70 self.kernelSizeMin = 11 

71 self.kernelSizeMax = 35 

72 

73 

74def computeWeight(maskedImage, maxSNR, zeroWeightMaskBits): 

75 """Derive a weight map without Poisson variance component due to signal. 

76 

77 Parameters 

78 ---------- 

79 maskedImage : `afw.image.MaskedImage` 

80 PSF candidate postage stamp 

81 maxSNR : `float` 

82 Maximum SNR applying variance floor. 

83 zeroWeightMaskBits : `List[str]` 

84 List of mask bits for which to set pixel weights to zero. 

85 

86 Returns 

87 ------- 

88 weightArr : `ndarry` 

89 Array to use for weight. 

90 """ 

91 imArr = maskedImage.image.array 

92 varArr = maskedImage.variance.array 

93 bitmask = maskedImage.mask.getPlaneBitMask(zeroWeightMaskBits) 

94 good = ( 

95 (varArr != 0) 

96 & (np.isfinite(varArr)) 

97 & (np.isfinite(imArr)) 

98 & ((maskedImage.mask.array & bitmask) == 0) 

99 ) 

100 

101 # Fit a straight line to variance vs (sky-subtracted) signal. 

102 # The evaluate that line at zero signal to get an estimate of the 

103 # signal-free variance. 

104 fit = np.polyfit(imArr[good], varArr[good], deg=1) 

105 # fit is [1/gain, sky_var] 

106 weightArr = np.zeros_like(imArr, dtype=float) 

107 weightArr[good] = 1./fit[1] 

108 

109 applyMaxSNR(imArr, weightArr, good, maxSNR) 

110 return weightArr 

111 

112 

113def applyMaxSNR(imArr, weightArr, good, maxSNR): 

114 """Rescale weight of bright stars to cap the computed SNR. 

115 

116 Parameters 

117 ---------- 

118 imArr : `ndarray` 

119 Signal (image) array of stamp. 

120 weightArr : `ndarray` 

121 Weight map array. May be rescaled in place. 

122 good : `ndarray` 

123 Index array of pixels to use when computing SNR. 

124 maxSNR : `float` 

125 Threshold for adjusting variance plane implementing maximum SNR. 

126 """ 

127 # We define the SNR value following Piff. Here's the comment from that 

128 # code base explaining the calculation. 

129 # 

130 # The S/N value that we use will be the weighted total flux where the 

131 # weight function is the star's profile itself. This is the maximum S/N 

132 # value that any flux measurement can possibly produce, which will be 

133 # closer to an in-practice S/N than using all the pixels equally. 

134 # 

135 # F = Sum_i w_i I_i^2 

136 # var(F) = Sum_i w_i^2 I_i^2 var(I_i) 

137 # = Sum_i w_i I_i^2 <--- Assumes var(I_i) = 1/w_i 

138 # 

139 # S/N = F / sqrt(var(F)) 

140 # 

141 # Note that if the image is pure noise, this will produce a "signal" of 

142 # 

143 # F_noise = Sum_i w_i 1/w_i = Npix 

144 # 

145 # So for a more accurate estimate of the S/N of the actual star itself, one 

146 # should subtract off Npix from the measured F. 

147 # 

148 # The final formula then is: 

149 # 

150 # F = Sum_i w_i I_i^2 

151 # S/N = (F-Npix) / sqrt(F) 

152 F = np.sum(weightArr[good]*imArr[good]**2, dtype=float) 

153 Npix = np.sum(good) 

154 SNR = 0.0 if F < Npix else (F-Npix)/np.sqrt(F) 

155 # rescale weight of bright stars. Essentially makes an error floor. 

156 if SNR > maxSNR: 

157 factor = (maxSNR / SNR)**2 

158 weightArr[good] *= factor 

159 

160 

161def _computeWeightAlternative(maskedImage, maxSNR): 

162 """Alternative algorithm for creating weight map. 

163 

164 This version is equivalent to that used by Piff internally. The weight map 

165 it produces tends to leave a residual when removing the Poisson component 

166 due to the signal. We leave it here as a reference, but without intending 

167 that it be used. 

168 """ 

169 imArr = maskedImage.image.array 

170 varArr = maskedImage.variance.array 

171 good = (varArr != 0) & np.isfinite(varArr) & np.isfinite(imArr) 

172 

173 fit = np.polyfit(imArr[good], varArr[good], deg=1) 

174 # fit is [1/gain, sky_var] 

175 gain = 1./fit[0] 

176 varArr[good] -= imArr[good] / gain 

177 weightArr = np.zeros_like(imArr, dtype=float) 

178 weightArr[good] = 1./varArr[good] 

179 

180 applyMaxSNR(imArr, weightArr, good, maxSNR) 

181 return weightArr 

182 

183 

184class PiffPsfDeterminerTask(BasePsfDeterminerTask): 

185 """A measurePsfTask PSF estimator using Piff as the implementation. 

186 """ 

187 ConfigClass = PiffPsfDeterminerConfig 

188 _DefaultName = "psfDeterminer.Piff" 

189 

190 def determinePsf( 

191 self, exposure, psfCandidateList, metadata=None, flagKey=None 

192 ): 

193 """Determine a Piff PSF model for an exposure given a list of PSF 

194 candidates. 

195 

196 Parameters 

197 ---------- 

198 exposure : `lsst.afw.image.Exposure` 

199 Exposure containing the PSF candidates. 

200 psfCandidateList : `list` of `lsst.meas.algorithms.PsfCandidate` 

201 A sequence of PSF candidates typically obtained by detecting sources 

202 and then running them through a star selector. 

203 metadata : `lsst.daf.base import PropertyList` or `None`, optional 

204 A home for interesting tidbits of information. 

205 flagKey : `str` or `None`, optional 

206 Schema key used to mark sources actually used in PSF determination. 

207 

208 Returns 

209 ------- 

210 psf : `lsst.meas.extensions.piff.PiffPsf` 

211 The measured PSF model. 

212 psfCellSet : `None` 

213 Unused by this PsfDeterminer. 

214 """ 

215 stars = [] 

216 for candidate in psfCandidateList: 

217 cmi = candidate.getMaskedImage() 

218 weight = computeWeight( 

219 cmi, 

220 self.config.maxSNR, 

221 self.config.zeroWeightMaskBits 

222 ) 

223 

224 bbox = cmi.getBBox() 

225 bds = galsim.BoundsI( 

226 galsim.PositionI(*bbox.getMin()), 

227 galsim.PositionI(*bbox.getMax()) 

228 ) 

229 gsImage = galsim.Image(bds, scale=1.0, dtype=float) 

230 gsImage.array[:] = cmi.image.array 

231 gsWeight = galsim.Image(bds, scale=1.0, dtype=float) 

232 gsWeight.array[:] = weight 

233 

234 source = candidate.getSource() 

235 image_pos = galsim.PositionD(source.getX(), source.getY()) 

236 

237 data = piff.StarData( 

238 gsImage, 

239 image_pos, 

240 weight=gsWeight 

241 ) 

242 stars.append(piff.Star(data, None)) 

243 

244 kernelSize = int(np.clip( 

245 self.config.kernelSize, 

246 self.config.kernelSizeMin, 

247 self.config.kernelSizeMax 

248 )) 

249 

250 piffConfig = { 

251 'type': "Simple", 

252 'model': { 

253 'type': 'PixelGrid', 

254 'scale': self.config.samplingSize, 

255 'size': kernelSize 

256 }, 

257 'interp': { 

258 'type': 'BasisPolynomial', 

259 'order': self.config.spatialOrder 

260 }, 

261 'outliers': { 

262 'type': 'Chisq', 

263 'nsigma': self.config.outlierNSigma, 

264 'max_remove': self.config.outlierMaxRemove 

265 } 

266 } 

267 

268 piffResult = piff.PSF.process(piffConfig) 

269 # Run on a single CCD, and in image coords rather than sky coords. 

270 wcs = {0: galsim.PixelScale(1.0)} 

271 pointing = None 

272 

273 piffResult.fit(stars, wcs, pointing, logger=self.log) 

274 psf = PiffPsf(kernelSize, kernelSize, piffResult) 

275 

276 used_image_pos = [s.image_pos for s in piffResult.stars] 

277 if flagKey: 

278 for candidate in psfCandidateList: 

279 source = candidate.getSource() 

280 posd = galsim.PositionD(source.getX(), source.getY()) 

281 if posd in used_image_pos: 

282 source.set(flagKey, True) 

283 

284 if metadata is not None: 

285 metadata["spatialFitChi2"] = piffResult.chisq 

286 metadata["numAvailStars"] = len(stars) 

287 metadata["numGoodStars"] = len(piffResult.stars) 

288 metadata["avgX"] = np.mean([p.x for p in piffResult.stars]) 

289 metadata["avgY"] = np.mean([p.y for p in piffResult.stars]) 

290 

291 return psf, None 

292 

293 

294measAlg.psfDeterminerRegistry.register("piff", PiffPsfDeterminerTask)