Coverage for python/lsst/pipe/tasks/measurePsf.py: 17%

128 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-06-02 04:29 -0700

1# This file is part of pipe_tasks. 

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 

22import lsst.afw.display as afwDisplay 

23import lsst.afw.math as afwMath 

24import lsst.meas.algorithms as measAlg 

25import lsst.meas.algorithms.utils as maUtils 

26import lsst.pex.config as pexConfig 

27import lsst.pipe.base as pipeBase 

28import lsst.meas.extensions.piff.piffPsfDeterminer # noqa: F401 

29from lsst.utils.timer import timeMethod 

30 

31 

32class MeasurePsfConfig(pexConfig.Config): 

33 starSelector = measAlg.sourceSelectorRegistry.makeField( 

34 "Star selection algorithm", 

35 default="objectSize" 

36 ) 

37 makePsfCandidates = pexConfig.ConfigurableField( 

38 target=measAlg.MakePsfCandidatesTask, 

39 doc="Task to make psf candidates from selected stars.", 

40 ) 

41 psfDeterminer = measAlg.psfDeterminerRegistry.makeField( 

42 "PSF Determination algorithm", 

43 default="piff" 

44 ) 

45 reserve = pexConfig.ConfigurableField( 

46 target=measAlg.ReserveSourcesTask, 

47 doc="Reserve sources from fitting" 

48 ) 

49 

50 def validate(self): 

51 super().validate() 

52 if (self.psfDeterminer.name == "piff" 

53 and self.psfDeterminer["piff"].kernelSize > self.makePsfCandidates.kernelSize): 

54 msg = (f"PIFF kernelSize={self.psfDeterminer['piff'].kernelSize}" 

55 f" must be >= psf candidate kernelSize={self.makePsfCandidates.kernelSize}.") 

56 raise pexConfig.FieldValidationError(MeasurePsfConfig.makePsfCandidates, self, msg) 

57 

58## @addtogroup LSST_task_documentation 

59## @{ 

60## @page page_MeasurePsfTask MeasurePsfTask 

61## @ref MeasurePsfTask_ "MeasurePsfTask" 

62## @copybrief MeasurePsfTask 

63## @} 

64 

65 

66class MeasurePsfTask(pipeBase.Task): 

67 r"""! 

68@anchor MeasurePsfTask_ 

69 

70@brief Measure the PSF 

71 

72@section pipe_tasks_measurePsf_Contents Contents 

73 

74 - @ref pipe_tasks_measurePsf_Purpose 

75 - @ref pipe_tasks_measurePsf_Initialize 

76 - @ref pipe_tasks_measurePsf_IO 

77 - @ref pipe_tasks_measurePsf_Config 

78 - @ref pipe_tasks_measurePsf_Debug 

79 - @ref pipe_tasks_measurePsf_Example 

80 

81@section pipe_tasks_measurePsf_Purpose Description 

82 

83A task that selects stars from a catalog of sources and uses those to measure the PSF. 

84 

85The star selector is a subclass of 

86@ref lsst.meas.algorithms.starSelector.BaseStarSelectorTask "lsst.meas.algorithms.BaseStarSelectorTask" 

87and the PSF determiner is a sublcass of 

88@ref lsst.meas.algorithms.psfDeterminer.BasePsfDeterminerTask "lsst.meas.algorithms.BasePsfDeterminerTask" 

89 

90@warning 

91There is no establised set of configuration parameters for these algorithms, so once you start modifying 

92parameters (as we do in @ref pipe_tasks_measurePsf_Example) your code is no longer portable. 

93 

94@section pipe_tasks_measurePsf_Initialize Task initialisation 

95 

96@copydoc \_\_init\_\_ 

97 

98@section pipe_tasks_measurePsf_IO Invoking the Task 

99 

100@copydoc run 

101 

102@section pipe_tasks_measurePsf_Config Configuration parameters 

103 

104See @ref MeasurePsfConfig. 

105 

106@section pipe_tasks_measurePsf_Debug Debug variables 

107 

108The command line task interface supports a 

109flag @c -d to import @b debug.py from your @c PYTHONPATH; see 

110<a href="https://pipelines.lsst.io/modules/lsstDebug/">the lsstDebug documentation</a> 

111for more about @b debug.py files. 

112 

113<DL> 

114 <DT> @c display 

115 <DD> If True, display debugging plots 

116 <DT> displayExposure 

117 <DD> display the Exposure + spatialCells 

118 <DT> displayPsfCandidates 

119 <DD> show mosaic of candidates 

120 <DT> showBadCandidates 

121 <DD> Include bad candidates 

122 <DT> displayPsfMosaic 

123 <DD> show mosaic of reconstructed PSF(xy) 

124 <DT> displayResiduals 

125 <DD> show residuals 

126 <DT> normalizeResiduals 

127 <DD> Normalise residuals by object amplitude 

128</DL> 

129 

130Additionally you can enable any debug outputs that your chosen star selector and psf determiner support. 

131 

132@section pipe_tasks_measurePsf_Example A complete example of using MeasurePsfTask 

133 

134This code is in `measurePsfTask.py` in the examples directory, and can be run as @em e.g. 

135@code 

136examples/measurePsfTask.py --doDisplay 

137@endcode 

138@dontinclude measurePsfTask.py 

139 

140The example also runs SourceDetectionTask and SingleFrameMeasurementTask. 

141 

142Import the tasks (there are some other standard imports; read the file to see them all): 

143 

144@skip SourceDetectionTask 

145@until MeasurePsfTask 

146 

147We need to create the tasks before processing any data as the task constructor 

148can add an extra column to the schema, but first we need an almost-empty 

149Schema: 

150 

151@skipline makeMinimalSchema 

152 

153We can now call the constructors for the tasks we need to find and characterize candidate 

154PSF stars: 

155 

156@skip SourceDetectionTask.ConfigClass 

157@until measureTask 

158 

159Note that we've chosen a minimal set of measurement plugins: we need the 

160outputs of @c base_SdssCentroid, @c base_SdssShape and @c base_CircularApertureFlux as 

161inputs to the PSF measurement algorithm, while @c base_PixelFlags identifies 

162and flags bad sources (e.g. with pixels too close to the edge) so they can be 

163removed later. 

164 

165Now we can create and configure the task that we're interested in: 

166 

167@skip MeasurePsfTask 

168@until measurePsfTask 

169 

170We're now ready to process the data (we could loop over multiple exposures/catalogues using the same 

171task objects). First create the output table: 

172 

173@skipline afwTable 

174 

175And process the image: 

176 

177@skip sources = 

178@until result 

179 

180We can then unpack and use the results: 

181 

182@skip psf 

183@until cellSet 

184 

185If you specified @c --doDisplay you can see the PSF candidates: 

186 

187@skip display 

188@until RED 

189 

190<HR> 

191 

192To investigate the @ref pipe_tasks_measurePsf_Debug, put something like 

193@code{.py} 

194 import lsstDebug 

195 def DebugInfo(name): 

196 di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 

197 

198 if name == "lsst.pipe.tasks.measurePsf" : 

199 di.display = True 

200 di.displayExposure = False # display the Exposure + spatialCells 

201 di.displayPsfCandidates = True # show mosaic of candidates 

202 di.displayPsfMosaic = True # show mosaic of reconstructed PSF(xy) 

203 di.displayResiduals = True # show residuals 

204 di.showBadCandidates = True # Include bad candidates 

205 di.normalizeResiduals = False # Normalise residuals by object amplitude 

206 

207 return di 

208 

209 lsstDebug.Info = DebugInfo 

210@endcode 

211into your debug.py file and run measurePsfTask.py with the @c --debug flag. 

212 """ 

213 ConfigClass = MeasurePsfConfig 

214 _DefaultName = "measurePsf" 

215 

216 def __init__(self, schema=None, **kwargs): 

217 """!Create the detection task. Most arguments are simply passed onto pipe.base.Task. 

218 

219 @param schema An lsst::afw::table::Schema used to create the output lsst.afw.table.SourceCatalog 

220 @param **kwargs Keyword arguments passed to lsst.pipe.base.task.Task.__init__. 

221 

222 If schema is not None, 'calib_psf_candidate' and 'calib_psf_used' fields will be added to 

223 identify which stars were employed in the PSF estimation. 

224 

225 @note This task can add fields to the schema, so any code calling this task must ensure that 

226 these fields are indeed present in the input table. 

227 """ 

228 

229 pipeBase.Task.__init__(self, **kwargs) 

230 if schema is not None: 

231 self.candidateKey = schema.addField( 

232 "calib_psf_candidate", type="Flag", 

233 doc=("Flag set if the source was a candidate for PSF determination, " 

234 "as determined by the star selector.") 

235 ) 

236 self.usedKey = schema.addField( 

237 "calib_psf_used", type="Flag", 

238 doc=("Flag set if the source was actually used for PSF determination, " 

239 "as determined by the '%s' PSF determiner.") % self.config.psfDeterminer.name 

240 ) 

241 else: 

242 self.candidateKey = None 

243 self.usedKey = None 

244 self.makeSubtask("starSelector") 

245 self.makeSubtask("makePsfCandidates") 

246 self.makeSubtask("psfDeterminer", schema=schema) 

247 self.makeSubtask("reserve", columnName="calib_psf", schema=schema, 

248 doc="set if source was reserved from PSF determination") 

249 

250 @timeMethod 

251 def run(self, exposure, sources, expId=0, matches=None): 

252 """!Measure the PSF 

253 

254 @param[in,out] exposure Exposure to process; measured PSF will be added. 

255 @param[in,out] sources Measured sources on exposure; flag fields will be set marking 

256 stars chosen by the star selector and the PSF determiner if a schema 

257 was passed to the task constructor. 

258 @param[in] expId Exposure id used for generating random seed. 

259 @param[in] matches A list of lsst.afw.table.ReferenceMatch objects 

260 (@em i.e. of lsst.afw.table.Match 

261 with @c first being of type lsst.afw.table.SimpleRecord and @c second 

262 type lsst.afw.table.SourceRecord --- the reference object and detected 

263 object respectively) as returned by @em e.g. the AstrometryTask. 

264 Used by star selectors that choose to refer to an external catalog. 

265 

266 @return a pipe.base.Struct with fields: 

267 - psf: The measured PSF (also set in the input exposure) 

268 - cellSet: an lsst.afw.math.SpatialCellSet containing the PSF candidates 

269 as returned by the psf determiner. 

270 """ 

271 self.log.info("Measuring PSF") 

272 

273 import lsstDebug 

274 display = lsstDebug.Info(__name__).display 

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

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

277 displayPsfCandidates = lsstDebug.Info(__name__).displayPsfCandidates # show mosaic of candidates 

278 displayResiduals = lsstDebug.Info(__name__).displayResiduals # show residuals 

279 showBadCandidates = lsstDebug.Info(__name__).showBadCandidates # include bad candidates 

280 normalizeResiduals = lsstDebug.Info(__name__).normalizeResiduals # normalise residuals by object peak 

281 

282 # 

283 # Run star selector 

284 # 

285 stars = self.starSelector.run(sourceCat=sources, matches=matches, exposure=exposure) 

286 selectionResult = self.makePsfCandidates.run(stars.sourceCat, exposure=exposure) 

287 self.log.info("PSF star selector found %d candidates", len(selectionResult.psfCandidates)) 

288 reserveResult = self.reserve.run(selectionResult.goodStarCat, expId=expId) 

289 # Make list of psf candidates to send to the determiner (omitting those marked as reserved) 

290 psfDeterminerList = [cand for cand, use 

291 in zip(selectionResult.psfCandidates, reserveResult.use) if use] 

292 

293 if selectionResult.psfCandidates and self.candidateKey is not None: 

294 for cand in selectionResult.psfCandidates: 

295 source = cand.getSource() 

296 source.set(self.candidateKey, True) 

297 

298 self.log.info("Sending %d candidates to PSF determiner", len(psfDeterminerList)) 

299 

300 if display: 

301 frame = 1 

302 if displayExposure: 

303 disp = afwDisplay.Display(frame=frame) 

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

305 frame += 1 

306 # 

307 # Determine PSF 

308 # 

309 psf, cellSet = self.psfDeterminer.determinePsf(exposure, psfDeterminerList, self.metadata, 

310 flagKey=self.usedKey) 

311 self.log.info("PSF determination using %d/%d stars.", 

312 self.metadata.getScalar("numGoodStars"), self.metadata.getScalar("numAvailStars")) 

313 

314 exposure.setPsf(psf) 

315 

316 if display: 

317 frame = display 

318 if displayExposure: 

319 disp = afwDisplay.Display(frame=frame) 

320 showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=frame) 

321 frame += 1 

322 

323 if displayPsfCandidates: # Show a mosaic of PSF candidates 

324 plotPsfCandidates(cellSet, showBadCandidates=showBadCandidates, frame=frame) 

325 frame += 1 

326 

327 if displayResiduals: 

328 frame = plotResiduals(exposure, cellSet, 

329 showBadCandidates=showBadCandidates, 

330 normalizeResiduals=normalizeResiduals, 

331 frame=frame) 

332 if displayPsfMosaic: 

333 disp = afwDisplay.Display(frame=frame) 

334 maUtils.showPsfMosaic(exposure, psf, display=disp, showFwhm=True) 

335 disp.scale("linear", 0, 1) 

336 frame += 1 

337 

338 return pipeBase.Struct( 

339 psf=psf, 

340 cellSet=cellSet, 

341 ) 

342 

343 @property 

344 def usesMatches(self): 

345 """Return True if this task makes use of the "matches" argument to the run method""" 

346 return self.starSelector.usesMatches 

347 

348# 

349# Debug code 

350# 

351 

352 

353def showPsfSpatialCells(exposure, cellSet, showBadCandidates, frame=1): 

354 disp = afwDisplay.Display(frame=frame) 

355 maUtils.showPsfSpatialCells(exposure, cellSet, 

356 symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW, 

357 size=4, display=disp) 

358 for cell in cellSet.getCellList(): 

359 for cand in cell.begin(not showBadCandidates): # maybe include bad candidates 

360 status = cand.getStatus() 

361 disp.dot('+', *cand.getSource().getCentroid(), 

362 ctype=afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else 

363 afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else afwDisplay.RED) 

364 

365 

366def plotPsfCandidates(cellSet, showBadCandidates=False, frame=1): 

367 stamps = [] 

368 for cell in cellSet.getCellList(): 

369 for cand in cell.begin(not showBadCandidates): # maybe include bad candidates 

370 try: 

371 im = cand.getMaskedImage() 

372 

373 chi2 = cand.getChi2() 

374 if chi2 < 1e100: 

375 chi2 = "%.1f" % chi2 

376 else: 

377 chi2 = float("nan") 

378 

379 stamps.append((im, "%d%s" % 

380 (maUtils.splitId(cand.getSource().getId(), True)["objId"], chi2), 

381 cand.getStatus())) 

382 except Exception: 

383 continue 

384 

385 mos = afwDisplay.utils.Mosaic() 

386 disp = afwDisplay.Display(frame=frame) 

387 for im, label, status in stamps: 

388 im = type(im)(im, True) 

389 try: 

390 im /= afwMath.makeStatistics(im, afwMath.MAX).getValue() 

391 except NotImplementedError: 

392 pass 

393 

394 mos.append(im, label, 

395 afwDisplay.GREEN if status == afwMath.SpatialCellCandidate.GOOD else 

396 afwDisplay.YELLOW if status == afwMath.SpatialCellCandidate.UNKNOWN else afwDisplay.RED) 

397 

398 if mos.images: 

399 disp.mtv(mos.makeMosaic(), title="Psf Candidates") 

400 

401 

402def plotResiduals(exposure, cellSet, showBadCandidates=False, normalizeResiduals=True, frame=2): 

403 psf = exposure.getPsf() 

404 disp = afwDisplay.Display(frame=frame) 

405 while True: 

406 try: 

407 maUtils.showPsfCandidates(exposure, cellSet, psf=psf, display=disp, 

408 normalize=normalizeResiduals, 

409 showBadCandidates=showBadCandidates) 

410 frame += 1 

411 maUtils.showPsfCandidates(exposure, cellSet, psf=psf, display=disp, 

412 normalize=normalizeResiduals, 

413 showBadCandidates=showBadCandidates, 

414 variance=True) 

415 frame += 1 

416 except Exception: 

417 if not showBadCandidates: 

418 showBadCandidates = True 

419 continue 

420 break 

421 

422 return frame