def test_shifts(shifts, precision): np.random.seed(0) n, k = 70, 10 A = np.random.random((n, n)) if shifts is not None and ((shifts < 0) or (k > min(n-1-shifts, n))): with pytest.raises(ValueError): _svdp(A, k, shifts=shifts, kmax=5*k, irl_mode=True) else: _svdp(A, k, shifts=shifts, kmax=5*k, irl_mode=True)
def test_shifts_accuracy(): np.random.seed(0) n, k = 70, 10 A = np.random.random((n, n)).astype(np.double) u1, s1, vt1, _ = _svdp(A, k, shifts=None, which='SM', irl_mode=True) u2, s2, vt2, _ = _svdp(A, k, shifts=32, which='SM', irl_mode=True) # shifts <= 32 doesn't agree with shifts > 32 # Does agree when which='LM' instead of 'SM' assert_allclose(s1, s2)
def check_svdp(n, m, constructor, dtype, k, irl_mode, which, f=0.8): tol = TOLS[dtype] M = generate_matrix(np.asarray, n, m, f, dtype) Msp = constructor(M) u1, sigma1, vt1 = np.linalg.svd(M, full_matrices=False) u2, sigma2, vt2, _ = _svdp(Msp, k=k, which=which, irl_mode=irl_mode, tol=tol) # check the which if which.upper() == 'SM': u1 = np.roll(u1, k, 1) vt1 = np.roll(vt1, k, 0) sigma1 = np.roll(sigma1, k) # check that singular values agree assert_allclose(sigma1[:k], sigma2, rtol=tol, atol=tol) # check that singular vectors are orthogonal assert_orthogonal(u1, u2, rtol=tol, atol=tol) assert_orthogonal(vt1.T, vt2.T, rtol=tol, atol=tol)
def test_examples(precision, irl): atol = { 'single': 1e-3, 'double': 1e-9, 'complex8': 1e-4, 'complex16': 1e-9, }[precision] path_prefix = os.path.dirname(__file__) relative_path = ['..', '_propack', 'PROPACK'] folder = os.path.join(path_prefix, *relative_path) A = { 'single': load_real, 'double': load_real, 'complex8': load_complex, 'complex16': load_complex, }[precision](folder, precision) s_expected = load_sigma(folder, precision, irl=irl) u_expected = load_uv(folder, precision, "U", irl=irl) vh_expected = load_uv(folder, precision, "V", irl=irl).T k = len(s_expected) u, s, vh, _ = _svdp(A, k, irl_mode=irl, random_state=0) # Check singular values assert_allclose(s, s_expected.real, atol=atol) # complex example matrix has many repeated singular values, so check only # beginning non-repeated singular vectors to avoid permutations sv_check = 27 if precision in {'complex8', 'complex16'} else k u = u[:, :sv_check] u_expected = u_expected[:, :sv_check] vh = vh[:sv_check, :] vh_expected = vh_expected[:sv_check, :] s = s[:sv_check] assert_allclose(np.abs(u), np.abs(u_expected), atol=atol) assert_allclose(np.abs(vh), np.abs(vh_expected), atol=atol) # Check orthogonality of singular vectors assert_allclose(np.eye(u.shape[1]), u.conj().T @ u, atol=atol) assert_allclose(np.eye(vh.shape[0]), vh @ vh.conj().T, atol=atol) # Ensure the norm of the difference between the np.linalg.svd and # PROPACK reconstructed matrices is small u3, s3, vh3 = np.linalg.svd(A.todense()) u3 = u3[:, :sv_check] s3 = s3[:sv_check] vh3 = vh3[:sv_check, :] A3 = u3 @ np.diag(s3) @ vh3 recon = u @ np.diag(s) @ vh assert_allclose(np.linalg.norm(A3 - recon), 0, atol=atol)
def test_examples(precision, irl): # Note: atol for complex8 bumped from 1e-4 to 1e-3 because of test failures # with BLIS, Netlib, and MKL+AVX512 - see # https://github.com/conda-forge/scipy-feedstock/pull/198#issuecomment-999180432 atol = { 'single': 1.2e-4, 'double': 1e-9, 'complex8': 1e-3, 'complex16': 1e-9, }[precision] path_prefix = os.path.dirname(__file__) # Test matrices from `illc1850.coord` and `mhd1280b.cua` distributed with # PROPACK 2.1: http://sun.stanford.edu/~rmunk/PROPACK/ relative_path = "propack_test_data.npz" filename = os.path.join(path_prefix, relative_path) data = np.load(filename, allow_pickle=True) dtype = _dtype_map[precision] if precision in {'single', 'double'}: A = data['A_real'].item().astype(dtype) elif precision in {'complex8', 'complex16'}: A = data['A_complex'].item().astype(dtype) k = 200 u, s, vh, _ = _svdp(A, k, irl_mode=irl, random_state=0) # complex example matrix has many repeated singular values, so check only # beginning non-repeated singular vectors to avoid permutations sv_check = 27 if precision in {'complex8', 'complex16'} else k u = u[:, :sv_check] vh = vh[:sv_check, :] s = s[:sv_check] # Check orthogonality of singular vectors assert_allclose(np.eye(u.shape[1]), u.conj().T @ u, atol=atol) assert_allclose(np.eye(vh.shape[0]), vh @ vh.conj().T, atol=atol) # Ensure the norm of the difference between the np.linalg.svd and # PROPACK reconstructed matrices is small u3, s3, vh3 = np.linalg.svd(A.todense()) u3 = u3[:, :sv_check] s3 = s3[:sv_check] vh3 = vh3[:sv_check, :] A3 = u3 @ np.diag(s3) @ vh3 recon = u @ np.diag(s) @ vh assert_allclose(np.linalg.norm(A3 - recon), 0, atol=atol)
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', 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 ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on which one is more efficient, followed by the Rayleigh-Ritz method as postprocessing; see https://w.wiki/4zms Alternatively, the PROPACK solver can be called. ``form="array"`` Choices of the input matrix ``A`` numeric dtype may be limited. Only ``solver="lobpcg"`` supports all floating point dtypes real: 'np.single', 'np.double', 'np.longdouble' and complex: 'np.csingle', 'np.cdouble', 'np.clongdouble'. The ``solver="arpack"`` supports only 'np.single', 'np.double', and 'np.cdouble'. 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 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: if rs_was_None: X = np.random.RandomState(52).randn(min(A.shape), k) else: X = random_state.uniform(size=(min(A.shape), k)) _, eigvec = lobpcg(XH_X, X, tol=tol**2, maxiter=maxiter, largest=largest) # lobpcg does not guarantee exactly orthonormal eigenvectors eigvec, _ = np.linalg.qr(eigvec) elif solver == 'propack': if not HAS_PROPACK: raise ValueError("`solver='propack'` is opt-in due " "to potential issues on Windows, " "it can be enabled by setting the " "`SCIPY_USE_PROPACK` environment " "variable before importing scipy") 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), )) _, eigvec = eigsh(XH_X, k=k, tol=tol**2, maxiter=maxiter, ncv=ncv, which=which, v0=v0) 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