Skip to main content

rust_igraph/algorithms/operators/
bipartite_projection.rs

1//! Bipartite graph projection (ALGO-OP-024).
2//!
3//! Counterpart of `igraph_bipartite_projection()` from
4//! `references/igraph/src/misc/bipartite.c`.
5//!
6//! Projects a bipartite (two-mode) network onto one of its vertex
7//! sets. Two vertices of the same type are connected in the projection
8//! if they share at least one common neighbor of the other type.
9
10use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
11
12/// Result of a bipartite projection.
13#[derive(Debug, Clone)]
14pub struct BipartiteProjection {
15    /// The projected graph (undirected).
16    pub graph: Graph,
17    /// Mapping from projection vertex IDs to original vertex IDs.
18    /// `vertex_map[proj_v]` is the original vertex ID.
19    pub vertex_map: Vec<VertexId>,
20    /// Edge multiplicities: `multiplicity[e]` is the number of common
21    /// neighbors that caused projection edge `e` to exist.
22    pub multiplicity: Vec<u32>,
23}
24
25/// Project a bipartite graph onto one of its vertex types.
26///
27/// Given a bipartite graph with vertex types specified by `types`
28/// (where `types[v]` is `false` for type-0 and `true` for type-1),
29/// this function creates the projection onto the specified type.
30/// Two vertices of the target type are connected in the projection
31/// if they share at least one common neighbor of the other type.
32///
33/// The graph is treated as undirected regardless of its directedness.
34///
35/// # Arguments
36///
37/// * `graph` — the bipartite input graph.
38/// * `types` — boolean slice of length `vcount()`, giving vertex types.
39/// * `project_type` — which type to project onto (`false` = type-0,
40///   `true` = type-1).
41///
42/// # Errors
43///
44/// - `InvalidArgument` if `types.len() != vcount()`.
45/// - `InvalidArgument` if a non-bipartite edge is found (both
46///   endpoints have the same type).
47///
48/// # Examples
49///
50/// ```
51/// use rust_igraph::{Graph, bipartite_projection};
52///
53/// // Bipartite graph: types [F, F, T, T]
54/// // Edges: 0-2, 0-3, 1-2, 1-3
55/// // Projection onto type F (false): 0 and 1 share neighbors 2 and 3.
56/// let mut g = Graph::with_vertices(4);
57/// g.add_edge(0, 2).unwrap();
58/// g.add_edge(0, 3).unwrap();
59/// g.add_edge(1, 2).unwrap();
60/// g.add_edge(1, 3).unwrap();
61/// let types = [false, false, true, true];
62/// let proj = bipartite_projection(&g, &types, false).unwrap();
63/// assert_eq!(proj.graph.vcount(), 2); // vertices 0, 1
64/// assert_eq!(proj.graph.ecount(), 1); // edge 0-1
65/// assert_eq!(proj.multiplicity[0], 2); // shared neighbors: 2 and 3
66/// assert_eq!(proj.vertex_map, vec![0, 1]);
67/// ```
68pub fn bipartite_projection(
69    graph: &Graph,
70    types: &[bool],
71    project_type: bool,
72) -> IgraphResult<BipartiteProjection> {
73    let n = graph.vcount();
74    let n_us = n as usize;
75
76    if types.len() != n_us {
77        return Err(IgraphError::InvalidArgument(format!(
78            "bipartite_projection: types length ({}) != vcount ({n})",
79            types.len()
80        )));
81    }
82
83    // Build undirected adjacency list (ignore directedness).
84    let adj = build_undirected_adj(graph, n_us)?;
85
86    // Identify vertices of the target type and build index mapping.
87    let mut vertex_map: Vec<VertexId> = Vec::new();
88    let mut new_index: Vec<Option<u32>> = vec![None; n_us];
89    for (v, &t) in types.iter().enumerate() {
90        if t == project_type {
91            #[allow(clippy::cast_possible_truncation)]
92            let new_id = vertex_map.len() as u32;
93            new_index[v] = Some(new_id);
94            #[allow(clippy::cast_possible_truncation)]
95            let v_id = v as u32;
96            vertex_map.push(v_id);
97        }
98    }
99
100    let proj_n = vertex_map.len();
101    let mut edges: Vec<(u32, u32)> = Vec::new();
102    let mut multiplicity: Vec<u32> = Vec::new();
103
104    // For each vertex `i` of the target type, find all vertices of
105    // the target type reachable through exactly one intermediate step.
106    // `added[v]` tracks which vertex `i` last "discovered" a link to `v`.
107    let mut added: Vec<u32> = vec![0; n_us];
108    let mut mult_count: Vec<u32> = vec![0; n_us];
109
110    for (idx, &orig_i) in vertex_map.iter().enumerate() {
111        let i_us = orig_i as usize;
112        #[allow(clippy::cast_possible_truncation)]
113        let marker = (idx as u32) + 1; // non-zero marker for vertex idx
114
115        for &nei in &adj[i_us] {
116            let nei_us = nei as usize;
117            if types[nei_us] == project_type {
118                return Err(IgraphError::InvalidArgument(format!(
119                    "bipartite_projection: non-bipartite edge found ({orig_i}-{nei})"
120                )));
121            }
122            // nei is of the other type. Look at nei's neighbors.
123            for &nei2 in &adj[nei_us] {
124                let nb2_idx = nei2 as usize;
125                if nei2 <= orig_i {
126                    continue; // avoid duplicates and self-loops
127                }
128                if types[nb2_idx] != project_type {
129                    continue; // skip vertices of the other type
130                }
131                if added[nb2_idx] == marker {
132                    mult_count[nb2_idx] += 1;
133                    continue;
134                }
135                added[nb2_idx] = marker;
136                mult_count[nb2_idx] = 1;
137            }
138        }
139
140        // Collect edges from this vertex.
141        for &nei in &adj[i_us] {
142            let nei_us = nei as usize;
143            for &nei2 in &adj[nei_us] {
144                let nb2_idx = nei2 as usize;
145                if nei2 <= orig_i || types[nb2_idx] != project_type {
146                    continue;
147                }
148                if added[nb2_idx] == marker {
149                    // First time seeing this in the collection pass
150                    #[allow(clippy::cast_possible_truncation)]
151                    let new_i = idx as u32;
152                    let new_j = new_index[nb2_idx].unwrap_or(0);
153                    edges.push((new_i, new_j));
154                    multiplicity.push(mult_count[nb2_idx]);
155                    added[nb2_idx] = 0; // clear so we don't double-add
156                }
157            }
158        }
159    }
160
161    // Build the projection graph.
162    #[allow(clippy::cast_possible_truncation)]
163    let proj_n_u32 = proj_n as u32;
164    let mut proj_graph = Graph::with_vertices(proj_n_u32);
165    for &(u, v) in &edges {
166        proj_graph.add_edge(u, v)?;
167    }
168
169    Ok(BipartiteProjection {
170        graph: proj_graph,
171        vertex_map,
172        multiplicity,
173    })
174}
175
176fn build_undirected_adj(graph: &Graph, n_us: usize) -> IgraphResult<Vec<Vec<VertexId>>> {
177    let m =
178        u32::try_from(graph.ecount()).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
179    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n_us];
180    for eid in 0..m {
181        let (from, to) = graph.edge(eid)?;
182        adj[from as usize].push(to);
183        if from != to {
184            adj[to as usize].push(from);
185        }
186    }
187    Ok(adj)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn simple_complete_bipartite() {
196        // K_{2,2}: types [F, F, T, T], edges: 0-2, 0-3, 1-2, 1-3
197        let mut g = Graph::with_vertices(4);
198        g.add_edge(0, 2).unwrap();
199        g.add_edge(0, 3).unwrap();
200        g.add_edge(1, 2).unwrap();
201        g.add_edge(1, 3).unwrap();
202        let types = [false, false, true, true];
203
204        let p0 = bipartite_projection(&g, &types, false).unwrap();
205        assert_eq!(p0.graph.vcount(), 2);
206        assert_eq!(p0.graph.ecount(), 1);
207        assert_eq!(p0.multiplicity[0], 2);
208        assert_eq!(p0.vertex_map, vec![0, 1]);
209
210        let p1 = bipartite_projection(&g, &types, true).unwrap();
211        assert_eq!(p1.graph.vcount(), 2);
212        assert_eq!(p1.graph.ecount(), 1);
213        assert_eq!(p1.multiplicity[0], 2);
214        assert_eq!(p1.vertex_map, vec![2, 3]);
215    }
216
217    #[test]
218    fn star_bipartite() {
219        // Star: center 0 (type F), leaves 1,2,3 (type T)
220        // Projection onto T: all leaves share center → complete graph K3
221        let mut g = Graph::with_vertices(4);
222        g.add_edge(0, 1).unwrap();
223        g.add_edge(0, 2).unwrap();
224        g.add_edge(0, 3).unwrap();
225        let types = [false, true, true, true];
226
227        let p = bipartite_projection(&g, &types, true).unwrap();
228        assert_eq!(p.graph.vcount(), 3);
229        assert_eq!(p.graph.ecount(), 3); // K3
230        for &m in &p.multiplicity {
231            assert_eq!(m, 1); // only one shared neighbor (vertex 0)
232        }
233    }
234
235    #[test]
236    fn no_edges() {
237        // Bipartite with no edges: projection has vertices but no edges.
238        let g = Graph::with_vertices(4);
239        let types = [false, false, true, true];
240        let p = bipartite_projection(&g, &types, false).unwrap();
241        assert_eq!(p.graph.vcount(), 2);
242        assert_eq!(p.graph.ecount(), 0);
243        assert!(p.multiplicity.is_empty());
244    }
245
246    #[test]
247    fn single_type() {
248        // All vertices are type F with edges between them → error (non-bipartite).
249        let mut g = Graph::with_vertices(3);
250        g.add_edge(0, 1).unwrap();
251        let types = [false, false, false];
252        assert!(bipartite_projection(&g, &types, false).is_err());
253    }
254
255    #[test]
256    fn non_bipartite_edge_error() {
257        // Mixed: 0(F)-1(F) is non-bipartite edge
258        let mut g = Graph::with_vertices(3);
259        g.add_edge(0, 1).unwrap();
260        g.add_edge(0, 2).unwrap();
261        let types = [false, false, true];
262        assert!(bipartite_projection(&g, &types, false).is_err());
263    }
264
265    #[test]
266    fn types_length_mismatch() {
267        let g = Graph::with_vertices(3);
268        let types = [false, true];
269        assert!(bipartite_projection(&g, &types, false).is_err());
270    }
271
272    #[test]
273    fn path_bipartite() {
274        // Path: 0(F)-1(T)-2(F)-3(T)-4(F)
275        // Projection onto F: 0 and 2 share neighbor 1, 2 and 4 share neighbor 3
276        let mut g = Graph::with_vertices(5);
277        g.add_edge(0, 1).unwrap();
278        g.add_edge(1, 2).unwrap();
279        g.add_edge(2, 3).unwrap();
280        g.add_edge(3, 4).unwrap();
281        let types = [false, true, false, true, false];
282
283        let p = bipartite_projection(&g, &types, false).unwrap();
284        assert_eq!(p.graph.vcount(), 3); // vertices 0, 2, 4
285        assert_eq!(p.graph.ecount(), 2); // 0-2 and 2-4
286        assert_eq!(p.vertex_map, vec![0, 2, 4]);
287        for &m in &p.multiplicity {
288            assert_eq!(m, 1);
289        }
290    }
291
292    #[test]
293    fn multiplicity_multiple() {
294        // 0(F) connected to 2(T) and 3(T), 1(F) connected to 2(T) and 3(T) and 4(T)
295        // Projection onto F: 0 and 1 share TWO common neighbors (2 and 3)
296        let mut g = Graph::with_vertices(5);
297        g.add_edge(0, 2).unwrap();
298        g.add_edge(0, 3).unwrap();
299        g.add_edge(1, 2).unwrap();
300        g.add_edge(1, 3).unwrap();
301        g.add_edge(1, 4).unwrap();
302        let types = [false, false, true, true, true];
303
304        let p = bipartite_projection(&g, &types, false).unwrap();
305        assert_eq!(p.graph.vcount(), 2);
306        assert_eq!(p.graph.ecount(), 1);
307        assert_eq!(p.multiplicity[0], 2); // shared: 2 and 3
308    }
309
310    #[test]
311    fn empty_graph() {
312        let g = Graph::with_vertices(0);
313        let types: [bool; 0] = [];
314        let p = bipartite_projection(&g, &types, false).unwrap();
315        assert_eq!(p.graph.vcount(), 0);
316        assert_eq!(p.graph.ecount(), 0);
317    }
318
319    #[test]
320    fn directed_treated_as_undirected() {
321        // Directed edges should still work (treated as undirected)
322        let mut g = Graph::new(4, true).unwrap();
323        g.add_edge(0, 2).unwrap();
324        g.add_edge(3, 0).unwrap();
325        g.add_edge(1, 2).unwrap();
326        g.add_edge(3, 1).unwrap();
327        let types = [false, false, true, true];
328
329        let p = bipartite_projection(&g, &types, false).unwrap();
330        assert_eq!(p.graph.vcount(), 2);
331        assert_eq!(p.graph.ecount(), 1);
332        assert_eq!(p.multiplicity[0], 2);
333    }
334}