Skip to main content

oxicuda_sparse/format/
reorder.rs

1//! Sparse matrix reordering algorithms.
2//!
3//! Reordering reduces bandwidth (RCM) or fill-in (AMD) of sparse matrices,
4//! improving the performance of direct and iterative solvers.
5//!
6//! ## Algorithms
7//!
8//! - **Reverse Cuthill-McKee (RCM)**: BFS from a peripheral node, reversed.
9//!   Reduces matrix bandwidth, which improves cache locality for SpMV and
10//!   reduces fill-in for banded preconditioners.
11//!
12//! - **Approximate Minimum Degree (AMD)**: Greedy elimination choosing the
13//!   node with minimum degree. Reduces fill-in for Cholesky/LU factorization.
14//!
15//! ## Usage
16//!
17//! ```rust,no_run
18//! # use oxicuda_sparse::format::reorder::{rcm_ordering, permute_csr};
19//! # use oxicuda_sparse::format::CsrMatrix;
20//! # fn example(matrix: &CsrMatrix<f64>) -> Result<(), oxicuda_sparse::error::SparseError> {
21//! let perm = rcm_ordering(matrix)?;
22//! let reordered = permute_csr(matrix, &perm)?;
23//! # Ok(())
24//! # }
25//! ```
26#![allow(dead_code)]
27
28use std::collections::VecDeque;
29
30use oxicuda_blas::GpuFloat;
31
32use crate::error::{SparseError, SparseResult};
33use crate::format::CsrMatrix;
34
35// ---------------------------------------------------------------------------
36// Reverse Cuthill-McKee (RCM)
37// ---------------------------------------------------------------------------
38
39/// Compute the Reverse Cuthill-McKee (RCM) ordering of a sparse matrix.
40///
41/// The RCM ordering is a BFS-based reordering that reduces the bandwidth of
42/// the matrix. The algorithm:
43/// 1. Find a pseudo-peripheral node (node with near-maximal eccentricity).
44/// 2. Perform BFS from that node, visiting neighbors in order of increasing
45///    degree.
46/// 3. Reverse the resulting ordering.
47///
48/// # Arguments
49///
50/// * `matrix` -- A square CSR matrix.
51///
52/// # Returns
53///
54/// A permutation vector `perm` of length `n` where `perm[new_index] = old_index`.
55///
56/// # Errors
57///
58/// Returns [`SparseError::DimensionMismatch`] if the matrix is not square.
59pub fn rcm_ordering<T: GpuFloat>(matrix: &CsrMatrix<T>) -> SparseResult<Vec<usize>> {
60    if matrix.rows() != matrix.cols() {
61        return Err(SparseError::DimensionMismatch(format!(
62            "RCM requires square matrix, got {}x{}",
63            matrix.rows(),
64            matrix.cols()
65        )));
66    }
67
68    let n = matrix.rows() as usize;
69    if n == 0 {
70        return Ok(Vec::new());
71    }
72
73    let (h_row_ptr, h_col_idx, _) = matrix.to_host()?;
74    rcm_ordering_host(&h_row_ptr, &h_col_idx, n)
75}
76
77/// Host-side RCM ordering computation.
78pub fn rcm_ordering_host(row_ptr: &[i32], col_idx: &[i32], n: usize) -> SparseResult<Vec<usize>> {
79    if n == 0 {
80        return Ok(Vec::new());
81    }
82
83    // Compute degree of each node (excluding self-loops)
84    let degrees: Vec<usize> = (0..n)
85        .map(|i| {
86            let start = row_ptr[i] as usize;
87            let end = row_ptr[i + 1] as usize;
88            col_idx[start..end]
89                .iter()
90                .filter(|&&c| c as usize != i && (c as usize) < n)
91                .count()
92        })
93        .collect();
94
95    // Find a pseudo-peripheral starting node
96    let start_node = find_pseudo_peripheral(row_ptr, col_idx, &degrees, n);
97
98    // BFS with neighbors sorted by degree
99    let mut visited = vec![false; n];
100    let mut order = Vec::with_capacity(n);
101
102    // Handle potentially disconnected graph
103    let starts = [start_node];
104    // We will add other starting nodes if components are disconnected
105
106    let mut queue: VecDeque<usize> = VecDeque::new();
107    let mut component_start = 0;
108    while order.len() < n {
109        let root = if component_start < starts.len() {
110            starts[component_start]
111        } else {
112            // Find next unvisited node
113            match visited.iter().position(|&v| !v) {
114                Some(node) => node,
115                None => break,
116            }
117        };
118        component_start += 1;
119
120        if visited[root] {
121            continue;
122        }
123
124        visited[root] = true;
125        queue.push_back(root);
126
127        while let Some(node) = queue.pop_front() {
128            order.push(node);
129
130            // Collect unvisited neighbors and sort by degree
131            let start = row_ptr[node] as usize;
132            let end = row_ptr[node + 1] as usize;
133            let mut neighbors: Vec<usize> = col_idx[start..end]
134                .iter()
135                .map(|&c| c as usize)
136                .filter(|&c| c < n && c != node && !visited[c])
137                .collect();
138
139            // Deduplicate
140            neighbors.sort_unstable();
141            neighbors.dedup();
142
143            // Sort by degree (ascending) for RCM
144            neighbors.sort_by_key(|&nbr| degrees[nbr]);
145
146            for nbr in neighbors {
147                if !visited[nbr] {
148                    visited[nbr] = true;
149                    queue.push_back(nbr);
150                }
151            }
152        }
153    }
154
155    // Reverse the ordering (Reverse Cuthill-McKee)
156    order.reverse();
157
158    Ok(order)
159}
160
161/// Find a pseudo-peripheral node using the Gibbs-Poole-Stockmeyer approach.
162///
163/// Starting from the node with minimum degree, performs two BFS passes to find
164/// a node with near-maximal eccentricity.
165fn find_pseudo_peripheral(row_ptr: &[i32], col_idx: &[i32], degrees: &[usize], n: usize) -> usize {
166    // Start from a minimum-degree node
167    let mut current = 0;
168    let mut min_deg = degrees[0];
169    for (i, &d) in degrees.iter().enumerate().skip(1) {
170        if d < min_deg {
171            min_deg = d;
172            current = i;
173        }
174    }
175
176    // Refine: do a few BFS iterations to move toward a peripheral node
177    for _ in 0..5 {
178        let (last_level, _) = bfs_levels(row_ptr, col_idx, n, current);
179        if last_level.is_empty() {
180            break;
181        }
182        // Pick the node with minimum degree from the last BFS level
183        let mut best = last_level[0];
184        let mut best_deg = degrees[best];
185        for &node in &last_level[1..] {
186            if degrees[node] < best_deg {
187                best_deg = degrees[node];
188                best = node;
189            }
190        }
191        if best == current {
192            break;
193        }
194        current = best;
195    }
196
197    current
198}
199
200/// Performs BFS from `root` and returns the last level's nodes and the number of levels.
201fn bfs_levels(row_ptr: &[i32], col_idx: &[i32], n: usize, root: usize) -> (Vec<usize>, usize) {
202    let mut visited = vec![false; n];
203    let mut current_level = Vec::new();
204    let mut next_level = Vec::new();
205
206    visited[root] = true;
207    current_level.push(root);
208    let mut num_levels = 1;
209
210    loop {
211        for &node in &current_level {
212            let start = row_ptr[node] as usize;
213            let end = row_ptr[node + 1] as usize;
214            for &c in &col_idx[start..end] {
215                let nbr = c as usize;
216                if nbr < n && !visited[nbr] {
217                    visited[nbr] = true;
218                    next_level.push(nbr);
219                }
220            }
221        }
222
223        if next_level.is_empty() {
224            break;
225        }
226
227        num_levels += 1;
228        current_level.clear();
229        std::mem::swap(&mut current_level, &mut next_level);
230    }
231
232    (current_level, num_levels)
233}
234
235// ---------------------------------------------------------------------------
236// Approximate Minimum Degree (AMD)
237// ---------------------------------------------------------------------------
238
239/// Compute the Approximate Minimum Degree (AMD) ordering of a sparse matrix.
240///
241/// AMD is a greedy fill-reducing ordering for Cholesky and LU factorization.
242/// At each step, the node with minimum approximate degree is eliminated.
243///
244/// # Arguments
245///
246/// * `matrix` -- A square CSR matrix.
247///
248/// # Returns
249///
250/// A permutation vector `perm` of length `n` where `perm[i]` is the `i`-th
251/// node to be eliminated.
252///
253/// # Errors
254///
255/// Returns [`SparseError::DimensionMismatch`] if the matrix is not square.
256pub fn amd_ordering<T: GpuFloat>(matrix: &CsrMatrix<T>) -> SparseResult<Vec<usize>> {
257    if matrix.rows() != matrix.cols() {
258        return Err(SparseError::DimensionMismatch(format!(
259            "AMD requires square matrix, got {}x{}",
260            matrix.rows(),
261            matrix.cols()
262        )));
263    }
264
265    let n = matrix.rows() as usize;
266    if n == 0 {
267        return Ok(Vec::new());
268    }
269
270    let (h_row_ptr, h_col_idx, _) = matrix.to_host()?;
271    amd_ordering_host(&h_row_ptr, &h_col_idx, n)
272}
273
274/// Host-side AMD ordering computation.
275pub fn amd_ordering_host(row_ptr: &[i32], col_idx: &[i32], n: usize) -> SparseResult<Vec<usize>> {
276    if n == 0 {
277        return Ok(Vec::new());
278    }
279
280    // Build adjacency lists (excluding self-loops, symmetric)
281    let mut adj: Vec<Vec<usize>> = Vec::with_capacity(n);
282    for i in 0..n {
283        let start = row_ptr[i] as usize;
284        let end = row_ptr[i + 1] as usize;
285        let mut neighbors: Vec<usize> = col_idx[start..end]
286            .iter()
287            .map(|&c| c as usize)
288            .filter(|&c| c != i && c < n)
289            .collect();
290        neighbors.sort_unstable();
291        neighbors.dedup();
292        adj.push(neighbors);
293    }
294
295    let mut eliminated = vec![false; n];
296    let mut degree: Vec<usize> = adj.iter().map(|a| a.len()).collect();
297    let mut perm = Vec::with_capacity(n);
298
299    for _ in 0..n {
300        // Find node with minimum degree among non-eliminated nodes
301        let mut min_node = None;
302        let mut min_deg = usize::MAX;
303        for (i, (&d, &elim)) in degree.iter().zip(eliminated.iter()).enumerate() {
304            if !elim && d < min_deg {
305                min_deg = d;
306                min_node = Some(i);
307            }
308        }
309
310        let node = match min_node {
311            Some(v) => v,
312            None => break,
313        };
314
315        perm.push(node);
316        eliminated[node] = true;
317
318        // Collect non-eliminated neighbors
319        let neighbors: Vec<usize> = adj[node]
320            .iter()
321            .copied()
322            .filter(|&nbr| !eliminated[nbr])
323            .collect();
324
325        // Mass elimination: connect all neighbors of `node` to each other
326        // and update degrees
327        for &nbr in &neighbors {
328            // Remove `node` from nbr's adjacency
329            adj[nbr].retain(|&x| x != node);
330
331            // Add edges to all other neighbors of `node`
332            for &other in &neighbors {
333                if other != nbr && !adj[nbr].contains(&other) {
334                    adj[nbr].push(other);
335                }
336            }
337
338            // Update degree
339            degree[nbr] = adj[nbr].iter().filter(|&&x| !eliminated[x]).count();
340        }
341
342        degree[node] = 0;
343    }
344
345    Ok(perm)
346}
347
348// ---------------------------------------------------------------------------
349// Permutation utilities
350// ---------------------------------------------------------------------------
351
352/// Apply a permutation to a CSR matrix: `P * A * P^T`.
353///
354/// Given a permutation `perm` where `perm[new_index] = old_index`,
355/// computes the reordered matrix.
356///
357/// # Arguments
358///
359/// * `matrix` -- CSR matrix to permute.
360/// * `perm` -- Permutation vector of length `n`.
361///
362/// # Errors
363///
364/// Returns [`SparseError::InvalidArgument`] if `perm` length does not match
365/// the matrix dimension or contains invalid indices.
366pub fn permute_csr<T: GpuFloat>(
367    matrix: &CsrMatrix<T>,
368    perm: &[usize],
369) -> SparseResult<CsrMatrix<T>> {
370    let n = matrix.rows() as usize;
371    if perm.len() != n {
372        return Err(SparseError::InvalidArgument(format!(
373            "permutation length ({}) must match matrix dimension ({})",
374            perm.len(),
375            n
376        )));
377    }
378
379    // Validate permutation
380    let inv_perm = inverse_permutation(perm);
381    if inv_perm.len() != n {
382        return Err(SparseError::InvalidArgument(
383            "invalid permutation: not a valid bijection".to_string(),
384        ));
385    }
386
387    let (h_row_ptr, h_col_idx, h_values) = matrix.to_host()?;
388
389    // Build reordered matrix: new_row i corresponds to old_row perm[i]
390    let mut new_row_ptr = vec![0i32; n + 1];
391    let mut new_entries: Vec<Vec<(i32, T)>> = Vec::with_capacity(n);
392
393    for new_row in 0..n {
394        let old_row = perm[new_row];
395        if old_row >= n {
396            return Err(SparseError::InvalidArgument(format!(
397                "permutation index {} out of bounds (n={})",
398                old_row, n
399            )));
400        }
401
402        let start = h_row_ptr[old_row] as usize;
403        let end = h_row_ptr[old_row + 1] as usize;
404
405        let mut entries: Vec<(i32, T)> = Vec::with_capacity(end - start);
406        for k in start..end {
407            let old_col = h_col_idx[k] as usize;
408            if old_col >= n {
409                return Err(SparseError::InvalidArgument(format!(
410                    "column index {} out of bounds (n={})",
411                    old_col, n
412                )));
413            }
414            let new_col = inv_perm[old_col];
415            entries.push((new_col as i32, h_values[k]));
416        }
417
418        // Sort by new column index
419        entries.sort_by_key(|&(c, _)| c);
420
421        new_row_ptr[new_row + 1] = new_row_ptr[new_row] + entries.len() as i32;
422        new_entries.push(entries);
423    }
424
425    let nnz = new_row_ptr[n] as usize;
426    if nnz == 0 {
427        return Err(SparseError::ZeroNnz);
428    }
429
430    let mut new_col_idx = Vec::with_capacity(nnz);
431    let mut new_values = Vec::with_capacity(nnz);
432    for entries in &new_entries {
433        for &(c, v) in entries {
434            new_col_idx.push(c);
435            new_values.push(v);
436        }
437    }
438
439    CsrMatrix::from_host(
440        matrix.rows(),
441        matrix.cols(),
442        &new_row_ptr,
443        &new_col_idx,
444        &new_values,
445    )
446}
447
448/// Compute the inverse permutation.
449///
450/// Given `perm[new] = old`, returns `inv[old] = new`.
451pub fn inverse_permutation(perm: &[usize]) -> Vec<usize> {
452    let n = perm.len();
453    let mut inv = vec![0usize; n];
454    for (new_idx, &old_idx) in perm.iter().enumerate() {
455        if old_idx < n {
456            inv[old_idx] = new_idx;
457        }
458    }
459    inv
460}
461
462/// Compute the bandwidth of a matrix given its CSR structure.
463///
464/// Bandwidth = max |i - j| over all nonzero entries (i, j).
465pub fn bandwidth(row_ptr: &[i32], col_idx: &[i32], n: usize) -> usize {
466    let mut bw = 0usize;
467    for i in 0..n {
468        let start = row_ptr[i] as usize;
469        let end = row_ptr[i + 1] as usize;
470        for &c in &col_idx[start..end] {
471            let j = c as usize;
472            let diff = i.abs_diff(j);
473            if diff > bw {
474                bw = diff;
475            }
476        }
477    }
478    bw
479}
480
481// ---------------------------------------------------------------------------
482// Tests
483// ---------------------------------------------------------------------------
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    #[test]
490    fn rcm_identity() {
491        // Identity: any ordering is valid, bandwidth = 0
492        let row_ptr = vec![0, 1, 2, 3];
493        let col_idx = vec![0, 1, 2];
494        let perm = rcm_ordering_host(&row_ptr, &col_idx, 3);
495        assert!(perm.is_ok());
496        let perm = perm.expect("test: should succeed");
497        assert_eq!(perm.len(), 3);
498        // Should be a valid permutation
499        let mut sorted = perm.clone();
500        sorted.sort_unstable();
501        assert_eq!(sorted, vec![0, 1, 2]);
502    }
503
504    #[test]
505    fn rcm_tridiagonal() {
506        // Tridiagonal 5x5
507        let row_ptr = vec![0, 2, 5, 8, 11, 13];
508        let col_idx = vec![0, 1, 0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4];
509        let perm = rcm_ordering_host(&row_ptr, &col_idx, 5);
510        assert!(perm.is_ok());
511        let perm = perm.expect("test: should succeed");
512        assert_eq!(perm.len(), 5);
513        // Valid permutation
514        let mut sorted = perm.clone();
515        sorted.sort_unstable();
516        assert_eq!(sorted, vec![0, 1, 2, 3, 4]);
517    }
518
519    #[test]
520    fn rcm_reduces_bandwidth() {
521        // Arrow matrix: row 0 connects to all, others only to 0 and self
522        // Original bandwidth is n-1.
523        // [1 1 1 1 1]
524        // [1 1 0 0 0]
525        // [1 0 1 0 0]
526        // [1 0 0 1 0]
527        // [1 0 0 0 1]
528        let row_ptr = vec![0, 5, 7, 9, 11, 13];
529        let col_idx = vec![0, 1, 2, 3, 4, 0, 1, 0, 2, 0, 3, 0, 4];
530        let n = 5;
531        let orig_bw = bandwidth(&row_ptr, &col_idx, n);
532        assert_eq!(orig_bw, 4);
533
534        let perm = rcm_ordering_host(&row_ptr, &col_idx, n);
535        assert!(perm.is_ok());
536        let perm = perm.expect("test: should succeed");
537
538        // Apply permutation to check new bandwidth
539        let inv = inverse_permutation(&perm);
540        let mut new_bw = 0;
541        for (i, &old_row) in perm.iter().enumerate().take(n) {
542            let start = row_ptr[old_row] as usize;
543            let end = row_ptr[old_row + 1] as usize;
544            for &c in &col_idx[start..end] {
545                let new_col = inv[c as usize];
546                let diff = i.abs_diff(new_col);
547                if diff > new_bw {
548                    new_bw = diff;
549                }
550            }
551        }
552        // RCM should not increase bandwidth
553        assert!(new_bw <= orig_bw);
554    }
555
556    #[test]
557    fn amd_identity() {
558        let row_ptr = vec![0, 1, 2, 3];
559        let col_idx = vec![0, 1, 2];
560        let perm = amd_ordering_host(&row_ptr, &col_idx, 3);
561        assert!(perm.is_ok());
562        let perm = perm.expect("test: should succeed");
563        assert_eq!(perm.len(), 3);
564        let mut sorted = perm.clone();
565        sorted.sort_unstable();
566        assert_eq!(sorted, vec![0, 1, 2]);
567    }
568
569    #[test]
570    fn amd_tridiagonal() {
571        let row_ptr = vec![0, 2, 5, 8, 10];
572        let col_idx = vec![0, 1, 0, 1, 2, 1, 2, 3, 2, 3];
573        let perm = amd_ordering_host(&row_ptr, &col_idx, 4);
574        assert!(perm.is_ok());
575        let perm = perm.expect("test: should succeed");
576        assert_eq!(perm.len(), 4);
577        // Valid permutation: all nodes appear exactly once
578        let mut sorted = perm.clone();
579        sorted.sort_unstable();
580        assert_eq!(sorted, vec![0, 1, 2, 3]);
581    }
582
583    #[test]
584    fn inverse_permutation_roundtrip() {
585        let perm = vec![3, 1, 0, 2];
586        let inv = inverse_permutation(&perm);
587        assert_eq!(inv, vec![2, 1, 3, 0]);
588
589        // inv(inv(perm)) == perm
590        let inv_inv = inverse_permutation(&inv);
591        assert_eq!(inv_inv, perm);
592    }
593
594    #[test]
595    fn bandwidth_calculation() {
596        // Tridiagonal: bandwidth = 1
597        let row_ptr = vec![0, 2, 5, 7];
598        let col_idx = vec![0, 1, 0, 1, 2, 1, 2];
599        assert_eq!(bandwidth(&row_ptr, &col_idx, 3), 1);
600
601        // Diagonal: bandwidth = 0
602        let row_ptr = vec![0, 1, 2, 3];
603        let col_idx = vec![0, 1, 2];
604        assert_eq!(bandwidth(&row_ptr, &col_idx, 3), 0);
605    }
606
607    #[test]
608    fn rcm_empty() {
609        let perm = rcm_ordering_host(&[0], &[], 0);
610        assert!(perm.is_ok());
611        assert!(perm.expect("test: should succeed").is_empty());
612    }
613
614    #[test]
615    fn amd_empty() {
616        let perm = amd_ordering_host(&[0], &[], 0);
617        assert!(perm.is_ok());
618        assert!(perm.expect("test: should succeed").is_empty());
619    }
620}