Skip to main content

rust_igraph/algorithms/properties/
get_biadjacency.rs

1//! Biadjacency matrix extraction (ALGO-PR-043).
2//!
3//! Counterpart of `igraph_get_biadjacency()` in
4//! `references/igraph/src/misc/bipartite.c:874-956`.
5//!
6//! Extracts the biadjacency matrix from a bipartite graph given
7//! a per-vertex type assignment.
8
9use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
10
11/// Result of [`get_biadjacency_matrix`].
12#[derive(Debug, Clone)]
13pub struct GetBiadjacencyResult {
14    /// Row-major `n1 × n2` biadjacency matrix.
15    /// `matrix[i][j]` is the number of edges between row-vertex `i`
16    /// and column-vertex `j`.
17    pub matrix: Vec<Vec<f64>>,
18    /// Vertex IDs corresponding to rows (partition with `types[v] == false`).
19    pub row_ids: Vec<VertexId>,
20    /// Vertex IDs corresponding to columns (partition with `types[v] == true`).
21    pub col_ids: Vec<VertexId>,
22}
23
24/// Extract the biadjacency matrix from a bipartite graph.
25///
26/// Given a graph and a per-vertex type vector (`false` = row partition,
27/// `true` = column partition), returns the `n1 × n2` biadjacency
28/// matrix where entry `(i, j)` counts the number of edges between
29/// row-vertex `i` and column-vertex `j`.
30///
31/// Edges within the same partition are silently ignored (as in the C
32/// implementation).
33///
34/// # Parameters
35///
36/// * `graph` — the input graph (directed or undirected).
37/// * `types` — per-vertex boolean: `false` = row (partition 1),
38///   `true` = column (partition 2). Must have length equal to `graph.vcount()`.
39///
40/// # Errors
41///
42/// * [`IgraphError::InvalidArgument`] — if `types.len() != graph.vcount()`.
43///
44/// # Examples
45///
46/// ```
47/// use rust_igraph::{biadjacency, get_biadjacency_matrix, BipartiteMode};
48///
49/// let matrix: &[&[f64]] = &[
50///     &[1.0, 0.0, 2.0],
51///     &[0.0, 1.0, 0.0],
52/// ];
53/// let r = biadjacency(matrix, false, BipartiteMode::All, true).unwrap();
54/// let extracted = get_biadjacency_matrix(&r.graph, &r.types).unwrap();
55/// assert_eq!(extracted.matrix.len(), 2); // 2 rows
56/// assert_eq!(extracted.matrix[0].len(), 3); // 3 columns
57/// assert_eq!(extracted.matrix[0][0], 1.0);
58/// assert_eq!(extracted.matrix[0][2], 2.0);
59/// assert_eq!(extracted.matrix[1][1], 1.0);
60/// ```
61pub fn get_biadjacency_matrix(graph: &Graph, types: &[bool]) -> IgraphResult<GetBiadjacencyResult> {
62    let vcount = graph.vcount();
63
64    if types.len() != vcount as usize {
65        return Err(IgraphError::InvalidArgument(format!(
66            "get_biadjacency_matrix: types length ({}) != vcount ({})",
67            types.len(),
68            vcount
69        )));
70    }
71
72    // Count partition sizes and build mapping from vertex ID → row/col index.
73    let mut n1: usize = 0;
74    let mut n2: usize = 0;
75    let mut row_ids: Vec<VertexId> = Vec::new();
76    let mut col_ids: Vec<VertexId> = Vec::new();
77    // Maps vertex ID → index within its partition.
78    let mut vertex_to_idx: Vec<u32> = vec![0; vcount as usize];
79
80    for v in 0..vcount {
81        if types[v as usize] {
82            #[allow(clippy::cast_possible_truncation)]
83            let idx = n2 as u32;
84            vertex_to_idx[v as usize] = idx;
85            col_ids.push(v);
86            n2 += 1;
87        } else {
88            #[allow(clippy::cast_possible_truncation)]
89            let idx = n1 as u32;
90            vertex_to_idx[v as usize] = idx;
91            row_ids.push(v);
92            n1 += 1;
93        }
94    }
95
96    // Initialize n1 × n2 matrix of zeros.
97    let mut matrix = vec![vec![0.0_f64; n2]; n1];
98
99    // Iterate over all edges.
100    let ecount = graph.ecount();
101    for eid in 0..ecount {
102        #[allow(clippy::cast_possible_truncation)]
103        let e = eid as u32;
104        let (src, tgt) = graph.edge(e)?;
105
106        let src_type = types[src as usize];
107        let tgt_type = types[tgt as usize];
108
109        // Skip edges within the same partition (matches C behaviour).
110        if src_type == tgt_type {
111            continue;
112        }
113
114        // Determine which is the row vertex and which is the column vertex.
115        let (row_v, col_v) = if src_type { (tgt, src) } else { (src, tgt) };
116
117        let ri = vertex_to_idx[row_v as usize] as usize;
118        let ci = vertex_to_idx[col_v as usize] as usize;
119        matrix[ri][ci] += 1.0;
120    }
121
122    Ok(GetBiadjacencyResult {
123        matrix,
124        row_ids,
125        col_ids,
126    })
127}
128
129#[cfg(test)]
130#[allow(clippy::float_cmp)]
131mod tests {
132    use super::*;
133    use crate::algorithms::constructors::biadjacency::biadjacency;
134    use crate::algorithms::games::bipartite::BipartiteMode;
135
136    #[test]
137    fn empty_graph() {
138        let g = Graph::new(0, false).unwrap();
139        let r = get_biadjacency_matrix(&g, &[]).unwrap();
140        assert!(r.matrix.is_empty());
141        assert!(r.row_ids.is_empty());
142        assert!(r.col_ids.is_empty());
143    }
144
145    #[test]
146    fn single_vertex_row() {
147        let g = Graph::new(1, false).unwrap();
148        let r = get_biadjacency_matrix(&g, &[false]).unwrap();
149        assert_eq!(r.matrix.len(), 1);
150        assert!(r.matrix[0].is_empty()); // 1×0
151        assert_eq!(r.row_ids, vec![0]);
152        assert!(r.col_ids.is_empty());
153    }
154
155    #[test]
156    fn single_vertex_col() {
157        let g = Graph::new(1, false).unwrap();
158        let r = get_biadjacency_matrix(&g, &[true]).unwrap();
159        assert!(r.matrix.is_empty()); // 0×1
160        assert!(r.row_ids.is_empty());
161        assert_eq!(r.col_ids, vec![0]);
162    }
163
164    #[test]
165    fn types_length_mismatch() {
166        let g = Graph::new(3, false).unwrap();
167        let result = get_biadjacency_matrix(&g, &[false, true]);
168        assert!(result.is_err());
169    }
170
171    #[test]
172    fn roundtrip_simple() {
173        let matrix: &[&[f64]] = &[&[1.0, 0.0, 1.0], &[0.0, 1.0, 0.0]];
174        let built = biadjacency(matrix, false, BipartiteMode::All, false).unwrap();
175        let extracted = get_biadjacency_matrix(&built.graph, &built.types).unwrap();
176
177        assert_eq!(extracted.matrix.len(), 2);
178        assert_eq!(extracted.matrix[0].len(), 3);
179        assert_eq!(extracted.matrix[0][0], 1.0);
180        assert_eq!(extracted.matrix[0][1], 0.0);
181        assert_eq!(extracted.matrix[0][2], 1.0);
182        assert_eq!(extracted.matrix[1][0], 0.0);
183        assert_eq!(extracted.matrix[1][1], 1.0);
184        assert_eq!(extracted.matrix[1][2], 0.0);
185    }
186
187    #[test]
188    fn roundtrip_with_multiple() {
189        let matrix: &[&[f64]] = &[&[1.0, 0.0, 2.0], &[0.0, 1.0, 0.0]];
190        let built = biadjacency(matrix, false, BipartiteMode::All, true).unwrap();
191        let extracted = get_biadjacency_matrix(&built.graph, &built.types).unwrap();
192
193        assert_eq!(extracted.matrix[0][0], 1.0);
194        assert_eq!(extracted.matrix[0][2], 2.0);
195        assert_eq!(extracted.matrix[1][1], 1.0);
196    }
197
198    #[test]
199    fn directed_out() {
200        let matrix: &[&[f64]] = &[&[1.0, 1.0]];
201        let built = biadjacency(matrix, true, BipartiteMode::Out, false).unwrap();
202        let extracted = get_biadjacency_matrix(&built.graph, &built.types).unwrap();
203        assert_eq!(extracted.matrix[0][0], 1.0);
204        assert_eq!(extracted.matrix[0][1], 1.0);
205    }
206
207    #[test]
208    fn directed_in() {
209        let matrix: &[&[f64]] = &[&[1.0, 1.0]];
210        let built = biadjacency(matrix, true, BipartiteMode::In, false).unwrap();
211        let extracted = get_biadjacency_matrix(&built.graph, &built.types).unwrap();
212        // In mode: arcs from col→row, but biadjacency counts edges crossing partitions
213        assert_eq!(extracted.matrix[0][0], 1.0);
214        assert_eq!(extracted.matrix[0][1], 1.0);
215    }
216
217    #[test]
218    fn directed_all_mutual() {
219        let matrix: &[&[f64]] = &[&[1.0]];
220        let built = biadjacency(matrix, true, BipartiteMode::All, false).unwrap();
221        // All mode creates 2 arcs (mutual), extraction counts both
222        let extracted = get_biadjacency_matrix(&built.graph, &built.types).unwrap();
223        assert_eq!(extracted.matrix[0][0], 2.0);
224    }
225
226    #[test]
227    fn ignores_same_partition_edges() {
228        // Manually create a graph with an intra-partition edge
229        let mut g = Graph::new(4, false).unwrap();
230        // vertices 0,1 = rows (false); vertices 2,3 = cols (true)
231        g.add_edges(vec![(0, 2), (0, 1), (1, 3)]).unwrap();
232        let types = vec![false, false, true, true];
233        let r = get_biadjacency_matrix(&g, &types).unwrap();
234        // Edge (0,1) is same-partition → ignored
235        assert_eq!(r.matrix[0][0], 1.0); // edge (0,2)
236        assert_eq!(r.matrix[0][1], 0.0);
237        assert_eq!(r.matrix[1][0], 0.0);
238        assert_eq!(r.matrix[1][1], 1.0); // edge (1,3)
239    }
240
241    #[test]
242    fn row_col_ids_ordering() {
243        // Types: [true, false, true, false] → rows are {1,3}, cols are {0,2}
244        let mut g = Graph::new(4, false).unwrap();
245        g.add_edges(vec![(1, 0), (3, 2)]).unwrap();
246        let types = vec![true, false, true, false];
247        let r = get_biadjacency_matrix(&g, &types).unwrap();
248        assert_eq!(r.row_ids, vec![1, 3]);
249        assert_eq!(r.col_ids, vec![0, 2]);
250        assert_eq!(r.matrix.len(), 2); // 2 rows
251        assert_eq!(r.matrix[0].len(), 2); // 2 cols
252        assert_eq!(r.matrix[0][0], 1.0); // row 1, col 0
253        assert_eq!(r.matrix[1][1], 1.0); // row 3, col 2
254    }
255
256    #[test]
257    fn no_edges() {
258        let g = Graph::new(4, false).unwrap();
259        let types = vec![false, false, true, true];
260        let r = get_biadjacency_matrix(&g, &types).unwrap();
261        assert_eq!(r.matrix, vec![vec![0.0, 0.0], vec![0.0, 0.0]]);
262    }
263
264    #[test]
265    fn self_loop_ignored() {
266        let mut g = Graph::new(3, false).unwrap();
267        g.add_edges(vec![(0, 0), (0, 2)]).unwrap();
268        let types = vec![false, false, true];
269        let r = get_biadjacency_matrix(&g, &types).unwrap();
270        // Self-loop (0,0) is same-partition → ignored
271        assert_eq!(r.matrix[0][0], 1.0); // edge (0,2)
272        assert_eq!(r.matrix[1][0], 0.0);
273    }
274}