Skip to content

Commit bdad808

Browse files
authored
feat: expose fluid communities to python (#835)
1 parent 027237b commit bdad808

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

src/_igraph/graphobject.c

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13743,6 +13743,38 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self,
1374313743
return error ? NULL : Py_BuildValue("Nd", res, (double) quality);
1374413744
}
1374513745

13746+
/**
13747+
* Fluid communities
13748+
*/
13749+
PyObject *igraphmodule_Graph_community_fluid_communities(igraphmodule_GraphObject *self,
13750+
PyObject *args, PyObject *kwds) {
13751+
static char *kwlist[] = {"no_of_communities", NULL};
13752+
Py_ssize_t no_of_communities;
13753+
igraph_vector_int_t membership;
13754+
PyObject *result;
13755+
13756+
// Parse the Python integer argument
13757+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "n", kwlist, &no_of_communities)) {
13758+
return NULL;
13759+
}
13760+
13761+
if (igraph_vector_int_init(&membership, 0)) {
13762+
igraphmodule_handle_igraph_error();
13763+
return NULL;
13764+
}
13765+
13766+
if (igraph_community_fluid_communities(&self->g, no_of_communities, &membership)) {
13767+
igraphmodule_handle_igraph_error();
13768+
igraph_vector_int_destroy(&membership);
13769+
return NULL;
13770+
}
13771+
13772+
result = igraphmodule_vector_int_t_to_PyList(&membership);
13773+
igraph_vector_int_destroy(&membership);
13774+
13775+
return result;
13776+
}
13777+
1374613778
/**********************************************************************
1374713779
* Random walks *
1374813780
**********************************************************************/
@@ -18394,6 +18426,28 @@ struct PyMethodDef igraphmodule_Graph_methods[] = {
1839418426
"\n"
1839518427
"@see: modularity()\n"
1839618428
},
18429+
{"community_fluid_communities",
18430+
(PyCFunction) igraphmodule_Graph_community_fluid_communities,
18431+
METH_VARARGS | METH_KEYWORDS,
18432+
"community_fluid_communities(no_of_communities)\n--\n\n"
18433+
"Community detection based on fluids interacting on the graph.\n\n"
18434+
"The algorithm is based on the simple idea of several fluids interacting\n"
18435+
"in a non-homogeneous environment (the graph topology), expanding and\n"
18436+
"contracting based on their interaction and density. Weighted graphs are\n"
18437+
"not supported.\n\n"
18438+
"B{Reference}\n\n"
18439+
" - Parés F, Gasulla DG, et. al. (2018) Fluid Communities: A Competitive,\n"
18440+
" Scalable and Diverse Community Detection Algorithm. In: Complex Networks\n"
18441+
" & Their Applications VI: Proceedings of Complex Networks 2017 (The Sixth\n"
18442+
" International Conference on Complex Networks and Their Applications),\n"
18443+
" Springer, vol 689, p 229. https://doi.org/10.1007/978-3-319-72150-7_19\n\n"
18444+
"@param no_of_communities: The number of communities to be found. Must be\n"
18445+
" greater than 0 and fewer than number of vertices in the graph.\n"
18446+
"@return: a list with the community membership of each vertex.\n"
18447+
"@note: The graph must be simple and connected. Edge directions will be\n"
18448+
" ignored if the graph is directed.\n"
18449+
"@note: Time complexity: O(|E|)\n",
18450+
},
1839718451
{"community_infomap",
1839818452
(PyCFunction) igraphmodule_Graph_community_infomap,
1839918453
METH_VARARGS | METH_KEYWORDS,
@@ -18402,7 +18456,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = {
1840218456
"method of Martin Rosvall and Carl T. Bergstrom.\n\n"
1840318457
"See U{https://www.mapequation.org} for a visualization of the algorithm\n"
1840418458
"or one of the references provided below.\n"
18405-
"B{References}\n"
18459+
"B{Reference}: "
1840618460
" - M. Rosvall and C. T. Bergstrom: I{Maps of information flow reveal\n"
1840718461
" community structure in complex networks}. PNAS 105, 1118 (2008).\n"
1840818462
" U{https://arxiv.org/abs/0707.0609}\n"

src/igraph/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
_community_multilevel,
110110
_community_optimal_modularity,
111111
_community_edge_betweenness,
112+
_community_fluid_communities,
112113
_community_spinglass,
113114
_community_walktrap,
114115
_k_core,
@@ -658,6 +659,7 @@ def es(self):
658659
community_multilevel = _community_multilevel
659660
community_optimal_modularity = _community_optimal_modularity
660661
community_edge_betweenness = _community_edge_betweenness
662+
community_fluid_communities = _community_fluid_communities
661663
community_spinglass = _community_spinglass
662664
community_walktrap = _community_walktrap
663665
k_core = _k_core
@@ -1100,6 +1102,7 @@ def write(graph, filename, *args, **kwds):
11001102
_community_multilevel,
11011103
_community_optimal_modularity,
11021104
_community_edge_betweenness,
1105+
_community_fluid_communities,
11031106
_community_spinglass,
11041107
_community_walktrap,
11051108
_k_core,

src/igraph/community.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,47 @@ def _community_leiden(
468468
)
469469

470470

471+
def _community_fluid_communities(graph, no_of_communities):
472+
"""Community detection based on fluids interacting on the graph.
473+
474+
The algorithm is based on the simple idea of several fluids interacting
475+
in a non-homogeneous environment (the graph topology), expanding and
476+
contracting based on their interaction and density. Weighted graphs are
477+
not supported.
478+
479+
This function implements the community detection method described in:
480+
Parés F, Gasulla DG, et. al. (2018) Fluid Communities: A Competitive,
481+
Scalable and Diverse Community Detection Algorithm.
482+
483+
@param no_of_communities: The number of communities to be found. Must be
484+
greater than 0 and fewer than or equal to the number of vertices in the graph.
485+
@return: an appropriate L{VertexClustering} object.
486+
"""
487+
# Validate input parameters
488+
if no_of_communities <= 0:
489+
raise ValueError("no_of_communities must be greater than 0")
490+
491+
if no_of_communities > graph.vcount():
492+
raise ValueError("no_of_communities must be fewer than or equal to the number of vertices")
493+
494+
# Check if graph is weighted (not supported)
495+
if graph.is_weighted():
496+
raise ValueError("Weighted graphs are not supported by the fluid communities algorithm")
497+
498+
# Handle directed graphs - the algorithm works on undirected graphs
499+
# but can accept directed graphs (they are treated as undirected)
500+
if graph.is_directed():
501+
import warnings
502+
warnings.warn(
503+
"Directed graphs are treated as undirected in the fluid communities algorithm",
504+
UserWarning,
505+
stacklevel=2
506+
)
507+
508+
membership = GraphBase.community_fluid_communities(graph, no_of_communities)
509+
return VertexClustering(graph, membership)
510+
511+
471512
def _modularity(self, membership, weights=None, resolution=1, directed=True):
472513
"""Calculates the modularity score of the graph with respect to a given
473514
clustering.

tests/test_decomposition.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,61 @@ def testEigenvector(self):
276276
cl = g.community_leading_eigenvector(2)
277277
self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1])
278278
self.assertAlmostEqual(cl.q, 0.4523, places=3)
279+
280+
def testFluidCommunities(self):
281+
# Test with a simple graph: two cliques connected by a single edge
282+
g = Graph.Full(5) + Graph.Full(5)
283+
g.add_edges([(0, 5)])
284+
285+
# Test basic functionality - should find 2 communities
286+
cl = g.community_fluid_communities(2)
287+
self.assertEqual(len(set(cl.membership)), 2)
288+
self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1])
289+
290+
# Test with 3 cliques
291+
g = Graph.Full(4) + Graph.Full(4) + Graph.Full(4)
292+
g += [(0, 4), (4, 8)] # Connect the cliques
293+
cl = g.community_fluid_communities(3)
294+
self.assertEqual(len(set(cl.membership)), 3)
295+
self.assertMembershipsEqual(cl, [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2])
296+
297+
# Test error conditions
298+
# Number of communities must be positive
299+
with self.assertRaises(Exception):
300+
g.community_fluid_communities(0)
301+
302+
# Number of communities cannot exceed number of vertices
303+
with self.assertRaises(Exception):
304+
g.community_fluid_communities(g.vcount() + 1)
305+
306+
# Test with disconnected graph (should raise error)
307+
g_disconnected = Graph.Full(3) + Graph.Full(3) # No connecting edge
308+
with self.assertRaises(Exception):
309+
g_disconnected.community_fluid_communities(2)
310+
311+
# Test with single vertex (edge case)
312+
g_single = Graph(1)
313+
cl = g_single.community_fluid_communities(1)
314+
self.assertEqual(cl.membership, [0])
315+
316+
# Test with small connected graph
317+
g_small = Graph([(0, 1), (1, 2), (2, 0)]) # Triangle
318+
cl = g_small.community_fluid_communities(1)
319+
self.assertEqual(len(set(cl.membership)), 1)
320+
self.assertEqual(cl.membership, [0, 0, 0])
321+
322+
# Test deterministic behavior on simple structure
323+
# Note: Fluid communities can be non-deterministic due to randomization,
324+
# but on very simple structures it should be consistent
325+
g_path = Graph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)])
326+
cl = g_path.community_fluid_communities(2)
327+
self.assertEqual(len(set(cl.membership)), 2)
328+
329+
# Test that it returns a VertexClustering object
330+
g = Graph.Full(6)
331+
cl = g.community_fluid_communities(2)
332+
self.assertIsInstance(cl, VertexClustering)
333+
self.assertEqual(len(cl.membership), g.vcount())
279334

280335
def testInfomap(self):
281336
g = Graph.Famous("zachary")

0 commit comments

Comments
 (0)