Coverage for python/lsst/ip/isr/photodiode.py: 21%

85 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-07-03 01:44 -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/>. 

21""" 

22Photodiode storage class. 

23""" 

24import numpy as np 

25from astropy.table import Table 

26 

27from lsst.ip.isr import IsrCalib 

28 

29 

30__all__ = ["PhotodiodeCalib"] 

31 

32 

33class PhotodiodeCalib(IsrCalib): 

34 """Independent current measurements from photodiode for linearity 

35 calculations. 

36 

37 Parameters 

38 ---------- 

39 timeSamples : `list` or `numpy.ndarray` 

40 List of samples the photodiode was measured at. 

41 currentSamples : `list` or `numpy.ndarray` 

42 List of current measurements at each time sample. 

43 log : `lsst.log.Log`, optional 

44 Log to write messages to. 

45 **kwargs : 

46 Additional parameters. These will be passed to the parent 

47 constructor with the exception of: 

48 

49 ``"integrationMethod"`` 

50 Name of the algorithm to use to integrate the current 

51 samples. Allowed values are ``DIRECT_SUM`` and 

52 ``TRIMMED_SUM`` (`str`). 

53 """ 

54 

55 _OBSTYPE = 'PHOTODIODE' 

56 _SCHEMA = 'Photodiode' 

57 _VERSION = 1.0 

58 

59 def __init__(self, timeSamples=None, currentSamples=None, **kwargs): 

60 if timeSamples is not None and currentSamples is not None: 

61 if len(timeSamples) != len(currentSamples): 

62 raise RuntimeError(f"Inconsitent vector lengths: {len(timeSamples)} vs {len(currentSamples)}") 

63 else: 

64 self.timeSamples = np.array(timeSamples) 

65 self.currentSamples = np.array(currentSamples) 

66 else: 

67 self.timeSamples = np.array([]) 

68 self.currentSamples = np.array([]) 

69 

70 super().__init__(**kwargs) 

71 

72 if 'integrationMethod' in kwargs: 

73 self.integrationMethod = kwargs.pop('integrationMethod') 

74 else: 

75 self.integrationMethod = 'DIRECT_SUM' 

76 

77 if 'day_obs' in kwargs: 

78 self.updateMetadata(day_obs=kwargs['day_obs']) 

79 if 'seq_num' in kwargs: 

80 self.updateMetadata(seq_num=kwargs['seq_num']) 

81 

82 self.requiredAttributes.update(['timeSamples', 'currentSamples', 'integrationMethod']) 

83 

84 @classmethod 

85 def fromDict(cls, dictionary): 

86 """Construct a PhotodiodeCalib from a dictionary of properties. 

87 

88 Parameters 

89 ---------- 

90 dictionary : `dict` 

91 Dictionary of properties. 

92 

93 Returns 

94 ------- 

95 calib : `lsst.ip.isr.PhotodiodeCalib` 

96 Constructed photodiode data. 

97 

98 Raises 

99 ------ 

100 RuntimeError : 

101 Raised if the supplied dictionary is for a different 

102 calibration type. 

103 """ 

104 calib = cls() 

105 

106 if calib._OBSTYPE != dictionary['metadata']['OBSTYPE']: 

107 raise RuntimeError(f"Incorrect photodiode supplied. Expected {calib._OBSTYPE}, " 

108 f"found {dictionary['metadata']['OBSTYPE']}") 

109 

110 calib.setMetadata(dictionary['metadata']) 

111 

112 calib.timeSamples = np.array(dictionary['timeSamples']) 

113 calib.currentSamples = np.array(dictionary['currentSamples']) 

114 calib.integrationMethod = dictionary.get('integrationMethod', "DIRECT_SUM") 

115 

116 calib.updateMetadata() 

117 return calib 

118 

119 def toDict(self): 

120 """Return a dictionary containing the photodiode properties. 

121 

122 The dictionary should be able to be round-tripped through. 

123 `fromDict`. 

124 

125 Returns 

126 ------- 

127 dictionary : `dict` 

128 Dictionary of properties. 

129 """ 

130 self.updateMetadata() 

131 

132 outDict = {} 

133 outDict['metadata'] = self.getMetadata() 

134 

135 outDict['timeSamples'] = self.timeSamples.tolist() 

136 outDict['currentSamples'] = self.currentSamples.tolist() 

137 

138 outDict['integrationMethod'] = self.integrationMethod 

139 

140 return outDict 

141 

142 @classmethod 

143 def fromTable(cls, tableList): 

144 """Construct calibration from a list of tables. 

145 

146 This method uses the `fromDict` method to create the 

147 calibration after constructing an appropriate dictionary from 

148 the input tables. 

149 

150 Parameters 

151 ---------- 

152 tableList : `list` [`astropy.table.Table`] 

153 List of tables to use to construct the crosstalk 

154 calibration. 

155 

156 Returns 

157 ------- 

158 calib : `lsst.ip.isr.PhotodiodeCalib` 

159 The calibration defined in the tables. 

160 """ 

161 dataTable = tableList[0] 

162 

163 metadata = dataTable.meta 

164 inDict = {} 

165 inDict['metadata'] = metadata 

166 inDict['integrationMethod'] = metadata.pop('INTEGRATION_METHOD', 'DIRECT_SUM') 

167 

168 inDict['timeSamples'] = dataTable['TIME'] 

169 inDict['currentSamples'] = dataTable['CURRENT'] 

170 

171 return cls().fromDict(inDict) 

172 

173 def toTable(self): 

174 """Construct a list of tables containing the information in this 

175 calibration. 

176 

177 The list of tables should create an identical calibration 

178 after being passed to this class's fromTable method. 

179 

180 Returns 

181 ------- 

182 tableList : `list` [`astropy.table.Table`] 

183 List of tables containing the photodiode calibration 

184 information. 

185 """ 

186 self.updateMetadata() 

187 catalog = Table([{'TIME': self.timeSamples, 

188 'CURRENT': self.currentSamples}]) 

189 inMeta = self.getMetadata().toDict() 

190 outMeta = {k: v for k, v in inMeta.items() if v is not None} 

191 outMeta.update({k: "" for k, v in inMeta.items() if v is None}) 

192 outMeta['INTEGRATION_METHOD'] = self.integrationMethod 

193 catalog.meta = outMeta 

194 

195 return([catalog]) 

196 

197 @classmethod 

198 def readTwoColumnPhotodiodeData(cls, filename): 

199 """Construct a PhotodiodeCalib by reading the simple column format. 

200 

201 Parameters 

202 ---------- 

203 filename : `str` 

204 File to read samples from. 

205 

206 Returns 

207 ------- 

208 calib : `lsst.ip.isr.PhotodiodeCalib` 

209 The calibration defined in the file. 

210 """ 

211 import os.path 

212 

213 rawData = np.loadtxt(filename, dtype=[('time', 'float'), ('current', 'float')]) 

214 

215 basename = os.path.basename(filename) 

216 cleaned = os.path.splitext(basename)[0] 

217 _, _, day_obs, seq_num = cleaned.split("_") 

218 

219 return cls(timeSamples=rawData['time'], currentSamples=rawData['current'], 

220 day_obs=int(day_obs), seq_num=int(seq_num)) 

221 

222 def integrate(self): 

223 """Integrate the current. 

224 

225 Raises 

226 ------ 

227 RuntimeError : 

228 Raised if the integration method is not known. 

229 """ 

230 if self.integrationMethod == 'DIRECT_SUM': 

231 return self.integrateDirectSum() 

232 elif self.integrationMethod == 'TRIMMED_SUM': 

233 return self.integrateTrimmedSum() 

234 else: 

235 raise RuntimeError(f"Unknown integration method {self.integrationMethod}") 

236 

237 def integrateDirectSum(self): 

238 """Integrate points. 

239 

240 This uses numpy's trapezoidal integrator. 

241 

242 Returns 

243 ------- 

244 sum : `float` 

245 Total charge measured. 

246 """ 

247 return np.trapz(self.currentSamples, x=self.timeSamples) 

248 

249 def integrateTrimmedSum(self): 

250 """Integrate points with a baseline level subtracted. 

251 

252 This uses numpy's trapezoidal integrator. 

253 

254 Returns 

255 ------- 

256 sum : `float` 

257 Total charge measured. 

258 

259 See Also 

260 -------- 

261 lsst.eotask.gen3.eoPtc 

262 """ 

263 currentThreshold = ((max(self.currentSamples) - min(self.currentSamples))/5.0 

264 + min(self.currentSamples)) 

265 lowValueIndices = np.where(self.currentSamples < currentThreshold) 

266 baseline = np.median(self.currentSamples[lowValueIndices]) 

267 return np.trapz(self.currentSamples - baseline, self.timeSamples)