diff --git a/CHANGELOG.md b/CHANGELOG.md index d9fc960c2e7..ed4779cdb96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,7 +83,7 @@ This release is compatible with NumPy 2.5. * Fixed comparison functions (`dpnp.equal`, `dpnp.not_equal`, `dpnp.less`, `dpnp.less_equal`, `dpnp.greater`, `dpnp.greater_equal`) and `dpnp.divide` raising `OverflowError` when comparing an integer array against a Python integer scalar outside the array dtype's range [#3017](https://github.com/IntelPython/dpnp/pull/3017) * Fixed a crash in boolean-mask advanced indexing (`dpnp.ndarray` get/set item) when the selection is empty (e.g. a scalar `False` index that injects a length-0 axis) [#3019](https://github.com/IntelPython/dpnp/pull/3019) * Released the GIL before the remaining blocking OneMKL BLAS and LAPACK calls to prevent host tasks contention, completing the work started in [#2850](https://github.com/IntelPython/dpnp/pull/2850) [#3027](https://github.com/IntelPython/dpnp/pull/3027) - +* Fixed `dpnp.repeat` raising an unclear `TypeError` for a nested sequence of `repeats` [#3024](https://github.com/IntelPython/dpnp/pull/3024) ### Security diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 96d03532cac..f697449dd5b 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -2800,14 +2800,15 @@ def repeat(a, repeats, axis=None): Parameters ---------- - x : {dpnp.ndarray, usm_ndarray} + a : {dpnp.ndarray, usm_ndarray} Input array. repeats : {int, tuple, list, range, dpnp.ndarray, usm_ndarray} The number of repetitions for each element. `repeats` is broadcasted to fit the shape of the given axis. If `repeats` is an array, it must have an integer data type. Otherwise, `repeats` must be a Python integer or sequence of Python - integers (i.e., a tuple, list, or range). + integers (i.e., a tuple, list, or range). A sequence must be 0- or + 1-dimensional. axis : {None, int}, optional The axis along which to repeat values. By default, use the flattened input array, and return a flat output array. diff --git a/dpnp/tensor/_manipulation_functions.py b/dpnp/tensor/_manipulation_functions.py index 0fdc7209e26..080e79aa881 100644 --- a/dpnp/tensor/_manipulation_functions.py +++ b/dpnp/tensor/_manipulation_functions.py @@ -576,7 +576,8 @@ def repeat(x, repeats, /, *, axis=None): If `repeats` is an array, it must have an integer data type. Otherwise, `repeats` must be a Python integer or sequence of - Python integers (i.e., a tuple, list, or range). + Python integers (i.e., a tuple, list, or range). A sequence must + be 0- or 1-dimensional. axis (Optional[int]): The axis along which to repeat values. If `axis` is `None`, the @@ -663,14 +664,20 @@ def repeat(x, repeats, /, *, axis=None): usm_type = x.usm_type exec_q = x.sycl_queue - len_reps = len(repeats) - if len_reps == 1: - repeats = repeats[0] + # inspect the sequence on the host to preserve the scalar fast path + repeats = np.asarray(repeats) + if repeats.ndim > 1: + raise ValueError( + "`repeats` sequence must be 0- or 1-dimensional, got " + f"{repeats.ndim} dimensions" + ) + if repeats.size == 1: + scalar = True + repeats = int(repeats[0]) if repeats < 0: raise ValueError("`repeats` elements must be positive") - scalar = True else: - if len_reps != axis_size: + if repeats.size != axis_size: raise ValueError( "`repeats` sequence must have the same length as the " "repeated axis" diff --git a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py index a97e473d574..bf0bd322630 100644 --- a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py +++ b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py @@ -1464,6 +1464,14 @@ def test_repeat_arg_validation(): with pytest.raises(ValueError): dpt.repeat(x, dpt.ones((1, 1), dtype="i8")) + # repeats nested sequence must be 0d or 1d + with pytest.raises(ValueError, match="0- or 1-dimensional"): + dpt.repeat(x, [[4]]) + with pytest.raises(ValueError, match="0- or 1-dimensional"): + dpt.repeat(x, [[1, 2, 3, 4, 5]]) + with pytest.raises(ValueError, match="0- or 1-dimensional"): + dpt.repeat(x, [[1], [2], [3], [4], [5]]) + # repeats must be castable to i8 with pytest.raises(TypeError): dpt.repeat(x, dpt.asarray(2.0, dtype="f4")) diff --git a/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py b/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py index a0ea1f35865..032b0503089 100644 --- a/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py +++ b/dpnp/tests/third_party/cupy/manipulation_tests/test_tiling.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import unittest import numpy @@ -29,11 +31,7 @@ def test_array_repeat(self, xp): {"repeats": [2], "axis": None}, {"repeats": [2], "axis": 1}, ) -class TestRepeatListBroadcast(unittest.TestCase): - """Test for `repeats` argument using single element list. - - This feature is only supported in NumPy 1.10 or later. - """ +class TestRepeatListBroadcast: @testing.numpy_cupy_array_equal() def test_array_repeat(self, xp): @@ -48,7 +46,7 @@ def test_array_repeat(self, xp): {"repeats": [1, 2, 3, 4], "axis": None}, {"repeats": [1, 2, 3, 4], "axis": 0}, ) -class TestRepeat1D(unittest.TestCase): +class TestRepeat1D: @testing.numpy_cupy_array_equal() def test_array_repeat(self, xp): @@ -60,8 +58,7 @@ def test_array_repeat(self, xp): {"repeats": [2], "axis": None}, {"repeats": [2], "axis": 0}, ) -class TestRepeat1DListBroadcast(unittest.TestCase): - """See comment in TestRepeatListBroadcast class.""" +class TestRepeat1DListBroadcast: @testing.numpy_cupy_array_equal() def test_array_repeat(self, xp): @@ -77,7 +74,7 @@ def test_array_repeat(self, xp): {"repeats": 2, "axis": -4}, {"repeats": 2, "axis": 3}, ) -class TestRepeatFailure(unittest.TestCase): +class TestRepeatFailure: def test_repeat_failure(self): for xp in (numpy, cupy): @@ -191,38 +188,6 @@ def test_reversed(self, xp): return xp.repeat(x, xp.array([0, 1, 2, 1, 0])) -class TestRepeatNdarrayDtypeEdges: - - @testing.numpy_cupy_array_equal() - def test_bool_perelement(self, xp): - return xp.repeat(xp.arange(3), xp.array([True, False, True])) - - @testing.numpy_cupy_array_equal() - def test_bool_broadcast(self, xp): - return xp.repeat( - testing.shaped_arange((3, 4), xp), xp.array([True]), axis=0 - ) - - @testing.numpy_cupy_array_equal() - def test_uint32_accepted(self, xp): - return xp.repeat( - xp.arange(4), xp.array([1, 2, 3, 4], dtype=numpy.uint32) - ) - - -class TestRepeatNdarrayLarge: - - @testing.numpy_cupy_array_equal() - def test_large_single(self, xp): - return xp.repeat( - testing.shaped_arange((3,), xp), xp.array([0, 100000, 0]) - ) - - @testing.numpy_cupy_array_equal() - def test_large_broadcast(self, xp): - return xp.repeat(testing.shaped_arange((3,), xp), xp.array([50000])) - - class TestRepeatScalarEquivalence: """All scalar-like repeats inputs produce identical results.""" @@ -293,9 +258,8 @@ def test_ndim_gt1_matches_numpy(self): with pytest.raises(ValueError): xp.repeat(xp.arange(6), xp.array([[1, 2, 3, 4, 5, 6]])) - @pytest.mark.skip("different message for nested lists") def test_ndim_gt1_list_rejected(self): - with pytest.raises(ValueError, match=r"too deep"): + with pytest.raises(ValueError, match=r"0- or 1-dimensional"): cupy.repeat(cupy.arange(6), [[1, 2, 3, 4, 5, 6]]) def test_bad_axis(self): @@ -310,6 +274,38 @@ def test_method_interface(self): testing.assert_array_equal(a.repeat(reps), cupy.repeat(a, reps)) +class TestRepeatNdarrayDtypeEdges: + + @testing.numpy_cupy_array_equal() + def test_bool_perelement(self, xp): + return xp.repeat(xp.arange(3), xp.array([True, False, True])) + + @testing.numpy_cupy_array_equal() + def test_bool_broadcast(self, xp): + return xp.repeat( + testing.shaped_arange((3, 4), xp), xp.array([True]), axis=0 + ) + + @testing.numpy_cupy_array_equal() + def test_uint32_accepted(self, xp): + return xp.repeat( + xp.arange(4), xp.array([1, 2, 3, 4], dtype=numpy.uint32) + ) + + +class TestRepeatNdarrayLarge: + + @testing.numpy_cupy_array_equal() + def test_large_single(self, xp): + return xp.repeat( + testing.shaped_arange((3,), xp), xp.array([0, 100000, 0]) + ) + + @testing.numpy_cupy_array_equal() + def test_large_broadcast(self, xp): + return xp.repeat(testing.shaped_arange((3,), xp), xp.array([50000])) + + @testing.parameterize( {"reps": 0}, {"reps": 1},