use lattix::algo::centrality::{
betweenness_centrality, closeness_centrality, degree_centrality, eigenvector_centrality, hits,
katz_centrality, BetweennessConfig, ClosenessConfig, EigenvectorConfig, HitsConfig, KatzConfig,
};
use lattix::algo::pagerank::{pagerank, PageRankConfig};
use lattix::{KnowledgeGraph, Triple};
fn social_network() -> KnowledgeGraph {
let mut kg = KnowledgeGraph::new();
kg.add_triple(Triple::new("Alice", "friends", "Bob"));
kg.add_triple(Triple::new("Alice", "friends", "Carol"));
kg.add_triple(Triple::new("Alice", "friends", "Dave"));
kg.add_triple(Triple::new("Bob", "friends", "Eve"));
kg.add_triple(Triple::new("Carol", "friends", "Eve"));
kg.add_triple(Triple::new("Dave", "friends", "Eve"));
kg
}
fn citation_network() -> KnowledgeGraph {
let mut kg = KnowledgeGraph::new();
kg.add_triple(Triple::new("Survey1", "cites", "Paper1"));
kg.add_triple(Triple::new("Survey1", "cites", "Paper2"));
kg.add_triple(Triple::new("Survey2", "cites", "Paper1"));
kg.add_triple(Triple::new("Survey2", "cites", "Paper2"));
kg.add_triple(Triple::new("Survey2", "cites", "Paper3"));
kg
}
fn chain_graph(length: usize) -> KnowledgeGraph {
let mut kg = KnowledgeGraph::new();
for i in 0..length {
let from = format!("N{i}");
let to = format!("N{}", i + 1);
kg.add_triple(Triple::new(from, "next", to));
}
kg
}
fn complete_graph(n: usize) -> KnowledgeGraph {
let mut kg = KnowledgeGraph::new();
for i in 0..n {
for j in 0..n {
if i != j {
let from = format!("N{i}");
let to = format!("N{j}");
kg.add_triple(Triple::new(from, "connected", to));
}
}
}
kg
}
#[test]
fn test_degree_social_network() {
let kg = social_network();
let degrees = degree_centrality(&kg);
let alice = degrees.get("Alice").unwrap();
assert_eq!(alice.out_degree, 3);
assert_eq!(alice.in_degree, 0);
let eve = degrees.get("Eve").unwrap();
assert_eq!(eve.in_degree, 3);
assert_eq!(eve.out_degree, 0);
}
#[test]
fn test_degree_complete_graph() {
let kg = complete_graph(4);
let degrees = degree_centrality(&kg);
for (name, deg) in degrees {
assert_eq!(deg.in_degree, 3, "{name} in-degree");
assert_eq!(deg.out_degree, 3, "{name} out-degree");
assert!((deg.in_normalized - 1.0).abs() < 1e-6);
}
}
#[test]
fn test_betweenness_chain() {
let kg = chain_graph(4);
let config = BetweennessConfig {
normalized: false,
undirected: false,
};
let scores = betweenness_centrality(&kg, config);
assert_eq!(*scores.get("N0").unwrap(), 0.0);
assert_eq!(*scores.get("N4").unwrap(), 0.0);
let n1 = *scores.get("N1").unwrap();
let n2 = *scores.get("N2").unwrap();
let n3 = *scores.get("N3").unwrap();
assert!(n2 >= n1, "N2={n2} should be >= N1={n1}");
assert!(n2 >= n3, "N2={n2} should be >= N3={n3}");
}
#[test]
fn test_betweenness_social_network() {
let kg = social_network();
let config = BetweennessConfig::default();
let scores = betweenness_centrality(&kg, config);
let bob = *scores.get("Bob").unwrap();
let alice = *scores.get("Alice").unwrap();
let _eve = *scores.get("Eve").unwrap();
assert!(bob >= alice, "Bob={bob} should be >= Alice={alice}");
}
#[test]
fn test_closeness_star() {
let mut kg = KnowledgeGraph::new();
for leaf in ["A", "B", "C", "D"] {
kg.add_triple(Triple::new("Hub", "rel", leaf));
kg.add_triple(Triple::new(leaf, "rel", "Hub"));
}
let config = ClosenessConfig::default();
let scores = closeness_centrality(&kg, config);
let hub = *scores.get("Hub").unwrap();
let leaf = *scores.get("A").unwrap();
assert!(
hub > leaf,
"Hub={hub} should be more central than leaf={leaf}"
);
}
#[test]
fn test_closeness_complete_graph() {
let kg = complete_graph(4);
let config = ClosenessConfig {
normalized: true,
undirected: false,
harmonic: true,
};
let scores = closeness_centrality(&kg, config);
let values: Vec<_> = scores.values().cloned().collect();
let first = values[0];
for v in &values {
assert!(
(v - first).abs() < 1e-6,
"All should be equal in complete graph"
);
}
}
#[test]
fn test_eigenvector_convergence() {
let kg = complete_graph(5);
let config = EigenvectorConfig::default();
let scores = eigenvector_centrality(&kg, config);
let values: Vec<_> = scores.values().cloned().collect();
let first = values[0];
for v in &values {
assert!(
(v - first).abs() < 0.01,
"Complete graph should have equal eigenvector centrality"
);
}
}
#[test]
fn test_eigenvector_normalized() {
let kg = social_network();
let scores = eigenvector_centrality(&kg, EigenvectorConfig::default());
let norm: f64 = scores.values().map(|x| x * x).sum::<f64>().sqrt();
assert!((norm - 1.0).abs() < 1e-4, "Should be L2 normalized: {norm}");
}
#[test]
fn test_katz_baseline() {
let kg = social_network();
let config = KatzConfig::default();
let scores = katz_centrality(&kg, config);
for (name, score) in &scores {
assert!(*score > 0.0, "{name} should have positive Katz score");
}
}
#[test]
fn test_katz_chain_ordering() {
let kg = chain_graph(3);
let config = KatzConfig {
normalized: false,
..Default::default()
};
let scores = katz_centrality(&kg, config);
let n0 = *scores.get("N0").unwrap();
let n1 = *scores.get("N1").unwrap();
let n2 = *scores.get("N2").unwrap();
let n3 = *scores.get("N3").unwrap();
assert!(n3 >= n2, "N3={n3} >= N2={n2}");
assert!(n2 >= n1, "N2={n2} >= N1={n1}");
assert!(n1 >= n0, "N1={n1} >= N0={n0}");
}
#[test]
fn test_pagerank_sums_to_one() {
let kg = social_network();
let scores = pagerank(&kg, PageRankConfig::default());
let total: f64 = scores.values().sum();
assert!(
(total - 1.0).abs() < 1e-4,
"PageRank should sum to 1: {total}"
);
}
#[test]
fn test_pagerank_dangling_nodes() {
let kg = social_network();
let scores = pagerank(&kg, PageRankConfig::default());
let eve = *scores.get("Eve").unwrap();
assert!(
eve > 0.0,
"Dangling node Eve should have positive PR: {eve}"
);
let alice = *scores.get("Alice").unwrap();
assert!(eve > alice, "Eve={eve} should be > Alice={alice}");
}
#[test]
fn test_hits_citation_network() {
let kg = citation_network();
let scores = hits(&kg, HitsConfig::default());
let survey1 = scores.get("Survey1").unwrap();
let survey2 = scores.get("Survey2").unwrap();
let paper1 = scores.get("Paper1").unwrap();
assert!(
survey1.hub > survey1.authority,
"Survey1 should be more hub than authority"
);
assert!(
paper1.authority > paper1.hub,
"Paper1 should be more authority than hub"
);
assert!(
survey2.hub > survey1.hub,
"Survey2={} should be better hub than Survey1={}",
survey2.hub,
survey1.hub
);
}
#[test]
fn test_hits_normalized() {
let kg = citation_network();
let scores = hits(
&kg,
HitsConfig {
normalized: true,
..Default::default()
},
);
let hub_sum: f64 = scores.values().map(|s| s.hub).sum();
let auth_sum: f64 = scores.values().map(|s| s.authority).sum();
assert!(
(hub_sum - 1.0).abs() < 1e-4,
"Hub sum should be 1: {hub_sum}"
);
assert!(
(auth_sum - 1.0).abs() < 1e-4,
"Auth sum should be 1: {auth_sum}"
);
}
#[test]
fn test_algorithms_agree_on_obvious_cases() {
let mut kg = KnowledgeGraph::new();
for leaf in ["A", "B", "C", "D", "E"] {
kg.add_triple(Triple::new("Hub", "rel", leaf));
kg.add_triple(Triple::new(leaf, "rel", "Hub"));
}
let degrees = degree_centrality(&kg);
let closeness = closeness_centrality(&kg, ClosenessConfig::default());
let eigenvector = eigenvector_centrality(&kg, EigenvectorConfig::default());
let katz = katz_centrality(&kg, KatzConfig::default());
let pr = pagerank(&kg, PageRankConfig::default());
let hub_degree = degrees.get("Hub").unwrap().total();
let leaf_degree = degrees.get("A").unwrap().total();
assert!(hub_degree > leaf_degree, "Hub should have higher degree");
let hub_close = *closeness.get("Hub").unwrap();
let leaf_close = *closeness.get("A").unwrap();
assert!(hub_close > leaf_close, "Hub should have higher closeness");
let hub_eigen = *eigenvector.get("Hub").unwrap();
let leaf_eigen = *eigenvector.get("A").unwrap();
assert!(hub_eigen > leaf_eigen, "Hub should have higher eigenvector");
let hub_katz = *katz.get("Hub").unwrap();
let leaf_katz = *katz.get("A").unwrap();
assert!(hub_katz >= leaf_katz, "Hub should have higher Katz");
let hub_pr = *pr.get("Hub").unwrap();
let leaf_pr = *pr.get("A").unwrap();
assert!(hub_pr > leaf_pr, "Hub should have higher PageRank");
}
#[test]
fn test_empty_graph() {
let kg = KnowledgeGraph::new();
assert!(degree_centrality(&kg).is_empty());
assert!(betweenness_centrality(&kg, BetweennessConfig::default()).is_empty());
assert!(closeness_centrality(&kg, ClosenessConfig::default()).is_empty());
assert!(eigenvector_centrality(&kg, EigenvectorConfig::default()).is_empty());
assert!(katz_centrality(&kg, KatzConfig::default()).is_empty());
assert!(pagerank(&kg, PageRankConfig::default()).is_empty());
assert!(hits(&kg, HitsConfig::default()).is_empty());
}
#[test]
fn test_single_node() {
let mut kg = KnowledgeGraph::new();
kg.add_triple(Triple::new("A", "rel", "A"));
let degrees = degree_centrality(&kg);
assert_eq!(degrees.len(), 1);
}