Coverage for python/lsst/cp/verify/verifyStats.py: 29%
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
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
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
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
31from lsst.ip.isr.vignette import maskVignettedRegion
32from lsst.pipe.tasks.repair import RepairTask
33from .utils import mergeStatDict
36__all__ = ['CpVerifyStatsConfig', 'CpVerifyStatsTask']
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 inputCatalog = cT.Input(
55 name="src",
56 doc="Input catalog to calculate statistics for.",
57 storageClass="SourceCatalog",
58 dimensions=["instrument", "visit", "detector"],
59 )
60 uncorrectedCatalog = cT.Input(
61 name="uncorrectedSrc",
62 doc="Input catalog without correction applied.",
63 storageClass="SourceCatalog",
64 dimensions=["instrument", "visit", "detector"],
65 )
66 camera = cT.PrerequisiteInput(
67 name="camera",
68 storageClass="Camera",
69 doc="Input camera.",
70 dimensions=["instrument", ],
71 isCalibration=True,
72 )
74 outputStats = cT.Output(
75 name="detectorStats",
76 doc="Output statistics from cp_verify.",
77 storageClass="StructuredDataDict",
78 dimensions=["instrument", "exposure", "detector"],
79 )
81 def __init__(self, *, config=None):
82 super().__init__(config=config)
84 if len(config.metadataStatKeywords) < 1:
85 self.inputs.discard('taskMetadata')
88class CpVerifyStatsConfig(pipeBase.PipelineTaskConfig,
89 pipelineConnections=CpVerifyStatsConnections):
90 """Configuration parameters for CpVerifyStatsTask.
91 """
92 maskNameList = pexConfig.ListField(
93 dtype=str,
94 doc="Mask list to exclude from statistics calculations.",
95 default=['DETECTED', 'BAD', 'NO_DATA'],
96 )
97 doVignette = pexConfig.Field(
98 dtype=bool,
99 doc="Mask vignetted regions?",
100 default=False,
101 )
102 doNormalize = pexConfig.Field(
103 dtype=bool,
104 doc="Normalize by exposure time?",
105 default=False,
106 )
108 # Cosmic ray handling options.
109 doCR = pexConfig.Field(
110 dtype=bool,
111 doc="Run CR rejection?",
112 default=False,
113 )
114 repair = pexConfig.ConfigurableField(
115 target=RepairTask,
116 doc="Repair task to use.",
117 )
118 psfFwhm = pexConfig.Field(
119 dtype=float,
120 default=3.0,
121 doc="Repair PSF FWHM (pixels).",
122 )
123 psfSize = pexConfig.Field(
124 dtype=int,
125 default=21,
126 doc="Repair PSF bounding-box size (pixels).",
127 )
128 crGrow = pexConfig.Field(
129 dtype=int,
130 default=0,
131 doc="Grow radius for CR (pixels).",
132 )
134 # Statistics options.
135 useReadNoise = pexConfig.Field(
136 dtype=bool,
137 doc="Compare sigma against read noise?",
138 default=True,
139 )
140 numSigmaClip = pexConfig.Field(
141 dtype=float,
142 doc="Rejection threshold (sigma) for statistics clipping.",
143 default=5.0,
144 )
145 clipMaxIter = pexConfig.Field(
146 dtype=int,
147 doc="Max number of clipping iterations to apply.",
148 default=3,
149 )
151 # Keywords and statistics to measure from different sources.
152 imageStatKeywords = pexConfig.DictField(
153 keytype=str,
154 itemtype=str,
155 doc="Image statistics to run on amplifier segments.",
156 default={},
157 )
158 unmaskedImageStatKeywords = pexConfig.DictField(
159 keytype=str,
160 itemtype=str,
161 doc="Image statistics to run on amplifier segments, ignoring masks.",
162 default={},
163 )
164 crImageStatKeywords = pexConfig.DictField(
165 keytype=str,
166 itemtype=str,
167 doc="Image statistics to run on CR cleaned amplifier segments.",
168 default={},
169 )
170 normImageStatKeywords = pexConfig.DictField(
171 keytype=str,
172 itemtype=str,
173 doc="Image statistics to run on expTime normalized amplifier segments.",
174 default={},
175 )
176 metadataStatKeywords = pexConfig.DictField(
177 keytype=str,
178 itemtype=str,
179 doc="Statistics to measure from the metadata of the exposure.",
180 default={},
181 )
182 catalogStatKeywords = pexConfig.DictField(
183 keytype=str,
184 itemtype=str,
185 doc="Statistics to measure from source catalogs of objects in the exposure.",
186 default={},
187 )
188 detectorStatKeywords = pexConfig.DictField(
189 keytype=str,
190 itemtype=str,
191 doc="Statistics to create for the full detector from the per-amplifier measurements.",
192 default={},
193 )
196class CpVerifyStatsTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
197 """Main statistic measurement and validation class.
199 This operates on a single (exposure, detector) pair, and is
200 designed to be subclassed so specific calibrations can apply their
201 own validation methods.
202 """
203 ConfigClass = CpVerifyStatsConfig
204 _DefaultName = 'cpVerifyStats'
206 def __init__(self, **kwargs):
207 super().__init__(**kwargs)
208 self.makeSubtask("repair")
210 def run(self, inputExp, camera, taskMetadata=None, inputCatalog=None, uncorrectedCatalog=None):
211 """Calculate quality statistics and verify they meet the requirements
212 for a calibration.
214 Parameters
215 ----------
216 inputExp : `lsst.afw.image.Exposure`
217 The ISR processed exposure to be measured.
218 camera : `lsst.afw.cameraGeom.Camera`
219 The camera geometry for ``inputExp``.
220 taskMetadata : `lsst.daf.base.PropertySet`, optional
221 Task metadata containing additional statistics.
222 inputCatalog : `lsst.afw.image.Table`
223 The source catalog to measure.
224 uncorrectedCatalog : `lsst.afw.image.Table`
225 The alternate source catalog to measure.
227 Returns
228 -------
229 result : `lsst.pipe.base.Struct`
230 Result struct with components:
231 - ``outputStats`` : `dict`
232 The output measured statistics.
234 Notes
235 -----
236 The outputStats should have a yaml representation of the form
238 AMP:
239 Amp1:
240 STAT: value
241 STAT2: value2
242 Amp2:
243 Amp3:
244 DET:
245 STAT: value
246 STAT2: value
247 CATALOG:
248 STAT: value
249 STAT2: value
250 VERIFY:
251 DET:
252 TEST: boolean
253 CATALOG:
254 TEST: boolean
255 AMP:
256 Amp1:
257 TEST: boolean
258 TEST2: boolean
259 Amp2:
260 Amp3:
261 SUCCESS: boolean
263 """
264 outputStats = {}
266 if self.config.doVignette:
267 polygon = inputExp.getInfo().getValidPolygon()
268 maskVignettedRegion(inputExp, polygon, maskPlane='NO_DATA',
269 vignetteValue=None, log=self.log)
271 mask = inputExp.getMask()
272 maskVal = mask.getPlaneBitMask(self.config.maskNameList)
273 statControl = afwMath.StatisticsControl(self.config.numSigmaClip,
274 self.config.clipMaxIter,
275 maskVal)
277 # This is wrapped below to check for config lengths, as we can
278 # make a number of different image stats.
279 outputStats['AMP'] = self.imageStatistics(inputExp, statControl)
281 if len(self.config.metadataStatKeywords):
282 # These are also defined on a amp-by-amp basis.
283 outputStats['METADATA'] = self.metadataStatistics(inputExp, taskMetadata)
284 else:
285 outputStats['METADATA'] = {}
287 if len(self.config.catalogStatKeywords):
288 outputStats['CATALOG'] = self.catalogStatistics(inputExp, inputCatalog, uncorrectedCatalog,
289 statControl)
290 else:
291 outputStats['CATALOG'] = {}
292 if len(self.config.detectorStatKeywords):
293 outputStats['DET'] = self.detectorStatistics(outputStats, statControl)
294 else:
295 outputStats['DET'] = {}
297 outputStats['VERIFY'], outputStats['SUCCESS'] = self.verify(inputExp, outputStats)
299 return pipeBase.Struct(
300 outputStats=outputStats,
301 )
303 @staticmethod
304 def _emptyAmpDict(exposure):
305 """Construct empty dictionary indexed by amplifier names.
307 Parameters
308 ----------
309 exposure : `lsst.afw.image.Exposure`
310 Exposure to extract detector from.
312 Returns
313 -------
314 outputStatistics : `dict` [`str`, `dict`]
315 A skeleton statistics dictionary.
317 Raises
318 ------
319 RuntimeError :
320 Raised if no detector can be found.
321 """
322 outputStatistics = {}
323 detector = exposure.getDetector()
324 if detector is None:
325 raise RuntimeError("No detector found in exposure!")
327 for amp in detector.getAmplifiers():
328 outputStatistics[amp.getName()] = {}
330 return outputStatistics
332 # Image measurement methods.
333 def imageStatistics(self, exposure, statControl):
334 """Measure image statistics for a number of simple image
335 modifications.
337 Parameters
338 ----------
339 exposure : `lsst.afw.image.Exposure`
340 Exposure containing the ISR processed data to measure.
341 statControl : `lsst.afw.math.StatisticsControl`
342 Statistics control object with parameters defined by
343 the config.
345 Returns
346 -------
347 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
348 A dictionary indexed by the amplifier name, containing
349 dictionaries of the statistics measured and their values.
351 """
352 outputStatistics = self._emptyAmpDict(exposure)
354 if len(self.config.imageStatKeywords):
355 outputStatistics = mergeStatDict(outputStatistics,
356 self.amplifierStats(exposure,
357 self.config.imageStatKeywords,
358 statControl))
359 if len(self.config.unmaskedImageStatKeywords):
360 outputStatistics = mergeStatDict(outputStatistics, self.unmaskedImageStats(exposure))
362 if len(self.config.normImageStatKeywords):
363 outputStatistics = mergeStatDict(outputStatistics,
364 self.normalizedImageStats(exposure, statControl))
366 if len(self.config.crImageStatKeywords):
367 outputStatistics = mergeStatDict(outputStatistics,
368 self.crImageStats(exposure, statControl))
370 return outputStatistics
372 @staticmethod
373 def _configHelper(keywordDict):
374 """Helper to convert keyword dictionary to stat value.
376 Convert the string names in the keywordDict to the afwMath values.
377 The statisticToRun is then the bitwise-or of that set.
379 Parameters
380 ----------
381 keywordDict : `dict` [`str`, `str`]
382 A dictionary of keys to use in the output results, with
383 values the string name associated with the
384 `lsst.afw.math.statistics.Property` to measure.
386 Returns
387 -------
388 statisticToRun : `int`
389 The merged `lsst.afw.math` statistics property.
390 statAccessor : `dict` [`str`, `int`]
391 Dictionary containing statistics property indexed by name.
392 """
393 statisticToRun = 0
394 statAccessor = {}
395 for k, v in keywordDict.items():
396 statValue = afwMath.stringToStatisticsProperty(v)
397 statisticToRun |= statValue
398 statAccessor[k] = statValue
400 return statisticToRun, statAccessor
402 def metadataStatistics(self, exposure, taskMetadata):
403 """Extract task metadata information for verification.
405 Parameters
406 ----------
407 exposure : `lsst.afw.image.Exposure`
408 The exposure to measure.
409 taskMetadata : `lsst.daf.base.PropertySet`
410 The metadata to extract values from.
412 Returns
413 -------
414 ampStats : `dict` [`str`, `dict` [`str`, scalar]]
415 A dictionary indexed by the amplifier name, containing
416 dictionaries of the statistics measured and their values.
417 """
418 metadataStats = {}
419 keywordDict = self.config.metadataStatKeywords
421 if taskMetadata:
422 for key, value in keywordDict.items():
423 if value == 'AMP':
424 metadataStats[key] = {}
425 for ampIdx, amp in enumerate(exposure.getDetector()):
426 ampName = amp.getName()
427 expectedKey = f"{key} {ampName}"
428 metadataStats[key][ampName] = None
429 for name in taskMetadata:
430 if expectedKey in taskMetadata[name]:
431 metadataStats[key][ampName] = taskMetadata[name][expectedKey]
432 else:
433 # Assume it's detector-wide.
434 expectedKey = key
435 for name in taskMetadata:
436 if expectedKey in taskMetadata[name]:
437 metadataStats[key] = taskMetadata[name][expectedKey]
438 return metadataStats
440 def amplifierStats(self, exposure, keywordDict, statControl, failAll=False):
441 """Measure amplifier level statistics from the exposure.
443 Parameters
444 ----------
445 exposure : `lsst.afw.image.Exposure`
446 The exposure to measure.
447 keywordDict : `dict` [`str`, `str`]
448 A dictionary of keys to use in the output results, with
449 values the string name associated with the
450 `lsst.afw.math.statistics.Property` to measure.
451 statControl : `lsst.afw.math.StatisticsControl`
452 Statistics control object with parameters defined by
453 the config.
454 failAll : `bool`, optional
455 If True, all tests will be set as failed.
457 Returns
458 -------
459 ampStats : `dict` [`str`, `dict` [`str`, scalar]]
460 A dictionary indexed by the amplifier name, containing
461 dictionaries of the statistics measured and their values.
462 """
463 ampStats = {}
465 statisticToRun, statAccessor = self._configHelper(keywordDict)
467 # Measure stats on all amplifiers.
468 for ampIdx, amp in enumerate(exposure.getDetector()):
469 ampName = amp.getName()
470 theseStats = {}
471 ampExp = exposure.Factory(exposure, amp.getBBox())
472 stats = afwMath.makeStatistics(ampExp.getMaskedImage(), statisticToRun, statControl)
474 for k, v in statAccessor.items():
475 theseStats[k] = stats.getValue(v)
477 if failAll:
478 theseStats['FORCE_FAILURE'] = failAll
479 ampStats[ampName] = theseStats
481 return ampStats
483 def unmaskedImageStats(self, exposure):
484 """Measure amplifier level statistics on the exposure, including all
485 pixels in the exposure, regardless of any mask planes set.
487 Parameters
488 ----------
489 exposure : `lsst.afw.image.Exposure`
490 The exposure to measure.
492 Returns
493 -------
494 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
495 A dictionary indexed by the amplifier name, containing
496 dictionaries of the statistics measured and their values.
497 """
498 noMaskStatsControl = afwMath.StatisticsControl(self.config.numSigmaClip,
499 self.config.clipMaxIter,
500 0x0)
501 return self.amplifierStats(exposure, self.config.unmaskedImageStatKeywords, noMaskStatsControl)
503 def normalizedImageStats(self, exposure, statControl):
504 """Measure amplifier level statistics on the exposure after dividing
505 by the exposure time.
507 Parameters
508 ----------
509 exposure : `lsst.afw.image.Exposure`
510 The exposure to measure.
511 statControl : `lsst.afw.math.StatisticsControl`
512 Statistics control object with parameters defined by
513 the config.
515 Returns
516 -------
517 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
518 A dictionary indexed by the amplifier name, containing
519 dictionaries of the statistics measured and their values.
521 Raises
522 ------
523 RuntimeError :
524 Raised if the exposure time cannot be used for normalization.
525 """
526 scaledExposure = exposure.clone()
527 exposureTime = scaledExposure.getInfo().getVisitInfo().getExposureTime()
528 if exposureTime <= 0:
529 raise RuntimeError(f"Invalid exposureTime {exposureTime}.")
530 mi = scaledExposure.getMaskedImage()
531 mi /= exposureTime
533 return self.amplifierStats(scaledExposure, self.config.normImageStatKeywords, statControl)
535 def crImageStats(self, exposure, statControl):
536 """Measure amplifier level statistics on the exposure,
537 after running cosmic ray rejection.
539 Parameters
540 ----------
541 exposure : `lsst.afw.image.Exposure`
542 The exposure to measure.
543 statControl : `lsst.afw.math.StatisticsControl`
544 Statistics control object with parameters defined by
545 the config.
547 Returns
548 -------
549 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
550 A dictionary indexed by the amplifier name, containing
551 dictionaries of the statistics measured and their values.
553 """
554 crRejectedExp = exposure.clone()
555 psf = measAlg.SingleGaussianPsf(self.config.psfSize,
556 self.config.psfSize,
557 self.config.psfFwhm/(2*math.sqrt(2*math.log(2))))
558 crRejectedExp.setPsf(psf)
559 try:
560 self.repair.run(crRejectedExp, keepCRs=False)
561 failAll = False
562 except pexException.LengthError:
563 self.log.warning("Failure masking cosmic rays (too many found). Continuing.")
564 failAll = True
566 if self.config.crGrow > 0:
567 crMask = crRejectedExp.getMaskedImage().getMask().getPlaneBitMask("CR")
568 spans = afwGeom.SpanSet.fromMask(crRejectedExp.mask, crMask)
569 spans = spans.dilated(self.config.crGrow)
570 spans = spans.clippedTo(crRejectedExp.getBBox())
571 spans.setMask(crRejectedExp.mask, crMask)
573 return self.amplifierStats(crRejectedExp, self.config.crImageStatKeywords,
574 statControl, failAll=failAll)
576 # Methods that need to be implemented by the calibration-level subclasses.
577 def catalogStatistics(self, exposure, catalog, uncorrectedCatalog, statControl):
578 """Calculate statistics from a catalog.
580 Parameters
581 ----------
582 exposure : `lsst.afw.image.Exposure`
583 The exposure to measure.
584 catalog : `lsst.afw.table.Table`
585 The catalog to measure.
586 uncorrectedCatalog : `lsst.afw.table.Table`
587 The alternate catalog to measure.
588 statControl : `lsst.afw.math.StatisticsControl`
589 Statistics control object with parameters defined by
590 the config.
592 Returns
593 -------
594 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
595 A dictionary indexed by the amplifier name, containing
596 dictionaries of the statistics measured and their values.
597 """
598 raise NotImplementedError("Subclasses must implement catalog statistics method.")
600 def detectorStatistics(self, statisticsDict, statControl):
601 """Calculate detector level statistics based on the existing
602 per-amplifier measurements.
604 Parameters
605 ----------
606 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]],
607 Dictionary of measured statistics. The inner dictionary
608 should have keys that are statistic names (`str`) with
609 values that are some sort of scalar (`int` or `float` are
610 the mostly likely types).
612 Returns
613 -------
614 outputStatistics : `dict` [`str`, scalar]
615 A dictionary of the statistics measured and their values.
617 Raises
618 ------
619 NotImplementedError :
620 This method must be implemented by the calibration-type
621 subclass.
622 """
623 raise NotImplementedError("Subclasses must implement detector statistics method.")
625 def verify(self, exposure, statisticsDict):
626 """Verify that the measured statistics meet the verification criteria.
628 Parameters
629 ----------
630 exposure : `lsst.afw.image.Exposure`
631 The exposure the statistics are from.
632 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]],
633 Dictionary of measured statistics. The inner dictionary
634 should have keys that are statistic names (`str`) with
635 values that are some sort of scalar (`int` or `float` are
636 the mostly likely types).
638 Returns
639 -------
640 outputStatistics : `dict` [`str`, `dict` [`str`, `bool`]]
641 A dictionary indexed by the amplifier name, containing
642 dictionaries of the verification criteria.
643 success : `bool`
644 A boolean indicating whether all tests have passed.
646 Raises
647 ------
648 NotImplementedError :
649 This method must be implemented by the calibration-type
650 subclass.
651 """
652 raise NotImplementedError("Subclasses must implement verification criteria.")