diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f98f0f4108..5162e4fcdfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ This release is compatible with NumPy 2.5. * Fixed incorrect `dpnp.tensor.expm1` result for `complex(±0, 0)` special case on CPU to match the Python Array API specification [#2926](https://github.com/IntelPython/dpnp/pull/2926) * Fixed tests which expected lists from `dpctl` functions which now return tuples (i.e., `dpctl.SyclDevice.create_sub_devices`) [#2945](https://github.com/IntelPython/dpnp/pull/2945) * Fixed `PytestRemovedIn10Warning` raised by `pytest` 9.1.0 by converting class-scoped fixtures to class methods [#2952](https://github.com/IntelPython/dpnp/pull/2952) +* Fixed `dpnp.linalg.svd(..., hermitian=True)` returning a non-unitary `vh` for singular input arrays due to a zero sign appearing [#2954](https://github.com/IntelPython/dpnp/pull/2954) ### Security diff --git a/dpnp/linalg/dpnp_utils_linalg.py b/dpnp/linalg/dpnp_utils_linalg.py index ee2023befc2..2a4600a5973 100644 --- a/dpnp/linalg/dpnp_utils_linalg.py +++ b/dpnp/linalg/dpnp_utils_linalg.py @@ -914,7 +914,8 @@ def _hermitian_svd(a, compute_uv): # the eigenvalues and related arrays to have the correct order if compute_uv: s, u = dpnp_eigh(a, eigen_mode="V") - sgn = dpnp.sign(s) + # avoid zero sign + sgn = dpnp.copysign(1.0, s) s = dpnp.abs(s, out=s) sidx = dpnp.argsort(s)[..., ::-1] # Rearrange the signs according to sorted indices diff --git a/dpnp/tests/test_linalg.py b/dpnp/tests/test_linalg.py index 723b1dcedc4..302a722bd1b 100644 --- a/dpnp/tests/test_linalg.py +++ b/dpnp/tests/test_linalg.py @@ -27,6 +27,8 @@ from .qr_helper import check_qr from .third_party.cupy import testing +ALL_DTYPES_NO_BOOL = get_all_dtypes(no_none=True, no_bool=True) + def vvsort(val, vec, size, xp): val_kwargs = {} @@ -464,8 +466,6 @@ def test_det_errors(self): class TestEigenvalue: - ALL_DTYPES_NO_BOOL = get_all_dtypes(no_none=True, no_bool=True) - # Eigenvalue decomposition of a matrix or a batch of matrices # by checking if the eigen equation A*v=w*v holds for given eigenvalues(w) # and eigenvectors(v). @@ -3974,7 +3974,7 @@ def get_tol(self, dtype): # Additionally checks for equality of singular values # between dpnp and numpy decompositions def check_decomposition( - self, dp_a, dp_u, dp_s, dp_vt, np_u, np_s, np_vt, compute_vt + self, dp_a, dp_u, dp_s, dp_vt, np_u, np_s, np_vt, compute_vt=False ): tol = self._tol if compute_vt: @@ -4006,13 +4006,13 @@ def check_decomposition( atol=tol, ) - @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize("dtype", ALL_DTYPES_NO_BOOL) @pytest.mark.parametrize( "shape", [(2, 2), (3, 4), (5, 3), (16, 16)], ids=["(2, 2)", "(3, 4)", "(5, 3)", "(16, 16)"], ) - def test_svd(self, dtype, shape): + def test_basic(self, dtype, shape): a = numpy.arange(shape[0] * shape[1], dtype=dtype).reshape(shape) dp_a = dpnp.array(a) @@ -4030,7 +4030,7 @@ def test_svd(self, dtype, shape): @pytest.mark.parametrize( "shape", [(2, 2), (16, 16)], ids=["(2, 2)", "(16, 16)"] ) - def test_svd_hermitian(self, dtype, compute_vt, shape): + def test_hermitian(self, dtype, compute_vt, shape): a = generate_random_numpy_array(shape, dtype, hermitian=True) dp_a = dpnp.array(a) @@ -4053,6 +4053,16 @@ def test_svd_hermitian(self, dtype, compute_vt, shape): dp_a, dp_u, dp_s, dp_vh, np_u, np_s, np_vh, compute_vt ) + @testing.with_requires("numpy>=2.5") + def test_hermitian_singular(self): + a = numpy.array([[1, 0], [0, 0]]) + dp_a = dpnp.array(a) + + np_u, _, np_vh = numpy.linalg.svd(a, hermitian=True) + u, _, vh = dpnp.linalg.svd(dp_a, hermitian=True) + assert_allclose(u, np_u) + assert_allclose(vh, np_vh) + def test_svd_errors(self): a_dp = dpnp.array([[1, 2], [3, 4]], dtype="float32")