1use ahash::AHashMap;
31use log::info;
32use std::{hash::Hash, time::Instant};
33
34use crate::prelude::*;
35use rayon::prelude::*;
36
37pub use crate::afforest::Afforest;
38pub use crate::dss::DisjointSetStruct;
39
40#[derive(Copy, Clone, Debug)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42#[cfg_attr(feature = "clap", derive(clap::Args))]
43pub struct WccConfig {
44 #[cfg_attr(feature = "clap", clap(long, default_value_t = WccConfig::DEFAULT_CHUNK_SIZE))]
46 pub chunk_size: usize,
47
48 #[cfg_attr(feature = "clap", clap(long, default_value_t = WccConfig::DEFAULT_NEIGHBOR_ROUNDS))]
50 pub neighbor_rounds: usize,
51
52 #[cfg_attr(feature = "clap", clap(long, default_value_t = WccConfig::DEFAULT_SAMPLING_SIZE))]
54 pub sampling_size: usize,
55}
56
57impl Default for WccConfig {
58 fn default() -> Self {
59 Self {
60 chunk_size: WccConfig::DEFAULT_CHUNK_SIZE,
61 neighbor_rounds: WccConfig::DEFAULT_NEIGHBOR_ROUNDS,
62 sampling_size: WccConfig::DEFAULT_SAMPLING_SIZE,
63 }
64 }
65}
66
67impl WccConfig {
68 pub const DEFAULT_CHUNK_SIZE: usize = 16384;
69 pub const DEFAULT_NEIGHBOR_ROUNDS: usize = 2;
70 pub const DEFAULT_SAMPLING_SIZE: usize = 1024;
71
72 pub fn new(chunk_size: usize, neighbor_rounds: usize, sampling_size: usize) -> Self {
73 Self {
74 chunk_size,
75 neighbor_rounds,
76 sampling_size,
77 }
78 }
79}
80
81#[allow(clippy::len_without_is_empty)]
82pub trait UnionFind<NI> {
83 fn union(&self, u: NI, v: NI);
85 fn find(&self, u: NI) -> NI;
87 fn len(&self) -> usize;
90 fn compress(&self);
93}
94
95pub trait Components<NI> {
96 fn component(&self, node: NI) -> NI;
97
98 fn to_vec(self) -> Vec<NI>;
99}
100
101pub fn wcc_baseline<NI, G>(graph: &G, config: WccConfig) -> impl Components<NI>
104where
105 NI: Idx,
106 G: Graph<NI> + DirectedNeighbors<NI> + Sync,
107{
108 let node_count = graph.node_count().index();
109 let dss = DisjointSetStruct::new(node_count);
110
111 (0..node_count)
112 .into_par_iter()
113 .chunks(config.chunk_size)
114 .for_each(|chunk| {
115 for u in chunk {
116 let u = NI::new(u);
117 graph.out_neighbors(u).for_each(|v| dss.union(u, *v));
118 }
119 });
120
121 dss
122}
123
124pub fn wcc_afforest<NI, G>(graph: &G, config: WccConfig) -> impl Components<NI>
128where
129 NI: Idx + Hash,
130 G: Graph<NI> + DirectedDegrees<NI> + DirectedNeighbors<NI> + Sync,
131{
132 let start = Instant::now();
133 let comp = Afforest::new(graph.node_count().index());
134 info!("Afforest creation took {:?}", start.elapsed());
135
136 wcc(graph, &comp, config);
137
138 comp
139}
140
141pub fn wcc_afforest_dss<NI, G>(graph: &G, config: WccConfig) -> impl Components<NI>
145where
146 NI: Idx + Hash,
147 G: Graph<NI> + DirectedDegrees<NI> + DirectedNeighbors<NI> + Sync,
148{
149 let start = Instant::now();
150 let dss = DisjointSetStruct::new(graph.node_count().index());
151 info!("DSS creation took {:?}", start.elapsed());
152
153 wcc(graph, &dss, config);
154
155 dss
156}
157
158fn wcc<NI, G, UF>(graph: &G, comp: &UF, config: WccConfig)
159where
160 NI: Idx + Hash,
161 G: Graph<NI> + DirectedDegrees<NI> + DirectedNeighbors<NI> + Sync,
162 UF: UnionFind<NI> + Send + Sync,
163{
164 let start = Instant::now();
165 sample_subgraph(graph, comp, config);
166 info!("Link subgraph took {:?}", start.elapsed());
167
168 let start = Instant::now();
169 comp.compress();
170 info!("Sample compress took {:?}", start.elapsed());
171
172 let start = Instant::now();
173 let largest_component = find_largest_component(comp, config);
174 info!("Get component took {:?}", start.elapsed());
175
176 let start = Instant::now();
177 link_remaining(graph, comp, largest_component, config);
178 info!("Link remaining took {:?}", start.elapsed());
179
180 let start = Instant::now();
181 comp.compress();
182 info!("Final compress took {:?}", start.elapsed());
183}
184
185fn sample_subgraph<NI, G, UF>(graph: &G, uf: &UF, config: WccConfig)
187where
188 NI: Idx,
189 G: Graph<NI> + DirectedNeighbors<NI> + Sync,
190 UF: UnionFind<NI> + Send + Sync,
191{
192 (0..graph.node_count().index())
193 .into_par_iter()
194 .chunks(config.chunk_size)
195 .for_each(|chunk| {
196 for u in chunk {
197 let u = NI::new(u);
198
199 for v in graph.out_neighbors(u).take(config.neighbor_rounds) {
200 uf.union(u, *v);
201 }
202 }
203 });
204}
205
206#[allow(dead_code)]
209fn sample_subgraph_afforest<NI, G>(graph: &G, af: &Afforest<NI>, config: WccConfig)
210where
211 NI: Idx,
212 G: Graph<NI> + DirectedDegrees<NI> + DirectedNeighbors<NI> + Sync,
213{
214 let neighbor_rounds = config.neighbor_rounds;
215 for r in 0..neighbor_rounds {
216 info!("Neighbor round {} of {neighbor_rounds}", r + 1);
217
218 let start = Instant::now();
219 (0..graph.node_count().index())
220 .into_par_iter()
221 .chunks(config.chunk_size)
222 .for_each(|chunk| {
223 for u in chunk {
224 let u = NI::new(u);
225 if r < graph.out_degree(u).index() {
226 for v in graph.out_neighbors(u).skip(r).take(1) {
227 af.union(u, *v);
228 }
229 }
230 }
231 });
232
233 info!(
234 "Neighbor round {r} of {neighbor_rounds} took {:?}",
235 start.elapsed()
236 );
237
238 let start = Instant::now();
239 af.compress();
240 info!("Compress took {:?}", start.elapsed());
241 }
242}
243
244fn find_largest_component<NI, UF>(uf: &UF, config: WccConfig) -> NI
246where
247 NI: Idx + Hash,
248 UF: UnionFind<NI> + Send + Sync,
249{
250 use nanorand::{Rng, WyRand};
251 let mut rng = WyRand::new();
252 let mut sample_counts = AHashMap::<NI, usize>::new();
253
254 for _ in 0..config.sampling_size {
255 let component = uf.find(NI::new(rng.generate_range(0..uf.len())));
256 let count = sample_counts.entry(component).or_insert(0);
257 *count += 1;
258 }
259
260 let (most_frequent, size) = sample_counts
261 .iter()
262 .max_by(|(_, v1), (_, v2)| v1.cmp(v2))
263 .unwrap();
264
265 info!(
266 "Largest intermediate component {most_frequent:?} containing approx. {}% of the graph.",
267 (*size as f32 / config.sampling_size as f32 * 100.0) as usize
268 );
269
270 *most_frequent
271}
272
273fn link_remaining<NI, G, UF>(graph: &G, uf: &UF, skip_component: NI, config: WccConfig)
275where
276 NI: Idx,
277 G: Graph<NI> + DirectedDegrees<NI> + DirectedNeighbors<NI> + Sync,
278 UF: UnionFind<NI> + Send + Sync,
279{
280 (0..graph.node_count().index())
281 .into_par_iter()
282 .chunks(config.chunk_size)
283 .for_each(|chunk| {
284 for u in chunk {
285 let u = NI::new(u);
286 if uf.find(u) == skip_component {
287 continue;
288 }
289
290 if graph.out_degree(u).index() > config.neighbor_rounds {
291 for v in graph.out_neighbors(u).skip(config.neighbor_rounds) {
292 uf.union(u, *v);
293 }
294 }
295
296 for v in graph.in_neighbors(u) {
297 uf.union(u, *v);
298 }
299 }
300 });
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn two_components_afforest_dss() {
309 let graph: DirectedCsrGraph<usize> =
310 GraphBuilder::new().edges(vec![(0, 1), (2, 3)]).build();
311
312 let res = wcc_afforest_dss(&graph, WccConfig::default());
313
314 assert_eq!(res.component(0), res.component(1));
315 assert_eq!(res.component(2), res.component(3));
316 assert_ne!(res.component(1), res.component(2));
317 }
318
319 #[test]
320 fn two_components_afforest() {
321 let graph: DirectedCsrGraph<usize> =
322 GraphBuilder::new().edges(vec![(0, 1), (2, 3)]).build();
323
324 let res = wcc_afforest(&graph, WccConfig::default());
325
326 assert_eq!(res.component(0), res.component(1));
327 assert_eq!(res.component(2), res.component(3));
328 assert_ne!(res.component(1), res.component(2));
329 }
330}