BicubicInterpolatingFunction.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.math4.legacy.analysis.interpolation;

import java.util.Arrays;
import java.util.function.DoubleBinaryOperator;
import java.util.function.Function;

import org.apache.commons.numbers.core.Sum;
import org.apache.commons.math4.legacy.analysis.BivariateFunction;
import org.apache.commons.math4.legacy.exception.DimensionMismatchException;
import org.apache.commons.math4.legacy.exception.NoDataException;
import org.apache.commons.math4.legacy.exception.NonMonotonicSequenceException;
import org.apache.commons.math4.legacy.exception.OutOfRangeException;
import org.apache.commons.math4.legacy.core.MathArrays;

/**
 * Function that implements the
 * <a href="http://en.wikipedia.org/wiki/Bicubic_interpolation">
 * bicubic spline interpolation</a>.
 *
 * @since 3.4
 */
public class BicubicInterpolatingFunction
    implements BivariateFunction {
    /** Number of coefficients. */
    private static final int NUM_COEFF = 16;
    /**
     * Matrix to compute the spline coefficients from the function values
     * and function derivatives values.
     */
    private static final double[][] AINV = {
        { 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 },
        { 0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0 },
        { -3,3,0,0,-2,-1,0,0,0,0,0,0,0,0,0,0 },
        { 2,-2,0,0,1,1,0,0,0,0,0,0,0,0,0,0 },
        { 0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0 },
        { 0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0 },
        { 0,0,0,0,0,0,0,0,-3,3,0,0,-2,-1,0,0 },
        { 0,0,0,0,0,0,0,0,2,-2,0,0,1,1,0,0 },
        { -3,0,3,0,0,0,0,0,-2,0,-1,0,0,0,0,0 },
        { 0,0,0,0,-3,0,3,0,0,0,0,0,-2,0,-1,0 },
        { 9,-9,-9,9,6,3,-6,-3,6,-6,3,-3,4,2,2,1 },
        { -6,6,6,-6,-3,-3,3,3,-4,4,-2,2,-2,-2,-1,-1 },
        { 2,0,-2,0,0,0,0,0,1,0,1,0,0,0,0,0 },
        { 0,0,0,0,2,0,-2,0,0,0,0,0,1,0,1,0 },
        { -6,6,6,-6,-4,-2,4,2,-3,3,-3,3,-2,-1,-2,-1 },
        { 4,-4,-4,4,2,2,-2,-2,2,-2,2,-2,1,1,1,1 }
    };

    /** Samples x-coordinates. */
    private final double[] xval;
    /** Samples y-coordinates. */
    private final double[] yval;
    /** Set of cubic splines patching the whole data grid. */
    private final BicubicFunction[][] splines;

    /**
     * @param x Sample values of the x-coordinate, in increasing order.
     * @param y Sample values of the y-coordinate, in increasing order.
     * @param f Values of the function on every grid point.
     * @param dFdX Values of the partial derivative of function with respect
     * to x on every grid point.
     * @param dFdY Values of the partial derivative of function with respect
     * to y on every grid point.
     * @param d2FdXdY Values of the cross partial derivative of function on
     * every grid point.
     * @throws DimensionMismatchException if the various arrays do not contain
     * the expected number of elements.
     * @throws NonMonotonicSequenceException if {@code x} or {@code y} are
     * not strictly increasing.
     * @throws NoDataException if any of the arrays has zero length.
     */
    public BicubicInterpolatingFunction(double[] x,
                                        double[] y,
                                        double[][] f,
                                        double[][] dFdX,
                                        double[][] dFdY,
                                        double[][] d2FdXdY)
        throws DimensionMismatchException,
               NoDataException,
               NonMonotonicSequenceException {
        this(x, y, f, dFdX, dFdY, d2FdXdY, false);
    }

    /**
     * @param x Sample values of the x-coordinate, in increasing order.
     * @param y Sample values of the y-coordinate, in increasing order.
     * @param f Values of the function on every grid point.
     * @param dFdX Values of the partial derivative of function with respect
     * to x on every grid point.
     * @param dFdY Values of the partial derivative of function with respect
     * to y on every grid point.
     * @param d2FdXdY Values of the cross partial derivative of function on
     * every grid point.
     * @param initializeDerivatives Whether to initialize the internal data
     * needed for calling any of the methods that compute the partial derivatives
     * this function.
     * @throws DimensionMismatchException if the various arrays do not contain
     * the expected number of elements.
     * @throws NonMonotonicSequenceException if {@code x} or {@code y} are
     * not strictly increasing.
     * @throws NoDataException if any of the arrays has zero length.
     */
    public BicubicInterpolatingFunction(double[] x,
                                        double[] y,
                                        double[][] f,
                                        double[][] dFdX,
                                        double[][] dFdY,
                                        double[][] d2FdXdY,
                                        boolean initializeDerivatives)
        throws DimensionMismatchException,
               NoDataException,
               NonMonotonicSequenceException {
        final int xLen = x.length;
        final int yLen = y.length;

        if (xLen == 0 || yLen == 0 || f.length == 0 || f[0].length == 0) {
            throw new NoDataException();
        }
        if (xLen != f.length) {
            throw new DimensionMismatchException(xLen, f.length);
        }
        if (xLen != dFdX.length) {
            throw new DimensionMismatchException(xLen, dFdX.length);
        }
        if (xLen != dFdY.length) {
            throw new DimensionMismatchException(xLen, dFdY.length);
        }
        if (xLen != d2FdXdY.length) {
            throw new DimensionMismatchException(xLen, d2FdXdY.length);
        }

        MathArrays.checkOrder(x);
        MathArrays.checkOrder(y);

        xval = x.clone();
        yval = y.clone();

        final int lastI = xLen - 1;
        final int lastJ = yLen - 1;
        splines = new BicubicFunction[lastI][lastJ];

        for (int i = 0; i < lastI; i++) {
            if (f[i].length != yLen) {
                throw new DimensionMismatchException(f[i].length, yLen);
            }
            if (dFdX[i].length != yLen) {
                throw new DimensionMismatchException(dFdX[i].length, yLen);
            }
            if (dFdY[i].length != yLen) {
                throw new DimensionMismatchException(dFdY[i].length, yLen);
            }
            if (d2FdXdY[i].length != yLen) {
                throw new DimensionMismatchException(d2FdXdY[i].length, yLen);
            }
            final int ip1 = i + 1;
            final double xR = xval[ip1] - xval[i];
            for (int j = 0; j < lastJ; j++) {
                final int jp1 = j + 1;
                final double yR = yval[jp1] - yval[j];
                final double xRyR = xR * yR;
                final double[] beta = new double[] {
                    f[i][j], f[ip1][j], f[i][jp1], f[ip1][jp1],
                    dFdX[i][j] * xR, dFdX[ip1][j] * xR, dFdX[i][jp1] * xR, dFdX[ip1][jp1] * xR,
                    dFdY[i][j] * yR, dFdY[ip1][j] * yR, dFdY[i][jp1] * yR, dFdY[ip1][jp1] * yR,
                    d2FdXdY[i][j] * xRyR, d2FdXdY[ip1][j] * xRyR, d2FdXdY[i][jp1] * xRyR, d2FdXdY[ip1][jp1] * xRyR
                };

                splines[i][j] = new BicubicFunction(computeSplineCoefficients(beta),
                                                    xR,
                                                    yR,
                                                    initializeDerivatives);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public double value(double x, double y)
        throws OutOfRangeException {
        final int i = searchIndex(x, xval);
        final int j = searchIndex(y, yval);

        final double xN = (x - xval[i]) / (xval[i + 1] - xval[i]);
        final double yN = (y - yval[j]) / (yval[j + 1] - yval[j]);

        return splines[i][j].value(xN, yN);
    }

    /**
     * Indicates whether a point is within the interpolation range.
     *
     * @param x First coordinate.
     * @param y Second coordinate.
     * @return {@code true} if (x, y) is a valid point.
     */
    public boolean isValidPoint(double x, double y) {
        return !(x < xval[0] ||
            x > xval[xval.length - 1] ||
            y < yval[0] ||
            y > yval[yval.length - 1]);
    }

    /**
     * @return the first partial derivative respect to x.
     * @throws NullPointerException if the internal data were not initialized
     * (cf. {@link #BicubicInterpolatingFunction(double[],double[],double[][],
     *             double[][],double[][],double[][],boolean) constructor}).
     */
    public DoubleBinaryOperator partialDerivativeX() {
        return partialDerivative(BicubicFunction::partialDerivativeX);
    }

    /**
     * @return the first partial derivative respect to y.
     * @throws NullPointerException if the internal data were not initialized
     * (cf. {@link #BicubicInterpolatingFunction(double[],double[],double[][],
     *             double[][],double[][],double[][],boolean) constructor}).
     */
    public DoubleBinaryOperator partialDerivativeY() {
        return partialDerivative(BicubicFunction::partialDerivativeY);
    }

    /**
     * @return the second partial derivative respect to x.
     * @throws NullPointerException if the internal data were not initialized
     * (cf. {@link #BicubicInterpolatingFunction(double[],double[],double[][],
     *             double[][],double[][],double[][],boolean) constructor}).
     */
    public DoubleBinaryOperator partialDerivativeXX() {
        return partialDerivative(BicubicFunction::partialDerivativeXX);
    }

    /**
     * @return the second partial derivative respect to y.
     * @throws NullPointerException if the internal data were not initialized
     * (cf. {@link #BicubicInterpolatingFunction(double[],double[],double[][],
     *             double[][],double[][],double[][],boolean) constructor}).
     */
    public DoubleBinaryOperator partialDerivativeYY() {
        return partialDerivative(BicubicFunction::partialDerivativeYY);
    }

    /**
     * @return the second partial cross derivative.
     * @throws NullPointerException if the internal data were not initialized
     * (cf. {@link #BicubicInterpolatingFunction(double[],double[],double[][],
     *             double[][],double[][],double[][],boolean) constructor}).
     */
    public DoubleBinaryOperator partialDerivativeXY() {
        return partialDerivative(BicubicFunction::partialDerivativeXY);
    }

    /**
     * @param which derivative function to apply.
     * @return the selected partial derivative.
     * @throws NullPointerException if the internal data were not initialized
     * (cf. {@link #BicubicInterpolatingFunction(double[],double[],double[][],
     *             double[][],double[][],double[][],boolean) constructor}).
     */
    private DoubleBinaryOperator partialDerivative(Function<BicubicFunction, BivariateFunction> which) {
        return (x, y) -> {
            final int i = searchIndex(x, xval);
            final int j = searchIndex(y, yval);

            final double xN = (x - xval[i]) / (xval[i + 1] - xval[i]);
            final double yN = (y - yval[j]) / (yval[j + 1] - yval[j]);

            return which.apply(splines[i][j]).value(xN, yN);
        };
    }

    /**
     * @param c Coordinate.
     * @param val Coordinate samples.
     * @return the index in {@code val} corresponding to the interval
     * containing {@code c}.
     * @throws OutOfRangeException if {@code c} is out of the
     * range defined by the boundary values of {@code val}.
     */
    private static int searchIndex(double c, double[] val) {
        final int r = Arrays.binarySearch(val, c);

        if (r == -1 ||
            r == -val.length - 1) {
            throw new OutOfRangeException(c, val[0], val[val.length - 1]);
        }

        if (r < 0) {
            // "c" in within an interpolation sub-interval: Return the
            // index of the sample at the lower end of the sub-interval.
            return -r - 2;
        }
        final int last = val.length - 1;
        if (r == last) {
            // "c" is the last sample of the range: Return the index
            // of the sample at the lower end of the last sub-interval.
            return last - 1;
        }

        // "c" is another sample point.
        return r;
    }

    /**
     * Compute the spline coefficients from the list of function values and
     * function partial derivatives values at the four corners of a grid
     * element. They must be specified in the following order:
     * <ul>
     *  <li>f(0,0)</li>
     *  <li>f(1,0)</li>
     *  <li>f(0,1)</li>
     *  <li>f(1,1)</li>
     *  <li>f<sub>x</sub>(0,0)</li>
     *  <li>f<sub>x</sub>(1,0)</li>
     *  <li>f<sub>x</sub>(0,1)</li>
     *  <li>f<sub>x</sub>(1,1)</li>
     *  <li>f<sub>y</sub>(0,0)</li>
     *  <li>f<sub>y</sub>(1,0)</li>
     *  <li>f<sub>y</sub>(0,1)</li>
     *  <li>f<sub>y</sub>(1,1)</li>
     *  <li>f<sub>xy</sub>(0,0)</li>
     *  <li>f<sub>xy</sub>(1,0)</li>
     *  <li>f<sub>xy</sub>(0,1)</li>
     *  <li>f<sub>xy</sub>(1,1)</li>
     * </ul>
     * where the subscripts indicate the partial derivative with respect to
     * the corresponding variable(s).
     *
     * @param beta List of function values and function partial derivatives
     * values.
     * @return the spline coefficients.
     */
    private static double[] computeSplineCoefficients(double[] beta) {
        final double[] a = new double[NUM_COEFF];

        for (int i = 0; i < NUM_COEFF; i++) {
            double result = 0;
            final double[] row = AINV[i];
            for (int j = 0; j < NUM_COEFF; j++) {
                result += row[j] * beta[j];
            }
            a[i] = result;
        }

        return a;
    }

    /**
     * Bicubic function.
     */
    static final class BicubicFunction implements BivariateFunction {
        /** Number of points. */
        private static final short N = 4;
        /** Coefficients. */
        private final double[][] a;
        /** First partial derivative along x. */
        private final BivariateFunction partialDerivativeX;
        /** First partial derivative along y. */
        private final BivariateFunction partialDerivativeY;
        /** Second partial derivative along x. */
        private final BivariateFunction partialDerivativeXX;
        /** Second partial derivative along y. */
        private final BivariateFunction partialDerivativeYY;
        /** Second crossed partial derivative. */
        private final BivariateFunction partialDerivativeXY;

        /**
         * Simple constructor.
         *
         * @param coeff Spline coefficients.
         * @param xR x spacing.
         * @param yR y spacing.
         * @param initializeDerivatives Whether to initialize the internal data
         * needed for calling any of the methods that compute the partial derivatives
         * this function.
         */
        BicubicFunction(double[] coeff,
                        double xR,
                        double yR,
                        boolean initializeDerivatives) {
            a = new double[N][N];
            for (int j = 0; j < N; j++) {
                final double[] aJ = a[j];
                for (int i = 0; i < N; i++) {
                    aJ[i] = coeff[i * N + j];
                }
            }

            if (initializeDerivatives) {
                // Compute all partial derivatives functions.
                final double[][] aX = new double[N][N];
                final double[][] aY = new double[N][N];
                final double[][] aXX = new double[N][N];
                final double[][] aYY = new double[N][N];
                final double[][] aXY = new double[N][N];

                for (int i = 0; i < N; i++) {
                    for (int j = 0; j < N; j++) {
                        final double c = a[i][j];
                        aX[i][j] = i * c;
                        aY[i][j] = j * c;
                        aXX[i][j] = (i - 1) * aX[i][j];
                        aYY[i][j] = (j - 1) * aY[i][j];
                        aXY[i][j] = j * aX[i][j];
                    }
                }

                partialDerivativeX = (double x, double y) -> {
                    final double x2 = x * x;
                    final double[] pX = {0, 1, x, x2};

                    final double y2 = y * y;
                    final double y3 = y2 * y;
                    final double[] pY = {1, y, y2, y3};

                    return apply(pX, 1, pY, 0, aX) / xR;
                };
                partialDerivativeY = (double x, double y) -> {
                    final double x2 = x * x;
                    final double x3 = x2 * x;
                    final double[] pX = {1, x, x2, x3};

                    final double y2 = y * y;
                    final double[] pY = {0, 1, y, y2};

                    return apply(pX, 0, pY, 1, aY) / yR;
                };
                partialDerivativeXX = (double x, double y) -> {
                    final double[] pX = {0, 0, 1, x};

                    final double y2 = y * y;
                    final double y3 = y2 * y;
                    final double[] pY = {1, y, y2, y3};

                    return apply(pX, 2, pY, 0, aXX) / (xR * xR);
                };
                partialDerivativeYY = (double x, double y) -> {
                    final double x2 = x * x;
                    final double x3 = x2 * x;
                    final double[] pX = {1, x, x2, x3};

                    final double[] pY = {0, 0, 1, y};

                    return apply(pX, 0, pY, 2, aYY) / (yR * yR);
                };
                partialDerivativeXY = (double x, double y) -> {
                    final double x2 = x * x;
                    final double[] pX = {0, 1, x, x2};

                    final double y2 = y * y;
                    final double[] pY = {0, 1, y, y2};

                    return apply(pX, 1, pY, 1, aXY) / (xR * yR);
                };
            } else {
                partialDerivativeX = null;
                partialDerivativeY = null;
                partialDerivativeXX = null;
                partialDerivativeYY = null;
                partialDerivativeXY = null;
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public double value(double x, double y) {
            if (x < 0 || x > 1) {
                throw new OutOfRangeException(x, 0, 1);
            }
            if (y < 0 || y > 1) {
                throw new OutOfRangeException(y, 0, 1);
            }

            final double x2 = x * x;
            final double x3 = x2 * x;
            final double[] pX = {1, x, x2, x3};

            final double y2 = y * y;
            final double y3 = y2 * y;
            final double[] pY = {1, y, y2, y3};

            return apply(pX, 0, pY, 0, a);
        }

        /**
         * Compute the value of the bicubic polynomial.
         *
         * <p>Assumes the powers are zero below the provided index, and 1 at the provided
         * index. This allows skipping some zero products and optimising multiplication
         * by one.
         *
         * @param pX Powers of the x-coordinate.
         * @param i Index of pX[i] == 1
         * @param pY Powers of the y-coordinate.
         * @param j Index of pX[j] == 1
         * @param coeff Spline coefficients.
         * @return the interpolated value.
         */
        private static double apply(double[] pX, int i, double[] pY, int j, double[][] coeff) {
            // assert pX[i] == 1
            double result = sumOfProducts(coeff[i], pY, j);
            while (++i < N) {
                final double r = sumOfProducts(coeff[i], pY, j);
                result += r * pX[i];
            }
            return result;
        }

        /**
         * Compute the sum of products starting from the provided index.
         * Assumes that factor {@code b[j] == 1}.
         *
         * @param a Factors.
         * @param b Factors.
         * @param j Index to initialise the sum.
         * @return the double
         */
        private static double sumOfProducts(double[] a, double[] b, int j) {
            // assert b[j] == 1
            final Sum sum = Sum.of(a[j]);
            while (++j < N) {
                sum.addProduct(a[j], b[j]);
            }
            return sum.getAsDouble();
        }

        /**
         * @return the partial derivative wrt {@code x}.
         */
        BivariateFunction partialDerivativeX() {
            return partialDerivativeX;
        }

        /**
         * @return the partial derivative wrt {@code y}.
         */
        BivariateFunction partialDerivativeY() {
            return partialDerivativeY;
        }

        /**
         * @return the second partial derivative wrt {@code x}.
         */
        BivariateFunction partialDerivativeXX() {
            return partialDerivativeXX;
        }

        /**
         * @return the second partial derivative wrt {@code y}.
         */
        BivariateFunction partialDerivativeYY() {
            return partialDerivativeYY;
        }

        /**
         * @return the second partial cross-derivative.
         */
        BivariateFunction partialDerivativeXY() {
            return partialDerivativeXY;
        }
    }
}