Files
agent_compositor_test/references/igraph-1.0.1/src/community/leiden.c
T
Abdelrahman Said a11edf0c53 Add graph references
2026-06-28 13:49:01 +01:00

1504 lines
63 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
igraph library.
Copyright (C) 2020-2025 The igraph development team <igraph@igraph.org>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "igraph_community.h"
#include "igraph_adjlist.h"
#include "igraph_bitset.h"
#include "igraph_constructors.h"
#include "igraph_dqueue.h"
#include "igraph_interface.h"
#include "igraph_memory.h"
#include "igraph_random.h"
#include "igraph_stack.h"
#include "igraph_structural.h"
#include "igraph_vector.h"
#include "igraph_vector_list.h"
#include "core/interruption.h"
/* Move vertices in order to improve the quality of a partition.
*
* This function considers each vertex and greedily moves it to a neighboring
* community that maximizes the improvement in the quality of a partition.
* Only moves that strictly improve the quality are considered.
*
* The vertices are examined in a queue, and initially all vertices are put in the
* queue in a random order. Vertices are popped from the queue when they are
* examined, and only neighbors of vertices that are moved (which are not part of
* the cluster the vertex was moved to) are pushed to the queue again.
*
* The \p membership vector is used as the starting point to move around vertices,
* and is updated in-place.
*
*/
static igraph_error_t leiden_fastmove_vertices(
const igraph_t *graph,
const igraph_inclist_t *edges_per_vertex,
const igraph_vector_t *edge_weights,
const igraph_vector_t *vertex_out_weights,
const igraph_vector_t *vertex_in_weights,
const igraph_real_t resolution,
igraph_int_t *nb_clusters,
igraph_vector_int_t *membership,
igraph_bool_t *changed) {
const igraph_int_t n = igraph_vcount(graph);
const igraph_bool_t directed = (vertex_in_weights != NULL);
igraph_dqueue_int_t unstable_vertices;
igraph_real_t max_diff, diff;
igraph_bitset_t neighbor_cluster_added, vertex_is_stable;
igraph_vector_t cluster_out_weights, cluster_in_weights;
igraph_vector_t edge_weights_per_cluster;
igraph_vector_int_t neighbor_clusters;
igraph_vector_int_t vertex_order;
igraph_vector_int_t nb_vertices_per_cluster;
igraph_stack_int_t empty_clusters;
igraph_int_t c, nb_neigh_clusters;
int iter = 0;
/* Initialize queue of unstable vertices and whether vertex is stable. Only
* unstable vertices are in the queue. */
IGRAPH_BITSET_INIT_FINALLY(&vertex_is_stable, n);
IGRAPH_DQUEUE_INT_INIT_FINALLY(&unstable_vertices, n);
/* Shuffle vertices */
IGRAPH_CHECK(igraph_vector_int_init_range(&vertex_order, 0, n));
IGRAPH_FINALLY(igraph_vector_int_destroy, &vertex_order);
igraph_vector_int_shuffle(&vertex_order);
/* Add to the queue */
for (igraph_int_t i = 0; i < n; i++) {
IGRAPH_CHECK(igraph_dqueue_int_push(&unstable_vertices, VECTOR(vertex_order)[i]));
}
/* Initialize cluster weights and nb vertices */
IGRAPH_VECTOR_INIT_FINALLY(&cluster_out_weights, n);
if (directed) {
IGRAPH_VECTOR_INIT_FINALLY(&cluster_in_weights, n);
}
IGRAPH_VECTOR_INT_INIT_FINALLY(&nb_vertices_per_cluster, n);
for (igraph_int_t i = 0; i < n; i++) {
c = VECTOR(*membership)[i];
VECTOR(cluster_out_weights)[c] += VECTOR(*vertex_out_weights)[i];
if (directed) {
VECTOR(cluster_in_weights)[c] += VECTOR(*vertex_in_weights)[i];
}
VECTOR(nb_vertices_per_cluster)[c] += 1;
}
/* Initialize empty clusters */
IGRAPH_STACK_INT_INIT_FINALLY(&empty_clusters, n);
for (c = 0; c < n; c++) {
if (VECTOR(nb_vertices_per_cluster)[c] == 0) {
IGRAPH_CHECK(igraph_stack_int_push(&empty_clusters, c));
}
}
/* Initialize vectors to be used in calculating differences */
IGRAPH_VECTOR_INIT_FINALLY(&edge_weights_per_cluster, n);
/* Initialize neighboring cluster */
IGRAPH_BITSET_INIT_FINALLY(&neighbor_cluster_added, n);
IGRAPH_VECTOR_INT_INIT_FINALLY(&neighbor_clusters, n);
/* Iterate while the queue is not empty */
while (!igraph_dqueue_int_empty(&unstable_vertices)) {
igraph_int_t v = igraph_dqueue_int_pop(&unstable_vertices);
igraph_int_t best_cluster, current_cluster = VECTOR(*membership)[v];
igraph_int_t degree;
igraph_vector_int_t *edges;
/* Remove vertex from current cluster */
VECTOR(cluster_out_weights)[current_cluster] -= VECTOR(*vertex_out_weights)[v];
if (directed) {
VECTOR(cluster_in_weights)[current_cluster] -= VECTOR(*vertex_in_weights)[v];
}
VECTOR(nb_vertices_per_cluster)[current_cluster]--;
if (VECTOR(nb_vertices_per_cluster)[current_cluster] == 0) {
IGRAPH_CHECK(igraph_stack_int_push(&empty_clusters, current_cluster));
}
/* Find out neighboring clusters */
c = igraph_stack_int_top(&empty_clusters);
VECTOR(neighbor_clusters)[0] = c;
IGRAPH_BIT_SET(neighbor_cluster_added, c);
nb_neigh_clusters = 1;
/* Determine the edge weight to each neighboring cluster */
edges = igraph_inclist_get(edges_per_vertex, v);
degree = igraph_vector_int_size(edges);
for (igraph_int_t i = 0; i < degree; i++) {
igraph_int_t e = VECTOR(*edges)[i];
igraph_int_t u = IGRAPH_OTHER(graph, e, v);
if (u != v) {
c = VECTOR(*membership)[u];
if (!IGRAPH_BIT_TEST(neighbor_cluster_added, c)) {
IGRAPH_BIT_SET(neighbor_cluster_added, c);
VECTOR(neighbor_clusters)[nb_neigh_clusters++] = c;
}
VECTOR(edge_weights_per_cluster)[c] += VECTOR(*edge_weights)[e];
}
}
/* Calculate maximum diff */
best_cluster = current_cluster;
max_diff = VECTOR(edge_weights_per_cluster)[current_cluster];
if (directed) {
max_diff -=
(VECTOR(*vertex_in_weights)[v] * VECTOR(cluster_out_weights)[current_cluster] +
VECTOR(*vertex_out_weights)[v] * VECTOR(cluster_in_weights)[current_cluster]) * resolution;
} else {
max_diff -= VECTOR(*vertex_out_weights)[v] * VECTOR(cluster_out_weights)[current_cluster] * resolution;
}
for (igraph_int_t i = 0; i < nb_neigh_clusters; i++) {
c = VECTOR(neighbor_clusters)[i];
diff = VECTOR(edge_weights_per_cluster)[c];
if (directed) {
diff -= (VECTOR(*vertex_out_weights)[v] * VECTOR(cluster_in_weights)[c] +
VECTOR(*vertex_in_weights)[v] * VECTOR(cluster_out_weights)[c]) * resolution;
} else {
diff -= VECTOR(*vertex_out_weights)[v] * VECTOR(cluster_out_weights)[c] * resolution;
}
/* Only consider strictly improving moves.
* Note that this is important in considering convergence.
*/
if (diff > max_diff) {
best_cluster = c;
max_diff = diff;
}
VECTOR(edge_weights_per_cluster)[c] = 0.0;
IGRAPH_BIT_CLEAR(neighbor_cluster_added, c);
}
/* Move vertex to best cluster */
VECTOR(cluster_out_weights)[best_cluster] += VECTOR(*vertex_out_weights)[v];
if (directed) {
VECTOR(cluster_in_weights)[best_cluster] += VECTOR(*vertex_in_weights)[v];
}
VECTOR(nb_vertices_per_cluster)[best_cluster]++;
if (best_cluster == igraph_stack_int_top(&empty_clusters)) {
igraph_stack_int_pop(&empty_clusters);
}
/* Mark vertex as stable */
IGRAPH_BIT_SET(vertex_is_stable, v);
/* Add stable neighbours that are not part of the new cluster to the queue */
if (best_cluster != current_cluster) {
*changed = true;
VECTOR(*membership)[v] = best_cluster;
for (igraph_int_t i = 0; i < degree; i++) {
igraph_int_t e = VECTOR(*edges)[i];
igraph_int_t u = IGRAPH_OTHER(graph, e, v);
if (IGRAPH_BIT_TEST(vertex_is_stable, u) && VECTOR(*membership)[u] != best_cluster) {
IGRAPH_CHECK(igraph_dqueue_int_push(&unstable_vertices, u));
IGRAPH_BIT_CLEAR(vertex_is_stable, u);
}
}
}
IGRAPH_ALLOW_INTERRUPTION_LIMITED(iter, 1 << 14);
}
IGRAPH_CHECK(igraph_reindex_membership(membership, NULL, nb_clusters));
igraph_vector_int_destroy(&neighbor_clusters);
igraph_bitset_destroy(&neighbor_cluster_added);
igraph_vector_destroy(&edge_weights_per_cluster);
igraph_stack_int_destroy(&empty_clusters);
igraph_vector_int_destroy(&nb_vertices_per_cluster);
if (directed) igraph_vector_destroy(&cluster_in_weights);
igraph_vector_destroy(&cluster_out_weights);
igraph_vector_int_destroy(&vertex_order);
igraph_dqueue_int_destroy(&unstable_vertices);
igraph_bitset_destroy(&vertex_is_stable);
if (directed) {
IGRAPH_FINALLY_CLEAN(10);
} else {
IGRAPH_FINALLY_CLEAN(9);
}
return IGRAPH_SUCCESS;
}
/* Clean a refined membership vector.
*
* This function examines all vertices in \p vertex_subset and updates
* \p refined_membership to ensure that the clusters are numbered consecutively,
* starting from \p nb_refined_clusters. The \p nb_refined_clusters is also
* updated itself. If C is the initial \p nb_refined_clusters and C' the
* resulting \p nb_refined_clusters, then vertices in \p vertex_subset are numbered
* C, C + 1, ..., C' - 1.
*/
static igraph_error_t leiden_clean_refined_membership(
const igraph_vector_int_t* vertex_subset,
igraph_vector_int_t *refined_membership,
igraph_int_t* nb_refined_clusters) {
const igraph_int_t n = igraph_vector_int_size(vertex_subset);
igraph_vector_int_t new_cluster;
IGRAPH_VECTOR_INT_INIT_FINALLY(&new_cluster, n);
/* Clean clusters. We will store the new cluster + 1 so that cluster == 0
* indicates that no membership was assigned yet. */
*nb_refined_clusters += 1;
for (igraph_int_t i = 0; i < n; i++) {
igraph_int_t v = VECTOR(*vertex_subset)[i];
igraph_int_t c = VECTOR(*refined_membership)[v];
if (VECTOR(new_cluster)[c] == 0) {
VECTOR(new_cluster)[c] = *nb_refined_clusters;
*nb_refined_clusters += 1;
}
}
/* Assign new cluster */
for (igraph_int_t i = 0; i < n; i++) {
igraph_int_t v = VECTOR(*vertex_subset)[i];
igraph_int_t c = VECTOR(*refined_membership)[v];
VECTOR(*refined_membership)[v] = VECTOR(new_cluster)[c] - 1;
}
/* We used the cluster + 1, so correct */
*nb_refined_clusters -= 1;
igraph_vector_int_destroy(&new_cluster);
IGRAPH_FINALLY_CLEAN(1);
return IGRAPH_SUCCESS;
}
/* Merge vertices for a subset of the vertices. This is used to refine a partition.
*
* The vertices included in \p vertex_subset are assumed to be the vertices i for which
* membership[i] = cluster_subset.
*
* All vertices in \p vertex_subset are initialized to a singleton partition in \p
* refined_membership. Only singleton clusters can be merged if they are
* sufficiently well connected to the current subgraph induced by \p
* vertex_subset.
*
* We only examine each vertex once. Instead of greedily choosing the maximum
* possible cluster to merge with, the cluster is chosen randomly among all
* possibilities that do not decrease the quality of the partition. The
* probability of choosing a certain cluster is proportional to exp(diff/beta).
* For beta to 0 this converges to selecting a cluster with the maximum
* improvement. For beta to infinity this converges to a uniform distribution
* among all eligible clusters.
*
* The \p refined_membership is updated for vertex in \p vertex_subset. The number
* of refined clusters, \p nb_refined_clusters is used to set the actual refined
* cluster membership and is updated after this routine. Within each cluster
* (i.e. for a given \p vertex_subset), the refined membership is initially simply
* set to 0, ..., n - 1 (for n vertices in \p vertex_subset). However, for each \p
* vertex_subset the refined membership should of course be unique. Hence, after
* merging, the refined membership starts with \p nb_refined_clusters, which is
* also updated to ensure that the resulting \p nb_refined_clusters counts all
* refined clusters that have already been processed. See
* leiden_clean_refined_membership for more information about
* this aspect.
*/
static igraph_error_t leiden_merge_vertices(
const igraph_t *graph,
const igraph_inclist_t *edges_per_vertex,
const igraph_vector_t *edge_weights,
const igraph_vector_t *vertex_out_weights,
const igraph_vector_t *vertex_in_weights,
const igraph_vector_int_t *vertex_subset,
const igraph_vector_int_t *membership,
const igraph_int_t cluster_subset,
const igraph_real_t resolution,
const igraph_real_t beta,
igraph_int_t *nb_refined_clusters,
igraph_vector_int_t *refined_membership) {
const igraph_bool_t directed = (vertex_in_weights != NULL);
igraph_vector_int_t vertex_order;
igraph_bitset_t non_singleton_cluster, neighbor_cluster_added;
igraph_real_t max_diff, total_cum_trans_diff, diff;
igraph_real_t total_vertex_out_weight = 0.0, total_vertex_in_weight = 0.0;
const igraph_int_t n = igraph_vector_int_size(vertex_subset);
igraph_vector_t cluster_out_weights, cluster_in_weights;
igraph_vector_t cum_trans_diff, edge_weights_per_cluster, external_edge_weight_per_cluster_in_subset;
igraph_vector_int_t neighbor_clusters;
igraph_vector_int_t *edges, nb_vertices_per_cluster;
igraph_int_t degree, nb_neigh_clusters;
/* Initialize cluster weights */
IGRAPH_VECTOR_INIT_FINALLY(&cluster_out_weights, n);
if (directed) {
IGRAPH_VECTOR_INIT_FINALLY(&cluster_in_weights, n);
}
/* Initialize number of vertices per cluster */
IGRAPH_VECTOR_INT_INIT_FINALLY(&nb_vertices_per_cluster, n);
/* Initialize external edge weight per cluster in subset */
IGRAPH_VECTOR_INIT_FINALLY(&external_edge_weight_per_cluster_in_subset, n);
/* Initialize administration for a singleton partition */
for (igraph_int_t i = 0; i < n; i++) {
igraph_int_t v = VECTOR(*vertex_subset)[i];
VECTOR(*refined_membership)[v] = i;
VECTOR(cluster_out_weights)[i] += VECTOR(*vertex_out_weights)[v];
total_vertex_out_weight += VECTOR(*vertex_out_weights)[v];
if (directed) {
VECTOR(cluster_in_weights)[i] += VECTOR(*vertex_in_weights)[v];
total_vertex_in_weight += VECTOR(*vertex_in_weights)[v];
}
VECTOR(nb_vertices_per_cluster)[i] += 1;
/* Find out neighboring clusters */
edges = igraph_inclist_get(edges_per_vertex, v);
degree = igraph_vector_int_size(edges);
for (igraph_int_t j = 0; j < degree; j++) {
igraph_int_t e = VECTOR(*edges)[j];
igraph_int_t u = IGRAPH_OTHER(graph, e, v);
if (u != v && VECTOR(*membership)[u] == cluster_subset) {
VECTOR(external_edge_weight_per_cluster_in_subset)[i] += VECTOR(*edge_weights)[e];
}
}
}
/* Shuffle vertices */
IGRAPH_CHECK(igraph_vector_int_init_copy(&vertex_order, vertex_subset));
IGRAPH_FINALLY(igraph_vector_int_destroy, &vertex_order);
igraph_vector_int_shuffle(&vertex_order);
/* Initialize non singleton clusters */
IGRAPH_BITSET_INIT_FINALLY(&non_singleton_cluster, n);
/* Initialize vectors to be used in calculating differences */
IGRAPH_VECTOR_INIT_FINALLY(&edge_weights_per_cluster, n);
/* Initialize neighboring cluster */
IGRAPH_BITSET_INIT_FINALLY(&neighbor_cluster_added, n);
IGRAPH_VECTOR_INT_INIT_FINALLY(&neighbor_clusters, n);
/* Initialize cumulative transformed difference */
IGRAPH_VECTOR_INIT_FINALLY(&cum_trans_diff, n);
for (igraph_int_t i = 0; i < n; i++) {
igraph_int_t v = VECTOR(vertex_order)[i];
igraph_int_t chosen_cluster, best_cluster, current_cluster = VECTOR(*refined_membership)[v];
igraph_real_t vertex_weight_prod;
if (directed) {
vertex_weight_prod =
VECTOR(cluster_out_weights)[current_cluster] * (total_vertex_in_weight - VECTOR(cluster_in_weights)[current_cluster]) +
VECTOR(cluster_in_weights)[current_cluster] * (total_vertex_out_weight - VECTOR(cluster_out_weights)[current_cluster]);
} else {
vertex_weight_prod = VECTOR(cluster_out_weights)[current_cluster] * (total_vertex_out_weight - VECTOR(cluster_out_weights)[current_cluster]);
}
if (!IGRAPH_BIT_TEST(non_singleton_cluster, current_cluster) &&
(VECTOR(external_edge_weight_per_cluster_in_subset)[current_cluster] >=
vertex_weight_prod * resolution)) {
/* Remove vertex from current cluster, which is then a singleton by
* definition. */
VECTOR(cluster_out_weights)[current_cluster] = 0.0;
if (directed) {
VECTOR(cluster_in_weights)[current_cluster] = 0.0;
}
VECTOR(nb_vertices_per_cluster)[current_cluster] = 0;
/* Find out neighboring clusters */
edges = igraph_inclist_get(edges_per_vertex, v);
degree = igraph_vector_int_size(edges);
/* Also add current cluster to ensure it can be chosen. */
VECTOR(neighbor_clusters)[0] = current_cluster;
IGRAPH_BIT_SET(neighbor_cluster_added, current_cluster);
nb_neigh_clusters = 1;
for (igraph_int_t j = 0; j < degree; j++) {
igraph_int_t e = VECTOR(*edges)[j];
igraph_int_t u = IGRAPH_OTHER(graph, e, v);
if (u != v && VECTOR(*membership)[u] == cluster_subset) {
igraph_int_t c = VECTOR(*refined_membership)[u];
if (!IGRAPH_BIT_TEST(neighbor_cluster_added, c)) {
IGRAPH_BIT_SET(neighbor_cluster_added, c);
VECTOR(neighbor_clusters)[nb_neigh_clusters++] = c;
}
VECTOR(edge_weights_per_cluster)[c] += VECTOR(*edge_weights)[e];
}
}
/* Calculate diffs */
best_cluster = current_cluster;
max_diff = 0.0;
total_cum_trans_diff = 0.0;
for (igraph_int_t j = 0; j < nb_neigh_clusters; j++) {
igraph_int_t c = VECTOR(neighbor_clusters)[j];
if (directed) {
vertex_weight_prod =
VECTOR(cluster_out_weights)[c] * (total_vertex_in_weight - VECTOR(cluster_in_weights)[c]) +
VECTOR(cluster_in_weights)[c] * (total_vertex_out_weight - VECTOR(cluster_out_weights)[c]);
} else {
vertex_weight_prod = VECTOR(cluster_out_weights)[c] * (total_vertex_out_weight - VECTOR(cluster_out_weights)[c]);
}
if (VECTOR(external_edge_weight_per_cluster_in_subset)[c] >= vertex_weight_prod * resolution) {
diff = VECTOR(edge_weights_per_cluster)[c];
if (directed) {
diff -= (VECTOR(*vertex_out_weights)[v] * VECTOR(cluster_in_weights)[c] +
VECTOR(*vertex_in_weights)[v] * VECTOR(cluster_out_weights)[c]) * resolution;
} else {
diff -= VECTOR(*vertex_out_weights)[v] * VECTOR(cluster_out_weights)[c] * resolution;
}
if (diff > max_diff) {
best_cluster = c;
max_diff = diff;
}
/* Calculate the transformed difference for sampling */
if (diff >= 0) {
total_cum_trans_diff += exp(diff / beta);
}
}
VECTOR(cum_trans_diff)[j] = total_cum_trans_diff;
VECTOR(edge_weights_per_cluster)[c] = 0.0;
IGRAPH_BIT_CLEAR(neighbor_cluster_added, c);
}
/* Determine the neighboring cluster to which the currently selected vertex
* will be moved.
*/
if (total_cum_trans_diff < IGRAPH_INFINITY) {
igraph_real_t r = RNG_UNIF(0, total_cum_trans_diff);
igraph_int_t chosen_idx;
igraph_vector_binsearch_slice(&cum_trans_diff, r, &chosen_idx, 0, nb_neigh_clusters);
chosen_cluster = VECTOR(neighbor_clusters)[chosen_idx];
} else {
chosen_cluster = best_cluster;
}
/* Move vertex to randomly chosen cluster */
VECTOR(cluster_out_weights)[chosen_cluster] += VECTOR(*vertex_out_weights)[v];
if (directed) {
VECTOR(cluster_in_weights)[chosen_cluster] += VECTOR(*vertex_in_weights)[v];
}
VECTOR(nb_vertices_per_cluster)[chosen_cluster]++;
for (igraph_int_t j = 0; j < degree; j++) {
igraph_int_t e = VECTOR(*edges)[j];
igraph_int_t u = IGRAPH_OTHER(graph, e, v);
if (VECTOR(*membership)[u] == cluster_subset) {
if (VECTOR(*refined_membership)[u] == chosen_cluster) {
VECTOR(external_edge_weight_per_cluster_in_subset)[chosen_cluster] -= VECTOR(*edge_weights)[e];
} else {
VECTOR(external_edge_weight_per_cluster_in_subset)[chosen_cluster] += VECTOR(*edge_weights)[e];
}
}
}
/* Set cluster */
if (chosen_cluster != current_cluster) {
VECTOR(*refined_membership)[v] = chosen_cluster;
IGRAPH_BIT_SET(non_singleton_cluster, chosen_cluster);
}
} /* end if singleton and may be merged */
}
IGRAPH_CHECK(leiden_clean_refined_membership(vertex_subset, refined_membership, nb_refined_clusters));
igraph_vector_destroy(&cum_trans_diff);
igraph_vector_int_destroy(&neighbor_clusters);
igraph_bitset_destroy(&neighbor_cluster_added);
igraph_vector_destroy(&edge_weights_per_cluster);
igraph_bitset_destroy(&non_singleton_cluster);
igraph_vector_int_destroy(&vertex_order);
igraph_vector_destroy(&external_edge_weight_per_cluster_in_subset);
igraph_vector_int_destroy(&nb_vertices_per_cluster);
if (directed) igraph_vector_destroy(&cluster_in_weights);
igraph_vector_destroy(&cluster_out_weights);
if (directed) {
IGRAPH_FINALLY_CLEAN(10);
} else {
IGRAPH_FINALLY_CLEAN(9);
}
return IGRAPH_SUCCESS;
}
/* Create clusters out of a membership vector.
*
* It is assumed that the incoming list of integer vectors is already sized
* appropriately (i.e. it has at least as many items as the number of clusters
* in the membership vector), and that each item in the list of integer vectors
* is empty.
*/
static igraph_error_t leiden_get_clusters(
const igraph_vector_int_t *membership,
igraph_vector_int_list_t *clusters) {
const igraph_int_t n = igraph_vector_int_size(membership);
for (igraph_int_t i = 0; i < n; i++) {
/* Get cluster for vertex i */
igraph_vector_int_t *cluster = igraph_vector_int_list_get_ptr(clusters, VECTOR(*membership)[i]);
/* Add vertex i to cluster vector */
IGRAPH_CHECK(igraph_vector_int_push_back(cluster, i));
}
return IGRAPH_SUCCESS;
}
/* Aggregate the graph based on the \p refined membership while setting the
* membership of each aggregated vertex according to the \p membership.
*
* Technically speaking we have that
* aggregated_membership[refined_membership[v]] = membership[v] for each vertex v.
*
* The new aggregated graph is returned in \p aggregated_graph. This graph
* object should not yet be initialized, igraph_create() is called on it, and
* responsibility for destroying the object lies with the calling method
*
* The remaining results, aggregated_edge_weights, aggregate_vertex_weights and
* aggregated_membership are all expected to be initialized.
*
*/
static igraph_error_t leiden_aggregate(
const igraph_t *graph,
const igraph_inclist_t *edges_per_vertex,
const igraph_vector_t *edge_weights,
const igraph_vector_t *vertex_out_weights,
const igraph_vector_t *vertex_in_weights,
const igraph_vector_int_t *membership,
const igraph_vector_int_t *refined_membership,
const igraph_int_t nb_refined_clusters,
igraph_t *aggregated_graph,
igraph_vector_t *aggregated_edge_weights,
igraph_vector_t *aggregated_vertex_out_weights,
igraph_vector_t *aggregated_vertex_in_weights,
igraph_vector_int_t *aggregated_membership) {
const igraph_bool_t directed = (vertex_in_weights != NULL);
igraph_vector_int_t aggregated_edges;
igraph_vector_t edge_weight_to_cluster;
igraph_vector_int_list_t refined_clusters;
igraph_vector_int_t *incident_edges;
igraph_vector_int_t neighbor_clusters;
igraph_bitset_t neighbor_cluster_added;
igraph_int_t c, degree, nb_neigh_clusters;
/* Get refined clusters */
IGRAPH_VECTOR_INT_LIST_INIT_FINALLY(&refined_clusters, nb_refined_clusters);
IGRAPH_CHECK(leiden_get_clusters(refined_membership, &refined_clusters));
/* Initialize new edges */
IGRAPH_VECTOR_INT_INIT_FINALLY(&aggregated_edges, 0);
/* We clear the aggregated edge weights, we will push each new edge weight */
igraph_vector_clear(aggregated_edge_weights);
/* Simply resize the aggregated vertex weights and membership, they can be set directly */
IGRAPH_CHECK(igraph_vector_resize(aggregated_vertex_out_weights, nb_refined_clusters));
if (directed) {
IGRAPH_CHECK(igraph_vector_resize(aggregated_vertex_in_weights, nb_refined_clusters));
}
IGRAPH_CHECK(igraph_vector_int_resize(aggregated_membership, nb_refined_clusters));
IGRAPH_VECTOR_INIT_FINALLY(&edge_weight_to_cluster, nb_refined_clusters);
/* Initialize neighboring cluster */
IGRAPH_BITSET_INIT_FINALLY(&neighbor_cluster_added, nb_refined_clusters);
IGRAPH_VECTOR_INT_INIT_FINALLY(&neighbor_clusters, nb_refined_clusters);
/* Check per cluster */
for (c = 0; c < nb_refined_clusters; c++) {
igraph_vector_int_t* refined_cluster = igraph_vector_int_list_get_ptr(&refined_clusters, c);
igraph_int_t n_c = igraph_vector_int_size(refined_cluster);
igraph_int_t v = -1;
/* Calculate the total edge weight to other clusters */
VECTOR(*aggregated_vertex_out_weights)[c] = 0.0;
if (directed) {
VECTOR(*aggregated_vertex_in_weights)[c] = 0.0;
}
nb_neigh_clusters = 0;
for (igraph_int_t i = 0; i < n_c; i++) {
v = VECTOR(*refined_cluster)[i];
incident_edges = igraph_inclist_get(edges_per_vertex, v);
degree = igraph_vector_int_size(incident_edges);
for (igraph_int_t j = 0; j < degree; j++) {
igraph_int_t e = VECTOR(*incident_edges)[j];
igraph_int_t u = IGRAPH_OTHER(graph, e, v);
igraph_int_t c2 = VECTOR(*refined_membership)[u];
if (c2 > c) {
if (!IGRAPH_BIT_TEST(neighbor_cluster_added, c2)) {
IGRAPH_BIT_SET(neighbor_cluster_added, c2);
VECTOR(neighbor_clusters)[nb_neigh_clusters++] = c2;
}
VECTOR(edge_weight_to_cluster)[c2] += VECTOR(*edge_weights)[e];
}
}
VECTOR(*aggregated_vertex_out_weights)[c] += VECTOR(*vertex_out_weights)[v];
if (directed) {
VECTOR(*aggregated_vertex_in_weights)[c] += VECTOR(*vertex_in_weights)[v];
}
}
/* Add actual edges from this cluster to the other clusters */
for (igraph_int_t i = 0; i < nb_neigh_clusters; i++) {
igraph_int_t c2 = VECTOR(neighbor_clusters)[i];
/* Add edge */
IGRAPH_CHECK(igraph_vector_int_push_back(&aggregated_edges, c));
IGRAPH_CHECK(igraph_vector_int_push_back(&aggregated_edges, c2));
/* Add edge weight */
IGRAPH_CHECK(igraph_vector_push_back(aggregated_edge_weights, VECTOR(edge_weight_to_cluster)[c2]));
VECTOR(edge_weight_to_cluster)[c2] = 0.0;
IGRAPH_BIT_CLEAR(neighbor_cluster_added, c2);
}
VECTOR(*aggregated_membership)[c] = VECTOR(*membership)[v];
}
igraph_vector_int_destroy(&neighbor_clusters);
igraph_bitset_destroy(&neighbor_cluster_added);
igraph_vector_destroy(&edge_weight_to_cluster);
igraph_vector_int_list_destroy(&refined_clusters);
IGRAPH_FINALLY_CLEAN(4);
igraph_destroy(aggregated_graph);
IGRAPH_CHECK(igraph_create(aggregated_graph, &aggregated_edges, nb_refined_clusters,
directed));
igraph_vector_int_destroy(&aggregated_edges);
IGRAPH_FINALLY_CLEAN(1);
return IGRAPH_SUCCESS;
}
/* Calculate the quality of the partition.
*
* The quality is defined as
*
* 1 / 2m sum_ij (A_ij - gamma n_i n_j) d(s_i, s_j)
*
* for undirected graphs and as
*
* 1 / m sum_ij (A_ij - gamma n^out_i n^in_j) d(s_i, s_j)
*
* where m is the total edge weight, A_ij is the weight of edge (i, j), gamma is
* the so-called resolution parameter, n_i is the vertex weight of vertex i, s_i is
* the cluster of vertex i and d(x, y) = 1 if and only if x = y and 0 otherwise.
*
* Note that by setting n_i = k_i the degree of vertex i and dividing gamma by 2m,
* we effectively optimize modularity. By setting n_i = 1 we optimize the
* Constant Potts Model.
*
* This can be represented as a sum over clusters as
*
* 1 / 2m sum_c (e_c - gamma N_c^2)
*
* where e_c = sum_ij A_ij d(s_i, c)d(s_j, c) is the internal edge weight
* in cluster c (or twice this value if undirected) and
* N_c = sum_i n_i d(s_i, c) is the sum of the vertex weights inside cluster c.
* This is how the quality is calculated in practice.
*/
static igraph_error_t leiden_quality(
const igraph_t *graph,
const igraph_vector_t *edge_weights,
const igraph_vector_t *vertex_out_weights,
const igraph_vector_t *vertex_in_weights,
const igraph_vector_int_t *membership,
const igraph_int_t nb_clusters,
const igraph_real_t resolution,
igraph_real_t *quality) {
const igraph_int_t vcount = igraph_vcount(graph);
const igraph_int_t ecount = igraph_ecount(graph);
const igraph_bool_t directed = (vertex_in_weights != NULL);
const igraph_real_t directed_multiplier = directed ? 1.0 : 2.0;
igraph_vector_t cluster_out_weights, cluster_in_weights;
igraph_real_t total_edge_weight = 0.0;
*quality = 0.0;
for (igraph_int_t e=0; e < ecount; e++) {
igraph_int_t from = IGRAPH_FROM(graph, e);
igraph_int_t to = IGRAPH_TO(graph, e);
total_edge_weight += VECTOR(*edge_weights)[e];
/* We add the internal edge weights. */
if (VECTOR(*membership)[from] == VECTOR(*membership)[to]) {
*quality += directed_multiplier * VECTOR(*edge_weights)[e];
}
}
/* Initialize and compute cluster weights. */
IGRAPH_VECTOR_INIT_FINALLY(&cluster_out_weights, vcount);
if (directed) {
IGRAPH_VECTOR_INIT_FINALLY(&cluster_in_weights, vcount);
}
for (igraph_int_t i = 0; i < vcount; i++) {
igraph_int_t c = VECTOR(*membership)[i];
VECTOR(cluster_out_weights)[c] += VECTOR(*vertex_out_weights)[i];
if (directed) {
VECTOR(cluster_in_weights)[c] += VECTOR(*vertex_in_weights)[i];
}
}
/* We subtract gamma * N^out_c * N^in_c */
for (igraph_int_t c = 0; c < nb_clusters; c++) {
if (directed) {
*quality -= resolution * VECTOR(cluster_out_weights)[c] * VECTOR(cluster_in_weights)[c];
} else {
*quality -= resolution * VECTOR(cluster_out_weights)[c] * VECTOR(cluster_out_weights)[c];
}
}
if (directed) {
igraph_vector_destroy(&cluster_in_weights);
IGRAPH_FINALLY_CLEAN(1);
}
igraph_vector_destroy(&cluster_out_weights);
IGRAPH_FINALLY_CLEAN(1);
/* We normalise by m or 2m depending on directedness */
*quality /= (directed_multiplier * total_edge_weight);
return IGRAPH_SUCCESS;
}
/* This is the core of the Leiden algorithm and relies on subroutines to
* perform the three different phases: (1) local moving of vertices, (2)
* refinement of the partition and (3) aggregation of the network based on the
* refined partition, using the non-refined partition to create an initial
* partition for the aggregate network.
*/
static igraph_error_t community_leiden(
const igraph_t *graph,
igraph_vector_t *edge_weights,
igraph_vector_t *vertex_out_weights,
igraph_vector_t *vertex_in_weights,
igraph_real_t resolution,
igraph_real_t beta,
igraph_vector_int_t *membership,
igraph_int_t *nb_clusters,
igraph_real_t *quality,
igraph_bool_t *changed) {
const igraph_int_t n = igraph_vcount(graph);
const igraph_bool_t directed = (vertex_in_weights != NULL);
igraph_int_t nb_refined_clusters;
igraph_int_t i, c;
igraph_t aggregated_graph, *i_graph;
igraph_vector_t aggregated_edge_weights;
igraph_vector_t aggregated_vertex_out_weights, aggregated_vertex_in_weights;
igraph_vector_int_t aggregated_membership;
igraph_vector_t *i_edge_weights;
igraph_vector_t *i_vertex_out_weights, *i_vertex_in_weights;
igraph_vector_int_t *i_membership;
igraph_vector_t tmp_edge_weights, tmp_vertex_out_weights, tmp_vertex_in_weights;
igraph_vector_int_t tmp_membership;
igraph_vector_int_t refined_membership;
igraph_vector_int_t aggregate_vertex;
igraph_vector_int_list_t clusters;
igraph_inclist_t edges_per_vertex;
igraph_bool_t continue_clustering;
igraph_int_t level = 0;
/* Initialize temporary weights and membership to be used in aggregation */
IGRAPH_VECTOR_INIT_FINALLY(&tmp_edge_weights, 0);
IGRAPH_VECTOR_INIT_FINALLY(&tmp_vertex_out_weights, 0);
if (directed) {
IGRAPH_VECTOR_INIT_FINALLY(&tmp_vertex_in_weights, 0);
}
IGRAPH_VECTOR_INT_INIT_FINALLY(&tmp_membership, 0);
/* Initialize clusters */
IGRAPH_VECTOR_INT_LIST_INIT_FINALLY(&clusters, n);
/* Initialize aggregate vertices, which initially is identical to simply the
* vertices in the graph. */
IGRAPH_CHECK(igraph_vector_int_init_range(&aggregate_vertex, 0, n));
IGRAPH_FINALLY(igraph_vector_int_destroy, &aggregate_vertex);
/* Initialize refined membership */
IGRAPH_VECTOR_INT_INIT_FINALLY(&refined_membership, 0);
/* Initialize aggregated graph */
IGRAPH_CHECK(igraph_empty(&aggregated_graph, 0, directed));
IGRAPH_FINALLY(igraph_destroy, &aggregated_graph);
/* Initialize aggregated edge weights */
IGRAPH_VECTOR_INIT_FINALLY(&aggregated_edge_weights, 0);
/* Initialize aggregated vertex weights */
IGRAPH_VECTOR_INIT_FINALLY(&aggregated_vertex_out_weights, 0);
if (directed) {
IGRAPH_VECTOR_INIT_FINALLY(&aggregated_vertex_in_weights, 0);
}
/* Initialize aggregated membership */
IGRAPH_VECTOR_INT_INIT_FINALLY(&aggregated_membership, 0);
/* Set actual graph, weights and membership to be used. */
i_graph = (igraph_t*)graph;
i_edge_weights = edge_weights;
i_vertex_out_weights = vertex_out_weights;
i_vertex_in_weights = directed ? vertex_in_weights : NULL;
i_membership = membership;
/* Clean membership: ensure that cluster indices are 0 <= c < n. */
IGRAPH_CHECK(igraph_reindex_membership(i_membership, NULL, nb_clusters));
/* We start out with no changes, whenever a vertex is moved, this will be set to true. */
*changed = false;
do {
/* Get incidence list for fast iteration */
IGRAPH_CHECK(igraph_inclist_init( i_graph, &edges_per_vertex, IGRAPH_ALL, IGRAPH_LOOPS_TWICE));
IGRAPH_FINALLY(igraph_inclist_destroy, &edges_per_vertex);
/* Move around the vertices in order to increase the quality */
IGRAPH_CHECK(leiden_fastmove_vertices(i_graph,
&edges_per_vertex,
i_edge_weights,
i_vertex_out_weights, i_vertex_in_weights,
resolution,
nb_clusters,
i_membership,
changed));
/* We only continue clustering if not all clusters are represented by a
* single vertex yet
*/
continue_clustering = (*nb_clusters < igraph_vcount(i_graph));
if (continue_clustering) {
/* Set original membership */
if (level > 0) {
for (i = 0; i < n; i++) {
igraph_int_t v_aggregate = VECTOR(aggregate_vertex)[i];
VECTOR(*membership)[i] = VECTOR(*i_membership)[v_aggregate];
}
}
/* Get vertex sets for each cluster. */
IGRAPH_CHECK(leiden_get_clusters(i_membership, &clusters));
/* Ensure refined membership is correct size */
IGRAPH_CHECK(igraph_vector_int_resize(&refined_membership, igraph_vcount(i_graph)));
/* Refine each cluster */
nb_refined_clusters = 0;
for (c = 0; c < *nb_clusters; c++) {
igraph_vector_int_t* cluster = igraph_vector_int_list_get_ptr(&clusters, c);
IGRAPH_CHECK(leiden_merge_vertices(i_graph,
&edges_per_vertex,
i_edge_weights,
i_vertex_out_weights, i_vertex_in_weights,
cluster, i_membership, c,
resolution, beta,
&nb_refined_clusters, &refined_membership));
/* Empty cluster */
igraph_vector_int_clear(cluster);
}
/* If refinement didn't aggregate anything, we aggregate on the basis of
* the actual clustering */
if (nb_refined_clusters >= igraph_vcount(i_graph)) {
IGRAPH_CHECK(igraph_vector_int_update(&refined_membership, i_membership));
nb_refined_clusters = *nb_clusters;
}
/* Keep track of aggregate vertex. */
for (i = 0; i < n; i++) {
/* Current aggregate vertex */
igraph_int_t v_aggregate = VECTOR(aggregate_vertex)[i];
/* New aggregate vertex */
VECTOR(aggregate_vertex)[i] = VECTOR(refined_membership)[v_aggregate];
}
IGRAPH_CHECK(leiden_aggregate(
i_graph,
&edges_per_vertex,
i_edge_weights,
i_vertex_out_weights, i_vertex_in_weights,
i_membership, &refined_membership, nb_refined_clusters,
&aggregated_graph,
&tmp_edge_weights,
&tmp_vertex_out_weights, directed ? &tmp_vertex_in_weights : NULL,
&tmp_membership));
/* On the lowest level, the actual graph and vertex and edge weights and
* membership are used. On higher levels, we will use the aggregated graph
* and associated vectors.
*/
if (level == 0) {
/* Set actual graph, weights and membership to be used. */
i_graph = &aggregated_graph;
i_edge_weights = &aggregated_edge_weights;
i_vertex_out_weights = &aggregated_vertex_out_weights;
if (directed) {
i_vertex_in_weights = &aggregated_vertex_in_weights;
}
i_membership = &aggregated_membership;
}
/* Update the aggregated administration. */
IGRAPH_CHECK(igraph_vector_update(i_edge_weights, &tmp_edge_weights));
IGRAPH_CHECK(igraph_vector_update(i_vertex_out_weights, &tmp_vertex_out_weights));
if (directed) {
IGRAPH_CHECK(igraph_vector_update(i_vertex_in_weights, &tmp_vertex_in_weights));
}
IGRAPH_CHECK(igraph_vector_int_update(i_membership, &tmp_membership));
level += 1;
}
/* We are done iterating, so we destroy the incidence list */
igraph_inclist_destroy(&edges_per_vertex);
IGRAPH_FINALLY_CLEAN(1);
} while (continue_clustering);
/* Free aggregated graph and associated vectors */
igraph_vector_int_destroy(&aggregated_membership);
if (directed) igraph_vector_destroy(&aggregated_vertex_in_weights);
igraph_vector_destroy(&aggregated_vertex_out_weights);
igraph_vector_destroy(&aggregated_edge_weights);
igraph_destroy(&aggregated_graph);
/* Free remaining memory */
igraph_vector_int_destroy(&refined_membership);
igraph_vector_int_destroy(&aggregate_vertex);
igraph_vector_int_list_destroy(&clusters);
igraph_vector_int_destroy(&tmp_membership);
igraph_vector_destroy(&tmp_vertex_out_weights);
if (directed) igraph_vector_destroy(&tmp_vertex_in_weights);
igraph_vector_destroy(&tmp_edge_weights);
if (directed) {
IGRAPH_FINALLY_CLEAN(12);
} else {
IGRAPH_FINALLY_CLEAN(10);
}
/* Calculate quality */
if (quality) {
IGRAPH_CHECK(leiden_quality(graph,
edge_weights, vertex_out_weights, vertex_in_weights,
membership,
*nb_clusters, resolution,
quality));
}
return IGRAPH_SUCCESS;
}
/**
* \ingroup communities
* \function igraph_community_leiden
* \brief Finding community structure using the Leiden algorithm.
*
* This function implements the Leiden algorithm for finding community
* structure.
*
* </para><para>
* It is similar to the multilevel algorithm, often called the Louvain
* algorithm, but it is faster and yields higher quality solutions. It can
* optimize both modularity and the Constant Potts Model, which does not suffer
* from the resolution-limit (see Traag, Van Dooren &amp; Nesterov).
*
* </para><para>
* The Leiden algorithm consists of three phases: (1) local moving of vertices, (2)
* refinement of the partition and (3) aggregation of the network based on the
* refined partition, using the non-refined partition to create an initial
* partition for the aggregate network. In the local move procedure in the
* Leiden algorithm, only vertices whose neighborhood has changed are visited. Only
* moves that strictly improve the quality function are made. The refinement is
* done by restarting from a singleton partition within each cluster and
* gradually merging the subclusters. When aggregating, a single cluster may
* then be represented by several vertices (which are the subclusters identified in
* the refinement).
*
* </para><para>
* The Leiden algorithm provides several guarantees. The Leiden algorithm is
* typically iterated: the output of one iteration is used as the input for the
* next iteration. At each iteration all clusters are guaranteed to be (weakly)
* connected and well-separated. After an iteration in which nothing has
* changed, all vertices and some parts are guaranteed to be locally optimally
* assigned. Note that even if a single iteration did not result in any change,
* it is still possible that a subsequent iteration might find some
* improvement. Each iteration explores different subsets of vertices to consider
* for moving from one cluster to another. Finally, asymptotically, all subsets
* of all clusters are guaranteed to be locally optimally assigned. For more
* details, please see Traag, Waltman &amp; van Eck (2019).
*
* </para><para>
* The objective function being optimized is
*
* </para><para>
* <code>1 / 2m sum_ij (A_ij - γ n_i n_j) δ(s_i, s_j)</code>
*
* </para><para>
* in the undirected case and
*
* </para><para>
* <code>1 / m sum_ij (A_ij - γ n^out_i n^in_j) δ(s_i, s_j)</code>
*
* </para><para>
* in the directed case.
* Here \c m is the total edge weight, <code>A_ij</code> is the weight of edge
* (i, j), \c γ is the so-called resolution parameter, <code>n_i</code>
* is the vertex weight of vertex \c i (separate out- and in-weights are used
* with directed graphs), <code>s_i</code> is the cluster of vertex
* \c i and <code>δ(x, y) = 1</code> if and only if <code>x = y</code> and 0
* otherwise.
*
* </para><para>
* By setting <code>n_i = k_i</code>, the degree of vertex \c i, and
* dividing \c γ by <code>2m</code> (by \c m in the directed case), we effectively
* obtain an expression for modularity. Hence, the standard modularity will be
* optimized when you supply the degrees (out- and in-degrees with directed graphs)
* as the vertex weights and by supplying as a resolution parameter
* <code>1/(2m)</code> (<code>1/m</code> with directed graphs).
* Use the \ref igraph_community_leiden_simple() convenience function to
* compute vertex weights automatically for modularity maximization.
*
* </para><para>
* References:
*
* </para><para>
* V. A. Traag, L. Waltman, N. J. van Eck:
* From Louvain to Leiden: guaranteeing well-connected communities.
* Scientific Reports, 9(1), 5233 (2019).
* http://dx.doi.org/10.1038/s41598-019-41695-z
*
* </para><para>
* V. A. Traag, P. Van Dooren, and Y. Nesterov:
* Narrow scope for resolution-limit-free community detection.
* Phys. Rev. E 84, 016114 (2011).
* https://doi.org/10.1103/PhysRevE.84.016114
*
* \param graph The input graph.
* \param edge_weights Numeric vector containing edge weights. If \c NULL,
* every edge has equal weight of 1. The weights need not be non-negative.
* \param vertex_out_weights Numeric vector containing vertex weights, or vertex
* out-weights for directed graphs. If \c NULL, every vertex has equal
* weight of 1.
* \param vertex_in_weights Numeric vector containing vertex in-weights for
* directed graphs. If set to \c NULL, in-weights are assumed to be the same
* as out-weights, which effectively ignores edge directions.
* Must be \c NULL for undirected graphs.
* \param n_iterations Iterate the core Leiden algorithm the indicated number
* of times. If this is a negative number, it will continue iterating until
* an iteration did not change the clustering. Two iterations are often
* sufficient, thus 2 is a reasonable default.
* \param beta The randomness used in the refinement step when merging. A small
* amount of randomness (\c beta = 0.01) typically works well.
* \param start Start from membership vector. If this is true, the optimization
* will start from the provided membership vector. If this is false, the
* optimization will start from a singleton partition.
* \param n_iterations Iterate the core Leiden algorithm for the indicated number
* of times. If this is a negative number, it will continue iterating until
* an iteration did not change the clustering.
* \param membership The membership vector. This is both used as the initial
* membership from which optimisation starts and is updated in place. It
* must hence be properly initialized. When finding clusters from scratch it
* is typically started using a singleton clustering. This can be achieved
* using \ref igraph_vector_int_init_range().
* \param nb_clusters The number of clusters contained in the final \p membership.
* If \c NULL, the number of clusters will not be returned.
* \param quality The quality of the partition, in terms of the objective
* function as included in the documentation. If \c NULL the quality will
* not be calculated.
* \return Error code.
*
* Time complexity: near linear on sparse graphs.
*
* \sa \ref igraph_community_leiden_simple() for a simplified interface
* that allows specifying an objective function directly and does not require
* vertex weights.
*
* \example examples/simple/igraph_community_leiden.c
*/
igraph_error_t igraph_community_leiden(
const igraph_t *graph,
const igraph_vector_t *edge_weights,
const igraph_vector_t *vertex_out_weights,
const igraph_vector_t *vertex_in_weights,
igraph_real_t resolution,
igraph_real_t beta,
igraph_bool_t start,
igraph_int_t n_iterations,
igraph_vector_int_t *membership,
igraph_int_t *nb_clusters,
igraph_real_t *quality) {
const igraph_int_t vcount = igraph_vcount(graph);
const igraph_int_t ecount = igraph_ecount(graph);
const igraph_bool_t directed = igraph_is_directed(graph);
igraph_vector_t *i_edge_weights, *i_vertex_out_weights, *i_vertex_in_weights;
igraph_int_t i_nb_clusters;
if (!nb_clusters) {
nb_clusters = &i_nb_clusters;
}
if (start) {
if (!membership) {
IGRAPH_ERROR("Cannot start optimization if membership is missing.", IGRAPH_EINVAL);
}
if (igraph_vector_int_size(membership) != vcount) {
IGRAPH_ERROR("Membership vector length does not equal the number of vertices.", IGRAPH_EINVAL);
}
} else {
if (!membership)
IGRAPH_ERROR("Membership vector should be supplied and initialized, "
"even when not starting optimization from it.", IGRAPH_EINVAL);
IGRAPH_CHECK(igraph_vector_int_range(membership, 0, vcount));
}
/* Check edge weights to possibly use default. */
if (!edge_weights) {
i_edge_weights = IGRAPH_CALLOC(1, igraph_vector_t);
IGRAPH_CHECK_OOM(i_edge_weights, "Leiden algorithm failed, could not allocate memory for edge weights.");
IGRAPH_FINALLY(igraph_free, i_edge_weights);
IGRAPH_CHECK(igraph_vector_init(i_edge_weights, igraph_ecount(graph)));
IGRAPH_FINALLY(igraph_vector_destroy, i_edge_weights);
igraph_vector_fill(i_edge_weights, 1);
} else {
if (igraph_vector_size(edge_weights) != ecount) {
IGRAPH_ERRORF("Edge weight vector length (%" IGRAPH_PRId ") does not match number of edges (%" IGRAPH_PRId ").",
IGRAPH_EINVAL, igraph_vector_size(edge_weights), ecount);
}
i_edge_weights = (igraph_vector_t*)edge_weights;
}
/* Check vertex out-weights to possibly use default. */
if (!vertex_out_weights) {
i_vertex_out_weights = IGRAPH_CALLOC(1, igraph_vector_t);
IGRAPH_CHECK_OOM(i_vertex_out_weights, "Leiden algorithm failed, could not allocate memory for vertex weights.");
IGRAPH_FINALLY(igraph_free, i_vertex_out_weights);
IGRAPH_VECTOR_INIT_FINALLY(i_vertex_out_weights, vcount);
igraph_vector_fill(i_vertex_out_weights, 1);
} else {
if (igraph_vector_size(vertex_out_weights) != vcount) {
IGRAPH_ERRORF("Vertex %sweight vector length (%" IGRAPH_PRId ") does not match number of vertices (%" IGRAPH_PRId ").",
IGRAPH_EINVAL,
directed ? "out-" : "",
igraph_vector_size(vertex_out_weights), vcount);
}
i_vertex_out_weights = (igraph_vector_t*)vertex_out_weights;
}
if (directed) {
/* When in-weights are not given for a directed graph,
* assume that they are the same as the out-weights.
* This effectively ignores edge directions. */
if (vertex_in_weights) {
if (igraph_vector_size(vertex_in_weights) != vcount) {
IGRAPH_ERRORF("Vertex in-weight vector length (%" IGRAPH_PRId ") does not match number of vertices (%" IGRAPH_PRId ").",
IGRAPH_EINVAL,
igraph_vector_size(vertex_in_weights), vcount);
}
i_vertex_in_weights = (igraph_vector_t*)vertex_in_weights;
} else {
i_vertex_in_weights = i_vertex_out_weights;
}
} else {
/* In-weights must be NULL in the undirected case. */
if (vertex_in_weights) {
IGRAPH_ERROR("Vertex in-weights must not be given for undirected graphs.", IGRAPH_EINVAL);
} else {
i_vertex_in_weights = NULL;
}
}
/* Perform actual Leiden algorithm iteratively. We either
* perform a fixed number of iterations, or we perform
* iterations until the quality remains unchanged. Even if
* a single iteration did not change anything, a subsequent
* iteration may still find some improvement. This is because
* each iteration explores different subsets of vertices.
*/
igraph_bool_t changed = true;
for (igraph_int_t itr = 0;
n_iterations < 0 ? changed : itr < n_iterations;
itr++) {
IGRAPH_CHECK(community_leiden(graph,
i_edge_weights, i_vertex_out_weights, i_vertex_in_weights,
resolution, beta,
membership, nb_clusters, quality, &changed));
}
if (!edge_weights) {
igraph_vector_destroy(i_edge_weights);
IGRAPH_FREE(i_edge_weights);
IGRAPH_FINALLY_CLEAN(2);
}
if (!vertex_out_weights) {
igraph_vector_destroy(i_vertex_out_weights);
IGRAPH_FREE(i_vertex_out_weights);
IGRAPH_FINALLY_CLEAN(2);
}
return IGRAPH_SUCCESS;
}
/**
* \function igraph_community_leiden_simple
* \brief Finding community structure using the Leiden algorithm, simple interface.
*
* This is a simplified interface to \ref igraph_community_leiden() for
* convenience purposes. Instead of requiring vertex weights, it allows
* choosing from a set of objective functions to maximize. It implements
* these objective functions by passing suitable vertex weights to
* \ref igraph_community_leiden(), as explained in the documentation of
* that function.
*
* \param graph The input graph. May be directed or undirected.
* \param weights The edge weights. If \c NULL, all weights are assumed to be 1.
* \param objective The objective function to maximize.
* \clist
* \cli IGRAPH_LEIDEN_OBJECTIVE_MODULARITY
* Use the generalized modularity, defined as
* <code>Q = 1/(2m) sum_ij (A_ij - γ k_i k_j / (2m)) δ(c_i, c_j)</code>
* for undirected graphs and as
* <code>Q = 1/m sum_ij (A_ij - γ k^out_i k^in_j / m) δ(c_i, c_j)</code>
* for directed graphs. This effectively uses a multigraph configuration
* model as the null model. Edge weights must not be negative.
* \cli IGRAPH_LEIDEN_OBJECTIVE_CPM
* Use the constant Potts model, whose objective function is defined as
* <code>Q = 1/(2m) sum_ij (A_ij - γ) δ(c_i, c_j)</code>
* for undirected graphs and as
* <code>Q = 1/m sum_ij (A_ij - γ) δ(c_i, c_j)</code>
* for directed graphs. Edge weights are allowed to be negative.
* Edge directions have no impact on the result.
* \cli IGRAPH_LEIDEN_OBJECTIVE_ER
* Use an objective function based on the multigraph Erdős-Rényi G(n,p)
* null model, defined as
* <code>Q = 1/(2m) sum_ij (A_ij - γ p) δ(c_i, c_j)</code>
* for undirected graphs and as
* <code>Q = 1/m sum_ij (A_ij - γ p) δ(c_i, c_j)</code>
* for directed graphs. \c p is the weighted density, i.e. the average
* link strength between all vertex pairs (whether adjacent or not).
* Edge weights must not be negative. Edge directions have no impact on
* the result.
* \endclist
* In the above formulas, \c A is the adjacency matrix, \c m is the total
* edge weight, \c k are the (out- and in-) degrees, \c γ is the resolution
* parameter, and <code>δ(c_i, c_j)</code> is 1 if vertices \c i and \c j
* are in the same community and 0 otherwise. Edge directions are only
* relevant with \c IGRAPH_LEIDEN_OBJECTIVE_MODULARITY. The other two
* objective functions are equivalent between directed and undirected graphs:
* the formal difference is due to each edge being included twice in
* undirected (symmetric) adjacency matrices.
* \param resolution The resolution parameter, which is represented by γ in
* the objective functions detailed above.
* \param beta The randomness used in the refinement step when merging. A small
* amount of randomness (\c beta = 0.01) typically works well.
* \param start Start from membership vector. If this is true, the optimization
* will start from the provided membership vector. If this is false, the
* optimization will start from a singleton partition.
* \param n_iterations Iterate the core Leiden algorithm the indicated number
* of times. If this is a negative number, it will continue iterating until
* an iteration did not change the clustering. Two iterations are often
* sufficient, thus 2 is a reasonable default.
* \param membership The membership vector. If \p start is set to \c false,
* it will be resized appropriately. If \p start is \c true, it must be
* a valid membership vector for the given \p graph.
* \param nb_clusters The number of clusters contained in the final \p membership.
* If \c NULL, the number of clusters will not be returned.
* \param quality The quality of the partition, in terms of the objective
* function selected by \p objective. If \c NULL the quality will
* not be calculated.
* \return Error code.
*
* Time complexity: near linear on sparse graphs.
*
* \sa \ref igraph_community_leiden() for a more flexible interface that
* allows specifying raw vertex weights.
*/
igraph_error_t igraph_community_leiden_simple(
const igraph_t *graph,
const igraph_vector_t *weights,
igraph_leiden_objective_t objective,
igraph_real_t resolution,
igraph_real_t beta,
igraph_bool_t start,
igraph_int_t n_iterations,
igraph_vector_int_t *membership,
igraph_int_t *nb_clusters,
igraph_real_t *quality) {
const igraph_int_t vcount = igraph_vcount(graph);
const igraph_int_t ecount = igraph_ecount(graph);
const igraph_bool_t directed = igraph_is_directed(graph);
igraph_vector_t vertex_out_weights, vertex_in_weights;
igraph_vector_int_t i_membership, *p_membership;
igraph_real_t min_weight = IGRAPH_INFINITY;
/* Basic weight vector validation, calculate properties used for validation steps
* specific to different objective functions. */
if (weights) {
if (igraph_vector_size(weights) != ecount) {
IGRAPH_ERROR("Edge weight vector length does not match number of edges.", IGRAPH_EINVAL);
}
for (igraph_int_t i=0; i < ecount; i++) {
igraph_real_t w = VECTOR(*weights)[i];
if (w < min_weight) {
min_weight = w;
}
if (! isfinite(w)) {
IGRAPH_ERRORF("Edge weights must not be infinite or NaN, got %g.",
IGRAPH_EINVAL, w);
}
}
}
IGRAPH_VECTOR_INIT_FINALLY(&vertex_out_weights, vcount);
if (directed) {
IGRAPH_VECTOR_INIT_FINALLY(&vertex_in_weights, vcount);
}
/* igraph_community_leiden() always requires an initialized membership vector
* of the correct size to be given. We relax this requirement to the case
* when start = true. */
if (start) {
if (!membership) {
IGRAPH_ERROR("Requesting to start the computation from a specific "
"community assignment, but no membership vector given.",
IGRAPH_EINVAL);
}
if (igraph_vector_int_size(membership) != vcount) {
IGRAPH_ERRORF("Requesting to start the computation from a specific "
"community assignment, but the given membership vector "
"has a different size (%" IGRAPH_PRId " than the vertex "
"count (%" IGRAPH_PRId ").",
IGRAPH_EINVAL,
igraph_vector_int_size(membership), vcount);
}
p_membership = membership;
} else {
if (!membership) {
IGRAPH_VECTOR_INT_INIT_FINALLY(&i_membership, vcount);
p_membership = &i_membership;
} else {
IGRAPH_CHECK(igraph_vector_int_resize(membership, vcount));
p_membership = membership;
}
}
switch (objective) {
case IGRAPH_LEIDEN_OBJECTIVE_MODULARITY:
if (min_weight < 0) {
IGRAPH_ERRORF("Edge weights must not be negative for Leiden community "
"detection with modularity objective function, got %g.",
IGRAPH_EINVAL,
min_weight);
}
IGRAPH_CHECK(igraph_strength(
graph, &vertex_out_weights,
igraph_vss_all(), IGRAPH_OUT, IGRAPH_LOOPS, weights));
if (directed) {
IGRAPH_CHECK(igraph_strength(
graph, &vertex_in_weights,
igraph_vss_all(), IGRAPH_IN, IGRAPH_LOOPS, weights));
}
/* If directed, the sum of vertex_out_weights is the total edge weight.
* If undirected, it is twice the total edge weight. */
resolution /= igraph_vector_sum(&vertex_out_weights);
break;
case IGRAPH_LEIDEN_OBJECTIVE_CPM:
/* TODO: Potential minor optimization is to use the same vector for both. */
igraph_vector_fill(&vertex_out_weights, 1);
if (directed) {
igraph_vector_fill(&vertex_in_weights, 1);
}
break;
case IGRAPH_LEIDEN_OBJECTIVE_ER:
if (min_weight < 0) {
IGRAPH_ERRORF("Edge weights must not be negative for Leiden community "
"detection with ER objective function, got %g.",
IGRAPH_EINVAL,
min_weight);
}
/* TODO: Potential minor optimization is to use the same vector for both. */
igraph_vector_fill(&vertex_out_weights, 1);
if (directed) {
igraph_vector_fill(&vertex_in_weights, 1);
}
{
igraph_real_t p;
/* Note: Loops must be allowed, as the aggregation step of the
* algorithm effectively creates them. */
IGRAPH_CHECK(igraph_density(graph, weights, &p, /* loops */ true));
resolution *= p;
}
break;
default:
IGRAPH_ERROR("Invalid objective function for Leiden community detection.",
IGRAPH_EINVAL);
}
IGRAPH_CHECK(igraph_community_leiden(
graph, weights,
&vertex_out_weights, directed ? &vertex_in_weights : NULL,
resolution, beta, start, n_iterations, p_membership, nb_clusters, quality));
if (!membership) {
igraph_vector_int_destroy(&i_membership);
IGRAPH_FINALLY_CLEAN(1);
}
if (directed) {
igraph_vector_destroy(&vertex_in_weights);
IGRAPH_FINALLY_CLEAN(1);
}
igraph_vector_destroy(&vertex_out_weights);
IGRAPH_FINALLY_CLEAN(1);
return IGRAPH_SUCCESS;
}