Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/scipy/sparse/linalg/_eigen/_svds.py: 11%

132 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-23 06:43 +0000

1import os 

2import numpy as np 

3 

4from .arpack import _arpack # type: ignore[attr-defined] 

5from . import eigsh 

6 

7from scipy._lib._util import check_random_state 

8from scipy.sparse.linalg._interface import LinearOperator, aslinearoperator 

9from scipy.sparse.linalg._eigen.lobpcg import lobpcg # type: ignore[no-redef] 

10if os.environ.get("SCIPY_USE_PROPACK"): 

11 from scipy.sparse.linalg._svdp import _svdp 

12 HAS_PROPACK = True 

13else: 

14 HAS_PROPACK = False 

15from scipy.linalg import svd 

16 

17arpack_int = _arpack.timing.nbx.dtype 

18__all__ = ['svds'] 

19 

20 

21def _herm(x): 

22 return x.T.conj() 

23 

24 

25def _iv(A, k, ncv, tol, which, v0, maxiter, 

26 return_singular, solver, random_state): 

27 

28 # input validation/standardization for `solver` 

29 # out of order because it's needed for other parameters 

30 solver = str(solver).lower() 

31 solvers = {"arpack", "lobpcg", "propack"} 

32 if solver not in solvers: 

33 raise ValueError(f"solver must be one of {solvers}.") 

34 

35 # input validation/standardization for `A` 

36 A = aslinearoperator(A) # this takes care of some input validation 

37 if not (np.issubdtype(A.dtype, np.complexfloating) 

38 or np.issubdtype(A.dtype, np.floating)): 

39 message = "`A` must be of floating or complex floating data type." 

40 raise ValueError(message) 

41 if np.prod(A.shape) == 0: 

42 message = "`A` must not be empty." 

43 raise ValueError(message) 

44 

45 # input validation/standardization for `k` 

46 kmax = min(A.shape) if solver == 'propack' else min(A.shape) - 1 

47 if int(k) != k or not (0 < k <= kmax): 

48 message = "`k` must be an integer satisfying `0 < k < min(A.shape)`." 

49 raise ValueError(message) 

50 k = int(k) 

51 

52 # input validation/standardization for `ncv` 

53 if solver == "arpack" and ncv is not None: 

54 if int(ncv) != ncv or not (k < ncv < min(A.shape)): 

55 message = ("`ncv` must be an integer satisfying " 

56 "`k < ncv < min(A.shape)`.") 

57 raise ValueError(message) 

58 ncv = int(ncv) 

59 

60 # input validation/standardization for `tol` 

61 if tol < 0 or not np.isfinite(tol): 

62 message = "`tol` must be a non-negative floating point value." 

63 raise ValueError(message) 

64 tol = float(tol) 

65 

66 # input validation/standardization for `which` 

67 which = str(which).upper() 

68 whichs = {'LM', 'SM'} 

69 if which not in whichs: 

70 raise ValueError(f"`which` must be in {whichs}.") 

71 

72 # input validation/standardization for `v0` 

73 if v0 is not None: 

74 v0 = np.atleast_1d(v0) 

75 if not (np.issubdtype(v0.dtype, np.complexfloating) 

76 or np.issubdtype(v0.dtype, np.floating)): 

77 message = ("`v0` must be of floating or complex floating " 

78 "data type.") 

79 raise ValueError(message) 

80 

81 shape = (A.shape[0],) if solver == 'propack' else (min(A.shape),) 

82 if v0.shape != shape: 

83 message = f"`v0` must have shape {shape}." 

84 raise ValueError(message) 

85 

86 # input validation/standardization for `maxiter` 

87 if maxiter is not None and (int(maxiter) != maxiter or maxiter <= 0): 

88 message = "`maxiter` must be a positive integer." 

89 raise ValueError(message) 

90 maxiter = int(maxiter) if maxiter is not None else maxiter 

91 

92 # input validation/standardization for `return_singular_vectors` 

93 # not going to be flexible with this; too complicated for little gain 

94 rs_options = {True, False, "vh", "u"} 

95 if return_singular not in rs_options: 

96 raise ValueError(f"`return_singular_vectors` must be in {rs_options}.") 

97 

98 random_state = check_random_state(random_state) 

99 

100 return (A, k, ncv, tol, which, v0, maxiter, 

101 return_singular, solver, random_state) 

102 

103 

104def svds(A, k=6, ncv=None, tol=0, which='LM', v0=None, 

105 maxiter=None, return_singular_vectors=True, 

106 solver='arpack', random_state=None, options=None): 

107 """ 

108 Partial singular value decomposition of a sparse matrix. 

109 

110 Compute the largest or smallest `k` singular values and corresponding 

111 singular vectors of a sparse matrix `A`. The order in which the singular 

112 values are returned is not guaranteed. 

113 

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

115 

116 Parameters 

117 ---------- 

118 A : ndarray, sparse matrix, or LinearOperator 

119 Matrix to decompose of a floating point numeric dtype. 

120 k : int, default: 6 

121 Number of singular values and singular vectors to compute. 

122 Must satisfy ``1 <= k <= kmax``, where ``kmax=min(M, N)`` for 

123 ``solver='propack'`` and ``kmax=min(M, N) - 1`` otherwise. 

124 ncv : int, optional 

125 When ``solver='arpack'``, this is the number of Lanczos vectors 

126 generated. See :ref:`'arpack' <sparse.linalg.svds-arpack>` for details. 

127 When ``solver='lobpcg'`` or ``solver='propack'``, this parameter is 

128 ignored. 

129 tol : float, optional 

130 Tolerance for singular values. Zero (default) means machine precision. 

131 which : {'LM', 'SM'} 

132 Which `k` singular values to find: either the largest magnitude ('LM') 

133 or smallest magnitude ('SM') singular values. 

134 v0 : ndarray, optional 

135 The starting vector for iteration; see method-specific 

136 documentation (:ref:`'arpack' <sparse.linalg.svds-arpack>`, 

137 :ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`), or 

138 :ref:`'propack' <sparse.linalg.svds-propack>` for details. 

139 maxiter : int, optional 

140 Maximum number of iterations; see method-specific 

141 documentation (:ref:`'arpack' <sparse.linalg.svds-arpack>`, 

142 :ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`), or 

143 :ref:`'propack' <sparse.linalg.svds-propack>` for details. 

144 return_singular_vectors : {True, False, "u", "vh"} 

145 Singular values are always computed and returned; this parameter 

146 controls the computation and return of singular vectors. 

147 

148 - ``True``: return singular vectors. 

149 - ``False``: do not return singular vectors. 

150 - ``"u"``: if ``M <= N``, compute only the left singular vectors and 

151 return ``None`` for the right singular vectors. Otherwise, compute 

152 all singular vectors. 

153 - ``"vh"``: if ``M > N``, compute only the right singular vectors and 

154 return ``None`` for the left singular vectors. Otherwise, compute 

155 all singular vectors. 

156 

157 If ``solver='propack'``, the option is respected regardless of the 

158 matrix shape. 

159 

160 solver : {'arpack', 'propack', 'lobpcg'}, optional 

161 The solver used. 

162 :ref:`'arpack' <sparse.linalg.svds-arpack>`, 

163 :ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`, and 

164 :ref:`'propack' <sparse.linalg.svds-propack>` are supported. 

165 Default: `'arpack'`. 

166 random_state : {None, int, `numpy.random.Generator`, 

167 `numpy.random.RandomState`}, optional 

168 

169 Pseudorandom number generator state used to generate resamples. 

170 

171 If `random_state` is ``None`` (or `np.random`), the 

172 `numpy.random.RandomState` singleton is used. 

173 If `random_state` is an int, a new ``RandomState`` instance is used, 

174 seeded with `random_state`. 

175 If `random_state` is already a ``Generator`` or ``RandomState`` 

176 instance then that instance is used. 

177 options : dict, optional 

178 A dictionary of solver-specific options. No solver-specific options 

179 are currently supported; this parameter is reserved for future use. 

180 

181 Returns 

182 ------- 

183 u : ndarray, shape=(M, k) 

184 Unitary matrix having left singular vectors as columns. 

185 s : ndarray, shape=(k,) 

186 The singular values. 

187 vh : ndarray, shape=(k, N) 

188 Unitary matrix having right singular vectors as rows. 

189 

190 Notes 

191 ----- 

192 This is a naive implementation using ARPACK or LOBPCG as an eigensolver 

193 on the matrix ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on 

194 which one is smaller size, followed by the Rayleigh-Ritz method 

195 as postprocessing; see 

196 Using the normal matrix, in Rayleigh-Ritz method, (2022, Nov. 19), 

197 Wikipedia, https://w.wiki/4zms. 

198 

199 Alternatively, the PROPACK solver can be called. 

200 

201 Choices of the input matrix `A` numeric dtype may be limited. 

202 Only ``solver="lobpcg"`` supports all floating point dtypes 

203 real: 'np.float32', 'np.float64', 'np.longdouble' and 

204 complex: 'np.complex64', 'np.complex128', 'np.clongdouble'. 

205 The ``solver="arpack"`` supports only 

206 'np.float32', 'np.float64', and 'np.complex128'. 

207 

208 Examples 

209 -------- 

210 Construct a matrix `A` from singular values and vectors. 

211 

212 >>> import numpy as np 

213 >>> from scipy import sparse, linalg, stats 

214 >>> from scipy.sparse.linalg import svds, aslinearoperator, LinearOperator 

215 

216 Construct a dense matrix `A` from singular values and vectors. 

217 

218 >>> rng = np.random.default_rng(258265244568965474821194062361901728911) 

219 >>> orthogonal = stats.ortho_group.rvs(10, random_state=rng) 

220 >>> s = [1e-3, 1, 2, 3, 4] # non-zero singular values 

221 >>> u = orthogonal[:, :5] # left singular vectors 

222 >>> vT = orthogonal[:, 5:].T # right singular vectors 

223 >>> A = u @ np.diag(s) @ vT 

224 

225 With only four singular values/vectors, the SVD approximates the original 

226 matrix. 

227 

228 >>> u4, s4, vT4 = svds(A, k=4) 

229 >>> A4 = u4 @ np.diag(s4) @ vT4 

230 >>> np.allclose(A4, A, atol=1e-3) 

231 True 

232 

233 With all five non-zero singular values/vectors, we can reproduce 

234 the original matrix more accurately. 

235 

236 >>> u5, s5, vT5 = svds(A, k=5) 

237 >>> A5 = u5 @ np.diag(s5) @ vT5 

238 >>> np.allclose(A5, A) 

239 True 

240 

241 The singular values match the expected singular values. 

242 

243 >>> np.allclose(s5, s) 

244 True 

245 

246 Since the singular values are not close to each other in this example, 

247 every singular vector matches as expected up to a difference in sign. 

248 

249 >>> (np.allclose(np.abs(u5), np.abs(u)) and 

250 ... np.allclose(np.abs(vT5), np.abs(vT))) 

251 True 

252 

253 The singular vectors are also orthogonal. 

254 

255 >>> (np.allclose(u5.T @ u5, np.eye(5)) and 

256 ... np.allclose(vT5 @ vT5.T, np.eye(5))) 

257 True 

258 

259 If there are (nearly) multiple singular values, the corresponding 

260 individual singular vectors may be unstable, but the whole invariant 

261 subspace containing all such singular vectors is computed accurately 

262 as can be measured by angles between subspaces via 'subspace_angles'. 

263 

264 >>> rng = np.random.default_rng(178686584221410808734965903901790843963) 

265 >>> s = [1, 1 + 1e-6] # non-zero singular values 

266 >>> u, _ = np.linalg.qr(rng.standard_normal((99, 2))) 

267 >>> v, _ = np.linalg.qr(rng.standard_normal((99, 2))) 

268 >>> vT = v.T 

269 >>> A = u @ np.diag(s) @ vT 

270 >>> A = A.astype(np.float32) 

271 >>> u2, s2, vT2 = svds(A, k=2, random_state=rng) 

272 >>> np.allclose(s2, s) 

273 True 

274 

275 The angles between the individual exact and computed singular vectors 

276 may not be so small. To check use: 

277 

278 >>> (linalg.subspace_angles(u2[:, :1], u[:, :1]) + 

279 ... linalg.subspace_angles(u2[:, 1:], u[:, 1:])) 

280 array([0.06562513]) # may vary 

281 >>> (linalg.subspace_angles(vT2[:1, :].T, vT[:1, :].T) + 

282 ... linalg.subspace_angles(vT2[1:, :].T, vT[1:, :].T)) 

283 array([0.06562507]) # may vary 

284 

285 As opposed to the angles between the 2-dimensional invariant subspaces 

286 that these vectors span, which are small for rights singular vectors 

287 

288 >>> linalg.subspace_angles(u2, u).sum() < 1e-6 

289 True 

290 

291 as well as for left singular vectors. 

292 

293 >>> linalg.subspace_angles(vT2.T, vT.T).sum() < 1e-6 

294 True 

295 

296 The next example follows that of 'sklearn.decomposition.TruncatedSVD'. 

297 

298 >>> rng = np.random.RandomState(0) 

299 >>> X_dense = rng.random(size=(100, 100)) 

300 >>> X_dense[:, 2 * np.arange(50)] = 0 

301 >>> X = sparse.csr_matrix(X_dense) 

302 >>> _, singular_values, _ = svds(X, k=5, random_state=rng) 

303 >>> print(singular_values) 

304 [ 4.3293... 4.4491... 4.5420... 4.5987... 35.2410...] 

305 

306 The function can be called without the transpose of the input matrix 

307 ever explicitly constructed. 

308 

309 >>> rng = np.random.default_rng(102524723947864966825913730119128190974) 

310 >>> G = sparse.rand(8, 9, density=0.5, random_state=rng) 

311 >>> Glo = aslinearoperator(G) 

312 >>> _, singular_values_svds, _ = svds(Glo, k=5, random_state=rng) 

313 >>> _, singular_values_svd, _ = linalg.svd(G.toarray()) 

314 >>> np.allclose(singular_values_svds, singular_values_svd[-4::-1]) 

315 True 

316 

317 The most memory efficient scenario is where neither 

318 the original matrix, nor its transpose, is explicitly constructed. 

319 Our example computes the smallest singular values and vectors 

320 of 'LinearOperator' constructed from the numpy function 'np.diff' used 

321 column-wise to be consistent with 'LinearOperator' operating on columns. 

322 

323 >>> diff0 = lambda a: np.diff(a, axis=0) 

324 

325 Let us create the matrix from 'diff0' to be used for validation only. 

326 

327 >>> n = 5 # The dimension of the space. 

328 >>> M_from_diff0 = diff0(np.eye(n)) 

329 >>> print(M_from_diff0.astype(int)) 

330 [[-1 1 0 0 0] 

331 [ 0 -1 1 0 0] 

332 [ 0 0 -1 1 0] 

333 [ 0 0 0 -1 1]] 

334 

335 The matrix 'M_from_diff0' is bi-diagonal and could be alternatively 

336 created directly by 

337 

338 >>> M = - np.eye(n - 1, n, dtype=int) 

339 >>> np.fill_diagonal(M[:,1:], 1) 

340 >>> np.allclose(M, M_from_diff0) 

341 True 

342 

343 Its transpose 

344 

345 >>> print(M.T) 

346 [[-1 0 0 0] 

347 [ 1 -1 0 0] 

348 [ 0 1 -1 0] 

349 [ 0 0 1 -1] 

350 [ 0 0 0 1]] 

351 

352 can be viewed as the incidence matrix; see 

353 Incidence matrix, (2022, Nov. 19), Wikipedia, https://w.wiki/5YXU, 

354 of a linear graph with 5 vertices and 4 edges. The 5x5 normal matrix 

355 ``M.T @ M`` thus is 

356 

357 >>> print(M.T @ M) 

358 [[ 1 -1 0 0 0] 

359 [-1 2 -1 0 0] 

360 [ 0 -1 2 -1 0] 

361 [ 0 0 -1 2 -1] 

362 [ 0 0 0 -1 1]] 

363 

364 the graph Laplacian, while the actually used in 'svds' smaller size 

365 4x4 normal matrix ``M @ M.T`` 

366 

367 >>> print(M @ M.T) 

368 [[ 2 -1 0 0] 

369 [-1 2 -1 0] 

370 [ 0 -1 2 -1] 

371 [ 0 0 -1 2]] 

372 

373 is the so-called edge-based Laplacian; see 

374 Symmetric Laplacian via the incidence matrix, in Laplacian matrix, 

375 (2022, Nov. 19), Wikipedia, https://w.wiki/5YXW. 

376 

377 The 'LinearOperator' setup needs the options 'rmatvec' and 'rmatmat' 

378 of multiplication by the matrix transpose ``M.T``, but we want to be 

379 matrix-free to save memory, so knowing how ``M.T`` looks like, we 

380 manually construct the following function to be 

381 used in ``rmatmat=diff0t``. 

382 

383 >>> def diff0t(a): 

384 ... if a.ndim == 1: 

385 ... a = a[:,np.newaxis] # Turn 1D into 2D array 

386 ... d = np.zeros((a.shape[0] + 1, a.shape[1]), dtype=a.dtype) 

387 ... d[0, :] = - a[0, :] 

388 ... d[1:-1, :] = a[0:-1, :] - a[1:, :] 

389 ... d[-1, :] = a[-1, :] 

390 ... return d 

391 

392 We check that our function 'diff0t' for the matrix transpose is valid. 

393 

394 >>> np.allclose(M.T, diff0t(np.eye(n-1))) 

395 True 

396 

397 Now we setup our matrix-free 'LinearOperator' called 'diff0_func_aslo' 

398 and for validation the matrix-based 'diff0_matrix_aslo'. 

399 

400 >>> def diff0_func_aslo_def(n): 

401 ... return LinearOperator(matvec=diff0, 

402 ... matmat=diff0, 

403 ... rmatvec=diff0t, 

404 ... rmatmat=diff0t, 

405 ... shape=(n - 1, n)) 

406 >>> diff0_func_aslo = diff0_func_aslo_def(n) 

407 >>> diff0_matrix_aslo = aslinearoperator(M_from_diff0) 

408 

409 And validate both the matrix and its transpose in 'LinearOperator'. 

410 

411 >>> np.allclose(diff0_func_aslo(np.eye(n)), 

412 ... diff0_matrix_aslo(np.eye(n))) 

413 True 

414 >>> np.allclose(diff0_func_aslo.T(np.eye(n-1)), 

415 ... diff0_matrix_aslo.T(np.eye(n-1))) 

416 True 

417 

418 Having the 'LinearOperator' setup validated, we run the solver. 

419 

420 >>> n = 100 

421 >>> diff0_func_aslo = diff0_func_aslo_def(n) 

422 >>> u, s, vT = svds(diff0_func_aslo, k=3, which='SM') 

423 

424 The singular values squared and the singular vectors are known 

425 explicitly; see 

426 Pure Dirichlet boundary conditions, in 

427 Eigenvalues and eigenvectors of the second derivative, 

428 (2022, Nov. 19), Wikipedia, https://w.wiki/5YX6, 

429 since 'diff' corresponds to first 

430 derivative, and its smaller size n-1 x n-1 normal matrix 

431 ``M @ M.T`` represent the discrete second derivative with the Dirichlet 

432 boundary conditions. We use these analytic expressions for validation. 

433 

434 >>> se = 2. * np.sin(np.pi * np.arange(1, 4) / (2. * n)) 

435 >>> ue = np.sqrt(2 / n) * np.sin(np.pi * np.outer(np.arange(1, n), 

436 ... np.arange(1, 4)) / n) 

437 >>> np.allclose(s, se, atol=1e-3) 

438 True 

439 >>> print(np.allclose(np.abs(u), np.abs(ue), atol=1e-6)) 

440 True 

441 

442 """ 

443 args = _iv(A, k, ncv, tol, which, v0, maxiter, return_singular_vectors, 

444 solver, random_state) 

445 (A, k, ncv, tol, which, v0, maxiter, 

446 return_singular_vectors, solver, random_state) = args 

447 

448 largest = (which == 'LM') 

449 n, m = A.shape 

450 

451 if n >= m: 

452 X_dot = A.matvec 

453 X_matmat = A.matmat 

454 XH_dot = A.rmatvec 

455 XH_mat = A.rmatmat 

456 transpose = False 

457 else: 

458 X_dot = A.rmatvec 

459 X_matmat = A.rmatmat 

460 XH_dot = A.matvec 

461 XH_mat = A.matmat 

462 transpose = True 

463 

464 dtype = getattr(A, 'dtype', None) 

465 if dtype is None: 

466 dtype = A.dot(np.zeros([m, 1])).dtype 

467 

468 def matvec_XH_X(x): 

469 return XH_dot(X_dot(x)) 

470 

471 def matmat_XH_X(x): 

472 return XH_mat(X_matmat(x)) 

473 

474 XH_X = LinearOperator(matvec=matvec_XH_X, dtype=A.dtype, 

475 matmat=matmat_XH_X, 

476 shape=(min(A.shape), min(A.shape))) 

477 

478 # Get a low rank approximation of the implicitly defined gramian matrix. 

479 # This is not a stable way to approach the problem. 

480 if solver == 'lobpcg': 

481 

482 if k == 1 and v0 is not None: 

483 X = np.reshape(v0, (-1, 1)) 

484 else: 

485 X = random_state.standard_normal(size=(min(A.shape), k)) 

486 

487 _, eigvec = lobpcg(XH_X, X, tol=tol ** 2, maxiter=maxiter, 

488 largest=largest) 

489 

490 elif solver == 'propack': 

491 if not HAS_PROPACK: 

492 raise ValueError("`solver='propack'` is opt-in due " 

493 "to potential issues on Windows, " 

494 "it can be enabled by setting the " 

495 "`SCIPY_USE_PROPACK` environment " 

496 "variable before importing scipy") 

497 jobu = return_singular_vectors in {True, 'u'} 

498 jobv = return_singular_vectors in {True, 'vh'} 

499 irl_mode = (which == 'SM') 

500 res = _svdp(A, k=k, tol=tol**2, which=which, maxiter=None, 

501 compute_u=jobu, compute_v=jobv, irl_mode=irl_mode, 

502 kmax=maxiter, v0=v0, random_state=random_state) 

503 

504 u, s, vh, _ = res # but we'll ignore bnd, the last output 

505 

506 # PROPACK order appears to be largest first. `svds` output order is not 

507 # guaranteed, according to documentation, but for ARPACK and LOBPCG 

508 # they actually are ordered smallest to largest, so reverse for 

509 # consistency. 

510 s = s[::-1] 

511 u = u[:, ::-1] 

512 vh = vh[::-1] 

513 

514 u = u if jobu else None 

515 vh = vh if jobv else None 

516 

517 if return_singular_vectors: 

518 return u, s, vh 

519 else: 

520 return s 

521 

522 elif solver == 'arpack' or solver is None: 

523 if v0 is None: 

524 v0 = random_state.standard_normal(size=(min(A.shape),)) 

525 _, eigvec = eigsh(XH_X, k=k, tol=tol ** 2, maxiter=maxiter, 

526 ncv=ncv, which=which, v0=v0) 

527 # arpack do not guarantee exactly orthonormal eigenvectors 

528 # for clustered eigenvalues, especially in complex arithmetic 

529 eigvec, _ = np.linalg.qr(eigvec) 

530 

531 # the eigenvectors eigvec must be orthonomal here; see gh-16712 

532 Av = X_matmat(eigvec) 

533 if not return_singular_vectors: 

534 s = svd(Av, compute_uv=False, overwrite_a=True) 

535 return s[::-1] 

536 

537 # compute the left singular vectors of X and update the right ones 

538 # accordingly 

539 u, s, vh = svd(Av, full_matrices=False, overwrite_a=True) 

540 u = u[:, ::-1] 

541 s = s[::-1] 

542 vh = vh[::-1] 

543 

544 jobu = return_singular_vectors in {True, 'u'} 

545 jobv = return_singular_vectors in {True, 'vh'} 

546 

547 if transpose: 

548 u_tmp = eigvec @ _herm(vh) if jobu else None 

549 vh = _herm(u) if jobv else None 

550 u = u_tmp 

551 else: 

552 if not jobu: 

553 u = None 

554 vh = vh @ _herm(eigvec) if jobv else None 

555 

556 return u, s, vh