use std::collections::VecDeque;
use ahash::AHashMap;
use crate::backend::GraphBackend;
use crate::errors::SqliteGraphError;
pub fn pagerank(
graph: &dyn GraphBackend,
damping: f64,
iterations: usize,
) -> Result<Vec<(i64, f64)>, SqliteGraphError> {
let all_ids = graph.all_entity_ids()?;
let n = all_ids.len();
if n == 0 {
return Ok(Vec::new());
}
let mut scores: AHashMap<i64, f64> = all_ids.iter().map(|&id| (id, 1.0 / n as f64)).collect();
let mut outgoing_counts: AHashMap<i64, usize> = AHashMap::new();
for &id in &all_ids {
let count = graph.fetch_outgoing(id)?.len();
outgoing_counts.insert(id, count);
}
for _ in 0..iterations {
let mut new_scores: AHashMap<i64, f64> = AHashMap::new();
let base_score = (1.0 - damping) / n as f64;
for &id in &all_ids {
new_scores.insert(id, base_score);
}
let mut dangling_score = 0.0;
for &id in &all_ids {
let score = scores[&id];
let out_count = outgoing_counts[&id];
if out_count == 0 {
dangling_score += score;
} else {
let share = score / out_count as f64;
for &neighbor in &graph.fetch_outgoing(id)? {
if let Some(score_entry) = new_scores.get_mut(&neighbor) {
*score_entry += damping * share;
} else {
return Err(SqliteGraphError::graph_corruption(format!(
"Edge from node {} to node {} points to non-existent entity",
id, neighbor
)));
}
}
}
}
let dangling_share = damping * dangling_score / n as f64;
for (_, score) in new_scores.iter_mut() {
*score += dangling_share;
}
scores = new_scores;
}
let mut result: Vec<(i64, f64)> = scores.into_iter().collect();
result.sort_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
Ok(result)
}
pub fn betweenness_centrality(
graph: &dyn GraphBackend,
) -> Result<Vec<(i64, f64)>, SqliteGraphError> {
let all_ids = graph.all_entity_ids()?;
let n = all_ids.len();
if n == 0 {
return Ok(Vec::new());
}
let mut centrality: AHashMap<i64, f64> = all_ids.iter().map(|&id| (id, 0.0)).collect();
for &s in &all_ids {
let mut dist: AHashMap<i64, i64> = AHashMap::new();
let mut sigma: AHashMap<i64, f64> = AHashMap::new(); let mut predecessors: AHashMap<i64, Vec<i64>> = AHashMap::new();
dist.insert(s, 0);
sigma.insert(s, 1.0);
let mut queue = VecDeque::new();
queue.push_back(s);
while let Some(v) = queue.pop_front() {
for &w in &graph.fetch_outgoing(v)? {
if !dist.contains_key(&w) {
dist.insert(w, dist[&v] + 1);
queue.push_back(w);
}
if dist.get(&w) == Some(&(dist[&v] + 1)) {
*sigma.entry(w).or_insert(0.0) += sigma[&v];
predecessors.entry(w).or_default().push(v);
}
}
}
let mut delta: AHashMap<i64, f64> = all_ids.iter().map(|&id| (id, 0.0)).collect();
let mut nodes: Vec<i64> = dist.keys().copied().collect();
nodes.sort_by_key(|&id| std::cmp::Reverse(dist[&id]));
for w in nodes {
if w == s {
continue;
}
for &v in predecessors.get(&w).unwrap_or(&vec![]) {
let contribution = (sigma[&v] / sigma[&w]) * (1.0 + delta[&w]);
if let Some(delta_entry) = delta.get_mut(&v) {
*delta_entry += contribution;
} else {
return Err(SqliteGraphError::graph_corruption(format!(
"Betweenness centrality: predecessor node {} not found in delta map",
v
)));
}
}
if w != s {
if let Some(centrality_entry) = centrality.get_mut(&w) {
*centrality_entry += delta[&w];
} else {
return Err(SqliteGraphError::graph_corruption(format!(
"Betweenness centrality: node {} not found in centrality map",
w
)));
}
}
}
}
let mut result: Vec<(i64, f64)> = centrality.into_iter().collect();
result.sort_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
Ok(result)
}
#[cfg(all(test, feature = "native-v3"))]
mod tests {
use super::*;
use crate::backend::native::v3::V3Backend;
use crate::backend::{EdgeSpec, NodeSpec};
use tempfile::TempDir;
fn create_test_backend() -> (V3Backend, TempDir) {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.graph");
let backend = V3Backend::create(&db_path).unwrap();
(backend, temp_dir)
}
fn build_chain(backend: &V3Backend) -> Vec<i64> {
let mut nodes = Vec::new();
for _ in 0..4 {
let id = backend
.insert_node(NodeSpec {
kind: "Node".to_string(),
name: "node".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
nodes.push(id);
}
for i in 0..nodes.len() - 1 {
backend
.insert_edge(EdgeSpec {
from: nodes[i],
to: nodes[i + 1],
edge_type: "links".to_string(),
data: serde_json::json!({}),
})
.unwrap();
}
nodes
}
#[test]
fn test_backend_pagerank_chain() {
let (backend, _temp) = create_test_backend();
let nodes = build_chain(&backend);
let scores = pagerank(&backend, 0.85, 20).unwrap();
assert_eq!(scores.len(), 4);
let total: f64 = scores.iter().map(|(_, s)| s).sum();
assert!((total - 1.0).abs() < 0.01);
let scores_map: std::collections::HashMap<i64, f64> = scores.into_iter().collect();
assert!(scores_map[&nodes[3]] > scores_map[&nodes[0]]);
}
#[test]
fn test_backend_betweenness_chain() {
let (backend, _temp) = create_test_backend();
build_chain(&backend);
let centrality = betweenness_centrality(&backend).unwrap();
assert_eq!(centrality.len(), 4);
}
#[test]
fn test_backend_pagerank_empty() {
let (backend, _temp) = create_test_backend();
let scores = pagerank(&backend, 0.85, 20).unwrap();
assert!(scores.is_empty());
}
#[test]
fn test_backend_betweenness_empty() {
let (backend, _temp) = create_test_backend();
let centrality = betweenness_centrality(&backend).unwrap();
assert!(centrality.is_empty());
}
#[test]
fn test_pagerank_handles_inconsistent_graph() {
let (backend, _temp) = create_test_backend();
let node1 = backend
.insert_node(NodeSpec {
kind: "Node".to_string(),
name: "node1".to_string(),
file_path: None,
data: serde_json::json!({}),
})
.unwrap();
let fake_neighbor_id = 99999; backend
.insert_edge(EdgeSpec {
from: node1,
to: fake_neighbor_id,
edge_type: "links".to_string(),
data: serde_json::json!({}),
})
.unwrap();
let result = pagerank(&backend, 0.85, 5);
assert!(
result.is_err(),
"Should detect graph inconsistency and return error"
);
}
}