use super::traits::CommunityDetection;
use crate::error::{Error, Result};
use petgraph::graph::UnGraph;
use petgraph::visit::EdgeRef;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct Louvain {
resolution: f64,
max_iter: usize,
max_levels: usize,
min_modularity_gain: f64,
}
impl Louvain {
pub fn new() -> Self {
Self {
resolution: 1.0,
max_iter: 100,
max_levels: 10,
min_modularity_gain: 1e-7,
}
}
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_max_levels(mut self, levels: usize) -> Self {
self.max_levels = levels;
self
}
fn modularity_weighted(
&self,
n: usize,
edges: &[(usize, usize, f64)],
self_loops: &[f64],
communities: &[usize],
) -> f64 {
let m: f64 = edges.iter().map(|(_, _, w)| w).sum::<f64>() + self_loops.iter().sum::<f64>();
if m == 0.0 {
return 0.0;
}
let mut degrees = vec![0.0; n];
for &(i, j, w) in edges {
degrees[i] += w;
degrees[j] += w;
}
for (i, &sl) in self_loops.iter().enumerate() {
degrees[i] += 2.0 * sl; }
let mut q = 0.0;
for &(i, j, w) in edges {
if communities[i] == communities[j] {
let expected = degrees[i] * degrees[j] / (2.0 * m);
q += w - self.resolution * expected;
}
}
for (i, &sl) in self_loops.iter().enumerate() {
if sl > 0.0 {
let expected = degrees[i] * degrees[i] / (2.0 * m);
q += sl - self.resolution * expected / 2.0;
}
}
q / m
}
fn local_moving(
&self,
n: usize,
edges: &[(usize, usize, f64)],
self_loops: &[f64],
) -> (Vec<usize>, bool) {
let mut adj: Vec<HashMap<usize, f64>> = vec![HashMap::new(); n];
for &(i, j, w) in edges {
*adj[i].entry(j).or_insert(0.0) += w;
*adj[j].entry(i).or_insert(0.0) += w;
}
let m: f64 = edges.iter().map(|(_, _, w)| w).sum::<f64>() + self_loops.iter().sum::<f64>();
if m == 0.0 {
return ((0..n).collect(), false);
}
let mut degrees = vec![0.0; n];
for &(i, j, w) in edges {
degrees[i] += w;
degrees[j] += w;
}
for (i, &sl) in self_loops.iter().enumerate() {
degrees[i] += 2.0 * sl;
}
let mut communities: Vec<usize> = (0..n).collect();
let mut community_degrees = degrees.clone();
let mut any_improved = false;
for _iter in 0..self.max_iter {
let mut improved = false;
for node in 0..n {
let current_community = communities[node];
let ki = degrees[node];
community_degrees[current_community] -= ki;
let mut community_weights: HashMap<usize, f64> = HashMap::new();
for (&neighbor, &w) in &adj[node] {
let nc = communities[neighbor];
*community_weights.entry(nc).or_insert(0.0) += w;
}
let mut best_community = current_community;
let mut best_gain = 0.0;
for (&target_comm, &ki_in) in &community_weights {
let sigma_tot = community_degrees[target_comm];
let gain = ki_in / m - self.resolution * sigma_tot * ki / (2.0 * m * m);
if gain > best_gain {
best_gain = gain;
best_community = target_comm;
}
}
if best_community != current_community {
communities[node] = best_community;
community_degrees[best_community] += ki;
improved = true;
any_improved = true;
} else {
community_degrees[current_community] += ki;
}
}
if !improved {
break;
}
}
(communities, any_improved)
}
fn aggregate(
&self,
_n: usize, edges: &[(usize, usize, f64)],
self_loops: &[f64],
communities: &[usize],
) -> (Vec<(usize, usize, f64)>, Vec<f64>, Vec<Vec<usize>>) {
let mut unique_comms: Vec<usize> = communities.to_vec();
unique_comms.sort_unstable();
unique_comms.dedup();
let n_new = unique_comms.len();
let comm_to_new: HashMap<usize, usize> = unique_comms
.iter()
.enumerate()
.map(|(i, &c)| (c, i))
.collect();
let mut new_to_old: Vec<Vec<usize>> = vec![Vec::new(); n_new];
for (node, &comm) in communities.iter().enumerate() {
new_to_old[comm_to_new[&comm]].push(node);
}
let mut new_edge_weights: HashMap<(usize, usize), f64> = HashMap::new();
for &(i, j, w) in edges {
let ci = comm_to_new[&communities[i]];
let cj = comm_to_new[&communities[j]];
if ci == cj {
continue;
}
let key = if ci < cj { (ci, cj) } else { (cj, ci) };
*new_edge_weights.entry(key).or_insert(0.0) += w;
}
let new_edges: Vec<(usize, usize, f64)> = new_edge_weights
.into_iter()
.map(|((i, j), w)| (i, j, w))
.collect();
let mut new_self_loops = vec![0.0; n_new];
for (i, &sl) in self_loops.iter().enumerate() {
let ci = comm_to_new[&communities[i]];
new_self_loops[ci] += sl;
}
for &(i, j, w) in edges {
let ci = comm_to_new[&communities[i]];
let cj = comm_to_new[&communities[j]];
if ci == cj {
new_self_loops[ci] += w;
}
}
(new_edges, new_self_loops, new_to_old)
}
fn expand_partition(partition: &[usize], node_mapping: &[Vec<usize>]) -> Vec<usize> {
let max_node = node_mapping.iter().flatten().copied().max().unwrap_or(0);
let mut result = vec![0; max_node + 1];
for (agg_node, original_nodes) in node_mapping.iter().enumerate() {
let comm = partition[agg_node];
for &orig in original_nodes {
result[orig] = comm;
}
}
result
}
}
impl Default for Louvain {
fn default() -> Self {
Self::new()
}
}
impl CommunityDetection for Louvain {
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 mut edges: Vec<(usize, usize, f64)> = Vec::new();
for edge in graph.edge_references() {
let i = edge.source().index();
let j = edge.target().index();
if i < j {
edges.push((i, j, 1.0));
}
}
let self_loops = vec![0.0; n];
let mut current_n = n;
let mut current_edges = edges;
let mut current_self_loops = self_loops;
let mut mapping_stack: Vec<Vec<Vec<usize>>> = Vec::new();
let _initial_mapping: Vec<Vec<usize>> = (0..n).map(|i| vec![i]).collect();
let mut prev_modularity = f64::NEG_INFINITY;
for _level in 0..self.max_levels {
let (partition, improved) =
self.local_moving(current_n, ¤t_edges, ¤t_self_loops);
if !improved {
break;
}
let mod_now = self.modularity_weighted(
current_n,
¤t_edges,
¤t_self_loops,
&partition,
);
if mod_now - prev_modularity < self.min_modularity_gain {
break;
}
prev_modularity = mod_now;
let (new_edges, new_self_loops, node_mapping) =
self.aggregate(current_n, ¤t_edges, ¤t_self_loops, &partition);
if node_mapping.len() == current_n {
break;
}
mapping_stack.push(node_mapping.clone());
current_n = node_mapping.len();
current_edges = new_edges;
current_self_loops = new_self_loops;
}
let mut result: Vec<usize> = (0..current_n).collect();
while let Some(mapping) = mapping_stack.pop() {
result = Self::expand_partition(&result, &mapping);
}
if result.len() < n {
result.resize(n, 0);
}
result.truncate(n);
let mut unique: Vec<usize> = result.to_vec();
unique.sort_unstable();
unique.dedup();
Ok(result
.iter()
.map(|&c| unique.iter().position(|&u| u == c).unwrap_or(0))
.collect())
}
fn resolution(&self) -> f64 {
self.resolution
}
}
#[cfg(test)]
mod tests {
use super::*;
use petgraph::graph::UnGraph;
#[test]
fn test_louvain_triangle() {
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 louvain = Louvain::new();
let communities = louvain.detect(&graph).unwrap();
assert_eq!(communities.len(), 3);
assert_eq!(communities[0], communities[1]);
assert_eq!(communities[1], communities[2]);
}
#[test]
fn test_louvain_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 louvain = Louvain::new();
let communities = louvain.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_louvain_empty_graph() {
let graph = UnGraph::<(), ()>::new_undirected();
let louvain = Louvain::new();
let result = louvain.detect(&graph);
assert!(result.is_err());
}
#[test]
fn test_louvain_single_node() {
let mut graph = UnGraph::<(), ()>::new_undirected();
let _ = graph.add_node(());
let louvain = Louvain::new();
let communities = louvain.detect(&graph).unwrap();
assert_eq!(communities.len(), 1);
assert_eq!(communities[0], 0);
}
#[test]
fn test_louvain_disconnected() {
let mut graph = UnGraph::<(), ()>::new_undirected();
let _ = graph.add_node(());
let _ = graph.add_node(());
let louvain = Louvain::new();
let communities = louvain.detect(&graph).unwrap();
assert_eq!(communities.len(), 2);
assert_ne!(communities[0], communities[1]);
}
}