Repository: cupy/cupy

Issue:
ndimage.map_coordinates performs incorrectly with several order values
The results given by map_coordinates are unexpected when testing the [example given in the function description](https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html#scipy.ndimage.map_coordinates).

In particular, the `order=None` and `order=1` are giving incorrect results. In the example below I compare the cpu and gpu versions of the function and show this. (cupy left, scipy right)

```python
import cupy as cp
from cupyx.scipy.ndimage import map_coordinates as cupy_map_coordinates
from scipy.ndimage import map_coordinates as scipy_map_coordinates

arr = cp.arange(12.).reshape((4, 3))
points = cp.array([[0.5, 2], [0.5, 1]])

print(f"order={None} ->", 
      cupy_map_coordinates(arr, points), 
      scipy_map_coordinates(arr.get(), points.get())) # scipy defaults to order=3, but cupy gives only zeros
for order in [0, 1]:
    print(f"order={order} ->", 
          cupy_map_coordinates(arr, points, order=order), 
          scipy_map_coordinates(arr.get(), points.get(), order=order))

# order=None -> [0. 0.] [1.3625 7.    ]
# order=0 -> [0. 7.] [4. 7.]
# order=1 -> [2. 7.] [2. 7.]
```

```
OS                           : Windows-10-10.0.18362-SP0
CuPy Version                 : 8.3.0
NumPy Version                : 1.19.4
SciPy Version                : 1.5.3
Cython Build Version         : 0.29.21
CUDA Root                    : C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.1
CUDA Build Version           : 11010
CUDA Driver Version          : 11020
CUDA Runtime Version         : 11010
cuBLAS Version               : 11201
cuFFT Version                : 10300
cuRAND Version               : 10202
cuSOLVER Version             : (11, 0, 0)
cuSPARSE Version             : 11200
NVRTC Version                : (11, 1)
Thrust Version               : 100910
CUB Build Version            : 100910
cuDNN Build Version          : None
cuDNN Version                : None
NCCL Build Version           : None
NCCL Runtime Version         : None
cuTENSOR Version             : None
Device 0 Name                : GeForce GTX 1080 Ti
Device 0 Compute Capability  : 61
```


Discussion:
The `None` case seems to be fixed in v9. 

```
order=None -> [1.3625 7.    ] [1.3625 7.    ]
order=0 -> [0. 7.] [4. 7.]
order=1 -> [2. 7.] [2. 7.]
```

Maybe @grlee77 can help with this :)
Yes, order=3 is implemented for CuPy 9.0 and in general various SciPy 1.6 bugfixes/enhancements have been incorporated in that branch that are not in CuPy 8.x.

There does indeed seem to be a difference in rounding when the coordinate is exactly 0.5, though. I will take a look at why. If I use
```Python
points = cp.array([[0.49999, 2], [0.49999, 1]])
```
or
```Python
points = cp.array([[0.50001, 2], [0.50001, 1]])
```
then I get agreement between CuPy and SciPy for all cases on CuPy 9.0.0b1, so I think any behavior difference lies in the direction of rounding at half integer coordinates.
Nice catch! @grlee77, just a quick thank you to you and the others who work on this - for me, now that `map_coordinates` and `histogram2d`* are implemented in CuPy, there is a whole lot image correction that is possible to consider doing for us physicists :) (that would have taken an awful lot longer before).

\* I installed cupy v9 today and realised it was there :)

Resolving patch:
diff --git a/cupyx/scipy/ndimage/_interp_kernels.py b/cupyx/scipy/ndimage/_interp_kernels.py
--- a/cupyx/scipy/ndimage/_interp_kernels.py
+++ b/cupyx/scipy/ndimage/_interp_kernels.py
@@ -302,7 +302,7 @@ def _generate_interp_custom(coord_func, ndim, large_int, yshape, mode, cval,
                 dcoord = c_{j};''')
             else:
                 ops.append(f'''
-                {int_t} cf_{j} = ({int_t})lrint((double)c_{j});''')
+                {int_t} cf_{j} = ({int_t})floor((double)c_{j} + 0.5);''')
 
             # handle boundary
             if mode != 'constant':


Test patch:
diff --git a/tests/cupyx_tests/scipy_tests/ndimage_tests/test_interpolation.py b/tests/cupyx_tests/scipy_tests/ndimage_tests/test_interpolation.py
--- a/tests/cupyx_tests/scipy_tests/ndimage_tests/test_interpolation.py
+++ b/tests/cupyx_tests/scipy_tests/ndimage_tests/test_interpolation.py
@@ -117,6 +117,29 @@ def test_map_coordinates_int(self, xp, scp, dtype):
         return out
 
 
+@testing.parameterize(*testing.product({
+    'order': [0, 1, 2, 3, 4, 5],
+    'mode': ['constant', 'nearest', 'mirror'] + scipy16_modes,
+}))
+@testing.gpu
+@testing.with_requires('scipy')
+class TestMapCoordinatesHalfInteger:
+
+    def _map_coordinates(self, xp, scp, a, coordinates):
+        _conditional_scipy_version_skip(self.mode, self.order)
+        map_coordinates = scp.ndimage.map_coordinates
+        return map_coordinates(a, coordinates, None, self.order, self.mode)
+
+    @testing.for_float_dtypes(no_float16=True)
+    @testing.numpy_cupy_allclose(atol=1e-4, scipy_name='scp')
+    def test_map_coordinates_float(self, xp, scp, dtype):
+        # Half integer coordinate rounding test case from:
+        # https://github.com/cupy/cupy/issues/4550
+        a = testing.shaped_arange((4, 3), xp, dtype)
+        coordinates = xp.array([[0.5, 2], [0.5, 1]])
+        return self._map_coordinates(xp, scp, a, coordinates)
+
+
 @testing.parameterize(*testing.product({
     'matrix_shape': [(2,), (2, 2), (2, 3), (3, 3)],
     'offset': [0.3, [-1.3, 1.3]],