Skip to content

Commit

Permalink
ENH : Provide non-normalized and normalized directed laplacian matrix…
Browse files Browse the repository at this point in the history
… calculation (networkx#7199)

Adds functionality for directed laplacian matrices

* Remove the `not_implemented_for("directed")` decorator on `laplacian_matrix`

* Add tests for 'laplacian_matrix` as applied to directed graphs

* Add pointers to `directed_laplacian_matrix` and `directed_combinatorial_laplacian_matrix` in docstring of `laplacian_matrix`

* Allow DiGraphs in `normalized_laplacian_matrix`
  • Loading branch information
smokestacklightnin authored and cvanelteren committed Apr 22, 2024
1 parent ad34422 commit d967c5f
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 4 deletions.
83 changes: 79 additions & 4 deletions networkx/linalg/laplacianmatrix.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
"""Laplacian matrix of graphs.
All calculations here are done using the out-degree. For Laplacians
using in-degree, us `G.reverse(copy=False)` instead of `G`.
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
Expand All @@ -12,7 +19,6 @@
]


@not_implemented_for("directed")
@nx._dispatch(edge_attrs="weight")
def laplacian_matrix(G, nodelist=None, weight="weight"):
"""Returns the Laplacian matrix of G.
Expand Down Expand Up @@ -42,10 +48,19 @@ def laplacian_matrix(G, nodelist=None, weight="weight"):
-----
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)` instead.
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
Expand All @@ -62,6 +77,25 @@ def laplacian_matrix(G, nodelist=None, weight="weight"):
[ 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]]
>>> G = nx.Graph(DiG)
>>> print(nx.laplacian_matrix(G).toarray())
[[ 1 -1 0 0]
[-1 2 -1 0]
[ 0 -1 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

Expand All @@ -74,7 +108,6 @@ def laplacian_matrix(G, nodelist=None, weight="weight"):
return D - A


@not_implemented_for("directed")
@nx._dispatch(edge_attrs="weight")
def normalized_laplacian_matrix(G, nodelist=None, weight="weight"):
r"""Returns the normalized Laplacian matrix of G.
Expand Down Expand Up @@ -114,10 +147,36 @@ def normalized_laplacian_matrix(G, nodelist=None, weight="weight"):
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)` instead.
For an unnormalized output, use `laplacian_matrix`.
Examples
--------
>>> import numpy as np
>>> np.set_printoptions(precision=4) # To print with lower precision
>>> edges = [(1, 2), (2, 1), (2, 4), (4, 3), (3, 4),]
>>> DiG = nx.DiGraph(edges)
>>> print(nx.normalized_laplacian_matrix(DiG).toarray())
[[ 1. -0.7071 0. 0. ]
[-0.7071 1. -0.7071 0. ]
[ 0. 0. 1. -1. ]
[ 0. 0. -1. 1. ]]
>>> G = nx.Graph(DiG)
>>> print(nx.normalized_laplacian_matrix(G).toarray())
[[ 1. -0.7071 0. 0. ]
[-0.7071 1. -0.5 0. ]
[ 0. -0.5 1. -0.7071]
[ 0. 0. -0.7071 1. ]]
See Also
--------
laplacian_matrix
normalized_laplacian_spectrum
directed_laplacian_matrix
directed_combinatorial_laplacian_matrix
References
----------
Expand All @@ -126,6 +185,8 @@ def normalized_laplacian_matrix(G, nodelist=None, weight="weight"):
.. [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
Expand Down Expand Up @@ -195,7 +256,7 @@ def directed_laplacian_matrix(
.. math::
L = I - (\Phi^{1/2} P \Phi^{-1/2} + \Phi^{-1/2} P^T \Phi^{1/2} ) / 2
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
Expand Down Expand Up @@ -237,9 +298,16 @@ def directed_laplacian_matrix(
-----
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)` instead.
See Also
--------
laplacian_matrix
normalized_laplacian_matrix
directed_combinatorial_laplacian_matrix
References
----------
Expand Down Expand Up @@ -287,7 +355,7 @@ def directed_combinatorial_laplacian_matrix(
.. math::
L = \Phi - (\Phi P + P^T \Phi) / 2
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]_.
Expand Down Expand Up @@ -328,9 +396,16 @@ def directed_combinatorial_laplacian_matrix(
-----
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)` instead.
See Also
--------
laplacian_matrix
normalized_laplacian_matrix
directed_laplacian_matrix
References
----------
Expand Down
94 changes: 94 additions & 0 deletions networkx/linalg/tests/test_laplacian.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,31 @@ def setup_class(cls):
for node in cls.Gsl.nodes():
cls.Gsl.add_edge(node, node)

# Graph used as an example in Sec. 4.1 of Langville and Meyer,
# "Google's PageRank and Beyond".
cls.DiG = nx.DiGraph()
cls.DiG.add_edges_from(
(
(1, 2),
(1, 3),
(3, 1),
(3, 2),
(3, 5),
(4, 5),
(4, 6),
(5, 4),
(5, 6),
(6, 4),
)
)
cls.DiMG = nx.MultiDiGraph(cls.DiG)
cls.DiWG = nx.DiGraph(
(u, v, {"weight": 0.5, "other": 0.3}) for (u, v) in cls.DiG.edges()
)
cls.DiGsl = cls.DiG.copy()
for node in cls.DiGsl.nodes():
cls.DiGsl.add_edge(node, node)

def test_laplacian(self):
"Graph Laplacian"
# fmt: off
Expand All @@ -35,6 +60,16 @@ def test_laplacian(self):
# fmt: on
WL = 0.5 * NL
OL = 0.3 * NL
# fmt: off
DiNL = np.array([[ 2, -1, -1, 0, 0, 0],
[ 0, 0, 0, 0, 0, 0],
[-1, -1, 3, -1, 0, 0],
[ 0, 0, 0, 2, -1, -1],
[ 0, 0, 0, -1, 2, -1],
[ 0, 0, 0, 0, -1, 1]])
# fmt: on
DiWL = 0.5 * DiNL
DiOL = 0.3 * DiNL
np.testing.assert_equal(nx.laplacian_matrix(self.G).todense(), NL)
np.testing.assert_equal(nx.laplacian_matrix(self.MG).todense(), NL)
np.testing.assert_equal(
Expand All @@ -47,6 +82,20 @@ def test_laplacian(self):
nx.laplacian_matrix(self.WG, weight="other").todense(), OL
)

np.testing.assert_equal(nx.laplacian_matrix(self.DiG).todense(), DiNL)
np.testing.assert_equal(nx.laplacian_matrix(self.DiMG).todense(), DiNL)
np.testing.assert_equal(
nx.laplacian_matrix(self.DiG, nodelist=[1, 2]).todense(),
np.array([[1, -1], [0, 0]]),
)
np.testing.assert_equal(nx.laplacian_matrix(self.DiWG).todense(), DiWL)
np.testing.assert_equal(
nx.laplacian_matrix(self.DiWG, weight=None).todense(), DiNL
)
np.testing.assert_equal(
nx.laplacian_matrix(self.DiWG, weight="other").todense(), DiOL
)

def test_normalized_laplacian(self):
"Generalized Graph Laplacian"
# fmt: off
Expand All @@ -65,6 +114,25 @@ def test_normalized_laplacian(self):
[-0.2887, -0.3333, 0.6667, 0. , 0. ],
[-0.3536, 0. , 0. , 0.5 , 0. ],
[ 0. , 0. , 0. , 0. , 0. ]])

DiG = np.array([[ 1. , 0. , -0.4082, 0. , 0. , 0. ],
[ 0. , 0. , 0. , 0. , 0. , 0. ],
[-0.4082, 0. , 1. , 0. , -0.4082, 0. ],
[ 0. , 0. , 0. , 1. , -0.5 , -0.7071],
[ 0. , 0. , 0. , -0.5 , 1. , -0.7071],
[ 0. , 0. , 0. , -0.7071, 0. , 1. ]])
DiGL = np.array([[ 1. , 0. , -0.4082, 0. , 0. , 0. ],
[ 0. , 0. , 0. , 0. , 0. , 0. ],
[-0.4082, 0. , 1. , -0.4082, 0. , 0. ],
[ 0. , 0. , 0. , 1. , -0.5 , -0.7071],
[ 0. , 0. , 0. , -0.5 , 1. , -0.7071],
[ 0. , 0. , 0. , 0. , -0.7071, 1. ]])
DiLsl = np.array([[ 0.6667, -0.5774, -0.2887, 0. , 0. , 0. ],
[ 0. , 0. , 0. , 0. , 0. , 0. ],
[-0.2887, -0.5 , 0.75 , -0.2887, 0. , 0. ],
[ 0. , 0. , 0. , 0.6667, -0.3333, -0.4082],
[ 0. , 0. , 0. , -0.3333, 0.6667, -0.4082],
[ 0. , 0. , 0. , 0. , -0.4082, 0.5 ]])
# fmt: on

np.testing.assert_almost_equal(
Expand All @@ -90,6 +158,32 @@ def test_normalized_laplacian(self):
nx.normalized_laplacian_matrix(self.Gsl).todense(), Lsl, decimal=3
)

np.testing.assert_almost_equal(
nx.normalized_laplacian_matrix(
self.DiG,
nodelist=range(1, 1 + 6),
).todense(),
DiG,
decimal=3,
)
np.testing.assert_almost_equal(
nx.normalized_laplacian_matrix(self.DiG).todense(), DiGL, decimal=3
)
np.testing.assert_almost_equal(
nx.normalized_laplacian_matrix(self.DiMG).todense(), DiGL, decimal=3
)
np.testing.assert_almost_equal(
nx.normalized_laplacian_matrix(self.DiWG).todense(), DiGL, decimal=3
)
np.testing.assert_almost_equal(
nx.normalized_laplacian_matrix(self.DiWG, weight="other").todense(),
DiGL,
decimal=3,
)
np.testing.assert_almost_equal(
nx.normalized_laplacian_matrix(self.DiGsl).todense(), DiLsl, decimal=3
)


def test_directed_laplacian():
"Directed Laplacian"
Expand Down

0 comments on commit d967c5f

Please sign in to comment.