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, 

36 MeasureCrosstalkTask, CrosstalkTask, NullCrosstalkTask) 

37 

38try: 

39 display 

40except NameError: 

41 display = False 

42else: 

43 import lsst.afw.display as afwDisplay 

44 afwDisplay.setDefaultMaskTransparency(75) 

45 

46 

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

48 

49 

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

51 def setUp(self): 

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

64 # 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' 

95 detId = 1 

96 detSerial = 'serial' 

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 # Create amp info 

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

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

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

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

116 

117 amp = cameraGeom.Amplifier.Builder() 

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

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

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

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

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

123 amp.setReadoutCorner(corner) 

124 detBuilder.append(amp) 

125 

126 cam = camBuilder.finish() 

127 ccd = cam.get('detector') 

128 

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

130 self.exposure.setDetector(ccd) 

131 

132 self.corrected = construct(withoutCrosstalk) 

133 

134 if display: 

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

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

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

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

139 

140 def tearDown(self): 

141 del self.exposure 

142 del self.corrected 

143 

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

145 """Check that coefficients are as expected 

146 

147 Parameters 

148 ---------- 

149 coeff : `numpy.ndarray` 

150 Crosstalk coefficients. 

151 coeffErr : `numpy.ndarray` 

152 Crosstalk coefficient errors. 

153 coeffNum : `numpy.ndarray` 

154 Number of pixels to produce each coefficient. 

155 """ 

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

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

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

159 

160 for ii in range(self.numAmps): 

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

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

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

164 

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

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

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

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

169 

170 def checkSubtracted(self, exposure): 

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

172 

173 Parameters 

174 ---------- 

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

176 Crosstalk-subtracted exposure. 

177 """ 

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

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

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

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

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

183 

184 def testDirectAPI(self): 

185 """Test that individual function calls work""" 

186 config = MeasureCrosstalkTask.ConfigClass() 

187 measure = MeasureCrosstalkTask(config=config) 

188 ratios = measure.extractCrosstalkRatios(self.exposure, threshold=self.value - 1) 

189 coeff, coeffErr, coeffNum = measure.measureCrosstalkCoefficients(ratios) 

190 self.checkCoefficients(coeff, coeffErr, coeffNum) 

191 calib = CrosstalkCalib() 

192 calib.coeffs = coeff.transpose() 

193 calib.coeffErr = coeffErr.transpose() 

194 calib.coeffNum = coeffNum.transpose() 

195 calib.subtractCrosstalk(self.exposure, crosstalkCoeffs=coeff.transpose(), 

196 minPixelToMask=self.value - 1, 

197 crosstalkStr=self.crosstalkStr) 

198 self.checkSubtracted(self.exposure) 

199 

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

201 outPath += '.yaml' 

202 calib.writeText(outPath) 

203 

204 def testTaskAPI(self): 

205 """Test that the Tasks work 

206 

207 Checks both MeasureCrosstalkTask and the CrosstalkTask. 

208 """ 

209 # make exposure available to NullIsrTask 

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

211 exposure = self.exposure 

212 

213 class NullIsrTask(IsrTask): 

214 def runDataRef(self, dataRef): 

215 return Struct(exposure=exposure) 

216 

217 config = MeasureCrosstalkTask.ConfigClass() 

218 config.isr.retarget(NullIsrTask) 

219 config.threshold = self.value - 1 

220 measure = MeasureCrosstalkTask(config=config) 

221 fakeDataRef = Struct(dataId={'fake': 1}) 

222 coeff, coeffErr, coeffNum = measure.reduce([measure.runDataRef(fakeDataRef)]) 

223 self.checkCoefficients(coeff, coeffErr, coeffNum) 

224 

225 config = IsrTask.ConfigClass() 

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

227 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr 

228 isr = IsrTask(config=config) 

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

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

231 self.checkSubtracted(self.exposure) 

232 

233 def test_prepCrosstalk(self): 

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

235 crosstalkSources to find. 

236 """ 

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

238 task = CrosstalkTask() 

239 result = task.prepCrosstalk(dataRef) 

240 self.assertIsNone(result) 

241 

242 def test_nullCrosstalkTask(self): 

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

244 """ 

245 exposure = self.exposure 

246 task = NullCrosstalkTask() 

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

248 self.assertIsNone(result) 

249 

250 

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

252 pass 

253 

254 

255def setup_module(module): 

256 lsst.utils.tests.init() 

257 

258 

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

260 import sys 

261 setup_module(sys.modules[__name__]) 

262 unittest.main()