use crate::Snapshot;
use crate::algo::{Components, tidy};
use yo_common::Rng;
pub const RESOLUTION: f64 = 1.0;
const THETA: f64 = 0.01;
const LEVELS: u32 = 64;
const PASSES: u32 = 8;
const SEED: u64 = 0x1ead_e401;
#[must_use]
pub fn leiden(g: &Snapshot) -> Components {
leiden_with(g, RESOLUTION)
}
#[must_use]
pub fn leiden_with(g: &Snapshot, resolution: f64) -> Components {
unfold(g, resolution, true)
}
#[must_use]
pub fn louvain(g: &Snapshot) -> Components {
louvain_with(g, RESOLUTION)
}
#[must_use]
pub fn louvain_with(g: &Snapshot, resolution: f64) -> Components {
unfold(g, resolution, false)
}
#[must_use]
pub fn modularity(g: &Snapshot, labels: &[u32]) -> f64 {
modularity_with(g, labels, RESOLUTION)
}
#[must_use]
pub fn modularity_with(g: &Snapshot, labels: &[u32], resolution: f64) -> f64 {
let w = Weighted::of(g);
assert_eq!(labels.len(), w.nodes(), "one label a node");
let (labels, groups) = renumber(labels);
w.quality(&labels, groups, resolution)
}
#[derive(Clone)]
struct Weighted {
at: Vec<u64>,
to: Vec<u32>,
w: Vec<f64>,
self_w: Vec<f64>,
strength: Vec<f64>,
total: f64,
}
impl Weighted {
fn of(g: &Snapshot) -> Weighted {
let n = g.nodes() as usize;
let mut at = vec![0u64; n + 1];
for node in 0..n {
let both = g.out_degree(node as u32) + g.in_degree(node as u32);
at[node + 1] = at[node] + u64::from(both);
}
let mut to = vec![0u32; at[n] as usize];
let mut self_w = vec![0f64; n];
let mut fill = at.clone();
for node in 0..n as u32 {
for other in g.out(node) {
if *other == node {
self_w[node as usize] += 1.0;
continue;
}
to[fill[node as usize] as usize] = *other;
fill[node as usize] += 1;
}
for other in g.into_(node) {
if *other == node {
continue;
}
to[fill[node as usize] as usize] = *other;
fill[node as usize] += 1;
}
}
let mut edges: Vec<(u32, f64)> = Vec::new();
let mut out = Vec::with_capacity(to.len());
let mut w = Vec::with_capacity(to.len());
let mut next = vec![0u64; n + 1];
for node in 0..n {
let mine = &mut to[at[node] as usize..fill[node] as usize];
mine.sort_unstable();
edges.clear();
for other in mine.iter() {
match edges.last_mut() {
Some((last, weight)) if last == other => *weight += 1.0,
_ => edges.push((*other, 1.0)),
}
}
for (other, weight) in &edges {
out.push(*other);
w.push(*weight);
}
next[node + 1] = out.len() as u64;
}
Weighted::new(next, out, w, self_w)
}
fn new(at: Vec<u64>, to: Vec<u32>, w: Vec<f64>, self_w: Vec<f64>) -> Weighted {
let n = self_w.len();
let mut strength = vec![0f64; n];
for node in 0..n {
let mine = at[node] as usize..at[node + 1] as usize;
strength[node] = w[mine].iter().sum::<f64>() + 2.0 * self_w[node];
}
let total = strength.iter().sum();
Weighted {
at,
to,
w,
self_w,
strength,
total,
}
}
fn nodes(&self) -> usize {
self.self_w.len()
}
fn near(&self, node: u32) -> (&[u32], &[f64]) {
let mine = self.at[node as usize] as usize..self.at[node as usize + 1] as usize;
(&self.to[mine.clone()], &self.w[mine])
}
fn quality(&self, of: &[u32], groups: usize, resolution: f64) -> f64 {
if self.total == 0.0 {
return 0.0;
}
let mut inside = vec![0f64; groups];
let mut tot = vec![0f64; groups];
for node in 0..self.nodes() {
let mine = of[node] as usize;
tot[mine] += self.strength[node];
inside[mine] += 2.0 * self.self_w[node];
let (near, w) = self.near(node as u32);
for (other, weight) in near.iter().zip(w) {
if of[*other as usize] as usize == mine {
inside[mine] += weight;
}
}
}
(0..groups)
.map(|c| inside[c] / self.total - resolution * (tot[c] / self.total).powi(2))
.sum()
}
}
fn unfold(g: &Snapshot, resolution: f64, refined: bool) -> Components {
let base = Weighted::of(g);
let n = base.nodes();
let mut answer: Vec<u32> = (0..n as u32).collect();
if n == 0 {
return tidy(answer);
}
let mut rng = Rng::new(SEED);
for _ in 0..PASSES {
let next = pass(&base, &answer, resolution, refined, &mut rng);
if next == answer {
break;
}
answer = next;
}
tidy(answer)
}
fn pass(base: &Weighted, start: &[u32], resolution: f64, refined: bool, rng: &mut Rng) -> Vec<u32> {
let n = base.nodes();
let mut answer = vec![0u32; n];
let mut w = base.clone();
let mut at: Vec<u32> = (0..n as u32).collect();
let (mut comm, _) = renumber(start);
for _ in 0..LEVELS {
local_move(&w, &mut comm, resolution, rng);
let (tidied, groups) = renumber(&comm);
for (node, at) in at.iter().enumerate() {
answer[node] = tidied[*at as usize];
}
if groups == w.nodes() {
break;
}
let split = if refined {
refine(&w, &tidied, groups, resolution, rng)
} else {
tidied.clone()
};
let (next, next_comm, moved) = aggregate(&w, &split, &tidied);
for at in &mut at {
*at = moved[*at as usize];
}
w = next;
comm = next_comm;
}
renumber(&answer).0
}
fn local_move(g: &Weighted, comm: &mut [u32], resolution: f64, rng: &mut Rng) {
let n = g.nodes();
if n == 0 || g.total == 0.0 {
return;
}
let mut tot = vec![0f64; n];
let mut size = vec![0u32; n];
for node in 0..n {
tot[comm[node] as usize] += g.strength[node];
size[comm[node] as usize] += 1;
}
let mut free: Vec<u32> = (0..n as u32).filter(|c| size[*c as usize] == 0).collect();
let mut queue: Vec<u32> = (0..n as u32).collect();
shuffle(&mut queue, rng);
let mut queued = vec![true; n];
let mut head = 0usize;
let mut link = vec![0f64; n];
let mut seen: Vec<u32> = Vec::new();
while head < queue.len() {
let node = queue[head];
head += 1;
queued[node as usize] = false;
let was = comm[node as usize];
let strength = g.strength[node as usize];
tot[was as usize] -= strength;
size[was as usize] -= 1;
if size[was as usize] == 0 {
free.push(was);
}
seen.clear();
let (near, w) = g.near(node);
for (other, weight) in near.iter().zip(w) {
let at = comm[*other as usize] as usize;
if link[at] == 0.0 {
seen.push(comm[*other as usize]);
}
link[at] += weight;
}
let value = |c: u32, link: &[f64]| {
link[c as usize] - resolution * strength * tot[c as usize] / g.total
};
let mut best = was;
let mut most = value(was, &link);
if most < 0.0 && size[was as usize] > 0 {
while let Some(empty) = free.pop() {
if size[empty as usize] == 0 {
(best, most) = (empty, 0.0);
free.push(empty);
break;
}
}
}
for c in &seen {
let worth = value(*c, &link);
if worth > most || (worth == most && *c < best) {
(best, most) = (*c, worth);
}
}
for c in &seen {
link[*c as usize] = 0.0;
}
comm[node as usize] = best;
tot[best as usize] += strength;
size[best as usize] += 1;
if best == was {
continue;
}
for other in near {
if comm[*other as usize] != best && !queued[*other as usize] {
queued[*other as usize] = true;
queue.push(*other);
}
}
}
}
fn refine(g: &Weighted, comm: &[u32], groups: usize, resolution: f64, rng: &mut Rng) -> Vec<u32> {
let n = g.nodes();
let mut refined: Vec<u32> = (0..n as u32).collect();
if g.total == 0.0 {
return refined;
}
let mut at = vec![0u32; groups + 1];
for c in comm {
at[*c as usize + 1] += 1;
}
for c in 0..groups {
at[c + 1] += at[c];
}
let mut member = vec![0u32; n];
let mut fill = at.clone();
for (node, c) in comm.iter().enumerate() {
member[fill[*c as usize] as usize] = node as u32;
fill[*c as usize] += 1;
}
let mut tot = g.strength.clone();
let mut out = vec![0f64; n];
let mut link = vec![0f64; n];
let mut seen: Vec<u32> = Vec::new();
let mut pick: Vec<(u32, f64)> = Vec::new();
let mut order: Vec<u32> = Vec::new();
for c in 0..groups {
let mine = &member[at[c] as usize..at[c + 1] as usize];
if mine.len() < 3 {
continue;
}
let whole: f64 = mine.iter().map(|node| g.strength[*node as usize]).sum();
for node in mine {
let (near, w) = g.near(*node);
out[*node as usize] = near
.iter()
.zip(w)
.filter(|(other, _)| comm[**other as usize] as usize == c)
.map(|(_, weight)| *weight)
.sum();
}
order.clear();
order.extend_from_slice(mine);
shuffle(&mut order, rng);
for node in &order {
let node = *node;
if refined[node as usize] != node || tot[node as usize] != g.strength[node as usize] {
continue;
}
let strength = g.strength[node as usize];
if out[node as usize] < resolution * strength * (whole - strength) / g.total {
continue;
}
seen.clear();
let (near, w) = g.near(node);
for (other, weight) in near.iter().zip(w) {
if comm[*other as usize] as usize != c {
continue;
}
let into = refined[*other as usize] as usize;
if into == node as usize {
continue;
}
if link[into] == 0.0 {
seen.push(refined[*other as usize]);
}
link[into] += weight;
}
pick.clear();
let mut top = f64::NEG_INFINITY;
for subset in &seen {
let there = tot[*subset as usize];
if out[*subset as usize] < resolution * there * (whole - there) / g.total {
continue;
}
let worth = link[*subset as usize] - resolution * strength * there / g.total;
if worth >= 0.0 {
top = top.max(worth);
pick.push((*subset, worth));
}
}
if !pick.is_empty() {
let mut sum = 0.0;
for (_, worth) in &mut pick {
*worth = ((*worth - top) / THETA).exp();
sum += *worth;
}
let mut want = uniform(rng) * sum;
let mut into = pick[pick.len() - 1].0;
for (subset, weight) in &pick {
want -= weight;
if want <= 0.0 {
into = *subset;
break;
}
}
refined[node as usize] = into;
tot[into as usize] += strength;
out[into as usize] += out[node as usize] - 2.0 * link[into as usize];
tot[node as usize] = 0.0;
}
for subset in &seen {
link[*subset as usize] = 0.0;
}
}
}
refined
}
fn aggregate(g: &Weighted, split: &[u32], comm: &[u32]) -> (Weighted, Vec<u32>, Vec<u32>) {
let (moved, n) = renumber(split);
let mut self_w = vec![0f64; n];
let mut edges: Vec<(u32, u32, f64)> = Vec::new();
for node in 0..g.nodes() {
let mine = moved[node];
self_w[mine as usize] += g.self_w[node];
let (near, w) = g.near(node as u32);
for (other, weight) in near.iter().zip(w) {
let theirs = moved[*other as usize];
if theirs == mine {
self_w[mine as usize] += weight / 2.0;
} else {
edges.push((mine, theirs, *weight));
}
}
}
edges.sort_unstable_by_key(|(from, to, _)| (*from, *to));
let mut at = vec![0u64; n + 1];
let mut to = Vec::new();
let mut w = Vec::new();
for (from, other, weight) in &edges {
match to.last() {
Some(last) if *last == *other && at[*from as usize + 1] == to.len() as u64 => {
*w.last_mut().expect("a weight") += weight;
}
_ => {
to.push(*other);
w.push(*weight);
at[*from as usize + 1] = to.len() as u64;
}
}
at[*from as usize + 1] = to.len() as u64;
}
for node in 0..n {
at[node + 1] = at[node + 1].max(at[node]);
}
let mut starts = vec![0u32; n];
for node in 0..g.nodes() {
starts[moved[node] as usize] = comm[node];
}
let (starts, _) = renumber(&starts);
(Weighted::new(at, to, w, self_w), starts, moved)
}
fn renumber(of: &[u32]) -> (Vec<u32>, usize) {
let mut seen = vec![u32::MAX; of.len()];
let mut next = 0u32;
let mut out = vec![0u32; of.len()];
for (node, at) in of.iter().enumerate() {
let seen = &mut seen[*at as usize];
if *seen == u32::MAX {
*seen = next;
next += 1;
}
out[node] = *seen;
}
(out, next as usize)
}
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);
}
}
fn uniform(rng: &mut Rng) -> f64 {
(rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
#[cfg(test)]
mod tests {
use super::*;
use crate::algo::{label_propagation, 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
}
fn ring(groups: u64, size: u64) -> Vec<(u64, u64)> {
let mut edges = Vec::new();
for group in 0..groups {
edges.extend(clique(group * 1000, size));
}
for group in 0..groups {
edges.push((group * 1000, (group + 1) % groups * 1000 + 1));
}
edges
}
fn slow(g: &Snapshot, of: &[u32], resolution: f64) -> f64 {
let n = g.nodes() as usize;
let mut a = vec![vec![0f64; n]; n];
for node in 0..n as u32 {
for other in g.out(node) {
a[node as usize][*other as usize] += 1.0;
a[*other as usize][node as usize] += 1.0;
}
}
let degree: Vec<f64> = (0..n).map(|node| a[node].iter().sum()).collect();
let total: f64 = degree.iter().sum();
if total == 0.0 {
return 0.0;
}
let mut q = 0.0;
for i in 0..n {
for j in 0..n {
if of[i] == of[j] {
q += a[i][j] - resolution * degree[i] * degree[j] / total;
}
}
}
q / total
}
#[test]
fn the_measure_agrees_with_the_definition() {
let mut rng = Rng::new(0x9d1);
let (cases, spread) = if cfg!(miri) { (3, 8) } else { (40, 30) };
for case in 0..cases {
let nodes = 2 + rng.next_u64() % spread;
let edges: Vec<(u64, u64)> = (0..nodes * 2)
.map(|_| (rng.next_u64() % nodes, rng.next_u64() % nodes))
.collect();
let s = Snapshot::of(&linked(&edges));
let groups = u64::from(s.nodes()).clamp(1, 3);
let of: Vec<u32> = (0..s.nodes())
.map(|_| (rng.next_u64() % groups) as u32)
.collect();
for resolution in [0.5, 1.0, 2.0] {
let (mine, theirs) = (
modularity_with(&s, &of, resolution),
slow(&s, &of, resolution),
);
assert!(
(mine - theirs).abs() < 1e-9,
"case {case} at {resolution}, {mine} against {theirs}"
);
}
}
}
#[test]
fn the_measure_knows_a_good_split_from_a_bad_one() {
let s = Snapshot::of(&linked(&ring(4, 8)));
let good: Vec<u32> = (0..s.nodes()).map(|node| node / 8).collect();
let one = vec![0u32; s.nodes() as usize];
let each: Vec<u32> = (0..s.nodes()).collect();
assert!(modularity(&s, &good) > 0.6, "{}", modularity(&s, &good));
assert!((modularity(&s, &one)).abs() < 1e-9);
assert!(modularity(&s, &each) < 0.0);
}
#[test]
fn both_find_the_ring_of_cliques() {
let s = Snapshot::of(&linked(&ring(6, 10)));
for c in [leiden(&s), louvain(&s)] {
assert_eq!(c.count(), 6);
for group in 0..6u64 {
let a = s.dense(group * 1000 + 2).expect("a");
let b = s.dense(group * 1000 + 7).expect("b");
assert!(c.same(a, b), "group {group}");
}
}
}
#[test]
fn both_beat_label_propagation_for_modularity() {
let s = Snapshot::of(&linked(&ring(8, 6)));
let quick = modularity(&s, label_propagation(&s).labels());
for c in [leiden(&s), louvain(&s)] {
assert!(modularity(&s, c.labels()) >= quick - 1e-9);
}
}
#[test]
fn leiden_communities_are_never_disconnected() {
let mut rng = Rng::new(0x1ead);
let (cases, spread) = if cfg!(miri) { (3, 6) } else { (30, 90) };
for case in 0..cases {
let nodes = if cfg!(miri) { 6 } else { 10 } + rng.next_u64() % spread;
let edges: Vec<(u64, u64)> = (0..nodes * 3)
.map(|_| (rng.next_u64() % nodes, rng.next_u64() % nodes))
.collect();
let s = Snapshot::of(&linked(&edges));
let c = leiden(&s);
assert!(connected(&s, c.labels()), "case {case}");
}
}
fn connected(g: &Snapshot, of: &[u32]) -> bool {
let n = g.nodes() as usize;
let mut seen = vec![false; n];
let mut groups = std::collections::HashSet::new();
for node in 0..n {
if seen[node] || !groups.insert(of[node]) {
if !seen[node] {
return false;
}
continue;
}
let mut todo = vec![node as u32];
seen[node] = true;
while let Some(at) = todo.pop() {
for other in g.out(at).iter().chain(g.into_(at)) {
if of[*other as usize] == of[node] && !seen[*other as usize] {
seen[*other as usize] = true;
todo.push(*other);
}
}
}
}
true
}
#[test]
fn a_community_never_crosses_a_component() {
let mut rng = Rng::new(0x1ea0);
let (cases, spread) = if cfg!(miri) { (3, 10) } else { (30, 50) };
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 = wcc(&s);
for c in [leiden(&s), louvain(&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}");
}
}
}
}
}
}
#[test]
fn one_clique_is_one_community() {
let s = Snapshot::of(&linked(&clique(0, 15)));
assert_eq!(leiden(&s).count(), 1);
assert_eq!(louvain(&s).count(), 1);
}
#[test]
fn a_higher_resolution_cuts_finer() {
let s = Snapshot::of(&linked(&ring(4, 12)));
let coarse = leiden_with(&s, 0.25).count();
let plain = leiden_with(&s, 1.0).count();
let fine = leiden_with(&s, 6.0).count();
assert!(coarse <= plain, "{coarse} against {plain}");
assert!(fine > plain, "{fine} against {plain}");
}
#[test]
fn nothing_at_all() {
for c in [leiden(&Snapshot::default()), louvain(&Snapshot::default())] {
assert_eq!(c.count(), 0);
assert!(c.is_empty());
}
assert_eq!(modularity(&Snapshot::default(), &[]), 0.0);
}
#[test]
fn a_graph_with_no_edges_is_all_singletons() {
let mut g = Graph::new();
for id in 0..6u64 {
g.add_node(id).expect("a node");
}
let s = Snapshot::of(&g);
assert_eq!(leiden(&s).count(), 6);
assert_eq!(louvain(&s).count(), 6);
}
#[test]
fn a_self_loop_does_not_break_the_measure() {
let s = Snapshot::of(&linked(&[(1, 1), (1, 2), (2, 3), (3, 1)]));
for c in [leiden(&s), louvain(&s)] {
assert!(modularity(&s, c.labels()).abs() < 1e-9);
}
}
#[test]
fn two_runs_agree() {
let s = Snapshot::of(&linked(&ring(5, 9)));
assert_eq!(leiden(&s).labels(), leiden(&s).labels());
assert_eq!(louvain(&s).labels(), louvain(&s).labels());
}
#[test]
fn direction_does_not_matter() {
let edges = ring(4, 8);
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!(leiden(&forward).labels(), leiden(&back).labels());
}
#[test]
fn no_single_node_move_helps() {
let mut rng = Rng::new(0x1ea2);
let (cases, base, spread) = if cfg!(miri) { (2, 6, 4) } else { (15, 20, 40) };
for case in 0..cases {
let nodes = base + rng.next_u64() % spread;
let edges: Vec<(u64, u64)> = (0..nodes * 4)
.map(|_| (rng.next_u64() % nodes, rng.next_u64() % nodes))
.collect();
let s = Snapshot::of(&linked(&edges));
for c in [leiden(&s), louvain(&s)] {
let mut of = c.labels().to_vec();
let now = modularity(&s, &of);
for node in 0..s.nodes() {
let was = of[node as usize];
for other in c.labels() {
of[node as usize] = *other;
let then = modularity(&s, &of);
assert!(then <= now + 1e-9, "case {case}, node {node}");
}
of[node as usize] = was;
}
}
}
}
#[test]
fn the_labels_are_tidy() {
let s = Snapshot::of(&linked(&ring(4, 7)));
let c = leiden(&s);
for node in 0..s.nodes() {
assert_eq!(c.of(c.of(node)), c.of(node));
assert!(c.of(node) <= node);
}
}
}