some new features
This commit is contained in:
@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
@ -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
|
||||
@ -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.
|
||||
@ -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 *
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
@ -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
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
Reference in New Issue
Block a user