Coverage for python/lsst/cp/pipe/linearity.py: 13%
244 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-16 12:41 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-16 12:41 +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#
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
37def ptcLookup(datasetType, registry, quantumDataId, collections):
38 """Butler lookup function to allow PTC to be found.
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).
57 Returns
58 -------
59 refs : `list` [ `DatasetRef` ]
60 A zero- or single-element list containing the matching
61 dataset, if one was found.
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?")
73 return refs
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 )
87 camera = cT.PrerequisiteInput(
88 name="camera",
89 doc="Camera Geometry definition.",
90 storageClass="Camera",
91 dimensions=("instrument", ),
92 isCalibration=True,
93 )
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 )
104 inputPhotodiodeCorrection = cT.Input(
105 name="pdCorrection",
106 doc="Input photodiode correction.",
107 storageClass="IsrCalib",
108 dimensions=("instrument", ),
109 isCalibration=True,
110 )
112 outputLinearizer = cT.Output(
113 name="linearity",
114 doc="Output linearity measurements.",
115 storageClass="Linearizer",
116 dimensions=("instrument", "detector"),
117 isCalibration=True,
118 )
120 def __init__(self, *, config=None):
121 if not config.applyPhotodiodeCorrection:
122 del self.inputPhotodiodeCorrection
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 )
244class LinearitySolveTask(pipeBase.PipelineTask):
245 """Fit the linearity from the PTC dataset.
246 """
248 ConfigClass = LinearitySolveConfig
249 _DefaultName = 'cpLinearitySolve'
251 def runQuantum(self, butlerQC, inputRefs, outputRefs):
252 """Ensure that the input and output dimensions are passed along.
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)
265 # Use the dimensions to set calib/provenance information.
266 inputs['inputDims'] = inputRefs.inputPtc.dataId.byName()
268 outputs = self.run(**inputs)
269 butlerQC.put(outputs, outputRefs)
271 def run(self, inputPtc, dummy, camera, inputDims,
272 inputPhotodiodeCorrection=None):
273 """Fit non-linearity to PTC data, returning the correct Linearizer
274 object.
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.
292 Returns
293 -------
294 results : `lsst.pipe.base.Struct`
295 The results struct containing:
297 ``outputLinearizer``
298 Final linearizer calibration (`lsst.ip.isr.Linearizer`).
299 ``outputProvenance``
300 Provenance data for the new calibration
301 (`lsst.ip.isr.IsrProvenance`).
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.")
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.
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
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
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
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.")
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()
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
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
386 inputAbscissa = modExpTimes
387 else:
388 inputAbscissa = inputPtc.rawExpTimes[ampName].copy()
390 inputOrdinate = inputPtc.rawMeans[ampName].copy()
392 mask &= (inputOrdinate < self.config.maxLinearAdu)
393 mask &= (inputOrdinate > self.config.minLinearAdu)
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
401 if self.config.linearityType != 'Spline':
402 linearFit, linearFitErr, chiSq, weights = irlsFit([0.0, 100.0], inputAbscissa[mask],
403 inputOrdinate[mask], funcPolynomial)
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))
411 mask[np.abs(inputOrdinate - linearOrdinate) >= threshold] = False
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
419 self.debugFit('linearFit', inputAbscissa, inputOrdinate, linearOrdinate, mask, ampName)
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)
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)
436 modelOrdinate = funcPolynomial(polyFit, linearOrdinate)
438 self.debugFit(
439 'polyFit',
440 inputAbscissa[mask],
441 inputOrdinate[mask],
442 modelOrdinate[mask],
443 None,
444 ampName,
445 )
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
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).
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.
484 nodes = np.linspace(0.0, np.max(inputOrdinate[mask]), self.config.splineKnots)
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 )
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
510 linearityCoeffs = np.concatenate([nodes, pars[0: len(nodes)]])
511 linearFit = np.array([0.0, np.mean(pars[len(nodes):])])
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])
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
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)
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
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, :]
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
564 linearizer.fitResiduals[ampName] = residuals
566 self.debugFit(
567 'solution',
568 inputOrdinate[mask],
569 linearOrdinate[mask],
570 linearizeModel[mask],
571 None,
572 ampName,
573 )
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')
581 return pipeBase.Struct(
582 outputLinearizer=linearizer,
583 outputProvenance=provenance,
584 )
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
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
614 def debugFit(self, stepname, xVector, yVector, yModel, mask, ampName):
615 """Debug method for linearity fitting.
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)
641 if mask is None:
642 mask = np.ones_like(xVector, dtype=bool)
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)")
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')
667 axs[1].scatter(yModel, yVector[mask] - yModel)
668 fig.tight_layout()
669 fig.show()
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()