From a2f50c367fd412603b48aae470173f1d4afae4d2 Mon Sep 17 00:00:00 2001 From: "Harlow, Jordan" Date: Wed, 6 May 2026 13:09:55 -0600 Subject: [PATCH 1/8] task: add broadcast class implementation --- CHANGELOG.md | 1 + dpnp/__init__.py | 2 + dpnp/dpnp_broadcast.py | 170 +++++++++++++++ dpnp/tests/test_manipulation.py | 195 ++++++++++++++++++ .../cupy/manipulation_tests/test_dims.py | 2 - 5 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 dpnp/dpnp_broadcast.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 752bf2ad4b3f..33cae14e89ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/dpnp/__init__.py b/dpnp/__init__.py index d2ea158d4d44..cafbd972ff5e 100644 --- a/dpnp/__init__.py +++ b/dpnp/__init__.py @@ -304,6 +304,7 @@ unravel_index, ) from .dpnp_flatiter import flatiter +from .dpnp_broadcast import broadcast # ----------------------------------------------------------------------------- # Linear algebra @@ -691,6 +692,7 @@ "atleast_1d", "atleast_2d", "atleast_3d", + "broadcast", "broadcast_arrays", "broadcast_to", "column_stack", diff --git a/dpnp/dpnp_broadcast.py b/dpnp/dpnp_broadcast.py new file mode 100644 index 000000000000..a386483dad06 --- /dev/null +++ b/dpnp/dpnp_broadcast.py @@ -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 + arg = dpnp.asarray(arg) + arrays.append(arg) + + if len(arrays) == 0: + raise TypeError("broadcast() requires at least one array") + + 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) + + def __repr__(self): + return f"" diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 4fc4b8cb1619..503c2db28cea 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -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 diff --git a/dpnp/tests/third_party/cupy/manipulation_tests/test_dims.py b/dpnp/tests/third_party/cupy/manipulation_tests/test_dims.py index ae0f6ce18b47..9ee219630384 100644 --- a/dpnp/tests/third_party/cupy/manipulation_tests/test_dims.py +++ b/dpnp/tests/third_party/cupy/manipulation_tests/test_dims.py @@ -300,7 +300,6 @@ def _broadcast(self, xp, dtype, shapes): arrays = [testing.shaped_arange(s, xp, dtype) for s in shapes] return xp.broadcast(*arrays) - @pytest.mark.skip("broadcast() is not supported yet") @testing.for_all_dtypes() def test_broadcast(self, dtype): broadcast_np = self._broadcast(numpy, dtype, self.shapes) @@ -340,7 +339,6 @@ def test_broadcast_arrays(self, xp, dtype): ) class TestInvalidBroadcast(unittest.TestCase): - @pytest.mark.skip("broadcast() is not supported yet") @testing.for_all_dtypes() def test_invalid_broadcast(self, dtype): for xp in (numpy, cupy): From 3f45c6675e0514fd2359351abc249c3607019661 Mon Sep 17 00:00:00 2001 From: "Harlow, Jordan" Date: Thu, 7 May 2026 05:06:32 -0600 Subject: [PATCH 2/8] fix: incompatible broadcast test case --- dpnp/tests/test_manipulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 503c2db28cea..953ee1e702cf 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -2081,7 +2081,7 @@ def test_broadcast_incompatible_shapes(self): def test_broadcast_incompatible_shapes_2d(self): # Test incompatible 2D shapes - a = dpnp.array([[1, 2, 3]]) + a = dpnp.array([[1, 2, 3], [4, 5, 6]]) b = dpnp.array([[1], [2], [3], [4]]) with pytest.raises(ValueError): From 76c65dea4224c11e3d4bdc3ca004f62477f8e3e1 Mon Sep 17 00:00:00 2001 From: "Harlow, Jordan" Date: Tue, 26 May 2026 13:25:02 -0600 Subject: [PATCH 3/8] fix: review --- dpnp/dpnp_broadcast.py | 41 ++++++++++++++++++++++----------- dpnp/tests/test_manipulation.py | 36 +++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 23 deletions(-) diff --git a/dpnp/dpnp_broadcast.py b/dpnp/dpnp_broadcast.py index a386483dad06..059b408cbe15 100644 --- a/dpnp/dpnp_broadcast.py +++ b/dpnp/dpnp_broadcast.py @@ -29,6 +29,7 @@ """Implementation of broadcast class.""" import dpnp +import dpnp.tensor as dpt from dpnp.tensor._manipulation_functions import _broadcast_shapes @@ -40,8 +41,8 @@ class broadcast: Parameters ---------- - *args : array_like - Input parameters. + *args : object + Input parameters. Every argument must define ``shape`` attribute. Returns ------- @@ -79,18 +80,30 @@ class broadcast: """ 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 - arg = dpnp.asarray(arg) - arrays.append(arg) - - if len(arrays) == 0: - raise TypeError("broadcast() requires at least one array") - - self._arrays = tuple(arrays) + for i, arg in enumerate(args): + if not hasattr(arg, "shape"): + raise TypeError( + f"Argument at position {i} must define shape attribute" + ) + + self._arrays = tuple(args) + + dpnp_arrays = [arg for arg in self._arrays if isinstance(arg, dpnp.ndarray)] + if len(dpnp_arrays) > 1: + exec_q = dpt.get_execution_queue( + tuple(array.sycl_queue for array in dpnp_arrays) + ) + if exec_q is None: + raise dpt.ExecutionPlacementError( + "Execution placement can not be unambiguously inferred " + "from input arguments." + ) + + if len(self._arrays) == 0: + self._shape = () + self._size = 1 + self._nd = 0 + return # Compute the broadcasted shape using _broadcast_shapes self._shape = _broadcast_shapes(*self._arrays) diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 953ee1e702cf..a78bf54b003a 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -26,6 +26,7 @@ numpy_version, ) from .third_party.cupy import testing +from .tensor.helper import get_queue_or_skip def _compare_results(result, expected): @@ -2124,16 +2125,12 @@ def test_broadcast_complex_shapes(self): assert bc.size == bc_np.size def test_broadcast_with_array_like(self): - # Test broadcast with array-like inputs (lists) + # Conversion from array-like inputs is not implemented for broadcast yet. 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 + with pytest.raises(TypeError): + dpnp.broadcast(a, b) @pytest.mark.parametrize( "shapes", @@ -2173,9 +2170,30 @@ def test_broadcast_single_array(self): assert bc.numiter == 1 def test_broadcast_no_args(self): - # Test that broadcast with no arguments raises TypeError + # Test broadcast with no arguments. + bc = dpnp.broadcast() + + assert bc.shape == () + assert bc.nd == 0 + assert bc.ndim == 0 + assert bc.size == 1 + assert bc.numiter == 0 + + def test_broadcast_argument_without_shape(self): + a = dpnp.array([1, 2, 3]) + with pytest.raises(TypeError): - dpnp.broadcast() + dpnp.broadcast(a, 3) + + def test_broadcast_compute_follows_data(self): + q1 = get_queue_or_skip() + q2 = get_queue_or_skip() + + a = dpt.ones((2, 1), sycl_queue=q1) + b = dpt.ones((1, 2), sycl_queue=q2) + + with pytest.raises(dpt.ExecutionPlacementError): + dpnp.broadcast(a, b) def test_broadcast_repr(self): # Test __repr__ method From c6434f374700d40e17df09a9dc163d5e3462f13f Mon Sep 17 00:00:00 2001 From: "Harlow, Jordan" Date: Wed, 5 Aug 2026 13:08:25 -0600 Subject: [PATCH 4/8] fix review --- dpnp/dpnp_broadcast.py | 30 ++++++++----------------- dpnp/tests/test_manipulation.py | 39 +++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/dpnp/dpnp_broadcast.py b/dpnp/dpnp_broadcast.py index 059b408cbe15..f93d2a296d7e 100644 --- a/dpnp/dpnp_broadcast.py +++ b/dpnp/dpnp_broadcast.py @@ -29,7 +29,6 @@ """Implementation of broadcast class.""" import dpnp -import dpnp.tensor as dpt from dpnp.tensor._manipulation_functions import _broadcast_shapes @@ -41,16 +40,15 @@ class broadcast: Parameters ---------- - *args : object - Input parameters. Every argument must define ``shape`` attribute. + *args : {dpnp.ndarray, usm_ndarray} + Input arrays to broadcast against one another. 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. + Amongst others, it has ``shape`` and ``nd`` properties. See Also -------- @@ -73,6 +71,11 @@ class broadcast: >>> b.size 9 + Limitations + ----------- + Input arrays are not coerced, so array-like objects and scalars are not + supported and ``TypeError`` exception will be raised. + Notes ----- Iterator functionality is not supported. @@ -80,25 +83,10 @@ class broadcast: """ def __init__(self, *args): - for i, arg in enumerate(args): - if not hasattr(arg, "shape"): - raise TypeError( - f"Argument at position {i} must define shape attribute" - ) + dpnp.check_supported_arrays_type(*args) self._arrays = tuple(args) - dpnp_arrays = [arg for arg in self._arrays if isinstance(arg, dpnp.ndarray)] - if len(dpnp_arrays) > 1: - exec_q = dpt.get_execution_queue( - tuple(array.sycl_queue for array in dpnp_arrays) - ) - if exec_q is None: - raise dpt.ExecutionPlacementError( - "Execution placement can not be unambiguously inferred " - "from input arguments." - ) - if len(self._arrays) == 0: self._shape = () self._size = 1 diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index ccf62c44bb6c..94ad99cc5961 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -24,8 +24,8 @@ get_unsigned_dtypes, has_support_aspect64, ) -from .third_party.cupy import testing from .tensor.helper import get_queue_or_skip +from .third_party.cupy import testing def _compare_results(result, expected): @@ -307,7 +307,7 @@ def test_no_copy(self): assert_array_equal(b, a) -class TestBroadcast: +class TestBroadcastShapes: @pytest.mark.parametrize( "shape", [ @@ -2095,12 +2095,14 @@ def test_broadcast_three_arrays(self): assert bc.numiter == 3 def test_broadcast_ndim_property(self): - # Test that ndim property equals nd property + # Test that ndim property matches numpy and equals nd property a = dpnp.array([[1, 2], [3, 4]]) b = dpnp.array([5, 6]) bc = dpnp.broadcast(a, b) + bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy()) + assert bc.ndim == bc_np.ndim assert bc.ndim == bc.nd def test_broadcast_complex_shapes(self): @@ -2116,13 +2118,19 @@ def test_broadcast_complex_shapes(self): assert bc.nd == bc_np.nd assert bc.size == bc_np.size - def test_broadcast_with_array_like(self): - # Conversion from array-like inputs is not implemented for broadcast yet. + @pytest.mark.parametrize( + "arg", + [[[1], [2]], 3, numpy.ones((2, 1))], + ids=["list", "scalar", "numpy"], + ) + def test_broadcast_unsupported_type(self, arg): + # unlike numpy, input arrays are not coerced, so array-like objects, + # scalars and host arrays are rejected the same way as they are by + # dpnp.broadcast_to and dpnp.broadcast_arrays a = dpnp.array([1, 2, 3]) - b = [[1], [2]] with pytest.raises(TypeError): - dpnp.broadcast(a, b) + dpnp.broadcast(a, arg) @pytest.mark.parametrize( "shapes", @@ -2171,21 +2179,20 @@ def test_broadcast_no_args(self): assert bc.size == 1 assert bc.numiter == 0 - def test_broadcast_argument_without_shape(self): - a = dpnp.array([1, 2, 3]) - - with pytest.raises(TypeError): - dpnp.broadcast(a, 3) - - def test_broadcast_compute_follows_data(self): + def test_broadcast_different_queues(self): + # Broadcasting is a shape-only query, so inputs are not required + # to share a common execution placement q1 = get_queue_or_skip() q2 = get_queue_or_skip() a = dpt.ones((2, 1), sycl_queue=q1) b = dpt.ones((1, 2), sycl_queue=q2) - with pytest.raises(dpt.ExecutionPlacementError): - dpnp.broadcast(a, b) + bc = dpnp.broadcast(a, b) + + assert bc.shape == (2, 2) + assert bc.size == 4 + assert bc.nd == 2 def test_broadcast_repr(self): # Test __repr__ method From 38c83325bb94c1880538e15da4ae65aa78eaa8eb Mon Sep 17 00:00:00 2001 From: "Harlow, Jordan" Date: Thu, 13 Aug 2026 09:26:21 -0600 Subject: [PATCH 5/8] fix: review --- CHANGELOG.md | 2 +- doc/reference/array-manipulation.rst | 5 + dpnp/__init__.py | 2 +- dpnp/dpnp_broadcast.py | 171 --------------- dpnp/dpnp_iface_manipulation.py | 195 ++++++++++++++++++ dpnp/tests/test_manipulation.py | 63 ++++-- .../cupy/manipulation_tests/test_dims.py | 3 +- 7 files changed, 253 insertions(+), 188 deletions(-) delete mode 100644 dpnp/dpnp_broadcast.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b491a9962b7..98d7c6e0f222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This release is compatible with NumPy 2.4.5. ### Added -* Added `dpnp.broadcast` class implementation [#2901](https://github.com/IntelPython/dpnp/pull/2901) * Added C API functions for `dpnp.tensor.usm_ndarray` setters and getters to avoid ABI breakage if `dpnp.tensor.usm_ndarray` is modified [gh-2866](https://github.com/IntelPython/dpnp/pull/2866) * Added support for buffer protocol objects as advanced index keys in `dpnp.ndarray` [#2889](https://github.com/IntelPython/dpnp/pull/2889) +* Added `dpnp.broadcast` class implementation [#2901](https://github.com/IntelPython/dpnp/pull/2901) ### Changed diff --git a/doc/reference/array-manipulation.rst b/doc/reference/array-manipulation.rst index 70a4fa790e5f..956a54a4394f 100644 --- a/doc/reference/array-manipulation.rst +++ b/doc/reference/array-manipulation.rst @@ -58,6 +58,11 @@ Changing number of dimensions atleast_2d atleast_3d broadcast + broadcast.shape + broadcast.size + broadcast.ndim + broadcast.numiter + broadcast.values broadcast_to broadcast_arrays expand_dims diff --git a/dpnp/__init__.py b/dpnp/__init__.py index cafbd972ff5e..8fd36ec5340f 100644 --- a/dpnp/__init__.py +++ b/dpnp/__init__.py @@ -188,6 +188,7 @@ atleast_1d, atleast_2d, atleast_3d, + broadcast, broadcast_arrays, broadcast_to, column_stack, @@ -304,7 +305,6 @@ unravel_index, ) from .dpnp_flatiter import flatiter -from .dpnp_broadcast import broadcast # ----------------------------------------------------------------------------- # Linear algebra diff --git a/dpnp/dpnp_broadcast.py b/dpnp/dpnp_broadcast.py deleted file mode 100644 index f93d2a296d7e..000000000000 --- a/dpnp/dpnp_broadcast.py +++ /dev/null @@ -1,171 +0,0 @@ -# ***************************************************************************** -# 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 : {dpnp.ndarray, usm_ndarray} - Input arrays to broadcast against one another. - - 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. - - 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 - - Limitations - ----------- - Input arrays are not coerced, so array-like objects and scalars are not - supported and ``TypeError`` exception will be raised. - - Notes - ----- - Iterator functionality is not supported. - - """ - - def __init__(self, *args): - dpnp.check_supported_arrays_type(*args) - - self._arrays = tuple(args) - - if len(self._arrays) == 0: - self._shape = () - self._size = 1 - self._nd = 0 - return - - # 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) - - def __repr__(self): - return f"" diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index b96d36a40e6a..d526eadb34e0 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -56,6 +56,7 @@ from .dpnp_utils import get_usm_allocations from .dpnp_utils.dpnp_utils_pad import dpnp_pad from .exceptions import AxisError +from .tensor._manipulation_functions import _broadcast_shapes from .tensor._numpy_helper import ( normalize_axis_index, normalize_axis_tuple, @@ -1047,6 +1048,193 @@ def atleast_3d(*arys): return res +class broadcast: # pylint: disable=invalid-name + """ + Produce an object that mimics broadcasting. + + For full documentation refer to :obj:`numpy.broadcast`. + + Parameters + ---------- + *args : {dpnp.ndarray, usm_ndarray} + Input arrays to broadcast against one another. + + 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 ``ndim`` properties. + + Limitations + ----------- + Input arrays are not coerced, so array-like objects and scalars are not + supported and ``TypeError`` exception will be raised. + + See Also + -------- + :obj:`dpnp.broadcast_arrays` : Broadcast any number of arrays against + each other. + :obj:`dpnp.broadcast_shapes` : Broadcast the input shapes into a single + shape. + :obj:`dpnp.broadcast_to` : Broadcast an array to a new shape. + + Notes + ----- + Iterator functionality is not supported. + + The legacy ``nd`` attribute of :obj:`numpy.broadcast` is not provided, + ``ndim`` has to be used instead. + + 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.ndim + 2 + >>> b.size + 9 + + """ + + def __init__(self, *args): + dpnp.check_supported_arrays_type(*args) + + self._arrays = args + self._values = None + + # _broadcast_shapes() does not accept an empty sequence of arrays + self._shape = _broadcast_shapes(*args) if args else () + self._size = math.prod(self._shape) + self._ndim = len(self._shape) + + @property + def shape(self): + """ + Shape of the broadcasted result. + + Returns + ------- + out : tuple + A tuple containing the shape of the broadcasted result. + + Examples + -------- + >>> import dpnp as np + >>> x = np.array([[1], [2], [3]]) + >>> y = np.array([4, 5, 6]) + >>> np.broadcast(x, y).shape + (3, 3) + + """ + 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. + + Examples + -------- + >>> import dpnp as np + >>> x = np.array([[1], [2], [3]]) + >>> y = np.array([4, 5, 6]) + >>> np.broadcast(x, y).size + 9 + + """ + return self._size + + @property + def ndim(self): + """ + Number of dimensions of the broadcasted result. + + Returns + ------- + out : int + The number of dimensions of the broadcasted result. + + Examples + -------- + >>> import dpnp as np + >>> x = np.array([[1], [2], [3]]) + >>> y = np.array([4, 5, 6]) + >>> np.broadcast(x, y).ndim + 2 + + """ + return self._ndim + + @property + def numiter(self): + """ + Number of iterators possessed by the broadcast object. + + Returns + ------- + out : int + The number of iterators. + + Examples + -------- + >>> import dpnp as np + >>> x = np.array([[1], [2], [3]]) + >>> y = np.array([4, 5, 6]) + >>> np.broadcast(x, y).numiter + 2 + + """ + return len(self._arrays) + + @property + def values(self): + """ + The input arrays broadcast against one another. + + Returns + ------- + out : tuple of dpnp.ndarray + A tuple of arrays which are views on the original input arrays. + + Examples + -------- + >>> import dpnp as np + >>> x = np.array([[1], [2], [3]]) + >>> y = np.array([4, 5, 6]) + >>> b = np.broadcast(x, y) + >>> b.values[0] + array([[1, 1, 1], + [2, 2, 2], + [3, 3, 3]]) + >>> b.values[1] + array([[4, 5, 6], + [4, 5, 6], + [4, 5, 6]]) + + """ + if self._values is None: + self._values = tuple( + broadcast_to(a, self._shape) for a in self._arrays + ) + return self._values + + def __repr__(self): + return ( + f"" + ) + + def broadcast_arrays(*args, subok=False): """ Broadcast any number of arrays against each other. @@ -1070,6 +1258,9 @@ def broadcast_arrays(*args, subok=False): See Also -------- + :obj:`dpnp.broadcast` : Produce an object that mimics broadcasting. + :obj:`dpnp.broadcast_shapes` : Broadcast the input shapes into a single + shape. :obj:`dpnp.broadcast_to` : Broadcast an array to a new shape. Examples @@ -1112,6 +1303,7 @@ def broadcast_shapes(*args): See Also -------- + :obj:`dpnp.broadcast` : Produce an object that mimics broadcasting. :obj:`dpnp.broadcast_arrays` : Broadcast any number of arrays against each other. :obj:`dpnp.broadcast_to` : Broadcast an array to a new shape. @@ -1157,8 +1349,11 @@ def broadcast_to(array, /, shape, subok=False): See Also -------- + :obj:`dpnp.broadcast` : Produce an object that mimics broadcasting. :obj:`dpnp.broadcast_arrays` : Broadcast any number of arrays against each other. + :obj:`dpnp.broadcast_shapes` : Broadcast the input shapes into a single + shape. Examples -------- diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index 94ad99cc5961..5b1224bd8e60 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -2000,7 +2000,7 @@ def test_broadcast_basic(self): b_np = numpy.broadcast(x.asnumpy(), y.asnumpy()) assert b.shape == b_np.shape - assert b.nd == b_np.nd + assert b.ndim == b_np.ndim assert b.size == b_np.size assert b.numiter == b_np.numiter @@ -2013,7 +2013,7 @@ def test_broadcast_scalar(self): b_np = numpy.broadcast(a.asnumpy(), s.asnumpy()) assert b.shape == b_np.shape - assert b.nd == b_np.nd + assert b.ndim == b_np.ndim assert b.size == b_np.size def test_broadcast_multiple_arrays(self): @@ -2025,7 +2025,7 @@ def test_broadcast_multiple_arrays(self): b_np = numpy.broadcast(a1.asnumpy(), a2.asnumpy()) assert b.shape == b_np.shape - assert b.nd == b_np.nd + assert b.ndim == b_np.ndim assert b.size == b_np.size def test_broadcast_same_shape(self): @@ -2037,7 +2037,7 @@ def test_broadcast_same_shape(self): bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy()) assert bc.shape == bc_np.shape - assert bc.nd == bc_np.nd + assert bc.ndim == bc_np.ndim assert bc.size == bc_np.size def test_broadcast_0d_arrays(self): @@ -2049,7 +2049,7 @@ def test_broadcast_0d_arrays(self): bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy()) assert bc.shape == bc_np.shape - assert bc.nd == bc_np.nd + assert bc.ndim == bc_np.ndim assert bc.size == bc_np.size def test_broadcast_empty_arrays(self): @@ -2061,7 +2061,7 @@ def test_broadcast_empty_arrays(self): bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy()) assert bc.shape == bc_np.shape - assert bc.nd == bc_np.nd + assert bc.ndim == bc_np.ndim assert bc.size == bc_np.size def test_broadcast_incompatible_shapes(self): @@ -2090,12 +2090,13 @@ def test_broadcast_three_arrays(self): bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy(), c.asnumpy()) assert bc.shape == bc_np.shape - assert bc.nd == bc_np.nd + assert bc.ndim == bc_np.ndim assert bc.size == bc_np.size assert bc.numiter == 3 def test_broadcast_ndim_property(self): - # Test that ndim property matches numpy and equals nd property + # unlike numpy, only ndim is exposed, because numpy itself states that + # the more consistent ndim is preferred over the legacy nd attribute a = dpnp.array([[1, 2], [3, 4]]) b = dpnp.array([5, 6]) @@ -2103,7 +2104,28 @@ def test_broadcast_ndim_property(self): bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy()) assert bc.ndim == bc_np.ndim - assert bc.ndim == bc.nd + assert not hasattr(bc, "nd") + + def test_broadcast_values_property(self): + # values mimics cupy.broadcast.values and holds the input arrays + # broadcast against one another + a = dpnp.array([[1], [2], [3]]) + b = dpnp.array([4, 5, 6]) + + bc = dpnp.broadcast(a, b) + expected = dpnp.broadcast_arrays(a, b) + + assert isinstance(bc.values, tuple) + # the property is evaluated once and then cached + assert bc.values is bc.values + + assert len(bc.values) == len(expected) + for res, exp in zip(bc.values, expected): + assert res.shape == bc.shape + assert_array_equal(res, exp) + + def test_broadcast_values_no_args(self): + assert dpnp.broadcast().values == () def test_broadcast_complex_shapes(self): # Test broadcast with complex compatible shapes @@ -2115,7 +2137,7 @@ def test_broadcast_complex_shapes(self): bc_np = numpy.broadcast(a.asnumpy(), b.asnumpy(), c.asnumpy()) assert bc.shape == bc_np.shape - assert bc.nd == bc_np.nd + assert bc.ndim == bc_np.ndim assert bc.size == bc_np.size @pytest.mark.parametrize( @@ -2154,9 +2176,17 @@ def test_broadcast_parametrized_shapes(self, shapes): bc_np = numpy.broadcast(*arrays_np) assert bc.shape == bc_np.shape - assert bc.nd == bc_np.nd + assert bc.ndim == bc_np.ndim assert bc.size == bc_np.size + # numpy.broadcast has no counterpart of the values property, so the + # broadcasted arrays are compared against numpy.broadcast_arrays + expected = numpy.broadcast_arrays(*arrays_np) + assert len(bc.values) == len(expected) + for res, exp in zip(bc.values, expected): + assert res.shape == exp.shape + assert_array_equal(res, exp) + def test_broadcast_single_array(self): # Test broadcast with a single array a = dpnp.array([[1, 2], [3, 4]]) @@ -2165,7 +2195,7 @@ def test_broadcast_single_array(self): bc_np = numpy.broadcast(a.asnumpy()) assert bc.shape == bc_np.shape - assert bc.nd == bc_np.nd + assert bc.ndim == bc_np.ndim assert bc.size == bc_np.size assert bc.numiter == 1 @@ -2174,7 +2204,6 @@ def test_broadcast_no_args(self): bc = dpnp.broadcast() assert bc.shape == () - assert bc.nd == 0 assert bc.ndim == 0 assert bc.size == 1 assert bc.numiter == 0 @@ -2192,7 +2221,11 @@ def test_broadcast_different_queues(self): assert bc.shape == (2, 2) assert bc.size == 4 - assert bc.nd == 2 + assert bc.ndim == 2 + + # each broadcasted array stays on the queue of its input array + assert bc.values[0].sycl_queue == q1 + assert bc.values[1].sycl_queue == q2 def test_broadcast_repr(self): # Test __repr__ method @@ -2205,3 +2238,5 @@ def test_broadcast_repr(self): assert "broadcast" in repr_str assert "shape" in repr_str assert str(bc.shape) in repr_str + assert f"ndim={bc.ndim}" in repr_str + assert f"size={bc.size}" in repr_str diff --git a/dpnp/tests/third_party/cupy/manipulation_tests/test_dims.py b/dpnp/tests/third_party/cupy/manipulation_tests/test_dims.py index 9ee219630384..5b9c7ce2e495 100644 --- a/dpnp/tests/third_party/cupy/manipulation_tests/test_dims.py +++ b/dpnp/tests/third_party/cupy/manipulation_tests/test_dims.py @@ -306,7 +306,8 @@ def test_broadcast(self, dtype): broadcast_cp = self._broadcast(cupy, dtype, self.shapes) assert broadcast_np.shape == broadcast_cp.shape assert broadcast_np.size == broadcast_cp.size - assert broadcast_np.nd == broadcast_cp.nd + # `nd` is not exposed by dpnp, since NumPy prefers `ndim` over it + assert broadcast_np.ndim == broadcast_cp.ndim @testing.for_all_dtypes() @testing.numpy_cupy_array_equal() From 4115c7719f071777b73312708ecb67be61c9bbeb Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 19 Aug 2026 15:09:37 +0200 Subject: [PATCH 6/8] docs: document broadcast attributes on the class page Add an Attributes section to the dpnp.broadcast class docstring so the attributes (shape, size, ndim, numiter, values) render on the class page the way numpy/cupy present them, and drop the now-redundant per-attribute autosummary entries from the reference. Also document broadcast_arrays' variadic parameter as *args for consistency with broadcast/broadcast_shapes. --- doc/reference/array-manipulation.rst | 5 ----- dpnp/dpnp_iface_manipulation.py | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/doc/reference/array-manipulation.rst b/doc/reference/array-manipulation.rst index 3bec89738edf..0490119dd295 100644 --- a/doc/reference/array-manipulation.rst +++ b/doc/reference/array-manipulation.rst @@ -58,11 +58,6 @@ Changing number of dimensions atleast_2d atleast_3d broadcast - broadcast.shape - broadcast.size - broadcast.ndim - broadcast.numiter - broadcast.values broadcast_to broadcast_arrays expand_dims diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 7802c1277800..349043368e95 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1059,6 +1059,19 @@ class broadcast: # pylint: disable=invalid-name *args : {dpnp.ndarray, usm_ndarray} Input arrays to broadcast against one another. + Attributes + ---------- + shape : tuple of ints + Shape of the broadcasted result. + size : int + Total size (number of elements) of the broadcasted result. + ndim : int + Number of dimensions of the broadcasted result. + numiter : int + Number of iterators possessed by the broadcast object. + values : tuple of dpnp.ndarray + The input arrays broadcast against one another. + Returns ------- broadcast : broadcast object @@ -1243,7 +1256,7 @@ def broadcast_arrays(*args, subok=False): Parameters ---------- - args : {dpnp.ndarray, usm_ndarray} + *args : {dpnp.ndarray, usm_ndarray} A list of arrays to broadcast. Returns From 316ba4c5550d421ba9bf29f0a38b66835976f5c6 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 19 Aug 2026 16:26:16 +0200 Subject: [PATCH 7/8] docs: render broadcast attributes under an Attributes heading Use a dedicated autosummary template for the broadcast class that keeps the Attributes rubric visible (instead of the default template which hides member tables), so shape/size/ndim/numiter/values render under an "Attributes" heading like numpy/cupy, rather than the "Variables" label produced by a napoleon Attributes docstring section. Also add "numiter" to the docs spell-check word list. --- .../autosummary/class_with_attributes.rst | 28 +++++++++++++++++++ doc/known_words.txt | 1 + doc/reference/array-manipulation.rst | 8 +++++- dpnp/dpnp_iface_manipulation.py | 13 --------- 4 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 doc/_templates/autosummary/class_with_attributes.rst diff --git a/doc/_templates/autosummary/class_with_attributes.rst b/doc/_templates/autosummary/class_with_attributes.rst new file mode 100644 index 000000000000..735054ff8260 --- /dev/null +++ b/doc/_templates/autosummary/class_with_attributes.rst @@ -0,0 +1,28 @@ +{% extends "!autosummary/class.rst" %} + +{% block methods %} +{% if methods %} + .. HACK -- the point here is that we don't want this to appear in the output, but the autosummary should still generate the pages. + .. autosummary:: + :toctree: + {% for item in all_methods %} + {%- if not item.startswith('_') or item in ['__call__'] %} + {{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} +{% endif %} +{% endblock %} + +{% block attributes %} +{% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + :toctree: + {% for item in all_attributes %} + {%- if not item.startswith('_') %} + {{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} +{% endif %} +{% endblock %} diff --git a/doc/known_words.txt b/doc/known_words.txt index 7de17047c721..b8f0ec87e530 100644 --- a/doc/known_words.txt +++ b/doc/known_words.txt @@ -69,6 +69,7 @@ Nj Nk normed nuc +numiter numpy nx ny diff --git a/doc/reference/array-manipulation.rst b/doc/reference/array-manipulation.rst index 0490119dd295..00bffda6b63d 100644 --- a/doc/reference/array-manipulation.rst +++ b/doc/reference/array-manipulation.rst @@ -50,6 +50,13 @@ Transpose-like operations Changing number of dimensions ----------------------------- +.. autosummary:: + :toctree: generated/ + :nosignatures: + :template: autosummary/class_with_attributes.rst + + broadcast + .. autosummary:: :toctree: generated/ :nosignatures: @@ -57,7 +64,6 @@ Changing number of dimensions atleast_1d atleast_2d atleast_3d - broadcast broadcast_to broadcast_arrays expand_dims diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 349043368e95..7674f84f77b9 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1059,19 +1059,6 @@ class broadcast: # pylint: disable=invalid-name *args : {dpnp.ndarray, usm_ndarray} Input arrays to broadcast against one another. - Attributes - ---------- - shape : tuple of ints - Shape of the broadcasted result. - size : int - Total size (number of elements) of the broadcasted result. - ndim : int - Number of dimensions of the broadcasted result. - numiter : int - Number of iterators possessed by the broadcast object. - values : tuple of dpnp.ndarray - The input arrays broadcast against one another. - Returns ------- broadcast : broadcast object From 7a4fa81f564062e67e5b91157c4d5bed4a3351e0 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 19 Aug 2026 22:58:08 +0200 Subject: [PATCH 8/8] docs: show short attribute names in broadcast Attributes table Prefix the autosummary entries with ~ so the Attributes table lists the attributes by their short names (shape, size, ndim, numiter, values) instead of the fully qualified broadcast. form. --- doc/_templates/autosummary/class_with_attributes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/_templates/autosummary/class_with_attributes.rst b/doc/_templates/autosummary/class_with_attributes.rst index 735054ff8260..2a10a7beb68f 100644 --- a/doc/_templates/autosummary/class_with_attributes.rst +++ b/doc/_templates/autosummary/class_with_attributes.rst @@ -21,7 +21,7 @@ :toctree: {% for item in all_attributes %} {%- if not item.startswith('_') %} - {{ name }}.{{ item }} + ~{{ name }}.{{ item }} {%- endif -%} {%- endfor %} {% endif %}