some new features

This commit is contained in:
ilgazca
2025-07-30 17:09:11 +03:00
parent db5d46760a
commit 8019bd3b7c
20616 changed files with 4375466 additions and 8 deletions

View File

@ -0,0 +1,22 @@
"""
Sparse Eigenvalue Solvers
-------------------------
The submodules of sparse.linalg._eigen:
1. lobpcg: Locally Optimal Block Preconditioned Conjugate Gradient Method
"""
from .arpack import *
from .lobpcg import *
from ._svds import svds
from . import arpack
__all__ = [
'ArpackError', 'ArpackNoConvergence',
'eigs', 'eigsh', 'lobpcg', 'svds'
]
from scipy._lib._testutils import PytestTester
test = PytestTester(__name__)
del PytestTester

View File

@ -0,0 +1,546 @@
import math
import numpy as np
from .arpack import _arpack # type: ignore[attr-defined]
from . import eigsh
from scipy._lib._util import check_random_state
from scipy.sparse.linalg._interface import LinearOperator, aslinearoperator
from scipy.sparse.linalg._eigen.lobpcg import lobpcg # type: ignore[no-redef]
from scipy.sparse.linalg._svdp import _svdp
from scipy.linalg import svd
arpack_int = _arpack.timing.nbx.dtype
__all__ = ['svds']
def _herm(x):
return x.T.conj()
def _iv(A, k, ncv, tol, which, v0, maxiter,
return_singular, solver, random_state):
# input validation/standardization for `solver`
# out of order because it's needed for other parameters
solver = str(solver).lower()
solvers = {"arpack", "lobpcg", "propack"}
if solver not in solvers:
raise ValueError(f"solver must be one of {solvers}.")
# input validation/standardization for `A`
A = aslinearoperator(A) # this takes care of some input validation
if not (np.issubdtype(A.dtype, np.complexfloating)
or np.issubdtype(A.dtype, np.floating)):
message = "`A` must be of floating or complex floating data type."
raise ValueError(message)
if math.prod(A.shape) == 0:
message = "`A` must not be empty."
raise ValueError(message)
# input validation/standardization for `k`
kmax = min(A.shape) if solver == 'propack' else min(A.shape) - 1
if int(k) != k or not (0 < k <= kmax):
message = "`k` must be an integer satisfying `0 < k < min(A.shape)`."
raise ValueError(message)
k = int(k)
# input validation/standardization for `ncv`
if solver == "arpack" and ncv is not None:
if int(ncv) != ncv or not (k < ncv < min(A.shape)):
message = ("`ncv` must be an integer satisfying "
"`k < ncv < min(A.shape)`.")
raise ValueError(message)
ncv = int(ncv)
# input validation/standardization for `tol`
if tol < 0 or not np.isfinite(tol):
message = "`tol` must be a non-negative floating point value."
raise ValueError(message)
tol = float(tol)
# input validation/standardization for `which`
which = str(which).upper()
whichs = {'LM', 'SM'}
if which not in whichs:
raise ValueError(f"`which` must be in {whichs}.")
# input validation/standardization for `v0`
if v0 is not None:
v0 = np.atleast_1d(v0)
if not (np.issubdtype(v0.dtype, np.complexfloating)
or np.issubdtype(v0.dtype, np.floating)):
message = ("`v0` must be of floating or complex floating "
"data type.")
raise ValueError(message)
shape = (A.shape[0],) if solver == 'propack' else (min(A.shape),)
if v0.shape != shape:
message = f"`v0` must have shape {shape}."
raise ValueError(message)
# input validation/standardization for `maxiter`
if maxiter is not None and (int(maxiter) != maxiter or maxiter <= 0):
message = "`maxiter` must be a positive integer."
raise ValueError(message)
maxiter = int(maxiter) if maxiter is not None else maxiter
# input validation/standardization for `return_singular_vectors`
# not going to be flexible with this; too complicated for little gain
rs_options = {True, False, "vh", "u"}
if return_singular not in rs_options:
raise ValueError(f"`return_singular_vectors` must be in {rs_options}.")
random_state = check_random_state(random_state)
return (A, k, ncv, tol, which, v0, maxiter,
return_singular, solver, random_state)
def svds(A, k=6, ncv=None, tol=0, which='LM', v0=None,
maxiter=None, return_singular_vectors=True,
solver='arpack', random_state=None, options=None):
"""
Partial singular value decomposition of a sparse matrix.
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 : ndarray, sparse matrix, or LinearOperator
Matrix to decompose of a floating point numeric dtype.
k : int, default: 6
Number of singular values and singular vectors to compute.
Must satisfy ``1 <= k <= kmax``, where ``kmax=min(M, N)`` for
``solver='propack'`` and ``kmax=min(M, N) - 1`` otherwise.
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'`` 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>`,
: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>`,
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`), or
:ref:`'propack' <sparse.linalg.svds-propack>` for details.
return_singular_vectors : {True, False, "u", "vh"}
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"``: if ``M <= N``, compute only the left singular vectors and
return ``None`` for the right singular vectors. Otherwise, compute
all singular vectors.
- ``"vh"``: if ``M > N``, compute only the right singular vectors and
return ``None`` for the left singular vectors. Otherwise, compute
all singular vectors.
If ``solver='propack'``, the option is respected regardless of the
matrix shape.
solver : {'arpack', 'propack', 'lobpcg'}, optional
The solver used.
:ref:`'arpack' <sparse.linalg.svds-arpack>`,
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`, and
:ref:`'propack' <sparse.linalg.svds-propack>` are supported.
Default: `'arpack'`.
random_state : {None, int, `numpy.random.Generator`,
`numpy.random.RandomState`}, optional
Pseudorandom number generator state used to generate resamples.
If `random_state` is ``None`` (or `np.random`), the
`numpy.random.RandomState` singleton is used.
If `random_state` is an int, a new ``RandomState`` instance is used,
seeded with `random_state`.
If `random_state` is already a ``Generator`` or ``RandomState``
instance then that instance is used.
options : dict, optional
A dictionary of solver-specific options. No solver-specific options
are currently supported; this parameter is reserved for future use.
Returns
-------
u : ndarray, shape=(M, k)
Unitary matrix having left singular vectors as columns.
s : ndarray, shape=(k,)
The singular values.
vh : ndarray, shape=(k, N)
Unitary matrix having right singular vectors as rows.
Notes
-----
This is a naive implementation using ARPACK or LOBPCG as an eigensolver
on the matrix ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on
which one is smaller size, followed by the Rayleigh-Ritz method
as postprocessing; see
Using the normal matrix, in Rayleigh-Ritz method, (2022, Nov. 19),
Wikipedia, https://w.wiki/4zms.
Alternatively, the PROPACK solver can be called.
Choices of the input matrix `A` numeric dtype may be limited.
Only ``solver="lobpcg"`` supports all floating point dtypes
real: 'np.float32', 'np.float64', 'np.longdouble' and
complex: 'np.complex64', 'np.complex128', 'np.clongdouble'.
The ``solver="arpack"`` supports only
'np.float32', 'np.float64', and 'np.complex128'.
Examples
--------
Construct a matrix `A` from singular values and vectors.
>>> import numpy as np
>>> from scipy import sparse, linalg, stats
>>> from scipy.sparse.linalg import svds, aslinearoperator, LinearOperator
Construct a dense matrix `A` from singular values and vectors.
>>> rng = np.random.default_rng(258265244568965474821194062361901728911)
>>> orthogonal = stats.ortho_group.rvs(10, random_state=rng)
>>> s = [1e-3, 1, 2, 3, 4] # non-zero singular values
>>> u = orthogonal[:, :5] # left singular vectors
>>> vT = orthogonal[:, 5:].T # right singular vectors
>>> A = u @ np.diag(s) @ vT
With only four singular values/vectors, the SVD approximates the original
matrix.
>>> u4, s4, vT4 = svds(A, k=4)
>>> A4 = u4 @ np.diag(s4) @ vT4
>>> np.allclose(A4, A, atol=1e-3)
True
With all five non-zero singular values/vectors, we can reproduce
the original matrix more accurately.
>>> u5, s5, vT5 = svds(A, k=5)
>>> A5 = u5 @ np.diag(s5) @ vT5
>>> np.allclose(A5, A)
True
The singular values match the expected singular values.
>>> np.allclose(s5, s)
True
Since the singular values are not close to each other in this example,
every singular vector matches as expected up to a difference in sign.
>>> (np.allclose(np.abs(u5), np.abs(u)) and
... np.allclose(np.abs(vT5), np.abs(vT)))
True
The singular vectors are also orthogonal.
>>> (np.allclose(u5.T @ u5, np.eye(5)) and
... np.allclose(vT5 @ vT5.T, np.eye(5)))
True
If there are (nearly) multiple singular values, the corresponding
individual singular vectors may be unstable, but the whole invariant
subspace containing all such singular vectors is computed accurately
as can be measured by angles between subspaces via 'subspace_angles'.
>>> rng = np.random.default_rng(178686584221410808734965903901790843963)
>>> s = [1, 1 + 1e-6] # non-zero singular values
>>> u, _ = np.linalg.qr(rng.standard_normal((99, 2)))
>>> v, _ = np.linalg.qr(rng.standard_normal((99, 2)))
>>> vT = v.T
>>> A = u @ np.diag(s) @ vT
>>> A = A.astype(np.float32)
>>> u2, s2, vT2 = svds(A, k=2, random_state=rng)
>>> np.allclose(s2, s)
True
The angles between the individual exact and computed singular vectors
may not be so small. To check use:
>>> (linalg.subspace_angles(u2[:, :1], u[:, :1]) +
... linalg.subspace_angles(u2[:, 1:], u[:, 1:]))
array([0.06562513]) # may vary
>>> (linalg.subspace_angles(vT2[:1, :].T, vT[:1, :].T) +
... linalg.subspace_angles(vT2[1:, :].T, vT[1:, :].T))
array([0.06562507]) # may vary
As opposed to the angles between the 2-dimensional invariant subspaces
that these vectors span, which are small for rights singular vectors
>>> linalg.subspace_angles(u2, u).sum() < 1e-6
True
as well as for left singular vectors.
>>> linalg.subspace_angles(vT2.T, vT.T).sum() < 1e-6
True
The next example follows that of 'sklearn.decomposition.TruncatedSVD'.
>>> rng = np.random.RandomState(0)
>>> X_dense = rng.random(size=(100, 100))
>>> X_dense[:, 2 * np.arange(50)] = 0
>>> X = sparse.csr_matrix(X_dense)
>>> _, singular_values, _ = svds(X, k=5, random_state=rng)
>>> print(singular_values)
[ 4.3293... 4.4491... 4.5420... 4.5987... 35.2410...]
The function can be called without the transpose of the input matrix
ever explicitly constructed.
>>> rng = np.random.default_rng(102524723947864966825913730119128190974)
>>> G = sparse.rand(8, 9, density=0.5, random_state=rng)
>>> Glo = aslinearoperator(G)
>>> _, singular_values_svds, _ = svds(Glo, k=5, random_state=rng)
>>> _, singular_values_svd, _ = linalg.svd(G.toarray())
>>> np.allclose(singular_values_svds, singular_values_svd[-4::-1])
True
The most memory efficient scenario is where neither
the original matrix, nor its transpose, is explicitly constructed.
Our example computes the smallest singular values and vectors
of 'LinearOperator' constructed from the numpy function 'np.diff' used
column-wise to be consistent with 'LinearOperator' operating on columns.
>>> diff0 = lambda a: np.diff(a, axis=0)
Let us create the matrix from 'diff0' to be used for validation only.
>>> n = 5 # The dimension of the space.
>>> M_from_diff0 = diff0(np.eye(n))
>>> print(M_from_diff0.astype(int))
[[-1 1 0 0 0]
[ 0 -1 1 0 0]
[ 0 0 -1 1 0]
[ 0 0 0 -1 1]]
The matrix 'M_from_diff0' is bi-diagonal and could be alternatively
created directly by
>>> M = - np.eye(n - 1, n, dtype=int)
>>> np.fill_diagonal(M[:,1:], 1)
>>> np.allclose(M, M_from_diff0)
True
Its transpose
>>> print(M.T)
[[-1 0 0 0]
[ 1 -1 0 0]
[ 0 1 -1 0]
[ 0 0 1 -1]
[ 0 0 0 1]]
can be viewed as the incidence matrix; see
Incidence matrix, (2022, Nov. 19), Wikipedia, https://w.wiki/5YXU,
of a linear graph with 5 vertices and 4 edges. The 5x5 normal matrix
``M.T @ M`` thus is
>>> print(M.T @ M)
[[ 1 -1 0 0 0]
[-1 2 -1 0 0]
[ 0 -1 2 -1 0]
[ 0 0 -1 2 -1]
[ 0 0 0 -1 1]]
the graph Laplacian, while the actually used in 'svds' smaller size
4x4 normal matrix ``M @ M.T``
>>> print(M @ M.T)
[[ 2 -1 0 0]
[-1 2 -1 0]
[ 0 -1 2 -1]
[ 0 0 -1 2]]
is the so-called edge-based Laplacian; see
Symmetric Laplacian via the incidence matrix, in Laplacian matrix,
(2022, Nov. 19), Wikipedia, https://w.wiki/5YXW.
The 'LinearOperator' setup needs the options 'rmatvec' and 'rmatmat'
of multiplication by the matrix transpose ``M.T``, but we want to be
matrix-free to save memory, so knowing how ``M.T`` looks like, we
manually construct the following function to be
used in ``rmatmat=diff0t``.
>>> def diff0t(a):
... if a.ndim == 1:
... a = a[:,np.newaxis] # Turn 1D into 2D array
... d = np.zeros((a.shape[0] + 1, a.shape[1]), dtype=a.dtype)
... d[0, :] = - a[0, :]
... d[1:-1, :] = a[0:-1, :] - a[1:, :]
... d[-1, :] = a[-1, :]
... return d
We check that our function 'diff0t' for the matrix transpose is valid.
>>> np.allclose(M.T, diff0t(np.eye(n-1)))
True
Now we setup our matrix-free 'LinearOperator' called 'diff0_func_aslo'
and for validation the matrix-based 'diff0_matrix_aslo'.
>>> def diff0_func_aslo_def(n):
... return LinearOperator(matvec=diff0,
... matmat=diff0,
... rmatvec=diff0t,
... rmatmat=diff0t,
... shape=(n - 1, n))
>>> diff0_func_aslo = diff0_func_aslo_def(n)
>>> diff0_matrix_aslo = aslinearoperator(M_from_diff0)
And validate both the matrix and its transpose in 'LinearOperator'.
>>> np.allclose(diff0_func_aslo(np.eye(n)),
... diff0_matrix_aslo(np.eye(n)))
True
>>> np.allclose(diff0_func_aslo.T(np.eye(n-1)),
... diff0_matrix_aslo.T(np.eye(n-1)))
True
Having the 'LinearOperator' setup validated, we run the solver.
>>> n = 100
>>> diff0_func_aslo = diff0_func_aslo_def(n)
>>> u, s, vT = svds(diff0_func_aslo, k=3, which='SM')
The singular values squared and the singular vectors are known
explicitly; see
Pure Dirichlet boundary conditions, in
Eigenvalues and eigenvectors of the second derivative,
(2022, Nov. 19), Wikipedia, https://w.wiki/5YX6,
since 'diff' corresponds to first
derivative, and its smaller size n-1 x n-1 normal matrix
``M @ M.T`` represent the discrete second derivative with the Dirichlet
boundary conditions. We use these analytic expressions for validation.
>>> se = 2. * np.sin(np.pi * np.arange(1, 4) / (2. * n))
>>> ue = np.sqrt(2 / n) * np.sin(np.pi * np.outer(np.arange(1, n),
... np.arange(1, 4)) / n)
>>> np.allclose(s, se, atol=1e-3)
True
>>> print(np.allclose(np.abs(u), np.abs(ue), atol=1e-6))
True
"""
args = _iv(A, k, ncv, tol, which, v0, maxiter, return_singular_vectors,
solver, random_state)
(A, k, ncv, tol, which, v0, maxiter,
return_singular_vectors, solver, random_state) = args
largest = (which == 'LM')
n, m = A.shape
if n >= m:
X_dot = A.matvec
X_matmat = A.matmat
XH_dot = A.rmatvec
XH_mat = A.rmatmat
transpose = False
else:
X_dot = A.rmatvec
X_matmat = A.rmatmat
XH_dot = A.matvec
XH_mat = A.matmat
transpose = True
dtype = getattr(A, 'dtype', None)
if dtype is None:
dtype = A.dot(np.zeros([m, 1])).dtype
def matvec_XH_X(x):
return XH_dot(X_dot(x))
def matmat_XH_X(x):
return XH_mat(X_matmat(x))
XH_X = LinearOperator(matvec=matvec_XH_X, dtype=A.dtype,
matmat=matmat_XH_X,
shape=(min(A.shape), min(A.shape)))
# Get a low rank approximation of the implicitly defined gramian matrix.
# This is not a stable way to approach the problem.
if solver == 'lobpcg':
if k == 1 and v0 is not None:
X = np.reshape(v0, (-1, 1))
else:
X = random_state.standard_normal(size=(min(A.shape), k))
_, eigvec = lobpcg(XH_X, X, tol=tol ** 2, maxiter=maxiter,
largest=largest)
elif solver == 'propack':
jobu = return_singular_vectors in {True, 'u'}
jobv = return_singular_vectors in {True, 'vh'}
irl_mode = (which == 'SM')
res = _svdp(A, k=k, tol=tol**2, which=which, maxiter=None,
compute_u=jobu, compute_v=jobv, irl_mode=irl_mode,
kmax=maxiter, v0=v0, random_state=random_state)
u, s, vh, _ = res # but we'll ignore bnd, the last output
# PROPACK order appears to be largest first. `svds` output order is not
# guaranteed, according to documentation, but for ARPACK and LOBPCG
# they actually are ordered smallest to largest, so reverse for
# consistency.
s = s[::-1]
u = u[:, ::-1]
vh = vh[::-1]
u = u if jobu else None
vh = vh if jobv else None
if return_singular_vectors:
return u, s, vh
else:
return s
elif solver == 'arpack' or solver is None:
if v0 is None:
v0 = random_state.standard_normal(size=(min(A.shape),))
_, eigvec = eigsh(XH_X, k=k, tol=tol ** 2, maxiter=maxiter,
ncv=ncv, which=which, v0=v0)
# arpack do not guarantee exactly orthonormal eigenvectors
# for clustered eigenvalues, especially in complex arithmetic
eigvec, _ = np.linalg.qr(eigvec)
# the eigenvectors eigvec must be orthonomal here; see gh-16712
Av = X_matmat(eigvec)
if not return_singular_vectors:
s = svd(Av, compute_uv=False, overwrite_a=True)
return s[::-1]
# compute the left singular vectors of X and update the right ones
# accordingly
u, s, vh = svd(Av, full_matrices=False, overwrite_a=True)
u = u[:, ::-1]
s = s[::-1]
vh = vh[::-1]
jobu = return_singular_vectors in {True, 'u'}
jobv = return_singular_vectors in {True, 'vh'}
if transpose:
u_tmp = eigvec @ _herm(vh) if jobu else None
vh = _herm(u) if jobv else None
u = u_tmp
else:
if not jobu:
u = None
vh = vh @ _herm(eigvec) if jobv else None
return u, s, vh

View File

@ -0,0 +1,400 @@
def _svds_arpack_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,
maxiter=None, return_singular_vectors=True,
solver='arpack', random_state=None):
"""
Partial singular value decomposition of a sparse matrix using ARPACK.
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.
k : int, optional
Number of singular values and singular vectors to compute.
Must satisfy ``1 <= k <= min(M, N) - 1``.
Default is 6.
ncv : int, optional
The number of Lanczos vectors generated.
The default is ``min(n, max(2*k + 1, 20))``.
If specified, must satistify ``k + 1 < ncv < min(M, N)``; ``ncv > 2*k``
is recommended.
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:
an (approximate) left singular vector if ``N > M`` and a right singular
vector otherwise. Must be of length ``min(M, N)``.
Default: random
maxiter : int, optional
Maximum number of Arnoldi update iterations allowed;
default is ``min(M, N) * 10``.
return_singular_vectors : {True, False, "u", "vh"}
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"``: if ``M <= N``, compute only the left singular vectors and
return ``None`` for the right singular vectors. Otherwise, compute
all singular vectors.
- ``"vh"``: if ``M > N``, compute only the right singular vectors and
return ``None`` for the left singular vectors. Otherwise, compute
all singular vectors.
solver : {'arpack', 'propack', 'lobpcg'}, optional
This is the solver-specific documentation for ``solver='arpack'``.
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>` and
:ref:`'propack' <sparse.linalg.svds-propack>`
are also supported.
random_state : {None, int, `numpy.random.Generator`,
`numpy.random.RandomState`}, optional
Pseudorandom number generator state used to generate resamples.
If `random_state` is ``None`` (or `np.random`), the
`numpy.random.RandomState` singleton is used.
If `random_state` is an int, a new ``RandomState`` instance is used,
seeded with `random_state`.
If `random_state` is already a ``Generator`` or ``RandomState``
instance then that instance is used.
options : dict, optional
A dictionary of solver-specific options. No solver-specific options
are currently supported; this parameter is reserved for future use.
Returns
-------
u : ndarray, shape=(M, k)
Unitary matrix having left singular vectors as columns.
s : ndarray, shape=(k,)
The singular values.
vh : ndarray, shape=(k, N)
Unitary matrix having right singular vectors as rows.
Notes
-----
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.
Examples
--------
Construct a matrix ``A`` from singular values and vectors.
>>> import numpy as np
>>> 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='arpack')
>>> A2 = u2 @ np.diag(s2) @ vT2
>>> np.allclose(A2, A.toarray(), atol=1e-3)
True
With all five singular values/vectors, we can reproduce the original
matrix.
>>> u3, s3, vT3 = svds(A, k=5, solver='arpack')
>>> A3 = u3 @ np.diag(s3) @ vT3
>>> np.allclose(A3, A.toarray())
True
The singular values match the expected singular values, and the singular
vectors are as expected up to a difference in sign.
>>> (np.allclose(s3, s) and
... np.allclose(np.abs(u3), np.abs(u.toarray())) and
... np.allclose(np.abs(vT3), np.abs(vT.toarray())))
True
The singular vectors are also orthogonal.
>>> (np.allclose(u3.T @ u3, np.eye(5)) and
... np.allclose(vT3 @ vT3.T, np.eye(5)))
True
"""
pass
def _svds_lobpcg_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,
maxiter=None, return_singular_vectors=True,
solver='lobpcg', random_state=None):
"""
Partial singular value decomposition of a sparse matrix using LOBPCG.
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.
k : int, default: 6
Number of singular values and singular vectors to compute.
Must satisfy ``1 <= k <= min(M, N) - 1``.
ncv : int, optional
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
If `k` is 1, the starting vector for iteration:
an (approximate) left singular vector if ``N > M`` and a right singular
vector otherwise. Must be of length ``min(M, N)``.
Ignored otherwise.
Default: random
maxiter : int, default: 20
Maximum number of iterations.
return_singular_vectors : {True, False, "u", "vh"}
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"``: if ``M <= N``, compute only the left singular vectors and
return ``None`` for the right singular vectors. Otherwise, compute
all singular vectors.
- ``"vh"``: if ``M > N``, compute only the right singular vectors and
return ``None`` for the left singular vectors. Otherwise, compute
all singular vectors.
solver : {'arpack', 'propack', 'lobpcg'}, optional
This is the solver-specific documentation for ``solver='lobpcg'``.
:ref:`'arpack' <sparse.linalg.svds-arpack>` and
:ref:`'propack' <sparse.linalg.svds-propack>`
are also supported.
random_state : {None, int, `numpy.random.Generator`,
`numpy.random.RandomState`}, optional
Pseudorandom number generator state used to generate resamples.
If `random_state` is ``None`` (or `np.random`), the
`numpy.random.RandomState` singleton is used.
If `random_state` is an int, a new ``RandomState`` instance is used,
seeded with `random_state`.
If `random_state` is already a ``Generator`` or ``RandomState``
instance then that instance is used.
options : dict, optional
A dictionary of solver-specific options. No solver-specific options
are currently supported; this parameter is reserved for future use.
Returns
-------
u : ndarray, shape=(M, k)
Unitary matrix having left singular vectors as columns.
s : ndarray, shape=(k,)
The singular values.
vh : ndarray, shape=(k, N)
Unitary matrix having right singular vectors as rows.
Notes
-----
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.
Examples
--------
Construct a matrix ``A`` from singular values and vectors.
>>> import numpy as np
>>> 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='lobpcg')
>>> A2 = u2 @ np.diag(s2) @ vT2
>>> np.allclose(A2, A.toarray(), atol=1e-3)
True
With all five singular values/vectors, we can reproduce the original
matrix.
>>> u3, s3, vT3 = svds(A, k=5, solver='lobpcg')
>>> A3 = u3 @ np.diag(s3) @ vT3
>>> np.allclose(A3, A.toarray())
True
The singular values match the expected singular values, and the singular
vectors are as expected up to a difference in sign.
>>> (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
The singular vectors are also orthogonal.
>>> (np.allclose(u3.T @ u3, np.eye(5)) and
... np.allclose(vT3 @ vT3.T, np.eye(5)))
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', random_state=None):
"""
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 ``10 * k``.
return_singular_vectors : {True, False, "u", "vh"}
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"``: compute only the left singular vectors; return ``None`` for
the right singular vectors.
- ``"vh"``: compute only the right singular vectors; return ``None``
for the left singular vectors.
solver : {'arpack', 'propack', 'lobpcg'}, 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.
random_state : {None, int, `numpy.random.Generator`,
`numpy.random.RandomState`}, optional
Pseudorandom number generator state used to generate resamples.
If `random_state` is ``None`` (or `np.random`), the
`numpy.random.RandomState` singleton is used.
If `random_state` is an int, a new ``RandomState`` instance is used,
seeded with `random_state`.
If `random_state` is already a ``Generator`` or ``RandomState``
instance then that instance is used.
options : dict, optional
A dictionary of solver-specific options. No solver-specific options
are currently supported; this parameter is reserved for future use.
Returns
-------
u : ndarray, shape=(M, k)
Unitary matrix having left singular vectors as columns.
s : ndarray, shape=(k,)
The singular values.
vh : ndarray, shape=(k, N)
Unitary matrix having right singular vectors as rows.
Notes
-----
This is an interface to the Fortran library PROPACK [1]_.
The current default is to run with IRL mode disabled unless seeking the
smallest singular values/vectors (``which='SM'``).
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.
>>> import numpy as np
>>> 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
vectors are as expected up to a difference in sign.
>>> (np.allclose(s3, s) and
... np.allclose(np.abs(u3), np.abs(u.toarray())) and
... np.allclose(np.abs(vT3), np.abs(vT.toarray())))
True
The singular vectors are also orthogonal.
>>> (np.allclose(u3.T @ u3, np.eye(5)) and
... np.allclose(vT3 @ vT3.T, np.eye(5)))
True
"""
pass

View File

@ -0,0 +1,45 @@
BSD Software License
Pertains to ARPACK and P_ARPACK
Copyright (c) 1996-2008 Rice University.
Developed by D.C. Sorensen, R.B. Lehoucq, C. Yang, and K. Maschhoff.
All rights reserved.
Arpack has been renamed to arpack-ng.
Copyright (c) 2001-2011 - Scilab Enterprises
Updated by Allan Cornet, Sylvestre Ledru.
Copyright (c) 2010 - Jordi Gutiérrez Hermoso (Octave patch)
Copyright (c) 2007 - Sébastien Fabbro (gentoo patch)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer listed
in this license in the documentation and/or other materials
provided with the distribution.
- Neither the name of the copyright holders nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,20 @@
"""
Eigenvalue solver using iterative methods.
Find k eigenvectors and eigenvalues of a matrix A using the
Arnoldi/Lanczos iterative methods from ARPACK [1]_,[2]_.
These methods are most useful for large sparse matrices.
- eigs(A,k)
- eigsh(A,k)
References
----------
.. [1] ARPACK Software, http://www.caam.rice.edu/software/ARPACK/
.. [2] R. B. Lehoucq, D. C. Sorensen, and C. Yang, ARPACK USERS GUIDE:
Solution of Large Scale Eigenvalue Problems by Implicitly Restarted
Arnoldi Methods. SIAM, Philadelphia, PA, 1998.
"""
from .arpack import *

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,718 @@
__usage__ = """
To run tests locally:
python tests/test_arpack.py [-l<int>] [-v<int>]
"""
import threading
import itertools
import numpy as np
from numpy.testing import assert_allclose, assert_equal, suppress_warnings
from pytest import raises as assert_raises
import pytest
from numpy import dot, conj, random
from scipy.linalg import eig, eigh
from scipy.sparse import csc_matrix, csr_matrix, diags, rand
from scipy.sparse.linalg import LinearOperator, aslinearoperator
from scipy.sparse.linalg._eigen.arpack import (eigs, eigsh, arpack,
ArpackNoConvergence)
from scipy._lib._gcutils import assert_deallocated, IS_PYPY
# precision for tests
_ndigits = {'f': 3, 'd': 11, 'F': 3, 'D': 11}
def _get_test_tolerance(type_char, mattype=None, D_type=None, which=None):
"""
Return tolerance values suitable for a given test:
Parameters
----------
type_char : {'f', 'd', 'F', 'D'}
Data type in ARPACK eigenvalue problem
mattype : {csr_matrix, aslinearoperator, asarray}, optional
Linear operator type
Returns
-------
tol
Tolerance to pass to the ARPACK routine
rtol
Relative tolerance for outputs
atol
Absolute tolerance for outputs
"""
rtol = {'f': 3000 * np.finfo(np.float32).eps,
'F': 3000 * np.finfo(np.float32).eps,
'd': 2000 * np.finfo(np.float64).eps,
'D': 2000 * np.finfo(np.float64).eps}[type_char]
atol = rtol
tol = 0
if mattype is aslinearoperator and type_char in ('f', 'F'):
# iterative methods in single precision: worse errors
# also: bump ARPACK tolerance so that the iterative method converges
tol = 30 * np.finfo(np.float32).eps
rtol *= 5
if mattype is csr_matrix and type_char in ('f', 'F'):
# sparse in single precision: worse errors
rtol *= 5
if (
which in ('LM', 'SM', 'LA')
and D_type.name == "gen-hermitian-Mc"
):
if type_char == 'F':
# missing case 1, 2, and more, from PR 14798
rtol *= 5
if type_char == 'D':
# missing more cases, from PR 14798
rtol *= 10
atol *= 10
return tol, rtol, atol
def generate_matrix(N, complex_=False, hermitian=False,
pos_definite=False, sparse=False):
M = np.random.random((N, N))
if complex_:
M = M + 1j * np.random.random((N, N))
if hermitian:
if pos_definite:
if sparse:
i = np.arange(N)
j = np.random.randint(N, size=N-2)
i, j = np.meshgrid(i, j)
M[i, j] = 0
M = np.dot(M.conj(), M.T)
else:
M = np.dot(M.conj(), M.T)
if sparse:
i = np.random.randint(N, size=N * N // 4)
j = np.random.randint(N, size=N * N // 4)
ind = np.nonzero(i == j)
j[ind] = (j[ind] + 1) % N
M[i, j] = 0
M[j, i] = 0
else:
if sparse:
i = np.random.randint(N, size=N * N // 2)
j = np.random.randint(N, size=N * N // 2)
M[i, j] = 0
return M
def generate_matrix_symmetric(N, pos_definite=False, sparse=False):
M = np.random.random((N, N))
M = 0.5 * (M + M.T) # Make M symmetric
if pos_definite:
Id = N * np.eye(N)
if sparse:
M = csr_matrix(M)
M += Id
else:
if sparse:
M = csr_matrix(M)
return M
def assert_allclose_cc(actual, desired, **kw):
"""Almost equal or complex conjugates almost equal"""
try:
assert_allclose(actual, desired, **kw)
except AssertionError:
assert_allclose(actual, conj(desired), **kw)
def argsort_which(eigenvalues, typ, k, which,
sigma=None, OPpart=None, mode=None):
"""Return sorted indices of eigenvalues using the "which" keyword
from eigs and eigsh"""
if sigma is None:
reval = np.round(eigenvalues, decimals=_ndigits[typ])
else:
if mode is None or mode == 'normal':
if OPpart is None:
reval = 1. / (eigenvalues - sigma)
elif OPpart == 'r':
reval = 0.5 * (1. / (eigenvalues - sigma)
+ 1. / (eigenvalues - np.conj(sigma)))
elif OPpart == 'i':
reval = -0.5j * (1. / (eigenvalues - sigma)
- 1. / (eigenvalues - np.conj(sigma)))
elif mode == 'cayley':
reval = (eigenvalues + sigma) / (eigenvalues - sigma)
elif mode == 'buckling':
reval = eigenvalues / (eigenvalues - sigma)
else:
raise ValueError("mode='%s' not recognized" % mode)
reval = np.round(reval, decimals=_ndigits[typ])
if which in ['LM', 'SM']:
ind = np.argsort(abs(reval))
elif which in ['LR', 'SR', 'LA', 'SA', 'BE']:
ind = np.argsort(np.real(reval))
elif which in ['LI', 'SI']:
# for LI,SI ARPACK returns largest,smallest abs(imaginary) why?
if typ.islower():
ind = np.argsort(abs(np.imag(reval)))
else:
ind = np.argsort(np.imag(reval))
else:
raise ValueError("which='%s' is unrecognized" % which)
if which in ['LM', 'LA', 'LR', 'LI']:
return ind[-k:]
elif which in ['SM', 'SA', 'SR', 'SI']:
return ind[:k]
elif which == 'BE':
return np.concatenate((ind[:k//2], ind[k//2-k:]))
def eval_evec(symmetric, d, typ, k, which, v0=None, sigma=None,
mattype=np.asarray, OPpart=None, mode='normal'):
general = ('bmat' in d)
if symmetric:
eigs_func = eigsh
else:
eigs_func = eigs
if general:
err = ("error for {}:general, typ={}, which={}, sigma={}, "
"mattype={}, OPpart={}, mode={}".format(eigs_func.__name__,
typ, which, sigma,
mattype.__name__,
OPpart, mode))
else:
err = ("error for {}:standard, typ={}, which={}, sigma={}, "
"mattype={}, OPpart={}, mode={}".format(eigs_func.__name__,
typ, which, sigma,
mattype.__name__,
OPpart, mode))
a = d['mat'].astype(typ)
ac = mattype(a)
if general:
b = d['bmat'].astype(typ)
bc = mattype(b)
# get exact eigenvalues
exact_eval = d['eval'].astype(typ.upper())
ind = argsort_which(exact_eval, typ, k, which,
sigma, OPpart, mode)
exact_eval = exact_eval[ind]
# compute arpack eigenvalues
kwargs = dict(which=which, v0=v0, sigma=sigma)
if eigs_func is eigsh:
kwargs['mode'] = mode
else:
kwargs['OPpart'] = OPpart
# compute suitable tolerances
kwargs['tol'], rtol, atol = _get_test_tolerance(typ, mattype, d, which)
# on rare occasions, ARPACK routines return results that are proper
# eigenvalues and -vectors, but not necessarily the ones requested in
# the parameter which. This is inherent to the Krylov methods, and
# should not be treated as a failure. If such a rare situation
# occurs, the calculation is tried again (but at most a few times).
ntries = 0
while ntries < 5:
# solve
if general:
try:
eigenvalues, evec = eigs_func(ac, k, bc, **kwargs)
except ArpackNoConvergence:
kwargs['maxiter'] = 20*a.shape[0]
eigenvalues, evec = eigs_func(ac, k, bc, **kwargs)
else:
try:
eigenvalues, evec = eigs_func(ac, k, **kwargs)
except ArpackNoConvergence:
kwargs['maxiter'] = 20*a.shape[0]
eigenvalues, evec = eigs_func(ac, k, **kwargs)
ind = argsort_which(eigenvalues, typ, k, which,
sigma, OPpart, mode)
eigenvalues = eigenvalues[ind]
evec = evec[:, ind]
try:
# check eigenvalues
assert_allclose_cc(eigenvalues, exact_eval, rtol=rtol, atol=atol,
err_msg=err)
check_evecs = True
except AssertionError:
check_evecs = False
ntries += 1
if check_evecs:
# check eigenvectors
LHS = np.dot(a, evec)
if general:
RHS = eigenvalues * np.dot(b, evec)
else:
RHS = eigenvalues * evec
assert_allclose(LHS, RHS, rtol=rtol, atol=atol, err_msg=err)
break
# check eigenvalues
assert_allclose_cc(eigenvalues, exact_eval, rtol=rtol, atol=atol, err_msg=err)
class DictWithRepr(dict):
def __init__(self, name):
self.name = name
def __repr__(self):
return "<%s>" % self.name
class SymmetricParams:
def __init__(self):
self.eigs = eigsh
self.which = ['LM', 'SM', 'LA', 'SA', 'BE']
self.mattypes = [csr_matrix, aslinearoperator, np.asarray]
self.sigmas_modes = {None: ['normal'],
0.5: ['normal', 'buckling', 'cayley']}
# generate matrices
# these should all be float32 so that the eigenvalues
# are the same in float32 and float64
N = 6
np.random.seed(2300)
Ar = generate_matrix(N, hermitian=True,
pos_definite=True).astype('f').astype('d')
M = generate_matrix(N, hermitian=True,
pos_definite=True).astype('f').astype('d')
Ac = generate_matrix(N, hermitian=True, pos_definite=True,
complex_=True).astype('F').astype('D')
Mc = generate_matrix(N, hermitian=True, pos_definite=True,
complex_=True).astype('F').astype('D')
v0 = np.random.random(N)
# standard symmetric problem
SS = DictWithRepr("std-symmetric")
SS['mat'] = Ar
SS['v0'] = v0
SS['eval'] = eigh(SS['mat'], eigvals_only=True)
# general symmetric problem
GS = DictWithRepr("gen-symmetric")
GS['mat'] = Ar
GS['bmat'] = M
GS['v0'] = v0
GS['eval'] = eigh(GS['mat'], GS['bmat'], eigvals_only=True)
# standard hermitian problem
SH = DictWithRepr("std-hermitian")
SH['mat'] = Ac
SH['v0'] = v0
SH['eval'] = eigh(SH['mat'], eigvals_only=True)
# general hermitian problem
GH = DictWithRepr("gen-hermitian")
GH['mat'] = Ac
GH['bmat'] = M
GH['v0'] = v0
GH['eval'] = eigh(GH['mat'], GH['bmat'], eigvals_only=True)
# general hermitian problem with hermitian M
GHc = DictWithRepr("gen-hermitian-Mc")
GHc['mat'] = Ac
GHc['bmat'] = Mc
GHc['v0'] = v0
GHc['eval'] = eigh(GHc['mat'], GHc['bmat'], eigvals_only=True)
self.real_test_cases = [SS, GS]
self.complex_test_cases = [SH, GH, GHc]
class NonSymmetricParams:
def __init__(self):
self.eigs = eigs
self.which = ['LM', 'LR', 'LI'] # , 'SM', 'LR', 'SR', 'LI', 'SI']
self.mattypes = [csr_matrix, aslinearoperator, np.asarray]
self.sigmas_OPparts = {None: [None],
0.1: ['r'],
0.1 + 0.1j: ['r', 'i']}
# generate matrices
# these should all be float32 so that the eigenvalues
# are the same in float32 and float64
N = 6
np.random.seed(2300)
Ar = generate_matrix(N).astype('f').astype('d')
M = generate_matrix(N, hermitian=True,
pos_definite=True).astype('f').astype('d')
Ac = generate_matrix(N, complex_=True).astype('F').astype('D')
v0 = np.random.random(N)
# standard real nonsymmetric problem
SNR = DictWithRepr("std-real-nonsym")
SNR['mat'] = Ar
SNR['v0'] = v0
SNR['eval'] = eig(SNR['mat'], left=False, right=False)
# general real nonsymmetric problem
GNR = DictWithRepr("gen-real-nonsym")
GNR['mat'] = Ar
GNR['bmat'] = M
GNR['v0'] = v0
GNR['eval'] = eig(GNR['mat'], GNR['bmat'], left=False, right=False)
# standard complex nonsymmetric problem
SNC = DictWithRepr("std-cmplx-nonsym")
SNC['mat'] = Ac
SNC['v0'] = v0
SNC['eval'] = eig(SNC['mat'], left=False, right=False)
# general complex nonsymmetric problem
GNC = DictWithRepr("gen-cmplx-nonsym")
GNC['mat'] = Ac
GNC['bmat'] = M
GNC['v0'] = v0
GNC['eval'] = eig(GNC['mat'], GNC['bmat'], left=False, right=False)
self.real_test_cases = [SNR, GNR]
self.complex_test_cases = [SNC, GNC]
def test_symmetric_modes():
params = SymmetricParams()
k = 2
symmetric = True
for D in params.real_test_cases:
for typ in 'fd':
for which in params.which:
for mattype in params.mattypes:
for (sigma, modes) in params.sigmas_modes.items():
for mode in modes:
eval_evec(symmetric, D, typ, k, which,
None, sigma, mattype, None, mode)
def test_hermitian_modes():
params = SymmetricParams()
k = 2
symmetric = True
for D in params.complex_test_cases:
for typ in 'FD':
for which in params.which:
if which == 'BE':
continue # BE invalid for complex
for mattype in params.mattypes:
for sigma in params.sigmas_modes:
eval_evec(symmetric, D, typ, k, which,
None, sigma, mattype)
def test_symmetric_starting_vector():
params = SymmetricParams()
symmetric = True
for k in [1, 2, 3, 4, 5]:
for D in params.real_test_cases:
for typ in 'fd':
v0 = random.rand(len(D['v0'])).astype(typ)
eval_evec(symmetric, D, typ, k, 'LM', v0)
def test_symmetric_no_convergence():
np.random.seed(1234)
m = generate_matrix(30, hermitian=True, pos_definite=True)
tol, rtol, atol = _get_test_tolerance('d')
try:
w, v = eigsh(m, 4, which='LM', v0=m[:, 0], maxiter=5, tol=tol, ncv=9)
raise AssertionError("Spurious no-error exit")
except ArpackNoConvergence as err:
k = len(err.eigenvalues)
if k <= 0:
raise AssertionError("Spurious no-eigenvalues-found case") from err
w, v = err.eigenvalues, err.eigenvectors
assert_allclose(dot(m, v), w * v, rtol=rtol, atol=atol)
def test_real_nonsymmetric_modes():
params = NonSymmetricParams()
k = 2
symmetric = False
for D in params.real_test_cases:
for typ in 'fd':
for which in params.which:
for mattype in params.mattypes:
for sigma, OPparts in params.sigmas_OPparts.items():
for OPpart in OPparts:
eval_evec(symmetric, D, typ, k, which,
None, sigma, mattype, OPpart)
def test_complex_nonsymmetric_modes():
params = NonSymmetricParams()
k = 2
symmetric = False
for D in params.complex_test_cases:
for typ in 'DF':
for which in params.which:
for mattype in params.mattypes:
for sigma in params.sigmas_OPparts:
eval_evec(symmetric, D, typ, k, which,
None, sigma, mattype)
def test_standard_nonsymmetric_starting_vector():
params = NonSymmetricParams()
sigma = None
symmetric = False
for k in [1, 2, 3, 4]:
for d in params.complex_test_cases:
for typ in 'FD':
A = d['mat']
n = A.shape[0]
v0 = random.rand(n).astype(typ)
eval_evec(symmetric, d, typ, k, "LM", v0, sigma)
def test_general_nonsymmetric_starting_vector():
params = NonSymmetricParams()
sigma = None
symmetric = False
for k in [1, 2, 3, 4]:
for d in params.complex_test_cases:
for typ in 'FD':
A = d['mat']
n = A.shape[0]
v0 = random.rand(n).astype(typ)
eval_evec(symmetric, d, typ, k, "LM", v0, sigma)
def test_standard_nonsymmetric_no_convergence():
np.random.seed(1234)
m = generate_matrix(30, complex_=True)
tol, rtol, atol = _get_test_tolerance('d')
try:
w, v = eigs(m, 4, which='LM', v0=m[:, 0], maxiter=5, tol=tol)
raise AssertionError("Spurious no-error exit")
except ArpackNoConvergence as err:
k = len(err.eigenvalues)
if k <= 0:
raise AssertionError("Spurious no-eigenvalues-found case") from err
w, v = err.eigenvalues, err.eigenvectors
for ww, vv in zip(w, v.T):
assert_allclose(dot(m, vv), ww * vv, rtol=rtol, atol=atol)
def test_eigen_bad_shapes():
# A is not square.
A = csc_matrix(np.zeros((2, 3)))
assert_raises(ValueError, eigs, A)
def test_eigen_bad_kwargs():
# Test eigen on wrong keyword argument
A = csc_matrix(np.zeros((8, 8)))
assert_raises(ValueError, eigs, A, which='XX')
def test_ticket_1459_arpack_crash():
for dtype in [np.float32, np.float64]:
# This test does not seem to catch the issue for float32,
# but we made the same fix there, just to be sure
N = 6
k = 2
np.random.seed(2301)
A = np.random.random((N, N)).astype(dtype)
v0 = np.array([-0.71063568258907849895, -0.83185111795729227424,
-0.34365925382227402451, 0.46122533684552280420,
-0.58001341115969040629, -0.78844877570084292984e-01],
dtype=dtype)
# Should not crash:
evals, evecs = eigs(A, k, v0=v0)
@pytest.mark.skipif(IS_PYPY, reason="Test not meaningful on PyPy")
def test_linearoperator_deallocation():
# Check that the linear operators used by the Arpack wrappers are
# deallocatable by reference counting -- they are big objects, so
# Python's cyclic GC may not collect them fast enough before
# running out of memory if eigs/eigsh are called in a tight loop.
M_d = np.eye(10)
M_s = csc_matrix(M_d)
M_o = aslinearoperator(M_d)
with assert_deallocated(lambda: arpack.SpLuInv(M_s)):
pass
with assert_deallocated(lambda: arpack.LuInv(M_d)):
pass
with assert_deallocated(lambda: arpack.IterInv(M_s)):
pass
with assert_deallocated(lambda: arpack.IterOpInv(M_o, None, 0.3)):
pass
with assert_deallocated(lambda: arpack.IterOpInv(M_o, M_o, 0.3)):
pass
def test_parallel_threads():
results = []
v0 = np.random.rand(50)
def worker():
x = diags([1, -2, 1], [-1, 0, 1], shape=(50, 50))
w, v = eigs(x, k=3, v0=v0)
results.append(w)
w, v = eigsh(x, k=3, v0=v0)
results.append(w)
threads = [threading.Thread(target=worker) for k in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
worker()
for r in results:
assert_allclose(r, results[-1])
def test_reentering():
# Just some linear operator that calls eigs recursively
def A_matvec(x):
x = diags([1, -2, 1], [-1, 0, 1], shape=(50, 50))
w, v = eigs(x, k=1)
return v / w[0]
A = LinearOperator(matvec=A_matvec, dtype=float, shape=(50, 50))
# The Fortran code is not reentrant, so this fails (gracefully, not crashing)
assert_raises(RuntimeError, eigs, A, k=1)
assert_raises(RuntimeError, eigsh, A, k=1)
def test_regression_arpackng_1315():
# Check that issue arpack-ng/#1315 is not present.
# Adapted from arpack-ng/TESTS/bug_1315_single.c
# If this fails, then the installed ARPACK library is faulty.
for dtype in [np.float32, np.float64]:
np.random.seed(1234)
w0 = np.arange(1, 1000+1).astype(dtype)
A = diags([w0], [0], shape=(1000, 1000))
v0 = np.random.rand(1000).astype(dtype)
w, v = eigs(A, k=9, ncv=2*9+1, which="LM", v0=v0)
assert_allclose(np.sort(w), np.sort(w0[-9:]),
rtol=1e-4)
def test_eigs_for_k_greater():
# Test eigs() for k beyond limits.
A_sparse = diags([1, -2, 1], [-1, 0, 1], shape=(4, 4)) # sparse
A = generate_matrix(4, sparse=False)
M_dense = np.random.random((4, 4))
M_sparse = generate_matrix(4, sparse=True)
M_linop = aslinearoperator(M_dense)
eig_tuple1 = eig(A, b=M_dense)
eig_tuple2 = eig(A, b=M_sparse)
with suppress_warnings() as sup:
sup.filter(RuntimeWarning)
assert_equal(eigs(A, M=M_dense, k=3), eig_tuple1)
assert_equal(eigs(A, M=M_dense, k=4), eig_tuple1)
assert_equal(eigs(A, M=M_dense, k=5), eig_tuple1)
assert_equal(eigs(A, M=M_sparse, k=5), eig_tuple2)
# M as LinearOperator
assert_raises(TypeError, eigs, A, M=M_linop, k=3)
# Test 'A' for different types
assert_raises(TypeError, eigs, aslinearoperator(A), k=3)
assert_raises(TypeError, eigs, A_sparse, k=3)
def test_eigsh_for_k_greater():
# Test eigsh() for k beyond limits.
A_sparse = diags([1, -2, 1], [-1, 0, 1], shape=(4, 4)) # sparse
A = generate_matrix(4, sparse=False)
M_dense = generate_matrix_symmetric(4, pos_definite=True)
M_sparse = generate_matrix_symmetric(4, pos_definite=True, sparse=True)
M_linop = aslinearoperator(M_dense)
eig_tuple1 = eigh(A, b=M_dense)
eig_tuple2 = eigh(A, b=M_sparse)
with suppress_warnings() as sup:
sup.filter(RuntimeWarning)
assert_equal(eigsh(A, M=M_dense, k=4), eig_tuple1)
assert_equal(eigsh(A, M=M_dense, k=5), eig_tuple1)
assert_equal(eigsh(A, M=M_sparse, k=5), eig_tuple2)
# M as LinearOperator
assert_raises(TypeError, eigsh, A, M=M_linop, k=4)
# Test 'A' for different types
assert_raises(TypeError, eigsh, aslinearoperator(A), k=4)
assert_raises(TypeError, eigsh, A_sparse, M=M_dense, k=4)
def test_real_eigs_real_k_subset():
np.random.seed(1)
n = 10
A = rand(n, n, density=0.5)
A.data *= 2
A.data -= 1
v0 = np.ones(n)
whichs = ['LM', 'SM', 'LR', 'SR', 'LI', 'SI']
dtypes = [np.float32, np.float64]
for which, sigma, dtype in itertools.product(whichs, [None, 0, 5], dtypes):
prev_w = np.array([], dtype=dtype)
eps = np.finfo(dtype).eps
for k in range(1, 9):
w, z = eigs(A.astype(dtype), k=k, which=which, sigma=sigma,
v0=v0.astype(dtype), tol=0)
assert_allclose(np.linalg.norm(A.dot(z) - z * w), 0, atol=np.sqrt(eps))
# Check that the set of eigenvalues for `k` is a subset of that for `k+1`
dist = abs(prev_w[:,None] - w).min(axis=1)
assert_allclose(dist, 0, atol=np.sqrt(eps))
prev_w = w
# Check sort order
if sigma is None:
d = w
else:
d = 1 / (w - sigma)
if which == 'LM':
# ARPACK is systematic for 'LM', but sort order
# appears not well defined for other modes
assert np.all(np.diff(abs(d)) <= 1e-6)

View File

@ -0,0 +1,16 @@
"""
Locally Optimal Block Preconditioned Conjugate Gradient Method (LOBPCG)
LOBPCG is a preconditioned eigensolver for large symmetric positive definite
(SPD) generalized eigenproblems.
Call the function lobpcg - see help for lobpcg.lobpcg.
"""
from .lobpcg import *
__all__ = [s for s in dir() if not s.startswith('_')]
from scipy._lib._testutils import PytestTester
test = PytestTester(__name__)
del PytestTester

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,641 @@
""" Test functions for the sparse.linalg._eigen.lobpcg module
"""
import itertools
import platform
import sys
import pytest
import numpy as np
from numpy import ones, r_, diag
from numpy.testing import (assert_almost_equal, assert_equal,
assert_allclose, assert_array_less)
from scipy import sparse
from scipy.linalg import eig, eigh, toeplitz, orth
from scipy.sparse import spdiags, diags, eye, csr_matrix
from scipy.sparse.linalg import eigs, LinearOperator
from scipy.sparse.linalg._eigen.lobpcg import lobpcg
from scipy.sparse.linalg._eigen.lobpcg.lobpcg import _b_orthonormalize
from scipy._lib._util import np_long, np_ulong
_IS_32BIT = (sys.maxsize < 2**32)
INT_DTYPES = {np.intc, np_long, np.longlong, np.uintc, np_ulong, np.ulonglong}
# np.half is unsupported on many test systems so excluded
REAL_DTYPES = {np.float32, np.float64, np.longdouble}
COMPLEX_DTYPES = {np.complex64, np.complex128, np.clongdouble}
# use sorted list to ensure fixed order of tests
VDTYPES = sorted(REAL_DTYPES ^ COMPLEX_DTYPES, key=str)
MDTYPES = sorted(INT_DTYPES ^ REAL_DTYPES ^ COMPLEX_DTYPES, key=str)
def sign_align(A, B):
"""Align signs of columns of A match those of B: column-wise remove
sign of A by multiplying with its sign then multiply in sign of B.
"""
return np.array([col_A * np.sign(col_A[0]) * np.sign(col_B[0])
for col_A, col_B in zip(A.T, B.T)]).T
def ElasticRod(n):
"""Build the matrices for the generalized eigenvalue problem of the
fixed-free elastic rod vibration model.
"""
L = 1.0
le = L/n
rho = 7.85e3
S = 1.e-4
E = 2.1e11
mass = rho*S*le/6.
k = E*S/le
A = k*(diag(r_[2.*ones(n-1), 1])-diag(ones(n-1), 1)-diag(ones(n-1), -1))
B = mass*(diag(r_[4.*ones(n-1), 2])+diag(ones(n-1), 1)+diag(ones(n-1), -1))
return A, B
def MikotaPair(n):
"""Build a pair of full diagonal matrices for the generalized eigenvalue
problem. The Mikota pair acts as a nice test since the eigenvalues are the
squares of the integers n, n=1,2,...
"""
x = np.arange(1, n+1)
B = diag(1./x)
y = np.arange(n-1, 0, -1)
z = np.arange(2*n-1, 0, -2)
A = diag(z)-diag(y, -1)-diag(y, 1)
return A, B
def compare_solutions(A, B, m):
"""Check eig vs. lobpcg consistency.
"""
n = A.shape[0]
rnd = np.random.RandomState(0)
V = rnd.random((n, m))
X = orth(V)
eigvals, _ = lobpcg(A, X, B=B, tol=1e-2, maxiter=50, largest=False)
eigvals.sort()
w, _ = eig(A, b=B)
w.sort()
assert_almost_equal(w[:int(m/2)], eigvals[:int(m/2)], decimal=2)
def test_Small():
A, B = ElasticRod(10)
with pytest.warns(UserWarning, match="The problem size"):
compare_solutions(A, B, 10)
A, B = MikotaPair(10)
with pytest.warns(UserWarning, match="The problem size"):
compare_solutions(A, B, 10)
def test_ElasticRod():
A, B = ElasticRod(20)
msg = "Exited at iteration.*|Exited postprocessing with accuracies.*"
with pytest.warns(UserWarning, match=msg):
compare_solutions(A, B, 2)
def test_MikotaPair():
A, B = MikotaPair(20)
compare_solutions(A, B, 2)
@pytest.mark.parametrize("n", [50])
@pytest.mark.parametrize("m", [1, 2, 10])
@pytest.mark.parametrize("Vdtype", sorted(REAL_DTYPES, key=str))
@pytest.mark.parametrize("Bdtype", sorted(REAL_DTYPES, key=str))
@pytest.mark.parametrize("BVdtype", sorted(REAL_DTYPES, key=str))
def test_b_orthonormalize(n, m, Vdtype, Bdtype, BVdtype):
"""Test B-orthonormalization by Cholesky with callable 'B'.
The function '_b_orthonormalize' is key in LOBPCG but may
lead to numerical instabilities. The input vectors are often
badly scaled, so the function needs scale-invariant Cholesky;
see https://netlib.org/lapack/lawnspdf/lawn14.pdf.
"""
rnd = np.random.RandomState(0)
X = rnd.standard_normal((n, m)).astype(Vdtype)
Xcopy = np.copy(X)
vals = np.arange(1, n+1, dtype=float)
B = diags([vals], [0], (n, n)).astype(Bdtype)
BX = B @ X
BX = BX.astype(BVdtype)
dtype = min(X.dtype, B.dtype, BX.dtype)
# np.longdouble tol cannot be achieved on most systems
atol = m * n * max(np.finfo(dtype).eps, np.finfo(np.float64).eps)
Xo, BXo, _ = _b_orthonormalize(lambda v: B @ v, X, BX)
# Check in-place.
assert_equal(X, Xo)
assert_equal(id(X), id(Xo))
assert_equal(BX, BXo)
assert_equal(id(BX), id(BXo))
# Check BXo.
assert_allclose(B @ Xo, BXo, atol=atol, rtol=atol)
# Check B-orthonormality
assert_allclose(Xo.T.conj() @ B @ Xo, np.identity(m),
atol=atol, rtol=atol)
# Repeat without BX in outputs
X = np.copy(Xcopy)
Xo1, BXo1, _ = _b_orthonormalize(lambda v: B @ v, X)
assert_allclose(Xo, Xo1, atol=atol, rtol=atol)
assert_allclose(BXo, BXo1, atol=atol, rtol=atol)
# Check in-place.
assert_equal(X, Xo1)
assert_equal(id(X), id(Xo1))
# Check BXo1.
assert_allclose(B @ Xo1, BXo1, atol=atol, rtol=atol)
# Introduce column-scaling in X.
scaling = 1.0 / np.geomspace(10, 1e10, num=m)
X = Xcopy * scaling
X = X.astype(Vdtype)
BX = B @ X
BX = BX.astype(BVdtype)
# Check scaling-invariance of Cholesky-based orthonormalization
Xo1, BXo1, _ = _b_orthonormalize(lambda v: B @ v, X, BX)
# The output should be the same, up the signs of the columns.
Xo1 = sign_align(Xo1, Xo)
assert_allclose(Xo, Xo1, atol=atol, rtol=atol)
BXo1 = sign_align(BXo1, BXo)
assert_allclose(BXo, BXo1, atol=atol, rtol=atol)
@pytest.mark.filterwarnings("ignore:Exited at iteration 0")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_nonhermitian_warning(capsys):
"""Check the warning of a Ritz matrix being not Hermitian
by feeding a non-Hermitian input matrix.
Also check stdout since verbosityLevel=1 and lack of stderr.
"""
n = 10
X = np.arange(n * 2).reshape(n, 2).astype(np.float32)
A = np.arange(n * n).reshape(n, n).astype(np.float32)
with pytest.warns(UserWarning, match="Matrix gramA"):
_, _ = lobpcg(A, X, verbosityLevel=1, maxiter=0)
out, err = capsys.readouterr() # Capture output
assert out.startswith("Solving standard eigenvalue") # Test stdout
assert err == '' # Test empty stderr
# Make the matrix symmetric and the UserWarning disappears.
A += A.T
_, _ = lobpcg(A, X, verbosityLevel=1, maxiter=0)
out, err = capsys.readouterr() # Capture output
assert out.startswith("Solving standard eigenvalue") # Test stdout
assert err == '' # Test empty stderr
def test_regression():
"""Check the eigenvalue of the identity matrix is one.
"""
# https://mail.python.org/pipermail/scipy-user/2010-October/026944.html
n = 10
X = np.ones((n, 1))
A = np.identity(n)
w, _ = lobpcg(A, X)
assert_allclose(w, [1])
@pytest.mark.filterwarnings("ignore:The problem size")
@pytest.mark.parametrize('n, m, m_excluded', [(30, 4, 3), (4, 2, 0)])
def test_diagonal(n, m, m_excluded):
"""Test ``m - m_excluded`` eigenvalues and eigenvectors of
diagonal matrices of the size ``n`` varying matrix formats:
dense array, spare matrix, and ``LinearOperator`` for both
matrixes in the generalized eigenvalue problem ``Av = cBv``
and for the preconditioner.
"""
rnd = np.random.RandomState(0)
# Define the generalized eigenvalue problem Av = cBv
# where (c, v) is a generalized eigenpair,
# A is the diagonal matrix whose entries are 1,...n,
# B is the identity matrix.
vals = np.arange(1, n+1, dtype=float)
A_s = diags([vals], [0], (n, n))
A_a = A_s.toarray()
def A_f(x):
return A_s @ x
A_lo = LinearOperator(matvec=A_f,
matmat=A_f,
shape=(n, n), dtype=float)
B_a = eye(n)
B_s = csr_matrix(B_a)
def B_f(x):
return B_a @ x
B_lo = LinearOperator(matvec=B_f,
matmat=B_f,
shape=(n, n), dtype=float)
# Let the preconditioner M be the inverse of A.
M_s = diags([1./vals], [0], (n, n))
M_a = M_s.toarray()
def M_f(x):
return M_s @ x
M_lo = LinearOperator(matvec=M_f,
matmat=M_f,
shape=(n, n), dtype=float)
# Pick random initial vectors.
X = rnd.normal(size=(n, m))
# Require that the returned eigenvectors be in the orthogonal complement
# of the first few standard basis vectors.
if m_excluded > 0:
Y = np.eye(n, m_excluded)
else:
Y = None
for A in [A_a, A_s, A_lo]:
for B in [B_a, B_s, B_lo]:
for M in [M_a, M_s, M_lo]:
eigvals, vecs = lobpcg(A, X, B, M=M, Y=Y,
maxiter=40, largest=False)
assert_allclose(eigvals, np.arange(1+m_excluded,
1+m_excluded+m))
_check_eigen(A, eigvals, vecs, rtol=1e-3, atol=1e-3)
def _check_eigen(M, w, V, rtol=1e-8, atol=1e-14):
"""Check if the eigenvalue residual is small.
"""
mult_wV = np.multiply(w, V)
dot_MV = M.dot(V)
assert_allclose(mult_wV, dot_MV, rtol=rtol, atol=atol)
def _check_fiedler(n, p):
"""Check the Fiedler vector computation.
"""
# This is not necessarily the recommended way to find the Fiedler vector.
col = np.zeros(n)
col[1] = 1
A = toeplitz(col)
D = np.diag(A.sum(axis=1))
L = D - A
# Compute the full eigendecomposition using tricks, e.g.
# http://www.cs.yale.edu/homes/spielman/561/2009/lect02-09.pdf
tmp = np.pi * np.arange(n) / n
analytic_w = 2 * (1 - np.cos(tmp))
analytic_V = np.cos(np.outer(np.arange(n) + 1/2, tmp))
_check_eigen(L, analytic_w, analytic_V)
# Compute the full eigendecomposition using eigh.
eigh_w, eigh_V = eigh(L)
_check_eigen(L, eigh_w, eigh_V)
# Check that the first eigenvalue is near zero and that the rest agree.
assert_array_less(np.abs([eigh_w[0], analytic_w[0]]), 1e-14)
assert_allclose(eigh_w[1:], analytic_w[1:])
# Check small lobpcg eigenvalues.
X = analytic_V[:, :p]
lobpcg_w, lobpcg_V = lobpcg(L, X, largest=False)
assert_equal(lobpcg_w.shape, (p,))
assert_equal(lobpcg_V.shape, (n, p))
_check_eigen(L, lobpcg_w, lobpcg_V)
assert_array_less(np.abs(np.min(lobpcg_w)), 1e-14)
assert_allclose(np.sort(lobpcg_w)[1:], analytic_w[1:p])
# Check large lobpcg eigenvalues.
X = analytic_V[:, -p:]
lobpcg_w, lobpcg_V = lobpcg(L, X, largest=True)
assert_equal(lobpcg_w.shape, (p,))
assert_equal(lobpcg_V.shape, (n, p))
_check_eigen(L, lobpcg_w, lobpcg_V)
assert_allclose(np.sort(lobpcg_w), analytic_w[-p:])
# Look for the Fiedler vector using good but not exactly correct guesses.
fiedler_guess = np.concatenate((np.ones(n//2), -np.ones(n-n//2)))
X = np.vstack((np.ones(n), fiedler_guess)).T
lobpcg_w, _ = lobpcg(L, X, largest=False)
# Mathematically, the smaller eigenvalue should be zero
# and the larger should be the algebraic connectivity.
lobpcg_w = np.sort(lobpcg_w)
assert_allclose(lobpcg_w, analytic_w[:2], atol=1e-14)
def test_fiedler_small_8():
"""Check the dense workaround path for small matrices.
"""
# This triggers the dense path because 8 < 2*5.
with pytest.warns(UserWarning, match="The problem size"):
_check_fiedler(8, 2)
def test_fiedler_large_12():
"""Check the dense workaround path avoided for non-small matrices.
"""
# This does not trigger the dense path, because 2*5 <= 12.
_check_fiedler(12, 2)
@pytest.mark.filterwarnings("ignore:Failed at iteration")
@pytest.mark.filterwarnings("ignore:Exited at iteration")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_failure_to_run_iterations():
"""Check that the code exits gracefully without breaking. Issue #10974.
The code may or not issue a warning, filtered out. Issue #15935, #17954.
"""
rnd = np.random.RandomState(0)
X = rnd.standard_normal((100, 10))
A = X @ X.T
Q = rnd.standard_normal((X.shape[0], 4))
eigenvalues, _ = lobpcg(A, Q, maxiter=40, tol=1e-12)
assert np.max(eigenvalues) > 0
def test_failure_to_run_iterations_nonsymmetric():
"""Check that the code exists gracefully without breaking
if the matrix in not symmetric.
"""
A = np.zeros((10, 10))
A[0, 1] = 1
Q = np.ones((10, 1))
msg = "Exited at iteration 2|Exited postprocessing with accuracies.*"
with pytest.warns(UserWarning, match=msg):
eigenvalues, _ = lobpcg(A, Q, maxiter=20)
assert np.max(eigenvalues) > 0
@pytest.mark.filterwarnings("ignore:The problem size")
def test_hermitian():
"""Check complex-value Hermitian cases.
"""
rnd = np.random.RandomState(0)
sizes = [3, 12]
ks = [1, 2]
gens = [True, False]
for s, k, gen, dh, dx, db in (
itertools.product(sizes, ks, gens, gens, gens, gens)
):
H = rnd.random((s, s)) + 1.j * rnd.random((s, s))
H = 10 * np.eye(s) + H + H.T.conj()
H = H.astype(np.complex128) if dh else H.astype(np.complex64)
X = rnd.standard_normal((s, k))
X = X + 1.j * rnd.standard_normal((s, k))
X = X.astype(np.complex128) if dx else X.astype(np.complex64)
if not gen:
B = np.eye(s)
w, v = lobpcg(H, X, maxiter=99, verbosityLevel=0)
# Also test mixing complex H with real B.
wb, _ = lobpcg(H, X, B, maxiter=99, verbosityLevel=0)
assert_allclose(w, wb, rtol=1e-6)
w0, _ = eigh(H)
else:
B = rnd.random((s, s)) + 1.j * rnd.random((s, s))
B = 10 * np.eye(s) + B.dot(B.T.conj())
B = B.astype(np.complex128) if db else B.astype(np.complex64)
w, v = lobpcg(H, X, B, maxiter=99, verbosityLevel=0)
w0, _ = eigh(H, B)
for wx, vx in zip(w, v.T):
# Check eigenvector
assert_allclose(np.linalg.norm(H.dot(vx) - B.dot(vx) * wx)
/ np.linalg.norm(H.dot(vx)),
0, atol=5e-2, rtol=0)
# Compare eigenvalues
j = np.argmin(abs(w0 - wx))
assert_allclose(wx, w0[j], rtol=1e-4)
# The n=5 case tests the alternative small matrix code path that uses eigh().
@pytest.mark.filterwarnings("ignore:The problem size")
@pytest.mark.parametrize('n, atol', [(20, 1e-3), (5, 1e-8)])
def test_eigs_consistency(n, atol):
"""Check eigs vs. lobpcg consistency.
"""
vals = np.arange(1, n+1, dtype=np.float64)
A = spdiags(vals, 0, n, n)
rnd = np.random.RandomState(0)
X = rnd.standard_normal((n, 2))
lvals, lvecs = lobpcg(A, X, largest=True, maxiter=100)
vals, _ = eigs(A, k=2)
_check_eigen(A, lvals, lvecs, atol=atol, rtol=0)
assert_allclose(np.sort(vals), np.sort(lvals), atol=1e-14)
def test_verbosity():
"""Check that nonzero verbosity level code runs.
"""
rnd = np.random.RandomState(0)
X = rnd.standard_normal((10, 10))
A = X @ X.T
Q = rnd.standard_normal((X.shape[0], 1))
msg = "Exited at iteration.*|Exited postprocessing with accuracies.*"
with pytest.warns(UserWarning, match=msg):
_, _ = lobpcg(A, Q, maxiter=3, verbosityLevel=9)
@pytest.mark.xfail(_IS_32BIT and sys.platform == 'win32',
reason="tolerance violation on windows")
@pytest.mark.xfail(platform.machine() == 'ppc64le',
reason="fails on ppc64le")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_tolerance_float32():
"""Check lobpcg for attainable tolerance in float32.
"""
rnd = np.random.RandomState(0)
n = 50
m = 3
vals = -np.arange(1, n + 1)
A = diags([vals], [0], (n, n))
A = A.astype(np.float32)
X = rnd.standard_normal((n, m))
X = X.astype(np.float32)
eigvals, _ = lobpcg(A, X, tol=1.25e-5, maxiter=50, verbosityLevel=0)
assert_allclose(eigvals, -np.arange(1, 1 + m), atol=2e-5, rtol=1e-5)
@pytest.mark.parametrize("vdtype", VDTYPES)
@pytest.mark.parametrize("mdtype", MDTYPES)
@pytest.mark.parametrize("arr_type", [np.array,
sparse.csr_matrix,
sparse.coo_matrix])
def test_dtypes(vdtype, mdtype, arr_type):
"""Test lobpcg in various dtypes.
"""
rnd = np.random.RandomState(0)
n = 12
m = 2
A = arr_type(np.diag(np.arange(1, n + 1)).astype(mdtype))
X = rnd.random((n, m))
X = X.astype(vdtype)
eigvals, eigvecs = lobpcg(A, X, tol=1e-2, largest=False)
assert_allclose(eigvals, np.arange(1, 1 + m), atol=1e-1)
# eigenvectors must be nearly real in any case
assert_allclose(np.sum(np.abs(eigvecs - eigvecs.conj())), 0, atol=1e-2)
@pytest.mark.filterwarnings("ignore:Exited at iteration")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_inplace_warning():
"""Check lobpcg gives a warning in '_b_orthonormalize'
that in-place orthogonalization is impossible due to dtype mismatch.
"""
rnd = np.random.RandomState(0)
n = 6
m = 1
vals = -np.arange(1, n + 1)
A = diags([vals], [0], (n, n))
A = A.astype(np.cdouble)
X = rnd.standard_normal((n, m))
with pytest.warns(UserWarning, match="Inplace update"):
eigvals, _ = lobpcg(A, X, maxiter=2, verbosityLevel=1)
def test_maxit():
"""Check lobpcg if maxit=maxiter runs maxiter iterations and
if maxit=None runs 20 iterations (the default)
by checking the size of the iteration history output, which should
be the number of iterations plus 3 (initial, final, and postprocessing)
typically when maxiter is small and the choice of the best is passive.
"""
rnd = np.random.RandomState(0)
n = 50
m = 4
vals = -np.arange(1, n + 1)
A = diags([vals], [0], (n, n))
A = A.astype(np.float32)
X = rnd.standard_normal((n, m))
X = X.astype(np.float64)
msg = "Exited at iteration.*|Exited postprocessing with accuracies.*"
for maxiter in range(1, 4):
with pytest.warns(UserWarning, match=msg):
_, _, l_h, r_h = lobpcg(A, X, tol=1e-8, maxiter=maxiter,
retLambdaHistory=True,
retResidualNormsHistory=True)
assert_allclose(np.shape(l_h)[0], maxiter+3)
assert_allclose(np.shape(r_h)[0], maxiter+3)
with pytest.warns(UserWarning, match=msg):
l, _, l_h, r_h = lobpcg(A, X, tol=1e-8,
retLambdaHistory=True,
retResidualNormsHistory=True)
assert_allclose(np.shape(l_h)[0], 20+3)
assert_allclose(np.shape(r_h)[0], 20+3)
# Check that eigenvalue output is the last one in history
assert_allclose(l, l_h[-1])
# Make sure that both history outputs are lists
assert isinstance(l_h, list)
assert isinstance(r_h, list)
# Make sure that both history lists are arrays-like
assert_allclose(np.shape(l_h), np.shape(np.asarray(l_h)))
assert_allclose(np.shape(r_h), np.shape(np.asarray(r_h)))
@pytest.mark.slow
@pytest.mark.parametrize("n", [15])
@pytest.mark.parametrize("m", [1, 2])
@pytest.mark.filterwarnings("ignore:Exited at iteration")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_diagonal_data_types(n, m):
"""Check lobpcg for diagonal matrices for all matrix types.
Constraints are imposed, so a dense eigensolver eig cannot run.
"""
rnd = np.random.RandomState(0)
# Define the generalized eigenvalue problem Av = cBv
# where (c, v) is a generalized eigenpair,
# and where we choose A and B to be diagonal.
vals = np.arange(1, n + 1)
list_sparse_format = ['bsr', 'coo', 'csc', 'csr', 'dia', 'dok', 'lil']
for s_f_i, s_f in enumerate(list_sparse_format):
As64 = diags([vals * vals], [0], (n, n), format=s_f)
As32 = As64.astype(np.float32)
Af64 = As64.toarray()
Af32 = Af64.astype(np.float32)
def As32f(x):
return As32 @ x
As32LO = LinearOperator(matvec=As32f,
matmat=As32f,
shape=(n, n),
dtype=As32.dtype)
listA = [Af64, As64, Af32, As32, As32f, As32LO, lambda v: As32 @ v]
Bs64 = diags([vals], [0], (n, n), format=s_f)
Bf64 = Bs64.toarray()
Bs32 = Bs64.astype(np.float32)
def Bs32f(x):
return Bs32 @ x
Bs32LO = LinearOperator(matvec=Bs32f,
matmat=Bs32f,
shape=(n, n),
dtype=Bs32.dtype)
listB = [Bf64, Bs64, Bs32, Bs32f, Bs32LO, lambda v: Bs32 @ v]
# Define the preconditioner function as LinearOperator.
Ms64 = diags([1./vals], [0], (n, n), format=s_f)
def Ms64precond(x):
return Ms64 @ x
Ms64precondLO = LinearOperator(matvec=Ms64precond,
matmat=Ms64precond,
shape=(n, n),
dtype=Ms64.dtype)
Mf64 = Ms64.toarray()
def Mf64precond(x):
return Mf64 @ x
Mf64precondLO = LinearOperator(matvec=Mf64precond,
matmat=Mf64precond,
shape=(n, n),
dtype=Mf64.dtype)
Ms32 = Ms64.astype(np.float32)
def Ms32precond(x):
return Ms32 @ x
Ms32precondLO = LinearOperator(matvec=Ms32precond,
matmat=Ms32precond,
shape=(n, n),
dtype=Ms32.dtype)
Mf32 = Ms32.toarray()
def Mf32precond(x):
return Mf32 @ x
Mf32precondLO = LinearOperator(matvec=Mf32precond,
matmat=Mf32precond,
shape=(n, n),
dtype=Mf32.dtype)
listM = [None, Ms64, Ms64precondLO, Mf64precondLO, Ms64precond,
Ms32, Ms32precondLO, Mf32precondLO, Ms32precond]
# Setup matrix of the initial approximation to the eigenvectors
# (cannot be sparse array).
Xf64 = rnd.random((n, m))
Xf32 = Xf64.astype(np.float32)
listX = [Xf64, Xf32]
# Require that the returned eigenvectors be in the orthogonal complement
# of the first few standard basis vectors (cannot be sparse array).
m_excluded = 3
Yf64 = np.eye(n, m_excluded, dtype=float)
Yf32 = np.eye(n, m_excluded, dtype=np.float32)
listY = [Yf64, Yf32]
tests = list(itertools.product(listA, listB, listM, listX, listY))
for A, B, M, X, Y in tests:
# This is one of the slower tests because there are >1,000 configs
# to test here. Flip a biased coin to decide whether to run each
# test to get decent coverage in less time.
if rnd.random() < 0.98:
continue # too many tests
eigvals, _ = lobpcg(A, X, B=B, M=M, Y=Y, tol=1e-4,
maxiter=100, largest=False)
assert_allclose(eigvals,
np.arange(1 + m_excluded, 1 + m_excluded + m),
atol=1e-5)

View File

@ -0,0 +1,862 @@
import re
import copy
import numpy as np
from numpy.testing import assert_allclose, assert_equal, assert_array_equal
import pytest
from scipy.linalg import svd, null_space
from scipy.sparse import csc_matrix, issparse, spdiags, random
from scipy.sparse.linalg import LinearOperator, aslinearoperator
from scipy.sparse.linalg import svds
from scipy.sparse.linalg._eigen.arpack import ArpackNoConvergence
# --- Helper Functions / Classes ---
def sorted_svd(m, k, which='LM'):
# Compute svd of a dense matrix m, and return singular vectors/values
# sorted.
if issparse(m):
m = m.toarray()
u, s, vh = svd(m)
if which == 'LM':
ii = np.argsort(s)[-k:]
elif which == 'SM':
ii = np.argsort(s)[:k]
else:
raise ValueError(f"unknown which={which!r}")
return u[:, ii], s[ii], vh[ii]
def _check_svds(A, k, u, s, vh, which="LM", check_usvh_A=False,
check_svd=True, atol=1e-10, rtol=1e-7):
n, m = A.shape
# Check shapes.
assert_equal(u.shape, (n, k))
assert_equal(s.shape, (k,))
assert_equal(vh.shape, (k, m))
# Check that the original matrix can be reconstituted.
A_rebuilt = (u*s).dot(vh)
assert_equal(A_rebuilt.shape, A.shape)
if check_usvh_A:
assert_allclose(A_rebuilt, A, atol=atol, rtol=rtol)
# Check that u is a semi-orthogonal matrix.
uh_u = np.dot(u.T.conj(), u)
assert_equal(uh_u.shape, (k, k))
assert_allclose(uh_u, np.identity(k), atol=atol, rtol=rtol)
# Check that vh is a semi-orthogonal matrix.
vh_v = np.dot(vh, vh.T.conj())
assert_equal(vh_v.shape, (k, k))
assert_allclose(vh_v, np.identity(k), atol=atol, rtol=rtol)
# Check that scipy.sparse.linalg.svds ~ scipy.linalg.svd
if check_svd:
u2, s2, vh2 = sorted_svd(A, k, which)
assert_allclose(np.abs(u), np.abs(u2), atol=atol, rtol=rtol)
assert_allclose(s, s2, atol=atol, rtol=rtol)
assert_allclose(np.abs(vh), np.abs(vh2), atol=atol, rtol=rtol)
def _check_svds_n(A, k, u, s, vh, which="LM", check_res=True,
check_svd=True, atol=1e-10, rtol=1e-7):
n, m = A.shape
# Check shapes.
assert_equal(u.shape, (n, k))
assert_equal(s.shape, (k,))
assert_equal(vh.shape, (k, m))
# Check that u is a semi-orthogonal matrix.
uh_u = np.dot(u.T.conj(), u)
assert_equal(uh_u.shape, (k, k))
error = np.sum(np.abs(uh_u - np.identity(k))) / (k * k)
assert_allclose(error, 0.0, atol=atol, rtol=rtol)
# Check that vh is a semi-orthogonal matrix.
vh_v = np.dot(vh, vh.T.conj())
assert_equal(vh_v.shape, (k, k))
error = np.sum(np.abs(vh_v - np.identity(k))) / (k * k)
assert_allclose(error, 0.0, atol=atol, rtol=rtol)
# Check residuals
if check_res:
ru = A.T.conj() @ u - vh.T.conj() * s
rus = np.sum(np.abs(ru)) / (n * k)
rvh = A @ vh.T.conj() - u * s
rvhs = np.sum(np.abs(rvh)) / (m * k)
assert_allclose(rus, 0.0, atol=atol, rtol=rtol)
assert_allclose(rvhs, 0.0, atol=atol, rtol=rtol)
# Check that scipy.sparse.linalg.svds ~ scipy.linalg.svd
if check_svd:
u2, s2, vh2 = sorted_svd(A, k, which)
assert_allclose(s, s2, atol=atol, rtol=rtol)
A_rebuilt_svd = (u2*s2).dot(vh2)
A_rebuilt = (u*s).dot(vh)
assert_equal(A_rebuilt.shape, A.shape)
error = np.sum(np.abs(A_rebuilt_svd - A_rebuilt)) / (k * k)
assert_allclose(error, 0.0, atol=atol, rtol=rtol)
class CheckingLinearOperator(LinearOperator):
def __init__(self, A):
self.A = A
self.dtype = A.dtype
self.shape = A.shape
def _matvec(self, x):
assert_equal(max(x.shape), np.size(x))
return self.A.dot(x)
def _rmatvec(self, x):
assert_equal(max(x.shape), np.size(x))
return self.A.T.conjugate().dot(x)
# --- Test Input Validation ---
# Tests input validation on parameters `k` and `which`.
# Needs better input validation checks for all other parameters.
class SVDSCommonTests:
solver = None
# some of these IV tests could run only once, say with solver=None
_A_empty_msg = "`A` must not be empty."
_A_dtype_msg = "`A` must be of floating or complex floating data type"
_A_type_msg = "type not understood"
_A_ndim_msg = "array must have ndim <= 2"
_A_validation_inputs = [
(np.asarray([[]]), ValueError, _A_empty_msg),
(np.asarray([[1, 2], [3, 4]]), ValueError, _A_dtype_msg),
("hi", TypeError, _A_type_msg),
(np.asarray([[[1., 2.], [3., 4.]]]), ValueError, _A_ndim_msg)]
@pytest.mark.parametrize("args", _A_validation_inputs)
def test_svds_input_validation_A(self, args):
A, error_type, message = args
with pytest.raises(error_type, match=message):
svds(A, k=1, solver=self.solver)
@pytest.mark.parametrize("k", [-1, 0, 3, 4, 5, 1.5, "1"])
def test_svds_input_validation_k_1(self, k):
rng = np.random.default_rng(0)
A = rng.random((4, 3))
# propack can do complete SVD
if self.solver == 'propack' and k == 3:
res = svds(A, k=k, solver=self.solver, random_state=0)
_check_svds(A, k, *res, check_usvh_A=True, check_svd=True)
return
message = ("`k` must be an integer satisfying")
with pytest.raises(ValueError, match=message):
svds(A, k=k, solver=self.solver)
def test_svds_input_validation_k_2(self):
# I think the stack trace is reasonable when `k` can't be converted
# to an int.
message = "int() argument must be a"
with pytest.raises(TypeError, match=re.escape(message)):
svds(np.eye(10), k=[], solver=self.solver)
message = "invalid literal for int()"
with pytest.raises(ValueError, match=message):
svds(np.eye(10), k="hi", solver=self.solver)
@pytest.mark.parametrize("tol", (-1, np.inf, np.nan))
def test_svds_input_validation_tol_1(self, tol):
message = "`tol` must be a non-negative floating point value."
with pytest.raises(ValueError, match=message):
svds(np.eye(10), tol=tol, solver=self.solver)
@pytest.mark.parametrize("tol", ([], 'hi'))
def test_svds_input_validation_tol_2(self, tol):
# I think the stack trace is reasonable here
message = "'<' not supported between instances"
with pytest.raises(TypeError, match=message):
svds(np.eye(10), tol=tol, solver=self.solver)
@pytest.mark.parametrize("which", ('LA', 'SA', 'ekki', 0))
def test_svds_input_validation_which(self, which):
# Regression test for a github issue.
# https://github.com/scipy/scipy/issues/4590
# Function was not checking for eigenvalue type and unintended
# values could be returned.
with pytest.raises(ValueError, match="`which` must be in"):
svds(np.eye(10), which=which, solver=self.solver)
@pytest.mark.parametrize("transpose", (True, False))
@pytest.mark.parametrize("n", range(4, 9))
def test_svds_input_validation_v0_1(self, transpose, n):
rng = np.random.default_rng(0)
A = rng.random((5, 7))
v0 = rng.random(n)
if transpose:
A = A.T
k = 2
message = "`v0` must have shape"
required_length = (A.shape[0] if self.solver == 'propack'
else min(A.shape))
if n != required_length:
with pytest.raises(ValueError, match=message):
svds(A, k=k, v0=v0, solver=self.solver)
def test_svds_input_validation_v0_2(self):
A = np.ones((10, 10))
v0 = np.ones((1, 10))
message = "`v0` must have shape"
with pytest.raises(ValueError, match=message):
svds(A, k=1, v0=v0, solver=self.solver)
@pytest.mark.parametrize("v0", ("hi", 1, np.ones(10, dtype=int)))
def test_svds_input_validation_v0_3(self, v0):
A = np.ones((10, 10))
message = "`v0` must be of floating or complex floating data type."
with pytest.raises(ValueError, match=message):
svds(A, k=1, v0=v0, solver=self.solver)
@pytest.mark.parametrize("maxiter", (-1, 0, 5.5))
def test_svds_input_validation_maxiter_1(self, maxiter):
message = ("`maxiter` must be a positive integer.")
with pytest.raises(ValueError, match=message):
svds(np.eye(10), maxiter=maxiter, solver=self.solver)
def test_svds_input_validation_maxiter_2(self):
# I think the stack trace is reasonable when `k` can't be converted
# to an int.
message = "int() argument must be a"
with pytest.raises(TypeError, match=re.escape(message)):
svds(np.eye(10), maxiter=[], solver=self.solver)
message = "invalid literal for int()"
with pytest.raises(ValueError, match=message):
svds(np.eye(10), maxiter="hi", solver=self.solver)
@pytest.mark.parametrize("rsv", ('ekki', 10))
def test_svds_input_validation_return_singular_vectors(self, rsv):
message = "`return_singular_vectors` must be in"
with pytest.raises(ValueError, match=message):
svds(np.eye(10), return_singular_vectors=rsv, solver=self.solver)
# --- Test Parameters ---
@pytest.mark.parametrize("k", [3, 5])
@pytest.mark.parametrize("which", ["LM", "SM"])
def test_svds_parameter_k_which(self, k, which):
# check that the `k` parameter sets the number of eigenvalues/
# eigenvectors returned.
# Also check that the `which` parameter sets whether the largest or
# smallest eigenvalues are returned
rng = np.random.default_rng(0)
A = rng.random((10, 10))
if self.solver == 'lobpcg':
with pytest.warns(UserWarning, match="The problem size"):
res = svds(A, k=k, which=which, solver=self.solver,
random_state=0)
else:
res = svds(A, k=k, which=which, solver=self.solver,
random_state=0)
_check_svds(A, k, *res, which=which, atol=1e-9, rtol=2e-13)
@pytest.mark.filterwarnings("ignore:Exited",
reason="Ignore LOBPCG early exit.")
# loop instead of parametrize for simplicity
def test_svds_parameter_tol(self):
# check the effect of the `tol` parameter on solver accuracy by solving
# the same problem with varying `tol` and comparing the eigenvalues
# against ground truth computed
n = 100 # matrix size
k = 3 # number of eigenvalues to check
# generate a random, sparse-ish matrix
# effect isn't apparent for matrices that are too small
rng = np.random.default_rng(0)
A = rng.random((n, n))
A[A > .1] = 0
A = A @ A.T
_, s, _ = svd(A) # calculate ground truth
# calculate the error as a function of `tol`
A = csc_matrix(A)
def err(tol):
_, s2, _ = svds(A, k=k, v0=np.ones(n), maxiter=1000,
solver=self.solver, tol=tol, random_state=0)
return np.linalg.norm((s2 - s[k-1::-1])/s[k-1::-1])
tols = [1e-4, 1e-2, 1e0] # tolerance levels to check
# for 'arpack' and 'propack', accuracies make discrete steps
accuracies = {'propack': [1e-12, 1e-6, 1e-4],
'arpack': [2.5e-15, 1e-10, 1e-10],
'lobpcg': [2e-12, 4e-2, 2]}
for tol, accuracy in zip(tols, accuracies[self.solver]):
error = err(tol)
assert error < accuracy
def test_svd_v0(self):
# check that the `v0` parameter affects the solution
n = 100
k = 1
# If k != 1, LOBPCG needs more initial vectors, which are generated
# with random_state, so it does not pass w/ k >= 2.
# For some other values of `n`, the AssertionErrors are not raised
# with different v0s, which is reasonable.
rng = np.random.default_rng(0)
A = rng.random((n, n))
# with the same v0, solutions are the same, and they are accurate
# v0 takes precedence over random_state
v0a = rng.random(n)
res1a = svds(A, k, v0=v0a, solver=self.solver, random_state=0)
res2a = svds(A, k, v0=v0a, solver=self.solver, random_state=1)
for idx in range(3):
assert_allclose(res1a[idx], res2a[idx], rtol=1e-15, atol=2e-16)
_check_svds(A, k, *res1a)
# with the same v0, solutions are the same, and they are accurate
v0b = rng.random(n)
res1b = svds(A, k, v0=v0b, solver=self.solver, random_state=2)
res2b = svds(A, k, v0=v0b, solver=self.solver, random_state=3)
for idx in range(3):
assert_allclose(res1b[idx], res2b[idx], rtol=1e-15, atol=2e-16)
_check_svds(A, k, *res1b)
# with different v0, solutions can be numerically different
message = "Arrays are not equal"
with pytest.raises(AssertionError, match=message):
assert_equal(res1a, res1b)
def test_svd_random_state(self):
# check that the `random_state` parameter affects the solution
# Admittedly, `n` and `k` are chosen so that all solver pass all
# these checks. That's a tall order, since LOBPCG doesn't want to
# achieve the desired accuracy and ARPACK often returns the same
# singular values/vectors for different v0.
n = 100
k = 1
rng = np.random.default_rng(0)
A = rng.random((n, n))
# with the same random_state, solutions are the same and accurate
res1a = svds(A, k, solver=self.solver, random_state=0)
res2a = svds(A, k, solver=self.solver, random_state=0)
for idx in range(3):
assert_allclose(res1a[idx], res2a[idx], rtol=1e-15, atol=2e-16)
_check_svds(A, k, *res1a)
# with the same random_state, solutions are the same and accurate
res1b = svds(A, k, solver=self.solver, random_state=1)
res2b = svds(A, k, solver=self.solver, random_state=1)
for idx in range(3):
assert_allclose(res1b[idx], res2b[idx], rtol=1e-15, atol=2e-16)
_check_svds(A, k, *res1b)
# with different random_state, solutions can be numerically different
message = "Arrays are not equal"
with pytest.raises(AssertionError, match=message):
assert_equal(res1a, res1b)
@pytest.mark.parametrize("random_state", (0, 1,
np.random.RandomState(0),
np.random.default_rng(0)))
def test_svd_random_state_2(self, random_state):
n = 100
k = 1
rng = np.random.default_rng(0)
A = rng.random((n, n))
random_state_2 = copy.deepcopy(random_state)
# with the same random_state, solutions are the same and accurate
res1a = svds(A, k, solver=self.solver, random_state=random_state)
res2a = svds(A, k, solver=self.solver, random_state=random_state_2)
for idx in range(3):
assert_allclose(res1a[idx], res2a[idx], rtol=1e-15, atol=2e-16)
_check_svds(A, k, *res1a)
@pytest.mark.parametrize("random_state", (None,
np.random.RandomState(0),
np.random.default_rng(0)))
@pytest.mark.filterwarnings("ignore:Exited",
reason="Ignore LOBPCG early exit.")
def test_svd_random_state_3(self, random_state):
n = 100
k = 5
rng = np.random.default_rng(0)
A = rng.random((n, n))
random_state = copy.deepcopy(random_state)
# random_state in different state produces accurate - but not
# not necessarily identical - results
res1a = svds(A, k, solver=self.solver, random_state=random_state, maxiter=1000)
res2a = svds(A, k, solver=self.solver, random_state=random_state, maxiter=1000)
_check_svds(A, k, *res1a, atol=2e-7)
_check_svds(A, k, *res2a, atol=2e-7)
message = "Arrays are not equal"
with pytest.raises(AssertionError, match=message):
assert_equal(res1a, res2a)
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
def test_svd_maxiter(self):
# check that maxiter works as expected: should not return accurate
# solution after 1 iteration, but should with default `maxiter`
A = np.diag(np.arange(9)).astype(np.float64)
k = 1
u, s, vh = sorted_svd(A, k)
# Use default maxiter by default
maxiter = None
if self.solver == 'arpack':
message = "ARPACK error -1: No convergence"
with pytest.raises(ArpackNoConvergence, match=message):
svds(A, k, ncv=3, maxiter=1, solver=self.solver)
elif self.solver == 'lobpcg':
# Set maxiter higher so test passes without changing
# default and breaking backward compatibility (gh-20221)
maxiter = 30
with pytest.warns(UserWarning, match="Exited at iteration"):
svds(A, k, maxiter=1, solver=self.solver)
elif self.solver == 'propack':
message = "k=1 singular triplets did not converge within"
with pytest.raises(np.linalg.LinAlgError, match=message):
svds(A, k, maxiter=1, solver=self.solver)
ud, sd, vhd = svds(A, k, solver=self.solver, maxiter=maxiter,
random_state=0)
_check_svds(A, k, ud, sd, vhd, atol=1e-8)
assert_allclose(np.abs(ud), np.abs(u), atol=1e-8)
assert_allclose(np.abs(vhd), np.abs(vh), atol=1e-8)
assert_allclose(np.abs(sd), np.abs(s), atol=1e-9)
@pytest.mark.parametrize("rsv", (True, False, 'u', 'vh'))
@pytest.mark.parametrize("shape", ((5, 7), (6, 6), (7, 5)))
def test_svd_return_singular_vectors(self, rsv, shape):
# check that the return_singular_vectors parameter works as expected
rng = np.random.default_rng(0)
A = rng.random(shape)
k = 2
M, N = shape
u, s, vh = sorted_svd(A, k)
respect_u = True if self.solver == 'propack' else M <= N
respect_vh = True if self.solver == 'propack' else M > N
if self.solver == 'lobpcg':
with pytest.warns(UserWarning, match="The problem size"):
if rsv is False:
s2 = svds(A, k, return_singular_vectors=rsv,
solver=self.solver, random_state=rng)
assert_allclose(s2, s)
elif rsv == 'u' and respect_u:
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
solver=self.solver, random_state=rng)
assert_allclose(np.abs(u2), np.abs(u))
assert_allclose(s2, s)
assert vh2 is None
elif rsv == 'vh' and respect_vh:
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
solver=self.solver, random_state=rng)
assert u2 is None
assert_allclose(s2, s)
assert_allclose(np.abs(vh2), np.abs(vh))
else:
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
solver=self.solver, random_state=rng)
if u2 is not None:
assert_allclose(np.abs(u2), np.abs(u))
assert_allclose(s2, s)
if vh2 is not None:
assert_allclose(np.abs(vh2), np.abs(vh))
else:
if rsv is False:
s2 = svds(A, k, return_singular_vectors=rsv,
solver=self.solver, random_state=rng)
assert_allclose(s2, s)
elif rsv == 'u' and respect_u:
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
solver=self.solver, random_state=rng)
assert_allclose(np.abs(u2), np.abs(u))
assert_allclose(s2, s)
assert vh2 is None
elif rsv == 'vh' and respect_vh:
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
solver=self.solver, random_state=rng)
assert u2 is None
assert_allclose(s2, s)
assert_allclose(np.abs(vh2), np.abs(vh))
else:
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
solver=self.solver, random_state=rng)
if u2 is not None:
assert_allclose(np.abs(u2), np.abs(u))
assert_allclose(s2, s)
if vh2 is not None:
assert_allclose(np.abs(vh2), np.abs(vh))
# --- Test Basic Functionality ---
# Tests the accuracy of each solver for real and complex matrices provided
# as list, dense array, sparse matrix, and LinearOperator.
A1 = [[1, 2, 3], [3, 4, 3], [1 + 1j, 0, 2], [0, 0, 1]]
A2 = [[1, 2, 3, 8 + 5j], [3 - 2j, 4, 3, 5], [1, 0, 2, 3], [0, 0, 1, 0]]
@pytest.mark.filterwarnings("ignore:k >= N - 1",
reason="needed to demonstrate #16725")
@pytest.mark.parametrize('A', (A1, A2))
@pytest.mark.parametrize('k', range(1, 5))
# PROPACK fails a lot if @pytest.mark.parametrize('which', ("SM", "LM"))
@pytest.mark.parametrize('real', (True, False))
@pytest.mark.parametrize('transpose', (False, True))
# In gh-14299, it was suggested the `svds` should _not_ work with lists
@pytest.mark.parametrize('lo_type', (np.asarray, csc_matrix,
aslinearoperator))
def test_svd_simple(self, A, k, real, transpose, lo_type):
A = np.asarray(A)
A = np.real(A) if real else A
A = A.T if transpose else A
A2 = lo_type(A)
# could check for the appropriate errors, but that is tested above
if k > min(A.shape):
pytest.skip("`k` cannot be greater than `min(A.shape)`")
if self.solver != 'propack' and k >= min(A.shape):
pytest.skip("Only PROPACK supports complete SVD")
if self.solver == 'arpack' and not real and k == min(A.shape) - 1:
pytest.skip("#16725")
atol = 3e-10
if self.solver == 'propack':
atol = 3e-9 # otherwise test fails on Linux aarch64 (see gh-19855)
if self.solver == 'lobpcg':
with pytest.warns(UserWarning, match="The problem size"):
u, s, vh = svds(A2, k, solver=self.solver, random_state=0)
else:
u, s, vh = svds(A2, k, solver=self.solver, random_state=0)
_check_svds(A, k, u, s, vh, atol=atol)
def test_svd_linop(self):
solver = self.solver
nmks = [(6, 7, 3),
(9, 5, 4),
(10, 8, 5)]
def reorder(args):
U, s, VH = args
j = np.argsort(s)
return U[:, j], s[j], VH[j, :]
for n, m, k in nmks:
# Test svds on a LinearOperator.
A = np.random.RandomState(52).randn(n, m)
L = CheckingLinearOperator(A)
if solver == 'propack':
v0 = np.ones(n)
else:
v0 = np.ones(min(A.shape))
if solver == 'lobpcg':
with pytest.warns(UserWarning, match="The problem size"):
U1, s1, VH1 = reorder(svds(A, k, v0=v0, solver=solver,
random_state=0))
U2, s2, VH2 = reorder(svds(L, k, v0=v0, solver=solver,
random_state=0))
else:
U1, s1, VH1 = reorder(svds(A, k, v0=v0, solver=solver,
random_state=0))
U2, s2, VH2 = reorder(svds(L, k, v0=v0, solver=solver,
random_state=0))
assert_allclose(np.abs(U1), np.abs(U2))
assert_allclose(s1, s2)
assert_allclose(np.abs(VH1), np.abs(VH2))
assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)),
np.dot(U2, np.dot(np.diag(s2), VH2)))
# Try again with which="SM".
A = np.random.RandomState(1909).randn(n, m)
L = CheckingLinearOperator(A)
# TODO: arpack crashes when v0=v0, which="SM"
kwargs = {'v0': v0} if solver not in {None, 'arpack'} else {}
if self.solver == 'lobpcg':
with pytest.warns(UserWarning, match="The problem size"):
U1, s1, VH1 = reorder(svds(A, k, which="SM", solver=solver,
random_state=0, **kwargs))
U2, s2, VH2 = reorder(svds(L, k, which="SM", solver=solver,
random_state=0, **kwargs))
else:
U1, s1, VH1 = reorder(svds(A, k, which="SM", solver=solver,
random_state=0, **kwargs))
U2, s2, VH2 = reorder(svds(L, k, which="SM", solver=solver,
random_state=0, **kwargs))
assert_allclose(np.abs(U1), np.abs(U2))
assert_allclose(s1 + 1, s2 + 1)
assert_allclose(np.abs(VH1), np.abs(VH2))
assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)),
np.dot(U2, np.dot(np.diag(s2), VH2)))
if k < min(n, m) - 1:
# Complex input and explicit which="LM".
for (dt, eps) in [(complex, 1e-7), (np.complex64, 3e-3)]:
rng = np.random.RandomState(1648)
A = (rng.randn(n, m) + 1j * rng.randn(n, m)).astype(dt)
L = CheckingLinearOperator(A)
if self.solver == 'lobpcg':
with pytest.warns(UserWarning,
match="The problem size"):
U1, s1, VH1 = reorder(svds(A, k, which="LM",
solver=solver,
random_state=0))
U2, s2, VH2 = reorder(svds(L, k, which="LM",
solver=solver,
random_state=0))
else:
U1, s1, VH1 = reorder(svds(A, k, which="LM",
solver=solver,
random_state=0))
U2, s2, VH2 = reorder(svds(L, k, which="LM",
solver=solver,
random_state=0))
assert_allclose(np.abs(U1), np.abs(U2), rtol=eps)
assert_allclose(s1, s2, rtol=eps)
assert_allclose(np.abs(VH1), np.abs(VH2), rtol=eps)
assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)),
np.dot(U2, np.dot(np.diag(s2), VH2)),
rtol=eps)
SHAPES = ((100, 100), (100, 101), (101, 100))
@pytest.mark.filterwarnings("ignore:Exited at iteration")
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
@pytest.mark.parametrize("shape", SHAPES)
# ARPACK supports only dtype float, complex, or np.float32
@pytest.mark.parametrize("dtype", (float, complex, np.float32))
def test_small_sigma_sparse(self, shape, dtype):
# https://github.com/scipy/scipy/pull/11829
solver = self.solver
# 2do: PROPACK fails orthogonality of singular vectors
# if dtype == complex and self.solver == 'propack':
# pytest.skip("PROPACK unsupported for complex dtype")
rng = np.random.default_rng(0)
k = 5
(m, n) = shape
S = random(m, n, density=0.1, random_state=rng)
if dtype == complex:
S = + 1j * random(m, n, density=0.1, random_state=rng)
e = np.ones(m)
e[0:5] *= 1e1 ** np.arange(-5, 0, 1)
S = spdiags(e, 0, m, m) @ S
S = S.astype(dtype)
u, s, vh = svds(S, k, which='SM', solver=solver, maxiter=1000,
random_state=0)
c_svd = False # partial SVD can be different from full SVD
_check_svds_n(S, k, u, s, vh, which="SM", check_svd=c_svd, atol=2e-1)
# --- Test Edge Cases ---
# Checks a few edge cases.
@pytest.mark.parametrize("shape", ((6, 5), (5, 5), (5, 6)))
@pytest.mark.parametrize("dtype", (float, complex))
def test_svd_LM_ones_matrix(self, shape, dtype):
# Check that svds can deal with matrix_rank less than k in LM mode.
k = 3
n, m = shape
A = np.ones((n, m), dtype=dtype)
if self.solver == 'lobpcg':
with pytest.warns(UserWarning, match="The problem size"):
U, s, VH = svds(A, k, solver=self.solver, random_state=0)
else:
U, s, VH = svds(A, k, solver=self.solver, random_state=0)
_check_svds(A, k, U, s, VH, check_usvh_A=True, check_svd=False)
# Check that the largest singular value is near sqrt(n*m)
# and the other singular values have been forced to zero.
assert_allclose(np.max(s), np.sqrt(n*m))
s = np.array(sorted(s)[:-1]) + 1
z = np.ones_like(s)
assert_allclose(s, z)
@pytest.mark.filterwarnings("ignore:k >= N - 1",
reason="needed to demonstrate #16725")
@pytest.mark.parametrize("shape", ((3, 4), (4, 4), (4, 3), (4, 2)))
@pytest.mark.parametrize("dtype", (float, complex))
def test_zero_matrix(self, shape, dtype):
# Check that svds can deal with matrices containing only zeros;
# see https://github.com/scipy/scipy/issues/3452/
# shape = (4, 2) is included because it is the particular case
# reported in the issue
k = 1
n, m = shape
A = np.zeros((n, m), dtype=dtype)
if (self.solver == 'arpack' and dtype is complex
and k == min(A.shape) - 1):
pytest.skip("#16725")
if self.solver == 'propack':
pytest.skip("PROPACK failures unrelated to PR #16712")
if self.solver == 'lobpcg':
with pytest.warns(UserWarning, match="The problem size"):
U, s, VH = svds(A, k, solver=self.solver, random_state=0)
else:
U, s, VH = svds(A, k, solver=self.solver, random_state=0)
# Check some generic properties of svd.
_check_svds(A, k, U, s, VH, check_usvh_A=True, check_svd=False)
# Check that the singular values are zero.
assert_array_equal(s, 0)
@pytest.mark.parametrize("shape", ((20, 20), (20, 21), (21, 20)))
# ARPACK supports only dtype float, complex, or np.float32
@pytest.mark.parametrize("dtype", (float, complex, np.float32))
@pytest.mark.filterwarnings("ignore:Exited",
reason="Ignore LOBPCG early exit.")
def test_small_sigma(self, shape, dtype):
rng = np.random.default_rng(179847540)
A = rng.random(shape).astype(dtype)
u, _, vh = svd(A, full_matrices=False)
if dtype == np.float32:
e = 10.0
else:
e = 100.0
t = e**(-np.arange(len(vh))).astype(dtype)
A = (u*t).dot(vh)
k = 4
u, s, vh = svds(A, k, solver=self.solver, maxiter=100, random_state=0)
t = np.sum(s > 0)
assert_equal(t, k)
# LOBPCG needs larger atol and rtol to pass
_check_svds_n(A, k, u, s, vh, atol=1e-3, rtol=1e0, check_svd=False)
# ARPACK supports only dtype float, complex, or np.float32
@pytest.mark.filterwarnings("ignore:The problem size")
@pytest.mark.parametrize("dtype", (float, complex, np.float32))
def test_small_sigma2(self, dtype):
rng = np.random.default_rng(179847540)
# create a 10x10 singular matrix with a 4-dim null space
dim = 4
size = 10
x = rng.random((size, size-dim))
y = x[:, :dim] * rng.random(dim)
mat = np.hstack((x, y))
mat = mat.astype(dtype)
nz = null_space(mat)
assert_equal(nz.shape[1], dim)
# Tolerances atol and rtol adjusted to pass np.float32
# Use non-sparse svd
u, s, vh = svd(mat)
# Singular values are 0:
assert_allclose(s[-dim:], 0, atol=1e-6, rtol=1e0)
# Smallest right singular vectors in null space:
assert_allclose(mat @ vh[-dim:, :].T, 0, atol=1e-6, rtol=1e0)
# Smallest singular values should be 0
sp_mat = csc_matrix(mat)
su, ss, svh = svds(sp_mat, k=dim, which='SM', solver=self.solver,
random_state=0)
# Smallest dim singular values are 0:
assert_allclose(ss, 0, atol=1e-5, rtol=1e0)
# Smallest singular vectors via svds in null space:
n, m = mat.shape
if n < m: # else the assert fails with some libraries unclear why
assert_allclose(sp_mat.transpose() @ su, 0, atol=1e-5, rtol=1e0)
assert_allclose(sp_mat @ svh.T, 0, atol=1e-5, rtol=1e0)
# --- Perform tests with each solver ---
class Test_SVDS_once:
@pytest.mark.parametrize("solver", ['ekki', object])
def test_svds_input_validation_solver(self, solver):
message = "solver must be one of"
with pytest.raises(ValueError, match=message):
svds(np.ones((3, 4)), k=2, solver=solver)
class Test_SVDS_ARPACK(SVDSCommonTests):
def setup_method(self):
self.solver = 'arpack'
@pytest.mark.parametrize("ncv", list(range(-1, 8)) + [4.5, "5"])
def test_svds_input_validation_ncv_1(self, ncv):
rng = np.random.default_rng(0)
A = rng.random((6, 7))
k = 3
if ncv in {4, 5}:
u, s, vh = svds(A, k=k, ncv=ncv, solver=self.solver, random_state=0)
# partial decomposition, so don't check that u@diag(s)@vh=A;
# do check that scipy.sparse.linalg.svds ~ scipy.linalg.svd
_check_svds(A, k, u, s, vh)
else:
message = ("`ncv` must be an integer satisfying")
with pytest.raises(ValueError, match=message):
svds(A, k=k, ncv=ncv, solver=self.solver)
def test_svds_input_validation_ncv_2(self):
# I think the stack trace is reasonable when `ncv` can't be converted
# to an int.
message = "int() argument must be a"
with pytest.raises(TypeError, match=re.escape(message)):
svds(np.eye(10), ncv=[], solver=self.solver)
message = "invalid literal for int()"
with pytest.raises(ValueError, match=message):
svds(np.eye(10), ncv="hi", solver=self.solver)
# I can't see a robust relationship between `ncv` and relevant outputs
# (e.g. accuracy, time), so no test of the parameter.
class Test_SVDS_LOBPCG(SVDSCommonTests):
def setup_method(self):
self.solver = 'lobpcg'
class Test_SVDS_PROPACK(SVDSCommonTests):
def setup_method(self):
self.solver = 'propack'
def test_svd_LM_ones_matrix(self):
message = ("PROPACK does not return orthonormal singular vectors "
"associated with zero singular values.")
# There are some other issues with this matrix of all ones, e.g.
# `which='sm'` and `k=1` returns the largest singular value
pytest.xfail(message)
def test_svd_LM_zeros_matrix(self):
message = ("PROPACK does not return orthonormal singular vectors "
"associated with zero singular values.")
pytest.xfail(message)