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 

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 : `lsst.log.Log`, 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 read from the header of 

275 extension 1, which must exist. Then the table is loaded, and the ['AMPLIFIER_NAME', 'TYPE', 

276 'COEFFS', 'BBOX_X0', 'BBOX_Y0', 'BBOX_DX', 'BBOX_DY'] columns are read and used to 

277 set each dictionary by looping over rows. 

278 Eextension 2 is then attempted to read in the try block (which only exists for lookup tables). 

279 It has a column named 'LOOKUP_VALUES' that contains a vector of the lookup entries in each row. 

280 

281 """ 

282 coeffTable = tableList[0] 

283 

284 metadata = coeffTable.meta 

285 inDict = dict() 

286 inDict['metadata'] = metadata 

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

288 inDict['amplifiers'] = dict() 

289 

290 for record in coeffTable: 

291 ampName = record['AMPLIFIER_NAME'] 

292 

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

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

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

296 

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

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

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

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

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

302 'fitParams': fitParams, 

303 'fitParamsErr': fitParamsErr, 

304 'fitChiSq': fitChiSq, 

305 } 

306 

307 if len(tableList) > 1: 

308 tableData = tableList[1] 

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

310 

311 return cls().fromDict(inDict) 

312 

313 def toTable(self): 

314 """Construct a list of tables containing the information in this calibration 

315 

316 The list of tables should create an identical calibration 

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

318 

319 Returns 

320 ------- 

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

322 List of tables containing the linearity calibration 

323 information. 

324 """ 

325 

326 tableList = [] 

327 self.updateMetadata() 

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

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

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

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

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

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

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

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

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

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

338 } for ampName in self.ampNames]) 

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

340 tableList.append(catalog) 

341 

342 if self.tableData: 

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

344 tableList.append(catalog) 

345 return(tableList) 

346 

347 def getLinearityTypeByName(self, linearityTypeName): 

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

349 

350 Parameters 

351 ---------- 

352 linearityTypeName : str 

353 String name of the linearity type that is needed. 

354 

355 Returns 

356 ------- 

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

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

359 is found, `None` is returned. 

360 """ 

361 for t in [LinearizeLookupTable, 

362 LinearizeSquared, 

363 LinearizePolynomial, 

364 LinearizeProportional, 

365 LinearizeSpline, 

366 LinearizeNone]: 

367 if t.LinearityType == linearityTypeName: 

368 return t 

369 return None 

370 

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

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

373 

374 Parameters 

375 ---------- 

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

377 Detector to validate, along with its amplifiers. 

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

379 Single amplifier to validate. 

380 

381 Raises 

382 ------ 

383 RuntimeError : 

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

385 the cameraGeom parameters are not being overridden. 

386 """ 

387 amplifiersToCheck = [] 

388 if detector: 

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

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

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

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

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

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

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

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

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

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

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

400 (len(detector.getAmplifiers()), 

401 len(self.linearityCoeffs.keys()))) 

402 amplifiersToCheck.extend(detector.getAmplifiers()) 

403 

404 if amplifier: 

405 amplifiersToCheck.extend(amplifier) 

406 

407 for amp in amplifiersToCheck: 

408 ampName = amp.getName() 

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

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

411 (ampName, )) 

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

413 if self.override: 

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

415 self.linearityType[ampName], ampName) 

416 else: 

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

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

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

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

421 if self.override: 

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

423 self.linearityCoeffs[ampName], ampName) 

424 else: 

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

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

427 

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

429 """Apply the linearity to an image. 

430 

431 If the linearity parameters are populated, use those, 

432 otherwise use the values from the detector. 

433 

434 Parameters 

435 ---------- 

436 image : `~lsst.afw.image.image` 

437 Image to correct. 

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

439 Detector to use for linearity parameters if not already 

440 populated. 

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

442 Log object to use for logging. 

443 """ 

444 if log is None: 

445 log = self.log 

446 if detector and not self.hasLinearity: 

447 self.fromDetector(detector) 

448 

449 self.validate(detector) 

450 

451 numAmps = 0 

452 numLinearized = 0 

453 numOutOfRange = 0 

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

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

456 numAmps += 1 

457 if linearizer is not None: 

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

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

460 'table': self.tableData, 

461 'log': self.log}) 

462 numOutOfRange += outOfRange 

463 if success: 

464 numLinearized += 1 

465 elif log is not None: 

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

467 ampName) 

468 return Struct( 

469 numAmps=numAmps, 

470 numLinearized=numLinearized, 

471 numOutOfRange=numOutOfRange 

472 ) 

473 

474 

475class LinearizeBase(metaclass=abc.ABCMeta): 

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

477 

478 Subclasses must define __call__ and set class variable 

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

480 the cameraGeom.Amplifier.linearityType field. 

481 

482 All linearity corrections should be defined in terms of an 

483 additive correction, such that: 

484 

485 corrected_value = uncorrected_value + f(uncorrected_value) 

486 """ 

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

488 

489 @abc.abstractmethod 

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

491 """Correct non-linearity. 

492 

493 Parameters 

494 ---------- 

495 image : `lsst.afw.image.Image` 

496 Image to be corrected 

497 kwargs : `dict` 

498 Dictionary of parameter keywords: 

499 ``"coeffs"`` 

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

501 ``"table"`` 

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

503 ``"log"`` 

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

505 

506 Returns 

507 ------- 

508 output : `bool` 

509 If true, a correction was applied successfully. 

510 

511 Raises 

512 ------ 

513 RuntimeError: 

514 Raised if the linearity type listed in the 

515 detector does not match the class type. 

516 """ 

517 pass 

518 

519 

520class LinearizeLookupTable(LinearizeBase): 

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

522 

523 The lookup table consists of entries such that given 

524 "coefficients" c0, c1: 

525 

526 for each i,j of image: 

527 rowInd = int(c0) 

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

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

530 

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

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

533 amplifiers use the same table) 

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

535 before truncation; this supports tables that can handle 

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

537 the nearest index is used instead of truncating to the 

538 next smaller index 

539 """ 

540 LinearityType = "LookupTable" 

541 

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

543 """Correct for non-linearity. 

544 

545 Parameters 

546 ---------- 

547 image : `lsst.afw.image.Image` 

548 Image to be corrected 

549 kwargs : `dict` 

550 Dictionary of parameter keywords: 

551 ``"coeffs"`` 

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

553 ``"table"`` 

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

555 ``"log"`` 

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

557 

558 Returns 

559 ------- 

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

561 If true, a correction was applied successfully. The 

562 integer indicates the number of pixels that were 

563 uncorrectable by being out of range. 

564 

565 Raises 

566 ------ 

567 RuntimeError: 

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

569 bounds. 

570 """ 

571 numOutOfRange = 0 

572 

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

574 table = kwargs['table'] 

575 log = kwargs['log'] 

576 

577 numTableRows = table.shape[0] 

578 rowInd = int(rowInd) 

579 if rowInd < 0 or rowInd > numTableRows: 

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

581 (rowInd, numTableRows)) 

582 tableRow = table[rowInd, :] 

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

584 

585 if numOutOfRange > 0 and log is not None: 

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

587 numOutOfRange) 

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

589 return True, numOutOfRange 

590 else: 

591 return False, numOutOfRange 

592 

593 

594class LinearizePolynomial(LinearizeBase): 

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

596 

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

598 

599 where c_i are the linearity coefficients for each amplifier. 

600 Lower order coefficients are not included as they duplicate other 

601 calibration parameters: 

602 ``"k0"`` 

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

604 bias level. Irrelevant for correcting non-linearity. 

605 ``"k1"`` 

606 A coefficient multiplied by uncorrImage**1 is proportional 

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

608 """ 

609 LinearityType = "Polynomial" 

610 

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

612 """Correct non-linearity. 

613 

614 Parameters 

615 ---------- 

616 image : `lsst.afw.image.Image` 

617 Image to be corrected 

618 kwargs : `dict` 

619 Dictionary of parameter keywords: 

620 ``"coeffs"`` 

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

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

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

624 not needed for the correction). 

625 ``"log"`` 

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

627 

628 Returns 

629 ------- 

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

631 If true, a correction was applied successfully. The 

632 integer indicates the number of pixels that were 

633 uncorrectable by being out of range. 

634 """ 

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

636 return False, 0 

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

638 return False, 0 

639 

640 ampArray = image.getArray() 

641 correction = np.zeros_like(ampArray) 

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

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

644 ampArray += correction 

645 

646 return True, 0 

647 

648 

649class LinearizeSquared(LinearizeBase): 

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

651 

652 corrImage = uncorrImage + c0*uncorrImage^2 

653 

654 where c0 is linearity coefficient 0 for each amplifier. 

655 """ 

656 LinearityType = "Squared" 

657 

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

659 """Correct for non-linearity. 

660 

661 Parameters 

662 ---------- 

663 image : `lsst.afw.image.Image` 

664 Image to be corrected 

665 kwargs : `dict` 

666 Dictionary of parameter keywords: 

667 ``"coeffs"`` 

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

669 ``"log"`` 

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

671 

672 Returns 

673 ------- 

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

675 If true, a correction was applied successfully. The 

676 integer indicates the number of pixels that were 

677 uncorrectable by being out of range. 

678 """ 

679 

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

681 if sqCoeff != 0: 

682 ampArr = image.getArray() 

683 ampArr *= (1 + sqCoeff*ampArr) 

684 return True, 0 

685 else: 

686 return False, 0 

687 

688 

689class LinearizeSpline(LinearizeBase): 

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

691 

692 corrImage = uncorrImage - Spline(coeffs, uncorrImage) 

693 

694 Notes 

695 ----- 

696 

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

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

699 to be subtracted from the observed flux. 

700 

701 """ 

702 LinearityType = "Spline" 

703 

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

705 """Correct for non-linearity. 

706 

707 Parameters 

708 ---------- 

709 image : `lsst.afw.image.Image` 

710 Image to be corrected 

711 kwargs : `dict` 

712 Dictionary of parameter keywords: 

713 ``"coeffs"`` 

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

715 ``"log"`` 

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

717 

718 Returns 

719 ------- 

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

721 If true, a correction was applied successfully. The 

722 integer indicates the number of pixels that were 

723 uncorrectable by being out of range. 

724 """ 

725 splineCoeff = kwargs['coeffs'] 

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

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

728 afwMath.stringToInterpStyle("AKIMA_SPLINE")) 

729 

730 ampArr = image.getArray() 

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

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

733 

734 return True, 0 

735 

736 

737class LinearizeProportional(LinearizeBase): 

738 """Do not correct non-linearity. 

739 """ 

740 LinearityType = "Proportional" 

741 

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

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

744 

745 Parameters 

746 ---------- 

747 image : `lsst.afw.image.Image` 

748 Image to be corrected 

749 kwargs : `dict` 

750 Dictionary of parameter keywords: 

751 ``"coeffs"`` 

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

753 ``"log"`` 

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

755 

756 Returns 

757 ------- 

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

759 If true, a correction was applied successfully. The 

760 integer indicates the number of pixels that were 

761 uncorrectable by being out of range. 

762 """ 

763 return True, 0 

764 

765 

766class LinearizeNone(LinearizeBase): 

767 """Do not correct non-linearity. 

768 """ 

769 LinearityType = "None" 

770 

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

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

773 

774 Parameters 

775 ---------- 

776 image : `lsst.afw.image.Image` 

777 Image to be corrected 

778 kwargs : `dict` 

779 Dictionary of parameter keywords: 

780 ``"coeffs"`` 

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

782 ``"log"`` 

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

784 

785 Returns 

786 ------- 

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

788 If true, a correction was applied successfully. The 

789 integer indicates the number of pixels that were 

790 uncorrectable by being out of range. 

791 """ 

792 return True, 0