Coverage for python/lsst/ip/isr/isrStatistics.py: 25%

66 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-18 12:16 -0700

1# This file is part of ip_isr. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21import numpy as np 

22import lsst.afw.math as afwMath 

23import lsst.afw.image as afwImage 

24import lsst.pipe.base as pipeBase 

25import lsst.pex.config as pexConfig 

26 

27from lsst.afw.cameraGeom import ReadoutCorner 

28 

29 

30class IsrStatisticsTaskConfig(pexConfig.Config): 

31 """Image statistics options. 

32 """ 

33 doCtiStatistics = pexConfig.Field( 

34 dtype=bool, 

35 doc="Measure CTI statistics from image and overscans?", 

36 default=False, 

37 ) 

38 stat = pexConfig.Field( 

39 dtype=str, 

40 default='MEANCLIP', 

41 doc="Statistic name to use to measure regions.", 

42 ) 

43 nSigmaClip = pexConfig.Field( 

44 dtype=float, 

45 default=3.0, 

46 doc="Clipping threshold for background", 

47 ) 

48 nIter = pexConfig.Field( 

49 dtype=int, 

50 default=3, 

51 doc="Clipping iterations for background", 

52 ) 

53 badMask = pexConfig.ListField( 

54 dtype=str, 

55 default=["BAD", "INTRP", "SAT"], 

56 doc="Mask planes to ignore when identifying source pixels." 

57 ) 

58 

59 

60class IsrStatisticsTask(pipeBase.Task): 

61 """Task to measure arbitrary statistics on ISR processed exposures. 

62 

63 The goal is to wrap a number of optional measurements that are 

64 useful for calibration production and detector stability. 

65 """ 

66 ConfigClass = IsrStatisticsTaskConfig 

67 _DefaultName = "isrStatistics" 

68 

69 def __init__(self, statControl=None, **kwargs): 

70 super().__init__(**kwargs) 

71 self.statControl = afwMath.StatisticsControl(self.config.nSigmaClip, self.config.nIter, 

72 afwImage.Mask.getPlaneBitMask(self.config.badMask)) 

73 self.statType = afwMath.stringToStatisticsProperty(self.config.stat) 

74 

75 def run(self, inputExp, ptc=None, overscanResults=None, **kwargs): 

76 """Task to run arbitrary statistics. 

77 

78 The statistics should be measured by individual methods, and 

79 add to the dictionary in the return struct. 

80 

81 Parameters 

82 ---------- 

83 inputExp : `lsst.afw.image.Exposure` 

84 The exposure to measure. 

85 ptc : `lsst.ip.isr.PtcDataset`, optional 

86 A PTC object containing gains to use. 

87 overscanResults : `list` [`lsst.pipe.base.Struct`], optional 

88 List of overscan results. Expected fields are: 

89 

90 ``imageFit`` 

91 Value or fit subtracted from the amplifier image data 

92 (scalar or `lsst.afw.image.Image`). 

93 ``overscanFit`` 

94 Value or fit subtracted from the overscan image data 

95 (scalar or `lsst.afw.image.Image`). 

96 ``overscanImage`` 

97 Image of the overscan region with the overscan 

98 correction applied (`lsst.afw.image.Image`). This 

99 quantity is used to estimate the amplifier read noise 

100 empirically. 

101 

102 Returns 

103 ------- 

104 resultStruct : `lsst.pipe.base.Struct` 

105 Contains the measured statistics as a dict stored in a 

106 field named ``results``. 

107 

108 Raises 

109 ------ 

110 RuntimeError 

111 Raised if the amplifier gains could not be found. 

112 """ 

113 # Find gains. 

114 detector = inputExp.getDetector() 

115 if ptc is not None: 

116 gains = ptc.gain 

117 elif detector is not None: 

118 gains = {amp.getName(): amp.getGain() for amp in detector.getAmplifiers()} 

119 else: 

120 raise RuntimeError("No source of gains provided.") 

121 if self.config.doCtiStatistics: 

122 ctiResults = self.measureCti(inputExp, overscanResults, gains) 

123 

124 return pipeBase.Struct( 

125 results={'CTI': ctiResults, }, 

126 ) 

127 

128 def measureCti(self, inputExp, overscans, gains): 

129 """Task to measure CTI statistics. 

130 

131 Parameters 

132 ---------- 

133 inputExp : `lsst.afw.image.Exposure` 

134 Exposure to measure. 

135 overscans : `list` [`lsst.pipe.base.Struct`] 

136 List of overscan results. Expected fields are: 

137 

138 ``imageFit`` 

139 Value or fit subtracted from the amplifier image data 

140 (scalar or `lsst.afw.image.Image`). 

141 ``overscanFit`` 

142 Value or fit subtracted from the overscan image data 

143 (scalar or `lsst.afw.image.Image`). 

144 ``overscanImage`` 

145 Image of the overscan region with the overscan 

146 correction applied (`lsst.afw.image.Image`). This 

147 quantity is used to estimate the amplifier read noise 

148 empirically. 

149 gains : `dict` [`str` `float] 

150 Dictionary of per-amplifier gains, indexed by amplifier name. 

151 

152 Returns 

153 ------- 

154 outputStats : `dict` [`str`, [`dict` [`str`,`float]] 

155 Dictionary of measurements, keyed by amplifier name and 

156 statistics segment. 

157 """ 

158 outputStats = {} 

159 

160 detector = inputExp.getDetector() 

161 image = inputExp.image 

162 

163 # Ensure we have the same number of overscans as amplifiers. 

164 assert len(overscans) == len(detector.getAmplifiers()) 

165 

166 for ampIter, amp in enumerate(detector.getAmplifiers()): 

167 ampStats = {} 

168 gain = gains[amp.getName()] 

169 readoutCorner = amp.getReadoutCorner() 

170 # Full data region. 

171 dataRegion = image[amp.getBBox()] 

172 ampStats['IMAGE_MEAN'] = afwMath.makeStatistics(dataRegion, self.statType, 

173 self.statControl).getValue() 

174 

175 # First and last image columns. 

176 pixelA = afwMath.makeStatistics(dataRegion.array[:, 0], 

177 self.statType, 

178 self.statControl).getValue() 

179 pixelZ = afwMath.makeStatistics(dataRegion.array[:, -1], 

180 self.statType, 

181 self.statControl).getValue() 

182 

183 # We want these relative to the readout corner. If that's 

184 # on the right side, we need to swap them. 

185 if readoutCorner in (ReadoutCorner.LR, ReadoutCorner.UR): 

186 ampStats['FIRST_MEAN'] = pixelZ 

187 ampStats['LAST_MEAN'] = pixelA 

188 else: 

189 ampStats['FIRST_MEAN'] = pixelA 

190 ampStats['LAST_MEAN'] = pixelZ 

191 

192 # Measure the columns of the overscan. 

193 if overscans[ampIter] is None: 

194 # The amplifier is likely entirely bad, and needs to 

195 # be skipped. 

196 self.log.warn("No overscan information available for ISR statistics for amp %s.", 

197 amp.getName()) 

198 nCols = amp.getSerialOverscanBBox().getWidth() 

199 ampStats['OVERSCAN_COLUMNS'] = np.full((nCols, ), np.nan) 

200 ampStats['OVERSCAN_VALUES'] = np.full((nCols, ), np.nan) 

201 else: 

202 overscanImage = overscans[ampIter].overscanImage 

203 columns = [] 

204 values = [] 

205 for column in range(0, overscanImage.getWidth()): 

206 osMean = afwMath.makeStatistics(overscanImage.image.array[:, column], 

207 self.statType, self.statControl).getValue() 

208 columns.append(column) 

209 values.append(gain * osMean) 

210 

211 # We want these relative to the readout corner. If that's 

212 # on the right side, we need to swap them. 

213 if readoutCorner in (ReadoutCorner.LR, ReadoutCorner.UR): 

214 ampStats['OVERSCAN_COLUMNS'] = list(reversed(columns)) 

215 ampStats['OVERSCAN_VALUES'] = list(reversed(values)) 

216 else: 

217 ampStats['OVERSCAN_COLUMNS'] = columns 

218 ampStats['OVERSCAN_VALUES'] = values 

219 

220 outputStats[amp.getName()] = ampStats 

221 

222 return outputStats