use crate::Snapshot;
use crate::algo::{Components, tidy};
use yo_common::Rng;
pub const ROUNDS: u32 = 100;
const SEED: u64 = 0x1abe_15ee;
#[must_use]
pub fn label_propagation(g: &Snapshot) -> Components {
label_propagation_with(g, ROUNDS)
}
#[must_use]
pub fn label_propagation_with(g: &Snapshot, rounds: u32) -> Components {
let n = g.nodes() as usize;
let mut label: Vec<u32> = (0..g.nodes()).collect();
if n == 0 {
return tidy(label);
}
let mut count = vec![0u32; n];
let mut seen: Vec<u32> = Vec::new();
let mut rng = Rng::new(SEED);
let mut order: Vec<u32> = (0..g.nodes()).collect();
for _ in 0..rounds {
shuffle(&mut order, &mut rng);
let mut agreed = true;
for node in &order {
seen.clear();
for to in g.out(*node).iter().chain(g.into_(*node)) {
if to == node {
continue;
}
let at = label[*to as usize] as usize;
if count[at] == 0 {
seen.push(label[*to as usize]);
}
count[at] += 1;
}
let (mut best, mut ties, mut pick) = (0u32, 0u32, label[*node as usize]);
for at in &seen {
let held = count[*at as usize];
if held > best {
(best, ties, pick) = (held, 1, *at);
} else if held == best {
ties += 1;
if rng.next_u64().is_multiple_of(u64::from(ties)) {
pick = *at;
}
}
}
let content = seen.is_empty() || count[label[*node as usize] as usize] == best;
for at in &seen {
count[*at as usize] = 0;
}
if !content {
label[*node as usize] = pick;
agreed = false;
}
}
if agreed {
break;
}
}
tidy(label)
}
fn shuffle(order: &mut [u32], rng: &mut Rng) {
for at in (1..order.len()).rev() {
order.swap(at, (rng.next_u64() % (at as u64 + 1)) as usize);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::algo::wcc;
use crate::graph::NO_PROPS;
use crate::{Graph, Snapshot};
use yo_common::Rng;
fn linked(edges: &[(u64, u64)]) -> Graph {
let mut g = Graph::new();
for (from, to) in edges {
g.link(*from, *to, 1, NO_PROPS).expect("an edge");
}
g
}
fn clique(first: u64, size: u64) -> Vec<(u64, u64)> {
let mut edges = Vec::new();
for a in first..first + size {
for b in a + 1..first + size {
edges.push((a, b));
}
}
edges
}
#[test]
fn two_cliques_on_a_thread() {
let mut edges = clique(0, 8);
edges.extend(clique(100, 8));
edges.push((7, 100));
let s = Snapshot::of(&linked(&edges));
let c = label_propagation(&s);
assert_eq!(c.count(), 2);
for pair in [(0u64, 5u64), (100, 105)] {
let (a, b) = (s.dense(pair.0).expect("a"), s.dense(pair.1).expect("b"));
assert!(c.same(a, b), "{pair:?} should be together");
}
assert!(!c.same(s.dense(0).expect("0"), s.dense(100).expect("100")));
}
#[test]
fn four_cliques_in_a_ring() {
let mut edges = Vec::new();
for group in 0..4u64 {
edges.extend(clique(group * 100, 10));
}
for group in 0..4u64 {
edges.push((group * 100 + 9, (group + 1) % 4 * 100));
}
let s = Snapshot::of(&linked(&edges));
let c = label_propagation(&s);
assert_eq!(c.count(), 4);
for group in 0..4u64 {
let a = s.dense(group * 100 + 1).expect("a");
let b = s.dense(group * 100 + 5).expect("b");
assert!(c.same(a, b), "group {group}");
}
}
#[test]
fn it_never_crosses_a_component() {
let mut rng = Rng::new(0x1ab0);
let (cases, spread) = if cfg!(miri) { (3, 10) } else { (30, 60) };
for case in 0..cases {
let nodes = 2 + rng.next_u64() % spread;
let edges: Vec<(u64, u64)> = (0..nodes)
.map(|_| (rng.next_u64() % nodes, rng.next_u64() % nodes))
.collect();
let s = Snapshot::of(&linked(&edges));
let (weak, c) = (wcc(&s), label_propagation(&s));
for node in 0..s.nodes() {
for other in 0..s.nodes() {
if c.same(node, other) {
assert!(weak.same(node, other), "case {case}, {node} and {other}");
}
}
}
assert!(c.count() >= weak.count(), "case {case}");
}
}
#[test]
fn a_graph_with_no_edges_is_all_singletons() {
let mut g = Graph::new();
for id in 0..5u64 {
g.add_node(id).expect("a node");
}
let c = label_propagation(&Snapshot::of(&g));
assert_eq!(c.count(), 5);
assert_eq!(c.labels(), [0, 1, 2, 3, 4]);
}
#[test]
fn nothing_at_all() {
let c = label_propagation(&Snapshot::default());
assert_eq!(c.count(), 0);
assert!(c.is_empty());
}
#[test]
fn a_self_loop_does_not_vote() {
let s = Snapshot::of(&linked(&[(1, 1), (1, 2), (2, 3), (3, 1)]));
let c = label_propagation(&s);
assert_eq!(c.count(), 1);
}
#[test]
fn one_clique_is_one_community() {
let s = Snapshot::of(&linked(&clique(0, 12)));
assert_eq!(label_propagation(&s).count(), 1);
}
#[test]
fn no_rounds_at_all_leaves_everybody_alone() {
let s = Snapshot::of(&linked(&clique(0, 6)));
let c = label_propagation_with(&s, 0);
assert_eq!(c.count(), 6);
}
#[test]
fn two_runs_agree() {
let mut edges = clique(0, 20);
edges.extend(clique(100, 20));
edges.push((3, 104));
let s = Snapshot::of(&linked(&edges));
assert_eq!(
label_propagation(&s).labels(),
label_propagation(&s).labels()
);
}
#[test]
fn the_labels_are_tidy() {
let mut edges = clique(0, 5);
edges.extend(clique(50, 5));
let s = Snapshot::of(&linked(&edges));
let c = label_propagation(&s);
for node in 0..s.nodes() {
assert_eq!(c.of(c.of(node)), c.of(node), "node {node}");
assert!(c.of(node) <= node);
}
}
#[test]
fn direction_does_not_matter() {
let mut edges = clique(0, 7);
edges.extend(clique(100, 7));
edges.push((6, 100));
let forward = Snapshot::of(&linked(&edges));
let flipped: Vec<(u64, u64)> = edges.iter().map(|(a, b)| (*b, *a)).collect();
let back = Snapshot::of(&linked(&flipped));
assert_eq!(
label_propagation(&forward).labels(),
label_propagation(&back).labels()
);
}
}