Skip to main content

rust_igraph/algorithms/connectivity/
cohesive_blocks.rs

1//! Cohesive blocking (ALGO-CN-032).
2//!
3//! Counterpart of `igraph_cohesive_blocks()` from
4//! `references/igraph/src/connectivity/cohesive_blocks.c`.
5//!
6//! Cohesive blocking (Moody & White, 2003) finds the hierarchical
7//! structure of maximally k-cohesive vertex subsets of a graph: starting
8//! from the whole graph, it recursively removes minimum-size vertex
9//! separators and keeps the resulting subgraphs whose connectivity is
10//! strictly higher than that of their parent.
11
12use std::collections::VecDeque;
13
14use crate::algorithms::connectivity::separators::minimum_size_separators;
15use crate::algorithms::constructors::create::create;
16use crate::algorithms::flow::vertex_connectivity::vertex_connectivity;
17use crate::algorithms::operators::induced_subgraph::induced_subgraph;
18use crate::algorithms::properties::degree::{DegreeMode, max_degree};
19use crate::algorithms::properties::is_simple::is_simple;
20use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
21
22/// Result of [`cohesive_blocks`].
23///
24/// All four members are index-aligned: entry `i` of [`cohesion`](Self::cohesion)
25/// and [`parent`](Self::parent) describes block `i` of [`blocks`](Self::blocks),
26/// and vertex `i` of [`block_tree`](Self::block_tree) is the same block.
27#[derive(Debug, Clone)]
28pub struct CohesiveBlocks {
29    /// The cohesive blocks; each is the sorted list of original vertex IDs
30    /// that make up the block. Block `0` is always the whole graph.
31    pub blocks: Vec<Vec<VertexId>>,
32    /// The vertex connectivity (structural cohesion) of each block.
33    pub cohesion: Vec<i64>,
34    /// The block-hierarchy parent of each block (an index into
35    /// [`blocks`](Self::blocks)), or `-1` for the root block.
36    pub parent: Vec<i64>,
37    /// The block hierarchy as a directed graph: one vertex per block, an
38    /// arc from each block's parent to the block.
39    pub block_tree: Graph,
40}
41
42/// Connected components of `graph` after deleting the `excluded` vertices.
43///
44/// Mirrors `igraph_i_cb_components`: excluded vertices are not traversed,
45/// but every excluded vertex that borders a component is appended to that
46/// component (so a separator vertex can appear in several components).
47/// Each returned component lists its non-excluded vertices in BFS order
48/// followed by the bordering excluded vertices in discovery order.
49fn cb_components(graph: &Graph, excluded: &[bool]) -> IgraphResult<Vec<Vec<VertexId>>> {
50    let n = graph.vcount() as usize;
51    let mut compid = vec![0usize; n];
52    let mut comps: Vec<Vec<VertexId>> = Vec::new();
53    let mut cno = 0usize;
54    let mut q: VecDeque<VertexId> = VecDeque::new();
55
56    for i in 0..n {
57        if compid[i] != 0 || excluded[i] {
58            continue;
59        }
60        cno += 1;
61        let mut comp: Vec<VertexId> = Vec::new();
62        let start = u32::try_from(i)
63            .map_err(|_| IgraphError::InvalidArgument("cohesive_blocks: vertex overflow".into()))?;
64        q.push_back(start);
65        comp.push(start);
66        compid[i] = cno;
67
68        while let Some(node) = q.pop_front() {
69            for v in graph.neighbors_iter(node)? {
70                let vi = v as usize;
71                if excluded[vi] {
72                    if compid[vi] != cno {
73                        compid[vi] = cno;
74                        comp.push(v);
75                    }
76                } else if compid[vi] == 0 {
77                    compid[vi] = cno;
78                    comp.push(v);
79                    q.push_back(v);
80                }
81            }
82        }
83        comps.push(comp);
84    }
85
86    Ok(comps)
87}
88
89/// Subset test for two ascending-sorted vertex lists: is `needle ⊆ haystack`?
90///
91/// Mirrors `igraph_i_cb_isin`; both slices must be sorted ascending.
92fn cb_isin(needle: &[VertexId], haystack: &[VertexId]) -> bool {
93    if haystack.len() < needle.len() {
94        return false;
95    }
96    let mut np = 0;
97    let mut hp = 0;
98    while np < needle.len() && hp < haystack.len() {
99        match needle[np].cmp(&haystack[hp]) {
100            std::cmp::Ordering::Equal => {
101                np += 1;
102                hp += 1;
103            }
104            std::cmp::Ordering::Less => return false,
105            std::cmp::Ordering::Greater => hp += 1,
106        }
107    }
108    np == needle.len()
109}
110
111/// Convert a known-non-negative `i64` queue index into a `usize`.
112fn qidx(p: i64) -> IgraphResult<usize> {
113    usize::try_from(p)
114        .map_err(|_| IgraphError::InvalidArgument("cohesive_blocks: negative queue index".into()))
115}
116
117/// Identify the hierarchical cohesive block structure of a graph.
118///
119/// Cohesive blocking (J. Moody and D. R. White, "Structural cohesion and
120/// embeddedness: A hierarchical concept of social groups", American
121/// Sociological Review 68(1):103–127, 2003) determines nested subsets of
122/// vertices ordered by their structural cohesion (vertex connectivity).
123/// The whole graph is the root block; each block's maximally more-cohesive
124/// subsets are found recursively by removing minimum-size separators.
125///
126/// The returned [`CohesiveBlocks`] reports, for every block, its vertex
127/// set (sorted ascending), its cohesion, its parent in the hierarchy, and
128/// the hierarchy itself as a directed tree. Block `0` is always the whole
129/// graph with `parent = -1`.
130///
131/// For undirected, simple graphs only.
132///
133/// # Errors
134///
135/// - [`IgraphError::InvalidArgument`] if the graph is directed.
136/// - [`IgraphError::InvalidArgument`] if the graph is not simple (has
137///   self-loops or multiple edges).
138///
139/// # Examples
140///
141/// ```
142/// use rust_igraph::{Graph, cohesive_blocks};
143///
144/// // A 4-clique 0-1-2-3 with a pendant path 3-4-5: the clique is a more
145/// // cohesive block (connectivity 3) inside the whole graph.
146/// let mut g = Graph::with_vertices(6);
147/// for (a, b) in [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3), (3, 4), (4, 5)] {
148///     g.add_edge(a, b).unwrap();
149/// }
150/// let cb = cohesive_blocks(&g).unwrap();
151/// assert_eq!(cb.blocks[0], (0..6).collect::<Vec<_>>());
152/// assert_eq!(cb.parent[0], -1);
153/// // The 4-clique appears as a block with cohesion 3.
154/// assert!(cb.blocks.iter().zip(&cb.cohesion).any(|(b, &c)| b == &[0, 1, 2, 3] && c == 3));
155/// ```
156#[allow(clippy::too_many_lines)]
157pub fn cohesive_blocks(graph: &Graph) -> IgraphResult<CohesiveBlocks> {
158    if graph.is_directed() {
159        return Err(IgraphError::InvalidArgument(
160            "cohesive_blocks: only works on undirected graphs".into(),
161        ));
162    }
163    if !is_simple(graph)? {
164        return Err(IgraphError::InvalidArgument(
165            "cohesive_blocks: only works on simple graphs".into(),
166        ));
167    }
168
169    // The work queue of subgraphs, with parallel bookkeeping arrays.
170    // `q_graph[i]` is a subgraph; `q_mapping[i]` maps its vertex IDs to the
171    // parent's vertex IDs (later rewritten to original IDs); `q_parent[i]`
172    // is the index of its parent in the queue; `q_cohesion[i]` its vertex
173    // connectivity; `q_check[i]` flags blocks born from a separator split
174    // (these need the containment dedup pass).
175    let mut q_graph: Vec<Option<Graph>> = Vec::new();
176    let mut q_mapping: Vec<Vec<VertexId>> = Vec::new();
177    let mut q_parent: Vec<i64> = Vec::new();
178    let mut q_cohesion: Vec<i64> = Vec::new();
179    let mut q_check: Vec<bool> = Vec::new();
180
181    q_graph.push(Some(graph.clone()));
182    q_mapping.push(Vec::new());
183    q_parent.push(-1);
184    q_cohesion.push(vertex_connectivity(graph, true)?);
185    q_check.push(false);
186
187    let mut qptr = 0usize;
188    while qptr < q_graph.len() {
189        let mygraph = q_graph[qptr]
190            .take()
191            .ok_or_else(|| IgraphError::InvalidArgument("cohesive_blocks: queue slot".into()))?;
192        let mycheck = q_check[qptr];
193        let my_cohesion = q_cohesion[qptr];
194        let mynodes = mygraph.vcount() as usize;
195
196        // Minimum-size separators of the current block.
197        let separators = minimum_size_separators(&mygraph)?;
198
199        // Mark all separator vertices.
200        let mut marked = vec![false; mynodes];
201        let mut nsepv = 0usize;
202        for sep in &separators {
203            for &vv in sep {
204                let vi = vv as usize;
205                if !marked[vi] {
206                    nsepv += 1;
207                    marked[vi] = true;
208                }
209            }
210        }
211
212        // Components after removing the separators (bordering separators
213        // are kept attached to each touching component).
214        let mut components = cb_components(&mygraph, &marked)?;
215
216        // Add the separator vertices themselves as one more component, but
217        // only if some vertex is outside every separator.
218        let addedsep = nsepv != mynodes;
219        if addedsep {
220            let sep_comp: Vec<VertexId> = (0..mynodes)
221                .filter(|&i| marked[i])
222                .map(u32::try_from)
223                .collect::<Result<Vec<_>, _>>()
224                .map_err(|_| {
225                    IgraphError::InvalidArgument("cohesive_blocks: vertex overflow".into())
226                })?;
227            components.push(sep_comp);
228        }
229
230        for compvertices in components {
231            let sub = induced_subgraph(&mygraph, &compvertices)?;
232            let maxdeg = i64::from(max_degree(&sub.graph, DegreeMode::All)?);
233            if maxdeg > my_cohesion {
234                let newconn = vertex_connectivity(&sub.graph, true)?;
235                q_graph.push(Some(sub.graph));
236                q_mapping.push(sub.invmap);
237                q_cohesion.push(newconn);
238                q_parent.push(i64::try_from(qptr).map_err(|_| {
239                    IgraphError::InvalidArgument("cohesive_blocks: queue overflow".into())
240                })?);
241                q_check.push(mycheck || addedsep);
242            }
243        }
244
245        qptr += 1;
246    }
247
248    let noblocks_full = qptr;
249    let mut removed = vec![false; noblocks_full];
250    let mut rewritemap = vec![0i64; noblocks_full];
251    let mut badblocks = 0usize;
252
253    // Drop a block whose nearest surviving ancestor is at least as cohesive.
254    for i in 1..noblocks_full {
255        let mut p = qidx(q_parent[i])?;
256        while removed[p] {
257            p = qidx(q_parent[p])?;
258        }
259        if q_cohesion[p] >= q_cohesion[i] {
260            removed[i] = true;
261            badblocks += 1;
262        }
263    }
264
265    // Rewrite each mapping up one level until every mapping is in terms of
266    // the original graph's vertex IDs. Parents are processed before
267    // children (parent index < child index), so the parent's mapping is
268    // already in original IDs when we reach the child.
269    for i in 1..noblocks_full {
270        let p = qidx(q_parent[i])?;
271        if p == 0 {
272            continue;
273        }
274        let pmapping = q_mapping[p].clone();
275        for v in &mut q_mapping[i] {
276            *v = pmapping[*v as usize];
277        }
278    }
279
280    // Separator-as-block components can be subsets of other blocks; drop a
281    // checked block that is contained in another checked block of at least
282    // equal cohesion.
283    for i in 1..noblocks_full {
284        if !q_check[i] || removed[i] {
285            continue;
286        }
287        let ic = q_cohesion[i];
288        for j in 1..noblocks_full {
289            if j == i || !q_check[j] || removed[j] {
290                continue;
291            }
292            if q_cohesion[j] >= ic && cb_isin(&q_mapping[i], &q_mapping[j]) {
293                badblocks += 1;
294                removed[i] = true;
295                break;
296            }
297        }
298    }
299
300    let noblocks = noblocks_full - badblocks;
301
302    let mut blocks: Vec<Vec<VertexId>> = vec![Vec::new(); noblocks];
303    let mut cohesion = vec![0i64; noblocks];
304    let mut parent = vec![0i64; noblocks];
305
306    let mut resptr = 0usize;
307    for i in 0..noblocks_full {
308        if removed[i] {
309            continue;
310        }
311        rewritemap[i] = i64::try_from(resptr)
312            .map_err(|_| IgraphError::InvalidArgument("cohesive_blocks: block overflow".into()))?;
313        cohesion[resptr] = q_cohesion[i];
314
315        let mut p = q_parent[i];
316        while p >= 0 && removed[qidx(p)?] {
317            p = q_parent[qidx(p)?];
318        }
319        if p >= 0 {
320            p = rewritemap[qidx(p)?];
321        }
322        q_parent[i] = p;
323        parent[resptr] = p;
324
325        blocks[resptr] = std::mem::take(&mut q_mapping[i]);
326        resptr += 1;
327    }
328
329    // Block 0 is the whole graph.
330    blocks[0] = (0..graph.vcount()).collect();
331
332    // Build the directed block tree (parent -> child).
333    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(noblocks.saturating_sub(1));
334    for i in 1..noblocks_full {
335        if removed[i] {
336            continue;
337        }
338        let p = u32::try_from(q_parent[i])
339            .map_err(|_| IgraphError::InvalidArgument("cohesive_blocks: tree parent".into()))?;
340        let c = u32::try_from(rewritemap[i])
341            .map_err(|_| IgraphError::InvalidArgument("cohesive_blocks: tree child".into()))?;
342        edges.push((p, c));
343    }
344    let block_tree = create(&edges, u32::try_from(noblocks).unwrap_or(0), true)?;
345
346    Ok(CohesiveBlocks {
347        blocks,
348        cohesion,
349        parent,
350        block_tree,
351    })
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    fn ug(n: u32, edges: &[(u32, u32)]) -> Graph {
359        let mut g = Graph::with_vertices(n);
360        for &(a, b) in edges {
361            g.add_edge(a, b).expect("edge in range");
362        }
363        g
364    }
365
366    /// Canonicalise the output as a set of (sorted block, cohesion) pairs so
367    /// comparisons are independent of block enumeration order.
368    fn canon(cb: &CohesiveBlocks) -> Vec<(Vec<VertexId>, i64)> {
369        let mut v: Vec<(Vec<VertexId>, i64)> = cb
370            .blocks
371            .iter()
372            .zip(&cb.cohesion)
373            .map(|(b, &c)| {
374                let mut bb = b.clone();
375                bb.sort_unstable();
376                (bb, c)
377            })
378            .collect();
379        v.sort();
380        v
381    }
382
383    #[test]
384    fn rejects_directed() {
385        let mut g = Graph::new(3, true).expect("graph");
386        g.add_edge(0, 1).expect("edge");
387        assert!(cohesive_blocks(&g).is_err());
388    }
389
390    #[test]
391    fn rejects_non_simple() {
392        let mut g = Graph::with_vertices(3);
393        g.add_edge(0, 1).expect("edge");
394        g.add_edge(0, 1).expect("edge"); // multi-edge
395        assert!(cohesive_blocks(&g).is_err());
396    }
397
398    #[test]
399    fn single_block_clique() {
400        // K4 has a single cohesive block: the whole graph, cohesion 3.
401        let g = ug(4, &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
402        let cb = cohesive_blocks(&g).expect("cohesive_blocks");
403        assert_eq!(cb.blocks.len(), 1);
404        assert_eq!(cb.blocks[0], vec![0, 1, 2, 3]);
405        assert_eq!(cb.cohesion, vec![3]);
406        assert_eq!(cb.parent, vec![-1]);
407    }
408
409    #[test]
410    fn moody_white() {
411        // The graph from the Moody-White paper (igraph C test fixture).
412        let g = ug(
413            23,
414            &[
415                (0, 1),
416                (0, 2),
417                (0, 3),
418                (0, 4),
419                (0, 5),
420                (1, 2),
421                (1, 3),
422                (1, 4),
423                (1, 6),
424                (2, 3),
425                (2, 5),
426                (2, 6),
427                (3, 4),
428                (3, 5),
429                (3, 6),
430                (4, 5),
431                (4, 6),
432                (4, 20),
433                (5, 6),
434                (6, 7),
435                (6, 10),
436                (6, 13),
437                (6, 18),
438                (7, 8),
439                (7, 10),
440                (7, 13),
441                (8, 9),
442                (9, 11),
443                (9, 12),
444                (10, 11),
445                (10, 13),
446                (11, 15),
447                (12, 15),
448                (13, 14),
449                (14, 15),
450                (16, 17),
451                (16, 18),
452                (16, 19),
453                (17, 19),
454                (17, 20),
455                (18, 19),
456                (18, 21),
457                (18, 22),
458                (19, 20),
459                (20, 21),
460                (20, 22),
461                (21, 22),
462            ],
463        );
464        let cb = cohesive_blocks(&g).expect("cohesive_blocks");
465        let got = canon(&cb);
466        let mut expected = vec![
467            ((0..23).collect::<Vec<_>>(), 1),
468            (vec![0, 1, 2, 3, 4, 5, 6, 16, 17, 18, 19, 20, 21, 22], 2),
469            (vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 2),
470            (vec![0, 1, 2, 3, 4, 5, 6], 5),
471            (vec![6, 7, 10, 13], 3),
472        ];
473        expected.sort();
474        assert_eq!(got, expected);
475    }
476
477    #[test]
478    fn tricky_separators_form_a_block() {
479        let g = ug(
480            8,
481            &[
482                (0, 1),
483                (0, 4),
484                (0, 5),
485                (1, 2),
486                (1, 4),
487                (1, 5),
488                (1, 6),
489                (2, 3),
490                (2, 5),
491                (2, 6),
492                (2, 7),
493                (3, 6),
494                (3, 7),
495                (4, 5),
496                (5, 6),
497                (6, 7),
498            ],
499        );
500        let cb = cohesive_blocks(&g).expect("cohesive_blocks");
501        let got = canon(&cb);
502        let mut expected = vec![
503            ((0..8).collect::<Vec<_>>(), 2),
504            (vec![0, 1, 4, 5], 3),
505            (vec![2, 3, 6, 7], 3),
506            (vec![1, 2, 5, 6], 3),
507        ];
508        expected.sort();
509        assert_eq!(got, expected);
510    }
511
512    #[test]
513    fn science_camp() {
514        let g = ug(
515            18,
516            &[
517                (0, 1),
518                (0, 2),
519                (0, 3),
520                (1, 2),
521                (1, 3),
522                (1, 16),
523                (1, 17),
524                (2, 3),
525                (3, 17),
526                (4, 5),
527                (4, 6),
528                (4, 7),
529                (4, 8),
530                (5, 6),
531                (5, 7),
532                (6, 7),
533                (6, 8),
534                (7, 8),
535                (7, 16),
536                (8, 9),
537                (8, 10),
538                (9, 11),
539                (9, 12),
540                (9, 13),
541                (9, 14),
542                (10, 11),
543                (10, 12),
544                (10, 13),
545                (11, 14),
546                (12, 13),
547                (12, 14),
548                (12, 15),
549                (15, 16),
550                (15, 17),
551                (16, 17),
552            ],
553        );
554        let cb = cohesive_blocks(&g).expect("cohesive_blocks");
555        let got = canon(&cb);
556        let mut expected = vec![
557            ((0..18).collect::<Vec<_>>(), 2),
558            (vec![0, 1, 2, 3], 3),
559            (vec![4, 5, 6, 7, 8], 3),
560            (vec![9, 10, 11, 12, 13, 14], 3),
561        ];
562        expected.sort();
563        assert_eq!(got, expected);
564    }
565
566    #[test]
567    fn root_is_whole_graph_and_tree_consistent() {
568        let g = ug(
569            6,
570            &[
571                (0, 1),
572                (0, 2),
573                (0, 3),
574                (1, 2),
575                (1, 3),
576                (2, 3),
577                (3, 4),
578                (4, 5),
579            ],
580        );
581        let cb = cohesive_blocks(&g).expect("cohesive_blocks");
582        assert_eq!(cb.blocks[0], (0..6).collect::<Vec<_>>());
583        assert_eq!(cb.parent[0], -1);
584        // block_tree vertex count equals number of blocks; one arc per non-root.
585        assert_eq!(cb.block_tree.vcount() as usize, cb.blocks.len());
586        let non_root = cb.parent.iter().filter(|&&p| p >= 0).count();
587        assert_eq!(cb.block_tree.ecount() as usize, non_root);
588    }
589}