From 4b640de247198050bccd13546e85f7ab2af50772 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Tue, 8 Oct 2024 14:13:07 +0200 Subject: [PATCH 01/12] Add implementaion of dpnp.matrix_transpose() --- dpnp/dpnp_iface_manipulation.py | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 1e847366d14f..0e88c3b64fb7 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -72,6 +72,7 @@ "flipud", "hsplit", "hstack", + "matrix_transpose", "moveaxis", "ndim", "permute_dims", @@ -1751,6 +1752,53 @@ def hstack(tup, *, dtype=None, casting="same_kind"): return dpnp.concatenate(arrs, axis=1, dtype=dtype, casting=casting) +def matrix_transpose(x, /): + """ + Transposes a matrix (or a stack of matrices) ``x``. + + For full documentation refer to :obj:`numpy.matrix_transpose`. + + Parameters + ---------- + x : (..., M, N) {dpnp.ndarray, usm_ndarray} + Input array with ``x.ndim >= 2` and whose two innermost + dimensions form ``MxN`` matrices. + + Returns + ------- + out : dpnp.ndarray + An array containing the transpose for each matrix and having shape + (..., N, M). + + See Also + -------- + :obj:`dpnp.transpose` : Returns an array with axes transposed. + + Examples + -------- + >>> import dpnp as np + >>> a = np.array([[1, 2], [3, 4]]) + >>> np.matrix_transpose(a) + array([[1, 3], + [2, 4]]) + + >>> b = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) + >>> np.matrix_transpose(b) + array([[[1, 3], + [2, 4]], + [[5, 7], + [6, 8]]]) + + """ + + dpnp.check_supported_arrays_type(x) + if x.ndim < 2: + raise ValueError( + f"Input array must be at least 2-dimensional, but it is {x.ndim}" + ) + return dpnp.swapaxes(x, -1, -2) + + def moveaxis(a, source, destination): """ Move axes of an array to new positions. Other axes remain in their original From b737d1ced61505b5d06d1e6093c68321be2305a0 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Tue, 8 Oct 2024 14:29:45 +0200 Subject: [PATCH 02/12] Add TestMatrixtranspose to dpnp tests --- tests/test_arraymanipulation.py | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_arraymanipulation.py b/tests/test_arraymanipulation.py index e8bc95574620..7eb210471fa5 100644 --- a/tests/test_arraymanipulation.py +++ b/tests/test_arraymanipulation.py @@ -556,6 +556,50 @@ def test_one_element(self): assert_array_equal(res, a) +# numpy.matrix_transpose() is available since numpy >= 2.0 +@testing.with_requires("numpy>=2.0") +class TestMatrixtranspose: + @pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True)) + @pytest.mark.parametrize( + "shape", + [(3, 5), (4, 2), (2, 5, 2), (2, 3, 3, 6)], + ids=["(3,5)", "(4,2)", "(2,5,2)", "(2,3,3,6)"], + ) + def test_matrix_transpose(self, dtype, shape): + a = numpy.arange(numpy.prod(shape), dtype=dtype).reshape(shape) + dp_a = dpnp.array(a) + + expected = numpy.matrix_transpose(a) + result = dpnp.matrix_transpose(dp_a) + + assert_allclose(result, expected) + + @pytest.mark.parametrize( + "shape", + [(0, 0), (1, 0, 0), (0, 2, 2), (0, 1, 0, 4)], + ids=["(0,0)", "(1,0,0)", "(0,2,2)", "(0, 1, 0, 4)"], + ) + def test_matrix_transpose_empty(self, shape): + a = numpy.empty(shape, dtype=dpnp.default_float_type()) + dp_a = dpnp.array(a) + + expected = numpy.matrix_transpose(a) + result = dpnp.matrix_transpose(dp_a) + + assert_allclose(result, expected) + + def test_matrix_transpose_errors(self): + a_dp = dpnp.array([[1, 2], [3, 4]], dtype="float32") + + # unsupported type + a_np = dpnp.asnumpy(a_dp) + assert_raises(TypeError, dpnp.matrix_transpose, a_np) + + # a.ndim < 2 + a_dp_ndim_1 = a_dp.flatten() + assert_raises(ValueError, dpnp.matrix_transpose, a_dp_ndim_1) + + class TestRollaxis: data = [ (0, 0), From 0c5db412c876a896f887ba97941af65184a3633b Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Tue, 8 Oct 2024 15:54:47 +0200 Subject: [PATCH 03/12] Add .mT attribute for ndarray --- dpnp/dpnp_array.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 20825c6c396c..2bbd585effc2 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -108,6 +108,14 @@ def T(self): """View of the transposed array.""" return self.transpose() + @property + def mT(self): + """View of the matrix transposed array.""" + if self.ndim < 2: + raise ValueError("matrix transpose with ndim < 2 is undefined") + + return self.swapaxes(-1, -2) + def to_device(self, target_device): """Transfer array to target device.""" From 7e0a38cd465e53e1408558b70a6fc55247cefbd8 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Tue, 8 Oct 2024 16:39:38 +0200 Subject: [PATCH 04/12] Add tests for .mT arrribute --- tests/test_ndarray.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_ndarray.py b/tests/test_ndarray.py index ac9757c580a8..f4444db8736b 100644 --- a/tests/test_ndarray.py +++ b/tests/test_ndarray.py @@ -4,6 +4,7 @@ from numpy.testing import assert_allclose, assert_array_equal import dpnp +from tests.third_party.cupy import testing from .helper import ( get_all_dtypes, @@ -258,6 +259,38 @@ def test_array_as_index(shape, index_dtype): assert a[tuple(ind_arr)] == a[1] +# numpy.matrix_transpose() is available since numpy >= 2.0 +@testing.with_requires("numpy>=2.0") +@pytest.mark.parametrize( + "shape", + [(3, 5), (2, 5, 2), (2, 3, 3, 6)], + ids=["(3,5)", "(2,5,2)", "(2,3,3,6)"], +) +def test_matrix_transpose(shape): + a = numpy.arange(numpy.prod(shape)).reshape(shape) + dp_a = dpnp.array(a) + + expected = a.mT + result = dp_a.mT + + assert_allclose(result, expected) + + # result is a view of dp_a: + # changing result, modifies dp_a + first_elem = (0,) * dp_a.ndim + + result[first_elem] = -1.0 + assert dp_a[first_elem] == -1.0 + + +@testing.with_requires("numpy>=2.0") +def test_matrix_transpose_error(): + # 1D array + dp_a = dpnp.arange(6) + with pytest.raises(ValueError): + dp_a.mT + + def test_ravel(): a = dpnp.ones((2, 2)) b = a.ravel() From 5fb231154c2033dea42adae4b4702626703c6ba2 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Thu, 10 Oct 2024 12:25:36 +0200 Subject: [PATCH 05/12] Add matrix_transpose() for dpnp.linalg module --- dpnp/linalg/dpnp_iface_linalg.py | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index 69cf31303140..422802e6de6c 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -77,6 +77,7 @@ "matmul", "matrix_power", "matrix_rank", + "matrix_transpose", "multi_dot", "norm", "pinv", @@ -871,6 +872,48 @@ def matrix_rank(A, tol=None, hermitian=False): return dpnp_matrix_rank(A, tol=tol, hermitian=hermitian) +def matrix_transpose(x, /): + """ + Transposes a matrix (or a stack of matrices) ``x``. + + For full documentation refer to :obj:`numpy.linalg.matrix_transpose`. + + Parameters + ---------- + x : (..., M, N) {dpnp.ndarray, usm_ndarray} + Input array with ``x.ndim >= 2` and whose two innermost + dimensions form ``MxN`` matrices. + + Returns + ------- + out : dpnp.ndarray + An array containing the transpose for each matrix and having shape + (..., N, M). + + See Also + -------- + :obj:`dpnp.transpose` : Returns an array with axes transposed. + + Examples + -------- + >>> import dpnp as np + >>> a = np.array([[1, 2], [3, 4]]) + >>> np.linalg.matrix_transpose(a) + array([[1, 3], + [2, 4]]) + + >>> b = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) + >>> np.linalg.matrix_transpose(b) + array([[[1, 3], + [2, 4]], + [[5, 7], + [6, 8]]]) + + """ + + return dpnp.matrix_transpose(x) + + def multi_dot(arrays, *, out=None): """ Compute the dot product of two or more arrays in a single function call. From 57687bd4ca4b656b157bdb30791a9f5de0022b8b Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Thu, 10 Oct 2024 12:29:24 +0200 Subject: [PATCH 06/12] Add 'Other matrix operation' section to linalg docs --- doc/reference/linalg.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/reference/linalg.rst b/doc/reference/linalg.rst index ddde45cb41de..f301309f4310 100644 --- a/doc/reference/linalg.rst +++ b/doc/reference/linalg.rst @@ -59,7 +59,6 @@ Norms and other numbers dpnp.linalg.slogdet dpnp.trace - Solving linear equations -------------------------- @@ -73,3 +72,12 @@ Solving linear equations dpnp.linalg.inv dpnp.linalg.pinv dpnp.linalg.tensorinv + +Other matrix operations +----------------------- +.. autosummary:: + :toctree: generated/ + :nosignatures: + + dpnp.linalg.diagonal (Array API compatible) + dpnp.linalg.matrix_transpose (Array API compatible) From 6e5111621e92fcfbdb9b5db448aba9f7ce52a185 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Thu, 10 Oct 2024 12:42:20 +0200 Subject: [PATCH 07/12] Add test for dpnp.linalg.matrix_transpose() --- tests/test_linalg.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_linalg.py b/tests/test_linalg.py index 5951f103b170..4b706400c03e 100644 --- a/tests/test_linalg.py +++ b/tests/test_linalg.py @@ -2109,6 +2109,27 @@ def test_matrix_rank_errors(self): ) +# numpy.linalg.matrix_transpose() is available since numpy >= 2.0 +@testing.with_requires("numpy>=2.0") +# dpnp.linalg.matrix_transpose() calls dpnp.matrix_transpose() +# 1 test to increase code coverage +def test_matrix_transpose(): + a = numpy.arange(6).reshape((2, 3)) + a_dp = inp.array(a) + + expected = numpy.linalg.matrix_transpose(a) + result = inp.linalg.matrix_transpose(a_dp) + + assert_allclose(expected, result) + + with assert_raises_regex( + ValueError, "array must be at least 2-dimensional" + ): + inp.linalg.matrix_transpose(a_dp[:, 0]) + + assert_raises(ValueError, inp.linalg.matrix_transpose, a_dp[:, 0]) + + class TestNorm: def setup_method(self): numpy.random.seed(42) From 2c5a9350074095adce6414d80e2c6a402d687d18 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Fri, 11 Oct 2024 11:45:57 +0200 Subject: [PATCH 08/12] Expand docs for .mT attribute --- dpnp/dpnp_array.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 2bbd585effc2..683007aa97a2 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -110,7 +110,42 @@ def T(self): @property def mT(self): - """View of the matrix transposed array.""" + """ + View of the matrix transposed array. + + The matrix transpose is the transpose of the last two dimensions, even + if the array is of higher dimension. + + Raises + ------ + ValueError + If the array is of dimension less than 2. + + Examples + -------- + >>> import dpnp as np + >>> a = np.array([[1, 2], [3, 4]]) + >>> a + array([[1, 2], + [3, 4]]) + >>> a.mT + array([[1, 3], + [2, 4]]) + + >>> a = np.arange(8).reshape((2, 2, 2)) + >>> a + array([[[0, 1], + [2, 3]], + [[4, 5], + [6, 7]]]) + >>> a.mT + array([[[0, 2], + [1, 3]], + [[4, 6], + [5, 7]]]) + + """ + if self.ndim < 2: raise ValueError("matrix transpose with ndim < 2 is undefined") From 86d18f2875c85fb2354989f69aa3ab77519d9610 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Fri, 11 Oct 2024 11:50:44 +0200 Subject: [PATCH 09/12] Apply review remarks --- dpnp/dpnp_iface_manipulation.py | 4 +++- dpnp/linalg/dpnp_iface_linalg.py | 4 +++- tests/test_linalg.py | 2 -- tests/test_ndarray.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 0e88c3b64fb7..15bb9cf057fc 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1754,7 +1754,7 @@ def hstack(tup, *, dtype=None, casting="same_kind"): def matrix_transpose(x, /): """ - Transposes a matrix (or a stack of matrices) ``x``. + Transposes a matrix (or a stack of matrices) `x`. For full documentation refer to :obj:`numpy.matrix_transpose`. @@ -1773,6 +1773,8 @@ def matrix_transpose(x, /): See Also -------- :obj:`dpnp.transpose` : Returns an array with axes transposed. + :obj:`dpnp.linalg.matrix_transpose` : Equivalent function. + :obj:`dpnp.ndarray.mT` : Equivalent method. Examples -------- diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index 844e5fd61197..bba00dacbb39 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -875,7 +875,7 @@ def matrix_rank(A, tol=None, hermitian=False): def matrix_transpose(x, /): """ - Transposes a matrix (or a stack of matrices) ``x``. + Transposes a matrix (or a stack of matrices) `x`. For full documentation refer to :obj:`numpy.linalg.matrix_transpose`. @@ -894,6 +894,8 @@ def matrix_transpose(x, /): See Also -------- :obj:`dpnp.transpose` : Returns an array with axes transposed. + :obj:`dpnp.matrix_transpose` : Equivalent function. + :obj:`dpnp.ndarray.mT` : Equivalent method. Examples -------- diff --git a/tests/test_linalg.py b/tests/test_linalg.py index 3b965540f88b..74e09576cf6b 100644 --- a/tests/test_linalg.py +++ b/tests/test_linalg.py @@ -2127,8 +2127,6 @@ def test_matrix_transpose(): ): inp.linalg.matrix_transpose(a_dp[:, 0]) - assert_raises(ValueError, inp.linalg.matrix_transpose, a_dp[:, 0]) - class TestNorm: def setup_method(self): diff --git a/tests/test_ndarray.py b/tests/test_ndarray.py index f4444db8736b..44e514f0f74b 100644 --- a/tests/test_ndarray.py +++ b/tests/test_ndarray.py @@ -259,7 +259,7 @@ def test_array_as_index(shape, index_dtype): assert a[tuple(ind_arr)] == a[1] -# numpy.matrix_transpose() is available since numpy >= 2.0 +# numpy.ndarray.mT is available since numpy >= 2.0 @testing.with_requires("numpy>=2.0") @pytest.mark.parametrize( "shape", From 9b62161ed8d4f0d2fb4a4a4ab955101c1699bd1a Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 14 Oct 2024 11:29:26 +0200 Subject: [PATCH 10/12] Reuse dpctl for matrix_transpose --- dpnp/dpnp_array.py | 2 +- dpnp/dpnp_iface_manipulation.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dpnp/dpnp_array.py b/dpnp/dpnp_array.py index 6bed096d6d01..1c05d7807fa0 100644 --- a/dpnp/dpnp_array.py +++ b/dpnp/dpnp_array.py @@ -149,7 +149,7 @@ def mT(self): if self.ndim < 2: raise ValueError("matrix transpose with ndim < 2 is undefined") - return self.swapaxes(-1, -2) + return self._array_obj.mT def to_device(self, target_device): """Transfer array to target device.""" diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index ae52bd8d4df7..c03419278b63 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1799,7 +1799,10 @@ def matrix_transpose(x, /): raise ValueError( f"Input array must be at least 2-dimensional, but it is {x.ndim}" ) - return dpnp.swapaxes(x, -1, -2) + + usm_x = dpnp.get_usm_ndarray(x) + usm_res = dpt.matrix_transpose(usm_x) + return dpnp_array._create_from_usm_ndarray(usm_res) def moveaxis(a, source, destination): From cd3983c624d6a927dc13804d601b25521fd7c213 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 14 Oct 2024 11:30:39 +0200 Subject: [PATCH 11/12] Apply review remarks --- doc/reference/ndarray.rst | 1 + dpnp/dpnp_iface_manipulation.py | 2 +- dpnp/linalg/dpnp_iface_linalg.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/reference/ndarray.rst b/doc/reference/ndarray.rst index 5eadd5b9aba7..c130bcf6c5f7 100644 --- a/doc/reference/ndarray.rst +++ b/doc/reference/ndarray.rst @@ -92,6 +92,7 @@ Other attributes :nosignatures: dpnp.ndarray.T + dpnp.ndarray.mT dpnp.ndarray.real dpnp.ndarray.imag dpnp.ndarray.flat diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index c03419278b63..f8c896b91b6f 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1762,7 +1762,7 @@ def matrix_transpose(x, /): Parameters ---------- x : (..., M, N) {dpnp.ndarray, usm_ndarray} - Input array with ``x.ndim >= 2` and whose two innermost + Input array with ``x.ndim >= 2`` and whose two innermost dimensions form ``MxN`` matrices. Returns diff --git a/dpnp/linalg/dpnp_iface_linalg.py b/dpnp/linalg/dpnp_iface_linalg.py index bba00dacbb39..b875e6b92c90 100644 --- a/dpnp/linalg/dpnp_iface_linalg.py +++ b/dpnp/linalg/dpnp_iface_linalg.py @@ -882,7 +882,7 @@ def matrix_transpose(x, /): Parameters ---------- x : (..., M, N) {dpnp.ndarray, usm_ndarray} - Input array with ``x.ndim >= 2` and whose two innermost + Input array with ``x.ndim >= 2`` and whose two innermost dimensions form ``MxN`` matrices. Returns From 4c083520ffe64e7e631c6ea736740515731fede7 Mon Sep 17 00:00:00 2001 From: Vladislav Perevezentsev Date: Mon, 14 Oct 2024 12:29:27 +0200 Subject: [PATCH 12/12] Update dpnp.matrix_transpose() --- dpnp/dpnp_iface_manipulation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 62c7fbba5661..fb58d1e4b48d 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1794,13 +1794,13 @@ def matrix_transpose(x, /): """ - dpnp.check_supported_arrays_type(x) - if x.ndim < 2: + usm_x = dpnp.get_usm_ndarray(x) + if usm_x.ndim < 2: raise ValueError( - f"Input array must be at least 2-dimensional, but it is {x.ndim}" + "Input array must be at least 2-dimensional, " + f"but it is {usm_x.ndim}" ) - usm_x = dpnp.get_usm_ndarray(x) usm_res = dpt.matrix_transpose(usm_x) return dpnp_array._create_from_usm_ndarray(usm_res)