rust_igraph/algorithms/operators/
compose.rs1use crate::core::error::IgraphError;
6use crate::core::{Graph, IgraphResult, VertexId};
7
8pub 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 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 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 assert_eq!(c.ecount(), 1);
117 assert_eq!(c.edge(0).unwrap(), (0, 2));
118 }
119
120 #[test]
121 fn test_identity_compose() {
122 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 assert_eq!(c.ecount(), 12);
136 }
137
138 #[test]
139 fn test_different_sizes() {
140 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); assert_eq!(c.ecount(), 1);
151 assert_eq!(c.edge(0).unwrap(), (0, 4));
152 }
153
154 #[test]
155 fn test_undirected() {
156 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 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 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 assert_eq!(c.ecount(), 0);
207 }
208
209 #[test]
210 fn test_self_loop_generation() {
211 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}