diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index a7ce7fb560e5..821f6502b3b4 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -58,6 +58,9 @@ "concatenate", "copyto", "expand_dims", + "flip", + "fliplr", + "flipud", "hstack", "moveaxis", "ravel", @@ -399,7 +402,7 @@ def expand_dims(a, axis): Returns ------- - dpnp.ndarray + out : dpnp.ndarray An array with the number of dimensions increased. A view is returned whenever possible. @@ -476,6 +479,184 @@ def expand_dims(a, axis): ) +def flip(m, axis=None): + """ + Reverse the order of elements in an array along the given axis. + + The shape of the array is preserved, but the elements are reordered. + + For full documentation refer to :obj:`numpy.flip`. + + Returns + ------- + out : dpnp.ndarray + A view of `m` with the entries of axis reversed. + + Limitations + ----------- + Parameters `m` is supported either as :class:`dpnp.ndarray` + or :class:`dpctl.tensor.usm_ndarray`. + Input array data types are limited by supported DPNP :ref:`Data types`. + Otherwise ``TypeError`` exception will be raised. + + See Also + -------- + :obj:`dpnp.flipud` : Flip an array vertically (axis=0). + :obj:`dpnp.fliplr` : Flip an array horizontally (axis=1). + + Examples + -------- + >>> import dpnp as np + >>> A = np.arange(8).reshape((2, 2, 2)) + >>> A + array([[[0, 1], + [2, 3]], + [[4, 5], + [6, 7]]]) + >>> np.flip(A, 0) + array([[[4, 5], + [6, 7]], + [[0, 1], + [2, 3]]]) + >>> np.flip(A, 1) + array([[[2, 3], + [0, 1]], + [[6, 7], + [4, 5]]]) + >>> np.flip(A) + array([[[7, 6], + [5, 4]], + [[3, 2], + [1, 0]]]) + >>> np.flip(A, (0, 2)) + array([[[5, 4], + [7, 6]], + [[1, 0], + [3, 2]]]) + >>> A = np.random.randn(3, 4, 5) + >>> np.all(np.flip(A, 2) == A[:, :, ::-1, ...]) + array(True) + + """ + + m_usm = dpnp.get_usm_ndarray(m) + return dpnp_array._create_from_usm_ndarray(dpt.flip(m_usm, axis=axis)) + + +def fliplr(m): + """ + Reverse the order of elements along axis 1 (left/right). + + For a 2-D array, this flips the entries in each row in the left/right + direction. Columns are preserved, but appear in a different order than + before. + + For full documentation refer to :obj:`numpy.fliplr`. + + Returns + ------- + out : dpnp.ndarray + A view of `m` with the columns reversed. + + Limitations + ----------- + Parameters `m` is supported either as :class:`dpnp.ndarray` + or :class:`dpctl.tensor.usm_ndarray`. + Input array data types are limited by supported DPNP :ref:`Data types`. + Otherwise ``TypeError`` exception will be raised. + + See Also + -------- + :obj:`dpnp.flipud` : Flip an array vertically (axis=0). + :obj:`dpnp.flip` : Flip array in one or more dimensions. + + Examples + -------- + >>> import dpnp as np + >>> A = np.diag(np.array([1., 2., 3.])) + >>> A + array([[1., 0., 0.], + [0., 2., 0.], + [0., 0., 3.]]) + >>> np.fliplr(A) + array([[0., 0., 1.], + [0., 2., 0.], + [3., 0., 0.]]) + + >>> A = np.random.randn(2, 3, 5) + >>> np.all(np.fliplr(A) == A[:, ::-1, ...]) + array(True) + + """ + + if not dpnp.is_supported_array_type(m): + raise TypeError( + "An array must be any of supported type, but got {}".format(type(m)) + ) + + if m.ndim < 2: + raise ValueError(f"Input must be >= 2-d, but got {m.ndim}") + return m[:, ::-1] + + +def flipud(m): + """ + Reverse the order of elements along axis 0 (up/down). + + For a 2-D array, this flips the entries in each column in the up/down + direction. Rows are preserved, but appear in a different order than before. + + For full documentation refer to :obj:`numpy.flipud`. + + Returns + ------- + out : dpnp.ndarray + A view of `m` with the rows reversed. + + Limitations + ----------- + Parameters `m` is supported either as :class:`dpnp.ndarray` + or :class:`dpctl.tensor.usm_ndarray`. + Input array data types are limited by supported DPNP :ref:`Data types`. + Otherwise ``TypeError`` exception will be raised. + + See Also + -------- + :obj:`dpnp.fliplr` : Flip array in the left/right direction. + :obj:`dpnp.flip` : Flip array in one or more dimensions. + + Examples + -------- + >>> import dpnp as np + >>> A = np.diag(np.array([1., 2., 3.])) + >>> A + array([[1., 0., 0.], + [0., 2., 0.], + [0., 0., 3.]]) + >>> np.flipud(A) + array([[0., 0., 3.], + [0., 2., 0.], + [1., 0., 0.]]) + + >>> A = np.random.randn(2, 3, 5) + >>> np.all(np.flipud(A) == A[::-1, ...]) + array(True) + + >>> np.flipud(np.array([1, 2])) + array([2, 1]) + + """ + + if not dpnp.is_supported_array_type(m): + raise TypeError( + "An array must be any of supported type, but got {}".format(type(m)) + ) + + if m.ndim < 1: + raise ValueError(f"Input must be >= 1-d, but got {m.ndim}") + return m[::-1, ...] + + def hstack(tup): """ Stack arrays in sequence horizontally (column wise). @@ -949,7 +1130,7 @@ def swapaxes(a, axis1, axis2): Returns ------- - dpnp.ndarray + out : dpnp.ndarray An array with with swapped axes. A view is returned whenever possible. diff --git a/tests/test_flipping.py b/tests/test_flipping.py new file mode 100644 index 000000000000..36365be1be71 --- /dev/null +++ b/tests/test_flipping.py @@ -0,0 +1,169 @@ +from math import prod + +import numpy +import pytest +from numpy.testing import ( + assert_equal, +) + +import dpnp + +from .helper import ( + get_all_dtypes, +) + + +class TestFlip: + @pytest.mark.parametrize("dtype", get_all_dtypes()) + def test_arange_2d_default_axis(self, dtype): + sh = (2, 3) if dtype != dpnp.bool else (1, 1) + dp_a = dpnp.arange(prod(sh), dtype=dtype).reshape(sh) + np_a = numpy.arange(prod(sh), dtype=dtype).reshape(sh) + + assert_equal(dpnp.flip(dp_a), numpy.flip(np_a)) + + @pytest.mark.parametrize("axis", list(range(3))) + @pytest.mark.parametrize( + "dtype", get_all_dtypes(no_bool=True, no_none=True) + ) + def test_arange_3d(self, axis, dtype): + sh = (2, 2, 2) + dp_a = dpnp.arange(prod(sh), dtype=dtype).reshape(sh) + np_a = numpy.arange(prod(sh), dtype=dtype).reshape(sh) + + assert_equal(dpnp.flip(dp_a, axis=axis), numpy.flip(np_a, axis=axis)) + + @pytest.mark.parametrize("axis", [(), (0, 2), (1, 2)]) + @pytest.mark.parametrize( + "dtype", get_all_dtypes(no_bool=True, no_none=True) + ) + def test_arange_3d_multiple_axes(self, axis, dtype): + sh = (2, 2, 2) + dp_a = dpnp.arange(prod(sh), dtype=dtype).reshape(sh) + np_a = numpy.arange(prod(sh), dtype=dtype).reshape(sh) + + assert_equal(dpnp.flip(dp_a, axis=axis), numpy.flip(np_a, axis=axis)) + + @pytest.mark.parametrize("axis", list(range(4))) + @pytest.mark.parametrize( + "dtype", get_all_dtypes(no_bool=True, no_none=True) + ) + def test_arange_4d(self, axis, dtype): + sh = (2, 3, 4, 5) + dp_a = dpnp.arange(prod(sh), dtype=dtype).reshape(sh) + np_a = numpy.arange(prod(sh), dtype=dtype).reshape(sh) + + assert_equal(dpnp.flip(dp_a, axis=axis), numpy.flip(np_a, axis=axis)) + + @pytest.mark.parametrize( + "dtype", get_all_dtypes(no_bool=True, no_none=True) + ) + def test_lr_equivalent(self, dtype): + dp_a = dpnp.arange(4, dtype=dtype) + dp_a = dp_a[:, dpnp.newaxis] + dp_a[dpnp.newaxis, :] + assert_equal(dpnp.flip(dp_a, 1), dpnp.fliplr(dp_a)) + + np_a = numpy.arange(4, dtype=dtype) + np_a = numpy.add.outer(np_a, np_a) + assert_equal(dpnp.flip(dp_a, 1), numpy.flip(np_a, 1)) + + @pytest.mark.parametrize( + "dtype", get_all_dtypes(no_bool=True, no_none=True) + ) + def test_ud_equivalent(self, dtype): + dp_a = dpnp.arange(4, dtype=dtype) + dp_a = dp_a[:, dpnp.newaxis] + dp_a[dpnp.newaxis, :] + assert_equal(dpnp.flip(dp_a, 0), dpnp.flipud(dp_a)) + + np_a = numpy.arange(4, dtype=dtype) + np_a = numpy.add.outer(np_a, np_a) + assert_equal(dpnp.flip(dp_a, 0), numpy.flip(np_a, 0)) + + @pytest.mark.parametrize( + "x, axis", + [ + pytest.param(dpnp.ones(4), 1, id="1-d, axis=1"), + pytest.param(dpnp.ones((4, 4)), 2, id="2-d, axis=2"), + pytest.param(dpnp.ones((4, 4)), -3, id="2-d, axis=-3"), + pytest.param(dpnp.ones((4, 4)), (0, 3), id="2-d, axis=(0, 3)"), + ], + ) + def test_axes(self, x, axis): + with pytest.raises(numpy.AxisError): + dpnp.flip(x, axis=axis) + + +class TestFliplr: + @pytest.mark.parametrize("dtype", get_all_dtypes()) + def test_arange(self, dtype): + sh = (2, 3) if dtype != dpnp.bool else (1, 1) + dp_a = dpnp.arange(prod(sh), dtype=dtype).reshape(sh) + np_a = numpy.arange(prod(sh), dtype=dtype).reshape(sh) + + assert_equal(dpnp.fliplr(dp_a), numpy.fliplr(np_a)) + + @pytest.mark.parametrize( + "dtype", get_all_dtypes(no_bool=True, no_none=True) + ) + def test_equivalent(self, dtype): + dp_a = dpnp.arange(4, dtype=dtype) + dp_a = dp_a[:, dpnp.newaxis] + dp_a[dpnp.newaxis, :] + assert_equal(dpnp.fliplr(dp_a), dp_a[:, ::-1]) + + np_a = numpy.arange(4, dtype=dtype) + np_a = numpy.add.outer(np_a, np_a) + assert_equal(dpnp.fliplr(dp_a), numpy.fliplr(np_a)) + + @pytest.mark.parametrize( + "val", + [-1.2, numpy.arange(7), [2, 7, 3.6], (-3, 4), range(4)], + ids=["scalar", "numpy.array", "list", "tuple", "range"], + ) + def test_raises_array_type(self, val): + with pytest.raises( + TypeError, match="An array must be any of supported type, but got" + ): + dpnp.fliplr(val) + + def test_raises_1d(self): + a = dpnp.ones(4) + with pytest.raises(ValueError, match="Input must be >= 2-d, but got"): + dpnp.fliplr(a) + + +class TestFlipud: + @pytest.mark.parametrize("dtype", get_all_dtypes()) + def test_arange(self, dtype): + sh = (2, 3) if dtype != dpnp.bool else (1, 1) + dp_a = dpnp.arange(prod(sh), dtype=dtype).reshape(sh) + np_a = numpy.arange(prod(sh), dtype=dtype).reshape(sh) + + assert_equal(dpnp.flipud(dp_a), numpy.flipud(np_a)) + + @pytest.mark.parametrize( + "dtype", get_all_dtypes(no_bool=True, no_none=True) + ) + def test_equivalent(self, dtype): + dp_a = dpnp.arange(4, dtype=dtype) + dp_a = dp_a[:, dpnp.newaxis] + dp_a[dpnp.newaxis, :] + assert_equal(dpnp.flipud(dp_a), dp_a[::-1, :]) + + np_a = numpy.arange(4, dtype=dtype) + np_a = numpy.add.outer(np_a, np_a) + assert_equal(dpnp.flipud(dp_a), numpy.flipud(np_a)) + + @pytest.mark.parametrize( + "val", + [3.4, numpy.arange(6), [2, -1.7, 6], (-2, 4), range(5)], + ids=["scalar", "numpy.array", "list", "tuple", "range"], + ) + def test_raises_array_type(self, val): + with pytest.raises( + TypeError, match="An array must be any of supported type, but got" + ): + dpnp.flipud(val) + + def test_raises_0d(self): + a = dpnp.array(3) + with pytest.raises(ValueError, match="Input must be >= 1-d, but got"): + dpnp.flipud(a) diff --git a/tests/third_party/cupy/manipulation_tests/test_rearrange.py b/tests/third_party/cupy/manipulation_tests/test_rearrange.py new file mode 100644 index 000000000000..59ef8b97cfb9 --- /dev/null +++ b/tests/third_party/cupy/manipulation_tests/test_rearrange.py @@ -0,0 +1,270 @@ +import unittest + +import numpy +import pytest + +import dpnp as cupy +from tests.third_party.cupy import testing + + +@testing.parameterize( + {"shape": (10,), "shift": 2, "axis": None}, + {"shape": (5, 2), "shift": 1, "axis": None}, + {"shape": (5, 2), "shift": -2, "axis": None}, + {"shape": (5, 2), "shift": 1, "axis": 0}, + {"shape": (5, 2), "shift": 1, "axis": -1}, + {"shape": (10,), "shift": 35, "axis": None}, + {"shape": (5, 2), "shift": 11, "axis": 0}, + {"shape": (), "shift": 5, "axis": None}, + {"shape": (5, 2), "shift": 1, "axis": (0, 1)}, + {"shape": (5, 2), "shift": 1, "axis": (0, 0)}, + {"shape": (5, 2), "shift": 50, "axis": 0}, + {"shape": (5, 2), "shift": (2, 1), "axis": (0, 1)}, + {"shape": (5, 2), "shift": (2, 1), "axis": (0, -1)}, + {"shape": (5, 2), "shift": (2, 1), "axis": (1, -1)}, + {"shape": (5, 2), "shift": (2, 1, 3), "axis": 0}, + {"shape": (5, 2), "shift": (2, 1, 3), "axis": None}, +) +@pytest.mark.skip("`roll` isn't supported yet") +class TestRoll(unittest.TestCase): + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_roll(self, xp, dtype): + x = testing.shaped_arange(self.shape, xp, dtype) + return xp.roll(x, self.shift, axis=self.axis) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_roll_cupy_shift(self, xp, dtype): + x = testing.shaped_arange(self.shape, xp, dtype) + shift = self.shift + if xp is cupy: + shift = cupy.array(shift) + return xp.roll(x, shift, axis=self.axis) + + +@pytest.mark.skip("`roll` isn't supported yet") +class TestRollTypeError(unittest.TestCase): + def test_roll_invalid_shift(self): + for xp in (numpy, cupy): + x = testing.shaped_arange((5, 2), xp) + with pytest.raises(TypeError): + xp.roll(x, "0", axis=0) + + def test_roll_invalid_axis_type(self): + for xp in (numpy, cupy): + x = testing.shaped_arange((5, 2), xp) + with pytest.raises(TypeError): + xp.roll(x, 2, axis="0") + + +@testing.parameterize( + {"shape": (5, 2, 3), "shift": (2, 2, 2), "axis": (0, 1)}, + {"shape": (5, 2), "shift": 1, "axis": 2}, + {"shape": (5, 2), "shift": 1, "axis": -3}, + {"shape": (5, 2, 2), "shift": (1, 0), "axis": (0, 1, 2)}, + {"shape": (5, 2), "shift": 1, "axis": -3}, + {"shape": (5, 2), "shift": 1, "axis": (1, -3)}, +) +@pytest.mark.skip("`roll` isn't supported yet") +class TestRollValueError(unittest.TestCase): + def test_roll_invalid(self): + for xp in (numpy, cupy): + x = testing.shaped_arange(self.shape, xp) + with pytest.raises(ValueError): + xp.roll(x, self.shift, axis=self.axis) + + def test_roll_invalid_cupy_shift(self): + for xp in (numpy, cupy): + x = testing.shaped_arange(self.shape, xp) + shift = self.shift + if xp is cupy: + shift = cupy.array(shift) + with pytest.raises(ValueError): + xp.roll(x, shift, axis=self.axis) + + +class TestFliplr(unittest.TestCase): + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_fliplr_2(self, xp, dtype): + x = testing.shaped_arange((3, 4), xp, dtype) + return xp.fliplr(x) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_fliplr_3(self, xp, dtype): + x = testing.shaped_arange((3, 4, 2), xp, dtype) + return xp.fliplr(x) + + @testing.for_all_dtypes() + def test_fliplr_insufficient_ndim(self, dtype): + for xp in (numpy, cupy): + x = testing.shaped_arange((3,), xp, dtype) + with pytest.raises(ValueError): + xp.fliplr(x) + + +class TestFlipud(unittest.TestCase): + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flipud_1(self, xp, dtype): + x = testing.shaped_arange((3,), xp, dtype) + return xp.flipud(x) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flipud_2(self, xp, dtype): + x = testing.shaped_arange((3, 4), xp, dtype) + return xp.flipud(x) + + @testing.for_all_dtypes() + def test_flipud_insufficient_ndim(self, dtype): + for xp in (numpy, cupy): + x = testing.shaped_arange((), xp, dtype) + with pytest.raises(ValueError): + xp.flipud(x) + + +class TestFlip(unittest.TestCase): + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flip_1(self, xp, dtype): + x = testing.shaped_arange((3,), xp, dtype) + return xp.flip(x, 0) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flip_2(self, xp, dtype): + x = testing.shaped_arange((3, 4), xp, dtype) + return xp.flip(x, 1) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flip_all(self, xp, dtype): + x = testing.shaped_arange((3, 4), xp, dtype) + return xp.flip(x) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flip_with_negative_axis(self, xp, dtype): + x = testing.shaped_arange((3, 4, 2), xp, dtype) + return xp.flip(x, -1) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flip_with_axis_tuple(self, xp, dtype): + x = testing.shaped_arange((3, 4, 2), xp, dtype) + return xp.flip(x, (0, 2)) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flip_dim_0(self, xp, dtype): + x = testing.shaped_arange((), xp, dtype) + return xp.flip(x) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flip_empty_dim_1(self, xp, dtype): + x = xp.array([], dtype).reshape((0,)) + return xp.flip(x, 0) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flip_empty_dim_2(self, xp, dtype): + x = xp.array([], dtype).reshape((0, 0)) + return xp.flip(x, 1) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flip_empty_dim_3(self, xp, dtype): + x = xp.array([], dtype).reshape((1, 0, 1)) + return xp.flip(x, 1) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_flip_empty_dim_all(self, xp, dtype): + x = xp.array([], dtype).reshape((1, 0, 1)) + return xp.flip(x) + + @testing.for_all_dtypes() + def test_flip_insufficient_ndim(self, dtype): + for xp in (numpy, cupy): + x = testing.shaped_arange((), xp, dtype) + with pytest.raises(ValueError): + xp.flip(x, 0) + + @testing.for_all_dtypes() + def test_flip_invalid_axis(self, dtype): + for xp in (numpy, cupy): + x = testing.shaped_arange((3, 4), xp, dtype) + with pytest.raises(ValueError): + xp.flip(x, 2) + + @testing.for_all_dtypes() + def test_flip_invalid_negative_axis(self, dtype): + for xp in (numpy, cupy): + x = testing.shaped_arange((3, 4), xp, dtype) + with pytest.raises(ValueError): + xp.flip(x, -3) + + +@pytest.mark.skip("`rot90` isn't supported yet") +class TestRot90(unittest.TestCase): + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_rot90_none(self, xp, dtype): + x = testing.shaped_arange((3, 4), xp, dtype) + return xp.rot90(x, 0) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_rot90_twice(self, xp, dtype): + x = testing.shaped_arange((3, 4, 2), xp, dtype) + return xp.rot90(x, 2) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_rot90_negative(self, xp, dtype): + x = testing.shaped_arange((3, 4, 2), xp, dtype) + return xp.rot90(x, -1) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_rot90_with_axes(self, xp, dtype): + x = testing.shaped_arange((3, 4, 2), xp, dtype) + return xp.rot90(x, 1, axes=(1, 2)) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_rot90_with_negative_axes(self, xp, dtype): + x = testing.shaped_arange((3, 4, 2), xp, dtype) + return xp.rot90(x, 1, axes=(1, -1)) + + @testing.for_all_dtypes() + def test_rot90_insufficient_ndim(self, dtype): + for xp in (numpy, cupy): + x = testing.shaped_arange((3,), xp, dtype) + with pytest.raises(ValueError): + xp.rot90(x) + + @testing.for_all_dtypes() + def test_rot90_too_much_axes(self, dtype): + for xp in (numpy, cupy): + x = testing.shaped_arange((3, 4, 2), xp, dtype) + with pytest.raises(ValueError): + xp.rot90(x, 1, axes=(0, 1, 2)) + + @testing.for_all_dtypes() + def test_rot90_invalid_axes(self, dtype): + for xp in (numpy, cupy): + x = testing.shaped_arange((3, 4, 2), xp, dtype) + with pytest.raises(ValueError): + xp.rot90(x, 1, axes=(1, 3)) + + @testing.for_all_dtypes() + def test_rot90_invalid_negative_axes(self, dtype): + for xp in (numpy, cupy): + x = testing.shaped_arange((3, 4, 2), xp, dtype) + with pytest.raises(ValueError): + xp.rot90(x, 1, axes=(1, -2))