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

67 statements  

« prev     ^ index     » next       coverage.py v7.0.0, created at 2022-12-20 10:00 +0000

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/>. 

21 

22__all__ = ["IsrStatisticsTaskConfig", "IsrStatisticsTask"] 

23 

24import numpy as np 

25import lsst.afw.math as afwMath 

26import lsst.afw.image as afwImage 

27import lsst.pipe.base as pipeBase 

28import lsst.pex.config as pexConfig 

29 

30from lsst.afw.cameraGeom import ReadoutCorner 

31 

32 

33class IsrStatisticsTaskConfig(pexConfig.Config): 

34 """Image statistics options. 

35 """ 

36 doCtiStatistics = pexConfig.Field( 

37 dtype=bool, 

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

39 default=False, 

40 ) 

41 stat = pexConfig.Field( 

42 dtype=str, 

43 default='MEANCLIP', 

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

45 ) 

46 nSigmaClip = pexConfig.Field( 

47 dtype=float, 

48 default=3.0, 

49 doc="Clipping threshold for background", 

50 ) 

51 nIter = pexConfig.Field( 

52 dtype=int, 

53 default=3, 

54 doc="Clipping iterations for background", 

55 ) 

56 badMask = pexConfig.ListField( 

57 dtype=str, 

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

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

60 ) 

61 

62 

63class IsrStatisticsTask(pipeBase.Task): 

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

65 

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

67 useful for calibration production and detector stability. 

68 """ 

69 ConfigClass = IsrStatisticsTaskConfig 

70 _DefaultName = "isrStatistics" 

71 

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

73 super().__init__(**kwargs) 

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

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

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

77 

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

79 """Task to run arbitrary statistics. 

80 

81 The statistics should be measured by individual methods, and 

82 add to the dictionary in the return struct. 

83 

84 Parameters 

85 ---------- 

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

87 The exposure to measure. 

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

89 A PTC object containing gains to use. 

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

91 List of overscan results. Expected fields are: 

92 

93 ``imageFit`` 

94 Value or fit subtracted from the amplifier image data 

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

96 ``overscanFit`` 

97 Value or fit subtracted from the overscan image data 

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

99 ``overscanImage`` 

100 Image of the overscan region with the overscan 

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

102 quantity is used to estimate the amplifier read noise 

103 empirically. 

104 

105 Returns 

106 ------- 

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

108 Contains the measured statistics as a dict stored in a 

109 field named ``results``. 

110 

111 Raises 

112 ------ 

113 RuntimeError 

114 Raised if the amplifier gains could not be found. 

115 """ 

116 # Find gains. 

117 detector = inputExp.getDetector() 

118 if ptc is not None: 

119 gains = ptc.gain 

120 elif detector is not None: 

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

122 else: 

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

124 if self.config.doCtiStatistics: 

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

126 

127 return pipeBase.Struct( 

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

129 ) 

130 

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

132 """Task to measure CTI statistics. 

133 

134 Parameters 

135 ---------- 

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

137 Exposure to measure. 

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

139 List of overscan results. Expected fields are: 

140 

141 ``imageFit`` 

142 Value or fit subtracted from the amplifier image data 

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

144 ``overscanFit`` 

145 Value or fit subtracted from the overscan image data 

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

147 ``overscanImage`` 

148 Image of the overscan region with the overscan 

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

150 quantity is used to estimate the amplifier read noise 

151 empirically. 

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

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

154 

155 Returns 

156 ------- 

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

158 Dictionary of measurements, keyed by amplifier name and 

159 statistics segment. 

160 """ 

161 outputStats = {} 

162 

163 detector = inputExp.getDetector() 

164 image = inputExp.image 

165 

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

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

168 

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

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'] = list(reversed(columns)) 

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

219 else: 

220 ampStats['OVERSCAN_COLUMNS'] = columns 

221 ampStats['OVERSCAN_VALUES'] = values 

222 

223 outputStats[amp.getName()] = ampStats 

224 

225 return outputStats