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 

33 

34 

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

36 

37 

38class MeasurePhotonTransferCurveTaskConfig(pexConfig.Config): 

39 extract = pexConfig.ConfigurableField( 

40 target=PhotonTransferCurveExtractTask, 

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

42 ) 

43 solve = pexConfig.ConfigurableField( 

44 target=PhotonTransferCurveSolveTask, 

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

46 ) 

47 ccdKey = pexConfig.Field( 

48 dtype=str, 

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

50 default='ccd', 

51 ) 

52 doPhotodiode = pexConfig.Field( 

53 dtype=bool, 

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

55 default=False, 

56 ) 

57 photodiodeDataPath = pexConfig.Field( 

58 dtype=str, 

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

60 default="" 

61 ) 

62 

63 

64class MeasurePhotonTransferCurveTask(pipeBase.CmdLineTask): 

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

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

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

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

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

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

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

72 of the difference image and the mean of the average image 

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

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

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

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

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

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

79 in this task this is not supported. 

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

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

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

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

84 to get the gain and the noise. 

85 

86 Parameters 

87 ---------- 

88 *args: `list` 

89 Positional arguments passed to the Task constructor. None used at this 

90 time. 

91 **kwargs: `dict` 

92 Keyword arguments passed on to the Task constructor. None used at this 

93 time. 

94 """ 

95 

96 RunnerClass = DataRefListRunner 

97 ConfigClass = MeasurePhotonTransferCurveTaskConfig 

98 _DefaultName = "measurePhotonTransferCurve" 

99 

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

101 super().__init__(**kwargs) 

102 self.makeSubtask("extract") 

103 self.makeSubtask("solve") 

104 

105 @pipeBase.timeMethod 

106 def runDataRef(self, dataRefList): 

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

108 For a dataRef (which is each detector here), 

109 and given a list of exposure pairs (postISR) at different exposure times, 

110 measure the PTC. 

111 

112 Parameters 

113 ---------- 

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

115 Data references for exposures. 

116 """ 

117 if len(dataRefList) < 2: 

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

119 

120 # setup necessary objects 

121 dataRef = dataRefList[0] 

122 camera = dataRef.get('camera') 

123 

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

125 raise RuntimeError("Too many detectors supplied") 

126 # Get exposure list. 

127 expList = [] 

128 for dataRef in dataRefList: 

129 try: 

130 tempFlat = dataRef.get("postISRCCD") 

131 except RuntimeError: 

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

133 continue 

134 expList.append(tempFlat) 

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

136 

137 # Create dictionary of exposures, keyed by exposure time 

138 expDict = arrangeFlatsByExpTime(expList) 

139 # Call the "extract" (measure flat covariances) and "solve" (fit covariances) subtasks 

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

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

142 

143 # Fill up the photodiode data, if found, that will be used by linearity task. 

144 # Get expIdPairs from one of the amps 

145 expIdsPairsList = [] 

146 ampNames = resultsSolve.outputPtcDataset.ampNames 

147 for ampName in ampNames: 

148 tempAmpName = ampName 

149 if ampName not in resultsSolve.outputPtcDataset.badAmps: 

150 break 

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

152 first, second = pair[0] 

153 expIdsPairsList.append((first, second)) 

154 

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

156 expIdsPairsList) 

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

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

159 

160 return 

161 

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

163 """Set photoCharge attribute in PTC dataset 

164 

165 Parameters 

166 ---------- 

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

168 Data reference for exposurre for detector to process. 

169 

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

171 The dataset containing information such as the means, variances 

172 and exposure times. 

173 

174 expIdList : `list` 

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

176 

177 Returns 

178 ------- 

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

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

181 it has been modified to update the datasetPtc.photoCharge 

182 attribute. 

183 """ 

184 if self.config.doPhotodiode: 

185 for (expId1, expId2) in expIdList: 

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

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

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

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

190 # registry. Currently expId is concatenated with the 

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

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

193 if self.config.photodiodeDataPath: 

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

195 else: 

196 photodiodeData = getBOTphotodiodeData(dataRef) 

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

198 charges[i] = photodiodeData.getCharge() 

199 else: 

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

201 # the detector number as so is fully qualifying 

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

203 

204 for ampName in datasetPtc.ampNames: 

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

206 else: 

207 # Can't be an empty list, as initialized, because astropy.Table won't allow it 

208 # when saving as fits 

209 for ampName in datasetPtc.ampNames: 

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

211 

212 return datasetPtc