Repository: scipy/scipy

Issue:
scipy.interpolate.interpn: Handle ValueError('The points in dimension %d must be strictly ascending')
**Is your feature request related to a problem? Please describe.**

`scipy.interpolate.interpn` and `RegularGridInterpolator` raises a `ValueError` when the grid (arg `points`) is not strictly ascending in a dimension.

**Describe the solution you'd like**

That points in descending order raise no error but be handled internally.

**Describe alternatives you've considered**

For now, I manage it with code like:
    
    # Assume all points dimension are sorted and have no duplicate values
    slicer = tuple(slice(None, None, None if x[0] <= x[-1] else -1) for x in points) # '<=' is in case len(x) == 1
    points_asc = [x[s] for x, s in zip(points, slicer)]
    values_asc = values[slicer]
    interp = RegularGridInterpolator(points_asc, values_asc)


Discussion:

Resolving patch:
diff --git a/scipy/interpolate/_interpolate.py b/scipy/interpolate/_interpolate.py
--- a/scipy/interpolate/_interpolate.py
+++ b/scipy/interpolate/_interpolate.py
@@ -2348,6 +2348,27 @@ def integrate(self, ranges, extrapolate=None):
         return c
 
 
+def _make_points_and_values_ascending(points, values):
+    # create ascending points
+    sorted_indexes = tuple(np.argsort(point) for point in points)
+    points_asc = tuple(
+        np.asarray(point)[sort_index] for (point, sort_index) in
+        zip(points, sorted_indexes))
+
+    # create ascending values
+    ordered_indexes = tuple([*range(len(x))] for x in sorted_indexes)
+    ordered_indexes_array = np.array(
+        [i.flatten() for i in np.meshgrid(*ordered_indexes)]).transpose()
+    sorted_indexes_array = np.array(
+        [i.flatten() for i in np.meshgrid(*sorted_indexes)]).transpose()
+
+    values_asc = np.zeros_like(np.asarray(values))
+    for o, s in zip(ordered_indexes_array, sorted_indexes_array):
+        values_asc[tuple(o)] = values[tuple(s)]
+
+    return points_asc, values_asc
+
+
 class RegularGridInterpolator:
     """
     Interpolation on a regular grid in arbitrary dimensions
@@ -2360,7 +2381,9 @@ class RegularGridInterpolator:
     Parameters
     ----------
     points : tuple of ndarray of float, with shapes (m1, ), ..., (mn, )
-        The points defining the regular grid in n dimensions.
+        The points defining the regular grid in n dimensions. The points in
+        each dimension (i.e. every elements of the points tuple) must be
+        strictly ascending or descending.
 
     values : array_like, shape (m1, ..., mn, ...)
         The data on the regular grid in n dimensions. Complex data can be
@@ -2493,9 +2516,16 @@ def __init__(self, points, values, method="linear", bounds_error=True,
                                  "of a type compatible with values")
 
         for i, p in enumerate(points):
-            if not np.all(np.diff(p) > 0.):
-                raise ValueError("The points in dimension %d must be strictly "
-                                 "ascending" % i)
+            diff_p = np.diff(p)
+            if not np.all(diff_p > 0.):
+                if np.all(diff_p < 0.):
+                    # input is descending, so make it ascending
+                    points, values = _make_points_and_values_ascending(
+                        points, values)
+                else:
+                    raise ValueError(
+                        "The points in dimension %d must be strictly "
+                        "ascending or descending" % i)
             if not np.asarray(p).ndim == 1:
                 raise ValueError("The points in dimension %d must be "
                                  "1-dimensional" % i)
@@ -2668,7 +2698,9 @@ def interpn(points, values, xi, method="linear", bounds_error=True,
     Parameters
     ----------
     points : tuple of ndarray of float, with shapes (m1, ), ..., (mn, )
-        The points defining the regular grid in n dimensions.
+        The points defining the regular grid in n dimensions. The points in
+        each dimension (i.e. every elements of the points tuple) must be
+        strictly ascending or descending.
 
     values : array_like, shape (m1, ..., mn, ...)
         The data on the regular grid in n dimensions. Complex data can be
@@ -2762,9 +2794,15 @@ def interpn(points, values, xi, method="linear", bounds_error=True,
 
     # sanity check input grid
     for i, p in enumerate(points):
-        if not np.all(np.diff(p) > 0.):
-            raise ValueError("The points in dimension %d must be strictly "
-                             "ascending" % i)
+        diff_p = np.diff(p)
+        if not np.all(diff_p > 0.):
+            if np.all(diff_p < 0.):
+                # input is descending, so make it ascending
+                points, values = _make_points_and_values_ascending(points,
+                                                                   values)
+            else:
+                raise ValueError("The points in dimension %d must be strictly "
+                                 "ascending or descending" % i)
         if not np.asarray(p).ndim == 1:
             raise ValueError("The points in dimension %d must be "
                              "1-dimensional" % i)


Test patch:
diff --git a/scipy/interpolate/tests/test_interpolate.py b/scipy/interpolate/tests/test_interpolate.py
--- a/scipy/interpolate/tests/test_interpolate.py
+++ b/scipy/interpolate/tests/test_interpolate.py
@@ -2794,6 +2794,47 @@ def test_read_only(self):
             interp = interpolator(xy, z)
             interp(XY)
 
+    def test_descending_points(self):
+        def val_func_3d(x, y, z):
+            return 2 * x ** 3 + 3 * y ** 2 - z
+
+        x = np.linspace(1, 4, 11)
+        y = np.linspace(4, 7, 22)
+        z = np.linspace(7, 9, 33)
+        points = (x, y, z)
+        values = val_func_3d(
+            *np.meshgrid(*points, indexing='ij', sparse=True))
+        my_interpolating_function = RegularGridInterpolator(points,
+                                                            values)
+        pts = np.array([[2.1, 6.2, 8.3], [3.3, 5.2, 7.1]])
+        correct_result = my_interpolating_function(pts)
+
+        # descending data
+        x_descending = x[::-1]
+        y_descending = y[::-1]
+        z_descending = z[::-1]
+        points_shuffled = (x_descending, y_descending, z_descending)
+        values_shuffled = val_func_3d(
+            *np.meshgrid(*points_shuffled, indexing='ij', sparse=True))
+        my_interpolating_function = RegularGridInterpolator(
+            points_shuffled, values_shuffled)
+        test_result = my_interpolating_function(pts)
+
+        assert_array_equal(correct_result, test_result)
+
+    def test_invalid_points_order(self):
+        def val_func_2d(x, y):
+            return 2 * x ** 3 + 3 * y ** 2
+
+        x = np.array([.5, 2., 0., 4., 5.5])  # not ascending or descending
+        y = np.array([.5, 2., 3., 4., 5.5])
+        points = (x, y)
+        values = val_func_2d(*np.meshgrid(*points, indexing='ij',
+                                          sparse=True))
+        match = "must be strictly ascending or descending"
+        with pytest.raises(ValueError, match=match):
+            RegularGridInterpolator(points, values)
+
 
 class MyValue:
     """
@@ -3040,3 +3081,41 @@ def test_length_one_axis(self):
                       bounds_error=False, fill_value=None)
 
         assert_allclose(res, wanted, atol=1e-15)
+
+    def test_descending_points(self):
+        def value_func_4d(x, y, z, a):
+            return 2 * x ** 3 + 3 * y ** 2 - z - a
+
+        x1 = np.array([0, 1, 2, 3])
+        x2 = np.array([0, 10, 20, 30])
+        x3 = np.array([0, 10, 20, 30])
+        x4 = np.array([0, .1, .2, .30])
+        points = (x1, x2, x3, x4)
+        values = value_func_4d(
+            *np.meshgrid(*points, indexing='ij', sparse=True))
+        pts = (0.1, 0.3, np.transpose(np.linspace(0, 30, 4)),
+               np.linspace(0, 0.3, 4))
+        correct_result = interpn(points, values, pts)
+
+        x1_descend = x1[::-1]
+        x2_descend = x2[::-1]
+        x3_descend = x3[::-1]
+        x4_descend = x4[::-1]
+        points_shuffled = (x1_descend, x2_descend, x3_descend, x4_descend)
+        values_shuffled = value_func_4d(
+            *np.meshgrid(*points_shuffled, indexing='ij', sparse=True))
+        test_result = interpn(points_shuffled, values_shuffled, pts)
+
+        assert_array_equal(correct_result, test_result)
+
+    def test_invalid_points_order(self):
+        x = np.array([.5, 2., 0., 4., 5.5])  # not ascending or descending
+        y = np.array([.5, 2., 3., 4., 5.5])
+        z = np.array([[1, 2, 1, 2, 1], [1, 2, 1, 2, 1], [1, 2, 3, 2, 1],
+                      [1, 2, 2, 2, 1], [1, 2, 1, 2, 1]])
+        xi = np.array([[1, 2.3, 6.3, 0.5, 3.3, 1.2, 3],
+                       [1, 3.3, 1.2, -4.0, 5.0, 1.0, 3]]).T
+
+        match = "must be strictly ascending or descending"
+        with pytest.raises(ValueError, match=match):
+            interpn((x, y), z, xi)