Coverage for tests/test_crosstalk.py: 14%

197 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-03 03:19 -0700

1# This file is part of ip_isr. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://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 <https://www.gnu.org/licenses/>. 

21 

22import unittest 

23import itertools 

24import tempfile 

25 

26import numpy as np 

27 

28import lsst.geom 

29import lsst.utils.tests 

30import lsst.afw.image 

31import lsst.afw.table 

32import lsst.afw.cameraGeom as cameraGeom 

33 

34from lsst.ip.isr import IsrTask, CrosstalkCalib, NullCrosstalkTask, IsrTaskLSST 

35 

36try: 

37 display 

38except NameError: 

39 display = False 

40else: 

41 import lsst.afw.display as afwDisplay 

42 afwDisplay.setDefaultMaskTransparency(75) 

43 

44 

45outputName = None # specify a name (as a string) to save the output crosstalk coeffs. 

46 

47 

48class CrosstalkTestCase(lsst.utils.tests.TestCase): 

49 # Define a new set up function to be able to pass 

50 # NL-crosstalk correction boolean. 

51 def setUp_general(self, doSqrCrosstalk=False): 

52 width, height = 250, 500 

53 self.numAmps = 4 

54 numPixelsPerAmp = 1000 

55 # crosstalk[i][j] is the fraction of the j-th amp present on the i-th 

56 # amp. 

57 self.crosstalk = [[0.0, 1e-4, 2e-4, 3e-4], 

58 [3e-4, 0.0, 2e-4, 1e-4], 

59 [4e-4, 5e-4, 0.0, 6e-4], 

60 [7e-4, 8e-4, 9e-4, 0.0]] 

61 if doSqrCrosstalk: 

62 # Measured quadratic crosstalk from spots is O[-10], O[-11] 

63 self.crosstalk_sqr = [[0.0, 1e-10, 2e-10, 3e-10], 

64 [3e-10, 0.0, 2e-10, 1e-10], 

65 [4e-10, 5e-10, 0.0, 6e-10], 

66 [7e-10, 8e-10, 9e-10, 0.0]] 

67 else: 

68 self.crosstalk_sqr = np.zeros((self.numAmps, self.numAmps)) 

69 self.value = 12345 

70 self.crosstalkStr = "XTLK" 

71 

72 # A bit of noise is important, because otherwise the pixel 

73 # distributions are razor-thin and then rejection doesn't work. 

74 rng = np.random.RandomState(12345) 

75 self.noise = rng.normal(0.0, 0.1, (2*height, 2*width)) 

76 

77 # Create amp images 

78 withoutCrosstalk = [lsst.afw.image.ImageF(width, height) for _ in range(self.numAmps)] 

79 for image in withoutCrosstalk: 

80 image.set(0) 

81 xx = rng.randint(0, width, numPixelsPerAmp) 

82 yy = rng.randint(0, height, numPixelsPerAmp) 

83 image.getArray()[yy, xx] = self.value 

84 

85 # Add in crosstalk 

86 withCrosstalk = [image.Factory(image, True) for image in withoutCrosstalk] 

87 for ii, iImage in enumerate(withCrosstalk): 

88 for jj, jImage in enumerate(withoutCrosstalk): 

89 value = self.crosstalk[ii][jj] 

90 iImage.scaledPlus(value, jImage) 

91 # NL crosstalk will be added if boolean argument is True 

92 jImageSqr = jImage.clone() 

93 jImageSqr.scaledMultiplies(1.0, jImage) 

94 valueSqr = self.crosstalk_sqr[ii][jj] 

95 iImage.scaledPlus(valueSqr, jImageSqr) 

96 

97 # Put amp images together 

98 def construct(imageList): 

99 image = lsst.afw.image.ImageF(2*width, 2*height) 

100 image.getArray()[:height, :width] = imageList[0].getArray() 

101 image.getArray()[:height, width:] = imageList[1].getArray()[:, ::-1] # flip in x 

102 image.getArray()[height:, :width] = imageList[2].getArray()[::-1, :] # flip in y 

103 image.getArray()[height:, width:] = imageList[3].getArray()[::-1, ::-1] # flip in x and y 

104 image.getArray()[:] += self.noise 

105 return image 

106 

107 # Construct detector 

108 detName = 'detector 1' 

109 detId = 1 

110 detSerial = 'serial 1' 

111 orientation = cameraGeom.Orientation() 

112 pixelSize = lsst.geom.Extent2D(1, 1) 

113 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

114 lsst.geom.Extent2I(2*width, 2*height)) 

115 crosstalk = np.array(self.crosstalk, dtype=np.float32) 

116 

117 camBuilder = cameraGeom.Camera.Builder("fakeCam") 

118 detBuilder = camBuilder.add(detName, detId) 

119 detBuilder.setSerial(detSerial) 

120 detBuilder.setBBox(bbox) 

121 detBuilder.setOrientation(orientation) 

122 detBuilder.setPixelSize(pixelSize) 

123 detBuilder.setCrosstalk(crosstalk) 

124 

125 # Construct second detector in this fake camera 

126 detName = 'detector 2' 

127 detId = 2 

128 detSerial = 'serial 2' 

129 orientation = cameraGeom.Orientation() 

130 pixelSize = lsst.geom.Extent2D(1, 1) 

131 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), 

132 lsst.geom.Extent2I(2*width, 2*height)) 

133 crosstalk = np.array(self.crosstalk, dtype=np.float32) 

134 

135 detBuilder2 = camBuilder.add(detName, detId) 

136 detBuilder2.setSerial(detSerial) 

137 detBuilder2.setBBox(bbox) 

138 detBuilder2.setOrientation(orientation) 

139 detBuilder2.setPixelSize(pixelSize) 

140 detBuilder2.setCrosstalk(crosstalk) 

141 

142 # Create amp info 

143 for ii, (xx, yy, corner) in enumerate([(0, 0, lsst.afw.cameraGeom.ReadoutCorner.LL), 

144 (width, 0, lsst.afw.cameraGeom.ReadoutCorner.LR), 

145 (0, height, lsst.afw.cameraGeom.ReadoutCorner.UL), 

146 (width, height, lsst.afw.cameraGeom.ReadoutCorner.UR)]): 

147 

148 amp = cameraGeom.Amplifier.Builder() 

149 amp.setName("amp %d" % ii) 

150 amp.setBBox(lsst.geom.Box2I(lsst.geom.Point2I(xx, yy), 

151 lsst.geom.Extent2I(width, height))) 

152 amp.setRawDataBBox(lsst.geom.Box2I(lsst.geom.Point2I(xx, yy), 

153 lsst.geom.Extent2I(width, height))) 

154 amp.setReadoutCorner(corner) 

155 detBuilder.append(amp) 

156 detBuilder2.append(amp) 

157 

158 cam = camBuilder.finish() 

159 ccd1 = cam.get('detector 1') 

160 ccd2 = cam.get('detector 2') 

161 

162 self.exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(construct(withCrosstalk))) 

163 self.exposure.setDetector(ccd1) 

164 

165 # Create a single ctSource that will be used for interChip CT 

166 # correction. 

167 self.ctSource = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(construct(withCrosstalk))) 

168 self.ctSource.setDetector(ccd2) 

169 

170 self.corrected = construct(withoutCrosstalk) 

171 

172 if display: 

173 disp = lsst.afw.display.Display(frame=1) 

174 disp.mtv(self.exposure, title="exposure") 

175 disp = lsst.afw.display.Display(frame=0) 

176 disp.mtv(self.corrected, title="corrected exposure") 

177 

178 def tearDown(self): 

179 del self.exposure 

180 del self.corrected 

181 

182 def checkCoefficients(self, coeff, coeffErr, coeffNum): 

183 """Check that coefficients are as expected 

184 

185 Parameters 

186 ---------- 

187 coeff : `numpy.ndarray` 

188 Crosstalk coefficients. 

189 coeffErr : `numpy.ndarray` 

190 Crosstalk coefficient errors. 

191 coeffNum : `numpy.ndarray` 

192 Number of pixels to produce each coefficient. 

193 """ 

194 for matrix in (coeff, coeffErr, coeffNum): 

195 self.assertEqual(matrix.shape, (self.numAmps, self.numAmps)) 

196 self.assertFloatsAlmostEqual(coeff, np.array(self.crosstalk), atol=1.0e-6) 

197 

198 for ii in range(self.numAmps): 

199 self.assertEqual(coeff[ii, ii], 0.0) 

200 self.assertTrue(np.isnan(coeffErr[ii, ii])) 

201 self.assertEqual(coeffNum[ii, ii], 1) 

202 

203 self.assertTrue(np.all(coeffErr[ii, jj] > 0 for ii, jj in 

204 itertools.product(range(self.numAmps), range(self.numAmps)) if ii != jj)) 

205 self.assertTrue(np.all(coeffNum[ii, jj] > 0 for ii, jj in 

206 itertools.product(range(self.numAmps), range(self.numAmps)) if ii != jj)) 

207 

208 def checkSubtracted(self, exposure): 

209 """Check that the subtracted image is as expected 

210 

211 Parameters 

212 ---------- 

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

214 Crosstalk-subtracted exposure. 

215 """ 

216 image = exposure.getMaskedImage().getImage() 

217 mask = exposure.getMaskedImage().getMask() 

218 self.assertFloatsAlmostEqual(image.getArray(), self.corrected.getArray(), atol=2.0e-2) 

219 self.assertIn(self.crosstalkStr, mask.getMaskPlaneDict()) 

220 self.assertGreater((mask.getArray() & mask.getPlaneBitMask(self.crosstalkStr) > 0).sum(), 0) 

221 

222 def checkTaskAPI_NL(self, this_isr_task): 

223 """Check the the crosstalk task under different ISR tasks. 

224 (e.g., IsrTask and IsrTaskLSST) 

225 

226 Parameters 

227 ---------- 

228 this_isr_task : `lsst.pipe.base.PipelineTask` 

229 """ 

230 self.setUp_general(doSqrCrosstalk=True) 

231 coeff = np.array(self.crosstalk).transpose() 

232 coeffSqr = np.array(self.crosstalk_sqr).transpose() 

233 config = this_isr_task.ConfigClass() 

234 config.crosstalk.minPixelToMask = self.value - 1 

235 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr 

236 # Turn on the NL correction 

237 config.crosstalk.doQuadraticCrosstalkCorrection = True 

238 isr = this_isr_task(config=config) 

239 calib = CrosstalkCalib().fromDetector(self.exposure.getDetector(), 

240 coeffVector=coeff, 

241 coeffSqrVector=coeffSqr) 

242 isr.crosstalk.run(self.exposure, crosstalk=calib) 

243 self.checkSubtracted(self.exposure) 

244 

245 def testDirectAPI(self): 

246 """Test that individual function calls work""" 

247 self.setUp_general() 

248 calib = CrosstalkCalib() 

249 calib.coeffs = np.array(self.crosstalk).transpose() 

250 calib.subtractCrosstalk(self.exposure, crosstalkCoeffs=calib.coeffs, 

251 minPixelToMask=self.value - 1, 

252 crosstalkStr=self.crosstalkStr) 

253 self.checkSubtracted(self.exposure) 

254 

255 outPath = tempfile.mktemp() if outputName is None else "{}-isrCrosstalk".format(outputName) 

256 outPath += '.yaml' 

257 calib.writeText(outPath) 

258 

259 def testTaskAPI(self): 

260 """Test that the Tasks work 

261 

262 Checks both MeasureCrosstalkTask and the CrosstalkTask. 

263 """ 

264 self.setUp_general() 

265 coeff = np.array(self.crosstalk).transpose() 

266 coeffSqr = np.array(self.crosstalk_sqr).transpose() 

267 config = IsrTask.ConfigClass() 

268 config.crosstalk.minPixelToMask = self.value - 1 

269 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr 

270 isr = IsrTask(config=config) 

271 calib = CrosstalkCalib().fromDetector(self.exposure.getDetector(), 

272 coeffVector=coeff, 

273 coeffSqrVector=coeffSqr) 

274 isr.crosstalk.run(self.exposure, crosstalk=calib) 

275 self.checkSubtracted(self.exposure) 

276 

277 def testTaskAPI_NL(self): 

278 """Test that the Tasks work 

279 

280 Checks both MeasureCrosstalkTask and the CrosstalkTask. 

281 This test is for the quadratic (non-linear) corsstalk 

282 correction. 

283 """ 

284 for this_isr_task in [IsrTask, IsrTaskLSST]: 

285 self.checkTaskAPI_NL(this_isr_task) 

286 

287 def test_nullCrosstalkTask(self): 

288 """Test that the null crosstalk task does not create an error. 

289 """ 

290 self.setUp_general() 

291 exposure = self.exposure 

292 task = NullCrosstalkTask() 

293 result = task.run(exposure, crosstalkSources=None) 

294 self.assertIsNone(result) 

295 

296 def test_interChip(self): 

297 """Test that passing an external exposure as the crosstalk source 

298 works. 

299 """ 

300 self.setUp_general() 

301 exposure = self.exposure 

302 ctSources = [self.ctSource] 

303 

304 coeff = np.array(self.crosstalk).transpose() 

305 calib = CrosstalkCalib().fromDetector(exposure.getDetector(), coeffVector=coeff) 

306 # Now convert this into zero intra-chip, full inter-chip: 

307 calib.interChip['detector 2'] = coeff 

308 calib.coeffs = np.zeros_like(coeff) 

309 

310 # Process and check as above 

311 config = IsrTask.ConfigClass() 

312 config.crosstalk.minPixelToMask = self.value - 1 

313 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr 

314 isr = IsrTask(config=config) 

315 isr.crosstalk.run(exposure, crosstalk=calib, crosstalkSources=ctSources) 

316 self.checkSubtracted(exposure) 

317 

318 def test_crosstalkIO(self): 

319 """Test that crosstalk doesn't change on being converted to persistable 

320 formats. 

321 """ 

322 self.setUp_general() 

323 # Add the interchip crosstalk as in the previous test. 

324 exposure = self.exposure 

325 

326 coeff = np.array(self.crosstalk).transpose() 

327 calib = CrosstalkCalib().fromDetector(exposure.getDetector(), coeffVector=coeff) 

328 # Now convert this into zero intra-chip, full inter-chip: 

329 calib.interChip['detector 2'] = coeff 

330 

331 outPath = tempfile.mktemp() + '.yaml' 

332 calib.writeText(outPath) 

333 newCrosstalk = CrosstalkCalib().readText(outPath) 

334 self.assertEqual(calib, newCrosstalk) 

335 

336 outPath = tempfile.mktemp() + '.fits' 

337 calib.writeFits(outPath) 

338 newCrosstalk = CrosstalkCalib().readFits(outPath) 

339 self.assertEqual(calib, newCrosstalk) 

340 

341 

342class MemoryTester(lsst.utils.tests.MemoryTestCase): 

343 pass 

344 

345 

346def setup_module(module): 

347 lsst.utils.tests.init() 

348 

349 

350if __name__ == "__main__": 350 ↛ 351line 350 didn't jump to line 351, because the condition on line 350 was never true

351 import sys 

352 setup_module(sys.modules[__name__]) 

353 unittest.main()