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

Hot-keys 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
35__all__ = ['CpVerifyStatsConfig', 'CpVerifyStatsTask']
38class CpVerifyStatsConnections(pipeBase.PipelineTaskConnections,
39 dimensions={"instrument", "exposure", "detector"},
40 defaultTemplates={}):
41 inputExp = cT.Input(
42 name="postISRCCD",
43 doc="Input exposure to calculate statistics for.",
44 storageClass="Exposure",
45 dimensions=["instrument", "exposure", "detector"],
46 )
47 camera = cT.PrerequisiteInput(
48 name="camera",
49 storageClass="Camera",
50 doc="Input camera.",
51 dimensions=["instrument", ],
52 isCalibration=True,
53 )
55 outputStats = cT.Output(
56 name="detectorStats",
57 doc="Output statistics from cp_verify.",
58 storageClass="StructuredDataDict",
59 dimensions=["instrument", "exposure", "detector"],
60 )
63class CpVerifyStatsConfig(pipeBase.PipelineTaskConfig,
64 pipelineConnections=CpVerifyStatsConnections):
65 """Configuration parameters for CpVerifyStatsTask.
66 """
67 maskNameList = pexConfig.ListField(
68 dtype=str,
69 doc="Mask list to exclude from statistics calculations.",
70 default=['DETECTED', 'BAD', 'NO_DATA'],
71 )
72 doVignette = pexConfig.Field(
73 dtype=bool,
74 doc="Mask vignetted regions?",
75 default=False,
76 )
77 doNormalize = pexConfig.Field(
78 dtype=bool,
79 doc="Normalize by exposure time?",
80 default=False,
81 )
83 # Cosmic ray handling options.
84 doCR = pexConfig.Field(
85 dtype=bool,
86 doc="Run CR rejection?",
87 default=False,
88 )
89 repair = pexConfig.ConfigurableField(
90 target=RepairTask,
91 doc="Repair task to use.",
92 )
93 psfFwhm = pexConfig.Field(
94 dtype=float,
95 default=3.0,
96 doc="Repair PSF FWHM (pixels).",
97 )
98 psfSize = pexConfig.Field(
99 dtype=int,
100 default=21,
101 doc="Repair PSF bounding-box size (pixels).",
102 )
103 crGrow = pexConfig.Field(
104 dtype=int,
105 default=0,
106 doc="Grow radius for CR (pixels).",
107 )
109 # Statistics options.
110 useReadNoise = pexConfig.Field(
111 dtype=bool,
112 doc="Compare sigma against read noise?",
113 default=True,
114 )
115 numSigmaClip = pexConfig.Field(
116 dtype=float,
117 doc="Rejection threshold (sigma) for statistics clipping.",
118 default=5.0,
119 )
120 clipMaxIter = pexConfig.Field(
121 dtype=int,
122 doc="Max number of clipping iterations to apply.",
123 default=3,
124 )
126 # Keywords and statistics to measure from different sources.
127 imageStatKeywords = pexConfig.DictField(
128 keytype=str,
129 itemtype=str,
130 doc="Image statistics to run on amplifier segments.",
131 default={},
132 )
133 unmaskedImageStatKeywords = pexConfig.DictField(
134 keytype=str,
135 itemtype=str,
136 doc="Image statistics to run on amplifier segments, ignoring masks.",
137 default={},
138 )
139 crImageStatKeywords = pexConfig.DictField(
140 keytype=str,
141 itemtype=str,
142 doc="Image statistics to run on CR cleaned amplifier segments.",
143 default={},
144 )
145 normImageStatKeywords = pexConfig.DictField(
146 keytype=str,
147 itemtype=str,
148 doc="Image statistics to run on expTime normalized amplifier segments.",
149 default={},
150 )
151 catalogStatKeywords = pexConfig.DictField(
152 keytype=str,
153 itemtype=str,
154 doc="Statistics to measure from source catalogs of objects in the exposure.",
155 default={},
156 )
157 detectorStatKeywords = pexConfig.DictField(
158 keytype=str,
159 itemtype=str,
160 doc="Statistics to create for the full detector from the per-amplifier measurements.",
161 default={},
162 )
165class CpVerifyStatsTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
166 """Main statistic measurement and validation class.
168 This operates on a single (exposure, detector) pair, and is
169 designed to be subclassed so specific calibrations can apply their
170 own validation methods.
171 """
172 ConfigClass = CpVerifyStatsConfig
173 _DefaultName = 'cpVerifyStats'
175 def __init__(self, **kwargs):
176 super().__init__(**kwargs)
177 self.makeSubtask("repair")
179 def run(self, inputExp, camera):
180 """Calculate quality statistics and verify they meet the requirements
181 for a calibration.
183 Parameters
184 ----------
185 inputExp : `lsst.afw.image.Exposure`
186 The ISR processed exposure to be measured.
187 camera : `lsst.afw.cameraGeom.Camera`
188 The camera geometry for ``inputExp``.
190 Returns
191 -------
192 result : `lsst.pipe.base.Struct`
193 Result struct with components:
194 - ``outputStats`` : `dict`
195 The output measured statistics.
197 Notes
198 -----
199 The outputStats should have a yaml representation of the form
201 AMP:
202 Amp1:
203 STAT: value
204 STAT2: value2
205 Amp2:
206 Amp3:
207 DET:
208 STAT: value
209 STAT2: value
210 CATALOG:
211 STAT: value
212 STAT2: value
213 VERIFY:
214 DET:
215 TEST: boolean
216 CATALOG:
217 TEST: boolean
218 AMP:
219 Amp1:
220 TEST: boolean
221 TEST2: boolean
222 Amp2:
223 Amp3:
224 SUCCESS: boolean
226 """
227 outputStats = {}
229 if self.config.doVignette:
230 vignetteExposure(inputExp, doUpdateMask=True, maskPlane='NO_DATA',
231 doSetValue=False, log=self.log)
233 mask = inputExp.getMask()
234 maskVal = mask.getPlaneBitMask(self.config.maskNameList)
235 statControl = afwMath.StatisticsControl(self.config.numSigmaClip,
236 self.config.clipMaxIter,
237 maskVal)
239 # This is wrapped below to check for config lengths, as we can
240 # make a number of different image stats.
241 outputStats['AMP'] = self.imageStatistics(inputExp, statControl)
242 if len(self.config.catalogStatKeywords):
243 outputStats['CATALOG'] = self.catalogStatistics(inputExp, statControl)
244 else:
245 outputStats['CATALOG'] = {}
246 if len(self.config.detectorStatKeywords):
247 outputStats['DET'] = self.detectorStatistics(outputStats, statControl)
248 else:
249 outputStats['DET'] = {}
251 outputStats['VERIFY'], outputStats['SUCCESS'] = self.verify(inputExp, outputStats)
253 return pipeBase.Struct(
254 outputStats=outputStats,
255 )
257 @staticmethod
258 def _emptyAmpDict(exposure):
259 """Construct empty dictionary indexed by amplifier names.
261 Parameters
262 ----------
263 exposure : `lsst.afw.image.Exposure`
264 Exposure to extract detector from.
266 Returns
267 -------
268 outputStatistics : `dict` [`str`, `dict`]
269 A skeleton statistics dictionary.
271 Raises
272 ------
273 RuntimeError :
274 Raised if no detector can be found.
275 """
276 outputStatistics = {}
277 detector = exposure.getDetector()
278 if detector is None:
279 raise RuntimeError("No detector found in exposure!")
281 for amp in detector.getAmplifiers():
282 outputStatistics[amp.getName()] = {}
284 return outputStatistics
286 # Image measurement methods.
287 def imageStatistics(self, exposure, statControl):
288 """Measure image statistics for a number of simple image
289 modifications.
291 Parameters
292 ----------
293 exposure : `lsst.afw.image.Exposure`
294 Exposure containing the ISR processed data to measure.
295 statControl : `lsst.afw.math.StatisticsControl`
296 Statistics control object with parameters defined by
297 the config.
299 Returns
300 -------
301 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
302 A dictionary indexed by the amplifier name, containing
303 dictionaries of the statistics measured and their values.
305 """
306 outputStatistics = self._emptyAmpDict(exposure)
308 if len(self.config.imageStatKeywords):
309 outputStatistics = mergeStatDict(outputStatistics,
310 self.amplifierStats(exposure,
311 self.config.imageStatKeywords,
312 statControl))
314 if len(self.config.unmaskedImageStatKeywords):
315 outputStatistics = mergeStatDict(outputStatistics, self.unmaskedImageStats(exposure))
317 if len(self.config.normImageStatKeywords):
318 outputStatistics = mergeStatDict(outputStatistics,
319 self.normalizedImageStats(exposure, statControl))
321 if len(self.config.crImageStatKeywords):
322 outputStatistics = mergeStatDict(outputStatistics,
323 self.crImageStats(exposure, statControl))
325 return outputStatistics
327 @staticmethod
328 def _configHelper(keywordDict):
329 """Helper to convert keyword dictionary to stat value.
331 Convert the string names in the keywordDict to the afwMath values.
332 The statisticToRun is then the bitwise-or of that set.
334 Parameters
335 ----------
336 keywordDict : `dict` [`str`, `str`]
337 A dictionary of keys to use in the output results, with
338 values the string name associated with the
339 `lsst.afw.math.statistics.Property` to measure.
341 Returns
342 -------
343 statisticToRun : `int`
344 The merged `lsst.afw.math` statistics property.
345 statAccessor : `dict` [`str`, `int`]
346 Dictionary containing statistics property indexed by name.
347 """
348 statisticToRun = 0
349 statAccessor = {}
350 for k, v in keywordDict.items():
351 statValue = afwMath.stringToStatisticsProperty(v)
352 statisticToRun |= statValue
353 statAccessor[k] = statValue
355 return statisticToRun, statAccessor
357 def amplifierStats(self, exposure, keywordDict, statControl, failAll=False):
358 """Measure amplifier level statistics from the exposure.
360 Parameters
361 ----------
362 exposure : `lsst.afw.image.Exposure`
363 The exposure to measure.
364 keywordDict : `dict` [`str`, `str`]
365 A dictionary of keys to use in the output results, with
366 values the string name associated with the
367 `lsst.afw.math.statistics.Property` to measure.
368 statControl : `lsst.afw.math.StatisticsControl`
369 Statistics control object with parameters defined by
370 the config.
371 failAll : `bool`, optional
372 If True, all tests will be set as failed.
374 Returns
375 -------
376 ampStats : `dict` [`str`, `dict` [`str`, scalar]]
377 A dictionary indexed by the amplifier name, containing
378 dictionaries of the statistics measured and their values.
379 """
380 ampStats = {}
382 statisticToRun, statAccessor = self._configHelper(keywordDict)
384 # Measure stats on all amplifiers.
385 for ampIdx, amp in enumerate(exposure.getDetector()):
386 ampName = amp.getName()
387 theseStats = {}
388 ampExp = exposure.Factory(exposure, amp.getBBox())
389 stats = afwMath.makeStatistics(ampExp.getMaskedImage(), statisticToRun, statControl)
391 for k, v in statAccessor.items():
392 theseStats[k] = stats.getValue(v)
394 if failAll:
395 theseStats['FORCE_FAILURE'] = failAll
396 ampStats[ampName] = theseStats
398 return ampStats
400 def unmaskedImageStats(self, exposure):
401 """Measure amplifier level statistics on the exposure, including all
402 pixels in the exposure, regardless of any mask planes set.
404 Parameters
405 ----------
406 exposure : `lsst.afw.image.Exposure`
407 The exposure to measure.
409 Returns
410 -------
411 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
412 A dictionary indexed by the amplifier name, containing
413 dictionaries of the statistics measured and their values.
414 """
415 noMaskStatsControl = afwMath.StatisticsControl(self.config.numSigmaClip,
416 self.config.clipMaxIter,
417 0x0)
418 return self.amplifierStats(exposure, self.config.unmaskedImageStatKeywords, noMaskStatsControl)
420 def normalizedImageStats(self, exposure, statControl):
421 """Measure amplifier level statistics on the exposure after dividing
422 by the exposure time.
424 Parameters
425 ----------
426 exposure : `lsst.afw.image.Exposure`
427 The exposure to measure.
428 statControl : `lsst.afw.math.StatisticsControl`
429 Statistics control object with parameters defined by
430 the config.
432 Returns
433 -------
434 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
435 A dictionary indexed by the amplifier name, containing
436 dictionaries of the statistics measured and their values.
438 Raises
439 ------
440 RuntimeError :
441 Raised if the exposure time cannot be used for normalization.
442 """
443 scaledExposure = exposure.clone()
444 exposureTime = scaledExposure.getInfo().getVisitInfo().getExposureTime()
445 if exposureTime <= 0:
446 raise RuntimeError(f"Invalid exposureTime {exposureTime}.")
447 mi = scaledExposure.getMaskedImage()
448 mi /= exposureTime
450 return self.amplifierStats(scaledExposure, self.config.normImageStatKeywords, statControl)
452 def crImageStats(self, exposure, statControl):
453 """Measure amplifier level statistics on the exposure,
454 after running cosmic ray rejection.
456 Parameters
457 ----------
458 exposure : `lsst.afw.image.Exposure`
459 The exposure to measure.
460 statControl : `lsst.afw.math.StatisticsControl`
461 Statistics control object with parameters defined by
462 the config.
464 Returns
465 -------
466 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
467 A dictionary indexed by the amplifier name, containing
468 dictionaries of the statistics measured and their values.
470 """
471 crRejectedExp = exposure.clone()
472 psf = measAlg.SingleGaussianPsf(self.config.psfSize,
473 self.config.psfSize,
474 self.config.psfFwhm/(2*math.sqrt(2*math.log(2))))
475 crRejectedExp.setPsf(psf)
476 try:
477 self.repair.run(crRejectedExp, keepCRs=False)
478 failAll = False
479 except pexException.LengthError:
480 self.log.warn("Failure masking cosmic rays (too many found). Continuing.")
481 failAll = True
483 if self.config.crGrow > 0:
484 crMask = crRejectedExp.getMaskedImage().getMask().getPlaneBitMask("CR")
485 spans = afwGeom.SpanSet.fromMask(crRejectedExp.mask, crMask)
486 spans = spans.dilated(self.config.crGrow)
487 spans = spans.clippedTo(crRejectedExp.getBBox())
488 spans.setMask(crRejectedExp.mask, crMask)
490 return self.amplifierStats(crRejectedExp, self.config.crImageStatKeywords,
491 statControl, failAll=failAll)
493 # Methods that need to be implemented by the calibration-level subclasses.
494 def catalogStatistics(self, exposure, statControl):
495 """Calculate statistics from a catalog.
497 Parameters
498 ----------
499 exposure : `lsst.afw.image.Exposure`
500 The exposure to measure.
501 statControl : `lsst.afw.math.StatisticsControl`
502 Statistics control object with parameters defined by
503 the config.
505 Returns
506 -------
507 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
508 A dictionary indexed by the amplifier name, containing
509 dictionaries of the statistics measured and their values.
510 """
511 raise NotImplementedError("Subclasses must implement catalog statistics method.")
513 def detectorStatistics(self, statisticsDict, statControl):
514 """Calculate detector level statistics based on the existing
515 per-amplifier measurements.
517 Parameters
518 ----------
519 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]],
520 Dictionary of measured statistics. The inner dictionary
521 should have keys that are statistic names (`str`) with
522 values that are some sort of scalar (`int` or `float` are
523 the mostly likely types).
525 Returns
526 -------
527 outputStatistics : `dict` [`str`, scalar]
528 A dictionary of the statistics measured and their values.
530 Raises
531 ------
532 NotImplementedError :
533 This method must be implemented by the calibration-type
534 subclass.
535 """
536 raise NotImplementedError("Subclasses must implement detector statistics method.")
538 def verify(self, exposure, statisticsDict):
539 """Verify that the measured statistics meet the verification criteria.
541 Parameters
542 ----------
543 exposure : `lsst.afw.image.Exposure`
544 The exposure the statistics are from.
545 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]],
546 Dictionary of measured statistics. The inner dictionary
547 should have keys that are statistic names (`str`) with
548 values that are some sort of scalar (`int` or `float` are
549 the mostly likely types).
551 Returns
552 -------
553 outputStatistics : `dict` [`str`, `dict` [`str`, `bool`]]
554 A dictionary indexed by the amplifier name, containing
555 dictionaries of the verification criteria.
556 success : `bool`
557 A boolean indicating whether all tests have passed.
559 Raises
560 ------
561 NotImplementedError :
562 This method must be implemented by the calibration-type
563 subclass.
564 """
565 raise NotImplementedError("Subclasses must implement verification criteria.")