Coverage for python/lsst/cp/pipe/linearity.py: 12%
263 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-27 03:56 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-27 03:56 -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#
23__all__ = ["LinearitySolveTask", "LinearitySolveConfig"]
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
31from lsstDebug import getDebugFrame
32from lsst.ip.isr import (Linearizer, IsrProvenance)
34from .utils import funcPolynomial, irlsFit, AstierSplineLinearityFitter
35from ._lookupStaticCalibration import lookupStaticCalibration
38def ptcLookup(datasetType, registry, quantumDataId, collections):
39 """Butler lookup function to allow PTC to be found.
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).
58 Returns
59 -------
60 refs : `list` [ `DatasetRef` ]
61 A zero- or single-element list containing the matching
62 dataset, if one was found.
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?")
74 return refs
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 )
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 )
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 )
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 )
115 inputPhotodiodeCorrection = cT.Input(
116 name="pdCorrection",
117 doc="Input photodiode correction.",
118 storageClass="IsrCalib",
119 dimensions=("instrument", ),
120 isCalibration=True,
121 )
123 outputLinearizer = cT.Output(
124 name="linearity",
125 doc="Output linearity measurements.",
126 storageClass="Linearizer",
127 dimensions=("instrument", "detector"),
128 isCalibration=True,
129 )
131 def __init__(self, *, config=None):
132 if not config.applyPhotodiodeCorrection:
133 del self.inputPhotodiodeCorrection
135 if not config.usePhotodiode:
136 del self.inputPhotodiodeData
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 )
247class LinearitySolveTask(pipeBase.PipelineTask):
248 """Fit the linearity from the PTC dataset.
249 """
251 ConfigClass = LinearitySolveConfig
252 _DefaultName = 'cpLinearitySolve'
254 def runQuantum(self, butlerQC, inputRefs, outputRefs):
255 """Ensure that the input and output dimensions are passed along.
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)
268 # Use the dimensions to set calib/provenance information.
269 inputs['inputDims'] = inputRefs.inputPtc.dataId.byName()
271 outputs = self.run(**inputs)
272 butlerQC.put(outputs, outputRefs)
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.
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.
297 Returns
298 -------
299 results : `lsst.pipe.base.Struct`
300 The results struct containing:
302 ``outputLinearizer``
303 Final linearizer calibration (`lsst.ip.isr.Linearizer`).
304 ``outputProvenance``
305 Provenance data for the new calibration
306 (`lsst.ip.isr.IsrProvenance`).
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.")
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.
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
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
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
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()
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
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])
404 inputOrdinate = inputPtc.rawMeans[ampName].copy()
406 mask &= (inputOrdinate < self.config.maxLinearAdu)
407 mask &= (inputOrdinate > self.config.minLinearAdu)
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
415 if self.config.linearityType != 'Spline':
416 linearFit, linearFitErr, chiSq, weights = irlsFit([0.0, 100.0], inputAbscissa[mask],
417 inputOrdinate[mask], funcPolynomial)
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))
425 mask[np.abs(inputOrdinate - linearOrdinate) >= threshold] = False
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
433 self.debugFit('linearFit', inputAbscissa, inputOrdinate, linearOrdinate, mask, ampName)
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)
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)
450 modelOrdinate = funcPolynomial(polyFit, linearOrdinate)
452 self.debugFit(
453 'polyFit',
454 inputAbscissa[mask],
455 inputOrdinate[mask],
456 modelOrdinate[mask],
457 None,
458 ampName,
459 )
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
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).
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.
498 nodes = np.linspace(0.0, inputOrdinate.max(), self.config.splineKnots)
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 )
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
524 linearityCoeffs = np.concatenate([nodes, pars[0: len(nodes)]])
525 linearFit = np.array([0.0, np.mean(pars[len(nodes):])])
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])
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
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)
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
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, :]
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
578 linearizer.fitResiduals[ampName] = residuals
580 self.debugFit(
581 'solution',
582 inputOrdinate[mask],
583 linearOrdinate[mask],
584 linearizeModel[mask],
585 None,
586 ampName,
587 )
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')
595 return pipeBase.Struct(
596 outputLinearizer=linearizer,
597 outputProvenance=provenance,
598 )
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
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
628 def debugFit(self, stepname, xVector, yVector, yModel, mask, ampName):
629 """Debug method for linearity fitting.
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)
655 if mask is None:
656 mask = np.ones_like(xVector, dtype=bool)
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)")
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')
681 axs[1].scatter(yModel, yVector[mask] - yModel)
682 fig.tight_layout()
683 fig.show()
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()