com.opengamma.analytics.math.interpolation.ShapePreservingCubicSplineInterpolator.java Source code

Java tutorial

Introduction

Here is the source code for com.opengamma.analytics.math.interpolation.ShapePreservingCubicSplineInterpolator.java

Source

/**
 * Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */
package com.opengamma.analytics.math.interpolation;

import java.util.Arrays;

import org.apache.commons.lang.NotImplementedException;

import com.opengamma.analytics.math.interpolation.data.Interpolator1DPiecewisePoynomialWithExtraKnotsDataBundle;
import com.opengamma.analytics.math.matrix.DoubleMatrix1D;
import com.opengamma.analytics.math.matrix.DoubleMatrix2D;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.ParallelArrayBinarySort;

/**
 * Shape-preserving C2 cubic spline interpolation based on
 * S. Pruess "Shape preserving C2 cubic spline interpolation"
 *  IMA Journal of Numerical Analysis (1993) 13 (4): 493-507.
 * where two extra knots are introduced between adjacent data points
 * As the position of the new knots are data dependent, the matrix form of yValues producing multi-splines is not relevant
 */
public class ShapePreservingCubicSplineInterpolator extends PiecewisePolynomialInterpolator {

    private static final double INF = 1. / 0.;
    private static final double ERROR = 1.e-12;

    @Override
    public PiecewisePolynomialResult interpolate(final double[] xValues, final double[] yValues) {

        ArgumentChecker.notNull(xValues, "xValues");
        ArgumentChecker.notNull(yValues, "yValues");

        ArgumentChecker.isTrue(xValues.length == yValues.length, "xValues length = yValues length");
        ArgumentChecker.isTrue(xValues.length > 2, "Data points should be more than 1");

        final int nDataPts = xValues.length;

        for (int i = 0; i < nDataPts; ++i) {
            ArgumentChecker.isFalse(Double.isNaN(xValues[i]), "xData containing NaN");
            ArgumentChecker.isFalse(Double.isInfinite(xValues[i]), "xData containing Infinity");
            ArgumentChecker.isFalse(Double.isNaN(yValues[i]), "yData containing NaN");
            ArgumentChecker.isFalse(Double.isInfinite(yValues[i]), "yData containing Infinity");
        }

        for (int i = 0; i < nDataPts; ++i) {
            for (int j = i + 1; j < nDataPts; ++j) {
                ArgumentChecker.isFalse(xValues[i] == xValues[j], "xValues should be distinct");
            }
        }

        final double[] xValuesSrt = Arrays.copyOf(xValues, nDataPts);
        final double[] yValuesSrt = Arrays.copyOf(yValues, nDataPts);
        ParallelArrayBinarySort.parallelBinarySort(xValuesSrt, yValuesSrt);

        final double[] intervals = intervalsCalculator(xValuesSrt);
        final double[] slopes = slopesCalculator(yValuesSrt, intervals);
        final double[] beta = betaCalculator(slopes);
        double[] first = firstDiffFinder(intervals, slopes);
        double[] rValues = rValuesCalculator(slopes, first);

        boolean correctSign = false;
        int it = 0;

        while (correctSign == false) {
            correctSign = signChecker(beta, rValues);
            if (correctSign == false) {
                first = firstDiffSweep(intervals, slopes, beta, first);
                rValues = rValuesCalculator(slopes, first);
            }
            ++it;
            if (it > 10) {
                throw new IllegalArgumentException("Spline is not found!");
            }
        }

        final double[] second = secondDiffFinder(intervals, beta, rValues);
        final double[] tau = tauFinder(intervals, slopes, beta, first, second);
        final double[] knots = knotsProvider(xValuesSrt, intervals, tau);

        final double[][] coefMatrix = solve(yValuesSrt, intervals, slopes, first, second, tau);

        for (int i = 0; i < coefMatrix.length; ++i) {
            double ref = 0.;
            final double interval = knots[i + 1] - knots[i];
            for (int j = 0; j < 4; ++j) {
                ref += coefMatrix[i][j] * Math.pow(interval, 3 - j);
                ArgumentChecker.isFalse(Double.isNaN(coefMatrix[i][j]), "Too large input");
                ArgumentChecker.isFalse(Double.isInfinite(coefMatrix[i][j]), "Too large input");
            }
            final double yVal = i == coefMatrix.length - 1 ? yValues[nDataPts - 1] : coefMatrix[i + 1][3];
            final double bound = Math.max(Math.abs(ref) + Math.abs(yVal), 1.e-1);
            ArgumentChecker.isTrue(Math.abs(ref - yVal) < ERROR * bound,
                    "Input is too large/small or data points are too close");
        }

        return new PiecewisePolynomialResult(new DoubleMatrix1D(knots), new DoubleMatrix2D(coefMatrix), 4, 1);
    }

    @Override
    public PiecewisePolynomialResult interpolate(final double[] xValues, final double[][] yValuesMatrix) {
        throw new IllegalArgumentException("Method with multidimensional yValues is not supported");
    }

    /**
     * Since this interpolation method introduces new breakpoints in certain cases, {@link PiecewisePolynomialResultsWithSensitivity} is not well-defined
     * Instead the node sensitivity is computed in {@link MonotoneConvexSplineInterpolator1D} via {@link Interpolator1DPiecewisePoynomialWithExtraKnotsDataBundle}
     * @param xValues The xValues
     * @param yValues The yValues
     * @return NotImplementedException
     */
    @Override
    public PiecewisePolynomialResultsWithSensitivity interpolateWithSensitivity(final double[] xValues,
            final double[] yValues) {
        throw new NotImplementedException();
    }

    /**
     * @param xValues
     * @return Intervals of xValues, ( xValues_i - xValues_{i-1} )
     */
    private double[] intervalsCalculator(final double[] xValues) {

        final int nDataPts = xValues.length;
        final double[] intervals = new double[nDataPts - 1];

        for (int i = 0; i < nDataPts - 1; ++i) {
            intervals[i] = xValues[i + 1] - xValues[i];
        }

        return intervals;
    }

    /**
     * @param yValues Y values of data
     * @param intervals Intervals of x data
     * @return Slopes
     */
    private double[] slopesCalculator(final double[] yValues, final double[] intervals) {

        final int nDataPts = yValues.length;
        final double[] slopes = new double[nDataPts - 1];

        for (int i = 0; i < nDataPts - 1; ++i) {
            slopes[i] = (yValues[i + 1] - yValues[i]) / intervals[i];
        }

        return slopes;
    }

    /**
     * @param intervals
     * @param slopes
     * @return First derivative at knots
     */
    private double[] firstDiffFinder(final double[] intervals, final double[] slopes) {
        final int nInts = intervals.length;
        final double[] res = new double[nInts + 1];

        res[0] = endpointFirst(intervals[0], intervals[1], slopes[0], slopes[1]);
        res[nInts] = endpointFirst(intervals[nInts - 1], intervals[nInts - 2], slopes[nInts - 1],
                slopes[nInts - 2]);

        for (int i = 1; i < nInts; ++i) {
            if (Math.signum(slopes[i]) != Math.signum(slopes[i - 1]) | (slopes[i] == 0 | slopes[i - 1] == 0)) {
                res[i] = 0.;
            } else {
                final double den1 = 2. * intervals[i] + intervals[i - 1];
                final double den2 = intervals[i] + 2. * intervals[i - 1];
                res[i] = 3. * (intervals[i] + intervals[i - 1]) / (den1 / slopes[i - 1] + den2 / slopes[i]);
            }
        }

        return res;
    }

    /**
     * Estimate first derivatives at endpoints
     * @param ints1 First (last) interval
     * @param ints2 Second (second last) Interval
     * @param grads1 First (last) slope
     * @param grads2 Second (second last) slope
     * @return Slope at the first (last) data point
     */
    private double endpointFirst(final double ints1, final double ints2, final double grads1, final double grads2) {
        final double val = (2. * ints1 + ints2) * grads1 / (ints1 + ints2) - ints1 * grads2 / (ints1 + ints2);

        if (Math.signum(val) != Math.signum(grads1)) {
            return 0.;
        }
        if (Math.signum(grads1) != Math.signum(grads2) && Math.abs(val) > 3. * Math.abs(grads1)) {
            return 3. * grads1;
        }
        return val;
    }

    /**
     * @param slopes
     * @return sign(slopes_{i + 1} - slopes_i)
     */
    private double[] betaCalculator(final double[] slopes) {
        final int nSlopes = slopes.length;
        final double[] res = new double[nSlopes + 1];

        for (int i = 0; i < nSlopes - 1; ++i) {
            res[i + 1] = Math.signum(slopes[i + 1] - slopes[i]);
        }
        res[0] = res[1];
        res[nSlopes] = res[nSlopes - 1];

        return res;
    }

    /**
     * In the notation i =1,2,...,N-1,
     * R_{2*i-1} = 6*slopes_i - 4*first_i - 2*first_{i+1}
     * R_{2*i}   = - 6*slopes_i + 2*first_i + 4*first_{i+1}
     * @param slopes
     * @param first First derivatives
     * @return R functions
     */
    private double[] rValuesCalculator(final double[] slopes, final double[] first) {
        final int nData = first.length;
        final double[] res = new double[2 * nData];

        for (int i = 1; i < nData - 1; ++i) {
            res[2 * i] = 2. * first[i - 1] + 4. * first[i] - 6. * slopes[i - 1];
            res[2 * i - 1] = -4. * first[i - 1] - 2. * first[i] + 6. * slopes[i - 1];
            if (Math.abs(res[2 * i]) <= 0.1 * ERROR) {
                res[2 * i] = 0.;
            }
            if (Math.abs(res[2 * i - 1]) <= 0.1 * ERROR) {
                res[2 * i - 1] = 0.;
            }
        }
        res[0] = INF;
        res[2 * nData - 1] = INF;

        return res;
    }

    /**
     * Check beta_i * R_{2*i-1} \geq 0 and beta_{i+1} *R_{2*i} \geq 0
     * @param beta
     * @param rValues
     * @return True if the two inequalities are satisfied
     */
    private boolean signChecker(final double[] beta, final double[] rValues) {
        final int nData = beta.length;

        for (int i = 1; i < nData - 2; ++i) {
            if (beta[i] * rValues[2 * i + 1] < 0 | beta[i + 1] * rValues[2 * i + 2] < 0) {
                return false;
            }
        }
        return true;
    }

    /**
     * As the double sweep algorithm determines only intervals [minTmp, maxTmp] which should contain first derivative, choice of the values of first derivatives is not unique.
     * Infeasibility is returned in some cases, which is resolved by another choice of first derivatives within the allowed intervals, [minTmp, maxTmp].
     * @param intervals
     * @param slopes
     * @param beta
     * @param first
     * @return First derivatives satisfying convexity conditions
     */
    private double[] firstDiffSweep(final double[] intervals, final double[] slopes, final double[] beta,
            final double[] first) {
        final int nData = intervals.length + 1;
        final double[] res = new double[nData];

        double minVal = 0.;
        double maxVal = 0.;

        if (beta[0] > 0) {
            minVal = 3. * slopes[0] - 2. * slopes[1];
            maxVal = slopes[0];
        } else {
            minVal = slopes[0];
            maxVal = 3. * slopes[0] - 2. * slopes[1];
        }

        for (int i = 1; i < nData; ++i) {
            double minTmp = 0.;
            double maxTmp = 0.;
            if (beta[i - 1] == 0.) {
                if (beta[i] == 0.) {
                    minTmp = -INF;
                    maxTmp = INF;
                } else {
                    if (beta[i] > 0.) {
                        minTmp = 0.5 * (3. * slopes[i - 1] - maxVal);
                        maxTmp = INF;
                    } else {
                        minTmp = -INF;
                        maxTmp = Math.min(slopes[i - 1], 0.5 * (3. * slopes[i - 1] - minVal));
                    }
                }
            } else {
                if (beta[i - 1] > 0.) {
                    if (beta[i] == 0.) {
                        minTmp = slopes[i - 1];
                        maxTmp = 3. * slopes[i - 1] - 2. * minVal;
                    } else {
                        if (beta[i] > 0.) {
                            minTmp = Math.max(slopes[i - 1], 0.5 * (3. * slopes[i - 1] - maxVal));
                            maxTmp = 3. * slopes[i - 1] - 2. * minVal;
                        } else {
                            minTmp = -INF;
                            maxTmp = Math.min(3. * slopes[i - 1] - 2. * minVal,
                                    0.5 * (3. * slopes[i - 1] - minVal));
                        }
                    }
                } else {
                    if (beta[i] == 0) {
                        minTmp = 3. * slopes[i - 1] - 2. * minVal;
                        maxTmp = INF;
                    } else {
                        if (beta[i] > 0.) {
                            minTmp = Math.max(3. * slopes[i - 1] - 2. * maxVal,
                                    0.5 * (3. * slopes[i - 1] - maxVal));
                            maxTmp = INF;
                        } else {
                            minTmp = 3. * slopes[i - 1] - 2. * maxVal;
                            maxTmp = Math.min(slopes[i - 1], 0.5 * (3. * slopes[i - 1] - minVal));
                        }
                    }
                }
            }
            minVal = minTmp;
            maxVal = maxTmp;

            if (minTmp != -INF && maxTmp != INF) {
                res[i] = 0.5 * (minTmp + maxTmp);
            } else {
                if (first[i] < minTmp) {
                    res[i] = minTmp != INF ? minTmp : first[i];
                } else {
                    if (first[i] > maxTmp) {
                        res[i] = maxTmp != -INF ? maxTmp : first[i];
                    } else {
                        res[i] = first[i];
                    }
                }
            }
        }

        if (minVal > maxVal) {
            res[nData - 1] = first[nData - 1];
        } else {
            if (first[nData - 1] < minVal) {
                res[nData - 1] = minVal != INF ? minVal : first[nData - 1];
            } else {
                if (first[nData - 1] > maxVal) {
                    res[nData - 1] = maxVal != -INF ? maxVal : first[nData - 1];
                } else {
                    res[nData - 1] = first[nData - 1];
                }
            }
        }

        double minTmp = 0.;
        double maxTmp = 0.;
        for (int i = nData - 2; i > -1; --i) {
            minTmp = 0.;
            maxTmp = 0.;
            if (beta[i] == 0.) {
                if (beta[i + 1] == 0.) {
                    minTmp = -INF;
                    maxTmp = INF;
                } else {
                    if (beta[i + 1] > 0.) {
                        minTmp = 3. * slopes[i] - 2. * res[i + 1];
                        maxTmp = INF;
                    } else {
                        minTmp = -INF;
                        maxTmp = 3. * slopes[i] - 2. * res[i + 1];
                    }
                }
            } else {
                if (beta[i] > 0.) {
                    if (beta[i + 1] == 0.) {
                        minTmp = -INF;
                        maxTmp = 0.5 * (3. * slopes[i] - res[i + 1]);
                    } else {
                        if (beta[i + 1] > 0.) {
                            minTmp = 3. * slopes[i] - 2. * res[i + 1];
                            maxTmp = 0.5 * (3. * slopes[i] - res[i + 1]);
                        } else {
                            minTmp = -INF;
                            maxTmp = Math.min(3. * slopes[i] - 2. * res[i + 1],
                                    0.5 * (3. * slopes[i] - res[i + 1]));
                        }
                    }
                } else {
                    if (beta[i + 1] == 0.) {
                        minTmp = 0.5 * (3. * slopes[i] - res[i + 1]);
                        maxTmp = INF;
                    } else {
                        if (beta[i + 1] > 0.) {
                            minTmp = Math.max(3. * slopes[i] - 2. * res[i + 1],
                                    0.5 * (3. * slopes[i] - res[i + 1]));
                            maxTmp = INF;
                        } else {
                            minTmp = 0.5 * (3. * slopes[i] - res[i + 1]);
                            maxTmp = 3. * slopes[i] - 2. * res[i + 1];
                        }
                    }
                }
            }

            if (minTmp > maxTmp) {
                throw new IllegalArgumentException("Local monotonicity can not be preserved");
            }
            if (res[i] < minTmp) {
                res[i] = minTmp != INF ? minTmp : res[i];
            } else {
                if (res[i] > maxTmp) {
                    res[i] = maxTmp != -INF ? maxTmp : res[i];
                }
            }
        }

        return res;
    }

    /**
     * @param intervals
     * @param beta
     * @param rValues
     * @return Second derivatives satisfying local monotonicity
     */
    private double[] secondDiffFinder(final double[] intervals, final double[] beta, final double[] rValues) {
        final int nData = intervals.length + 1;
        final double[] res = new double[nData];

        for (int i = 1; i < nData - 1; ++i) {
            res[i] = (rValues[2 * i + 1] > 0 && rValues[2 * i] > 0) ? beta[i] * Math
                    .min(beta[i] * rValues[2 * i + 1] / intervals[i], beta[i] * rValues[2 * i] / intervals[i - 1])
                    : 0.;
        }
        res[0] = rValues[1] > 0 ? beta[0] * rValues[1] / intervals[0] : 0.;
        res[nData - 1] = rValues[2 * (nData - 1)] > 0
                ? beta[nData - 1] * rValues[2 * (nData - 1)] / intervals[nData - 2]
                : 0.;

        return res;
    }

    /**
     * Extra knots are introduced at xValues[i] + tau[i] * intervals[i] and xValues[i + 1] - tau[i] * intervals[i]
     * @param intervals
     * @param slopes
     * @param beta
     * @param first
     * @param second
     * @return tau
     */
    private double[] tauFinder(final double[] intervals, final double[] slopes, final double[] beta,
            final double[] first, final double[] second) {
        final int nData = intervals.length + 1;
        final double[] res = new double[nData - 1];
        Arrays.fill(res, 1. / 3.);

        for (int i = 1; i < nData - 2; ++i) {
            boolean ineq1 = false;
            boolean ineq2 = false;
            final double bound1 = 6. * slopes[i] * beta[i];
            final double bound2 = 6. * slopes[i] * beta[i + 1];
            double ref1 = (4. * first[i] + 2. * first[i + 1] + intervals[i] * second[i] * res[i] * (2. - res[i])
                    - intervals[i] * second[i + 1] * res[i] * (1. - res[i])) * beta[i];
            double ref2 = (2. * first[i] + 4. * first[i + 1] + intervals[i] * second[i] * res[i] * (1. - res[i])
                    - intervals[i] * second[i + 1] * res[i] * (2. - res[i])) * beta[i + 1];
            while (ineq1 == false) {
                if (ref1 - ERROR * Math.abs(ref1) <= bound1 + ERROR * Math.abs(bound1)) {
                    ineq1 = true;
                } else {
                    res[i] *= 0.8;
                }
                if (res[i] < ERROR / 100.) {
                    throw new IllegalArgumentException("Spline is not found");
                }
                ref1 = (4. * first[i] + 2. * first[i + 1] + intervals[i] * second[i] * res[i] * (2. - res[i])
                        - intervals[i] * second[i + 1] * res[i] * (1. - res[i])) * beta[i];
            }
            while (ineq2 == false) {
                if (ref2 + ERROR * Math.abs(ref2) >= bound2 - ERROR * Math.abs(bound2)) {
                    ineq2 = true;
                } else {
                    res[i] *= 0.8;
                }
                if (res[i] < ERROR / 100.) {
                    throw new IllegalArgumentException("Spline is not found");
                }
                ref2 = (2. * first[i] + 4. * first[i + 1] + intervals[i] * second[i] * res[i] * (1. - res[i])
                        - intervals[i] * second[i + 1] * res[i] * (2. - res[i])) * beta[i + 1];
            }

        }

        return res;
    }

    /**
     * @param xValues
     * @param intervals
     * @param tau
     * @return {... , xValues[i], xValues[i] + tau[i] * intervals[i], xValues[i + 1] - tau[i] * intervals[i], xValues[i + 1], ...}
     */
    private double[] knotsProvider(final double[] xValues, final double[] intervals, final double[] tau) {
        final int nData = xValues.length;
        final double[] res = new double[3 * nData - 2];

        for (int i = 0; i < nData - 1; ++i) {
            res[3 * i] = xValues[i];
            res[3 * i + 1] = xValues[i] + tau[i] * intervals[i];
            res[3 * i + 2] = xValues[i + 1] - tau[i] * intervals[i];
        }
        res[3 * (nData - 1)] = xValues[nData - 1];

        return res;
    }

    /**
     * Determine value, first derivative, second derivative and third derivative of interpolant at extra knots
     * @param yValues
     * @param intervals
     * @param slopes
     * @param first
     * @param second
     * @param tau
     * @return Coefficient matrix whose i-th row vector is {a_n, a_{n-1}, ... } of f(x) = a_n * (x-x_i)^n + a_{n-1} * (x-x_i)^{n-1} +... for the i-th interval
     */
    private double[][] solve(final double[] yValues, final double[] intervals, final double[] slopes,
            final double[] first, final double[] second, final double[] tau) {
        final int nData = yValues.length;
        final double[][] res = new double[3 * (nData - 1)][4];

        final double[] secNewKnots1 = new double[nData - 1];
        final double[] secNewKnots2 = new double[nData - 1];
        for (int i = 0; i < nData - 1; ++i) {
            secNewKnots1[i] = 6. * slopes[i] / intervals[i] / (1. - tau[i])
                    - 4. * first[i] / intervals[i] / (1. - tau[i])
                    - 2. * first[i + 1] / intervals[i] / (1. - tau[i])
                    - tau[i] * (2. - tau[i]) * second[i] / (1. - tau[i]) + tau[i] * second[i + 1];
            secNewKnots2[i] = -6. * slopes[i] / intervals[i] / (1. - tau[i])
                    + 4. * first[i + 1] / intervals[i] / (1. - tau[i])
                    + 2. * first[i] / intervals[i] / (1. - tau[i])
                    - tau[i] * (2. - tau[i]) * second[i + 1] / (1. - tau[i]) + tau[i] * second[i];
        }

        for (int i = 0; i < nData - 1; ++i) {
            res[3 * i][0] = (secNewKnots1[i] - second[i]) / 6. / tau[i] / intervals[i];
            res[3 * i][1] = 0.5 * second[i];
            res[3 * i][2] = first[i];
            res[3 * i][3] = yValues[i];
            res[3 * i + 1][0] = (secNewKnots2[i] - secNewKnots1[i]) / 6. / (1. - 2. * tau[i]) / intervals[i];
            res[3 * i + 1][1] = 0.5 * secNewKnots1[i];
            res[3 * i + 1][2] = first[i] + (second[i] + secNewKnots1[i]) * tau[i] * intervals[i] * 0.5;
            res[3 * i + 1][3] = yValues[i] + tau[i] * intervals[i] * first[i]
                    + (2. * second[i] + secNewKnots1[i]) * tau[i] * intervals[i] * tau[i] * intervals[i] / 6.;
            res[3 * i + 2][0] = (second[i + 1] - secNewKnots2[i]) / 6. / tau[i] / intervals[i];
            res[3 * i + 2][1] = 0.5 * secNewKnots2[i];
            res[3 * i + 2][2] = first[i + 1] - (second[i + 1] + secNewKnots2[i]) * tau[i] * intervals[i] * 0.5;
            res[3 * i + 2][3] = yValues[i + 1] - tau[i] * intervals[i] * first[i + 1]
                    + (2. * second[i + 1] + secNewKnots2[i]) * tau[i] * intervals[i] * tau[i] * intervals[i] / 6.;
        }

        return res;
    }

}