LSSTApplications  21.0.0+75b29a8a7f,21.0.0+e70536a077,21.0.0-1-ga51b5d4+62c747d40b,21.0.0-11-ga6ea59e8e+47cba9fc36,21.0.0-2-g103fe59+914993bf7c,21.0.0-2-g1367e85+e2614ded12,21.0.0-2-g45278ab+e70536a077,21.0.0-2-g4bc9b9f+7b2b5f8678,21.0.0-2-g5242d73+e2614ded12,21.0.0-2-g54e2caa+6403186824,21.0.0-2-g7f82c8f+3ac4acbffc,21.0.0-2-g8dde007+04a6aea1af,21.0.0-2-g8f08a60+9402881886,21.0.0-2-ga326454+3ac4acbffc,21.0.0-2-ga63a54e+81dd751046,21.0.0-2-gc738bc1+5f65c6e7a9,21.0.0-2-gde069b7+26c92b3210,21.0.0-2-gecfae73+0993ddc9bd,21.0.0-2-gfc62afb+e2614ded12,21.0.0-21-gba890a8+5a4f502a26,21.0.0-23-g9966ff26+03098d1af8,21.0.0-3-g357aad2+8ad216c477,21.0.0-3-g4be5c26+e2614ded12,21.0.0-3-g6d51c4a+4d2fe0280d,21.0.0-3-g7d9da8d+75b29a8a7f,21.0.0-3-gaa929c8+522e0f12c2,21.0.0-3-ge02ed75+4d2fe0280d,21.0.0-4-g3300ddd+e70536a077,21.0.0-4-gc004bbf+eac6615e82,21.0.0-4-gccdca77+f94adcd104,21.0.0-4-gd1c1571+18b81799f9,21.0.0-5-g7b47fff+4d2fe0280d,21.0.0-5-gb155db7+d2632f662b,21.0.0-5-gdf36809+637e4641ee,21.0.0-6-g722ad07+28c848f42a,21.0.0-7-g959bb79+522e0f12c2,21.0.0-7-gfd72ab2+cf01990774,21.0.0-9-g87fb7b8d+e2ab11cdd6,w.2021.04
LSSTDataManagementBasePackage
crosstalk.py
Go to the documentation of this file.
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 """
23 Apply intra-detector crosstalk corrections
24 """
25 import numpy as np
26 from astropy.table import Table
27 
28 import lsst.afw.math
29 import lsst.afw.detection
30 from lsst.pex.config import Config, Field, ChoiceField, ListField
31 from lsst.pipe.base import Task
32 
33 from lsst.ip.isr import IsrCalib
34 
35 
36 __all__ = ["CrosstalkCalib", "CrosstalkConfig", "CrosstalkTask",
37  "NullCrosstalkTask"]
38 
39 
41  """Calibration of amp-to-amp crosstalk coefficients.
42 
43  Parameters
44  ----------
45  detector : `lsst.afw.cameraGeom.Detector`, optional
46  Detector to use to pull coefficients from.
47  nAmp : `int`, optional
48  Number of amplifiers to initialize.
49  log : `lsst.log.Log`, optional
50  Log to write messages to.
51  **kwargs :
52  Parameters to pass to parent constructor.
53 
54  Notes
55  -----
56  The crosstalk attributes stored are:
57 
58  hasCrosstalk : `bool`
59  Whether there is crosstalk defined for this detector.
60  nAmp : `int`
61  Number of amplifiers in this detector.
62  crosstalkShape : `tuple` [`int`, `int`]
63  A tuple containing the shape of the ``coeffs`` matrix. This
64  should be equivalent to (``nAmp``, ``nAmp``).
65  coeffs : `np.ndarray`
66  A matrix containing the crosstalk coefficients. coeff[i][j]
67  contains the coefficients to calculate the contribution
68  amplifier_j has on amplifier_i (each row[i] contains the
69  corrections for detector_i).
70  coeffErr : `np.ndarray`, optional
71  A matrix (as defined by ``coeffs``) containing the standard
72  distribution of the crosstalk measurements.
73  coeffNum : `np.ndarray`, optional
74  A matrix containing the number of pixel pairs used to measure
75  the ``coeffs`` and ``coeffErr``.
76  coeffValid : `np.ndarray`, optional
77  A matrix of Boolean values indicating if the coefficient is
78  valid, defined as abs(coeff) > coeffErr / sqrt(coeffNum).
79  interChip : `dict` [`np.ndarray`]
80  A dictionary keyed by detectorName containing ``coeffs``
81  matrices used to correct for inter-chip crosstalk with a
82  source on the detector indicated.
83 
84  """
85  _OBSTYPE = 'CROSSTALK'
86  _SCHEMA = 'Gen3 Crosstalk'
87  _VERSION = 1.0
88 
89  def __init__(self, detector=None, nAmp=0, **kwargs):
90  self.hasCrosstalk = False
91  self.nAmp = nAmp if nAmp else 0
92  self.crosstalkShape = (self.nAmp, self.nAmp)
93 
94  self.coeffs = np.zeros(self.crosstalkShape) if self.nAmp else None
95  self.coeffErr = np.zeros(self.crosstalkShape) if self.nAmp else None
96  self.coeffNum = np.zeros(self.crosstalkShape,
97  dtype=int) if self.nAmp else None
98  self.coeffValid = np.zeros(self.crosstalkShape,
99  dtype=bool) if self.nAmp else None
100  self.interChip = {}
101 
102  super().__init__(**kwargs)
103  self.requiredAttributes.update(['hasCrosstalk', 'nAmp', 'coeffs',
104  'coeffErr', 'coeffNum', 'coeffValid',
105  'interChip'])
106  if detector:
107  self.fromDetector(detector)
108 
109  def updateMetadata(self, setDate=False, **kwargs):
110  """Update calibration metadata.
111 
112  This calls the base class's method after ensuring the required
113  calibration keywords will be saved.
114 
115  Parameters
116  ----------
117  setDate : `bool`, optional
118  Update the CALIBDATE fields in the metadata to the current
119  time. Defaults to False.
120  kwargs :
121  Other keyword parameters to set in the metadata.
122  """
123  kwargs['DETECTOR'] = self._detectorId
124  kwargs['DETECTOR_NAME'] = self._detectorName
125  kwargs['DETECTOR_SERIAL'] = self._detectorSerial
126  kwargs['HAS_CROSSTALK'] = self.hasCrosstalk
127  kwargs['NAMP'] = self.nAmp
128  self.crosstalkShape = (self.nAmp, self.nAmp)
129  kwargs['CROSSTALK_SHAPE'] = self.crosstalkShape
130 
131  super().updateMetadata(setDate=setDate, **kwargs)
132 
133  def fromDetector(self, detector, coeffVector=None):
134  """Set calibration parameters from the detector.
135 
136  Parameters
137  ----------
138  detector : `lsst.afw.cameraGeom.Detector`
139  Detector to use to set parameters from.
140  coeffVector : `numpy.array`, optional
141  Use the detector geometry (bounding boxes and flip
142  information), but use ``coeffVector`` instead of the
143  output of ``detector.getCrosstalk()``.
144 
145  Returns
146  -------
147  calib : `lsst.ip.isr.CrosstalkCalib`
148  The calibration constructed from the detector.
149 
150  """
151  if detector.hasCrosstalk() or coeffVector:
152  self._detectorName = detector.getName()
153  self._detectorSerial = detector.getSerial()
154 
155  self.nAmp = len(detector)
156  self.crosstalkShape = (self.nAmp, self.nAmp)
157 
158  if coeffVector is not None:
159  crosstalkCoeffs = coeffVector
160  else:
161  crosstalkCoeffs = detector.getCrosstalk()
162  if len(crosstalkCoeffs) == 1 and crosstalkCoeffs[0] == 0.0:
163  return self
164  self.coeffs = np.array(crosstalkCoeffs).reshape(self.crosstalkShape)
165 
166  if self.coeffs.shape != self.crosstalkShape:
167  raise RuntimeError("Crosstalk coefficients do not match detector shape. "
168  f"{self.crosstalkShape} {self.nAmp}")
169 
170  self.interChip = {}
171  self.hasCrosstalk = True
172  self.updateMetadata()
173  return self
174 
175  @classmethod
176  def fromDict(cls, dictionary):
177  """Construct a calibration from a dictionary of properties.
178 
179  Must be implemented by the specific calibration subclasses.
180 
181  Parameters
182  ----------
183  dictionary : `dict`
184  Dictionary of properties.
185 
186  Returns
187  -------
188  calib : `lsst.ip.isr.CalibType`
189  Constructed calibration.
190 
191  Raises
192  ------
193  RuntimeError :
194  Raised if the supplied dictionary is for a different
195  calibration.
196  """
197  calib = cls()
198 
199  if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
200  raise RuntimeError(f"Incorrect crosstalk supplied. Expected {calib._OBSTYPE}, "
201  f"found {dictionary['metadata']['OBSTYPE']}")
202 
203  calib.setMetadata(dictionary['metadata'])
204 
205  if 'detectorName' in dictionary:
206  calib._detectorName = dictionary.get('detectorName')
207  elif 'DETECTOR_NAME' in dictionary:
208  calib._detectorName = dictionary.get('DETECTOR_NAME')
209  elif 'DET_NAME' in dictionary['metadata']:
210  calib._detectorName = dictionary['metadata']['DET_NAME']
211  else:
212  calib._detectorName = None
213 
214  if 'detectorSerial' in dictionary:
215  calib._detectorSerial = dictionary.get('detectorSerial')
216  elif 'DETECTOR_SERIAL' in dictionary:
217  calib._detectorSerial = dictionary.get('DETECTOR_SERIAL')
218  elif 'DET_SER' in dictionary['metadata']:
219  calib._detectorSerial = dictionary['metadata']['DET_SER']
220  else:
221  calib._detectorSerial = None
222 
223  if 'detectorId' in dictionary:
224  calib._detectorId = dictionary.get('detectorId')
225  elif 'DETECTOR' in dictionary:
226  calib._detectorId = dictionary.get('DETECTOR')
227  elif 'DETECTOR' in dictionary['metadata']:
228  calib._detectorId = dictionary['metadata']['DETECTOR']
229  elif calib._detectorSerial:
230  calib._detectorId = calib._detectorSerial
231  else:
232  calib._detectorId = None
233 
234  if 'instrument' in dictionary:
235  calib._instrument = dictionary.get('instrument')
236  elif 'INSTRUME' in dictionary['metadata']:
237  calib._instrument = dictionary['metadata']['INSTRUME']
238  else:
239  calib._instrument = None
240 
241  calib.hasCrosstalk = dictionary.get('hasCrosstalk',
242  dictionary['metadata'].get('HAS_CROSSTALK', False))
243  if calib.hasCrosstalk:
244  calib.nAmp = dictionary.get('nAmp', dictionary['metadata'].get('NAMP', 0))
245  calib.crosstalkShape = (calib.nAmp, calib.nAmp)
246  calib.coeffs = np.array(dictionary['coeffs']).reshape(calib.crosstalkShape)
247  if 'coeffErr' in dictionary:
248  calib.coeffErr = np.array(dictionary['coeffErr']).reshape(calib.crosstalkShape)
249  else:
250  calib.coeffErr = np.zeros_like(calib.coeffs)
251  if 'coeffNum' in dictionary:
252  calib.coeffNum = np.array(dictionary['coeffNum']).reshape(calib.crosstalkShape)
253  else:
254  calib.coeffNum = np.zeros_like(calib.coeffs, dtype=int)
255  if 'coeffValid' in dictionary:
256  calib.coeffValid = np.array(dictionary['coeffValid']).reshape(calib.crosstalkShape)
257  else:
258  calib.coeffValid = np.ones_like(calib.coeffs, dtype=bool)
259 
260  calib.interChip = dictionary.get('interChip', None)
261  if calib.interChip:
262  for detector in calib.interChip:
263  coeffVector = calib.interChip[detector]
264  calib.interChip[detector] = np.array(coeffVector).reshape(calib.crosstalkShape)
265 
266  calib.updateMetadata()
267  return calib
268 
269  def toDict(self):
270  """Return a dictionary containing the calibration properties.
271 
272  The dictionary should be able to be round-tripped through
273  `fromDict`.
274 
275  Returns
276  -------
277  dictionary : `dict`
278  Dictionary of properties.
279  """
280  self.updateMetadata()
281 
282  outDict = {}
283  metadata = self.getMetadata()
284  outDict['metadata'] = metadata
285 
286  outDict['hasCrosstalk'] = self.hasCrosstalk
287  outDict['nAmp'] = self.nAmp
288  outDict['crosstalkShape'] = self.crosstalkShape
289 
290  ctLength = self.nAmp*self.nAmp
291  outDict['coeffs'] = self.coeffs.reshape(ctLength).tolist()
292 
293  if self.coeffErr is not None:
294  outDict['coeffErr'] = self.coeffErr.reshape(ctLength).tolist()
295  if self.coeffNum is not None:
296  outDict['coeffNum'] = self.coeffNum.reshape(ctLength).tolist()
297  if self.coeffValid is not None:
298  outDict['coeffValid'] = self.coeffValid.reshape(ctLength).tolist()
299 
300  if self.interChip:
301  outDict['interChip'] = dict()
302  for detector in self.interChip:
303  outDict['interChip'][detector] = self.interChip[detector].reshape(ctLength).tolist()
304 
305  return outDict
306 
307  @classmethod
308  def fromTable(cls, tableList):
309  """Construct calibration from a list of tables.
310 
311  This method uses the `fromDict` method to create the
312  calibration, after constructing an appropriate dictionary from
313  the input tables.
314 
315  Parameters
316  ----------
317  tableList : `list` [`lsst.afw.table.Table`]
318  List of tables to use to construct the crosstalk
319  calibration.
320 
321  Returns
322  -------
323  calib : `lsst.ip.isr.CrosstalkCalib`
324  The calibration defined in the tables.
325 
326  """
327  coeffTable = tableList[0]
328 
329  metadata = coeffTable.meta
330  inDict = dict()
331  inDict['metadata'] = metadata
332  inDict['hasCrosstalk'] = metadata['HAS_CROSSTALK']
333  inDict['nAmp'] = metadata['NAMP']
334 
335  inDict['coeffs'] = coeffTable['CT_COEFFS']
336  if 'CT_ERRORS' in coeffTable:
337  inDict['coeffErr'] = coeffTable['CT_ERRORS']
338  if 'CT_COUNTS' in coeffTable:
339  inDict['coeffNum'] = coeffTable['CT_COUNTS']
340  if 'CT_VALID' in coeffTable:
341  inDict['coeffValid'] = coeffTable['CT_VALID']
342 
343  if len(tableList) > 1:
344  inDict['interChip'] = dict()
345  interChipTable = tableList[1]
346  for record in interChipTable:
347  inDict['interChip'][record['IC_SOURCE_DET']] = record['IC_COEFFS']
348 
349  return cls().fromDict(inDict)
350 
351  def toTable(self):
352  """Construct a list of tables containing the information in this calibration.
353 
354  The list of tables should create an identical calibration
355  after being passed to this class's fromTable method.
356 
357  Returns
358  -------
359  tableList : `list` [`lsst.afw.table.Table`]
360  List of tables containing the crosstalk calibration
361  information.
362 
363  """
364  tableList = []
365  self.updateMetadata()
366  catalog = Table([{'CT_COEFFS': self.coeffs.reshape(self.nAmp*self.nAmp),
367  'CT_ERRORS': self.coeffErr.reshape(self.nAmp*self.nAmp),
368  'CT_COUNTS': self.coeffNum.reshape(self.nAmp*self.nAmp),
369  'CT_VALID': self.coeffValid.reshape(self.nAmp*self.nAmp),
370  }])
371  # filter None, because astropy can't deal.
372  inMeta = self.getMetadata().toDict()
373  outMeta = {k: v for k, v in inMeta.items() if v is not None}
374  outMeta.update({k: "" for k, v in inMeta.items() if v is None})
375  catalog.meta = outMeta
376  tableList.append(catalog)
377 
378  if self.interChip:
379  interChipTable = Table([{'IC_SOURCE_DET': sourceDet,
380  'IC_COEFFS': self.interChip[sourceDet].reshape(self.nAmp*self.nAmp)}
381  for sourceDet in self.interChip.keys()])
382  tableList.append(interChipTable)
383  return tableList
384 
385  # Implementation methods.
386  @staticmethod
387  def extractAmp(image, amp, ampTarget, isTrimmed=False):
388  """Extract the image data from an amp, flipped to match ampTarget.
389 
390  Parameters
391  ----------
392  image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
393  Image containing the amplifier of interest.
394  amp : `lsst.afw.cameraGeom.Amplifier`
395  Amplifier on image to extract.
396  ampTarget : `lsst.afw.cameraGeom.Amplifier`
397  Target amplifier that the extracted image will be flipped
398  to match.
399  isTrimmed : `bool`
400  The image is already trimmed.
401  TODO : DM-15409 will resolve this.
402 
403  Returns
404  -------
405  output : `lsst.afw.image.Image`
406  Image of the amplifier in the desired configuration.
407  """
408  X_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False,
409  lsst.afw.cameraGeom.ReadoutCorner.LR: True,
410  lsst.afw.cameraGeom.ReadoutCorner.UL: False,
411  lsst.afw.cameraGeom.ReadoutCorner.UR: True}
412  Y_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False,
413  lsst.afw.cameraGeom.ReadoutCorner.LR: False,
414  lsst.afw.cameraGeom.ReadoutCorner.UL: True,
415  lsst.afw.cameraGeom.ReadoutCorner.UR: True}
416 
417  output = image[amp.getBBox() if isTrimmed else amp.getRawDataBBox()]
418  thisAmpCorner = amp.getReadoutCorner()
419  targetAmpCorner = ampTarget.getReadoutCorner()
420 
421  # Flipping is necessary only if the desired configuration doesn't match what we currently have
422  xFlip = X_FLIP[targetAmpCorner] ^ X_FLIP[thisAmpCorner]
423  yFlip = Y_FLIP[targetAmpCorner] ^ Y_FLIP[thisAmpCorner]
424  return lsst.afw.math.flipImage(output, xFlip, yFlip)
425 
426  @staticmethod
427  def calculateBackground(mi, badPixels=["BAD"]):
428  """Estimate median background in image.
429 
430  Getting a great background model isn't important for crosstalk correction,
431  since the crosstalk is at a low level. The median should be sufficient.
432 
433  Parameters
434  ----------
435  mi : `lsst.afw.image.MaskedImage`
436  MaskedImage for which to measure background.
437  badPixels : `list` of `str`
438  Mask planes to ignore.
439  Returns
440  -------
441  bg : `float`
442  Median background level.
443  """
444  mask = mi.getMask()
446  stats.setAndMask(mask.getPlaneBitMask(badPixels))
447  return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue()
448 
449  def subtractCrosstalk(self, thisExposure, sourceExposure=None, crosstalkCoeffs=None,
450  badPixels=["BAD"], minPixelToMask=45000,
451  crosstalkStr="CROSSTALK", isTrimmed=False,
452  backgroundMethod="None"):
453  """Subtract the crosstalk from thisExposure, optionally using a different source.
454 
455  We set the mask plane indicated by ``crosstalkStr`` in a target amplifier
456  for pixels in a source amplifier that exceed ``minPixelToMask``. Note that
457  the correction is applied to all pixels in the amplifier, but only those
458  that have a substantial crosstalk are masked with ``crosstalkStr``.
459 
460  The uncorrected image is used as a template for correction. This is good
461  enough if the crosstalk is small (e.g., coefficients < ~ 1e-3), but if it's
462  larger you may want to iterate.
463 
464  Parameters
465  ----------
466  thisExposure : `lsst.afw.image.Exposure`
467  Exposure for which to subtract crosstalk.
468  sourceExposure : `lsst.afw.image.Exposure`, optional
469  Exposure to use as the source of the crosstalk. If not set,
470  thisExposure is used as the source (intra-detector crosstalk).
471  crosstalkCoeffs : `numpy.ndarray`, optional.
472  Coefficients to use to correct crosstalk.
473  badPixels : `list` of `str`
474  Mask planes to ignore.
475  minPixelToMask : `float`
476  Minimum pixel value (relative to the background level) in
477  source amplifier for which to set ``crosstalkStr`` mask plane
478  in target amplifier.
479  crosstalkStr : `str`
480  Mask plane name for pixels greatly modified by crosstalk
481  (above minPixelToMask).
482  isTrimmed : `bool`
483  The image is already trimmed.
484  This should no longer be needed once DM-15409 is resolved.
485  backgroundMethod : `str`
486  Method used to subtract the background. "AMP" uses
487  amplifier-by-amplifier background levels, "DETECTOR" uses full
488  exposure/maskedImage levels. Any other value results in no
489  background subtraction.
490  """
491  mi = thisExposure.getMaskedImage()
492  mask = mi.getMask()
493  detector = thisExposure.getDetector()
494  if self.hasCrosstalk is False:
495  self.fromDetector(detector, coeffVector=crosstalkCoeffs)
496 
497  numAmps = len(detector)
498  if numAmps != self.nAmp:
499  raise RuntimeError(f"Crosstalk built for {self.nAmp} in {self._detectorName}, received "
500  f"{numAmps} in {detector.getName()}")
501 
502  if sourceExposure:
503  source = sourceExposure.getMaskedImage()
504  sourceDetector = sourceExposure.getDetector()
505  else:
506  source = mi
507  sourceDetector = detector
508 
509  if crosstalkCoeffs is not None:
510  coeffs = crosstalkCoeffs
511  else:
512  coeffs = self.coeffs
513  self.log.debug("CT COEFF: %s", coeffs)
514  # Set background level based on the requested method. The
515  # thresholdBackground holds the offset needed so that we only mask
516  # pixels high relative to the background, not in an absolute
517  # sense.
518  thresholdBackground = self.calculateBackground(source, badPixels)
519 
520  backgrounds = [0.0 for amp in sourceDetector]
521  if backgroundMethod is None:
522  pass
523  elif backgroundMethod == "AMP":
524  backgrounds = [self.calculateBackground(source[amp.getBBox()], badPixels)
525  for amp in sourceDetector]
526  elif backgroundMethod == "DETECTOR":
527  backgrounds = [self.calculateBackground(source, badPixels) for amp in sourceDetector]
528 
529  # Set the crosstalkStr bit for the bright pixels (those which will have
530  # significant crosstalk correction)
531  crosstalkPlane = mask.addMaskPlane(crosstalkStr)
532  footprints = lsst.afw.detection.FootprintSet(source,
533  lsst.afw.detection.Threshold(minPixelToMask
534  + thresholdBackground))
535  footprints.setMask(mask, crosstalkStr)
536  crosstalk = mask.getPlaneBitMask(crosstalkStr)
537 
538  # Define a subtrahend image to contain all the scaled crosstalk signals
539  subtrahend = source.Factory(source.getBBox())
540  subtrahend.set((0, 0, 0))
541 
542  coeffs = coeffs.transpose()
543  for ii, iAmp in enumerate(sourceDetector):
544  iImage = subtrahend[iAmp.getBBox() if isTrimmed else iAmp.getRawDataBBox()]
545  for jj, jAmp in enumerate(detector):
546  if coeffs[ii, jj] == 0.0:
547  continue
548  jImage = self.extractAmp(mi, jAmp, iAmp, isTrimmed)
549  jImage.getMask().getArray()[:] &= crosstalk # Remove all other masks
550  jImage -= backgrounds[jj]
551  iImage.scaledPlus(coeffs[ii, jj], jImage)
552 
553  # Set crosstalkStr bit only for those pixels that have been significantly modified (i.e., those
554  # masked as such in 'subtrahend'), not necessarily those that are bright originally.
555  mask.clearMaskPlane(crosstalkPlane)
556  mi -= subtrahend # also sets crosstalkStr bit for bright pixels
557 
558 
560  """Configuration for intra-detector crosstalk removal."""
561  minPixelToMask = Field(
562  dtype=float,
563  doc="Set crosstalk mask plane for pixels over this value.",
564  default=45000
565  )
566  crosstalkMaskPlane = Field(
567  dtype=str,
568  doc="Name for crosstalk mask plane.",
569  default="CROSSTALK"
570  )
571  crosstalkBackgroundMethod = ChoiceField(
572  dtype=str,
573  doc="Type of background subtraction to use when applying correction.",
574  default="None",
575  allowed={
576  "None": "Do no background subtraction.",
577  "AMP": "Subtract amplifier-by-amplifier background levels.",
578  "DETECTOR": "Subtract detector level background."
579  },
580  )
581  useConfigCoefficients = Field(
582  dtype=bool,
583  doc="Ignore the detector crosstalk information in favor of CrosstalkConfig values?",
584  default=False,
585  )
586  crosstalkValues = ListField(
587  dtype=float,
588  doc=("Amplifier-indexed crosstalk coefficients to use. This should be arranged as a 1 x nAmp**2 "
589  "list of coefficients, such that when reshaped by crosstalkShape, the result is nAmp x nAmp. "
590  "This matrix should be structured so CT * [amp0 amp1 amp2 ...]^T returns the column "
591  "vector [corr0 corr1 corr2 ...]^T."),
592  default=[0.0],
593  )
594  crosstalkShape = ListField(
595  dtype=int,
596  doc="Shape of the coefficient array. This should be equal to [nAmp, nAmp].",
597  default=[1],
598  )
599 
600  def getCrosstalk(self, detector=None):
601  """Return a 2-D numpy array of crosstalk coefficients in the proper shape.
602 
603  Parameters
604  ----------
605  detector : `lsst.afw.cameraGeom.detector`
606  Detector that is to be crosstalk corrected.
607 
608  Returns
609  -------
610  coeffs : `numpy.ndarray`
611  Crosstalk coefficients that can be used to correct the detector.
612 
613  Raises
614  ------
615  RuntimeError
616  Raised if no coefficients could be generated from this detector/configuration.
617  """
618  if self.useConfigCoefficients is True:
619  coeffs = np.array(self.crosstalkValues).reshape(self.crosstalkShape)
620  if detector is not None:
621  nAmp = len(detector)
622  if coeffs.shape != (nAmp, nAmp):
623  raise RuntimeError("Constructed crosstalk coeffients do not match detector shape. "
624  f"{coeffs.shape} {nAmp}")
625  return coeffs
626  elif detector is not None and detector.hasCrosstalk() is True:
627  # Assume the detector defines itself consistently.
628  return detector.getCrosstalk()
629  else:
630  raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients")
631 
632  def hasCrosstalk(self, detector=None):
633  """Return a boolean indicating if crosstalk coefficients exist.
634 
635  Parameters
636  ----------
637  detector : `lsst.afw.cameraGeom.detector`
638  Detector that is to be crosstalk corrected.
639 
640  Returns
641  -------
642  hasCrosstalk : `bool`
643  True if this detector/configuration has crosstalk coefficients defined.
644  """
645  if self.useConfigCoefficients is True and self.crosstalkValues is not None:
646  return True
647  elif detector is not None and detector.hasCrosstalk() is True:
648  return True
649  else:
650  return False
651 
652 
654  """Apply intra-detector crosstalk correction."""
655  ConfigClass = CrosstalkConfig
656  _DefaultName = 'isrCrosstalk'
657 
658  def prepCrosstalk(self, dataRef, crosstalk=None):
659  """Placeholder for crosstalk preparation method, e.g., for inter-detector crosstalk.
660 
661  Parameters
662  ----------
663  dataRef : `daf.persistence.butlerSubset.ButlerDataRef`
664  Butler reference of the detector data to be processed.
665  crosstalk : `~lsst.ip.isr.CrosstalkConfig`
666  Crosstalk calibration that will be used.
667 
668  See also
669  --------
670  lsst.obs.decam.crosstalk.DecamCrosstalkTask.prepCrosstalk
671  """
672  return
673 
674  def run(self, exposure, crosstalk=None,
675  crosstalkSources=None, isTrimmed=False):
676  """Apply intra-detector crosstalk correction
677 
678  Parameters
679  ----------
680  exposure : `lsst.afw.image.Exposure`
681  Exposure for which to remove crosstalk.
682  crosstalkCalib : `lsst.ip.isr.CrosstalkCalib`, optional
683  External crosstalk calibration to apply. Constructed from
684  detector if not found.
685  crosstalkSources : `defaultdict`, optional
686  Image data for other detectors that are sources of
687  crosstalk in exposure. The keys are expected to be names
688  of the other detectors, with the values containing
689  `lsst.afw.image.Exposure` at the same level of processing
690  as ``exposure``.
691  The default for intra-detector crosstalk here is None.
692  isTrimmed : `bool`
693  The image is already trimmed.
694  This should no longer be needed once DM-15409 is resolved.
695 
696  Raises
697  ------
698  RuntimeError
699  Raised if called for a detector that does not have a
700  crosstalk correction.
701  """
702  if not crosstalk:
703  crosstalk = CrosstalkCalib(log=self.log)
704  crosstalk = crosstalk.fromDetector(exposure.getDetector(),
705  coeffVector=self.config.crosstalkValues)
706  if not crosstalk.log:
707  crosstalk.log = self.log
708  if not crosstalk.hasCrosstalk:
709  raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients.")
710 
711  else:
712  self.log.info("Applying crosstalk correction.")
713  crosstalk.subtractCrosstalk(exposure, crosstalkCoeffs=crosstalk.coeffs,
714  minPixelToMask=self.config.minPixelToMask,
715  crosstalkStr=self.config.crosstalkMaskPlane, isTrimmed=isTrimmed,
716  backgroundMethod=self.config.crosstalkBackgroundMethod)
717 
718  if crosstalk.interChip:
719  if crosstalkSources:
720  for detName in crosstalk.interChip:
721  if isinstance(crosstalkSources[0], 'lsst.afw.image.Exposure'):
722  # Received afwImage.Exposure
723  sourceNames = [exp.getDetector().getName() for exp in crosstalkSources]
724  else:
725  # Received dafButler.DeferredDatasetHandle
726  sourceNames = [expRef.get(datasetType='isrOscanCorr').getDetector().getName()
727  for expRef in crosstalkSources]
728  if detName not in sourceNames:
729  self.log.warn("Crosstalk lists %s, not found in sources: %s",
730  detName, sourceNames)
731  continue
732  interChipCoeffs = crosstalk.interChip[detName]
733  sourceExposure = crosstalkSources[sourceNames.index(detName)]
734  crosstalk.subtractCrosstalk(exposure, sourceExposure=sourceExposure,
735  crosstalkCoeffs=interChipCoeffs,
736  minPixelToMask=self.config.minPixelToMask,
737  crosstalkStr=self.config.crosstalkMaskPlane,
738  isTrimmed=isTrimmed,
739  backgroundMethod=self.config.crosstalkBackgroundMethod)
740  else:
741  self.log.warn("Crosstalk contains interChip coefficients, but no sources found!")
742 
743 
745  def run(self, exposure, crosstalkSources=None):
746  self.log.info("Not performing any crosstalk correction")
lsst.pipe.base.task.Task.getName
def getName(self)
Definition: task.py:274
lsst::log.log.logContinued.warn
def warn(fmt, *args)
Definition: logContinued.py:205
lsst::log.log.logContinued.info
def info(fmt, *args)
Definition: logContinued.py:201
lsst::ip::isr.calibType.IsrCalib.fromDetector
def fromDetector(self, detector)
Definition: calibType.py:430
lsst::ip::isr.crosstalk.CrosstalkCalib
Definition: crosstalk.py:40
lsst.pex.config.listField.ListField
Definition: listField.py:216
lsst::ip::isr.crosstalk.CrosstalkTask
Definition: crosstalk.py:653
lsst.gdb.afw.printers.debug
bool debug
Definition: printers.py:9
lsst::afw::math::makeStatistics
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Definition: Statistics.h:520
lsst::ip::isr.crosstalk.NullCrosstalkTask.run
def run(self, exposure, crosstalkSources=None)
Definition: crosstalk.py:745
astshim.keyMap.keyMapContinued.keys
def keys(self)
Definition: keyMapContinued.py:6
lsst::ip::isr.crosstalk.CrosstalkConfig.crosstalkValues
crosstalkValues
Definition: crosstalk.py:586
lsst::ip::isr.calibType.IsrCalib.updateMetadata
def updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition: calibType.py:148
lsst::ip::isr.crosstalk.CrosstalkCalib.fromDetector
def fromDetector(self, detector, coeffVector=None)
Definition: crosstalk.py:133
lsst::ip::isr.crosstalk.CrosstalkCalib.hasCrosstalk
hasCrosstalk
Definition: crosstalk.py:90
lsst::afw::geom.transform.transformContinued.cls
cls
Definition: transformContinued.py:33
lsst::ip::isr.calibType.IsrCalib.log
log
Definition: calibType.py:82
lsst::ip::isr.crosstalk.CrosstalkCalib.subtractCrosstalk
def subtractCrosstalk(self, thisExposure, sourceExposure=None, crosstalkCoeffs=None, badPixels=["BAD"], minPixelToMask=45000, crosstalkStr="CROSSTALK", isTrimmed=False, backgroundMethod="None")
Definition: crosstalk.py:449
lsst::afw::detection::FootprintSet
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
lsst::ip::isr.calibType.IsrCalib._detectorSerial
_detectorSerial
Definition: calibType.py:68
lsst::ip::isr.calibType.IsrCalib.requiredAttributes
requiredAttributes
Definition: calibType.py:77
lsst.pex.config
Definition: __init__.py:1
lsst::ip::isr.crosstalk.NullCrosstalkTask
Definition: crosstalk.py:744
lsst::afw::detection::Threshold
A Threshold is used to pass a threshold value to detection algorithms.
Definition: Threshold.h:43
lsst.pipe.base.task.Task.log
log
Definition: task.py:161
lsst::ip::isr.crosstalk.CrosstalkCalib.coeffNum
coeffNum
Definition: crosstalk.py:96
lsst::ip::isr.crosstalk.CrosstalkCalib.extractAmp
def extractAmp(image, amp, ampTarget, isTrimmed=False)
Definition: crosstalk.py:387
lsst::ip::isr.crosstalk.CrosstalkConfig.hasCrosstalk
def hasCrosstalk(self, detector=None)
Definition: crosstalk.py:632
lsst::ip::isr.crosstalk.CrosstalkCalib.coeffErr
coeffErr
Definition: crosstalk.py:95
lsst::ip::isr.crosstalk.CrosstalkCalib.fromDict
def fromDict(cls, dictionary)
Definition: crosstalk.py:176
lsst::ip::isr.crosstalk.CrosstalkCalib.__init__
def __init__(self, detector=None, nAmp=0, **kwargs)
Definition: crosstalk.py:89
lsst.pex.config.choiceField.ChoiceField
Definition: choiceField.py:34
lsst::ip::isr.calibType.IsrCalib.getMetadata
def getMetadata(self)
Definition: calibType.py:114
lsst::ip::isr.crosstalk.CrosstalkCalib.coeffValid
coeffValid
Definition: crosstalk.py:98
lsst::afw::detection
Definition: Footprint.h:50
lsst::afw::math::StatisticsControl
Pass parameters to a Statistics object.
Definition: Statistics.h:93
lsst::ip::isr.calibType.IsrCalib._detectorId
_detectorId
Definition: calibType.py:69
lsst::ip::isr.crosstalk.CrosstalkConfig.getCrosstalk
def getCrosstalk(self, detector=None)
Definition: crosstalk.py:600
lsst::ip::isr.crosstalk.CrosstalkCalib.updateMetadata
def updateMetadata(self, setDate=False, **kwargs)
Definition: crosstalk.py:109
lsst::ip::isr.crosstalk.CrosstalkCalib.toDict
def toDict(self)
Definition: crosstalk.py:269
lsst.pipe.base.task.Task
Definition: task.py:47
lsst::ip::isr
Definition: applyLookupTable.h:34
lsst::afw::math
Definition: statistics.dox:6
lsst.pex.config.config.Config
Definition: config.py:736
lsst::ip::isr.calibType.IsrCalib
Definition: calibType.py:36
lsst::ip::isr.crosstalk.CrosstalkCalib.toTable
def toTable(self)
Definition: crosstalk.py:351
lsst.pex.config.config.Field
Definition: config.py:247
lsst::ip::isr.crosstalk.CrosstalkCalib.calculateBackground
def calculateBackground(mi, badPixels=["BAD"])
Definition: crosstalk.py:427
lsst::ip::isr.crosstalk.CrosstalkConfig.useConfigCoefficients
useConfigCoefficients
Definition: crosstalk.py:581
lsst::ip::isr.calibType.IsrCalib._detectorName
_detectorName
Definition: calibType.py:67
lsst.pipe.base
Definition: __init__.py:1
lsst::ip::isr.crosstalk.CrosstalkCalib.coeffs
coeffs
Definition: crosstalk.py:94
lsst::ip::isr.crosstalk.CrosstalkTask.run
def run(self, exposure, crosstalk=None, crosstalkSources=None, isTrimmed=False)
Definition: crosstalk.py:674
lsst::ip::isr.crosstalk.CrosstalkTask.prepCrosstalk
def prepCrosstalk(self, dataRef, crosstalk=None)
Definition: crosstalk.py:658
lsst::ip::isr.crosstalk.CrosstalkCalib.interChip
interChip
Definition: crosstalk.py:100
lsst::ip::isr.crosstalk.CrosstalkCalib.crosstalkShape
crosstalkShape
Definition: crosstalk.py:92
lsst::ip::isr.crosstalk.CrosstalkConfig.crosstalkShape
crosstalkShape
Definition: crosstalk.py:594
lsst::ip::isr.crosstalk.CrosstalkConfig
Definition: crosstalk.py:559
lsst::afw::math::flipImage
std::shared_ptr< ImageT > flipImage(ImageT const &inImage, bool flipLR, bool flipTB)
Flip an image left–right and/or top–bottom.
Definition: rotateImage.cc:92
lsst::ip::isr.crosstalk.CrosstalkCalib.fromTable
def fromTable(cls, tableList)
Definition: crosstalk.py:308
lsst::ip::isr.crosstalk.CrosstalkCalib.nAmp
nAmp
Definition: crosstalk.py:91