diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c52a1166c9..1a2dc01b898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ This release is compatible with NumPy 2.5. * Clarified support for negative axes in `dpnp.transpose`/`dpnp.permute_dims` documentation [#2940](https://github.com/IntelPython/dpnp/pull/2940) * Allowed `dpnp.take` and `dpnp.compress` to cast the result into an `out` array of a different but same-kind dtype [#2959](https://github.com/IntelPython/dpnp/pull/2959) * Clarified the summary in `dpnp.reshape` and `dpnp.ndarray.reshape` docstrings [#2964](https://github.com/IntelPython/dpnp/pull/2964) +* Changed `dpnp.atleast_1d`, `dpnp.atleast_2d`, `dpnp.atleast_3d`, and `dpnp.ogrid` to return a tuple of arrays instead of a list [#2965](https://github.com/IntelPython/dpnp/pull/2965) ### Deprecated diff --git a/dpnp/dpnp_algo/dpnp_arraycreation.py b/dpnp/dpnp_algo/dpnp_arraycreation.py index 9c9110b8538..6052ad46727 100644 --- a/dpnp/dpnp_algo/dpnp_arraycreation.py +++ b/dpnp/dpnp_algo/dpnp_arraycreation.py @@ -439,4 +439,5 @@ def __getitem__(self, key): slobj[k] = slice(None, None) nn[k] = nn[k][tuple(slobj)] slobj[k] = dpnp.newaxis - return nn + return tuple(nn) # ogrid -> tuple of arrays + return nn # mgrid -> ndarray diff --git a/dpnp/dpnp_iface_arraycreation.py b/dpnp/dpnp_iface_arraycreation.py index 5a245c69037..560ad16e1d5 100644 --- a/dpnp/dpnp_iface_arraycreation.py +++ b/dpnp/dpnp_iface_arraycreation.py @@ -3036,7 +3036,7 @@ def logspace( # pylint: disable=redefined-outer-name def meshgrid(*xi, copy=True, sparse=False, indexing="xy"): """ - Return coordinate matrices from coordinate vectors. + Return a tuple of coordinate matrices from coordinate vectors. Make N-D coordinate arrays for vectorized evaluations of N-D scalar/vector fields over N-D grids, given @@ -3069,6 +3069,13 @@ def meshgrid(*xi, copy=True, sparse=False, indexing="xy"): the elements of `xi` repeated to fill the matrix along the first dimension for `x1`, the second for `x2` and so on. + See Also + -------- + :obj:`dpnp.mgrid` : Construct a multi-dimensional "meshgrid" using + indexing notation. + :obj:`dpnp.ogrid` : Construct an open multi-dimensional "meshgrid" using + indexing notation. + Examples -------- >>> import dpnp as np @@ -3164,9 +3171,9 @@ class MGridClass: Returns ------- - out : one dpnp.ndarray or tuple of dpnp.ndarray - Returns one array of grid indices, - ``grid.shape = (len(dimensions),) + tuple(dimensions)``. + out : dpnp.ndarray + A single array, containing a set of arrays all of the same dimensions, + stacked along the first axis. Examples -------- @@ -3183,6 +3190,13 @@ class MGridClass: [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]]]) + >>> np.mgrid[0:4].shape + (4,) + >>> np.mgrid[0:4, 0:5].shape + (2, 4, 5) + >>> np.mgrid[0:4, 0:5, 0:6].shape + (3, 4, 5, 6) + Creating an array on a different device or with a specified usm_type >>> x = np.mgrid[-1:1:5j] # default case @@ -3243,20 +3257,20 @@ class OGridClass: Returns ------- - out : one dpnp.ndarray or tuple of dpnp.ndarray - Returns a tuple of arrays, - with grid[i].shape = (1, ..., 1, dimensions[i], 1, ..., 1) - with dimensions[i] in the i-th place. + out : dpnp.ndarray or tuple of dpnp.ndarray + If the input is a single slice, returns an array. + If the input is multiple slices, returns a tuple of arrays, with + only one dimension not equal to 1. Examples -------- >>> import dpnp as np >>> np.ogrid[0:5, 0:5] - [array([[0], + (array([[0], [1], [2], [3], - [4]]), array([[0, 1, 2, 3, 4]])] + [4]]), array([[0, 1, 2, 3, 4]])) Creating an array on a different device or with a specified usm_type diff --git a/dpnp/dpnp_iface_histograms.py b/dpnp/dpnp_iface_histograms.py index 7e91968926f..b60ec12cc48 100644 --- a/dpnp/dpnp_iface_histograms.py +++ b/dpnp/dpnp_iface_histograms.py @@ -1151,7 +1151,7 @@ def histogramdd(sample, bins=10, range=None, density=None, weights=None): H : dpnp.ndarray The multidimensional histogram of sample x. See density and weights for the different possible semantics. - edges : list of {dpnp.ndarray or usm_ndarray} + edges : list of dpnp.ndarray A list of D arrays describing the bin edges for each dimension. See Also diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 6ef4400562f..a8b2fc931de 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -881,7 +881,7 @@ def atleast_1d(*arys): Returns ------- out : dpnp.ndarray - An array, or list of arrays, each with ``a.ndim >= 1``. + An array, or tuple of arrays, each with ``a.ndim >= 1``. Copies are made only if necessary. See Also @@ -899,7 +899,7 @@ def atleast_1d(*arys): >>> y = np.array([3, 4]) >>> np.atleast_1d(x, y) - [array([1.]), array([3, 4])] + (array([1.]), array([3, 4])) >>> x = np.arange(9.0).reshape(3, 3) >>> np.atleast_1d(x) @@ -926,7 +926,7 @@ def atleast_1d(*arys): res.append(result) if len(res) == 1: return res[0] - return res + return tuple(res) def atleast_2d(*arys): @@ -944,7 +944,7 @@ def atleast_2d(*arys): Returns ------- out : dpnp.ndarray - An array, or list of arrays, each with ``a.ndim >= 2``. + An array, or tuple of arrays, each with ``a.ndim >= 2``. Copies are avoided where possible, and views with two or more dimensions are returned. @@ -982,7 +982,7 @@ def atleast_2d(*arys): res.append(result) if len(res) == 1: return res[0] - return res + return tuple(res) def atleast_3d(*arys): @@ -1000,7 +1000,7 @@ def atleast_3d(*arys): Returns ------- out : dpnp.ndarray - An array, or list of arrays, each with ``a.ndim >= 3``. Copies are + An array, or tuple of arrays, each with ``a.ndim >= 3``. Copies are avoided where possible, and views with three or more dimensions are returned. @@ -1044,7 +1044,7 @@ def atleast_3d(*arys): res.append(result) if len(res) == 1: return res[0] - return res + return tuple(res) def broadcast_arrays(*args, subok=False): @@ -1753,8 +1753,8 @@ def dstack(tup): _check_stack_arrays(tup) arrs = atleast_3d(*tup) - if not isinstance(arrs, list): - arrs = [arrs] + if not isinstance(arrs, tuple): + arrs = (arrs,) return dpnp.concatenate(arrs, axis=2) @@ -2178,8 +2178,8 @@ def hstack(tup, *, dtype=None, casting="same_kind"): _check_stack_arrays(tup) arrs = dpnp.atleast_1d(*tup) - if not isinstance(arrs, list): - arrs = [arrs] + if not isinstance(arrs, tuple): + arrs = (arrs,) # As a special case, dimension 0 of 1-dimensional arrays is "horizontal" if arrs and arrs[0].ndim == 1: @@ -4664,6 +4664,6 @@ def vstack(tup, *, dtype=None, casting="same_kind"): _check_stack_arrays(tup) arrs = dpnp.atleast_2d(*tup) - if not isinstance(arrs, list): - arrs = [arrs] + if not isinstance(arrs, tuple): + arrs = (arrs,) return dpnp.concatenate(arrs, axis=0, dtype=dtype, casting=casting) diff --git a/dpnp/tests/test_arraycreation.py b/dpnp/tests/test_arraycreation.py index 5a9a1c73c6e..d6e72672636 100644 --- a/dpnp/tests/test_arraycreation.py +++ b/dpnp/tests/test_arraycreation.py @@ -1050,13 +1050,19 @@ def test_meshgrid_raise_error(): dpnp.meshgrid(b, indexing="ab") -class TestMgrid: +@pytest.mark.parametrize("grid", ["mgrid", "ogrid"]) +class TestGrid: def check_results(self, result, expected): - if isinstance(result, (list, tuple)): + # mgrid always returns a single array; ogrid returns a single array for + # a single slice and a tuple of arrays for multiple slices. In every + # case the container kind (tuple vs. bare array) must match NumPy. + if isinstance(expected, tuple): + assert isinstance(result, tuple) assert len(result) == len(expected) for dp_arr, np_arr in zip(result, expected): assert_allclose(dp_arr, np_arr) else: + assert not isinstance(result, tuple) assert_allclose(result, expected) @pytest.mark.parametrize( @@ -1069,9 +1075,9 @@ def check_results(self, result, expected): slice(0, 5, None), # no step ], ) - def test_single_slice(self, slice): - dpnp_result = dpnp.mgrid[slice] - numpy_result = numpy.mgrid[slice] + def test_single_slice(self, grid, slice): + dpnp_result = getattr(dpnp, grid)[slice] + numpy_result = getattr(numpy, grid)[slice] self.check_results(dpnp_result, numpy_result) @pytest.mark.parametrize( @@ -1086,9 +1092,9 @@ def test_single_slice(self, slice): ), # float start and complex step ], ) - def test_md_slice(self, slices): - dpnp_result = dpnp.mgrid[slices] - numpy_result = numpy.mgrid[slices] + def test_md_slice(self, grid, slices): + dpnp_result = getattr(dpnp, grid)[slices] + numpy_result = getattr(numpy, grid)[slices] self.check_results(dpnp_result, numpy_result) diff --git a/dpnp/tests/test_arraymanipulation.py b/dpnp/tests/test_arraymanipulation.py index 25c454b9761..d1587b9a50c 100644 --- a/dpnp/tests/test_arraymanipulation.py +++ b/dpnp/tests/test_arraymanipulation.py @@ -78,7 +78,9 @@ def test_dpnp_dpt_array(self): a = dpnp.array([1, 2]) b = dpt.asarray([2, 3]) res = dpnp.atleast_1d(a, b) - desired = [dpnp.array([1, 2]), dpnp.array([2, 3])] + desired = numpy.atleast_1d(numpy.array([1, 2]), numpy.array([2, 3])) + # multiple inputs are returned as a tuple of arrays, matching NumPy + assert type(res) is type(desired) is tuple assert_array_equal(res, desired) @@ -117,7 +119,9 @@ def test_dpnp_dpt_array(self): a = dpnp.array([1, 2]) b = dpt.asarray([2, 3]) res = dpnp.atleast_2d(a, b) - desired = [dpnp.array([[1, 2]]), dpnp.array([[2, 3]])] + desired = numpy.atleast_2d(numpy.array([1, 2]), numpy.array([2, 3])) + # multiple inputs are returned as a tuple of arrays, matching NumPy + assert type(res) is type(desired) is tuple assert_array_equal(res, desired) @@ -156,7 +160,9 @@ def test_dpnp_dpt_array(self): a = dpnp.array([1, 2]) b = dpt.asarray([2, 3]) res = dpnp.atleast_3d(a, b) - desired = [dpnp.array([[[1], [2]]]), dpnp.array([[[2], [3]]])] + desired = numpy.atleast_3d(numpy.array([1, 2]), numpy.array([2, 3])) + # multiple inputs are returned as a tuple of arrays, matching NumPy + assert type(res) is type(desired) is tuple assert_array_equal(res, desired) @@ -1067,6 +1073,8 @@ def test_1d_array(self, dtype): np_res = numpy.unstack(np_a) dp_res = dpnp.unstack(dp_a) + # the result is a tuple of arrays, matching NumPy + assert type(dp_res) is type(np_res) is tuple assert len(dp_res) == len(np_res) for dp_arr, np_arr in zip(dp_res, np_res): assert_array_equal(dp_arr, np_arr) diff --git a/dpnp/tests/test_histogram.py b/dpnp/tests/test_histogram.py index b0988609d2c..9fbf18bdc32 100644 --- a/dpnp/tests/test_histogram.py +++ b/dpnp/tests/test_histogram.py @@ -738,6 +738,9 @@ def test_bins(self, bins): expected_hist, expected_edges = numpy.histogramdd(v, bins) result_hist, result_edges = dpnp.histogramdd(iv, bins_dpnp) assert_allclose(result_hist, expected_hist) + + assert isinstance(result_edges, list) + assert isinstance(expected_edges, list) for x, y in zip(result_edges, expected_edges): assert_allclose(x, y) diff --git a/dpnp/tests/test_indexing.py b/dpnp/tests/test_indexing.py index b7dc5f791b7..84bf62d0356 100644 --- a/dpnp/tests/test_indexing.py +++ b/dpnp/tests/test_indexing.py @@ -480,6 +480,7 @@ def test_ix(self, x0, x1): expected = dpnp.ix_(dpnp.array(x0), dpnp.array(x1)) result = numpy.ix_(numpy.array(x0), numpy.array(x1)) + assert type(expected) is type(result) is tuple assert_array_equal(result[0], expected[0]) assert_array_equal(result[1], expected[1]) @@ -571,6 +572,7 @@ def test_1d(self, dtype): np_res = numpy.nonzero(a) dpnp_res = dpnp.nonzero(ia) + assert type(dpnp_res) is type(np_res) is tuple assert_array_equal(np_res, dpnp_res) @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) @@ -1295,6 +1297,7 @@ def test_putmask3(arr, mask, vals): def test_tril_indices(n, k, m): result = dpnp.tril_indices(n, k, m) expected = numpy.tril_indices(n, k, m) + assert type(result) is type(expected) is tuple assert_array_equal(expected, result) @@ -1322,6 +1325,7 @@ def test_tril_indices_from(array, k): def test_triu_indices(n, k, m): result = dpnp.triu_indices(n, k, m) expected = numpy.triu_indices(n, k, m) + assert type(result) is type(expected) is tuple assert_array_equal(expected, result) @@ -1451,6 +1455,7 @@ def test_basic(self): expected = numpy.unravel_index(x_np, (7, 6)) result = dpnp.unravel_index(x_dp, (7, 6)) + assert type(result) is type(expected) is tuple assert_equal(expected, result) def test_order_f(self): diff --git a/dpnp/tests/test_manipulation.py b/dpnp/tests/test_manipulation.py index d09f19f1d77..9eedcfe275d 100644 --- a/dpnp/tests/test_manipulation.py +++ b/dpnp/tests/test_manipulation.py @@ -28,7 +28,9 @@ def _compare_results(result, expected): - """Compare lists of arrays.""" + """Compare lists of arrays returned by the split family of functions.""" + # split/array_split/{h,v,d}split return a list of arrays + assert type(result) is type(expected) is list if len(result) != len(expected): raise ValueError("Iterables have different lengths") diff --git a/dpnp/tests/test_search.py b/dpnp/tests/test_search.py index 493c1edce2d..4c6e3fd6595 100644 --- a/dpnp/tests/test_search.py +++ b/dpnp/tests/test_search.py @@ -320,8 +320,12 @@ def test_empty_result(self): a = numpy.zeros((1, 1)) ia = dpnp.array(a) - expected = numpy.vstack(numpy.where(a == 99.0)) - result = dpnp.vstack(dpnp.where(ia == 99.0)) + np_res = numpy.where(a == 99.0) + dp_res = dpnp.where(ia == 99.0) + assert type(dp_res) is type(np_res) is tuple + + expected = numpy.vstack(np_res) + result = dpnp.vstack(dp_res) assert_array_equal(result, expected) @testing.with_requires("numpy>=2.5")