Coverage for python/lsst/ip/isr/crosstalk.py: 12%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# LSST Data Management System
3# Copyright 2008-2017 AURA/LSST.
4#
5# This product includes software developed by the
6# LSST Project (http://www.lsst.org/).
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the LSST License Statement and
19# the GNU General Public License along with this program. If not,
20# see <https://www.lsstcorp.org/LegalNotices/>.
21#
22"""
23Apply intra-detector crosstalk corrections
24"""
25import numpy as np
26from astropy.table import Table
28import lsst.afw.math
29import lsst.afw.detection
30import lsst.daf.butler
31from lsst.pex.config import Config, Field, ChoiceField, ListField
32from lsst.pipe.base import Task
34from lsst.ip.isr import IsrCalib
37__all__ = ["CrosstalkCalib", "CrosstalkConfig", "CrosstalkTask",
38 "NullCrosstalkTask"]
41class CrosstalkCalib(IsrCalib):
42 """Calibration of amp-to-amp crosstalk coefficients.
44 Parameters
45 ----------
46 detector : `lsst.afw.cameraGeom.Detector`, optional
47 Detector to use to pull coefficients from.
48 nAmp : `int`, optional
49 Number of amplifiers to initialize.
50 log : `logging.Logger`, optional
51 Log to write messages to.
52 **kwargs :
53 Parameters to pass to parent constructor.
55 Notes
56 -----
57 The crosstalk attributes stored are:
59 hasCrosstalk : `bool`
60 Whether there is crosstalk defined for this detector.
61 nAmp : `int`
62 Number of amplifiers in this detector.
63 crosstalkShape : `tuple` [`int`, `int`]
64 A tuple containing the shape of the ``coeffs`` matrix. This
65 should be equivalent to (``nAmp``, ``nAmp``).
66 coeffs : `np.ndarray`
67 A matrix containing the crosstalk coefficients. coeff[i][j]
68 contains the coefficients to calculate the contribution
69 amplifier_j has on amplifier_i (each row[i] contains the
70 corrections for detector_i).
71 coeffErr : `np.ndarray`, optional
72 A matrix (as defined by ``coeffs``) containing the standard
73 distribution of the crosstalk measurements.
74 coeffNum : `np.ndarray`, optional
75 A matrix containing the number of pixel pairs used to measure
76 the ``coeffs`` and ``coeffErr``.
77 coeffValid : `np.ndarray`, optional
78 A matrix of Boolean values indicating if the coefficient is
79 valid, defined as abs(coeff) > coeffErr / sqrt(coeffNum).
80 interChip : `dict` [`np.ndarray`]
81 A dictionary keyed by detectorName containing ``coeffs``
82 matrices used to correct for inter-chip crosstalk with a
83 source on the detector indicated.
85 """
86 _OBSTYPE = 'CROSSTALK'
87 _SCHEMA = 'Gen3 Crosstalk'
88 _VERSION = 1.0
90 def __init__(self, detector=None, nAmp=0, **kwargs):
91 self.hasCrosstalk = False
92 self.nAmp = nAmp if nAmp else 0
93 self.crosstalkShape = (self.nAmp, self.nAmp)
95 self.coeffs = np.zeros(self.crosstalkShape) if self.nAmp else None
96 self.coeffErr = np.zeros(self.crosstalkShape) if self.nAmp else None
97 self.coeffNum = np.zeros(self.crosstalkShape,
98 dtype=int) if self.nAmp else None
99 self.coeffValid = np.zeros(self.crosstalkShape,
100 dtype=bool) if self.nAmp else None
101 self.interChip = {}
103 super().__init__(**kwargs)
104 self.requiredAttributes.update(['hasCrosstalk', 'nAmp', 'coeffs',
105 'coeffErr', 'coeffNum', 'coeffValid',
106 'interChip'])
107 if detector:
108 self.fromDetector(detector)
110 def updateMetadata(self, setDate=False, **kwargs):
111 """Update calibration metadata.
113 This calls the base class's method after ensuring the required
114 calibration keywords will be saved.
116 Parameters
117 ----------
118 setDate : `bool`, optional
119 Update the CALIBDATE fields in the metadata to the current
120 time. Defaults to False.
121 kwargs :
122 Other keyword parameters to set in the metadata.
123 """
124 kwargs['DETECTOR'] = self._detectorId
125 kwargs['DETECTOR_NAME'] = self._detectorName
126 kwargs['DETECTOR_SERIAL'] = self._detectorSerial
127 kwargs['HAS_CROSSTALK'] = self.hasCrosstalk
128 kwargs['NAMP'] = self.nAmp
129 self.crosstalkShape = (self.nAmp, self.nAmp)
130 kwargs['CROSSTALK_SHAPE'] = self.crosstalkShape
132 super().updateMetadata(setDate=setDate, **kwargs)
134 def fromDetector(self, detector, coeffVector=None):
135 """Set calibration parameters from the detector.
137 Parameters
138 ----------
139 detector : `lsst.afw.cameraGeom.Detector`
140 Detector to use to set parameters from.
141 coeffVector : `numpy.array`, optional
142 Use the detector geometry (bounding boxes and flip
143 information), but use ``coeffVector`` instead of the
144 output of ``detector.getCrosstalk()``.
146 Returns
147 -------
148 calib : `lsst.ip.isr.CrosstalkCalib`
149 The calibration constructed from the detector.
151 """
152 if detector.hasCrosstalk() or coeffVector:
153 self._detectorId = detector.getId()
154 self._detectorName = detector.getName()
155 self._detectorSerial = detector.getSerial()
157 self.nAmp = len(detector)
158 self.crosstalkShape = (self.nAmp, self.nAmp)
160 if coeffVector is not None:
161 crosstalkCoeffs = coeffVector
162 else:
163 crosstalkCoeffs = detector.getCrosstalk()
164 if len(crosstalkCoeffs) == 1 and crosstalkCoeffs[0] == 0.0:
165 return self
166 self.coeffs = np.array(crosstalkCoeffs).reshape(self.crosstalkShape)
168 if self.coeffs.shape != self.crosstalkShape:
169 raise RuntimeError("Crosstalk coefficients do not match detector shape. "
170 f"{self.crosstalkShape} {self.nAmp}")
172 self.coeffErr = np.zeros(self.crosstalkShape)
173 self.coeffNum = np.zeros(self.crosstalkShape, dtype=int)
174 self.coeffValid = np.ones(self.crosstalkShape, dtype=bool)
175 self.interChip = {}
177 self.hasCrosstalk = True
178 self.updateMetadata()
179 return self
181 @classmethod
182 def fromDict(cls, dictionary):
183 """Construct a calibration from a dictionary of properties.
185 Must be implemented by the specific calibration subclasses.
187 Parameters
188 ----------
189 dictionary : `dict`
190 Dictionary of properties.
192 Returns
193 -------
194 calib : `lsst.ip.isr.CalibType`
195 Constructed calibration.
197 Raises
198 ------
199 RuntimeError :
200 Raised if the supplied dictionary is for a different
201 calibration.
202 """
203 calib = cls()
205 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
206 raise RuntimeError(f"Incorrect crosstalk supplied. Expected {calib._OBSTYPE}, "
207 f"found {dictionary['metadata']['OBSTYPE']}")
209 calib.setMetadata(dictionary['metadata'])
211 if 'detectorName' in dictionary:
212 calib._detectorName = dictionary.get('detectorName')
213 elif 'DETECTOR_NAME' in dictionary:
214 calib._detectorName = dictionary.get('DETECTOR_NAME')
215 elif 'DET_NAME' in dictionary['metadata']:
216 calib._detectorName = dictionary['metadata']['DET_NAME']
217 else:
218 calib._detectorName = None
220 if 'detectorSerial' in dictionary:
221 calib._detectorSerial = dictionary.get('detectorSerial')
222 elif 'DETECTOR_SERIAL' in dictionary:
223 calib._detectorSerial = dictionary.get('DETECTOR_SERIAL')
224 elif 'DET_SER' in dictionary['metadata']:
225 calib._detectorSerial = dictionary['metadata']['DET_SER']
226 else:
227 calib._detectorSerial = None
229 if 'detectorId' in dictionary:
230 calib._detectorId = dictionary.get('detectorId')
231 elif 'DETECTOR' in dictionary:
232 calib._detectorId = dictionary.get('DETECTOR')
233 elif 'DETECTOR' in dictionary['metadata']:
234 calib._detectorId = dictionary['metadata']['DETECTOR']
235 elif calib._detectorSerial:
236 calib._detectorId = calib._detectorSerial
237 else:
238 calib._detectorId = None
240 if 'instrument' in dictionary:
241 calib._instrument = dictionary.get('instrument')
242 elif 'INSTRUME' in dictionary['metadata']:
243 calib._instrument = dictionary['metadata']['INSTRUME']
244 else:
245 calib._instrument = None
247 calib.hasCrosstalk = dictionary.get('hasCrosstalk',
248 dictionary['metadata'].get('HAS_CROSSTALK', False))
249 if calib.hasCrosstalk:
250 calib.nAmp = dictionary.get('nAmp', dictionary['metadata'].get('NAMP', 0))
251 calib.crosstalkShape = (calib.nAmp, calib.nAmp)
252 calib.coeffs = np.array(dictionary['coeffs']).reshape(calib.crosstalkShape)
253 if 'coeffErr' in dictionary:
254 calib.coeffErr = np.array(dictionary['coeffErr']).reshape(calib.crosstalkShape)
255 else:
256 calib.coeffErr = np.zeros_like(calib.coeffs)
257 if 'coeffNum' in dictionary:
258 calib.coeffNum = np.array(dictionary['coeffNum']).reshape(calib.crosstalkShape)
259 else:
260 calib.coeffNum = np.zeros_like(calib.coeffs, dtype=int)
261 if 'coeffValid' in dictionary:
262 calib.coeffValid = np.array(dictionary['coeffValid']).reshape(calib.crosstalkShape)
263 else:
264 calib.coeffValid = np.ones_like(calib.coeffs, dtype=bool)
266 calib.interChip = dictionary.get('interChip', None)
267 if calib.interChip:
268 for detector in calib.interChip:
269 coeffVector = calib.interChip[detector]
270 calib.interChip[detector] = np.array(coeffVector).reshape(calib.crosstalkShape)
272 calib.updateMetadata()
273 return calib
275 def toDict(self):
276 """Return a dictionary containing the calibration properties.
278 The dictionary should be able to be round-tripped through
279 `fromDict`.
281 Returns
282 -------
283 dictionary : `dict`
284 Dictionary of properties.
285 """
286 self.updateMetadata()
288 outDict = {}
289 metadata = self.getMetadata()
290 outDict['metadata'] = metadata
292 outDict['hasCrosstalk'] = self.hasCrosstalk
293 outDict['nAmp'] = self.nAmp
294 outDict['crosstalkShape'] = self.crosstalkShape
296 ctLength = self.nAmp*self.nAmp
297 outDict['coeffs'] = self.coeffs.reshape(ctLength).tolist()
299 if self.coeffErr is not None:
300 outDict['coeffErr'] = self.coeffErr.reshape(ctLength).tolist()
301 if self.coeffNum is not None:
302 outDict['coeffNum'] = self.coeffNum.reshape(ctLength).tolist()
303 if self.coeffValid is not None:
304 outDict['coeffValid'] = self.coeffValid.reshape(ctLength).tolist()
306 if self.interChip:
307 outDict['interChip'] = dict()
308 for detector in self.interChip:
309 outDict['interChip'][detector] = self.interChip[detector].reshape(ctLength).tolist()
311 return outDict
313 @classmethod
314 def fromTable(cls, tableList):
315 """Construct calibration from a list of tables.
317 This method uses the `fromDict` method to create the
318 calibration, after constructing an appropriate dictionary from
319 the input tables.
321 Parameters
322 ----------
323 tableList : `list` [`lsst.afw.table.Table`]
324 List of tables to use to construct the crosstalk
325 calibration.
327 Returns
328 -------
329 calib : `lsst.ip.isr.CrosstalkCalib`
330 The calibration defined in the tables.
332 """
333 coeffTable = tableList[0]
335 metadata = coeffTable.meta
336 inDict = dict()
337 inDict['metadata'] = metadata
338 inDict['hasCrosstalk'] = metadata['HAS_CROSSTALK']
339 inDict['nAmp'] = metadata['NAMP']
341 inDict['coeffs'] = coeffTable['CT_COEFFS']
342 if 'CT_ERRORS' in coeffTable.columns:
343 inDict['coeffErr'] = coeffTable['CT_ERRORS']
344 if 'CT_COUNTS' in coeffTable.columns:
345 inDict['coeffNum'] = coeffTable['CT_COUNTS']
346 if 'CT_VALID' in coeffTable.columns:
347 inDict['coeffValid'] = coeffTable['CT_VALID']
349 if len(tableList) > 1:
350 inDict['interChip'] = dict()
351 interChipTable = tableList[1]
352 for record in interChipTable:
353 inDict['interChip'][record['IC_SOURCE_DET']] = record['IC_COEFFS']
355 return cls().fromDict(inDict)
357 def toTable(self):
358 """Construct a list of tables containing the information in this calibration.
360 The list of tables should create an identical calibration
361 after being passed to this class's fromTable method.
363 Returns
364 -------
365 tableList : `list` [`lsst.afw.table.Table`]
366 List of tables containing the crosstalk calibration
367 information.
369 """
370 tableList = []
371 self.updateMetadata()
372 catalog = Table([{'CT_COEFFS': self.coeffs.reshape(self.nAmp*self.nAmp),
373 'CT_ERRORS': self.coeffErr.reshape(self.nAmp*self.nAmp),
374 'CT_COUNTS': self.coeffNum.reshape(self.nAmp*self.nAmp),
375 'CT_VALID': self.coeffValid.reshape(self.nAmp*self.nAmp),
376 }])
377 # filter None, because astropy can't deal.
378 inMeta = self.getMetadata().toDict()
379 outMeta = {k: v for k, v in inMeta.items() if v is not None}
380 outMeta.update({k: "" for k, v in inMeta.items() if v is None})
381 catalog.meta = outMeta
382 tableList.append(catalog)
384 if self.interChip:
385 interChipTable = Table([{'IC_SOURCE_DET': sourceDet,
386 'IC_COEFFS': self.interChip[sourceDet].reshape(self.nAmp*self.nAmp)}
387 for sourceDet in self.interChip.keys()])
388 tableList.append(interChipTable)
389 return tableList
391 # Implementation methods.
392 @staticmethod
393 def extractAmp(image, amp, ampTarget, isTrimmed=False):
394 """Extract the image data from an amp, flipped to match ampTarget.
396 Parameters
397 ----------
398 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
399 Image containing the amplifier of interest.
400 amp : `lsst.afw.cameraGeom.Amplifier`
401 Amplifier on image to extract.
402 ampTarget : `lsst.afw.cameraGeom.Amplifier`
403 Target amplifier that the extracted image will be flipped
404 to match.
405 isTrimmed : `bool`
406 The image is already trimmed.
407 TODO : DM-15409 will resolve this.
409 Returns
410 -------
411 output : `lsst.afw.image.Image`
412 Image of the amplifier in the desired configuration.
413 """
414 X_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False,
415 lsst.afw.cameraGeom.ReadoutCorner.LR: True,
416 lsst.afw.cameraGeom.ReadoutCorner.UL: False,
417 lsst.afw.cameraGeom.ReadoutCorner.UR: True}
418 Y_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False,
419 lsst.afw.cameraGeom.ReadoutCorner.LR: False,
420 lsst.afw.cameraGeom.ReadoutCorner.UL: True,
421 lsst.afw.cameraGeom.ReadoutCorner.UR: True}
423 output = image[amp.getBBox() if isTrimmed else amp.getRawDataBBox()]
424 thisAmpCorner = amp.getReadoutCorner()
425 targetAmpCorner = ampTarget.getReadoutCorner()
427 # Flipping is necessary only if the desired configuration doesn't match what we currently have
428 xFlip = X_FLIP[targetAmpCorner] ^ X_FLIP[thisAmpCorner]
429 yFlip = Y_FLIP[targetAmpCorner] ^ Y_FLIP[thisAmpCorner]
430 return lsst.afw.math.flipImage(output, xFlip, yFlip)
432 @staticmethod
433 def calculateBackground(mi, badPixels=["BAD"]):
434 """Estimate median background in image.
436 Getting a great background model isn't important for crosstalk correction,
437 since the crosstalk is at a low level. The median should be sufficient.
439 Parameters
440 ----------
441 mi : `lsst.afw.image.MaskedImage`
442 MaskedImage for which to measure background.
443 badPixels : `list` of `str`
444 Mask planes to ignore.
445 Returns
446 -------
447 bg : `float`
448 Median background level.
449 """
450 mask = mi.getMask()
451 stats = lsst.afw.math.StatisticsControl()
452 stats.setAndMask(mask.getPlaneBitMask(badPixels))
453 return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue()
455 def subtractCrosstalk(self, thisExposure, sourceExposure=None, crosstalkCoeffs=None,
456 badPixels=["BAD"], minPixelToMask=45000,
457 crosstalkStr="CROSSTALK", isTrimmed=False,
458 backgroundMethod="None"):
459 """Subtract the crosstalk from thisExposure, optionally using a different source.
461 We set the mask plane indicated by ``crosstalkStr`` in a target amplifier
462 for pixels in a source amplifier that exceed ``minPixelToMask``. Note that
463 the correction is applied to all pixels in the amplifier, but only those
464 that have a substantial crosstalk are masked with ``crosstalkStr``.
466 The uncorrected image is used as a template for correction. This is good
467 enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's
468 larger you may want to iterate.
470 Parameters
471 ----------
472 thisExposure : `lsst.afw.image.Exposure`
473 Exposure for which to subtract crosstalk.
474 sourceExposure : `lsst.afw.image.Exposure`, optional
475 Exposure to use as the source of the crosstalk. If not set,
476 thisExposure is used as the source (intra-detector crosstalk).
477 crosstalkCoeffs : `numpy.ndarray`, optional.
478 Coefficients to use to correct crosstalk.
479 badPixels : `list` of `str`
480 Mask planes to ignore.
481 minPixelToMask : `float`
482 Minimum pixel value (relative to the background level) in
483 source amplifier for which to set ``crosstalkStr`` mask plane
484 in target amplifier.
485 crosstalkStr : `str`
486 Mask plane name for pixels greatly modified by crosstalk
487 (above minPixelToMask).
488 isTrimmed : `bool`
489 The image is already trimmed.
490 This should no longer be needed once DM-15409 is resolved.
491 backgroundMethod : `str`
492 Method used to subtract the background. "AMP" uses
493 amplifier-by-amplifier background levels, "DETECTOR" uses full
494 exposure/maskedImage levels. Any other value results in no
495 background subtraction.
496 """
497 mi = thisExposure.getMaskedImage()
498 mask = mi.getMask()
499 detector = thisExposure.getDetector()
500 if self.hasCrosstalk is False:
501 self.fromDetector(detector, coeffVector=crosstalkCoeffs)
503 numAmps = len(detector)
504 if numAmps != self.nAmp:
505 raise RuntimeError(f"Crosstalk built for {self.nAmp} in {self._detectorName}, received "
506 f"{numAmps} in {detector.getName()}")
508 if sourceExposure:
509 source = sourceExposure.getMaskedImage()
510 sourceDetector = sourceExposure.getDetector()
511 else:
512 source = mi
513 sourceDetector = detector
515 if crosstalkCoeffs is not None:
516 coeffs = crosstalkCoeffs
517 else:
518 coeffs = self.coeffs
519 self.log.debug("CT COEFF: %s", coeffs)
520 # Set background level based on the requested method. The
521 # thresholdBackground holds the offset needed so that we only mask
522 # pixels high relative to the background, not in an absolute
523 # sense.
524 thresholdBackground = self.calculateBackground(source, badPixels)
526 backgrounds = [0.0 for amp in sourceDetector]
527 if backgroundMethod is None:
528 pass
529 elif backgroundMethod == "AMP":
530 backgrounds = [self.calculateBackground(source[amp.getBBox()], badPixels)
531 for amp in sourceDetector]
532 elif backgroundMethod == "DETECTOR":
533 backgrounds = [self.calculateBackground(source, badPixels) for amp in sourceDetector]
535 # Set the crosstalkStr bit for the bright pixels (those which will have
536 # significant crosstalk correction)
537 crosstalkPlane = mask.addMaskPlane(crosstalkStr)
538 footprints = lsst.afw.detection.FootprintSet(source,
539 lsst.afw.detection.Threshold(minPixelToMask
540 + thresholdBackground))
541 footprints.setMask(mask, crosstalkStr)
542 crosstalk = mask.getPlaneBitMask(crosstalkStr)
544 # Define a subtrahend image to contain all the scaled crosstalk signals
545 subtrahend = source.Factory(source.getBBox())
546 subtrahend.set((0, 0, 0))
548 coeffs = coeffs.transpose()
549 for ii, iAmp in enumerate(sourceDetector):
550 iImage = subtrahend[iAmp.getBBox() if isTrimmed else iAmp.getRawDataBBox()]
551 for jj, jAmp in enumerate(detector):
552 if coeffs[ii, jj] == 0.0:
553 continue
554 jImage = self.extractAmp(mi, jAmp, iAmp, isTrimmed)
555 jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks
556 jImage -= backgrounds[jj]
557 iImage.scaledPlus(coeffs[ii, jj], jImage)
559 # Set crosstalkStr bit only for those pixels that have been significantly modified (i.e., those
560 # masked as such in 'subtrahend'), not necessarily those that are bright originally.
561 mask.clearMaskPlane(crosstalkPlane)
562 mi -= subtrahend # also sets crosstalkStr bit for bright pixels
565class CrosstalkConfig(Config):
566 """Configuration for intra-detector crosstalk removal."""
567 minPixelToMask = Field(
568 dtype=float,
569 doc="Set crosstalk mask plane for pixels over this value.",
570 default=45000
571 )
572 crosstalkMaskPlane = Field(
573 dtype=str,
574 doc="Name for crosstalk mask plane.",
575 default="CROSSTALK"
576 )
577 crosstalkBackgroundMethod = ChoiceField(
578 dtype=str,
579 doc="Type of background subtraction to use when applying correction.",
580 default="None",
581 allowed={
582 "None": "Do no background subtraction.",
583 "AMP": "Subtract amplifier-by-amplifier background levels.",
584 "DETECTOR": "Subtract detector level background."
585 },
586 )
587 useConfigCoefficients = Field(
588 dtype=bool,
589 doc="Ignore the detector crosstalk information in favor of CrosstalkConfig values?",
590 default=False,
591 )
592 crosstalkValues = ListField(
593 dtype=float,
594 doc=("Amplifier-indexed crosstalk coefficients to use. This should be arranged as a 1 x nAmp**2 "
595 "list of coefficients, such that when reshaped by crosstalkShape, the result is nAmp x nAmp. "
596 "This matrix should be structured so CT * [amp0 amp1 amp2 ...]^T returns the column "
597 "vector [corr0 corr1 corr2 ...]^T."),
598 default=[0.0],
599 )
600 crosstalkShape = ListField(
601 dtype=int,
602 doc="Shape of the coefficient array. This should be equal to [nAmp, nAmp].",
603 default=[1],
604 )
606 def getCrosstalk(self, detector=None):
607 """Return a 2-D numpy array of crosstalk coefficients in the proper shape.
609 Parameters
610 ----------
611 detector : `lsst.afw.cameraGeom.detector`
612 Detector that is to be crosstalk corrected.
614 Returns
615 -------
616 coeffs : `numpy.ndarray`
617 Crosstalk coefficients that can be used to correct the detector.
619 Raises
620 ------
621 RuntimeError
622 Raised if no coefficients could be generated from this detector/configuration.
623 """
624 if self.useConfigCoefficients is True:
625 coeffs = np.array(self.crosstalkValues).reshape(self.crosstalkShape)
626 if detector is not None:
627 nAmp = len(detector)
628 if coeffs.shape != (nAmp, nAmp):
629 raise RuntimeError("Constructed crosstalk coeffients do not match detector shape. "
630 f"{coeffs.shape} {nAmp}")
631 return coeffs
632 elif detector is not None and detector.hasCrosstalk() is True:
633 # Assume the detector defines itself consistently.
634 return detector.getCrosstalk()
635 else:
636 raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients")
638 def hasCrosstalk(self, detector=None):
639 """Return a boolean indicating if crosstalk coefficients exist.
641 Parameters
642 ----------
643 detector : `lsst.afw.cameraGeom.detector`
644 Detector that is to be crosstalk corrected.
646 Returns
647 -------
648 hasCrosstalk : `bool`
649 True if this detector/configuration has crosstalk coefficients defined.
650 """
651 if self.useConfigCoefficients is True and self.crosstalkValues is not None:
652 return True
653 elif detector is not None and detector.hasCrosstalk() is True:
654 return True
655 else:
656 return False
659class CrosstalkTask(Task):
660 """Apply intra-detector crosstalk correction."""
661 ConfigClass = CrosstalkConfig
662 _DefaultName = 'isrCrosstalk'
664 def prepCrosstalk(self, dataRef, crosstalk=None):
665 """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk.
667 Parameters
668 ----------
669 dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
670 Butler reference of the detector data to be processed.
671 crosstalk : `~lsst.ip.isr.CrosstalkConfig`
672 Crosstalk calibration that will be used.
674 See also
675 --------
676 lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk
677 """
678 return
680 def run(self, exposure, crosstalk=None,
681 crosstalkSources=None, isTrimmed=False, camera=None):
682 """Apply intra-detector crosstalk correction
684 Parameters
685 ----------
686 exposure : `lsst.afw.image.Exposure`
687 Exposure for which to remove crosstalk.
688 crosstalkCalib : `lsst.ip.isr.CrosstalkCalib`, optional
689 External crosstalk calibration to apply. Constructed from
690 detector if not found.
691 crosstalkSources : `defaultdict`, optional
692 Image data for other detectors that are sources of
693 crosstalk in exposure. The keys are expected to be names
694 of the other detectors, with the values containing
695 `lsst.afw.image.Exposure` at the same level of processing
696 as ``exposure``.
697 The default for intra-detector crosstalk here is None.
698 isTrimmed : `bool`, optional
699 The image is already trimmed.
700 This should no longer be needed once DM-15409 is resolved.
701 camera : `lsst.afw.cameraGeom.Camera`, optional
702 Camera associated with this exposure. Only used for
703 inter-chip matching.
705 Raises
706 ------
707 RuntimeError
708 Raised if called for a detector that does not have a
709 crosstalk correction. Also raised if the crosstalkSource
710 is not an expected type.
711 """
712 if not crosstalk:
713 crosstalk = CrosstalkCalib(log=self.log)
714 crosstalk = crosstalk.fromDetector(exposure.getDetector(),
715 coeffVector=self.config.crosstalkValues)
716 if not crosstalk.log:
717 crosstalk.log = self.log
718 if not crosstalk.hasCrosstalk:
719 raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients.")
721 else:
722 self.log.info("Applying crosstalk correction.")
723 crosstalk.subtractCrosstalk(exposure, crosstalkCoeffs=crosstalk.coeffs,
724 minPixelToMask=self.config.minPixelToMask,
725 crosstalkStr=self.config.crosstalkMaskPlane, isTrimmed=isTrimmed,
726 backgroundMethod=self.config.crosstalkBackgroundMethod)
728 if crosstalk.interChip:
729 if crosstalkSources:
730 # Parse crosstalkSources: Identify which detectors we have available
731 if isinstance(crosstalkSources[0], lsst.afw.image.Exposure):
732 # Received afwImage.Exposure
733 sourceNames = [exp.getDetector().getName() for exp in crosstalkSources]
734 elif isinstance(crosstalkSources[0], lsst.daf.butler.DeferredDatasetHandle):
735 # Received dafButler.DeferredDatasetHandle
736 detectorList = [source.dataId['detector'] for source in crosstalkSources]
737 sourceNames = [camera[detector].getName() for detector in detectorList]
738 else:
739 raise RuntimeError("Unknown object passed as crosstalk sources.",
740 type(crosstalkSources[0]))
742 for detName in crosstalk.interChip:
743 if detName not in sourceNames:
744 self.log.warning("Crosstalk lists %s, not found in sources: %s",
745 detName, sourceNames)
746 continue
747 # Get the coefficients.
748 interChipCoeffs = crosstalk.interChip[detName]
750 sourceExposure = crosstalkSources[sourceNames.index(detName)]
751 if isinstance(sourceExposure, lsst.daf.butler.DeferredDatasetHandle):
752 # Dereference the dafButler.DeferredDatasetHandle.
753 sourceExposure = sourceExposure.get()
754 if not isinstance(sourceExposure, lsst.afw.image.Exposure):
755 raise RuntimeError("Unknown object passed as crosstalk sources.",
756 type(sourceExposure))
758 self.log.info("Correcting detector %s with ctSource %s",
759 exposure.getDetector().getName(),
760 sourceExposure.getDetector().getName())
761 crosstalk.subtractCrosstalk(exposure, sourceExposure=sourceExposure,
762 crosstalkCoeffs=interChipCoeffs,
763 minPixelToMask=self.config.minPixelToMask,
764 crosstalkStr=self.config.crosstalkMaskPlane,
765 isTrimmed=isTrimmed,
766 backgroundMethod=self.config.crosstalkBackgroundMethod)
767 else:
768 self.log.warning("Crosstalk contains interChip coefficients, but no sources found!")
771class NullCrosstalkTask(CrosstalkTask):
772 def run(self, exposure, crosstalkSources=None):
773 self.log.info("Not performing any crosstalk correction")