diff --git a/CHANGELOG.md b/CHANGELOG.md index f8aaae542ec5..33859bd9118a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ Also, that release drops support for Python 3.9, making Python 3.10 the minimum * Resolved an issue with strides calculation in `dpnp.diagonal` to return correct values for empty diagonals [#2814](https://github.com/IntelPython/dpnp/pull/2814) * Fixed test tolerance issues for float16 intermediate precision that became visible when testing against conda-forge's NumPy [#2828](https://github.com/IntelPython/dpnp/pull/2828) * Ensured device aware dtype handling in `dpnp.identity` and `dpnp.gradient` [#2835](https://github.com/IntelPython/dpnp/pull/2835) +* Fixed `dpnp.linalg.matrix_rank` to properly handle an empty input array [#2853](https://github.com/IntelPython/dpnp/pull/2853) ### Security diff --git a/dpnp/linalg/dpnp_utils_linalg.py b/dpnp/linalg/dpnp_utils_linalg.py index 6881c7787e9f..9b49ac7d2eae 100644 --- a/dpnp/linalg/dpnp_utils_linalg.py +++ b/dpnp/linalg/dpnp_utils_linalg.py @@ -2277,6 +2277,13 @@ def dpnp_matrix_rank(A, tol=None, hermitian=False, rtol=None): S = dpnp_svd(A, compute_uv=False, hermitian=hermitian) + # Handle empty matrices: if either dimension is 0, there are no singular + # values and the rank is 0. For stacked matrices, return array of zeros + # with proper shape. + if S.shape[-1] == 0: + # S has shape (..., 0), so result should have shape (...) + return dpnp.count_nonzero(S, axis=-1) + if tol is None: if rtol is None: rtol = max(A.shape[-2:]) * dpnp.finfo(S.dtype).eps diff --git a/dpnp/tests/test_linalg.py b/dpnp/tests/test_linalg.py index 20d974b32f0c..9fdc31de816a 100644 --- a/dpnp/tests/test_linalg.py +++ b/dpnp/tests/test_linalg.py @@ -3104,7 +3104,7 @@ def test_matrix_power_errors(self): class TestMatrixRank: - @pytest.mark.parametrize("dtype", get_all_dtypes()) + @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) @pytest.mark.parametrize( "data", [ @@ -3116,7 +3116,7 @@ class TestMatrixRank: numpy.array(1), ], ) - def test_matrix_rank(self, data, dtype): + def test_basic(self, data, dtype): a = data.astype(dtype) a_dp = dpnp.array(a) @@ -3124,7 +3124,7 @@ def test_matrix_rank(self, data, dtype): dp_rank = dpnp.linalg.matrix_rank(a_dp) assert dp_rank.asnumpy() == np_rank - @pytest.mark.parametrize("dtype", get_all_dtypes()) + @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) @pytest.mark.parametrize( "data", [ @@ -3134,7 +3134,7 @@ def test_matrix_rank(self, data, dtype): numpy.diag([1, 1, 1, 0]), ], ) - def test_matrix_rank_hermitian(self, data, dtype): + def test_hermitian(self, data, dtype): a = data.astype(dtype) a_dp = dpnp.array(a) @@ -3151,7 +3151,7 @@ def test_matrix_rank_hermitian(self, data, dtype): ], ids=["float", "0-D array", "1-D array"], ) - def test_matrix_rank_tolerance(self, high_tol, low_tol): + def test_tolerance(self, high_tol, low_tol): a = numpy.eye(4) a[-1, -1] = 1e-6 a_dp = dpnp.array(a) @@ -3190,7 +3190,7 @@ def test_matrix_rank_tolerance(self, high_tol, low_tol): [0.99e-6, numpy.array(1.01e-6), numpy.ones(4) * [0.99e-6]], ids=["float", "0-D array", "1-D array"], ) - def test_matrix_rank_tol(self, tol): + def test_tol(self, tol): a = numpy.zeros((4, 3, 2)) a_dp = dpnp.array(a) @@ -3209,7 +3209,7 @@ def test_matrix_rank_tol(self, tol): result = dpnp.linalg.matrix_rank(a_dp, tol=dp_tol) assert_dtype_allclose(result, expected) - def test_matrix_rank_errors(self): + def test_errors(self): a_dp = dpnp.array([[1, 2], [3, 4]], dtype="float32") # unsupported type `a` @@ -3238,6 +3238,41 @@ def test_matrix_rank_errors(self): ValueError, dpnp.linalg.matrix_rank, a_dp, tol=1e-06, rtol=1e-04 ) + # TODO: use below fixture when NumPy 2.5 is released + # @testing.with_requires("numpy>=2.5") + @pytest.mark.parametrize( + "shape", + [ + (0, 0), + (0, 5), + (5, 0), + (0, 5, 5), + (3, 0, 5), + (2, 0, 0), + (2, 5, 0), + (2, 3, 0, 4), + ], + ) + def test_empty(self, shape): + a = numpy.zeros(shape) + ia = dpnp.array(a) + + result = dpnp.linalg.matrix_rank(ia) + if numpy_version() < "2.5.0": # TODO: remove + # Expected behavior: rank of empty matrix is 0 + # For stacked matrices, return array of zeros + expected = numpy.zeros(shape[:-2], dtype=numpy.intp) + if expected.ndim == 0: + expected = numpy.array(0) + else: + result = numpy.linalg.matrix_rank(a) + assert_array_equal(result, expected, strict=True) + + # Also test with hermitian=True + if len(shape) >= 2 and shape[-2] == shape[-1]: + result = dpnp.linalg.matrix_rank(ia, hermitian=True) + assert_array_equal(result, expected, strict=True) + # numpy.linalg.matrix_transpose() is available since numpy >= 2.0 @testing.with_requires("numpy>=2.0")