use super::common::{GraphView, NodeId};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct LccResult {
pub coefficients: HashMap<NodeId, f64>,
pub average: f64,
}
pub fn local_clustering_coefficient(view: &GraphView) -> LccResult {
local_clustering_coefficient_directed(view, false)
}
pub fn local_clustering_coefficient_directed(view: &GraphView, directed: bool) -> LccResult {
let n = view.node_count;
if n == 0 {
return LccResult { coefficients: HashMap::new(), average: 0.0 };
}
let mut neighbors: Vec<HashSet<usize>> = Vec::with_capacity(n);
for idx in 0..n {
let mut set = HashSet::new();
for &s in view.successors(idx) {
if s != idx { set.insert(s); }
}
for &p in view.predecessors(idx) {
if p != idx { set.insert(p); }
}
neighbors.push(set);
}
let successor_sets: Option<Vec<HashSet<usize>>> = if directed {
let mut sets = Vec::with_capacity(n);
for idx in 0..n {
let set: HashSet<usize> = view.successors(idx).iter().copied().collect();
sets.push(set);
}
Some(sets)
} else {
None
};
let mut coefficients = HashMap::with_capacity(n);
let mut sum = 0.0;
for idx in 0..n {
let deg = neighbors[idx].len();
if deg < 2 {
coefficients.insert(view.index_to_node[idx], 0.0);
continue;
}
let neighbor_vec: Vec<usize> = neighbors[idx].iter().cloned().collect();
if directed {
let succ_sets = successor_sets.as_ref().unwrap();
let mut directed_edges = 0usize;
for i in 0..neighbor_vec.len() {
for j in 0..neighbor_vec.len() {
if i != j && succ_sets[neighbor_vec[i]].contains(&neighbor_vec[j]) {
directed_edges += 1;
}
}
}
let max_edges = deg * (deg - 1); let cc = directed_edges as f64 / max_edges as f64;
coefficients.insert(view.index_to_node[idx], cc);
sum += cc;
} else {
let mut triangle_edges = 0usize;
for i in 0..neighbor_vec.len() {
for j in (i + 1)..neighbor_vec.len() {
if neighbors[neighbor_vec[i]].contains(&neighbor_vec[j]) {
triangle_edges += 1;
}
}
}
let max_edges = deg * (deg - 1) / 2; let cc = triangle_edges as f64 / max_edges as f64;
coefficients.insert(view.index_to_node[idx], cc);
sum += cc;
}
}
let average = if n > 0 { sum / n as f64 } else { 0.0 };
LccResult { coefficients, average }
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use crate::common::GraphView;
#[test]
fn test_lcc_triangle() {
let index_to_node = vec![1, 2, 3];
let mut node_to_index = HashMap::new();
node_to_index.insert(1, 0);
node_to_index.insert(2, 1);
node_to_index.insert(3, 2);
let outgoing = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
let incoming = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
let view = GraphView::from_adjacency_list(3, index_to_node, node_to_index, outgoing, incoming, None);
let result = local_clustering_coefficient(&view);
for (_node, cc) in &result.coefficients {
assert!((cc - 1.0).abs() < 1e-10, "Complete triangle LCC should be 1.0, got {}", cc);
}
assert!((result.average - 1.0).abs() < 1e-10);
}
#[test]
fn test_lcc_star() {
let index_to_node = vec![1, 2, 3, 4];
let mut node_to_index = HashMap::new();
for (i, &id) in index_to_node.iter().enumerate() {
node_to_index.insert(id, i);
}
let outgoing = vec![vec![1, 2, 3], vec![0], vec![0], vec![0]];
let incoming = vec![vec![1, 2, 3], vec![0], vec![0], vec![0]];
let view = GraphView::from_adjacency_list(4, index_to_node, node_to_index, outgoing, incoming, None);
let result = local_clustering_coefficient(&view);
assert!((result.coefficients[&1] - 0.0).abs() < 1e-10);
assert!((result.coefficients[&2] - 0.0).abs() < 1e-10);
}
#[test]
fn test_lcc_empty() {
let view = GraphView::from_adjacency_list(
0, vec![], HashMap::new(), vec![], vec![], None,
);
let result = local_clustering_coefficient(&view);
assert!(result.coefficients.is_empty());
assert!((result.average - 0.0).abs() < 1e-10);
}
#[test]
fn test_lcc_directed_triangle() {
let index_to_node = vec![1, 2, 3];
let mut node_to_index = HashMap::new();
node_to_index.insert(1, 0);
node_to_index.insert(2, 1);
node_to_index.insert(3, 2);
let outgoing = vec![vec![1], vec![2], vec![0]];
let incoming = vec![vec![2], vec![0], vec![1]];
let view = GraphView::from_adjacency_list(3, index_to_node, node_to_index, outgoing, incoming, None);
let result = local_clustering_coefficient_directed(&view, true);
for (&_node, &cc) in &result.coefficients {
assert!((cc - 0.5).abs() < 1e-10, "Directed cycle triangle LCC should be 0.5, got {}", cc);
}
}
#[test]
fn test_lcc_directed_complete_triangle() {
let index_to_node = vec![1, 2, 3];
let mut node_to_index = HashMap::new();
node_to_index.insert(1, 0);
node_to_index.insert(2, 1);
node_to_index.insert(3, 2);
let outgoing = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
let incoming = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
let view = GraphView::from_adjacency_list(3, index_to_node, node_to_index, outgoing, incoming, None);
let result = local_clustering_coefficient_directed(&view, true);
for (&_node, &cc) in &result.coefficients {
assert!((cc - 1.0).abs() < 1e-10, "Fully connected directed triangle LCC should be 1.0, got {}", cc);
}
}
}