Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 19 additions & 1 deletion dpnp/dpnp_iface_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,25 @@ def broadcast_shapes(*args):

"""

return numpy.broadcast_shapes(*args)
shapes = []
for sh in args:
# a bare integer is treated as a one-dimensional shape
if not isinstance(sh, (tuple, list)):
sh = (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)


# pylint: disable=redefined-outer-name
Expand Down
2 changes: 2 additions & 0 deletions dpnp/tensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
)
from ._manipulation_functions import (
broadcast_arrays,
broadcast_shapes,
broadcast_to,
concat,
expand_dims,
Expand Down Expand Up @@ -282,6 +283,7 @@
"bitwise_right_shift",
"bitwise_xor",
"broadcast_arrays",
"broadcast_shapes",
"broadcast_to",
"can_cast",
"cbrt",
Expand Down
26 changes: 26 additions & 0 deletions dpnp/tensor/_manipulation_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,32 @@ 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:
TypeError: if a shape contains a non-integer dimension.
ValueError: if the input shapes are not broadcast-compatible.
"""
if len(shapes) == 0:
return ()

normalized = [tuple(map(operator.index, sh)) for sh in shapes]
return _broadcast_shape_impl(normalized)


def broadcast_to(X, /, shape):
"""broadcast_to(x, shape)

Expand Down
36 changes: 36 additions & 0 deletions dpnp/tests/tensor/test_usm_ndarray_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,42 @@ 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))


@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()

Expand Down
27 changes: 26 additions & 1 deletion dpnp/tests/test_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ def test_no_copy(self):
assert_array_equal(b, a)


class TestBroadcast:
class TestBroadcastShapes:
@pytest.mark.parametrize(
"shape",
[
Expand All @@ -332,6 +332,31 @@ 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="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)


class TestCopyTo:
testdata = []
Expand Down
Loading