Skip to main content

rust_igraph/algorithms/operators/
connect_neighborhood.rs

1//! Connect-neighborhood / graph-power operators (ALGO-OP-011).
2//!
3//! Connects each vertex to all vertices reachable within k steps.
4
5use std::collections::VecDeque;
6
7use crate::core::{Graph, IgraphResult, VertexId};
8
9/// Returns a new graph where each vertex is connected to all vertices
10/// reachable within `order` steps in the original graph.
11///
12/// Existing edges are preserved. Only new edges (not already present) are
13/// added. Self-loops are never added. For undirected graphs, only one edge
14/// per pair is created.
15///
16/// This is equivalent to computing the k-th power of a graph and simplifying
17/// (removing multi-edges and self-loops).
18///
19/// # Arguments
20///
21/// * `graph` — the input graph (undirected).
22/// * `order` — the maximum distance within which vertices are connected.
23///   Order < 2 leaves the graph unchanged.
24///
25/// # Examples
26///
27/// ```
28/// use rust_igraph::{Graph, connect_neighborhood};
29///
30/// // Path graph: 0-1-2-3
31/// let mut g = Graph::with_vertices(4);
32/// g.add_edge(0, 1).unwrap();
33/// g.add_edge(1, 2).unwrap();
34/// g.add_edge(2, 3).unwrap();
35///
36/// let cg = connect_neighborhood(&g, 2).unwrap();
37/// assert_eq!(cg.vcount(), 4);
38/// // Original 3 edges + new edges: (0,2), (1,3) = 5 total
39/// assert_eq!(cg.ecount(), 5);
40/// ```
41pub fn connect_neighborhood(graph: &Graph, order: u32) -> IgraphResult<Graph> {
42    let n = graph.vcount();
43    let directed = graph.is_directed();
44
45    if order < 2 || n == 0 {
46        // Return a structural copy
47        let mut result = Graph::new(n, directed)?;
48        let ecount = graph.ecount();
49        let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
50        for eid in 0..ecount {
51            #[allow(clippy::cast_possible_truncation)]
52            let eid_u32 = eid as u32;
53            edges.push(graph.edge(eid_u32)?);
54        }
55        result.add_edges(edges)?;
56        return Ok(result);
57    }
58
59    // Build adjacency list from the original graph
60    let adj = build_adjacency_list(graph, directed)?;
61
62    // Collect new edges via BFS from each vertex
63    let mut new_edges: Vec<(VertexId, VertexId)> = Vec::new();
64    let mut visited = vec![0u32; n as usize];
65
66    for i in 0..n {
67        let marker = i + 1;
68        visited[i as usize] = marker;
69
70        // Mark direct neighbors as visited
71        for &nei in &adj[i as usize] {
72            visited[nei as usize] = marker;
73        }
74
75        // BFS to find vertices at distance 2..order
76        let mut queue: VecDeque<(VertexId, u32)> = VecDeque::new();
77        for &nei in &adj[i as usize] {
78            queue.push_back((nei, 1));
79        }
80
81        while let Some((node, dist)) = queue.pop_front() {
82            if dist >= order {
83                continue;
84            }
85            for &nei in &adj[node as usize] {
86                if visited[nei as usize] != marker {
87                    visited[nei as usize] = marker;
88                    if directed || i < nei {
89                        new_edges.push((i, nei));
90                    }
91                    if dist + 1 < order {
92                        queue.push_back((nei, dist + 1));
93                    }
94                }
95            }
96        }
97    }
98
99    // Build result with original edges + new edges
100    let ecount = graph.ecount();
101    let mut all_edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount + new_edges.len());
102    for eid in 0..ecount {
103        #[allow(clippy::cast_possible_truncation)]
104        let eid_u32 = eid as u32;
105        all_edges.push(graph.edge(eid_u32)?);
106    }
107    all_edges.extend(new_edges);
108
109    let mut result = Graph::new(n, directed)?;
110    result.add_edges(all_edges)?;
111    Ok(result)
112}
113
114/// Returns the k-th power of a graph as a simple graph.
115///
116/// In the k-th power, vertex u is connected to vertex v if v is reachable
117/// from u within at most k steps. The result is always simple (no self-loops,
118/// no multi-edges).
119///
120/// By convention, the zeroth power has no edges. The first power is the
121/// simplified original graph.
122///
123/// # Arguments
124///
125/// * `graph` — the input graph.
126/// * `order` — non-negative integer, the power to raise the graph to.
127///
128/// # Examples
129///
130/// ```
131/// use rust_igraph::{Graph, graph_power};
132///
133/// // Path: 0-1-2-3
134/// let mut g = Graph::with_vertices(4);
135/// g.add_edge(0, 1).unwrap();
136/// g.add_edge(1, 2).unwrap();
137/// g.add_edge(2, 3).unwrap();
138///
139/// let p2 = graph_power(&g, 2).unwrap();
140/// assert_eq!(p2.vcount(), 4);
141/// // Edges: (0,1),(0,2),(1,2),(1,3),(2,3) = 5
142/// assert_eq!(p2.ecount(), 5);
143/// ```
144pub fn graph_power(graph: &Graph, order: u32) -> IgraphResult<Graph> {
145    let n = graph.vcount();
146    let directed = graph.is_directed();
147
148    if order == 0 {
149        return Graph::new(n, directed);
150    }
151
152    // Build adjacency (simple, no loops) from original
153    let adj = build_simple_adjacency_list(graph, directed)?;
154
155    if order == 1 {
156        // First power = simplified original
157        return build_graph_from_adj(&adj, n, directed);
158    }
159
160    // BFS from each vertex to find all within `order` distance
161    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
162    let mut visited = vec![0u32; n as usize];
163
164    for i in 0..n {
165        let marker = i + 1;
166        visited[i as usize] = marker;
167
168        let mut queue: VecDeque<(VertexId, u32)> = VecDeque::new();
169
170        // Seed with direct neighbors
171        for &nei in &adj[i as usize] {
172            if visited[nei as usize] != marker {
173                visited[nei as usize] = marker;
174                if directed || i < nei {
175                    edges.push((i, nei));
176                }
177                if order > 1 {
178                    queue.push_back((nei, 1));
179                }
180            }
181        }
182
183        // Expand BFS
184        while let Some((node, dist)) = queue.pop_front() {
185            if dist >= order {
186                continue;
187            }
188            for &nei in &adj[node as usize] {
189                if visited[nei as usize] != marker {
190                    visited[nei as usize] = marker;
191                    if directed || i < nei {
192                        edges.push((i, nei));
193                    }
194                    if dist + 1 < order {
195                        queue.push_back((nei, dist + 1));
196                    }
197                }
198            }
199        }
200    }
201
202    let mut result = Graph::new(n, directed)?;
203    result.add_edges(edges)?;
204    Ok(result)
205}
206
207fn build_adjacency_list(graph: &Graph, _directed: bool) -> IgraphResult<Vec<Vec<VertexId>>> {
208    let n = graph.vcount() as usize;
209    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
210    let ecount = graph.ecount();
211
212    for eid in 0..ecount {
213        #[allow(clippy::cast_possible_truncation)]
214        let eid_u32 = eid as u32;
215        let (src, tgt) = graph.edge(eid_u32)?;
216        adj[src as usize].push(tgt);
217        if !graph.is_directed() && src != tgt {
218            adj[tgt as usize].push(src);
219        }
220    }
221    Ok(adj)
222}
223
224fn build_simple_adjacency_list(graph: &Graph, _directed: bool) -> IgraphResult<Vec<Vec<VertexId>>> {
225    let n = graph.vcount() as usize;
226    let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
227    let ecount = graph.ecount();
228
229    for eid in 0..ecount {
230        #[allow(clippy::cast_possible_truncation)]
231        let eid_u32 = eid as u32;
232        let (src, tgt) = graph.edge(eid_u32)?;
233        if src == tgt {
234            continue; // skip self-loops
235        }
236        adj[src as usize].push(tgt);
237        if !graph.is_directed() {
238            adj[tgt as usize].push(src);
239        }
240    }
241
242    // Deduplicate
243    for list in &mut adj {
244        list.sort_unstable();
245        list.dedup();
246    }
247    Ok(adj)
248}
249
250fn build_graph_from_adj(adj: &[Vec<VertexId>], n: u32, directed: bool) -> IgraphResult<Graph> {
251    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
252    for (i, neighbors) in adj.iter().enumerate() {
253        #[allow(clippy::cast_possible_truncation)]
254        let i_u32 = i as u32;
255        for &nei in neighbors {
256            if directed || i_u32 < nei {
257                edges.push((i_u32, nei));
258            }
259        }
260    }
261    let mut result = Graph::new(n, directed)?;
262    result.add_edges(edges)?;
263    Ok(result)
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn test_connect_neighborhood_path() {
272        // Path: 0-1-2-3
273        let mut g = Graph::with_vertices(4);
274        g.add_edge(0, 1).unwrap();
275        g.add_edge(1, 2).unwrap();
276        g.add_edge(2, 3).unwrap();
277
278        let cg = connect_neighborhood(&g, 2).unwrap();
279        assert_eq!(cg.vcount(), 4);
280        // 3 original + 2 new (0-2, 1-3)
281        assert_eq!(cg.ecount(), 5);
282    }
283
284    #[test]
285    fn test_connect_neighborhood_order_1() {
286        let mut g = Graph::with_vertices(3);
287        g.add_edge(0, 1).unwrap();
288
289        let cg = connect_neighborhood(&g, 1).unwrap();
290        assert_eq!(cg.ecount(), 1); // unchanged
291    }
292
293    #[test]
294    fn test_connect_neighborhood_order_0() {
295        let mut g = Graph::with_vertices(3);
296        g.add_edge(0, 1).unwrap();
297
298        let cg = connect_neighborhood(&g, 0).unwrap();
299        assert_eq!(cg.ecount(), 1); // unchanged
300    }
301
302    #[test]
303    fn test_connect_neighborhood_complete() {
304        // K3 — order 2 should not add edges (already fully connected)
305        let mut g = Graph::with_vertices(3);
306        g.add_edge(0, 1).unwrap();
307        g.add_edge(0, 2).unwrap();
308        g.add_edge(1, 2).unwrap();
309
310        let cg = connect_neighborhood(&g, 2).unwrap();
311        assert_eq!(cg.ecount(), 3);
312    }
313
314    #[test]
315    fn test_connect_neighborhood_large_order() {
316        // Path: 0-1-2-3, order=10 → complete graph K4 = 6 edges
317        let mut g = Graph::with_vertices(4);
318        g.add_edge(0, 1).unwrap();
319        g.add_edge(1, 2).unwrap();
320        g.add_edge(2, 3).unwrap();
321
322        let cg = connect_neighborhood(&g, 10).unwrap();
323        assert_eq!(cg.ecount(), 6); // K4
324    }
325
326    #[test]
327    fn test_connect_neighborhood_empty() {
328        let g = Graph::with_vertices(0);
329        let cg = connect_neighborhood(&g, 5).unwrap();
330        assert_eq!(cg.vcount(), 0);
331    }
332
333    #[test]
334    fn test_connect_neighborhood_isolated() {
335        let g = Graph::with_vertices(5);
336        let cg = connect_neighborhood(&g, 3).unwrap();
337        assert_eq!(cg.ecount(), 0);
338    }
339
340    #[test]
341    fn test_graph_power_zero() {
342        let mut g = Graph::with_vertices(3);
343        g.add_edge(0, 1).unwrap();
344
345        let p = graph_power(&g, 0).unwrap();
346        assert_eq!(p.vcount(), 3);
347        assert_eq!(p.ecount(), 0);
348    }
349
350    #[test]
351    fn test_graph_power_one() {
352        let mut g = Graph::with_vertices(3);
353        g.add_edge(0, 1).unwrap();
354        g.add_edge(1, 2).unwrap();
355
356        let p = graph_power(&g, 1).unwrap();
357        assert_eq!(p.vcount(), 3);
358        assert_eq!(p.ecount(), 2);
359    }
360
361    #[test]
362    fn test_graph_power_two_path() {
363        // Path: 0-1-2-3
364        let mut g = Graph::with_vertices(4);
365        g.add_edge(0, 1).unwrap();
366        g.add_edge(1, 2).unwrap();
367        g.add_edge(2, 3).unwrap();
368
369        let p = graph_power(&g, 2).unwrap();
370        assert_eq!(p.vcount(), 4);
371        // (0,1),(0,2),(1,2),(1,3),(2,3) = 5
372        assert_eq!(p.ecount(), 5);
373    }
374
375    #[test]
376    fn test_graph_power_removes_self_loops() {
377        let mut g = Graph::with_vertices(3);
378        g.add_edge(0, 0).unwrap(); // self-loop
379        g.add_edge(0, 1).unwrap();
380        g.add_edge(1, 2).unwrap();
381
382        let p = graph_power(&g, 1).unwrap();
383        // Self-loop removed
384        assert_eq!(p.ecount(), 2);
385    }
386
387    #[test]
388    fn test_graph_power_removes_multi_edges() {
389        let mut g = Graph::with_vertices(3);
390        g.add_edge(0, 1).unwrap();
391        g.add_edge(0, 1).unwrap(); // duplicate
392        g.add_edge(1, 2).unwrap();
393
394        let p = graph_power(&g, 1).unwrap();
395        assert_eq!(p.ecount(), 2);
396    }
397
398    #[test]
399    fn test_graph_power_directed() {
400        // Directed path: 0→1→2
401        let mut g = Graph::new(3, true).unwrap();
402        g.add_edge(0, 1).unwrap();
403        g.add_edge(1, 2).unwrap();
404
405        let p = graph_power(&g, 2).unwrap();
406        assert!(p.is_directed());
407        assert_eq!(p.vcount(), 3);
408        // (0→1),(1→2),(0→2) = 3
409        assert_eq!(p.ecount(), 3);
410    }
411}