aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/linalg
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/networkx/linalg
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/linalg')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/__init__.py13
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/algebraicconnectivity.py657
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/attrmatrix.py465
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/bethehessianmatrix.py79
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/graphmatrix.py168
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/laplacianmatrix.py617
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/modularitymatrix.py166
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/spectrum.py186
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_algebraic_connectivity.py402
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_attrmatrix.py108
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_bethehessian.py41
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_graphmatrix.py276
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_laplacian.py336
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_modularity.py87
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_spectrum.py71
16 files changed, 3672 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/__init__.py b/.venv/lib/python3.12/site-packages/networkx/linalg/__init__.py
new file mode 100644
index 00000000..119db185
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/__init__.py
@@ -0,0 +1,13 @@
+from networkx.linalg.attrmatrix import *
+from networkx.linalg import attrmatrix
+from networkx.linalg.spectrum import *
+from networkx.linalg import spectrum
+from networkx.linalg.graphmatrix import *
+from networkx.linalg import graphmatrix
+from networkx.linalg.laplacianmatrix import *
+from networkx.linalg import laplacianmatrix
+from networkx.linalg.algebraicconnectivity import *
+from networkx.linalg.modularitymatrix import *
+from networkx.linalg import modularitymatrix
+from networkx.linalg.bethehessianmatrix import *
+from networkx.linalg import bethehessianmatrix
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/algebraicconnectivity.py b/.venv/lib/python3.12/site-packages/networkx/linalg/algebraicconnectivity.py
new file mode 100644
index 00000000..c94972a5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/algebraicconnectivity.py
@@ -0,0 +1,657 @@
+"""
+Algebraic connectivity and Fiedler vectors of undirected graphs.
+"""
+
+from functools import partial
+
+import networkx as nx
+from networkx.utils import (
+ not_implemented_for,
+ np_random_state,
+ reverse_cuthill_mckee_ordering,
+)
+
+__all__ = [
+ "algebraic_connectivity",
+ "fiedler_vector",
+ "spectral_ordering",
+ "spectral_bisection",
+]
+
+
+class _PCGSolver:
+ """Preconditioned conjugate gradient method.
+
+ To solve Ax = b:
+ M = A.diagonal() # or some other preconditioner
+ solver = _PCGSolver(lambda x: A * x, lambda x: M * x)
+ x = solver.solve(b)
+
+ The inputs A and M are functions which compute
+ matrix multiplication on the argument.
+ A - multiply by the matrix A in Ax=b
+ M - multiply by M, the preconditioner surrogate for A
+
+ Warning: There is no limit on number of iterations.
+ """
+
+ def __init__(self, A, M):
+ self._A = A
+ self._M = M
+
+ def solve(self, B, tol):
+ import numpy as np
+
+ # Densifying step - can this be kept sparse?
+ B = np.asarray(B)
+ X = np.ndarray(B.shape, order="F")
+ for j in range(B.shape[1]):
+ X[:, j] = self._solve(B[:, j], tol)
+ return X
+
+ def _solve(self, b, tol):
+ import numpy as np
+ import scipy as sp
+
+ A = self._A
+ M = self._M
+ tol *= sp.linalg.blas.dasum(b)
+ # Initialize.
+ x = np.zeros(b.shape)
+ r = b.copy()
+ z = M(r)
+ rz = sp.linalg.blas.ddot(r, z)
+ p = z.copy()
+ # Iterate.
+ while True:
+ Ap = A(p)
+ alpha = rz / sp.linalg.blas.ddot(p, Ap)
+ x = sp.linalg.blas.daxpy(p, x, a=alpha)
+ r = sp.linalg.blas.daxpy(Ap, r, a=-alpha)
+ if sp.linalg.blas.dasum(r) < tol:
+ return x
+ z = M(r)
+ beta = sp.linalg.blas.ddot(r, z)
+ beta, rz = beta / rz, beta
+ p = sp.linalg.blas.daxpy(p, z, a=beta)
+
+
+class _LUSolver:
+ """LU factorization.
+
+ To solve Ax = b:
+ solver = _LUSolver(A)
+ x = solver.solve(b)
+
+ optional argument `tol` on solve method is ignored but included
+ to match _PCGsolver API.
+ """
+
+ def __init__(self, A):
+ import scipy as sp
+
+ self._LU = sp.sparse.linalg.splu(
+ A,
+ permc_spec="MMD_AT_PLUS_A",
+ diag_pivot_thresh=0.0,
+ options={"Equil": True, "SymmetricMode": True},
+ )
+
+ def solve(self, B, tol=None):
+ import numpy as np
+
+ B = np.asarray(B)
+ X = np.ndarray(B.shape, order="F")
+ for j in range(B.shape[1]):
+ X[:, j] = self._LU.solve(B[:, j])
+ return X
+
+
+def _preprocess_graph(G, weight):
+ """Compute edge weights and eliminate zero-weight edges."""
+ if G.is_directed():
+ H = nx.MultiGraph()
+ H.add_nodes_from(G)
+ H.add_weighted_edges_from(
+ ((u, v, e.get(weight, 1.0)) for u, v, e in G.edges(data=True) if u != v),
+ weight=weight,
+ )
+ G = H
+ if not G.is_multigraph():
+ edges = (
+ (u, v, abs(e.get(weight, 1.0))) for u, v, e in G.edges(data=True) if u != v
+ )
+ else:
+ edges = (
+ (u, v, sum(abs(e.get(weight, 1.0)) for e in G[u][v].values()))
+ for u, v in G.edges()
+ if u != v
+ )
+ H = nx.Graph()
+ H.add_nodes_from(G)
+ H.add_weighted_edges_from((u, v, e) for u, v, e in edges if e != 0)
+ return H
+
+
+def _rcm_estimate(G, nodelist):
+ """Estimate the Fiedler vector using the reverse Cuthill-McKee ordering."""
+ import numpy as np
+
+ G = G.subgraph(nodelist)
+ order = reverse_cuthill_mckee_ordering(G)
+ n = len(nodelist)
+ index = dict(zip(nodelist, range(n)))
+ x = np.ndarray(n, dtype=float)
+ for i, u in enumerate(order):
+ x[index[u]] = i
+ x -= (n - 1) / 2.0
+ return x
+
+
+def _tracemin_fiedler(L, X, normalized, tol, method):
+ """Compute the Fiedler vector of L using the TraceMIN-Fiedler algorithm.
+
+ The Fiedler vector of a connected undirected graph is the eigenvector
+ corresponding to the second smallest eigenvalue of the Laplacian matrix
+ of the graph. This function starts with the Laplacian L, not the Graph.
+
+ Parameters
+ ----------
+ L : Laplacian of a possibly weighted or normalized, but undirected graph
+
+ X : Initial guess for a solution. Usually a matrix of random numbers.
+ This function allows more than one column in X to identify more than
+ one eigenvector if desired.
+
+ normalized : bool
+ Whether the normalized Laplacian matrix is used.
+
+ tol : float
+ Tolerance of relative residual in eigenvalue computation.
+ Warning: There is no limit on number of iterations.
+
+ method : string
+ Should be 'tracemin_pcg' or 'tracemin_lu'.
+ Otherwise exception is raised.
+
+ Returns
+ -------
+ sigma, X : Two NumPy arrays of floats.
+ The lowest eigenvalues and corresponding eigenvectors of L.
+ The size of input X determines the size of these outputs.
+ As this is for Fiedler vectors, the zero eigenvalue (and
+ constant eigenvector) are avoided.
+ """
+ import numpy as np
+ import scipy as sp
+
+ n = X.shape[0]
+
+ if normalized:
+ # Form the normalized Laplacian matrix and determine the eigenvector of
+ # its nullspace.
+ e = np.sqrt(L.diagonal())
+ # TODO: rm csr_array wrapper when spdiags array creation becomes available
+ D = sp.sparse.csr_array(sp.sparse.spdiags(1 / e, 0, n, n, format="csr"))
+ L = D @ L @ D
+ e *= 1.0 / np.linalg.norm(e, 2)
+
+ if normalized:
+
+ def project(X):
+ """Make X orthogonal to the nullspace of L."""
+ X = np.asarray(X)
+ for j in range(X.shape[1]):
+ X[:, j] -= (X[:, j] @ e) * e
+
+ else:
+
+ def project(X):
+ """Make X orthogonal to the nullspace of L."""
+ X = np.asarray(X)
+ for j in range(X.shape[1]):
+ X[:, j] -= X[:, j].sum() / n
+
+ if method == "tracemin_pcg":
+ D = L.diagonal().astype(float)
+ solver = _PCGSolver(lambda x: L @ x, lambda x: D * x)
+ elif method == "tracemin_lu":
+ # Convert A to CSC to suppress SparseEfficiencyWarning.
+ A = sp.sparse.csc_array(L, dtype=float, copy=True)
+ # Force A to be nonsingular. Since A is the Laplacian matrix of a
+ # connected graph, its rank deficiency is one, and thus one diagonal
+ # element needs to modified. Changing to infinity forces a zero in the
+ # corresponding element in the solution.
+ i = (A.indptr[1:] - A.indptr[:-1]).argmax()
+ A[i, i] = np.inf
+ solver = _LUSolver(A)
+ else:
+ raise nx.NetworkXError(f"Unknown linear system solver: {method}")
+
+ # Initialize.
+ Lnorm = abs(L).sum(axis=1).flatten().max()
+ project(X)
+ W = np.ndarray(X.shape, order="F")
+
+ while True:
+ # Orthonormalize X.
+ X = np.linalg.qr(X)[0]
+ # Compute iteration matrix H.
+ W[:, :] = L @ X
+ H = X.T @ W
+ sigma, Y = sp.linalg.eigh(H, overwrite_a=True)
+ # Compute the Ritz vectors.
+ X = X @ Y
+ # Test for convergence exploiting the fact that L * X == W * Y.
+ res = sp.linalg.blas.dasum(W @ Y[:, 0] - sigma[0] * X[:, 0]) / Lnorm
+ if res < tol:
+ break
+ # Compute X = L \ X / (X' * (L \ X)).
+ # L \ X can have an arbitrary projection on the nullspace of L,
+ # which will be eliminated.
+ W[:, :] = solver.solve(X, tol)
+ X = (sp.linalg.inv(W.T @ X) @ W.T).T # Preserves Fortran storage order.
+ project(X)
+
+ return sigma, np.asarray(X)
+
+
+def _get_fiedler_func(method):
+ """Returns a function that solves the Fiedler eigenvalue problem."""
+ import numpy as np
+
+ if method == "tracemin": # old style keyword <v2.1
+ method = "tracemin_pcg"
+ if method in ("tracemin_pcg", "tracemin_lu"):
+
+ def find_fiedler(L, x, normalized, tol, seed):
+ q = 1 if method == "tracemin_pcg" else min(4, L.shape[0] - 1)
+ X = np.asarray(seed.normal(size=(q, L.shape[0]))).T
+ sigma, X = _tracemin_fiedler(L, X, normalized, tol, method)
+ return sigma[0], X[:, 0]
+
+ elif method == "lanczos" or method == "lobpcg":
+
+ def find_fiedler(L, x, normalized, tol, seed):
+ import scipy as sp
+
+ L = sp.sparse.csc_array(L, dtype=float)
+ n = L.shape[0]
+ if normalized:
+ # TODO: rm csc_array wrapping when spdiags array becomes available
+ D = sp.sparse.csc_array(
+ sp.sparse.spdiags(
+ 1.0 / np.sqrt(L.diagonal()), [0], n, n, format="csc"
+ )
+ )
+ L = D @ L @ D
+ if method == "lanczos" or n < 10:
+ # Avoid LOBPCG when n < 10 due to
+ # https://github.com/scipy/scipy/issues/3592
+ # https://github.com/scipy/scipy/pull/3594
+ sigma, X = sp.sparse.linalg.eigsh(
+ L, 2, which="SM", tol=tol, return_eigenvectors=True
+ )
+ return sigma[1], X[:, 1]
+ else:
+ X = np.asarray(np.atleast_2d(x).T)
+ # TODO: rm csr_array wrapping when spdiags array becomes available
+ M = sp.sparse.csr_array(sp.sparse.spdiags(1.0 / L.diagonal(), 0, n, n))
+ Y = np.ones(n)
+ if normalized:
+ Y /= D.diagonal()
+ sigma, X = sp.sparse.linalg.lobpcg(
+ L, X, M=M, Y=np.atleast_2d(Y).T, tol=tol, maxiter=n, largest=False
+ )
+ return sigma[0], X[:, 0]
+
+ else:
+ raise nx.NetworkXError(f"unknown method {method!r}.")
+
+ return find_fiedler
+
+
+@not_implemented_for("directed")
+@np_random_state(5)
+@nx._dispatchable(edge_attrs="weight")
+def algebraic_connectivity(
+ G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None
+):
+ r"""Returns the algebraic connectivity of an undirected graph.
+
+ The algebraic connectivity of a connected undirected graph is the second
+ smallest eigenvalue of its Laplacian matrix.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ weight : object, optional (default: None)
+ The data key used to determine the weight of each edge. If None, then
+ each edge has unit weight.
+
+ normalized : bool, optional (default: False)
+ Whether the normalized Laplacian matrix is used.
+
+ tol : float, optional (default: 1e-8)
+ Tolerance of relative residual in eigenvalue computation.
+
+ method : string, optional (default: 'tracemin_pcg')
+ Method of eigenvalue computation. It must be one of the tracemin
+ options shown below (TraceMIN), 'lanczos' (Lanczos iteration)
+ or 'lobpcg' (LOBPCG).
+
+ The TraceMIN algorithm uses a linear system solver. The following
+ values allow specifying the solver to be used.
+
+ =============== ========================================
+ Value Solver
+ =============== ========================================
+ 'tracemin_pcg' Preconditioned conjugate gradient method
+ 'tracemin_lu' LU factorization
+ =============== ========================================
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ algebraic_connectivity : float
+ Algebraic connectivity.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If G is directed.
+
+ NetworkXError
+ If G has less than two nodes.
+
+ Notes
+ -----
+ Edge weights are interpreted by their absolute values. For MultiGraph's,
+ weights of parallel edges are summed. Zero-weighted edges are ignored.
+
+ See Also
+ --------
+ laplacian_matrix
+
+ Examples
+ --------
+ For undirected graphs algebraic connectivity can tell us if a graph is connected or not
+ `G` is connected iff ``algebraic_connectivity(G) > 0``:
+
+ >>> G = nx.complete_graph(5)
+ >>> nx.algebraic_connectivity(G) > 0
+ True
+ >>> G.add_node(10) # G is no longer connected
+ >>> nx.algebraic_connectivity(G) > 0
+ False
+
+ """
+ if len(G) < 2:
+ raise nx.NetworkXError("graph has less than two nodes.")
+ G = _preprocess_graph(G, weight)
+ if not nx.is_connected(G):
+ return 0.0
+
+ L = nx.laplacian_matrix(G)
+ if L.shape[0] == 2:
+ return 2.0 * float(L[0, 0]) if not normalized else 2.0
+
+ find_fiedler = _get_fiedler_func(method)
+ x = None if method != "lobpcg" else _rcm_estimate(G, G)
+ sigma, fiedler = find_fiedler(L, x, normalized, tol, seed)
+ return float(sigma)
+
+
+@not_implemented_for("directed")
+@np_random_state(5)
+@nx._dispatchable(edge_attrs="weight")
+def fiedler_vector(
+ G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None
+):
+ """Returns the Fiedler vector of a connected undirected graph.
+
+ The Fiedler vector of a connected undirected graph is the eigenvector
+ corresponding to the second smallest eigenvalue of the Laplacian matrix
+ of the graph.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ weight : object, optional (default: None)
+ The data key used to determine the weight of each edge. If None, then
+ each edge has unit weight.
+
+ normalized : bool, optional (default: False)
+ Whether the normalized Laplacian matrix is used.
+
+ tol : float, optional (default: 1e-8)
+ Tolerance of relative residual in eigenvalue computation.
+
+ method : string, optional (default: 'tracemin_pcg')
+ Method of eigenvalue computation. It must be one of the tracemin
+ options shown below (TraceMIN), 'lanczos' (Lanczos iteration)
+ or 'lobpcg' (LOBPCG).
+
+ The TraceMIN algorithm uses a linear system solver. The following
+ values allow specifying the solver to be used.
+
+ =============== ========================================
+ Value Solver
+ =============== ========================================
+ 'tracemin_pcg' Preconditioned conjugate gradient method
+ 'tracemin_lu' LU factorization
+ =============== ========================================
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ fiedler_vector : NumPy array of floats.
+ Fiedler vector.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If G is directed.
+
+ NetworkXError
+ If G has less than two nodes or is not connected.
+
+ Notes
+ -----
+ Edge weights are interpreted by their absolute values. For MultiGraph's,
+ weights of parallel edges are summed. Zero-weighted edges are ignored.
+
+ See Also
+ --------
+ laplacian_matrix
+
+ Examples
+ --------
+ Given a connected graph the signs of the values in the Fiedler vector can be
+ used to partition the graph into two components.
+
+ >>> G = nx.barbell_graph(5, 0)
+ >>> nx.fiedler_vector(G, normalized=True, seed=1)
+ array([-0.32864129, -0.32864129, -0.32864129, -0.32864129, -0.26072899,
+ 0.26072899, 0.32864129, 0.32864129, 0.32864129, 0.32864129])
+
+ The connected components are the two 5-node cliques of the barbell graph.
+ """
+ import numpy as np
+
+ if len(G) < 2:
+ raise nx.NetworkXError("graph has less than two nodes.")
+ G = _preprocess_graph(G, weight)
+ if not nx.is_connected(G):
+ raise nx.NetworkXError("graph is not connected.")
+
+ if len(G) == 2:
+ return np.array([1.0, -1.0])
+
+ find_fiedler = _get_fiedler_func(method)
+ L = nx.laplacian_matrix(G)
+ x = None if method != "lobpcg" else _rcm_estimate(G, G)
+ sigma, fiedler = find_fiedler(L, x, normalized, tol, seed)
+ return fiedler
+
+
+@np_random_state(5)
+@nx._dispatchable(edge_attrs="weight")
+def spectral_ordering(
+ G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None
+):
+ """Compute the spectral_ordering of a graph.
+
+ The spectral ordering of a graph is an ordering of its nodes where nodes
+ in the same weakly connected components appear contiguous and ordered by
+ their corresponding elements in the Fiedler vector of the component.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ A graph.
+
+ weight : object, optional (default: None)
+ The data key used to determine the weight of each edge. If None, then
+ each edge has unit weight.
+
+ normalized : bool, optional (default: False)
+ Whether the normalized Laplacian matrix is used.
+
+ tol : float, optional (default: 1e-8)
+ Tolerance of relative residual in eigenvalue computation.
+
+ method : string, optional (default: 'tracemin_pcg')
+ Method of eigenvalue computation. It must be one of the tracemin
+ options shown below (TraceMIN), 'lanczos' (Lanczos iteration)
+ or 'lobpcg' (LOBPCG).
+
+ The TraceMIN algorithm uses a linear system solver. The following
+ values allow specifying the solver to be used.
+
+ =============== ========================================
+ Value Solver
+ =============== ========================================
+ 'tracemin_pcg' Preconditioned conjugate gradient method
+ 'tracemin_lu' LU factorization
+ =============== ========================================
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ spectral_ordering : NumPy array of floats.
+ Spectral ordering of nodes.
+
+ Raises
+ ------
+ NetworkXError
+ If G is empty.
+
+ Notes
+ -----
+ Edge weights are interpreted by their absolute values. For MultiGraph's,
+ weights of parallel edges are summed. Zero-weighted edges are ignored.
+
+ See Also
+ --------
+ laplacian_matrix
+ """
+ if len(G) == 0:
+ raise nx.NetworkXError("graph is empty.")
+ G = _preprocess_graph(G, weight)
+
+ find_fiedler = _get_fiedler_func(method)
+ order = []
+ for component in nx.connected_components(G):
+ size = len(component)
+ if size > 2:
+ L = nx.laplacian_matrix(G, component)
+ x = None if method != "lobpcg" else _rcm_estimate(G, component)
+ sigma, fiedler = find_fiedler(L, x, normalized, tol, seed)
+ sort_info = zip(fiedler, range(size), component)
+ order.extend(u for x, c, u in sorted(sort_info))
+ else:
+ order.extend(component)
+
+ return order
+
+
+@nx._dispatchable(edge_attrs="weight")
+def spectral_bisection(
+ G, weight="weight", normalized=False, tol=1e-8, method="tracemin_pcg", seed=None
+):
+ """Bisect the graph using the Fiedler vector.
+
+ This method uses the Fiedler vector to bisect a graph.
+ The partition is defined by the nodes which are associated with
+ either positive or negative values in the vector.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ weight : str, optional (default: weight)
+ The data key used to determine the weight of each edge. If None, then
+ each edge has unit weight.
+
+ normalized : bool, optional (default: False)
+ Whether the normalized Laplacian matrix is used.
+
+ tol : float, optional (default: 1e-8)
+ Tolerance of relative residual in eigenvalue computation.
+
+ method : string, optional (default: 'tracemin_pcg')
+ Method of eigenvalue computation. It must be one of the tracemin
+ options shown below (TraceMIN), 'lanczos' (Lanczos iteration)
+ or 'lobpcg' (LOBPCG).
+
+ The TraceMIN algorithm uses a linear system solver. The following
+ values allow specifying the solver to be used.
+
+ =============== ========================================
+ Value Solver
+ =============== ========================================
+ 'tracemin_pcg' Preconditioned conjugate gradient method
+ 'tracemin_lu' LU factorization
+ =============== ========================================
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ bisection : tuple of sets
+ Sets with the bisection of nodes
+
+ Examples
+ --------
+ >>> G = nx.barbell_graph(3, 0)
+ >>> nx.spectral_bisection(G)
+ ({0, 1, 2}, {3, 4, 5})
+
+ References
+ ----------
+ .. [1] M. E. J Newman 'Networks: An Introduction', pages 364-370
+ Oxford University Press 2011.
+ """
+ import numpy as np
+
+ v = nx.fiedler_vector(G, weight, normalized, tol, method, seed)
+ nodes = np.array(list(G))
+ pos_vals = v >= 0
+
+ return set(nodes[~pos_vals].tolist()), set(nodes[pos_vals].tolist())
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/attrmatrix.py b/.venv/lib/python3.12/site-packages/networkx/linalg/attrmatrix.py
new file mode 100644
index 00000000..b5a70492
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/attrmatrix.py
@@ -0,0 +1,465 @@
+"""
+Functions for constructing matrix-like objects from graph attributes.
+"""
+
+import networkx as nx
+
+__all__ = ["attr_matrix", "attr_sparse_matrix"]
+
+
+def _node_value(G, node_attr):
+ """Returns a function that returns a value from G.nodes[u].
+
+ We return a function expecting a node as its sole argument. Then, in the
+ simplest scenario, the returned function will return G.nodes[u][node_attr].
+ However, we also handle the case when `node_attr` is None or when it is a
+ function itself.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ node_attr : {None, str, callable}
+ Specification of how the value of the node attribute should be obtained
+ from the node attribute dictionary.
+
+ Returns
+ -------
+ value : function
+ A function expecting a node as its sole argument. The function will
+ returns a value from G.nodes[u] that depends on `edge_attr`.
+
+ """
+ if node_attr is None:
+
+ def value(u):
+ return u
+
+ elif not callable(node_attr):
+ # assume it is a key for the node attribute dictionary
+ def value(u):
+ return G.nodes[u][node_attr]
+
+ else:
+ # Advanced: Allow users to specify something else.
+ #
+ # For example,
+ # node_attr = lambda u: G.nodes[u].get('size', .5) * 3
+ #
+ value = node_attr
+
+ return value
+
+
+def _edge_value(G, edge_attr):
+ """Returns a function that returns a value from G[u][v].
+
+ Suppose there exists an edge between u and v. Then we return a function
+ expecting u and v as arguments. For Graph and DiGraph, G[u][v] is
+ the edge attribute dictionary, and the function (essentially) returns
+ G[u][v][edge_attr]. However, we also handle cases when `edge_attr` is None
+ and when it is a function itself. For MultiGraph and MultiDiGraph, G[u][v]
+ is a dictionary of all edges between u and v. In this case, the returned
+ function sums the value of `edge_attr` for every edge between u and v.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ edge_attr : {None, str, callable}
+ Specification of how the value of the edge attribute should be obtained
+ from the edge attribute dictionary, G[u][v]. For multigraphs, G[u][v]
+ is a dictionary of all the edges between u and v. This allows for
+ special treatment of multiedges.
+
+ Returns
+ -------
+ value : function
+ A function expecting two nodes as parameters. The nodes should
+ represent the from- and to- node of an edge. The function will
+ return a value from G[u][v] that depends on `edge_attr`.
+
+ """
+
+ if edge_attr is None:
+ # topological count of edges
+
+ if G.is_multigraph():
+
+ def value(u, v):
+ return len(G[u][v])
+
+ else:
+
+ def value(u, v):
+ return 1
+
+ elif not callable(edge_attr):
+ # assume it is a key for the edge attribute dictionary
+
+ if edge_attr == "weight":
+ # provide a default value
+ if G.is_multigraph():
+
+ def value(u, v):
+ return sum(d.get(edge_attr, 1) for d in G[u][v].values())
+
+ else:
+
+ def value(u, v):
+ return G[u][v].get(edge_attr, 1)
+
+ else:
+ # otherwise, the edge attribute MUST exist for each edge
+ if G.is_multigraph():
+
+ def value(u, v):
+ return sum(d[edge_attr] for d in G[u][v].values())
+
+ else:
+
+ def value(u, v):
+ return G[u][v][edge_attr]
+
+ else:
+ # Advanced: Allow users to specify something else.
+ #
+ # Alternative default value:
+ # edge_attr = lambda u,v: G[u][v].get('thickness', .5)
+ #
+ # Function on an attribute:
+ # edge_attr = lambda u,v: abs(G[u][v]['weight'])
+ #
+ # Handle Multi(Di)Graphs differently:
+ # edge_attr = lambda u,v: numpy.prod([d['size'] for d in G[u][v].values()])
+ #
+ # Ignore multiple edges
+ # edge_attr = lambda u,v: 1 if len(G[u][v]) else 0
+ #
+ value = edge_attr
+
+ return value
+
+
+@nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr")
+def attr_matrix(
+ G,
+ edge_attr=None,
+ node_attr=None,
+ normalized=False,
+ rc_order=None,
+ dtype=None,
+ order=None,
+):
+ """Returns the attribute matrix using attributes from `G` as a numpy array.
+
+ If only `G` is passed in, then the adjacency matrix is constructed.
+
+ Let A be a discrete set of values for the node attribute `node_attr`. Then
+ the elements of A represent the rows and columns of the constructed matrix.
+ Now, iterate through every edge e=(u,v) in `G` and consider the value
+ of the edge attribute `edge_attr`. If ua and va are the values of the
+ node attribute `node_attr` for u and v, respectively, then the value of
+ the edge attribute is added to the matrix element at (ua, va).
+
+ Parameters
+ ----------
+ G : graph
+ The NetworkX graph used to construct the attribute matrix.
+
+ edge_attr : str, optional
+ Each element of the matrix represents a running total of the
+ specified edge attribute for edges whose node attributes correspond
+ to the rows/cols of the matrix. The attribute must be present for
+ all edges in the graph. If no attribute is specified, then we
+ just count the number of edges whose node attributes correspond
+ to the matrix element.
+
+ node_attr : str, optional
+ Each row and column in the matrix represents a particular value
+ of the node attribute. The attribute must be present for all nodes
+ in the graph. Note, the values of this attribute should be reliably
+ hashable. So, float values are not recommended. If no attribute is
+ specified, then the rows and columns will be the nodes of the graph.
+
+ normalized : bool, optional
+ If True, then each row is normalized by the summation of its values.
+
+ rc_order : list, optional
+ A list of the node attribute values. This list specifies the ordering
+ of rows and columns of the array. If no ordering is provided, then
+ the ordering will be random (and also, a return value).
+
+ Other Parameters
+ ----------------
+ dtype : NumPy data-type, optional
+ A valid NumPy dtype used to initialize the array. Keep in mind certain
+ dtypes can yield unexpected results if the array is to be normalized.
+ The parameter is passed to numpy.zeros(). If unspecified, the NumPy
+ default is used.
+
+ order : {'C', 'F'}, optional
+ Whether to store multidimensional data in C- or Fortran-contiguous
+ (row- or column-wise) order in memory. This parameter is passed to
+ numpy.zeros(). If unspecified, the NumPy default is used.
+
+ Returns
+ -------
+ M : 2D NumPy ndarray
+ The attribute matrix.
+
+ ordering : list
+ If `rc_order` was specified, then only the attribute matrix is returned.
+ However, if `rc_order` was None, then the ordering used to construct
+ the matrix is returned as well.
+
+ Examples
+ --------
+ Construct an adjacency matrix:
+
+ >>> G = nx.Graph()
+ >>> G.add_edge(0, 1, thickness=1, weight=3)
+ >>> G.add_edge(0, 2, thickness=2)
+ >>> G.add_edge(1, 2, thickness=3)
+ >>> nx.attr_matrix(G, rc_order=[0, 1, 2])
+ array([[0., 1., 1.],
+ [1., 0., 1.],
+ [1., 1., 0.]])
+
+ Alternatively, we can obtain the matrix describing edge thickness.
+
+ >>> nx.attr_matrix(G, edge_attr="thickness", rc_order=[0, 1, 2])
+ array([[0., 1., 2.],
+ [1., 0., 3.],
+ [2., 3., 0.]])
+
+ We can also color the nodes and ask for the probability distribution over
+ all edges (u,v) describing:
+
+ Pr(v has color Y | u has color X)
+
+ >>> G.nodes[0]["color"] = "red"
+ >>> G.nodes[1]["color"] = "red"
+ >>> G.nodes[2]["color"] = "blue"
+ >>> rc = ["red", "blue"]
+ >>> nx.attr_matrix(G, node_attr="color", normalized=True, rc_order=rc)
+ array([[0.33333333, 0.66666667],
+ [1. , 0. ]])
+
+ For example, the above tells us that for all edges (u,v):
+
+ Pr( v is red | u is red) = 1/3
+ Pr( v is blue | u is red) = 2/3
+
+ Pr( v is red | u is blue) = 1
+ Pr( v is blue | u is blue) = 0
+
+ Finally, we can obtain the total weights listed by the node colors.
+
+ >>> nx.attr_matrix(G, edge_attr="weight", node_attr="color", rc_order=rc)
+ array([[3., 2.],
+ [2., 0.]])
+
+ Thus, the total weight over all edges (u,v) with u and v having colors:
+
+ (red, red) is 3 # the sole contribution is from edge (0,1)
+ (red, blue) is 2 # contributions from edges (0,2) and (1,2)
+ (blue, red) is 2 # same as (red, blue) since graph is undirected
+ (blue, blue) is 0 # there are no edges with blue endpoints
+
+ """
+ import numpy as np
+
+ edge_value = _edge_value(G, edge_attr)
+ node_value = _node_value(G, node_attr)
+
+ if rc_order is None:
+ ordering = list({node_value(n) for n in G})
+ else:
+ ordering = rc_order
+
+ N = len(ordering)
+ undirected = not G.is_directed()
+ index = dict(zip(ordering, range(N)))
+ M = np.zeros((N, N), dtype=dtype, order=order)
+
+ seen = set()
+ for u, nbrdict in G.adjacency():
+ for v in nbrdict:
+ # Obtain the node attribute values.
+ i, j = index[node_value(u)], index[node_value(v)]
+ if v not in seen:
+ M[i, j] += edge_value(u, v)
+ if undirected:
+ M[j, i] = M[i, j]
+
+ if undirected:
+ seen.add(u)
+
+ if normalized:
+ M /= M.sum(axis=1).reshape((N, 1))
+
+ if rc_order is None:
+ return M, ordering
+ else:
+ return M
+
+
+@nx._dispatchable(edge_attrs={"edge_attr": None}, node_attrs="node_attr")
+def attr_sparse_matrix(
+ G, edge_attr=None, node_attr=None, normalized=False, rc_order=None, dtype=None
+):
+ """Returns a SciPy sparse array using attributes from G.
+
+ If only `G` is passed in, then the adjacency matrix is constructed.
+
+ Let A be a discrete set of values for the node attribute `node_attr`. Then
+ the elements of A represent the rows and columns of the constructed matrix.
+ Now, iterate through every edge e=(u,v) in `G` and consider the value
+ of the edge attribute `edge_attr`. If ua and va are the values of the
+ node attribute `node_attr` for u and v, respectively, then the value of
+ the edge attribute is added to the matrix element at (ua, va).
+
+ Parameters
+ ----------
+ G : graph
+ The NetworkX graph used to construct the NumPy matrix.
+
+ edge_attr : str, optional
+ Each element of the matrix represents a running total of the
+ specified edge attribute for edges whose node attributes correspond
+ to the rows/cols of the matrix. The attribute must be present for
+ all edges in the graph. If no attribute is specified, then we
+ just count the number of edges whose node attributes correspond
+ to the matrix element.
+
+ node_attr : str, optional
+ Each row and column in the matrix represents a particular value
+ of the node attribute. The attribute must be present for all nodes
+ in the graph. Note, the values of this attribute should be reliably
+ hashable. So, float values are not recommended. If no attribute is
+ specified, then the rows and columns will be the nodes of the graph.
+
+ normalized : bool, optional
+ If True, then each row is normalized by the summation of its values.
+
+ rc_order : list, optional
+ A list of the node attribute values. This list specifies the ordering
+ of rows and columns of the array. If no ordering is provided, then
+ the ordering will be random (and also, a return value).
+
+ Other Parameters
+ ----------------
+ dtype : NumPy data-type, optional
+ A valid NumPy dtype used to initialize the array. Keep in mind certain
+ dtypes can yield unexpected results if the array is to be normalized.
+ The parameter is passed to numpy.zeros(). If unspecified, the NumPy
+ default is used.
+
+ Returns
+ -------
+ M : SciPy sparse array
+ The attribute matrix.
+
+ ordering : list
+ If `rc_order` was specified, then only the matrix is returned.
+ However, if `rc_order` was None, then the ordering used to construct
+ the matrix is returned as well.
+
+ Examples
+ --------
+ Construct an adjacency matrix:
+
+ >>> G = nx.Graph()
+ >>> G.add_edge(0, 1, thickness=1, weight=3)
+ >>> G.add_edge(0, 2, thickness=2)
+ >>> G.add_edge(1, 2, thickness=3)
+ >>> M = nx.attr_sparse_matrix(G, rc_order=[0, 1, 2])
+ >>> M.toarray()
+ array([[0., 1., 1.],
+ [1., 0., 1.],
+ [1., 1., 0.]])
+
+ Alternatively, we can obtain the matrix describing edge thickness.
+
+ >>> M = nx.attr_sparse_matrix(G, edge_attr="thickness", rc_order=[0, 1, 2])
+ >>> M.toarray()
+ array([[0., 1., 2.],
+ [1., 0., 3.],
+ [2., 3., 0.]])
+
+ We can also color the nodes and ask for the probability distribution over
+ all edges (u,v) describing:
+
+ Pr(v has color Y | u has color X)
+
+ >>> G.nodes[0]["color"] = "red"
+ >>> G.nodes[1]["color"] = "red"
+ >>> G.nodes[2]["color"] = "blue"
+ >>> rc = ["red", "blue"]
+ >>> M = nx.attr_sparse_matrix(G, node_attr="color", normalized=True, rc_order=rc)
+ >>> M.toarray()
+ array([[0.33333333, 0.66666667],
+ [1. , 0. ]])
+
+ For example, the above tells us that for all edges (u,v):
+
+ Pr( v is red | u is red) = 1/3
+ Pr( v is blue | u is red) = 2/3
+
+ Pr( v is red | u is blue) = 1
+ Pr( v is blue | u is blue) = 0
+
+ Finally, we can obtain the total weights listed by the node colors.
+
+ >>> M = nx.attr_sparse_matrix(G, edge_attr="weight", node_attr="color", rc_order=rc)
+ >>> M.toarray()
+ array([[3., 2.],
+ [2., 0.]])
+
+ Thus, the total weight over all edges (u,v) with u and v having colors:
+
+ (red, red) is 3 # the sole contribution is from edge (0,1)
+ (red, blue) is 2 # contributions from edges (0,2) and (1,2)
+ (blue, red) is 2 # same as (red, blue) since graph is undirected
+ (blue, blue) is 0 # there are no edges with blue endpoints
+
+ """
+ import numpy as np
+ import scipy as sp
+
+ edge_value = _edge_value(G, edge_attr)
+ node_value = _node_value(G, node_attr)
+
+ if rc_order is None:
+ ordering = list({node_value(n) for n in G})
+ else:
+ ordering = rc_order
+
+ N = len(ordering)
+ undirected = not G.is_directed()
+ index = dict(zip(ordering, range(N)))
+ M = sp.sparse.lil_array((N, N), dtype=dtype)
+
+ seen = set()
+ for u, nbrdict in G.adjacency():
+ for v in nbrdict:
+ # Obtain the node attribute values.
+ i, j = index[node_value(u)], index[node_value(v)]
+ if v not in seen:
+ M[i, j] += edge_value(u, v)
+ if undirected:
+ M[j, i] = M[i, j]
+
+ if undirected:
+ seen.add(u)
+
+ if normalized:
+ M *= 1 / M.sum(axis=1)[:, np.newaxis] # in-place mult preserves sparse
+
+ if rc_order is None:
+ return M, ordering
+ else:
+ return M
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/bethehessianmatrix.py b/.venv/lib/python3.12/site-packages/networkx/linalg/bethehessianmatrix.py
new file mode 100644
index 00000000..3d42fc6d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/bethehessianmatrix.py
@@ -0,0 +1,79 @@
+"""Bethe Hessian or deformed Laplacian matrix of graphs."""
+
+import networkx as nx
+from networkx.utils import not_implemented_for
+
+__all__ = ["bethe_hessian_matrix"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable
+def bethe_hessian_matrix(G, r=None, nodelist=None):
+ r"""Returns the Bethe Hessian matrix of G.
+
+ The Bethe Hessian is a family of matrices parametrized by r, defined as
+ H(r) = (r^2 - 1) I - r A + D where A is the adjacency matrix, D is the
+ diagonal matrix of node degrees, and I is the identify matrix. It is equal
+ to the graph laplacian when the regularizer r = 1.
+
+ The default choice of regularizer should be the ratio [2]_
+
+ .. math::
+ r_m = \left(\sum k_i \right)^{-1}\left(\sum k_i^2 \right) - 1
+
+ Parameters
+ ----------
+ G : Graph
+ A NetworkX graph
+ r : float
+ Regularizer parameter
+ 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()``.
+
+ Returns
+ -------
+ H : scipy.sparse.csr_array
+ The Bethe Hessian matrix of `G`, with parameter `r`.
+
+ Examples
+ --------
+ >>> k = [3, 2, 2, 1, 0]
+ >>> G = nx.havel_hakimi_graph(k)
+ >>> H = nx.bethe_hessian_matrix(G)
+ >>> H.toarray()
+ array([[ 3.5625, -1.25 , -1.25 , -1.25 , 0. ],
+ [-1.25 , 2.5625, -1.25 , 0. , 0. ],
+ [-1.25 , -1.25 , 2.5625, 0. , 0. ],
+ [-1.25 , 0. , 0. , 1.5625, 0. ],
+ [ 0. , 0. , 0. , 0. , 0.5625]])
+
+ See Also
+ --------
+ bethe_hessian_spectrum
+ adjacency_matrix
+ laplacian_matrix
+
+ References
+ ----------
+ .. [1] A. Saade, F. Krzakala and L. Zdeborová
+ "Spectral Clustering of Graphs with the Bethe Hessian",
+ Advances in Neural Information Processing Systems, 2014.
+ .. [2] C. M. Le, E. Levina
+ "Estimating the number of communities in networks by spectral methods"
+ arXiv:1507.00827, 2015.
+ """
+ import scipy as sp
+
+ if nodelist is None:
+ nodelist = list(G)
+ if r is None:
+ r = sum(d**2 for v, d in nx.degree(G)) / sum(d for v, d in nx.degree(G)) - 1
+ A = nx.to_scipy_sparse_array(G, nodelist=nodelist, format="csr")
+ n, m = A.shape
+ # TODO: Rm csr_array wrapper when spdiags array creation becomes available
+ D = sp.sparse.csr_array(sp.sparse.spdiags(A.sum(axis=1), 0, m, n, format="csr"))
+ # TODO: Rm csr_array wrapper when eye array creation becomes available
+ I = sp.sparse.csr_array(sp.sparse.eye(m, n, format="csr"))
+ return (r**2 - 1) * I - r * A + D
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/graphmatrix.py b/.venv/lib/python3.12/site-packages/networkx/linalg/graphmatrix.py
new file mode 100644
index 00000000..9f477bcc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/graphmatrix.py
@@ -0,0 +1,168 @@
+"""
+Adjacency matrix and incidence matrix of graphs.
+"""
+
+import networkx as nx
+
+__all__ = ["incidence_matrix", "adjacency_matrix"]
+
+
+@nx._dispatchable(edge_attrs="weight")
+def incidence_matrix(
+ G, nodelist=None, edgelist=None, oriented=False, weight=None, *, dtype=None
+):
+ """Returns incidence matrix of G.
+
+ The incidence matrix assigns each row to a node and each column to an edge.
+ For a standard incidence matrix a 1 appears wherever a row's node is
+ incident on the column's edge. For an oriented incidence matrix each
+ edge is assigned an orientation (arbitrarily for undirected and aligning to
+ direction for directed). A -1 appears for the source (tail) of an edge and
+ 1 for the destination (head) of the edge. The elements are zero otherwise.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ nodelist : list, optional (default= all nodes in G)
+ The rows are ordered according to the nodes in nodelist.
+ If nodelist is None, then the ordering is produced by G.nodes().
+
+ edgelist : list, optional (default= all edges in G)
+ The columns are ordered according to the edges in edgelist.
+ If edgelist is None, then the ordering is produced by G.edges().
+
+ oriented: bool, optional (default=False)
+ If True, matrix elements are +1 or -1 for the head or tail node
+ respectively of each edge. If False, +1 occurs at both nodes.
+
+ weight : string or None, optional (default=None)
+ The edge data key used to provide each value in the matrix.
+ If None, then each edge has weight 1. Edge weights, if used,
+ should be positive so that the orientation can provide the sign.
+
+ dtype : a NumPy dtype or None (default=None)
+ The dtype of the output sparse array. This type should be a compatible
+ type of the weight argument, eg. if weight would return a float this
+ argument should also be a float.
+ If None, then the default for SciPy is used.
+
+ Returns
+ -------
+ A : SciPy sparse array
+ The incidence matrix of G.
+
+ Notes
+ -----
+ For MultiGraph/MultiDiGraph, the edges in edgelist should be
+ (u,v,key) 3-tuples.
+
+ "Networks are the best discrete model for so many problems in
+ applied mathematics" [1]_.
+
+ References
+ ----------
+ .. [1] Gil Strang, Network applications: A = incidence matrix,
+ http://videolectures.net/mit18085f07_strang_lec03/
+ """
+ import scipy as sp
+
+ if nodelist is None:
+ nodelist = list(G)
+ if edgelist is None:
+ if G.is_multigraph():
+ edgelist = list(G.edges(keys=True))
+ else:
+ edgelist = list(G.edges())
+ A = sp.sparse.lil_array((len(nodelist), len(edgelist)), dtype=dtype)
+ node_index = {node: i for i, node in enumerate(nodelist)}
+ for ei, e in enumerate(edgelist):
+ (u, v) = e[:2]
+ if u == v:
+ continue # self loops give zero column
+ try:
+ ui = node_index[u]
+ vi = node_index[v]
+ except KeyError as err:
+ raise nx.NetworkXError(
+ f"node {u} or {v} in edgelist but not in nodelist"
+ ) from err
+ if weight is None:
+ wt = 1
+ else:
+ if G.is_multigraph():
+ ekey = e[2]
+ wt = G[u][v][ekey].get(weight, 1)
+ else:
+ wt = G[u][v].get(weight, 1)
+ if oriented:
+ A[ui, ei] = -wt
+ A[vi, ei] = wt
+ else:
+ A[ui, ei] = wt
+ A[vi, ei] = wt
+ return A.asformat("csc")
+
+
+@nx._dispatchable(edge_attrs="weight")
+def adjacency_matrix(G, nodelist=None, dtype=None, weight="weight"):
+ """Returns adjacency matrix of `G`.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ nodelist : list, optional
+ The rows and columns are ordered according to the nodes in `nodelist`.
+ If ``nodelist=None`` (the default), then the ordering is produced by
+ ``G.nodes()``.
+
+ dtype : NumPy data-type, optional
+ The desired data-type for the array.
+ If `None`, then the NumPy default is used.
+
+ weight : string or None, optional (default='weight')
+ The edge data key used to provide each value in the matrix.
+ If None, then each edge has weight 1.
+
+ Returns
+ -------
+ A : SciPy sparse array
+ Adjacency matrix representation of G.
+
+ Notes
+ -----
+ For directed graphs, entry ``i, j`` corresponds to an edge from ``i`` to ``j``.
+
+ If you want a pure Python adjacency matrix representation try
+ :func:`~networkx.convert.to_dict_of_dicts` which will return a
+ dictionary-of-dictionaries format that can be addressed as a
+ sparse matrix.
+
+ For multigraphs with parallel edges the weights are summed.
+ See :func:`networkx.convert_matrix.to_numpy_array` for other options.
+
+ The convention used for self-loop edges in graphs is to assign the
+ diagonal matrix entry value to the edge weight attribute
+ (or the number 1 if the edge has no weight attribute). If the
+ alternate convention of doubling the edge weight is desired the
+ resulting SciPy sparse array can be modified as follows::
+
+ >>> G = nx.Graph([(1, 1)])
+ >>> A = nx.adjacency_matrix(G)
+ >>> A.toarray()
+ array([[1]])
+ >>> A.setdiag(A.diagonal() * 2)
+ >>> A.toarray()
+ array([[2]])
+
+ See Also
+ --------
+ to_numpy_array
+ to_scipy_sparse_array
+ to_dict_of_dicts
+ adjacency_spectrum
+ """
+ return nx.to_scipy_sparse_array(G, nodelist=nodelist, dtype=dtype, weight=weight)
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/laplacianmatrix.py b/.venv/lib/python3.12/site-packages/networkx/linalg/laplacianmatrix.py
new file mode 100644
index 00000000..d49ae491
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/laplacianmatrix.py
@@ -0,0 +1,617 @@
+"""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",
+ "total_spanning_tree_weight",
+ "directed_laplacian_matrix",
+ "directed_combinatorial_laplacian_matrix",
+]
+
+
+@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
+ # 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
+
+
+@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)
+ # TODO: rm csr_array wrapper when spdiags can produce arrays
+ D = sp.sparse.csr_array(sp.sparse.spdiags(diags, 0, n, n, format="csr"))
+ L = D - A
+ with np.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, n, n, format="csr"))
+ return DH @ (L @ DH)
+
+
+@nx._dispatchable(edge_attrs="weight")
+def total_spanning_tree_weight(G, weight=None, root=None):
+ """
+ Returns the total weight of all spanning trees of `G`.
+
+ Kirchoff's Tree Matrix Theorem [1]_, [2]_ 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.
+
+ For unweighted graphs, the total weight equals the number of spanning trees in `G`.
+
+ For directed graphs, the total weight follows by summing over all directed
+ spanning trees in `G` that start in the `root` node [3]_.
+
+ .. deprecated:: 3.3
+
+ ``total_spanning_tree_weight`` is deprecated and will be removed in v3.5.
+ Use ``nx.number_of_spanning_trees(G)`` instead.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ weight : string or None, optional (default=None)
+ The key for the edge attribute holding the edge weight.
+ If None, then each edge has weight 1.
+
+ root : node (only required for directed graphs)
+ A node in the directed graph `G`.
+
+ Returns
+ -------
+ total_weight : float
+ Undirected graphs:
+ The sum of the total multiplicative weights for all spanning trees in `G`.
+ Directed graphs:
+ The sum of the total multiplicative weights for all spanning trees of `G`,
+ rooted at node `root`.
+
+ Raises
+ ------
+ NetworkXPointlessConcept
+ If `G` does not contain any nodes.
+
+ NetworkXError
+ If the graph `G` is not (weakly) connected,
+ or if `G` is directed and the root node is not specified or not in G.
+
+ Examples
+ --------
+ >>> G = nx.complete_graph(5)
+ >>> round(nx.total_spanning_tree_weight(G))
+ 125
+
+ >>> G = nx.Graph()
+ >>> G.add_edge(1, 2, weight=2)
+ >>> G.add_edge(1, 3, weight=1)
+ >>> G.add_edge(2, 3, weight=1)
+ >>> round(nx.total_spanning_tree_weight(G, "weight"))
+ 5
+
+ Notes
+ -----
+ Self-loops are excluded. Multi-edges are contracted in one edge
+ equal to the sum of the weights.
+
+ References
+ ----------
+ .. [1] Wikipedia
+ "Kirchhoff's theorem."
+ https://en.wikipedia.org/wiki/Kirchhoff%27s_theorem
+ .. [2] Kirchhoff, G. R.
+ Über die Auflösung der Gleichungen, auf welche man
+ bei der Untersuchung der linearen Vertheilung
+ Galvanischer Ströme geführt wird
+ Annalen der Physik und Chemie, vol. 72, pp. 497-508, 1847.
+ .. [3] Margoliash, J.
+ "Matrix-Tree Theorem for Directed Graphs"
+ https://www.math.uchicago.edu/~may/VIGRE/VIGRE2010/REUPapers/Margoliash.pdf
+ """
+ import warnings
+
+ warnings.warn(
+ (
+ "\n\ntotal_spanning_tree_weight is deprecated and will be removed in v3.5.\n"
+ "Use `nx.number_of_spanning_trees(G)` instead."
+ ),
+ category=DeprecationWarning,
+ stacklevel=3,
+ )
+
+ return nx.number_of_spanning_trees(G, weight=weight, root=root)
+
+
+###############################################################################
+# Code based on work from https://github.com/bjedwards
+
+
+@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 = (
+ # 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")
+@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
+ # 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)
+ 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"]:
+ # 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
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/modularitymatrix.py b/.venv/lib/python3.12/site-packages/networkx/linalg/modularitymatrix.py
new file mode 100644
index 00000000..0287910b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/modularitymatrix.py
@@ -0,0 +1,166 @@
+"""Modularity matrix of graphs."""
+
+import networkx as nx
+from networkx.utils import not_implemented_for
+
+__all__ = ["modularity_matrix", "directed_modularity_matrix"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable(edge_attrs="weight")
+def modularity_matrix(G, nodelist=None, weight=None):
+ r"""Returns the modularity matrix of G.
+
+ The modularity matrix is the matrix B = A - <A>, where A is the adjacency
+ matrix and <A> is the average adjacency matrix, assuming that the graph
+ is described by the configuration model.
+
+ More specifically, the element B_ij of B is defined as
+
+ .. math::
+ A_{ij} - {k_i k_j \over 2 m}
+
+ where k_i is the degree of node i, and where m is the number of edges
+ in the graph. When weight is set to a name of an attribute edge, Aij, k_i,
+ k_j and m are computed using its value.
+
+ 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=None)
+ The edge attribute that holds the numerical value used for
+ the edge weight. If None then all edge weights are 1.
+
+ Returns
+ -------
+ B : Numpy array
+ The modularity matrix of G.
+
+ Examples
+ --------
+ >>> k = [3, 2, 2, 1, 0]
+ >>> G = nx.havel_hakimi_graph(k)
+ >>> B = nx.modularity_matrix(G)
+
+
+ See Also
+ --------
+ to_numpy_array
+ modularity_spectrum
+ adjacency_matrix
+ directed_modularity_matrix
+
+ References
+ ----------
+ .. [1] M. E. J. Newman, "Modularity and community structure in networks",
+ Proc. Natl. Acad. Sci. USA, vol. 103, pp. 8577-8582, 2006.
+ """
+ import numpy as np
+
+ if nodelist is None:
+ nodelist = list(G)
+ A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr")
+ k = A.sum(axis=1)
+ m = k.sum() * 0.5
+ # Expected adjacency matrix
+ X = np.outer(k, k) / (2 * m)
+
+ return A - X
+
+
+@not_implemented_for("undirected")
+@not_implemented_for("multigraph")
+@nx._dispatchable(edge_attrs="weight")
+def directed_modularity_matrix(G, nodelist=None, weight=None):
+ """Returns the directed modularity matrix of G.
+
+ The modularity matrix is the matrix B = A - <A>, where A is the adjacency
+ matrix and <A> is the expected adjacency matrix, assuming that the graph
+ is described by the configuration model.
+
+ More specifically, the element B_ij of B is defined as
+
+ .. math::
+ B_{ij} = A_{ij} - k_i^{out} k_j^{in} / m
+
+ where :math:`k_i^{in}` is the in degree of node i, and :math:`k_j^{out}` is the out degree
+ of node j, with m the number of edges in the graph. When weight is set
+ to a name of an attribute edge, Aij, k_i, k_j and m are computed using
+ its value.
+
+ Parameters
+ ----------
+ G : DiGraph
+ A NetworkX DiGraph
+
+ 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=None)
+ The edge attribute that holds the numerical value used for
+ the edge weight. If None then all edge weights are 1.
+
+ Returns
+ -------
+ B : Numpy array
+ The modularity matrix of G.
+
+ Examples
+ --------
+ >>> G = nx.DiGraph()
+ >>> G.add_edges_from(
+ ... (
+ ... (1, 2),
+ ... (1, 3),
+ ... (3, 1),
+ ... (3, 2),
+ ... (3, 5),
+ ... (4, 5),
+ ... (4, 6),
+ ... (5, 4),
+ ... (5, 6),
+ ... (6, 4),
+ ... )
+ ... )
+ >>> B = nx.directed_modularity_matrix(G)
+
+
+ Notes
+ -----
+ NetworkX defines the element A_ij of the adjacency matrix as 1 if there
+ is a link going from node i to node j. Leicht and Newman use the opposite
+ definition. This explains the different expression for B_ij.
+
+ See Also
+ --------
+ to_numpy_array
+ modularity_spectrum
+ adjacency_matrix
+ modularity_matrix
+
+ References
+ ----------
+ .. [1] E. A. Leicht, M. E. J. Newman,
+ "Community structure in directed networks",
+ Phys. Rev Lett., vol. 100, no. 11, p. 118703, 2008.
+ """
+ import numpy as np
+
+ if nodelist is None:
+ nodelist = list(G)
+ A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr")
+ k_in = A.sum(axis=0)
+ k_out = A.sum(axis=1)
+ m = k_in.sum()
+ # Expected adjacency matrix
+ X = np.outer(k_out, k_in) / m
+
+ return A - X
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/spectrum.py b/.venv/lib/python3.12/site-packages/networkx/linalg/spectrum.py
new file mode 100644
index 00000000..079b1855
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/spectrum.py
@@ -0,0 +1,186 @@
+"""
+Eigenvalue spectrum of graphs.
+"""
+
+import networkx as nx
+
+__all__ = [
+ "laplacian_spectrum",
+ "adjacency_spectrum",
+ "modularity_spectrum",
+ "normalized_laplacian_spectrum",
+ "bethe_hessian_spectrum",
+]
+
+
+@nx._dispatchable(edge_attrs="weight")
+def laplacian_spectrum(G, weight="weight"):
+ """Returns eigenvalues of the Laplacian of G
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ 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
+ -------
+ evals : NumPy array
+ Eigenvalues
+
+ Notes
+ -----
+ For MultiGraph/MultiDiGraph, the edges weights are summed.
+ See :func:`~networkx.convert_matrix.to_numpy_array` for other options.
+
+ See Also
+ --------
+ laplacian_matrix
+
+ Examples
+ --------
+ The multiplicity of 0 as an eigenvalue of the laplacian matrix is equal
+ to the number of connected components of G.
+
+ >>> G = nx.Graph() # Create a graph with 5 nodes and 3 connected components
+ >>> G.add_nodes_from(range(5))
+ >>> G.add_edges_from([(0, 2), (3, 4)])
+ >>> nx.laplacian_spectrum(G)
+ array([0., 0., 0., 2., 2.])
+
+ """
+ import scipy as sp
+
+ return sp.linalg.eigvalsh(nx.laplacian_matrix(G, weight=weight).todense())
+
+
+@nx._dispatchable(edge_attrs="weight")
+def normalized_laplacian_spectrum(G, weight="weight"):
+ """Return eigenvalues of the normalized Laplacian of G
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ 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
+ -------
+ evals : NumPy array
+ Eigenvalues
+
+ Notes
+ -----
+ For MultiGraph/MultiDiGraph, the edges weights are summed.
+ See to_numpy_array for other options.
+
+ See Also
+ --------
+ normalized_laplacian_matrix
+ """
+ import scipy as sp
+
+ return sp.linalg.eigvalsh(
+ nx.normalized_laplacian_matrix(G, weight=weight).todense()
+ )
+
+
+@nx._dispatchable(edge_attrs="weight")
+def adjacency_spectrum(G, weight="weight"):
+ """Returns eigenvalues of the adjacency matrix of G.
+
+ Parameters
+ ----------
+ G : graph
+ A NetworkX graph
+
+ 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
+ -------
+ evals : NumPy array
+ Eigenvalues
+
+ Notes
+ -----
+ For MultiGraph/MultiDiGraph, the edges weights are summed.
+ See to_numpy_array for other options.
+
+ See Also
+ --------
+ adjacency_matrix
+ """
+ import scipy as sp
+
+ return sp.linalg.eigvals(nx.adjacency_matrix(G, weight=weight).todense())
+
+
+@nx._dispatchable
+def modularity_spectrum(G):
+ """Returns eigenvalues of the modularity matrix of G.
+
+ Parameters
+ ----------
+ G : Graph
+ A NetworkX Graph or DiGraph
+
+ Returns
+ -------
+ evals : NumPy array
+ Eigenvalues
+
+ See Also
+ --------
+ modularity_matrix
+
+ References
+ ----------
+ .. [1] M. E. J. Newman, "Modularity and community structure in networks",
+ Proc. Natl. Acad. Sci. USA, vol. 103, pp. 8577-8582, 2006.
+ """
+ import scipy as sp
+
+ if G.is_directed():
+ return sp.linalg.eigvals(nx.directed_modularity_matrix(G))
+ else:
+ return sp.linalg.eigvals(nx.modularity_matrix(G))
+
+
+@nx._dispatchable
+def bethe_hessian_spectrum(G, r=None):
+ """Returns eigenvalues of the Bethe Hessian matrix of G.
+
+ Parameters
+ ----------
+ G : Graph
+ A NetworkX Graph or DiGraph
+
+ r : float
+ Regularizer parameter
+
+ Returns
+ -------
+ evals : NumPy array
+ Eigenvalues
+
+ See Also
+ --------
+ bethe_hessian_matrix
+
+ References
+ ----------
+ .. [1] A. Saade, F. Krzakala and L. Zdeborová
+ "Spectral clustering of graphs with the bethe hessian",
+ Advances in Neural Information Processing Systems. 2014.
+ """
+ import scipy as sp
+
+ return sp.linalg.eigvalsh(nx.bethe_hessian_matrix(G, r).todense())
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_algebraic_connectivity.py b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_algebraic_connectivity.py
new file mode 100644
index 00000000..089d917a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_algebraic_connectivity.py
@@ -0,0 +1,402 @@
+from math import sqrt
+
+import pytest
+
+np = pytest.importorskip("numpy")
+
+
+import networkx as nx
+
+methods = ("tracemin_pcg", "tracemin_lu", "lanczos", "lobpcg")
+
+
+def test_algebraic_connectivity_tracemin_chol():
+ """Test that "tracemin_chol" raises an exception."""
+ pytest.importorskip("scipy")
+ G = nx.barbell_graph(5, 4)
+ with pytest.raises(nx.NetworkXError):
+ nx.algebraic_connectivity(G, method="tracemin_chol")
+
+
+def test_fiedler_vector_tracemin_chol():
+ """Test that "tracemin_chol" raises an exception."""
+ pytest.importorskip("scipy")
+ G = nx.barbell_graph(5, 4)
+ with pytest.raises(nx.NetworkXError):
+ nx.fiedler_vector(G, method="tracemin_chol")
+
+
+def test_spectral_ordering_tracemin_chol():
+ """Test that "tracemin_chol" raises an exception."""
+ pytest.importorskip("scipy")
+ G = nx.barbell_graph(5, 4)
+ with pytest.raises(nx.NetworkXError):
+ nx.spectral_ordering(G, method="tracemin_chol")
+
+
+def test_fiedler_vector_tracemin_unknown():
+ """Test that "tracemin_unknown" raises an exception."""
+ pytest.importorskip("scipy")
+ G = nx.barbell_graph(5, 4)
+ L = nx.laplacian_matrix(G)
+ X = np.asarray(np.random.normal(size=(1, L.shape[0]))).T
+ with pytest.raises(nx.NetworkXError, match="Unknown linear system solver"):
+ nx.linalg.algebraicconnectivity._tracemin_fiedler(
+ L, X, normalized=False, tol=1e-8, method="tracemin_unknown"
+ )
+
+
+def test_spectral_bisection():
+ pytest.importorskip("scipy")
+ G = nx.barbell_graph(3, 0)
+ C = nx.spectral_bisection(G)
+ assert C == ({0, 1, 2}, {3, 4, 5})
+
+ mapping = dict(enumerate("badfec"))
+ G = nx.relabel_nodes(G, mapping)
+ C = nx.spectral_bisection(G)
+ assert C == (
+ {mapping[0], mapping[1], mapping[2]},
+ {mapping[3], mapping[4], mapping[5]},
+ )
+
+
+def check_eigenvector(A, l, x):
+ nx = np.linalg.norm(x)
+ # Check zeroness.
+ assert nx != pytest.approx(0, abs=1e-07)
+ y = A @ x
+ ny = np.linalg.norm(y)
+ # Check collinearity.
+ assert x @ y == pytest.approx(nx * ny, abs=1e-7)
+ # Check eigenvalue.
+ assert ny == pytest.approx(l * nx, abs=1e-7)
+
+
+class TestAlgebraicConnectivity:
+ @pytest.mark.parametrize("method", methods)
+ def test_directed(self, method):
+ G = nx.DiGraph()
+ pytest.raises(
+ nx.NetworkXNotImplemented, nx.algebraic_connectivity, G, method=method
+ )
+ pytest.raises(nx.NetworkXNotImplemented, nx.fiedler_vector, G, method=method)
+
+ @pytest.mark.parametrize("method", methods)
+ def test_null_and_singleton(self, method):
+ G = nx.Graph()
+ pytest.raises(nx.NetworkXError, nx.algebraic_connectivity, G, method=method)
+ pytest.raises(nx.NetworkXError, nx.fiedler_vector, G, method=method)
+ G.add_edge(0, 0)
+ pytest.raises(nx.NetworkXError, nx.algebraic_connectivity, G, method=method)
+ pytest.raises(nx.NetworkXError, nx.fiedler_vector, G, method=method)
+
+ @pytest.mark.parametrize("method", methods)
+ def test_disconnected(self, method):
+ G = nx.Graph()
+ G.add_nodes_from(range(2))
+ assert nx.algebraic_connectivity(G) == 0
+ pytest.raises(nx.NetworkXError, nx.fiedler_vector, G, method=method)
+ G.add_edge(0, 1, weight=0)
+ assert nx.algebraic_connectivity(G) == 0
+ pytest.raises(nx.NetworkXError, nx.fiedler_vector, G, method=method)
+
+ def test_unrecognized_method(self):
+ pytest.importorskip("scipy")
+ G = nx.path_graph(4)
+ pytest.raises(nx.NetworkXError, nx.algebraic_connectivity, G, method="unknown")
+ pytest.raises(nx.NetworkXError, nx.fiedler_vector, G, method="unknown")
+
+ @pytest.mark.parametrize("method", methods)
+ def test_two_nodes(self, method):
+ pytest.importorskip("scipy")
+ G = nx.Graph()
+ G.add_edge(0, 1, weight=1)
+ A = nx.laplacian_matrix(G)
+ assert nx.algebraic_connectivity(G, tol=1e-12, method=method) == pytest.approx(
+ 2, abs=1e-7
+ )
+ x = nx.fiedler_vector(G, tol=1e-12, method=method)
+ check_eigenvector(A, 2, x)
+
+ @pytest.mark.parametrize("method", methods)
+ def test_two_nodes_multigraph(self, method):
+ pytest.importorskip("scipy")
+ G = nx.MultiGraph()
+ G.add_edge(0, 0, spam=1e8)
+ G.add_edge(0, 1, spam=1)
+ G.add_edge(0, 1, spam=-2)
+ A = -3 * nx.laplacian_matrix(G, weight="spam")
+ assert nx.algebraic_connectivity(
+ G, weight="spam", tol=1e-12, method=method
+ ) == pytest.approx(6, abs=1e-7)
+ x = nx.fiedler_vector(G, weight="spam", tol=1e-12, method=method)
+ check_eigenvector(A, 6, x)
+
+ def test_abbreviation_of_method(self):
+ pytest.importorskip("scipy")
+ G = nx.path_graph(8)
+ A = nx.laplacian_matrix(G)
+ sigma = 2 - sqrt(2 + sqrt(2))
+ ac = nx.algebraic_connectivity(G, tol=1e-12, method="tracemin")
+ assert ac == pytest.approx(sigma, abs=1e-7)
+ x = nx.fiedler_vector(G, tol=1e-12, method="tracemin")
+ check_eigenvector(A, sigma, x)
+
+ @pytest.mark.parametrize("method", methods)
+ def test_path(self, method):
+ pytest.importorskip("scipy")
+ G = nx.path_graph(8)
+ A = nx.laplacian_matrix(G)
+ sigma = 2 - sqrt(2 + sqrt(2))
+ ac = nx.algebraic_connectivity(G, tol=1e-12, method=method)
+ assert ac == pytest.approx(sigma, abs=1e-7)
+ x = nx.fiedler_vector(G, tol=1e-12, method=method)
+ check_eigenvector(A, sigma, x)
+
+ @pytest.mark.parametrize("method", methods)
+ def test_problematic_graph_issue_2381(self, method):
+ pytest.importorskip("scipy")
+ G = nx.path_graph(4)
+ G.add_edges_from([(4, 2), (5, 1)])
+ A = nx.laplacian_matrix(G)
+ sigma = 0.438447187191
+ ac = nx.algebraic_connectivity(G, tol=1e-12, method=method)
+ assert ac == pytest.approx(sigma, abs=1e-7)
+ x = nx.fiedler_vector(G, tol=1e-12, method=method)
+ check_eigenvector(A, sigma, x)
+
+ @pytest.mark.parametrize("method", methods)
+ def test_cycle(self, method):
+ pytest.importorskip("scipy")
+ G = nx.cycle_graph(8)
+ A = nx.laplacian_matrix(G)
+ sigma = 2 - sqrt(2)
+ ac = nx.algebraic_connectivity(G, tol=1e-12, method=method)
+ assert ac == pytest.approx(sigma, abs=1e-7)
+ x = nx.fiedler_vector(G, tol=1e-12, method=method)
+ check_eigenvector(A, sigma, x)
+
+ @pytest.mark.parametrize("method", methods)
+ def test_seed_argument(self, method):
+ pytest.importorskip("scipy")
+ G = nx.cycle_graph(8)
+ A = nx.laplacian_matrix(G)
+ sigma = 2 - sqrt(2)
+ ac = nx.algebraic_connectivity(G, tol=1e-12, method=method, seed=1)
+ assert ac == pytest.approx(sigma, abs=1e-7)
+ x = nx.fiedler_vector(G, tol=1e-12, method=method, seed=1)
+ check_eigenvector(A, sigma, x)
+
+ @pytest.mark.parametrize(
+ ("normalized", "sigma", "laplacian_fn"),
+ (
+ (False, 0.2434017461399311, nx.laplacian_matrix),
+ (True, 0.08113391537997749, nx.normalized_laplacian_matrix),
+ ),
+ )
+ @pytest.mark.parametrize("method", methods)
+ def test_buckminsterfullerene(self, normalized, sigma, laplacian_fn, method):
+ pytest.importorskip("scipy")
+ G = nx.Graph(
+ [
+ (1, 10),
+ (1, 41),
+ (1, 59),
+ (2, 12),
+ (2, 42),
+ (2, 60),
+ (3, 6),
+ (3, 43),
+ (3, 57),
+ (4, 8),
+ (4, 44),
+ (4, 58),
+ (5, 13),
+ (5, 56),
+ (5, 57),
+ (6, 10),
+ (6, 31),
+ (7, 14),
+ (7, 56),
+ (7, 58),
+ (8, 12),
+ (8, 32),
+ (9, 23),
+ (9, 53),
+ (9, 59),
+ (10, 15),
+ (11, 24),
+ (11, 53),
+ (11, 60),
+ (12, 16),
+ (13, 14),
+ (13, 25),
+ (14, 26),
+ (15, 27),
+ (15, 49),
+ (16, 28),
+ (16, 50),
+ (17, 18),
+ (17, 19),
+ (17, 54),
+ (18, 20),
+ (18, 55),
+ (19, 23),
+ (19, 41),
+ (20, 24),
+ (20, 42),
+ (21, 31),
+ (21, 33),
+ (21, 57),
+ (22, 32),
+ (22, 34),
+ (22, 58),
+ (23, 24),
+ (25, 35),
+ (25, 43),
+ (26, 36),
+ (26, 44),
+ (27, 51),
+ (27, 59),
+ (28, 52),
+ (28, 60),
+ (29, 33),
+ (29, 34),
+ (29, 56),
+ (30, 51),
+ (30, 52),
+ (30, 53),
+ (31, 47),
+ (32, 48),
+ (33, 45),
+ (34, 46),
+ (35, 36),
+ (35, 37),
+ (36, 38),
+ (37, 39),
+ (37, 49),
+ (38, 40),
+ (38, 50),
+ (39, 40),
+ (39, 51),
+ (40, 52),
+ (41, 47),
+ (42, 48),
+ (43, 49),
+ (44, 50),
+ (45, 46),
+ (45, 54),
+ (46, 55),
+ (47, 54),
+ (48, 55),
+ ]
+ )
+ A = laplacian_fn(G)
+ try:
+ assert nx.algebraic_connectivity(
+ G, normalized=normalized, tol=1e-12, method=method
+ ) == pytest.approx(sigma, abs=1e-7)
+ x = nx.fiedler_vector(G, normalized=normalized, tol=1e-12, method=method)
+ check_eigenvector(A, sigma, x)
+ except nx.NetworkXError as err:
+ if err.args not in (
+ ("Cholesky solver unavailable.",),
+ ("LU solver unavailable.",),
+ ):
+ raise
+
+
+class TestSpectralOrdering:
+ _graphs = (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)
+
+ @pytest.mark.parametrize("graph", _graphs)
+ def test_nullgraph(self, graph):
+ G = graph()
+ pytest.raises(nx.NetworkXError, nx.spectral_ordering, G)
+
+ @pytest.mark.parametrize("graph", _graphs)
+ def test_singleton(self, graph):
+ G = graph()
+ G.add_node("x")
+ assert nx.spectral_ordering(G) == ["x"]
+ G.add_edge("x", "x", weight=33)
+ G.add_edge("x", "x", weight=33)
+ assert nx.spectral_ordering(G) == ["x"]
+
+ def test_unrecognized_method(self):
+ G = nx.path_graph(4)
+ pytest.raises(nx.NetworkXError, nx.spectral_ordering, G, method="unknown")
+
+ @pytest.mark.parametrize("method", methods)
+ def test_three_nodes(self, method):
+ pytest.importorskip("scipy")
+ G = nx.Graph()
+ G.add_weighted_edges_from([(1, 2, 1), (1, 3, 2), (2, 3, 1)], weight="spam")
+ order = nx.spectral_ordering(G, weight="spam", method=method)
+ assert set(order) == set(G)
+ assert {1, 3} in (set(order[:-1]), set(order[1:]))
+
+ @pytest.mark.parametrize("method", methods)
+ def test_three_nodes_multigraph(self, method):
+ pytest.importorskip("scipy")
+ G = nx.MultiDiGraph()
+ G.add_weighted_edges_from([(1, 2, 1), (1, 3, 2), (2, 3, 1), (2, 3, 2)])
+ order = nx.spectral_ordering(G, method=method)
+ assert set(order) == set(G)
+ assert {2, 3} in (set(order[:-1]), set(order[1:]))
+
+ @pytest.mark.parametrize("method", methods)
+ def test_path(self, method):
+ pytest.importorskip("scipy")
+ path = list(range(10))
+ np.random.shuffle(path)
+ G = nx.Graph()
+ nx.add_path(G, path)
+ order = nx.spectral_ordering(G, method=method)
+ assert order in [path, list(reversed(path))]
+
+ @pytest.mark.parametrize("method", methods)
+ def test_seed_argument(self, method):
+ pytest.importorskip("scipy")
+ path = list(range(10))
+ np.random.shuffle(path)
+ G = nx.Graph()
+ nx.add_path(G, path)
+ order = nx.spectral_ordering(G, method=method, seed=1)
+ assert order in [path, list(reversed(path))]
+
+ @pytest.mark.parametrize("method", methods)
+ def test_disconnected(self, method):
+ pytest.importorskip("scipy")
+ G = nx.Graph()
+ nx.add_path(G, range(0, 10, 2))
+ nx.add_path(G, range(1, 10, 2))
+ order = nx.spectral_ordering(G, method=method)
+ assert set(order) == set(G)
+ seqs = [
+ list(range(0, 10, 2)),
+ list(range(8, -1, -2)),
+ list(range(1, 10, 2)),
+ list(range(9, -1, -2)),
+ ]
+ assert order[:5] in seqs
+ assert order[5:] in seqs
+
+ @pytest.mark.parametrize(
+ ("normalized", "expected_order"),
+ (
+ (False, [[1, 2, 0, 3, 4, 5, 6, 9, 7, 8], [8, 7, 9, 6, 5, 4, 3, 0, 2, 1]]),
+ (True, [[1, 2, 3, 0, 4, 5, 9, 6, 7, 8], [8, 7, 6, 9, 5, 4, 0, 3, 2, 1]]),
+ ),
+ )
+ @pytest.mark.parametrize("method", methods)
+ def test_cycle(self, normalized, expected_order, method):
+ pytest.importorskip("scipy")
+ path = list(range(10))
+ G = nx.Graph()
+ nx.add_path(G, path, weight=5)
+ G.add_edge(path[-1], path[0], weight=1)
+ A = nx.laplacian_matrix(G).todense()
+ order = nx.spectral_ordering(G, normalized=normalized, method=method)
+ assert order in expected_order
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_attrmatrix.py b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_attrmatrix.py
new file mode 100644
index 00000000..01574bb3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_attrmatrix.py
@@ -0,0 +1,108 @@
+import pytest
+
+np = pytest.importorskip("numpy")
+
+import networkx as nx
+
+
+def test_attr_matrix():
+ G = nx.Graph()
+ G.add_edge(0, 1, thickness=1, weight=3)
+ G.add_edge(0, 1, thickness=1, weight=3)
+ G.add_edge(0, 2, thickness=2)
+ G.add_edge(1, 2, thickness=3)
+
+ def node_attr(u):
+ return G.nodes[u].get("size", 0.5) * 3
+
+ def edge_attr(u, v):
+ return G[u][v].get("thickness", 0.5)
+
+ M = nx.attr_matrix(G, edge_attr=edge_attr, node_attr=node_attr)
+ np.testing.assert_equal(M[0], np.array([[6.0]]))
+ assert M[1] == [1.5]
+
+
+def test_attr_matrix_directed():
+ G = nx.DiGraph()
+ G.add_edge(0, 1, thickness=1, weight=3)
+ G.add_edge(0, 1, thickness=1, weight=3)
+ G.add_edge(0, 2, thickness=2)
+ G.add_edge(1, 2, thickness=3)
+ M = nx.attr_matrix(G, rc_order=[0, 1, 2])
+ # fmt: off
+ data = np.array(
+ [[0., 1., 1.],
+ [0., 0., 1.],
+ [0., 0., 0.]]
+ )
+ # fmt: on
+ np.testing.assert_equal(M, np.array(data))
+
+
+def test_attr_matrix_multigraph():
+ G = nx.MultiGraph()
+ G.add_edge(0, 1, thickness=1, weight=3)
+ G.add_edge(0, 1, thickness=1, weight=3)
+ G.add_edge(0, 1, thickness=1, weight=3)
+ G.add_edge(0, 2, thickness=2)
+ G.add_edge(1, 2, thickness=3)
+ M = nx.attr_matrix(G, rc_order=[0, 1, 2])
+ # fmt: off
+ data = np.array(
+ [[0., 3., 1.],
+ [3., 0., 1.],
+ [1., 1., 0.]]
+ )
+ # fmt: on
+ np.testing.assert_equal(M, np.array(data))
+ M = nx.attr_matrix(G, edge_attr="weight", rc_order=[0, 1, 2])
+ # fmt: off
+ data = np.array(
+ [[0., 9., 1.],
+ [9., 0., 1.],
+ [1., 1., 0.]]
+ )
+ # fmt: on
+ np.testing.assert_equal(M, np.array(data))
+ M = nx.attr_matrix(G, edge_attr="thickness", rc_order=[0, 1, 2])
+ # fmt: off
+ data = np.array(
+ [[0., 3., 2.],
+ [3., 0., 3.],
+ [2., 3., 0.]]
+ )
+ # fmt: on
+ np.testing.assert_equal(M, np.array(data))
+
+
+def test_attr_sparse_matrix():
+ pytest.importorskip("scipy")
+ G = nx.Graph()
+ G.add_edge(0, 1, thickness=1, weight=3)
+ G.add_edge(0, 2, thickness=2)
+ G.add_edge(1, 2, thickness=3)
+ M = nx.attr_sparse_matrix(G)
+ mtx = M[0]
+ data = np.ones((3, 3), float)
+ np.fill_diagonal(data, 0)
+ np.testing.assert_equal(mtx.todense(), np.array(data))
+ assert M[1] == [0, 1, 2]
+
+
+def test_attr_sparse_matrix_directed():
+ pytest.importorskip("scipy")
+ G = nx.DiGraph()
+ G.add_edge(0, 1, thickness=1, weight=3)
+ G.add_edge(0, 1, thickness=1, weight=3)
+ G.add_edge(0, 2, thickness=2)
+ G.add_edge(1, 2, thickness=3)
+ M = nx.attr_sparse_matrix(G, rc_order=[0, 1, 2])
+ # fmt: off
+ data = np.array(
+ [[0., 1., 1.],
+ [0., 0., 1.],
+ [0., 0., 0.]]
+ )
+ # fmt: on
+ np.testing.assert_equal(M.todense(), np.array(data))
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_bethehessian.py b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_bethehessian.py
new file mode 100644
index 00000000..339fe1be
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_bethehessian.py
@@ -0,0 +1,41 @@
+import pytest
+
+np = pytest.importorskip("numpy")
+pytest.importorskip("scipy")
+
+import networkx as nx
+from networkx.generators.degree_seq import havel_hakimi_graph
+
+
+class TestBetheHessian:
+ @classmethod
+ def setup_class(cls):
+ deg = [3, 2, 2, 1, 0]
+ cls.G = havel_hakimi_graph(deg)
+ cls.P = nx.path_graph(3)
+
+ def test_bethe_hessian(self):
+ "Bethe Hessian matrix"
+ # fmt: off
+ H = np.array([[4, -2, 0],
+ [-2, 5, -2],
+ [0, -2, 4]])
+ # fmt: on
+ permutation = [2, 0, 1]
+ # Bethe Hessian gives expected form
+ np.testing.assert_equal(nx.bethe_hessian_matrix(self.P, r=2).todense(), H)
+ # nodelist is correctly implemented
+ np.testing.assert_equal(
+ nx.bethe_hessian_matrix(self.P, r=2, nodelist=permutation).todense(),
+ H[np.ix_(permutation, permutation)],
+ )
+ # Equal to Laplacian matrix when r=1
+ np.testing.assert_equal(
+ nx.bethe_hessian_matrix(self.G, r=1).todense(),
+ nx.laplacian_matrix(self.G).todense(),
+ )
+ # Correct default for the regularizer r
+ np.testing.assert_equal(
+ nx.bethe_hessian_matrix(self.G).todense(),
+ nx.bethe_hessian_matrix(self.G, r=1.25).todense(),
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_graphmatrix.py b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_graphmatrix.py
new file mode 100644
index 00000000..519198bc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_graphmatrix.py
@@ -0,0 +1,276 @@
+import pytest
+
+np = pytest.importorskip("numpy")
+pytest.importorskip("scipy")
+
+import networkx as nx
+from networkx.exception import NetworkXError
+from networkx.generators.degree_seq import havel_hakimi_graph
+
+
+def test_incidence_matrix_simple():
+ deg = [3, 2, 2, 1, 0]
+ G = havel_hakimi_graph(deg)
+ deg = [(1, 0), (1, 0), (1, 0), (2, 0), (1, 0), (2, 1), (0, 1), (0, 1)]
+ MG = nx.random_clustered_graph(deg, seed=42)
+
+ I = nx.incidence_matrix(G, dtype=int).todense()
+ # fmt: off
+ expected = np.array(
+ [[1, 1, 1, 0],
+ [0, 1, 0, 1],
+ [1, 0, 0, 1],
+ [0, 0, 1, 0],
+ [0, 0, 0, 0]]
+ )
+ # fmt: on
+ np.testing.assert_equal(I, expected)
+
+ I = nx.incidence_matrix(MG, dtype=int).todense()
+ # fmt: off
+ expected = np.array(
+ [[1, 0, 0, 0, 0, 0, 0],
+ [1, 0, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0],
+ [0, 1, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 1, 1, 0],
+ [0, 0, 0, 0, 0, 1, 1],
+ [0, 0, 0, 0, 1, 0, 1]]
+ )
+ # fmt: on
+ np.testing.assert_equal(I, expected)
+
+ with pytest.raises(NetworkXError):
+ nx.incidence_matrix(G, nodelist=[0, 1])
+
+
+class TestGraphMatrix:
+ @classmethod
+ def setup_class(cls):
+ deg = [3, 2, 2, 1, 0]
+ cls.G = havel_hakimi_graph(deg)
+ # fmt: off
+ cls.OI = np.array(
+ [[-1, -1, -1, 0],
+ [1, 0, 0, -1],
+ [0, 1, 0, 1],
+ [0, 0, 1, 0],
+ [0, 0, 0, 0]]
+ )
+ cls.A = np.array(
+ [[0, 1, 1, 1, 0],
+ [1, 0, 1, 0, 0],
+ [1, 1, 0, 0, 0],
+ [1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0]]
+ )
+ # fmt: on
+ cls.WG = havel_hakimi_graph(deg)
+ cls.WG.add_edges_from(
+ (u, v, {"weight": 0.5, "other": 0.3}) for (u, v) in cls.G.edges()
+ )
+ # fmt: off
+ cls.WA = np.array(
+ [[0, 0.5, 0.5, 0.5, 0],
+ [0.5, 0, 0.5, 0, 0],
+ [0.5, 0.5, 0, 0, 0],
+ [0.5, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0]]
+ )
+ # fmt: on
+ cls.MG = nx.MultiGraph(cls.G)
+ cls.MG2 = cls.MG.copy()
+ cls.MG2.add_edge(0, 1)
+ # fmt: off
+ cls.MG2A = np.array(
+ [[0, 2, 1, 1, 0],
+ [2, 0, 1, 0, 0],
+ [1, 1, 0, 0, 0],
+ [1, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0]]
+ )
+ cls.MGOI = np.array(
+ [[-1, -1, -1, -1, 0],
+ [1, 1, 0, 0, -1],
+ [0, 0, 1, 0, 1],
+ [0, 0, 0, 1, 0],
+ [0, 0, 0, 0, 0]]
+ )
+ # fmt: on
+ cls.no_edges_G = nx.Graph([(1, 2), (3, 2, {"weight": 8})])
+ cls.no_edges_A = np.array([[0, 0], [0, 0]])
+
+ def test_incidence_matrix(self):
+ "Conversion to incidence matrix"
+ I = nx.incidence_matrix(
+ self.G,
+ nodelist=sorted(self.G),
+ edgelist=sorted(self.G.edges()),
+ oriented=True,
+ dtype=int,
+ ).todense()
+ np.testing.assert_equal(I, self.OI)
+
+ I = nx.incidence_matrix(
+ self.G,
+ nodelist=sorted(self.G),
+ edgelist=sorted(self.G.edges()),
+ oriented=False,
+ dtype=int,
+ ).todense()
+ np.testing.assert_equal(I, np.abs(self.OI))
+
+ I = nx.incidence_matrix(
+ self.MG,
+ nodelist=sorted(self.MG),
+ edgelist=sorted(self.MG.edges()),
+ oriented=True,
+ dtype=int,
+ ).todense()
+ np.testing.assert_equal(I, self.OI)
+
+ I = nx.incidence_matrix(
+ self.MG,
+ nodelist=sorted(self.MG),
+ edgelist=sorted(self.MG.edges()),
+ oriented=False,
+ dtype=int,
+ ).todense()
+ np.testing.assert_equal(I, np.abs(self.OI))
+
+ I = nx.incidence_matrix(
+ self.MG2,
+ nodelist=sorted(self.MG2),
+ edgelist=sorted(self.MG2.edges()),
+ oriented=True,
+ dtype=int,
+ ).todense()
+ np.testing.assert_equal(I, self.MGOI)
+
+ I = nx.incidence_matrix(
+ self.MG2,
+ nodelist=sorted(self.MG),
+ edgelist=sorted(self.MG2.edges()),
+ oriented=False,
+ dtype=int,
+ ).todense()
+ np.testing.assert_equal(I, np.abs(self.MGOI))
+
+ I = nx.incidence_matrix(self.G, dtype=np.uint8)
+ assert I.dtype == np.uint8
+
+ def test_weighted_incidence_matrix(self):
+ I = nx.incidence_matrix(
+ self.WG,
+ nodelist=sorted(self.WG),
+ edgelist=sorted(self.WG.edges()),
+ oriented=True,
+ dtype=int,
+ ).todense()
+ np.testing.assert_equal(I, self.OI)
+
+ I = nx.incidence_matrix(
+ self.WG,
+ nodelist=sorted(self.WG),
+ edgelist=sorted(self.WG.edges()),
+ oriented=False,
+ dtype=int,
+ ).todense()
+ np.testing.assert_equal(I, np.abs(self.OI))
+
+ # np.testing.assert_equal(nx.incidence_matrix(self.WG,oriented=True,
+ # weight='weight').todense(),0.5*self.OI)
+ # np.testing.assert_equal(nx.incidence_matrix(self.WG,weight='weight').todense(),
+ # np.abs(0.5*self.OI))
+ # np.testing.assert_equal(nx.incidence_matrix(self.WG,oriented=True,weight='other').todense(),
+ # 0.3*self.OI)
+
+ I = nx.incidence_matrix(
+ self.WG,
+ nodelist=sorted(self.WG),
+ edgelist=sorted(self.WG.edges()),
+ oriented=True,
+ weight="weight",
+ ).todense()
+ np.testing.assert_equal(I, 0.5 * self.OI)
+
+ I = nx.incidence_matrix(
+ self.WG,
+ nodelist=sorted(self.WG),
+ edgelist=sorted(self.WG.edges()),
+ oriented=False,
+ weight="weight",
+ ).todense()
+ np.testing.assert_equal(I, np.abs(0.5 * self.OI))
+
+ I = nx.incidence_matrix(
+ self.WG,
+ nodelist=sorted(self.WG),
+ edgelist=sorted(self.WG.edges()),
+ oriented=True,
+ weight="other",
+ ).todense()
+ np.testing.assert_equal(I, 0.3 * self.OI)
+
+ # WMG=nx.MultiGraph(self.WG)
+ # WMG.add_edge(0,1,weight=0.5,other=0.3)
+ # np.testing.assert_equal(nx.incidence_matrix(WMG,weight='weight').todense(),
+ # np.abs(0.5*self.MGOI))
+ # np.testing.assert_equal(nx.incidence_matrix(WMG,weight='weight',oriented=True).todense(),
+ # 0.5*self.MGOI)
+ # np.testing.assert_equal(nx.incidence_matrix(WMG,weight='other',oriented=True).todense(),
+ # 0.3*self.MGOI)
+
+ WMG = nx.MultiGraph(self.WG)
+ WMG.add_edge(0, 1, weight=0.5, other=0.3)
+
+ I = nx.incidence_matrix(
+ WMG,
+ nodelist=sorted(WMG),
+ edgelist=sorted(WMG.edges(keys=True)),
+ oriented=True,
+ weight="weight",
+ ).todense()
+ np.testing.assert_equal(I, 0.5 * self.MGOI)
+
+ I = nx.incidence_matrix(
+ WMG,
+ nodelist=sorted(WMG),
+ edgelist=sorted(WMG.edges(keys=True)),
+ oriented=False,
+ weight="weight",
+ ).todense()
+ np.testing.assert_equal(I, np.abs(0.5 * self.MGOI))
+
+ I = nx.incidence_matrix(
+ WMG,
+ nodelist=sorted(WMG),
+ edgelist=sorted(WMG.edges(keys=True)),
+ oriented=True,
+ weight="other",
+ ).todense()
+ np.testing.assert_equal(I, 0.3 * self.MGOI)
+
+ def test_adjacency_matrix(self):
+ "Conversion to adjacency matrix"
+ np.testing.assert_equal(nx.adjacency_matrix(self.G).todense(), self.A)
+ np.testing.assert_equal(nx.adjacency_matrix(self.MG).todense(), self.A)
+ np.testing.assert_equal(nx.adjacency_matrix(self.MG2).todense(), self.MG2A)
+ np.testing.assert_equal(
+ nx.adjacency_matrix(self.G, nodelist=[0, 1]).todense(), self.A[:2, :2]
+ )
+ np.testing.assert_equal(nx.adjacency_matrix(self.WG).todense(), self.WA)
+ np.testing.assert_equal(
+ nx.adjacency_matrix(self.WG, weight=None).todense(), self.A
+ )
+ np.testing.assert_equal(
+ nx.adjacency_matrix(self.MG2, weight=None).todense(), self.MG2A
+ )
+ np.testing.assert_equal(
+ nx.adjacency_matrix(self.WG, weight="other").todense(), 0.6 * self.WA
+ )
+ np.testing.assert_equal(
+ nx.adjacency_matrix(self.no_edges_G, nodelist=[1, 3]).todense(),
+ self.no_edges_A,
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_laplacian.py b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_laplacian.py
new file mode 100644
index 00000000..23f1b28e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_laplacian.py
@@ -0,0 +1,336 @@
+import pytest
+
+np = pytest.importorskip("numpy")
+pytest.importorskip("scipy")
+
+import networkx as nx
+from networkx.generators.degree_seq import havel_hakimi_graph
+from networkx.generators.expanders import margulis_gabber_galil_graph
+
+
+class TestLaplacian:
+ @classmethod
+ def setup_class(cls):
+ deg = [3, 2, 2, 1, 0]
+ cls.G = havel_hakimi_graph(deg)
+ cls.WG = nx.Graph(
+ (u, v, {"weight": 0.5, "other": 0.3}) for (u, v) in cls.G.edges()
+ )
+ cls.WG.add_node(4)
+ cls.MG = nx.MultiGraph(cls.G)
+
+ # Graph with clsloops
+ cls.Gsl = cls.G.copy()
+ 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
+ NL = np.array([[ 3, -1, -1, -1, 0],
+ [-1, 2, -1, 0, 0],
+ [-1, -1, 2, 0, 0],
+ [-1, 0, 0, 1, 0],
+ [ 0, 0, 0, 0, 0]])
+ # 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(
+ nx.laplacian_matrix(self.G, nodelist=[0, 1]).todense(),
+ np.array([[1, -1], [-1, 1]]),
+ )
+ np.testing.assert_equal(nx.laplacian_matrix(self.WG).todense(), WL)
+ np.testing.assert_equal(nx.laplacian_matrix(self.WG, weight=None).todense(), NL)
+ np.testing.assert_equal(
+ 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
+ G = np.array([[ 1. , -0.408, -0.408, -0.577, 0.],
+ [-0.408, 1. , -0.5 , 0. , 0.],
+ [-0.408, -0.5 , 1. , 0. , 0.],
+ [-0.577, 0. , 0. , 1. , 0.],
+ [ 0. , 0. , 0. , 0. , 0.]])
+ GL = np.array([[ 1. , -0.408, -0.408, -0.577, 0. ],
+ [-0.408, 1. , -0.5 , 0. , 0. ],
+ [-0.408, -0.5 , 1. , 0. , 0. ],
+ [-0.577, 0. , 0. , 1. , 0. ],
+ [ 0. , 0. , 0. , 0. , 0. ]])
+ Lsl = np.array([[ 0.75 , -0.2887, -0.2887, -0.3536, 0. ],
+ [-0.2887, 0.6667, -0.3333, 0. , 0. ],
+ [-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(
+ nx.normalized_laplacian_matrix(self.G, nodelist=range(5)).todense(),
+ G,
+ decimal=3,
+ )
+ np.testing.assert_almost_equal(
+ nx.normalized_laplacian_matrix(self.G).todense(), GL, decimal=3
+ )
+ np.testing.assert_almost_equal(
+ nx.normalized_laplacian_matrix(self.MG).todense(), GL, decimal=3
+ )
+ np.testing.assert_almost_equal(
+ nx.normalized_laplacian_matrix(self.WG).todense(), GL, decimal=3
+ )
+ np.testing.assert_almost_equal(
+ nx.normalized_laplacian_matrix(self.WG, weight="other").todense(),
+ GL,
+ decimal=3,
+ )
+ np.testing.assert_almost_equal(
+ 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"
+ # Graph used as an example in Sec. 4.1 of Langville and Meyer,
+ # "Google's PageRank and Beyond". The graph contains dangling nodes, so
+ # the pagerank random walk is selected by directed_laplacian
+ G = nx.DiGraph()
+ G.add_edges_from(
+ (
+ (1, 2),
+ (1, 3),
+ (3, 1),
+ (3, 2),
+ (3, 5),
+ (4, 5),
+ (4, 6),
+ (5, 4),
+ (5, 6),
+ (6, 4),
+ )
+ )
+ # fmt: off
+ GL = np.array([[ 0.9833, -0.2941, -0.3882, -0.0291, -0.0231, -0.0261],
+ [-0.2941, 0.8333, -0.2339, -0.0536, -0.0589, -0.0554],
+ [-0.3882, -0.2339, 0.9833, -0.0278, -0.0896, -0.0251],
+ [-0.0291, -0.0536, -0.0278, 0.9833, -0.4878, -0.6675],
+ [-0.0231, -0.0589, -0.0896, -0.4878, 0.9833, -0.2078],
+ [-0.0261, -0.0554, -0.0251, -0.6675, -0.2078, 0.9833]])
+ # fmt: on
+ L = nx.directed_laplacian_matrix(G, alpha=0.9, nodelist=sorted(G))
+ np.testing.assert_almost_equal(L, GL, decimal=3)
+
+ # Make the graph strongly connected, so we can use a random and lazy walk
+ G.add_edges_from(((2, 5), (6, 1)))
+ # fmt: off
+ GL = np.array([[ 1. , -0.3062, -0.4714, 0. , 0. , -0.3227],
+ [-0.3062, 1. , -0.1443, 0. , -0.3162, 0. ],
+ [-0.4714, -0.1443, 1. , 0. , -0.0913, 0. ],
+ [ 0. , 0. , 0. , 1. , -0.5 , -0.5 ],
+ [ 0. , -0.3162, -0.0913, -0.5 , 1. , -0.25 ],
+ [-0.3227, 0. , 0. , -0.5 , -0.25 , 1. ]])
+ # fmt: on
+ L = nx.directed_laplacian_matrix(
+ G, alpha=0.9, nodelist=sorted(G), walk_type="random"
+ )
+ np.testing.assert_almost_equal(L, GL, decimal=3)
+
+ # fmt: off
+ GL = np.array([[ 0.5 , -0.1531, -0.2357, 0. , 0. , -0.1614],
+ [-0.1531, 0.5 , -0.0722, 0. , -0.1581, 0. ],
+ [-0.2357, -0.0722, 0.5 , 0. , -0.0456, 0. ],
+ [ 0. , 0. , 0. , 0.5 , -0.25 , -0.25 ],
+ [ 0. , -0.1581, -0.0456, -0.25 , 0.5 , -0.125 ],
+ [-0.1614, 0. , 0. , -0.25 , -0.125 , 0.5 ]])
+ # fmt: on
+ L = nx.directed_laplacian_matrix(G, alpha=0.9, nodelist=sorted(G), walk_type="lazy")
+ np.testing.assert_almost_equal(L, GL, decimal=3)
+
+ # Make a strongly connected periodic graph
+ G = nx.DiGraph()
+ G.add_edges_from(((1, 2), (2, 4), (4, 1), (1, 3), (3, 4)))
+ # fmt: off
+ GL = np.array([[ 0.5 , -0.176, -0.176, -0.25 ],
+ [-0.176, 0.5 , 0. , -0.176],
+ [-0.176, 0. , 0.5 , -0.176],
+ [-0.25 , -0.176, -0.176, 0.5 ]])
+ # fmt: on
+ L = nx.directed_laplacian_matrix(G, alpha=0.9, nodelist=sorted(G))
+ np.testing.assert_almost_equal(L, GL, decimal=3)
+
+
+def test_directed_combinatorial_laplacian():
+ "Directed combinatorial Laplacian"
+ # Graph used as an example in Sec. 4.1 of Langville and Meyer,
+ # "Google's PageRank and Beyond". The graph contains dangling nodes, so
+ # the pagerank random walk is selected by directed_laplacian
+ G = nx.DiGraph()
+ G.add_edges_from(
+ (
+ (1, 2),
+ (1, 3),
+ (3, 1),
+ (3, 2),
+ (3, 5),
+ (4, 5),
+ (4, 6),
+ (5, 4),
+ (5, 6),
+ (6, 4),
+ )
+ )
+ # fmt: off
+ GL = np.array([[ 0.0366, -0.0132, -0.0153, -0.0034, -0.0020, -0.0027],
+ [-0.0132, 0.0450, -0.0111, -0.0076, -0.0062, -0.0069],
+ [-0.0153, -0.0111, 0.0408, -0.0035, -0.0083, -0.0027],
+ [-0.0034, -0.0076, -0.0035, 0.3688, -0.1356, -0.2187],
+ [-0.0020, -0.0062, -0.0083, -0.1356, 0.2026, -0.0505],
+ [-0.0027, -0.0069, -0.0027, -0.2187, -0.0505, 0.2815]])
+ # fmt: on
+
+ L = nx.directed_combinatorial_laplacian_matrix(G, alpha=0.9, nodelist=sorted(G))
+ np.testing.assert_almost_equal(L, GL, decimal=3)
+
+ # Make the graph strongly connected, so we can use a random and lazy walk
+ G.add_edges_from(((2, 5), (6, 1)))
+
+ # fmt: off
+ GL = np.array([[ 0.1395, -0.0349, -0.0465, 0. , 0. , -0.0581],
+ [-0.0349, 0.093 , -0.0116, 0. , -0.0465, 0. ],
+ [-0.0465, -0.0116, 0.0698, 0. , -0.0116, 0. ],
+ [ 0. , 0. , 0. , 0.2326, -0.1163, -0.1163],
+ [ 0. , -0.0465, -0.0116, -0.1163, 0.2326, -0.0581],
+ [-0.0581, 0. , 0. , -0.1163, -0.0581, 0.2326]])
+ # fmt: on
+
+ L = nx.directed_combinatorial_laplacian_matrix(
+ G, alpha=0.9, nodelist=sorted(G), walk_type="random"
+ )
+ np.testing.assert_almost_equal(L, GL, decimal=3)
+
+ # fmt: off
+ GL = np.array([[ 0.0698, -0.0174, -0.0233, 0. , 0. , -0.0291],
+ [-0.0174, 0.0465, -0.0058, 0. , -0.0233, 0. ],
+ [-0.0233, -0.0058, 0.0349, 0. , -0.0058, 0. ],
+ [ 0. , 0. , 0. , 0.1163, -0.0581, -0.0581],
+ [ 0. , -0.0233, -0.0058, -0.0581, 0.1163, -0.0291],
+ [-0.0291, 0. , 0. , -0.0581, -0.0291, 0.1163]])
+ # fmt: on
+
+ L = nx.directed_combinatorial_laplacian_matrix(
+ G, alpha=0.9, nodelist=sorted(G), walk_type="lazy"
+ )
+ np.testing.assert_almost_equal(L, GL, decimal=3)
+
+ E = nx.DiGraph(margulis_gabber_galil_graph(2))
+ L = nx.directed_combinatorial_laplacian_matrix(E)
+ # fmt: off
+ expected = np.array(
+ [[ 0.16666667, -0.08333333, -0.08333333, 0. ],
+ [-0.08333333, 0.16666667, 0. , -0.08333333],
+ [-0.08333333, 0. , 0.16666667, -0.08333333],
+ [ 0. , -0.08333333, -0.08333333, 0.16666667]]
+ )
+ # fmt: on
+ np.testing.assert_almost_equal(L, expected, decimal=6)
+
+ with pytest.raises(nx.NetworkXError):
+ nx.directed_combinatorial_laplacian_matrix(G, walk_type="pagerank", alpha=100)
+ with pytest.raises(nx.NetworkXError):
+ nx.directed_combinatorial_laplacian_matrix(G, walk_type="silly")
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_modularity.py b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_modularity.py
new file mode 100644
index 00000000..9f94ff4d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_modularity.py
@@ -0,0 +1,87 @@
+import pytest
+
+np = pytest.importorskip("numpy")
+pytest.importorskip("scipy")
+
+import networkx as nx
+from networkx.generators.degree_seq import havel_hakimi_graph
+
+
+class TestModularity:
+ @classmethod
+ def setup_class(cls):
+ deg = [3, 2, 2, 1, 0]
+ cls.G = havel_hakimi_graph(deg)
+ # Graph used as an example in Sec. 4.1 of Langville and Meyer,
+ # "Google's PageRank and Beyond". (Used for test_directed_laplacian)
+ cls.DG = nx.DiGraph()
+ cls.DG.add_edges_from(
+ (
+ (1, 2),
+ (1, 3),
+ (3, 1),
+ (3, 2),
+ (3, 5),
+ (4, 5),
+ (4, 6),
+ (5, 4),
+ (5, 6),
+ (6, 4),
+ )
+ )
+
+ def test_modularity(self):
+ "Modularity matrix"
+ # fmt: off
+ B = np.array([[-1.125, 0.25, 0.25, 0.625, 0.],
+ [0.25, -0.5, 0.5, -0.25, 0.],
+ [0.25, 0.5, -0.5, -0.25, 0.],
+ [0.625, -0.25, -0.25, -0.125, 0.],
+ [0., 0., 0., 0., 0.]])
+ # fmt: on
+
+ permutation = [4, 0, 1, 2, 3]
+ np.testing.assert_equal(nx.modularity_matrix(self.G), B)
+ np.testing.assert_equal(
+ nx.modularity_matrix(self.G, nodelist=permutation),
+ B[np.ix_(permutation, permutation)],
+ )
+
+ def test_modularity_weight(self):
+ "Modularity matrix with weights"
+ # fmt: off
+ B = np.array([[-1.125, 0.25, 0.25, 0.625, 0.],
+ [0.25, -0.5, 0.5, -0.25, 0.],
+ [0.25, 0.5, -0.5, -0.25, 0.],
+ [0.625, -0.25, -0.25, -0.125, 0.],
+ [0., 0., 0., 0., 0.]])
+ # fmt: on
+
+ G_weighted = self.G.copy()
+ for n1, n2 in G_weighted.edges():
+ G_weighted.edges[n1, n2]["weight"] = 0.5
+ # The following test would fail in networkx 1.1
+ np.testing.assert_equal(nx.modularity_matrix(G_weighted), B)
+ # The following test that the modularity matrix get rescaled accordingly
+ np.testing.assert_equal(
+ nx.modularity_matrix(G_weighted, weight="weight"), 0.5 * B
+ )
+
+ def test_directed_modularity(self):
+ "Directed Modularity matrix"
+ # fmt: off
+ B = np.array([[-0.2, 0.6, 0.8, -0.4, -0.4, -0.4],
+ [0., 0., 0., 0., 0., 0.],
+ [0.7, 0.4, -0.3, -0.6, 0.4, -0.6],
+ [-0.2, -0.4, -0.2, -0.4, 0.6, 0.6],
+ [-0.2, -0.4, -0.2, 0.6, -0.4, 0.6],
+ [-0.1, -0.2, -0.1, 0.8, -0.2, -0.2]])
+ # fmt: on
+ node_permutation = [5, 1, 2, 3, 4, 6]
+ idx_permutation = [4, 0, 1, 2, 3, 5]
+ mm = nx.directed_modularity_matrix(self.DG, nodelist=sorted(self.DG))
+ np.testing.assert_equal(mm, B)
+ np.testing.assert_equal(
+ nx.directed_modularity_matrix(self.DG, nodelist=node_permutation),
+ B[np.ix_(idx_permutation, idx_permutation)],
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_spectrum.py b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_spectrum.py
new file mode 100644
index 00000000..e9101303
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/linalg/tests/test_spectrum.py
@@ -0,0 +1,71 @@
+import pytest
+
+np = pytest.importorskip("numpy")
+pytest.importorskip("scipy")
+
+import networkx as nx
+from networkx.generators.degree_seq import havel_hakimi_graph
+
+
+class TestSpectrum:
+ @classmethod
+ def setup_class(cls):
+ deg = [3, 2, 2, 1, 0]
+ cls.G = havel_hakimi_graph(deg)
+ cls.P = nx.path_graph(3)
+ cls.WG = nx.Graph(
+ (u, v, {"weight": 0.5, "other": 0.3}) for (u, v) in cls.G.edges()
+ )
+ cls.WG.add_node(4)
+ cls.DG = nx.DiGraph()
+ nx.add_path(cls.DG, [0, 1, 2])
+
+ def test_laplacian_spectrum(self):
+ "Laplacian eigenvalues"
+ evals = np.array([0, 0, 1, 3, 4])
+ e = sorted(nx.laplacian_spectrum(self.G))
+ np.testing.assert_almost_equal(e, evals)
+ e = sorted(nx.laplacian_spectrum(self.WG, weight=None))
+ np.testing.assert_almost_equal(e, evals)
+ e = sorted(nx.laplacian_spectrum(self.WG))
+ np.testing.assert_almost_equal(e, 0.5 * evals)
+ e = sorted(nx.laplacian_spectrum(self.WG, weight="other"))
+ np.testing.assert_almost_equal(e, 0.3 * evals)
+
+ def test_normalized_laplacian_spectrum(self):
+ "Normalized Laplacian eigenvalues"
+ evals = np.array([0, 0, 0.7712864461218, 1.5, 1.7287135538781])
+ e = sorted(nx.normalized_laplacian_spectrum(self.G))
+ np.testing.assert_almost_equal(e, evals)
+ e = sorted(nx.normalized_laplacian_spectrum(self.WG, weight=None))
+ np.testing.assert_almost_equal(e, evals)
+ e = sorted(nx.normalized_laplacian_spectrum(self.WG))
+ np.testing.assert_almost_equal(e, evals)
+ e = sorted(nx.normalized_laplacian_spectrum(self.WG, weight="other"))
+ np.testing.assert_almost_equal(e, evals)
+
+ def test_adjacency_spectrum(self):
+ "Adjacency eigenvalues"
+ evals = np.array([-np.sqrt(2), 0, np.sqrt(2)])
+ e = sorted(nx.adjacency_spectrum(self.P))
+ np.testing.assert_almost_equal(e, evals)
+
+ def test_modularity_spectrum(self):
+ "Modularity eigenvalues"
+ evals = np.array([-1.5, 0.0, 0.0])
+ e = sorted(nx.modularity_spectrum(self.P))
+ np.testing.assert_almost_equal(e, evals)
+ # Directed modularity eigenvalues
+ evals = np.array([-0.5, 0.0, 0.0])
+ e = sorted(nx.modularity_spectrum(self.DG))
+ np.testing.assert_almost_equal(e, evals)
+
+ def test_bethe_hessian_spectrum(self):
+ "Bethe Hessian eigenvalues"
+ evals = np.array([0.5 * (9 - np.sqrt(33)), 4, 0.5 * (9 + np.sqrt(33))])
+ e = sorted(nx.bethe_hessian_spectrum(self.P, r=2))
+ np.testing.assert_almost_equal(e, evals)
+ # Collapses back to Laplacian:
+ e1 = sorted(nx.bethe_hessian_spectrum(self.P, r=1))
+ e2 = sorted(nx.laplacian_spectrum(self.P))
+ np.testing.assert_almost_equal(e1, e2)