Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of cp_pipe. 

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# 

22import numpy as np 

23 

24import lsst.pex.config as pexConfig 

25import lsst.pipe.base as pipeBase 

26from lsst.cp.pipe.utils import arrangeFlatsByExpTime 

27 

28from .photodiode import getBOTphotodiodeData 

29 

30from lsst.pipe.tasks.getRepositoryData import DataRefListRunner 

31from lsst.cp.pipe.ptc.cpExtractPtcTask import PhotonTransferCurveExtractTask 

32from lsst.cp.pipe.ptc.cpSolvePtcTask import PhotonTransferCurveSolveTask 

33from lsst.utils.timer import timeMethod 

34 

35 

36__all__ = ['MeasurePhotonTransferCurveTask', 'MeasurePhotonTransferCurveTaskConfig'] 

37 

38 

39class MeasurePhotonTransferCurveTaskConfig(pexConfig.Config): 

40 extract = pexConfig.ConfigurableField( 

41 target=PhotonTransferCurveExtractTask, 

42 doc="Task to measure covariances from flats.", 

43 ) 

44 solve = pexConfig.ConfigurableField( 

45 target=PhotonTransferCurveSolveTask, 

46 doc="Task to fit models to the measured covariances.", 

47 ) 

48 ccdKey = pexConfig.Field( 

49 dtype=str, 

50 doc="The key by which to pull a detector from a dataId, e.g. 'ccd' or 'detector'.", 

51 default='ccd', 

52 ) 

53 doPhotodiode = pexConfig.Field( 

54 dtype=bool, 

55 doc="Apply a correction based on the photodiode readings if available?", 

56 default=False, 

57 ) 

58 photodiodeDataPath = pexConfig.Field( 

59 dtype=str, 

60 doc="Gen2 only: path to locate the data photodiode data files.", 

61 default="" 

62 ) 

63 

64 

65class MeasurePhotonTransferCurveTask(pipeBase.CmdLineTask): 

66 """A class to calculate, fit, and plot a PTC from a set of flat pairs. 

67 

68 The Photon Transfer Curve (var(signal) vs mean(signal)) is a standard 

69 tool used in astronomical detectors characterization (e.g., Janesick 2001, 

70 Janesick 2007). If ptcFitType is "EXPAPPROXIMATION" or "POLYNOMIAL", 

71 this task calculates the PTC from a series of pairs of flat-field images; 

72 each pair taken at identical exposure times. The difference image of each 

73 pair is formed to eliminate fixed pattern noise, and then the variance 

74 of the difference image and the mean of the average image 

75 are used to produce the PTC. An n-degree polynomial or the approximation 

76 in Equation 16 of Astier+19 ("The Shape of the Photon Transfer Curve 

77 of CCD sensors", arXiv:1905.08677) can be fitted to the PTC curve. These 

78 models include parameters such as the gain (e/DN) and readout noise. 

79 

80 Linearizers to correct for signal-chain non-linearity are also calculated. 

81 The `Linearizer` class, in general, can support per-amp linearizers, but 

82 in this task this is not supported. 

83 

84 If ptcFitType is "FULLCOVARIANCE", the covariances of the difference 

85 images are calculated via the DFT methods described in Astier+19 and the 

86 variances for the PTC are given by the cov[0,0] elements at each signal 

87 level. The full model in Equation 20 of Astier+19 is fit to the PTC 

88 to get the gain and the noise. 

89 

90 Parameters 

91 ---------- 

92 *args: `list` 

93 Positional arguments passed to the Task constructor. None used 

94 at this time. 

95 

96 **kwargs: `dict` 

97 Keyword arguments passed on to the Task constructor. None used 

98 at this time. 

99 """ 

100 

101 RunnerClass = DataRefListRunner 

102 ConfigClass = MeasurePhotonTransferCurveTaskConfig 

103 _DefaultName = "measurePhotonTransferCurve" 

104 

105 def __init__(self, *args, **kwargs): 

106 super().__init__(**kwargs) 

107 self.makeSubtask("extract") 

108 self.makeSubtask("solve") 

109 

110 @timeMethod 

111 def runDataRef(self, dataRefList): 

112 """Run the Photon Transfer Curve (PTC) measurement task. 

113 

114 For a dataRef (which is each detector here), and given a list 

115 of exposure pairs (postISR) at different exposure times, 

116 measure the PTC. 

117 

118 Parameters 

119 ---------- 

120 dataRefList : `list` [`lsst.daf.peristence.ButlerDataRef`] 

121 Data references for exposures. 

122 """ 

123 if len(dataRefList) < 2: 

124 raise RuntimeError("Insufficient inputs to combine.") 

125 

126 # setup necessary objects 

127 dataRef = dataRefList[0] 

128 camera = dataRef.get('camera') 

129 

130 if len(set([dataRef.dataId[self.config.ccdKey] for dataRef in dataRefList])) > 1: 

131 raise RuntimeError("Too many detectors supplied") 

132 # Get exposure list. 

133 expList = [] 

134 for dataRef in dataRefList: 

135 try: 

136 tempFlat = dataRef.get("postISRCCD") 

137 except RuntimeError: 

138 self.log.warn("postISR exposure could not be retrieved. Ignoring flat.") 

139 continue 

140 expList.append(tempFlat) 

141 expIds = [exp.getInfo().getVisitInfo().getExposureId() for exp in expList] 

142 

143 # Create dictionary of exposures, keyed by exposure time 

144 expDict = arrangeFlatsByExpTime(expList) 

145 # Call the "extract" (measure flat covariances) and "solve" 

146 # (fit covariances) subtasks 

147 resultsExtract = self.extract.run(inputExp=expDict, inputDims=expIds) 

148 resultsSolve = self.solve.run(resultsExtract.outputCovariances, camera=camera) 

149 

150 # Fill up the photodiode data, if found, that will be used by 

151 # linearity task. 

152 # Get expIdPairs from one of the amps 

153 expIdsPairsList = [] 

154 ampNames = resultsSolve.outputPtcDataset.ampNames 

155 for ampName in ampNames: 

156 tempAmpName = ampName 

157 if ampName not in resultsSolve.outputPtcDataset.badAmps: 

158 break 

159 for pair in resultsSolve.outputPtcDataset.inputExpIdPairs[tempAmpName]: 

160 first, second = pair[0] 

161 expIdsPairsList.append((first, second)) 

162 

163 resultsSolve.outputPtcDataset = self._setBOTPhotocharge(dataRef, resultsSolve.outputPtcDataset, 

164 expIdsPairsList) 

165 self.log.info("Writing PTC data.") 

166 dataRef.put(resultsSolve.outputPtcDataset, datasetType="photonTransferCurveDataset") 

167 

168 return 

169 

170 def _setBOTPhotocharge(self, dataRef, datasetPtc, expIdList): 

171 """Set photoCharge attribute in PTC dataset 

172 

173 Parameters 

174 ---------- 

175 dataRef : `lsst.daf.peristence.ButlerDataRef` 

176 Data reference for exposurre for detector to process. 

177 

178 datasetPtc : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

179 The dataset containing information such as the means, variances 

180 and exposure times. 

181 

182 expIdList : `list` 

183 List with exposure pairs Ids (one pair per list entry). 

184 

185 Returns 

186 ------- 

187 datasetPtc : `lsst.ip.isr.ptcDataset.PhotonTransferCurveDataset` 

188 This is the same dataset as the input parameter, however, 

189 it has been modified to update the datasetPtc.photoCharge 

190 attribute. 

191 """ 

192 if self.config.doPhotodiode: 

193 for (expId1, expId2) in expIdList: 

194 charges = [-1, -1] # necessary to have a not-found value to keep lists in step 

195 for i, expId in enumerate([expId1, expId2]): 

196 # //1000 is a Gen2 only hack, working around the fact an 

197 # exposure's ID is not the same as the expId in the 

198 # registry. Currently expId is concatenated with the 

199 # zero-padded detector ID. This will all go away in Gen3. 

200 dataRef.dataId['expId'] = expId//1000 

201 if self.config.photodiodeDataPath: 

202 photodiodeData = getBOTphotodiodeData(dataRef, self.config.photodiodeDataPath) 

203 else: 

204 photodiodeData = getBOTphotodiodeData(dataRef) 

205 if photodiodeData: # default path stored in function def to keep task clean 

206 charges[i] = photodiodeData.getCharge() 

207 else: 

208 # full expId (not //1000) here, as that encodes the 

209 # the detector number as so is fully qualifying 

210 self.log.warn(f"No photodiode data found for {expId}") 

211 

212 for ampName in datasetPtc.ampNames: 

213 datasetPtc.photoCharge[ampName].append((charges[0], charges[1])) 

214 else: 

215 # Can't be an empty list, as initialized, because 

216 # astropy.Table won't allow it when saving as fits 

217 for ampName in datasetPtc.ampNames: 

218 datasetPtc.photoCharge[ampName] = np.repeat(np.nan, len(expIdList)) 

219 

220 return datasetPtc