From bbe7d3a4b0bc15a8d483359cf5615fa76f3b211e Mon Sep 17 00:00:00 2001 From: plazas Date: Tue, 27 Feb 2024 11:26:49 -0800 Subject: [PATCH 1/4] Add initial CTI EPER code --- python/lsst/ip/isr/isrStatistics.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/python/lsst/ip/isr/isrStatistics.py b/python/lsst/ip/isr/isrStatistics.py index fce95176e..71b7ed2bb 100644 --- a/python/lsst/ip/isr/isrStatistics.py +++ b/python/lsst/ip/isr/isrStatistics.py @@ -43,6 +43,12 @@ class IsrStatisticsTaskConfig(pexConfig.Config): doc="Measure CTI statistics from image and overscans?", default=False, ) + doCtiEper = pexConfig.Field( + dtype=bool, + doc="Measure serial and parallel Charge Transfer Inefficiency " + "using the Extended Pixel Edge Response (EPER) method?", + default=False, + ) doApplyGainsForCtiStatistics = pexConfig.Field( dtype=bool, doc="Apply gain to the overscan region when measuring CTI statistics?", @@ -274,6 +280,10 @@ def run(self, inputExp, ptc=None, overscanResults=None, **kwargs): if self.config.doCtiStatistics: ctiResults = self.measureCti(inputExp, overscanResults, gains) + ctiEperResults = None + if self.config.doCtiEper: + ctiEperResults = self.measureCtiEper(inputExp, overscanResults) + bandingResults = None if self.config.doBandingStatistics: bandingResults = self.measureBanding(inputExp, overscanResults) @@ -302,13 +312,14 @@ def run(self, inputExp, ptc=None, overscanResults=None, **kwargs): return pipeBase.Struct( results={"CTI": ctiResults, + "CTIEPER": ctiEperResults, "BANDING": bandingResults, "PROJECTION": projectionResults, "CALIBDIST": calibDistributionResults, "BIASSHIFT": biasShiftResults, "AMPCORR": ampCorrelationResults, "MJD": mjd, - 'DIVISADERO': divisaderoResults, + "DIVISADERO": divisaderoResults, }, ) From 772f3f329ef0989e0e59c768cc7622a8d8a67034 Mon Sep 17 00:00:00 2001 From: plazas Date: Thu, 29 Feb 2024 12:34:54 -0800 Subject: [PATCH 2/4] Add sCTI and pCTI calculations --- python/lsst/ip/isr/isrStatistics.py | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/python/lsst/ip/isr/isrStatistics.py b/python/lsst/ip/isr/isrStatistics.py index 71b7ed2bb..32a6d1ef1 100644 --- a/python/lsst/ip/isr/isrStatistics.py +++ b/python/lsst/ip/isr/isrStatistics.py @@ -49,6 +49,12 @@ class IsrStatisticsTaskConfig(pexConfig.Config): "using the Extended Pixel Edge Response (EPER) method?", default=False, ) + nPixelsOverscanCtiEper = pexConfig.Field( + dtype=int, + doc="Number of overscan rows or columns to use for " + "evaluating the trailed signal in the overscan regions.", + default=3, + ) doApplyGainsForCtiStatistics = pexConfig.Field( dtype=bool, doc="Apply gain to the overscan region when measuring CTI statistics?", @@ -429,6 +435,87 @@ def measureCti(self, inputExp, overscans, gains): return outputStats + def measureCtiEper(self, inputExp, overscans): + """Task to measure CTI using the EPER method. + + Parameters + ---------- + inputExp : `lsst.afw.image.Exposure` + Exposure to measure. + overscans : `list` [`lsst.pipe.base.Struct`] + List of overscan results. Expected fields are: + + ``imageFit`` + Value or fit subtracted from the amplifier image data + (scalar or `lsst.afw.image.Image`). + ``overscanFit`` + Value or fit subtracted from the overscan image data + (scalar or `lsst.afw.image.Image`). + ``overscanImage`` + Image of the overscan region with the overscan + correction applied (`lsst.afw.image.Image`). This + quantity is used to estimate the amplifier read noise + empirically. + + Returns + ------- + outputStats : `dict` [`str`, [`dict` [`str`,`float]] + Dictionary of measurements, keyed by amplifier name and + statistics segment. + """ + outputStats = {} + + detector = inputExp.getDetector() + image = inputExp.image + + # Ensure we have the same number of overscans as amplifiers. + assert len(overscans) == len(detector.getAmplifiers()) + + # Number of pixels in the overscan for trailed signal + nPixOs = self.config.nPixelsOverscanCtiEper + + for ampIter, amp in enumerate(detector.getAmplifiers()): + ampStats = {} + # Full data region. + dataBox = amp.getBBox() + firstCol = dataBox.minX + lastCol = dataBox.maxX + firstRow = dataBox.minY + lastRow = dataBox.maxY + dataRegion = image[dataBox] + + # Overscan + overscanBox = amp.getRawSerialOverscanBBox() + firstColOs = overscanBox.minX + lastColOs = overscanBox.maxX + + if overscans[ampIter] is None: + # The amplifier is likely entirely bad, and needs to + # be skipped. + self.log.warning("No overscan information available for EPER CTI for amp %s.", + amp.getName()) + ampStats["SERIAL_CTI_EPER"] = np.nan + ampStats["PARALLEL_CTI_EPER"] = np.nan + else: + overscanImage = overscans[ampIter].overscanImage + + # serial CTI with EPER + signal = np.nansum(dataRegion[firstRow:lastRow + 1, lastCol]) + trailed = np.nansum(overscanImage[firstRow:lastRow + 1, firstColOs:firstColOs + nPixOs]) + serialCTI = (trailed/signal)/(dataBox.width + 1) + + # parallel CTI with EPER + signal = np.nansum(dataRegion[lastRow, firstCol:lastCol + 1]) + trailed = np.nansum(overscanImage[lastRow:lastRow + 1 + nPixOs, firstColOs:lastColOs + 1]) + parallelCTI = (trailed/signal)/(dataBox.height + 1) + + ampStats["SERIAL_CTI_EPER"] = serialCTI + ampStats["PARALLEL_CTI_EPER"] = parallelCTI + + outputStats[amp.getName()] = ampStats + + return outputStats + @staticmethod def makeKernel(kernelSize): """Make a boxcar smoothing kernel. From 17444d9d31725cbb54bc774fd87ed1aff5b8a172 Mon Sep 17 00:00:00 2001 From: plazas Date: Tue, 5 Mar 2024 07:48:25 -0800 Subject: [PATCH 3/4] Add check for readout corner --- python/lsst/ip/isr/isrStatistics.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/python/lsst/ip/isr/isrStatistics.py b/python/lsst/ip/isr/isrStatistics.py index 32a6d1ef1..f921f5bf4 100644 --- a/python/lsst/ip/isr/isrStatistics.py +++ b/python/lsst/ip/isr/isrStatistics.py @@ -484,6 +484,13 @@ def measureCtiEper(self, inputExp, overscans): lastRow = dataBox.maxY dataRegion = image[dataBox] + # If readout corner is on the right side, + # we need to swap the first and last columns + # of teh data region + readoutCorner = amp.getReadoutCorner() + if readoutCorner in (ReadoutCorner.LR, ReadoutCorner.UR): + firstCol, lastCol = lastCol, firstCol + # Overscan overscanBox = amp.getRawSerialOverscanBBox() firstColOs = overscanBox.minX From 46a901b40c41e220629b93fce8362894174b8163 Mon Sep 17 00:00:00 2001 From: plazas Date: Tue, 5 Mar 2024 07:50:06 -0800 Subject: [PATCH 4/4] Define first row for overscan --- python/lsst/ip/isr/isrStatistics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/lsst/ip/isr/isrStatistics.py b/python/lsst/ip/isr/isrStatistics.py index f921f5bf4..66548b3e8 100644 --- a/python/lsst/ip/isr/isrStatistics.py +++ b/python/lsst/ip/isr/isrStatistics.py @@ -495,6 +495,7 @@ def measureCtiEper(self, inputExp, overscans): overscanBox = amp.getRawSerialOverscanBBox() firstColOs = overscanBox.minX lastColOs = overscanBox.maxX + firstRowOs = overscanBox.minY if overscans[ampIter] is None: # The amplifier is likely entirely bad, and needs to @@ -513,7 +514,7 @@ def measureCtiEper(self, inputExp, overscans): # parallel CTI with EPER signal = np.nansum(dataRegion[lastRow, firstCol:lastCol + 1]) - trailed = np.nansum(overscanImage[lastRow:lastRow + 1 + nPixOs, firstColOs:lastColOs + 1]) + trailed = np.nansum(overscanImage[firstRowOs:firstRowOs + 1 + nPixOs, firstColOs:lastColOs + 1]) parallelCTI = (trailed/signal)/(dataBox.height + 1) ampStats["SERIAL_CTI_EPER"] = serialCTI