diff --git a/CHANGELOG.md b/CHANGELOG.md index 225b27b1a523..f8a79b628491 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.21.0] - 2026-MM-DD -This release is compatible with NumPy 2.4.5. +This release is compatible with NumPy 2.5. ### Added @@ -27,6 +27,8 @@ This release is compatible with NumPy 2.4.5. ### Removed +* Removed support for arrays of 2-dimensional vectors in `dpnp.cross`, which now requires (arrays of) 3-dimensional vectors and raises `ValueError` otherwise [#2950](https://github.com/IntelPython/dpnp/pull/2950) + ### Fixed * Fixed incorrect in-place advanced indexing for 4D arrays when using `range` or `list` as index keys [#2872](https://github.com/IntelPython/dpnp/pull/2872) diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index ddecd1f751d9..f68fa68f3a63 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -45,7 +45,6 @@ import builtins -import warnings import dpctl.utils as dpu import numpy @@ -874,11 +873,8 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None): The cross product of `a` and `b` in :math:`R^3` is a vector perpendicular to both `a` and `b`. If `a` and `b` are arrays of vectors, the vectors - are defined by the last axis of `a` and `b` by default, and these axes - can have dimensions 2 or 3. Where the dimension of either `a` or `b` is - 2, the third component of the input vector is assumed to be zero and the - cross product calculated accordingly. In cases where both input vectors - have dimension 2, the z-component of the cross product is returned. + are defined by the last axis of `a` and `b` by default, and these axes must + have 3 dimensions. For full documentation refer to :obj:`numpy.cross`. @@ -897,8 +893,7 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None): Default: ``-1``. axisc : int, optional - Axis of `c` containing the cross product vector(s). Ignored if both - input vectors have dimension ``2``, as the return is scalar. By default, + Axis of `c` containing the cross product vector(s). By default, the last axis. Default: ``-1``. @@ -913,9 +908,14 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None): out : dpnp.ndarray Vector cross product(s). + Raises + ------ + ValueError + When the dimension of the vector(s) in `a` or `b` does not equal 3. + See Also -------- - :obj:`dpnp.linalg.cross` : Array API compatible version. + :obj:`dpnp.linalg.cross` : Array API compatible variation. :obj:`dpnp.inner` : Inner product. :obj:`dpnp.outer` : Outer product. @@ -929,27 +929,6 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None): >>> np.cross(x, y) array([-3, 6, -3]) - One vector with dimension 2. - - >>> x = np.array([1, 2]) - >>> y = np.array([4, 5, 6]) - >>> np.cross(x, y) - array([12, -6, -3]) - - Equivalently: - - >>> x = np.array([1, 2, 0]) - >>> y = np.array([4, 5, 6]) - >>> np.cross(x, y) - array([12, -6, -3]) - - Both vectors with dimension 2. - - >>> x = np.array([1, 2]) - >>> y = np.array([4, 5]) - >>> np.cross(x, y) - array(-3) - Multiple vector cross-products. Note that the direction of the cross product vector is defined by the *right-hand rule*. @@ -992,6 +971,9 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None): "Input arrays with boolean data type are not supported." ) + if (a.ndim < 1) or (b.ndim < 1): + raise ValueError("At least one array has zero dimension") + # Check axisa and axisb are within bounds axisa = normalize_axis_index(axisa, a.ndim, msg_prefix="axisa") axisb = normalize_axis_index(axisb, b.ndim, msg_prefix="axisb") @@ -999,36 +981,18 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None): # Move working axis to the end of the shape a = dpnp.moveaxis(a, axisa, -1) b = dpnp.moveaxis(b, axisb, -1) - if a.shape[-1] not in (2, 3) or b.shape[-1] not in (2, 3): - raise ValueError( - "Incompatible vector dimensions for cross product\n" - "(the dimension of vector used in cross product must be 2 or 3)" - ) - - if a.shape[-1] == 2 or b.shape[-1] == 2: - warnings.warn( - "Arrays of 2-dimensional vectors are deprecated. Use arrays of " - "3-dimensional vectors instead. (deprecated in dpnp 0.17.0)", - DeprecationWarning, - stacklevel=2, - ) - # Modify the shape of input arrays if necessary a_shape = a.shape b_shape = b.shape + if a_shape[-1] != 3 or b_shape[-1] != 3: + raise ValueError( + "Both input arrays must be (arrays of) 3-dimensional vectors, " + f"but they are {a_shape[-1]} and {b_shape[-1]} dimensional instead." + ) - res_shape = dpnp.broadcast_shapes(a_shape[:-1], b_shape[:-1]) - if a_shape[:-1] != res_shape: - a = dpnp.broadcast_to(a, res_shape + (a_shape[-1],)) - a_shape = a.shape - if b_shape[:-1] != res_shape: - b = dpnp.broadcast_to(b, res_shape + (b_shape[-1],)) - b_shape = b.shape - - if a_shape[-1] == 3 or b_shape[-1] == 3: - res_shape += (3,) - # Check axisc is within bounds - axisc = normalize_axis_index(axisc, len(res_shape), msg_prefix="axisc") + # Check axisc is within bounds + res_shape = *dpnp.broadcast_shapes(a_shape[:-1], b_shape[:-1]), 3 + axisc = normalize_axis_index(axisc, len(res_shape), msg_prefix="axisc") # Create the output array dtype = dpnp.result_type(a, b) @@ -1042,9 +1006,6 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None): b = b.astype(dtype, copy=False) cp = dpnp_cross(a, b, cp) - if a_shape[-1] == 2 and b_shape[-1] == 2: - return cp - return dpnp.moveaxis(cp, -1, axisc) diff --git a/dpnp/dpnp_utils/dpnp_utils_linearalgebra.py b/dpnp/dpnp_utils/dpnp_utils_linearalgebra.py index 2331eb7a10cc..c3070b8b25a6 100644 --- a/dpnp/dpnp_utils/dpnp_utils_linearalgebra.py +++ b/dpnp/dpnp_utils/dpnp_utils_linearalgebra.py @@ -700,67 +700,33 @@ def _validate_out_array(out, exec_q): def dpnp_cross(a, b, cp): - """Return the cross product of two (arrays of) vectors.""" + """Return the cross product of two (arrays of) 3-dimensional vectors.""" # create local aliases for readability a0 = a[..., 0] a1 = a[..., 1] - if a.shape[-1] == 3: - a2 = a[..., 2] + a2 = a[..., 2] b0 = b[..., 0] b1 = b[..., 1] - if b.shape[-1] == 3: - b2 = b[..., 2] - - if cp.ndim != 0 and cp.shape[-1] == 3: - cp0 = cp[..., 0] - cp1 = cp[..., 1] - cp2 = cp[..., 2] - - if a.shape[-1] == 2: - if b.shape[-1] == 2: - # a0 * b1 - a1 * b0 - cp = dpnp.multiply(a0, b1, out=cp) - cp -= a1 * b0 - else: - assert b.shape[-1] == 3 - # cp0 = a1 * b2 - 0 (a2 = 0) - cp0 = dpnp.multiply(a1, b2, out=cp0) + b2 = b[..., 2] - # cp1 = 0 - a0 * b2 (a2 = 0) - cp1 = dpnp.multiply(a0, b2, out=cp1) - cp1 = dpnp.negative(cp1, out=cp1) + cp0 = cp[..., 0] + cp1 = cp[..., 1] + cp2 = cp[..., 2] - # cp2 = a0 * b1 - a1 * b0 - cp2 = dpnp.multiply(a0, b1, out=cp2) - cp2 -= a1 * b0 - else: - assert a.shape[-1] == 3 - if b.shape[-1] == 3: - # cp0 = a1 * b2 - a2 * b1 - cp0 = dpnp.multiply(a1, b2, out=cp0) - cp0 -= a2 * b1 - - # cp1 = a2 * b0 - a0 * b2 - cp1 = dpnp.multiply(a2, b0, out=cp1) - cp1 -= a0 * b2 - - # cp2 = a0 * b1 - a1 * b0 - cp2 = dpnp.multiply(a0, b1, out=cp2) - cp2 -= a1 * b0 - else: - assert b.shape[-1] == 2 - # cp0 = 0 - a2 * b1 (b2 = 0) - cp0 = dpnp.multiply(a2, b1, out=cp0) - cp0 = dpnp.negative(cp0, out=cp0) + # cp0 = a1 * b2 - a2 * b1 + cp0 = dpnp.multiply(a1, b2, out=cp0) + cp0 -= a2 * b1 + + # cp1 = a2 * b0 - a0 * b2 + cp1 = dpnp.multiply(a2, b0, out=cp1) + cp1 -= a0 * b2 - # cp1 = a2 * b0 - 0 (b2 = 0) - cp1 = dpnp.multiply(a2, b0, out=cp1) + # cp2 = a0 * b1 - a1 * b0 + cp2 = dpnp.multiply(a0, b1, out=cp2) + cp2 -= a1 * b0 - # cp2 = a0 * b1 - a1 * b0 - cp2 = dpnp.multiply(a0, b1, out=cp2) - cp2 -= a1 * b0 return cp diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index 625d387667ac..9923971577a8 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -309,14 +309,6 @@ def cross(x1, x2, /, *, axis=-1): """ - dpnp.check_supported_arrays_type(x1, x2) - if x1.shape[axis] != 3 or x2.shape[axis] != 3: - raise ValueError( - "Both input arrays must be (arrays of) 3-dimensional vectors, " - f"but they are {x1.shape[axis]} and {x2.shape[axis]} " - "dimensional instead." - ) - return dpnp.cross(x1, x2, axis=axis) diff --git a/dpnp/tests/test_product.py b/dpnp/tests/test_product.py index ebf9b640d8c4..9d43b6453e0b 100644 --- a/dpnp/tests/test_product.py +++ b/dpnp/tests/test_product.py @@ -1,7 +1,12 @@ import dpctl import numpy import pytest -from numpy.testing import assert_allclose, assert_array_equal, assert_raises +from numpy.testing import ( + assert_allclose, + assert_array_equal, + assert_raises, + assert_raises_regex, +) import dpnp from dpnp.dpnp_utils import map_dtype_to_device @@ -23,6 +28,8 @@ class TestCross: + ALL_DTYPES_NO_BOOL = get_all_dtypes(no_none=True, no_bool=True) + @pytest.mark.parametrize("axis", [None, 0]) @pytest.mark.parametrize("axisc", [-1, 0]) @pytest.mark.parametrize("axisb", [-1, 0]) @@ -48,14 +55,10 @@ def test_3x3(self, x1, x2, axisa, axisb, axisc, axis): expected = numpy.cross(np_x1, np_x2, axisa, axisb, axisc, axis) assert_dtype_allclose(result, expected) - @pytest.mark.filterwarnings("ignore::DeprecationWarning") - @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("dtype", ALL_DTYPES_NO_BOOL) @pytest.mark.parametrize( "shape1, shape2, axis_a, axis_b, axis_c", [ - ((4, 2, 3, 5), (2, 4, 3, 5), 1, 0, -2), - ((2, 2, 4, 5), (2, 4, 3, 5), 1, 2, -1), - ((2, 3, 4, 5), (2, 4, 2, 5), 1, 2, -1), ((2, 3, 4, 5), (2, 4, 3, 5), 1, 2, -1), ((2, 3, 4, 5), (2, 4, 3, 5), -3, -2, 0), ], @@ -70,18 +73,17 @@ def test_basic(self, dtype, shape1, shape2, axis_a, axis_b, axis_c): expected = numpy.cross(a, b, axis_a, axis_b, axis_c) assert_dtype_allclose(result, expected) - @pytest.mark.filterwarnings("ignore::DeprecationWarning") - @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("dtype", ALL_DTYPES_NO_BOOL) @pytest.mark.parametrize( "shape1, shape2, axis", [ - ((2, 3, 4, 5), (2, 3, 4, 5), 0), + ((3, 2, 4, 5), (3, 2, 4, 5), 0), ((2, 3, 4, 5), (2, 3, 4, 5), 1), ], ) def test_axis(self, dtype, shape1, shape2, axis): - a = generate_random_numpy_array(shape1, dtype) - b = generate_random_numpy_array(shape2, dtype) + a = generate_random_numpy_array(shape1, dtype, seed_value=1) + b = generate_random_numpy_array(shape2, dtype, seed_value=2) ia = dpnp.array(a) ib = dpnp.array(b) @@ -89,11 +91,12 @@ def test_axis(self, dtype, shape1, shape2, axis): expected = numpy.cross(a, b, axis=axis) assert_dtype_allclose(result, expected, factor=24) - @pytest.mark.parametrize("dtype1", get_all_dtypes()) - @pytest.mark.parametrize("dtype2", get_all_dtypes()) + @pytest.mark.parametrize("dtype1", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize("dtype2", get_all_dtypes(no_none=True)) def test_input_dtype_matrix(self, dtype1, dtype2): if dtype1 == dpnp.bool and dtype2 == dpnp.bool: - pytest.skip("boolean input arrays is not supported.") + pytest.skip("boolean input arrays is not supported") + a = generate_random_numpy_array(3, dtype1) b = generate_random_numpy_array(3, dtype2) ia = dpnp.array(a) @@ -103,14 +106,10 @@ def test_input_dtype_matrix(self, dtype1, dtype2): expected = numpy.cross(a, b) assert_dtype_allclose(result, expected, factor=24) - @pytest.mark.filterwarnings("ignore::DeprecationWarning") - @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("dtype", ALL_DTYPES_NO_BOOL) @pytest.mark.parametrize( "shape1, shape2, axis_a, axis_b, axis_c", [ - ((4, 2, 1, 5), (2, 4, 3, 5), 1, 0, -2), - ((2, 2, 4, 5), (2, 4, 3, 1), 1, 2, -1), - ((2, 3, 4, 1), (2, 4, 2, 5), 1, 2, -1), ((1, 3, 4, 5), (2, 4, 3, 5), 1, 2, -1), ((2, 3, 4, 5), (1, 1, 3, 1), -3, -2, 0), ], @@ -125,7 +124,7 @@ def test_broadcast(self, dtype, shape1, shape2, axis_a, axis_b, axis_c): expected = numpy.cross(a, b, axis_a, axis_b, axis_c) assert_dtype_allclose(result, expected, factor=24) - @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("dtype", ALL_DTYPES_NO_BOOL) @pytest.mark.parametrize("stride", [3, -3]) def test_strided(self, dtype, stride): a = numpy.arange(1, 10, dtype=dtype) @@ -149,31 +148,41 @@ def test_linalg(self, axis): expected = numpy.linalg.cross(a, b, axis=axis) assert_dtype_allclose(result, expected) - def test_error(self): - a = dpnp.arange(3) - b = dpnp.arange(4) - # Incompatible vector dimensions - with pytest.raises(ValueError): - dpnp.cross(a, b) + @testing.with_requires("numpy>=2.5") + @pytest.mark.parametrize("xp", [numpy, dpnp]) + def test_2x2(self, xp): + a = xp.array([1, 2]) + b = xp.array([3, 4]) + assert_raises(ValueError, xp.cross, a, b) + + @testing.with_requires("numpy>=2.5") + @pytest.mark.parametrize("xp", [numpy, dpnp]) + def test_2x3(self, xp): + a = xp.array([1, 2]) + b = xp.array([3, 4, 5]) + assert_raises(ValueError, xp.cross, a, b) + assert_raises(ValueError, xp.cross, b, a) + + @pytest.mark.parametrize("xp", [numpy, dpnp]) + @pytest.mark.parametrize("a, b", [(0, [1, 2]), ([1, 2], 3)]) + def test_zero_dim(self, xp, a, b): + x, y = xp.array(a), xp.array(b) + assert_raises_regex( + ValueError, "At least one array has zero dimension", xp.cross, x, y + ) + + @pytest.mark.parametrize("xp", [numpy, dpnp]) + def test_error(self, xp): + a = xp.arange(3) + b = xp.arange(1, 4) - a = dpnp.arange(3) - b = dpnp.arange(4) # axis should be an integer - with pytest.raises(TypeError): - dpnp.cross(a, b, axis=0.0) + assert_raises(TypeError, xp.cross, a, b, axis=0.0) - a = dpnp.arange(2, dtype=dpnp.bool) # Input arrays with boolean data type are not supported - with pytest.raises(TypeError): - dpnp.cross(a, a) - - @testing.with_requires("numpy>=2.0") - def test_linalg_error(self): - a = dpnp.arange(4) - b = dpnp.arange(4) - # Both input arrays must be (arrays of) 3-dimensional vectors - with pytest.raises(ValueError): - dpnp.linalg.cross(a, b) + a = a.astype(dtype=dpnp.bool) + b = b.astype(dtype=dpnp.bool) + assert_raises(TypeError, xp.cross, a, b) class TestDot: diff --git a/dpnp/tests/third_party/cupy/linalg_tests/test_product.py b/dpnp/tests/third_party/cupy/linalg_tests/test_product.py index adf0dfd63470..c69e6ea8c1ae 100644 --- a/dpnp/tests/third_party/cupy/linalg_tests/test_product.py +++ b/dpnp/tests/third_party/cupy/linalg_tests/test_product.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys import unittest import warnings @@ -130,6 +132,7 @@ def test_cross(self, xp, dtype_a, dtype_b): } ) ) +@pytest.mark.skip("deprecation dropped in NumPy 2.5") class TestCrossProductDeprecated(unittest.TestCase): @testing.for_all_dtypes_combination(["dtype_a", "dtype_b"]) @testing.numpy_cupy_allclose(type_check=has_support_aspect64()) @@ -483,6 +486,65 @@ def test_kron_accepts_numbers_as_arguments(self, a, b, xp): ] return xp.kron(*args) + @pytest.mark.parametrize( + "shape_a, shape_b", + [ + # 2-D, empty in `a` + ((1, 0), (2, 2)), + ((3, 0), (2, 4)), + ((0, 3), (2, 4)), + ((0, 0), (2, 4)), + # 2-D, empty in `b` + ((2, 4), (3, 0)), + ((2, 4), (0, 0)), + # 1-D + ((0,), (4,)), + ((4,), (0,)), + ((0,), (0,)), + # >2-D with an empty dim + ((2, 0, 3), (1, 4, 2)), + ((1, 2, 0), (3, 4, 5)), + # mixed ndim, empty operand smaller-rank + ((0,), (3, 4)), + ((4,), (3, 0)), + ((3, 0), (2,)), + ], + ) + @testing.for_all_dtypes() + @testing.numpy_cupy_allclose() + def test_kron_empty(self, xp, dtype, shape_a, shape_b): + a = xp.empty(shape_a, dtype=dtype) + b = xp.empty(shape_b, dtype=dtype) + return xp.kron(a, b) + + @pytest.mark.parametrize( + "shape_b", + [(0,), (0, 5), (3, 0), (2, 0, 4)], + ) + @testing.for_all_dtypes() + @testing.numpy_cupy_allclose() + def test_kron_zerodim_with_empty(self, xp, dtype, shape_b): + # 0-D scalar array × empty array — exercises the early + # `cupy.multiply` branch rather than the empty-result short-circuit. + a = xp.array(2, dtype=dtype) + b = xp.empty(shape_b, dtype=dtype) + return xp.kron(a, b) + + @testing.for_all_dtypes() + @testing.numpy_cupy_allclose() + def test_kron_empty_fortran_order(self, xp, dtype): + a = xp.empty((3, 0), dtype=dtype, order="F") + b = xp.empty((2, 4), dtype=dtype, order="F") + return xp.kron(a, b) + + @testing.for_all_dtypes() + @testing.numpy_cupy_allclose() + def test_kron_empty_via_slice(self, xp, dtype): + # Empty array produced by zero-length slicing of a non-empty buffer. + a = xp.zeros((3, 4), dtype=dtype)[:0] + b = xp.zeros((1, 2), dtype=dtype) + return xp.kron(a, b) + @testing.parameterize( *testing.product( @@ -595,3 +657,38 @@ def test_matrix_power_batched(self, xp, dtype, shape, n): a = testing.shaped_arange(shape, xp, dtype) a += xp.identity(shape[-1], dtype) return xp.linalg.matrix_power(a, n) + + +@pytest.mark.parametrize( + "shapes", + [ + ((3, 4), (4, 5)), + ((1, 1), (1, 1)), + ((5, 5), (5, 5)), + ((1, 7), (7, 1)), + ], +) +class TestLinalgMatmul2D: + + @testing.for_float_dtypes() + @testing.numpy_cupy_allclose(atol=1e-3, rtol=1e-3) + def test_matmul_2d(self, xp, dtype, shapes): + shape_a, shape_b = shapes + a = testing.shaped_random(shape_a, xp, dtype) + b = testing.shaped_random(shape_b, xp, dtype) + return xp.linalg.matmul(a, b) + + +class TestLinalgMatrixTranspose: + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal() + def test_matrix_transpose(self, xp, dtype): + a = testing.shaped_arange((2, 3), xp, dtype) + return xp.linalg.matrix_transpose(a) + + @testing.for_all_dtypes() + @testing.numpy_cupy_array_equal(accept_error=ValueError) + def test_matrix_transpose_error(self, xp, dtype): + a = testing.shaped_arange((10,), xp, dtype) + return xp.linalg.matrix_transpose(a)