Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 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 distributions are razor-thin 

63 # 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 correction. 

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

152 self.ctSource.setDetector(ccd2) 

153 

154 self.corrected = construct(withoutCrosstalk) 

155 

156 if display: 

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

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

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

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

161 

162 def tearDown(self): 

163 del self.exposure 

164 del self.corrected 

165 

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

167 """Check that coefficients are as expected 

168 

169 Parameters 

170 ---------- 

171 coeff : `numpy.ndarray` 

172 Crosstalk coefficients. 

173 coeffErr : `numpy.ndarray` 

174 Crosstalk coefficient errors. 

175 coeffNum : `numpy.ndarray` 

176 Number of pixels to produce each coefficient. 

177 """ 

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

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

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

181 

182 for ii in range(self.numAmps): 

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

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

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

186 

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

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

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

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

191 

192 def checkSubtracted(self, exposure): 

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

194 

195 Parameters 

196 ---------- 

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

198 Crosstalk-subtracted exposure. 

199 """ 

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

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

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

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

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

205 

206 def testDirectAPI(self): 

207 """Test that individual function calls work""" 

208 calib = CrosstalkCalib() 

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

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

211 minPixelToMask=self.value - 1, 

212 crosstalkStr=self.crosstalkStr) 

213 self.checkSubtracted(self.exposure) 

214 

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

216 outPath += '.yaml' 

217 calib.writeText(outPath) 

218 

219 def testTaskAPI(self): 

220 """Test that the Tasks work 

221 

222 Checks both MeasureCrosstalkTask and the CrosstalkTask. 

223 """ 

224 # make exposure available to NullIsrTask 

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

226 exposure = self.exposure 

227 

228 class NullIsrTask(IsrTask): 

229 def runDataRef(self, dataRef): 

230 return Struct(exposure=exposure) 

231 

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

233 config = IsrTask.ConfigClass() 

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

235 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr 

236 isr = IsrTask(config=config) 

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

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

239 self.checkSubtracted(self.exposure) 

240 

241 def test_prepCrosstalk(self): 

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

243 crosstalkSources to find. 

244 """ 

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

246 task = CrosstalkTask() 

247 result = task.prepCrosstalk(dataRef) 

248 self.assertIsNone(result) 

249 

250 def test_nullCrosstalkTask(self): 

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

252 """ 

253 exposure = self.exposure 

254 task = NullCrosstalkTask() 

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

256 self.assertIsNone(result) 

257 

258 def test_interChip(self): 

259 """Test that passing an external exposure as the crosstalk source works. 

260 """ 

261 exposure = self.exposure 

262 ctSources = [self.ctSource] 

263 

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

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

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

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

268 calib.coeffs = np.zeros_like(coeff) 

269 

270 # Process and check as above 

271 config = IsrTask.ConfigClass() 

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

273 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr 

274 isr = IsrTask(config=config) 

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

276 self.checkSubtracted(exposure) 

277 

278 

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

280 pass 

281 

282 

283def setup_module(module): 

284 lsst.utils.tests.init() 

285 

286 

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

288 import sys 

289 setup_module(sys.modules[__name__]) 

290 unittest.main()