Coverage for tests/test_crosstalk.py: 17%
179 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-29 03:04 -0700
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-29 03:04 -0700
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
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"
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))
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
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)
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
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)
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)
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)
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)
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)]):
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)
144 cam = camBuilder.finish()
145 ccd1 = cam.get('detector 1')
146 ccd2 = cam.get('detector 2')
148 self.exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(construct(withCrosstalk)))
149 self.exposure.setDetector(ccd1)
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)
156 self.corrected = construct(withoutCrosstalk)
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")
164 def tearDown(self):
165 del self.exposure
166 del self.corrected
168 def checkCoefficients(self, coeff, coeffErr, coeffNum):
169 """Check that coefficients are as expected
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)
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)
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))
194 def checkSubtracted(self, exposure):
195 """Check that the subtracted image is as expected
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)
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)
217 outPath = tempfile.mktemp() if outputName is None else "{}-isrCrosstalk".format(outputName)
218 outPath += '.yaml'
219 calib.writeText(outPath)
221 def testTaskAPI(self):
222 """Test that the Tasks work
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
230 class NullIsrTask(IsrTask):
231 def runDataRef(self, dataRef):
232 return Struct(exposure=exposure)
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)
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)
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)
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]
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)
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)
281 def test_crosstalkIO(self):
282 """Test that crosstalk doesn't change on being converted to persistable
283 formats.
284 """
286 # Add the interchip crosstalk as in the previous test.
287 exposure = self.exposure
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
294 outPath = tempfile.mktemp() + '.yaml'
295 calib.writeText(outPath)
296 newCrosstalk = CrosstalkCalib().readText(outPath)
297 self.assertEqual(calib, newCrosstalk)
299 outPath = tempfile.mktemp() + '.fits'
300 calib.writeFits(outPath)
301 newCrosstalk = CrosstalkCalib().readFits(outPath)
302 self.assertEqual(calib, newCrosstalk)
305class MemoryTester(lsst.utils.tests.MemoryTestCase):
306 pass
309def setup_module(module):
310 lsst.utils.tests.init()
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()