From f6971d39cd63e089c83913c66234cc56ce283f36 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 23 Jan 2025 17:41:28 +0200 Subject: [PATCH 01/13] Return named tuple for eig, eigh, qr, slogdet, svd functions --- dpnp/linalg/dpnp_iface_linalg.py | 19 +++++- dpnp/linalg/dpnp_utils_linalg.py | 108 ++++++++++++++++++++----------- 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index 377bbcdb37c2..4761953f8e84 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -39,12 +39,18 @@ # pylint: disable=invalid-name # pylint: disable=no-member +from typing import NamedTuple + import numpy from dpctl.tensor._numpy_helper import normalize_axis_tuple import dpnp from .dpnp_utils_linalg import ( + EighResult, + QRResult, + SlogdetResult, + SVDResult, assert_2d, assert_stacked_2d, assert_stacked_square, @@ -66,6 +72,11 @@ ) __all__ = [ + "EigResult", + "EighResult", + "QRResult", + "SlogdetResult", + "SVDResult", "cholesky", "cond", "cross", @@ -100,6 +111,12 @@ ] +# pylint:disable=missing-class-docstring +class EigResult(NamedTuple): + eigenvalues: dpnp.ndarray + eigenvectors: dpnp.ndarray + + def cholesky(a, /, *, upper=False): """ Cholesky decomposition. @@ -532,7 +549,7 @@ def eig(a): # Since geev function from OneMKL LAPACK is not implemented yet, # use NumPy for this calculation. w_np, v_np = numpy.linalg.eig(dpnp.asnumpy(a)) - return ( + return EigResult( dpnp.array(w_np, sycl_queue=a_sycl_queue, usm_type=a_usm_type), dpnp.array(v_np, sycl_queue=a_sycl_queue, usm_type=a_usm_type), ) diff --git a/dpnp/linalg/dpnp_utils_linalg.py b/dpnp/linalg/dpnp_utils_linalg.py index 97ff26a70432..5b69475b553b 100644 --- a/dpnp/linalg/dpnp_utils_linalg.py +++ b/dpnp/linalg/dpnp_utils_linalg.py @@ -38,6 +38,8 @@ # pylint: disable=protected-access # pylint: disable=useless-import-alias +from typing import NamedTuple + import dpctl.tensor._tensor_impl as ti import dpctl.utils as dpu import numpy @@ -50,6 +52,10 @@ from dpnp.linalg import LinAlgError as LinAlgError __all__ = [ + "EighResult", + "QRResult", + "SlogdetResult", + "SVDResult", "assert_2d", "assert_stacked_2d", "assert_stacked_square", @@ -70,6 +76,29 @@ "dpnp_svd", ] + +# pylint:disable=missing-class-docstring +class EighResult(NamedTuple): + eigenvalues: dpnp.ndarray + eigenvectors: dpnp.ndarray + + +class QRResult(NamedTuple): + Q: dpnp.ndarray + R: dpnp.ndarray + + +class SlogdetResult(NamedTuple): + sign: dpnp.ndarray + logabsdet: dpnp.ndarray + + +class SVDResult(NamedTuple): + U: dpnp.ndarray + S: dpnp.ndarray + Vh: dpnp.ndarray + + _jobz = {"N": 0, "V": 1} _upper_lower = {"U": 0, "L": 1} @@ -162,7 +191,7 @@ def _batched_eigh(a, UPLO, eigen_mode, w_type, v_type): # Convert to contiguous to align with NumPy if a_orig_order == "C": v = dpnp.ascontiguousarray(v) - return w, v + return EighResult(w, v) return w @@ -476,7 +505,7 @@ def _batched_qr(a, mode="reduced"): r = _triu_inplace(r) - return ( + return QRResult( q.reshape(batch_shape + q.shape[-2:]), r.reshape(batch_shape + r.shape[-2:]), ) @@ -632,7 +661,7 @@ def _batched_svd( u = dpnp.ascontiguousarray(u) vt = dpnp.ascontiguousarray(vt) # Swap `u` and `vt` for transposed input to restore correct order - return (vt, s, u) if trans_flag else (u, s, vt) + return SVDResult(vt, s, u) if trans_flag else SVDResult(u, s, vt) return s @@ -819,9 +848,9 @@ def _hermitian_svd(a, compute_uv): # but dpnp.linalg.eigh returns s sorted ascending so we re-order # the eigenvalues and related arrays to have the correct order if compute_uv: - s, u = dpnp.linalg.eigh(a) - sgn = dpnp.sign(s) - s = dpnp.absolute(s) + s, u = s = dpnp_eigh(a, eigen_mode="V") + sgn = dpnp.sign(s, out=s) + s = dpnp.abs(s, out=s) sidx = dpnp.argsort(s)[..., ::-1] # Rearrange the signs according to sorted indices sgn = dpnp.take_along_axis(sgn, sidx, axis=-1) @@ -832,11 +861,10 @@ def _hermitian_svd(a, compute_uv): # Singular values are unsigned, move the sign into v # Compute V^T adjusting for the sign and conjugating vt = dpnp.transpose(u * sgn[..., None, :]).conjugate() - return u, s, vt + return SVDResult(u, s, vt) - # TODO: use dpnp.linalg.eighvals when it is updated - s, _ = dpnp.linalg.eigh(a) - s = dpnp.abs(s) + s = dpnp_eigh(a, eigen_mode="N") + s = dpnp.abs(s, out=s) return dpnp.sort(s)[..., ::-1] @@ -1423,7 +1451,7 @@ def _zero_batched_qr(a, mode, m, n, k, res_type): batch_shape = a.shape[:-2] if mode == "reduced": - return ( + return QRResult( dpnp.empty_like( a, shape=batch_shape + (m, k), @@ -1443,7 +1471,7 @@ def _zero_batched_qr(a, mode, m, n, k, res_type): usm_type=a_usm_type, sycl_queue=a_sycl_queue, ) - return ( + return QRResult( q, dpnp.empty_like( a, @@ -1530,7 +1558,7 @@ def _zero_batched_svd( usm_type=usm_type, sycl_queue=exec_q, ) - return u, s, vt + return SVDResult(u, s, vt) return s @@ -1548,22 +1576,28 @@ def _zero_k_qr(a, mode, m, n, res_type): m, n = a.shape if mode == "reduced": - return dpnp.empty_like( - a, - shape=(m, 0), - dtype=res_type, - ), dpnp.empty_like( - a, - shape=(0, n), - dtype=res_type, + return QRResult( + dpnp.empty_like( + a, + shape=(m, 0), + dtype=res_type, + ), + dpnp.empty_like( + a, + shape=(0, n), + dtype=res_type, + ), ) if mode == "complete": - return dpnp.identity( - m, dtype=res_type, sycl_queue=a_sycl_queue, usm_type=a_usm_type - ), dpnp.empty_like( - a, - shape=(m, n), - dtype=res_type, + return QRResult( + dpnp.identity( + m, dtype=res_type, sycl_queue=a_sycl_queue, usm_type=a_usm_type + ), + dpnp.empty_like( + a, + shape=(m, n), + dtype=res_type, + ), ) if mode == "r": return dpnp.empty_like( @@ -1648,7 +1682,7 @@ def _zero_m_n_batched_svd( usm_type=usm_type, sycl_queue=exec_q, ) - return u, s, vt + return SVDResult(u, s, vt) return s @@ -1692,7 +1726,7 @@ def _zero_m_n_svd( usm_type=usm_type, sycl_queue=exec_q, ) - return u, s, vt + return SVDResult(u, s, vt) return s @@ -1993,7 +2027,7 @@ def dpnp_det(a): return det.reshape(shape) -def dpnp_eigh(a, UPLO, eigen_mode="V"): +def dpnp_eigh(a, UPLO="L", eigen_mode="V"): """ dpnp_eigh(a, UPLO, eigen_mode="V") @@ -2016,7 +2050,7 @@ def dpnp_eigh(a, UPLO, eigen_mode="V"): w = dpnp.empty_like(a, shape=a.shape[:-1], dtype=w_type) if eigen_mode == "V": v = dpnp.empty_like(a, dtype=v_type) - return w, v + return EighResult(w, v) return w if a.ndim > 2: @@ -2097,7 +2131,7 @@ def dpnp_eigh(a, UPLO, eigen_mode="V"): else: out_v = v - return (w, out_v) if eigen_mode == "V" else w + return EighResult(w, out_v) if eigen_mode == "V" else w def dpnp_inv(a): @@ -2546,7 +2580,7 @@ def dpnp_qr(a, mode="reduced"): r = a_t[:, :mc].transpose() r = _triu_inplace(r) - return (q, r) + return QRResult(q, r) def dpnp_solve(a, b): @@ -2675,7 +2709,7 @@ def dpnp_slogdet(a): usm_type=a_usm_type, sycl_queue=a_sycl_queue, ) - return sign, logdet + return SlogdetResult(sign, logdet) lu, ipiv, dev_info = _lu_factor(a, res_type) @@ -2687,7 +2721,7 @@ def dpnp_slogdet(a): logdet = logdet.astype(logdet_dtype, copy=False) singular = dev_info > 0 - return ( + return SlogdetResult( dpnp.where(singular, res_type.type(0), sign).reshape(shape), dpnp.where(singular, logdet_dtype.type("-inf"), logdet).reshape(shape), ) @@ -2815,10 +2849,10 @@ def dpnp_svd( # For A^T = V S^T U^T, `u_h` becomes V and `vt_h` becomes U^T. # Transpose and swap them back to restore correct order for A. if trans_flag: - return vt_h.T, s_h, u_h.T + return SVDResult(vt_h.T, s_h, u_h.T) # gesvd call writes `u_h` and `vt_h` in Fortran order; # Convert to contiguous to align with NumPy u_h = dpnp.ascontiguousarray(u_h) vt_h = dpnp.ascontiguousarray(vt_h) - return u_h, s_h, vt_h + return SVDResult(u_h, s_h, vt_h) return s_h From a1dbceaf2a6c58fe435058b41f8c6be5afabb7f5 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 23 Jan 2025 17:42:22 +0200 Subject: [PATCH 02/13] Enable python array api tests --- .github/workflows/array-api-skips.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/array-api-skips.txt b/.github/workflows/array-api-skips.txt index 097e6ffef42b..f21086f55212 100644 --- a/.github/workflows/array-api-skips.txt +++ b/.github/workflows/array-api-skips.txt @@ -17,13 +17,7 @@ array_api_tests/test_signatures.py::test_func_signature[unique_counts] array_api_tests/test_signatures.py::test_func_signature[unique_inverse] array_api_tests/test_signatures.py::test_func_signature[unique_values] -# do not return a namedtuple -array_api_tests/test_linalg.py::test_eigh -array_api_tests/test_linalg.py::test_slogdet -array_api_tests/test_linalg.py::test_svd - # hypothesis found failures -array_api_tests/test_linalg.py::test_qr array_api_tests/test_operators_and_elementwise_functions.py::test_clip # unexpected result is returned From faf1332955efa5a5c4e62070552efd855de0be1a Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 23 Jan 2025 18:16:32 +0200 Subject: [PATCH 03/13] Update docstrings to mention namedtuple in return sections --- dpnp/linalg/dpnp_iface_linalg.py | 88 ++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index 4761953f8e84..65076a85b80b 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -468,17 +468,18 @@ def eig(a): Returns ------- + A namedtuple with the following attributes: + eigenvalues : (..., M) dpnp.ndarray The eigenvalues, each repeated according to its multiplicity. - The eigenvalues are not necessarily ordered. The resulting - array will be of complex type, unless the imaginary part is - zero in which case it will be cast to a real type. When `a` - is real the resulting eigenvalues will be real (0 imaginary - part) or occur in conjugate pairs + The eigenvalues are not necessarily ordered. The resulting array will + be of complex type, unless the imaginary part is zero in which case it + will be cast to a real type. When `a` is real the resulting eigenvalues + will be real (zero imaginary part) or occur in conjugate pairs. eigenvectors : (..., M, M) dpnp.ndarray - The normalized (unit "length") eigenvectors, such that the - column ``v[:,i]`` is the eigenvector corresponding to the - eigenvalue ``w[i]``. + The normalized (unit "length") eigenvectors, such that the column + ``eigenvectors[:,i]`` is the eigenvector corresponding to the + eigenvalue ``eigenvalues[i]``. Note ---- @@ -582,12 +583,14 @@ def eigh(a, UPLO="L"): Returns ------- - w : (..., M) dpnp.ndarray - The eigenvalues in ascending order, each repeated according to - its multiplicity. - v : (..., M, M) dpnp.ndarray - The column ``v[:, i]`` is the normalized eigenvector corresponding - to the eigenvalue ``w[i]``. + A namedtuple with the following attributes: + + eigenvalues : (..., M) dpnp.ndarray + The eigenvalues in ascending order, each repeated according to its + multiplicity. + eigenvectors : (..., M, M) dpnp.ndarray + The column ``eigenvectors[:, i]`` is the normalized eigenvector + corresponding to the eigenvalue ``eigenvalues[i]``. See Also -------- @@ -661,7 +664,7 @@ def eigvals(a): Illustration, using the fact that the eigenvalues of a diagonal matrix are its diagonal elements, that multiplying a matrix on the left by an orthogonal matrix, `Q`, and on the right by `Q.T` (the transpose - of `Q`), preserves the eigenvalues of the "middle" matrix. In other words, + of `Q`), preserves the eigenvalues of the "middle" matrix. In other words, if `Q` is orthogonal, then ``Q * A * Q.T`` has the same eigenvalues as ``A``: @@ -856,7 +859,7 @@ def lstsq(a, b, rcond=None): gradient of roughly 1 and cut the y-axis at, more or less, -1. We can rewrite the line equation as ``y = Ap``, where ``A = [[x 1]]`` - and ``p = [[m], [c]]``. Now use `lstsq` to solve for `p`: + and ``p = [[m], [c]]``. Now use `lstsq` to solve for `p`: >>> A = np.vstack([x, np.ones(len(x))]).T >>> A @@ -1269,7 +1272,7 @@ def norm(x, ord=None, axis=None, keepdims=False): Parameters ---------- x : {dpnp.ndarray, usm_ndarray} - Input array. If `axis` is ``None``, `x` must be 1-D or 2-D, unless + Input array. If `axis` is ``None``, `x` must be 1-D or 2-D, unless `ord` is ``None``. If both `axis` and `ord` are ``None``, the 2-norm of ``x.ravel`` will be returned. ord : {int, float, inf, -inf, "fro", "nuc"}, optional @@ -1574,20 +1577,22 @@ def qr(a, mode="reduced"): Returns ------- When mode is "reduced" or "complete", the result will be a namedtuple with - the attributes Q and R. - Q : dpnp.ndarray + the attributes `Q` and `R`. + + Q : dpnp.ndarray of float or complex, optional A matrix with orthonormal columns. - When mode = "complete" the result is an orthogonal/unitary matrix - depending on whether or not a is real/complex. - The determinant may be either +/- 1 in that case. - In case the number of dimensions in the input array is greater - than 2 then a stack of the matrices with above properties is returned. - R : dpnp.ndarray - The upper-triangular matrix or a stack of upper-triangular matrices - if the number of dimensions in the input array is greater than 2. - (h, tau) : tuple of dpnp.ndarray - The `h` array contains the Householder reflectors that generate Q along - with R. The `tau` array contains scaling factors for the reflectors. + When mode is ``"complete"`` the result is an orthogonal/unitary matrix + depending on whether or not `a` is real/complex. The determinant may be + either ``+/- 1`` in that case. In case the number of dimensions in the + input array is greater than 2 then a stack of the matrices with above + properties is returned. + R : dpnp.ndarray of float or complex, optional + The upper-triangular matrix or a stack of upper-triangular matrices if + the number of dimensions in the input array is greater than 2. + (h, tau) : tuple of dpnp.ndarray of float or complex, optional + The array `h` contains the Householder reflectors that generate `Q` + along with `R`. The `tau` array contains scaling factors for the + reflectors. Examples -------- @@ -1726,22 +1731,25 @@ def svd(a, full_matrices=True, compute_uv=True, hermitian=False): Returns ------- - u : { (…, M, M), (…, M, K) } dpnp.ndarray + When `compute_uv` is ``True``, the result is a namedtuple with the + following attribute names: + + U : { (…, M, M), (…, M, K) } dpnp.ndarray Unitary matrix, where M is the number of rows of the input array `a`. - The shape of the matrix `u` depends on the value of `full_matrices`. - If `full_matrices` is ``True``, `u` has the shape (…, M, M). - If `full_matrices` is ``False``, `u` has the shape (…, M, K), where - K = min(M, N), and N is the number of columns of the input array `a`. - If `compute_uv` is ``False``, neither `u` or `Vh` are computed. - s : (…, K) dpnp.ndarray + The shape of the matrix `U` depends on the value of `full_matrices`. + If `full_matrices` is ``True``, `U` has the shape (…, M, M). If + `full_matrices` is ``False``, `U` has the shape (…, M, K), where + ``K = min(M, N)``, and N is the number of columns of the input array + `a`. If `compute_uv` is ``False``, neither `U` or `Vh` are computed. + S : (…, K) dpnp.ndarray Vector containing the singular values of `a`, sorted in descending - order. The length of `s` is min(M, N). + order. The length of `S` is min(M, N). Vh : { (…, N, N), (…, K, N) } dpnp.ndarray Unitary matrix, where N is the number of columns of the input array `a`. The shape of the matrix `Vh` depends on the value of `full_matrices`. If `full_matrices` is ``True``, `Vh` has the shape (…, N, N). If `full_matrices` is ``False``, `Vh` has the shape (…, K, N). - If `compute_uv` is ``False``, neither `u` or `Vh` are computed. + If `compute_uv` is ``False``, neither `U` or `Vh` are computed. Examples -------- @@ -1869,6 +1877,8 @@ def slogdet(a): Returns ------- + A namedtuple with the following attributes: + sign : (...) dpnp.ndarray A number representing the sign of the determinant. For a real matrix, this is 1, 0, or -1. For a complex matrix, this is a complex number From ae17d9b50580048cb60ce22eee80ed98aac58191 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 23 Jan 2025 18:35:58 +0200 Subject: [PATCH 04/13] Test namedtuple returned by affected linalg functions --- dpnp/tests/test_linalg.py | 74 +++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/dpnp/tests/test_linalg.py b/dpnp/tests/test_linalg.py index 111c7845bdd8..d67e859c2675 100644 --- a/dpnp/tests/test_linalg.py +++ b/dpnp/tests/test_linalg.py @@ -521,7 +521,8 @@ def test_eigenvalues(self, func, shape, dtype, order): # we verify them through the eigen equation A*v=w*v. if func in ("eig", "eigh"): w, _ = getattr(numpy.linalg, func)(a) - w_dp, v_dp = getattr(dpnp.linalg, func)(a_dp) + result = getattr(dpnp.linalg, func)(a_dp) + w_dp, v_dp = result.eigenvalues, result.eigenvectors self.assert_eigen_decomposition(a_dp, w_dp, v_dp) @@ -545,7 +546,8 @@ def test_eigenvalue_empty(self, func, shape, dtype): if func == "eig": w, v = getattr(numpy.linalg, func)(a_np) - w_dp, v_dp = getattr(dpnp.linalg, func)(a_dp) + result = getattr(dpnp.linalg, func)(a_dp) + w_dp, v_dp = result.eigenvalues, result.eigenvectors assert_dtype_allclose(v_dp, v) @@ -2388,16 +2390,18 @@ def test_qr(self, dtype, shape, mode): dpnp_r = dpnp.linalg.qr(ia, mode) else: np_q, np_r = numpy.linalg.qr(a, mode) - dpnp_q, dpnp_r = dpnp.linalg.qr(ia, mode) # check decomposition if mode in ("complete", "reduced"): + result = dpnp.linalg.qr(ia, mode) + dpnp_q, dpnp_r = result.Q, result.R assert_almost_equal( dpnp.matmul(dpnp_q, dpnp_r), a, decimal=5, ) else: # mode=="raw" + dpnp_q, dpnp_r = dpnp.linalg.qr(ia, mode) assert_dtype_allclose(dpnp_q, np_q) if mode in ("raw", "r"): @@ -2421,15 +2425,18 @@ def test_qr_large(self, dtype, shape, mode): dpnp_r = dpnp.linalg.qr(ia, mode) else: np_q, np_r = numpy.linalg.qr(a, mode) - dpnp_q, dpnp_r = dpnp.linalg.qr(ia, mode) + # check decomposition if mode in ("complete", "reduced"): + result = dpnp.linalg.qr(ia, mode) + dpnp_q, dpnp_r = result.Q, result.R assert_almost_equal( dpnp.matmul(dpnp_q, dpnp_r), a, decimal=5, ) else: # mode=="raw" + dpnp_q, dpnp_r = dpnp.linalg.qr(ia, mode) assert_allclose(np_q, dpnp_q, atol=1e-4) if mode in ("raw", "r"): assert_allclose(np_r, dpnp_r, atol=1e-4) @@ -2457,7 +2464,12 @@ def test_qr_empty(self, dtype, shape, mode): dpnp_r = dpnp.linalg.qr(ia, mode) else: np_q, np_r = numpy.linalg.qr(a, mode) - dpnp_q, dpnp_r = dpnp.linalg.qr(ia, mode) + + if mode in ("complete", "reduced"): + result = dpnp.linalg.qr(ia, mode) + dpnp_q, dpnp_r = result.Q, result.R + else: + dpnp_q, dpnp_r = dpnp.linalg.qr(ia, mode) assert_dtype_allclose(dpnp_q, np_q) @@ -2474,7 +2486,12 @@ def test_qr_strides(self, mode): dpnp_r = dpnp.linalg.qr(ia[::2, ::2], mode) else: np_q, np_r = numpy.linalg.qr(a[::2, ::2], mode) - dpnp_q, dpnp_r = dpnp.linalg.qr(ia[::2, ::2], mode) + + if mode in ("complete", "reduced"): + result = dpnp.linalg.qr(ia[::2, ::2], mode) + dpnp_q, dpnp_r = result.Q, result.R + else: + dpnp_q, dpnp_r = dpnp.linalg.qr(ia[::2, ::2], mode) assert_dtype_allclose(dpnp_q, np_q) @@ -2486,7 +2503,12 @@ def test_qr_strides(self, mode): dpnp_r = dpnp.linalg.qr(ia[::-2, ::-2], mode) else: np_q, np_r = numpy.linalg.qr(a[::-2, ::-2], mode) - dpnp_q, dpnp_r = dpnp.linalg.qr(ia[::-2, ::-2], mode) + + if mode in ("complete", "reduced"): + result = dpnp.linalg.qr(ia[::-2, ::-2], mode) + dpnp_q, dpnp_r = result.Q, result.R + else: + dpnp_q, dpnp_r = dpnp.linalg.qr(ia[::-2, ::-2], mode) assert_dtype_allclose(dpnp_q, np_q) @@ -2660,7 +2682,8 @@ def test_slogdet_2d(self, dtype): a_dp = dpnp.array(a_np) sign_expected, logdet_expected = numpy.linalg.slogdet(a_np) - sign_result, logdet_result = dpnp.linalg.slogdet(a_dp) + result = dpnp.linalg.slogdet(a_dp) + sign_result, logdet_result = result.sign, result.logabsdet assert_allclose(sign_expected, sign_result) assert_allclose(logdet_expected, logdet_result, rtol=1e-3, atol=1e-4) @@ -2678,7 +2701,8 @@ def test_slogdet_3d(self, dtype): a_dp = dpnp.array(a_np) sign_expected, logdet_expected = numpy.linalg.slogdet(a_np) - sign_result, logdet_result = dpnp.linalg.slogdet(a_dp) + result = dpnp.linalg.slogdet(a_dp) + sign_result, logdet_result = result.sign, result.logabsdet assert_allclose(sign_expected, sign_result) assert_allclose(logdet_expected, logdet_result, rtol=1e-3, atol=1e-4) @@ -2698,13 +2722,15 @@ def test_slogdet_strides(self): # positive strides sign_expected, logdet_expected = numpy.linalg.slogdet(a_np[::2, ::2]) - sign_result, logdet_result = dpnp.linalg.slogdet(a_dp[::2, ::2]) + result = dpnp.linalg.slogdet(a_dp[::2, ::2]) + sign_result, logdet_result = result.sign, result.logabsdet assert_allclose(sign_expected, sign_result) assert_allclose(logdet_expected, logdet_result, rtol=1e-3, atol=1e-4) # negative strides sign_expected, logdet_expected = numpy.linalg.slogdet(a_np[::-2, ::-2]) - sign_result, logdet_result = dpnp.linalg.slogdet(a_dp[::-2, ::-2]) + result = dpnp.linalg.slogdet(a_dp[::-2, ::-2]) + sign_result, logdet_result = result.sign, result.logabsdet assert_allclose(sign_expected, sign_result) assert_allclose(logdet_expected, logdet_result, rtol=1e-3, atol=1e-4) @@ -2732,7 +2758,8 @@ def test_slogdet_singular_matrix(self, matrix): a_dp = dpnp.array(a_np) sign_expected, logdet_expected = numpy.linalg.slogdet(a_np) - sign_result, logdet_result = dpnp.linalg.slogdet(a_dp) + result = dpnp.linalg.slogdet(a_dp) + sign_result, logdet_result = result.sign, result.logabsdet assert_allclose(sign_expected, sign_result) assert_allclose(logdet_expected, logdet_result, rtol=1e-3, atol=1e-4) @@ -2748,7 +2775,8 @@ def test_slogdet_singular_matrix_3D(self): a_dp = dpnp.array(a_np) sign_expected, logdet_expected = numpy.linalg.slogdet(a_np) - sign_result, logdet_result = dpnp.linalg.slogdet(a_dp) + result = dpnp.linalg.slogdet(a_dp) + sign_result, logdet_result = result.sign, result.logabsdet assert_allclose(sign_expected, sign_result) assert_allclose(logdet_expected, logdet_result, rtol=1e-3, atol=1e-4) @@ -2841,13 +2869,14 @@ def test_svd(self, dtype, shape): a = numpy.arange(shape[0] * shape[1], dtype=dtype).reshape(shape) dp_a = dpnp.array(a) - np_u, np_s, np_vt = numpy.linalg.svd(a) - dp_u, dp_s, dp_vt = dpnp.linalg.svd(dp_a) + np_u, np_s, np_vh = numpy.linalg.svd(a) + result = dpnp.linalg.svd(dp_a) + dp_u, dp_s, dp_vh = result.U, result.S, result.Vh - self.check_types_shapes(dp_u, dp_s, dp_vt, np_u, np_s, np_vt) + self.check_types_shapes(dp_u, dp_s, dp_vh, np_u, np_s, np_vh) self.get_tol(dtype) self.check_decomposition( - dp_a, dp_u, dp_s, dp_vt, np_u, np_s, np_vt, True + dp_a, dp_u, dp_s, dp_vh, np_u, np_s, np_vh, True ) @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) @@ -2860,25 +2889,26 @@ def test_svd_hermitian(self, dtype, compute_vt, shape): dp_a = dpnp.array(a) if compute_vt: - np_u, np_s, np_vt = numpy.linalg.svd( + np_u, np_s, np_vh = numpy.linalg.svd( a, compute_uv=compute_vt, hermitian=True ) - dp_u, dp_s, dp_vt = dpnp.linalg.svd( + result = dpnp.linalg.svd( dp_a, compute_uv=compute_vt, hermitian=True ) + dp_u, dp_s, dp_vh = result.U, result.S, result.Vh else: np_s = numpy.linalg.svd(a, compute_uv=compute_vt, hermitian=True) dp_s = dpnp.linalg.svd(dp_a, compute_uv=compute_vt, hermitian=True) - np_u = np_vt = dp_u = dp_vt = None + np_u = np_vh = dp_u = dp_vh = None self.check_types_shapes( - dp_u, dp_s, dp_vt, np_u, np_s, np_vt, compute_vt + dp_u, dp_s, dp_vh, np_u, np_s, np_vh, compute_vt ) self.get_tol(dtype) self.check_decomposition( - dp_a, dp_u, dp_s, dp_vt, np_u, np_s, np_vt, compute_vt + dp_a, dp_u, dp_s, dp_vh, np_u, np_s, np_vh, compute_vt ) def test_svd_errors(self): From e2e08faa3eb64ddebe6fb979034677505e01323a Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 23 Jan 2025 19:33:46 +0200 Subject: [PATCH 05/13] Fixed typos in hermitian SVD --- dpnp/linalg/dpnp_utils_linalg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dpnp/linalg/dpnp_utils_linalg.py b/dpnp/linalg/dpnp_utils_linalg.py index 5b69475b553b..c9f894d6ebee 100644 --- a/dpnp/linalg/dpnp_utils_linalg.py +++ b/dpnp/linalg/dpnp_utils_linalg.py @@ -848,8 +848,8 @@ def _hermitian_svd(a, compute_uv): # but dpnp.linalg.eigh returns s sorted ascending so we re-order # the eigenvalues and related arrays to have the correct order if compute_uv: - s, u = s = dpnp_eigh(a, eigen_mode="V") - sgn = dpnp.sign(s, out=s) + s, u = dpnp_eigh(a, eigen_mode="V") + sgn = dpnp.sign(s) s = dpnp.abs(s, out=s) sidx = dpnp.argsort(s)[..., ::-1] # Rearrange the signs according to sorted indices From beab9ef5734624ce70d9de2ace60babd43aa9aaf Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 7 Feb 2025 17:09:26 +0100 Subject: [PATCH 06/13] Fix typo in Limitation block separator in dpnp.max docstring --- dpnp/dpnp_iface_statistics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpnp/dpnp_iface_statistics.py b/dpnp/dpnp_iface_statistics.py index e462dc806ea0..7c955985a321 100644 --- a/dpnp/dpnp_iface_statistics.py +++ b/dpnp/dpnp_iface_statistics.py @@ -832,7 +832,7 @@ def max(a, axis=None, out=None, keepdims=False, initial=None, where=True): dimension ``a.ndim - len(axis)``. Limitations - -----------. + ----------- Parameters `where`, and `initial` are only supported with their default values. Otherwise ``NotImplementedError`` exception will be raised. From 5fc7c1ad449776445e14a6be6065a3b7a1bb75d5 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 7 Feb 2025 17:30:41 +0100 Subject: [PATCH 07/13] Path parsing of Returns block by Napoleon extension --- doc/conf.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index c2a475e09da9..bee1f42c72c6 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -9,6 +9,7 @@ from datetime import datetime from sphinx.ext.autodoc import FunctionDocumenter +from sphinx.ext.napoleon import NumpyDocstring, docstring from dpnp.dpnp_algo.dpnp_elementwise_common import DPNPBinaryFunc, DPNPUnaryFunc @@ -231,3 +232,58 @@ def _can_document_member(member, *args, **kwargs): napoleon_use_ivar = True napoleon_include_special_with_doc = True napoleon_custom_sections = ["limitations"] + + +# Napoleon extension can't properly render "Returns" section in case of +# namedtuple as a return type. That patch proposes to extend the parse logic +# which allows text in a header of "Returns" section. +def _parse_returns_section_patched(self, section: str) -> list[str]: + fields = self._consume_returns_section() + multi = len(fields) > 1 + use_rtype = False if multi else self._config.napoleon_use_rtype + lines: list[str] = [] + + for _name, _type, _desc in fields: + is_header_block = False + if _name == "" and (not _desc or len(_desc) == 1 and _desc[0] == ""): + # self._consume_returns_section() stores the header block + # into `_type` argument, while `_name` and `_desc` have to be empty + is_header_block = True + if not lines: + docstring.logger.info( + "parse a header block of 'Returns' section", + location=self._get_location(), + ) + + if use_rtype: + field = self._format_field(_name, "", _desc) + elif not is_header_block: + field = self._format_field(_name, _type, _desc) + else: + # assign processing field to `_type` value + field = _type + + if multi: + if lines: + if is_header_block: + # add the next line of header text + lines.append(field) + else: + lines.extend(self._format_block(" * ", field)) + else: + if is_header_block: + # add a beginning of header text + lines.extend([":returns:", "", field]) + else: + lines.extend(self._format_block(":returns: * ", field)) + else: + if any(field): # only add :returns: if there's something to say + lines.extend(self._format_block(":returns: ", field)) + if _type and use_rtype: + lines.extend([f":rtype: {_type}", ""]) + if lines and lines[-1]: + lines.append("") + return lines + + +NumpyDocstring._parse_returns_section = _parse_returns_section_patched From 883adef19e8b5992079608daf9f508f8e5fe5780 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 7 Feb 2025 18:47:53 +0100 Subject: [PATCH 08/13] Log parsing of the header only once --- doc/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index bee1f42c72c6..4a979e5b3f0c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -242,6 +242,7 @@ def _parse_returns_section_patched(self, section: str) -> list[str]: multi = len(fields) > 1 use_rtype = False if multi else self._config.napoleon_use_rtype lines: list[str] = [] + is_logged_header = False for _name, _type, _desc in fields: is_header_block = False @@ -249,11 +250,12 @@ def _parse_returns_section_patched(self, section: str) -> list[str]: # self._consume_returns_section() stores the header block # into `_type` argument, while `_name` and `_desc` have to be empty is_header_block = True - if not lines: + if not is_logged_header: docstring.logger.info( "parse a header block of 'Returns' section", location=self._get_location(), ) + is_logged_header = True if use_rtype: field = self._format_field(_name, "", _desc) From 67724affa155ab24ee39ffe6c9ca8827f384c7f1 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 7 Feb 2025 19:01:26 +0100 Subject: [PATCH 09/13] Add a blank line prior Default note --- dpnp/linalg/dpnp_iface_linalg.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index 65076a85b80b..8ff1b5a46975 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -140,6 +140,7 @@ def cholesky(a, /, *, upper=False): upper : {bool}, optional If ``True``, the result must be the upper-triangular Cholesky factor. If ``False``, the result must be the lower-triangular Cholesky factor. + Default: ``False``. Returns @@ -191,6 +192,7 @@ def cond(x, p=None): Order of the norm used in the condition number computation. ``inf`` means the `dpnp.inf` object, and the Frobenius norm is the root-of-sum-of-squares norm. + Default: ``None``. Returns @@ -261,6 +263,7 @@ def cross(x1, x2, /, *, axis=-1): axis : int, optional The axis (dimension) of `x1` and `x2` containing the vectors for which to compute the cross-product. + Default: ``-1``. Returns @@ -579,6 +582,7 @@ def eigh(a, UPLO="L"): considered to preserve the Hermite matrix property. It therefore follows that the imaginary part of the diagonal will always be treated as zero. + Default: ``"L"``. Returns @@ -718,6 +722,7 @@ def eigvalsh(a, UPLO="L"): considered to preserve the Hermite matrix property. It therefore follows that the imaginary part of the diagonal will always be treated as zero. + Default: ``"L"``. Returns @@ -829,6 +834,7 @@ def lstsq(a, b, rcond=None): of `a`. The default uses the machine precision times ``max(M, N)``. Passing ``-1`` will use machine precision. + Default: ``None``. Returns @@ -969,10 +975,12 @@ def matrix_norm(x, /, *, keepdims=False, ord="fro"): If this is set to ``True``, the axes which are normed over are left in the result as dimensions with size one. With this option the result will broadcast correctly against the original `x`. + Default: ``False``. ord : {None, 1, -1, 2, -2, dpnp.inf, -dpnp.inf, 'fro', 'nuc'}, optional The order of the norm. For details see the table under ``Notes`` section in :obj:`dpnp.linalg.norm`. + Default: ``"fro"``. Returns @@ -1099,16 +1107,19 @@ def matrix_rank(A, tol=None, hermitian=False, *, rtol=None): `rtol` can be set at a time. If none of them are provided, defaults to ``S.max() * max(M, N) * eps`` where `S` is an array with singular values for `A`, and `eps` is the epsilon value for datatype of `S`. + Default: ``None``. hermitian : bool, optional If ``True``, `A` is assumed to be Hermitian (symmetric if real-valued), enabling a more efficient method for finding singular values. + Default: ``False``. rtol : (...) {None, float, dpnp.ndarray, usm_ndarray}, optional Parameter for the relative tolerance component. Only `tol` or `rtol` can be set at a time. If none of them are provided, defaults to ``max(M, N) * eps`` where `eps` is the epsilon value for datatype of `S` (an array with singular values for `A`). + Default: ``None``. Returns @@ -1211,6 +1222,7 @@ def multi_dot(arrays, *, out=None): C-contiguous, and its dtype must be the dtype that would be returned for `dot(a, b)`. If these conditions are not met, an exception is raised, instead of attempting to be flexible. + Default: ``None``. Returns @@ -1277,6 +1289,7 @@ def norm(x, ord=None, axis=None, keepdims=False): of ``x.ravel`` will be returned. ord : {int, float, inf, -inf, "fro", "nuc"}, optional Norm type. inf means dpnp's `inf` object. + Default: ``None``. axis : {None, int, 2-tuple of ints}, optional If `axis` is an integer, it specifies the axis of `x` along which to @@ -1284,11 +1297,13 @@ def norm(x, ord=None, axis=None, keepdims=False): axes that hold 2-D matrices, and the matrix norms of these matrices are computed. If `axis` is ``None`` then either a vector norm (when `x` is 1-D) or a matrix norm (when `x` is 2-D) is returned. + Default: ``None``. keepdims : bool, optional If this is set to ``True``, the axes which are normed over are left in the result as dimensions with size one. With this option the result will broadcast correctly against the original `x`. + Default: ``False``. Returns @@ -1506,15 +1521,18 @@ def pinv(a, rcond=None, hermitian=False, *, rtol=None): are set to zero. Broadcasts against the stack of matrices. Only `rcond` or `rtol` can be set at a time. If none of them are provided, defaults to ``max(M, N) * dpnp.finfo(a.dtype).eps``. + Default: ``None``. hermitian : bool, optional If ``True``, a is assumed to be Hermitian (symmetric if real-valued), enabling a more efficient method for finding singular values. + Default: ``False``. rtol : (...) {None, float, dpnp.ndarray, usm_ndarray}, optional Same as `rcond`, but it's an Array API compatible parameter name. Only `rcond` or `rtol` can be set at a time. If none of them are provided, defaults to ``max(M, N) * dpnp.finfo(a.dtype).eps``. + Default: ``None``. Returns @@ -1577,7 +1595,7 @@ def qr(a, mode="reduced"): Returns ------- When mode is "reduced" or "complete", the result will be a namedtuple with - the attributes `Q` and `R`. + the attributes `Q` and `R`: Q : dpnp.ndarray of float or complex, optional A matrix with orthonormal columns. @@ -1720,13 +1738,16 @@ def svd(a, full_matrices=True, compute_uv=True, hermitian=False): full_matrices : {bool}, optional If ``True``, it returns `u` and `Vh` with full-sized matrices. If ``False``, the matrices are reduced in size. + Default: ``True``. compute_uv : {bool}, optional If ``False``, it only returns singular values. + Default: ``True``. hermitian : {bool}, optional If True, a is assumed to be Hermitian (symmetric if real-valued), enabling a more efficient method for finding singular values. + Default: ``False``. Returns @@ -1951,6 +1972,8 @@ def tensordot(a, b, /, *, axes=2): applying to `a`, second to `b`. Both elements array_like must be of the same length. + Default: ``2``. + Returns ------- out : dpnp.ndarray @@ -2036,6 +2059,7 @@ def tensorinv(a, ind=2): ind : int, optional Number of first indices that are involved in the inverse sum. Must be a positive integer. + Default: ``2``. Returns @@ -2101,6 +2125,7 @@ def tensorsolve(a, b, axes=None): axes : {None, tuple of ints}, optional Axes in `a` to reorder to the right, before inversion. If ``None`` , no reordering is done. + Default: ``None``. Returns @@ -2185,6 +2210,7 @@ def trace(x, /, *, offset=0, dtype=None): `a` is of integer type of precision less than the default integer precision, then the default integer precision is used. Otherwise, the precision is the same as that of `a`. + Default: ``None``. Returns @@ -2270,6 +2296,7 @@ def vecdot(x1, x2, /, *, axis=-1): Second input array. axis : int, optional Axis over which to compute the dot product. + Default: ``-1``. Returns @@ -2316,15 +2343,18 @@ def vector_norm(x, /, *, axis=None, keepdims=False, ord=2): (dimensions) along which to compute batched vector norms. If ``None``, the vector norm must be computed over all array values (i.e., equivalent to computing the vector norm of a flattened array). + Default: ``None``. keepdims : bool, optional If this is set to ``True``, the axes which are normed over are left in the result as dimensions with size one. With this option the result will broadcast correctly against the original `x`. + Default: ``False``. ord : {int, float, inf, -inf, 'fro', 'nuc'}, optional The order of the norm. For details see the table under ``Notes`` section in :obj:`dpnp.linalg.norm`. + Default: ``2``. Returns From 735933d9d231507ca064287c4a790ec78689bd0a Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 7 Feb 2025 19:59:26 +0100 Subject: [PATCH 10/13] Build a list with lines of the header block --- doc/conf.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 4a979e5b3f0c..03cd711c30f4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -242,14 +242,14 @@ def _parse_returns_section_patched(self, section: str) -> list[str]: multi = len(fields) > 1 use_rtype = False if multi else self._config.napoleon_use_rtype lines: list[str] = [] + header: list[str] = [] is_logged_header = False for _name, _type, _desc in fields: - is_header_block = False + # self._consume_returns_section() stores the header block + # into `_type` argument, while `_name` has to be empty string and + # `_desc` has to be empty list of strings if _name == "" and (not _desc or len(_desc) == 1 and _desc[0] == ""): - # self._consume_returns_section() stores the header block - # into `_type` argument, while `_name` and `_desc` have to be empty - is_header_block = True if not is_logged_header: docstring.logger.info( "parse a header block of 'Returns' section", @@ -257,25 +257,24 @@ def _parse_returns_section_patched(self, section: str) -> list[str]: ) is_logged_header = True + # build a list with lines of the header block + header.extend([_type]) + continue + if use_rtype: field = self._format_field(_name, "", _desc) - elif not is_header_block: - field = self._format_field(_name, _type, _desc) else: - # assign processing field to `_type` value - field = _type + field = self._format_field(_name, _type, _desc) if multi: if lines: - if is_header_block: - # add the next line of header text - lines.append(field) - else: - lines.extend(self._format_block(" * ", field)) + lines.extend(self._format_block(" * ", field)) else: - if is_header_block: - # add a beginning of header text - lines.extend([":returns:", "", field]) + if header: + # add the header block + the 1st parameter stored in `field` + lines.extend([":returns:", ""]) + lines.extend(self._format_block(" " * 4, header)) + lines.extend(self._format_block(" * ", field)) else: lines.extend(self._format_block(":returns: * ", field)) else: From 775c4552a4c5e360aa3aad6a0d706cb0b55e903e Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 7 Feb 2025 21:00:04 +0100 Subject: [PATCH 11/13] Update docstring of asnumpy() method --- dpnp/dpnp_array.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 4f5d885da15c..bb1ab32d51b3 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -760,12 +760,14 @@ def argsort( def asnumpy(self): """ - Copy content of the array into :class:`numpy.ndarray` instance of the same shape and data type. + Copy content of the array into :class:`numpy.ndarray` instance of + the same shape and data type. Returns ------- - numpy.ndarray - An instance of :class:`numpy.ndarray` populated with the array content. + out : numpy.ndarray + An instance of :class:`numpy.ndarray` populated with the array + content. """ From fb53232d25dc8a9944386ddd4d7677c73d49bbcf Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 7 Feb 2025 21:01:10 +0100 Subject: [PATCH 12/13] Name return value from broadcast_shapes() function --- dpnp/dpnp_iface_manipulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index af5784ff526b..5bb30ad6894e 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1112,7 +1112,7 @@ def broadcast_shapes(*args): Returns ------- - tuple + out : tuple Broadcasted shape. See Also From 8b3c32f9e2fd477465757b485674bd2567e3f6b4 Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Tue, 11 Feb 2025 16:44:12 +0100 Subject: [PATCH 13/13] Don't expose the new return types as namedtuples --- dpnp/linalg/dpnp_iface_linalg.py | 9 --------- dpnp/linalg/dpnp_utils_linalg.py | 4 ---- 2 files changed, 13 deletions(-) diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index 8ff1b5a46975..364fb4de2c0a 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -47,10 +47,6 @@ import dpnp from .dpnp_utils_linalg import ( - EighResult, - QRResult, - SlogdetResult, - SVDResult, assert_2d, assert_stacked_2d, assert_stacked_square, @@ -72,11 +68,6 @@ ) __all__ = [ - "EigResult", - "EighResult", - "QRResult", - "SlogdetResult", - "SVDResult", "cholesky", "cond", "cross", diff --git a/dpnp/linalg/dpnp_utils_linalg.py b/dpnp/linalg/dpnp_utils_linalg.py index c9f894d6ebee..f105cff5a6c2 100644 --- a/dpnp/linalg/dpnp_utils_linalg.py +++ b/dpnp/linalg/dpnp_utils_linalg.py @@ -52,10 +52,6 @@ from dpnp.linalg import LinAlgError as LinAlgError __all__ = [ - "EighResult", - "QRResult", - "SlogdetResult", - "SVDResult", "assert_2d", "assert_stacked_2d", "assert_stacked_square",