Hide keyboard shortcuts

Hot-keys 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

1# 

2# LSST Data Management System 

3# Copyright 2016 AURA/LSST. 

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 abc 

23 

24import numpy as np 

25 

26from lsst.pipe.base import Struct 

27from .applyLookupTable import applyLookupTable 

28 

29__all__ = ["Linearizer", 

30 "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared", 

31 "LinearizeProportional", "LinearizePolynomial", "LinearizeNone"] 

32 

33 

34class Linearizer(abc.ABC): 

35 """Parameter set for linearization. 

36 

37 These parameters are included in cameraGeom.Amplifier, but 

38 should be accessible externally to allow for testing. 

39 

40 Parameters 

41 ---------- 

42 table : `numpy.array`, optional 

43 Lookup table; a 2-dimensional array of floats: 

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

45 - one column for each image value 

46 To avoid copying the table the last index should vary fastest 

47 (numpy default "C" order) 

48 override : `bool`, optional 

49 Override the parameters defined in the detector/amplifier. 

50 log : `lsst.log.Log`, optional 

51 Logger to handle messages. 

52 

53 Raises 

54 ------ 

55 RuntimeError : 

56 Raised if the supplied table is not 2D, or if the table has fewer 

57 columns than rows (indicating that the indices are swapped). 

58 """ 

59 def __init__(self, table=None, detector=None, override=False, log=None): 

60 self._detectorName = None 

61 self._detectorSerial = None 

62 

63 self.linearityCoeffs = dict() 

64 self.linearityType = dict() 

65 self.linearityThreshold = dict() 

66 self.linearityMaximum = dict() 

67 self.linearityUnits = dict() 

68 self.linearityBBox = dict() 

69 

70 self.override = override 

71 self.populated = False 

72 self.log = log 

73 

74 self.tableData = None 

75 if table is not None: 

76 if len(table.shape) != 2: 

77 raise RuntimeError("table shape = %s; must have two dimensions" % (table.shape,)) 

78 if table.shape[1] < table.shape[0]: 

79 raise RuntimeError("table shape = %s; indices are switched" % (table.shape,)) 

80 self.tableData = np.array(table, order="C") 

81 

82 if detector: 

83 self.fromDetector(detector) 

84 

85 def __call__(self, exposure): 

86 """Apply linearity, setting parameters if necessary. 

87 

88 Parameters 

89 ---------- 

90 exposure : `lsst.afw.image.Exposure` 

91 Exposure to correct. 

92 

93 Returns 

94 ------- 

95 output : `lsst.pipe.base.Struct` 

96 Linearization results: 

97 ``"numAmps"`` 

98 Number of amplifiers considered. 

99 ``"numLinearized"`` 

100 Number of amplifiers linearized. 

101 """ 

102 

103 def fromDetector(self, detector): 

104 """Read linearity parameters from a detector. 

105 

106 Parameters 

107 ---------- 

108 detector : `lsst.afw.cameraGeom.detector` 

109 Input detector with parameters to use. 

110 """ 

111 self._detectorName = detector.getName() 

112 self._detectorSerial = detector.getSerial() 

113 self.populated = True 

114 

115 # Do not translate Threshold, Maximum, Units. 

116 for amp in detector.getAmplifiers(): 

117 ampName = amp.getName() 

118 self.linearityCoeffs[ampName] = amp.getLinearityCoeffs() 

119 self.linearityType[ampName] = amp.getLinearityType() 

120 self.linearityBBox[ampName] = amp.getBBox() 

121 

122 def fromYaml(self, yamlObject): 

123 """Read linearity parameters from a dict. 

124 

125 Parameters 

126 ---------- 

127 yamlObject : `dict` 

128 Dictionary containing detector and amplifier information. 

129 """ 

130 self._detectorName = yamlObject['detectorName'] 

131 self._detectorSerial = yamlObject['detectorSerial'] 

132 self.populated = True 

133 self.override = True 

134 

135 for amp in yamlObject['amplifiers']: 

136 ampName = amp['name'] 

137 self.linearityCoeffs[ampName] = amp.get('linearityCoeffs', None) 

138 self.linearityType[ampName] = amp.get('linearityType', 'None') 

139 self.linearityBBox[ampName] = amp.get('linearityBBox', None) 

140 

141 def toDict(self): 

142 """Return linearity parameters as a dict. 

143 

144 Returns 

145 ------- 

146 outDict : `dict`: 

147 """ 

148 outDict = {'detectorName': self._detectorName, 

149 'detectorSerial': self._detectorSerial, 

150 'hasTable': self._table is not None, 

151 'amplifiers': dict()} 

152 for ampName in self.linearityType: 

153 outDict['amplifiers'][ampName] = {'linearityType': self.linearityType[ampName], 

154 'linearityCoeffs': self.linearityCoeffs[ampName], 

155 'linearityBBox': self.linearityBBox[ampName]} 

156 

157 def getLinearityTypeByName(self, linearityTypeName): 

158 """Determine the linearity class to use from the type name. 

159 

160 Parameters 

161 ---------- 

162 linearityTypeName : str 

163 String name of the linearity type that is needed. 

164 

165 Returns 

166 ------- 

167 linearityType : `~lsst.ip.isr.linearize.LinearizeBase` 

168 The appropriate linearity class to use. If no matching class 

169 is found, `None` is returned. 

170 """ 

171 for t in [LinearizeLookupTable, 

172 LinearizeSquared, 

173 LinearizePolynomial, 

174 LinearizeProportional, 

175 LinearizeNone]: 

176 if t.LinearityType == linearityTypeName: 

177 return t 

178 return None 

179 

180 def validate(self, detector=None, amplifier=None): 

181 """Validate linearity for a detector/amplifier. 

182 

183 Parameters 

184 ---------- 

185 detector : `lsst.afw.cameraGeom.Detector`, optional 

186 Detector to validate, along with its amplifiers. 

187 amplifier : `lsst.afw.cameraGeom.Amplifier`, optional 

188 Single amplifier to validate. 

189 

190 Raises 

191 ------ 

192 RuntimeError : 

193 Raised if there is a mismatch in linearity parameters, and 

194 the cameraGeom parameters are not being overridden. 

195 """ 

196 amplifiersToCheck = [] 

197 if detector: 

198 if self._detectorName != detector.getName(): 

199 raise RuntimeError("Detector names don't match: %s != %s" % 

200 (self._detectorName, detector.getName())) 

201 if self._detectorSerial != detector.getSerial(): 

202 raise RuntimeError("Detector serial numbers don't match: %s != %s" % 

203 (self._detectorSerial, detector.getSerial())) 

204 if len(detector.getAmplifiers()) != len(self.linearityCoeffs.keys()): 

205 raise RuntimeError("Detector number of amps = %s does not match saved value %s" % 

206 (len(detector.getAmplifiers()), 

207 len(self.linearityCoeffs.keys()))) 

208 amplifiersToCheck.extend(detector.getAmplifiers()) 

209 

210 if amplifier: 

211 amplifiersToCheck.extend(amplifier) 

212 

213 for amp in amplifiersToCheck: 

214 ampName = amp.getName() 

215 if ampName not in self.linearityCoeffs.keys(): 

216 raise RuntimeError("Amplifier %s is not in linearity data" % 

217 (ampName, )) 

218 if amp.getLinearityType() != self.linearityType[ampName]: 

219 if self.override: 

220 self.log.warn("Overriding amplifier defined linearityType (%s) for %s", 

221 self.linearityType[ampName], ampName) 

222 else: 

223 raise RuntimeError("Amplifier %s type %s does not match saved value %s" % 

224 (ampName, amp.getLinearityType(), self.linearityType[ampName])) 

225 if not np.allclose(amp.getLinearityCoeffs(), self.linearityCoeffs[ampName], equal_nan=True): 

226 if self.override: 

227 self.log.warn("Overriding amplifier defined linearityCoeffs (%s) for %s", 

228 self.linearityCoeffs[ampName], ampName) 

229 else: 

230 raise RuntimeError("Amplifier %s coeffs %s does not match saved value %s" % 

231 (ampName, amp.getLinearityCoeffs(), self.linearityCoeffs[ampName])) 

232 

233 def applyLinearity(self, image, detector=None, log=None): 

234 """Apply the linearity to an image. 

235 

236 If the linearity parameters are populated, use those, 

237 otherwise use the values from the detector. 

238 

239 Parameters 

240 ---------- 

241 image : `~lsst.afw.image.image` 

242 Image to correct. 

243 detector : `~lsst.afw.cameraGeom.detector` 

244 Detector to use for linearity parameters if not already 

245 populated. 

246 log : `~lsst.log.Log`, optional 

247 Log object to use for logging. 

248 """ 

249 if log is None: 

250 log = self.log 

251 

252 if detector and not self.populated: 

253 self.fromDetector(detector) 

254 

255 self.validate(detector) 

256 

257 numAmps = 0 

258 numLinearized = 0 

259 numOutOfRange = 0 

260 for ampName in self.linearityType.keys(): 

261 linearizer = self.getLinearityTypeByName(self.linearityType[ampName]) 

262 numAmps += 1 

263 if linearizer is not None: 

264 ampView = image.Factory(image, self.linearityBBox[ampName]) 

265 success, outOfRange = linearizer()(ampView, **{'coeffs': self.linearityCoeffs[ampName], 

266 'table': self.tableData, 

267 'log': self.log}) 

268 numOutOfRange += outOfRange 

269 if success: 

270 numLinearized += 1 

271 elif log is not None: 

272 log.warn("Amplifier %s did not linearize.", 

273 ampName) 

274 return Struct( 

275 numAmps=numAmps, 

276 numLinearized=numLinearized, 

277 numOutOfRange=numOutOfRange 

278 ) 

279 

280 

281class LinearizeBase(metaclass=abc.ABCMeta): 

282 """Abstract base class functor for correcting non-linearity. 

283 

284 Subclasses must define __call__ and set class variable 

285 LinearityType to a string that will be used for linearity type in 

286 the cameraGeom.Amplifier.linearityType field. 

287 

288 All linearity corrections should be defined in terms of an 

289 additive correction, such that: 

290 

291 corrected_value = uncorrected_value + f(uncorrected_value) 

292 """ 

293 LinearityType = None # linearity type, a string used for AmpInfoCatalogs 

294 

295 @abc.abstractmethod 

296 def __call__(self, image, **kwargs): 

297 """Correct non-linearity. 

298 

299 Parameters 

300 ---------- 

301 image : `lsst.afw.image.Image` 

302 Image to be corrected 

303 kwargs : `dict` 

304 Dictionary of parameter keywords: 

305 ``"coeffs"`` 

306 Coefficient vector (`list` or `numpy.array`). 

307 ``"table"`` 

308 Lookup table data (`numpy.array`). 

309 ``"log"`` 

310 Logger to handle messages (`lsst.log.Log`). 

311 

312 Returns 

313 ------- 

314 output : `bool` 

315 If true, a correction was applied successfully. 

316 

317 Raises 

318 ------ 

319 RuntimeError: 

320 Raised if the linearity type listed in the 

321 detector does not match the class type. 

322 """ 

323 pass 

324 

325 

326class LinearizeLookupTable(LinearizeBase): 

327 """Correct non-linearity with a persisted lookup table. 

328 

329 The lookup table consists of entries such that given 

330 "coefficients" c0, c1: 

331 

332 for each i,j of image: 

333 rowInd = int(c0) 

334 colInd = int(c1 + uncorrImage[i,j]) 

335 corrImage[i,j] = uncorrImage[i,j] + table[rowInd, colInd] 

336 

337 - c0: row index; used to identify which row of the table to use 

338 (typically one per amplifier, though one can have multiple 

339 amplifiers use the same table) 

340 - c1: column index offset; added to the uncorrected image value 

341 before truncation; this supports tables that can handle 

342 negative image values; also, if the c1 ends with .5 then 

343 the nearest index is used instead of truncating to the 

344 next smaller index 

345 """ 

346 LinearityType = "LookupTable" 

347 

348 def __call__(self, image, **kwargs): 

349 """Correct for non-linearity. 

350 

351 Parameters 

352 ---------- 

353 image : `lsst.afw.image.Image` 

354 Image to be corrected 

355 kwargs : `dict` 

356 Dictionary of parameter keywords: 

357 ``"coeffs"`` 

358 Columnation vector (`list` or `numpy.array`). 

359 ``"table"`` 

360 Lookup table data (`numpy.array`). 

361 ``"log"`` 

362 Logger to handle messages (`lsst.log.Log`). 

363 

364 Returns 

365 ------- 

366 output : `bool` 

367 If true, a correction was applied successfully. 

368 

369 Raises 

370 ------ 

371 RuntimeError: 

372 Raised if the requested row index is out of the table 

373 bounds. 

374 """ 

375 numOutOfRange = 0 

376 

377 rowInd, colIndOffset = kwargs['coeffs'][0:2] 

378 table = kwargs['table'] 

379 log = kwargs['log'] 

380 

381 numTableRows = table.shape[0] 

382 rowInd = int(rowInd) 

383 if rowInd < 0 or rowInd > numTableRows: 

384 raise RuntimeError("LinearizeLookupTable rowInd=%s not in range[0, %s)" % 

385 (rowInd, numTableRows)) 

386 tableRow = table[rowInd, :] 

387 numOutOfRange += applyLookupTable(image, tableRow, colIndOffset) 

388 

389 if numOutOfRange > 0 and log is not None: 

390 log.warn("%s pixels were out of range of the linearization table", 

391 numOutOfRange) 

392 if numOutOfRange < image.getArray().size: 

393 return True, numOutOfRange 

394 else: 

395 return False, numOutOfRange 

396 

397 

398class LinearizePolynomial(LinearizeBase): 

399 """Correct non-linearity with a polynomial mode. 

400 

401 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i) 

402 

403 where c_i are the linearity coefficients for each amplifier. 

404 Lower order coefficients are not included as they duplicate other 

405 calibration parameters: 

406 ``"k0"`` 

407 A coefficient multiplied by uncorrImage**0 is equivalent to 

408 bias level. Irrelevant for correcting non-linearity. 

409 ``"k1"`` 

410 A coefficient multiplied by uncorrImage**1 is proportional 

411 to the gain. Not necessary for correcting non-linearity. 

412 """ 

413 LinearityType = "Polynomial" 

414 

415 def __call__(self, image, **kwargs): 

416 """Correct non-linearity. 

417 

418 Parameters 

419 ---------- 

420 image : `lsst.afw.image.Image` 

421 Image to be corrected 

422 kwargs : `dict` 

423 Dictionary of parameter keywords: 

424 ``"coeffs"`` 

425 Coefficient vector (`list` or `numpy.array`). 

426 ``"log"`` 

427 Logger to handle messages (`lsst.log.Log`). 

428 

429 Returns 

430 ------- 

431 output : `bool` 

432 If true, a correction was applied successfully. 

433 """ 

434 if np.any(np.isfinite(kwargs['coeffs'])): 

435 return False, 0 

436 if not np.any(kwargs['coeffs']): 

437 return False, 0 

438 

439 ampArray = image.getArray() 

440 correction = np.zeroes_like(ampArray) 

441 for coeff, order in enumerate(kwargs['coeffs'], start=2): 

442 correction += coeff * np.power(ampArray, order) 

443 ampArray += correction 

444 

445 return True, 0 

446 

447 

448class LinearizeSquared(LinearizeBase): 

449 """Correct non-linearity with a squared model. 

450 

451 corrImage = uncorrImage + c0*uncorrImage^2 

452 

453 where c0 is linearity coefficient 0 for each amplifier. 

454 """ 

455 LinearityType = "Squared" 

456 

457 def __call__(self, image, **kwargs): 

458 """Correct for non-linearity. 

459 

460 Parameters 

461 ---------- 

462 image : `lsst.afw.image.Image` 

463 Image to be corrected 

464 kwargs : `dict` 

465 Dictionary of parameter keywords: 

466 ``"coeffs"`` 

467 Coefficient vector (`list` or `numpy.array`). 

468 ``"log"`` 

469 Logger to handle messages (`lsst.log.Log`). 

470 

471 Returns 

472 ------- 

473 output : `bool` 

474 If true, a correction was applied successfully. 

475 """ 

476 

477 sqCoeff = kwargs['coeffs'][0] 

478 if sqCoeff != 0: 

479 ampArr = image.getArray() 

480 ampArr *= (1 + sqCoeff*ampArr) 

481 return True, 0 

482 else: 

483 return False, 0 

484 

485 

486class LinearizeProportional(LinearizeBase): 

487 """Do not correct non-linearity. 

488 """ 

489 LinearityType = "Proportional" 

490 

491 def __call__(self, image, **kwargs): 

492 """Do not correct for non-linearity. 

493 

494 Parameters 

495 ---------- 

496 image : `lsst.afw.image.Image` 

497 Image to be corrected 

498 kwargs : `dict` 

499 Dictionary of parameter keywords: 

500 ``"coeffs"`` 

501 Coefficient vector (`list` or `numpy.array`). 

502 ``"log"`` 

503 Logger to handle messages (`lsst.log.Log`). 

504 

505 Returns 

506 ------- 

507 output : `bool` 

508 If true, a correction was applied successfully. 

509 """ 

510 return True, 0 

511 

512 

513class LinearizeNone(LinearizeBase): 

514 """Do not correct non-linearity. 

515 """ 

516 LinearityType = "None" 

517 

518 def __call__(self, image, **kwargs): 

519 """Do not correct for non-linearity. 

520 

521 Parameters 

522 ---------- 

523 image : `lsst.afw.image.Image` 

524 Image to be corrected 

525 kwargs : `dict` 

526 Dictionary of parameter keywords: 

527 ``"coeffs"`` 

528 Coefficient vector (`list` or `numpy.array`). 

529 ``"log"`` 

530 Logger to handle messages (`lsst.log.Log`). 

531 

532 Returns 

533 ------- 

534 output : `bool` 

535 If true, a correction was applied successfully. 

536 """ 

537 return True, 0