Skip to content
56 changes: 55 additions & 1 deletion src/_igraph/graphobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -13748,6 +13748,38 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self,
return error ? NULL : Py_BuildValue("Nd", res, (double) quality);
}

/**
* Fluid communities
*/
PyObject *igraphmodule_Graph_community_fluid_communities(igraphmodule_GraphObject *self,
PyObject *args, PyObject *kwds) {
static char *kwlist[] = {"no_of_communities", NULL};
Py_ssize_t no_of_communities;
igraph_vector_int_t membership;
PyObject *result;

// Parse the Python integer argument
if (!PyArg_ParseTupleAndKeywords(args, kwds, "n", kwlist, &no_of_communities)) {
return NULL;
}

if (igraph_vector_int_init(&membership, 0)) {
igraphmodule_handle_igraph_error();
return NULL;
}

if (igraph_community_fluid_communities(&self->g, no_of_communities, &membership)) {
igraphmodule_handle_igraph_error();
igraph_vector_int_destroy(&membership);
return NULL;
}

result = igraphmodule_vector_int_t_to_PyList(&membership);
igraph_vector_int_destroy(&membership);

return result;
}

/**********************************************************************
* Random walks *
**********************************************************************/
Expand Down Expand Up @@ -18399,6 +18431,28 @@ struct PyMethodDef igraphmodule_Graph_methods[] = {
"\n"
"@see: modularity()\n"
},
{"community_fluid_communities",
(PyCFunction) igraphmodule_Graph_community_fluid_communities,
METH_VARARGS | METH_KEYWORDS,
"community_fluid_communities(no_of_communities)\n--\n\n"
"Community detection based on fluids interacting on the graph.\n\n"
"The algorithm is based on the simple idea of several fluids interacting\n"
"in a non-homogeneous environment (the graph topology), expanding and\n"
"contracting based on their interaction and density. Weighted graphs are\n"
"not supported.\n\n"
"B{Reference}\n\n"
" - Parés F, Gasulla DG, et. al. (2018) Fluid Communities: A Competitive,\n"
" Scalable and Diverse Community Detection Algorithm. In: Complex Networks\n"
" & Their Applications VI: Proceedings of Complex Networks 2017 (The Sixth\n"
" International Conference on Complex Networks and Their Applications),\n"
" Springer, vol 689, p 229. https://doi.org/10.1007/978-3-319-72150-7_19\n\n"
"@param no_of_communities: The number of communities to be found. Must be\n"
" greater than 0 and fewer than number of vertices in the graph.\n"
"@return: a list with the community membership of each vertex.\n"
"@note: The graph must be simple and connected. Edge directions will be\n"
" ignored if the graph is directed.\n"
"@note: Time complexity: O(|E|)\n",
},
{"community_infomap",
(PyCFunction) igraphmodule_Graph_community_infomap,
METH_VARARGS | METH_KEYWORDS,
Expand All @@ -18407,7 +18461,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = {
"method of Martin Rosvall and Carl T. Bergstrom.\n\n"
"See U{https://www.mapequation.org} for a visualization of the algorithm\n"
"or one of the references provided below.\n"
"B{References}\n"
"B{Reference}: "
" - M. Rosvall and C. T. Bergstrom: I{Maps of information flow reveal\n"
" community structure in complex networks}. PNAS 105, 1118 (2008).\n"
" U{https://arxiv.org/abs/0707.0609}\n"
Expand Down
3 changes: 3 additions & 0 deletions src/igraph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
_community_multilevel,
_community_optimal_modularity,
_community_edge_betweenness,
_community_fluid_communities,
_community_spinglass,
_community_walktrap,
_k_core,
Expand Down Expand Up @@ -658,6 +659,7 @@ def es(self):
community_multilevel = _community_multilevel
community_optimal_modularity = _community_optimal_modularity
community_edge_betweenness = _community_edge_betweenness
community_fluid_communities = _community_fluid_communities
community_spinglass = _community_spinglass
community_walktrap = _community_walktrap
k_core = _k_core
Expand Down Expand Up @@ -1100,6 +1102,7 @@ def write(graph, filename, *args, **kwds):
_community_multilevel,
_community_optimal_modularity,
_community_edge_betweenness,
_community_fluid_communities,
_community_spinglass,
_community_walktrap,
_k_core,
Expand Down
41 changes: 41 additions & 0 deletions src/igraph/community.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,47 @@ def _community_leiden(
)


def _community_fluid_communities(graph, no_of_communities):
"""Community detection based on fluids interacting on the graph.

The algorithm is based on the simple idea of several fluids interacting
in a non-homogeneous environment (the graph topology), expanding and
contracting based on their interaction and density. Weighted graphs are
not supported.

This function implements the community detection method described in:
Parés F, Gasulla DG, et. al. (2018) Fluid Communities: A Competitive,
Scalable and Diverse Community Detection Algorithm.

@param no_of_communities: The number of communities to be found. Must be
greater than 0 and fewer than or equal to the number of vertices in the graph.
@return: an appropriate L{VertexClustering} object.
"""
# Validate input parameters
if no_of_communities <= 0:
raise ValueError("no_of_communities must be greater than 0")

if no_of_communities > graph.vcount():
raise ValueError("no_of_communities must be fewer than or equal to the number of vertices")

# Check if graph is weighted (not supported)
if graph.is_weighted():
raise ValueError("Weighted graphs are not supported by the fluid communities algorithm")

# Handle directed graphs - the algorithm works on undirected graphs
# but can accept directed graphs (they are treated as undirected)
if graph.is_directed():
import warnings
warnings.warn(
"Directed graphs are treated as undirected in the fluid communities algorithm",
UserWarning,
stacklevel=2
)

membership = GraphBase.community_fluid_communities(graph, no_of_communities)
return VertexClustering(graph, membership)


def _modularity(self, membership, weights=None, resolution=1, directed=True):
"""Calculates the modularity score of the graph with respect to a given
clustering.
Expand Down
55 changes: 55 additions & 0 deletions tests/test_decomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,61 @@ def testEigenvector(self):
cl = g.community_leading_eigenvector(2)
self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1])
self.assertAlmostEqual(cl.q, 0.4523, places=3)

def testFluidCommunities(self):
# Test with a simple graph: two cliques connected by a single edge
g = Graph.Full(5) + Graph.Full(5)
g.add_edges([(0, 5)])

# Test basic functionality - should find 2 communities
cl = g.community_fluid_communities(2)
self.assertEqual(len(set(cl.membership)), 2)
self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1])

# Test with 3 cliques
g = Graph.Full(4) + Graph.Full(4) + Graph.Full(4)
g += [(0, 4), (4, 8)] # Connect the cliques
cl = g.community_fluid_communities(3)
self.assertEqual(len(set(cl.membership)), 3)
self.assertMembershipsEqual(cl, [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2])

# Test error conditions
# Number of communities must be positive
with self.assertRaises(Exception):
g.community_fluid_communities(0)

# Number of communities cannot exceed number of vertices
with self.assertRaises(Exception):
g.community_fluid_communities(g.vcount() + 1)

# Test with disconnected graph (should raise error)
g_disconnected = Graph.Full(3) + Graph.Full(3) # No connecting edge
with self.assertRaises(Exception):
g_disconnected.community_fluid_communities(2)

# Test with single vertex (edge case)
g_single = Graph(1)
cl = g_single.community_fluid_communities(1)
self.assertEqual(cl.membership, [0])

# Test with small connected graph
g_small = Graph([(0, 1), (1, 2), (2, 0)]) # Triangle
cl = g_small.community_fluid_communities(1)
self.assertEqual(len(set(cl.membership)), 1)
self.assertEqual(cl.membership, [0, 0, 0])

# Test deterministic behavior on simple structure
# Note: Fluid communities can be non-deterministic due to randomization,
# but on very simple structures it should be consistent
g_path = Graph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)])
cl = g_path.community_fluid_communities(2)
self.assertEqual(len(set(cl.membership)), 2)

# Test that it returns a VertexClustering object
g = Graph.Full(6)
cl = g.community_fluid_communities(2)
self.assertIsInstance(cl, VertexClustering)
self.assertEqual(len(cl.membership), g.vcount())

def testInfomap(self):
g = Graph.Famous("zachary")
Expand Down
Loading