Skip to main content

rust_igraph/algorithms/operators/
induced_subgraph.rs

1//! Induced subgraph extraction (ALGO-OP-007).
2//!
3//! Given a graph and a set of vertex IDs, returns a new graph containing
4//! only those vertices and all edges between them, plus the vertex mapping.
5
6use crate::core::error::IgraphError;
7use crate::core::{Graph, IgraphResult, VertexId};
8
9/// Result of an induced subgraph extraction.
10#[derive(Debug, Clone)]
11pub struct InducedSubgraphResult {
12    /// The subgraph.
13    pub graph: Graph,
14    /// Mapping from old vertex IDs to new vertex IDs. Contains `u32::MAX`
15    /// for vertices not in the subgraph.
16    pub map: Vec<u32>,
17    /// Mapping from new vertex IDs to old vertex IDs (inverse map).
18    pub invmap: Vec<VertexId>,
19}
20
21/// Creates the subgraph induced by the specified vertices.
22///
23/// Collects the specified vertices and all edges between them into a new
24/// graph. Vertex IDs in the result are contiguous starting from 0 and
25/// follow the order of the original graph (sorted by increasing old ID).
26///
27/// Duplicate vertex IDs in `vids` are silently ignored (only the first
28/// occurrence counts). The vertex order in the output always matches the
29/// ascending order of original IDs regardless of the order in `vids`.
30///
31/// # Arguments
32///
33/// * `graph` — the input graph.
34/// * `vids` — slice of vertex IDs to include in the subgraph.
35///
36/// # Errors
37///
38/// Returns `InvalidArgument` if any vertex ID in `vids` is >= `graph.vcount()`.
39///
40/// # Examples
41///
42/// ```
43/// use rust_igraph::{Graph, induced_subgraph};
44///
45/// let mut g = Graph::with_vertices(5);
46/// g.add_edge(0, 1).unwrap();
47/// g.add_edge(1, 2).unwrap();
48/// g.add_edge(2, 3).unwrap();
49/// g.add_edge(3, 4).unwrap();
50/// g.add_edge(0, 4).unwrap();
51///
52/// let result = induced_subgraph(&g, &[0, 1, 2]).unwrap();
53/// assert_eq!(result.graph.vcount(), 3);
54/// assert_eq!(result.graph.ecount(), 2); // edges 0-1 and 1-2
55/// assert_eq!(result.invmap, vec![0, 1, 2]);
56/// ```
57pub fn induced_subgraph(graph: &Graph, vids: &[VertexId]) -> IgraphResult<InducedSubgraphResult> {
58    let n = graph.vcount();
59
60    // Validate vertex IDs
61    for &v in vids {
62        if v >= n {
63            return Err(IgraphError::InvalidArgument(format!(
64                "vertex id {v} is out of range [0, {n})"
65            )));
66        }
67    }
68
69    // Build old-to-new mapping. Use u32::MAX as sentinel for "not in subgraph".
70    let mut map = vec![u32::MAX; n as usize];
71    let mut invmap: Vec<VertexId> = Vec::new();
72
73    // Collect unique vertices and sort by original ID
74    let mut sorted_vids: Vec<VertexId> = vids.to_vec();
75    sorted_vids.sort_unstable();
76    sorted_vids.dedup();
77
78    let no_of_new_nodes = sorted_vids.len();
79    invmap.reserve(no_of_new_nodes);
80
81    for (new_id, &old_id) in sorted_vids.iter().enumerate() {
82        // SAFETY: new_id < no_of_new_nodes <= n which is u32
83        #[allow(clippy::cast_possible_truncation)]
84        let new_id_u32 = new_id as u32;
85        map[old_id as usize] = new_id_u32;
86        invmap.push(old_id);
87    }
88
89    // Build edge list for the new graph
90    let directed = graph.is_directed();
91    let mut new_edges: Vec<(VertexId, VertexId)> = Vec::new();
92
93    for (new_vid_idx, &old_vid) in invmap.iter().enumerate() {
94        #[allow(clippy::cast_possible_truncation)]
95        let new_vid = new_vid_idx as u32;
96        let incident = graph.incident(old_vid)?;
97
98        if directed {
99            for eid in incident {
100                let src = graph.edge_source(eid)?;
101                if src != old_vid {
102                    continue;
103                }
104                let tgt = graph.edge_target(eid)?;
105                let new_tgt = map[tgt as usize];
106                if new_tgt == u32::MAX {
107                    continue;
108                }
109                new_edges.push((new_vid, new_tgt));
110            }
111        } else {
112            // Undirected: process each edge once.
113            // For an edge (u, v) stored as edge_source=u, edge_target=v,
114            // we process it from the vertex that is edge_source.
115            // Self-loops: each appears twice in incident list, emit once.
116            let mut skip_loop = false;
117            for eid in incident {
118                let src = graph.edge_source(eid)?;
119                if src != old_vid {
120                    continue;
121                }
122                let tgt = graph.edge_target(eid)?;
123                let new_tgt = map[tgt as usize];
124                if new_tgt == u32::MAX {
125                    continue;
126                }
127                if new_vid == new_tgt {
128                    skip_loop = !skip_loop;
129                    if skip_loop {
130                        continue;
131                    }
132                }
133                new_edges.push((new_vid, new_tgt));
134            }
135        }
136    }
137
138    // Create the new graph (no_of_new_nodes <= n which is u32)
139    #[allow(clippy::cast_possible_truncation)]
140    let new_node_count = no_of_new_nodes as u32;
141    let mut result_graph = Graph::new(new_node_count, directed)?;
142    result_graph.add_edges(new_edges)?;
143
144    Ok(InducedSubgraphResult {
145        graph: result_graph,
146        map,
147        invmap,
148    })
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_empty_vids() {
157        let g = Graph::with_vertices(5);
158        let result = induced_subgraph(&g, &[]).unwrap();
159        assert_eq!(result.graph.vcount(), 0);
160        assert_eq!(result.graph.ecount(), 0);
161        assert!(result.invmap.is_empty());
162    }
163
164    #[test]
165    fn test_all_vertices() {
166        let mut g = Graph::with_vertices(4);
167        g.add_edge(0, 1).unwrap();
168        g.add_edge(1, 2).unwrap();
169        g.add_edge(2, 3).unwrap();
170        let result = induced_subgraph(&g, &[0, 1, 2, 3]).unwrap();
171        assert_eq!(result.graph.vcount(), 4);
172        assert_eq!(result.graph.ecount(), 3);
173        assert_eq!(result.invmap, vec![0, 1, 2, 3]);
174    }
175
176    #[test]
177    fn test_partial_subgraph() {
178        let mut g = Graph::with_vertices(5);
179        g.add_edge(0, 1).unwrap();
180        g.add_edge(1, 2).unwrap();
181        g.add_edge(2, 3).unwrap();
182        g.add_edge(3, 4).unwrap();
183        g.add_edge(0, 4).unwrap();
184
185        let result = induced_subgraph(&g, &[1, 2, 3]).unwrap();
186        assert_eq!(result.graph.vcount(), 3);
187        assert_eq!(result.graph.ecount(), 2);
188        assert_eq!(result.invmap, vec![1, 2, 3]);
189        assert_eq!(result.map[0], u32::MAX);
190        assert_eq!(result.map[1], 0);
191        assert_eq!(result.map[2], 1);
192        assert_eq!(result.map[3], 2);
193        assert_eq!(result.map[4], u32::MAX);
194    }
195
196    #[test]
197    fn test_duplicate_vids() {
198        let mut g = Graph::with_vertices(4);
199        g.add_edge(0, 1).unwrap();
200        g.add_edge(1, 2).unwrap();
201        let result = induced_subgraph(&g, &[0, 1, 1, 0]).unwrap();
202        assert_eq!(result.graph.vcount(), 2);
203        assert_eq!(result.graph.ecount(), 1);
204    }
205
206    #[test]
207    fn test_unordered_vids() {
208        let mut g = Graph::with_vertices(4);
209        g.add_edge(0, 1).unwrap();
210        g.add_edge(1, 2).unwrap();
211        g.add_edge(2, 3).unwrap();
212        // Even though vids are [3, 1, 0], result should be ordered [0, 1, 3]
213        let result = induced_subgraph(&g, &[3, 1, 0]).unwrap();
214        assert_eq!(result.graph.vcount(), 3);
215        assert_eq!(result.invmap, vec![0, 1, 3]);
216        // Only edge 0-1 is within {0, 1, 3}
217        assert_eq!(result.graph.ecount(), 1);
218    }
219
220    #[test]
221    fn test_invalid_vertex() {
222        let g = Graph::with_vertices(3);
223        let result = induced_subgraph(&g, &[0, 5]);
224        assert!(result.is_err());
225    }
226
227    #[test]
228    fn test_directed() {
229        let mut g = Graph::new(4, true).unwrap();
230        g.add_edge(0, 1).unwrap();
231        g.add_edge(1, 0).unwrap();
232        g.add_edge(1, 2).unwrap();
233        g.add_edge(2, 3).unwrap();
234
235        let result = induced_subgraph(&g, &[0, 1, 2]).unwrap();
236        assert_eq!(result.graph.vcount(), 3);
237        assert_eq!(result.graph.ecount(), 3); // 0→1, 1→0, 1→2
238        assert!(result.graph.is_directed());
239    }
240
241    #[test]
242    fn test_self_loop() {
243        let mut g = Graph::with_vertices(3);
244        g.add_edge(0, 0).unwrap(); // self-loop
245        g.add_edge(0, 1).unwrap();
246        g.add_edge(1, 2).unwrap();
247
248        let result = induced_subgraph(&g, &[0, 1]).unwrap();
249        assert_eq!(result.graph.vcount(), 2);
250        assert_eq!(result.graph.ecount(), 2); // self-loop + edge 0-1
251    }
252
253    #[test]
254    fn test_isolated_vertices() {
255        let mut g = Graph::with_vertices(5);
256        g.add_edge(0, 1).unwrap();
257        g.add_edge(2, 3).unwrap();
258
259        // Extract vertices 1, 3, 4 — no edges between them
260        let result = induced_subgraph(&g, &[1, 3, 4]).unwrap();
261        assert_eq!(result.graph.vcount(), 3);
262        assert_eq!(result.graph.ecount(), 0);
263    }
264
265    #[test]
266    fn test_single_vertex() {
267        let mut g = Graph::with_vertices(3);
268        g.add_edge(0, 1).unwrap();
269        let result = induced_subgraph(&g, &[1]).unwrap();
270        assert_eq!(result.graph.vcount(), 1);
271        assert_eq!(result.graph.ecount(), 0);
272        assert_eq!(result.invmap, vec![1]);
273    }
274
275    #[test]
276    fn test_complete_subgraph() {
277        // K4, extract K3 from vertices {0,1,2}
278        let mut g = Graph::with_vertices(4);
279        g.add_edge(0, 1).unwrap();
280        g.add_edge(0, 2).unwrap();
281        g.add_edge(0, 3).unwrap();
282        g.add_edge(1, 2).unwrap();
283        g.add_edge(1, 3).unwrap();
284        g.add_edge(2, 3).unwrap();
285
286        let result = induced_subgraph(&g, &[0, 1, 2]).unwrap();
287        assert_eq!(result.graph.vcount(), 3);
288        assert_eq!(result.graph.ecount(), 3); // K3
289    }
290
291    #[test]
292    fn test_directed_self_loop() {
293        let mut g = Graph::new(3, true).unwrap();
294        g.add_edge(0, 0).unwrap(); // self-loop
295        g.add_edge(0, 1).unwrap();
296        g.add_edge(1, 2).unwrap();
297
298        let result = induced_subgraph(&g, &[0, 1]).unwrap();
299        assert_eq!(result.graph.vcount(), 2);
300        assert_eq!(result.graph.ecount(), 2); // self-loop + 0→1
301    }
302
303    #[test]
304    fn test_multi_edges() {
305        let mut g = Graph::with_vertices(3);
306        g.add_edge(0, 1).unwrap();
307        g.add_edge(0, 1).unwrap(); // multi-edge
308        g.add_edge(1, 2).unwrap();
309
310        let result = induced_subgraph(&g, &[0, 1]).unwrap();
311        assert_eq!(result.graph.vcount(), 2);
312        assert_eq!(result.graph.ecount(), 2); // both parallel edges preserved
313    }
314
315    #[test]
316    fn test_map_consistency() {
317        let mut g = Graph::with_vertices(6);
318        g.add_edge(0, 2).unwrap();
319        g.add_edge(2, 4).unwrap();
320        g.add_edge(4, 0).unwrap();
321
322        let result = induced_subgraph(&g, &[0, 2, 4]).unwrap();
323        // Check map and invmap are inverses
324        #[allow(clippy::cast_possible_truncation)]
325        for (new_id, &old_id) in result.invmap.iter().enumerate() {
326            assert_eq!(result.map[old_id as usize], new_id as u32);
327        }
328        #[allow(clippy::cast_possible_truncation)]
329        for (old_id, &new_id) in result.map.iter().enumerate() {
330            if new_id != u32::MAX {
331                assert_eq!(result.invmap[new_id as usize], old_id as u32);
332            }
333        }
334    }
335}