"""
Unit tests for the standalone interpolator.
"""
from copy import deepcopy
import unittest

import numpy as np

from openmdao.components.interp_util.interp import InterpND, SPLINE_METHODS, TABLE_METHODS
from openmdao.components.interp_util.interp_semi import InterpNDSemi
from openmdao.components.interp_util.outofbounds_error import OutOfBoundsError
from openmdao.utils.assert_utils import assert_near_equal, assert_equal_arrays

def rel_error(actual, computed):
    return np.linalg.norm(actual - computed) / np.linalg.norm(actual)

scipy_gte_019 = True
try:
    from scipy.interpolate._bsplines import make_interp_spline
except ImportError:
    scipy_gte_019 = False


class InterpNDStandaloneFeatureTestcase(unittest.TestCase):

    def test_interp_spline_akima(self):

        xcp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])
        ycp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])
        n = 50
        x = np.linspace(1.0, 12.0, n)

        interp = InterpND(method='akima', points=xcp, x_interp=x, delta_x=0.1)
        y = interp.evaluate_spline(ycp)

        assert_near_equal(y,
                          np.array([ 5.       ,  7.20902005,  9.21276849, 10.81097162, 11.80335574,
                                     12.1278001 , 12.35869145, 12.58588536, 12.81022332, 13.03254681,
                                     13.25369732, 13.47451633, 13.69584534, 13.91852582, 14.14281484,
                                     14.36710105, 14.59128625, 14.81544619, 15.03965664, 15.26399335,
                                     15.48853209, 15.7133486 , 15.93851866, 16.16573502, 16.39927111,
                                     16.63928669, 16.8857123 , 17.1384785 , 17.39751585, 17.66275489,
                                     17.93412619, 18.21156029, 18.49498776, 18.78433915, 19.07954501,
                                     19.38053589, 19.68724235, 19.99959495, 20.31752423, 20.64096076,
                                     20.96983509, 21.37579297, 21.94811407, 22.66809748, 23.51629844,
                                     24.47327219, 25.51957398, 26.63575905, 27.80238264, 29.        ]),
                          tolerance=1e-6)

    def test_interp_spline_akima_derivs(self):

        xcp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])
        ycp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])
        n = 5
        x = np.linspace(1.0, 12.0, n)

        interp = InterpND(method='akima', points=xcp, x_interp=x, delta_x=0.1)
        y, dy_dycp = interp.evaluate_spline(ycp, compute_derivative=True)

        assert_near_equal(dy_dycp,
                          np.array([[ 1.00000000e+00,  0.00000000e+00,  0.00000000e+00,
                                      0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
                                    [-1.86761492e-06,  3.31278014e-02,  1.05874907e+00,
                                     -9.18750000e-02,  0.00000000e+00,  0.00000000e+00],
                                    [ 0.00000000e+00,  0.00000000e+00, -2.10964627e-01,
                                      1.19119941e+00,  2.02602810e-02, -4.95062934e-04],
                                    [ 0.00000000e+00,  0.00000000e+00, -2.64126732e-01,
                                      5.82784977e-01,  6.83151998e-01, -1.81024253e-03],
                                    [ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
                                      0.00000000e+00,  0.00000000e+00,  1.00000000e+00]]),
                          tolerance=1e-6)

    def test_interp_spline_bsplines(self):

        xcp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])
        ycp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])
        n = 50
        x = np.linspace(1.0, 12.0, n)

        interp = InterpND(method='bsplines', num_cp=6, x_interp=x)

        y = interp.evaluate_spline(ycp)

        assert_near_equal(y,
                          np.array([ 9.21614583,  9.90911525, 10.52244151, 11.06231159, 11.53491244,
                                     11.94643105, 12.30305438, 12.61096939, 12.87636305, 13.10542234,
                                     13.30433422, 13.47928566, 13.63646363, 13.7820551 , 13.92203064,
                                     14.05954727, 14.19579437, 14.33192094, 14.46907599, 14.60840854,
                                     14.75106758, 14.89820214, 15.05096121, 15.2104938 , 15.37794893,
                                     15.5544756 , 15.74122282, 15.9393396 , 16.14997495, 16.37427787,
                                     16.61339737, 16.86848247, 17.14102103, 17.43486416, 17.75486932,
                                     18.10589772, 18.49281052, 18.92046894, 19.39373414, 19.91746734,
                                     20.4965297 , 21.13578243, 21.8400867 , 22.61430372, 23.46329467,
                                     24.39192074, 25.40504312, 26.507523  , 27.70422156, 29.        ]),
                          tolerance=1e-6)

    def test_table_interp(self):

        # create input param training data, of sizes 25, 5, and 10 points resp.
        p1 = np.linspace(0, 100, 25)
        p2 = np.linspace(-10, 10, 5)
        p3 = np.linspace(0, 1, 10)

        # can use meshgrid to create a 3D array of test data
        P1, P2, P3 = np.meshgrid(p1, p2, p3, indexing='ij')
        f = np.sqrt(P1) + P2 * P3

        interp = InterpND(method='lagrange3', points=(p1, p2, p3), values=f)

        x = np.array([55.12, -2.14, 0.323])
        f, df_dx = interp.interpolate(x, compute_derivative=True)

        actual = np.array([6.73306794])
        deriv_actual = np.array([[ 0.06734927, 0.323 , -2.14]])

        assert_near_equal(f, actual, tolerance=1e-7)
        assert_near_equal(df_dx, deriv_actual, tolerance=1e-7)

    def test_cs_across_interp(self):
        # The standalone interpolator is used inside of components, so the imaginary part must
        # be carried through all operations to the outputs.
        xcp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])
        ycp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])
        n = 50
        x = np.linspace(1.0, 12.0, n)

        ycp = np.array([[5.0 + 1j, 12.0, 14.0, 16.0, 21.0, 29.0],
                        [5.0, 12.0 + 1j, 14.0, 16.0, 21.0, 29.0]])

        for method in SPLINE_METHODS:

            # complex step not supported on scipy methods
            if method.startswith('scipy'):
                continue

            interp = InterpND(method=method, points=xcp, x_interp=x)
            y, dy = interp.evaluate_spline(ycp, compute_derivative=True)

            self.assertTrue(y.dtype == complex)

            if method in ['akima']:
                # Derivs depend on values only for akima.
                self.assertTrue(dy.dtype == complex)

        p1 = np.linspace(0, 100, 25)
        p2 = np.linspace(-10, 10, 5)
        p3 = np.linspace(0, 1, 10)

        # can use meshgrid to create a 3D array of test data
        P1, P2, P3 = np.meshgrid(p1, p2, p3, indexing='ij')
        f = np.sqrt(P1) + P2 * P3

        x = np.array([[55.12 + 1j, -2.14, 0.323],
                      [55.12, -2.14 + 1j, 0.323],
                      [55.12, -2.14, 0.323 + 1j]])

        for method in TABLE_METHODS:

            # complex step not supported on scipy methods
            if method.startswith('scipy'):
                continue

            if method in ['akima1D']:
                # These methods are for fixed grids other than 3d.
                continue

            interp = InterpND(method=method, points=(p1, p2, p3), values=f)
            y, dy = interp.interpolate(x, compute_derivative=True)

            self.assertTrue(y.dtype == complex)
            self.assertTrue(dy.dtype == complex)


class TestInterpNDSemiPython(unittest.TestCase):
    """Tests for the standalone semi structured interp."""

    def setUp(self):
        self.interp_configs = {
            "slinear": 2,
            "lagrange2": 3,
            "lagrange3": 4,
            "akima": 4,
        }
        self.interp_methods = self.interp_configs.keys()

        self.tol = {
            "slinear": 5e-2,
            "lagrange2": 5e-2,
            "lagrange3": 1e-4,
            "akima": 1e-3,
        }

    def _get_sample_2d(self):
        # test problem with enough points for smooth spline fits
        def f(u, v):
            return u * np.cos(u * v) + v * np.sin(u * v)

        def df(u, v):
            return (-u * v * np.sin(u * v) + v**2 * np.cos(u * v) +
                    np.cos(u * v),
                    -u**2 * np.sin(u * v) + u * v * np.cos(u * v) +
                    np.sin(u * v))

        # uniformly spaced axis
        u = np.linspace(0, 3, 50)
        # randomly spaced axis
        np.random.seed(7590)
        v = np.random.uniform(0, 3, 50)
        v.sort()

        points = [u, v]
        values = f(*np.meshgrid(*points, indexing='ij'))
        return points, values, f, df

    def _get_sample_4d_large(self):
        def f(x, y, z, w):
            return x**2 + y**2 + z**2 + w**2
        X = np.linspace(-10, 10, 6)
        Y = np.linspace(-10, 10, 7)
        np.random.seed(0)
        Z = np.random.uniform(-10, 10, 6)
        Z.sort()
        W = np.linspace(-10, 10, 8)
        points = [X, Y, Z, W]
        values = f(*np.meshgrid(*points, indexing='ij'))
        return points, values

    def test_2d(self):
        # test interpolated values
        points, values, func, df = self._get_sample_2d()
        np.random.seed(1)
        X, Y = np.meshgrid(*points, indexing='ij')
        X = X.ravel()
        Y = Y.ravel()
        grid = np.array([X, Y]).T

        test_pt = np.random.uniform(0, 3, 2)
        actual = func(*test_pt)
        for method in self.interp_methods:
            interp = InterpNDSemi(grid, values.ravel(), method=method)
            computed = interp.interpolate(test_pt)
            r_err = rel_error(actual, computed)
            assert r_err < self.tol[method]

    def test_minimum_required_gridsize(self):
        for method in self.interp_methods:

            k = self.interp_configs[method] - 1
            x = np.linspace(0, 1, k)
            y = np.linspace(0, 1, k)

            points = [x, y]
            X, Y = np.meshgrid(*points, indexing='ij')
            X = X.ravel()
            Y = Y.ravel()
            values = X + Y
            grid = np.array([X, Y]).T
            with self.assertRaises(ValueError) as cm:
                interp = InterpNDSemi(grid, values, method=method)

            msg = 'There are {} points in a data dimension, but method'.format(k)
            self.assertTrue(str(cm.exception).startswith(msg))

    def test_NaN_exception(self):
        np.random.seed(1234)
        x = np.linspace(0, 2, 5)
        y = np.linspace(0, 1, 7)
        points = [x, y]
        X, Y = np.meshgrid(*points, indexing='ij')
        X = X.ravel()
        Y = Y.ravel()
        grid = np.array([X, Y]).T
        values = np.random.rand(5, 7).ravel()
        interp = InterpNDSemi(grid, values, method='slinear', extrapolate=False)

        with self.assertRaises(OutOfBoundsError) as cm:
            interp.interpolate(np.array([1, np.nan]))

        err = cm.exception

        self.assertEqual(str(err), 'One of the requested xi contains a NaN')
        self.assertEqual(err.idx, 1)
        self.assertTrue(np.isnan(err.value))

    def test_error_messages(self):
        points, values = self._get_sample_4d_large()
        X, Y, Z, W = np.meshgrid(*points, indexing='ij')
        X = X.ravel()
        Y = Y.ravel()
        Z = Z.ravel()
        W = W.ravel()
        grid = np.array([X, Y, Z, W]).T
        values = values.ravel()

        with self.assertRaises(ValueError) as cm:
            interp = InterpNDSemi(grid, values, method='junk')

        msg = ('Interpolation method "junk" is not defined. Valid methods are')
        self.assertTrue(cm.exception.args[0].startswith(msg))

        with self.assertRaises(ValueError) as cm:
            interp = InterpNDSemi(grid, values, method=points)

        msg = ("Argument 'method' should be a string.")
        self.assertTrue(cm.exception.args[0].startswith(msg))

        with self.assertRaises(ValueError) as cm:
            interp = InterpNDSemi(grid, values[:-1], method='slinear')

        msg = ('There are 2016 point arrays, but 2015 values.')
        self.assertEqual(cm.exception.args[0], msg)

        badgrid = deepcopy(grid)
        badgrid[0][0] = -6.0
        with self.assertRaises(ValueError) as cm:
            interp = InterpNDSemi(badgrid, values, method='slinear')

        msg = ('The points in dimension 0 must be strictly ascending.')
        self.assertEqual(cm.exception.args[0], msg)

        with self.assertRaises(KeyError) as cm:
            interp = InterpNDSemi(grid, values, method='slinear', bad_arg=1)

        msg = ("Option 'bad_arg' cannot be set because it has not been declared.")
        self.assertTrue(cm.exception.args[0].endswith(msg))


class TestInterpNDPython(unittest.TestCase):

    """Tests for the non-scipy interpolation algorithms."""

    def setUp(self):
        self.interp_configs = {
            "slinear": 2,
            "cubic": 3,
            "lagrange2": 3,
            "lagrange3": 4,
            "akima": 4,
            "scipy_slinear": 1,
            "scipy_cubic": 3,
            "scipy_quintic": 5,
        }
        self.spline_configs = {
            "slinear": 2,
            "cubic": 3,
            "lagrange2": 3,
            "lagrange3": 4,
            "akima": 4,
            "bsplines": 4,
            "scipy_slinear": 1,
            "scipy_cubic": 3,
            "scipy_quintic": 5,
        }
        self.interp_methods = self.interp_configs.keys()
        self.spline_methods = self.spline_configs.keys()

        self.tol = {
            "slinear": 5e-2,
            "lagrange2": 5e-2,
            "lagrange3": 1e-4,
            "cubic": 1e-4,
            "akima": 1e-3,
            "bsplines": 1e-1,
            "scipy_slinear": 5e-2,
            "scipy_cubic": 1e-4,
            "scipy_quintic": 1e-6,
        }

    def _get_sample_1d(self):
        # test problem with enough points for smooth spline fits
        def f(u):
            return 2.0 * np.cos(u)

        def df(u):
            return -2.0 * np.sin(u)

        # uniformly spaced axis
        u = np.linspace(0, 3, 50)

        points = [u]
        values = f(u)
        return points, values, f, df

    def _get_sample_2d(self):
        # test problem with enough points for smooth spline fits
        def f(u, v):
            return u * np.cos(u * v) + v * np.sin(u * v)

        def df(u, v):
            return (-u * v * np.sin(u * v) + v**2 * np.cos(u * v) +
                    np.cos(u * v),
                    -u**2 * np.sin(u * v) + u * v * np.cos(u * v) +
                    np.sin(u * v))

        # uniformly spaced axis
        u = np.linspace(0, 3, 50)
        # randomly spaced axis
        np.random.seed(7590)
        v = np.random.uniform(0, 3, 50)
        v.sort()

        points = [u, v]
        values = f(*np.meshgrid(*points, indexing='ij'))
        return points, values, f, df

    def _get_sample_4d_large(self):
        def f(x, y, z, w):
            return x**2 + y**2 + z**2 + w**2
        X = np.linspace(-10, 10, 6)
        Y = np.linspace(-10, 10, 7)
        np.random.seed(0)
        Z = np.random.uniform(-10, 10, 6)
        Z.sort()
        W = np.linspace(-10, 10, 8)
        points = [X, Y, Z, W]
        values = f(*np.meshgrid(*points, indexing='ij'))
        return points, values

    def test_minimum_required_gridsize(self):
        for method in self.interp_methods:

            # Scipy does order reduction as needed.
            if method.startswith('scipy'):
                continue

            k = self.interp_configs[method] - 1
            x = np.linspace(0, 1, k)
            y = np.linspace(0, 1, k)
            points = [x, y]
            X, Y = np.meshgrid(*points, indexing='ij')
            values = X + Y
            #self.assertRaises(ValueError, InterpND, points, values, method)
            with self.assertRaises(ValueError) as cm:
                interp = InterpND(method=method, points=points, values=values)

            msg = 'There are {} points in a data dimension, but method'.format(k)
            self.assertTrue(str(cm.exception).startswith(msg))

    def test_spline_single_dim(self):
        # test interpolated values
        points, values, func, df = self._get_sample_1d()
        test_pt = np.array([[0.76], [.33]])
        actual = func(test_pt).flatten()
        for method in self.interp_methods:
            interp = InterpND(method=method, points=points, values=values)
            computed = interp.interpolate(test_pt)
            r_err = rel_error(actual, computed)
            assert r_err < self.tol[method]

    def test_spline_xi1d(self):
        # test interpolated values
        points, values, func, df = self._get_sample_2d()
        np.random.seed(1)
        test_pt = np.random.uniform(0, 3, 2)
        actual = func(*test_pt)
        for method in self.interp_methods:
            interp = InterpND(method=method, points=points, values=values)
            computed = interp.interpolate(test_pt)
            r_err = rel_error(actual, computed)
            assert r_err < self.tol[method]

    def test_spline_out_of_bounds_extrap(self):
        points, values, func, df = self. _get_sample_2d()
        np.random.seed(5)
        test_pt = np.random.uniform(3, 3.1, 2)
        actual = func(*test_pt)
        gradient = np.array(df(*test_pt))
        tol = 1e-1
        for method in self.interp_methods:
            k = self.interp_configs[method]
            if method == 'slinear':
                tol = 2
            interp = InterpND(method=method, points=points, values=values,
                              extrapolate=True)
            computed, computed_grad = interp.interpolate(test_pt, compute_derivative=True)
            computed_grad = interp.gradient(test_pt)
            r_err = rel_error(actual, computed)
            assert r_err < tol

            r_err = rel_error(gradient, computed_grad)
            # extrapolated gradients are even trickier, but usable still
            assert r_err < 2 * tol

    def test_spline_xi3d(self):
        points, values, func, df = self. _get_sample_2d()
        np.random.seed(1)
        test_pt = np.random.uniform(0, 3, 6).reshape(3, 2)
        actual = func(*test_pt.T)
        for method in self.interp_methods:
            interp = InterpND(method=method, points=points, values=values)
            computed = interp.interpolate(test_pt)
            r_err = rel_error(actual, computed)
            #print(method, computed, actual, r_err)
            assert r_err < self.tol[method]

    def test_spline_xi3d_akima_delta_x(self):
        points, values, func, df = self. _get_sample_2d()
        np.random.seed(1)
        test_pt = np.random.uniform(0, 3, 6).reshape(3, 2)
        actual = func(*test_pt.T)
        interp = InterpND(method='akima', points=points, values=values, delta_x=0.01)
        computed = interp.interpolate(test_pt)
        r_err = rel_error(actual, computed)
        #print('akima', computed, actual, r_err)
        assert r_err < self.tol['akima']

    def test_spline_deriv_xi1d(self):
        # tests gradient values
        points, values, func, df = self._get_sample_2d()
        np.random.seed(1234)
        test_pt = np.random.uniform(0, 3, 2)
        actual = np.array(df(*test_pt))
        for method in self.interp_methods:
            interp = InterpND(method=method,points=points, values=values)
            computed = interp.gradient(test_pt)
            r_err = rel_error(actual, computed)
            assert r_err < 2.5 * self.tol[method]

            # test that gradients have been cached
            assert_equal_arrays(interp._xi.flatten(), test_pt.flatten())
            assert_equal_arrays(interp._d_dx.flatten(), computed.flatten())

    def test_gradients_returned_by_xi(self):
        # verifies that gradients with respect to xi are returned if cached
        points, values, func, df = self._get_sample_2d()
        np.random.seed(4321)
        for method in self.interp_methods:
            interp = InterpND(method=method, points=points, values=values)
            x = np.array([0.9, 0.1])
            interp._xi = x
            dy = np.array([0.997901, 0.08915])
            interp._d_dx = dy
            assert_near_equal(interp.gradient(x), dy, tolerance=1e-7)

    def test_akima_interpolating_spline(self):
        n_cp = 80
        n_point = 160

        t = np.linspace(0, 3.0*np.pi, n_cp)
        tt = np.linspace(0, 3.0*np.pi, n_point)

        x = np.sin(t)

        # Now, test newer interface for order_reducing spline.

        interp = InterpND(method='akima', points=t, x_interp=tt)
        computed = interp.evaluate_spline(x)

        x_expected = np.sin(tt)
        delta = computed.flatten() - x_expected

        # Here we test that we don't have crazy interpolation error.
        self.assertLess(max(delta), .15)
        # And that it gets middle points a little better.
        self.assertLess(max(delta[15:-15]), .06)

    def test_bsplines_interpolating_spline(self):
        n_cp = 80
        n_point = 160

        t = np.linspace(0, 3.0*np.pi, n_cp)
        tt = np.linspace(0, 3.0*np.pi, n_point)

        x = np.sin(t)

        # Now, test newer interface for order_reducing spline.

        interp = InterpND(method='bsplines', points=t, x_interp=tt)
        computed = interp.evaluate_spline(x)

        x_expected = np.sin(tt)
        delta = computed.flatten() - x_expected

        # Here we test that we don't have crazy interpolation error.
        self.assertLess(max(delta), .15)
        # And that it gets middle points a little better.
        self.assertLess(max(delta[15:-15]), .06)

    def test_interpolating_spline_derivs(self):
        n_cp = 12
        n_point = 21

        t = np.linspace(0, 3.0*np.pi, n_cp)
        tt = np.linspace(0, 3.0*np.pi, n_point)

        x = np.sin(t)

        for method in self.spline_methods:
            interp = InterpND(method=method, points=t, x_interp=tt)
            computed, deriv = interp.evaluate_spline(x, compute_derivative=True)

            x_expected = np.sin(tt)
            delta = computed.flatten() - x_expected

            # Here we test that we don't have crazy interpolation error.
            self.assertLess(max(delta), .25)

    def test_interp_akima_derivs(self):

        xcp = np.array([1.0, 2.0, 4.0, 6.0, 10.0, 12.0])
        ycp = np.array([5.0, 12.0, 14.0, 16.0, 21.0, 29.0])
        n = 23
        x = np.linspace(1.0, 12.0, n)

        interp = InterpND(method='akima', points=xcp, x_interp=x, delta_x=0.1)
        y, dy_dycp = interp.evaluate_spline(ycp, compute_derivative=True)

        deriv = np.array([[ 1.00000000e+00,  0.00000000e+00,  0.00000000e+00,
                            0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
                          [ 3.12508538e-01,  7.81237193e-01, -9.37457312e-02,
                            0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
                          [ 0.00000000e+00,  1.00000000e+00,  0.00000000e+00,
                            0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
                          [-1.92097534e-05,  7.05028815e-01,  3.39990395e-01,
                           -4.50000000e-02,  0.00000000e+00,  0.00000000e+00],
                          [-1.70753364e-05,  3.80025613e-01,  7.39991462e-01,
                           -1.20000000e-01,  0.00000000e+00,  0.00000000e+00],
                          [-6.40325114e-06,  1.15009605e-01,  1.01999680e+00,
                           -1.35000000e-01,  0.00000000e+00,  0.00000000e+00],
                          [ 0.00000000e+00,  1.11022302e-16,  1.00000000e+00,
                            0.00000000e+00,  0.00000000e+00,  0.00000000e+00],
                          [ 0.00000000e+00, -5.62500000e-03,  7.60412946e-01,
                            2.45667949e-01, -5.30632175e-04,  7.47369260e-05],
                          [ 0.00000000e+00, -5.00000000e-03,  5.07767857e-01,
                            4.98447864e-01, -1.41501913e-03,  1.99298469e-04],
                          [ 0.00000000e+00, -1.87500000e-03,  2.51238839e-01,
                            7.52003846e-01, -1.59189652e-03,  2.24210778e-04],
                          [ 0.00000000e+00,  0.00000000e+00,  1.11022302e-16,
                            1.00000000e+00,  0.00000000e+00,  0.00000000e+00],
                          [ 0.00000000e+00,  0.00000000e+00, -2.10964627e-01,
                            1.19119941e+00,  2.02602810e-02, -4.95062934e-04],
                          [ 0.00000000e+00,  0.00000000e+00, -3.55003720e-01,
                            1.28195585e+00,  7.41473347e-02, -1.09946322e-03],
                          [ 0.00000000e+00,  0.00000000e+00, -4.35442243e-01,
                            1.27731946e+00,  1.59810592e-01, -1.68780891e-03],
                          [ 0.00000000e+00,  0.00000000e+00, -4.55605159e-01,
                            1.18234038e+00,  2.75399483e-01, -2.13470805e-03],
                          [ 0.00000000e+00,  0.00000000e+00, -4.18817429e-01,
                            1.00206876e+00,  4.19063438e-01, -2.31476868e-03],
                          [ 0.00000000e+00,  0.00000000e+00, -3.28404018e-01,
                            7.41554727e-01,  5.88951889e-01, -2.10259885e-03],
                          [ 0.00000000e+00,  0.00000000e+00, -1.87689887e-01,
                            4.05848427e-01,  7.83214266e-01, -1.37280661e-03],
                          [ 0.00000000e+00,  0.00000000e+00,  2.22044605e-16,
                            0.00000000e+00,  1.00000000e+00,  4.33680869e-19],
                          [ 0.00000000e+00,  0.00000000e+00,  1.18164062e-01,
                           -2.58789062e-01,  1.05371094e+00,  8.69140625e-02],
                          [ 0.00000000e+00,  0.00000000e+00,  1.05034722e-01,
                           -2.50868056e-01,  8.32465278e-01,  3.13368056e-01],
                          [ 0.00000000e+00,  0.00000000e+00,  3.93880208e-02,
                           -1.17513021e-01,  4.44986979e-01,  6.33138021e-01],
                          [ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00,
                            0.00000000e+00,  0.00000000e+00,  1.00000000e+00]])

        assert_near_equal(deriv, dy_dycp, tolerance=1e-6)

    def test_scipy_auto_reduce_spline_order(self):
        # if a spline method is used and spline_dim_error=False and a dimension
        # does not have enough points, the spline order for that dimension
        # should be automatically reduced
        np.random.seed(314)

        # x dimension is too small for cubic, should fall back to linear
        x = [0, 1]
        y = np.linspace(-10, 4, 10)
        z = np.linspace(1000, 2000, 20)

        points = [x, y, z]
        values = np.random.randn(2, 10, 20)

        interp = InterpND(method='scipy_cubic', points=points, values=values)

        # first dimension (x) should be reduced to k=1 (linear)
        self.assertEqual(interp.table._ki[0], 1)

        # should operate as normal
        x = np.array([0.5, 0, 1001])
        result = interp.interpolate(x)
        assert_near_equal(result, -0.046325695741704434, tolerance=1e-5)

        interp = InterpND(method='scipy_slinear', points=points, values=values)
        value1 = interp.interpolate(x)

        # cycle through different methods that require order reduction
        # in the first dimension
        interp = InterpND(method='scipy_quintic', points=points, values=values)
        value2, grad2 = interp.interpolate(x, compute_derivative=True)
        interp = InterpND(method='scipy_cubic', points=points, values=values)
        value3, grad3 = interp.interpolate(x, compute_derivative=True)

        # values from different methods should be different
        self.assertTrue(value1[0] != value2[0])
        self.assertTrue(value2[0] != value3[0])

    def test_NaN_exception(self):
        np.random.seed(1234)
        x = np.linspace(0, 2, 5)
        y = np.linspace(0, 1, 7)
        values = np.random.rand(5, 7)
        interp = InterpND(method='slinear', points=(x, y), values=values)

        with self.assertRaises(OutOfBoundsError) as cm:
            interp.interpolate(np.array([1, np.nan]))

        err = cm.exception

        self.assertEqual(str(err), 'One of the requested xi contains a NaN')
        self.assertEqual(err.idx, 1)
        self.assertTrue(np.isnan(err.value))
        self.assertEqual(err.lower, 0)
        self.assertEqual(err.upper, 1)

    def test_interp_1Dflat(self):

        x = np.array([0.,1.,2.,3.,4.])
        y = x**2
        f = InterpND(points=x, values=y)

        computed = f.interpolate(np.array([2.5, 3.5]))
        assert_equal_arrays(computed, np.array([6.5, 12.5]))

    def test_interp_1Dflat_list_points(self):

        x = np.array([0.,1.,2.,3.,4.])
        y = x**2
        f = InterpND(points=[x], values=y)

        computed = f.interpolate(np.array([2.5, 3.5]))
        assert_equal_arrays(computed, np.array([6.5, 12.5]))

    def test_interp_1Dflat_list_x(self):

        x = np.array([0.,1.,2.,3.,4.])
        y = x**2
        f = InterpND(points=x, values=y)

        computed = f.interpolate([np.array([2.5]), np.array([3.5])])
        assert_equal_arrays(computed, np.array([6.5, 12.5]))

    def test_error_messages(self):
        points, values = self._get_sample_4d_large()

        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method='junk', points=points, values=values.tolist())

        msg = ('Interpolation method "junk" is not defined. Valid methods are')
        self.assertTrue(str(cm.exception).startswith(msg))

        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method=points, points=points, values=values.tolist())

        msg = ("Argument 'method' should be a string.")
        self.assertTrue(str(cm.exception).startswith(msg))

        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method='slinear', points=points, values=values.tolist()[1])

        msg = ('There are 4 point arrays, but values has 3 dimensions')
        self.assertEqual(str(cm.exception), msg)

        badpoints = deepcopy(points)
        badpoints[0][0] = 55.0
        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method='slinear', points=badpoints, values=values.tolist())

        msg = ('The points in dimension 0 must be strictly ascending')
        self.assertEqual(str(cm.exception), msg)

        badpoints[0] = np.vstack((np.arange(6), np.arange(6)))
        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method='slinear', points=badpoints, values=values.tolist())

        msg = ('The points in dimension 0 must be 1-dimensional')
        self.assertEqual(str(cm.exception), msg)

        badpoints[0] = (np.arange(4))
        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method='slinear', points=badpoints, values=values.tolist())

        msg = ('There are 4 points and 6 values in dimension 0')
        self.assertEqual(str(cm.exception), msg)

        badvalues = np.array(values, dtype=complex)
        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method='slinear', points=badpoints, values=badvalues.tolist())

        msg = ("Interpolation method 'slinear' does not support complex values.")
        self.assertEqual(str(cm.exception), msg)

        interp = InterpND(method='slinear', points=points, values=values.tolist())
        x = [0.5, 0, 0.5, 0.9]

        with self.assertRaises(KeyError) as cm:
            interp = InterpND(method='slinear', points=points, values=values.tolist(), bad_arg=1)

        msg = ("\"InterpLinear: Option 'bad_arg' cannot be set because it has not been declared.")
        self.assertTrue(str(cm.exception).startswith(msg))

        # Bspline not supported for tables.
        points, values, func, df = self. _get_sample_2d()
        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method='bsplines', points=points, values=values)

        msg = "Method 'bsplines' is not supported for table interpolation."
        self.assertTrue(str(cm.exception).startswith(msg))

        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method='bsplines', points=points)

        msg = "Either 'values' or 'x_interp' must be specified."
        self.assertTrue(str(cm.exception).startswith(msg))

    def test_derivative_hysteresis_bug(self):
        alt = np.array([-1000, 0, 1000], dtype=float)
        rho = np.array([0.00244752, 0.00237717, 0.00230839])

        rho_interp = InterpND(method='slinear', points=alt, values=rho, extrapolate=True)

        x = 0.0
        _, dval1 = rho_interp.interpolate([x], compute_derivative=True)

        x = 0.5
        _, dval2 = rho_interp.interpolate([x], compute_derivative=True)

        x = 0.0
        _, dval3 = rho_interp.interpolate([x], compute_derivative=True)

        x = -0.5
        _, dval4 = rho_interp.interpolate([x], compute_derivative=True)

        x = 0.0
        _, dval5 = rho_interp.interpolate([x], compute_derivative=True)

        assert_near_equal(dval3 - dval1, np.array([[0.0]]))
        assert_near_equal(dval5 - dval1, np.array([[0.0]]))


class TestInterpNDFixedPython(unittest.TestCase):
    """Tests for efficient fixed-grid interpolation."""

    def test_error_messages(self):
        p = np.array([0, 1, 2])
        f = 2.0 * np.array([0, 1, 2])

        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method='akima1D', points=p, values=f)

        msg = "There are 3 points in a data dimension, but method 'akima1D' requires at least 4 points per dimension."
        self.assertTrue(str(cm.exception).startswith(msg))

        with self.assertRaises(ValueError) as cm:
            interp = InterpND(method='trilinear', points=p, values=f)

        msg = "There are 1 dimensions, but method 'trilinear' only works with a fixed table dimension of 3."
        self.assertTrue(str(cm.exception).startswith(msg))

    def test_trilinear(self):
        # Test trilinear vs 3d slinear.

        p1 = np.linspace(0, 100, 25)
        p2 = np.linspace(-10, 10, 15)
        p3 = np.linspace(0, 1, 12)

        # can use meshgrid to create a 3D array of test data
        P1, P2, P3 = np.meshgrid(p1, p2, p3, indexing='ij')
        f_p = np.sqrt(P1) + P2 * P3

        x1 = np.linspace(-2, 101, 5)
        x2 = np.linspace(-10.5, 11, 5)
        x3 = np.linspace(-0.2, 1.1, 5)
        X1, X2, X3 = np.meshgrid(x1, x2, x3, indexing='ij')
        x = np.zeros((125, 3))
        x[:, 0] = X1.ravel()
        x[:, 1] = X2.ravel()
        x[:, 2] = X3.ravel()

        interp = InterpND(points=(p1, p2, p3), values=f_p, method='trilinear', extrapolate=True)
        f, df_dx = interp.interpolate(x, compute_derivative=True)

        interp_base = InterpND(points=(p1, p2, p3), values=f_p, method='slinear', extrapolate=True)
        f_base, df_dx_base = interp_base.interpolate(x, compute_derivative=True)

        assert_near_equal(f, f_base, 1e-11)
        assert_near_equal(df_dx, df_dx_base, 1e-11)

        # Test non-vectorized.
        for j, x_i in enumerate(x):
            interp = InterpND(points=(p1, p2, p3), values=f_p, method='trilinear', extrapolate=True)
            f, df_dx = interp.interpolate(x_i, compute_derivative=True)

            assert_near_equal(f, f_base[j], 1e-11)
            assert_near_equal(df_dx[0], df_dx_base[j, :], 1e-11)

    def test_akima1D(self):
        # Test akima1D vs 1D akima.

        p = np.linspace(0, 100, 25)
        f_p = np.cos(p * np.pi * 0.5)
        x = np.linspace(-1, 101, 33)

        interp = InterpND(points=p, values=f_p, method='akima1D', extrapolate=True)
        f, df_dx = interp.interpolate(x, compute_derivative=True)

        interp_base = InterpND(points=p, values=f_p, method='akima', extrapolate=True)
        f_base, df_dx_base = interp_base.interpolate(x, compute_derivative=True)

        assert_near_equal(f, f_base, 1e-13)
        assert_near_equal(df_dx, df_dx_base.ravel(), 1e-13)

        # Test non-vectorized.
        for j, x_i in enumerate(x):
            interp = InterpND(points=p, values=f_p, method='akima1D', extrapolate=True)
            f, df_dx = interp.interpolate(x_i, compute_derivative=True)

            assert_near_equal(f, f_base[j], 1e-13)

            # Compare abs error since deriv is near zero in some spots.
            abs_err = np.abs(df_dx[0] -  df_dx_base[j])
            assert_near_equal(abs_err, 0.0, 1e-13)


if __name__ == '__main__':
    unittest.main()
