Skip to main content

scirs2_sparse/csgraph/
mod.rs

1//! Compressed sparse graph algorithms module
2//!
3//! This module provides graph algorithms optimized for sparse matrices,
4//! similar to SciPy's `sparse.csgraph` module.
5//!
6//! ## Features
7//!
8//! * Shortest path algorithms (Dijkstra, Bellman-Ford)
9//! * Connected components analysis  
10//! * Graph traversal utilities (BFS, DFS)
11//! * Laplacian matrix computation
12//! * Minimum spanning tree algorithms
13//! * Graph connectivity testing
14//!
15//! ## Examples
16//!
17//! ### Shortest Path
18//!
19//! ```
20//! use scirs2_sparse::csgraph::shortest_path;
21//! use scirs2_sparse::csr_array::CsrArray;
22//!
23//! // Create a graph as a sparse matrix
24//! let rows = vec![0, 0, 1, 2];
25//! let cols = vec![1, 2, 2, 0];
26//! let data = vec![1.0, 4.0, 2.0, 3.0];
27//! let graph = CsrArray::from_triplets(&rows, &cols, &data, (3, 3), false).expect("Operation failed");
28//!
29//! // Find shortest paths from vertex 0
30//! let distances = shortest_path(&graph, Some(0), None, "dijkstra", true, false).expect("Operation failed");
31//! ```
32//!
33//! ### Connected Components
34//!
35//! ```
36//! use scirs2_sparse::csgraph::connected_components;
37//! use scirs2_sparse::csr_array::CsrArray;
38//!
39//! // Create a graph
40//! let rows = vec![0, 1, 2, 3];
41//! let cols = vec![1, 0, 3, 2];
42//! let data = vec![1.0, 1.0, 1.0, 1.0];
43//! let graph = CsrArray::from_triplets(&rows, &cols, &data, (4, 4), false).expect("Operation failed");
44//!
45//! // Find connected components
46//! let (n_components, labels) = connected_components(&graph, false, "weak", true).expect("Operation failed");
47//! ```
48
49use crate::error::{SparseError, SparseResult};
50use crate::sparray::SparseArray;
51use scirs2_core::numeric::{Float, SparseElement};
52use std::cmp::Ordering;
53use std::fmt::Debug;
54
55pub mod connected_components;
56pub mod laplacian;
57pub mod minimum_spanning_tree;
58pub mod shortest_path;
59pub mod traversal;
60
61// v0.2.0 Advanced graph algorithms
62pub mod centrality;
63pub mod community_detection;
64pub mod max_flow;
65
66pub use centrality::*;
67pub use community_detection::*;
68pub use connected_components::*;
69pub use laplacian::*;
70pub use max_flow::*;
71pub use minimum_spanning_tree::*;
72pub use shortest_path::*;
73pub use traversal::*;
74
75/// Graph representation modes
76#[derive(Debug, Clone, Copy, PartialEq)]
77pub enum GraphMode {
78    /// Treat the matrix as a directed graph
79    Directed,
80    /// Treat the matrix as an undirected graph
81    Undirected,
82}
83
84/// Priority queue element for graph algorithms
85#[derive(Debug, Clone)]
86struct PriorityQueueNode<T>
87where
88    T: Float + PartialOrd,
89{
90    distance: T,
91    node: usize,
92}
93
94impl<T> PartialEq for PriorityQueueNode<T>
95where
96    T: Float + PartialOrd,
97{
98    fn eq(&self, other: &Self) -> bool {
99        self.distance == other.distance && self.node == other.node
100    }
101}
102
103impl<T> Eq for PriorityQueueNode<T> where T: Float + PartialOrd {}
104
105impl<T> PartialOrd for PriorityQueueNode<T>
106where
107    T: Float + PartialOrd,
108{
109    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
110        Some(self.cmp(other))
111    }
112}
113
114impl<T> Ord for PriorityQueueNode<T>
115where
116    T: Float + PartialOrd,
117{
118    fn cmp(&self, other: &Self) -> Ordering {
119        // Reverse the ordering for min-heap behavior
120        other
121            .distance
122            .partial_cmp(&self.distance)
123            .unwrap_or(Ordering::Equal)
124    }
125}
126
127/// Check if a sparse matrix represents a valid graph
128///
129/// # Arguments
130///
131/// * `matrix` - The sparse matrix to check
132/// * `directed` - Whether the graph is directed
133///
134/// # Returns
135///
136/// Result indicating if the matrix is a valid graph
137#[allow(dead_code)]
138pub fn validate_graph<T, S>(matrix: &S, directed: bool) -> SparseResult<()>
139where
140    T: Float + SparseElement + Debug + Copy + 'static,
141    S: SparseArray<T>,
142{
143    let (rows, cols) = matrix.shape();
144
145    // Graph matrices must be square
146    if rows != cols {
147        return Err(SparseError::ValueError(
148            "Graph _matrix must be square".to_string(),
149        ));
150    }
151
152    // Check for negative weights (not allowed in some algorithms)
153    let (row_indices, col_indices, values) = matrix.find();
154    for &value in values.iter() {
155        if value < T::sparse_zero() {
156            return Err(SparseError::ValueError(
157                "Negative edge weights not supported".to_string(),
158            ));
159        }
160    }
161
162    // For undirected graphs, check symmetry
163    if !directed {
164        for (i, (&row, &col)) in row_indices.iter().zip(col_indices.iter()).enumerate() {
165            if row != col {
166                let weight = values[i];
167                let reverse_weight = matrix.get(col, row);
168
169                if (weight - reverse_weight).abs() > T::from(1e-10).expect("Operation failed") {
170                    return Err(SparseError::ValueError(
171                        "Undirected graph _matrix must be symmetric".to_string(),
172                    ));
173                }
174            }
175        }
176    }
177
178    Ok(())
179}
180
181/// Convert a sparse matrix to adjacency list representation
182///
183/// # Arguments
184///
185/// * `matrix` - The sparse matrix
186/// * `directed` - Whether the graph is directed
187///
188/// # Returns
189///
190/// Adjacency list as a vector of vectors of (neighbor, weight) pairs
191#[allow(dead_code)]
192pub fn to_adjacency_list<T, S>(matrix: &S, directed: bool) -> SparseResult<Vec<Vec<(usize, T)>>>
193where
194    T: Float + SparseElement + Debug + Copy + 'static,
195    S: SparseArray<T>,
196{
197    let (n_, _) = matrix.shape();
198    let mut adj_list = vec![Vec::new(); n_];
199
200    let (row_indices, col_indices, values) = matrix.find();
201
202    for (i, (&row, &col)) in row_indices.iter().zip(col_indices.iter()).enumerate() {
203        let weight = values[i];
204
205        if !SparseElement::is_zero(&weight) {
206            adj_list[row].push((col, weight));
207
208            // For undirected graphs, add the reverse edge only if it doesn't already exist
209            if !directed && row != col {
210                // Check if the reverse edge already exists in the _matrix
211                let reverse_exists = row_indices
212                    .iter()
213                    .zip(col_indices.iter())
214                    .any(|(r, c)| *r == col && *c == row);
215
216                if !reverse_exists {
217                    adj_list[col].push((row, weight));
218                }
219            }
220        }
221    }
222
223    Ok(adj_list)
224}
225
226/// Get the number of vertices in a graph matrix
227#[allow(dead_code)]
228pub fn num_vertices<T, S>(matrix: &S) -> usize
229where
230    T: Float + SparseElement + Debug + Copy + 'static,
231    S: SparseArray<T>,
232{
233    matrix.shape().0
234}
235
236/// Get the number of edges in a graph matrix
237#[allow(dead_code)]
238pub fn num_edges<T, S>(matrix: &S, directed: bool) -> SparseResult<usize>
239where
240    T: Float + SparseElement + Debug + Copy + 'static,
241    S: SparseArray<T>,
242{
243    let nnz = matrix.nnz();
244
245    if directed {
246        Ok(nnz)
247    } else {
248        // For undirected graphs, count diagonal elements once and off-diagonal elements half
249        let (row_indices, col_indices_, _) = matrix.find();
250        let mut diagonal_count = 0;
251        let mut off_diagonal_count = 0;
252
253        for (&row, &col) in row_indices.iter().zip(col_indices_.iter()) {
254            if row == col {
255                diagonal_count += 1;
256            } else {
257                off_diagonal_count += 1;
258            }
259        }
260
261        // Off-diagonal edges are counted twice in the _matrix (i,j) and (j,i)
262        Ok(diagonal_count + off_diagonal_count / 2)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::csr_array::CsrArray;
270
271    fn create_test_graph() -> CsrArray<f64> {
272        // Create a simple 4-vertex graph:
273        //   0 -- 1
274        //   |    |
275        //   2 -- 3
276        let rows = vec![0, 0, 1, 1, 2, 2, 3, 3];
277        let cols = vec![1, 2, 0, 3, 0, 3, 1, 2];
278        let data = vec![1.0, 2.0, 1.0, 3.0, 2.0, 1.0, 3.0, 1.0];
279
280        CsrArray::from_triplets(&rows, &cols, &data, (4, 4), false).expect("Operation failed")
281    }
282
283    #[test]
284    fn test_validate_graph_symmetric() {
285        let graph = create_test_graph();
286
287        // Should be valid as undirected graph
288        assert!(validate_graph(&graph, false).is_ok());
289
290        // Should also be valid as directed graph
291        assert!(validate_graph(&graph, true).is_ok());
292    }
293
294    #[test]
295    fn test_validate_graph_asymmetric() {
296        // Create an asymmetric graph
297        let rows = vec![0, 1];
298        let cols = vec![1, 0];
299        let data = vec![1.0, 2.0]; // Different weights
300
301        let graph =
302            CsrArray::from_triplets(&rows, &cols, &data, (2, 2), false).expect("Operation failed");
303
304        // Should be valid as directed graph
305        assert!(validate_graph(&graph, true).is_ok());
306
307        // Should fail as undirected graph due to asymmetry
308        assert!(validate_graph(&graph, false).is_err());
309    }
310
311    #[test]
312    fn test_validate_graph_negative_weights() {
313        let rows = vec![0, 1];
314        let cols = vec![1, 0];
315        let data = vec![-1.0, 1.0]; // Negative weight
316
317        let graph =
318            CsrArray::from_triplets(&rows, &cols, &data, (2, 2), false).expect("Operation failed");
319
320        // Should fail due to negative weights
321        assert!(validate_graph(&graph, true).is_err());
322        assert!(validate_graph(&graph, false).is_err());
323    }
324
325    #[test]
326    fn test_to_adjacency_list() {
327        let graph = create_test_graph();
328        let adj_list = to_adjacency_list(&graph, false).expect("Operation failed");
329
330        assert_eq!(adj_list.len(), 4);
331
332        // Vertex 0 should be connected to 1 and 2
333        assert_eq!(adj_list[0].len(), 2);
334        assert!(adj_list[0].contains(&(1, 1.0)));
335        assert!(adj_list[0].contains(&(2, 2.0)));
336
337        // Vertex 1 should be connected to 0 and 3
338        assert_eq!(adj_list[1].len(), 2);
339        assert!(adj_list[1].contains(&(0, 1.0)));
340        assert!(adj_list[1].contains(&(3, 3.0)));
341    }
342
343    #[test]
344    fn test_num_vertices() {
345        let graph = create_test_graph();
346        assert_eq!(num_vertices(&graph), 4);
347    }
348
349    #[test]
350    fn test_num_edges() {
351        let graph = create_test_graph();
352
353        // Directed: all 8 edges
354        assert_eq!(num_edges(&graph, true).expect("Operation failed"), 8);
355
356        // Undirected: 4 unique edges
357        assert_eq!(num_edges(&graph, false).expect("Operation failed"), 4);
358    }
359}