def test_hermitian(): np.random.seed(1234) sizes = [3, 10, 50] ks = [1, 3, 10, 50] gens = [True, False] for size, k, gen in itertools.product(sizes, ks, gens): if k > size: continue H = np.random.rand(size, size) + 1.j * np.random.rand(size, size) H = 10 * np.eye(size) + H + H.T.conj() X = np.random.rand(size, k) if not gen: B = np.eye(size) w, v = lobpcg(H, X, maxiter=5000) w0, v0 = eigh(H) else: B = np.random.rand(size, size) + 1.j * np.random.rand(size, size) B = 10 * np.eye(size) + B.dot(B.T.conj()) w, v = lobpcg(H, X, B, maxiter=5000) w0, v0 = 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-4, rtol=0) # Compare eigenvalues j = np.argmin(abs(w0 - wx)) assert_allclose(wx, w0[j], rtol=1e-4)
def _check_fiedler(n, p): """Check the Fiedler vector computation. """ # This is not necessarily the recommended way to find the Fiedler vector. np.random.seed(1234) 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_regression(): # https://mail.scipy.org/pipermail/scipy-user/2010-October/026944.html n = 10 X = np.ones((n, 1)) A = np.identity(n) w, V = lobpcg(A, X) assert_allclose(w, [1])
def test_regression(): # https://mail.python.org/pipermail/scipy-user/2010-October/026944.html n = 10 X = np.ones((n, 1)) A = np.identity(n) w, V = lobpcg(A, X) assert_allclose(w, [1])
def _check_fiedler(n, p): # This is not necessarily the recommended way to find the Fiedler vector. np.random.seed(1234) 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_V = 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_verbosity(tmpdir): """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)) with pytest.warns(UserWarning, match="Exited at iteration"): _, _ = lobpcg(A, Q, maxiter=3, verbosityLevel=9)
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])
def test_eigs_consistency(n, atol): vals = np.arange(1, n + 1, dtype=np.float64) A = spdiags(vals, 0, n, n) X = np.random.rand(n, 2) lvals, lvecs = lobpcg(A, X, largest=True, maxiter=100) vals, vecs = eigs(A, k=2) _check_eigen(A, lvals, lvecs, atol=atol, rtol=0) assert_allclose(vals, lvals, atol=1e-14)
def test_eigs_consistency(n, atol): vals = np.arange(1, n+1, dtype=np.float64) A = spdiags(vals, 0, n, n) X = np.random.rand(n, 2) lvals, lvecs = lobpcg(A, X, largest=True, maxiter=100) vals, vecs = eigs(A, k=2) _check_eigen(A, lvals, lvecs, atol=atol, rtol=0) assert_allclose(vals, lvals, atol=1e-14)
def test_failure_to_run_iterations(): """Check that the code exists gracefully without breaking. Issue #10974. """ rnd = np.random.RandomState(0) X = rnd.standard_normal((100, 10)) A = X @ X.T Q = rnd.standard_normal((X.shape[0], 4)) with pytest.warns(UserWarning, match="Exited at iteration"): eigenvalues, _ = lobpcg(A, Q, maxiter=20) assert (np.max(eigenvalues) > 0)
def test_failure_to_run_iterations(): """Check that the code exists gracefully without breaking. Issue #10974. """ X = np.random.randn(100, 10) A = X @ X.T Q = np.random.randn(X.shape[0], 4) with suppress_warnings() as sup: sup.filter(UserWarning, ".*not reaching.*") eigenvalues, _ = lobpcg(A, Q, maxiter=20) assert (np.max(eigenvalues) > 0)
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 dissappears. 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_verbosity(): """Check that nonzero verbosity level code runs. """ A, B = ElasticRod(100) n = A.shape[0] m = 20 np.random.seed(0) V = rand(n, m) X = orth(V) _, _ = lobpcg(A, X, B=B, tol=1e-5, maxiter=30, largest=False, verbosityLevel=9)
def test_maxit(): """Check lobpcg if maxit=10 runs 10 iterations 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 2 (initial and final values). """ 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.float32) with pytest.warns(UserWarning, match="Exited at iteration"): _, _, l_h = lobpcg(A, X, tol=1e-8, maxiter=10, retLambdaHistory=True) assert_allclose(np.shape(l_h)[0], 10 + 2) with pytest.warns(UserWarning, match="Exited at iteration"): _, _, l_h = lobpcg(A, X, tol=1e-8, retLambdaHistory=True) assert_allclose(np.shape(l_h)[0], 20 + 2)
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) np.random.seed(345678) X = np.random.rand(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 compare_solutions(A, B, m): """Check eig vs. lobpcg consistency. """ n = A.shape[0] np.random.seed(0) V = rand(n, m) X = orth(V) eigvals, _ = lobpcg(A, X, B=B, tol=1e-5, maxiter=30, 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_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=1e-5, maxiter=50, verbosityLevel=0) assert_allclose(eigvals, -np.arange(1, 1 + m), atol=1e-5)
def test_hermitian(): """Check complex-value Hermitian cases. """ rnd = np.random.RandomState(0) sizes = [3, 10, 50] ks = [1, 3, 10, 50] gens = [True, False] for s, k, gen in itertools.product(sizes, ks, gens): if k > s: continue H = rnd.random((s, s)) + 1.j * rnd.random((s, s)) H = 10 * np.eye(s) + H + H.T.conj() X = rnd.random((s, k)) if not gen: B = np.eye(s) w, v = lobpcg(H, X, maxiter=5000) 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()) w, v = lobpcg(H, X, B, maxiter=5000, largest=False) 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-4, rtol=0) # Compare eigenvalues j = np.argmin(abs(w0 - wx)) assert_allclose(wx, w0[j], rtol=1e-4)
def test_tolerance_float32(): """Check lobpcg for attainable tolerance in float32. """ np.random.seed(1234) n = 50 m = 3 vals = -np.arange(1, n + 1) A = diags([vals], [0], (n, n)) A = A.astype(np.float32) X = np.random.randn(n, m) X = X.astype(np.float32) eigvals, _ = lobpcg(A, X, tol=1e-9, maxiter=50, verbosityLevel=0) assert_allclose(eigvals, -np.arange(1, 1 + m), atol=1e-5)
def test_random_initial_float32(): """Check lobpcg in float32 for specific initial. """ np.random.seed(3) n = 50 m = 4 vals = -np.arange(1, n + 1) A = diags([vals], [0], (n, n)) A = A.astype(np.float32) X = np.random.rand(n, m) X = X.astype(np.float32) eigvals, _ = lobpcg(A, X, tol=1e-3, maxiter=50, verbosityLevel=1) assert_allclose(eigvals, -np.arange(1, 1 + m), atol=1e-2)
def test_verbosity(): """Check that nonzero verbosity level code runs. """ A, B = ElasticRod(100) n = A.shape[0] m = 20 np.random.seed(0) V = rand(n,m) X = linalg.orth(V) eigs,vecs = lobpcg(A, X, B=B, tol=1e-5, maxiter=30, largest=False, verbosityLevel=11)
def test_maxit_None(): """Check lobpcg 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 2 (initial and final values). """ np.random.seed(1566950023) n = 50 m = 4 vals = -np.arange(1, n + 1) A = diags([vals], [0], (n, n)) A = A.astype(np.float32) X = np.random.randn(n, m) X = X.astype(np.float32) _, _, l_h = lobpcg(A, X, tol=1e-8, maxiter=20, retLambdaHistory=True) assert_allclose(np.shape(l_h)[0], 20 + 2)
def compare_solutions(A, B, m): n = A.shape[0] np.random.seed(0) V = rand(n, m) X = linalg.orth(V) eigs, vecs = lobpcg(A, X, B=B, tol=1e-5, maxiter=30) eigs.sort() w, v = eig(A, b=B) w.sort() assert_almost_equal(w[:int(m / 2)], eigs[:int(m / 2)], decimal=2)
def compare_solutions(A,B,m): n = A.shape[0] np.random.seed(0) V = rand(n,m) X = linalg.orth(V) eigs,vecs = lobpcg(A, X, B=B, tol=1e-5, maxiter=30) eigs.sort() w,v = eig(A,b=B) w.sort() assert_almost_equal(w[:int(m/2)],eigs[:int(m/2)],decimal=2)
def test_diagonal(): """Check for diagonal matrices. """ # This test was moved from '__main__' in lobpcg.py. # Coincidentally or not, this is the same eigensystem # required to reproduce arpack bug # https://forge.scilab.org/p/arpack-ng/issues/1397/ # even using the same n=100. np.random.seed(1234) # The system of interest is of size n x n. n = 100 # We care about only m eigenpairs. m = 4 # Define the generalized eigenvalue problem Av = cBv # where (c, v) is a generalized eigenpair, # and where we choose A to be the diagonal matrix whose entries are 1..n # and where B is chosen to be the identity matrix. vals = np.arange(1, n + 1, dtype=float) A = diags([vals], [0], (n, n)) B = eye(n) # Let the preconditioner M be the inverse of A. M = diags([1. / vals], [0], (n, n)) # Pick random initial vectors. X = np.random.rand(n, m) # Require that the returned eigenvectors be in the orthogonal complement # of the first few standard basis vectors. m_excluded = 3 Y = np.eye(n, m_excluded) eigvals, vecs = lobpcg(A, X, B, M=M, Y=Y, tol=1e-4, 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 test_diagonal(): # This test was moved from '__main__' in lobpcg.py. # Coincidentally or not, this is the same eigensystem # required to reproduce arpack bug # https://forge.scilab.org/p/arpack-ng/issues/1397/ # even using the same n=100. np.random.seed(1234) # The system of interest is of size n x n. n = 100 # We care about only m eigenpairs. m = 4 # Define the generalized eigenvalue problem Av = cBv # where (c, v) is a generalized eigenpair, # and where we choose A to be the diagonal matrix whose entries are 1..n # and where B is chosen to be the identity matrix. vals = np.arange(1, n+1, dtype=float) A = scipy.sparse.diags([vals], [0], (n, n)) B = scipy.sparse.eye(n) # Let the preconditioner M be the inverse of A. M = scipy.sparse.diags([np.reciprocal(vals)], [0], (n, n)) # Pick random initial vectors. X = np.random.rand(n, m) # Require that the returned eigenvectors be in the orthogonal complement # of the first few standard basis vectors. m_excluded = 3 Y = np.eye(n, m_excluded) eigs, vecs = lobpcg(A, X, B, M=M, Y=Y, tol=1e-4, maxiter=40, largest=False) assert_allclose(eigs, np.arange(1+m_excluded, 1+m_excluded+m)) _check_eigen(A, eigs, vecs, rtol=1e-3, atol=1e-3)
def test_diagonal(): """Check for diagonal matrices. """ rnd = np.random.RandomState(0) n = 100 m = 4 # Define the generalized eigenvalue problem Av = cBv # where (c, v) is a generalized eigenpair, # and where we choose A to be the diagonal matrix whose entries are 1..n # and where B is chosen to be the identity matrix. vals = np.arange(1, n + 1, dtype=float) A = diags([vals], [0], (n, n)) B = eye(n) # Let the preconditioner M be the inverse of A. M = diags([1. / vals], [0], (n, n)) # Pick random initial vectors. X = rnd.random((n, m)) # Require that the returned eigenvectors be in the orthogonal complement # of the first few standard basis vectors. m_excluded = 3 Y = np.eye(n, m_excluded) eigvals, vecs = lobpcg(A, X, B, M=M, Y=Y, tol=1e-4, 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 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 : sparse matrix or LinearOperator Matrix to decompose. 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 ``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. >>> 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) >>> 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) >>> 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 """ rs_was_None = random_state is None # avoid changing v0 for arpack/lobpcg 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 else: X_dot = A.rmatvec X_matmat = A.rmatmat XH_dot = A.matvec XH_mat = A.matmat 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: if rs_was_None: X = np.random.RandomState(52).randn(min(A.shape), k) else: X = random_state.uniform(size=(min(A.shape), k)) eigvals, 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 and not rs_was_None: v0 = random_state.uniform(size=(min(A.shape), )) eigvals, eigvec = eigsh(XH_X, k=k, tol=tol**2, maxiter=maxiter, ncv=ncv, which=which, v0=v0) # Gramian matrices have real non-negative eigenvalues. eigvals = np.maximum(eigvals.real, 0) # Use the sophisticated detection of small eigenvalues from pinvh. t = eigvec.dtype.char.lower() factor = {'f': 1E3, 'd': 1E6} cond = factor[t] * np.finfo(t).eps cutoff = cond * np.max(eigvals) # Get a mask indicating which eigenpairs are not degenerately tiny, # and create the re-ordered array of thresholded singular values. above_cutoff = (eigvals > cutoff) nlarge = above_cutoff.sum() nsmall = k - nlarge slarge = np.sqrt(eigvals[above_cutoff]) s = np.zeros_like(eigvals) s[:nlarge] = slarge if not return_singular_vectors: return np.sort(s) if n > m: vlarge = eigvec[:, above_cutoff] ularge = (X_matmat(vlarge) / slarge if return_singular_vectors != 'vh' else None) vhlarge = _herm(vlarge) else: ularge = eigvec[:, above_cutoff] vhlarge = (_herm(X_matmat(ularge) / slarge) if return_singular_vectors != 'u' else None) u = (_augmented_orthonormal_cols(ularge, nsmall, random_state) if ularge is not None else None) vh = (_augmented_orthonormal_rows(vhlarge, nsmall, random_state) if vhlarge is not None else None) indexes_sorted = np.argsort(s) s = s[indexes_sorted] if u is not None: u = u[:, indexes_sorted] if vh is not None: vh = vh[indexes_sorted] return u, s, vh
def svds(A, k=6, ncv=None, tol=0, which='LM', v0=None, maxiter=None, return_singular_vectors=True, solver='arpack', 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 : 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)``. ncv : int, optional When ``solver='arpack'``, this is the number of Lanczos vectors generated. See :ref:`'arpack' <sparse.linalg.svds-arpack>` for details. When ``solver='lobpcg'``, this parameter is ignored. tol : float, optional Tolerance for singular values. Zero (default) means machine precision. which : {'LM', 'SM'} Which `k` singular values to find: either the largest magnitude ('LM') or smallest magnitude ('SM') singular values. v0 : ndarray, optional The starting vector for iteration; see method-specific documentation (:ref:`'arpack' <sparse.linalg.svds-arpack>` or :ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`) for details. maxiter : int, optional Maximum number of iterations; see method-specific documentation (:ref:`'arpack' <sparse.linalg.svds-arpack>` or :ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`) for details. return_singular_vectors : bool or str, optional Singular values are always computed and returned; this parameter controls the computation and return of singular vectors. - ``True``: return singular vectors. - ``False``: do not return singular vectors. - ``"u"``: only return the left singular values, without computing the right singular vectors (if ``N > M``). - ``"vh"``: only return the right singular values, without computing the left singular vectors (if ``N <= M``). solver : str, optional The solver used. :ref:`'arpack' <sparse.linalg.svds-arpack>` and :ref:`'lobpcg' <sparse.linalg.svds-lobpcg>` are supported. Default: `'arpack'`. 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. If `return_singular_vectors` is ``"vh"``, this variable is not computed, and ``None`` is returned instead. s : ndarray, shape=(k,) The singular values. vh : ndarray, shape=(k, N) Unitary matrix having right singular vectors as rows. If `return_singular_vectors` is ``"u"``, this variable is not computed, and ``None`` is returned instead. Notes ----- This is a naive implementation using ARPACK or 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. >>> 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) >>> 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) >>> A3 = u3 @ np.diag(s3) @ vT3 >>> np.allclose(A3, A.todense()) True The singular values match the expected singular values, and the singular values are as expected up to a difference in sign. Consequently, the returned arrays of singular vectors must also be orthogonal. >>> (np.allclose(s3, s) and ... np.allclose(np.abs(u3), np.abs(u.todense())) and ... np.allclose(np.abs(vT3), np.abs(vT.todense()))) True """ if which == 'LM': largest = True elif which == 'SM': largest = False else: raise ValueError("which must be either 'LM' or 'SM'.") if (not (isinstance(A, LinearOperator) or isspmatrix(A) or is_pydata_spmatrix(A))): A = np.asarray(A) n, m = A.shape if k <= 0 or k >= min(n, m): raise ValueError("k must be between 1 and min(A.shape), k=%d" % k) if isinstance(A, LinearOperator): if n > m: X_dot = A.matvec X_matmat = A.matmat XH_dot = A.rmatvec XH_mat = A.rmatmat else: X_dot = A.rmatvec X_matmat = A.rmatmat XH_dot = A.matvec XH_mat = A.matmat dtype = getattr(A, 'dtype', None) if dtype is None: dtype = A.dot(np.zeros([m, 1])).dtype else: if n > m: X_dot = X_matmat = A.dot XH_dot = XH_mat = _herm(A).dot else: XH_dot = XH_mat = A.dot X_dot = X_matmat = _herm(A).dot 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 = np.random.RandomState(52).randn(min(A.shape), k) eigvals, eigvec = lobpcg(XH_X, X, tol=tol ** 2, maxiter=maxiter, largest=largest) elif solver == 'arpack' or solver is None: eigvals, eigvec = eigsh(XH_X, k=k, tol=tol ** 2, maxiter=maxiter, ncv=ncv, which=which, v0=v0) else: raise ValueError("solver must be either 'arpack', or 'lobpcg'.") # Gramian matrices have real non-negative eigenvalues. eigvals = np.maximum(eigvals.real, 0) # Use the sophisticated detection of small eigenvalues from pinvh. t = eigvec.dtype.char.lower() factor = {'f': 1E3, 'd': 1E6} cond = factor[t] * np.finfo(t).eps cutoff = cond * np.max(eigvals) # Get a mask indicating which eigenpairs are not degenerately tiny, # and create the re-ordered array of thresholded singular values. above_cutoff = (eigvals > cutoff) nlarge = above_cutoff.sum() nsmall = k - nlarge slarge = np.sqrt(eigvals[above_cutoff]) s = np.zeros_like(eigvals) s[:nlarge] = slarge if not return_singular_vectors: return np.sort(s) if n > m: vlarge = eigvec[:, above_cutoff] ularge = (X_matmat(vlarge) / slarge if return_singular_vectors != 'vh' else None) vhlarge = _herm(vlarge) else: ularge = eigvec[:, above_cutoff] vhlarge = (_herm(X_matmat(ularge) / slarge) if return_singular_vectors != 'u' else None) u = (_augmented_orthonormal_cols(ularge, nsmall) if ularge is not None else None) vh = (_augmented_orthonormal_rows(vhlarge, nsmall) if vhlarge is not None else None) indexes_sorted = np.argsort(s) s = s[indexes_sorted] if u is not None: u = u[:, indexes_sorted] if vh is not None: vh = vh[indexes_sorted] return u, s, vh
def test_diagonal_data_types(): """Check lobpcg for diagonal matrices for all matrix types. """ np.random.seed(1234) n = 40 m = 4 # 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'] sparse_formats = len(list_sparse_format) 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) listA = [Af64, As64, Af32, As32] Bs64 = diags([vals], [0], (n, n), format=s_f) Bf64 = Bs64.toarray() listB = [Bf64, Bs64] # 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=float) Mf64 = Ms64.toarray() def Mf64precond(x): return Mf64 @ x Mf64precondLO = LinearOperator(matvec=Mf64precond, matmat=Mf64precond, shape=(n, n), dtype=float) Ms32 = Ms64.astype(np.float32) def Ms32precond(x): return Ms32 @ x Ms32precondLO = LinearOperator(matvec=Ms32precond, matmat=Ms32precond, shape=(n, n), dtype=np.float32) Mf32 = Ms32.toarray() def Mf32precond(x): return Mf32 @ x Mf32precondLO = LinearOperator(matvec=Mf32precond, matmat=Mf32precond, shape=(n, n), dtype=np.float32) listM = [ None, Ms64precondLO, Mf64precondLO, Ms32precondLO, Mf32precondLO ] # Setup matrix of the initial approximation to the eigenvectors # (cannot be sparse array). Xf64 = np.random.rand(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)) # This is one of the slower tests because there are >1,000 configs # to test here, instead of checking product of all input, output types # test each configuration for the first sparse format, and then # for one additional sparse format. this takes 2/7=30% as long as # testing all configurations for all sparse formats. if s_f_i > 0: tests = tests[s_f_i - 1::sparse_formats - 1] for A, B, M, X, Y in 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))
def svds(A, k=6, ncv=None, tol=0, which='LM', v0=None, maxiter=None, return_singular_vectors=True, solver='arpack'): """Compute the largest or smallest k singular values/vectors for a sparse matrix. The order of the singular values is not guaranteed. Parameters ---------- A : {sparse matrix, LinearOperator} Array to compute the SVD on, of shape (M, N) k : int, optional Number of singular values and vectors to compute. Must be 1 <= k < min(A.shape). ncv : int, optional The number of Lanczos vectors generated ncv must be greater than k+1 and smaller than n; it is recommended that ncv > 2*k Default: ``min(n, max(2*k + 1, 20))`` tol : float, optional Tolerance for singular values. Zero (default) means machine precision. which : str, ['LM' | 'SM'], optional Which `k` singular values to find: - 'LM' : largest singular values - 'SM' : smallest singular values .. versionadded:: 0.12.0 v0 : ndarray, optional Starting vector for iteration, of length min(A.shape). Should be an (approximate) left singular vector if N > M and a right singular vector otherwise. Default: random .. versionadded:: 0.12.0 maxiter : int, optional Maximum number of iterations. .. versionadded:: 0.12.0 return_singular_vectors : bool or str, optional - True: return singular vectors (True) in addition to singular values. .. versionadded:: 0.12.0 - "u": only return the u matrix, without computing vh (if N > M). - "vh": only return the vh matrix, without computing u (if N <= M). .. versionadded:: 0.16.0 solver : str, optional Eigenvalue solver to use. Should be 'arpack' or 'lobpcg'. Default: 'arpack' Returns ------- u : ndarray, shape=(M, k) Unitary matrix having left singular vectors as columns. If `return_singular_vectors` is "vh", this variable is not computed, and None is returned instead. s : ndarray, shape=(k,) The singular values. vt : ndarray, shape=(k, N) Unitary matrix having right singular vectors as rows. If `return_singular_vectors` is "u", this variable is not computed, and None is returned instead. Notes ----- This is a naive implementation using ARPACK or LOBPCG as an eigensolver on A.H * A or A * A.H, depending on which one is more efficient. Examples -------- >>> from scipy.sparse import csc_matrix >>> from scipy.sparse.linalg import svds, eigs >>> A = csc_matrix([[1, 0, 0], [5, 0, 2], [0, -1, 0], [0, 0, 3]], dtype=float) >>> u, s, vt = svds(A, k=2) >>> s array([ 2.75193379, 5.6059665 ]) >>> np.sqrt(eigs(A.dot(A.T), k=2)[0]).real array([ 5.6059665 , 2.75193379]) """ if which == 'LM': largest = True elif which == 'SM': largest = False else: raise ValueError("which must be either 'LM' or 'SM'.") if not (isinstance(A, LinearOperator) or isspmatrix(A) or is_pydata_spmatrix(A)): A = np.asarray(A) n, m = A.shape if k <= 0 or k >= min(n, m): raise ValueError("k must be between 1 and min(A.shape), k=%d" % k) if isinstance(A, LinearOperator): 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 dtype = getattr(A, 'dtype', None) if dtype is None: dtype = A.dot(np.zeros([m, 1])).dtype transpose = True else: if n > m: X_dot = X_matmat = A.dot XH_dot = XH_mat = _herm(A).dot transpose = False else: XH_dot = XH_mat = A.dot X_dot = X_matmat = _herm(A).dot transpose = True 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 = np.random.RandomState(52).randn(min(A.shape), k) eigvals, eigvec = lobpcg(XH_X, X, tol=tol, maxiter=maxiter, largest=largest) elif solver == 'arpack' or solver is None: eigvals, eigvec = eigsh(XH_X, k=k, tol=tol, maxiter=maxiter, ncv=ncv, which=which, v0=v0) else: raise ValueError("solver must be either 'arpack', or 'lobpcg'.") u = X_matmat(eigvec) if not return_singular_vectors: s = svd(u, compute_uv=False) return s[::-1] # compute the right singular vectors of X and update the left ones accordingly u, s, vh = svd(u, full_matrices=False) u = u[:, ::-1] s = s[::-1] vh = vh[::-1] return_u = (return_singular_vectors == 'u') return_vh = (return_singular_vectors == 'vh') if not transpose: if return_vh: u = None if return_u: vh = None else: vh = vh @ _herm(eigvec) return u, s, vh else: if return_u: u = eigvec @ _herm(vh) return u, s, None if return_vh: return None, s, _herm(u) u, vh = eigvec @ _herm(vh), _herm(u) return u, s, vh
def test_diagonal_data_types(): """Check lobpcg for diagonal matrices for all matrix types. """ np.random.seed(1234) n = 50 m = 4 # 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 in 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) listA = [Af64, As64, Af32, As32] Bs64 = diags([vals], [0], (n, n), format=s_f) Bf64 = Bs64.toarray() listB = [Bf64, Bs64] # 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=float) Mf64 = Ms64.toarray() def Mf64precond(x): return Mf64 @ x Mf64precondLO = LinearOperator(matvec=Mf64precond, matmat=Mf64precond, shape=(n, n), dtype=float) Ms32 = Ms64.astype(np.float32) def Ms32precond(x): return Ms32 @ x Ms32precondLO = LinearOperator(matvec=Ms32precond, matmat=Ms32precond, shape=(n, n), dtype=np.float32) Mf32 = Ms32.toarray() def Mf32precond(x): return Mf32 @ x Mf32precondLO = LinearOperator(matvec=Mf32precond, matmat=Mf32precond, shape=(n, n), dtype=np.float32) listM = [ None, Ms64precondLO, Mf64precondLO, Ms32precondLO, Mf32precondLO ] # Setup matrix of the initial approximation to the eigenvectors # (cannot be sparse array). Xf64 = np.random.rand(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] for A, B, M, X, Y in itertools.product(listA, listB, listM, listX, listY): 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))