#include #include #include #include #include #include #include #include #ifdef _OPENMP #include #else #warning "OpenMP is not available: cpp_louvain_communities will fall back to single-threaded execution." #endif #include "../../classes/graph.h" #include "../../common/utils.h" #include "../../classes/linkgraph.h" #include "../../functions/community/graph_coloring.h" namespace py = pybind11; using namespace std; const double epsilon = 1e-10; const int MAX_ITERATIONS = 50; const double CONVERGENCE_THRESHOLD = 1e-7; struct CommunityState { int size = 0; double weight_inside = 0; double weight_all = 0; }; double calculate_norm(const Graph_L& G) { double total_weight = 0.0; for (int u = 1; u < G.n + 1; ++u) { for (int i = G.head[u]; i != -1; i = G.edges[i].next) { total_weight += G.edges[i].w; } } return total_weight; } double calculate_actual_modularity(const Graph_L& G, const vector& membership, double norm, double resolution) { unordered_map comm_weight_inside; unordered_map comm_weight_total; for (int u = 1; u <= G.n; ++u) { int comm_u = membership[u - 1]; for (int e_idx = G.head[u]; e_idx != -1; e_idx = G.edges[e_idx].next) { int v = G.edges[e_idx].to; double w = G.edges[e_idx].w; int comm_v = membership[v - 1]; comm_weight_total[comm_u] += w; if (comm_u == comm_v) { comm_weight_inside[comm_u] += w; } } } double modularity = 0.0; for (const auto& kv : comm_weight_inside) { int comm = kv.first; double inside = kv.second; double total = comm_weight_total[comm]; modularity += (inside / 2.0) - resolution * (total * total) / (4.0 * norm * norm); } return modularity; } inline double calculate_modularity_gain(double comm_weight_without_u, double norm, double node_weight_all, double weight_to_comm, double resolution) { double gain = weight_to_comm - resolution * (comm_weight_without_u * node_weight_all) / norm; return 2 * gain / norm; } double modularity_optimization_serial(const Graph_L& G, vector& membership, double resolution_val, double norm) { int n = G.n; double total_modularity_gain = 0.0; vector comms(n); for (int i = 0; i < n; ++i) { membership[i] = i; comms[i].size = 1; comms[i].weight_all = 0.0; comms[i].weight_inside = 0.0; for (int j = G.head[i + 1]; j != -1; j = G.edges[j].next) { int v = G.edges[j].to; double w = G.edges[j].w; comms[i].weight_all += w; if (v == i + 1) comms[i].weight_inside += w; } } vector node_weight(n + 1, 0.0); vector node_loop(n + 1, 0.0); for (int i = 0; i < n; ++i) { node_weight[i + 1] = comms[i].weight_all; node_loop[i + 1] = comms[i].weight_inside; } vector neigh_weight(n, 0.0); vector timestamp(n, 0); vector touched; touched.reserve(512); int version = 0; std::random_device rd; std::mt19937 rng(rd()); vector node_order(n); for (int i = 0; i < n; ++i) node_order[i] = i + 1; int num_moved_nodes = 1; int iteration = 0; double previous_modularity = -1e100; do { num_moved_nodes = 0; iteration++; std::shuffle(node_order.begin(), node_order.end(), rng); for (int idx = 0; idx < n; ++idx) { int u = node_order[idx]; touched.clear(); version++; if (version == 0) version = 1; double weight_all = node_weight[u]; double weight_loop = node_loop[u]; double weight_inside = 0.0; int my_comm = membership[u - 1]; for (int e = G.head[u]; e != -1; e = G.edges[e].next) { int v = G.edges[e].to; double w = G.edges[e].w; if (u == v) continue; int v_comm = membership[v - 1]; if (timestamp[v_comm] != version) { timestamp[v_comm] = version; neigh_weight[v_comm] = 0.0; touched.push_back(v_comm); } neigh_weight[v_comm] += w; if (v_comm == my_comm) { weight_inside += w; } } if (weight_all < epsilon) continue; int old_comm = my_comm; double comm_old_w = comms[old_comm].weight_all - weight_all; double old_gain = calculate_modularity_gain(comm_old_w, norm, weight_all, weight_inside, resolution_val); double max_gain = std::max(old_gain, 0.0); int best_comm = old_comm; double best_weight_to_comm = weight_inside; for (int target_comm : touched) { if (target_comm == old_comm) continue; double w_to_target = neigh_weight[target_comm]; double comm_target_w = comms[target_comm].weight_all; double gain = calculate_modularity_gain(comm_target_w, norm, weight_all, w_to_target, resolution_val); if (gain > max_gain + epsilon) { max_gain = gain; best_comm = target_comm; best_weight_to_comm = w_to_target; } } if (best_comm != old_comm) { comms[old_comm].size--; comms[old_comm].weight_all -= weight_all; comms[old_comm].weight_inside -= (2.0 * weight_inside + weight_loop); comms[best_comm].size++; comms[best_comm].weight_all += weight_all; comms[best_comm].weight_inside += (2.0 * best_weight_to_comm + weight_loop); membership[u - 1] = best_comm; num_moved_nodes++; } } if (num_moved_nodes == 0) { return calculate_actual_modularity(G, membership, norm, resolution_val); } if (iteration >= MAX_ITERATIONS) { return calculate_actual_modularity(G, membership, norm, resolution_val); } } while (true); for (int i = 0; i < n; ++i) { comms[i].weight_all = node_weight[i + 1]; } return calculate_actual_modularity(G, membership, norm, resolution_val); } double modularity_optimization_parallel_simplified(const Graph_L& G, vector& membership, double resolution_val, double norm) { int n = G.n; vector comms(n); for (int i = 0; i < n; ++i) { membership[i] = i; comms[i].size = 1; comms[i].weight_all = 0.0; comms[i].weight_inside = 0.0; for (int j = G.head[i + 1]; j != -1; j = G.edges[j].next) { int v = G.edges[j].to; double w = G.edges[j].w; comms[i].weight_all += w; if (v == i + 1) comms[i].weight_inside += w; } } vector node_weight(n + 1, 0.0); vector node_loop(n + 1, 0.0); for (int i = 0; i < n; ++i) { node_weight[i + 1] = comms[i].weight_all; node_loop[i + 1] = comms[i].weight_inside; } std::random_device rd; std::mt19937 rng(rd()); vector node_order(n); iota(node_order.begin(), node_order.end(), 0); int num_moved_nodes = 1; int iteration = 0; vector comm_weights(n, 0.0); for (int i = 0; i < n; ++i) { comm_weights[i] = comms[i].weight_all; } do { num_moved_nodes = 0; iteration++; std::shuffle(node_order.begin(), node_order.end(), rng); #pragma omp parallel { #pragma omp for reduction(+:num_moved_nodes) schedule(guided) for (int idx = 0; idx < n; ++idx) { int u = node_order[idx]; double weight_all = node_weight[u]; int my_comm = membership[u - 1]; if (weight_all < epsilon) continue; unordered_map neigh_weight; neigh_weight.reserve(32); vector touched; touched.reserve(32); double weight_inside = 0.0; for (int e = G.head[u]; e != -1; e = G.edges[e].next) { int v = G.edges[e].to; double w = G.edges[e].w; if (u == v) continue; int v_comm = membership[v - 1]; auto it = neigh_weight.find(v_comm); if (it == neigh_weight.end()) { touched.push_back(v_comm); neigh_weight[v_comm] = w; } else { it->second += w; } if (v_comm == my_comm) { weight_inside += w; } } int old_comm = my_comm; double comm_old_w = comm_weights[old_comm] - weight_all; double old_gain = calculate_modularity_gain(comm_old_w, norm, weight_all, weight_inside, resolution_val); double max_gain = std::max(old_gain, 0.0); int best_comm = old_comm; for (int target_comm : touched) { if (target_comm == old_comm) continue; double w_to_target = neigh_weight.at(target_comm); double comm_target_w = comm_weights[target_comm]; double gain = calculate_modularity_gain(comm_target_w, norm, weight_all, w_to_target, resolution_val); if (gain > max_gain + epsilon) { max_gain = gain; best_comm = target_comm; } } if (best_comm != old_comm) { #pragma omp atomic comm_weights[old_comm] -= weight_all; #pragma omp atomic comm_weights[best_comm] += weight_all; membership[u - 1] = best_comm; num_moved_nodes++; } } } if (num_moved_nodes == 0) break; if (iteration >= MAX_ITERATIONS) break; } while (true); for (int i = 0; i < n; ++i) { comms[i].weight_all = comm_weights[i]; } return calculate_actual_modularity(G, membership, norm, resolution_val); } struct SuperEdge { int u, v; double w; }; Graph_L* graph_compress(const Graph_L* G, vector& membership) { unordered_map new_comm_id; int new_vcount = 0; for (int i = 0; i < G->n; i++) { int c = membership[i]; if (new_comm_id.find(c) == new_comm_id.end()) { new_comm_id[c] = new_vcount++; } membership[i] = new_comm_id[c]; } vector edges; for (int u = 1; u < G->n + 1; ++u) { int su = membership[u-1] + 1; for (int e = G->head[u]; e != -1; e = G->edges[e].next) { int v = G->edges[e].to; double w = G->edges[e].w; int sv = membership[v-1] + 1; edges.push_back({su, sv, w}); } } sort(edges.begin(), edges.end(), [](const SuperEdge& a, const SuperEdge& b) { if (a.u != b.u) return a.u < b.u; return a.v < b.v; }); Graph_L* super_g = new Graph_L(new_vcount, G->is_directed, G->is_deg); if (edges.empty()) return super_g; int curr_u = edges[0].u; int curr_v = edges[0].v; double curr_w = edges[0].w; for (size_t i = 1; i < edges.size(); ++i) { if (edges[i].u == curr_u && edges[i].v == curr_v) { curr_w += edges[i].w; } else { super_g->add_weighted_edge(curr_u, curr_v, curr_w); curr_u = edges[i].u; curr_v = edges[i].v; curr_w = edges[i].w; } } super_g->add_weighted_edge(curr_u, curr_v, curr_w); return super_g; } py::object cpp_louvain_communities_serial(py::object G, py::object weight, py::object threshold, py::object resolution) { Graph& G_ = G.cast(); { if (G_.is_linkgraph_dirty()) { G_._get_linkgraph_structure(); } } Graph_L original_GL = G_._get_linkgraph_structure(); Graph_L* current_GL = &original_GL; double threshold_val = threshold.cast(); double resolution_val = resolution.cast(); int vcount = current_GL->n; if (vcount == 0) return py::list(); double norm = calculate_norm(*current_GL); vector membership(vcount); iota(membership.begin(), membership.end(), 0); vector allocated_graphs; int iteration = 0; double previous_modularity = -1e100; if (norm > 0.0) { while (true) { iteration++; int curr_vcount = current_GL->n; vector curr_membership(curr_vcount); double current_modularity; current_modularity = modularity_optimization_serial(*current_GL, curr_membership, resolution_val, norm); double modularity_gain = current_modularity - previous_modularity; if (modularity_gain <= threshold_val || iteration > 100) { break; } Graph_L* next_GL = graph_compress(current_GL, curr_membership); allocated_graphs.push_back(next_GL); current_GL = next_GL; for (int i = 0; i < vcount; i++) { membership[i] = curr_membership[membership[i]]; } previous_modularity = current_modularity; } } unordered_map> cpp_groups; for (int i = 0; i < vcount; ++i) { int comm_id = membership[i]; node_t node_id = i + 1; cpp_groups[comm_id].push_back(node_id); } py::dict id_to_node = G_.id_to_node; py::list final_result; for (auto& kv : cpp_groups) { py::set py_comm; for (node_t node_id : kv.second) { py::object node_obj = id_to_node[py::cast(node_id)]; py_comm.add(node_obj); } final_result.append(py_comm); } for (auto g : allocated_graphs) { delete g; } return final_result; } py::object cpp_louvain_communities(py::object G, py::object weight, py::object threshold, py::object resolution) { Graph& G_ = G.cast(); #ifdef _OPENMP omp_set_num_threads(8); #endif Graph_L original_GL = G_._get_linkgraph_structure(); Graph_L* current_GL = &original_GL; double threshold_val = threshold.cast(); double resolution_val = resolution.cast(); int vcount = current_GL->n; if (vcount == 0) return py::list(); double norm = calculate_norm(*current_GL); vector membership(vcount); iota(membership.begin(), membership.end(), 0); vector allocated_graphs; int iteration = 0; double previous_modularity = 0.0; if (norm > 0.0) { while (true) { iteration++; int curr_vcount = current_GL->n; vector curr_membership(curr_vcount); double current_modularity; current_modularity = modularity_optimization_parallel_simplified(*current_GL, curr_membership, resolution_val, norm); double modularity_gain = current_modularity - previous_modularity; previous_modularity = current_modularity; if (modularity_gain <= threshold_val || iteration > 100) { break; } Graph_L* next_GL = graph_compress(current_GL, curr_membership); allocated_graphs.push_back(next_GL); current_GL = next_GL; for (int i = 0; i < vcount; i++) { membership[i] = curr_membership[membership[i]]; } } } unordered_map> cpp_groups; for (int i = 0; i < vcount; ++i) { int comm_id = membership[i]; node_t node_id = i + 1; cpp_groups[comm_id].push_back(node_id); } py::dict id_to_node = G_.id_to_node; py::list final_result; for (auto& kv : cpp_groups) { py::set py_comm; for (node_t node_id : kv.second) { py::object node_obj = id_to_node[py::cast(node_id)]; py_comm.add(node_obj); } final_result.append(py_comm); } for (auto g : allocated_graphs) { delete g; } return final_result; }