use super::graph_algorithms::{
algorithm_timeout_err, compute_modularity, edge_in_scope, edge_weight, intern_connection_types,
scoped_node_set, NodeScope,
};
use super::Interrupt;
use crate::graph::schema::{DirGraph, InternedKey};
use crate::graph::storage::GraphRead;
use petgraph::graph::NodeIndex;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct CommunityAssignment {
pub node_idx: NodeIndex,
pub community_id: usize,
}
#[derive(Debug)]
pub struct CommunityResult {
pub assignments: Vec<CommunityAssignment>,
pub num_communities: usize,
pub modularity: f64,
pub levels: Vec<Vec<CommunityAssignment>>,
}
type Adjacency = Vec<Vec<(usize, f64)>>;
type RefineStep = Option<(Vec<usize>, usize, Vec<usize>)>;
fn build_weighted_adjacency(
graph: &DirGraph,
weight_property: Option<&str>,
connection_types: Option<&[String]>,
scope: Option<&NodeScope>,
) -> (Vec<NodeIndex>, Adjacency, f64) {
let nodes: Vec<NodeIndex> = scoped_node_set(graph, scope);
let n = nodes.len();
let bound = graph.graph.node_bound();
let mut node_to_idx = vec![0usize; bound];
for (i, &node) in nodes.iter().enumerate() {
node_to_idx[node.index()] = i;
}
let interned_ct = intern_connection_types(connection_types);
let mut adj: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
let mut total_weight = 0.0f64;
for edge in {
let g = &graph.graph;
g.edge_references()
} {
if let Some(ref types) = interned_ct {
if !types.iter().any(|t| *t == edge.connection_type()) {
continue;
}
}
if !edge_in_scope(scope, edge.source(), edge.target()) {
continue;
}
let w = edge_weight(graph, edge.id(), weight_property);
let src_i = node_to_idx[edge.source().index()];
let tgt_i = node_to_idx[edge.target().index()];
adj[src_i].push((tgt_i, w));
adj[tgt_i].push((src_i, w));
total_weight += w;
}
for neighbors in &mut adj {
neighbors.sort_unstable_by_key(|&(idx, _)| idx);
neighbors.dedup_by(|a, b| {
if a.0 == b.0 {
b.1 += a.1;
true
} else {
false
}
});
}
(nodes, adj, total_weight)
}
trait NeighborSource {
fn len(&self) -> usize;
fn total_weight(&self) -> f64;
fn for_each_neighbor(&self, i: usize, f: impl FnMut(usize, f64));
}
struct MaterializedAdj<'a> {
adj: &'a [Vec<(usize, f64)>],
m: f64,
}
impl NeighborSource for MaterializedAdj<'_> {
#[inline]
fn len(&self) -> usize {
self.adj.len()
}
#[inline]
fn total_weight(&self) -> f64 {
self.m
}
#[inline]
fn for_each_neighbor(&self, i: usize, mut f: impl FnMut(usize, f64)) {
for &(j, w) in &self.adj[i] {
f(j, w);
}
}
}
struct CsrSource<'a> {
graph: &'a DirGraph,
nodes: Vec<NodeIndex>,
node_to_idx: Vec<usize>,
interned_ct: Option<Vec<InternedKey>>,
weight_property: Option<&'a str>,
use_fast: bool,
fast_conn: Option<u64>,
m: f64,
}
impl<'a> CsrSource<'a> {
fn new(
graph: &'a DirGraph,
weight_property: Option<&'a str>,
connection_types: Option<&[String]>,
) -> Self {
let nodes: Vec<NodeIndex> = graph.graph.node_indices().collect();
let bound = graph.graph.node_bound();
let mut node_to_idx = vec![0usize; bound];
for (i, &node) in nodes.iter().enumerate() {
node_to_idx[node.index()] = i;
}
let interned_ct = intern_connection_types(connection_types);
let multi_type = connection_types.is_some_and(|c| c.len() > 1);
let use_fast = weight_property.is_none() && !multi_type;
let fast_conn = if use_fast {
interned_ct
.as_ref()
.and_then(|v| v.first())
.map(|k| k.as_u64())
} else {
None
};
let mut m = 0.0f64;
for edge in graph.graph.edge_references() {
if let Some(ref types) = interned_ct {
if !types.iter().any(|t| *t == edge.connection_type()) {
continue;
}
}
m += edge_weight(graph, edge.id(), weight_property);
}
Self {
graph,
nodes,
node_to_idx,
interned_ct,
weight_property,
use_fast,
fast_conn,
m,
}
}
}
impl NeighborSource for CsrSource<'_> {
fn len(&self) -> usize {
self.nodes.len()
}
fn total_weight(&self) -> f64 {
self.m
}
fn for_each_neighbor(&self, i: usize, mut f: impl FnMut(usize, f64)) {
use petgraph::Direction;
let node = self.nodes[i];
if self.use_fast {
for dir in [Direction::Outgoing, Direction::Incoming] {
for (peer, _eid) in self
.graph
.graph
.iter_peers_filtered(node, dir, self.fast_conn)
{
f(self.node_to_idx[peer.index()], 1.0);
}
}
return;
}
for dir in [Direction::Outgoing, Direction::Incoming] {
for edge in self.graph.graph.edges_directed(node, dir) {
if let Some(ref types) = self.interned_ct {
if !types.iter().any(|t| *t == edge.connection_type()) {
continue;
}
}
let peer = match dir {
Direction::Outgoing => edge.target(),
Direction::Incoming => edge.source(),
};
let w = edge_weight(self.graph, edge.id(), self.weight_property);
f(self.node_to_idx[peer.index()], w);
}
}
}
}
pub(super) fn scoped_universe(
graph: &DirGraph,
node_types: Option<&[String]>,
edge_types: Option<&[InternedKey]>,
) -> Vec<NodeIndex> {
if let Some(types) = node_types {
let mut v = Vec::new();
for t in types {
if let Some(tn) = graph.type_indices.get(t.as_str()) {
v.extend(tn.iter());
}
}
v
} else if let Some(keys) = edge_types {
let mut seen: HashSet<NodeIndex> = HashSet::new();
for edge in graph.graph.edge_references() {
if keys.contains(&edge.connection_type()) {
seen.insert(edge.source());
seen.insert(edge.target());
}
}
seen.into_iter().collect()
} else {
graph.graph.node_indices().collect()
}
}
pub(super) struct DedupNeighborSource<'a> {
graph: &'a DirGraph,
pub(super) nodes: Vec<NodeIndex>,
node_to_idx: Vec<u32>,
edge_types: Option<Vec<InternedKey>>,
}
impl<'a> DedupNeighborSource<'a> {
pub(super) fn new(
graph: &'a DirGraph,
nodes: Vec<NodeIndex>,
edge_types: Option<Vec<InternedKey>>,
) -> Self {
let bound = graph.graph.node_bound();
let mut node_to_idx = vec![u32::MAX; bound];
for (i, &node) in nodes.iter().enumerate() {
node_to_idx[node.index()] = i as u32;
}
Self {
graph,
nodes,
node_to_idx,
edge_types,
}
}
pub(super) fn len(&self) -> usize {
self.nodes.len()
}
pub(super) fn neighbors_deduped(&self, v: usize, buf: &mut Vec<u32>) {
use petgraph::Direction;
buf.clear();
let node = self.nodes[v];
match &self.edge_types {
None => {
for dir in [Direction::Outgoing, Direction::Incoming] {
for (peer, _e) in self.graph.graph.iter_peers_filtered(node, dir, None) {
let p = self.node_to_idx[peer.index()];
if p != u32::MAX && p as usize != v {
buf.push(p);
}
}
}
}
Some(types) => {
for dir in [Direction::Outgoing, Direction::Incoming] {
for edge in self.graph.graph.edges_directed(node, dir) {
if !types.contains(&edge.connection_type()) {
continue;
}
let peer = match dir {
Direction::Outgoing => edge.target(),
Direction::Incoming => edge.source(),
};
let p = self.node_to_idx[peer.index()];
if p != u32::MAX && p as usize != v {
buf.push(p);
}
}
}
}
}
buf.sort_unstable();
buf.dedup();
}
}
fn local_move<S: NeighborSource>(
src: &S,
init: Option<&[usize]>,
resolution: f64,
deadline: Interrupt,
) -> Result<Vec<usize>, String> {
let n = src.len();
let total_weight = src.total_weight();
let mut community: Vec<usize> = match init {
Some(p) => p.to_vec(),
None => (0..n).collect(),
};
if n == 0 || total_weight == 0.0 {
return Ok(community);
}
let mut degree: Vec<f64> = vec![0.0; n];
for (i, d) in degree.iter_mut().enumerate() {
src.for_each_neighbor(i, |_, w| *d += w);
}
let mut sigma_tot: Vec<f64> = vec![0.0; n];
for i in 0..n {
sigma_tot[community[i]] += degree[i];
}
let m = total_weight;
let two_m = 2.0 * m;
let inv_m = 1.0 / m;
let resolution_over_two_m_sq = resolution / (two_m * two_m);
let mut comm_weight: Vec<f64> = vec![0.0; n];
let mut touched_comms: Vec<usize> = Vec::with_capacity(64);
let max_iterations = 100;
for _ in 0..max_iterations {
if deadline.exceeded() {
{
return Err(algorithm_timeout_err());
}
}
let mut improved = false;
for i in 0..n {
let current_community = community[i];
let k_i = degree[i];
let k_i_res = k_i * resolution_over_two_m_sq;
touched_comms.clear();
src.for_each_neighbor(i, |neighbor, w| {
if neighbor == i {
return; }
let c = community[neighbor];
if comm_weight[c] == 0.0 {
touched_comms.push(c);
}
comm_weight[c] += w;
});
let k_i_in_current = comm_weight[current_community];
let mut best_community = current_community;
let mut best_delta = 0.0f64;
for &cand_community in &touched_comms {
if cand_community == current_community {
continue;
}
let k_i_in_cand = comm_weight[cand_community];
let sigma_cand = sigma_tot[cand_community];
let sigma_curr = sigma_tot[current_community] - k_i;
let gain_add = k_i_in_cand * inv_m - sigma_cand * k_i_res;
let loss_remove = k_i_in_current * inv_m - sigma_curr * k_i_res;
let delta = gain_add - loss_remove;
if delta > best_delta {
best_delta = delta;
best_community = cand_community;
}
}
for &c in &touched_comms {
comm_weight[c] = 0.0;
}
if best_community != current_community {
sigma_tot[current_community] -= k_i;
sigma_tot[best_community] += k_i;
community[i] = best_community;
improved = true;
}
}
if !improved {
break;
}
}
Ok(community)
}
fn renumber_communities(community: &[usize]) -> (Vec<usize>, usize) {
let mut id_map: HashMap<usize, usize> = HashMap::new();
let renumbered: Vec<usize> = community
.iter()
.map(|&c| {
let next = id_map.len();
*id_map.entry(c).or_insert(next)
})
.collect();
let k = id_map.len();
(renumbered, k)
}
fn aggregate_graph<S: NeighborSource>(src: &S, community: &[usize], k: usize) -> Adjacency {
let mut acc: Vec<HashMap<usize, f64>> = vec![HashMap::new(); k];
for i in 0..src.len() {
let ci = community[i];
src.for_each_neighbor(i, |j, w| {
let cj = community[j];
*acc[ci].entry(cj).or_insert(0.0) += w;
});
}
let mut new_adj: Vec<Vec<(usize, f64)>> = vec![Vec::new(); k];
for (ci, entry) in acc.into_iter().enumerate() {
let mut row: Vec<(usize, f64)> = entry.into_iter().collect();
row.sort_unstable_by_key(|&(idx, _)| idx); new_adj[ci] = row;
}
new_adj
}
fn louvain_levels<S: NeighborSource>(
level0: &S,
resolution: f64,
deadline: Interrupt,
) -> Result<Vec<Vec<usize>>, String> {
let n0 = level0.len();
let m = level0.total_weight();
let mut levels: Vec<Vec<usize>> = Vec::new();
let mut node_to_super: Vec<usize> = (0..n0).collect();
let moved = local_move(level0, None, resolution, deadline)?;
let (partition, k) = renumber_communities(&moved);
for s in node_to_super.iter_mut() {
*s = partition[*s];
}
levels.push(node_to_super.clone());
if k == n0 {
return Ok(levels);
}
let mut adj = aggregate_graph(level0, &partition, k);
loop {
let src = MaterializedAdj { adj: &adj, m };
let moved = local_move(&src, None, resolution, deadline)?;
let (partition, k) = renumber_communities(&moved);
if k == adj.len() {
break;
}
for s in node_to_super.iter_mut() {
*s = partition[*s];
}
levels.push(node_to_super.clone());
adj = aggregate_graph(&src, &partition, k);
}
Ok(levels)
}
fn empty_community_result() -> CommunityResult {
CommunityResult {
assignments: Vec::new(),
num_communities: 0,
modularity: 0.0,
levels: Vec::new(),
}
}
fn build_community_result(
graph: &DirGraph,
nodes: &[NodeIndex],
levels: &[Vec<usize>],
total_weight: f64,
weight_property: Option<&str>,
) -> CommunityResult {
let best = levels.last().expect("at least one level");
let bound = graph.graph.node_bound();
let mut community_bound: Vec<usize> = vec![0; bound];
let mut node_exists: Vec<bool> = vec![false; bound];
for (i, &node) in nodes.iter().enumerate() {
community_bound[node.index()] = best[i];
node_exists[node.index()] = true;
}
let num_communities = best.iter().copied().max().map(|m| m + 1).unwrap_or(0);
let modularity = if total_weight == 0.0 {
0.0
} else {
compute_modularity(
graph,
&community_bound,
&node_exists,
total_weight,
weight_property,
)
};
let levels_out: Vec<Vec<CommunityAssignment>> = levels
.iter()
.map(|lc| {
lc.iter()
.enumerate()
.map(|(i, &c)| CommunityAssignment {
node_idx: nodes[i],
community_id: c,
})
.collect()
})
.collect();
let assignments = levels_out.last().cloned().unwrap_or_default();
CommunityResult {
assignments,
num_communities,
modularity,
levels: levels_out,
}
}
pub fn louvain_communities(
graph: &DirGraph,
weight_property: Option<&str>,
resolution: f64,
connection_types: Option<&[String]>,
scope: Option<&NodeScope>,
deadline: Interrupt,
) -> Result<CommunityResult, String> {
let _arena_guard = graph.graph.begin_query();
let (nodes, levels, m) =
if (graph.graph.is_disk() || graph.graph.is_mapped()) && scope.is_none() {
let nodes: Vec<NodeIndex> = graph.graph.node_indices().collect();
if nodes.is_empty() {
return Ok(empty_community_result());
}
let src = CsrSource::new(graph, weight_property, connection_types);
let m = src.total_weight();
let levels = louvain_levels(&src, resolution, deadline)?;
(nodes, levels, m)
} else {
let (nodes, adj, m) =
build_weighted_adjacency(graph, weight_property, connection_types, scope);
if nodes.is_empty() {
return Ok(empty_community_result());
}
let src = MaterializedAdj { adj: &adj, m };
let levels = louvain_levels(&src, resolution, deadline)?;
(nodes, levels, m)
};
Ok(build_community_result(
graph,
&nodes,
&levels,
m,
weight_property,
))
}
fn refine_connected<S: NeighborSource>(src: &S, partition: &[usize]) -> (Vec<usize>, usize) {
let n = src.len();
let mut refined = vec![usize::MAX; n];
let mut next_id = 0usize;
let mut stack: Vec<usize> = Vec::new();
for start in 0..n {
if refined[start] != usize::MAX {
continue;
}
let comm = partition[start];
let id = next_id;
next_id += 1;
refined[start] = id;
stack.push(start);
while let Some(u) = stack.pop() {
src.for_each_neighbor(u, |v, _w| {
if v != u && partition[v] == comm && refined[v] == usize::MAX {
refined[v] = id;
stack.push(v);
}
});
}
}
(refined, next_id)
}
fn leiden_levels<S: NeighborSource>(
level0: &S,
resolution: f64,
deadline: Interrupt,
) -> Result<Vec<Vec<usize>>, String> {
let n0 = level0.len();
let m = level0.total_weight();
let mut levels: Vec<Vec<usize>> = Vec::new();
let mut node_to_super: Vec<usize> = (0..n0).collect();
fn step<S: NeighborSource>(
src: &S,
n0: usize,
node_to_super: &[usize],
init: Option<&[usize]>,
levels: &mut Vec<Vec<usize>>,
resolution: f64,
deadline: Interrupt,
) -> Result<RefineStep, String> {
let moved = local_move(src, init, resolution, deadline)?;
let (p, _kp) = renumber_communities(&moved);
let raw_level: Vec<usize> = (0..n0).map(|o| p[node_to_super[o]]).collect();
let (level_norm, _) = renumber_communities(&raw_level);
if levels.last() == Some(&level_norm) {
return Ok(None); }
levels.push(level_norm);
let (refined, k_ref) = refine_connected(src, &p);
if k_ref == src.len() {
return Ok(None); }
let mut next_init = vec![0usize; k_ref];
for s in 0..src.len() {
next_init[refined[s]] = p[s];
}
Ok(Some((refined, k_ref, next_init)))
}
let Some((refined, k_ref, next_init)) = step(
level0,
n0,
&node_to_super,
None,
&mut levels,
resolution,
deadline,
)?
else {
if levels.is_empty() {
levels.push((0..n0).collect());
}
return Ok(levels);
};
let mut adj = aggregate_graph(level0, &refined, k_ref);
for s in node_to_super.iter_mut() {
*s = refined[*s];
}
let mut init = next_init;
loop {
let src = MaterializedAdj { adj: &adj, m };
match step(
&src,
n0,
&node_to_super,
Some(&init),
&mut levels,
resolution,
deadline,
)? {
None => break,
Some((refined, k_ref, next_init)) => {
adj = aggregate_graph(&src, &refined, k_ref);
for s in node_to_super.iter_mut() {
*s = refined[*s];
}
init = next_init;
}
}
}
Ok(levels)
}
pub fn leiden_communities(
graph: &DirGraph,
weight_property: Option<&str>,
resolution: f64,
connection_types: Option<&[String]>,
scope: Option<&NodeScope>,
deadline: Interrupt,
) -> Result<CommunityResult, String> {
let _arena_guard = graph.graph.begin_query();
let (nodes, levels, m) =
if (graph.graph.is_disk() || graph.graph.is_mapped()) && scope.is_none() {
let nodes: Vec<NodeIndex> = graph.graph.node_indices().collect();
if nodes.is_empty() {
return Ok(empty_community_result());
}
let src = CsrSource::new(graph, weight_property, connection_types);
let m = src.total_weight();
let levels = leiden_levels(&src, resolution, deadline)?;
(nodes, levels, m)
} else {
let (nodes, adj, m) =
build_weighted_adjacency(graph, weight_property, connection_types, scope);
if nodes.is_empty() {
return Ok(empty_community_result());
}
let src = MaterializedAdj { adj: &adj, m };
let levels = leiden_levels(&src, resolution, deadline)?;
(nodes, levels, m)
};
Ok(build_community_result(
graph,
&nodes,
&levels,
m,
weight_property,
))
}
pub fn label_propagation(
graph: &DirGraph,
max_iterations: usize,
connection_types: Option<&[String]>,
scope: Option<&NodeScope>,
deadline: Interrupt,
) -> Result<CommunityResult, String> {
let _arena_guard = graph.graph.begin_query();
if (graph.graph.is_disk() || graph.graph.is_mapped()) && scope.is_none() {
return label_propagation_streaming(graph, max_iterations, connection_types, deadline);
}
let nodes: Vec<NodeIndex> = scoped_node_set(graph, scope);
let n = nodes.len();
if n == 0 {
return Ok(empty_community_result());
}
let bound = graph.graph.node_bound();
let mut node_to_idx = vec![0usize; bound];
for (i, &node) in nodes.iter().enumerate() {
node_to_idx[node.index()] = i;
}
let interned_ct = intern_connection_types(connection_types);
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for edge in {
let g = &graph.graph;
g.edge_references()
} {
if let Some(ref types) = interned_ct {
if !types.iter().any(|t| *t == edge.connection_type()) {
continue;
}
}
if !edge_in_scope(scope, edge.source(), edge.target()) {
continue;
}
let src_i = node_to_idx[edge.source().index()];
let tgt_i = node_to_idx[edge.target().index()];
adj[src_i].push(tgt_i);
adj[tgt_i].push(src_i);
}
for neighbors in &mut adj {
neighbors.sort_unstable();
neighbors.dedup();
}
let mut labels: Vec<usize> = (0..n).collect();
let mut label_count: Vec<usize> = vec![0; n];
let mut touched_labels: Vec<usize> = Vec::with_capacity(64);
for _ in 0..max_iterations {
if deadline.exceeded() {
{
return Err(algorithm_timeout_err());
}
}
let mut changed = false;
for i in 0..n {
let neighbors = &adj[i];
if neighbors.is_empty() {
continue; }
touched_labels.clear();
for &neighbor in neighbors {
let lbl = labels[neighbor];
if label_count[lbl] == 0 {
touched_labels.push(lbl);
}
label_count[lbl] += 1;
}
let mut best_label = labels[i];
let mut best_count = 0;
for &lbl in &touched_labels {
if label_count[lbl] > best_count {
best_count = label_count[lbl];
best_label = lbl;
}
}
for &lbl in &touched_labels {
label_count[lbl] = 0;
}
if best_label != labels[i] {
labels[i] = best_label;
changed = true;
}
}
if !changed {
break;
}
}
Ok(label_prop_result(graph, &nodes, &labels))
}
fn label_prop_result(graph: &DirGraph, nodes: &[NodeIndex], labels: &[usize]) -> CommunityResult {
let bound = graph.graph.node_bound();
let mut labels_bound: Vec<usize> = vec![0; bound];
let mut node_exists: Vec<bool> = vec![false; bound];
for (i, &node) in nodes.iter().enumerate() {
labels_bound[node.index()] = labels[i];
node_exists[node.index()] = true;
}
let mut id_map: HashMap<usize, usize> = HashMap::new();
for &lbl in labels {
let next_id = id_map.len();
id_map.entry(lbl).or_insert(next_id);
}
let assignments: Vec<CommunityAssignment> = nodes
.iter()
.enumerate()
.map(|(i, &idx)| CommunityAssignment {
node_idx: idx,
community_id: *id_map.get(&labels[i]).unwrap(),
})
.collect();
let total_weight = graph.graph.edge_count() as f64;
let num_communities = id_map.len();
let modularity = compute_modularity(graph, &labels_bound, &node_exists, total_weight, None);
CommunityResult {
assignments,
num_communities,
modularity,
levels: Vec::new(),
}
}
fn label_propagation_streaming(
graph: &DirGraph,
max_iterations: usize,
connection_types: Option<&[String]>,
deadline: Interrupt,
) -> Result<CommunityResult, String> {
let nodes: Vec<NodeIndex> = graph.graph.node_indices().collect();
if nodes.is_empty() {
return Ok(empty_community_result());
}
let interned_ct = intern_connection_types(connection_types);
let src = DedupNeighborSource::new(graph, nodes, interned_ct);
let n = src.len();
let mut labels: Vec<usize> = (0..n).collect();
let mut label_count: Vec<usize> = vec![0; n];
let mut touched_labels: Vec<usize> = Vec::with_capacity(64);
let mut buf: Vec<u32> = Vec::new();
for _ in 0..max_iterations {
if deadline.exceeded() {
{
return Err(algorithm_timeout_err());
}
}
let mut changed = false;
for i in 0..n {
src.neighbors_deduped(i, &mut buf);
if buf.is_empty() {
continue; }
touched_labels.clear();
for &neighbor in &buf {
let lbl = labels[neighbor as usize];
if label_count[lbl] == 0 {
touched_labels.push(lbl);
}
label_count[lbl] += 1;
}
let mut best_label = labels[i];
let mut best_count = 0;
for &lbl in &touched_labels {
if label_count[lbl] > best_count {
best_count = label_count[lbl];
best_label = lbl;
}
}
for &lbl in &touched_labels {
label_count[lbl] = 0;
}
if best_label != labels[i] {
labels[i] = best_label;
changed = true;
}
}
if !changed {
break;
}
}
Ok(label_prop_result(graph, &src.nodes, &labels))
}