Coverage for tests/test_linearize.py: 12%
158 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-12 03:09 -0700
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-12 03:09 -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
24import logging
25import numpy as np
27import lsst.utils.tests
28import lsst.utils
29import lsst.afw.image as afwImage
30import lsst.afw.math as afwMath
31import lsst.afw.cameraGeom as cameraGeom
32from lsst.afw.geom.testUtils import BoxGrid
33from lsst.afw.image.testUtils import makeRampImage
34from lsst.ip.isr import applyLookupTable, Linearizer
37def referenceImage(image, detector, linearityType, inputData, table=None):
38 """Generate a reference linearization.
40 Parameters
41 ----------
42 image: `lsst.afw.image.Image`
43 Image to linearize.
44 detector: `lsst.afw.cameraGeom.Detector`
45 Detector this image is from.
46 linearityType: `str`
47 Type of linearity to apply.
48 inputData: `numpy.array`
49 An array of values for the linearity correction.
50 table: `numpy.array`, optional
51 An optional lookup table to use.
53 Returns
54 -------
55 outImage: `lsst.afw.image.Image`
56 The output linearized image.
57 numOutOfRange: `int`
58 The number of values that could not be linearized.
60 Raises
61 ------
62 RuntimeError :
63 Raised if an invalid linearityType is supplied.
64 """
65 numOutOfRange = 0
66 for ampIdx, amp in enumerate(detector.getAmplifiers()):
67 ampIdx = (ampIdx // 3, ampIdx % 3)
68 bbox = amp.getBBox()
69 imageView = image.Factory(image, bbox)
71 if linearityType == 'Squared':
72 sqCoeff = inputData[ampIdx]
73 array = imageView.getArray()
75 array[:] = array + sqCoeff*array**2
76 elif linearityType == 'LookupTable':
77 rowInd, colIndOffset = inputData[ampIdx]
78 rowInd = int(rowInd)
79 tableRow = table[rowInd, :]
80 numOutOfRange += applyLookupTable(imageView, tableRow, colIndOffset)
81 elif linearityType == 'Polynomial':
82 coeffs = inputData[ampIdx]
83 array = imageView.getArray()
84 summation = np.zeros_like(array)
85 for index, coeff in enumerate(coeffs):
86 summation += coeff*np.power(array, (index + 2))
87 array += summation
88 elif linearityType == 'Spline':
89 centers, values = np.split(inputData, 2) # This uses the full data
90 interp = afwMath.makeInterpolate(centers.tolist(), values.tolist(),
91 afwMath.stringToInterpStyle('AKIMA_SPLINE'))
92 array = imageView.getArray()
93 delta = interp.interpolate(array.flatten())
94 array -= np.array(delta).reshape(array.shape)
95 else:
96 raise RuntimeError(f"Unknown linearity: {linearityType}")
97 return image, numOutOfRange
100class LinearizeTestCase(lsst.utils.tests.TestCase):
101 """Unit tests for linearizers.
102 """
104 def setUp(self):
105 # This uses the same arbitrary values used in previous tests.
106 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(-31, 22), lsst.geom.Extent2I(100, 85))
107 self.ampArrangement = (2, 3)
108 self.numAmps = self.ampArrangement[0]*self.ampArrangement[1]
109 # Squared Parameters
110 self.sqCoeffs = np.array([[0, 5e-6, 2.5e-5], [1e-5, 1.1e-6, 2.1e-6]], dtype=float)
112 # Lookup Table Parameters
113 self.colIndOffsets = np.array([[0, -50, 2.5], [37, 1, -3]], dtype=float)
114 self.rowInds = np.array([[0, 1, 4], [3, 5, 2]])
115 # This creates a 2x3 array (matching the amplifiers) that contains a
116 # 2x1 array containing [colIndOffset_i, rowInd_i].
117 self.lookupIndices = np.transpose(np.stack((self.rowInds, self.colIndOffsets), axis=0),
118 axes=[1, 2, 0])
120 self.table = np.random.normal(scale=55, size=(self.numAmps, 2500))
121 self.assertLess(np.max(self.rowInds), self.numAmps, "error in test conditions; invalid row index")
123 # Polynomial Parameters: small perturbation on Squared
124 self.polyCoeffs = np.array([[[0, 1e-7], [5e-6, 1e-7], [2.5e-5, 1e-7]],
125 [[1e-5, 1e-7], [1.1e-6, 1e-7], [2.1e-6, 1e-7]]], dtype=float)
127 # Spline coefficients: should match a 1e-6 Squared solution
128 self.splineCoeffs = np.array([-100, 0.0, 1000, 2000, 3000, 4000, 5000,
129 0.0, 0.0, 1.0, 4.0, 9.0, 16.0, 25.0])
130 self.log = logging.getLogger("ip.isr.testLinearizer")
132 def tearDown(self):
133 # destroy LSST objects so memory test passes.
134 self.bbox = None
135 self.detector = None
137 def compareResults(self, linearizedImage, linearizedOutOfRange, linearizedCount, linearizedAmps,
138 referenceImage, referenceOutOfRange, referenceCount, referenceAmps):
139 """Run assert tests on results.
141 Parameters
142 ----------
143 linearizedImage : `lsst.afw.image.Image`
144 Corrected image.
145 linearizedOutOfRange : `int`
146 Number of measured out-of-range pixels.
147 linearizedCount : `int`
148 Number of amplifiers that should be linearized.
149 linearizedAmps : `int`
150 Total number of amplifiers checked.
151 referenceImage : `lsst.afw.image.Image`
152 Truth image to compare against.
153 referenceOutOfRange : `int`
154 Number of expected out-of-range-pixels.
155 referenceCount : `int`
156 Number of amplifiers that are expected to be linearized.
157 referenceAmps : `int`
158 Expected number of amplifiers checked.
159 """
160 self.assertImagesAlmostEqual(linearizedImage, referenceImage)
161 self.assertEqual(linearizedOutOfRange, referenceOutOfRange)
162 self.assertEqual(linearizedCount, referenceCount)
163 self.assertEqual(linearizedAmps, referenceAmps)
165 def testBasics(self):
166 """Test basic linearization functionality.
167 """
168 for imageClass in (afwImage.ImageF, afwImage.ImageD):
169 inImage = makeRampImage(bbox=self.bbox, start=-5, stop=2500, imageClass=imageClass)
171 for linearityType in ('Squared', 'LookupTable', 'Polynomial', 'Spline'):
172 detector = self.makeDetector(linearityType)
173 table = None
174 inputData = {'Squared': self.sqCoeffs,
175 'LookupTable': self.lookupIndices,
176 'Polynomial': self.polyCoeffs,
177 'Spline': self.splineCoeffs}[linearityType]
178 if linearityType == 'LookupTable':
179 table = np.array(self.table, dtype=inImage.getArray().dtype)
180 linearizer = Linearizer(detector=detector, table=table)
182 measImage = inImage.Factory(inImage, True)
183 result = linearizer.applyLinearity(measImage, detector=detector, log=self.log)
184 refImage, refNumOutOfRange = referenceImage(inImage.Factory(inImage, True),
185 detector, linearityType, inputData, table)
187 # This is necessary for the same tests to be used on
188 # all types. The first amplifier has 0.0 for the
189 # coefficient, which should be tested (it has a log
190 # message), but we are not linearizing an amplifier
191 # with no correction, so it fails the test that
192 # numLinearized == numAmps.
193 zeroLinearity = 1 if linearityType == 'Squared' else 0
195 self.compareResults(measImage, result.numOutOfRange, result.numLinearized, result.numAmps,
196 refImage, refNumOutOfRange, self.numAmps - zeroLinearity, self.numAmps)
198 # Test a stand alone linearizer. This ignores validate checks.
199 measImage = inImage.Factory(inImage, True)
200 storedLinearizer = self.makeLinearizer(linearityType)
201 storedResult = storedLinearizer.applyLinearity(measImage, log=self.log)
203 self.compareResults(measImage, storedResult.numOutOfRange, storedResult.numLinearized,
204 storedResult.numAmps,
205 refImage, refNumOutOfRange, self.numAmps - zeroLinearity, self.numAmps)
207 # "Save to yaml" and test again
208 storedDict = storedLinearizer.toDict()
209 storedLinearizer = Linearizer().fromDict(storedDict)
211 measImage = inImage.Factory(inImage, True)
212 storedLinearizer = self.makeLinearizer(linearityType)
213 storedResult = storedLinearizer.applyLinearity(measImage, log=self.log)
215 self.compareResults(measImage, storedResult.numOutOfRange, storedResult.numLinearized,
216 storedResult.numAmps,
217 refImage, refNumOutOfRange, self.numAmps - zeroLinearity, self.numAmps)
219 # "Save to fits" and test again
220 storedTable = storedLinearizer.toTable()
221 storedLinearizer = Linearizer().fromTable(storedTable)
223 measImage = inImage.Factory(inImage, True)
224 storedLinearizer = self.makeLinearizer(linearityType)
225 storedResult = storedLinearizer.applyLinearity(measImage, log=self.log)
227 self.compareResults(measImage, storedResult.numOutOfRange, storedResult.numLinearized,
228 storedResult.numAmps,
229 refImage, refNumOutOfRange, self.numAmps - zeroLinearity, self.numAmps)
231 def makeDetector(self, linearityType, bbox=None):
232 """Generate a fake detector for the test.
234 Parameters
235 ----------
236 linearityType : `str`
237 Which linearity to assign to the detector's cameraGeom.
238 bbox : `lsst.geom.Box2I`, optional
239 Bounding box to use for the detector.
241 Returns
242 -------
243 detBuilder : `lsst.afw.cameraGeom.Detector`
244 The fake detector.
245 """
246 bbox = bbox if bbox is not None else self.bbox
247 numAmps = self.ampArrangement
249 detName = "det_a"
250 detId = 1
251 detSerial = "123"
252 orientation = cameraGeom.Orientation()
253 pixelSize = lsst.geom.Extent2D(1, 1)
255 camBuilder = cameraGeom.Camera.Builder("fakeCam")
256 detBuilder = camBuilder.add(detName, detId)
257 detBuilder.setSerial(detSerial)
258 detBuilder.setBBox(bbox)
259 detBuilder.setOrientation(orientation)
260 detBuilder.setPixelSize(pixelSize)
262 boxArr = BoxGrid(box=bbox, numColRow=numAmps)
263 for i in range(numAmps[0]):
264 for j in range(numAmps[1]):
265 ampInfo = cameraGeom.Amplifier.Builder()
266 ampInfo.setName("amp %d_%d" % (i + 1, j + 1))
267 ampInfo.setBBox(boxArr[i, j])
268 ampInfo.setLinearityType(linearityType)
269 if linearityType == 'Squared':
270 ampInfo.setLinearityCoeffs([self.sqCoeffs[i, j]])
271 elif linearityType == 'LookupTable':
272 # setLinearityCoeffs is picky about getting a mixed int/float list.
273 ampInfo.setLinearityCoeffs(np.array([self.rowInds[i, j], self.colIndOffsets[i, j],
274 0, 0], dtype=float))
275 elif linearityType == 'Polynomial':
276 ampInfo.setLinearityCoeffs(self.polyCoeffs[i, j])
277 elif linearityType == 'Spline':
278 ampInfo.setLinearityCoeffs(self.splineCoeffs)
279 detBuilder.append(ampInfo)
281 return detBuilder
283 def makeLinearizer(self, linearityType, bbox=None):
284 """Construct a linearizer with the test coefficients.
286 Parameters
287 ----------
288 linearityType : `str`
289 Type of linearity to use. The coefficients are set by the
290 setUp method.
291 bbox : `lsst.geom.Box2I`
292 Bounding box for the full detector. Used to assign
293 amp-based bounding boxes.
295 Returns
296 -------
297 linearizer : `lsst.ip.isr.Linearizer`
298 A fully constructed, persistable linearizer.
299 """
300 bbox = bbox if bbox is not None else self.bbox
301 numAmps = self.ampArrangement
302 boxArr = BoxGrid(box=bbox, numColRow=numAmps)
303 linearizer = Linearizer()
304 linearizer.hasLinearity = True
306 for i in range(numAmps[0]):
307 for j in range(numAmps[1]):
308 ampName = f"amp {i+1}_{j+1}"
309 ampBox = boxArr[i, j]
310 linearizer.ampNames.append(ampName)
312 if linearityType == 'Squared':
313 linearizer.linearityCoeffs[ampName] = np.array([self.sqCoeffs[i, j]])
314 elif linearityType == 'LookupTable':
315 linearizer.linearityCoeffs[ampName] = np.array(self.lookupIndices[i, j])
316 linearizer.tableData = self.table
317 elif linearityType == 'Polynomial':
318 linearizer.linearityCoeffs[ampName] = np.array(self.polyCoeffs[i, j])
319 elif linearityType == 'Spline':
320 linearizer.linearityCoeffs[ampName] = np.array(self.splineCoeffs)
322 linearizer.linearityType[ampName] = linearityType
323 linearizer.linearityBBox[ampName] = ampBox
324 linearizer.fitParams[ampName] = np.array([])
325 linearizer.fitParamsErr[ampName] = np.array([])
326 linearizer.fitChiSq[ampName] = np.nan
328 return linearizer
331class MemoryTester(lsst.utils.tests.MemoryTestCase):
332 pass
335def setup_module(module):
336 lsst.utils.tests.init()
339if __name__ == "__main__": 339 ↛ 340line 339 didn't jump to line 340, because the condition on line 339 was never true
340 lsst.utils.tests.init()
341 unittest.main()