laplacianmatrix.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. """Laplacian matrix of graphs.
  2. """
  3. import networkx as nx
  4. from networkx.utils import not_implemented_for
  5. __all__ = [
  6. "laplacian_matrix",
  7. "normalized_laplacian_matrix",
  8. "total_spanning_tree_weight",
  9. "directed_laplacian_matrix",
  10. "directed_combinatorial_laplacian_matrix",
  11. ]
  12. @not_implemented_for("directed")
  13. def laplacian_matrix(G, nodelist=None, weight="weight"):
  14. """Returns the Laplacian matrix of G.
  15. The graph Laplacian is the matrix L = D - A, where
  16. A is the adjacency matrix and D is the diagonal matrix of node degrees.
  17. Parameters
  18. ----------
  19. G : graph
  20. A NetworkX graph
  21. nodelist : list, optional
  22. The rows and columns are ordered according to the nodes in nodelist.
  23. If nodelist is None, then the ordering is produced by G.nodes().
  24. weight : string or None, optional (default='weight')
  25. The edge data key used to compute each value in the matrix.
  26. If None, then each edge has weight 1.
  27. Returns
  28. -------
  29. L : SciPy sparse array
  30. The Laplacian matrix of G.
  31. Notes
  32. -----
  33. For MultiGraph, the edges weights are summed.
  34. See Also
  35. --------
  36. to_numpy_array
  37. normalized_laplacian_matrix
  38. laplacian_spectrum
  39. Examples
  40. --------
  41. For graphs with multiple connected components, L is permutation-similar
  42. to a block diagonal matrix where each block is the respective Laplacian
  43. matrix for each component.
  44. >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)])
  45. >>> print(nx.laplacian_matrix(G).toarray())
  46. [[ 1 -1 0 0 0]
  47. [-1 2 -1 0 0]
  48. [ 0 -1 1 0 0]
  49. [ 0 0 0 1 -1]
  50. [ 0 0 0 -1 1]]
  51. """
  52. import scipy as sp
  53. import scipy.sparse # call as sp.sparse
  54. if nodelist is None:
  55. nodelist = list(G)
  56. A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr")
  57. n, m = A.shape
  58. # TODO: rm csr_array wrapper when spdiags can produce arrays
  59. D = sp.sparse.csr_array(sp.sparse.spdiags(A.sum(axis=1), 0, m, n, format="csr"))
  60. return D - A
  61. @not_implemented_for("directed")
  62. def normalized_laplacian_matrix(G, nodelist=None, weight="weight"):
  63. r"""Returns the normalized Laplacian matrix of G.
  64. The normalized graph Laplacian is the matrix
  65. .. math::
  66. N = D^{-1/2} L D^{-1/2}
  67. where `L` is the graph Laplacian and `D` is the diagonal matrix of
  68. node degrees [1]_.
  69. Parameters
  70. ----------
  71. G : graph
  72. A NetworkX graph
  73. nodelist : list, optional
  74. The rows and columns are ordered according to the nodes in nodelist.
  75. If nodelist is None, then the ordering is produced by G.nodes().
  76. weight : string or None, optional (default='weight')
  77. The edge data key used to compute each value in the matrix.
  78. If None, then each edge has weight 1.
  79. Returns
  80. -------
  81. N : SciPy sparse array
  82. The normalized Laplacian matrix of G.
  83. Notes
  84. -----
  85. For MultiGraph, the edges weights are summed.
  86. See :func:`to_numpy_array` for other options.
  87. If the Graph contains selfloops, D is defined as ``diag(sum(A, 1))``, where A is
  88. the adjacency matrix [2]_.
  89. See Also
  90. --------
  91. laplacian_matrix
  92. normalized_laplacian_spectrum
  93. References
  94. ----------
  95. .. [1] Fan Chung-Graham, Spectral Graph Theory,
  96. CBMS Regional Conference Series in Mathematics, Number 92, 1997.
  97. .. [2] Steve Butler, Interlacing For Weighted Graphs Using The Normalized
  98. Laplacian, Electronic Journal of Linear Algebra, Volume 16, pp. 90-98,
  99. March 2007.
  100. """
  101. import numpy as np
  102. import scipy as sp
  103. import scipy.sparse # call as sp.sparse
  104. if nodelist is None:
  105. nodelist = list(G)
  106. A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr")
  107. n, m = A.shape
  108. diags = A.sum(axis=1)
  109. # TODO: rm csr_array wrapper when spdiags can produce arrays
  110. D = sp.sparse.csr_array(sp.sparse.spdiags(diags, 0, m, n, format="csr"))
  111. L = D - A
  112. with sp.errstate(divide="ignore"):
  113. diags_sqrt = 1.0 / np.sqrt(diags)
  114. diags_sqrt[np.isinf(diags_sqrt)] = 0
  115. # TODO: rm csr_array wrapper when spdiags can produce arrays
  116. DH = sp.sparse.csr_array(sp.sparse.spdiags(diags_sqrt, 0, m, n, format="csr"))
  117. return DH @ (L @ DH)
  118. def total_spanning_tree_weight(G, weight=None):
  119. """
  120. Returns the total weight of all spanning trees of `G`.
  121. Kirchoff's Tree Matrix Theorem states that the determinant of any cofactor of the
  122. Laplacian matrix of a graph is the number of spanning trees in the graph. For a
  123. weighted Laplacian matrix, it is the sum across all spanning trees of the
  124. multiplicative weight of each tree. That is, the weight of each tree is the
  125. product of its edge weights.
  126. Parameters
  127. ----------
  128. G : NetworkX Graph
  129. The graph to use Kirchhoff's theorem on.
  130. weight : string or None
  131. The key for the edge attribute holding the edge weight. If `None`, then
  132. each edge is assumed to have a weight of 1 and this function returns the
  133. total number of spanning trees in `G`.
  134. Returns
  135. -------
  136. float
  137. The sum of the total multiplicative weights for all spanning trees in `G`
  138. """
  139. import numpy as np
  140. G_laplacian = nx.laplacian_matrix(G, weight=weight).toarray()
  141. # Determinant ignoring first row and column
  142. return abs(np.linalg.det(G_laplacian[1:, 1:]))
  143. ###############################################################################
  144. # Code based on work from https://github.com/bjedwards
  145. @not_implemented_for("undirected")
  146. @not_implemented_for("multigraph")
  147. def directed_laplacian_matrix(
  148. G, nodelist=None, weight="weight", walk_type=None, alpha=0.95
  149. ):
  150. r"""Returns the directed Laplacian matrix of G.
  151. The graph directed Laplacian is the matrix
  152. .. math::
  153. L = I - (\Phi^{1/2} P \Phi^{-1/2} + \Phi^{-1/2} P^T \Phi^{1/2} ) / 2
  154. where `I` is the identity matrix, `P` is the transition matrix of the
  155. graph, and `\Phi` a matrix with the Perron vector of `P` in the diagonal and
  156. zeros elsewhere [1]_.
  157. Depending on the value of walk_type, `P` can be the transition matrix
  158. induced by a random walk, a lazy random walk, or a random walk with
  159. teleportation (PageRank).
  160. Parameters
  161. ----------
  162. G : DiGraph
  163. A NetworkX graph
  164. nodelist : list, optional
  165. The rows and columns are ordered according to the nodes in nodelist.
  166. If nodelist is None, then the ordering is produced by G.nodes().
  167. weight : string or None, optional (default='weight')
  168. The edge data key used to compute each value in the matrix.
  169. If None, then each edge has weight 1.
  170. walk_type : string or None, optional (default=None)
  171. If None, `P` is selected depending on the properties of the
  172. graph. Otherwise is one of 'random', 'lazy', or 'pagerank'
  173. alpha : real
  174. (1 - alpha) is the teleportation probability used with pagerank
  175. Returns
  176. -------
  177. L : NumPy matrix
  178. Normalized Laplacian of G.
  179. Notes
  180. -----
  181. Only implemented for DiGraphs
  182. See Also
  183. --------
  184. laplacian_matrix
  185. References
  186. ----------
  187. .. [1] Fan Chung (2005).
  188. Laplacians and the Cheeger inequality for directed graphs.
  189. Annals of Combinatorics, 9(1), 2005
  190. """
  191. import numpy as np
  192. import scipy as sp
  193. import scipy.sparse # call as sp.sparse
  194. import scipy.sparse.linalg # call as sp.sparse.linalg
  195. # NOTE: P has type ndarray if walk_type=="pagerank", else csr_array
  196. P = _transition_matrix(
  197. G, nodelist=nodelist, weight=weight, walk_type=walk_type, alpha=alpha
  198. )
  199. n, m = P.shape
  200. evals, evecs = sp.sparse.linalg.eigs(P.T, k=1)
  201. v = evecs.flatten().real
  202. p = v / v.sum()
  203. sqrtp = np.sqrt(p)
  204. Q = (
  205. # TODO: rm csr_array wrapper when spdiags creates arrays
  206. sp.sparse.csr_array(sp.sparse.spdiags(sqrtp, 0, n, n))
  207. @ P
  208. # TODO: rm csr_array wrapper when spdiags creates arrays
  209. @ sp.sparse.csr_array(sp.sparse.spdiags(1.0 / sqrtp, 0, n, n))
  210. )
  211. # NOTE: This could be sparsified for the non-pagerank cases
  212. I = np.identity(len(G))
  213. return I - (Q + Q.T) / 2.0
  214. @not_implemented_for("undirected")
  215. @not_implemented_for("multigraph")
  216. def directed_combinatorial_laplacian_matrix(
  217. G, nodelist=None, weight="weight", walk_type=None, alpha=0.95
  218. ):
  219. r"""Return the directed combinatorial Laplacian matrix of G.
  220. The graph directed combinatorial Laplacian is the matrix
  221. .. math::
  222. L = \Phi - (\Phi P + P^T \Phi) / 2
  223. where `P` is the transition matrix of the graph and `\Phi` a matrix
  224. with the Perron vector of `P` in the diagonal and zeros elsewhere [1]_.
  225. Depending on the value of walk_type, `P` can be the transition matrix
  226. induced by a random walk, a lazy random walk, or a random walk with
  227. teleportation (PageRank).
  228. Parameters
  229. ----------
  230. G : DiGraph
  231. A NetworkX graph
  232. nodelist : list, optional
  233. The rows and columns are ordered according to the nodes in nodelist.
  234. If nodelist is None, then the ordering is produced by G.nodes().
  235. weight : string or None, optional (default='weight')
  236. The edge data key used to compute each value in the matrix.
  237. If None, then each edge has weight 1.
  238. walk_type : string or None, optional (default=None)
  239. If None, `P` is selected depending on the properties of the
  240. graph. Otherwise is one of 'random', 'lazy', or 'pagerank'
  241. alpha : real
  242. (1 - alpha) is the teleportation probability used with pagerank
  243. Returns
  244. -------
  245. L : NumPy matrix
  246. Combinatorial Laplacian of G.
  247. Notes
  248. -----
  249. Only implemented for DiGraphs
  250. See Also
  251. --------
  252. laplacian_matrix
  253. References
  254. ----------
  255. .. [1] Fan Chung (2005).
  256. Laplacians and the Cheeger inequality for directed graphs.
  257. Annals of Combinatorics, 9(1), 2005
  258. """
  259. import scipy as sp
  260. import scipy.sparse # call as sp.sparse
  261. import scipy.sparse.linalg # call as sp.sparse.linalg
  262. P = _transition_matrix(
  263. G, nodelist=nodelist, weight=weight, walk_type=walk_type, alpha=alpha
  264. )
  265. n, m = P.shape
  266. evals, evecs = sp.sparse.linalg.eigs(P.T, k=1)
  267. v = evecs.flatten().real
  268. p = v / v.sum()
  269. # NOTE: could be improved by not densifying
  270. # TODO: Rm csr_array wrapper when spdiags array creation becomes available
  271. Phi = sp.sparse.csr_array(sp.sparse.spdiags(p, 0, n, n)).toarray()
  272. return Phi - (Phi @ P + P.T @ Phi) / 2.0
  273. def _transition_matrix(G, nodelist=None, weight="weight", walk_type=None, alpha=0.95):
  274. """Returns the transition matrix of G.
  275. This is a row stochastic giving the transition probabilities while
  276. performing a random walk on the graph. Depending on the value of walk_type,
  277. P can be the transition matrix induced by a random walk, a lazy random walk,
  278. or a random walk with teleportation (PageRank).
  279. Parameters
  280. ----------
  281. G : DiGraph
  282. A NetworkX graph
  283. nodelist : list, optional
  284. The rows and columns are ordered according to the nodes in nodelist.
  285. If nodelist is None, then the ordering is produced by G.nodes().
  286. weight : string or None, optional (default='weight')
  287. The edge data key used to compute each value in the matrix.
  288. If None, then each edge has weight 1.
  289. walk_type : string or None, optional (default=None)
  290. If None, `P` is selected depending on the properties of the
  291. graph. Otherwise is one of 'random', 'lazy', or 'pagerank'
  292. alpha : real
  293. (1 - alpha) is the teleportation probability used with pagerank
  294. Returns
  295. -------
  296. P : numpy.ndarray
  297. transition matrix of G.
  298. Raises
  299. ------
  300. NetworkXError
  301. If walk_type not specified or alpha not in valid range
  302. """
  303. import numpy as np
  304. import scipy as sp
  305. import scipy.sparse # call as sp.sparse
  306. if walk_type is None:
  307. if nx.is_strongly_connected(G):
  308. if nx.is_aperiodic(G):
  309. walk_type = "random"
  310. else:
  311. walk_type = "lazy"
  312. else:
  313. walk_type = "pagerank"
  314. A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, dtype=float)
  315. n, m = A.shape
  316. if walk_type in ["random", "lazy"]:
  317. # TODO: Rm csr_array wrapper when spdiags array creation becomes available
  318. DI = sp.sparse.csr_array(sp.sparse.spdiags(1.0 / A.sum(axis=1), 0, n, n))
  319. if walk_type == "random":
  320. P = DI @ A
  321. else:
  322. # TODO: Rm csr_array wrapper when identity array creation becomes available
  323. I = sp.sparse.csr_array(sp.sparse.identity(n))
  324. P = (I + DI @ A) / 2.0
  325. elif walk_type == "pagerank":
  326. if not (0 < alpha < 1):
  327. raise nx.NetworkXError("alpha must be between 0 and 1")
  328. # this is using a dense representation. NOTE: This should be sparsified!
  329. A = A.toarray()
  330. # add constant to dangling nodes' row
  331. A[A.sum(axis=1) == 0, :] = 1 / n
  332. # normalize
  333. A = A / A.sum(axis=1)[np.newaxis, :].T
  334. P = alpha * A + (1 - alpha) / n
  335. else:
  336. raise nx.NetworkXError("walk_type must be random, lazy, or pagerank")
  337. return P