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 copy 

24import datetime 

25 

26import numpy as np 

27import yaml 

28 

29import lsst.afw.table as afwTable 

30from lsst.daf.base import PropertyList 

31from lsst.pipe.base import Struct 

32from lsst.geom import Box2I, Point2I, Extent2I 

33from .applyLookupTable import applyLookupTable 

34 

35__all__ = ["Linearizer", 

36 "LinearizeBase", "LinearizeLookupTable", "LinearizeSquared", 

37 "LinearizeProportional", "LinearizePolynomial", "LinearizeNone"] 

38 

39 

40class Linearizer(abc.ABC): 

41 """Parameter set for linearization. 

42 

43 These parameters are included in cameraGeom.Amplifier, but 

44 should be accessible externally to allow for testing. 

45 

46 Parameters 

47 ---------- 

48 table : `numpy.array`, optional 

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

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

51 - one column for each image value 

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

53 (numpy default "C" order) 

54 detector : `lsst.afw.cameraGeom.Detector` 

55 Detector object 

56 override : `bool`, optional 

57 Override the parameters defined in the detector/amplifier. 

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

59 Logger to handle messages. 

60 

61 Raises 

62 ------ 

63 RuntimeError : 

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

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

66 """ 

67 

68 _OBSTYPE = "linearizer" 

69 """The dataset type name used for this class""" 

70 

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

72 self._detectorName = None 

73 self._detectorSerial = None 

74 self._detectorId = None 

75 self._metadata = PropertyList() 

76 

77 self.linearityCoeffs = dict() 

78 self.linearityType = dict() 

79 self.linearityThreshold = dict() 

80 self.linearityMaximum = dict() 

81 self.linearityUnits = dict() 

82 self.linearityBBox = dict() 

83 

84 self.override = override 

85 self.populated = False 

86 self.log = log 

87 

88 self.tableData = None 

89 if table is not None: 

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

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

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

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

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

95 

96 if detector: 

97 self.fromDetector(detector) 

98 

99 def __call__(self, exposure): 

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

101 

102 Parameters 

103 ---------- 

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

105 Exposure to correct. 

106 

107 Returns 

108 ------- 

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

110 Linearization results: 

111 ``"numAmps"`` 

112 Number of amplifiers considered. 

113 ``"numLinearized"`` 

114 Number of amplifiers linearized. 

115 """ 

116 

117 def fromDetector(self, detector): 

118 """Read linearity parameters from a detector. 

119 

120 Parameters 

121 ---------- 

122 detector : `lsst.afw.cameraGeom.detector` 

123 Input detector with parameters to use. 

124 """ 

125 self._detectorName = detector.getName() 

126 self._detectorSerial = detector.getSerial() 

127 self._detectorId = detector.getId() 

128 self.populated = True 

129 

130 # Do not translate Threshold, Maximum, Units. 

131 for amp in detector.getAmplifiers(): 

132 ampName = amp.getName() 

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

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

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

136 

137 def fromYaml(self, yamlObject): 

138 """Read linearity parameters from a dict. 

139 

140 Parameters 

141 ---------- 

142 yamlObject : `dict` 

143 Dictionary containing detector and amplifier information. 

144 """ 

145 self.setMetadata(metadata=yamlObject.get('metadata', None)) 

146 self._detectorName = yamlObject['detectorName'] 

147 self._detectorSerial = yamlObject['detectorSerial'] 

148 self._detectorId = yamlObject['detectorId'] 

149 self.populated = True 

150 self.override = True 

151 

152 for ampName in yamlObject['amplifiers']: 

153 amp = yamlObject['amplifiers'][ampName] 

154 self.linearityCoeffs[ampName] = np.array(amp.get('linearityCoeffs', None), dtype=np.float64) 

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

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

157 

158 if self.tableData is None: 

159 self.tableData = yamlObject.get('tableData', None) 

160 if self.tableData: 

161 self.tableData = np.array(self.tableData) 

162 

163 return self 

164 

165 def toDict(self): 

166 """Return linearity parameters as a dict. 

167 

168 Returns 

169 ------- 

170 outDict : `dict`: 

171 """ 

172 # metadata copied from defects code 

173 now = datetime.datetime.utcnow() 

174 self.updateMetadata(date=now) 

175 

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

177 'detectorName': self._detectorName, 

178 'detectorSerial': self._detectorSerial, 

179 'detectorId': self._detectorId, 

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

181 'amplifiers': dict()} 

182 for ampName in self.linearityType: 

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

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

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

186 if self.tableData is not None: 

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

188 

189 return outDict 

190 

191 @classmethod 

192 def readText(cls, filename): 

193 """Read linearity from text file. 

194 

195 Parameters 

196 ---------- 

197 filename : `str` 

198 Name of the file containing the linearity definition. 

199 Returns 

200 ------- 

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

202 Linearity parameters. 

203 """ 

204 data = '' 

205 with open(filename, 'r') as f: 

206 data = yaml.load(f, Loader=yaml.CLoader) 

207 return cls().fromYaml(data) 

208 

209 def writeText(self, filename): 

210 """Write the linearity model to a text file. 

211 

212 Parameters 

213 ---------- 

214 filename : `str` 

215 Name of the file to write. 

216 

217 Returns 

218 ------- 

219 used : `str` 

220 The name of the file used to write the data. 

221 

222 Raises 

223 ------ 

224 RuntimeError : 

225 Raised if filename does not end in ".yaml". 

226 

227 Notes 

228 ----- 

229 The file is written to YAML format and will include any metadata 

230 associated with the `Linearity`. 

231 """ 

232 outDict = self.toDict() 

233 if filename.lower().endswith((".yaml")): 

234 with open(filename, 'w') as f: 

235 yaml.dump(outDict, f) 

236 else: 

237 raise RuntimeError(f"Attempt to write to a file {filename} that does not end in '.yaml'") 

238 

239 return filename 

240 

241 @classmethod 

242 def fromTable(cls, table, tableExtTwo=None): 

243 """Read linearity from a FITS file. 

244 

245 Parameters 

246 ---------- 

247 table : `lsst.afw.table` 

248 afwTable read from input file name. 

249 tableExtTwo: `lsst.afw.table`, optional 

250 afwTable read from second extension of input file name 

251 

252 Returns 

253 ------- 

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

255 Linearity parameters. 

256 

257 Notes 

258 ----- 

259 The method reads a FITS file with 1 or 2 extensions. The metadata is read from the header of 

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

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

262 set each dictionary by looping over rows. 

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

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

265 """ 

266 metadata = table.getMetadata() 

267 schema = table.getSchema() 

268 

269 linDict = dict() 

270 linDict['metadata'] = metadata 

271 linDict['detectorId'] = metadata['DETECTOR'] 

272 linDict['detectorName'] = metadata['DETECTOR_NAME'] 

273 try: 

274 linDict['detectorSerial'] = metadata['DETECTOR_SERIAL'] 

275 except Exception: 

276 linDict['detectorSerial'] = 'NOT SET' 

277 linDict['amplifiers'] = dict() 

278 

279 # Preselect the keys 

280 ampNameKey = schema['AMPLIFIER_NAME'].asKey() 

281 typeKey = schema['TYPE'].asKey() 

282 coeffsKey = schema['COEFFS'].asKey() 

283 x0Key = schema['BBOX_X0'].asKey() 

284 y0Key = schema['BBOX_Y0'].asKey() 

285 dxKey = schema['BBOX_DX'].asKey() 

286 dyKey = schema['BBOX_DY'].asKey() 

287 

288 for record in table: 

289 ampName = record[ampNameKey] 

290 ampDict = dict() 

291 ampDict['linearityType'] = record[typeKey] 

292 ampDict['linearityCoeffs'] = record[coeffsKey] 

293 ampDict['linearityBBox'] = Box2I(Point2I(record[x0Key], record[y0Key]), 

294 Extent2I(record[dxKey], record[dyKey])) 

295 

296 linDict['amplifiers'][ampName] = ampDict 

297 

298 if tableExtTwo is not None: 

299 lookupValuesKey = 'LOOKUP_VALUES' 

300 linDict["tableData"] = [record[lookupValuesKey] for record in tableExtTwo] 

301 

302 return cls().fromYaml(linDict) 

303 

304 @classmethod 

305 def readFits(cls, filename): 

306 """Read linearity from a FITS file. 

307 

308 Parameters 

309 ---------- 

310 filename : `str` 

311 Name of the file containing the linearity definition. 

312 Returns 

313 ------- 

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

315 Linearity parameters. 

316 

317 Notes 

318 ----- 

319 This method and `fromTable` read a FITS file with 1 or 2 extensions. The metadata is read from the 

320 header of extension 1, which must exist. Then the table is loaded, and the ['AMPLIFIER_NAME', 

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

322 set each dictionary by looping over rows. 

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

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

325 """ 

326 table = afwTable.BaseCatalog.readFits(filename) 

327 tableExtTwo = None 

328 try: 

329 tableExtTwo = afwTable.BaseCatalog.readFits(filename, 2) 

330 except Exception: 

331 pass 

332 return cls().fromTable(table, tableExtTwo=tableExtTwo) 

333 

334 def toAmpTable(self, metadata): 

335 """Produce linearity catalog 

336 

337 Parameters 

338 ---------- 

339 metadata : `lsst.daf.base.PropertyList` 

340 Linearizer metadata 

341 

342 Returns 

343 ------- 

344 catalog : `lsst.afw.table.BaseCatalog` 

345 Catalog to write 

346 """ 

347 metadata["LINEARITY_SCHEMA"] = "Linearity table" 

348 metadata["LINEARITY_VERSION"] = 1 

349 

350 # Now pack it into a fits table. 

351 length = max([len(self.linearityCoeffs[x]) for x in self.linearityCoeffs.keys()]) 

352 

353 schema = afwTable.Schema() 

354 names = schema.addField("AMPLIFIER_NAME", type="String", size=16, doc="linearity amplifier name") 

355 types = schema.addField("TYPE", type="String", size=16, doc="linearity type names") 

356 coeffs = schema.addField("COEFFS", type="ArrayD", size=length, doc="linearity coefficients") 

357 boxX = schema.addField("BBOX_X0", type="I", doc="linearity bbox minimum x") 

358 boxY = schema.addField("BBOX_Y0", type="I", doc="linearity bbox minimum y") 

359 boxDx = schema.addField("BBOX_DX", type="I", doc="linearity bbox x dimension") 

360 boxDy = schema.addField("BBOX_DY", type="I", doc="linearity bbox y dimension") 

361 

362 catalog = afwTable.BaseCatalog(schema) 

363 catalog.resize(len(self.linearityCoeffs.keys())) 

364 

365 for ii, ampName in enumerate(self.linearityType): 

366 catalog[ii][names] = ampName 

367 catalog[ii][types] = self.linearityType[ampName] 

368 catalog[ii][coeffs] = np.array(self.linearityCoeffs[ampName], dtype=float) 

369 

370 bbox = self.linearityBBox[ampName] 

371 catalog[ii][boxX], catalog[ii][boxY] = bbox.getMin() 

372 catalog[ii][boxDx], catalog[ii][boxDy] = bbox.getDimensions() 

373 catalog.setMetadata(metadata) 

374 

375 return catalog 

376 

377 def toTableDataTable(self, metadata): 

378 """Produce linearity catalog from table data 

379 

380 Parameters 

381 ---------- 

382 metadata : `lsst.daf.base.PropertyList` 

383 Linearizer metadata 

384 

385 Returns 

386 ------- 

387 catalog : `lsst.afw.table.BaseCatalog` 

388 Catalog to write 

389 """ 

390 

391 schema = afwTable.Schema() 

392 dimensions = self.tableData.shape 

393 lut = schema.addField("LOOKUP_VALUES", type='ArrayF', size=dimensions[1], 

394 doc="linearity lookup data") 

395 catalog = afwTable.BaseCatalog(schema) 

396 catalog.resize(dimensions[0]) 

397 

398 for ii in range(dimensions[0]): 

399 catalog[ii][lut] = np.array(self.tableData[ii], dtype=np.float32) 

400 

401 metadata["LINEARITY_LOOKUP"] = True 

402 catalog.setMetadata(metadata) 

403 

404 return catalog 

405 

406 def writeFits(self, filename): 

407 """Write the linearity model to a FITS file. 

408 

409 Parameters 

410 ---------- 

411 filename : `str` 

412 Name of the file to write. 

413 

414 Notes 

415 ----- 

416 The file is written to YAML format and will include any metadata 

417 associated with the `Linearity`. 

418 """ 

419 now = datetime.datetime.utcnow() 

420 self.updateMetadata(date=now) 

421 metadata = copy.copy(self.getMetadata()) 

422 catalog = self.toAmpTable(metadata) 

423 catalog.writeFits(filename) 

424 

425 if self.tableData is not None: 

426 catalog = self.toTableDataTable(metadata) 

427 catalog.writeFits(filename, "a") 

428 

429 return 

430 

431 def getMetadata(self): 

432 """Retrieve metadata associated with this `Linearizer`. 

433 

434 Returns 

435 ------- 

436 meta : `lsst.daf.base.PropertyList` 

437 Metadata. The returned `~lsst.daf.base.PropertyList` can be 

438 modified by the caller and the changes will be written to 

439 external files. 

440 """ 

441 return self._metadata 

442 

443 def setMetadata(self, metadata=None): 

444 """Store a copy of the supplied metadata with the `Linearizer`. 

445 

446 Parameters 

447 ---------- 

448 metadata : `lsst.daf.base.PropertyList`, optional 

449 Metadata to associate with the linearizer. Will be copied and 

450 overwrite existing metadata. If not supplied the existing 

451 metadata will be reset. 

452 """ 

453 if metadata is None: 

454 self._metadata = PropertyList() 

455 else: 

456 self._metadata = copy.copy(metadata) 

457 

458 # Ensure that we have the obs type required by calibration ingest 

459 self._metadata["OBSTYPE"] = self._OBSTYPE 

460 

461 def updateMetadata(self, date=None, detectorId=None, detectorName=None, instrumentName=None, calibId=None, 

462 serial=None): 

463 """Update metadata keywords with new values. 

464 

465 Parameters 

466 ---------- 

467 date : `datetime.datetime`, optional 

468 detectorId : `int`, optional 

469 detectorName: `str`, optional 

470 instrumentName : `str`, optional 

471 calibId: `str`, optional 

472 serial: detector serial, `str`, optional 

473 

474 """ 

475 mdOriginal = self.getMetadata() 

476 mdSupplemental = dict() 

477 

478 if date: 

479 mdSupplemental['CALIBDATE'] = date.isoformat() 

480 mdSupplemental['CALIB_CREATION_DATE'] = date.date().isoformat(), 

481 mdSupplemental['CALIB_CREATION_TIME'] = date.time().isoformat(), 

482 if detectorId: 

483 mdSupplemental['DETECTOR'] = f"{detectorId}" 

484 if detectorName: 

485 mdSupplemental['DETECTOR_NAME'] = detectorName 

486 if instrumentName: 

487 mdSupplemental['INSTRUME'] = instrumentName 

488 if calibId: 

489 mdSupplemental['CALIB_ID'] = calibId 

490 if serial: 

491 mdSupplemental['DETECTOR_SERIAL'] = serial 

492 

493 mdOriginal.update(mdSupplemental) 

494 

495 def getLinearityTypeByName(self, linearityTypeName): 

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

497 

498 Parameters 

499 ---------- 

500 linearityTypeName : str 

501 String name of the linearity type that is needed. 

502 

503 Returns 

504 ------- 

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

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

507 is found, `None` is returned. 

508 """ 

509 for t in [LinearizeLookupTable, 

510 LinearizeSquared, 

511 LinearizePolynomial, 

512 LinearizeProportional, 

513 LinearizeNone]: 

514 if t.LinearityType == linearityTypeName: 

515 return t 

516 return None 

517 

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

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

520 

521 Parameters 

522 ---------- 

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

524 Detector to validate, along with its amplifiers. 

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

526 Single amplifier to validate. 

527 

528 Raises 

529 ------ 

530 RuntimeError : 

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

532 the cameraGeom parameters are not being overridden. 

533 """ 

534 amplifiersToCheck = [] 

535 if detector: 

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

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

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

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

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

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

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

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

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

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

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

547 (len(detector.getAmplifiers()), 

548 len(self.linearityCoeffs.keys()))) 

549 amplifiersToCheck.extend(detector.getAmplifiers()) 

550 

551 if amplifier: 

552 amplifiersToCheck.extend(amplifier) 

553 

554 for amp in amplifiersToCheck: 

555 ampName = amp.getName() 

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

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

558 (ampName, )) 

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

560 if self.override: 

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

562 self.linearityType[ampName], ampName) 

563 else: 

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

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

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

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

568 if self.override: 

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

570 self.linearityCoeffs[ampName], ampName) 

571 else: 

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

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

574 

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

576 """Apply the linearity to an image. 

577 

578 If the linearity parameters are populated, use those, 

579 otherwise use the values from the detector. 

580 

581 Parameters 

582 ---------- 

583 image : `~lsst.afw.image.image` 

584 Image to correct. 

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

586 Detector to use for linearity parameters if not already 

587 populated. 

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

589 Log object to use for logging. 

590 """ 

591 if log is None: 

592 log = self.log 

593 if detector and not self.populated: 

594 self.fromDetector(detector) 

595 

596 self.validate(detector) 

597 

598 numAmps = 0 

599 numLinearized = 0 

600 numOutOfRange = 0 

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

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

603 numAmps += 1 

604 if linearizer is not None: 

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

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

607 'table': self.tableData, 

608 'log': self.log}) 

609 numOutOfRange += outOfRange 

610 if success: 

611 numLinearized += 1 

612 elif log is not None: 

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

614 ampName) 

615 return Struct( 

616 numAmps=numAmps, 

617 numLinearized=numLinearized, 

618 numOutOfRange=numOutOfRange 

619 ) 

620 

621 

622class LinearizeBase(metaclass=abc.ABCMeta): 

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

624 

625 Subclasses must define __call__ and set class variable 

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

627 the cameraGeom.Amplifier.linearityType field. 

628 

629 All linearity corrections should be defined in terms of an 

630 additive correction, such that: 

631 

632 corrected_value = uncorrected_value + f(uncorrected_value) 

633 """ 

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

635 

636 @abc.abstractmethod 

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

638 """Correct non-linearity. 

639 

640 Parameters 

641 ---------- 

642 image : `lsst.afw.image.Image` 

643 Image to be corrected 

644 kwargs : `dict` 

645 Dictionary of parameter keywords: 

646 ``"coeffs"`` 

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

648 ``"table"`` 

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

650 ``"log"`` 

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

652 

653 Returns 

654 ------- 

655 output : `bool` 

656 If true, a correction was applied successfully. 

657 

658 Raises 

659 ------ 

660 RuntimeError: 

661 Raised if the linearity type listed in the 

662 detector does not match the class type. 

663 """ 

664 pass 

665 

666 

667class LinearizeLookupTable(LinearizeBase): 

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

669 

670 The lookup table consists of entries such that given 

671 "coefficients" c0, c1: 

672 

673 for each i,j of image: 

674 rowInd = int(c0) 

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

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

677 

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

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

680 amplifiers use the same table) 

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

682 before truncation; this supports tables that can handle 

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

684 the nearest index is used instead of truncating to the 

685 next smaller index 

686 """ 

687 LinearityType = "LookupTable" 

688 

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

690 """Correct for non-linearity. 

691 

692 Parameters 

693 ---------- 

694 image : `lsst.afw.image.Image` 

695 Image to be corrected 

696 kwargs : `dict` 

697 Dictionary of parameter keywords: 

698 ``"coeffs"`` 

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

700 ``"table"`` 

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

702 ``"log"`` 

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

704 

705 Returns 

706 ------- 

707 output : `bool` 

708 If true, a correction was applied successfully. 

709 

710 Raises 

711 ------ 

712 RuntimeError: 

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

714 bounds. 

715 """ 

716 numOutOfRange = 0 

717 

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

719 table = kwargs['table'] 

720 log = kwargs['log'] 

721 

722 numTableRows = table.shape[0] 

723 rowInd = int(rowInd) 

724 if rowInd < 0 or rowInd > numTableRows: 

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

726 (rowInd, numTableRows)) 

727 tableRow = table[rowInd, :] 

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

729 

730 if numOutOfRange > 0 and log is not None: 

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

732 numOutOfRange) 

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

734 return True, numOutOfRange 

735 else: 

736 return False, numOutOfRange 

737 

738 

739class LinearizePolynomial(LinearizeBase): 

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

741 

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

743 

744 where c_i are the linearity coefficients for each amplifier. 

745 Lower order coefficients are not included as they duplicate other 

746 calibration parameters: 

747 ``"k0"`` 

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

749 bias level. Irrelevant for correcting non-linearity. 

750 ``"k1"`` 

751 A coefficient multiplied by uncorrImage**1 is proportional 

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

753 """ 

754 LinearityType = "Polynomial" 

755 

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

757 """Correct non-linearity. 

758 

759 Parameters 

760 ---------- 

761 image : `lsst.afw.image.Image` 

762 Image to be corrected 

763 kwargs : `dict` 

764 Dictionary of parameter keywords: 

765 ``"coeffs"`` 

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

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

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

769 not needed for the correction). 

770 ``"log"`` 

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

772 

773 Returns 

774 ------- 

775 output : `bool` 

776 If true, a correction was applied successfully. 

777 """ 

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

779 return False, 0 

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

781 return False, 0 

782 

783 ampArray = image.getArray() 

784 correction = np.zeros_like(ampArray) 

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

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

787 ampArray += correction 

788 

789 return True, 0 

790 

791 

792class LinearizeSquared(LinearizeBase): 

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

794 

795 corrImage = uncorrImage + c0*uncorrImage^2 

796 

797 where c0 is linearity coefficient 0 for each amplifier. 

798 """ 

799 LinearityType = "Squared" 

800 

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

802 """Correct for non-linearity. 

803 

804 Parameters 

805 ---------- 

806 image : `lsst.afw.image.Image` 

807 Image to be corrected 

808 kwargs : `dict` 

809 Dictionary of parameter keywords: 

810 ``"coeffs"`` 

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

812 ``"log"`` 

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

814 

815 Returns 

816 ------- 

817 output : `bool` 

818 If true, a correction was applied successfully. 

819 """ 

820 

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

822 if sqCoeff != 0: 

823 ampArr = image.getArray() 

824 ampArr *= (1 + sqCoeff*ampArr) 

825 return True, 0 

826 else: 

827 return False, 0 

828 

829 

830class LinearizeProportional(LinearizeBase): 

831 """Do not correct non-linearity. 

832 """ 

833 LinearityType = "Proportional" 

834 

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

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

837 

838 Parameters 

839 ---------- 

840 image : `lsst.afw.image.Image` 

841 Image to be corrected 

842 kwargs : `dict` 

843 Dictionary of parameter keywords: 

844 ``"coeffs"`` 

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

846 ``"log"`` 

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

848 

849 Returns 

850 ------- 

851 output : `bool` 

852 If true, a correction was applied successfully. 

853 """ 

854 return True, 0 

855 

856 

857class LinearizeNone(LinearizeBase): 

858 """Do not correct non-linearity. 

859 """ 

860 LinearityType = "None" 

861 

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

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

864 

865 Parameters 

866 ---------- 

867 image : `lsst.afw.image.Image` 

868 Image to be corrected 

869 kwargs : `dict` 

870 Dictionary of parameter keywords: 

871 ``"coeffs"`` 

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

873 ``"log"`` 

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

875 

876 Returns 

877 ------- 

878 output : `bool` 

879 If true, a correction was applied successfully. 

880 """ 

881 return True, 0