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)? {
*new_scores.get_mut(&neighbor).unwrap() += damping * share;
}
}
}
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]);
*delta.get_mut(&v).unwrap() += contribution;
}
if w != s {
*centrality.get_mut(&w).unwrap() += delta[&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());
}
}