Coverage for python/lsst/cp/pipe/linearity.py: 11%
270 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-09 03:02 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-09 03:02 -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.afw.math as afwMath
28import lsst.pipe.base as pipeBase
29import lsst.pipe.base.connectionTypes as cT
30import lsst.pex.config as pexConfig
32from lsstDebug import getDebugFrame
33from lsst.ip.isr import (Linearizer, IsrProvenance)
35from .utils import (funcPolynomial, irlsFit)
36from ._lookupStaticCalibration import lookupStaticCalibration
39def ptcLookup(datasetType, registry, quantumDataId, collections):
40 """Butler lookup function to allow PTC to be found.
42 Parameters
43 ----------
44 datasetType : `lsst.daf.butler.DatasetType`
45 Dataset type to look up.
46 registry : `lsst.daf.butler.Registry`
47 Registry for the data repository being searched.
48 quantumDataId : `lsst.daf.butler.DataCoordinate`
49 Data ID for the quantum of the task this dataset will be passed to.
50 This must include an "instrument" key, and should also include any
51 keys that are present in ``datasetType.dimensions``. If it has an
52 ``exposure`` or ``visit`` key, that's a sign that this function is
53 not actually needed, as those come with the temporal information that
54 would allow a real validity-range lookup.
55 collections : `lsst.daf.butler.registry.CollectionSearch`
56 Collections passed by the user when generating a QuantumGraph. Ignored
57 by this function (see notes below).
59 Returns
60 -------
61 refs : `list` [ `DatasetRef` ]
62 A zero- or single-element list containing the matching
63 dataset, if one was found.
65 Raises
66 ------
67 RuntimeError
68 Raised if more than one PTC reference is found.
69 """
70 refs = list(registry.queryDatasets(datasetType, dataId=quantumDataId, collections=collections,
71 findFirst=False))
72 if len(refs) >= 2:
73 RuntimeError("Too many PTC connections found. Incorrect collections supplied?")
75 return refs
78class LinearitySolveConnections(pipeBase.PipelineTaskConnections,
79 dimensions=("instrument", "detector")):
80 dummy = cT.Input(
81 name="raw",
82 doc="Dummy exposure.",
83 storageClass='Exposure',
84 dimensions=("instrument", "exposure", "detector"),
85 multiple=True,
86 deferLoad=True,
87 )
89 camera = cT.PrerequisiteInput(
90 name="camera",
91 doc="Camera Geometry definition.",
92 storageClass="Camera",
93 dimensions=("instrument", ),
94 isCalibration=True,
95 lookupFunction=lookupStaticCalibration,
96 )
98 inputPtc = cT.PrerequisiteInput(
99 name="ptc",
100 doc="Input PTC dataset.",
101 storageClass="PhotonTransferCurveDataset",
102 dimensions=("instrument", "detector"),
103 isCalibration=True,
104 lookupFunction=ptcLookup,
105 )
107 inputPhotodiodeData = cT.PrerequisiteInput(
108 name="photodiode",
109 doc="Photodiode readings data.",
110 storageClass="IsrCalib",
111 dimensions=("instrument", "exposure"),
112 multiple=True,
113 deferLoad=True,
114 minimum=0,
115 )
117 inputPhotodiodeCorrection = cT.Input(
118 name="pdCorrection",
119 doc="Input photodiode correction.",
120 storageClass="IsrCalib",
121 dimensions=("instrument", ),
122 isCalibration=True,
123 )
125 outputLinearizer = cT.Output(
126 name="linearity",
127 doc="Output linearity measurements.",
128 storageClass="Linearizer",
129 dimensions=("instrument", "detector"),
130 isCalibration=True,
131 )
133 def __init__(self, *, config=None):
134 super().__init__(config=config)
136 if config.applyPhotodiodeCorrection is not True:
137 self.inputs.discard("inputPhotodiodeCorrection")
139 if config.usePhotodiode is not True:
140 self.inputs.discard("inputPhotodiodeData")
143class LinearitySolveConfig(pipeBase.PipelineTaskConfig,
144 pipelineConnections=LinearitySolveConnections):
145 """Configuration for solving the linearity from PTC dataset.
146 """
147 linearityType = pexConfig.ChoiceField(
148 dtype=str,
149 doc="Type of linearizer to construct.",
150 default="Squared",
151 allowed={
152 "LookupTable": "Create a lookup table solution.",
153 "Polynomial": "Create an arbitrary polynomial solution.",
154 "Squared": "Create a single order squared solution.",
155 "Spline": "Create a spline based solution.",
156 "None": "Create a dummy solution.",
157 }
158 )
159 polynomialOrder = pexConfig.Field(
160 dtype=int,
161 doc="Degree of polynomial to fit.",
162 default=3,
163 )
164 splineKnots = pexConfig.Field(
165 dtype=int,
166 doc="Number of spline knots to use in fit.",
167 default=10,
168 )
169 maxLookupTableAdu = pexConfig.Field(
170 dtype=int,
171 doc="Maximum DN value for a LookupTable linearizer.",
172 default=2**18,
173 )
174 maxLinearAdu = pexConfig.Field(
175 dtype=float,
176 doc="Maximum DN value to use to estimate linear term.",
177 default=20000.0,
178 )
179 minLinearAdu = pexConfig.Field(
180 dtype=float,
181 doc="Minimum DN value to use to estimate linear term.",
182 default=30.0,
183 )
184 nSigmaClipLinear = pexConfig.Field(
185 dtype=float,
186 doc="Maximum deviation from linear solution for Poissonian noise.",
187 default=5.0,
188 )
189 ignorePtcMask = pexConfig.Field(
190 dtype=bool,
191 doc="Ignore the expIdMask set by the PTC solver?",
192 default=False,
193 )
194 usePhotodiode = pexConfig.Field(
195 dtype=bool,
196 doc="Use the photodiode info instead of the raw expTimes?",
197 default=False,
198 )
199 photodiodeIntegrationMethod = pexConfig.ChoiceField(
200 dtype=str,
201 doc="Integration method for photodiode monitoring data.",
202 default="DIRECT_SUM",
203 allowed={
204 "DIRECT_SUM": ("Use numpy's trapz integrator on all photodiode "
205 "readout entries"),
206 "TRIMMED_SUM": ("Use numpy's trapz integrator, clipping the "
207 "leading and trailing entries, which are "
208 "nominally at zero baseline level."),
209 }
210 )
211 applyPhotodiodeCorrection = pexConfig.Field(
212 dtype=bool,
213 doc="Calculate and apply a correction to the photodiode readings?",
214 default=False,
215 )
218class LinearitySolveTask(pipeBase.PipelineTask):
219 """Fit the linearity from the PTC dataset.
220 """
222 ConfigClass = LinearitySolveConfig
223 _DefaultName = 'cpLinearitySolve'
225 def runQuantum(self, butlerQC, inputRefs, outputRefs):
226 """Ensure that the input and output dimensions are passed along.
228 Parameters
229 ----------
230 butlerQC : `lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
231 Butler to operate on.
232 inputRefs : `lsst.pipe.base.connections.InputQuantizedConnection`
233 Input data refs to load.
234 ouptutRefs : `lsst.pipe.base.connections.OutputQuantizedConnection`
235 Output data refs to persist.
236 """
237 inputs = butlerQC.get(inputRefs)
239 # Use the dimensions to set calib/provenance information.
240 inputs['inputDims'] = inputRefs.inputPtc.dataId.byName()
242 outputs = self.run(**inputs)
243 butlerQC.put(outputs, outputRefs)
245 def run(self, inputPtc, dummy, camera, inputDims, inputPhotodiodeData=None,
246 inputPhotodiodeCorrection=None):
247 """Fit non-linearity to PTC data, returning the correct Linearizer
248 object.
250 Parameters
251 ----------
252 inputPtc : `lsst.ip.isr.PtcDataset`
253 Pre-measured PTC dataset.
254 dummy : `lsst.afw.image.Exposure`
255 The exposure used to select the appropriate PTC dataset.
256 In almost all circumstances, one of the input exposures
257 used to generate the PTC dataset is the best option.
258 inputPhotodiodeCorrection : `lsst.ip.isr.PhotodiodeCorrection`
259 Pre-measured photodiode correction used in the case when
260 applyPhotodiodeCorrection=True.
261 camera : `lsst.afw.cameraGeom.Camera`
262 Camera geometry.
263 inputPhotodiodeData : `dict` [`str`, `lsst.ip.isr.PhotodiodeCalib`]
264 Photodiode readings data.
265 inputDims : `lsst.daf.butler.DataCoordinate` or `dict`
266 DataIds to use to populate the output calibration.
268 Returns
269 -------
270 results : `lsst.pipe.base.Struct`
271 The results struct containing:
273 ``outputLinearizer``
274 Final linearizer calibration (`lsst.ip.isr.Linearizer`).
275 ``outputProvenance``
276 Provenance data for the new calibration
277 (`lsst.ip.isr.IsrProvenance`).
279 Notes
280 -----
281 This task currently fits only polynomial-defined corrections,
282 where the correction coefficients are defined such that:
283 :math:`corrImage = uncorrImage + \\sum_i c_i uncorrImage^(2 + i)`
284 These :math:`c_i` are defined in terms of the direct polynomial fit:
285 :math:`meanVector ~ P(x=timeVector) = \\sum_j k_j x^j`
286 such that :math:`c_(j-2) = -k_j/(k_1^j)` in units of DN^(1-j) (c.f.,
287 Eq. 37 of 2003.05978). The `config.polynomialOrder` or
288 `config.splineKnots` define the maximum order of :math:`x^j` to fit.
289 As :math:`k_0` and :math:`k_1` are degenerate with bias level and gain,
290 they are not included in the non-linearity correction.
291 """
292 if len(dummy) == 0:
293 self.log.warning("No dummy exposure found.")
295 detector = camera[inputDims['detector']]
296 if self.config.linearityType == 'LookupTable':
297 table = np.zeros((len(detector), self.config.maxLookupTableAdu), dtype=np.float32)
298 tableIndex = 0
299 else:
300 table = None
301 tableIndex = None # This will fail if we increment it.
303 if self.config.linearityType == 'Spline':
304 fitOrder = self.config.splineKnots
305 else:
306 fitOrder = self.config.polynomialOrder
308 # Initialize the linearizer.
309 linearizer = Linearizer(detector=detector, table=table, log=self.log)
310 linearizer.updateMetadataFromExposures([inputPtc])
311 if self.config.usePhotodiode:
312 # Compute the photodiode integrals once, outside the loop
313 # over amps.
314 monDiodeCharge = {}
315 for handle in inputPhotodiodeData:
316 expId = handle.dataId['exposure']
317 pd_calib = handle.get()
318 pd_calib.integrationMethod = self.config.photodiodeIntegrationMethod
319 monDiodeCharge[expId] = pd_calib.integrate()[0]
320 if self.config.applyPhotodiodeCorrection:
321 abscissaCorrections = inputPhotodiodeCorrection.abscissaCorrections
323 for i, amp in enumerate(detector):
324 ampName = amp.getName()
325 if ampName in inputPtc.badAmps:
326 linearizer = self.fillBadAmp(linearizer, fitOrder, inputPtc, amp)
327 self.log.warning("Amp %s in detector %s has no usable PTC information. Skipping!",
328 ampName, detector.getName())
329 continue
331 if (len(inputPtc.expIdMask[ampName]) == 0) or self.config.ignorePtcMask:
332 self.log.warning("Mask not found for %s in detector %s in fit. Using all points.",
333 ampName, detector.getName())
334 mask = np.repeat(True, len(inputPtc.expIdMask[ampName]))
335 else:
336 mask = np.array(inputPtc.expIdMask[ampName], dtype=bool)
338 if self.config.usePhotodiode:
339 modExpTimes = []
340 for i, pair in enumerate(inputPtc.inputExpIdPairs[ampName]):
341 pair = pair[0]
342 modExpTime = 0.0
343 nExps = 0
344 for j in range(2):
345 expId = pair[j]
346 if expId in monDiodeCharge:
347 modExpTime += monDiodeCharge[expId]
348 nExps += 1
349 if nExps > 0:
350 modExpTime = modExpTime / nExps
351 else:
352 mask[i] = False
354 # Get the photodiode correction
355 if self.config.applyPhotodiodeCorrection:
356 try:
357 correction = abscissaCorrections[str(pair)]
358 except KeyError:
359 correction = 0.0
360 else:
361 correction = 0.0
362 modExpTimes.append(modExpTime + correction)
363 inputAbscissa = np.array(modExpTimes)[mask]
364 else:
365 inputAbscissa = np.array(inputPtc.rawExpTimes[ampName])[mask]
367 inputOrdinate = np.array(inputPtc.rawMeans[ampName])[mask]
368 # Determine proxy-to-linear-flux transformation
369 fluxMask = inputOrdinate < self.config.maxLinearAdu
370 lowMask = inputOrdinate > self.config.minLinearAdu
371 fluxMask = fluxMask & lowMask
372 linearAbscissa = inputAbscissa[fluxMask]
373 linearOrdinate = inputOrdinate[fluxMask]
374 if len(linearAbscissa) < 2:
375 linearizer = self.fillBadAmp(linearizer, fitOrder, inputPtc, amp)
376 self.log.warning("Amp %s in detector %s has not enough points for linear fit. Skipping!",
377 ampName, detector.getName())
378 continue
380 linearFit, linearFitErr, chiSq, weights = irlsFit([0.0, 100.0], linearAbscissa,
381 linearOrdinate, funcPolynomial)
382 # Convert this proxy-to-flux fit into an expected linear flux
383 linearOrdinate = linearFit[0] + linearFit[1] * inputAbscissa
384 # Exclude low end outliers
385 threshold = self.config.nSigmaClipLinear * np.sqrt(abs(linearOrdinate))
386 fluxMask = np.abs(inputOrdinate - linearOrdinate) < threshold
387 linearOrdinate = linearOrdinate[fluxMask]
388 fitOrdinate = inputOrdinate[fluxMask]
389 fitAbscissa = inputAbscissa[fluxMask]
390 if len(linearOrdinate) < 2:
391 linearizer = self.fillBadAmp(linearizer, fitOrder, inputPtc, amp)
392 self.log.warning("Amp %s in detector %s has not enough points in linear ordinate. Skipping!",
393 ampName, detector.getName())
394 continue
396 self.debugFit('linearFit', inputAbscissa, inputOrdinate, linearOrdinate, fluxMask, ampName)
397 # Do fits
398 if self.config.linearityType in ['Polynomial', 'Squared', 'LookupTable']:
399 polyFit = np.zeros(fitOrder + 1)
400 polyFit[1] = 1.0
401 polyFit, polyFitErr, chiSq, weights = irlsFit(polyFit, linearOrdinate,
402 fitOrdinate, funcPolynomial)
404 # Truncate the polynomial fit
405 k1 = polyFit[1]
406 linearityFit = [-coeff/(k1**order) for order, coeff in enumerate(polyFit)]
407 significant = np.where(np.abs(linearityFit) > 1e-10, True, False)
408 self.log.info("Significant polynomial fits: %s", significant)
410 modelOrdinate = funcPolynomial(polyFit, fitAbscissa)
412 self.debugFit('polyFit', linearAbscissa, fitOrdinate, modelOrdinate, None, ampName)
414 if self.config.linearityType == 'Squared':
415 linearityFit = [linearityFit[2]]
416 elif self.config.linearityType == 'LookupTable':
417 # Use linear part to get time at which signal is
418 # maxAduForLookupTableLinearizer DN
419 tMax = (self.config.maxLookupTableAdu - polyFit[0])/polyFit[1]
420 timeRange = np.linspace(0, tMax, self.config.maxLookupTableAdu)
421 signalIdeal = polyFit[0] + polyFit[1]*timeRange
422 signalUncorrected = funcPolynomial(polyFit, timeRange)
423 lookupTableRow = signalIdeal - signalUncorrected # LinearizerLookupTable has correction
425 linearizer.tableData[tableIndex, :] = lookupTableRow
426 linearityFit = [tableIndex, 0]
427 tableIndex += 1
428 elif self.config.linearityType in ['Spline']:
429 # See discussion in `lsst.ip.isr.linearize.py` before
430 # modifying.
431 numPerBin, binEdges = np.histogram(linearOrdinate, bins=fitOrder)
432 with np.errstate(invalid="ignore"):
433 # Algorithm note: With the counts of points per
434 # bin above, the next histogram calculates the
435 # values to put in each bin by weighting each
436 # point by the correction value.
437 values = np.histogram(linearOrdinate, bins=fitOrder,
438 weights=(inputOrdinate[fluxMask] - linearOrdinate))[0]/numPerBin
440 # After this is done, the binCenters are
441 # calculated by weighting by the value we're
442 # binning over. This ensures that widely
443 # spaced/poorly sampled data aren't assigned to
444 # the midpoint of the bin (as could be done using
445 # the binEdges above), but to the weighted mean of
446 # the inputs. Note that both histograms are
447 # scaled by the count per bin to normalize what
448 # the histogram returns (a sum of the points
449 # inside) into an average.
450 binCenters = np.histogram(linearOrdinate, bins=fitOrder,
451 weights=linearOrdinate)[0]/numPerBin
452 values = values[numPerBin > 0]
453 binCenters = binCenters[numPerBin > 0]
455 self.debugFit('splineFit', binCenters, np.abs(values), values, None, ampName)
456 # Anchor the spline to have zero correction at zero
457 # flux as well as at the lowest measured flux bin.
458 if np.any(np.array(binCenters) < 0):
459 raise ValueError("Linearity correction has negative flux values!")
461 if binCenters[0] != 0.0:
462 if values[0] != 0.0:
463 offset = values[0]
464 values -= offset
465 np.concatenate(([0.0], binCenters))
466 np.concatenate(([0.0], values))
468 interp = afwMath.makeInterpolate(binCenters.tolist(), values.tolist(),
469 afwMath.stringToInterpStyle("AKIMA_SPLINE"))
470 modelOrdinate = linearOrdinate + interp.interpolate(linearOrdinate)
471 self.debugFit('splineFit', linearOrdinate, fitOrdinate, modelOrdinate, None, ampName)
473 # If we exclude a lot of points, we may end up with
474 # less than fitOrder points. Pad out the low-flux end
475 # to ensure equal lengths.
476 if len(binCenters) != fitOrder:
477 padN = fitOrder - len(binCenters)
478 binCenters = np.pad(binCenters, (padN, 0), 'linear_ramp',
479 end_values=(binCenters.min() - 1.0, ))
480 # This stores the correction, which is zero at low values.
481 values = np.pad(values, (padN, 0))
483 # Pack the spline into a single array.
484 linearityFit = np.concatenate((binCenters.tolist(), values.tolist())).tolist()
485 polyFit = [0.0]
486 polyFitErr = [0.0]
487 chiSq = np.nan
488 else:
489 polyFit = [0.0]
490 polyFitErr = [0.0]
491 chiSq = np.nan
492 linearityFit = [0.0]
494 linearizer.linearityType[ampName] = self.config.linearityType
495 linearizer.linearityCoeffs[ampName] = np.array(linearityFit)
496 linearizer.linearityBBox[ampName] = amp.getBBox()
497 linearizer.fitParams[ampName] = np.array(polyFit)
498 linearizer.fitParamsErr[ampName] = np.array(polyFitErr)
499 linearizer.fitChiSq[ampName] = chiSq
500 linearizer.linearFit[ampName] = linearFit
501 residuals = fitOrdinate - modelOrdinate
503 # The residuals only include flux values which are
504 # not masked out. To be able to access this later and
505 # associate it with the PTC flux values, we need to
506 # fill out the residuals with NaNs where the flux
507 # value is masked.
509 # First convert mask to a composite of the two masks:
510 mask[mask] = fluxMask
511 fullResiduals = np.full(len(mask), np.nan)
512 fullResiduals[mask] = residuals
513 linearizer.fitResiduals[ampName] = fullResiduals
514 image = afwImage.ImageF(len(inputOrdinate), 1)
515 image.getArray()[:, :] = inputOrdinate
516 linearizeFunction = linearizer.getLinearityTypeByName(linearizer.linearityType[ampName])
517 linearizeFunction()(image,
518 **{'coeffs': linearizer.linearityCoeffs[ampName],
519 'table': linearizer.tableData,
520 'log': linearizer.log})
521 linearizeModel = image.getArray()[0, :]
523 self.debugFit('solution', inputOrdinate[fluxMask], linearOrdinate,
524 linearizeModel[fluxMask], None, ampName)
526 linearizer.hasLinearity = True
527 linearizer.validate()
528 linearizer.updateMetadata(camera=camera, detector=detector, filterName='NONE')
529 linearizer.updateMetadata(setDate=True, setCalibId=True)
530 provenance = IsrProvenance(calibType='linearizer')
532 return pipeBase.Struct(
533 outputLinearizer=linearizer,
534 outputProvenance=provenance,
535 )
537 def fillBadAmp(self, linearizer, fitOrder, inputPtc, amp):
538 # Need to fill linearizer with empty values
539 # if the amp is non-functional
540 ampName = amp.getName()
541 nEntries = 1
542 pEntries = 1
543 if self.config.linearityType in ['Polynomial']:
544 nEntries = fitOrder + 1
545 pEntries = fitOrder + 1
546 elif self.config.linearityType in ['Spline']:
547 nEntries = fitOrder * 2
548 elif self.config.linearityType in ['Squared', 'None']:
549 nEntries = 1
550 pEntries = fitOrder + 1
551 elif self.config.linearityType in ['LookupTable']:
552 nEntries = 2
553 pEntries = fitOrder + 1
555 linearizer.linearityType[ampName] = "None"
556 linearizer.linearityCoeffs[ampName] = np.zeros(nEntries)
557 linearizer.linearityBBox[ampName] = amp.getBBox()
558 linearizer.fitParams[ampName] = np.zeros(pEntries)
559 linearizer.fitParamsErr[ampName] = np.zeros(pEntries)
560 linearizer.fitChiSq[ampName] = np.nan
561 linearizer.fitResiduals[ampName] = np.zeros(len(inputPtc.expIdMask[ampName]))
562 linearizer.linearFit[ampName] = np.zeros(2)
563 return linearizer
565 def debugFit(self, stepname, xVector, yVector, yModel, mask, ampName):
566 """Debug method for linearity fitting.
568 Parameters
569 ----------
570 stepname : `str`
571 A label to use to check if we care to debug at a given
572 line of code.
573 xVector : `numpy.array`, (N,)
574 The values to use as the independent variable in the
575 linearity fit.
576 yVector : `numpy.array`, (N,)
577 The values to use as the dependent variable in the
578 linearity fit.
579 yModel : `numpy.array`, (N,)
580 The values to use as the linearized result.
581 mask : `numpy.array` [`bool`], (N,) , optional
582 A mask to indicate which entries of ``xVector`` and
583 ``yVector`` to keep.
584 ampName : `str`
585 Amplifier name to lookup linearity correction values.
586 """
587 frame = getDebugFrame(self._display, stepname)
588 if frame:
589 import matplotlib.pyplot as plt
590 fig, axs = plt.subplots(2)
592 if mask is None:
593 mask = np.ones_like(xVector, dtype=bool)
595 fig.suptitle(f"{stepname} {ampName} {self.config.linearityType}")
596 if stepname == 'linearFit':
597 axs[0].set_xlabel("Input Abscissa (time or mondiode)")
598 axs[0].set_ylabel("Input Ordinate (flux)")
599 axs[1].set_xlabel("Linear Ordinate (linear flux)")
600 axs[1].set_ylabel("Flux Difference: (input - linear)")
601 elif stepname in ('polyFit', 'splineFit'):
602 axs[0].set_xlabel("Linear Abscissa (linear flux)")
603 axs[0].set_ylabel("Input Ordinate (flux)")
604 axs[1].set_xlabel("Linear Ordinate (linear flux)")
605 axs[1].set_ylabel("Flux Difference: (input - full model fit)")
606 elif stepname == 'solution':
607 axs[0].set_xlabel("Input Abscissa (time or mondiode)")
608 axs[0].set_ylabel("Linear Ordinate (linear flux)")
609 axs[1].set_xlabel("Model flux (linear flux)")
610 axs[1].set_ylabel("Flux Difference: (linear - model)")
612 axs[0].set_yscale('log')
613 axs[0].set_xscale('log')
614 axs[0].scatter(xVector, yVector)
615 axs[0].scatter(xVector[~mask], yVector[~mask], c='red', marker='x')
616 axs[1].set_xscale('log')
618 axs[1].scatter(yModel, yVector[mask] - yModel)
619 fig.show()
621 prompt = "Press Enter or c to continue [chpx]..."
622 while True:
623 ans = input(prompt).lower()
624 if ans in ("", " ", "c",):
625 break
626 elif ans in ("p", ):
627 import pdb
628 pdb.set_trace()
629 elif ans in ("h", ):
630 print("[h]elp [c]ontinue [p]db")
631 elif ans in ('x', ):
632 exit()
633 plt.close()