Coverage for python/lsst/cp/pipe/pdCorrection.py: 19%

65 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-23 02:19 -0700

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.pipe.base as pipeBase 

25import lsst.pipe.base.connectionTypes as cT 

26 

27from lsst.ip.isr import (PhotodiodeCorrection, IsrProvenance) 

28 

29__all__ = ["PhotodiodeCorrectionTask", "PhotodiodeCorrectionConfig"] 

30 

31 

32class PhotodiodeCorrectionConnections(pipeBase.PipelineTaskConnections, 

33 dimensions=("instrument", )): 

34 

35 camera = cT.PrerequisiteInput( 

36 name="camera", 

37 doc="Camera Geometry definition.", 

38 storageClass="Camera", 

39 dimensions=("instrument", ), 

40 isCalibration=True, 

41 ) 

42 

43 inputPtc = cT.PrerequisiteInput( 

44 name="ptc", 

45 doc="Input PTC dataset.", 

46 storageClass="PhotonTransferCurveDataset", 

47 dimensions=("instrument", "detector"), 

48 multiple=True, 

49 isCalibration=True, 

50 ) 

51 

52 inputLinearizer = cT.Input( 

53 name="unCorrectedLinearizer", 

54 doc="Raw linearizers that have not been corrected.", 

55 storageClass="Linearizer", 

56 dimensions=("instrument", "detector"), 

57 multiple=True, 

58 isCalibration=True, 

59 ) 

60 

61 outputPhotodiodeCorrection = cT.Output( 

62 name="pdCorrection", 

63 doc="Correction of photodiode systematic error.", 

64 storageClass="IsrCalib", 

65 dimensions=("instrument", ), 

66 isCalibration=True, 

67 ) 

68 

69 

70class PhotodiodeCorrectionConfig(pipeBase.PipelineTaskConfig, 

71 pipelineConnections=PhotodiodeCorrectionConnections): 

72 """Configuration for calculating the photodiode corrections. 

73 """ 

74 

75 

76class PhotodiodeCorrectionTask(pipeBase.PipelineTask): 

77 """Calculate the photodiode corrections. 

78 """ 

79 

80 ConfigClass = PhotodiodeCorrectionConfig 

81 _DefaultName = 'cpPhotodiodeCorrection' 

82 

83 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

84 """Ensure that the input and output dimensions are passed along. 

85 

86 Parameters 

87 ---------- 

88 butlerQC : `lsst.daf.butler.QuantumContext` 

89 Butler to operate on. 

90 inputRefs : `lsst.pipe.base.InputQuantizedConnection` 

91 Input data refs to load. 

92 outputRefs : `lsst.pipe.base.OutputQuantizedConnection` 

93 Output data refs to persist. 

94 """ 

95 inputs = butlerQC.get(inputRefs) 

96 

97 # Use the dimensions to set calib/provenance information. 

98 inputs['inputDims'] = dict(inputRefs.inputPtc[0].dataId.required) 

99 

100 # Need to generate a joint list of detectors present in both 

101 # inputPtc and inputLinearizer. We do this here because the 

102 # detector info is not present in inputPtc metadata. We could 

103 # move it when that is fixed. 

104 

105 self.detectorList = [] 

106 for i, lin in enumerate(inputRefs.inputLinearizer): 

107 linDetector = lin.dataId["detector"] 

108 for j, ptc in enumerate(inputRefs.inputPtc): 

109 ptcDetector = ptc.dataId["detector"] 

110 if ptcDetector == linDetector: 

111 self.detectorList.append((linDetector, i, j)) 

112 break 

113 

114 outputs = self.run(**inputs) 

115 butlerQC.put(outputs, outputRefs) 

116 

117 def run(self, inputPtc, inputLinearizer, camera, inputDims): 

118 """Calculate the systematic photodiode correction. 

119 

120 Parameters 

121 ---------- 

122 inputPtc : `lsst.ip.isr.PtcDataset` 

123 Pre-measured PTC dataset. 

124 inputLinearizer : `lsst.ip.isr.Linearizer` 

125 Previously measured linearizer. 

126 camera : `lsst.afw.cameraGeom.Camera` 

127 Camera geometry. 

128 inputDims : `lsst.daf.butler.DataCoordinate` or `dict` 

129 DataIds to use to populate the output calibration. 

130 

131 Returns 

132 ------- 

133 results : `lsst.pipe.base.Struct` 

134 The results struct containing: 

135 

136 ``outputCorrection`` 

137 Final correction calibration 

138 (`lsst.ip.isr.PhotodiodeCorrection`). 

139 ``outputProvenance`` 

140 Provenance data for the new calibration 

141 (`lsst.ip.isr.IsrProvenance`). 

142 

143 Notes 

144 ----- 

145 Basic correction algorithm (due to Aaron Roodman) is 

146 as follows: 

147 (1) Run the spline fit to the flux vs monitor diode. 

148 (2) For each amp and each exposure, calculate the 

149 correction needed to the monitor diode reading to 

150 bring it to the spline. We call this the 

151 abscissaCorrection. 

152 (3) For each exposure, take the median correction 

153 across the focal plane. Random variations will cancel 

154 out, but systematic variations will not. 

155 (4) Subtract this correction from each monitor diode 

156 reading. 

157 (5) Re-run the spline fit using the corrected monitor 

158 diode readings. 

159 """ 

160 # Initialize photodiodeCorrection. 

161 photodiodeCorrection = PhotodiodeCorrection(log=self.log) 

162 

163 abscissaCorrections = {} 

164 # Load all of the corrections, keyed by exposure pair 

165 for (detector, linIndex, ptcIndex) in self.detectorList: 

166 try: 

167 thisLinearizer = inputLinearizer[linIndex] 

168 thisPtc = inputPtc[ptcIndex] 

169 except (RuntimeError, OSError): 

170 continue 

171 

172 for amp in camera[detector].getAmplifiers(): 

173 ampName = amp.getName() 

174 fluxResidual = thisLinearizer.fitResiduals[ampName] 

175 linearSlope = thisLinearizer.linearFit[ampName] 

176 if np.isnan(linearSlope[1]): 

177 abscissaCorrection = np.zeros(len(fluxResidual)) 

178 elif linearSlope[1] < 1.0E-12: 

179 abscissaCorrection = np.zeros(len(fluxResidual)) 

180 else: 

181 abscissaCorrection = fluxResidual / linearSlope[1] 

182 for i, pair in enumerate(thisPtc.inputExpIdPairs[ampName]): 

183 key = str(pair[0]) 

184 try: 

185 abscissaCorrections[key].append(abscissaCorrection[i]) 

186 except KeyError: 

187 abscissaCorrections[key] = [] 

188 abscissaCorrections[key].append(abscissaCorrection[i]) 

189 # Now the correction is the median correction 

190 # across the whole focal plane. 

191 for key in abscissaCorrections.keys(): 

192 correction = np.nanmedian(abscissaCorrections[key]) 

193 if np.isnan(correction): 

194 correction = 0.0 

195 abscissaCorrections[key] = correction 

196 photodiodeCorrection.abscissaCorrections = abscissaCorrections 

197 

198 photodiodeCorrection.validate() 

199 photodiodeCorrection.updateMetadataFromExposures(inputPtc) 

200 photodiodeCorrection.updateMetadataFromExposures(inputLinearizer) 

201 photodiodeCorrection.updateMetadata(camera=camera, filterName='NONE') 

202 photodiodeCorrection.updateMetadata(setDate=True, setCalibId=True) 

203 provenance = IsrProvenance(calibType='photodiodeCorrection') 

204 

205 return pipeBase.Struct( 

206 outputPhotodiodeCorrection=photodiodeCorrection, 

207 outputProvenance=provenance, 

208 )