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

244 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-25 16:45 +0000

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# 

22 

23__all__ = ["LinearitySolveTask", "LinearitySolveConfig"] 

24 

25import numpy as np 

26import lsst.afw.image as afwImage 

27import lsst.pipe.base as pipeBase 

28import lsst.pipe.base.connectionTypes as cT 

29import lsst.pex.config as pexConfig 

30 

31from lsstDebug import getDebugFrame 

32from lsst.ip.isr import (Linearizer, IsrProvenance) 

33 

34from .utils import funcPolynomial, irlsFit, AstierSplineLinearityFitter 

35 

36 

37def ptcLookup(datasetType, registry, quantumDataId, collections): 

38 """Butler lookup function to allow PTC to be found. 

39 

40 Parameters 

41 ---------- 

42 datasetType : `lsst.daf.butler.DatasetType` 

43 Dataset type to look up. 

44 registry : `lsst.daf.butler.Registry` 

45 Registry for the data repository being searched. 

46 quantumDataId : `lsst.daf.butler.DataCoordinate` 

47 Data ID for the quantum of the task this dataset will be passed to. 

48 This must include an "instrument" key, and should also include any 

49 keys that are present in ``datasetType.dimensions``. If it has an 

50 ``exposure`` or ``visit`` key, that's a sign that this function is 

51 not actually needed, as those come with the temporal information that 

52 would allow a real validity-range lookup. 

53 collections : `lsst.daf.butler.registry.CollectionSearch` 

54 Collections passed by the user when generating a QuantumGraph. Ignored 

55 by this function (see notes below). 

56 

57 Returns 

58 ------- 

59 refs : `list` [ `DatasetRef` ] 

60 A zero- or single-element list containing the matching 

61 dataset, if one was found. 

62 

63 Raises 

64 ------ 

65 RuntimeError 

66 Raised if more than one PTC reference is found. 

67 """ 

68 refs = list(registry.queryDatasets(datasetType, dataId=quantumDataId, collections=collections, 

69 findFirst=False)) 

70 if len(refs) >= 2: 

71 RuntimeError("Too many PTC connections found. Incorrect collections supplied?") 

72 

73 return refs 

74 

75 

76class LinearitySolveConnections(pipeBase.PipelineTaskConnections, 

77 dimensions=("instrument", "detector")): 

78 dummy = cT.Input( 

79 name="raw", 

80 doc="Dummy exposure.", 

81 storageClass='Exposure', 

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

83 multiple=True, 

84 deferLoad=True, 

85 ) 

86 

87 camera = cT.PrerequisiteInput( 

88 name="camera", 

89 doc="Camera Geometry definition.", 

90 storageClass="Camera", 

91 dimensions=("instrument", ), 

92 isCalibration=True, 

93 ) 

94 

95 inputPtc = cT.PrerequisiteInput( 

96 name="ptc", 

97 doc="Input PTC dataset.", 

98 storageClass="PhotonTransferCurveDataset", 

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

100 isCalibration=True, 

101 lookupFunction=ptcLookup, 

102 ) 

103 

104 inputPhotodiodeCorrection = cT.Input( 

105 name="pdCorrection", 

106 doc="Input photodiode correction.", 

107 storageClass="IsrCalib", 

108 dimensions=("instrument", ), 

109 isCalibration=True, 

110 ) 

111 

112 outputLinearizer = cT.Output( 

113 name="linearity", 

114 doc="Output linearity measurements.", 

115 storageClass="Linearizer", 

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

117 isCalibration=True, 

118 ) 

119 

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

121 if not config.applyPhotodiodeCorrection: 

122 del self.inputPhotodiodeCorrection 

123 

124 

125class LinearitySolveConfig(pipeBase.PipelineTaskConfig, 

126 pipelineConnections=LinearitySolveConnections): 

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

128 """ 

129 linearityType = pexConfig.ChoiceField( 

130 dtype=str, 

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

132 default="Squared", 

133 allowed={ 

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

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

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

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

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

139 } 

140 ) 

141 polynomialOrder = pexConfig.RangeField( 

142 dtype=int, 

143 doc="Degree of polynomial to fit. Must be at least 2.", 

144 default=3, 

145 min=2, 

146 ) 

147 splineKnots = pexConfig.Field( 

148 dtype=int, 

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

150 default=10, 

151 ) 

152 maxLookupTableAdu = pexConfig.Field( 

153 dtype=int, 

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

155 default=2**18, 

156 ) 

157 maxLinearAdu = pexConfig.Field( 

158 dtype=float, 

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

160 default=20000.0, 

161 ) 

162 minLinearAdu = pexConfig.Field( 

163 dtype=float, 

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

165 default=30.0, 

166 ) 

167 nSigmaClipLinear = pexConfig.Field( 

168 dtype=float, 

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

170 default=5.0, 

171 ) 

172 ignorePtcMask = pexConfig.Field( 

173 dtype=bool, 

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

175 default=False, 

176 ) 

177 usePhotodiode = pexConfig.Field( 

178 dtype=bool, 

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

180 default=False, 

181 ) 

182 photodiodeIntegrationMethod = pexConfig.ChoiceField( 

183 dtype=str, 

184 doc="Integration method for photodiode monitoring data.", 

185 default="DIRECT_SUM", 

186 allowed={ 

187 "DIRECT_SUM": ("Use numpy's trapz integrator on all photodiode " 

188 "readout entries"), 

189 "TRIMMED_SUM": ("Use numpy's trapz integrator, clipping the " 

190 "leading and trailing entries, which are " 

191 "nominally at zero baseline level."), 

192 "CHARGE_SUM": ("Treat the current values as integrated charge " 

193 "over the sampling interval and simply sum " 

194 "the values, after subtracting a baseline level."), 

195 }, 

196 # TODO: remove on DM-40065. 

197 deprecated="This config has been moved to cpExtractPtcTask, and will be removed after v26.", 

198 ) 

199 photodiodeCurrentScale = pexConfig.Field( 

200 dtype=float, 

201 doc="Scale factor to apply to photodiode current values for the " 

202 "``CHARGE_SUM`` integration method.", 

203 default=-1.0, 

204 # TODO: remove on DM-40065. 

205 deprecated="This config has been moved to cpExtractPtcTask, and will be removed after v26.", 

206 ) 

207 applyPhotodiodeCorrection = pexConfig.Field( 

208 dtype=bool, 

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

210 default=False, 

211 ) 

212 splineGroupingColumn = pexConfig.Field( 

213 dtype=str, 

214 doc="Column to use for grouping together points for Spline mode, to allow " 

215 "for different proportionality constants. If not set, no grouping " 

216 "will be done.", 

217 default=None, 

218 optional=True, 

219 ) 

220 splineGroupingMinPoints = pexConfig.Field( 

221 dtype=int, 

222 doc="Minimum number of linearity points to allow grouping together points " 

223 "for Spline mode with splineGroupingColumn. This configuration is here " 

224 "to prevent misuse of the Spline code to avoid over-fitting.", 

225 default=100, 

226 ) 

227 splineFitMinIter = pexConfig.Field( 

228 dtype=int, 

229 doc="Minimum number of iterations for spline fit.", 

230 default=3, 

231 ) 

232 splineFitMaxIter = pexConfig.Field( 

233 dtype=int, 

234 doc="Maximum number of iterations for spline fit.", 

235 default=20, 

236 ) 

237 splineFitMaxRejectionPerIteration = pexConfig.Field( 

238 dtype=int, 

239 doc="Maximum number of rejections per iteration for spline fit.", 

240 default=5, 

241 ) 

242 

243 

244class LinearitySolveTask(pipeBase.PipelineTask): 

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

246 """ 

247 

248 ConfigClass = LinearitySolveConfig 

249 _DefaultName = 'cpLinearitySolve' 

250 

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

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

253 

254 Parameters 

255 ---------- 

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

257 Butler to operate on. 

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

259 Input data refs to load. 

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

261 Output data refs to persist. 

262 """ 

263 inputs = butlerQC.get(inputRefs) 

264 

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

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

267 

268 outputs = self.run(**inputs) 

269 butlerQC.put(outputs, outputRefs) 

270 

271 def run(self, inputPtc, dummy, camera, inputDims, 

272 inputPhotodiodeCorrection=None): 

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

274 object. 

275 

276 Parameters 

277 ---------- 

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

279 Pre-measured PTC dataset. 

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

281 The exposure used to select the appropriate PTC dataset. 

282 In almost all circumstances, one of the input exposures 

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

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

285 Pre-measured photodiode correction used in the case when 

286 applyPhotodiodeCorrection=True. 

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

288 Camera geometry. 

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

290 DataIds to use to populate the output calibration. 

291 

292 Returns 

293 ------- 

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

295 The results struct containing: 

296 

297 ``outputLinearizer`` 

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

299 ``outputProvenance`` 

300 Provenance data for the new calibration 

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

302 

303 Notes 

304 ----- 

305 This task currently fits only polynomial-defined corrections, 

306 where the correction coefficients are defined such that: 

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

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

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

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

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

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

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

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

315 """ 

316 if len(dummy) == 0: 

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

318 

319 detector = camera[inputDims['detector']] 

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

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

322 tableIndex = 0 

323 else: 

324 table = None 

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

326 

327 # Initialize the linearizer. 

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

329 linearizer.updateMetadataFromExposures([inputPtc]) 

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

331 abscissaCorrections = inputPhotodiodeCorrection.abscissaCorrections 

332 

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

334 if self.config.splineGroupingColumn is not None: 

335 if self.config.splineGroupingColumn not in inputPtc.auxValues: 

336 raise ValueError(f"Config requests grouping by {self.config.splineGroupingColumn}, " 

337 "but this column is not available in inputPtc.auxValues.") 

338 groupingValue = inputPtc.auxValues[self.config.splineGroupingColumn] 

339 else: 

340 groupingValue = np.ones(len(inputPtc.rawMeans[inputPtc.ampNames[0]]), dtype=int) 

341 # We set this to have a value to fill the bad amps. 

342 fitOrder = self.config.splineKnots 

343 else: 

344 fitOrder = self.config.polynomialOrder 

345 

346 for i, amp in enumerate(detector): 

347 ampName = amp.getName() 

348 if ampName in inputPtc.badAmps: 

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

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

351 ampName, detector.getName()) 

352 continue 

353 

354 # Check for too few points. 

355 if self.config.linearityType == "Spline" \ 

356 and self.config.splineGroupingColumn is not None \ 

357 and len(inputPtc.inputExpIdPairs[ampName]) < self.config.splineGroupingMinPoints: 

358 raise RuntimeError( 

359 "The input PTC has too few points to reliably run with PD grouping. " 

360 "The recommended course of action is to set splineGroupingColumn to None. " 

361 "If you really know what you are doing, you may reduce " 

362 "config.splineGroupingMinPoints.") 

363 

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

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

366 ampName, detector.getName()) 

367 mask = np.ones(len(inputPtc.expIdMask[ampName]), dtype=bool) 

368 else: 

369 mask = inputPtc.expIdMask[ampName].copy() 

370 

371 if self.config.usePhotodiode: 

372 modExpTimes = inputPtc.photoCharges[ampName].copy() 

373 # Make sure any exposure pairs that do not have photodiode data 

374 # are masked. 

375 mask[~np.isfinite(modExpTimes)] = False 

376 

377 # Get the photodiode correction. 

378 if self.config.applyPhotodiodeCorrection: 

379 for j, pair in enumerate(inputPtc.inputExpIdPairs[ampName]): 

380 try: 

381 correction = abscissaCorrections[str(pair)] 

382 except KeyError: 

383 correction = 0.0 

384 modExpTimes[j] += correction 

385 

386 inputAbscissa = modExpTimes 

387 else: 

388 inputAbscissa = inputPtc.rawExpTimes[ampName].copy() 

389 

390 inputOrdinate = inputPtc.rawMeans[ampName].copy() 

391 

392 mask &= (inputOrdinate < self.config.maxLinearAdu) 

393 mask &= (inputOrdinate > self.config.minLinearAdu) 

394 

395 if mask.sum() < 2: 

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

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

398 ampName, detector.getName()) 

399 continue 

400 

401 if self.config.linearityType != 'Spline': 

402 linearFit, linearFitErr, chiSq, weights = irlsFit([0.0, 100.0], inputAbscissa[mask], 

403 inputOrdinate[mask], funcPolynomial) 

404 

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

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

407 # Exclude low end outliers. 

408 # This is compared to the original values. 

409 threshold = self.config.nSigmaClipLinear * np.sqrt(abs(inputOrdinate)) 

410 

411 mask[np.abs(inputOrdinate - linearOrdinate) >= threshold] = False 

412 

413 if mask.sum() < 2: 

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

415 self.log.warning("Amp %s in detector %s has not enough points in linear ordinate. " 

416 "Skipping!", ampName, detector.getName()) 

417 continue 

418 

419 self.debugFit('linearFit', inputAbscissa, inputOrdinate, linearOrdinate, mask, ampName) 

420 

421 # Do fits 

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

423 polyFit = np.zeros(fitOrder + 1) 

424 polyFit[1] = 1.0 

425 polyFit, polyFitErr, chiSq, weights = irlsFit(polyFit, linearOrdinate[mask], 

426 inputOrdinate[mask], funcPolynomial) 

427 

428 # Truncate the polynomial fit to the squared term. 

429 k1 = polyFit[1] 

430 linearityCoeffs = np.array( 

431 [-coeff/(k1**order) for order, coeff in enumerate(polyFit)] 

432 )[2:] 

433 significant = np.where(np.abs(linearityCoeffs) > 1e-10) 

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

435 

436 modelOrdinate = funcPolynomial(polyFit, linearOrdinate) 

437 

438 self.debugFit( 

439 'polyFit', 

440 inputAbscissa[mask], 

441 inputOrdinate[mask], 

442 modelOrdinate[mask], 

443 None, 

444 ampName, 

445 ) 

446 

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

448 # The first term is the squared term. 

449 linearityCoeffs = linearityCoeffs[0: 1] 

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

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

452 # maxAduForLookupTableLinearizer DN 

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

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

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

456 signalUncorrected = funcPolynomial(polyFit, timeRange) 

457 lookupTableRow = signalIdeal - signalUncorrected # LinearizerLookupTable has correction 

458 

459 linearizer.tableData[tableIndex, :] = lookupTableRow 

460 linearityCoeffs = np.array([tableIndex, 0]) 

461 tableIndex += 1 

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

463 # This is a spline fit with photodiode data based on a model 

464 # from Pierre Astier. 

465 # This model fits a spline with (optional) nuisance parameters 

466 # to allow for different linearity coefficients with different 

467 # photodiode settings. The minimization is a least-squares 

468 # fit with the residual of 

469 # Sum[(S(mu_i) + mu_i)/(k_j * D_i) - 1]**2, where S(mu_i) is 

470 # an Akima Spline function of mu_i, the observed flat-pair 

471 # mean; D_j is the photo-diode measurement corresponding to 

472 # that flat-pair; and k_j is a constant of proportionality 

473 # which is over index j as it is allowed to 

474 # be different based on different photodiode settings (e.g. 

475 # CCOBCURR). 

476 

477 # The fit has additional constraints to ensure that the spline 

478 # goes through the (0, 0) point, as well as a normalization 

479 # condition so that the average of the spline over the full 

480 # range is 0. The normalization ensures that the spline only 

481 # fits deviations from linearity, rather than the linear 

482 # function itself which is degenerate with the gain. 

483 

484 nodes = np.linspace(0.0, np.max(inputOrdinate[mask]), self.config.splineKnots) 

485 

486 fitter = AstierSplineLinearityFitter( 

487 nodes, 

488 groupingValue, 

489 inputAbscissa, 

490 inputOrdinate, 

491 mask=mask, 

492 log=self.log, 

493 ) 

494 p0 = fitter.estimate_p0() 

495 pars = fitter.fit( 

496 p0, 

497 min_iter=self.config.splineFitMinIter, 

498 max_iter=self.config.splineFitMaxIter, 

499 max_rejection_per_iteration=self.config.splineFitMaxRejectionPerIteration, 

500 n_sigma_clip=self.config.nSigmaClipLinear, 

501 ) 

502 

503 # Confirm that the first parameter is 0, and set it to 

504 # exactly zero. 

505 if not np.isclose(pars[0], 0): 

506 raise RuntimeError("Programmer error! First spline parameter must " 

507 "be consistent with zero.") 

508 pars[0] = 0.0 

509 

510 linearityCoeffs = np.concatenate([nodes, pars[0: len(nodes)]]) 

511 linearFit = np.array([0.0, np.mean(pars[len(nodes):])]) 

512 

513 # We modify the inputAbscissa according to the linearity fits 

514 # here, for proper residual computation. 

515 for j, group_index in enumerate(fitter.group_indices): 

516 inputOrdinate[group_index] /= (pars[len(nodes) + j] / linearFit[1]) 

517 

518 linearOrdinate = linearFit[1] * inputOrdinate 

519 # For the spline fit, reuse the "polyFit -> fitParams" 

520 # field to record the linear coefficients for the groups. 

521 polyFit = pars[len(nodes):] 

522 polyFitErr = np.zeros_like(polyFit) 

523 chiSq = np.nan 

524 

525 # Update mask based on what the fitter rejected. 

526 mask = fitter.mask 

527 else: 

528 polyFit = np.zeros(1) 

529 polyFitErr = np.zeros(1) 

530 chiSq = np.nan 

531 linearityCoeffs = np.zeros(1) 

532 

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

534 linearizer.linearityCoeffs[ampName] = linearityCoeffs 

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

536 linearizer.fitParams[ampName] = polyFit 

537 linearizer.fitParamsErr[ampName] = polyFitErr 

538 linearizer.fitChiSq[ampName] = chiSq 

539 linearizer.linearFit[ampName] = linearFit 

540 

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

542 image.array[:, :] = inputOrdinate 

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

544 linearizeFunction()( 

545 image, 

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

547 'table': linearizer.tableData, 

548 'log': linearizer.log} 

549 ) 

550 linearizeModel = image.array[0, :] 

551 

552 # The residuals that we record are the final residuals compared to 

553 # a linear model, after everything has been (properly?) linearized. 

554 postLinearFit, _, _, _ = irlsFit( 

555 [0.0, 100.0], 

556 inputAbscissa[mask], 

557 linearizeModel[mask], 

558 funcPolynomial, 

559 ) 

560 residuals = linearizeModel - (postLinearFit[0] + postLinearFit[1] * inputAbscissa) 

561 # We set masked residuals to nan. 

562 residuals[~mask] = np.nan 

563 

564 linearizer.fitResiduals[ampName] = residuals 

565 

566 self.debugFit( 

567 'solution', 

568 inputOrdinate[mask], 

569 linearOrdinate[mask], 

570 linearizeModel[mask], 

571 None, 

572 ampName, 

573 ) 

574 

575 linearizer.hasLinearity = True 

576 linearizer.validate() 

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

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

579 provenance = IsrProvenance(calibType='linearizer') 

580 

581 return pipeBase.Struct( 

582 outputLinearizer=linearizer, 

583 outputProvenance=provenance, 

584 ) 

585 

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

587 # Need to fill linearizer with empty values 

588 # if the amp is non-functional 

589 ampName = amp.getName() 

590 nEntries = 1 

591 pEntries = 1 

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

593 nEntries = fitOrder + 1 

594 pEntries = fitOrder + 1 

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

596 nEntries = fitOrder * 2 

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

598 nEntries = 1 

599 pEntries = fitOrder + 1 

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

601 nEntries = 2 

602 pEntries = fitOrder + 1 

603 

604 linearizer.linearityType[ampName] = "None" 

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

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

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

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

609 linearizer.fitChiSq[ampName] = np.nan 

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

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

612 return linearizer 

613 

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

615 """Debug method for linearity fitting. 

616 

617 Parameters 

618 ---------- 

619 stepname : `str` 

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

621 line of code. 

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

623 The values to use as the independent variable in the 

624 linearity fit. 

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

626 The values to use as the dependent variable in the 

627 linearity fit. 

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

629 The values to use as the linearized result. 

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

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

632 ``yVector`` to keep. 

633 ampName : `str` 

634 Amplifier name to lookup linearity correction values. 

635 """ 

636 frame = getDebugFrame(self._display, stepname) 

637 if frame: 

638 import matplotlib.pyplot as plt 

639 fig, axs = plt.subplots(2) 

640 

641 if mask is None: 

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

643 

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

645 if stepname == 'linearFit': 

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

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

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

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

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

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

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

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

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

655 elif stepname == 'solution': 

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

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

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

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

660 

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

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

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

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

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

666 

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

668 fig.tight_layout() 

669 fig.show() 

670 

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

672 while True: 

673 ans = input(prompt).lower() 

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

675 break 

676 elif ans in ("p", ): 

677 import pdb 

678 pdb.set_trace() 

679 elif ans in ("h", ): 

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

681 elif ans in ('x', ): 

682 exit() 

683 plt.close()