1"""Laplacian matrix of graphs.
2
3All calculations here are done using the out-degree. For Laplacians using
4in-degree, use `G.reverse(copy=False)` instead of `G` and take the transpose.
5
6The `laplacian_matrix` function provides an unnormalized matrix,
7while `normalized_laplacian_matrix`, `directed_laplacian_matrix`,
8and `directed_combinatorial_laplacian_matrix` are all normalized.
9"""
10
11import networkx as nx
12from networkx.utils import not_implemented_for
13
14__all__ = [
15 "laplacian_matrix",
16 "normalized_laplacian_matrix",
17 "directed_laplacian_matrix",
18 "directed_combinatorial_laplacian_matrix",
19]
20
21
22@nx._dispatchable(edge_attrs="weight")
23def laplacian_matrix(G, nodelist=None, weight="weight"):
24 """Returns the Laplacian matrix of G.
25
26 The graph Laplacian is the matrix L = D - A, where
27 A is the adjacency matrix and D is the diagonal matrix of node degrees.
28
29 Parameters
30 ----------
31 G : graph
32 A NetworkX graph
33
34 nodelist : list, optional
35 The rows and columns are ordered according to the nodes in nodelist.
36 If nodelist is None, then the ordering is produced by G.nodes().
37
38 weight : string or None, optional (default='weight')
39 The edge data key used to compute each value in the matrix.
40 If None, then each edge has weight 1.
41
42 Returns
43 -------
44 L : SciPy sparse array
45 The Laplacian matrix of G.
46
47 Notes
48 -----
49 For MultiGraph, the edges weights are summed.
50
51 This returns an unnormalized matrix. For a normalized output,
52 use `normalized_laplacian_matrix`, `directed_laplacian_matrix`,
53 or `directed_combinatorial_laplacian_matrix`.
54
55 This calculation uses the out-degree of the graph `G`. To use the
56 in-degree for calculations instead, use `G.reverse(copy=False)` and
57 take the transpose.
58
59 See Also
60 --------
61 :func:`~networkx.convert_matrix.to_numpy_array`
62 normalized_laplacian_matrix
63 directed_laplacian_matrix
64 directed_combinatorial_laplacian_matrix
65 :func:`~networkx.linalg.spectrum.laplacian_spectrum`
66
67 Examples
68 --------
69 For graphs with multiple connected components, L is permutation-similar
70 to a block diagonal matrix where each block is the respective Laplacian
71 matrix for each component.
72
73 >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)])
74 >>> print(nx.laplacian_matrix(G).toarray())
75 [[ 1 -1 0 0 0]
76 [-1 2 -1 0 0]
77 [ 0 -1 1 0 0]
78 [ 0 0 0 1 -1]
79 [ 0 0 0 -1 1]]
80
81 >>> edges = [
82 ... (1, 2),
83 ... (2, 1),
84 ... (2, 4),
85 ... (4, 3),
86 ... (3, 4),
87 ... ]
88 >>> DiG = nx.DiGraph(edges)
89 >>> print(nx.laplacian_matrix(DiG).toarray())
90 [[ 1 -1 0 0]
91 [-1 2 -1 0]
92 [ 0 0 1 -1]
93 [ 0 0 -1 1]]
94
95 Notice that node 4 is represented by the third column and row. This is because
96 by default the row/column order is the order of `G.nodes` (i.e. the node added
97 order -- in the edgelist, 4 first appears in (2, 4), before node 3 in edge (4, 3).)
98 To control the node order of the matrix, use the `nodelist` argument.
99
100 >>> print(nx.laplacian_matrix(DiG, nodelist=[1, 2, 3, 4]).toarray())
101 [[ 1 -1 0 0]
102 [-1 2 0 -1]
103 [ 0 0 1 -1]
104 [ 0 0 -1 1]]
105
106 This calculation uses the out-degree of the graph `G`. To use the
107 in-degree for calculations instead, use `G.reverse(copy=False)` and
108 take the transpose.
109
110 >>> print(nx.laplacian_matrix(DiG.reverse(copy=False)).toarray().T)
111 [[ 1 -1 0 0]
112 [-1 1 -1 0]
113 [ 0 0 2 -1]
114 [ 0 0 -1 1]]
115
116 References
117 ----------
118 .. [1] Langville, Amy N., and Carl D. Meyer. Google’s PageRank and Beyond:
119 The Science of Search Engine Rankings. Princeton University Press, 2006.
120
121 """
122 import scipy as sp
123
124 if nodelist is None:
125 nodelist = list(G)
126 A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr")
127 n, m = A.shape
128 D = sp.sparse.dia_array((A.sum(axis=1), 0), shape=(m, n)).tocsr()
129 return D - A
130
131
132@nx._dispatchable(edge_attrs="weight")
133def normalized_laplacian_matrix(G, nodelist=None, weight="weight"):
134 r"""Returns the normalized Laplacian matrix of G.
135
136 The normalized graph Laplacian is the matrix
137
138 .. math::
139
140 N = D^{-1/2} L D^{-1/2}
141
142 where `L` is the graph Laplacian and `D` is the diagonal matrix of
143 node degrees [1]_.
144
145 Parameters
146 ----------
147 G : graph
148 A NetworkX graph
149
150 nodelist : list, optional
151 The rows and columns are ordered according to the nodes in nodelist.
152 If nodelist is None, then the ordering is produced by G.nodes().
153
154 weight : string or None, optional (default='weight')
155 The edge data key used to compute each value in the matrix.
156 If None, then each edge has weight 1.
157
158 Returns
159 -------
160 N : SciPy sparse array
161 The normalized Laplacian matrix of G.
162
163 Notes
164 -----
165 For MultiGraph, the edges weights are summed.
166 See :func:`to_numpy_array` for other options.
167
168 If the Graph contains selfloops, D is defined as ``diag(sum(A, 1))``, where A is
169 the adjacency matrix [2]_.
170
171 This calculation uses the out-degree of the graph `G`. To use the
172 in-degree for calculations instead, use `G.reverse(copy=False)` and
173 take the transpose.
174
175 For an unnormalized output, use `laplacian_matrix`.
176
177 Examples
178 --------
179
180 >>> import numpy as np
181 >>> edges = [
182 ... (1, 2),
183 ... (2, 1),
184 ... (2, 4),
185 ... (4, 3),
186 ... (3, 4),
187 ... ]
188 >>> DiG = nx.DiGraph(edges)
189 >>> print(nx.normalized_laplacian_matrix(DiG).toarray())
190 [[ 1. -0.70710678 0. 0. ]
191 [-0.70710678 1. -0.70710678 0. ]
192 [ 0. 0. 1. -1. ]
193 [ 0. 0. -1. 1. ]]
194
195 Notice that node 4 is represented by the third column and row. This is because
196 by default the row/column order is the order of `G.nodes` (i.e. the node added
197 order -- in the edgelist, 4 first appears in (2, 4), before node 3 in edge (4, 3).)
198 To control the node order of the matrix, use the `nodelist` argument.
199
200 >>> print(nx.normalized_laplacian_matrix(DiG, nodelist=[1, 2, 3, 4]).toarray())
201 [[ 1. -0.70710678 0. 0. ]
202 [-0.70710678 1. 0. -0.70710678]
203 [ 0. 0. 1. -1. ]
204 [ 0. 0. -1. 1. ]]
205 >>> G = nx.Graph(edges)
206 >>> print(nx.normalized_laplacian_matrix(G).toarray())
207 [[ 1. -0.70710678 0. 0. ]
208 [-0.70710678 1. -0.5 0. ]
209 [ 0. -0.5 1. -0.70710678]
210 [ 0. 0. -0.70710678 1. ]]
211
212 See Also
213 --------
214 laplacian_matrix
215 normalized_laplacian_spectrum
216 directed_laplacian_matrix
217 directed_combinatorial_laplacian_matrix
218
219 References
220 ----------
221 .. [1] Fan Chung-Graham, Spectral Graph Theory,
222 CBMS Regional Conference Series in Mathematics, Number 92, 1997.
223 .. [2] Steve Butler, Interlacing For Weighted Graphs Using The Normalized
224 Laplacian, Electronic Journal of Linear Algebra, Volume 16, pp. 90-98,
225 March 2007.
226 .. [3] Langville, Amy N., and Carl D. Meyer. Google’s PageRank and Beyond:
227 The Science of Search Engine Rankings. Princeton University Press, 2006.
228 """
229 import numpy as np
230 import scipy as sp
231
232 if nodelist is None:
233 nodelist = list(G)
234 A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr")
235 n, _ = A.shape
236 diags = A.sum(axis=1)
237 D = sp.sparse.dia_array((diags, 0), shape=(n, n)).tocsr()
238 L = D - A
239 with np.errstate(divide="ignore"):
240 diags_sqrt = 1.0 / np.sqrt(diags)
241 diags_sqrt[np.isinf(diags_sqrt)] = 0
242 DH = sp.sparse.dia_array((diags_sqrt, 0), shape=(n, n)).tocsr()
243 return DH @ (L @ DH)
244
245
246###############################################################################
247# Code based on work from https://github.com/bjedwards
248
249
250@not_implemented_for("undirected")
251@not_implemented_for("multigraph")
252@nx._dispatchable(edge_attrs="weight")
253def directed_laplacian_matrix(
254 G, nodelist=None, weight="weight", walk_type=None, alpha=0.95
255):
256 r"""Returns the directed Laplacian matrix of G.
257
258 The graph directed Laplacian is the matrix
259
260 .. math::
261
262 L = I - \frac{1}{2} \left (\Phi^{1/2} P \Phi^{-1/2} + \Phi^{-1/2} P^T \Phi^{1/2} \right )
263
264 where `I` is the identity matrix, `P` is the transition matrix of the
265 graph, and `\Phi` a matrix with the Perron vector of `P` in the diagonal and
266 zeros elsewhere [1]_.
267
268 Depending on the value of walk_type, `P` can be the transition matrix
269 induced by a random walk, a lazy random walk, or a random walk with
270 teleportation (PageRank).
271
272 Parameters
273 ----------
274 G : DiGraph
275 A NetworkX graph
276
277 nodelist : list, optional
278 The rows and columns are ordered according to the nodes in nodelist.
279 If nodelist is None, then the ordering is produced by G.nodes().
280
281 weight : string or None, optional (default='weight')
282 The edge data key used to compute each value in the matrix.
283 If None, then each edge has weight 1.
284
285 walk_type : string or None, optional (default=None)
286 One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None``
287 (the default), then a value is selected according to the properties of `G`:
288 - ``walk_type="random"`` if `G` is strongly connected and aperiodic
289 - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic
290 - ``walk_type="pagerank"`` for all other cases.
291
292 alpha : real
293 (1 - alpha) is the teleportation probability used with pagerank
294
295 Returns
296 -------
297 L : NumPy matrix
298 Normalized Laplacian of G.
299
300 Notes
301 -----
302 Only implemented for DiGraphs
303
304 The result is always a symmetric matrix.
305
306 This calculation uses the out-degree of the graph `G`. To use the
307 in-degree for calculations instead, use `G.reverse(copy=False)` and
308 take the transpose.
309
310 See Also
311 --------
312 laplacian_matrix
313 normalized_laplacian_matrix
314 directed_combinatorial_laplacian_matrix
315
316 References
317 ----------
318 .. [1] Fan Chung (2005).
319 Laplacians and the Cheeger inequality for directed graphs.
320 Annals of Combinatorics, 9(1), 2005
321 """
322 import numpy as np
323 import scipy as sp
324
325 # NOTE: P has type ndarray if walk_type=="pagerank", else csr_array
326 P = _transition_matrix(
327 G, nodelist=nodelist, weight=weight, walk_type=walk_type, alpha=alpha
328 )
329
330 n, m = P.shape
331
332 evals, evecs = sp.sparse.linalg.eigs(P.T, k=1)
333 v = evecs.flatten().real
334 p = v / v.sum()
335 # p>=0 by Perron-Frobenius Thm. Use abs() to fix roundoff across zero gh-6865
336 sqrtp = np.sqrt(np.abs(p))
337 Q = (
338 sp.sparse.dia_array((sqrtp, 0), shape=(n, n)).tocsr()
339 @ P
340 @ sp.sparse.dia_array((1.0 / sqrtp, 0), shape=(n, n)).tocsr()
341 )
342 # NOTE: This could be sparsified for the non-pagerank cases
343 I = np.identity(len(G))
344
345 return I - (Q + Q.T) / 2.0
346
347
348@not_implemented_for("undirected")
349@not_implemented_for("multigraph")
350@nx._dispatchable(edge_attrs="weight")
351def directed_combinatorial_laplacian_matrix(
352 G, nodelist=None, weight="weight", walk_type=None, alpha=0.95
353):
354 r"""Return the directed combinatorial Laplacian matrix of G.
355
356 The graph directed combinatorial Laplacian is the matrix
357
358 .. math::
359
360 L = \Phi - \frac{1}{2} \left (\Phi P + P^T \Phi \right)
361
362 where `P` is the transition matrix of the graph and `\Phi` a matrix
363 with the Perron vector of `P` in the diagonal and zeros elsewhere [1]_.
364
365 Depending on the value of walk_type, `P` can be the transition matrix
366 induced by a random walk, a lazy random walk, or a random walk with
367 teleportation (PageRank).
368
369 Parameters
370 ----------
371 G : DiGraph
372 A NetworkX graph
373
374 nodelist : list, optional
375 The rows and columns are ordered according to the nodes in nodelist.
376 If nodelist is None, then the ordering is produced by G.nodes().
377
378 weight : string or None, optional (default='weight')
379 The edge data key used to compute each value in the matrix.
380 If None, then each edge has weight 1.
381
382 walk_type : string or None, optional (default=None)
383 One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None``
384 (the default), then a value is selected according to the properties of `G`:
385 - ``walk_type="random"`` if `G` is strongly connected and aperiodic
386 - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic
387 - ``walk_type="pagerank"`` for all other cases.
388
389 alpha : real
390 (1 - alpha) is the teleportation probability used with pagerank
391
392 Returns
393 -------
394 L : NumPy matrix
395 Combinatorial Laplacian of G.
396
397 Notes
398 -----
399 Only implemented for DiGraphs
400
401 The result is always a symmetric matrix.
402
403 This calculation uses the out-degree of the graph `G`. To use the
404 in-degree for calculations instead, use `G.reverse(copy=False)` and
405 take the transpose.
406
407 See Also
408 --------
409 laplacian_matrix
410 normalized_laplacian_matrix
411 directed_laplacian_matrix
412
413 References
414 ----------
415 .. [1] Fan Chung (2005).
416 Laplacians and the Cheeger inequality for directed graphs.
417 Annals of Combinatorics, 9(1), 2005
418 """
419 import scipy as sp
420
421 P = _transition_matrix(
422 G, nodelist=nodelist, weight=weight, walk_type=walk_type, alpha=alpha
423 )
424
425 n, m = P.shape
426
427 evals, evecs = sp.sparse.linalg.eigs(P.T, k=1)
428 v = evecs.flatten().real
429 p = v / v.sum()
430 # NOTE: could be improved by not densifying
431 Phi = sp.sparse.dia_array((p, 0), shape=(n, n)).toarray()
432
433 return Phi - (Phi @ P + P.T @ Phi) / 2.0
434
435
436def _transition_matrix(G, nodelist=None, weight="weight", walk_type=None, alpha=0.95):
437 """Returns the transition matrix of G.
438
439 This is a row stochastic giving the transition probabilities while
440 performing a random walk on the graph. Depending on the value of walk_type,
441 P can be the transition matrix induced by a random walk, a lazy random walk,
442 or a random walk with teleportation (PageRank).
443
444 Parameters
445 ----------
446 G : DiGraph
447 A NetworkX graph
448
449 nodelist : list, optional
450 The rows and columns are ordered according to the nodes in nodelist.
451 If nodelist is None, then the ordering is produced by G.nodes().
452
453 weight : string or None, optional (default='weight')
454 The edge data key used to compute each value in the matrix.
455 If None, then each edge has weight 1.
456
457 walk_type : string or None, optional (default=None)
458 One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None``
459 (the default), then a value is selected according to the properties of `G`:
460 - ``walk_type="random"`` if `G` is strongly connected and aperiodic
461 - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic
462 - ``walk_type="pagerank"`` for all other cases.
463
464 alpha : real
465 (1 - alpha) is the teleportation probability used with pagerank
466
467 Returns
468 -------
469 P : numpy.ndarray
470 transition matrix of G.
471
472 Raises
473 ------
474 NetworkXError
475 If walk_type not specified or alpha not in valid range
476 """
477 import numpy as np
478 import scipy as sp
479
480 if walk_type is None:
481 if nx.is_strongly_connected(G):
482 if nx.is_aperiodic(G):
483 walk_type = "random"
484 else:
485 walk_type = "lazy"
486 else:
487 walk_type = "pagerank"
488
489 A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, dtype=float)
490 n, m = A.shape
491 if walk_type in ["random", "lazy"]:
492 DI = sp.sparse.dia_array((1.0 / A.sum(axis=1), 0), shape=(n, n)).tocsr()
493 if walk_type == "random":
494 P = DI @ A
495 else:
496 I = sp.sparse.eye_array(n, format="csr")
497 P = (I + DI @ A) / 2.0
498
499 elif walk_type == "pagerank":
500 if not (0 < alpha < 1):
501 raise nx.NetworkXError("alpha must be between 0 and 1")
502 # this is using a dense representation. NOTE: This should be sparsified!
503 A = A.toarray()
504 # add constant to dangling nodes' row
505 A[A.sum(axis=1) == 0, :] = 1 / n
506 # normalize
507 A = A / A.sum(axis=1)[np.newaxis, :].T
508 P = alpha * A + (1 - alpha) / n
509 else:
510 raise nx.NetworkXError("walk_type must be random, lazy, or pagerank")
511
512 return P