Skip to main content

rust_igraph/algorithms/properties/
trussness.rs

1//! Per-edge trussness via k-truss decomposition (ALGO-PR-036).
2//!
3//! Counterpart of `igraph_trussness()` from
4//! `references/igraph/src/centrality/truss.cpp` (Wang-Cheng 2012).
5//!
6//! A *k-truss* is a maximal subgraph in which every edge participates
7//! in at least k-2 triangles. The *trussness* of an edge is the
8//! largest k such that the edge belongs to a k-truss.
9//!
10//! Edges that do not participate in any triangle have trussness 2
11//! (by convention they belong to the 2-truss, which is the entire
12//! graph). Self-loops also receive trussness 2.
13
14use std::collections::HashSet;
15
16use crate::algorithms::properties::multiplicity::has_multiple;
17use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
18
19/// Compute the trussness of every edge in an undirected graph.
20///
21/// Returns a `Vec<u32>` of length `graph.ecount()` where entry `i`
22/// is the trussness of edge `i`. Multigraphs are rejected with an
23/// error; self-loops are accepted and receive trussness 2.
24///
25/// Counterpart of `igraph_trussness()` from
26/// `references/igraph/src/centrality/truss.cpp`.
27///
28/// # Errors
29///
30/// - `IgraphError::Unsupported` if the graph has parallel edges
31///   (multigraph).
32/// - `IgraphError::InvalidArgument` if the graph is directed.
33///
34/// # Examples
35///
36/// ```
37/// use rust_igraph::{Graph, trussness};
38///
39/// // K4 (complete graph on 4 vertices): every edge is in 2
40/// // triangles → trussness = 4 for all 6 edges.
41/// let mut g = Graph::with_vertices(4);
42/// for u in 0..4u32 {
43///     for v in (u + 1)..4 {
44///         g.add_edge(u, v).unwrap();
45///     }
46/// }
47/// let t = trussness(&g).unwrap();
48/// assert!(t.iter().all(|&x| x == 4));
49///
50/// // Triangle (K3): every edge is in 1 triangle → trussness = 3.
51/// let mut g = Graph::with_vertices(3);
52/// for (u, v) in [(0, 1), (0, 2), (1, 2)] {
53///     g.add_edge(u, v).unwrap();
54/// }
55/// let t = trussness(&g).unwrap();
56/// assert!(t.iter().all(|&x| x == 3));
57///
58/// // Path 0-1-2: no triangles → trussness = 2 for both edges.
59/// let mut g = Graph::with_vertices(3);
60/// g.add_edge(0, 1).unwrap();
61/// g.add_edge(1, 2).unwrap();
62/// let t = trussness(&g).unwrap();
63/// assert!(t.iter().all(|&x| x == 2));
64/// ```
65pub fn trussness(graph: &Graph) -> IgraphResult<Vec<u32>> {
66    if graph.is_directed() {
67        return Err(IgraphError::InvalidArgument(
68            "trussness is only defined for undirected graphs".into(),
69        ));
70    }
71
72    let edge_count = graph.ecount();
73    if edge_count == 0 {
74        return Ok(Vec::new());
75    }
76
77    if has_multiple(graph)? {
78        return Err(IgraphError::Unsupported(
79            "trussness does not support multigraphs",
80        ));
81    }
82
83    let adj = build_simple_adj(graph)?;
84    let mut support = compute_support(graph, &adj)?;
85    peel_trussness(graph, &adj, &mut support)
86}
87
88fn build_simple_adj(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
89    let vert_count = graph.vcount();
90    let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(vert_count as usize);
91    for vid in 0..vert_count {
92        let raw = graph.neighbors(vid)?;
93        let mut simple: Vec<VertexId> = raw.into_iter().filter(|&nb| nb != vid).collect();
94        simple.sort_unstable();
95        simple.dedup();
96        adj.push(simple);
97    }
98    Ok(adj)
99}
100
101fn compute_support(graph: &Graph, adj: &[Vec<VertexId>]) -> IgraphResult<Vec<u32>> {
102    let vert_count = graph.vcount();
103    let mut support: Vec<u32> = vec![0; graph.ecount()];
104    let mut mark: Vec<u32> = vec![0; vert_count as usize];
105
106    for v1 in 0..vert_count {
107        let nei1 = &adj[v1 as usize];
108        if nei1.len() < 2 {
109            continue;
110        }
111        let v1_mark = v1
112            .checked_add(1)
113            .ok_or(IgraphError::Internal("vertex id overflow"))?;
114
115        for &v2 in nei1 {
116            if v2 >= v1 {
117                break;
118            }
119            mark[v2 as usize] = v1_mark;
120        }
121
122        for &v2 in nei1 {
123            if v2 >= v1 {
124                break;
125            }
126            for &v3 in &adj[v2 as usize] {
127                if v3 >= v2 {
128                    break;
129                }
130                if mark[v3 as usize] == v1_mark {
131                    if let Some(eid) = graph.find_eid(v1, v2)? {
132                        support[eid as usize] = support[eid as usize].saturating_add(1);
133                    }
134                    if let Some(eid) = graph.find_eid(v1, v3)? {
135                        support[eid as usize] = support[eid as usize].saturating_add(1);
136                    }
137                    if let Some(eid) = graph.find_eid(v2, v3)? {
138                        support[eid as usize] = support[eid as usize].saturating_add(1);
139                    }
140                }
141            }
142        }
143    }
144    Ok(support)
145}
146
147#[allow(clippy::similar_names)]
148fn peel_trussness(
149    graph: &Graph,
150    adj: &[Vec<VertexId>],
151    support: &mut [u32],
152) -> IgraphResult<Vec<u32>> {
153    let edge_count = support.len();
154    let max_support = support.iter().copied().max().unwrap_or(0);
155    let bucket_count = (max_support as usize).saturating_add(1);
156
157    let mut truss: Vec<u32> = vec![0; edge_count];
158    let mut completed: Vec<bool> = vec![false; edge_count];
159    let mut buckets: Vec<HashSet<u32>> = vec![HashSet::new(); bucket_count];
160
161    for eid in 0..edge_count {
162        let eid_u32 = u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow"))?;
163        buckets[support[eid] as usize].insert(eid_u32);
164    }
165
166    for level in 0..bucket_count {
167        let level_u32 =
168            u32::try_from(level).map_err(|_| IgraphError::Internal("level overflow"))?;
169
170        while let Some(&eid) = buckets[level].iter().next() {
171            buckets[level].remove(&eid);
172            completed[eid as usize] = true;
173            truss[eid as usize] = level_u32
174                .checked_add(2)
175                .ok_or(IgraphError::Internal("trussness overflow"))?;
176
177            let (src, dst) = graph.edge(eid)?;
178
179            if src == dst {
180                continue;
181            }
182
183            let common = sorted_intersection(&adj[src as usize], &adj[dst as usize]);
184
185            for nb in common {
186                let Some(edge_sn) = graph.find_eid(src, nb)? else {
187                    continue;
188                };
189                let Some(edge_dn) = graph.find_eid(dst, nb)? else {
190                    continue;
191                };
192
193                if completed[edge_sn as usize] || completed[edge_dn as usize] {
194                    continue;
195                }
196
197                for &edge in &[edge_sn, edge_dn] {
198                    let old_sup = support[edge as usize];
199                    if old_sup > level_u32 {
200                        buckets[old_sup as usize].remove(&edge);
201                        let new_sup = old_sup.saturating_sub(1);
202                        support[edge as usize] = new_sup;
203                        let target = (new_sup as usize).max(level);
204                        buckets[target].insert(edge);
205                    }
206                }
207            }
208        }
209    }
210
211    Ok(truss)
212}
213
214fn sorted_intersection(a: &[VertexId], b: &[VertexId]) -> Vec<VertexId> {
215    let mut result = Vec::new();
216    let (mut ia, mut ib) = (0, 0);
217    while ia < a.len() && ib < b.len() {
218        match a[ia].cmp(&b[ib]) {
219            std::cmp::Ordering::Less => ia += 1,
220            std::cmp::Ordering::Greater => ib += 1,
221            std::cmp::Ordering::Equal => {
222                result.push(a[ia]);
223                ia += 1;
224                ib += 1;
225            }
226        }
227    }
228    result
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    fn make_undirected(n: u32, edges: &[(u32, u32)]) -> Graph {
236        let mut g = Graph::with_vertices(n);
237        for &(u, v) in edges {
238            g.add_edge(u, v).unwrap();
239        }
240        g
241    }
242
243    #[test]
244    fn empty_graph() {
245        let g = Graph::with_vertices(0);
246        let t = trussness(&g).unwrap();
247        assert!(t.is_empty());
248    }
249
250    #[test]
251    fn singleton_no_edges() {
252        let g = Graph::with_vertices(1);
253        let t = trussness(&g).unwrap();
254        assert!(t.is_empty());
255    }
256
257    #[test]
258    fn no_edges() {
259        let g = Graph::with_vertices(10);
260        let t = trussness(&g).unwrap();
261        assert!(t.is_empty());
262    }
263
264    #[test]
265    fn path_graph() {
266        let g = make_undirected(3, &[(0, 1), (1, 2)]);
267        let t = trussness(&g).unwrap();
268        assert_eq!(t, vec![2, 2]);
269    }
270
271    #[test]
272    fn triangle_k3() {
273        let g = make_undirected(3, &[(0, 1), (0, 2), (1, 2)]);
274        let t = trussness(&g).unwrap();
275        assert_eq!(t, vec![3, 3, 3]);
276    }
277
278    #[test]
279    fn k4_complete() {
280        let g = make_undirected(4, &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
281        let t = trussness(&g).unwrap();
282        assert!(t.iter().all(|&x| x == 4), "K4 trussness: {t:?}");
283    }
284
285    #[test]
286    fn k5_complete() {
287        let g = make_undirected(
288            5,
289            &[
290                (0, 1),
291                (0, 2),
292                (0, 3),
293                (0, 4),
294                (1, 2),
295                (1, 3),
296                (1, 4),
297                (2, 3),
298                (2, 4),
299                (3, 4),
300            ],
301        );
302        let t = trussness(&g).unwrap();
303        assert!(t.iter().all(|&x| x == 5), "K5 trussness: {t:?}");
304    }
305
306    #[test]
307    fn igraph_c_test_graph() {
308        let g = make_undirected(
309            12,
310            &[
311                (0, 1),
312                (0, 2),
313                (0, 3),
314                (0, 4),
315                (1, 2),
316                (1, 3),
317                (1, 4),
318                (2, 3),
319                (2, 4),
320                (3, 4),
321                (3, 6),
322                (3, 11),
323                (4, 5),
324                (4, 6),
325                (5, 6),
326                (5, 7),
327                (5, 8),
328                (5, 9),
329                (6, 7),
330                (6, 10),
331                (6, 11),
332                (7, 8),
333                (7, 9),
334                (8, 9),
335                (8, 10),
336            ],
337        );
338        let t = trussness(&g).unwrap();
339        #[rustfmt::skip]
340        let expected = vec![
341            5, 5, 5, 5,         // 0-1, 0-2, 0-3, 0-4
342            5, 5, 5, 5, 5, 5,   // 1-2, 1-3, 1-4, 2-3, 2-4, 3-4
343            3, 3,                // 3-6, 3-11
344            3, 3, 3,             // 4-5, 4-6, 5-6
345            4, 4, 4,             // 5-7, 5-8, 5-9
346            3, 2, 3,             // 6-7, 6-10, 6-11
347            4, 4, 4, 2,          // 7-8, 7-9, 8-9, 8-10
348        ];
349        assert_eq!(t, expected);
350    }
351
352    #[test]
353    fn graph_with_self_loops() {
354        let mut g = make_undirected(3, &[(0, 1), (0, 2), (1, 2)]);
355        g.add_edge(0, 0).unwrap();
356        g.add_edge(2, 2).unwrap();
357        let t = trussness(&g).unwrap();
358        assert_eq!(t, vec![3, 3, 3, 2, 2]);
359    }
360
361    #[test]
362    fn directed_graph_rejected() {
363        let g = Graph::new(3, true).unwrap();
364        let err = trussness(&g).unwrap_err();
365        assert!(matches!(err, IgraphError::InvalidArgument(_)));
366    }
367
368    #[test]
369    fn multigraph_rejected() {
370        let mut g = Graph::with_vertices(3);
371        g.add_edge(0, 1).unwrap();
372        g.add_edge(0, 1).unwrap();
373        g.add_edge(1, 2).unwrap();
374        let err = trussness(&g).unwrap_err();
375        assert!(matches!(err, IgraphError::Unsupported(_)));
376    }
377
378    #[test]
379    fn star_graph() {
380        let g = make_undirected(5, &[(0, 1), (0, 2), (0, 3), (0, 4)]);
381        let t = trussness(&g).unwrap();
382        assert!(t.iter().all(|&x| x == 2));
383    }
384
385    #[test]
386    fn two_triangles_sharing_edge() {
387        let g = make_undirected(4, &[(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]);
388        let t = trussness(&g).unwrap();
389        assert!(
390            t.iter().all(|&x| x == 3),
391            "two triangles sharing edge: {t:?}"
392        );
393    }
394
395    #[test]
396    fn single_edge() {
397        let g = make_undirected(2, &[(0, 1)]);
398        let t = trussness(&g).unwrap();
399        assert_eq!(t, vec![2]);
400    }
401
402    #[test]
403    fn disconnected_triangles() {
404        let g = make_undirected(6, &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5)]);
405        let t = trussness(&g).unwrap();
406        assert!(t.iter().all(|&x| x == 3));
407    }
408
409    #[test]
410    fn bridge_between_triangles() {
411        let g = make_undirected(6, &[(0, 1), (0, 2), (1, 2), (2, 3), (3, 4), (3, 5), (4, 5)]);
412        let t = trussness(&g).unwrap();
413        assert_eq!(t[0], 3); // 0-1
414        assert_eq!(t[1], 3); // 0-2
415        assert_eq!(t[2], 3); // 1-2
416        assert_eq!(t[3], 2); // 2-3 (bridge)
417        assert_eq!(t[4], 3); // 3-4
418        assert_eq!(t[5], 3); // 3-5
419        assert_eq!(t[6], 3); // 4-5
420    }
421}
422
423#[cfg(all(test, feature = "proptest-harness"))]
424mod proptest_tests {
425    use super::*;
426    use proptest::prelude::*;
427
428    fn arb_simple_undirected(max_n: u32, max_e: usize) -> impl Strategy<Value = Graph> {
429        (3..=max_n).prop_flat_map(move |n| {
430            let possible = ((n as u64) * (n as u64 - 1) / 2) as usize;
431            let cap = max_e.min(possible);
432            proptest::collection::hash_set((0..n, 0..n), 0..=cap).prop_map(move |pairs| {
433                let mut g = Graph::with_vertices(n);
434                for (a, b) in pairs {
435                    if a != b {
436                        let (lo, hi) = if a < b { (a, b) } else { (b, a) };
437                        if g.find_eid(lo, hi).ok().flatten().is_none() {
438                            g.add_edge(lo, hi).unwrap();
439                        }
440                    }
441                }
442                g
443            })
444        })
445    }
446
447    proptest! {
448        #[test]
449        fn trussness_at_least_two(g in arb_simple_undirected(20, 40)) {
450            let t = trussness(&g).unwrap();
451            for &val in &t {
452                prop_assert!(val >= 2, "trussness must be >= 2, got {val}");
453            }
454        }
455
456        #[test]
457        fn trussness_k_complete(n in 3u32..=8) {
458            let mut g = Graph::with_vertices(n);
459            for u in 0..n {
460                for v in (u + 1)..n {
461                    g.add_edge(u, v).unwrap();
462                }
463            }
464            let t = trussness(&g).unwrap();
465            for &val in &t {
466                prop_assert_eq!(val, n, "K{} trussness should be {}, got {}", n, n, val);
467            }
468        }
469
470        #[test]
471        fn trussness_bounded_by_graph_size(g in arb_simple_undirected(15, 30)) {
472            let t = trussness(&g).unwrap();
473            let n = g.vcount();
474            for &val in &t {
475                prop_assert!(val <= n, "trussness {val} exceeds vertex count {n}");
476            }
477        }
478    }
479}