Skip to content

feat: expose fluid communities to python #835

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):
Comment on lines +303 to +312
Copy link
Preview

Copilot AI Jun 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider catching a more specific exception type (e.g., ValueError) instead of the generic Exception for better clarity in testing error conditions.

Suggested change
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):
with self.assertRaises(ValueError):
g.community_fluid_communities(0)
# Number of communities cannot exceed number of vertices
with self.assertRaises(ValueError):
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(ValueError):

Copilot uses AI. Check for mistakes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ntamas This is actually an InternalError. Should we assert that? How does that Python interface determine what becomes a ValueError? Note that this is indeed an IGRAPH_EINVAL.

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