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
@@ -0,0 +1 @@
#include "centrality.h"
@@ -0,0 +1,467 @@
#ifdef _OPENMP
#include <omp.h>
#endif
#include <vector>
#include <queue>
#include <limits.h>
#include <algorithm>
#include <string>
#include <cstdio>
#include "centrality.h"
#ifdef EASYGRAPH_ENABLE_GPU
#include <gpu_easygraph.h>
#endif
#include "../../classes/graph.h"
#include "../../common/utils.h"
#include "../../classes/linkgraph.h"
namespace py = pybind11;
void betweenness_bfs_worker(
const Graph_L& G_l, const int& S, std::vector<double>& bc, int cutoff, int endpoints_,
std::vector<int>& q, std::vector<int>& dis, std::vector<int>& head_path, std::vector<int>& St,
std::vector<long long>& count_path, std::vector<double>& delta, std::vector<LinkEdge>& E_path,
std::vector<int>& stamp, int& cur_stamp
) {
int N = G_l.n;
int edge_number_path = 0;
int cnt_St = 0;
++cur_stamp;
if ((int)q.size() < N + 1)
q.resize(N + 1);
int front = 0, back = 0;
int cutoff_int = (cutoff < 0) ? -1 : cutoff;
stamp[S] = cur_stamp;
dis[S] = 0;
count_path[S] = 1;
delta[S] = 0.0;
head_path[S] = 0;
q[back++] = S;
const std::vector<int>& head = G_l.head;
const std::vector<LinkEdge>& E = G_l.edges;
while (front < back) {
int u = q[front++];
int du = dis[u];
if (cutoff_int >= 0 && du > cutoff_int)
break;
St[cnt_St++] = u;
for (int p = head[u]; p != -1; p = E[p].next) {
int v = E[p].to;
int new_dis = du + 1;
if (cutoff_int >= 0 && new_dis > cutoff_int)
continue;
if (stamp[v] != cur_stamp) {
stamp[v] = cur_stamp;
dis[v] = new_dis;
count_path[v] = count_path[u];
delta[v] = 0.0;
head_path[v] = 0;
q[back++] = v;
E_path[++edge_number_path].next = head_path[v];
E_path[edge_number_path].to = u;
head_path[v] = edge_number_path;
} else if (dis[v] == new_dis) {
count_path[v] += count_path[u];
E_path[++edge_number_path].next = head_path[v];
E_path[edge_number_path].to = u;
head_path[v] = edge_number_path;
}
}
}
if (endpoints_)
bc[S] += cnt_St - 1;
while (cnt_St > 0) {
int u = St[--cnt_St];
double cu = count_path[u];
if (cu != 0) {
double coeff = (1.0 + delta[u]) / cu;
for (int p = head_path[u]; p; p = E_path[p].next) {
int w = E_path[p].to;
delta[w] += count_path[w] * coeff;
}
}
if (u != S)
bc[u] += delta[u] + endpoints_;
}
}
void betweenness_dijkstra_worker(
const Graph_L& G_l, const int& S, std::vector<double>& bc, double cutoff,
std::vector<int>& dis, std::vector<int>& head_path,
std::vector<int>& St, std::vector<long long>& count_path, std::vector<double>& delta,
std::vector<LinkEdge>& E_path, int endpoints_,
std::vector<int>& stamp, int& cur_stamp
) {
const int dis_inf = 0x3f3f3f3f;
int N = G_l.n;
int edge_number_path = 0;
int cnt_St = 0;
++cur_stamp;
stamp[S] = cur_stamp;
dis[S] = 0;
count_path[S] = 1;
delta[S] = 0.0;
head_path[S] = 0;
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> pq;
pq.push({0, S});
const std::vector<int>& head = G_l.head;
const std::vector<LinkEdge>& E = G_l.edges;
while (!pq.empty()) {
std::pair<int, int> top = pq.top();
pq.pop();
int d = top.first;
int u = top.second;
if (d > dis[u]) continue;
if (cutoff >= 0 && d > cutoff) continue;
St[cnt_St++] = u;
for (int p = head[u]; p != -1; p = E[p].next) {
int v = E[p].to;
int w = E[p].w;
int nd = dis[u] + w;
if (cutoff >= 0 && nd > cutoff) continue;
bool first_visit = (stamp[v] != cur_stamp);
if (first_visit || dis[v] > nd) {
if (first_visit) {
stamp[v] = cur_stamp;
delta[v] = 0.0;
}
dis[v] = nd;
count_path[v] = count_path[u];
head_path[v] = 0;
E_path[++edge_number_path].next = head_path[v];
E_path[edge_number_path].to = u;
head_path[v] = edge_number_path;
pq.push({nd, v});
} else if (dis[v] == nd) {
count_path[v] += count_path[u];
E_path[++edge_number_path].next = head_path[v];
E_path[edge_number_path].to = u;
head_path[v] = edge_number_path;
}
}
}
if (endpoints_)
bc[S] += cnt_St - 1;
while (cnt_St > 0) {
int u = St[--cnt_St];
double cu = count_path[u];
if (cu != 0) {
double coeff = (1.0 + delta[u]) / cu;
for (int p = head_path[u]; p; p = E_path[p].next) {
int w = E_path[p].to;
delta[w] += count_path[w] * coeff;
}
}
if (u != S)
bc[u] += delta[u] + endpoints_;
}
}
static double calc_scale(int len_V, int is_directed, int normalized, int endpoints) {
double scale = 1.0;
if (normalized) {
if (endpoints) {
if (len_V < 2) {
scale = 1.0;
} else {
scale = 1.0 / (double(len_V) * (len_V - 1));
}
} else {
if (len_V <= 2) {
scale = 1.0;
} else {
scale = 1.0 / ((double(len_V) - 1) * (len_V - 2));
}
}
} else {
if (!is_directed) {
scale = 0.5;
} else {
scale = 1.0;
}
}
return scale;
}
static py::object invoke_cpp_betweenness_centrality(
py::object G, py::object weight, py::object cutoff, py::object sources,
py::object normalized, py::object endpoints
) {
Graph& G_ = G.cast<Graph&>();
int cutoff_ = -1;
if (!cutoff.is_none()) {
cutoff_ = cutoff.cast<int>();
}
int N = G_.node.size();
bool is_directed = G.attr("is_directed")().cast<bool>();
int normalized_ = normalized.cast<bool>();
int endpoints_ = endpoints.cast<bool>();
double scale = calc_scale(N, is_directed, normalized_, endpoints_);
bool use_weights = !weight.is_none();
std::string weight_key = "";
if (use_weights) {
weight_key = weight_to_string(weight);
}
Graph_L G_l;
if (G_.linkgraph_dirty) {
G_l = graph_to_linkgraph(G_, is_directed, weight_key, false, false);
G_.linkgraph_structure = G_l;
} else {
G_l = G_.linkgraph_structure;
}
int edges_num = G_l.edges.size();
std::vector<double> bc(N + 1, 0.0);
std::vector<double> BC;
int num_threads = 1;
#ifdef _OPENMP
num_threads = omp_get_max_threads();
#endif
std::vector<std::vector<int>> dis_all(num_threads, std::vector<int>(N + 1));
std::vector<std::vector<int>> head_path_all(num_threads, std::vector<int>(N + 1));
std::vector<std::vector<int>> St_all(num_threads, std::vector<int>(N + 1));
std::vector<std::vector<long long>> count_path_all(num_threads, std::vector<long long>(N + 1));
std::vector<std::vector<double>> delta_all(num_threads, std::vector<double>(N + 1));
std::vector<std::vector<LinkEdge>> E_path_all(num_threads, std::vector<LinkEdge>(edges_num + 1));
std::vector<std::vector<int>> queue_all(num_threads, std::vector<int>(N + 1));
std::vector<std::vector<int>> stamp_all(num_threads, std::vector<int>(N + 1, 0));
std::vector<int> cur_stamp_all(num_threads, 0);
std::vector<std::vector<double>> bc_local_all(num_threads, std::vector<double>(N + 1, 0.0));
if (!sources.is_none()) {
py::list sources_list = py::list(sources);
int sources_list_len = py::len(sources_list);
std::vector<node_t> sources_vec;
sources_vec.reserve(sources_list_len);
for (int i = 0; i < sources_list_len; i++) {
if (G_.node_to_id.attr("get")(sources_list[i], py::none()).is_none()) {
printf("The node should exist in the graph!");
return py::none();
}
sources_vec.push_back(G_.node_to_id.attr("get")(sources_list[i]).cast<node_t>());
}
#ifdef _OPENMP
#pragma omp parallel for schedule(dynamic)
#endif
for (int i = 0; i < sources_list_len; i++) {
node_t source_id = sources_vec[i];
#ifdef _OPENMP
int tid = omp_get_thread_num();
#else
int tid = 0;
#endif
auto& bc_local = bc_local_all[tid];
auto& dis = dis_all[tid];
auto& head_path = head_path_all[tid];
auto& St = St_all[tid];
auto& count_path = count_path_all[tid];
auto& delta = delta_all[tid];
auto& E_path = E_path_all[tid];
auto& q = queue_all[tid];
auto& stamp = stamp_all[tid];
int& cur_stamp = cur_stamp_all[tid];
if (use_weights) {
betweenness_dijkstra_worker(
G_l, source_id, bc_local, cutoff_, dis, head_path,
St, count_path, delta, E_path, endpoints_, stamp, cur_stamp
);
} else {
betweenness_bfs_worker(
G_l, source_id, bc_local, cutoff_, endpoints_, q, dis, head_path,
St, count_path, delta, E_path, stamp, cur_stamp
);
}
}
} else {
#ifdef _OPENMP
#pragma omp parallel for schedule(dynamic)
#endif
for (int i = 1; i <= N; ++i) {
#ifdef _OPENMP
int tid = omp_get_thread_num();
#else
int tid = 0;
#endif
auto& bc_local = bc_local_all[tid];
auto& dis = dis_all[tid];
auto& head_path = head_path_all[tid];
auto& St = St_all[tid];
auto& count_path = count_path_all[tid];
auto& delta = delta_all[tid];
auto& E_path = E_path_all[tid];
auto& q = queue_all[tid];
auto& stamp = stamp_all[tid];
int& cur_stamp = cur_stamp_all[tid];
if (use_weights) {
betweenness_dijkstra_worker(
G_l, i, bc_local, cutoff_, dis, head_path,
St, count_path, delta, E_path, endpoints_, stamp, cur_stamp
);
} else {
betweenness_bfs_worker(
G_l, i, bc_local, cutoff_, endpoints_, q, dis, head_path,
St, count_path, delta, E_path, stamp, cur_stamp
);
}
}
}
#ifdef _OPENMP
#pragma omp parallel for schedule(static)
for (int j = 1; j <= N; ++j) {
double s = 0.0;
for (int tid = 0; tid < num_threads; ++tid)
s += bc_local_all[tid][j];
bc[j] += s;
}
#else
for (int j = 1; j <= N; ++j) {
bc[j] += bc_local_all[0][j];
}
#endif
BC.reserve(N);
for (int i = 1; i <= N; i++) {
BC.push_back(scale * bc[i]);
}
py::array::ShapeContainer ret_shape{(int)BC.size()};
py::array_t<double> ret(ret_shape, BC.data());
return ret;
}
#ifdef EASYGRAPH_ENABLE_GPU
static py::object invoke_gpu_betweenness_centrality(py::object G, py::object weight,
py::object py_sources, py::object normalized, py::object endpoints) {
Graph& G_ = G.cast<Graph&>();
if (weight.is_none()) {
G_.gen_CSR();
} else {
G_.gen_CSR(weight_to_string(weight));
}
auto csr_graph = G_.csr_graph;
std::vector<int>& E = csr_graph->E;
std::vector<int>& V = csr_graph->V;
std::vector<double> *W_p = weight.is_none() ? &(csr_graph->unweighted_W)
: csr_graph->W_map.find(weight_to_string(weight))->second.get();
auto sources = G_.gen_CSR_sources(py_sources);
std::vector<double> BC;
bool is_directed = G.attr("is_directed")().cast<bool>();
int gpu_r = gpu_easygraph::betweenness_centrality(V, E, *W_p, *sources,
is_directed, normalized.cast<py::bool_>(),
endpoints.cast<py::bool_>(), BC);
if (gpu_r != gpu_easygraph::EG_GPU_SUCC) {
// the code below will throw an exception
py::pybind11_fail(gpu_easygraph::err_code_detail(gpu_r));
}
py::array::ShapeContainer ret_shape{(int)BC.size()};
py::array_t<double> ret(ret_shape, BC.data());
return ret;
}
#endif
py::object betweenness_centrality(py::object G, py::object weight, py::object cutoff, py::object sources,
py::object normalized, py::object endpoints) {
#ifdef EASYGRAPH_ENABLE_GPU
return invoke_gpu_betweenness_centrality(G, weight, sources, normalized, endpoints);
#else
return invoke_cpp_betweenness_centrality(G, weight, cutoff, sources, normalized, endpoints);
#endif
}
// void betweenness_dijkstra(const Graph_L& G_l, const int &S, std::vector<double>& bc, double cutoff) {
// int N = G_l.n;
// int edge_number_path = 0;
// __gnu_pbds::priority_queue<compare_node> q;
// std::vector<double> dis(N+1, INFINITY);
// std::vector<bool> vis(N+1, false);
// std::vector<int> head_path(N+1, 0);
// const std::vector<int>& head = G_l.head;
// const std::vector<LinkEdge>& E = G_l.edges;
// int edges_num = E.size();
// std::vector<int> St(N+1, 0);
// std::vector<long long> count_path(N+1, 0);
// std::vector<double> delta(N+1, 0);
// std::vector<LinkEdge> E_path(edges_num+1);
// head_path[S] = 0;
// dis[S] = 0;
// count_path[S] = 1;
// dis[S] = 0;
// count_path[S] = 1;
// q.push(compare_node(S, 0));
// int cnt_St = 0;
// while(!q.empty()) {
// int u = q.top().x;
// q.pop();
// if (vis[u]){
// continue;
// }
// if (cutoff >= 0 && dis[u] > cutoff){
// continue;
// }
// St[cnt_St++] = u;
// vis[u] = true;
// for(int p = head[u]; p != -1; p = E[p].next) {
// int v = E[p].to;
// if(cutoff >= 0 && (dis[u] + E[p].w) > cutoff){
// continue;
// }
// if (dis[v] > dis[u] + E[p].w) {
// dis[v] = dis[u] + E[p].w;
// q.push(compare_node(v, dis[v]));
// count_path[v] = count_path[u];
// head_path[v] = 0;
// E_path[++edge_number_path].next = head_path[v];
// E_path[edge_number_path].to = u;
// head_path[v] = edge_number_path;
// }
// else if (dis[v] == dis[u] + E[p].w) {
// count_path[v] += count_path[u];
// E_path[++edge_number_path].next = head_path[v];
// E_path[edge_number_path].to = u;
// head_path[v] = edge_number_path;
// }
// }
// }
// while (cnt_St > 0) {
// int u = St[--cnt_St];
// float coeff = (1.0 + delta[u]) / count_path[u];
// for(int p = head_path[u]; p; p = E_path[p].next){
// delta[E_path[p].to] += count_path[E_path[p].to] * coeff;
// }
// if (u != S)
// bc[u] += delta[u];
// }
// }
@@ -0,0 +1,27 @@
#pragma once
#include "../../common/common.h"
py::object closeness_centrality(py::object G, py::object weight, py::object cutoff, py::object sources);
py::object betweenness_centrality(py::object G, py::object weight, py::object cutoff, py::object sources,
py::object normalized, py::object endpoints);
py::object cpp_katz_centrality(
py::object G,
py::object py_alpha,
py::object py_beta,
py::object py_max_iter,
py::object py_tol,
py::object py_normalized
);
py::object degree_centrality(py::object G);
py::object in_degree_centrality(py::object G);
py::object out_degree_centrality(py::object G);
py::object cpp_eigenvector_centrality(
py::object G,
py::object py_max_iter,
py::object py_tol,
py::object py_nstart,
py::object py_weight
);
@@ -0,0 +1,353 @@
#include "centrality.h"
#ifdef EASYGRAPH_ENABLE_GPU
#include <gpu_easygraph.h>
#endif
#include "../../classes/graph.h"
#include "../../common/utils.h"
#include "../../classes/linkgraph.h"
#include <queue>
#include <vector>
#include <limits>
#include <cmath>
#ifdef _OPENMP
#include <omp.h>
#endif
// Heap node: use negative value + max heap to implement min heap
typedef std::pair<float, int> HeapNode;
// Optimized adjacency list cache
struct FastAdjCache {
std::vector<int*> neighbor_ptrs;
std::vector<int> neighbor_counts;
std::vector<float*> weight_ptrs;
std::vector<std::vector<int>> neighbor_storage;
std::vector<std::vector<float>> weight_storage;
void init(int N) {
neighbor_ptrs.resize(N + 1, nullptr);
neighbor_counts.resize(N + 1, 0);
neighbor_storage.resize(N + 1);
}
void init_with_weights(int N) {
init(N);
weight_ptrs.resize(N + 1, nullptr);
weight_storage.resize(N + 1);
}
inline void build_if_needed(int u, const std::vector<int>& head,
const std::vector<LinkEdge>& edges, bool store_weights) {
if (neighbor_ptrs[u] != nullptr) return;
std::vector<int>& neis = neighbor_storage[u];
for (int p = head[u]; p != -1; p = edges[p].next) {
neis.push_back(edges[p].to);
if (store_weights) {
weight_storage[u].push_back(edges[p].w);
}
}
neighbor_counts[u] = neis.size();
neighbor_ptrs[u] = neis.data();
if (store_weights) {
weight_ptrs[u] = weight_storage[u].data();
}
}
inline int* get_neighbors_ptr(int u) const { return neighbor_ptrs[u]; }
inline int get_neighbor_count(int u) const { return neighbor_counts[u]; }
inline float* get_weights_ptr(int u) const { return weight_ptrs[u]; }
};
// BFS implementation - directly use raw adjacency list
double closeness_bfs_direct(const Graph_L& G_l, const int &S, int cutoff,
std::vector<int>& already_counted,
std::vector<int>& queue_storage,
int timestamp) {
int N = G_l.n;
const std::vector<LinkEdge>& E = G_l.edges;
const std::vector<int>& head = G_l.head;
int nodes_reached = 0;
long long sum_dis = 0;
queue_storage.clear();
int queue_front = 0;
already_counted[S] = timestamp;
queue_storage.push_back(S);
queue_storage.push_back(0);
while (queue_front < static_cast<int>(queue_storage.size())) {
int u = queue_storage[queue_front++];
int actdist = queue_storage[queue_front++];
if (cutoff >= 0 && actdist > cutoff) {
continue;
}
sum_dis += actdist;
nodes_reached++;
for (int p = head[u]; p != -1; p = E[p].next) {
int v = E[p].to;
if (already_counted[v] == timestamp) {
continue;
}
already_counted[v] = timestamp;
queue_storage.push_back(v);
queue_storage.push_back(actdist + 1);
}
}
if (nodes_reached == 1)
return 0.0;
else
return 1.0 * (nodes_reached - 1) * (nodes_reached - 1) / ((N - 1) * sum_dis);
}
// Check if the graph is unweighted
inline bool is_unweighted_graph(const Graph_L& G_l) {
const std::vector<LinkEdge>& E = G_l.edges;
for (const auto& edge : E) {
if (std::abs(edge.w - 1.0f) > 1e-9) {
return false;
}
}
return true;
}
// Dijkstra implementation - use on-demand adjacency cache
double closeness_dijkstra_cached(const Graph_L& G_l, const int &S, int cutoff,
std::vector<float>& dist,
std::vector<int>& which,
FastAdjCache& cache,
int timestamp) {
int N = G_l.n;
const std::vector<LinkEdge>& E = G_l.edges;
const std::vector<int>& head = G_l.head;
int nodes_reached = 0;
double sum_dis = 0.0;
std::priority_queue<HeapNode> heap;
dist[S] = 1.0f;
which[S] = timestamp;
heap.push({-1.0f, S});
while (!heap.empty()) {
HeapNode top = heap.top();
heap.pop();
float mindist = -top.first;
int minnei = top.second;
if (mindist > dist[minnei]) {
continue;
}
float actual_dist = mindist - 1.0f;
if (cutoff >= 0 && actual_dist > cutoff) {
continue;
}
sum_dis += actual_dist;
nodes_reached++;
cache.build_if_needed(minnei, head, E, true);
int* neis = cache.get_neighbors_ptr(minnei);
float* ws = cache.get_weights_ptr(minnei);
int nlen = cache.get_neighbor_count(minnei);
for (int j = 0; j < nlen; j++) {
int to = neis[j];
float altdist = mindist + ws[j];
float curdist = dist[to];
if (which[to] != timestamp) {
which[to] = timestamp;
dist[to] = altdist;
heap.push({-altdist, to});
} else if (curdist == 0.0f || altdist < curdist) {
dist[to] = altdist;
heap.push({-altdist, to});
}
}
}
if (nodes_reached == 1)
return 0.0;
else
return 1.0 * (nodes_reached - 1) * (nodes_reached - 1) / ((N - 1) * sum_dis);
}
static py::object invoke_cpp_closeness_centrality(py::object G, py::object weight,
py::object cutoff, py::object sources) {
Graph& G_ = G.cast<Graph&>();
int N = G_.node.size();
bool is_directed = G.attr("is_directed")().cast<bool>();
std::string weight_key = weight_to_string(weight);
const Graph_L& G_l = graph_to_linkgraph(G_, is_directed, weight_key, false, false);
int cutoff_ = -1;
if (!cutoff.is_none()){
cutoff_ = cutoff.cast<int>();
}
// Auto algorithm selection
bool use_bfs = (weight.is_none() || is_unweighted_graph(G_l));
std::vector<double> CC;
if(!sources.is_none()){
py::list sources_list = py::list(sources);
int sources_list_len = py::len(sources_list);
CC.resize(sources_list_len);
// Collect all source node IDs
std::vector<node_t> source_ids(sources_list_len);
for(int i = 0; i < sources_list_len; i++){
if(G_.node_to_id.attr("get")(sources_list[i],py::none()).is_none()){
printf("The node should exist in the graph!");
return py::none();
}
source_ids[i] = G_.node_to_id.attr("get")(sources_list[i]).cast<node_t>();
}
// OpenMP parallel computation
// Only enable parallelism when sources are many to avoid overhead
#pragma omp parallel if(sources_list_len > 100)
{
// Per-thread data structures (avoid race conditions)
std::vector<int> already_counted(N + 1, 0);
std::vector<int> queue_storage;
queue_storage.reserve(N * 2);
std::vector<float> dist(N + 1, 0.0f);
std::vector<int> which(N + 1, 0);
FastAdjCache cache;
if (!use_bfs) {
cache.init_with_weights(N);
}
// Assign unique timestamp start for each thread
#ifdef _OPENMP
int thread_id = omp_get_thread_num();
int num_threads = omp_get_num_threads();
int timestamp = thread_id * sources_list_len;
#else
int timestamp = 0;
#endif
// Parallel loop: each thread handles different source node
#pragma omp for schedule(dynamic, 1)
for(int i = 0; i < sources_list_len; i++){
timestamp++;
double res;
if (use_bfs) {
res = closeness_bfs_direct(G_l, source_ids[i], cutoff_,
already_counted, queue_storage, timestamp);
} else {
res = closeness_dijkstra_cached(G_l, source_ids[i], cutoff_,
dist, which, cache, timestamp);
}
CC[i] = res;
}
}
}
else{
CC.resize(N);
// OpenMP parallel computation for all nodes
// Only enable parallelism when node count is large
#pragma omp parallel if(N > 100)
{
// Per-thread data structures
std::vector<int> already_counted(N + 1, 0);
std::vector<int> queue_storage;
queue_storage.reserve(N * 2);
std::vector<float> dist(N + 1, 0.0f);
std::vector<int> which(N + 1, 0);
FastAdjCache cache;
if (!use_bfs) {
cache.init_with_weights(N);
}
// Assign unique timestamp start for each thread
#ifdef _OPENMP
int thread_id = omp_get_thread_num();
int num_threads = omp_get_num_threads();
int timestamp = thread_id * N;
#else
int timestamp = 0;
#endif
// Parallel loop: dynamic scheduling for load balancing
#pragma omp for schedule(dynamic, 10)
for(int i = 1; i <= N; i++){
timestamp++;
double res;
if (use_bfs) {
res = closeness_bfs_direct(G_l, i, cutoff_,
already_counted, queue_storage, timestamp);
} else {
res = closeness_dijkstra_cached(G_l, i, cutoff_,
dist, which, cache, timestamp);
}
CC[i - 1] = res;
}
}
}
py::array::ShapeContainer ret_shape{(int)CC.size()};
py::array_t<double> ret(ret_shape, CC.data());
return ret;
}
#ifdef EASYGRAPH_ENABLE_GPU
static py::object invoke_gpu_closeness_centrality(py::object G, py::object weight, py::object py_sources) {
Graph& G_ = G.cast<Graph&>();
if (weight.is_none()) {
G_.gen_CSR();
} else {
G_.gen_CSR(weight_to_string(weight));
}
auto csr_graph = G_.csr_graph;
std::vector<int>& E = csr_graph->E;
std::vector<int>& V = csr_graph->V;
std::vector<double> *W_p = weight.is_none() ? &(csr_graph->unweighted_W)
: csr_graph->W_map.find(weight_to_string(weight))->second.get();
auto sources = G_.gen_CSR_sources(py_sources);
std::vector<double> CC;
int gpu_r = gpu_easygraph::closeness_centrality(V, E, *W_p, *sources, CC);
if (gpu_r != gpu_easygraph::EG_GPU_SUCC) {
// the code below will throw an exception
py::pybind11_fail(gpu_easygraph::err_code_detail(gpu_r));
}
py::array::ShapeContainer ret_shape{(int)CC.size()};
py::array_t<double> ret(ret_shape, CC.data());
return ret;
}
#endif
py::object closeness_centrality(py::object G, py::object weight, py::object cutoff, py::object sources) {
#ifdef EASYGRAPH_ENABLE_GPU
return invoke_gpu_closeness_centrality(G, weight, sources);
#else
return invoke_cpp_closeness_centrality(G, weight, cutoff, sources);
#endif
}
@@ -0,0 +1,96 @@
#include "centrality.h"
#include "../../classes/graph.h"
#include "../../classes/directed_graph.h"
#include "../../common/utils.h"
#include "../../classes/linkgraph.h"
namespace py = pybind11;
py::object degree_centrality(
py::object G
) {
Graph* graph = G.cast<Graph*>();
py::dict centrality_map = py::dict();
py::object nodes = graph->get_nodes();
int n = py::len(nodes);
if (n <= 1) {
for (const auto& node_handle : nodes) {
centrality_map[node_handle] = 0.0;
}
return centrality_map;
}
double scale = 1.0 / (n - 1);
std::string class_name = G.attr("__class__").attr("__name__").cast<std::string>();
if (class_name == "DiGraphC") {
// 有向图 (DiGraph)
DiGraph* digraph = G.cast<DiGraph*>();
py::object adj = digraph->get_adj();
py::object pred = digraph->get_pred();
for (const auto& node_handle : nodes) {
int out_deg = py::len(adj[node_handle]);
int in_deg = py::len(pred[node_handle]);
centrality_map[node_handle] = (double)(out_deg + in_deg) * scale;
}
} else {
py::object adj = graph->get_adj();
for (const auto& node_handle : nodes) {
int degree = py::len(adj[node_handle]);
centrality_map[node_handle] = (double)degree * scale;
}
}
return centrality_map;
}
py::object in_degree_centrality(
py::object G
) {
DiGraph* graph = G.cast<DiGraph*>();
py::dict centrality_map = py::dict();
py::object nodes = graph->get_nodes();
int n = py::len(nodes);
if (n <= 1) {
return centrality_map;
}
double scale = 1.0 / (n - 1);
py::object pred = graph->get_pred();
for (const auto& node_handle : nodes) {
int in_degree = py::len(pred[node_handle]);
centrality_map[node_handle] = in_degree * scale;
}
return centrality_map;
}
py::object out_degree_centrality(
py::object G
) {
Graph* graph = G.cast<Graph*>();
py::dict centrality_map = py::dict();
py::object nodes = graph->get_nodes();
int n = py::len(nodes);
if (n <= 1) {
return centrality_map;
}
double scale = 1.0 / (n - 1);
py::object adj = graph->get_adj();
for (const auto& node_handle : nodes) {
int out_degree = py::len(adj[node_handle]);
centrality_map[node_handle] = out_degree * scale;
}
return centrality_map;
}
@@ -0,0 +1,198 @@
#include <vector>
#include <cmath>
#include <algorithm>
#include <iostream>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#ifdef _OPENMP
#include <omp.h>
#endif
#include "../../classes/graph.h"
#include "../../common/utils.h"
namespace py = pybind11;
class CSRMatrix {
public:
std::vector<int> indptr;
std::vector<int> indices;
std::vector<double> data; // Empty if unweighted (all 1.0)
int rows, cols;
bool is_weighted;
CSRMatrix(int r, int c) : rows(r), cols(c), is_weighted(false) {
indptr.assign(r + 1, 0);
}
};
// Power iteration with branch optimization for weighted/unweighted paths
std::vector<double> power_iteration_optimized(
const CSRMatrix& A,
int max_iter,
double tol,
std::vector<double>& x
) {
const int n = A.rows;
std::vector<double> x_next(n);
bool use_weight = A.is_weighted && !A.data.empty();
// Initial normalization
double norm = 0.0;
#pragma omp parallel for reduction(+:norm)
for (int i = 0; i < n; ++i) norm += x[i] * x[i];
norm = std::sqrt(norm);
if (norm < 1e-12) {
std::fill(x.begin(), x.end(), 1.0 / std::sqrt(n));
} else {
double inv_norm = 1.0 / norm;
#pragma omp parallel for
for (int i = 0; i < n; ++i) x[i] *= inv_norm;
}
double delta = tol + 1.0;
for (int iter = 0; iter < max_iter && delta >= tol; ++iter) {
double next_norm_sq = 0.0;
#pragma omp parallel for reduction(+:next_norm_sq) schedule(dynamic, 64)
for (int i = 0; i < n; ++i) {
double sum = 0.0;
const int start = A.indptr[i];
const int end = A.indptr[i+1];
if (use_weight) {
for (int j = start; j < end; ++j) {
sum += A.data[j] * x[A.indices[j]];
}
} else {
for (int j = start; j < end; ++j) {
sum += x[A.indices[j]];
}
}
x_next[i] = sum;
next_norm_sq += sum * sum;
}
double next_norm = std::sqrt(next_norm_sq);
if (next_norm < 1e-12) break;
double inv_next_norm = 1.0 / next_norm;
delta = 0.0;
#pragma omp parallel for reduction(+:delta) schedule(static)
for (int i = 0; i < n; ++i) {
double val = x_next[i] * inv_next_norm;
delta += std::abs(val - x[i]);
x_next[i] = val;
}
x.swap(x_next);
}
return x;
}
// Build transpose CSR with fallback logic for missing weight keys
CSRMatrix build_transpose_matrix_smart(Graph& graph, const std::vector<node_t>& nodes, const std::string& weight_key) {
std::shared_ptr<CSRGraph> csr_ptr = weight_key.empty() ? graph.gen_CSR() : graph.gen_CSR(weight_key);
int n = static_cast<int>(nodes.size());
CSRMatrix At(n, n);
if (!csr_ptr) return At;
const auto& src_indptr = csr_ptr->V;
const auto& src_indices = csr_ptr->E;
std::vector<double> src_data;
bool actually_weighted = false;
// Detect if weighted calculation is required
if (!weight_key.empty()) {
auto it = csr_ptr->W_map.find(weight_key);
if (it != csr_ptr->W_map.end() && it->second) {
src_data = *(it->second);
for (double w : src_data) {
if (std::abs(w - 1.0) > 1e-9) {
actually_weighted = true;
break;
}
}
}
}
At.is_weighted = actually_weighted;
// Calculate row counts for transpose
for (int x_idx : src_indices) {
if (x_idx >= 0 && x_idx < n) At.indptr[x_idx + 1]++;
}
for (int i = 0; i < n; ++i) At.indptr[i + 1] += At.indptr[i];
At.indices.resize(src_indices.size());
if (actually_weighted) At.data.resize(src_indices.size());
std::vector<int> cur_pos(At.indptr.begin(), At.indptr.end());
// Populate transpose CSR data
for (int r = 0; r < n; ++r) {
for (int p = src_indptr[r]; p < src_indptr[r+1]; ++p) {
int c = src_indices[p];
if (c < 0 || c >= n) continue;
int dest = cur_pos[c]++;
At.indices[dest] = r;
if (actually_weighted) At.data[dest] = src_data[p];
}
}
return At;
}
py::object cpp_eigenvector_centrality(
py::object G,
py::object py_max_iter,
py::object py_tol,
py::object py_nstart,
py::object py_weight
) {
try {
Graph& graph = G.cast<Graph&>();
int max_iter = py_max_iter.cast<int>();
double tol = py_tol.cast<double>();
std::string weight_key = py_weight.is_none() ? "" : py_weight.cast<std::string>();
if (graph.node.empty()) return py::dict();
std::vector<node_t> nodes;
for (auto& pair : graph.node) nodes.push_back(pair.first);
int n = nodes.size();
CSRMatrix A_transpose = build_transpose_matrix_smart(graph, nodes, weight_key);
// Initialize x vector (prefer degree-based or uniform)
std::vector<double> x(n, 1.0 / n);
if (!py_nstart.is_none()) {
py::dict nstart = py_nstart.cast<py::dict>();
for (int i = 0; i < n; i++) {
py::object node_obj = graph.id_to_node[py::cast(nodes[i])];
if (nstart.contains(node_obj)) x[i] = nstart[node_obj].cast<double>();
}
} else {
for (int i = 0; i < n; i++) {
int degree = A_transpose.indptr[i+1] - A_transpose.indptr[i];
x[i] = (degree > 0) ? (double)degree : 1.0/n;
}
}
std::vector<double> res = power_iteration_optimized(A_transpose, max_iter, tol, x);
py::dict result;
for (int i = 0; i < n; i++) {
py::object node_obj = graph.id_to_node[py::cast(nodes[i])];
result[node_obj] = res[i];
}
return result;
} catch (const std::exception& e) {
throw std::runtime_error(std::string("C++ Eigenvector Error: ") + e.what());
}
}
@@ -0,0 +1,194 @@
#ifdef _OPENMP
#include <omp.h>
#endif
#include <vector>
#include <cmath>
#include <algorithm>
#include <stdexcept>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "centrality.h"
#include "../../classes/graph.h"
namespace py = pybind11;
class CSRMatrix {
public:
std::vector<int> indptr; // size rows+1
std::vector<int> indices; // size nnz
std::vector<double> data; // size nnz
int rows = 0;
int cols = 0;
CSRMatrix() = default;
CSRMatrix(int r, int c) : rows(r), cols(c) {
indptr.assign(r + 1, 0);
}
};
// Build transpose CSR from EasyGraph CSR so that row i contains in-neighbors of i.
static CSRMatrix build_transpose_matrix_from_csr(const std::shared_ptr<CSRGraph>& csr_ptr) {
if (!csr_ptr) return CSRMatrix();
const int n = static_cast<int>(csr_ptr->nodes.size());
if (n == 0) return CSRMatrix(0, 0);
const auto& src_indptr = csr_ptr->V;
const auto& src_indices = csr_ptr->E;
// Unweighted: all ones.
std::vector<double> src_data(src_indices.size(), 1.0);
CSRMatrix At(n, n);
// Count nnz per column in the source (becomes nnz per row in transpose).
for (int c : src_indices) {
if (c >= 0 && c < n) At.indptr[c + 1]++;
}
// Prefix sum.
for (int i = 0; i < n; ++i) {
At.indptr[i + 1] += At.indptr[i];
}
const int nnz = static_cast<int>(src_indices.size());
At.indices.resize(nnz);
At.data.resize(nnz);
std::vector<int> cur_pos(At.indptr.begin(), At.indptr.end());
// Fill transpose.
for (int r = 0; r < n; ++r) {
const int start = src_indptr[r];
const int end = src_indptr[r + 1];
for (int p = start; p < end; ++p) {
const int c = src_indices[p];
if (c < 0 || c >= n) continue;
const int dest = cur_pos[c]++;
At.indices[dest] = r;
At.data[dest] = src_data[p];
}
}
return At;
}
static std::vector<double> katz_centrality_omp(const CSRMatrix& A,
double alpha,
const std::vector<double>& beta,
int max_iters,
double tol,
bool normalize) {
const int n = A.rows;
std::vector<double> x(n, 1.0); // initial guess
std::vector<double> x_next(n, 0.0); // next iterate
if (n == 0) return x;
for (int iter = 0; iter < max_iters; ++iter) {
double err_sq = 0.0;
double norm_sq = 0.0;
// SpMV + Katz update + error and norm in ONE pass
#pragma omp parallel for reduction(+ : err_sq, norm_sq) schedule(static)
for (int i = 0; i < n; ++i) {
double sum = 0.0;
const int row_start = A.indptr[i];
const int row_end = A.indptr[i + 1];
for (int e = row_start; e < row_end; ++e) {
sum += A.data[e] * x[A.indices[e]];
}
const double new_val = alpha * sum + beta[i];
const double diff = new_val - x[i];
x_next[i] = new_val;
err_sq += diff * diff;
norm_sq += new_val * new_val;
}
const double err = std::sqrt(err_sq);
const double norm = std::sqrt(norm_sq);
x.swap(x_next);
if (norm > 0.0 && (err / norm) < tol) {
break;
}
}
if (normalize) {
double norm_sq2 = 0.0;
#pragma omp parallel for reduction(+ : norm_sq2) schedule(static)
for (int i = 0; i < n; ++i) {
norm_sq2 += x[i] * x[i];
}
const double norm = std::sqrt(norm_sq2);
if (norm > 0.0) {
#pragma omp parallel for schedule(static)
for (int i = 0; i < n; ++i) {
x[i] /= norm;
}
}
}
return x;
}
py::object cpp_katz_centrality(py::object G,
py::object py_alpha,
py::object py_beta,
py::object py_max_iter,
py::object py_tol,
py::object py_normalized) {
Graph& graph = G.cast<Graph&>();
const double alpha = py_alpha.cast<double>();
const int max_iter = py_max_iter.cast<int>();
const double tol = py_tol.cast<double>();
const bool normalized = py_normalized.cast<bool>();
std::shared_ptr<CSRGraph> csr_ptr = graph.gen_CSR();
if (!csr_ptr || csr_ptr->nodes.empty()) {
return py::dict();
}
const int n = static_cast<int>(csr_ptr->nodes.size());
// Build transpose CSR so that we accumulate from in-neighbors.
CSRMatrix A = build_transpose_matrix_from_csr(csr_ptr);
// Process beta parameter: scalar or dict(node->beta).
std::vector<double> beta(n, 1.0);
if (py::isinstance<py::float_>(py_beta) || py::isinstance<py::int_>(py_beta)) {
const double beta_val = py_beta.cast<double>();
#pragma omp parallel for schedule(static)
for (int i = 0; i < n; ++i) {
beta[i] = beta_val;
}
} else if (py::isinstance<py::dict>(py_beta)) {
py::dict beta_dict = py_beta.cast<py::dict>();
for (int i = 0; i < n; ++i) {
node_t internal_id = csr_ptr->nodes[i];
py::object node_obj = graph.id_to_node[py::cast(internal_id)];
if (beta_dict.contains(node_obj)) {
beta[i] = beta_dict[node_obj].cast<double>();
}
}
} else {
throw py::type_error("beta must be a float/int or a dict");
}
std::vector<double> scores = katz_centrality_omp(A, alpha, beta, max_iter, tol, normalized);
// Prepare results
py::dict result;
for (int i = 0; i < n; ++i) {
node_t internal_id = csr_ptr->nodes[i];
py::object node_obj = graph.id_to_node[py::cast(internal_id)];
result[node_obj] = scores[i];
}
return result;
}