LSST Applications g013ef56533+9b09c56ab3,g083dd6704c+a047e97985,g199a45376c+0ba108daf9,g19c4beb06c+f87d968cda,g1fd858c14a+0ebb8fe952,g210f2d0738+989426243d,g262e1987ae+b51a336e7c,g29ae962dfc+97650ea04b,g2cef7863aa+aef1011c0b,g35bb328faa+8c5ae1fdc5,g3fd5ace14f+deda572d24,g47891489e3+f459a6810c,g511e8cfd20+b3c771ffa5,g53246c7159+8c5ae1fdc5,g54cd7ddccb+890c8e1e5d,g5fd55ab2c7+7caddb52a2,g64539dfbff+989426243d,g67b6fd64d1+f459a6810c,g74acd417e5+866715c704,g786e29fd12+668abc6043,g87389fa792+8856018cbb,g89139ef638+f459a6810c,g8c0b9db444+5bcd39efc6,g8d7436a09f+38cd1e2ebb,g8ea07a8fe4+81eaaadc04,g90f42f885a+34c0557caf,g9486f8a5af+4ba9141ddb,g97be763408+c9d9d08df4,gbf99507273+8c5ae1fdc5,gc2a301910b+989426243d,gca7fc764a6+f459a6810c,gd7ef33dd92+f459a6810c,gdab6d2f7ff+866715c704,ge410e46f29+f459a6810c,ge41e95a9f2+989426243d,geaed405ab2+e3b4b2a692,gf2b17d7741+8c5ae1fdc5,gf9a733ac38+8c5ae1fdc5,w.2025.42
LSST Data Management Base Package
Loading...
Searching...
No Matches
crosstalk.py
Go to the documentation of this file.
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"""
25
26__all__ = ["CrosstalkCalib", "CrosstalkConfig", "CrosstalkTask",
27 "NullCrosstalkTask"]
28
29import numpy as np
30import warnings
31from astropy.table import Table
32
33import lsst.afw.math
35import lsst.daf.butler
36from lsst.pex.config import Config, Field, ChoiceField, ListField
37from lsst.pipe.base import Task
38
39from .calibType import IsrCalib
40from .isrFunctions import gainContext, isTrimmedImage
41
42
44 """Calibration of amp-to-amp crosstalk coefficients.
45
46 Parameters
47 ----------
48 detector : `lsst.afw.cameraGeom.Detector`, optional
49 Detector to use to pull coefficients from.
50 nAmp : `int`, optional
51 Number of amplifiers to initialize.
52 log : `logging.Logger`, optional
53 Log to write messages to.
54 **kwargs :
55 Parameters to pass to parent constructor.
56
57 Notes
58 -----
59 The crosstalk attributes stored are:
60
61 hasCrosstalk : `bool`
62 Whether there is crosstalk defined for this detector.
63 nAmp : `int`
64 Number of amplifiers in this detector.
65 crosstalkShape : `tuple` [`int`, `int`]
66 A tuple containing the shape of the ``coeffs`` matrix. This
67 should be equivalent to (``nAmp``, ``nAmp``).
68 coeffs : `numpy.ndarray`
69 A matrix containing the crosstalk coefficients. coeff[i][j]
70 contains the coefficients to calculate the contribution
71 amplifier_j has on amplifier_i (each row[i] contains the
72 corrections for detector_i).
73 coeffErr : `numpy.ndarray`, optional
74 A matrix (as defined by ``coeffs``) containing the standard
75 distribution of the crosstalk measurements.
76 coeffNum : `numpy.ndarray`, optional
77 A matrix containing the number of pixel pairs used to measure
78 the ``coeffs`` and ``coeffErr``.
79 coeffValid : `numpy.ndarray`, optional
80 A matrix of Boolean values indicating if the coefficient is
81 valid, defined as abs(coeff) > coeffErr / sqrt(coeffNum).
82 coeffsSqr : `numpy.ndarray`, optional
83 A matrix containing potential quadratic crosstalk coefficients
84 (see e.g., Snyder+21, 2001.03223). coeffsSqr[i][j]
85 contains the coefficients to calculate the contribution
86 amplifier_j has on amplifier_i (each row[i] contains the
87 corrections for detector_i).
88 coeffErrSqr : `numpy.ndarray`, optional
89 A matrix (as defined by ``coeffsSqr``) containing the standard
90 distribution of the quadratic term of the crosstalk measurements.
91 interChip : `dict` [`numpy.ndarray`]
92 A dictionary keyed by detectorName containing ``coeffs``
93 matrices used to correct for inter-chip crosstalk with a
94 source on the detector indicated.
95
96 Version 1.1 adds quadratic coefficients, a matrix with the ratios
97 of amplifiers gains per detector, and a field to indicate the units
98 of the numerator and denominator of the source and target signals, with
99 "adu" meaning "ADU / ADU" and "electron" meaning "e- / e-".
100
101 Version 1.2 adds the original gains used in the crosstalk fit.
102 """
103 _OBSTYPE = 'CROSSTALK'
104 _SCHEMA = 'Gen3 Crosstalk'
105 _VERSION = 1.2
106
107 def __init__(self, detector=None, nAmp=0, **kwargs):
108 self.hasCrosstalk = False
109 self.nAmp = nAmp if nAmp else 0
110 self.crosstalkShape = (self.nAmp, self.nAmp)
111
112 self.coeffs = np.zeros(self.crosstalkShape) if self.nAmp else None
113 self.coeffErr = np.zeros(self.crosstalkShape) if self.nAmp else None
114 self.coeffNum = np.zeros(self.crosstalkShape,
115 dtype=int) if self.nAmp else None
116 self.coeffValid = np.ones(self.crosstalkShape,
117 dtype=bool) if self.nAmp else None
118 # Quadratic terms, if any.
119 self.coeffsSqr = np.zeros(self.crosstalkShape) if self.nAmp else None
120 self.coeffErrSqr = np.zeros(self.crosstalkShape) if self.nAmp else None
121
122 # Gain ratios
123 self.ampGainRatios = np.zeros(self.crosstalkShape) if self.nAmp else None
124
125 # Gains used for fit.
126 self.fitGains = np.zeros(self.nAmp) if self.nAmp else None
127
128 # Units
129 self.crosstalkRatiosUnits = 'adu' if self.nAmp else None
130
131 self.interChip = {}
132
133 super().__init__(**kwargs)
134 self.requiredAttributes.update(['hasCrosstalk', 'nAmp', 'coeffs',
135 'coeffErr', 'coeffNum', 'coeffValid',
136 'coeffsSqr', 'coeffErrSqr',
137 'ampGainRatios', 'crosstalkRatiosUnits',
138 'fitGains', 'interChip'])
139 if detector:
140 self.fromDetector(detector)
141
142 def updateMetadata(self, setDate=False, **kwargs):
143 """Update calibration metadata.
144
145 This calls the base class's method after ensuring the required
146 calibration keywords will be saved.
147
148 Parameters
149 ----------
150 setDate : `bool`, optional
151 Update the CALIBDATE fields in the metadata to the current
152 time. Defaults to False.
153 kwargs :
154 Other keyword parameters to set in the metadata.
155 """
156 kwargs['DETECTOR'] = self._detectorId
157 kwargs['DETECTOR_NAME'] = self._detectorName
158 kwargs['DETECTOR_SERIAL'] = self._detectorSerial
159 kwargs['HAS_CROSSTALK'] = self.hasCrosstalk
160 kwargs['NAMP'] = self.nAmp
161 self.crosstalkShape = (self.nAmp, self.nAmp)
162 kwargs['CROSSTALK_SHAPE'] = self.crosstalkShape
163 kwargs['CROSSTALK_RATIOS_UNITS'] = self.crosstalkRatiosUnits
164
165 super().updateMetadata(setDate=setDate, **kwargs)
166
167 def fromDetector(self, detector, coeffVector=None, coeffSqrVector=None):
168 """Set calibration parameters from the detector.
169
170 Parameters
171 ----------
172 detector : `lsst.afw.cameraGeom.Detector`
173 Detector to use to set parameters from.
174 coeffVector : `numpy.array`, optional
175 Use the detector geometry (bounding boxes and flip
176 information), but use ``coeffVector`` instead of the
177 output of ``detector.getCrosstalk()``.
178 coeffSqrVector : `numpy.array`, optional
179 Quadratic crosstalk coefficients.
180
181 Returns
182 -------
183 calib : `lsst.ip.isr.CrosstalkCalib`
184 The calibration constructed from the detector.
185
186 """
187 self._detectorId = detector.getId()
188 self._detectorName = detector.getName()
189 self._detectorSerial = detector.getSerial()
190
191 self.nAmp = len(detector)
192 self.crosstalkShape = (self.nAmp, self.nAmp)
193
194 if coeffVector is not None:
195 crosstalkCoeffs = coeffVector
196 else:
197 crosstalkCoeffs = detector.getCrosstalk()
198 if coeffSqrVector is not None:
199 self.coeffsSqr = coeffSqrVector
200 else:
201 self.coeffsSqr = np.zeros(self.crosstalkShape)
202 if len(crosstalkCoeffs) == 1 and crosstalkCoeffs[0] == 0.0:
203 return self
204 self.coeffs = np.array(crosstalkCoeffs).reshape(self.crosstalkShape)
205
206 if self.coeffs.shape != self.crosstalkShape:
207 raise RuntimeError("Crosstalk coefficients do not match detector shape. "
208 f"{self.crosstalkShape} {self.nAmp}")
209 # Set default as in __init__
210 self.coeffErr = np.zeros(self.crosstalkShape)
211 self.coeffNum = np.zeros(self.crosstalkShape, dtype=int)
212 self.coeffValid = np.ones(self.crosstalkShape, dtype=bool)
213 self.coeffErrSqr = np.zeros(self.crosstalkShape)
214 self.ampGainRatios = np.zeros(self.crosstalkShape)
215 self.fitGains = np.zeros(self.nAmp)
216 self.crosstalkRatiosUnits = 'adu'
217
218 self.interChip = {}
219
220 self.hasCrosstalk = True
221 self.updateMetadata()
222
223 return self
224
225 @classmethod
226 def fromDict(cls, dictionary):
227 """Construct a calibration from a dictionary of properties.
228
229 Must be implemented by the specific calibration subclasses.
230
231 Parameters
232 ----------
233 dictionary : `dict`
234 Dictionary of properties.
235
236 Returns
237 -------
238 calib : `lsst.ip.isr.CalibType`
239 Constructed calibration.
240
241 Raises
242 ------
243 RuntimeError
244 Raised if the supplied dictionary is for a different
245 calibration.
246 """
247 calib = cls()
248
249 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']:
250 raise RuntimeError(f"Incorrect crosstalk supplied. Expected {calib._OBSTYPE}, "
251 f"found {dictionary['metadata']['OBSTYPE']}")
252
253 calib.setMetadata(dictionary['metadata'])
254
255 if 'detectorName' in dictionary:
256 calib._detectorName = dictionary.get('detectorName')
257 elif 'DETECTOR_NAME' in dictionary:
258 calib._detectorName = dictionary.get('DETECTOR_NAME')
259 elif 'DET_NAME' in dictionary['metadata']:
260 calib._detectorName = dictionary['metadata']['DET_NAME']
261 else:
262 calib._detectorName = None
263
264 if 'detectorSerial' in dictionary:
265 calib._detectorSerial = dictionary.get('detectorSerial')
266 elif 'DETECTOR_SERIAL' in dictionary:
267 calib._detectorSerial = dictionary.get('DETECTOR_SERIAL')
268 elif 'DET_SER' in dictionary['metadata']:
269 calib._detectorSerial = dictionary['metadata']['DET_SER']
270 else:
271 calib._detectorSerial = None
272
273 if 'detectorId' in dictionary:
274 calib._detectorId = dictionary.get('detectorId')
275 elif 'DETECTOR' in dictionary:
276 calib._detectorId = dictionary.get('DETECTOR')
277 elif 'DETECTOR' in dictionary['metadata']:
278 calib._detectorId = dictionary['metadata']['DETECTOR']
279 elif calib._detectorSerial:
280 calib._detectorId = calib._detectorSerial
281 else:
282 calib._detectorId = None
283
284 if 'instrument' in dictionary:
285 calib._instrument = dictionary.get('instrument')
286 elif 'INSTRUME' in dictionary['metadata']:
287 calib._instrument = dictionary['metadata']['INSTRUME']
288 else:
289 calib._instrument = None
290
291 calib.hasCrosstalk = dictionary.get('hasCrosstalk',
292 dictionary['metadata'].get('HAS_CROSSTALK', False))
293 if calib.hasCrosstalk:
294 calib.nAmp = dictionary.get('nAmp', dictionary['metadata'].get('NAMP', 0))
295 calib.crosstalkShape = (calib.nAmp, calib.nAmp)
296 calib.coeffs = np.array(dictionary['coeffs']).reshape(calib.crosstalkShape)
297 calib.crosstalkRatiosUnits = dictionary.get(
298 'crosstalkRatiosUnits',
299 dictionary['metadata'].get('CROSSTALK_RATIOS_UNITS', 'adu'))
300 if 'coeffErr' in dictionary:
301 calib.coeffErr = np.array(dictionary['coeffErr']).reshape(calib.crosstalkShape)
302 else:
303 calib.coeffErr = np.zeros_like(calib.coeffs)
304 if 'coeffNum' in dictionary:
305 calib.coeffNum = np.array(dictionary['coeffNum']).reshape(calib.crosstalkShape)
306 else:
307 calib.coeffNum = np.zeros_like(calib.coeffs, dtype=int)
308 if 'coeffValid' in dictionary:
309 calib.coeffValid = np.array(dictionary['coeffValid']).reshape(calib.crosstalkShape)
310 else:
311 calib.coeffValid = np.ones_like(calib.coeffs, dtype=bool)
312 if 'coeffsSqr' in dictionary:
313 calib.coeffsSqr = np.array(dictionary['coeffsSqr']).reshape(calib.crosstalkShape)
314 else:
315 calib.coeffsSqr = np.zeros_like(calib.coeffs)
316 if 'coeffErrSqr' in dictionary:
317 calib.coeffErrSqr = np.array(dictionary['coeffErrSqr']).reshape(calib.crosstalkShape)
318 else:
319 calib.coeffErrSqr = np.zeros_like(calib.coeffs)
320 if 'ampGainRatios' in dictionary:
321 calib.ampGainRatios = np.array(dictionary['ampGainRatios']).reshape(calib.crosstalkShape)
322 else:
323 calib.ampGainRatios = np.zeros_like(calib.coeffs)
324 if 'fitGains' in dictionary:
325 # Compatibility for matrices that were stored with fitGains
326 # of length 1.
327 fitGains = np.array(dictionary['fitGains'])
328 if len(fitGains) == 1:
329 # Expand to the correct number of amps, all zero (unknown).
330 calib.fitGains = np.zeros(calib.nAmp)
331 else:
332 calib.fitGains = np.array(dictionary['fitGains']).reshape(calib.nAmp)
333 else:
334 calib.fitGains = np.zeros(calib.nAmp)
335
336 calib.interChip = dictionary.get('interChip', None)
337 if calib.interChip:
338 for detector in calib.interChip:
339 coeffVector = calib.interChip[detector]
340 calib.interChip[detector] = np.array(coeffVector).reshape(calib.crosstalkShape)
341
342 calib.updateMetadata()
343 return calib
344
345 def toDict(self):
346 """Return a dictionary containing the calibration properties.
347
348 The dictionary should be able to be round-tripped through
349 `fromDict`.
350
351 Returns
352 -------
353 dictionary : `dict`
354 Dictionary of properties.
355 """
356 self.updateMetadata()
357
358 outDict = {}
359 metadata = self.getMetadata()
360 outDict['metadata'] = metadata
361
362 outDict['hasCrosstalk'] = self.hasCrosstalk
363 outDict['nAmp'] = self.nAmp
364 outDict['crosstalkShape'] = self.crosstalkShape
365 outDict['crosstalkRatiosUnits'] = self.crosstalkRatiosUnits
366
367 ctLength = self.nAmp*self.nAmp
368 outDict['coeffs'] = self.coeffs.reshape(ctLength).tolist()
369
370 if self.coeffErr is not None:
371 outDict['coeffErr'] = self.coeffErr.reshape(ctLength).tolist()
372 if self.coeffNum is not None:
373 outDict['coeffNum'] = self.coeffNum.reshape(ctLength).tolist()
374 if self.coeffValid is not None:
375 outDict['coeffValid'] = self.coeffValid.reshape(ctLength).tolist()
376 if self.coeffsSqr is not None:
377 outDict['coeffsSqr'] = self.coeffsSqr.reshape(ctLength).tolist()
378 if self.coeffErrSqr is not None:
379 outDict['coeffErrSqr'] = self.coeffErrSqr.reshape(ctLength).tolist()
380 if self.ampGainRatios is not None:
381 outDict['ampGainRatios'] = self.ampGainRatios.reshape(ctLength).tolist()
382 if self.fitGains is not None:
383 outDict['fitGains'] = self.fitGains.tolist()
384
385 if self.interChip:
386 outDict['interChip'] = dict()
387 for detector in self.interChip:
388 outDict['interChip'][detector] = self.interChip[detector].reshape(ctLength).tolist()
389
390 return outDict
391
392 @classmethod
393 def fromTable(cls, tableList):
394 """Construct calibration from a list of tables.
395
396 This method uses the `fromDict` method to create the
397 calibration, after constructing an appropriate dictionary from
398 the input tables.
399
400 Parameters
401 ----------
402 tableList : `list` [`lsst.afw.table.Table`]
403 List of tables to use to construct the crosstalk
404 calibration.
405
406 Returns
407 -------
408 calib : `lsst.ip.isr.CrosstalkCalib`
409 The calibration defined in the tables.
410
411 """
412 coeffTable = tableList[0]
413
414 metadata = coeffTable.meta
415 inDict = dict()
416 inDict['metadata'] = metadata
417 inDict['hasCrosstalk'] = metadata['HAS_CROSSTALK']
418 inDict['nAmp'] = metadata['NAMP']
419 calibVersion = metadata['CROSSTALK_VERSION']
420 if calibVersion < 1.1:
421 inDict['crosstalkRatiosUnits'] = 'adu'
422 else:
423 inDict['crosstalkRatiosUnits'] = metadata['CROSSTALK_RATIOS_UNITS']
424 inDict['coeffs'] = coeffTable['CT_COEFFS']
425 if 'CT_ERRORS' in coeffTable.columns:
426 inDict['coeffErr'] = coeffTable['CT_ERRORS']
427 if 'CT_COUNTS' in coeffTable.columns:
428 inDict['coeffNum'] = coeffTable['CT_COUNTS']
429 if 'CT_VALID' in coeffTable.columns:
430 inDict['coeffValid'] = coeffTable['CT_VALID']
431 if 'CT_COEFFS_SQR' in coeffTable.columns:
432 inDict['coeffsSqr'] = coeffTable['CT_COEFFS_SQR']
433 if 'CT_ERRORS_SQR' in coeffTable.columns:
434 inDict['coeffErrSqr'] = coeffTable['CT_ERRORS_SQR']
435 if 'CT_AMP_GAIN_RATIOS' in coeffTable.columns:
436 inDict['ampGainRatios'] = coeffTable['CT_AMP_GAIN_RATIOS']
437 if 'CT_FIT_GAINS' in coeffTable.columns:
438 inDict['fitGains'] = coeffTable['CT_FIT_GAINS']
439
440 if len(tableList) > 1:
441 inDict['interChip'] = dict()
442 interChipTable = tableList[1]
443 for record in interChipTable:
444 inDict['interChip'][record['IC_SOURCE_DET']] = record['IC_COEFFS']
445
446 return cls().fromDict(inDict)
447
448 def toTable(self):
449 """Construct a list of tables containing the information in this
450 calibration.
451
452 The list of tables should create an identical calibration
453 after being passed to this class's fromTable method.
454
455 Returns
456 -------
457 tableList : `list` [`lsst.afw.table.Table`]
458 List of tables containing the crosstalk calibration
459 information.
460
461 """
462 tableList = []
463 self.updateMetadata()
464 catalog = Table([{'CT_COEFFS': self.coeffs.reshape(self.nAmp*self.nAmp),
465 'CT_ERRORS': self.coeffErr.reshape(self.nAmp*self.nAmp),
466 'CT_COUNTS': self.coeffNum.reshape(self.nAmp*self.nAmp),
467 'CT_VALID': self.coeffValid.reshape(self.nAmp*self.nAmp),
468 'CT_COEFFS_SQR': self.coeffsSqr.reshape(self.nAmp*self.nAmp),
469 'CT_ERRORS_SQR': self.coeffErrSqr.reshape(self.nAmp*self.nAmp),
470 'CT_AMP_GAIN_RATIOS': self.ampGainRatios.reshape(self.nAmp*self.nAmp),
471 'CT_FIT_GAINS': self.fitGains,
472 }])
473 # filter None, because astropy can't deal.
474 inMeta = self.getMetadata().toDict()
475 outMeta = {k: v for k, v in inMeta.items() if v is not None}
476 outMeta.update({k: "" for k, v in inMeta.items() if v is None})
477 catalog.meta = outMeta
478 tableList.append(catalog)
479
480 if self.interChip:
481 interChipTable = Table([{'IC_SOURCE_DET': sourceDet,
482 'IC_COEFFS': self.interChip[sourceDet].reshape(self.nAmp*self.nAmp)}
483 for sourceDet in self.interChip.keys()])
484 tableList.append(interChipTable)
485 return tableList
486
487 # Implementation methods.
488 @staticmethod
489 def extractAmp(image, ampToFlip, ampTarget, isTrimmed=False, fullAmplifier=False, parallelOverscan=None):
490 """Extract the image data from an amp, flipped to match ampTarget.
491
492 Parameters
493 ----------
494 image : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
495 Image containing the amplifier of interest.
496 amp : `lsst.afw.cameraGeom.Amplifier`
497 Amplifier on image to extract.
498 ampTarget : `lsst.afw.cameraGeom.Amplifier`
499 Target amplifier that the extracted image will be flipped
500 to match.
501 isTrimmed : `bool`, optional
502 The image is already trimmed.
503 fullAmplifier : `bool`, optional
504 Use full amplifier and not just imaging region.
505 parallelOverscan : `bool`, optional
506 This has been deprecated and is unused, and will be removed
507 after v29.
508
509 Returns
510 -------
511 output : `lsst.afw.image.Image`
512 Amplifier from image, flipped to desired configuration.
513 This will always return a copy of the original data.
514 """
515 if parallelOverscan is not None:
516 warnings.warn(
517 "The parallelOverscan option has been deprecated and will be removed after v29.",
518 FutureWarning,
519 )
520
521 X_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False,
522 lsst.afw.cameraGeom.ReadoutCorner.LR: True,
523 lsst.afw.cameraGeom.ReadoutCorner.UL: False,
524 lsst.afw.cameraGeom.ReadoutCorner.UR: True}
525 Y_FLIP = {lsst.afw.cameraGeom.ReadoutCorner.LL: False,
526 lsst.afw.cameraGeom.ReadoutCorner.LR: False,
527 lsst.afw.cameraGeom.ReadoutCorner.UL: True,
528 lsst.afw.cameraGeom.ReadoutCorner.UR: True}
529
530 bbox = CrosstalkCalib._getAppropriateBBox(ampToFlip, isTrimmed, fullAmplifier)
531 output = image[bbox]
532
533 sourceAmpCorner = ampToFlip.getReadoutCorner()
534 targetAmpCorner = ampTarget.getReadoutCorner()
535
536 # Flipping is necessary only if the desired configuration doesn't match
537 # what we currently have.
538 xFlip = X_FLIP[targetAmpCorner] ^ X_FLIP[sourceAmpCorner]
539 yFlip = Y_FLIP[targetAmpCorner] ^ Y_FLIP[sourceAmpCorner]
540 # This always makes a copy of the image.
541 return lsst.afw.math.flipImage(output, xFlip, yFlip)
542
543 @staticmethod
544 def _getAppropriateBBox(amp, isTrimmed, fullAmplifier):
545 """Get the appropriate bounding box from an amplifier.
546
547 Parameters
548 ----------
549 amp : `lsst.afw.cameraGeom.Amplifier`
550 Amplifier to get bounding box.
551 isTrimmed : `bool`
552 Is this a trimmed image?
553 fullAmplifier : `bool`
554 Extract full amplifier for an untrimmed image?
555 """
556 if isTrimmed:
557 return amp.getBBox()
558 elif fullAmplifier and not isTrimmed:
559 return amp.getRawBBox()
560 else:
561 return amp.getRawDataBBox()
562
563 @staticmethod
564 def calculateBackground(mi, badPixels=["BAD"]):
565 """Estimate median background in image.
566
567 Getting a great background model isn't important for crosstalk
568 correction, since the crosstalk is at a low level. The median should
569 be sufficient.
570
571 Parameters
572 ----------
573 mi : `lsst.afw.image.MaskedImage`
574 MaskedImage for which to measure background.
575 badPixels : `list` of `str`
576 Mask planes to ignore.
577 Returns
578 -------
579 bg : `float`
580 Median background level.
581 """
582 mask = mi.getMask()
584 stats.setAndMask(mask.getPlaneBitMask(badPixels))
585 return lsst.afw.math.makeStatistics(mi, lsst.afw.math.MEDIAN, stats).getValue()
586
587 def subtractCrosstalk(self, thisExposure, sourceExposure=None, crosstalkCoeffs=None,
588 crosstalkCoeffsSqr=None, crosstalkCoeffsValid=None,
589 badPixels=["BAD"], minPixelToMask=45000, doSubtrahendMasking=False,
590 crosstalkStr="CROSSTALK", isTrimmed=None,
591 backgroundMethod="None", doSqrCrosstalk=False, fullAmplifier=False,
592 parallelOverscan=None, detectorConfig=None, badAmpDict=None,
593 ignoreVariance=False):
594 """Subtract the crosstalk from thisExposure, optionally using a
595 different source.
596
597 We set the mask plane indicated by ``crosstalkStr`` in a
598 target amplifier for pixels in a source amplifier that exceed
599 ``minPixelToMask``, if ``doSubtrahendMasking`` is False. With
600 that enabled, the mask is only set if the absolute value of
601 the correction applied exceeds ``minPixelToMask``. Note that
602 the correction is applied to all pixels in the amplifier, but
603 only those that have a substantial crosstalk are masked with
604 ``crosstalkStr``.
605
606 The uncorrected image is used as a template for correction. This is
607 good enough if the crosstalk is small (e.g., coefficients < ~ 1e-3),
608 but if it's larger you may want to iterate.
609
610 Parameters
611 ----------
612 thisExposure : `lsst.afw.image.Exposure`
613 Exposure for which to subtract crosstalk.
614 sourceExposure : `lsst.afw.image.Exposure`, optional
615 Exposure to use as the source of the crosstalk. If not set,
616 thisExposure is used as the source (intra-detector crosstalk).
617 crosstalkCoeffs : `numpy.ndarray`, optional.
618 Coefficients to use to correct crosstalk.
619 crosstalkCoeffsSqr : `numpy.ndarray`, optional.
620 Quadratic coefficients to use to correct crosstalk.
621 crosstalkCoeffsValid : `numpy.ndarray`, optional
622 Boolean array that is True where coefficients are valid.
623 badPixels : `list` of `str`, optional
624 Mask planes to ignore.
625 minPixelToMask : `float`, optional
626 Minimum pixel value to set the ``crosstalkStr`` mask
627 plane. If doSubtrahendMasking is True, this is calculated
628 from the absolute magnitude of the subtrahend image.
629 Otherwise, this sets the minimum source value to use to
630 set that mask.
631 doSubtrahendMasking : `bool`, optional
632 If true, the mask is calculated from the properties of the
633 subtrahend image, not from the brightness of the source
634 pixel.
635 crosstalkStr : `str`, optional
636 Mask plane name for pixels greatly modified by crosstalk
637 (above minPixelToMask).
638 isTrimmed : `bool`, optional
639 This option has been deprecated and does not do anything.
640 It will be removed after v29.
641 backgroundMethod : `str`, optional
642 Method used to subtract the background. "AMP" uses
643 amplifier-by-amplifier background levels, "DETECTOR" uses full
644 exposure/maskedImage levels. Any other value results in no
645 background subtraction.
646 doSqrCrosstalk: `bool`, optional
647 Should the quadratic crosstalk coefficients be used for the
648 crosstalk correction?
649 fullAmplifier : `bool`, optional
650 Use full amplifier and not just imaging region.
651 parallelOverscan : `bool`, optional
652 This option is deprecated and will be removed after v29.
653 detectorConfig : `lsst.ip.isr.overscanDetectorConfig`, optional
654 Per-amplifier configs to use if parallelOverscan is True.
655 This option is deprecated and will be removed after v29.
656 badAmpDict : `dict` [`str`, `bool`], optional
657 Dictionary to identify bad amplifiers that should not be
658 source or target for crosstalk correction.
659 ignoreVariance : `bool`, optional
660 Ignore the variance plane when doing crosstalk correction?
661
662 Notes
663 -----
664
665 For a given image I, we want to find the crosstalk subtrahend
666 image CT, such that
667 I_corrected = I - CT
668 The subtrahend image is the sum of all crosstalk contributions
669 that appear in I, so we can build it up by amplifier. Each
670 amplifier A in image I sees the contributions from all other
671 amplifiers B_v != A. For the current linear model, we set `sImage`
672 equal to the segment of the subtrahend image CT corresponding to
673 amplifier A, and then build it up as:
674 simage_linear = sum_v coeffsA_v * (B_v - bkg_v) where coeffsA_v
675 is the vector of crosstalk coefficients for sources that cause
676 images in amplifier A. The bkg_v term in this equation is
677 identically 0.0 for all cameras except obs_subaru (and is only
678 non-zero there for historical reasons).
679 To include the non-linear term, we can again add to the subtrahend
680 image using the same loop, as:
681
682 simage_nonlinear = sum_v (coeffsA_v * B_v) + (NLcoeffsA_v * B_v * B_v)
683 = sum_v linear_term_v + nonlinear_term_v
684
685 where coeffsA_v is the linear term, and NLcoeffsA_v are the quadratic
686 component. For LSSTCam, it has been observed that the linear_term_v >>
687 nonlinear_term_v.
688 """
689 targetMaskedImage = thisExposure.maskedImage
690 targetDetector = thisExposure.getDetector()
691 if self.hasCrosstalk is False:
692 self.fromDetector(targetDetector, coeffVector=crosstalkCoeffs)
693
694 # TODO: Remove on DM-48394
695 if isTrimmed is not None:
696 warnings.warn(
697 "The isTrimmed option has been deprecated and will be removed after v29.",
698 FutureWarning,
699 )
700 isTrimmed = isTrimmedImage(targetMaskedImage, targetDetector)
701
702 # TODO: Remove on DM-48394
703 if parallelOverscan is not None:
704 warnings.warn(
705 "The parallelOverscan option has been deprecated and will be removed after v29.",
706 FutureWarning,
707 )
708 if detectorConfig is not None:
709 warnings.warn(
710 "The detectorConfig option has been deprecated and will be removed after v29.",
711 FutureWarning,
712 )
713
714 numAmps = len(targetDetector)
715 if numAmps != self.nAmp:
716 raise RuntimeError(f"Crosstalk built for {self.nAmp} in {self._detectorName}, received "
717 f"{numAmps} in {targetDetector.getName()}")
718
719 if doSqrCrosstalk and crosstalkCoeffsSqr is None:
720 raise RuntimeError("Attempted to perform NL crosstalk correction without NL "
721 "crosstalk coefficients.")
722
723 if fullAmplifier and (backgroundMethod != "None"):
724 raise RuntimeError("Cannot do full amplifier with background subtraction.")
725
726 if sourceExposure:
727 sourceMaskedImage = sourceExposure.maskedImage
728 sourceDetector = sourceExposure.getDetector()
729 else:
730 sourceMaskedImage = targetMaskedImage
731 sourceDetector = targetDetector
732
733 if crosstalkCoeffs is not None:
734 coeffs = np.asarray(crosstalkCoeffs).copy()
735 else:
736 coeffs = np.asarray(self.coeffs).copy()
737 self.log.debug("CT COEFF: %s", coeffs)
738
739 if doSqrCrosstalk:
740 coeffsSqr = np.asarray(crosstalkCoeffsSqr).copy()
741 self.log.debug("CT COEFF SQR: %s", coeffsSqr)
742 else:
743 coeffsSqr = np.zeros_like(coeffs)
744
745 # Check for valid values; set to 0 otherwise.
746 badCoeffs = ~np.isfinite(coeffs) | (coeffs == 0.0)
747 coeffs[badCoeffs] = 0.0
748 coeffsSqr[badCoeffs] = 0.0
749 if crosstalkCoeffsValid is not None:
750 coeffs[~crosstalkCoeffsValid] = 0.0
751 coeffsSqr[~crosstalkCoeffsValid] = 0.0
752
753 if badAmpDict:
754 for index, amp in enumerate(sourceDetector):
755 if badAmpDict[amp.getName()]:
756 coeffs[index, :] = 0.0
757 coeffs[:, index] = 0.0
758
759 # Compute backgrounds if requested.
760 backgrounds = {amp.getName(): 0.0 for amp in sourceDetector}
761 if backgroundMethod == "AMP":
762 backgrounds = {
763 amp.getName(): self.calculateBackground(
764 sourceMaskedImage[self._getAppropriateBBox(
765 amp,
766 isTrimmed,
767 False,
768 )], badPixels)
769 for amp in sourceDetector
770 }
771 elif backgroundMethod == "DETECTOR":
772 background = self.calculateBackground(sourceMaskedImage, badPixels)
773 backgrounds = {amp.getName(): background for amp in sourceDetector}
774
775 # Add the crosstalk mask plane to the target mask.
776 sourceCrosstalkPlane = sourceMaskedImage.mask.addMaskPlane(crosstalkStr)
777 if sourceExposure is not None:
778 targetCrosstalkPlane = targetMaskedImage.mask.addMaskPlane(crosstalkStr)
779 else:
780 targetCrosstalkPlane = sourceCrosstalkPlane
781
782 # If we are not doing subtrahend masking, the CROSSTALK mask bit
783 # is set according to the counts in the source pixel (above
784 # background), regardless of the crosstalk coefficient value.
785 if not doSubtrahendMasking:
786 # When we are not using subtrahend masking, we set the mask.
787 thresholdBackground = self.calculateBackground(sourceMaskedImage, badPixels)
788 toMask = (sourceMaskedImage.image.array >= (minPixelToMask + thresholdBackground))
789 sourceMaskedImage.mask.array[toMask] |= sourceMaskedImage.mask.getPlaneBitMask(crosstalkStr)
790
791 crosstalk = sourceMaskedImage.mask.getPlaneBitMask(crosstalkStr)
792
793 # Define a subtrahend image to contain all the scaled crosstalk
794 # signals. These will be applied to the target image.
795 subtrahend = targetMaskedImage.Factory(targetMaskedImage.getBBox())
796 subtrahend.set((0, 0, 0))
797
798 # If we are ignoring variance and doing subtrahend masking, then
799 # we can work on only the image plane (3x speed increase).
800 imageOnly = ignoreVariance and doSubtrahendMasking
801
802 for sourceIndex, sourceAmp in enumerate(sourceDetector):
803 for targetIndex, targetAmp in enumerate(targetDetector):
804 coeff = coeffs[sourceIndex, targetIndex]
805 coeffSqr = coeffsSqr[sourceIndex, targetIndex]
806
807 if coeff == 0.0:
808 continue
809
810 targetBBox = self._getAppropriateBBox(targetAmp, isTrimmed, fullAmplifier)
811
812 # The extractAmp() method will always make a copy of the source
813 # amplifier data.
814 if imageOnly:
815 sourceImage = self.extractAmp(
816 sourceMaskedImage.image,
817 sourceAmp,
818 targetAmp,
819 isTrimmed=isTrimmed,
820 fullAmplifier=fullAmplifier,
821 )
822 targetImage = subtrahend[targetBBox].image
823 else:
824 sourceImage = self.extractAmp(
825 sourceMaskedImage,
826 sourceAmp,
827 targetAmp,
828 isTrimmed=isTrimmed,
829 fullAmplifier=fullAmplifier,
830 )
831 targetImage = subtrahend[targetBBox]
832
833 # Remove all other masks from copied sourceImage.
834 sourceImage.mask.array[:] &= crosstalk
835
836 if backgrounds[sourceAmp.getName()] != 0.0:
837 sourceImage -= backgrounds[sourceAmp.getName()]
838
839 # This operation will also transfer the CROSSTALK mask bit from
840 # above to the target (subtrahend) image if we are using a
841 # masked image.
842 targetImage.scaledPlus(coeff, sourceImage)
843 if coeffSqr != 0.0:
844 sourceImage.scaledMultiplies(1.0, sourceImage)
845 targetImage.scaledPlus(coeffSqr, sourceImage)
846
847 # Clear the mask in the target image, because the subtrahend image
848 # contains the crosstalk mask.
849 sourceMaskedImage.mask.clearMaskPlane(sourceCrosstalkPlane)
850 if sourceExposure is not None:
851 targetMaskedImage.mask.clearMaskPlane(targetCrosstalkPlane)
852
853 if doSubtrahendMasking:
854 # Set crosstalkStr bit only for those pixels that have
855 # been significantly modified (i.e., those masked as such
856 # in 'subtrahend'), not necessarily those that are bright
857 # originally.
858
859 # The existing mask in the subtrahend comes from the
860 # threshold set above. It should be cleared so we can
861 # recalculate it.
862 subtrahend.mask.clearMaskPlane(targetCrosstalkPlane)
863
864 # For masking purposes, we only really care when the
865 # correction is significantly different than the median
866 # value on that amplifier (which includes the contribution
867 # of every other amplifier background that crosstalks onto
868 # that amplifier). Subtract and save this "background"
869 # level, so we can threshold to set the mask relative to
870 # that background, but still include that contribution in
871 # the correction we're applying.
872 subtrahendBackgrounds = {}
873 for amp in targetDetector:
874 # Note that we never want the full amplifier for background
875 # calculations.
876 bbox = self._getAppropriateBBox(amp, isTrimmed, False)
877 ampData = subtrahend[bbox]
878 background = np.median(ampData.image.array)
879 subtrahendBackgrounds[amp.getName()] = background
880 ampData.image.array[:, :] -= background
881 self.log.debug(f"Subtrahend background level: {amp.getName()} {background}")
882
883 toMask = (subtrahend.image.array >= minPixelToMask) | (subtrahend.image.array <= -minPixelToMask)
884 subtrahend.mask.array[toMask] |= subtrahend.mask.getPlaneBitMask(crosstalkStr)
885
886 # Put the backgrounds back.
887 for amp in targetDetector:
888 bbox = self._getAppropriateBBox(amp, isTrimmed, False)
889 ampData = subtrahend[bbox]
890 background = subtrahendBackgrounds[amp.getName()]
891 ampData.image.array[:, :] += background
892
893 # Subtract subtrahend from input. The mask plane is fully
894 # populated, so this operation also sets the ``crosstalkStr``
895 # bit where applicable.
896 targetMaskedImage -= subtrahend
897
898
900 """Configuration for intra-detector crosstalk removal."""
901 minPixelToMask = Field(
902 dtype=float,
903 doc="Set crosstalk mask plane for pixels over this value.",
904 default=45000
905 )
906 crosstalkMaskPlane = Field(
907 dtype=str,
908 doc="Name for crosstalk mask plane.",
909 default="CROSSTALK"
910 )
911 doSubtrahendMasking = Field(
912 dtype=bool,
913 doc="Use subtrahend image thresholding instead of input image thesholding to set crosstalk mask?",
914 default=False,
915 )
916 crosstalkBackgroundMethod = ChoiceField(
917 dtype=str,
918 doc="Type of background subtraction to use when applying correction.",
919 default="None",
920 allowed={
921 "None": "Do no background subtraction.",
922 "AMP": "Subtract amplifier-by-amplifier background levels.",
923 "DETECTOR": "Subtract detector level background."
924 },
925 )
926 useConfigCoefficients = Field(
927 dtype=bool,
928 doc="Ignore the detector crosstalk information in favor of CrosstalkConfig values?",
929 default=False,
930 )
931 crosstalkValues = ListField(
932 dtype=float,
933 doc=("Amplifier-indexed crosstalk coefficients to use. This should be arranged as a 1 x nAmp**2 "
934 "list of coefficients, such that when reshaped by crosstalkShape, the result is nAmp x nAmp. "
935 "This matrix should be structured so CT * [amp0 amp1 amp2 ...]^T returns the column "
936 "vector [corr0 corr1 corr2 ...]^T."),
937 default=[0.0],
938 )
939 crosstalkShape = ListField(
940 dtype=int,
941 doc="Shape of the coefficient array. This should be equal to [nAmp, nAmp].",
942 default=[1],
943 )
944 doQuadraticCrosstalkCorrection = Field(
945 dtype=bool,
946 doc="Use quadratic crosstalk coefficients in the crosstalk correction",
947 default=False,
948 )
949
950 def getCrosstalk(self, detector=None):
951 """Return a 2-D numpy array of crosstalk coefficients in the proper
952 shape.
953
954 Parameters
955 ----------
956 detector : `lsst.afw.cameraGeom.detector`
957 Detector that is to be crosstalk corrected.
958
959 Returns
960 -------
961 coeffs : `numpy.ndarray`
962 Crosstalk coefficients that can be used to correct the detector.
963
964 Raises
965 ------
966 RuntimeError
967 Raised if no coefficients could be generated from this
968 detector/configuration.
969 """
970 if self.useConfigCoefficients is True:
971 coeffs = np.array(self.crosstalkValues).reshape(self.crosstalkShape)
972 if detector is not None:
973 nAmp = len(detector)
974 if coeffs.shape != (nAmp, nAmp):
975 raise RuntimeError("Constructed crosstalk coeffients do not match detector shape. "
976 f"{coeffs.shape} {nAmp}")
977 return coeffs
978 elif detector is not None and detector.hasCrosstalk() is True:
979 # Assume the detector defines itself consistently.
980 return detector.getCrosstalk()
981 else:
982 raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients")
983
984 def hasCrosstalk(self, detector=None):
985 """Return a boolean indicating if crosstalk coefficients exist.
986
987 Parameters
988 ----------
989 detector : `lsst.afw.cameraGeom.detector`
990 Detector that is to be crosstalk corrected.
991
992 Returns
993 -------
994 hasCrosstalk : `bool`
995 True if this detector/configuration has crosstalk coefficients
996 defined.
997 """
998 if self.useConfigCoefficients is True and self.crosstalkValues is not None:
999 return True
1000 elif detector is not None and detector.hasCrosstalk() is True:
1001 return True
1002 else:
1003 return False
1004
1005
1006class CrosstalkTask(Task):
1007 """Apply intra-detector crosstalk correction."""
1008 ConfigClass = CrosstalkConfig
1009 _DefaultName = 'isrCrosstalk'
1010
1011 def run(
1012 self,
1013 exposure,
1014 crosstalk=None,
1015 crosstalkSources=None,
1016 isTrimmed=None,
1017 camera=None,
1018 parallelOverscanRegion=None,
1019 detectorConfig=None,
1020 fullAmplifier=False,
1021 gains=None,
1022 badAmpDict=None,
1023 ignoreVariance=False,
1024 ):
1025 """Apply intra-detector crosstalk correction
1026
1027 Parameters
1028 ----------
1029 exposure : `lsst.afw.image.Exposure`
1030 Exposure for which to remove crosstalk.
1031 crosstalkCalib : `lsst.ip.isr.CrosstalkCalib`, optional
1032 External crosstalk calibration to apply. Constructed from
1033 detector if not found.
1034 crosstalkSources : `defaultdict`, optional
1035 Image data for other detectors that are sources of
1036 crosstalk in exposure. The keys are expected to be names
1037 of the other detectors, with the values containing
1038 `lsst.afw.image.Exposure` at the same level of processing
1039 as ``exposure``.
1040 The default for intra-detector crosstalk here is None.
1041 isTrimmed : `bool`, optional
1042 This option has been deprecated and does not do anything.
1043 It will be removed after v29.
1044 camera : `lsst.afw.cameraGeom.Camera`, optional
1045 Camera associated with this exposure. Only used for
1046 inter-chip matching.
1047 parallelOverscanRegion : `bool`, optional
1048 This option has been deprecated and will be removed after v29.
1049 detectorConfig : `lsst.ip.isr.OverscanDetectorConfig`, optional
1050 This option has been deprecated and will be removed after v29.
1051 fullAmplifier : `bool`, optional
1052 Use full amplifier and not just imaging region.
1053 gains : `dict` [`str`, `float`], optional
1054 Dictionary of amp name to gain. Required if there is a unit
1055 mismatch between the exposure and the crosstalk matrix.
1056 badAmpDict : `dict` [`str`, `bool`], optional
1057 Dictionary to identify bad amplifiers that should not be
1058 source or target for crosstalk correction.
1059 ignoreVariance : `bool`, optional
1060 Ignore variance plane when applying crosstalk?
1061
1062 Raises
1063 ------
1064 RuntimeError
1065 Raised if called for a detector that does not have a
1066 crosstalk correction. Also raised if the crosstalkSource
1067 is not an expected type.
1068 """
1069 # TODO: Remove on DM-48394
1070 if isTrimmed is not None:
1071 warnings.warn(
1072 "The isTrimmed option has been deprecated and will be removed after v29.",
1073 FutureWarning,
1074 )
1075 isTrimmed = isTrimmedImage(exposure.maskedImage, exposure.getDetector())
1076
1077 # TODO: Remove on DM-48394
1078 if parallelOverscanRegion is not None:
1079 warnings.warn(
1080 "The parallelOverscanRegion option has been deprecated and will be removed after v29.",
1081 FutureWarning,
1082 )
1083 if detectorConfig is not None:
1084 warnings.warn(
1085 "The detectorConfig option has been deprecated and will be removed after v29.",
1086 FutureWarning,
1087 )
1088
1089 if not crosstalk:
1090 crosstalk = CrosstalkCalib(log=self.log)
1091 crosstalk = crosstalk.fromDetector(exposure.getDetector(),
1092 coeffVector=self.config.crosstalkValues)
1093 if not crosstalk.log:
1094 crosstalk.log = self.log
1095
1096 doSqrCrosstalk = self.config.doQuadraticCrosstalkCorrection
1097
1098 if doSqrCrosstalk and crosstalk.coeffsSqr is None:
1099 raise RuntimeError("Attempted to perform NL crosstalk correction without NL "
1100 "crosstalk coefficients.")
1101 if doSqrCrosstalk:
1102 crosstalkCoeffsSqr = crosstalk.coeffsSqr
1103 else:
1104 crosstalkCoeffsSqr = None
1105
1106 if not crosstalk.hasCrosstalk:
1107 raise RuntimeError("Attempted to correct crosstalk without crosstalk coefficients.")
1108
1109 # Get the exposure units; defaults to adu.
1110 exposureUnits = exposure.metadata.get("LSST ISR UNITS", "adu")
1111 invertGains = False
1112 gainApply = False
1113 if crosstalk.crosstalkRatiosUnits != exposureUnits:
1114 detector = exposure.getDetector()
1115
1116 if gains is None and np.all(crosstalk.fitGains == 0.0):
1117 raise RuntimeError(
1118 f"Unit mismatch between exposure ({exposureUnits}) and "
1119 f"crosstalk ratios ({crosstalk.crosstalkRatiosUnits}) and "
1120 "no gains were supplied or available in crosstalk calibration.",
1121 )
1122 elif gains is None:
1123 self.log.info("Using crosstalk calib fitGains for gain corrections.")
1124 gains = {}
1125 for i, amp in enumerate(detector):
1126 gains[amp.getName()] = crosstalk.fitGains[i]
1127
1128 # Check individual gains for finite-ness.
1129 gainArray = np.zeros(len(detector))
1130 for i, amp in enumerate(detector):
1131 gainArray[i] = gains[amp.getName()]
1132 badGains = (~np.isfinite(gainArray) | (gainArray == 0.0))
1133 if np.any(badGains):
1134 if np.all(badGains):
1135 raise RuntimeError("No valid (finite, non-zero) gains found for crosstalk correction.")
1136
1137 badAmpNames = [amp.getName() for i, amp in enumerate(detector) if badGains[i]]
1138
1139 self.log.warning("Illegal gain value found for %d amplifiers %r in crosstalk correction; "
1140 "substituting with median gain.", badGains.sum(), badAmpNames)
1141 medGain = np.median(gainArray[~badGains])
1142 gainArray[badGains] = medGain
1143 for i, amp in enumerate(detector):
1144 gains[amp.getName()] = gainArray[i]
1145
1146 gainApply = True
1147
1148 if crosstalk.crosstalkRatiosUnits == "adu":
1149 invertGains = True
1150
1151 if exposureUnits == "adu":
1152 self.log.info("Temporarily applying gains to perform crosstalk correction "
1153 "because crosstalk is in electron units and exposure is in "
1154 "adu units.")
1155 else:
1156 self.log.info("Temporarily un-applying gains to perform crosstalk correction "
1157 "because crosstalk is in adu units and exposure is in "
1158 "electron units.")
1159
1160 self.log.info("Applying crosstalk correction.")
1161
1162 with gainContext(
1163 exposure,
1164 exposure.image,
1165 gainApply,
1166 gains=gains,
1167 invert=invertGains,
1168 isTrimmed=isTrimmed,
1169 ):
1170 crosstalk.subtractCrosstalk(
1171 exposure,
1172 crosstalkCoeffs=crosstalk.coeffs,
1173 crosstalkCoeffsSqr=crosstalkCoeffsSqr,
1174 crosstalkCoeffsValid=crosstalk.coeffValid,
1175 minPixelToMask=self.config.minPixelToMask,
1176 doSubtrahendMasking=self.config.doSubtrahendMasking,
1177 crosstalkStr=self.config.crosstalkMaskPlane,
1178 backgroundMethod=self.config.crosstalkBackgroundMethod,
1179 doSqrCrosstalk=doSqrCrosstalk,
1180 fullAmplifier=fullAmplifier,
1181 badAmpDict=badAmpDict,
1182 ignoreVariance=ignoreVariance,
1183 )
1184
1185 if crosstalk.interChip:
1186 if crosstalkSources:
1187 # Parse crosstalkSources: Identify which detectors we have
1188 # available
1189 if isinstance(crosstalkSources[0], lsst.afw.image.Exposure):
1190 # Received afwImage.Exposure
1191 sourceNames = [exp.getDetector().getName() for exp in crosstalkSources]
1192 elif isinstance(crosstalkSources[0], lsst.daf.butler.DeferredDatasetHandle):
1193 # Received dafButler.DeferredDatasetHandle
1194 detectorList = [source.dataId['detector'] for source in crosstalkSources]
1195 sourceNames = [camera[detectorId].getName() for detectorId in detectorList]
1196 else:
1197 raise RuntimeError("Unknown object passed as crosstalk sources.",
1198 type(crosstalkSources[0]))
1199
1200 for detName in crosstalk.interChip:
1201 if detName not in sourceNames:
1202 self.log.warning("Crosstalk lists %s, not found in sources: %s",
1203 detName, sourceNames)
1204 continue
1205 # Get the coefficients.
1206 interChipCoeffs = crosstalk.interChip[detName]
1207
1208 sourceExposure = crosstalkSources[sourceNames.index(detName)]
1209 if isinstance(sourceExposure, lsst.daf.butler.DeferredDatasetHandle):
1210 # Dereference the dafButler.DeferredDatasetHandle.
1211 sourceExposure = sourceExposure.get()
1212 if not isinstance(sourceExposure, lsst.afw.image.Exposure):
1213 raise RuntimeError("Unknown object passed as crosstalk sources.",
1214 type(sourceExposure))
1215
1216 if sourceExposure.getBBox() != exposure.getBBox():
1217 raise RuntimeError(
1218 "Mis-match between exposure bounding box and crosstalk source "
1219 "exposure bounding box. Were these run with the same trim state?",
1220 )
1221
1222 self.log.info("Correcting detector %s with ctSource %s",
1223 exposure.getDetector().getName(),
1224 sourceExposure.getDetector().getName())
1225 crosstalk.subtractCrosstalk(
1226 exposure,
1227 sourceExposure=sourceExposure,
1228 crosstalkCoeffs=interChipCoeffs,
1229 minPixelToMask=self.config.minPixelToMask,
1230 crosstalkStr=self.config.crosstalkMaskPlane,
1231 backgroundMethod=self.config.crosstalkBackgroundMethod,
1232 ignoreVariance=ignoreVariance,
1233 )
1234 else:
1235 self.log.warning("Crosstalk contains interChip coefficients, but no sources found!")
1236
1237
1239 def run(self, exposure, crosstalkSources=None):
1240 self.log.info("Not performing any crosstalk correction")
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition Exposure.h:72
Pass parameters to a Statistics object.
Definition Statistics.h:83
updateMetadata(self, camera=None, detector=None, filterName=None, setCalibId=False, setCalibInfo=False, setDate=False, **kwargs)
Definition calibType.py:210
_getAppropriateBBox(amp, isTrimmed, fullAmplifier)
Definition crosstalk.py:544
__init__(self, detector=None, nAmp=0, **kwargs)
Definition crosstalk.py:107
updateMetadata(self, setDate=False, **kwargs)
Definition crosstalk.py:142
extractAmp(image, ampToFlip, ampTarget, isTrimmed=False, fullAmplifier=False, parallelOverscan=None)
Definition crosstalk.py:489
calculateBackground(mi, badPixels=["BAD"])
Definition crosstalk.py:564
subtractCrosstalk(self, thisExposure, sourceExposure=None, crosstalkCoeffs=None, crosstalkCoeffsSqr=None, crosstalkCoeffsValid=None, badPixels=["BAD"], minPixelToMask=45000, doSubtrahendMasking=False, crosstalkStr="CROSSTALK", isTrimmed=None, backgroundMethod="None", doSqrCrosstalk=False, fullAmplifier=False, parallelOverscan=None, detectorConfig=None, badAmpDict=None, ignoreVariance=False)
Definition crosstalk.py:593
fromDetector(self, detector, coeffVector=None, coeffSqrVector=None)
Definition crosstalk.py:167
run(self, exposure, crosstalk=None, crosstalkSources=None, isTrimmed=None, camera=None, parallelOverscanRegion=None, detectorConfig=None, fullAmplifier=False, gains=None, badAmpDict=None, ignoreVariance=False)
run(self, exposure, crosstalkSources=None)
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition Statistics.h:361
std::shared_ptr< ImageT > flipImage(ImageT const &inImage, bool flipLR, bool flipTB)
Flip an image left–right and/or top–bottom.
gainContext(exp, image, apply, gains=None, invert=False, isTrimmed=True)
isTrimmedImage(image, detector)