24 from collections
import defaultdict
27 import lsst.pipe.base.connectionTypes
as cT
29 from lsstDebug
import getDebugFrame
33 from lsst.ip.isr import CrosstalkCalib, IsrProvenance
37 from ._lookupStaticCalibration
import lookupStaticCalibration
39 __all__ = [
"CrosstalkExtractConfig",
"CrosstalkExtractTask",
40 "CrosstalkSolveTask",
"CrosstalkSolveConfig",
41 "MeasureCrosstalkConfig",
"MeasureCrosstalkTask"]
45 dimensions=(
"instrument",
"exposure",
"detector")):
47 name=
"crosstalkInputs",
48 doc=
"Input post-ISR processed exposure to measure crosstalk from.",
49 storageClass=
"Exposure",
50 dimensions=(
"instrument",
"exposure",
"detector"),
55 name=
"crosstalkSource",
56 doc=
"Post-ISR exposure to measure for inter-chip crosstalk onto inputExp.",
57 storageClass=
"Exposure",
58 dimensions=(
"instrument",
"exposure",
"detector"),
64 outputRatios = cT.Output(
65 name=
"crosstalkRatios",
66 doc=
"Extracted crosstalk pixel ratios.",
67 storageClass=
"StructuredDataDict",
68 dimensions=(
"instrument",
"exposure",
"detector"),
70 outputFluxes = cT.Output(
71 name=
"crosstalkFluxes",
72 doc=
"Source pixel fluxes used in ratios.",
73 storageClass=
"StructuredDataDict",
74 dimensions=(
"instrument",
"exposure",
"detector"),
81 self.inputs.discard(
"sourceExp")
85 pipelineConnections=CrosstalkExtractConnections):
86 """Configuration for the measurement of pixel ratios.
91 doc=
"Measure inter-chip crosstalk as well?",
96 doc=
"Minimum level of source pixels for which to measure crosstalk."
101 doc=
"Should saturated pixels be ignored?"
105 default=[
"BAD",
"INTRP"],
106 doc=
"Mask planes to ignore when identifying source pixels."
111 doc=
"Is the input exposure trimmed?"
120 if 'SAT' not in self.
badMaskbadMask:
123 if 'SAT' in self.
badMaskbadMask:
124 self.
badMaskbadMask = [mask
for mask
in self.
badMaskbadMask
if mask !=
'SAT']
128 pipeBase.CmdLineTask):
129 """Task to measure pixel ratios to find crosstalk.
131 ConfigClass = CrosstalkExtractConfig
132 _DefaultName =
'cpCrosstalkExtract'
134 def run(self, inputExp, sourceExps=[]):
135 """Measure pixel ratios between amplifiers in inputExp.
137 Extract crosstalk ratios between different amplifiers.
139 For pixels above ``config.threshold``, we calculate the ratio
140 between each background-subtracted target amp and the source
141 amp. We return a list of ratios for each pixel for each
142 target/source combination, as nested dictionary containing the
147 inputExp : `lsst.afw.image.Exposure`
148 Input exposure to measure pixel ratios on.
149 sourceExp : `list` [`lsst.afw.image.Exposure`], optional
150 List of chips to use as sources to measure inter-chip
155 results : `lsst.pipe.base.Struct`
156 The results struct containing:
158 ``outputRatios`` : `dict` [`dict` [`dict` [`dict` [`list`]]]]
159 A catalog of ratio lists. The dictionaries are
161 outputRatios[targetChip][sourceChip][targetAmp][sourceAmp]
162 contains the ratio list for that combination.
163 ``outputFluxes`` : `dict` [`dict` [`list`]]
164 A catalog of flux lists. The dictionaries are
166 outputFluxes[sourceChip][sourceAmp]
167 contains the flux list used in the outputRatios.
171 The lsstDebug.Info() method can be rewritten for __name__ =
172 `lsst.cp.pipe.measureCrosstalk`, and supports the parameters:
174 debug.display['extract'] : `bool`
175 Display the exposure under consideration, with the pixels used
176 for crosstalk measurement indicated by the DETECTED mask plane.
177 debug.display['pixels'] : `bool`
178 Display a plot of the ratio calculated for each pixel used in this
179 exposure, split by amplifier pairs. The median value is listed
182 outputRatios = defaultdict(
lambda: defaultdict(dict))
183 outputFluxes = defaultdict(
lambda: defaultdict(dict))
185 threshold = self.config.threshold
186 badPixels =
list(self.config.badMask)
188 targetDetector = inputExp.getDetector()
189 targetChip = targetDetector.getName()
192 sourceExtractExps = [inputExp]
193 sourceExtractExps.extend(sourceExps)
195 self.log.
info(
"Measuring full detector background for target: %s", targetChip)
196 targetIm = inputExp.getMaskedImage()
198 detected = targetIm.getMask().getPlaneBitMask(
"DETECTED")
199 bg = CrosstalkCalib.calculateBackground(targetIm, badPixels + [
"DETECTED"])
201 self.
debugViewdebugView(
'extract', inputExp)
203 for sourceExp
in sourceExtractExps:
204 sourceDetector = sourceExp.getDetector()
205 sourceChip = sourceDetector.getName()
206 sourceIm = sourceExp.getMaskedImage()
207 bad = sourceIm.getMask().getPlaneBitMask(badPixels)
208 self.log.
info(
"Measuring crosstalk from source: %s", sourceChip)
210 if sourceExp != inputExp:
212 detected = sourceIm.getMask().getPlaneBitMask(
"DETECTED")
215 ratioDict = defaultdict(
lambda: defaultdict(list))
218 for sourceAmp
in sourceDetector:
219 sourceAmpName = sourceAmp.getName()
220 sourceAmpBBox = sourceAmp.getBBox()
if self.config.isTrimmed
else sourceAmp.getRawDataBBox()
221 sourceAmpImage = sourceIm[sourceAmpBBox]
222 sourceMask = sourceAmpImage.mask.array
223 select = ((sourceMask & detected > 0)
224 & (sourceMask & bad == 0)
225 & np.isfinite(sourceAmpImage.image.array))
226 count = np.sum(select)
227 self.log.
debug(
" Source amplifier: %s", sourceAmpName)
229 outputFluxes[sourceChip][sourceAmpName] = sourceAmpImage.image.array[select].tolist()
231 for targetAmp
in targetDetector:
233 targetAmpName = targetAmp.getName()
234 if sourceAmpName == targetAmpName
and sourceChip == targetChip:
235 ratioDict[sourceAmpName][targetAmpName] = []
237 self.log.
debug(
" Target amplifier: %s", targetAmpName)
239 targetAmpImage = CrosstalkCalib.extractAmp(targetIm.image,
240 targetAmp, sourceAmp,
241 isTrimmed=self.config.isTrimmed)
242 ratios = (targetAmpImage.array[select] - bg)/sourceAmpImage.image.array[select]
243 ratioDict[targetAmpName][sourceAmpName] = ratios.tolist()
244 extractedCount += count
247 sourceAmpImage.image.array[select],
248 targetAmpImage.array[select] - bg,
249 sourceAmpName, targetAmpName)
251 self.log.
info(
"Extracted %d pixels from %s -> %s (targetBG: %f)",
252 extractedCount, sourceChip, targetChip, bg)
253 outputRatios[targetChip][sourceChip] = ratioDict
255 return pipeBase.Struct(
261 """Utility function to examine the image being processed.
266 State of processing to view.
267 exposure : `lsst.afw.image.Exposure`
273 display.scale(
'asinh',
'zscale')
274 display.mtv(exposure)
276 prompt =
"Press Enter to continue: "
278 ans = input(prompt).lower()
279 if ans
in (
"",
"c",):
282 def debugPixels(self, stepname, pixelsIn, pixelsOut, sourceName, targetName):
283 """Utility function to examine the CT ratio pixel values.
288 State of processing to view.
289 pixelsIn : `np.ndarray`
290 Pixel values from the potential crosstalk source.
291 pixelsOut : `np.ndarray`
292 Pixel values from the potential crosstalk target.
294 Source amplifier name
296 Target amplifier name
300 import matplotlib.pyplot
as plt
301 figure = plt.figure(1)
304 axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
305 axes.plot(pixelsIn, pixelsOut / pixelsIn,
'k+')
306 plt.xlabel(
"Source amplifier pixel value")
307 plt.ylabel(
"Measured pixel ratio")
308 plt.title(f
"(Source {sourceName} -> Target {targetName}) median ratio: "
309 f
"{(np.median(pixelsOut / pixelsIn))}")
312 prompt =
"Press Enter to continue: "
314 ans = input(prompt).lower()
315 if ans
in (
"",
"c",):
321 dimensions=(
"instrument",
"detector")):
322 inputRatios = cT.Input(
323 name=
"crosstalkRatios",
324 doc=
"Ratios measured for an input exposure.",
325 storageClass=
"StructuredDataDict",
326 dimensions=(
"instrument",
"exposure",
"detector"),
329 inputFluxes = cT.Input(
330 name=
"crosstalkFluxes",
331 doc=
"Fluxes of CT source pixels, for nonlinear fits.",
332 storageClass=
"StructuredDataDict",
333 dimensions=(
"instrument",
"exposure",
"detector"),
336 camera = cT.PrerequisiteInput(
338 doc=
"Camera the input data comes from.",
339 storageClass=
"Camera",
340 dimensions=(
"instrument",),
342 lookupFunction=lookupStaticCalibration,
345 outputCrosstalk = cT.Output(
347 doc=
"Output proposed crosstalk calibration.",
348 storageClass=
"CrosstalkCalib",
349 dimensions=(
"instrument",
"detector"),
357 if config.fluxOrder == 0:
358 self.inputs.discard(
"inputFluxes")
362 pipelineConnections=CrosstalkSolveConnections):
363 """Configuration for the solving of crosstalk from pixel ratios.
368 doc=
"Number of rejection iterations for final coefficient calculation.",
373 doc=
"Rejection threshold (sigma) for final coefficient calculation.",
378 doc=
"Polynomial order in source flux to fit crosstalk.",
383 doc=
"Filter generated crosstalk to remove marginal measurements.",
388 pipeBase.CmdLineTask):
389 """Task to solve crosstalk from pixel ratios.
391 ConfigClass = CrosstalkSolveConfig
392 _DefaultName =
'cpCrosstalkSolve'
395 """Ensure that the input and output dimensions are passed along.
399 butlerQC : `lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
400 Butler to operate on.
401 inputRefs : `lsst.pipe.base.connections.InputQuantizedConnection`
402 Input data refs to load.
403 ouptutRefs : `lsst.pipe.base.connections.OutputQuantizedConnection`
404 Output data refs to persist.
406 inputs = butlerQC.get(inputRefs)
409 inputs[
'inputDims'] = [exp.dataId.byName()
for exp
in inputRefs.inputRatios]
410 inputs[
'outputDims'] = outputRefs.outputCrosstalk.dataId.byName()
412 outputs = self.
runrun(**inputs)
413 butlerQC.put(outputs, outputRefs)
415 def run(self, inputRatios, inputFluxes=None, camera=None, inputDims=None, outputDims=None):
416 """Combine ratios to produce crosstalk coefficients.
420 inputRatios : `list` [`dict` [`dict` [`dict` [`dict` [`list`]]]]]
421 A list of nested dictionaries of ratios indexed by target
422 and source chip, then by target and source amplifier.
423 inputFluxes : `list` [`dict` [`dict` [`list`]]]
424 A list of nested dictionaries of source pixel fluxes, indexed
425 by source chip and amplifier.
426 camera : `lsst.afw.cameraGeom.Camera`
428 inputDims : `list` [`lsst.daf.butler.DataCoordinate`]
429 DataIds to use to construct provenance.
430 outputDims : `list` [`lsst.daf.butler.DataCoordinate`]
431 DataIds to use to populate the output calibration.
435 results : `lsst.pipe.base.Struct`
436 The results struct containing:
438 ``outputCrosstalk`` : `lsst.ip.isr.CrosstalkCalib`
439 Final crosstalk calibration.
440 ``outputProvenance`` : `lsst.ip.isr.IsrProvenance`
441 Provenance data for the new calibration.
446 Raised if the input data contains multiple target detectors.
450 The lsstDebug.Info() method can be rewritten for __name__ =
451 `lsst.ip.isr.measureCrosstalk`, and supports the parameters:
453 debug.display['reduce'] : `bool`
454 Display a histogram of the combined ratio measurements for
455 a pair of source/target amplifiers from all input
460 calibChip = outputDims[
'detector']
461 instrument = outputDims[
'instrument']
467 if camera
and calibChip:
468 calibDetector = camera[calibChip]
472 self.log.
info(
"Combining measurements from %d ratios and %d fluxes",
473 len(inputRatios), len(inputFluxes)
if inputFluxes
else 0)
475 if inputFluxes
is None:
476 inputFluxes = [
None for exp
in inputRatios]
478 combinedRatios = defaultdict(
lambda: defaultdict(list))
479 combinedFluxes = defaultdict(
lambda: defaultdict(list))
480 for ratioDict, fluxDict
in zip(inputRatios, inputFluxes):
481 for targetChip
in ratioDict:
482 if calibChip
and targetChip != calibChip
and targetChip != calibDetector.getName():
483 raise RuntimeError(f
"Target chip: {targetChip} does not match calibration dimension: "
484 f
"{calibChip}, {calibDetector.getName()}!")
486 sourceChip = targetChip
487 if sourceChip
in ratioDict[targetChip]:
488 ratios = ratioDict[targetChip][sourceChip]
490 for targetAmp
in ratios:
491 for sourceAmp
in ratios[targetAmp]:
492 combinedRatios[targetAmp][sourceAmp].extend(ratios[targetAmp][sourceAmp])
494 combinedFluxes[targetAmp][sourceAmp].extend(fluxDict[sourceChip][sourceAmp])
499 for targetAmp
in combinedRatios:
500 for sourceAmp
in combinedRatios[targetAmp]:
501 self.log.
info(
"Read %d pixels for %s -> %s",
502 len(combinedRatios[targetAmp][sourceAmp]),
503 targetAmp, sourceAmp)
504 if len(combinedRatios[targetAmp][sourceAmp]) > 1:
505 self.
debugRatiosdebugRatios(
'reduce', combinedRatios, targetAmp, sourceAmp)
507 if self.config.fluxOrder == 0:
508 self.log.
info(
"Fitting crosstalk coefficients.")
510 self.config.rejIter, self.config.rejSigma)
512 raise NotImplementedError(
"Non-linear crosstalk terms are not yet supported.")
514 self.log.
info(
"Number of valid coefficients: %d", np.sum(calib.coeffValid))
516 if self.config.doFiltering:
519 self.log.
info(
"Filtering measured crosstalk to remove invalid solutions.")
523 calib.hasCrosstalk =
True
527 calib._detectorId = calibChip
529 calib._detectorName = calibDetector.getName()
530 calib._detectorSerial = calibDetector.getSerial()
532 calib._instrument = instrument
533 calib.updateMetadata(setCalibId=
True, setDate=
True)
537 provenance._detectorName = calibChip
539 provenance.fromDataIds(inputDims)
540 provenance._instrument = instrument
541 provenance.updateMetadata()
543 return pipeBase.Struct(
544 outputCrosstalk=calib,
545 outputProvenance=provenance,
549 """Measure crosstalk coefficients from the ratios.
551 Given a list of ratios for each target/source amp combination,
552 we measure a sigma clipped mean and error.
554 The coefficient errors returned are the standard deviation of
555 the final set of clipped input ratios.
559 ratios : `dict` of `dict` of `numpy.ndarray`
560 Catalog of arrays of ratios.
562 Number of rejection iterations.
564 Rejection threshold (sigma).
568 calib : `lsst.ip.isr.CrosstalkCalib`
569 The output crosstalk calibration.
573 The lsstDebug.Info() method can be rewritten for __name__ =
574 `lsst.ip.isr.measureCrosstalk`, and supports the parameters:
576 debug.display['measure'] : `bool`
577 Display the CDF of the combined ratio measurements for
578 a pair of source/target amplifiers from the final set of
579 clipped input ratios.
584 ordering =
list(ratios.keys())
585 for ii, jj
in itertools.product(range(calib.nAmp), range(calib.nAmp)):
589 values = np.array(ratios[ordering[ii]][ordering[jj]])
590 values = values[np.abs(values) < 1.0]
592 calib.coeffNum[ii][jj] = len(values)
595 self.log.
warn(
"No values for matrix element %d,%d" % (ii, jj))
596 calib.coeffs[ii][jj] = np.nan
597 calib.coeffErr[ii][jj] = np.nan
598 calib.coeffValid[ii][jj] =
False
601 for rej
in range(rejIter):
602 lo, med, hi = np.percentile(values, [25.0, 50.0, 75.0])
603 sigma = 0.741*(hi - lo)
604 good = np.abs(values - med) < rejSigma*sigma
605 if good.sum() == len(good):
607 values = values[good]
609 calib.coeffs[ii][jj] = np.mean(values)
610 if calib.coeffNum[ii][jj] == 1:
611 calib.coeffErr[ii][jj] = np.nan
614 calib.coeffErr[ii][jj] = np.std(values) * correctionFactor
615 calib.coeffValid[ii][jj] = (np.abs(calib.coeffs[ii][jj])
616 > calib.coeffErr[ii][jj] / np.sqrt(calib.coeffNum[ii][jj]))
618 if calib.coeffNum[ii][jj] > 1:
619 self.
debugRatiosdebugRatios(
'measure', ratios, ordering[ii], ordering[jj],
620 calib.coeffs[ii][jj], calib.coeffValid[ii][jj])
626 """Apply valid constraints to the measured values.
628 Any measured coefficient that is determined to be invalid is
629 set to zero, and has the error set to nan. The validation is
630 determined by checking that the measured coefficient is larger
631 than the calculated standard error of the mean.
635 inCalib : `lsst.ip.isr.CrosstalkCalib`
636 Input calibration to filter.
640 outCalib : `lsst.ip.isr.CrosstalkCalib`
641 Filtered calibration.
644 outCalib.numAmps = inCalib.numAmps
646 outCalib.coeffs = inCalib.coeffs
647 outCalib.coeffs[~inCalib.coeffValid] = 0.0
649 outCalib.coeffErr = inCalib.coeffErr
650 outCalib.coeffErr[~inCalib.coeffValid] = np.nan
652 outCalib.coeffNum = inCalib.coeffNum
653 outCalib.coeffValid = inCalib.coeffValid
657 def debugRatios(self, stepname, ratios, i, j, coeff=0.0, valid=False):
658 """Utility function to examine the final CT ratio set.
663 State of processing to view.
664 ratios : `dict` of `dict` of `np.ndarray`
665 Array of measured CT ratios, indexed by source/victim
668 Index of the source amplifier.
670 Index of the target amplifier.
671 coeff : `float`, optional
672 Coefficient calculated to plot along with the simple mean.
673 valid : `bool`, optional
674 Validity to be added to the plot title.
678 if i == j
or ratios
is None or len(ratios) < 1:
681 ratioList = ratios[i][j]
682 if ratioList
is None or len(ratioList) < 1:
685 mean = np.mean(ratioList)
686 std = np.std(ratioList)
687 import matplotlib.pyplot
as plt
688 figure = plt.figure(1)
690 plt.hist(x=ratioList, bins=len(ratioList),
691 cumulative=
True, color=
'b', density=
True, histtype=
'step')
692 plt.xlabel(
"Measured pixel ratio")
693 plt.ylabel(f
"CDF: n={len(ratioList)}")
694 plt.xlim(np.percentile(ratioList, [1.0, 99]))
695 plt.axvline(x=mean, color=
"k")
696 plt.axvline(x=coeff, color=
'g')
697 plt.axvline(x=(std / np.sqrt(len(ratioList))), color=
'r')
698 plt.axvline(x=-(std / np.sqrt(len(ratioList))), color=
'r')
699 plt.title(f
"(Source {i} -> Target {j}) mean: {mean:.2g} coeff: {coeff:.2g} valid: {valid}")
702 prompt =
"Press Enter to continue: "
704 ans = input(prompt).lower()
705 if ans
in (
"",
"c",):
707 elif ans
in (
"pdb",
"p",):
715 target=CrosstalkExtractTask,
716 doc=
"Task to measure pixel ratios.",
719 target=CrosstalkSolveTask,
720 doc=
"Task to convert ratio lists to crosstalk coefficients.",
725 """Measure intra-detector crosstalk.
729 lsst.ip.isr.crosstalk.CrosstalkCalib
730 lsst.cp.pipe.measureCrosstalk.CrosstalkExtractTask
731 lsst.cp.pipe.measureCrosstalk.CrosstalkSolveTask
735 The crosstalk this method measures assumes that when a bright
736 pixel is found in one detector amplifier, all other detector
737 amplifiers may see a signal change in the same pixel location
738 (relative to the readout amplifier) as these other pixels are read
739 out at the same time.
741 After processing each input exposure through a limited set of ISR
742 stages, bright unmasked pixels above the threshold are identified.
743 The potential CT signal is found by taking the ratio of the
744 appropriate background-subtracted pixel value on the other
745 amplifiers to the input value on the source amplifier. If the
746 source amplifier has a large number of bright pixels as well, the
747 background level may be elevated, leading to poor ratio
750 The set of ratios found between each pair of amplifiers across all
751 input exposures is then gathered to produce the final CT
752 coefficients. The sigma-clipped mean and sigma are returned from
753 these sets of ratios, with the coefficient to supply to the ISR
754 CrosstalkTask() being the multiplicative inverse of these values.
756 This Task simply calls the pipetask versions of the measure
759 ConfigClass = MeasureCrosstalkConfig
760 _DefaultName =
"measureCrosstalk"
763 RunnerClass = DataRefListRunner
767 self.makeSubtask(
"extract")
768 self.makeSubtask(
"solver")
771 """Run extract task on each of inputs in the dataRef list, then pass
772 that to the solver task.
776 dataRefList : `list` [`lsst.daf.peristence.ButlerDataRef`]
777 Data references for exposures for detectors to process.
781 results : `lsst.pipe.base.Struct`
782 The results struct containing:
784 ``outputCrosstalk`` : `lsst.ip.isr.CrosstalkCalib`
785 Final crosstalk calibration.
786 ``outputProvenance`` : `lsst.ip.isr.IsrProvenance`
787 Provenance data for the new calibration.
792 Raised if multiple target detectors are supplied.
794 dataRef = dataRefList[0]
795 camera = dataRef.get(
"camera")
799 for dataRef
in dataRefList:
800 exposure = dataRef.get(
"postISRCCD")
802 if exposure.getDetector().
getName() != activeChip:
803 raise RuntimeError(
"Too many input detectors supplied!")
805 activeChip = exposure.getDetector().
getName()
807 self.extract.debugView(
"extract", exposure)
808 result = self.extract.
run(exposure)
809 ratios.append(result.outputRatios)
811 for detIter, detector
in enumerate(camera):
812 if detector.getName() == activeChip:
814 outputDims = {
'instrument': camera.getName(),
815 'detector': detectorId,
818 finalResults = self.solver.
run(ratios, camera=camera, outputDims=outputDims)
819 dataRef.put(finalResults.outputCrosstalk,
"crosstalk")
A Threshold is used to pass a threshold value to detection algorithms.
def __init__(self, *config=None)
def run(self, inputRatios, inputFluxes=None, camera=None, inputDims=None, outputDims=None)
def debugRatios(self, stepname, ratios, i, j, coeff=0.0, valid=False)
def filterCrosstalkCalib(inCalib)
def measureCrosstalkCoefficients(self, ratios, rejIter, rejSigma)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def __init__(self, **kwargs)
def runDataRef(self, dataRefList)
daf::base::PropertyList * list
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
std::string const & getName() const noexcept
Return a filter's name.
def sigmaClipCorrection(nSigClip)
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)
def getDebugFrame(debugDisplay, name)