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

220 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-28 09:22 +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 numpy as np 

22 

23from astropy.table import Table 

24 

25import lsst.afw.geom as afwGeom 

26import lsst.afw.math as afwMath 

27import lsst.pex.config as pexConfig 

28import lsst.pex.exceptions as pexException 

29import lsst.pipe.base as pipeBase 

30import lsst.pipe.base.connectionTypes as cT 

31import lsst.meas.algorithms as measAlg 

32 

33from lsst.ip.isr.vignette import maskVignettedRegion 

34from lsst.pipe.tasks.repair import RepairTask 

35from .utils import mergeStatDict 

36 

37 

38__all__ = ["CpVerifyStatsConfig", "CpVerifyStatsTask"] 

39 

40 

41class CpVerifyStatsConnections( 

42 pipeBase.PipelineTaskConnections, 

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

44 defaultTemplates={}, 

45): 

46 inputExp = cT.Input( 

47 name="postISRCCD", 

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

49 storageClass="Exposure", 

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

51 ) 

52 uncorrectedExp = cT.Input( 

53 name="uncorrectedExp", 

54 doc="Uncorrected input exposure to calculate statistics for.", 

55 storageClass="ExposureF", 

56 dimensions=["instrument", "visit", "detector"], 

57 ) 

58 # TODO DM-45802: Remove deprecated taskMetadata connection. 

59 taskMetadata = cT.Input( 

60 name="isrTask_metadata", 

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

62 storageClass="TaskMetadata", 

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

64 deprecated="This connection is deprecated and will be removed after v28.", 

65 ) 

66 inputCatalog = cT.Input( 

67 name="src", 

68 doc="Input catalog to calculate statistics for.", 

69 storageClass="SourceCatalog", 

70 dimensions=["instrument", "visit", "detector"], 

71 ) 

72 uncorrectedCatalog = cT.Input( 

73 name="uncorrectedSrc", 

74 doc="Input catalog without correction applied.", 

75 storageClass="SourceCatalog", 

76 dimensions=["instrument", "visit", "detector"], 

77 ) 

78 camera = cT.PrerequisiteInput( 

79 name="camera", 

80 storageClass="Camera", 

81 doc="Input camera.", 

82 dimensions=["instrument", ], 

83 isCalibration=True, 

84 ) 

85 isrStatistics = cT.Input( 

86 name="isrStatistics", 

87 storageClass="StructuredDataDict", 

88 doc="Pre-calculated statistics from IsrTask.", 

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

90 ) 

91 

92 outputStats = cT.Output( 

93 name="detectorStats", 

94 doc="Output statistics from cp_verify.", 

95 storageClass="StructuredDataDict", 

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

97 ) 

98 outputResults = cT.Output( 

99 name="detectorResults", 

100 doc="Output results from cp_verify.", 

101 storageClass="ArrowAstropy", 

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

103 ) 

104 outputMatrix = cT.Output( 

105 name="detectorMatrix", 

106 doc="Output matrix results from cp_verify.", 

107 storageClass="ArrowAstropy", 

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

109 ) 

110 

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

112 super().__init__(config=config) 

113 

114 # TODO DM-45802: Remove deprecated taskMetadata connection. 

115 self.inputs.discard("taskMetadata") 

116 

117 if len(config.catalogStatKeywords) < 1: 

118 self.inputs.discard("inputCatalog") 

119 self.inputs.discard("uncorrectedCatalog") 

120 

121 if len(config.uncorrectedImageStatKeywords) < 1: 

122 self.inputs.discard("uncorrectedExp") 

123 

124 if config.useIsrStatistics is not True: 

125 self.inputs.discard("isrStatistics") 

126 

127 if not config.hasMatrixCatalog: 

128 self.outputs.discard("outputMatrix") 

129 

130 

131class CpVerifyStatsConfig( 

132 pipeBase.PipelineTaskConfig, pipelineConnections=CpVerifyStatsConnections 

133): 

134 """Configuration parameters for CpVerifyStatsTask.""" 

135 

136 maskNameList = pexConfig.ListField( 

137 dtype=str, 

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

139 default=["DETECTED", "BAD", "NO_DATA"], 

140 ) 

141 doVignette = pexConfig.Field( 

142 dtype=bool, 

143 doc="Mask vignetted regions?", 

144 default=False, 

145 ) 

146 doNormalize = pexConfig.Field( 

147 dtype=bool, 

148 doc="Normalize by exposure time?", 

149 default=False, 

150 ) 

151 

152 # Cosmic ray handling options. 

153 doCR = pexConfig.Field( 

154 dtype=bool, 

155 doc="Run CR rejection?", 

156 default=False, 

157 ) 

158 repair = pexConfig.ConfigurableField( 

159 target=RepairTask, 

160 doc="Repair task to use.", 

161 ) 

162 psfFwhm = pexConfig.Field( 

163 dtype=float, 

164 default=3.0, 

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

166 ) 

167 psfSize = pexConfig.Field( 

168 dtype=int, 

169 default=21, 

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

171 ) 

172 crGrow = pexConfig.Field( 

173 dtype=int, 

174 default=0, 

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

176 ) 

177 

178 # Statistics options. 

179 useReadNoise = pexConfig.Field( 

180 dtype=bool, 

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

182 default=True, 

183 ) 

184 numSigmaClip = pexConfig.Field( 

185 dtype=float, 

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

187 default=5.0, 

188 ) 

189 clipMaxIter = pexConfig.Field( 

190 dtype=int, 

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

192 default=3, 

193 ) 

194 

195 # Keywords and statistics to measure from different sources. 

196 imageStatKeywords = pexConfig.DictField( 

197 keytype=str, 

198 itemtype=str, 

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

200 default={}, 

201 ) 

202 unmaskedImageStatKeywords = pexConfig.DictField( 

203 keytype=str, 

204 itemtype=str, 

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

206 default={}, 

207 ) 

208 uncorrectedImageStatKeywords = pexConfig.DictField( 

209 keytype=str, 

210 itemtype=str, 

211 doc="Uncorrected image statistics to run on amplifier segments.", 

212 default={}, 

213 ) 

214 crImageStatKeywords = pexConfig.DictField( 

215 keytype=str, 

216 itemtype=str, 

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

218 default={}, 

219 ) 

220 normImageStatKeywords = pexConfig.DictField( 

221 keytype=str, 

222 itemtype=str, 

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

224 default={}, 

225 ) 

226 metadataStatKeywords = pexConfig.DictField( 

227 keytype=str, 

228 itemtype=str, 

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

230 default={}, 

231 ) 

232 catalogStatKeywords = pexConfig.DictField( 

233 keytype=str, 

234 itemtype=str, 

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

236 default={}, 

237 ) 

238 detectorStatKeywords = pexConfig.DictField( 

239 keytype=str, 

240 itemtype=str, 

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

242 default={}, 

243 ) 

244 

245 stageName = pexConfig.Field( 

246 dtype=str, 

247 doc="Stage name to use for table columns.", 

248 default="NOSTAGE", 

249 ) 

250 useIsrStatistics = pexConfig.Field( 

251 dtype=bool, 

252 doc="Use statistics calculated by IsrTask?", 

253 default=False, 

254 ) 

255 hasMatrixCatalog = pexConfig.Field( 

256 dtype=bool, 

257 doc="Will a matrix table of results be made?", 

258 default=False, 

259 ) 

260 expectedDistributionLevels = pexConfig.ListField( 

261 dtype=float, 

262 doc="Percentile levels expected in the calibration header.", 

263 default=[0, 5, 16, 50, 84, 95, 100], 

264 ) 

265 

266 

267class CpVerifyStatsTask(pipeBase.PipelineTask): 

268 """Main statistic measurement and validation class. 

269 

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

271 designed to be subclassed so specific calibrations can apply their 

272 own validation methods. 

273 """ 

274 

275 ConfigClass = CpVerifyStatsConfig 

276 _DefaultName = "cpVerifyStats" 

277 

278 def __init__(self, **kwargs): 

279 super().__init__(**kwargs) 

280 self.makeSubtask("repair") 

281 

282 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

283 inputs = butlerQC.get(inputRefs) 

284 

285 # Pass the full dataId, as we want to retain filter info. 

286 inputs["dimensions"] = dict(inputRefs.inputExp.dataId.mapping) 

287 outputs = self.run(**inputs) 

288 butlerQC.put(outputs, outputRefs) 

289 

290 def run( 

291 self, 

292 inputExp, 

293 camera, 

294 isrStatistics=None, 

295 uncorrectedExp=None, 

296 taskMetadata=None, 

297 inputCatalog=None, 

298 uncorrectedCatalog=None, 

299 dimensions=None, 

300 ): 

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

302 for a calibration. 

303 

304 Parameters 

305 ---------- 

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

307 The ISR processed exposure to be measured. 

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

309 The camera geometry for ``inputExp``. 

310 uncorrectedExp : `lsst.afw.image.Exposure` 

311 The alternate exposure to measure. 

312 taskMetadata : `lsst.pipe.base.TaskMetadata`, optional 

313 Task metadata containing additional statistics. 

314 inputCatalog : `lsst.afw.image.Table` 

315 The source catalog to measure. 

316 uncorrectedCatalog : `lsst.afw.image.Table` 

317 The alternate source catalog to measure. 

318 dimensions : `dict` 

319 Dictionary of input dictionary. 

320 

321 Returns 

322 ------- 

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

324 Result struct with components: 

325 - ``outputStats`` : `dict` 

326 The output measured statistics. 

327 """ 

328 outputStats = {} 

329 

330 if self.config.doVignette: 

331 polygon = inputExp.getInfo().getValidPolygon() 

332 maskVignettedRegion( 

333 inputExp, polygon, maskPlane="NO_DATA", vignetteValue=None, log=self.log 

334 ) 

335 

336 mask = inputExp.getMask() 

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

338 statControl = afwMath.StatisticsControl( 

339 self.config.numSigmaClip, self.config.clipMaxIter, maskVal 

340 ) 

341 

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

343 # make a number of different image stats. 

344 outputStats["AMP"] = self.imageStatistics(inputExp, uncorrectedExp, statControl) 

345 

346 if len(self.config.metadataStatKeywords): 

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

348 outputStats["METADATA"] = self.metadataStatistics(inputExp, taskMetadata) 

349 else: 

350 outputStats["METADATA"] = {} 

351 

352 if len(self.config.catalogStatKeywords): 

353 outputStats["CATALOG"] = self.catalogStatistics( 

354 inputExp, inputCatalog, uncorrectedCatalog, statControl 

355 ) 

356 else: 

357 outputStats["CATALOG"] = {} 

358 if len(self.config.detectorStatKeywords): 

359 outputStats["DET"] = self.detectorStatistics( 

360 outputStats, statControl, inputExp, uncorrectedExp 

361 ) 

362 else: 

363 outputStats["DET"] = {} 

364 

365 if self.config.useIsrStatistics: 

366 outputStats["ISR"] = isrStatistics 

367 

368 outputStats["VERIFY"], outputStats["SUCCESS"] = self.verify( 

369 inputExp, outputStats 

370 ) 

371 

372 outputResults, outputMatrix = self.repackStats(outputStats, dimensions) 

373 

374 return pipeBase.Struct( 

375 outputStats=outputStats, 

376 outputResults=Table(outputResults), 

377 outputMatrix=Table(outputMatrix), 

378 ) 

379 

380 @staticmethod 

381 def _emptyAmpDict(exposure): 

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

383 

384 Parameters 

385 ---------- 

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

387 Exposure to extract detector from. 

388 

389 Returns 

390 ------- 

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

392 A skeleton statistics dictionary. 

393 

394 Raises 

395 ------ 

396 RuntimeError : 

397 Raised if no detector can be found. 

398 """ 

399 outputStatistics = {} 

400 detector = exposure.getDetector() 

401 if detector is None: 

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

403 

404 for amp in detector.getAmplifiers(): 

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

406 

407 return outputStatistics 

408 

409 # Image measurement methods. 

410 def imageStatistics(self, exposure, uncorrectedExposure, statControl): 

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

412 modifications. 

413 

414 Parameters 

415 ---------- 

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

417 Exposure containing the ISR processed data to measure. 

418 uncorrectedExposure: `lsst.afw.image.Exposure` 

419 Uncorrected exposure containing the ISR processed data to measure. 

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

421 Statistics control object with parameters defined by 

422 the config. 

423 

424 Returns 

425 ------- 

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

427 A dictionary indexed by the amplifier name, containing 

428 dictionaries of the statistics measured and their values. 

429 

430 """ 

431 outputStatistics = self._emptyAmpDict(exposure) 

432 

433 if len(self.config.imageStatKeywords): 

434 outputStatistics = mergeStatDict( 

435 outputStatistics, 

436 self.amplifierStats( 

437 exposure, self.config.imageStatKeywords, statControl 

438 ), 

439 ) 

440 if len(self.config.uncorrectedImageStatKeywords): 

441 outputStatistics = mergeStatDict( 

442 outputStatistics, 

443 self.amplifierStats( 

444 uncorrectedExposure, 

445 self.config.uncorrectedImageStatKeywords, 

446 statControl, 

447 ), 

448 ) 

449 if len(self.config.unmaskedImageStatKeywords): 

450 outputStatistics = mergeStatDict( 

451 outputStatistics, self.unmaskedImageStats(exposure) 

452 ) 

453 

454 if len(self.config.normImageStatKeywords): 

455 outputStatistics = mergeStatDict( 

456 outputStatistics, self.normalizedImageStats(exposure, statControl) 

457 ) 

458 

459 if len(self.config.crImageStatKeywords): 

460 outputStatistics = mergeStatDict( 

461 outputStatistics, self.crImageStats(exposure, statControl) 

462 ) 

463 

464 return outputStatistics 

465 

466 @staticmethod 

467 def _configHelper(keywordDict): 

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

469 

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

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

472 

473 Parameters 

474 ---------- 

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

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

477 values the string name associated with the 

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

479 

480 Returns 

481 ------- 

482 statisticToRun : `int` 

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

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

485 Dictionary containing statistics property indexed by name. 

486 """ 

487 statisticToRun = 0 

488 statAccessor = {} 

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

490 statValue = afwMath.stringToStatisticsProperty(v) 

491 statisticToRun |= statValue 

492 statAccessor[k] = statValue 

493 

494 return statisticToRun, statAccessor 

495 

496 def metadataStatistics(self, exposure, taskMetadata=None): 

497 """Extract task metadata information for verification. 

498 

499 Parameters 

500 ---------- 

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

502 The exposure to measure. 

503 taskMetadata : `lsst.pipe.base.TaskMetadata`, optional 

504 The metadata to extract values from.This is not used, 

505 and will be completely removed on DM-45802. 

506 

507 Returns 

508 ------- 

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

510 A dictionary indexed by the amplifier name, containing 

511 dictionaries of the statistics measured and their values. 

512 """ 

513 metadataStats = {} 

514 keywordDict = self.config.metadataStatKeywords 

515 

516 # Changing how we're handling this: keywordDict contains (key, 

517 # storeKey) pairs, where `key` is what we're trying to find, 

518 # and `storeKey` is the name we'll use to store the value we 

519 # find from `key`. 

520 expMD = exposure.getMetadata() 

521 for key, storeKey in keywordDict.items(): 

522 found = False 

523 expectedKey = key 

524 # Try to find this in the exposure metadata first: 

525 if expectedKey in expMD: 

526 metadataStats[storeKey] = expMD[expectedKey] 

527 found = True 

528 else: 

529 # Maybe this is a per-amp quantity: 

530 results = {} 

531 for amp in exposure.getDetector(): 

532 ampName = amp.getName() 

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

534 if expectedKey in expMD: 

535 results[ampName] = expMD[expectedKey] 

536 if len(results) != 0: 

537 for amp in exposure.getDetector(): 

538 ampName = amp.getName() 

539 if ampName not in results: 

540 results[ampName] = np.nan 

541 metadataStats[storeKey] = results 

542 found = True 

543 

544 if not found: 

545 self.log.debug(f"Could not find expected key: {key}") 

546 return metadataStats 

547 

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

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

550 

551 Parameters 

552 ---------- 

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

554 The exposure to measure. 

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

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

557 values the string name associated with the 

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

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

560 Statistics control object with parameters defined by 

561 the config. 

562 failAll : `bool`, optional 

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

564 

565 Returns 

566 ------- 

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

568 A dictionary indexed by the amplifier name, containing 

569 dictionaries of the statistics measured and their values. 

570 """ 

571 ampStats = {} 

572 statisticToRun, statAccessor = self._configHelper(keywordDict) 

573 # Measure stats on all amplifiers. 

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

575 ampName = amp.getName() 

576 theseStats = {} 

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

578 stats = afwMath.makeStatistics( 

579 ampExp.getMaskedImage(), statisticToRun, statControl 

580 ) 

581 

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

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

584 

585 if failAll: 

586 theseStats["FORCE_FAILURE"] = failAll 

587 ampStats[ampName] = theseStats 

588 

589 return ampStats 

590 

591 def unmaskedImageStats(self, exposure): 

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

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

594 

595 Parameters 

596 ---------- 

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

598 The exposure to measure. 

599 

600 Returns 

601 ------- 

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

603 A dictionary indexed by the amplifier name, containing 

604 dictionaries of the statistics measured and their values. 

605 """ 

606 noMaskStatsControl = afwMath.StatisticsControl( 

607 self.config.numSigmaClip, self.config.clipMaxIter, 0x0 

608 ) 

609 return self.amplifierStats( 

610 exposure, self.config.unmaskedImageStatKeywords, noMaskStatsControl 

611 ) 

612 

613 def normalizedImageStats(self, exposure, statControl): 

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

615 by the exposure time. 

616 

617 Parameters 

618 ---------- 

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

620 The exposure to measure. 

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

622 Statistics control object with parameters defined by 

623 the config. 

624 

625 Returns 

626 ------- 

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

628 A dictionary indexed by the amplifier name, containing 

629 dictionaries of the statistics measured and their values. 

630 

631 Raises 

632 ------ 

633 RuntimeError : 

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

635 """ 

636 scaledExposure = exposure.clone() 

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

638 if exposureTime <= 0: 

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

640 mi = scaledExposure.getMaskedImage() 

641 mi /= exposureTime 

642 

643 return self.amplifierStats( 

644 scaledExposure, self.config.normImageStatKeywords, statControl 

645 ) 

646 

647 def crImageStats(self, exposure, statControl): 

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

649 after running cosmic ray rejection. 

650 

651 Parameters 

652 ---------- 

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

654 The exposure to measure. 

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

656 Statistics control object with parameters defined by 

657 the config. 

658 

659 Returns 

660 ------- 

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

662 A dictionary indexed by the amplifier name, containing 

663 dictionaries of the statistics measured and their values. 

664 

665 """ 

666 crRejectedExp = exposure.clone() 

667 psf = measAlg.SingleGaussianPsf( 

668 self.config.psfSize, 

669 self.config.psfSize, 

670 self.config.psfFwhm / (2 * np.sqrt(2 * np.log(2))), 

671 ) 

672 crRejectedExp.setPsf(psf) 

673 try: 

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

675 failAll = False 

676 except pexException.LengthError: 

677 self.log.warning( 

678 "Failure masking cosmic rays (too many found). Continuing." 

679 ) 

680 failAll = True 

681 

682 if self.config.crGrow > 0: 

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

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

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

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

687 spans.setMask(crRejectedExp.mask, crMask) 

688 

689 return self.amplifierStats( 

690 crRejectedExp, self.config.crImageStatKeywords, statControl, failAll=failAll 

691 ) 

692 

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

694 def catalogStatistics(self, exposure, catalog, uncorrectedCatalog, statControl): 

695 """Calculate statistics from a catalog. 

696 

697 Parameters 

698 ---------- 

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

700 The exposure to measure. 

701 catalog : `lsst.afw.table.Table` 

702 The catalog to measure. 

703 uncorrectedCatalog : `lsst.afw.table.Table` 

704 The alternate catalog to measure. 

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

706 Statistics control object with parameters defined by 

707 the config. 

708 

709 Returns 

710 ------- 

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

712 A dictionary indexed by the amplifier name, containing 

713 dictionaries of the statistics measured and their values. 

714 """ 

715 raise NotImplementedError( 

716 "Subclasses must implement catalog statistics method." 

717 ) 

718 

719 def detectorStatistics( 

720 self, statisticsDict, statControl, exposure=None, uncorrectedExposure=None 

721 ): 

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

723 per-amplifier measurements. 

724 

725 Parameters 

726 ---------- 

727 statisticsDict : `dict` [`str`, scalar] 

728 Dictionary of measured statistics. The inner dictionary 

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

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

731 the mostly likely types). 

732 statControl : `lsst.afw.math.StatControl` 

733 Statistics control object with parameters defined by 

734 the config. 

735 exposure : `lsst.afw.image.Exposure`, optional 

736 Exposure containing the ISR-processed data to measure. 

737 uncorrectedExposure : `lsst.afw.image.Exposure`, optional 

738 uncorrected esposure (no defects) containing the 

739 ISR-processed data to measure. 

740 

741 Returns 

742 ------- 

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

744 A dictionary of the statistics measured and their values. 

745 

746 Raises 

747 ------ 

748 NotImplementedError : 

749 This method must be implemented by the calibration-type 

750 subclass. 

751 """ 

752 raise NotImplementedError( 

753 "Subclasses must implement detector statistics method." 

754 ) 

755 

756 def verify(self, exposure, statisticsDict): 

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

758 

759 Parameters 

760 ---------- 

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

762 The exposure the statistics are from. 

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

764 Dictionary of measured statistics. The inner dictionary 

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

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

767 the mostly likely types). 

768 

769 Returns 

770 ------- 

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

772 A dictionary indexed by the amplifier name, containing 

773 dictionaries of the verification criteria. 

774 success : `bool` 

775 A boolean indicating whether all tests have passed. 

776 

777 Raises 

778 ------ 

779 NotImplementedError : 

780 This method must be implemented by the calibration-type 

781 subclass. 

782 """ 

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

784 

785 def repackStats(self, statisticsDict, dimensions): 

786 """Repack information into flat tables. 

787 

788 This method may be redefined in subclasses. This default 

789 version will repack simple amp-level statistics and 

790 verification results. 

791 

792 Parameters 

793 ---------- 

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

795 Dictionary of measured statistics. The inner dictionary 

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

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

798 the mostly likely types). 

799 dimensions : `dict` 

800 The dictionary of dimensions values for this data, to be 

801 included in the output results. 

802 

803 Returns 

804 ------- 

805 outputResults : `list` [`dict`] 

806 A list of rows to add to the output table. 

807 outputMatrix : `list` [`dict`] 

808 A list of rows to add to the output matrix. 

809 """ 

810 rows = {} 

811 rowList = [] 

812 matrixRowList = None 

813 

814 if self.config.useIsrStatistics: 

815 mjd = statisticsDict["ISR"]["MJD"] 

816 else: 

817 mjd = np.nan 

818 

819 rowBase = { 

820 "instrument": dimensions["instrument"], 

821 "detector": dimensions["detector"], 

822 "mjd": mjd, 

823 } 

824 

825 # AMP results: 

826 for ampName, stats in statisticsDict["AMP"].items(): 

827 rows[ampName] = {} 

828 rows[ampName].update(rowBase) 

829 rows[ampName]["amplifier"] = ampName 

830 for key, value in stats.items(): 

831 rows[ampName][f"{self.config.stageName}_{key}"] = value 

832 

833 # VERIFY results 

834 if "AMP" in statisticsDict["VERIFY"]: 

835 for ampName, stats in statisticsDict["VERIFY"]["AMP"].items(): 

836 for key, value in stats.items(): 

837 rows[ampName][f"{self.config.stageName}_VERIFY_{key}"] = value 

838 

839 # pack final list 

840 for ampName, stats in rows.items(): 

841 rowList.append(stats) 

842 

843 return rowList, matrixRowList