use std::collections::VecDeque;
use scirs2_core::ndarray::{Array1, Array2};
use crate::error::{GraphError, Result};
#[derive(Debug, Clone)]
pub struct VoteRankCentrality {
pub k: usize,
}
impl VoteRankCentrality {
pub fn new(k: usize) -> Self {
Self { k }
}
pub fn compute(&self, adj: &Array2<f64>) -> Result<Vec<usize>> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
}
let k = self.k.min(n);
let degree: Vec<f64> = (0..n)
.map(|i| adj.row(i).iter().filter(|&&x| x != 0.0).count() as f64)
.collect();
let mut vote_ability = vec![1.0_f64; n];
let mut selected = Vec::with_capacity(k);
let mut is_selected = vec![false; n];
for _ in 0..k {
let mut scores = vec![0.0_f64; n];
for i in 0..n {
if is_selected[i] {
continue;
}
for j in 0..n {
if adj[[i, j]] != 0.0 && !is_selected[j] {
scores[i] += vote_ability[j];
}
}
}
let best = (0..n)
.filter(|&i| !is_selected[i])
.max_by(|&a, &b| {
scores[a]
.partial_cmp(&scores[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
match best {
Some(node) if scores[node] > 0.0 => {
is_selected[node] = true;
selected.push(node);
let deg_inv = if degree[node] > 0.0 { 1.0 / degree[node] } else { 0.0 };
for j in 0..n {
if adj[[node, j]] != 0.0 {
vote_ability[j] = (vote_ability[j] - deg_inv).max(0.0);
}
}
}
_ => break, }
}
Ok(selected)
}
pub fn compute_with_scores(&self, adj: &Array2<f64>) -> Result<Vec<(usize, f64)>> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
}
let k = self.k.min(n);
let degree: Vec<f64> = (0..n)
.map(|i| adj.row(i).iter().filter(|&&x| x != 0.0).count() as f64)
.collect();
let mut vote_ability = vec![1.0_f64; n];
let mut selected = Vec::with_capacity(k);
let mut is_selected = vec![false; n];
for _ in 0..k {
let mut scores = vec![0.0_f64; n];
for i in 0..n {
if is_selected[i] {
continue;
}
for j in 0..n {
if adj[[i, j]] != 0.0 && !is_selected[j] {
scores[i] += vote_ability[j];
}
}
}
let best = (0..n)
.filter(|&i| !is_selected[i])
.max_by(|&a, &b| {
scores[a]
.partial_cmp(&scores[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
match best {
Some(node) if scores[node] > 0.0 => {
is_selected[node] = true;
selected.push((node, scores[node]));
let deg_inv = if degree[node] > 0.0 { 1.0 / degree[node] } else { 0.0 };
for j in 0..n {
if adj[[node, j]] != 0.0 {
vote_ability[j] = (vote_ability[j] - deg_inv).max(0.0);
}
}
}
_ => break,
}
}
Ok(selected)
}
}
#[derive(Debug, Clone)]
pub struct HITSCentrality {
pub max_iter: usize,
pub tol: f64,
}
impl HITSCentrality {
pub fn new(max_iter: usize, tol: f64) -> Self {
Self { max_iter, tol }
}
pub fn compute(&self, adj: &Array2<f64>) -> Result<(Array1<f64>, Array1<f64>)> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency".into()));
}
let mut h = Array1::from_elem(n, 1.0_f64 / n as f64);
let mut a = Array1::<f64>::zeros(n);
for iter in 0..self.max_iter {
let mut new_a = Array1::<f64>::zeros(n);
for i in 0..n {
for j in 0..n {
new_a[i] += adj[[j, i]] * h[j];
}
}
let a_norm = new_a.iter().map(|&x| x * x).sum::<f64>().sqrt();
if a_norm > 1e-14 {
new_a.iter_mut().for_each(|x| *x /= a_norm);
}
let mut new_h = Array1::<f64>::zeros(n);
for i in 0..n {
for j in 0..n {
new_h[i] += adj[[i, j]] * new_a[j];
}
}
let h_norm = new_h.iter().map(|&x| x * x).sum::<f64>().sqrt();
if h_norm > 1e-14 {
new_h.iter_mut().for_each(|x| *x /= h_norm);
}
let h_diff = h
.iter()
.zip(new_h.iter())
.map(|(&a, &b)| (a - b).abs())
.fold(0.0_f64, f64::max);
let a_diff = a
.iter()
.zip(new_a.iter())
.map(|(&a, &b)| (a - b).abs())
.fold(0.0_f64, f64::max);
a = new_a;
h = new_h;
if h_diff < self.tol && a_diff < self.tol && iter > 0 {
break;
}
}
Ok((h, a))
}
}
#[derive(Debug, Clone)]
pub struct TrustCentrality;
#[derive(Debug, Clone)]
pub struct TrustCentralityResult {
pub net_trust: Array1<f64>,
pub positive_in_degree: Vec<usize>,
pub negative_in_degree: Vec<usize>,
pub trust_ratio: Array1<f64>,
pub global_balance_score: f64,
}
impl TrustCentrality {
pub fn compute(adj: &Array2<f64>) -> Result<TrustCentralityResult> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency".into()));
}
let mut net_trust = Array1::<f64>::zeros(n);
let mut pos_in = vec![0usize; n];
let mut neg_in = vec![0usize; n];
for i in 0..n {
for j in 0..n {
let w = adj[[j, i]]; if w > 0.0 {
net_trust[i] += w;
pos_in[i] += 1;
} else if w < 0.0 {
net_trust[i] += w; neg_in[i] += 1;
}
}
}
let trust_ratio = Array1::from_iter((0..n).map(|i| {
let total = pos_in[i] + neg_in[i];
if total == 0 {
0.5 } else {
pos_in[i] as f64 / total as f64
}
}));
let mut balanced = 0u64;
let mut total_triangles = 0u64;
for i in 0..n {
for j in (i + 1)..n {
for k in (j + 1)..n {
let w_ij = adj[[i, j]];
let w_jk = adj[[j, k]];
let w_ik = adj[[i, k]];
if w_ij != 0.0 && w_jk != 0.0 && w_ik != 0.0 {
total_triangles += 1;
let product = w_ij.signum() * w_jk.signum() * w_ik.signum();
if product > 0.0 {
balanced += 1;
}
}
}
}
}
let global_balance_score = if total_triangles > 0 {
balanced as f64 / total_triangles as f64
} else {
1.0 };
Ok(TrustCentralityResult {
net_trust,
positive_in_degree: pos_in,
negative_in_degree: neg_in,
trust_ratio,
global_balance_score,
})
}
pub fn propagated_trust(
adj: &Array2<f64>,
sources: &[usize],
alpha: f64,
max_iter: usize,
tol: f64,
) -> Result<Array1<f64>> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency".into()));
}
for &s in sources {
if s >= n {
return Err(GraphError::InvalidParameter {
param: "source node".into(),
value: s.to_string(),
expected: format!("< {n}"),
context: "TrustCentrality::propagated_trust".into(),
});
}
}
let mut trans = Array2::<f64>::zeros((n, n));
for i in 0..n {
let row_sum: f64 = (0..n).map(|j| adj[[i, j]].max(0.0)).sum();
if row_sum > 1e-14 {
for j in 0..n {
if adj[[i, j]] > 0.0 {
trans[[i, j]] = adj[[i, j]] / row_sum;
}
}
}
}
let mut personalization = Array1::<f64>::zeros(n);
for &s in sources {
personalization[s] = 1.0;
}
let src_sum = personalization.sum();
if src_sum > 0.0 {
personalization.iter_mut().for_each(|x| *x /= src_sum);
}
let mut trust = personalization.clone();
for _ in 0..max_iter {
let mut new_trust = Array1::<f64>::zeros(n);
for i in 0..n {
for j in 0..n {
new_trust[i] += trans[[j, i]] * trust[j];
}
new_trust[i] = (1.0 - alpha) * new_trust[i] + alpha * personalization[i];
}
let diff = trust
.iter()
.zip(new_trust.iter())
.map(|(&a, &b)| (a - b).abs())
.fold(0.0_f64, f64::max);
trust = new_trust;
if diff < tol {
break;
}
}
Ok(trust)
}
}
#[derive(Debug, Clone)]
pub struct CoreDecomposition {
pub core_numbers: Vec<usize>,
pub shell_index: Vec<usize>,
pub max_core: usize,
pub num_nodes: usize,
}
impl CoreDecomposition {
pub fn compute(adj: &Array2<f64>) -> Self {
let n = adj.nrows();
if n == 0 {
return Self {
core_numbers: Vec::new(),
shell_index: Vec::new(),
max_core: 0,
num_nodes: 0,
};
}
let mut degree: Vec<usize> = (0..n)
.map(|i| (0..n).filter(|&j| adj[[i, j]] != 0.0).count())
.collect();
let mut core_numbers = vec![0usize; n];
let mut removed = vec![false; n];
let max_deg = degree.iter().copied().max().unwrap_or(0);
let mut bins: Vec<Vec<usize>> = vec![Vec::new(); max_deg + 1];
for i in 0..n {
bins[degree[i]].push(i);
}
let mut current_core = 0usize;
let mut order: Vec<usize> = Vec::with_capacity(n);
for d in 0..=max_deg {
let bin_nodes: Vec<usize> = bins[d].clone();
for &v in &bin_nodes {
if removed[v] {
continue;
}
current_core = current_core.max(d);
core_numbers[v] = current_core;
removed[v] = true;
order.push(v);
for u in 0..n {
if adj[[v, u]] != 0.0 && !removed[u] {
degree[u] = degree[u].saturating_sub(1);
bins[degree[u]].push(u);
}
}
}
}
let max_core = core_numbers.iter().copied().max().unwrap_or(0);
let shell_index = core_numbers.clone();
Self { core_numbers, shell_index, max_core, num_nodes: n }
}
pub fn k_core_nodes(&self, k: usize) -> Vec<usize> {
(0..self.num_nodes)
.filter(|&i| self.core_numbers[i] >= k)
.collect()
}
pub fn k_shell_nodes(&self, k: usize) -> Vec<usize> {
(0..self.num_nodes)
.filter(|&i| self.core_numbers[i] == k)
.collect()
}
pub fn main_core_nodes(&self) -> Vec<usize> {
self.k_core_nodes(self.max_core)
}
pub fn degeneracy_ordering(&self) -> Vec<usize> {
let mut order: Vec<usize> = (0..self.num_nodes).collect();
order.sort_by_key(|&i| self.core_numbers[i]);
order
}
pub fn shell_distribution(&self) -> Vec<(usize, usize)> {
let mut counts: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
for &c in &self.core_numbers {
*counts.entry(c).or_insert(0) += 1;
}
let mut dist: Vec<(usize, usize)> = counts.into_iter().collect();
dist.sort_by_key(|&(k, _)| k);
dist
}
}
#[derive(Debug, Clone)]
pub struct EffectiveResistance {
pub l_plus: Array2<f64>,
pub resistance_matrix: Array2<f64>,
pub num_nodes: usize,
}
impl EffectiveResistance {
pub fn compute(adj: &Array2<f64>) -> Result<Self> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency".into()));
}
let mut lap = Array2::<f64>::zeros((n, n));
for i in 0..n {
let deg: f64 = adj.row(i).iter().map(|&x| x.abs()).sum();
lap[[i, i]] = deg;
for j in 0..n {
if i != j {
lap[[i, j]] = -adj[[i, j]];
}
}
}
let mut m = lap.clone();
let inv_n = 1.0 / n as f64;
for i in 0..n {
for j in 0..n {
m[[i, j]] += inv_n;
}
}
let m_inv = invert_matrix(&m)?;
let mut l_plus = m_inv;
for i in 0..n {
for j in 0..n {
l_plus[[i, j]] -= inv_n;
}
}
let mut r = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
r[[i, j]] = (l_plus[[i, i]] + l_plus[[j, j]] - 2.0 * l_plus[[i, j]]).max(0.0);
}
}
Ok(Self { l_plus, resistance_matrix: r, num_nodes: n })
}
pub fn resistance(&self, i: usize, j: usize) -> Result<f64> {
let n = self.num_nodes;
if i >= n || j >= n {
return Err(GraphError::InvalidParameter {
param: "i or j".into(),
value: format!("{i}, {j}"),
expected: format!("< {n}"),
context: "EffectiveResistance::resistance".into(),
});
}
Ok(self.resistance_matrix[[i, j]])
}
pub fn kirchhoff_index(&self) -> f64 {
let n = self.num_nodes;
let mut kf = 0.0_f64;
for i in 0..n {
for j in (i + 1)..n {
kf += self.resistance_matrix[[i, j]];
}
}
kf
}
pub fn resistance_centre(&self) -> usize {
let n = self.num_nodes;
let avg_resistance: Vec<f64> = (0..n).map(|i| {
(0..n).map(|j| self.resistance_matrix[[i, j]]).sum::<f64>() / n as f64
}).collect();
avg_resistance
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0)
}
pub fn resistance_diameter(&self) -> f64 {
self.resistance_matrix
.iter()
.cloned()
.fold(0.0_f64, f64::max)
}
}
fn invert_matrix(a: &Array2<f64>) -> Result<Array2<f64>> {
let n = a.nrows();
let mut aug: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row: Vec<f64> = (0..n).map(|j| a[[i, j]]).collect();
row.extend((0..n).map(|j| if i == j { 1.0 } else { 0.0 }));
row
})
.collect();
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[col][col].abs();
for row in (col + 1)..n {
if aug[row][col].abs() > max_val {
max_val = aug[row][col].abs();
max_row = row;
}
}
if max_val < 1e-14 {
return Err(GraphError::LinAlgError {
operation: "matrix_inversion".into(),
details: "matrix is singular or near-singular".into(),
});
}
aug.swap(col, max_row);
let pivot = aug[col][col];
for k in col..(2 * n) {
aug[col][k] /= pivot;
}
for row in 0..n {
if row != col {
let factor = aug[row][col];
for k in col..(2 * n) {
let val = aug[col][k];
aug[row][k] -= factor * val;
}
}
}
}
let mut inv = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
inv[[i, j]] = aug[i][n + j];
}
}
Ok(inv)
}
#[cfg(test)]
mod tests {
use super::*;
fn complete4() -> Array2<f64> {
let mut adj = Array2::<f64>::zeros((4, 4));
for i in 0..4 {
for j in 0..4 {
if i != j {
adj[[i, j]] = 1.0;
}
}
}
adj
}
fn path4() -> Array2<f64> {
let mut adj = Array2::<f64>::zeros((4, 4));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
adj[[2, 3]] = 1.0; adj[[3, 2]] = 1.0;
adj
}
#[test]
fn test_voterank_returns_k_nodes() {
let adj = complete4();
let vr = VoteRankCentrality::new(3);
let spreaders = vr.compute(&adj).unwrap();
assert_eq!(spreaders.len(), 3);
let mut uniq = spreaders.clone();
uniq.sort();
uniq.dedup();
assert_eq!(uniq.len(), 3);
}
#[test]
fn test_voterank_with_scores() {
let adj = complete4();
let vr = VoteRankCentrality::new(2);
let results = vr.compute_with_scores(&adj).unwrap();
assert_eq!(results.len(), 2);
for (_, score) in &results {
assert!(*score > 0.0);
}
}
#[test]
fn test_hits_undirected_symmetric() {
let adj = complete4();
let hits = HITSCentrality::new(100, 1e-10);
let (h, a) = hits.compute(&adj).unwrap();
assert_eq!(h.len(), 4);
assert_eq!(a.len(), 4);
let h_vals: Vec<f64> = h.to_vec();
let max_h = h_vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min_h = h_vals.iter().cloned().fold(f64::INFINITY, f64::min);
assert!((max_h - min_h).abs() < 1e-6, "Hub scores should be equal on K4: max={max_h}, min={min_h}");
}
#[test]
fn test_hits_directed_hub_auth() {
let mut adj = Array2::<f64>::zeros((4, 4));
adj[[0, 1]] = 1.0;
adj[[0, 2]] = 1.0;
adj[[0, 3]] = 1.0;
let hits = HITSCentrality::new(100, 1e-10);
let (h, a) = hits.compute(&adj).unwrap();
assert!(h[0] > h[1].max(h[2]).max(h[3]));
assert!(a[1] > a[0]);
}
#[test]
fn test_trust_centrality_balanced_triangle() {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
adj[[0, 2]] = 1.0; adj[[2, 0]] = 1.0;
let res = TrustCentrality::compute(&adj).unwrap();
assert_eq!(res.global_balance_score, 1.0); for &nt in res.net_trust.iter() {
assert!(nt > 0.0);
}
}
#[test]
fn test_trust_centrality_imbalanced() {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
adj[[0, 2]] = -1.0; adj[[2, 0]] = -1.0;
let res = TrustCentrality::compute(&adj).unwrap();
assert!((res.global_balance_score - 0.0).abs() < 1e-9);
}
#[test]
fn test_core_decomposition_path() {
let adj = path4();
let core = CoreDecomposition::compute(&adj);
for &c in &core.core_numbers {
assert_eq!(c, 1, "All path nodes should have core number 1");
}
assert_eq!(core.max_core, 1);
}
#[test]
fn test_core_decomposition_complete() {
let adj = complete4();
let core = CoreDecomposition::compute(&adj);
for &c in &core.core_numbers {
assert_eq!(c, 3);
}
assert_eq!(core.max_core, 3);
assert_eq!(core.main_core_nodes().len(), 4);
}
#[test]
fn test_effective_resistance_path() {
let adj = path4();
let er = EffectiveResistance::compute(&adj).unwrap();
let r01 = er.resistance(0, 1).unwrap();
let r02 = er.resistance(0, 2).unwrap();
let r03 = er.resistance(0, 3).unwrap();
assert!((r01 - 1.0).abs() < 1e-8, "R(0,1)={r01}");
assert!((r02 - 2.0).abs() < 1e-8, "R(0,2)={r02}");
assert!((r03 - 3.0).abs() < 1e-8, "R(0,3)={r03}");
}
#[test]
fn test_effective_resistance_complete() {
let adj = complete4();
let er = EffectiveResistance::compute(&adj).unwrap();
let r01 = er.resistance(0, 1).unwrap();
assert!((r01 - 2.0 / 4.0).abs() < 1e-8, "R(0,1) in K4 = {r01}");
}
#[test]
fn test_kirchhoff_index() {
let adj = path4();
let er = EffectiveResistance::compute(&adj).unwrap();
let kf = er.kirchhoff_index();
assert!((kf - 10.0).abs() < 1e-7, "Kf(P4) = {kf}");
}
#[test]
fn test_resistance_centre_path() {
let adj = path4();
let er = EffectiveResistance::compute(&adj).unwrap();
let centre = er.resistance_centre();
assert!(centre == 1 || centre == 2);
}
#[test]
fn test_propagated_trust() {
let mut adj = Array2::<f64>::zeros((4, 4));
adj[[0, 1]] = 1.0;
adj[[1, 2]] = 0.8;
adj[[2, 3]] = 0.6;
let trust = TrustCentrality::propagated_trust(&adj, &[0], 0.15, 100, 1e-9).unwrap();
assert!(trust[0] > trust[3], "Source trust should be > distal trust");
}
}