123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- """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",
- ]
- @not_implemented_for("directed")
- 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
- --------
- to_numpy_array
- normalized_laplacian_matrix
- 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
- import scipy.sparse # call as sp.sparse
- 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
- @not_implemented_for("directed")
- 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
- import scipy.sparse # call as sp.sparse
- 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 sp.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
- @not_implemented_for("undirected")
- @not_implemented_for("multigraph")
- 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
- import scipy.sparse # call as sp.sparse
- import scipy.sparse.linalg # call as sp.sparse.linalg
- # 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()
- sqrtp = np.sqrt(p)
- Q = (
- # TODO: rm csr_array wrapper when spdiags creates arrays
- sp.sparse.csr_array(sp.sparse.spdiags(sqrtp, 0, n, n))
- @ P
- # TODO: rm csr_array wrapper when spdiags creates arrays
- @ sp.sparse.csr_array(sp.sparse.spdiags(1.0 / sqrtp, 0, n, n))
- )
- # NOTE: This could be sparsified for the non-pagerank cases
- I = np.identity(len(G))
- return I - (Q + Q.T) / 2.0
- @not_implemented_for("undirected")
- @not_implemented_for("multigraph")
- 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
- import scipy.sparse # call as sp.sparse
- import scipy.sparse.linalg # call as sp.sparse.linalg
- 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
- import scipy.sparse # call as sp.sparse
- 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
|