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.fitParams = dict() 

85 self.fitParamsErr = dict() 

86 self.linearityFitReducedChiSquared = dict() 

87 

88 self.override = override 

89 self.populated = False 

90 self.log = log 

91 

92 self.tableData = None 

93 if table is not None: 

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

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

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

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

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

99 

100 if detector: 

101 self.fromDetector(detector) 

102 

103 def __call__(self, exposure): 

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

105 

106 Parameters 

107 ---------- 

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

109 Exposure to correct. 

110 

111 Returns 

112 ------- 

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

114 Linearization results: 

115 ``"numAmps"`` 

116 Number of amplifiers considered. 

117 ``"numLinearized"`` 

118 Number of amplifiers linearized. 

119 """ 

120 

121 def fromDetector(self, detector): 

122 """Read linearity parameters from a detector. 

123 

124 Parameters 

125 ---------- 

126 detector : `lsst.afw.cameraGeom.detector` 

127 Input detector with parameters to use. 

128 """ 

129 self._detectorName = detector.getName() 

130 self._detectorSerial = detector.getSerial() 

131 self._detectorId = detector.getId() 

132 self.populated = True 

133 

134 # Do not translate Threshold, Maximum, Units. 

135 for amp in detector.getAmplifiers(): 

136 ampName = amp.getName() 

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

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

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

140 

141 def fromYaml(self, yamlObject): 

142 """Read linearity parameters from a dict. 

143 

144 Parameters 

145 ---------- 

146 yamlObject : `dict` 

147 Dictionary containing detector and amplifier information. 

148 """ 

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

150 self._detectorName = yamlObject['detectorName'] 

151 self._detectorSerial = yamlObject['detectorSerial'] 

152 self._detectorId = yamlObject['detectorId'] 

153 self.populated = True 

154 self.override = True 

155 

156 for ampName in yamlObject['amplifiers']: 

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

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

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

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

161 self.fitParams[ampName] = np.array(amp.get('linearityFitParams', None), dtype=np.float64) 

162 self.fitParamsErr[ampName] = np.array(amp.get('linearityFitParamsErr', None), dtype=np.float64) 

163 self.linearityFitReducedChiSquared[ampName] = amp.get('linearityFitReducedChiSquared', None) 

164 

165 if self.tableData is None: 

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

167 if self.tableData: 

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

169 

170 return self 

171 

172 def toDict(self): 

173 """Return linearity parameters as a dict. 

174 

175 Returns 

176 ------- 

177 outDict : `dict`: 

178 """ 

179 # metadata copied from defects code 

180 now = datetime.datetime.utcnow() 

181 self.updateMetadata(date=now) 

182 

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

184 'detectorName': self._detectorName, 

185 'detectorSerial': self._detectorSerial, 

186 'detectorId': self._detectorId, 

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

188 'amplifiers': dict()} 

189 for ampName in self.linearityType: 

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

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

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

193 'linearityFitParams': self.fitParams[ampName], 

194 'linearityFitParamsErr': self.fitParamsErr[ampName], 

195 'linearityFitReducedChiSquared': ( 

196 self.linearityFitReducedChiSquared[ampName])} 

197 if self.tableData is not None: 

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

199 

200 return outDict 

201 

202 @classmethod 

203 def readText(cls, filename): 

204 """Read linearity from text file. 

205 

206 Parameters 

207 ---------- 

208 filename : `str` 

209 Name of the file containing the linearity definition. 

210 Returns 

211 ------- 

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

213 Linearity parameters. 

214 """ 

215 data = '' 

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

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

218 return cls().fromYaml(data) 

219 

220 def writeText(self, filename): 

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

222 

223 Parameters 

224 ---------- 

225 filename : `str` 

226 Name of the file to write. 

227 

228 Returns 

229 ------- 

230 used : `str` 

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

232 

233 Raises 

234 ------ 

235 RuntimeError : 

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

237 

238 Notes 

239 ----- 

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

241 associated with the `Linearity`. 

242 """ 

243 outDict = self.toDict() 

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

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

246 yaml.dump(outDict, f) 

247 else: 

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

249 

250 return filename 

251 

252 @classmethod 

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

254 """Read linearity from a FITS file. 

255 

256 Parameters 

257 ---------- 

258 table : `lsst.afw.table` 

259 afwTable read from input file name. 

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

261 afwTable read from second extension of input file name 

262 

263 Returns 

264 ------- 

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

266 Linearity parameters. 

267 

268 Notes 

269 ----- 

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

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

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

273 set each dictionary by looping over rows. 

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

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

276 """ 

277 metadata = table.getMetadata() 

278 schema = table.getSchema() 

279 

280 linDict = dict() 

281 linDict['metadata'] = metadata 

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

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

284 try: 

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

286 except Exception: 

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

288 linDict['amplifiers'] = dict() 

289 

290 # Preselect the keys 

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

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

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

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

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

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

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

298 fitParamsKey = schema["FIT_PARAMS"].asKey() 

299 fitParamsErrKey = schema["FIT_PARAMS_ERR"].asKey() 

300 reducedChiSquaredKey = schema["RED_CHI_SQ"].asKey() 

301 

302 for record in table: 

303 ampName = record[ampNameKey] 

304 ampDict = dict() 

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

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

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

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

309 ampDict['linearityFitParams'] = record[fitParamsKey] 

310 ampDict['linearityFitParamsErr'] = record[fitParamsErrKey] 

311 ampDict['linearityFitReducedChiSquared'] = record[reducedChiSquaredKey] 

312 

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

314 

315 if tableExtTwo is not None: 

316 lookupValuesKey = 'LOOKUP_VALUES' 

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

318 

319 return cls().fromYaml(linDict) 

320 

321 @classmethod 

322 def readFits(cls, filename): 

323 """Read linearity from a FITS file. 

324 

325 Parameters 

326 ---------- 

327 filename : `str` 

328 Name of the file containing the linearity definition. 

329 Returns 

330 ------- 

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

332 Linearity parameters. 

333 

334 Notes 

335 ----- 

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

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

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

339 set each dictionary by looping over rows. 

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

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

342 """ 

343 table = afwTable.BaseCatalog.readFits(filename) 

344 tableExtTwo = None 

345 try: 

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

347 except Exception: 

348 pass 

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

350 

351 def toAmpTable(self, metadata): 

352 """Produce linearity catalog 

353 

354 Parameters 

355 ---------- 

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

357 Linearizer metadata 

358 

359 Returns 

360 ------- 

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

362 Catalog to write 

363 """ 

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

365 metadata["LINEARITY_VERSION"] = 1 

366 

367 # Now pack it into a fits table. 

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

369 

370 schema = afwTable.Schema() 

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

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

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

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

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

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

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

378 

379 if (self.fitParams): 

380 lengthFitParams = max([len(self.fitParams[x]) for x in self.fitParams.keys()]) 

381 

382 fitParams = schema.addField("FIT_PARAMS", type="ArrayD", size=lengthFitParams, 

383 doc="parameters of linearity polynomial fit") 

384 fitParamsErr = schema.addField("FIT_PARAMS_ERR", type="ArrayD", size=lengthFitParams, 

385 doc="errors of parameters of linearity polynomial fit") 

386 reducedChiSquared = schema.addField("RED_CHI_SQ", type="D", 

387 doc="unweighted reduced chi sq. from linearity pol. fit") 

388 

389 catalog = afwTable.BaseCatalog(schema) 

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

391 

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

393 catalog[ii][names] = ampName 

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

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

396 if (self.fitParams): 

397 catalog[ii][fitParams] = np.array(self.fitParams[ampName], dtype=float) 

398 catalog[ii][fitParamsErr] = np.array(self.fitParamsErr[ampName], dtype=float) 

399 catalog[ii][reducedChiSquared] = self.linearityFitReducedChiSquared[ampName] 

400 

401 bbox = self.linearityBBox[ampName] 

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

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

404 catalog.setMetadata(metadata) 

405 

406 return catalog 

407 

408 def toTableDataTable(self, metadata): 

409 """Produce linearity catalog from table data 

410 

411 Parameters 

412 ---------- 

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

414 Linearizer metadata 

415 

416 Returns 

417 ------- 

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

419 Catalog to write 

420 """ 

421 

422 schema = afwTable.Schema() 

423 dimensions = self.tableData.shape 

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

425 doc="linearity lookup data") 

426 catalog = afwTable.BaseCatalog(schema) 

427 catalog.resize(dimensions[0]) 

428 

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

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

431 

432 metadata["LINEARITY_LOOKUP"] = True 

433 catalog.setMetadata(metadata) 

434 

435 return catalog 

436 

437 def writeFits(self, filename): 

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

439 

440 Parameters 

441 ---------- 

442 filename : `str` 

443 Name of the file to write. 

444 

445 Notes 

446 ----- 

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

448 associated with the `Linearity`. 

449 """ 

450 now = datetime.datetime.utcnow() 

451 self.updateMetadata(date=now) 

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

453 catalog = self.toAmpTable(metadata) 

454 catalog.writeFits(filename) 

455 

456 if self.tableData is not None: 

457 catalog = self.toTableDataTable(metadata) 

458 catalog.writeFits(filename, "a") 

459 

460 return 

461 

462 def getMetadata(self): 

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

464 

465 Returns 

466 ------- 

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

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

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

470 external files. 

471 """ 

472 return self._metadata 

473 

474 def setMetadata(self, metadata=None): 

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

476 

477 Parameters 

478 ---------- 

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

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

481 overwrite existing metadata. If not supplied the existing 

482 metadata will be reset. 

483 """ 

484 if metadata is None: 

485 self._metadata = PropertyList() 

486 else: 

487 self._metadata = copy.copy(metadata) 

488 

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

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

491 

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

493 serial=None): 

494 """Update metadata keywords with new values. 

495 

496 Parameters 

497 ---------- 

498 date : `datetime.datetime`, optional 

499 detectorId : `int`, optional 

500 detectorName: `str`, optional 

501 instrumentName : `str`, optional 

502 calibId: `str`, optional 

503 serial: detector serial, `str`, optional 

504 

505 """ 

506 mdOriginal = self.getMetadata() 

507 mdSupplemental = dict() 

508 

509 if date: 

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

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

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

513 if detectorId: 

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

515 if detectorName: 

516 mdSupplemental['DETECTOR_NAME'] = detectorName 

517 if instrumentName: 

518 mdSupplemental['INSTRUME'] = instrumentName 

519 if calibId: 

520 mdSupplemental['CALIB_ID'] = calibId 

521 if serial: 

522 mdSupplemental['DETECTOR_SERIAL'] = serial 

523 

524 mdOriginal.update(mdSupplemental) 

525 

526 def getLinearityTypeByName(self, linearityTypeName): 

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

528 

529 Parameters 

530 ---------- 

531 linearityTypeName : str 

532 String name of the linearity type that is needed. 

533 

534 Returns 

535 ------- 

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

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

538 is found, `None` is returned. 

539 """ 

540 for t in [LinearizeLookupTable, 

541 LinearizeSquared, 

542 LinearizePolynomial, 

543 LinearizeProportional, 

544 LinearizeNone]: 

545 if t.LinearityType == linearityTypeName: 

546 return t 

547 return None 

548 

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

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

551 

552 Parameters 

553 ---------- 

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

555 Detector to validate, along with its amplifiers. 

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

557 Single amplifier to validate. 

558 

559 Raises 

560 ------ 

561 RuntimeError : 

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

563 the cameraGeom parameters are not being overridden. 

564 """ 

565 amplifiersToCheck = [] 

566 if detector: 

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

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

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

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

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

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

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

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

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

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

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

578 (len(detector.getAmplifiers()), 

579 len(self.linearityCoeffs.keys()))) 

580 amplifiersToCheck.extend(detector.getAmplifiers()) 

581 

582 if amplifier: 

583 amplifiersToCheck.extend(amplifier) 

584 

585 for amp in amplifiersToCheck: 

586 ampName = amp.getName() 

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

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

589 (ampName, )) 

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

591 if self.override: 

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

593 self.linearityType[ampName], ampName) 

594 else: 

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

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

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

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

599 if self.override: 

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

601 self.linearityCoeffs[ampName], ampName) 

602 else: 

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

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

605 

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

607 """Apply the linearity to an image. 

608 

609 If the linearity parameters are populated, use those, 

610 otherwise use the values from the detector. 

611 

612 Parameters 

613 ---------- 

614 image : `~lsst.afw.image.image` 

615 Image to correct. 

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

617 Detector to use for linearity parameters if not already 

618 populated. 

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

620 Log object to use for logging. 

621 """ 

622 if log is None: 

623 log = self.log 

624 if detector and not self.populated: 

625 self.fromDetector(detector) 

626 

627 self.validate(detector) 

628 

629 numAmps = 0 

630 numLinearized = 0 

631 numOutOfRange = 0 

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

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

634 numAmps += 1 

635 if linearizer is not None: 

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

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

638 'table': self.tableData, 

639 'log': self.log}) 

640 numOutOfRange += outOfRange 

641 if success: 

642 numLinearized += 1 

643 elif log is not None: 

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

645 ampName) 

646 return Struct( 

647 numAmps=numAmps, 

648 numLinearized=numLinearized, 

649 numOutOfRange=numOutOfRange 

650 ) 

651 

652 

653class LinearizeBase(metaclass=abc.ABCMeta): 

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

655 

656 Subclasses must define __call__ and set class variable 

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

658 the cameraGeom.Amplifier.linearityType field. 

659 

660 All linearity corrections should be defined in terms of an 

661 additive correction, such that: 

662 

663 corrected_value = uncorrected_value + f(uncorrected_value) 

664 """ 

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

666 

667 @abc.abstractmethod 

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

669 """Correct non-linearity. 

670 

671 Parameters 

672 ---------- 

673 image : `lsst.afw.image.Image` 

674 Image to be corrected 

675 kwargs : `dict` 

676 Dictionary of parameter keywords: 

677 ``"coeffs"`` 

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

679 ``"table"`` 

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

681 ``"log"`` 

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

683 

684 Returns 

685 ------- 

686 output : `bool` 

687 If true, a correction was applied successfully. 

688 

689 Raises 

690 ------ 

691 RuntimeError: 

692 Raised if the linearity type listed in the 

693 detector does not match the class type. 

694 """ 

695 pass 

696 

697 

698class LinearizeLookupTable(LinearizeBase): 

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

700 

701 The lookup table consists of entries such that given 

702 "coefficients" c0, c1: 

703 

704 for each i,j of image: 

705 rowInd = int(c0) 

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

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

708 

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

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

711 amplifiers use the same table) 

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

713 before truncation; this supports tables that can handle 

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

715 the nearest index is used instead of truncating to the 

716 next smaller index 

717 """ 

718 LinearityType = "LookupTable" 

719 

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

721 """Correct for non-linearity. 

722 

723 Parameters 

724 ---------- 

725 image : `lsst.afw.image.Image` 

726 Image to be corrected 

727 kwargs : `dict` 

728 Dictionary of parameter keywords: 

729 ``"coeffs"`` 

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

731 ``"table"`` 

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

733 ``"log"`` 

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

735 

736 Returns 

737 ------- 

738 output : `bool` 

739 If true, a correction was applied successfully. 

740 

741 Raises 

742 ------ 

743 RuntimeError: 

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

745 bounds. 

746 """ 

747 numOutOfRange = 0 

748 

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

750 table = kwargs['table'] 

751 log = kwargs['log'] 

752 

753 numTableRows = table.shape[0] 

754 rowInd = int(rowInd) 

755 if rowInd < 0 or rowInd > numTableRows: 

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

757 (rowInd, numTableRows)) 

758 tableRow = table[rowInd, :] 

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

760 

761 if numOutOfRange > 0 and log is not None: 

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

763 numOutOfRange) 

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

765 return True, numOutOfRange 

766 else: 

767 return False, numOutOfRange 

768 

769 

770class LinearizePolynomial(LinearizeBase): 

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

772 

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

774 

775 where c_i are the linearity coefficients for each amplifier. 

776 Lower order coefficients are not included as they duplicate other 

777 calibration parameters: 

778 ``"k0"`` 

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

780 bias level. Irrelevant for correcting non-linearity. 

781 ``"k1"`` 

782 A coefficient multiplied by uncorrImage**1 is proportional 

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

784 """ 

785 LinearityType = "Polynomial" 

786 

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

788 """Correct non-linearity. 

789 

790 Parameters 

791 ---------- 

792 image : `lsst.afw.image.Image` 

793 Image to be corrected 

794 kwargs : `dict` 

795 Dictionary of parameter keywords: 

796 ``"coeffs"`` 

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

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

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

800 not needed for the correction). 

801 ``"log"`` 

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

803 

804 Returns 

805 ------- 

806 output : `bool` 

807 If true, a correction was applied successfully. 

808 """ 

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

810 return False, 0 

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

812 return False, 0 

813 

814 ampArray = image.getArray() 

815 correction = np.zeros_like(ampArray) 

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

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

818 ampArray += correction 

819 

820 return True, 0 

821 

822 

823class LinearizeSquared(LinearizeBase): 

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

825 

826 corrImage = uncorrImage + c0*uncorrImage^2 

827 

828 where c0 is linearity coefficient 0 for each amplifier. 

829 """ 

830 LinearityType = "Squared" 

831 

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

833 """Correct for non-linearity. 

834 

835 Parameters 

836 ---------- 

837 image : `lsst.afw.image.Image` 

838 Image to be corrected 

839 kwargs : `dict` 

840 Dictionary of parameter keywords: 

841 ``"coeffs"`` 

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

843 ``"log"`` 

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

845 

846 Returns 

847 ------- 

848 output : `bool` 

849 If true, a correction was applied successfully. 

850 """ 

851 

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

853 if sqCoeff != 0: 

854 ampArr = image.getArray() 

855 ampArr *= (1 + sqCoeff*ampArr) 

856 return True, 0 

857 else: 

858 return False, 0 

859 

860 

861class LinearizeProportional(LinearizeBase): 

862 """Do not correct non-linearity. 

863 """ 

864 LinearityType = "Proportional" 

865 

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

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

868 

869 Parameters 

870 ---------- 

871 image : `lsst.afw.image.Image` 

872 Image to be corrected 

873 kwargs : `dict` 

874 Dictionary of parameter keywords: 

875 ``"coeffs"`` 

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

877 ``"log"`` 

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

879 

880 Returns 

881 ------- 

882 output : `bool` 

883 If true, a correction was applied successfully. 

884 """ 

885 return True, 0 

886 

887 

888class LinearizeNone(LinearizeBase): 

889 """Do not correct non-linearity. 

890 """ 

891 LinearityType = "None" 

892 

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

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

895 

896 Parameters 

897 ---------- 

898 image : `lsst.afw.image.Image` 

899 Image to be corrected 

900 kwargs : `dict` 

901 Dictionary of parameter keywords: 

902 ``"coeffs"`` 

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

904 ``"log"`` 

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

906 

907 Returns 

908 ------- 

909 output : `bool` 

910 If true, a correction was applied successfully. 

911 """ 

912 return True, 0