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

Implement dpnp.rot90 and dpnp.resize #2030

Merged
merged 6 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 202 additions & 12 deletions dpnp/dpnp_iface_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@
"ravel",
"repeat",
"reshape",
"resize",
"result_type",
"roll",
"rollaxis",
"rot90",
"row_stack",
"shape",
"size",
Expand Down Expand Up @@ -1985,7 +1987,8 @@ def reshape(a, /, newshape, order="C", copy=None):
If ``False``, the result array can never be a copy
and a ValueError exception will be raised in case the copy is necessary.
If ``None``, the result array will reuse existing memory buffer of `a`
if possible and copy otherwise. Default: None.
if possible and copy otherwise.
Default: ``None``.

Returns
-------
Expand All @@ -2004,14 +2007,14 @@ def reshape(a, /, newshape, order="C", copy=None):

Examples
--------
>>> import dpnp as dp
>>> a = dp.array([[1, 2, 3], [4, 5, 6]])
>>> dp.reshape(a, 6)
>>> import dpnp as np
>>> a = np.array([[1, 2, 3], [4, 5, 6]])
>>> np.reshape(a, 6)
array([1, 2, 3, 4, 5, 6])
>>> dp.reshape(a, 6, order='F')
>>> np.reshape(a, 6, order='F')
array([1, 4, 2, 5, 3, 6])

>>> dp.reshape(a, (3, -1)) # the unspecified value is inferred to be 2
>>> np.reshape(a, (3, -1)) # the unspecified value is inferred to be 2
array([[1, 2],
[3, 4],
[5, 6]])
Expand All @@ -2031,6 +2034,91 @@ def reshape(a, /, newshape, order="C", copy=None):
return dpnp_array._create_from_usm_ndarray(usm_res)


def resize(a, new_shape):
"""
Return a new array with the specified shape.

If the new array is larger than the original array, then the new array is
filled with repeated copies of `a`. Note that this behavior is different
from ``a.resize(new_shape)`` which fills with zeros instead of repeated
copies of `a`.

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

Parameters
----------
a : {dpnp.ndarray, usm_ndarray}
Array to be resized.
new_shape : {int, tuple or list of ints}
Shape of resized array.

Returns
-------
out : dpnp.ndarray
The new array is formed from the data in the old array, repeated
if necessary to fill out the required number of elements. The
vtavana marked this conversation as resolved.
Show resolved Hide resolved
data are repeated iterating over the array in C-order.

See Also
--------
:obj:`dpnp.ndarray.reshape` : Resize an array in-place.
vtavana marked this conversation as resolved.
Show resolved Hide resolved
:obj:`dpnp.reshape` : Reshape an array without changing the total size.
:obj:`dpnp.pad` : Enlarge and pad an array.
:obj:`dpnp.repeat` : Repeat elements of an array.

Notes
-----
When the total size of the array does not change :obj:`dpnp.reshape` should
be used. In most other cases either indexing (to reduce the size) or
padding (to increase the size) may be a more appropriate solution.

Warning: This functionality does **not** consider axes separately,
i.e. it does not apply interpolation/extrapolation.
It fills the return array with the required number of elements, iterating
over `a` in C-order, disregarding axes (and cycling back from the start if
the new shape is larger). This functionality is therefore not suitable to
resize images, or data where each axis represents a separate and distinct
entity.

Examples
--------
>>> import dpnp as np
>>> a=np.array([[0, 1], [2, 3]])
vtavana marked this conversation as resolved.
Show resolved Hide resolved
>>> np.resize(a, (2 ,3))
array([[0, 1, 2],
[3, 0, 1]])
>>> np.resize(a, (1, 4))
array([[0, 1, 2, 3]])
>>> np.resize(a, (2, 4))
array([[0, 1, 2, 3],
[0, 1, 2, 3]])

"""

dpnp.check_supported_arrays_type(a)
if a.ndim == 0:
return dpnp.full(new_shape, a)
vtavana marked this conversation as resolved.
Show resolved Hide resolved

if isinstance(new_shape, (int, numpy.integer)):
new_shape = (new_shape,)

a = dpnp.ravel(a)
vtavana marked this conversation as resolved.
Show resolved Hide resolved
new_size = 1
for dim_length in new_shape:
if dim_length < 0:
raise ValueError("all elements of `new_shape` must be non-negative")
new_size *= dim_length

if a.size == 0 or new_size == 0:
# First case must zero fill. The second would have repeats == 0.
return dpnp.zeros_like(a, shape=new_shape)

repeats = -(-new_size // a.size) # ceil division
a = dpnp.concatenate((a,) * repeats)[:new_size]

return a.reshape(new_shape)


def result_type(*arrays_and_dtypes):
"""
result_type(*arrays_and_dtypes)
Expand All @@ -2052,16 +2140,16 @@ def result_type(*arrays_and_dtypes):

Examples
--------
>>> import dpnp as dp
>>> a = dp.arange(3, dtype=dp.int64)
>>> b = dp.arange(7, dtype=dp.int32)
>>> dp.result_type(a, b)
>>> import dpnp as np
>>> a = np.arange(3, dtype=np.int64)
>>> b = np.arange(7, dtype=np.int32)
>>> np.result_type(a, b)
dtype('int64')

>>> dp.result_type(dp.int64, dp.complex128)
>>> np.result_type(np.int64, np.complex128)
dtype('complex128')

>>> dp.result_type(dp.ones(10, dtype=dp.float32), dp.float64)
>>> np.result_type(np.ones(10, dtype=np.float32), np.float64)
dtype('float64')

"""
Expand Down Expand Up @@ -2200,6 +2288,108 @@ def rollaxis(x, axis, start=0):
return dpnp.moveaxis(usm_array, source=axis, destination=start)


def rot90(m, k=1, axes=(0, 1)):
"""
Rotate an array by 90 degrees in the plane specified by axes.

Rotation direction is from the first towards the second axis.
This means for a 2D array with the default `k` and `axes`, the
rotation will be counterclockwise.

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

Parameters
----------
m : {dpnp.ndarray, usm_ndarray}
Array of two or more dimensions.
k : integer, optional
Number of times the array is rotated by 90 degrees.
Default: ``1``.
axes : (2,) array_like of ints, optional
The array is rotated in the plane defined by the axes.
Axes must be different.
Default: ``(0, 1)``.

Returns
-------
out : dpnp.ndarray
A rotated view of `m`.

See Also
--------
:obj:`dpnp.flip` : Reverse the order of elements in an array along
the given axis.
:obj:`dpnp.fliplr` : Flip an array horizontally.
vtavana marked this conversation as resolved.
Show resolved Hide resolved
:obj:`dpnp.flipud` : Flip an array vertically.
vtavana marked this conversation as resolved.
Show resolved Hide resolved

Notes
-----
``rot90(m, k=1, axes=(1,0))`` is the reverse of
vtavana marked this conversation as resolved.
Show resolved Hide resolved
``rot90(m, k=1, axes=(0,1))``.

``rot90(m, k=1, axes=(1,0))`` is equivalent to
``rot90(m, k=-1, axes=(0,1))``.

Examples
--------
>>> import dpnp as np
>>> m = np.array([[1, 2], [3, 4]])
>>> m
array([[1, 2],
[3, 4]])
>>> np.rot90(m)
array([[2, 4],
[1, 3]])
>>> np.rot90(m, 2)
array([[4, 3],
[2, 1]])
>>> m = np.arange(8).reshape((2, 2, 2))
>>> np.rot90(m, 1, (1, 2))
array([[[1, 3],
[0, 2]],
[[5, 7],
[4, 6]]])

"""

dpnp.check_supported_arrays_type(m)
if not isinstance(k, (int, dpnp.integer)):
raise TypeError("k must be an integer.")
vtavana marked this conversation as resolved.
Show resolved Hide resolved

m_ndim = m.ndim
if m_ndim < 2:
raise ValueError("Input must be at least 2-d.")

if len(axes) != 2:
raise ValueError("len(axes) must be 2.")

if axes[0] == axes[1] or abs(axes[0] - axes[1]) == m_ndim:
raise ValueError("Axes must be different.")

if not (-m_ndim <= axes[0] < m_ndim and -m_ndim <= axes[1] < m_ndim):
raise ValueError(
f"Axes={axes} out of range for array of ndim={m_ndim}."
)

k %= 4
if k == 0:
return m[:]
if k == 2:
return dpnp.flip(dpnp.flip(m, axes[0]), axes[1])

axes_list = list(range(0, m_ndim))
(axes_list[axes[0]], axes_list[axes[1]]) = (
axes_list[axes[1]],
axes_list[axes[0]],
)

if k == 1:
return dpnp.transpose(flip(m, axes[1]), axes_list)
vtavana marked this conversation as resolved.
Show resolved Hide resolved

# k == 3
return dpnp.flip(dpnp.transpose(m, axes_list), axes[1])


def shape(a):
"""
Return the shape of an array.
Expand Down
118 changes: 117 additions & 1 deletion tests/test_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import numpy
import pytest
from dpctl.tensor._numpy_helper import AxisError
from numpy.testing import assert_array_equal, assert_raises
from numpy.testing import assert_array_equal, assert_equal, assert_raises

import dpnp
from tests.third_party.cupy import testing
Expand Down Expand Up @@ -665,6 +665,122 @@ def test_minimum_signed_integers(self, data, dtype):
assert_array_equal(result, expected)


class TestResize:
def test_copies(self):
vtavana marked this conversation as resolved.
Show resolved Hide resolved
a = numpy.array([[1, 2], [3, 4]])
ia = dpnp.array(a)
assert_equal(dpnp.resize(ia, (2, 4)), numpy.resize(a, (2, 4)))

a = numpy.array([[1, 2], [3, 4], [1, 2], [3, 4]])
ia = dpnp.array(a)
assert_equal(dpnp.resize(ia, (4, 2)), numpy.resize(a, (4, 2)))

a = numpy.array([[1, 2, 3], [4, 1, 2], [3, 4, 1], [2, 3, 4]])
ia = dpnp.array(a)
assert_equal(dpnp.resize(ia, (4, 3)), numpy.resize(a, (4, 3)))

@pytest.mark.parametrize("newshape", [(2, 4), [2, 4], (10,), 10])
def test_newshape_type(self, newshape):
a = numpy.array([[1, 2], [3, 4]])
ia = dpnp.array(a)
assert_equal(dpnp.resize(ia, newshape), numpy.resize(a, newshape))

def test_repeats(self):
a = numpy.array([1, 2, 3])
vtavana marked this conversation as resolved.
Show resolved Hide resolved
ia = dpnp.array(a)
assert_equal(dpnp.resize(ia, (2, 4)), numpy.resize(a, (2, 4)))

a = numpy.array([[1, 2], [3, 1], [2, 3], [1, 2]])
ia = dpnp.array(a)
assert_equal(dpnp.resize(ia, (4, 2)), numpy.resize(a, (4, 2)))

a = numpy.array([[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]])
ia = dpnp.array(a)
assert_equal(dpnp.resize(ia, (4, 3)), numpy.resize(a, (4, 3)))

def test_zeroresize(self):
a = numpy.array([[1, 2], [3, 4]])
ia = dpnp.array(a)
assert_array_equal(dpnp.resize(ia, (0,)), numpy.resize(a, (0,)))
assert_equal(a.dtype, ia.dtype)

assert_equal(dpnp.resize(ia, (0, 2)), numpy.resize(a, (0, 2)))
assert_equal(dpnp.resize(ia, (2, 0)), numpy.resize(a, (2, 0)))

def test_reshape_from_zero(self):
a = numpy.zeros(0, dtype=numpy.float32)
ia = dpnp.array(a)
assert_array_equal(dpnp.resize(ia, (2, 1)), numpy.resize(a, (2, 1)))
assert_equal(a.dtype, ia.dtype)

@pytest.mark.parametrize("xp", [numpy, dpnp])
def test_negative_resize(self, xp):
a = xp.arange(0, 10, dtype=xp.float32)
new_shape = (-10, -1)
with pytest.raises(ValueError, match=r"negative"):
xp.resize(a, new_shape=new_shape)


class TestRot90:
@pytest.mark.parametrize("xp", [numpy, dpnp])
def test_error(self, xp):
assert_raises(ValueError, xp.rot90, xp.ones(4))
assert_raises(ValueError, xp.rot90, xp.ones((2, 2, 2)), axes=(0, 1, 2))
assert_raises(ValueError, xp.rot90, xp.ones((2, 2)), axes=(0, 2))
assert_raises(ValueError, xp.rot90, xp.ones((2, 2)), axes=(1, 1))
assert_raises(ValueError, xp.rot90, xp.ones((2, 2, 2)), axes=(-2, 1))
if xp == dpnp:
# NumPy return result of k=3 incorrectly when k is float
assert_raises(TypeError, xp.rot90, xp.ones((2, 2)), k=2.5)
vtavana marked this conversation as resolved.
Show resolved Hide resolved

def test_basic(self):
a = numpy.array([[0, 1, 2], [3, 4, 5]])
ia = dpnp.array(a)

for k in range(-3, 13, 4):
assert_equal(dpnp.rot90(ia, k=k), numpy.rot90(a, k=k))
for k in range(-2, 13, 4):
assert_equal(dpnp.rot90(ia, k=k), numpy.rot90(a, k=k))
for k in range(-1, 13, 4):
assert_equal(dpnp.rot90(ia, k=k), numpy.rot90(a, k=k))
for k in range(0, 13, 4):
assert_equal(dpnp.rot90(ia, k=k), numpy.rot90(a, k=k))

assert_equal(dpnp.rot90(dpnp.rot90(ia, axes=(0, 1)), axes=(1, 0)), ia)
assert_equal(
dpnp.rot90(ia, k=1, axes=(1, 0)), dpnp.rot90(ia, k=-1, axes=(0, 1))
)

def test_axes(self):
a = numpy.ones((50, 40, 3))
ia = dpnp.array(a)
assert_equal(dpnp.rot90(ia), numpy.rot90(a))
assert_equal(dpnp.rot90(ia, axes=(0, 2)), dpnp.rot90(ia, axes=(0, -1)))
assert_equal(dpnp.rot90(ia, axes=(1, 2)), dpnp.rot90(ia, axes=(-2, -1)))

@pytest.mark.parametrize(
"axes", [(1, 2), [1, 2], numpy.array([1, 2]), dpnp.array([1, 2])]
)
def test_axes_type(self, axes):
a = numpy.ones((50, 40, 3))
ia = dpnp.array(a)
assert_equal(dpnp.rot90(ia, axes=axes), numpy.rot90(a, axes=axes))

def test_rotation_axes(self):
a = numpy.arange(8).reshape((2, 2, 2))
ia = dpnp.array(a)

assert_equal(dpnp.rot90(ia, axes=(0, 1)), numpy.rot90(a, axes=(0, 1)))
assert_equal(dpnp.rot90(ia, axes=(1, 0)), numpy.rot90(a, axes=(1, 0)))
assert_equal(dpnp.rot90(ia, axes=(1, 2)), numpy.rot90(a, axes=(1, 2)))

for k in range(1, 5):
assert_equal(
dpnp.rot90(ia, k=k, axes=(2, 0)),
numpy.rot90(a, k=k, axes=(2, 0)),
)


class TestTranspose:
@pytest.mark.parametrize("axes", [(0, 1), (1, 0), [0, 1]])
def test_2d_with_axes(self, axes):
Expand Down
Loading
Loading