Skip to content

Commit 23bd001

Browse files
authored
Merge branch 'master' into matmul_cupy_test
2 parents ba7bed4 + 2c38ae8 commit 23bd001

File tree

6 files changed

+403
-8
lines changed

6 files changed

+403
-8
lines changed

dpnp/linalg/dpnp_iface_linalg.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
dpnp_det,
5252
dpnp_eigh,
5353
dpnp_inv,
54+
dpnp_pinv,
5455
dpnp_qr,
5556
dpnp_slogdet,
5657
dpnp_solve,
@@ -69,6 +70,7 @@
6970
"matrix_rank",
7071
"multi_dot",
7172
"norm",
73+
"pinv",
7274
"qr",
7375
"solve",
7476
"svd",
@@ -474,6 +476,56 @@ def multi_dot(arrays, out=None):
474476
return result
475477

476478

479+
def pinv(a, rcond=1e-15, hermitian=False):
480+
"""
481+
Compute the (Moore-Penrose) pseudo-inverse of a matrix.
482+
483+
Calculate the generalized inverse of a matrix using its
484+
singular-value decomposition (SVD) and including all large singular values.
485+
486+
For full documentation refer to :obj:`numpy.linalg.inv`.
487+
488+
Parameters
489+
----------
490+
a : (..., M, N) {dpnp.ndarray, usm_ndarray}
491+
Matrix or stack of matrices to be pseudo-inverted.
492+
rcond : {float, dpnp.ndarray, usm_ndarray}, optional
493+
Cutoff for small singular values.
494+
Singular values less than or equal to ``rcond * largest_singular_value``
495+
are set to zero. Broadcasts against the stack of matrices.
496+
Default: ``1e-15``.
497+
hermitian : bool, optional
498+
If ``True``, a is assumed to be Hermitian (symmetric if real-valued),
499+
enabling a more efficient method for finding singular values.
500+
Default: ``False``.
501+
502+
Returns
503+
-------
504+
out : (..., N, M) dpnp.ndarray
505+
The pseudo-inverse of a.
506+
507+
Examples
508+
--------
509+
The following example checks that ``a * a+ * a == a`` and
510+
``a+ * a * a+ == a+``:
511+
512+
>>> import dpnp as np
513+
>>> a = np.random.randn(9, 6)
514+
>>> B = np.linalg.pinv(a)
515+
>>> np.allclose(a, np.dot(a, np.dot(B, a)))
516+
array([ True])
517+
>>> np.allclose(B, np.dot(B, np.dot(a, B)))
518+
array([ True])
519+
520+
"""
521+
522+
dpnp.check_supported_arrays_type(a)
523+
dpnp.check_supported_arrays_type(rcond, scalar_type=True)
524+
check_stacked_2d(a)
525+
526+
return dpnp_pinv(a, rcond=rcond, hermitian=hermitian)
527+
528+
477529
def norm(x1, ord=None, axis=None, keepdims=False):
478530
"""
479531
Matrix or vector norm.

dpnp/linalg/dpnp_utils_linalg.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"dpnp_det",
4040
"dpnp_eigh",
4141
"dpnp_inv",
42+
"dpnp_pinv",
4243
"dpnp_qr",
4344
"dpnp_slogdet",
4445
"dpnp_solve",
@@ -998,6 +999,46 @@ def dpnp_inv(a):
998999
return b_f
9991000

10001001

1002+
def dpnp_pinv(a, rcond=1e-15, hermitian=False):
1003+
"""
1004+
dpnp_pinv(a, rcond=1e-15, hermitian=False):
1005+
1006+
Compute the Moore-Penrose pseudoinverse of `a` matrix.
1007+
1008+
It computes a pseudoinverse of a matrix `a`, which is a generalization
1009+
of the inverse matrix with Singular Value Decomposition (SVD).
1010+
1011+
"""
1012+
1013+
if a.size == 0:
1014+
m, n = a.shape[-2:]
1015+
if m == 0 or n == 0:
1016+
res_type = a.dtype
1017+
else:
1018+
res_type = _common_type(a)
1019+
return dpnp.empty_like(a, shape=(a.shape[:-2] + (n, m)), dtype=res_type)
1020+
1021+
if dpnp.is_supported_array_type(rcond):
1022+
# Check that `a` and `rcond` are allocated on the same device
1023+
# and have the same queue. Otherwise, `ValueError`` will be raised.
1024+
get_usm_allocations([a, rcond])
1025+
else:
1026+
# Allocate dpnp.ndarray if rcond is a scalar
1027+
rcond = dpnp.array(rcond, usm_type=a.usm_type, sycl_queue=a.sycl_queue)
1028+
1029+
u, s, vt = dpnp_svd(a.conj(), full_matrices=False, hermitian=hermitian)
1030+
1031+
# discard small singular values
1032+
cutoff = rcond * dpnp.max(s, axis=-1)
1033+
leq = s <= cutoff[..., None]
1034+
dpnp.reciprocal(s, out=s)
1035+
s[leq] = 0
1036+
1037+
u = u.swapaxes(-2, -1)
1038+
dpnp.multiply(s[..., None], u, out=u)
1039+
return dpnp.matmul(vt.swapaxes(-2, -1), u)
1040+
1041+
10011042
def dpnp_qr_batch(a, mode="reduced"):
10021043
"""
10031044
dpnp_qr_batch(a, mode="reduced")

tests/test_linalg.py

Lines changed: 173 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .helper import (
1515
assert_dtype_allclose,
1616
get_all_dtypes,
17-
get_complex_dtypes,
17+
get_float_complex_dtypes,
1818
has_support_aspect64,
1919
is_cpu_device,
2020
)
@@ -678,6 +678,11 @@ def test_norm3(array, ord, axis):
678678

679679

680680
class TestQr:
681+
# Set numpy.random.seed for test methods to prevent
682+
# random generation of the input singular matrix
683+
def setup_method(self):
684+
numpy.random.seed(81)
685+
681686
# TODO: New packages that fix issue CMPLRLLVM-53771 are only available in internal CI.
682687
# Skip the tests on cpu until these packages are available for the external CI.
683688
# Specifically dpcpp_linux-64>=2024.1.0
@@ -702,7 +707,9 @@ class TestQr:
702707
ids=["r", "raw", "complete", "reduced"],
703708
)
704709
def test_qr(self, dtype, shape, mode):
705-
a = numpy.random.rand(*shape).astype(dtype)
710+
a = numpy.random.randn(*shape).astype(dtype)
711+
if numpy.issubdtype(dtype, numpy.complexfloating):
712+
a += 1j * numpy.random.randn(*shape)
706713
ia = inp.array(a)
707714

708715
if mode == "r":
@@ -772,7 +779,7 @@ def test_qr_empty(self, dtype, shape, mode):
772779
ids=["r", "raw", "complete", "reduced"],
773780
)
774781
def test_qr_strides(self, mode):
775-
a = numpy.random.rand(5, 5)
782+
a = numpy.random.randn(5, 5)
776783
ia = inp.array(a)
777784

778785
# positive strides
@@ -1032,6 +1039,11 @@ def test_slogdet_errors(self):
10321039

10331040

10341041
class TestSvd:
1042+
# Set numpy.random.seed for test methods to prevent
1043+
# random generation of the input singular matrix
1044+
def setup_method(self):
1045+
numpy.random.seed(81)
1046+
10351047
def get_tol(self, dtype):
10361048
tol = 1e-06
10371049
if dtype in (inp.float32, inp.complex64):
@@ -1121,18 +1133,19 @@ def test_svd(self, dtype, shape):
11211133
dp_a, dp_u, dp_s, dp_vt, np_u, np_s, np_vt, True
11221134
)
11231135

1124-
@pytest.mark.parametrize("dtype", get_complex_dtypes())
1136+
@pytest.mark.parametrize("dtype", get_float_complex_dtypes())
11251137
@pytest.mark.parametrize("compute_vt", [True, False], ids=["True", "False"])
11261138
@pytest.mark.parametrize(
11271139
"shape",
11281140
[(2, 2), (16, 16)],
1129-
ids=["(2,2)", "(16, 16)"],
1141+
ids=["(2, 2)", "(16, 16)"],
11301142
)
11311143
def test_svd_hermitian(self, dtype, compute_vt, shape):
1132-
a = numpy.random.randn(*shape) + 1j * numpy.random.randn(*shape)
1133-
a = numpy.conj(a.T) @ a
1144+
a = numpy.random.randn(*shape).astype(dtype)
1145+
if numpy.issubdtype(dtype, numpy.complexfloating):
1146+
a += 1j * numpy.random.randn(*shape)
1147+
a = (a + a.conj().T) / 2
11341148

1135-
a = a.astype(dtype)
11361149
dp_a = inp.array(a)
11371150

11381151
if compute_vt:
@@ -1167,3 +1180,155 @@ def test_svd_errors(self):
11671180
# a.ndim < 2
11681181
a_dp_ndim_1 = a_dp.flatten()
11691182
assert_raises(inp.linalg.LinAlgError, inp.linalg.svd, a_dp_ndim_1)
1183+
1184+
1185+
class TestPinv:
1186+
# Set numpy.random.seed for test methods to prevent
1187+
# random generation of the input singular matrix
1188+
def setup_method(self):
1189+
numpy.random.seed(81)
1190+
1191+
def get_tol(self, dtype):
1192+
tol = 1e-06
1193+
if dtype in (inp.float32, inp.complex64):
1194+
tol = 1e-04
1195+
elif not has_support_aspect64() and dtype in (
1196+
inp.int32,
1197+
inp.int64,
1198+
None,
1199+
):
1200+
tol = 1e-04
1201+
self._tol = tol
1202+
1203+
def check_types_shapes(self, dp_B, np_B):
1204+
if has_support_aspect64():
1205+
assert dp_B.dtype == np_B.dtype
1206+
else:
1207+
assert dp_B.dtype.kind == np_B.dtype.kind
1208+
1209+
assert dp_B.shape == np_B.shape
1210+
1211+
@pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True))
1212+
@pytest.mark.parametrize(
1213+
"shape",
1214+
[(2, 2), (3, 4), (5, 3), (16, 16), (2, 2, 2), (2, 4, 2), (2, 2, 4)],
1215+
ids=[
1216+
"(2, 2)",
1217+
"(3, 4)",
1218+
"(5, 3)",
1219+
"(16, 16)",
1220+
"(2, 2, 2)",
1221+
"(2, 4, 2)",
1222+
"(2, 2, 4)",
1223+
],
1224+
)
1225+
def test_pinv(self, dtype, shape):
1226+
a = numpy.random.randn(*shape).astype(dtype)
1227+
if numpy.issubdtype(dtype, numpy.complexfloating):
1228+
a += 1j * numpy.random.randn(*shape)
1229+
a_dp = inp.array(a)
1230+
1231+
B = numpy.linalg.pinv(a)
1232+
B_dp = inp.linalg.pinv(a_dp)
1233+
1234+
self.check_types_shapes(B_dp, B)
1235+
self.get_tol(dtype)
1236+
tol = self._tol
1237+
assert_allclose(B_dp, B, rtol=tol, atol=tol)
1238+
1239+
if a.ndim == 2:
1240+
reconstructed = inp.dot(a_dp, inp.dot(B_dp, a_dp))
1241+
else: # a.ndim > 2
1242+
reconstructed = inp.matmul(a_dp, inp.matmul(B_dp, a_dp))
1243+
1244+
assert_allclose(reconstructed, a_dp, rtol=tol, atol=tol)
1245+
1246+
@pytest.mark.parametrize("dtype", get_float_complex_dtypes())
1247+
@pytest.mark.parametrize(
1248+
"shape",
1249+
[(2, 2), (16, 16)],
1250+
ids=["(2, 2)", "(16, 16)"],
1251+
)
1252+
def test_pinv_hermitian(self, dtype, shape):
1253+
a = numpy.random.randn(*shape).astype(dtype)
1254+
if numpy.issubdtype(dtype, numpy.complexfloating):
1255+
a += 1j * numpy.random.randn(*shape)
1256+
a = (a + a.conj().T) / 2
1257+
1258+
a_dp = inp.array(a)
1259+
1260+
B = numpy.linalg.pinv(a, hermitian=True)
1261+
B_dp = inp.linalg.pinv(a_dp, hermitian=True)
1262+
1263+
self.check_types_shapes(B_dp, B)
1264+
self.get_tol(dtype)
1265+
tol = self._tol
1266+
1267+
reconstructed = inp.dot(inp.dot(a_dp, B_dp), a_dp)
1268+
assert_allclose(reconstructed, a_dp, rtol=tol, atol=tol)
1269+
1270+
@pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True))
1271+
@pytest.mark.parametrize(
1272+
"shape",
1273+
[(0, 0), (0, 2), (2, 0), (2, 0, 3), (2, 3, 0), (0, 2, 3)],
1274+
ids=[
1275+
"(0, 0)",
1276+
"(0, 2)",
1277+
"(2 ,0)",
1278+
"(2, 0, 3)",
1279+
"(2, 3, 0)",
1280+
"(0, 2, 3)",
1281+
],
1282+
)
1283+
def test_pinv_empty(self, dtype, shape):
1284+
a = numpy.empty(shape, dtype=dtype)
1285+
a_dp = inp.array(a)
1286+
1287+
B = numpy.linalg.pinv(a)
1288+
B_dp = inp.linalg.pinv(a_dp)
1289+
1290+
assert_dtype_allclose(B_dp, B)
1291+
1292+
def test_pinv_strides(self):
1293+
a = numpy.random.randn(5, 5)
1294+
a_dp = inp.array(a)
1295+
1296+
self.get_tol(a_dp.dtype)
1297+
tol = self._tol
1298+
1299+
# positive strides
1300+
B = numpy.linalg.pinv(a[::2, ::2])
1301+
B_dp = inp.linalg.pinv(a_dp[::2, ::2])
1302+
assert_allclose(B_dp, B, rtol=tol, atol=tol)
1303+
1304+
# negative strides
1305+
B = numpy.linalg.pinv(a[::-2, ::-2])
1306+
B_dp = inp.linalg.pinv(a_dp[::-2, ::-2])
1307+
assert_allclose(B_dp, B, rtol=tol, atol=tol)
1308+
1309+
def test_pinv_errors(self):
1310+
a_dp = inp.array([[1, 2], [3, 4]], dtype="float32")
1311+
1312+
# unsupported type `a`
1313+
a_np = inp.asnumpy(a_dp)
1314+
assert_raises(TypeError, inp.linalg.pinv, a_np)
1315+
1316+
# unsupported type `rcond`
1317+
rcond = numpy.array(0.5, dtype="float32")
1318+
assert_raises(TypeError, inp.linalg.pinv, a_dp, rcond)
1319+
assert_raises(TypeError, inp.linalg.pinv, a_dp, [0.5])
1320+
1321+
# non-broadcastable `rcond`
1322+
rcond_dp = inp.array([0.5], dtype="float32")
1323+
assert_raises(ValueError, inp.linalg.pinv, a_dp, rcond_dp)
1324+
1325+
# a.ndim < 2
1326+
a_dp_ndim_1 = a_dp.flatten()
1327+
assert_raises(inp.linalg.LinAlgError, inp.linalg.pinv, a_dp_ndim_1)
1328+
1329+
# diffetent queue
1330+
a_queue = dpctl.SyclQueue()
1331+
rcond_queue = dpctl.SyclQueue()
1332+
a_dp_q = inp.array(a_dp, sycl_queue=a_queue)
1333+
rcond_dp_q = inp.array([0.5], dtype="float32", sycl_queue=rcond_queue)
1334+
assert_raises(ValueError, inp.linalg.pinv, a_dp_q, rcond_dp_q)

0 commit comments

Comments
 (0)