30 from lsstDebug
import getDebugFrame
33 from .utils
import (funcPolynomial, irlsFit)
36 __all__ = [
"LinearitySolveTask",
"LinearitySolveConfig",
"MeasureLinearityTask"]
40 dimensions=(
"instrument",
"detector")):
43 doc=
"Input PTC dataset.",
44 storageClass=
"StructuredDataDict",
45 dimensions=(
"instrument",
"detector"),
50 doc=
"Camera Geometry definition.",
51 storageClass=
"Camera",
52 dimensions=(
"instrument", ),
54 outputLinearizer = cT.Output(
56 doc=
"Output linearity measurements.",
57 storageClass=
"Linearizer",
58 dimensions=(
"instrument",
"detector"),
64 pipelineConnections=LinearitySolveConnections):
65 """Configuration for solving the linearity from PTC dataset.
67 linearityType = pexConfig.ChoiceField(
69 doc=
"Type of linearizer to construct.",
72 "LookupTable":
"Create a lookup table solution.",
73 "Polynomial":
"Create an arbitrary polynomial solution.",
74 "Squared":
"Create a single order squared solution.",
75 "Spline":
"Create a spline based solution.",
76 "None":
"Create a dummy solution.",
79 polynomialOrder = pexConfig.Field(
81 doc=
"Degree of polynomial to fit.",
84 splineKnots = pexConfig.Field(
86 doc=
"Number of spline knots to use in fit.",
89 maxLookupTableAdu = pexConfig.Field(
91 doc=
"Maximum DN value for a LookupTable linearizer.",
94 maxLinearAdu = pexConfig.Field(
96 doc=
"Maximum DN value to use to estimate linear term.",
99 minLinearAdu = pexConfig.Field(
101 doc=
"Minimum DN value to use to estimate linear term.",
104 nSigmaClipLinear = pexConfig.Field(
106 doc=
"Maximum deviation from linear solution for Poissonian noise.",
112 """Fit the linearity from the PTC dataset.
114 ConfigClass = LinearitySolveConfig
115 _DefaultName =
'cpLinearitySolve'
118 """Ensure that the input and output dimensions are passed along.
122 butlerQC : `lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
123 Butler to operate on.
124 inputRefs : `lsst.pipe.base.connections.InputQuantizedConnection`
125 Input data refs to load.
126 ouptutRefs : `lsst.pipe.base.connections.OutputQuantizedConnection`
127 Output data refs to persist.
129 inputs = butlerQC.get(inputRefs)
132 inputs[
'inputDims'] = [exp.dataId.byName()
for exp
in inputRefs.inputPtc]
134 outputs = self.
run(**inputs)
135 butlerQC.put(outputs, outputRefs)
137 def run(self, inputPtc, camera, inputDims):
138 """Fit non-linearity to PTC data, returning the correct Linearizer
143 inputPtc : `lsst.cp.pipe.PtcDataset`
144 Pre-measured PTC dataset.
145 camera : `lsst.afw.cameraGeom.Camera`
147 inputDims : `lsst.daf.butler.DataCoordinate` or `dict`
148 DataIds to use to populate the output calibration.
152 results : `lsst.pipe.base.Struct`
153 The results struct containing:
155 ``outputLinearizer`` : `lsst.ip.isr.Linearizer`
156 Final linearizer calibration.
157 ``outputProvenance`` : `lsst.ip.isr.IsrProvenance`
158 Provenance data for the new calibration.
162 This task currently fits only polynomial-defined corrections,
163 where the correction coefficients are defined such that:
164 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
165 These `c_i` are defined in terms of the direct polynomial fit:
166 meanVector ~ P(x=timeVector) = sum_j k_j x^j
167 such that c_(j-2) = -k_j/(k_1^j) in units of DN^(1-j) (c.f.,
168 Eq. 37 of 2003.05978). The `config.polynomialOrder` or
169 `config.splineKnots` define the maximum order of x^j to fit.
170 As k_0 and k_1 are degenerate with bias level and gain, they
171 are not included in the non-linearity correction.
173 detector = camera[inputDims[
'detector']]
175 if self.config.linearityType ==
'LookupTable':
176 table = np.zeros((len(detector), self.config.maxLookupTableAdu), dtype=np.float32)
182 if self.config.linearityType ==
'Spline':
183 fitOrder = self.config.splineKnots
185 fitOrder = self.config.polynomialOrder
188 linearizer =
Linearizer(detector=detector, table=table, log=self.log)
190 for i, amp
in enumerate(detector):
191 ampName = amp.getName()
192 if (len(inputPtc.expIdMask[ampName]) == 0):
193 self.log.
warn(f
"Mask not found for {ampName} in non-linearity fit. Using all points.")
194 mask = np.repeat(
True, len(inputPtc.expIdMask[ampName]))
196 mask = inputPtc.expIdMask[ampName]
198 inputAbscissa = np.array(inputPtc.rawExpTimes[ampName])[mask]
199 inputOrdinate = np.array(inputPtc.rawMeans[ampName])[mask]
202 fluxMask = inputOrdinate < self.config.maxLinearAdu
203 lowMask = inputOrdinate > self.config.minLinearAdu
204 fluxMask = fluxMask & lowMask
205 linearAbscissa = inputAbscissa[fluxMask]
206 linearOrdinate = inputOrdinate[fluxMask]
208 linearFit, linearFitErr, chiSq, weights =
irlsFit([0.0, 100.0], linearAbscissa,
209 linearOrdinate, funcPolynomial)
211 linearOrdinate = linearFit[0] + linearFit[1] * inputAbscissa
214 threshold = self.config.nSigmaClipLinear * np.sqrt(linearOrdinate)
215 fluxMask = np.abs(inputOrdinate - linearOrdinate) < threshold
216 linearOrdinate = linearOrdinate[fluxMask]
217 fitOrdinate = inputOrdinate[fluxMask]
218 self.
debugFit(
'linearFit', inputAbscissa, inputOrdinate, linearOrdinate, fluxMask, ampName)
221 if self.config.linearityType
in [
'Polynomial',
'Squared',
'LookupTable']:
222 polyFit = np.zeros(fitOrder + 1)
224 polyFit, polyFitErr, chiSq, weights =
irlsFit(polyFit, linearOrdinate,
225 fitOrdinate, funcPolynomial)
229 linearityFit = [-coeff/(k1**order)
for order, coeff
in enumerate(polyFit)]
230 significant = np.where(np.abs(linearityFit) > 1e-10,
True,
False)
231 self.log.
info(f
"Significant polynomial fits: {significant}")
234 self.
debugFit(
'polyFit', linearAbscissa, fitOrdinate, modelOrdinate,
None, ampName)
236 if self.config.linearityType ==
'Squared':
237 linearityFit = [linearityFit[2]]
238 elif self.config.linearityType ==
'LookupTable':
240 tMax = (self.config.maxLookupTableAdu - polyFit[0])/polyFit[1]
241 timeRange = np.linspace(0, tMax, self.config.maxLookupTableAdu)
242 signalIdeal = polyFit[0] + polyFit[1]*timeRange
244 lookupTableRow = signalIdeal - signalUncorrected
246 linearizer.tableData[tableIndex, :] = lookupTableRow
247 linearityFit = [tableIndex, 0]
249 elif self.config.linearityType
in [
'Spline']:
251 numPerBin, binEdges = np.histogram(linearOrdinate, bins=fitOrder)
252 with np.errstate(invalid=
"ignore"):
257 values = np.histogram(linearOrdinate, bins=fitOrder,
258 weights=(inputOrdinate[fluxMask] - linearOrdinate))[0]/numPerBin
270 binCenters = np.histogram(linearOrdinate, bins=fitOrder,
271 weights=linearOrdinate)[0]/numPerBin
272 values = values[numPerBin > 0]
273 binCenters = binCenters[numPerBin > 0]
275 self.
debugFit(
'splineFit', binCenters, np.abs(values), values,
None, ampName)
278 modelOrdinate = linearOrdinate + interp.interpolate(linearOrdinate)
279 self.
debugFit(
'splineFit', linearOrdinate, fitOrdinate, modelOrdinate,
None, ampName)
284 if len(binCenters) != fitOrder:
285 padN = fitOrder - len(binCenters)
286 binCenters = np.pad(binCenters, (padN, 0),
'linear_ramp',
287 end_values=(binCenters.min() - 1.0, ))
289 values = np.pad(values, (padN, 0))
292 linearityFit = np.concatenate((binCenters.tolist(), values.tolist())).tolist()
302 linearizer.linearityType[ampName] = self.config.linearityType
303 linearizer.linearityCoeffs[ampName] = np.array(linearityFit)
304 linearizer.linearityBBox[ampName] = amp.getBBox()
305 linearizer.fitParams[ampName] = np.array(polyFit)
306 linearizer.fitParamsErr[ampName] = np.array(polyFitErr)
307 linearizer.fitChiSq[ampName] = chiSq
309 image = afwImage.ImageF(len(inputOrdinate), 1)
310 image.getArray()[:, :] = inputOrdinate
311 linearizeFunction = linearizer.getLinearityTypeByName(linearizer.linearityType[ampName])
312 linearizeFunction()(image,
313 **{
'coeffs': linearizer.linearityCoeffs[ampName],
314 'table': linearizer.tableData,
315 'log': linearizer.log})
316 linearizeModel = image.getArray()[0, :]
318 self.
debugFit(
'solution', inputOrdinate[fluxMask], linearOrdinate,
319 linearizeModel[fluxMask],
None, ampName)
321 linearizer.hasLinearity =
True
322 linearizer.validate()
323 linearizer.updateMetadata(camera=camera, detector=detector, filterName=
'NONE')
324 linearizer.updateMetadata(setDate=
True, setCalibId=
True)
327 return pipeBase.Struct(
328 outputLinearizer=linearizer,
329 outputProvenance=provenance,
332 def debugFit(self, stepname, xVector, yVector, yModel, mask, ampName):
333 """Debug method for linearity fitting.
338 A label to use to check if we care to debug at a given
340 xVector : `numpy.array`
341 The values to use as the independent variable in the
343 yVector : `numpy.array`
344 The values to use as the dependent variable in the
346 yModel : `numpy.array`
347 The values to use as the linearized result.
348 mask : `numpy.array` [ `bool` ], optional
349 A mask to indicate which entries of ``xVector`` and
352 Amplifier name to lookup linearity correction values.
357 import matplotlib.pyplot
as plt
358 fig, axs = plt.subplots(2)
361 mask = np.ones_like(xVector, dtype=bool)
363 fig.suptitle(f
"{stepname} {ampName} {self.config.linearityType}")
364 if stepname ==
'linearFit':
365 axs[0].set_xlabel(
"Input Abscissa (time or mondiode)")
366 axs[0].set_ylabel(
"Input Ordinate (flux)")
367 axs[1].set_xlabel(
"Linear Ordinate (linear flux)")
368 axs[1].set_ylabel(
"Flux Difference: (input - linear)")
369 elif stepname
in (
'polyFit',
'splineFit'):
370 axs[0].set_xlabel(
"Linear Abscissa (linear flux)")
371 axs[0].set_ylabel(
"Input Ordinate (flux)")
372 axs[1].set_xlabel(
"Linear Ordinate (linear flux)")
373 axs[1].set_ylabel(
"Flux Difference: (input - full model fit)")
374 elif stepname ==
'solution':
375 axs[0].set_xlabel(
"Input Abscissa (time or mondiode)")
376 axs[0].set_ylabel(
"Linear Ordinate (linear flux)")
377 axs[1].set_xlabel(
"Model flux (linear flux)")
378 axs[1].set_ylabel(
"Flux Difference: (linear - model)")
380 axs[0].set_yscale(
'log')
381 axs[0].set_xscale(
'log')
382 axs[0].scatter(xVector, yVector)
383 axs[0].scatter(xVector[~mask], yVector[~mask], c=
'red', marker=
'x')
384 axs[1].set_xscale(
'log')
386 axs[1].scatter(yModel, yVector[mask] - yModel)
389 prompt =
"Press Enter or c to continue [chpx]..."
391 ans = input(prompt).lower()
392 if ans
in (
"",
" ",
"c",):
398 print(
"[h]elp [c]ontinue [p]db")
405 solver = pexConfig.ConfigurableField(
406 target=LinearitySolveTask,
407 doc=
"Task to convert PTC data to linearity solutions.",
412 """Stand alone Gen2 linearity measurement.
414 This class wraps the Gen3 linearity task to allow it to be run as
417 ConfigClass = MeasureLinearityConfig
418 _DefaultName =
"measureLinearity"
422 self.makeSubtask(
"solver")
425 """Run new linearity code for gen2.
429 dataRef : `lsst.daf.persistence.ButlerDataRef`
430 Input dataref for the photon transfer curve data.
434 results : `lsst.pipe.base.Struct`
435 The results struct containing:
437 ``outputLinearizer`` : `lsst.ip.isr.Linearizer`
438 Final linearizer calibration.
439 ``outputProvenance`` : `lsst.ip.isr.IsrProvenance`
440 Provenance data for the new calibration.
442 ptc = dataRef.get(
'photonTransferCurveDataset')
443 camera = dataRef.get(
'camera')
444 inputDims = dataRef.dataId
445 linearityResults = self.solver.
run(ptc, camera=camera, inputDims=inputDims)
447 inputDims[
'calibDate'] = linearityResults.outputLinearizer.getMetadata().get(
'CALIBDATE')
448 butler = dataRef.getButler()
449 butler.put(linearityResults.outputLinearizer,
"linearizer", inputDims)
450 return linearityResults