Skip to main content

rust_igraph/algorithms/properties/
coreness.rs

1//! k-core decomposition / coreness (ALGO-PR-015 + ALGO-PR-015b).
2//!
3//! Counterpart of `igraph_coreness()` from
4//! `references/igraph/src/centrality/coreness.c`. Implements Batagelj &
5//! Zaversnik's O(|E|) "An O(m) Algorithm for Cores Decomposition of
6//! Networks" (<https://arxiv.org/abs/cs/0310049>): bin sort by degree,
7//! walk vertices in ascending current-core order, decrement higher-core
8//! neighbours and shuffle them down the bins.
9//!
10//! [`coreness`] (PR-015) is the canonical undirected entry. For
11//! directed graphs [`coreness_with_mode`] (PR-015b) selects between
12//! in-cores, out-cores, or the undirected projection.
13
14use crate::core::{Graph, IgraphError, IgraphResult};
15
16/// Direction-handling for [`coreness_with_mode`].
17///
18/// Counterpart of upstream's `igraph_neimode_t`. Undirected graphs
19/// always behave as if `All`.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum CorenessMode {
22    /// All edges count regardless of direction. The classical k-core.
23    All,
24    /// In-cores: degree is the number of incoming edges. The k-in-core
25    /// is the maximal subgraph in which every vertex has at least
26    /// `k` incoming edges.
27    In,
28    /// Out-cores: degree is the number of outgoing edges.
29    Out,
30}
31
32/// Per-vertex coreness number.
33///
34/// Returns `Vec<u32>` of length `vcount`: `result[v]` is the highest
35/// `k` such that `v` belongs to the k-core. The empty graph yields an
36/// empty vector. Self-loops contribute 2 to a vertex's degree (matches
37/// upstream `IGRAPH_LOOPS`).
38///
39/// # Examples
40///
41/// ```
42/// use rust_igraph::{Graph, coreness};
43///
44/// // K3 triangle: every vertex has degree 2 in a graph where the
45/// // minimum degree is 2 → coreness 2 for all three.
46/// let mut g = Graph::with_vertices(3);
47/// g.add_edge(0, 1).unwrap();
48/// g.add_edge(1, 2).unwrap();
49/// g.add_edge(0, 2).unwrap();
50/// assert_eq!(coreness(&g).unwrap(), vec![2, 2, 2]);
51/// ```
52pub fn coreness(graph: &Graph) -> IgraphResult<Vec<u32>> {
53    coreness_with_mode(graph, CorenessMode::All)
54}
55
56/// Coreness with explicit [`CorenessMode`] (ALGO-PR-015b).
57///
58/// Counterpart of `igraph_coreness(_, _, mode)`. On undirected graphs
59/// every mode reduces to [`CorenessMode::All`]. Self-loops contribute
60/// 2 to total degree (`All`) or 1 to each of in/out (`In`/`Out`).
61///
62/// # Examples
63///
64/// ```
65/// use rust_igraph::{Graph, coreness_with_mode, CorenessMode};
66///
67/// // Directed 3-cycle 0→1→2→0: each vertex has out-degree 1 (and
68/// // in-degree 1). Out-cores → all 1; in-cores → all 1. As undirected
69/// // (every vertex degree 2 in a graph with min degree 2) → all 2.
70/// let mut g = Graph::new(3, true).unwrap();
71/// g.add_edge(0, 1).unwrap();
72/// g.add_edge(1, 2).unwrap();
73/// g.add_edge(2, 0).unwrap();
74/// assert_eq!(coreness_with_mode(&g, CorenessMode::Out).unwrap(), vec![1, 1, 1]);
75/// assert_eq!(coreness_with_mode(&g, CorenessMode::In).unwrap(),  vec![1, 1, 1]);
76/// assert_eq!(coreness_with_mode(&g, CorenessMode::All).unwrap(), vec![2, 2, 2]);
77/// ```
78pub fn coreness_with_mode(graph: &Graph, mode: CorenessMode) -> IgraphResult<Vec<u32>> {
79    let n = graph.vcount();
80    let n_us = n as usize;
81    if n_us == 0 {
82        return Ok(Vec::new());
83    }
84
85    // Effective mode: undirected graphs always collapse to All.
86    let eff_mode = if graph.is_directed() {
87        mode
88    } else {
89        CorenessMode::All
90    };
91
92    // Initial cores: degree under the chosen mode.
93    let mut cores = vec![0u32; n_us];
94    let mut max_deg: u32 = 0;
95    for v in 0..n {
96        let d = vertex_degree_in_mode(graph, v, eff_mode)?;
97        cores[v as usize] = d;
98        if d > max_deg {
99            max_deg = d;
100        }
101    }
102
103    let max_deg_us = max_deg as usize;
104    let mut bin = vec![0usize; max_deg_us + 1];
105    for &c in &cores {
106        bin[c as usize] += 1;
107    }
108    let mut start = 0usize;
109    for slot in bin.iter_mut().take(max_deg_us + 1) {
110        let count = *slot;
111        *slot = start;
112        start += count;
113    }
114
115    let mut vert = vec![0u32; n_us];
116    let mut pos = vec![0usize; n_us];
117    let mut bin_cursor = bin.clone();
118    for v in 0..n {
119        let c = cores[v as usize] as usize;
120        let p = bin_cursor[c];
121        pos[v as usize] = p;
122        vert[p] = v;
123        bin_cursor[c] += 1;
124    }
125    drop(bin_cursor);
126
127    // Main loop: when peeling vertex `v`, walk the **reverse-mode**
128    // neighbours so we touch the vertices whose `eff_mode`-degree
129    // depended on `v`. For All, that's every neighbour; for Out, the
130    // in-neighbours (vertices `u` such that `u → v`); for In, the
131    // out-neighbours (vertices `u` such that `v → u`).
132    for i in 0..n_us {
133        let v = vert[i];
134        let neis = peel_neighbors_in_mode(graph, v, eff_mode)?;
135        for u in neis {
136            if cores[u as usize] > cores[v as usize] {
137                let du = cores[u as usize] as usize;
138                let pu = pos[u as usize];
139                let pw = bin[du];
140                let w = vert[pw];
141                if u != w {
142                    pos[u as usize] = pw;
143                    pos[w as usize] = pu;
144                    vert[pu] = w;
145                    vert[pw] = u;
146                }
147                bin[du] += 1;
148                cores[u as usize] -= 1;
149            }
150        }
151    }
152
153    Ok(cores)
154}
155
156fn vertex_degree_in_mode(graph: &Graph, v: u32, mode: CorenessMode) -> IgraphResult<u32> {
157    let raw = match mode {
158        CorenessMode::All => graph.degree(v)?,
159        CorenessMode::Out => graph.out_neighbors_vec(v)?.len(),
160        CorenessMode::In => graph.in_neighbors_vec(v)?.len(),
161    };
162    u32::try_from(raw).map_err(|_| IgraphError::Internal("vertex degree overflows u32"))
163}
164
165fn peel_neighbors_in_mode(graph: &Graph, v: u32, mode: CorenessMode) -> IgraphResult<Vec<u32>> {
166    match mode {
167        // Reverse-mode: `In` peels via out-neighbours, `Out` via
168        // in-neighbours, `All` via the merged neighbour view.
169        CorenessMode::All => graph.neighbors(v),
170        CorenessMode::Out => graph.in_neighbors_vec(v),
171        CorenessMode::In => graph.out_neighbors_vec(v),
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn empty_graph_returns_empty() {
181        let g = Graph::with_vertices(0);
182        assert_eq!(coreness(&g).unwrap(), Vec::<u32>::new());
183    }
184
185    #[test]
186    fn singleton_zero() {
187        let g = Graph::with_vertices(1);
188        assert_eq!(coreness(&g).unwrap(), vec![0]);
189    }
190
191    #[test]
192    fn isolated_vertices_all_zero() {
193        let g = Graph::with_vertices(5);
194        assert_eq!(coreness(&g).unwrap(), vec![0; 5]);
195    }
196
197    #[test]
198    fn single_edge_two_one_cores() {
199        let mut g = Graph::with_vertices(2);
200        g.add_edge(0, 1).unwrap();
201        assert_eq!(coreness(&g).unwrap(), vec![1, 1]);
202    }
203
204    #[test]
205    fn triangle_all_two_cores() {
206        let mut g = Graph::with_vertices(3);
207        g.add_edge(0, 1).unwrap();
208        g.add_edge(1, 2).unwrap();
209        g.add_edge(0, 2).unwrap();
210        assert_eq!(coreness(&g).unwrap(), vec![2, 2, 2]);
211    }
212
213    #[test]
214    fn path_all_one_cores() {
215        // Path 0-1-2-3-4: all vertices belong to the 1-core (degree-1
216        // leaves drop the inner ones to coreness 1 too).
217        let mut g = Graph::with_vertices(5);
218        for i in 0..4 {
219            g.add_edge(i, i + 1).unwrap();
220        }
221        assert_eq!(coreness(&g).unwrap(), vec![1; 5]);
222    }
223
224    #[test]
225    fn star_centre_vs_leaves() {
226        // 4-star: centre 0 is adjacent to leaves 1, 2, 3. The leaves
227        // each have degree 1 → coreness 1. After peeling them, the
228        // centre has nothing left → coreness 1 too.
229        let mut g = Graph::with_vertices(4);
230        g.add_edge(0, 1).unwrap();
231        g.add_edge(0, 2).unwrap();
232        g.add_edge(0, 3).unwrap();
233        assert_eq!(coreness(&g).unwrap(), vec![1, 1, 1, 1]);
234    }
235
236    #[test]
237    fn k4_all_three_cores() {
238        // K4: every vertex has degree 3 in a graph with min degree 3
239        // → coreness 3.
240        let mut g = Graph::with_vertices(4);
241        for u in 0..4 {
242            for v in (u + 1)..4 {
243                g.add_edge(u, v).unwrap();
244            }
245        }
246        assert_eq!(coreness(&g).unwrap(), vec![3, 3, 3, 3]);
247    }
248
249    #[test]
250    fn triangle_with_pendant_mixed_cores() {
251        // Triangle 0-1-2 plus pendant 3 attached to vertex 2:
252        //   - vertex 3 has degree 1 → coreness 1
253        //   - removing 3 leaves a triangle → 0, 1, 2 are coreness 2.
254        let mut g = Graph::with_vertices(4);
255        g.add_edge(0, 1).unwrap();
256        g.add_edge(1, 2).unwrap();
257        g.add_edge(0, 2).unwrap();
258        g.add_edge(2, 3).unwrap();
259        assert_eq!(coreness(&g).unwrap(), vec![2, 2, 2, 1]);
260    }
261
262    #[test]
263    fn two_components_independent() {
264        // Disjoint union of K3 and a single edge: K3 vertices →
265        // coreness 2, edge vertices → coreness 1.
266        let mut g = Graph::with_vertices(5);
267        g.add_edge(0, 1).unwrap();
268        g.add_edge(1, 2).unwrap();
269        g.add_edge(0, 2).unwrap();
270        g.add_edge(3, 4).unwrap();
271        assert_eq!(coreness(&g).unwrap(), vec![2, 2, 2, 1, 1]);
272    }
273
274    #[test]
275    fn self_loop_counts_twice() {
276        // A self-loop contributes 2 to the loop-vertex's degree.
277        // Vertex 0 has self-loop + edge to 1 → degree 3, but vertex 1
278        // has degree 1 → 1-core. Once vertex 1 is peeled, vertex 0 is
279        // alone with the self-loop → degree 2 → still has nowhere to
280        // go (no neighbours other than itself), so its core gets
281        // dragged down to 1 by the algorithm even though the
282        // "structural" self-loop persists. Matches upstream
283        // IGRAPH_LOOPS semantics.
284        let mut g = Graph::with_vertices(2);
285        g.add_edge(0, 0).unwrap();
286        g.add_edge(0, 1).unwrap();
287        let cores = coreness(&g).unwrap();
288        // Vertex 1 must be coreness 1.
289        assert_eq!(cores[1], 1);
290    }
291
292    #[test]
293    fn directed_default_is_undirected_view_for_canonical_entry() {
294        // PR-015b extension: `coreness()` no longer rejects directed
295        // graphs — it delegates to `coreness_with_mode(_, All)` which
296        // counts every edge regardless of direction.
297        let mut g = Graph::new(3, true).unwrap();
298        g.add_edge(0, 1).unwrap();
299        g.add_edge(1, 2).unwrap();
300        g.add_edge(2, 0).unwrap();
301        // Treated as undirected, this is K3 → 2-core for every vertex.
302        assert_eq!(coreness(&g).unwrap(), vec![2, 2, 2]);
303    }
304
305    // ----- ALGO-PR-015b: directed in/out cores -----
306
307    #[test]
308    fn directed_3_cycle_in_out_modes_match() {
309        let mut g = Graph::new(3, true).unwrap();
310        g.add_edge(0, 1).unwrap();
311        g.add_edge(1, 2).unwrap();
312        g.add_edge(2, 0).unwrap();
313        assert_eq!(
314            coreness_with_mode(&g, CorenessMode::Out).unwrap(),
315            vec![1, 1, 1]
316        );
317        assert_eq!(
318            coreness_with_mode(&g, CorenessMode::In).unwrap(),
319            vec![1, 1, 1]
320        );
321    }
322
323    #[test]
324    fn directed_star_out_mode_drains_to_zero() {
325        // Directed star: 0 → 1, 0 → 2, 0 → 3. Out-degrees: [3, 0, 0, 0].
326        // After peeling 1, 2, 3 (out-degree 0 → 0-core), vertex 0's
327        // out-degree falls to 0 (since each peel decrements an in-edge,
328        // not out — wait, peeling target u in OUT mode looks at u's
329        // IN-neighbours; these are vertices that pointed AT u. For
330        // each such v, decrement v's OUT-degree). Vertices 1, 2, 3 have
331        // in-edges from 0, so peeling 1 decrements 0's core to 2,
332        // peeling 2 → 1, peeling 3 → 0. Final out-cores = [0, 0, 0, 0].
333        let mut g = Graph::new(4, true).unwrap();
334        g.add_edge(0, 1).unwrap();
335        g.add_edge(0, 2).unwrap();
336        g.add_edge(0, 3).unwrap();
337        assert_eq!(
338            coreness_with_mode(&g, CorenessMode::Out).unwrap(),
339            vec![0, 0, 0, 0]
340        );
341    }
342
343    #[test]
344    fn directed_star_in_mode_drains_to_zero() {
345        // Same directed star (0 → 1, 0 → 2, 0 → 3). In-degrees:
346        // [0, 1, 1, 1]. Vertex 0 starts at 0-core. Peeling 0 looks at
347        // 0's OUT-neighbours (1, 2, 3) and decrements each of their
348        // in-cores to 0. Final in-cores = [0, 0, 0, 0].
349        let mut g = Graph::new(4, true).unwrap();
350        g.add_edge(0, 1).unwrap();
351        g.add_edge(0, 2).unwrap();
352        g.add_edge(0, 3).unwrap();
353        assert_eq!(
354            coreness_with_mode(&g, CorenessMode::In).unwrap(),
355            vec![0, 0, 0, 0]
356        );
357    }
358
359    #[test]
360    fn directed_complete_3_all_one_in_one_out() {
361        // Directed K3 in both directions: 6 edges, every vertex has
362        // in-degree 2 and out-degree 2. After peeling, all stay at 2.
363        let mut g = Graph::new(3, true).unwrap();
364        for &(u, v) in &[(0u32, 1), (1, 0), (1, 2), (2, 1), (0, 2), (2, 0)] {
365            g.add_edge(u, v).unwrap();
366        }
367        assert_eq!(
368            coreness_with_mode(&g, CorenessMode::Out).unwrap(),
369            vec![2, 2, 2]
370        );
371        assert_eq!(
372            coreness_with_mode(&g, CorenessMode::In).unwrap(),
373            vec![2, 2, 2]
374        );
375        // ALL mode counts each edge once → 4 each → 4-core.
376        assert_eq!(
377            coreness_with_mode(&g, CorenessMode::All).unwrap(),
378            vec![4, 4, 4]
379        );
380    }
381
382    #[test]
383    fn undirected_modes_all_agree() {
384        // On undirected graphs every mode collapses to All.
385        let mut g = Graph::with_vertices(3);
386        g.add_edge(0, 1).unwrap();
387        g.add_edge(1, 2).unwrap();
388        g.add_edge(0, 2).unwrap();
389        let a = coreness_with_mode(&g, CorenessMode::All).unwrap();
390        let i = coreness_with_mode(&g, CorenessMode::In).unwrap();
391        let o = coreness_with_mode(&g, CorenessMode::Out).unwrap();
392        assert_eq!(a, i);
393        assert_eq!(a, o);
394        assert_eq!(a, vec![2, 2, 2]);
395    }
396
397    #[test]
398    fn directed_chain_out_mode_descends() {
399        // Directed chain 0 → 1 → 2 → 3. Out-degrees: [1, 1, 1, 0].
400        // Peel vertex 3 (out-deg 0) — its in-neighbour 2's out-core
401        // stays 1 (peeled vertex 3 has cores[3]=0, so cores[2]>cores[3]
402        // → decrement 2's out-core to 0). Continuing: 2 → peel decrements 1 to 0; 1 → peels 0.
403        // Final: [0, 0, 0, 0].
404        let mut g = Graph::new(4, true).unwrap();
405        g.add_edge(0, 1).unwrap();
406        g.add_edge(1, 2).unwrap();
407        g.add_edge(2, 3).unwrap();
408        assert_eq!(
409            coreness_with_mode(&g, CorenessMode::Out).unwrap(),
410            vec![0, 0, 0, 0]
411        );
412    }
413
414    #[test]
415    fn coreness_bounded_by_max_degree() {
416        // Property: for every vertex, coreness(v) ≤ degree(v).
417        let mut g = Graph::with_vertices(6);
418        // Petersen-fragment style irregular graph.
419        for &(u, v) in &[(0u32, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 5), (5, 3)] {
420            g.add_edge(u, v).unwrap();
421        }
422        let cores = coreness(&g).unwrap();
423        for v in 0..g.vcount() {
424            let d = u32::try_from(g.degree(v).unwrap()).unwrap();
425            assert!(
426                cores[v as usize] <= d,
427                "vertex {}: coreness {} exceeds degree {}",
428                v,
429                cores[v as usize],
430                d
431            );
432        }
433    }
434}