Esempio n. 1
0
    def testInvalidInputs(self):
        """Test that invalid inputs cause an abort"""

        self.assertRaises(pexExcept.InvalidParameterError,
                                       lambda : afwMath.makeInterpolate([], [],
                                                                        afwMath.Interpolate.CONSTANT))

        afwMath.makeInterpolate([0], [1], afwMath.Interpolate.CONSTANT)

        self.assertRaises(pexExcept.OutOfRangeError,
                                       lambda : afwMath.makeInterpolate([0], [1],
                                                                        afwMath.Interpolate.LINEAR))
Esempio n. 2
0
    def testInvalidInputs(self):
        """Test that invalid inputs cause an abort"""

        self.assertRaises(pexExcept.InvalidParameterError,
                                       lambda : afwMath.makeInterpolate([], [],
                                                                        afwMath.Interpolate.CONSTANT))

        interp = afwMath.makeInterpolate([0], [1], afwMath.Interpolate.CONSTANT)

        self.assertRaises(pexExcept.OutOfRangeError,
                                       lambda : afwMath.makeInterpolate([0], [1],
                                                                        afwMath.Interpolate.LINEAR))
Esempio n. 3
0
    def testInvalidInputs(self):
        """Test that invalid inputs cause an abort"""

        utilsTests.assertRaisesLsstCpp(
            self,
            pexExcept.InvalidParameterException,
            lambda: afwMath.makeInterpolate([], [], afwMath.Interpolate.CONSTANT),
        )

        interp = afwMath.makeInterpolate([0], [1], afwMath.Interpolate.CONSTANT)

        utilsTests.assertRaisesLsstCpp(
            self, pexExcept.MemoryException, lambda: afwMath.makeInterpolate([0], [1], afwMath.Interpolate.LINEAR)
        )
Esempio n. 4
0
    def testAkimaSplineParabola(self):
        """test the Spline interpolator"""
        # specify interp type with the enum style interface
        yinterpS = afwMath.makeInterpolate(self.x, self.y2, afwMath.Interpolate.AKIMA_SPLINE)
        youtS = yinterpS.interpolate(self.xtest)

        self.assertEqual(youtS, self.y2test)
    def getInterpImage(self, bbox):
        """Return an image interpolated in R.A direction covering supplied bounding box

        @param[in] bbox: integer bounding box for image (afwGeom.Box2I)
        """

        npoints = len(self._xList)
        #sort by X coordinate
        if npoints < 1:
            raise RuntimeError("Cannot create scaling image. Found no fluxMag0s to interpolate")

        x, z = zip(*sorted(zip(self._xList, self._scaleList)))

        xvec = afwMath.vectorD(x)
        zvec = afwMath.vectorD(z)
        height = bbox.getHeight()
        width = bbox.getWidth()
        x0, y0 = bbox.getMin()

        interp = afwMath.makeInterpolate(xvec, zvec, self.interpStyle)
        interpValArr = numpy.zeros(width, dtype=numpy.float32)

        for i, xInd in enumerate(range(x0, x0 + width)):
            xPos = afwImage.indexToPosition(xInd)
            interpValArr[i] = interp.interpolate(xPos)

        # assume the maskedImage being scaled is MaskedImageF (which is usually true); see ticket #3070
        interpGrid = numpy.meshgrid(interpValArr, range(0, height))[0].astype(numpy.float32)
        image = afwImage.makeImageFromArray(interpGrid)
        image.setXY0(x0, y0)
        return image
    def getInterpImage(self, bbox):
        """Return an image interpolated in R.A direction covering supplied bounding box

        @param[in] bbox: integer bounding box for image (afwGeom.Box2I)
        """

        npoints = len(self._xList)
        # sort by X coordinate
        if npoints < 1:
            raise RuntimeError(
                "Cannot create scaling image. Found no fluxMag0s to interpolate"
            )

        x, z = list(zip(*sorted(zip(self._xList, self._scaleList))))

        xvec = np.array(x, dtype=float)
        zvec = np.array(z, dtype=float)
        height = bbox.getHeight()
        width = bbox.getWidth()
        x0, y0 = bbox.getMin()

        interp = afwMath.makeInterpolate(xvec, zvec, self.interpStyle)
        interpValArr = np.zeros(width, dtype=np.float32)

        for i, xInd in enumerate(range(x0, x0 + width)):
            xPos = afwImage.indexToPosition(xInd)
            interpValArr[i] = interp.interpolate(xPos)

        # assume the maskedImage being scaled is MaskedImageF (which is usually true); see ticket #3070
        interpGrid = np.meshgrid(interpValArr,
                                 range(0, height))[0].astype(np.float32)
        image = afwImage.makeImageFromArray(interpGrid)
        image.setXY0(x0, y0)
        return image
Esempio n. 7
0
    def __call__(self, image, **kwargs):
        """Correct for non-linearity.

        Parameters
        ----------
        image : `lsst.afw.image.Image`
            Image to be corrected
        kwargs : `dict`
            Dictionary of parameter keywords:
            ``"coeffs"``
                Coefficient vector (`list` or `numpy.array`).
            ``"log"``
                Logger to handle messages (`lsst.log.Log`).

        Returns
        -------
        output : `tuple` [`bool`, `int`]
            If true, a correction was applied successfully.  The
            integer indicates the number of pixels that were
            uncorrectable by being out of range.
        """
        splineCoeff = kwargs['coeffs']
        centers, values = np.split(splineCoeff, 2)
        interp = afwMath.makeInterpolate(centers.tolist(), values.tolist(),
                                         afwMath.stringToInterpStyle("AKIMA_SPLINE"))

        ampArr = image.getArray()
        delta = interp.interpolate(ampArr.flatten())
        ampArr -= np.array(delta).reshape(ampArr.shape)

        return True, 0
Esempio n. 8
0
    def testConstant(self):
        """test the constant interpolator"""
        # [xy]vec:   point samples
        # [xy]vec_c: centered values        
        xvec =   np.array([    0.0, 1.0, 2.0, 3.0,  4.0,  5.0,  6.0,  7.0,  8.0,  9.0])
        xvec_c = np.array([-0.5, 0.5, 1.5, 2.5,  3.5,  4.5,  5.5,  6.5,  7.5,  8.5, 9.5])
        yvec =   np.array([    1.0, 2.4, 5.0, 8.4, 13.0, 18.4, 25.0, 32.6, 41.0, 50.6])
        yvec_c = np.array([ 1.0, 1.7, 3.7, 6.7, 10.7, 15.7, 21.7, 28.8, 36.8, 45.8, 50.6])

        interp = afwMath.makeInterpolate(xvec, yvec, afwMath.Interpolate.CONSTANT)

        for x, y in zip(xvec_c, yvec_c):
            self.assertAlmostEqual(interp.interpolate(x + 0.1), y)
            self.assertAlmostEqual(interp.interpolate(x), y)

        self.assertEqual(interp.interpolate(xvec[0] - 10), yvec[0])
        n = len(yvec)
        self.assertEqual(interp.interpolate(xvec[n - 1] + 10), yvec[n - 1])

        for x, y in reversed(zip(xvec_c, yvec_c)): # test caching as we go backwards
            self.assertAlmostEqual(interp.interpolate(x + 0.1), y)
            self.assertAlmostEqual(interp.interpolate(x), y)

        i = 2
        for x in np.arange(xvec_c[i], xvec_c[i + 1], 10):
            self.assertEqual(interp.interpolate(x), yvec_c[i])
Esempio n. 9
0
    def testConstant(self):
        """test the constant interpolator"""
        # [xy]vec:   point samples
        # [xy]vec_c: centered values
        xvec = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
        xvec_c = np.array(
            [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5])
        yvec = np.array(
            [1.0, 2.4, 5.0, 8.4, 13.0, 18.4, 25.0, 32.6, 41.0, 50.6])
        yvec_c = np.array(
            [1.0, 1.7, 3.7, 6.7, 10.7, 15.7, 21.7, 28.8, 36.8, 45.8, 50.6])

        interp = afwMath.makeInterpolate(xvec, yvec,
                                         afwMath.Interpolate.CONSTANT)

        for x, y in zip(xvec_c, yvec_c):
            self.assertAlmostEqual(interp.interpolate(x + 0.1), y)
            self.assertAlmostEqual(interp.interpolate(x), y)

        self.assertEqual(interp.interpolate(xvec[0] - 10), yvec[0])
        n = len(yvec)
        self.assertEqual(interp.interpolate(xvec[n - 1] + 10), yvec[n - 1])

        for x, y in reversed(list(zip(
                xvec_c, yvec_c))):  # test caching as we go backwards
            self.assertAlmostEqual(interp.interpolate(x + 0.1), y)
            self.assertAlmostEqual(interp.interpolate(x), y)

        i = 2
        for x in np.arange(xvec_c[i], xvec_c[i + 1], 10):
            self.assertEqual(interp.interpolate(x), yvec_c[i])
Esempio n. 10
0
    def testInvalidInputs(self):
        """Test that invalid inputs cause an abort"""

        utilsTests.assertRaisesLsstCpp(
            self, pexExcept.InvalidParameterException,
            lambda : afwMath.makeInterpolate(np.array([], dtype=float), np.array([], dtype=float),
                                             afwMath.Interpolate.CONSTANT)
            )

        interp = afwMath.makeInterpolate(np.array([0], dtype=float), np.array([1], dtype=float),
                                         afwMath.Interpolate.CONSTANT)

        utilsTests.assertRaisesLsstCpp(
            self, pexExcept.OutOfRangeException,
            lambda : afwMath.makeInterpolate(np.array([0], dtype=float), np.array([1], dtype=float),
                                             afwMath.Interpolate.LINEAR)
            )
Esempio n. 11
0
    def testLinearRamp(self):

        # === test the Linear Interpolator ============================
        # default is akima spline
        yinterpL = afwMath.makeInterpolate(self.x, self.y1)
        youtL = yinterpL.interpolate(self.xtest)

        self.assertEqual(youtL, self.y1test)
Esempio n. 12
0
 def testNaturalSplineRamp(self):
     
     # === test the Spline interpolator =======================
     # specify interp type with the string interface
     yinterpS = afwMath.makeInterpolate(self.x, self.y1, afwMath.Interpolate.NATURAL_SPLINE)
     youtS = yinterpS.interpolate(self.xtest)
     
     self.assertEqual(youtS, self.y1test)
Esempio n. 13
0
 def testNaturalSplineRamp(self):
     
     # === test the Spline interpolator =======================
     # specify interp type with the string interface
     yinterpS = afwMath.makeInterpolate(self.x, self.y1, afwMath.Interpolate.NATURAL_SPLINE)
     youtS = yinterpS.interpolate(self.xtest)
     
     self.assertEqual(youtS, self.y1test)
Esempio n. 14
0
    def testAkimaSplineParabola(self):
        """test the Spline interpolator"""
        # specify interp type with the enum style interface
        yinterpS = afwMath.makeInterpolate(self.x, self.y2,
                                           afwMath.Interpolate.AKIMA_SPLINE)
        youtS = yinterpS.interpolate(self.xtest)

        self.assertEqual(youtS, self.y2test)
Esempio n. 15
0
    def testLinearRamp(self):

        # === test the Linear Interpolator ============================
        # default is akima spline
        yinterpL = afwMath.makeInterpolate(self.x, self.y1)
        youtL = yinterpL.interpolate(self.xtest)

        self.assertEqual(youtL, self.y1test)
Esempio n. 16
0
    def splineFit(self, indices, collapsed, numBins):
        """Wrapper function to match spline fit API to polynomial fit API.

        Parameters
        ----------
        indices : `numpy.ndarray`
            Locations to evaluate the spline.
        collapsed : `numpy.ndarray`
            Collapsed overscan values corresponding to the spline
            evaluation points.
        numBins : `int`
            Number of bins to use in constructing the spline.

        Returns
        -------
        interp : `lsst.afw.math.Interpolate`
            Interpolation object for later evaluation.
        """
        if not np.ma.is_masked(collapsed):
            collapsed.mask = np.array(len(collapsed) * [np.ma.nomask])

        numPerBin, binEdges = np.histogram(indices,
                                           bins=numBins,
                                           weights=1 -
                                           collapsed.mask.astype(int))
        with np.errstate(invalid="ignore"):
            values = np.histogram(
                indices,
                bins=numBins,
                weights=collapsed.data * ~collapsed.mask)[0] / numPerBin
            binCenters = np.histogram(
                indices, bins=numBins,
                weights=indices * ~collapsed.mask)[0] / numPerBin

            if len(binCenters[numPerBin > 0]) < 5:
                self.log.warn(
                    "Cannot do spline fitting for overscan: %s valid points.",
                    len(binCenters[numPerBin > 0]))
                # Return a scalar value if we have one, otherwise
                # return zero.  This amplifier is hopefully already
                # masked.
                if len(values[numPerBin > 0]) != 0:
                    return float(values[numPerBin > 0][0])
                else:
                    return 0.0

            interp = afwMath.makeInterpolate(
                binCenters.astype(float)[numPerBin > 0],
                values.astype(float)[numPerBin > 0],
                afwMath.stringToInterpStyle(self.config.fitType))
        return interp
Esempio n. 17
0
    def testInvalidInputs(self):
        """Test that invalid inputs cause an abort"""

        with self.assertRaises(pexExcept.OutOfRangeError):
            afwMath.makeInterpolate(np.array([], dtype=float), np.array([], dtype=float),
                                    afwMath.Interpolate.CONSTANT)

        afwMath.makeInterpolate(np.array([0], dtype=float), np.array([1], dtype=float),
                                afwMath.Interpolate.CONSTANT)

        with self.assertRaises(pexExcept.OutOfRangeError):
            afwMath.makeInterpolate(np.array([0], dtype=float), np.array([1], dtype=float),
                                    afwMath.Interpolate.LINEAR)
Esempio n. 18
0
def interpolate1D(method, xSample, ySample, xInterp):
    """Interpolate in one dimension

    Interpolates the curve provided by `xSample` and `ySample` at
    the positions of `xInterp`. Automatically backs off the
    interpolation method to achieve successful interpolation.

    Parameters
    ----------
    method : `lsst.afw.math.Interpolate.Style`
        Interpolation method to use.
    xSample : `numpy.ndarray`
        Vector of ordinates.
    ySample : `numpy.ndarray`
        Vector of coordinates.
    xInterp : `numpy.ndarray`
        Vector of ordinates to which to interpolate.

    Returns
    -------
    yInterp : `numpy.ndarray`
        Vector of interpolated coordinates.

    """
    if len(xSample) == 0:
        return numpy.ones_like(xInterp) * numpy.nan
    try:
        return afwMath.makeInterpolate(xSample.astype(float),
                                       ySample.astype(float),
                                       method).interpolate(
                                           xInterp.astype(float))
    except Exception:
        if method == afwMath.Interpolate.CONSTANT:
            # We've already tried the most basic interpolation and it failed
            return numpy.ones_like(xInterp) * numpy.nan
        newMethod = afwMath.lookupMaxInterpStyle(len(xSample))
        if newMethod == method:
            newMethod = afwMath.Interpolate.CONSTANT
        return interpolate1D(newMethod, xSample, ySample, xInterp)
Esempio n. 19
0
    def splineFit(self, indices, collapsed, numBins):
        """Wrapper function to match spline fit API to polynomial fit API.

        Parameters
        ----------
        indices : `numpy.ndarray`
            Locations to evaluate the spline.
        collapsed : `numpy.ndarray`
            Collapsed overscan values corresponding to the spline
            evaluation points.
        numBins : `int`
            Number of bins to use in constructing the spline.

        Returns
        -------
        interp : `lsst.afw.math.Interpolate`
            Interpolation object for later evaluation.
        """
        if not np.ma.is_masked(collapsed):
            collapsed.mask = np.array(len(collapsed) * [np.ma.nomask])

        numPerBin, binEdges = np.histogram(indices,
                                           bins=numBins,
                                           weights=1 -
                                           collapsed.mask.astype(int))
        with np.errstate(invalid="ignore"):
            values = np.histogram(
                indices,
                bins=numBins,
                weights=collapsed.data * ~collapsed.mask)[0] / numPerBin
            binCenters = np.histogram(
                indices, bins=numBins,
                weights=indices * ~collapsed.mask)[0] / numPerBin
            interp = afwMath.makeInterpolate(
                binCenters.astype(float)[numPerBin > 0],
                values.astype(float)[numPerBin > 0],
                afwMath.stringToInterpStyle(self.config.fitType))
        return interp
Esempio n. 20
0
def overscanCorrection(ampMaskedImage, overscanImage, fitType='MEDIAN', order=1, collapseRej=3.0,
                       statControl=None):
    """Apply overscan correction in place

    @param[in,out] ampMaskedImage  masked image to correct
    @param[in] overscanImage  overscan data as an afw.image.Image or afw.image.MaskedImage.
                              If a masked image is passed in the mask plane will be used
                              to constrain the fit of the bias level.
    @param[in] fitType  type of fit for overscan correction; one of:
                        - 'MEAN'
                        - 'MEDIAN'
                        - 'POLY' (ordinary polynomial)
                        - 'CHEB' (Chebyshev polynomial)
                        - 'LEG' (Legendre polynomial)
                        - 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE' (splines)
    @param[in] order  polynomial order or spline knots (ignored unless fitType
                      indicates a polynomial or spline)
    @param[in] collapseRej  Rejection threshold (sigma) for collapsing dimension of overscan
    @param[in] statControl  Statistics control object
    """
    ampImage = ampMaskedImage.getImage()
    if statControl is None:
        statControl = afwMath.StatisticsControl()
    if fitType == 'MEAN':
        offImage = afwMath.makeStatistics(overscanImage, afwMath.MEAN, statControl).getValue(afwMath.MEAN)
    elif fitType == 'MEDIAN':
        offImage = afwMath.makeStatistics(overscanImage, afwMath.MEDIAN, statControl).getValue(afwMath.MEDIAN)
    elif fitType in ('POLY', 'CHEB', 'LEG', 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'):
        if hasattr(overscanImage, "getImage"):
            biasArray = overscanImage.getImage().getArray()
            biasArray = numpy.ma.masked_where(overscanImage.getMask().getArray() & statControl.getAndMask(),
                                              biasArray)
        else:
            biasArray = overscanImage.getArray()
        # Fit along the long axis, so collapse along each short row and fit the resulting array
        shortInd = numpy.argmin(biasArray.shape)
        if shortInd == 0:
            # Convert to some 'standard' representation to make things easier
            biasArray = numpy.transpose(biasArray)

        # Do a single round of clipping to weed out CR hits and signal leaking into the overscan
        percentiles = numpy.percentile(biasArray, [25.0, 50.0, 75.0], axis=1)
        medianBiasArr = percentiles[1]
        stdevBiasArr = 0.74*(percentiles[2] - percentiles[0]) # robust stdev
        diff = numpy.abs(biasArray - medianBiasArr[:,numpy.newaxis])
        biasMaskedArr = numpy.ma.masked_where(diff > collapseRej*stdevBiasArr[:,numpy.newaxis], biasArray)
        collapsed = numpy.mean(biasMaskedArr, axis=1)
        del biasArray, percentiles, stdevBiasArr, diff, biasMaskedArr

        if shortInd == 0:
            collapsed = numpy.transpose(collapsed)

        num = len(collapsed)
        indices = 2.0*numpy.arange(num)/float(num) - 1.0

        if fitType in ('POLY', 'CHEB', 'LEG'):
            # A numpy polynomial
            poly = numpy.polynomial
            fitter, evaler = {"POLY": (poly.polynomial.polyfit, poly.polynomial.polyval),
                              "CHEB": (poly.chebyshev.chebfit, poly.chebyshev.chebval),
                              "LEG":  (poly.legendre.legfit, poly.legendre.legval),
                              }[fitType]

            coeffs = fitter(indices, collapsed, order)
            fitBiasArr = evaler(indices, coeffs)
        elif 'SPLINE' in fitType:
            # An afw interpolation
            numBins = order
            #
            # numpy.histogram needs a real array for the mask, but numpy.ma "optimises" the case
            # no-values-are-masked by replacing the mask array by a scalar, numpy.ma.nomask
            #
            # Issue DM-415
            #
            collapsedMask = collapsed.mask
            try:
                if collapsedMask == numpy.ma.nomask:
                    collapsedMask = numpy.array(len(collapsed)*[numpy.ma.nomask])
            except ValueError:      # If collapsedMask is an array the test fails [needs .all()]
                pass

            numPerBin, binEdges = numpy.histogram(indices, bins=numBins,
                                                  weights=1-collapsedMask.astype(int))
            # Binning is just a histogram, with weights equal to the values.
            # Use a similar trick to get the bin centers (this deals with different numbers per bin).
            values = numpy.histogram(indices, bins=numBins, weights=collapsed)[0]/numPerBin
            binCenters = numpy.histogram(indices, bins=numBins, weights=indices)[0]/numPerBin
            interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
                                             values.astype(float)[numPerBin > 0],
                                             afwMath.stringToInterpStyle(fitType))
            fitBiasArr = numpy.array([interp.interpolate(i) for i in indices])

        import lsstDebug
        if lsstDebug.Info(__name__).display:
            import matplotlib.pyplot as plot
            figure = plot.figure(1)
            figure.clear()
            axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
            axes.plot(indices, collapsed, 'k+')
            axes.plot(indices, fitBiasArr, 'r-')
            figure.show()
            prompt = "Press Enter or c to continue [chp]... "
            while True:
                ans = raw_input(prompt).lower()
                if ans in ("", "c",):
                    break
                if ans in ("p",):
                    import pdb; pdb.set_trace()
                elif ans in ("h", ):
                    print "h[elp] c[ontinue] p[db]"
                figure.close()

        offImage = ampImage.Factory(ampImage.getDimensions())
        offArray = offImage.getArray()
        if shortInd == 1:
            offArray[:,:] = fitBiasArr[:,numpy.newaxis]
        else:
            offArray[:,:] = fitBiasArr[numpy.newaxis,:]

        # We don't trust any extrapolation: mask those pixels as SUSPECT
        # This will occur when the top and or bottom edges of the overscan
        # contain saturated values. The values will be extrapolated from
        # the surrounding pixels, but we cannot entirely trust the value of
        # the extrapolation, and will mark the image mask plane to flag the
        # image as such.
        mask = ampMaskedImage.getMask()
        maskArray = mask.getArray() if shortInd == 1 else mask.getArray().transpose()
        suspect = mask.getPlaneBitMask("SUSPECT")
        try:
            if collapsed.mask == numpy.ma.nomask:
                # There is no mask, so the whole array is fine
                pass
        except ValueError:      # If collapsed.mask is an array the test fails [needs .all()]
            for low in xrange(num):
                if not collapsed.mask[low]:
                    break
            if low > 0:
                maskArray[:low,:] |= suspect
            for high in xrange(1, num):
                if not collapsed.mask[-high]:
                    break
            if high > 1:
                maskArray[-high:,:] |= suspect

    else:
        raise pexExcept.Exception, '%s : %s an invalid overscan type' % \
            ("overscanCorrection", fitType)
    ampImage -= offImage
Esempio n. 21
0
def overscanCorrection(ampMaskedImage, overscanImage, fitType='MEDIAN', order=1, collapseRej=3.0,
                       statControl=None, overscanIsInt=True):
    """Apply overscan correction in place.

    Parameters
    ----------
    ampMaskedImage : `lsst.afw.image.MaskedImage`
        Image of amplifier to correct; modified.
    overscanImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
        Image of overscan; modified.
    fitType : `str`
        Type of fit for overscan correction. May be one of:

        - ``MEAN``: use mean of overscan.
        - ``MEANCLIP``: use clipped mean of overscan.
        - ``MEDIAN``: use median of overscan.
        - ``POLY``: fit with ordinary polynomial.
        - ``CHEB``: fit with Chebyshev polynomial.
        - ``LEG``: fit with Legendre polynomial.
        - ``NATURAL_SPLINE``: fit with natural spline.
        - ``CUBIC_SPLINE``: fit with cubic spline.
        - ``AKIMA_SPLINE``: fit with Akima spline.

    order : `int`
        Polynomial order or number of spline knots; ignored unless
        ``fitType`` indicates a polynomial or spline.
    statControl : `lsst.afw.math.StatisticsControl`
        Statistics control object.  In particular, we pay attention to numSigmaClip
    overscanIsInt : `bool`
        Treat the overscan region as consisting of integers, even if it's been
        converted to float.  E.g. handle ties properly.

    Returns
    -------
    result : `lsst.pipe.base.Struct`
        Result struct with components:

        - ``imageFit``: Value(s) removed from image (scalar or
            `lsst.afw.image.Image`)
        - ``overscanFit``: Value(s) removed from overscan (scalar or
            `lsst.afw.image.Image`)
        - ``overscanImage``: Overscan corrected overscan region
            (`lsst.afw.image.Image`)
    Raises
    ------
    pexExcept.Exception
        Raised if ``fitType`` is not an allowed value.

    Notes
    -----
    The ``ampMaskedImage`` and ``overscanImage`` are modified, with the fit
    subtracted. Note that the ``overscanImage`` should not be a subimage of
    the ``ampMaskedImage``, to avoid being subtracted twice.

    Debug plots are available for the SPLINE fitTypes by setting the
    `debug.display` for `name` == "lsst.ip.isr.isrFunctions".  These
    plots show the scatter plot of the overscan data (collapsed along
    the perpendicular dimension) as a function of position on the CCD
    (normalized between +/-1).
    """
    ampImage = ampMaskedImage.getImage()
    if statControl is None:
        statControl = afwMath.StatisticsControl()

    numSigmaClip = statControl.getNumSigmaClip()

    if fitType in ('MEAN', 'MEANCLIP'):
        fitType = afwMath.stringToStatisticsProperty(fitType)
        offImage = afwMath.makeStatistics(overscanImage, fitType, statControl).getValue()
        overscanFit = offImage
    elif fitType in ('MEDIAN',):
        if overscanIsInt:
            # we need an image with integer pixels to handle ties properly
            if hasattr(overscanImage, "image"):
                imageI = overscanImage.image.convertI()
                overscanImageI = afwImage.MaskedImageI(imageI, overscanImage.mask, overscanImage.variance)
            else:
                overscanImageI = overscanImage.convertI()
        else:
            overscanImageI = overscanImage

        fitType = afwMath.stringToStatisticsProperty(fitType)
        offImage = afwMath.makeStatistics(overscanImageI, fitType, statControl).getValue()
        overscanFit = offImage

        if overscanIsInt:
            del overscanImageI
    elif fitType in ('POLY', 'CHEB', 'LEG', 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE'):
        if hasattr(overscanImage, "getImage"):
            biasArray = overscanImage.getImage().getArray()
            biasArray = numpy.ma.masked_where(overscanImage.getMask().getArray() & statControl.getAndMask(),
                                              biasArray)
        else:
            biasArray = overscanImage.getArray()
        # Fit along the long axis, so collapse along each short row and fit the resulting array
        shortInd = numpy.argmin(biasArray.shape)
        if shortInd == 0:
            # Convert to some 'standard' representation to make things easier
            biasArray = numpy.transpose(biasArray)

        # Do a single round of clipping to weed out CR hits and signal leaking into the overscan
        percentiles = numpy.percentile(biasArray, [25.0, 50.0, 75.0], axis=1)
        medianBiasArr = percentiles[1]
        stdevBiasArr = 0.74*(percentiles[2] - percentiles[0])  # robust stdev
        diff = numpy.abs(biasArray - medianBiasArr[:, numpy.newaxis])
        biasMaskedArr = numpy.ma.masked_where(diff > numSigmaClip*stdevBiasArr[:, numpy.newaxis], biasArray)
        collapsed = numpy.mean(biasMaskedArr, axis=1)
        if collapsed.mask.sum() > 0:
            collapsed.data[collapsed.mask] = numpy.mean(biasArray.data[collapsed.mask], axis=1)
        del biasArray, percentiles, stdevBiasArr, diff, biasMaskedArr

        if shortInd == 0:
            collapsed = numpy.transpose(collapsed)

        num = len(collapsed)
        indices = 2.0*numpy.arange(num)/float(num) - 1.0

        if fitType in ('POLY', 'CHEB', 'LEG'):
            # A numpy polynomial
            poly = numpy.polynomial
            fitter, evaler = {"POLY": (poly.polynomial.polyfit, poly.polynomial.polyval),
                              "CHEB": (poly.chebyshev.chebfit, poly.chebyshev.chebval),
                              "LEG": (poly.legendre.legfit, poly.legendre.legval),
                              }[fitType]

            coeffs = fitter(indices, collapsed, order)
            fitBiasArr = evaler(indices, coeffs)
        elif 'SPLINE' in fitType:
            # An afw interpolation
            numBins = order
            #
            # numpy.histogram needs a real array for the mask, but numpy.ma "optimises" the case
            # no-values-are-masked by replacing the mask array by a scalar, numpy.ma.nomask
            #
            # Issue DM-415
            #
            collapsedMask = collapsed.mask
            try:
                if collapsedMask == numpy.ma.nomask:
                    collapsedMask = numpy.array(len(collapsed)*[numpy.ma.nomask])
            except ValueError:      # If collapsedMask is an array the test fails [needs .all()]
                pass

            numPerBin, binEdges = numpy.histogram(indices, bins=numBins,
                                                  weights=1-collapsedMask.astype(int))
            # Binning is just a histogram, with weights equal to the values.
            # Use a similar trick to get the bin centers (this deals with different numbers per bin).
            with numpy.errstate(invalid="ignore"):  # suppress NAN warnings
                values = numpy.histogram(indices, bins=numBins,
                                         weights=collapsed.data*~collapsedMask)[0]/numPerBin
                binCenters = numpy.histogram(indices, bins=numBins,
                                             weights=indices*~collapsedMask)[0]/numPerBin
                interp = afwMath.makeInterpolate(binCenters.astype(float)[numPerBin > 0],
                                                 values.astype(float)[numPerBin > 0],
                                                 afwMath.stringToInterpStyle(fitType))
            fitBiasArr = numpy.array([interp.interpolate(i) for i in indices])

        import lsstDebug
        if lsstDebug.Info(__name__).display:
            import matplotlib.pyplot as plot
            figure = plot.figure(1)
            figure.clear()
            axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
            axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 'k+')
            if collapsedMask.sum() > 0:
                axes.plot(indices[collapsedMask], collapsed.data[collapsedMask], 'b+')
            axes.plot(indices, fitBiasArr, 'r-')
            plot.xlabel("centered/scaled position along overscan region")
            plot.ylabel("pixel value/fit value")
            figure.show()
            prompt = "Press Enter or c to continue [chp]... "
            while True:
                ans = input(prompt).lower()
                if ans in ("", "c",):
                    break
                if ans in ("p",):
                    import pdb
                    pdb.set_trace()
                elif ans in ("h", ):
                    print("h[elp] c[ontinue] p[db]")
            plot.close()

        offImage = ampImage.Factory(ampImage.getDimensions())
        offArray = offImage.getArray()
        overscanFit = afwImage.ImageF(overscanImage.getDimensions())
        overscanArray = overscanFit.getArray()
        if shortInd == 1:
            offArray[:, :] = fitBiasArr[:, numpy.newaxis]
            overscanArray[:, :] = fitBiasArr[:, numpy.newaxis]
        else:
            offArray[:, :] = fitBiasArr[numpy.newaxis, :]
            overscanArray[:, :] = fitBiasArr[numpy.newaxis, :]

        # We don't trust any extrapolation: mask those pixels as SUSPECT
        # This will occur when the top and or bottom edges of the overscan
        # contain saturated values. The values will be extrapolated from
        # the surrounding pixels, but we cannot entirely trust the value of
        # the extrapolation, and will mark the image mask plane to flag the
        # image as such.
        mask = ampMaskedImage.getMask()
        maskArray = mask.getArray() if shortInd == 1 else mask.getArray().transpose()
        suspect = mask.getPlaneBitMask("SUSPECT")
        try:
            if collapsed.mask == numpy.ma.nomask:
                # There is no mask, so the whole array is fine
                pass
        except ValueError:      # If collapsed.mask is an array the test fails [needs .all()]
            for low in range(num):
                if not collapsed.mask[low]:
                    break
            if low > 0:
                maskArray[:low, :] |= suspect
            for high in range(1, num):
                if not collapsed.mask[-high]:
                    break
            if high > 1:
                maskArray[-high:, :] |= suspect

    else:
        raise pexExcept.Exception('%s : %s an invalid overscan type' % ("overscanCorrection", fitType))
    ampImage -= offImage
    overscanImage -= overscanFit
    return Struct(imageFit=offImage, overscanFit=overscanFit, overscanImage=overscanImage)
Esempio n. 22
0
def getDistanceFromFocus(dIcSrc,
                         dCcd,
                         dCcdDims,
                         zemaxFilename,
                         config,
                         plotFilename=None):
    # Focus error is measured by using rms^2 of stars on focus CCDs.
    # If there is a focus error d, rms^2 can be written as
    # rms^2 = rms_atm^2 + rms_opt_0^2 + alpha*d^2,
    # where rms_atm is from atmosphere and rms_opt if from optics with out any focus error.
    # On the focus CCDs which have +/-delta offset, the equation becomes
    # rms_+^2 = rms_atm^2 + rms_opt_0^2 + alpha(d+delta)^2
    # rms_-^2 = rms_atm^2 + rms_opt_0^2 + alpha(d-delta)^2
    # Thus, the difference of these rms^2 gives the focus error as
    # d = (rms_+^2 - rms_-^2)/(4 alpha delta)
    # alpha is determined by ZEMAX simulations. It turned out that alpha is a function of distance from the center of FOV r.
    # Also the best focus varies as a function of r. Thus the focus error can be rewritten as
    # d(r) = (rms_+(r)^2 - rms_-(r)^2)/(4 alpha(r) delta) + d0(r)
    # I take a pair of CCDs on the corner, divide the focus CCDs into radian bins, calculate focus error d for each radial bin with alpha and d0 values at this radius, and then take median of these focus errors for all the radian bins and CCD pairs.
    # rms^2 is measured by shape.simple. Although I intend to include minimum measurement bias, there exists still some bias. This is corrected by getCorrectedFocusError() at the end, which is a polynomial function derived by calibration data (well-behaved focus sweeps).

    # set up radial bins
    lRadialBinEdges = config.radialBinEdges
    lRadialBinCenters = config.radialBinCenters
    lRadialBinsLowerEdges = lRadialBinEdges[0:-1]
    lRadialBinsUpperEdges = lRadialBinEdges[1:]

    # make selection on data and get rms^2 for each bin, CCD by CCD
    dlRmssq = dict(
    )  # rmssq list for radial bin, which is dictionary for each ccd

    for ccdId in dIcSrc.keys():
        # use only objects classified as PSF candidate
        icSrc = dIcSrc[ccdId][dIcSrc[ccdId].get("hscPipeline_focus_candidate")]

        # prepare for getting distance from center for each object
        ccd = dCcd[ccdId]
        x1, y1 = dCcdDims[ccdId]
        # Get focal plane position in pixels
        # Note that we constructed the zemax values alpha(r), d0(r), and this r is in pixel.
        transform = ccd.getTransformMap().get(
            ccd.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
        uLlc, vLlc = transform.forwardTransform(afwGeom.PointD(0., 0.))
        uLrc, vLrc = transform.forwardTransform(afwGeom.PointD(x1, 0.))
        uUlc, vUlc = transform.forwardTransform(afwGeom.PointD(0., y1))
        uUrc, vUrc = transform.forwardTransform(afwGeom.PointD(x1, y1))

        lDistanceFromCenter = list()
        lRmssq = list()
        for s in icSrc:
            # reject blended objects
            if len(s.getFootprint().getPeaks()) != 1:
                continue

            # calculate distance from center for each objects
            x = s.getX()
            y = s.getY()

            uL = (uLrc - uLlc) / x1 * x + uLlc
            uU = (uUrc - uUlc) / x1 * x + uUlc
            u = (uU - uL) / y1 * y + uL

            vL = (vLrc - vLlc) / x1 * x + vLlc
            vU = (vUrc - vUlc) / x1 * x + vUlc
            v = (vU - vL) / y1 * y + vL
            lDistanceFromCenter.append(np.sqrt(u**2 + v**2))

            # calculate rms^2
            ixx = s.get(config.shape + "_xx")
            iyy = s.get(config.shape + "_yy")
            lRmssq.append((ixx + iyy) *
                          config.pixelScale**2)  # convert from pixel^2 to mm^2

        # calculate median rms^2 for each radial bin
        lDistanceFromCenter = np.array(lDistanceFromCenter)
        lRmssq = np.array(lRmssq)
        lRmssqMedian = list()
        for radialBinLowerEdge, radialBinUpperEdge in zip(
                lRadialBinsLowerEdges, lRadialBinsUpperEdges):
            sel = np.logical_and(lDistanceFromCenter > radialBinLowerEdge,
                                 lDistanceFromCenter < radialBinUpperEdge)
            lRmssqMedian.append(np.median(lRmssq[sel]))
        dlRmssq[ccdId] = np.ma.masked_array(lRmssqMedian,
                                            mask=np.isnan(lRmssqMedian))

    # get ZEMAX values
    d = np.loadtxt(zemaxFilename)

    interpStyle = afwMath.stringToInterpStyle("NATURAL_SPLINE")
    sAlpha = afwMath.makeInterpolate(d[:, 0], d[:, 1], interpStyle).interpolate
    sD0 = afwMath.makeInterpolate(d[:, 0], d[:, 2], interpStyle).interpolate

    # calculate rms^2 for each CCD pair
    lCcdPairs = zip(config.belowList, config.aboveList)
    llFocurErrors = list()
    for ccdPair in lCcdPairs:
        lFocusErrors = list()
        if (ccdPair[0] not in dlRmssq or ccdPair[1] not in dlRmssq
                or dlRmssq[ccdPair[0]] is None or dlRmssq[ccdPair[1]] is None):
            continue
        for i, radialBinCenter in enumerate(lRadialBinCenters):
            rmssqAbove = dlRmssq[ccdPair[1]][i]
            rmssqBelow = dlRmssq[ccdPair[0]][i]
            rmssqDiff = rmssqAbove - rmssqBelow
            delta = getFocusCcdOffset(ccdPair[1], config)
            alpha = sAlpha(radialBinCenter)
            focusError = rmssqDiff / 4. / alpha / delta + sD0(radialBinCenter)
            lFocusErrors.append(focusError)
        llFocurErrors.append(np.array(lFocusErrors))

    llFocurErrors = np.ma.masked_array(llFocurErrors,
                                       mask=np.isnan(llFocurErrors))
    reconstructedFocusError = np.ma.median(llFocurErrors)
    n = np.sum(np.invert(llFocurErrors.mask))
    reconstructedFocusErrorStd = np.ma.std(llFocurErrors) * np.sqrt(
        np.pi / 2.) / np.sqrt(n)

    if config.doPlot == True:
        if not plotFilename:
            raise ValueError("no filename for focus plot")
        import matplotlib
        matplotlib.use("Agg")
        import matplotlib.pyplot as plt

        lMarker = ["o", "x", "d", "^", "<", ">"]
        lColor = ["blue", "green", "red", "cyan", "magenta", "yellow"]
        for i, ccdPair in enumerate(lCcdPairs):
            delta_plot = np.ma.masked_array([
                getFocusCcdOffset(ccdPair[0], config),
                getFocusCcdOffset(ccdPair[1], config)
            ])
            rmssq_plot = np.ma.masked_array(
                [dlRmssq[ccdPair[0]], dlRmssq[ccdPair[1]]])
            for j in range(len(lRadialBinCenters)):
                plt.plot(delta_plot,
                         rmssq_plot[:, j],
                         "%s--" % lMarker[i],
                         color=lColor[j])
        plt.savefig(plotFilename)

    correctedFocusError, correctedFocusErrorStd = getCorrectedFocusError(
        reconstructedFocusError, reconstructedFocusErrorStd, config.corrCoeff)
    return (correctedFocusError[0], correctedFocusErrorStd[0],
            reconstructedFocusError[0], reconstructedFocusErrorStd, n)
Esempio n. 23
0
def overscanCorrection(ampMaskedImage,
                       overscanImage,
                       fitType='MEDIAN',
                       order=1,
                       collapseRej=3.0,
                       statControl=None):
    """Apply overscan correction in place

    @param[in,out] ampMaskedImage  masked image to correct
    @param[in] overscanImage  overscan data as an afw.image.Image or afw.image.MaskedImage.
                              If a masked image is passed in the mask plane will be used
                              to constrain the fit of the bias level.
    @param[in] fitType  type of fit for overscan correction; one of:
                        - 'MEAN'
                        - 'MEDIAN'
                        - 'POLY' (ordinary polynomial)
                        - 'CHEB' (Chebyshev polynomial)
                        - 'LEG' (Legendre polynomial)
                        - 'NATURAL_SPLINE', 'CUBIC_SPLINE', 'AKIMA_SPLINE' (splines)
    @param[in] order  polynomial order or spline knots (ignored unless fitType
                      indicates a polynomial or spline)
    @param[in] collapseRej  Rejection threshold (sigma) for collapsing dimension of overscan
    @param[in] statControl  Statistics control object
    """
    ampImage = ampMaskedImage.getImage()
    if statControl is None:
        statControl = afwMath.StatisticsControl()
    if fitType == 'MEAN':
        offImage = afwMath.makeStatistics(overscanImage, afwMath.MEAN,
                                          statControl).getValue(afwMath.MEAN)
    elif fitType == 'MEDIAN':
        offImage = afwMath.makeStatistics(overscanImage, afwMath.MEDIAN,
                                          statControl).getValue(afwMath.MEDIAN)
    elif fitType in ('POLY', 'CHEB', 'LEG', 'NATURAL_SPLINE', 'CUBIC_SPLINE',
                     'AKIMA_SPLINE'):
        if hasattr(overscanImage, "getImage"):
            biasArray = overscanImage.getImage().getArray()
            biasArray = numpy.ma.masked_where(
                overscanImage.getMask().getArray() & statControl.getAndMask(),
                biasArray)
        else:
            biasArray = overscanImage.getArray()
        # Fit along the long axis, so collapse along each short row and fit the resulting array
        shortInd = numpy.argmin(biasArray.shape)
        if shortInd == 0:
            # Convert to some 'standard' representation to make things easier
            biasArray = numpy.transpose(biasArray)

        # Do a single round of clipping to weed out CR hits and signal leaking into the overscan
        percentiles = numpy.percentile(biasArray, [25.0, 50.0, 75.0], axis=1)
        medianBiasArr = percentiles[1]
        stdevBiasArr = 0.74 * (percentiles[2] - percentiles[0])  # robust stdev
        diff = numpy.abs(biasArray - medianBiasArr[:, numpy.newaxis])
        biasMaskedArr = numpy.ma.masked_where(
            diff > collapseRej * stdevBiasArr[:, numpy.newaxis], biasArray)
        collapsed = numpy.mean(biasMaskedArr, axis=1)
        if collapsed.mask.sum() > 0:
            collapsed.data[collapsed.mask] = numpy.mean(
                biasArray.data[collapsed.mask], axis=1)
        del biasArray, percentiles, stdevBiasArr, diff, biasMaskedArr

        if shortInd == 0:
            collapsed = numpy.transpose(collapsed)

        num = len(collapsed)
        indices = 2.0 * numpy.arange(num) / float(num) - 1.0

        if fitType in ('POLY', 'CHEB', 'LEG'):
            # A numpy polynomial
            poly = numpy.polynomial
            fitter, evaler = {
                "POLY": (poly.polynomial.polyfit, poly.polynomial.polyval),
                "CHEB": (poly.chebyshev.chebfit, poly.chebyshev.chebval),
                "LEG": (poly.legendre.legfit, poly.legendre.legval),
            }[fitType]

            coeffs = fitter(indices, collapsed, order)
            fitBiasArr = evaler(indices, coeffs)
        elif 'SPLINE' in fitType:
            # An afw interpolation
            numBins = order
            #
            # numpy.histogram needs a real array for the mask, but numpy.ma "optimises" the case
            # no-values-are-masked by replacing the mask array by a scalar, numpy.ma.nomask
            #
            # Issue DM-415
            #
            collapsedMask = collapsed.mask
            try:
                if collapsedMask == numpy.ma.nomask:
                    collapsedMask = numpy.array(
                        len(collapsed) * [numpy.ma.nomask])
            except ValueError:  # If collapsedMask is an array the test fails [needs .all()]
                pass

            numPerBin, binEdges = numpy.histogram(indices,
                                                  bins=numBins,
                                                  weights=1 -
                                                  collapsedMask.astype(int))
            # Binning is just a histogram, with weights equal to the values.
            # Use a similar trick to get the bin centers (this deals with different numbers per bin).
            values = numpy.histogram(
                indices, bins=numBins,
                weights=collapsed.data * ~collapsedMask)[0] / numPerBin
            binCenters = numpy.histogram(
                indices, bins=numBins,
                weights=indices * ~collapsedMask)[0] / numPerBin
            interp = afwMath.makeInterpolate(
                binCenters.astype(float)[numPerBin > 0],
                values.astype(float)[numPerBin > 0],
                afwMath.stringToInterpStyle(fitType))
            fitBiasArr = numpy.array([interp.interpolate(i) for i in indices])

        import lsstDebug
        if lsstDebug.Info(__name__).display:
            import matplotlib.pyplot as plot
            figure = plot.figure(1)
            figure.clear()
            axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
            axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 'k+')
            if collapsedMask.sum() > 0:
                axes.plot(indices[collapsedMask],
                          collapsed.data[collapsedMask], 'b+')
            axes.plot(indices, fitBiasArr, 'r-')
            figure.show()
            prompt = "Press Enter or c to continue [chp]... "
            while True:
                ans = input(prompt).lower()
                if ans in (
                        "",
                        "c",
                ):
                    break
                if ans in ("p", ):
                    import pdb
                    pdb.set_trace()
                elif ans in ("h", ):
                    print("h[elp] c[ontinue] p[db]")
                figure.close()

        offImage = ampImage.Factory(ampImage.getDimensions())
        offArray = offImage.getArray()
        if shortInd == 1:
            offArray[:, :] = fitBiasArr[:, numpy.newaxis]
        else:
            offArray[:, :] = fitBiasArr[numpy.newaxis, :]

        # We don't trust any extrapolation: mask those pixels as SUSPECT
        # This will occur when the top and or bottom edges of the overscan
        # contain saturated values. The values will be extrapolated from
        # the surrounding pixels, but we cannot entirely trust the value of
        # the extrapolation, and will mark the image mask plane to flag the
        # image as such.
        mask = ampMaskedImage.getMask()
        maskArray = mask.getArray() if shortInd == 1 else mask.getArray(
        ).transpose()
        suspect = mask.getPlaneBitMask("SUSPECT")
        try:
            if collapsed.mask == numpy.ma.nomask:
                # There is no mask, so the whole array is fine
                pass
        except ValueError:  # If collapsed.mask is an array the test fails [needs .all()]
            for low in range(num):
                if not collapsed.mask[low]:
                    break
            if low > 0:
                maskArray[:low, :] |= suspect
            for high in range(1, num):
                if not collapsed.mask[-high]:
                    break
            if high > 1:
                maskArray[-high:, :] |= suspect

    else:
        raise pexExcept.Exception('%s : %s an invalid overscan type' % \
            ("overscanCorrection", fitType))
    ampImage -= offImage
Esempio n. 24
0
def overscanCorrection(ampMaskedImage,
                       overscanImage,
                       fitType='MEDIAN',
                       order=1,
                       collapseRej=3.0,
                       statControl=None,
                       overscanIsInt=True):
    """Apply overscan correction in place.

    Parameters
    ----------
    ampMaskedImage : `lsst.afw.image.MaskedImage`
        Image of amplifier to correct; modified.
    overscanImage : `lsst.afw.image.Image` or `lsst.afw.image.MaskedImage`
        Image of overscan; modified.
    fitType : `str`
        Type of fit for overscan correction. May be one of:

        - ``MEAN``: use mean of overscan.
        - ``MEANCLIP``: use clipped mean of overscan.
        - ``MEDIAN``: use median of overscan.
        - ``POLY``: fit with ordinary polynomial.
        - ``CHEB``: fit with Chebyshev polynomial.
        - ``LEG``: fit with Legendre polynomial.
        - ``NATURAL_SPLINE``: fit with natural spline.
        - ``CUBIC_SPLINE``: fit with cubic spline.
        - ``AKIMA_SPLINE``: fit with Akima spline.

    order : `int`
        Polynomial order or number of spline knots; ignored unless
        ``fitType`` indicates a polynomial or spline.
    statControl : `lsst.afw.math.StatisticsControl`
        Statistics control object.  In particular, we pay attention to numSigmaClip
    overscanIsInt : `bool`
        Treat the overscan region as consisting of integers, even if it's been
        converted to float.  E.g. handle ties properly.

    Returns
    -------
    result : `lsst.pipe.base.Struct`
        Result struct with components:

        - ``imageFit``: Value(s) removed from image (scalar or
            `lsst.afw.image.Image`)
        - ``overscanFit``: Value(s) removed from overscan (scalar or
            `lsst.afw.image.Image`)
        - ``overscanImage``: Overscan corrected overscan region
            (`lsst.afw.image.Image`)
    Raises
    ------
    pexExcept.Exception
        Raised if ``fitType`` is not an allowed value.

    Notes
    -----
    The ``ampMaskedImage`` and ``overscanImage`` are modified, with the fit
    subtracted. Note that the ``overscanImage`` should not be a subimage of
    the ``ampMaskedImage``, to avoid being subtracted twice.

    Debug plots are available for the SPLINE fitTypes by setting the
    `debug.display` for `name` == "lsst.ip.isr.isrFunctions".  These
    plots show the scatter plot of the overscan data (collapsed along
    the perpendicular dimension) as a function of position on the CCD
    (normalized between +/-1).
    """
    ampImage = ampMaskedImage.getImage()
    if statControl is None:
        statControl = afwMath.StatisticsControl()

    numSigmaClip = statControl.getNumSigmaClip()

    if fitType in ('MEAN', 'MEANCLIP'):
        fitType = afwMath.stringToStatisticsProperty(fitType)
        offImage = afwMath.makeStatistics(overscanImage, fitType,
                                          statControl).getValue()
        overscanFit = offImage
    elif fitType in ('MEDIAN', ):
        if overscanIsInt:
            # we need an image with integer pixels to handle ties properly
            if hasattr(overscanImage, "image"):
                imageI = overscanImage.image.convertI()
                overscanImageI = afwImage.MaskedImageI(imageI,
                                                       overscanImage.mask,
                                                       overscanImage.variance)
            else:
                overscanImageI = overscanImage.convertI()
        else:
            overscanImageI = overscanImage

        fitType = afwMath.stringToStatisticsProperty(fitType)
        offImage = afwMath.makeStatistics(overscanImageI, fitType,
                                          statControl).getValue()
        overscanFit = offImage

        if overscanIsInt:
            del overscanImageI
    elif fitType in ('POLY', 'CHEB', 'LEG', 'NATURAL_SPLINE', 'CUBIC_SPLINE',
                     'AKIMA_SPLINE'):
        if hasattr(overscanImage, "getImage"):
            biasArray = overscanImage.getImage().getArray()
            biasArray = numpy.ma.masked_where(
                overscanImage.getMask().getArray() & statControl.getAndMask(),
                biasArray)
        else:
            biasArray = overscanImage.getArray()
        # Fit along the long axis, so collapse along each short row and fit the resulting array
        shortInd = numpy.argmin(biasArray.shape)
        if shortInd == 0:
            # Convert to some 'standard' representation to make things easier
            biasArray = numpy.transpose(biasArray)

        # Do a single round of clipping to weed out CR hits and signal leaking into the overscan
        percentiles = numpy.percentile(biasArray, [25.0, 50.0, 75.0], axis=1)
        medianBiasArr = percentiles[1]
        stdevBiasArr = 0.74 * (percentiles[2] - percentiles[0])  # robust stdev
        diff = numpy.abs(biasArray - medianBiasArr[:, numpy.newaxis])
        biasMaskedArr = numpy.ma.masked_where(
            diff > numSigmaClip * stdevBiasArr[:, numpy.newaxis], biasArray)
        collapsed = numpy.mean(biasMaskedArr, axis=1)
        if collapsed.mask.sum() > 0:
            collapsed.data[collapsed.mask] = numpy.mean(
                biasArray.data[collapsed.mask], axis=1)
        del biasArray, percentiles, stdevBiasArr, diff, biasMaskedArr

        if shortInd == 0:
            collapsed = numpy.transpose(collapsed)

        num = len(collapsed)
        indices = 2.0 * numpy.arange(num) / float(num) - 1.0

        if fitType in ('POLY', 'CHEB', 'LEG'):
            # A numpy polynomial
            poly = numpy.polynomial
            fitter, evaler = {
                "POLY": (poly.polynomial.polyfit, poly.polynomial.polyval),
                "CHEB": (poly.chebyshev.chebfit, poly.chebyshev.chebval),
                "LEG": (poly.legendre.legfit, poly.legendre.legval),
            }[fitType]

            coeffs = fitter(indices, collapsed, order)
            fitBiasArr = evaler(indices, coeffs)
        elif 'SPLINE' in fitType:
            # An afw interpolation
            numBins = order
            #
            # numpy.histogram needs a real array for the mask, but numpy.ma "optimises" the case
            # no-values-are-masked by replacing the mask array by a scalar, numpy.ma.nomask
            #
            # Issue DM-415
            #
            collapsedMask = collapsed.mask
            try:
                if collapsedMask == numpy.ma.nomask:
                    collapsedMask = numpy.array(
                        len(collapsed) * [numpy.ma.nomask])
            except ValueError:  # If collapsedMask is an array the test fails [needs .all()]
                pass

            numPerBin, binEdges = numpy.histogram(indices,
                                                  bins=numBins,
                                                  weights=1 -
                                                  collapsedMask.astype(int))
            # Binning is just a histogram, with weights equal to the values.
            # Use a similar trick to get the bin centers (this deals with different numbers per bin).
            with numpy.errstate(invalid="ignore"):  # suppress NAN warnings
                values = numpy.histogram(
                    indices,
                    bins=numBins,
                    weights=collapsed.data * ~collapsedMask)[0] / numPerBin
                binCenters = numpy.histogram(
                    indices, bins=numBins,
                    weights=indices * ~collapsedMask)[0] / numPerBin
                interp = afwMath.makeInterpolate(
                    binCenters.astype(float)[numPerBin > 0],
                    values.astype(float)[numPerBin > 0],
                    afwMath.stringToInterpStyle(fitType))
            fitBiasArr = numpy.array([interp.interpolate(i) for i in indices])

        import lsstDebug
        if lsstDebug.Info(__name__).display:
            import matplotlib.pyplot as plot
            figure = plot.figure(1)
            figure.clear()
            axes = figure.add_axes((0.1, 0.1, 0.8, 0.8))
            axes.plot(indices[~collapsedMask], collapsed[~collapsedMask], 'k+')
            if collapsedMask.sum() > 0:
                axes.plot(indices[collapsedMask],
                          collapsed.data[collapsedMask], 'b+')
            axes.plot(indices, fitBiasArr, 'r-')
            plot.xlabel("centered/scaled position along overscan region")
            plot.ylabel("pixel value/fit value")
            figure.show()
            prompt = "Press Enter or c to continue [chp]... "
            while True:
                ans = input(prompt).lower()
                if ans in (
                        "",
                        "c",
                ):
                    break
                if ans in ("p", ):
                    import pdb
                    pdb.set_trace()
                elif ans in ("h", ):
                    print("h[elp] c[ontinue] p[db]")
            plot.close()

        offImage = ampImage.Factory(ampImage.getDimensions())
        offArray = offImage.getArray()
        overscanFit = afwImage.ImageF(overscanImage.getDimensions())
        overscanArray = overscanFit.getArray()
        if shortInd == 1:
            offArray[:, :] = fitBiasArr[:, numpy.newaxis]
            overscanArray[:, :] = fitBiasArr[:, numpy.newaxis]
        else:
            offArray[:, :] = fitBiasArr[numpy.newaxis, :]
            overscanArray[:, :] = fitBiasArr[numpy.newaxis, :]

        # We don't trust any extrapolation: mask those pixels as SUSPECT
        # This will occur when the top and or bottom edges of the overscan
        # contain saturated values. The values will be extrapolated from
        # the surrounding pixels, but we cannot entirely trust the value of
        # the extrapolation, and will mark the image mask plane to flag the
        # image as such.
        mask = ampMaskedImage.getMask()
        maskArray = mask.getArray() if shortInd == 1 else mask.getArray(
        ).transpose()
        suspect = mask.getPlaneBitMask("SUSPECT")
        try:
            if collapsed.mask == numpy.ma.nomask:
                # There is no mask, so the whole array is fine
                pass
        except ValueError:  # If collapsed.mask is an array the test fails [needs .all()]
            for low in range(num):
                if not collapsed.mask[low]:
                    break
            if low > 0:
                maskArray[:low, :] |= suspect
            for high in range(1, num):
                if not collapsed.mask[-high]:
                    break
            if high > 1:
                maskArray[-high:, :] |= suspect

    else:
        raise pexExcept.Exception('%s : %s an invalid overscan type' %
                                  ("overscanCorrection", fitType))
    ampImage -= offImage
    overscanImage -= overscanFit
    return Struct(imageFit=offImage,
                  overscanFit=overscanFit,
                  overscanImage=overscanImage)
Esempio n. 25
0
def getDistanceFromFocus(dIcSrc, dCcd, dCcdDims, zemaxFilename, config, plotFilename=None):
    # Focus error is measured by using rms^2 of stars on focus CCDs.
    # If there is a focus error d, rms^2 can be written as
    # rms^2 = rms_atm^2 + rms_opt_0^2 + alpha*d^2,
    # where rms_atm is from atmosphere and rms_opt if from optics with out any focus error. 
    # On the focus CCDs which have +/-delta offset, the equation becomes
    # rms_+^2 = rms_atm^2 + rms_opt_0^2 + alpha(d+delta)^2
    # rms_-^2 = rms_atm^2 + rms_opt_0^2 + alpha(d-delta)^2
    # Thus, the difference of these rms^2 gives the focus error as
    # d = (rms_+^2 - rms_-^2)/(4 alpha delta)
    # alpha is determined by ZEMAX simulations. It turned out that alpha is a function of distance from the center of FOV r.
    # Also the best focus varies as a function of r. Thus the focus error can be rewritten as
    # d(r) = (rms_+(r)^2 - rms_-(r)^2)/(4 alpha(r) delta) + d0(r)
    # I take a pair of CCDs on the corner, divide the focus CCDs into radian bins, calculate focus error d for each radial bin with alpha and d0 values at this radius, and then take median of these focus errors for all the radian bins and CCD pairs.
    # rms^2 is measured by shape.simple. Although I intend to include minimum measurement bias, there exists still some bias. This is corrected by getCorrectedFocusError() at the end, which is a polynomial function derived by calibration data (well-behaved focus sweeps).

    # set up radial bins
    lRadialBinEdges = config.radialBinEdges
    lRadialBinCenters = config.radialBinCenters
    lRadialBinsLowerEdges = lRadialBinEdges[0:-1]
    lRadialBinsUpperEdges = lRadialBinEdges[1:]

    # make selection on data and get rms^2 for each bin, CCD by CCD
    dlRmssq = dict() # rmssq list for radial bin, which is dictionary for each ccd

    for ccdId in dIcSrc.keys():
        # use only objects classified as PSF candidate
        icSrc = dIcSrc[ccdId][dIcSrc[ccdId].get("hscPipeline_focus_candidate")]

        # prepare for getting distance from center for each object
        ccd = dCcd[ccdId]
        x1, y1 = dCcdDims[ccdId]
        # Get focal plane position in pixels
        # Note that we constructed the zemax values alpha(r), d0(r), and this r is in pixel.
        transform = ccd.getTransformMap().get(ccd.makeCameraSys(afwCameraGeom.FOCAL_PLANE))
        uLlc, vLlc = transform.forwardTransform(afwGeom.PointD(0., 0.))
        uLrc, vLrc = transform.forwardTransform(afwGeom.PointD(x1, 0.))
        uUlc, vUlc = transform.forwardTransform(afwGeom.PointD(0., y1))
        uUrc, vUrc = transform.forwardTransform(afwGeom.PointD(x1, y1))

        lDistanceFromCenter = list()
        lRmssq = list()
        for s in icSrc:
            # reject blended objects
            if len(s.getFootprint().getPeaks()) != 1:
                continue

            # calculate distance from center for each objects
            x = s.getX()
            y = s.getY()

            uL = (uLrc-uLlc)/x1*x+uLlc
            uU = (uUrc-uUlc)/x1*x+uUlc
            u = (uU-uL)/y1*y+uL

            vL = (vLrc-vLlc)/x1*x+vLlc
            vU = (vUrc-vUlc)/x1*x+vUlc
            v = (vU-vL)/y1*y+vL
            lDistanceFromCenter.append(np.sqrt(u**2 + v**2))

            # calculate rms^2
            ixx = s.get(config.shape + "_xx")
            iyy = s.get(config.shape + "_yy")
            lRmssq.append((ixx + iyy)*config.pixelScale**2) # convert from pixel^2 to mm^2

        # calculate median rms^2 for each radial bin
        lDistanceFromCenter = np.array(lDistanceFromCenter)
        lRmssq = np.array(lRmssq)
        lRmssqMedian = list()
        for radialBinLowerEdge, radialBinUpperEdge in zip(lRadialBinsLowerEdges, lRadialBinsUpperEdges):
            sel = np.logical_and(lDistanceFromCenter > radialBinLowerEdge, lDistanceFromCenter < radialBinUpperEdge)
            lRmssqMedian.append(np.median(lRmssq[sel]))
        dlRmssq[ccdId] = np.ma.masked_array(lRmssqMedian, mask = np.isnan(lRmssqMedian))

    # get ZEMAX values
    d = np.loadtxt(zemaxFilename)

    interpStyle = afwMath.stringToInterpStyle("NATURAL_SPLINE")
    sAlpha = afwMath.makeInterpolate(d[:,0], d[:,1], interpStyle).interpolate
    sD0 = afwMath.makeInterpolate(d[:,0], d[:,2], interpStyle).interpolate

    # calculate rms^2 for each CCD pair
    lCcdPairs = zip(config.belowList, config.aboveList)
    llFocurErrors = list()
    for ccdPair in lCcdPairs:
        lFocusErrors = list()
        if (ccdPair[0] not in dlRmssq or ccdPair[1] not in dlRmssq or
            dlRmssq[ccdPair[0]] is None or dlRmssq[ccdPair[1]] is None):
            continue
        for i, radialBinCenter in enumerate(lRadialBinCenters):
            rmssqAbove = dlRmssq[ccdPair[1]][i]
            rmssqBelow = dlRmssq[ccdPair[0]][i]
            rmssqDiff = rmssqAbove - rmssqBelow
            delta = getFocusCcdOffset(ccdPair[1], config)
            alpha = sAlpha(radialBinCenter)
            focusError = rmssqDiff/4./alpha/delta + sD0(radialBinCenter)
            lFocusErrors.append(focusError)
        llFocurErrors.append(np.array(lFocusErrors))

    llFocurErrors = np.ma.masked_array(llFocurErrors, mask = np.isnan(llFocurErrors))
    reconstructedFocusError = np.ma.median(llFocurErrors)
    n = np.sum(np.invert(llFocurErrors.mask))
    reconstructedFocusErrorStd= np.ma.std(llFocurErrors)*np.sqrt(np.pi/2.)/np.sqrt(n)

    if config.doPlot == True:
        if not plotFilename:
            raise ValueError("no filename for focus plot")
        import matplotlib
        matplotlib.use("Agg")
        import matplotlib.pyplot as plt

        lMarker = ["o", "x", "d", "^", "<", ">"]
        lColor = ["blue", "green", "red", "cyan", "magenta", "yellow"]
        for i, ccdPair in enumerate(lCcdPairs):
            delta_plot = np.ma.masked_array([getFocusCcdOffset(ccdPair[0], config),
                                             getFocusCcdOffset(ccdPair[1], config)])
            rmssq_plot = np.ma.masked_array([dlRmssq[ccdPair[0]], dlRmssq[ccdPair[1]]])
            for j in range(len(lRadialBinCenters)):
                plt.plot(delta_plot, rmssq_plot[:, j], "%s--" % lMarker[i], color = lColor[j])
        plt.savefig(plotFilename)

    correctedFocusError, correctedFocusErrorStd = getCorrectedFocusError(
        reconstructedFocusError, reconstructedFocusErrorStd, config.corrCoeff)
    return (correctedFocusError[0], correctedFocusErrorStd[0],
            reconstructedFocusError[0], reconstructedFocusErrorStd, n)
Esempio n. 26
0
def referenceImage(image, detector, linearityType, inputData, table=None):
    """Generate a reference linearization.

    Parameters
    ----------
    image: `lsst.afw.image.Image`
        Image to linearize.
    detector: `lsst.afw.cameraGeom.Detector`
        Detector this image is from.
    linearityType: `str`
        Type of linearity to apply.
    inputData: `numpy.array`
        An array of values for the linearity correction.
    table: `numpy.array`, optional
        An optional lookup table to use.

    Returns
    -------
    outImage: `lsst.afw.image.Image`
        The output linearized image.
    numOutOfRange: `int`
        The number of values that could not be linearized.

    Raises
    ------
    RuntimeError :
        Raised if an invalid linearityType is supplied.
    """
    numOutOfRange = 0
    for ampIdx, amp in enumerate(detector.getAmplifiers()):
        ampIdx = (ampIdx // 3, ampIdx % 3)
        bbox = amp.getBBox()
        imageView = image.Factory(image, bbox)

        if linearityType == 'Squared':
            sqCoeff = inputData[ampIdx]
            array = imageView.getArray()

            array[:] = array + sqCoeff * array**2
        elif linearityType == 'LookupTable':
            rowInd, colIndOffset = inputData[ampIdx]
            rowInd = int(rowInd)
            tableRow = table[rowInd, :]
            numOutOfRange += applyLookupTable(imageView, tableRow,
                                              colIndOffset)
        elif linearityType == 'Polynomial':
            coeffs = inputData[ampIdx]
            array = imageView.getArray()
            summation = np.zeros_like(array)
            for index, coeff in enumerate(coeffs):
                summation += coeff * np.power(array, (index + 2))
            array += summation
        elif linearityType == 'Spline':
            centers, values = np.split(inputData, 2)  # This uses the full data
            interp = afwMath.makeInterpolate(
                centers.tolist(), values.tolist(),
                afwMath.stringToInterpStyle('AKIMA_SPLINE'))
            array = imageView.getArray()
            delta = interp.interpolate(array.flatten())
            array -= np.array(delta).reshape(array.shape)
        else:
            raise RuntimeError(f"Unknown linearity: {linearityType}")
    return image, numOutOfRange
Esempio n. 27
0
    def run(self, inputPtc, camera, inputDims):
        """Fit non-linearity to PTC data, returning the correct Linearizer
        object.

        Parameters
        ----------
        inputPtc : `lsst.cp.pipe.PtcDataset`
            Pre-measured PTC dataset.
        camera : `lsst.afw.cameraGeom.Camera`
            Camera geometry.
        inputDims : `lsst.daf.butler.DataCoordinate` or `dict`
            DataIds to use to populate the output calibration.

        Returns
        -------
        results : `lsst.pipe.base.Struct`
            The results struct containing:

            ``outputLinearizer`` : `lsst.ip.isr.Linearizer`
                Final linearizer calibration.
            ``outputProvenance`` : `lsst.ip.isr.IsrProvenance`
                Provenance data for the new calibration.

        Notes
        -----
        This task currently fits only polynomial-defined corrections,
        where the correction coefficients are defined such that:
            corrImage = uncorrImage + sum_i c_i uncorrImage^(2 + i)
        These `c_i` are defined in terms of the direct polynomial fit:
            meanVector ~ P(x=timeVector) = sum_j k_j x^j
        such that c_(j-2) = -k_j/(k_1^j) in units of DN^(1-j) (c.f.,
        Eq. 37 of 2003.05978). The `config.polynomialOrder` or
        `config.splineKnots` define the maximum order of x^j to fit.
        As k_0 and k_1 are degenerate with bias level and gain, they
        are not included in the non-linearity correction.
        """
        detector = camera[inputDims['detector']]
        if self.config.linearityType == 'LookupTable':
            table = np.zeros((len(detector), self.config.maxLookupTableAdu),
                             dtype=np.float32)
            tableIndex = 0
        else:
            table = None
            tableIndex = None  # This will fail if we increment it.

        if self.config.linearityType == 'Spline':
            fitOrder = self.config.splineKnots
        else:
            fitOrder = self.config.polynomialOrder

        # Initialize the linearizer.
        linearizer = Linearizer(detector=detector, table=table, log=self.log)

        for i, amp in enumerate(detector):
            ampName = amp.getName()
            if (len(inputPtc.expIdMask[ampName]) == 0):
                self.log.warn(
                    f"Mask not found for {ampName} in non-linearity fit. Using all points."
                )
                mask = np.repeat(True, len(inputPtc.expIdMask[ampName]))
            else:
                mask = inputPtc.expIdMask[ampName]

            inputAbscissa = np.array(inputPtc.rawExpTimes[ampName])[mask]
            inputOrdinate = np.array(inputPtc.rawMeans[ampName])[mask]

            # Determine proxy-to-linear-flux transformation
            fluxMask = inputOrdinate < self.config.maxLinearAdu
            lowMask = inputOrdinate > self.config.minLinearAdu
            fluxMask = fluxMask & lowMask
            linearAbscissa = inputAbscissa[fluxMask]
            linearOrdinate = inputOrdinate[fluxMask]

            linearFit, linearFitErr, chiSq, weights = irlsFit([0.0, 100.0],
                                                              linearAbscissa,
                                                              linearOrdinate,
                                                              funcPolynomial)
            # Convert this proxy-to-flux fit into an expected linear flux
            linearOrdinate = linearFit[0] + linearFit[1] * inputAbscissa

            # Exclude low end outliers
            threshold = self.config.nSigmaClipLinear * np.sqrt(linearOrdinate)
            fluxMask = np.abs(inputOrdinate - linearOrdinate) < threshold
            linearOrdinate = linearOrdinate[fluxMask]
            fitOrdinate = inputOrdinate[fluxMask]
            self.debugFit('linearFit', inputAbscissa, inputOrdinate,
                          linearOrdinate, fluxMask, ampName)
            # Do fits
            if self.config.linearityType in [
                    'Polynomial', 'Squared', 'LookupTable'
            ]:
                polyFit = np.zeros(fitOrder + 1)
                polyFit[1] = 1.0
                polyFit, polyFitErr, chiSq, weights = irlsFit(
                    polyFit, linearOrdinate, fitOrdinate, funcPolynomial)

                # Truncate the polynomial fit
                k1 = polyFit[1]
                linearityFit = [
                    -coeff / (k1**order) for order, coeff in enumerate(polyFit)
                ]
                significant = np.where(
                    np.abs(linearityFit) > 1e-10, True, False)
                self.log.info(f"Significant polynomial fits: {significant}")

                modelOrdinate = funcPolynomial(polyFit, linearAbscissa)
                self.debugFit('polyFit', linearAbscissa, fitOrdinate,
                              modelOrdinate, None, ampName)

                if self.config.linearityType == 'Squared':
                    linearityFit = [linearityFit[2]]
                elif self.config.linearityType == 'LookupTable':
                    # Use linear part to get time at wich signal is maxAduForLookupTableLinearizer DN
                    tMax = (self.config.maxLookupTableAdu -
                            polyFit[0]) / polyFit[1]
                    timeRange = np.linspace(0, tMax,
                                            self.config.maxLookupTableAdu)
                    signalIdeal = polyFit[0] + polyFit[1] * timeRange
                    signalUncorrected = funcPolynomial(polyFit, timeRange)
                    lookupTableRow = signalIdeal - signalUncorrected  # LinearizerLookupTable has correction

                    linearizer.tableData[tableIndex, :] = lookupTableRow
                    linearityFit = [tableIndex, 0]
                    tableIndex += 1
            elif self.config.linearityType in ['Spline']:
                # See discussion in `lsst.ip.isr.linearize.py` before modifying.
                numPerBin, binEdges = np.histogram(linearOrdinate,
                                                   bins=fitOrder)
                with np.errstate(invalid="ignore"):
                    # Algorithm note: With the counts of points per
                    # bin above, the next histogram calculates the
                    # values to put in each bin by weighting each
                    # point by the correction value.
                    values = np.histogram(
                        linearOrdinate,
                        bins=fitOrder,
                        weights=(inputOrdinate[fluxMask] -
                                 linearOrdinate))[0] / numPerBin

                    # After this is done, the binCenters are
                    # calculated by weighting by the value we're
                    # binning over.  This ensures that widely
                    # spaced/poorly sampled data aren't assigned to
                    # the midpoint of the bin (as could be done using
                    # the binEdges above), but to the weighted mean of
                    # the inputs.  Note that both histograms are
                    # scaled by the count per bin to normalize what
                    # the histogram returns (a sum of the points
                    # inside) into an average.
                    binCenters = np.histogram(
                        linearOrdinate, bins=fitOrder,
                        weights=linearOrdinate)[0] / numPerBin
                    values = values[numPerBin > 0]
                    binCenters = binCenters[numPerBin > 0]

                self.debugFit('splineFit', binCenters, np.abs(values), values,
                              None, ampName)
                interp = afwMath.makeInterpolate(
                    binCenters.tolist(), values.tolist(),
                    afwMath.stringToInterpStyle("AKIMA_SPLINE"))
                modelOrdinate = linearOrdinate + interp.interpolate(
                    linearOrdinate)
                self.debugFit('splineFit', linearOrdinate, fitOrdinate,
                              modelOrdinate, None, ampName)

                # If we exclude a lot of points, we may end up with
                # less than fitOrder points.  Pad out the low-flux end
                # to ensure equal lengths.
                if len(binCenters) != fitOrder:
                    padN = fitOrder - len(binCenters)
                    binCenters = np.pad(binCenters, (padN, 0),
                                        'linear_ramp',
                                        end_values=(binCenters.min() - 1.0, ))
                    # This stores the correction, which is zero at low values.
                    values = np.pad(values, (padN, 0))

                # Pack the spline into a single array.
                linearityFit = np.concatenate(
                    (binCenters.tolist(), values.tolist())).tolist()
                polyFit = [0.0]
                polyFitErr = [0.0]
                chiSq = np.nan
            else:
                polyFit = [0.0]
                polyFitErr = [0.0]
                chiSq = np.nan
                linearityFit = [0.0]

            linearizer.linearityType[ampName] = self.config.linearityType
            linearizer.linearityCoeffs[ampName] = np.array(linearityFit)
            linearizer.linearityBBox[ampName] = amp.getBBox()
            linearizer.fitParams[ampName] = np.array(polyFit)
            linearizer.fitParamsErr[ampName] = np.array(polyFitErr)
            linearizer.fitChiSq[ampName] = chiSq

            image = afwImage.ImageF(len(inputOrdinate), 1)
            image.getArray()[:, :] = inputOrdinate
            linearizeFunction = linearizer.getLinearityTypeByName(
                linearizer.linearityType[ampName])
            linearizeFunction()(image, **{
                'coeffs': linearizer.linearityCoeffs[ampName],
                'table': linearizer.tableData,
                'log': linearizer.log
            })
            linearizeModel = image.getArray()[0, :]

            self.debugFit('solution', inputOrdinate[fluxMask], linearOrdinate,
                          linearizeModel[fluxMask], None, ampName)

        linearizer.hasLinearity = True
        linearizer.validate()
        linearizer.updateMetadata(camera=camera,
                                  detector=detector,
                                  filterName='NONE')
        linearizer.updateMetadata(setDate=True, setCalibId=True)
        provenance = IsrProvenance(calibType='linearizer')

        return pipeBase.Struct(
            outputLinearizer=linearizer,
            outputProvenance=provenance,
        )