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()
400 ampStats[
"INPUT_GAIN"] = float(gains[amp.getName()])
403 dataRegion = image[amp.getBBox()]
404 serialOverscanImage = untrimmedImage[amp.getRawSerialOverscanBBox()]
405 parallelOverscanImage = untrimmedImage[amp.getRawSerialOverscanBBox()]
408 ampStats[
"IMAGE_MEAN"] = afwMath.makeStatistics(dataRegion, self.
statType,
412 colA = afwMath.makeStatistics(
413 dataRegion.array[:, 0],
417 colZ = afwMath.makeStatistics(
418 dataRegion.array[:, -1],
424 rowA = afwMath.makeStatistics(
425 dataRegion.array[0, :],
429 rowZ = afwMath.makeStatistics(
430 dataRegion.array[-1, :],
437 if readoutCorner
in (ReadoutCorner.LR, ReadoutCorner.UR):
438 ampStats[
"FIRST_COLUMN_MEAN"] = colZ
439 ampStats[
"LAST_COLUMN_MEAN"] = colA
441 ampStats[
"FIRST_COLUMN_MEAN"] = colA
442 ampStats[
"LAST_COLUMN_MEAN"] = colZ
446 if readoutCorner
in (ReadoutCorner.UR, ReadoutCorner.UL):
447 ampStats[
"FIRST_ROW_MEAN"] = rowZ
448 ampStats[
"LAST_ROW_MEAN"] = rowA
450 ampStats[
"FIRST_ROW_MEAN"] = rowA
451 ampStats[
"LAST_ROW_MEAN"] = rowZ
455 nSerialOverscanCols = amp.getRawSerialOverscanBBox().getWidth()
456 nParallelOverscanRows = amp.getRawParallelOverscanBBox().getHeight()
461 for idx
in range(0, nSerialOverscanCols):
465 serialOverscanColMean = afwMath.makeStatistics(
466 serialOverscanImage.array[:, idx],
473 columnValues.append(serialOverscanColMean)
477 if readoutCorner
in (ReadoutCorner.LR, ReadoutCorner.UR):
478 ampStats[
"SERIAL_OVERSCAN_COLUMNS"] = list(reversed(columns))
479 ampStats[
"SERIAL_OVERSCAN_VALUES"] = list(reversed(columnValues))
481 ampStats[
"SERIAL_OVERSCAN_COLUMNS"] = columns
482 ampStats[
"SERIAL_OVERSCAN_VALUES"] = columnValues
487 for idx
in range(0, nParallelOverscanRows):
488 parallelOverscanRowMean = afwMath.makeStatistics(
489 parallelOverscanImage.array[idx, :],
496 rowValues.append(parallelOverscanRowMean)
500 if readoutCorner
in (ReadoutCorner.UR, ReadoutCorner.UL):
501 ampStats[
"PARALLEL_OVERSCAN_ROWS"] = list(reversed(rows))
502 ampStats[
"PARALLEL_OVERSCAN_VALUES"] = list(reversed(rowValues))
504 ampStats[
"PARALLEL_OVERSCAN_ROWS"] = rows
505 ampStats[
"PARALLEL_OVERSCAN_VALUES"] = rowValues
507 outputStats[amp.getName()] = ampStats
512 """Task to measure CTI statistics.
516 inputExp : `lsst.afw.image.Exposure`
518 serialOverscans : `list` [`lsst.pipe.base.Struct`]
519 List of serial overscan results (expects base units of adu).
523 Value or fit subtracted from the amplifier image data
524 (scalar or `lsst.afw.image.Image`).
526 Value or fit subtracted from the overscan image data
527 (scalar or `lsst.afw.image.Image`).
529 Image of the overscan region with the overscan
530 correction applied (`lsst.afw.image.Image`). This
531 quantity is used to estimate the amplifier read noise
533 gains : `dict` [`str` `float`]
534 Dictionary of per-amplifier gains, indexed by amplifier name.
538 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
539 Dictionary of measurements, keyed by amplifier name and
540 statistics segment. Everything in units based on electron.
542 self.log.warning(
"Using the legacy version of CTI statistics may give wrong CTI")
546 detector = inputExp.getDetector()
547 image = inputExp.image
551 imageUnits = inputExp.getMetadata().get(
"LSST ISR UNITS")
553 if imageUnits ==
"adu":
560 assert len(serialOverscans) == len(detector.getAmplifiers())
562 with gainContext(inputExp, image, applyGain, gains, isTrimmed=isTrimmed):
563 for ampIter, amp
in enumerate(detector.getAmplifiers()):
565 readoutCorner = amp.getReadoutCorner()
567 ampStats[
"INPUT_GAIN"] = float(gains[amp.getName()])
570 dataRegion = image[amp.getBBox()
if isTrimmed
else amp.getRawDataBBox()]
573 ampStats[
"IMAGE_MEAN"] = afwMath.makeStatistics(dataRegion, self.
statType,
577 pixelA = afwMath.makeStatistics(dataRegion.array[:, 0],
580 pixelZ = afwMath.makeStatistics(dataRegion.array[:, -1],
586 if readoutCorner
in (ReadoutCorner.LR, ReadoutCorner.UR):
587 ampStats[
"FIRST_MEAN"] = pixelZ
588 ampStats[
"LAST_MEAN"] = pixelA
590 ampStats[
"FIRST_MEAN"] = pixelA
591 ampStats[
"LAST_MEAN"] = pixelZ
594 if serialOverscans[ampIter]
is None:
597 self.log.warning(
"No overscan information available for ISR statistics for amp %s.",
599 nCols = amp.getRawSerialOverscanBBox().getWidth()
600 ampStats[
"OVERSCAN_COLUMNS"] = np.full((nCols, ), np.nan)
601 ampStats[
"OVERSCAN_VALUES"] = np.full((nCols, ), np.nan)
603 overscanImage = serialOverscans[ampIter].overscanImage
607 for column
in range(0, overscanImage.getWidth()):
614 nRows = amp.getRawSerialOverscanBBox().getHeight()
615 osMean = afwMath.makeStatistics(overscanImage.image.array[:nRows, column],
617 columns.append(column)
620 values.append(osMean * gains[amp.getName()])
624 if readoutCorner
in (ReadoutCorner.LR, ReadoutCorner.UR):
625 ampStats[
"OVERSCAN_COLUMNS"] = list(reversed(columns))
626 ampStats[
"OVERSCAN_VALUES"] = list(reversed(values))
628 ampStats[
"OVERSCAN_COLUMNS"] = columns
629 ampStats[
"OVERSCAN_VALUES"] = values
631 outputStats[amp.getName()] = ampStats
656 """Task to measure banding statistics.
660 inputExp : `lsst.afw.image.Exposure`
662 overscans : `list` [`lsst.pipe.base.Struct`]
663 List of overscan results. Expected fields are:
666 Value or fit subtracted from the amplifier image data
667 (scalar or `lsst.afw.image.Image`).
669 Value or fit subtracted from the overscan image data
670 (scalar or `lsst.afw.image.Image`).
672 Image of the overscan region with the overscan
673 correction applied (`lsst.afw.image.Image`). This
674 quantity is used to estimate the amplifier read noise
679 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
680 Dictionary of measurements, keyed by amplifier name and
685 detector = inputExp.getDetector()
686 kernel = self.
makeKernel(self.config.bandingKernelSize)
688 outputStats[
"AMP_BANDING"] = []
689 for amp, overscanData
in zip(detector.getAmplifiers(), overscans):
690 overscanFit = np.array(overscanData.overscanFit)
691 overscanArray = overscanData.overscanImage.image.array
692 rawOverscan = np.mean(overscanArray + overscanFit, axis=1)
694 smoothedOverscan = np.convolve(rawOverscan, kernel, mode=
"valid")
696 low, high = np.quantile(smoothedOverscan, [self.config.bandingFractionLow,
697 self.config.bandingFractionHigh])
698 outputStats[
"AMP_BANDING"].append(float(high - low))
700 if self.config.bandingUseHalfDetector:
701 fullLength = len(outputStats[
"AMP_BANDING"])
702 outputStats[
"DET_BANDING"] = float(np.nanmedian(outputStats[
"AMP_BANDING"][0:fullLength//2]))
704 outputStats[
"DET_BANDING"] = float(np.nanmedian(outputStats[
"AMP_BANDING"]))
709 """Task to measure metrics from image slicing.
713 inputExp : `lsst.afw.image.Exposure`
715 overscans : `list` [`lsst.pipe.base.Struct`]
716 List of overscan results. Expected fields are:
719 Value or fit subtracted from the amplifier image data
720 (scalar or `lsst.afw.image.Image`).
722 Value or fit subtracted from the overscan image data
723 (scalar or `lsst.afw.image.Image`).
725 Image of the overscan region with the overscan
726 correction applied (`lsst.afw.image.Image`). This
727 quantity is used to estimate the amplifier read noise
732 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
733 Dictionary of measurements, keyed by amplifier name and
738 detector = inputExp.getDetector()
739 kernel = self.
makeKernel(self.config.projectionKernelSize)
741 outputStats[
"AMP_VPROJECTION"] = {}
742 outputStats[
"AMP_HPROJECTION"] = {}
743 convolveMode =
"valid"
744 if self.config.doProjectionFft:
745 outputStats[
"AMP_VFFT_REAL"] = {}
746 outputStats[
"AMP_VFFT_IMAG"] = {}
747 outputStats[
"AMP_HFFT_REAL"] = {}
748 outputStats[
"AMP_HFFT_IMAG"] = {}
749 convolveMode =
"same"
751 for amp
in detector.getAmplifiers():
752 ampArray = inputExp.image[amp.getBBox()].array
754 horizontalProjection = np.mean(ampArray, axis=0)
755 verticalProjection = np.mean(ampArray, axis=1)
757 horizontalProjection = np.convolve(horizontalProjection, kernel, mode=convolveMode)
758 verticalProjection = np.convolve(verticalProjection, kernel, mode=convolveMode)
760 outputStats[
"AMP_HPROJECTION"][amp.getName()] = horizontalProjection.tolist()
761 outputStats[
"AMP_VPROJECTION"][amp.getName()] = verticalProjection.tolist()
763 if self.config.doProjectionFft:
764 horizontalWindow = np.ones_like(horizontalProjection)
765 verticalWindow = np.ones_like(verticalProjection)
766 if self.config.projectionFftWindow ==
"NONE":
768 elif self.config.projectionFftWindow ==
"HAMMING":
769 horizontalWindow = hamming(len(horizontalProjection))
770 verticalWindow = hamming(len(verticalProjection))
771 elif self.config.projectionFftWindow ==
"HANN":
772 horizontalWindow = hann(len(horizontalProjection))
773 verticalWindow = hann(len(verticalProjection))
774 elif self.config.projectionFftWindow ==
"GAUSSIAN":
775 horizontalWindow = gaussian(len(horizontalProjection))
776 verticalWindow = gaussian(len(verticalProjection))
778 raise RuntimeError(f
"Invalid window function: {self.config.projectionFftWindow}")
780 horizontalFFT = np.fft.rfft(np.multiply(horizontalProjection, horizontalWindow))
781 verticalFFT = np.fft.rfft(np.multiply(verticalProjection, verticalWindow))
783 outputStats[
"AMP_HFFT_REAL"][amp.getName()] = np.real(horizontalFFT).tolist()
784 outputStats[
"AMP_HFFT_IMAG"][amp.getName()] = np.imag(horizontalFFT).tolist()
785 outputStats[
"AMP_VFFT_REAL"][amp.getName()] = np.real(verticalFFT).tolist()
786 outputStats[
"AMP_VFFT_IMAG"][amp.getName()] = np.imag(verticalFFT).tolist()
791 """Copy calibration statistics for this exposure.
795 inputExp : `lsst.afw.image.Exposure`
796 The exposure being processed.
798 Keyword arguments with calibrations.
802 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
803 Dictionary of measurements, keyed by amplifier name and
809 for amp
in inputExp.getDetector():
812 for calibType
in (
"bias",
"dark",
"flat"):
813 if kwargs.get(calibType,
None)
is not None:
814 metadata = kwargs[calibType].getMetadata()
815 for pct
in self.config.expectedDistributionLevels:
816 key = f
"LSST CALIB {calibType.upper()} {amp.getName()} DISTRIBUTION {pct}-PCT"
817 ampStats[key] = metadata.get(key, np.nan)
819 for calibType
in (
"defects"):
820 if kwargs.get(calibType,
None)
is not None:
821 metadata = kwargs[calibType].getMetadata()
822 for key
in (f
"LSST CALIB {calibType.upper()} {amp.getName()} N_HOT",
823 f
"LSST CALIB {calibType.upper()} {amp.getName()} N_COLD"):
824 ampStats[key] = metadata.get(key, np.nan)
825 outputStats[amp.getName()] = ampStats
828 for calibType
in (
"defects"):
829 if kwargs.get(calibType,
None)
is not None:
830 metadata = kwargs[calibType].getMetadata()
831 for key
in (f
"LSST CALIB {calibType.upper()} N_BAD_COLUMNS"):
832 outputStats[
"detector"][key] = metadata.get(key, np.nan)
837 """Measure number of bias shifts from overscan data.
841 inputExp : `lsst.afw.image.Exposure`
843 overscans : `list` [`lsst.pipe.base.Struct`]
844 List of overscan results. Expected fields are:
847 Value or fit subtracted from the amplifier image data
848 (scalar or `lsst.afw.image.Image`).
850 Value or fit subtracted from the overscan image data
851 (scalar or `lsst.afw.image.Image`).
853 Image of the overscan region with the overscan
854 correction applied (`lsst.afw.image.Image`). This
855 quantity is used to estimate the amplifier read noise
860 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
861 Dictionary of measurements, keyed by amplifier name and
866 Based on eo_pipe implementation:
867 https://github.com/lsst-camera-dh/eo_pipe/blob/main/python/lsst/eo/pipe/biasShiftsTask.py # noqa: E501 W505
871 detector = inputExp.getDetector()
872 for amp, overscans
in zip(detector, serialOverscanResults):
878 if overscans
is None:
879 ampStats[
"LOCAL_NOISE"] = 0.0
880 ampStats[
"BIAS_SHIFTS"] = []
881 outputStats[amp.getName()] = ampStats
885 rawOverscan = overscans.overscanImage.image.array + overscans.overscanFit
888 rawOverscan = np.mean(rawOverscan[:, self.config.biasShiftColumnSkip:], axis=1)
892 ampStats[
"LOCAL_NOISE"] = float(noise)
893 ampStats[
"BIAS_SHIFTS"] = shift_peaks
895 outputStats[amp.getName()] = ampStats
899 """Scan overscan data for shifts.
903 overscanData : `list` [`float`]
904 Overscan data to search for shifts.
909 Noise estimated from Butterworth filtered overscan data.
910 peaks : `list` [`float`, `float`, `int`, `int`]
911 Shift peak information, containing the convolved peak
912 value, the raw peak value, and the lower and upper bounds
913 of the region checked.
915 numerator, denominator = butter(self.config.biasShiftFilterOrder,
916 self.config.biasShiftCutoff,
917 btype=
"high", analog=
False)
918 noise = np.std(filtfilt(numerator, denominator, overscanData))
919 kernel = np.concatenate([np.arange(self.config.biasShiftWindow),
920 np.arange(-self.config.biasShiftWindow + 1, 0)])
921 kernel = kernel/np.sum(kernel[:self.config.biasShiftWindow])
923 convolved = np.convolve(overscanData, kernel, mode=
"valid")
924 convolved = np.pad(convolved, (self.config.biasShiftWindow - 1, self.config.biasShiftWindow))
926 shift_check = np.abs(convolved)/noise
927 shift_mask = shift_check > self.config.biasShiftThreshold
928 shift_mask[:self.config.biasShiftRowSkip] =
False
930 shift_regions = np.flatnonzero(np.diff(np.r_[np.int8(0),
931 shift_mask.view(np.int8),
932 np.int8(0)])).reshape(-1, 2)
934 for region
in shift_regions:
935 region_peak = np.argmax(shift_check[region[0]:region[1]]) + region[0]
938 [float(convolved[region_peak]), float(region_peak),
939 int(region[0]), int(region[1])])
940 return noise, shift_peaks
943 """Determine if a region is flat.
948 Row with possible peak.
950 Value at the possible peak.
951 overscanData : `list` [`float`]
952 Overscan data used to fit around the possible peak.
957 Indicates if the region is flat, and so the peak is valid.
959 prerange = np.arange(shiftRow - self.config.biasShiftWindow, shiftRow)
960 postrange = np.arange(shiftRow, shiftRow + self.config.biasShiftWindow)
962 preFit = linregress(prerange, overscanData[prerange])
963 postFit = linregress(postrange, overscanData[postrange])
966 preTrend = (2*preFit[0]*len(prerange) < shiftPeak)
967 postTrend = (2*postFit[0]*len(postrange) < shiftPeak)
969 preTrend = (2*preFit[0]*len(prerange) > shiftPeak)
970 postTrend = (2*postFit[0]*len(postrange) > shiftPeak)
972 return (preTrend
and postTrend)
975 """Measure correlations between amplifier segments.
979 inputExp : `lsst.afw.image.Exposure`
981 overscans : `list` [`lsst.pipe.base.Struct`]
982 List of overscan results. Expected fields are:
985 Value or fit subtracted from the amplifier image data
986 (scalar or `lsst.afw.image.Image`).
988 Value or fit subtracted from the overscan image data
989 (scalar or `lsst.afw.image.Image`).
991 Image of the overscan region with the overscan
992 correction applied (`lsst.afw.image.Image`). This
993 quantity is used to estimate the amplifier read noise
998 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
999 Dictionary of measurements, keyed by amplifier name and
1004 Based on eo_pipe implementation:
1005 https://github.com/lsst-camera-dh/eo_pipe/blob/main/python/lsst/eo/pipe/raft_level_correlations.py # noqa: E501 W505
1007 detector = inputExp.getDetector()
1010 for ampId, overscan
in enumerate(serialOverscanResults):
1014 if overscan
is None:
1015 for ampId2, overscan2
in enumerate(serialOverscanResults):
1020 'detector': detector.getId(),
1021 'detectorComp': detector.getId(),
1022 'ampName': detector[ampId].getName(),
1023 'ampComp': detector[ampId2].getName(),
1024 'imageCorr': float(imC),
1025 'overscanCorr': float(osC),
1029 rawOverscan = overscan.overscanImage.image.array + overscan.overscanFit
1030 rawOverscan = rawOverscan.ravel()
1032 ampImage = inputExp[detector[ampId].getBBox()]
1033 ampImage = ampImage.image.array.ravel()
1035 for ampId2, overscan2
in enumerate(serialOverscanResults):
1042 if overscan2
is None:
1046 rawOverscan2 = overscan2.overscanImage.image.array + overscan2.overscanFit
1047 rawOverscan2 = rawOverscan2.ravel()
1049 osC = np.corrcoef(rawOverscan, rawOverscan2)[0, 1]
1051 ampImage2 = inputExp[detector[ampId2].getBBox()]
1052 ampImage2 = ampImage2.image.array.ravel()
1054 imC = np.corrcoef(ampImage, ampImage2)[0, 1]
1056 'detector': detector.getId(),
1057 'detectorComp': detector.getId(),
1058 'ampName': detector[ampId].getName(),
1059 'ampComp': detector[ampId2].getName(),
1060 'imageCorr': float(imC),
1061 'overscanCorr': float(osC),
1067 """Measure Max Divisadero Tearing effect per amp.
1071 inputExp : `lsst.afw.image.Exposure`
1072 Exposure to measure. Usually a flat.
1074 The flat will be selected from here.
1078 outputStats : `dict` [`str`, [`dict` [`str`, `float`]]]
1079 Dictionary of measurements, keyed by amplifier name and
1081 Measurements include
1083 - DIVISADERO_PROFILE: Robust mean of rows between
1084 divisaderoProjection<Maximum|Minumum> on readout edge of ccd
1085 normalized by a linear fit to the same rows.
1086 - DIVISADERO_MAX_PAIR: Tuple of maximum of the absolute values of
1087 the DIVISADERO_PROFILE, for number of pixels (specified by
1088 divisaderoNumImpactPixels on left and right side of amp.
1089 - DIVISADERO_MAX: Maximum of the absolute values of the
1090 the DIVISADERO_PROFILE, for the divisaderoNumImpactPixels on
1091 boundaries of neighboring amps (including the pixels in those
1092 neighborboring amps).
1096 for amp
in inputExp.getDetector():
1098 ampArray = inputExp.image[amp.getBBox()].array
1100 if amp.getReadoutCorner().name
in (
'UL',
'UR'):
1101 minRow = amp.getBBox().getHeight() - self.config.divisaderoProjectionMaximum
1102 maxRow = amp.getBBox().getHeight() - self.config.divisaderoProjectionMinimum
1104 minRow = self.config.divisaderoProjectionMinimum
1105 maxRow = self.config.divisaderoProjectionMaximum
1107 segment = slice(minRow, maxRow)
1108 projection, _, _ = astropy.stats.sigma_clipped_stats(ampArray[segment, :], axis=0)
1111 projection /= np.median(projection)
1112 columns = np.arange(len(projection))
1114 segment = slice(self.config.divisaderoEdgePixels, -self.config.divisaderoEdgePixels)
1115 model = np.polyfit(columns[segment], projection[segment], 1)
1116 modelProjection = model[0] * columns + model[1]
1117 divisaderoProfile = projection / modelProjection
1120 leftMax = np.nanmax(np.abs(divisaderoProfile[0:self.config.divisaderoNumImpactPixels] - 1.0))
1121 rightMax = np.nanmax(np.abs(divisaderoProfile[-self.config.divisaderoNumImpactPixels:] - 1.0))
1123 ampStats[
'DIVISADERO_PROFILE'] = np.array(divisaderoProfile).tolist()
1124 ampStats[
'DIVISADERO_MAX_PAIR'] = [leftMax, rightMax]
1125 outputStats[amp.getName()] = ampStats
1127 detector = inputExp.getDetector()
1128 xCenters = [amp.getBBox().getCenterX()
for amp
in detector]
1129 yCenters = [amp.getBBox().getCenterY()
for amp
in detector]
1130 xIndices = np.ceil(xCenters / np.min(xCenters) / 2).astype(int) - 1
1131 yIndices = np.ceil(yCenters / np.min(yCenters) / 2).astype(int) - 1
1132 ampIds = np.zeros((len(set(yIndices)), len(set(xIndices))), dtype=int)
1133 for ampId, xIndex, yIndex
in zip(np.arange(len(detector)), xIndices, yIndices):
1134 ampIds[yIndex, xIndex] = ampId
1138 for i, amp
in enumerate(detector):
1139 y, x = np.where(ampIds == i)
1140 end = ampIds.shape[1] - 1
1143 thisAmpsPair = outputStats[amp.getName()][
'DIVISADERO_MAX_PAIR']
1147 myMax = thisAmpsPair[1]
1149 neighborMax = outputStats[detector[ampIds[yInd, 1]].getName()][
'DIVISADERO_MAX_PAIR'][0]
1152 myMax = thisAmpsPair[0]
1154 neighborMax = outputStats[detector[ampIds[yInd, end - 1]].getName()][
'DIVISADERO_MAX_PAIR'][1]
1157 myMax = max(thisAmpsPair)
1158 leftName = detector[ampIds[yInd, max(xInd - 1, 0)]].getName()
1159 rightName = detector[ampIds[yInd, min(xInd + 1, ampIds.shape[1] - 1)]].getName()
1162 neighborMax = max(outputStats[leftName][
'DIVISADERO_MAX_PAIR'][1],
1163 outputStats[rightName][
'DIVISADERO_MAX_PAIR'][0])
1165 divisaderoMax = max([myMax, neighborMax])
1166 outputStats[amp.getName()][
'DIVISADERO_MAX'] = divisaderoMax