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

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

161 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 

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

37 

38 

39class CpVerifyStatsConnections(pipeBase.PipelineTaskConnections, 

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

41 defaultTemplates={}): 

42 inputExp = cT.Input( 

43 name="postISRCCD", 

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

45 storageClass="Exposure", 

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

47 ) 

48 taskMetadata = cT.Input( 

49 name="isrTask_metadata", 

50 doc="Input task metadata to extract statistics from.", 

51 storageClass="PropertySet", 

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

53 ) 

54 camera = cT.PrerequisiteInput( 

55 name="camera", 

56 storageClass="Camera", 

57 doc="Input camera.", 

58 dimensions=["instrument", ], 

59 isCalibration=True, 

60 ) 

61 

62 outputStats = cT.Output( 

63 name="detectorStats", 

64 doc="Output statistics from cp_verify.", 

65 storageClass="StructuredDataDict", 

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

67 ) 

68 

69 def __init__(self, *, config=None): 

70 super().__init__(config=config) 

71 

72 if len(config.metadataStatKeywords) < 1: 

73 self.inputs.discard('taskMetadata') 

74 

75 

76class CpVerifyStatsConfig(pipeBase.PipelineTaskConfig, 

77 pipelineConnections=CpVerifyStatsConnections): 

78 """Configuration parameters for CpVerifyStatsTask. 

79 """ 

80 maskNameList = pexConfig.ListField( 

81 dtype=str, 

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

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

84 ) 

85 doVignette = pexConfig.Field( 

86 dtype=bool, 

87 doc="Mask vignetted regions?", 

88 default=False, 

89 ) 

90 doNormalize = pexConfig.Field( 

91 dtype=bool, 

92 doc="Normalize by exposure time?", 

93 default=False, 

94 ) 

95 

96 # Cosmic ray handling options. 

97 doCR = pexConfig.Field( 

98 dtype=bool, 

99 doc="Run CR rejection?", 

100 default=False, 

101 ) 

102 repair = pexConfig.ConfigurableField( 

103 target=RepairTask, 

104 doc="Repair task to use.", 

105 ) 

106 psfFwhm = pexConfig.Field( 

107 dtype=float, 

108 default=3.0, 

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

110 ) 

111 psfSize = pexConfig.Field( 

112 dtype=int, 

113 default=21, 

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

115 ) 

116 crGrow = pexConfig.Field( 

117 dtype=int, 

118 default=0, 

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

120 ) 

121 

122 # Statistics options. 

123 useReadNoise = pexConfig.Field( 

124 dtype=bool, 

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

126 default=True, 

127 ) 

128 numSigmaClip = pexConfig.Field( 

129 dtype=float, 

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

131 default=5.0, 

132 ) 

133 clipMaxIter = pexConfig.Field( 

134 dtype=int, 

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

136 default=3, 

137 ) 

138 

139 # Keywords and statistics to measure from different sources. 

140 imageStatKeywords = pexConfig.DictField( 

141 keytype=str, 

142 itemtype=str, 

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

144 default={}, 

145 ) 

146 unmaskedImageStatKeywords = pexConfig.DictField( 

147 keytype=str, 

148 itemtype=str, 

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

150 default={}, 

151 ) 

152 crImageStatKeywords = pexConfig.DictField( 

153 keytype=str, 

154 itemtype=str, 

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

156 default={}, 

157 ) 

158 normImageStatKeywords = pexConfig.DictField( 

159 keytype=str, 

160 itemtype=str, 

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

162 default={}, 

163 ) 

164 metadataStatKeywords = pexConfig.DictField( 

165 keytype=str, 

166 itemtype=str, 

167 doc="Statistics to measure from the metadata of the exposure.", 

168 default={}, 

169 ) 

170 catalogStatKeywords = pexConfig.DictField( 

171 keytype=str, 

172 itemtype=str, 

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

174 default={}, 

175 ) 

176 detectorStatKeywords = pexConfig.DictField( 

177 keytype=str, 

178 itemtype=str, 

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

180 default={}, 

181 ) 

182 

183 

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

185 """Main statistic measurement and validation class. 

186 

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

188 designed to be subclassed so specific calibrations can apply their 

189 own validation methods. 

190 """ 

191 ConfigClass = CpVerifyStatsConfig 

192 _DefaultName = 'cpVerifyStats' 

193 

194 def __init__(self, **kwargs): 

195 super().__init__(**kwargs) 

196 self.makeSubtask("repair") 

197 

198 def run(self, inputExp, camera, taskMetadata=None): 

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

200 for a calibration. 

201 

202 Parameters 

203 ---------- 

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

205 The ISR processed exposure to be measured. 

206 taskMetadata : `lsst.daf.base.PropertySet`, optional 

207 Task metadata containing additional statistics. 

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

209 The camera geometry for ``inputExp``. 

210 

211 Returns 

212 ------- 

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

214 Result struct with components: 

215 - ``outputStats`` : `dict` 

216 The output measured statistics. 

217 

218 Notes 

219 ----- 

220 The outputStats should have a yaml representation of the form 

221 

222 AMP: 

223 Amp1: 

224 STAT: value 

225 STAT2: value2 

226 Amp2: 

227 Amp3: 

228 DET: 

229 STAT: value 

230 STAT2: value 

231 CATALOG: 

232 STAT: value 

233 STAT2: value 

234 VERIFY: 

235 DET: 

236 TEST: boolean 

237 CATALOG: 

238 TEST: boolean 

239 AMP: 

240 Amp1: 

241 TEST: boolean 

242 TEST2: boolean 

243 Amp2: 

244 Amp3: 

245 SUCCESS: boolean 

246 

247 """ 

248 outputStats = {} 

249 

250 if self.config.doVignette: 

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

252 doSetValue=False, log=self.log) 

253 

254 mask = inputExp.getMask() 

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

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

257 self.config.clipMaxIter, 

258 maskVal) 

259 

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

261 # make a number of different image stats. 

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

263 

264 if len(self.config.metadataStatKeywords): 

265 # These are also defined on a amp-by-amp basis. 

266 outputStats['METADATA'] = self.metadataStatistics(inputExp, taskMetadata) 

267 else: 

268 outputStats['METADATA'] = {} 

269 

270 if len(self.config.catalogStatKeywords): 

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

272 else: 

273 outputStats['CATALOG'] = {} 

274 if len(self.config.detectorStatKeywords): 

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

276 else: 

277 outputStats['DET'] = {} 

278 

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

280 

281 return pipeBase.Struct( 

282 outputStats=outputStats, 

283 ) 

284 

285 @staticmethod 

286 def _emptyAmpDict(exposure): 

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

288 

289 Parameters 

290 ---------- 

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

292 Exposure to extract detector from. 

293 

294 Returns 

295 ------- 

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

297 A skeleton statistics dictionary. 

298 

299 Raises 

300 ------ 

301 RuntimeError : 

302 Raised if no detector can be found. 

303 """ 

304 outputStatistics = {} 

305 detector = exposure.getDetector() 

306 if detector is None: 

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

308 

309 for amp in detector.getAmplifiers(): 

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

311 

312 return outputStatistics 

313 

314 # Image measurement methods. 

315 def imageStatistics(self, exposure, statControl): 

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

317 modifications. 

318 

319 Parameters 

320 ---------- 

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

322 Exposure containing the ISR processed data to measure. 

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

324 Statistics control object with parameters defined by 

325 the config. 

326 

327 Returns 

328 ------- 

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

330 A dictionary indexed by the amplifier name, containing 

331 dictionaries of the statistics measured and their values. 

332 

333 """ 

334 outputStatistics = self._emptyAmpDict(exposure) 

335 

336 if len(self.config.imageStatKeywords): 

337 outputStatistics = mergeStatDict(outputStatistics, 

338 self.amplifierStats(exposure, 

339 self.config.imageStatKeywords, 

340 statControl)) 

341 if len(self.config.unmaskedImageStatKeywords): 

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

343 

344 if len(self.config.normImageStatKeywords): 

345 outputStatistics = mergeStatDict(outputStatistics, 

346 self.normalizedImageStats(exposure, statControl)) 

347 

348 if len(self.config.crImageStatKeywords): 

349 outputStatistics = mergeStatDict(outputStatistics, 

350 self.crImageStats(exposure, statControl)) 

351 

352 return outputStatistics 

353 

354 @staticmethod 

355 def _configHelper(keywordDict): 

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

357 

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

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

360 

361 Parameters 

362 ---------- 

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 

368 Returns 

369 ------- 

370 statisticToRun : `int` 

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

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

373 Dictionary containing statistics property indexed by name. 

374 """ 

375 statisticToRun = 0 

376 statAccessor = {} 

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

378 statValue = afwMath.stringToStatisticsProperty(v) 

379 statisticToRun |= statValue 

380 statAccessor[k] = statValue 

381 

382 return statisticToRun, statAccessor 

383 

384 def metadataStatistics(self, exposure, taskMetadata): 

385 """Extract task metadata information for verification. 

386 

387 Parameters 

388 ---------- 

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

390 The exposure to measure. 

391 taskMetadata : `lsst.daf.base.PropertySet` 

392 The metadata to extract values from. 

393 

394 Returns 

395 ------- 

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

397 A dictionary indexed by the amplifier name, containing 

398 dictionaries of the statistics measured and their values. 

399 """ 

400 metadataStats = {} 

401 keywordDict = self.config.metadataStatKeywords 

402 

403 if taskMetadata: 

404 for key, value in keywordDict.items(): 

405 if value == 'AMP': 

406 metadataStats[key] = {} 

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

408 ampName = amp.getName() 

409 expectedKey = f"{key} {ampName}" 

410 metadataStats[key][ampName] = None 

411 for name in taskMetadata.names(): 

412 if expectedKey in taskMetadata[name]: 

413 metadataStats[key][ampName] = taskMetadata[name][expectedKey] 

414 else: 

415 # Assume it's detector-wide. 

416 expectedKey = key 

417 for name in taskMetadata.names(): 

418 if expectedKey in taskMetadata[name]: 

419 metadataStats[key] = taskMetadata[name][expectedKey] 

420 return metadataStats 

421 

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

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

424 

425 Parameters 

426 ---------- 

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

428 The exposure to measure. 

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

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

431 values the string name associated with the 

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

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

434 Statistics control object with parameters defined by 

435 the config. 

436 failAll : `bool`, optional 

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

438 

439 Returns 

440 ------- 

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

442 A dictionary indexed by the amplifier name, containing 

443 dictionaries of the statistics measured and their values. 

444 """ 

445 ampStats = {} 

446 

447 statisticToRun, statAccessor = self._configHelper(keywordDict) 

448 

449 # Measure stats on all amplifiers. 

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

451 ampName = amp.getName() 

452 theseStats = {} 

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

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

455 

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

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

458 

459 if failAll: 

460 theseStats['FORCE_FAILURE'] = failAll 

461 ampStats[ampName] = theseStats 

462 

463 return ampStats 

464 

465 def unmaskedImageStats(self, exposure): 

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

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

468 

469 Parameters 

470 ---------- 

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

472 The exposure to measure. 

473 

474 Returns 

475 ------- 

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

477 A dictionary indexed by the amplifier name, containing 

478 dictionaries of the statistics measured and their values. 

479 """ 

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

481 self.config.clipMaxIter, 

482 0x0) 

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

484 

485 def normalizedImageStats(self, exposure, statControl): 

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

487 by the exposure time. 

488 

489 Parameters 

490 ---------- 

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

492 The exposure to measure. 

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

494 Statistics control object with parameters defined by 

495 the config. 

496 

497 Returns 

498 ------- 

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

500 A dictionary indexed by the amplifier name, containing 

501 dictionaries of the statistics measured and their values. 

502 

503 Raises 

504 ------ 

505 RuntimeError : 

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

507 """ 

508 scaledExposure = exposure.clone() 

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

510 if exposureTime <= 0: 

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

512 mi = scaledExposure.getMaskedImage() 

513 mi /= exposureTime 

514 

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

516 

517 def crImageStats(self, exposure, statControl): 

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

519 after running cosmic ray rejection. 

520 

521 Parameters 

522 ---------- 

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

524 The exposure to measure. 

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

526 Statistics control object with parameters defined by 

527 the config. 

528 

529 Returns 

530 ------- 

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

532 A dictionary indexed by the amplifier name, containing 

533 dictionaries of the statistics measured and their values. 

534 

535 """ 

536 crRejectedExp = exposure.clone() 

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

538 self.config.psfSize, 

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

540 crRejectedExp.setPsf(psf) 

541 try: 

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

543 failAll = False 

544 except pexException.LengthError: 

545 self.log.warning("Failure masking cosmic rays (too many found). Continuing.") 

546 failAll = True 

547 

548 if self.config.crGrow > 0: 

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

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

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

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

553 spans.setMask(crRejectedExp.mask, crMask) 

554 

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

556 statControl, failAll=failAll) 

557 

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

559 def catalogStatistics(self, exposure, statControl): 

560 """Calculate statistics from a catalog. 

561 

562 Parameters 

563 ---------- 

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

565 The exposure to measure. 

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

567 Statistics control object with parameters defined by 

568 the config. 

569 

570 Returns 

571 ------- 

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

573 A dictionary indexed by the amplifier name, containing 

574 dictionaries of the statistics measured and their values. 

575 """ 

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

577 

578 def detectorStatistics(self, statisticsDict, statControl): 

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

580 per-amplifier measurements. 

581 

582 Parameters 

583 ---------- 

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

585 Dictionary of measured statistics. The inner dictionary 

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

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

588 the mostly likely types). 

589 

590 Returns 

591 ------- 

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

593 A dictionary of the statistics measured and their values. 

594 

595 Raises 

596 ------ 

597 NotImplementedError : 

598 This method must be implemented by the calibration-type 

599 subclass. 

600 """ 

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

602 

603 def verify(self, exposure, statisticsDict): 

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

605 

606 Parameters 

607 ---------- 

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

609 The exposure the statistics are from. 

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

611 Dictionary of measured statistics. The inner dictionary 

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

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

614 the mostly likely types). 

615 

616 Returns 

617 ------- 

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

619 A dictionary indexed by the amplifier name, containing 

620 dictionaries of the verification criteria. 

621 success : `bool` 

622 A boolean indicating whether all tests have passed. 

623 

624 Raises 

625 ------ 

626 NotImplementedError : 

627 This method must be implemented by the calibration-type 

628 subclass. 

629 """ 

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