test_svds.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  1. import os
  2. import re
  3. import copy
  4. import numpy as np
  5. from numpy.testing import assert_allclose, assert_equal, assert_array_equal
  6. import pytest
  7. from scipy.linalg import svd, null_space
  8. from scipy.sparse import csc_matrix, isspmatrix, spdiags, random
  9. from scipy.sparse.linalg import LinearOperator, aslinearoperator
  10. if os.environ.get("SCIPY_USE_PROPACK"):
  11. has_propack = True
  12. else:
  13. has_propack = False
  14. from scipy.sparse.linalg import svds
  15. from scipy.sparse.linalg._eigen.arpack import ArpackNoConvergence
  16. # --- Helper Functions / Classes ---
  17. def sorted_svd(m, k, which='LM'):
  18. # Compute svd of a dense matrix m, and return singular vectors/values
  19. # sorted.
  20. if isspmatrix(m):
  21. m = m.toarray()
  22. u, s, vh = svd(m)
  23. if which == 'LM':
  24. ii = np.argsort(s)[-k:]
  25. elif which == 'SM':
  26. ii = np.argsort(s)[:k]
  27. else:
  28. raise ValueError("unknown which=%r" % (which,))
  29. return u[:, ii], s[ii], vh[ii]
  30. def svd_estimate(u, s, vh):
  31. return np.dot(u, np.dot(np.diag(s), vh))
  32. def _check_svds(A, k, u, s, vh, which="LM", check_usvh_A=False,
  33. check_svd=True, atol=1e-10, rtol=1e-7):
  34. n, m = A.shape
  35. # Check shapes.
  36. assert_equal(u.shape, (n, k))
  37. assert_equal(s.shape, (k,))
  38. assert_equal(vh.shape, (k, m))
  39. # Check that the original matrix can be reconstituted.
  40. A_rebuilt = (u*s).dot(vh)
  41. assert_equal(A_rebuilt.shape, A.shape)
  42. if check_usvh_A:
  43. assert_allclose(A_rebuilt, A, atol=atol, rtol=rtol)
  44. # Check that u is a semi-orthogonal matrix.
  45. uh_u = np.dot(u.T.conj(), u)
  46. assert_equal(uh_u.shape, (k, k))
  47. assert_allclose(uh_u, np.identity(k), atol=atol, rtol=rtol)
  48. # Check that vh is a semi-orthogonal matrix.
  49. vh_v = np.dot(vh, vh.T.conj())
  50. assert_equal(vh_v.shape, (k, k))
  51. assert_allclose(vh_v, np.identity(k), atol=atol, rtol=rtol)
  52. # Check that scipy.sparse.linalg.svds ~ scipy.linalg.svd
  53. if check_svd:
  54. u2, s2, vh2 = sorted_svd(A, k, which)
  55. assert_allclose(np.abs(u), np.abs(u2), atol=atol, rtol=rtol)
  56. assert_allclose(s, s2, atol=atol, rtol=rtol)
  57. assert_allclose(np.abs(vh), np.abs(vh2), atol=atol, rtol=rtol)
  58. def _check_svds_n(A, k, u, s, vh, which="LM", check_res=True,
  59. check_svd=True, atol=1e-10, rtol=1e-7):
  60. n, m = A.shape
  61. # Check shapes.
  62. assert_equal(u.shape, (n, k))
  63. assert_equal(s.shape, (k,))
  64. assert_equal(vh.shape, (k, m))
  65. # Check that u is a semi-orthogonal matrix.
  66. uh_u = np.dot(u.T.conj(), u)
  67. assert_equal(uh_u.shape, (k, k))
  68. error = np.sum(np.abs(uh_u - np.identity(k))) / (k * k)
  69. assert_allclose(error, 0.0, atol=atol, rtol=rtol)
  70. # Check that vh is a semi-orthogonal matrix.
  71. vh_v = np.dot(vh, vh.T.conj())
  72. assert_equal(vh_v.shape, (k, k))
  73. error = np.sum(np.abs(vh_v - np.identity(k))) / (k * k)
  74. assert_allclose(error, 0.0, atol=atol, rtol=rtol)
  75. # Check residuals
  76. if check_res:
  77. ru = A.T.conj() @ u - vh.T.conj() * s
  78. rus = np.sum(np.abs(ru)) / (n * k)
  79. rvh = A @ vh.T.conj() - u * s
  80. rvhs = np.sum(np.abs(rvh)) / (m * k)
  81. assert_allclose(rus, 0.0, atol=atol, rtol=rtol)
  82. assert_allclose(rvhs, 0.0, atol=atol, rtol=rtol)
  83. # Check that scipy.sparse.linalg.svds ~ scipy.linalg.svd
  84. if check_svd:
  85. u2, s2, vh2 = sorted_svd(A, k, which)
  86. assert_allclose(s, s2, atol=atol, rtol=rtol)
  87. A_rebuilt_svd = (u2*s2).dot(vh2)
  88. A_rebuilt = (u*s).dot(vh)
  89. assert_equal(A_rebuilt.shape, A.shape)
  90. error = np.sum(np.abs(A_rebuilt_svd - A_rebuilt)) / (k * k)
  91. assert_allclose(error, 0.0, atol=atol, rtol=rtol)
  92. class CheckingLinearOperator(LinearOperator):
  93. def __init__(self, A):
  94. self.A = A
  95. self.dtype = A.dtype
  96. self.shape = A.shape
  97. def _matvec(self, x):
  98. assert_equal(max(x.shape), np.size(x))
  99. return self.A.dot(x)
  100. def _rmatvec(self, x):
  101. assert_equal(max(x.shape), np.size(x))
  102. return self.A.T.conjugate().dot(x)
  103. # --- Test Input Validation ---
  104. # Tests input validation on parameters `k` and `which`.
  105. # Needs better input validation checks for all other parameters.
  106. class SVDSCommonTests:
  107. solver = None
  108. # some of these IV tests could run only once, say with solver=None
  109. _A_empty_msg = "`A` must not be empty."
  110. _A_dtype_msg = "`A` must be of floating or complex floating data type"
  111. _A_type_msg = "type not understood"
  112. _A_ndim_msg = "array must have ndim <= 2"
  113. _A_validation_inputs = [
  114. (np.asarray([[]]), ValueError, _A_empty_msg),
  115. (np.asarray([[1, 2], [3, 4]]), ValueError, _A_dtype_msg),
  116. ("hi", TypeError, _A_type_msg),
  117. (np.asarray([[[1., 2.], [3., 4.]]]), ValueError, _A_ndim_msg)]
  118. @pytest.mark.parametrize("args", _A_validation_inputs)
  119. def test_svds_input_validation_A(self, args):
  120. A, error_type, message = args
  121. with pytest.raises(error_type, match=message):
  122. svds(A, k=1, solver=self.solver)
  123. @pytest.mark.parametrize("k", [-1, 0, 3, 4, 5, 1.5, "1"])
  124. def test_svds_input_validation_k_1(self, k):
  125. rng = np.random.default_rng(0)
  126. A = rng.random((4, 3))
  127. # propack can do complete SVD
  128. if self.solver == 'propack' and k == 3:
  129. if not has_propack:
  130. pytest.skip("PROPACK not enabled")
  131. res = svds(A, k=k, solver=self.solver)
  132. _check_svds(A, k, *res, check_usvh_A=True, check_svd=True)
  133. return
  134. message = ("`k` must be an integer satisfying")
  135. with pytest.raises(ValueError, match=message):
  136. svds(A, k=k, solver=self.solver)
  137. def test_svds_input_validation_k_2(self):
  138. # I think the stack trace is reasonable when `k` can't be converted
  139. # to an int.
  140. message = "int() argument must be a"
  141. with pytest.raises(TypeError, match=re.escape(message)):
  142. svds(np.eye(10), k=[], solver=self.solver)
  143. message = "invalid literal for int()"
  144. with pytest.raises(ValueError, match=message):
  145. svds(np.eye(10), k="hi", solver=self.solver)
  146. @pytest.mark.parametrize("tol", (-1, np.inf, np.nan))
  147. def test_svds_input_validation_tol_1(self, tol):
  148. message = "`tol` must be a non-negative floating point value."
  149. with pytest.raises(ValueError, match=message):
  150. svds(np.eye(10), tol=tol, solver=self.solver)
  151. @pytest.mark.parametrize("tol", ([], 'hi'))
  152. def test_svds_input_validation_tol_2(self, tol):
  153. # I think the stack trace is reasonable here
  154. message = "'<' not supported between instances"
  155. with pytest.raises(TypeError, match=message):
  156. svds(np.eye(10), tol=tol, solver=self.solver)
  157. @pytest.mark.parametrize("which", ('LA', 'SA', 'ekki', 0))
  158. def test_svds_input_validation_which(self, which):
  159. # Regression test for a github issue.
  160. # https://github.com/scipy/scipy/issues/4590
  161. # Function was not checking for eigenvalue type and unintended
  162. # values could be returned.
  163. with pytest.raises(ValueError, match="`which` must be in"):
  164. svds(np.eye(10), which=which, solver=self.solver)
  165. @pytest.mark.parametrize("transpose", (True, False))
  166. @pytest.mark.parametrize("n", range(4, 9))
  167. def test_svds_input_validation_v0_1(self, transpose, n):
  168. rng = np.random.default_rng(0)
  169. A = rng.random((5, 7))
  170. v0 = rng.random(n)
  171. if transpose:
  172. A = A.T
  173. k = 2
  174. message = "`v0` must have shape"
  175. required_length = (A.shape[0] if self.solver == 'propack'
  176. else min(A.shape))
  177. if n != required_length:
  178. with pytest.raises(ValueError, match=message):
  179. svds(A, k=k, v0=v0, solver=self.solver)
  180. def test_svds_input_validation_v0_2(self):
  181. A = np.ones((10, 10))
  182. v0 = np.ones((1, 10))
  183. message = "`v0` must have shape"
  184. with pytest.raises(ValueError, match=message):
  185. svds(A, k=1, v0=v0, solver=self.solver)
  186. @pytest.mark.parametrize("v0", ("hi", 1, np.ones(10, dtype=int)))
  187. def test_svds_input_validation_v0_3(self, v0):
  188. A = np.ones((10, 10))
  189. message = "`v0` must be of floating or complex floating data type."
  190. with pytest.raises(ValueError, match=message):
  191. svds(A, k=1, v0=v0, solver=self.solver)
  192. @pytest.mark.parametrize("maxiter", (-1, 0, 5.5))
  193. def test_svds_input_validation_maxiter_1(self, maxiter):
  194. message = ("`maxiter` must be a positive integer.")
  195. with pytest.raises(ValueError, match=message):
  196. svds(np.eye(10), maxiter=maxiter, solver=self.solver)
  197. def test_svds_input_validation_maxiter_2(self):
  198. # I think the stack trace is reasonable when `k` can't be converted
  199. # to an int.
  200. message = "int() argument must be a"
  201. with pytest.raises(TypeError, match=re.escape(message)):
  202. svds(np.eye(10), maxiter=[], solver=self.solver)
  203. message = "invalid literal for int()"
  204. with pytest.raises(ValueError, match=message):
  205. svds(np.eye(10), maxiter="hi", solver=self.solver)
  206. @pytest.mark.parametrize("rsv", ('ekki', 10))
  207. def test_svds_input_validation_return_singular_vectors(self, rsv):
  208. message = "`return_singular_vectors` must be in"
  209. with pytest.raises(ValueError, match=message):
  210. svds(np.eye(10), return_singular_vectors=rsv, solver=self.solver)
  211. # --- Test Parameters ---
  212. @pytest.mark.parametrize("k", [3, 5])
  213. @pytest.mark.parametrize("which", ["LM", "SM"])
  214. def test_svds_parameter_k_which(self, k, which):
  215. if self.solver == 'propack':
  216. if not has_propack:
  217. pytest.skip("PROPACK not available")
  218. # check that the `k` parameter sets the number of eigenvalues/
  219. # eigenvectors returned.
  220. # Also check that the `which` parameter sets whether the largest or
  221. # smallest eigenvalues are returned
  222. rng = np.random.default_rng(0)
  223. A = rng.random((10, 10))
  224. if self.solver == 'lobpcg':
  225. with pytest.warns(UserWarning, match="The problem size"):
  226. res = svds(A, k=k, which=which, solver=self.solver,
  227. random_state=0)
  228. else:
  229. res = svds(A, k=k, which=which, solver=self.solver,
  230. random_state=0)
  231. _check_svds(A, k, *res, which=which, atol=8e-10)
  232. # loop instead of parametrize for simplicity
  233. def test_svds_parameter_tol(self):
  234. if self.solver == 'propack':
  235. if not has_propack:
  236. pytest.skip("PROPACK not available")
  237. return # TODO: needs work, disabling for now
  238. # check the effect of the `tol` parameter on solver accuracy by solving
  239. # the same problem with varying `tol` and comparing the eigenvalues
  240. # against ground truth computed
  241. n = 100 # matrix size
  242. k = 3 # number of eigenvalues to check
  243. # generate a random, sparse-ish matrix
  244. # effect isn't apparent for matrices that are too small
  245. rng = np.random.default_rng(0)
  246. A = rng.random((n, n))
  247. A[A > .1] = 0
  248. A = A @ A.T
  249. _, s, _ = svd(A) # calculate ground truth
  250. # calculate the error as a function of `tol`
  251. A = csc_matrix(A)
  252. def err(tol):
  253. if self.solver == 'lobpcg' and tol == 1e-4:
  254. with pytest.warns(UserWarning, match="Exited at iteration"):
  255. _, s2, _ = svds(A, k=k, v0=np.ones(n),
  256. solver=self.solver, tol=tol)
  257. else:
  258. _, s2, _ = svds(A, k=k, v0=np.ones(n),
  259. solver=self.solver, tol=tol)
  260. return np.linalg.norm((s2 - s[k-1::-1])/s[k-1::-1])
  261. tols = [1e-4, 1e-2, 1e0] # tolerance levels to check
  262. # for 'arpack' and 'propack', accuracies make discrete steps
  263. accuracies = {'propack': [1e-12, 1e-6, 1e-4],
  264. 'arpack': [2e-15, 1e-10, 1e-10],
  265. 'lobpcg': [1e-11, 1e-3, 10]}
  266. for tol, accuracy in zip(tols, accuracies[self.solver]):
  267. error = err(tol)
  268. assert error < accuracy
  269. assert error > accuracy/10
  270. def test_svd_v0(self):
  271. if self.solver == 'propack':
  272. if not has_propack:
  273. pytest.skip("PROPACK not available")
  274. # check that the `v0` parameter affects the solution
  275. n = 100
  276. k = 1
  277. # If k != 1, LOBPCG needs more initial vectors, which are generated
  278. # with random_state, so it does not pass w/ k >= 2.
  279. # For some other values of `n`, the AssertionErrors are not raised
  280. # with different v0s, which is reasonable.
  281. rng = np.random.default_rng(0)
  282. A = rng.random((n, n))
  283. # with the same v0, solutions are the same, and they are accurate
  284. # v0 takes precedence over random_state
  285. v0a = rng.random(n)
  286. res1a = svds(A, k, v0=v0a, solver=self.solver, random_state=0)
  287. res2a = svds(A, k, v0=v0a, solver=self.solver, random_state=1)
  288. assert_equal(res1a, res2a)
  289. _check_svds(A, k, *res1a)
  290. # with the same v0, solutions are the same, and they are accurate
  291. v0b = rng.random(n)
  292. res1b = svds(A, k, v0=v0b, solver=self.solver, random_state=2)
  293. res2b = svds(A, k, v0=v0b, solver=self.solver, random_state=3)
  294. assert_equal(res1b, res2b)
  295. _check_svds(A, k, *res1b)
  296. # with different v0, solutions can be numerically different
  297. message = "Arrays are not equal"
  298. with pytest.raises(AssertionError, match=message):
  299. assert_equal(res1a, res1b)
  300. def test_svd_random_state(self):
  301. if self.solver == 'propack':
  302. if not has_propack:
  303. pytest.skip("PROPACK not available")
  304. # check that the `random_state` parameter affects the solution
  305. # Admittedly, `n` and `k` are chosen so that all solver pass all
  306. # these checks. That's a tall order, since LOBPCG doesn't want to
  307. # achieve the desired accuracy and ARPACK often returns the same
  308. # singular values/vectors for different v0.
  309. n = 100
  310. k = 1
  311. rng = np.random.default_rng(0)
  312. A = rng.random((n, n))
  313. # with the same random_state, solutions are the same and accurate
  314. res1a = svds(A, k, solver=self.solver, random_state=0)
  315. res2a = svds(A, k, solver=self.solver, random_state=0)
  316. assert_equal(res1a, res2a)
  317. _check_svds(A, k, *res1a)
  318. # with the same random_state, solutions are the same and accurate
  319. res1b = svds(A, k, solver=self.solver, random_state=1)
  320. res2b = svds(A, k, solver=self.solver, random_state=1)
  321. assert_equal(res1b, res2b)
  322. _check_svds(A, k, *res1b)
  323. # with different random_state, solutions can be numerically different
  324. message = "Arrays are not equal"
  325. with pytest.raises(AssertionError, match=message):
  326. assert_equal(res1a, res1b)
  327. @pytest.mark.parametrize("random_state", (0, 1,
  328. np.random.RandomState(0),
  329. np.random.default_rng(0)))
  330. def test_svd_random_state_2(self, random_state):
  331. if self.solver == 'propack':
  332. if not has_propack:
  333. pytest.skip("PROPACK not available")
  334. n = 100
  335. k = 1
  336. rng = np.random.default_rng(0)
  337. A = rng.random((n, n))
  338. random_state_2 = copy.deepcopy(random_state)
  339. # with the same random_state, solutions are the same and accurate
  340. res1a = svds(A, k, solver=self.solver, random_state=random_state)
  341. res2a = svds(A, k, solver=self.solver, random_state=random_state_2)
  342. assert_equal(res1a, res2a)
  343. _check_svds(A, k, *res1a)
  344. @pytest.mark.parametrize("random_state", (None,
  345. np.random.RandomState(0),
  346. np.random.default_rng(0)))
  347. def test_svd_random_state_3(self, random_state):
  348. if self.solver == 'propack':
  349. if not has_propack:
  350. pytest.skip("PROPACK not available")
  351. n = 100
  352. k = 5
  353. rng = np.random.default_rng(0)
  354. A = rng.random((n, n))
  355. # random_state in different state produces accurate - but not
  356. # not necessarily identical - results
  357. res1a = svds(A, k, solver=self.solver, random_state=random_state)
  358. res2a = svds(A, k, solver=self.solver, random_state=random_state)
  359. _check_svds(A, k, *res1a, atol=2e-10, rtol=1e-6)
  360. _check_svds(A, k, *res2a, atol=2e-10, rtol=1e-6)
  361. message = "Arrays are not equal"
  362. with pytest.raises(AssertionError, match=message):
  363. assert_equal(res1a, res2a)
  364. @pytest.mark.filterwarnings("ignore:Exited postprocessing")
  365. def test_svd_maxiter(self):
  366. # check that maxiter works as expected: should not return accurate
  367. # solution after 1 iteration, but should with default `maxiter`
  368. if self.solver == 'propack':
  369. if not has_propack:
  370. pytest.skip("PROPACK not available")
  371. A = np.diag(np.arange(9)).astype(np.float64)
  372. k = 1
  373. u, s, vh = sorted_svd(A, k)
  374. if self.solver == 'arpack':
  375. message = "ARPACK error -1: No convergence"
  376. with pytest.raises(ArpackNoConvergence, match=message):
  377. svds(A, k, ncv=3, maxiter=1, solver=self.solver)
  378. elif self.solver == 'lobpcg':
  379. with pytest.warns(UserWarning, match="Exited at iteration"):
  380. svds(A, k, maxiter=1, solver=self.solver)
  381. elif self.solver == 'propack':
  382. message = "k=1 singular triplets did not converge within"
  383. with pytest.raises(np.linalg.LinAlgError, match=message):
  384. svds(A, k, maxiter=1, solver=self.solver)
  385. ud, sd, vhd = svds(A, k, solver=self.solver) # default maxiter
  386. _check_svds(A, k, ud, sd, vhd, atol=1e-8)
  387. assert_allclose(np.abs(ud), np.abs(u), atol=1e-8)
  388. assert_allclose(np.abs(vhd), np.abs(vh), atol=1e-8)
  389. assert_allclose(np.abs(sd), np.abs(s), atol=1e-9)
  390. @pytest.mark.parametrize("rsv", (True, False, 'u', 'vh'))
  391. @pytest.mark.parametrize("shape", ((5, 7), (6, 6), (7, 5)))
  392. def test_svd_return_singular_vectors(self, rsv, shape):
  393. # check that the return_singular_vectors parameter works as expected
  394. if self.solver == 'propack':
  395. if not has_propack:
  396. pytest.skip("PROPACK not available")
  397. rng = np.random.default_rng(0)
  398. A = rng.random(shape)
  399. k = 2
  400. M, N = shape
  401. u, s, vh = sorted_svd(A, k)
  402. respect_u = True if self.solver == 'propack' else M <= N
  403. respect_vh = True if self.solver == 'propack' else M > N
  404. if self.solver == 'lobpcg':
  405. with pytest.warns(UserWarning, match="The problem size"):
  406. if rsv is False:
  407. s2 = svds(A, k, return_singular_vectors=rsv,
  408. solver=self.solver, random_state=rng)
  409. assert_allclose(s2, s)
  410. elif rsv == 'u' and respect_u:
  411. u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
  412. solver=self.solver, random_state=rng)
  413. assert_allclose(np.abs(u2), np.abs(u))
  414. assert_allclose(s2, s)
  415. assert vh2 is None
  416. elif rsv == 'vh' and respect_vh:
  417. u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
  418. solver=self.solver, random_state=rng)
  419. assert u2 is None
  420. assert_allclose(s2, s)
  421. assert_allclose(np.abs(vh2), np.abs(vh))
  422. else:
  423. u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
  424. solver=self.solver, random_state=rng)
  425. if u2 is not None:
  426. assert_allclose(np.abs(u2), np.abs(u))
  427. assert_allclose(s2, s)
  428. if vh2 is not None:
  429. assert_allclose(np.abs(vh2), np.abs(vh))
  430. else:
  431. if rsv is False:
  432. s2 = svds(A, k, return_singular_vectors=rsv,
  433. solver=self.solver, random_state=rng)
  434. assert_allclose(s2, s)
  435. elif rsv == 'u' and respect_u:
  436. u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
  437. solver=self.solver, random_state=rng)
  438. assert_allclose(np.abs(u2), np.abs(u))
  439. assert_allclose(s2, s)
  440. assert vh2 is None
  441. elif rsv == 'vh' and respect_vh:
  442. u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
  443. solver=self.solver, random_state=rng)
  444. assert u2 is None
  445. assert_allclose(s2, s)
  446. assert_allclose(np.abs(vh2), np.abs(vh))
  447. else:
  448. u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
  449. solver=self.solver, random_state=rng)
  450. if u2 is not None:
  451. assert_allclose(np.abs(u2), np.abs(u))
  452. assert_allclose(s2, s)
  453. if vh2 is not None:
  454. assert_allclose(np.abs(vh2), np.abs(vh))
  455. # --- Test Basic Functionality ---
  456. # Tests the accuracy of each solver for real and complex matrices provided
  457. # as list, dense array, sparse matrix, and LinearOperator.
  458. A1 = [[1, 2, 3], [3, 4, 3], [1 + 1j, 0, 2], [0, 0, 1]]
  459. A2 = [[1, 2, 3, 8 + 5j], [3 - 2j, 4, 3, 5], [1, 0, 2, 3], [0, 0, 1, 0]]
  460. @pytest.mark.filterwarnings("ignore:k >= N - 1",
  461. reason="needed to demonstrate #16725")
  462. @pytest.mark.parametrize('A', (A1, A2))
  463. @pytest.mark.parametrize('k', range(1, 5))
  464. # PROPACK fails a lot if @pytest.mark.parametrize('which', ("SM", "LM"))
  465. @pytest.mark.parametrize('real', (True, False))
  466. @pytest.mark.parametrize('transpose', (False, True))
  467. # In gh-14299, it was suggested the `svds` should _not_ work with lists
  468. @pytest.mark.parametrize('lo_type', (np.asarray, csc_matrix,
  469. aslinearoperator))
  470. def test_svd_simple(self, A, k, real, transpose, lo_type):
  471. if self.solver == 'propack':
  472. if not has_propack:
  473. pytest.skip("PROPACK not available")
  474. A = np.asarray(A)
  475. A = np.real(A) if real else A
  476. A = A.T if transpose else A
  477. A2 = lo_type(A)
  478. # could check for the appropriate errors, but that is tested above
  479. if k > min(A.shape):
  480. pytest.skip("`k` cannot be greater than `min(A.shape)`")
  481. if self.solver != 'propack' and k >= min(A.shape):
  482. pytest.skip("Only PROPACK supports complete SVD")
  483. if self.solver == 'arpack' and not real and k == min(A.shape) - 1:
  484. pytest.skip("#16725")
  485. if self.solver == 'propack' and (np.intp(0).itemsize < 8 and not real):
  486. pytest.skip('PROPACK complex-valued SVD methods not available '
  487. 'for 32-bit builds')
  488. if self.solver == 'lobpcg':
  489. with pytest.warns(UserWarning, match="The problem size"):
  490. u, s, vh = svds(A2, k, solver=self.solver)
  491. else:
  492. u, s, vh = svds(A2, k, solver=self.solver)
  493. _check_svds(A, k, u, s, vh, atol=3e-10)
  494. def test_svd_linop(self):
  495. solver = self.solver
  496. if self.solver == 'propack':
  497. if not has_propack:
  498. pytest.skip("PROPACK not available")
  499. nmks = [(6, 7, 3),
  500. (9, 5, 4),
  501. (10, 8, 5)]
  502. def reorder(args):
  503. U, s, VH = args
  504. j = np.argsort(s)
  505. return U[:, j], s[j], VH[j, :]
  506. for n, m, k in nmks:
  507. # Test svds on a LinearOperator.
  508. A = np.random.RandomState(52).randn(n, m)
  509. L = CheckingLinearOperator(A)
  510. if solver == 'propack':
  511. v0 = np.ones(n)
  512. else:
  513. v0 = np.ones(min(A.shape))
  514. if solver == 'lobpcg':
  515. with pytest.warns(UserWarning, match="The problem size"):
  516. U1, s1, VH1 = reorder(svds(A, k, v0=v0, solver=solver))
  517. U2, s2, VH2 = reorder(svds(L, k, v0=v0, solver=solver))
  518. else:
  519. U1, s1, VH1 = reorder(svds(A, k, v0=v0, solver=solver))
  520. U2, s2, VH2 = reorder(svds(L, k, v0=v0, solver=solver))
  521. assert_allclose(np.abs(U1), np.abs(U2))
  522. assert_allclose(s1, s2)
  523. assert_allclose(np.abs(VH1), np.abs(VH2))
  524. assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)),
  525. np.dot(U2, np.dot(np.diag(s2), VH2)))
  526. # Try again with which="SM".
  527. A = np.random.RandomState(1909).randn(n, m)
  528. L = CheckingLinearOperator(A)
  529. # TODO: arpack crashes when v0=v0, which="SM"
  530. kwargs = {'v0': v0} if solver not in {None, 'arpack'} else {}
  531. if self.solver == 'lobpcg':
  532. with pytest.warns(UserWarning, match="The problem size"):
  533. U1, s1, VH1 = reorder(svds(A, k, which="SM", solver=solver,
  534. **kwargs))
  535. U2, s2, VH2 = reorder(svds(L, k, which="SM", solver=solver,
  536. **kwargs))
  537. else:
  538. U1, s1, VH1 = reorder(svds(A, k, which="SM", solver=solver,
  539. **kwargs))
  540. U2, s2, VH2 = reorder(svds(L, k, which="SM", solver=solver,
  541. **kwargs))
  542. assert_allclose(np.abs(U1), np.abs(U2))
  543. assert_allclose(s1 + 1, s2 + 1)
  544. assert_allclose(np.abs(VH1), np.abs(VH2))
  545. assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)),
  546. np.dot(U2, np.dot(np.diag(s2), VH2)))
  547. if k < min(n, m) - 1:
  548. # Complex input and explicit which="LM".
  549. for (dt, eps) in [(complex, 1e-7), (np.complex64, 1e-3)]:
  550. if self.solver == 'propack' and np.intp(0).itemsize < 8:
  551. pytest.skip('PROPACK complex-valued SVD methods '
  552. 'not available for 32-bit builds')
  553. rng = np.random.RandomState(1648)
  554. A = (rng.randn(n, m) + 1j * rng.randn(n, m)).astype(dt)
  555. L = CheckingLinearOperator(A)
  556. if self.solver == 'lobpcg':
  557. with pytest.warns(UserWarning,
  558. match="The problem size"):
  559. U1, s1, VH1 = reorder(svds(A, k, which="LM",
  560. solver=solver))
  561. U2, s2, VH2 = reorder(svds(L, k, which="LM",
  562. solver=solver))
  563. else:
  564. U1, s1, VH1 = reorder(svds(A, k, which="LM",
  565. solver=solver))
  566. U2, s2, VH2 = reorder(svds(L, k, which="LM",
  567. solver=solver))
  568. assert_allclose(np.abs(U1), np.abs(U2), rtol=eps)
  569. assert_allclose(s1, s2, rtol=eps)
  570. assert_allclose(np.abs(VH1), np.abs(VH2), rtol=eps)
  571. assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)),
  572. np.dot(U2, np.dot(np.diag(s2), VH2)),
  573. rtol=eps)
  574. SHAPES = ((100, 100), (100, 101), (101, 100))
  575. @pytest.mark.filterwarnings("ignore:Exited at iteration")
  576. @pytest.mark.filterwarnings("ignore:Exited postprocessing")
  577. @pytest.mark.parametrize("shape", SHAPES)
  578. # ARPACK supports only dtype float, complex, or np.float32
  579. @pytest.mark.parametrize("dtype", (float, complex, np.float32))
  580. def test_small_sigma_sparse(self, shape, dtype):
  581. # https://github.com/scipy/scipy/pull/11829
  582. solver = self.solver
  583. # 2do: PROPACK fails orthogonality of singular vectors
  584. # if dtype == complex and self.solver == 'propack':
  585. # pytest.skip("PROPACK unsupported for complex dtype")
  586. if solver == 'propack':
  587. pytest.skip("PROPACK failures unrelated to PR")
  588. rng = np.random.default_rng(0)
  589. k = 5
  590. (m, n) = shape
  591. S = random(m, n, density=0.1, random_state=rng)
  592. if dtype == complex:
  593. S = + 1j * random(m, n, density=0.1, random_state=rng)
  594. e = np.ones(m)
  595. e[0:5] *= 1e1 ** np.arange(-5, 0, 1)
  596. S = spdiags(e, 0, m, m) @ S
  597. S = S.astype(dtype)
  598. u, s, vh = svds(S, k, which='SM', solver=solver, maxiter=1000)
  599. c_svd = False # partial SVD can be different from full SVD
  600. _check_svds_n(S, k, u, s, vh, which="SM", check_svd=c_svd, atol=1e-1)
  601. # --- Test Edge Cases ---
  602. # Checks a few edge cases.
  603. @pytest.mark.parametrize("shape", ((6, 5), (5, 5), (5, 6)))
  604. @pytest.mark.parametrize("dtype", (float, complex))
  605. def test_svd_LM_ones_matrix(self, shape, dtype):
  606. # Check that svds can deal with matrix_rank less than k in LM mode.
  607. k = 3
  608. n, m = shape
  609. A = np.ones((n, m), dtype=dtype)
  610. if self.solver == 'lobpcg':
  611. with pytest.warns(UserWarning, match="The problem size"):
  612. U, s, VH = svds(A, k, solver=self.solver)
  613. else:
  614. U, s, VH = svds(A, k, solver=self.solver)
  615. _check_svds(A, k, U, s, VH, check_usvh_A=True, check_svd=False)
  616. # Check that the largest singular value is near sqrt(n*m)
  617. # and the other singular values have been forced to zero.
  618. assert_allclose(np.max(s), np.sqrt(n*m))
  619. s = np.array(sorted(s)[:-1]) + 1
  620. z = np.ones_like(s)
  621. assert_allclose(s, z)
  622. @pytest.mark.filterwarnings("ignore:k >= N - 1",
  623. reason="needed to demonstrate #16725")
  624. @pytest.mark.parametrize("shape", ((3, 4), (4, 4), (4, 3), (4, 2)))
  625. @pytest.mark.parametrize("dtype", (float, complex))
  626. def test_zero_matrix(self, shape, dtype):
  627. # Check that svds can deal with matrices containing only zeros;
  628. # see https://github.com/scipy/scipy/issues/3452/
  629. # shape = (4, 2) is included because it is the particular case
  630. # reported in the issue
  631. k = 1
  632. n, m = shape
  633. A = np.zeros((n, m), dtype=dtype)
  634. if (self.solver == 'arpack' and dtype is complex
  635. and k == min(A.shape) - 1):
  636. pytest.skip("#16725")
  637. if self.solver == 'propack':
  638. pytest.skip("PROPACK failures unrelated to PR #16712")
  639. if self.solver == 'lobpcg':
  640. with pytest.warns(UserWarning, match="The problem size"):
  641. U, s, VH = svds(A, k, solver=self.solver)
  642. else:
  643. U, s, VH = svds(A, k, solver=self.solver)
  644. # Check some generic properties of svd.
  645. _check_svds(A, k, U, s, VH, check_usvh_A=True, check_svd=False)
  646. # Check that the singular values are zero.
  647. assert_array_equal(s, 0)
  648. @pytest.mark.parametrize("shape", ((20, 20), (20, 21), (21, 20)))
  649. # ARPACK supports only dtype float, complex, or np.float32
  650. @pytest.mark.parametrize("dtype", (float, complex, np.float32))
  651. def test_small_sigma(self, shape, dtype):
  652. if not has_propack:
  653. pytest.skip("PROPACK not enabled")
  654. # https://github.com/scipy/scipy/pull/11829
  655. if dtype == complex and self.solver == 'propack':
  656. pytest.skip("PROPACK unsupported for complex dtype")
  657. rng = np.random.default_rng(179847540)
  658. A = rng.random(shape).astype(dtype)
  659. u, _, vh = svd(A, full_matrices=False)
  660. if dtype == np.float32:
  661. e = 10.0
  662. else:
  663. e = 100.0
  664. t = e**(-np.arange(len(vh))).astype(dtype)
  665. A = (u*t).dot(vh)
  666. k = 4
  667. u, s, vh = svds(A, k, solver=self.solver, maxiter=100)
  668. t = np.sum(s > 0)
  669. assert_equal(t, k)
  670. # LOBPCG needs larger atol and rtol to pass
  671. _check_svds_n(A, k, u, s, vh, atol=1e-3, rtol=1e0, check_svd=False)
  672. # ARPACK supports only dtype float, complex, or np.float32
  673. @pytest.mark.filterwarnings("ignore:The problem size")
  674. @pytest.mark.parametrize("dtype", (float, complex, np.float32))
  675. def test_small_sigma2(self, dtype):
  676. if self.solver == 'propack':
  677. if not has_propack:
  678. pytest.skip("PROPACK not enabled")
  679. elif dtype == np.float32:
  680. pytest.skip("Test failures in CI, see gh-17004")
  681. elif dtype == complex:
  682. # https://github.com/scipy/scipy/issues/11406
  683. pytest.skip("PROPACK unsupported for complex dtype")
  684. rng = np.random.default_rng(179847540)
  685. # create a 10x10 singular matrix with a 4-dim null space
  686. dim = 4
  687. size = 10
  688. x = rng.random((size, size-dim))
  689. y = x[:, :dim] * rng.random(dim)
  690. mat = np.hstack((x, y))
  691. mat = mat.astype(dtype)
  692. nz = null_space(mat)
  693. assert_equal(nz.shape[1], dim)
  694. # Tolerances atol and rtol adjusted to pass np.float32
  695. # Use non-sparse svd
  696. u, s, vh = svd(mat)
  697. # Singular values are 0:
  698. assert_allclose(s[-dim:], 0, atol=1e-6, rtol=1e0)
  699. # Smallest right singular vectors in null space:
  700. assert_allclose(mat @ vh[-dim:, :].T, 0, atol=1e-6, rtol=1e0)
  701. # Smallest singular values should be 0
  702. sp_mat = csc_matrix(mat)
  703. su, ss, svh = svds(sp_mat, k=dim, which='SM', solver=self.solver)
  704. # Smallest dim singular values are 0:
  705. assert_allclose(ss, 0, atol=1e-5, rtol=1e0)
  706. # Smallest singular vectors via svds in null space:
  707. n, m = mat.shape
  708. if n < m: # else the assert fails with some libraries unclear why
  709. assert_allclose(sp_mat.transpose() @ su, 0, atol=1e-5, rtol=1e0)
  710. assert_allclose(sp_mat @ svh.T, 0, atol=1e-5, rtol=1e0)
  711. # --- Perform tests with each solver ---
  712. class Test_SVDS_once():
  713. @pytest.mark.parametrize("solver", ['ekki', object])
  714. def test_svds_input_validation_solver(self, solver):
  715. message = "solver must be one of"
  716. with pytest.raises(ValueError, match=message):
  717. svds(np.ones((3, 4)), k=2, solver=solver)
  718. class Test_SVDS_ARPACK(SVDSCommonTests):
  719. def setup_method(self):
  720. self.solver = 'arpack'
  721. @pytest.mark.parametrize("ncv", list(range(-1, 8)) + [4.5, "5"])
  722. def test_svds_input_validation_ncv_1(self, ncv):
  723. rng = np.random.default_rng(0)
  724. A = rng.random((6, 7))
  725. k = 3
  726. if ncv in {4, 5}:
  727. u, s, vh = svds(A, k=k, ncv=ncv, solver=self.solver)
  728. # partial decomposition, so don't check that u@diag(s)@vh=A;
  729. # do check that scipy.sparse.linalg.svds ~ scipy.linalg.svd
  730. _check_svds(A, k, u, s, vh)
  731. else:
  732. message = ("`ncv` must be an integer satisfying")
  733. with pytest.raises(ValueError, match=message):
  734. svds(A, k=k, ncv=ncv, solver=self.solver)
  735. def test_svds_input_validation_ncv_2(self):
  736. # I think the stack trace is reasonable when `ncv` can't be converted
  737. # to an int.
  738. message = "int() argument must be a"
  739. with pytest.raises(TypeError, match=re.escape(message)):
  740. svds(np.eye(10), ncv=[], solver=self.solver)
  741. message = "invalid literal for int()"
  742. with pytest.raises(ValueError, match=message):
  743. svds(np.eye(10), ncv="hi", solver=self.solver)
  744. # I can't see a robust relationship between `ncv` and relevant outputs
  745. # (e.g. accuracy, time), so no test of the parameter.
  746. class Test_SVDS_LOBPCG(SVDSCommonTests):
  747. def setup_method(self):
  748. self.solver = 'lobpcg'
  749. def test_svd_random_state_3(self):
  750. pytest.xfail("LOBPCG is having trouble with accuracy.")
  751. class Test_SVDS_PROPACK(SVDSCommonTests):
  752. def setup_method(self):
  753. self.solver = 'propack'
  754. def test_svd_LM_ones_matrix(self):
  755. message = ("PROPACK does not return orthonormal singular vectors "
  756. "associated with zero singular values.")
  757. # There are some other issues with this matrix of all ones, e.g.
  758. # `which='sm'` and `k=1` returns the largest singular value
  759. pytest.xfail(message)
  760. def test_svd_LM_zeros_matrix(self):
  761. message = ("PROPACK does not return orthonormal singular vectors "
  762. "associated with zero singular values.")
  763. pytest.xfail(message)