chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:36:30 +08:00
commit 55ab4e4a73
473 changed files with 72932 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <random>
#include <queue>
#include <cstdint>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "../../classes/graph.h"
#include "../../classes/linkgraph.h"
#include "../../common/utils.h"
namespace py = pybind11;
using namespace std;
py::object cpp_LPA(py::object G) {
Graph& G_ = G.cast<Graph&>();
Graph_L linkgraph = G_._get_linkgraph_structure();
int n = linkgraph.n;
py::dict id_to_node = G_.id_to_node;
if (n <= 1) {
py::dict result;
if (n == 1) {
py::list node_list;
node_list.append(id_to_node[py::cast(1)]);
result[py::cast(1)] = node_list;
}
return result;
}
vector<vector<int>> adj(n + 1);
for (int u = 1; u <= n; ++u) {
for (int e = linkgraph.head[u]; e != -1; e = linkgraph.edges[e].next) {
int v = linkgraph.edges[e].to;
if (u != v) {
adj[u].push_back(v);
}
}
}
for (int u = 1; u <= n; ++u) {
if (adj[u].size() > 1) {
sort(adj[u].begin(), adj[u].end());
adj[u].erase(unique(adj[u].begin(), adj[u].end()), adj[u].end());
}
}
vector<int> labels(n);
vector<int> nodes(n);
for (int i = 0; i < n; ++i) {
labels[i] = i;
nodes[i] = i + 1;
}
random_device rd;
mt19937 gen(rd());
const int MAX_ITERATIONS = 1000;
int iteration_count = 0;
vector<int> label_count(n, 0);
vector<int> active_labels;
active_labels.reserve(128);
while (iteration_count < MAX_ITERATIONS) {
iteration_count++;
shuffle(nodes.begin(), nodes.end(), gen);
bool changed = false;
for (int node : nodes) {
int max_count = 0;
for (int neighbor : adj[node]) {
int neighbor_label = labels[neighbor - 1];
if (label_count[neighbor_label] == 0) {
active_labels.push_back(neighbor_label);
}
label_count[neighbor_label]++;
if (label_count[neighbor_label] > max_count) {
max_count = label_count[neighbor_label];
}
}
if (max_count > 0) {
int current_label = labels[node - 1];
vector<int> best_labels;
for (int label : active_labels) {
if (label_count[label] == max_count) {
best_labels.push_back(label);
}
}
bool current_is_best = false;
for (int label : best_labels) {
if (label == current_label) {
current_is_best = true;
break;
}
}
if (!current_is_best) {
changed = true;
if (best_labels.size() == 1) {
labels[node - 1] = best_labels[0];
} else {
uniform_int_distribution<> dis(0, best_labels.size() - 1);
labels[node - 1] = best_labels[dis(gen)];
}
}
}
for (int label : active_labels) {
label_count[label] = 0;
}
active_labels.clear();
}
if (!changed) {
break;
}
}
unordered_map<int, vector<int>> label_to_nodes;
for (int i = 0; i < n; ++i) {
int label = labels[i];
label_to_nodes[label].push_back(i + 1);
}
py::dict result;
int community_id = 1;
for (const auto& pair : label_to_nodes) {
py::list node_list;
for (int internal_id : pair.second) {
node_list.append(id_to_node[py::cast(internal_id)]);
}
result[py::cast(community_id)] = node_list;
community_id++;
}
return result;
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#include <pybind11/pybind11.h>
namespace py = pybind11;
py::object cpp_LPA(py::object G);
@@ -0,0 +1,10 @@
#pragma once
#include "modularity.h"
#include "greedy_modularity.h"
#include "motif.h"
#include "louvain.h"
#include "LPA.h"
#include "ego_graph.h"
#include "graph_coloring.h"
#include "localsearch.h"
@@ -0,0 +1,155 @@
#include "ego_graph.h"
#include "subgraph.h"
#include "../../classes/graph.h"
#include "../../classes/directed_graph.h"
#include "../../classes/csr_graph.h"
#include "../../common/utils.h"
#include "../../classes/linkgraph.h"
#include "../path/path.h"
#include <algorithm>
struct _EgoGraphCore {
Graph_L G_l;
std::vector<node_t> node_ids;
std::unordered_map<node_t, int> node_to_idx;
py::dict id_to_node_py;
bool has_error = false;
};
static _EgoGraphCore _cpp_ego_graph_compute_impl(
Graph& G_,
py::object n,
double radius_val,
bool center_val,
bool undirected_val,
bool is_directed,
const std::string& weight_key) {
_EgoGraphCore out;
if (G_.node_to_id.contains(n) == 0) {
PyErr_Format(PyExc_KeyError, "Node %R is not in the graph.", n.ptr());
out.has_error = true;
return out;
}
node_t center_id = G_.node_to_id[n].cast<node_t>();
out.id_to_node_py = G_.id_to_node;
if (G_.linkgraph_dirty || G_.linkgraph_structure.max_deg == -1) {
if (undirected_val) {
out.G_l = graph_to_linkgraph(G_, false, weight_key, true, false);
} else {
out.G_l = graph_to_linkgraph(G_, is_directed, weight_key, true, false);
}
G_.linkgraph_dirty = false;
G_.linkgraph_structure = out.G_l; // also cache the freshly built linkgraph
} else {
out.G_l = G_.linkgraph_structure;
}
std::vector<double> dist = _dijkstra(out.G_l, center_id, -1);
int N = out.G_l.n;
for (int i = 1; i <= N; i++) {
if (dist[i] > radius_val) continue;
if (!center_val && i == (int)center_id) continue;
out.node_to_idx[i] = (int)out.node_ids.size();
out.node_ids.push_back(i);
}
return out;
}
static _EgoGraphCore _cpp_ego_graph_compute(
py::object G,
py::object n,
py::object radius,
py::object center,
py::object undirected,
py::object distance) {
bool is_directed = G.attr("is_directed")().cast<bool>();
bool center_val = center.cast<bool>();
bool undirected_val = undirected.cast<bool>();
double radius_val = radius.cast<double>();
std::string weight_key = weight_to_string(distance);
if (is_directed) {
DiGraph& G_ = G.cast<DiGraph&>();
return _cpp_ego_graph_compute_impl(
G_, n, radius_val, center_val, undirected_val, is_directed, weight_key);
} else {
Graph& G_ = G.cast<Graph&>();
return _cpp_ego_graph_compute_impl(
G_, n, radius_val, center_val, undirected_val, is_directed, weight_key);
}
}
py::object cpp_ego_graph(
py::object G,
py::object n,
py::object radius,
py::object center,
py::object undirected,
py::object distance) {
_EgoGraphCore core = _cpp_ego_graph_compute(G, n, radius, center, undirected, distance);
if (core.has_error) return py::none();
return nodes_subgraph_cpp(G, core.node_ids);
}
py::object cpp_ego_graph_csr(
py::object G,
py::object n,
py::object radius,
py::object center,
py::object undirected,
py::object distance) {
_EgoGraphCore core = _cpp_ego_graph_compute(G, n, radius, center, undirected, distance);
if (core.has_error) return py::none();
int n_nodes = (int)core.node_ids.size();
if (n_nodes == 0) {
return py::dict();
}
std::vector<int> V(n_nodes + 1, 0);
std::vector<int> E;
std::vector<double> W;
for (int new_u = 0; new_u < n_nodes; new_u++) {
node_t u = core.node_ids[new_u];
V[new_u + 1] = V[new_u];
int edge_idx = core.G_l.head[u];
while (edge_idx != -1 && edge_idx < core.G_l.e) {
node_t v = core.G_l.edges[edge_idx].to;
auto it = core.node_to_idx.find(v);
if (it != core.node_to_idx.end()) {
E.push_back(it->second);
W.push_back(core.G_l.edges[edge_idx].w);
V[new_u + 1]++;
}
edge_idx = core.G_l.edges[edge_idx].next;
}
}
py::list original_nodes;
for (node_t internal_id : core.node_ids) {
original_nodes.append(core.id_to_node_py[py::cast(internal_id)]);
}
py::dict result;
result["nodes"] = original_nodes;
result["V"] = py::cast(V);
result["E"] = py::cast(E);
result["W"] = py::cast(W);
return result;
}
@@ -0,0 +1,23 @@
#pragma once
#include <pybind11/pybind11.h>
namespace py = pybind11;
py::object cpp_ego_graph(
py::object G,
py::object n,
py::object radius = py::int_(1),
py::object center = py::bool_(true),
py::object undirected = py::bool_(false),
py::object distance = py::none()
);
py::object cpp_ego_graph_csr(
py::object G,
py::object n,
py::object radius = py::int_(1),
py::object center = py::bool_(true),
py::object undirected = py::bool_(false),
py::object distance = py::none()
);
@@ -0,0 +1,135 @@
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <iostream>
#include <random>
#include <numeric>
#include <cstdint>
#include <chrono>
#include <queue>
#include <cstring>
#ifdef _OPENMP
#include <omp.h>
#else
#warning "OpenMP is not available: omp_graph_coloring will fall back to single-threaded execution."
#endif
#include "../../classes/linkgraph.h"
using namespace std;
vector<int> greedy_graph_coloring(const Graph_L& G) {
int n = G.n;
if (n == 0) return vector<int>();
vector<int> degree(n + 1, 0);
for (int v = 1; v <= n; ++v) {
for (int e = G.head[v]; e != -1; e = G.edges[e].next) {
degree[v]++;
}
}
vector<int> nodes(n);
iota(nodes.begin(), nodes.end(), 1);
sort(nodes.begin(), nodes.end(), [&degree](int a, int b) {
return degree[a] > degree[b];
});
vector<int> colors(n, -1);
vector<int> used(n + 1, -1);
for (int u : nodes) {
for (int e = G.head[u]; e != -1; e = G.edges[e].next) {
int v = G.edges[e].to;
if (v == u) continue;
int neighbor_color = colors[v - 1];
if (neighbor_color != -1) {
used[neighbor_color] = u;
}
}
int cr = 0;
while (cr <= n && used[cr] == u) {
cr++;
}
colors[u - 1] = cr;
}
return colors;
}
vector<int> omp_graph_coloring(const Graph_L& G) {
int n = G.n;
if (n == 0) return vector<int>();
vector<int> degree(n + 1, 0);
for (int v = 1; v <= n; ++v) {
for (int e = G.head[v]; e != -1; e = G.edges[e].next) {
degree[v]++;
}
}
vector<int> nodes(n);
iota(nodes.begin(), nodes.end(), 1);
sort(nodes.begin(), nodes.end(), [&degree](int a, int b) {
return degree[a] > degree[b];
});
vector<int> colors(n, -1);
#pragma omp parallel
{
vector<int> local_used(n + 1, -1);
#pragma omp for schedule(guided)
for (int i = 0; i < n; ++i) {
int u = nodes[i];
fill(local_used.begin(), local_used.end(), -1);
for (int e = G.head[u]; e != -1; e = G.edges[e].next) {
int v = G.edges[e].to;
if (v == u) continue;
int neighbor_color = colors[v - 1];
if (neighbor_color != -1 && neighbor_color <= n) {
local_used[neighbor_color] = u;
}
}
int cr = 0;
while (cr <= n && local_used[cr] == u) {
cr++;
}
colors[u - 1] = cr;
}
}
return colors;
}
bool verify_coloring(const Graph_L& g, const vector<int>& colors) {
int n = g.n;
for (int v = 1; v <= n; v++) {
int v_color = colors[v - 1];
for (int e = g.head[v]; e != -1; e = g.edges[e].next) {
int u = g.edges[e].to;
if (u == v) continue;
int u_color = colors[u - 1];
if (v_color == u_color) {
return false;
}
}
}
return true;
}
int count_colors(const vector<int>& colors) {
int max_color = -1;
for (int c : colors) {
if (c > max_color) {
max_color = c;
}
}
return max_color + 1;
}
@@ -0,0 +1,12 @@
#pragma once
#include <vector>
#include "../../classes/linkgraph.h"
std::vector<int> greedy_graph_coloring(const Graph_L& G);
std::vector<int> omp_graph_coloring(const Graph_L& G);
bool verify_coloring(const Graph_L& g, const std::vector<int>& colors);
int count_colors(const std::vector<int>& colors);
@@ -0,0 +1,289 @@
#include "indexed_heap.h"
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <cmath>
#include <queue>
#include <set>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "../../classes/graph.h"
#include "../../common/utils.h"
#include "../../classes/linkgraph.h"
#include "greedy_modularity.h"
namespace py = pybind11;
using namespace std;
py::object cpp_greedy_modularity_communities(py::object G, py::object weight) {
Graph& G_ = G.cast<Graph&>();
bool is_directed = G.attr("is_directed")().cast<bool>();
if (is_directed) {
throw py::value_error("greedy_modularity_communities currently only supports undirected graphs (Graph class). "
"For directed graphs, please use other community detection algorithms.");
}
string weight_key = weight.is_none() ? "weight" : weight.cast<string>();
Graph_L GL;
bool used_cached_linkgraph = false;
if (G_.linkgraph_dirty || G_.linkgraph_structure.max_deg == -1) {
GL = graph_to_linkgraph(G_, false, weight_key, true, false);
G_.linkgraph_dirty = false;
} else {
GL = G_.linkgraph_structure;
used_cached_linkgraph = true;
}
int N = GL.n;
if (N == 0) {
return py::list();
}
double m = 0.0;
vector<double> k(N + 1, 0.0);
for (int u = 1; u <= N; ++u) {
for (int e = GL.head[u]; e != -1; e = GL.edges[e].next) {
int v = GL.edges[e].to;
double w = GL.edges[e].w;
if (v > u) {
m += w;
}
k[u] += w;
}
}
if (m == 0) {
py::list result;
py::dict id_to_node = G_.id_to_node;
for (int i = 1; i <= N; ++i) {
py::set comm;
comm.add(id_to_node[py::cast(i)]);
result.append(comm);
}
return result;
}
double q0 = 1.0 / (2.0 * m);
vector<double> a(N);
for (int i = 0; i < N; ++i) {
a[i] = k[i + 1] * q0;
}
vector<int> parent(N);
vector<vector<int>> nodes(N);
for (int i = 0; i < N; ++i) {
parent[i] = i;
nodes[i].push_back(i + 1);
}
vector<vector<int>> neigh(N);
int max_neigh_size = 0;
for (int i = 0; i < N; ++i) {
int u = i + 1;
for (int e = GL.head[u]; e != -1; e = GL.edges[e].next) {
int v = GL.edges[e].to;
if (v > 0 && v <= N && v != u) {
neigh[i].push_back(v - 1);
}
}
sort(neigh[i].begin(), neigh[i].end());
neigh[i].erase(unique(neigh[i].begin(), neigh[i].end()), neigh[i].end());
if ((int)neigh[i].size() > max_neigh_size) {
max_neigh_size = neigh[i].size();
}
}
unordered_map<long long, double> edge_weights;
for (int u = 1; u <= N; ++u) {
for (int e = GL.head[u]; e != -1; e = GL.edges[e].next) {
int v = GL.edges[e].to;
double w = GL.edges[e].w;
if (v > u) {
long long key = ((long long)(u - 1) << 32) | (unsigned int)(v - 1);
edge_weights[key] = w;
}
}
}
unordered_map<long long, double> dq;
dq.reserve(N * 4);
for (int i = 0; i < N; ++i) {
int u = i + 1;
for (int j : neigh[i]) {
if (j > i) {
int v = j + 1;
long long edge_key = ((long long)i << 32) | (unsigned int)j;
double w = edge_weights.count(edge_key) ? edge_weights[edge_key] : 0.0;
double dq_val = 2.0 * w * q0 - 2.0 * k[u] * k[v] * q0 * q0;
long long key = ((long long)i << 32) | (unsigned int)j;
dq[key] = dq_val;
}
}
}
IndexedMaxHeap H(N);
for (const auto& kv : dq) {
int i = (int)(kv.first >> 32);
int j = (int)(kv.first & 0xFFFFFFFF);
H.push(kv.second, i, j);
}
vector<char> merged(N, 0);
int merge_count = 0;
vector<int> combined;
combined.reserve(max_neigh_size * 2);
while (!H.empty()) {
auto best = H.pop();
int i = best.i;
int j = best.j;
if (merged[i] || merged[j]) {
continue;
}
if (parent[i] != i || parent[j] != j) {
continue;
}
long long key = ((long long)i << 32) | (unsigned int)j;
auto it = dq.find(key);
if (it == dq.end() || it->second != best.dq) {
continue;
}
if (best.dq <= 0) {
break;
}
nodes[i].insert(nodes[i].end(), nodes[j].begin(), nodes[j].end());
nodes[j].clear();
parent[j] = i;
merged[j] = 1;
merge_count++;
combined.clear();
const vector<int>& list_i = neigh[i];
const vector<int>& list_j = neigh[j];
size_t p1 = 0, p2 = 0;
while (p1 < list_i.size() && p2 < list_j.size()) {
int val1 = list_i[p1];
int val2 = list_j[p2];
if (val1 < val2) {
if (val1 != i && val1 != j && !merged[val1]) {
combined.push_back(val1);
}
p1++;
} else if (val1 > val2) {
if (val2 != i && val2 != j && !merged[val2]) {
combined.push_back(val2);
}
p2++;
} else {
if (val1 != i && val1 != j && !merged[val1]) {
combined.push_back(val1);
}
p1++;
p2++;
}
}
while (p1 < list_i.size()) {
int val = list_i[p1];
if (val != i && val != j && !merged[val]) {
combined.push_back(val);
}
p1++;
}
while (p2 < list_j.size()) {
int val = list_j[p2];
if (val != i && val != j && !merged[val]) {
combined.push_back(val);
}
p2++;
}
neigh[i].swap(combined);
for (int k_node : neigh[i]) {
if (k_node == i) continue;
if (parent[k_node] != k_node) continue;
long long key_ik = ((long long)min(i, k_node) << 32) | (unsigned int)max(i, k_node);
long long key_jk = ((long long)min(j, k_node) << 32) | (unsigned int)max(j, k_node);
bool has_ik = dq.find(key_ik) != dq.end();
bool has_jk = dq.find(key_jk) != dq.end();
double new_dq;
if (has_ik && has_jk) {
new_dq = dq[key_ik] + dq[key_jk];
} else if (has_jk) {
new_dq = dq[key_jk] - 2.0 * a[i] * a[k_node];
} else {
new_dq = dq[key_ik] - 2.0 * a[j] * a[k_node];
}
dq[key_ik] = new_dq;
if (has_jk) {
dq.erase(key_jk);
}
if (H.get_index(i, k_node) >= 0) {
H.update(new_dq, i, k_node);
} else {
H.push(new_dq, i, k_node);
}
}
const vector<int>& old_j_neigh = neigh[j];
for (int k_node : old_j_neigh) {
if (k_node == i || k_node == j) continue;
if (parent[k_node] != k_node) continue;
H.remove(j, k_node);
}
neigh[j].clear();
a[i] += a[j];
a[j] = 0;
}
py::dict id_to_node = G_.id_to_node;
py::list result;
for (int i = 0; i < N; ++i) {
if (parent[i] == i && !nodes[i].empty()) {
py::set comm;
for (int node_id : nodes[i]) {
comm.add(id_to_node[py::cast(node_id)]);
}
result.append(comm);
}
}
py::list sorted_result;
vector<int> sizes;
for (size_t i = 0; i < result.size(); ++i) {
sizes.push_back(py::len(result[i]));
}
vector<int> indices(result.size());
iota(indices.begin(), indices.end(), 0);
sort(indices.begin(), indices.end(), [&](int a_idx, int b_idx) {
return sizes[a_idx] > sizes[b_idx];
});
for (int idx : indices) {
sorted_result.append(result[idx]);
}
return sorted_result;
}
@@ -0,0 +1,11 @@
#pragma once
#include <pybind11/pybind11.h>
namespace py = pybind11;
py::object cpp_greedy_modularity_communities(
py::object G,
py::object weight = py::str("weight")
);
@@ -0,0 +1,191 @@
#pragma once
#include <vector>
#include <algorithm>
class IndexedMaxHeap {
public:
struct HeapEntry {
double dq;
int i;
int j;
};
private:
std::vector<HeapEntry> heap;
std::vector<std::vector<int>> pair_to_idx;
size_t heap_size;
int capacity_n;
static inline long long make_key(int i, int j) {
int a = std::min(i, j);
int b = std::max(i, j);
return ((long long)a << 32) | (unsigned int)b;
}
public:
IndexedMaxHeap(size_t capacity = 0) : heap_size(0), capacity_n((int)capacity) {
heap.reserve(capacity);
if (capacity > 0) {
pair_to_idx.assign(capacity, std::vector<int>(capacity, -1));
}
}
bool empty() const { return heap_size == 0; }
size_t size() const { return heap_size; }
int get_index(int i, int j) const {
int a = std::min(i, j);
int b = std::max(i, j);
if (a < 0 || b < 0 || a >= capacity_n || b >= capacity_n) return -1;
size_t idx = (size_t)pair_to_idx[a][b];
if (idx >= heap.size()) return -1;
if (heap[idx].i == a && heap[idx].j == b) {
return (int)idx;
}
return -1;
}
void push(double dq, int i, int j) {
int a = std::min(i, j);
int b = std::max(i, j);
if (a < 0 || b < 0 || a >= capacity_n || b >= capacity_n) return;
size_t existing = (size_t)pair_to_idx[a][b];
if (existing < heap.size() && heap[existing].i == a && heap[existing].j == b) {
update(dq, i, j);
return;
}
HeapEntry entry = {dq, a, b};
heap.push_back(entry);
size_t idx = heap.size() - 1;
pair_to_idx[a][b] = (int)idx;
sift_up(idx);
heap_size++;
}
void update(double dq, int i, int j) {
int a = std::min(i, j);
int b = std::max(i, j);
if (a < 0 || b < 0 || a >= capacity_n || b >= capacity_n) return;
size_t idx = (size_t)pair_to_idx[a][b];
if (idx >= heap.size()) return;
if (heap[idx].i != a || heap[idx].j != b) return;
if (heap[idx].dq == dq) return;
bool is_increase = dq > heap[idx].dq;
heap[idx].dq = dq;
if (is_increase) {
sift_up(idx);
} else {
sift_down(idx);
}
}
void remove(int i, int j) {
int a = std::min(i, j);
int b = std::max(i, j);
if (a < 0 || b < 0 || a >= capacity_n || b >= capacity_n) return;
size_t idx = (size_t)pair_to_idx[a][b];
if (idx >= heap.size()) return;
if (heap[idx].i != a || heap[idx].j != b) return;
pair_to_idx[a][b] = -1;
if (idx == heap.size() - 1) {
heap.pop_back();
} else {
heap[idx] = heap.back();
int last_a = heap[idx].i;
int last_b = heap[idx].j;
if (last_a >= 0 && last_a < capacity_n && last_b >= 0 && last_b < capacity_n) {
pair_to_idx[last_a][last_b] = (int)idx;
}
heap.pop_back();
sift_up(idx);
sift_down(idx);
}
if (heap_size > 0) heap_size--;
}
HeapEntry pop() {
HeapEntry result = heap[0];
pair_to_idx[result.i][result.j] = -1;
if (heap.size() > 1) {
heap[0] = heap.back();
int new_a = heap[0].i;
int new_b = heap[0].j;
if (new_a >= 0 && new_a < capacity_n && new_b >= 0 && new_b < capacity_n) {
pair_to_idx[new_a][new_b] = 0;
}
heap.pop_back();
sift_down(0);
} else {
heap.pop_back();
}
if (heap_size > 0) heap_size--;
return result;
}
const HeapEntry& top() const { return heap[0]; }
private:
void sift_up(size_t idx) {
while (idx > 0) {
size_t parent = (idx - 1) >> 1;
if (heap[parent].dq >= heap[idx].dq) break;
std::swap(heap[parent], heap[idx]);
int p_i = heap[parent].i, p_j = heap[parent].j;
int c_i = heap[idx].i, c_j = heap[idx].j;
if (p_i >= 0 && p_i < capacity_n && p_j >= 0 && p_j < capacity_n) {
pair_to_idx[p_i][p_j] = (int)parent;
}
if (c_i >= 0 && c_i < capacity_n && c_j >= 0 && c_j < capacity_n) {
pair_to_idx[c_i][c_j] = (int)idx;
}
idx = parent;
}
}
void sift_down(size_t idx) {
size_t n = heap.size();
while (true) {
size_t largest = idx;
size_t left = idx * 2 + 1;
size_t right = idx * 2 + 2;
if (left < n && heap[left].dq > heap[largest].dq) {
largest = left;
}
if (right < n && heap[right].dq > heap[largest].dq) {
largest = right;
}
if (largest == idx) break;
std::swap(heap[largest], heap[idx]);
int l_i = heap[largest].i, l_j = heap[largest].j;
int c_i = heap[idx].i, c_j = heap[idx].j;
if (l_i >= 0 && l_i < capacity_n && l_j >= 0 && l_j < capacity_n) {
pair_to_idx[l_i][l_j] = (int)largest;
}
if (c_i >= 0 && c_i < capacity_n && c_j >= 0 && c_j < capacity_n) {
pair_to_idx[c_i][c_j] = (int)idx;
}
idx = largest;
}
}
};
@@ -0,0 +1,613 @@
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
#include <queue>
#include <map>
#include <random>
#include <cmath>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "../../classes/graph.h"
#include "../../common/utils.h"
#include "../../classes/linkgraph.h"
#include "localsearch.h"
namespace py = pybind11;
using namespace std;
struct RootDecision {
int superior;
int path_length;
int degree;
};
int choose_center(const vector<pair<int, double>>& sorted_multi) {
if (sorted_multi.size() < 2) {
return 1;
}
vector<double> y;
for (const auto& p : sorted_multi) {
y.push_back(p.second);
}
vector<double> delta;
for (size_t i = 1; i < y.size(); ++i) {
delta.push_back(fabs(y[i] - y[i-1]));
}
if (delta.empty()) {
return 1;
}
vector<double> delta_nozero;
for (double d : delta) {
if (d != 0) {
delta_nozero.push_back(d);
}
}
if (delta_nozero.empty()) {
return 1;
}
double mean = 0;
for (double d : delta_nozero) {
mean += d;
}
mean /= delta_nozero.size();
double variance = 0;
for (double d : delta_nozero) {
variance += (d - mean) * (d - mean);
}
variance /= delta_nozero.size();
double std_dev = sqrt(variance);
double threshold = std_dev + mean;
for (size_t i = 0; i < delta.size(); ++i) {
if (delta[i] > threshold) {
return i + 1;
}
}
return 0;
}
py::object cpp_localsearch(
py::object G,
py::object center_num,
py::object auto_choose_centers,
py::object maximum_tree,
py::object seed,
py::object self_loop
) {
Graph_generate_linkgraph(G, py::str("weight"));
Graph& G_ = G.cast<Graph&>();
Graph_L GL = G_._get_linkgraph_structure();
py::dict id_to_node = G_.id_to_node;
int n = GL.n;
if (n == 0) {
return py::make_tuple(py::none(), py::list(), py::list(), py::dict(), py::dict(), py::none());
}
bool has_edges = false;
for (int u = 1; u <= n; ++u) {
if (GL.head[u] != -1) {
has_edges = true;
break;
}
}
if (!has_edges) {
py::dict result_grouped;
py::list result_center_dcd;
py::list result_y_dcd;
py::dict result_y_partition;
for (int u = 1; u <= n; ++u) {
py::object node = id_to_node[py::cast(u)];
py::list members;
members.append(node);
result_grouped[node] = members;
result_center_dcd.append(node);
result_y_dcd.append(node);
result_y_partition[node] = node;
}
return py::make_tuple(py::none(), result_center_dcd, result_y_dcd, result_y_partition, result_grouped, py::none());
}
std::mt19937 rng(42);
if (!seed.is_none()) {
try {
int seed_value = seed.cast<int>();
rng.seed(seed_value);
} catch (...) {
}
}
std::uniform_real_distribution<double> random_dist(0.0, 1.0);
unordered_set<int> selfloop_nodes;
bool has_selfloop = self_loop.cast<bool>();
if (!has_selfloop) {
selfloop_nodes.clear();
}
vector<int> degree(n + 1, 0);
for (int u = 1; u <= n; ++u) {
int deg = 0;
for (int e = GL.head[u]; e != -1; e = GL.edges[e].next) {
deg++;
}
if (selfloop_nodes.count(u)) {
degree[u] = deg + 1;
} else {
degree[u] = deg;
}
}
unordered_map<int, vector<int>> dag_adj;
unordered_map<int, vector<int>> dag_pred;
for (int v = 1; v <= n; ++v) {
if (degree[v] == 0) continue;
int kv = degree[v];
vector<pair<int, int>> neighbors;
for (int e = GL.head[v]; e != -1; e = GL.edges[e].next) {
int nn = GL.edges[e].to;
if (nn == v && selfloop_nodes.count(v)) continue;
int deg_nn = degree[nn];
neighbors.push_back({nn, deg_nn});
}
if (!neighbors.empty()) {
int knnmax = -1;
for (auto& p : neighbors) {
if (p.second > knnmax) knnmax = p.second;
}
if (knnmax >= kv) {
for (auto& p : neighbors) {
if (p.second == knnmax) {
int nn = p.first;
bool already_has = false;
bool has_reverse = false;
for (int existing : dag_adj[v]) {
if (existing == nn) {
already_has = true;
break;
}
}
for (int existing : dag_adj[nn]) {
if (existing == v) {
has_reverse = true;
break;
}
}
if (!already_has && !has_reverse) {
dag_adj[v].push_back(nn);
dag_pred[nn].push_back(v);
}
}
}
}
}
}
vector<int> out_degree_dag(n + 1, 0);
for (int u = 1; u <= n; ++u) {
out_degree_dag[u] = (int)dag_adj[u].size();
}
vector<int> roots;
for (int u = 1; u <= n; ++u) {
if (out_degree_dag[u] == 0 && degree[u] > 0) {
roots.push_back(u);
}
}
if (roots.empty()) {
for (int u = 1; u <= n; ++u) {
if (degree[u] > 0) {
roots.push_back(u);
}
}
}
if (roots.size() > 1) {
bool all_same_degree = true;
int first_degree = -1;
for (int root : roots) {
if (first_degree == -1) {
first_degree = degree[root];
} else if (degree[root] != first_degree) {
all_same_degree = false;
break;
}
}
if (all_same_degree) {
int max_root = -1;
for (int root : roots) {
if (root > max_root) {
max_root = root;
}
}
roots.clear();
roots.push_back(max_root);
}
}
unordered_map<int, int> tree_rootnode;
unordered_map<int, int> tree_parentnode;
unordered_map<int, int> tree_distancetoroot;
for (int i = 1; i <= n; ++i) {
tree_rootnode[i] = -1;
tree_parentnode[i] = -1;
tree_distancetoroot[i] = -1;
}
queue<pair<int, int>> bfs_queue;
for (int root : roots) {
bfs_queue.push({root, 0});
tree_rootnode[root] = root;
tree_parentnode[root] = -1;
tree_distancetoroot[root] = 0;
}
vector<int> visited(n + 1, 0);
for (int root : roots) {
visited[root] = 1;
}
while (!bfs_queue.empty()) {
int parent, dist;
std::tie(parent, dist) = bfs_queue.front();
bfs_queue.pop();
for (int pred : dag_pred[parent]) {
if (tree_distancetoroot[pred] != -1 && tree_distancetoroot[pred] < dist + 1) {
continue;
}
if (tree_distancetoroot[pred] == -1) {
tree_rootnode[pred] = tree_rootnode[parent];
tree_parentnode[pred] = parent;
tree_distancetoroot[pred] = dist + 1;
bfs_queue.push({pred, dist + 1});
} else if (tree_distancetoroot[pred] == dist + 1) {
if (random_dist(rng) < 0.5) {
continue;
}
tree_rootnode[pred] = tree_rootnode[parent];
tree_parentnode[pred] = parent;
}
}
}
unordered_map<int, vector<int>> root_to_node;
for (int node = 1; node <= n; ++node) {
int root = tree_rootnode[node];
if (root != -1) {
root_to_node[root].push_back(node);
}
}
vector<int> valid_roots;
for (auto& kv : root_to_node) {
if ((int)kv.second.size() > 1) {
valid_roots.push_back(kv.first);
}
}
unordered_set<int> root_set(valid_roots.begin(), valid_roots.end());
unordered_map<int, RootDecision> root_decision;
auto BFS_from_s = [&](int s) -> pair<int, int> {
queue<int> search_queue;
unordered_map<int, int> path_dict;
unordered_set<int> seen;
search_queue.push(s);
seen.insert(s);
path_dict[s] = 0;
while (!search_queue.empty()) {
int vertex = search_queue.front();
search_queue.pop();
int current_dist = path_dict[vertex];
vector<pair<int, int>> neighbors;
for (int e = GL.head[vertex]; e != -1; e = GL.edges[e].next) {
int nn = GL.edges[e].to;
int deg_nn = degree[nn];
neighbors.push_back({nn, deg_nn});
}
sort(neighbors.begin(), neighbors.end(),
[](const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second;
});
for (auto& p : neighbors) {
int w = p.first;
if (!seen.count(w)) {
path_dict[w] = current_dist + 1;
seen.insert(w);
search_queue.push(w);
}
if (root_set.count(w) && degree[w] > degree[s]) {
return {w, path_dict[w]};
}
}
}
return {s, -1};
};
for (int root : valid_roots) {
auto result = BFS_from_s(root);
root_decision[root] = {result.first, result.second, degree[root]};
}
int max_path = -1;
for (auto& kv : root_decision) {
if (kv.second.path_length > max_path) {
max_path = kv.second.path_length;
}
}
if (max_path < 0) max_path = 2;
for (auto& kv : root_decision) {
if (kv.second.path_length == -1) {
kv.second.path_length = max_path;
}
}
unordered_map<int, RootDecision> node_plot = root_decision;
for (int node = 1; node <= n; ++node) {
if (node_plot.find(node) == node_plot.end() && degree[node] > 0) {
int parent = tree_parentnode[node];
node_plot[node] = {parent, 1, degree[node]};
}
}
vector<int> node_ids;
vector<int> degrees;
vector<int> path_lens;
for (auto& kv : node_plot) {
node_ids.push_back(kv.first);
degrees.push_back(kv.second.degree);
path_lens.push_back(kv.second.path_length);
}
for (size_t i = 0; i < path_lens.size(); ++i) {
if (degrees[i] <= 1) {
path_lens[i] = 1;
}
}
unordered_map<int, int> degree_rank;
vector<pair<int, int>> sorted_by_deg;
for (size_t i = 0; i < node_ids.size(); ++i) {
sorted_by_deg.push_back({degrees[i], node_ids[i]});
}
sort(sorted_by_deg.begin(), sorted_by_deg.end(),
[](const pair<int, int>& a, const pair<int, int>& b) {
return a.first < b.first;
});
int rank = 1;
int last_deg = -1;
for (auto& p : sorted_by_deg) {
if (p.first != last_deg) {
degree_rank[p.second] = rank;
rank++;
last_deg = p.first;
} else {
degree_rank[p.second] = rank - 1;
}
}
int min_rank = 1;
int max_rank = rank - 1;
double rank_range = (max_rank - min_rank);
if (rank_range == 0) rank_range = 1;
vector<double> square_path(node_ids.size());
double max_sq_path = 0;
double min_sq_path = 1e9;
for (size_t i = 0; i < node_ids.size(); ++i) {
square_path[i] = (double)path_lens[i] * (double)path_lens[i];
if (square_path[i] > max_sq_path) max_sq_path = square_path[i];
if (square_path[i] < min_sq_path) min_sq_path = square_path[i];
}
double sq_range = max_sq_path - min_sq_path;
if (sq_range == 0) sq_range = 1;
unordered_map<int, double> multi_dict;
for (size_t i = 0; i < node_ids.size(); ++i) {
int node = node_ids[i];
double norm_deg, norm_sq_path;
if (max_rank == min_rank) {
norm_deg = 1.0 / (double)node_ids.size();
} else {
norm_deg = (double)(degree_rank[node] - min_rank) / rank_range;
}
if (max_sq_path == min_sq_path) {
norm_sq_path = 1.0 / (double)node_ids.size();
} else {
norm_sq_path = (square_path[i] - min_sq_path) / sq_range;
}
multi_dict[node] = norm_deg * norm_sq_path;
}
vector<pair<int, double>> sorted_multi;
for (auto& kv : multi_dict) {
sorted_multi.push_back(kv);
}
sort(sorted_multi.begin(), sorted_multi.end(),
[](const pair<int, double>& a, const pair<int, double>& b) {
if (fabs(a.second - b.second) > 1e-9) return a.second > b.second;
return a.first > b.first;
});
int num_centers = (int)valid_roots.size();
bool auto_choose = auto_choose_centers.cast<bool>();
if (auto_choose && sorted_multi.size() > 0) {
int auto_centernum = choose_center(sorted_multi);
if (!center_num.is_none()) {
int user_center_num = center_num.cast<int>();
num_centers = (auto_centernum < user_center_num) ? auto_centernum : user_center_num;
} else {
num_centers = auto_centernum;
}
} else if (!center_num.is_none()) {
num_centers = center_num.cast<int>();
}
if (num_centers <= 0) {
num_centers = (int)valid_roots.size();
}
vector<int> center_dcd;
int local_cnt = 0;
for (size_t i = 0; i < sorted_multi.size() && local_cnt < num_centers; ++i) {
if (sorted_multi[i].second > 0) {
local_cnt++;
center_dcd.push_back(sorted_multi[i].first);
}
}
if (center_dcd.empty() && !sorted_multi.empty()) {
center_dcd.push_back(sorted_multi[0].first);
}
bool all_same_degree = true;
int first_deg = -1;
for (int i = 1; i <= n && all_same_degree; ++i) {
if (degree[i] > 0) {
if (first_deg == -1) {
first_deg = degree[i];
} else if (degree[i] != first_deg) {
all_same_degree = false;
}
}
}
if (all_same_degree && n > 0) {
center_dcd.clear();
center_dcd.push_back(n);
}
unordered_set<int> center_set(center_dcd.begin(), center_dcd.end());
for (int node : valid_roots) {
int superior = root_decision[node].superior;
tree_parentnode[node] = superior;
tree_rootnode[node] = superior;
}
for (int node = 0; node < n; ++node) {
if (degree[node] > 0 && center_set.count(node)) {
tree_rootnode[node] = node;
}
}
for (int node = 0; node < n; ++node) {
if (degree[node] == 0) continue;
vector<int> recent;
recent.push_back(node);
bool flag = false;
while (center_set.find(tree_rootnode[node]) == center_set.end() && !flag) {
int j = tree_rootnode[node];
if (j == -1 || find(recent.begin(), recent.end(), j) != recent.end()) {
tree_rootnode[node] = -1;
flag = true;
break;
}
recent.push_back(j);
tree_rootnode[node] = tree_rootnode[j];
}
}
unordered_map<int, int> y_partition;
vector<int> y_dcd;
for (int node = 1; node <= n; ++node) {
if (degree[node] == 0) continue;
int root = tree_rootnode[node];
if (root == -1 && !center_dcd.empty()) {
root = center_dcd[0];
tree_rootnode[node] = root;
}
y_partition[node] = root;
if (root == -1) {
y_dcd.push_back(-1);
} else {
y_dcd.push_back(root);
}
}
unordered_map<int, vector<int>> grouped;
for (auto& kv : y_partition) {
int center = kv.second;
if (center != -1) {
grouped[center].push_back(kv.first);
}
}
py::dict result_grouped;
for (auto& kv : grouped) {
py::list members;
for (int node_id : kv.second) {
members.append(id_to_node[py::cast(node_id)]);
}
result_grouped[id_to_node[py::cast(kv.first)]] = members;
}
py::list result_center_dcd;
for (int center : center_dcd) {
result_center_dcd.append(id_to_node[py::cast(center)]);
}
py::list result_y_dcd;
for (int label : y_dcd) {
if (label == -1) {
result_y_dcd.append(py::cast(-1));
} else {
result_y_dcd.append(id_to_node[py::cast(label)]);
}
}
py::dict result_y_partition;
for (auto& kv : y_partition) {
if (kv.second == -1) {
result_y_partition[id_to_node[py::cast(kv.first)]] = py::cast(-1);
} else {
result_y_partition[id_to_node[py::cast(kv.first)]] = id_to_node[py::cast(kv.second)];
}
}
return py::make_tuple(
py::none(),
result_center_dcd,
result_y_dcd,
result_y_partition,
result_grouped,
py::none()
);
}
@@ -0,0 +1,13 @@
#pragma once
#include <pybind11/pybind11.h>
namespace py = pybind11;
py::object cpp_localsearch(
py::object G,
py::object center_num = py::none(),
py::object auto_choose_centers = py::bool_(false),
py::object maximum_tree = py::bool_(true),
py::object seed = py::none(),
py::object self_loop = py::bool_(false)
);
@@ -0,0 +1,544 @@
#include <vector>
#include <unordered_map>
#include <algorithm>
#include <random>
#include <numeric>
#include <cstdint>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#ifdef _OPENMP
#include <omp.h>
#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<int>& membership,
double norm, double resolution) {
unordered_map<int, double> comm_weight_inside;
unordered_map<int, double> 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<int>& membership, double resolution_val, double norm) {
int n = G.n;
double total_modularity_gain = 0.0;
vector<CommunityState> 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<double> node_weight(n + 1, 0.0);
vector<double> 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<double> neigh_weight(n, 0.0);
vector<int> timestamp(n, 0);
vector<int> touched;
touched.reserve(512);
int version = 0;
std::random_device rd;
std::mt19937 rng(rd());
vector<int> 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<int>& membership, double resolution_val, double norm) {
int n = G.n;
vector<CommunityState> 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<double> node_weight(n + 1, 0.0);
vector<double> 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<int> node_order(n);
iota(node_order.begin(), node_order.end(), 0);
int num_moved_nodes = 1;
int iteration = 0;
vector<double> 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<int, double> neigh_weight;
neigh_weight.reserve(32);
vector<int> 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<int>& membership) {
unordered_map<int, int> 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<SuperEdge> 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<Graph&>();
{
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>();
double resolution_val = resolution.cast<double>();
int vcount = current_GL->n;
if (vcount == 0) return py::list();
double norm = calculate_norm(*current_GL);
vector<int> membership(vcount);
iota(membership.begin(), membership.end(), 0);
vector<Graph_L*> allocated_graphs;
int iteration = 0;
double previous_modularity = -1e100;
if (norm > 0.0) {
while (true) {
iteration++;
int curr_vcount = current_GL->n;
vector<int> 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<int, vector<node_t>> 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<Graph&>();
#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>();
double resolution_val = resolution.cast<double>();
int vcount = current_GL->n;
if (vcount == 0) return py::list();
double norm = calculate_norm(*current_GL);
vector<int> membership(vcount);
iota(membership.begin(), membership.end(), 0);
vector<Graph_L*> allocated_graphs;
int iteration = 0;
double previous_modularity = 0.0;
if (norm > 0.0) {
while (true) {
iteration++;
int curr_vcount = current_GL->n;
vector<int> 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<int, vector<node_t>> 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;
}
@@ -0,0 +1,20 @@
#pragma once
#include <pybind11/pybind11.h>
namespace py = pybind11;
py::object cpp_louvain_communities(
py::object G,
py::object weight = py::str("weight"),
py::object threshold = py::float_(0.0),
py::object resolution = py::float_(1.0)
);
py::object cpp_louvain_communities_serial(
py::object G,
py::object weight = py::str("weight"),
py::object threshold = py::float_(0.0),
py::object resolution = py::float_(1.0)
);
@@ -0,0 +1,213 @@
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <string>
#include <cstddef>
#ifdef _OPENMP
#include <omp.h>
#else
#warning "OpenMP is not available: modularity utility functions will fall back to single-threaded execution."
#endif
#include "../../classes/graph.h"
#include "../../common/utils.h"
using namespace std;
void addVectorsInPlace(std::vector<double>& v1, std::vector<double>& v2) {
if (v1.size() != v2.size()) {
throw std::invalid_argument("Vectors must have the same size for element-wise addition.");
}
const std::ptrdiff_t n = static_cast<std::ptrdiff_t>(v1.size());
#pragma omp parallel for
for (std::ptrdiff_t i = 0; i < n; ++i) {
double sum = v1[i] + v2[i];
v1[i] = sum;
v2[i] = sum;
}
}
double dotProduct(const std::vector<double>& v1, const std::vector<double>& v2) {
if (v1.size() != v2.size()) {
throw std::invalid_argument("Vectors must have the same size for dot product.");
}
double result = 0.0;
const std::ptrdiff_t n = static_cast<std::ptrdiff_t>(v1.size());
#pragma omp parallel for reduction(+:result)
for (std::ptrdiff_t i = 0; i < n; ++i) {
result += v1[i] * v2[i];
}
return result;
}
void calculate_degrees_and_edges_adj_parallel(
const adj_dict_factory& adj,
const std::vector<int>& membership,
bool directed,
int num_communities,
double& e,
double& m,
std::vector<double>& k_out,
std::vector<double>& k_in
)
{
const int N = membership.size();
#pragma omp parallel
{
double local_e = 0.0;
double local_m = 0.0;
std::vector<double> local_k_out(num_communities, 0.0);
std::vector<double> local_k_in(num_communities, 0.0);
double directed_factor = directed ? 1.0 : 2.0;
#pragma omp for
for (int i = 0; i < N; i++) {
node_t u = i + 1;
int c1 = membership[i];
auto adj_it = adj.find(u);
if (adj_it == adj.end()) continue;
auto& u_neighbors = adj_it->second;
for (auto& v_pair : u_neighbors) {
node_t v = v_pair.first;
if (!directed && u > v) continue;
int c2 = membership[v - 1];
double w = 1.0;
if (!v_pair.second.empty()) {
auto it = v_pair.second.begin();
w = it->second;
}
if (c1 == c2) {
local_e += directed_factor * w;
}
local_k_out[c1] += w;
local_k_in[c2] += w;
local_m += w;
}
}
#pragma omp critical
{
e += local_e;
m += local_m;
for (int i = 0; i < num_communities; i++) {
k_out[i] += local_k_out[i];
k_in[i] += local_k_in[i];
}
}
}
}
// The input `communities` may be either:
// (a) a membership list: a flat sequence of ints, membership[i] = community id of node (i+1); or
// (b) a community list: a sequence of iterables of node ids
py::object cpp_modularity(py::object G, py::object communities, py::object weight=py::str("weight")) {
Graph& G_ = G.cast<Graph&>();
bool directed = G.attr("is_directed")().cast<bool>();
adj_dict_factory& adj = G_.adj;
const int N = G_.node.size();
{
bool is_empty_seq = false;
try {
py::sequence seq = communities.cast<py::sequence>();
is_empty_seq = (seq.size() == 0);
} catch (const py::cast_error&) {
is_empty_seq = false;
}
if (is_empty_seq) {
py::module warnings = py::module::import("warnings");
warnings.attr("warn")(
"cpp_modularity: received an empty community list; returning Q = 0.0."
);
return py::float_(0.0);
}
}
std::vector<int> membership_vec;
bool is_membership = false;
py::sequence outer_seq;
try {
outer_seq = communities.cast<py::sequence>();
if (outer_seq.size() > 0) {
try {
outer_seq[0].cast<int>();
is_membership = true;
} catch (const py::cast_error&) {
is_membership = false;
}
}
} catch (const py::cast_error&) {
is_membership = true;
}
if (is_membership) {
// Already a membership vector: membership[i] is the community id of the (i+1)-th node.
membership_vec = communities.cast<std::vector<int>>();
} else {
membership_vec = std::vector<int>(N, -1);
int comm_id = 0;
for (auto community_handle : py::iter(communities)) {
for (auto node_handle : py::iter(community_handle)) {
py::object node = py::reinterpret_borrow<py::object>(node_handle.ptr());
if (G_.node_to_id.contains(node)) {
int node_id = G_.node_to_id[node].cast<node_t>() - 1;
if (node_id >= 0 && node_id < N) {
membership_vec[node_id] = comm_id;
}
}
}
comm_id++;
}
}
int num_communities = membership_vec.size();
double e = 0.0;
double m = 0.0;
std::vector<double> k_out(num_communities, 0.0);
std::vector<double> k_in(num_communities, 0.0);
calculate_degrees_and_edges_adj_parallel(adj, membership_vec, directed, num_communities, e, m, k_out, k_in);
if (!directed) addVectorsInPlace(k_out, k_in);
// Handle empty graph / zero total edge weight: m == 0 makes
// `norm = 1.0 / (directed_factor * m)` divide by zero and produces
// inf / nan. Define Q = 0.0 in this degenerate case (no edges -> no
// community structure to measure), with a Python warning.
if (m == 0.0) {
py::module warnings = py::module::import("warnings");
warnings.attr("warn")(
"cpp_modularity: graph has no edges (m == 0); returning Q = 0.0."
);
return py::float_(0.0);
}
double directed_factor = directed ? 1.0 : 2.0;
double norm = 1.0 / (directed_factor * m);
e *= norm;
double sum_products = dotProduct(k_out, k_in);
sum_products *= norm * norm;
double Q = e - sum_products;
return py::float_(Q);
}
@@ -0,0 +1,11 @@
#pragma once
#include <vector>
#include <unordered_set>
#include <string>
#include "../../classes/graph.h"
using namespace std;
py::object cpp_modularity(py::object G, py::object communities, py::object weight = py::str("weight"));
+553
View File
@@ -0,0 +1,553 @@
#include <vector>
#include <unordered_set>
#include <unordered_map>
#include <random>
#include <algorithm>
#include <queue>
#include <bitset>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#ifdef _OPENMP
#include <omp.h>
#else
#warning "OpenMP is not available: motif counting functions will fall back to single-threaded execution."
#endif
#include "../../classes/graph.h"
#include "../../classes/linkgraph.h"
#include "motif.h"
namespace py = pybind11;
using namespace std;
const int MAX_NODES = 65536;
struct MotifResult {
vector<int> nodes;
};
class MotifEnumerator {
private:
Graph_L GL;
int k;
int v_start;
vector<int>& id_to_node;
mt19937 rng;
uniform_real_distribution<double> dist;
vector<double> cut_prob;
vector<int> temp_nexcl;
vector<MotifResult> results;
bitset<MAX_NODES> nvp_bitset;
public:
vector<int> node_list;
vector<int> neighbor_offsets;
vector<int> neighbor_data;
int n_edges;
void build_neighbor_structure() {
neighbor_offsets.resize(GL.n + 2, 0);
for (int i = 1; i <= GL.n; ++i) {
int count = 0;
for (int e = GL.head[i]; e != -1; e = GL.edges[e].next) {
count++;
}
neighbor_offsets[i + 1] = neighbor_offsets[i] + count;
}
n_edges = neighbor_offsets[GL.n + 1];
neighbor_data.resize(n_edges);
for (int i = 1; i <= GL.n; ++i) {
int idx = neighbor_offsets[i];
for (int e = GL.head[i]; e != -1; e = GL.edges[e].next) {
neighbor_data[idx++] = GL.edges[e].to;
}
}
node_list.reserve(GL.n);
for (int i = 1; i <= GL.n; ++i) {
if (GL.head[i] != -1) {
node_list.push_back(i);
}
}
}
int get_neighbor_count(int v) const {
return neighbor_offsets[v + 1] - neighbor_offsets[v];
}
const int* get_neighbors_ptr(int v) const {
return neighbor_data.data() + neighbor_offsets[v];
}
void exclusive_neighborhood(int v, const vector<int>& vp, vector<int>& result) {
result.clear();
const int* nbr_ptr = get_neighbors_ptr(v);
int nbr_count = get_neighbor_count(v);
nvp_bitset.reset();
for (int node : vp) {
nvp_bitset.set(node);
const int* node_nbr_ptr = get_neighbors_ptr(node);
int node_nbr_count = get_neighbor_count(node);
for (int i = 0; i < node_nbr_count; ++i) {
nvp_bitset.set(node_nbr_ptr[i]);
}
}
for (int i = 0; i < nbr_count; ++i) {
int neighbor = nbr_ptr[i];
if (!nvp_bitset.test(neighbor)) {
result.push_back(neighbor);
}
}
}
void extend_subgraph_local(const vector<int>& Vsubgraph, const vector<int>& Vextension, int local_v_start,
vector<int>& local_temp_nexcl, bitset<MAX_NODES>& local_nvp_bitset,
vector<MotifResult>& local_results) {
if ((int)Vsubgraph.size() == k) {
local_results.push_back({Vsubgraph});
return;
}
int original_size = Vextension.size();
for (int i = original_size - 1; i >= 0; --i) {
int w = Vextension[i];
if (!cut_prob.empty() && (int)cut_prob.size() > (int)Vsubgraph.size() && dist(rng) > cut_prob[Vsubgraph.size()]) {
continue;
}
vector<int> new_subgraph;
new_subgraph.reserve(Vsubgraph.size() + 1);
new_subgraph.assign(Vsubgraph.begin(), Vsubgraph.end());
new_subgraph.push_back(w);
exclusive_neighborhood_local(w, Vsubgraph, local_temp_nexcl, local_nvp_bitset);
vector<int> next_ext;
next_ext.reserve(original_size + local_temp_nexcl.size());
for (int j = 0; j < original_size; ++j) {
if (j != i) {
int u = Vextension[j];
if (u > local_v_start) {
next_ext.push_back(u);
}
}
}
for (int u : local_temp_nexcl) {
if (u > local_v_start) {
next_ext.push_back(u);
}
}
extend_subgraph_local(new_subgraph, next_ext, local_v_start, local_temp_nexcl, local_nvp_bitset, local_results);
}
}
int extend_subgraph_count_local(const vector<int>& Vsubgraph, const vector<int>& Vextension, int local_v_start, vector<int>& local_temp_nexcl, bitset<MAX_NODES>& local_nvp_bitset) {
if ((int)Vsubgraph.size() == k) {
return 1;
}
int count = 0;
int original_size = Vextension.size();
for (int i = original_size - 1; i >= 0; --i) {
int w = Vextension[i];
vector<int> new_subgraph;
new_subgraph.reserve(Vsubgraph.size() + 1);
new_subgraph.assign(Vsubgraph.begin(), Vsubgraph.end());
new_subgraph.push_back(w);
exclusive_neighborhood_local(w, Vsubgraph, local_temp_nexcl, local_nvp_bitset);
vector<int> next_ext;
next_ext.reserve(original_size + local_temp_nexcl.size());
for (int j = 0; j < original_size; ++j) {
if (j != i) {
int u = Vextension[j];
if (u > local_v_start) {
next_ext.push_back(u);
}
}
}
for (int u : local_temp_nexcl) {
if (u > local_v_start) {
next_ext.push_back(u);
}
}
count += extend_subgraph_count_local(new_subgraph, next_ext, local_v_start, local_temp_nexcl, local_nvp_bitset);
}
return count;
}
int extend_subgraph_count(const vector<int>& Vsubgraph, const vector<int>& Vextension) {
return extend_subgraph_count_local(Vsubgraph, Vextension, v_start, temp_nexcl, nvp_bitset);
}
void exclusive_neighborhood_local(int v, const vector<int>& vp, vector<int>& result, bitset<MAX_NODES>& local_nvp_bitset) {
result.clear();
const int* nbr_ptr = get_neighbors_ptr(v);
int nbr_count = get_neighbor_count(v);
local_nvp_bitset.reset();
for (int node : vp) {
local_nvp_bitset.set(node);
const int* node_nbr_ptr = get_neighbors_ptr(node);
int node_nbr_count = get_neighbor_count(node);
for (int i = 0; i < node_nbr_count; ++i) {
local_nvp_bitset.set(node_nbr_ptr[i]);
}
}
for (int i = 0; i < nbr_count; ++i) {
int neighbor = nbr_ptr[i];
if (!local_nvp_bitset.test(neighbor)) {
result.push_back(neighbor);
}
}
}
public:
MotifEnumerator(Graph_L gl, int k, vector<int>& id_to_node)
: GL(gl), k(k), v_start(0), id_to_node(id_to_node), rng(42), dist(0.0, 1.0) {
build_neighbor_structure();
temp_nexcl.reserve(gl.n);
}
MotifEnumerator(Graph_L gl, int k, vector<int>& id_to_node, const vector<double>& cut_prob_vec, int seed)
: GL(gl), k(k), v_start(0), id_to_node(id_to_node), cut_prob(cut_prob_vec), rng(seed), dist(0.0, 1.0) {
build_neighbor_structure();
temp_nexcl.reserve(gl.n);
}
py::array_t<int> enumerate() {
results.clear();
if (k == 2) {
for (int v : node_list) {
if (!cut_prob.empty() && dist(rng) > cut_prob[0]) {
continue;
}
const int* nbr_ptr = get_neighbors_ptr(v);
int nbr_count = get_neighbor_count(v);
for (int j = 0; j < nbr_count; ++j) {
int u = nbr_ptr[j];
if (u > v) {
results.push_back({vector<int>{v, u}});
}
}
}
return convert_results_to_numpy();
}
vector<vector<MotifResult>> thread_results(8);
#ifdef _OPENMP
omp_set_num_threads(8);
#endif
#pragma omp parallel
{
int thread_id = 0;
#ifdef _OPENMP
thread_id = omp_get_thread_num();
#endif
vector<MotifResult>& local_results = thread_results[thread_id];
local_results.reserve(10000);
#pragma omp for schedule(dynamic)
for (int i = 0; i < (int)node_list.size(); ++i) {
int v = node_list[i];
if (!cut_prob.empty() && dist(rng) > cut_prob[0]) {
continue;
}
int local_v_start = v;
vector<int> Vsubgraph;
Vsubgraph.reserve(k);
Vsubgraph.push_back(v);
vector<int> Vextension;
Vextension.reserve(GL.n);
const int* nbr_ptr = get_neighbors_ptr(v);
int nbr_count = get_neighbor_count(v);
for (int j = 0; j < nbr_count; ++j) {
int u = nbr_ptr[j];
if (u > v) {
Vextension.push_back(u);
}
}
vector<int> local_temp_nexcl;
local_temp_nexcl.reserve(GL.n);
bitset<MAX_NODES> local_nvp_bitset;
extend_subgraph_local(Vsubgraph, Vextension, local_v_start, local_temp_nexcl, local_nvp_bitset, local_results);
}
}
for (const auto& tr : thread_results) {
results.insert(results.end(), tr.begin(), tr.end());
}
return convert_results_to_numpy();
}
void extend_subgraph(const vector<int>& Vsubgraph, const vector<int>& Vextension) {
if ((int)Vsubgraph.size() == k) {
results.push_back({Vsubgraph});
return;
}
int original_size = Vextension.size();
for (int i = original_size - 1; i >= 0; --i) {
int w = Vextension[i];
if (w <= v_start) {
continue;
}
vector<int> new_subgraph;
new_subgraph.reserve(Vsubgraph.size() + 1);
new_subgraph.assign(Vsubgraph.begin(), Vsubgraph.end());
new_subgraph.push_back(w);
exclusive_neighborhood(w, Vsubgraph, temp_nexcl);
vector<int> next_ext;
next_ext.reserve(original_size + temp_nexcl.size());
for (int j = 0; j < original_size; ++j) {
if (j != i) {
int u = Vextension[j];
if (u > v_start) {
next_ext.push_back(u);
}
}
}
for (int u : temp_nexcl) {
if (u > v_start) {
next_ext.push_back(u);
}
}
extend_subgraph(new_subgraph, next_ext);
}
}
int count_enumerate() {
int count = 0;
if (k == 2) {
for (int v : node_list) {
const int* nbr_ptr = get_neighbors_ptr(v);
int nbr_count = get_neighbor_count(v);
for (int j = 0; j < nbr_count; ++j) {
int u = nbr_ptr[j];
if (u > v) {
count++;
}
}
}
return count;
}
#ifdef _OPENMP
omp_set_num_threads(8);
#endif
#pragma omp parallel reduction(+:count)
{
#pragma omp for schedule(dynamic)
for (int i = 0; i < (int)node_list.size(); ++i) {
int v = node_list[i];
int local_v_start = v;
vector<int> Vsubgraph;
Vsubgraph.reserve(k);
Vsubgraph.push_back(v);
vector<int> Vextension;
Vextension.reserve(GL.n);
const int* nbr_ptr = get_neighbors_ptr(v);
int nbr_count = get_neighbor_count(v);
for (int j = 0; j < nbr_count; ++j) {
int u = nbr_ptr[j];
if (u > v) {
Vextension.push_back(u);
}
}
vector<int> local_temp_nexcl;
local_temp_nexcl.reserve(GL.n);
bitset<MAX_NODES> local_nvp_bitset;
count += extend_subgraph_count_local(Vsubgraph, Vextension, local_v_start, local_temp_nexcl, local_nvp_bitset);
}
}
return count;
}
py::list convert_results_to_python() {
py::list py_results;
for (const auto& result : results) {
py::set py_set;
for (int node_id : result.nodes) {
if (node_id > 0 && node_id < (int)id_to_node.size()) {
py_set.add(py::cast(id_to_node[node_id]));
}
}
py_results.append(py_set);
}
return py_results;
}
py::array_t<int> convert_results_to_numpy() {
size_t num_results = results.size();
size_t k_size = k;
int* data = new int[num_results * k_size];
size_t idx = 0;
for (const auto& result : results) {
for (size_t i = 0; i < k_size; ++i) {
if (i < result.nodes.size() && result.nodes[i] > 0 && result.nodes[i] < (int)id_to_node.size()) {
data[idx++] = id_to_node[result.nodes[i]];
} else {
data[idx++] = -1;
}
}
}
// Create a capsule to manage the memory
py::capsule capsule(data, [](void* ptr) {
delete[] static_cast<int*>(ptr);
});
// Create numpy array with the capsule
py::array_t<int> result_array = py::array_t<int>(
{num_results, k_size}, // shape
{k_size * sizeof(int), sizeof(int)}, // strides
data,
capsule
);
return result_array;
}
const vector<MotifResult>& get_results() const {
return results;
}
size_t get_k() const {
return k;
}
};
py::list cpp_enumerate_subgraph(py::object G, int k) {
Graph& G_ = G.cast<Graph&>();
Graph_L GL = G_.linkgraph_structure;
py::dict id_to_node_py = G_.id_to_node;
vector<int> id_to_node_vec;
id_to_node_vec.reserve(id_to_node_py.size() + 1);
id_to_node_vec.push_back(0);
for (int i = 1; i <= (int)id_to_node_py.size(); ++i) {
py::object node_obj = id_to_node_py[py::cast(i)];
id_to_node_vec.push_back(node_obj.cast<int>());
}
MotifEnumerator enumerator(GL, k, id_to_node_vec);
enumerator.enumerate();
return enumerator.convert_results_to_python();
}
py::int_ cpp_count_enumerate_subgraph(py::object G, int k) {
Graph& G_ = G.cast<Graph&>();
Graph_L GL = G_.linkgraph_structure;
py::dict id_to_node_py = G_.id_to_node;
vector<int> id_to_node_vec;
id_to_node_vec.reserve(id_to_node_py.size() + 1);
id_to_node_vec.push_back(0);
for (int i = 1; i <= (int)id_to_node_py.size(); ++i) {
py::object node_obj = id_to_node_py[py::cast(i)];
id_to_node_vec.push_back(node_obj.cast<int>());
}
MotifEnumerator enumerator(GL, k, id_to_node_vec);
int count = enumerator.count_enumerate();
return count;
}
py::list cpp_random_enumerate_subgraph(py::object G, int k, py::object cut_prob) {
Graph& G_ = G.cast<Graph&>();
Graph_L GL = G_.linkgraph_structure;
py::dict id_to_node_py = G_.id_to_node;
vector<int> id_to_node_vec;
id_to_node_vec.reserve(id_to_node_py.size() + 1);
id_to_node_vec.push_back(0);
for (int i = 1; i <= (int)id_to_node_py.size(); ++i) {
py::object node_obj = id_to_node_py[py::cast(i)];
id_to_node_vec.push_back(node_obj.cast<int>());
}
vector<double> cut_prob_vec;
if (py::isinstance<py::list>(cut_prob)) {
py::list cut_prob_list = cut_prob.cast<py::list>();
cut_prob_vec.reserve(cut_prob_list.size());
for (size_t i = 0; i < cut_prob_list.size(); ++i) {
cut_prob_vec.push_back(cut_prob_list[i].cast<double>());
}
} else {
throw std::runtime_error("cut_prob must be a list");
}
if (cut_prob_vec.size() != k) {
throw py::value_error("length of cut_prob invalid, should equal to k");
}
MotifEnumerator enumerator(GL, k, id_to_node_vec, cut_prob_vec, 42);
enumerator.enumerate();
return enumerator.convert_results_to_python();
}
py::list cpp_random_enumerate_subgraph_with_seed(py::object G, int k, py::object cut_prob, int seed) {
Graph& G_ = G.cast<Graph&>();
Graph_L GL = G_.linkgraph_structure;
py::dict id_to_node_py = G_.id_to_node;
vector<int> id_to_node_vec;
id_to_node_vec.reserve(id_to_node_py.size() + 1);
id_to_node_vec.push_back(0);
for (int i = 1; i <= (int)id_to_node_py.size(); ++i) {
py::object node_obj = id_to_node_py[py::cast(i)];
id_to_node_vec.push_back(node_obj.cast<int>());
}
vector<double> cut_prob_vec;
if (py::isinstance<py::list>(cut_prob)) {
py::list cut_prob_list = cut_prob.cast<py::list>();
cut_prob_vec.reserve(cut_prob_list.size());
for (size_t i = 0; i < cut_prob_list.size(); ++i) {
cut_prob_vec.push_back(cut_prob_list[i].cast<double>());
}
} else {
throw std::runtime_error("cut_prob must be a list");
}
if (cut_prob_vec.size() != k) {
throw py::value_error("length of cut_prob invalid, should equal to k");
}
MotifEnumerator enumerator(GL, k, id_to_node_vec, cut_prob_vec, seed);
enumerator.enumerate();
return enumerator.convert_results_to_python();
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
py::list cpp_enumerate_subgraph(py::object G, int k);
py::int_ cpp_count_enumerate_subgraph(py::object G, int k);
py::list cpp_random_enumerate_subgraph(py::object G, int k, py::object cut_prob);
@@ -0,0 +1,179 @@
#include "subgraph.h"
#include "../../classes/graph.h"
#include "../../classes/directed_graph.h"
#include "../../common/utils.h"
static node_t _add_one_node_for_subgraph(Graph& self, py::object one_node_for_adding, py::object node_attr = py::dict()) {
node_t id;
if (self.node_to_id.contains(one_node_for_adding)) {
id = self.node_to_id[one_node_for_adding].cast<node_t>();
} else {
id = ++(self.id);
self.id_to_node[py::cast(id)] = one_node_for_adding;
self.node_to_id[one_node_for_adding] = id;
}
py::list items = py::list(node_attr.attr("items")());
self.node[id] = node_attr_dict_factory();
for (int i = 0; i < len(items); i++) {
py::tuple kv = items[i].cast<py::tuple>();
py::object pkey = kv[0];
std::string weight_key = weight_to_string(pkey);
weight_t value = kv[1].cast<weight_t>();
self.node[id].insert(std::make_pair(weight_key, value));
}
return id;
}
static void _add_one_edge_for_subgraph(Graph& self, node_t u, node_t v, py::object edge_attr) {
py::list items = py::list(edge_attr.attr("items")());
self.adj[u][v] = node_attr_dict_factory();
self.adj[v][u] = node_attr_dict_factory();
for (int i = 0; i < len(items); i++) {
py::tuple kv = items[i].cast<py::tuple>();
py::object pkey = kv[0];
std::string weight_key = weight_to_string(pkey);
weight_t value = kv[1].cast<weight_t>();
self.adj[u][v].insert(std::make_pair(weight_key, value));
self.adj[v][u].insert(std::make_pair(weight_key, value));
}
}
static py::object _nodes_subgraph_cpp_graph(py::object self, std::vector<node_t>& node_ids) {
Graph& self_ = self.cast<Graph&>();
py::object G = self.attr("__class__")();
Graph& G_ = G.cast<Graph&>();
G_.graph.attr("update")(self_.graph);
py::object nodes = self.attr("nodes");
py::object adj = self.attr("adj");
// 修复:维护原图内部索引到新图内部索引的映射
std::unordered_map<node_t, node_t> old_id_to_new_id;
// 第一遍:添加所有节点并建立映射
for (node_t node_id : node_ids) {
py::object node = self_.id_to_node[py::cast(node_id)];
py::object node_attr = nodes[node];
node_t new_id = _add_one_node_for_subgraph(G_, node, node_attr);
old_id_to_new_id[node_id] = new_id;
}
// 第二遍:添加边(使用映射后的索引)
for (node_t node_id : node_ids) {
py::object node = self_.id_to_node[py::cast(node_id)];
py::object out_edges = adj[node];
py::list edge_items = py::list(out_edges.attr("items")());
// 获取当前节点的新索引
node_t new_u = old_id_to_new_id[node_id];
for (int j = 0; j < py::len(edge_items); j++) {
py::tuple item = edge_items[j].cast<py::tuple>();
py::object v = item[0];
py::object edge_attr = item[1];
if (self_.node_to_id.contains(v)) {
node_t v_id = self_.node_to_id[v].cast<node_t>();
bool v_in_subgraph = std::find(node_ids.begin(), node_ids.end(), v_id) != node_ids.end();
if (v_in_subgraph) {
// 使用映射后的索引
node_t new_v = old_id_to_new_id[v_id];
_add_one_edge_for_subgraph(G_, new_u, new_v, edge_attr);
}
}
}
}
return G;
}
static node_t DiGraph_add_one_node_for_subgraph(DiGraph& self, py::object one_node_for_adding, py::object node_attr = py::dict()) {
node_t id;
if (self.node_to_id.contains(one_node_for_adding)) {
id = self.node_to_id[one_node_for_adding].cast<node_t>();
} else {
id = ++(self.id);
self.id_to_node[py::cast(id)] = one_node_for_adding;
self.node_to_id[one_node_for_adding] = id;
}
py::list items = py::list(node_attr.attr("items")());
self.node[id] = node_attr_dict_factory();
for (int i = 0; i < len(items); i++) {
py::tuple kv = items[i].cast<py::tuple>();
py::object pkey = kv[0];
std::string weight_key = weight_to_string(pkey);
weight_t value = kv[1].cast<weight_t>();
self.node[id].insert(std::make_pair(weight_key, value));
}
return id;
}
static void DiGraph_add_one_edge_for_subgraph(DiGraph& self, node_t u, node_t v, py::object edge_attr) {
py::list items = py::list(edge_attr.attr("items")());
self.adj[u][v] = node_attr_dict_factory();
for (int i = 0; i < len(items); i++) {
py::tuple kv = items[i].cast<py::tuple>();
py::object pkey = kv[0];
std::string weight_key = weight_to_string(pkey);
weight_t value = kv[1].cast<weight_t>();
self.adj[u][v].insert(std::make_pair(weight_key, value));
}
}
static py::object _nodes_subgraph_cpp_digraph(py::object self, std::vector<node_t>& node_ids) {
DiGraph& self_ = self.cast<DiGraph&>();
py::object G = self.attr("__class__")();
DiGraph& G_ = G.cast<DiGraph&>();
G_.graph.attr("update")(self_.graph);
py::object nodes = self.attr("nodes");
py::object adj = self.attr("adj");
// 修复:维护原图内部索引到新图内部索引的映射
std::unordered_map<node_t, node_t> old_id_to_new_id;
// 第一遍:添加所有节点并建立映射
for (node_t node_id : node_ids) {
py::object node = self_.id_to_node[py::cast(node_id)];
py::object node_attr = nodes[node];
node_t new_id = DiGraph_add_one_node_for_subgraph(G_, node, node_attr);
old_id_to_new_id[node_id] = new_id;
}
// 第二遍:添加边(使用映射后的索引)
for (node_t node_id : node_ids) {
py::object node = self_.id_to_node[py::cast(node_id)];
py::object out_edges = adj[node];
py::list edge_items = py::list(out_edges.attr("items")());
// 获取当前节点的新索引
node_t new_u = old_id_to_new_id[node_id];
for (int j = 0; j < py::len(edge_items); j++) {
py::tuple item = edge_items[j].cast<py::tuple>();
py::object v = item[0];
py::object edge_attr = item[1];
if (self_.node_to_id.contains(v)) {
node_t v_id = self_.node_to_id[v].cast<node_t>();
bool v_in_subgraph = std::find(node_ids.begin(), node_ids.end(), v_id) != node_ids.end();
if (v_in_subgraph) {
// 使用映射后的索引
node_t new_v = old_id_to_new_id[v_id];
DiGraph_add_one_edge_for_subgraph(G_, new_u, new_v, edge_attr);
}
}
}
}
return G;
}
py::object nodes_subgraph_cpp(py::object self, std::vector<node_t>& node_ids) {
bool is_directed = self.attr("is_directed")().cast<bool>();
if (is_directed) {
return _nodes_subgraph_cpp_digraph(self, node_ids);
} else {
return _nodes_subgraph_cpp_graph(self, node_ids);
}
}
@@ -0,0 +1,10 @@
#pragma once
#include <vector>
#include <memory>
#include "../../common/common.h"
#include "../../classes/graph.h"
#include "../../classes/directed_graph.h"
#include "../../classes/csr_graph.h"
py::object nodes_subgraph_cpp(py::object self, std::vector<node_t>& node_ids);
@@ -0,0 +1 @@
# TEST package init
@@ -0,0 +1,139 @@
import unittest
import easygraph as eg
def _get_cpp_module():
try:
import cpp_easygraph
return cpp_easygraph
except ImportError:
return None
def _to_undirected_cpp(G_cpp_digraph):
import cpp_easygraph
G_cpp = cpp_easygraph.Graph()
G_cpp.graph.update(G_cpp_digraph.graph)
for node, node_attr in G_cpp_digraph.nodes.items():
G_cpp.add_node(node, **node_attr)
seen_edges = set()
for u, v, edge_data in G_cpp_digraph.edges:
edge = (min(u, v), max(u, v))
if edge not in seen_edges:
seen_edges.add(edge)
G_cpp.add_edge(u, v, **edge_data)
return G_cpp
class TestLPA(unittest.TestCase):
def setUp(self):
self.G_simple = eg.Graph()
self.G_simple.add_edges_from([(0, 1), (1, 2), (3, 4)])
self.G_weighted = eg.Graph()
self.G_weighted.add_edges_from([
(0, 1, {"weight": 3}),
(1, 2, {"weight": 2}),
(2, 0, {"weight": 4}),
(3, 4, {"weight": 1}),
])
self.G_disconnected = eg.Graph()
self.G_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
self.G_single = eg.Graph()
self.G_single.add_node(42)
self.G_empty = eg.Graph()
def _get_cpp_module(self):
return _get_cpp_module()
def test_lpa_simple(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
result = cpeg.cpp_LPA(self.G_simple.cpp())
self.assertIsInstance(result, dict)
all_nodes = set()
for community in result.values():
all_nodes.update(community)
self.assertEqual(all_nodes, set(self.G_simple.nodes))
def test_lpa_weighted(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
result = cpeg.cpp_LPA(self.G_weighted.cpp())
self.assertIsInstance(result, dict)
all_nodes = set(self.G_weighted.nodes)
result_nodes = set()
for community in result.values():
result_nodes.update(community)
self.assertEqual(all_nodes, result_nodes)
def test_lpa_disconnected(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
result = cpeg.cpp_LPA(self.G_disconnected.cpp())
self.assertIsInstance(result, dict)
all_nodes = set(self.G_disconnected.nodes)
result_nodes = set()
for community in result.values():
result_nodes.update(community)
self.assertEqual(all_nodes, result_nodes)
def test_lpa_single_node(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
result = cpeg.cpp_LPA(self.G_single.cpp())
self.assertIsInstance(result, dict)
self.assertEqual(len(result), 1)
for community in result.values():
self.assertIn(42, community)
def test_lpa_empty_graph(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
result = cpeg.cpp_LPA(self.G_empty.cpp())
self.assertIsInstance(result, dict)
self.assertEqual(result, {})
def test_python_cpp_consistency(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
result_cpp = cpeg.cpp_LPA(self.G_simple.cpp())
result_py = eg.functions.community.LPA(self.G_simple)
self.assertEqual(len(result_cpp), len(result_py))
cpp_nodes = set()
for community in result_cpp.values():
cpp_nodes.update(community)
py_nodes = set()
for community in result_py.values():
py_nodes.update(community)
self.assertEqual(cpp_nodes, py_nodes)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,254 @@
import unittest
import easygraph as eg
def _get_cpp_module():
"""获取cpp_easygraph模块"""
try:
import cpp_easygraph
return cpp_easygraph
except ImportError:
return None
def _to_undirected_cpp(G_cpp_digraph):
"""将C++ DiGraph转换为C++ Graph无向图"""
import cpp_easygraph
G_cpp = cpp_easygraph.Graph()
G_cpp.graph.update(G_cpp_digraph.graph)
for node, node_attr in G_cpp_digraph.nodes.items():
G_cpp.add_node(node, **node_attr)
seen_edges = set()
for u, v, edge_data in G_cpp_digraph.edges:
edge = (min(u, v), max(u, v))
if edge not in seen_edges:
seen_edges.add(edge)
G_cpp.add_edge(u, v, **edge_data)
return G_cpp
def _csr_dict_to_graph(csr_dict):
import cpp_easygraph
if not csr_dict or 'nodes' not in csr_dict:
return cpp_easygraph.Graph()
G = cpp_easygraph.Graph()
nodes = csr_dict['nodes']
for node in nodes:
G.add_node(node)
V = csr_dict['V']
E = csr_dict['E']
W = csr_dict.get('W', [1.0] * len(E))
for u in range(len(V) - 1):
start = V[u]
end = V[u + 1]
for i in range(start, end):
v_idx = E[i]
weight = W[i] if i < len(W) else 1.0
G.add_edge(nodes[u], nodes[v_idx], weight=weight)
return G
class TestEgoGraph(unittest.TestCase):
def setUp(self):
self.simple_graph = eg.Graph()
self.simple_graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
self.directed_graph = eg.DiGraph()
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
self.weighted_graph = eg.Graph()
self.weighted_graph.add_edges_from(
[(0, 1, {"weight": 1}), (1, 2, {"weight": 2}), (2, 3, {"weight": 3})]
)
self.disconnected_graph = eg.Graph()
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
self.single_node_graph = eg.Graph()
self.single_node_graph.add_node(42)
def _get_cpp_module(self):
return _get_cpp_module()
def test_ego_graph_simple_radius_1(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.simple_graph.cpp()
ego = cpeg.cpp_ego_graph(G_cpp, 2, radius=1)
self.assertIsInstance(ego, type(G_cpp))
self.assertIn(2, ego.nodes)
self.assertIn(1, ego.nodes)
self.assertIn(3, ego.nodes)
def test_ego_graph_simple_radius_2(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.simple_graph.cpp()
ego = cpeg.cpp_ego_graph(G_cpp, 2, radius=2)
self.assertIsInstance(ego, type(G_cpp))
self.assertIn(0, ego.nodes)
self.assertIn(1, ego.nodes)
self.assertIn(2, ego.nodes)
self.assertIn(3, ego.nodes)
self.assertIn(4, ego.nodes)
def test_ego_graph_directed(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.directed_graph.cpp()
ego = cpeg.cpp_ego_graph(G_cpp, 1, radius=1)
self.assertIsInstance(ego, type(G_cpp))
self.assertIn(1, ego.nodes)
self.assertIn(2, ego.nodes)
def test_ego_graph_weighted_with_distance(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.weighted_graph.cpp()
G_cpp.generate_linkgraph('weight')
ego = cpeg.cpp_ego_graph(G_cpp, 0, radius=2, distance="weight")
self.assertIsInstance(ego, type(G_cpp))
self.assertIn(0, ego.nodes)
self.assertIn(1, ego.nodes)
def test_ego_graph_disconnected(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.disconnected_graph.cpp()
ego = cpeg.cpp_ego_graph(G_cpp, 0, radius=1)
self.assertIsInstance(ego, type(G_cpp))
self.assertIn(0, ego.nodes)
self.assertIn(1, ego.nodes)
def test_ego_graph_single_node(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.single_node_graph.cpp()
ego = cpeg.cpp_ego_graph(G_cpp, 42, radius=1)
self.assertIsInstance(ego, type(G_cpp))
self.assertIn(42, ego.nodes)
def test_ego_graph_center_false(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.simple_graph.cpp()
ego = cpeg.cpp_ego_graph(G_cpp, 2, radius=1, center=False)
self.assertIsInstance(ego, type(G_cpp))
self.assertNotIn(2, ego.nodes)
self.assertIn(1, ego.nodes)
self.assertIn(3, ego.nodes)
def test_python_cpp_consistency(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.simple_graph.cpp()
ego_py = eg.functions.community.ego_graph(self.simple_graph, 2, radius=1)
ego_cpp = cpeg.cpp_ego_graph(G_cpp, 2, radius=1)
self.assertEqual(set(ego_py.nodes), set(ego_cpp.nodes))
class TestEgoGraphCSR(unittest.TestCase):
def setUp(self):
self.simple_graph = eg.Graph()
self.simple_graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
self.disconnected_graph = eg.Graph()
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
self.single_node_graph = eg.Graph()
self.single_node_graph.add_node(42)
def _get_cpp_module(self):
return _get_cpp_module()
def test_ego_graph_csr_simple(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.simple_graph.cpp()
G_cpp.generate_linkgraph('weight')
ego_csr_dict = cpeg.cpp_ego_graph_csr(G_cpp, 2, radius=1)
ego = _csr_dict_to_graph(ego_csr_dict)
self.assertTrue(hasattr(ego, 'nodes'))
self.assertIn(2, ego.nodes)
def test_ego_graph_csr_disconnected(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.disconnected_graph.cpp()
G_cpp.generate_linkgraph('weight')
ego_csr_dict = cpeg.cpp_ego_graph_csr(G_cpp, 0, radius=1)
ego = _csr_dict_to_graph(ego_csr_dict)
self.assertTrue(hasattr(ego, 'nodes'))
self.assertIn(0, ego.nodes)
self.assertIn(1, ego.nodes)
def test_ego_graph_csr_single_node(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.single_node_graph.cpp()
G_cpp.generate_linkgraph('weight')
ego_csr_dict = cpeg.cpp_ego_graph_csr(G_cpp, 42, radius=1)
ego = _csr_dict_to_graph(ego_csr_dict)
self.assertTrue(hasattr(ego, 'nodes'))
self.assertIn(42, ego.nodes)
def test_csr_vs_original_consistency(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.simple_graph.cpp()
G_cpp.generate_linkgraph('weight')
ego_original = cpeg.cpp_ego_graph(G_cpp, 2, radius=1, undirected=False)
ego_csr_dict = cpeg.cpp_ego_graph_csr(G_cpp, 2, radius=1)
ego_csr = _csr_dict_to_graph(ego_csr_dict)
self.assertIn(2, ego_original.nodes)
self.assertIn(2, ego_csr.nodes)
self.assertTrue(set(ego_csr.nodes).issubset(set(ego_original.nodes) | {4}))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,143 @@
import random
import unittest
import easygraph as eg
def _get_cpp_module():
try:
import cpp_easygraph
return cpp_easygraph
except ImportError:
return None
def _to_undirected_cpp(G_cpp_digraph):
import cpp_easygraph
G_cpp = cpp_easygraph.Graph()
G_cpp.graph.update(G_cpp_digraph.graph)
for node, node_attr in G_cpp_digraph.nodes.items():
G_cpp.add_node(node, **node_attr)
seen_edges = set()
for u, v, edge_data in G_cpp_digraph.edges:
edge = (min(u, v), max(u, v))
if edge not in seen_edges:
seen_edges.add(edge)
G_cpp.add_edge(u, v, **edge_data)
return G_cpp
class TestEnumerateSubgraph(unittest.TestCase):
def setUp(self):
self.G_triangle = eg.Graph()
self.G_triangle.add_edges_from([(1, 2), (2, 3), (3, 1)])
self.G_complex = eg.Graph()
self.G_complex.add_edges_from([
(1, 3), (2, 3), (3, 4), (4, 5), (3, 5)
])
self.G_empty = eg.Graph()
self.G_small = eg.Graph()
self.G_small.add_edges_from([(1, 2)])
def _get_cpp_module(self):
return _get_cpp_module()
def test_enumerate_subgraph_k3(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
motifs = cpeg.cpp_enumerate_subgraph(self.G_complex.cpp(), 3)
self.assertIsInstance(motifs, list)
for m in motifs:
self.assertEqual(len(m), 3)
def test_enumerate_subgraph_k4(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
motifs = cpeg.cpp_enumerate_subgraph(self.G_complex.cpp(), 4)
for m in motifs:
self.assertEqual(len(m), 4)
def test_enumerate_subgraph_empty_graph(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
motifs = cpeg.cpp_enumerate_subgraph(self.G_empty.cpp(), 3)
self.assertEqual(motifs, [])
def test_enumerate_subgraph_k_larger_than_graph(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
motifs = cpeg.cpp_enumerate_subgraph(self.G_small.cpp(), 3)
self.assertEqual(motifs, [])
def test_python_cpp_consistency(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
motifs_py = eg.enumerate_subgraph(self.G_complex, 3)
motifs_py = [frozenset(m) for m in motifs_py]
motifs_cpp = cpeg.cpp_enumerate_subgraph(self.G_complex.cpp(), 3)
motifs_cpp = [frozenset(m) for m in motifs_cpp]
self.assertEqual(set(motifs_py), set(motifs_cpp))
class TestRandomEnumerateSubgraph(unittest.TestCase):
def setUp(self):
self.G_complex = eg.Graph()
self.G_complex.add_edges_from([
(1, 3), (2, 3), (3, 4), (4, 5), (3, 5)
])
self.G_empty = eg.Graph()
def _get_cpp_module(self):
return _get_cpp_module()
def test_random_enumerate_valid_cut_prob(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
cut_prob = [1.0, 1.0, 1.0]
motifs = cpeg.cpp_random_enumerate_subgraph(self.G_complex.cpp(), 3, cut_prob)
self.assertIsInstance(motifs, list)
def test_random_enumerate_zero_cut_prob(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
cut_prob = [0.0, 0.0, 0.0]
motifs = cpeg.cpp_random_enumerate_subgraph(self.G_complex.cpp(), 3, cut_prob)
self.assertEqual(motifs, [])
def test_random_enumerate_different_results(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
cut_prob = [1.0, 1.0, 1.0]
motifs1 = cpeg.cpp_random_enumerate_subgraph(self.G_complex.cpp(), 3, cut_prob)
motifs2 = cpeg.cpp_random_enumerate_subgraph(self.G_complex.cpp(), 3, cut_prob)
self.assertIsInstance(motifs1, list)
self.assertIsInstance(motifs2, list)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,117 @@
import unittest
import easygraph as eg
def _get_cpp_module():
try:
import cpp_easygraph
return cpp_easygraph
except ImportError:
return None
def _to_undirected_cpp(G_cpp_digraph):
import cpp_easygraph
G_cpp = cpp_easygraph.Graph()
G_cpp.graph.update(G_cpp_digraph.graph)
for node, node_attr in G_cpp_digraph.nodes.items():
G_cpp.add_node(node, **node_attr)
seen_edges = set()
for u, v, edge_data in G_cpp_digraph.edges:
edge = (min(u, v), max(u, v))
if edge not in seen_edges:
seen_edges.add(edge)
G_cpp.add_edge(u, v, **edge_data)
return G_cpp
class TestGreedyModularity(unittest.TestCase):
def setUp(self):
self.G_simple = eg.Graph()
self.G_simple.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)])
self.G_disconnected = eg.Graph()
self.G_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
self.G_weighted = eg.Graph()
self.G_weighted.add_edge(0, 1, weight=5)
self.G_weighted.add_edge(1, 2, weight=3)
self.G_weighted.add_edge(2, 0, weight=2)
self.G_weighted.add_edge(3, 4, weight=1)
self.G_single = eg.Graph()
self.G_single.add_node(42)
self.G_empty = eg.Graph()
def _get_cpp_module(self):
return _get_cpp_module()
def test_greedy_simple(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_greedy_modularity_communities(self.G_simple.cpp())
self.assertIsInstance(communities, list)
flat = {node for comm in communities for node in comm}
self.assertSetEqual(flat, set(self.G_simple.nodes))
def test_greedy_weighted(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_greedy_modularity_communities(self.G_weighted.cpp(), weight="weight")
self.assertIsInstance(communities, list)
flat = {node for comm in communities for node in comm}
self.assertSetEqual(flat, set(self.G_weighted.nodes))
def test_greedy_disconnected(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_greedy_modularity_communities(self.G_disconnected.cpp())
self.assertIsInstance(communities, list)
flat = {node for comm in communities for node in comm}
self.assertSetEqual(flat, set(self.G_disconnected.nodes))
def test_greedy_single_node(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_greedy_modularity_communities(self.G_single.cpp())
self.assertEqual(len(communities), 1)
self.assertIn(42, communities[0])
def test_greedy_empty_graph(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_greedy_modularity_communities(self.G_empty.cpp())
self.assertEqual(communities, [])
def test_python_cpp_consistency(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities_cpp = cpeg.cpp_greedy_modularity_communities(self.G_simple.cpp())
communities_py = eg.functions.community.greedy_modularity_communities(self.G_simple)
self.assertEqual(len(communities_cpp), len(communities_py))
flat_cpp = {node for comm in communities_cpp for node in comm}
flat_py = {node for comm in communities_py for node in comm}
self.assertEqual(flat_cpp, flat_py)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,188 @@
import unittest
import easygraph as eg
def _get_cpp_module():
try:
import cpp_easygraph
return cpp_easygraph
except ImportError:
return None
def _to_undirected_cpp(G_cpp_digraph):
import cpp_easygraph
G_cpp = cpp_easygraph.Graph()
G_cpp.graph.update(G_cpp_digraph.graph)
for node, node_attr in G_cpp_digraph.nodes.items():
G_cpp.add_node(node, **node_attr)
seen_edges = set()
for u, v, edge_data in G_cpp_digraph.edges:
edge = (min(u, v), max(u, v))
if edge not in seen_edges:
seen_edges.add(edge)
G_cpp.add_edge(u, v, **edge_data)
return G_cpp
class TestLocalsearch(unittest.TestCase):
def setUp(self):
self.G_simple = eg.Graph()
self.G_simple.add_edges_from([(0, 2), (0, 3), (0, 4), (0, 5), (1, 2), (1, 3), (1, 4), (1, 5)])
self.G_disconnected = eg.Graph()
self.G_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
self.G_single = eg.Graph()
self.G_single.add_node(42)
self.G_empty = eg.Graph()
def _get_cpp_module(self):
return _get_cpp_module()
def test_localsearch_simple(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.G_simple.cpp()
G_cpp.generate_linkgraph('weight')
result = cpeg.cpp_localsearch(G_cpp, seed=163)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 6)
y_partition = result[3]
self.assertIsInstance(y_partition, dict)
all_nodes = set(self.G_simple.nodes)
result_nodes = set(y_partition.keys())
self.assertEqual(all_nodes, result_nodes)
def test_localsearch_disconnected(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.G_disconnected.cpp()
G_cpp.generate_linkgraph('weight')
result = cpeg.cpp_localsearch(G_cpp, seed=163)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 6)
y_partition = result[3]
self.assertIsInstance(y_partition, dict)
all_nodes = set(self.G_disconnected.nodes)
result_nodes = set(y_partition.keys())
self.assertEqual(all_nodes, result_nodes)
def test_localsearch_single_node(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.G_single.cpp()
G_cpp.generate_linkgraph('weight')
result = cpeg.cpp_localsearch(G_cpp, seed=163)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 6)
y_partition = result[3]
self.assertIsInstance(y_partition, dict)
self.assertIn(42, y_partition)
def test_localsearch_empty_graph(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.G_empty.cpp()
G_cpp.generate_linkgraph('weight')
result = cpeg.cpp_localsearch(G_cpp, seed=163)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 6)
y_partition = result[3]
self.assertIsInstance(y_partition, dict)
def test_localsearch_maximum_tree_true(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.G_simple.cpp()
G_cpp.generate_linkgraph('weight')
result = cpeg.cpp_localsearch(G_cpp, maximum_tree=True, seed=163)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 6)
y_partition = result[3]
self.assertIsInstance(y_partition, dict)
def test_localsearch_maximum_tree_false(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.G_simple.cpp()
G_cpp.generate_linkgraph('weight')
result = cpeg.cpp_localsearch(G_cpp, maximum_tree=False, seed=163)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 6)
y_partition = result[3]
self.assertIsInstance(y_partition, dict)
def test_localsearch_auto_choose_centers(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.G_simple.cpp()
G_cpp.generate_linkgraph('weight')
result = cpeg.cpp_localsearch(G_cpp, auto_choose_centers=True, seed=163)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 6)
y_partition = result[3]
self.assertIsInstance(y_partition, dict)
def test_localsearch_center_num(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.G_simple.cpp()
G_cpp.generate_linkgraph('weight')
result = cpeg.cpp_localsearch(G_cpp, center_num=2, seed=163)
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 6)
y_partition = result[3]
self.assertIsInstance(y_partition, dict)
def test_localsearch_with_seed_reproducibility(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
G_cpp = self.G_simple.cpp()
G_cpp.generate_linkgraph('weight')
result1 = cpeg.cpp_localsearch(G_cpp, seed=42)
result2 = cpeg.cpp_localsearch(G_cpp, seed=42)
self.assertIsInstance(result1, tuple)
self.assertIsInstance(result2, tuple)
y_partition1 = result1[3]
y_partition2 = result2[3]
self.assertEqual(y_partition1.keys(), y_partition2.keys())
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,178 @@
import unittest
import easygraph as eg
def _get_cpp_module():
try:
import cpp_easygraph
return cpp_easygraph
except ImportError:
return None
def _to_undirected_cpp(G_cpp_digraph):
import cpp_easygraph
G_cpp = cpp_easygraph.Graph()
G_cpp.graph.update(G_cpp_digraph.graph)
for node, node_attr in G_cpp_digraph.nodes.items():
G_cpp.add_node(node, **node_attr)
seen_edges = set()
for u, v, edge_data in G_cpp_digraph.edges:
edge = (min(u, v), max(u, v))
if edge not in seen_edges:
seen_edges.add(edge)
G_cpp.add_edge(u, v, **edge_data)
return G_cpp
class TestLouvainCommunities(unittest.TestCase):
def setUp(self):
self.G_simple = eg.Graph()
self.G_simple.add_edges_from([(0, 1), (1, 2), (3, 4)])
self.G_weighted = eg.Graph()
self.G_weighted.add_edge(0, 1, weight=5)
self.G_weighted.add_edge(1, 2, weight=3)
self.G_weighted.add_edge(3, 4, weight=2)
self.G_disconnected = eg.Graph()
self.G_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
self.G_single = eg.Graph()
self.G_single.add_node(42)
self.G_empty = eg.Graph()
def _get_cpp_module(self):
return _get_cpp_module()
def test_louvain_simple(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_louvain_communities(self.G_simple.cpp())
self.assertIsInstance(communities, list)
flat = {node for comm in communities for node in comm}
self.assertSetEqual(flat, set(self.G_simple.nodes))
def test_louvain_weighted(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_louvain_communities(self.G_weighted.cpp(), weight="weight")
self.assertIsInstance(communities, list)
flat = {node for comm in communities for node in comm}
self.assertSetEqual(flat, set(self.G_weighted.nodes))
def test_louvain_disconnected(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_louvain_communities(self.G_disconnected.cpp())
self.assertIsInstance(communities, list)
flat = {node for comm in communities for node in comm}
self.assertSetEqual(flat, set(self.G_disconnected.nodes))
def test_louvain_single_node(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_louvain_communities(self.G_single.cpp())
self.assertEqual(len(communities), 1)
self.assertIn(42, communities[0])
def test_louvain_empty_graph(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_louvain_communities(self.G_empty.cpp())
self.assertEqual(communities, [])
def test_louvain_resolution_parameter(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities1 = cpeg.cpp_louvain_communities(self.G_simple.cpp(), resolution=1.0)
communities2 = cpeg.cpp_louvain_communities(self.G_simple.cpp(), resolution=0.5)
self.assertIsInstance(communities1, list)
self.assertIsInstance(communities2, list)
flat1 = {node for comm in communities1 for node in comm}
flat2 = {node for comm in communities2 for node in comm}
self.assertEqual(flat1, flat2)
def test_python_cpp_consistency(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities_cpp = cpeg.cpp_louvain_communities(self.G_simple.cpp())
communities_py = eg.functions.community.louvain_communities(self.G_simple)
self.assertEqual(len(communities_cpp), len(communities_py))
flat_cpp = {node for comm in communities_cpp for node in comm}
flat_py = {node for comm in communities_py for node in comm}
self.assertEqual(flat_cpp, flat_py)
class TestLouvainCommunitiesSerial(unittest.TestCase):
def setUp(self):
self.G_simple = eg.Graph()
self.G_simple.add_edges_from([(0, 1), (1, 2), (3, 4)])
self.G_weighted = eg.Graph()
self.G_weighted.add_edge(0, 1, weight=5)
self.G_weighted.add_edge(1, 2, weight=3)
self.G_weighted.add_edge(3, 4, weight=2)
def _get_cpp_module(self):
return _get_cpp_module()
def test_louvain_serial_simple(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_louvain_communities_serial(self.G_simple.cpp())
self.assertIsInstance(communities, list)
flat = {node for comm in communities for node in comm}
self.assertSetEqual(flat, set(self.G_simple.nodes))
def test_louvain_serial_weighted(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = cpeg.cpp_louvain_communities_serial(self.G_weighted.cpp(), weight="weight")
self.assertIsInstance(communities, list)
flat = {node for comm in communities for node in comm}
self.assertSetEqual(flat, set(self.G_weighted.nodes))
def test_parallel_vs_serial_consistency(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities_parallel = cpeg.cpp_louvain_communities(self.G_simple.cpp())
communities_serial = cpeg.cpp_louvain_communities_serial(self.G_simple.cpp())
flat_parallel = {frozenset(comm) for comm in communities_parallel}
flat_serial = {frozenset(comm) for comm in communities_serial}
self.assertEqual(flat_parallel, flat_serial)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,161 @@
import unittest
import easygraph as eg
def _get_cpp_module():
"""获取cpp_easygraph模块"""
try:
import cpp_easygraph
return cpp_easygraph
except ImportError:
return None
def _to_undirected_cpp(G_cpp_digraph):
"""将C++ DiGraph转换为C++ Graph无向图"""
import cpp_easygraph
G_cpp = cpp_easygraph.Graph()
G_cpp.graph.update(G_cpp_digraph.graph)
for node, node_attr in G_cpp_digraph.nodes.items():
G_cpp.add_node(node, **node_attr)
seen_edges = set()
for u, v, edge_data in G_cpp_digraph.edges:
edge = (min(u, v), max(u, v))
if edge not in seen_edges:
seen_edges.add(edge)
G_cpp.add_edge(u, v, **edge_data)
return G_cpp
def _communities_to_membership(G, communities):
if not isinstance(communities, list):
communities = list(communities)
N = G.number_of_nodes()
membership = [-1] * N
node_index = G.node_index
for comm_id, community in enumerate(communities):
for node in community:
if node in node_index:
node_id = node_index[node]
membership[node_id] = comm_id
return membership
class TestModularity(unittest.TestCase):
def setUp(self):
self.G = eg.Graph()
self.G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)])
self.DG = eg.DiGraph()
self.DG.add_edges_from([(0, 1), (1, 2), (2, 0)])
self.G_weighted = eg.Graph()
self.G_weighted.add_edge(0, 1, weight=2)
self.G_weighted.add_edge(1, 2, weight=3)
self.G_weighted.add_edge(2, 0, weight=1)
self.G_selfloop = eg.Graph()
self.G_selfloop.add_edges_from([(0, 0), (1, 1), (0, 1)])
self.G_empty = eg.Graph()
self.config = type('Config', (), {
'is_directed': False
})()
def _get_cpp_module(self):
return _get_cpp_module()
def test_undirected_modularity(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = [{0, 1}, {2, 3}]
membership = _communities_to_membership(self.G, communities)
q = cpeg.cpp_modularity(self.G.cpp(), membership)
self.assertIsInstance(q, float)
self.assertGreaterEqual(q, -1.0)
self.assertLessEqual(q, 1.0)
def test_directed_modularity(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = [{0, 1, 2}]
membership = _communities_to_membership(self.DG, communities)
q = cpeg.cpp_modularity(self.DG.cpp(), membership)
self.assertIsInstance(q, float)
def test_weighted_graph(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = [{0, 1}, {2}]
membership = _communities_to_membership(self.G_weighted, communities)
q = cpeg.cpp_modularity(self.G_weighted.cpp(), membership, weight="weight")
self.assertIsInstance(q, float)
def test_self_loops(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = [{0, 1}]
membership = _communities_to_membership(self.G_selfloop, communities)
q = cpeg.cpp_modularity(self.G_selfloop.cpp(), membership)
self.assertIsInstance(q, float)
def test_single_community(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = [{0, 1, 2, 3}]
membership = _communities_to_membership(self.G, communities)
q = cpeg.cpp_modularity(self.G.cpp(), membership)
self.assertIsInstance(q, float)
def test_each_node_its_own_community(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities = [{0}, {1}, {2}, {3}]
membership = _communities_to_membership(self.G, communities)
q = cpeg.cpp_modularity(self.G.cpp(), membership)
self.assertIsInstance(q, float)
def test_empty_community_list(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
q = cpeg.cpp_modularity(self.G.cpp(), [])
self.assertEqual(q, 0.0)
def test_python_cpp_consistency(self):
cpeg = self._get_cpp_module()
if cpeg is None:
self.skipTest("cpp_easygraph module not available")
communities_py = [{0, 1}, {2, 3}]
q_py = eg.functions.community.modularity(self.G, communities_py)
q_cpp = cpeg.cpp_modularity(self.G.cpp(), communities_py)
self.assertAlmostEqual(q_py, q_cpp, places=5)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,55 @@
import unittest
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from cpp_modularity_test import TestModularity
from cpp_greedy_modularity_test import TestGreedyModularity
from cpp_enumerate_subgraph_test import (
TestEnumerateSubgraph,
TestRandomEnumerateSubgraph
)
from cpp_louvain_test import TestLouvainCommunities, TestLouvainCommunitiesSerial
from cpp_LPA_test import TestLPA
from cpp_ego_graph_test import TestEgoGraph, TestEgoGraphCSR
from cpp_localsearch_test import TestLocalsearch
def create_test_suite():
suite = unittest.TestSuite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestModularity))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestGreedyModularity))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestEnumerateSubgraph))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestRandomEnumerateSubgraph))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestLouvainCommunities))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestLouvainCommunitiesSerial))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestLPA))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestEgoGraph))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestEgoGraphCSR))
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestLocalsearch))
return suite
if __name__ == "__main__":
print("=" * 70)
print("Easy-Graph C++ Module Test Suite")
print("=" * 70)
print()
suite = create_test_suite()
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
print()
print("=" * 70)
print("Test Summary")
print("=" * 70)
print(f"Tests run: {result.testsRun}")
print(f"Successes: {result.testsRun - len(result.failures) - len(result.errors)}")
print(f"Failures: {len(result.failures)}")
print(f"Errors: {len(result.errors)}")
print(f"Skipped: {len(result.skipped)}")
print("=" * 70)
sys.exit(0 if result.wasSuccessful() else 1)