Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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(operator.index(dim) for dim in sh) for sh in shapes]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
normalized = [tuple(operator.index(dim) for dim in sh) for sh in shapes]
normalized = [tuple(map(operator.index, sh)) for sh in shapes]

map is often a bit more efficient than a generator expression from my understanding

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