Coverage for python / lsst / obs / subaru / crosstalkYagi.py: 0%

108 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-22 09:23 +0000

1# 

2# LSST Data Management System 

3# Copyright 2008, 2009, 2010, 2011 LSST Corporation. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <http://www.lsstcorp.org/LegalNotices/>. 

21# 

22""" 

23Determine and apply crosstalk corrections 

24 

25N.b. This code was written and tested for the 4-amplifier Hamamatsu chips used 

26in (Hyper)?SuprimeCam, and will need to be generalised to handle other 

27amplifier layouts. I don't want to do this until we have an example. 

28 

29N.b. To estimate crosstalk from the SuprimeCam data, the commands are e.g.: 

30 

31.. code-block:: python 

32 

33 import crosstalk 

34 coeffs = crosstalk.estimateCoeffs(range(131634, 131642), range(10), 

35 threshold=1e5, plot=True, title="CCD0..9", 

36 fig=1) 

37 crosstalk.fixCcd(131634, 0, coeffs) 

38""" 

39import numpy as np 

40import lsst.afw.detection as afwDetect 

41import lsst.afw.image as afwImage 

42import lsst.afw.math as afwMath 

43import lsst.afw.display as afwDisplay 

44import lsst.geom as geom 

45import lsst.pipe.base as pipeBase 

46import lsst.pex.config as pexConfig 

47from lsst.obs.subaru import subtractCrosstalk 

48from lsst.obs.subaru.crosstalk import makeList, estimateCoeffs, printCoeffs, readImage, subtractXTalk 

49 

50 

51class CrosstalkYagiCoeffsConfig(pexConfig.Config): 

52 """Specify crosstalk coefficients for a CCD""" 

53 

54 crossTalkCoeffs1 = pexConfig.ListField( 

55 dtype=float, 

56 doc="crosstalk coefficients (primary by the high-count pixel)", 

57 default=[0, -0.000148, -0.000162, -0.000167, # cAA,cAB,cAC,cAD 

58 -0.000148, 0, -0.000077, -0.000162, # cBA,cBB,cBC,cBD 

59 -0.000162, -0.000077, 0, -0.000148, # cCA,cCB,cCC,cCD 

60 -0.000167, -0.000162, -0.000148, 0, ], # cDA,cDB,cDC,cDD 

61 ) 

62 crossTalkCoeffs2 = pexConfig.ListField( 

63 dtype=float, 

64 doc="crosstalk coefficients (secondary by the high-count pixel + 2pix)", 

65 default=[0, 0.000051, 0.000050, 0.000053, 

66 0.000051, 0, 0, 0.000050, 

67 0.000050, 0, 0, 0.000051, 

68 0.000053, 0.000050, 0.000051, 0, ], 

69 ) 

70 

71 relativeGainsPreampAndSigboard = pexConfig.ListField( 

72 dtype=float, 

73 doc="effective gain of combination of SIG board + preAmp for CCD, relatve to chB@CCD=5. \ 

74 g2*g3 in Yagi+2012", 

75 default=[0.949, 0.993, 0.976, 0.996, 

76 0.973, 0.984, 0.966, 0.977, 

77 1.008, 0.989, 0.970, 0.976, 

78 0.961, 0.966, 1.008, 0.967, 

79 0.967, 0.984, 0.998, 1.000, 

80 0.989, 1.000, 1.034, 1.030, 

81 0.957, 1.019, 0.952, 0.979, 

82 0.974, 1.015, 0.967, 0.962, 

83 0.972, 0.932, 0.999, 0.963, 

84 0.987, 0.985, 0.986, 1.012, ], 

85 ) 

86 relativeGainsTotalBeforeOct2010 = pexConfig.ListField( 

87 dtype=float, 

88 doc="total effective gain of SIG board + preAmp + CCD on-chip amp, relatve to chB@CCD=5. \ 

89 effective before Oct 2010 g1*g2*g3 in Yagi+2012", 

90 default=[1.0505, 1.06294, 1.08287, 1.06868, 

91 1.06962, 1.13801, 1.21669, 1.21205, 

92 0.99456, 1.01931, 1.038, 0.947461, 

93 1.02245, 1.03952, 1.04192, 1.1544, 

94 1.0055, 1.12767, 1.08451, 1.03309, 

95 1.01532, 1.00000, 0.972106, 1.08237, 

96 1.19014, 0.987111, 0.984164, 1.03382, 

97 0.971049, 1.05928, 1.06713, 0.980967, 

98 1.0107, 1.25209, 1.27565, 1.17183, 

99 0.993771, 1.077, 1.03909, 1.03241, ], 

100 ) 

101 

102 relativeGainsTotalAfterOct2010 = pexConfig.ListField( 

103 dtype=float, 

104 doc="total effective gain of SIG board + preAmp + CCD on-chip amp, relatve to chB@CCD=5. \ 

105 effective after Oct 2010 g1*g2*g3 in Yagi+2012", 

106 default=[1.0505, 1.06294, 1.08287, 1.06868, 

107 1.06962, 1.13801, 1.21669, 1.21205, 

108 0.99456, 1.01931, 1.038, 0.947461, 

109 1.02245, 1.03952, 1.04192, 1.1544, 

110 1.0055, 1.12767, 1.08451, 1.03309, 

111 1.01532, 1.00000, 0.972106, 1.08237, 

112 1.19014, 0.987111, 0.984164, 1.03382, 

113 0.971049, 1.05928, 1.06713, 0.980967, 

114 1.0107, 1.25209, 1.27565, 1.17183, 

115 1.13003, 1.077, 1.03909, 1.03241, ], 

116 ) 

117 

118 shapeGainsArray = pexConfig.ListField( 

119 dtype=int, 

120 doc="Shape of gain arrays", 

121 default=[10, 4], # 10 CCDs and 4 channels per CCD 

122 minLength=1, # really 2, but there's a bug in pex_config 

123 maxLength=2, 

124 ) 

125 

126 shapeCoeffsArray = pexConfig.ListField( 

127 dtype=int, 

128 doc="Shape of coeffs arrays", 

129 default=[4, 4], # 4 channels and 4 mirror-symmetry patterns per channel 

130 minLength=1, # really 2, but there's a bug in pex_config 

131 maxLength=2, 

132 ) 

133 

134 def getCoeffs1(self): 

135 """Return a 2-D numpy array of crosstalk coefficients of the proper 

136 shape""" 

137 return np.array(self.crossTalkCoeffs1).reshape(self.shapeCoeffsArray).tolist() 

138 

139 def getCoeffs2(self): 

140 """Return a 2-D numpy array of crosstalk coefficients of the proper 

141 shape""" 

142 return np.array(self.crossTalkCoeffs2).reshape(self.shapeCoeffsArray).tolist() 

143 

144 def getGainsPreampSigboard(self): 

145 """Return a 2-D numpy array of effective gain for preamp+SIG of the 

146 proper shape""" 

147 return np.array(self.relativeGainsPreampAndSigboard).reshape(self.shapeGainsArray).tolist() 

148 

149 def getGainsTotal(self, dateobs='2008-08-01'): 

150 """Return a 2-D numpy array of effective total gain of the proper 

151 shape""" 

152 if dateobs < '2010-10': # may need rewritten to a more reliable way 

153 return np.array(self.relativeGainsTotalBeforeOct2010).reshape(self.shapeGainsArray).tolist() 

154 else: 

155 return np.array(self.relativeGainsTotalAfterOct2010).reshape(self.shapeGainsArray).tolist() 

156 

157 

158nAmp = 4 

159 

160 

161def getCrosstalkX1(x, xmax=512): 

162 """ 

163 Return the primary X positions in CCD which affect the count of input 

164 x pixel 

165 

166 Parameters 

167 ---------- 

168 x : 

169 xpos in Ccd, affected by the returned pixels 

170 xmax : 

171 nx per amp 

172 """ 

173 ctx1List = [x, 

174 2 * xmax - x - 1, 

175 2 * xmax + x, 

176 4 * xmax - x - 1, ] 

177 

178 return ctx1List 

179 

180 

181def getCrosstalkX2(x, xmax=512): 

182 """ 

183 Return the 2ndary X positions (dX=2) in CCD which affect the count of 

184 input x pixel 

185 

186 Those X pixels are read by 2-pixel ealier than the primary pixels. 

187 

188 Parameters 

189 ---------- 

190 x : 

191 xpos in Ccd, affected by the returned pixels 

192 xmax : 

193 nx per amp 

194 """ 

195 # ch0,ch2: ctx2 = ctx1 - 2, ch1,ch3: ctx2 = ctx1 + 2 

196 ctx2List = [x - 2, 

197 2 * xmax - x - 1 + 2, 

198 2 * xmax + x - 2, 

199 4 * xmax - x - 1 + 2, ] 

200 

201 return ctx2List 

202 

203 

204def getAmplifier(image, amp, ampReference=None, offset=2): 

205 """Extract an image of the amplifier from the CCD, along with an offset 

206 version 

207 

208 The amplifier image will be flipped (if required) to match the 

209 orientation of a nominated reference amplifier. 

210 

211 An additional image, with the nominated offset applied, is also produced. 

212 

213 Parameters 

214 ---------- 

215 image : 

216 Image of CCD 

217 amp : 

218 Index of amplifier 

219 ampReference : 

220 Index of reference amplifier 

221 offset : 

222 Offset to apply 

223 

224 Returns 

225 ------- 

226 amp_image : 

227 amplifier image, offset amplifier image 

228 """ 

229 height = image.getHeight() 

230 ampBox = geom.Box2I(geom.Point2I(amp*512, 0), geom.Extent2I(512, height)) 

231 ampImage = image.Factory(image, ampBox, afwImage.LOCAL) 

232 if ampReference is not None and amp % 2 != ampReference % 2: 

233 ampImage = afwMath.flipImage(ampImage, True, False) 

234 offBox = geom.Box2I(geom.Point2I(offset if amp == ampReference else 0, 0), 

235 geom.Extent2I(510, height)) 

236 offsetImage = ampImage.Factory(ampImage, offBox, afwImage.LOCAL) 

237 return ampImage, offsetImage 

238 

239 

240def subtractCrosstalkYagi(mi, coeffs1List, coeffs2List, gainsPreampSig, minPixelToMask=45000, 

241 crosstalkStr="CROSSTALK"): 

242 """Subtract the crosstalk from MaskedImage mi given a set of coefficients 

243 

244 Based on procedure presented in Yagi et al. 2012, PASP in publication; 

245 arXiv:1210.8212 The pixels affected by signal over minPixelToMask have 

246 the crosstalkStr bit set 

247 """ 

248 

249 # 

250 # These are the pixels that are bright enough to cause crosstalk (more 

251 # precisely, the ones that we label as causing crosstalk; in reality all 

252 # pixels cause crosstalk) 

253 # 

254 if True: 

255 tempStr = "TEMP" # mask plane used to record the bright pixels that we need to mask 

256 mi.getMask().addMaskPlane(tempStr) 

257 fs = afwDetect.FootprintSet(mi, afwDetect.Threshold(minPixelToMask), tempStr) 

258 

259 mi.getMask().addMaskPlane(crosstalkStr) 

260 afwDisplay.setMaskPlaneColor(crosstalkStr, afwDisplay.MAGENTA) 

261 fs.setMask(mi.getMask(), crosstalkStr) # the crosstalkStr bit will now be set 

262 # whenever we subtract crosstalk 

263 crosstalk = mi.getMask().getPlaneBitMask(crosstalkStr) 

264 

265 if True: 

266 # This python implementation is fairly fast 

267 image = mi.getImage() 

268 xtalk = afwImage.ImageF(image.getDimensions()) 

269 xtalk.set(0) 

270 for i in range(4): 

271 xAmp, xOff = getAmplifier(xtalk, i, i) 

272 for j in range(4): 

273 if i == j: 

274 continue 

275 gainRatio = gainsPreampSig[j] / gainsPreampSig[i] 

276 jAmp, jOff = getAmplifier(image, j, i) 

277 xAmp.scaledPlus(gainRatio*coeffs1List[i][j], jAmp) 

278 xOff.scaledPlus(gainRatio*coeffs2List[i][j], jOff) 

279 

280 image -= xtalk 

281 else: 

282 nAmp = 4 

283 subtractCrosstalk(mi, nAmp, coeffs1List, coeffs2List, gainsPreampSig) 

284 

285 # 

286 # Clear the crosstalkStr bit in the original bright pixels, where tempStr 

287 # is set 

288 # 

289 msk = mi.getMask() 

290 temp = msk.getPlaneBitMask(tempStr) 

291 xtalk_temp = crosstalk | temp 

292 np_msk = msk.getArray() 

293 np_msk[np.where(np.bitwise_and(np_msk, xtalk_temp) == xtalk_temp)] &= ~crosstalk 

294 

295 try: 

296 msk.removeAndClearMaskPlane(tempStr, True) # added in afw #1853 

297 except AttributeError: 

298 afwDisplay.setMaskPlaneColor(tempStr, color="ignore") 

299 

300 

301def main(visit=131634, ccd=None, threshold=45000, nSample=1, showCoeffs=True, fixXTalk=True, 

302 plot=False, title=None): 

303 if ccd is None: 

304 visitList = list(range(nSample)) 

305 ccdList = ["simulated", ] 

306 else: 

307 ccdList = makeList(ccd) 

308 visitList = makeList(visit) 

309 

310 coeffs = estimateCoeffs(visitList, ccdList, threshold=45000, plot=plot, title=title) 

311 

312 if showCoeffs: 

313 printCoeffs(coeffs) 

314 

315 mi = readImage(visitList[0], ccdList[0]) 

316 if fixXTalk: 

317 subtractXTalk(mi, coeffs, threshold) 

318 

319 return mi, coeffs 

320 

321 

322class YagiCrosstalkConfig(pexConfig.Config): 

323 coeffs = pexConfig.ConfigField( 

324 dtype=CrosstalkYagiCoeffsConfig, 

325 doc="Crosstalk coefficients by Yagi+ 2012", 

326 ) 

327 crosstalkMaskPlane = pexConfig.Field( 

328 dtype=str, 

329 doc="Name of Mask plane for crosstalk corrected pixels", 

330 default="CROSSTALK", 

331 ) 

332 minPixelToMask = pexConfig.Field( 

333 dtype=float, 

334 doc="Minimum pixel value (in electrons) to cause crosstalkMaskPlane bit to be set", 

335 default=45000, 

336 ) 

337 

338 

339class YagiCrosstalkTask(pipeBase.Task): 

340 ConfigClass = YagiCrosstalkConfig 

341 

342 def run(self, exposure): 

343 coeffs1List = self.config.coeffs.getCoeffs1() # primary crosstalk 

344 coeffs2List = self.config.coeffs.getCoeffs2() # secondary crosstalk 

345 gainsPreampSig = self.config.coeffs.getGainsPreampSigboard() 

346 if not np.any(coeffs1List): 

347 self.log.info("No crosstalk info available. Skipping crosstalk corrections to CCD %s", 

348 exposure.getDetector().getId()) 

349 return 

350 

351 self.log.info("Applying crosstalk corrections to CCD %s based on Yagi+2012", 

352 exposure.getDetector().getId()) 

353 

354 ccdId = int(exposure.getDetector().getId().getSerial()) 

355 gainsPreampSigCcd = gainsPreampSig[ccdId] 

356 

357 subtractCrosstalkYagi(exposure.getMaskedImage(), coeffs1List, coeffs2List, 

358 gainsPreampSigCcd, 

359 self.config.minPixelToMask, self.config.crosstalkMaskPlane) 

360 

361 

362if __name__ == "__main__": 

363 main()