Skip to main content

rust_igraph/algorithms/operators/
bipartite_projection_size.rs

1//! Bipartite projection size estimation (ALGO-OP-025).
2//!
3//! Counterpart of `igraph_bipartite_projection_size()` from
4//! `references/igraph/src/misc/bipartite.c`.
5//!
6//! Computes the number of vertices and edges in both projections of a
7//! bipartite graph without constructing the projected graphs. Useful
8//! for memory estimation before calling [`super::bipartite_projection`].
9
10use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
11
12/// Sizes of both bipartite projections.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct BipartiteProjectionSize {
15    /// Number of vertices in the projection of type-0 (false) vertices.
16    pub vcount1: u32,
17    /// Number of edges in the projection of type-0 (false) vertices.
18    pub ecount1: u32,
19    /// Number of vertices in the projection of type-1 (true) vertices.
20    pub vcount2: u32,
21    /// Number of edges in the projection of type-1 (true) vertices.
22    pub ecount2: u32,
23}
24
25/// Compute sizes of both bipartite projections without building them.
26///
27/// Given a bipartite graph with vertex types specified by `types`,
28/// this function counts the vertices and edges that each projection
29/// would contain if [`super::bipartite_projection`] were called.
30///
31/// Two vertices of the same type are connected in a projection if
32/// they share at least one common neighbor of the other type. This
33/// function counts such pairs efficiently using a marking array.
34///
35/// The graph is treated as undirected regardless of its directedness.
36///
37/// # Arguments
38///
39/// * `graph` — the bipartite input graph.
40/// * `types` — boolean slice of length `vcount()`, giving vertex types.
41///
42/// # Errors
43///
44/// - `InvalidArgument` if `types.len() != vcount()`.
45/// - `InvalidArgument` if a non-bipartite edge is found.
46///
47/// # Examples
48///
49/// ```
50/// use rust_igraph::{Graph, bipartite_projection_size};
51///
52/// // K_{2,3}: types [F, F, T, T, T]
53/// // Edges: 0-2, 0-3, 0-4, 1-2, 1-3, 1-4
54/// let mut g = Graph::with_vertices(5);
55/// g.add_edge(0, 2).unwrap();
56/// g.add_edge(0, 3).unwrap();
57/// g.add_edge(0, 4).unwrap();
58/// g.add_edge(1, 2).unwrap();
59/// g.add_edge(1, 3).unwrap();
60/// g.add_edge(1, 4).unwrap();
61/// let types = [false, false, true, true, true];
62/// let sz = bipartite_projection_size(&g, &types).unwrap();
63/// assert_eq!(sz.vcount1, 2); // type-0 vertices: 0, 1
64/// assert_eq!(sz.ecount1, 1); // edge 0-1 (share 3 common neighbors)
65/// assert_eq!(sz.vcount2, 3); // type-1 vertices: 2, 3, 4
66/// assert_eq!(sz.ecount2, 3); // K3 (all share 2 common neighbors)
67/// ```
68pub fn bipartite_projection_size(
69    graph: &Graph,
70    types: &[bool],
71) -> IgraphResult<BipartiteProjectionSize> {
72    let n = graph.vcount();
73    let n_us = n as usize;
74
75    if types.len() != n_us {
76        return Err(IgraphError::InvalidArgument(format!(
77            "bipartite_projection_size: types length ({}) != vcount ({n})",
78            types.len()
79        )));
80    }
81
82    let adj = build_undirected_adj(graph, n_us)?;
83
84    let mut vc1: u32 = 0;
85    let mut ec1: u32 = 0;
86    let mut vc2: u32 = 0;
87    let mut ec2: u32 = 0;
88
89    // `added[v]` stores `i + 1` when vertex `v` was last "discovered"
90    // as a 2-hop neighbor of vertex `i`, avoiding double-counting.
91    let mut added: Vec<u32> = vec![0; n_us];
92
93    for i in 0..n_us {
94        if types[i] {
95            vc2 = vc2.saturating_add(1);
96        } else {
97            vc1 = vc1.saturating_add(1);
98        }
99
100        #[allow(clippy::cast_possible_truncation)]
101        let marker = (i as u32).wrapping_add(1);
102
103        for &nei in &adj[i] {
104            let nei_us = nei as usize;
105            if types[nei_us] == types[i] {
106                return Err(IgraphError::InvalidArgument(format!(
107                    "bipartite_projection_size: non-bipartite edge found ({i}-{nei})"
108                )));
109            }
110            for &nb2 in &adj[nei_us] {
111                if nb2 as usize <= i {
112                    continue;
113                }
114                if added[nb2 as usize] == marker {
115                    continue;
116                }
117                added[nb2 as usize] = marker;
118                if types[i] {
119                    ec2 = ec2.saturating_add(1);
120                } else {
121                    ec1 = ec1.saturating_add(1);
122                }
123            }
124        }
125    }
126
127    Ok(BipartiteProjectionSize {
128        vcount1: vc1,
129        ecount1: ec1,
130        vcount2: vc2,
131        ecount2: ec2,
132    })
133}
134
135fn build_undirected_adj(graph: &Graph, n_us: usize) -> IgraphResult<Vec<Vec<VertexId>>> {
136    let m =
137        u32::try_from(graph.ecount()).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
138    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n_us];
139    for eid in 0..m {
140        let (from, to) = graph.edge(eid)?;
141        adj[from as usize].push(to);
142        if from != to {
143            adj[to as usize].push(from);
144        }
145    }
146    Ok(adj)
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn k22() {
155        let mut g = Graph::with_vertices(4);
156        g.add_edge(0, 2).unwrap();
157        g.add_edge(0, 3).unwrap();
158        g.add_edge(1, 2).unwrap();
159        g.add_edge(1, 3).unwrap();
160        let types = [false, false, true, true];
161        let sz = bipartite_projection_size(&g, &types).unwrap();
162        assert_eq!(sz.vcount1, 2);
163        assert_eq!(sz.ecount1, 1);
164        assert_eq!(sz.vcount2, 2);
165        assert_eq!(sz.ecount2, 1);
166    }
167
168    #[test]
169    fn k23() {
170        let mut g = Graph::with_vertices(5);
171        g.add_edge(0, 2).unwrap();
172        g.add_edge(0, 3).unwrap();
173        g.add_edge(0, 4).unwrap();
174        g.add_edge(1, 2).unwrap();
175        g.add_edge(1, 3).unwrap();
176        g.add_edge(1, 4).unwrap();
177        let types = [false, false, true, true, true];
178        let sz = bipartite_projection_size(&g, &types).unwrap();
179        assert_eq!(sz.vcount1, 2);
180        assert_eq!(sz.ecount1, 1);
181        assert_eq!(sz.vcount2, 3);
182        assert_eq!(sz.ecount2, 3);
183    }
184
185    #[test]
186    fn star_bipartite() {
187        // Center 0 (F), leaves 1,2,3 (T)
188        let mut g = Graph::with_vertices(4);
189        g.add_edge(0, 1).unwrap();
190        g.add_edge(0, 2).unwrap();
191        g.add_edge(0, 3).unwrap();
192        let types = [false, true, true, true];
193        let sz = bipartite_projection_size(&g, &types).unwrap();
194        assert_eq!(sz.vcount1, 1);
195        assert_eq!(sz.ecount1, 0); // single vertex, no edges
196        assert_eq!(sz.vcount2, 3);
197        assert_eq!(sz.ecount2, 3); // K3
198    }
199
200    #[test]
201    fn no_edges() {
202        let g = Graph::with_vertices(4);
203        let types = [false, false, true, true];
204        let sz = bipartite_projection_size(&g, &types).unwrap();
205        assert_eq!(sz.vcount1, 2);
206        assert_eq!(sz.ecount1, 0);
207        assert_eq!(sz.vcount2, 2);
208        assert_eq!(sz.ecount2, 0);
209    }
210
211    #[test]
212    fn empty_graph() {
213        let g = Graph::with_vertices(0);
214        let types: [bool; 0] = [];
215        let sz = bipartite_projection_size(&g, &types).unwrap();
216        assert_eq!(sz.vcount1, 0);
217        assert_eq!(sz.ecount1, 0);
218        assert_eq!(sz.vcount2, 0);
219        assert_eq!(sz.ecount2, 0);
220    }
221
222    #[test]
223    fn types_mismatch() {
224        let g = Graph::with_vertices(3);
225        let types = [false, true];
226        assert!(bipartite_projection_size(&g, &types).is_err());
227    }
228
229    #[test]
230    fn non_bipartite_edge() {
231        let mut g = Graph::with_vertices(3);
232        g.add_edge(0, 1).unwrap();
233        let types = [false, false, true];
234        assert!(bipartite_projection_size(&g, &types).is_err());
235    }
236
237    #[test]
238    fn path_bipartite() {
239        // Path: 0(F)-1(T)-2(F)-3(T)-4(F)
240        let mut g = Graph::with_vertices(5);
241        g.add_edge(0, 1).unwrap();
242        g.add_edge(1, 2).unwrap();
243        g.add_edge(2, 3).unwrap();
244        g.add_edge(3, 4).unwrap();
245        let types = [false, true, false, true, false];
246        let sz = bipartite_projection_size(&g, &types).unwrap();
247        assert_eq!(sz.vcount1, 3); // 0, 2, 4
248        assert_eq!(sz.ecount1, 2); // 0-2 (via 1) and 2-4 (via 3)
249        assert_eq!(sz.vcount2, 2); // 1, 3
250        assert_eq!(sz.ecount2, 1); // 1-3 (via 2)
251    }
252
253    #[test]
254    fn directed_treated_as_undirected() {
255        let mut g = Graph::new(4, true).unwrap();
256        g.add_edge(0, 2).unwrap();
257        g.add_edge(3, 0).unwrap();
258        g.add_edge(1, 2).unwrap();
259        g.add_edge(3, 1).unwrap();
260        let types = [false, false, true, true];
261        let sz = bipartite_projection_size(&g, &types).unwrap();
262        assert_eq!(sz.vcount1, 2);
263        assert_eq!(sz.ecount1, 1);
264        assert_eq!(sz.vcount2, 2);
265        assert_eq!(sz.ecount2, 1);
266    }
267}