Skip to main content

samyama_graph_algorithms/
cdlp.rs

1//! Community Detection via Label Propagation (CDLP)
2//!
3//! Synchronous label propagation algorithm for community detection.
4//! Each node starts with its own ID as label, then iteratively sets its
5//! label to the most frequent label among its neighbors.
6
7use super::common::{GraphView, NodeId};
8use std::collections::HashMap;
9use rayon::prelude::*;
10
11/// Result of CDLP algorithm
12#[derive(Debug, Clone)]
13pub struct CdlpResult {
14    /// Mapping from NodeId to community label
15    pub labels: HashMap<NodeId, NodeId>,
16    /// Number of iterations until convergence
17    pub iterations: usize,
18}
19
20/// Configuration for CDLP
21pub struct CdlpConfig {
22    /// Maximum number of iterations
23    pub max_iterations: usize,
24}
25
26impl Default for CdlpConfig {
27    fn default() -> Self {
28        Self {
29            max_iterations: 100,
30        }
31    }
32}
33
34/// Run synchronous CDLP on the given graph view
35///
36/// Returns community labels for each node. Uses undirected edges
37/// (both successors and predecessors).
38pub fn cdlp(view: &GraphView, config: &CdlpConfig) -> CdlpResult {
39    let n = view.node_count;
40    if n == 0 {
41        return CdlpResult { labels: HashMap::new(), iterations: 0 };
42    }
43
44    // Initialize: each node gets its own NodeId as label
45    let mut labels: Vec<NodeId> = (0..n).map(|i| view.index_to_node[i]).collect();
46    let mut new_labels = labels.clone();
47    let mut converged = false;
48    let mut iterations = 0;
49
50    let use_parallel = n >= 1000;
51
52    for _iter in 0..config.max_iterations {
53        iterations += 1;
54
55        if use_parallel {
56            let results: Vec<(NodeId, bool)> = (0..n).into_par_iter().map(|idx| {
57                let mut label_counts: HashMap<NodeId, usize> = HashMap::new();
58                for &neighbor in view.successors(idx) {
59                    *label_counts.entry(labels[neighbor]).or_insert(0) += 1;
60                }
61                for &neighbor in view.predecessors(idx) {
62                    *label_counts.entry(labels[neighbor]).or_insert(0) += 1;
63                }
64                if label_counts.is_empty() {
65                    return (labels[idx], true);
66                }
67                let max_count = *label_counts.values().max().unwrap();
68                let best_label = label_counts.into_iter()
69                    .filter(|(_, count)| *count == max_count)
70                    .map(|(label, _)| label)
71                    .min()
72                    .unwrap();
73                (best_label, best_label == labels[idx])
74            }).collect();
75
76            converged = true;
77            for (idx, (label, same)) in results.into_iter().enumerate() {
78                new_labels[idx] = label;
79                if !same { converged = false; }
80            }
81        } else {
82            converged = true;
83            for idx in 0..n {
84                let mut label_counts: HashMap<NodeId, usize> = HashMap::new();
85                for &neighbor in view.successors(idx) {
86                    *label_counts.entry(labels[neighbor]).or_insert(0) += 1;
87                }
88                for &neighbor in view.predecessors(idx) {
89                    *label_counts.entry(labels[neighbor]).or_insert(0) += 1;
90                }
91                if label_counts.is_empty() {
92                    new_labels[idx] = labels[idx];
93                    continue;
94                }
95                let max_count = *label_counts.values().max().unwrap();
96                let best_label = label_counts.into_iter()
97                    .filter(|(_, count)| *count == max_count)
98                    .map(|(label, _)| label)
99                    .min()
100                    .unwrap();
101                if best_label != labels[idx] {
102                    converged = false;
103                }
104                new_labels[idx] = best_label;
105            }
106        }
107
108        std::mem::swap(&mut labels, &mut new_labels);
109
110        if converged {
111            break;
112        }
113    }
114
115    let result_labels: HashMap<NodeId, NodeId> = (0..n)
116        .map(|idx| (view.index_to_node[idx], labels[idx]))
117        .collect();
118
119    CdlpResult { labels: result_labels, iterations }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use std::collections::HashMap;
126    use crate::common::GraphView;
127
128    fn make_triangle_graph() -> GraphView {
129        // Triangle: 1-2, 2-3, 1-3
130        let index_to_node = vec![1, 2, 3];
131        let mut node_to_index = HashMap::new();
132        node_to_index.insert(1, 0);
133        node_to_index.insert(2, 1);
134        node_to_index.insert(3, 2);
135
136        let outgoing = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
137        let incoming = vec![vec![1, 2], vec![0, 2], vec![0, 1]];
138
139        GraphView::from_adjacency_list(3, index_to_node, node_to_index, outgoing, incoming, None)
140    }
141
142    #[test]
143    fn test_cdlp_triangle() {
144        let view = make_triangle_graph();
145        let result = cdlp(&view, &CdlpConfig::default());
146        // In a triangle, all nodes should converge to the same label
147        let labels: Vec<NodeId> = result.labels.values().cloned().collect();
148        assert!(labels.iter().all(|l| *l == labels[0]),
149            "All nodes in a triangle should have the same community label");
150    }
151
152    #[test]
153    fn test_cdlp_disconnected() {
154        // Two disconnected nodes: 1 and 2
155        let index_to_node = vec![1, 2];
156        let mut node_to_index = HashMap::new();
157        node_to_index.insert(1, 0);
158        node_to_index.insert(2, 1);
159
160        let view = GraphView::from_adjacency_list(
161            2, index_to_node, node_to_index,
162            vec![vec![], vec![]], vec![vec![], vec![]], None,
163        );
164        let result = cdlp(&view, &CdlpConfig::default());
165        assert_ne!(result.labels[&1], result.labels[&2]);
166    }
167
168    #[test]
169    fn test_cdlp_empty() {
170        let view = GraphView::from_adjacency_list(
171            0, vec![], HashMap::new(), vec![], vec![], None,
172        );
173        let result = cdlp(&view, &CdlpConfig::default());
174        assert!(result.labels.is_empty());
175    }
176}