Skip to content

Commit aa39048

Browse files
committed
task: add boradcast class implementation
1 parent 35010ee commit aa39048

5 files changed

Lines changed: 368 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [0.21.0] - MM/DD/2026
88

99
### Added
10+
* Added implementation of `dpnp.broadcast` class.
1011

1112
### Changed
1213

dpnp/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@
304304
unravel_index,
305305
)
306306
from .dpnp_flatiter import flatiter
307+
from .dpnp_broadcast import broadcast
307308

308309
# -----------------------------------------------------------------------------
309310
# Linear algebra
@@ -691,6 +692,7 @@
691692
"atleast_1d",
692693
"atleast_2d",
693694
"atleast_3d",
695+
"broadcast",
694696
"broadcast_arrays",
695697
"broadcast_to",
696698
"column_stack",

dpnp/dpnp_broadcast.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# *****************************************************************************
2+
# Copyright (c) 2026, Intel Corporation
3+
# All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are met:
7+
# - Redistributions of source code must retain the above copyright notice,
8+
# this list of conditions and the following disclaimer.
9+
# - Redistributions in binary form must reproduce the above copyright notice,
10+
# this list of conditions and the following disclaimer in the documentation
11+
# and/or other materials provided with the distribution.
12+
# - Neither the name of the copyright holder nor the names of its contributors
13+
# may be used to endorse or promote products derived from this software
14+
# without specific prior written permission.
15+
#
16+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
26+
# THE POSSIBILITY OF SUCH DAMAGE.
27+
# *****************************************************************************
28+
29+
"""Implementation of broadcast class."""
30+
31+
import dpnp
32+
from dpnp.tensor._manipulation_functions import _broadcast_shapes
33+
34+
35+
class broadcast:
36+
"""
37+
Produce an object that mimics broadcasting.
38+
39+
For full documentation refer to :obj:`numpy.broadcast`.
40+
41+
Parameters
42+
----------
43+
*args : array_like
44+
Input parameters.
45+
46+
Returns
47+
-------
48+
broadcast : broadcast object
49+
Broadcast the input parameters against one another, and
50+
return an object that encapsulates the result.
51+
Amongst others, it has ``shape`` and ``nd`` properties, and
52+
may be used as an iterator.
53+
54+
See Also
55+
--------
56+
:obj:`dpnp.broadcast_arrays` : Broadcast any number of arrays against
57+
each other.
58+
:obj:`dpnp.broadcast_to` : Broadcast an array to a new shape.
59+
:obj:`dpnp.broadcast_shapes` : Broadcast the input shapes into a single
60+
shape.
61+
62+
Examples
63+
--------
64+
>>> import dpnp as np
65+
>>> x = np.array([[1], [2], [3]])
66+
>>> y = np.array([4, 5, 6])
67+
>>> b = np.broadcast(x, y)
68+
>>> b.shape
69+
(3, 3)
70+
>>> b.nd
71+
2
72+
>>> b.size
73+
9
74+
75+
Notes
76+
-----
77+
Iterator functionality is not supported.
78+
79+
"""
80+
81+
def __init__(self, *args):
82+
# Convert all arguments to dpnp arrays
83+
arrays = []
84+
for arg in args:
85+
if not isinstance(arg, dpnp.ndarray):
86+
# Convert array-like to dpnp.ndarray
87+
arg = dpnp.asarray(arg)
88+
arrays.append(arg)
89+
90+
if len(arrays) == 0:
91+
raise TypeError("broadcast() requires at least one array")
92+
93+
self._arrays = tuple(arrays)
94+
95+
# Compute the broadcasted shape using _broadcast_shapes
96+
self._shape = _broadcast_shapes(*self._arrays)
97+
98+
# Calculate size and ndim
99+
self._size = 1
100+
for dim in self._shape:
101+
self._size *= dim
102+
self._nd = len(self._shape)
103+
104+
@property
105+
def shape(self):
106+
"""
107+
Shape of the broadcasted result.
108+
109+
Returns
110+
-------
111+
out : tuple
112+
A tuple containing the shape of the broadcasted result.
113+
114+
"""
115+
return self._shape
116+
117+
@property
118+
def size(self):
119+
"""
120+
Total size of the broadcasted result.
121+
122+
Returns
123+
-------
124+
out : int
125+
The total size (number of elements) of the broadcasted result.
126+
127+
"""
128+
return self._size
129+
130+
@property
131+
def nd(self):
132+
"""
133+
Number of dimensions of the broadcasted result.
134+
135+
Returns
136+
-------
137+
out : int
138+
The number of dimensions of the broadcasted result.
139+
140+
"""
141+
return self._nd
142+
143+
@property
144+
def ndim(self):
145+
"""
146+
Number of dimensions of the broadcasted result.
147+
148+
Returns
149+
-------
150+
out : int
151+
The number of dimensions of the broadcasted result.
152+
153+
"""
154+
return self._nd
155+
156+
@property
157+
def numiter(self):
158+
"""
159+
Number of iterators possessed by the broadcast object.
160+
161+
Returns
162+
-------
163+
out : int
164+
The number of iterators.
165+
166+
"""
167+
return len(self._arrays)
168+
169+
def __repr__(self):
170+
return f"<broadcast shape={self.shape}, nd={self.nd}, size={self.size}>"

dpnp/tests/test_manipulation.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,3 +1993,198 @@ def test_2D_array(self):
19931993
expected = numpy.vsplit(a, 2)
19941994
result = dpnp.vsplit(a_dp, 2)
19951995
_compare_results(result, expected)
1996+
1997+
1998+
class TestBroadcast:
1999+
"""Test cases for dpnp.broadcast class."""
2000+
2001+
def test_broadcast_basic(self):
2002+
# Test basic broadcast with compatible shapes
2003+
x = dpnp.array([[1], [2], [3]])
2004+
y = dpnp.array([4, 5, 6])
2005+
2006+
b = dpnp.broadcast(x, y)
2007+
b_np = numpy.broadcast(x.asnumpy(), y.asnumpy())
2008+
2009+
assert b.shape == b_np.shape
2010+
assert b.nd == b_np.nd
2011+
assert b.size == b_np.size
2012+
assert b.numiter == b_np.numiter
2013+
2014+
def test_broadcast_scalar(self):
2015+
# Test broadcast with scalar
2016+
a = dpnp.array([1, 2, 3])
2017+
s = dpnp.array(5)
2018+
2019+
b = dpnp.broadcast(a, s)
2020+
b_np = numpy.broadcast(a.asnumpy(), s.asnumpy())
2021+
2022+
assert b.shape == b_np.shape
2023+
assert b.nd == b_np.nd
2024+
assert b.size == b_np.size
2025+
2026+
def test_broadcast_multiple_arrays(self):
2027+
# Test broadcast with multiple arrays
2028+
a1 = dpnp.array([1, 2, 3])
2029+
a2 = dpnp.array([[1], [2]])
2030+
2031+
b = dpnp.broadcast(a1, a2)
2032+
b_np = numpy.broadcast(a1.asnumpy(), a2.asnumpy())
2033+
2034+
assert b.shape == b_np.shape
2035+
assert b.nd == b_np.nd
2036+
assert b.size == b_np.size
2037+
2038+
def test_broadcast_same_shape(self):
2039+
# Test broadcast with arrays of the same shape
2040+
a = dpnp.array([[1, 2], [3, 4]])
2041+
b = dpnp.array([[5, 6], [7, 8]])
2042+
2043+
bc = dpnp.broadcast(a, b)
2044+
bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy())
2045+
2046+
assert bc.shape == bc_np.shape
2047+
assert bc.nd == bc_np.nd
2048+
assert bc.size == bc_np.size
2049+
2050+
def test_broadcast_0d_arrays(self):
2051+
# Test broadcast with 0-D arrays
2052+
a = dpnp.array(5)
2053+
b = dpnp.array(10)
2054+
2055+
bc = dpnp.broadcast(a, b)
2056+
bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy())
2057+
2058+
assert bc.shape == bc_np.shape
2059+
assert bc.nd == bc_np.nd
2060+
assert bc.size == bc_np.size
2061+
2062+
def test_broadcast_empty_arrays(self):
2063+
# Test broadcast with empty arrays
2064+
a = dpnp.array([])
2065+
b = dpnp.array([])
2066+
2067+
bc = dpnp.broadcast(a, b)
2068+
bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy())
2069+
2070+
assert bc.shape == bc_np.shape
2071+
assert bc.nd == bc_np.nd
2072+
assert bc.size == bc_np.size
2073+
2074+
def test_broadcast_incompatible_shapes(self):
2075+
# Test that incompatible shapes raise ValueError
2076+
a = dpnp.array([1, 2, 3])
2077+
b = dpnp.array([1, 2])
2078+
2079+
with pytest.raises(ValueError):
2080+
dpnp.broadcast(a, b)
2081+
2082+
def test_broadcast_incompatible_shapes_2d(self):
2083+
# Test incompatible 2D shapes
2084+
a = dpnp.array([[1, 2, 3]])
2085+
b = dpnp.array([[1], [2], [3], [4]])
2086+
2087+
with pytest.raises(ValueError):
2088+
dpnp.broadcast(a, b)
2089+
2090+
def test_broadcast_three_arrays(self):
2091+
# Test broadcast with three arrays
2092+
a = dpnp.array([1, 2, 3])
2093+
b = dpnp.array([[1], [2]])
2094+
c = dpnp.array(5)
2095+
2096+
bc = dpnp.broadcast(a, b, c)
2097+
bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy(), c.asnumpy())
2098+
2099+
assert bc.shape == bc_np.shape
2100+
assert bc.nd == bc_np.nd
2101+
assert bc.size == bc_np.size
2102+
assert bc.numiter == 3
2103+
2104+
def test_broadcast_ndim_property(self):
2105+
# Test that ndim property equals nd property
2106+
a = dpnp.array([[1, 2], [3, 4]])
2107+
b = dpnp.array([5, 6])
2108+
2109+
bc = dpnp.broadcast(a, b)
2110+
2111+
assert bc.ndim == bc.nd
2112+
2113+
def test_broadcast_complex_shapes(self):
2114+
# Test broadcast with complex compatible shapes
2115+
a = dpnp.array([[[1]]])
2116+
b = dpnp.array([[1, 2, 3]])
2117+
c = dpnp.array([[1], [2]])
2118+
2119+
bc = dpnp.broadcast(a, b, c)
2120+
bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy(), c.asnumpy())
2121+
2122+
assert bc.shape == bc_np.shape
2123+
assert bc.nd == bc_np.nd
2124+
assert bc.size == bc_np.size
2125+
2126+
def test_broadcast_with_array_like(self):
2127+
# Test broadcast with array-like inputs (lists)
2128+
a = dpnp.array([1, 2, 3])
2129+
b = [[1], [2]]
2130+
2131+
bc = dpnp.broadcast(a, b)
2132+
bc_np = numpy.broadcast(a.asnumpy(), b)
2133+
2134+
assert bc.shape == bc_np.shape
2135+
assert bc.nd == bc_np.nd
2136+
assert bc.size == bc_np.size
2137+
2138+
@pytest.mark.parametrize(
2139+
"shapes",
2140+
[
2141+
((), ()),
2142+
((1,), (1,)),
2143+
((2,), (2,)),
2144+
((0,), (1,)),
2145+
((2, 3), (1, 3)),
2146+
((2, 1, 3, 4), (3, 1, 4)),
2147+
((4, 3, 2, 3), (2, 3)),
2148+
((2, 0, 1, 1, 3), (2, 1, 0, 0, 3)),
2149+
],
2150+
)
2151+
def test_broadcast_parametrized_shapes(self, shapes):
2152+
# Test various compatible shape combinations
2153+
arrays_dp = [dpnp.ones(s) for s in shapes]
2154+
arrays_np = [numpy.ones(s) for s in shapes]
2155+
2156+
bc = dpnp.broadcast(*arrays_dp)
2157+
bc_np = numpy.broadcast(*arrays_np)
2158+
2159+
assert bc.shape == bc_np.shape
2160+
assert bc.nd == bc_np.nd
2161+
assert bc.size == bc_np.size
2162+
2163+
def test_broadcast_single_array(self):
2164+
# Test broadcast with a single array
2165+
a = dpnp.array([[1, 2], [3, 4]])
2166+
2167+
bc = dpnp.broadcast(a)
2168+
bc_np = numpy.broadcast(a.asnumpy())
2169+
2170+
assert bc.shape == bc_np.shape
2171+
assert bc.nd == bc_np.nd
2172+
assert bc.size == bc_np.size
2173+
assert bc.numiter == 1
2174+
2175+
def test_broadcast_no_args(self):
2176+
# Test that broadcast with no arguments raises TypeError
2177+
with pytest.raises(TypeError):
2178+
dpnp.broadcast()
2179+
2180+
def test_broadcast_repr(self):
2181+
# Test __repr__ method
2182+
a = dpnp.array([1, 2, 3])
2183+
b = dpnp.array([[1], [2]])
2184+
2185+
bc = dpnp.broadcast(a, b)
2186+
repr_str = repr(bc)
2187+
2188+
assert "broadcast" in repr_str
2189+
assert "shape" in repr_str
2190+
assert str(bc.shape) in repr_str

0 commit comments

Comments
 (0)