Coverage for python/lsst/cp/pipe/measureCrosstalk.py: 14%
278 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-21 04:23 -0700
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-21 04:23 -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 <http://www.gnu.org/licenses/>.
21import itertools
22import numpy as np
24from collections import defaultdict
26import lsst.pipe.base as pipeBase
27import lsst.pipe.base.connectionTypes as cT
29from lsstDebug import getDebugFrame
30from lsst.afw.detection import FootprintSet, Threshold
31from lsst.afw.display import getDisplay
32from lsst.pex.config import Field, ListField
33from lsst.ip.isr import CrosstalkCalib, IsrProvenance
34from lsst.cp.pipe.utils import (ddict2dict, sigmaClipCorrection)
36from ._lookupStaticCalibration import lookupStaticCalibration
38__all__ = ["CrosstalkExtractConfig", "CrosstalkExtractTask",
39 "CrosstalkSolveTask", "CrosstalkSolveConfig"]
42class CrosstalkExtractConnections(pipeBase.PipelineTaskConnections,
43 dimensions=("instrument", "exposure", "detector")):
44 inputExp = cT.Input(
45 name="crosstalkInputs",
46 doc="Input post-ISR processed exposure to measure crosstalk from.",
47 storageClass="Exposure",
48 dimensions=("instrument", "exposure", "detector"),
49 multiple=False,
50 )
51 # TODO: Depends on DM-21904.
52 sourceExp = cT.Input(
53 name="crosstalkSource",
54 doc="Post-ISR exposure to measure for inter-chip crosstalk onto inputExp.",
55 storageClass="Exposure",
56 dimensions=("instrument", "exposure", "detector"),
57 multiple=True,
58 deferLoad=True,
59 # lookupFunction=None,
60 )
62 outputRatios = cT.Output(
63 name="crosstalkRatios",
64 doc="Extracted crosstalk pixel ratios.",
65 storageClass="StructuredDataDict",
66 dimensions=("instrument", "exposure", "detector"),
67 )
68 outputFluxes = cT.Output(
69 name="crosstalkFluxes",
70 doc="Source pixel fluxes used in ratios.",
71 storageClass="StructuredDataDict",
72 dimensions=("instrument", "exposure", "detector"),
73 )
75 def __init__(self, *, config=None):
76 super().__init__(config=config)
77 # Discard sourceExp until DM-21904 allows full interchip
78 # measurements.
79 self.inputs.discard("sourceExp")
82class CrosstalkExtractConfig(pipeBase.PipelineTaskConfig,
83 pipelineConnections=CrosstalkExtractConnections):
84 """Configuration for the measurement of pixel ratios.
85 """
87 doMeasureInterchip = Field(
88 dtype=bool,
89 default=False,
90 doc="Measure inter-chip crosstalk as well?",
91 )
92 threshold = Field(
93 dtype=float,
94 default=30000,
95 doc="Minimum level of source pixels for which to measure crosstalk."
96 )
97 ignoreSaturatedPixels = Field(
98 dtype=bool,
99 default=False,
100 doc="Should saturated pixels be ignored?"
101 )
102 badMask = ListField(
103 dtype=str,
104 default=["BAD", "INTRP"],
105 doc="Mask planes to ignore when identifying source pixels."
106 )
107 isTrimmed = Field(
108 dtype=bool,
109 default=True,
110 doc="Is the input exposure trimmed?"
111 )
113 def validate(self):
114 super().validate()
116 # Ensure the handling of the SAT mask plane is consistent
117 # with the ignoreSaturatedPixels value.
118 if self.ignoreSaturatedPixels:
119 if 'SAT' not in self.badMask:
120 self.badMask.append('SAT')
121 else:
122 if 'SAT' in self.badMask:
123 self.badMask = [mask for mask in self.badMask if mask != 'SAT']
126class CrosstalkExtractTask(pipeBase.PipelineTask):
127 """Task to measure pixel ratios to find crosstalk.
128 """
130 ConfigClass = CrosstalkExtractConfig
131 _DefaultName = 'cpCrosstalkExtract'
133 def run(self, inputExp, sourceExps=[]):
134 """Measure pixel ratios between amplifiers in inputExp.
136 Extract crosstalk ratios between different amplifiers.
138 For pixels above ``config.threshold``, we calculate the ratio
139 between each background-subtracted target amp and the source
140 amp. We return a list of ratios for each pixel for each
141 target/source combination, as nested dictionary containing the
142 ratio.
144 Parameters
145 ----------
146 inputExp : `lsst.afw.image.Exposure`
147 Input exposure to measure pixel ratios on.
148 sourceExp : `list` [`lsst.afw.image.Exposure`], optional
149 List of chips to use as sources to measure inter-chip
150 crosstalk.
152 Returns
153 -------
154 results : `lsst.pipe.base.Struct`
155 The results struct containing:
157 ``outputRatios``
158 A catalog of ratio lists. The dictionaries are
159 indexed such that:
160 outputRatios[targetChip][sourceChip][targetAmp][sourceAmp]
161 contains the ratio list for that combination (`dict`
162 [`dict` [`dict` [`dict` [`list`]]]]).
163 ``outputFluxes``
164 A catalog of flux lists. The dictionaries are
165 indexed such that:
166 outputFluxes[sourceChip][sourceAmp] contains the flux
167 list used in the outputRatios (`dict` [`dict`
168 [`list`]]).
169 """
170 outputRatios = defaultdict(lambda: defaultdict(dict))
171 outputFluxes = defaultdict(lambda: defaultdict(dict))
173 threshold = self.config.threshold
174 badPixels = list(self.config.badMask)
176 targetDetector = inputExp.getDetector()
177 targetChip = targetDetector.getName()
179 # Always look at the target chip first, then go to any other
180 # supplied exposures.
181 sourceExtractExps = [inputExp]
182 sourceExtractExps.extend(sourceExps)
184 self.log.info("Measuring full detector background for target: %s", targetChip)
185 targetIm = inputExp.getMaskedImage()
186 FootprintSet(targetIm, Threshold(threshold), "DETECTED")
187 detected = targetIm.getMask().getPlaneBitMask("DETECTED")
188 bg = CrosstalkCalib.calculateBackground(targetIm, badPixels + ["DETECTED"])
190 self.debugView('extract', inputExp)
192 for sourceExp in sourceExtractExps:
193 sourceDetector = sourceExp.getDetector()
194 sourceChip = sourceDetector.getName()
195 sourceIm = sourceExp.getMaskedImage()
196 bad = sourceIm.getMask().getPlaneBitMask(badPixels)
197 self.log.info("Measuring crosstalk from source: %s", sourceChip)
199 if sourceExp != inputExp:
200 FootprintSet(sourceIm, Threshold(threshold), "DETECTED")
201 detected = sourceIm.getMask().getPlaneBitMask("DETECTED")
203 # The dictionary of amp-to-amp ratios for this pair of
204 # source->target detectors.
205 ratioDict = defaultdict(lambda: defaultdict(list))
206 extractedCount = 0
208 for sourceAmp in sourceDetector:
209 sourceAmpName = sourceAmp.getName()
210 sourceAmpBBox = sourceAmp.getBBox() if self.config.isTrimmed else sourceAmp.getRawDataBBox()
211 sourceAmpImage = sourceIm[sourceAmpBBox]
212 sourceMask = sourceAmpImage.mask.array
213 select = ((sourceMask & detected > 0)
214 & (sourceMask & bad == 0)
215 & np.isfinite(sourceAmpImage.image.array))
216 count = np.sum(select)
217 self.log.debug(" Source amplifier: %s", sourceAmpName)
219 outputFluxes[sourceChip][sourceAmpName] = sourceAmpImage.image.array[select].tolist()
221 for targetAmp in targetDetector:
222 # iterate over targetExposure
223 targetAmpName = targetAmp.getName()
224 if sourceAmpName == targetAmpName and sourceChip == targetChip:
225 ratioDict[targetAmpName][sourceAmpName] = []
226 continue
227 self.log.debug(" Target amplifier: %s", targetAmpName)
229 targetAmpImage = CrosstalkCalib.extractAmp(targetIm.image,
230 targetAmp, sourceAmp,
231 isTrimmed=self.config.isTrimmed)
232 ratios = (targetAmpImage.array[select] - bg)/sourceAmpImage.image.array[select]
233 ratioDict[targetAmpName][sourceAmpName] = ratios.tolist()
234 extractedCount += count
236 self.debugPixels('pixels',
237 sourceAmpImage.image.array[select],
238 targetAmpImage.array[select] - bg,
239 sourceAmpName, targetAmpName)
241 self.log.info("Extracted %d pixels from %s -> %s (targetBG: %f)",
242 extractedCount, sourceChip, targetChip, bg)
243 outputRatios[targetChip][sourceChip] = ratioDict
245 return pipeBase.Struct(
246 outputRatios=ddict2dict(outputRatios),
247 outputFluxes=ddict2dict(outputFluxes)
248 )
250 def debugView(self, stepname, exposure):
251 """Utility function to examine the image being processed.
253 Parameters
254 ----------
255 stepname : `str`
256 State of processing to view.
257 exposure : `lsst.afw.image.Exposure`
258 Exposure to view.
259 """
260 frame = getDebugFrame(self._display, stepname)
261 if frame:
262 display = getDisplay(frame)
263 display.scale('asinh', 'zscale')
264 display.mtv(exposure)
266 prompt = "Press Enter to continue: "
267 while True:
268 ans = input(prompt).lower()
269 if ans in ("", "c",):
270 break
272 def debugPixels(self, stepname, pixelsIn, pixelsOut, sourceName, targetName):
273 """Utility function to examine the CT ratio pixel values.
275 Parameters
276 ----------
277 stepname : `str`
278 State of processing to view.
279 pixelsIn : `np.ndarray`, (N,)
280 Pixel values from the potential crosstalk source.
281 pixelsOut : `np.ndarray`, (N,)
282 Pixel values from the potential crosstalk target.
283 sourceName : `str`
284 Source amplifier name
285 targetName : `str`
286 Target amplifier name
287 """
288 frame = getDebugFrame(self._display, stepname)
289 if frame:
290 import matplotlib.pyplot as plt
291 figure = plt.figure(1)
292 figure.clear()
294 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
295 axes.plot(pixelsIn, pixelsOut / pixelsIn, 'k+')
296 plt.xlabel("Source amplifier pixel value")
297 plt.ylabel("Measured pixel ratio")
298 plt.title(f"(Source {sourceName} -> Target {targetName}) median ratio: "
299 f"{(np.median(pixelsOut / pixelsIn))}")
300 figure.show()
302 prompt = "Press Enter to continue: "
303 while True:
304 ans = input(prompt).lower()
305 if ans in ("", "c",):
306 break
307 plt.close()
310class CrosstalkSolveConnections(pipeBase.PipelineTaskConnections,
311 dimensions=("instrument", "detector")):
312 inputRatios = cT.Input(
313 name="crosstalkRatios",
314 doc="Ratios measured for an input exposure.",
315 storageClass="StructuredDataDict",
316 dimensions=("instrument", "exposure", "detector"),
317 multiple=True,
318 )
319 inputFluxes = cT.Input(
320 name="crosstalkFluxes",
321 doc="Fluxes of CT source pixels, for nonlinear fits.",
322 storageClass="StructuredDataDict",
323 dimensions=("instrument", "exposure", "detector"),
324 multiple=True,
325 )
326 camera = cT.PrerequisiteInput(
327 name="camera",
328 doc="Camera the input data comes from.",
329 storageClass="Camera",
330 dimensions=("instrument",),
331 isCalibration=True,
332 lookupFunction=lookupStaticCalibration,
333 )
335 outputCrosstalk = cT.Output(
336 name="crosstalk",
337 doc="Output proposed crosstalk calibration.",
338 storageClass="CrosstalkCalib",
339 dimensions=("instrument", "detector"),
340 multiple=False,
341 isCalibration=True,
342 )
344 def __init__(self, *, config=None):
345 super().__init__(config=config)
347 if config.fluxOrder == 0:
348 self.inputs.discard("inputFluxes")
351class CrosstalkSolveConfig(pipeBase.PipelineTaskConfig,
352 pipelineConnections=CrosstalkSolveConnections):
353 """Configuration for the solving of crosstalk from pixel ratios.
354 """
356 rejIter = Field(
357 dtype=int,
358 default=3,
359 doc="Number of rejection iterations for final coefficient calculation.",
360 )
361 rejSigma = Field(
362 dtype=float,
363 default=2.0,
364 doc="Rejection threshold (sigma) for final coefficient calculation.",
365 )
366 fluxOrder = Field(
367 dtype=int,
368 default=0,
369 doc="Polynomial order in source flux to fit crosstalk.",
370 )
371 significanceLimit = Field(
372 dtype=float,
373 default=3.0,
374 doc="Sigma significance level to use in marking a coefficient valid.",
375 )
376 doSignificanceScaling = Field(
377 dtype=bool,
378 default=True,
379 doc="Scale error by 1/sqrt(N) in calculating significant coefficients?",
380 )
381 doFiltering = Field(
382 dtype=bool,
383 default=False,
384 doc="Filter generated crosstalk to remove marginal measurements?",
385 )
388class CrosstalkSolveTask(pipeBase.PipelineTask):
389 """Task to solve crosstalk from pixel ratios.
390 """
392 ConfigClass = CrosstalkSolveConfig
393 _DefaultName = 'cpCrosstalkSolve'
395 def runQuantum(self, butlerQC, inputRefs, outputRefs):
396 """Ensure that the input and output dimensions are passed along.
398 Parameters
399 ----------
400 butlerQC : `lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
401 Butler to operate on.
402 inputRefs : `lsst.pipe.base.connections.InputQuantizedConnection`
403 Input data refs to load.
404 ouptutRefs : `lsst.pipe.base.connections.OutputQuantizedConnection`
405 Output data refs to persist.
406 """
407 inputs = butlerQC.get(inputRefs)
409 # Use the dimensions to set calib/provenance information.
410 inputs['inputDims'] = [exp.dataId.byName() for exp in inputRefs.inputRatios]
411 inputs['outputDims'] = outputRefs.outputCrosstalk.dataId.byName()
413 outputs = self.run(**inputs)
414 butlerQC.put(outputs, outputRefs)
416 def run(self, inputRatios, inputFluxes=None, camera=None, inputDims=None, outputDims=None):
417 """Combine ratios to produce crosstalk coefficients.
419 Parameters
420 ----------
421 inputRatios : `list` [`dict` [`dict` [`dict` [`dict` [`list`]]]]]
422 A list of nested dictionaries of ratios indexed by target
423 and source chip, then by target and source amplifier.
424 inputFluxes : `list` [`dict` [`dict` [`list`]]]
425 A list of nested dictionaries of source pixel fluxes, indexed
426 by source chip and amplifier.
427 camera : `lsst.afw.cameraGeom.Camera`
428 Input camera.
429 inputDims : `list` [`lsst.daf.butler.DataCoordinate`]
430 DataIds to use to construct provenance.
431 outputDims : `list` [`lsst.daf.butler.DataCoordinate`]
432 DataIds to use to populate the output calibration.
434 Returns
435 -------
436 results : `lsst.pipe.base.Struct`
437 The results struct containing:
439 ``outputCrosstalk``
440 Final crosstalk calibration
441 (`lsst.ip.isr.CrosstalkCalib`).
442 ``outputProvenance``
443 Provenance data for the new calibration
444 (`lsst.ip.isr.IsrProvenance`).
446 Raises
447 ------
448 RuntimeError
449 Raised if the input data contains multiple target detectors.
450 """
451 if outputDims:
452 calibChip = outputDims['detector']
453 instrument = outputDims['instrument']
454 else:
455 # calibChip needs to be set manually in Gen2.
456 calibChip = None
457 instrument = None
459 if camera and calibChip is not None:
460 calibDetector = camera[calibChip]
461 ordering = [amp.getName() for amp in calibDetector]
462 else:
463 calibDetector = None
464 ordering = None
466 self.log.info("Combining measurements from %d ratios and %d fluxes",
467 len(inputRatios), len(inputFluxes) if inputFluxes else 0)
469 if inputFluxes is None:
470 inputFluxes = [None for exp in inputRatios]
472 combinedRatios = defaultdict(lambda: defaultdict(list))
473 combinedFluxes = defaultdict(lambda: defaultdict(list))
474 for ratioDict, fluxDict in zip(inputRatios, inputFluxes):
475 for targetChip in ratioDict:
476 if calibChip and targetChip != calibChip and targetChip != calibDetector.getName():
477 raise RuntimeError(f"Target chip: {targetChip} does not match calibration dimension: "
478 f"{calibChip}, {calibDetector.getName()}!")
480 sourceChip = targetChip
481 if sourceChip in ratioDict[targetChip]:
482 ratios = ratioDict[targetChip][sourceChip]
484 for targetAmp in ratios:
485 for sourceAmp in ratios[targetAmp]:
486 combinedRatios[targetAmp][sourceAmp].extend(ratios[targetAmp][sourceAmp])
487 if fluxDict:
488 combinedFluxes[targetAmp][sourceAmp].extend(fluxDict[sourceChip][sourceAmp])
489 # TODO: DM-21904
490 # Iterating over all other entries in
491 # ratioDict[targetChip] will yield inter-chip terms.
493 for targetAmp in combinedRatios:
494 for sourceAmp in combinedRatios[targetAmp]:
495 self.log.info("Read %d pixels for %s -> %s",
496 len(combinedRatios[targetAmp][sourceAmp]),
497 targetAmp, sourceAmp)
498 if len(combinedRatios[targetAmp][sourceAmp]) > 1:
499 self.debugRatios('reduce', combinedRatios, targetAmp, sourceAmp)
501 if self.config.fluxOrder == 0:
502 self.log.info("Fitting crosstalk coefficients.")
504 calib = self.measureCrosstalkCoefficients(combinedRatios, ordering,
505 self.config.rejIter, self.config.rejSigma)
506 else:
507 raise NotImplementedError("Non-linear crosstalk terms are not yet supported.")
509 self.log.info("Number of valid coefficients: %d", np.sum(calib.coeffValid))
511 if self.config.doFiltering:
512 # This step will apply the calculated validity values to
513 # censor poorly measured coefficients.
514 self.log.info("Filtering measured crosstalk to remove invalid solutions.")
515 calib = self.filterCrosstalkCalib(calib)
517 # Populate the remainder of the calibration information.
518 calib.hasCrosstalk = True
519 calib.interChip = {}
521 # calibChip is the detector dimension, which is the detector Id
522 calib._detectorId = calibChip
523 if calibDetector:
524 calib._detectorName = calibDetector.getName()
525 calib._detectorSerial = calibDetector.getSerial()
527 calib._instrument = instrument
528 calib.updateMetadata(setCalibId=True, setDate=True)
530 # Make an IsrProvenance().
531 provenance = IsrProvenance(calibType="CROSSTALK")
532 provenance._detectorName = calibChip
533 if inputDims:
534 provenance.fromDataIds(inputDims)
535 provenance._instrument = instrument
536 provenance.updateMetadata()
538 return pipeBase.Struct(
539 outputCrosstalk=calib,
540 outputProvenance=provenance,
541 )
543 def measureCrosstalkCoefficients(self, ratios, ordering, rejIter, rejSigma):
544 """Measure crosstalk coefficients from the ratios.
546 Given a list of ratios for each target/source amp combination,
547 we measure a sigma clipped mean and error.
549 The coefficient errors returned are the standard deviation of
550 the final set of clipped input ratios.
552 Parameters
553 ----------
554 ratios : `dict` [`dict` [`numpy.ndarray`]]
555 Catalog of arrays of ratios. The ratio arrays are one-dimensional
556 ordering : `list` [`str`] or None
557 List to use as a mapping between amplifier names (the
558 elements of the list) and their position in the output
559 calibration (the matching index of the list). If no
560 ordering is supplied, the order of the keys in the ratio
561 catalog is used.
562 rejIter : `int`
563 Number of rejection iterations.
564 rejSigma : `float`
565 Rejection threshold (sigma).
567 Returns
568 -------
569 calib : `lsst.ip.isr.CrosstalkCalib`
570 The output crosstalk calibration.
571 """
572 calib = CrosstalkCalib(nAmp=len(ratios))
574 if ordering is None:
575 ordering = list(ratios.keys())
577 # Calibration stores coefficients as a numpy ndarray.
578 for ii, jj in itertools.product(range(calib.nAmp), range(calib.nAmp)):
579 if ii == jj:
580 values = [0.0]
581 else:
582 values = np.array(ratios[ordering[ii]][ordering[jj]])
583 values = values[np.abs(values) < 1.0] # Discard unreasonable values
585 # Sigma clip using the inter-quartile distance and a
586 # normal distribution.
587 if ii != jj:
588 for rej in range(rejIter):
589 if len(values) == 0:
590 break
591 lo, med, hi = np.percentile(values, [25.0, 50.0, 75.0])
592 sigma = 0.741*(hi - lo)
593 good = np.abs(values - med) < rejSigma*sigma
594 if good.sum() == len(good) or good.sum() == 0:
595 break
596 values = values[good]
598 calib.coeffNum[ii][jj] = len(values)
599 significanceThreshold = 0.0
600 if len(values) == 0:
601 self.log.warning("No values for matrix element %d,%d" % (ii, jj))
602 calib.coeffs[ii][jj] = np.nan
603 calib.coeffErr[ii][jj] = np.nan
604 calib.coeffValid[ii][jj] = False
605 else:
606 calib.coeffs[ii][jj] = np.mean(values)
607 if calib.coeffNum[ii][jj] == 1:
608 calib.coeffErr[ii][jj] = np.nan
609 calib.coeffValid[ii][jj] = False
610 else:
611 correctionFactor = sigmaClipCorrection(rejSigma)
612 calib.coeffErr[ii][jj] = np.std(values) * correctionFactor
614 # Use sample stdev.
615 significanceThreshold = self.config.significanceLimit * calib.coeffErr[ii][jj]
616 if self.config.doSignificanceScaling is True:
617 # Enabling this calculates the stdev of the mean.
618 significanceThreshold /= np.sqrt(calib.coeffNum[ii][jj])
619 calib.coeffValid[ii][jj] = np.abs(calib.coeffs[ii][jj]) > significanceThreshold
620 self.debugRatios('measure', ratios, ordering[ii], ordering[jj],
621 calib.coeffs[ii][jj], calib.coeffValid[ii][jj])
622 self.log.info("Measured %s -> %s Coeff: %e Err: %e N: %d Valid: %s Limit: %e",
623 ordering[jj], ordering[ii], calib.coeffs[ii][jj], calib.coeffErr[ii][jj],
624 calib.coeffNum[ii][jj], calib.coeffValid[ii][jj], significanceThreshold)
626 return calib
628 @staticmethod
629 def filterCrosstalkCalib(inCalib):
630 """Apply valid constraints to the measured values.
632 Any measured coefficient that is determined to be invalid is
633 set to zero, and has the error set to nan. The validation is
634 determined by checking that the measured coefficient is larger
635 than the calculated standard error of the mean.
637 Parameters
638 ----------
639 inCalib : `lsst.ip.isr.CrosstalkCalib`
640 Input calibration to filter.
642 Returns
643 -------
644 outCalib : `lsst.ip.isr.CrosstalkCalib`
645 Filtered calibration.
646 """
647 outCalib = CrosstalkCalib()
648 outCalib.numAmps = inCalib.numAmps
650 outCalib.coeffs = inCalib.coeffs
651 outCalib.coeffs[~inCalib.coeffValid] = 0.0
653 outCalib.coeffErr = inCalib.coeffErr
654 outCalib.coeffErr[~inCalib.coeffValid] = np.nan
656 outCalib.coeffNum = inCalib.coeffNum
657 outCalib.coeffValid = inCalib.coeffValid
659 return outCalib
661 def debugRatios(self, stepname, ratios, i, j, coeff=0.0, valid=False):
662 """Utility function to examine the final CT ratio set.
664 Parameters
665 ----------
666 stepname : `str`
667 State of processing to view.
668 ratios : `dict` [`dict` [`numpy.ndarray`]]
669 Array of measured CT ratios, indexed by source/victim
670 amplifier. These arrays are one-dimensional.
671 i : `str`
672 Index of the source amplifier.
673 j : `str`
674 Index of the target amplifier.
675 coeff : `float`, optional
676 Coefficient calculated to plot along with the simple mean.
677 valid : `bool`, optional
678 Validity to be added to the plot title.
679 """
680 frame = getDebugFrame(self._display, stepname)
681 if frame:
682 if i == j or ratios is None or len(ratios) < 1:
683 pass
685 ratioList = ratios[i][j]
686 if ratioList is None or len(ratioList) < 1:
687 pass
689 mean = np.mean(ratioList)
690 std = np.std(ratioList)
691 import matplotlib.pyplot as plt
692 figure = plt.figure(1)
693 figure.clear()
694 plt.hist(x=ratioList, bins=len(ratioList),
695 cumulative=True, color='b', density=True, histtype='step')
696 plt.xlabel("Measured pixel ratio")
697 plt.ylabel(f"CDF: n={len(ratioList)}")
698 plt.xlim(np.percentile(ratioList, [1.0, 99]))
699 plt.axvline(x=mean, color="k")
700 plt.axvline(x=coeff, color='g')
701 plt.axvline(x=(std / np.sqrt(len(ratioList))), color='r')
702 plt.axvline(x=-(std / np.sqrt(len(ratioList))), color='r')
703 plt.title(f"(Source {i} -> Target {j}) mean: {mean:.2g} coeff: {coeff:.2g} valid: {valid}")
704 figure.show()
706 prompt = "Press Enter to continue: "
707 while True:
708 ans = input(prompt).lower()
709 if ans in ("", "c",):
710 break
711 elif ans in ("pdb", "p",):
712 import pdb
713 pdb.set_trace()
714 plt.close()