Source code for networkx.linalg.laplacianmatrix

"""Laplacian matrix of graphs.

All calculations here are done using the out-degree. For Laplacians using
in-degree, use `G.reverse(copy=False)` instead of `G` and take the transpose.

The `laplacian_matrix` function provides an unnormalized matrix,
while `normalized_laplacian_matrix`, `directed_laplacian_matrix`,
and `directed_combinatorial_laplacian_matrix` are all normalized.
"""

import networkx as nx
from networkx.utils import not_implemented_for

__all__ = [
    "laplacian_matrix",
    "normalized_laplacian_matrix",
    "directed_laplacian_matrix",
    "directed_combinatorial_laplacian_matrix",
    "magnetic_laplacian_matrix",
]


[docs] @nx._dispatchable(edge_attrs="weight") 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. This returns an unnormalized matrix. For a normalized output, use `normalized_laplacian_matrix`, `directed_laplacian_matrix`, or `directed_combinatorial_laplacian_matrix`. This calculation uses the out-degree of the graph `G`. To use the in-degree for calculations instead, use `G.reverse(copy=False)` and take the transpose. See Also -------- :func:`~networkx.convert_matrix.to_numpy_array` normalized_laplacian_matrix directed_laplacian_matrix directed_combinatorial_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]] >>> edges = [ ... (1, 2), ... (2, 1), ... (2, 4), ... (4, 3), ... (3, 4), ... ] >>> DiG = nx.DiGraph(edges) >>> print(nx.laplacian_matrix(DiG).toarray()) [[ 1 -1 0 0] [-1 2 -1 0] [ 0 0 1 -1] [ 0 0 -1 1]] Notice that node 4 is represented by the third column and row. This is because by default the row/column order is the order of `G.nodes` (i.e. the node added order -- in the edgelist, 4 first appears in (2, 4), before node 3 in edge (4, 3).) To control the node order of the matrix, use the `nodelist` argument. >>> print(nx.laplacian_matrix(DiG, nodelist=[1, 2, 3, 4]).toarray()) [[ 1 -1 0 0] [-1 2 0 -1] [ 0 0 1 -1] [ 0 0 -1 1]] This calculation uses the out-degree of the graph `G`. To use the in-degree for calculations instead, use `G.reverse(copy=False)` and take the transpose. >>> print(nx.laplacian_matrix(DiG.reverse(copy=False)).toarray().T) [[ 1 -1 0 0] [-1 1 -1 0] [ 0 0 2 -1] [ 0 0 -1 1]] References ---------- .. [1] Langville, Amy N., and Carl D. Meyer. Google’s PageRank and Beyond: The Science of Search Engine Rankings. Princeton University Press, 2006. """ 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 D = sp.sparse.dia_array((A.sum(axis=1), 0), shape=(m, n)).tocsr() return D - A
[docs] @nx._dispatchable(edge_attrs="weight") 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]_. This calculation uses the out-degree of the graph `G`. To use the in-degree for calculations instead, use `G.reverse(copy=False)` and take the transpose. For an unnormalized output, use `laplacian_matrix`. Examples -------- >>> import numpy as np >>> edges = [ ... (1, 2), ... (2, 1), ... (2, 4), ... (4, 3), ... (3, 4), ... ] >>> DiG = nx.DiGraph(edges) >>> print(nx.normalized_laplacian_matrix(DiG).toarray()) [[ 1. -0.70710678 0. 0. ] [-0.70710678 1. -0.70710678 0. ] [ 0. 0. 1. -1. ] [ 0. 0. -1. 1. ]] Notice that node 4 is represented by the third column and row. This is because by default the row/column order is the order of `G.nodes` (i.e. the node added order -- in the edgelist, 4 first appears in (2, 4), before node 3 in edge (4, 3).) To control the node order of the matrix, use the `nodelist` argument. >>> print(nx.normalized_laplacian_matrix(DiG, nodelist=[1, 2, 3, 4]).toarray()) [[ 1. -0.70710678 0. 0. ] [-0.70710678 1. 0. -0.70710678] [ 0. 0. 1. -1. ] [ 0. 0. -1. 1. ]] >>> G = nx.Graph(edges) >>> print(nx.normalized_laplacian_matrix(G).toarray()) [[ 1. -0.70710678 0. 0. ] [-0.70710678 1. -0.5 0. ] [ 0. -0.5 1. -0.70710678] [ 0. 0. -0.70710678 1. ]] See Also -------- laplacian_matrix normalized_laplacian_spectrum directed_laplacian_matrix directed_combinatorial_laplacian_matrix 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. .. [3] Langville, Amy N., and Carl D. Meyer. Google’s PageRank and Beyond: The Science of Search Engine Rankings. Princeton University Press, 2006. """ 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, _ = A.shape diags = A.sum(axis=1) D = sp.sparse.dia_array((diags, 0), shape=(n, n)).tocsr() L = D - A with np.errstate(divide="ignore"): diags_sqrt = 1.0 / np.sqrt(diags) diags_sqrt[np.isinf(diags_sqrt)] = 0 DH = sp.sparse.dia_array((diags_sqrt, 0), shape=(n, n)).tocsr() return DH @ (L @ DH)
[docs] @not_implemented_for("multigraph") @not_implemented_for("undirected") @nx._dispatchable(edge_attrs="weight") def magnetic_laplacian_matrix( G, *, nodelist=None, normalized=False, q=0.25, weight="weight" ): r"""Returns the magnetic Laplacian matrix of DiGraph G. The magnetic Laplacian matrix (also called the q-magnetic Laplacian) is a Hermitian matrix for directed graphs that encodes edge directionality via complex phases [1]_. Given a weighted directed graph, :math:`G = (V, E, W)`, with :math:`W` the weighted adjacency matrix, the symmetrized weighted adjacency matrix is defined as :math:`W' = 0.5 (W + W^{T})`. A skew-symmetric term :math:`\delta` is introduced to encode directionality, where ..math:: \delta_{jk} = \begin{cases} +1 & \text{if} j \to k \text{ is an edge and } k \to j \text{ is not}, \\ -1 & \text{if} k \to j \text{ is an edge and } j \to k \text{ is not}, \\ 0 & \text{if both or neither edge is present.} \end{cases} Then, the magnetic Laplacian matrix is defined as: .. math:: L^{(q)} := D - H^{(q)} where :math:`H^{(q)}` is the Hermitian adjacency matrix with entries :math:`H^{(q)}_{jk} = W'_{jk} e^{2\pi i q \delta_{jk}}`, and :math:`D` is the degree matrix associated with the symmetrized weight adjacency matrix :math:`W'`. If `normalized` is True, compute the normalized version using the Moore-Penrose inverse :math:`D^{+}}` of the degree matrix :math:`D`. The normalized formula is then: .. math:: L^{(q)}_{norm} = (D^{+})^{0.5} L^{(q)} (D^{+})^{0.5} Parameters ---------- G : DiGraph A directed graph nodelist : list, optional (default=list(G)) Node ordering for row/columns. normalized : bool, optional (default=False) Bool that encodes if return the magnetic Laplacian or the normalized magnetic Laplacian. If True returns the normalized version. q : float, optional (default=0.25) The phase of the magnetic potential is the charge parameter 0 <= q <= 0.5. At q=0 returns the standard Laplacian. weight : string or None, optional (default='weight') Edge attribute key for weights. If None, all edges have weight 1. Returns ------- L : SciPy sparse array (complex dtype) The magnetic Laplacian matrix of `G` if not `normalized` and the normalized version if `normalized` Raises ------ ValueError If q is not between 0 and 0.5 NetworkXNotImplemented If `G` is undirected or a multigraph References ---------- .. [1] Fanuel, M., Alaíz, C. M., Fernández, Á., & Suykens, J. A. (2018). Magnetic eigenmaps for the visualization of directed graphs. Applied and Computational Harmonic Analysis, 44(1), 189–199. <https://doi.org/10.1016/j.acha.2017.01.004> """ from collections import defaultdict import numpy as np import scipy as sp if nodelist is None: nodelist = list(G) # Build Hermitian adjacency H n = len(nodelist) node_index = {v: i for i, v in enumerate(nodelist)} if not (0 <= q <= 0.5): raise ValueError("Parameter q must be a value between 0 and 0.5") phase = 2 * np.pi * q # Find "phase" matrix # Dict to encode where have been added symetries delta_phase_edge = defaultdict(int) matrix_weights = defaultdict(float) phases = {1: np.exp(1j * phase), -1: np.exp(-1j * phase), 0: 1} for u, v, wt in G.edges(data=weight, default=1): if u not in node_index or v not in node_index: continue ui, vi = node_index[u], node_index[v] delta_phase_edge[(ui, vi)] += 1 delta_phase_edge[(vi, ui)] -= 1 matrix_weights[(ui, vi)] += 0.5 * wt matrix_weights[(vi, ui)] += 0.5 * wt rows, cols, data = [], [], [] for u, v in G.edges(): if u not in node_index or v not in node_index: continue ui, vi = node_index[u], node_index[v] if ui != vi: if delta_phase_edge[(ui, vi)] == 0: rows.append(ui) cols.append(vi) data.append(matrix_weights[(ui, vi)]) else: rows.append(ui) cols.append(vi) data.append( matrix_weights[(ui, vi)] * phases[delta_phase_edge[(ui, vi)]] ) rows.append(vi) cols.append(ui) data.append( matrix_weights[(vi, ui)] * phases[delta_phase_edge[(vi, ui)]] ) H = sp.sparse.csr_array((data, (rows, cols)), shape=(n, n), dtype=complex) # Build degree matrix D diags = np.abs(H).sum(axis=1).ravel() if normalized: with np.errstate(divide="ignore"): diags_sqrt = 1.0 / np.sqrt(diags) diags_sqrt[np.isinf(diags_sqrt)] = 0 DH = sp.sparse.dia_array((diags_sqrt, 0), shape=(n, n)).tocsr() H = DH @ (H @ DH) diags = np.ones( n, ) D = sp.sparse.dia_array((diags, 0), shape=(n, n), dtype=complex).tocsr() return D - H
############################################################################### # Code based on work from https://github.com/bjedwards
[docs] @not_implemented_for("undirected") @not_implemented_for("multigraph") @nx._dispatchable(edge_attrs="weight") 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 - \frac{1}{2} \left (\Phi^{1/2} P \Phi^{-1/2} + \Phi^{-1/2} P^T \Phi^{1/2} \right ) 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) One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None`` (the default), then a value is selected according to the properties of `G`: - ``walk_type="random"`` if `G` is strongly connected and aperiodic - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic - ``walk_type="pagerank"`` for all other cases. alpha : real (1 - alpha) is the teleportation probability used with pagerank Returns ------- L : NumPy matrix Normalized Laplacian of G. Notes ----- Only implemented for DiGraphs The result is always a symmetric matrix. This calculation uses the out-degree of the graph `G`. To use the in-degree for calculations instead, use `G.reverse(copy=False)` and take the transpose. See Also -------- laplacian_matrix normalized_laplacian_matrix directed_combinatorial_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 = ( sp.sparse.dia_array((sqrtp, 0), shape=(n, n)).tocsr()
[docs] @ P @ sp.sparse.dia_array((1.0 / sqrtp, 0), shape=(n, n)).tocsr() ) # 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") @nx._dispatchable(edge_attrs="weight") 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 - \frac{1}{2} \left (\Phi P + P^T \Phi \right) 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) One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None`` (the default), then a value is selected according to the properties of `G`: - ``walk_type="random"`` if `G` is strongly connected and aperiodic - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic - ``walk_type="pagerank"`` for all other cases. alpha : real (1 - alpha) is the teleportation probability used with pagerank Returns ------- L : NumPy matrix Combinatorial Laplacian of G. Notes ----- Only implemented for DiGraphs The result is always a symmetric matrix. This calculation uses the out-degree of the graph `G`. To use the in-degree for calculations instead, use `G.reverse(copy=False)` and take the transpose. See Also -------- laplacian_matrix normalized_laplacian_matrix directed_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 Phi = sp.sparse.dia_array((p, 0), shape=(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) One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None`` (the default), then a value is selected according to the properties of `G`: - ``walk_type="random"`` if `G` is strongly connected and aperiodic - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic - ``walk_type="pagerank"`` for all other cases. 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"]: DI = sp.sparse.dia_array((1.0 / A.sum(axis=1), 0), shape=(n, n)).tocsr() if walk_type == "random": P = DI @ A else: I = sp.sparse.eye_array(n, format="csr") 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