Coverage for tests/test_linearize.py: 14%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

158 statements  

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/>. 

21 

22import unittest 

23 

24import logging 

25import numpy as np 

26 

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 

35 

36 

37def referenceImage(image, detector, linearityType, inputData, table=None): 

38 """Generate a reference linearization. 

39 

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. 

52 

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. 

59 

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) 

70 

71 if linearityType == 'Squared': 

72 sqCoeff = inputData[ampIdx] 

73 array = imageView.getArray() 

74 

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 

98 

99 

100class LinearizeTestCase(lsst.utils.tests.TestCase): 

101 """Unit tests for linearizers. 

102 """ 

103 

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) 

111 

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]) 

119 

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") 

122 

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) 

126 

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("lsst.ip.isr.testLinearizer") 

131 

132 def tearDown(self): 

133 # destroy LSST objects so memory test passes. 

134 self.bbox = None 

135 self.detector = None 

136 

137 def compareResults(self, linearizedImage, linearizedOutOfRange, linearizedCount, linearizedAmps, 

138 referenceImage, referenceOutOfRange, referenceCount, referenceAmps): 

139 """Run assert tests on results. 

140 

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) 

164 

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) 

170 

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) 

181 

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) 

186 

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 

194 

195 self.compareResults(measImage, result.numOutOfRange, result.numLinearized, result.numAmps, 

196 refImage, refNumOutOfRange, self.numAmps - zeroLinearity, self.numAmps) 

197 

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) 

202 

203 self.compareResults(measImage, storedResult.numOutOfRange, storedResult.numLinearized, 

204 storedResult.numAmps, 

205 refImage, refNumOutOfRange, self.numAmps - zeroLinearity, self.numAmps) 

206 

207 # "Save to yaml" and test again 

208 storedDict = storedLinearizer.toDict() 

209 storedLinearizer = Linearizer().fromDict(storedDict) 

210 

211 measImage = inImage.Factory(inImage, True) 

212 storedLinearizer = self.makeLinearizer(linearityType) 

213 storedResult = storedLinearizer.applyLinearity(measImage, log=self.log) 

214 

215 self.compareResults(measImage, storedResult.numOutOfRange, storedResult.numLinearized, 

216 storedResult.numAmps, 

217 refImage, refNumOutOfRange, self.numAmps - zeroLinearity, self.numAmps) 

218 

219 # "Save to fits" and test again 

220 storedTable = storedLinearizer.toTable() 

221 storedLinearizer = Linearizer().fromTable(storedTable) 

222 

223 measImage = inImage.Factory(inImage, True) 

224 storedLinearizer = self.makeLinearizer(linearityType) 

225 storedResult = storedLinearizer.applyLinearity(measImage, log=self.log) 

226 

227 self.compareResults(measImage, storedResult.numOutOfRange, storedResult.numLinearized, 

228 storedResult.numAmps, 

229 refImage, refNumOutOfRange, self.numAmps - zeroLinearity, self.numAmps) 

230 

231 def makeDetector(self, linearityType, bbox=None): 

232 """Generate a fake detector for the test. 

233 

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. 

240 

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 

248 

249 detName = "det_a" 

250 detId = 1 

251 detSerial = "123" 

252 orientation = cameraGeom.Orientation() 

253 pixelSize = lsst.geom.Extent2D(1, 1) 

254 

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) 

261 

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) 

280 

281 return detBuilder 

282 

283 def makeLinearizer(self, linearityType, bbox=None): 

284 """Construct a linearizer with the test coefficients. 

285 

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. 

294 

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 

305 

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) 

311 

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) 

321 

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 

327 

328 return linearizer 

329 

330 

331class MemoryTester(lsst.utils.tests.MemoryTestCase): 

332 pass 

333 

334 

335def setup_module(module): 

336 lsst.utils.tests.init() 

337 

338 

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()