Coverage for tests/test_linearize.py: 12%
160 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-01 03:20 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-01 03:20 -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([0.0, 1000, 2000, 3000, 4000, 5000,
129 0.0, 1.0, 4.0, 9.0, 16.0, 25.0])
130 self.log = logging.getLogger("lsst.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
273 # int/float list.
274 ampInfo.setLinearityCoeffs(np.array([self.rowInds[i, j], self.colIndOffsets[i, j],
275 0, 0], dtype=float))
276 elif linearityType == 'Polynomial':
277 ampInfo.setLinearityCoeffs(self.polyCoeffs[i, j])
278 elif linearityType == 'Spline':
279 ampInfo.setLinearityCoeffs(self.splineCoeffs)
280 detBuilder.append(ampInfo)
282 return detBuilder
284 def makeLinearizer(self, linearityType, bbox=None):
285 """Construct a linearizer with the test coefficients.
287 Parameters
288 ----------
289 linearityType : `str`
290 Type of linearity to use. The coefficients are set by the
291 setUp method.
292 bbox : `lsst.geom.Box2I`
293 Bounding box for the full detector. Used to assign
294 amp-based bounding boxes.
296 Returns
297 -------
298 linearizer : `lsst.ip.isr.Linearizer`
299 A fully constructed, persistable linearizer.
300 """
301 bbox = bbox if bbox is not None else self.bbox
302 numAmps = self.ampArrangement
303 boxArr = BoxGrid(box=bbox, numColRow=numAmps)
304 linearizer = Linearizer()
305 linearizer.hasLinearity = True
307 for i in range(numAmps[0]):
308 for j in range(numAmps[1]):
309 ampName = f"amp {i+1}_{j+1}"
310 ampBox = boxArr[i, j]
311 linearizer.ampNames.append(ampName)
313 if linearityType == 'Squared':
314 linearizer.linearityCoeffs[ampName] = np.array([self.sqCoeffs[i, j]])
315 elif linearityType == 'LookupTable':
316 linearizer.linearityCoeffs[ampName] = np.array(self.lookupIndices[i, j])
317 linearizer.tableData = self.table
318 elif linearityType == 'Polynomial':
319 linearizer.linearityCoeffs[ampName] = np.array(self.polyCoeffs[i, j])
320 elif linearityType == 'Spline':
321 linearizer.linearityCoeffs[ampName] = np.array(self.splineCoeffs)
323 linearizer.linearityType[ampName] = linearityType
324 linearizer.linearityBBox[ampName] = ampBox
325 linearizer.fitParams[ampName] = np.array([])
326 linearizer.fitParamsErr[ampName] = np.array([])
327 linearizer.fitChiSq[ampName] = np.nan
328 linearizer.fitResiduals[ampName] = np.array([])
329 linearizer.linearFit[ampName] = np.array([])
331 return linearizer
334class MemoryTester(lsst.utils.tests.MemoryTestCase):
335 pass
338def setup_module(module):
339 lsst.utils.tests.init()
342if __name__ == "__main__": 342 ↛ 343line 342 didn't jump to line 343, because the condition on line 342 was never true
343 lsst.utils.tests.init()
344 unittest.main()