/** * Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.math.impl.interpolation; import java.util.Arrays; import com.google.common.primitives.Doubles; import com.opengamma.strata.collect.ArgChecker; import com.opengamma.strata.collect.DoubleArrayMath; import com.opengamma.strata.collect.array.DoubleArray; import com.opengamma.strata.collect.array.DoubleMatrix; import com.opengamma.strata.math.impl.function.PiecewisePolynomialWithSensitivityFunction1D; /** * Filter for nonnegativity of cubic spline interpolation based on * R. L. Dougherty, A. Edelman, and J. M. Hyman, "Nonnegativity-, Monotonicity-, or Convexity-Preserving Cubic and Quintic Hermite Interpolation" * Mathematics Of Computation, v. 52, n. 186, April 1989, pp. 471-494. * * First, interpolant is computed by another cubic interpolation method. Then the first derivatives are modified such that non-negativity conditions are satisfied. * Note that shape-preserving three-point formula is used at endpoints in order to ensure positivity of an interpolant in the first interval and the last interval */ public class NonnegativityPreservingCubicSplineInterpolator extends PiecewisePolynomialInterpolator { private static final double SMALL = 1.e-14; private final HermiteCoefficientsProvider _solver = new HermiteCoefficientsProvider(); private final PiecewisePolynomialWithSensitivityFunction1D _function = new PiecewisePolynomialWithSensitivityFunction1D(); private PiecewisePolynomialInterpolator _method; /** * Primary interpolation method should be passed * @param method PiecewisePolynomialInterpolator */ public NonnegativityPreservingCubicSplineInterpolator(final PiecewisePolynomialInterpolator method) { _method = method; } @Override public PiecewisePolynomialResult interpolate(final double[] xValues, final double[] yValues) { ArgChecker.notNull(xValues, "xValues"); ArgChecker.notNull(yValues, "yValues"); ArgChecker.isTrue(xValues.length == yValues.length | xValues.length + 2 == yValues.length, "(xValues length = yValues length) or (xValues length + 2 = yValues length)"); ArgChecker.isTrue(xValues.length > 2, "Data points should be more than 2"); final int nDataPts = xValues.length; final int yValuesLen = yValues.length; for (int i = 0; i < nDataPts; ++i) { ArgChecker.isFalse(Double.isNaN(xValues[i]), "xValues containing NaN"); ArgChecker.isFalse(Double.isInfinite(xValues[i]), "xValues containing Infinity"); } for (int i = 0; i < yValuesLen; ++i) { ArgChecker.isFalse(Double.isNaN(yValues[i]), "yValues containing NaN"); ArgChecker.isFalse(Double.isInfinite(yValues[i]), "yValues containing Infinity"); } for (int i = 0; i < nDataPts - 1; ++i) { for (int j = i + 1; j < nDataPts; ++j) { ArgChecker.isFalse(xValues[i] == xValues[j], "xValues should be distinct"); } } double[] xValuesSrt = Arrays.copyOf(xValues, nDataPts); double[] yValuesSrt = new double[nDataPts]; if (nDataPts == yValuesLen) { yValuesSrt = Arrays.copyOf(yValues, nDataPts); } else { yValuesSrt = Arrays.copyOfRange(yValues, 1, nDataPts + 1); } DoubleArrayMath.sortPairs(xValuesSrt, yValuesSrt); final double[] intervals = _solver.intervalsCalculator(xValuesSrt); final double[] slopes = _solver.slopesCalculator(yValuesSrt, intervals); final PiecewisePolynomialResult result = _method.interpolate(xValues, yValues); ArgChecker.isTrue(result.getOrder() == 4, "Primary interpolant is not cubic"); final double[] initialFirst = _function.differentiate(result, xValuesSrt).rowArray(0); final double[] first = firstDerivativeCalculator(yValuesSrt, intervals, slopes, initialFirst); final double[][] coefs = _solver.solve(yValuesSrt, intervals, slopes, first); for (int i = 0; i < nDataPts - 1; ++i) { for (int j = 0; j < 4; ++j) { ArgChecker.isFalse(Double.isNaN(coefs[i][j]), "Too large input"); ArgChecker.isFalse(Double.isInfinite(coefs[i][j]), "Too large input"); } } return new PiecewisePolynomialResult(DoubleArray.copyOf(xValuesSrt), DoubleMatrix.copyOf(coefs), 4, 1); } @Override public PiecewisePolynomialResult interpolate(final double[] xValues, final double[][] yValuesMatrix) { ArgChecker.notNull(xValues, "xValues"); ArgChecker.notNull(yValuesMatrix, "yValuesMatrix"); ArgChecker.isTrue(xValues.length == yValuesMatrix[0].length | xValues.length + 2 == yValuesMatrix[0].length, "(xValues length = yValuesMatrix's row vector length) or (xValues length + 2 = yValuesMatrix's row vector length)"); ArgChecker.isTrue(xValues.length > 2, "Data points should be more than 2"); final int nDataPts = xValues.length; final int yValuesLen = yValuesMatrix[0].length; final int dim = yValuesMatrix.length; for (int i = 0; i < nDataPts; ++i) { ArgChecker.isFalse(Double.isNaN(xValues[i]), "xValues containing NaN"); ArgChecker.isFalse(Double.isInfinite(xValues[i]), "xValues containing Infinity"); } for (int i = 0; i < yValuesLen; ++i) { for (int j = 0; j < dim; ++j) { ArgChecker.isFalse(Double.isNaN(yValuesMatrix[j][i]), "yValuesMatrix containing NaN"); ArgChecker.isFalse(Double.isInfinite(yValuesMatrix[j][i]), "yValuesMatrix containing Infinity"); } } for (int i = 0; i < nDataPts; ++i) { for (int j = i + 1; j < nDataPts; ++j) { ArgChecker.isFalse(xValues[i] == xValues[j], "xValues should be distinct"); } } double[] xValuesSrt = new double[nDataPts]; DoubleMatrix[] coefMatrix = new DoubleMatrix[dim]; for (int i = 0; i < dim; ++i) { xValuesSrt = Arrays.copyOf(xValues, nDataPts); double[] yValuesSrt = new double[nDataPts]; if (nDataPts == yValuesLen) { yValuesSrt = Arrays.copyOf(yValuesMatrix[i], nDataPts); } else { yValuesSrt = Arrays.copyOfRange(yValuesMatrix[i], 1, nDataPts + 1); } DoubleArrayMath.sortPairs(xValuesSrt, yValuesSrt); final double[] intervals = _solver.intervalsCalculator(xValuesSrt); final double[] slopes = _solver.slopesCalculator(yValuesSrt, intervals); final PiecewisePolynomialResult result = _method.interpolate(xValues, yValuesMatrix[i]); ArgChecker.isTrue(result.getOrder() == 4, "Primary interpolant is not cubic"); final double[] initialFirst = _function.differentiate(result, xValuesSrt).rowArray(0); final double[] first = firstDerivativeCalculator(yValuesSrt, intervals, slopes, initialFirst); coefMatrix[i] = DoubleMatrix.copyOf(_solver.solve(yValuesSrt, intervals, slopes, first)); } final int nIntervals = coefMatrix[0].rowCount(); final int nCoefs = coefMatrix[0].columnCount(); double[][] resMatrix = new double[dim * nIntervals][nCoefs]; for (int i = 0; i < nIntervals; ++i) { for (int j = 0; j < dim; ++j) { resMatrix[dim * i + j] = coefMatrix[j].row(i).toArray(); } } for (int i = 0; i < (nIntervals * dim); ++i) { for (int j = 0; j < nCoefs; ++j) { ArgChecker.isFalse(Double.isNaN(resMatrix[i][j]), "Too large input"); ArgChecker.isFalse(Double.isInfinite(resMatrix[i][j]), "Too large input"); } } return new PiecewisePolynomialResult(DoubleArray.copyOf(xValuesSrt), DoubleMatrix.copyOf(resMatrix), nCoefs, dim); } @Override public PiecewisePolynomialResultsWithSensitivity interpolateWithSensitivity(final double[] xValues, final double[] yValues) { ArgChecker.notNull(xValues, "xValues"); ArgChecker.notNull(yValues, "yValues"); ArgChecker.isTrue(xValues.length == yValues.length | xValues.length + 2 == yValues.length, "(xValues length = yValues length) or (xValues length + 2 = yValues length)"); ArgChecker.isTrue(xValues.length > 2, "Data points should be more than 2"); final int nDataPts = xValues.length; final int yValuesLen = yValues.length; for (int i = 0; i < nDataPts; ++i) { ArgChecker.isFalse(Double.isNaN(xValues[i]), "xValues containing NaN"); ArgChecker.isFalse(Double.isInfinite(xValues[i]), "xValues containing Infinity"); } for (int i = 0; i < yValuesLen; ++i) { ArgChecker.isFalse(Double.isNaN(yValues[i]), "yValues containing NaN"); ArgChecker.isFalse(Double.isInfinite(yValues[i]), "yValues containing Infinity"); } for (int i = 0; i < nDataPts - 1; ++i) { for (int j = i + 1; j < nDataPts; ++j) { ArgChecker.isFalse(xValues[i] == xValues[j], "xValues should be distinct"); } } double[] yValuesSrt = new double[nDataPts]; if (nDataPts == yValuesLen) { yValuesSrt = Arrays.copyOf(yValues, nDataPts); } else { yValuesSrt = Arrays.copyOfRange(yValues, 1, nDataPts + 1); } final double[] intervals = _solver.intervalsCalculator(xValues); final double[] slopes = _solver.slopesCalculator(yValuesSrt, intervals); final PiecewisePolynomialResultsWithSensitivity resultWithSensitivity = _method.interpolateWithSensitivity(xValues, yValues); ArgChecker.isTrue(resultWithSensitivity.getOrder() == 4, "Primary interpolant is not cubic"); final double[] initialFirst = _function.differentiate(resultWithSensitivity, xValues).rowArray(0); final double[][] slopeSensitivity = _solver.slopeSensitivityCalculator(intervals); final DoubleArray[] initialFirstSense = _function.differentiateNodeSensitivity(resultWithSensitivity, xValues); final DoubleArray[] firstWithSensitivity = firstDerivativeWithSensitivityCalculator(yValuesSrt, intervals, initialFirst, initialFirstSense); final DoubleMatrix[] resMatrix = _solver.solveWithSensitivity(yValuesSrt, intervals, slopes, slopeSensitivity, firstWithSensitivity); for (int k = 0; k < nDataPts; k++) { DoubleMatrix m = resMatrix[k]; final int rows = m.rowCount(); final int cols = m.columnCount(); for (int i = 0; i < rows; ++i) { for (int j = 0; j < cols; ++j) { ArgChecker.isTrue(Doubles.isFinite(m.get(i, j)), "Matrix contains a NaN or infinite"); } } } final DoubleMatrix coefMatrix = resMatrix[0]; final DoubleMatrix[] coefSenseMatrix = new DoubleMatrix[nDataPts - 1]; System.arraycopy(resMatrix, 1, coefSenseMatrix, 0, nDataPts - 1); final int nCoefs = coefMatrix.columnCount(); return new PiecewisePolynomialResultsWithSensitivity(DoubleArray.copyOf(xValues), coefMatrix, nCoefs, 1, coefSenseMatrix); } @Override public PiecewisePolynomialInterpolator getPrimaryMethod() { return _method; } /** * First derivatives are modified such that cubic interpolant has the same sign as linear interpolator * @param yValues * @param intervals * @param slopes * @param initialFirst * @return first derivative */ private double[] firstDerivativeCalculator(final double[] yValues, final double[] intervals, final double[] slopes, final double[] initialFirst) { final int nDataPts = yValues.length; double[] res = new double[nDataPts]; for (int i = 1; i < nDataPts - 1; ++i) { final double tau = Math.signum(yValues[i]); res[i] = tau == 0. ? initialFirst[i] : Math.min(3. * tau * yValues[i] / intervals[i - 1], Math.max(-3. * tau * yValues[i] / intervals[i], tau * initialFirst[i])) / tau; } final double tauIni = Math.signum(yValues[0]); final double tauFin = Math.signum(yValues[nDataPts - 1]); res[0] = tauIni == 0. ? initialFirst[0] : Math.min(3. * tauIni * yValues[0] / intervals[0], Math.max(-3. * tauIni * yValues[0] / intervals[0], tauIni * initialFirst[0])) / tauIni; res[nDataPts - 1] = tauFin == 0. ? initialFirst[nDataPts - 1] : Math.min(3. * tauFin * yValues[nDataPts - 1] / intervals[nDataPts - 2], Math.max(-3. * tauFin * yValues[nDataPts - 1] / intervals[nDataPts - 2], tauFin * initialFirst[nDataPts - 1])) / tauFin; return res; } private DoubleArray[] firstDerivativeWithSensitivityCalculator(final double[] yValues, final double[] intervals, final double[] initialFirst, final DoubleArray[] initialFirstSense) { final int nDataPts = yValues.length; final DoubleArray[] res = new DoubleArray[nDataPts + 1]; final double[] newFirst = new double[nDataPts]; for (int i = 1; i < nDataPts - 1; ++i) { final double tau = Math.signum(yValues[i]); final double lower = -3. * tau * yValues[i] / intervals[i]; final double upper = 3. * tau * yValues[i] / intervals[i - 1]; final double ref = tau * initialFirst[i]; final double[] tmp = new double[nDataPts]; Arrays.fill(tmp, 0.); if (Math.abs(ref - lower) < SMALL && tau != 0.) { newFirst[i] = ref >= lower ? initialFirst[i] : lower / tau; for (int k = 0; k < nDataPts; ++k) { tmp[k] = 0.5 * initialFirstSense[i].get(k); } tmp[i] -= 1.5 / intervals[i]; } else { if (ref < lower) { newFirst[i] = lower / tau; tmp[i] = -3. / intervals[i]; } else { if (Math.abs(ref - upper) < SMALL && tau != 0.) { newFirst[i] = ref <= upper ? initialFirst[i] : upper / tau; for (int k = 0; k < nDataPts; ++k) { tmp[k] = 0.5 * initialFirstSense[i].get(k); } tmp[i] += 1.5 / intervals[i - 1]; } else { if (ref > upper) { newFirst[i] = upper / tau; tmp[i] = 3. / intervals[i - 1]; } else { newFirst[i] = initialFirst[i]; System.arraycopy(initialFirstSense[i].toArray(), 0, tmp, 0, nDataPts); } } } } res[i + 1] = DoubleArray.copyOf(tmp); } final double tauIni = Math.signum(yValues[0]); final double lowerIni = -3. * tauIni * yValues[0] / intervals[0]; final double upperIni = 3. * tauIni * yValues[0] / intervals[0]; final double refIni = tauIni * initialFirst[0]; final double[] tmpIni = new double[nDataPts]; Arrays.fill(tmpIni, 0.); if (Math.abs(refIni - lowerIni) < SMALL && tauIni != 0.) { newFirst[0] = refIni >= lowerIni ? initialFirst[0] : lowerIni / tauIni; for (int k = 0; k < nDataPts; ++k) { tmpIni[k] = 0.5 * initialFirstSense[0].get(k); } tmpIni[0] -= 1.5 / intervals[0]; } else { if (refIni < lowerIni) { newFirst[0] = lowerIni / tauIni; tmpIni[0] = -3. / intervals[0]; } else { if (Math.abs(refIni - upperIni) < SMALL && tauIni != 0.) { newFirst[0] = refIni <= upperIni ? initialFirst[0] : upperIni / tauIni; for (int k = 0; k < nDataPts; ++k) { tmpIni[k] = 0.5 * initialFirstSense[0].get(k); } tmpIni[0] += 1.5 / intervals[0]; } else { if (refIni > upperIni) { newFirst[0] = upperIni / tauIni; tmpIni[0] = 3. / intervals[0]; } else { newFirst[0] = initialFirst[0]; System.arraycopy(initialFirstSense[0].toArray(), 0, tmpIni, 0, nDataPts); } } } } res[1] = DoubleArray.copyOf(tmpIni); final double tauFin = Math.signum(yValues[nDataPts - 1]); final double lowerFin = -3. * tauFin * yValues[nDataPts - 1] / intervals[nDataPts - 2]; final double upperFin = 3. * tauFin * yValues[nDataPts - 1] / intervals[nDataPts - 2]; final double refFin = tauFin * initialFirst[nDataPts - 1]; final double[] tmpFin = new double[nDataPts]; Arrays.fill(tmpFin, 0.); if (Math.abs(refFin - lowerFin) < SMALL && tauFin != 0.) { newFirst[nDataPts - 1] = refFin >= lowerFin ? initialFirst[nDataPts - 1] : lowerFin / tauFin; for (int k = 0; k < nDataPts; ++k) { tmpFin[k] = 0.5 * initialFirstSense[nDataPts - 1].get(k); } tmpFin[nDataPts - 1] -= 1.5 / intervals[nDataPts - 2]; } else { if (refFin < lowerFin) { newFirst[nDataPts - 1] = lowerFin / tauFin; tmpFin[nDataPts - 1] = -3. / intervals[nDataPts - 2]; } else { if (Math.abs(refFin - upperFin) < SMALL && tauFin != 0.) { newFirst[nDataPts - 1] = refFin <= upperFin ? initialFirst[nDataPts - 1] : upperFin / tauFin; for (int k = 0; k < nDataPts; ++k) { tmpFin[k] = 0.5 * initialFirstSense[nDataPts - 1].get(k); } tmpFin[nDataPts - 1] += 1.5 / intervals[nDataPts - 2]; } else { if (refFin > upperFin) { newFirst[nDataPts - 1] = upperFin / tauFin; tmpFin[nDataPts - 1] = 3. / intervals[nDataPts - 2]; } else { newFirst[nDataPts - 1] = initialFirst[nDataPts - 1]; System.arraycopy(initialFirstSense[nDataPts - 1].toArray(), 0, tmpFin, 0, nDataPts); } } } } res[nDataPts] = DoubleArray.copyOf(tmpFin); res[0] = DoubleArray.copyOf(newFirst); return res; } }