From 02e74a240f25c01ecff05ff0ce9752e8f9817000 Mon Sep 17 00:00:00 2001 From: MIBea13 Date: Mon, 16 Jun 2025 18:12:43 +0300 Subject: [PATCH 01/19] add: expose voronoi to python - wip --- src/_igraph/graphobject.c | 166 +++++++++++++++++++++++++++++ src/igraph/__init__.py | 203 +++++++++++++++++------------------- src/igraph/community.py | 123 ++++++++++++++-------- tests/test_decomposition.py | 151 ++++++++++++++------------- 4 files changed, 416 insertions(+), 227 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8c82bf79f..1640ed796 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13895,6 +13895,130 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, } } + +/********************************************************************** + * Other methods * + **********************************************************************/ + +/** + * Voronoi clustering + */ +PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"lengths", "weights", "mode", "radius", NULL}; + PyObject *lengths_o = Py_None, *weights_o = Py_None; + PyObject *mode_o = Py_None; + PyObject *radius_o = Py_None; + igraph_vector_t lengths_v, weights_v; + igraph_vector_int_t membership_v, generators_v; + igraph_neimode_t mode = IGRAPH_ALL; + igraph_real_t radius = -1.0; /* negative means auto-optimize */ + igraph_real_t modularity; + PyObject *membership_o, *generators_o, *result_o; + igraph_bool_t return_modularity = 1; + igraph_bool_t lengths_allocated = 0, weights_allocated = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, + &lengths_o, &weights_o, &mode_o, &radius_o)) + return NULL; + + /* Handle mode parameter */ + if (mode_o != Py_None) { + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + return NULL; + } + + /* Handle radius parameter */ + if (radius_o != Py_None) { + if (PyFloat_Check(radius_o)) { + radius = PyFloat_AsDouble(radius_o); + } else if (PyLong_Check(radius_o)) { + radius = PyLong_AsDouble(radius_o); + } else { + PyErr_SetString(PyExc_TypeError, "radius must be a number or None"); + return NULL; + } + if (PyErr_Occurred()) return NULL; + } + + /* Handle lengths parameter */ + if (lengths_o != Py_None) { + if (igraphmodule_PyObject_to_vector_t(lengths_o, &lengths_v, 1)) { + return NULL; + } + lengths_allocated = 1; + } + + /* Handle weights parameter */ + if (weights_o != Py_None) { + if (igraphmodule_PyObject_to_vector_t(weights_o, &weights_v, 1)) { + if (lengths_allocated) { + igraph_vector_destroy(&lengths_v); + } + return NULL; + } + weights_allocated = 1; + } + + /* Initialize result vectors */ + if (igraph_vector_int_init(&membership_v, 0)) { + if (lengths_allocated) igraph_vector_destroy(&lengths_v); + if (weights_allocated) igraph_vector_destroy(&weights_v); + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_vector_int_init(&generators_v, 0)) { + if (lengths_allocated) igraph_vector_destroy(&lengths_v); + if (weights_allocated) igraph_vector_destroy(&weights_v); + igraph_vector_int_destroy(&membership_v); + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* Call the C function - pass NULL for None parameters */ + if (igraph_community_voronoi(&self->g, &membership_v, &generators_v, + return_modularity ? &modularity : NULL, + lengths_allocated ? &lengths_v : NULL, + weights_allocated ? &weights_v : NULL, + mode, radius)) { + if (lengths_allocated) igraph_vector_destroy(&lengths_v); + if (weights_allocated) igraph_vector_destroy(&weights_v); + igraph_vector_int_destroy(&membership_v); + igraph_vector_int_destroy(&generators_v); + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* Clean up input vectors */ + if (lengths_allocated) igraph_vector_destroy(&lengths_v); + if (weights_allocated) igraph_vector_destroy(&weights_v); + + /* Convert results to Python objects */ + membership_o = igraphmodule_vector_int_t_to_PyList(&membership_v); + igraph_vector_int_destroy(&membership_v); + if (!membership_o) { + igraph_vector_int_destroy(&generators_v); + return NULL; + } + + generators_o = igraphmodule_vector_int_t_to_PyList(&generators_v); + igraph_vector_int_destroy(&generators_v); + if (!generators_o) { + Py_DECREF(membership_o); + return NULL; + } + + /* Return tuple with membership, generators, and modularity */ + if (return_modularity) { + result_o = Py_BuildValue("(NNd)", membership_o, generators_o, (double)modularity); + } else { + result_o = Py_BuildValue("(NN)", membership_o, generators_o); + } + + return result_o; +} + /********************************************************************** * Special internal methods that you won't need to mess around with * **********************************************************************/ @@ -18628,6 +18752,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " current membership vector any more.\n" "@return: the community membership vector.\n" }, + {"community_voronoi", (PyCFunction) igraphmodule_Graph_community_voronoi, + METH_VARARGS | METH_KEYWORDS, "Finds communities using Voronoi partitioning"}, {"community_walktrap", (PyCFunction) igraphmodule_Graph_community_walktrap, METH_VARARGS | METH_KEYWORDS, @@ -18693,6 +18819,46 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: a random walk that starts from the given vertex and has at most\n" " the given length (shorter if the random walk got stuck).\n" }, + + /****************/ + /* OTHER METHODS */ + /****************/{ + "community_voronoi", + (PyCFunction) igraphmodule_Graph_community_voronoi, + METH_VARARGS | METH_KEYWORDS, + "community_voronoi(lengths=None, weights=None, mode=\"all\", radius=None)\n\n" + "Finds communities using Voronoi partitioning.\n\n" + "This function finds communities using a Voronoi partitioning of vertices based\n" + "on the given edge lengths divided by the edge clustering coefficient.\n" + "The generator vertices are chosen to be those with the largest local relative\n" + "density within a radius, with the local relative density of a vertex defined as\n" + "s * m / (m + k), where s is the strength of the vertex, m is the number of\n" + "edges within the vertex's first order neighborhood, while k is the number of\n" + "edges with only one endpoint within this neighborhood.\n\n" + "@param lengths: edge lengths, or C{None} to consider all edges as having\n" + " unit length. Voronoi partitioning will use edge lengths equal to\n" + " lengths / ECC where ECC is the edge clustering coefficient.\n" + "@param weights: edge weights, or C{None} to consider all edges as having\n" + " unit weight. Weights are used when selecting generator points, as well\n" + " as for computing modularity.\n" + "@param mode: if C{\"out\"}, distances from generator points to all other\n" + " nodes are considered. If C{\"in\"}, the reverse distances are used.\n" + " If C{\"all\"}, edge directions are ignored. This parameter is ignored\n" + " for undirected graphs.\n" + "@param radius: the radius/resolution to use when selecting generator points.\n" + " The larger this value, the fewer partitions there will be. Pass C{None}\n" + " to automatically select the radius that maximizes modularity.\n" + "@return: a tuple containing the membership vector, generator vertices,\n" + " and modularity score.\n" + "@rtype: tuple\n\n" + "@newfield ref: Reference\n" + "@ref: Deritei et al., Community detection by graph Voronoi diagrams,\n" + " New Journal of Physics 16, 063007 (2014)\n" + " U{https://doi.org/10.1088/1367-2630/16/6/063007}\n" + "@ref: Molnár et al., Community Detection in Directed Weighted Networks\n" + " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" + " U{https://doi.org/10.1038/s41598-024-58624-4}\n" +}, /**********************/ /* INTERNAL FUNCTIONS */ diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index b7f920cac..9aea11682 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2,7 +2,6 @@ igraph library. """ - __license__ = """ Copyright (C) 2006- The igraph development team @@ -22,6 +21,9 @@ 02110-1301 USA """ +import os +import sys + from igraph._igraph import ( ADJ_DIRECTED, ADJ_LOWER, @@ -31,22 +33,16 @@ ADJ_UNDIRECTED, ADJ_UPPER, ALL, - ARPACKOptions, - BFSIter, BLISS_F, BLISS_FL, BLISS_FLM, BLISS_FM, BLISS_FS, BLISS_FSM, - DFSIter, - Edge, GET_ADJACENCY_BOTH, GET_ADJACENCY_LOWER, GET_ADJACENCY_UPPER, - GraphBase, IN, - InternalError, OUT, REWIRING_SIMPLE, REWIRING_SIMPLE_LOOPS, @@ -60,9 +56,18 @@ TREE_IN, TREE_OUT, TREE_UNDIRECTED, - Vertex, WEAK, - arpack_options as default_arpack_options, + ARPACKOptions, + BFSIter, + DFSIter, + Edge, + GraphBase, + InternalError, + Vertex, + __igraph_version__, +) +from igraph._igraph import arpack_options as default_arpack_options +from igraph._igraph import ( community_to_membership, convex_hull, is_bigraphical, @@ -73,7 +78,6 @@ set_random_number_generator, set_status_handler, umap_compute_weights, - __igraph_version__, ) from igraph.adjacency import ( _get_adjacency, @@ -82,54 +86,54 @@ _get_biadjacency, _get_inclist, ) -from igraph.automorphisms import ( - _count_automorphisms_vf2, - _get_automorphisms_vf2, -) +from igraph.automorphisms import _count_automorphisms_vf2, _get_automorphisms_vf2 from igraph.basic import ( _add_edge, _add_edges, _add_vertex, _add_vertices, - _delete_edges, - _clear, _as_directed, _as_undirected, + _clear, + _delete_edges, ) from igraph.bipartite import ( - _maximum_bipartite_matching, _bipartite_projection, _bipartite_projection_size, + _maximum_bipartite_matching, +) +from igraph.clustering import ( + Clustering, + CohesiveBlocks, + Cover, + Dendrogram, + VertexClustering, + VertexCover, + VertexDendrogram, + _biconnected_components, + _clusters, + _cohesive_blocks, + _connected_components, + compare_communities, + split_join_distance, ) from igraph.community import ( + _community_edge_betweenness, _community_fastgreedy, _community_infomap, - _community_leading_eigenvector, _community_label_propagation, + _community_leading_eigenvector, + _community_leiden, _community_multilevel, _community_optimal_modularity, - _community_edge_betweenness, _community_spinglass, + _community_voronoi, _community_walktrap, _k_core, - _community_leiden, _modularity, ) -from igraph.clustering import ( - Clustering, - VertexClustering, - Dendrogram, - VertexDendrogram, - Cover, - VertexCover, - CohesiveBlocks, - compare_communities, - split_join_distance, - _biconnected_components, - _cohesive_blocks, - _connected_components, - _clusters, -) +from igraph.configuration import Configuration +from igraph.configuration import init as init_configuration from igraph.cut import ( Cut, Flow, @@ -140,7 +144,7 @@ _mincut, _st_mincut, ) -from igraph.configuration import Configuration, init as init_configuration +from igraph.datatypes import DyadCensus, Matrix, TriadCensus, UniqueIdGenerator from igraph.drawing import ( BoundingBox, CairoGraphDrawer, @@ -152,93 +156,87 @@ plot, ) from igraph.drawing.colors import ( - Palette, - GradientPalette, AdvancedGradientPalette, - RainbowPalette, - PrecalculatedPalette, ClusterColoringPalette, + GradientPalette, + Palette, + PrecalculatedPalette, + RainbowPalette, color_name_to_rgb, color_name_to_rgba, - hsv_to_rgb, - hsva_to_rgba, hsl_to_rgb, hsla_to_rgba, - rgb_to_hsv, - rgba_to_hsva, + hsv_to_rgb, + hsva_to_rgba, + known_colors, + palettes, rgb_to_hsl, + rgb_to_hsv, rgba_to_hsla, - palettes, - known_colors, + rgba_to_hsva, ) from igraph.drawing.graph import __plot__ as _graph_plot from igraph.drawing.utils import autocurve -from igraph.datatypes import Matrix, DyadCensus, TriadCensus, UniqueIdGenerator from igraph.formula import construct_graph_from_formula from igraph.io import _format_mapping +from igraph.io.adjacency import ( + _construct_graph_from_adjacency, + _construct_graph_from_weighted_adjacency, +) +from igraph.io.bipartite import ( + _construct_bipartite_graph, + _construct_bipartite_graph_from_adjacency, + _construct_full_bipartite_graph, + _construct_random_bipartite_graph, +) from igraph.io.files import ( - _construct_graph_from_graphmlz_file, + _construct_graph_from_adjacency_file, _construct_graph_from_dimacs_file, + _construct_graph_from_file, + _construct_graph_from_graphmlz_file, _construct_graph_from_pickle_file, _construct_graph_from_picklez_file, - _construct_graph_from_adjacency_file, - _construct_graph_from_file, _write_graph_to_adjacency_file, _write_graph_to_dimacs_file, + _write_graph_to_file, _write_graph_to_graphmlz_file, _write_graph_to_pickle_file, _write_graph_to_picklez_file, - _write_graph_to_file, +) +from igraph.io.images import _write_graph_to_svg +from igraph.io.libraries import ( + _construct_graph_from_graph_tool, + _construct_graph_from_networkx, + _export_graph_to_graph_tool, + _export_graph_to_networkx, ) from igraph.io.objects import ( + _construct_graph_from_dataframe, + _construct_graph_from_dict_dict, _construct_graph_from_dict_list, - _export_graph_to_dict_list, - _construct_graph_from_tuple_list, - _export_graph_to_tuple_list, _construct_graph_from_list_dict, - _export_graph_to_list_dict, - _construct_graph_from_dict_dict, + _construct_graph_from_tuple_list, + _export_edge_dataframe, _export_graph_to_dict_dict, - _construct_graph_from_dataframe, + _export_graph_to_dict_list, + _export_graph_to_list_dict, + _export_graph_to_tuple_list, _export_vertex_dataframe, - _export_edge_dataframe, -) -from igraph.io.adjacency import ( - _construct_graph_from_adjacency, - _construct_graph_from_weighted_adjacency, -) -from igraph.io.libraries import ( - _construct_graph_from_networkx, - _export_graph_to_networkx, - _construct_graph_from_graph_tool, - _export_graph_to_graph_tool, -) -from igraph.io.random import ( - _construct_random_geometric_graph, -) -from igraph.io.bipartite import ( - _construct_bipartite_graph, - _construct_bipartite_graph_from_adjacency, - _construct_full_bipartite_graph, - _construct_random_bipartite_graph, ) -from igraph.io.images import _write_graph_to_svg +from igraph.io.random import _construct_random_geometric_graph from igraph.layout import ( Layout, + _3d_version_for, _layout, _layout_auto, - _layout_sugiyama, - _layout_method_wrapper, - _3d_version_for, _layout_mapping, + _layout_method_wrapper, + _layout_sugiyama, ) from igraph.matching import Matching -from igraph.operators import ( - disjoint_union, - union, - intersection, - operator_method_registry as _operator_method_registry, -) +from igraph.operators import disjoint_union, intersection +from igraph.operators import operator_method_registry as _operator_method_registry +from igraph.operators import union from igraph.seq import EdgeSeq, VertexSeq, _add_proxy_methods from igraph.statistics import ( FittedPowerLaw, @@ -247,27 +245,20 @@ mean, median, percentile, - quantile, power_law_fit, + quantile, ) from igraph.structural import ( + _degree_distribution, _indegree, _outdegree, - _degree_distribution, _pagerank, _shortest_paths, ) from igraph.summary import GraphSummary, summary -from igraph.utils import ( - deprecated, - numpy_to_contiguous_memoryview, - rescale, -) +from igraph.utils import deprecated, numpy_to_contiguous_memoryview, rescale from igraph.version import __version__, __version_info__ -import os -import sys - class Graph(GraphBase): """Generic graph. @@ -424,7 +415,7 @@ def __init__(self, *args, **kwds): # When 'edges' is a NumPy array or matrix, convert it into a memoryview # as the lower-level C API works with memoryviews only try: - from numpy import ndarray, matrix + from numpy import matrix, ndarray if isinstance(edges, (ndarray, matrix)): edges = numpy_to_contiguous_memoryview(edges) @@ -659,6 +650,7 @@ def es(self): community_optimal_modularity = _community_optimal_modularity community_edge_betweenness = _community_edge_betweenness community_spinglass = _community_spinglass + community_voronoi = _community_voronoi community_walktrap = _community_walktrap k_core = _k_core community_leiden = _community_leiden @@ -949,18 +941,12 @@ def Incidence(cls, *args, **kwds): def are_connected(self, *args, **kwds): """Deprecated alias to L{Graph.are_adjacent()}.""" - deprecated( - "Graph.are_connected() is deprecated; use Graph.are_adjacent() " - "instead" - ) + deprecated("Graph.are_connected() is deprecated; use Graph.are_adjacent() " "instead") return self.are_adjacent(*args, **kwds) def get_incidence(self, *args, **kwds): """Deprecated alias to L{Graph.get_biadjacency()}.""" - deprecated( - "Graph.get_incidence() is deprecated; use Graph.get_biadjacency() " - "instead" - ) + deprecated("Graph.get_incidence() is deprecated; use Graph.get_biadjacency() " "instead") return self.get_biadjacency(*args, **kwds) @@ -991,9 +977,7 @@ def get_incidence(self, *args, **kwds): ############################################################## # Adding aliases for the 3D versions of the layout methods -Graph.layout_fruchterman_reingold_3d = _3d_version_for( - Graph.layout_fruchterman_reingold -) +Graph.layout_fruchterman_reingold_3d = _3d_version_for(Graph.layout_fruchterman_reingold) Graph.layout_kamada_kawai_3d = _3d_version_for(Graph.layout_kamada_kawai) Graph.layout_random_3d = _3d_version_for(Graph.layout_random) Graph.layout_grid_3d = _3d_version_for(Graph.layout_grid) @@ -1101,6 +1085,7 @@ def write(graph, filename, *args, **kwds): _community_optimal_modularity, _community_edge_betweenness, _community_spinglass, + _community_voronoi, _community_walktrap, _k_core, _community_leiden, diff --git a/src/igraph/community.py b/src/igraph/community.py index 0fdcdf154..5f0c0e50f 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -1,9 +1,9 @@ +from typing import List, Sequence, Tuple + from igraph._igraph import GraphBase -from igraph.clustering import VertexDendrogram, VertexClustering +from igraph.clustering import VertexClustering, VertexDendrogram from igraph.utils import deprecated -from typing import List, Sequence, Tuple - def _community_fastgreedy(graph, weights=None): """Community structure based on the greedy optimization of modularity. @@ -24,9 +24,7 @@ def _community_fastgreedy(graph, weights=None): """ merges, qs = GraphBase.community_fastgreedy(graph, weights) optimal_count = _optimal_cluster_count_from_merges_and_modularity(graph, merges, qs) - return VertexDendrogram( - graph, merges, optimal_count, modularity_params={"weights": weights} - ) + return VertexDendrogram(graph, merges, optimal_count, modularity_params={"weights": weights}) def _community_infomap(graph, edge_weights=None, vertex_weights=None, trials=10): @@ -53,9 +51,7 @@ def _community_infomap(graph, edge_weights=None, vertex_weights=None, trials=10) called C{codelength} that stores the code length determined by the algorithm. """ - membership, codelength = GraphBase.community_infomap( - graph, edge_weights, vertex_weights, trials - ) + membership, codelength = GraphBase.community_infomap(graph, edge_weights, vertex_weights, trials) return VertexClustering( graph, membership, @@ -64,9 +60,7 @@ def _community_infomap(graph, edge_weights=None, vertex_weights=None, trials=10) ) -def _community_leading_eigenvector( - graph, clusters=None, weights=None, arpack_options=None -): +def _community_leading_eigenvector(graph, clusters=None, weights=None, arpack_options=None): """Newman's leading eigenvector method for detecting community structure. This is the proper implementation of the recursive, divisive algorithm: @@ -185,21 +179,13 @@ def _community_multilevel(graph, weights=None, return_levels=False, resolution=1 modularity_params = {"weights": weights, "resolution": resolution} if return_levels: - levels, qs = GraphBase.community_multilevel( - graph, weights, return_levels=True, resolution=resolution - ) + levels, qs = GraphBase.community_multilevel(graph, weights, return_levels=True, resolution=resolution) result = [] for level, q in zip(levels, qs): - result.append( - VertexClustering(graph, level, q, modularity_params=modularity_params) - ) + result.append(VertexClustering(graph, level, q, modularity_params=modularity_params)) else: - membership = GraphBase.community_multilevel( - graph, weights, return_levels=False, resolution=resolution - ) - result = VertexClustering( - graph, membership, modularity_params=modularity_params - ) + membership = GraphBase.community_multilevel(graph, weights, return_levels=False, resolution=resolution) + result = VertexClustering(graph, membership, modularity_params=modularity_params) return result @@ -217,9 +203,7 @@ def _community_optimal_modularity(graph, *args, **kwds): @return: the calculated membership vector and the corresponding modularity in a tuple.""" - membership, modularity = GraphBase.community_optimal_modularity( - graph, *args, **kwds - ) + membership, modularity = GraphBase.community_optimal_modularity(graph, *args, **kwds) return VertexClustering(graph, membership, modularity) @@ -252,15 +236,11 @@ def _community_edge_betweenness(graph, clusters=None, directed=True, weights=Non merges, qs = GraphBase.community_edge_betweenness(graph, directed, weights) if clusters is None: if qs is not None: - clusters = _optimal_cluster_count_from_merges_and_modularity( - graph, merges, qs - ) + clusters = _optimal_cluster_count_from_merges_and_modularity(graph, merges, qs) else: clusters = 1 - return VertexDendrogram( - graph, merges, clusters, modularity_params={"weights": weights} - ) + return VertexDendrogram(graph, merges, clusters, modularity_params={"weights": weights}) def _community_spinglass(graph, *args, **kwds): @@ -340,9 +320,7 @@ def _community_walktrap(graph, weights=None, steps=4): """ merges, qs = GraphBase.community_walktrap(graph, weights, steps) optimal_count = _optimal_cluster_count_from_merges_and_modularity(graph, merges, qs) - return VertexDendrogram( - graph, merges, optimal_count, modularity_params={"weights": weights} - ) + return VertexDendrogram(graph, merges, optimal_count, modularity_params={"weights": weights}) def _k_core(graph, *args): @@ -392,7 +370,7 @@ def _community_leiden( initial_membership=None, n_iterations=2, node_weights=None, - **kwds + **kwds, ): """Finds the community structure of the graph using the Leiden algorithm of Traag, van Eck & Waltman. @@ -430,10 +408,7 @@ def _community_leiden( raise ValueError('objective_function must be "CPM" or "modularity".') if "resolution_parameter" in kwds: - deprecated( - "resolution_parameter keyword argument is deprecated, use " - "resolution=... instead" - ) + deprecated("resolution_parameter keyword argument is deprecated, use " "resolution=... instead") resolution = kwds.pop("resolution_parameter") if kwds: @@ -456,10 +431,68 @@ def _community_leiden( if weights is not None: modularity_params["weights"] = weights - return VertexClustering( - graph, membership, params=params, modularity_params=modularity_params + return VertexClustering(graph, membership, params=params, modularity_params=modularity_params) + + +def _community_voronoi(graph, lengths=None, weights=None, mode="all", radius=None): + """Finds communities using Voronoi partitioning. + + This function finds communities using a Voronoi partitioning of vertices based + on the given edge lengths divided by the edge clustering coefficient + (L{igraph.Graph.ecc}). The generator vertices are chosen to be those with the + largest local relative density within a radius, with the local relative + density of a vertex defined as C{s * m / (m + k)}, where C{s} is the strength + of the vertex, C{m} is the number of edges within the vertex's first order + neighborhood, while C{k} is the number of edges with only one endpoint within + this neighborhood. + + B{References} + + - Deritei et al., Community detection by graph Voronoi diagrams, + I{New Journal of Physics} 16, 063007 (2014). + U{https://doi.org/10.1088/1367-2630/16/6/063007}. + - Molnár et al., Community Detection in Directed Weighted Networks using + Voronoi Partitioning, I{Scientific Reports} 14, 8124 (2024). + U{https://doi.org/10.1038/s41598-024-58624-4}. + + @param lengths: edge lengths, or C{None} to consider all edges as having + unit length. Voronoi partitioning will use edge lengths equal to + lengths / ECC where ECC is the edge clustering coefficient. + @param weights: edge weights, or C{None} to consider all edges as having + unit weight. Weights are used when selecting generator points, as well + as for computing modularity. + @param mode: if C{"out"}, distances from generator points to all other + nodes are considered. If C{"in"}, the reverse distances are used. + If C{"all"}, edge directions are ignored. This parameter is ignored + for undirected graphs. + @param radius: the radius/resolution to use when selecting generator points. + The larger this value, the fewer partitions there will be. Pass C{None} + to automatically select the radius that maximizes modularity. + @return: an appropriate L{VertexClustering} object with extra attributes + called C{generators} (the generator vertices). + """ + # Convert mode string to proper enum value to avoid deprecation warning + if isinstance(mode, str): + mode_map = {"out": "out", "in": "in", "all": "all", "total": "all"} # alias + if mode.lower() in mode_map: + mode = mode_map[mode.lower()] + else: + raise ValueError(f"Invalid mode '{mode}'. Must be one of: out, in, all") + + membership, generators, modularity = GraphBase.community_voronoi(graph, lengths, weights, mode, radius) + + params = {"generators": generators} + modularity_params = {} + if weights is not None: + modularity_params["weights"] = weights + + clustering = VertexClustering( + graph, membership, modularity=modularity, params=params, modularity_params=modularity_params ) + clustering.generators = generators + return clustering + def _modularity(self, membership, weights=None, resolution=1, directed=True): """Calculates the modularity score of the graph with respect to a given @@ -499,9 +532,7 @@ def _modularity(self, membership, weights=None, resolution=1, directed=True): if isinstance(membership, VertexClustering): if membership.graph != self: raise ValueError("clustering object belongs to another graph") - return GraphBase.modularity( - self, membership.membership, weights, resolution, directed - ) + return GraphBase.modularity(self, membership.membership, weights, resolution, directed) else: return GraphBase.modularity(self, membership, weights, resolution, directed) diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 0bfdd1b6a..f3f87e044 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -12,8 +12,8 @@ UniqueIdGenerator, VertexClustering, compare_communities, - split_join_distance, set_random_number_generator, + split_join_distance, ) @@ -25,9 +25,7 @@ def testSubgraph(self): vs = [0, 1, 2, 10, 11, 12, 20, 21, 22] sg = g.subgraph(vs) - self.assertTrue( - sg.isomorphic(Graph.Lattice([3, 3], circular=False, mutual=False)) - ) + self.assertTrue(sg.isomorphic(Graph.Lattice([3, 3], circular=False, mutual=False))) self.assertTrue(sg.vs["id"] == vs) def testSubgraphEdges(self): @@ -152,12 +150,7 @@ def testClusterGraph(self): clg = cl.cluster_graph({"string": "concat", "int": max}, False) self.assertTrue( - sorted(clg.get_edgelist()) - == [(0, 0)] * 3 - + [(0, 2)] * 12 - + [(1, 1)] * 3 - + [(1, 2)] * 12 - + [(2, 2)] * 6 + sorted(clg.get_edgelist()) == [(0, 0)] * 3 + [(0, 2)] * 12 + [(1, 1)] * 3 + [(1, 2)] * 12 + [(2, 2)] * 6 ) self.assertTrue(not clg.is_directed()) self.assertTrue(clg.vs["string"] == ["aaa", "bbc", "ccab"]) @@ -217,9 +210,7 @@ def assertMembershipsEqual(self, observed, expected): observed = observed.membership if hasattr(expected, "membership"): expected = expected.membership - self.assertEqual( - self.reindexMembership(expected), self.reindexMembership(observed) - ) + self.assertEqual(self.reindexMembership(expected), self.reindexMembership(observed)) def testClauset(self): # Two cliques of size 5 with one connecting edge @@ -288,8 +279,7 @@ def testInfomap(self): self.assertAlmostEqual(cl.q, 0.40203, places=3) self.assertMembershipsEqual( cl, - [1, 1, 1, 1, 2, 2, 2, 1, 0, 1, 2, 1, 1, 1, 0, 0, 2, 1, 0, 1, 0, 1] - + [0] * 12, + [1, 1, 1, 1, 2, 2, 2, 1, 0, 1, 2, 1, 1, 1, 0, 0, 2, 1, 0, 1, 0, 1] + [0] * 12, ) # Smoke testing with vertex and edge weights @@ -312,25 +302,19 @@ def testLabelPropagation(self): self.assertMembershipsEqual(cl, [0, 0, 1, 1]) cl = g.community_label_propagation(initial="initial", fixed=[1, 0, 0, 1]) self.assertTrue( - cl.membership == [0, 0, 1, 1] - or cl.membership == [0, 1, 1, 1] - or cl.membership == [0, 0, 0, 1] + cl.membership == [0, 0, 1, 1] or cl.membership == [0, 1, 1, 1] or cl.membership == [0, 0, 0, 1] ) g = Graph.GRG(100, 0.2) g.vs["initial"] = [ - 0 if i == 0 else 1 if i == 99 else 2 if i == 49 else random.randint(0, 50) - for i in range(g.vcount()) + 0 if i == 0 else 1 if i == 99 else 2 if i == 49 else random.randint(0, 50) for i in range(g.vcount()) ] g.vs["dont_move"] = [i in (0, 49, 99) for i in range(g.vcount())] cl = g.community_label_propagation(initial="initial", fixed="dont_move") # igraph is free to reorder the clusters so only co-membership will be # preserved, hence the next assertion - self.assertTrue( - cl.membership[0] != cl.membership[49] - and cl.membership[49] != cl.membership[99] - ) + self.assertTrue(cl.membership[0] != cl.membership[49] and cl.membership[49] != cl.membership[99]) self.assertTrue(x >= 0 and x <= 5 for x in cl.membership) def testMultilevel(self): @@ -370,20 +354,14 @@ def testMultilevel(self): cls = g.community_multilevel(return_levels=True) self.assertTrue(len(cls) == 2) - self.assertMembershipsEqual( - cls[0], [1, 1, 1, 0, 1, 1, 0, 0, 2, 2, 2, 3, 2, 3, 2, 2] - ) - self.assertMembershipsEqual( - cls[1], [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1] - ) + self.assertMembershipsEqual(cls[0], [1, 1, 1, 0, 1, 1, 0, 0, 2, 2, 2, 3, 2, 3, 2, 2]) + self.assertMembershipsEqual(cls[1], [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]) self.assertAlmostEqual(cls[0].q, 0.346301, places=5) self.assertAlmostEqual(cls[1].q, 0.392219, places=5) cls = g.community_multilevel() self.assertTrue(len(cls.membership) == g.vcount()) - self.assertMembershipsEqual( - cls, [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1] - ) + self.assertMembershipsEqual(cls, [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]) self.assertAlmostEqual(cls.q, 0.392219, places=5) def testOptimalModularity(self): @@ -397,9 +375,7 @@ def testOptimalModularity(self): ws = [i % 5 for i in range(g.ecount())] cl = g.community_optimal_modularity(weights=ws) - self.assertAlmostEqual( - cl.q, g.modularity(cl.membership, weights=ws), places=7 - ) + self.assertAlmostEqual(cl.q, g.modularity(cl.membership, weights=ws), places=7) g = Graph.Famous("zachary") cl = g.community_optimal_modularity() @@ -447,9 +423,7 @@ def testOptimalModularity(self): ws = [2 + (i % 3) for i in range(g.ecount())] cl = g.community_optimal_modularity(weights=ws) - self.assertAlmostEqual( - cl.q, g.modularity(cl.membership, weights=ws), places=7 - ) + self.assertAlmostEqual(cl.q, g.modularity(cl.membership, weights=ws), places=7) except NotImplementedError: # Well, meh @@ -518,9 +492,7 @@ def testLeiden(self): random.seed(42) set_random_number_generator(random) # We don't find the optimal partition if we are greedy - cl = G.community_leiden( - "CPM", resolution=1, weights="weight", beta=0, n_iterations=-1 - ) + cl = G.community_leiden("CPM", resolution=1, weights="weight", beta=0, n_iterations=-1) self.assertMembershipsEqual(cl, [0, 0, 1, 1, 1, 2, 2, 2]) random.seed(42) @@ -539,17 +511,68 @@ def testLeiden(self): ) self.assertMembershipsEqual(cl, [0, 1, 0, 0, 0, 1, 1, 1]) + def testVoronoi(self): + # Test 1: Two disconnected cliques - should find exactly 2 communities + g = Graph.Full(5) + Graph.Full(5) # Two separate complete graphs + cl = g.community_voronoi() + + # Should find exactly 2 communities + self.assertEqual(len(cl), 2) + + # Vertices 0-4 should be in one community, vertices 5-9 in another + communities = [set(), set()] + for vertex, community in enumerate(cl.membership): + communities[community].add(vertex) + + # One community should have vertices 0-4, the other should have 5-9 + expected_communities = [{0, 1, 2, 3, 4}, {5, 6, 7, 8, 9}] + self.assertTrue( + communities == expected_communities or communities == expected_communities[::-1] # Order might be swapped + ) + + # Test 2: Two cliques connected by a single bridge edge + g = Graph.Full(4) + Graph.Full(4) + g.add_edges([(0, 4)]) # Bridge connecting the two cliques + + cl = g.community_voronoi() + + # Should still find 2 communities (bridge is weak) + self.assertEqual(len(cl), 2) + + # Check that vertices within each clique are in the same community + # Vertices 0,1,2,3 should be together, and 4,5,6,7 should be together + comm_0123 = {cl.membership[i] for i in [0, 1, 2, 3]} + comm_4567 = {cl.membership[i] for i in [4, 5, 6, 7]} + + self.assertEqual(len(comm_0123), 1) # All in same community + self.assertEqual(len(comm_4567), 1) # All in same community + self.assertNotEqual(comm_0123, comm_4567) # Different communities + + # Test 3: Three disconnected triangles + g = Graph(9) + g.add_edges([(0, 1), (1, 2), (2, 0)]) # Triangle 1 + g.add_edges([(3, 4), (4, 5), (5, 3)]) # Triangle 2 + g.add_edges([(6, 7), (7, 8), (8, 6)]) # Triangle 3 + + cl = g.community_voronoi() + + # Should find exactly 3 communities + self.assertEqual(len(cl), 3) + + # Each triangle should be in its own community + triangles = [ + {cl.membership[0], cl.membership[1], cl.membership[2]}, + {cl.membership[3], cl.membership[4], cl.membership[5]}, + {cl.membership[6], cl.membership[7], cl.membership[8]}, + ] + class CohesiveBlocksTests(unittest.TestCase): def genericTests(self, cbs): self.assertTrue(isinstance(cbs, CohesiveBlocks)) - self.assertTrue( - all(cbs.cohesion(i) == c for i, c in enumerate(cbs.cohesions())) - ) + self.assertTrue(all(cbs.cohesion(i) == c for i, c in enumerate(cbs.cohesions()))) self.assertTrue(all(cbs.parent(i) == c for i, c in enumerate(cbs.parents()))) - self.assertTrue( - all(cbs.max_cohesion(i) == c for i, c in enumerate(cbs.max_cohesions())) - ) + self.assertTrue(all(cbs.max_cohesion(i) == c for i, c in enumerate(cbs.max_cohesions()))) def testCohesiveBlocks1(self): # Taken from the igraph R manual @@ -571,9 +594,7 @@ def testCohesiveBlocks1(self): ], ) self.assertEqual(cbs.cohesions(), [1, 2, 2, 4, 3, 3]) - self.assertEqual( - cbs.max_cohesions(), [4, 4, 4, 4, 4, 1, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 2, 1] - ) + self.assertEqual(cbs.max_cohesions(), [4, 4, 4, 4, 4, 1, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 2, 1]) self.assertEqual(cbs.parents(), [None, 0, 0, 1, 2, 1]) def testCohesiveBlocks2(self): @@ -596,15 +617,11 @@ def testCohesiveBlocks2(self): list(range(6, 16)), [6, 7, 10, 13], ] - observed_blocks = sorted( - sorted(int(x) - 1 for x in g.vs[bl]["name"]) for bl in cbs - ) + observed_blocks = sorted(sorted(int(x) - 1 for x in g.vs[bl]["name"]) for bl in cbs) self.assertEqual(expected_blocks, observed_blocks) self.assertTrue(cbs.cohesions() == [1, 2, 2, 5, 3]) self.assertTrue(cbs.parents() == [None, 0, 0, 1, 2]) - self.assertTrue( - sorted(cbs.hierarchy().get_edgelist()) == [(0, 1), (0, 2), (1, 3), (2, 4)] - ) + self.assertTrue(sorted(cbs.hierarchy().get_edgelist()) == [(0, 1), (0, 2), (1, 3), (2, 4)]) def testCohesiveBlockingErrors(self): g = Graph.GRG(100, 0.2) @@ -626,9 +643,7 @@ def setUp(self): def _testMethod(self, method, expected): for (comm1, comm2), result in zip(self.clusterings, expected): - self.assertAlmostEqual( - compare_communities(comm1, comm2, method=method), result, places=3 - ) + self.assertAlmostEqual(compare_communities(comm1, comm2, method=method), result, places=3) def testCompareVI(self): expected = [0, 0.8675, math.log(6)] @@ -657,24 +672,16 @@ def testCompareAdjustedRand(self): def testRemoveNone(self): l1 = Clustering([1, 1, 1, None, None, 2, 2, 2, 2]) l2 = Clustering([1, 1, 2, 2, None, 2, 3, 3, None]) - self.assertAlmostEqual( - compare_communities(l1, l2, "nmi", remove_none=True), 0.5158, places=3 - ) + self.assertAlmostEqual(compare_communities(l1, l2, "nmi", remove_none=True), 0.5158, places=3) def suite(): - decomposition_suite = unittest.defaultTestLoader.loadTestsFromTestCase( - DecompositionTests - ) + decomposition_suite = unittest.defaultTestLoader.loadTestsFromTestCase(DecompositionTests) clustering_suite = unittest.defaultTestLoader.loadTestsFromTestCase(ClusteringTests) - vertex_clustering_suite = unittest.defaultTestLoader.loadTestsFromTestCase( - VertexClusteringTests - ) + vertex_clustering_suite = unittest.defaultTestLoader.loadTestsFromTestCase(VertexClusteringTests) cover_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CoverTests) community_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CommunityTests) - cohesive_blocks_suite = unittest.defaultTestLoader.loadTestsFromTestCase( - CohesiveBlocksTests - ) + cohesive_blocks_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CohesiveBlocksTests) comparison_suite = unittest.defaultTestLoader.loadTestsFromTestCase(ComparisonTests) return unittest.TestSuite( [ From 178bde62caaf0cde6615ea2a12ba229d6106f621 Mon Sep 17 00:00:00 2001 From: MIBea13 Date: Tue, 17 Jun 2025 11:34:54 +0300 Subject: [PATCH 02/19] changes in voronoi function --- src/_igraph/graphobject.c | 54 +++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 1640ed796..b89a33b86 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13909,14 +13909,14 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, PyObject *lengths_o = Py_None, *weights_o = Py_None; PyObject *mode_o = Py_None; PyObject *radius_o = Py_None; - igraph_vector_t lengths_v, weights_v; + igraph_vector_t *lengths_v = 0; + igraph_vector_t *weights_v = 0; igraph_vector_int_t membership_v, generators_v; igraph_neimode_t mode = IGRAPH_ALL; igraph_real_t radius = -1.0; /* negative means auto-optimize */ - igraph_real_t modularity; + igraph_real_t modularity = IGRAPH_NAN; PyObject *membership_o, *generators_o, *result_o; igraph_bool_t return_modularity = 1; - igraph_bool_t lengths_allocated = 0, weights_allocated = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, &lengths_o, &weights_o, &mode_o, &radius_o)) @@ -13943,34 +13943,40 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, /* Handle lengths parameter */ if (lengths_o != Py_None) { - if (igraphmodule_PyObject_to_vector_t(lengths_o, &lengths_v, 1)) { + if (igraphmodule_attrib_to_vector_t(lengths_o, self, &lengths_v, ATTRIBUTE_TYPE_EDGE)) { return NULL; } - lengths_allocated = 1; } /* Handle weights parameter */ if (weights_o != Py_None) { - if (igraphmodule_PyObject_to_vector_t(weights_o, &weights_v, 1)) { - if (lengths_allocated) { - igraph_vector_destroy(&lengths_v); + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); } return NULL; } - weights_allocated = 1; } /* Initialize result vectors */ if (igraph_vector_int_init(&membership_v, 0)) { - if (lengths_allocated) igraph_vector_destroy(&lengths_v); - if (weights_allocated) igraph_vector_destroy(&weights_v); + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != 0) { + igraph_vector_destroy(weights_v); free(weights_v); + } igraphmodule_handle_igraph_error(); return NULL; } if (igraph_vector_int_init(&generators_v, 0)) { - if (lengths_allocated) igraph_vector_destroy(&lengths_v); - if (weights_allocated) igraph_vector_destroy(&weights_v); + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != 0) { + igraph_vector_destroy(weights_v); free(weights_v); + } igraph_vector_int_destroy(&membership_v); igraphmodule_handle_igraph_error(); return NULL; @@ -13979,11 +13985,16 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, /* Call the C function - pass NULL for None parameters */ if (igraph_community_voronoi(&self->g, &membership_v, &generators_v, return_modularity ? &modularity : NULL, - lengths_allocated ? &lengths_v : NULL, - weights_allocated ? &weights_v : NULL, + lengths_v, + weights_v, mode, radius)) { - if (lengths_allocated) igraph_vector_destroy(&lengths_v); - if (weights_allocated) igraph_vector_destroy(&weights_v); + + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != 0) { + igraph_vector_destroy(weights_v); free(weights_v); + } igraph_vector_int_destroy(&membership_v); igraph_vector_int_destroy(&generators_v); igraphmodule_handle_igraph_error(); @@ -13991,8 +14002,13 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, } /* Clean up input vectors */ - if (lengths_allocated) igraph_vector_destroy(&lengths_v); - if (weights_allocated) igraph_vector_destroy(&weights_v); + + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != 0) { + igraph_vector_destroy(weights_v); free(weights_v); + } /* Convert results to Python objects */ membership_o = igraphmodule_vector_int_t_to_PyList(&membership_v); From b7669cc416c35c13d03421840aa3f68dd53b97ab Mon Sep 17 00:00:00 2001 From: MIBea13 Date: Tue, 17 Jun 2025 11:57:23 +0300 Subject: [PATCH 03/19] add: py files without formatting for check --- src/igraph/__init__.py | 17 +++++-- src/igraph/community.py | 61 +++++++++++++++++------- tests/test_decomposition.py | 94 ++++++++++++++++++++++++++++--------- 3 files changed, 129 insertions(+), 43 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 9aea11682..064d4a2da 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2,6 +2,7 @@ igraph library. """ + __license__ = """ Copyright (C) 2006- The igraph development team @@ -941,12 +942,18 @@ def Incidence(cls, *args, **kwds): def are_connected(self, *args, **kwds): """Deprecated alias to L{Graph.are_adjacent()}.""" - deprecated("Graph.are_connected() is deprecated; use Graph.are_adjacent() " "instead") + deprecated( + "Graph.are_connected() is deprecated; use Graph.are_adjacent() " + "instead" + ) return self.are_adjacent(*args, **kwds) def get_incidence(self, *args, **kwds): """Deprecated alias to L{Graph.get_biadjacency()}.""" - deprecated("Graph.get_incidence() is deprecated; use Graph.get_biadjacency() " "instead") + deprecated( + "Graph.get_incidence() is deprecated; use Graph.get_biadjacency() " + "instead" + ) return self.get_biadjacency(*args, **kwds) @@ -977,7 +984,9 @@ def get_incidence(self, *args, **kwds): ############################################################## # Adding aliases for the 3D versions of the layout methods -Graph.layout_fruchterman_reingold_3d = _3d_version_for(Graph.layout_fruchterman_reingold) +Graph.layout_fruchterman_reingold_3d = _3d_version_for( + Graph.layout_fruchterman_reingold +) Graph.layout_kamada_kawai_3d = _3d_version_for(Graph.layout_kamada_kawai) Graph.layout_random_3d = _3d_version_for(Graph.layout_random) Graph.layout_grid_3d = _3d_version_for(Graph.layout_grid) @@ -1245,4 +1254,4 @@ def write(graph, filename, *args, **kwds): "TREE_OUT", "TREE_UNDIRECTED", "WEAK", -) +) \ No newline at end of file diff --git a/src/igraph/community.py b/src/igraph/community.py index 5f0c0e50f..9b7359d6c 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -24,7 +24,9 @@ def _community_fastgreedy(graph, weights=None): """ merges, qs = GraphBase.community_fastgreedy(graph, weights) optimal_count = _optimal_cluster_count_from_merges_and_modularity(graph, merges, qs) - return VertexDendrogram(graph, merges, optimal_count, modularity_params={"weights": weights}) + return VertexDendrogram( + graph, merges, optimal_count, modularity_params={"weights": weights} + ) def _community_infomap(graph, edge_weights=None, vertex_weights=None, trials=10): @@ -51,7 +53,9 @@ def _community_infomap(graph, edge_weights=None, vertex_weights=None, trials=10) called C{codelength} that stores the code length determined by the algorithm. """ - membership, codelength = GraphBase.community_infomap(graph, edge_weights, vertex_weights, trials) + membership, codelength = GraphBase.community_infomap( + graph, edge_weights, vertex_weights, trials + ) return VertexClustering( graph, membership, @@ -60,7 +64,9 @@ def _community_infomap(graph, edge_weights=None, vertex_weights=None, trials=10) ) -def _community_leading_eigenvector(graph, clusters=None, weights=None, arpack_options=None): +def _community_leading_eigenvector( + graph, clusters=None, weights=None, arpack_options=None +): """Newman's leading eigenvector method for detecting community structure. This is the proper implementation of the recursive, divisive algorithm: @@ -179,13 +185,21 @@ def _community_multilevel(graph, weights=None, return_levels=False, resolution=1 modularity_params = {"weights": weights, "resolution": resolution} if return_levels: - levels, qs = GraphBase.community_multilevel(graph, weights, return_levels=True, resolution=resolution) + levels, qs = GraphBase.community_multilevel( + graph, weights, return_levels=True, resolution=resolution + ) result = [] for level, q in zip(levels, qs): - result.append(VertexClustering(graph, level, q, modularity_params=modularity_params)) + result.append( + VertexClustering(graph, level, q, modularity_params=modularity_params) + ) else: - membership = GraphBase.community_multilevel(graph, weights, return_levels=False, resolution=resolution) - result = VertexClustering(graph, membership, modularity_params=modularity_params) + membership = GraphBase.community_multilevel( + graph, weights, return_levels=False, resolution=resolution + ) + result = VertexClustering( + graph, membership, modularity_params=modularity_params + ) return result @@ -203,7 +217,9 @@ def _community_optimal_modularity(graph, *args, **kwds): @return: the calculated membership vector and the corresponding modularity in a tuple.""" - membership, modularity = GraphBase.community_optimal_modularity(graph, *args, **kwds) + membership, modularity = GraphBase.community_optimal_modularity( + graph, *args, **kwds + ) return VertexClustering(graph, membership, modularity) @@ -236,11 +252,15 @@ def _community_edge_betweenness(graph, clusters=None, directed=True, weights=Non merges, qs = GraphBase.community_edge_betweenness(graph, directed, weights) if clusters is None: if qs is not None: - clusters = _optimal_cluster_count_from_merges_and_modularity(graph, merges, qs) + clusters = _optimal_cluster_count_from_merges_and_modularity( + graph, merges, qs + ) else: clusters = 1 - return VertexDendrogram(graph, merges, clusters, modularity_params={"weights": weights}) + return VertexDendrogram( + graph, merges, clusters, modularity_params={"weights": weights} + ) def _community_spinglass(graph, *args, **kwds): @@ -320,7 +340,9 @@ def _community_walktrap(graph, weights=None, steps=4): """ merges, qs = GraphBase.community_walktrap(graph, weights, steps) optimal_count = _optimal_cluster_count_from_merges_and_modularity(graph, merges, qs) - return VertexDendrogram(graph, merges, optimal_count, modularity_params={"weights": weights}) + return VertexDendrogram( + graph, merges, optimal_count, modularity_params={"weights": weights} + ) def _k_core(graph, *args): @@ -370,7 +392,7 @@ def _community_leiden( initial_membership=None, n_iterations=2, node_weights=None, - **kwds, + **kwds ): """Finds the community structure of the graph using the Leiden algorithm of Traag, van Eck & Waltman. @@ -408,7 +430,10 @@ def _community_leiden( raise ValueError('objective_function must be "CPM" or "modularity".') if "resolution_parameter" in kwds: - deprecated("resolution_parameter keyword argument is deprecated, use " "resolution=... instead") + deprecated( + "resolution_parameter keyword argument is deprecated, use " + "resolution=... instead" + ) resolution = kwds.pop("resolution_parameter") if kwds: @@ -431,7 +456,9 @@ def _community_leiden( if weights is not None: modularity_params["weights"] = weights - return VertexClustering(graph, membership, params=params, modularity_params=modularity_params) + return VertexClustering( + graph, membership, params=params, modularity_params=modularity_params + ) def _community_voronoi(graph, lengths=None, weights=None, mode="all", radius=None): @@ -492,7 +519,7 @@ def _community_voronoi(graph, lengths=None, weights=None, mode="all", radius=Non clustering.generators = generators return clustering - + def _modularity(self, membership, weights=None, resolution=1, directed=True): """Calculates the modularity score of the graph with respect to a given @@ -532,7 +559,9 @@ def _modularity(self, membership, weights=None, resolution=1, directed=True): if isinstance(membership, VertexClustering): if membership.graph != self: raise ValueError("clustering object belongs to another graph") - return GraphBase.modularity(self, membership.membership, weights, resolution, directed) + return GraphBase.modularity( + self, membership.membership, weights, resolution, directed + ) else: return GraphBase.modularity(self, membership, weights, resolution, directed) diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index f3f87e044..d366d5db3 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -25,7 +25,9 @@ def testSubgraph(self): vs = [0, 1, 2, 10, 11, 12, 20, 21, 22] sg = g.subgraph(vs) - self.assertTrue(sg.isomorphic(Graph.Lattice([3, 3], circular=False, mutual=False))) + self.assertTrue( + sg.isomorphic(Graph.Lattice([3, 3], circular=False, mutual=False)) + ) self.assertTrue(sg.vs["id"] == vs) def testSubgraphEdges(self): @@ -150,7 +152,12 @@ def testClusterGraph(self): clg = cl.cluster_graph({"string": "concat", "int": max}, False) self.assertTrue( - sorted(clg.get_edgelist()) == [(0, 0)] * 3 + [(0, 2)] * 12 + [(1, 1)] * 3 + [(1, 2)] * 12 + [(2, 2)] * 6 + sorted(clg.get_edgelist()) + == [(0, 0)] * 3 + + [(0, 2)] * 12 + + [(1, 1)] * 3 + + [(1, 2)] * 12 + + [(2, 2)] * 6 ) self.assertTrue(not clg.is_directed()) self.assertTrue(clg.vs["string"] == ["aaa", "bbc", "ccab"]) @@ -210,7 +217,9 @@ def assertMembershipsEqual(self, observed, expected): observed = observed.membership if hasattr(expected, "membership"): expected = expected.membership - self.assertEqual(self.reindexMembership(expected), self.reindexMembership(observed)) + self.assertEqual( + self.reindexMembership(expected), self.reindexMembership(observed) + ) def testClauset(self): # Two cliques of size 5 with one connecting edge @@ -279,7 +288,8 @@ def testInfomap(self): self.assertAlmostEqual(cl.q, 0.40203, places=3) self.assertMembershipsEqual( cl, - [1, 1, 1, 1, 2, 2, 2, 1, 0, 1, 2, 1, 1, 1, 0, 0, 2, 1, 0, 1, 0, 1] + [0] * 12, + [1, 1, 1, 1, 2, 2, 2, 1, 0, 1, 2, 1, 1, 1, 0, 0, 2, 1, 0, 1, 0, 1] + + [0] * 12, ) # Smoke testing with vertex and edge weights @@ -302,19 +312,25 @@ def testLabelPropagation(self): self.assertMembershipsEqual(cl, [0, 0, 1, 1]) cl = g.community_label_propagation(initial="initial", fixed=[1, 0, 0, 1]) self.assertTrue( - cl.membership == [0, 0, 1, 1] or cl.membership == [0, 1, 1, 1] or cl.membership == [0, 0, 0, 1] + cl.membership == [0, 0, 1, 1] + or cl.membership == [0, 1, 1, 1] + or cl.membership == [0, 0, 0, 1] ) g = Graph.GRG(100, 0.2) g.vs["initial"] = [ - 0 if i == 0 else 1 if i == 99 else 2 if i == 49 else random.randint(0, 50) for i in range(g.vcount()) + 0 if i == 0 else 1 if i == 99 else 2 if i == 49 else random.randint(0, 50) + for i in range(g.vcount()) ] g.vs["dont_move"] = [i in (0, 49, 99) for i in range(g.vcount())] cl = g.community_label_propagation(initial="initial", fixed="dont_move") # igraph is free to reorder the clusters so only co-membership will be # preserved, hence the next assertion - self.assertTrue(cl.membership[0] != cl.membership[49] and cl.membership[49] != cl.membership[99]) + self.assertTrue( + cl.membership[0] != cl.membership[49] + and cl.membership[49] != cl.membership[99] + ) self.assertTrue(x >= 0 and x <= 5 for x in cl.membership) def testMultilevel(self): @@ -354,14 +370,20 @@ def testMultilevel(self): cls = g.community_multilevel(return_levels=True) self.assertTrue(len(cls) == 2) - self.assertMembershipsEqual(cls[0], [1, 1, 1, 0, 1, 1, 0, 0, 2, 2, 2, 3, 2, 3, 2, 2]) - self.assertMembershipsEqual(cls[1], [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]) + self.assertMembershipsEqual( + cls[0], [1, 1, 1, 0, 1, 1, 0, 0, 2, 2, 2, 3, 2, 3, 2, 2] + ) + self.assertMembershipsEqual( + cls[1], [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1] + ) self.assertAlmostEqual(cls[0].q, 0.346301, places=5) self.assertAlmostEqual(cls[1].q, 0.392219, places=5) cls = g.community_multilevel() self.assertTrue(len(cls.membership) == g.vcount()) - self.assertMembershipsEqual(cls, [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]) + self.assertMembershipsEqual( + cls, [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1] + ) self.assertAlmostEqual(cls.q, 0.392219, places=5) def testOptimalModularity(self): @@ -375,7 +397,9 @@ def testOptimalModularity(self): ws = [i % 5 for i in range(g.ecount())] cl = g.community_optimal_modularity(weights=ws) - self.assertAlmostEqual(cl.q, g.modularity(cl.membership, weights=ws), places=7) + self.assertAlmostEqual( + cl.q, g.modularity(cl.membership, weights=ws), places=7 + ) g = Graph.Famous("zachary") cl = g.community_optimal_modularity() @@ -423,7 +447,9 @@ def testOptimalModularity(self): ws = [2 + (i % 3) for i in range(g.ecount())] cl = g.community_optimal_modularity(weights=ws) - self.assertAlmostEqual(cl.q, g.modularity(cl.membership, weights=ws), places=7) + self.assertAlmostEqual( + cl.q, g.modularity(cl.membership, weights=ws), places=7 + ) except NotImplementedError: # Well, meh @@ -492,7 +518,9 @@ def testLeiden(self): random.seed(42) set_random_number_generator(random) # We don't find the optimal partition if we are greedy - cl = G.community_leiden("CPM", resolution=1, weights="weight", beta=0, n_iterations=-1) + cl = G.community_leiden( + "CPM", resolution=1, weights="weight", beta=0, n_iterations=-1 + ) self.assertMembershipsEqual(cl, [0, 0, 1, 1, 1, 2, 2, 2]) random.seed(42) @@ -570,9 +598,13 @@ def testVoronoi(self): class CohesiveBlocksTests(unittest.TestCase): def genericTests(self, cbs): self.assertTrue(isinstance(cbs, CohesiveBlocks)) - self.assertTrue(all(cbs.cohesion(i) == c for i, c in enumerate(cbs.cohesions()))) + self.assertTrue( + all(cbs.cohesion(i) == c for i, c in enumerate(cbs.cohesions())) + ) self.assertTrue(all(cbs.parent(i) == c for i, c in enumerate(cbs.parents()))) - self.assertTrue(all(cbs.max_cohesion(i) == c for i, c in enumerate(cbs.max_cohesions()))) + self.assertTrue( + all(cbs.max_cohesion(i) == c for i, c in enumerate(cbs.max_cohesions())) + ) def testCohesiveBlocks1(self): # Taken from the igraph R manual @@ -594,7 +626,9 @@ def testCohesiveBlocks1(self): ], ) self.assertEqual(cbs.cohesions(), [1, 2, 2, 4, 3, 3]) - self.assertEqual(cbs.max_cohesions(), [4, 4, 4, 4, 4, 1, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 2, 1]) + self.assertEqual( + cbs.max_cohesions(), [4, 4, 4, 4, 4, 1, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 2, 1] + ) self.assertEqual(cbs.parents(), [None, 0, 0, 1, 2, 1]) def testCohesiveBlocks2(self): @@ -617,11 +651,15 @@ def testCohesiveBlocks2(self): list(range(6, 16)), [6, 7, 10, 13], ] - observed_blocks = sorted(sorted(int(x) - 1 for x in g.vs[bl]["name"]) for bl in cbs) + observed_blocks = sorted( + sorted(int(x) - 1 for x in g.vs[bl]["name"]) for bl in cbs + ) self.assertEqual(expected_blocks, observed_blocks) self.assertTrue(cbs.cohesions() == [1, 2, 2, 5, 3]) self.assertTrue(cbs.parents() == [None, 0, 0, 1, 2]) - self.assertTrue(sorted(cbs.hierarchy().get_edgelist()) == [(0, 1), (0, 2), (1, 3), (2, 4)]) + self.assertTrue( + sorted(cbs.hierarchy().get_edgelist()) == [(0, 1), (0, 2), (1, 3), (2, 4)] + ) def testCohesiveBlockingErrors(self): g = Graph.GRG(100, 0.2) @@ -643,7 +681,9 @@ def setUp(self): def _testMethod(self, method, expected): for (comm1, comm2), result in zip(self.clusterings, expected): - self.assertAlmostEqual(compare_communities(comm1, comm2, method=method), result, places=3) + self.assertAlmostEqual( + compare_communities(comm1, comm2, method=method), result, places=3 + ) def testCompareVI(self): expected = [0, 0.8675, math.log(6)] @@ -672,16 +712,24 @@ def testCompareAdjustedRand(self): def testRemoveNone(self): l1 = Clustering([1, 1, 1, None, None, 2, 2, 2, 2]) l2 = Clustering([1, 1, 2, 2, None, 2, 3, 3, None]) - self.assertAlmostEqual(compare_communities(l1, l2, "nmi", remove_none=True), 0.5158, places=3) + self.assertAlmostEqual( + compare_communities(l1, l2, "nmi", remove_none=True), 0.5158, places=3 + ) def suite(): - decomposition_suite = unittest.defaultTestLoader.loadTestsFromTestCase(DecompositionTests) + decomposition_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + DecompositionTests + ) clustering_suite = unittest.defaultTestLoader.loadTestsFromTestCase(ClusteringTests) - vertex_clustering_suite = unittest.defaultTestLoader.loadTestsFromTestCase(VertexClusteringTests) + vertex_clustering_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + VertexClusteringTests + ) cover_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CoverTests) community_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CommunityTests) - cohesive_blocks_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CohesiveBlocksTests) + cohesive_blocks_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + CohesiveBlocksTests + ) comparison_suite = unittest.defaultTestLoader.loadTestsFromTestCase(ComparisonTests) return unittest.TestSuite( [ From 4fa9667b7d3b6e67506011af2d1eb29546b609ba Mon Sep 17 00:00:00 2001 From: BeaMarton13 Date: Tue, 17 Jun 2025 15:16:23 +0300 Subject: [PATCH 04/19] fix: updated py files formatting --- src/igraph/__init__.py | 187 +++++++++++++++++++----------------- src/igraph/community.py | 126 ++++++++++++------------ tests/test_decomposition.py | 112 ++++++++++----------- 3 files changed, 217 insertions(+), 208 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 064d4a2da..716693f11 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -22,9 +22,6 @@ 02110-1301 USA """ -import os -import sys - from igraph._igraph import ( ADJ_DIRECTED, ADJ_LOWER, @@ -34,16 +31,22 @@ ADJ_UNDIRECTED, ADJ_UPPER, ALL, + ARPACKOptions, + BFSIter, BLISS_F, BLISS_FL, BLISS_FLM, BLISS_FM, BLISS_FS, BLISS_FSM, + DFSIter, + Edge, GET_ADJACENCY_BOTH, GET_ADJACENCY_LOWER, GET_ADJACENCY_UPPER, + GraphBase, IN, + InternalError, OUT, REWIRING_SIMPLE, REWIRING_SIMPLE_LOOPS, @@ -57,18 +60,9 @@ TREE_IN, TREE_OUT, TREE_UNDIRECTED, - WEAK, - ARPACKOptions, - BFSIter, - DFSIter, - Edge, - GraphBase, - InternalError, Vertex, - __igraph_version__, -) -from igraph._igraph import arpack_options as default_arpack_options -from igraph._igraph import ( + WEAK, + arpack_options as default_arpack_options, community_to_membership, convex_hull, is_bigraphical, @@ -79,6 +73,7 @@ set_random_number_generator, set_status_handler, umap_compute_weights, + __igraph_version__, ) from igraph.adjacency import ( _get_adjacency, @@ -87,54 +82,55 @@ _get_biadjacency, _get_inclist, ) -from igraph.automorphisms import _count_automorphisms_vf2, _get_automorphisms_vf2 +from igraph.automorphisms import ( + _count_automorphisms_vf2, + _get_automorphisms_vf2, +) from igraph.basic import ( _add_edge, _add_edges, _add_vertex, _add_vertices, + _delete_edges, + _clear, _as_directed, _as_undirected, - _clear, - _delete_edges, ) from igraph.bipartite import ( + _maximum_bipartite_matching, _bipartite_projection, _bipartite_projection_size, - _maximum_bipartite_matching, -) -from igraph.clustering import ( - Clustering, - CohesiveBlocks, - Cover, - Dendrogram, - VertexClustering, - VertexCover, - VertexDendrogram, - _biconnected_components, - _clusters, - _cohesive_blocks, - _connected_components, - compare_communities, - split_join_distance, ) from igraph.community import ( - _community_edge_betweenness, _community_fastgreedy, _community_infomap, - _community_label_propagation, _community_leading_eigenvector, - _community_leiden, + _community_label_propagation, _community_multilevel, _community_optimal_modularity, + _community_edge_betweenness, _community_spinglass, _community_voronoi, _community_walktrap, _k_core, + _community_leiden, _modularity, ) -from igraph.configuration import Configuration -from igraph.configuration import init as init_configuration +from igraph.clustering import ( + Clustering, + VertexClustering, + Dendrogram, + VertexDendrogram, + Cover, + VertexCover, + CohesiveBlocks, + compare_communities, + split_join_distance, + _biconnected_components, + _cohesive_blocks, + _connected_components, + _clusters, +) from igraph.cut import ( Cut, Flow, @@ -145,7 +141,7 @@ _mincut, _st_mincut, ) -from igraph.datatypes import DyadCensus, Matrix, TriadCensus, UniqueIdGenerator +from igraph.configuration import Configuration, init as init_configuration from igraph.drawing import ( BoundingBox, CairoGraphDrawer, @@ -157,87 +153,93 @@ plot, ) from igraph.drawing.colors import ( - AdvancedGradientPalette, - ClusterColoringPalette, - GradientPalette, Palette, - PrecalculatedPalette, + GradientPalette, + AdvancedGradientPalette, RainbowPalette, + PrecalculatedPalette, + ClusterColoringPalette, color_name_to_rgb, color_name_to_rgba, - hsl_to_rgb, - hsla_to_rgba, hsv_to_rgb, hsva_to_rgba, - known_colors, - palettes, - rgb_to_hsl, + hsl_to_rgb, + hsla_to_rgba, rgb_to_hsv, - rgba_to_hsla, rgba_to_hsva, + rgb_to_hsl, + rgba_to_hsla, + palettes, + known_colors, ) from igraph.drawing.graph import __plot__ as _graph_plot from igraph.drawing.utils import autocurve +from igraph.datatypes import Matrix, DyadCensus, TriadCensus, UniqueIdGenerator from igraph.formula import construct_graph_from_formula from igraph.io import _format_mapping -from igraph.io.adjacency import ( - _construct_graph_from_adjacency, - _construct_graph_from_weighted_adjacency, -) -from igraph.io.bipartite import ( - _construct_bipartite_graph, - _construct_bipartite_graph_from_adjacency, - _construct_full_bipartite_graph, - _construct_random_bipartite_graph, -) from igraph.io.files import ( - _construct_graph_from_adjacency_file, - _construct_graph_from_dimacs_file, - _construct_graph_from_file, _construct_graph_from_graphmlz_file, + _construct_graph_from_dimacs_file, _construct_graph_from_pickle_file, _construct_graph_from_picklez_file, + _construct_graph_from_adjacency_file, + _construct_graph_from_file, _write_graph_to_adjacency_file, _write_graph_to_dimacs_file, - _write_graph_to_file, _write_graph_to_graphmlz_file, _write_graph_to_pickle_file, _write_graph_to_picklez_file, -) -from igraph.io.images import _write_graph_to_svg -from igraph.io.libraries import ( - _construct_graph_from_graph_tool, - _construct_graph_from_networkx, - _export_graph_to_graph_tool, - _export_graph_to_networkx, + _write_graph_to_file, ) from igraph.io.objects import ( - _construct_graph_from_dataframe, - _construct_graph_from_dict_dict, _construct_graph_from_dict_list, - _construct_graph_from_list_dict, - _construct_graph_from_tuple_list, - _export_edge_dataframe, - _export_graph_to_dict_dict, _export_graph_to_dict_list, - _export_graph_to_list_dict, + _construct_graph_from_tuple_list, _export_graph_to_tuple_list, + _construct_graph_from_list_dict, + _export_graph_to_list_dict, + _construct_graph_from_dict_dict, + _export_graph_to_dict_dict, + _construct_graph_from_dataframe, _export_vertex_dataframe, + _export_edge_dataframe, +) +from igraph.io.adjacency import ( + _construct_graph_from_adjacency, + _construct_graph_from_weighted_adjacency, +) +from igraph.io.libraries import ( + _construct_graph_from_networkx, + _export_graph_to_networkx, + _construct_graph_from_graph_tool, + _export_graph_to_graph_tool, +) +from igraph.io.random import ( + _construct_random_geometric_graph, +) +from igraph.io.bipartite import ( + _construct_bipartite_graph, + _construct_bipartite_graph_from_adjacency, + _construct_full_bipartite_graph, + _construct_random_bipartite_graph, ) -from igraph.io.random import _construct_random_geometric_graph +from igraph.io.images import _write_graph_to_svg from igraph.layout import ( Layout, - _3d_version_for, _layout, _layout_auto, - _layout_mapping, - _layout_method_wrapper, _layout_sugiyama, + _layout_method_wrapper, + _3d_version_for, + _layout_mapping, ) from igraph.matching import Matching -from igraph.operators import disjoint_union, intersection -from igraph.operators import operator_method_registry as _operator_method_registry -from igraph.operators import union +from igraph.operators import ( + disjoint_union, + union, + intersection, + operator_method_registry as _operator_method_registry, +) from igraph.seq import EdgeSeq, VertexSeq, _add_proxy_methods from igraph.statistics import ( FittedPowerLaw, @@ -246,20 +248,27 @@ mean, median, percentile, - power_law_fit, quantile, + power_law_fit, ) from igraph.structural import ( - _degree_distribution, _indegree, _outdegree, + _degree_distribution, _pagerank, _shortest_paths, ) from igraph.summary import GraphSummary, summary -from igraph.utils import deprecated, numpy_to_contiguous_memoryview, rescale +from igraph.utils import ( + deprecated, + numpy_to_contiguous_memoryview, + rescale, +) from igraph.version import __version__, __version_info__ +import os +import sys + class Graph(GraphBase): """Generic graph. @@ -416,7 +425,7 @@ def __init__(self, *args, **kwds): # When 'edges' is a NumPy array or matrix, convert it into a memoryview # as the lower-level C API works with memoryviews only try: - from numpy import matrix, ndarray + from numpy import ndarray, matrix if isinstance(edges, (ndarray, matrix)): edges = numpy_to_contiguous_memoryview(edges) @@ -1254,4 +1263,4 @@ def write(graph, filename, *args, **kwds): "TREE_OUT", "TREE_UNDIRECTED", "WEAK", -) \ No newline at end of file +) diff --git a/src/igraph/community.py b/src/igraph/community.py index 9b7359d6c..f2f2696a4 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -1,9 +1,9 @@ -from typing import List, Sequence, Tuple - from igraph._igraph import GraphBase -from igraph.clustering import VertexClustering, VertexDendrogram +from igraph.clustering import VertexDendrogram, VertexClustering from igraph.utils import deprecated +from typing import List, Sequence, Tuple + def _community_fastgreedy(graph, weights=None): """Community structure based on the greedy optimization of modularity. @@ -320,6 +320,66 @@ def _community_spinglass(graph, *args, **kwds): return VertexClustering(graph, membership, modularity_params=modularity_params) +def _community_voronoi(graph, lengths=None, weights=None, mode="all", radius=None): + """Finds communities using Voronoi partitioning. + + This function finds communities using a Voronoi partitioning of vertices based + on the given edge lengths divided by the edge clustering coefficient + (L{igraph.Graph.ecc}). The generator vertices are chosen to be those with the + largest local relative density within a radius, with the local relative + density of a vertex defined as C{s * m / (m + k)}, where C{s} is the strength + of the vertex, C{m} is the number of edges within the vertex's first order + neighborhood, while C{k} is the number of edges with only one endpoint within + this neighborhood. + + B{References} + + - Deritei et al., Community detection by graph Voronoi diagrams, + I{New Journal of Physics} 16, 063007 (2014). + U{https://doi.org/10.1088/1367-2630/16/6/063007}. + - Molnár et al., Community Detection in Directed Weighted Networks using + Voronoi Partitioning, I{Scientific Reports} 14, 8124 (2024). + U{https://doi.org/10.1038/s41598-024-58624-4}. + + @param lengths: edge lengths, or C{None} to consider all edges as having + unit length. Voronoi partitioning will use edge lengths equal to + lengths / ECC where ECC is the edge clustering coefficient. + @param weights: edge weights, or C{None} to consider all edges as having + unit weight. Weights are used when selecting generator points, as well + as for computing modularity. + @param mode: if C{"out"}, distances from generator points to all other + nodes are considered. If C{"in"}, the reverse distances are used. + If C{"all"}, edge directions are ignored. This parameter is ignored + for undirected graphs. + @param radius: the radius/resolution to use when selecting generator points. + The larger this value, the fewer partitions there will be. Pass C{None} + to automatically select the radius that maximizes modularity. + @return: an appropriate L{VertexClustering} object with extra attributes + called C{generators} (the generator vertices). + """ + # Convert mode string to proper enum value to avoid deprecation warning + if isinstance(mode, str): + mode_map = {"out": "out", "in": "in", "all": "all", "total": "all"} # alias + if mode.lower() in mode_map: + mode = mode_map[mode.lower()] + else: + raise ValueError(f"Invalid mode '{mode}'. Must be one of: out, in, all") + + membership, generators, modularity = GraphBase.community_voronoi(graph, lengths, weights, mode, radius) + + params = {"generators": generators} + modularity_params = {} + if weights is not None: + modularity_params["weights"] = weights + + clustering = VertexClustering( + graph, membership, modularity=modularity, params=params, modularity_params=modularity_params + ) + + clustering.generators = generators + return clustering + + def _community_walktrap(graph, weights=None, steps=4): """Community detection algorithm of Latapy & Pons, based on random walks. @@ -461,66 +521,6 @@ def _community_leiden( ) -def _community_voronoi(graph, lengths=None, weights=None, mode="all", radius=None): - """Finds communities using Voronoi partitioning. - - This function finds communities using a Voronoi partitioning of vertices based - on the given edge lengths divided by the edge clustering coefficient - (L{igraph.Graph.ecc}). The generator vertices are chosen to be those with the - largest local relative density within a radius, with the local relative - density of a vertex defined as C{s * m / (m + k)}, where C{s} is the strength - of the vertex, C{m} is the number of edges within the vertex's first order - neighborhood, while C{k} is the number of edges with only one endpoint within - this neighborhood. - - B{References} - - - Deritei et al., Community detection by graph Voronoi diagrams, - I{New Journal of Physics} 16, 063007 (2014). - U{https://doi.org/10.1088/1367-2630/16/6/063007}. - - Molnár et al., Community Detection in Directed Weighted Networks using - Voronoi Partitioning, I{Scientific Reports} 14, 8124 (2024). - U{https://doi.org/10.1038/s41598-024-58624-4}. - - @param lengths: edge lengths, or C{None} to consider all edges as having - unit length. Voronoi partitioning will use edge lengths equal to - lengths / ECC where ECC is the edge clustering coefficient. - @param weights: edge weights, or C{None} to consider all edges as having - unit weight. Weights are used when selecting generator points, as well - as for computing modularity. - @param mode: if C{"out"}, distances from generator points to all other - nodes are considered. If C{"in"}, the reverse distances are used. - If C{"all"}, edge directions are ignored. This parameter is ignored - for undirected graphs. - @param radius: the radius/resolution to use when selecting generator points. - The larger this value, the fewer partitions there will be. Pass C{None} - to automatically select the radius that maximizes modularity. - @return: an appropriate L{VertexClustering} object with extra attributes - called C{generators} (the generator vertices). - """ - # Convert mode string to proper enum value to avoid deprecation warning - if isinstance(mode, str): - mode_map = {"out": "out", "in": "in", "all": "all", "total": "all"} # alias - if mode.lower() in mode_map: - mode = mode_map[mode.lower()] - else: - raise ValueError(f"Invalid mode '{mode}'. Must be one of: out, in, all") - - membership, generators, modularity = GraphBase.community_voronoi(graph, lengths, weights, mode, radius) - - params = {"generators": generators} - modularity_params = {} - if weights is not None: - modularity_params["weights"] = weights - - clustering = VertexClustering( - graph, membership, modularity=modularity, params=params, modularity_params=modularity_params - ) - - clustering.generators = generators - return clustering - - 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 d366d5db3..bf1882914 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -12,8 +12,8 @@ UniqueIdGenerator, VertexClustering, compare_communities, - set_random_number_generator, split_join_distance, + set_random_number_generator, ) @@ -483,6 +483,61 @@ def testSpinglass(self): ok = True break self.assertTrue(ok) + + def testVoronoi(self): + # Test 1: Two disconnected cliques - should find exactly 2 communities + g = Graph.Full(5) + Graph.Full(5) # Two separate complete graphs + cl = g.community_voronoi() + + # Should find exactly 2 communities + self.assertEqual(len(cl), 2) + + # Vertices 0-4 should be in one community, vertices 5-9 in another + communities = [set(), set()] + for vertex, community in enumerate(cl.membership): + communities[community].add(vertex) + + # One community should have vertices 0-4, the other should have 5-9 + expected_communities = [{0, 1, 2, 3, 4}, {5, 6, 7, 8, 9}] + self.assertTrue( + communities == expected_communities or communities == expected_communities[::-1] # Order might be swapped + ) + + # Test 2: Two cliques connected by a single bridge edge + g = Graph.Full(4) + Graph.Full(4) + g.add_edges([(0, 4)]) # Bridge connecting the two cliques + + cl = g.community_voronoi() + + # Should still find 2 communities (bridge is weak) + self.assertEqual(len(cl), 2) + + # Check that vertices within each clique are in the same community + # Vertices 0,1,2,3 should be together, and 4,5,6,7 should be together + comm_0123 = {cl.membership[i] for i in [0, 1, 2, 3]} + comm_4567 = {cl.membership[i] for i in [4, 5, 6, 7]} + + self.assertEqual(len(comm_0123), 1) # All in same community + self.assertEqual(len(comm_4567), 1) # All in same community + self.assertNotEqual(comm_0123, comm_4567) # Different communities + + # Test 3: Three disconnected triangles + g = Graph(9) + g.add_edges([(0, 1), (1, 2), (2, 0)]) # Triangle 1 + g.add_edges([(3, 4), (4, 5), (5, 3)]) # Triangle 2 + g.add_edges([(6, 7), (7, 8), (8, 6)]) # Triangle 3 + + cl = g.community_voronoi() + + # Should find exactly 3 communities + self.assertEqual(len(cl), 3) + + # Each triangle should be in its own community + triangles = [ + {cl.membership[0], cl.membership[1], cl.membership[2]}, + {cl.membership[3], cl.membership[4], cl.membership[5]}, + {cl.membership[6], cl.membership[7], cl.membership[8]}, + ] def testWalktrap(self): g = Graph.Full(5) + Graph.Full(5) + Graph.Full(5) @@ -539,61 +594,6 @@ def testLeiden(self): ) self.assertMembershipsEqual(cl, [0, 1, 0, 0, 0, 1, 1, 1]) - def testVoronoi(self): - # Test 1: Two disconnected cliques - should find exactly 2 communities - g = Graph.Full(5) + Graph.Full(5) # Two separate complete graphs - cl = g.community_voronoi() - - # Should find exactly 2 communities - self.assertEqual(len(cl), 2) - - # Vertices 0-4 should be in one community, vertices 5-9 in another - communities = [set(), set()] - for vertex, community in enumerate(cl.membership): - communities[community].add(vertex) - - # One community should have vertices 0-4, the other should have 5-9 - expected_communities = [{0, 1, 2, 3, 4}, {5, 6, 7, 8, 9}] - self.assertTrue( - communities == expected_communities or communities == expected_communities[::-1] # Order might be swapped - ) - - # Test 2: Two cliques connected by a single bridge edge - g = Graph.Full(4) + Graph.Full(4) - g.add_edges([(0, 4)]) # Bridge connecting the two cliques - - cl = g.community_voronoi() - - # Should still find 2 communities (bridge is weak) - self.assertEqual(len(cl), 2) - - # Check that vertices within each clique are in the same community - # Vertices 0,1,2,3 should be together, and 4,5,6,7 should be together - comm_0123 = {cl.membership[i] for i in [0, 1, 2, 3]} - comm_4567 = {cl.membership[i] for i in [4, 5, 6, 7]} - - self.assertEqual(len(comm_0123), 1) # All in same community - self.assertEqual(len(comm_4567), 1) # All in same community - self.assertNotEqual(comm_0123, comm_4567) # Different communities - - # Test 3: Three disconnected triangles - g = Graph(9) - g.add_edges([(0, 1), (1, 2), (2, 0)]) # Triangle 1 - g.add_edges([(3, 4), (4, 5), (5, 3)]) # Triangle 2 - g.add_edges([(6, 7), (7, 8), (8, 6)]) # Triangle 3 - - cl = g.community_voronoi() - - # Should find exactly 3 communities - self.assertEqual(len(cl), 3) - - # Each triangle should be in its own community - triangles = [ - {cl.membership[0], cl.membership[1], cl.membership[2]}, - {cl.membership[3], cl.membership[4], cl.membership[5]}, - {cl.membership[6], cl.membership[7], cl.membership[8]}, - ] - class CohesiveBlocksTests(unittest.TestCase): def genericTests(self, cbs): From 91412050357b6283a275176270505990e62c2e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bea=20M=C3=A1rton?= Date: Wed, 18 Jun 2025 09:57:58 +0300 Subject: [PATCH 05/19] Update src/_igraph/graphobject.c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Szabolcs Horvát --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index b89a33b86..548e0bcc0 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14027,7 +14027,7 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, /* Return tuple with membership, generators, and modularity */ if (return_modularity) { - result_o = Py_BuildValue("(NNd)", membership_o, generators_o, (double)modularity); + result_o = Py_BuildValue("(NNd)", membership_o, generators_o, modularity); } else { result_o = Py_BuildValue("(NN)", membership_o, generators_o); } From 25405119ad036f66bab5d7e505bc2c86e3102f75 Mon Sep 17 00:00:00 2001 From: BeaMarton13 Date: Wed, 18 Jun 2025 10:03:32 +0300 Subject: [PATCH 06/19] fix: PR comments --- src/_igraph/graphobject.c | 82 ++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 548e0bcc0..2c2cd6cfd 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13909,8 +13909,8 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, PyObject *lengths_o = Py_None, *weights_o = Py_None; PyObject *mode_o = Py_None; PyObject *radius_o = Py_None; - igraph_vector_t *lengths_v = 0; - igraph_vector_t *weights_v = 0; + igraph_vector_t *lengths_v = NULL; + igraph_vector_t *weights_v = NULL; igraph_vector_int_t membership_v, generators_v; igraph_neimode_t mode = IGRAPH_ALL; igraph_real_t radius = -1.0; /* negative means auto-optimize */ @@ -18738,6 +18738,42 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " original implementation is used.\n" "@return: the community membership vector.\n" }, + {"community_voronoi", + (PyCFunction) igraphmodule_Graph_community_voronoi, + METH_VARARGS | METH_KEYWORDS, + "community_voronoi(lengths=None, weights=None, mode=\"all\", radius=None)\n\n" + "Finds communities using Voronoi partitioning.\n\n" + "This function finds communities using a Voronoi partitioning of vertices based\n" + "on the given edge lengths divided by the edge clustering coefficient.\n" + "The generator vertices are chosen to be those with the largest local relative\n" + "density within a radius, with the local relative density of a vertex defined as\n" + "s * m / (m + k), where s is the strength of the vertex, m is the number of\n" + "edges within the vertex's first order neighborhood, while k is the number of\n" + "edges with only one endpoint within this neighborhood.\n\n" + "@param lengths: edge lengths, or C{None} to consider all edges as having\n" + " unit length. Voronoi partitioning will use edge lengths equal to\n" + " lengths / ECC where ECC is the edge clustering coefficient.\n" + "@param weights: edge weights, or C{None} to consider all edges as having\n" + " unit weight. Weights are used when selecting generator points, as well\n" + " as for computing modularity.\n" + "@param mode: if C{\"out\"}, distances from generator points to all other\n" + " nodes are considered. If C{\"in\"}, the reverse distances are used.\n" + " If C{\"all\"}, edge directions are ignored. This parameter is ignored\n" + " for undirected graphs.\n" + "@param radius: the radius/resolution to use when selecting generator points.\n" + " The larger this value, the fewer partitions there will be. Pass C{None}\n" + " to automatically select the radius that maximizes modularity.\n" + "@return: a tuple containing the membership vector, generator vertices,\n" + " and modularity score.\n" + "@rtype: tuple\n\n" + "@newfield ref: Reference\n" + "@ref: Deritei et al., Community detection by graph Voronoi diagrams,\n" + " New Journal of Physics 16, 063007 (2014)\n" + " U{https://doi.org/10.1088/1367-2630/16/6/063007}\n" + "@ref: Molnár et al., Community Detection in Directed Weighted Networks\n" + " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" + " U{https://doi.org/10.1038/s41598-024-58624-4}\n" + }, {"community_leiden", (PyCFunction) igraphmodule_Graph_community_leiden, METH_VARARGS | METH_KEYWORDS, @@ -18768,8 +18804,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " current membership vector any more.\n" "@return: the community membership vector.\n" }, - {"community_voronoi", (PyCFunction) igraphmodule_Graph_community_voronoi, - METH_VARARGS | METH_KEYWORDS, "Finds communities using Voronoi partitioning"}, {"community_walktrap", (PyCFunction) igraphmodule_Graph_community_walktrap, METH_VARARGS | METH_KEYWORDS, @@ -18835,46 +18869,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: a random walk that starts from the given vertex and has at most\n" " the given length (shorter if the random walk got stuck).\n" }, - - /****************/ - /* OTHER METHODS */ - /****************/{ - "community_voronoi", - (PyCFunction) igraphmodule_Graph_community_voronoi, - METH_VARARGS | METH_KEYWORDS, - "community_voronoi(lengths=None, weights=None, mode=\"all\", radius=None)\n\n" - "Finds communities using Voronoi partitioning.\n\n" - "This function finds communities using a Voronoi partitioning of vertices based\n" - "on the given edge lengths divided by the edge clustering coefficient.\n" - "The generator vertices are chosen to be those with the largest local relative\n" - "density within a radius, with the local relative density of a vertex defined as\n" - "s * m / (m + k), where s is the strength of the vertex, m is the number of\n" - "edges within the vertex's first order neighborhood, while k is the number of\n" - "edges with only one endpoint within this neighborhood.\n\n" - "@param lengths: edge lengths, or C{None} to consider all edges as having\n" - " unit length. Voronoi partitioning will use edge lengths equal to\n" - " lengths / ECC where ECC is the edge clustering coefficient.\n" - "@param weights: edge weights, or C{None} to consider all edges as having\n" - " unit weight. Weights are used when selecting generator points, as well\n" - " as for computing modularity.\n" - "@param mode: if C{\"out\"}, distances from generator points to all other\n" - " nodes are considered. If C{\"in\"}, the reverse distances are used.\n" - " If C{\"all\"}, edge directions are ignored. This parameter is ignored\n" - " for undirected graphs.\n" - "@param radius: the radius/resolution to use when selecting generator points.\n" - " The larger this value, the fewer partitions there will be. Pass C{None}\n" - " to automatically select the radius that maximizes modularity.\n" - "@return: a tuple containing the membership vector, generator vertices,\n" - " and modularity score.\n" - "@rtype: tuple\n\n" - "@newfield ref: Reference\n" - "@ref: Deritei et al., Community detection by graph Voronoi diagrams,\n" - " New Journal of Physics 16, 063007 (2014)\n" - " U{https://doi.org/10.1088/1367-2630/16/6/063007}\n" - "@ref: Molnár et al., Community Detection in Directed Weighted Networks\n" - " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" - " U{https://doi.org/10.1038/s41598-024-58624-4}\n" -}, /**********************/ /* INTERNAL FUNCTIONS */ From 239fe5b0a1a38aa044650dc2b77a339876734524 Mon Sep 17 00:00:00 2001 From: BeaMarton13 Date: Wed, 18 Jun 2025 14:43:32 +0300 Subject: [PATCH 07/19] add: modularity as param and conversion if needed --- src/_igraph/graphobject.c | 86 ++++++++++++++++++++++----------------- src/igraph/community.py | 9 ++-- 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 2c2cd6cfd..d71d37f5e 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13905,10 +13905,11 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"lengths", "weights", "mode", "radius", NULL}; + static char *kwlist[] = {"modularity", "lengths", "weights", "mode", "radius", NULL}; PyObject *lengths_o = Py_None, *weights_o = Py_None; PyObject *mode_o = Py_None; PyObject *radius_o = Py_None; + PyObject *modularity_o = Py_None; igraph_vector_t *lengths_v = NULL; igraph_vector_t *weights_v = NULL; igraph_vector_int_t membership_v, generators_v; @@ -13916,12 +13917,19 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, igraph_real_t radius = -1.0; /* negative means auto-optimize */ igraph_real_t modularity = IGRAPH_NAN; PyObject *membership_o, *generators_o, *result_o; - igraph_bool_t return_modularity = 1; + igraph_bool_t return_modularity = false; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, - &lengths_o, &weights_o, &mode_o, &radius_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, + &modularity_o, &lengths_o, &weights_o, &mode_o, &radius_o)) return NULL; + if (modularity_o != Py_None){ + modularity = (igraph_real_t)PyFloat_AsDouble(modularity_o); + } + else { + return_modularity = true; + } + /* Handle mode parameter */ if (mode_o != Py_None) { if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) @@ -18739,40 +18747,44 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: the community membership vector.\n" }, {"community_voronoi", - (PyCFunction) igraphmodule_Graph_community_voronoi, + (PyCFunction) igraphmodule_Graph_community_voronoi, METH_VARARGS | METH_KEYWORDS, - "community_voronoi(lengths=None, weights=None, mode=\"all\", radius=None)\n\n" - "Finds communities using Voronoi partitioning.\n\n" - "This function finds communities using a Voronoi partitioning of vertices based\n" - "on the given edge lengths divided by the edge clustering coefficient.\n" - "The generator vertices are chosen to be those with the largest local relative\n" - "density within a radius, with the local relative density of a vertex defined as\n" - "s * m / (m + k), where s is the strength of the vertex, m is the number of\n" - "edges within the vertex's first order neighborhood, while k is the number of\n" - "edges with only one endpoint within this neighborhood.\n\n" - "@param lengths: edge lengths, or C{None} to consider all edges as having\n" - " unit length. Voronoi partitioning will use edge lengths equal to\n" - " lengths / ECC where ECC is the edge clustering coefficient.\n" - "@param weights: edge weights, or C{None} to consider all edges as having\n" - " unit weight. Weights are used when selecting generator points, as well\n" - " as for computing modularity.\n" - "@param mode: if C{\"out\"}, distances from generator points to all other\n" - " nodes are considered. If C{\"in\"}, the reverse distances are used.\n" - " If C{\"all\"}, edge directions are ignored. This parameter is ignored\n" - " for undirected graphs.\n" - "@param radius: the radius/resolution to use when selecting generator points.\n" - " The larger this value, the fewer partitions there will be. Pass C{None}\n" - " to automatically select the radius that maximizes modularity.\n" - "@return: a tuple containing the membership vector, generator vertices,\n" - " and modularity score.\n" - "@rtype: tuple\n\n" - "@newfield ref: Reference\n" - "@ref: Deritei et al., Community detection by graph Voronoi diagrams,\n" - " New Journal of Physics 16, 063007 (2014)\n" - " U{https://doi.org/10.1088/1367-2630/16/6/063007}\n" - "@ref: Molnár et al., Community Detection in Directed Weighted Networks\n" - " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" - " U{https://doi.org/10.1038/s41598-024-58624-4}\n" + "community_voronoi(lengths=None, weights=None, mode=\"all\", radius=None, modularity=None)\n\n" + "Finds communities using Voronoi partitioning.\n\n" + "This function finds communities using a Voronoi partitioning of vertices based\n" + "on the given edge lengths divided by the edge clustering coefficient.\n" + "The generator vertices are chosen to be those with the largest local relative\n" + "density within a radius, with the local relative density of a vertex defined as\n" + "s * m / (m + k), where s is the strength of the vertex, m is the number of\n" + "edges within the vertex's first order neighborhood, while k is the number of\n" + "edges with only one endpoint within this neighborhood.\n\n" + "@param lengths: edge lengths, or C{None} to consider all edges as having\n" + " unit length. Voronoi partitioning will use edge lengths equal to\n" + " lengths / ECC where ECC is the edge clustering coefficient.\n" + "@param weights: edge weights, or C{None} to consider all edges as having\n" + " unit weight. Weights are used when selecting generator points, as well\n" + " as for computing modularity.\n" + "@param mode: if C{\"out\"}, distances from generator points to all other\n" + " nodes are considered. If C{\"in\"}, the reverse distances are used.\n" + " If C{\"all\"}, edge directions are ignored. This parameter is ignored\n" + " for undirected graphs.\n" + "@param radius: the radius/resolution to use when selecting generator points.\n" + " The larger this value, the fewer partitions there will be. Pass C{None}\n" + " to automatically select the radius that maximizes modularity.\n" + "@param modularity: if not C{None}, the modularity score will be calculated\n" + " and returned as part of the result tuple.\n" + "@return: a tuple containing the membership vector and generator vertices.\n" + " When modularity calculation is requested, also includes the modularity score\n" + " as a third element: (membership, generators, modularity).\n" + " Otherwise: (membership, generators).\n" + "@rtype: tuple\n\n" + "B{References}\n\n" + " - Deritei et al., Community detection by graph Voronoi diagrams,\n" + " New Journal of Physics 16, 063007 (2014)\n" + " https://doi.org/10.1088/1367-2630/16/6/063007\n" + " - Molnár et al., Community Detection in Directed Weighted Networks\n" + " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" + " https://doi.org/10.1038/s41598-024-58624-4\n" }, {"community_leiden", (PyCFunction) igraphmodule_Graph_community_leiden, diff --git a/src/igraph/community.py b/src/igraph/community.py index f2f2696a4..73faee0b9 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -320,7 +320,7 @@ def _community_spinglass(graph, *args, **kwds): return VertexClustering(graph, membership, modularity_params=modularity_params) -def _community_voronoi(graph, lengths=None, weights=None, mode="all", radius=None): +def _community_voronoi(graph, modularity=None, lengths=None, weights=None, mode="all", radius=None): """Finds communities using Voronoi partitioning. This function finds communities using a Voronoi partitioning of vertices based @@ -364,8 +364,11 @@ def _community_voronoi(graph, lengths=None, weights=None, mode="all", radius=Non mode = mode_map[mode.lower()] else: raise ValueError(f"Invalid mode '{mode}'. Must be one of: out, in, all") - - membership, generators, modularity = GraphBase.community_voronoi(graph, lengths, weights, mode, radius) + + if modularity is None: + membership, generators, modularity = GraphBase.community_voronoi(graph, modularity, lengths, weights, mode, radius) + else: + membership, generators = GraphBase.community_voronoi(graph, modularity, lengths, weights, mode, radius) params = {"generators": generators} modularity_params = {} From 6bd5cc7fc0362d8b8ac9ac33641ced93ea2c3754 Mon Sep 17 00:00:00 2001 From: BeaMarton13 Date: Thu, 19 Jun 2025 11:45:24 +0300 Subject: [PATCH 08/19] fix: moved voronoi implementation --- src/_igraph/graphobject.c | 290 +++++++++++++++++++------------------- 1 file changed, 143 insertions(+), 147 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d71d37f5e..d9fd21957 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13748,6 +13748,149 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, return error ? NULL : Py_BuildValue("Nd", res, (double) quality); } +/** + * Voronoi clustering + */ +PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"modularity", "lengths", "weights", "mode", "radius", NULL}; + PyObject *lengths_o = Py_None, *weights_o = Py_None; + PyObject *mode_o = Py_None; + PyObject *radius_o = Py_None; + PyObject *modularity_o = Py_None; + igraph_vector_t *lengths_v = NULL; + igraph_vector_t *weights_v = NULL; + igraph_vector_int_t membership_v, generators_v; + igraph_neimode_t mode = IGRAPH_ALL; + igraph_real_t radius = -1.0; /* negative means auto-optimize */ + igraph_real_t modularity = IGRAPH_NAN; + PyObject *membership_o, *generators_o, *result_o; + igraph_bool_t return_modularity = false; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, + &modularity_o, &lengths_o, &weights_o, &mode_o, &radius_o)) + return NULL; + + if (modularity_o != Py_None){ + modularity = (igraph_real_t)PyFloat_AsDouble(modularity_o); + } + else { + return_modularity = true; + } + + /* Handle mode parameter */ + if (mode_o != Py_None) { + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + return NULL; + } + + /* Handle radius parameter */ + if (radius_o != Py_None) { + if (PyFloat_Check(radius_o)) { + radius = PyFloat_AsDouble(radius_o); + } else if (PyLong_Check(radius_o)) { + radius = PyLong_AsDouble(radius_o); + } else { + PyErr_SetString(PyExc_TypeError, "radius must be a number or None"); + return NULL; + } + if (PyErr_Occurred()) return NULL; + } + + /* Handle lengths parameter */ + if (lengths_o != Py_None) { + if (igraphmodule_attrib_to_vector_t(lengths_o, self, &lengths_v, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + } + + /* Handle weights parameter */ + if (weights_o != Py_None) { + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + return NULL; + } + } + + /* Initialize result vectors */ + if (igraph_vector_int_init(&membership_v, 0)) { + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != 0) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_vector_int_init(&generators_v, 0)) { + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != 0) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraph_vector_int_destroy(&membership_v); + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* Call the C function - pass NULL for None parameters */ + if (igraph_community_voronoi(&self->g, &membership_v, &generators_v, + return_modularity ? &modularity : NULL, + lengths_v, + weights_v, + mode, radius)) { + + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != 0) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraph_vector_int_destroy(&membership_v); + igraph_vector_int_destroy(&generators_v); + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* Clean up input vectors */ + + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != 0) { + igraph_vector_destroy(weights_v); free(weights_v); + } + + /* Convert results to Python objects */ + membership_o = igraphmodule_vector_int_t_to_PyList(&membership_v); + igraph_vector_int_destroy(&membership_v); + if (!membership_o) { + igraph_vector_int_destroy(&generators_v); + return NULL; + } + + generators_o = igraphmodule_vector_int_t_to_PyList(&generators_v); + igraph_vector_int_destroy(&generators_v); + if (!generators_o) { + Py_DECREF(membership_o); + return NULL; + } + + /* Return tuple with membership, generators, and modularity */ + if (return_modularity) { + result_o = Py_BuildValue("(NNd)", membership_o, generators_o, modularity); + } else { + result_o = Py_BuildValue("(NN)", membership_o, generators_o); + } + + return result_o; +} + /********************************************************************** * Random walks * **********************************************************************/ @@ -13896,153 +14039,6 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, } -/********************************************************************** - * Other methods * - **********************************************************************/ - -/** - * Voronoi clustering - */ -PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, - PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"modularity", "lengths", "weights", "mode", "radius", NULL}; - PyObject *lengths_o = Py_None, *weights_o = Py_None; - PyObject *mode_o = Py_None; - PyObject *radius_o = Py_None; - PyObject *modularity_o = Py_None; - igraph_vector_t *lengths_v = NULL; - igraph_vector_t *weights_v = NULL; - igraph_vector_int_t membership_v, generators_v; - igraph_neimode_t mode = IGRAPH_ALL; - igraph_real_t radius = -1.0; /* negative means auto-optimize */ - igraph_real_t modularity = IGRAPH_NAN; - PyObject *membership_o, *generators_o, *result_o; - igraph_bool_t return_modularity = false; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, - &modularity_o, &lengths_o, &weights_o, &mode_o, &radius_o)) - return NULL; - - if (modularity_o != Py_None){ - modularity = (igraph_real_t)PyFloat_AsDouble(modularity_o); - } - else { - return_modularity = true; - } - - /* Handle mode parameter */ - if (mode_o != Py_None) { - if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) - return NULL; - } - - /* Handle radius parameter */ - if (radius_o != Py_None) { - if (PyFloat_Check(radius_o)) { - radius = PyFloat_AsDouble(radius_o); - } else if (PyLong_Check(radius_o)) { - radius = PyLong_AsDouble(radius_o); - } else { - PyErr_SetString(PyExc_TypeError, "radius must be a number or None"); - return NULL; - } - if (PyErr_Occurred()) return NULL; - } - - /* Handle lengths parameter */ - if (lengths_o != Py_None) { - if (igraphmodule_attrib_to_vector_t(lengths_o, self, &lengths_v, ATTRIBUTE_TYPE_EDGE)) { - return NULL; - } - } - - /* Handle weights parameter */ - if (weights_o != Py_None) { - if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { - if (lengths_v != 0) { - igraph_vector_destroy(lengths_v); free(lengths_v); - } - return NULL; - } - } - - /* Initialize result vectors */ - if (igraph_vector_int_init(&membership_v, 0)) { - if (lengths_v != 0) { - igraph_vector_destroy(lengths_v); free(lengths_v); - } - if (weights_v != 0) { - igraph_vector_destroy(weights_v); free(weights_v); - } - igraphmodule_handle_igraph_error(); - return NULL; - } - - if (igraph_vector_int_init(&generators_v, 0)) { - if (lengths_v != 0) { - igraph_vector_destroy(lengths_v); free(lengths_v); - } - if (weights_v != 0) { - igraph_vector_destroy(weights_v); free(weights_v); - } - igraph_vector_int_destroy(&membership_v); - igraphmodule_handle_igraph_error(); - return NULL; - } - - /* Call the C function - pass NULL for None parameters */ - if (igraph_community_voronoi(&self->g, &membership_v, &generators_v, - return_modularity ? &modularity : NULL, - lengths_v, - weights_v, - mode, radius)) { - - if (lengths_v != 0) { - igraph_vector_destroy(lengths_v); free(lengths_v); - } - if (weights_v != 0) { - igraph_vector_destroy(weights_v); free(weights_v); - } - igraph_vector_int_destroy(&membership_v); - igraph_vector_int_destroy(&generators_v); - igraphmodule_handle_igraph_error(); - return NULL; - } - - /* Clean up input vectors */ - - if (lengths_v != 0) { - igraph_vector_destroy(lengths_v); free(lengths_v); - } - if (weights_v != 0) { - igraph_vector_destroy(weights_v); free(weights_v); - } - - /* Convert results to Python objects */ - membership_o = igraphmodule_vector_int_t_to_PyList(&membership_v); - igraph_vector_int_destroy(&membership_v); - if (!membership_o) { - igraph_vector_int_destroy(&generators_v); - return NULL; - } - - generators_o = igraphmodule_vector_int_t_to_PyList(&generators_v); - igraph_vector_int_destroy(&generators_v); - if (!generators_o) { - Py_DECREF(membership_o); - return NULL; - } - - /* Return tuple with membership, generators, and modularity */ - if (return_modularity) { - result_o = Py_BuildValue("(NNd)", membership_o, generators_o, modularity); - } else { - result_o = Py_BuildValue("(NN)", membership_o, generators_o); - } - - return result_o; -} - /********************************************************************** * Special internal methods that you won't need to mess around with * **********************************************************************/ From 4e125fd6dc7631a62d9243c3970f24b17442d750 Mon Sep 17 00:00:00 2001 From: BeaMarton13 Date: Fri, 20 Jun 2025 09:43:06 +0300 Subject: [PATCH 09/19] fix: based on PR comments --- src/_igraph/graphobject.c | 41 +++++++++++++-------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d9fd21957..263d72664 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13772,47 +13772,35 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, return NULL; if (modularity_o != Py_None){ - modularity = (igraph_real_t)PyFloat_AsDouble(modularity_o); + if (igraphmodule_PyObject_to_real_t(modularity_o, &modularity)) + return NULL;; } else { return_modularity = true; } /* Handle mode parameter */ - if (mode_o != Py_None) { - if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) - return NULL; - } + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + return NULL; /* Handle radius parameter */ if (radius_o != Py_None) { - if (PyFloat_Check(radius_o)) { - radius = PyFloat_AsDouble(radius_o); - } else if (PyLong_Check(radius_o)) { - radius = PyLong_AsDouble(radius_o); - } else { - PyErr_SetString(PyExc_TypeError, "radius must be a number or None"); + if (igraphmodule_PyObject_to_real_t(radius_o, &radius)) return NULL; - } - if (PyErr_Occurred()) return NULL; } /* Handle lengths parameter */ - if (lengths_o != Py_None) { - if (igraphmodule_attrib_to_vector_t(lengths_o, self, &lengths_v, ATTRIBUTE_TYPE_EDGE)) { - return NULL; - } - } + if (igraphmodule_attrib_to_vector_t(lengths_o, self, &lengths_v, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } /* Handle weights parameter */ - if (weights_o != Py_None) { - if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { - if (lengths_v != 0) { - igraph_vector_destroy(lengths_v); free(lengths_v); - } - return NULL; - } - } + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { + if (lengths_v != 0) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + return NULL; + } /* Initialize result vectors */ if (igraph_vector_int_init(&membership_v, 0)) { @@ -14038,7 +14026,6 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, } } - /********************************************************************** * Special internal methods that you won't need to mess around with * **********************************************************************/ From 8662811cdd17093fa77f127dbe875779d219a8df Mon Sep 17 00:00:00 2001 From: BeaMarton13 Date: Mon, 23 Jun 2025 13:47:25 +0300 Subject: [PATCH 10/19] fix: PR comments - Copilot version --- src/_igraph/graphobject.c | 18 +++++++++--------- tests/test_decomposition.py | 5 +++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 263d72664..55e74f26f 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13796,7 +13796,7 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, /* Handle weights parameter */ if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { - if (lengths_v != 0) { + if (lengths_v != NULL) { igraph_vector_destroy(lengths_v); free(lengths_v); } return NULL; @@ -13804,10 +13804,10 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, /* Initialize result vectors */ if (igraph_vector_int_init(&membership_v, 0)) { - if (lengths_v != 0) { + if (lengths_v != NULL) { igraph_vector_destroy(lengths_v); free(lengths_v); } - if (weights_v != 0) { + if (weights_v != NULL) { igraph_vector_destroy(weights_v); free(weights_v); } igraphmodule_handle_igraph_error(); @@ -13815,10 +13815,10 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, } if (igraph_vector_int_init(&generators_v, 0)) { - if (lengths_v != 0) { + if (lengths_v != NULL) { igraph_vector_destroy(lengths_v); free(lengths_v); } - if (weights_v != 0) { + if (weights_v != NULL) { igraph_vector_destroy(weights_v); free(weights_v); } igraph_vector_int_destroy(&membership_v); @@ -13833,10 +13833,10 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, weights_v, mode, radius)) { - if (lengths_v != 0) { + if (lengths_v != NULL) { igraph_vector_destroy(lengths_v); free(lengths_v); } - if (weights_v != 0) { + if (weights_v != NULL) { igraph_vector_destroy(weights_v); free(weights_v); } igraph_vector_int_destroy(&membership_v); @@ -13847,10 +13847,10 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, /* Clean up input vectors */ - if (lengths_v != 0) { + if (lengths_v != NULL) { igraph_vector_destroy(lengths_v); free(lengths_v); } - if (weights_v != 0) { + if (weights_v != NULL) { igraph_vector_destroy(weights_v); free(weights_v); } diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index bf1882914..8a89e0652 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -499,8 +499,9 @@ def testVoronoi(self): # One community should have vertices 0-4, the other should have 5-9 expected_communities = [{0, 1, 2, 3, 4}, {5, 6, 7, 8, 9}] - self.assertTrue( - communities == expected_communities or communities == expected_communities[::-1] # Order might be swapped + self.assertEqual( + set(frozenset(c) for c in communities), + set(frozenset(c) for c in expected_communities) ) # Test 2: Two cliques connected by a single bridge edge From 505f25a0c07f3a99d6c10aaa9a36f2e64c8b746b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bea=20M=C3=A1rton?= Date: Wed, 25 Jun 2025 10:09:10 +0300 Subject: [PATCH 11/19] Update src/_igraph/graphobject.c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Szabolcs Horvát --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8f2ddaf82..e53edafdb 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13800,7 +13800,7 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, if (modularity_o != Py_None){ if (igraphmodule_PyObject_to_real_t(modularity_o, &modularity)) - return NULL;; + return NULL; } else { return_modularity = true; From 06fca7378714020fa88950e7073e4a7607cb5cde Mon Sep 17 00:00:00 2001 From: BeaMarton13 Date: Wed, 25 Jun 2025 10:58:19 +0300 Subject: [PATCH 12/19] fix: mode out and modularity calculation --- src/_igraph/graphobject.c | 39 +++++++++++---------------------------- src/igraph/community.py | 18 +++++++++--------- 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index e53edafdb..9aeca3161 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13780,32 +13780,22 @@ PyObject *igraphmodule_Graph_community_fluid_communities(igraphmodule_GraphObjec */ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"modularity", "lengths", "weights", "mode", "radius", NULL}; + static char *kwlist[] = {"lengths", "weights", "mode", "radius", NULL}; PyObject *lengths_o = Py_None, *weights_o = Py_None; PyObject *mode_o = Py_None; PyObject *radius_o = Py_None; - PyObject *modularity_o = Py_None; igraph_vector_t *lengths_v = NULL; igraph_vector_t *weights_v = NULL; igraph_vector_int_t membership_v, generators_v; - igraph_neimode_t mode = IGRAPH_ALL; + igraph_neimode_t mode = IGRAPH_OUT; igraph_real_t radius = -1.0; /* negative means auto-optimize */ igraph_real_t modularity = IGRAPH_NAN; PyObject *membership_o, *generators_o, *result_o; - igraph_bool_t return_modularity = false; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, - &modularity_o, &lengths_o, &weights_o, &mode_o, &radius_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, + &lengths_o, &weights_o, &mode_o, &radius_o)) return NULL; - if (modularity_o != Py_None){ - if (igraphmodule_PyObject_to_real_t(modularity_o, &modularity)) - return NULL; - } - else { - return_modularity = true; - } - /* Handle mode parameter */ if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; @@ -13855,7 +13845,7 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, /* Call the C function - pass NULL for None parameters */ if (igraph_community_voronoi(&self->g, &membership_v, &generators_v, - return_modularity ? &modularity : NULL, + &modularity, lengths_v, weights_v, mode, radius)) { @@ -13897,11 +13887,8 @@ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, } /* Return tuple with membership, generators, and modularity */ - if (return_modularity) { - result_o = Py_BuildValue("(NNd)", membership_o, generators_o, modularity); - } else { - result_o = Py_BuildValue("(NN)", membership_o, generators_o); - } + result_o = Py_BuildValue("(NNd)", membership_o, generators_o, modularity); + return result_o; } @@ -18787,7 +18774,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_voronoi", (PyCFunction) igraphmodule_Graph_community_voronoi, METH_VARARGS | METH_KEYWORDS, - "community_voronoi(lengths=None, weights=None, mode=\"all\", radius=None, modularity=None)\n\n" + "community_voronoi(lengths=None, weights=None, mode=\"out\", radius=None)\n\n" "Finds communities using Voronoi partitioning.\n\n" "This function finds communities using a Voronoi partitioning of vertices based\n" "on the given edge lengths divided by the edge clustering coefficient.\n" @@ -18802,19 +18789,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param weights: edge weights, or C{None} to consider all edges as having\n" " unit weight. Weights are used when selecting generator points, as well\n" " as for computing modularity.\n" - "@param mode: if C{\"out\"}, distances from generator points to all other\n" + "@param mode: if C{\"out\"} (the default), distances from generator points to all other\n" " nodes are considered. If C{\"in\"}, the reverse distances are used.\n" " If C{\"all\"}, edge directions are ignored. This parameter is ignored\n" " for undirected graphs.\n" "@param radius: the radius/resolution to use when selecting generator points.\n" " The larger this value, the fewer partitions there will be. Pass C{None}\n" " to automatically select the radius that maximizes modularity.\n" - "@param modularity: if not C{None}, the modularity score will be calculated\n" - " and returned as part of the result tuple.\n" - "@return: a tuple containing the membership vector and generator vertices.\n" - " When modularity calculation is requested, also includes the modularity score\n" - " as a third element: (membership, generators, modularity).\n" - " Otherwise: (membership, generators).\n" + "@return: a tuple containing the membership vector, generator vertices, and\n" + " modularity score: (membership, generators, modularity).\n" "@rtype: tuple\n\n" "B{References}\n\n" " - Deritei et al., Community detection by graph Voronoi diagrams,\n" diff --git a/src/igraph/community.py b/src/igraph/community.py index a57869168..77afd6967 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -327,7 +327,7 @@ def _community_spinglass(graph, *args, **kwds): return VertexClustering(graph, membership, modularity_params=modularity_params) -def _community_voronoi(graph, modularity=None, lengths=None, weights=None, mode="all", radius=None): +def _community_voronoi(graph, lengths=None, weights=None, mode="out", radius=None): """Finds communities using Voronoi partitioning. This function finds communities using a Voronoi partitioning of vertices based @@ -354,10 +354,13 @@ def _community_voronoi(graph, modularity=None, lengths=None, weights=None, mode= @param weights: edge weights, or C{None} to consider all edges as having unit weight. Weights are used when selecting generator points, as well as for computing modularity. - @param mode: if C{"out"}, distances from generator points to all other - nodes are considered. If C{"in"}, the reverse distances are used. - If C{"all"}, edge directions are ignored. This parameter is ignored - for undirected graphs. + @param mode: specifies how to use the direction of edges when computing + distances from generator points. If C{"out"} (the default), distances + from generator points to all other nodes are considered following the + direction of edges. If C{"in"}, distances are computed in the reverse + direction (i.e., from all nodes to generator points). If C{"all"}, + edge directions are ignored and the graph is treated as undirected. + This parameter is ignored for undirected graphs. @param radius: the radius/resolution to use when selecting generator points. The larger this value, the fewer partitions there will be. Pass C{None} to automatically select the radius that maximizes modularity. @@ -372,10 +375,7 @@ def _community_voronoi(graph, modularity=None, lengths=None, weights=None, mode= else: raise ValueError(f"Invalid mode '{mode}'. Must be one of: out, in, all") - if modularity is None: - membership, generators, modularity = GraphBase.community_voronoi(graph, modularity, lengths, weights, mode, radius) - else: - membership, generators = GraphBase.community_voronoi(graph, modularity, lengths, weights, mode, radius) + membership, generators, modularity = GraphBase.community_voronoi(graph, lengths, weights, mode, radius) params = {"generators": generators} modularity_params = {} From 7a79c5c5ff155e5361b4edc193552c2ea5e0c4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bea=20M=C3=A1rton?= Date: Fri, 27 Jun 2025 09:14:17 +0300 Subject: [PATCH 13/19] Update tests/test_decomposition.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Szabolcs Horvát --- tests/test_decomposition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 2cc5fe607..b12c969f9 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -575,9 +575,9 @@ def testVoronoi(self): # Test 3: Three disconnected triangles g = Graph(9) - g.add_edges([(0, 1), (1, 2), (2, 0)]) # Triangle 1 - g.add_edges([(3, 4), (4, 5), (5, 3)]) # Triangle 2 - g.add_edges([(6, 7), (7, 8), (8, 6)]) # Triangle 3 + g.add_edges([(0, 1), (1, 2), (2, 0), # Triangle 1 + (3, 4), (4, 5), (5, 3), # Triangle 2 + (6, 7), (7, 8), (8, 6)]) # Triangle 3 cl = g.community_voronoi() From 1d7e7ff76219ef9554c0f546f3b1c37cb02da454 Mon Sep 17 00:00:00 2001 From: BeaMarton13 Date: Fri, 27 Jun 2025 09:18:46 +0300 Subject: [PATCH 14/19] fix: removed triangles - Copilot review --- tests/test_decomposition.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index b12c969f9..109cad0dd 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -584,13 +584,6 @@ def testVoronoi(self): # Should find exactly 3 communities self.assertEqual(len(cl), 3) - # Each triangle should be in its own community - triangles = [ - {cl.membership[0], cl.membership[1], cl.membership[2]}, - {cl.membership[3], cl.membership[4], cl.membership[5]}, - {cl.membership[6], cl.membership[7], cl.membership[8]}, - ] - def testWalktrap(self): g = Graph.Full(5) + Graph.Full(5) + Graph.Full(5) g += [(0, 5), (5, 10), (10, 0)] From 2f872378f3b895fa8cd5a106402b1c85618e797e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 27 Jun 2025 08:57:38 +0000 Subject: [PATCH 15/19] reformat for consistency --- src/_igraph/graphobject.c | 179 +++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 89 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 9aeca3161..b89db8242 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13780,117 +13780,118 @@ PyObject *igraphmodule_Graph_community_fluid_communities(igraphmodule_GraphObjec */ PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"lengths", "weights", "mode", "radius", NULL}; - PyObject *lengths_o = Py_None, *weights_o = Py_None; - PyObject *mode_o = Py_None; - PyObject *radius_o = Py_None; - igraph_vector_t *lengths_v = NULL; - igraph_vector_t *weights_v = NULL; - igraph_vector_int_t membership_v, generators_v; - igraph_neimode_t mode = IGRAPH_OUT; - igraph_real_t radius = -1.0; /* negative means auto-optimize */ - igraph_real_t modularity = IGRAPH_NAN; - PyObject *membership_o, *generators_o, *result_o; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, - &lengths_o, &weights_o, &mode_o, &radius_o)) - return NULL; + static char *kwlist[] = {"lengths", "weights", "mode", "radius", NULL}; + PyObject *lengths_o = Py_None, *weights_o = Py_None; + PyObject *mode_o = Py_None; + PyObject *radius_o = Py_None; + igraph_vector_t *lengths_v = NULL; + igraph_vector_t *weights_v = NULL; + igraph_vector_int_t membership_v, generators_v; + igraph_neimode_t mode = IGRAPH_OUT; + igraph_real_t radius = -1.0; /* negative means auto-optimize */ + igraph_real_t modularity = IGRAPH_NAN; + PyObject *membership_o, *generators_o, *result_o; - /* Handle mode parameter */ - if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) - return NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, + &lengths_o, &weights_o, &mode_o, &radius_o)) { + return NULL; + } - /* Handle radius parameter */ - if (radius_o != Py_None) { - if (igraphmodule_PyObject_to_real_t(radius_o, &radius)) - return NULL; - } + /* Handle mode parameter */ + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { + return NULL; + } - /* Handle lengths parameter */ - if (igraphmodule_attrib_to_vector_t(lengths_o, self, &lengths_v, ATTRIBUTE_TYPE_EDGE)) { - return NULL; - } + /* Handle radius parameter */ + if (radius_o != Py_None) { + if (igraphmodule_PyObject_to_real_t(radius_o, &radius)) { + return NULL; + } + } - /* Handle weights parameter */ - if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { - if (lengths_v != NULL) { - igraph_vector_destroy(lengths_v); free(lengths_v); - } - return NULL; - } + /* Handle lengths parameter */ + if (igraphmodule_attrib_to_vector_t(lengths_o, self, &lengths_v, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } - /* Initialize result vectors */ - if (igraph_vector_int_init(&membership_v, 0)) { - if (lengths_v != NULL) { - igraph_vector_destroy(lengths_v); free(lengths_v); - } - if (weights_v != NULL) { - igraph_vector_destroy(weights_v); free(weights_v); - } - igraphmodule_handle_igraph_error(); - return NULL; + /* Handle weights parameter */ + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); } + return NULL; + } - if (igraph_vector_int_init(&generators_v, 0)) { - if (lengths_v != NULL) { - igraph_vector_destroy(lengths_v); free(lengths_v); - } - if (weights_v != NULL) { - igraph_vector_destroy(weights_v); free(weights_v); - } - igraph_vector_int_destroy(&membership_v); - igraphmodule_handle_igraph_error(); - return NULL; + /* Initialize result vectors */ + if (igraph_vector_int_init(&membership_v, 0)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); } - - /* Call the C function - pass NULL for None parameters */ - if (igraph_community_voronoi(&self->g, &membership_v, &generators_v, - &modularity, - lengths_v, - weights_v, - mode, radius)) { - - if (lengths_v != NULL) { - igraph_vector_destroy(lengths_v); free(lengths_v); - } - if (weights_v != NULL) { - igraph_vector_destroy(weights_v); free(weights_v); - } - igraph_vector_int_destroy(&membership_v); - igraph_vector_int_destroy(&generators_v); - igraphmodule_handle_igraph_error(); - return NULL; + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); } + igraphmodule_handle_igraph_error(); + return NULL; + } - /* Clean up input vectors */ - + if (igraph_vector_int_init(&generators_v, 0)) { if (lengths_v != NULL) { igraph_vector_destroy(lengths_v); free(lengths_v); } if (weights_v != NULL) { igraph_vector_destroy(weights_v); free(weights_v); } - - /* Convert results to Python objects */ - membership_o = igraphmodule_vector_int_t_to_PyList(&membership_v); igraph_vector_int_destroy(&membership_v); - if (!membership_o) { - igraph_vector_int_destroy(&generators_v); - return NULL; + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* Call the C function - pass NULL for None parameters */ + if (igraph_community_voronoi(&self->g, &membership_v, &generators_v, + &modularity, + lengths_v, + weights_v, + mode, radius)) { + + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); } + igraph_vector_int_destroy(&membership_v); + igraph_vector_int_destroy(&generators_v); + igraphmodule_handle_igraph_error(); + return NULL; + } - generators_o = igraphmodule_vector_int_t_to_PyList(&generators_v); + /* Clean up input vectors */ + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + + /* Convert results to Python objects */ + membership_o = igraphmodule_vector_int_t_to_PyList(&membership_v); + igraph_vector_int_destroy(&membership_v); + if (!membership_o) { igraph_vector_int_destroy(&generators_v); - if (!generators_o) { - Py_DECREF(membership_o); - return NULL; - } + return NULL; + } - /* Return tuple with membership, generators, and modularity */ - result_o = Py_BuildValue("(NNd)", membership_o, generators_o, modularity); - + generators_o = igraphmodule_vector_int_t_to_PyList(&generators_v); + igraph_vector_int_destroy(&generators_v); + if (!generators_o) { + Py_DECREF(membership_o); + return NULL; + } - return result_o; + /* Return tuple with membership, generators, and modularity */ + result_o = Py_BuildValue("(NNd)", membership_o, generators_o, modularity); + + return result_o; } /********************************************************************** From c9d5cbe6c5c223d22c5097218494ef9ecb663c8e Mon Sep 17 00:00:00 2001 From: BeaMarton13 Date: Sat, 28 Jun 2025 12:07:12 +0300 Subject: [PATCH 16/19] fix: documentation updated --- src/_igraph/graphobject.c | 14 +++++++------- src/igraph/community.py | 13 ++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 9aeca3161..ba76de7cf 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -18780,7 +18780,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "on the given edge lengths divided by the edge clustering coefficient.\n" "The generator vertices are chosen to be those with the largest local relative\n" "density within a radius, with the local relative density of a vertex defined as\n" - "s * m / (m + k), where s is the strength of the vertex, m is the number of\n" + "C{s * m / (m + k)}, where s is the strength of the vertex, m is the number of\n" "edges within the vertex's first order neighborhood, while k is the number of\n" "edges with only one endpoint within this neighborhood.\n\n" "@param lengths: edge lengths, or C{None} to consider all edges as having\n" @@ -18800,12 +18800,12 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " modularity score: (membership, generators, modularity).\n" "@rtype: tuple\n\n" "B{References}\n\n" - " - Deritei et al., Community detection by graph Voronoi diagrams,\n" - " New Journal of Physics 16, 063007 (2014)\n" - " https://doi.org/10.1088/1367-2630/16/6/063007\n" - " - Molnár et al., Community Detection in Directed Weighted Networks\n" - " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" - " https://doi.org/10.1038/s41598-024-58624-4\n" + " - Deritei et al., Community detection by graph Voronoi diagrams,\n" + " New Journal of Physics 16, 063007 (2014)\n" + " https://doi.org/10.1088/1367-2630/16/6/063007\n" + " - Molnár et al., Community Detection in Directed Weighted Networks\n" + " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" + " https://doi.org/10.1038/s41598-024-58624-4\n" }, {"community_leiden", (PyCFunction) igraphmodule_Graph_community_leiden, diff --git a/src/igraph/community.py b/src/igraph/community.py index 77afd6967..6160dd0fe 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -331,13 +331,12 @@ def _community_voronoi(graph, lengths=None, weights=None, mode="out", radius=Non """Finds communities using Voronoi partitioning. This function finds communities using a Voronoi partitioning of vertices based - on the given edge lengths divided by the edge clustering coefficient - (L{igraph.Graph.ecc}). The generator vertices are chosen to be those with the - largest local relative density within a radius, with the local relative - density of a vertex defined as C{s * m / (m + k)}, where C{s} is the strength - of the vertex, C{m} is the number of edges within the vertex's first order - neighborhood, while C{k} is the number of edges with only one endpoint within - this neighborhood. + on the given edge lengths divided by the edge clustering coefficient. + The generator vertices are chosen to be those with the largest local relative + density within a radius, with the local relative density of a vertex defined + as C{s * m / (m + k)}, where C{s} is the strength of the vertex, C{m} is + the number of edges within the vertex's first order neighborhood, while C{k} + is the number of edges with only one endpoint within this neighborhood. B{References} From 7c30c6ccd58af9c6bd64062e0dbe28c9cc820852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 28 Jun 2025 10:08:18 +0000 Subject: [PATCH 17/19] fix docstring of GraphBase.community_voronoi --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index c753b22a8..8ed4461b5 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -18775,7 +18775,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_voronoi", (PyCFunction) igraphmodule_Graph_community_voronoi, METH_VARARGS | METH_KEYWORDS, - "community_voronoi(lengths=None, weights=None, mode=\"out\", radius=None)\n\n" + "community_voronoi(lengths=None, weights=None, mode=\"out\", radius=None)\n--\n\n" "Finds communities using Voronoi partitioning.\n\n" "This function finds communities using a Voronoi partitioning of vertices based\n" "on the given edge lengths divided by the edge clustering coefficient.\n" From 068ffb70922b0fbb7f0e2976345b30fa87639734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 28 Jun 2025 10:25:35 +0000 Subject: [PATCH 18/19] more doc formatting --- src/_igraph/graphobject.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8ed4461b5..0c1401f60 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -18784,6 +18784,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "C{s * m / (m + k)}, where s is the strength of the vertex, m is the number of\n" "edges within the vertex's first order neighborhood, while k is the number of\n" "edges with only one endpoint within this neighborhood.\n\n" + "B{References}\n\n" + " - Deritei et al., Community detection by graph Voronoi diagrams,\n" + " New Journal of Physics 16, 063007 (2014)\n" + " U{https://doi.org/10.1088/1367-2630/16/6/063007}\n" + " - Molnár et al., Community Detection in Directed Weighted Networks\n" + " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" + " U{https://doi.org/10.1038/s41598-024-58624-4}\n\n" "@param lengths: edge lengths, or C{None} to consider all edges as having\n" " unit length. Voronoi partitioning will use edge lengths equal to\n" " lengths / ECC where ECC is the edge clustering coefficient.\n" @@ -18799,14 +18806,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " to automatically select the radius that maximizes modularity.\n" "@return: a tuple containing the membership vector, generator vertices, and\n" " modularity score: (membership, generators, modularity).\n" - "@rtype: tuple\n\n" - "B{References}\n\n" - " - Deritei et al., Community detection by graph Voronoi diagrams,\n" - " New Journal of Physics 16, 063007 (2014)\n" - " https://doi.org/10.1088/1367-2630/16/6/063007\n" - " - Molnár et al., Community Detection in Directed Weighted Networks\n" - " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" - " https://doi.org/10.1038/s41598-024-58624-4\n" + "@rtype: tuple\n" }, {"community_leiden", (PyCFunction) igraphmodule_Graph_community_leiden, From 575bf208d4fccb35a84df5e0c161fbed7ca5dfaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Sat, 28 Jun 2025 11:00:58 +0000 Subject: [PATCH 19/19] doc nitpick --- src/igraph/community.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/igraph/community.py b/src/igraph/community.py index 6160dd0fe..d08546bd7 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -363,7 +363,7 @@ def _community_voronoi(graph, lengths=None, weights=None, mode="out", radius=Non @param radius: the radius/resolution to use when selecting generator points. The larger this value, the fewer partitions there will be. Pass C{None} to automatically select the radius that maximizes modularity. - @return: an appropriate L{VertexClustering} object with extra attributes + @return: an appropriate L{VertexClustering} object with an extra attribute called C{generators} (the generator vertices). """ # Convert mode string to proper enum value to avoid deprecation warning