Coverage for tests/test_crosstalk.py: 14%
197 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-01 04:11 -0700
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-01 04:11 -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.ip.isr import IsrTask, CrosstalkCalib, NullCrosstalkTask, IsrTaskLSST
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 # Define a new set up function to be able to pass
50 # NL-crosstalk correction boolean.
51 def setUp_general(self, doSqrCrosstalk=False):
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
56 # amp.
57 self.crosstalk = [[0.0, 1e-4, 2e-4, 3e-4],
58 [3e-4, 0.0, 2e-4, 1e-4],
59 [4e-4, 5e-4, 0.0, 6e-4],
60 [7e-4, 8e-4, 9e-4, 0.0]]
61 if doSqrCrosstalk:
62 # Measured quadratic crosstalk from spots is O[-10], O[-11]
63 self.crosstalk_sqr = [[0.0, 1e-10, 2e-10, 3e-10],
64 [3e-10, 0.0, 2e-10, 1e-10],
65 [4e-10, 5e-10, 0.0, 6e-10],
66 [7e-10, 8e-10, 9e-10, 0.0]]
67 else:
68 self.crosstalk_sqr = np.zeros((self.numAmps, self.numAmps))
69 self.value = 12345
70 self.crosstalkStr = "XTLK"
72 # A bit of noise is important, because otherwise the pixel
73 # distributions are razor-thin and then rejection doesn't work.
74 rng = np.random.RandomState(12345)
75 self.noise = rng.normal(0.0, 0.1, (2*height, 2*width))
77 # Create amp images
78 withoutCrosstalk = [lsst.afw.image.ImageF(width, height) for _ in range(self.numAmps)]
79 for image in withoutCrosstalk:
80 image.set(0)
81 xx = rng.randint(0, width, numPixelsPerAmp)
82 yy = rng.randint(0, height, numPixelsPerAmp)
83 image.getArray()[yy, xx] = self.value
85 # Add in crosstalk
86 withCrosstalk = [image.Factory(image, True) for image in withoutCrosstalk]
87 for ii, iImage in enumerate(withCrosstalk):
88 for jj, jImage in enumerate(withoutCrosstalk):
89 value = self.crosstalk[ii][jj]
90 iImage.scaledPlus(value, jImage)
91 # NL crosstalk will be added if boolean argument is True
92 jImageSqr = jImage.clone()
93 jImageSqr.scaledMultiplies(1.0, jImage)
94 valueSqr = self.crosstalk_sqr[ii][jj]
95 iImage.scaledPlus(valueSqr, jImageSqr)
97 # Put amp images together
98 def construct(imageList):
99 image = lsst.afw.image.ImageF(2*width, 2*height)
100 image.getArray()[:height, :width] = imageList[0].getArray()
101 image.getArray()[:height, width:] = imageList[1].getArray()[:, ::-1] # flip in x
102 image.getArray()[height:, :width] = imageList[2].getArray()[::-1, :] # flip in y
103 image.getArray()[height:, width:] = imageList[3].getArray()[::-1, ::-1] # flip in x and y
104 image.getArray()[:] += self.noise
105 return image
107 # Construct detector
108 detName = 'detector 1'
109 detId = 1
110 detSerial = 'serial 1'
111 orientation = cameraGeom.Orientation()
112 pixelSize = lsst.geom.Extent2D(1, 1)
113 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
114 lsst.geom.Extent2I(2*width, 2*height))
115 crosstalk = np.array(self.crosstalk, dtype=np.float32)
117 camBuilder = cameraGeom.Camera.Builder("fakeCam")
118 detBuilder = camBuilder.add(detName, detId)
119 detBuilder.setSerial(detSerial)
120 detBuilder.setBBox(bbox)
121 detBuilder.setOrientation(orientation)
122 detBuilder.setPixelSize(pixelSize)
123 detBuilder.setCrosstalk(crosstalk)
125 # Construct second detector in this fake camera
126 detName = 'detector 2'
127 detId = 2
128 detSerial = 'serial 2'
129 orientation = cameraGeom.Orientation()
130 pixelSize = lsst.geom.Extent2D(1, 1)
131 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0),
132 lsst.geom.Extent2I(2*width, 2*height))
133 crosstalk = np.array(self.crosstalk, dtype=np.float32)
135 detBuilder2 = camBuilder.add(detName, detId)
136 detBuilder2.setSerial(detSerial)
137 detBuilder2.setBBox(bbox)
138 detBuilder2.setOrientation(orientation)
139 detBuilder2.setPixelSize(pixelSize)
140 detBuilder2.setCrosstalk(crosstalk)
142 # Create amp info
143 for ii, (xx, yy, corner) in enumerate([(0, 0, lsst.afw.cameraGeom.ReadoutCorner.LL),
144 (width, 0, lsst.afw.cameraGeom.ReadoutCorner.LR),
145 (0, height, lsst.afw.cameraGeom.ReadoutCorner.UL),
146 (width, height, lsst.afw.cameraGeom.ReadoutCorner.UR)]):
148 amp = cameraGeom.Amplifier.Builder()
149 amp.setName("amp %d" % ii)
150 amp.setBBox(lsst.geom.Box2I(lsst.geom.Point2I(xx, yy),
151 lsst.geom.Extent2I(width, height)))
152 amp.setRawDataBBox(lsst.geom.Box2I(lsst.geom.Point2I(xx, yy),
153 lsst.geom.Extent2I(width, height)))
154 amp.setReadoutCorner(corner)
155 detBuilder.append(amp)
156 detBuilder2.append(amp)
158 cam = camBuilder.finish()
159 ccd1 = cam.get('detector 1')
160 ccd2 = cam.get('detector 2')
162 self.exposure = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(construct(withCrosstalk)))
163 self.exposure.setDetector(ccd1)
165 # Create a single ctSource that will be used for interChip CT
166 # correction.
167 self.ctSource = lsst.afw.image.makeExposure(lsst.afw.image.makeMaskedImage(construct(withCrosstalk)))
168 self.ctSource.setDetector(ccd2)
170 self.corrected = construct(withoutCrosstalk)
172 if display:
173 disp = lsst.afw.display.Display(frame=1)
174 disp.mtv(self.exposure, title="exposure")
175 disp = lsst.afw.display.Display(frame=0)
176 disp.mtv(self.corrected, title="corrected exposure")
178 def tearDown(self):
179 del self.exposure
180 del self.corrected
182 def checkCoefficients(self, coeff, coeffErr, coeffNum):
183 """Check that coefficients are as expected
185 Parameters
186 ----------
187 coeff : `numpy.ndarray`
188 Crosstalk coefficients.
189 coeffErr : `numpy.ndarray`
190 Crosstalk coefficient errors.
191 coeffNum : `numpy.ndarray`
192 Number of pixels to produce each coefficient.
193 """
194 for matrix in (coeff, coeffErr, coeffNum):
195 self.assertEqual(matrix.shape, (self.numAmps, self.numAmps))
196 self.assertFloatsAlmostEqual(coeff, np.array(self.crosstalk), atol=1.0e-6)
198 for ii in range(self.numAmps):
199 self.assertEqual(coeff[ii, ii], 0.0)
200 self.assertTrue(np.isnan(coeffErr[ii, ii]))
201 self.assertEqual(coeffNum[ii, ii], 1)
203 self.assertTrue(np.all(coeffErr[ii, jj] > 0 for ii, jj in
204 itertools.product(range(self.numAmps), range(self.numAmps)) if ii != jj))
205 self.assertTrue(np.all(coeffNum[ii, jj] > 0 for ii, jj in
206 itertools.product(range(self.numAmps), range(self.numAmps)) if ii != jj))
208 def checkSubtracted(self, exposure):
209 """Check that the subtracted image is as expected
211 Parameters
212 ----------
213 exposure : `lsst.afw.image.Exposure`
214 Crosstalk-subtracted exposure.
215 """
216 image = exposure.getMaskedImage().getImage()
217 mask = exposure.getMaskedImage().getMask()
218 self.assertFloatsAlmostEqual(image.getArray(), self.corrected.getArray(), atol=2.0e-2)
219 self.assertIn(self.crosstalkStr, mask.getMaskPlaneDict())
220 self.assertGreater((mask.getArray() & mask.getPlaneBitMask(self.crosstalkStr) > 0).sum(), 0)
222 def checkTaskAPI_NL(self, this_isr_task):
223 """Check the the crosstalk task under different ISR tasks.
224 (e.g., IsrTask and IsrTaskLSST)
226 Parameters
227 ----------
228 this_isr_task : `lsst.pipe.base.PipelineTask`
229 """
230 self.setUp_general(doSqrCrosstalk=True)
231 coeff = np.array(self.crosstalk).transpose()
232 coeffSqr = np.array(self.crosstalk_sqr).transpose()
233 config = this_isr_task.ConfigClass()
234 config.crosstalk.minPixelToMask = self.value - 1
235 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr
236 # Turn on the NL correction
237 config.crosstalk.doQuadraticCrosstalkCorrection = True
238 isr = this_isr_task(config=config)
239 calib = CrosstalkCalib().fromDetector(self.exposure.getDetector(),
240 coeffVector=coeff,
241 coeffSqrVector=coeffSqr)
242 isr.crosstalk.run(self.exposure, crosstalk=calib)
243 self.checkSubtracted(self.exposure)
245 def testDirectAPI(self):
246 """Test that individual function calls work"""
247 self.setUp_general()
248 calib = CrosstalkCalib()
249 calib.coeffs = np.array(self.crosstalk).transpose()
250 calib.subtractCrosstalk(self.exposure, crosstalkCoeffs=calib.coeffs,
251 minPixelToMask=self.value - 1,
252 crosstalkStr=self.crosstalkStr)
253 self.checkSubtracted(self.exposure)
255 outPath = tempfile.mktemp() if outputName is None else "{}-isrCrosstalk".format(outputName)
256 outPath += '.yaml'
257 calib.writeText(outPath)
259 def testTaskAPI(self):
260 """Test that the Tasks work
262 Checks both MeasureCrosstalkTask and the CrosstalkTask.
263 """
264 self.setUp_general()
265 coeff = np.array(self.crosstalk).transpose()
266 coeffSqr = np.array(self.crosstalk_sqr).transpose()
267 config = IsrTask.ConfigClass()
268 config.crosstalk.minPixelToMask = self.value - 1
269 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr
270 isr = IsrTask(config=config)
271 calib = CrosstalkCalib().fromDetector(self.exposure.getDetector(),
272 coeffVector=coeff,
273 coeffSqrVector=coeffSqr)
274 isr.crosstalk.run(self.exposure, crosstalk=calib)
275 self.checkSubtracted(self.exposure)
277 def testTaskAPI_NL(self):
278 """Test that the Tasks work
280 Checks both MeasureCrosstalkTask and the CrosstalkTask.
281 This test is for the quadratic (non-linear) corsstalk
282 correction.
283 """
284 for this_isr_task in [IsrTask, IsrTaskLSST]:
285 self.checkTaskAPI_NL(this_isr_task)
287 def test_nullCrosstalkTask(self):
288 """Test that the null crosstalk task does not create an error.
289 """
290 self.setUp_general()
291 exposure = self.exposure
292 task = NullCrosstalkTask()
293 result = task.run(exposure, crosstalkSources=None)
294 self.assertIsNone(result)
296 def test_interChip(self):
297 """Test that passing an external exposure as the crosstalk source
298 works.
299 """
300 self.setUp_general()
301 exposure = self.exposure
302 ctSources = [self.ctSource]
304 coeff = np.array(self.crosstalk).transpose()
305 calib = CrosstalkCalib().fromDetector(exposure.getDetector(), coeffVector=coeff)
306 # Now convert this into zero intra-chip, full inter-chip:
307 calib.interChip['detector 2'] = coeff
308 calib.coeffs = np.zeros_like(coeff)
310 # Process and check as above
311 config = IsrTask.ConfigClass()
312 config.crosstalk.minPixelToMask = self.value - 1
313 config.crosstalk.crosstalkMaskPlane = self.crosstalkStr
314 isr = IsrTask(config=config)
315 isr.crosstalk.run(exposure, crosstalk=calib, crosstalkSources=ctSources)
316 self.checkSubtracted(exposure)
318 def test_crosstalkIO(self):
319 """Test that crosstalk doesn't change on being converted to persistable
320 formats.
321 """
322 self.setUp_general()
323 # Add the interchip crosstalk as in the previous test.
324 exposure = self.exposure
326 coeff = np.array(self.crosstalk).transpose()
327 calib = CrosstalkCalib().fromDetector(exposure.getDetector(), coeffVector=coeff)
328 # Now convert this into zero intra-chip, full inter-chip:
329 calib.interChip['detector 2'] = coeff
331 outPath = tempfile.mktemp() + '.yaml'
332 calib.writeText(outPath)
333 newCrosstalk = CrosstalkCalib().readText(outPath)
334 self.assertEqual(calib, newCrosstalk)
336 outPath = tempfile.mktemp() + '.fits'
337 calib.writeFits(outPath)
338 newCrosstalk = CrosstalkCalib().readFits(outPath)
339 self.assertEqual(calib, newCrosstalk)
342class MemoryTester(lsst.utils.tests.MemoryTestCase):
343 pass
346def setup_module(module):
347 lsst.utils.tests.init()
350if __name__ == "__main__": 350 ↛ 351line 350 didn't jump to line 351, because the condition on line 350 was never true
351 import sys
352 setup_module(sys.modules[__name__])
353 unittest.main()