Skip to main content

rust_igraph/algorithms/constructors/
biadjacency.rs

1//! Bipartite graph from biadjacency matrix (ALGO-CN-037).
2//!
3//! Counterpart of `igraph_biadjacency()` in
4//! `references/igraph/src/misc/bipartite.c:634-737`.
5//!
6//! A *biadjacency matrix* (also called incidence matrix of a bipartite
7//! graph) is an `n1 × n2` matrix where rows correspond to vertices in
8//! partition 1 and columns to partition 2. Non-zero entries indicate
9//! edges between the corresponding vertices.
10
11use crate::algorithms::games::bipartite::BipartiteMode;
12use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
13
14/// Result of [`biadjacency`].
15#[derive(Debug, Clone)]
16pub struct BiadjacencyResult {
17    /// The constructed bipartite graph on `n1 + n2` vertices.
18    pub graph: Graph,
19    /// Per-vertex type: `false` = partition 1 (rows), `true` = partition 2 (columns).
20    pub types: Vec<bool>,
21}
22
23/// Build a bipartite graph from a biadjacency matrix.
24///
25/// The matrix has `n1` rows and `n2` columns. The resulting graph has
26/// `n1 + n2` vertices: vertices `[0, n1)` correspond to rows (partition 1)
27/// and vertices `[n1, n1+n2)` correspond to columns (partition 2).
28///
29/// # Parameters
30///
31/// * `matrix` — row-major `n1 × n2` biadjacency matrix (all rows must have
32///   equal length `n2`).
33/// * `directed` — whether the resulting graph is directed.
34/// * `mode` — edge direction policy (only meaningful when `directed = true`):
35///   - [`BipartiteMode::Out`] — arcs from rows to columns.
36///   - [`BipartiteMode::In`] — arcs from columns to rows.
37///   - [`BipartiteMode::All`] — mutual arcs in both directions.
38/// * `multiple` — if `true`, matrix values are interpreted as integer
39///   multiplicities (must be non-negative); if `false`, any non-zero
40///   value produces a single edge.
41///
42/// # Errors
43///
44/// * [`IgraphError::InvalidArgument`] — ragged matrix, negative entry
45///   when `multiple = true`, or vertex count overflow.
46///
47/// # Examples
48///
49/// ```
50/// use rust_igraph::{biadjacency, BipartiteMode};
51///
52/// // 2×3 matrix → graph with 5 vertices.
53/// let matrix: &[&[f64]] = &[
54///     &[1.0, 0.0, 2.0],
55///     &[0.0, 1.0, 0.0],
56/// ];
57/// let r = biadjacency(matrix, false, BipartiteMode::All, false).unwrap();
58/// assert_eq!(r.graph.vcount(), 5);
59/// assert_eq!(r.graph.ecount(), 3); // three non-zero entries
60/// assert_eq!(r.types, vec![false, false, true, true, true]);
61///
62/// // With multiple=true, entry 2.0 produces 2 edges.
63/// let m = biadjacency(matrix, false, BipartiteMode::All, true).unwrap();
64/// assert_eq!(m.graph.ecount(), 4); // 1 + 2 + 1
65/// ```
66pub fn biadjacency(
67    matrix: &[&[f64]],
68    directed: bool,
69    mode: BipartiteMode,
70    multiple: bool,
71) -> IgraphResult<BiadjacencyResult> {
72    let n1 = matrix.len();
73    let n2 = if n1 == 0 { 0 } else { matrix[0].len() };
74
75    for (row_idx, row) in matrix.iter().enumerate() {
76        if row.len() != n2 {
77            return Err(IgraphError::InvalidArgument(format!(
78                "biadjacency: row {row_idx} has length {} but expected {n2}",
79                row.len()
80            )));
81        }
82    }
83
84    let total = n1.checked_add(n2).ok_or_else(|| {
85        IgraphError::InvalidArgument("biadjacency: total vertex count overflows usize".to_string())
86    })?;
87    let total_u32 = u32::try_from(total).map_err(|_| {
88        IgraphError::InvalidArgument("biadjacency: total vertex count exceeds u32::MAX".to_string())
89    })?;
90    // Safe: n1 <= total which fits in u32.
91    let n1_u32 = u32::try_from(n1).map_err(|_| {
92        IgraphError::InvalidArgument("biadjacency: n1 exceeds u32::MAX".to_string())
93    })?;
94
95    let n2_u32 = u32::try_from(n2).map_err(|_| {
96        IgraphError::InvalidArgument("biadjacency: n2 exceeds u32::MAX".to_string())
97    })?;
98
99    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
100
101    if multiple {
102        if n1 > 0 && n2 > 0 {
103            for row in matrix {
104                for &val in *row {
105                    if val < 0.0 {
106                        return Err(IgraphError::InvalidArgument(format!(
107                            "biadjacency: matrix elements must be non-negative when multiple=true, found {val}"
108                        )));
109                    }
110                }
111            }
112        }
113
114        for j in 0..n2_u32 {
115            for i in 0..n1_u32 {
116                #[allow(clippy::cast_possible_truncation)]
117                let elem = matrix[i as usize][j as usize] as i64;
118                if elem == 0 {
119                    continue;
120                }
121
122                let (from, to) = if mode == BipartiteMode::In {
123                    (n1_u32 + j, i)
124                } else {
125                    (i, n1_u32 + j)
126                };
127
128                if mode != BipartiteMode::All || !directed {
129                    for _ in 0..elem {
130                        edges.push((from, to));
131                    }
132                } else {
133                    for _ in 0..elem {
134                        edges.push((from, to));
135                        edges.push((to, from));
136                    }
137                }
138            }
139        }
140    } else {
141        for j in 0..n2_u32 {
142            for i in 0..n1_u32 {
143                if matrix[i as usize][j as usize] != 0.0 {
144                    let (from, to) = if mode == BipartiteMode::In {
145                        (n1_u32 + j, i)
146                    } else {
147                        (i, n1_u32 + j)
148                    };
149
150                    if mode != BipartiteMode::All || !directed {
151                        edges.push((from, to));
152                    } else {
153                        edges.push((from, to));
154                        edges.push((to, from));
155                    }
156                }
157            }
158        }
159    }
160
161    let mut graph = Graph::new(total_u32, directed)?;
162    if !edges.is_empty() {
163        graph.add_edges(edges)?;
164    }
165
166    let mut types = vec![false; total];
167    for t in types.iter_mut().skip(n1) {
168        *t = true;
169    }
170
171    Ok(BiadjacencyResult { graph, types })
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn empty_matrix() {
180        let matrix: &[&[f64]] = &[];
181        let r = biadjacency(matrix, false, BipartiteMode::All, false).unwrap();
182        assert_eq!(r.graph.vcount(), 0);
183        assert_eq!(r.graph.ecount(), 0);
184        assert!(r.types.is_empty());
185    }
186
187    #[test]
188    fn single_entry_nonzero() {
189        let matrix: &[&[f64]] = &[&[1.0]];
190        let r = biadjacency(matrix, false, BipartiteMode::All, false).unwrap();
191        assert_eq!(r.graph.vcount(), 2);
192        assert_eq!(r.graph.ecount(), 1);
193        assert_eq!(r.types, vec![false, true]);
194    }
195
196    #[test]
197    fn single_entry_zero() {
198        let matrix: &[&[f64]] = &[&[0.0]];
199        let r = biadjacency(matrix, false, BipartiteMode::All, false).unwrap();
200        assert_eq!(r.graph.vcount(), 2);
201        assert_eq!(r.graph.ecount(), 0);
202    }
203
204    #[test]
205    fn two_by_three_no_multiple() {
206        let matrix: &[&[f64]] = &[&[1.0, 0.0, 3.0], &[0.0, 1.0, 0.0]];
207        let r = biadjacency(matrix, false, BipartiteMode::All, false).unwrap();
208        assert_eq!(r.graph.vcount(), 5);
209        assert_eq!(r.graph.ecount(), 3);
210        assert_eq!(r.types, vec![false, false, true, true, true]);
211    }
212
213    #[test]
214    fn two_by_three_with_multiple() {
215        let matrix: &[&[f64]] = &[&[1.0, 0.0, 2.0], &[0.0, 1.0, 0.0]];
216        let r = biadjacency(matrix, false, BipartiteMode::All, true).unwrap();
217        assert_eq!(r.graph.vcount(), 5);
218        assert_eq!(r.graph.ecount(), 4); // 1 + 2 + 1
219    }
220
221    #[test]
222    fn directed_out() {
223        let matrix: &[&[f64]] = &[&[1.0, 1.0], &[1.0, 0.0]];
224        let r = biadjacency(matrix, true, BipartiteMode::Out, false).unwrap();
225        assert!(r.graph.is_directed());
226        assert_eq!(r.graph.ecount(), 3);
227        for eid in 0..r.graph.ecount() {
228            #[allow(clippy::cast_possible_truncation)]
229            let (s, t) = r.graph.edge(eid as u32).unwrap();
230            assert!(!r.types[s as usize], "source should be row (partition 1)");
231            assert!(r.types[t as usize], "target should be col (partition 2)");
232        }
233    }
234
235    #[test]
236    fn directed_in() {
237        let matrix: &[&[f64]] = &[&[1.0, 1.0], &[1.0, 0.0]];
238        let r = biadjacency(matrix, true, BipartiteMode::In, false).unwrap();
239        assert!(r.graph.is_directed());
240        assert_eq!(r.graph.ecount(), 3);
241        for eid in 0..r.graph.ecount() {
242            #[allow(clippy::cast_possible_truncation)]
243            let (s, t) = r.graph.edge(eid as u32).unwrap();
244            assert!(r.types[s as usize], "source should be col (partition 2)");
245            assert!(!r.types[t as usize], "target should be row (partition 1)");
246        }
247    }
248
249    #[test]
250    fn directed_all_mutual() {
251        let matrix: &[&[f64]] = &[&[1.0]];
252        let r = biadjacency(matrix, true, BipartiteMode::All, false).unwrap();
253        assert!(r.graph.is_directed());
254        assert_eq!(r.graph.ecount(), 2); // mutual
255    }
256
257    #[test]
258    fn multiple_with_mutual() {
259        let matrix: &[&[f64]] = &[&[2.0]];
260        let r = biadjacency(matrix, true, BipartiteMode::All, true).unwrap();
261        assert_eq!(r.graph.ecount(), 4); // 2 × 2 mutual
262    }
263
264    #[test]
265    fn rejects_negative_in_multiple_mode() {
266        let matrix: &[&[f64]] = &[&[-1.0]];
267        let result = biadjacency(matrix, false, BipartiteMode::All, true);
268        assert!(result.is_err());
269    }
270
271    #[test]
272    fn negative_allowed_in_non_multiple_mode() {
273        let matrix: &[&[f64]] = &[&[-1.0]];
274        let r = biadjacency(matrix, false, BipartiteMode::All, false).unwrap();
275        assert_eq!(r.graph.ecount(), 1); // non-zero → edge
276    }
277
278    #[test]
279    fn ragged_matrix_rejected() {
280        let matrix: &[&[f64]] = &[&[1.0, 2.0], &[3.0]];
281        let result = biadjacency(matrix, false, BipartiteMode::All, false);
282        assert!(result.is_err());
283    }
284
285    #[test]
286    fn undirected_ignores_mode_out() {
287        let matrix: &[&[f64]] = &[&[1.0]];
288        let r = biadjacency(matrix, false, BipartiteMode::Out, false).unwrap();
289        assert!(!r.graph.is_directed());
290        assert_eq!(r.graph.ecount(), 1);
291    }
292}