Skip to content

Commit 98e7c60

Browse files
Merge pull request #264 from marvin-hansen/main
Added Tarjan's algorithm to UltraGraph.
2 parents c6bcaa2 + 9426636 commit 98e7c60

File tree

7 files changed

+328
-12
lines changed

7 files changed

+328
-12
lines changed

ultragraph/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,46 @@ This is the high-performance, read-only state.
7373
If you need to make further changes after a period of analysis, `g.unfreeze()` efficiently converts the `CsmGraph` back
7474
into a `DynamicGraph`, allowing the cycle of mutation and analysis to begin again.
7575

76+
## ⚙️ Graph Algorithms
77+
78+
The `UltraGraph` crate provides a suite of high-performance, read-only analytical algorithms for
79+
graph analysis. These algorithms are implemented on static, optimized graph structures for efficient
80+
computation.
81+
82+
* **`find_cycle()`**: Finds a single cycle in the
83+
graph and returns the path of nodes that form it.
84+
Returns `None` if the graph is a Directed Acyclic Graph
85+
(DAG).
86+
87+
* **`has_cycle()`**: Checks if the graph contains any
88+
directed cycles.
89+
90+
* **`topological_sort()`**: Computes a topological
91+
sort of the graph if it is a DAG. Returns `None` if the
92+
graph contains a cycle.
93+
94+
* **`is_reachable(start_index, stop_index)`**: Checks
95+
if a path of any length exists from a start node to a
96+
stop node.
97+
98+
* **`shortest_path_len(start_index, stop_index)`**:
99+
Returns the length (number of nodes) of the shortest
100+
path from a start node to a stop node.
101+
102+
* **`shortest_path(start_index, stop_index)`**: Finds
103+
the complete shortest path (sequence of nodes) from a
104+
start node to a stop node.
105+
106+
* **`shortest_weighted_path(start_index, stop_
107+
index)`**: Finds the shortest path in a weighted graph
108+
using Dijkstra's algorithm, returning the path and its
109+
total cost.
110+
111+
* **`strongly_connected_components()`**: Finds all
112+
Strongly Connected Components (SCCs) in the graph using
113+
Tarjan's algorithm, returning a list of node sets, where
114+
each set represents an SCC.
115+
76116
## 🚀 Benchmark Results
77117

78118
### Dynamic Graph

ultragraph/src/errors/graph_error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ pub enum GraphError {
2929

3030
/// Root node already exists
3131
RootNodeAlreadyExists,
32+
33+
/// Graph algorithm error
34+
AlgorithmError(&'static str),
3235
}
3336

3437
impl fmt::Display for GraphError {
@@ -67,6 +70,10 @@ impl fmt::Display for GraphError {
6770
Self::RootNodeAlreadyExists => {
6871
write!(f, "Root node already exists")
6972
}
73+
74+
Self::AlgorithmError(e) => {
75+
write!(f, "AlgorithmError: {e}")
76+
}
7077
}
7178
}
7279
}

ultragraph/src/traits/graph_algo.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,10 @@ pub trait GraphAlgorithms<N, W>: GraphView<N, W> {
6666
where
6767
W: Copy + Ord + Default + std::ops::Add<Output = W>;
6868

69-
// Implement later.
70-
// /// Finds all Strongly Connected Components in the graph using Tarjan's algorithm.
71-
// ///
72-
// /// # Returns
73-
// /// A vector of vectors, where each inner vector is a list of node indices
74-
// /// belonging to a single SCC.
75-
// fn strongly_connected_components(&self) -> Result<Vec<Vec<usize>>, GraphError>;
69+
/// Finds all Strongly Connected Components in the graph using Tarjan's algorithm.
70+
///
71+
/// # Returns
72+
/// A vector of vectors, where each inner vector is a list of node indices
73+
/// belonging to a single SCC.
74+
fn strongly_connected_components(&self) -> Result<Vec<Vec<usize>>, GraphError>;
7675
}

ultragraph/src/types/storage/graph_csm/graph_csm_algo.rs

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use crate::{CsmGraph, GraphAlgorithms, GraphError, GraphView};
2+
use std::cmp::Reverse;
3+
use std::collections::BinaryHeap;
24
use std::collections::VecDeque;
35
use std::slice;
46

@@ -271,7 +273,6 @@ where
271273
Ok(Some(path))
272274
}
273275

274-
/// Finds the shortest path between two nodes in a weighted graph using Dijkstra's algorithm.
275276
fn shortest_weighted_path(
276277
&self,
277278
start_index: usize,
@@ -280,9 +281,6 @@ where
280281
where
281282
W: Copy + Ord + Default + std::ops::Add<Output = W>,
282283
{
283-
use std::cmp::Reverse;
284-
use std::collections::BinaryHeap;
285-
286284
if !self.contains_node(start_index) || !self.contains_node(stop_index) {
287285
return Ok(None);
288286
}
@@ -329,7 +327,7 @@ where
329327
let weight = self.forward_edges.weights[i];
330328
let new_dist = dist + weight;
331329

332-
if distances[v].map_or(true, |d| new_dist < d) {
330+
if distances[v].is_none() || new_dist < distances[v].unwrap() {
333331
distances[v] = Some(new_dist);
334332
predecessors[v] = Some(u);
335333
pq.push((Reverse(new_dist), v));
@@ -339,4 +337,110 @@ where
339337

340338
Ok(None)
341339
}
340+
341+
/// Finds all Strongly Connected Components in the graph using Tarjan's algorithm.
342+
///
343+
/// # Returns
344+
/// A vector of vectors, where each inner vector is a list of node indices
345+
/// belonging to a single SCC.
346+
fn strongly_connected_components(&self) -> Result<Vec<Vec<usize>>, GraphError> {
347+
let num_nodes = self.number_nodes();
348+
if num_nodes == 0 {
349+
return Ok(Vec::new());
350+
}
351+
352+
let mut dfs_num: Vec<Option<usize>> = vec![None; num_nodes];
353+
let mut low_link: Vec<Option<usize>> = vec![None; num_nodes];
354+
let mut on_stack: Vec<bool> = vec![false; num_nodes];
355+
let mut tarjan_stack: Vec<usize> = Vec::new();
356+
let mut time: usize = 0;
357+
let mut sccs: Vec<Vec<usize>> = Vec::new();
358+
359+
// Stack for iterative DFS. Stores (node_index, iterator_over_neighbors)
360+
// The iterator is used to keep track of which neighbor to visit next.
361+
let mut dfs_stack: Vec<(usize, slice::Iter<'_, usize>)> = Vec::new();
362+
363+
for i in 0..num_nodes {
364+
if dfs_num[i].is_none() {
365+
// Start DFS from node i
366+
let start_offset = self.forward_edges.offsets[i];
367+
let end_offset = self.forward_edges.offsets[i + 1];
368+
let neighbors_iter = self.forward_edges.targets[start_offset..end_offset].iter();
369+
dfs_stack.push((i, neighbors_iter));
370+
371+
// Simulate recursion
372+
while let Some((u, neighbors)) = dfs_stack.last_mut() {
373+
// On first visit to u (pre-order traversal)
374+
if dfs_num[*u].is_none() {
375+
dfs_num[*u] = Some(time);
376+
low_link[*u] = Some(time);
377+
time += 1;
378+
tarjan_stack.push(*u);
379+
on_stack[*u] = true;
380+
}
381+
382+
// Process neighbors
383+
if let Some(&v) = neighbors.next() {
384+
if dfs_num[v].is_none() {
385+
// Neighbor v not visited, "recurse"
386+
let v_start_offset = self.forward_edges.offsets[v];
387+
let v_end_offset = self.forward_edges.offsets[v + 1];
388+
let v_neighbors_iter =
389+
self.forward_edges.targets[v_start_offset..v_end_offset].iter();
390+
dfs_stack.push((v, v_neighbors_iter));
391+
} else if on_stack[v] {
392+
// Neighbor v is on stack, back-edge
393+
low_link[*u] = Some(
394+
low_link[*u]
395+
.ok_or(GraphError::AlgorithmError("low_link for u not set"))?
396+
.min(dfs_num[v].ok_or(GraphError::AlgorithmError(
397+
"dfs_num for v not set",
398+
))?),
399+
);
400+
}
401+
} else {
402+
// All neighbors processed, "return" from u (post-order traversal)
403+
404+
// If u is the root of an SCC
405+
if dfs_num[*u] == low_link[*u] {
406+
let mut current_scc = Vec::new();
407+
loop {
408+
let node = tarjan_stack
409+
.pop()
410+
.ok_or(GraphError::AlgorithmError("tarjan_stack is empty"))?;
411+
on_stack[node] = false;
412+
current_scc.push(node);
413+
if node == *u {
414+
break;
415+
}
416+
}
417+
current_scc.reverse(); // SCC nodes are popped in reverse order
418+
sccs.push(current_scc);
419+
}
420+
421+
// Update parent's low_link if u is not the root of an SCC
422+
// This must happen AFTER processing the current node's SCC, but BEFORE popping u from dfs_stack
423+
let popped_u = dfs_stack
424+
.pop()
425+
.ok_or(GraphError::AlgorithmError("DFS stack was empty in a post-order step, which should be impossible"))?
426+
.0;
427+
428+
if let Some((parent_node, _)) = dfs_stack.last() {
429+
low_link[*parent_node] = Some(
430+
low_link[*parent_node]
431+
.ok_or(GraphError::AlgorithmError(
432+
"low_link for parent_node not set",
433+
))?
434+
.min(low_link[popped_u].ok_or(GraphError::AlgorithmError(
435+
"low_link for popped_u not set",
436+
))?),
437+
);
438+
}
439+
}
440+
}
441+
}
442+
}
443+
444+
Ok(sccs)
445+
}
342446
}

ultragraph/src/types/ultra_graph/graph_algo.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,22 @@ where
139139
GraphState::Dynamic(_) => Err(GraphError::GraphNotFrozen),
140140
}
141141
}
142+
143+
/// Finds the strongly connected components (SCCs) of the graph.
144+
///
145+
/// # Preconditions
146+
/// This high-performance operation is only available when the graph is in a `Static` (frozen) state.
147+
///
148+
/// # Returns
149+
/// A `Result` containing a `Vec<Vec<usize>>`, where each inner `Vec<usize>` represents
150+
/// a strongly connected component as a list of node indices.
151+
///
152+
/// # Errors
153+
/// Returns `GraphError::GraphNotFrozen` if the graph is in a `Dynamic` state.
154+
fn strongly_connected_components(&self) -> Result<Vec<Vec<usize>>, GraphError> {
155+
match &self.state {
156+
GraphState::Static(g) => g.strongly_connected_components(),
157+
GraphState::Dynamic(_) => Err(GraphError::GraphNotFrozen),
158+
}
159+
}
142160
}

ultragraph/tests/errors/error_tests.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,65 @@ fn test_error_traits() {
9292
let copied_error = error1;
9393
assert_eq!(error1, copied_error);
9494
}
95+
96+
/// Tests the creation and content of the AlgorithmError variant.
97+
#[test]
98+
fn test_algorithm_error_creation() {
99+
let err_msg = "Pathfinding failed: graph is disconnected";
100+
let error = GraphError::AlgorithmError(err_msg);
101+
102+
// You can use a `match` or `if let` to inspect the enum variant and its data.
103+
if let GraphError::AlgorithmError(msg) = error {
104+
assert_eq!(
105+
msg, err_msg,
106+
"The error message should be stored correctly."
107+
);
108+
} else {
109+
panic!("Expected GraphError::AlgorithmError, but got a different variant.");
110+
}
111+
}
112+
113+
/// Tests the `Display` trait implementation for user-friendly output.
114+
#[test]
115+
fn test_algorithm_error_display_formatting() {
116+
let err_msg = "Dijkstra's algorithm requires non-negative weights";
117+
let error = GraphError::AlgorithmError(err_msg);
118+
let expected_display_msg = format!("AlgorithmError: {err_msg}");
119+
120+
assert_eq!(error.to_string(), expected_display_msg);
121+
}
122+
123+
/// Tests the `Debug` trait implementation for developer-focused output.
124+
#[test]
125+
fn test_algorithm_error_debug_formatting() {
126+
let err_msg = "A cycle was detected in a directed acyclic graph (DAG)";
127+
let error = GraphError::AlgorithmError(err_msg);
128+
// The `Debug` format is derived automatically and includes the variant name.
129+
let expected_debug_msg = format!("AlgorithmError(\"{err_msg}\")");
130+
131+
assert_eq!(format!("{error:?}"), expected_debug_msg);
132+
}
133+
134+
/// Demonstrates how to check for a specific error variant within a `Result`.
135+
#[test]
136+
fn test_algorithm_error_in_a_result() {
137+
let err_msg = "Invalid start node for traversal";
138+
139+
// A sample function that returns our specific error.
140+
fn find_path() -> Result<Vec<usize>, GraphError> {
141+
Err(GraphError::AlgorithmError(
142+
"Invalid start node for traversal",
143+
))
144+
}
145+
146+
let result = find_path();
147+
assert!(result.is_err());
148+
149+
// Match on the error to confirm its type and contents.
150+
match result {
151+
Err(GraphError::AlgorithmError(msg)) => {
152+
assert_eq!(msg, err_msg);
153+
}
154+
_ => panic!("Expected an AlgorithmError variant."),
155+
}
156+
}

0 commit comments

Comments
 (0)