Coverage for python/lsst/ip/isr/linearize.py: 19%

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

222 statements  

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 

23import numpy as np 

24 

25from astropy.table import Table 

26 

27import lsst.afw.math as afwMath 

28from lsst.pipe.base import Struct 

29from lsst.geom import Box2I, Point2I, Extent2I 

30from .applyLookupTable import applyLookupTable 

31from .calibType import IsrCalib 

32 

33__all__ = ["Linearizer", 

34 "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared", 

35 "LinearizeProportional", "LinearizePolynomial", "LinearizeSpline", "LinearizeNone"] 

36 

37 

38class Linearizer(IsrCalib): 

39 """Parameter set for linearization. 

40 

41 These parameters are included in cameraGeom.Amplifier, but 

42 should be accessible externally to allow for testing. 

43 

44 Parameters 

45 ---------- 

46 table : `numpy.array`, optional 

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

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

49 - one column for each image value 

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

51 (numpy default "C" order) 

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

53 Detector object. Passed to self.fromDetector() on init. 

54 log : `logging.Logger`, optional 

55 Logger to handle messages. 

56 kwargs : `dict`, optional 

57 Other keyword arguments to pass to the parent init. 

58 

59 Raises 

60 ------ 

61 RuntimeError : 

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

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

64 

65 Notes 

66 ----- 

67 The linearizer attributes stored are: 

68 

69 hasLinearity : `bool` 

70 Whether a linearity correction is defined for this detector. 

71 override : `bool` 

72 Whether the detector parameters should be overridden. 

73 ampNames : `list` [`str`] 

74 List of amplifier names to correct. 

75 linearityCoeffs : `dict` [`str`, `numpy.array`] 

76 Coefficients to use in correction. Indexed by amplifier 

77 names. The format of the array depends on the type of 

78 correction to apply. 

79 linearityType : `dict` [`str`, `str`] 

80 Type of correction to use, indexed by amplifier names. 

81 linearityBBox : `dict` [`str`, `lsst.geom.Box2I`] 

82 Bounding box the correction is valid over, indexed by 

83 amplifier names. 

84 fitParams : `dict` [`str`, `numpy.array`], optional 

85 Linearity fit parameters used to construct the correction 

86 coefficients, indexed as above. 

87 fitParamsErr : `dict` [`str`, `numpy.array`], optional 

88 Uncertainty values of the linearity fit parameters used to 

89 construct the correction coefficients, indexed as above. 

90 fitChiSq : `dict` [`str`, `float`], optional 

91 Chi-squared value of the linearity fit, indexed as above. 

92 tableData : `numpy.array`, optional 

93 Lookup table data for the linearity correction. 

94 """ 

95 _OBSTYPE = "LINEARIZER" 

96 _SCHEMA = 'Gen3 Linearizer' 

97 _VERSION = 1.1 

98 

99 def __init__(self, table=None, **kwargs): 

100 self.hasLinearity = False 

101 self.override = False 

102 

103 self.ampNames = list() 

104 self.linearityCoeffs = dict() 

105 self.linearityType = dict() 

106 self.linearityBBox = dict() 

107 

108 self.fitParams = dict() 

109 self.fitParamsErr = dict() 

110 self.fitChiSq = dict() 

111 

112 self.tableData = None 

113 if table is not None: 

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

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

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

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

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

119 

120 super().__init__(**kwargs) 

121 self.requiredAttributes.update(['hasLinearity', 'override', 

122 'ampNames', 

123 'linearityCoeffs', 'linearityType', 'linearityBBox', 

124 'fitParams', 'fitParamsErr', 'fitChiSq', 

125 'tableData']) 

126 

127 def updateMetadata(self, setDate=False, **kwargs): 

128 """Update metadata keywords with new values. 

129 

130 This calls the base class's method after ensuring the required 

131 calibration keywords will be saved. 

132 

133 Parameters 

134 ---------- 

135 setDate : `bool`, optional 

136 Update the CALIBDATE fields in the metadata to the current 

137 time. Defaults to False. 

138 kwargs : 

139 Other keyword parameters to set in the metadata. 

140 """ 

141 kwargs['HAS_LINEARITY'] = self.hasLinearity 

142 kwargs['OVERRIDE'] = self.override 

143 kwargs['HAS_TABLE'] = self.tableData is not None 

144 

145 super().updateMetadata(setDate=setDate, **kwargs) 

146 

147 def fromDetector(self, detector): 

148 """Read linearity parameters from a detector. 

149 

150 Parameters 

151 ---------- 

152 detector : `lsst.afw.cameraGeom.detector` 

153 Input detector with parameters to use. 

154 

155 Returns 

156 ------- 

157 calib : `lsst.ip.isr.Linearizer` 

158 The calibration constructed from the detector. 

159 """ 

160 self._detectorName = detector.getName() 

161 self._detectorSerial = detector.getSerial() 

162 self._detectorId = detector.getId() 

163 self.hasLinearity = True 

164 

165 # Do not translate Threshold, Maximum, Units. 

166 for amp in detector.getAmplifiers(): 

167 ampName = amp.getName() 

168 self.ampNames.append(ampName) 

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

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

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

172 

173 return self 

174 

175 @classmethod 

176 def fromDict(cls, dictionary): 

177 """Construct a calibration from a dictionary of properties 

178 

179 Parameters 

180 ---------- 

181 dictionary : `dict` 

182 Dictionary of properties 

183 

184 Returns 

185 ------- 

186 calib : `lsst.ip.isr.Linearity` 

187 Constructed calibration. 

188 

189 Raises 

190 ------ 

191 RuntimeError 

192 Raised if the supplied dictionary is for a different 

193 calibration. 

194 """ 

195 

196 calib = cls() 

197 

198 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']: 

199 raise RuntimeError(f"Incorrect linearity supplied. Expected {calib._OBSTYPE}, " 

200 f"found {dictionary['metadata']['OBSTYPE']}") 

201 

202 calib.setMetadata(dictionary['metadata']) 

203 

204 calib.hasLinearity = dictionary.get('hasLinearity', 

205 dictionary['metadata'].get('HAS_LINEARITY', False)) 

206 calib.override = dictionary.get('override', True) 

207 

208 if calib.hasLinearity: 

209 for ampName in dictionary['amplifiers']: 

210 amp = dictionary['amplifiers'][ampName] 

211 calib.ampNames.append(ampName) 

212 calib.linearityCoeffs[ampName] = np.array(amp.get('linearityCoeffs', [0.0])) 

213 calib.linearityType[ampName] = amp.get('linearityType', 'None') 

214 calib.linearityBBox[ampName] = amp.get('linearityBBox', None) 

215 

216 calib.fitParams[ampName] = np.array(amp.get('fitParams', [0.0])) 

217 calib.fitParamsErr[ampName] = np.array(amp.get('fitParamsErr', [0.0])) 

218 calib.fitChiSq[ampName] = amp.get('fitChiSq', np.nan) 

219 

220 calib.tableData = dictionary.get('tableData', None) 

221 if calib.tableData: 

222 calib.tableData = np.array(calib.tableData) 

223 

224 return calib 

225 

226 def toDict(self): 

227 """Return linearity parameters as a dict. 

228 

229 Returns 

230 ------- 

231 outDict : `dict`: 

232 """ 

233 self.updateMetadata() 

234 

235 outDict = {'metadata': self.getMetadata(), 

236 'detectorName': self._detectorName, 

237 'detectorSerial': self._detectorSerial, 

238 'detectorId': self._detectorId, 

239 'hasTable': self.tableData is not None, 

240 'amplifiers': dict(), 

241 } 

242 for ampName in self.linearityType: 

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

244 'linearityCoeffs': self.linearityCoeffs[ampName].tolist(), 

245 'linearityBBox': self.linearityBBox[ampName], 

246 'fitParams': self.fitParams[ampName].tolist(), 

247 'fitParamsErr': self.fitParamsErr[ampName].tolist(), 

248 'fitChiSq': self.fitChiSq[ampName]} 

249 if self.tableData is not None: 

250 outDict['tableData'] = self.tableData.tolist() 

251 

252 return outDict 

253 

254 @classmethod 

255 def fromTable(cls, tableList): 

256 """Read linearity from a FITS file. 

257 

258 This method uses the `fromDict` method to create the 

259 calibration, after constructing an appropriate dictionary from 

260 the input tables. 

261 

262 Parameters 

263 ---------- 

264 tableList : `list` [`astropy.table.Table`] 

265 afwTable read from input file name. 

266 

267 Returns 

268 ------- 

269 linearity : `~lsst.ip.isr.linearize.Linearizer`` 

270 Linearity parameters. 

271 

272 Notes 

273 ----- 

274 The method reads a FITS file with 1 or 2 extensions. The metadata is 

275 read from the header of extension 1, which must exist. Then the table 

276 is loaded, and the ['AMPLIFIER_NAME', 'TYPE', 'COEFFS', 'BBOX_X0', 

277 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to set each 

278 dictionary by looping over rows. 

279 Extension 2 is then attempted to read in the try block (which only 

280 exists for lookup tables). It has a column named 'LOOKUP_VALUES' that 

281 contains a vector of the lookup entries in each row. 

282 """ 

283 coeffTable = tableList[0] 

284 

285 metadata = coeffTable.meta 

286 inDict = dict() 

287 inDict['metadata'] = metadata 

288 inDict['hasLinearity'] = metadata.get('HAS_LINEARITY', False) 

289 inDict['amplifiers'] = dict() 

290 

291 for record in coeffTable: 

292 ampName = record['AMPLIFIER_NAME'] 

293 

294 fitParams = record['FIT_PARAMS'] if 'FIT_PARAMS' in record.columns else np.array([0.0]) 

295 fitParamsErr = record['FIT_PARAMS_ERR'] if 'FIT_PARAMS_ERR' in record.columns else np.array([0.0]) 

296 fitChiSq = record['RED_CHI_SQ'] if 'RED_CHI_SQ' in record.columns else np.nan 

297 

298 inDict['amplifiers'][ampName] = { 

299 'linearityType': record['TYPE'], 

300 'linearityCoeffs': record['COEFFS'], 

301 'linearityBBox': Box2I(Point2I(record['BBOX_X0'], record['BBOX_Y0']), 

302 Extent2I(record['BBOX_DX'], record['BBOX_DY'])), 

303 'fitParams': fitParams, 

304 'fitParamsErr': fitParamsErr, 

305 'fitChiSq': fitChiSq, 

306 } 

307 

308 if len(tableList) > 1: 

309 tableData = tableList[1] 

310 inDict['tableData'] = [record['LOOKUP_VALUES'] for record in tableData] 

311 

312 return cls().fromDict(inDict) 

313 

314 def toTable(self): 

315 """Construct a list of tables containing the information in this 

316 calibration. 

317 

318 The list of tables should create an identical calibration 

319 after being passed to this class's fromTable method. 

320 

321 Returns 

322 ------- 

323 tableList : `list` [`astropy.table.Table`] 

324 List of tables containing the linearity calibration 

325 information. 

326 """ 

327 

328 tableList = [] 

329 self.updateMetadata() 

330 catalog = Table([{'AMPLIFIER_NAME': ampName, 

331 'TYPE': self.linearityType[ampName], 

332 'COEFFS': self.linearityCoeffs[ampName], 

333 'BBOX_X0': self.linearityBBox[ampName].getMinX(), 

334 'BBOX_Y0': self.linearityBBox[ampName].getMinY(), 

335 'BBOX_DX': self.linearityBBox[ampName].getWidth(), 

336 'BBOX_DY': self.linearityBBox[ampName].getHeight(), 

337 'FIT_PARAMS': self.fitParams[ampName], 

338 'FIT_PARAMS_ERR': self.fitParamsErr[ampName], 

339 'RED_CHI_SQ': self.fitChiSq[ampName], 

340 } for ampName in self.ampNames]) 

341 catalog.meta = self.getMetadata().toDict() 

342 tableList.append(catalog) 

343 

344 if self.tableData is not None: 

345 catalog = Table([{'LOOKUP_VALUES': value} for value in self.tableData]) 

346 tableList.append(catalog) 

347 return(tableList) 

348 

349 def getLinearityTypeByName(self, linearityTypeName): 

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

351 

352 Parameters 

353 ---------- 

354 linearityTypeName : str 

355 String name of the linearity type that is needed. 

356 

357 Returns 

358 ------- 

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

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

361 is found, `None` is returned. 

362 """ 

363 for t in [LinearizeLookupTable, 

364 LinearizeSquared, 

365 LinearizePolynomial, 

366 LinearizeProportional, 

367 LinearizeSpline, 

368 LinearizeNone]: 

369 if t.LinearityType == linearityTypeName: 

370 return t 

371 return None 

372 

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

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

375 

376 Parameters 

377 ---------- 

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

379 Detector to validate, along with its amplifiers. 

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

381 Single amplifier to validate. 

382 

383 Raises 

384 ------ 

385 RuntimeError : 

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

387 the cameraGeom parameters are not being overridden. 

388 """ 

389 amplifiersToCheck = [] 

390 if detector: 

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

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

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

394 if int(self._detectorId) != int(detector.getId()): 

395 raise RuntimeError("Detector IDs don't match: %s != %s" % 

396 (int(self._detectorId), int(detector.getId()))) 

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

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

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

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

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

402 (len(detector.getAmplifiers()), 

403 len(self.linearityCoeffs.keys()))) 

404 amplifiersToCheck.extend(detector.getAmplifiers()) 

405 

406 if amplifier: 

407 amplifiersToCheck.extend(amplifier) 

408 

409 for amp in amplifiersToCheck: 

410 ampName = amp.getName() 

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

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

413 (ampName, )) 

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

415 if self.override: 

416 self.log.warning("Overriding amplifier defined linearityType (%s) for %s", 

417 self.linearityType[ampName], ampName) 

418 else: 

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

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

421 if (amp.getLinearityCoeffs().shape != self.linearityCoeffs[ampName].shape or not 

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

423 if self.override: 

424 self.log.warning("Overriding amplifier defined linearityCoeffs (%s) for %s", 

425 self.linearityCoeffs[ampName], ampName) 

426 else: 

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

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

429 

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

431 """Apply the linearity to an image. 

432 

433 If the linearity parameters are populated, use those, 

434 otherwise use the values from the detector. 

435 

436 Parameters 

437 ---------- 

438 image : `~lsst.afw.image.image` 

439 Image to correct. 

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

441 Detector to use for linearity parameters if not already 

442 populated. 

443 log : `~logging.Logger`, optional 

444 Log object to use for logging. 

445 """ 

446 if log is None: 

447 log = self.log 

448 if detector and not self.hasLinearity: 

449 self.fromDetector(detector) 

450 

451 self.validate(detector) 

452 

453 numAmps = 0 

454 numLinearized = 0 

455 numOutOfRange = 0 

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

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

458 numAmps += 1 

459 if linearizer is not None: 

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

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

462 'table': self.tableData, 

463 'log': self.log}) 

464 numOutOfRange += outOfRange 

465 if success: 

466 numLinearized += 1 

467 elif log is not None: 

468 log.warning("Amplifier %s did not linearize.", 

469 ampName) 

470 return Struct( 

471 numAmps=numAmps, 

472 numLinearized=numLinearized, 

473 numOutOfRange=numOutOfRange 

474 ) 

475 

476 

477class LinearizeBase(metaclass=abc.ABCMeta): 

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

479 

480 Subclasses must define __call__ and set class variable 

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

482 the cameraGeom.Amplifier.linearityType field. 

483 

484 All linearity corrections should be defined in terms of an 

485 additive correction, such that: 

486 

487 corrected_value = uncorrected_value + f(uncorrected_value) 

488 """ 

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

490 

491 @abc.abstractmethod 

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

493 """Correct non-linearity. 

494 

495 Parameters 

496 ---------- 

497 image : `lsst.afw.image.Image` 

498 Image to be corrected 

499 kwargs : `dict` 

500 Dictionary of parameter keywords: 

501 ``"coeffs"`` 

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

503 ``"table"`` 

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

505 ``"log"`` 

506 Logger to handle messages (`logging.Logger`). 

507 

508 Returns 

509 ------- 

510 output : `bool` 

511 If true, a correction was applied successfully. 

512 

513 Raises 

514 ------ 

515 RuntimeError: 

516 Raised if the linearity type listed in the 

517 detector does not match the class type. 

518 """ 

519 pass 

520 

521 

522class LinearizeLookupTable(LinearizeBase): 

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

524 

525 The lookup table consists of entries such that given 

526 "coefficients" c0, c1: 

527 

528 for each i,j of image: 

529 rowInd = int(c0) 

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

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

532 

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

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

535 amplifiers use the same table) 

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

537 before truncation; this supports tables that can handle 

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

539 the nearest index is used instead of truncating to the 

540 next smaller index 

541 """ 

542 LinearityType = "LookupTable" 

543 

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

545 """Correct for non-linearity. 

546 

547 Parameters 

548 ---------- 

549 image : `lsst.afw.image.Image` 

550 Image to be corrected 

551 kwargs : `dict` 

552 Dictionary of parameter keywords: 

553 ``"coeffs"`` 

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

555 ``"table"`` 

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

557 ``"log"`` 

558 Logger to handle messages (`logging.Logger`). 

559 

560 Returns 

561 ------- 

562 output : `tuple` [`bool`, `int`] 

563 If true, a correction was applied successfully. The 

564 integer indicates the number of pixels that were 

565 uncorrectable by being out of range. 

566 

567 Raises 

568 ------ 

569 RuntimeError: 

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

571 bounds. 

572 """ 

573 numOutOfRange = 0 

574 

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

576 table = kwargs['table'] 

577 log = kwargs['log'] 

578 

579 numTableRows = table.shape[0] 

580 rowInd = int(rowInd) 

581 if rowInd < 0 or rowInd > numTableRows: 

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

583 (rowInd, numTableRows)) 

584 tableRow = np.array(table[rowInd, :], dtype=image.getArray().dtype) 

585 

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

587 

588 if numOutOfRange > 0 and log is not None: 

589 log.warning("%s pixels were out of range of the linearization table", 

590 numOutOfRange) 

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

592 return True, numOutOfRange 

593 else: 

594 return False, numOutOfRange 

595 

596 

597class LinearizePolynomial(LinearizeBase): 

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

599 

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

601 

602 where c_i are the linearity coefficients for each amplifier. 

603 Lower order coefficients are not included as they duplicate other 

604 calibration parameters: 

605 ``"k0"`` 

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

607 bias level. Irrelevant for correcting non-linearity. 

608 ``"k1"`` 

609 A coefficient multiplied by uncorrImage**1 is proportional 

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

611 """ 

612 LinearityType = "Polynomial" 

613 

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

615 """Correct non-linearity. 

616 

617 Parameters 

618 ---------- 

619 image : `lsst.afw.image.Image` 

620 Image to be corrected 

621 kwargs : `dict` 

622 Dictionary of parameter keywords: 

623 ``"coeffs"`` 

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

625 If the order of the polynomial is n, this list 

626 should have a length of n-1 ("k0" and "k1" are 

627 not needed for the correction). 

628 ``"log"`` 

629 Logger to handle messages (`logging.Logger`). 

630 

631 Returns 

632 ------- 

633 output : `tuple` [`bool`, `int`] 

634 If true, a correction was applied successfully. The 

635 integer indicates the number of pixels that were 

636 uncorrectable by being out of range. 

637 """ 

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

639 return False, 0 

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

641 return False, 0 

642 

643 ampArray = image.getArray() 

644 correction = np.zeros_like(ampArray) 

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

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

647 ampArray += correction 

648 

649 return True, 0 

650 

651 

652class LinearizeSquared(LinearizeBase): 

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

654 

655 corrImage = uncorrImage + c0*uncorrImage^2 

656 

657 where c0 is linearity coefficient 0 for each amplifier. 

658 """ 

659 LinearityType = "Squared" 

660 

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

662 """Correct for non-linearity. 

663 

664 Parameters 

665 ---------- 

666 image : `lsst.afw.image.Image` 

667 Image to be corrected 

668 kwargs : `dict` 

669 Dictionary of parameter keywords: 

670 ``"coeffs"`` 

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

672 ``"log"`` 

673 Logger to handle messages (`logging.Logger`). 

674 

675 Returns 

676 ------- 

677 output : `tuple` [`bool`, `int`] 

678 If true, a correction was applied successfully. The 

679 integer indicates the number of pixels that were 

680 uncorrectable by being out of range. 

681 """ 

682 

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

684 if sqCoeff != 0: 

685 ampArr = image.getArray() 

686 ampArr *= (1 + sqCoeff*ampArr) 

687 return True, 0 

688 else: 

689 return False, 0 

690 

691 

692class LinearizeSpline(LinearizeBase): 

693 """Correct non-linearity with a spline model. 

694 

695 corrImage = uncorrImage - Spline(coeffs, uncorrImage) 

696 

697 Notes 

698 ----- 

699 

700 The spline fit calculates a correction as a function of the 

701 expected linear flux term. Because of this, the correction needs 

702 to be subtracted from the observed flux. 

703 

704 """ 

705 LinearityType = "Spline" 

706 

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

708 """Correct for non-linearity. 

709 

710 Parameters 

711 ---------- 

712 image : `lsst.afw.image.Image` 

713 Image to be corrected 

714 kwargs : `dict` 

715 Dictionary of parameter keywords: 

716 ``"coeffs"`` 

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

718 ``"log"`` 

719 Logger to handle messages (`logging.Logger`). 

720 

721 Returns 

722 ------- 

723 output : `tuple` [`bool`, `int`] 

724 If true, a correction was applied successfully. The 

725 integer indicates the number of pixels that were 

726 uncorrectable by being out of range. 

727 """ 

728 splineCoeff = kwargs['coeffs'] 

729 centers, values = np.split(splineCoeff, 2) 

730 interp = afwMath.makeInterpolate(centers.tolist(), values.tolist(), 

731 afwMath.stringToInterpStyle("AKIMA_SPLINE")) 

732 

733 ampArr = image.getArray() 

734 delta = interp.interpolate(ampArr.flatten()) 

735 ampArr -= np.array(delta).reshape(ampArr.shape) 

736 

737 return True, 0 

738 

739 

740class LinearizeProportional(LinearizeBase): 

741 """Do not correct non-linearity. 

742 """ 

743 LinearityType = "Proportional" 

744 

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

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

747 

748 Parameters 

749 ---------- 

750 image : `lsst.afw.image.Image` 

751 Image to be corrected 

752 kwargs : `dict` 

753 Dictionary of parameter keywords: 

754 ``"coeffs"`` 

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

756 ``"log"`` 

757 Logger to handle messages (`logging.Logger`). 

758 

759 Returns 

760 ------- 

761 output : `tuple` [`bool`, `int`] 

762 If true, a correction was applied successfully. The 

763 integer indicates the number of pixels that were 

764 uncorrectable by being out of range. 

765 """ 

766 return True, 0 

767 

768 

769class LinearizeNone(LinearizeBase): 

770 """Do not correct non-linearity. 

771 """ 

772 LinearityType = "None" 

773 

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

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

776 

777 Parameters 

778 ---------- 

779 image : `lsst.afw.image.Image` 

780 Image to be corrected 

781 kwargs : `dict` 

782 Dictionary of parameter keywords: 

783 ``"coeffs"`` 

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

785 ``"log"`` 

786 Logger to handle messages (`logging.Logger`). 

787 

788 Returns 

789 ------- 

790 output : `tuple` [`bool`, `int`] 

791 If true, a correction was applied successfully. The 

792 integer indicates the number of pixels that were 

793 uncorrectable by being out of range. 

794 """ 

795 return True, 0