Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.21.0] - MM/DD/2026

### Added
* Added `dpnp.broadcast` class implementation [#2901](https://github.com/IntelPython/dpnp/pull/2901)

### Changed

Expand Down
2 changes: 2 additions & 0 deletions dpnp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@
unravel_index,
)
from .dpnp_flatiter import flatiter
from .dpnp_broadcast import broadcast

# -----------------------------------------------------------------------------
# Linear algebra
Expand Down Expand Up @@ -691,6 +692,7 @@
"atleast_1d",
"atleast_2d",
"atleast_3d",
"broadcast",
"broadcast_arrays",
"broadcast_to",
"column_stack",
Expand Down
170 changes: 170 additions & 0 deletions dpnp/dpnp_broadcast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# *****************************************************************************
# Copyright (c) 2026, Intel Corporation
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# - Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# - Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# - Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
# THE POSSIBILITY OF SUCH DAMAGE.
# *****************************************************************************

"""Implementation of broadcast class."""

import dpnp
from dpnp.tensor._manipulation_functions import _broadcast_shapes


class broadcast:
"""
Produce an object that mimics broadcasting.

For full documentation refer to :obj:`numpy.broadcast`.

Parameters
----------
*args : array_like
Input parameters.

Returns
-------
broadcast : broadcast object
Broadcast the input parameters against one another, and
return an object that encapsulates the result.
Amongst others, it has ``shape`` and ``nd`` properties, and
may be used as an iterator.

See Also
--------
:obj:`dpnp.broadcast_arrays` : Broadcast any number of arrays against
each other.
:obj:`dpnp.broadcast_to` : Broadcast an array to a new shape.
:obj:`dpnp.broadcast_shapes` : Broadcast the input shapes into a single
shape.

Examples
--------
>>> import dpnp as np
>>> x = np.array([[1], [2], [3]])
>>> y = np.array([4, 5, 6])
>>> b = np.broadcast(x, y)
>>> b.shape
(3, 3)
>>> b.nd
2
>>> b.size
9

Notes
-----
Iterator functionality is not supported.

"""

def __init__(self, *args):
# Convert all arguments to dpnp arrays
arrays = []
for arg in args:
if not isinstance(arg, dpnp.ndarray):
# Convert array-like to dpnp.ndarray
Comment thread
jharlow-intel marked this conversation as resolved.
Outdated
arg = dpnp.asarray(arg)
arrays.append(arg)

if len(arrays) == 0:
raise TypeError("broadcast() requires at least one array")
Comment thread
jharlow-intel marked this conversation as resolved.
Outdated

self._arrays = tuple(arrays)

# Compute the broadcasted shape using _broadcast_shapes
self._shape = _broadcast_shapes(*self._arrays)

# Calculate size and ndim
self._size = 1
for dim in self._shape:
self._size *= dim
self._nd = len(self._shape)

@property
def shape(self):
"""
Shape of the broadcasted result.

Returns
-------
out : tuple
A tuple containing the shape of the broadcasted result.

"""
return self._shape

@property
def size(self):
"""
Total size of the broadcasted result.

Returns
-------
out : int
The total size (number of elements) of the broadcasted result.

"""
return self._size

@property
def nd(self):
"""
Number of dimensions of the broadcasted result.

Returns
-------
out : int
The number of dimensions of the broadcasted result.

"""
return self._nd

@property
def ndim(self):
"""
Number of dimensions of the broadcasted result.

Returns
-------
out : int
The number of dimensions of the broadcasted result.

"""
return self._nd

@property
def numiter(self):
"""
Number of iterators possessed by the broadcast object.

Returns
-------
out : int
The number of iterators.

"""
return len(self._arrays)

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.

primarily a question for @antonwolfy and @vlad-perevezentsev since it's design-related, but NumPy and CuPy differ drastically in this class implementation

https://numpy.org/doc/2.1/reference/generated/numpy.broadcast.html
https://docs.cupy.dev/en/latest/reference/generated/cupy.broadcast.html

do we want more of the CuPy or NumPy behavior? What is the intended use-case of this class to users?


def __repr__(self):
return f"<broadcast shape={self.shape}, nd={self.nd}, size={self.size}>"
195 changes: 195 additions & 0 deletions dpnp/tests/test_manipulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1993,3 +1993,198 @@ def test_2D_array(self):
expected = numpy.vsplit(a, 2)
result = dpnp.vsplit(a_dp, 2)
_compare_results(result, expected)


class TestBroadcast:
"""Test cases for dpnp.broadcast class."""

def test_broadcast_basic(self):
# Test basic broadcast with compatible shapes
x = dpnp.array([[1], [2], [3]])
y = dpnp.array([4, 5, 6])

b = dpnp.broadcast(x, y)
b_np = numpy.broadcast(x.asnumpy(), y.asnumpy())

assert b.shape == b_np.shape
assert b.nd == b_np.nd
assert b.size == b_np.size
assert b.numiter == b_np.numiter

def test_broadcast_scalar(self):
# Test broadcast with scalar
a = dpnp.array([1, 2, 3])
s = dpnp.array(5)

b = dpnp.broadcast(a, s)
b_np = numpy.broadcast(a.asnumpy(), s.asnumpy())

assert b.shape == b_np.shape
assert b.nd == b_np.nd
assert b.size == b_np.size

def test_broadcast_multiple_arrays(self):
# Test broadcast with multiple arrays
a1 = dpnp.array([1, 2, 3])
a2 = dpnp.array([[1], [2]])

b = dpnp.broadcast(a1, a2)
b_np = numpy.broadcast(a1.asnumpy(), a2.asnumpy())

assert b.shape == b_np.shape
assert b.nd == b_np.nd
assert b.size == b_np.size

def test_broadcast_same_shape(self):
# Test broadcast with arrays of the same shape
a = dpnp.array([[1, 2], [3, 4]])
b = dpnp.array([[5, 6], [7, 8]])

bc = dpnp.broadcast(a, b)
bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy())

assert bc.shape == bc_np.shape
assert bc.nd == bc_np.nd
assert bc.size == bc_np.size

def test_broadcast_0d_arrays(self):
# Test broadcast with 0-D arrays
a = dpnp.array(5)
b = dpnp.array(10)

bc = dpnp.broadcast(a, b)
bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy())

assert bc.shape == bc_np.shape
assert bc.nd == bc_np.nd
assert bc.size == bc_np.size

def test_broadcast_empty_arrays(self):
# Test broadcast with empty arrays
a = dpnp.array([])
b = dpnp.array([])

bc = dpnp.broadcast(a, b)
bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy())

assert bc.shape == bc_np.shape
assert bc.nd == bc_np.nd
assert bc.size == bc_np.size

def test_broadcast_incompatible_shapes(self):
# Test that incompatible shapes raise ValueError
a = dpnp.array([1, 2, 3])
b = dpnp.array([1, 2])

with pytest.raises(ValueError):
dpnp.broadcast(a, b)

def test_broadcast_incompatible_shapes_2d(self):
# Test incompatible 2D shapes
a = dpnp.array([[1, 2, 3]])
b = dpnp.array([[1], [2], [3], [4]])

with pytest.raises(ValueError):
dpnp.broadcast(a, b)

def test_broadcast_three_arrays(self):
# Test broadcast with three arrays
a = dpnp.array([1, 2, 3])
b = dpnp.array([[1], [2]])
c = dpnp.array(5)

bc = dpnp.broadcast(a, b, c)
bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy(), c.asnumpy())

assert bc.shape == bc_np.shape
assert bc.nd == bc_np.nd
assert bc.size == bc_np.size
assert bc.numiter == 3

def test_broadcast_ndim_property(self):
# Test that ndim property equals nd property
a = dpnp.array([[1, 2], [3, 4]])
b = dpnp.array([5, 6])

bc = dpnp.broadcast(a, b)

assert bc.ndim == bc.nd

def test_broadcast_complex_shapes(self):
# Test broadcast with complex compatible shapes
a = dpnp.array([[[1]]])
b = dpnp.array([[1, 2, 3]])
c = dpnp.array([[1], [2]])

bc = dpnp.broadcast(a, b, c)
bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy(), c.asnumpy())

assert bc.shape == bc_np.shape
assert bc.nd == bc_np.nd
assert bc.size == bc_np.size

def test_broadcast_with_array_like(self):
# Test broadcast with array-like inputs (lists)
a = dpnp.array([1, 2, 3])
b = [[1], [2]]

bc = dpnp.broadcast(a, b)
bc_np = numpy.broadcast(a.asnumpy(), b)

assert bc.shape == bc_np.shape
assert bc.nd == bc_np.nd
assert bc.size == bc_np.size

@pytest.mark.parametrize(
"shapes",
[
((), ()),
((1,), (1,)),
((2,), (2,)),
((0,), (1,)),
((2, 3), (1, 3)),
((2, 1, 3, 4), (3, 1, 4)),
((4, 3, 2, 3), (2, 3)),
((2, 0, 1, 1, 3), (2, 1, 0, 0, 3)),
],
)
def test_broadcast_parametrized_shapes(self, shapes):
# Test various compatible shape combinations
arrays_dp = [dpnp.ones(s) for s in shapes]
arrays_np = [numpy.ones(s) for s in shapes]

bc = dpnp.broadcast(*arrays_dp)
bc_np = numpy.broadcast(*arrays_np)

assert bc.shape == bc_np.shape
assert bc.nd == bc_np.nd
assert bc.size == bc_np.size

def test_broadcast_single_array(self):
# Test broadcast with a single array
a = dpnp.array([[1, 2], [3, 4]])

bc = dpnp.broadcast(a)
bc_np = numpy.broadcast(a.asnumpy())

assert bc.shape == bc_np.shape
assert bc.nd == bc_np.nd
assert bc.size == bc_np.size
assert bc.numiter == 1

def test_broadcast_no_args(self):
# Test that broadcast with no arguments raises TypeError
with pytest.raises(TypeError):
dpnp.broadcast()

def test_broadcast_repr(self):
# Test __repr__ method
a = dpnp.array([1, 2, 3])
b = dpnp.array([[1], [2]])

bc = dpnp.broadcast(a, b)
repr_str = repr(bc)

assert "broadcast" in repr_str
assert "shape" in repr_str
assert str(bc.shape) in repr_str
Loading
Loading