chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user