Coverage for tests/test_linearizeLookupTable.py: 17%

147 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-02-05 18:17 -0800

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 

26 

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 

34 

35 

36def refLinearize(image, detector, table): 

37 """!Basic implementation of lookup table based non-linearity correction 

38 

39 @param[in,out] image image to correct in place (an lsst.afw.image.Image of some type) 

40 @param[in] detector detector info (an lsst.afw.cameraGeom.Detector) 

41 @param[in] table lookup table: a 2D array of values of the same type as image; 

42 - one row for each row index (value of coef[0] in the amp info catalog) 

43 - one column for each image value 

44 

45 @return the number of pixels whose values were out of range of the lookup table 

46 """ 

47 ampInfoCat = detector.getAmplifiers() 

48 numOutOfRange = 0 

49 for ampInfo in ampInfoCat: 

50 bbox = ampInfo.getBBox() 

51 rowInd, colIndOffset = ampInfo.getLinearityCoeffs()[0:2] 

52 rowInd = int(rowInd) 

53 tableRow = table[rowInd, :] 

54 imView = image.Factory(image, bbox) 

55 numOutOfRange += applyLookupTable(imView, tableRow, colIndOffset) 

56 return numOutOfRange 

57 

58 

59class LinearizeLookupTableTestCase(lsst.utils.tests.TestCase): 

60 """!Unit tests for LinearizeLookupTable""" 

61 

62 def setUp(self): 

63 # the following values are all arbitrary, but sane and varied 

64 self.bbox = lsst.geom.Box2I(lsst.geom.Point2I(-31, 22), lsst.geom.Extent2I(100, 85)) 

65 self.numAmps = (2, 3) 

66 self.colIndOffsets = np.array([[0, -50, 2.5], [37, 1, -3]], dtype=float) 

67 self.rowInds = np.array([[0, 1, 4], [3, 5, 2]]) 

68 numCols = self.numAmps[0]*self.numAmps[1] 

69 self.assertLess(np.max(self.rowInds), numCols, "error in test conditions; invalid row index") 

70 self.detector = self.makeDetector() 

71 

72 def tearDown(self): 

73 # destroy LSST objects so memory test passes 

74 self.bbox = None 

75 self.detector = None 

76 

77 def testBasics(self): 

78 """!Test basic functionality of LinearizeLookupTable 

79 """ 

80 for imageClass in (afwImage.ImageF, afwImage.ImageD): 

81 inImage = makeRampImage(bbox=self.bbox, start=-5, stop=250, imageClass=imageClass) 

82 table = self.makeTable(inImage) 

83 

84 log = logging.getLogger("ip.isr.LinearizeLookupTable") 

85 

86 measImage = inImage.Factory(inImage, True) 

87 llt = Linearizer(table=table, detector=self.detector) 

88 linRes = llt.applyLinearity(measImage, detector=self.detector, log=log) 

89 

90 refImage = inImage.Factory(inImage, True) 

91 refNumOutOfRange = refLinearize(image=refImage, detector=self.detector, table=table) 

92 

93 self.assertEqual(linRes.numAmps, len(self.detector.getAmplifiers())) 

94 self.assertEqual(linRes.numAmps, linRes.numLinearized) 

95 self.assertEqual(linRes.numOutOfRange, refNumOutOfRange) 

96 self.assertImagesAlmostEqual(refImage, measImage) 

97 

98 # make sure logging is accepted 

99 log = logging.getLogger("ip.isr.LinearizeLookupTable") 

100 linRes = llt.applyLinearity(image=measImage, detector=self.detector, log=log) 

101 

102 def testErrorHandling(self): 

103 """!Test error handling in LinearizeLookupTable 

104 """ 

105 image = makeRampImage(bbox=self.bbox, start=-5, stop=250) 

106 table = self.makeTable(image) 

107 llt = Linearizer(table=table, detector=self.detector) 

108 

109 # bad name 

110 detBadName = self.makeDetector(detName="bad_detector_name") 

111 with self.assertRaises(RuntimeError): 

112 llt.applyLinearity(image, detBadName) 

113 

114 # bad serial 

115 detBadSerial = self.makeDetector(detSerial="bad_detector_serial") 

116 with self.assertRaises(RuntimeError): 

117 llt.applyLinearity(image, detBadSerial) 

118 

119 # bad number of amplifiers 

120 badNumAmps = (self.numAmps[0]-1, self.numAmps[1]) 

121 detBadNumMaps = self.makeDetector(numAmps=badNumAmps) 

122 with self.assertRaises(RuntimeError): 

123 llt.applyLinearity(image, detBadNumMaps) 

124 

125 # bad linearity type 

126 detBadLinType = self.makeDetector(linearityType="bad_linearity_type") 

127 with self.assertRaises(RuntimeError): 

128 llt.applyLinearity(image, detBadLinType) 

129 

130 # wrong dimension 

131 badTable = table[..., np.newaxis] 

132 with self.assertRaises(RuntimeError): 

133 Linearizer(table=badTable, detector=self.detector) 

134 

135 # wrong size 

136 badTable = np.transpose(table) 

137 with self.assertRaises(RuntimeError): 

138 Linearizer(table=badTable, detector=self.detector) 

139 

140 def testKnown(self): 

141 """!Test a few known values 

142 """ 

143 numAmps = (2, 2) 

144 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Extent2I(4, 4)) 

145 # make a 4x4 image with 4 identical 2x2 subregions that flatten to -1, 0, 1, 2 

146 im = afwImage.ImageF(bbox) 

147 imArr = im.getArray() 

148 imArr[:, :] = np.array(((-1, 0, -1, 0), 

149 (1, 2, 1, 2), 

150 (-1, 0, -1, 0), 

151 (1, 2, 1, 2)), dtype=imArr.dtype) 

152 

153 def castAndReshape(arr): 

154 arr = np.array(arr, dtype=float) 

155 arr.shape = numAmps 

156 return arr 

157 

158 rowInds = castAndReshape((3, 2, 1, 0)) # avoid the trivial mapping to exercise more of the code 

159 colIndOffsets = castAndReshape((0, 0, 1, 1)) 

160 detector = self.makeDetector(bbox=bbox, numAmps=numAmps, rowInds=rowInds, colIndOffsets=colIndOffsets) 

161 ampInfoCat = detector.getAmplifiers() 

162 

163 # note: table rows are reversed relative to amplifier order because rowInds is a descending ramp 

164 table = np.array(((7, 6, 5, 4), (1, 1, 1, 1), (5, 4, 3, 2), (0, 0, 0, 0)), dtype=imArr.dtype) 

165 

166 llt = Linearizer(table=table, detector=detector) 

167 

168 lltRes = llt.applyLinearity(image=im, detector=detector) 

169 self.assertEqual(lltRes.numOutOfRange, 2) 

170 

171 # amp 0 is a constant correction of 0; one image value is out of range, but it doesn't matter 

172 imArr0 = im.Factory(im, ampInfoCat[0].getBBox()).getArray() 

173 self.assertFloatsAlmostEqual(imArr0.flatten(), (-1, 0, 1, 2)) 

174 

175 # amp 1 is a correction of (5, 4, 3, 2), but the first image value is under range 

176 imArr1 = im.Factory(im, ampInfoCat[1].getBBox()).getArray() 

177 self.assertFloatsAlmostEqual(imArr1.flatten(), (4, 5, 5, 5)) 

178 

179 # amp 2 is a constant correction of +1; all image values are in range, but it doesn't matter 

180 imArr2 = im.Factory(im, ampInfoCat[2].getBBox()).getArray() 

181 self.assertFloatsAlmostEqual(imArr2.flatten(), (0, 1, 2, 3)) 

182 

183 # amp 3 is a correction of (7, 6, 5, 4); all image values in range 

184 imArr1 = im.Factory(im, ampInfoCat[3].getBBox()).getArray() 

185 self.assertFloatsAlmostEqual(imArr1.flatten(), (6, 6, 6, 6)) 

186 

187 def testPickle(self): 

188 """!Test that a LinearizeLookupTable can be pickled and unpickled 

189 """ 

190 inImage = makeRampImage(bbox=self.bbox, start=-5, stop=2500) 

191 table = self.makeTable(inImage) 

192 llt = Linearizer(table=table, detector=self.detector) 

193 

194 refImage = inImage.Factory(inImage, True) 

195 refNumOutOfRange = llt.applyLinearity(refImage, self.detector) 

196 

197 pickledStr = pickle.dumps(llt) 

198 restoredLlt = pickle.loads(pickledStr) 

199 

200 measImage = inImage.Factory(inImage, True) 

201 measNumOutOfRange = restoredLlt.applyLinearity(measImage, self.detector) 

202 

203 self.assertEqual(refNumOutOfRange, measNumOutOfRange) 

204 self.assertImagesAlmostEqual(refImage, measImage) 

205 

206 def makeDetector(self, bbox=None, numAmps=None, rowInds=None, colIndOffsets=None, 

207 detName="det_a", detSerial="123", linearityType="LookupTable"): 

208 """!Make a detector 

209 

210 @param[in] bbox bounding box for image 

211 @param[n] numAmps x,y number of amplifiers (pair of int) 

212 @param[in] rowInds index of lookup table for each amplifier (array of shape numAmps) 

213 @param[in] colIndOffsets column index offset for each amplifier (array of shape numAmps) 

214 @param[in] detName detector name (a str) 

215 @param[in] detSerial detector serial numbe (a str) 

216 @param[in] linearityType name of linearity type (a str) 

217 

218 @return a detector (an lsst.afw.cameraGeom.Detector) 

219 """ 

220 bbox = bbox if bbox is not None else self.bbox 

221 numAmps = numAmps if numAmps is not None else self.numAmps 

222 rowInds = rowInds if rowInds is not None else self.rowInds 

223 colIndOffsets = colIndOffsets if colIndOffsets is not None else self.colIndOffsets 

224 

225 detId = 1 

226 orientation = cameraGeom.Orientation() 

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

228 

229 camBuilder = cameraGeom.Camera.Builder("fakeCam") 

230 detBuilder = camBuilder.add(detName, detId) 

231 detBuilder.setSerial(detSerial) 

232 detBuilder.setBBox(bbox) 

233 detBuilder.setOrientation(orientation) 

234 detBuilder.setPixelSize(pixelSize) 

235 

236 boxArr = BoxGrid(box=bbox, numColRow=numAmps) 

237 for i in range(numAmps[0]): 

238 for j in range(numAmps[1]): 

239 ampInfo = cameraGeom.Amplifier.Builder() 

240 ampInfo.setName("amp %d_%d" % (i + 1, j + 1)) 

241 ampInfo.setBBox(boxArr[i, j]) 

242 ampInfo.setLinearityType(linearityType) 

243 # setLinearityCoeffs is picky about getting a mixed int/float list. 

244 ampInfo.setLinearityCoeffs(np.array([rowInds[i, j], colIndOffsets[i, j], 0, 0], dtype=float)) 

245 detBuilder.append(ampInfo) 

246 

247 return detBuilder 

248 

249 def makeTable(self, image, numCols=None, numRows=2500, sigma=55): 

250 """!Make a 2D lookup table 

251 

252 @param[in] image image whose type is used for the table 

253 @param[in] numCols number of columns for table; defaults to self.numCols 

254 @param[in] numRows number of rows for the table 

255 @param[in] sigma standard deviation of normal distribution 

256 """ 

257 numCols = numCols or self.numAmps[0]*self.numAmps[1] 

258 dtype = image.getArray().dtype 

259 table = np.random.normal(scale=sigma, size=(numCols, numRows)) 

260 return np.array(table, dtype=dtype) 

261 

262 

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

264 pass 

265 

266 

267def setup_module(module): 

268 lsst.utils.tests.init() 

269 

270 

271if __name__ == "__main__": 271 ↛ 272line 271 didn't jump to line 272, because the condition on line 271 was never true

272 lsst.utils.tests.init() 

273 unittest.main()