use super::csr::CsrGraph;
pub const LABEL_PROPAGATION_ITERS: usize = 10;
pub fn connected_components(g: &CsrGraph) -> Vec<u32> {
let n = g.node_count();
let mut parent: Vec<u32> = (0..n as u32).collect();
let mut rank: Vec<u32> = vec![0; n];
fn find(parent: &mut [u32], x: u32) -> u32 {
let mut root = x;
while parent[root as usize] != root {
root = parent[root as usize];
}
let mut cur = x;
while parent[cur as usize] != root {
let next = parent[cur as usize];
parent[cur as usize] = root;
cur = next;
}
root
}
for i in g.node_indices() {
for (j, _) in g.out_neighbors(i) {
let ri = find(&mut parent, i);
let rj = find(&mut parent, j);
if ri == rj {
continue;
}
let (a, b) = if rank[ri as usize] < rank[rj as usize] {
(rj, ri)
} else {
(ri, rj)
};
parent[b as usize] = a;
if rank[ri as usize] == rank[rj as usize] {
rank[a as usize] += 1;
}
}
}
for i in 0..n as u32 {
let root = {
let mut root = i;
while parent[root as usize] != root {
root = parent[root as usize];
}
root
};
let mut cur = i;
while parent[cur as usize] != root {
let next = parent[cur as usize];
parent[cur as usize] = root;
cur = next;
}
}
parent
}
pub fn label_propagation(g: &CsrGraph, max_iters: usize) -> Vec<u32> {
let n = g.node_count();
let mut label: Vec<u32> = (0..n as u32).collect();
for _ in 0..max_iters {
let mut changed = false;
for i in g.node_indices() {
let mut votes: Vec<(u32, f64)> = g
.out_neighbors(i)
.map(|(t, w)| (label[t as usize], w))
.chain(g.in_neighbors(i).map(|(s, w)| (label[s as usize], w)))
.collect();
if votes.is_empty() {
continue;
}
votes.sort_unstable_by_key(|&(l, _)| l);
let mut best_label = votes[0].0;
let mut best_weight = 0.0_f64;
let mut cur_label = votes[0].0;
let mut cur_weight = 0.0_f64;
for &(l, w) in &votes {
if l == cur_label {
cur_weight += w;
} else {
cur_label = l;
cur_weight = w;
}
if cur_weight > best_weight {
best_label = cur_label;
best_weight = cur_weight;
}
}
if best_label != label[i as usize] {
label[i as usize] = best_label;
changed = true;
}
}
if !changed {
break;
}
}
label
}
pub fn label_propagation_default(g: &CsrGraph) -> Vec<u32> {
label_propagation(g, LABEL_PROPAGATION_ITERS)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::algo::csr::CsrGraph;
use crate::graph::types::GraphEdge;
fn edge(id: &str, s: &str, t: &str, w: f64) -> GraphEdge {
GraphEdge {
id: id.into(),
source: s.into(),
target: t.into(),
relation: "related".into(),
weight: w,
ts: "2026-01-01T00:00:00Z".into(),
}
}
fn graph(nodes: &[&str], edges: &[GraphEdge]) -> CsrGraph {
let node_ids: Vec<String> = nodes.iter().map(|s| s.to_string()).collect();
CsrGraph::from_edges(&node_ids, edges)
}
#[test]
fn two_disjoint_components() {
let g = graph(
&["A", "B", "C", "D", "E"],
&[
edge("e1", "A", "B", 1.0),
edge("e2", "B", "C", 1.0),
edge("e3", "D", "E", 1.0),
],
);
let comp = connected_components(&g);
let a = g.node_index("A").unwrap() as usize;
let b = g.node_index("B").unwrap() as usize;
let c = g.node_index("C").unwrap() as usize;
let d = g.node_index("D").unwrap() as usize;
let e = g.node_index("E").unwrap() as usize;
assert_eq!(comp[a], comp[b]);
assert_eq!(comp[b], comp[c]);
assert_eq!(comp[d], comp[e]);
assert_ne!(comp[a], comp[d]);
}
#[test]
fn single_component_connected() {
let g = graph(
&["A", "B", "C"],
&[edge("e1", "A", "B", 1.0), edge("e2", "B", "C", 1.0)],
);
let comp = connected_components(&g);
let labels: std::collections::HashSet<u32> = comp.iter().copied().collect();
assert_eq!(labels.len(), 1, "fully connected graph has one component");
}
#[test]
fn direction_ignored_for_components() {
let g = graph(
&["A", "B", "C"],
&[edge("e1", "A", "B", 1.0), edge("e2", "C", "B", 1.0)],
);
let comp = connected_components(&g);
let labels: std::collections::HashSet<u32> = comp.iter().copied().collect();
assert_eq!(labels.len(), 1);
}
#[test]
fn label_propagation_unites_connected_graph() {
let g = graph(
&["A", "B", "C"],
&[edge("e1", "A", "B", 1.0), edge("e2", "B", "C", 1.0)],
);
let label = label_propagation_default(&g);
let labels: std::collections::HashSet<u32> = label.iter().copied().collect();
assert_eq!(
labels.len(),
1,
"connected graph collapses to one community"
);
}
#[test]
fn label_propagation_splits_disjoint_components() {
let g = graph(
&["A", "B", "C", "D"],
&[edge("e1", "A", "B", 1.0), edge("e2", "C", "D", 1.0)],
);
let label = label_propagation_default(&g);
let a = g.node_index("A").unwrap() as usize;
let b = g.node_index("B").unwrap() as usize;
let c = g.node_index("C").unwrap() as usize;
let d = g.node_index("D").unwrap() as usize;
assert_eq!(label[a], label[b]);
assert_eq!(label[c], label[d]);
assert_ne!(label[a], label[c]);
let distinct: std::collections::HashSet<u32> = label.iter().copied().collect();
assert_eq!(distinct.len(), 2);
}
#[test]
fn isolated_nodes_each_own_component() {
let g = graph(&["A", "B", "C"], &[]);
let comp = connected_components(&g);
let distinct: std::collections::HashSet<u32> = comp.iter().copied().collect();
assert_eq!(distinct.len(), 3);
}
}