Coverage for python/lsst/cp/verify/verifyDefects.py: 24%
81 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-28 10:03 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-28 10:03 +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
22import scipy.stats
23import lsst.pipe.base.connectionTypes as cT
24from lsst.ip.isr.isrFunctions import countMaskedPixels
26from .verifyStats import (
27 CpVerifyStatsConfig,
28 CpVerifyStatsTask,
29 CpVerifyStatsConnections,
30)
33__all__ = ["CpVerifyDefectsConfig", "CpVerifyDefectsTask"]
36class CpVerifyDefectsConnections(
37 CpVerifyStatsConnections, dimensions={"instrument", "visit", "detector"}
38):
39 inputExp = cT.Input(
40 name="icExp",
41 doc="Input exposure to calculate statistics for.",
42 storageClass="ExposureF",
43 dimensions=["instrument", "visit", "detector"],
44 )
45 uncorrectedExp = cT.Input(
46 name="uncorrectedExp",
47 doc="Uncorrected input exposure to calculate statistics for.",
48 storageClass="ExposureF",
49 dimensions=["instrument", "visit", "detector"],
50 )
51 inputCatalog = cT.Input(
52 name="icSrc",
53 doc="Input catalog to calculate statistics from.",
54 storageClass="SourceCatalog",
55 dimensions=["instrument", "visit", "detector"],
56 )
57 uncorrectedCatalog = cT.Input(
58 name="uncorrectedSrc",
59 doc="Input catalog without correction applied.",
60 storageClass="SourceCatalog",
61 dimensions=["instrument", "visit", "detector"],
62 )
63 camera = cT.PrerequisiteInput(
64 name="camera",
65 storageClass="Camera",
66 doc="Input camera.",
67 dimensions=["instrument", ],
68 isCalibration=True,
69 )
70 outputStats = cT.Output(
71 name="detectorStats",
72 doc="Output statistics from cp_verify.",
73 storageClass="StructuredDataDict",
74 dimensions=["instrument", "visit", "detector"],
75 )
78class CpVerifyDefectsConfig(
79 CpVerifyStatsConfig, pipelineConnections=CpVerifyDefectsConnections
80):
81 """Inherits from base CpVerifyStatsConfig."""
83 def setDefaults(self):
84 super().setDefaults()
85 self.maskNameList = ["BAD"] # noqa F821
87 self.imageStatKeywords = {
88 "DEFECT_PIXELS": "NMASKED", # noqa F821
89 "OUTLIERS": "NCLIPPED",
90 "MEDIAN": "MEDIAN",
91 "STDEV": "STDEVCLIP",
92 "MIN": "MIN",
93 "MAX": "MAX",
94 }
95 self.unmaskedImageStatKeywords = {
96 "UNMASKED_MIN": "MIN", # noqa F821
97 "UNMASKED_MAX": "MAX",
98 "UNMASKED_STDEV": "STDEVCLIP",
99 "UNMASKED_OUTLIERS": "NCLIPPED",
100 }
101 self.uncorrectedImageStatKeywords = {
102 "UNC_DEFECT_PIXELS": "NMASKED", # noqa F821
103 "UNC_OUTLIERS": "NCLIPPED",
104 "UNC_MEDIAN": "MEDIAN",
105 "UNC_STDEV": "STDEVCLIP",
106 "UNC_MIN": "MIN",
107 "UNC_MAX": "MAX",
108 }
109 # These config options need to have a key/value pair
110 # to run verification analysis, but the contents of
111 # that pair are not used.
112 self.catalogStatKeywords = {"empty": "dictionary"}
113 self.detectorStatKeywords = {"empty": "dictionary"}
116class CpVerifyDefectsTask(CpVerifyStatsTask):
117 """Defects verification sub-class, implementing the verify method.
119 This also applies additional image processing statistics.
120 """
122 ConfigClass = CpVerifyDefectsConfig
123 _DefaultName = "cpVerifyDefects"
125 def catalogStatistics(self, exposure, catalog, uncorrectedCatalog, statControl):
126 """Measure the catalog statistics.
128 Parameters
129 ----------
130 exposure : `lsst.afw.image.Exposure`
131 The exposure to measure.
132 catalog : `lsst.afw.table.Table`
133 The catalog to measure.
134 uncorrectedCatalog : `lsst.afw.table.Table`
135 The uncorrected catalog to measure.
136 statControl : `lsst.afw.math.StatisticsControl`
137 Statistics control object with parameters defined by
138 the config.
140 Returns
141 -------
142 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
143 A dictionary indexed by the amplifier name, containing
144 dictionaries of the statistics measured and their values.
146 Notes
147 -----
148 Number of detections test: running with defects would have fewer
149 detections
150 """
151 outputStatistics = {}
153 # Number of detections test
154 outputStatistics["NUM_OBJECTS_BEFORE"] = len(uncorrectedCatalog)
155 outputStatistics["NUM_OBJECTS_AFTER"] = len(catalog)
157 return outputStatistics
159 def detectorStatistics(self, statsDict, statControl, exposure, uncorrectedExposure):
160 """Measure the detector statistics.
162 Parameters
163 ----------
164 statsDict : `dict` [`str`, scalar]
165 Dictionary with detector tests.
166 exposure : `lsst.afw.image.Exposure`
167 Exposure containing the ISR processed data to measure.
168 statControl : `lsst.afw.math.StatControl`
169 Statistics control object with parameters defined by
170 the config.
171 exposure : `lsst.afw.image.Exposure`
172 Exposure containing the ISR-processed data to measure.
173 uncorrectedExposure : `lsst.afw.image.Exposure`
174 uncorrected esposure (no defects) containing the
175 ISR-processed data to measure.
177 Returns
178 -------
179 outputStatistics : `dict` [`str`, scalar]
180 A dictionary containing statistics measured and their values.
182 Notes
183 -----
184 Number of cosmic rays test: If there are defects in our data that
185 we didn't properly identify and cover, they might appear similar
186 to cosmic rays because they have sharp edges compared to the point
187 spread function (PSF). When we process the data, if these defects
188 aren't marked in our defect mask, the software might mistakenly think
189 they are cosmic rays and try to remove them. However, if we've already
190 included these defects in the defect mask, the software won't treat
191 them as cosmic rays, so we'll have fewer pixels that are falsely
192 identified and removed as cosmic rays when we compare two sets of
193 data reductions.
194 """
195 outputStatistics = {}
196 # Cosmic Rays test: Count number of cosmic rays before
197 # and after masking with defects
198 nCosmicsBefore = countMaskedPixels(uncorrectedExposure, ["CR"])
199 nCosmicsAfter = countMaskedPixels(exposure, ["CR"])
201 outputStatistics["NUM_COSMICS_BEFORE"] = nCosmicsBefore
202 outputStatistics["NUM_COSMICS_AFTER"] = nCosmicsAfter
204 return outputStatistics
206 def imageStatistics(self, exposure, uncorrectedExposure, statControl):
207 """Measure additional defect statistics.
209 This calls the parent class method first, then adds additional
210 measurements.
212 Parameters
213 ----------
214 exposure : `lsst.afw.image.Exposure`
215 Exposure containing the ISR processed data to measure.
216 statControl : `lsst.afw.math.StatControl`
217 Statistics control object with parameters defined by
218 the config.
220 Returns
221 -------
222 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
223 A dictionary indexed by the amplifier name, containing
224 dictionaries of the statistics measured and their values.
226 Notes
227 -----
228 Number of cosmic rays test: If there are defects in our data that
229 we didn't properly identify and cover, they might appear similar
230 to cosmic rays because they have sharp edges compared to the point
231 spread function (PSF). When we process the data, if these defects
232 aren't marked in our defect mask, the software might mistakenly think
233 they are cosmic rays and try to remove them. However, if we've already
234 included these defects in the defect mask, the software won't treat
235 them as cosmic rays, so we'll have fewer pixels that are falsely
236 identified and removed as cosmic rays when we compare two sets of
237 data reductions.
238 """
239 outputStatistics = super().imageStatistics(
240 exposure, uncorrectedExposure, statControl
241 )
243 # Is this a useful test? It saves having to do chi^2 fits,
244 # which are going to be biased by the bulk of points.
245 for amp in exposure.getDetector():
246 ampName = amp.getName()
247 ampExp = exposure.Factory(exposure, amp.getBBox())
249 normImage = ampExp.getImage()
250 normArray = normImage.getArray()
252 normArray -= outputStatistics[ampName]["MEDIAN"]
253 normArray /= outputStatistics[ampName]["STDEV"]
255 probability = scipy.stats.norm.pdf(normArray)
256 outliers = np.where(probability < 1.0 / probability.size, 1.0, 0.0)
257 outputStatistics[ampName]["STAT_OUTLIERS"] = int(np.sum(outliers))
259 return outputStatistics
261 def verify(self, exposure, statisticsDict):
262 """Verify that the measured statistics meet the verification criteria.
264 Parameters
265 ----------
266 exposure : `lsst.afw.image.Exposure`
267 The exposure the statistics are from.
268 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]],
269 Dictionary of measured statistics. The inner dictionary
270 should have keys that are statistic names (`str`) with
271 values that are some sort of scalar (`int` or `float` are
272 the mostly likely types).
274 Returns
275 -------
276 outputStatistics : `dict` [`str`, `dict` [`str`, `bool`]]
277 A dictionary indexed by the amplifier name, containing
278 dictionaries of the verification criteria.
279 success : `bool`
280 A boolean indicating if all tests have passed.
281 """
282 # Amplifier statistics
283 ampStats = statisticsDict["AMP"]
284 verifyStats = {}
285 successAmp = True
286 for ampName, stats in ampStats.items():
287 verify = {}
289 # These are not defined in DMTN-101 yet.
290 verify["OUTLIERS"] = bool(stats["UNMASKED_OUTLIERS"] >= stats["OUTLIERS"])
291 verify["STDEV"] = bool(stats["UNMASKED_STDEV"] >= stats["STDEV"])
292 verify["MIN"] = bool(stats["UNMASKED_MIN"] <= stats["MIN"])
293 verify["MAX"] = bool(stats["UNMASKED_MAX"] >= stats["MAX"])
295 # This test is bad, and should be made not bad.
296 verify["PROB_TEST"] = bool(stats["STAT_OUTLIERS"] == stats["DEFECT_PIXELS"])
298 verify["SUCCESS"] = bool(np.all(list(verify.values())))
299 if verify["SUCCESS"] is False:
300 successAmp = False
302 verifyStats[ampName] = verify
304 # Detector statistics
305 detStats = statisticsDict["DET"]
306 verifyStatsDet = {}
307 successDet = True
308 # Cosmic rays test from DM-38563, before and after defects.
309 verifyStatsDet["NUMBER_COSMIC_RAYS"] = bool(
310 detStats["NUM_COSMICS_BEFORE"] > detStats["NUM_COSMICS_AFTER"]
311 )
313 verifyStatsDet["SUCCESS"] = bool(np.all(list(verifyStatsDet.values())))
314 if verifyStatsDet["SUCCESS"] is False:
315 successDet = False
317 # Catalog statistics
318 catStats = statisticsDict["CATALOG"]
319 verifyStatsCat = {}
320 successCat = True
321 # Detection tests from DM-38563, before and after defects.
322 verifyStatsCat["NUMBER_DETECTIONS"] = bool(
323 catStats["NUM_OBJECTS_BEFORE"] > catStats["NUM_OBJECTS_AFTER"]
324 )
326 verifyStatsCat["SUCCESS"] = bool(np.all(list(verifyStatsCat.values())))
327 if verifyStatsCat["SUCCESS"] is False:
328 successCat = False
330 success = successDet & successAmp & successCat
331 return {
332 "AMP": verifyStats,
333 "DET": verifyStatsDet,
334 "CATALOG": verifyStatsCat,
335 }, bool(success)