Coverage for tests/test_crosstalk.py: 16%

179 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-07 20:32 +0000

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.pipe.base import Struct 

35from lsst.ip.isr import IsrTask, CrosstalkCalib, CrosstalkTask, NullCrosstalkTask 

36 

37try: 

38 display 

39except NameError: 

40 display = False 

41else: 

42 import lsst.afw.display as afwDisplay 

43 afwDisplay.setDefaultMaskTransparency(75) 

44 

45 

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

47 

48 

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

50 def setUp(self): 

51 width, height = 250, 500 

52 self.numAmps = 4 

53 numPixelsPerAmp = 1000 

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

55 # amp. 

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

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

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

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

60 self.value = 12345 

61 self.crosstalkStr = "XTLK" 

62 

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

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

65 rng = np.random.RandomState(12345) 

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

67 

68 # Create amp images 

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

70 for image in withoutCrosstalk: 

71 image.set(0) 

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

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

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

75 

76 # Add in crosstalk 

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

78 for ii, iImage in enumerate(withCrosstalk): 

79 for jj, jImage in enumerate(withoutCrosstalk): 

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

81 iImage.scaledPlus(value, jImage) 

82 

83 # Put amp images together 

84 def construct(imageList): 

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

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

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

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

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

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

91 return image 

92 

93 # Construct detector 

94 detName = 'detector 1' 

95 detId = 1 

96 detSerial = 'serial 1' 

97 orientation = cameraGeom.Orientation() 

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

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

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

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

102 

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

104 detBuilder = camBuilder.add(detName, detId) 

105 detBuilder.setSerial(detSerial) 

106 detBuilder.setBBox(bbox) 

107 detBuilder.setOrientation(orientation) 

108 detBuilder.setPixelSize(pixelSize) 

109 detBuilder.setCrosstalk(crosstalk) 

110 

111 # Construct second detector in this fake camera 

112 detName = 'detector 2' 

113 detId = 2 

114 detSerial = 'serial 2' 

115 orientation = cameraGeom.Orientation() 

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

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

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

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

120 

121 detBuilder2 = camBuilder.add(detName, detId) 

122 detBuilder2.setSerial(detSerial) 

123 detBuilder2.setBBox(bbox) 

124 detBuilder2.setOrientation(orientation) 

125 detBuilder2.setPixelSize(pixelSize) 

126 detBuilder2.setCrosstalk(crosstalk) 

127 

128 # Create amp info 

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

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

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

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

133 

134 amp = cameraGeom.Amplifier.Builder() 

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

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

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

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

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

140 amp.setReadoutCorner(corner) 

141 detBuilder.append(amp) 

142 detBuilder2.append(amp) 

143 

144 cam = camBuilder.finish() 

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

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

147 

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

149 self.exposure.setDetector(ccd1) 

150 

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

152 # correction. 

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

154 self.ctSource.setDetector(ccd2) 

155 

156 self.corrected = construct(withoutCrosstalk) 

157 

158 if display: 

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

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

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

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

163 

164 def tearDown(self): 

165 del self.exposure 

166 del self.corrected 

167 

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

169 """Check that coefficients are as expected 

170 

171 Parameters 

172 ---------- 

173 coeff : `numpy.ndarray` 

174 Crosstalk coefficients. 

175 coeffErr : `numpy.ndarray` 

176 Crosstalk coefficient errors. 

177 coeffNum : `numpy.ndarray` 

178 Number of pixels to produce each coefficient. 

179 """ 

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

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

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

183 

184 for ii in range(self.numAmps): 

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

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

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

188 

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

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

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

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

193 

194 def checkSubtracted(self, exposure): 

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

196 

197 Parameters 

198 ---------- 

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

200 Crosstalk-subtracted exposure. 

201 """ 

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

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

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

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

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

207 

208 def testDirectAPI(self): 

209 """Test that individual function calls work""" 

210 calib = CrosstalkCalib() 

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

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

213 minPixelToMask=self.value - 1, 

214 crosstalkStr=self.crosstalkStr) 

215 self.checkSubtracted(self.exposure) 

216 

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

218 outPath += '.yaml' 

219 calib.writeText(outPath) 

220 

221 def testTaskAPI(self): 

222 """Test that the Tasks work 

223 

224 Checks both MeasureCrosstalkTask and the CrosstalkTask. 

225 """ 

226 # make exposure available to NullIsrTask 

227 # without NullIsrTask's `self` hiding this test class's `self` 

228 exposure = self.exposure 

229 

230 class NullIsrTask(IsrTask): 

231 def runDataRef(self, dataRef): 

232 return Struct(exposure=exposure) 

233 

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

235 config = IsrTask.ConfigClass() 

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

237 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr 

238 isr = IsrTask(config=config) 

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

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

241 self.checkSubtracted(self.exposure) 

242 

243 def test_prepCrosstalk(self): 

244 """Test that prep crosstalk does not error when given a dataRef with no 

245 crosstalkSources to find. 

246 """ 

247 dataRef = Struct(dataId={'fake': 1}) 

248 task = CrosstalkTask() 

249 result = task.prepCrosstalk(dataRef) 

250 self.assertIsNone(result) 

251 

252 def test_nullCrosstalkTask(self): 

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

254 """ 

255 exposure = self.exposure 

256 task = NullCrosstalkTask() 

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

258 self.assertIsNone(result) 

259 

260 def test_interChip(self): 

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

262 works. 

263 """ 

264 exposure = self.exposure 

265 ctSources = [self.ctSource] 

266 

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

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

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

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

271 calib.coeffs = np.zeros_like(coeff) 

272 

273 # Process and check as above 

274 config = IsrTask.ConfigClass() 

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

276 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr 

277 isr = IsrTask(config=config) 

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

279 self.checkSubtracted(exposure) 

280 

281 def test_crosstalkIO(self): 

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

283 formats. 

284 """ 

285 

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

287 exposure = self.exposure 

288 

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

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

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

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

293 

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

295 calib.writeText(outPath) 

296 newCrosstalk = CrosstalkCalib().readText(outPath) 

297 self.assertEqual(calib, newCrosstalk) 

298 

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

300 calib.writeFits(outPath) 

301 newCrosstalk = CrosstalkCalib().readFits(outPath) 

302 self.assertEqual(calib, newCrosstalk) 

303 

304 

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

306 pass 

307 

308 

309def setup_module(module): 

310 lsst.utils.tests.init() 

311 

312 

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

314 import sys 

315 setup_module(sys.modules[__name__]) 

316 unittest.main()