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 

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

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

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

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

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

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

73 of the difference image and the mean of the average image 

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

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

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

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

78 

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

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

81 in this task this is not supported. 

82 

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

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

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

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

87 to get the gain and the noise. 

88 

89 Parameters 

90 ---------- 

91 *args: `list` 

92 Positional arguments passed to the Task constructor. None used 

93 at this time. 

94 

95 **kwargs: `dict` 

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

97 at this time. 

98 """ 

99 

100 RunnerClass = DataRefListRunner 

101 ConfigClass = MeasurePhotonTransferCurveTaskConfig 

102 _DefaultName = "measurePhotonTransferCurve" 

103 

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

105 super().__init__(**kwargs) 

106 self.makeSubtask("extract") 

107 self.makeSubtask("solve") 

108 

109 @pipeBase.timeMethod 

110 def runDataRef(self, dataRefList): 

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

112 

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

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

115 measure the PTC. 

116 

117 Parameters 

118 ---------- 

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

120 Data references for exposures. 

121 """ 

122 if len(dataRefList) < 2: 

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

124 

125 # setup necessary objects 

126 dataRef = dataRefList[0] 

127 camera = dataRef.get('camera') 

128 

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

130 raise RuntimeError("Too many detectors supplied") 

131 # Get exposure list. 

132 expList = [] 

133 for dataRef in dataRefList: 

134 try: 

135 tempFlat = dataRef.get("postISRCCD") 

136 except RuntimeError: 

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

138 continue 

139 expList.append(tempFlat) 

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

141 

142 # Create dictionary of exposures, keyed by exposure time 

143 expDict = arrangeFlatsByExpTime(expList) 

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

145 # (fit covariances) subtasks 

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

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

148 

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

150 # linearity task. 

151 # Get expIdPairs from one of the amps 

152 expIdsPairsList = [] 

153 ampNames = resultsSolve.outputPtcDataset.ampNames 

154 for ampName in ampNames: 

155 tempAmpName = ampName 

156 if ampName not in resultsSolve.outputPtcDataset.badAmps: 

157 break 

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

159 first, second = pair[0] 

160 expIdsPairsList.append((first, second)) 

161 

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

163 expIdsPairsList) 

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

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

166 

167 return 

168 

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

170 """Set photoCharge attribute in PTC dataset 

171 

172 Parameters 

173 ---------- 

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

175 Data reference for exposurre for detector to process. 

176 

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

178 The dataset containing information such as the means, variances 

179 and exposure times. 

180 

181 expIdList : `list` 

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

183 

184 Returns 

185 ------- 

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

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

188 it has been modified to update the datasetPtc.photoCharge 

189 attribute. 

190 """ 

191 if self.config.doPhotodiode: 

192 for (expId1, expId2) in expIdList: 

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

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

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

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

197 # registry. Currently expId is concatenated with the 

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

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

200 if self.config.photodiodeDataPath: 

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

202 else: 

203 photodiodeData = getBOTphotodiodeData(dataRef) 

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

205 charges[i] = photodiodeData.getCharge() 

206 else: 

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

208 # the detector number as so is fully qualifying 

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

210 

211 for ampName in datasetPtc.ampNames: 

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

213 else: 

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

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

216 for ampName in datasetPtc.ampNames: 

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

218 

219 return datasetPtc