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

232 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-12 13:11 +0000

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# 

22 

23__all__ = ["Linearizer", 

24 "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared", 

25 "LinearizeProportional", "LinearizePolynomial", "LinearizeSpline", "LinearizeNone"] 

26 

27import abc 

28import numpy as np 

29 

30from astropy.table import Table 

31 

32import lsst.afw.math as afwMath 

33from lsst.pipe.base import Struct 

34from lsst.geom import Box2I, Point2I, Extent2I 

35from .applyLookupTable import applyLookupTable 

36from .calibType import IsrCalib 

37 

38 

39class Linearizer(IsrCalib): 

40 """Parameter set for linearization. 

41 

42 These parameters are included in `lsst.afw.cameraGeom.Amplifier`, but 

43 should be accessible externally to allow for testing. 

44 

45 Parameters 

46 ---------- 

47 table : `numpy.array`, optional 

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

49 

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

51 - one column for each image value 

52 

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

54 (numpy default "C" order) 

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

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

57 log : `logging.Logger`, optional 

58 Logger to handle messages. 

59 kwargs : `dict`, optional 

60 Other keyword arguments to pass to the parent init. 

61 

62 Raises 

63 ------ 

64 RuntimeError 

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

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

67 

68 Notes 

69 ----- 

70 The linearizer attributes stored are: 

71 

72 hasLinearity : `bool` 

73 Whether a linearity correction is defined for this detector. 

74 override : `bool` 

75 Whether the detector parameters should be overridden. 

76 ampNames : `list` [`str`] 

77 List of amplifier names to correct. 

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

79 Coefficients to use in correction. Indexed by amplifier 

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

81 correction to apply. 

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

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

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

85 Bounding box the correction is valid over, indexed by 

86 amplifier names. 

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

88 Linearity fit parameters used to construct the correction 

89 coefficients, indexed as above. 

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

91 Uncertainty values of the linearity fit parameters used to 

92 construct the correction coefficients, indexed as above. 

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

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

95 fitResiduals : `dict` [`str`, `numpy.array`], optional 

96 Residuals of the fit, indexed as above. Used for 

97 calculating photdiode corrections 

98 linearFit : The linear fit to the low flux region of the curve. 

99 [intercept, slope]. 

100 tableData : `numpy.array`, optional 

101 Lookup table data for the linearity correction. 

102 """ 

103 _OBSTYPE = "LINEARIZER" 

104 _SCHEMA = 'Gen3 Linearizer' 

105 _VERSION = 1.1 

106 

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

108 self.hasLinearity = False 

109 self.override = False 

110 

111 self.ampNames = list() 

112 self.linearityCoeffs = dict() 

113 self.linearityType = dict() 

114 self.linearityBBox = dict() 

115 self.fitParams = dict() 

116 self.fitParamsErr = dict() 

117 self.fitChiSq = dict() 

118 self.fitResiduals = dict() 

119 self.linearFit = dict() 

120 self.tableData = None 

121 if table is not None: 

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

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

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

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

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

127 

128 super().__init__(**kwargs) 

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

130 'ampNames', 

131 'linearityCoeffs', 'linearityType', 'linearityBBox', 

132 'fitParams', 'fitParamsErr', 'fitChiSq', 

133 'fitResiduals', 'linearFit', 'tableData']) 

134 

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

136 """Update metadata keywords with new values. 

137 

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

139 calibration keywords will be saved. 

140 

141 Parameters 

142 ---------- 

143 setDate : `bool`, optional 

144 Update the CALIBDATE fields in the metadata to the current 

145 time. Defaults to False. 

146 kwargs : 

147 Other keyword parameters to set in the metadata. 

148 """ 

149 kwargs['HAS_LINEARITY'] = self.hasLinearity 

150 kwargs['OVERRIDE'] = self.override 

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

152 

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

154 

155 def fromDetector(self, detector): 

156 """Read linearity parameters from a detector. 

157 

158 Parameters 

159 ---------- 

160 detector : `lsst.afw.cameraGeom.detector` 

161 Input detector with parameters to use. 

162 

163 Returns 

164 ------- 

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

166 The calibration constructed from the detector. 

167 """ 

168 self._detectorName = detector.getName() 

169 self._detectorSerial = detector.getSerial() 

170 self._detectorId = detector.getId() 

171 self.hasLinearity = True 

172 

173 # Do not translate Threshold, Maximum, Units. 

174 for amp in detector.getAmplifiers(): 

175 ampName = amp.getName() 

176 self.ampNames.append(ampName) 

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

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

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

180 

181 return self 

182 

183 @classmethod 

184 def fromDict(cls, dictionary): 

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

186 

187 Parameters 

188 ---------- 

189 dictionary : `dict` 

190 Dictionary of properties 

191 

192 Returns 

193 ------- 

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

195 Constructed calibration. 

196 

197 Raises 

198 ------ 

199 RuntimeError 

200 Raised if the supplied dictionary is for a different 

201 calibration. 

202 """ 

203 

204 calib = cls() 

205 

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

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

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

209 

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

211 

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

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

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

215 

216 if calib.hasLinearity: 

217 for ampName in dictionary['amplifiers']: 

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

219 calib.ampNames.append(ampName) 

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

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

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

223 

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

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

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

227 calib.fitResiduals[ampName] = np.array(amp.get('fitResiduals', [0.0])) 

228 calib.linearFit[ampName] = np.array(amp.get('linearFit', [0.0])) 

229 

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

231 if calib.tableData: 

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

233 

234 return calib 

235 

236 def toDict(self): 

237 """Return linearity parameters as a dict. 

238 

239 Returns 

240 ------- 

241 outDict : `dict`: 

242 """ 

243 self.updateMetadata() 

244 

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

246 'detectorName': self._detectorName, 

247 'detectorSerial': self._detectorSerial, 

248 'detectorId': self._detectorId, 

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

250 'amplifiers': dict(), 

251 } 

252 for ampName in self.linearityType: 

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

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

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

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

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

258 'fitChiSq': self.fitChiSq[ampName], 

259 'fitResiduals': self.fitResiduals[ampName].tolist(), 

260 'linearFit': self.linearFit[ampName].tolist()} 

261 if self.tableData is not None: 

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

263 

264 return outDict 

265 

266 @classmethod 

267 def fromTable(cls, tableList): 

268 """Read linearity from a FITS file. 

269 

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

271 calibration, after constructing an appropriate dictionary from 

272 the input tables. 

273 

274 Parameters 

275 ---------- 

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

277 afwTable read from input file name. 

278 

279 Returns 

280 ------- 

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

282 Linearity parameters. 

283 

284 Notes 

285 ----- 

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

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

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

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

290 dictionary by looping over rows. 

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

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

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

294 """ 

295 coeffTable = tableList[0] 

296 

297 metadata = coeffTable.meta 

298 inDict = dict() 

299 inDict['metadata'] = metadata 

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

301 inDict['amplifiers'] = dict() 

302 

303 for record in coeffTable: 

304 ampName = record['AMPLIFIER_NAME'] 

305 

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

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

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

309 fitResiduals = record['FIT_RES'] if 'FIT_RES' in record.columns else np.array([0.0]) 

310 linearFit = record['LIN_FIT'] if 'LIN_FIT' in record.columns else np.array([0.0]) 

311 

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

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

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

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

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

317 'fitParams': fitParams, 

318 'fitParamsErr': fitParamsErr, 

319 'fitChiSq': fitChiSq, 

320 'fitResiduals': fitResiduals, 

321 'linearFit': linearFit, 

322 } 

323 

324 if len(tableList) > 1: 

325 tableData = tableList[1] 

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

327 

328 return cls().fromDict(inDict) 

329 

330 def toTable(self): 

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

332 calibration. 

333 

334 The list of tables should create an identical calibration 

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

336 

337 Returns 

338 ------- 

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

340 List of tables containing the linearity calibration 

341 information. 

342 """ 

343 

344 tableList = [] 

345 self.updateMetadata() 

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

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

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

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

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

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

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

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

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

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

356 'FIT_RES': self.fitResiduals[ampName], 

357 'LIN_FIT': self.linearFit[ampName], 

358 } for ampName in self.ampNames]) 

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

360 tableList.append(catalog) 

361 

362 if self.tableData is not None: 

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

364 tableList.append(catalog) 

365 return tableList 

366 

367 def getLinearityTypeByName(self, linearityTypeName): 

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

369 

370 Parameters 

371 ---------- 

372 linearityTypeName : str 

373 String name of the linearity type that is needed. 

374 

375 Returns 

376 ------- 

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

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

379 is found, `None` is returned. 

380 """ 

381 for t in [LinearizeLookupTable, 

382 LinearizeSquared, 

383 LinearizePolynomial, 

384 LinearizeProportional, 

385 LinearizeSpline, 

386 LinearizeNone]: 

387 if t.LinearityType == linearityTypeName: 

388 return t 

389 return None 

390 

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

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

393 

394 Parameters 

395 ---------- 

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

397 Detector to validate, along with its amplifiers. 

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

399 Single amplifier to validate. 

400 

401 Raises 

402 ------ 

403 RuntimeError 

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

405 the cameraGeom parameters are not being overridden. 

406 """ 

407 amplifiersToCheck = [] 

408 if detector: 

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

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

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

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

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

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

415 # TODO: DM-38778: This check fails on LATISS due to an 

416 # error in the camera configuration. 

417 # if self._detectorSerial != detector.getSerial(): 

418 # raise RuntimeError( 

419 # "Detector serial numbers don't match: %s != %s" % 

420 # (self._detectorSerial, detector.getSerial())) 

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

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

423 (len(detector.getAmplifiers()), 

424 len(self.linearityCoeffs.keys()))) 

425 amplifiersToCheck.extend(detector.getAmplifiers()) 

426 

427 if amplifier: 

428 amplifiersToCheck.extend(amplifier) 

429 

430 for amp in amplifiersToCheck: 

431 ampName = amp.getName() 

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

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

434 (ampName, )) 

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

436 if self.override: 

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

438 self.linearityType[ampName], ampName) 

439 else: 

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

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

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

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

444 if self.override: 

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

446 self.linearityCoeffs[ampName], ampName) 

447 else: 

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

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

450 

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

452 """Apply the linearity to an image. 

453 

454 If the linearity parameters are populated, use those, 

455 otherwise use the values from the detector. 

456 

457 Parameters 

458 ---------- 

459 image : `~lsst.afw.image.image` 

460 Image to correct. 

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

462 Detector to use for linearity parameters if not already 

463 populated. 

464 log : `~logging.Logger`, optional 

465 Log object to use for logging. 

466 """ 

467 if log is None: 

468 log = self.log 

469 if detector and not self.hasLinearity: 

470 self.fromDetector(detector) 

471 

472 self.validate(detector) 

473 

474 numAmps = 0 

475 numLinearized = 0 

476 numOutOfRange = 0 

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

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

479 numAmps += 1 

480 if linearizer is not None: 

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

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

483 'table': self.tableData, 

484 'log': self.log}) 

485 numOutOfRange += outOfRange 

486 if success: 

487 numLinearized += 1 

488 elif log is not None: 

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

490 ampName) 

491 return Struct( 

492 numAmps=numAmps, 

493 numLinearized=numLinearized, 

494 numOutOfRange=numOutOfRange 

495 ) 

496 

497 

498class LinearizeBase(metaclass=abc.ABCMeta): 

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

500 

501 Subclasses must define ``__call__`` and set class variable 

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

503 the cameraGeom.Amplifier.linearityType field. 

504 

505 All linearity corrections should be defined in terms of an 

506 additive correction, such that: 

507 

508 corrected_value = uncorrected_value + f(uncorrected_value) 

509 """ 

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

511 

512 @abc.abstractmethod 

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

514 """Correct non-linearity. 

515 

516 Parameters 

517 ---------- 

518 image : `lsst.afw.image.Image` 

519 Image to be corrected 

520 kwargs : `dict` 

521 Dictionary of parameter keywords: 

522 

523 ``coeffs`` 

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

525 ``table`` 

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

527 ``log`` 

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

529 

530 Returns 

531 ------- 

532 output : `bool` 

533 If `True`, a correction was applied successfully. 

534 

535 Raises 

536 ------ 

537 RuntimeError: 

538 Raised if the linearity type listed in the 

539 detector does not match the class type. 

540 """ 

541 pass 

542 

543 

544class LinearizeLookupTable(LinearizeBase): 

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

546 

547 The lookup table consists of entries such that given 

548 "coefficients" c0, c1: 

549 

550 for each i,j of image: 

551 rowInd = int(c0) 

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

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

554 

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

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

557 amplifiers use the same table) 

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

559 before truncation; this supports tables that can handle 

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

561 the nearest index is used instead of truncating to the 

562 next smaller index 

563 """ 

564 LinearityType = "LookupTable" 

565 

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

567 """Correct for non-linearity. 

568 

569 Parameters 

570 ---------- 

571 image : `lsst.afw.image.Image` 

572 Image to be corrected 

573 kwargs : `dict` 

574 Dictionary of parameter keywords: 

575 

576 ``coeffs`` 

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

578 ``table`` 

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

580 ``log`` 

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

582 

583 Returns 

584 ------- 

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

586 If true, a correction was applied successfully. The 

587 integer indicates the number of pixels that were 

588 uncorrectable by being out of range. 

589 

590 Raises 

591 ------ 

592 RuntimeError: 

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

594 bounds. 

595 """ 

596 numOutOfRange = 0 

597 

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

599 table = kwargs['table'] 

600 log = kwargs['log'] 

601 

602 numTableRows = table.shape[0] 

603 rowInd = int(rowInd) 

604 if rowInd < 0 or rowInd > numTableRows: 

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

606 (rowInd, numTableRows)) 

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

608 

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

610 

611 if numOutOfRange > 0 and log is not None: 

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

613 numOutOfRange) 

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

615 return True, numOutOfRange 

616 else: 

617 return False, numOutOfRange 

618 

619 

620class LinearizePolynomial(LinearizeBase): 

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

622 

623 .. code-block:: 

624 

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

626 

627 where ``c_i`` are the linearity coefficients for each amplifier. 

628 Lower order coefficients are not included as they duplicate other 

629 calibration parameters: 

630 

631 ``k0`` 

632 A coefficient multiplied by ``uncorrImage**0`` is equivalent to 

633 bias level. Irrelevant for correcting non-linearity. 

634 ``k1`` 

635 A coefficient multiplied by ``uncorrImage**1`` is proportional 

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

637 """ 

638 LinearityType = "Polynomial" 

639 

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

641 """Correct non-linearity. 

642 

643 Parameters 

644 ---------- 

645 image : `lsst.afw.image.Image` 

646 Image to be corrected 

647 kwargs : `dict` 

648 Dictionary of parameter keywords: 

649 

650 ``coeffs`` 

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

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

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

654 not needed for the correction). 

655 ``log`` 

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

657 

658 Returns 

659 ------- 

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

661 If true, a correction was applied successfully. The 

662 integer indicates the number of pixels that were 

663 uncorrectable by being out of range. 

664 """ 

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

666 return False, 0 

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

668 return False, 0 

669 

670 ampArray = image.getArray() 

671 correction = np.zeros_like(ampArray) 

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

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

674 ampArray += correction 

675 

676 return True, 0 

677 

678 

679class LinearizeSquared(LinearizeBase): 

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

681 

682 corrImage = uncorrImage + c0*uncorrImage^2 

683 

684 where c0 is linearity coefficient 0 for each amplifier. 

685 """ 

686 LinearityType = "Squared" 

687 

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

689 """Correct for non-linearity. 

690 

691 Parameters 

692 ---------- 

693 image : `lsst.afw.image.Image` 

694 Image to be corrected 

695 kwargs : `dict` 

696 Dictionary of parameter keywords: 

697 

698 ``coeffs`` 

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

700 ``log`` 

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

702 

703 Returns 

704 ------- 

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

706 If true, a correction was applied successfully. The 

707 integer indicates the number of pixels that were 

708 uncorrectable by being out of range. 

709 """ 

710 

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

712 if sqCoeff != 0: 

713 ampArr = image.getArray() 

714 ampArr *= (1 + sqCoeff*ampArr) 

715 return True, 0 

716 else: 

717 return False, 0 

718 

719 

720class LinearizeSpline(LinearizeBase): 

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

722 

723 corrImage = uncorrImage - Spline(coeffs, uncorrImage) 

724 

725 Notes 

726 ----- 

727 

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

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

730 to be subtracted from the observed flux. 

731 

732 """ 

733 LinearityType = "Spline" 

734 

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

736 """Correct for non-linearity. 

737 

738 Parameters 

739 ---------- 

740 image : `lsst.afw.image.Image` 

741 Image to be corrected 

742 kwargs : `dict` 

743 Dictionary of parameter keywords: 

744 

745 ``coeffs`` 

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

747 ``log`` 

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

749 

750 Returns 

751 ------- 

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

753 If true, a correction was applied successfully. The 

754 integer indicates the number of pixels that were 

755 uncorrectable by being out of range. 

756 """ 

757 splineCoeff = kwargs['coeffs'] 

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

759 # If the spline is not anchored at zero, remove the offset 

760 # found at the lowest flux available, and add an anchor at 

761 # flux=0.0 if there's no entry at that point. 

762 if values[0] != 0: 

763 offset = values[0] 

764 values -= offset 

765 if centers[0] != 0.0: 

766 centers = np.concatenate(([0.0], centers)) 

767 values = np.concatenate(([0.0], values)) 

768 

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

770 afwMath.stringToInterpStyle("AKIMA_SPLINE")) 

771 

772 ampArr = image.getArray() 

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

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

775 

776 return True, 0 

777 

778 

779class LinearizeProportional(LinearizeBase): 

780 """Do not correct non-linearity. 

781 """ 

782 LinearityType = "Proportional" 

783 

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

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

786 

787 Parameters 

788 ---------- 

789 image : `lsst.afw.image.Image` 

790 Image to be corrected 

791 kwargs : `dict` 

792 Dictionary of parameter keywords: 

793 

794 ``coeffs`` 

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

796 ``log`` 

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

798 

799 Returns 

800 ------- 

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

802 If true, a correction was applied successfully. The 

803 integer indicates the number of pixels that were 

804 uncorrectable by being out of range. 

805 """ 

806 return True, 0 

807 

808 

809class LinearizeNone(LinearizeBase): 

810 """Do not correct non-linearity. 

811 """ 

812 LinearityType = "None" 

813 

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

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

816 

817 Parameters 

818 ---------- 

819 image : `lsst.afw.image.Image` 

820 Image to be corrected 

821 kwargs : `dict` 

822 Dictionary of parameter keywords: 

823 

824 ``coeffs`` 

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

826 ``log`` 

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

828 

829 Returns 

830 ------- 

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

832 If true, a correction was applied successfully. The 

833 integer indicates the number of pixels that were 

834 uncorrectable by being out of range. 

835 """ 

836 return True, 0