Coverage for tests / test_linearizeLookupTable.py: 17%

145 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-23 08:35 +0000

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 

40 of some type) 

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

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

43 image; 

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

45 catalog) 

46 - one column for each image value 

47 

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

49 table 

50 """ 

51 ampInfoCat = detector.getAmplifiers() 

52 numOutOfRange = 0 

53 for ampInfo in ampInfoCat: 

54 bbox = ampInfo.getBBox() 

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

56 rowInd = int(rowInd) 

57 tableRow = table[rowInd, :] 

58 imView = image.Factory(image, bbox) 

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

60 return numOutOfRange 

61 

62 

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

64 """!Unit tests for LinearizeLookupTable""" 

65 

66 def setUp(self): 

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

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

69 self.numAmps = (2, 3) 

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

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

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

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

74 self.detector = self.makeDetector() 

75 self.rng = np.random.Generator(np.random.MT19937(1)) 

76 

77 def tearDown(self): 

78 # destroy LSST objects so memory test passes 

79 self.bbox = None 

80 self.detector = None 

81 

82 def testBasics(self): 

83 """!Test basic functionality of LinearizeLookupTable 

84 """ 

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

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

87 table = self.makeTable(inImage) 

88 

89 log = logging.getLogger("lsst.ip.isr.LinearizeLookupTable") 

90 

91 measImage = inImage.Factory(inImage, True) 

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

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

94 

95 refImage = inImage.Factory(inImage, True) 

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

97 

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

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

100 self.assertEqual(linRes.numOutOfRange, refNumOutOfRange) 

101 self.assertImagesAlmostEqual(refImage, measImage) 

102 

103 # make sure logging is accepted 

104 log = logging.getLogger("lsst.ip.isr.LinearizeLookupTable") 

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

106 

107 def testErrorHandling(self): 

108 """!Test error handling in LinearizeLookupTable 

109 """ 

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

111 table = self.makeTable(image) 

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

113 

114 # bad name 

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

116 with self.assertRaises(RuntimeError): 

117 llt.applyLinearity(image, detBadName) 

118 

119 # TODO: DM-38778: bad serial value disabled. 

120 # detBadSerial = self.makeDetector(detSerial="bad_detector_serial") 

121 # with self.assertRaises(RuntimeError): 

122 # llt.applyLinearity(image, detBadSerial) 

123 

124 # bad number of amplifiers 

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

126 detBadNumMaps = self.makeDetector(numAmps=badNumAmps) 

127 with self.assertRaises(RuntimeError): 

128 llt.applyLinearity(image, detBadNumMaps) 

129 

130 # bad linearity type 

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

132 with self.assertRaises(RuntimeError): 

133 llt.applyLinearity(image, detBadLinType) 

134 

135 # wrong dimension 

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

137 with self.assertRaises(RuntimeError): 

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

139 

140 # wrong size 

141 badTable = np.transpose(table) 

142 with self.assertRaises(RuntimeError): 

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

144 

145 def testKnown(self): 

146 """!Test a few known values 

147 """ 

148 numAmps = (2, 2) 

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

150 # make a 4x4 image with 4 identical 2x2 subregions that flatten 

151 # to -1, 0, 1, 2 

152 im = afwImage.ImageF(bbox) 

153 imArr = im.getArray() 

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

155 (1, 2, 1, 2), 

156 (-1, 0, -1, 0), 

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

158 

159 def castAndReshape(arr): 

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

161 arr.shape = numAmps 

162 return arr 

163 

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

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

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

167 ampInfoCat = detector.getAmplifiers() 

168 

169 # note: table rows are reversed relative to amplifier order because 

170 # rowInds is a descending ramp 

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

172 

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

174 

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

176 self.assertEqual(lltRes.numOutOfRange, 2) 

177 

178 # amp 0 is a constant correction of 0; one image value is out of range, 

179 # but it doesn't matter 

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

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

182 

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

184 # under range 

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

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

187 

188 # amp 2 is a constant correction of +1; all image values are in range, 

189 # but it doesn't matter 

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

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

192 

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

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

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

196 

197 def testPickle(self): 

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

199 """ 

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

201 table = self.makeTable(inImage) 

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

203 

204 refImage = inImage.Factory(inImage, True) 

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

206 

207 pickledStr = pickle.dumps(llt) 

208 restoredLlt = pickle.loads(pickledStr) 

209 

210 measImage = inImage.Factory(inImage, True) 

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

212 

213 self.assertEqual(refNumOutOfRange, measNumOutOfRange) 

214 self.assertImagesAlmostEqual(refImage, measImage) 

215 

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

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

218 """!Make a detector 

219 

220 @param[in] bbox bounding box for image 

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

222 @param[in] rowInds index of lookup table for each amplifier (array of 

223 shape numAmps) 

224 @param[in] colIndOffsets column index offset for each amplifier 

225 (array of shape numAmps) 

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

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

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

229 

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

231 """ 

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

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

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

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

236 

237 detId = 1 

238 orientation = cameraGeom.Orientation() 

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

240 

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

242 detBuilder = camBuilder.add(detName, detId) 

243 detBuilder.setSerial(detSerial) 

244 detBuilder.setBBox(bbox) 

245 detBuilder.setOrientation(orientation) 

246 detBuilder.setPixelSize(pixelSize) 

247 

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

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

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

251 ampInfo = cameraGeom.Amplifier.Builder() 

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

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

254 ampInfo.setLinearityType(linearityType) 

255 # setLinearityCoeffs is picky about getting a mixed int/float 

256 # list. 

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

258 detBuilder.append(ampInfo) 

259 

260 return detBuilder 

261 

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

263 """!Make a 2D lookup table 

264 

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

266 @param[in] numCols number of columns for table; defaults to 

267 self.numCols 

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

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

270 """ 

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

272 dtype = image.getArray().dtype 

273 table = self.rng.normal(scale=sigma, size=(numCols, numRows)) 

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

275 

276 

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

278 pass 

279 

280 

281def setup_module(module): 

282 lsst.utils.tests.init() 

283 

284 

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

286 lsst.utils.tests.init() 

287 unittest.main()