228 def run(self, inputExp, untrimmedInputExposure=None, ptc=None, serialOverscanResults=None,
229 parallelOverscanResults=None, doLegacyCtiStatistics=False, **kwargs):
230 """Task to run arbitrary statistics.
232 The statistics should be measured by individual methods, and
233 add to the dictionary in the return struct.
237 inputExp : `lsst.afw.image.Exposure`
238 The exposure to measure.
240 The exposure to measure overscan statistics from.
241 ptc : `lsst.ip.isr.PtcDataset`, optional
242 A PTC object containing gains to use.
243 serialOverscanResults : `list` [`lsst.pipe.base.Struct`], optional
244 List of serial overscan results. Expected fields are:
247 Value or fit subtracted from the amplifier image data
248 (scalar or `lsst.afw.image.Image`).
250 Value or fit subtracted from the overscan image data
251 (scalar or `lsst.afw.image.Image`).
253 Image of the overscan region with the overscan
254 correction applied (`lsst.afw.image.Image`). This
255 quantity is used to estimate the amplifier read noise
257 parallelOverscanResults : `list` [`lsst.pipe.base.Struct`], optional
258 List of parallel overscan results. Expected fields are:
261 Value or fit subtracted from the amplifier image data
262 (scalar or `lsst.afw.image.Image`).
264 Value or fit subtracted from the overscan image data
265 (scalar or `lsst.afw.image.Image`).
267 Image of the overscan region with the overscan
268 correction applied (`lsst.afw.image.Image`). This
269 quantity is used to estimate the amplifier read noise
271 doLegacyCtiStatistics : `bool`, optional
272 Use the older version of measureCti (not recommended).
273 This should be True if and only if this task is called
274 from IsrTask. TODO: Deprecate legacy CTI + CTI correction
275 from IsrTask (DM-48757).
277 Keyword arguments. Calibrations being passed in should
282 resultStruct : `lsst.pipe.base.Struct`
283 Contains the measured statistics as a dict stored in a
284 field named ``results``.
289 Raised if the amplifier gains could not be found.
292 if self.config.doCtiStatistics
and not doLegacyCtiStatistics:
293 if untrimmedInputExposure
is None:
294 raise RuntimeError(
"Must pass untrimmed exposure if doCtiStatistics==True.")
297 detector = inputExp.getDetector()
300 elif detector
is not None:
301 gains = {amp.getName(): amp.getGain()
for amp
in detector.getAmplifiers()}
303 raise RuntimeError(
"No source of gains provided.")
306 if self.config.doCtiStatistics:
307 if doLegacyCtiStatistics:
308 self.log.warning(
"Calculating the legacy CTI statistics is not recommended.")
311 ctiResults = self.
measureCti(inputExp, untrimmedInputExposure, gains)
313 bandingResults =
None
314 if self.config.doBandingStatistics:
315 bandingResults = self.
measureBanding(inputExp, serialOverscanResults)
317 projectionResults =
None
318 if self.config.doProjectionStatistics:
321 divisaderoResults =
None
322 if self.config.doDivisaderoStatistics:
325 calibDistributionResults =
None
326 if self.config.doCopyCalibDistributionStatistics:
329 biasShiftResults =
None
330 if self.config.doBiasShiftStatistics:
333 ampCorrelationResults =
None
334 if self.config.doAmplifierCorrelationStatistics:
337 mjd = inputExp.getMetadata().get(
"MJD",
None)
339 return pipeBase.Struct(
340 results={
"CTI": ctiResults,
341 "BANDING": bandingResults,
342 "PROJECTION": projectionResults,
343 "CALIBDIST": calibDistributionResults,
344 "BIASSHIFT": biasShiftResults,
345 "AMPCORR": ampCorrelationResults,
347 'DIVISADERO': divisaderoResults,
352 """Task to measure CTI statistics.
356 inputExp : `lsst.afw.image.Exposure`
357 The exposure to measure.
358 untrimmedInputExp : `lsst.afw.image.Exposure`
359 Exposure to measure overscan from.
360 gains : `dict` [`str` `float`]
361 Dictionary of per-amplifier gains, indexed by amplifier name.
365 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
366 Dictionary of measurements, keyed by amplifier name and
367 statistics segment. Everything in units based on electron.
371 The input exposure is needed because it contains the last imaging
372 pixel, with defects applied. And the untrimmed input exposure is
373 needed because it contains the overscan regions. It needs to be
374 this way because the defect masking code requires that the image
375 be trimmed, but we need the image with defects masked to measure
376 the CTI from the last imaging pixel.
380 detector = inputExp.getDetector()
381 image = inputExp.image
382 untrimmedImage = untrimmedInputExp.image
386 imageUnits = inputExp.getMetadata().get(
"LSST ISR UNITS")
387 untrimmedImageUnits = untrimmedInputExp.getMetadata().get(
"LSST ISR UNITS")
390 applyGainToImage =
True if imageUnits ==
"adu" else False
391 applyGainToUntrimmedImage =
True if untrimmedImageUnits ==
"adu" else False
393 with gainContext(inputExp, image, applyGainToImage, gains, isTrimmed=
True):
394 with gainContext(untrimmedInputExp, untrimmedImage, applyGainToUntrimmedImage,
395 gains, isTrimmed=
False):
396 for amp
in detector.getAmplifiers():
398 readoutCorner = amp.getReadoutCorner()
401 dataRegion = image[amp.getBBox()]
402 serialOverscanImage = untrimmedImage[amp.getRawSerialOverscanBBox()]
403 parallelOverscanImage = untrimmedImage[amp.getRawSerialOverscanBBox()]
406 ampStats[
"IMAGE_MEAN"] = afwMath.makeStatistics(dataRegion, self.
statType,
410 colA = afwMath.makeStatistics(
411 dataRegion.array[:, 0],
415 colZ = afwMath.makeStatistics(
416 dataRegion.array[:, -1],
422 rowA = afwMath.makeStatistics(
423 dataRegion.array[0, :],
427 rowZ = afwMath.makeStatistics(
428 dataRegion.array[-1, :],
435 if readoutCorner
in (ReadoutCorner.LR, ReadoutCorner.UR):
436 ampStats[
"FIRST_COLUMN_MEAN"] = colZ
437 ampStats[
"LAST_COLUMN_MEAN"] = colA
439 ampStats[
"FIRST_COLUMN_MEAN"] = colA
440 ampStats[
"LAST_COLUMN_MEAN"] = colZ
444 if readoutCorner
in (ReadoutCorner.UR, ReadoutCorner.UL):
445 ampStats[
"FIRST_ROW_MEAN"] = rowZ
446 ampStats[
"LAST_ROW_MEAN"] = rowA
448 ampStats[
"FIRST_ROW_MEAN"] = rowA
449 ampStats[
"LAST_ROW_MEAN"] = rowZ
453 nSerialOverscanCols = amp.getRawSerialOverscanBBox().getWidth()
454 nParallelOverscanRows = amp.getRawParallelOverscanBBox().getHeight()
459 for idx
in range(0, nSerialOverscanCols):
463 serialOverscanColMean = afwMath.makeStatistics(
464 serialOverscanImage.array[:, idx],
471 columnValues.append(serialOverscanColMean)
475 if readoutCorner
in (ReadoutCorner.LR, ReadoutCorner.UR):
476 ampStats[
"SERIAL_OVERSCAN_COLUMNS"] = list(reversed(columns))
477 ampStats[
"SERIAL_OVERSCAN_VALUES"] = list(reversed(columnValues))
479 ampStats[
"SERIAL_OVERSCAN_COLUMNS"] = columns
480 ampStats[
"SERIAL_OVERSCAN_VALUES"] = columnValues
485 for idx
in range(0, nParallelOverscanRows):
486 parallelOverscanRowMean = afwMath.makeStatistics(
487 parallelOverscanImage.array[idx, :],
494 rowValues.append(parallelOverscanRowMean)
498 if readoutCorner
in (ReadoutCorner.UR, ReadoutCorner.UL):
499 ampStats[
"PARALLEL_OVERSCAN_ROWS"] = list(reversed(rows))
500 ampStats[
"PARALLEL_OVERSCAN_VALUES"] = list(reversed(rowValues))
502 ampStats[
"PARALLEL_OVERSCAN_ROWS"] = rows
503 ampStats[
"PARALLEL_OVERSCAN_VALUES"] = rowValues
505 outputStats[amp.getName()] = ampStats
510 """Task to measure CTI statistics.
514 inputExp : `lsst.afw.image.Exposure`
516 serialOverscans : `list` [`lsst.pipe.base.Struct`]
517 List of serial overscan results (expects base units of adu).
521 Value or fit subtracted from the amplifier image data
522 (scalar or `lsst.afw.image.Image`).
524 Value or fit subtracted from the overscan image data
525 (scalar or `lsst.afw.image.Image`).
527 Image of the overscan region with the overscan
528 correction applied (`lsst.afw.image.Image`). This
529 quantity is used to estimate the amplifier read noise
531 gains : `dict` [`str` `float`]
532 Dictionary of per-amplifier gains, indexed by amplifier name.
536 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
537 Dictionary of measurements, keyed by amplifier name and
538 statistics segment. Everything in units based on electron.
540 self.log.warning(
"Using the legacy version of CTI statistics may give wrong CTI")
544 detector = inputExp.getDetector()
545 image = inputExp.image
549 imageUnits = inputExp.getMetadata().get(
"LSST ISR UNITS")
551 if imageUnits ==
"adu":
558 assert len(serialOverscans) == len(detector.getAmplifiers())
560 with gainContext(inputExp, image, applyGain, gains, isTrimmed=isTrimmed):
561 for ampIter, amp
in enumerate(detector.getAmplifiers()):
563 readoutCorner = amp.getReadoutCorner()
566 dataRegion = image[amp.getBBox()
if isTrimmed
else amp.getRawDataBBox()]
569 ampStats[
"IMAGE_MEAN"] = afwMath.makeStatistics(dataRegion, self.
statType,
573 pixelA = afwMath.makeStatistics(dataRegion.array[:, 0],
576 pixelZ = afwMath.makeStatistics(dataRegion.array[:, -1],
582 if readoutCorner
in (ReadoutCorner.LR, ReadoutCorner.UR):
583 ampStats[
"FIRST_MEAN"] = pixelZ
584 ampStats[
"LAST_MEAN"] = pixelA
586 ampStats[
"FIRST_MEAN"] = pixelA
587 ampStats[
"LAST_MEAN"] = pixelZ
590 if serialOverscans[ampIter]
is None:
593 self.log.warning(
"No overscan information available for ISR statistics for amp %s.",
595 nCols = amp.getRawSerialOverscanBBox().getWidth()
596 ampStats[
"OVERSCAN_COLUMNS"] = np.full((nCols, ), np.nan)
597 ampStats[
"OVERSCAN_VALUES"] = np.full((nCols, ), np.nan)
599 overscanImage = serialOverscans[ampIter].overscanImage
603 for column
in range(0, overscanImage.getWidth()):
610 nRows = amp.getRawSerialOverscanBBox().getHeight()
611 osMean = afwMath.makeStatistics(overscanImage.image.array[:nRows, column],
613 columns.append(column)
616 values.append(osMean * gains[amp.getName()])
620 if readoutCorner
in (ReadoutCorner.LR, ReadoutCorner.UR):
621 ampStats[
"OVERSCAN_COLUMNS"] = list(reversed(columns))
622 ampStats[
"OVERSCAN_VALUES"] = list(reversed(values))
624 ampStats[
"OVERSCAN_COLUMNS"] = columns
625 ampStats[
"OVERSCAN_VALUES"] = values
627 outputStats[amp.getName()] = ampStats
652 """Task to measure banding statistics.
656 inputExp : `lsst.afw.image.Exposure`
658 overscans : `list` [`lsst.pipe.base.Struct`]
659 List of overscan results. Expected fields are:
662 Value or fit subtracted from the amplifier image data
663 (scalar or `lsst.afw.image.Image`).
665 Value or fit subtracted from the overscan image data
666 (scalar or `lsst.afw.image.Image`).
668 Image of the overscan region with the overscan
669 correction applied (`lsst.afw.image.Image`). This
670 quantity is used to estimate the amplifier read noise
675 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
676 Dictionary of measurements, keyed by amplifier name and
681 detector = inputExp.getDetector()
682 kernel = self.
makeKernel(self.config.bandingKernelSize)
684 outputStats[
"AMP_BANDING"] = []
685 for amp, overscanData
in zip(detector.getAmplifiers(), overscans):
686 overscanFit = np.array(overscanData.overscanFit)
687 overscanArray = overscanData.overscanImage.image.array
688 rawOverscan = np.mean(overscanArray + overscanFit, axis=1)
690 smoothedOverscan = np.convolve(rawOverscan, kernel, mode=
"valid")
692 low, high = np.quantile(smoothedOverscan, [self.config.bandingFractionLow,
693 self.config.bandingFractionHigh])
694 outputStats[
"AMP_BANDING"].append(float(high - low))
696 if self.config.bandingUseHalfDetector:
697 fullLength = len(outputStats[
"AMP_BANDING"])
698 outputStats[
"DET_BANDING"] = float(np.nanmedian(outputStats[
"AMP_BANDING"][0:fullLength//2]))
700 outputStats[
"DET_BANDING"] = float(np.nanmedian(outputStats[
"AMP_BANDING"]))
705 """Task to measure metrics from image slicing.
709 inputExp : `lsst.afw.image.Exposure`
711 overscans : `list` [`lsst.pipe.base.Struct`]
712 List of overscan results. Expected fields are:
715 Value or fit subtracted from the amplifier image data
716 (scalar or `lsst.afw.image.Image`).
718 Value or fit subtracted from the overscan image data
719 (scalar or `lsst.afw.image.Image`).
721 Image of the overscan region with the overscan
722 correction applied (`lsst.afw.image.Image`). This
723 quantity is used to estimate the amplifier read noise
728 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
729 Dictionary of measurements, keyed by amplifier name and
734 detector = inputExp.getDetector()
735 kernel = self.
makeKernel(self.config.projectionKernelSize)
737 outputStats[
"AMP_VPROJECTION"] = {}
738 outputStats[
"AMP_HPROJECTION"] = {}
739 convolveMode =
"valid"
740 if self.config.doProjectionFft:
741 outputStats[
"AMP_VFFT_REAL"] = {}
742 outputStats[
"AMP_VFFT_IMAG"] = {}
743 outputStats[
"AMP_HFFT_REAL"] = {}
744 outputStats[
"AMP_HFFT_IMAG"] = {}
745 convolveMode =
"same"
747 for amp
in detector.getAmplifiers():
748 ampArray = inputExp.image[amp.getBBox()].array
750 horizontalProjection = np.mean(ampArray, axis=0)
751 verticalProjection = np.mean(ampArray, axis=1)
753 horizontalProjection = np.convolve(horizontalProjection, kernel, mode=convolveMode)
754 verticalProjection = np.convolve(verticalProjection, kernel, mode=convolveMode)
756 outputStats[
"AMP_HPROJECTION"][amp.getName()] = horizontalProjection.tolist()
757 outputStats[
"AMP_VPROJECTION"][amp.getName()] = verticalProjection.tolist()
759 if self.config.doProjectionFft:
760 horizontalWindow = np.ones_like(horizontalProjection)
761 verticalWindow = np.ones_like(verticalProjection)
762 if self.config.projectionFftWindow ==
"NONE":
764 elif self.config.projectionFftWindow ==
"HAMMING":
765 horizontalWindow = hamming(len(horizontalProjection))
766 verticalWindow = hamming(len(verticalProjection))
767 elif self.config.projectionFftWindow ==
"HANN":
768 horizontalWindow = hann(len(horizontalProjection))
769 verticalWindow = hann(len(verticalProjection))
770 elif self.config.projectionFftWindow ==
"GAUSSIAN":
771 horizontalWindow = gaussian(len(horizontalProjection))
772 verticalWindow = gaussian(len(verticalProjection))
774 raise RuntimeError(f
"Invalid window function: {self.config.projectionFftWindow}")
776 horizontalFFT = np.fft.rfft(np.multiply(horizontalProjection, horizontalWindow))
777 verticalFFT = np.fft.rfft(np.multiply(verticalProjection, verticalWindow))
779 outputStats[
"AMP_HFFT_REAL"][amp.getName()] = np.real(horizontalFFT).tolist()
780 outputStats[
"AMP_HFFT_IMAG"][amp.getName()] = np.imag(horizontalFFT).tolist()
781 outputStats[
"AMP_VFFT_REAL"][amp.getName()] = np.real(verticalFFT).tolist()
782 outputStats[
"AMP_VFFT_IMAG"][amp.getName()] = np.imag(verticalFFT).tolist()
787 """Copy calibration statistics for this exposure.
791 inputExp : `lsst.afw.image.Exposure`
792 The exposure being processed.
794 Keyword arguments with calibrations.
798 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
799 Dictionary of measurements, keyed by amplifier name and
805 for amp
in inputExp.getDetector():
808 for calibType
in (
"bias",
"dark",
"flat"):
809 if kwargs.get(calibType,
None)
is not None:
810 metadata = kwargs[calibType].getMetadata()
811 for pct
in self.config.expectedDistributionLevels:
812 key = f
"LSST CALIB {calibType.upper()} {amp.getName()} DISTRIBUTION {pct}-PCT"
813 ampStats[key] = metadata.get(key, np.nan)
815 for calibType
in (
"defects"):
816 if kwargs.get(calibType,
None)
is not None:
817 metadata = kwargs[calibType].getMetadata()
818 for key
in (f
"LSST CALIB {calibType.upper()} {amp.getName()} N_HOT",
819 f
"LSST CALIB {calibType.upper()} {amp.getName()} N_COLD"):
820 ampStats[key] = metadata.get(key, np.nan)
821 outputStats[amp.getName()] = ampStats
824 for calibType
in (
"defects"):
825 if kwargs.get(calibType,
None)
is not None:
826 metadata = kwargs[calibType].getMetadata()
827 for key
in (f
"LSST CALIB {calibType.upper()} N_BAD_COLUMNS"):
828 outputStats[
"detector"][key] = metadata.get(key, np.nan)
833 """Measure number of bias shifts from overscan data.
837 inputExp : `lsst.afw.image.Exposure`
839 overscans : `list` [`lsst.pipe.base.Struct`]
840 List of overscan results. Expected fields are:
843 Value or fit subtracted from the amplifier image data
844 (scalar or `lsst.afw.image.Image`).
846 Value or fit subtracted from the overscan image data
847 (scalar or `lsst.afw.image.Image`).
849 Image of the overscan region with the overscan
850 correction applied (`lsst.afw.image.Image`). This
851 quantity is used to estimate the amplifier read noise
856 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
857 Dictionary of measurements, keyed by amplifier name and
862 Based on eo_pipe implementation:
863 https://github.com/lsst-camera-dh/eo_pipe/blob/main/python/lsst/eo/pipe/biasShiftsTask.py # noqa: E501 W505
867 detector = inputExp.getDetector()
868 for amp, overscans
in zip(detector, serialOverscanResults):
874 if overscans
is None:
875 ampStats[
"LOCAL_NOISE"] = 0.0
876 ampStats[
"BIAS_SHIFTS"] = []
877 outputStats[amp.getName()] = ampStats
881 rawOverscan = overscans.overscanImage.image.array + overscans.overscanFit
884 rawOverscan = np.mean(rawOverscan[:, self.config.biasShiftColumnSkip:], axis=1)
888 ampStats[
"LOCAL_NOISE"] = float(noise)
889 ampStats[
"BIAS_SHIFTS"] = shift_peaks
891 outputStats[amp.getName()] = ampStats
895 """Scan overscan data for shifts.
899 overscanData : `list` [`float`]
900 Overscan data to search for shifts.
905 Noise estimated from Butterworth filtered overscan data.
906 peaks : `list` [`float`, `float`, `int`, `int`]
907 Shift peak information, containing the convolved peak
908 value, the raw peak value, and the lower and upper bounds
909 of the region checked.
911 numerator, denominator = butter(self.config.biasShiftFilterOrder,
912 self.config.biasShiftCutoff,
913 btype=
"high", analog=
False)
914 noise = np.std(filtfilt(numerator, denominator, overscanData))
915 kernel = np.concatenate([np.arange(self.config.biasShiftWindow),
916 np.arange(-self.config.biasShiftWindow + 1, 0)])
917 kernel = kernel/np.sum(kernel[:self.config.biasShiftWindow])
919 convolved = np.convolve(overscanData, kernel, mode=
"valid")
920 convolved = np.pad(convolved, (self.config.biasShiftWindow - 1, self.config.biasShiftWindow))
922 shift_check = np.abs(convolved)/noise
923 shift_mask = shift_check > self.config.biasShiftThreshold
924 shift_mask[:self.config.biasShiftRowSkip] =
False
926 shift_regions = np.flatnonzero(np.diff(np.r_[np.int8(0),
927 shift_mask.view(np.int8),
928 np.int8(0)])).reshape(-1, 2)
930 for region
in shift_regions:
931 region_peak = np.argmax(shift_check[region[0]:region[1]]) + region[0]
934 [float(convolved[region_peak]), float(region_peak),
935 int(region[0]), int(region[1])])
936 return noise, shift_peaks
939 """Determine if a region is flat.
944 Row with possible peak.
946 Value at the possible peak.
947 overscanData : `list` [`float`]
948 Overscan data used to fit around the possible peak.
953 Indicates if the region is flat, and so the peak is valid.
955 prerange = np.arange(shiftRow - self.config.biasShiftWindow, shiftRow)
956 postrange = np.arange(shiftRow, shiftRow + self.config.biasShiftWindow)
958 preFit = linregress(prerange, overscanData[prerange])
959 postFit = linregress(postrange, overscanData[postrange])
962 preTrend = (2*preFit[0]*len(prerange) < shiftPeak)
963 postTrend = (2*postFit[0]*len(postrange) < shiftPeak)
965 preTrend = (2*preFit[0]*len(prerange) > shiftPeak)
966 postTrend = (2*postFit[0]*len(postrange) > shiftPeak)
968 return (preTrend
and postTrend)
971 """Measure correlations between amplifier segments.
975 inputExp : `lsst.afw.image.Exposure`
977 overscans : `list` [`lsst.pipe.base.Struct`]
978 List of overscan results. Expected fields are:
981 Value or fit subtracted from the amplifier image data
982 (scalar or `lsst.afw.image.Image`).
984 Value or fit subtracted from the overscan image data
985 (scalar or `lsst.afw.image.Image`).
987 Image of the overscan region with the overscan
988 correction applied (`lsst.afw.image.Image`). This
989 quantity is used to estimate the amplifier read noise
994 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
995 Dictionary of measurements, keyed by amplifier name and
1000 Based on eo_pipe implementation:
1001 https://github.com/lsst-camera-dh/eo_pipe/blob/main/python/lsst/eo/pipe/raft_level_correlations.py # noqa: E501 W505
1003 detector = inputExp.getDetector()
1006 for ampId, overscan
in enumerate(serialOverscanResults):
1010 if overscan
is None:
1011 for ampId2, overscan2
in enumerate(serialOverscanResults):
1016 'detector': detector.getId(),
1017 'detectorComp': detector.getId(),
1018 'ampName': detector[ampId].getName(),
1019 'ampComp': detector[ampId2].getName(),
1020 'imageCorr': float(imC),
1021 'overscanCorr': float(osC),
1025 rawOverscan = overscan.overscanImage.image.array + overscan.overscanFit
1026 rawOverscan = rawOverscan.ravel()
1028 ampImage = inputExp[detector[ampId].getBBox()]
1029 ampImage = ampImage.image.array.ravel()
1031 for ampId2, overscan2
in enumerate(serialOverscanResults):
1038 if overscan2
is None:
1042 rawOverscan2 = overscan2.overscanImage.image.array + overscan2.overscanFit
1043 rawOverscan2 = rawOverscan2.ravel()
1045 osC = np.corrcoef(rawOverscan, rawOverscan2)[0, 1]
1047 ampImage2 = inputExp[detector[ampId2].getBBox()]
1048 ampImage2 = ampImage2.image.array.ravel()
1050 imC = np.corrcoef(ampImage, ampImage2)[0, 1]
1052 'detector': detector.getId(),
1053 'detectorComp': detector.getId(),
1054 'ampName': detector[ampId].getName(),
1055 'ampComp': detector[ampId2].getName(),
1056 'imageCorr': float(imC),
1057 'overscanCorr': float(osC),
1063 """Measure Max Divisadero Tearing effect per amp.
1067 inputExp : `lsst.afw.image.Exposure`
1068 Exposure to measure. Usually a flat.
1070 The flat will be selected from here.
1074 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
1075 Dictionary of measurements, keyed by amplifier name and
1077 Measurements include
1079 - DIVISADERO_PROFILE: Robust mean of rows between
1080 divisaderoProjection<Maximum|Minumum> on readout edge of ccd
1081 normalized by a linear fit to the same rows.
1082 - DIVISADERO_MAX_PAIR: Tuple of maximum of the absolute values of
1083 the DIVISADERO_PROFILE, for number of pixels (specified by
1084 divisaderoNumImpactPixels on left and right side of amp.
1085 - DIVISADERO_MAX: Maximum of the absolute values of the
1086 the DIVISADERO_PROFILE, for the divisaderoNumImpactPixels on
1087 boundaries of neighboring amps (including the pixels in those
1088 neighborboring amps).
1092 for amp
in inputExp.getDetector():
1094 ampArray = inputExp.image[amp.getBBox()].array
1096 if amp.getReadoutCorner().name
in (
'UL',
'UR'):
1097 minRow = amp.getBBox().getHeight() - self.config.divisaderoProjectionMaximum
1098 maxRow = amp.getBBox().getHeight() - self.config.divisaderoProjectionMinimum
1100 minRow = self.config.divisaderoProjectionMinimum
1101 maxRow = self.config.divisaderoProjectionMaximum
1103 segment = slice(minRow, maxRow)
1104 projection, _, _ = astropy.stats.sigma_clipped_stats(ampArray[segment, :], axis=0)
1107 projection /= np.median(projection)
1108 columns = np.arange(len(projection))
1110 segment = slice(self.config.divisaderoEdgePixels, -self.config.divisaderoEdgePixels)
1111 model = np.polyfit(columns[segment], projection[segment], 1)
1112 modelProjection = model[0] * columns + model[1]
1113 divisaderoProfile = projection / modelProjection
1116 leftMax = np.nanmax(np.abs(divisaderoProfile[0:self.config.divisaderoNumImpactPixels] - 1.0))
1117 rightMax = np.nanmax(np.abs(divisaderoProfile[-self.config.divisaderoNumImpactPixels:] - 1.0))
1119 ampStats[
'DIVISADERO_PROFILE'] = np.array(divisaderoProfile).tolist()
1120 ampStats[
'DIVISADERO_MAX_PAIR'] = [leftMax, rightMax]
1121 outputStats[amp.getName()] = ampStats
1123 detector = inputExp.getDetector()
1124 xCenters = [amp.getBBox().getCenterX()
for amp
in detector]
1125 yCenters = [amp.getBBox().getCenterY()
for amp
in detector]
1126 xIndices = np.ceil(xCenters / np.min(xCenters) / 2).astype(int) - 1
1127 yIndices = np.ceil(yCenters / np.min(yCenters) / 2).astype(int) - 1
1128 ampIds = np.zeros((len(set(yIndices)), len(set(xIndices))), dtype=int)
1129 for ampId, xIndex, yIndex
in zip(np.arange(len(detector)), xIndices, yIndices):
1130 ampIds[yIndex, xIndex] = ampId
1134 for i, amp
in enumerate(detector):
1135 y, x = np.where(ampIds == i)
1136 end = ampIds.shape[1] - 1
1139 thisAmpsPair = outputStats[amp.getName()][
'DIVISADERO_MAX_PAIR']
1143 myMax = thisAmpsPair[1]
1145 neighborMax = outputStats[detector[ampIds[yInd, 1]].getName()][
'DIVISADERO_MAX_PAIR'][0]
1148 myMax = thisAmpsPair[0]
1150 neighborMax = outputStats[detector[ampIds[yInd, end - 1]].getName()][
'DIVISADERO_MAX_PAIR'][1]
1153 myMax = max(thisAmpsPair)
1154 leftName = detector[ampIds[yInd, max(xInd - 1, 0)]].getName()
1155 rightName = detector[ampIds[yInd, min(xInd + 1, ampIds.shape[1] - 1)]].getName()
1158 neighborMax = max(outputStats[leftName][
'DIVISADERO_MAX_PAIR'][1],
1159 outputStats[rightName][
'DIVISADERO_MAX_PAIR'][0])
1161 divisaderoMax = max([myMax, neighborMax])
1162 outputStats[amp.getName()][
'DIVISADERO_MAX'] = divisaderoMax