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

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

133 statements  

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.pex.exceptions as pexException 

27import lsst.pipe.base as pipeBase 

28import lsst.pipe.base.connectionTypes as cT 

29import lsst.meas.algorithms as measAlg 

30 

31from lsst.cp.pipe.cpCombine import vignetteExposure 

32from lsst.pipe.tasks.repair import RepairTask 

33from .utils import mergeStatDict 

34 

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

36 

37 

38class CpVerifyStatsConnections(pipeBase.PipelineTaskConnections, 

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

40 defaultTemplates={}): 

41 inputExp = cT.Input( 

42 name="postISRCCD", 

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

44 storageClass="Exposure", 

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

46 ) 

47 camera = cT.PrerequisiteInput( 

48 name="camera", 

49 storageClass="Camera", 

50 doc="Input camera.", 

51 dimensions=["instrument", ], 

52 isCalibration=True, 

53 ) 

54 

55 outputStats = cT.Output( 

56 name="detectorStats", 

57 doc="Output statistics from cp_verify.", 

58 storageClass="StructuredDataDict", 

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

60 ) 

61 

62 

63class CpVerifyStatsConfig(pipeBase.PipelineTaskConfig, 

64 pipelineConnections=CpVerifyStatsConnections): 

65 """Configuration parameters for CpVerifyStatsTask. 

66 """ 

67 maskNameList = pexConfig.ListField( 

68 dtype=str, 

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

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

71 ) 

72 doVignette = pexConfig.Field( 

73 dtype=bool, 

74 doc="Mask vignetted regions?", 

75 default=False, 

76 ) 

77 doNormalize = pexConfig.Field( 

78 dtype=bool, 

79 doc="Normalize by exposure time?", 

80 default=False, 

81 ) 

82 

83 # Cosmic ray handling options. 

84 doCR = pexConfig.Field( 

85 dtype=bool, 

86 doc="Run CR rejection?", 

87 default=False, 

88 ) 

89 repair = pexConfig.ConfigurableField( 

90 target=RepairTask, 

91 doc="Repair task to use.", 

92 ) 

93 psfFwhm = pexConfig.Field( 

94 dtype=float, 

95 default=3.0, 

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

97 ) 

98 psfSize = pexConfig.Field( 

99 dtype=int, 

100 default=21, 

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

102 ) 

103 crGrow = pexConfig.Field( 

104 dtype=int, 

105 default=0, 

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

107 ) 

108 

109 # Statistics options. 

110 useReadNoise = pexConfig.Field( 

111 dtype=bool, 

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

113 default=True, 

114 ) 

115 numSigmaClip = pexConfig.Field( 

116 dtype=float, 

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

118 default=5.0, 

119 ) 

120 clipMaxIter = pexConfig.Field( 

121 dtype=int, 

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

123 default=3, 

124 ) 

125 

126 # Keywords and statistics to measure from different sources. 

127 imageStatKeywords = pexConfig.DictField( 

128 keytype=str, 

129 itemtype=str, 

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

131 default={}, 

132 ) 

133 unmaskedImageStatKeywords = pexConfig.DictField( 

134 keytype=str, 

135 itemtype=str, 

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

137 default={}, 

138 ) 

139 crImageStatKeywords = pexConfig.DictField( 

140 keytype=str, 

141 itemtype=str, 

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

143 default={}, 

144 ) 

145 normImageStatKeywords = pexConfig.DictField( 

146 keytype=str, 

147 itemtype=str, 

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

149 default={}, 

150 ) 

151 catalogStatKeywords = pexConfig.DictField( 

152 keytype=str, 

153 itemtype=str, 

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

155 default={}, 

156 ) 

157 detectorStatKeywords = pexConfig.DictField( 

158 keytype=str, 

159 itemtype=str, 

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

161 default={}, 

162 ) 

163 

164 

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

166 """Main statistic measurement and validation class. 

167 

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

169 designed to be subclassed so specific calibrations can apply their 

170 own validation methods. 

171 """ 

172 ConfigClass = CpVerifyStatsConfig 

173 _DefaultName = 'cpVerifyStats' 

174 

175 def __init__(self, **kwargs): 

176 super().__init__(**kwargs) 

177 self.makeSubtask("repair") 

178 

179 def run(self, inputExp, camera): 

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

181 for a calibration. 

182 

183 Parameters 

184 ---------- 

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

186 The ISR processed exposure to be measured. 

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

188 The camera geometry for ``inputExp``. 

189 

190 Returns 

191 ------- 

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

193 Result struct with components: 

194 - ``outputStats`` : `dict` 

195 The output measured statistics. 

196 

197 Notes 

198 ----- 

199 The outputStats should have a yaml representation of the form 

200 

201 AMP: 

202 Amp1: 

203 STAT: value 

204 STAT2: value2 

205 Amp2: 

206 Amp3: 

207 DET: 

208 STAT: value 

209 STAT2: value 

210 CATALOG: 

211 STAT: value 

212 STAT2: value 

213 VERIFY: 

214 DET: 

215 TEST: boolean 

216 CATALOG: 

217 TEST: boolean 

218 AMP: 

219 Amp1: 

220 TEST: boolean 

221 TEST2: boolean 

222 Amp2: 

223 Amp3: 

224 SUCCESS: boolean 

225 

226 """ 

227 outputStats = {} 

228 

229 if self.config.doVignette: 

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

231 doSetValue=False, log=self.log) 

232 

233 mask = inputExp.getMask() 

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

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

236 self.config.clipMaxIter, 

237 maskVal) 

238 

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

240 # make a number of different image stats. 

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

242 if len(self.config.catalogStatKeywords): 

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

244 else: 

245 outputStats['CATALOG'] = {} 

246 if len(self.config.detectorStatKeywords): 

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

248 else: 

249 outputStats['DET'] = {} 

250 

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

252 

253 return pipeBase.Struct( 

254 outputStats=outputStats, 

255 ) 

256 

257 @staticmethod 

258 def _emptyAmpDict(exposure): 

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

260 

261 Parameters 

262 ---------- 

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

264 Exposure to extract detector from. 

265 

266 Returns 

267 ------- 

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

269 A skeleton statistics dictionary. 

270 

271 Raises 

272 ------ 

273 RuntimeError : 

274 Raised if no detector can be found. 

275 """ 

276 outputStatistics = {} 

277 detector = exposure.getDetector() 

278 if detector is None: 

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

280 

281 for amp in detector.getAmplifiers(): 

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

283 

284 return outputStatistics 

285 

286 # Image measurement methods. 

287 def imageStatistics(self, exposure, statControl): 

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

289 modifications. 

290 

291 Parameters 

292 ---------- 

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

294 Exposure containing the ISR processed data to measure. 

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

296 Statistics control object with parameters defined by 

297 the config. 

298 

299 Returns 

300 ------- 

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

302 A dictionary indexed by the amplifier name, containing 

303 dictionaries of the statistics measured and their values. 

304 

305 """ 

306 outputStatistics = self._emptyAmpDict(exposure) 

307 

308 if len(self.config.imageStatKeywords): 

309 outputStatistics = mergeStatDict(outputStatistics, 

310 self.amplifierStats(exposure, 

311 self.config.imageStatKeywords, 

312 statControl)) 

313 

314 if len(self.config.unmaskedImageStatKeywords): 

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

316 

317 if len(self.config.normImageStatKeywords): 

318 outputStatistics = mergeStatDict(outputStatistics, 

319 self.normalizedImageStats(exposure, statControl)) 

320 

321 if len(self.config.crImageStatKeywords): 

322 outputStatistics = mergeStatDict(outputStatistics, 

323 self.crImageStats(exposure, statControl)) 

324 

325 return outputStatistics 

326 

327 @staticmethod 

328 def _configHelper(keywordDict): 

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

330 

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

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

333 

334 Parameters 

335 ---------- 

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

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

338 values the string name associated with the 

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

340 

341 Returns 

342 ------- 

343 statisticToRun : `int` 

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

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

346 Dictionary containing statistics property indexed by name. 

347 """ 

348 statisticToRun = 0 

349 statAccessor = {} 

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

351 statValue = afwMath.stringToStatisticsProperty(v) 

352 statisticToRun |= statValue 

353 statAccessor[k] = statValue 

354 

355 return statisticToRun, statAccessor 

356 

357 def amplifierStats(self, exposure, keywordDict, statControl, failAll=False): 

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

359 

360 Parameters 

361 ---------- 

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

363 The exposure to measure. 

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

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

366 values the string name associated with the 

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

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

369 Statistics control object with parameters defined by 

370 the config. 

371 failAll : `bool`, optional 

372 If True, all tests will be set as failed. 

373 

374 Returns 

375 ------- 

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

377 A dictionary indexed by the amplifier name, containing 

378 dictionaries of the statistics measured and their values. 

379 """ 

380 ampStats = {} 

381 

382 statisticToRun, statAccessor = self._configHelper(keywordDict) 

383 

384 # Measure stats on all amplifiers. 

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

386 ampName = amp.getName() 

387 theseStats = {} 

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

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

390 

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

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

393 

394 if failAll: 

395 theseStats['FORCE_FAILURE'] = failAll 

396 ampStats[ampName] = theseStats 

397 

398 return ampStats 

399 

400 def unmaskedImageStats(self, exposure): 

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

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

403 

404 Parameters 

405 ---------- 

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

407 The exposure to measure. 

408 

409 Returns 

410 ------- 

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

412 A dictionary indexed by the amplifier name, containing 

413 dictionaries of the statistics measured and their values. 

414 """ 

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

416 self.config.clipMaxIter, 

417 0x0) 

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

419 

420 def normalizedImageStats(self, exposure, statControl): 

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

422 by the exposure time. 

423 

424 Parameters 

425 ---------- 

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

427 The exposure to measure. 

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

429 Statistics control object with parameters defined by 

430 the config. 

431 

432 Returns 

433 ------- 

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

435 A dictionary indexed by the amplifier name, containing 

436 dictionaries of the statistics measured and their values. 

437 

438 Raises 

439 ------ 

440 RuntimeError : 

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

442 """ 

443 scaledExposure = exposure.clone() 

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

445 if exposureTime <= 0: 

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

447 mi = scaledExposure.getMaskedImage() 

448 mi /= exposureTime 

449 

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

451 

452 def crImageStats(self, exposure, statControl): 

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

454 after running cosmic ray rejection. 

455 

456 Parameters 

457 ---------- 

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

459 The exposure to measure. 

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

461 Statistics control object with parameters defined by 

462 the config. 

463 

464 Returns 

465 ------- 

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

467 A dictionary indexed by the amplifier name, containing 

468 dictionaries of the statistics measured and their values. 

469 

470 """ 

471 crRejectedExp = exposure.clone() 

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

473 self.config.psfSize, 

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

475 crRejectedExp.setPsf(psf) 

476 try: 

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

478 failAll = False 

479 except pexException.LengthError: 

480 self.log.warn("Failure masking cosmic rays (too many found). Continuing.") 

481 failAll = True 

482 

483 if self.config.crGrow > 0: 

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

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

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

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

488 spans.setMask(crRejectedExp.mask, crMask) 

489 

490 return self.amplifierStats(crRejectedExp, self.config.crImageStatKeywords, 

491 statControl, failAll=failAll) 

492 

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

494 def catalogStatistics(self, exposure, statControl): 

495 """Calculate statistics from a catalog. 

496 

497 Parameters 

498 ---------- 

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

500 The exposure to measure. 

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

502 Statistics control object with parameters defined by 

503 the config. 

504 

505 Returns 

506 ------- 

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

508 A dictionary indexed by the amplifier name, containing 

509 dictionaries of the statistics measured and their values. 

510 """ 

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

512 

513 def detectorStatistics(self, statisticsDict, statControl): 

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

515 per-amplifier measurements. 

516 

517 Parameters 

518 ---------- 

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

520 Dictionary of measured statistics. The inner dictionary 

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

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

523 the mostly likely types). 

524 

525 Returns 

526 ------- 

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

528 A dictionary of the statistics measured and their values. 

529 

530 Raises 

531 ------ 

532 NotImplementedError : 

533 This method must be implemented by the calibration-type 

534 subclass. 

535 """ 

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

537 

538 def verify(self, exposure, statisticsDict): 

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

540 

541 Parameters 

542 ---------- 

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

544 The exposure the statistics are from. 

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

546 Dictionary of measured statistics. The inner dictionary 

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

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

549 the mostly likely types). 

550 

551 Returns 

552 ------- 

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

554 A dictionary indexed by the amplifier name, containing 

555 dictionaries of the verification criteria. 

556 success : `bool` 

557 A boolean indicating whether all tests have passed. 

558 

559 Raises 

560 ------ 

561 NotImplementedError : 

562 This method must be implemented by the calibration-type 

563 subclass. 

564 """ 

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