Coverage for python/lsst/cp/pipe/linearity.py: 15%

272 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-07-09 07:21 -0700

1# This file is part of cp_pipe. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21# 

22import numpy as np 

23import lsst.afw.image as afwImage 

24import lsst.afw.math as afwMath 

25import lsst.pipe.base as pipeBase 

26import lsst.pipe.base.connectionTypes as cT 

27import lsst.pex.config as pexConfig 

28 

29from lsstDebug import getDebugFrame 

30from lsst.ip.isr import (Linearizer, IsrProvenance, PhotodiodeCalib) 

31 

32from .utils import (funcPolynomial, irlsFit) 

33from ._lookupStaticCalibration import lookupStaticCalibration 

34 

35__all__ = ["LinearitySolveTask", "LinearitySolveConfig", "MeasureLinearityTask"] 

36 

37 

38class LinearitySolveConnections(pipeBase.PipelineTaskConnections, 

39 dimensions=("instrument", "exposure", "detector")): 

40 dummy = cT.Input( 

41 name="raw", 

42 doc="Dummy exposure.", 

43 storageClass='Exposure', 

44 dimensions=("instrument", "exposure", "detector"), 

45 multiple=True, 

46 deferLoad=True, 

47 ) 

48 

49 camera = cT.PrerequisiteInput( 

50 name="camera", 

51 doc="Camera Geometry definition.", 

52 storageClass="Camera", 

53 dimensions=("instrument", ), 

54 isCalibration=True, 

55 lookupFunction=lookupStaticCalibration, 

56 ) 

57 

58 inputPtc = cT.PrerequisiteInput( 

59 name="ptc", 

60 doc="Input PTC dataset.", 

61 storageClass="PhotonTransferCurveDataset", 

62 dimensions=("instrument", "detector"), 

63 isCalibration=True, 

64 ) 

65 

66 inputPhotodiodeCorrection = cT.Input( 

67 name="pdCorrection", 

68 doc="Input photodiode correction.", 

69 storageClass="IsrCalib", 

70 dimensions=("instrument", ), 

71 isCalibration=True, 

72 ) 

73 

74 outputLinearizer = cT.Output( 

75 name="linearity", 

76 doc="Output linearity measurements.", 

77 storageClass="Linearizer", 

78 dimensions=("instrument", "detector"), 

79 isCalibration=True, 

80 ) 

81 

82 def __init__(self, *, config=None): 

83 super().__init__(config=config) 

84 

85 if config.applyPhotodiodeCorrection is not True: 

86 self.inputs.discard("inputPhotodiodeCorrection") 

87 

88 

89class LinearitySolveConfig(pipeBase.PipelineTaskConfig, 

90 pipelineConnections=LinearitySolveConnections): 

91 """Configuration for solving the linearity from PTC dataset. 

92 """ 

93 linearityType = pexConfig.ChoiceField( 

94 dtype=str, 

95 doc="Type of linearizer to construct.", 

96 default="Squared", 

97 allowed={ 

98 "LookupTable": "Create a lookup table solution.", 

99 "Polynomial": "Create an arbitrary polynomial solution.", 

100 "Squared": "Create a single order squared solution.", 

101 "Spline": "Create a spline based solution.", 

102 "None": "Create a dummy solution.", 

103 } 

104 ) 

105 polynomialOrder = pexConfig.Field( 

106 dtype=int, 

107 doc="Degree of polynomial to fit.", 

108 default=3, 

109 ) 

110 splineKnots = pexConfig.Field( 

111 dtype=int, 

112 doc="Number of spline knots to use in fit.", 

113 default=10, 

114 ) 

115 maxLookupTableAdu = pexConfig.Field( 

116 dtype=int, 

117 doc="Maximum DN value for a LookupTable linearizer.", 

118 default=2**18, 

119 ) 

120 maxLinearAdu = pexConfig.Field( 

121 dtype=float, 

122 doc="Maximum DN value to use to estimate linear term.", 

123 default=20000.0, 

124 ) 

125 minLinearAdu = pexConfig.Field( 

126 dtype=float, 

127 doc="Minimum DN value to use to estimate linear term.", 

128 default=2000.0, 

129 ) 

130 nSigmaClipLinear = pexConfig.Field( 

131 dtype=float, 

132 doc="Maximum deviation from linear solution for Poissonian noise.", 

133 default=5.0, 

134 ) 

135 ignorePtcMask = pexConfig.Field( 

136 dtype=bool, 

137 doc="Ignore the expIdMask set by the PTC solver?", 

138 default=False, 

139 ) 

140 usePhotodiode = pexConfig.Field( 

141 dtype=bool, 

142 doc="Use the photodiode info instead of the raw expTimes?", 

143 default=False, 

144 ) 

145 applyPhotodiodeCorrection = pexConfig.Field( 

146 dtype=bool, 

147 doc="Calculate and apply a correction to the photodiode readings?", 

148 default=False, 

149 ) 

150 

151 

152class LinearitySolveTask(pipeBase.PipelineTask, pipeBase.CmdLineTask): 

153 """Fit the linearity from the PTC dataset. 

154 """ 

155 

156 ConfigClass = LinearitySolveConfig 

157 _DefaultName = 'cpLinearitySolve' 

158 

159 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

160 """Ensure that the input and output dimensions are passed along. 

161 

162 Parameters 

163 ---------- 

164 butlerQC : `lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext` 

165 Butler to operate on. 

166 inputRefs : `lsst.pipe.base.connections.InputQuantizedConnection` 

167 Input data refs to load. 

168 ouptutRefs : `lsst.pipe.base.connections.OutputQuantizedConnection` 

169 Output data refs to persist. 

170 """ 

171 inputs = butlerQC.get(inputRefs) 

172 

173 # Use the dimensions to set calib/provenance information. 

174 inputs['inputDims'] = inputRefs.inputPtc.dataId.byName() 

175 

176 outputs = self.run(**inputs) 

177 butlerQC.put(outputs, outputRefs) 

178 

179 def run(self, inputPtc, dummy, camera, inputDims, inputPhotodiodeCorrection=None): 

180 """Fit non-linearity to PTC data, returning the correct Linearizer 

181 object. 

182 

183 Parameters 

184 ---------- 

185 inputPtc : `lsst.ip.isr.PtcDataset` 

186 Pre-measured PTC dataset. 

187 inputPhotodiodeCorrection : `lsst.ip.isr.PhotodiodeCorrection` 

188 Pre-measured photodiode correction used in the case when 

189 applyPhotodiodeCorrection=True. 

190 dummy : `lsst.afw.image.Exposure` 

191 The exposure used to select the appropriate PTC dataset. 

192 In almost all circumstances, one of the input exposures 

193 used to generate the PTC dataset is the best option. 

194 camera : `lsst.afw.cameraGeom.Camera` 

195 Camera geometry. 

196 inputDims : `lsst.daf.butler.DataCoordinate` or `dict` 

197 DataIds to use to populate the output calibration. 

198 

199 Returns 

200 ------- 

201 results : `lsst.pipe.base.Struct` 

202 The results struct containing: 

203 

204 ``outputLinearizer`` 

205 Final linearizer calibration (`lsst.ip.isr.Linearizer`). 

206 ``outputProvenance`` 

207 Provenance data for the new calibration 

208 (`lsst.ip.isr.IsrProvenance`). 

209 

210 Notes 

211 ----- 

212 This task currently fits only polynomial-defined corrections, 

213 where the correction coefficients are defined such that: 

214 :math:`corrImage = uncorrImage + \\sum_i c_i uncorrImage^(2 + i)` 

215 These :math:`c_i` are defined in terms of the direct polynomial fit: 

216 :math:`meanVector ~ P(x=timeVector) = \\sum_j k_j x^j` 

217 such that :math:`c_(j-2) = -k_j/(k_1^j)` in units of DN^(1-j) (c.f., 

218 Eq. 37 of 2003.05978). The `config.polynomialOrder` or 

219 `config.splineKnots` define the maximum order of :math:`x^j` to fit. 

220 As :math:`k_0` and :math:`k_1` are degenerate with bias level and gain, 

221 they are not included in the non-linearity correction. 

222 """ 

223 if len(dummy) == 0: 

224 self.log.warning("No dummy exposure found.") 

225 

226 detector = camera[inputDims['detector']] 

227 if self.config.linearityType == 'LookupTable': 

228 table = np.zeros((len(detector), self.config.maxLookupTableAdu), dtype=np.float32) 

229 tableIndex = 0 

230 else: 

231 table = None 

232 tableIndex = None # This will fail if we increment it. 

233 

234 if self.config.linearityType == 'Spline': 

235 fitOrder = self.config.splineKnots 

236 else: 

237 fitOrder = self.config.polynomialOrder 

238 

239 # Initialize the linearizer. 

240 linearizer = Linearizer(detector=detector, table=table, log=self.log) 

241 

242 if self.config.usePhotodiode and self.config.applyPhotodiodeCorrection: 

243 abscissaCorrections = inputPhotodiodeCorrection.abscissaCorrections 

244 

245 for i, amp in enumerate(detector): 

246 ampName = amp.getName() 

247 if ampName in inputPtc.badAmps: 

248 linearizer = self.fillBadAmp(linearizer, fitOrder, inputPtc, amp) 

249 self.log.warning("Amp %s in detector %s has no usable PTC information. Skipping!", 

250 ampName, detector.getName()) 

251 continue 

252 

253 if (len(inputPtc.expIdMask[ampName]) == 0) or self.config.ignorePtcMask: 

254 self.log.warning("Mask not found for %s in detector %s in fit. Using all points.", 

255 ampName, detector.getName()) 

256 mask = np.repeat(True, len(inputPtc.expIdMask[ampName])) 

257 else: 

258 mask = np.array(inputPtc.expIdMask[ampName], dtype=bool) 

259 

260 if self.config.usePhotodiode: 

261 # Here's where we bring in the photodiode data 

262 # TODO: DM-33585. Replace when pd data is ingested. 

263 DATA_DIR = '/lsstdata/offline/teststand/BOT/storage/' 

264 modExpTimes = [] 

265 for i, pair in enumerate(inputPtc.inputExpIdPairs[ampName]): 

266 pair = pair[0] 

267 modExpTime = 0.0 

268 nExps = 0 

269 for j in range(2): 

270 expId = pair[j] 

271 try: 

272 date = int(expId/100000) 

273 seq = expId - date * 100000 

274 date = date - 10000000 

275 filename = DATA_DIR + \ 

276 '%d/MC_C_%d_%06d/Photodiode_Readings_%d_%06d.txt'\ 

277 % (date, date, seq, date, seq) 

278 photodiodeCalib = PhotodiodeCalib.readTwoColumnPhotodiodeData(filename) 

279 photodiodeCalib.integrationMethod = 'TRIMMED_SUM' 

280 monDiodeCharge = photodiodeCalib.integrate() 

281 modExpTime += monDiodeCharge 

282 nExps += 1 

283 except (RuntimeError, OSError): 

284 continue 

285 if nExps > 0: 

286 # The 5E8 factor bring the modExpTimes back to about 

287 # the same order as the expTimes. 

288 # Probably a better way to do this. 

289 modExpTime = 5.0E8 * modExpTime / nExps 

290 else: 

291 mask[i] = False 

292 

293 # Get the photodiode correction 

294 if self.config.applyPhotodiodeCorrection: 

295 try: 

296 correction = abscissaCorrections[str(pair)] 

297 except KeyError: 

298 correction = 0.0 

299 else: 

300 correction = 0.0 

301 modExpTimes.append(modExpTime + correction) 

302 

303 inputAbscissa = np.array(modExpTimes)[mask] 

304 else: 

305 inputAbscissa = np.array(inputPtc.rawExpTimes[ampName])[mask] 

306 

307 inputOrdinate = np.array(inputPtc.rawMeans[ampName])[mask] 

308 # Determine proxy-to-linear-flux transformation 

309 fluxMask = inputOrdinate < self.config.maxLinearAdu 

310 lowMask = inputOrdinate > self.config.minLinearAdu 

311 fluxMask = fluxMask & lowMask 

312 linearAbscissa = inputAbscissa[fluxMask] 

313 linearOrdinate = inputOrdinate[fluxMask] 

314 if len(linearAbscissa) < 2: 

315 linearizer = self.fillBadAmp(linearizer, fitOrder, inputPtc, amp) 

316 self.log.warning("Amp %s in detector %s has not enough points for linear fit. Skipping!", 

317 ampName, detector.getName()) 

318 continue 

319 

320 linearFit, linearFitErr, chiSq, weights = irlsFit([0.0, 100.0], linearAbscissa, 

321 linearOrdinate, funcPolynomial) 

322 # Convert this proxy-to-flux fit into an expected linear flux 

323 linearOrdinate = linearFit[0] + linearFit[1] * inputAbscissa 

324 # Exclude low end outliers 

325 threshold = self.config.nSigmaClipLinear * np.sqrt(abs(linearOrdinate)) 

326 fluxMask = np.abs(inputOrdinate - linearOrdinate) < threshold 

327 linearOrdinate = linearOrdinate[fluxMask] 

328 fitOrdinate = inputOrdinate[fluxMask] 

329 fitAbscissa = inputAbscissa[fluxMask] 

330 if len(linearOrdinate) < 2: 

331 linearizer = self.fillBadAmp(linearizer, fitOrder, inputPtc, amp) 

332 self.log.warning("Amp %s in detector %s has not enough points in linear ordinate. Skipping!", 

333 ampName, detector.getName()) 

334 continue 

335 

336 self.debugFit('linearFit', inputAbscissa, inputOrdinate, linearOrdinate, fluxMask, ampName) 

337 # Do fits 

338 if self.config.linearityType in ['Polynomial', 'Squared', 'LookupTable']: 

339 polyFit = np.zeros(fitOrder + 1) 

340 polyFit[1] = 1.0 

341 polyFit, polyFitErr, chiSq, weights = irlsFit(polyFit, linearOrdinate, 

342 fitOrdinate, funcPolynomial) 

343 

344 # Truncate the polynomial fit 

345 k1 = polyFit[1] 

346 linearityFit = [-coeff/(k1**order) for order, coeff in enumerate(polyFit)] 

347 significant = np.where(np.abs(linearityFit) > 1e-10, True, False) 

348 self.log.info("Significant polynomial fits: %s", significant) 

349 

350 modelOrdinate = funcPolynomial(polyFit, fitAbscissa) 

351 

352 self.debugFit('polyFit', linearAbscissa, fitOrdinate, modelOrdinate, None, ampName) 

353 

354 if self.config.linearityType == 'Squared': 

355 linearityFit = [linearityFit[2]] 

356 elif self.config.linearityType == 'LookupTable': 

357 # Use linear part to get time at which signal is 

358 # maxAduForLookupTableLinearizer DN 

359 tMax = (self.config.maxLookupTableAdu - polyFit[0])/polyFit[1] 

360 timeRange = np.linspace(0, tMax, self.config.maxLookupTableAdu) 

361 signalIdeal = polyFit[0] + polyFit[1]*timeRange 

362 signalUncorrected = funcPolynomial(polyFit, timeRange) 

363 lookupTableRow = signalIdeal - signalUncorrected # LinearizerLookupTable has correction 

364 

365 linearizer.tableData[tableIndex, :] = lookupTableRow 

366 linearityFit = [tableIndex, 0] 

367 tableIndex += 1 

368 elif self.config.linearityType in ['Spline']: 

369 # See discussion in `lsst.ip.isr.linearize.py` before 

370 # modifying. 

371 numPerBin, binEdges = np.histogram(linearOrdinate, bins=fitOrder) 

372 with np.errstate(invalid="ignore"): 

373 # Algorithm note: With the counts of points per 

374 # bin above, the next histogram calculates the 

375 # values to put in each bin by weighting each 

376 # point by the correction value. 

377 values = np.histogram(linearOrdinate, bins=fitOrder, 

378 weights=(inputOrdinate[fluxMask] - linearOrdinate))[0]/numPerBin 

379 

380 # After this is done, the binCenters are 

381 # calculated by weighting by the value we're 

382 # binning over. This ensures that widely 

383 # spaced/poorly sampled data aren't assigned to 

384 # the midpoint of the bin (as could be done using 

385 # the binEdges above), but to the weighted mean of 

386 # the inputs. Note that both histograms are 

387 # scaled by the count per bin to normalize what 

388 # the histogram returns (a sum of the points 

389 # inside) into an average. 

390 binCenters = np.histogram(linearOrdinate, bins=fitOrder, 

391 weights=linearOrdinate)[0]/numPerBin 

392 values = values[numPerBin > 0] 

393 binCenters = binCenters[numPerBin > 0] 

394 

395 self.debugFit('splineFit', binCenters, np.abs(values), values, None, ampName) 

396 interp = afwMath.makeInterpolate(binCenters.tolist(), values.tolist(), 

397 afwMath.stringToInterpStyle("AKIMA_SPLINE")) 

398 modelOrdinate = linearOrdinate + interp.interpolate(linearOrdinate) 

399 self.debugFit('splineFit', linearOrdinate, fitOrdinate, modelOrdinate, None, ampName) 

400 

401 # If we exclude a lot of points, we may end up with 

402 # less than fitOrder points. Pad out the low-flux end 

403 # to ensure equal lengths. 

404 if len(binCenters) != fitOrder: 

405 padN = fitOrder - len(binCenters) 

406 binCenters = np.pad(binCenters, (padN, 0), 'linear_ramp', 

407 end_values=(binCenters.min() - 1.0, )) 

408 # This stores the correction, which is zero at low values. 

409 values = np.pad(values, (padN, 0)) 

410 

411 # Pack the spline into a single array. 

412 linearityFit = np.concatenate((binCenters.tolist(), values.tolist())).tolist() 

413 polyFit = [0.0] 

414 polyFitErr = [0.0] 

415 chiSq = np.nan 

416 else: 

417 polyFit = [0.0] 

418 polyFitErr = [0.0] 

419 chiSq = np.nan 

420 linearityFit = [0.0] 

421 

422 linearizer.linearityType[ampName] = self.config.linearityType 

423 linearizer.linearityCoeffs[ampName] = np.array(linearityFit) 

424 linearizer.linearityBBox[ampName] = amp.getBBox() 

425 linearizer.fitParams[ampName] = np.array(polyFit) 

426 linearizer.fitParamsErr[ampName] = np.array(polyFitErr) 

427 linearizer.fitChiSq[ampName] = chiSq 

428 linearizer.linearFit[ampName] = linearFit 

429 residuals = fitOrdinate - modelOrdinate 

430 

431 # The residuals only include flux values which are 

432 # not masked out. To be able to access this later and 

433 # associate it with the PTC flux values, we need to 

434 # fill out the residuals with NaNs where the flux 

435 # value is masked. 

436 

437 # First convert mask to a composite of the two masks: 

438 mask[mask] = fluxMask 

439 fullResiduals = np.full(len(mask), np.nan) 

440 fullResiduals[mask] = residuals 

441 linearizer.fitResiduals[ampName] = fullResiduals 

442 image = afwImage.ImageF(len(inputOrdinate), 1) 

443 image.getArray()[:, :] = inputOrdinate 

444 linearizeFunction = linearizer.getLinearityTypeByName(linearizer.linearityType[ampName]) 

445 linearizeFunction()(image, 

446 **{'coeffs': linearizer.linearityCoeffs[ampName], 

447 'table': linearizer.tableData, 

448 'log': linearizer.log}) 

449 linearizeModel = image.getArray()[0, :] 

450 

451 self.debugFit('solution', inputOrdinate[fluxMask], linearOrdinate, 

452 linearizeModel[fluxMask], None, ampName) 

453 

454 linearizer.hasLinearity = True 

455 linearizer.validate() 

456 linearizer.updateMetadata(camera=camera, detector=detector, filterName='NONE') 

457 linearizer.updateMetadata(setDate=True, setCalibId=True) 

458 provenance = IsrProvenance(calibType='linearizer') 

459 

460 return pipeBase.Struct( 

461 outputLinearizer=linearizer, 

462 outputProvenance=provenance, 

463 ) 

464 

465 def fillBadAmp(self, linearizer, fitOrder, inputPtc, amp): 

466 # Need to fill linearizer with empty values 

467 # if the amp is non-functional 

468 ampName = amp.getName() 

469 nEntries = 1 

470 pEntries = 1 

471 if self.config.linearityType in ['Polynomial']: 

472 nEntries = fitOrder + 1 

473 pEntries = fitOrder + 1 

474 elif self.config.linearityType in ['Spline']: 

475 nEntries = fitOrder * 2 

476 elif self.config.linearityType in ['Squared', 'None']: 

477 nEntries = 1 

478 pEntries = fitOrder + 1 

479 elif self.config.linearityType in ['LookupTable']: 

480 nEntries = 2 

481 pEntries = fitOrder + 1 

482 

483 linearizer.linearityType[ampName] = "None" 

484 linearizer.linearityCoeffs[ampName] = np.zeros(nEntries) 

485 linearizer.linearityBBox[ampName] = amp.getBBox() 

486 linearizer.fitParams[ampName] = np.zeros(pEntries) 

487 linearizer.fitParamsErr[ampName] = np.zeros(pEntries) 

488 linearizer.fitChiSq[ampName] = np.nan 

489 linearizer.fitResiduals[ampName] = np.zeros(len(inputPtc.expIdMask[ampName])) 

490 linearizer.linearFit[ampName] = np.zeros(2) 

491 return linearizer 

492 

493 def debugFit(self, stepname, xVector, yVector, yModel, mask, ampName): 

494 """Debug method for linearity fitting. 

495 

496 Parameters 

497 ---------- 

498 stepname : `str` 

499 A label to use to check if we care to debug at a given 

500 line of code. 

501 xVector : `numpy.array`, (N,) 

502 The values to use as the independent variable in the 

503 linearity fit. 

504 yVector : `numpy.array`, (N,) 

505 The values to use as the dependent variable in the 

506 linearity fit. 

507 yModel : `numpy.array`, (N,) 

508 The values to use as the linearized result. 

509 mask : `numpy.array` [`bool`], (N,) , optional 

510 A mask to indicate which entries of ``xVector`` and 

511 ``yVector`` to keep. 

512 ampName : `str` 

513 Amplifier name to lookup linearity correction values. 

514 """ 

515 frame = getDebugFrame(self._display, stepname) 

516 if frame: 

517 import matplotlib.pyplot as plt 

518 fig, axs = plt.subplots(2) 

519 

520 if mask is None: 

521 mask = np.ones_like(xVector, dtype=bool) 

522 

523 fig.suptitle(f"{stepname} {ampName} {self.config.linearityType}") 

524 if stepname == 'linearFit': 

525 axs[0].set_xlabel("Input Abscissa (time or mondiode)") 

526 axs[0].set_ylabel("Input Ordinate (flux)") 

527 axs[1].set_xlabel("Linear Ordinate (linear flux)") 

528 axs[1].set_ylabel("Flux Difference: (input - linear)") 

529 elif stepname in ('polyFit', 'splineFit'): 

530 axs[0].set_xlabel("Linear Abscissa (linear flux)") 

531 axs[0].set_ylabel("Input Ordinate (flux)") 

532 axs[1].set_xlabel("Linear Ordinate (linear flux)") 

533 axs[1].set_ylabel("Flux Difference: (input - full model fit)") 

534 elif stepname == 'solution': 

535 axs[0].set_xlabel("Input Abscissa (time or mondiode)") 

536 axs[0].set_ylabel("Linear Ordinate (linear flux)") 

537 axs[1].set_xlabel("Model flux (linear flux)") 

538 axs[1].set_ylabel("Flux Difference: (linear - model)") 

539 

540 axs[0].set_yscale('log') 

541 axs[0].set_xscale('log') 

542 axs[0].scatter(xVector, yVector) 

543 axs[0].scatter(xVector[~mask], yVector[~mask], c='red', marker='x') 

544 axs[1].set_xscale('log') 

545 

546 axs[1].scatter(yModel, yVector[mask] - yModel) 

547 fig.show() 

548 

549 prompt = "Press Enter or c to continue [chpx]..." 

550 while True: 

551 ans = input(prompt).lower() 

552 if ans in ("", " ", "c",): 

553 break 

554 elif ans in ("p", ): 

555 import pdb 

556 pdb.set_trace() 

557 elif ans in ("h", ): 

558 print("[h]elp [c]ontinue [p]db") 

559 elif ans in ('x', ): 

560 exit() 

561 plt.close() 

562 

563 

564class MeasureLinearityConfig(pexConfig.Config): 

565 solver = pexConfig.ConfigurableField( 

566 target=LinearitySolveTask, 

567 doc="Task to convert PTC data to linearity solutions.", 

568 ) 

569 

570 

571class MeasureLinearityTask(pipeBase.CmdLineTask): 

572 """Stand alone Gen2 linearity measurement. 

573 

574 This class wraps the Gen3 linearity task to allow it to be run as 

575 a Gen2 CmdLineTask. 

576 """ 

577 

578 ConfigClass = MeasureLinearityConfig 

579 _DefaultName = "measureLinearity" 

580 

581 def __init__(self, **kwargs): 

582 super().__init__(**kwargs) 

583 self.makeSubtask("solver") 

584 

585 def runDataRef(self, dataRef): 

586 """Run new linearity code for gen2. 

587 

588 Parameters 

589 ---------- 

590 dataRef : `lsst.daf.persistence.ButlerDataRef` 

591 Input dataref for the photon transfer curve data. 

592 

593 Returns 

594 ------- 

595 results : `lsst.pipe.base.Struct` 

596 The results struct containing: 

597 

598 ``outputLinearizer`` 

599 Final linearizer calibration (`lsst.ip.isr.Linearizer`). 

600 ``outputProvenance`` 

601 Provenance data for the new calibration 

602 (`lsst.ip.isr.IsrProvenance`). 

603 """ 

604 ptc = dataRef.get('photonTransferCurveDataset') 

605 camera = dataRef.get('camera') 

606 inputDims = dataRef.dataId # This is the closest gen2 has. 

607 linearityResults = self.solver.run(ptc, camera=camera, inputDims=inputDims) 

608 

609 inputDims['calibDate'] = linearityResults.outputLinearizer.getMetadata().get('CALIBDATE') 

610 butler = dataRef.getButler() 

611 butler.put(linearityResults.outputLinearizer, "linearizer", inputDims) 

612 return linearityResults