From d172933a881567c7dbc65ecc41d53ce6a075e380 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 29 Jul 2026 15:06:08 +0200 Subject: [PATCH 1/7] Add broadcast_shapes to dpnp.tensor --- dpnp/tensor/__init__.py | 2 ++ dpnp/tensor/_manipulation_functions.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/dpnp/tensor/__init__.py b/dpnp/tensor/__init__.py index 0118e04f7ab1..c2ad5bdaff95 100644 --- a/dpnp/tensor/__init__.py +++ b/dpnp/tensor/__init__.py @@ -174,6 +174,7 @@ ) from ._manipulation_functions import ( broadcast_arrays, + broadcast_shapes, broadcast_to, concat, expand_dims, @@ -282,6 +283,7 @@ "bitwise_right_shift", "bitwise_xor", "broadcast_arrays", + "broadcast_shapes", "broadcast_to", "can_cast", "cbrt", diff --git a/dpnp/tensor/_manipulation_functions.py b/dpnp/tensor/_manipulation_functions.py index d12422fdefa4..0a21acddb85d 100644 --- a/dpnp/tensor/_manipulation_functions.py +++ b/dpnp/tensor/_manipulation_functions.py @@ -248,6 +248,29 @@ def broadcast_arrays(*args): return [broadcast_to(X, shape) for X in args] +def broadcast_shapes(*shapes): + """broadcast_shapes(*shapes) + + Broadcasts one or more shapes against one another. + + Args: + shapes (Tuple[int, ...]): an arbitrary number of shapes to be + broadcasted against one another. Each shape must be a tuple of + integers. + + Returns: + Tuple[int, ...]: + The broadcasted shape resulting from broadcasting the input + shapes against one another. + + Raises: + ValueError: if the input shapes are not broadcast-compatible. + """ + if len(shapes) == 0: + return () + return _broadcast_shape_impl([tuple(sh) for sh in shapes]) + + def broadcast_to(X, /, shape): """broadcast_to(x, shape) From 74b075a2824ec2f5bf3f0eb65e4a13ad92f3550e Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 29 Jul 2026 15:08:29 +0200 Subject: [PATCH 2/7] Update dpnp.broadcast_shapes to rely on tensor function --- dpnp/dpnp_iface_manipulation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index b96e5593c2a6..d33cef268464 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1126,7 +1126,12 @@ def broadcast_shapes(*args): """ - return numpy.broadcast_shapes(*args) + # a bare integer is treated as a one-dimensional shape + shapes = [ + sh if isinstance(sh, (tuple, list)) else (operator.index(sh),) + for sh in args + ] + return dpt.broadcast_shapes(*shapes) # pylint: disable=redefined-outer-name From d616d45266bdc6fe8b4cf5a9b845db7c14679caa Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 29 Jul 2026 15:11:02 +0200 Subject: [PATCH 3/7] Add tensor tests to cover dpnp.tensor.broadcast_shapes --- .../tensor/test_usm_ndarray_manipulation.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py index bb0a99a537ff..1c0bd5faadc0 100644 --- a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py +++ b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py @@ -469,6 +469,36 @@ def test_broadcast_arrays_no_args(): dpt.broadcast_arrays() +@pytest.mark.parametrize( + "shapes", + [ + [(1,), (3,)], + [(1, 3), (3, 3)], + [(3, 1), (3, 3)], + [(1, 3), (3, 1)], + [(6, 7), (5, 6, 1), (7,), (5, 1, 7)], + [(1, 2), (3, 1), (3, 2)], + [(1, 0), (0, 1)], + [()], + [(5,)], + ], +) +def test_broadcast_shapes(shapes): + expected = np.broadcast_shapes(*shapes) + result = dpt.broadcast_shapes(*shapes) + assert result == expected + + +def test_broadcast_shapes_no_args(): + # matches numpy.broadcast_shapes() returning an empty shape + assert dpt.broadcast_shapes() == () + + +def test_broadcast_shapes_raises(): + with pytest.raises(ValueError): + dpt.broadcast_shapes((2, 3), (4, 5)) + + def test_flip_axis_incorrect(): q = get_queue_or_skip() From a339688d4408359cc15b63fab5742207ee2c3f5d Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 29 Jul 2026 15:24:03 +0200 Subject: [PATCH 4/7] Add handling of negative dim in passed shapes and tests coverage for dpnp.broadcast_shapes --- dpnp/dpnp_iface_manipulation.py | 14 +++++++++----- dpnp/tests/test_manipulation.py | 21 ++++++++++++++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index d33cef268464..6ba4770d0bbb 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1126,11 +1126,15 @@ def broadcast_shapes(*args): """ - # a bare integer is treated as a one-dimensional shape - shapes = [ - sh if isinstance(sh, (tuple, list)) else (operator.index(sh),) - for sh in args - ] + shapes = [] + for sh in args: + if not isinstance(sh, (tuple, list)): + # a bare integer is treated as a one-dimensional shape + sh = (operator.index(sh),) + + if any(dim < 0 for dim in sh): + raise ValueError("negative dimensions are not allowed") + shapes.append(sh) return dpt.broadcast_shapes(*shapes) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 9eedcfe275d1..07f18cc02a2f 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -308,7 +308,7 @@ def test_no_copy(self): assert_array_equal(b, a) -class TestBroadcast: +class TestBroadcastShapes: @pytest.mark.parametrize( "shape", [ @@ -332,6 +332,25 @@ def test_broadcast_shapes(self, shape): result = dpnp.broadcast_shapes(*shape) assert_equal(result, expected) + @pytest.mark.parametrize( + "shape", + [ + [1, 2], + [(3, 1), 3], + [1, (5, 1), 5], + ], + ) + def test_scalar(self, shape): + expected = numpy.broadcast_shapes(*shape) + result = dpnp.broadcast_shapes(*shape) + assert_equal(result, expected) + + @pytest.mark.parametrize("xp", [dpnp, numpy]) + @pytest.mark.parametrize("shape", [[(-1,)], [(2, -3), (2, 3)], [-1, 2]]) + def test_negative_dim(self, xp, shape): + with pytest.raises(ValueError, match="aaa"): + xp.broadcast_shapes(*shape) + class TestCopyTo: testdata = [] From db5eed7fb85c314cc273daf2ee334bd3b3eff916 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 29 Jul 2026 15:26:26 +0200 Subject: [PATCH 5/7] Populate changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5f3a243ae2..ae959ed6946d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This release is compatible with NumPy 2.5. * Added `--includes` and `--include-dir` options to the `dpnp` CLI [#2916](https://github.com/IntelPython/dpnp/pull/2916) * Added `dpnp-config.cmake` to make `find_package(Dpnp)` work out of the box, and an example which uses it [#2941](https://github.com/IntelPython/dpnp/pull/2941) * Added implementation of `dpnp.lib.stride_tricks.as_strided` [#2991](https://github.com/IntelPython/dpnp/pull/2991) +* Added `dpnp.tensor.broadcast_shapes` to align with the 2025.12 version of the Python array API [#3009](https://github.com/IntelPython/dpnp/pull/3009) ### Changed From 181a7289c49865fd8e4a23e517ceff9fd7c1f8a8 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 29 Jul 2026 15:36:37 +0200 Subject: [PATCH 6/7] Validate every dim in passed shape --- dpnp/dpnp_iface_manipulation.py | 19 ++++++++++++++----- dpnp/tests/test_manipulation.py | 8 +++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 6ba4770d0bbb..bab98391057d 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1128,13 +1128,22 @@ def broadcast_shapes(*args): shapes = [] for sh in args: + # a bare integer is treated as a one-dimensional shape if not isinstance(sh, (tuple, list)): - # a bare integer is treated as a one-dimensional shape - sh = (operator.index(sh),) + sh = (sh,) - if any(dim < 0 for dim in sh): - raise ValueError("negative dimensions are not allowed") - shapes.append(sh) + new_sh = [] + for dim in sh: + if isinstance(dim, bool): + raise TypeError( + "'bool' object cannot be interpreted as an integer" + ) + + dim = operator.index(dim) + if dim < 0: + raise ValueError("negative dimensions are not allowed") + new_sh.append(dim) + shapes.append(tuple(new_sh)) return dpt.broadcast_shapes(*shapes) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 07f18cc02a2f..a33af5594bcb 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -348,7 +348,13 @@ def test_scalar(self, shape): @pytest.mark.parametrize("xp", [dpnp, numpy]) @pytest.mark.parametrize("shape", [[(-1,)], [(2, -3), (2, 3)], [-1, 2]]) def test_negative_dim(self, xp, shape): - with pytest.raises(ValueError, match="aaa"): + with pytest.raises(ValueError, match="negative dimensions"): + xp.broadcast_shapes(*shape) + + @pytest.mark.parametrize("xp", [dpnp, numpy]) + @pytest.mark.parametrize("shape", [[(2.0,)], [(True, 2)], [2.5]]) + def test_non_integer_dim(self, xp, shape): + with pytest.raises(TypeError, match="integer"): xp.broadcast_shapes(*shape) From fa6ab410a3b4764987f34b859173a4cb57ab84f9 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 29 Jul 2026 15:45:46 +0200 Subject: [PATCH 7/7] dpnp.tensor.broadcast_shapes now rejects non-integer dimensions --- dpnp/tensor/_manipulation_functions.py | 5 ++++- dpnp/tests/tensor/test_usm_ndarray_manipulation.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/dpnp/tensor/_manipulation_functions.py b/dpnp/tensor/_manipulation_functions.py index 0a21acddb85d..4f443feb1363 100644 --- a/dpnp/tensor/_manipulation_functions.py +++ b/dpnp/tensor/_manipulation_functions.py @@ -264,11 +264,14 @@ def broadcast_shapes(*shapes): shapes against one another. Raises: + TypeError: if a shape contains a non-integer dimension. ValueError: if the input shapes are not broadcast-compatible. """ if len(shapes) == 0: return () - return _broadcast_shape_impl([tuple(sh) for sh in shapes]) + + normalized = [tuple(operator.index(dim) for dim in sh) for sh in shapes] + return _broadcast_shape_impl(normalized) def broadcast_to(X, /, shape): diff --git a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py index 1c0bd5faadc0..1ea36cd4f019 100644 --- a/dpnp/tests/tensor/test_usm_ndarray_manipulation.py +++ b/dpnp/tests/tensor/test_usm_ndarray_manipulation.py @@ -499,6 +499,12 @@ def test_broadcast_shapes_raises(): dpt.broadcast_shapes((2, 3), (4, 5)) +@pytest.mark.parametrize("shapes", [[(2.0,)], [(1.5,), (2,)], [(2,), (3.0, 1)]]) +def test_broadcast_shapes_non_integer_dim(shapes): + with pytest.raises(TypeError): + dpt.broadcast_shapes(*shapes) + + def test_flip_axis_incorrect(): q = get_queue_or_skip()