Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dpnp.roll implementation and update dpnp.rollaxis/dpnp.moveaxis #1517

Merged
merged 10 commits into from
Aug 29, 2023
1 change: 1 addition & 0 deletions doc/reference/manipulation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Transpose-like operations
:nosignatures:

dpnp.moveaxis
dpnp.roll
dpnp.rollaxis
dpnp.swapaxes
dpnp.transpose
Expand Down
101 changes: 85 additions & 16 deletions dpnp/dpnp_iface_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"repeat",
"reshape",
"result_type",
"roll",
"rollaxis",
"shape",
"squeeze",
Expand Down Expand Up @@ -493,8 +494,8 @@ def moveaxis(x, source, destination):

"""

if isinstance(x, dpnp_array) or isinstance(x, dpt.usm_ndarray):
dpt_array = x.get_array() if isinstance(x, dpnp_array) else x
if dpnp.is_supported_array_type(x):
dpt_array = dpnp.get_usm_ndarray(x)
return dpnp_array._create_from_usm_ndarray(
dpt.moveaxis(dpt_array, source, destination)
)
Expand Down Expand Up @@ -694,19 +695,84 @@ def result_type(*arrays_and_dtypes):
return dpt.result_type(*usm_arrays_and_dtypes)


def rollaxis(x1, axis, start=0):
def roll(x, shift, axis=None):
"""
Roll the elements of an array by a number of positions along a given axis.

Array elements that roll beyond the last position are re-introduced
at the first position. Array elements that roll beyond the first position
are re-introduced at the last position.

For full documentation refer to :obj:`numpy.roll`.

Returns
-------
dpnp.ndarray
An array with the same data type as `x`
containing elements are shifted relative to `x`.
vlad-perevezentsev marked this conversation as resolved.
Show resolved Hide resolved

Limitations
-----------
Parameter `x` 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 the function will be executed sequentially on CPU.

See Also
--------
:obj:`dpnp.moveaxis` : Move array axes to new positions.
:obj:`dpnp.rollaxis` : Roll the specified axis backwards
until it lies in a given position.

Examples
--------
>>> import dpnp as np
>>> x1 = np.arange(10)
>>> np.roll(x1, 2)
array([8, 9, 0, 1, 2, 3, 4, 5, 6, 7])

>>> np.roll(x1, -2)
array([2, 3, 4, 5, 6, 7, 8, 9, 0, 1])

>>> x2 = np.reshape(x1, (2, 5))
>>> np.roll(x2, 1, axis=0)
array([[5, 6, 7, 8, 9],
[0, 1, 2, 3, 4]])

>>> np.roll(x2, (2, 1), axis=(1, 0))
array([[8, 9, 5, 6, 7],
[3, 4, 0, 1, 2]])

"""

if dpnp.is_supported_array_type(x):
dpt_array = dpnp.get_usm_ndarray(x)
return dpnp_array._create_from_usm_ndarray(
dpt.roll(dpt_array, shift=shift, axis=axis)
)

return call_origin(numpy.roll, x, shift=shift, axis=axis)
vlad-perevezentsev marked this conversation as resolved.
Show resolved Hide resolved


def rollaxis(x, axis, start=0):
"""
Roll the specified axis backwards, until it lies in a given position.

For full documentation refer to :obj:`numpy.rollaxis`.

Returns
-------
dpnp.ndarray
An array with the same data type as `x` where the specified axis
has been repositioned to the desired position.

Limitations
-----------
Input array is supported as :obj:`dpnp.ndarray`.
Parameter ``axis`` is supported as integer only.
Parameter ``start`` is limited by ``-a.ndim <= start <= a.ndim``.
Otherwise the function will be executed sequentially on CPU.
Parameter `x` is supported either as :class:`dpnp.ndarray`
or :class:`dpctl.tensor.usm_ndarray`.
Parameter `start` is limited by `-x.ndim <= start <= x.ndim`.
vlad-perevezentsev marked this conversation as resolved.
Show resolved Hide resolved
Input array data types are limited by supported DPNP :ref:`Data types`.
Otherwise the function will be executed sequentially on CPU.

See Also
--------
Expand All @@ -727,19 +793,22 @@ def rollaxis(x1, axis, start=0):

"""

x1_desc = dpnp.get_dpnp_descriptor(x1, copy_when_nondefault_queue=False)
if x1_desc:
if not isinstance(axis, int):
pass
elif start < -x1_desc.ndim or start > x1_desc.ndim:
if dpnp.is_supported_array_type(x):
if start < -x.ndim or start > x.ndim:
pass
else:
start_norm = start + x1_desc.ndim if start < 0 else start
destination = start_norm - 1 if start_norm > axis else start_norm
axis_norm = axis + x.ndim if axis < 0 else axis
start_norm = start + x.ndim if start < 0 else start
destination = (
start_norm - 1 if start_norm > axis_norm else start_norm
)

return dpnp.moveaxis(x1_desc.get_pyobj(), axis, destination)
dpt_array = dpnp.get_usm_ndarray(x)
return dpnp.moveaxis(
dpt_array, source=axis, destination=destination
)

return call_origin(numpy.rollaxis, x1, axis, start)
return call_origin(numpy.rollaxis, x, axis=axis, start=start)


def shape(a):
Expand Down
60 changes: 60 additions & 0 deletions tests/test_arraymanipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,3 +605,63 @@ def test_2D_array2(self):
def test_generator(self):
with assert_warns(FutureWarning):
dpnp.vstack((numpy.arange(3) for _ in range(2)))


class TestRollaxis:
data = [
(0, 0),
(0, 1),
(0, 2),
(0, 3),
(0, 4),
(1, 0),
(1, 1),
(1, 2),
(1, 3),
(1, 4),
(2, 0),
(2, 1),
(2, 2),
(2, 3),
(2, 4),
(3, 0),
(3, 1),
(3, 2),
(3, 3),
(3, 4),
]

@pytest.mark.usefixtures("allow_fall_back_on_numpy")
def test_exceptions(self):
a = dpnp.arange(1 * 2 * 3 * 4).reshape(1, 2, 3, 4)
assert_raises(numpy.AxisError, dpnp.rollaxis, a, -5, 0)
assert_raises(numpy.AxisError, dpnp.rollaxis, a, 0, -5)
assert_raises(numpy.AxisError, dpnp.rollaxis, a, 4, 0)
assert_raises(numpy.AxisError, dpnp.rollaxis, a, 0, 5)

def test_results(self):
np_a = numpy.arange(1 * 2 * 3 * 4).reshape(1, 2, 3, 4)
dp_a = dpnp.array(np_a)
for i, j in self.data:
# positive axis, positive start
res = dpnp.rollaxis(dp_a, axis=i, start=j)
exp = numpy.rollaxis(np_a, axis=i, start=j)
assert res.shape == exp.shape

# negative axis, positive start
ip = i + 1
res = dpnp.rollaxis(dp_a, axis=-ip, start=j)
exp = numpy.rollaxis(np_a, axis=-ip, start=j)
assert res.shape == exp.shape

# positive axis, negative start
jp = j + 1 if j < 4 else j
res = dpnp.rollaxis(dp_a, axis=i, start=-jp)
exp = numpy.rollaxis(np_a, axis=i, start=-jp)
assert res.shape == exp.shape

# negative axis, negative start
ip = i + 1
jp = j + 1 if j < 4 else j
res = dpnp.rollaxis(dp_a, axis=-ip, start=-jp)
exp = numpy.rollaxis(np_a, axis=-ip, start=-jp)
76 changes: 76 additions & 0 deletions tests/third_party/cupy/manipulation_tests/test_rearrange.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import unittest
vlad-perevezentsev marked this conversation as resolved.
Show resolved Hide resolved
vlad-perevezentsev marked this conversation as resolved.
Show resolved Hide resolved

import numpy
import pytest

import dpnp
vlad-perevezentsev marked this conversation as resolved.
Show resolved Hide resolved
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},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any reason why you skip

{'shape': (5, 2), 'shift': (2, 1, 3), 'axis': None},

from parameterize?

)
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
return xp.roll(x, shift, axis=self.axis)


class TestRollTypeError(unittest.TestCase):
def test_roll_invalid_shift(self):
for xp in (numpy, dpnp):
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, dpnp):
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)},
)
class TestRollValueError(unittest.TestCase):
def test_roll_invalid(self):
for xp in (numpy, dpnp):
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, dpnp):
x = testing.shaped_arange(self.shape, xp)
shift = self.shift
with pytest.raises(ValueError):
xp.roll(x, shift, axis=self.axis)
Loading