use super::graph_algorithms::{
algorithm_timeout_err, edge_in_scope, intern_connection_types, scoped_node_set, NodeScope,
};
use super::Interrupt;
use crate::graph::schema::DirGraph;
use crate::graph::storage::GraphRead;
use petgraph::graph::NodeIndex;
#[derive(Debug, Clone)]
pub struct CentralityResult {
pub node_idx: NodeIndex,
pub score: f64,
}
#[derive(Clone)]
#[non_exhaustive]
pub struct PagerankOptions<'a> {
pub damping_factor: f64,
pub max_iterations: usize,
pub tolerance: f64,
pub connection_types: Option<&'a [String]>,
pub scope: Option<&'a NodeScope>,
pub interrupt: Interrupt,
}
impl Default for PagerankOptions<'_> {
fn default() -> Self {
Self {
damping_factor: 0.85,
max_iterations: 100,
tolerance: 1e-6,
connection_types: None,
scope: None,
interrupt: Interrupt::default(),
}
}
}
impl<'a> PagerankOptions<'a> {
pub fn with_damping_factor(mut self, damping_factor: f64) -> Self {
self.damping_factor = damping_factor;
self
}
pub fn with_max_iterations(mut self, max_iterations: usize) -> Self {
self.max_iterations = max_iterations;
self
}
pub fn with_tolerance(mut self, tolerance: f64) -> Self {
self.tolerance = tolerance;
self
}
pub fn with_connection_types(mut self, connection_types: &'a [String]) -> Self {
self.connection_types = Some(connection_types);
self
}
pub fn with_scope(mut self, scope: &'a NodeScope) -> Self {
self.scope = Some(scope);
self
}
pub fn with_interrupt(mut self, interrupt: Interrupt) -> Self {
self.interrupt = interrupt;
self
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct CentralityOptions<'a> {
pub normalized: bool,
pub sample_size: Option<usize>,
pub connection_types: Option<&'a [String]>,
pub scope: Option<&'a NodeScope>,
pub interrupt: Interrupt,
}
impl Default for CentralityOptions<'_> {
fn default() -> Self {
Self {
normalized: true,
sample_size: None,
connection_types: None,
scope: None,
interrupt: Interrupt::default(),
}
}
}
impl<'a> CentralityOptions<'a> {
pub fn with_normalized(mut self, normalized: bool) -> Self {
self.normalized = normalized;
self
}
pub fn with_sample_size(mut self, sample_size: usize) -> Self {
self.sample_size = Some(sample_size);
self
}
pub fn with_connection_types(mut self, connection_types: &'a [String]) -> Self {
self.connection_types = Some(connection_types);
self
}
pub fn with_scope(mut self, scope: &'a NodeScope) -> Self {
self.scope = Some(scope);
self
}
pub fn with_interrupt(mut self, interrupt: Interrupt) -> Self {
self.interrupt = interrupt;
self
}
}
fn sampled_source_indices(
node_count: usize,
sample_size: Option<usize>,
) -> Result<Vec<usize>, String> {
let Some(sample_size) = sample_size else {
return Ok((0..node_count).collect());
};
if sample_size == 0 {
return Err("sample_size must be greater than 0".to_string());
}
let sample_size = sample_size.min(node_count);
if sample_size == node_count {
return Ok((0..node_count).collect());
}
let step = node_count as f64 / sample_size as f64;
Ok((0..sample_size)
.map(|i| (i as f64 * step) as usize)
.collect())
}
#[derive(Clone)]
#[non_exhaustive]
pub struct DegreeCentralityOptions<'a> {
pub normalized: bool,
pub connection_types: Option<&'a [String]>,
pub scope: Option<&'a NodeScope>,
pub interrupt: Interrupt,
}
impl Default for DegreeCentralityOptions<'_> {
fn default() -> Self {
Self {
normalized: true,
connection_types: None,
scope: None,
interrupt: Interrupt::default(),
}
}
}
impl<'a> DegreeCentralityOptions<'a> {
pub fn with_normalized(mut self, normalized: bool) -> Self {
self.normalized = normalized;
self
}
pub fn with_connection_types(mut self, connection_types: &'a [String]) -> Self {
self.connection_types = Some(connection_types);
self
}
pub fn with_scope(mut self, scope: &'a NodeScope) -> Self {
self.scope = Some(scope);
self
}
pub fn with_interrupt(mut self, interrupt: Interrupt) -> Self {
self.interrupt = interrupt;
self
}
}
pub fn betweenness_centrality(
graph: &DirGraph,
options: &CentralityOptions,
) -> Result<Vec<CentralityResult>, String> {
let CentralityOptions {
normalized,
sample_size,
connection_types,
scope,
interrupt: deadline,
} = *options;
let _arena_guard = graph.graph.begin_query();
use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
let nodes: Vec<NodeIndex> = scoped_node_set(graph, scope);
let n = nodes.len();
let source_indices = sampled_source_indices(n, sample_size)?;
if n <= 2 {
return Ok(nodes
.iter()
.map(|&idx| CentralityResult {
node_idx: idx,
score: 0.0,
})
.collect());
}
let bound = graph.graph.node_bound();
let mut node_to_idx = vec![0usize; bound];
for (i, &node) in nodes.iter().enumerate() {
node_to_idx[node.index()] = i;
}
let interned_ct = intern_connection_types(connection_types);
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for edge in {
let g = &graph.graph;
g.edge_references()
} {
if let Some(ref types) = interned_ct {
if !types.iter().any(|t| *t == edge.connection_type()) {
continue;
}
}
if !edge_in_scope(scope, edge.source(), edge.target()) {
continue;
}
let src_i = node_to_idx[edge.source().index()];
let tgt_i = node_to_idx[edge.target().index()];
adj[src_i].push(tgt_i);
adj[tgt_i].push(src_i);
}
for neighbors in &mut adj {
neighbors.sort_unstable();
neighbors.dedup();
}
let use_parallel = n >= 4096;
let timed_out = AtomicBool::new(false);
let mut betweenness: Vec<f64> = if use_parallel {
use rayon::prelude::*;
let adj_ref = &adj;
let deadline_ref = &deadline;
let timed_out_ref = &timed_out;
let num_threads = rayon::current_num_threads();
let chunk_size = (source_indices.len() / num_threads).max(1);
source_indices
.par_chunks(chunk_size)
.map(|chunk| {
let mut local_betweenness: Vec<f64> = vec![0.0; n];
let mut stack: Vec<usize> = Vec::with_capacity(n);
let mut pred: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut sigma: Vec<f64> = vec![0.0; n];
let mut dist: Vec<i64> = vec![-1; n];
let mut delta: Vec<f64> = vec![0.0; n];
let mut queue: VecDeque<usize> = VecDeque::with_capacity(n);
for (local_counter, &s_idx) in chunk.iter().enumerate() {
if local_counter % 10 == 0 {
if timed_out_ref.load(Ordering::Relaxed) {
break;
}
if deadline_ref.exceeded() {
timed_out_ref.store(true, Ordering::Relaxed);
break;
}
}
stack.clear();
queue.clear();
sigma[s_idx] = 1.0;
dist[s_idx] = 0;
queue.push_back(s_idx);
while let Some(v_idx) = queue.pop_front() {
stack.push(v_idx);
let v_dist = dist[v_idx];
for &w_idx in &adj_ref[v_idx] {
let d = dist[w_idx];
if d < 0 {
dist[w_idx] = v_dist + 1;
queue.push_back(w_idx);
sigma[w_idx] += sigma[v_idx];
pred[w_idx].push(v_idx);
} else if d == v_dist + 1 {
sigma[w_idx] += sigma[v_idx];
pred[w_idx].push(v_idx);
}
}
}
while let Some(w_idx) = stack.pop() {
for &v_idx in &pred[w_idx] {
let contribution = (sigma[v_idx] / sigma[w_idx]) * (1.0 + delta[w_idx]);
delta[v_idx] += contribution;
}
if w_idx != s_idx {
local_betweenness[w_idx] += delta[w_idx];
}
pred[w_idx].clear();
sigma[w_idx] = 0.0;
dist[w_idx] = -1;
delta[w_idx] = 0.0;
}
}
local_betweenness
})
.reduce(
|| vec![0.0; n],
|mut a, b| {
for i in 0..n {
a[i] += b[i];
}
a
},
)
} else {
let mut betweenness: Vec<f64> = vec![0.0; n];
let mut stack: Vec<usize> = Vec::with_capacity(n);
let mut pred: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut sigma: Vec<f64> = vec![0.0; n];
let mut dist: Vec<i64> = vec![-1; n];
let mut delta: Vec<f64> = vec![0.0; n];
let mut queue: VecDeque<usize> = VecDeque::with_capacity(n);
for (source_counter, &s_idx) in source_indices.iter().enumerate() {
if source_counter.is_multiple_of(10) && deadline.exceeded() {
return Err(algorithm_timeout_err());
}
stack.clear();
queue.clear();
sigma[s_idx] = 1.0;
dist[s_idx] = 0;
queue.push_back(s_idx);
while let Some(v_idx) = queue.pop_front() {
stack.push(v_idx);
let v_dist = dist[v_idx];
for &w_idx in &adj[v_idx] {
let d = dist[w_idx];
if d < 0 {
dist[w_idx] = v_dist + 1;
queue.push_back(w_idx);
sigma[w_idx] += sigma[v_idx];
pred[w_idx].push(v_idx);
} else if d == v_dist + 1 {
sigma[w_idx] += sigma[v_idx];
pred[w_idx].push(v_idx);
}
}
}
while let Some(w_idx) = stack.pop() {
for &v_idx in &pred[w_idx] {
let contribution = (sigma[v_idx] / sigma[w_idx]) * (1.0 + delta[w_idx]);
delta[v_idx] += contribution;
}
if w_idx != s_idx {
betweenness[w_idx] += delta[w_idx];
}
pred[w_idx].clear();
sigma[w_idx] = 0.0;
dist[w_idx] = -1;
delta[w_idx] = 0.0;
}
}
betweenness
};
if timed_out.load(Ordering::Relaxed) {
return Err(algorithm_timeout_err());
}
for score in betweenness.iter_mut() {
*score /= 2.0;
}
if normalized && n > 2 {
let scale = 2.0 / ((n - 1) as f64 * (n - 2) as f64);
for score in betweenness.iter_mut() {
*score *= scale;
}
}
if let Some(k) = sample_size {
if k < n {
let scale = n as f64 / k as f64;
for score in betweenness.iter_mut() {
*score *= scale;
}
}
}
let mut results: Vec<CentralityResult> = nodes
.iter()
.enumerate()
.map(|(i, &node_idx)| CentralityResult {
node_idx,
score: betweenness[i],
})
.collect();
results.sort_unstable_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(results)
}
pub fn pagerank(
graph: &DirGraph,
options: &PagerankOptions,
) -> Result<Vec<CentralityResult>, String> {
let PagerankOptions {
damping_factor,
max_iterations,
tolerance,
connection_types,
scope,
interrupt: deadline,
} = *options;
let _arena_guard = graph.graph.begin_query();
let nodes: Vec<NodeIndex> = scoped_node_set(graph, scope);
let n = nodes.len();
if n == 0 {
return Ok(Vec::new());
}
let bound = graph.graph.node_bound();
let mut node_to_idx = vec![0usize; bound];
for (i, &node) in nodes.iter().enumerate() {
node_to_idx[node.index()] = i;
}
let interned_ct = intern_connection_types(connection_types);
let mut in_adj: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut out_degrees: Vec<usize> = vec![0; n];
for edge in {
let g = &graph.graph;
g.edge_references()
} {
if let Some(ref types) = interned_ct {
if !types.iter().any(|t| *t == edge.connection_type()) {
continue;
}
}
if !edge_in_scope(scope, edge.source(), edge.target()) {
continue;
}
let src_i = node_to_idx[edge.source().index()];
let tgt_i = node_to_idx[edge.target().index()];
in_adj[tgt_i].push(src_i);
out_degrees[src_i] += 1;
}
let mut pr: Vec<f64> = vec![1.0 / n as f64; n];
let mut new_pr: Vec<f64> = vec![0.0; n];
let inv_out_degrees: Vec<f64> = out_degrees
.iter()
.map(|&d| {
if d > 0 {
damping_factor / d as f64
} else {
0.0
}
})
.collect();
let is_dangling: Vec<bool> = out_degrees.iter().map(|&d| d == 0).collect();
let teleport = (1.0 - damping_factor) / n as f64;
let inv_n = 1.0 / n as f64;
let use_parallel = n >= 4096;
for _iteration in 0..max_iterations {
if deadline.exceeded() {
return Err(algorithm_timeout_err());
}
let dangling_sum: f64 = if use_parallel {
use rayon::prelude::*;
(0..n)
.into_par_iter()
.filter(|&i| is_dangling[i])
.map(|i| pr[i])
.sum()
} else {
(0..n).filter(|&i| is_dangling[i]).map(|i| pr[i]).sum()
};
let base_score = teleport + damping_factor * dangling_sum * inv_n;
if use_parallel {
use rayon::prelude::*;
new_pr.par_iter_mut().enumerate().for_each(|(j, score)| {
let mut s = base_score;
for &src in &in_adj[j] {
s += inv_out_degrees[src] * pr[src];
}
*score = s;
});
} else {
for j in 0..n {
let mut s = base_score;
for &src in &in_adj[j] {
s += inv_out_degrees[src] * pr[src];
}
new_pr[j] = s;
}
}
let diff: f64 = if use_parallel {
use rayon::prelude::*;
pr.par_iter()
.zip(new_pr.par_iter())
.map(|(a, b)| (a - b).abs())
.sum()
} else {
pr.iter()
.zip(new_pr.iter())
.map(|(a, b)| (a - b).abs())
.sum()
};
std::mem::swap(&mut pr, &mut new_pr);
if diff < tolerance {
break;
}
}
let mut results: Vec<CentralityResult> = nodes
.iter()
.enumerate()
.map(|(i, &node_idx)| CentralityResult {
node_idx,
score: pr[i],
})
.collect();
results.sort_unstable_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(results)
}
pub fn degree_centrality(
graph: &DirGraph,
options: &DegreeCentralityOptions,
) -> Result<Vec<CentralityResult>, String> {
let DegreeCentralityOptions {
normalized,
connection_types,
scope,
interrupt: deadline,
} = *options;
let _arena_guard = graph.graph.begin_query();
let nodes: Vec<NodeIndex> = scoped_node_set(graph, scope);
let n = nodes.len();
if n == 0 {
return Ok(Vec::new());
}
let scale = if normalized && n > 1 {
1.0 / (n - 1) as f64
} else {
1.0
};
let interned_ct = intern_connection_types(connection_types);
let bound = graph.graph.node_bound();
let mut degrees = vec![0usize; bound];
let mut edge_counter: usize = 0;
for edge in {
let g = &graph.graph;
g.edge_references()
} {
edge_counter += 1;
if edge_counter & 0xFFFFF == 0 && deadline.exceeded() {
return Err(algorithm_timeout_err());
}
if let Some(ref types) = interned_ct {
if !types.iter().any(|t| *t == edge.connection_type()) {
continue;
}
}
if !edge_in_scope(scope, edge.source(), edge.target()) {
continue;
}
degrees[edge.source().index()] += 1; degrees[edge.target().index()] += 1; }
let mut results: Vec<CentralityResult> = nodes
.iter()
.map(|&node_idx| CentralityResult {
node_idx,
score: degrees[node_idx.index()] as f64 * scale,
})
.collect();
results.sort_unstable_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(results)
}
pub fn closeness_centrality(
graph: &DirGraph,
options: &CentralityOptions,
) -> Result<Vec<CentralityResult>, String> {
let CentralityOptions {
normalized,
sample_size,
connection_types,
scope,
interrupt: deadline,
} = *options;
let _arena_guard = graph.graph.begin_query();
use std::sync::atomic::{AtomicBool, Ordering};
let nodes: Vec<NodeIndex> = scoped_node_set(graph, scope);
let n = nodes.len();
let source_indices = sampled_source_indices(n, sample_size)?;
if n == 0 {
return Ok(Vec::new());
}
let bound = graph.graph.node_bound();
let mut node_to_idx = vec![0usize; bound];
for (i, &node) in nodes.iter().enumerate() {
node_to_idx[node.index()] = i;
}
let interned_ct = intern_connection_types(connection_types);
let mut adj_incoming: Vec<Vec<usize>> = vec![Vec::new(); n];
for edge in {
let g = &graph.graph;
g.edge_references()
} {
if let Some(ref types) = interned_ct {
if !types.iter().any(|t| *t == edge.connection_type()) {
continue;
}
}
if !edge_in_scope(scope, edge.source(), edge.target()) {
continue;
}
let src_i = node_to_idx[edge.source().index()];
let tgt_i = node_to_idx[edge.target().index()];
adj_incoming[tgt_i].push(src_i);
}
for neighbors in &mut adj_incoming {
neighbors.sort_unstable();
neighbors.dedup();
}
let use_parallel = source_indices.len() >= 4096;
let timed_out = AtomicBool::new(false);
if use_parallel {
use rayon::prelude::*;
let adj_ref = &adj_incoming;
let deadline_ref = &deadline;
let nodes_ref = &nodes;
let timed_out_ref = &timed_out;
let mut results: Vec<CentralityResult> = source_indices
.par_iter()
.enumerate()
.map(|(i, &s_idx)| {
let source = nodes_ref[s_idx];
if i % 100 == 0 {
if timed_out_ref.load(Ordering::Relaxed) {
return CentralityResult {
node_idx: source,
score: 0.0,
};
}
if deadline_ref.exceeded() {
timed_out_ref.store(true, Ordering::Relaxed);
return CentralityResult {
node_idx: source,
score: 0.0,
};
}
}
let mut dist: Vec<i64> = vec![-1; n];
let mut current_level: Vec<usize> = Vec::with_capacity(n / 4);
let mut next_level: Vec<usize> = Vec::with_capacity(n / 4);
let mut touched: Vec<usize> = Vec::with_capacity(n / 4);
current_level.push(s_idx);
dist[s_idx] = 0;
touched.push(s_idx);
let mut depth: i64 = 0;
while !current_level.is_empty() {
depth += 1;
next_level.clear();
for ¤t_idx in ¤t_level {
for &neighbor_idx in &adj_ref[current_idx] {
if dist[neighbor_idx] < 0 {
dist[neighbor_idx] = depth;
next_level.push(neighbor_idx);
touched.push(neighbor_idx);
}
}
}
std::mem::swap(&mut current_level, &mut next_level);
}
let reachable = touched.len();
let total_distance: i64 = touched.iter().map(|&idx| dist[idx]).sum();
if reachable > 1 && total_distance > 0 {
let closeness = (reachable - 1) as f64 / total_distance as f64;
let score = if normalized {
closeness * (reachable - 1) as f64 / (n - 1) as f64
} else {
closeness
};
CentralityResult {
node_idx: source,
score,
}
} else {
CentralityResult {
node_idx: source,
score: 0.0,
}
}
})
.collect();
if timed_out.load(Ordering::Relaxed) {
return Err(algorithm_timeout_err());
}
results.sort_unstable_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
return Ok(results);
}
let mut results = Vec::with_capacity(source_indices.len());
let mut dist: Vec<i64> = vec![-1; n];
let mut current_level: Vec<usize> = Vec::with_capacity(n);
let mut next_level: Vec<usize> = Vec::with_capacity(n);
let mut touched: Vec<usize> = Vec::with_capacity(n);
for (i, &s_idx) in source_indices.iter().enumerate() {
let source = nodes[s_idx];
if i.is_multiple_of(10) && deadline.exceeded() {
return Err(algorithm_timeout_err());
}
for &idx in &touched {
dist[idx] = -1;
}
touched.clear();
current_level.clear();
current_level.push(s_idx);
dist[s_idx] = 0;
touched.push(s_idx);
let mut depth: i64 = 0;
while !current_level.is_empty() {
depth += 1;
next_level.clear();
for ¤t_idx in ¤t_level {
for &neighbor_idx in &adj_incoming[current_idx] {
if dist[neighbor_idx] < 0 {
dist[neighbor_idx] = depth;
next_level.push(neighbor_idx);
touched.push(neighbor_idx);
}
}
}
std::mem::swap(&mut current_level, &mut next_level);
}
let reachable = touched.len();
let total_distance: i64 = touched.iter().map(|&idx| dist[idx]).sum();
if reachable > 1 && total_distance > 0 {
let closeness = (reachable - 1) as f64 / total_distance as f64;
let score = if normalized {
closeness * (reachable - 1) as f64 / (n - 1) as f64
} else {
closeness
};
results.push(CentralityResult {
node_idx: source,
score,
});
} else {
results.push(CentralityResult {
node_idx: source,
score: 0.0,
});
}
}
results.sort_unstable_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
});
Ok(results)
}