1use super::csr::CsrGraph;
11
12pub const LABEL_PROPAGATION_ITERS: usize = 10;
14
15pub fn connected_components(g: &CsrGraph) -> Vec<u32> {
21 let n = g.node_count();
22 let mut parent: Vec<u32> = (0..n as u32).collect();
23 let mut rank: Vec<u32> = vec![0; n];
24
25 fn find(parent: &mut [u32], x: u32) -> u32 {
26 let mut root = x;
28 while parent[root as usize] != root {
29 root = parent[root as usize];
30 }
31 let mut cur = x;
33 while parent[cur as usize] != root {
34 let next = parent[cur as usize];
35 parent[cur as usize] = root;
36 cur = next;
37 }
38 root
39 }
40
41 for i in g.node_indices() {
44 for (j, _) in g.out_neighbors(i) {
45 let ri = find(&mut parent, i);
46 let rj = find(&mut parent, j);
47 if ri == rj {
48 continue;
49 }
50 let (a, b) = if rank[ri as usize] < rank[rj as usize] {
52 (rj, ri)
53 } else {
54 (ri, rj)
55 };
56 parent[b as usize] = a;
57 if rank[ri as usize] == rank[rj as usize] {
58 rank[a as usize] += 1;
59 }
60 }
61 }
62
63 for i in 0..n as u32 {
65 let root = {
67 let mut root = i;
68 while parent[root as usize] != root {
69 root = parent[root as usize];
70 }
71 root
72 };
73 let mut cur = i;
74 while parent[cur as usize] != root {
75 let next = parent[cur as usize];
76 parent[cur as usize] = root;
77 cur = next;
78 }
79 }
80 parent
81}
82
83pub fn label_propagation(g: &CsrGraph, max_iters: usize) -> Vec<u32> {
90 let n = g.node_count();
91 let mut label: Vec<u32> = (0..n as u32).collect();
92
93 for _ in 0..max_iters {
94 let mut changed = false;
95 for i in g.node_indices() {
96 let mut votes: Vec<(u32, f64)> = g
98 .out_neighbors(i)
99 .map(|(t, w)| (label[t as usize], w))
100 .chain(g.in_neighbors(i).map(|(s, w)| (label[s as usize], w)))
101 .collect();
102 if votes.is_empty() {
103 continue;
104 }
105 votes.sort_unstable_by_key(|&(l, _)| l);
106
107 let mut best_label = votes[0].0;
109 let mut best_weight = 0.0_f64;
110 let mut cur_label = votes[0].0;
111 let mut cur_weight = 0.0_f64;
112 for &(l, w) in &votes {
113 if l == cur_label {
114 cur_weight += w;
115 } else {
116 cur_label = l;
117 cur_weight = w;
118 }
119 if cur_weight > best_weight {
122 best_label = cur_label;
123 best_weight = cur_weight;
124 }
125 }
126
127 if best_label != label[i as usize] {
128 label[i as usize] = best_label;
129 changed = true;
130 }
131 }
132 if !changed {
133 break;
134 }
135 }
136 label
137}
138
139pub fn label_propagation_default(g: &CsrGraph) -> Vec<u32> {
141 label_propagation(g, LABEL_PROPAGATION_ITERS)
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::graph::algo::csr::CsrGraph;
148 use crate::graph::types::GraphEdge;
149
150 fn edge(id: &str, s: &str, t: &str, w: f64) -> GraphEdge {
151 GraphEdge {
152 id: id.into(),
153 source: s.into(),
154 target: t.into(),
155 relation: "related".into(),
156 weight: w,
157 ts: "2026-01-01T00:00:00Z".into(),
158 }
159 }
160
161 fn graph(nodes: &[&str], edges: &[GraphEdge]) -> CsrGraph {
162 let node_ids: Vec<String> = nodes.iter().map(|s| s.to_string()).collect();
163 CsrGraph::from_edges(&node_ids, edges)
164 }
165
166 #[test]
167 fn two_disjoint_components() {
168 let g = graph(
169 &["A", "B", "C", "D", "E"],
170 &[
171 edge("e1", "A", "B", 1.0),
172 edge("e2", "B", "C", 1.0),
173 edge("e3", "D", "E", 1.0),
174 ],
175 );
176 let comp = connected_components(&g);
177 let a = g.node_index("A").unwrap() as usize;
178 let b = g.node_index("B").unwrap() as usize;
179 let c = g.node_index("C").unwrap() as usize;
180 let d = g.node_index("D").unwrap() as usize;
181 let e = g.node_index("E").unwrap() as usize;
182 assert_eq!(comp[a], comp[b]);
184 assert_eq!(comp[b], comp[c]);
185 assert_eq!(comp[d], comp[e]);
186 assert_ne!(comp[a], comp[d]);
187 }
188
189 #[test]
190 fn single_component_connected() {
191 let g = graph(
192 &["A", "B", "C"],
193 &[edge("e1", "A", "B", 1.0), edge("e2", "B", "C", 1.0)],
194 );
195 let comp = connected_components(&g);
196 let labels: std::collections::HashSet<u32> = comp.iter().copied().collect();
197 assert_eq!(labels.len(), 1, "fully connected graph has one component");
198 }
199
200 #[test]
201 fn direction_ignored_for_components() {
202 let g = graph(
204 &["A", "B", "C"],
205 &[edge("e1", "A", "B", 1.0), edge("e2", "C", "B", 1.0)],
206 );
207 let comp = connected_components(&g);
208 let labels: std::collections::HashSet<u32> = comp.iter().copied().collect();
209 assert_eq!(labels.len(), 1);
210 }
211
212 #[test]
213 fn label_propagation_unites_connected_graph() {
214 let g = graph(
216 &["A", "B", "C"],
217 &[edge("e1", "A", "B", 1.0), edge("e2", "B", "C", 1.0)],
218 );
219 let label = label_propagation_default(&g);
220 let labels: std::collections::HashSet<u32> = label.iter().copied().collect();
221 assert_eq!(
222 labels.len(),
223 1,
224 "connected graph collapses to one community"
225 );
226 }
227
228 #[test]
229 fn label_propagation_splits_disjoint_components() {
230 let g = graph(
232 &["A", "B", "C", "D"],
233 &[edge("e1", "A", "B", 1.0), edge("e2", "C", "D", 1.0)],
234 );
235 let label = label_propagation_default(&g);
236 let a = g.node_index("A").unwrap() as usize;
237 let b = g.node_index("B").unwrap() as usize;
238 let c = g.node_index("C").unwrap() as usize;
239 let d = g.node_index("D").unwrap() as usize;
240 assert_eq!(label[a], label[b]);
241 assert_eq!(label[c], label[d]);
242 assert_ne!(label[a], label[c]);
243 let distinct: std::collections::HashSet<u32> = label.iter().copied().collect();
244 assert_eq!(distinct.len(), 2);
245 }
246
247 #[test]
248 fn isolated_nodes_each_own_component() {
249 let g = graph(&["A", "B", "C"], &[]);
251 let comp = connected_components(&g);
252 let distinct: std::collections::HashSet<u32> = comp.iter().copied().collect();
253 assert_eq!(distinct.len(), 3);
254 }
255}