1"""Asynchronous Fluid Communities algorithm for community detection."""
2
3from collections import Counter
4
5import networkx as nx
6from networkx.algorithms.components import is_connected
7from networkx.exception import NetworkXError
8from networkx.utils import groups, not_implemented_for, py_random_state
9
10__all__ = ["asyn_fluidc"]
11
12
13@not_implemented_for("directed")
14@not_implemented_for("multigraph")
15@py_random_state(3)
16@nx._dispatchable
17def asyn_fluidc(G, k, max_iter=100, seed=None):
18 """Returns communities in `G` as detected by Fluid Communities algorithm.
19
20 The asynchronous fluid communities algorithm is described in
21 [1]_. The algorithm is based on the simple idea of fluids interacting
22 in an environment, expanding and pushing each other. Its initialization is
23 random, so found communities may vary on different executions.
24
25 The algorithm proceeds as follows. First each of the initial k communities
26 is initialized in a random vertex in the graph. Then the algorithm iterates
27 over all vertices in a random order, updating the community of each vertex
28 based on its own community and the communities of its neighbors. This
29 process is performed several times until convergence.
30 At all times, each community has a total density of 1, which is equally
31 distributed among the vertices it contains. If a vertex changes of
32 community, vertex densities of affected communities are adjusted
33 immediately. When a complete iteration over all vertices is done, such that
34 no vertex changes the community it belongs to, the algorithm has converged
35 and returns.
36
37 This is the original version of the algorithm described in [1]_.
38 Unfortunately, it does not support weighted graphs yet.
39
40 Parameters
41 ----------
42 G : NetworkX graph
43 Graph must be simple and undirected.
44
45 k : integer
46 The number of communities to be found.
47
48 max_iter : integer
49 The number of maximum iterations allowed. By default 100.
50
51 seed : integer, random_state, or None (default)
52 Indicator of random number generation state.
53 See :ref:`Randomness<randomness>`.
54
55 Returns
56 -------
57 communities : iterable
58 Iterable of communities given as sets of nodes.
59
60 Notes
61 -----
62 k variable is not an optional argument.
63
64 References
65 ----------
66 .. [1] Parés F., Garcia-Gasulla D. et al. "Fluid Communities: A
67 Competitive and Highly Scalable Community Detection Algorithm".
68 [https://arxiv.org/pdf/1703.09307.pdf].
69 """
70 # Initial checks
71 if not isinstance(k, int):
72 raise NetworkXError("k must be an integer.")
73 if not k > 0:
74 raise NetworkXError("k must be greater than 0.")
75 if not is_connected(G):
76 raise NetworkXError("Fluid Communities require connected Graphs.")
77 if len(G) < k:
78 raise NetworkXError("k cannot be bigger than the number of nodes.")
79 if max_iter <= 0:
80 msg = f"{max_iter=} must be greater than 0"
81 raise ValueError(msg)
82 # Initialization
83 max_density = 1.0
84 vertices = list(G)
85 seed.shuffle(vertices)
86 communities = {n: i for i, n in enumerate(vertices[:k])}
87 density = {}
88 com_to_numvertices = {}
89 for vertex in communities:
90 com_to_numvertices[communities[vertex]] = 1
91 density[communities[vertex]] = max_density
92 # Set up control variables and start iterating
93 iter_count = 0
94 cont = True
95 while cont and iter_count < max_iter:
96 cont = False
97 iter_count += 1
98 # Loop over all vertices in graph in a random order
99 vertices = list(G)
100 seed.shuffle(vertices)
101 for vertex in vertices:
102 # Updating rule
103 com_counter = Counter()
104 # Take into account self vertex community
105 try:
106 com_counter.update({communities[vertex]: density[communities[vertex]]})
107 except KeyError:
108 pass
109 # Gather neighbor vertex communities
110 for v in G[vertex]:
111 try:
112 com_counter.update({communities[v]: density[communities[v]]})
113 except KeyError:
114 continue
115 # Check which is the community with highest density
116 new_com = -1
117 if len(com_counter.keys()) > 0:
118 max_freq = max(com_counter.values())
119 best_communities = [
120 com
121 for com, freq in com_counter.items()
122 if (max_freq - freq) < 0.0001
123 ]
124 # If actual vertex com in best communities, it is preserved
125 try:
126 if communities[vertex] in best_communities:
127 new_com = communities[vertex]
128 except KeyError:
129 pass
130
131 # If vertex community changes...
132 if new_com == -1:
133 # Set flag of non-convergence
134 cont = True
135 # Randomly chose a new community from candidates
136 new_com = seed.choice(best_communities)
137 # Update previous community status
138 try:
139 com_to_numvertices[communities[vertex]] -= 1
140 density[communities[vertex]] = (
141 max_density / com_to_numvertices[communities[vertex]]
142 )
143 except KeyError:
144 pass
145 # Update new community status
146 communities[vertex] = new_com
147 com_to_numvertices[communities[vertex]] += 1
148 density[communities[vertex]] = (
149 max_density / com_to_numvertices[communities[vertex]]
150 )
151 # If maximum iterations reached --> output actual results
152 # Return results by grouping communities as list of vertices
153 return iter(groups(communities).values())