Coverage for python / lsst / cp / verify / verifyDefects.py: 19%

99 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-07 08:54 +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 

25 

26from .verifyStats import ( 

27 CpVerifyStatsConfig, 

28 CpVerifyStatsTask, 

29 CpVerifyStatsConnections, 

30) 

31 

32 

33__all__ = ["CpVerifyDefectsConfig", "CpVerifyDefectsTask"] 

34 

35 

36class CpVerifyDefectsConnections( 

37 CpVerifyStatsConnections, dimensions={"instrument", "exposure", "detector"} 

38): 

39 inputExp = cT.Input( 

40 name="icExp", 

41 doc="Input exposure to calculate statistics for.", 

42 storageClass="Exposure", 

43 dimensions=["instrument", "exposure", "detector"], 

44 ) 

45 uncorrectedExp = cT.Input( 

46 name="uncorrectedExp", 

47 doc="Uncorrected input exposure to calculate statistics for.", 

48 storageClass="Exposure", 

49 dimensions=["instrument", "exposure", "detector"], 

50 ) 

51 camera = cT.PrerequisiteInput( 

52 name="camera", 

53 storageClass="Camera", 

54 doc="Input camera.", 

55 dimensions=["instrument", ], 

56 isCalibration=True, 

57 ) 

58 outputStats = cT.Output( 

59 name="detectorStats", 

60 doc="Output statistics from cp_verify.", 

61 storageClass="StructuredDataDict", 

62 dimensions=["instrument", "exposure", "detector"], 

63 ) 

64 outputResults = cT.Output( 

65 name="detectorResults", 

66 doc="Output results from cp_verify.", 

67 storageClass="ArrowAstropy", 

68 dimensions=["instrument", "exposure", "detector"], 

69 ) 

70 outputMatrix = cT.Output( 

71 name="detectorMatrix", 

72 doc="Output matrix results from cp_verify.", 

73 storageClass="ArrowAstropy", 

74 dimensions=["instrument", "exposure", "detector"], 

75 ) 

76 

77 

78class CpVerifyDefectsConfig( 

79 CpVerifyStatsConfig, pipelineConnections=CpVerifyDefectsConnections 

80): 

81 """Inherits from base CpVerifyStatsConfig.""" 

82 

83 def setDefaults(self): 

84 super().setDefaults() 

85 self.stageName = 'DEFECTS' 

86 

87 self.maskNameList = ["BAD"] # noqa F821 

88 

89 self.imageStatKeywords = { 

90 "DEFECT_PIXELS": "NMASKED", # noqa F821 

91 "OUTLIERS": "NCLIPPED", 

92 "MEDIAN": "MEDIAN", 

93 "STDEV": "STDEVCLIP", 

94 "MIN": "MIN", 

95 "MAX": "MAX", 

96 } 

97 self.unmaskedImageStatKeywords = { 

98 "UNMASKED_MIN": "MIN", # noqa F821 

99 "UNMASKED_MAX": "MAX", 

100 "UNMASKED_STDEV": "STDEVCLIP", 

101 "UNMASKED_OUTLIERS": "NCLIPPED", 

102 } 

103 self.uncorrectedImageStatKeywords = { 

104 "UNC_DEFECT_PIXELS": "NMASKED", # noqa F821 

105 "UNC_OUTLIERS": "NCLIPPED", 

106 "UNC_MEDIAN": "MEDIAN", 

107 "UNC_STDEV": "STDEVCLIP", 

108 "UNC_MIN": "MIN", 

109 "UNC_MAX": "MAX", 

110 } 

111 # These config options need to have a key/value pair 

112 # to run verification analysis, but the contents of 

113 # that pair are not used. 

114 self.catalogStatKeywords = {"empty": "dictionary"} 

115 self.detectorStatKeywords = {"empty": "dictionary"} 

116 

117 

118class CpVerifyDefectsTask(CpVerifyStatsTask): 

119 """Defects verification sub-class, implementing the verify method. 

120 

121 This also applies additional image processing statistics. 

122 """ 

123 

124 ConfigClass = CpVerifyDefectsConfig 

125 _DefaultName = "cpVerifyDefects" 

126 

127 def catalogStatistics(self, exposure, catalog, uncorrectedCatalog, statControl): 

128 """Measure the catalog statistics. 

129 

130 Parameters 

131 ---------- 

132 exposure : `lsst.afw.image.Exposure` 

133 The exposure to measure. 

134 catalog : `lsst.afw.table.Table` 

135 The catalog to measure. 

136 uncorrectedCatalog : `lsst.afw.table.Table` 

137 The uncorrected catalog to measure. 

138 statControl : `lsst.afw.math.StatisticsControl` 

139 Statistics control object with parameters defined by 

140 the config. 

141 

142 Returns 

143 ------- 

144 outputStatistics : `dict` [`str`, `dict` [`str`, scalar]] 

145 A dictionary indexed by the amplifier name, containing 

146 dictionaries of the statistics measured and their values. 

147 

148 Notes 

149 ----- 

150 Number of detections test: running with defects would have fewer 

151 detections 

152 """ 

153 outputStatistics = {} 

154 

155 # Number of detections test 

156 outputStatistics["NUM_OBJECTS_BEFORE"] = len(uncorrectedCatalog) 

157 outputStatistics["NUM_OBJECTS_AFTER"] = len(catalog) 

158 

159 return outputStatistics 

160 

161 def detectorStatistics(self, statisticsDict, statControl, exposure=None, uncorrectedExposure=None): 

162 """Measure the detector statistics. 

163 

164 Parameters 

165 ---------- 

166 statisticsDict : `dict` [`str`, scalar] 

167 Dictionary with detector tests. 

168 statControl : `lsst.afw.math.StatControl` 

169 Statistics control object with parameters defined by 

170 the config. 

171 exposure : `lsst.afw.image.Exposure`, optional 

172 Exposure containing the ISR-processed data to measure. 

173 uncorrectedExposure : `lsst.afw.image.Exposure`, optional 

174 uncorrected esposure (no defects) containing the 

175 ISR-processed data to measure. 

176 

177 Returns 

178 ------- 

179 outputStatistics : `dict` [`str`, scalar] 

180 A dictionary containing statistics measured and their values. 

181 

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"]) 

200 

201 outputStatistics["NUM_COSMICS_BEFORE"] = nCosmicsBefore 

202 outputStatistics["NUM_COSMICS_AFTER"] = nCosmicsAfter 

203 

204 return outputStatistics 

205 

206 def imageStatistics(self, exposure, uncorrectedExposure, statControl): 

207 """Measure additional defect statistics. 

208 

209 This calls the parent class method first, then adds additional 

210 measurements. 

211 

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. 

219 

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. 

225 

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 ) 

242 

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()) 

248 

249 normImage = ampExp.getImage() 

250 normArray = normImage.getArray() 

251 

252 normArray -= outputStatistics[ampName]["MEDIAN"] 

253 normArray /= outputStatistics[ampName]["STDEV"] 

254 

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)) 

258 

259 # Get fraction of defects per amp 

260 ampSize = amp.getBBox().height*amp.getBBox().width 

261 outputStatistics[ampName]["FRAC"] = outputStatistics[ampName]["DEFECT_PIXELS"]/ampSize 

262 

263 return outputStatistics 

264 

265 def verify(self, exposure, statisticsDict): 

266 """Verify that the measured statistics meet the verification criteria. 

267 

268 Parameters 

269 ---------- 

270 exposure : `lsst.afw.image.Exposure` 

271 The exposure the statistics are from. 

272 statisticsDictionary : `dict` [`str`, `dict` [`str`, scalar]], 

273 Dictionary of measured statistics. The inner dictionary 

274 should have keys that are statistic names (`str`) with 

275 values that are some sort of scalar (`int` or `float` are 

276 the mostly likely types). 

277 

278 Returns 

279 ------- 

280 outputStatistics : `dict` [`str`, `dict` [`str`, `bool`]] 

281 A dictionary indexed by the amplifier name, containing 

282 dictionaries of the verification criteria. 

283 success : `bool` 

284 A boolean indicating if all tests have passed. 

285 """ 

286 # Amplifier statistics 

287 ampStats = statisticsDict["AMP"] 

288 verifyStats = {} 

289 successAmp = True 

290 for ampName, stats in ampStats.items(): 

291 verify = {} 

292 

293 # These are not defined in DMTN-101 yet. 

294 verify["OUTLIERS"] = bool(stats["UNMASKED_OUTLIERS"] >= stats["OUTLIERS"]) 

295 verify["STDEV"] = bool(stats["UNMASKED_STDEV"] >= stats["STDEV"]) 

296 verify["MIN"] = bool(stats["UNMASKED_MIN"] <= stats["MIN"]) 

297 verify["MAX"] = bool(stats["UNMASKED_MAX"] >= stats["MAX"]) 

298 

299 # This test is bad, and should be made not bad. 

300 verify["PROB_TEST"] = bool(stats["STAT_OUTLIERS"] == stats["DEFECT_PIXELS"]) 

301 

302 verify["SUCCESS"] = bool(np.all(list(verify.values()))) 

303 if verify["SUCCESS"] is False: 

304 successAmp = False 

305 

306 verifyStats[ampName] = verify 

307 

308 success = successAmp 

309 return { 

310 "AMP": verifyStats 

311 }, success 

312 

313 def repackStats(self, statisticsDict, dimensions): 

314 # docstring inherited 

315 rows = {} 

316 rowList = [] 

317 matrixRowList = None 

318 

319 if self.config.useIsrStatistics: 

320 mjd = statisticsDict["ISR"]["MJD"] 

321 else: 

322 mjd = np.nan 

323 

324 rowBase = { 

325 "instrument": dimensions["instrument"], 

326 "exposure": dimensions["exposure"], # ensure an exposure dimension for downstream. 

327 "detector": dimensions["detector"], 

328 "mjd": mjd, 

329 } 

330 

331 # AMP results 

332 for ampName, stats in statisticsDict["AMP"].items(): 

333 rows[ampName] = {} 

334 rows[ampName].update(rowBase) 

335 rows[ampName]["amplifier"] = ampName 

336 for key, value in stats.items(): 

337 rows[ampName][f"{self.config.stageName}_{key}"] = value 

338 

339 # ISR results 

340 nBadColumns = np.nan 

341 if self.config.useIsrStatistics and "ISR" in statisticsDict: 

342 for ampName, stats in statisticsDict["ISR"]["CALIBDIST"].items(): 

343 if ampName == "detector": 

344 nBadColumns = stats[ampName].get("LSST CALIB DEFECTS N_BAD_COLUMNS", np.nan) 

345 elif ampName in stats.keys(): 

346 key = f"LSST CALIB DEFECTS {ampName} N_HOT" 

347 rows[ampName][f"{self.config.stageName}_N_HOT"] = stats[ampName].get(key, np.nan) 

348 key = f"LSST CALIB DEFECTS {ampName} N_COLD" 

349 rows[ampName][f"{self.config.stageName}_N_COLD"] = stats[ampName].get(key, np.nan) 

350 

351 # DET results 

352 rows["detector"] = rowBase 

353 rows["detector"][f"{self.config.stageName}_N_BAD_COLUMNS"] = nBadColumns 

354 

355 for ampName, stats in rows.items(): 

356 rowList.append(stats) 

357 

358 return rowList, matrixRowList