AkimaSplineInterpolator.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      https://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. /*
  18.  * This is not the original file distributed by the Apache Software Foundation
  19.  * It has been modified by the Hipparchus project
  20.  */
  21. package org.hipparchus.analysis.interpolation;

  22. import java.lang.reflect.Array;

  23. import org.hipparchus.Field;
  24. import org.hipparchus.CalculusFieldElement;
  25. import org.hipparchus.analysis.polynomials.FieldPolynomialFunction;
  26. import org.hipparchus.analysis.polynomials.FieldPolynomialSplineFunction;
  27. import org.hipparchus.analysis.polynomials.PolynomialFunction;
  28. import org.hipparchus.analysis.polynomials.PolynomialSplineFunction;
  29. import org.hipparchus.exception.LocalizedCoreFormats;
  30. import org.hipparchus.exception.MathIllegalArgumentException;
  31. import org.hipparchus.exception.NullArgumentException;
  32. import org.hipparchus.util.FastMath;
  33. import org.hipparchus.util.MathArrays;
  34. import org.hipparchus.util.Precision;

  35. /**
  36.  * Computes a cubic spline interpolation for the data set using the Akima
  37.  * algorithm, as originally formulated by Hiroshi Akima in his 1970 paper
  38.  * <a href="http://doi.acm.org/10.1145/321607.321609">A New Method of
  39.  * Interpolation and Smooth Curve Fitting Based on Local Procedures.</a>
  40.  * J. ACM 17, 4 (October 1970), 589-602. DOI=10.1145/321607.321609
  41.  * <p>
  42.  * This implementation is based on the Akima implementation in the CubicSpline
  43.  * class in the Math.NET Numerics library. The method referenced is
  44.  * CubicSpline.InterpolateAkimaSorted
  45.  * </p>
  46.  * <p>
  47.  * The {@link #interpolate(double[], double[]) interpolate} method returns a
  48.  * {@link PolynomialSplineFunction} consisting of n cubic polynomials, defined
  49.  * over the subintervals determined by the x values, {@code x[0] < x[i] ... < x[n]}.
  50.  * The Akima algorithm requires that {@code n >= 5}.
  51.  * </p>
  52.  */
  53. public class AkimaSplineInterpolator
  54.     implements UnivariateInterpolator, FieldUnivariateInterpolator {

  55.     /** The minimum number of points that are needed to compute the function. */
  56.     private static final int MINIMUM_NUMBER_POINTS = 5;

  57.     /** Weight modifier to avoid overshoots. */
  58.     private final boolean useModifiedWeights;

  59.     /** Simple constructor.
  60.      * <p>
  61.      * This constructor is equivalent to call {@link #AkimaSplineInterpolator(boolean)
  62.      * AkimaSplineInterpolator(false)}, i.e. to use original Akima weights
  63.      * </p>
  64.      * @since 2.1
  65.      */
  66.     public AkimaSplineInterpolator() {
  67.         this(false);
  68.     }

  69.     /** Simple constructor.
  70.      * <p>
  71.      * The weight modification is described in <a
  72.      * href="https://blogs.mathworks.com/cleve/2019/04/29/makima-piecewise-cubic-interpolation/">
  73.      * Makima Piecewise Cubic Interpolation</a>. It attempts to avoid overshoots
  74.      * near near constant slopes sub-samples.
  75.      * </p>
  76.      * @param useModifiedWeights if true, use modified weights to avoid overshoots
  77.      * @since 2.1
  78.      */
  79.     public AkimaSplineInterpolator(final boolean useModifiedWeights) {
  80.         this.useModifiedWeights = useModifiedWeights;
  81.     }

  82.     /**
  83.      * Computes an interpolating function for the data set.
  84.      *
  85.      * @param xvals the arguments for the interpolation points
  86.      * @param yvals the values for the interpolation points
  87.      * @return a function which interpolates the data set
  88.      * @throws MathIllegalArgumentException if {@code xvals} and {@code yvals} have
  89.      *         different sizes.
  90.      * @throws MathIllegalArgumentException if {@code xvals} is not sorted in
  91.      *         strict increasing order.
  92.      * @throws MathIllegalArgumentException if the size of {@code xvals} is smaller
  93.      *         than 5.
  94.      */
  95.     @Override
  96.     public PolynomialSplineFunction interpolate(double[] xvals,
  97.                                                 double[] yvals)
  98.         throws MathIllegalArgumentException {
  99.         if (xvals == null ||
  100.             yvals == null) {
  101.             throw new NullArgumentException();
  102.         }

  103.         MathArrays.checkEqualLength(xvals, yvals);

  104.         if (xvals.length < MINIMUM_NUMBER_POINTS) {
  105.             throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_OF_POINTS,
  106.                                                 xvals.length,
  107.                                                 MINIMUM_NUMBER_POINTS, true);
  108.         }

  109.         MathArrays.checkOrder(xvals);

  110.         final int numberOfDiffAndWeightElements = xvals.length - 1;

  111.         final double[] differences = new double[numberOfDiffAndWeightElements];
  112.         final double[] weights = new double[numberOfDiffAndWeightElements];

  113.         for (int i = 0; i < differences.length; i++) {
  114.             differences[i] = (yvals[i + 1] - yvals[i]) / (xvals[i + 1] - xvals[i]);
  115.         }

  116.         for (int i = 1; i < weights.length; i++) {
  117.             weights[i] = FastMath.abs(differences[i] - differences[i - 1]);
  118.             if (useModifiedWeights) {
  119.                 // modify weights to avoid overshoots near constant slopes sub-samples
  120.                 weights[i] += FastMath.abs(differences[i] + differences[i - 1]);
  121.             }
  122.         }

  123.         // Prepare Hermite interpolation scheme.
  124.         final double[] firstDerivatives = new double[xvals.length];

  125.         for (int i = 2; i < firstDerivatives.length - 2; i++) {
  126.             final double wP = weights[i + 1];
  127.             final double wM = weights[i - 1];
  128.             if (Precision.equals(wP, 0.0) &&
  129.                 Precision.equals(wM, 0.0)) {
  130.                 final double xv = xvals[i];
  131.                 final double xvP = xvals[i + 1];
  132.                 final double xvM = xvals[i - 1];
  133.                 firstDerivatives[i] = (((xvP - xv) * differences[i - 1]) + ((xv - xvM) * differences[i])) / (xvP - xvM);
  134.             } else {
  135.                 firstDerivatives[i] = ((wP * differences[i - 1]) + (wM * differences[i])) / (wP + wM);
  136.             }
  137.         }

  138.         firstDerivatives[0] = differentiateThreePoint(xvals, yvals, 0, 0, 1, 2);
  139.         firstDerivatives[1] = differentiateThreePoint(xvals, yvals, 1, 0, 1, 2);
  140.         firstDerivatives[xvals.length - 2] = differentiateThreePoint(xvals, yvals, xvals.length - 2,
  141.                                                                      xvals.length - 3, xvals.length - 2,
  142.                                                                      xvals.length - 1);
  143.         firstDerivatives[xvals.length - 1] = differentiateThreePoint(xvals, yvals, xvals.length - 1,
  144.                                                                      xvals.length - 3, xvals.length - 2,
  145.                                                                      xvals.length - 1);

  146.         return interpolateHermiteSorted(xvals, yvals, firstDerivatives);

  147.     }

  148.     /**
  149.      * Computes an interpolating function for the data set.
  150.      *
  151.      * @param xvals the arguments for the interpolation points
  152.      * @param yvals the values for the interpolation points
  153.      * @param <T> the type of the field elements
  154.      * @return a function which interpolates the data set
  155.      * @throws MathIllegalArgumentException if {@code xvals} and {@code yvals} have
  156.      *         different sizes.
  157.      * @throws MathIllegalArgumentException if {@code xvals} is not sorted in
  158.      *         strict increasing order.
  159.      * @throws MathIllegalArgumentException if the size of {@code xvals} is smaller
  160.      *         than 5.
  161.      * @since 1.5
  162.      */
  163.     @Override
  164.     public <T extends CalculusFieldElement<T>> FieldPolynomialSplineFunction<T> interpolate(final T[] xvals,
  165.                                                                                         final T[] yvals)
  166.         throws MathIllegalArgumentException {
  167.         if (xvals == null ||
  168.             yvals == null) {
  169.             throw new NullArgumentException();
  170.         }

  171.         MathArrays.checkEqualLength(xvals, yvals);

  172.         if (xvals.length < MINIMUM_NUMBER_POINTS) {
  173.             throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_OF_POINTS,
  174.                                                 xvals.length,
  175.                                                 MINIMUM_NUMBER_POINTS, true);
  176.         }

  177.         MathArrays.checkOrder(xvals);

  178.         final Field<T> field = xvals[0].getField();
  179.         final int numberOfDiffAndWeightElements = xvals.length - 1;

  180.         final T[] differences = MathArrays.buildArray(field, numberOfDiffAndWeightElements);
  181.         final T[] weights     = MathArrays.buildArray(field, numberOfDiffAndWeightElements);

  182.         for (int i = 0; i < differences.length; i++) {
  183.             differences[i] = yvals[i + 1].subtract(yvals[i]).divide(xvals[i + 1].subtract(xvals[i]));
  184.         }

  185.         for (int i = 1; i < weights.length; i++) {
  186.             weights[i] = FastMath.abs(differences[i].subtract(differences[i - 1]));
  187.         }

  188.         // Prepare Hermite interpolation scheme.
  189.         final T[] firstDerivatives = MathArrays.buildArray(field, xvals.length);

  190.         for (int i = 2; i < firstDerivatives.length - 2; i++) {
  191.             final T wP = weights[i + 1];
  192.             final T wM = weights[i - 1];
  193.             if (Precision.equals(wP.getReal(), 0.0) &&
  194.                 Precision.equals(wM.getReal(), 0.0)) {
  195.                 final T xv = xvals[i];
  196.                 final T xvP = xvals[i + 1];
  197.                 final T xvM = xvals[i - 1];
  198.                 firstDerivatives[i] =     xvP.subtract(xv).multiply(differences[i - 1]).
  199.                                       add(xv.subtract(xvM).multiply(differences[i])).
  200.                                       divide(xvP.subtract(xvM));
  201.             } else {
  202.                 firstDerivatives[i] =     wP.multiply(differences[i - 1]).
  203.                                       add(wM.multiply(differences[i])).
  204.                                       divide(wP.add(wM));
  205.             }
  206.         }

  207.         firstDerivatives[0] = differentiateThreePoint(xvals, yvals, 0, 0, 1, 2);
  208.         firstDerivatives[1] = differentiateThreePoint(xvals, yvals, 1, 0, 1, 2);
  209.         firstDerivatives[xvals.length - 2] = differentiateThreePoint(xvals, yvals, xvals.length - 2,
  210.                                                                      xvals.length - 3, xvals.length - 2,
  211.                                                                      xvals.length - 1);
  212.         firstDerivatives[xvals.length - 1] = differentiateThreePoint(xvals, yvals, xvals.length - 1,
  213.                                                                      xvals.length - 3, xvals.length - 2,
  214.                                                                      xvals.length - 1);

  215.         return interpolateHermiteSorted(xvals, yvals, firstDerivatives);

  216.     }

  217.     /**
  218.      * Three point differentiation helper, modeled off of the same method in the
  219.      * Math.NET CubicSpline class. This is used by both the Apache Math and the
  220.      * Math.NET Akima Cubic Spline algorithms
  221.      *
  222.      * @param xvals x values to calculate the numerical derivative with
  223.      * @param yvals y values to calculate the numerical derivative with
  224.      * @param indexOfDifferentiation index of the elemnt we are calculating the derivative around
  225.      * @param indexOfFirstSample index of the first element to sample for the three point method
  226.      * @param indexOfSecondsample index of the second element to sample for the three point method
  227.      * @param indexOfThirdSample index of the third element to sample for the three point method
  228.      * @return the derivative
  229.      */
  230.     private double differentiateThreePoint(double[] xvals, double[] yvals,
  231.                                            int indexOfDifferentiation,
  232.                                            int indexOfFirstSample,
  233.                                            int indexOfSecondsample,
  234.                                            int indexOfThirdSample) {
  235.         final double x0 = yvals[indexOfFirstSample];
  236.         final double x1 = yvals[indexOfSecondsample];
  237.         final double x2 = yvals[indexOfThirdSample];

  238.         final double t = xvals[indexOfDifferentiation] - xvals[indexOfFirstSample];
  239.         final double t1 = xvals[indexOfSecondsample] - xvals[indexOfFirstSample];
  240.         final double t2 = xvals[indexOfThirdSample] - xvals[indexOfFirstSample];

  241.         final double a = (x2 - x0 - (t2 / t1 * (x1 - x0))) / (t2 * t2 - t1 * t2);
  242.         final double b = (x1 - x0 - a * t1 * t1) / t1;

  243.         return (2 * a * t) + b;
  244.     }

  245.     /**
  246.      * Three point differentiation helper, modeled off of the same method in the
  247.      * Math.NET CubicSpline class. This is used by both the Apache Math and the
  248.      * Math.NET Akima Cubic Spline algorithms
  249.      *
  250.      * @param xvals x values to calculate the numerical derivative with
  251.      * @param yvals y values to calculate the numerical derivative with
  252.      * @param <T> the type of the field elements
  253.      * @param indexOfDifferentiation index of the elemnt we are calculating the derivative around
  254.      * @param indexOfFirstSample index of the first element to sample for the three point method
  255.      * @param indexOfSecondsample index of the second element to sample for the three point method
  256.      * @param indexOfThirdSample index of the third element to sample for the three point method
  257.      * @return the derivative
  258.      * @since 1.5
  259.      */
  260.     private <T extends CalculusFieldElement<T>> T differentiateThreePoint(T[] xvals, T[] yvals,
  261.                                                                       int indexOfDifferentiation,
  262.                                                                       int indexOfFirstSample,
  263.                                                                       int indexOfSecondsample,
  264.                                                                       int indexOfThirdSample) {
  265.         final T x0 = yvals[indexOfFirstSample];
  266.         final T x1 = yvals[indexOfSecondsample];
  267.         final T x2 = yvals[indexOfThirdSample];

  268.         final T t = xvals[indexOfDifferentiation].subtract(xvals[indexOfFirstSample]);
  269.         final T t1 = xvals[indexOfSecondsample].subtract(xvals[indexOfFirstSample]);
  270.         final T t2 = xvals[indexOfThirdSample].subtract(xvals[indexOfFirstSample]);

  271.         final T a = x2.subtract(x0).subtract(t2.divide(t1).multiply(x1.subtract(x0))).
  272.                     divide(t2.multiply(t2).subtract(t1.multiply(t2)));
  273.         final T b = x1.subtract(x0).subtract(a.multiply(t1).multiply(t1)).divide(t1);

  274.         return a.multiply(t).multiply(2).add(b);
  275.     }

  276.     /**
  277.      * Creates a Hermite cubic spline interpolation from the set of (x,y) value
  278.      * pairs and their derivatives. This is modeled off of the
  279.      * InterpolateHermiteSorted method in the Math.NET CubicSpline class.
  280.      *
  281.      * @param xvals x values for interpolation
  282.      * @param yvals y values for interpolation
  283.      * @param firstDerivatives first derivative values of the function
  284.      * @return polynomial that fits the function
  285.      */
  286.     private PolynomialSplineFunction interpolateHermiteSorted(double[] xvals,
  287.                                                               double[] yvals,
  288.                                                               double[] firstDerivatives) {
  289.         MathArrays.checkEqualLength(xvals, yvals);
  290.         MathArrays.checkEqualLength(xvals, firstDerivatives);

  291.         final int minimumLength = 2;
  292.         if (xvals.length < minimumLength) {
  293.             throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_OF_POINTS,
  294.                                                 xvals.length, minimumLength,
  295.                                                 true);
  296.         }

  297.         final int size = xvals.length - 1;
  298.         final PolynomialFunction[] polynomials = new PolynomialFunction[size];
  299.         final double[] coefficients = new double[4];

  300.         for (int i = 0; i < polynomials.length; i++) {
  301.             final double w = xvals[i + 1] - xvals[i];
  302.             final double w2 = w * w;

  303.             final double yv = yvals[i];
  304.             final double yvP = yvals[i + 1];

  305.             final double fd = firstDerivatives[i];
  306.             final double fdP = firstDerivatives[i + 1];

  307.             coefficients[0] = yv;
  308.             coefficients[1] = firstDerivatives[i];
  309.             coefficients[2] = (3 * (yvP - yv) / w - 2 * fd - fdP) / w;
  310.             coefficients[3] = (2 * (yv - yvP) / w + fd + fdP) / w2;
  311.             polynomials[i] = new PolynomialFunction(coefficients);
  312.         }

  313.         return new PolynomialSplineFunction(xvals, polynomials);

  314.     }
  315.     /**
  316.      * Creates a Hermite cubic spline interpolation from the set of (x,y) value
  317.      * pairs and their derivatives. This is modeled off of the
  318.      * InterpolateHermiteSorted method in the Math.NET CubicSpline class.
  319.      *
  320.      * @param xvals x values for interpolation
  321.      * @param yvals y values for interpolation
  322.      * @param firstDerivatives first derivative values of the function
  323.      * @param <T> the type of the field elements
  324.      * @return polynomial that fits the function
  325.      * @since 1.5
  326.      */
  327.     private <T extends CalculusFieldElement<T>> FieldPolynomialSplineFunction<T> interpolateHermiteSorted(T[] xvals,
  328.                                                                                                           T[] yvals,
  329.                                                                                                           T[] firstDerivatives) {
  330.         MathArrays.checkEqualLength(xvals, yvals);
  331.         MathArrays.checkEqualLength(xvals, firstDerivatives);

  332.         final int minimumLength = 2;
  333.         if (xvals.length < minimumLength) {
  334.             throw new MathIllegalArgumentException(LocalizedCoreFormats.NUMBER_OF_POINTS,
  335.                                                 xvals.length, minimumLength,
  336.                                                 true);
  337.         }

  338.         final Field<T> field = xvals[0].getField();
  339.         final int size = xvals.length - 1;
  340.         @SuppressWarnings("unchecked")
  341.         final FieldPolynomialFunction<T>[] polynomials =
  342.                         (FieldPolynomialFunction<T>[]) Array.newInstance(FieldPolynomialFunction.class, size);
  343.         final T[] coefficients = MathArrays.buildArray(field, 4);

  344.         for (int i = 0; i < polynomials.length; i++) {
  345.             final T w = xvals[i + 1].subtract(xvals[i]);
  346.             final T w2 = w.multiply(w);

  347.             final T yv = yvals[i];
  348.             final T yvP = yvals[i + 1];

  349.             final T fd = firstDerivatives[i];
  350.             final T fdP = firstDerivatives[i + 1];

  351.             coefficients[0] = yv;
  352.             coefficients[1] = firstDerivatives[i];
  353.             final T ratio = yvP.subtract(yv).divide(w);
  354.             coefficients[2] = ratio.multiply(+3).subtract(fd.add(fd)).subtract(fdP).divide(w);
  355.             coefficients[3] = ratio.multiply(-2).add(fd).add(fdP).divide(w2);
  356.             polynomials[i] = new FieldPolynomialFunction<>(coefficients);
  357.         }

  358.         return new FieldPolynomialSplineFunction<>(xvals, polynomials);

  359.     }
  360. }