blob: ffb256c5d536d94e1fa6771e82b56c0dfe0f3d41 [file] [log] [blame]
"""
Laplacian matrix of graphs.
"""
# Copyright (C) 2004-2013 by
# Aric Hagberg <hagberg@lanl.gov>
# Dan Schult <dschult@colgate.edu>
# Pieter Swart <swart@lanl.gov>
# All rights reserved.
# BSD license.
import networkx as nx
from networkx.utils import require, not_implemented_for
__author__ = "\n".join(['Aric Hagberg <aric.hagberg@gmail.com>',
'Pieter Swart (swart@lanl.gov)',
'Dan Schult (dschult@colgate.edu)',
'Alejandro Weinstein <alejandro.weinstein@gmail.com>'])
__all__ = ['laplacian_matrix',
'normalized_laplacian_matrix',
'directed_laplacian_matrix']
@require('numpy')
@not_implemented_for('directed')
def laplacian_matrix(G, nodelist=None, weight='weight'):
"""Return 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 : NumPy matrix
The Laplacian matrix of G.
Notes
-----
For MultiGraph/MultiDiGraph, the edges weights are summed.
See to_numpy_matrix for other options.
See Also
--------
to_numpy_matrix
normalized_laplacian_matrix
"""
import numpy as np
if nodelist is None:
nodelist = G.nodes()
if G.is_multigraph():
# this isn't the fastest way to do this...
A = np.asarray(nx.to_numpy_matrix(G,nodelist=nodelist,weight=weight))
I = np.identity(A.shape[0])
D = I*np.sum(A,axis=1)
L = D - A
else:
# Graph or DiGraph, this is faster than above
n = len(nodelist)
index = dict( (n,i) for i,n in enumerate(nodelist) )
L = np.zeros((n,n))
for ui,u in enumerate(nodelist):
totalwt = 0.0
for v,d in G[u].items():
try:
vi = index[v]
except KeyError:
continue
wt = d.get(weight,1)
L[ui,vi] = -wt
totalwt += wt
L[ui,ui] = totalwt
return np.asmatrix(L)
@require('numpy')
@not_implemented_for('directed')
def normalized_laplacian_matrix(G, nodelist=None, weight='weight'):
r"""Return the normalized Laplacian matrix of G.
The normalized graph Laplacian is the matrix
.. math::
NL = D^{-1/2} L D^{-1/2}
where `L` is the graph Laplacian 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 : NumPy matrix
The normalized Laplacian matrix of G.
Notes
-----
For MultiGraph/MultiDiGraph, the edges weights are summed.
See to_numpy_matrix for other options.
If the Graph contains selfloops, D is defined as diag(sum(A,1)), where A is
the adjencency matrix [2]_.
See Also
--------
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.
"""
import numpy as np
if G.is_multigraph():
L = laplacian_matrix(G, nodelist=nodelist, weight=weight)
D = np.diag(L)
elif G.number_of_selfloops() == 0:
L = laplacian_matrix(G, nodelist=nodelist, weight=weight)
D = np.diag(L)
else:
A = np.array(nx.adj_matrix(G))
D = np.sum(A, 1)
L = np.diag(D) - A
# Handle div by 0. It happens if there are unconnected nodes
with np.errstate(divide='ignore'):
Disqrt = np.diag(1 / np.sqrt(D))
Disqrt[np.isinf(Disqrt)] = 0
Ln = np.dot(Disqrt, np.dot(L,Disqrt))
return Ln
###############################################################################
# Code based on
# https://bitbucket.org/bedwards/networkx-community/src/370bd69fc02f/networkx/algorithms/community/
@require('numpy')
@not_implemented_for('undirected')
@not_implemented_for('multigraph')
def directed_laplacian_matrix(G, nodelist=None, weight='weight',
walk_type=None, alpha=0.95):
r"""Return 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.
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 array
Normalized Laplacian of G.
Raises
------
NetworkXError
If NumPy cannot be imported
NetworkXNotImplemnted
If G is not a DiGraph
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
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"
M = nx.to_numpy_matrix(G, nodelist=nodelist, weight=weight)
n, m = M.shape
if walk_type in ["random", "lazy"]:
DI = np.diagflat(1.0 / np.sum(M, axis=1))
if walk_type == "random":
P = DI * M
else:
I = np.identity(n)
P = (I + DI * M) / 2.0
elif walk_type == "pagerank":
if not (0 < alpha < 1):
raise nx.NetworkXError('alpha must be between 0 and 1')
# add constant to dangling nodes' row
dangling = np.where(M.sum(axis=1) == 0)
for d in dangling[0]:
M[d] = 1.0 / n
# normalize
M = M / M.sum(axis=1)
P = alpha * M + (1 - alpha) / n
else:
raise nx.NetworkXError("walk_type must be random, lazy, or pagerank")
evals, evecs = np.linalg.eig(P.T)
index = evals.argsort()[-1] # index of largest eval,evec
# eigenvector of largest eigenvalue at ind[-1]
v = np.array(evecs[:,index]).flatten().real
p = v / v.sum()
sp = np.sqrt(p)
Q = np.diag(sp) * P * np.diag(1.0/sp)
I = np.identity(len(G))
return I - (Q + Q.T) /2.0
# fixture for nose tests
def setup_module(module):
from nose import SkipTest
try:
import numpy
except:
raise SkipTest("NumPy not available")