use super::traits::CommunityDetection;
use crate::error::{Error, Result};
use petgraph::graph::UnGraph;
use petgraph::visit::EdgeRef;
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone)]
pub struct Leiden {
resolution: f64,
max_iter: usize,
min_gain: f64,
seed: u64,
}
impl Leiden {
pub fn new() -> Self {
Self {
resolution: 1.0,
max_iter: 100,
min_gain: 1e-7,
seed: 42,
}
}
pub fn with_resolution(mut self, resolution: f64) -> Self {
self.resolution = resolution;
self
}
pub fn with_max_iter(mut self, max_iter: usize) -> Self {
self.max_iter = max_iter;
self
}
pub fn with_min_gain(mut self, min_gain: f64) -> Self {
self.min_gain = min_gain;
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
#[deprecated(since = "0.2.0", note = "Use with_max_iter instead")]
pub fn with_refinement(self, n: usize) -> Self {
self.with_max_iter(n)
}
}
impl Default for Leiden {
fn default() -> Self {
Self::new()
}
}
struct WeightedGraph {
n: usize,
adj: Vec<Vec<(usize, f64)>>,
degrees: Vec<f64>,
total_weight: f64,
}
impl WeightedGraph {
fn from_edges(n: usize, edges: &[(usize, usize, f64)]) -> Self {
let mut adj = vec![Vec::new(); n];
let mut degrees = vec![0.0; n];
let mut total_weight = 0.0;
for &(i, j, w) in edges {
adj[i].push((j, w));
adj[j].push((i, w));
degrees[i] += w;
degrees[j] += w;
total_weight += 2.0 * w; }
Self {
n,
adj,
degrees,
total_weight,
}
}
fn modularity_gain(
&self,
node: usize,
target_comm: usize,
communities: &[usize],
comm_total_weight: &[f64],
resolution: f64,
) -> f64 {
if self.total_weight == 0.0 {
return 0.0;
}
let m = self.total_weight / 2.0;
let ki = self.degrees[node];
let ki_in: f64 = self.adj[node]
.iter()
.filter(|(neighbor, _)| communities[*neighbor] == target_comm)
.map(|(_, w)| w)
.sum();
let sigma_tot = comm_total_weight[target_comm];
ki_in / m - resolution * sigma_tot * ki / (2.0 * m * m)
}
}
struct CommunityState {
assignment: Vec<usize>,
comm_total_weight: Vec<f64>,
n_communities: usize,
}
impl CommunityState {
fn new_singletons(n: usize, degrees: &[f64]) -> Self {
Self {
assignment: (0..n).collect(),
comm_total_weight: degrees.to_vec(),
n_communities: n,
}
}
fn move_node(&mut self, node: usize, from: usize, to: usize, degree: f64) {
self.assignment[node] = to;
self.comm_total_weight[from] -= degree;
self.comm_total_weight[to] += degree;
}
}
impl CommunityDetection for Leiden {
fn detect<N, E>(&self, graph: &UnGraph<N, E>) -> Result<Vec<usize>> {
let n = graph.node_count();
if n == 0 {
return Err(Error::EmptyInput);
}
if graph.edge_count() == 0 {
return Ok((0..n).collect());
}
let edges: Vec<(usize, usize, f64)> = graph
.edge_references()
.filter_map(|e| {
let i = e.source().index();
let j = e.target().index();
if i < j {
Some((i, j, 1.0))
} else {
None
}
})
.collect();
let wg = WeightedGraph::from_edges(n, &edges);
let mut state = CommunityState::new_singletons(n, &wg.degrees);
for _level in 0..self.max_iter {
let improved = self.local_moving_phase(&wg, &mut state);
if !improved {
break;
}
self.refinement_phase(&wg, &mut state);
}
Ok(renumber_communities(&state.assignment))
}
fn resolution(&self) -> f64 {
self.resolution
}
}
impl Leiden {
fn local_moving_phase(&self, wg: &WeightedGraph, state: &mut CommunityState) -> bool {
let mut improved = false;
let mut queue: VecDeque<usize> = (0..wg.n).collect();
let mut in_queue = vec![true; wg.n];
while let Some(node) = queue.pop_front() {
in_queue[node] = false;
let current_comm = state.assignment[node];
let mut neighbor_comms: HashSet<usize> = HashSet::new();
for &(neighbor, _) in &wg.adj[node] {
let _ = neighbor_comms.insert(state.assignment[neighbor]);
}
let _ = neighbor_comms.insert(current_comm);
let mut best_comm = current_comm;
let mut best_gain = 0.0;
state.comm_total_weight[current_comm] -= wg.degrees[node];
for &target_comm in &neighbor_comms {
let gain = wg.modularity_gain(
node,
target_comm,
&state.assignment,
&state.comm_total_weight,
self.resolution,
);
if gain > best_gain + 1e-10 {
best_gain = gain;
best_comm = target_comm;
}
}
state.comm_total_weight[current_comm] += wg.degrees[node];
if best_comm != current_comm {
state.move_node(node, current_comm, best_comm, wg.degrees[node]);
improved = true;
for &(neighbor, _) in &wg.adj[node] {
if !in_queue[neighbor] {
queue.push_back(neighbor);
in_queue[neighbor] = true;
}
}
}
}
improved
}
fn refinement_phase(&self, wg: &WeightedGraph, state: &mut CommunityState) {
let phase1_communities = state.assignment.clone();
let mut unique_comms: Vec<usize> = phase1_communities.to_vec();
unique_comms.sort_unstable();
unique_comms.dedup();
for &comm in &unique_comms {
let nodes_in_comm: Vec<usize> = (0..wg.n)
.filter(|&i| phase1_communities[i] == comm)
.collect();
if nodes_in_comm.len() <= 1 {
continue;
}
self.refine_community(wg, state, &nodes_in_comm);
}
}
fn refine_community(&self, wg: &WeightedGraph, state: &mut CommunityState, nodes: &[usize]) {
let node_set: HashSet<usize> = nodes.iter().copied().collect();
let components = find_components_in_subset(wg, &node_set);
if components.len() <= 1 {
return;
}
let base_comm = state.n_communities;
state.n_communities += components.len() - 1;
while state.comm_total_weight.len() < state.n_communities {
state.comm_total_weight.push(0.0);
}
for (idx, component) in components.iter().enumerate().skip(1) {
let new_comm = base_comm + idx - 1;
for &node in component {
let old_comm = state.assignment[node];
state.move_node(node, old_comm, new_comm, wg.degrees[node]);
}
}
}
}
fn find_components_in_subset(wg: &WeightedGraph, node_set: &HashSet<usize>) -> Vec<Vec<usize>> {
let mut visited = HashSet::new();
let mut components = Vec::new();
for &start in node_set {
if visited.contains(&start) {
continue;
}
let mut component = Vec::new();
let mut queue = VecDeque::new();
queue.push_back(start);
while let Some(node) = queue.pop_front() {
if !visited.insert(node) {
continue;
}
component.push(node);
for &(neighbor, _) in &wg.adj[node] {
if node_set.contains(&neighbor) && !visited.contains(&neighbor) {
queue.push_back(neighbor);
}
}
}
if !component.is_empty() {
components.push(component);
}
}
components
}
fn renumber_communities(assignment: &[usize]) -> Vec<usize> {
let mut unique: Vec<usize> = assignment.to_vec();
unique.sort_unstable();
unique.dedup();
let mapping: HashMap<usize, usize> = unique
.into_iter()
.enumerate()
.map(|(new, old)| (old, new))
.collect();
assignment
.iter()
.map(|&c| mapping.get(&c).copied().unwrap_or(0))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_leiden_basic() {
let mut graph = UnGraph::<(), ()>::new_undirected();
let n0 = graph.add_node(());
let n1 = graph.add_node(());
let n2 = graph.add_node(());
let _ = graph.add_edge(n0, n1, ());
let _ = graph.add_edge(n1, n2, ());
let _ = graph.add_edge(n0, n2, ());
let leiden = Leiden::new();
let communities = leiden.detect(&graph).unwrap();
assert_eq!(communities[0], communities[1]);
assert_eq!(communities[1], communities[2]);
}
#[test]
fn test_leiden_two_cliques() {
let mut graph = UnGraph::<(), ()>::new_undirected();
let a0 = graph.add_node(());
let a1 = graph.add_node(());
let a2 = graph.add_node(());
let _ = graph.add_edge(a0, a1, ());
let _ = graph.add_edge(a1, a2, ());
let _ = graph.add_edge(a0, a2, ());
let b0 = graph.add_node(());
let b1 = graph.add_node(());
let b2 = graph.add_node(());
let _ = graph.add_edge(b0, b1, ());
let _ = graph.add_edge(b1, b2, ());
let _ = graph.add_edge(b0, b2, ());
let _ = graph.add_edge(a2, b0, ());
let leiden = Leiden::new();
let communities = leiden.detect(&graph).unwrap();
assert_eq!(communities.len(), 6);
assert_eq!(communities[0], communities[1]);
assert_eq!(communities[1], communities[2]);
assert_eq!(communities[3], communities[4]);
assert_eq!(communities[4], communities[5]);
assert_ne!(communities[0], communities[3]);
}
#[test]
fn test_leiden_disconnected_within_community() {
let mut graph = UnGraph::<(), ()>::new_undirected();
let a = graph.add_node(());
let b = graph.add_node(());
let c = graph.add_node(());
let d = graph.add_node(());
let e = graph.add_node(());
let _ = graph.add_edge(a, b, ());
let _ = graph.add_edge(b, c, ());
let _ = graph.add_edge(d, e, ());
let leiden = Leiden::new();
let communities = leiden.detect(&graph).unwrap();
assert_eq!(communities[0], communities[1]);
assert_eq!(communities[1], communities[2]);
assert_eq!(communities[3], communities[4]);
assert_ne!(communities[0], communities[3]);
}
#[test]
fn test_leiden_empty_graph() {
let graph = UnGraph::<(), ()>::new_undirected();
let leiden = Leiden::new();
let result = leiden.detect(&graph);
assert!(result.is_err());
}
#[test]
fn test_leiden_single_node() {
let mut graph = UnGraph::<(), ()>::new_undirected();
let _ = graph.add_node(());
let leiden = Leiden::new();
let communities = leiden.detect(&graph).unwrap();
assert_eq!(communities.len(), 1);
assert_eq!(communities[0], 0);
}
#[test]
fn test_leiden_resolution_parameter() {
let mut graph = UnGraph::<(), ()>::new_undirected();
for _ in 0..10 {
let _ = graph.add_node(());
}
for i in 0..9 {
let n1 = petgraph::graph::NodeIndex::new(i);
let n2 = petgraph::graph::NodeIndex::new(i + 1);
let _ = graph.add_edge(n1, n2, ());
}
let low_res = Leiden::new().with_resolution(0.5);
let high_res = Leiden::new().with_resolution(2.0);
let comms_low = low_res.detect(&graph).unwrap();
let comms_high = high_res.detect(&graph).unwrap();
assert_eq!(comms_low.len(), 10);
assert_eq!(comms_high.len(), 10);
let unique_low: HashSet<_> = comms_low.iter().collect();
let unique_high: HashSet<_> = comms_high.iter().collect();
assert!(!unique_low.is_empty());
assert!(!unique_high.is_empty());
}
#[test]
fn test_leiden_connectivity_guarantee() {
let mut graph = UnGraph::<(), ()>::new_undirected();
for _ in 0..20 {
let _ = graph.add_node(());
}
for i in 0..15 {
let n1 = petgraph::graph::NodeIndex::new(i);
let n2 = petgraph::graph::NodeIndex::new(i + 1);
let _ = graph.add_edge(n1, n2, ());
}
let _ = graph.add_edge(
petgraph::graph::NodeIndex::new(0),
petgraph::graph::NodeIndex::new(5),
(),
);
let _ = graph.add_edge(
petgraph::graph::NodeIndex::new(10),
petgraph::graph::NodeIndex::new(15),
(),
);
let leiden = Leiden::new();
let communities = leiden.detect(&graph).unwrap();
let mut by_community: HashMap<usize, Vec<usize>> = HashMap::new();
for (node, &comm) in communities.iter().enumerate() {
by_community.entry(comm).or_default().push(node);
}
for (_comm, nodes) in by_community {
if nodes.len() <= 1 {
continue;
}
let node_set: HashSet<usize> = nodes.iter().copied().collect();
let mut adj: HashMap<usize, Vec<usize>> = HashMap::new();
for edge in graph.edge_references() {
let i = edge.source().index();
let j = edge.target().index();
if node_set.contains(&i) && node_set.contains(&j) {
adj.entry(i).or_default().push(j);
adj.entry(j).or_default().push(i);
}
}
let start = nodes[0];
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
while let Some(node) = queue.pop_front() {
if !visited.insert(node) {
continue;
}
if let Some(neighbors) = adj.get(&node) {
for &n in neighbors {
if !visited.contains(&n) {
queue.push_back(n);
}
}
}
}
assert_eq!(
visited.len(),
nodes.len(),
"Community is not fully connected!"
);
}
}
}