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