use crate::base::{EdgeWeight, Graph, IndexType, Node};
use crate::error::{GraphError, Result};
use scirs2_core::random::prelude::*;
use std::collections::{HashMap, VecDeque};
fn bfs_distances<N, E, Ix>(graph: &Graph<N, E, Ix>, source: &N) -> HashMap<N, usize>
where
N: Node + Clone + std::fmt::Debug,
E: EdgeWeight,
Ix: IndexType,
{
let mut dist: HashMap<N, usize> = HashMap::new();
let mut queue: VecDeque<N> = VecDeque::new();
dist.insert(source.clone(), 0);
queue.push_back(source.clone());
while let Some(current) = queue.pop_front() {
let current_dist = *dist.get(¤t).unwrap_or(&0);
let neighbors = match graph.neighbors(¤t) {
Ok(nb) => nb,
Err(_) => continue,
};
for nb in neighbors {
if !dist.contains_key(&nb) {
dist.insert(nb.clone(), current_dist + 1);
queue.push_back(nb);
}
}
}
dist
}
pub fn eccentricity<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Option<Vec<(N, usize)>>
where
N: Node + Clone + std::fmt::Debug + Ord,
E: EdgeWeight,
Ix: IndexType,
{
let mut nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
if nodes.is_empty() {
return Some(vec![]);
}
nodes.sort();
let n = nodes.len();
let mut result: Vec<(N, usize)> = Vec::with_capacity(n);
for u in &nodes {
let dist = bfs_distances(graph, u);
if dist.len() < n {
return None;
}
let ecc = dist.values().copied().max().unwrap_or(0);
result.push((u.clone(), ecc));
}
Some(result)
}
pub fn diameter<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Option<usize>
where
N: Node + Clone + std::fmt::Debug + Ord,
E: EdgeWeight,
Ix: IndexType,
{
eccentricity(graph).and_then(|eccs| eccs.into_iter().map(|(_, e)| e).max())
}
pub fn radius<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Option<usize>
where
N: Node + Clone + std::fmt::Debug + Ord,
E: EdgeWeight,
Ix: IndexType,
{
eccentricity(graph).and_then(|eccs| eccs.into_iter().map(|(_, e)| e).min())
}
pub fn periphery<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Vec<N>
where
N: Node + Clone + std::fmt::Debug + Ord,
E: EdgeWeight,
Ix: IndexType,
{
let eccs = match eccentricity(graph) {
Some(e) if !e.is_empty() => e,
_ => return vec![],
};
let diam = eccs.iter().map(|(_, e)| *e).max().unwrap_or(0);
eccs.into_iter()
.filter_map(|(n, e)| if e == diam { Some(n) } else { None })
.collect()
}
pub fn center<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Vec<N>
where
N: Node + Clone + std::fmt::Debug + Ord,
E: EdgeWeight,
Ix: IndexType,
{
let eccs = match eccentricity(graph) {
Some(e) if !e.is_empty() => e,
_ => return vec![],
};
let rad = eccs.iter().map(|(_, e)| *e).min().unwrap_or(0);
eccs.into_iter()
.filter_map(|(n, e)| if e == rad { Some(n) } else { None })
.collect()
}
pub fn average_path_length<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Option<f64>
where
N: Node + Clone + std::fmt::Debug + Ord,
E: EdgeWeight,
Ix: IndexType,
{
let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
let n = nodes.len();
if n <= 1 {
return Some(0.0);
}
let mut total = 0u64;
let pairs = (n as u64) * (n as u64 - 1);
for u in &nodes {
let dist = bfs_distances(graph, u);
if dist.len() < n {
return None; }
for v in &nodes {
if u != v {
total += *dist.get(v).unwrap_or(&0) as u64;
}
}
}
Some(total as f64 / pairs as f64)
}
pub fn global_efficiency<N, E, Ix>(graph: &Graph<N, E, Ix>) -> Option<f64>
where
N: Node + Clone + std::fmt::Debug + Ord,
E: EdgeWeight,
Ix: IndexType,
{
let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
let n = nodes.len();
if n <= 1 {
return Some(0.0);
}
let mut sum_inv = 0.0f64;
let pairs = (n * (n - 1)) as f64;
for u in &nodes {
let dist = bfs_distances(graph, u);
for v in &nodes {
if u != v {
if let Some(&d) = dist.get(v) {
if d > 0 {
sum_inv += 1.0 / d as f64;
}
}
}
}
}
Some(sum_inv / pairs)
}
pub fn local_efficiency<N, E, Ix>(graph: &Graph<N, E, Ix>, node: &N) -> Result<f64>
where
N: Node + Clone + std::fmt::Debug + Ord,
E: EdgeWeight,
Ix: IndexType,
{
if !graph.has_node(node) {
return Err(GraphError::node_not_found(format!("{node:?}")));
}
let neighbors = graph.neighbors(node)?;
let k = neighbors.len();
if k < 2 {
return Ok(0.0);
}
let nb_set: std::collections::HashSet<N> = neighbors.iter().cloned().collect();
let mut sub: Graph<N, f64, u32> = Graph::new();
for nb in &neighbors {
sub.add_node(nb.clone());
}
for nb_u in &neighbors {
let nb_u_neighbors = graph.neighbors(nb_u).unwrap_or_default();
for nb_v in &nb_u_neighbors {
if nb_set.contains(nb_v) && nb_u < nb_v {
let _ = sub.add_edge(nb_u.clone(), nb_v.clone(), 1.0f64);
}
}
}
let eff = global_efficiency(&sub).unwrap_or(0.0);
Ok(eff)
}
fn average_clustering<N, E, Ix>(graph: &Graph<N, E, Ix>) -> f64
where
N: Node + Clone + std::fmt::Debug + Ord,
E: EdgeWeight,
Ix: IndexType,
{
let nodes: Vec<N> = graph.nodes().into_iter().cloned().collect();
let mut total = 0.0f64;
let mut count = 0usize;
for u in &nodes {
let nb = graph.neighbors(u).unwrap_or_default();
let k = nb.len();
if k < 2 {
continue;
}
let nb_set: std::collections::HashSet<N> = nb.iter().cloned().collect();
let mut triangles = 0usize;
for i in 0..nb.len() {
for j in (i + 1)..nb.len() {
if graph.has_edge(&nb[i], &nb[j]) {
triangles += 1;
}
}
}
let _ = nb_set; let c_u = 2.0 * triangles as f64 / (k * (k - 1)) as f64;
total += c_u;
count += 1;
}
if count == 0 {
0.0
} else {
total / count as f64
}
}
pub fn small_world_coefficient<N, E, Ix, R>(
graph: &Graph<N, E, Ix>,
n_random: usize,
rng: &mut R,
) -> Result<f64>
where
N: Node + Clone + std::fmt::Debug + Ord,
E: EdgeWeight,
Ix: IndexType,
R: Rng,
{
let n = graph.node_count();
let m = graph.edge_count();
if n < 3 {
return Err(GraphError::InvalidGraph(
"small_world_coefficient: graph must have ≥ 3 nodes".to_string(),
));
}
if n_random == 0 {
return Err(GraphError::InvalidGraph(
"small_world_coefficient: n_random must be ≥ 1".to_string(),
));
}
let c = average_clustering(graph);
let l = match average_path_length(graph) {
Some(v) if v > 0.0 => v,
_ => {
return Err(GraphError::InvalidGraph(
"small_world_coefficient: graph is disconnected or trivial".to_string(),
))
}
};
let max_edges = n * (n - 1) / 2;
if m > max_edges {
return Err(GraphError::InvalidGraph(
"small_world_coefficient: edge count exceeds maximum for simple graph".to_string(),
));
}
let mut sum_c_rand = 0.0f64;
let mut sum_l_rand = 0.0f64;
let mut valid_samples = 0usize;
for _ in 0..n_random {
let rg = crate::generators::random_graphs::erdos_renyi_g_nm(n, m, rng)
.unwrap_or_else(|_| crate::generators::erdos_renyi_graph(n, m as f64 / (max_edges as f64).max(1.0), rng).unwrap_or_default());
if let Some(l_r) = average_path_length(&rg) {
if l_r > 0.0 {
sum_c_rand += average_clustering(&rg);
sum_l_rand += l_r;
valid_samples += 1;
}
}
}
if valid_samples == 0 {
return Err(GraphError::ComputationError(
"small_world_coefficient: all random reference graphs were disconnected".to_string(),
));
}
let c_rand = sum_c_rand / valid_samples as f64;
let l_rand = sum_l_rand / valid_samples as f64;
if c_rand <= 0.0 || l_rand <= 0.0 || l <= 0.0 {
return Err(GraphError::ComputationError(
"small_world_coefficient: reference graph has degenerate clustering or path length"
.to_string(),
));
}
let sigma = (c / c_rand) / (l / l_rand);
Ok(sigma)
}
pub fn scale_free_exponent(degree_dist: &[usize]) -> Option<f64> {
if degree_dist.is_empty() {
return None;
}
let degrees: Vec<usize> = degree_dist.iter().copied().filter(|&d| d >= 1).collect();
if degrees.is_empty() {
return None;
}
let mut sorted = degrees.clone();
sorted.sort_unstable();
let distinct: Vec<usize> = {
let mut v: Vec<usize> = sorted.iter().copied().collect();
v.dedup();
v
};
if distinct.len() < 2 {
return None;
}
let candidates = &distinct[..distinct.len() - 1];
let mut best_ks_stat = f64::INFINITY;
let mut best_gamma: Option<f64> = None;
for &k_min in candidates {
let tail: Vec<usize> = sorted.iter().copied().filter(|&d| d >= k_min).collect();
let n_tail = tail.len();
if n_tail < 5 {
continue;
}
let ln_sum: f64 = tail
.iter()
.map(|&k| ((k as f64) / (k_min as f64 - 0.5)).ln())
.sum();
if ln_sum <= 0.0 {
continue;
}
let gamma = 1.0 + n_tail as f64 * (1.0 / ln_sum);
if !gamma.is_finite() || gamma <= 1.0 {
continue;
}
let ks = ks_statistic_discrete(&tail, gamma, k_min);
if ks < best_ks_stat {
best_ks_stat = ks;
best_gamma = Some(gamma);
}
}
best_gamma
}
fn ks_statistic_discrete(data: &[usize], gamma: f64, k_min: usize) -> f64 {
let n = data.len();
if n == 0 {
return f64::INFINITY;
}
let theoretical_cdf = |k: usize| -> f64 {
if k < k_min {
return 0.0;
}
let ratio = (k as f64 + 0.5) / (k_min as f64 - 0.5);
(1.0 - ratio.powf(1.0 - gamma)).max(0.0).min(1.0)
};
let mut max_diff = 0.0f64;
for (i, &k) in data.iter().enumerate() {
let emp_cdf = (i + 1) as f64 / n as f64;
let theo_cdf = theoretical_cdf(k);
let diff = (emp_cdf - theo_cdf).abs();
if diff > max_diff {
max_diff = diff;
}
}
max_diff
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generators::{
complete_graph, cycle_graph, path_graph, star_graph,
};
use crate::generators::random_graphs::erdos_renyi_g_np;
use scirs2_core::random::prelude::*;
#[test]
fn test_diameter_path() {
let g = path_graph(6).expect("path_graph failed");
assert_eq!(diameter(&g), Some(5));
}
#[test]
fn test_diameter_complete() {
let g = complete_graph(10).expect("complete_graph failed");
assert_eq!(diameter(&g), Some(1));
}
#[test]
fn test_radius_cycle() {
let g = cycle_graph(8).expect("cycle_graph failed");
assert_eq!(radius(&g), Some(4));
}
#[test]
fn test_radius_path() {
let g = path_graph(5).expect("path_graph failed");
assert_eq!(radius(&g), Some(2));
}
#[test]
fn test_center_star() {
let g = star_graph(5).expect("star_graph failed");
let c = center(&g);
assert!(!c.is_empty());
assert!(c.contains(&0));
}
#[test]
fn test_periphery_path() {
let g = path_graph(5).expect("path_graph failed");
let p = periphery(&g);
assert!(p.contains(&0));
assert!(p.contains(&4));
assert_eq!(p.len(), 2);
}
#[test]
fn test_center_and_periphery_empty_graph() {
let g: Graph<usize, f64> = Graph::new();
assert!(center(&g).is_empty());
assert!(periphery(&g).is_empty());
}
#[test]
fn test_apl_complete() {
let g = complete_graph(5).expect("complete_graph failed");
let apl = average_path_length(&g).expect("apl failed");
assert!((apl - 1.0).abs() < 1e-9, "K_n has APL=1, got {apl}");
}
#[test]
fn test_apl_path() {
let g = path_graph(4).expect("path_graph failed");
let apl = average_path_length(&g).expect("apl failed");
assert!((apl - 20.0 / 12.0).abs() < 1e-9, "got {apl}");
}
#[test]
fn test_apl_trivial() {
let mut g: Graph<usize, f64> = Graph::new();
g.add_node(0);
assert_eq!(average_path_length(&g), Some(0.0));
}
#[test]
fn test_global_efficiency_complete() {
let g = complete_graph(5).expect("complete_graph failed");
let eff = global_efficiency(&g).expect("eff failed");
assert!((eff - 1.0).abs() < 1e-9, "K_n has efficiency=1, got {eff}");
}
#[test]
fn test_global_efficiency_path() {
let g = path_graph(3).expect("path_graph failed");
let eff = global_efficiency(&g).expect("eff failed");
let expected = (1.0 + 0.5 + 1.0 + 1.0 + 0.5 + 1.0) / 6.0;
assert!((eff - expected).abs() < 1e-9, "got {eff}, expected {expected}");
}
#[test]
fn test_global_efficiency_disconnected() {
let mut g: Graph<usize, f64> = Graph::new();
for i in 0..6usize {
g.add_node(i);
}
g.add_edge(0, 1, 1.0).unwrap();
g.add_edge(1, 2, 1.0).unwrap();
g.add_edge(0, 2, 1.0).unwrap();
g.add_edge(3, 4, 1.0).unwrap();
g.add_edge(4, 5, 1.0).unwrap();
g.add_edge(3, 5, 1.0).unwrap();
let eff = global_efficiency(&g).expect("eff failed");
let expected = 12.0 / 30.0;
assert!((eff - expected).abs() < 1e-9, "got {eff}");
}
#[test]
fn test_local_efficiency_triangle() {
let g = complete_graph(3).expect("complete_graph failed");
let le = local_efficiency(&g, &0).expect("le failed");
assert!((le - 1.0).abs() < 1e-9, "got {le}");
}
#[test]
fn test_local_efficiency_star_center() {
let g = star_graph(5).expect("star_graph failed");
let le = local_efficiency(&g, &0).expect("le failed");
assert!((le - 0.0).abs() < 1e-9, "got {le}");
}
#[test]
fn test_local_efficiency_missing_node() {
let g = path_graph(4).expect("path_graph failed");
assert!(local_efficiency(&g, &99).is_err());
}
#[test]
fn test_small_world_coefficient_runs() {
let mut rng = StdRng::seed_from_u64(42);
let g = crate::generators::watts_strogatz_graph(30, 4, 0.1, &mut rng)
.expect("ws failed");
let sigma = small_world_coefficient(&g, 5, &mut rng);
assert!(sigma.is_ok(), "small_world_coefficient error: {sigma:?}");
}
#[test]
fn test_small_world_invalid_input() {
let mut rng = StdRng::seed_from_u64(1);
let g: Graph<usize, f64> = Graph::new();
assert!(small_world_coefficient(&g, 5, &mut rng).is_err());
let tiny_g = path_graph(2).expect("path failed");
assert!(small_world_coefficient(&tiny_g, 5, &mut rng).is_err());
}
#[test]
fn test_scale_free_exponent_power_law() {
let mut rng = StdRng::seed_from_u64(99);
let g = crate::generators::random_graphs::barabasi_albert(200, 2, &mut rng)
.expect("ba failed");
let degrees: Vec<usize> = (0..g.node_count()).map(|i| g.degree(&i)).collect();
if let Some(gamma) = scale_free_exponent(°rees) {
assert!(gamma > 1.5 && gamma < 8.0, "gamma={gamma}");
}
}
#[test]
fn test_scale_free_exponent_empty() {
assert_eq!(scale_free_exponent(&[]), None);
assert_eq!(scale_free_exponent(&[0, 0, 0]), None);
}
#[test]
fn test_scale_free_exponent_uniform() {
let degrees = vec![3usize; 20];
let _ = scale_free_exponent(°rees);
}
}