From 58975a9feb26a08d8cb4650bb767bfa4f73183f4 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Tue, 17 Jun 2025 13:57:44 +0200 Subject: [PATCH 1/3] Add third party tests for linalg.cond --- .../cupy/linalg_tests/test_norms.py | 131 +++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/dpnp/tests/third_party/cupy/linalg_tests/test_norms.py b/dpnp/tests/third_party/cupy/linalg_tests/test_norms.py index 297ce282928a..b26a6a4826a1 100644 --- a/dpnp/tests/third_party/cupy/linalg_tests/test_norms.py +++ b/dpnp/tests/third_party/cupy/linalg_tests/test_norms.py @@ -4,7 +4,6 @@ import pytest import dpnp as cupy -from dpnp.tests.helper import is_cpu_device from dpnp.tests.third_party.cupy import testing @@ -224,3 +223,133 @@ def test_slogdet_one_dim(self, dtype): a = testing.shaped_arange((2,), xp, dtype) with pytest.raises(xp.linalg.LinAlgError): xp.linalg.slogdet(a) + + +@testing.parameterize( + *testing.product({"ord": [-numpy.inf, -2, -1, 1, 2, numpy.inf, "fro"]}) +) +class TestCond(unittest.TestCase): + @testing.for_float_dtypes(no_float16=True) + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_singular_zeros(self, xp, dtype): + if self.ord not in [None, 2, -2]: + pytest.skip("no LinAlgError is raising on singular matrices") + + A = xp.zeros(shape=(2, 2), dtype=dtype) + result = xp.linalg.cond(A, self.ord) + + # singular matrices don't always hit infinity. + result = xp.asarray(result) # numpy is scalar and can't be replaced + large_number = 1.0 / (xp.finfo(dtype).eps) + result[result >= large_number] = xp.inf + + return result + + @testing.for_float_dtypes(no_float16=True) + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_singular_ones(self, xp, dtype): + if self.ord not in [None, 2, -2]: + pytest.skip("no LinAlgError is raising on singular matrices") + + A = xp.ones(shape=(2, 2), dtype=dtype) + result = xp.linalg.cond(A, self.ord) + + # singular matrices don't always hit infinity. + result = xp.asarray(result) # numpy is scalar and can't be replaced + large_number = 1.0 / (xp.finfo(dtype).eps) + result[result >= large_number] = xp.inf + + return result + + @testing.for_float_dtypes(no_float16=True) + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_stacked_singular(self, xp, dtype): + if self.ord not in [None, 2, -2]: + pytest.skip("no LinAlgError is raising on singular matrices") + + # Check behavior when only some of the stacked matrices are + # singular + + A = xp.arange(16, dtype=dtype).reshape((2, 2, 2, 2)) + A[0, 0] = 0 + A[1, 1] = 0 + + res = xp.linalg.cond(A, self.ord) + return res + + @testing.for_float_dtypes(no_float16=True) + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_default(self, xp, dtype): + A = testing.shaped_arange((2, 2), xp, dtype=dtype) + return xp.linalg.cond(A) + + @testing.for_float_dtypes(no_float16=True) + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_basic(self, xp, dtype): + A = testing.shaped_arange((2, 2), xp, dtype=dtype) + return xp.linalg.cond(A, self.ord) + + @testing.for_float_dtypes(no_float16=True) + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_generalized_1(self, xp, dtype): + A = testing.shaped_arange((2, 2), xp, dtype=dtype) + A = xp.array([A, 2 * A, 3 * A]) + return xp.linalg.cond(A, self.ord) + + @testing.for_float_dtypes(no_float16=True) + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_generalized_2(self, xp, dtype): + A = testing.shaped_arange((2, 2), xp, dtype=dtype) + A = xp.array([A, 2 * A, 3 * A]) + A = xp.array([A] * 2 * 3).reshape((3, 2) + A.shape) + + return xp.linalg.cond(A, self.ord) + + @testing.for_float_dtypes(no_float16=True) + def test_0x0(self, dtype): + for xp in (numpy, cupy): + A = xp.empty((0, 0), dtype=dtype) + with pytest.raises( + xp.linalg.LinAlgError, + match="cond is not defined on empty arrays", + ): + xp.linalg.cond(A, self.ord) + + @testing.for_float_dtypes(no_float16=True) + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_1x1(self, xp, dtype): + A = xp.ones((1, 1), dtype=dtype) + return xp.linalg.cond(A, self.ord) + + @testing.for_float_dtypes(no_float16=True) + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_8x8(self, xp, dtype): + A = testing.shaped_arange((8, 8), xp, dtype=dtype) + xp.diag( + xp.ones(8, dtype=dtype) + ) + return xp.linalg.cond(A, self.ord) + + @pytest.mark.skip("only ndarray input is supported") + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_nonarray(self, xp): + A = [[1.0, 2.0], [3.0, 4.0]] + return xp.linalg.cond(A, self.ord) + + @testing.for_float_dtypes(no_float16=True) + @testing.numpy_cupy_allclose(rtol=1e-3, atol=1e-4) + def test_hermitian(self, xp, dtype): + A = xp.array([[1.0, 2.0], [2.0, 1.0]], dtype=dtype) + return xp.linalg.cond(A, self.ord) + + +class TestCondBasicNonSVD(unittest.TestCase): + def test_basic_nonsvd(self): + # Smoketest the non-svd norms + A = cupy.array([[1.0, 0, 1], [0, -2.0, 0], [0, 0, 3.0]]) + testing.assert_array_almost_equal(cupy.linalg.cond(A, cupy.inf), 4) + testing.assert_array_almost_equal(cupy.linalg.cond(A, -cupy.inf), 2 / 3) + testing.assert_array_almost_equal(cupy.linalg.cond(A, 1), 4) + testing.assert_array_almost_equal(cupy.linalg.cond(A, -1), 0.5) + testing.assert_array_almost_equal( + cupy.linalg.cond(A, "fro"), numpy.sqrt(265 / 12) + ) From d3d48c174b13080458aa7272d6b10491013bb303 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Tue, 17 Jun 2025 13:58:44 +0200 Subject: [PATCH 2/3] Update the docstring with more details on possible values of `p` keyword in dpnp.linalg.cond --- dpnp/linalg/dpnp_iface_linalg.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index 137eed98d067..f032886dc833 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -180,8 +180,18 @@ def cond(x, p=None): x : {dpnp.ndarray, usm_ndarray} The matrix whose condition number is sought. p : {None, 1, -1, 2, -2, inf, -inf, "fro"}, optional - Order of the norm used in the condition number computation. - ``inf`` means the `dpnp.inf` object, and the Frobenius norm is + Order of the norm used in the condition number computation: + + - None: 2-norm. + - "fro": Frobenius norm. + - inf: max(sum(abs(x), axis=1)). + - -inf: min(sum(abs(x), axis=1)). + - 1: max(sum(abs(x), axis=0)). + - -1: min(sum(abs(x), axis=0)). + - 2: 2-norm (largest singular value). + - -2: smallest singular value. + + ``inf`` means the :obj:`dpnp.inf` object, and the Frobenius norm is the root-of-sum-of-squares norm. Default: ``None``. From a17ea71749faeb3d35c4026b39ddbd7e86997948 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Wed, 18 Jun 2025 13:45:05 +0200 Subject: [PATCH 3/3] Format the values of norm orders to the table --- dpnp/linalg/dpnp_iface_linalg.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index f032886dc833..07ed0078be28 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -182,14 +182,18 @@ def cond(x, p=None): p : {None, 1, -1, 2, -2, inf, -inf, "fro"}, optional Order of the norm used in the condition number computation: - - None: 2-norm. - - "fro": Frobenius norm. - - inf: max(sum(abs(x), axis=1)). - - -inf: min(sum(abs(x), axis=1)). - - 1: max(sum(abs(x), axis=0)). - - -1: min(sum(abs(x), axis=0)). - - 2: 2-norm (largest singular value). - - -2: smallest singular value. + ===== ============================ + p norm for matrices + ===== ============================ + None 2-norm + 'fro' Frobenius norm + inf max(sum(abs(x), axis=1)) + -inf min(sum(abs(x), axis=1)) + 1 max(sum(abs(x), axis=0)) + -1 min(sum(abs(x), axis=0)) + 2 2-norm (largest singular value) + -2 smallest singular value + ===== ============================ ``inf`` means the :obj:`dpnp.inf` object, and the Frobenius norm is the root-of-sum-of-squares norm.