Skip to main content

rust_igraph/algorithms/properties/
get_biadjacency_weighted.rs

1//! Weighted biadjacency matrix extraction (ALGO-PR-046).
2//!
3//! Counterpart of `igraph_get_biadjacency()` with weights in
4//! `references/igraph/src/misc/bipartite.c:874-956`.
5//!
6//! Extracts the weighted biadjacency matrix from a bipartite graph
7//! given a per-vertex type assignment and per-edge weight vector.
8
9use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
10
11/// Result of [`get_biadjacency_weighted`].
12#[derive(Debug, Clone)]
13pub struct GetBiadjacencyWeightedResult {
14    /// Row-major `n1 × n2` weighted biadjacency matrix.
15    /// `matrix[i][j]` is the sum of weights of edges between
16    /// row-vertex `i` 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 a weighted biadjacency matrix from a bipartite graph.
25///
26/// Given a graph, a per-vertex type vector, and per-edge weights,
27/// returns the `n1 × n2` biadjacency matrix where entry `(i, j)` is
28/// the sum of weights of edges between row-vertex `i` and
29/// column-vertex `j`.
30///
31/// Edges within the same partition are silently ignored.
32///
33/// # Parameters
34///
35/// * `graph` — the input graph.
36/// * `types` — per-vertex boolean: `false` = row, `true` = column.
37/// * `weights` — per-edge weight (must have length `graph.ecount()`).
38///
39/// # Errors
40///
41/// * [`IgraphError::InvalidArgument`] — if `types.len() != vcount` or
42///   `weights.len() != ecount`.
43///
44/// # Examples
45///
46/// ```
47/// use rust_igraph::{Graph, get_biadjacency_weighted};
48///
49/// let mut g = Graph::new(4, false).unwrap();
50/// // rows: {0, 1}, cols: {2, 3}
51/// g.add_edges(vec![(0, 2), (0, 3), (1, 2)]).unwrap();
52/// let types = vec![false, false, true, true];
53/// let weights = vec![1.5, 2.0, 3.0];
54/// let r = get_biadjacency_weighted(&g, &types, &weights).unwrap();
55/// assert_eq!(r.matrix[0][0], 1.5); // edge (0,2) weight
56/// assert_eq!(r.matrix[0][1], 2.0); // edge (0,3) weight
57/// assert_eq!(r.matrix[1][0], 3.0); // edge (1,2) weight
58/// assert_eq!(r.matrix[1][1], 0.0); // no edge (1,3)
59/// ```
60pub fn get_biadjacency_weighted(
61    graph: &Graph,
62    types: &[bool],
63    weights: &[f64],
64) -> IgraphResult<GetBiadjacencyWeightedResult> {
65    let vcount = graph.vcount();
66    let ecount = graph.ecount();
67
68    if types.len() != vcount as usize {
69        return Err(IgraphError::InvalidArgument(format!(
70            "get_biadjacency_weighted: types length ({}) != vcount ({})",
71            types.len(),
72            vcount
73        )));
74    }
75
76    if weights.len() != ecount {
77        return Err(IgraphError::InvalidArgument(format!(
78            "get_biadjacency_weighted: weights length ({}) != ecount ({})",
79            weights.len(),
80            ecount
81        )));
82    }
83
84    let mut n1: usize = 0;
85    let mut n2: usize = 0;
86    let mut row_ids: Vec<VertexId> = Vec::new();
87    let mut col_ids: Vec<VertexId> = Vec::new();
88    let mut vertex_to_idx: Vec<u32> = vec![0; vcount as usize];
89
90    for v in 0..vcount {
91        if types[v as usize] {
92            #[allow(clippy::cast_possible_truncation)]
93            let idx = n2 as u32;
94            vertex_to_idx[v as usize] = idx;
95            col_ids.push(v);
96            n2 += 1;
97        } else {
98            #[allow(clippy::cast_possible_truncation)]
99            let idx = n1 as u32;
100            vertex_to_idx[v as usize] = idx;
101            row_ids.push(v);
102            n1 += 1;
103        }
104    }
105
106    let mut matrix = vec![vec![0.0_f64; n2]; n1];
107
108    for (eid, &w) in weights.iter().enumerate().take(ecount) {
109        #[allow(clippy::cast_possible_truncation)]
110        let e = eid as u32;
111        let (src, tgt) = graph.edge(e)?;
112
113        let src_type = types[src as usize];
114        let tgt_type = types[tgt as usize];
115
116        if src_type == tgt_type {
117            continue;
118        }
119
120        let (row_v, col_v) = if src_type { (tgt, src) } else { (src, tgt) };
121
122        let ri = vertex_to_idx[row_v as usize] as usize;
123        let ci = vertex_to_idx[col_v as usize] as usize;
124        matrix[ri][ci] += w;
125    }
126
127    Ok(GetBiadjacencyWeightedResult {
128        matrix,
129        row_ids,
130        col_ids,
131    })
132}
133
134#[cfg(test)]
135#[allow(clippy::float_cmp)]
136mod tests {
137    use super::*;
138    use crate::core::Graph;
139
140    #[test]
141    fn empty_graph() {
142        let g = Graph::new(0, false).unwrap();
143        let r = get_biadjacency_weighted(&g, &[], &[]).unwrap();
144        assert!(r.matrix.is_empty());
145    }
146
147    #[test]
148    fn types_mismatch() {
149        let g = Graph::new(3, false).unwrap();
150        let result = get_biadjacency_weighted(&g, &[false, true], &[]);
151        assert!(result.is_err());
152    }
153
154    #[test]
155    fn weights_mismatch() {
156        let mut g = Graph::new(2, false).unwrap();
157        g.add_edges(vec![(0, 1)]).unwrap();
158        let result = get_biadjacency_weighted(&g, &[false, true], &[1.0, 2.0]);
159        assert!(result.is_err());
160    }
161
162    #[test]
163    fn simple_weighted() {
164        let mut g = Graph::new(4, false).unwrap();
165        g.add_edges(vec![(0, 2), (0, 3), (1, 2)]).unwrap();
166        let types = vec![false, false, true, true];
167        let weights = vec![1.5, 2.0, 3.0];
168        let r = get_biadjacency_weighted(&g, &types, &weights).unwrap();
169        assert_eq!(r.matrix[0][0], 1.5);
170        assert_eq!(r.matrix[0][1], 2.0);
171        assert_eq!(r.matrix[1][0], 3.0);
172        assert_eq!(r.matrix[1][1], 0.0);
173    }
174
175    #[test]
176    fn parallel_edges_sum_weights() {
177        let mut g = Graph::new(2, false).unwrap();
178        g.add_edges(vec![(0, 1), (0, 1)]).unwrap();
179        let types = vec![false, true];
180        let weights = vec![1.5, 2.5];
181        let r = get_biadjacency_weighted(&g, &types, &weights).unwrap();
182        assert_eq!(r.matrix[0][0], 4.0);
183    }
184
185    #[test]
186    fn ignores_same_partition() {
187        let mut g = Graph::new(3, false).unwrap();
188        g.add_edges(vec![(0, 1), (0, 2)]).unwrap();
189        let types = vec![false, false, true];
190        let weights = vec![5.0, 3.0];
191        let r = get_biadjacency_weighted(&g, &types, &weights).unwrap();
192        // Edge (0,1) is same partition → ignored
193        assert_eq!(r.matrix[0][0], 3.0); // only edge (0,2) with weight 3.0
194        assert_eq!(r.matrix[1][0], 0.0);
195    }
196
197    #[test]
198    fn negative_weights() {
199        let mut g = Graph::new(2, false).unwrap();
200        g.add_edges(vec![(0, 1)]).unwrap();
201        let types = vec![false, true];
202        let weights = vec![-2.5];
203        let r = get_biadjacency_weighted(&g, &types, &weights).unwrap();
204        assert_eq!(r.matrix[0][0], -2.5);
205    }
206
207    #[test]
208    fn directed_graph() {
209        let mut g = Graph::new(3, true).unwrap();
210        g.add_edges(vec![(0, 2), (2, 1)]).unwrap();
211        let types = vec![false, false, true];
212        let weights = vec![1.0, 2.0];
213        let r = get_biadjacency_weighted(&g, &types, &weights).unwrap();
214        assert_eq!(r.matrix[0][0], 1.0); // edge (0,2)
215        assert_eq!(r.matrix[1][0], 2.0); // edge (2,1): col→row counts
216    }
217
218    #[test]
219    fn row_col_ids() {
220        let mut g = Graph::new(4, false).unwrap();
221        g.add_edges(vec![(1, 0), (3, 2)]).unwrap();
222        let types = vec![true, false, true, false];
223        let weights = vec![1.0, 2.0];
224        let r = get_biadjacency_weighted(&g, &types, &weights).unwrap();
225        assert_eq!(r.row_ids, vec![1, 3]);
226        assert_eq!(r.col_ids, vec![0, 2]);
227        assert_eq!(r.matrix[0][0], 1.0);
228        assert_eq!(r.matrix[1][1], 2.0);
229    }
230}