Coverage for tests/test_linearize.py: 12%

160 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-07 20:32 +0000

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 

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) 

281 

282 return detBuilder 

283 

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

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

286 

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. 

295 

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 

306 

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) 

312 

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) 

322 

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

330 

331 return linearizer 

332 

333 

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

335 pass 

336 

337 

338def setup_module(module): 

339 lsst.utils.tests.init() 

340 

341 

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