1use rustc_hash::FxHashMap;
12
13use crate::graph::graph_structures::UnionFind;
14use crate::prelude::*;
15
16pub fn connected_components<T>(
31 graph: &CompressedSparseData2<T>,
32) -> Result<(usize, Vec<usize>), MethodsErrors>
33where
34 T: Clone,
35{
36 let n = validate_square_csr(graph)?;
37
38 let mut uf = UnionFind::new(n);
39 for i in 0..n {
40 for idx in graph.indptr[i] as usize..graph.indptr[i + 1] as usize {
41 uf.union(i as u32, graph.indices[idx]);
42 }
43 }
44
45 Ok(label_by_root(
46 &mut uf,
47 (0..n as u32).collect::<Vec<_>>().as_slice(),
48 ))
49}
50
51pub fn induced_components<T>(
66 graph: &CompressedSparseData2<T>,
67 members: &[usize],
68) -> Result<(usize, Vec<usize>), MethodsErrors>
69where
70 T: Clone,
71{
72 let n = validate_square_csr(graph)?;
73
74 let mut position: FxHashMap<usize, u32> = FxHashMap::default();
75 for (p, &node) in members.iter().enumerate() {
76 if node >= n {
77 return Err(CoreErrors::SliceIndexOutOfBounds {
78 index: node,
79 len: n,
80 }
81 .into());
82 }
83 position.entry(node).or_insert(p as u32);
84 }
85
86 let mut uf = UnionFind::new(members.len());
87
88 for (p, &node) in members.iter().enumerate() {
89 if let Some(&first) = position.get(&node)
91 && first != p as u32
92 {
93 uf.union(first, p as u32);
94 }
95 for idx in graph.indptr[node] as usize..graph.indptr[node + 1] as usize {
96 let neighbour = graph.indices[idx] as usize;
97 if let Some(&q) = position.get(&neighbour) {
98 uf.union(p as u32, q);
99 }
100 }
101 }
102
103 Ok(label_by_root(
104 &mut uf,
105 (0..members.len() as u32).collect::<Vec<_>>().as_slice(),
106 ))
107}
108
109fn label_by_root(uf: &mut UnionFind, elements: &[u32]) -> (usize, Vec<usize>) {
127 let mut root_to_label: FxHashMap<u32, usize> = FxHashMap::default();
128 let mut labels = Vec::with_capacity(elements.len());
129
130 for &e in elements {
131 let root = uf.find(e);
132 let next = root_to_label.len();
133 let label = *root_to_label.entry(root).or_insert(next);
134 labels.push(label);
135 }
136
137 (root_to_label.len(), labels)
138}
139
140fn validate_square_csr<T>(graph: &CompressedSparseData2<T>) -> Result<usize, MethodsErrors>
150where
151 T: Clone,
152{
153 if !graph.cs_type.is_csr() {
154 return Err(CoreErrors::SparseMatrixMustBeCsr.into());
155 }
156 let (rows, cols) = graph.shape;
157 if rows != cols {
158 return Err(CoreErrors::ShapeMismatch {
159 expected: (rows, rows),
160 got: (rows, cols),
161 }
162 .into());
163 }
164 Ok(rows)
165}
166
167#[cfg(test)]
172mod tests {
173 use super::*;
174
175 fn directed_csr(n: usize, edges: &[(usize, usize)]) -> CompressedSparseData2<f64> {
178 let mut rows: Vec<Vec<u32>> = vec![Vec::new(); n];
179 for &(a, b) in edges {
180 rows[a].push(b as u32);
181 }
182
183 let mut indptr: Vec<u32> = vec![0];
184 let mut indices: Vec<u32> = Vec::new();
185 let mut data: Vec<f64> = Vec::new();
186 for row in rows.iter_mut() {
187 row.sort_unstable();
188 for &j in row.iter() {
189 indices.push(j);
190 data.push(1.0);
191 }
192 indptr.push(indices.len() as u32);
193 }
194
195 CompressedSparseData2::new_csr(&data, &indices, &indptr, None, (n, n))
196 }
197
198 #[test]
200 fn test_components_of_two_cliques() {
201 let edges = [(0, 1), (1, 2), (2, 0), (3, 4), (4, 3)];
203 let graph = directed_csr(5, &edges);
204
205 let (n_comps, labels) = connected_components(&graph).unwrap();
206
207 assert_eq!(n_comps, 2);
208 assert_eq!(labels, vec![0, 0, 0, 1, 1]);
209 }
210
211 #[test]
213 fn test_components_are_weak_not_strong() {
214 let graph = directed_csr(3, &[(0, 1), (1, 2)]);
217
218 let (n_comps, labels) = connected_components(&graph).unwrap();
219
220 assert_eq!(n_comps, 1);
221 assert_eq!(labels, vec![0, 0, 0]);
222 }
223
224 #[test]
226 fn test_isolated_nodes_get_own_components() {
227 let graph = directed_csr(4, &[(0, 1)]);
228
229 let (n_comps, labels) = connected_components(&graph).unwrap();
230
231 assert_eq!(n_comps, 3);
232 assert_eq!(labels, vec![0, 0, 1, 2]);
233 }
234
235 #[test]
238 fn test_induced_components_split_a_connected_graph() {
239 let edges = [
241 (0, 1),
242 (1, 0),
243 (1, 2),
244 (2, 1),
245 (2, 3),
246 (3, 2),
247 (3, 4),
248 (4, 3),
249 ];
250 let graph = directed_csr(5, &edges);
251
252 let (n_comps, labels) = induced_components(&graph, &[0, 1, 3, 4]).unwrap();
253
254 assert_eq!(n_comps, 2);
255 assert_eq!(labels, vec![0, 0, 1, 1]);
256 }
257
258 #[test]
260 fn test_induced_components_ignore_edges_leaving_the_subset() {
261 let edges = [(0, 1), (1, 0), (1, 2), (2, 1)];
263 let graph = directed_csr(3, &edges);
264
265 let (n_comps, labels) = induced_components(&graph, &[0, 2]).unwrap();
266
267 assert_eq!(n_comps, 2);
268 assert_eq!(labels, vec![0, 1]);
269 }
270
271 #[test]
274 fn test_induced_components_put_duplicate_members_in_one_component() {
275 let edges = [(0, 1), (1, 0), (1, 2), (2, 1)];
279 let graph = directed_csr(3, &edges);
280
281 let (n_comps, labels) = induced_components(&graph, &[0, 2, 0, 2, 0]).unwrap();
282
283 assert_eq!(n_comps, 2);
284 assert_eq!(labels, vec![0, 1, 0, 1, 0]);
285 }
286
287 #[test]
290 fn test_induced_components_merge_through_a_duplicated_member() {
291 let edges = [(0, 1), (1, 0), (1, 2), (2, 1), (2, 3), (3, 2)];
294 let graph = directed_csr(4, &edges);
295
296 let (n_comps, labels) = induced_components(&graph, &[0, 1, 2, 1]).unwrap();
297
298 assert_eq!(n_comps, 1);
299 assert_eq!(labels, vec![0, 0, 0, 0]);
300 }
301
302 #[test]
304 fn test_induced_components_rejects_out_of_range_member() {
305 let graph = directed_csr(3, &[(0, 1)]);
306
307 assert!(induced_components(&graph, &[0, 9]).is_err());
308 }
309
310 #[test]
312 fn test_components_reject_csc_input() {
313 let graph = directed_csr(3, &[(0, 1)]).transform();
314
315 assert!(connected_components(&graph).is_err());
316 }
317}