23 __all__ = [
'MeasurePhotonTransferCurveTask',
24 'MeasurePhotonTransferCurveTaskConfig',
25 'PhotonTransferCurveDataset']
28 import matplotlib.pyplot
as plt
30 from matplotlib.backends.backend_pdf
import PdfPages
31 from sqlite3
import OperationalError
32 from collections
import Counter
35 import lsst.pex.config
as pexConfig
38 from .utils
import (NonexistentDatasetTaskDataIdContainer, PairedVisitListTaskRunner,
39 checkExpLengthEqual, validateIsrConfig)
40 from scipy.optimize
import leastsq, least_squares
41 import numpy.polynomial.polynomial
as poly
45 """Config class for photon transfer curve measurement task""" 46 isr = pexConfig.ConfigurableField(
48 doc=
"""Task to perform instrumental signature removal.""",
50 isrMandatorySteps = pexConfig.ListField(
52 doc=
"isr operations that must be performed for valid results. Raises if any of these are False.",
53 default=[
'doAssembleCcd']
55 isrForbiddenSteps = pexConfig.ListField(
57 doc=
"isr operations that must NOT be performed for valid results. Raises if any of these are True",
58 default=[
'doFlat',
'doFringe',
'doAddDistortionModel',
'doBrighterFatter',
'doUseOpticsTransmission',
59 'doUseFilterTransmission',
'doUseSensorTransmission',
'doUseAtmosphereTransmission']
61 isrDesirableSteps = pexConfig.ListField(
63 doc=
"isr operations that it is advisable to perform, but are not mission-critical." +
64 " WARNs are logged for any of these found to be False.",
65 default=[
'doBias',
'doDark',
'doCrosstalk',
'doDefect']
67 isrUndesirableSteps = pexConfig.ListField(
69 doc=
"isr operations that it is *not* advisable to perform in the general case, but are not" +
70 " forbidden as some use-cases might warrant them." +
71 " WARNs are logged for any of these found to be True.",
72 default=[
'doLinearize']
74 ccdKey = pexConfig.Field(
76 doc=
"The key by which to pull a detector from a dataId, e.g. 'ccd' or 'detector'.",
79 makePlots = pexConfig.Field(
81 doc=
"Plot the PTC curves?",
84 ptcFitType = pexConfig.ChoiceField(
86 doc=
"Fit PTC to approximation in Astier+19 (Equation 16) or to a polynomial.",
89 "POLYNOMIAL":
"n-degree polynomial (use 'polynomialFitDegree' to set 'n').",
90 "ASTIERAPPROXIMATION":
"Approximation in Astier+19 (Eq. 16)." 93 polynomialFitDegree = pexConfig.Field(
95 doc=
"Degree of polynomial to fit the PTC, when 'ptcFitType'=POLYNOMIAL.",
98 binSize = pexConfig.Field(
100 doc=
"Bin the image by this factor in both dimensions.",
103 minMeanSignal = pexConfig.Field(
105 doc=
"Minimum value (inclusive) of mean signal (in ADU) above which to consider.",
108 maxMeanSignal = pexConfig.Field(
110 doc=
"Maximum value (inclusive) of mean signal (in ADU) below which to consider.",
113 initialNonLinearityExclusionThresholdPositive = pexConfig.RangeField(
115 doc=
"Initially exclude data points with a variance that are more than a factor of this from being" 116 " linear in the positive direction, from the PTC fit. Note that these points will also be" 117 " excluded from the non-linearity fit. This is done before the iterative outlier rejection," 118 " to allow an accurate determination of the sigmas for said iterative fit.",
123 initialNonLinearityExclusionThresholdNegative = pexConfig.RangeField(
125 doc=
"Initially exclude data points with a variance that are more than a factor of this from being" 126 " linear in the negative direction, from the PTC fit. Note that these points will also be" 127 " excluded from the non-linearity fit. This is done before the iterative outlier rejection," 128 " to allow an accurate determination of the sigmas for said iterative fit.",
133 sigmaCutPtcOutliers = pexConfig.Field(
135 doc=
"Sigma cut for outlier rejection in PTC.",
138 maxIterationsPtcOutliers = pexConfig.Field(
140 doc=
"Maximum number of iterations for outlier rejection in PTC.",
143 doFitBootstrap = pexConfig.Field(
145 doc=
"Use bootstrap for the PTC fit parameters and errors?.",
148 linResidualTimeIndex = pexConfig.Field(
150 doc=
"Index position in time array for reference time in linearity residual calculation.",
156 """A simple class to hold the output data from the PTC task. 158 The dataset is made up of a dictionary for each item, keyed by the 159 amplifiers' names, which much be supplied at construction time. 161 New items cannot be added to the class to save accidentally saving to the 162 wrong property, and the class can be frozen if desired. 164 inputVisitPairs records the visits used to produce the data. 165 When fitPtcAndNl() is run, a mask is built up, which is by definition 166 always the same length as inputVisitPairs, rawExpTimes, rawMeans 167 and rawVars, and is a list of bools, which are incrementally set to False 168 as points are discarded from the fits. 170 PTC fit parameters for polynomials are stored in a list in ascending order 171 of polynomial term, i.e. par[0]*x^0 + par[1]*x + par[2]*x^2 etc 172 with the length of the list corresponding to the order of the polynomial 179 self.__dict__[
"ampNames"] = ampNames
180 self.__dict__[
"badAmps"] = []
183 self.__dict__[
"inputVisitPairs"] = {ampName: []
for ampName
in ampNames}
184 self.__dict__[
"visitMask"] = {ampName: []
for ampName
in ampNames}
185 self.__dict__[
"rawExpTimes"] = {ampName: []
for ampName
in ampNames}
186 self.__dict__[
"rawMeans"] = {ampName: []
for ampName
in ampNames}
187 self.__dict__[
"rawVars"] = {ampName: []
for ampName
in ampNames}
190 self.__dict__[
"ptcFitType"] = {ampName:
"" for ampName
in ampNames}
191 self.__dict__[
"ptcFitPars"] = {ampName: []
for ampName
in ampNames}
192 self.__dict__[
"ptcFitParsError"] = {ampName: []
for ampName
in ampNames}
193 self.__dict__[
"nonLinearity"] = {ampName: []
for ampName
in ampNames}
194 self.__dict__[
"nonLinearityError"] = {ampName: []
for ampName
in ampNames}
195 self.__dict__[
"nonLinearityResiduals"] = {ampName: []
for ampName
in ampNames}
198 self.__dict__[
"gain"] = {ampName: -1.
for ampName
in ampNames}
199 self.__dict__[
"gainErr"] = {ampName: -1.
for ampName
in ampNames}
200 self.__dict__[
"noise"] = {ampName: -1.
for ampName
in ampNames}
201 self.__dict__[
"noiseErr"] = {ampName: -1.
for ampName
in ampNames}
204 """Protect class attributes""" 205 if attribute
not in self.__dict__:
206 raise AttributeError(f
"{attribute} is not already a member of PhotonTransferCurveDataset, which" 207 " does not support setting of new attributes.")
209 self.__dict__[attribute] = value
212 """Get the visits used, i.e. not discarded, for a given amp. 214 If no mask has been created yet, all visits are returned. 216 if self.visitMask[ampName] == []:
217 return self.inputVisitPairs[ampName]
220 assert len(self.visitMask[ampName]) == len(self.inputVisitPairs[ampName])
222 pairs = self.inputVisitPairs[ampName]
223 mask = self.visitMask[ampName]
225 return [(v1, v2)
for ((v1, v2), m)
in zip(pairs, mask)
if bool(m)
is True]
228 return [amp
for amp
in self.ampNames
if amp
not in self.badAmps]
232 """A class to calculate, fit, and plot a PTC from a set of flat pairs. 234 The Photon Transfer Curve (var(signal) vs mean(signal)) is a standard tool 235 used in astronomical detectors characterization (e.g., Janesick 2001, 236 Janesick 2007). This task calculates the PTC from a series of pairs of 237 flat-field images; each pair taken at identical exposure times. The 238 difference image of each pair is formed to eliminate fixed pattern noise, 239 and then the variance of the difference image and the mean of the average image 240 are used to produce the PTC. An n-degree polynomial or the approximation in Equation 241 16 of Astier+19 ("The Shape of the Photon Transfer Curve of CCD sensors", 242 arXiv:1905.08677) can be fitted to the PTC curve. These models include 243 parameters such as the gain (e/ADU) and readout noise. 249 Positional arguments passed to the Task constructor. None used at this 252 Keyword arguments passed on to the Task constructor. None used at this 257 RunnerClass = PairedVisitListTaskRunner
258 ConfigClass = MeasurePhotonTransferCurveTaskConfig
259 _DefaultName =
"measurePhotonTransferCurve" 262 pipeBase.CmdLineTask.__init__(self, *args, **kwargs)
263 self.makeSubtask(
"isr")
264 plt.interactive(
False)
266 self.config.isrForbiddenSteps, self.config.isrDesirableSteps, checkTrim=
False)
271 def _makeArgumentParser(cls):
272 """Augment argument parser for the MeasurePhotonTransferCurveTask.""" 274 parser.add_argument(
"--visit-pairs", dest=
"visitPairs", nargs=
"*",
275 help=
"Visit pairs to use. Each pair must be of the form INT,INT e.g. 123,456")
276 parser.add_id_argument(
"--id", datasetType=
"photonTransferCurveDataset",
277 ContainerClass=NonexistentDatasetTaskDataIdContainer,
278 help=
"The ccds to use, e.g. --id ccd=0..100")
283 """Run the Photon Transfer Curve (PTC) measurement task. 285 For a dataRef (which is each detector here), 286 and given a list of visit pairs at different exposure times, 291 dataRef : list of lsst.daf.persistence.ButlerDataRef 292 dataRef for the detector for the visits to be fit. 293 visitPairs : `iterable` of `tuple` of `int` 294 Pairs of visit numbers to be processed together 298 detNum = dataRef.dataId[self.config.ccdKey]
299 detector = dataRef.get(
'camera')[dataRef.dataId[self.config.ccdKey]]
305 for name
in dataRef.getButler().getKeys(
'bias'):
306 if name
not in dataRef.dataId:
308 dataRef.dataId[name] = \
309 dataRef.getButler().queryMetadata(
'raw', [name], detector=detNum)[0]
310 except OperationalError:
313 amps = detector.getAmplifiers()
314 ampNames = [amp.getName()
for amp
in amps]
317 self.log.
info(
'Measuring PTC using %s visits for detector %s' % (visitPairs, detNum))
319 for (v1, v2)
in visitPairs:
321 dataRef.dataId[
'visit'] = v1
323 dataRef.dataId[
'visit'] = v2
325 del dataRef.dataId[
'visit']
328 expTime = exp1.getInfo().getVisitInfo().getExposureTime()
332 ampName = amp.getName()
334 dataset.rawExpTimes[ampName].
append(expTime)
335 dataset.rawMeans[ampName].
append(mu)
336 dataset.rawVars[ampName].
append(varDiff)
337 dataset.inputVisitPairs[ampName].
append((v1, v2))
341 dataset = self.
fitPtcAndNl(dataset, ptcFitType=self.config.ptcFitType)
343 if self.config.makePlots:
344 self.
plot(dataRef, dataset, ptcFitType=self.config.ptcFitType)
347 self.log.
info(f
"Writing PTC and NL data to {dataRef.getUri(write=True)}")
348 dataRef.put(dataset, datasetType=
"photonTransferCurveDataset")
350 self.log.
info(
'Finished measuring PTC for in detector %s' % detNum)
352 return pipeBase.Struct(exitStatus=0)
355 """Calculate the mean signal of two exposures and the variance of their difference. 359 exposure1 : `lsst.afw.image.exposure.exposure.ExposureF` 360 First exposure of flat field pair. 362 exposure2 : `lsst.afw.image.exposure.exposure.ExposureF` 363 Second exposure of flat field pair. 365 region : `lsst.geom.Box2I` 366 Region of each exposure where to perform the calculations (e.g, an amplifier). 372 0.5*(mu1 + mu2), where mu1, and mu2 are the clipped means of the regions in 376 Half of the clipped variance of the difference of the regions inthe two input 380 if region
is not None:
381 im1Area = exposure1.maskedImage[region]
382 im2Area = exposure2.maskedImage[region]
384 im1Area = exposure1.maskedImage
385 im2Area = exposure2.maskedImage
397 temp = im2Area.clone()
399 diffIm = im1Area.clone()
408 def _fitLeastSq(self, initialParams, dataX, dataY, function):
409 """Do a fit and estimate the parameter errors using using scipy.optimize.leastq. 411 optimize.leastsq returns the fractional covariance matrix. To estimate the 412 standard deviation of the fit parameters, multiply the entries of this matrix 413 by the reduced chi squared and take the square root of the diagonal elements. 417 initialParams : list of np.float 418 initial values for fit parameters. For ptcFitType=POLYNOMIAL, its length 419 determines the degree of the polynomial. 421 dataX : np.array of np.float 422 Data in the abscissa axis. 424 dataY : np.array of np.float 425 Data in the ordinate axis. 427 function : callable object (function) 428 Function to fit the data with. 432 pFitSingleLeastSquares : list of np.float 433 List with fitted parameters. 435 pErrSingleLeastSquares : list of np.float 436 List with errors for fitted parameters. 439 def errFunc(p, x, y):
442 pFit, pCov, infoDict, errMessage, success = leastsq(errFunc, initialParams,
443 args=(dataX, dataY), full_output=1, epsfcn=0.0001)
445 if (len(dataY) > len(initialParams))
and pCov
is not None:
446 reducedChiSq = (errFunc(pFit, dataX, dataY)**2).sum()/(len(dataY)-len(initialParams))
452 for i
in range(len(pFit)):
453 errorVec.append(np.fabs(pCov[i][i])**0.5)
455 pFitSingleLeastSquares = pFit
456 pErrSingleLeastSquares = np.array(errorVec)
458 return pFitSingleLeastSquares, pErrSingleLeastSquares
460 def _fitBootstrap(self, initialParams, dataX, dataY, function, confidenceSigma=1.):
461 """Do a fit using least squares and bootstrap to estimate parameter errors. 463 The bootstrap error bars are calculated by fitting 100 random data sets. 467 initialParams : list of np.float 468 initial values for fit parameters. For ptcFitType=POLYNOMIAL, its length 469 determines the degree of the polynomial. 471 dataX : np.array of np.float 472 Data in the abscissa axis. 474 dataY : np.array of np.float 475 Data in the ordinate axis. 477 function : callable object (function) 478 Function to fit the data with. 480 confidenceSigma : np.float 481 Number of sigmas that determine confidence interval for the bootstrap errors. 485 pFitBootstrap : list of np.float 486 List with fitted parameters. 488 pErrBootstrap : list of np.float 489 List with errors for fitted parameters. 492 def errFunc(p, x, y):
496 pFit, _ = leastsq(errFunc, initialParams, args=(dataX, dataY), full_output=0)
499 residuals = errFunc(pFit, dataX, dataY)
500 sigmaErrTotal = np.std(residuals)
505 randomDelta = np.random.normal(0., sigmaErrTotal, len(dataY))
506 randomDataY = dataY + randomDelta
507 randomFit, _ = leastsq(errFunc, initialParams,
508 args=(dataX, randomDataY), full_output=0)
509 pars.append(randomFit)
510 pars = np.array(pars)
511 meanPfit = np.mean(pars, 0)
514 nSigma = confidenceSigma
515 errPfit = nSigma*np.std(pars, 0)
516 pFitBootstrap = meanPfit
517 pErrBootstrap = errPfit
518 return pFitBootstrap, pErrBootstrap
521 """Polynomial function definition""" 522 return poly.polyval(x, [*pars])
525 """Single brighter-fatter parameter model for PTC; Equation 16 of Astier+19""" 526 a00, gain, noise = pars
527 return 0.5/(a00*gain*gain)*(np.exp(2*a00*x*gain)-1) + noise/(gain*gain)
530 def _initialParsForPolynomial(order):
532 pars = np.zeros(order, dtype=np.float)
539 def _boundsForPolynomial(initialPars):
540 lowers = [np.NINF
for p
in initialPars]
541 uppers = [np.inf
for p
in initialPars]
543 return (lowers, uppers)
546 def _boundsForAstier(initialPars):
547 lowers = [np.NINF
for p
in initialPars]
548 uppers = [np.inf
for p
in initialPars]
549 return (lowers, uppers)
552 def _getInitialGoodPoints(means, variances, maxDeviationPositive, maxDeviationNegative):
553 """Return a boolean array to mask bad points. 555 A linear function has a constant ratio, so find the median 556 value of the ratios, and exclude the points that deviate 557 from that by more than a factor of maxDeviationPositive/negative. 558 Asymmetric deviations are supported as we expect the PTC to turn 559 down as the flux increases, but sometimes it anomalously turns 560 upwards just before turning over, which ruins the fits, so it 561 is wise to be stricter about restricting positive outliers than 564 Too high and points that are so bad that fit will fail will be included 565 Too low and the non-linear points will be excluded, biasing the NL fit.""" 566 ratios = [b/a
for (a, b)
in zip(means, variances)]
567 medianRatio = np.median(ratios)
568 ratioDeviations = [(r/medianRatio)-1
for r
in ratios]
571 maxDeviationPositive =
abs(maxDeviationPositive)
572 maxDeviationNegative = -1. *
abs(maxDeviationNegative)
574 goodPoints = np.array([
True if (r < maxDeviationPositive
and r > maxDeviationNegative)
575 else False for r
in ratioDeviations])
578 def _makeZeroSafe(self, array, warn=True, substituteValue=1e-9):
580 nBad = Counter(array)[0]
585 msg = f
"Found {nBad} zeros in array at elements {[x for x in np.where(array==0)[0]]}" 588 array[array == 0] = substituteValue
592 """Fit the photon transfer curve and calculate linearity and residuals. 594 Fit the photon transfer curve with either a polynomial of the order 595 specified in the task config, or using the Astier approximation. 597 Sigma clipping is performed iteratively for the fit, as well as an 598 initial clipping of data points that are more than 599 config.initialNonLinearityExclusionThreshold away from lying on a 600 straight line. This other step is necessary because the photon transfer 601 curve turns over catastrophically at very high flux (because saturation 602 drops the variance to ~0) and these far outliers cause the initial fit 603 to fail, meaning the sigma cannot be calculated to perform the 608 dataset : `lsst.cp.pipe.ptc.PhotonTransferCurveDataset` 609 The dataset containing the means, variances and exposure times 611 Fit a 'POLYNOMIAL' (degree: 'polynomialFitDegree') or 612 'ASTIERAPPROXIMATION' to the PTC 615 def errFunc(p, x, y):
616 return ptcFunc(p, x) - y
618 sigmaCutPtcOutliers = self.config.sigmaCutPtcOutliers
619 maxIterationsPtcOutliers = self.config.maxIterationsPtcOutliers
621 for ampName
in dataset.ampNames:
622 timeVecOriginal = np.array(dataset.rawExpTimes[ampName])
623 meanVecOriginal = np.array(dataset.rawMeans[ampName])
624 varVecOriginal = np.array(dataset.rawVars[ampName])
627 mask = ((meanVecOriginal >= self.config.minMeanSignal) &
628 (meanVecOriginal <= self.config.maxMeanSignal))
631 self.config.initialNonLinearityExclusionThresholdPositive,
632 self.config.initialNonLinearityExclusionThresholdNegative)
633 mask = mask & goodPoints
635 if ptcFitType ==
'ASTIERAPPROXIMATION':
637 parsIniPtc = [-1e-9, 1.0, 10.]
639 if ptcFitType ==
'POLYNOMIAL':
646 while count <= maxIterationsPtcOutliers:
650 meanTempVec = meanVecOriginal[mask]
651 varTempVec = varVecOriginal[mask]
652 res = least_squares(errFunc, parsIniPtc, bounds=bounds, args=(meanTempVec, varTempVec))
658 sigResids = (varVecOriginal - ptcFunc(pars, meanVecOriginal))/np.sqrt(varVecOriginal)
659 newMask = np.array([
True if np.abs(r) < sigmaCutPtcOutliers
else False for r
in sigResids])
660 mask = mask & newMask
662 nDroppedTotal = Counter(mask)[
False]
663 self.log.
debug(f
"Iteration {count}: discarded {nDroppedTotal} points in total for {ampName}")
666 assert (len(mask) == len(timeVecOriginal) == len(meanVecOriginal) == len(varVecOriginal))
668 dataset.visitMask[ampName] = mask
671 timeVecFinal = timeVecOriginal[mask]
672 meanVecFinal = meanVecOriginal[mask]
673 varVecFinal = varVecOriginal[mask]
675 if Counter(mask)[
False] > 0:
676 self.log.
info((f
"Number of points discarded in PTC of amplifier {ampName}:" +
677 f
" {Counter(mask)[False]} out of {len(meanVecOriginal)}"))
679 if (len(meanVecFinal) < len(parsIniPtc)):
680 msg = (f
"\nSERIOUS: Not enough data points ({len(meanVecFinal)}) compared to the number of" 681 f
"parameters of the PTC model({len(parsIniPtc)}). Setting {ampName} to BAD.")
683 dataset.badAmps.append(ampName)
684 dataset.gain[ampName] = np.nan
685 dataset.gainErr[ampName] = np.nan
686 dataset.noise[ampName] = np.nan
687 dataset.noiseErr[ampName] = np.nan
688 dataset.nonLinearity[ampName] = np.nan
689 dataset.nonLinearityError[ampName] = np.nan
690 dataset.nonLinearityResiduals[ampName] = np.nan
694 if self.config.doFitBootstrap:
695 parsFit, parsFitErr = self.
_fitBootstrap(parsIniPtc, meanVecFinal, varVecFinal, ptcFunc)
697 parsFit, parsFitErr = self.
_fitLeastSq(parsIniPtc, meanVecFinal, varVecFinal, ptcFunc)
699 dataset.ptcFitPars[ampName] = parsFit
700 dataset.ptcFitParsError[ampName] = parsFitErr
702 if ptcFitType ==
'ASTIERAPPROXIMATION':
704 ptcGainErr = parsFitErr[1]
705 ptcNoise = np.sqrt(np.fabs(parsFit[2]))
706 ptcNoiseErr = 0.5*(parsFitErr[2]/np.fabs(parsFit[2]))*np.sqrt(np.fabs(parsFit[2]))
707 if ptcFitType ==
'POLYNOMIAL':
708 ptcGain = 1./parsFit[1]
709 ptcGainErr = np.fabs(1./parsFit[1])*(parsFitErr[1]/parsFit[1])
710 ptcNoise = np.sqrt(np.fabs(parsFit[0]))*ptcGain
711 ptcNoiseErr = (0.5*(parsFitErr[0]/np.fabs(parsFit[0]))*(np.sqrt(np.fabs(parsFit[0]))))*ptcGain
713 dataset.gain[ampName] = ptcGain
714 dataset.gainErr[ampName] = ptcGainErr
715 dataset.noise[ampName] = ptcNoise
716 dataset.noiseErr[ampName] = ptcNoiseErr
720 parsIniNl = [1., 1., 1.]
721 if self.config.doFitBootstrap:
722 parsFit, parsFitErr = self.
_fitBootstrap(parsIniNl, timeVecFinal, meanVecFinal,
725 parsFit, parsFitErr = self.
_fitLeastSq(parsIniNl, timeVecFinal, meanVecFinal,
727 linResidualTimeIndex = self.config.linResidualTimeIndex
728 if timeVecFinal[linResidualTimeIndex] == 0.0:
729 raise RuntimeError(
"Reference time for linearity residual can't be 0.0")
730 linResidual = 100*(1 - ((meanVecFinal[linResidualTimeIndex] /
731 timeVecFinal[linResidualTimeIndex]) / (meanVecFinal/timeVecFinal)))
733 dataset.nonLinearity[ampName] = parsFit
734 dataset.nonLinearityError[ampName] = parsFitErr
735 dataset.nonLinearityResiduals[ampName] = linResidual
739 def plot(self, dataRef, dataset, ptcFitType):
740 dirname = dataRef.getUri(datasetType=
'cpPipePlotRoot', write=
True)
741 if not os.path.exists(dirname):
744 detNum = dataRef.dataId[self.config.ccdKey]
745 filename = f
"PTC_det{detNum}.pdf" 746 filenameFull = os.path.join(dirname, filename)
747 with PdfPages(filenameFull)
as pdfPages:
748 self.
_plotPtc(dataset, ptcFitType, pdfPages)
750 def _plotPtc(self, dataset, ptcFitType, pdfPages):
751 """Plot PTC, linearity, and linearity residual per amplifier""" 753 if ptcFitType ==
'ASTIERAPPROXIMATION':
755 stringTitle =
r"Var = $\frac{1}{2g^2a_{00}}(\exp (2a_{00} \mu g) - 1) + \frac{n_{00}}{g^2}$" 757 if ptcFitType ==
'POLYNOMIAL':
759 stringTitle = f
"Polynomial (degree: {self.config.polynomialFitDegree})" 764 supTitleFontSize = 18
768 nAmps = len(dataset.ampNames)
771 nRows = np.sqrt(nAmps)
772 mantissa, _ = np.modf(nRows)
774 nRows = int(nRows) + 1
780 f, ax = plt.subplots(nrows=nRows, ncols=nCols, sharex=
'col', sharey=
'row', figsize=(13, 10))
781 f2, ax2 = plt.subplots(nrows=nRows, ncols=nCols, sharex=
'col', sharey=
'row', figsize=(13, 10))
783 for i, (amp, a, a2)
in enumerate(zip(dataset.ampNames, ax.flatten(), ax2.flatten())):
784 meanVecOriginal = np.array(dataset.rawMeans[amp])
785 varVecOriginal = np.array(dataset.rawVars[amp])
786 mask = dataset.visitMask[amp]
787 meanVecFinal = meanVecOriginal[mask]
788 varVecFinal = varVecOriginal[mask]
789 meanVecOutliers = meanVecOriginal[np.invert(mask)]
790 varVecOutliers = varVecOriginal[np.invert(mask)]
791 pars, parsErr = dataset.ptcFitPars[amp], dataset.ptcFitParsError[amp]
793 if ptcFitType ==
'ASTIERAPPROXIMATION':
794 ptcA00, ptcA00error = pars[0], parsErr[0]
795 ptcGain, ptcGainError = pars[1], parsErr[1]
796 ptcNoise = np.sqrt(np.fabs(pars[2]))
797 ptcNoiseError = 0.5*(parsErr[2]/np.fabs(pars[2]))*np.sqrt(np.fabs(pars[2]))
798 stringLegend = (f
"a00: {ptcA00:.2e}+/-{ptcA00error:.2e}" 799 f
"\n Gain: {ptcGain:.4}+/-{ptcGainError:.2e}" 800 f
"\n Noise: {ptcNoise:.4}+/-{ptcNoiseError:.2e}")
802 if ptcFitType ==
'POLYNOMIAL':
803 ptcGain, ptcGainError = 1./pars[1], np.fabs(1./pars[1])*(parsErr[1]/pars[1])
804 ptcNoise = np.sqrt(np.fabs(pars[0]))*ptcGain
805 ptcNoiseError = (0.5*(parsErr[0]/np.fabs(pars[0]))*(np.sqrt(np.fabs(pars[0]))))*ptcGain
806 stringLegend = (f
"Noise: {ptcNoise:.4}+/-{ptcNoiseError:.2e} \n" 807 f
"Gain: {ptcGain:.4}+/-{ptcGainError:.2e}")
809 minMeanVecFinal = np.min(meanVecFinal)
810 maxMeanVecFinal = np.max(meanVecFinal)
811 meanVecFit = np.linspace(minMeanVecFinal, maxMeanVecFinal, 100*len(meanVecFinal))
812 minMeanVecOriginal = np.min(meanVecOriginal)
813 maxMeanVecOriginal = np.max(meanVecOriginal)
814 deltaXlim = maxMeanVecOriginal - minMeanVecOriginal
816 a.plot(meanVecFit, ptcFunc(pars, meanVecFit), color=
'red')
817 a.plot(meanVecFinal, pars[0] + pars[1]*meanVecFinal, color=
'green', linestyle=
'--')
818 a.scatter(meanVecFinal, varVecFinal, c=
'blue', marker=
'o', s=markerSize)
819 a.scatter(meanVecOutliers, varVecOutliers, c=
'magenta', marker=
's', s=markerSize)
820 a.set_xlabel(
r'Mean signal ($\mu$, ADU)', fontsize=labelFontSize)
821 a.set_xticks(meanVecOriginal)
822 a.set_ylabel(
r'Variance (ADU$^2$)', fontsize=labelFontSize)
823 a.tick_params(labelsize=11)
824 a.text(0.03, 0.8, stringLegend, transform=a.transAxes, fontsize=legendFontSize)
825 a.set_xscale(
'linear', fontsize=labelFontSize)
826 a.set_yscale(
'linear', fontsize=labelFontSize)
827 a.set_title(amp, fontsize=titleFontSize)
828 a.set_xlim([minMeanVecOriginal - 0.2*deltaXlim, maxMeanVecOriginal + 0.2*deltaXlim])
831 a2.plot(meanVecFit, ptcFunc(pars, meanVecFit), color=
'red')
832 a2.scatter(meanVecFinal, varVecFinal, c=
'blue', marker=
'o', s=markerSize)
833 a2.scatter(meanVecOutliers, varVecOutliers, c=
'magenta', marker=
's', s=markerSize)
834 a2.set_xlabel(
r'Mean Signal ($\mu$, ADU)', fontsize=labelFontSize)
835 a2.set_ylabel(
r'Variance (ADU$^2$)', fontsize=labelFontSize)
836 a2.tick_params(labelsize=11)
837 a2.text(0.03, 0.8, stringLegend, transform=a2.transAxes, fontsize=legendFontSize)
840 a2.set_title(amp, fontsize=titleFontSize)
841 a2.set_xlim([minMeanVecOriginal, maxMeanVecOriginal])
843 f.suptitle(f
"PTC \n Fit: " + stringTitle, fontsize=20)
845 f2.suptitle(f
"PTC (log-log)", fontsize=20)
849 f, ax = plt.subplots(nrows=4, ncols=4, sharex=
'col', sharey=
'row', figsize=(13, 10))
850 for i, (amp, a)
in enumerate(zip(dataset.ampNames, ax.flatten())):
851 meanVecFinal = np.array(dataset.rawMeans[amp])[dataset.visitMask[amp]]
852 timeVecFinal = np.array(dataset.rawExpTimes[amp])[dataset.visitMask[amp]]
854 pars, parsErr = dataset.nonLinearity[amp], dataset.nonLinearityError[amp]
855 c0, c0Error = pars[0], parsErr[0]
856 c1, c1Error = pars[1], parsErr[1]
857 c2, c2Error = pars[2], parsErr[2]
858 stringLegend = f
"c0: {c0:.4}+/-{c0Error:.2e}\n c1: {c1:.4}+/-{c1Error:.2e}" \
859 + f
"\n c2(NL): {c2:.2e}+/-{c2Error:.2e}" 860 a.scatter(timeVecFinal, meanVecFinal)
861 a.plot(timeVecFinal, self.
funcPolynomial(pars, timeVecFinal), color=
'red')
862 a.set_xlabel(
'Time (sec)', fontsize=labelFontSize)
863 a.set_xticks(timeVecFinal)
864 a.set_ylabel(
r'Mean signal ($\mu$, ADU)', fontsize=labelFontSize)
865 a.tick_params(labelsize=labelFontSize)
866 a.text(0.03, 0.75, stringLegend, transform=a.transAxes, fontsize=legendFontSize)
867 a.set_xscale(
'linear', fontsize=labelFontSize)
868 a.set_yscale(
'linear', fontsize=labelFontSize)
869 a.set_title(amp, fontsize=titleFontSize)
871 f.suptitle(
"Linearity \n Fit: " +
r"$\mu = c_0 + c_1 t + c_2 t^2$", fontsize=supTitleFontSize)
875 f, ax = plt.subplots(nrows=4, ncols=4, sharex=
'col', sharey=
'row', figsize=(13, 10))
876 for i, (amp, a)
in enumerate(zip(dataset.ampNames, ax.flatten())):
877 meanVecFinal = np.array(dataset.rawMeans[amp])[dataset.visitMask[amp]]
878 linRes = np.array(dataset.nonLinearityResiduals[amp])
880 a.scatter(meanVecFinal, linRes)
881 a.axhline(y=0, color=
'k')
882 a.axvline(x=timeVecFinal[self.config.linResidualTimeIndex], color=
'g', linestyle=
'--')
883 a.set_xlabel(
r'Mean signal ($\mu$, ADU)', fontsize=labelFontSize)
884 a.set_xticks(meanVecFinal)
885 a.set_ylabel(
'LR (%)', fontsize=labelFontSize)
886 a.tick_params(labelsize=labelFontSize)
887 a.set_xscale(
'linear', fontsize=labelFontSize)
888 a.set_yscale(
'linear', fontsize=labelFontSize)
889 a.set_title(amp, fontsize=titleFontSize)
891 f.suptitle(
r"Linearity Residual: $100(1 - \mu_{\rm{ref}}/t_{\rm{ref}})/(\mu / t))$" +
"\n" +
892 r"$t_{\rm{ref}}$: " + f
"{timeVecFinal[2]} s", fontsize=supTitleFontSize)
def funcAstier(self, pars, x)
def _plotPtc(self, dataset, ptcFitType, pdfPages)
Angle abs(Angle const &a)
def _boundsForPolynomial(initialPars)
def __init__(self, ampNames)
def runDataRef(self, dataRef, visitPairs)
def _boundsForAstier(initialPars)
def __setattr__(self, attribute, value)
def fitPtcAndNl(self, dataset, ptcFitType)
def funcPolynomial(self, pars, x)
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
def checkExpLengthEqual(exp1, exp2, v1=None, v2=None, raiseWithMessage=False)
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
def validateIsrConfig(isrTask, mandatory=None, forbidden=None, desirable=None, undesirable=None, checkTrim=True, logName=None)
def _getInitialGoodPoints(means, variances, maxDeviationPositive, maxDeviationNegative)
def _fitBootstrap(self, initialParams, dataX, dataY, function, confidenceSigma=1.)
def getVisitsUsed(self, ampName)
def __init__(self, args, kwargs)
def measureMeanVarPair(self, exposure1, exposure2, region=None)
def _initialParsForPolynomial(order)
def _makeZeroSafe(self, array, warn=True, substituteValue=1e-9)
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binsize, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
def _fitLeastSq(self, initialParams, dataX, dataY, function)
def plot(self, dataRef, dataset, ptcFitType)