Coverage for python/lsst/cp/verify/verifyStats.py: 26%
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.cp.pipe.cpCombine import vignetteExposure
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 camera = cT.PrerequisiteInput(
55 name="camera",
56 storageClass="Camera",
57 doc="Input camera.",
58 dimensions=["instrument", ],
59 isCalibration=True,
60 )
62 outputStats = cT.Output(
63 name="detectorStats",
64 doc="Output statistics from cp_verify.",
65 storageClass="StructuredDataDict",
66 dimensions=["instrument", "exposure", "detector"],
67 )
69 def __init__(self, *, config=None):
70 super().__init__(config=config)
72 if len(config.metadataStatKeywords) < 1:
73 self.inputs.discard('taskMetadata')
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 )
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 )
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 )
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 )
184class CpVerifyStatsTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
185 """Main statistic measurement and validation class.
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'
194 def __init__(self, **kwargs):
195 super().__init__(**kwargs)
196 self.makeSubtask("repair")
198 def run(self, inputExp, camera, taskMetadata=None):
199 """Calculate quality statistics and verify they meet the requirements
200 for a calibration.
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``.
211 Returns
212 -------
213 result : `lsst.pipe.base.Struct`
214 Result struct with components:
215 - ``outputStats`` : `dict`
216 The output measured statistics.
218 Notes
219 -----
220 The outputStats should have a yaml representation of the form
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
247 """
248 outputStats = {}
250 if self.config.doVignette:
251 vignetteExposure(inputExp, doUpdateMask=True, maskPlane='NO_DATA',
252 doSetValue=False, log=self.log)
254 mask = inputExp.getMask()
255 maskVal = mask.getPlaneBitMask(self.config.maskNameList)
256 statControl = afwMath.StatisticsControl(self.config.numSigmaClip,
257 self.config.clipMaxIter,
258 maskVal)
260 # This is wrapped below to check for config lengths, as we can
261 # make a number of different image stats.
262 outputStats['AMP'] = self.imageStatistics(inputExp, statControl)
264 if len(self.config.metadataStatKeywords):
265 # These are also defined on a amp-by-amp basis.
266 outputStats['METADATA'] = self.metadataStatistics(inputExp, taskMetadata)
267 else:
268 outputStats['METADATA'] = {}
270 if len(self.config.catalogStatKeywords):
271 outputStats['CATALOG'] = self.catalogStatistics(inputExp, statControl)
272 else:
273 outputStats['CATALOG'] = {}
274 if len(self.config.detectorStatKeywords):
275 outputStats['DET'] = self.detectorStatistics(outputStats, statControl)
276 else:
277 outputStats['DET'] = {}
279 outputStats['VERIFY'], outputStats['SUCCESS'] = self.verify(inputExp, outputStats)
281 return pipeBase.Struct(
282 outputStats=outputStats,
283 )
285 @staticmethod
286 def _emptyAmpDict(exposure):
287 """Construct empty dictionary indexed by amplifier names.
289 Parameters
290 ----------
291 exposure : `lsst.afw.image.Exposure`
292 Exposure to extract detector from.
294 Returns
295 -------
296 outputStatistics : `dict` [`str`, `dict`]
297 A skeleton statistics dictionary.
299 Raises
300 ------
301 RuntimeError :
302 Raised if no detector can be found.
303 """
304 outputStatistics = {}
305 detector = exposure.getDetector()
306 if detector is None:
307 raise RuntimeError("No detector found in exposure!")
309 for amp in detector.getAmplifiers():
310 outputStatistics[amp.getName()] = {}
312 return outputStatistics
314 # Image measurement methods.
315 def imageStatistics(self, exposure, statControl):
316 """Measure image statistics for a number of simple image
317 modifications.
319 Parameters
320 ----------
321 exposure : `lsst.afw.image.Exposure`
322 Exposure containing the ISR processed data to measure.
323 statControl : `lsst.afw.math.StatisticsControl`
324 Statistics control object with parameters defined by
325 the config.
327 Returns
328 -------
329 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
330 A dictionary indexed by the amplifier name, containing
331 dictionaries of the statistics measured and their values.
333 """
334 outputStatistics = self._emptyAmpDict(exposure)
336 if len(self.config.imageStatKeywords):
337 outputStatistics = mergeStatDict(outputStatistics,
338 self.amplifierStats(exposure,
339 self.config.imageStatKeywords,
340 statControl))
341 if len(self.config.unmaskedImageStatKeywords):
342 outputStatistics = mergeStatDict(outputStatistics, self.unmaskedImageStats(exposure))
344 if len(self.config.normImageStatKeywords):
345 outputStatistics = mergeStatDict(outputStatistics,
346 self.normalizedImageStats(exposure, statControl))
348 if len(self.config.crImageStatKeywords):
349 outputStatistics = mergeStatDict(outputStatistics,
350 self.crImageStats(exposure, statControl))
352 return outputStatistics
354 @staticmethod
355 def _configHelper(keywordDict):
356 """Helper to convert keyword dictionary to stat value.
358 Convert the string names in the keywordDict to the afwMath values.
359 The statisticToRun is then the bitwise-or of that set.
361 Parameters
362 ----------
363 keywordDict : `dict` [`str`, `str`]
364 A dictionary of keys to use in the output results, with
365 values the string name associated with the
366 `lsst.afw.math.statistics.Property` to measure.
368 Returns
369 -------
370 statisticToRun : `int`
371 The merged `lsst.afw.math` statistics property.
372 statAccessor : `dict` [`str`, `int`]
373 Dictionary containing statistics property indexed by name.
374 """
375 statisticToRun = 0
376 statAccessor = {}
377 for k, v in keywordDict.items():
378 statValue = afwMath.stringToStatisticsProperty(v)
379 statisticToRun |= statValue
380 statAccessor[k] = statValue
382 return statisticToRun, statAccessor
384 def metadataStatistics(self, exposure, taskMetadata):
385 """Extract task metadata information for verification.
387 Parameters
388 ----------
389 exposure : `lsst.afw.image.Exposure`
390 The exposure to measure.
391 taskMetadata : `lsst.daf.base.PropertySet`
392 The metadata to extract values from.
394 Returns
395 -------
396 ampStats : `dict` [`str`, `dict` [`str`, scalar]]
397 A dictionary indexed by the amplifier name, containing
398 dictionaries of the statistics measured and their values.
399 """
400 metadataStats = {}
401 keywordDict = self.config.metadataStatKeywords
403 if taskMetadata:
404 for key, value in keywordDict.items():
405 if value == 'AMP':
406 metadataStats[key] = {}
407 for ampIdx, amp in enumerate(exposure.getDetector()):
408 ampName = amp.getName()
409 expectedKey = f"{key} {ampName}"
410 metadataStats[key][ampName] = None
411 for name in taskMetadata.names():
412 if expectedKey in taskMetadata[name]:
413 metadataStats[key][ampName] = taskMetadata[name][expectedKey]
414 else:
415 # Assume it's detector-wide.
416 expectedKey = key
417 for name in taskMetadata.names():
418 if expectedKey in taskMetadata[name]:
419 metadataStats[key] = taskMetadata[name][expectedKey]
420 return metadataStats
422 def amplifierStats(self, exposure, keywordDict, statControl, failAll=False):
423 """Measure amplifier level statistics from the exposure.
425 Parameters
426 ----------
427 exposure : `lsst.afw.image.Exposure`
428 The exposure to measure.
429 keywordDict : `dict` [`str`, `str`]
430 A dictionary of keys to use in the output results, with
431 values the string name associated with the
432 `lsst.afw.math.statistics.Property` to measure.
433 statControl : `lsst.afw.math.StatisticsControl`
434 Statistics control object with parameters defined by
435 the config.
436 failAll : `bool`, optional
437 If True, all tests will be set as failed.
439 Returns
440 -------
441 ampStats : `dict` [`str`, `dict` [`str`, scalar]]
442 A dictionary indexed by the amplifier name, containing
443 dictionaries of the statistics measured and their values.
444 """
445 ampStats = {}
447 statisticToRun, statAccessor = self._configHelper(keywordDict)
449 # Measure stats on all amplifiers.
450 for ampIdx, amp in enumerate(exposure.getDetector()):
451 ampName = amp.getName()
452 theseStats = {}
453 ampExp = exposure.Factory(exposure, amp.getBBox())
454 stats = afwMath.makeStatistics(ampExp.getMaskedImage(), statisticToRun, statControl)
456 for k, v in statAccessor.items():
457 theseStats[k] = stats.getValue(v)
459 if failAll:
460 theseStats['FORCE_FAILURE'] = failAll
461 ampStats[ampName] = theseStats
463 return ampStats
465 def unmaskedImageStats(self, exposure):
466 """Measure amplifier level statistics on the exposure, including all
467 pixels in the exposure, regardless of any mask planes set.
469 Parameters
470 ----------
471 exposure : `lsst.afw.image.Exposure`
472 The exposure to measure.
474 Returns
475 -------
476 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
477 A dictionary indexed by the amplifier name, containing
478 dictionaries of the statistics measured and their values.
479 """
480 noMaskStatsControl = afwMath.StatisticsControl(self.config.numSigmaClip,
481 self.config.clipMaxIter,
482 0x0)
483 return self.amplifierStats(exposure, self.config.unmaskedImageStatKeywords, noMaskStatsControl)
485 def normalizedImageStats(self, exposure, statControl):
486 """Measure amplifier level statistics on the exposure after dividing
487 by the exposure time.
489 Parameters
490 ----------
491 exposure : `lsst.afw.image.Exposure`
492 The exposure to measure.
493 statControl : `lsst.afw.math.StatisticsControl`
494 Statistics control object with parameters defined by
495 the config.
497 Returns
498 -------
499 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
500 A dictionary indexed by the amplifier name, containing
501 dictionaries of the statistics measured and their values.
503 Raises
504 ------
505 RuntimeError :
506 Raised if the exposure time cannot be used for normalization.
507 """
508 scaledExposure = exposure.clone()
509 exposureTime = scaledExposure.getInfo().getVisitInfo().getExposureTime()
510 if exposureTime <= 0:
511 raise RuntimeError(f"Invalid exposureTime {exposureTime}.")
512 mi = scaledExposure.getMaskedImage()
513 mi /= exposureTime
515 return self.amplifierStats(scaledExposure, self.config.normImageStatKeywords, statControl)
517 def crImageStats(self, exposure, statControl):
518 """Measure amplifier level statistics on the exposure,
519 after running cosmic ray rejection.
521 Parameters
522 ----------
523 exposure : `lsst.afw.image.Exposure`
524 The exposure to measure.
525 statControl : `lsst.afw.math.StatisticsControl`
526 Statistics control object with parameters defined by
527 the config.
529 Returns
530 -------
531 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
532 A dictionary indexed by the amplifier name, containing
533 dictionaries of the statistics measured and their values.
535 """
536 crRejectedExp = exposure.clone()
537 psf = measAlg.SingleGaussianPsf(self.config.psfSize,
538 self.config.psfSize,
539 self.config.psfFwhm/(2*math.sqrt(2*math.log(2))))
540 crRejectedExp.setPsf(psf)
541 try:
542 self.repair.run(crRejectedExp, keepCRs=False)
543 failAll = False
544 except pexException.LengthError:
545 self.log.warn("Failure masking cosmic rays (too many found). Continuing.")
546 failAll = True
548 if self.config.crGrow > 0:
549 crMask = crRejectedExp.getMaskedImage().getMask().getPlaneBitMask("CR")
550 spans = afwGeom.SpanSet.fromMask(crRejectedExp.mask, crMask)
551 spans = spans.dilated(self.config.crGrow)
552 spans = spans.clippedTo(crRejectedExp.getBBox())
553 spans.setMask(crRejectedExp.mask, crMask)
555 return self.amplifierStats(crRejectedExp, self.config.crImageStatKeywords,
556 statControl, failAll=failAll)
558 # Methods that need to be implemented by the calibration-level subclasses.
559 def catalogStatistics(self, exposure, statControl):
560 """Calculate statistics from a catalog.
562 Parameters
563 ----------
564 exposure : `lsst.afw.image.Exposure`
565 The exposure to measure.
566 statControl : `lsst.afw.math.StatisticsControl`
567 Statistics control object with parameters defined by
568 the config.
570 Returns
571 -------
572 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
573 A dictionary indexed by the amplifier name, containing
574 dictionaries of the statistics measured and their values.
575 """
576 raise NotImplementedError("Subclasses must implement catalog statistics method.")
578 def detectorStatistics(self, statisticsDict, statControl):
579 """Calculate detector level statistics based on the existing
580 per-amplifier measurements.
582 Parameters
583 ----------
584 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]],
585 Dictionary of measured statistics. The inner dictionary
586 should have keys that are statistic names (`str`) with
587 values that are some sort of scalar (`int` or `float` are
588 the mostly likely types).
590 Returns
591 -------
592 outputStatistics : `dict` [`str`, scalar]
593 A dictionary of the statistics measured and their values.
595 Raises
596 ------
597 NotImplementedError :
598 This method must be implemented by the calibration-type
599 subclass.
600 """
601 raise NotImplementedError("Subclasses must implement detector statistics method.")
603 def verify(self, exposure, statisticsDict):
604 """Verify that the measured statistics meet the verification criteria.
606 Parameters
607 ----------
608 exposure : `lsst.afw.image.Exposure`
609 The exposure the statistics are from.
610 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]],
611 Dictionary of measured statistics. The inner dictionary
612 should have keys that are statistic names (`str`) with
613 values that are some sort of scalar (`int` or `float` are
614 the mostly likely types).
616 Returns
617 -------
618 outputStatistics : `dict` [`str`, `dict` [`str`, `bool`]]
619 A dictionary indexed by the amplifier name, containing
620 dictionaries of the verification criteria.
621 success : `bool`
622 A boolean indicating whether all tests have passed.
624 Raises
625 ------
626 NotImplementedError :
627 This method must be implemented by the calibration-type
628 subclass.
629 """
630 raise NotImplementedError("Subclasses must implement verification criteria.")