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

162 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.ip.isr.vignette import maskVignettedRegion 

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 polygon = inputExp.getInfo().getValidPolygon() 

252 maskVignettedRegion(inputExp, polygon, maskPlane='NO_DATA', 

253 vignetteValue=None, log=self.log) 

254 

255 mask = inputExp.getMask() 

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

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

258 self.config.clipMaxIter, 

259 maskVal) 

260 

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

262 # make a number of different image stats. 

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

264 

265 if len(self.config.metadataStatKeywords): 

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

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

268 else: 

269 outputStats['METADATA'] = {} 

270 

271 if len(self.config.catalogStatKeywords): 

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

273 else: 

274 outputStats['CATALOG'] = {} 

275 if len(self.config.detectorStatKeywords): 

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

277 else: 

278 outputStats['DET'] = {} 

279 

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

281 

282 return pipeBase.Struct( 

283 outputStats=outputStats, 

284 ) 

285 

286 @staticmethod 

287 def _emptyAmpDict(exposure): 

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

289 

290 Parameters 

291 ---------- 

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

293 Exposure to extract detector from. 

294 

295 Returns 

296 ------- 

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

298 A skeleton statistics dictionary. 

299 

300 Raises 

301 ------ 

302 RuntimeError : 

303 Raised if no detector can be found. 

304 """ 

305 outputStatistics = {} 

306 detector = exposure.getDetector() 

307 if detector is None: 

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

309 

310 for amp in detector.getAmplifiers(): 

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

312 

313 return outputStatistics 

314 

315 # Image measurement methods. 

316 def imageStatistics(self, exposure, statControl): 

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

318 modifications. 

319 

320 Parameters 

321 ---------- 

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

323 Exposure containing the ISR processed data to measure. 

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

325 Statistics control object with parameters defined by 

326 the config. 

327 

328 Returns 

329 ------- 

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

331 A dictionary indexed by the amplifier name, containing 

332 dictionaries of the statistics measured and their values. 

333 

334 """ 

335 outputStatistics = self._emptyAmpDict(exposure) 

336 

337 if len(self.config.imageStatKeywords): 

338 outputStatistics = mergeStatDict(outputStatistics, 

339 self.amplifierStats(exposure, 

340 self.config.imageStatKeywords, 

341 statControl)) 

342 if len(self.config.unmaskedImageStatKeywords): 

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

344 

345 if len(self.config.normImageStatKeywords): 

346 outputStatistics = mergeStatDict(outputStatistics, 

347 self.normalizedImageStats(exposure, statControl)) 

348 

349 if len(self.config.crImageStatKeywords): 

350 outputStatistics = mergeStatDict(outputStatistics, 

351 self.crImageStats(exposure, statControl)) 

352 

353 return outputStatistics 

354 

355 @staticmethod 

356 def _configHelper(keywordDict): 

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

358 

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

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

361 

362 Parameters 

363 ---------- 

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 

369 Returns 

370 ------- 

371 statisticToRun : `int` 

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

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

374 Dictionary containing statistics property indexed by name. 

375 """ 

376 statisticToRun = 0 

377 statAccessor = {} 

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

379 statValue = afwMath.stringToStatisticsProperty(v) 

380 statisticToRun |= statValue 

381 statAccessor[k] = statValue 

382 

383 return statisticToRun, statAccessor 

384 

385 def metadataStatistics(self, exposure, taskMetadata): 

386 """Extract task metadata information for verification. 

387 

388 Parameters 

389 ---------- 

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

391 The exposure to measure. 

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

393 The metadata to extract values from. 

394 

395 Returns 

396 ------- 

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

398 A dictionary indexed by the amplifier name, containing 

399 dictionaries of the statistics measured and their values. 

400 """ 

401 metadataStats = {} 

402 keywordDict = self.config.metadataStatKeywords 

403 

404 if taskMetadata: 

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

406 if value == 'AMP': 

407 metadataStats[key] = {} 

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

409 ampName = amp.getName() 

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

411 metadataStats[key][ampName] = None 

412 for name in taskMetadata: 

413 if expectedKey in taskMetadata[name]: 

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

415 else: 

416 # Assume it's detector-wide. 

417 expectedKey = key 

418 for name in taskMetadata: 

419 if expectedKey in taskMetadata[name]: 

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

421 return metadataStats 

422 

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

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

425 

426 Parameters 

427 ---------- 

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

429 The exposure to measure. 

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

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

432 values the string name associated with the 

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

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

435 Statistics control object with parameters defined by 

436 the config. 

437 failAll : `bool`, optional 

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

439 

440 Returns 

441 ------- 

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

443 A dictionary indexed by the amplifier name, containing 

444 dictionaries of the statistics measured and their values. 

445 """ 

446 ampStats = {} 

447 

448 statisticToRun, statAccessor = self._configHelper(keywordDict) 

449 

450 # Measure stats on all amplifiers. 

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

452 ampName = amp.getName() 

453 theseStats = {} 

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

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

456 

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

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

459 

460 if failAll: 

461 theseStats['FORCE_FAILURE'] = failAll 

462 ampStats[ampName] = theseStats 

463 

464 return ampStats 

465 

466 def unmaskedImageStats(self, exposure): 

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

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

469 

470 Parameters 

471 ---------- 

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

473 The exposure to measure. 

474 

475 Returns 

476 ------- 

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

478 A dictionary indexed by the amplifier name, containing 

479 dictionaries of the statistics measured and their values. 

480 """ 

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

482 self.config.clipMaxIter, 

483 0x0) 

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

485 

486 def normalizedImageStats(self, exposure, statControl): 

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

488 by the exposure time. 

489 

490 Parameters 

491 ---------- 

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

493 The exposure to measure. 

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

495 Statistics control object with parameters defined by 

496 the config. 

497 

498 Returns 

499 ------- 

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

501 A dictionary indexed by the amplifier name, containing 

502 dictionaries of the statistics measured and their values. 

503 

504 Raises 

505 ------ 

506 RuntimeError : 

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

508 """ 

509 scaledExposure = exposure.clone() 

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

511 if exposureTime <= 0: 

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

513 mi = scaledExposure.getMaskedImage() 

514 mi /= exposureTime 

515 

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

517 

518 def crImageStats(self, exposure, statControl): 

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

520 after running cosmic ray rejection. 

521 

522 Parameters 

523 ---------- 

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

525 The exposure to measure. 

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

527 Statistics control object with parameters defined by 

528 the config. 

529 

530 Returns 

531 ------- 

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

533 A dictionary indexed by the amplifier name, containing 

534 dictionaries of the statistics measured and their values. 

535 

536 """ 

537 crRejectedExp = exposure.clone() 

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

539 self.config.psfSize, 

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

541 crRejectedExp.setPsf(psf) 

542 try: 

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

544 failAll = False 

545 except pexException.LengthError: 

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

547 failAll = True 

548 

549 if self.config.crGrow > 0: 

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

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

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

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

554 spans.setMask(crRejectedExp.mask, crMask) 

555 

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

557 statControl, failAll=failAll) 

558 

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

560 def catalogStatistics(self, exposure, statControl): 

561 """Calculate statistics from a catalog. 

562 

563 Parameters 

564 ---------- 

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

566 The exposure to measure. 

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

568 Statistics control object with parameters defined by 

569 the config. 

570 

571 Returns 

572 ------- 

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

574 A dictionary indexed by the amplifier name, containing 

575 dictionaries of the statistics measured and their values. 

576 """ 

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

578 

579 def detectorStatistics(self, statisticsDict, statControl): 

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

581 per-amplifier measurements. 

582 

583 Parameters 

584 ---------- 

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

586 Dictionary of measured statistics. The inner dictionary 

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

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

589 the mostly likely types). 

590 

591 Returns 

592 ------- 

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

594 A dictionary of the statistics measured and their values. 

595 

596 Raises 

597 ------ 

598 NotImplementedError : 

599 This method must be implemented by the calibration-type 

600 subclass. 

601 """ 

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

603 

604 def verify(self, exposure, statisticsDict): 

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

606 

607 Parameters 

608 ---------- 

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

610 The exposure the statistics are from. 

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

612 Dictionary of measured statistics. The inner dictionary 

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

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

615 the mostly likely types). 

616 

617 Returns 

618 ------- 

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

620 A dictionary indexed by the amplifier name, containing 

621 dictionaries of the verification criteria. 

622 success : `bool` 

623 A boolean indicating whether all tests have passed. 

624 

625 Raises 

626 ------ 

627 NotImplementedError : 

628 This method must be implemented by the calibration-type 

629 subclass. 

630 """ 

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