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

67 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-08-03 03:05 -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 # Ensure the detector hasn't been modified: 

168 assert ampIter == amp.getId() 

169 

170 ampStats = {} 

171 gain = gains[amp.getName()] 

172 readoutCorner = amp.getReadoutCorner() 

173 # Full data region. 

174 dataRegion = image[amp.getBBox()] 

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

176 self.statControl).getValue() 

177 

178 # First and last image columns. 

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

180 self.statType, 

181 self.statControl).getValue() 

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

183 self.statType, 

184 self.statControl).getValue() 

185 

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

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

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

189 ampStats['FIRST_MEAN'] = pixelZ 

190 ampStats['LAST_MEAN'] = pixelA 

191 else: 

192 ampStats['FIRST_MEAN'] = pixelA 

193 ampStats['LAST_MEAN'] = pixelZ 

194 

195 # Measure the columns of the overscan. 

196 if overscans[ampIter] is None: 

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

198 # be skipped. 

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

200 amp.getName()) 

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

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

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

204 else: 

205 overscanImage = overscans[ampIter].overscanImage 

206 columns = [] 

207 values = [] 

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

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

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

211 columns.append(column) 

212 values.append(gain * osMean) 

213 

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

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

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

217 ampStats['OVERSCAN_COLUMNS'] = reversed(columns) 

218 ampStats['OVERSCAN_VALUES'] = reversed(values) 

219 else: 

220 ampStats['OVERSCAN_COLUMNS'] = columns 

221 ampStats['OVERSCAN_VALUES'] = values 

222 

223 outputStats[amp.getName()] = ampStats 

224 

225 return outputStats