Coverage for python/lsst/cp/pipe/linearity.py: 15%
272 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-28 00:49 -0700
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-28 00:49 -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
23import lsst.afw.image as afwImage
24import lsst.afw.math as afwMath
25import lsst.pipe.base as pipeBase
26import lsst.pipe.base.connectionTypes as cT
27import lsst.pex.config as pexConfig
29from lsstDebug import getDebugFrame
30from lsst.ip.isr import (Linearizer, IsrProvenance, PhotodiodeCalib)
32from .utils import (funcPolynomial, irlsFit)
33from ._lookupStaticCalibration import lookupStaticCalibration
35__all__ = ["LinearitySolveTask", "LinearitySolveConfig", "MeasureLinearityTask"]
38class LinearitySolveConnections(pipeBase.PipelineTaskConnections,
39 dimensions=("instrument", "exposure", "detector")):
40 dummy = cT.Input(
41 name="raw",
42 doc="Dummy exposure.",
43 storageClass='Exposure',
44 dimensions=("instrument", "exposure", "detector"),
45 multiple=True,
46 deferLoad=True,
47 )
49 camera = cT.PrerequisiteInput(
50 name="camera",
51 doc="Camera Geometry definition.",
52 storageClass="Camera",
53 dimensions=("instrument", ),
54 isCalibration=True,
55 lookupFunction=lookupStaticCalibration,
56 )
58 inputPtc = cT.PrerequisiteInput(
59 name="ptc",
60 doc="Input PTC dataset.",
61 storageClass="PhotonTransferCurveDataset",
62 dimensions=("instrument", "detector"),
63 isCalibration=True,
64 )
66 inputPhotodiodeCorrection = cT.Input(
67 name="pdCorrection",
68 doc="Input photodiode correction.",
69 storageClass="IsrCalib",
70 dimensions=("instrument", ),
71 isCalibration=True,
72 )
74 outputLinearizer = cT.Output(
75 name="linearity",
76 doc="Output linearity measurements.",
77 storageClass="Linearizer",
78 dimensions=("instrument", "detector"),
79 isCalibration=True,
80 )
82 def __init__(self, *, config=None):
83 super().__init__(config=config)
85 if config.applyPhotodiodeCorrection is not True:
86 self.inputs.discard("inputPhotodiodeCorrection")
89class LinearitySolveConfig(pipeBase.PipelineTaskConfig,
90 pipelineConnections=LinearitySolveConnections):
91 """Configuration for solving the linearity from PTC dataset.
92 """
93 linearityType = pexConfig.ChoiceField(
94 dtype=str,
95 doc="Type of linearizer to construct.",
96 default="Squared",
97 allowed={
98 "LookupTable": "Create a lookup table solution.",
99 "Polynomial": "Create an arbitrary polynomial solution.",
100 "Squared": "Create a single order squared solution.",
101 "Spline": "Create a spline based solution.",
102 "None": "Create a dummy solution.",
103 }
104 )
105 polynomialOrder = pexConfig.Field(
106 dtype=int,
107 doc="Degree of polynomial to fit.",
108 default=3,
109 )
110 splineKnots = pexConfig.Field(
111 dtype=int,
112 doc="Number of spline knots to use in fit.",
113 default=10,
114 )
115 maxLookupTableAdu = pexConfig.Field(
116 dtype=int,
117 doc="Maximum DN value for a LookupTable linearizer.",
118 default=2**18,
119 )
120 maxLinearAdu = pexConfig.Field(
121 dtype=float,
122 doc="Maximum DN value to use to estimate linear term.",
123 default=20000.0,
124 )
125 minLinearAdu = pexConfig.Field(
126 dtype=float,
127 doc="Minimum DN value to use to estimate linear term.",
128 default=2000.0,
129 )
130 nSigmaClipLinear = pexConfig.Field(
131 dtype=float,
132 doc="Maximum deviation from linear solution for Poissonian noise.",
133 default=5.0,
134 )
135 ignorePtcMask = pexConfig.Field(
136 dtype=bool,
137 doc="Ignore the expIdMask set by the PTC solver?",
138 default=False,
139 )
140 usePhotodiode = pexConfig.Field(
141 dtype=bool,
142 doc="Use the photodiode info instead of the raw expTimes?",
143 default=False,
144 )
145 applyPhotodiodeCorrection = pexConfig.Field(
146 dtype=bool,
147 doc="Calculate and apply a correction to the photodiode readings?",
148 default=False,
149 )
152class LinearitySolveTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
153 """Fit the linearity from the PTC dataset.
154 """
156 ConfigClass = LinearitySolveConfig
157 _DefaultName = 'cpLinearitySolve'
159 def runQuantum(self, butlerQC, inputRefs, outputRefs):
160 """Ensure that the input and output dimensions are passed along.
162 Parameters
163 ----------
164 butlerQC : `lsst.daf.butler.butlerQuantumContext.ButlerQuantumContext`
165 Butler to operate on.
166 inputRefs : `lsst.pipe.base.connections.InputQuantizedConnection`
167 Input data refs to load.
168 ouptutRefs : `lsst.pipe.base.connections.OutputQuantizedConnection`
169 Output data refs to persist.
170 """
171 inputs = butlerQC.get(inputRefs)
173 # Use the dimensions to set calib/provenance information.
174 inputs['inputDims'] = inputRefs.inputPtc.dataId.byName()
176 outputs = self.run(**inputs)
177 butlerQC.put(outputs, outputRefs)
179 def run(self, inputPtc, dummy, camera, inputDims, inputPhotodiodeCorrection=None):
180 """Fit non-linearity to PTC data, returning the correct Linearizer
181 object.
183 Parameters
184 ----------
185 inputPtc : `lsst.ip.isr.PtcDataset`
186 Pre-measured PTC dataset.
187 inputPhotodiodeCorrection : `lsst.ip.isr.PhotodiodeCorrection`
188 Pre-measured photodiode correction used in the case when
189 applyPhotodiodeCorrection=True.
190 dummy : `lsst.afw.image.Exposure`
191 The exposure used to select the appropriate PTC dataset.
192 In almost all circumstances, one of the input exposures
193 used to generate the PTC dataset is the best option.
194 camera : `lsst.afw.cameraGeom.Camera`
195 Camera geometry.
196 inputDims : `lsst.daf.butler.DataCoordinate` or `dict`
197 DataIds to use to populate the output calibration.
199 Returns
200 -------
201 results : `lsst.pipe.base.Struct`
202 The results struct containing:
204 ``outputLinearizer``
205 Final linearizer calibration (`lsst.ip.isr.Linearizer`).
206 ``outputProvenance``
207 Provenance data for the new calibration
208 (`lsst.ip.isr.IsrProvenance`).
210 Notes
211 -----
212 This task currently fits only polynomial-defined corrections,
213 where the correction coefficients are defined such that:
214 :math:`corrImage = uncorrImage + \\sum_i c_i uncorrImage^(2 + i)`
215 These :math:`c_i` are defined in terms of the direct polynomial fit:
216 :math:`meanVector ~ P(x=timeVector) = \\sum_j k_j x^j`
217 such that :math:`c_(j-2) = -k_j/(k_1^j)` in units of DN^(1-j) (c.f.,
218 Eq. 37 of 2003.05978). The `config.polynomialOrder` or
219 `config.splineKnots` define the maximum order of :math:`x^j` to fit.
220 As :math:`k_0` and :math:`k_1` are degenerate with bias level and gain,
221 they are not included in the non-linearity correction.
222 """
223 if len(dummy) == 0:
224 self.log.warning("No dummy exposure found.")
226 detector = camera[inputDims['detector']]
227 if self.config.linearityType == 'LookupTable':
228 table = np.zeros((len(detector), self.config.maxLookupTableAdu), dtype=np.float32)
229 tableIndex = 0
230 else:
231 table = None
232 tableIndex = None # This will fail if we increment it.
234 if self.config.linearityType == 'Spline':
235 fitOrder = self.config.splineKnots
236 else:
237 fitOrder = self.config.polynomialOrder
239 # Initialize the linearizer.
240 linearizer = Linearizer(detector=detector, table=table, log=self.log)
242 if self.config.usePhotodiode and self.config.applyPhotodiodeCorrection:
243 abscissaCorrections = inputPhotodiodeCorrection.abscissaCorrections
245 for i, amp in enumerate(detector):
246 ampName = amp.getName()
247 if ampName in inputPtc.badAmps:
248 linearizer = self.fillBadAmp(linearizer, fitOrder, inputPtc, amp)
249 self.log.warning("Amp %s in detector %s has no usable PTC information. Skipping!",
250 ampName, detector.getName())
251 continue
253 if (len(inputPtc.expIdMask[ampName]) == 0) or self.config.ignorePtcMask:
254 self.log.warning("Mask not found for %s in detector %s in fit. Using all points.",
255 ampName, detector.getName())
256 mask = np.repeat(True, len(inputPtc.expIdMask[ampName]))
257 else:
258 mask = np.array(inputPtc.expIdMask[ampName], dtype=bool)
260 if self.config.usePhotodiode:
261 # Here's where we bring in the photodiode data
262 # TODO: DM-33585. Replace when pd data is ingested.
263 DATA_DIR = '/lsstdata/offline/teststand/BOT/storage/'
264 modExpTimes = []
265 for i, pair in enumerate(inputPtc.inputExpIdPairs[ampName]):
266 pair = pair[0]
267 modExpTime = 0.0
268 nExps = 0
269 for j in range(2):
270 expId = pair[j]
271 try:
272 date = int(expId/100000)
273 seq = expId - date * 100000
274 date = date - 10000000
275 filename = DATA_DIR + \
276 '%d/MC_C_%d_%06d/Photodiode_Readings_%d_%06d.txt'\
277 % (date, date, seq, date, seq)
278 photodiodeCalib = PhotodiodeCalib.readTwoColumnPhotodiodeData(filename)
279 photodiodeCalib.integrationMethod = 'TRIMMED_SUM'
280 monDiodeCharge = photodiodeCalib.integrate()
281 modExpTime += monDiodeCharge
282 nExps += 1
283 except (RuntimeError, OSError):
284 continue
285 if nExps > 0:
286 # The 5E8 factor bring the modExpTimes back to about
287 # the same order as the expTimes.
288 # Probably a better way to do this.
289 modExpTime = 5.0E8 * modExpTime / nExps
290 else:
291 mask[i] = False
293 # Get the photodiode correction
294 if self.config.applyPhotodiodeCorrection:
295 try:
296 correction = abscissaCorrections[str(pair)]
297 except KeyError:
298 correction = 0.0
299 else:
300 correction = 0.0
301 modExpTimes.append(modExpTime + correction)
303 inputAbscissa = np.array(modExpTimes)[mask]
304 else:
305 inputAbscissa = np.array(inputPtc.rawExpTimes[ampName])[mask]
307 inputOrdinate = np.array(inputPtc.rawMeans[ampName])[mask]
308 # Determine proxy-to-linear-flux transformation
309 fluxMask = inputOrdinate < self.config.maxLinearAdu
310 lowMask = inputOrdinate > self.config.minLinearAdu
311 fluxMask = fluxMask & lowMask
312 linearAbscissa = inputAbscissa[fluxMask]
313 linearOrdinate = inputOrdinate[fluxMask]
314 if len(linearAbscissa) < 2:
315 linearizer = self.fillBadAmp(linearizer, fitOrder, inputPtc, amp)
316 self.log.warning("Amp %s in detector %s has not enough points for linear fit. Skipping!",
317 ampName, detector.getName())
318 continue
320 linearFit, linearFitErr, chiSq, weights = irlsFit([0.0, 100.0], linearAbscissa,
321 linearOrdinate, funcPolynomial)
322 # Convert this proxy-to-flux fit into an expected linear flux
323 linearOrdinate = linearFit[0] + linearFit[1] * inputAbscissa
324 # Exclude low end outliers
325 threshold = self.config.nSigmaClipLinear * np.sqrt(abs(linearOrdinate))
326 fluxMask = np.abs(inputOrdinate - linearOrdinate) < threshold
327 linearOrdinate = linearOrdinate[fluxMask]
328 fitOrdinate = inputOrdinate[fluxMask]
329 fitAbscissa = inputAbscissa[fluxMask]
330 if len(linearOrdinate) < 2:
331 linearizer = self.fillBadAmp(linearizer, fitOrder, inputPtc, amp)
332 self.log.warning("Amp %s in detector %s has not enough points in linear ordinate. Skipping!",
333 ampName, detector.getName())
334 continue
336 self.debugFit('linearFit', inputAbscissa, inputOrdinate, linearOrdinate, fluxMask, ampName)
337 # Do fits
338 if self.config.linearityType in ['Polynomial', 'Squared', 'LookupTable']:
339 polyFit = np.zeros(fitOrder + 1)
340 polyFit[1] = 1.0
341 polyFit, polyFitErr, chiSq, weights = irlsFit(polyFit, linearOrdinate,
342 fitOrdinate, funcPolynomial)
344 # Truncate the polynomial fit
345 k1 = polyFit[1]
346 linearityFit = [-coeff/(k1**order) for order, coeff in enumerate(polyFit)]
347 significant = np.where(np.abs(linearityFit) > 1e-10, True, False)
348 self.log.info("Significant polynomial fits: %s", significant)
350 modelOrdinate = funcPolynomial(polyFit, fitAbscissa)
352 self.debugFit('polyFit', linearAbscissa, fitOrdinate, modelOrdinate, None, ampName)
354 if self.config.linearityType == 'Squared':
355 linearityFit = [linearityFit[2]]
356 elif self.config.linearityType == 'LookupTable':
357 # Use linear part to get time at which signal is
358 # maxAduForLookupTableLinearizer DN
359 tMax = (self.config.maxLookupTableAdu - polyFit[0])/polyFit[1]
360 timeRange = np.linspace(0, tMax, self.config.maxLookupTableAdu)
361 signalIdeal = polyFit[0] + polyFit[1]*timeRange
362 signalUncorrected = funcPolynomial(polyFit, timeRange)
363 lookupTableRow = signalIdeal - signalUncorrected # LinearizerLookupTable has correction
365 linearizer.tableData[tableIndex, :] = lookupTableRow
366 linearityFit = [tableIndex, 0]
367 tableIndex += 1
368 elif self.config.linearityType in ['Spline']:
369 # See discussion in `lsst.ip.isr.linearize.py` before
370 # modifying.
371 numPerBin, binEdges = np.histogram(linearOrdinate, bins=fitOrder)
372 with np.errstate(invalid="ignore"):
373 # Algorithm note: With the counts of points per
374 # bin above, the next histogram calculates the
375 # values to put in each bin by weighting each
376 # point by the correction value.
377 values = np.histogram(linearOrdinate, bins=fitOrder,
378 weights=(inputOrdinate[fluxMask] - linearOrdinate))[0]/numPerBin
380 # After this is done, the binCenters are
381 # calculated by weighting by the value we're
382 # binning over. This ensures that widely
383 # spaced/poorly sampled data aren't assigned to
384 # the midpoint of the bin (as could be done using
385 # the binEdges above), but to the weighted mean of
386 # the inputs. Note that both histograms are
387 # scaled by the count per bin to normalize what
388 # the histogram returns (a sum of the points
389 # inside) into an average.
390 binCenters = np.histogram(linearOrdinate, bins=fitOrder,
391 weights=linearOrdinate)[0]/numPerBin
392 values = values[numPerBin > 0]
393 binCenters = binCenters[numPerBin > 0]
395 self.debugFit('splineFit', binCenters, np.abs(values), values, None, ampName)
396 interp = afwMath.makeInterpolate(binCenters.tolist(), values.tolist(),
397 afwMath.stringToInterpStyle("AKIMA_SPLINE"))
398 modelOrdinate = linearOrdinate + interp.interpolate(linearOrdinate)
399 self.debugFit('splineFit', linearOrdinate, fitOrdinate, modelOrdinate, None, ampName)
401 # If we exclude a lot of points, we may end up with
402 # less than fitOrder points. Pad out the low-flux end
403 # to ensure equal lengths.
404 if len(binCenters) != fitOrder:
405 padN = fitOrder - len(binCenters)
406 binCenters = np.pad(binCenters, (padN, 0), 'linear_ramp',
407 end_values=(binCenters.min() - 1.0, ))
408 # This stores the correction, which is zero at low values.
409 values = np.pad(values, (padN, 0))
411 # Pack the spline into a single array.
412 linearityFit = np.concatenate((binCenters.tolist(), values.tolist())).tolist()
413 polyFit = [0.0]
414 polyFitErr = [0.0]
415 chiSq = np.nan
416 else:
417 polyFit = [0.0]
418 polyFitErr = [0.0]
419 chiSq = np.nan
420 linearityFit = [0.0]
422 linearizer.linearityType[ampName] = self.config.linearityType
423 linearizer.linearityCoeffs[ampName] = np.array(linearityFit)
424 linearizer.linearityBBox[ampName] = amp.getBBox()
425 linearizer.fitParams[ampName] = np.array(polyFit)
426 linearizer.fitParamsErr[ampName] = np.array(polyFitErr)
427 linearizer.fitChiSq[ampName] = chiSq
428 linearizer.linearFit[ampName] = linearFit
429 residuals = fitOrdinate - modelOrdinate
431 # The residuals only include flux values which are
432 # not masked out. To be able to access this later and
433 # associate it with the PTC flux values, we need to
434 # fill out the residuals with NaNs where the flux
435 # value is masked.
437 # First convert mask to a composite of the two masks:
438 mask[mask] = fluxMask
439 fullResiduals = np.full(len(mask), np.nan)
440 fullResiduals[mask] = residuals
441 linearizer.fitResiduals[ampName] = fullResiduals
442 image = afwImage.ImageF(len(inputOrdinate), 1)
443 image.getArray()[:, :] = inputOrdinate
444 linearizeFunction = linearizer.getLinearityTypeByName(linearizer.linearityType[ampName])
445 linearizeFunction()(image,
446 **{'coeffs': linearizer.linearityCoeffs[ampName],
447 'table': linearizer.tableData,
448 'log': linearizer.log})
449 linearizeModel = image.getArray()[0, :]
451 self.debugFit('solution', inputOrdinate[fluxMask], linearOrdinate,
452 linearizeModel[fluxMask], None, ampName)
454 linearizer.hasLinearity = True
455 linearizer.validate()
456 linearizer.updateMetadata(camera=camera, detector=detector, filterName='NONE')
457 linearizer.updateMetadata(setDate=True, setCalibId=True)
458 provenance = IsrProvenance(calibType='linearizer')
460 return pipeBase.Struct(
461 outputLinearizer=linearizer,
462 outputProvenance=provenance,
463 )
465 def fillBadAmp(self, linearizer, fitOrder, inputPtc, amp):
466 # Need to fill linearizer with empty values
467 # if the amp is non-functional
468 ampName = amp.getName()
469 nEntries = 1
470 pEntries = 1
471 if self.config.linearityType in ['Polynomial']:
472 nEntries = fitOrder + 1
473 pEntries = fitOrder + 1
474 elif self.config.linearityType in ['Spline']:
475 nEntries = fitOrder * 2
476 elif self.config.linearityType in ['Squared', 'None']:
477 nEntries = 1
478 pEntries = fitOrder + 1
479 elif self.config.linearityType in ['LookupTable']:
480 nEntries = 2
481 pEntries = fitOrder + 1
483 linearizer.linearityType[ampName] = "None"
484 linearizer.linearityCoeffs[ampName] = np.zeros(nEntries)
485 linearizer.linearityBBox[ampName] = amp.getBBox()
486 linearizer.fitParams[ampName] = np.zeros(pEntries)
487 linearizer.fitParamsErr[ampName] = np.zeros(pEntries)
488 linearizer.fitChiSq[ampName] = np.nan
489 linearizer.fitResiduals[ampName] = np.zeros(len(inputPtc.expIdMask[ampName]))
490 linearizer.linearFit[ampName] = np.zeros(2)
491 return linearizer
493 def debugFit(self, stepname, xVector, yVector, yModel, mask, ampName):
494 """Debug method for linearity fitting.
496 Parameters
497 ----------
498 stepname : `str`
499 A label to use to check if we care to debug at a given
500 line of code.
501 xVector : `numpy.array`, (N,)
502 The values to use as the independent variable in the
503 linearity fit.
504 yVector : `numpy.array`, (N,)
505 The values to use as the dependent variable in the
506 linearity fit.
507 yModel : `numpy.array`, (N,)
508 The values to use as the linearized result.
509 mask : `numpy.array` [`bool`], (N,) , optional
510 A mask to indicate which entries of ``xVector`` and
511 ``yVector`` to keep.
512 ampName : `str`
513 Amplifier name to lookup linearity correction values.
514 """
515 frame = getDebugFrame(self._display, stepname)
516 if frame:
517 import matplotlib.pyplot as plt
518 fig, axs = plt.subplots(2)
520 if mask is None:
521 mask = np.ones_like(xVector, dtype=bool)
523 fig.suptitle(f"{stepname} {ampName} {self.config.linearityType}")
524 if stepname == 'linearFit':
525 axs[0].set_xlabel("Input Abscissa (time or mondiode)")
526 axs[0].set_ylabel("Input Ordinate (flux)")
527 axs[1].set_xlabel("Linear Ordinate (linear flux)")
528 axs[1].set_ylabel("Flux Difference: (input - linear)")
529 elif stepname in ('polyFit', 'splineFit'):
530 axs[0].set_xlabel("Linear Abscissa (linear flux)")
531 axs[0].set_ylabel("Input Ordinate (flux)")
532 axs[1].set_xlabel("Linear Ordinate (linear flux)")
533 axs[1].set_ylabel("Flux Difference: (input - full model fit)")
534 elif stepname == 'solution':
535 axs[0].set_xlabel("Input Abscissa (time or mondiode)")
536 axs[0].set_ylabel("Linear Ordinate (linear flux)")
537 axs[1].set_xlabel("Model flux (linear flux)")
538 axs[1].set_ylabel("Flux Difference: (linear - model)")
540 axs[0].set_yscale('log')
541 axs[0].set_xscale('log')
542 axs[0].scatter(xVector, yVector)
543 axs[0].scatter(xVector[~mask], yVector[~mask], c='red', marker='x')
544 axs[1].set_xscale('log')
546 axs[1].scatter(yModel, yVector[mask] - yModel)
547 fig.show()
549 prompt = "Press Enter or c to continue [chpx]..."
550 while True:
551 ans = input(prompt).lower()
552 if ans in ("", " ", "c",):
553 break
554 elif ans in ("p", ):
555 import pdb
556 pdb.set_trace()
557 elif ans in ("h", ):
558 print("[h]elp [c]ontinue [p]db")
559 elif ans in ('x', ):
560 exit()
561 plt.close()
564class MeasureLinearityConfig(pexConfig.Config):
565 solver = pexConfig.ConfigurableField(
566 target=LinearitySolveTask,
567 doc="Task to convert PTC data to linearity solutions.",
568 )
571class MeasureLinearityTask(pipeBase.CmdLineTask):
572 """Stand alone Gen2 linearity measurement.
574 This class wraps the Gen3 linearity task to allow it to be run as
575 a Gen2 CmdLineTask.
576 """
578 ConfigClass = MeasureLinearityConfig
579 _DefaultName = "measureLinearity"
581 def __init__(self, **kwargs):
582 super().__init__(**kwargs)
583 self.makeSubtask("solver")
585 def runDataRef(self, dataRef):
586 """Run new linearity code for gen2.
588 Parameters
589 ----------
590 dataRef : `lsst.daf.persistence.ButlerDataRef`
591 Input dataref for the photon transfer curve data.
593 Returns
594 -------
595 results : `lsst.pipe.base.Struct`
596 The results struct containing:
598 ``outputLinearizer``
599 Final linearizer calibration (`lsst.ip.isr.Linearizer`).
600 ``outputProvenance``
601 Provenance data for the new calibration
602 (`lsst.ip.isr.IsrProvenance`).
603 """
604 ptc = dataRef.get('photonTransferCurveDataset')
605 camera = dataRef.get('camera')
606 inputDims = dataRef.dataId # This is the closest gen2 has.
607 linearityResults = self.solver.run(ptc, camera=camera, inputDims=inputDims)
609 inputDims['calibDate'] = linearityResults.outputLinearizer.getMetadata().get('CALIBDATE')
610 butler = dataRef.getButler()
611 butler.put(linearityResults.outputLinearizer, "linearizer", inputDims)
612 return linearityResults