use crate::core::view::NodeId;
use crate::core::CentralityError as CoreCentralityError;
use crate::graph::graph::Graph;
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
#[derive(Debug, thiserror::Error)]
pub enum CentralityError {
#[error("Invalid node reference: node ID {0} not found")]
InvalidNode(NodeId),
#[error("Distance calculation overflowed")]
DistanceOverflow,
}
impl From<CoreCentralityError> for CentralityError {
fn from(error: CoreCentralityError) -> Self {
match error {
CoreCentralityError::InvalidNode(id) => CentralityError::InvalidNode(id),
CoreCentralityError::InvalidParameter { .. } => CentralityError::DistanceOverflow,
}
}
}
#[deprecated(
since = "3.0.0",
note = "superseded by `xgraph::core::Centrality`, which is implemented for every graph \
view and adds eigenvector centrality, PageRank and Katz centrality"
)]
pub trait Centrality<W, N, E> {
fn degree_centrality(&self) -> Result<HashMap<NodeId, usize>, CentralityError>;
fn betweenness_centrality(&self) -> Result<HashMap<NodeId, f64>, CentralityError>;
fn closeness_centrality(&self) -> Result<HashMap<NodeId, f64>, CentralityError>;
}
#[allow(deprecated)]
impl<W, N, E> Centrality<W, N, E> for Graph<W, N, E>
where
W: Copy + Default + PartialEq,
N: Clone + Eq + Hash + Debug,
E: Clone + Default + Debug,
{
fn degree_centrality(&self) -> Result<HashMap<NodeId, usize>, CentralityError> {
Ok(crate::core::Centrality::degree_centrality(self))
}
fn betweenness_centrality(&self) -> Result<HashMap<NodeId, f64>, CentralityError> {
crate::core::Centrality::betweenness_centrality(self).map_err(CentralityError::from)
}
fn closeness_centrality(&self) -> Result<HashMap<NodeId, f64>, CentralityError> {
crate::core::Centrality::closeness_centrality(self).map_err(CentralityError::from)
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
use crate::core::view::NodeId;
use float_cmp::approx_eq;
const TOLERANCE: f64 = 1e-5;
fn assert_approx_eq(a: f64, b: f64) {
assert!(approx_eq!(f64, a, b, epsilon = TOLERANCE), "{} ≈ {}", a, b);
}
#[test]
fn test_degree_centrality() {
let matrix = vec![vec![0, 1, 0], vec![1, 0, 1], vec![0, 1, 0]];
let graph = Graph::from_adjacency_matrix(&matrix, false, (), ()).unwrap();
let centrality = graph.degree_centrality().unwrap();
assert_eq!(centrality[&NodeId::from_index(0)], 1);
assert_eq!(centrality[&NodeId::from_index(1)], 2);
assert_eq!(centrality[&NodeId::from_index(2)], 1);
}
#[test]
fn test_betweenness_centrality() {
let matrix = vec![
vec![0, 1, 0, 0],
vec![1, 0, 1, 1],
vec![0, 1, 0, 0],
vec![0, 1, 0, 0],
];
let graph = Graph::from_adjacency_matrix(&matrix, false, (), ()).unwrap();
let betweenness = graph.betweenness_centrality().unwrap();
assert_approx_eq(betweenness[&NodeId::from_index(1)], 1.0);
}
#[test]
fn test_closeness_centrality() {
let matrix = vec![vec![0, 1, 1], vec![1, 0, 1], vec![1, 1, 0]];
let graph = Graph::from_adjacency_matrix(&matrix, false, (), ()).unwrap();
let closeness = graph.closeness_centrality().unwrap();
assert_approx_eq(closeness[&NodeId::from_index(0)], 1.0);
}
#[test]
fn test_empty_graph() {
let graph = Graph::<u32, (), ()>::new(false);
assert!(graph.degree_centrality().unwrap().is_empty());
assert!(graph.betweenness_centrality().unwrap().is_empty());
assert!(graph.closeness_centrality().unwrap().is_empty());
}
#[test]
fn test_betweenness_with_sparse_ids() {
let mut graph = Graph::<u32, (), ()>::new(false);
let ids: Vec<_> = (0..4).map(|_| graph.add_node(())).collect();
graph.add_edge(ids[1], ids[2], 1, ()).unwrap();
graph.add_edge(ids[2], ids[3], 1, ()).unwrap();
graph.remove_node(ids[0]).unwrap();
let betweenness = graph.betweenness_centrality().unwrap();
assert!(betweenness.get(&ids[2]).copied().unwrap_or(0.0) > 0.0);
assert_eq!(betweenness.get(&ids[1]).copied().unwrap_or(0.0), 0.0);
}
}