Coverage for tests/test_crosstalk.py: 16%

169 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-21 02:22 -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 

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 def setUp(self): 

50 width, height = 250, 500 

51 self.numAmps = 4 

52 numPixelsPerAmp = 1000 

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

54 # amp. 

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

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

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

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

59 self.value = 12345 

60 self.crosstalkStr = "XTLK" 

61 

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

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

64 rng = np.random.RandomState(12345) 

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

66 

67 # Create amp images 

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

69 for image in withoutCrosstalk: 

70 image.set(0) 

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

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

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

74 

75 # Add in crosstalk 

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

77 for ii, iImage in enumerate(withCrosstalk): 

78 for jj, jImage in enumerate(withoutCrosstalk): 

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

80 iImage.scaledPlus(value, jImage) 

81 

82 # Put amp images together 

83 def construct(imageList): 

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

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

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

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

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

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

90 return image 

91 

92 # Construct detector 

93 detName = 'detector 1' 

94 detId = 1 

95 detSerial = 'serial 1' 

96 orientation = cameraGeom.Orientation() 

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

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

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

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

101 

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

103 detBuilder = camBuilder.add(detName, detId) 

104 detBuilder.setSerial(detSerial) 

105 detBuilder.setBBox(bbox) 

106 detBuilder.setOrientation(orientation) 

107 detBuilder.setPixelSize(pixelSize) 

108 detBuilder.setCrosstalk(crosstalk) 

109 

110 # Construct second detector in this fake camera 

111 detName = 'detector 2' 

112 detId = 2 

113 detSerial = 'serial 2' 

114 orientation = cameraGeom.Orientation() 

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

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

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

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

119 

120 detBuilder2 = camBuilder.add(detName, detId) 

121 detBuilder2.setSerial(detSerial) 

122 detBuilder2.setBBox(bbox) 

123 detBuilder2.setOrientation(orientation) 

124 detBuilder2.setPixelSize(pixelSize) 

125 detBuilder2.setCrosstalk(crosstalk) 

126 

127 # Create amp info 

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

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

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

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

132 

133 amp = cameraGeom.Amplifier.Builder() 

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

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

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

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

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

139 amp.setReadoutCorner(corner) 

140 detBuilder.append(amp) 

141 detBuilder2.append(amp) 

142 

143 cam = camBuilder.finish() 

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

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

146 

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

148 self.exposure.setDetector(ccd1) 

149 

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

151 # correction. 

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

153 self.ctSource.setDetector(ccd2) 

154 

155 self.corrected = construct(withoutCrosstalk) 

156 

157 if display: 

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

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

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

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

162 

163 def tearDown(self): 

164 del self.exposure 

165 del self.corrected 

166 

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

168 """Check that coefficients are as expected 

169 

170 Parameters 

171 ---------- 

172 coeff : `numpy.ndarray` 

173 Crosstalk coefficients. 

174 coeffErr : `numpy.ndarray` 

175 Crosstalk coefficient errors. 

176 coeffNum : `numpy.ndarray` 

177 Number of pixels to produce each coefficient. 

178 """ 

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

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

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

182 

183 for ii in range(self.numAmps): 

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

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

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

187 

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

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

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

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

192 

193 def checkSubtracted(self, exposure): 

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

195 

196 Parameters 

197 ---------- 

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

199 Crosstalk-subtracted exposure. 

200 """ 

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

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

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

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

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

206 

207 def testDirectAPI(self): 

208 """Test that individual function calls work""" 

209 calib = CrosstalkCalib() 

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

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

212 minPixelToMask=self.value - 1, 

213 crosstalkStr=self.crosstalkStr) 

214 self.checkSubtracted(self.exposure) 

215 

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

217 outPath += '.yaml' 

218 calib.writeText(outPath) 

219 

220 def testTaskAPI(self): 

221 """Test that the Tasks work 

222 

223 Checks both MeasureCrosstalkTask and the CrosstalkTask. 

224 """ 

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

226 config = IsrTask.ConfigClass() 

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

228 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr 

229 isr = IsrTask(config=config) 

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

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

232 self.checkSubtracted(self.exposure) 

233 

234 def test_nullCrosstalkTask(self): 

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

236 """ 

237 exposure = self.exposure 

238 task = NullCrosstalkTask() 

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

240 self.assertIsNone(result) 

241 

242 def test_interChip(self): 

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

244 works. 

245 """ 

246 exposure = self.exposure 

247 ctSources = [self.ctSource] 

248 

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

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

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

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

253 calib.coeffs = np.zeros_like(coeff) 

254 

255 # Process and check as above 

256 config = IsrTask.ConfigClass() 

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

258 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr 

259 isr = IsrTask(config=config) 

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

261 self.checkSubtracted(exposure) 

262 

263 def test_crosstalkIO(self): 

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

265 formats. 

266 """ 

267 

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

269 exposure = self.exposure 

270 

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

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

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

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

275 

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

277 calib.writeText(outPath) 

278 newCrosstalk = CrosstalkCalib().readText(outPath) 

279 self.assertEqual(calib, newCrosstalk) 

280 

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

282 calib.writeFits(outPath) 

283 newCrosstalk = CrosstalkCalib().readFits(outPath) 

284 self.assertEqual(calib, newCrosstalk) 

285 

286 

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

288 pass 

289 

290 

291def setup_module(module): 

292 lsst.utils.tests.init() 

293 

294 

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

296 import sys 

297 setup_module(sys.modules[__name__]) 

298 unittest.main()