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

263 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-28 04:29 -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# 

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 

35from ._lookupStaticCalibration import lookupStaticCalibration 

36 

37 

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

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

40 

41 Parameters 

42 ---------- 

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

44 Dataset type to look up. 

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

46 Registry for the data repository being searched. 

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

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

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

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

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

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

53 would allow a real validity-range lookup. 

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

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

56 by this function (see notes below). 

57 

58 Returns 

59 ------- 

60 refs : `list` [ `DatasetRef` ] 

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

62 dataset, if one was found. 

63 

64 Raises 

65 ------ 

66 RuntimeError 

67 Raised if more than one PTC reference is found. 

68 """ 

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

70 findFirst=False)) 

71 if len(refs) >= 2: 

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

73 

74 return refs 

75 

76 

77class LinearitySolveConnections(pipeBase.PipelineTaskConnections, 

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

79 dummy = cT.Input( 

80 name="raw", 

81 doc="Dummy exposure.", 

82 storageClass='Exposure', 

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

84 multiple=True, 

85 deferLoad=True, 

86 ) 

87 

88 camera = cT.PrerequisiteInput( 

89 name="camera", 

90 doc="Camera Geometry definition.", 

91 storageClass="Camera", 

92 dimensions=("instrument", ), 

93 isCalibration=True, 

94 lookupFunction=lookupStaticCalibration, 

95 ) 

96 

97 inputPtc = cT.PrerequisiteInput( 

98 name="ptc", 

99 doc="Input PTC dataset.", 

100 storageClass="PhotonTransferCurveDataset", 

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

102 isCalibration=True, 

103 lookupFunction=ptcLookup, 

104 ) 

105 

106 inputPhotodiodeData = cT.Input( 

107 name="photodiode", 

108 doc="Photodiode readings data.", 

109 storageClass="IsrCalib", 

110 dimensions=("instrument", "exposure"), 

111 multiple=True, 

112 deferLoad=True, 

113 ) 

114 

115 inputPhotodiodeCorrection = cT.Input( 

116 name="pdCorrection", 

117 doc="Input photodiode correction.", 

118 storageClass="IsrCalib", 

119 dimensions=("instrument", ), 

120 isCalibration=True, 

121 ) 

122 

123 outputLinearizer = cT.Output( 

124 name="linearity", 

125 doc="Output linearity measurements.", 

126 storageClass="Linearizer", 

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

128 isCalibration=True, 

129 ) 

130 

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

132 if not config.applyPhotodiodeCorrection: 

133 del self.inputPhotodiodeCorrection 

134 

135 if not config.usePhotodiode: 

136 del self.inputPhotodiodeData 

137 

138 

139class LinearitySolveConfig(pipeBase.PipelineTaskConfig, 

140 pipelineConnections=LinearitySolveConnections): 

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

142 """ 

143 linearityType = pexConfig.ChoiceField( 

144 dtype=str, 

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

146 default="Squared", 

147 allowed={ 

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

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

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

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

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

153 } 

154 ) 

155 polynomialOrder = pexConfig.RangeField( 

156 dtype=int, 

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

158 default=3, 

159 min=2, 

160 ) 

161 splineKnots = pexConfig.Field( 

162 dtype=int, 

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

164 default=10, 

165 ) 

166 maxLookupTableAdu = pexConfig.Field( 

167 dtype=int, 

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

169 default=2**18, 

170 ) 

171 maxLinearAdu = pexConfig.Field( 

172 dtype=float, 

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

174 default=20000.0, 

175 ) 

176 minLinearAdu = pexConfig.Field( 

177 dtype=float, 

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

179 default=30.0, 

180 ) 

181 nSigmaClipLinear = pexConfig.Field( 

182 dtype=float, 

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

184 default=5.0, 

185 ) 

186 ignorePtcMask = pexConfig.Field( 

187 dtype=bool, 

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

189 default=False, 

190 ) 

191 usePhotodiode = pexConfig.Field( 

192 dtype=bool, 

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

194 default=False, 

195 ) 

196 photodiodeIntegrationMethod = pexConfig.ChoiceField( 

197 dtype=str, 

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

199 default="DIRECT_SUM", 

200 allowed={ 

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

202 "readout entries"), 

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

204 "leading and trailing entries, which are " 

205 "nominally at zero baseline level."), 

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

207 "over the sampling interval and simply sum " 

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

209 } 

210 ) 

211 photodiodeCurrentScale = pexConfig.Field( 

212 dtype=float, 

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

214 "``CHARGE_SUM`` integration method.", 

215 default=-1.0, 

216 ) 

217 applyPhotodiodeCorrection = pexConfig.Field( 

218 dtype=bool, 

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

220 default=False, 

221 ) 

222 splineGroupingColumn = pexConfig.Field( 

223 dtype=str, 

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

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

226 "will be done.", 

227 default=None, 

228 optional=True, 

229 ) 

230 splineFitMinIter = pexConfig.Field( 

231 dtype=int, 

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

233 default=3, 

234 ) 

235 splineFitMaxIter = pexConfig.Field( 

236 dtype=int, 

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

238 default=20, 

239 ) 

240 splineFitMaxRejectionPerIteration = pexConfig.Field( 

241 dtype=int, 

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

243 default=5, 

244 ) 

245 

246 

247class LinearitySolveTask(pipeBase.PipelineTask): 

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

249 """ 

250 

251 ConfigClass = LinearitySolveConfig 

252 _DefaultName = 'cpLinearitySolve' 

253 

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

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

256 

257 Parameters 

258 ---------- 

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

260 Butler to operate on. 

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

262 Input data refs to load. 

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

264 Output data refs to persist. 

265 """ 

266 inputs = butlerQC.get(inputRefs) 

267 

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

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

270 

271 outputs = self.run(**inputs) 

272 butlerQC.put(outputs, outputRefs) 

273 

274 def run(self, inputPtc, dummy, camera, inputDims, inputPhotodiodeData=None, 

275 inputPhotodiodeCorrection=None): 

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

277 object. 

278 

279 Parameters 

280 ---------- 

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

282 Pre-measured PTC dataset. 

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

284 The exposure used to select the appropriate PTC dataset. 

285 In almost all circumstances, one of the input exposures 

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

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

288 Pre-measured photodiode correction used in the case when 

289 applyPhotodiodeCorrection=True. 

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

291 Camera geometry. 

292 inputPhotodiodeData : `dict` [`str`, `lsst.ip.isr.PhotodiodeCalib`] 

293 Photodiode readings data. 

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

295 DataIds to use to populate the output calibration. 

296 

297 Returns 

298 ------- 

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

300 The results struct containing: 

301 

302 ``outputLinearizer`` 

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

304 ``outputProvenance`` 

305 Provenance data for the new calibration 

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

307 

308 Notes 

309 ----- 

310 This task currently fits only polynomial-defined corrections, 

311 where the correction coefficients are defined such that: 

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

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

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

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

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

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

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

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

320 """ 

321 if len(dummy) == 0: 

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

323 

324 detector = camera[inputDims['detector']] 

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

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

327 tableIndex = 0 

328 else: 

329 table = None 

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

331 

332 # Initialize the linearizer. 

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

334 linearizer.updateMetadataFromExposures([inputPtc]) 

335 if self.config.usePhotodiode: 

336 # Compute the photodiode integrals once, outside the loop 

337 # over amps. 

338 monDiodeCharge = {} 

339 for handle in inputPhotodiodeData: 

340 expId = handle.dataId['exposure'] 

341 pd_calib = handle.get() 

342 pd_calib.integrationMethod = self.config.photodiodeIntegrationMethod 

343 pd_calib.currentScale = self.config.photodiodeCurrentScale 

344 monDiodeCharge[expId] = pd_calib.integrate() 

345 if self.config.applyPhotodiodeCorrection: 

346 abscissaCorrections = inputPhotodiodeCorrection.abscissaCorrections 

347 

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

349 if self.config.splineGroupingColumn is not None: 

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

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

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

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

354 else: 

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

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

357 fitOrder = self.config.splineKnots 

358 else: 

359 fitOrder = self.config.polynomialOrder 

360 

361 for i, amp in enumerate(detector): 

362 ampName = amp.getName() 

363 if ampName in inputPtc.badAmps: 

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

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

366 ampName, detector.getName()) 

367 continue 

368 

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

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

371 ampName, detector.getName()) 

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

373 else: 

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

375 

376 if self.config.usePhotodiode: 

377 modExpTimes = [] 

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

379 modExpTime = 0.0 

380 nExps = 0 

381 for k in range(2): 

382 expId = pair[k] 

383 if expId in monDiodeCharge: 

384 modExpTime += monDiodeCharge[expId] 

385 nExps += 1 

386 if nExps > 0: 

387 modExpTime = modExpTime / nExps 

388 else: 

389 mask[j] = False 

390 

391 # Get the photodiode correction 

392 if self.config.applyPhotodiodeCorrection: 

393 try: 

394 correction = abscissaCorrections[str(pair)] 

395 except KeyError: 

396 correction = 0.0 

397 else: 

398 correction = 0.0 

399 modExpTimes.append(modExpTime + correction) 

400 inputAbscissa = np.array(modExpTimes) 

401 else: 

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

403 

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

405 

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

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

408 

409 if mask.sum() < 2: 

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

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

412 ampName, detector.getName()) 

413 continue 

414 

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

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

417 inputOrdinate[mask], funcPolynomial) 

418 

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

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

421 # Exclude low end outliers. 

422 # This is compared to the original values. 

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

424 

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

426 

427 if mask.sum() < 2: 

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

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

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

431 continue 

432 

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

434 

435 # Do fits 

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

437 polyFit = np.zeros(fitOrder + 1) 

438 polyFit[1] = 1.0 

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

440 inputOrdinate[mask], funcPolynomial) 

441 

442 # Truncate the polynomial fit to the squared term. 

443 k1 = polyFit[1] 

444 linearityCoeffs = np.array( 

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

446 )[2:] 

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

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

449 

450 modelOrdinate = funcPolynomial(polyFit, linearOrdinate) 

451 

452 self.debugFit( 

453 'polyFit', 

454 inputAbscissa[mask], 

455 inputOrdinate[mask], 

456 modelOrdinate[mask], 

457 None, 

458 ampName, 

459 ) 

460 

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

462 # The first term is the squared term. 

463 linearityCoeffs = linearityCoeffs[0: 1] 

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

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

466 # maxAduForLookupTableLinearizer DN 

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

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

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

470 signalUncorrected = funcPolynomial(polyFit, timeRange) 

471 lookupTableRow = signalIdeal - signalUncorrected # LinearizerLookupTable has correction 

472 

473 linearizer.tableData[tableIndex, :] = lookupTableRow 

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

475 tableIndex += 1 

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

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

478 # from Pierre Astier. 

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

480 # to allow for different linearity coefficients with different 

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

482 # fit with the residual of 

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

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

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

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

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

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

489 # CCOBCURR). 

490 

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

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

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

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

495 # fits deviations from linearity, rather than the linear 

496 # function itself which is degenerate with the gain. 

497 

498 nodes = np.linspace(0.0, inputOrdinate.max(), self.config.splineKnots) 

499 

500 fitter = AstierSplineLinearityFitter( 

501 nodes, 

502 groupingValue, 

503 inputAbscissa, 

504 inputOrdinate, 

505 mask=mask, 

506 log=self.log, 

507 ) 

508 p0 = fitter.estimate_p0() 

509 pars = fitter.fit( 

510 p0, 

511 min_iter=self.config.splineFitMinIter, 

512 max_iter=self.config.splineFitMaxIter, 

513 max_rejection_per_iteration=self.config.splineFitMaxRejectionPerIteration, 

514 n_sigma_clip=self.config.nSigmaClipLinear, 

515 ) 

516 

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

518 # exactly zero. 

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

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

521 "be consistent with zero.") 

522 pars[0] = 0.0 

523 

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

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

526 

527 # We modify the inputAbscissa according to the linearity fits 

528 # here, for proper residual computation. 

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

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

531 

532 linearOrdinate = linearFit[1] * inputOrdinate 

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

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

535 polyFit = pars[len(nodes):] 

536 polyFitErr = np.zeros_like(polyFit) 

537 chiSq = np.nan 

538 

539 # Update mask based on what the fitter rejected. 

540 mask = fitter.mask 

541 else: 

542 polyFit = np.zeros(1) 

543 polyFitErr = np.zeros(1) 

544 chiSq = np.nan 

545 linearityCoeffs = np.zeros(1) 

546 

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

548 linearizer.linearityCoeffs[ampName] = linearityCoeffs 

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

550 linearizer.fitParams[ampName] = polyFit 

551 linearizer.fitParamsErr[ampName] = polyFitErr 

552 linearizer.fitChiSq[ampName] = chiSq 

553 linearizer.linearFit[ampName] = linearFit 

554 

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

556 image.array[:, :] = inputOrdinate 

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

558 linearizeFunction()( 

559 image, 

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

561 'table': linearizer.tableData, 

562 'log': linearizer.log} 

563 ) 

564 linearizeModel = image.array[0, :] 

565 

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

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

568 postLinearFit, _, _, _ = irlsFit( 

569 [0.0, 100.0], 

570 inputAbscissa[mask], 

571 linearizeModel[mask], 

572 funcPolynomial, 

573 ) 

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

575 # We set masked residuals to nan. 

576 residuals[~mask] = np.nan 

577 

578 linearizer.fitResiduals[ampName] = residuals 

579 

580 self.debugFit( 

581 'solution', 

582 inputOrdinate[mask], 

583 linearOrdinate[mask], 

584 linearizeModel[mask], 

585 None, 

586 ampName, 

587 ) 

588 

589 linearizer.hasLinearity = True 

590 linearizer.validate() 

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

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

593 provenance = IsrProvenance(calibType='linearizer') 

594 

595 return pipeBase.Struct( 

596 outputLinearizer=linearizer, 

597 outputProvenance=provenance, 

598 ) 

599 

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

601 # Need to fill linearizer with empty values 

602 # if the amp is non-functional 

603 ampName = amp.getName() 

604 nEntries = 1 

605 pEntries = 1 

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

607 nEntries = fitOrder + 1 

608 pEntries = fitOrder + 1 

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

610 nEntries = fitOrder * 2 

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

612 nEntries = 1 

613 pEntries = fitOrder + 1 

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

615 nEntries = 2 

616 pEntries = fitOrder + 1 

617 

618 linearizer.linearityType[ampName] = "None" 

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

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

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

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

623 linearizer.fitChiSq[ampName] = np.nan 

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

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

626 return linearizer 

627 

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

629 """Debug method for linearity fitting. 

630 

631 Parameters 

632 ---------- 

633 stepname : `str` 

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

635 line of code. 

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

637 The values to use as the independent variable in the 

638 linearity fit. 

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

640 The values to use as the dependent variable in the 

641 linearity fit. 

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

643 The values to use as the linearized result. 

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

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

646 ``yVector`` to keep. 

647 ampName : `str` 

648 Amplifier name to lookup linearity correction values. 

649 """ 

650 frame = getDebugFrame(self._display, stepname) 

651 if frame: 

652 import matplotlib.pyplot as plt 

653 fig, axs = plt.subplots(2) 

654 

655 if mask is None: 

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

657 

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

659 if stepname == 'linearFit': 

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

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

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

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

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

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

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

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

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

669 elif stepname == 'solution': 

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

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

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

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

674 

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

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

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

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

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

680 

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

682 fig.tight_layout() 

683 fig.show() 

684 

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

686 while True: 

687 ans = input(prompt).lower() 

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

689 break 

690 elif ans in ("p", ): 

691 import pdb 

692 pdb.set_trace() 

693 elif ans in ("h", ): 

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

695 elif ans in ('x', ): 

696 exit() 

697 plt.close()