use std::collections::{HashMap, HashSet, VecDeque};
use crate::core::types::{BaseGraph, GraphConstructor, NodeId};
use petgraph::EdgeType;
fn order_pair(a: usize, b: usize) -> (usize, usize) {
if a <= b { (a, b) } else { (b, a) }
}
pub fn diameter<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
) -> Option<usize> {
if graph.is_empty() {
return None;
}
let mut max_distance = 0;
for start_node in graph.node_ids() {
let distances = bfs_distances(graph, start_node);
if distances.len() != graph.node_count() {
return None;
}
if let Some(&max_dist) = distances.values().max() {
if max_dist > max_distance {
max_distance = max_dist;
}
}
}
Some(max_distance)
}
pub fn radius<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
) -> Option<usize> {
if graph.is_empty() {
return None;
}
let mut min_eccentricity = usize::MAX;
for start_node in graph.node_ids() {
let distances = bfs_distances(graph, start_node);
if distances.len() != graph.node_count() {
return None;
}
if let Some(&max_dist) = distances.values().max() {
if max_dist < min_eccentricity {
min_eccentricity = max_dist;
}
}
}
Some(min_eccentricity)
}
pub fn average_clustering_coefficient<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
) -> f64 {
if graph.is_empty() {
return 0.0;
}
let coefficients: Vec<f64> = graph
.node_ids()
.map(|node| super::node_metrics::clustering_coefficient(graph, node))
.collect();
coefficients.iter().sum::<f64>() / coefficients.len() as f64
}
pub fn transitivity<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
) -> f64 {
use petgraph::visit::NodeIndexable;
let mut edge_set: HashSet<(usize, usize), rustc_hash::FxBuildHasher> =
HashSet::with_capacity_and_hasher(graph.edge_count(), rustc_hash::FxBuildHasher);
for (u, v, _w) in graph.edges() {
let (lo, hi) = order_pair(u.index(), v.index());
edge_set.insert((lo, hi));
}
let bound = graph.as_petgraph().node_bound();
let mut degree = vec![0usize; bound];
for node in graph.node_ids() {
degree[node.index()] = graph.neighbors(node).count();
}
let higher_rank =
|a: usize, v: usize| degree[a] > degree[v] || (degree[a] == degree[v] && a > v);
let mut triples = 0usize;
for &k in °ree {
if k >= 2 {
triples += k * (k - 1) / 2;
}
}
if triples == 0 {
return 0.0;
}
let mut higher: Vec<usize> = Vec::new();
let mut distinct_triangles = 0usize;
for node in graph.node_ids() {
let vi = node.index();
higher.clear();
higher.extend(
graph
.neighbors(node)
.map(|nbr| nbr.index())
.filter(|&nbr| higher_rank(nbr, vi)),
);
let k = higher.len();
for i in 0..k {
let ni = higher[i];
for &other in higher.iter().skip(i + 1) {
let (lo, hi) = order_pair(ni, other);
if edge_set.contains(&(lo, hi)) {
distinct_triangles += 1;
}
}
}
}
(3 * distinct_triangles) as f64 / triples as f64
}
pub fn average_path_length<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
) -> Option<f64> {
if graph.is_empty() {
return None;
}
let mut total_distance = 0.0;
let mut pair_count = 0;
for start_node in graph.node_ids() {
let distances = bfs_distances(graph, start_node);
if distances.len() != graph.node_count() {
return None;
}
for &dist in distances.values() {
if dist > 0 {
total_distance += dist as f64;
pair_count += 1;
}
}
}
if pair_count == 0 {
return Some(0.0);
}
Some(total_distance / pair_count as f64)
}
pub fn assortativity<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
) -> f64 {
if graph.edge_count() == 0 {
return 0.0;
}
let mut sum_jk = 0.0;
let mut sum_j = 0.0;
let mut sum_k = 0.0;
let mut sum_j2 = 0.0;
let mut sum_k2 = 0.0;
let m = (graph.edge_count() * 2) as f64;
for (u, v, _) in graph.edges() {
let j = graph.degree(u).unwrap_or(0) as f64;
let k = graph.degree(v).unwrap_or(0) as f64;
sum_jk += 2.0 * j * k;
sum_j += j + k;
sum_k += j + k;
sum_j2 += j * j + k * k;
sum_k2 += j * j + k * k;
}
let numerator = sum_jk / m - (sum_j / m) * (sum_k / m);
let denominator =
((sum_j2 / m - (sum_j / m).powi(2)) * (sum_k2 / m - (sum_k / m).powi(2))).sqrt();
if denominator == 0.0 {
return 0.0;
}
numerator / denominator
}
fn bfs_distances<A, W, Ty: GraphConstructor<A, W> + EdgeType>(
graph: &BaseGraph<A, W, Ty>,
start: NodeId,
) -> HashMap<NodeId, usize> {
let mut distances = HashMap::new();
let mut queue = VecDeque::new();
distances.insert(start, 0);
queue.push_back(start);
while let Some(node) = queue.pop_front() {
let dist = distances[&node];
for neighbor in graph.neighbors(node) {
if let std::collections::hash_map::Entry::Vacant(e) = distances.entry(neighbor) {
e.insert(dist + 1);
queue.push_back(neighbor);
}
}
}
distances
}
#[cfg(test)]
mod tests {
#[test]
fn test_assortativity_is_symmetric_newman_coefficient() {
use crate::core::types::Graph;
use crate::metrics::assortativity;
let mut g: Graph<i32, f64> = Graph::new();
let center = g.add_node(0);
for i in 1..=4 {
let leaf = g.add_node(i);
g.add_edge(center, leaf, 1.0);
}
let r = assortativity(&g);
assert!(
(r - (-1.0)).abs() < 1e-9,
"star degree assortativity should be -1.0, got {r}"
);
}
use super::*;
use crate::core::types::Graph;
#[test]
fn test_diameter() {
let mut g = Graph::<i32, f64>::new();
let n1 = g.add_node(1);
let n2 = g.add_node(2);
let n3 = g.add_node(3);
g.add_edge(n1, n2, 1.0);
g.add_edge(n2, n3, 1.0);
assert_eq!(diameter(&g), Some(2));
}
#[test]
fn test_diameter_disconnected() {
let g = Graph::<i32, f64>::new();
assert_eq!(diameter(&g), None);
}
#[test]
fn test_radius() {
let mut g = Graph::<i32, f64>::new();
let n1 = g.add_node(1);
let n2 = g.add_node(2);
let n3 = g.add_node(3);
g.add_edge(n1, n2, 1.0);
g.add_edge(n2, n3, 1.0);
assert_eq!(radius(&g), Some(1)); }
#[test]
fn test_average_clustering_coefficient() {
let mut g = Graph::<i32, f64>::new();
let n1 = g.add_node(1);
let n2 = g.add_node(2);
let n3 = g.add_node(3);
g.add_edge(n1, n2, 1.0);
g.add_edge(n2, n3, 1.0);
g.add_edge(n3, n1, 1.0);
assert!((average_clustering_coefficient(&g) - 1.0).abs() < 0.001);
}
#[test]
fn test_transitivity() {
let mut g = Graph::<i32, f64>::new();
let n1 = g.add_node(1);
let n2 = g.add_node(2);
let n3 = g.add_node(3);
g.add_edge(n1, n2, 1.0);
g.add_edge(n2, n3, 1.0);
g.add_edge(n3, n1, 1.0);
assert!((transitivity(&g) - 1.0).abs() < 0.001);
}
#[test]
fn test_transitivity_fractional() {
let mut g = Graph::<i32, f64>::new();
let n0 = g.add_node(0);
let n1 = g.add_node(1);
let n2 = g.add_node(2);
let n3 = g.add_node(3);
g.add_edge(n0, n1, 1.0);
g.add_edge(n1, n2, 1.0);
g.add_edge(n2, n0, 1.0);
g.add_edge(n2, n3, 1.0);
assert!((transitivity(&g) - 0.6).abs() < 1e-9);
}
#[test]
fn test_transitivity_two_triangles_sharing_an_edge() {
let mut g = Graph::<i32, f64>::new();
let n0 = g.add_node(0);
let n1 = g.add_node(1);
let n2 = g.add_node(2);
let n3 = g.add_node(3);
g.add_edge(n0, n1, 1.0);
g.add_edge(n1, n2, 1.0);
g.add_edge(n2, n0, 1.0);
g.add_edge(n1, n3, 1.0);
g.add_edge(n2, n3, 1.0);
assert!((transitivity(&g) - 0.75).abs() < 1e-9);
}
#[test]
fn test_average_path_length() {
let mut g = Graph::<i32, f64>::new();
let n1 = g.add_node(1);
let n2 = g.add_node(2);
let n3 = g.add_node(3);
g.add_edge(n1, n2, 1.0);
g.add_edge(n2, n3, 1.0);
let avg = average_path_length(&g).expect("Connected graph should have average path length");
assert!((avg - 1.333).abs() < 0.01);
}
#[test]
fn test_assortativity() {
let mut g = Graph::<i32, f64>::new();
let n1 = g.add_node(1);
let n2 = g.add_node(2);
let n3 = g.add_node(3);
let n4 = g.add_node(4);
g.add_edge(n1, n2, 1.0);
g.add_edge(n2, n3, 1.0);
g.add_edge(n3, n4, 1.0);
let assort = assortativity(&g);
assert!((-1.0..=1.0).contains(&assort));
}
}