Skip to main content

ringkernel_graph/algorithms/
bfs.rs

1//! Breadth-first search algorithm.
2//!
3//! BFS computes shortest path distances from source nodes to all reachable nodes.
4//! The parallel version uses frontier-based expansion suitable for GPU execution.
5
6use crate::models::{CsrMatrix, Distance, NodeId};
7use crate::{GraphError, Result};
8
9/// BFS configuration.
10#[derive(Debug, Clone)]
11pub struct BfsConfig {
12    /// Maximum distance to explore.
13    pub max_distance: u32,
14    /// Enable bidirectional BFS for single-source single-target.
15    pub bidirectional: bool,
16}
17
18impl Default for BfsConfig {
19    fn default() -> Self {
20        Self {
21            max_distance: u32::MAX - 1,
22            bidirectional: false,
23        }
24    }
25}
26
27impl BfsConfig {
28    /// Create new BFS configuration.
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Set maximum distance.
34    pub fn with_max_distance(mut self, max: u32) -> Self {
35        self.max_distance = max;
36        self
37    }
38}
39
40/// Sequential BFS implementation.
41///
42/// Uses a queue-based approach with O(V + E) complexity.
43///
44/// # Arguments
45///
46/// * `adj` - Adjacency matrix in CSR format
47/// * `sources` - Source nodes to start BFS from
48///
49/// # Returns
50///
51/// Vector of distances, one per node. Distance::INFINITY for unreachable nodes.
52pub fn bfs_sequential(adj: &CsrMatrix, sources: &[NodeId]) -> Result<Vec<Distance>> {
53    bfs_sequential_with_config(adj, sources, &BfsConfig::default())
54}
55
56/// Sequential BFS with configuration.
57pub fn bfs_sequential_with_config(
58    adj: &CsrMatrix,
59    sources: &[NodeId],
60    config: &BfsConfig,
61) -> Result<Vec<Distance>> {
62    if adj.num_rows == 0 {
63        return Err(GraphError::EmptyGraph);
64    }
65
66    let mut distances = vec![Distance::INFINITY; adj.num_rows];
67    let mut queue = std::collections::VecDeque::new();
68
69    // Initialize sources
70    for &src in sources {
71        if src.0 as usize >= adj.num_rows {
72            return Err(GraphError::InvalidNodeId(src.0 as u64));
73        }
74        if distances[src.0 as usize] == Distance::INFINITY {
75            distances[src.0 as usize] = Distance::ZERO;
76            queue.push_back(src);
77        }
78    }
79
80    // BFS traversal
81    while let Some(node) = queue.pop_front() {
82        let current_dist = distances[node.0 as usize];
83
84        if current_dist.0 >= config.max_distance {
85            continue;
86        }
87
88        for &neighbor_id in adj.neighbors(node) {
89            let neighbor = neighbor_id as usize;
90            if distances[neighbor] == Distance::INFINITY {
91                distances[neighbor] = current_dist.increment();
92                queue.push_back(NodeId(neighbor_id));
93            }
94        }
95    }
96
97    Ok(distances)
98}
99
100/// Parallel BFS implementation (frontier-based).
101///
102/// Uses level-synchronous approach suitable for GPU parallelization:
103/// 1. Process all nodes in current frontier in parallel
104/// 2. Collect next frontier
105/// 3. Repeat until no more nodes to explore
106///
107/// On CPU, this uses rayon for parallelism (when available).
108/// The same algorithm structure maps directly to GPU kernels.
109pub fn bfs_parallel(adj: &CsrMatrix, sources: &[NodeId]) -> Result<Vec<Distance>> {
110    bfs_parallel_with_config(adj, sources, &BfsConfig::default())
111}
112
113/// Parallel BFS with configuration.
114pub fn bfs_parallel_with_config(
115    adj: &CsrMatrix,
116    sources: &[NodeId],
117    config: &BfsConfig,
118) -> Result<Vec<Distance>> {
119    if adj.num_rows == 0 {
120        return Err(GraphError::EmptyGraph);
121    }
122
123    let mut distances = vec![Distance::INFINITY; adj.num_rows];
124
125    // Initialize sources
126    let mut frontier: Vec<NodeId> = Vec::new();
127    for &src in sources {
128        if src.0 as usize >= adj.num_rows {
129            return Err(GraphError::InvalidNodeId(src.0 as u64));
130        }
131        if distances[src.0 as usize] == Distance::INFINITY {
132            distances[src.0 as usize] = Distance::ZERO;
133            frontier.push(src);
134        }
135    }
136
137    let mut level = 0u32;
138
139    // Level-synchronous BFS
140    while !frontier.is_empty() && level < config.max_distance {
141        let mut next_frontier = Vec::new();
142        level += 1;
143
144        // Process all frontier nodes (parallelizable)
145        for &node in &frontier {
146            for &neighbor_id in adj.neighbors(node) {
147                let neighbor = neighbor_id as usize;
148                // Atomic compare-and-swap in parallel version
149                if distances[neighbor] == Distance::INFINITY {
150                    distances[neighbor] = Distance::new(level);
151                    next_frontier.push(NodeId(neighbor_id));
152                }
153            }
154        }
155
156        frontier = next_frontier;
157    }
158
159    Ok(distances)
160}
161
162/// Multi-source BFS returning parent pointers for path reconstruction.
163pub fn bfs_with_parents(
164    adj: &CsrMatrix,
165    sources: &[NodeId],
166) -> Result<(Vec<Distance>, Vec<NodeId>)> {
167    if adj.num_rows == 0 {
168        return Err(GraphError::EmptyGraph);
169    }
170
171    let mut distances = vec![Distance::INFINITY; adj.num_rows];
172    let mut parents = vec![NodeId::INVALID; adj.num_rows];
173    let mut queue = std::collections::VecDeque::new();
174
175    // Initialize sources (they are their own parents)
176    for &src in sources {
177        if src.0 as usize >= adj.num_rows {
178            return Err(GraphError::InvalidNodeId(src.0 as u64));
179        }
180        if distances[src.0 as usize] == Distance::INFINITY {
181            distances[src.0 as usize] = Distance::ZERO;
182            parents[src.0 as usize] = src;
183            queue.push_back(src);
184        }
185    }
186
187    // BFS traversal
188    while let Some(node) = queue.pop_front() {
189        let current_dist = distances[node.0 as usize];
190
191        for &neighbor_id in adj.neighbors(node) {
192            let neighbor = neighbor_id as usize;
193            if distances[neighbor] == Distance::INFINITY {
194                distances[neighbor] = current_dist.increment();
195                parents[neighbor] = node;
196                queue.push_back(NodeId(neighbor_id));
197            }
198        }
199    }
200
201    Ok((distances, parents))
202}
203
204/// Reconstruct path from source to target using parent pointers.
205pub fn reconstruct_path(parents: &[NodeId], target: NodeId) -> Option<Vec<NodeId>> {
206    let target_idx = target.0 as usize;
207    if target_idx >= parents.len() || !parents[target_idx].is_valid() {
208        return None;
209    }
210
211    let mut path = vec![target];
212    let mut current = target;
213
214    // Walk back to source
215    while parents[current.0 as usize] != current {
216        current = parents[current.0 as usize];
217        if !current.is_valid() {
218            return None;
219        }
220        path.push(current);
221    }
222
223    path.reverse();
224    Some(path)
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    fn make_line_graph(n: usize) -> CsrMatrix {
232        // 0 -> 1 -> 2 -> ... -> n-1
233        let edges: Vec<_> = (0..n - 1).map(|i| (i as u32, i as u32 + 1)).collect();
234        CsrMatrix::from_edges(n, &edges)
235    }
236
237    fn make_star_graph(n: usize) -> CsrMatrix {
238        // 0 -> 1, 0 -> 2, ..., 0 -> n-1
239        let edges: Vec<_> = (1..n).map(|i| (0, i as u32)).collect();
240        CsrMatrix::from_edges(n, &edges)
241    }
242
243    #[test]
244    fn test_bfs_line_graph() {
245        let adj = make_line_graph(5);
246        let distances = bfs_sequential(&adj, &[NodeId(0)]).unwrap();
247
248        assert_eq!(distances[0], Distance::new(0));
249        assert_eq!(distances[1], Distance::new(1));
250        assert_eq!(distances[2], Distance::new(2));
251        assert_eq!(distances[3], Distance::new(3));
252        assert_eq!(distances[4], Distance::new(4));
253    }
254
255    #[test]
256    fn test_bfs_star_graph() {
257        let adj = make_star_graph(5);
258        let distances = bfs_sequential(&adj, &[NodeId(0)]).unwrap();
259
260        assert_eq!(distances[0], Distance::new(0));
261        for d in distances.iter().take(5).skip(1) {
262            assert_eq!(*d, Distance::new(1));
263        }
264    }
265
266    #[test]
267    fn test_bfs_multi_source() {
268        // Line graph: 0 -> 1 -> 2 -> 3 -> 4
269        let adj = make_line_graph(5);
270        let distances = bfs_sequential(&adj, &[NodeId(0), NodeId(4)]).unwrap();
271
272        // Node 4 is a source, so distance 0
273        assert_eq!(distances[4], Distance::new(0));
274        // Node 0 is a source
275        assert_eq!(distances[0], Distance::new(0));
276    }
277
278    #[test]
279    fn test_bfs_unreachable() {
280        // Two disconnected components: 0 -> 1, 2 -> 3
281        let edges = [(0, 1), (2, 3)];
282        let adj = CsrMatrix::from_edges(4, &edges);
283
284        let distances = bfs_sequential(&adj, &[NodeId(0)]).unwrap();
285
286        assert_eq!(distances[0], Distance::new(0));
287        assert_eq!(distances[1], Distance::new(1));
288        assert_eq!(distances[2], Distance::INFINITY);
289        assert_eq!(distances[3], Distance::INFINITY);
290    }
291
292    #[test]
293    fn test_bfs_parallel_same_as_sequential() {
294        let adj = make_line_graph(10);
295
296        let seq = bfs_sequential(&adj, &[NodeId(0)]).unwrap();
297        let par = bfs_parallel(&adj, &[NodeId(0)]).unwrap();
298
299        assert_eq!(seq, par);
300    }
301
302    #[test]
303    fn test_bfs_max_distance() {
304        let adj = make_line_graph(10);
305        let config = BfsConfig::new().with_max_distance(3);
306        let distances = bfs_sequential_with_config(&adj, &[NodeId(0)], &config).unwrap();
307
308        assert_eq!(distances[0], Distance::new(0));
309        assert_eq!(distances[1], Distance::new(1));
310        assert_eq!(distances[2], Distance::new(2));
311        assert_eq!(distances[3], Distance::new(3));
312        // Beyond max_distance
313        assert_eq!(distances[4], Distance::INFINITY);
314    }
315
316    #[test]
317    fn test_bfs_with_parents() {
318        let adj = make_line_graph(5);
319        let (distances, parents) = bfs_with_parents(&adj, &[NodeId(0)]).unwrap();
320
321        assert_eq!(distances[4], Distance::new(4));
322
323        // Reconstruct path
324        let path = reconstruct_path(&parents, NodeId(4)).unwrap();
325        assert_eq!(
326            path,
327            vec![NodeId(0), NodeId(1), NodeId(2), NodeId(3), NodeId(4)]
328        );
329    }
330
331    #[test]
332    fn test_empty_graph_error() {
333        let adj = CsrMatrix::empty(0);
334        let result = bfs_sequential(&adj, &[NodeId(0)]);
335        assert!(matches!(result, Err(GraphError::EmptyGraph)));
336    }
337
338    #[test]
339    fn test_invalid_source_error() {
340        let adj = make_line_graph(3);
341        let result = bfs_sequential(&adj, &[NodeId(100)]);
342        assert!(matches!(result, Err(GraphError::InvalidNodeId(_))));
343    }
344}