aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/smallworld.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/smallworld.py')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/smallworld.py404
1 files changed, 404 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/smallworld.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/smallworld.py
new file mode 100644
index 00000000..456a4ca1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/smallworld.py
@@ -0,0 +1,404 @@
+"""Functions for estimating the small-world-ness of graphs.
+
+A small world network is characterized by a small average shortest path length,
+and a large clustering coefficient.
+
+Small-worldness is commonly measured with the coefficient sigma or omega.
+
+Both coefficients compare the average clustering coefficient and shortest path
+length of a given graph against the same quantities for an equivalent random
+or lattice graph.
+
+For more information, see the Wikipedia article on small-world network [1]_.
+
+.. [1] Small-world network:: https://en.wikipedia.org/wiki/Small-world_network
+
+"""
+
+import networkx as nx
+from networkx.utils import not_implemented_for, py_random_state
+
+__all__ = ["random_reference", "lattice_reference", "sigma", "omega"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@py_random_state(3)
+@nx._dispatchable(returns_graph=True)
+def random_reference(G, niter=1, connectivity=True, seed=None):
+ """Compute a random graph by swapping edges of a given graph.
+
+ Parameters
+ ----------
+ G : graph
+ An undirected graph with 4 or more nodes.
+
+ niter : integer (optional, default=1)
+ An edge is rewired approximately `niter` times.
+
+ connectivity : boolean (optional, default=True)
+ When True, ensure connectivity for the randomized graph.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : graph
+ The randomized graph.
+
+ Raises
+ ------
+ NetworkXError
+ If there are fewer than 4 nodes or 2 edges in `G`
+
+ Notes
+ -----
+ The implementation is adapted from the algorithm by Maslov and Sneppen
+ (2002) [1]_.
+
+ References
+ ----------
+ .. [1] Maslov, Sergei, and Kim Sneppen.
+ "Specificity and stability in topology of protein networks."
+ Science 296.5569 (2002): 910-913.
+ """
+ if len(G) < 4:
+ raise nx.NetworkXError("Graph has fewer than four nodes.")
+ if len(G.edges) < 2:
+ raise nx.NetworkXError("Graph has fewer that 2 edges")
+
+ from networkx.utils import cumulative_distribution, discrete_sequence
+
+ local_conn = nx.connectivity.local_edge_connectivity
+
+ G = G.copy()
+ keys, degrees = zip(*G.degree()) # keys, degree
+ cdf = cumulative_distribution(degrees) # cdf of degree
+ nnodes = len(G)
+ nedges = nx.number_of_edges(G)
+ niter = niter * nedges
+ ntries = int(nnodes * nedges / (nnodes * (nnodes - 1) / 2))
+ swapcount = 0
+
+ for i in range(niter):
+ n = 0
+ while n < ntries:
+ # pick two random edges without creating edge list
+ # choose source node indices from discrete distribution
+ (ai, ci) = discrete_sequence(2, cdistribution=cdf, seed=seed)
+ if ai == ci:
+ continue # same source, skip
+ a = keys[ai] # convert index to label
+ c = keys[ci]
+ # choose target uniformly from neighbors
+ b = seed.choice(list(G.neighbors(a)))
+ d = seed.choice(list(G.neighbors(c)))
+ if b in [a, c, d] or d in [a, b, c]:
+ continue # all vertices should be different
+
+ # don't create parallel edges
+ if (d not in G[a]) and (b not in G[c]):
+ G.add_edge(a, d)
+ G.add_edge(c, b)
+ G.remove_edge(a, b)
+ G.remove_edge(c, d)
+
+ # Check if the graph is still connected
+ if connectivity and local_conn(G, a, b) == 0:
+ # Not connected, revert the swap
+ G.remove_edge(a, d)
+ G.remove_edge(c, b)
+ G.add_edge(a, b)
+ G.add_edge(c, d)
+ else:
+ swapcount += 1
+ break
+ n += 1
+ return G
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@py_random_state(4)
+@nx._dispatchable(returns_graph=True)
+def lattice_reference(G, niter=5, D=None, connectivity=True, seed=None):
+ """Latticize the given graph by swapping edges.
+
+ Parameters
+ ----------
+ G : graph
+ An undirected graph.
+
+ niter : integer (optional, default=1)
+ An edge is rewired approximately niter times.
+
+ D : numpy.array (optional, default=None)
+ Distance to the diagonal matrix.
+
+ connectivity : boolean (optional, default=True)
+ Ensure connectivity for the latticized graph when set to True.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ G : graph
+ The latticized graph.
+
+ Raises
+ ------
+ NetworkXError
+ If there are fewer than 4 nodes or 2 edges in `G`
+
+ Notes
+ -----
+ The implementation is adapted from the algorithm by Sporns et al. [1]_.
+ which is inspired from the original work by Maslov and Sneppen(2002) [2]_.
+
+ References
+ ----------
+ .. [1] Sporns, Olaf, and Jonathan D. Zwi.
+ "The small world of the cerebral cortex."
+ Neuroinformatics 2.2 (2004): 145-162.
+ .. [2] Maslov, Sergei, and Kim Sneppen.
+ "Specificity and stability in topology of protein networks."
+ Science 296.5569 (2002): 910-913.
+ """
+ import numpy as np
+
+ from networkx.utils import cumulative_distribution, discrete_sequence
+
+ local_conn = nx.connectivity.local_edge_connectivity
+
+ if len(G) < 4:
+ raise nx.NetworkXError("Graph has fewer than four nodes.")
+ if len(G.edges) < 2:
+ raise nx.NetworkXError("Graph has fewer that 2 edges")
+ # Instead of choosing uniformly at random from a generated edge list,
+ # this algorithm chooses nonuniformly from the set of nodes with
+ # probability weighted by degree.
+ G = G.copy()
+ keys, degrees = zip(*G.degree()) # keys, degree
+ cdf = cumulative_distribution(degrees) # cdf of degree
+
+ nnodes = len(G)
+ nedges = nx.number_of_edges(G)
+ if D is None:
+ D = np.zeros((nnodes, nnodes))
+ un = np.arange(1, nnodes)
+ um = np.arange(nnodes - 1, 0, -1)
+ u = np.append((0,), np.where(un < um, un, um))
+
+ for v in range(int(np.ceil(nnodes / 2))):
+ D[nnodes - v - 1, :] = np.append(u[v + 1 :], u[: v + 1])
+ D[v, :] = D[nnodes - v - 1, :][::-1]
+
+ niter = niter * nedges
+ # maximal number of rewiring attempts per 'niter'
+ max_attempts = int(nnodes * nedges / (nnodes * (nnodes - 1) / 2))
+
+ for _ in range(niter):
+ n = 0
+ while n < max_attempts:
+ # pick two random edges without creating edge list
+ # choose source node indices from discrete distribution
+ (ai, ci) = discrete_sequence(2, cdistribution=cdf, seed=seed)
+ if ai == ci:
+ continue # same source, skip
+ a = keys[ai] # convert index to label
+ c = keys[ci]
+ # choose target uniformly from neighbors
+ b = seed.choice(list(G.neighbors(a)))
+ d = seed.choice(list(G.neighbors(c)))
+ bi = keys.index(b)
+ di = keys.index(d)
+
+ if b in [a, c, d] or d in [a, b, c]:
+ continue # all vertices should be different
+
+ # don't create parallel edges
+ if (d not in G[a]) and (b not in G[c]):
+ if D[ai, bi] + D[ci, di] >= D[ai, ci] + D[bi, di]:
+ # only swap if we get closer to the diagonal
+ G.add_edge(a, d)
+ G.add_edge(c, b)
+ G.remove_edge(a, b)
+ G.remove_edge(c, d)
+
+ # Check if the graph is still connected
+ if connectivity and local_conn(G, a, b) == 0:
+ # Not connected, revert the swap
+ G.remove_edge(a, d)
+ G.remove_edge(c, b)
+ G.add_edge(a, b)
+ G.add_edge(c, d)
+ else:
+ break
+ n += 1
+
+ return G
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@py_random_state(3)
+@nx._dispatchable
+def sigma(G, niter=100, nrand=10, seed=None):
+ """Returns the small-world coefficient (sigma) of the given graph.
+
+ The small-world coefficient is defined as:
+ sigma = C/Cr / L/Lr
+ where C and L are respectively the average clustering coefficient and
+ average shortest path length of G. Cr and Lr are respectively the average
+ clustering coefficient and average shortest path length of an equivalent
+ random graph.
+
+ A graph is commonly classified as small-world if sigma>1.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+ niter : integer (optional, default=100)
+ Approximate number of rewiring per edge to compute the equivalent
+ random graph.
+ nrand : integer (optional, default=10)
+ Number of random graphs generated to compute the average clustering
+ coefficient (Cr) and average shortest path length (Lr).
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ sigma : float
+ The small-world coefficient of G.
+
+ Notes
+ -----
+ The implementation is adapted from Humphries et al. [1]_ [2]_.
+
+ References
+ ----------
+ .. [1] The brainstem reticular formation is a small-world, not scale-free,
+ network M. D. Humphries, K. Gurney and T. J. Prescott,
+ Proc. Roy. Soc. B 2006 273, 503-511, doi:10.1098/rspb.2005.3354.
+ .. [2] Humphries and Gurney (2008).
+ "Network 'Small-World-Ness': A Quantitative Method for Determining
+ Canonical Network Equivalence".
+ PLoS One. 3 (4). PMID 18446219. doi:10.1371/journal.pone.0002051.
+ """
+ import numpy as np
+
+ # Compute the mean clustering coefficient and average shortest path length
+ # for an equivalent random graph
+ randMetrics = {"C": [], "L": []}
+ for i in range(nrand):
+ Gr = random_reference(G, niter=niter, seed=seed)
+ randMetrics["C"].append(nx.transitivity(Gr))
+ randMetrics["L"].append(nx.average_shortest_path_length(Gr))
+
+ C = nx.transitivity(G)
+ L = nx.average_shortest_path_length(G)
+ Cr = np.mean(randMetrics["C"])
+ Lr = np.mean(randMetrics["L"])
+
+ sigma = (C / Cr) / (L / Lr)
+
+ return float(sigma)
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@py_random_state(3)
+@nx._dispatchable
+def omega(G, niter=5, nrand=10, seed=None):
+ """Returns the small-world coefficient (omega) of a graph
+
+ The small-world coefficient of a graph G is:
+
+ omega = Lr/L - C/Cl
+
+ where C and L are respectively the average clustering coefficient and
+ average shortest path length of G. Lr is the average shortest path length
+ of an equivalent random graph and Cl is the average clustering coefficient
+ of an equivalent lattice graph.
+
+ The small-world coefficient (omega) measures how much G is like a lattice
+ or a random graph. Negative values mean G is similar to a lattice whereas
+ positive values mean G is a random graph.
+ Values close to 0 mean that G has small-world characteristics.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ An undirected graph.
+
+ niter: integer (optional, default=5)
+ Approximate number of rewiring per edge to compute the equivalent
+ random graph.
+
+ nrand: integer (optional, default=10)
+ Number of random graphs generated to compute the maximal clustering
+ coefficient (Cr) and average shortest path length (Lr).
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+
+ Returns
+ -------
+ omega : float
+ The small-world coefficient (omega)
+
+ Notes
+ -----
+ The implementation is adapted from the algorithm by Telesford et al. [1]_.
+
+ References
+ ----------
+ .. [1] Telesford, Joyce, Hayasaka, Burdette, and Laurienti (2011).
+ "The Ubiquity of Small-World Networks".
+ Brain Connectivity. 1 (0038): 367-75. PMC 3604768. PMID 22432451.
+ doi:10.1089/brain.2011.0038.
+ """
+ import numpy as np
+
+ # Compute the mean clustering coefficient and average shortest path length
+ # for an equivalent random graph
+ randMetrics = {"C": [], "L": []}
+
+ # Calculate initial average clustering coefficient which potentially will
+ # get replaced by higher clustering coefficients from generated lattice
+ # reference graphs
+ Cl = nx.average_clustering(G)
+
+ niter_lattice_reference = niter
+ niter_random_reference = niter * 2
+
+ for _ in range(nrand):
+ # Generate random graph
+ Gr = random_reference(G, niter=niter_random_reference, seed=seed)
+ randMetrics["L"].append(nx.average_shortest_path_length(Gr))
+
+ # Generate lattice graph
+ Gl = lattice_reference(G, niter=niter_lattice_reference, seed=seed)
+
+ # Replace old clustering coefficient, if clustering is higher in
+ # generated lattice reference
+ Cl_temp = nx.average_clustering(Gl)
+ if Cl_temp > Cl:
+ Cl = Cl_temp
+
+ C = nx.average_clustering(G)
+ L = nx.average_shortest_path_length(G)
+ Lr = np.mean(randMetrics["L"])
+
+ omega = (Lr / L) - (C / Cl)
+
+ return float(omega)