diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8c82bf79f..9f65a60d2 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -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 * **********************************************************************/ @@ -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, @@ -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" diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index b7f920cac..a1e87d13e 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -109,6 +109,7 @@ _community_multilevel, _community_optimal_modularity, _community_edge_betweenness, + _community_fluid_communities, _community_spinglass, _community_walktrap, _k_core, @@ -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 @@ -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, diff --git a/src/igraph/community.py b/src/igraph/community.py index 0fdcdf154..2b9e158d3 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -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. diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 0bfdd1b6a..41ab588c8 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -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")