Coverage for tests / test_linearizeLookupTable.py: 17%
145 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 09:10 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 09:10 +0000
1#
2# LSST Data Management System
3# Copyright 2017 LSST Corporation.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <http://www.lsstcorp.org/LegalNotices/>.
21#
22import unittest
23import pickle
24import logging
25import numpy as np
27import lsst.utils.tests
28import lsst.utils
29import lsst.afw.image as afwImage
30import lsst.afw.cameraGeom as cameraGeom
31from lsst.afw.geom.testUtils import BoxGrid
32from lsst.afw.image.testUtils import makeRampImage
33from lsst.ip.isr import applyLookupTable, Linearizer
36def refLinearize(image, detector, table):
37 """!Basic implementation of lookup table based non-linearity correction
39 @param[in,out] image image to correct in place (an lsst.afw.image.Image
40 of some type)
41 @param[in] detector detector info (an lsst.afw.cameraGeom.Detector)
42 @param[in] table lookup table: a 2D array of values of the same type as
43 image;
44 - one row for each row index (value of coef[0] in the amp info
45 catalog)
46 - one column for each image value
48 @return the number of pixels whose values were out of range of the lookup
49 table
50 """
51 ampInfoCat = detector.getAmplifiers()
52 numOutOfRange = 0
53 for ampInfo in ampInfoCat:
54 bbox = ampInfo.getBBox()
55 rowInd, colIndOffset = ampInfo.getLinearityCoeffs()[0:2]
56 rowInd = int(rowInd)
57 tableRow = table[rowInd, :]
58 imView = image.Factory(image, bbox)
59 numOutOfRange += applyLookupTable(imView, tableRow, colIndOffset)
60 return numOutOfRange
63class LinearizeLookupTableTestCase(lsst.utils.tests.TestCase):
64 """!Unit tests for LinearizeLookupTable"""
66 def setUp(self):
67 # the following values are all arbitrary, but sane and varied
68 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(-31, 22), lsst.geom.Extent2I(100, 85))
69 self.numAmps = (2, 3)
70 self.colIndOffsets = np.array([[0, -50, 2.5], [37, 1, -3]], dtype=float)
71 self.rowInds = np.array([[0, 1, 4], [3, 5, 2]])
72 numCols = self.numAmps[0]*self.numAmps[1]
73 self.assertLess(np.max(self.rowInds), numCols, "error in test conditions; invalid row index")
74 self.detector = self.makeDetector()
75 self.rng = np.random.Generator(np.random.MT19937(1))
77 def tearDown(self):
78 # destroy LSST objects so memory test passes
79 self.bbox = None
80 self.detector = None
82 def testBasics(self):
83 """!Test basic functionality of LinearizeLookupTable
84 """
85 for imageClass in (afwImage.ImageF, afwImage.ImageD):
86 inImage = makeRampImage(bbox=self.bbox, start=-5, stop=250, imageClass=imageClass)
87 table = self.makeTable(inImage)
89 log = logging.getLogger("lsst.ip.isr.LinearizeLookupTable")
91 measImage = inImage.Factory(inImage, True)
92 llt = Linearizer(table=table, detector=self.detector)
93 linRes = llt.applyLinearity(measImage, detector=self.detector, log=log)
95 refImage = inImage.Factory(inImage, True)
96 refNumOutOfRange = refLinearize(image=refImage, detector=self.detector, table=table)
98 self.assertEqual(linRes.numAmps, len(self.detector.getAmplifiers()))
99 self.assertEqual(linRes.numAmps, linRes.numLinearized)
100 self.assertEqual(linRes.numOutOfRange, refNumOutOfRange)
101 self.assertImagesAlmostEqual(refImage, measImage)
103 # make sure logging is accepted
104 log = logging.getLogger("lsst.ip.isr.LinearizeLookupTable")
105 linRes = llt.applyLinearity(image=measImage, detector=self.detector, log=log)
107 def testErrorHandling(self):
108 """!Test error handling in LinearizeLookupTable
109 """
110 image = makeRampImage(bbox=self.bbox, start=-5, stop=250)
111 table = self.makeTable(image)
112 llt = Linearizer(table=table, detector=self.detector)
114 # bad name
115 detBadName = self.makeDetector(detName="bad_detector_name")
116 with self.assertRaises(RuntimeError):
117 llt.applyLinearity(image, detBadName)
119 # TODO: DM-38778: bad serial value disabled.
120 # detBadSerial = self.makeDetector(detSerial="bad_detector_serial")
121 # with self.assertRaises(RuntimeError):
122 # llt.applyLinearity(image, detBadSerial)
124 # bad number of amplifiers
125 badNumAmps = (self.numAmps[0]-1, self.numAmps[1])
126 detBadNumMaps = self.makeDetector(numAmps=badNumAmps)
127 with self.assertRaises(RuntimeError):
128 llt.applyLinearity(image, detBadNumMaps)
130 # bad linearity type
131 detBadLinType = self.makeDetector(linearityType="bad_linearity_type")
132 with self.assertRaises(RuntimeError):
133 llt.applyLinearity(image, detBadLinType)
135 # wrong dimension
136 badTable = table[..., np.newaxis]
137 with self.assertRaises(RuntimeError):
138 Linearizer(table=badTable, detector=self.detector)
140 # wrong size
141 badTable = np.transpose(table)
142 with self.assertRaises(RuntimeError):
143 Linearizer(table=badTable, detector=self.detector)
145 def testKnown(self):
146 """!Test a few known values
147 """
148 numAmps = (2, 2)
149 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(4, 4))
150 # make a 4x4 image with 4 identical 2x2 subregions that flatten
151 # to -1, 0, 1, 2
152 im = afwImage.ImageF(bbox)
153 imArr = im.getArray()
154 imArr[:, :] = np.array(((-1, 0, -1, 0),
155 (1, 2, 1, 2),
156 (-1, 0, -1, 0),
157 (1, 2, 1, 2)), dtype=imArr.dtype)
159 def castAndReshape(arr):
160 arr = np.array(arr, dtype=float)
161 arr.shape = numAmps
162 return arr
164 rowInds = castAndReshape((3, 2, 1, 0)) # avoid the trivial mapping to exercise more of the code
165 colIndOffsets = castAndReshape((0, 0, 1, 1))
166 detector = self.makeDetector(bbox=bbox, numAmps=numAmps, rowInds=rowInds, colIndOffsets=colIndOffsets)
167 ampInfoCat = detector.getAmplifiers()
169 # note: table rows are reversed relative to amplifier order because
170 # rowInds is a descending ramp
171 table = np.array(((7, 6, 5, 4), (1, 1, 1, 1), (5, 4, 3, 2), (0, 0, 0, 0)), dtype=imArr.dtype)
173 llt = Linearizer(table=table, detector=detector)
175 lltRes = llt.applyLinearity(image=im, detector=detector)
176 self.assertEqual(lltRes.numOutOfRange, 2)
178 # amp 0 is a constant correction of 0; one image value is out of range,
179 # but it doesn't matter
180 imArr0 = im.Factory(im, ampInfoCat[0].getBBox()).getArray()
181 self.assertFloatsAlmostEqual(imArr0.flatten(), (-1, 0, 1, 2))
183 # amp 1 is a correction of (5, 4, 3, 2), but the first image value is
184 # under range
185 imArr1 = im.Factory(im, ampInfoCat[1].getBBox()).getArray()
186 self.assertFloatsAlmostEqual(imArr1.flatten(), (4, 5, 5, 5))
188 # amp 2 is a constant correction of +1; all image values are in range,
189 # but it doesn't matter
190 imArr2 = im.Factory(im, ampInfoCat[2].getBBox()).getArray()
191 self.assertFloatsAlmostEqual(imArr2.flatten(), (0, 1, 2, 3))
193 # amp 3 is a correction of (7, 6, 5, 4); all image values in range
194 imArr1 = im.Factory(im, ampInfoCat[3].getBBox()).getArray()
195 self.assertFloatsAlmostEqual(imArr1.flatten(), (6, 6, 6, 6))
197 def testPickle(self):
198 """!Test that a LinearizeLookupTable can be pickled and unpickled
199 """
200 inImage = makeRampImage(bbox=self.bbox, start=-5, stop=2500)
201 table = self.makeTable(inImage)
202 llt = Linearizer(table=table, detector=self.detector)
204 refImage = inImage.Factory(inImage, True)
205 refNumOutOfRange = llt.applyLinearity(refImage, self.detector)
207 pickledStr = pickle.dumps(llt)
208 restoredLlt = pickle.loads(pickledStr)
210 measImage = inImage.Factory(inImage, True)
211 measNumOutOfRange = restoredLlt.applyLinearity(measImage, self.detector)
213 self.assertEqual(refNumOutOfRange, measNumOutOfRange)
214 self.assertImagesAlmostEqual(refImage, measImage)
216 def makeDetector(self, bbox=None, numAmps=None, rowInds=None, colIndOffsets=None,
217 detName="det_a", detSerial="123", linearityType="LookupTable"):
218 """!Make a detector
220 @param[in] bbox bounding box for image
221 @param[n] numAmps x,y number of amplifiers (pair of int)
222 @param[in] rowInds index of lookup table for each amplifier (array of
223 shape numAmps)
224 @param[in] colIndOffsets column index offset for each amplifier
225 (array of shape numAmps)
226 @param[in] detName detector name (a str)
227 @param[in] detSerial detector serial numbe (a str)
228 @param[in] linearityType name of linearity type (a str)
230 @return a detector (an lsst.afw.cameraGeom.Detector)
231 """
232 bbox = bbox if bbox is not None else self.bbox
233 numAmps = numAmps if numAmps is not None else self.numAmps
234 rowInds = rowInds if rowInds is not None else self.rowInds
235 colIndOffsets = colIndOffsets if colIndOffsets is not None else self.colIndOffsets
237 detId = 1
238 orientation = cameraGeom.Orientation()
239 pixelSize = lsst.geom.Extent2D(1, 1)
241 camBuilder = cameraGeom.Camera.Builder("fakeCam")
242 detBuilder = camBuilder.add(detName, detId)
243 detBuilder.setSerial(detSerial)
244 detBuilder.setBBox(bbox)
245 detBuilder.setOrientation(orientation)
246 detBuilder.setPixelSize(pixelSize)
248 boxArr = BoxGrid(box=bbox, numColRow=numAmps)
249 for i in range(numAmps[0]):
250 for j in range(numAmps[1]):
251 ampInfo = cameraGeom.Amplifier.Builder()
252 ampInfo.setName("amp %d_%d" % (i + 1, j + 1))
253 ampInfo.setBBox(boxArr[i, j])
254 ampInfo.setLinearityType(linearityType)
255 # setLinearityCoeffs is picky about getting a mixed int/float
256 # list.
257 ampInfo.setLinearityCoeffs(np.array([rowInds[i, j], colIndOffsets[i, j], 0, 0], dtype=float))
258 detBuilder.append(ampInfo)
260 return detBuilder
262 def makeTable(self, image, numCols=None, numRows=2500, sigma=55):
263 """!Make a 2D lookup table
265 @param[in] image image whose type is used for the table
266 @param[in] numCols number of columns for table; defaults to
267 self.numCols
268 @param[in] numRows number of rows for the table
269 @param[in] sigma standard deviation of normal distribution
270 """
271 numCols = numCols or self.numAmps[0]*self.numAmps[1]
272 dtype = image.getArray().dtype
273 table = self.rng.normal(scale=sigma, size=(numCols, numRows))
274 return np.array(table, dtype=dtype)
277class MemoryTester(lsst.utils.tests.MemoryTestCase):
278 pass
281def setup_module(module):
282 lsst.utils.tests.init()
285if __name__ == "__main__": 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true
286 lsst.utils.tests.init()
287 unittest.main()