From 63e641f6d5d920e0935515c9f68580e122cded10 Mon Sep 17 00:00:00 2001 From: vtavana <120411540+vtavana@users.noreply.github.com> Date: Thu, 10 Oct 2024 08:15:52 -0500 Subject: [PATCH] update `dpnp.reshape` to use `shape` keyword argument instead of `newshape` (#2080) * update using shape argument instead of newshape * Update dpnp/dpnp_iface_manipulation.py Co-authored-by: Anton <100830759+antonwolfy@users.noreply.github.com> * address comments * update dpnp.ravel * minor updates * remove pytest skip --------- Co-authored-by: Anton <100830759+antonwolfy@users.noreply.github.com> --- dpnp/dpnp_array.py | 8 +- dpnp/dpnp_iface_manipulation.py | 147 +++++++++++++--- tests/test_manipulation.py | 91 ++++++++++ .../cupy/manipulation_tests/test_shape.py | 159 +++++++++++------- 4 files changed, 314 insertions(+), 91 deletions(-) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 20825c6c396..615f709956a 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -1286,7 +1286,7 @@ def repeat(self, repeats, axis=None): return dpnp.repeat(self, repeats, axis=axis) - def reshape(self, *sh, **kwargs): + def reshape(self, *shape, order="C", copy=None): """ Returns an array containing the same data with a new shape. @@ -1311,9 +1311,9 @@ def reshape(self, *sh, **kwargs): """ - if len(sh) == 1: - sh = sh[0] - return dpnp.reshape(self, sh, **kwargs) + if len(shape) == 1: + shape = shape[0] + return dpnp.reshape(self, shape, order=order, copy=copy) # 'resize', diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 1e847366d14..6082fffebe6 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -40,6 +40,7 @@ import math import operator +import warnings import dpctl.tensor as dpt import numpy @@ -1848,23 +1849,50 @@ def ravel(a, order="C"): x : {dpnp.ndarray, usm_ndarray} Input array. The elements in `a` are read in the order specified by order, and packed as a 1-D array. - order : {"C", "F"}, optional + order : {None, "C", "F", "A"}, optional The elements of `a` are read using this index order. ``"C"`` means to index the elements in row-major, C-style order, with the last axis index changing fastest, back to the first axis index changing slowest. ``"F"`` means to index the elements in column-major, Fortran-style order, with the first index changing fastest, and the last index - changing slowest. By default, ``"C"`` index order is used. + changing slowest. Note that the "C" and "F" options take no account of + the memory layout of the underlying array, and only refer to + the order of axis indexing. "A" means to read the elements in + Fortran-like index order if `a` is Fortran *contiguous* in + memory, C-like order otherwise. ``order=None`` is an alias for + ``order="C"``. + Default: ``"C"``. Returns ------- out : dpnp.ndarray A contiguous 1-D array of the same subtype as `a`, with shape (a.size,). + Limitations + ----------- + `order="K"` is not supported and the function raises `NotImplementedError` + exception. + See Also -------- - :obj:`dpnp.reshape` : Change the shape of an array without changing its - data. + :obj:`dpnp.ndarray.flat` : 1-D iterator over an array. + :obj:`dpnp.ndarray.flatten` : 1-D array copy of the elements of an array + in row-major order. + :obj:`dpnp.ndarray.reshape` : Change the shape of an array without + changing its data. + :obj:`dpnp.reshape` : The same as :obj:`dpnp.ndarray.reshape`. + + Notes + ----- + In row-major, C-style order, in two dimensions, the row index + varies the slowest, and the column index the quickest. This can + be generalized to multiple dimensions, where row-major order + implies that the index along the first axis varies slowest, and + the index along the last quickest. The opposite holds for + column-major, Fortran-style index ordering. + + When a view is desired in as many cases as possible, ``arr.reshape(-1)`` + may be preferable. Examples -------- @@ -1879,9 +1907,27 @@ def ravel(a, order="C"): >>> np.ravel(x, order='F') array([1, 4, 2, 5, 3, 6]) + When `order` is ``"A"``, it will preserve the array's + ``"C"`` or ``"F"`` ordering: + + >>> np.ravel(x.T) + array([1, 4, 2, 5, 3, 6]) + >>> np.ravel(x.T, order='A') + array([1, 2, 3, 4, 5, 6]) + """ - return dpnp.reshape(a, -1, order=order) + if order in "kK": + raise NotImplementedError( + "Keyword argument `order` is supported only with " + f"values None, 'C', 'F', and 'A', but got '{order}'" + ) + + result = dpnp.reshape(a, -1, order=order) + if result.flags.c_contiguous: + return result + + return dpnp.ascontiguousarray(result) def repeat(a, repeats, axis=None): @@ -2055,7 +2101,7 @@ def require(a, dtype=None, requirements=None, *, like=None): return arr -def reshape(a, /, newshape, order="C", copy=None): +def reshape(a, /, shape=None, order="C", *, newshape=None, copy=None): """ Gives a new shape to an array without changing its data. @@ -2065,12 +2111,12 @@ def reshape(a, /, newshape, order="C", copy=None): ---------- a : {dpnp.ndarray, usm_ndarray} Array to be reshaped. - newshape : int or tuple of ints + shape : int or tuple of ints The new shape should be compatible with the original shape. If an integer, then the result will be a 1-D array of that length. One shape dimension can be -1. In this case, the value is inferred from the length of the array and remaining dimensions. - order : {"C", "F"}, optional + order : {None, "C", "F", "A"}, optional Read the elements of `a` using this index order, and place the elements into the reshaped array using this index order. ``"C"`` means to read / write the elements using C-like index order, @@ -2080,30 +2126,63 @@ def reshape(a, /, newshape, order="C", copy=None): changing fastest, and the last index changing slowest. Note that the ``"C"`` and ``"F"`` options take no account of the memory layout of the underlying array, and only refer to the order of indexing. + ``order=None`` is an alias for ``order="C"``. ``"A"`` means to + read / write the elements in Fortran-like index order if ``a`` is + Fortran *contiguous* in memory, C-like order otherwise. + Default: ``"C"``. + newshape : int or tuple of ints + Replaced by `shape` argument. Retained for backward compatibility. copy : {None, bool}, optional - Boolean indicating whether or not to copy the input array. - If ``True``, the result array will always be a copy of input `a`. - 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. + If ``True``, then the array data is copied. If ``None``, a copy will + only be made if it's required by ``order``. For ``False`` it raises + a ``ValueError`` if a copy cannot be avoided. Default: ``None``. Returns ------- out : dpnp.ndarray This will be a new view object if possible; otherwise, it will - be a copy. Note there is no guarantee of the *memory layout* (C- or + be a copy. Note there is no guarantee of the *memory layout* (C- or Fortran- contiguous) of the returned array. - Limitations - ----------- - Parameter `order` is supported only with values ``"C"`` and ``"F"``. - See Also -------- :obj:`dpnp.ndarray.reshape` : Equivalent method. + Notes + ----- + It is not always possible to change the shape of an array without copying + the data. + + The `order` keyword gives the index ordering both for *fetching* + the values from ``a``, and then *placing* the values into the output + array. For example, let's say you have an array: + + >>> import dpnp as np + >>> a = np.arange(6).reshape((3, 2)) + >>> a + array([[0, 1], + [2, 3], + [4, 5]]) + + You can think of reshaping as first raveling the array (using the given + index order), then inserting the elements from the raveled array into the + new array using the same kind of index ordering as was used for the + raveling. + + >>> np.reshape(a, (2, 3)) # C-like index ordering + array([[0, 1, 2], + [3, 4, 5]]) + >>> np.reshape(np.ravel(a), (2, 3)) # equivalent to C ravel then C reshape + array([[0, 1, 2], + [3, 4, 5]]) + >>> np.reshape(a, (2, 3), order='F') # Fortran-like index ordering + array([[0, 4, 3], + [2, 1, 5]]) + >>> np.reshape(np.ravel(a, order='F'), (2, 3), order='F') + array([[0, 4, 3], + [2, 1, 5]]) + Examples -------- >>> import dpnp as np @@ -2120,16 +2199,38 @@ def reshape(a, /, newshape, order="C", copy=None): """ - if newshape is None: - newshape = a.shape + if newshape is None and shape is None: + raise TypeError( + "reshape() missing 1 required positional argument: 'shape'" + ) + + if newshape is not None: + if shape is not None: + raise TypeError( + "You cannot specify 'newshape' and 'shape' arguments " + "at the same time." + ) + # Deprecated in dpnp 0.17.0 + warnings.warn( + "`newshape` keyword argument is deprecated, " + "use `shape=...` or pass shape positionally instead. " + "(deprecated in dpnp 0.17.0)", + DeprecationWarning, + stacklevel=2, + ) + shape = newshape if order is None: order = "C" + elif order in "aA": + order = "F" if a.flags.fnc else "C" elif order not in "cfCF": - raise ValueError(f"order must be one of 'C' or 'F' (got {order})") + raise ValueError( + f"order must be None, 'C', 'F', or 'A' (got '{order}')" + ) usm_a = dpnp.get_usm_ndarray(a) - usm_res = dpt.reshape(usm_a, shape=newshape, order=order, copy=copy) + usm_res = dpt.reshape(usm_a, shape=shape, order=order, copy=copy) return dpnp_array._create_from_usm_ndarray(usm_res) diff --git a/tests/test_manipulation.py b/tests/test_manipulation.py index ba016637990..c4d2b2b86a9 100644 --- a/tests/test_manipulation.py +++ b/tests/test_manipulation.py @@ -448,6 +448,22 @@ def test_no_copy(self): assert_array_equal(b, a) +class TestRavel: + def test_error(self): + ia = dpnp.arange(10).reshape(2, 5) + assert_raises(NotImplementedError, dpnp.ravel, ia, order="K") + + @pytest.mark.parametrize("order", ["C", "F", "A"]) + def test_non_contiguous(self, order): + a = numpy.arange(10)[::2] + ia = dpnp.arange(10)[::2] + expected = numpy.ravel(a, order=order) + result = dpnp.ravel(ia, order=order) + assert result.flags.c_contiguous == expected.flags.c_contiguous + assert result.flags.f_contiguous == expected.flags.f_contiguous + assert_array_equal(result, expected) + + class TestRepeat: @pytest.mark.parametrize( "data", @@ -790,6 +806,81 @@ def test_negative_resize(self, xp): xp.resize(a, new_shape=new_shape) +class TestReshape: + def test_error(self): + ia = dpnp.arange(10) + assert_raises(TypeError, dpnp.reshape, ia) + assert_raises( + TypeError, dpnp.reshape, ia, shape=(2, 5), newshape=(2, 5) + ) + + @pytest.mark.filterwarnings("ignore::DeprecationWarning") + def test_newshape(self): + a = numpy.arange(10) + ia = dpnp.array(a) + expected = numpy.reshape(a, (2, 5)) + result = dpnp.reshape(ia, newshape=(2, 5)) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("order", [None, "C", "F", "A"]) + def test_order(self, order): + a = numpy.arange(10) + ia = dpnp.array(a) + expected = numpy.reshape(a, (2, 5), order) + result = dpnp.reshape(ia, (2, 5), order) + assert result.flags.c_contiguous == expected.flags.c_contiguous + assert result.flags.f_contiguous == expected.flags.f_contiguous + assert_array_equal(result, expected) + + # ndarray + result = ia.reshape(2, 5, order=order) + assert result.flags.c_contiguous == expected.flags.c_contiguous + assert result.flags.f_contiguous == expected.flags.f_contiguous + assert_array_equal(result, expected) + + def test_ndarray(self): + a = numpy.arange(10) + ia = dpnp.array(a) + expected = a.reshape(2, 5) + result = ia.reshape(2, 5) + assert_array_equal(result, expected) + + # packed + result = ia.reshape((2, 5)) + assert_array_equal(result, expected) + + @testing.with_requires("numpy>=2.0") + def test_copy(self): + a = numpy.arange(10).reshape(2, 5) + ia = dpnp.array(a) + expected = numpy.reshape(a, 10, copy=None) + expected[0] = -1 + result = dpnp.reshape(ia, 10, copy=None) + result[0] = -1 + assert a[0, 0] == expected[0] # a is also modified, no copy + assert ia[0, 0] == result[0] # ia is also modified, no copy + assert_array_equal(result, expected) + + a = numpy.arange(10).reshape(2, 5) + ia = dpnp.array(a) + expected = numpy.reshape(a, 10, copy=True) + expected[0] = -1 + result = dpnp.reshape(ia, 10, copy=True) + result[0] = -1 + assert a[0, 0] != expected[0] # a is not modified, copy is done + assert ia[0, 0] != result[0] # ia is not modified, copy is done + assert_array_equal(result, expected) + + a = numpy.arange(10).reshape(2, 5) + ia = dpnp.array(a) + assert_raises( + ValueError, dpnp.reshape, ia, (5, 2), order="F", copy=False + ) + assert_raises( + ValueError, dpnp.reshape, ia, (5, 2), order="F", copy=False + ) + + class TestRot90: @pytest.mark.parametrize("xp", [numpy, dpnp]) def test_error(self, xp): diff --git a/tests/third_party/cupy/manipulation_tests/test_shape.py b/tests/third_party/cupy/manipulation_tests/test_shape.py index 1a99df5c954..3169d502e2e 100644 --- a/tests/third_party/cupy/manipulation_tests/test_shape.py +++ b/tests/third_party/cupy/manipulation_tests/test_shape.py @@ -1,37 +1,25 @@ -import unittest - import numpy import pytest import dpnp as cupy +from tests.helper import has_support_aspect64 from tests.third_party.cupy import testing -@testing.parameterize( - *testing.product( - { - "shape": [(2, 3), (), (4,)], - } - ) -) -class TestShape(unittest.TestCase): - def test_shape(self): - shape = self.shape +@pytest.mark.parametrize("shape", [(2, 3), (), (4,)]) +class TestShape: + def test_shape(self, shape): for xp in (numpy, cupy): a = testing.shaped_arange(shape, xp) assert cupy.shape(a) == shape - def test_shape_list(self): - shape = self.shape + def test_shape_list(self, shape): a = testing.shaped_arange(shape, numpy) a = a.tolist() assert cupy.shape(a) == shape -class TestReshape(unittest.TestCase): - # order = 'A' is out of support currently - _supported_orders = "CF" - +class TestReshape: def test_reshape_shapes(self): def func(xp): a = testing.shaped_arange((1, 1, 1, 2, 2), xp) @@ -46,7 +34,7 @@ def func(xp): assert func(numpy) == func(cupy) - @testing.for_orders(_supported_orders) + @testing.for_orders("CFA") @testing.for_all_dtypes() @testing.numpy_cupy_array_equal() def test_nocopy_reshape(self, xp, dtype, order): @@ -55,7 +43,7 @@ def test_nocopy_reshape(self, xp, dtype, order): b[1] = 1 return a - @testing.for_orders(_supported_orders) + @testing.for_orders("CFA") @testing.for_all_dtypes() @testing.numpy_cupy_array_equal() def test_nocopy_reshape_with_order(self, xp, dtype, order): @@ -64,13 +52,13 @@ def test_nocopy_reshape_with_order(self, xp, dtype, order): b[1] = 1 return a - @testing.for_orders(_supported_orders) + @testing.for_orders("CFA") @testing.numpy_cupy_array_equal() def test_transposed_reshape2(self, xp, order): a = testing.shaped_arange((2, 3, 4), xp).transpose(2, 0, 1) return a.reshape(2, 3, 4, order=order) - @testing.for_orders(_supported_orders) + @testing.for_orders("CFA") @testing.numpy_cupy_array_equal() def test_reshape_with_unknown_dimension(self, xp, order): a = testing.shaped_arange((2, 3, 4), xp) @@ -106,24 +94,22 @@ def test_reshape_zerosize_invalid_unknown(self): with pytest.raises(ValueError): a.reshape((-1, 0)) - @pytest.mark.skip("array.base is not implemented") - @testing.numpy_cupy_array_equal() + @testing.numpy_cupy_array_equal(type_check=has_support_aspect64()) def test_reshape_zerosize(self, xp): a = xp.zeros((0,)) b = a.reshape((0,)) - assert b.base is a + # assert b.base is a return b - @pytest.mark.skip("array.base is not implemented") - @testing.for_orders(_supported_orders) - @testing.numpy_cupy_array_equal(strides_check=True) + @testing.for_orders("CFA") + @testing.numpy_cupy_array_equal(type_check=has_support_aspect64()) def test_reshape_zerosize2(self, xp, order): a = xp.zeros((2, 0, 3)) b = a.reshape((5, 0, 4), order=order) - assert b.base is a + # assert b.base is a return b - @testing.for_orders(_supported_orders) + @testing.for_orders("CFA") @testing.numpy_cupy_array_equal() def test_external_reshape(self, xp, order): a = xp.zeros((8,), dtype=xp.float32) @@ -137,48 +123,98 @@ def _test_ndim_limit(self, xp, ndim, dtype, order): assert a.ndim == ndim return a - @testing.for_orders(_supported_orders) + @testing.with_requires("numpy>=2.0") + @testing.for_orders("CFA") @testing.for_all_dtypes() @testing.numpy_cupy_array_equal() def test_ndim_limit1(self, xp, dtype, order): - # from cupy/cupy#4193 - a = self._test_ndim_limit(xp, 32, dtype, order) + a = self._test_ndim_limit(xp, 64, dtype, order) return a @pytest.mark.skip("no max ndim limit for reshape in dpctl") - @testing.for_orders(_supported_orders) + @testing.for_orders("CFA") @testing.for_all_dtypes() def test_ndim_limit2(self, dtype, order): - # from cupy/cupy#4193 for xp in (numpy, cupy): with pytest.raises(ValueError): - self._test_ndim_limit(xp, 33, dtype, order) + self._test_ndim_limit(xp, 65, dtype, order) -class TestRavel(unittest.TestCase): - @testing.for_orders("CF") - # order = 'A' is out of support currently +class TestRavel: + @testing.for_orders("CFA") + # order = 'K' is not supported currently @testing.numpy_cupy_array_equal() def test_ravel(self, xp, order): a = testing.shaped_arange((2, 3, 4), xp) a = a.transpose(2, 0, 1) return a.ravel(order) - @testing.for_orders("CF") - # order = 'A' is out of support currently + @testing.for_orders("CFA") + # order = 'K' is not supported currently @testing.numpy_cupy_array_equal() def test_ravel2(self, xp, order): a = testing.shaped_arange((2, 3, 4), xp) return a.ravel(order) - @testing.for_orders("CF") - # order = 'A' is out of support currently + @testing.for_orders("CFA") + # order = 'K' is not supported currently @testing.numpy_cupy_array_equal() def test_ravel3(self, xp, order): a = testing.shaped_arange((2, 3, 4), xp) a = xp.asfortranarray(a) return a.ravel(order) + @testing.for_orders("CFA") + # order = 'K' is not supported currently + @testing.numpy_cupy_array_equal() + def test_ravel4(self, xp, order): + a = testing.shaped_arange((2, 3, 4), xp) + a = a.transpose(0, 2, 1)[:, ::-2] + return a.ravel(order) + + @testing.for_orders("CFA") + # order = 'K' is not supported currently + @testing.numpy_cupy_array_equal() + def test_ravel_non_contiguous(self, xp, order): + a = xp.arange(10)[::2] + assert not a.flags.c_contiguous and not a.flags.f_contiguous + b = a.ravel(order) + assert b.flags.c_contiguous + return b + + @testing.for_orders("CFA") + # order = 'K' is not supported currently + @testing.numpy_cupy_array_equal() + def test_ravel_broadcasted(self, xp, order): + a = xp.array([1]) + b = xp.broadcast_to(a, (10,)) + assert not b.flags.c_contiguous and not b.flags.f_contiguous + b = b.ravel(order) + assert b.flags.c_contiguous + return b + + @testing.for_orders("CFA") + # order = 'K' is not supported currently + @testing.numpy_cupy_array_equal() + def test_ravel_broadcasted2(self, xp, order): + a = testing.shaped_arange((2, 1), xp) + b = xp.broadcast_to(a, (3, 2, 4)) + assert not b.flags.c_contiguous and not b.flags.f_contiguous + b = b.ravel(order) + assert b.flags.c_contiguous + return b + + @testing.for_orders("CFA") + # order = 'K' is not supported currently + @testing.for_orders("CF", name="a_order") + @testing.numpy_cupy_array_equal() + def test_ravel_broadcasted3(self, xp, order, a_order): + a = testing.shaped_arange((2, 1, 3), xp, order=a_order) + b = xp.broadcast_to(a, (2, 4, 3)) + b = b.ravel(order) + assert b.flags.c_contiguous + return b + @testing.numpy_cupy_array_equal() def test_external_ravel(self, xp): a = testing.shaped_arange((2, 3, 4), xp) @@ -186,32 +222,27 @@ def test_external_ravel(self, xp): return xp.ravel(a) -@testing.parameterize( - *testing.product( - { - "order_init": ["C", "F"], - # order = 'A' is out of support currently - # 'order_reshape': ['C', 'F', 'A', 'c', 'f', 'a'], - "order_reshape": ["C", "F", "c", "f"], - "shape_in_out": [ - ((2, 3), (1, 6, 1)), # (shape_init, shape_final) - ((6,), (2, 3)), - ((3, 3, 3), (9, 3)), - ], - } - ) +@pytest.mark.parametrize("order_init", ["C", "F"]) +@pytest.mark.parametrize("order_reshape", ["C", "F", "A", "c", "f", "a"]) +@pytest.mark.parametrize( + "shape_in_out", + [ + ((2, 3), (1, 6, 1)), # (shape_init, shape_final) + ((6,), (2, 3)), + ((3, 3, 3), (9, 3)), + ], ) -class TestReshapeOrder(unittest.TestCase): - def test_reshape_contiguity(self): - shape_init, shape_final = self.shape_in_out +class TestReshapeOrder: + def test_reshape_contiguity(self, order_init, order_reshape, shape_in_out): + shape_init, shape_final = shape_in_out a_cupy = testing.shaped_arange(shape_init, xp=cupy) - a_cupy = cupy.asarray(a_cupy, order=self.order_init) - b_cupy = a_cupy.reshape(shape_final, order=self.order_reshape) + a_cupy = cupy.asarray(a_cupy, order=order_init) + b_cupy = a_cupy.reshape(shape_final, order=order_reshape) a_numpy = testing.shaped_arange(shape_init, xp=numpy) - a_numpy = numpy.asarray(a_numpy, order=self.order_init) - b_numpy = a_numpy.reshape(shape_final, order=self.order_reshape) + a_numpy = numpy.asarray(a_numpy, order=order_init) + b_numpy = a_numpy.reshape(shape_final, order=order_reshape) assert b_cupy.flags.f_contiguous == b_numpy.flags.f_contiguous assert b_cupy.flags.c_contiguous == b_numpy.flags.c_contiguous