Coverage for tests/test_crosstalk.py: 16%
Shortcuts 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
Shortcuts 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/>.
22import unittest
23import itertools
24import tempfile
26import numpy as np
28import lsst.geom
29import lsst.utils.tests
30import lsst.afw.image
31import lsst.afw.table
32import lsst.afw.cameraGeom as cameraGeom
34from lsst.pipe.base import Struct
35from lsst.ip.isr import IsrTask, CrosstalkCalib, CrosstalkTask, NullCrosstalkTask
37try:
38 display
39except NameError:
40 display = False
41else:
42 import lsst.afw.display as afwDisplay
43 afwDisplay.setDefaultMaskTransparency(75)
46outputName = None # specify a name (as a string) to save the output crosstalk coeffs.
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"
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))
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
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)
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
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)
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)
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)
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)
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)]):
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)
143 cam = camBuilder.finish()
144 ccd1 = cam.get('detector 1')
145 ccd2 = cam.get('detector 2')
147 self.exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(construct(withCrosstalk)))
148 self.exposure.setDetector(ccd1)
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)
154 self.corrected = construct(withoutCrosstalk)
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")
162 def tearDown(self):
163 del self.exposure
164 del self.corrected
166 def checkCoefficients(self, coeff, coeffErr, coeffNum):
167 """Check that coefficients are as expected
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)
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)
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))
192 def checkSubtracted(self, exposure):
193 """Check that the subtracted image is as expected
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)
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)
215 outPath = tempfile.mktemp() if outputName is None else "{}-isrCrosstalk".format(outputName)
216 outPath += '.yaml'
217 calib.writeText(outPath)
219 def testTaskAPI(self):
220 """Test that the Tasks work
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
228 class NullIsrTask(IsrTask):
229 def runDataRef(self, dataRef):
230 return Struct(exposure=exposure)
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)
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)
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)
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]
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)
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)
278 def test_crosstalkIO(self):
279 """Test that crosstalk doesn't change on being converted to persistable formats."""
281 # Add the interchip crosstalk as in the previous test.
282 exposure = self.exposure
284 coeff = np.array(self.crosstalk).transpose()
285 calib = CrosstalkCalib().fromDetector(exposure.getDetector(), coeffVector=coeff)
286 # Now convert this into zero intra-chip, full inter-chip:
287 calib.interChip['detector 2'] = coeff
289 outPath = tempfile.mktemp() + '.yaml'
290 calib.writeText(outPath)
291 newCrosstalk = CrosstalkCalib().readText(outPath)
292 self.assertEqual(calib, newCrosstalk)
294 outPath = tempfile.mktemp() + '.fits'
295 calib.writeFits(outPath)
296 newCrosstalk = CrosstalkCalib().readFits(outPath)
297 self.assertEqual(calib, newCrosstalk)
300class MemoryTester(lsst.utils.tests.MemoryTestCase):
301 pass
304def setup_module(module):
305 lsst.utils.tests.init()
308if __name__ == "__main__": 308 ↛ 309line 308 didn't jump to line 309, because the condition on line 308 was never true
309 import sys
310 setup_module(sys.modules[__name__])
311 unittest.main()