Repository: Project-MONAI/MONAI

Issue:
overriding argument bug in spatial_resample
**Describe the bug**
the `align_corners` default value is `False` in `SpatialResample.__call__`
https://github.com/Project-MONAI/MONAI/blob/009af550add6f293dea874665e89d68ae677bc23/monai/transforms/spatial/array.py#L198
this by default makes `self.align_corners` not in use, because:
https://github.com/Project-MONAI/MONAI/blob/009af550add6f293dea874665e89d68ae677bc23/monai/transforms/spatial/array.py#L239
it easily causes confusions and hides some bugs.


**expected:**
`align_corners=None` in `SpatialResample.__call__`

example to show the errors:
```py
import torch

from monai.transforms import SpatialResample

img = torch.arange(12).reshape((1, 2, 2, 3))
xform = SpatialResample(mode=0, padding_mode="reflect", align_corners=True)
dst_affine = torch.tensor([[1.0, 0.0, 0.0, 0.0], [0.0, -1.0, 0.0, 1.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]])
print(xform(img, dst_affine=dst_affine))
```
overriding argument bug in spatial_resample
**Describe the bug**
the `align_corners` default value is `False` in `SpatialResample.__call__`
https://github.com/Project-MONAI/MONAI/blob/009af550add6f293dea874665e89d68ae677bc23/monai/transforms/spatial/array.py#L198
this by default makes `self.align_corners` not in use, because:
https://github.com/Project-MONAI/MONAI/blob/009af550add6f293dea874665e89d68ae677bc23/monai/transforms/spatial/array.py#L239
it easily causes confusions and hides some bugs.


**expected:**
`align_corners=None` in `SpatialResample.__call__`

example to show the errors:
```py
import torch

from monai.transforms import SpatialResample

img = torch.arange(12).reshape((1, 2, 2, 3))
xform = SpatialResample(mode=0, padding_mode="reflect", align_corners=True)
dst_affine = torch.tensor([[1.0, 0.0, 0.0, 0.0], [0.0, -1.0, 0.0, 1.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]])
print(xform(img, dst_affine=dst_affine))
```


Discussion:

Resolving patch:
diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py
--- a/monai/transforms/spatial/array.py
+++ b/monai/transforms/spatial/array.py
@@ -195,7 +195,7 @@ def __call__(
         spatial_size: Optional[Union[Sequence[int], torch.Tensor, int]] = None,
         mode: Union[str, int, None] = None,
         padding_mode: Optional[str] = None,
-        align_corners: Optional[bool] = False,
+        align_corners: Optional[bool] = None,
         dtype: DtypeLike = None,
     ) -> torch.Tensor:
         """
@@ -224,6 +224,7 @@ def __call__(
                 See also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html
             align_corners: Geometrically, we consider the pixels of the input as squares rather than points.
                 See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html
+                Defaults to ``None``, effectively using the value of `self.align_corners`.
             dtype: data type for resampling computation. Defaults to ``self.dtype`` or
                 ``np.float64`` (for best precision). If ``None``, use the data type of input data.
                 To be compatible with other modules, the output data type is always `float32`.
@@ -295,7 +296,7 @@ def __call__(
             for idx, d_dst in enumerate(spatial_size[:spatial_rank]):
                 _t_r[idx, -1] = (max(d_dst, 2) - 1.0) / 2.0
             xform = xform @ _t_r
-            if not USE_COMPILED:
+            if not USE_COMPILED and not isinstance(mode, int):
                 _t_l = normalize_transform(
                     in_spatial_size, xform.device, xform.dtype, align_corners=True  # type: ignore
                 )[0]
@@ -365,7 +366,7 @@ def __call__(
         dst_meta: Optional[Dict] = None,
         mode: Union[str, int, None] = None,
         padding_mode: Optional[str] = None,
-        align_corners: Optional[bool] = False,
+        align_corners: Optional[bool] = None,
         dtype: DtypeLike = None,
     ) -> torch.Tensor:
         """
@@ -393,7 +394,7 @@ def __call__(
                 {'reflect', 'grid-mirror', 'constant', 'grid-constant', 'nearest', 'mirror', 'grid-wrap', 'wrap'}.
                 See also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html
             align_corners: Geometrically, we consider the pixels of the input as squares rather than points.
-                Defaults to ``False``.
+                Defaults to ``None``, effectively using the value of `self.align_corners`.
                 See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html
             dtype: data type for resampling computation. Defaults to ``self.dtype`` or
                 ``np.float64`` (for best precision). If ``None``, use the data type of input data.
@@ -513,6 +514,7 @@ def __call__(
                 See also: https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html
             align_corners: Geometrically, we consider the pixels of the input as squares rather than points.
                 See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.grid_sample.html
+                Defaults to ``None``, effectively using the value of `self.align_corners`.
             dtype: data type for resampling computation. Defaults to ``self.dtype``.
                 If None, use the data type of input data. To be compatible with other modules,
                 the output data type is always ``np.float32``.


Test patch:
diff --git a/tests/test_spatial_resample.py b/tests/test_spatial_resample.py
--- a/tests/test_spatial_resample.py
+++ b/tests/test_spatial_resample.py
@@ -10,6 +10,7 @@
 # limitations under the License.
 
 import unittest
+from copy import deepcopy
 
 import numpy as np
 import torch
@@ -19,6 +20,7 @@
 from monai.data.meta_tensor import MetaTensor
 from monai.data.utils import to_affine_nd
 from monai.transforms import SpatialResample
+from monai.utils import optional_import
 from tests.utils import TEST_DEVICES, TEST_NDARRAYS_ALL, assert_allclose
 
 TESTS = []
@@ -53,6 +55,9 @@
                             expct,
                         ]
                     )
+if optional_import("cupy")[1] and optional_import("scipy.ndimage")[1]:
+    TESTS.append(deepcopy(TESTS[-1]))
+    TESTS[-1][2].update({"align_corners": True, "mode": 1, "padding_mode": "reflect"})  # type: ignore
 
 
 destinations_2d = [
@@ -147,7 +152,7 @@ def test_4d_5d(self, new_shape, tile, device, dtype, expected_data):
 
         dst = torch.tensor([[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, -1.0, 1.5], [0.0, 0.0, 0.0, 1.0]])
         dst = dst.to(dtype)
-        out = SpatialResample(dtype=dtype)(img=img, dst_affine=dst)
+        out = SpatialResample(dtype=dtype, align_corners=True)(img=img, dst_affine=dst, align_corners=False)
         assert_allclose(out, expected_data[None], rtol=1e-2, atol=1e-2)
         assert_allclose(out.affine, dst.to(torch.float32), rtol=1e-2, atol=1e-2)
 
@@ -167,6 +172,12 @@ def test_ill_affine(self, device):
             img.affine = torch.eye(4)
             dst_affine = torch.eye(4) * 0.1
             SpatialResample(mode=None)(img=img, dst_affine=dst_affine)
+        if not (optional_import("scipy")[1] and optional_import("cupy")[1]):
+            return
+        with self.assertRaises(ValueError):  # requires scipy
+            SpatialResample(mode=1, align_corners=True)(img=img, dst_affine=dst_affine)
+        with self.assertRaises(ValueError):
+            SpatialResample(mode=1, align_corners=False)(img=img, dst_affine=dst_affine)
 
     @parameterized.expand(TEST_TORCH_INPUT)
     def test_input_torch(self, new_shape, tile, device, dtype, expected_data, track_meta):