Skip to content

Commit f635bde

Browse files
Merge 9cda6f6 into 50a9a34
2 parents 50a9a34 + 9cda6f6 commit f635bde

File tree

6 files changed

+373
-0
lines changed

6 files changed

+373
-0
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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,3 +1167,148 @@ def test_svd_errors(self):
11671167
# a.ndim < 2
11681168
a_dp_ndim_1 = a_dp.flatten()
11691169
assert_raises(inp.linalg.LinAlgError, inp.linalg.svd, a_dp_ndim_1)
1170+
1171+
1172+
class TestPinv:
1173+
def get_tol(self, dtype):
1174+
tol = 1e-06
1175+
if dtype in (inp.float32, inp.complex64):
1176+
tol = 1e-04
1177+
elif not has_support_aspect64() and dtype in (
1178+
inp.int32,
1179+
inp.int64,
1180+
None,
1181+
):
1182+
tol = 1e-04
1183+
self._tol = tol
1184+
1185+
def check_types_shapes(self, dp_B, np_B):
1186+
if has_support_aspect64():
1187+
assert dp_B.dtype == np_B.dtype
1188+
else:
1189+
assert dp_B.dtype.kind == np_B.dtype.kind
1190+
1191+
assert dp_B.shape == np_B.shape
1192+
1193+
@pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True))
1194+
@pytest.mark.parametrize(
1195+
"shape",
1196+
[(2, 2), (3, 4), (5, 3), (16, 16), (2, 2, 2), (2, 4, 2), (2, 2, 4)],
1197+
ids=[
1198+
"(2, 2)",
1199+
"(3, 4)",
1200+
"(5, 3)",
1201+
"(16, 16)",
1202+
"(2, 2, 2)",
1203+
"(2, 4, 2)",
1204+
"(2, 2, 4)",
1205+
],
1206+
)
1207+
def test_pinv(self, dtype, shape):
1208+
a = numpy.random.rand(*shape).astype(dtype)
1209+
a_dp = inp.array(a)
1210+
1211+
B = numpy.linalg.pinv(a)
1212+
B_dp = inp.linalg.pinv(a_dp)
1213+
1214+
self.check_types_shapes(B_dp, B)
1215+
self.get_tol(dtype)
1216+
tol = self._tol
1217+
assert_allclose(B_dp, B, rtol=tol, atol=tol)
1218+
1219+
if a.ndim == 2:
1220+
reconstructed = inp.dot(a_dp, inp.dot(B_dp, a_dp))
1221+
else: # a.ndim > 2
1222+
reconstructed = inp.matmul(a_dp, inp.matmul(B_dp, a_dp))
1223+
1224+
assert_allclose(reconstructed, a_dp, rtol=tol, atol=tol)
1225+
1226+
@pytest.mark.parametrize("dtype", get_complex_dtypes())
1227+
@pytest.mark.parametrize(
1228+
"shape",
1229+
[(2, 2), (16, 16)],
1230+
ids=["(2,2)", "(16, 16)"],
1231+
)
1232+
def test_pinv_hermitian(self, dtype, shape):
1233+
a = numpy.random.randn(*shape) + 1j * numpy.random.randn(*shape)
1234+
a = numpy.conj(a.T) @ a
1235+
1236+
a = a.astype(dtype)
1237+
a_dp = inp.array(a)
1238+
1239+
B = numpy.linalg.pinv(a)
1240+
B_dp = inp.linalg.pinv(a_dp)
1241+
1242+
self.check_types_shapes(B_dp, B)
1243+
self.get_tol(dtype)
1244+
1245+
reconstructed = inp.dot(inp.dot(a_dp, B_dp), a_dp)
1246+
# TODO : calculation accuracy decreases for matrix shape (16,16)
1247+
# Find out why
1248+
assert_allclose(reconstructed, a_dp, rtol=1e-02, atol=1e-02)
1249+
1250+
@pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True))
1251+
@pytest.mark.parametrize(
1252+
"shape",
1253+
[(0, 0), (0, 2), (2, 0), (2, 0, 3), (2, 3, 0), (0, 2, 3)],
1254+
ids=[
1255+
"(0, 0)",
1256+
"(0, 2)",
1257+
"(2 ,0)",
1258+
"(2, 0, 3)",
1259+
"(2, 3, 0)",
1260+
"(0, 2, 3)",
1261+
],
1262+
)
1263+
def test_pinv_empty(self, dtype, shape):
1264+
a = numpy.empty(shape, dtype=dtype)
1265+
a_dp = inp.array(a)
1266+
1267+
B = numpy.linalg.pinv(a)
1268+
B_dp = inp.linalg.pinv(a_dp)
1269+
1270+
assert_dtype_allclose(B_dp, B)
1271+
1272+
def test_pinv_strides(self):
1273+
a = numpy.random.rand(5, 5)
1274+
a_dp = inp.array(a)
1275+
1276+
self.get_tol(a_dp.dtype)
1277+
tol = self._tol
1278+
1279+
# positive strides
1280+
B = numpy.linalg.pinv(a[::2, ::2])
1281+
B_dp = inp.linalg.pinv(a_dp[::2, ::2])
1282+
assert_allclose(B_dp, B, rtol=tol, atol=tol)
1283+
1284+
# negative strides
1285+
B = numpy.linalg.pinv(a[::-2, ::-2])
1286+
B_dp = inp.linalg.pinv(a_dp[::-2, ::-2])
1287+
assert_allclose(B_dp, B, rtol=tol, atol=tol)
1288+
1289+
def test_pinv_errors(self):
1290+
a_dp = inp.array([[1, 2], [3, 4]], dtype="float32")
1291+
1292+
# unsupported type `a`
1293+
a_np = inp.asnumpy(a_dp)
1294+
assert_raises(TypeError, inp.linalg.pinv, a_np)
1295+
1296+
# unsupported type `rcond`
1297+
rcond = numpy.array(0.5, dtype="float32")
1298+
assert_raises(TypeError, inp.linalg.pinv, a_dp, rcond)
1299+
assert_raises(TypeError, inp.linalg.pinv, a_dp, [0.5])
1300+
1301+
# non-broadcastable `rcond`
1302+
rcond_dp = inp.array([0.5], dtype="float32")
1303+
assert_raises(ValueError, inp.linalg.pinv, a_dp, rcond_dp)
1304+
1305+
# a.ndim < 2
1306+
a_dp_ndim_1 = a_dp.flatten()
1307+
assert_raises(inp.linalg.LinAlgError, inp.linalg.pinv, a_dp_ndim_1)
1308+
1309+
# diffetent queue
1310+
a_queue = dpctl.SyclQueue()
1311+
rcond_queue = dpctl.SyclQueue()
1312+
a_dp_q = inp.array(a_dp, sycl_queue=a_queue)
1313+
rcond_dp_q = inp.array([0.5], dtype="float32", sycl_queue=rcond_queue)
1314+
assert_raises(ValueError, inp.linalg.pinv, a_dp_q, rcond_dp_q)

tests/test_sycl_queue.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1665,3 +1665,63 @@ def test_slogdet(shape, is_empty, device):
16651665

16661666
assert_sycl_queue_equal(sign_queue, dpnp_x.sycl_queue)
16671667
assert_sycl_queue_equal(logdet_queue, dpnp_x.sycl_queue)
1668+
1669+
1670+
@pytest.mark.parametrize(
1671+
"shape, hermitian, rcond_as_array",
1672+
[
1673+
((4, 4), False, False),
1674+
((4, 4), False, True),
1675+
((2, 0), False, False),
1676+
((4, 4), True, False),
1677+
((4, 4), True, True),
1678+
((2, 2, 3), False, False),
1679+
((2, 2, 3), False, True),
1680+
((0, 2, 3), False, False),
1681+
((1, 0, 3), False, False),
1682+
],
1683+
ids=[
1684+
"(4, 4)",
1685+
"(4, 4), rcond_as_array",
1686+
"(2, 0)",
1687+
"(2, 2), hermitian)",
1688+
"(2, 2), hermitian, rcond_as_array)",
1689+
"(2, 2, 3)",
1690+
"(2, 2, 3), rcond_as_array",
1691+
"(0, 2, 3)",
1692+
"(1, 0, 3)",
1693+
],
1694+
)
1695+
@pytest.mark.parametrize(
1696+
"device",
1697+
valid_devices,
1698+
ids=[device.filter_string for device in valid_devices],
1699+
)
1700+
def test_pinv(shape, hermitian, rcond_as_array, device):
1701+
if hermitian:
1702+
a_np = numpy.random.randn(*shape) + 1j * numpy.random.randn(*shape)
1703+
a_np = numpy.conj(a_np.T) @ a_np
1704+
else:
1705+
a_np = numpy.random.randn(*shape)
1706+
1707+
a_dp = dpnp.array(a_np, device=device)
1708+
1709+
if rcond_as_array:
1710+
rcond_np = numpy.array(1e-15)
1711+
rcond_dp = dpnp.array(1e-15, device=device)
1712+
1713+
B_result = dpnp.linalg.pinv(a_dp, rcond=rcond_dp, hermitian=hermitian)
1714+
B_expected = numpy.linalg.pinv(
1715+
a_np, rcond=rcond_np, hermitian=hermitian
1716+
)
1717+
1718+
else:
1719+
# rcond == 1e-15 by default
1720+
B_result = dpnp.linalg.pinv(a_dp, hermitian=hermitian)
1721+
B_expected = numpy.linalg.pinv(a_np, hermitian=hermitian)
1722+
1723+
assert_allclose(B_expected, B_result, rtol=1e-3, atol=1e-4)
1724+
1725+
B_queue = B_result.sycl_queue
1726+
1727+
assert_sycl_queue_equal(B_queue, a_dp.sycl_queue)

tests/test_usm_type.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,39 @@ def test_svd(usm_type, shape, full_matrices_param, compute_uv_param):
829829
assert x.usm_type == s.usm_type
830830

831831

832+
@pytest.mark.parametrize("usm_type", list_of_usm_types, ids=list_of_usm_types)
833+
@pytest.mark.parametrize(
834+
"shape, hermitian",
835+
[
836+
((4, 4), False),
837+
((2, 0), False),
838+
((4, 4), True),
839+
((2, 2, 3), False),
840+
((0, 2, 3), False),
841+
((1, 0, 3), False),
842+
],
843+
ids=[
844+
"(4, 4)",
845+
"(2, 0)",
846+
"(2, 2), hermitian)",
847+
"(2, 2, 3)",
848+
"(0, 2, 3)",
849+
"(1, 0, 3)",
850+
],
851+
)
852+
def test_pinv(shape, hermitian, usm_type):
853+
if hermitian:
854+
a = dp.random.randn(*shape) + 1j * dp.random.randn(*shape)
855+
a = dp.conj(a.T) @ a
856+
else:
857+
a = dp.random.randn(*shape)
858+
859+
a = dp.array(a, usm_type=usm_type)
860+
B = dp.lialg.pinv(a, hermitian=hermitian)
861+
862+
assert a.usm_type == B.usm_type
863+
864+
832865
@pytest.mark.parametrize("usm_type", list_of_usm_types, ids=list_of_usm_types)
833866
@pytest.mark.parametrize(
834867
"shape",

0 commit comments

Comments
 (0)