Coverage for python/lsst/cp/pipe/pdCorrection.py: 26%
64 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-14 03:52 -0700
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-14 03:52 -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
24import lsst.pipe.base as pipeBase
25import lsst.pipe.base.connectionTypes as cT
27from lsst.ip.isr import (PhotodiodeCorrection, IsrProvenance)
28from ._lookupStaticCalibration import lookupStaticCalibration
30__all__ = ["PhotodiodeCorrectionTask", "PhotodiodeCorrectionConfig"]
33class PhotodiodeCorrectionConnections(pipeBase.PipelineTaskConnections,
34 dimensions=("instrument", )):
36 camera = cT.PrerequisiteInput(
37 name="camera",
38 doc="Camera Geometry definition.",
39 storageClass="Camera",
40 dimensions=("instrument", ),
41 isCalibration=True,
42 lookupFunction=lookupStaticCalibration,
43 )
45 inputPtc = cT.PrerequisiteInput(
46 name="ptc",
47 doc="Input PTC dataset.",
48 storageClass="PhotonTransferCurveDataset",
49 dimensions=("instrument", "detector"),
50 multiple=True,
51 isCalibration=True,
52 )
54 inputLinearizer = cT.Input(
55 name="unCorrectedLinearizer",
56 doc="Raw linearizers that have not been corrected.",
57 storageClass="Linearizer",
58 dimensions=("instrument", "detector"),
59 multiple=True,
60 isCalibration=True,
61 )
63 outputPhotodiodeCorrection = cT.Output(
64 name="pdCorrection",
65 doc="Correction of photodiode systematic error.",
66 storageClass="IsrCalib",
67 dimensions=("instrument", ),
68 isCalibration=True,
69 )
72class PhotodiodeCorrectionConfig(pipeBase.PipelineTaskConfig,
73 pipelineConnections=PhotodiodeCorrectionConnections):
74 """Configuration for calculating the photodiode corrections.
75 """
78class PhotodiodeCorrectionTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
79 """Calculate the photodiode corrections.
80 """
82 ConfigClass = PhotodiodeCorrectionConfig
83 _DefaultName = 'cpPhotodiodeCorrection'
85 def runQuantum(self, butlerQC, inputRefs, outputRefs):
86 """Ensure that the input and output dimensions are passed along.
88 Parameters
89 ----------
90 butlerQC : `lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
91 Butler to operate on.
92 inputRefs : `lsst.pipe.base.connections.InputQuantizedConnection`
93 Input data refs to load.
94 outputRefs : `lsst.pipe.base.connections.OutputQuantizedConnection`
95 Output data refs to persist.
96 """
97 inputs = butlerQC.get(inputRefs)
99 # Use the dimensions to set calib/provenance information.
100 inputs['inputDims'] = inputRefs.inputPtc[0].dataId.byName()
102 # Need to generate a joint list of detectors present in both
103 # inputPtc and inputLinearizer. We do this here because the
104 # detector info is not present in inputPtc metadata. We could
105 # move it when that is fixed.
107 self.detectorList = []
108 for i, lin in enumerate(inputRefs.inputLinearizer):
109 linDetector = lin.dataId["detector"]
110 for j, ptc in enumerate(inputRefs.inputPtc):
111 ptcDetector = ptc.dataId["detector"]
112 if ptcDetector == linDetector:
113 self.detectorList.append((linDetector, i, j))
114 break
116 outputs = self.run(**inputs)
117 butlerQC.put(outputs, outputRefs)
119 def run(self, inputPtc, inputLinearizer, camera, inputDims):
120 """Calculate the systematic photodiode correction.
122 Parameters
123 ----------
124 inputPtc : `lsst.ip.isr.PtcDataset`
125 Pre-measured PTC dataset.
126 inputLinearizer : `lsst.ip.isr.Linearizer`
127 Previously measured linearizer.
128 camera : `lsst.afw.cameraGeom.Camera`
129 Camera geometry.
130 inputDims : `lsst.daf.butler.DataCoordinate` or `dict`
131 DataIds to use to populate the output calibration.
133 Returns
134 -------
135 results : `lsst.pipe.base.Struct`
136 The results struct containing:
138 ``outputCorrection``
139 Final correction calibration
140 (`lsst.ip.isr.PhotodiodeCorrection`).
141 ``outputProvenance``
142 Provenance data for the new calibration
143 (`lsst.ip.isr.IsrProvenance`).
145 Notes
146 -----
147 Basic correction algorithm (due to Aaron Roodman) is
148 as follows:
149 (1) Run the spline fit to the flux vs monitor diode.
150 (2) For each amp and each exposure, calculate the
151 correction needed to the monitor diode reading to
152 bring it to the spline. We call this the
153 abscissaCorrection.
154 (3) For each exposure, take the median correction
155 across the focal plane. Random variations will cancel
156 out, but systematic variations will not.
157 (4) Subtract this correction from each monitor diode
158 reading.
159 (5) Re-run the spline fit using the corrected monitor
160 diode readings.
161 """
162 # Initialize photodiodeCorrection.
163 photodiodeCorrection = PhotodiodeCorrection(log=self.log)
165 abscissaCorrections = {}
166 # Load all of the corrections, keyed by exposure pair
167 for (detector, linIndex, ptcIndex) in self.detectorList:
168 try:
169 thisLinearizer = inputLinearizer[linIndex]
170 thisPtc = inputPtc[ptcIndex]
171 except (RuntimeError, OSError):
172 continue
174 for amp in camera[detector].getAmplifiers():
175 ampName = amp.getName()
176 fluxResidual = thisLinearizer.fitResiduals[ampName]
177 linearSlope = thisLinearizer.linearFit[ampName]
178 if np.isnan(linearSlope[1]):
179 abscissaCorrection = np.zeros(len(fluxResidual))
180 elif linearSlope[1] < 1.0E-12:
181 abscissaCorrection = np.zeros(len(fluxResidual))
182 else:
183 abscissaCorrection = fluxResidual / linearSlope[1]
184 for i, pair in enumerate(thisPtc.inputExpIdPairs[ampName]):
185 key = str(pair[0])
186 try:
187 abscissaCorrections[key].append(abscissaCorrection[i])
188 except KeyError:
189 abscissaCorrections[key] = []
190 abscissaCorrections[key].append(abscissaCorrection[i])
191 # Now the correction is the median correction
192 # across the whole focal plane.
193 for key in abscissaCorrections.keys():
194 correction = np.nanmedian(abscissaCorrections[key])
195 if np.isnan(correction):
196 correction = 0.0
197 abscissaCorrections[key] = correction
198 photodiodeCorrection.abscissaCorrections = abscissaCorrections
200 photodiodeCorrection.validate()
201 photodiodeCorrection.updateMetadata(camera=camera, filterName='NONE')
202 photodiodeCorrection.updateMetadata(setDate=True, setCalibId=True)
203 provenance = IsrProvenance(calibType='photodiodeCorrection')
205 return pipeBase.Struct(
206 outputPhotodiodeCorrection=photodiodeCorrection,
207 outputProvenance=provenance,
208 )