use std::collections::HashMap;
use crate::dictionary::Dictionary;
#[derive(Debug, Clone)]
pub struct Graph {
n: usize,
adj: Vec<Vec<(usize, f64)>>,
degree: Vec<f64>,
m: f64,
}
impl Graph {
pub fn from_edges(n: usize, edges: &[(usize, usize, f64)]) -> Self {
let mut adj_w: Vec<HashMap<usize, f64>> = vec![HashMap::new(); n];
let mut degree = vec![0.0; n];
let mut m = 0.0;
for &(u, v, w) in edges {
assert!(u < n && v < n, "edge endpoint out of range");
*adj_w[u].entry(v).or_insert(0.0) += w;
degree[u] += w;
m += w;
if u != v {
*adj_w[v].entry(u).or_insert(0.0) += w;
degree[v] += w;
} else {
degree[u] += w;
}
}
let adj = adj_w
.into_iter()
.map(|h| {
let mut nbrs: Vec<(usize, f64)> = h.into_iter().collect();
nbrs.sort_unstable_by_key(|&(j, _)| j);
nbrs
})
.collect();
Graph { n, adj, degree, m }
}
pub fn node_count(&self) -> usize {
self.n
}
pub fn total_weight(&self) -> f64 {
self.m
}
pub fn modularity(&self, comm: &[usize]) -> f64 {
if self.m == 0.0 {
return 0.0;
}
let two_m = 2.0 * self.m;
let mut q = 0.0;
for (i, neighbors) in self.adj.iter().enumerate() {
for &(j, w) in neighbors {
if comm[i] == comm[j] {
q += w; }
}
}
let mut sigma_tot: HashMap<usize, f64> = HashMap::new();
for (i, &c) in comm.iter().enumerate() {
*sigma_tot.entry(c).or_insert(0.0) += self.degree[i];
}
let deg_term: f64 = sigma_tot.values().map(|&s| s * s).sum();
(q - deg_term / two_m) / two_m
}
pub fn quotient(&self, comm: &[usize], k: usize) -> Graph {
let mut edges: HashMap<(usize, usize), f64> = HashMap::new();
for (i, neighbors) in self.adj.iter().enumerate() {
for &(j, w) in neighbors {
if i <= j {
let (ci, cj) = (comm[i], comm[j]);
let key = (ci.min(cj), ci.max(cj));
*edges.entry(key).or_insert(0.0) += w;
}
}
}
let mut edge_vec: Vec<(usize, usize, f64)> =
edges.into_iter().map(|((a, b), w)| (a, b, w)).collect();
edge_vec.sort_unstable_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1)));
Graph::from_edges(k, &edge_vec)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Partition {
pub comm: Vec<usize>,
pub count: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PyramidAlgo {
#[default]
Louvain,
Types,
}
impl PyramidAlgo {
pub fn from_cli(s: &str) -> Option<Self> {
match s {
"louvain" => Some(Self::Louvain),
"types" => Some(Self::Types),
_ => None,
}
}
}
pub fn louvain_one_level(g: &Graph) -> Partition {
let n = g.n;
if g.m == 0.0 {
return Partition {
comm: (0..n).collect(),
count: n,
};
}
let two_m = 2.0 * g.m;
let mut comm: Vec<usize> = (0..n).collect();
let mut sigma_tot: Vec<f64> = g.degree.clone();
let mut k_to = vec![0.0f64; n];
let mut touched: Vec<usize> = Vec::new();
let mut improved = true;
while improved {
improved = false;
for (i, neighbors) in g.adj.iter().enumerate() {
let ci = comm[i];
let ki = g.degree[i];
sigma_tot[ci] -= ki;
for &(j, w) in neighbors {
if j != i {
let cj = comm[j];
if k_to[cj] == 0.0 {
touched.push(cj);
}
k_to[cj] += w;
}
}
let gain = |c: usize| -> f64 { k_to[c] - sigma_tot[c] * ki / two_m };
let mut best_c = ci;
let mut best_gain = gain(ci);
touched.sort_unstable();
for &c in &touched {
let gc = gain(c);
if gc > best_gain {
best_gain = gc;
best_c = c;
}
}
sigma_tot[best_c] += ki;
comm[i] = best_c;
if best_c != ci {
improved = true;
}
for &c in &touched {
k_to[c] = 0.0;
}
touched.clear();
}
}
let mut remap: HashMap<usize, usize> = HashMap::new();
for c in &mut comm {
let next = remap.len();
*c = *remap.entry(*c).or_insert(next);
}
Partition {
count: remap.len(),
comm,
}
}
pub fn project_graph(dict: &Dictionary, triples: &[(u32, u32, u32)]) -> Graph {
let n = dict.node_count() as usize;
let edges: Vec<(usize, usize, f64)> = triples
.iter()
.map(|&(s, _, o)| {
(
dict.subject_node(s) as usize,
dict.object_node(o) as usize,
1.0,
)
})
.collect();
Graph::from_edges(n, &edges)
}
#[derive(Debug, Clone)]
pub struct Dendrogram {
pub levels: Vec<Partition>,
}
impl Dendrogram {
pub fn rounds(&self) -> usize {
self.levels.len()
}
pub fn community_count(&self, round: usize) -> usize {
self.levels[round].count
}
pub fn base_community(&self, node: usize, round: usize) -> usize {
let mut c = node;
for level in &self.levels[..=round] {
c = level.comm[c];
}
c
}
}
pub fn build_dendrogram(g: &Graph) -> Dendrogram {
let mut levels = Vec::new();
let mut current = g.clone();
loop {
let p = louvain_one_level(¤t);
if p.count >= current.node_count() {
break; }
let next = current.quotient(&p.comm, p.count);
levels.push(p);
if next.node_count() <= 1 {
break;
}
current = next;
}
Dendrogram { levels }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dictionary::DictionaryBuilder;
fn barbell() -> Graph {
let edges = [
(0, 1, 1.0),
(1, 2, 1.0),
(0, 2, 1.0),
(3, 4, 1.0),
(4, 5, 1.0),
(3, 5, 1.0),
(2, 3, 1.0), ];
Graph::from_edges(6, &edges)
}
#[test]
fn two_communities() {
let g = barbell();
let p = louvain_one_level(&g);
assert_eq!(p.count, 2, "barbell should split into two communities");
assert_eq!(p.comm[0], p.comm[1]);
assert_eq!(p.comm[1], p.comm[2]);
assert_eq!(p.comm[3], p.comm[4]);
assert_eq!(p.comm[4], p.comm[5]);
assert_ne!(p.comm[2], p.comm[3]);
assert!(g.modularity(&p.comm) > g.modularity(&[0; 6]));
}
#[test]
fn dendrogram_is_deterministic_across_runs() {
let mut edges = Vec::new();
let clusters = 6;
let size = 6;
for c in 0..clusters {
let base = c * size;
for i in 0..size {
for j in (i + 1)..size {
edges.push((base + i, base + j, 1.0));
}
}
let next = ((c + 1) % clusters) * size;
edges.push((base, next, 1.0));
}
let g = Graph::from_edges(clusters * size, &edges);
let a = build_dendrogram(&g);
let b = build_dendrogram(&g);
assert_eq!(a.levels, b.levels, "dendrogram must be reproducible");
assert!(!a.levels.is_empty(), "the ring of cliques should compress");
}
#[test]
fn quotient_collapses_communities() {
let g = barbell();
let p = louvain_one_level(&g);
let q = g.quotient(&p.comm, p.count);
assert_eq!(q.node_count(), 2);
assert!((q.total_weight() - g.total_weight()).abs() < 1e-9);
}
#[test]
fn empty_graph_is_singletons() {
let g = Graph::from_edges(3, &[]);
let p = louvain_one_level(&g);
assert_eq!(p.count, 3);
}
#[test]
fn dendrogram_groups_barbell() {
let g = barbell();
let d = build_dendrogram(&g);
assert!(d.rounds() >= 1);
let c0 = d.base_community(0, 0);
assert_eq!(c0, d.base_community(1, 0));
assert_eq!(c0, d.base_community(2, 0));
let c3 = d.base_community(3, 0);
assert_eq!(c3, d.base_community(4, 0));
assert_ne!(c0, c3);
}
#[test]
fn project_and_cluster_rdf() {
let edges = [
("A", "B"),
("B", "C"),
("A", "C"),
("D", "E"),
("E", "F"),
("D", "F"),
("C", "D"), ];
let mut db = DictionaryBuilder::new();
for (s, o) in edges {
db.observe(s, "knows", o);
}
let dict = db.build();
let triples: Vec<_> = edges
.iter()
.map(|(s, o)| dict.encode(s, "knows", o).unwrap())
.collect();
let g = project_graph(&dict, &triples);
assert_eq!(g.node_count(), 6); let d = build_dendrogram(&g);
let node = |t: &str| {
dict.subject_id(t)
.map(|id| dict.subject_node(id))
.or_else(|| dict.object_id(t).map(|id| dict.object_node(id)))
.unwrap() as usize
};
let comm = |t: &str| d.base_community(node(t), 0);
assert_eq!(comm("A"), comm("B"));
assert_eq!(comm("B"), comm("C"));
assert_eq!(comm("D"), comm("E"));
assert_eq!(comm("E"), comm("F"));
assert_ne!(comm("C"), comm("D"));
}
}