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

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
24import lsst.pipe.base as pipeBase
25import lsst.pipe.base.connectionTypes as cT
26import lsst.pex.config as pexConfig
28from lsstDebug import getDebugFrame
29from lsst.ip.isr import (Linearizer, IsrProvenance)
31from .utils import (fitLeastSq, funcPolynomial)
34__all__ = ["LinearitySolveTask", "LinearitySolveConfig"]
37class LinearitySolveConnections(pipeBase.PipelineTaskConnections,
38 dimensions=("instrument", "detector")):
40 inputPtc = cT.Input(
41 name="inputPtc",
42 doc="Input PTC dataset.",
43 storageClass="StructuredDataDict",
44 dimensions=("instrument", "detector"),
45 multiple=False,
46 )
47 camera = cT.Input(
48 name="camera",
49 doc="Camera Geometry definition.",
50 storageClass="Camera",
51 dimensions=("instrument", ),
52 )
53 outputLinearizer = cT.Output(
54 name="linearity",
55 doc="Output linearity measurements.",
56 storageClass="Linearizer",
57 dimensions=("instrument", "detector"),
58 isCalibration=True,
59 )
62class LinearitySolveConfig(pipeBase.PipelineTaskConfig,
63 pipelineConnections=LinearitySolveConnections):
64 """Configuration for solving the linearity from PTC dataset.
65 """
66 linearityType = pexConfig.ChoiceField(
67 dtype=str,
68 doc="Type of linearizer to construct.",
69 default="Polynomial",
70 allowed={
71 "LookupTable": "Create a lookup table solution.",
72 "Polynomial": "Create an arbitrary polynomial solution.",
73 "Squared": "Create a single order squared solution.",
74 "None": "Create a dummy solution.",
75 }
76 )
77 polynomialOrder = pexConfig.Field(
78 dtype=int,
79 doc="Degree of polynomial to fit.",
80 default=3,
81 )
82 maxLookupTableAdu = pexConfig.Field(
83 dtype=int,
84 doc="Maximum DN value for a LookupTable linearizer.",
85 default=2**18,
86 )
89class LinearitySolveTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
90 """Fit the linearity from the PTC dataset.
91 """
92 ConfigClass = LinearitySolveConfig
93 _DefaultName = 'cpLinearitySolve'
95 def runQuantum(self, butlerQC, inputRefs, outputRefs):
96 """Ensure that the input and output dimensions are passed along.
98 Parameters
99 ----------
100 butlerQC : `lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
101 Butler to operate on.
102 inputRefs : `lsst.pipe.base.connections.InputQuantizedConnection`
103 Input data refs to load.
104 ouptutRefs : `lsst.pipe.base.connections.OutputQuantizedConnection`
105 Output data refs to persist.
106 """
107 inputs = butlerQC.get(inputRefs)
109 # Use the dimensions to set calib/provenance information.
110 inputs['inputDims'] = [exp.dataId.byName() for exp in inputRefs.inputPtc]
112 outputs = self.run(**inputs)
113 butlerQC.put(outputs, outputRefs)
115 def run(self, inputPtc, camera=None, inputDims=None):
116 """Fit non-linearity to PTC data, returning the correct Linearizer
117 object.
119 Parameters
120 ----------
121 inputPtc : `lsst.cp.pipe.PtcDataset`
122 Pre-measured PTC dataset.
123 camera : `lsst.afw.cameraGeom.Camera`, optional
124 Camera geometry.
125 inputDims : `lsst.daf.butler.DataCoordinate` or `dict`, optional
126 DataIds to use to populate the output calibration.
128 Returns
129 -------
130 results : `lsst.pipe.base.Struct`
131 The results struct containing:
133 ``outputLinearizer`` : `lsst.ip.isr.Linearizer`
134 Final linearizer calibration.
135 ``outputProvenance`` : `lsst.ip.isr.IsrProvenance`
136 Provenance data for the new calibration.
138 Notes
139 -----
140 This task currently fits only polynomial-defined corrections,
141 where the correction coefficients are defined such that:
142 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
143 These `c_i` are defined in terms of the direct polynomial fit:
144 meanVector ~ P(x=timeVector) = sum_j k_j x^j
145 such that c_(j-2) = -k_j/(k_1^j) in units of DN^(1-j) (c.f.,
146 Eq. 37 of 2003.05978). The `config.polynomialOrder` defines
147 the maximum order of x^j to fit. As k_0 and k_1 are
148 degenerate with bias level and gain, they are not included in
149 the non-linearity correction.
151 """
152 if camera:
153 detector = camera[inputDims['detector']]
155 if self.config.linearityType == 'LookupTable':
156 table = np.zeros((len(detector), self.config.maxLookupTableAdu), dtype=np.float32)
157 tableIndex = 0
158 else:
159 table = None
160 tableIndex = None # This will fail if we increment it.
162 # Initialize the linearizer.
163 linearizer = Linearizer(detector=detector, table=table, log=self.log)
165 for i, amp in enumerate(detector):
166 ampName = amp.getName()
167 if (len(inputPtc.expIdMask[ampName]) == 0):
168 self.log.warn(f"Mask not found for {ampName} in non-linearity fit. Using all points.")
169 mask = np.repeat(True, len(inputPtc.rawExpTimes[ampName]))
170 else:
171 mask = inputPtc.expIdMask[ampName]
173 timeVector = np.array(inputPtc.rawExpTimes[ampName])[mask]
174 meanVector = np.array(inputPtc.rawMeans[ampName])[mask]
176 if self.config.linearityType in ['Polynomial', 'Squared', 'LookupTable']:
177 polyFit = np.zeros(self.config.polynomialOrder + 1)
178 polyFit[1] = 1.0
179 polyFit, polyFitErr, chiSq = fitLeastSq(polyFit, timeVector, meanVector, funcPolynomial)
181 # Truncate the polynomial fit
182 k1 = polyFit[1]
183 linearityFit = [-coeff/(k1**order) for order, coeff in enumerate(polyFit)]
184 significant = np.where(np.abs(linearityFit) > 1e-10, True, False)
185 self.log.info(f"Significant polynomial fits: {significant}")
187 if self.config.linearityType == 'Squared':
188 linearityFit = [linearityFit[2]]
189 elif self.config.linearityType == 'LookupTable':
190 # Use linear part to get time at wich signal is maxAduForLookupTableLinearizer DN
191 tMax = (self.config.maxLookupTableAdu - polyFit[0])/polyFit[1]
192 timeRange = np.linspace(0, tMax, self.config.maxLookupTableAdu)
193 signalIdeal = polyFit[0] + polyFit[1]*timeRange
194 signalUncorrected = funcPolynomial(polyFit, timeRange)
195 lookupTableRow = signalIdeal - signalUncorrected # LinearizerLookupTable has corrections
197 linearizer.tableData[tableIndex, :] = lookupTableRow
198 linearityFit = [tableIndex, 0]
199 tableIndex += 1
200 else:
201 polyFit = [0.0]
202 polyFitErr = [0.0]
203 chiSq = np.nan
204 linearityFit = [0.0]
206 linearizer.linearityType[ampName] = 'self.config.linearityType'
207 linearizer.linearityCoeffs[ampName] = linearityFit
208 linearizer.linearityBBox[ampName] = amp.getBBox()
209 linearizer.fitParams[ampName] = polyFit
210 linearizer.fitParamsErr[ampName] = polyFitErr
211 linearizer.fitChiSq[ampName] = chiSq
212 self.debugFit('solution', timeVector, meanVector, linearizer, ampName)
214 linearizer.validate()
215 linearizer.updateMetadata(setDate=True)
216 provenance = IsrProvenance(calibType='linearizer')
218 return pipeBase.Struct(
219 outputLinearizer=linearizer,
220 outputProvenance=provenance,
221 )
223 def debugFit(self, stepname, timeVector, meanVector, linearizer, ampName):
224 """Debug method for linearity fitting.
226 Parameters
227 ----------
228 stepname : `str`
229 A label to use to check if we care to debug at a given
230 line of code.
231 timeVector : `numpy.array`
232 The values to use as the independent variable in the
233 linearity fit.
234 meanVector : `numpy.array`
235 The values to use as the dependent variable in the
236 linearity fit.
237 linearizer : `lsst.ip.isr.Linearizer`
238 The linearity correction to compare.
239 ampName : `str`
240 Amplifier name to lookup linearity correction values.
241 """
242 frame = getDebugFrame(self._display, stepname)
243 if frame:
244 import matplotlib.pyplot as plot
245 figure = plot.figure(1)
246 figure.clear()
248 axes = figure.add_axes((min(timeVector), min(meanVector),
249 max(timeVector), max(meanVector)))
250 axes.plot(timeVector, meanVector, 'k+')
252 axes.plot(timeVector,
253 np.polynomial.polynomial.polyval(linearizer.fitParams[ampName],
254 timeVector), 'r')
255 plot.xlabel("Exposure Time")
256 plot.ylabel("Mean Flux")
257 plot.title(f"Linearity {ampName} {linearizer.linearityType[ampName]}"
258 f" chi={linearizer.fitChiSq[ampName]}")
259 figure.show()
261 prompt = "Press enter to continue: "
262 while True:
263 ans = input(prompt).lower()
264 if ans in ("", "c",):
265 break
266 plot.close()