Coverage for python/lsst/cp/verify/verifyDefects.py: 24%
81 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-01 09:10 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-01 09:10 +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, statisticsDict, statControl, exposure=None, uncorrectedExposure=None):
160 """Measure the detector statistics.
162 Parameters
163 ----------
164 statisticsDict : `dict` [`str`, scalar]
165 Dictionary with detector tests.
166 statControl : `lsst.afw.math.StatControl`
167 Statistics control object with parameters defined by
168 the config.
169 exposure : `lsst.afw.image.Exposure`, optional
170 Exposure containing the ISR-processed data to measure.
171 uncorrectedExposure : `lsst.afw.image.Exposure`, optional
172 uncorrected esposure (no defects) containing the
173 ISR-processed data to measure.
175 Returns
176 -------
177 outputStatistics : `dict` [`str`, scalar]
178 A dictionary containing statistics measured and their values.
180 Notes
181 -----
182 Number of cosmic rays test: If there are defects in our data that
183 we didn't properly identify and cover, they might appear similar
184 to cosmic rays because they have sharp edges compared to the point
185 spread function (PSF). When we process the data, if these defects
186 aren't marked in our defect mask, the software might mistakenly think
187 they are cosmic rays and try to remove them. However, if we've already
188 included these defects in the defect mask, the software won't treat
189 them as cosmic rays, so we'll have fewer pixels that are falsely
190 identified and removed as cosmic rays when we compare two sets of
191 data reductions.
192 """
193 outputStatistics = {}
194 # Cosmic Rays test: Count number of cosmic rays before
195 # and after masking with defects
196 nCosmicsBefore = countMaskedPixels(uncorrectedExposure, ["CR"])
197 nCosmicsAfter = countMaskedPixels(exposure, ["CR"])
199 outputStatistics["NUM_COSMICS_BEFORE"] = nCosmicsBefore
200 outputStatistics["NUM_COSMICS_AFTER"] = nCosmicsAfter
202 return outputStatistics
204 def imageStatistics(self, exposure, uncorrectedExposure, statControl):
205 """Measure additional defect statistics.
207 This calls the parent class method first, then adds additional
208 measurements.
210 Parameters
211 ----------
212 exposure : `lsst.afw.image.Exposure`
213 Exposure containing the ISR processed data to measure.
214 statControl : `lsst.afw.math.StatControl`
215 Statistics control object with parameters defined by
216 the config.
218 Returns
219 -------
220 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]]
221 A dictionary indexed by the amplifier name, containing
222 dictionaries of the statistics measured and their values.
224 Notes
225 -----
226 Number of cosmic rays test: If there are defects in our data that
227 we didn't properly identify and cover, they might appear similar
228 to cosmic rays because they have sharp edges compared to the point
229 spread function (PSF). When we process the data, if these defects
230 aren't marked in our defect mask, the software might mistakenly think
231 they are cosmic rays and try to remove them. However, if we've already
232 included these defects in the defect mask, the software won't treat
233 them as cosmic rays, so we'll have fewer pixels that are falsely
234 identified and removed as cosmic rays when we compare two sets of
235 data reductions.
236 """
237 outputStatistics = super().imageStatistics(
238 exposure, uncorrectedExposure, statControl
239 )
241 # Is this a useful test? It saves having to do chi^2 fits,
242 # which are going to be biased by the bulk of points.
243 for amp in exposure.getDetector():
244 ampName = amp.getName()
245 ampExp = exposure.Factory(exposure, amp.getBBox())
247 normImage = ampExp.getImage()
248 normArray = normImage.getArray()
250 normArray -= outputStatistics[ampName]["MEDIAN"]
251 normArray /= outputStatistics[ampName]["STDEV"]
253 probability = scipy.stats.norm.pdf(normArray)
254 outliers = np.where(probability < 1.0 / probability.size, 1.0, 0.0)
255 outputStatistics[ampName]["STAT_OUTLIERS"] = int(np.sum(outliers))
257 return outputStatistics
259 def verify(self, exposure, statisticsDict):
260 """Verify that the measured statistics meet the verification criteria.
262 Parameters
263 ----------
264 exposure : `lsst.afw.image.Exposure`
265 The exposure the statistics are from.
266 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]],
267 Dictionary of measured statistics. The inner dictionary
268 should have keys that are statistic names (`str`) with
269 values that are some sort of scalar (`int` or `float` are
270 the mostly likely types).
272 Returns
273 -------
274 outputStatistics : `dict` [`str`, `dict` [`str`, `bool`]]
275 A dictionary indexed by the amplifier name, containing
276 dictionaries of the verification criteria.
277 success : `bool`
278 A boolean indicating if all tests have passed.
279 """
280 # Amplifier statistics
281 ampStats = statisticsDict["AMP"]
282 verifyStats = {}
283 successAmp = True
284 for ampName, stats in ampStats.items():
285 verify = {}
287 # These are not defined in DMTN-101 yet.
288 verify["OUTLIERS"] = bool(stats["UNMASKED_OUTLIERS"] >= stats["OUTLIERS"])
289 verify["STDEV"] = bool(stats["UNMASKED_STDEV"] >= stats["STDEV"])
290 verify["MIN"] = bool(stats["UNMASKED_MIN"] <= stats["MIN"])
291 verify["MAX"] = bool(stats["UNMASKED_MAX"] >= stats["MAX"])
293 # This test is bad, and should be made not bad.
294 verify["PROB_TEST"] = bool(stats["STAT_OUTLIERS"] == stats["DEFECT_PIXELS"])
296 verify["SUCCESS"] = bool(np.all(list(verify.values())))
297 if verify["SUCCESS"] is False:
298 successAmp = False
300 verifyStats[ampName] = verify
302 # Detector statistics
303 detStats = statisticsDict["DET"]
304 verifyStatsDet = {}
305 successDet = True
306 # Cosmic rays test from DM-38563, before and after defects.
307 verifyStatsDet["NUMBER_COSMIC_RAYS"] = bool(
308 detStats["NUM_COSMICS_BEFORE"] > detStats["NUM_COSMICS_AFTER"]
309 )
311 verifyStatsDet["SUCCESS"] = bool(np.all(list(verifyStatsDet.values())))
312 if verifyStatsDet["SUCCESS"] is False:
313 successDet = False
315 # Catalog statistics
316 catStats = statisticsDict["CATALOG"]
317 verifyStatsCat = {}
318 successCat = True
319 # Detection tests from DM-38563, before and after defects.
320 verifyStatsCat["NUMBER_DETECTIONS"] = bool(
321 catStats["NUM_OBJECTS_BEFORE"] > catStats["NUM_OBJECTS_AFTER"]
322 )
324 verifyStatsCat["SUCCESS"] = bool(np.all(list(verifyStatsCat.values())))
325 if verifyStatsCat["SUCCESS"] is False:
326 successCat = False
328 success = successDet & successAmp & successCat
329 return {
330 "AMP": verifyStats,
331 "DET": verifyStatsDet,
332 "CATALOG": verifyStatsCat,
333 }, bool(success)