Coverage for python/lsst/cp/verify/verifyStats.py: 33%

125 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-01 21:44 +0000

1# This file is part of cp_verify. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://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 <http://www.gnu.org/licenses/>. 

21import math 

22 

23import lsst.afw.geom as afwGeom 

24import lsst.afw.math as afwMath 

25import lsst.pex.config as pexConfig 

26import lsst.pipe.base as pipeBase 

27import lsst.pipe.base.connectionTypes as cT 

28import lsst.meas.algorithms as measAlg 

29 

30from lsst.cp.pipe.cpCombine import vignetteExposure 

31from lsst.pipe.tasks.repair import RepairTask 

32from .utils import mergeStatDict 

33 

34__all__ = ['CpVerifyStatsConfig', 'CpVerifyStatsTask'] 

35 

36 

37class CpVerifyStatsConnections(pipeBase.PipelineTaskConnections, 

38 dimensions={"instrument", "exposure", "detector"}, 

39 defaultTemplates={}): 

40 inputExp = cT.Input( 

41 name="postISRCCD", 

42 doc="Input exposure to calculate statistics for.", 

43 storageClass="Exposure", 

44 dimensions=["instrument", "exposure", "detector"], 

45 ) 

46 camera = cT.PrerequisiteInput( 

47 name="camera", 

48 storageClass="Camera", 

49 doc="Input camera.", 

50 dimensions=["instrument", ], 

51 isCalibration=True, 

52 ) 

53 

54 outputStats = cT.Output( 

55 name="detectorStats", 

56 doc="Output statistics from cp_verify.", 

57 storageClass="StructuredDataDict", 

58 dimensions=["instrument", "exposure", "detector"], 

59 ) 

60 

61 

62class CpVerifyStatsConfig(pipeBase.PipelineTaskConfig, 

63 pipelineConnections=CpVerifyStatsConnections): 

64 """Configuration parameters for CpVerifyStatsTask. 

65 """ 

66 maskNameList = pexConfig.ListField( 

67 dtype=str, 

68 doc="Mask list to exclude from statistics calculations.", 

69 default=['DETECTED', 'BAD', 'NO_DATA'], 

70 ) 

71 doVignette = pexConfig.Field( 

72 dtype=bool, 

73 doc="Mask vignetted regions?", 

74 default=False, 

75 ) 

76 doNormalize = pexConfig.Field( 

77 dtype=bool, 

78 doc="Normalize by exposure time?", 

79 default=False, 

80 ) 

81 

82 # Cosmic ray handling options. 

83 doCR = pexConfig.Field( 

84 dtype=bool, 

85 doc="Run CR rejection?", 

86 default=False, 

87 ) 

88 repair = pexConfig.ConfigurableField( 

89 target=RepairTask, 

90 doc="Repair task to use.", 

91 ) 

92 psfFwhm = pexConfig.Field( 

93 dtype=float, 

94 default=3.0, 

95 doc="Repair PSF FWHM (pixels).", 

96 ) 

97 psfSize = pexConfig.Field( 

98 dtype=int, 

99 default=21, 

100 doc="Repair PSF bounding-box size (pixels).", 

101 ) 

102 crGrow = pexConfig.Field( 

103 dtype=int, 

104 default=2, 

105 doc="Grow radius for CR (pixels).", 

106 ) 

107 

108 # Statistics options. 

109 useReadNoise = pexConfig.Field( 

110 dtype=bool, 

111 doc="Compare sigma against read noise?", 

112 default=True, 

113 ) 

114 numSigmaClip = pexConfig.Field( 

115 dtype=float, 

116 doc="Rejection threshold (sigma) for statistics clipping.", 

117 default=5.0, 

118 ) 

119 clipMaxIter = pexConfig.Field( 

120 dtype=int, 

121 doc="Max number of clipping iterations to apply.", 

122 default=3, 

123 ) 

124 

125 # Keywords and statistics to measure from different sources. 

126 imageStatKeywords = pexConfig.DictField( 

127 keytype=str, 

128 itemtype=str, 

129 doc="Image statistics to run on amplifier segments.", 

130 default={}, 

131 ) 

132 unmaskedImageStatKeywords = pexConfig.DictField( 

133 keytype=str, 

134 itemtype=str, 

135 doc="Image statistics to run on amplifier segments, ignoring masks.", 

136 default={}, 

137 ) 

138 crImageStatKeywords = pexConfig.DictField( 

139 keytype=str, 

140 itemtype=str, 

141 doc="Image statistics to run on CR cleaned amplifier segments.", 

142 default={}, 

143 ) 

144 normImageStatKeywords = pexConfig.DictField( 

145 keytype=str, 

146 itemtype=str, 

147 doc="Image statistics to run on expTime normalized amplifier segments.", 

148 default={}, 

149 ) 

150 catalogStatKeywords = pexConfig.DictField( 

151 keytype=str, 

152 itemtype=str, 

153 doc="Statistics to measure from source catalogs of objects in the exposure.", 

154 default={}, 

155 ) 

156 detectorStatKeywords = pexConfig.DictField( 

157 keytype=str, 

158 itemtype=str, 

159 doc="Statistics to create for the full detector from the per-amplifier measurements.", 

160 default={}, 

161 ) 

162 

163 

164class CpVerifyStatsTask(pipeBase.PipelineTask, pipeBase.CmdLineTask): 

165 """Main statistic measurement and validation class. 

166 

167 This operates on a single (exposure, detector) pair, and is 

168 designed to be subclassed so specific calibrations can apply their 

169 own validation methods. 

170 """ 

171 ConfigClass = CpVerifyStatsConfig 

172 _DefaultName = 'cpVerifyStats' 

173 

174 def __init__(self, **kwargs): 

175 super().__init__(**kwargs) 

176 self.makeSubtask("repair") 

177 

178 def run(self, inputExp, camera): 

179 """Calculate quality statistics and verify they meet the requirements 

180 for a calibration. 

181 

182 Parameters 

183 ---------- 

184 inputExp : `lsst.afw.image.Exposure` 

185 The ISR processed exposure to be measured. 

186 camera : `lsst.afw.cameraGeom.Camera` 

187 The camera geometry for ``inputExp``. 

188 

189 Returns 

190 ------- 

191 result : `lsst.pipe.base.Struct` 

192 Result struct with components: 

193 - ``outputStats`` : `dict` 

194 The output measured statistics. 

195 

196 Notes 

197 ----- 

198 The outputStats should have a yaml representation of the form 

199 

200 AMP: 

201 Amp1: 

202 STAT: value 

203 STAT2: value2 

204 Amp2: 

205 Amp3: 

206 DET: 

207 STAT: value 

208 STAT2: value 

209 CATALOG: 

210 STAT: value 

211 STAT2: value 

212 VERIFY: 

213 DET: 

214 TEST: boolean 

215 CATALOG: 

216 TEST: boolean 

217 AMP: 

218 Amp1: 

219 TEST: boolean 

220 TEST2: boolean 

221 Amp2: 

222 Amp3: 

223 SUCCESS: boolean 

224 

225 """ 

226 outputStats = {} 

227 

228 if self.config.doVignette: 

229 vignetteExposure(inputExp, doUpdateMask=True, maskPlane='NO_DATA', 

230 doSetValue=False, log=self.log) 

231 

232 mask = inputExp.getMask() 

233 maskVal = mask.getPlaneBitMask(self.config.maskNameList) 

234 statControl = afwMath.StatisticsControl(self.config.numSigmaClip, 

235 self.config.clipMaxIter, 

236 maskVal) 

237 

238 # This is wrapped below to check for config lengths, as we can 

239 # make a number of different image stats. 

240 outputStats['AMP'] = self.imageStatistics(inputExp, statControl) 

241 if len(self.config.catalogStatKeywords): 

242 outputStats['CATALOG'] = self.catalogStatistics(inputExp, statControl) 

243 else: 

244 outputStats['CATALOG'] = {} 

245 if len(self.config.detectorStatKeywords): 

246 outputStats['DET'] = self.detectorStatistics(outputStats, statControl) 

247 else: 

248 outputStats['DET'] = {} 

249 

250 outputStats['VERIFY'], outputStats['SUCCESS'] = self.verify(inputExp, outputStats) 

251 

252 return pipeBase.Struct( 

253 outputStats=outputStats, 

254 ) 

255 

256 @staticmethod 

257 def _emptyAmpDict(exposure): 

258 """Construct empty dictionary indexed by amplifier names. 

259 

260 Parameters 

261 ---------- 

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

263 Exposure to extract detector from. 

264 

265 Returns 

266 ------- 

267 outputStatistics : `dict` [`str`, `dict`] 

268 A skeleton statistics dictionary. 

269 

270 Raises 

271 ------ 

272 RuntimeError : 

273 Raised if no detector can be found. 

274 """ 

275 outputStatistics = {} 

276 detector = exposure.getDetector() 

277 if detector is None: 

278 raise RuntimeError("No detector found in exposure!") 

279 

280 for amp in detector.getAmplifiers(): 

281 outputStatistics[amp.getName()] = {} 

282 

283 return outputStatistics 

284 

285 # Image measurement methods. 

286 def imageStatistics(self, exposure, statControl): 

287 """Measure image statistics for a number of simple image 

288 modifications. 

289 

290 Parameters 

291 ---------- 

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

293 Exposure containing the ISR processed data to measure. 

294 statControl : `lsst.afw.math.StatisticsControl` 

295 Statistics control object with parameters defined by 

296 the config. 

297 

298 Returns 

299 ------- 

300 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]] 

301 A dictionary indexed by the amplifier name, containing 

302 dictionaries of the statistics measured and their values. 

303 

304 """ 

305 outputStatistics = self._emptyAmpDict(exposure) 

306 

307 if len(self.config.imageStatKeywords): 

308 outputStatistics = mergeStatDict(outputStatistics, 

309 self.amplifierStats(exposure, 

310 self.config.imageStatKeywords, 

311 statControl)) 

312 

313 if len(self.config.unmaskedImageStatKeywords): 

314 outputStatistics = mergeStatDict(outputStatistics, self.unmaskedImageStats(exposure)) 

315 

316 if len(self.config.normImageStatKeywords): 

317 outputStatistics = mergeStatDict(outputStatistics, 

318 self.normalizedImageStats(exposure, statControl)) 

319 

320 if len(self.config.crImageStatKeywords): 

321 outputStatistics = mergeStatDict(outputStatistics, 

322 self.crImageStats(exposure, statControl)) 

323 

324 return outputStatistics 

325 

326 @staticmethod 

327 def _configHelper(keywordDict): 

328 """Helper to convert keyword dictionary to stat value. 

329 

330 Convert the string names in the keywordDict to the afwMath values. 

331 The statisticToRun is then the bitwise-or of that set. 

332 

333 Parameters 

334 ---------- 

335 keywordDict : `dict` [`str`, `str`] 

336 A dictionary of keys to use in the output results, with 

337 values the string name associated with the 

338 `lsst.afw.math.statistics.Property` to measure. 

339 

340 Returns 

341 ------- 

342 statisticToRun : `int` 

343 The merged `lsst.afw.math` statistics property. 

344 statAccessor : `dict` [`str`, `int`] 

345 Dictionary containing statistics property indexed by name. 

346 """ 

347 statisticToRun = 0 

348 statAccessor = {} 

349 for k, v in keywordDict.items(): 

350 statValue = afwMath.stringToStatisticsProperty(v) 

351 statisticToRun |= statValue 

352 statAccessor[k] = statValue 

353 

354 return statisticToRun, statAccessor 

355 

356 def amplifierStats(self, exposure, keywordDict, statControl): 

357 """Measure amplifier level statistics from the exposure. 

358 

359 Parameters 

360 ---------- 

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

362 The exposure to measure. 

363 keywordDict : `dict` [`str`, `str`] 

364 A dictionary of keys to use in the output results, with 

365 values the string name associated with the 

366 `lsst.afw.math.statistics.Property` to measure. 

367 statControl : `lsst.afw.math.StatisticsControl` 

368 Statistics control object with parameters defined by 

369 the config. 

370 

371 Returns 

372 ------- 

373 ampStats : `dict` [`str`, `dict` [`str`, scalar]] 

374 A dictionary indexed by the amplifier name, containing 

375 dictionaries of the statistics measured and their values. 

376 """ 

377 ampStats = {} 

378 

379 statisticToRun, statAccessor = self._configHelper(keywordDict) 

380 

381 # Measure stats on all amplifiers. 

382 for ampIdx, amp in enumerate(exposure.getDetector()): 

383 ampName = amp.getName() 

384 theseStats = {} 

385 ampExp = exposure.Factory(exposure, amp.getBBox()) 

386 stats = afwMath.makeStatistics(ampExp.getMaskedImage(), statisticToRun, statControl) 

387 

388 for k, v in statAccessor.items(): 

389 theseStats[k] = stats.getValue(v) 

390 ampStats[ampName] = theseStats 

391 

392 return ampStats 

393 

394 def unmaskedImageStats(self, exposure): 

395 """Measure amplifier level statistics on the exposure, including all 

396 pixels in the exposure, regardless of any mask planes set. 

397 

398 Parameters 

399 ---------- 

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

401 The exposure to measure. 

402 

403 Returns 

404 ------- 

405 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]] 

406 A dictionary indexed by the amplifier name, containing 

407 dictionaries of the statistics measured and their values. 

408 """ 

409 noMaskStatsControl = afwMath.StatisticsControl(self.config.numSigmaClip, 

410 self.config.clipMaxIter, 

411 0x0) 

412 return self.amplifierStats(exposure, self.config.unmaskedImageStatKeywords, noMaskStatsControl) 

413 

414 def normalizedImageStats(self, exposure, statControl): 

415 """Measure amplifier level statistics on the exposure after dividing 

416 by the exposure time. 

417 

418 Parameters 

419 ---------- 

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

421 The exposure to measure. 

422 statControl : `lsst.afw.math.StatisticsControl` 

423 Statistics control object with parameters defined by 

424 the config. 

425 

426 Returns 

427 ------- 

428 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]] 

429 A dictionary indexed by the amplifier name, containing 

430 dictionaries of the statistics measured and their values. 

431 

432 Raises 

433 ------ 

434 RuntimeError : 

435 Raised if the exposure time cannot be used for normalization. 

436 """ 

437 scaledExposure = exposure.clone() 

438 exposureTime = scaledExposure.getInfo().getVisitInfo().getExposureTime() 

439 if exposureTime <= 0: 

440 raise RuntimeError(f"Invalid exposureTime {exposureTime}.") 

441 mi = scaledExposure.getMaskedImage() 

442 mi /= exposureTime 

443 

444 return self.amplifierStats(scaledExposure, self.config.normImageStatKeywords, statControl) 

445 

446 def crImageStats(self, exposure, statControl): 

447 """Measure amplifier level statistics on the exposure, 

448 after running cosmic ray rejection. 

449 

450 Parameters 

451 ---------- 

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

453 The exposure to measure. 

454 statControl : `lsst.afw.math.StatisticsControl` 

455 Statistics control object with parameters defined by 

456 the config. 

457 

458 Returns 

459 ------- 

460 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]] 

461 A dictionary indexed by the amplifier name, containing 

462 dictionaries of the statistics measured and their values. 

463 

464 """ 

465 crRejectedExp = exposure.clone() 

466 psf = measAlg.SingleGaussianPsf(self.config.psfSize, 

467 self.config.psfSize, 

468 self.config.psfFwhm/(2*math.sqrt(2*math.log(2)))) 

469 crRejectedExp.setPsf(psf) 

470 self.repair.run(crRejectedExp, keepCRs=False) 

471 if self.config.crGrow > 0: 

472 crMask = crRejectedExp.getMaskedImage().getMask().getPlaneBitMask("CR") 

473 spans = afwGeom.SpanSet.fromMask(crRejectedExp.mask, crMask) 

474 spans = spans.dilated(self.config.crGrow) 

475 spans = spans.clippedTo(crRejectedExp.getBBox()) 

476 spans.setMask(crRejectedExp.mask, crMask) 

477 

478 return self.amplifierStats(crRejectedExp, self.config.crImageStatKeywords, statControl) 

479 

480 # Methods that need to be implemented by the calibration-level subclasses. 

481 def catalogStatistics(self, exposure, statControl): 

482 """Calculate statistics from a catalog. 

483 

484 Parameters 

485 ---------- 

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

487 The exposure to measure. 

488 statControl : `lsst.afw.math.StatisticsControl` 

489 Statistics control object with parameters defined by 

490 the config. 

491 

492 Returns 

493 ------- 

494 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]] 

495 A dictionary indexed by the amplifier name, containing 

496 dictionaries of the statistics measured and their values. 

497 """ 

498 raise NotImplementedError("Subclasses must implement catalog statistics method.") 

499 

500 def detectorStatistics(self, statisticsDict, statControl): 

501 """Calculate detector level statistics based on the existing 

502 per-amplifier measurements. 

503 

504 Parameters 

505 ---------- 

506 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]], 

507 Dictionary of measured statistics. The inner dictionary 

508 should have keys that are statistic names (`str`) with 

509 values that are some sort of scalar (`int` or `float` are 

510 the mostly likely types). 

511 

512 Returns 

513 ------- 

514 outputStatistics : `dict` [`str`, scalar] 

515 A dictionary of the statistics measured and their values. 

516 

517 Raises 

518 ------ 

519 NotImplementedError : 

520 This method must be implemented by the calibration-type 

521 subclass. 

522 """ 

523 raise NotImplementedError("Subclasses must implement detector statistics method.") 

524 

525 def verify(self, exposure, statisticsDict): 

526 """Verify that the measured statistics meet the verification criteria. 

527 

528 Parameters 

529 ---------- 

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

531 The exposure the statistics are from. 

532 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]], 

533 Dictionary of measured statistics. The inner dictionary 

534 should have keys that are statistic names (`str`) with 

535 values that are some sort of scalar (`int` or `float` are 

536 the mostly likely types). 

537 

538 Returns 

539 ------- 

540 outputStatistics : `dict` [`str`, `dict` [`str`, `bool`]] 

541 A dictionary indexed by the amplifier name, containing 

542 dictionaries of the verification criteria. 

543 success : `bool` 

544 A boolean indicating whether all tests have passed. 

545 

546 Raises 

547 ------ 

548 NotImplementedError : 

549 This method must be implemented by the calibration-type 

550 subclass. 

551 """ 

552 raise NotImplementedError("Subclasses must implement verification criteria.")