use sdivi_graph::DependencyGraph;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
#[doc(hidden)]
pub struct LeidenGraph {
pub n: usize,
pub adj: Vec<Vec<usize>>,
pub edge_weights: Vec<Vec<f64>>,
pub degree: Vec<f64>,
pub total_weight: f64,
pub self_loops: Vec<f64>,
}
impl LeidenGraph {
pub fn from_dependency_graph(dg: &DependencyGraph) -> Self {
let n = dg.node_count();
Self::from_edges(n, &dg.edges_as_pairs())
}
pub fn from_dependency_graph_weighted(
dg: &DependencyGraph,
weight_map: &BTreeMap<(usize, usize), f64>,
) -> Self {
let n = dg.node_count();
let edges: Vec<(usize, usize, f64)> = dg
.edges_as_pairs()
.into_iter()
.map(|(u, v)| {
let key = if u < v { (u, v) } else { (v, u) };
let w = weight_map.get(&key).copied().unwrap_or(1.0);
(u, v, w)
})
.collect();
Self::from_edges_weighted(n, &edges)
}
pub fn from_edges(n: usize, edges: &[(usize, usize)]) -> Self {
let edges_w: Vec<(usize, usize, f64)> = edges.iter().map(|&(u, v)| (u, v, 1.0)).collect();
Self::from_edges_weighted(n, &edges_w)
}
pub fn from_edges_weighted(n: usize, edges: &[(usize, usize, f64)]) -> Self {
let mut weight_acc: BTreeMap<(usize, usize), f64> = BTreeMap::new();
let mut self_loop_acc: Vec<f64> = vec![0.0; n];
for &(u, v, w) in edges {
if u >= n || v >= n {
continue;
}
if u == v {
self_loop_acc[u] += w;
} else {
let key = if u < v { (u, v) } else { (v, u) };
*weight_acc.entry(key).or_insert(0.0) += w;
}
}
let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
let mut edge_weights: Vec<Vec<f64>> = vec![vec![]; n];
for (&(u, v), &w) in &weight_acc {
adj[u].push(v);
adj[v].push(u);
edge_weights[u].push(w);
edge_weights[v].push(w);
}
for i in 0..n {
let mut pairs: Vec<(usize, f64)> = adj[i]
.iter()
.copied()
.zip(edge_weights[i].iter().copied())
.collect();
pairs.sort_unstable_by_key(|&(nbr, _)| nbr);
adj[i] = pairs.iter().map(|&(nbr, _)| nbr).collect();
edge_weights[i] = pairs.iter().map(|&(_, w)| w).collect();
}
let degree: Vec<f64> = edge_weights
.iter()
.zip(self_loop_acc.iter())
.map(|(ws, &sl)| ws.iter().sum::<f64>() + 2.0 * sl)
.collect();
let cross_edge_total: f64 = weight_acc.values().sum();
let self_loop_total: f64 = self_loop_acc.iter().sum();
let total_weight = cross_edge_total + self_loop_total;
LeidenGraph {
n,
adj,
edge_weights,
degree,
total_weight,
self_loops: self_loop_acc,
}
}
pub fn edge_weight(&self, u: usize, v: usize) -> f64 {
if u == v {
return self.self_loops[u];
}
match self.adj[u].binary_search(&v) {
Ok(idx) => self.edge_weights[u][idx],
Err(_) => 0.0,
}
}
pub fn induced_subgraph(&self, nodes: &[usize]) -> (LeidenGraph, Vec<usize>) {
let mut members: Vec<usize> = nodes.to_vec();
members.sort_unstable();
members.dedup();
let local_n = members.len();
let global_to_local: BTreeMap<usize, usize> = members
.iter()
.enumerate()
.map(|(local, &global)| (global, local))
.collect();
let mut edges: Vec<(usize, usize, f64)> = Vec::new();
for &global_id in &members {
let local_id = global_to_local[&global_id];
for (idx, &nbr) in self.adj[global_id].iter().enumerate() {
if nbr <= global_id {
continue;
}
if let Some(&local_nbr) = global_to_local.get(&nbr) {
edges.push((local_id, local_nbr, self.edge_weights[global_id][idx]));
}
}
let sl = self.self_loops[global_id];
if sl > 0.0 {
edges.push((local_id, local_id, sl));
}
}
let sub = LeidenGraph::from_edges_weighted(local_n, &edges);
(sub, members)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_node_self_loop() {
let g = LeidenGraph::from_edges_weighted(1, &[(0, 0, 2.0)]);
assert_eq!(g.self_loops[0], 2.0);
assert_eq!(
g.degree[0], 4.0,
"self-loop weight 2.0 contributes 2×2.0=4.0 to degree"
);
assert_eq!(g.total_weight, 2.0);
assert!(g.adj[0].is_empty(), "self-loop must not appear in adj");
}
#[test]
fn two_node_cross_edge_plus_self_loops() {
let g = LeidenGraph::from_edges_weighted(2, &[(0, 0, 0.5), (1, 1, 0.5), (0, 1, 1.0)]);
assert_eq!(g.degree[0], 2.0, "1.0 cross + 2×0.5 self = 2.0");
assert_eq!(g.degree[1], 2.0);
assert_eq!(g.total_weight, 2.0, "1.0 cross + 0.5 + 0.5 self = 2.0");
assert_eq!(g.self_loops[0], 0.5);
assert_eq!(g.self_loops[1], 0.5);
}
#[test]
fn self_loop_accumulation() {
let g = LeidenGraph::from_edges_weighted(1, &[(0, 0, 1.0), (0, 0, 2.0)]);
assert_eq!(g.self_loops[0], 3.0);
assert_eq!(g.degree[0], 6.0);
assert_eq!(g.total_weight, 3.0);
}
#[test]
fn edge_weight_self_loop() {
let g = LeidenGraph::from_edges_weighted(2, &[(0, 0, 1.5), (0, 1, 2.0)]);
assert_eq!(
g.edge_weight(0, 0),
1.5,
"edge_weight(u,u) returns self_loops[u]"
);
assert_eq!(g.edge_weight(0, 1), 2.0);
assert_eq!(g.edge_weight(1, 0), 2.0);
assert_eq!(g.edge_weight(1, 1), 0.0, "no self-loop on node 1");
}
#[test]
fn no_self_loops_unchanged() {
let g = LeidenGraph::from_edges_weighted(3, &[(0, 1, 1.0), (1, 2, 1.0)]);
assert_eq!(g.self_loops, vec![0.0, 0.0, 0.0]);
assert_eq!(g.degree, vec![1.0, 2.0, 1.0]);
assert_eq!(g.total_weight, 2.0);
}
}