Coverage for tests/test_crosstalk.py: 16%
169 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-15 10:24 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-15 10:24 +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/>.
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.ip.isr import IsrTask, CrosstalkCalib, NullCrosstalkTask
36try:
37 display
38except NameError:
39 display = False
40else:
41 import lsst.afw.display as afwDisplay
42 afwDisplay.setDefaultMaskTransparency(75)
45outputName = None # specify a name (as a string) to save the output crosstalk coeffs.
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"
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))
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
151 # correction.
152 self.ctSource = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(construct(withCrosstalk)))
153 self.ctSource.setDetector(ccd2)
155 self.corrected = construct(withoutCrosstalk)
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")
163 def tearDown(self):
164 del self.exposure
165 del self.corrected
167 def checkCoefficients(self, coeff, coeffErr, coeffNum):
168 """Check that coefficients are as expected
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)
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)
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))
193 def checkSubtracted(self, exposure):
194 """Check that the subtracted image is as expected
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)
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)
216 outPath = tempfile.mktemp() if outputName is None else "{}-isrCrosstalk".format(outputName)
217 outPath += '.yaml'
218 calib.writeText(outPath)
220 def testTaskAPI(self):
221 """Test that the Tasks work
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)
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)
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]
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)
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)
263 def test_crosstalkIO(self):
264 """Test that crosstalk doesn't change on being converted to persistable
265 formats.
266 """
268 # Add the interchip crosstalk as in the previous test.
269 exposure = self.exposure
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
276 outPath = tempfile.mktemp() + '.yaml'
277 calib.writeText(outPath)
278 newCrosstalk = CrosstalkCalib().readText(outPath)
279 self.assertEqual(calib, newCrosstalk)
281 outPath = tempfile.mktemp() + '.fits'
282 calib.writeFits(outPath)
283 newCrosstalk = CrosstalkCalib().readFits(outPath)
284 self.assertEqual(calib, newCrosstalk)
287class MemoryTester(lsst.utils.tests.MemoryTestCase):
288 pass
291def setup_module(module):
292 lsst.utils.tests.init()
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()