Coverage for python / lsst / ip / isr / overscanAmpConfig.py: 31%

105 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-01 08:30 +0000

1import lsst.pex.config as pexConfig 

2import hashlib 

3 

4from .overscan import SerialOverscanCorrectionTaskConfig, ParallelOverscanCorrectionTaskConfig 

5 

6 

7__all__ = [ 

8 "OverscanAmpConfig", 

9 "OverscanDetectorConfig", 

10 "OverscanCameraConfig", 

11] 

12 

13 

14class OverscanAmpConfig(pexConfig.Config): 

15 """Overscan configurations applicable to a single amplifier.""" 

16 doSerialOverscan = pexConfig.Field( 

17 dtype=bool, 

18 doc="Do serial overscan subtraction?", 

19 default=True, 

20 ) 

21 serialOverscanConfig = pexConfig.ConfigField( 

22 dtype=SerialOverscanCorrectionTaskConfig, 

23 doc="Serial overscan configuration.", 

24 ) 

25 # TODO: Remove on DM-48394 

26 doParallelOverscanCrosstalk = pexConfig.Field( 

27 dtype=bool, 

28 doc="Apply crosstalk correction in parallel overscan region?", 

29 default=True, 

30 deprecated="This field is no longer used, and will be removed after v29.", 

31 ) 

32 doParallelOverscan = pexConfig.Field( 

33 dtype=bool, 

34 doc="Do parallel overscan subtraction?", 

35 default=True, 

36 ) 

37 parallelOverscanConfig = pexConfig.ConfigField( 

38 dtype=ParallelOverscanCorrectionTaskConfig, 

39 doc="Parallel overscan configuration.", 

40 ) 

41 saturation = pexConfig.Field( 

42 dtype=float, 

43 doc="The saturation level to use to override any detector/calibration product value " 

44 "(ignored if NaN). Units are ADU.", 

45 default=float("NaN"), 

46 ) 

47 suspectLevel = pexConfig.Field( 

48 dtype=float, 

49 doc="The ``suspect`` level to use to override any detector/calibration product value " 

50 "(ignored if NaN). Units are ADU.", 

51 default=float("NaN"), 

52 ) 

53 gain = pexConfig.Field( 

54 dtype=float, 

55 doc="The gain to use to override any calibration product value (ignored if NaN). " 

56 "Units are e-/ADU.", 

57 default=float("NaN"), 

58 ) 

59 

60 def setDefaults(self): 

61 super().setDefaults() 

62 

63 self.serialOverscanConfig.fitType = "MEDIAN_PER_ROW" 

64 self.serialOverscanConfig.leadingToSkip = 3 

65 self.serialOverscanConfig.trailingToSkip = 3 

66 self.serialOverscanConfig.overscanIsInt = False 

67 self.parallelOverscanConfig.fitType = "MEDIAN_PER_ROW" 

68 self.parallelOverscanConfig.leadingToSkip = 3 

69 self.parallelOverscanConfig.trailingToSkip = 3 

70 self.parallelOverscanConfig.overscanIsInt = False 

71 # We expect the parallel overscan to not deviate much 

72 # after serial overscan subtraction and crosstalk correction. 

73 self.parallelOverscanConfig.maxDeviation = 100.0 

74 

75 @property 

76 def _stringForHash(self): 

77 """Turn this config into a simple string for hashing. 

78 

79 Only essential data for tracking is returned. 

80 

81 Returns 

82 ------- 

83 stringForHash : `str` 

84 """ 

85 stringForHash = (f"doSerial={self.doSerialOverscan} " 

86 f"serialFitType={self.serialOverscanConfig.fitType} " 

87 f"doParallel={self.doParallelOverscan} " 

88 f"parallelFitType={self.parallelOverscanConfig.fitType}") 

89 return stringForHash 

90 

91 

92class OverscanDetectorConfig(pexConfig.Config): 

93 """Overscan configurations applicable to multiple amplifiers in 

94 a single detector. 

95 """ 

96 ampRules = pexConfig.ConfigDictField( 

97 doc="Amplifier level rules for overscan, keyed by amp name.", 

98 keytype=str, 

99 itemtype=OverscanAmpConfig, 

100 default={}, 

101 ) 

102 defaultAmpConfig = pexConfig.ConfigField( 

103 dtype=OverscanAmpConfig, 

104 doc="Default configuration for amplifiers.", 

105 ) 

106 integerDitherMode = pexConfig.ChoiceField( 

107 dtype=str, 

108 doc="Dithering mode to cancel integerization of counts.", 

109 default="SYMMETRIC", 

110 allowed={ 

111 "POSITIVE": "Dithering is done with a uniform random in the range [0, 1).", 

112 "NEGATIVE": "Dithering is done with a uniform random in the range [-1, 0).", 

113 "SYMMETRIC": "Dithering is done with a uniform random in the range [-0.5, 0.5).", 

114 "NONE": "No dithering is performed.", 

115 }, 

116 ) 

117 itlDipMinHeight = pexConfig.Field( 

118 dtype=int, 

119 doc="Minimum height for a saturated footprint column to contribute to a dip.", 

120 default=50, 

121 ) 

122 itlDipMinWidth = pexConfig.Field( 

123 dtype=int, 

124 doc="Minimum number of columns in a saturated footprint with idlDipMinHeight " 

125 "to contribute to a dip.", 

126 default=15, 

127 ) 

128 itlDipMaxWidth = pexConfig.Field( 

129 dtype=int, 

130 doc="Maximum number of columns to use for a dip mask.", 

131 default=50, 

132 ) 

133 itlDipWidthScale = pexConfig.Field( 

134 dtype=float, 

135 doc="Scaling factor to widen saturated core for dip masking.", 

136 default=1.5, 

137 ) 

138 itlDipBackgroundFraction = pexConfig.Field( 

139 dtype=float, 

140 doc="Fraction of background (scaled by width) that is in the center of the dip. " 

141 "Only dips that are greater than itlDipMinSkyNoiseFraction will be masked. " 

142 "If equal to 0.0, dip masking will be skipped.", 

143 default=0.0, 

144 ) 

145 itlDipMinBackgroundNoiseFraction = pexConfig.Field( 

146 dtype=float, 

147 doc="Only max model dip depth greater than this fraction of the approximate " 

148 "background noise will be masked.", 

149 default=0.5, 

150 ) 

151 itlDipMaxColsPerImage = pexConfig.Field( 

152 dtype=int, 

153 doc="Maximum number of columns detected as ``dip`` columns before dip masking " 

154 "is disabled on the image.", 

155 default=500, 

156 ) 

157 

158 @property 

159 def doAnySerialOverscan(self): 

160 """Check if any of the amp configs have doSerialOverscan. 

161 

162 Returns 

163 ------- 

164 doAnySerialOverscan : `bool` 

165 """ 

166 if self.defaultAmpConfig.doSerialOverscan: 

167 return True 

168 

169 for _, ampRule in self.ampRules.items(): 

170 if ampRule.doSerialOverscan: 

171 return True 

172 

173 return False 

174 

175 @property 

176 def doAnyParallelOverscan(self): 

177 """Check if any of the amp configs have doParallelOverscan. 

178 

179 Returns 

180 ------- 

181 doAnyParallelOverscan : `bool` 

182 """ 

183 if self.defaultAmpConfig.doParallelOverscan: 

184 return True 

185 

186 for _, ampRule in self.ampRules.items(): 

187 if ampRule.doParallelOverscan: 

188 return True 

189 

190 return False 

191 

192 def getOverscanAmpConfig(self, amplifier): 

193 """Get the OverscanAmpConfig for a specific amplifier. 

194 

195 Parameters 

196 ---------- 

197 amplifier : `lsst.afw.cameraGeom.Amplifier` 

198 

199 Returns 

200 ------- 

201 overscanAmpConfig : `lsst.ip.isr.overscanAmpConfig.OverscanAmpConfig` 

202 """ 

203 ampKey = amplifier.getName() 

204 

205 if ampKey in self.ampRules.keys(): 

206 overscanAmpConfig = self.ampRules[ampKey] 

207 else: 

208 overscanAmpConfig = self.defaultAmpConfig 

209 

210 return overscanAmpConfig 

211 

212 @property 

213 def _stringForHash(self): 

214 """Turn this config into a simple string for hashing. 

215 

216 Only the default and amps that are different than the 

217 default are used in the string representation. 

218 

219 Returns 

220 ------- 

221 stringForHash : `str` 

222 """ 

223 defaultString = self.defaultAmpConfig._stringForHash 

224 

225 stringForHash = f"default: {defaultString}" 

226 for ampName in self.ampRules: 

227 ampString = self.ampRules[ampName]._stringForHash 

228 if ampString != defaultString: 

229 stringForHash += f" {ampName}: {ampString}" 

230 

231 return stringForHash 

232 

233 @property 

234 def md5(self): 

235 """Compute the MD5 hash of this config (detector + amps). 

236 

237 This can be used to ensure overscan configs are consistent. 

238 

239 Returns 

240 ------- 

241 md5Hash : `str` 

242 """ 

243 return hashlib.md5(self._stringForHash.encode("UTF-8")).hexdigest() 

244 

245 

246class OverscanCameraConfig(pexConfig.Config): 

247 """Overscan configurations applicable to multiple detectors in 

248 a single camera. 

249 """ 

250 detectorRules = pexConfig.ConfigDictField( 

251 doc="Detector level rules for overscan", 

252 keytype=str, 

253 itemtype=OverscanDetectorConfig, 

254 default={}, 

255 ) 

256 defaultDetectorConfig = pexConfig.ConfigField( 

257 dtype=OverscanDetectorConfig, 

258 doc="Default configuration for detectors.", 

259 ) 

260 detectorRuleKeyType = pexConfig.ChoiceField( 

261 doc="Detector rule key type.", 

262 dtype=str, 

263 default="NAME", 

264 allowed={ 

265 "NAME": "DetectorRules has a key that is the detector name.", 

266 "SERIAL": "DetectorRules has a key that is the detector serial number.", 

267 "ID": "DetectorRules has a key that is the detector id number.", 

268 }, 

269 ) 

270 

271 @property 

272 def doAnySerialOverscan(self): 

273 """Check if any of the detector/amp configs have doSerialOverscan. 

274 

275 Returns 

276 ------- 

277 doAnySerialOverscan : `bool` 

278 """ 

279 if self.defaultDetectorConfig.doAnySerialOverscan: 

280 return True 

281 

282 for _, detectorRule in self.detectorRules.items(): 

283 if detectorRule.doAnySerialOverscan: 

284 return True 

285 

286 return False 

287 

288 @property 

289 def doAnyParallelOverscan(self): 

290 """Check if any of the detector/amp configs have 

291 doParallelOverscan. 

292 

293 Returns 

294 ------- 

295 doAnyParallelOverscan : `bool` 

296 """ 

297 

298 if self.defaultDetectorConfig.doAnyParallelOverscan: 

299 return True 

300 

301 for _, detectorRule in self.detectorRules.items(): 

302 if detectorRule.doAnyParallelOverscan: 

303 return True 

304 

305 return False 

306 

307 def getOverscanDetectorConfig(self, detector): 

308 """Get the OverscanDetectorConfig for a specific detector. 

309 

310 Parameters 

311 ---------- 

312 detector : `lsst.afw.cameraGeom.Detector` 

313 

314 Returns 

315 ------- 

316 overscanDetectorConfig : `OverscanDetectorConfig` 

317 """ 

318 match self.detectorRuleKeyType: 

319 case "NAME": 

320 key = detector.getName() 

321 case "SERIAL": 

322 key = detector.getSerial() 

323 case "ID": 

324 key = str(detector.getId()) 

325 

326 if key in self.detectorRules.keys(): 

327 overscanDetectorConfig = self.detectorRules[key] 

328 else: 

329 overscanDetectorConfig = self.defaultDetectorConfig 

330 

331 return overscanDetectorConfig