Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1950828
Impl dpnp.linalg.pinv()
vlad-perevezentsev Feb 8, 2024
095da9a
Add cupy tests for dpnp.linalg.pinv()
vlad-perevezentsev Feb 8, 2024
cb22462
Merge master into impl_pinv
vlad-perevezentsev Feb 8, 2024
b716704
Add tests to test_sycl_queue and test_usm_type
vlad-perevezentsev Feb 9, 2024
d0bcbbd
Add TestPinv to test_linalg.py
vlad-perevezentsev Feb 9, 2024
0293a3f
Merge master into impl_pinv
vlad-perevezentsev Feb 9, 2024
b7ee111
Address remarks
vlad-perevezentsev Feb 12, 2024
7f03075
Add additional checks for rcond parameter
vlad-perevezentsev Feb 12, 2024
34aa37c
Add a more efficient implementation
vlad-perevezentsev Feb 12, 2024
0766347
Update test_sycl_queue
vlad-perevezentsev Feb 12, 2024
bad4ad1
Update TestPinv in test_linalg.py
vlad-perevezentsev Feb 12, 2024
826610b
Merge master into impl_pinv
vlad-perevezentsev Feb 12, 2024
358216a
Merge branch 'master' into impl_pinv
antonwolfy Feb 12, 2024
35dede1
Update tests/test_usm_type.py
antonwolfy Feb 12, 2024
e7b899a
Update test_pinv_hermitian
vlad-perevezentsev Feb 13, 2024
a7bb8e0
Update test_svd_hermitian
vlad-perevezentsev Feb 13, 2024
f9f8010
Merge origin/impl_pinv into impl_pinv
vlad-perevezentsev Feb 13, 2024
4f72d33
Merge master into impl_pinv
vlad-perevezentsev Feb 13, 2024
a198de0
Merge master into impl_pinv
vlad-perevezentsev Feb 14, 2024
16c292c
Use numpy.random.seed in test_linalg
vlad-perevezentsev Feb 14, 2024
c08ea5e
Use setup_method to rudece code duplication in test_linalg
vlad-perevezentsev Feb 14, 2024
a975fc4
Change the seed number
vlad-perevezentsev Feb 15, 2024
c9b8fff
Merge branch 'master' into impl_pinv
antonwolfy Feb 15, 2024
32f8f68
Use numpy.random.seed in test_usm_type and test_sycl_queue
vlad-perevezentsev Feb 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions dpnp/linalg/dpnp_iface_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
dpnp_det,
dpnp_eigh,
dpnp_inv,
dpnp_pinv,
dpnp_qr,
dpnp_slogdet,
dpnp_solve,
Expand All @@ -69,6 +70,7 @@
"matrix_rank",
"multi_dot",
"norm",
"pinv",
"qr",
"solve",
"svd",
Expand Down Expand Up @@ -474,6 +476,55 @@ def multi_dot(arrays, out=None):
return result


def pinv(a, rcond=1e-15, hermitian=False):
"""
Compute the (Moore-Penrose) pseudo-inverse of a matrix.

Calculate the generalized inverse of a matrix using its
singular-value decomposition (SVD) and including all large singular values.

For full documentation refer to :obj:`numpy.linalg.inv`.

Parameters
----------
a : (..., M, N) {dpnp.ndarray, usm_ndarray}
Matrix or stack of matrices to be pseudo-inverted.
rcond : float or dpnp.ndarray of float, optional
Cutoff for small singular values.
Singular values less than or equal to ``rcond * largest_singular_value``
are set to zero.
Default: ``1e-15``.
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
-------
out : (..., N, M) dpnp.ndarray
The pseudo-inverse of a.

Examples
--------
The following example checks that ``a * a+ * a == a`` and
``a+ * a * a+ == a+``:

>>> import dpnp as np
>>> a = np.random.randn(9, 6)
>>> B = np.linalg.pinv(a)
>>> np.allclose(a, np.dot(a, np.dot(B, a)))
array([ True])
>>> np.allclose(B, np.dot(B, np.dot(a, B)))
array([ True])

"""

dpnp.check_supported_arrays_type(a)
check_stacked_2d(a)

return dpnp_pinv(a, rcond=rcond, hermitian=hermitian)


def norm(x1, ord=None, axis=None, keepdims=False):
"""
Matrix or vector norm.
Expand Down
31 changes: 31 additions & 0 deletions dpnp/linalg/dpnp_utils_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dpnp_det",
"dpnp_eigh",
"dpnp_inv",
"dpnp_pinv",
"dpnp_qr",
"dpnp_slogdet",
"dpnp_solve",
Expand Down Expand Up @@ -998,6 +999,36 @@ def dpnp_inv(a):
return b_f


def dpnp_pinv(a, rcond=1e-15, hermitian=False):
"""
dpnp_pinv(a, rcond=1e-15, hermitian=False):

Compute the Moore-Penrose pseudoinverse of `a` matrix.

It computes a pseudoinverse of a matrix `a`, which is a generalization
of the inverse matrix with Singular Value Decomposition (SVD).

"""

rcond = dpnp.array(rcond, device=a.sycl_device, sycl_queue=a.sycl_queue)
if a.size == 0:
res_type = _common_type(a)
m, n = a.shape[-2:]
if m == 0 or n == 0:
res_type = a.dtype
return dpnp.empty_like(a, shape=(a.shape[:-2] + (n, m)), dtype=res_type)

u, s, vt = dpnp_svd(a.conj(), full_matrices=False, hermitian=hermitian)

# discard small singular values
cutoff = rcond * dpnp.amax(s, axis=-1)
leq = s <= cutoff[..., None]
dpnp.reciprocal(s, out=s)
s[leq] = 0

return dpnp.matmul(vt.swapaxes(-2, -1), s[..., None] * u.swapaxes(-2, -1))


def dpnp_qr_batch(a, mode="reduced"):
"""
dpnp_qr_batch(a, mode="reduced")
Expand Down
129 changes: 129 additions & 0 deletions tests/test_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1167,3 +1167,132 @@ def test_svd_errors(self):
# a.ndim < 2
a_dp_ndim_1 = a_dp.flatten()
assert_raises(inp.linalg.LinAlgError, inp.linalg.svd, a_dp_ndim_1)


class TestPinv:
def get_tol(self, dtype):
tol = 1e-06
if dtype in (inp.float32, inp.complex64):
tol = 1e-04
elif not has_support_aspect64() and dtype in (
inp.int32,
inp.int64,
None,
):
tol = 1e-05
self._tol = tol

def check_types_shapes(self, dp_B, np_B):
if has_support_aspect64():
assert dp_B.dtype == np_B.dtype
else:
assert dp_B.dtype.kind == np_B.dtype.kind

assert dp_B.shape == np_B.shape

@pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True))
@pytest.mark.parametrize(
"shape",
[(2, 2), (3, 4), (5, 3), (16, 16), (2, 2, 2), (2, 4, 2), (2, 2, 4)],
ids=[
"(2, 2)",
"(3, 4)",
"(5, 3)",
"(16, 16)",
"(2, 2, 2)",
"(2, 4, 2)",
"(2, 2, 4)",
],
)
def test_pinv(self, dtype, shape):
a = numpy.random.rand(*shape).astype(dtype)
a_dp = inp.array(a)

B = numpy.linalg.pinv(a)
B_dp = inp.linalg.pinv(a_dp)

self.check_types_shapes(B_dp, B)
self.get_tol(dtype)
tol = self._tol
assert_allclose(B_dp, B, rtol=tol, atol=tol)

if a.ndim == 2:
reconstructed = inp.dot(a_dp, inp.dot(B_dp, a_dp))
else: # a.ndim > 2
reconstructed = inp.matmul(a_dp, inp.matmul(B_dp, a_dp))

assert_allclose(reconstructed, a, rtol=tol, atol=tol)

@pytest.mark.parametrize("dtype", get_complex_dtypes())
@pytest.mark.parametrize(
"shape",
[(2, 2), (16, 16)],
ids=["(2,2)", "(16, 16)"],
)
def test_pinv_hermitian(self, dtype, shape):
a = numpy.random.randn(*shape) + 1j * numpy.random.randn(*shape)
a = numpy.conj(a.T) @ a

a = a.astype(dtype)
a_dp = inp.array(a)

B = numpy.linalg.pinv(a)
B_dp = inp.linalg.pinv(a_dp)

self.check_types_shapes(B_dp, B)
self.get_tol(dtype)
tol = self._tol
assert_allclose(B_dp, B, rtol=tol, atol=tol)

reconstructed = inp.dot(a_dp, inp.dot(B_dp, a_dp))
assert_allclose(reconstructed, a, rtol=tol, atol=1e-03)

@pytest.mark.parametrize("dtype", get_all_dtypes(no_bool=True))
@pytest.mark.parametrize(
"shape",
[(0, 0), (0, 2), (2, 0), (2, 0, 3), (2, 3, 0), (0, 2, 3)],
ids=[
"(0, 0)",
"(0, 2)",
"(2 ,0)",
"(2, 0, 3)",
"(2, 3, 0)",
"(0, 2, 3)",
],
)
def test_pinv_empty(self, dtype, shape):
a = numpy.empty(shape, dtype=dtype)
a_dp = inp.array(a)

B = numpy.linalg.pinv(a)
B_dp = inp.linalg.pinv(a_dp)

assert_dtype_allclose(B_dp, B)

def test_pinv_strides(self):
a = numpy.random.rand(5, 5)
a_dp = inp.array(a)

self.get_tol(a_dp.dtype)
tol = self._tol

# positive strides
B = numpy.linalg.pinv(a[::2, ::2])
B_dp = inp.linalg.pinv(a_dp[::2, ::2])
assert_allclose(B_dp, B, rtol=tol, atol=tol)

# negative strides
B = numpy.linalg.pinv(a[::-2, ::-2])
B_dp = inp.linalg.pinv(a_dp[::-2, ::-2])
assert_allclose(B_dp, B, rtol=tol, atol=tol)

def test_pinv_errors(self):
a_dp = inp.array([[1, 2], [3, 4]], dtype="float32")

# unsupported type
a_np = inp.asnumpy(a_dp)
assert_raises(TypeError, inp.linalg.pinv, a_np)

# a.ndim < 2
a_dp_ndim_1 = a_dp.flatten()
assert_raises(inp.linalg.LinAlgError, inp.linalg.pinv, a_dp_ndim_1)
42 changes: 42 additions & 0 deletions tests/test_sycl_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -1660,3 +1660,45 @@ def test_slogdet(shape, is_empty, device):

assert_sycl_queue_equal(sign_queue, dpnp_x.sycl_queue)
assert_sycl_queue_equal(logdet_queue, dpnp_x.sycl_queue)


@pytest.mark.parametrize(
"shape, hermitian",
[
((4, 4), False),
((2, 0), False),
((4, 4), True),
((2, 2, 3), False),
((0, 2, 3), False),
((1, 0, 3), False),
],
ids=[
"(4, 4)",
"(2, 0)",
"(2, 2), hermitian)",
"(2, 2, 3)",
"(0, 2, 3)",
"(1, 0, 3)",
],
)
@pytest.mark.parametrize(
"device",
valid_devices,
ids=[device.filter_string for device in valid_devices],
)
def test_pinv(shape, hermitian, device):
if hermitian:
a_np = numpy.random.randn(*shape) + 1j * numpy.random.randn(*shape)
a_np = numpy.conj(a_np.T) @ a_np
else:
a_np = numpy.random.randn(*shape)

a_dp = dpnp.array(a_np, device=device)

B_result = dpnp.linalg.pinv(a_dp, hermitian=hermitian)
B_expected = numpy.linalg.pinv(a_np, hermitian=hermitian)
assert_allclose(B_expected, B_result, rtol=1e-3, atol=1e-4)

B_queue = B_result.sycl_queue

assert_sycl_queue_equal(B_queue, a_dp.sycl_queue)
35 changes: 34 additions & 1 deletion tests/test_usm_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,39 @@ def test_svd(usm_type, shape, full_matrices_param, compute_uv_param):
assert x.usm_type == s.usm_type


@pytest.mark.parametrize("usm_type", list_of_usm_types, ids=list_of_usm_types)
@pytest.mark.parametrize(
"shape, hermitian",
[
((4, 4), False),
((2, 0), False),
((4, 4), True),
((2, 2, 3), False),
((0, 2, 3), False),
((1, 0, 3), False),
],
ids=[
"(4, 4)",
"(2, 0)",
"(2, 2), hermitian)",
"(2, 2, 3)",
"(0, 2, 3)",
"(1, 0, 3)",
],
)
def test_pinv(shape, hermitian, usm_type):
if hermitian:
a = dp.random.randn(*shape) + 1j * dp.random.randn(*shape)
a = dp.conj(a.T) @ a
else:
a = dp.random.randn(*shape)

a = dp.array(a, usm_type=usm_type)
B = dp.lialg.pinv(a, hermitian=hermitian)

assert a.usm_type == B.usm_type


@pytest.mark.parametrize("usm_type", list_of_usm_types, ids=list_of_usm_types)
@pytest.mark.parametrize(
"shape",
Expand All @@ -847,7 +880,7 @@ def test_svd(usm_type, shape, full_matrices_param, compute_uv_param):
["r", "raw", "complete", "reduced"],
ids=["r", "raw", "complete", "reduced"],
)
def test_qr(shape, mode, usm_type):
def test_pinv(shape, mode, usm_type):
count_elems = numpy.prod(shape)
a = dp.arange(count_elems, usm_type=usm_type).reshape(shape)

Expand Down
42 changes: 42 additions & 0 deletions tests/third_party/cupy/linalg_tests/test_solve.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,45 @@ def test_batched_inv(self, dtype):
assert a.ndim >= 3 # CuPy internally uses a batched function.
with pytest.raises(xp.linalg.LinAlgError):
xp.linalg.inv(a)


class TestPinv(unittest.TestCase):
@testing.for_dtypes("ifdFD")
@_condition.retry(10)
def check_x(self, a_shape, rcond, dtype):
a_gpu = testing.shaped_random(a_shape, dtype=dtype)
a_cpu = cupy.asnumpy(a_gpu)
a_gpu_copy = a_gpu.copy()
if not isinstance(rcond, float):
rcond = numpy.asarray(rcond)
result_cpu = numpy.linalg.pinv(a_cpu, rcond=rcond)
if not isinstance(rcond, float):
rcond = cupy.asarray(rcond)
result_gpu = cupy.linalg.pinv(a_gpu, rcond=rcond)

assert_dtype_allclose(result_gpu, result_cpu)
testing.assert_array_equal(a_gpu_copy, a_gpu)

def test_pinv(self):
self.check_x((3, 3), rcond=1e-15)
self.check_x((2, 4), rcond=1e-15)
self.check_x((3, 2), rcond=1e-15)

self.check_x((4, 4), rcond=0.3)
self.check_x((2, 5), rcond=0.5)
self.check_x((5, 3), rcond=0.6)

def test_pinv_batched(self):
self.check_x((2, 3, 4), rcond=1e-15)
self.check_x((2, 3, 4, 5), rcond=1e-15)

def test_pinv_batched_vector_rcond(self):
self.check_x((2, 3, 4), rcond=[0.2, 0.8])
self.check_x((2, 3, 4, 5), rcond=[[0.2, 0.9, 0.1], [0.7, 0.2, 0.5]])

def test_pinv_size_0(self):
self.check_x((3, 0), rcond=1e-15)
self.check_x((0, 3), rcond=1e-15)
self.check_x((0, 0), rcond=1e-15)
self.check_x((0, 2, 3), rcond=1e-15)
self.check_x((2, 0, 3), rcond=1e-15)