aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/networkx/algorithms/community
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/algorithms/community
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/networkx/algorithms/community')
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/__init__.py26
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/asyn_fluid.py151
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/centrality.py171
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/community_utils.py30
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/divisive.py216
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/kclique.py79
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/kernighan_lin.py139
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/label_propagation.py338
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/louvain.py382
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/lukes.py227
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/modularity_max.py451
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/quality.py346
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_asyn_fluid.py136
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_centrality.py85
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_divisive.py106
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_kclique.py91
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_kernighan_lin.py92
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_label_propagation.py241
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_louvain.py264
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_lukes.py152
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_modularity_max.py340
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_quality.py139
-rw-r--r--.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_utils.py26
24 files changed, 4228 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/__init__.py
new file mode 100644
index 00000000..4dfa8481
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/__init__.py
@@ -0,0 +1,26 @@
+"""Functions for computing and measuring community structure.
+
+The ``community`` subpackage can be accessed by using :mod:`networkx.community`, then accessing the
+functions as attributes of ``community``. For example::
+
+ >>> import networkx as nx
+ >>> G = nx.barbell_graph(5, 1)
+ >>> communities_generator = nx.community.girvan_newman(G)
+ >>> top_level_communities = next(communities_generator)
+ >>> next_level_communities = next(communities_generator)
+ >>> sorted(map(sorted, next_level_communities))
+ [[0, 1, 2, 3, 4], [5], [6, 7, 8, 9, 10]]
+
+"""
+
+from networkx.algorithms.community.asyn_fluid import *
+from networkx.algorithms.community.centrality import *
+from networkx.algorithms.community.divisive import *
+from networkx.algorithms.community.kclique import *
+from networkx.algorithms.community.kernighan_lin import *
+from networkx.algorithms.community.label_propagation import *
+from networkx.algorithms.community.lukes import *
+from networkx.algorithms.community.modularity_max import *
+from networkx.algorithms.community.quality import *
+from networkx.algorithms.community.community_utils import *
+from networkx.algorithms.community.louvain import *
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/asyn_fluid.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/asyn_fluid.py
new file mode 100644
index 00000000..fea72c1b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/asyn_fluid.py
@@ -0,0 +1,151 @@
+"""Asynchronous Fluid Communities algorithm for community detection."""
+
+from collections import Counter
+
+import networkx as nx
+from networkx.algorithms.components import is_connected
+from networkx.exception import NetworkXError
+from networkx.utils import groups, not_implemented_for, py_random_state
+
+__all__ = ["asyn_fluidc"]
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@py_random_state(3)
+@nx._dispatchable
+def asyn_fluidc(G, k, max_iter=100, seed=None):
+ """Returns communities in `G` as detected by Fluid Communities algorithm.
+
+ The asynchronous fluid communities algorithm is described in
+ [1]_. The algorithm is based on the simple idea of fluids interacting
+ in an environment, expanding and pushing each other. Its initialization is
+ random, so found communities may vary on different executions.
+
+ The algorithm proceeds as follows. First each of the initial k communities
+ is initialized in a random vertex in the graph. Then the algorithm iterates
+ over all vertices in a random order, updating the community of each vertex
+ based on its own community and the communities of its neighbors. This
+ process is performed several times until convergence.
+ At all times, each community has a total density of 1, which is equally
+ distributed among the vertices it contains. If a vertex changes of
+ community, vertex densities of affected communities are adjusted
+ immediately. When a complete iteration over all vertices is done, such that
+ no vertex changes the community it belongs to, the algorithm has converged
+ and returns.
+
+ This is the original version of the algorithm described in [1]_.
+ Unfortunately, it does not support weighted graphs yet.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Graph must be simple and undirected.
+
+ k : integer
+ The number of communities to be found.
+
+ max_iter : integer
+ The number of maximum iterations allowed. By default 100.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ communities : iterable
+ Iterable of communities given as sets of nodes.
+
+ Notes
+ -----
+ k variable is not an optional argument.
+
+ References
+ ----------
+ .. [1] Parés F., Garcia-Gasulla D. et al. "Fluid Communities: A
+ Competitive and Highly Scalable Community Detection Algorithm".
+ [https://arxiv.org/pdf/1703.09307.pdf].
+ """
+ # Initial checks
+ if not isinstance(k, int):
+ raise NetworkXError("k must be an integer.")
+ if not k > 0:
+ raise NetworkXError("k must be greater than 0.")
+ if not is_connected(G):
+ raise NetworkXError("Fluid Communities require connected Graphs.")
+ if len(G) < k:
+ raise NetworkXError("k cannot be bigger than the number of nodes.")
+ # Initialization
+ max_density = 1.0
+ vertices = list(G)
+ seed.shuffle(vertices)
+ communities = {n: i for i, n in enumerate(vertices[:k])}
+ density = {}
+ com_to_numvertices = {}
+ for vertex in communities:
+ com_to_numvertices[communities[vertex]] = 1
+ density[communities[vertex]] = max_density
+ # Set up control variables and start iterating
+ iter_count = 0
+ cont = True
+ while cont:
+ cont = False
+ iter_count += 1
+ # Loop over all vertices in graph in a random order
+ vertices = list(G)
+ seed.shuffle(vertices)
+ for vertex in vertices:
+ # Updating rule
+ com_counter = Counter()
+ # Take into account self vertex community
+ try:
+ com_counter.update({communities[vertex]: density[communities[vertex]]})
+ except KeyError:
+ pass
+ # Gather neighbor vertex communities
+ for v in G[vertex]:
+ try:
+ com_counter.update({communities[v]: density[communities[v]]})
+ except KeyError:
+ continue
+ # Check which is the community with highest density
+ new_com = -1
+ if len(com_counter.keys()) > 0:
+ max_freq = max(com_counter.values())
+ best_communities = [
+ com
+ for com, freq in com_counter.items()
+ if (max_freq - freq) < 0.0001
+ ]
+ # If actual vertex com in best communities, it is preserved
+ try:
+ if communities[vertex] in best_communities:
+ new_com = communities[vertex]
+ except KeyError:
+ pass
+ # If vertex community changes...
+ if new_com == -1:
+ # Set flag of non-convergence
+ cont = True
+ # Randomly chose a new community from candidates
+ new_com = seed.choice(best_communities)
+ # Update previous community status
+ try:
+ com_to_numvertices[communities[vertex]] -= 1
+ density[communities[vertex]] = (
+ max_density / com_to_numvertices[communities[vertex]]
+ )
+ except KeyError:
+ pass
+ # Update new community status
+ communities[vertex] = new_com
+ com_to_numvertices[communities[vertex]] += 1
+ density[communities[vertex]] = (
+ max_density / com_to_numvertices[communities[vertex]]
+ )
+ # If maximum iterations reached --> output actual results
+ if iter_count > max_iter:
+ break
+ # Return results by grouping communities as list of vertices
+ return iter(groups(communities).values())
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/centrality.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/centrality.py
new file mode 100644
index 00000000..43281701
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/centrality.py
@@ -0,0 +1,171 @@
+"""Functions for computing communities based on centrality notions."""
+
+import networkx as nx
+
+__all__ = ["girvan_newman"]
+
+
+@nx._dispatchable(preserve_edge_attrs="most_valuable_edge")
+def girvan_newman(G, most_valuable_edge=None):
+ """Finds communities in a graph using the Girvan–Newman method.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ most_valuable_edge : function
+ Function that takes a graph as input and outputs an edge. The
+ edge returned by this function will be recomputed and removed at
+ each iteration of the algorithm.
+
+ If not specified, the edge with the highest
+ :func:`networkx.edge_betweenness_centrality` will be used.
+
+ Returns
+ -------
+ iterator
+ Iterator over tuples of sets of nodes in `G`. Each set of node
+ is a community, each tuple is a sequence of communities at a
+ particular level of the algorithm.
+
+ Examples
+ --------
+ To get the first pair of communities::
+
+ >>> G = nx.path_graph(10)
+ >>> comp = nx.community.girvan_newman(G)
+ >>> tuple(sorted(c) for c in next(comp))
+ ([0, 1, 2, 3, 4], [5, 6, 7, 8, 9])
+
+ To get only the first *k* tuples of communities, use
+ :func:`itertools.islice`::
+
+ >>> import itertools
+ >>> G = nx.path_graph(8)
+ >>> k = 2
+ >>> comp = nx.community.girvan_newman(G)
+ >>> for communities in itertools.islice(comp, k):
+ ... print(tuple(sorted(c) for c in communities))
+ ...
+ ([0, 1, 2, 3], [4, 5, 6, 7])
+ ([0, 1], [2, 3], [4, 5, 6, 7])
+
+ To stop getting tuples of communities once the number of communities
+ is greater than *k*, use :func:`itertools.takewhile`::
+
+ >>> import itertools
+ >>> G = nx.path_graph(8)
+ >>> k = 4
+ >>> comp = nx.community.girvan_newman(G)
+ >>> limited = itertools.takewhile(lambda c: len(c) <= k, comp)
+ >>> for communities in limited:
+ ... print(tuple(sorted(c) for c in communities))
+ ...
+ ([0, 1, 2, 3], [4, 5, 6, 7])
+ ([0, 1], [2, 3], [4, 5, 6, 7])
+ ([0, 1], [2, 3], [4, 5], [6, 7])
+
+ To just choose an edge to remove based on the weight::
+
+ >>> from operator import itemgetter
+ >>> G = nx.path_graph(10)
+ >>> edges = G.edges()
+ >>> nx.set_edge_attributes(G, {(u, v): v for u, v in edges}, "weight")
+ >>> def heaviest(G):
+ ... u, v, w = max(G.edges(data="weight"), key=itemgetter(2))
+ ... return (u, v)
+ ...
+ >>> comp = nx.community.girvan_newman(G, most_valuable_edge=heaviest)
+ >>> tuple(sorted(c) for c in next(comp))
+ ([0, 1, 2, 3, 4, 5, 6, 7, 8], [9])
+
+ To utilize edge weights when choosing an edge with, for example, the
+ highest betweenness centrality::
+
+ >>> from networkx import edge_betweenness_centrality as betweenness
+ >>> def most_central_edge(G):
+ ... centrality = betweenness(G, weight="weight")
+ ... return max(centrality, key=centrality.get)
+ ...
+ >>> G = nx.path_graph(10)
+ >>> comp = nx.community.girvan_newman(G, most_valuable_edge=most_central_edge)
+ >>> tuple(sorted(c) for c in next(comp))
+ ([0, 1, 2, 3, 4], [5, 6, 7, 8, 9])
+
+ To specify a different ranking algorithm for edges, use the
+ `most_valuable_edge` keyword argument::
+
+ >>> from networkx import edge_betweenness_centrality
+ >>> from random import random
+ >>> def most_central_edge(G):
+ ... centrality = edge_betweenness_centrality(G)
+ ... max_cent = max(centrality.values())
+ ... # Scale the centrality values so they are between 0 and 1,
+ ... # and add some random noise.
+ ... centrality = {e: c / max_cent for e, c in centrality.items()}
+ ... # Add some random noise.
+ ... centrality = {e: c + random() for e, c in centrality.items()}
+ ... return max(centrality, key=centrality.get)
+ ...
+ >>> G = nx.path_graph(10)
+ >>> comp = nx.community.girvan_newman(G, most_valuable_edge=most_central_edge)
+
+ Notes
+ -----
+ The Girvan–Newman algorithm detects communities by progressively
+ removing edges from the original graph. The algorithm removes the
+ "most valuable" edge, traditionally the edge with the highest
+ betweenness centrality, at each step. As the graph breaks down into
+ pieces, the tightly knit community structure is exposed and the
+ result can be depicted as a dendrogram.
+
+ """
+ # If the graph is already empty, simply return its connected
+ # components.
+ if G.number_of_edges() == 0:
+ yield tuple(nx.connected_components(G))
+ return
+ # If no function is provided for computing the most valuable edge,
+ # use the edge betweenness centrality.
+ if most_valuable_edge is None:
+
+ def most_valuable_edge(G):
+ """Returns the edge with the highest betweenness centrality
+ in the graph `G`.
+
+ """
+ # We have guaranteed that the graph is non-empty, so this
+ # dictionary will never be empty.
+ betweenness = nx.edge_betweenness_centrality(G)
+ return max(betweenness, key=betweenness.get)
+
+ # The copy of G here must include the edge weight data.
+ g = G.copy().to_undirected()
+ # Self-loops must be removed because their removal has no effect on
+ # the connected components of the graph.
+ g.remove_edges_from(nx.selfloop_edges(g))
+ while g.number_of_edges() > 0:
+ yield _without_most_central_edges(g, most_valuable_edge)
+
+
+def _without_most_central_edges(G, most_valuable_edge):
+ """Returns the connected components of the graph that results from
+ repeatedly removing the most "valuable" edge in the graph.
+
+ `G` must be a non-empty graph. This function modifies the graph `G`
+ in-place; that is, it removes edges on the graph `G`.
+
+ `most_valuable_edge` is a function that takes the graph `G` as input
+ (or a subgraph with one or more edges of `G` removed) and returns an
+ edge. That edge will be removed and this process will be repeated
+ until the number of connected components in the graph increases.
+
+ """
+ original_num_components = nx.number_connected_components(G)
+ num_new_components = original_num_components
+ while num_new_components <= original_num_components:
+ edge = most_valuable_edge(G)
+ G.remove_edge(*edge)
+ new_components = tuple(nx.connected_components(G))
+ num_new_components = len(new_components)
+ return new_components
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/community_utils.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/community_utils.py
new file mode 100644
index 00000000..ba73a6b3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/community_utils.py
@@ -0,0 +1,30 @@
+"""Helper functions for community-finding algorithms."""
+
+import networkx as nx
+
+__all__ = ["is_partition"]
+
+
+@nx._dispatchable
+def is_partition(G, communities):
+ """Returns *True* if `communities` is a partition of the nodes of `G`.
+
+ A partition of a universe set is a family of pairwise disjoint sets
+ whose union is the entire universe set.
+
+ Parameters
+ ----------
+ G : NetworkX graph.
+
+ communities : list or iterable of sets of nodes
+ If not a list, the iterable is converted internally to a list.
+ If it is an iterator it is exhausted.
+
+ """
+ # Alternate implementation:
+ # return all(sum(1 if v in c else 0 for c in communities) == 1 for v in G)
+ if not isinstance(communities, list):
+ communities = list(communities)
+ nodes = {n for c in communities for n in c if n in G}
+
+ return len(G) == len(nodes) == sum(len(c) for c in communities)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/divisive.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/divisive.py
new file mode 100644
index 00000000..be3c7d86
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/divisive.py
@@ -0,0 +1,216 @@
+import functools
+
+import networkx as nx
+
+__all__ = [
+ "edge_betweenness_partition",
+ "edge_current_flow_betweenness_partition",
+]
+
+
+@nx._dispatchable(edge_attrs="weight")
+def edge_betweenness_partition(G, number_of_sets, *, weight=None):
+ """Partition created by iteratively removing the highest edge betweenness edge.
+
+ This algorithm works by calculating the edge betweenness for all
+ edges and removing the edge with the highest value. It is then
+ determined whether the graph has been broken into at least
+ `number_of_sets` connected components.
+ If not the process is repeated.
+
+ Parameters
+ ----------
+ G : NetworkX Graph, DiGraph or MultiGraph
+ Graph to be partitioned
+
+ number_of_sets : int
+ Number of sets in the desired partition of the graph
+
+ weight : key, optional, default=None
+ The key to use if using weights for edge betweenness calculation
+
+ Returns
+ -------
+ C : list of sets
+ Partition of the nodes of G
+
+ Raises
+ ------
+ NetworkXError
+ If number_of_sets is <= 0 or if number_of_sets > len(G)
+
+ Examples
+ --------
+ >>> G = nx.karate_club_graph()
+ >>> part = nx.community.edge_betweenness_partition(G, 2)
+ >>> {0, 1, 3, 4, 5, 6, 7, 10, 11, 12, 13, 16, 17, 19, 21} in part
+ True
+ >>> {
+ ... 2,
+ ... 8,
+ ... 9,
+ ... 14,
+ ... 15,
+ ... 18,
+ ... 20,
+ ... 22,
+ ... 23,
+ ... 24,
+ ... 25,
+ ... 26,
+ ... 27,
+ ... 28,
+ ... 29,
+ ... 30,
+ ... 31,
+ ... 32,
+ ... 33,
+ ... } in part
+ True
+
+ See Also
+ --------
+ edge_current_flow_betweenness_partition
+
+ Notes
+ -----
+ This algorithm is fairly slow, as both the calculation of connected
+ components and edge betweenness relies on all pairs shortest
+ path algorithms. They could potentially be combined to cut down
+ on overall computation time.
+
+ References
+ ----------
+ .. [1] Santo Fortunato 'Community Detection in Graphs' Physical Reports
+ Volume 486, Issue 3-5 p. 75-174
+ http://arxiv.org/abs/0906.0612
+ """
+ if number_of_sets <= 0:
+ raise nx.NetworkXError("number_of_sets must be >0")
+ if number_of_sets == 1:
+ return [set(G)]
+ if number_of_sets == len(G):
+ return [{n} for n in G]
+ if number_of_sets > len(G):
+ raise nx.NetworkXError("number_of_sets must be <= len(G)")
+
+ H = G.copy()
+ partition = list(nx.connected_components(H))
+ while len(partition) < number_of_sets:
+ ranking = nx.edge_betweenness_centrality(H, weight=weight)
+ edge = max(ranking, key=ranking.get)
+ H.remove_edge(*edge)
+ partition = list(nx.connected_components(H))
+ return partition
+
+
+@nx._dispatchable(edge_attrs="weight")
+def edge_current_flow_betweenness_partition(G, number_of_sets, *, weight=None):
+ """Partition created by removing the highest edge current flow betweenness edge.
+
+ This algorithm works by calculating the edge current flow
+ betweenness for all edges and removing the edge with the
+ highest value. It is then determined whether the graph has
+ been broken into at least `number_of_sets` connected
+ components. If not the process is repeated.
+
+ Parameters
+ ----------
+ G : NetworkX Graph, DiGraph or MultiGraph
+ Graph to be partitioned
+
+ number_of_sets : int
+ Number of sets in the desired partition of the graph
+
+ weight : key, optional (default=None)
+ The edge attribute key to use as weights for
+ edge current flow betweenness calculations
+
+ Returns
+ -------
+ C : list of sets
+ Partition of G
+
+ Raises
+ ------
+ NetworkXError
+ If number_of_sets is <= 0 or number_of_sets > len(G)
+
+ Examples
+ --------
+ >>> G = nx.karate_club_graph()
+ >>> part = nx.community.edge_current_flow_betweenness_partition(G, 2)
+ >>> {0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 16, 17, 19, 21} in part
+ True
+ >>> {8, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33} in part
+ True
+
+
+ See Also
+ --------
+ edge_betweenness_partition
+
+ Notes
+ -----
+ This algorithm is extremely slow, as the recalculation of the edge
+ current flow betweenness is extremely slow.
+
+ References
+ ----------
+ .. [1] Santo Fortunato 'Community Detection in Graphs' Physical Reports
+ Volume 486, Issue 3-5 p. 75-174
+ http://arxiv.org/abs/0906.0612
+ """
+ if number_of_sets <= 0:
+ raise nx.NetworkXError("number_of_sets must be >0")
+ elif number_of_sets == 1:
+ return [set(G)]
+ elif number_of_sets == len(G):
+ return [{n} for n in G]
+ elif number_of_sets > len(G):
+ raise nx.NetworkXError("number_of_sets must be <= len(G)")
+
+ rank = functools.partial(
+ nx.edge_current_flow_betweenness_centrality, normalized=False, weight=weight
+ )
+
+ # current flow requires a connected network so we track the components explicitly
+ H = G.copy()
+ partition = list(nx.connected_components(H))
+ if len(partition) > 1:
+ Hcc_subgraphs = [H.subgraph(cc).copy() for cc in partition]
+ else:
+ Hcc_subgraphs = [H]
+
+ ranking = {}
+ for Hcc in Hcc_subgraphs:
+ ranking.update(rank(Hcc))
+
+ while len(partition) < number_of_sets:
+ edge = max(ranking, key=ranking.get)
+ for cc, Hcc in zip(partition, Hcc_subgraphs):
+ if edge[0] in cc:
+ Hcc.remove_edge(*edge)
+ del ranking[edge]
+ splitcc_list = list(nx.connected_components(Hcc))
+ if len(splitcc_list) > 1:
+ # there are 2 connected components. split off smaller one
+ cc_new = min(splitcc_list, key=len)
+ Hcc_new = Hcc.subgraph(cc_new).copy()
+ # update edge rankings for Hcc_new
+ newranks = rank(Hcc_new)
+ for e, r in newranks.items():
+ ranking[e if e in ranking else e[::-1]] = r
+ # append new cc and Hcc to their lists.
+ partition.append(cc_new)
+ Hcc_subgraphs.append(Hcc_new)
+
+ # leave existing cc and Hcc in their lists, but shrink them
+ Hcc.remove_nodes_from(cc_new)
+ cc.difference_update(cc_new)
+ # update edge rankings for Hcc whether it was split or not
+ newranks = rank(Hcc)
+ for e, r in newranks.items():
+ ranking[e if e in ranking else e[::-1]] = r
+ break
+ return partition
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/kclique.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/kclique.py
new file mode 100644
index 00000000..c7249104
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/kclique.py
@@ -0,0 +1,79 @@
+from collections import defaultdict
+
+import networkx as nx
+
+__all__ = ["k_clique_communities"]
+
+
+@nx._dispatchable
+def k_clique_communities(G, k, cliques=None):
+ """Find k-clique communities in graph using the percolation method.
+
+ A k-clique community is the union of all cliques of size k that
+ can be reached through adjacent (sharing k-1 nodes) k-cliques.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ k : int
+ Size of smallest clique
+
+ cliques: list or generator
+ Precomputed cliques (use networkx.find_cliques(G))
+
+ Returns
+ -------
+ Yields sets of nodes, one for each k-clique community.
+
+ Examples
+ --------
+ >>> G = nx.complete_graph(5)
+ >>> K5 = nx.convert_node_labels_to_integers(G, first_label=2)
+ >>> G.add_edges_from(K5.edges())
+ >>> c = list(nx.community.k_clique_communities(G, 4))
+ >>> sorted(list(c[0]))
+ [0, 1, 2, 3, 4, 5, 6]
+ >>> list(nx.community.k_clique_communities(G, 6))
+ []
+
+ References
+ ----------
+ .. [1] Gergely Palla, Imre Derényi, Illés Farkas1, and Tamás Vicsek,
+ Uncovering the overlapping community structure of complex networks
+ in nature and society Nature 435, 814-818, 2005,
+ doi:10.1038/nature03607
+ """
+ if k < 2:
+ raise nx.NetworkXError(f"k={k}, k must be greater than 1.")
+ if cliques is None:
+ cliques = nx.find_cliques(G)
+ cliques = [frozenset(c) for c in cliques if len(c) >= k]
+
+ # First index which nodes are in which cliques
+ membership_dict = defaultdict(list)
+ for clique in cliques:
+ for node in clique:
+ membership_dict[node].append(clique)
+
+ # For each clique, see which adjacent cliques percolate
+ perc_graph = nx.Graph()
+ perc_graph.add_nodes_from(cliques)
+ for clique in cliques:
+ for adj_clique in _get_adjacent_cliques(clique, membership_dict):
+ if len(clique.intersection(adj_clique)) >= (k - 1):
+ perc_graph.add_edge(clique, adj_clique)
+
+ # Connected components of clique graph with perc edges
+ # are the percolated cliques
+ for component in nx.connected_components(perc_graph):
+ yield (frozenset.union(*component))
+
+
+def _get_adjacent_cliques(clique, membership_dict):
+ adjacent_cliques = set()
+ for n in clique:
+ for adj_clique in membership_dict[n]:
+ if clique != adj_clique:
+ adjacent_cliques.add(adj_clique)
+ return adjacent_cliques
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/kernighan_lin.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/kernighan_lin.py
new file mode 100644
index 00000000..f6397d82
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/kernighan_lin.py
@@ -0,0 +1,139 @@
+"""Functions for computing the Kernighan–Lin bipartition algorithm."""
+
+from itertools import count
+
+import networkx as nx
+from networkx.algorithms.community.community_utils import is_partition
+from networkx.utils import BinaryHeap, not_implemented_for, py_random_state
+
+__all__ = ["kernighan_lin_bisection"]
+
+
+def _kernighan_lin_sweep(edges, side):
+ """
+ This is a modified form of Kernighan-Lin, which moves single nodes at a
+ time, alternating between sides to keep the bisection balanced. We keep
+ two min-heaps of swap costs to make optimal-next-move selection fast.
+ """
+ costs0, costs1 = costs = BinaryHeap(), BinaryHeap()
+ for u, side_u, edges_u in zip(count(), side, edges):
+ cost_u = sum(w if side[v] else -w for v, w in edges_u)
+ costs[side_u].insert(u, cost_u if side_u else -cost_u)
+
+ def _update_costs(costs_x, x):
+ for y, w in edges[x]:
+ costs_y = costs[side[y]]
+ cost_y = costs_y.get(y)
+ if cost_y is not None:
+ cost_y += 2 * (-w if costs_x is costs_y else w)
+ costs_y.insert(y, cost_y, True)
+
+ i = 0
+ totcost = 0
+ while costs0 and costs1:
+ u, cost_u = costs0.pop()
+ _update_costs(costs0, u)
+ v, cost_v = costs1.pop()
+ _update_costs(costs1, v)
+ totcost += cost_u + cost_v
+ i += 1
+ yield totcost, i, (u, v)
+
+
+@not_implemented_for("directed")
+@py_random_state(4)
+@nx._dispatchable(edge_attrs="weight")
+def kernighan_lin_bisection(G, partition=None, max_iter=10, weight="weight", seed=None):
+ """Partition a graph into two blocks using the Kernighan–Lin
+ algorithm.
+
+ This algorithm partitions a network into two sets by iteratively
+ swapping pairs of nodes to reduce the edge cut between the two sets. The
+ pairs are chosen according to a modified form of Kernighan-Lin [1]_, which
+ moves node individually, alternating between sides to keep the bisection
+ balanced.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Graph must be undirected.
+
+ partition : tuple
+ Pair of iterables containing an initial partition. If not
+ specified, a random balanced partition is used.
+
+ max_iter : int
+ Maximum number of times to attempt swaps to find an
+ improvement before giving up.
+
+ weight : key
+ Edge data key to use as weight. If None, the weights are all
+ set to one.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+ Only used if partition is None
+
+ Returns
+ -------
+ partition : tuple
+ A pair of sets of nodes representing the bipartition.
+
+ Raises
+ ------
+ NetworkXError
+ If partition is not a valid partition of the nodes of the graph.
+
+ References
+ ----------
+ .. [1] Kernighan, B. W.; Lin, Shen (1970).
+ "An efficient heuristic procedure for partitioning graphs."
+ *Bell Systems Technical Journal* 49: 291--307.
+ Oxford University Press 2011.
+
+ """
+ n = len(G)
+ labels = list(G)
+ seed.shuffle(labels)
+ index = {v: i for i, v in enumerate(labels)}
+
+ if partition is None:
+ side = [0] * (n // 2) + [1] * ((n + 1) // 2)
+ else:
+ try:
+ A, B = partition
+ except (TypeError, ValueError) as err:
+ raise nx.NetworkXError("partition must be two sets") from err
+ if not is_partition(G, (A, B)):
+ raise nx.NetworkXError("partition invalid")
+ side = [0] * n
+ for a in A:
+ side[index[a]] = 1
+
+ if G.is_multigraph():
+ edges = [
+ [
+ (index[u], sum(e.get(weight, 1) for e in d.values()))
+ for u, d in G[v].items()
+ ]
+ for v in labels
+ ]
+ else:
+ edges = [
+ [(index[u], e.get(weight, 1)) for u, e in G[v].items()] for v in labels
+ ]
+
+ for i in range(max_iter):
+ costs = list(_kernighan_lin_sweep(edges, side))
+ min_cost, min_i, _ = min(costs)
+ if min_cost >= 0:
+ break
+
+ for _, _, (u, v) in costs[:min_i]:
+ side[u] = 1
+ side[v] = 0
+
+ A = {u for u, s in zip(labels, side) if s == 0}
+ B = {u for u, s in zip(labels, side) if s == 1}
+ return A, B
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/label_propagation.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/label_propagation.py
new file mode 100644
index 00000000..74880286
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/label_propagation.py
@@ -0,0 +1,338 @@
+"""
+Label propagation community detection algorithms.
+"""
+
+from collections import Counter, defaultdict, deque
+
+import networkx as nx
+from networkx.utils import groups, not_implemented_for, py_random_state
+
+__all__ = [
+ "label_propagation_communities",
+ "asyn_lpa_communities",
+ "fast_label_propagation_communities",
+]
+
+
+@py_random_state("seed")
+@nx._dispatchable(edge_attrs="weight")
+def fast_label_propagation_communities(G, *, weight=None, seed=None):
+ """Returns communities in `G` as detected by fast label propagation.
+
+ The fast label propagation algorithm is described in [1]_. The algorithm is
+ probabilistic and the found communities may vary in different executions.
+
+ The algorithm operates as follows. First, the community label of each node is
+ set to a unique label. The algorithm then repeatedly updates the labels of
+ the nodes to the most frequent label in their neighborhood. In case of ties,
+ a random label is chosen from the most frequent labels.
+
+ The algorithm maintains a queue of nodes that still need to be processed.
+ Initially, all nodes are added to the queue in a random order. Then the nodes
+ are removed from the queue one by one and processed. If a node updates its label,
+ all its neighbors that have a different label are added to the queue (if not
+ already in the queue). The algorithm stops when the queue is empty.
+
+ Parameters
+ ----------
+ G : Graph, DiGraph, MultiGraph, or MultiDiGraph
+ Any NetworkX graph.
+
+ weight : string, or None (default)
+ The edge attribute representing a non-negative weight of an edge. If None,
+ each edge is assumed to have weight one. The weight of an edge is used in
+ determining the frequency with which a label appears among the neighbors of
+ a node (edge with weight `w` is equivalent to `w` unweighted edges).
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state. See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ communities : iterable
+ Iterable of communities given as sets of nodes.
+
+ Notes
+ -----
+ Edge directions are ignored for directed graphs.
+ Edge weights must be non-negative numbers.
+
+ References
+ ----------
+ .. [1] Vincent A. Traag & Lovro Šubelj. "Large network community detection by
+ fast label propagation." Scientific Reports 13 (2023): 2701.
+ https://doi.org/10.1038/s41598-023-29610-z
+ """
+
+ # Queue of nodes to be processed.
+ nodes_queue = deque(G)
+ seed.shuffle(nodes_queue)
+
+ # Set of nodes in the queue.
+ nodes_set = set(G)
+
+ # Assign unique label to each node.
+ comms = {node: i for i, node in enumerate(G)}
+
+ while nodes_queue:
+ # Remove next node from the queue to process.
+ node = nodes_queue.popleft()
+ nodes_set.remove(node)
+
+ # Isolated nodes retain their initial label.
+ if G.degree(node) > 0:
+ # Compute frequency of labels in node's neighborhood.
+ label_freqs = _fast_label_count(G, comms, node, weight)
+ max_freq = max(label_freqs.values())
+
+ # Always sample new label from most frequent labels.
+ comm = seed.choice(
+ [comm for comm in label_freqs if label_freqs[comm] == max_freq]
+ )
+
+ if comms[node] != comm:
+ comms[node] = comm
+
+ # Add neighbors that have different label to the queue.
+ for nbr in nx.all_neighbors(G, node):
+ if comms[nbr] != comm and nbr not in nodes_set:
+ nodes_queue.append(nbr)
+ nodes_set.add(nbr)
+
+ yield from groups(comms).values()
+
+
+def _fast_label_count(G, comms, node, weight=None):
+ """Computes the frequency of labels in the neighborhood of a node.
+
+ Returns a dictionary keyed by label to the frequency of that label.
+ """
+
+ if weight is None:
+ # Unweighted (un)directed simple graph.
+ if not G.is_multigraph():
+ label_freqs = Counter(map(comms.get, nx.all_neighbors(G, node)))
+
+ # Unweighted (un)directed multigraph.
+ else:
+ label_freqs = defaultdict(int)
+ for nbr in G[node]:
+ label_freqs[comms[nbr]] += len(G[node][nbr])
+
+ if G.is_directed():
+ for nbr in G.pred[node]:
+ label_freqs[comms[nbr]] += len(G.pred[node][nbr])
+
+ else:
+ # Weighted undirected simple/multigraph.
+ label_freqs = defaultdict(float)
+ for _, nbr, w in G.edges(node, data=weight, default=1):
+ label_freqs[comms[nbr]] += w
+
+ # Weighted directed simple/multigraph.
+ if G.is_directed():
+ for nbr, _, w in G.in_edges(node, data=weight, default=1):
+ label_freqs[comms[nbr]] += w
+
+ return label_freqs
+
+
+@py_random_state(2)
+@nx._dispatchable(edge_attrs="weight")
+def asyn_lpa_communities(G, weight=None, seed=None):
+ """Returns communities in `G` as detected by asynchronous label
+ propagation.
+
+ The asynchronous label propagation algorithm is described in
+ [1]_. The algorithm is probabilistic and the found communities may
+ vary on different executions.
+
+ The algorithm proceeds as follows. After initializing each node with
+ a unique label, the algorithm repeatedly sets the label of a node to
+ be the label that appears most frequently among that nodes
+ neighbors. The algorithm halts when each node has the label that
+ appears most frequently among its neighbors. The algorithm is
+ asynchronous because each node is updated without waiting for
+ updates on the remaining nodes.
+
+ This generalized version of the algorithm in [1]_ accepts edge
+ weights.
+
+ Parameters
+ ----------
+ G : Graph
+
+ weight : string
+ The edge attribute representing the weight of an edge.
+ If None, each edge is assumed to have weight one. In this
+ algorithm, the weight of an edge is used in determining the
+ frequency with which a label appears among the neighbors of a
+ node: a higher weight means the label appears more often.
+
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ communities : iterable
+ Iterable of communities given as sets of nodes.
+
+ Notes
+ -----
+ Edge weight attributes must be numerical.
+
+ References
+ ----------
+ .. [1] Raghavan, Usha Nandini, Réka Albert, and Soundar Kumara. "Near
+ linear time algorithm to detect community structures in large-scale
+ networks." Physical Review E 76.3 (2007): 036106.
+ """
+
+ labels = {n: i for i, n in enumerate(G)}
+ cont = True
+
+ while cont:
+ cont = False
+ nodes = list(G)
+ seed.shuffle(nodes)
+
+ for node in nodes:
+ if not G[node]:
+ continue
+
+ # Get label frequencies among adjacent nodes.
+ # Depending on the order they are processed in,
+ # some nodes will be in iteration t and others in t-1,
+ # making the algorithm asynchronous.
+ if weight is None:
+ # initialising a Counter from an iterator of labels is
+ # faster for getting unweighted label frequencies
+ label_freq = Counter(map(labels.get, G[node]))
+ else:
+ # updating a defaultdict is substantially faster
+ # for getting weighted label frequencies
+ label_freq = defaultdict(float)
+ for _, v, wt in G.edges(node, data=weight, default=1):
+ label_freq[labels[v]] += wt
+
+ # Get the labels that appear with maximum frequency.
+ max_freq = max(label_freq.values())
+ best_labels = [
+ label for label, freq in label_freq.items() if freq == max_freq
+ ]
+
+ # If the node does not have one of the maximum frequency labels,
+ # randomly choose one of them and update the node's label.
+ # Continue the iteration as long as at least one node
+ # doesn't have a maximum frequency label.
+ if labels[node] not in best_labels:
+ labels[node] = seed.choice(best_labels)
+ cont = True
+
+ yield from groups(labels).values()
+
+
+@not_implemented_for("directed")
+@nx._dispatchable
+def label_propagation_communities(G):
+ """Generates community sets determined by label propagation
+
+ Finds communities in `G` using a semi-synchronous label propagation
+ method [1]_. This method combines the advantages of both the synchronous
+ and asynchronous models. Not implemented for directed graphs.
+
+ Parameters
+ ----------
+ G : graph
+ An undirected NetworkX graph.
+
+ Returns
+ -------
+ communities : iterable
+ A dict_values object that contains a set of nodes for each community.
+
+ Raises
+ ------
+ NetworkXNotImplemented
+ If the graph is directed
+
+ References
+ ----------
+ .. [1] Cordasco, G., & Gargano, L. (2010, December). Community detection
+ via semi-synchronous label propagation algorithms. In Business
+ Applications of Social Network Analysis (BASNA), 2010 IEEE International
+ Workshop on (pp. 1-8). IEEE.
+ """
+ coloring = _color_network(G)
+ # Create a unique label for each node in the graph
+ labeling = {v: k for k, v in enumerate(G)}
+ while not _labeling_complete(labeling, G):
+ # Update the labels of every node with the same color.
+ for color, nodes in coloring.items():
+ for n in nodes:
+ _update_label(n, labeling, G)
+
+ clusters = defaultdict(set)
+ for node, label in labeling.items():
+ clusters[label].add(node)
+ return clusters.values()
+
+
+def _color_network(G):
+ """Colors the network so that neighboring nodes all have distinct colors.
+
+ Returns a dict keyed by color to a set of nodes with that color.
+ """
+ coloring = {} # color => set(node)
+ colors = nx.coloring.greedy_color(G)
+ for node, color in colors.items():
+ if color in coloring:
+ coloring[color].add(node)
+ else:
+ coloring[color] = {node}
+ return coloring
+
+
+def _labeling_complete(labeling, G):
+ """Determines whether or not LPA is done.
+
+ Label propagation is complete when all nodes have a label that is
+ in the set of highest frequency labels amongst its neighbors.
+
+ Nodes with no neighbors are considered complete.
+ """
+ return all(
+ labeling[v] in _most_frequent_labels(v, labeling, G) for v in G if len(G[v]) > 0
+ )
+
+
+def _most_frequent_labels(node, labeling, G):
+ """Returns a set of all labels with maximum frequency in `labeling`.
+
+ Input `labeling` should be a dict keyed by node to labels.
+ """
+ if not G[node]:
+ # Nodes with no neighbors are themselves a community and are labeled
+ # accordingly, hence the immediate if statement.
+ return {labeling[node]}
+
+ # Compute the frequencies of all neighbors of node
+ freqs = Counter(labeling[q] for q in G[node])
+ max_freq = max(freqs.values())
+ return {label for label, freq in freqs.items() if freq == max_freq}
+
+
+def _update_label(node, labeling, G):
+ """Updates the label of a node using the Prec-Max tie breaking algorithm
+
+ The algorithm is explained in: 'Community Detection via Semi-Synchronous
+ Label Propagation Algorithms' Cordasco and Gargano, 2011
+ """
+ high_labels = _most_frequent_labels(node, labeling, G)
+ if len(high_labels) == 1:
+ labeling[node] = high_labels.pop()
+ elif len(high_labels) > 1:
+ # Prec-Max
+ if labeling[node] not in high_labels:
+ labeling[node] = max(high_labels)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/louvain.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/louvain.py
new file mode 100644
index 00000000..959c93a5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/louvain.py
@@ -0,0 +1,382 @@
+"""Function for detecting communities based on Louvain Community Detection
+Algorithm"""
+
+import itertools
+from collections import defaultdict, deque
+
+import networkx as nx
+from networkx.algorithms.community import modularity
+from networkx.utils import py_random_state
+
+__all__ = ["louvain_communities", "louvain_partitions"]
+
+
+@py_random_state("seed")
+@nx._dispatchable(edge_attrs="weight")
+def louvain_communities(
+ G, weight="weight", resolution=1, threshold=0.0000001, max_level=None, seed=None
+):
+ r"""Find the best partition of a graph using the Louvain Community Detection
+ Algorithm.
+
+ Louvain Community Detection Algorithm is a simple method to extract the community
+ structure of a network. This is a heuristic method based on modularity optimization. [1]_
+
+ The algorithm works in 2 steps. On the first step it assigns every node to be
+ in its own community and then for each node it tries to find the maximum positive
+ modularity gain by moving each node to all of its neighbor communities. If no positive
+ gain is achieved the node remains in its original community.
+
+ The modularity gain obtained by moving an isolated node $i$ into a community $C$ can
+ easily be calculated by the following formula (combining [1]_ [2]_ and some algebra):
+
+ .. math::
+ \Delta Q = \frac{k_{i,in}}{2m} - \gamma\frac{ \Sigma_{tot} \cdot k_i}{2m^2}
+
+ where $m$ is the size of the graph, $k_{i,in}$ is the sum of the weights of the links
+ from $i$ to nodes in $C$, $k_i$ is the sum of the weights of the links incident to node $i$,
+ $\Sigma_{tot}$ is the sum of the weights of the links incident to nodes in $C$ and $\gamma$
+ is the resolution parameter.
+
+ For the directed case the modularity gain can be computed using this formula according to [3]_
+
+ .. math::
+ \Delta Q = \frac{k_{i,in}}{m}
+ - \gamma\frac{k_i^{out} \cdot\Sigma_{tot}^{in} + k_i^{in} \cdot \Sigma_{tot}^{out}}{m^2}
+
+ where $k_i^{out}$, $k_i^{in}$ are the outer and inner weighted degrees of node $i$ and
+ $\Sigma_{tot}^{in}$, $\Sigma_{tot}^{out}$ are the sum of in-going and out-going links incident
+ to nodes in $C$.
+
+ The first phase continues until no individual move can improve the modularity.
+
+ The second phase consists in building a new network whose nodes are now the communities
+ found in the first phase. To do so, the weights of the links between the new nodes are given by
+ the sum of the weight of the links between nodes in the corresponding two communities. Once this
+ phase is complete it is possible to reapply the first phase creating bigger communities with
+ increased modularity.
+
+ The above two phases are executed until no modularity gain is achieved (or is less than
+ the `threshold`, or until `max_levels` is reached).
+
+ Be careful with self-loops in the input graph. These are treated as
+ previously reduced communities -- as if the process had been started
+ in the middle of the algorithm. Large self-loop edge weights thus
+ represent strong communities and in practice may be hard to add
+ other nodes to. If your input graph edge weights for self-loops
+ do not represent already reduced communities you may want to remove
+ the self-loops before inputting that graph.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ weight : string or None, optional (default="weight")
+ The name of an edge attribute that holds the numerical value
+ used as a weight. If None then each edge has weight 1.
+ resolution : float, optional (default=1)
+ If resolution is less than 1, the algorithm favors larger communities.
+ Greater than 1 favors smaller communities
+ threshold : float, optional (default=0.0000001)
+ Modularity gain threshold for each level. If the gain of modularity
+ between 2 levels of the algorithm is less than the given threshold
+ then the algorithm stops and returns the resulting communities.
+ max_level : int or None, optional (default=None)
+ The maximum number of levels (steps of the algorithm) to compute.
+ Must be a positive integer or None. If None, then there is no max
+ level and the threshold parameter determines the stopping condition.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Returns
+ -------
+ list
+ A list of sets (partition of `G`). Each set represents one community and contains
+ all the nodes that constitute it.
+
+ Examples
+ --------
+ >>> import networkx as nx
+ >>> G = nx.petersen_graph()
+ >>> nx.community.louvain_communities(G, seed=123)
+ [{0, 4, 5, 7, 9}, {1, 2, 3, 6, 8}]
+
+ Notes
+ -----
+ The order in which the nodes are considered can affect the final output. In the algorithm
+ the ordering happens using a random shuffle.
+
+ References
+ ----------
+ .. [1] Blondel, V.D. et al. Fast unfolding of communities in
+ large networks. J. Stat. Mech 10008, 1-12(2008). https://doi.org/10.1088/1742-5468/2008/10/P10008
+ .. [2] Traag, V.A., Waltman, L. & van Eck, N.J. From Louvain to Leiden: guaranteeing
+ well-connected communities. Sci Rep 9, 5233 (2019). https://doi.org/10.1038/s41598-019-41695-z
+ .. [3] Nicolas Dugué, Anthony Perez. Directed Louvain : maximizing modularity in directed networks.
+ [Research Report] Université d’Orléans. 2015. hal-01231784. https://hal.archives-ouvertes.fr/hal-01231784
+
+ See Also
+ --------
+ louvain_partitions
+ """
+
+ partitions = louvain_partitions(G, weight, resolution, threshold, seed)
+ if max_level is not None:
+ if max_level <= 0:
+ raise ValueError("max_level argument must be a positive integer or None")
+ partitions = itertools.islice(partitions, max_level)
+ final_partition = deque(partitions, maxlen=1)
+ return final_partition.pop()
+
+
+@py_random_state("seed")
+@nx._dispatchable(edge_attrs="weight")
+def louvain_partitions(
+ G, weight="weight", resolution=1, threshold=0.0000001, seed=None
+):
+ """Yields partitions for each level of the Louvain Community Detection Algorithm
+
+ Louvain Community Detection Algorithm is a simple method to extract the community
+ structure of a network. This is a heuristic method based on modularity optimization. [1]_
+
+ The partitions at each level (step of the algorithm) form a dendrogram of communities.
+ A dendrogram is a diagram representing a tree and each level represents
+ a partition of the G graph. The top level contains the smallest communities
+ and as you traverse to the bottom of the tree the communities get bigger
+ and the overall modularity increases making the partition better.
+
+ Each level is generated by executing the two phases of the Louvain Community
+ Detection Algorithm.
+
+ Be careful with self-loops in the input graph. These are treated as
+ previously reduced communities -- as if the process had been started
+ in the middle of the algorithm. Large self-loop edge weights thus
+ represent strong communities and in practice may be hard to add
+ other nodes to. If your input graph edge weights for self-loops
+ do not represent already reduced communities you may want to remove
+ the self-loops before inputting that graph.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ weight : string or None, optional (default="weight")
+ The name of an edge attribute that holds the numerical value
+ used as a weight. If None then each edge has weight 1.
+ resolution : float, optional (default=1)
+ If resolution is less than 1, the algorithm favors larger communities.
+ Greater than 1 favors smaller communities
+ threshold : float, optional (default=0.0000001)
+ Modularity gain threshold for each level. If the gain of modularity
+ between 2 levels of the algorithm is less than the given threshold
+ then the algorithm stops and returns the resulting communities.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ Yields
+ ------
+ list
+ A list of sets (partition of `G`). Each set represents one community and contains
+ all the nodes that constitute it.
+
+ References
+ ----------
+ .. [1] Blondel, V.D. et al. Fast unfolding of communities in
+ large networks. J. Stat. Mech 10008, 1-12(2008)
+
+ See Also
+ --------
+ louvain_communities
+ """
+
+ partition = [{u} for u in G.nodes()]
+ if nx.is_empty(G):
+ yield partition
+ return
+ mod = modularity(G, partition, resolution=resolution, weight=weight)
+ is_directed = G.is_directed()
+ if G.is_multigraph():
+ graph = _convert_multigraph(G, weight, is_directed)
+ else:
+ graph = G.__class__()
+ graph.add_nodes_from(G)
+ graph.add_weighted_edges_from(G.edges(data=weight, default=1))
+
+ m = graph.size(weight="weight")
+ partition, inner_partition, improvement = _one_level(
+ graph, m, partition, resolution, is_directed, seed
+ )
+ improvement = True
+ while improvement:
+ # gh-5901 protect the sets in the yielded list from further manipulation here
+ yield [s.copy() for s in partition]
+ new_mod = modularity(
+ graph, inner_partition, resolution=resolution, weight="weight"
+ )
+ if new_mod - mod <= threshold:
+ return
+ mod = new_mod
+ graph = _gen_graph(graph, inner_partition)
+ partition, inner_partition, improvement = _one_level(
+ graph, m, partition, resolution, is_directed, seed
+ )
+
+
+def _one_level(G, m, partition, resolution=1, is_directed=False, seed=None):
+ """Calculate one level of the Louvain partitions tree
+
+ Parameters
+ ----------
+ G : NetworkX Graph/DiGraph
+ The graph from which to detect communities
+ m : number
+ The size of the graph `G`.
+ partition : list of sets of nodes
+ A valid partition of the graph `G`
+ resolution : positive number
+ The resolution parameter for computing the modularity of a partition
+ is_directed : bool
+ True if `G` is a directed graph.
+ seed : integer, random_state, or None (default)
+ Indicator of random number generation state.
+ See :ref:`Randomness<randomness>`.
+
+ """
+ node2com = {u: i for i, u in enumerate(G.nodes())}
+ inner_partition = [{u} for u in G.nodes()]
+ if is_directed:
+ in_degrees = dict(G.in_degree(weight="weight"))
+ out_degrees = dict(G.out_degree(weight="weight"))
+ Stot_in = list(in_degrees.values())
+ Stot_out = list(out_degrees.values())
+ # Calculate weights for both in and out neighbors without considering self-loops
+ nbrs = {}
+ for u in G:
+ nbrs[u] = defaultdict(float)
+ for _, n, wt in G.out_edges(u, data="weight"):
+ if u != n:
+ nbrs[u][n] += wt
+ for n, _, wt in G.in_edges(u, data="weight"):
+ if u != n:
+ nbrs[u][n] += wt
+ else:
+ degrees = dict(G.degree(weight="weight"))
+ Stot = list(degrees.values())
+ nbrs = {u: {v: data["weight"] for v, data in G[u].items() if v != u} for u in G}
+ rand_nodes = list(G.nodes)
+ seed.shuffle(rand_nodes)
+ nb_moves = 1
+ improvement = False
+ while nb_moves > 0:
+ nb_moves = 0
+ for u in rand_nodes:
+ best_mod = 0
+ best_com = node2com[u]
+ weights2com = _neighbor_weights(nbrs[u], node2com)
+ if is_directed:
+ in_degree = in_degrees[u]
+ out_degree = out_degrees[u]
+ Stot_in[best_com] -= in_degree
+ Stot_out[best_com] -= out_degree
+ remove_cost = (
+ -weights2com[best_com] / m
+ + resolution
+ * (out_degree * Stot_in[best_com] + in_degree * Stot_out[best_com])
+ / m**2
+ )
+ else:
+ degree = degrees[u]
+ Stot[best_com] -= degree
+ remove_cost = -weights2com[best_com] / m + resolution * (
+ Stot[best_com] * degree
+ ) / (2 * m**2)
+ for nbr_com, wt in weights2com.items():
+ if is_directed:
+ gain = (
+ remove_cost
+ + wt / m
+ - resolution
+ * (
+ out_degree * Stot_in[nbr_com]
+ + in_degree * Stot_out[nbr_com]
+ )
+ / m**2
+ )
+ else:
+ gain = (
+ remove_cost
+ + wt / m
+ - resolution * (Stot[nbr_com] * degree) / (2 * m**2)
+ )
+ if gain > best_mod:
+ best_mod = gain
+ best_com = nbr_com
+ if is_directed:
+ Stot_in[best_com] += in_degree
+ Stot_out[best_com] += out_degree
+ else:
+ Stot[best_com] += degree
+ if best_com != node2com[u]:
+ com = G.nodes[u].get("nodes", {u})
+ partition[node2com[u]].difference_update(com)
+ inner_partition[node2com[u]].remove(u)
+ partition[best_com].update(com)
+ inner_partition[best_com].add(u)
+ improvement = True
+ nb_moves += 1
+ node2com[u] = best_com
+ partition = list(filter(len, partition))
+ inner_partition = list(filter(len, inner_partition))
+ return partition, inner_partition, improvement
+
+
+def _neighbor_weights(nbrs, node2com):
+ """Calculate weights between node and its neighbor communities.
+
+ Parameters
+ ----------
+ nbrs : dictionary
+ Dictionary with nodes' neighbors as keys and their edge weight as value.
+ node2com : dictionary
+ Dictionary with all graph's nodes as keys and their community index as value.
+
+ """
+ weights = defaultdict(float)
+ for nbr, wt in nbrs.items():
+ weights[node2com[nbr]] += wt
+ return weights
+
+
+def _gen_graph(G, partition):
+ """Generate a new graph based on the partitions of a given graph"""
+ H = G.__class__()
+ node2com = {}
+ for i, part in enumerate(partition):
+ nodes = set()
+ for node in part:
+ node2com[node] = i
+ nodes.update(G.nodes[node].get("nodes", {node}))
+ H.add_node(i, nodes=nodes)
+
+ for node1, node2, wt in G.edges(data=True):
+ wt = wt["weight"]
+ com1 = node2com[node1]
+ com2 = node2com[node2]
+ temp = H.get_edge_data(com1, com2, {"weight": 0})["weight"]
+ H.add_edge(com1, com2, weight=wt + temp)
+ return H
+
+
+def _convert_multigraph(G, weight, is_directed):
+ """Convert a Multigraph to normal Graph"""
+ if is_directed:
+ H = nx.DiGraph()
+ else:
+ H = nx.Graph()
+ H.add_nodes_from(G)
+ for u, v, wt in G.edges(data=weight, default=1):
+ if H.has_edge(u, v):
+ H[u][v]["weight"] += wt
+ else:
+ H.add_edge(u, v, weight=wt)
+ return H
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/lukes.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/lukes.py
new file mode 100644
index 00000000..08dd7cd5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/lukes.py
@@ -0,0 +1,227 @@
+"""Lukes Algorithm for exact optimal weighted tree partitioning."""
+
+from copy import deepcopy
+from functools import lru_cache
+from random import choice
+
+import networkx as nx
+from networkx.utils import not_implemented_for
+
+__all__ = ["lukes_partitioning"]
+
+D_EDGE_W = "weight"
+D_EDGE_VALUE = 1.0
+D_NODE_W = "weight"
+D_NODE_VALUE = 1
+PKEY = "partitions"
+CLUSTER_EVAL_CACHE_SIZE = 2048
+
+
+def _split_n_from(n, min_size_of_first_part):
+ # splits j in two parts of which the first is at least
+ # the second argument
+ assert n >= min_size_of_first_part
+ for p1 in range(min_size_of_first_part, n + 1):
+ yield p1, n - p1
+
+
+@nx._dispatchable(node_attrs="node_weight", edge_attrs="edge_weight")
+def lukes_partitioning(G, max_size, node_weight=None, edge_weight=None):
+ """Optimal partitioning of a weighted tree using the Lukes algorithm.
+
+ This algorithm partitions a connected, acyclic graph featuring integer
+ node weights and float edge weights. The resulting clusters are such
+ that the total weight of the nodes in each cluster does not exceed
+ max_size and that the weight of the edges that are cut by the partition
+ is minimum. The algorithm is based on [1]_.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ max_size : int
+ Maximum weight a partition can have in terms of sum of
+ node_weight for all nodes in the partition
+
+ edge_weight : key
+ Edge data key to use as weight. If None, the weights are all
+ set to one.
+
+ node_weight : key
+ Node data key to use as weight. If None, the weights are all
+ set to one. The data must be int.
+
+ Returns
+ -------
+ partition : list
+ A list of sets of nodes representing the clusters of the
+ partition.
+
+ Raises
+ ------
+ NotATree
+ If G is not a tree.
+ TypeError
+ If any of the values of node_weight is not int.
+
+ References
+ ----------
+ .. [1] Lukes, J. A. (1974).
+ "Efficient Algorithm for the Partitioning of Trees."
+ IBM Journal of Research and Development, 18(3), 217–224.
+
+ """
+ # First sanity check and tree preparation
+ if not nx.is_tree(G):
+ raise nx.NotATree("lukes_partitioning works only on trees")
+ else:
+ if nx.is_directed(G):
+ root = [n for n, d in G.in_degree() if d == 0]
+ assert len(root) == 1
+ root = root[0]
+ t_G = deepcopy(G)
+ else:
+ root = choice(list(G.nodes))
+ # this has the desirable side effect of not inheriting attributes
+ t_G = nx.dfs_tree(G, root)
+
+ # Since we do not want to screw up the original graph,
+ # if we have a blank attribute, we make a deepcopy
+ if edge_weight is None or node_weight is None:
+ safe_G = deepcopy(G)
+ if edge_weight is None:
+ nx.set_edge_attributes(safe_G, D_EDGE_VALUE, D_EDGE_W)
+ edge_weight = D_EDGE_W
+ if node_weight is None:
+ nx.set_node_attributes(safe_G, D_NODE_VALUE, D_NODE_W)
+ node_weight = D_NODE_W
+ else:
+ safe_G = G
+
+ # Second sanity check
+ # The values of node_weight MUST BE int.
+ # I cannot see any room for duck typing without incurring serious
+ # danger of subtle bugs.
+ all_n_attr = nx.get_node_attributes(safe_G, node_weight).values()
+ for x in all_n_attr:
+ if not isinstance(x, int):
+ raise TypeError(
+ "lukes_partitioning needs integer "
+ f"values for node_weight ({node_weight})"
+ )
+
+ # SUBROUTINES -----------------------
+ # these functions are defined here for two reasons:
+ # - brevity: we can leverage global "safe_G"
+ # - caching: signatures are hashable
+
+ @not_implemented_for("undirected")
+ # this is intended to be called only on t_G
+ def _leaves(gr):
+ for x in gr.nodes:
+ if not nx.descendants(gr, x):
+ yield x
+
+ @not_implemented_for("undirected")
+ def _a_parent_of_leaves_only(gr):
+ tleaves = set(_leaves(gr))
+ for n in set(gr.nodes) - tleaves:
+ if all(x in tleaves for x in nx.descendants(gr, n)):
+ return n
+
+ @lru_cache(CLUSTER_EVAL_CACHE_SIZE)
+ def _value_of_cluster(cluster):
+ valid_edges = [e for e in safe_G.edges if e[0] in cluster and e[1] in cluster]
+ return sum(safe_G.edges[e][edge_weight] for e in valid_edges)
+
+ def _value_of_partition(partition):
+ return sum(_value_of_cluster(frozenset(c)) for c in partition)
+
+ @lru_cache(CLUSTER_EVAL_CACHE_SIZE)
+ def _weight_of_cluster(cluster):
+ return sum(safe_G.nodes[n][node_weight] for n in cluster)
+
+ def _pivot(partition, node):
+ ccx = [c for c in partition if node in c]
+ assert len(ccx) == 1
+ return ccx[0]
+
+ def _concatenate_or_merge(partition_1, partition_2, x, i, ref_weight):
+ ccx = _pivot(partition_1, x)
+ cci = _pivot(partition_2, i)
+ merged_xi = ccx.union(cci)
+
+ # We first check if we can do the merge.
+ # If so, we do the actual calculations, otherwise we concatenate
+ if _weight_of_cluster(frozenset(merged_xi)) <= ref_weight:
+ cp1 = list(filter(lambda x: x != ccx, partition_1))
+ cp2 = list(filter(lambda x: x != cci, partition_2))
+
+ option_2 = [merged_xi] + cp1 + cp2
+ return option_2, _value_of_partition(option_2)
+ else:
+ option_1 = partition_1 + partition_2
+ return option_1, _value_of_partition(option_1)
+
+ # INITIALIZATION -----------------------
+ leaves = set(_leaves(t_G))
+ for lv in leaves:
+ t_G.nodes[lv][PKEY] = {}
+ slot = safe_G.nodes[lv][node_weight]
+ t_G.nodes[lv][PKEY][slot] = [{lv}]
+ t_G.nodes[lv][PKEY][0] = [{lv}]
+
+ for inner in [x for x in t_G.nodes if x not in leaves]:
+ t_G.nodes[inner][PKEY] = {}
+ slot = safe_G.nodes[inner][node_weight]
+ t_G.nodes[inner][PKEY][slot] = [{inner}]
+ nx._clear_cache(t_G)
+
+ # CORE ALGORITHM -----------------------
+ while True:
+ x_node = _a_parent_of_leaves_only(t_G)
+ weight_of_x = safe_G.nodes[x_node][node_weight]
+ best_value = 0
+ best_partition = None
+ bp_buffer = {}
+ x_descendants = nx.descendants(t_G, x_node)
+ for i_node in x_descendants:
+ for j in range(weight_of_x, max_size + 1):
+ for a, b in _split_n_from(j, weight_of_x):
+ if (
+ a not in t_G.nodes[x_node][PKEY]
+ or b not in t_G.nodes[i_node][PKEY]
+ ):
+ # it's not possible to form this particular weight sum
+ continue
+
+ part1 = t_G.nodes[x_node][PKEY][a]
+ part2 = t_G.nodes[i_node][PKEY][b]
+ part, value = _concatenate_or_merge(part1, part2, x_node, i_node, j)
+
+ if j not in bp_buffer or bp_buffer[j][1] < value:
+ # we annotate in the buffer the best partition for j
+ bp_buffer[j] = part, value
+
+ # we also keep track of the overall best partition
+ if best_value <= value:
+ best_value = value
+ best_partition = part
+
+ # as illustrated in Lukes, once we finished a child, we can
+ # discharge the partitions we found into the graph
+ # (the key phrase is make all x == x')
+ # so that they are used by the subsequent children
+ for w, (best_part_for_vl, vl) in bp_buffer.items():
+ t_G.nodes[x_node][PKEY][w] = best_part_for_vl
+ bp_buffer.clear()
+
+ # the absolute best partition for this node
+ # across all weights has to be stored at 0
+ t_G.nodes[x_node][PKEY][0] = best_partition
+ t_G.remove_nodes_from(x_descendants)
+
+ if x_node == root:
+ # the 0-labeled partition of root
+ # is the optimal one for the whole tree
+ return t_G.nodes[root][PKEY][0]
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/modularity_max.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/modularity_max.py
new file mode 100644
index 00000000..f465e01c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/modularity_max.py
@@ -0,0 +1,451 @@
+"""Functions for detecting communities based on modularity."""
+
+from collections import defaultdict
+
+import networkx as nx
+from networkx.algorithms.community.quality import modularity
+from networkx.utils import not_implemented_for
+from networkx.utils.mapped_queue import MappedQueue
+
+__all__ = [
+ "greedy_modularity_communities",
+ "naive_greedy_modularity_communities",
+]
+
+
+def _greedy_modularity_communities_generator(G, weight=None, resolution=1):
+ r"""Yield community partitions of G and the modularity change at each step.
+
+ This function performs Clauset-Newman-Moore greedy modularity maximization [2]_
+ At each step of the process it yields the change in modularity that will occur in
+ the next step followed by yielding the new community partition after that step.
+
+ Greedy modularity maximization begins with each node in its own community
+ and repeatedly joins the pair of communities that lead to the largest
+ modularity until one community contains all nodes (the partition has one set).
+
+ This function maximizes the generalized modularity, where `resolution`
+ is the resolution parameter, often expressed as $\gamma$.
+ See :func:`~networkx.algorithms.community.quality.modularity`.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ weight : string or None, optional (default=None)
+ The name of an edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ resolution : float (default=1)
+ If resolution is less than 1, modularity favors larger communities.
+ Greater than 1 favors smaller communities.
+
+ Yields
+ ------
+ Alternating yield statements produce the following two objects:
+
+ communities: dict_values
+ A dict_values of frozensets of nodes, one for each community.
+ This represents a partition of the nodes of the graph into communities.
+ The first yield is the partition with each node in its own community.
+
+ dq: float
+ The change in modularity when merging the next two communities
+ that leads to the largest modularity.
+
+ See Also
+ --------
+ modularity
+
+ References
+ ----------
+ .. [1] Newman, M. E. J. "Networks: An Introduction", page 224
+ Oxford University Press 2011.
+ .. [2] Clauset, A., Newman, M. E., & Moore, C.
+ "Finding community structure in very large networks."
+ Physical Review E 70(6), 2004.
+ .. [3] Reichardt and Bornholdt "Statistical Mechanics of Community
+ Detection" Phys. Rev. E74, 2006.
+ .. [4] Newman, M. E. J."Analysis of weighted networks"
+ Physical Review E 70(5 Pt 2):056131, 2004.
+ """
+ directed = G.is_directed()
+ N = G.number_of_nodes()
+
+ # Count edges (or the sum of edge-weights for weighted graphs)
+ m = G.size(weight)
+ q0 = 1 / m
+
+ # Calculate degrees (notation from the papers)
+ # a : the fraction of (weighted) out-degree for each node
+ # b : the fraction of (weighted) in-degree for each node
+ if directed:
+ a = {node: deg_out * q0 for node, deg_out in G.out_degree(weight=weight)}
+ b = {node: deg_in * q0 for node, deg_in in G.in_degree(weight=weight)}
+ else:
+ a = b = {node: deg * q0 * 0.5 for node, deg in G.degree(weight=weight)}
+
+ # this preliminary step collects the edge weights for each node pair
+ # It handles multigraph and digraph and works fine for graph.
+ dq_dict = defaultdict(lambda: defaultdict(float))
+ for u, v, wt in G.edges(data=weight, default=1):
+ if u == v:
+ continue
+ dq_dict[u][v] += wt
+ dq_dict[v][u] += wt
+
+ # now scale and subtract the expected edge-weights term
+ for u, nbrdict in dq_dict.items():
+ for v, wt in nbrdict.items():
+ dq_dict[u][v] = q0 * wt - resolution * (a[u] * b[v] + b[u] * a[v])
+
+ # Use -dq to get a max_heap instead of a min_heap
+ # dq_heap holds a heap for each node's neighbors
+ dq_heap = {u: MappedQueue({(u, v): -dq for v, dq in dq_dict[u].items()}) for u in G}
+ # H -> all_dq_heap holds a heap with the best items for each node
+ H = MappedQueue([dq_heap[n].heap[0] for n in G if len(dq_heap[n]) > 0])
+
+ # Initialize single-node communities
+ communities = {n: frozenset([n]) for n in G}
+ yield communities.values()
+
+ # Merge the two communities that lead to the largest modularity
+ while len(H) > 1:
+ # Find best merge
+ # Remove from heap of row maxes
+ # Ties will be broken by choosing the pair with lowest min community id
+ try:
+ negdq, u, v = H.pop()
+ except IndexError:
+ break
+ dq = -negdq
+ yield dq
+ # Remove best merge from row u heap
+ dq_heap[u].pop()
+ # Push new row max onto H
+ if len(dq_heap[u]) > 0:
+ H.push(dq_heap[u].heap[0])
+ # If this element was also at the root of row v, we need to remove the
+ # duplicate entry from H
+ if dq_heap[v].heap[0] == (v, u):
+ H.remove((v, u))
+ # Remove best merge from row v heap
+ dq_heap[v].remove((v, u))
+ # Push new row max onto H
+ if len(dq_heap[v]) > 0:
+ H.push(dq_heap[v].heap[0])
+ else:
+ # Duplicate wasn't in H, just remove from row v heap
+ dq_heap[v].remove((v, u))
+
+ # Perform merge
+ communities[v] = frozenset(communities[u] | communities[v])
+ del communities[u]
+
+ # Get neighbor communities connected to the merged communities
+ u_nbrs = set(dq_dict[u])
+ v_nbrs = set(dq_dict[v])
+ all_nbrs = (u_nbrs | v_nbrs) - {u, v}
+ both_nbrs = u_nbrs & v_nbrs
+ # Update dq for merge of u into v
+ for w in all_nbrs:
+ # Calculate new dq value
+ if w in both_nbrs:
+ dq_vw = dq_dict[v][w] + dq_dict[u][w]
+ elif w in v_nbrs:
+ dq_vw = dq_dict[v][w] - resolution * (a[u] * b[w] + a[w] * b[u])
+ else: # w in u_nbrs
+ dq_vw = dq_dict[u][w] - resolution * (a[v] * b[w] + a[w] * b[v])
+ # Update rows v and w
+ for row, col in [(v, w), (w, v)]:
+ dq_heap_row = dq_heap[row]
+ # Update dict for v,w only (u is removed below)
+ dq_dict[row][col] = dq_vw
+ # Save old max of per-row heap
+ if len(dq_heap_row) > 0:
+ d_oldmax = dq_heap_row.heap[0]
+ else:
+ d_oldmax = None
+ # Add/update heaps
+ d = (row, col)
+ d_negdq = -dq_vw
+ # Save old value for finding heap index
+ if w in v_nbrs:
+ # Update existing element in per-row heap
+ dq_heap_row.update(d, d, priority=d_negdq)
+ else:
+ # We're creating a new nonzero element, add to heap
+ dq_heap_row.push(d, priority=d_negdq)
+ # Update heap of row maxes if necessary
+ if d_oldmax is None:
+ # No entries previously in this row, push new max
+ H.push(d, priority=d_negdq)
+ else:
+ # We've updated an entry in this row, has the max changed?
+ row_max = dq_heap_row.heap[0]
+ if d_oldmax != row_max or d_oldmax.priority != row_max.priority:
+ H.update(d_oldmax, row_max)
+
+ # Remove row/col u from dq_dict matrix
+ for w in dq_dict[u]:
+ # Remove from dict
+ dq_old = dq_dict[w][u]
+ del dq_dict[w][u]
+ # Remove from heaps if we haven't already
+ if w != v:
+ # Remove both row and column
+ for row, col in [(w, u), (u, w)]:
+ dq_heap_row = dq_heap[row]
+ # Check if replaced dq is row max
+ d_old = (row, col)
+ if dq_heap_row.heap[0] == d_old:
+ # Update per-row heap and heap of row maxes
+ dq_heap_row.remove(d_old)
+ H.remove(d_old)
+ # Update row max
+ if len(dq_heap_row) > 0:
+ H.push(dq_heap_row.heap[0])
+ else:
+ # Only update per-row heap
+ dq_heap_row.remove(d_old)
+
+ del dq_dict[u]
+ # Mark row u as deleted, but keep placeholder
+ dq_heap[u] = MappedQueue()
+ # Merge u into v and update a
+ a[v] += a[u]
+ a[u] = 0
+ if directed:
+ b[v] += b[u]
+ b[u] = 0
+
+ yield communities.values()
+
+
+@nx._dispatchable(edge_attrs="weight")
+def greedy_modularity_communities(
+ G,
+ weight=None,
+ resolution=1,
+ cutoff=1,
+ best_n=None,
+):
+ r"""Find communities in G using greedy modularity maximization.
+
+ This function uses Clauset-Newman-Moore greedy modularity maximization [2]_
+ to find the community partition with the largest modularity.
+
+ Greedy modularity maximization begins with each node in its own community
+ and repeatedly joins the pair of communities that lead to the largest
+ modularity until no further increase in modularity is possible (a maximum).
+ Two keyword arguments adjust the stopping condition. `cutoff` is a lower
+ limit on the number of communities so you can stop the process before
+ reaching a maximum (used to save computation time). `best_n` is an upper
+ limit on the number of communities so you can make the process continue
+ until at most n communities remain even if the maximum modularity occurs
+ for more. To obtain exactly n communities, set both `cutoff` and `best_n` to n.
+
+ This function maximizes the generalized modularity, where `resolution`
+ is the resolution parameter, often expressed as $\gamma$.
+ See :func:`~networkx.algorithms.community.quality.modularity`.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ weight : string or None, optional (default=None)
+ The name of an edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ resolution : float, optional (default=1)
+ If resolution is less than 1, modularity favors larger communities.
+ Greater than 1 favors smaller communities.
+
+ cutoff : int, optional (default=1)
+ A minimum number of communities below which the merging process stops.
+ The process stops at this number of communities even if modularity
+ is not maximized. The goal is to let the user stop the process early.
+ The process stops before the cutoff if it finds a maximum of modularity.
+
+ best_n : int or None, optional (default=None)
+ A maximum number of communities above which the merging process will
+ not stop. This forces community merging to continue after modularity
+ starts to decrease until `best_n` communities remain.
+ If ``None``, don't force it to continue beyond a maximum.
+
+ Raises
+ ------
+ ValueError : If the `cutoff` or `best_n` value is not in the range
+ ``[1, G.number_of_nodes()]``, or if `best_n` < `cutoff`.
+
+ Returns
+ -------
+ communities: list
+ A list of frozensets of nodes, one for each community.
+ Sorted by length with largest communities first.
+
+ Examples
+ --------
+ >>> G = nx.karate_club_graph()
+ >>> c = nx.community.greedy_modularity_communities(G)
+ >>> sorted(c[0])
+ [8, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
+
+ See Also
+ --------
+ modularity
+
+ References
+ ----------
+ .. [1] Newman, M. E. J. "Networks: An Introduction", page 224
+ Oxford University Press 2011.
+ .. [2] Clauset, A., Newman, M. E., & Moore, C.
+ "Finding community structure in very large networks."
+ Physical Review E 70(6), 2004.
+ .. [3] Reichardt and Bornholdt "Statistical Mechanics of Community
+ Detection" Phys. Rev. E74, 2006.
+ .. [4] Newman, M. E. J."Analysis of weighted networks"
+ Physical Review E 70(5 Pt 2):056131, 2004.
+ """
+ if not G.size():
+ return [{n} for n in G]
+
+ if (cutoff < 1) or (cutoff > G.number_of_nodes()):
+ raise ValueError(f"cutoff must be between 1 and {len(G)}. Got {cutoff}.")
+ if best_n is not None:
+ if (best_n < 1) or (best_n > G.number_of_nodes()):
+ raise ValueError(f"best_n must be between 1 and {len(G)}. Got {best_n}.")
+ if best_n < cutoff:
+ raise ValueError(f"Must have best_n >= cutoff. Got {best_n} < {cutoff}")
+ if best_n == 1:
+ return [set(G)]
+ else:
+ best_n = G.number_of_nodes()
+
+ # retrieve generator object to construct output
+ community_gen = _greedy_modularity_communities_generator(
+ G, weight=weight, resolution=resolution
+ )
+
+ # construct the first best community
+ communities = next(community_gen)
+
+ # continue merging communities until one of the breaking criteria is satisfied
+ while len(communities) > cutoff:
+ try:
+ dq = next(community_gen)
+ # StopIteration occurs when communities are the connected components
+ except StopIteration:
+ communities = sorted(communities, key=len, reverse=True)
+ # if best_n requires more merging, merge big sets for highest modularity
+ while len(communities) > best_n:
+ comm1, comm2, *rest = communities
+ communities = [comm1 ^ comm2]
+ communities.extend(rest)
+ return communities
+
+ # keep going unless max_mod is reached or best_n says to merge more
+ if dq < 0 and len(communities) <= best_n:
+ break
+ communities = next(community_gen)
+
+ return sorted(communities, key=len, reverse=True)
+
+
+@not_implemented_for("directed")
+@not_implemented_for("multigraph")
+@nx._dispatchable(edge_attrs="weight")
+def naive_greedy_modularity_communities(G, resolution=1, weight=None):
+ r"""Find communities in G using greedy modularity maximization.
+
+ This implementation is O(n^4), much slower than alternatives, but it is
+ provided as an easy-to-understand reference implementation.
+
+ Greedy modularity maximization begins with each node in its own community
+ and joins the pair of communities that most increases modularity until no
+ such pair exists.
+
+ This function maximizes the generalized modularity, where `resolution`
+ is the resolution parameter, often expressed as $\gamma$.
+ See :func:`~networkx.algorithms.community.quality.modularity`.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+ Graph must be simple and undirected.
+
+ resolution : float (default=1)
+ If resolution is less than 1, modularity favors larger communities.
+ Greater than 1 favors smaller communities.
+
+ weight : string or None, optional (default=None)
+ The name of an edge attribute that holds the numerical value used
+ as a weight. If None, then each edge has weight 1.
+ The degree is the sum of the edge weights adjacent to the node.
+
+ Returns
+ -------
+ list
+ A list of sets of nodes, one for each community.
+ Sorted by length with largest communities first.
+
+ Examples
+ --------
+ >>> G = nx.karate_club_graph()
+ >>> c = nx.community.naive_greedy_modularity_communities(G)
+ >>> sorted(c[0])
+ [8, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
+
+ See Also
+ --------
+ greedy_modularity_communities
+ modularity
+ """
+ # First create one community for each node
+ communities = [frozenset([u]) for u in G.nodes()]
+ # Track merges
+ merges = []
+ # Greedily merge communities until no improvement is possible
+ old_modularity = None
+ new_modularity = modularity(G, communities, resolution=resolution, weight=weight)
+ while old_modularity is None or new_modularity > old_modularity:
+ # Save modularity for comparison
+ old_modularity = new_modularity
+ # Find best pair to merge
+ trial_communities = list(communities)
+ to_merge = None
+ for i, u in enumerate(communities):
+ for j, v in enumerate(communities):
+ # Skip i==j and empty communities
+ if j <= i or len(u) == 0 or len(v) == 0:
+ continue
+ # Merge communities u and v
+ trial_communities[j] = u | v
+ trial_communities[i] = frozenset([])
+ trial_modularity = modularity(
+ G, trial_communities, resolution=resolution, weight=weight
+ )
+ if trial_modularity >= new_modularity:
+ # Check if strictly better or tie
+ if trial_modularity > new_modularity:
+ # Found new best, save modularity and group indexes
+ new_modularity = trial_modularity
+ to_merge = (i, j, new_modularity - old_modularity)
+ elif to_merge and min(i, j) < min(to_merge[0], to_merge[1]):
+ # Break ties by choosing pair with lowest min id
+ new_modularity = trial_modularity
+ to_merge = (i, j, new_modularity - old_modularity)
+ # Un-merge
+ trial_communities[i] = u
+ trial_communities[j] = v
+ if to_merge is not None:
+ # If the best merge improves modularity, use it
+ merges.append(to_merge)
+ i, j, dq = to_merge
+ u, v = communities[i], communities[j]
+ communities[j] = u | v
+ communities[i] = frozenset([])
+ # Remove empty communities and sort
+ return sorted((c for c in communities if len(c) > 0), key=len, reverse=True)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/quality.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/quality.py
new file mode 100644
index 00000000..f09a6d45
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/quality.py
@@ -0,0 +1,346 @@
+"""Functions for measuring the quality of a partition (into
+communities).
+
+"""
+
+from itertools import combinations
+
+import networkx as nx
+from networkx import NetworkXError
+from networkx.algorithms.community.community_utils import is_partition
+from networkx.utils.decorators import argmap
+
+__all__ = ["modularity", "partition_quality"]
+
+
+class NotAPartition(NetworkXError):
+ """Raised if a given collection is not a partition."""
+
+ def __init__(self, G, collection):
+ msg = f"{collection} is not a valid partition of the graph {G}"
+ super().__init__(msg)
+
+
+def _require_partition(G, partition):
+ """Decorator to check that a valid partition is input to a function
+
+ Raises :exc:`networkx.NetworkXError` if the partition is not valid.
+
+ This decorator should be used on functions whose first two arguments
+ are a graph and a partition of the nodes of that graph (in that
+ order)::
+
+ >>> @require_partition
+ ... def foo(G, partition):
+ ... print("partition is valid!")
+ ...
+ >>> G = nx.complete_graph(5)
+ >>> partition = [{0, 1}, {2, 3}, {4}]
+ >>> foo(G, partition)
+ partition is valid!
+ >>> partition = [{0}, {2, 3}, {4}]
+ >>> foo(G, partition)
+ Traceback (most recent call last):
+ ...
+ networkx.exception.NetworkXError: `partition` is not a valid partition of the nodes of G
+ >>> partition = [{0, 1}, {1, 2, 3}, {4}]
+ >>> foo(G, partition)
+ Traceback (most recent call last):
+ ...
+ networkx.exception.NetworkXError: `partition` is not a valid partition of the nodes of G
+
+ """
+ if is_partition(G, partition):
+ return G, partition
+ raise nx.NetworkXError("`partition` is not a valid partition of the nodes of G")
+
+
+require_partition = argmap(_require_partition, (0, 1))
+
+
+@nx._dispatchable
+def intra_community_edges(G, partition):
+ """Returns the number of intra-community edges for a partition of `G`.
+
+ Parameters
+ ----------
+ G : NetworkX graph.
+
+ partition : iterable of sets of nodes
+ This must be a partition of the nodes of `G`.
+
+ The "intra-community edges" are those edges joining a pair of nodes
+ in the same block of the partition.
+
+ """
+ return sum(G.subgraph(block).size() for block in partition)
+
+
+@nx._dispatchable
+def inter_community_edges(G, partition):
+ """Returns the number of inter-community edges for a partition of `G`.
+ according to the given
+ partition of the nodes of `G`.
+
+ Parameters
+ ----------
+ G : NetworkX graph.
+
+ partition : iterable of sets of nodes
+ This must be a partition of the nodes of `G`.
+
+ The *inter-community edges* are those edges joining a pair of nodes
+ in different blocks of the partition.
+
+ Implementation note: this function creates an intermediate graph
+ that may require the same amount of memory as that of `G`.
+
+ """
+ # Alternate implementation that does not require constructing a new
+ # graph object (but does require constructing an affiliation
+ # dictionary):
+ #
+ # aff = dict(chain.from_iterable(((v, block) for v in block)
+ # for block in partition))
+ # return sum(1 for u, v in G.edges() if aff[u] != aff[v])
+ #
+ MG = nx.MultiDiGraph if G.is_directed() else nx.MultiGraph
+ return nx.quotient_graph(G, partition, create_using=MG).size()
+
+
+@nx._dispatchable
+def inter_community_non_edges(G, partition):
+ """Returns the number of inter-community non-edges according to the
+ given partition of the nodes of `G`.
+
+ Parameters
+ ----------
+ G : NetworkX graph.
+
+ partition : iterable of sets of nodes
+ This must be a partition of the nodes of `G`.
+
+ A *non-edge* is a pair of nodes (undirected if `G` is undirected)
+ that are not adjacent in `G`. The *inter-community non-edges* are
+ those non-edges on a pair of nodes in different blocks of the
+ partition.
+
+ Implementation note: this function creates two intermediate graphs,
+ which may require up to twice the amount of memory as required to
+ store `G`.
+
+ """
+ # Alternate implementation that does not require constructing two
+ # new graph objects (but does require constructing an affiliation
+ # dictionary):
+ #
+ # aff = dict(chain.from_iterable(((v, block) for v in block)
+ # for block in partition))
+ # return sum(1 for u, v in nx.non_edges(G) if aff[u] != aff[v])
+ #
+ return inter_community_edges(nx.complement(G), partition)
+
+
+@nx._dispatchable(edge_attrs="weight")
+def modularity(G, communities, weight="weight", resolution=1):
+ r"""Returns the modularity of the given partition of the graph.
+
+ Modularity is defined in [1]_ as
+
+ .. math::
+ Q = \frac{1}{2m} \sum_{ij} \left( A_{ij} - \gamma\frac{k_ik_j}{2m}\right)
+ \delta(c_i,c_j)
+
+ where $m$ is the number of edges (or sum of all edge weights as in [5]_),
+ $A$ is the adjacency matrix of `G`, $k_i$ is the (weighted) degree of $i$,
+ $\gamma$ is the resolution parameter, and $\delta(c_i, c_j)$ is 1 if $i$ and
+ $j$ are in the same community else 0.
+
+ According to [2]_ (and verified by some algebra) this can be reduced to
+
+ .. math::
+ Q = \sum_{c=1}^{n}
+ \left[ \frac{L_c}{m} - \gamma\left( \frac{k_c}{2m} \right) ^2 \right]
+
+ where the sum iterates over all communities $c$, $m$ is the number of edges,
+ $L_c$ is the number of intra-community links for community $c$,
+ $k_c$ is the sum of degrees of the nodes in community $c$,
+ and $\gamma$ is the resolution parameter.
+
+ The resolution parameter sets an arbitrary tradeoff between intra-group
+ edges and inter-group edges. More complex grouping patterns can be
+ discovered by analyzing the same network with multiple values of gamma
+ and then combining the results [3]_. That said, it is very common to
+ simply use gamma=1. More on the choice of gamma is in [4]_.
+
+ The second formula is the one actually used in calculation of the modularity.
+ For directed graphs the second formula replaces $k_c$ with $k^{in}_c k^{out}_c$.
+
+ Parameters
+ ----------
+ G : NetworkX Graph
+
+ communities : list or iterable of set of nodes
+ These node sets must represent a partition of G's nodes.
+
+ weight : string or None, optional (default="weight")
+ The edge attribute that holds the numerical value used
+ as a weight. If None or an edge does not have that attribute,
+ then that edge has weight 1.
+
+ resolution : float (default=1)
+ If resolution is less than 1, modularity favors larger communities.
+ Greater than 1 favors smaller communities.
+
+ Returns
+ -------
+ Q : float
+ The modularity of the partition.
+
+ Raises
+ ------
+ NotAPartition
+ If `communities` is not a partition of the nodes of `G`.
+
+ Examples
+ --------
+ >>> G = nx.barbell_graph(3, 0)
+ >>> nx.community.modularity(G, [{0, 1, 2}, {3, 4, 5}])
+ 0.35714285714285715
+ >>> nx.community.modularity(G, nx.community.label_propagation_communities(G))
+ 0.35714285714285715
+
+ References
+ ----------
+ .. [1] M. E. J. Newman "Networks: An Introduction", page 224.
+ Oxford University Press, 2011.
+ .. [2] Clauset, Aaron, Mark EJ Newman, and Cristopher Moore.
+ "Finding community structure in very large networks."
+ Phys. Rev. E 70.6 (2004). <https://arxiv.org/abs/cond-mat/0408187>
+ .. [3] Reichardt and Bornholdt "Statistical Mechanics of Community Detection"
+ Phys. Rev. E 74, 016110, 2006. https://doi.org/10.1103/PhysRevE.74.016110
+ .. [4] M. E. J. Newman, "Equivalence between modularity optimization and
+ maximum likelihood methods for community detection"
+ Phys. Rev. E 94, 052315, 2016. https://doi.org/10.1103/PhysRevE.94.052315
+ .. [5] Blondel, V.D. et al. "Fast unfolding of communities in large
+ networks" J. Stat. Mech 10008, 1-12 (2008).
+ https://doi.org/10.1088/1742-5468/2008/10/P10008
+ """
+ if not isinstance(communities, list):
+ communities = list(communities)
+ if not is_partition(G, communities):
+ raise NotAPartition(G, communities)
+
+ directed = G.is_directed()
+ if directed:
+ out_degree = dict(G.out_degree(weight=weight))
+ in_degree = dict(G.in_degree(weight=weight))
+ m = sum(out_degree.values())
+ norm = 1 / m**2
+ else:
+ out_degree = in_degree = dict(G.degree(weight=weight))
+ deg_sum = sum(out_degree.values())
+ m = deg_sum / 2
+ norm = 1 / deg_sum**2
+
+ def community_contribution(community):
+ comm = set(community)
+ L_c = sum(wt for u, v, wt in G.edges(comm, data=weight, default=1) if v in comm)
+
+ out_degree_sum = sum(out_degree[u] for u in comm)
+ in_degree_sum = sum(in_degree[u] for u in comm) if directed else out_degree_sum
+
+ return L_c / m - resolution * out_degree_sum * in_degree_sum * norm
+
+ return sum(map(community_contribution, communities))
+
+
+@require_partition
+@nx._dispatchable
+def partition_quality(G, partition):
+ """Returns the coverage and performance of a partition of G.
+
+ The *coverage* of a partition is the ratio of the number of
+ intra-community edges to the total number of edges in the graph.
+
+ The *performance* of a partition is the number of
+ intra-community edges plus inter-community non-edges divided by the total
+ number of potential edges.
+
+ This algorithm has complexity $O(C^2 + L)$ where C is the number of communities and L is the number of links.
+
+ Parameters
+ ----------
+ G : NetworkX graph
+
+ partition : sequence
+ Partition of the nodes of `G`, represented as a sequence of
+ sets of nodes (blocks). Each block of the partition represents a
+ community.
+
+ Returns
+ -------
+ (float, float)
+ The (coverage, performance) tuple of the partition, as defined above.
+
+ Raises
+ ------
+ NetworkXError
+ If `partition` is not a valid partition of the nodes of `G`.
+
+ Notes
+ -----
+ If `G` is a multigraph;
+ - for coverage, the multiplicity of edges is counted
+ - for performance, the result is -1 (total number of possible edges is not defined)
+
+ References
+ ----------
+ .. [1] Santo Fortunato.
+ "Community Detection in Graphs".
+ *Physical Reports*, Volume 486, Issue 3--5 pp. 75--174
+ <https://arxiv.org/abs/0906.0612>
+ """
+
+ node_community = {}
+ for i, community in enumerate(partition):
+ for node in community:
+ node_community[node] = i
+
+ # `performance` is not defined for multigraphs
+ if not G.is_multigraph():
+ # Iterate over the communities, quadratic, to calculate `possible_inter_community_edges`
+ possible_inter_community_edges = sum(
+ len(p1) * len(p2) for p1, p2 in combinations(partition, 2)
+ )
+
+ if G.is_directed():
+ possible_inter_community_edges *= 2
+ else:
+ possible_inter_community_edges = 0
+
+ # Compute the number of edges in the complete graph -- `n` nodes,
+ # directed or undirected, depending on `G`
+ n = len(G)
+ total_pairs = n * (n - 1)
+ if not G.is_directed():
+ total_pairs //= 2
+
+ intra_community_edges = 0
+ inter_community_non_edges = possible_inter_community_edges
+
+ # Iterate over the links to count `intra_community_edges` and `inter_community_non_edges`
+ for e in G.edges():
+ if node_community[e[0]] == node_community[e[1]]:
+ intra_community_edges += 1
+ else:
+ inter_community_non_edges -= 1
+
+ coverage = intra_community_edges / len(G.edges)
+
+ if G.is_multigraph():
+ performance = -1.0
+ else:
+ performance = (intra_community_edges + inter_community_non_edges) / total_pairs
+
+ return coverage, performance
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/__init__.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_asyn_fluid.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_asyn_fluid.py
new file mode 100644
index 00000000..6c023be7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_asyn_fluid.py
@@ -0,0 +1,136 @@
+import pytest
+
+import networkx as nx
+from networkx import Graph, NetworkXError
+from networkx.algorithms.community import asyn_fluidc
+
+
+@pytest.mark.parametrize("graph_constructor", (nx.DiGraph, nx.MultiGraph))
+def test_raises_on_directed_and_multigraphs(graph_constructor):
+ G = graph_constructor([(0, 1), (1, 2)])
+ with pytest.raises(nx.NetworkXNotImplemented):
+ nx.community.asyn_fluidc(G, 1)
+
+
+def test_exceptions():
+ test = Graph()
+ test.add_node("a")
+ pytest.raises(NetworkXError, asyn_fluidc, test, "hi")
+ pytest.raises(NetworkXError, asyn_fluidc, test, -1)
+ pytest.raises(NetworkXError, asyn_fluidc, test, 3)
+ test.add_node("b")
+ pytest.raises(NetworkXError, asyn_fluidc, test, 1)
+
+
+def test_single_node():
+ test = Graph()
+
+ test.add_node("a")
+
+ # ground truth
+ ground_truth = {frozenset(["a"])}
+
+ communities = asyn_fluidc(test, 1)
+ result = {frozenset(c) for c in communities}
+ assert result == ground_truth
+
+
+def test_two_nodes():
+ test = Graph()
+
+ test.add_edge("a", "b")
+
+ # ground truth
+ ground_truth = {frozenset(["a"]), frozenset(["b"])}
+
+ communities = asyn_fluidc(test, 2)
+ result = {frozenset(c) for c in communities}
+ assert result == ground_truth
+
+
+def test_two_clique_communities():
+ test = Graph()
+
+ # c1
+ test.add_edge("a", "b")
+ test.add_edge("a", "c")
+ test.add_edge("b", "c")
+
+ # connection
+ test.add_edge("c", "d")
+
+ # c2
+ test.add_edge("d", "e")
+ test.add_edge("d", "f")
+ test.add_edge("f", "e")
+
+ # ground truth
+ ground_truth = {frozenset(["a", "c", "b"]), frozenset(["e", "d", "f"])}
+
+ communities = asyn_fluidc(test, 2, seed=7)
+ result = {frozenset(c) for c in communities}
+ assert result == ground_truth
+
+
+def test_five_clique_ring():
+ test = Graph()
+
+ # c1
+ test.add_edge("1a", "1b")
+ test.add_edge("1a", "1c")
+ test.add_edge("1a", "1d")
+ test.add_edge("1b", "1c")
+ test.add_edge("1b", "1d")
+ test.add_edge("1c", "1d")
+
+ # c2
+ test.add_edge("2a", "2b")
+ test.add_edge("2a", "2c")
+ test.add_edge("2a", "2d")
+ test.add_edge("2b", "2c")
+ test.add_edge("2b", "2d")
+ test.add_edge("2c", "2d")
+
+ # c3
+ test.add_edge("3a", "3b")
+ test.add_edge("3a", "3c")
+ test.add_edge("3a", "3d")
+ test.add_edge("3b", "3c")
+ test.add_edge("3b", "3d")
+ test.add_edge("3c", "3d")
+
+ # c4
+ test.add_edge("4a", "4b")
+ test.add_edge("4a", "4c")
+ test.add_edge("4a", "4d")
+ test.add_edge("4b", "4c")
+ test.add_edge("4b", "4d")
+ test.add_edge("4c", "4d")
+
+ # c5
+ test.add_edge("5a", "5b")
+ test.add_edge("5a", "5c")
+ test.add_edge("5a", "5d")
+ test.add_edge("5b", "5c")
+ test.add_edge("5b", "5d")
+ test.add_edge("5c", "5d")
+
+ # connections
+ test.add_edge("1a", "2c")
+ test.add_edge("2a", "3c")
+ test.add_edge("3a", "4c")
+ test.add_edge("4a", "5c")
+ test.add_edge("5a", "1c")
+
+ # ground truth
+ ground_truth = {
+ frozenset(["1a", "1b", "1c", "1d"]),
+ frozenset(["2a", "2b", "2c", "2d"]),
+ frozenset(["3a", "3b", "3c", "3d"]),
+ frozenset(["4a", "4b", "4c", "4d"]),
+ frozenset(["5a", "5b", "5c", "5d"]),
+ }
+
+ communities = asyn_fluidc(test, 5, seed=9)
+ result = {frozenset(c) for c in communities}
+ assert result == ground_truth
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_centrality.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_centrality.py
new file mode 100644
index 00000000..1a8982f0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_centrality.py
@@ -0,0 +1,85 @@
+"""Unit tests for the :mod:`networkx.algorithms.community.centrality`
+module.
+
+"""
+
+from operator import itemgetter
+
+import networkx as nx
+
+
+def set_of_sets(iterable):
+ return set(map(frozenset, iterable))
+
+
+def validate_communities(result, expected):
+ assert set_of_sets(result) == set_of_sets(expected)
+
+
+def validate_possible_communities(result, *expected):
+ assert any(set_of_sets(result) == set_of_sets(p) for p in expected)
+
+
+class TestGirvanNewman:
+ """Unit tests for the
+ :func:`networkx.algorithms.community.centrality.girvan_newman`
+ function.
+
+ """
+
+ def test_no_edges(self):
+ G = nx.empty_graph(3)
+ communities = list(nx.community.girvan_newman(G))
+ assert len(communities) == 1
+ validate_communities(communities[0], [{0}, {1}, {2}])
+
+ def test_undirected(self):
+ # Start with the graph .-.-.-.
+ G = nx.path_graph(4)
+ communities = list(nx.community.girvan_newman(G))
+ assert len(communities) == 3
+ # After one removal, we get the graph .-. .-.
+ validate_communities(communities[0], [{0, 1}, {2, 3}])
+ # After the next, we get the graph .-. . ., but there are two
+ # symmetric possible versions.
+ validate_possible_communities(
+ communities[1], [{0}, {1}, {2, 3}], [{0, 1}, {2}, {3}]
+ )
+ # After the last removal, we always get the empty graph.
+ validate_communities(communities[2], [{0}, {1}, {2}, {3}])
+
+ def test_directed(self):
+ G = nx.DiGraph(nx.path_graph(4))
+ communities = list(nx.community.girvan_newman(G))
+ assert len(communities) == 3
+ validate_communities(communities[0], [{0, 1}, {2, 3}])
+ validate_possible_communities(
+ communities[1], [{0}, {1}, {2, 3}], [{0, 1}, {2}, {3}]
+ )
+ validate_communities(communities[2], [{0}, {1}, {2}, {3}])
+
+ def test_selfloops(self):
+ G = nx.path_graph(4)
+ G.add_edge(0, 0)
+ G.add_edge(2, 2)
+ communities = list(nx.community.girvan_newman(G))
+ assert len(communities) == 3
+ validate_communities(communities[0], [{0, 1}, {2, 3}])
+ validate_possible_communities(
+ communities[1], [{0}, {1}, {2, 3}], [{0, 1}, {2}, {3}]
+ )
+ validate_communities(communities[2], [{0}, {1}, {2}, {3}])
+
+ def test_most_valuable_edge(self):
+ G = nx.Graph()
+ G.add_weighted_edges_from([(0, 1, 3), (1, 2, 2), (2, 3, 1)])
+ # Let the most valuable edge be the one with the highest weight.
+
+ def heaviest(G):
+ return max(G.edges(data="weight"), key=itemgetter(2))[:2]
+
+ communities = list(nx.community.girvan_newman(G, heaviest))
+ assert len(communities) == 3
+ validate_communities(communities[0], [{0}, {1, 2, 3}])
+ validate_communities(communities[1], [{0}, {1}, {2, 3}])
+ validate_communities(communities[2], [{0}, {1}, {2}, {3}])
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_divisive.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_divisive.py
new file mode 100644
index 00000000..6331503f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_divisive.py
@@ -0,0 +1,106 @@
+import pytest
+
+import networkx as nx
+
+
+def test_edge_betweenness_partition():
+ G = nx.barbell_graph(3, 0)
+ C = nx.community.edge_betweenness_partition(G, 2)
+ answer = [{0, 1, 2}, {3, 4, 5}]
+ assert len(C) == len(answer)
+ for s in answer:
+ assert s in C
+
+ G = nx.barbell_graph(3, 1)
+ C = nx.community.edge_betweenness_partition(G, 3)
+ answer = [{0, 1, 2}, {4, 5, 6}, {3}]
+ assert len(C) == len(answer)
+ for s in answer:
+ assert s in C
+
+ C = nx.community.edge_betweenness_partition(G, 7)
+ answer = [{n} for n in G]
+ assert len(C) == len(answer)
+ for s in answer:
+ assert s in C
+
+ C = nx.community.edge_betweenness_partition(G, 1)
+ assert C == [set(G)]
+
+ C = nx.community.edge_betweenness_partition(G, 1, weight="weight")
+ assert C == [set(G)]
+
+ with pytest.raises(nx.NetworkXError):
+ nx.community.edge_betweenness_partition(G, 0)
+
+ with pytest.raises(nx.NetworkXError):
+ nx.community.edge_betweenness_partition(G, -1)
+
+ with pytest.raises(nx.NetworkXError):
+ nx.community.edge_betweenness_partition(G, 10)
+
+
+def test_edge_current_flow_betweenness_partition():
+ pytest.importorskip("scipy")
+
+ G = nx.barbell_graph(3, 0)
+ C = nx.community.edge_current_flow_betweenness_partition(G, 2)
+ answer = [{0, 1, 2}, {3, 4, 5}]
+ assert len(C) == len(answer)
+ for s in answer:
+ assert s in C
+
+ G = nx.barbell_graph(3, 1)
+ C = nx.community.edge_current_flow_betweenness_partition(G, 2)
+ answers = [[{0, 1, 2, 3}, {4, 5, 6}], [{0, 1, 2}, {3, 4, 5, 6}]]
+ assert len(C) == len(answers[0])
+ assert any(all(s in answer for s in C) for answer in answers)
+
+ C = nx.community.edge_current_flow_betweenness_partition(G, 3)
+ answer = [{0, 1, 2}, {4, 5, 6}, {3}]
+ assert len(C) == len(answer)
+ for s in answer:
+ assert s in C
+
+ C = nx.community.edge_current_flow_betweenness_partition(G, 4)
+ answers = [[{1, 2}, {4, 5, 6}, {3}, {0}], [{0, 1, 2}, {5, 6}, {3}, {4}]]
+ assert len(C) == len(answers[0])
+ assert any(all(s in answer for s in C) for answer in answers)
+
+ C = nx.community.edge_current_flow_betweenness_partition(G, 5)
+ answer = [{1, 2}, {5, 6}, {3}, {0}, {4}]
+ assert len(C) == len(answer)
+ for s in answer:
+ assert s in C
+
+ C = nx.community.edge_current_flow_betweenness_partition(G, 6)
+ answers = [[{2}, {5, 6}, {3}, {0}, {4}, {1}], [{1, 2}, {6}, {3}, {0}, {4}, {5}]]
+ assert len(C) == len(answers[0])
+ assert any(all(s in answer for s in C) for answer in answers)
+
+ C = nx.community.edge_current_flow_betweenness_partition(G, 7)
+ answer = [{n} for n in G]
+ assert len(C) == len(answer)
+ for s in answer:
+ assert s in C
+
+ C = nx.community.edge_current_flow_betweenness_partition(G, 1)
+ assert C == [set(G)]
+
+ C = nx.community.edge_current_flow_betweenness_partition(G, 1, weight="weight")
+ assert C == [set(G)]
+
+ with pytest.raises(nx.NetworkXError):
+ nx.community.edge_current_flow_betweenness_partition(G, 0)
+
+ with pytest.raises(nx.NetworkXError):
+ nx.community.edge_current_flow_betweenness_partition(G, -1)
+
+ with pytest.raises(nx.NetworkXError):
+ nx.community.edge_current_flow_betweenness_partition(G, 10)
+
+ N = 10
+ G = nx.empty_graph(N)
+ for i in range(2, N - 1):
+ C = nx.community.edge_current_flow_betweenness_partition(G, i)
+ assert C == [{n} for n in G]
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_kclique.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_kclique.py
new file mode 100644
index 00000000..aa0b7e82
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_kclique.py
@@ -0,0 +1,91 @@
+from itertools import combinations
+
+import pytest
+
+import networkx as nx
+
+
+def test_overlapping_K5():
+ G = nx.Graph()
+ G.add_edges_from(combinations(range(5), 2)) # Add a five clique
+ G.add_edges_from(combinations(range(2, 7), 2)) # Add another five clique
+ c = list(nx.community.k_clique_communities(G, 4))
+ assert c == [frozenset(range(7))]
+ c = set(nx.community.k_clique_communities(G, 5))
+ assert c == {frozenset(range(5)), frozenset(range(2, 7))}
+
+
+def test_isolated_K5():
+ G = nx.Graph()
+ G.add_edges_from(combinations(range(5), 2)) # Add a five clique
+ G.add_edges_from(combinations(range(5, 10), 2)) # Add another five clique
+ c = set(nx.community.k_clique_communities(G, 5))
+ assert c == {frozenset(range(5)), frozenset(range(5, 10))}
+
+
+class TestZacharyKarateClub:
+ def setup_method(self):
+ self.G = nx.karate_club_graph()
+
+ def _check_communities(self, k, expected):
+ communities = set(nx.community.k_clique_communities(self.G, k))
+ assert communities == expected
+
+ def test_k2(self):
+ # clique percolation with k=2 is just connected components
+ expected = {frozenset(self.G)}
+ self._check_communities(2, expected)
+
+ def test_k3(self):
+ comm1 = [
+ 0,
+ 1,
+ 2,
+ 3,
+ 7,
+ 8,
+ 12,
+ 13,
+ 14,
+ 15,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ 22,
+ 23,
+ 26,
+ 27,
+ 28,
+ 29,
+ 30,
+ 31,
+ 32,
+ 33,
+ ]
+ comm2 = [0, 4, 5, 6, 10, 16]
+ comm3 = [24, 25, 31]
+ expected = {frozenset(comm1), frozenset(comm2), frozenset(comm3)}
+ self._check_communities(3, expected)
+
+ def test_k4(self):
+ expected = {
+ frozenset([0, 1, 2, 3, 7, 13]),
+ frozenset([8, 32, 30, 33]),
+ frozenset([32, 33, 29, 23]),
+ }
+ self._check_communities(4, expected)
+
+ def test_k5(self):
+ expected = {frozenset([0, 1, 2, 3, 7, 13])}
+ self._check_communities(5, expected)
+
+ def test_k6(self):
+ expected = set()
+ self._check_communities(6, expected)
+
+
+def test_bad_k():
+ with pytest.raises(nx.NetworkXError):
+ list(nx.community.k_clique_communities(nx.Graph(), 1))
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_kernighan_lin.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_kernighan_lin.py
new file mode 100644
index 00000000..25d53d5f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_kernighan_lin.py
@@ -0,0 +1,92 @@
+"""Unit tests for the :mod:`networkx.algorithms.community.kernighan_lin`
+module.
+"""
+
+from itertools import permutations
+
+import pytest
+
+import networkx as nx
+from networkx.algorithms.community import kernighan_lin_bisection
+
+
+def assert_partition_equal(x, y):
+ assert set(map(frozenset, x)) == set(map(frozenset, y))
+
+
+def test_partition():
+ G = nx.barbell_graph(3, 0)
+ C = kernighan_lin_bisection(G)
+ assert_partition_equal(C, [{0, 1, 2}, {3, 4, 5}])
+
+
+def test_partition_argument():
+ G = nx.barbell_graph(3, 0)
+ partition = [{0, 1, 2}, {3, 4, 5}]
+ C = kernighan_lin_bisection(G, partition)
+ assert_partition_equal(C, partition)
+
+
+def test_partition_argument_non_integer_nodes():
+ G = nx.Graph([("A", "B"), ("A", "C"), ("B", "C"), ("C", "D")])
+ partition = ({"A", "B"}, {"C", "D"})
+ C = kernighan_lin_bisection(G, partition)
+ assert_partition_equal(C, partition)
+
+
+def test_seed_argument():
+ G = nx.barbell_graph(3, 0)
+ C = kernighan_lin_bisection(G, seed=1)
+ assert_partition_equal(C, [{0, 1, 2}, {3, 4, 5}])
+
+
+def test_non_disjoint_partition():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.barbell_graph(3, 0)
+ partition = ({0, 1, 2}, {2, 3, 4, 5})
+ kernighan_lin_bisection(G, partition)
+
+
+def test_too_many_blocks():
+ with pytest.raises(nx.NetworkXError):
+ G = nx.barbell_graph(3, 0)
+ partition = ({0, 1}, {2}, {3, 4, 5})
+ kernighan_lin_bisection(G, partition)
+
+
+def test_multigraph():
+ G = nx.cycle_graph(4)
+ M = nx.MultiGraph(G.edges())
+ M.add_edges_from(G.edges())
+ M.remove_edge(1, 2)
+ for labels in permutations(range(4)):
+ mapping = dict(zip(M, labels))
+ A, B = kernighan_lin_bisection(nx.relabel_nodes(M, mapping), seed=0)
+ assert_partition_equal(
+ [A, B], [{mapping[0], mapping[1]}, {mapping[2], mapping[3]}]
+ )
+
+
+def test_max_iter_argument():
+ G = nx.Graph(
+ [
+ ("A", "B", {"weight": 1}),
+ ("A", "C", {"weight": 2}),
+ ("A", "D", {"weight": 3}),
+ ("A", "E", {"weight": 2}),
+ ("A", "F", {"weight": 4}),
+ ("B", "C", {"weight": 1}),
+ ("B", "D", {"weight": 4}),
+ ("B", "E", {"weight": 2}),
+ ("B", "F", {"weight": 1}),
+ ("C", "D", {"weight": 3}),
+ ("C", "E", {"weight": 2}),
+ ("C", "F", {"weight": 1}),
+ ("D", "E", {"weight": 4}),
+ ("D", "F", {"weight": 3}),
+ ("E", "F", {"weight": 2}),
+ ]
+ )
+ partition = ({"A", "B", "C"}, {"D", "E", "F"})
+ C = kernighan_lin_bisection(G, partition, max_iter=1)
+ assert_partition_equal(C, ({"A", "F", "C"}, {"D", "E", "B"}))
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_label_propagation.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_label_propagation.py
new file mode 100644
index 00000000..4be72dbf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_label_propagation.py
@@ -0,0 +1,241 @@
+from itertools import chain, combinations
+
+import pytest
+
+import networkx as nx
+
+
+def test_directed_not_supported():
+ with pytest.raises(nx.NetworkXNotImplemented):
+ # not supported for directed graphs
+ test = nx.DiGraph()
+ test.add_edge("a", "b")
+ test.add_edge("a", "c")
+ test.add_edge("b", "d")
+ result = nx.community.label_propagation_communities(test)
+
+
+def test_iterator_vs_iterable():
+ G = nx.empty_graph("a")
+ assert list(nx.community.label_propagation_communities(G)) == [{"a"}]
+ for community in nx.community.label_propagation_communities(G):
+ assert community == {"a"}
+ pytest.raises(TypeError, next, nx.community.label_propagation_communities(G))
+
+
+def test_one_node():
+ test = nx.Graph()
+ test.add_node("a")
+
+ # The expected communities are:
+ ground_truth = {frozenset(["a"])}
+
+ communities = nx.community.label_propagation_communities(test)
+ result = {frozenset(c) for c in communities}
+ assert result == ground_truth
+
+
+def test_unconnected_communities():
+ test = nx.Graph()
+ # community 1
+ test.add_edge("a", "c")
+ test.add_edge("a", "d")
+ test.add_edge("d", "c")
+ # community 2
+ test.add_edge("b", "e")
+ test.add_edge("e", "f")
+ test.add_edge("f", "b")
+
+ # The expected communities are:
+ ground_truth = {frozenset(["a", "c", "d"]), frozenset(["b", "e", "f"])}
+
+ communities = nx.community.label_propagation_communities(test)
+ result = {frozenset(c) for c in communities}
+ assert result == ground_truth
+
+
+def test_connected_communities():
+ test = nx.Graph()
+ # community 1
+ test.add_edge("a", "b")
+ test.add_edge("c", "a")
+ test.add_edge("c", "b")
+ test.add_edge("d", "a")
+ test.add_edge("d", "b")
+ test.add_edge("d", "c")
+ test.add_edge("e", "a")
+ test.add_edge("e", "b")
+ test.add_edge("e", "c")
+ test.add_edge("e", "d")
+ # community 2
+ test.add_edge("1", "2")
+ test.add_edge("3", "1")
+ test.add_edge("3", "2")
+ test.add_edge("4", "1")
+ test.add_edge("4", "2")
+ test.add_edge("4", "3")
+ test.add_edge("5", "1")
+ test.add_edge("5", "2")
+ test.add_edge("5", "3")
+ test.add_edge("5", "4")
+ # edge between community 1 and 2
+ test.add_edge("a", "1")
+ # community 3
+ test.add_edge("x", "y")
+ # community 4 with only a single node
+ test.add_node("z")
+
+ # The expected communities are:
+ ground_truth1 = {
+ frozenset(["a", "b", "c", "d", "e"]),
+ frozenset(["1", "2", "3", "4", "5"]),
+ frozenset(["x", "y"]),
+ frozenset(["z"]),
+ }
+ ground_truth2 = {
+ frozenset(["a", "b", "c", "d", "e", "1", "2", "3", "4", "5"]),
+ frozenset(["x", "y"]),
+ frozenset(["z"]),
+ }
+ ground_truth = (ground_truth1, ground_truth2)
+
+ communities = nx.community.label_propagation_communities(test)
+ result = {frozenset(c) for c in communities}
+ assert result in ground_truth
+
+
+def test_termination():
+ # ensure termination of asyn_lpa_communities in two cases
+ # that led to an endless loop in a previous version
+ test1 = nx.karate_club_graph()
+ test2 = nx.caveman_graph(2, 10)
+ test2.add_edges_from([(0, 20), (20, 10)])
+ nx.community.asyn_lpa_communities(test1)
+ nx.community.asyn_lpa_communities(test2)
+
+
+class TestAsynLpaCommunities:
+ def _check_communities(self, G, expected):
+ """Checks that the communities computed from the given graph ``G``
+ using the :func:`~networkx.asyn_lpa_communities` function match
+ the set of nodes given in ``expected``.
+
+ ``expected`` must be a :class:`set` of :class:`frozenset`
+ instances, each element of which is a node in the graph.
+
+ """
+ communities = nx.community.asyn_lpa_communities(G)
+ result = {frozenset(c) for c in communities}
+ assert result == expected
+
+ def test_null_graph(self):
+ G = nx.null_graph()
+ ground_truth = set()
+ self._check_communities(G, ground_truth)
+
+ def test_single_node(self):
+ G = nx.empty_graph(1)
+ ground_truth = {frozenset([0])}
+ self._check_communities(G, ground_truth)
+
+ def test_simple_communities(self):
+ # This graph is the disjoint union of two triangles.
+ G = nx.Graph(["ab", "ac", "bc", "de", "df", "fe"])
+ ground_truth = {frozenset("abc"), frozenset("def")}
+ self._check_communities(G, ground_truth)
+
+ def test_seed_argument(self):
+ G = nx.Graph(["ab", "ac", "bc", "de", "df", "fe"])
+ ground_truth = {frozenset("abc"), frozenset("def")}
+ communities = nx.community.asyn_lpa_communities(G, seed=1)
+ result = {frozenset(c) for c in communities}
+ assert result == ground_truth
+
+ def test_several_communities(self):
+ # This graph is the disjoint union of five triangles.
+ ground_truth = {frozenset(range(3 * i, 3 * (i + 1))) for i in range(5)}
+ edges = chain.from_iterable(combinations(c, 2) for c in ground_truth)
+ G = nx.Graph(edges)
+ self._check_communities(G, ground_truth)
+
+
+class TestFastLabelPropagationCommunities:
+ N = 100 # number of nodes
+ K = 15 # average node degree
+
+ def _check_communities(self, G, truth, weight=None, seed=42):
+ C = nx.community.fast_label_propagation_communities(G, weight=weight, seed=seed)
+ assert {frozenset(c) for c in C} == truth
+
+ def test_null_graph(self):
+ G = nx.null_graph()
+ truth = set()
+ self._check_communities(G, truth)
+
+ def test_empty_graph(self):
+ G = nx.empty_graph(self.N)
+ truth = {frozenset([i]) for i in G}
+ self._check_communities(G, truth)
+
+ def test_star_graph(self):
+ G = nx.star_graph(self.N)
+ truth = {frozenset(G)}
+ self._check_communities(G, truth)
+
+ def test_complete_graph(self):
+ G = nx.complete_graph(self.N)
+ truth = {frozenset(G)}
+ self._check_communities(G, truth)
+
+ def test_bipartite_graph(self):
+ G = nx.complete_bipartite_graph(self.N // 2, self.N // 2)
+ truth = {frozenset(G)}
+ self._check_communities(G, truth)
+
+ def test_random_graph(self):
+ G = nx.gnm_random_graph(self.N, self.N * self.K // 2, seed=42)
+ truth = {frozenset(G)}
+ self._check_communities(G, truth)
+
+ def test_disjoin_cliques(self):
+ G = nx.Graph(["ab", "AB", "AC", "BC", "12", "13", "14", "23", "24", "34"])
+ truth = {frozenset("ab"), frozenset("ABC"), frozenset("1234")}
+ self._check_communities(G, truth)
+
+ def test_ring_of_cliques(self):
+ N, K = self.N, self.K
+ G = nx.ring_of_cliques(N, K)
+ truth = {frozenset([K * i + k for k in range(K)]) for i in range(N)}
+ self._check_communities(G, truth)
+
+ def test_larger_graph(self):
+ G = nx.gnm_random_graph(100 * self.N, 50 * self.N * self.K, seed=42)
+ nx.community.fast_label_propagation_communities(G)
+
+ def test_graph_type(self):
+ G1 = nx.complete_graph(self.N, nx.MultiDiGraph())
+ G2 = nx.MultiGraph(G1)
+ G3 = nx.DiGraph(G1)
+ G4 = nx.Graph(G1)
+ truth = {frozenset(G1)}
+ self._check_communities(G1, truth)
+ self._check_communities(G2, truth)
+ self._check_communities(G3, truth)
+ self._check_communities(G4, truth)
+
+ def test_weight_argument(self):
+ G = nx.MultiDiGraph()
+ G.add_edge(1, 2, weight=1.41)
+ G.add_edge(2, 1, weight=1.41)
+ G.add_edge(2, 3)
+ G.add_edge(3, 4, weight=3.14)
+ truth = {frozenset({1, 2}), frozenset({3, 4})}
+ self._check_communities(G, truth, weight="weight")
+
+ def test_seed_argument(self):
+ G = nx.karate_club_graph()
+ C = nx.community.fast_label_propagation_communities(G, seed=2023)
+ truth = {frozenset(c) for c in C}
+ self._check_communities(G, truth, seed=2023)
+ # smoke test that seed=None works
+ C = nx.community.fast_label_propagation_communities(G, seed=None)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_louvain.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_louvain.py
new file mode 100644
index 00000000..816e6f14
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_louvain.py
@@ -0,0 +1,264 @@
+import pytest
+
+import networkx as nx
+
+
+def test_modularity_increase():
+ G = nx.LFR_benchmark_graph(
+ 250, 3, 1.5, 0.009, average_degree=5, min_community=20, seed=10
+ )
+ partition = [{u} for u in G.nodes()]
+ mod = nx.community.modularity(G, partition)
+ partition = nx.community.louvain_communities(G)
+
+ assert nx.community.modularity(G, partition) > mod
+
+
+def test_valid_partition():
+ G = nx.LFR_benchmark_graph(
+ 250, 3, 1.5, 0.009, average_degree=5, min_community=20, seed=10
+ )
+ H = G.to_directed()
+ partition = nx.community.louvain_communities(G)
+ partition2 = nx.community.louvain_communities(H)
+
+ assert nx.community.is_partition(G, partition)
+ assert nx.community.is_partition(H, partition2)
+
+
+def test_karate_club_partition():
+ G = nx.karate_club_graph()
+ part = [
+ {0, 1, 2, 3, 7, 9, 11, 12, 13, 17, 19, 21},
+ {16, 4, 5, 6, 10},
+ {23, 25, 27, 28, 24, 31},
+ {32, 33, 8, 14, 15, 18, 20, 22, 26, 29, 30},
+ ]
+ partition = nx.community.louvain_communities(G, seed=2, weight=None)
+
+ assert part == partition
+
+
+def test_partition_iterator():
+ G = nx.path_graph(15)
+ parts_iter = nx.community.louvain_partitions(G, seed=42)
+ first_part = next(parts_iter)
+ first_copy = [s.copy() for s in first_part]
+
+ # gh-5901 reports sets changing after next partition is yielded
+ assert first_copy[0] == first_part[0]
+ second_part = next(parts_iter)
+ assert first_copy[0] == first_part[0]
+
+
+def test_undirected_selfloops():
+ G = nx.karate_club_graph()
+ expected_partition = nx.community.louvain_communities(G, seed=2, weight=None)
+ part = [
+ {0, 1, 2, 3, 7, 9, 11, 12, 13, 17, 19, 21},
+ {16, 4, 5, 6, 10},
+ {23, 25, 27, 28, 24, 31},
+ {32, 33, 8, 14, 15, 18, 20, 22, 26, 29, 30},
+ ]
+ assert expected_partition == part
+
+ G.add_weighted_edges_from([(i, i, i * 1000) for i in range(9)])
+ # large self-loop weight impacts partition
+ partition = nx.community.louvain_communities(G, seed=2, weight="weight")
+ assert part != partition
+
+ # small self-loop weights aren't enough to impact partition in this graph
+ partition = nx.community.louvain_communities(G, seed=2, weight=None)
+ assert part == partition
+
+
+def test_directed_selfloops():
+ G = nx.DiGraph()
+ G.add_nodes_from(range(11))
+ G_edges = [
+ (0, 2),
+ (0, 1),
+ (1, 0),
+ (2, 1),
+ (2, 0),
+ (3, 4),
+ (4, 3),
+ (7, 8),
+ (8, 7),
+ (9, 10),
+ (10, 9),
+ ]
+ G.add_edges_from(G_edges)
+ G_expected_partition = nx.community.louvain_communities(G, seed=123, weight=None)
+
+ G.add_weighted_edges_from([(i, i, i * 1000) for i in range(3)])
+ # large self-loop weight impacts partition
+ G_partition = nx.community.louvain_communities(G, seed=123, weight="weight")
+ assert G_partition != G_expected_partition
+
+ # small self-loop weights aren't enough to impact partition in this graph
+ G_partition = nx.community.louvain_communities(G, seed=123, weight=None)
+ assert G_partition == G_expected_partition
+
+
+def test_directed_partition():
+ """
+ Test 2 cases that were looping infinitely
+ from issues #5175 and #5704
+ """
+ G = nx.DiGraph()
+ H = nx.DiGraph()
+ G.add_nodes_from(range(10))
+ H.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
+ G_edges = [
+ (0, 2),
+ (0, 1),
+ (1, 0),
+ (2, 1),
+ (2, 0),
+ (3, 4),
+ (4, 3),
+ (7, 8),
+ (8, 7),
+ (9, 10),
+ (10, 9),
+ ]
+ H_edges = [
+ (1, 2),
+ (1, 6),
+ (1, 9),
+ (2, 3),
+ (2, 4),
+ (2, 5),
+ (3, 4),
+ (4, 3),
+ (4, 5),
+ (5, 4),
+ (6, 7),
+ (6, 8),
+ (9, 10),
+ (9, 11),
+ (10, 11),
+ (11, 10),
+ ]
+ G.add_edges_from(G_edges)
+ H.add_edges_from(H_edges)
+
+ G_expected_partition = [{0, 1, 2}, {3, 4}, {5}, {6}, {8, 7}, {9, 10}]
+ G_partition = nx.community.louvain_communities(G, seed=123, weight=None)
+
+ H_expected_partition = [{2, 3, 4, 5}, {8, 1, 6, 7}, {9, 10, 11}]
+ H_partition = nx.community.louvain_communities(H, seed=123, weight=None)
+
+ assert G_partition == G_expected_partition
+ assert H_partition == H_expected_partition
+
+
+def test_none_weight_param():
+ G = nx.karate_club_graph()
+ nx.set_edge_attributes(
+ G, {edge: i * i for i, edge in enumerate(G.edges)}, name="foo"
+ )
+
+ part = [
+ {0, 1, 2, 3, 7, 9, 11, 12, 13, 17, 19, 21},
+ {16, 4, 5, 6, 10},
+ {23, 25, 27, 28, 24, 31},
+ {32, 33, 8, 14, 15, 18, 20, 22, 26, 29, 30},
+ ]
+ partition1 = nx.community.louvain_communities(G, weight=None, seed=2)
+ partition2 = nx.community.louvain_communities(G, weight="foo", seed=2)
+ partition3 = nx.community.louvain_communities(G, weight="weight", seed=2)
+
+ assert part == partition1
+ assert part != partition2
+ assert part != partition3
+ assert partition2 != partition3
+
+
+def test_quality():
+ G = nx.LFR_benchmark_graph(
+ 250, 3, 1.5, 0.009, average_degree=5, min_community=20, seed=10
+ )
+ H = nx.gn_graph(200, seed=1234)
+ I = nx.MultiGraph(G)
+ J = nx.MultiDiGraph(H)
+
+ partition = nx.community.louvain_communities(G)
+ partition2 = nx.community.louvain_communities(H)
+ partition3 = nx.community.louvain_communities(I)
+ partition4 = nx.community.louvain_communities(J)
+
+ quality = nx.community.partition_quality(G, partition)[0]
+ quality2 = nx.community.partition_quality(H, partition2)[0]
+ quality3 = nx.community.partition_quality(I, partition3)[0]
+ quality4 = nx.community.partition_quality(J, partition4)[0]
+
+ assert quality >= 0.65
+ assert quality2 >= 0.65
+ assert quality3 >= 0.65
+ assert quality4 >= 0.65
+
+
+def test_multigraph():
+ G = nx.karate_club_graph()
+ H = nx.MultiGraph(G)
+ G.add_edge(0, 1, weight=10)
+ H.add_edge(0, 1, weight=9)
+ G.add_edge(0, 9, foo=20)
+ H.add_edge(0, 9, foo=20)
+
+ partition1 = nx.community.louvain_communities(G, seed=1234)
+ partition2 = nx.community.louvain_communities(H, seed=1234)
+ partition3 = nx.community.louvain_communities(H, weight="foo", seed=1234)
+
+ assert partition1 == partition2 != partition3
+
+
+def test_resolution():
+ G = nx.LFR_benchmark_graph(
+ 250, 3, 1.5, 0.009, average_degree=5, min_community=20, seed=10
+ )
+
+ partition1 = nx.community.louvain_communities(G, resolution=0.5, seed=12)
+ partition2 = nx.community.louvain_communities(G, seed=12)
+ partition3 = nx.community.louvain_communities(G, resolution=2, seed=12)
+
+ assert len(partition1) <= len(partition2) <= len(partition3)
+
+
+def test_threshold():
+ G = nx.LFR_benchmark_graph(
+ 250, 3, 1.5, 0.009, average_degree=5, min_community=20, seed=10
+ )
+ partition1 = nx.community.louvain_communities(G, threshold=0.3, seed=2)
+ partition2 = nx.community.louvain_communities(G, seed=2)
+ mod1 = nx.community.modularity(G, partition1)
+ mod2 = nx.community.modularity(G, partition2)
+
+ assert mod1 <= mod2
+
+
+def test_empty_graph():
+ G = nx.Graph()
+ G.add_nodes_from(range(5))
+ expected = [{0}, {1}, {2}, {3}, {4}]
+ assert nx.community.louvain_communities(G) == expected
+
+
+def test_max_level():
+ G = nx.LFR_benchmark_graph(
+ 250, 3, 1.5, 0.009, average_degree=5, min_community=20, seed=10
+ )
+ parts_iter = nx.community.louvain_partitions(G, seed=42)
+ for max_level, expected in enumerate(parts_iter, 1):
+ partition = nx.community.louvain_communities(G, max_level=max_level, seed=42)
+ assert partition == expected
+ assert max_level > 1 # Ensure we are actually testing max_level
+ # max_level is an upper limit; it's okay if we stop before it's hit.
+ partition = nx.community.louvain_communities(G, max_level=max_level + 1, seed=42)
+ assert partition == expected
+ with pytest.raises(
+ ValueError, match="max_level argument must be a positive integer"
+ ):
+ nx.community.louvain_communities(G, max_level=0)
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_lukes.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_lukes.py
new file mode 100644
index 00000000..cfa48f0f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_lukes.py
@@ -0,0 +1,152 @@
+from itertools import product
+
+import pytest
+
+import networkx as nx
+
+EWL = "e_weight"
+NWL = "n_weight"
+
+
+# first test from the Lukes original paper
+def paper_1_case(float_edge_wt=False, explicit_node_wt=True, directed=False):
+ # problem-specific constants
+ limit = 3
+
+ # configuration
+ if float_edge_wt:
+ shift = 0.001
+ else:
+ shift = 0
+
+ if directed:
+ example_1 = nx.DiGraph()
+ else:
+ example_1 = nx.Graph()
+
+ # graph creation
+ example_1.add_edge(1, 2, **{EWL: 3 + shift})
+ example_1.add_edge(1, 4, **{EWL: 2 + shift})
+ example_1.add_edge(2, 3, **{EWL: 4 + shift})
+ example_1.add_edge(2, 5, **{EWL: 6 + shift})
+
+ # node weights
+ if explicit_node_wt:
+ nx.set_node_attributes(example_1, 1, NWL)
+ wtu = NWL
+ else:
+ wtu = None
+
+ # partitioning
+ clusters_1 = {
+ frozenset(x)
+ for x in nx.community.lukes_partitioning(
+ example_1, limit, node_weight=wtu, edge_weight=EWL
+ )
+ }
+
+ return clusters_1
+
+
+# second test from the Lukes original paper
+def paper_2_case(explicit_edge_wt=True, directed=False):
+ # problem specific constants
+ byte_block_size = 32
+
+ # configuration
+ if directed:
+ example_2 = nx.DiGraph()
+ else:
+ example_2 = nx.Graph()
+
+ if explicit_edge_wt:
+ edic = {EWL: 1}
+ wtu = EWL
+ else:
+ edic = {}
+ wtu = None
+
+ # graph creation
+ example_2.add_edge("name", "home_address", **edic)
+ example_2.add_edge("name", "education", **edic)
+ example_2.add_edge("education", "bs", **edic)
+ example_2.add_edge("education", "ms", **edic)
+ example_2.add_edge("education", "phd", **edic)
+ example_2.add_edge("name", "telephone", **edic)
+ example_2.add_edge("telephone", "home", **edic)
+ example_2.add_edge("telephone", "office", **edic)
+ example_2.add_edge("office", "no1", **edic)
+ example_2.add_edge("office", "no2", **edic)
+
+ example_2.nodes["name"][NWL] = 20
+ example_2.nodes["education"][NWL] = 10
+ example_2.nodes["bs"][NWL] = 1
+ example_2.nodes["ms"][NWL] = 1
+ example_2.nodes["phd"][NWL] = 1
+ example_2.nodes["home_address"][NWL] = 8
+ example_2.nodes["telephone"][NWL] = 8
+ example_2.nodes["home"][NWL] = 8
+ example_2.nodes["office"][NWL] = 4
+ example_2.nodes["no1"][NWL] = 1
+ example_2.nodes["no2"][NWL] = 1
+
+ # partitioning
+ clusters_2 = {
+ frozenset(x)
+ for x in nx.community.lukes_partitioning(
+ example_2, byte_block_size, node_weight=NWL, edge_weight=wtu
+ )
+ }
+
+ return clusters_2
+
+
+def test_paper_1_case():
+ ground_truth = {frozenset([1, 4]), frozenset([2, 3, 5])}
+
+ tf = (True, False)
+ for flt, nwt, drc in product(tf, tf, tf):
+ part = paper_1_case(flt, nwt, drc)
+ assert part == ground_truth
+
+
+def test_paper_2_case():
+ ground_truth = {
+ frozenset(["education", "bs", "ms", "phd"]),
+ frozenset(["name", "home_address"]),
+ frozenset(["telephone", "home", "office", "no1", "no2"]),
+ }
+
+ tf = (True, False)
+ for ewt, drc in product(tf, tf):
+ part = paper_2_case(ewt, drc)
+ assert part == ground_truth
+
+
+def test_mandatory_tree():
+ not_a_tree = nx.complete_graph(4)
+
+ with pytest.raises(nx.NotATree):
+ nx.community.lukes_partitioning(not_a_tree, 5)
+
+
+def test_mandatory_integrality():
+ byte_block_size = 32
+
+ ex_1_broken = nx.DiGraph()
+
+ ex_1_broken.add_edge(1, 2, **{EWL: 3.2})
+ ex_1_broken.add_edge(1, 4, **{EWL: 2.4})
+ ex_1_broken.add_edge(2, 3, **{EWL: 4.0})
+ ex_1_broken.add_edge(2, 5, **{EWL: 6.3})
+
+ ex_1_broken.nodes[1][NWL] = 1.2 # !
+ ex_1_broken.nodes[2][NWL] = 1
+ ex_1_broken.nodes[3][NWL] = 1
+ ex_1_broken.nodes[4][NWL] = 1
+ ex_1_broken.nodes[5][NWL] = 2
+
+ with pytest.raises(TypeError):
+ nx.community.lukes_partitioning(
+ ex_1_broken, byte_block_size, node_weight=NWL, edge_weight=EWL
+ )
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_modularity_max.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_modularity_max.py
new file mode 100644
index 00000000..0121367f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_modularity_max.py
@@ -0,0 +1,340 @@
+import pytest
+
+import networkx as nx
+from networkx.algorithms.community import (
+ greedy_modularity_communities,
+ naive_greedy_modularity_communities,
+)
+
+
+@pytest.mark.parametrize(
+ "func", (greedy_modularity_communities, naive_greedy_modularity_communities)
+)
+def test_modularity_communities(func):
+ G = nx.karate_club_graph()
+ john_a = frozenset(
+ [8, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
+ )
+ mr_hi = frozenset([0, 4, 5, 6, 10, 11, 16, 19])
+ overlap = frozenset([1, 2, 3, 7, 9, 12, 13, 17, 21])
+ expected = {john_a, overlap, mr_hi}
+ assert set(func(G, weight=None)) == expected
+
+
+@pytest.mark.parametrize(
+ "func", (greedy_modularity_communities, naive_greedy_modularity_communities)
+)
+def test_modularity_communities_categorical_labels(func):
+ # Using other than 0-starting contiguous integers as node-labels.
+ G = nx.Graph(
+ [
+ ("a", "b"),
+ ("a", "c"),
+ ("b", "c"),
+ ("b", "d"), # inter-community edge
+ ("d", "e"),
+ ("d", "f"),
+ ("d", "g"),
+ ("f", "g"),
+ ("d", "e"),
+ ("f", "e"),
+ ]
+ )
+ expected = {frozenset({"f", "g", "e", "d"}), frozenset({"a", "b", "c"})}
+ assert set(func(G)) == expected
+
+
+def test_greedy_modularity_communities_components():
+ # Test for gh-5530
+ G = nx.Graph([(0, 1), (2, 3), (4, 5), (5, 6)])
+ # usual case with 3 components
+ assert greedy_modularity_communities(G) == [{4, 5, 6}, {0, 1}, {2, 3}]
+ # best_n can make the algorithm continue even when modularity goes down
+ assert greedy_modularity_communities(G, best_n=3) == [{4, 5, 6}, {0, 1}, {2, 3}]
+ assert greedy_modularity_communities(G, best_n=2) == [{0, 1, 4, 5, 6}, {2, 3}]
+ assert greedy_modularity_communities(G, best_n=1) == [{0, 1, 2, 3, 4, 5, 6}]
+
+
+def test_greedy_modularity_communities_relabeled():
+ # Test for gh-4966
+ G = nx.balanced_tree(2, 2)
+ mapping = {0: "a", 1: "b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 7: "h"}
+ G = nx.relabel_nodes(G, mapping)
+ expected = [frozenset({"e", "d", "a", "b"}), frozenset({"c", "f", "g"})]
+ assert greedy_modularity_communities(G) == expected
+
+
+def test_greedy_modularity_communities_directed():
+ G = nx.DiGraph(
+ [
+ ("a", "b"),
+ ("a", "c"),
+ ("b", "c"),
+ ("b", "d"), # inter-community edge
+ ("d", "e"),
+ ("d", "f"),
+ ("d", "g"),
+ ("f", "g"),
+ ("d", "e"),
+ ("f", "e"),
+ ]
+ )
+ expected = [frozenset({"f", "g", "e", "d"}), frozenset({"a", "b", "c"})]
+ assert greedy_modularity_communities(G) == expected
+
+ # with loops
+ G = nx.DiGraph()
+ G.add_edges_from(
+ [(1, 1), (1, 2), (1, 3), (2, 3), (1, 4), (4, 4), (5, 5), (4, 5), (4, 6), (5, 6)]
+ )
+ expected = [frozenset({1, 2, 3}), frozenset({4, 5, 6})]
+ assert greedy_modularity_communities(G) == expected
+
+
+@pytest.mark.parametrize(
+ "func", (greedy_modularity_communities, naive_greedy_modularity_communities)
+)
+def test_modularity_communities_weighted(func):
+ G = nx.balanced_tree(2, 3)
+ for a, b in G.edges:
+ if ((a == 1) or (a == 2)) and (b != 0):
+ G[a][b]["weight"] = 10.0
+ else:
+ G[a][b]["weight"] = 1.0
+
+ expected = [{0, 1, 3, 4, 7, 8, 9, 10}, {2, 5, 6, 11, 12, 13, 14}]
+
+ assert func(G, weight="weight") == expected
+ assert func(G, weight="weight", resolution=0.9) == expected
+ assert func(G, weight="weight", resolution=0.3) == expected
+ assert func(G, weight="weight", resolution=1.1) != expected
+
+
+def test_modularity_communities_floating_point():
+ # check for floating point error when used as key in the mapped_queue dict.
+ # Test for gh-4992 and gh-5000
+ G = nx.Graph()
+ G.add_weighted_edges_from(
+ [(0, 1, 12), (1, 4, 71), (2, 3, 15), (2, 4, 10), (3, 6, 13)]
+ )
+ expected = [{0, 1, 4}, {2, 3, 6}]
+ assert greedy_modularity_communities(G, weight="weight") == expected
+ assert (
+ greedy_modularity_communities(G, weight="weight", resolution=0.99) == expected
+ )
+
+
+def test_modularity_communities_directed_weighted():
+ G = nx.DiGraph()
+ G.add_weighted_edges_from(
+ [
+ (1, 2, 5),
+ (1, 3, 3),
+ (2, 3, 6),
+ (2, 6, 1),
+ (1, 4, 1),
+ (4, 5, 3),
+ (4, 6, 7),
+ (5, 6, 2),
+ (5, 7, 5),
+ (5, 8, 4),
+ (6, 8, 3),
+ ]
+ )
+ expected = [frozenset({4, 5, 6, 7, 8}), frozenset({1, 2, 3})]
+ assert greedy_modularity_communities(G, weight="weight") == expected
+
+ # A large weight of the edge (2, 6) causes 6 to change group, even if it shares
+ # only one connection with the new group and 3 with the old one.
+ G[2][6]["weight"] = 20
+ expected = [frozenset({1, 2, 3, 6}), frozenset({4, 5, 7, 8})]
+ assert greedy_modularity_communities(G, weight="weight") == expected
+
+
+def test_greedy_modularity_communities_multigraph():
+ G = nx.MultiGraph()
+ G.add_edges_from(
+ [
+ (1, 2),
+ (1, 2),
+ (1, 3),
+ (2, 3),
+ (1, 4),
+ (2, 4),
+ (4, 5),
+ (5, 6),
+ (5, 7),
+ (5, 7),
+ (6, 7),
+ (7, 8),
+ (5, 8),
+ ]
+ )
+ expected = [frozenset({1, 2, 3, 4}), frozenset({5, 6, 7, 8})]
+ assert greedy_modularity_communities(G) == expected
+
+ # Converting (4, 5) into a multi-edge causes node 4 to change group.
+ G.add_edge(4, 5)
+ expected = [frozenset({4, 5, 6, 7, 8}), frozenset({1, 2, 3})]
+ assert greedy_modularity_communities(G) == expected
+
+
+def test_greedy_modularity_communities_multigraph_weighted():
+ G = nx.MultiGraph()
+ G.add_weighted_edges_from(
+ [
+ (1, 2, 5),
+ (1, 2, 3),
+ (1, 3, 6),
+ (1, 3, 6),
+ (2, 3, 4),
+ (1, 4, 1),
+ (1, 4, 1),
+ (2, 4, 3),
+ (2, 4, 3),
+ (4, 5, 1),
+ (5, 6, 3),
+ (5, 6, 7),
+ (5, 6, 4),
+ (5, 7, 9),
+ (5, 7, 9),
+ (6, 7, 8),
+ (7, 8, 2),
+ (7, 8, 2),
+ (5, 8, 6),
+ (5, 8, 6),
+ ]
+ )
+ expected = [frozenset({1, 2, 3, 4}), frozenset({5, 6, 7, 8})]
+ assert greedy_modularity_communities(G, weight="weight") == expected
+
+ # Adding multi-edge (4, 5, 16) causes node 4 to change group.
+ G.add_edge(4, 5, weight=16)
+ expected = [frozenset({4, 5, 6, 7, 8}), frozenset({1, 2, 3})]
+ assert greedy_modularity_communities(G, weight="weight") == expected
+
+ # Increasing the weight of edge (1, 4) causes node 4 to return to the former group.
+ G[1][4][1]["weight"] = 3
+ expected = [frozenset({1, 2, 3, 4}), frozenset({5, 6, 7, 8})]
+ assert greedy_modularity_communities(G, weight="weight") == expected
+
+
+def test_greed_modularity_communities_multidigraph():
+ G = nx.MultiDiGraph()
+ G.add_edges_from(
+ [
+ (1, 2),
+ (1, 2),
+ (3, 1),
+ (2, 3),
+ (2, 3),
+ (3, 2),
+ (1, 4),
+ (2, 4),
+ (4, 2),
+ (4, 5),
+ (5, 6),
+ (5, 6),
+ (6, 5),
+ (5, 7),
+ (6, 7),
+ (7, 8),
+ (5, 8),
+ (8, 4),
+ ]
+ )
+ expected = [frozenset({1, 2, 3, 4}), frozenset({5, 6, 7, 8})]
+ assert greedy_modularity_communities(G, weight="weight") == expected
+
+
+def test_greed_modularity_communities_multidigraph_weighted():
+ G = nx.MultiDiGraph()
+ G.add_weighted_edges_from(
+ [
+ (1, 2, 5),
+ (1, 2, 3),
+ (3, 1, 6),
+ (1, 3, 6),
+ (3, 2, 4),
+ (1, 4, 2),
+ (1, 4, 5),
+ (2, 4, 3),
+ (3, 2, 8),
+ (4, 2, 3),
+ (4, 3, 5),
+ (4, 5, 2),
+ (5, 6, 3),
+ (5, 6, 7),
+ (6, 5, 4),
+ (5, 7, 9),
+ (5, 7, 9),
+ (7, 6, 8),
+ (7, 8, 2),
+ (8, 7, 2),
+ (5, 8, 6),
+ (5, 8, 6),
+ ]
+ )
+ expected = [frozenset({1, 2, 3, 4}), frozenset({5, 6, 7, 8})]
+ assert greedy_modularity_communities(G, weight="weight") == expected
+
+
+def test_resolution_parameter_impact():
+ G = nx.barbell_graph(5, 3)
+
+ gamma = 1
+ expected = [frozenset(range(5)), frozenset(range(8, 13)), frozenset(range(5, 8))]
+ assert greedy_modularity_communities(G, resolution=gamma) == expected
+ assert naive_greedy_modularity_communities(G, resolution=gamma) == expected
+
+ gamma = 2.5
+ expected = [{0, 1, 2, 3}, {9, 10, 11, 12}, {5, 6, 7}, {4}, {8}]
+ assert greedy_modularity_communities(G, resolution=gamma) == expected
+ assert naive_greedy_modularity_communities(G, resolution=gamma) == expected
+
+ gamma = 0.3
+ expected = [frozenset(range(8)), frozenset(range(8, 13))]
+ assert greedy_modularity_communities(G, resolution=gamma) == expected
+ assert naive_greedy_modularity_communities(G, resolution=gamma) == expected
+
+
+def test_cutoff_parameter():
+ G = nx.circular_ladder_graph(4)
+
+ # No aggregation:
+ expected = [{k} for k in range(8)]
+ assert greedy_modularity_communities(G, cutoff=8) == expected
+
+ # Aggregation to half order (number of nodes)
+ expected = [{k, k + 1} for k in range(0, 8, 2)]
+ assert greedy_modularity_communities(G, cutoff=4) == expected
+
+ # Default aggregation case (here, 2 communities emerge)
+ expected = [frozenset(range(4)), frozenset(range(4, 8))]
+ assert greedy_modularity_communities(G, cutoff=1) == expected
+
+
+def test_best_n():
+ G = nx.barbell_graph(5, 3)
+
+ # Same result as without enforcing cutoff:
+ best_n = 3
+ expected = [frozenset(range(5)), frozenset(range(8, 13)), frozenset(range(5, 8))]
+ assert greedy_modularity_communities(G, best_n=best_n) == expected
+
+ # One additional merging step:
+ best_n = 2
+ expected = [frozenset(range(8)), frozenset(range(8, 13))]
+ assert greedy_modularity_communities(G, best_n=best_n) == expected
+
+ # Two additional merging steps:
+ best_n = 1
+ expected = [frozenset(range(13))]
+ assert greedy_modularity_communities(G, best_n=best_n) == expected
+
+
+def test_greedy_modularity_communities_corner_cases():
+ G = nx.empty_graph()
+ assert nx.community.greedy_modularity_communities(G) == []
+ G.add_nodes_from(range(3))
+ assert nx.community.greedy_modularity_communities(G) == [{0}, {1}, {2}]
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_quality.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_quality.py
new file mode 100644
index 00000000..c502c7e3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_quality.py
@@ -0,0 +1,139 @@
+"""Unit tests for the :mod:`networkx.algorithms.community.quality`
+module.
+
+"""
+
+import pytest
+
+import networkx as nx
+from networkx import barbell_graph
+from networkx.algorithms.community import modularity, partition_quality
+from networkx.algorithms.community.quality import inter_community_edges
+
+
+class TestPerformance:
+ """Unit tests for the :func:`performance` function."""
+
+ def test_bad_partition(self):
+ """Tests that a poor partition has a low performance measure."""
+ G = barbell_graph(3, 0)
+ partition = [{0, 1, 4}, {2, 3, 5}]
+ assert 8 / 15 == pytest.approx(partition_quality(G, partition)[1], abs=1e-7)
+
+ def test_good_partition(self):
+ """Tests that a good partition has a high performance measure."""
+ G = barbell_graph(3, 0)
+ partition = [{0, 1, 2}, {3, 4, 5}]
+ assert 14 / 15 == pytest.approx(partition_quality(G, partition)[1], abs=1e-7)
+
+
+class TestCoverage:
+ """Unit tests for the :func:`coverage` function."""
+
+ def test_bad_partition(self):
+ """Tests that a poor partition has a low coverage measure."""
+ G = barbell_graph(3, 0)
+ partition = [{0, 1, 4}, {2, 3, 5}]
+ assert 3 / 7 == pytest.approx(partition_quality(G, partition)[0], abs=1e-7)
+
+ def test_good_partition(self):
+ """Tests that a good partition has a high coverage measure."""
+ G = barbell_graph(3, 0)
+ partition = [{0, 1, 2}, {3, 4, 5}]
+ assert 6 / 7 == pytest.approx(partition_quality(G, partition)[0], abs=1e-7)
+
+
+def test_modularity():
+ G = nx.barbell_graph(3, 0)
+ C = [{0, 1, 4}, {2, 3, 5}]
+ assert (-16 / (14**2)) == pytest.approx(modularity(G, C), abs=1e-7)
+ C = [{0, 1, 2}, {3, 4, 5}]
+ assert (35 * 2) / (14**2) == pytest.approx(modularity(G, C), abs=1e-7)
+
+ n = 1000
+ G = nx.erdos_renyi_graph(n, 0.09, seed=42, directed=True)
+ C = [set(range(n // 2)), set(range(n // 2, n))]
+ assert 0.00017154251389292754 == pytest.approx(modularity(G, C), abs=1e-7)
+
+ G = nx.margulis_gabber_galil_graph(10)
+ mid_value = G.number_of_nodes() // 2
+ nodes = list(G.nodes)
+ C = [set(nodes[:mid_value]), set(nodes[mid_value:])]
+ assert 0.13 == pytest.approx(modularity(G, C), abs=1e-7)
+
+ G = nx.DiGraph()
+ G.add_edges_from([(2, 1), (2, 3), (3, 4)])
+ C = [{1, 2}, {3, 4}]
+ assert 2 / 9 == pytest.approx(modularity(G, C), abs=1e-7)
+
+
+def test_modularity_resolution():
+ G = nx.barbell_graph(3, 0)
+ C = [{0, 1, 4}, {2, 3, 5}]
+ assert modularity(G, C) == pytest.approx(3 / 7 - 100 / 14**2)
+ gamma = 2
+ result = modularity(G, C, resolution=gamma)
+ assert result == pytest.approx(3 / 7 - gamma * 100 / 14**2)
+ gamma = 0.2
+ result = modularity(G, C, resolution=gamma)
+ assert result == pytest.approx(3 / 7 - gamma * 100 / 14**2)
+
+ C = [{0, 1, 2}, {3, 4, 5}]
+ assert modularity(G, C) == pytest.approx(6 / 7 - 98 / 14**2)
+ gamma = 2
+ result = modularity(G, C, resolution=gamma)
+ assert result == pytest.approx(6 / 7 - gamma * 98 / 14**2)
+ gamma = 0.2
+ result = modularity(G, C, resolution=gamma)
+ assert result == pytest.approx(6 / 7 - gamma * 98 / 14**2)
+
+ G = nx.barbell_graph(5, 3)
+ C = [frozenset(range(5)), frozenset(range(8, 13)), frozenset(range(5, 8))]
+ gamma = 1
+ result = modularity(G, C, resolution=gamma)
+ # This C is maximal for gamma=1: modularity = 0.518229
+ assert result == pytest.approx((22 / 24) - gamma * (918 / (48**2)))
+ gamma = 2
+ result = modularity(G, C, resolution=gamma)
+ assert result == pytest.approx((22 / 24) - gamma * (918 / (48**2)))
+ gamma = 0.2
+ result = modularity(G, C, resolution=gamma)
+ assert result == pytest.approx((22 / 24) - gamma * (918 / (48**2)))
+
+ C = [{0, 1, 2, 3}, {9, 10, 11, 12}, {5, 6, 7}, {4}, {8}]
+ gamma = 1
+ result = modularity(G, C, resolution=gamma)
+ assert result == pytest.approx((14 / 24) - gamma * (598 / (48**2)))
+ gamma = 2.5
+ result = modularity(G, C, resolution=gamma)
+ # This C is maximal for gamma=2.5: modularity = -0.06553819
+ assert result == pytest.approx((14 / 24) - gamma * (598 / (48**2)))
+ gamma = 0.2
+ result = modularity(G, C, resolution=gamma)
+ assert result == pytest.approx((14 / 24) - gamma * (598 / (48**2)))
+
+ C = [frozenset(range(8)), frozenset(range(8, 13))]
+ gamma = 1
+ result = modularity(G, C, resolution=gamma)
+ assert result == pytest.approx((23 / 24) - gamma * (1170 / (48**2)))
+ gamma = 2
+ result = modularity(G, C, resolution=gamma)
+ assert result == pytest.approx((23 / 24) - gamma * (1170 / (48**2)))
+ gamma = 0.3
+ result = modularity(G, C, resolution=gamma)
+ # This C is maximal for gamma=0.3: modularity = 0.805990
+ assert result == pytest.approx((23 / 24) - gamma * (1170 / (48**2)))
+
+
+def test_inter_community_edges_with_digraphs():
+ G = nx.complete_graph(2, create_using=nx.DiGraph())
+ partition = [{0}, {1}]
+ assert inter_community_edges(G, partition) == 2
+
+ G = nx.complete_graph(10, create_using=nx.DiGraph())
+ partition = [{0}, {1, 2}, {3, 4, 5}, {6, 7, 8, 9}]
+ assert inter_community_edges(G, partition) == 70
+
+ G = nx.cycle_graph(4, create_using=nx.DiGraph())
+ partition = [{0, 1}, {2, 3}]
+ assert inter_community_edges(G, partition) == 2
diff --git a/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_utils.py b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_utils.py
new file mode 100644
index 00000000..ea019db9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/networkx/algorithms/community/tests/test_utils.py
@@ -0,0 +1,26 @@
+"""Unit tests for the :mod:`networkx.algorithms.community.utils` module."""
+
+import networkx as nx
+
+
+def test_is_partition():
+ G = nx.empty_graph(3)
+ assert nx.community.is_partition(G, [{0, 1}, {2}])
+ assert nx.community.is_partition(G, ({0, 1}, {2}))
+ assert nx.community.is_partition(G, ([0, 1], [2]))
+ assert nx.community.is_partition(G, [[0, 1], [2]])
+
+
+def test_not_covering():
+ G = nx.empty_graph(3)
+ assert not nx.community.is_partition(G, [{0}, {1}])
+
+
+def test_not_disjoint():
+ G = nx.empty_graph(3)
+ assert not nx.community.is_partition(G, [{0, 1}, {1, 2}])
+
+
+def test_not_node():
+ G = nx.empty_graph(3)
+ assert not nx.community.is_partition(G, [{0, 1}, {3}])