Coverage for python / lsst / cp / verify / verifyStats.py: 23%
220 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 09:20 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-18 09:20 +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
23from astropy.table import Table
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
33from lsst.ip.isr.vignette import maskVignettedRegion
34from lsst.pipe.tasks.repair import RepairTask
35from .utils import mergeStatDict
38__all__ = ["CpVerifyStatsConfig", "CpVerifyStatsTask"]
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 )
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 )
111 def __init__(self, *, config=None):
112 super().__init__(config=config)
114 # TODO DM-45802: Remove deprecated taskMetadata connection.
115 self.inputs.discard("taskMetadata")
117 if len(config.catalogStatKeywords) < 1:
118 self.inputs.discard("inputCatalog")
119 self.inputs.discard("uncorrectedCatalog")
121 if len(config.uncorrectedImageStatKeywords) < 1:
122 self.inputs.discard("uncorrectedExp")
124 if config.useIsrStatistics is not True:
125 self.inputs.discard("isrStatistics")
127 if not config.hasMatrixCatalog:
128 self.outputs.discard("outputMatrix")
131class CpVerifyStatsConfig(
132 pipeBase.PipelineTaskConfig, pipelineConnections=CpVerifyStatsConnections
133):
134 """Configuration parameters for CpVerifyStatsTask."""
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 )
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 )
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 )
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 )
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 )
267class CpVerifyStatsTask(pipeBase.PipelineTask):
268 """Main statistic measurement and validation class.
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 """
275 ConfigClass = CpVerifyStatsConfig
276 _DefaultName = "cpVerifyStats"
278 def __init__(self, **kwargs):
279 super().__init__(**kwargs)
280 self.makeSubtask("repair")
282 def runQuantum(self, butlerQC, inputRefs, outputRefs):
283 inputs = butlerQC.get(inputRefs)
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)
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.
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.
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 = {}
330 if self.config.doVignette:
331 polygon = inputExp.getInfo().getValidPolygon()
332 maskVignettedRegion(
333 inputExp, polygon, maskPlane="NO_DATA", vignetteValue=None, log=self.log
334 )
336 mask = inputExp.getMask()
337 maskVal = mask.getPlaneBitMask(self.config.maskNameList)
338 statControl = afwMath.StatisticsControl(
339 self.config.numSigmaClip, self.config.clipMaxIter, maskVal
340 )
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)
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"] = {}
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"] = {}
365 if self.config.useIsrStatistics:
366 outputStats["ISR"] = isrStatistics
368 outputStats["VERIFY"], outputStats["SUCCESS"] = self.verify(
369 inputExp, outputStats
370 )
372 outputResults, outputMatrix = self.repackStats(outputStats, dimensions)
374 return pipeBase.Struct(
375 outputStats=outputStats,
376 outputResults=Table(outputResults),
377 outputMatrix=Table(outputMatrix),
378 )
380 @staticmethod
381 def _emptyAmpDict(exposure):
382 """Construct empty dictionary indexed by amplifier names.
384 Parameters
385 ----------
386 exposure : `lsst.afw.image.Exposure`
387 Exposure to extract detector from.
389 Returns
390 -------
391 outputStatistics : `dict` [`str`, `dict`]
392 A skeleton statistics dictionary.
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!")
404 for amp in detector.getAmplifiers():
405 outputStatistics[amp.getName()] = {}
407 return outputStatistics
409 # Image measurement methods.
410 def imageStatistics(self, exposure, uncorrectedExposure, statControl):
411 """Measure image statistics for a number of simple image
412 modifications.
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.
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.
430 """
431 outputStatistics = self._emptyAmpDict(exposure)
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 )
454 if len(self.config.normImageStatKeywords):
455 outputStatistics = mergeStatDict(
456 outputStatistics, self.normalizedImageStats(exposure, statControl)
457 )
459 if len(self.config.crImageStatKeywords):
460 outputStatistics = mergeStatDict(
461 outputStatistics, self.crImageStats(exposure, statControl)
462 )
464 return outputStatistics
466 @staticmethod
467 def _configHelper(keywordDict):
468 """Helper to convert keyword dictionary to stat value.
470 Convert the string names in the keywordDict to the afwMath values.
471 The statisticToRun is then the bitwise-or of that set.
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.
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
494 return statisticToRun, statAccessor
496 def metadataStatistics(self, exposure, taskMetadata=None):
497 """Extract task metadata information for verification.
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.
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
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
544 if not found:
545 self.log.debug(f"Could not find expected key: {key}")
546 return metadataStats
548 def amplifierStats(self, exposure, keywordDict, statControl, failAll=False):
549 """Measure amplifier level statistics from the exposure.
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.
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 )
582 for k, v in statAccessor.items():
583 theseStats[k] = stats.getValue(v)
585 if failAll:
586 theseStats["FORCE_FAILURE"] = failAll
587 ampStats[ampName] = theseStats
589 return ampStats
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.
595 Parameters
596 ----------
597 exposure : `lsst.afw.image.Exposure`
598 The exposure to measure.
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 )
613 def normalizedImageStats(self, exposure, statControl):
614 """Measure amplifier level statistics on the exposure after dividing
615 by the exposure time.
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.
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.
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
643 return self.amplifierStats(
644 scaledExposure, self.config.normImageStatKeywords, statControl
645 )
647 def crImageStats(self, exposure, statControl):
648 """Measure amplifier level statistics on the exposure,
649 after running cosmic ray rejection.
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.
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.
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
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)
689 return self.amplifierStats(
690 crRejectedExp, self.config.crImageStatKeywords, statControl, failAll=failAll
691 )
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.
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.
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 )
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.
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.
741 Returns
742 -------
743 outputStatistics : `dict` [`str`, scalar]
744 A dictionary of the statistics measured and their values.
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 )
756 def verify(self, exposure, statisticsDict):
757 """Verify that the measured statistics meet the verification criteria.
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).
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.
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.")
785 def repackStats(self, statisticsDict, dimensions):
786 """Repack information into flat tables.
788 This method may be redefined in subclasses. This default
789 version will repack simple amp-level statistics and
790 verification results.
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.
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
814 if self.config.useIsrStatistics:
815 mjd = statisticsDict["ISR"]["MJD"]
816 else:
817 mjd = np.nan
819 rowBase = {
820 "instrument": dimensions["instrument"],
821 "detector": dimensions["detector"],
822 "mjd": mjd,
823 }
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
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
839 # pack final list
840 for ampName, stats in rows.items():
841 rowList.append(stats)
843 return rowList, matrixRowList