Skip to main content

rust_igraph/algorithms/operators/
compose.rs

1//! Graph composition operator (ALGO-OP-013).
2//!
3//! Computes the relational composition of two graphs.
4
5use crate::core::error::IgraphError;
6use crate::core::{Graph, IgraphResult, VertexId};
7
8/// Computes the composition of two graphs.
9///
10/// The composed graph has `max(|V1|, |V2|)` vertices. An edge `(i, j)`
11/// exists in the result if and only if there is some vertex `k` such that
12/// `(i, k)` is an edge in `g1` and `(k, j)` is an edge in `g2`.
13///
14/// Both graphs must have the same directedness. The result may contain
15/// multi-edges; use [`crate::simplify`] to remove them if needed.
16///
17/// # Arguments
18///
19/// * `g1` — the first operand (left side of composition).
20/// * `g2` — the second operand (right side of composition).
21///
22/// # Errors
23///
24/// Returns `InvalidArgument` if `g1` and `g2` differ in directedness.
25///
26/// # Examples
27///
28/// ```
29/// use rust_igraph::{Graph, compose};
30///
31/// // g1: 0→1→2, g2: 0→1→2 (directed paths)
32/// let mut g1 = Graph::new(3, true).unwrap();
33/// g1.add_edge(0, 1).unwrap();
34/// g1.add_edge(1, 2).unwrap();
35///
36/// let mut g2 = Graph::new(3, true).unwrap();
37/// g2.add_edge(0, 1).unwrap();
38/// g2.add_edge(1, 2).unwrap();
39///
40/// let c = compose(&g1, &g2).unwrap();
41/// assert_eq!(c.vcount(), 3);
42/// // (0,1) in g1 + (1,2) in g2 → (0,2) in result
43/// // (1,2) in g1 + ... vertex 2 has no outgoing in g2 → nothing
44/// assert_eq!(c.ecount(), 1);
45/// assert_eq!(c.edge(0).unwrap(), (0, 2));
46/// ```
47pub fn compose(g1: &Graph, g2: &Graph) -> IgraphResult<Graph> {
48    if g1.is_directed() != g2.is_directed() {
49        return Err(IgraphError::InvalidArgument(
50            "cannot compose directed and undirected graphs".to_string(),
51        ));
52    }
53
54    let n1 = g1.vcount();
55    let n2 = g2.vcount();
56    let directed = g1.is_directed();
57    let n = n1.max(n2);
58
59    // Build outgoing adjacency for g1 and g2
60    let adj1 = build_out_adjacency(g1, n1)?;
61    let adj2 = build_out_adjacency(g2, n2)?;
62
63    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
64
65    for i in 0..n1 {
66        for &k in &adj1[i as usize] {
67            if k < n2 {
68                for &j in &adj2[k as usize] {
69                    edges.push((i, j));
70                }
71            }
72        }
73    }
74
75    let mut result = Graph::new(n, directed)?;
76    result.add_edges(edges)?;
77    Ok(result)
78}
79
80fn build_out_adjacency(graph: &Graph, n: u32) -> IgraphResult<Vec<Vec<VertexId>>> {
81    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n as usize];
82    let ecount = graph.ecount();
83
84    for eid in 0..ecount {
85        #[allow(clippy::cast_possible_truncation)]
86        let eid_u32 = eid as u32;
87        let (src, tgt) = graph.edge(eid_u32)?;
88        adj[src as usize].push(tgt);
89        if !graph.is_directed() && src != tgt {
90            adj[tgt as usize].push(src);
91        }
92    }
93    Ok(adj)
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_directed_path_compose() {
102        // g1: 0→1→2, g2: 0→1→2
103        let mut g1 = Graph::new(3, true).unwrap();
104        g1.add_edge(0, 1).unwrap();
105        g1.add_edge(1, 2).unwrap();
106
107        let mut g2 = Graph::new(3, true).unwrap();
108        g2.add_edge(0, 1).unwrap();
109        g2.add_edge(1, 2).unwrap();
110
111        let c = compose(&g1, &g2).unwrap();
112        assert_eq!(c.vcount(), 3);
113        // g1 has (0,1),(1,2); g2 has (0,1),(1,2)
114        // Composition: (0,1) in g1 → check g2 from 1: (1,2) → result (0,2)
115        //              (1,2) in g1 → check g2 from 2: nothing
116        assert_eq!(c.ecount(), 1);
117        assert_eq!(c.edge(0).unwrap(), (0, 2));
118    }
119
120    #[test]
121    fn test_identity_compose() {
122        // g1 = g2 = complete K3 directed (all 6 directed edges)
123        let mut g = Graph::new(3, true).unwrap();
124        g.add_edge(0, 1).unwrap();
125        g.add_edge(0, 2).unwrap();
126        g.add_edge(1, 0).unwrap();
127        g.add_edge(1, 2).unwrap();
128        g.add_edge(2, 0).unwrap();
129        g.add_edge(2, 1).unwrap();
130
131        let c = compose(&g, &g).unwrap();
132        assert_eq!(c.vcount(), 3);
133        // Each vertex reaches all 2 others, each of those reaches 2 others
134        // = 3 * 2 * 2 = 12 edges (with duplicates since K3 composition = K3 with multiplicity 2)
135        assert_eq!(c.ecount(), 12);
136    }
137
138    #[test]
139    fn test_different_sizes() {
140        // g1 has 3 vertices, g2 has 5 vertices
141        let mut g1 = Graph::new(3, true).unwrap();
142        g1.add_edge(0, 1).unwrap();
143
144        let mut g2 = Graph::new(5, true).unwrap();
145        g2.add_edge(1, 4).unwrap();
146
147        let c = compose(&g1, &g2).unwrap();
148        assert_eq!(c.vcount(), 5); // max(3,5)
149        // g1: (0,1), g2 from 1: (1,4) → result: (0,4)
150        assert_eq!(c.ecount(), 1);
151        assert_eq!(c.edge(0).unwrap(), (0, 4));
152    }
153
154    #[test]
155    fn test_undirected() {
156        // Undirected: edge (a,b) means both (a,b) and (b,a) in the relation
157        let mut g1 = Graph::with_vertices(3);
158        g1.add_edge(0, 1).unwrap();
159
160        let mut g2 = Graph::with_vertices(3);
161        g2.add_edge(1, 2).unwrap();
162
163        let c = compose(&g1, &g2).unwrap();
164        assert_eq!(c.vcount(), 3);
165        // g1 relation: {(0,1),(1,0)}; g2 relation: {(1,2),(2,1)}
166        // Compose: (0,1)+(1,2)=(0,2); (1,0)+(0,?)=nothing from 0 in g2
167        //          (0,1)+(1,?)=(0,2) already counted for undirected adj
168        //          but we also have (1,0) in adj → check g2 from 0: nothing
169        // Actually: adj1[0] = [1], adj1[1] = [0]
170        // adj2[1] = [2], adj2[2] = [1]
171        // Compose: from 0: adj1[0]=[1], for k=1: adj2[1]=[2] → (0,2)
172        //          from 1: adj1[1]=[0], for k=0: adj2[0]=[] → nothing
173        //          from 2: not in g1 (no out-neighbors) → nothing
174        // Result: 1 edge (0,2)
175        assert_eq!(c.ecount(), 1);
176    }
177
178    #[test]
179    fn test_mixed_directedness_error() {
180        let g1 = Graph::new(3, true).unwrap();
181        let g2 = Graph::with_vertices(3);
182        assert!(compose(&g1, &g2).is_err());
183    }
184
185    #[test]
186    fn test_empty() {
187        let g1 = Graph::new(0, true).unwrap();
188        let g2 = Graph::new(0, true).unwrap();
189        let c = compose(&g1, &g2).unwrap();
190        assert_eq!(c.vcount(), 0);
191        assert_eq!(c.ecount(), 0);
192    }
193
194    #[test]
195    fn test_no_overlap() {
196        // g1: 0→1, g2: 2→3 (no shared intermediate vertex)
197        let mut g1 = Graph::new(4, true).unwrap();
198        g1.add_edge(0, 1).unwrap();
199
200        let mut g2 = Graph::new(4, true).unwrap();
201        g2.add_edge(2, 3).unwrap();
202
203        let c = compose(&g1, &g2).unwrap();
204        assert_eq!(c.vcount(), 4);
205        // g1: (0,1), check g2 from 1: nothing → 0 edges
206        assert_eq!(c.ecount(), 0);
207    }
208
209    #[test]
210    fn test_self_loop_generation() {
211        // g1: 0→1, g2: 1→0 → compose: (0,0) self-loop
212        let mut g1 = Graph::new(2, true).unwrap();
213        g1.add_edge(0, 1).unwrap();
214
215        let mut g2 = Graph::new(2, true).unwrap();
216        g2.add_edge(1, 0).unwrap();
217
218        let c = compose(&g1, &g2).unwrap();
219        assert_eq!(c.ecount(), 1);
220        assert_eq!(c.edge(0).unwrap(), (0, 0));
221    }
222}