graphos_adapters/plugins/algorithms/
traversal.rs1use std::collections::VecDeque;
7use std::sync::OnceLock;
8
9use graphos_common::types::{NodeId, Value};
10use graphos_common::utils::error::Result;
11use graphos_common::utils::hash::{FxHashMap, FxHashSet};
12use graphos_core::graph::Direction;
13use graphos_core::graph::lpg::LpgStore;
14
15use super::super::{AlgorithmResult, ParameterDef, ParameterType, Parameters};
16use super::traits::{Control, GraphAlgorithm, NodeValueResultBuilder, TraversalEvent};
17
18pub fn bfs(store: &LpgStore, start: NodeId) -> Vec<NodeId> {
35 let mut visited = Vec::new();
36 bfs_with_visitor(store, start, |event| -> Control<()> {
37 if let TraversalEvent::Discover(node) = event {
38 visited.push(node);
39 }
40 Control::Continue
41 });
42 visited
43}
44
45pub fn bfs_with_visitor<B, F>(store: &LpgStore, start: NodeId, mut visitor: F) -> Option<B>
60where
61 F: FnMut(TraversalEvent) -> Control<B>,
62{
63 let mut discovered: FxHashSet<NodeId> = FxHashSet::default();
64 let mut queue: VecDeque<NodeId> = VecDeque::new();
65
66 if store.get_node(start).is_none() {
68 return None;
69 }
70
71 discovered.insert(start);
73 queue.push_back(start);
74
75 match visitor(TraversalEvent::Discover(start)) {
76 Control::Break(b) => return Some(b),
77 Control::Prune => {
78 match visitor(TraversalEvent::Finish(start)) {
80 Control::Break(b) => return Some(b),
81 _ => return None,
82 }
83 }
84 Control::Continue => {}
85 }
86
87 while let Some(node) = queue.pop_front() {
88 for (neighbor, edge_id) in store.edges_from(node, Direction::Outgoing) {
90 if discovered.insert(neighbor) {
91 match visitor(TraversalEvent::TreeEdge {
93 source: node,
94 target: neighbor,
95 edge: edge_id,
96 }) {
97 Control::Break(b) => return Some(b),
98 Control::Prune => continue, Control::Continue => {}
100 }
101
102 match visitor(TraversalEvent::Discover(neighbor)) {
103 Control::Break(b) => return Some(b),
104 Control::Prune => continue, Control::Continue => {}
106 }
107
108 queue.push_back(neighbor);
109 } else {
110 match visitor(TraversalEvent::NonTreeEdge {
112 source: node,
113 target: neighbor,
114 edge: edge_id,
115 }) {
116 Control::Break(b) => return Some(b),
117 _ => {}
118 }
119 }
120 }
121
122 match visitor(TraversalEvent::Finish(node)) {
124 Control::Break(b) => return Some(b),
125 _ => {}
126 }
127 }
128
129 None
130}
131
132pub fn bfs_layers(store: &LpgStore, start: NodeId) -> Vec<Vec<NodeId>> {
143 let mut layers: Vec<Vec<NodeId>> = Vec::new();
144 let mut discovered: FxHashSet<NodeId> = FxHashSet::default();
145 let mut current_layer: Vec<NodeId> = Vec::new();
146 let mut next_layer: Vec<NodeId> = Vec::new();
147
148 if store.get_node(start).is_none() {
149 return layers;
150 }
151
152 discovered.insert(start);
153 current_layer.push(start);
154
155 while !current_layer.is_empty() {
156 layers.push(current_layer.clone());
157
158 for &node in ¤t_layer {
159 for (neighbor, _) in store.edges_from(node, Direction::Outgoing) {
160 if discovered.insert(neighbor) {
161 next_layer.push(neighbor);
162 }
163 }
164 }
165
166 current_layer.clear();
167 std::mem::swap(&mut current_layer, &mut next_layer);
168 }
169
170 layers
171}
172
173#[derive(Clone, Copy, PartialEq, Eq)]
179enum NodeColor {
180 White,
182 Gray,
184 Black,
186}
187
188pub fn dfs(store: &LpgStore, start: NodeId) -> Vec<NodeId> {
201 let mut finished = Vec::new();
202 dfs_with_visitor(store, start, |event| -> Control<()> {
203 if let TraversalEvent::Finish(node) = event {
204 finished.push(node);
205 }
206 Control::Continue
207 });
208 finished
209}
210
211pub fn dfs_with_visitor<B, F>(store: &LpgStore, start: NodeId, mut visitor: F) -> Option<B>
225where
226 F: FnMut(TraversalEvent) -> Control<B>,
227{
228 let mut color: FxHashMap<NodeId, NodeColor> = FxHashMap::default();
229
230 let mut stack: Vec<(NodeId, Vec<(NodeId, graphos_common::types::EdgeId)>, usize)> = Vec::new();
233
234 if store.get_node(start).is_none() {
236 return None;
237 }
238
239 color.insert(start, NodeColor::Gray);
241 match visitor(TraversalEvent::Discover(start)) {
242 Control::Break(b) => return Some(b),
243 Control::Prune => {
244 color.insert(start, NodeColor::Black);
245 match visitor(TraversalEvent::Finish(start)) {
246 Control::Break(b) => return Some(b),
247 _ => return None,
248 }
249 }
250 Control::Continue => {}
251 }
252
253 let neighbors: Vec<_> = store.edges_from(start, Direction::Outgoing).collect();
254 stack.push((start, neighbors, 0));
255
256 while let Some((node, neighbors, idx)) = stack.last_mut() {
257 if *idx >= neighbors.len() {
258 let node = *node;
260 stack.pop();
261 color.insert(node, NodeColor::Black);
262 match visitor(TraversalEvent::Finish(node)) {
263 Control::Break(b) => return Some(b),
264 _ => {}
265 }
266 continue;
267 }
268
269 let (neighbor, edge_id) = neighbors[*idx];
270 *idx += 1;
271
272 match color.get(&neighbor).copied().unwrap_or(NodeColor::White) {
273 NodeColor::White => {
274 match visitor(TraversalEvent::TreeEdge {
276 source: *node,
277 target: neighbor,
278 edge: edge_id,
279 }) {
280 Control::Break(b) => return Some(b),
281 Control::Prune => continue,
282 Control::Continue => {}
283 }
284
285 color.insert(neighbor, NodeColor::Gray);
286 match visitor(TraversalEvent::Discover(neighbor)) {
287 Control::Break(b) => return Some(b),
288 Control::Prune => {
289 color.insert(neighbor, NodeColor::Black);
290 match visitor(TraversalEvent::Finish(neighbor)) {
291 Control::Break(b) => return Some(b),
292 _ => {}
293 }
294 continue;
295 }
296 Control::Continue => {}
297 }
298
299 let neighbor_neighbors: Vec<_> =
300 store.edges_from(neighbor, Direction::Outgoing).collect();
301 stack.push((neighbor, neighbor_neighbors, 0));
302 }
303 NodeColor::Gray => {
304 match visitor(TraversalEvent::BackEdge {
306 source: *node,
307 target: neighbor,
308 edge: edge_id,
309 }) {
310 Control::Break(b) => return Some(b),
311 _ => {}
312 }
313 }
314 NodeColor::Black => {
315 match visitor(TraversalEvent::NonTreeEdge {
317 source: *node,
318 target: neighbor,
319 edge: edge_id,
320 }) {
321 Control::Break(b) => return Some(b),
322 _ => {}
323 }
324 }
325 }
326 }
327
328 None
329}
330
331pub fn dfs_all(store: &LpgStore) -> Vec<NodeId> {
335 let mut finished = Vec::new();
336 let mut visited: FxHashSet<NodeId> = FxHashSet::default();
337
338 for node_id in store.node_ids() {
339 if visited.contains(&node_id) {
340 continue;
341 }
342
343 dfs_with_visitor(store, node_id, |event| -> Control<()> {
344 match event {
345 TraversalEvent::Discover(n) => {
346 visited.insert(n);
347 }
348 TraversalEvent::Finish(n) => {
349 finished.push(n);
350 }
351 _ => {}
352 }
353 Control::Continue
354 });
355 }
356
357 finished
358}
359
360static BFS_PARAMS: OnceLock<Vec<ParameterDef>> = OnceLock::new();
366
367fn bfs_params() -> &'static [ParameterDef] {
368 BFS_PARAMS.get_or_init(|| {
369 vec![ParameterDef {
370 name: "start".to_string(),
371 description: "Starting node ID".to_string(),
372 param_type: ParameterType::NodeId,
373 required: true,
374 default: None,
375 }]
376 })
377}
378
379pub struct BfsAlgorithm;
381
382impl GraphAlgorithm for BfsAlgorithm {
383 fn name(&self) -> &str {
384 "bfs"
385 }
386
387 fn description(&self) -> &str {
388 "Breadth-first search traversal from a starting node"
389 }
390
391 fn parameters(&self) -> &[ParameterDef] {
392 bfs_params()
393 }
394
395 fn execute(&self, store: &LpgStore, params: &Parameters) -> Result<AlgorithmResult> {
396 let start_id = params.get_int("start").ok_or_else(|| {
397 graphos_common::utils::error::Error::InvalidValue(
398 "start parameter required".to_string(),
399 )
400 })?;
401
402 let start = NodeId::new(start_id as u64);
403 let layers = bfs_layers(store, start);
404
405 let mut result = AlgorithmResult::new(vec!["node_id".to_string(), "distance".to_string()]);
406
407 for (distance, layer) in layers.iter().enumerate() {
408 for &node in layer {
409 result.add_row(vec![
410 Value::Int64(node.0 as i64),
411 Value::Int64(distance as i64),
412 ]);
413 }
414 }
415
416 Ok(result)
417 }
418}
419
420static DFS_PARAMS: OnceLock<Vec<ParameterDef>> = OnceLock::new();
422
423fn dfs_params() -> &'static [ParameterDef] {
424 DFS_PARAMS.get_or_init(|| {
425 vec![ParameterDef {
426 name: "start".to_string(),
427 description: "Starting node ID".to_string(),
428 param_type: ParameterType::NodeId,
429 required: true,
430 default: None,
431 }]
432 })
433}
434
435pub struct DfsAlgorithm;
437
438impl GraphAlgorithm for DfsAlgorithm {
439 fn name(&self) -> &str {
440 "dfs"
441 }
442
443 fn description(&self) -> &str {
444 "Depth-first search traversal from a starting node"
445 }
446
447 fn parameters(&self) -> &[ParameterDef] {
448 dfs_params()
449 }
450
451 fn execute(&self, store: &LpgStore, params: &Parameters) -> Result<AlgorithmResult> {
452 let start_id = params.get_int("start").ok_or_else(|| {
453 graphos_common::utils::error::Error::InvalidValue(
454 "start parameter required".to_string(),
455 )
456 })?;
457
458 let start = NodeId::new(start_id as u64);
459 let finished = dfs(store, start);
460
461 let mut builder = NodeValueResultBuilder::with_capacity("finish_order", finished.len());
462 for (order, node) in finished.iter().enumerate() {
463 builder.push(*node, Value::Int64(order as i64));
464 }
465
466 Ok(builder.build())
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 fn create_test_graph() -> LpgStore {
475 let store = LpgStore::new();
476
477 let n0 = store.create_node(&["Node"]);
483 let n1 = store.create_node(&["Node"]);
484 let n2 = store.create_node(&["Node"]);
485 let n3 = store.create_node(&["Node"]);
486 let n4 = store.create_node(&["Node"]);
487
488 store.create_edge(n0, n1, "EDGE");
489 store.create_edge(n0, n3, "EDGE");
490 store.create_edge(n1, n2, "EDGE");
491 store.create_edge(n1, n4, "EDGE");
492 store.create_edge(n3, n4, "EDGE");
493
494 store
495 }
496
497 #[test]
498 fn test_bfs_simple() {
499 let store = create_test_graph();
500 let visited = bfs(&store, NodeId::new(0));
501
502 assert!(!visited.is_empty());
503 assert_eq!(visited[0], NodeId::new(0));
504 }
506
507 #[test]
508 fn test_bfs_layers() {
509 let store = create_test_graph();
510 let layers = bfs_layers(&store, NodeId::new(0));
511
512 assert!(!layers.is_empty());
513 assert_eq!(layers[0], vec![NodeId::new(0)]);
514 }
516
517 #[test]
518 fn test_dfs_simple() {
519 let store = create_test_graph();
520 let finished = dfs(&store, NodeId::new(0));
521
522 assert!(!finished.is_empty());
523 }
525
526 #[test]
527 fn test_bfs_nonexistent_start() {
528 let store = LpgStore::new();
529 let visited = bfs(&store, NodeId::new(999));
530 assert!(visited.is_empty());
531 }
532
533 #[test]
534 fn test_dfs_nonexistent_start() {
535 let store = LpgStore::new();
536 let finished = dfs(&store, NodeId::new(999));
537 assert!(finished.is_empty());
538 }
539
540 #[test]
541 fn test_bfs_early_termination() {
542 let store = create_test_graph();
543 let target = NodeId::new(2);
544
545 let found = bfs_with_visitor(&store, NodeId::new(0), |event| {
546 if let TraversalEvent::Discover(node) = event {
547 if node == target {
548 return Control::Break(true);
549 }
550 }
551 Control::Continue
552 });
553
554 assert_eq!(found, Some(true));
555 }
556}