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