Spaces:
Paused
Paused
| """Laplacian matrix of graphs. | |
| """ | |
| import networkx as nx | |
| from networkx.utils import not_implemented_for | |
| __all__ = [ | |
| "laplacian_matrix", | |
| "normalized_laplacian_matrix", | |
| "total_spanning_tree_weight", | |
| "directed_laplacian_matrix", | |
| "directed_combinatorial_laplacian_matrix", | |
| ] | |
| def laplacian_matrix(G, nodelist=None, weight="weight"): | |
| """Returns the Laplacian matrix of G. | |
| The graph Laplacian is the matrix L = D - A, where | |
| A is the adjacency matrix and D is the diagonal matrix of node degrees. | |
| Parameters | |
| ---------- | |
| G : graph | |
| A NetworkX graph | |
| nodelist : list, optional | |
| The rows and columns are ordered according to the nodes in nodelist. | |
| If nodelist is None, then the ordering is produced by G.nodes(). | |
| weight : string or None, optional (default='weight') | |
| The edge data key used to compute each value in the matrix. | |
| If None, then each edge has weight 1. | |
| Returns | |
| ------- | |
| L : SciPy sparse array | |
| The Laplacian matrix of G. | |
| Notes | |
| ----- | |
| For MultiGraph, the edges weights are summed. | |
| See Also | |
| -------- | |
| :func:`~networkx.convert_matrix.to_numpy_array` | |
| normalized_laplacian_matrix | |
| :func:`~networkx.linalg.spectrum.laplacian_spectrum` | |
| Examples | |
| -------- | |
| For graphs with multiple connected components, L is permutation-similar | |
| to a block diagonal matrix where each block is the respective Laplacian | |
| matrix for each component. | |
| >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)]) | |
| >>> print(nx.laplacian_matrix(G).toarray()) | |
| [[ 1 -1 0 0 0] | |
| [-1 2 -1 0 0] | |
| [ 0 -1 1 0 0] | |
| [ 0 0 0 1 -1] | |
| [ 0 0 0 -1 1]] | |
| """ | |
| import scipy as sp | |
| if nodelist is None: | |
| nodelist = list(G) | |
| A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr") | |
| n, m = A.shape | |
| # TODO: rm csr_array wrapper when spdiags can produce arrays | |
| D = sp.sparse.csr_array(sp.sparse.spdiags(A.sum(axis=1), 0, m, n, format="csr")) | |
| return D - A | |
| def normalized_laplacian_matrix(G, nodelist=None, weight="weight"): | |
| r"""Returns the normalized Laplacian matrix of G. | |
| The normalized graph Laplacian is the matrix | |
| .. math:: | |
| N = D^{-1/2} L D^{-1/2} | |
| where `L` is the graph Laplacian and `D` is the diagonal matrix of | |
| node degrees [1]_. | |
| Parameters | |
| ---------- | |
| G : graph | |
| A NetworkX graph | |
| nodelist : list, optional | |
| The rows and columns are ordered according to the nodes in nodelist. | |
| If nodelist is None, then the ordering is produced by G.nodes(). | |
| weight : string or None, optional (default='weight') | |
| The edge data key used to compute each value in the matrix. | |
| If None, then each edge has weight 1. | |
| Returns | |
| ------- | |
| N : SciPy sparse array | |
| The normalized Laplacian matrix of G. | |
| Notes | |
| ----- | |
| For MultiGraph, the edges weights are summed. | |
| See :func:`to_numpy_array` for other options. | |
| If the Graph contains selfloops, D is defined as ``diag(sum(A, 1))``, where A is | |
| the adjacency matrix [2]_. | |
| See Also | |
| -------- | |
| laplacian_matrix | |
| normalized_laplacian_spectrum | |
| References | |
| ---------- | |
| .. [1] Fan Chung-Graham, Spectral Graph Theory, | |
| CBMS Regional Conference Series in Mathematics, Number 92, 1997. | |
| .. [2] Steve Butler, Interlacing For Weighted Graphs Using The Normalized | |
| Laplacian, Electronic Journal of Linear Algebra, Volume 16, pp. 90-98, | |
| March 2007. | |
| """ | |
| import numpy as np | |
| import scipy as sp | |
| if nodelist is None: | |
| nodelist = list(G) | |
| A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr") | |
| n, m = A.shape | |
| diags = A.sum(axis=1) | |
| # TODO: rm csr_array wrapper when spdiags can produce arrays | |
| D = sp.sparse.csr_array(sp.sparse.spdiags(diags, 0, m, n, format="csr")) | |
| L = D - A | |
| with np.errstate(divide="ignore"): | |
| diags_sqrt = 1.0 / np.sqrt(diags) | |
| diags_sqrt[np.isinf(diags_sqrt)] = 0 | |
| # TODO: rm csr_array wrapper when spdiags can produce arrays | |
| DH = sp.sparse.csr_array(sp.sparse.spdiags(diags_sqrt, 0, m, n, format="csr")) | |
| return DH @ (L @ DH) | |
| def total_spanning_tree_weight(G, weight=None): | |
| """ | |
| Returns the total weight of all spanning trees of `G`. | |
| Kirchoff's Tree Matrix Theorem states that the determinant of any cofactor of the | |
| Laplacian matrix of a graph is the number of spanning trees in the graph. For a | |
| weighted Laplacian matrix, it is the sum across all spanning trees of the | |
| multiplicative weight of each tree. That is, the weight of each tree is the | |
| product of its edge weights. | |
| Parameters | |
| ---------- | |
| G : NetworkX Graph | |
| The graph to use Kirchhoff's theorem on. | |
| weight : string or None | |
| The key for the edge attribute holding the edge weight. If `None`, then | |
| each edge is assumed to have a weight of 1 and this function returns the | |
| total number of spanning trees in `G`. | |
| Returns | |
| ------- | |
| float | |
| The sum of the total multiplicative weights for all spanning trees in `G` | |
| """ | |
| import numpy as np | |
| G_laplacian = nx.laplacian_matrix(G, weight=weight).toarray() | |
| # Determinant ignoring first row and column | |
| return abs(np.linalg.det(G_laplacian[1:, 1:])) | |
| ############################################################################### | |
| # Code based on work from https://github.com/bjedwards | |
| def directed_laplacian_matrix( | |
| G, nodelist=None, weight="weight", walk_type=None, alpha=0.95 | |
| ): | |
| r"""Returns the directed Laplacian matrix of G. | |
| The graph directed Laplacian is the matrix | |
| .. math:: | |
| L = I - (\Phi^{1/2} P \Phi^{-1/2} + \Phi^{-1/2} P^T \Phi^{1/2} ) / 2 | |
| where `I` is the identity matrix, `P` is the transition matrix of the | |
| graph, and `\Phi` a matrix with the Perron vector of `P` in the diagonal and | |
| zeros elsewhere [1]_. | |
| Depending on the value of walk_type, `P` can be the transition matrix | |
| induced by a random walk, a lazy random walk, or a random walk with | |
| teleportation (PageRank). | |
| Parameters | |
| ---------- | |
| G : DiGraph | |
| A NetworkX graph | |
| nodelist : list, optional | |
| The rows and columns are ordered according to the nodes in nodelist. | |
| If nodelist is None, then the ordering is produced by G.nodes(). | |
| weight : string or None, optional (default='weight') | |
| The edge data key used to compute each value in the matrix. | |
| If None, then each edge has weight 1. | |
| walk_type : string or None, optional (default=None) | |
| If None, `P` is selected depending on the properties of the | |
| graph. Otherwise is one of 'random', 'lazy', or 'pagerank' | |
| alpha : real | |
| (1 - alpha) is the teleportation probability used with pagerank | |
| Returns | |
| ------- | |
| L : NumPy matrix | |
| Normalized Laplacian of G. | |
| Notes | |
| ----- | |
| Only implemented for DiGraphs | |
| See Also | |
| -------- | |
| laplacian_matrix | |
| References | |
| ---------- | |
| .. [1] Fan Chung (2005). | |
| Laplacians and the Cheeger inequality for directed graphs. | |
| Annals of Combinatorics, 9(1), 2005 | |
| """ | |
| import numpy as np | |
| import scipy as sp | |
| # NOTE: P has type ndarray if walk_type=="pagerank", else csr_array | |
| P = _transition_matrix( | |
| G, nodelist=nodelist, weight=weight, walk_type=walk_type, alpha=alpha | |
| ) | |
| n, m = P.shape | |
| evals, evecs = sp.sparse.linalg.eigs(P.T, k=1) | |
| v = evecs.flatten().real | |
| p = v / v.sum() | |
| # p>=0 by Perron-Frobenius Thm. Use abs() to fix roundoff across zero gh-6865 | |
| sqrtp = np.sqrt(np.abs(p)) | |
| Q = ( | |
| # TODO: rm csr_array wrapper when spdiags creates arrays | |
| sp.sparse.csr_array(sp.sparse.spdiags(sqrtp, 0, n, n)) | |
| # TODO: rm csr_array wrapper when spdiags creates arrays | |
| ) | |
| # NOTE: This could be sparsified for the non-pagerank cases | |
| I = np.identity(len(G)) | |
| return I - (Q + Q.T) / 2.0 | |
| def directed_combinatorial_laplacian_matrix( | |
| G, nodelist=None, weight="weight", walk_type=None, alpha=0.95 | |
| ): | |
| r"""Return the directed combinatorial Laplacian matrix of G. | |
| The graph directed combinatorial Laplacian is the matrix | |
| .. math:: | |
| L = \Phi - (\Phi P + P^T \Phi) / 2 | |
| where `P` is the transition matrix of the graph and `\Phi` a matrix | |
| with the Perron vector of `P` in the diagonal and zeros elsewhere [1]_. | |
| Depending on the value of walk_type, `P` can be the transition matrix | |
| induced by a random walk, a lazy random walk, or a random walk with | |
| teleportation (PageRank). | |
| Parameters | |
| ---------- | |
| G : DiGraph | |
| A NetworkX graph | |
| nodelist : list, optional | |
| The rows and columns are ordered according to the nodes in nodelist. | |
| If nodelist is None, then the ordering is produced by G.nodes(). | |
| weight : string or None, optional (default='weight') | |
| The edge data key used to compute each value in the matrix. | |
| If None, then each edge has weight 1. | |
| walk_type : string or None, optional (default=None) | |
| If None, `P` is selected depending on the properties of the | |
| graph. Otherwise is one of 'random', 'lazy', or 'pagerank' | |
| alpha : real | |
| (1 - alpha) is the teleportation probability used with pagerank | |
| Returns | |
| ------- | |
| L : NumPy matrix | |
| Combinatorial Laplacian of G. | |
| Notes | |
| ----- | |
| Only implemented for DiGraphs | |
| See Also | |
| -------- | |
| laplacian_matrix | |
| References | |
| ---------- | |
| .. [1] Fan Chung (2005). | |
| Laplacians and the Cheeger inequality for directed graphs. | |
| Annals of Combinatorics, 9(1), 2005 | |
| """ | |
| import scipy as sp | |
| P = _transition_matrix( | |
| G, nodelist=nodelist, weight=weight, walk_type=walk_type, alpha=alpha | |
| ) | |
| n, m = P.shape | |
| evals, evecs = sp.sparse.linalg.eigs(P.T, k=1) | |
| v = evecs.flatten().real | |
| p = v / v.sum() | |
| # NOTE: could be improved by not densifying | |
| # TODO: Rm csr_array wrapper when spdiags array creation becomes available | |
| Phi = sp.sparse.csr_array(sp.sparse.spdiags(p, 0, n, n)).toarray() | |
| return Phi - (Phi @ P + P.T @ Phi) / 2.0 | |
| def _transition_matrix(G, nodelist=None, weight="weight", walk_type=None, alpha=0.95): | |
| """Returns the transition matrix of G. | |
| This is a row stochastic giving the transition probabilities while | |
| performing a random walk on the graph. Depending on the value of walk_type, | |
| P can be the transition matrix induced by a random walk, a lazy random walk, | |
| or a random walk with teleportation (PageRank). | |
| Parameters | |
| ---------- | |
| G : DiGraph | |
| A NetworkX graph | |
| nodelist : list, optional | |
| The rows and columns are ordered according to the nodes in nodelist. | |
| If nodelist is None, then the ordering is produced by G.nodes(). | |
| weight : string or None, optional (default='weight') | |
| The edge data key used to compute each value in the matrix. | |
| If None, then each edge has weight 1. | |
| walk_type : string or None, optional (default=None) | |
| If None, `P` is selected depending on the properties of the | |
| graph. Otherwise is one of 'random', 'lazy', or 'pagerank' | |
| alpha : real | |
| (1 - alpha) is the teleportation probability used with pagerank | |
| Returns | |
| ------- | |
| P : numpy.ndarray | |
| transition matrix of G. | |
| Raises | |
| ------ | |
| NetworkXError | |
| If walk_type not specified or alpha not in valid range | |
| """ | |
| import numpy as np | |
| import scipy as sp | |
| if walk_type is None: | |
| if nx.is_strongly_connected(G): | |
| if nx.is_aperiodic(G): | |
| walk_type = "random" | |
| else: | |
| walk_type = "lazy" | |
| else: | |
| walk_type = "pagerank" | |
| A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, dtype=float) | |
| n, m = A.shape | |
| if walk_type in ["random", "lazy"]: | |
| # TODO: Rm csr_array wrapper when spdiags array creation becomes available | |
| DI = sp.sparse.csr_array(sp.sparse.spdiags(1.0 / A.sum(axis=1), 0, n, n)) | |
| if walk_type == "random": | |
| P = DI @ A | |
| else: | |
| # TODO: Rm csr_array wrapper when identity array creation becomes available | |
| I = sp.sparse.csr_array(sp.sparse.identity(n)) | |
| P = (I + DI @ A) / 2.0 | |
| elif walk_type == "pagerank": | |
| if not (0 < alpha < 1): | |
| raise nx.NetworkXError("alpha must be between 0 and 1") | |
| # this is using a dense representation. NOTE: This should be sparsified! | |
| A = A.toarray() | |
| # add constant to dangling nodes' row | |
| A[A.sum(axis=1) == 0, :] = 1 / n | |
| # normalize | |
| A = A / A.sum(axis=1)[np.newaxis, :].T | |
| P = alpha * A + (1 - alpha) / n | |
| else: | |
| raise nx.NetworkXError("walk_type must be random, lazy, or pagerank") | |
| return P | |