Hide keyboard shortcuts

Hot-keys 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

1# 

2# LSST Data Management System 

3# Copyright 2008-2015 AURA/LSST. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22import os 

23import numpy as np 

24 

25import lsst.daf.base as dafBase 

26import lsst.pex.config as pexConfig 

27import lsst.geom as geom 

28import lsst.afw.geom.ellipses as afwEll 

29import lsst.afw.display as afwDisplay 

30import lsst.afw.image as afwImage 

31import lsst.afw.math as afwMath 

32import lsst.meas.algorithms as measAlg 

33import lsst.meas.algorithms.utils as maUtils 

34import lsst.meas.extensions.psfex as psfex 

35 

36 

37class PsfexPsfDeterminerConfig(measAlg.BasePsfDeterminerConfig): 

38 spatialOrder = pexConfig.Field( 38 ↛ exitline 38 didn't jump to the function exit

39 doc="specify spatial order for PSF kernel creation", 

40 dtype=int, 

41 default=2, 

42 check=lambda x: x >= 0, 

43 ) 

44 sizeCellX = pexConfig.Field( 44 ↛ exitline 44 didn't jump to the function exit

45 doc="size of cell used to determine PSF (pixels, column direction)", 

46 dtype=int, 

47 default=256, 

48 # minValue = 10, 

49 check=lambda x: x >= 10, 

50 ) 

51 sizeCellY = pexConfig.Field( 51 ↛ exitline 51 didn't jump to the function exit

52 doc="size of cell used to determine PSF (pixels, row direction)", 

53 dtype=int, 

54 default=sizeCellX.default, 

55 # minValue = 10, 

56 check=lambda x: x >= 10, 

57 ) 

58 samplingSize = pexConfig.Field( 

59 doc="Resolution of the internal PSF model relative to the pixel size; " 

60 "e.g. 0.5 is equal to 2x oversampling", 

61 dtype=float, 

62 default=0.5, 

63 ) 

64 badMaskBits = pexConfig.ListField( 

65 doc="List of mask bits which cause a source to be rejected as bad " 

66 "N.b. INTRP is used specially in PsfCandidateSet; it means \"Contaminated by neighbour\"", 

67 dtype=str, 

68 default=["INTRP", "SAT"], 

69 ) 

70 psfexBasis = pexConfig.ChoiceField( 

71 doc="BASIS value given to psfex. PIXEL_AUTO will use the requested samplingSize only if " 

72 "the FWHM < 3 pixels. Otherwise, it will use samplingSize=1. PIXEL will always use the " 

73 "requested samplingSize", 

74 dtype=str, 

75 allowed={ 

76 "PIXEL": "Always use requested samplingSize", 

77 "PIXEL_AUTO": "Only use requested samplingSize when FWHM < 3", 

78 }, 

79 default='PIXEL_AUTO', 

80 optional=False, 

81 ) 

82 tolerance = pexConfig.Field( 

83 doc="tolerance of spatial fitting", 

84 dtype=float, 

85 default=1e-2, 

86 ) 

87 lam = pexConfig.Field( 

88 doc="floor for variance is lam*data", 

89 dtype=float, 

90 default=0.05, 

91 ) 

92 reducedChi2ForPsfCandidates = pexConfig.Field( 

93 doc="for psf candidate evaluation", 

94 dtype=float, 

95 default=2.0, 

96 ) 

97 spatialReject = pexConfig.Field( 

98 doc="Rejection threshold (stdev) for candidates based on spatial fit", 

99 dtype=float, 

100 default=3.0, 

101 ) 

102 recentroid = pexConfig.Field( 

103 doc="Should PSFEX be permitted to recentroid PSF candidates?", 

104 dtype=bool, 

105 default=False, 

106 ) 

107 kernelSize = pexConfig.Field( 

108 doc=("Size of the postage stamp around each star that is extracted for fitting." 

109 "Note: this reflects the oversampling setting of the psf, set by `samplingSize`;" 

110 "e.g. `samplingSize=0.5` would require this value to be 2x what you expect."), 

111 dtype=int, 

112 default=81, 

113 ) 

114 

115 

116class PsfexPsfDeterminerTask(measAlg.BasePsfDeterminerTask): 

117 ConfigClass = PsfexPsfDeterminerConfig 

118 

119 def determinePsf(self, exposure, psfCandidateList, metadata=None, flagKey=None): 

120 """Determine a PSFEX PSF model for an exposure given a list of PSF 

121 candidates. 

122 

123 Parameters 

124 ---------- 

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

126 Exposure containing the PSF candidates. 

127 psfCandidateList: iterable of `lsst.meas.algorithms.PsfCandidate` 

128 Sequence of PSF candidates typically obtained by detecting sources 

129 and then running them through a star selector. 

130 metadata: metadata, optional 

131 A home for interesting tidbits of information. 

132 flagKey: `lsst.afw.table.Key`, optional 

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

134 

135 Returns 

136 ------- 

137 psf: `lsst.meas.extensions.psfex.PsfexPsf` 

138 The determined PSF. 

139 """ 

140 

141 import lsstDebug 

142 display = lsstDebug.Info(__name__).display 

143 displayExposure = display and \ 

144 lsstDebug.Info(__name__).displayExposure # display the Exposure + spatialCells 

145 displayPsfComponents = display and \ 

146 lsstDebug.Info(__name__).displayPsfComponents # show the basis functions 

147 showBadCandidates = display and \ 

148 lsstDebug.Info(__name__).showBadCandidates # Include bad candidates (meaningless, methinks) 

149 displayResiduals = display and \ 

150 lsstDebug.Info(__name__).displayResiduals # show residuals 

151 displayPsfMosaic = display and \ 

152 lsstDebug.Info(__name__).displayPsfMosaic # show mosaic of reconstructed PSF(x,y) 

153 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals 

154 afwDisplay.setDefaultMaskTransparency(75) 

155 # Normalise residuals by object amplitude 

156 

157 mi = exposure.getMaskedImage() 

158 

159 nCand = len(psfCandidateList) 

160 if nCand == 0: 

161 raise RuntimeError("No PSF candidates supplied.") 

162 # 

163 # How big should our PSF models be? 

164 # 

165 if display: # only needed for debug plots 

166 # construct and populate a spatial cell set 

167 bbox = mi.getBBox(afwImage.PARENT) 

168 psfCellSet = afwMath.SpatialCellSet(bbox, self.config.sizeCellX, self.config.sizeCellY) 

169 else: 

170 psfCellSet = None 

171 

172 sizes = np.empty(nCand) 

173 for i, psfCandidate in enumerate(psfCandidateList): 

174 try: 

175 if psfCellSet: 

176 psfCellSet.insertCandidate(psfCandidate) 

177 except Exception as e: 

178 self.log.error("Skipping PSF candidate %d of %d: %s", i, len(psfCandidateList), e) 

179 continue 

180 

181 source = psfCandidate.getSource() 

182 quad = afwEll.Quadrupole(source.getIxx(), source.getIyy(), source.getIxy()) 

183 rmsSize = quad.getTraceRadius() 

184 sizes[i] = rmsSize 

185 

186 if self.config.kernelSize >= 15: 

187 self.log.warn("NOT scaling kernelSize by stellar quadrupole moment, but using absolute value") 

188 actualKernelSize = self.config.kernelSize 

189 else: 

190 actualKernelSize = 2 * int(self.config.kernelSize * np.sqrt(np.median(sizes)) + 0.5) + 1 

191 if actualKernelSize < self.config.kernelSizeMin: 

192 actualKernelSize = self.config.kernelSizeMin 

193 if actualKernelSize > self.config.kernelSizeMax: 

194 actualKernelSize = self.config.kernelSizeMax 

195 if display: 

196 rms = np.median(sizes) 

197 msg = "Median PSF RMS size=%.2f pixels (\"FWHM\"=%.2f)" % (rms, 2*np.sqrt(2*np.log(2))*rms) 

198 self.log.debug(msg) 

199 

200 # If we manually set the resolution then we need the size in pixel 

201 # units 

202 pixKernelSize = actualKernelSize 

203 if self.config.samplingSize > 0: 

204 pixKernelSize = int(actualKernelSize*self.config.samplingSize) 

205 if pixKernelSize % 2 == 0: 

206 pixKernelSize += 1 

207 self.log.trace("Psfex Kernel size=%.2f, Image Kernel Size=%.2f", actualKernelSize, pixKernelSize) 

208 psfCandidateList[0].setHeight(pixKernelSize) 

209 psfCandidateList[0].setWidth(pixKernelSize) 

210 

211 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- BEGIN PSFEX 

212 # 

213 # Insert the good candidates into the set 

214 # 

215 defaultsFile = os.path.join(os.environ["MEAS_EXTENSIONS_PSFEX_DIR"], "config", "default-lsst.psfex") 

216 args_md = dafBase.PropertySet() 

217 args_md.set("BASIS_TYPE", str(self.config.psfexBasis)) 

218 args_md.set("PSFVAR_DEGREES", str(self.config.spatialOrder)) 

219 args_md.set("PSF_SIZE", str(actualKernelSize)) 

220 args_md.set("PSF_SAMPLING", str(self.config.samplingSize)) 

221 prefs = psfex.Prefs(defaultsFile, args_md) 

222 prefs.setCommandLine([]) 

223 prefs.addCatalog("psfexPsfDeterminer") 

224 

225 prefs.use() 

226 principalComponentExclusionFlag = bool(bool(psfex.Context.REMOVEHIDDEN) 

227 if False else psfex.Context.KEEPHIDDEN) 

228 context = psfex.Context(prefs.getContextName(), prefs.getContextGroup(), 

229 prefs.getGroupDeg(), principalComponentExclusionFlag) 

230 psfSet = psfex.Set(context) 

231 psfSet.setVigSize(pixKernelSize, pixKernelSize) 

232 psfSet.setFwhm(2*np.sqrt(2*np.log(2))*np.median(sizes)) 

233 psfSet.setRecentroid(self.config.recentroid) 

234 

235 catindex, ext = 0, 0 

236 backnoise2 = afwMath.makeStatistics(mi.getImage(), afwMath.VARIANCECLIP).getValue() 

237 ccd = exposure.getDetector() 

238 if ccd: 

239 gain = np.mean(np.array([a.getGain() for a in ccd])) 

240 else: 

241 gain = 1.0 

242 self.log.warn("Setting gain to %g" % (gain,)) 

243 

244 contextvalp = [] 

245 for i, key in enumerate(context.getName()): 

246 if key[0] == ':': 

247 try: 

248 contextvalp.append(exposure.getMetadata().getScalar(key[1:])) 

249 except KeyError as e: 

250 raise RuntimeError("%s parameter not found in the header of %s" % 

251 (key[1:], prefs.getContextName())) from e 

252 else: 

253 try: 

254 contextvalp.append(np.array([psfCandidateList[_].getSource().get(key) 

255 for _ in range(nCand)])) 

256 except KeyError as e: 

257 raise RuntimeError("%s parameter not found" % (key,)) from e 

258 psfSet.setContextname(i, key) 

259 

260 if display: 

261 frame = 0 

262 if displayExposure: 

263 disp = afwDisplay.Display(frame=frame) 

264 disp.mtv(exposure, title="psf determination") 

265 

266 badBits = mi.getMask().getPlaneBitMask(self.config.badMaskBits) 

267 fluxName = prefs.getPhotfluxRkey() 

268 fluxFlagName = "base_" + fluxName + "_flag" 

269 

270 xpos, ypos = [], [] 

271 for i, psfCandidate in enumerate(psfCandidateList): 

272 source = psfCandidate.getSource() 

273 

274 # skip sources with bad centroids 

275 xc, yc = source.getX(), source.getY() 

276 if not np.isfinite(xc) or not np.isfinite(yc): 

277 continue 

278 # skip flagged sources 

279 if fluxFlagName in source.schema and source.get(fluxFlagName): 

280 continue 

281 # skip nonfinite and negative sources 

282 flux = source.get(fluxName) 

283 if flux < 0 or not np.isfinite(flux): 

284 continue 

285 

286 pstamp = psfCandidate.getMaskedImage().clone() 

287 

288 # From this point, we're configuring the "sample" (PSFEx's version 

289 # of a PSF candidate). 

290 # Having created the sample, we must proceed to configure it, and 

291 # then fini (finalize), or it will be malformed. 

292 try: 

293 sample = psfSet.newSample() 

294 sample.setCatindex(catindex) 

295 sample.setExtindex(ext) 

296 sample.setObjindex(i) 

297 

298 imArray = pstamp.getImage().getArray() 

299 imArray[np.where(np.bitwise_and(pstamp.getMask().getArray(), badBits))] = \ 

300 -2*psfex.BIG 

301 sample.setVig(imArray) 

302 

303 sample.setNorm(flux) 

304 sample.setBacknoise2(backnoise2) 

305 sample.setGain(gain) 

306 sample.setX(xc) 

307 sample.setY(yc) 

308 sample.setFluxrad(sizes[i]) 

309 

310 for j in range(psfSet.getNcontext()): 

311 sample.setContext(j, float(contextvalp[j][i])) 

312 except Exception as e: 

313 self.log.error("Exception when processing sample at (%f,%f): %s", xc, yc, e) 

314 continue 

315 else: 

316 psfSet.finiSample(sample) 

317 

318 xpos.append(xc) # for QA 

319 ypos.append(yc) 

320 

321 if displayExposure: 

322 with disp.Buffering(): 

323 disp.dot("o", xc, yc, ctype=afwDisplay.CYAN, size=4) 

324 

325 if psfSet.getNsample() == 0: 

326 raise RuntimeError("No good PSF candidates to pass to PSFEx") 

327 

328 # ---- Update min and max and then the scaling 

329 for i in range(psfSet.getNcontext()): 

330 cmin = contextvalp[i].min() 

331 cmax = contextvalp[i].max() 

332 psfSet.setContextScale(i, cmax - cmin) 

333 psfSet.setContextOffset(i, (cmin + cmax)/2.0) 

334 

335 # Don't waste memory! 

336 psfSet.trimMemory() 

337 

338 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- END PSFEX 

339 # 

340 # Do a PSFEX decomposition of those PSF candidates 

341 # 

342 fields = [] 

343 field = psfex.Field("Unknown") 

344 field.addExt(exposure.getWcs(), exposure.getWidth(), exposure.getHeight(), psfSet.getNsample()) 

345 field.finalize() 

346 

347 fields.append(field) 

348 

349 sets = [] 

350 sets.append(psfSet) 

351 

352 psfex.makeit(fields, sets) 

353 psfs = field.getPsfs() 

354 

355 # Flag which objects were actually used in psfex by 

356 good_indices = [] 

357 for i in range(sets[0].getNsample()): 

358 index = sets[0].getSample(i).getObjindex() 

359 if index > -1: 

360 good_indices.append(index) 

361 

362 if flagKey is not None: 

363 for i, psfCandidate in enumerate(psfCandidateList): 

364 source = psfCandidate.getSource() 

365 if i in good_indices: 

366 source.set(flagKey, True) 

367 

368 xpos = np.array(xpos) 

369 ypos = np.array(ypos) 

370 numGoodStars = len(good_indices) 

371 avgX, avgY = np.mean(xpos), np.mean(ypos) 

372 

373 psf = psfex.PsfexPsf(psfs[0], geom.Point2D(avgX, avgY)) 

374 

375 # 

376 # Display code for debugging 

377 # 

378 if display: 

379 assert psfCellSet is not None 

380 

381 if displayExposure: 

382 maUtils.showPsfSpatialCells(exposure, psfCellSet, showChi2=True, 

383 symb="o", ctype=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED, 

384 size=8, display=disp) 

385 if displayResiduals: 

386 disp4 = afwDisplay.Display(frame=4) 

387 maUtils.showPsfCandidates(exposure, psfCellSet, psf=psf, display=disp4, 

388 normalize=normalizeResiduals, 

389 showBadCandidates=showBadCandidates) 

390 if displayPsfComponents: 

391 disp6 = afwDisplay.Display(frame=6) 

392 maUtils.showPsf(psf, display=disp6) 

393 if displayPsfMosaic: 

394 disp7 = afwDisplay.Display(frame=7) 

395 maUtils.showPsfMosaic(exposure, psf, display=disp7, showFwhm=True) 

396 disp.scale('linear', 0, 1) 

397 # 

398 # Generate some QA information 

399 # 

400 # Count PSF stars 

401 # 

402 if metadata is not None: 

403 metadata.set("spatialFitChi2", np.nan) 

404 metadata.set("numAvailStars", nCand) 

405 metadata.set("numGoodStars", numGoodStars) 

406 metadata.set("avgX", avgX) 

407 metadata.set("avgY", avgY) 

408 

409 return psf, psfCellSet 

410 

411 

412measAlg.psfDeterminerRegistry.register("psfex", PsfexPsfDeterminerTask)