test_graph_laplacian.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import pytest
  2. import numpy as np
  3. from numpy.testing import assert_allclose
  4. from pytest import raises as assert_raises
  5. from scipy import sparse
  6. from scipy.sparse import csgraph
  7. def check_int_type(mat):
  8. return np.issubdtype(mat.dtype, np.signedinteger) or np.issubdtype(
  9. mat.dtype, np.uint
  10. )
  11. def test_laplacian_value_error():
  12. for t in int, float, complex:
  13. for m in ([1, 1],
  14. [[[1]]],
  15. [[1, 2, 3], [4, 5, 6]],
  16. [[1, 2], [3, 4], [5, 5]]):
  17. A = np.array(m, dtype=t)
  18. assert_raises(ValueError, csgraph.laplacian, A)
  19. def _explicit_laplacian(x, normed=False):
  20. if sparse.issparse(x):
  21. x = x.toarray()
  22. x = np.asarray(x)
  23. y = -1.0 * x
  24. for j in range(y.shape[0]):
  25. y[j,j] = x[j,j+1:].sum() + x[j,:j].sum()
  26. if normed:
  27. d = np.diag(y).copy()
  28. d[d == 0] = 1.0
  29. y /= d[:,None]**.5
  30. y /= d[None,:]**.5
  31. return y
  32. def _check_symmetric_graph_laplacian(mat, normed, copy=True):
  33. if not hasattr(mat, 'shape'):
  34. mat = eval(mat, dict(np=np, sparse=sparse))
  35. if sparse.issparse(mat):
  36. sp_mat = mat
  37. mat = sp_mat.toarray()
  38. else:
  39. sp_mat = sparse.csr_matrix(mat)
  40. mat_copy = np.copy(mat)
  41. sp_mat_copy = sparse.csr_matrix(sp_mat, copy=True)
  42. n_nodes = mat.shape[0]
  43. explicit_laplacian = _explicit_laplacian(mat, normed=normed)
  44. laplacian = csgraph.laplacian(mat, normed=normed, copy=copy)
  45. sp_laplacian = csgraph.laplacian(sp_mat, normed=normed,
  46. copy=copy)
  47. if copy:
  48. assert_allclose(mat, mat_copy)
  49. _assert_allclose_sparse(sp_mat, sp_mat_copy)
  50. else:
  51. if not (normed and check_int_type(mat)):
  52. assert_allclose(laplacian, mat)
  53. if sp_mat.format == 'coo':
  54. _assert_allclose_sparse(sp_laplacian, sp_mat)
  55. assert_allclose(laplacian, sp_laplacian.toarray())
  56. for tested in [laplacian, sp_laplacian.toarray()]:
  57. if not normed:
  58. assert_allclose(tested.sum(axis=0), np.zeros(n_nodes))
  59. assert_allclose(tested.T, tested)
  60. assert_allclose(tested, explicit_laplacian)
  61. def test_symmetric_graph_laplacian():
  62. symmetric_mats = (
  63. 'np.arange(10) * np.arange(10)[:, np.newaxis]',
  64. 'np.ones((7, 7))',
  65. 'np.eye(19)',
  66. 'sparse.diags([1, 1], [-1, 1], shape=(4, 4))',
  67. 'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).toarray()',
  68. 'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).todense()',
  69. 'np.vander(np.arange(4)) + np.vander(np.arange(4)).T'
  70. )
  71. for mat in symmetric_mats:
  72. for normed in True, False:
  73. for copy in True, False:
  74. _check_symmetric_graph_laplacian(mat, normed, copy)
  75. def _assert_allclose_sparse(a, b, **kwargs):
  76. # helper function that can deal with sparse matrices
  77. if sparse.issparse(a):
  78. a = a.toarray()
  79. if sparse.issparse(b):
  80. b = b.toarray()
  81. assert_allclose(a, b, **kwargs)
  82. def _check_laplacian_dtype_none(
  83. A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type
  84. ):
  85. mat = arr_type(A, dtype=dtype)
  86. L, d = csgraph.laplacian(
  87. mat,
  88. normed=normed,
  89. return_diag=True,
  90. use_out_degree=use_out_degree,
  91. copy=copy,
  92. dtype=None,
  93. )
  94. if normed and check_int_type(mat):
  95. assert L.dtype == np.float64
  96. assert d.dtype == np.float64
  97. _assert_allclose_sparse(L, desired_L, atol=1e-12)
  98. _assert_allclose_sparse(d, desired_d, atol=1e-12)
  99. else:
  100. assert L.dtype == dtype
  101. assert d.dtype == dtype
  102. desired_L = np.asarray(desired_L).astype(dtype)
  103. desired_d = np.asarray(desired_d).astype(dtype)
  104. _assert_allclose_sparse(L, desired_L, atol=1e-12)
  105. _assert_allclose_sparse(d, desired_d, atol=1e-12)
  106. if not copy:
  107. if not (normed and check_int_type(mat)):
  108. if type(mat) is np.ndarray:
  109. assert_allclose(L, mat)
  110. elif mat.format == "coo":
  111. _assert_allclose_sparse(L, mat)
  112. def _check_laplacian_dtype(
  113. A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type
  114. ):
  115. mat = arr_type(A, dtype=dtype)
  116. L, d = csgraph.laplacian(
  117. mat,
  118. normed=normed,
  119. return_diag=True,
  120. use_out_degree=use_out_degree,
  121. copy=copy,
  122. dtype=dtype,
  123. )
  124. assert L.dtype == dtype
  125. assert d.dtype == dtype
  126. desired_L = np.asarray(desired_L).astype(dtype)
  127. desired_d = np.asarray(desired_d).astype(dtype)
  128. _assert_allclose_sparse(L, desired_L, atol=1e-12)
  129. _assert_allclose_sparse(d, desired_d, atol=1e-12)
  130. if not copy:
  131. if not (normed and check_int_type(mat)):
  132. if type(mat) is np.ndarray:
  133. assert_allclose(L, mat)
  134. elif mat.format == 'coo':
  135. _assert_allclose_sparse(L, mat)
  136. INT_DTYPES = {np.intc, np.int_, np.longlong}
  137. REAL_DTYPES = {np.single, np.double, np.longdouble}
  138. COMPLEX_DTYPES = {np.csingle, np.cdouble, np.clongdouble}
  139. # use sorted tuple to ensure fixed order of tests
  140. DTYPES = tuple(sorted(INT_DTYPES ^ REAL_DTYPES ^ COMPLEX_DTYPES, key=str))
  141. @pytest.mark.parametrize("dtype", DTYPES)
  142. @pytest.mark.parametrize("arr_type", [np.array,
  143. sparse.csr_matrix,
  144. sparse.coo_matrix])
  145. @pytest.mark.parametrize("copy", [True, False])
  146. @pytest.mark.parametrize("normed", [True, False])
  147. @pytest.mark.parametrize("use_out_degree", [True, False])
  148. def test_asymmetric_laplacian(use_out_degree, normed,
  149. copy, dtype, arr_type):
  150. # adjacency matrix
  151. A = [[0, 1, 0],
  152. [4, 2, 0],
  153. [0, 0, 0]]
  154. A = arr_type(np.array(A), dtype=dtype)
  155. A_copy = A.copy()
  156. if not normed and use_out_degree:
  157. # Laplacian matrix using out-degree
  158. L = [[1, -1, 0],
  159. [-4, 4, 0],
  160. [0, 0, 0]]
  161. d = [1, 4, 0]
  162. if normed and use_out_degree:
  163. # normalized Laplacian matrix using out-degree
  164. L = [[1, -0.5, 0],
  165. [-2, 1, 0],
  166. [0, 0, 0]]
  167. d = [1, 2, 1]
  168. if not normed and not use_out_degree:
  169. # Laplacian matrix using in-degree
  170. L = [[4, -1, 0],
  171. [-4, 1, 0],
  172. [0, 0, 0]]
  173. d = [4, 1, 0]
  174. if normed and not use_out_degree:
  175. # normalized Laplacian matrix using in-degree
  176. L = [[1, -0.5, 0],
  177. [-2, 1, 0],
  178. [0, 0, 0]]
  179. d = [2, 1, 1]
  180. _check_laplacian_dtype_none(
  181. A,
  182. L,
  183. d,
  184. normed=normed,
  185. use_out_degree=use_out_degree,
  186. copy=copy,
  187. dtype=dtype,
  188. arr_type=arr_type,
  189. )
  190. _check_laplacian_dtype(
  191. A_copy,
  192. L,
  193. d,
  194. normed=normed,
  195. use_out_degree=use_out_degree,
  196. copy=copy,
  197. dtype=dtype,
  198. arr_type=arr_type,
  199. )
  200. @pytest.mark.parametrize("fmt", ['csr', 'csc', 'coo', 'lil',
  201. 'dok', 'dia', 'bsr'])
  202. @pytest.mark.parametrize("normed", [True, False])
  203. @pytest.mark.parametrize("copy", [True, False])
  204. def test_sparse_formats(fmt, normed, copy):
  205. mat = sparse.diags([1, 1], [-1, 1], shape=(4, 4), format=fmt)
  206. _check_symmetric_graph_laplacian(mat, normed, copy)
  207. @pytest.mark.parametrize(
  208. "arr_type", [np.asarray, sparse.csr_matrix, sparse.coo_matrix]
  209. )
  210. @pytest.mark.parametrize("form", ["array", "function", "lo"])
  211. def test_laplacian_symmetrized(arr_type, form):
  212. # adjacency matrix
  213. n = 3
  214. mat = arr_type(np.arange(n * n).reshape(n, n))
  215. L_in, d_in = csgraph.laplacian(
  216. mat,
  217. return_diag=True,
  218. form=form,
  219. )
  220. L_out, d_out = csgraph.laplacian(
  221. mat,
  222. return_diag=True,
  223. use_out_degree=True,
  224. form=form,
  225. )
  226. Ls, ds = csgraph.laplacian(
  227. mat,
  228. return_diag=True,
  229. symmetrized=True,
  230. form=form,
  231. )
  232. Ls_normed, ds_normed = csgraph.laplacian(
  233. mat,
  234. return_diag=True,
  235. symmetrized=True,
  236. normed=True,
  237. form=form,
  238. )
  239. mat += mat.T
  240. Lss, dss = csgraph.laplacian(mat, return_diag=True, form=form)
  241. Lss_normed, dss_normed = csgraph.laplacian(
  242. mat,
  243. return_diag=True,
  244. normed=True,
  245. form=form,
  246. )
  247. assert_allclose(ds, d_in + d_out)
  248. assert_allclose(ds, dss)
  249. assert_allclose(ds_normed, dss_normed)
  250. d = {}
  251. for L in ["L_in", "L_out", "Ls", "Ls_normed", "Lss", "Lss_normed"]:
  252. if form == "array":
  253. d[L] = eval(L)
  254. else:
  255. d[L] = eval(L)(np.eye(n, dtype=mat.dtype))
  256. _assert_allclose_sparse(d["Ls"], d["L_in"] + d["L_out"].T)
  257. _assert_allclose_sparse(d["Ls"], d["Lss"])
  258. _assert_allclose_sparse(d["Ls_normed"], d["Lss_normed"])
  259. @pytest.mark.parametrize(
  260. "arr_type", [np.asarray, sparse.csr_matrix, sparse.coo_matrix]
  261. )
  262. @pytest.mark.parametrize("dtype", DTYPES)
  263. @pytest.mark.parametrize("normed", [True, False])
  264. @pytest.mark.parametrize("symmetrized", [True, False])
  265. @pytest.mark.parametrize("use_out_degree", [True, False])
  266. @pytest.mark.parametrize("form", ["function", "lo"])
  267. def test_format(dtype, arr_type, normed, symmetrized, use_out_degree, form):
  268. n = 3
  269. mat = [[0, 1, 0], [4, 2, 0], [0, 0, 0]]
  270. mat = arr_type(np.array(mat), dtype=dtype)
  271. Lo, do = csgraph.laplacian(
  272. mat,
  273. return_diag=True,
  274. normed=normed,
  275. symmetrized=symmetrized,
  276. use_out_degree=use_out_degree,
  277. dtype=dtype,
  278. )
  279. La, da = csgraph.laplacian(
  280. mat,
  281. return_diag=True,
  282. normed=normed,
  283. symmetrized=symmetrized,
  284. use_out_degree=use_out_degree,
  285. dtype=dtype,
  286. form="array",
  287. )
  288. assert_allclose(do, da)
  289. _assert_allclose_sparse(Lo, La)
  290. L, d = csgraph.laplacian(
  291. mat,
  292. return_diag=True,
  293. normed=normed,
  294. symmetrized=symmetrized,
  295. use_out_degree=use_out_degree,
  296. dtype=dtype,
  297. form=form,
  298. )
  299. assert_allclose(d, do)
  300. assert d.dtype == dtype
  301. Lm = L(np.eye(n, dtype=mat.dtype)).astype(dtype)
  302. _assert_allclose_sparse(Lm, Lo, rtol=2e-7, atol=2e-7)
  303. x = np.arange(6).reshape(3, 2)
  304. if not (normed and dtype in INT_DTYPES):
  305. assert_allclose(L(x), Lo @ x)
  306. else:
  307. # Normalized Lo is casted to integer, but L() is not
  308. pass
  309. def test_format_error_message():
  310. with pytest.raises(ValueError, match="Invalid form: 'toto'"):
  311. _ = csgraph.laplacian(np.eye(1), form='toto')