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}