Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PROPACK to scipy.sparse.linalg documentation, tests #18

Merged
merged 3 commits into from
Jun 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions doc/source/reference/sparse.linalg.svds-propack.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. _sparse.linalg.svds-propack:

svds(solver='propack')
----------------------------------------

.. scipy-optimize:function:: scipy.sparse.linalg.svds
:impl: scipy.sparse.linalg.eigen._svds_doc._svds_propack_doc
:method: propack
39 changes: 29 additions & 10 deletions scipy/sparse/linalg/eigen/_svds.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,23 @@ def svds(A, k=6, ncv=None, tol=0, which='LM', v0=None,
ncv : int, optional
When ``solver='arpack'``, this is the number of Lanczos vectors
generated. See :ref:`'arpack' <sparse.linalg.svds-arpack>` for details.
When ``solver='lobpcg'``, this parameter is ignored.
When ``solver='lobpcg'`` or ``solver='propack'``, this parameter is
ignored.
tol : float, optional
Tolerance for singular values. Zero (default) means machine precision.
which : {'LM', 'SM'}
Which `k` singular values to find: either the largest magnitude ('LM')
or smallest magnitude ('SM') singular values.
v0 : ndarray, optional
The starting vector for iteration; see method-specific
documentation (:ref:`'arpack' <sparse.linalg.svds-arpack>` or
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`) for details.
documentation (:ref:`'arpack' <sparse.linalg.svds-arpack>`,
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`), or
:ref:`'propack' <sparse.linalg.svds-propack>` for details.
maxiter : int, optional
Maximum number of iterations; see method-specific
documentation (:ref:`'arpack' <sparse.linalg.svds-arpack>` or
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`) for details.
documentation (:ref:`'arpack' <sparse.linalg.svds-arpack>`,
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`), or
:ref:`'propack' <sparse.linalg.svds-propack>` for details.
return_singular_vectors : bool or str, optional
Singular values are always computed and returned; this parameter
controls the computation and return of singular vectors.
Expand All @@ -93,8 +96,9 @@ def svds(A, k=6, ncv=None, tol=0, which='LM', v0=None,

solver : str, optional
The solver used.
:ref:`'arpack' <sparse.linalg.svds-arpack>` and
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>` are supported.
:ref:`'arpack' <sparse.linalg.svds-arpack>`,
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`, and
:ref:`'propack' <sparse.linalg.svds-propack>` are supported.
Default: `'arpack'`.
options : dict, optional
A dictionary of solver-specific options. No solver-specific options
Expand Down Expand Up @@ -166,8 +170,8 @@ def svds(A, k=6, ncv=None, tol=0, which='LM', v0=None,
else:
raise ValueError("which must be either 'LM' or 'SM'.")

if (not (isinstance(A, LinearOperator) or isspmatrix(A)
or is_pydata_spmatrix(A))):
if not (isinstance(A, LinearOperator) or isspmatrix(A)
or is_pydata_spmatrix(A)):
A = np.asarray(A)

n, m = A.shape
Expand Down Expand Up @@ -221,12 +225,27 @@ def matmat_XH_X(x):
eigvals, eigvec = lobpcg(XH_X, X, tol=tol ** 2, maxiter=maxiter,
largest=largest)

elif solver == 'propack':
from scipy.sparse.linalg import svdp
jobu = ((isinstance(return_singular_vectors, str)
and 'u' in return_singular_vectors)
or (isinstance(return_singular_vectors, bool)
and return_singular_vectors))
jobv = ((isinstance(return_singular_vectors, str)
and 'v' in return_singular_vectors)
or (isinstance(return_singular_vectors, bool)
and return_singular_vectors))
return svdp(A, k=k, tol=tol**2, which=which, maxiter=None,
compute_u=jobu, compute_v=jobv, irl_mode=True,
kmax=maxiter, v0=v0)

elif solver == 'arpack' or solver is None:
eigvals, eigvec = eigsh(XH_X, k=k, tol=tol ** 2, maxiter=maxiter,
ncv=ncv, which=which, v0=v0)

else:
raise ValueError("solver must be either 'arpack', or 'lobpcg'.")
raise ValueError("solver must be either 'arpack', 'lobpcg', or "
"'propack'.")

# Gramian matrices have real non-negative eigenvalues.
eigvals = np.maximum(eigvals.real, 0)
Expand Down
132 changes: 127 additions & 5 deletions scipy/sparse/linalg/eigen/_svds_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ def _svds_arpack_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,

solver : str, optional
This is the solver-specific documentation for ``solver='arpack'``.
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>` is also supported.
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>` and
:ref:`'propack' <sparse.linalg.svds-propack>`
are also supported.

Returns
-------
Expand All @@ -66,7 +68,7 @@ def _svds_arpack_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,

Notes
-----
This is a naive implementation using ARPACK or LOBPCG as an eigensolver
This is a naive implementation using ARPACK as an eigensolver
on ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on which one is more
efficient.

Expand Down Expand Up @@ -132,7 +134,7 @@ def _svds_lobpcg_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,
Number of singular values and singular vectors to compute.
Must satisfy ``1 <= k < min(M, N)``.
ncv : int, optional
Ignored; only used for ``solver='arpack'``.
Ignored.
tol : float, optional
Tolerance for singular values. Zero (default) means machine precision.
which : {'LM', 'SM'}
Expand All @@ -159,7 +161,9 @@ def _svds_lobpcg_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,

solver : str, optional
This is the solver-specific documentation for ``solver='lobpcg'``.
:ref:`'arpack' <sparse.linalg.svds-arpack>` is also supported.
:ref:`'arpack' <sparse.linalg.svds-arpack>` and
:ref:`'propack' <sparse.linalg.svds-propack>`
are also supported.

Returns
-------
Expand All @@ -176,7 +180,7 @@ def _svds_lobpcg_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,

Notes
-----
This is a naive implementation using ARPACK or LOBPCG as an eigensolver
This is a naive implementation using LOBPCG as an eigensolver
on ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on which one is more
efficient.

Expand Down Expand Up @@ -220,3 +224,121 @@ def _svds_lobpcg_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,
True
"""
pass


def _svds_propack_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,
maxiter=None, return_singular_vectors=True,
solver='propack'):
"""
Partial singular value decomposition of a sparse matrix using PROPACK.

Compute the largest or smallest `k` singular values and corresponding
singular vectors of a sparse matrix `A`. The order in which the singular
values are returned is not guaranteed.

In the descriptions below, let ``M, N = A.shape``.

Parameters
----------
A : sparse matrix or LinearOperator
Matrix to decompose. If `A` is a ``LinearOperator``
object, it must define both ``matvec`` and ``rmatvec`` methods.
k : int, default: 6
Number of singular values and singular vectors to compute.
Must satisfy ``1 <= k <= min(M, N)``.
ncv : int, optional
Ignored.
tol : float, optional
The desired relative accuracy for computed singular values.
Zero (default) means machine precision.
which : {'LM', 'SM'}
Which `k` singular values to find: either the largest magnitude ('LM')
or smallest magnitude ('SM') singular values. Note that choosing
``which='SM'`` will force the ``irl`` option to be set ``True``.
v0 : ndarray, optional
Starting vector for iterations: must be of length ``A.shape[0]``.
If not specified, PROPACK will generate a starting vector.
maxiter : int, optional
Maximum number of iterations / maximal dimension of the Krylov
subspace. Default is ``5 * k``.
return_singular_vectors : bool or str, default: True
Singular values are always computed and returned; this parameter
controls the computation and return of singular vectors.

- ``True``: return singular vectors.
- ``False``: do not return singular vectors.
- ``"u"``: only return the left singular values, without computing the
right singular vectors (if ``N > M``).
- ``"vh"``: only return the right singular values, without computing
the left singular vectors (if ``N <= M``).

solver : str, optional
This is the solver-specific documentation for ``solver='propack'``.
:ref:`'arpack' <sparse.linalg.svds-arpack>` and
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`
are also supported.

Returns
-------
u : ndarray, shape=(M, k)
Unitary matrix having left singular vectors as columns.
If `return_singular_vectors` is ``"vh"``, this variable is not
computed, and ``None`` is returned instead.
s : ndarray, shape=(k,)
The singular values.
vh : ndarray, shape=(k, N)
Unitary matrix having right singular vectors as rows.
If `return_singular_vectors` is ``"u"``, this variable is not computed,
and ``None`` is returned instead.

Notes
-----
This is an interface to the Fortran library PROPACK [1]_.

References
----------

.. [1] Larsen, Rasmus Munk. "PROPACK-Software for large and sparse SVD
calculations." Available online. URL
http://sun. stanford. edu/rmunk/PROPACK (2004): 2008-2009.

Examples
--------
Construct a matrix ``A`` from singular values and vectors.

>>> from scipy.stats import ortho_group
>>> from scipy.sparse import csc_matrix, diags
>>> from scipy.sparse.linalg import svds
>>> rng = np.random.default_rng()
>>> orthogonal = csc_matrix(ortho_group.rvs(10, random_state=rng))
>>> s = [0.0001, 0.001, 3, 4, 5] # singular values
>>> u = orthogonal[:, :5] # left singular vectors
>>> vT = orthogonal[:, 5:].T # right singular vectors
>>> A = u @ diags(s) @ vT

With only three singular values/vectors, the SVD approximates the original
matrix.

>>> u2, s2, vT2 = svds(A, k=3, solver='propack')
>>> A2 = u2 @ np.diag(s2) @ vT2
>>> np.allclose(A2, A.todense(), atol=1e-3)
True

With all five singular values/vectors, we can reproduce the original
matrix.

>>> u3, s3, vT3 = svds(A, k=5, solver='propack')
>>> A3 = u3 @ np.diag(s3) @ vT3
>>> np.allclose(A3, A.todense())
True

The singular values match the expected singular values, and the singular
values are as expected up to a difference in sign. Consequently, the
returned arrays of singular vectors must also be orthogonal.

>>> (np.allclose(s3, s) and
... np.allclose(np.abs(u3), np.abs(u.todense())) and
... np.allclose(np.abs(vT3), np.abs(vT.todense())))
True
"""
pass
45 changes: 26 additions & 19 deletions scipy/sparse/linalg/eigen/tests/test_svds.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def test_svd_simple_real():
[0, 0, 1, 0]], float)
z = csc_matrix(x)

for solver in [None, 'arpack', 'lobpcg']:
for solver in [None, 'arpack', 'lobpcg', 'propack']:
for m in [x.T, x, y, z, z.T]:
for k in range(1, min(m.shape)):
u, s, vh = sorted_svd(m, k)
Expand All @@ -66,7 +66,8 @@ def test_svd_simple_real():
m_hat = svd_estimate(u, s, vh)
sm_hat = svd_estimate(su, ss, svh)

assert_array_almost_equal_nulp(m_hat, sm_hat, nulp=1000)
assert_array_almost_equal_nulp(
m_hat, sm_hat, nulp=1000 if solver != 'propack' else 1116)


def test_svd_simple_complex():
Expand All @@ -80,7 +81,7 @@ def test_svd_simple_complex():
[0, 0, 1, 0]], complex)
z = csc_matrix(x)

for solver in [None, 'arpack', 'lobpcg']:
for solver in [None, 'arpack', 'lobpcg', 'propack']:
for m in [x, x.T.conjugate(), x.T, y, y.conjugate(), z, z.T]:
for k in range(1, min(m.shape) - 1):
u, s, vh = sorted_svd(m, k)
Expand All @@ -89,7 +90,8 @@ def test_svd_simple_complex():
m_hat = svd_estimate(u, s, vh)
sm_hat = svd_estimate(su, ss, svh)

assert_array_almost_equal_nulp(m_hat, sm_hat, nulp=1000)
assert_array_almost_equal_nulp(
m_hat, sm_hat, nulp=1000 if solver != 'propack' else 1575)


def test_svd_maxiter():
Expand All @@ -116,7 +118,7 @@ def test_svd_which():
x = hilbert(6)
for which in ['LM', 'SM']:
_, s, _ = sorted_svd(x, 2, which=which)
for solver in [None, 'arpack', 'lobpcg']:
for solver in [None, 'arpack', 'lobpcg', 'propack']:
ss = svds(x, 2, which=which, return_singular_vectors=False,
solver=solver)
ss.sort()
Expand All @@ -127,7 +129,7 @@ def test_svd_v0():
# check that the v0 parameter works as expected
x = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], float)

for solver in [None, 'arpack', 'lobpcg']:
for solver in [None, 'arpack', 'lobpcg', 'propack']:
u, s, vh = svds(x, 1, solver=solver)
u2, s2, vh2 = svds(x, 1, v0=u[:, 0], solver=solver)

Expand Down Expand Up @@ -164,7 +166,7 @@ def test_svd_LM_ones_matrix():
for n, m in (6, 5), (5, 5), (5, 6):
for t in float, complex:
A = np.ones((n, m), dtype=t)
for solver in [None, 'arpack', 'lobpcg']:
for solver in [None, 'arpack', 'lobpcg']: # propack fails
U, s, VH = svds(A, k, solver=solver)

# Check some generic properties of svd.
Expand All @@ -182,7 +184,7 @@ def test_svd_LM_zeros_matrix():
for n, m in (3, 4), (4, 4), (4, 3):
for t in float, complex:
A = np.zeros((n, m), dtype=t)
for solver in [None, 'arpack', 'lobpcg']:
for solver in [None, 'arpack', 'lobpcg', 'propack']:
U, s, VH = svds(A, k, solver=solver)

# Check some generic properties of svd.
Expand All @@ -198,7 +200,7 @@ def test_svd_LM_zeros_matrix_gh_3452():
# Note that for complex dype the size of this matrix is too small for k=1.
n, m, k = 4, 2, 1
A = np.zeros((n, m))
for solver in [None, 'arpack', 'lobpcg']:
for solver in [None, 'arpack', 'lobpcg', 'propack']:
U, s, VH = svds(A, k, solver=solver)

# Check some generic properties of svd.
Expand Down Expand Up @@ -238,9 +240,12 @@ def reorder(args):
A = np.random.RandomState(52).randn(n, m)
L = CheckingLinearOperator(A)

v0 = np.ones(min(A.shape))
for solver in [None, 'arpack', 'lobpcg', 'propack']:
if solver == 'propack':
v0 = np.ones(n)
else:
v0 = np.ones(min(A.shape))

for solver in [None, 'arpack', 'lobpcg']:
U1, s1, VH1 = reorder(svds(A, k, v0=v0, solver=solver))
U2, s2, VH2 = reorder(svds(L, k, v0=v0, solver=solver))

Expand All @@ -254,9 +259,13 @@ def reorder(args):
A = np.random.RandomState(1909).randn(n, m)
L = CheckingLinearOperator(A)

for solver in [None, 'arpack', 'lobpcg']:
U1, s1, VH1 = reorder(svds(A, k, which="SM", solver=solver))
U2, s2, VH2 = reorder(svds(L, k, which="SM", solver=solver))
for solver in [None, 'arpack', 'lobpcg', 'propack']:
# TODO: arpack crashes when v0=v0, which="SM"
kwargs = {'v0': v0} if solver not in {None, 'arpack'} else {}
U1, s1, VH1 = reorder(svds(A, k, which="SM", solver=solver,
**kwargs))
U2, s2, VH2 = reorder(svds(L, k, which="SM", solver=solver,
**kwargs))

assert_allclose(np.abs(U1), np.abs(U2))
assert_allclose(s1, s2)
Expand All @@ -271,11 +280,9 @@ def reorder(args):
A = (rng.randn(n, m) + 1j * rng.randn(n, m)).astype(dt)
L = CheckingLinearOperator(A)

for solver in [None, 'arpack', 'lobpcg']:
U1, s1, VH1 = reorder(svds(A, k, which="LM",
solver=solver))
U2, s2, VH2 = reorder(svds(L, k, which="LM",
solver=solver))
for solver in [None, 'arpack', 'lobpcg', 'propack']:
U1, s1, VH1 = reorder(svds(A, k, which="LM", solver=solver))
U2, s2, VH2 = reorder(svds(L, k, which="LM", solver=solver))

assert_allclose(np.abs(U1), np.abs(U2), rtol=eps)
assert_allclose(s1, s2, rtol=eps)
Expand Down