27 import lsst.pipe.base.connectionTypes
as cT
30 from lsstDebug
import getDebugFrame
33 from .utils
import (funcPolynomial, irlsFit)
34 from ._lookupStaticCalibration
import lookupStaticCalibration
36 __all__ = [
"LinearitySolveTask",
"LinearitySolveConfig",
"MeasureLinearityTask"]
40 dimensions=(
"instrument",
"exposure",
"detector")):
43 doc=
"Dummy exposure.",
44 storageClass=
'Exposure',
45 dimensions=(
"instrument",
"exposure",
"detector"),
49 camera = cT.PrerequisiteInput(
51 doc=
"Camera Geometry definition.",
52 storageClass=
"Camera",
53 dimensions=(
"instrument", ),
55 lookupFunction=lookupStaticCalibration,
57 inputPtc = cT.PrerequisiteInput(
59 doc=
"Input PTC dataset.",
60 storageClass=
"PhotonTransferCurveDataset",
61 dimensions=(
"instrument",
"detector"),
65 outputLinearizer = cT.Output(
67 doc=
"Output linearity measurements.",
68 storageClass=
"Linearizer",
69 dimensions=(
"instrument",
"detector"),
75 pipelineConnections=LinearitySolveConnections):
76 """Configuration for solving the linearity from PTC dataset.
78 linearityType = pexConfig.ChoiceField(
80 doc=
"Type of linearizer to construct.",
83 "LookupTable":
"Create a lookup table solution.",
84 "Polynomial":
"Create an arbitrary polynomial solution.",
85 "Squared":
"Create a single order squared solution.",
86 "Spline":
"Create a spline based solution.",
87 "None":
"Create a dummy solution.",
90 polynomialOrder = pexConfig.Field(
92 doc=
"Degree of polynomial to fit.",
95 splineKnots = pexConfig.Field(
97 doc=
"Number of spline knots to use in fit.",
100 maxLookupTableAdu = pexConfig.Field(
102 doc=
"Maximum DN value for a LookupTable linearizer.",
105 maxLinearAdu = pexConfig.Field(
107 doc=
"Maximum DN value to use to estimate linear term.",
110 minLinearAdu = pexConfig.Field(
112 doc=
"Minimum DN value to use to estimate linear term.",
115 nSigmaClipLinear = pexConfig.Field(
117 doc=
"Maximum deviation from linear solution for Poissonian noise.",
120 ignorePtcMask = pexConfig.Field(
122 doc=
"Ignore the expIdMask set by the PTC solver?",
128 """Fit the linearity from the PTC dataset.
130 ConfigClass = LinearitySolveConfig
131 _DefaultName =
'cpLinearitySolve'
134 """Ensure that the input and output dimensions are passed along.
138 butlerQC : `lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
139 Butler to operate on.
140 inputRefs : `lsst.pipe.base.connections.InputQuantizedConnection`
141 Input data refs to load.
142 ouptutRefs : `lsst.pipe.base.connections.OutputQuantizedConnection`
143 Output data refs to persist.
145 inputs = butlerQC.get(inputRefs)
148 inputs[
'inputDims'] = inputRefs.inputPtc.dataId.byName()
150 outputs = self.
runrun(**inputs)
151 butlerQC.put(outputs, outputRefs)
153 def run(self, inputPtc, dummy, camera, inputDims):
154 """Fit non-linearity to PTC data, returning the correct Linearizer
159 inputPtc : `lsst.cp.pipe.PtcDataset`
160 Pre-measured PTC dataset.
161 dummy : `lsst.afw.image.Exposure`
162 The exposure used to select the appropriate PTC dataset.
163 In almost all circumstances, one of the input exposures
164 used to generate the PTC dataset is the best option.
165 camera : `lsst.afw.cameraGeom.Camera`
167 inputDims : `lsst.daf.butler.DataCoordinate` or `dict`
168 DataIds to use to populate the output calibration.
172 results : `lsst.pipe.base.Struct`
173 The results struct containing:
175 ``outputLinearizer`` : `lsst.ip.isr.Linearizer`
176 Final linearizer calibration.
177 ``outputProvenance`` : `lsst.ip.isr.IsrProvenance`
178 Provenance data for the new calibration.
182 This task currently fits only polynomial-defined corrections,
183 where the correction coefficients are defined such that:
184 corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
185 These `c_i` are defined in terms of the direct polynomial fit:
186 meanVector ~ P(x=timeVector) = sum_j k_j x^j
187 such that c_(j-2) = -k_j/(k_1^j) in units of DN^(1-j) (c.f.,
188 Eq. 37 of 2003.05978). The `config.polynomialOrder` or
189 `config.splineKnots` define the maximum order of x^j to fit.
190 As k_0 and k_1 are degenerate with bias level and gain, they
191 are not included in the non-linearity correction.
194 self.log.
warn(
"No dummy exposure found.")
196 detector = camera[inputDims[
'detector']]
197 if self.config.linearityType ==
'LookupTable':
198 table = np.zeros((len(detector), self.config.maxLookupTableAdu), dtype=np.float32)
204 if self.config.linearityType ==
'Spline':
205 fitOrder = self.config.splineKnots
207 fitOrder = self.config.polynomialOrder
210 linearizer =
Linearizer(detector=detector, table=table, log=self.log)
212 for i, amp
in enumerate(detector):
213 ampName = amp.getName()
214 if ampName
in inputPtc.badAmps:
217 if self.config.linearityType
in [
'Polynomial']:
218 nEntries = fitOrder + 1
219 pEntries = fitOrder + 1
220 elif self.config.linearityType
in [
'Spline']:
221 nEntries = fitOrder * 2
222 elif self.config.linearityType
in [
'Squared',
'None']:
224 pEntries = fitOrder + 1
225 elif self.config.linearityType
in [
'LookupTable']:
227 pEntries = fitOrder + 1
229 linearizer.linearityType[ampName] =
"None"
230 linearizer.linearityCoeffs[ampName] = np.zeros(nEntries)
231 linearizer.linearityBBox[ampName] = amp.getBBox()
232 linearizer.fitParams[ampName] = np.zeros(pEntries)
233 linearizer.fitParamsErr[ampName] = np.zeros(pEntries)
234 linearizer.fitChiSq[ampName] = np.nan
235 self.log.
warn(
"Amp %s has no usable PTC information. Skipping!", ampName)
238 if (len(inputPtc.expIdMask[ampName]) == 0)
or self.config.ignorePtcMask:
239 self.log.
warn(f
"Mask not found for {ampName} in non-linearity fit. Using all points.")
240 mask = np.repeat(
True, len(inputPtc.expIdMask[ampName]))
242 mask = np.array(inputPtc.expIdMask[ampName], dtype=bool)
244 inputAbscissa = np.array(inputPtc.rawExpTimes[ampName])[mask]
245 inputOrdinate = np.array(inputPtc.rawMeans[ampName])[mask]
248 fluxMask = inputOrdinate < self.config.maxLinearAdu
249 lowMask = inputOrdinate > self.config.minLinearAdu
250 fluxMask = fluxMask & lowMask
251 linearAbscissa = inputAbscissa[fluxMask]
252 linearOrdinate = inputOrdinate[fluxMask]
254 linearFit, linearFitErr, chiSq, weights =
irlsFit([0.0, 100.0], linearAbscissa,
255 linearOrdinate, funcPolynomial)
257 linearOrdinate = linearFit[0] + linearFit[1] * inputAbscissa
260 threshold = self.config.nSigmaClipLinear * np.sqrt(linearOrdinate)
261 fluxMask = np.abs(inputOrdinate - linearOrdinate) < threshold
262 linearOrdinate = linearOrdinate[fluxMask]
263 fitOrdinate = inputOrdinate[fluxMask]
264 self.
debugFitdebugFit(
'linearFit', inputAbscissa, inputOrdinate, linearOrdinate, fluxMask, ampName)
266 if self.config.linearityType
in [
'Polynomial',
'Squared',
'LookupTable']:
267 polyFit = np.zeros(fitOrder + 1)
269 polyFit, polyFitErr, chiSq, weights =
irlsFit(polyFit, linearOrdinate,
270 fitOrdinate, funcPolynomial)
274 linearityFit = [-coeff/(k1**order)
for order, coeff
in enumerate(polyFit)]
275 significant = np.where(np.abs(linearityFit) > 1e-10,
True,
False)
276 self.log.
info(f
"Significant polynomial fits: {significant}")
279 self.
debugFitdebugFit(
'polyFit', linearAbscissa, fitOrdinate, modelOrdinate,
None, ampName)
281 if self.config.linearityType ==
'Squared':
282 linearityFit = [linearityFit[2]]
283 elif self.config.linearityType ==
'LookupTable':
285 tMax = (self.config.maxLookupTableAdu - polyFit[0])/polyFit[1]
286 timeRange = np.linspace(0, tMax, self.config.maxLookupTableAdu)
287 signalIdeal = polyFit[0] + polyFit[1]*timeRange
289 lookupTableRow = signalIdeal - signalUncorrected
291 linearizer.tableData[tableIndex, :] = lookupTableRow
292 linearityFit = [tableIndex, 0]
294 elif self.config.linearityType
in [
'Spline']:
296 numPerBin, binEdges = np.histogram(linearOrdinate, bins=fitOrder)
297 with np.errstate(invalid=
"ignore"):
302 values = np.histogram(linearOrdinate, bins=fitOrder,
303 weights=(inputOrdinate[fluxMask] - linearOrdinate))[0]/numPerBin
315 binCenters = np.histogram(linearOrdinate, bins=fitOrder,
316 weights=linearOrdinate)[0]/numPerBin
317 values = values[numPerBin > 0]
318 binCenters = binCenters[numPerBin > 0]
320 self.
debugFitdebugFit(
'splineFit', binCenters, np.abs(values), values,
None, ampName)
323 modelOrdinate = linearOrdinate + interp.interpolate(linearOrdinate)
324 self.
debugFitdebugFit(
'splineFit', linearOrdinate, fitOrdinate, modelOrdinate,
None, ampName)
329 if len(binCenters) != fitOrder:
330 padN = fitOrder - len(binCenters)
331 binCenters = np.pad(binCenters, (padN, 0),
'linear_ramp',
332 end_values=(binCenters.min() - 1.0, ))
334 values = np.pad(values, (padN, 0))
337 linearityFit = np.concatenate((binCenters.tolist(), values.tolist())).tolist()
347 linearizer.linearityType[ampName] = self.config.linearityType
348 linearizer.linearityCoeffs[ampName] = np.array(linearityFit)
349 linearizer.linearityBBox[ampName] = amp.getBBox()
350 linearizer.fitParams[ampName] = np.array(polyFit)
351 linearizer.fitParamsErr[ampName] = np.array(polyFitErr)
352 linearizer.fitChiSq[ampName] = chiSq
354 image = afwImage.ImageF(len(inputOrdinate), 1)
355 image.getArray()[:, :] = inputOrdinate
356 linearizeFunction = linearizer.getLinearityTypeByName(linearizer.linearityType[ampName])
357 linearizeFunction()(image,
358 **{
'coeffs': linearizer.linearityCoeffs[ampName],
359 'table': linearizer.tableData,
360 'log': linearizer.log})
361 linearizeModel = image.getArray()[0, :]
363 self.
debugFitdebugFit(
'solution', inputOrdinate[fluxMask], linearOrdinate,
364 linearizeModel[fluxMask],
None, ampName)
366 linearizer.hasLinearity =
True
367 linearizer.validate()
368 linearizer.updateMetadata(camera=camera, detector=detector, filterName=
'NONE')
369 linearizer.updateMetadata(setDate=
True, setCalibId=
True)
372 return pipeBase.Struct(
373 outputLinearizer=linearizer,
374 outputProvenance=provenance,
377 def debugFit(self, stepname, xVector, yVector, yModel, mask, ampName):
378 """Debug method for linearity fitting.
383 A label to use to check if we care to debug at a given
385 xVector : `numpy.array`
386 The values to use as the independent variable in the
388 yVector : `numpy.array`
389 The values to use as the dependent variable in the
391 yModel : `numpy.array`
392 The values to use as the linearized result.
393 mask : `numpy.array` [ `bool` ], optional
394 A mask to indicate which entries of ``xVector`` and
397 Amplifier name to lookup linearity correction values.
402 import matplotlib.pyplot
as plt
403 fig, axs = plt.subplots(2)
406 mask = np.ones_like(xVector, dtype=bool)
408 fig.suptitle(f
"{stepname} {ampName} {self.config.linearityType}")
409 if stepname ==
'linearFit':
410 axs[0].set_xlabel(
"Input Abscissa (time or mondiode)")
411 axs[0].set_ylabel(
"Input Ordinate (flux)")
412 axs[1].set_xlabel(
"Linear Ordinate (linear flux)")
413 axs[1].set_ylabel(
"Flux Difference: (input - linear)")
414 elif stepname
in (
'polyFit',
'splineFit'):
415 axs[0].set_xlabel(
"Linear Abscissa (linear flux)")
416 axs[0].set_ylabel(
"Input Ordinate (flux)")
417 axs[1].set_xlabel(
"Linear Ordinate (linear flux)")
418 axs[1].set_ylabel(
"Flux Difference: (input - full model fit)")
419 elif stepname ==
'solution':
420 axs[0].set_xlabel(
"Input Abscissa (time or mondiode)")
421 axs[0].set_ylabel(
"Linear Ordinate (linear flux)")
422 axs[1].set_xlabel(
"Model flux (linear flux)")
423 axs[1].set_ylabel(
"Flux Difference: (linear - model)")
425 axs[0].set_yscale(
'log')
426 axs[0].set_xscale(
'log')
427 axs[0].scatter(xVector, yVector)
428 axs[0].scatter(xVector[~mask], yVector[~mask], c=
'red', marker=
'x')
429 axs[1].set_xscale(
'log')
431 axs[1].scatter(yModel, yVector[mask] - yModel)
434 prompt =
"Press Enter or c to continue [chpx]..."
436 ans = input(prompt).lower()
437 if ans
in (
"",
" ",
"c",):
443 print(
"[h]elp [c]ontinue [p]db")
450 solver = pexConfig.ConfigurableField(
451 target=LinearitySolveTask,
452 doc=
"Task to convert PTC data to linearity solutions.",
457 """Stand alone Gen2 linearity measurement.
459 This class wraps the Gen3 linearity task to allow it to be run as
462 ConfigClass = MeasureLinearityConfig
463 _DefaultName =
"measureLinearity"
467 self.makeSubtask(
"solver")
470 """Run new linearity code for gen2.
474 dataRef : `lsst.daf.persistence.ButlerDataRef`
475 Input dataref for the photon transfer curve data.
479 results : `lsst.pipe.base.Struct`
480 The results struct containing:
482 ``outputLinearizer`` : `lsst.ip.isr.Linearizer`
483 Final linearizer calibration.
484 ``outputProvenance`` : `lsst.ip.isr.IsrProvenance`
485 Provenance data for the new calibration.
487 ptc = dataRef.get(
'photonTransferCurveDataset')
488 camera = dataRef.get(
'camera')
489 inputDims = dataRef.dataId
490 linearityResults = self.solver.
run(ptc, camera=camera, inputDims=inputDims)
492 inputDims[
'calibDate'] = linearityResults.outputLinearizer.getMetadata().get(
'CALIBDATE')
493 butler = dataRef.getButler()
494 butler.put(linearityResults.outputLinearizer,
"linearizer", inputDims)
495 return linearityResults
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def run(self, inputPtc, dummy, camera, inputDims)
def debugFit(self, stepname, xVector, yVector, yModel, mask, ampName)
def __init__(self, **kwargs)
def runDataRef(self, dataRef)
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
Interpolate::Style stringToInterpStyle(std::string const &style)
Conversion function to switch a string to an Interpolate::Style.
std::shared_ptr< Interpolate > makeInterpolate(std::vector< double > const &x, std::vector< double > const &y, Interpolate::Style const style=Interpolate::AKIMA_SPLINE)
A factory function to make Interpolate objects.
def irlsFit(initialParams, dataX, dataY, function, weightsY=None, weightType='Cauchy')
def funcPolynomial(pars, x)
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)
def getDebugFrame(debugDisplay, name)